一、为什么选择uni-app?

在移动互联网时代,一个产品往往需要同时覆盖Android、iOS、H5、以及微信/支付宝/百度等多个小程序平台。传统开发模式下,每个平台都需要独立开发团队,成本高、周期长、维护难。

uni-app 是DCloud公司基于Vue.js开发的跨端框架,一套代码可以编译发布到 iOS、Android、H5、以及各类小程序(微信/支付宝/百度/字节/QQ/快手),真正做到 “Write once, run anywhere”

核心优势

维度 传统多端开发 uni-app
团队配置 每个平台2-3人 1个前端团队
代码复用率 低于20% 80%以上
开发周期 3-6个月 1-2个月
维护成本 多套代码并行 一套代码统一维护

技术原理

uni-app 使用 Vue.js 语法规范 + Weex 渲染引擎 + 小程序组件化 的混合架构:

  1. 开发者编写 .vue 文件

  2. 编译时根据 pages.json 和 manifest.json 配置

  3. 按目标平台输出对应的代码(H5、小程序、原生应用)


二、环境搭建与项目创建

2.1 安装脚手架

bash

复制

下载

# 安装vue-cli
npm install -g @vue/cli

# 创建uni-app项目(选择默认模板或TypeScript模板)
vue create -p dcloudio/uni-preset-vue my-project

# 进入项目目录
cd my-project

# 安装依赖
npm install

2.2 运行到不同平台

bash

复制

下载

# 运行到H5(浏览器自动打开)
npm run dev:h5

# 运行到微信小程序(需先打开微信开发者工具并导入unpackage目录)
npm run dev:mp-weixin

# 运行到支付宝小程序
npm run dev:mp-alipay

# 运行到百度小程序
npm run dev:mp-baidu

2.3 使用HBuilderX(推荐)

HBuilderX是uni-app官方IDE,提供可视化创建、一键真机调试、云端打包等功能:

  1. 下载HBuilderX官网版本

  2. 文件 → 新建 → 项目 → uni-app

  3. 运行 → 运行到浏览器/手机/小程序模拟器


三、项目结构与核心配置

3.1 目录结构详解

my-project/
├── pages/                   # 页面目录(每个.vue文件自动生成对应页面)
│   ├── index/
│   │   └── index.vue        # 首页
│   └── user/
│       └── user.vue         # 用户中心页
├── components/              # 公共组件目录
├── static/                  # 静态资源目录(图片/字体等,不参与编译)
├── unpackage/               # 构建输出目录(默认git忽略)
├── App.vue                  # 应用主入口(生命周期/全局样式)
├── pages.json               # 全局路由配置(路由/TabBar/窗口样式)
├── manifest.json            # 应用配置(AppID/权限/平台特性)
├── uni.scss                 # 全局样式变量(所有页面可引用) 
└── main.js                  # 入口文件(Vue挂载/插件引入)
 

3.2 pages.json 完整配置

{
  "pages": [
    {
      "path": "pages/index/index",
      "style": {
        "navigationBarTitleText": "首页",
        "navigationBarBackgroundColor": "#007AFF",
        "enablePullDownRefresh": true
      }
    },
    {
      "path": "pages/detail/detail",
      "style": {
        "navigationBarTitleText": "详情页"
      }
    }
  ],
  "globalStyle": {
    "navigationBarTextStyle": "white",
    "navigationBarBackgroundColor": "#007AFF",
    "backgroundColor": "#F8F8F8"
  },
  "tabBar": {
    "color": "#999999",
    "selectedColor": "#007AFF",
    "backgroundColor": "#FFFFFF",
    "list": [
      {
        "pagePath": "pages/index/index",
        "text": "首页",
        "iconPath": "static/home.png",
        "selectedIconPath": "static/home-active.png"
      },
      {
        "pagePath": "pages/user/user",
        "text": "我的",
        "iconPath": "static/user.png",
        "selectedIconPath": "static/user-active.png"
      }
    ]
  },
  "condition": {
    "current": 0,
    "list": [
      {
        "name": "详情页调试",
        "path": "pages/detail/detail",
        "query": "id=1001"
      }
    ]
  }
}
 


四、完整实战代码

4.1 新闻列表页面(完整功能)

<template>
  <view class="container">
    <!-- 轮播图区域 -->
    <swiper 
      :indicator-dots="true" 
      :autoplay="true" 
      :interval="3000" 
      class="banner"
    >
      <swiper-item v-for="(item, idx) in banners" :key="idx">
        <image 
          :src="item.image" 
          mode="aspectFill" 
          class="banner-img" 
        />
      </swiper-item>
    </swiper>

    <!-- 分类导航 -->
    <scroll-view scroll-x class="category-scroll">
      <view 
        v-for="(cat, idx) in categories" 
        :key="idx"
        :class="['category-item', { active: currentCategory === cat.id }]"
        @click="switchCategory(cat.id)"
      >
        {{ cat.name }}
      </view>
    </scroll-view>

    <!-- 新闻列表 -->
    <view class="news-list">
      <view 
        v-for="item in newsList" 
        :key="item.id" 
        class="news-item"
        @click="goDetail(item.id)"
      >
        <image 
          :src="item.cover" 
          lazy-load 
          mode="aspectFill" 
          class="news-cover" 
        />
        <view class="news-info">
          <text class="title">{{ item.title }}</text>
          <view class="meta">
            <text>{{ item.author }}</text>
            <text>{{ formatTime(item.createTime) }}</text>
            <text>{{ item.viewCount }}阅读</text>
          </view>
        </view>
      </view>
    </view>

    <!-- 加载状态 -->
    <view class="load-more" v-if="isLoading">
      <uni-load-more :status="loadingStatus" />
    </view>
    
    <!-- 空状态提示 -->
    <view class="empty" v-if="!isLoading && !newsList.length">
      <image src="/static/empty.png" mode="aspectFit" />
      <text>暂无数据</text>
    </view>
  </view>
</template>

<script>
import { formatTime } from '@/utils/date'
import { getNewsList, getBanners } from '@/api/news'

export default {
  data() {
    return {
      banners: [],
      categories: [
        { id: 1, name: '推荐' },
        { id: 2, name: '最新' },
        { id: 3, name: '热点' }
      ],
      currentCategory: 1,
      newsList: [],
      pageNum: 1,
      pageSize: 15,
      hasMore: true,
      isLoading: false,
      loadingStatus: 'loading'
    }
  },
  onLoad() {
    this.init()
  },
  onReachBottom() {
    if (this.hasMore && !this.isLoading) {
      this.pageNum++
      this.fetchNews()
    }
  },
  onPullDownRefresh() {
    this.reset()
    this.init().then(() => {
      uni.stopPullDownRefresh()
    })
  },
  methods: {
    formatTime,
    
    async init() {
      uni.showLoading({ title: '加载中' })
      await Promise.all([this.fetchBanners(), this.fetchNews()])
      uni.hideLoading()
    },
    
    reset() {
      this.pageNum = 1
      this.newsList = []
      this.hasMore = true
    },
    
    async fetchBanners() {
      try {
        const res = await getBanners()
        this.banners = res.data
      } catch (err) {
        console.error('获取轮播图失败', err)
      }
    },
    
    async fetchNews() {
      this.isLoading = true
      this.loadingStatus = 'loading'
      
      try {
        const res = await getNewsList({
          categoryId: this.currentCategory,
          pageNum: this.pageNum,
          pageSize: this.pageSize
        })
        
        const { list, total } = res.data
        this.newsList = this.pageNum === 1 ? list : [...this.newsList, ...list]
        this.hasMore = this.newsList.length < total
        this.loadingStatus = this.hasMore ? 'more' : 'noMore'
      } catch (err) {
        uni.showToast({ title: '加载失败', icon: 'none' })
        this.loadingStatus = 'error'
      } finally {
        this.isLoading = false
      }
    },
    
    switchCategory(categoryId) {
      if (this.currentCategory === categoryId) return
      this.currentCategory = categoryId
      this.reset()
      this.fetchNews()
    },
    
    goDetail(id) {
      uni.navigateTo({
        url: `/pages/detail/detail?id=${id}`
      })
    }
  }
}
</script>

<style lang="scss" scoped>
.container {
  background: #f5f5f5;
  min-height: 100vh;
}

.banner {
  height: 360rpx;
  
  &-img {
    width: 100%;
    height: 100%;
  }
}

.category-scroll {
  white-space: nowrap;
  background: white;
  padding: 20rpx 0;
}

.category-item {
  display: inline-block;
  padding: 10rpx 30rpx;
  margin: 0 10rpx;
  font-size: 28rpx;
  border-radius: 40rpx;
  background: #f0f0f0;
  
  &.active {
    background: #007AFF;
    color: white;
  }
}

.news-item {
  display: flex;
  margin: 20rpx;
  padding: 24rpx;
  background: white;
  border-radius: 16rpx;
  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
}

.news-cover {
  width: 200rpx;
  height: 140rpx;
  border-radius: 8rpx;
  margin-right: 20rpx;
}

.news-info {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}

.title {
  font-size: 32rpx;
  font-weight: bold;
  color: #333;
  line-height: 1.4;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.meta {
  display: flex;
  gap: 20rpx;
  font-size: 24rpx;
  color: #999;
}
</style>
 

4.2 条件编译(平台差异化)

<template>
  <view>
    <!-- H5平台特有内容 -->
    <!-- #ifdef H5 -->
    <view class="h5-only">这是H5特有的内容</view>
    <!-- #endif -->
    
    <!-- 微信小程序特有内容 -->
    <!-- #ifdef MP-WEIXIN -->
    <button open-type="getUserInfo" @getuserinfo="onGetUserInfo">微信授权登录</button>
    <!-- #endif -->
    
    <!-- App平台特有内容 -->
    <!-- #ifdef APP-PLUS -->
    <button @click="appLogin">APP一键登录</button>
    <!-- #endif -->
  </view>
</template>

<script>
export default {
  methods: {
    // 微信小程序方法
    // #ifdef MP-WEIXIN
    onGetUserInfo(e) {
      if (e.detail.userInfo) {
        uni.setStorageSync('userInfo', e.detail.userInfo)
      }
    },
    // #endif
    
    // App方法
    // #ifdef APP-PLUS
    appLogin() {
      plus.oauth.getServices(services => {
        // 苹果/安卓原生登录处理
      })
    }
    // #endif
  }
}
</script>

<style>
/* H5平台特有样式 */
/* #ifdef H5 */
.h5-only {
  background: yellow;
}
/* #endif */

/* 微信小程序特有样式 */
/* #ifdef MP-WEIXIN */
/* 微信小程序样式 */
/* #endif */
</style>
 

4.3 封装请求拦截器

// utils/request.js
const BASE_URL = 'https://api.example.com';

const request = (options) => {
  return new Promise((resolve, reject) => {
    const token = uni.getStorageSync('token');
    
    uni.request({
      url: BASE_URL + options.url,
      method: options.method || 'GET',
      data: options.data || {},
      header: {
        'Content-Type': 'application/json',
        'Authorization': token ? `Bearer ${token}` : ''
      },
      success: (res) => {
        if (res.statusCode === 200) {
          if (res.data.code === 401) {
            // 未授权,跳转登录页
            uni.reLaunch({ url: '/pages/login/login' });
            reject(res.data);
          } else {
            resolve(res.data);
          }
        } else {
          reject(res);
        }
      },
      fail: reject
    });
  });
};

// 使用示例
// const res = await request({ 
//   url: '/news/list', 
//   data: { page: 1 } 
// });
 


五、发布打包流程

5.1 H5发布

npm run build:h5
# 编译结果输出至 unpackage/dist/build/h5 目录
# 可将生成的静态资源部署至Nginx服务器或OSS存储
 

5.2 微信小程序发布

# 构建微信小程序生产环境代码
npm run build:mp-weixin

# 使用微信开发者工具打开构建目录
open unpackage/dist/build/mp-weixin

# 上传并提交审核代码
 

5.3 原生App发布

  1. HBuilderX → 发行 → 原生App-云打包

  2. 选择Android/iOS证书

  3. 等待云端编译 → 下载安装包


六、常见踩坑与解决方案

问题 原因 解决方案
图片不显示 路径问题或大小超限 使用 /static/ 绝对路径,小程序图片≤2M
页面栈溢出 频繁navigateTo超过10层 使用 uni.switchTab 或 uni.reLaunch
样式不生效 小程序不支持某些CSS 使用 display: flex + position 替代复杂布局
跨域请求失败 微信小程序限制 配置合法域名或使用云函数转发
条件编译无效 注释格式错误 必须使用 #ifdef #endif 且无空格

七、学习路径与资源

推荐学习顺序

  1. Vue.js基础(指令、组件、生命周期、Vuex)

  2. uni-app官方文档(重点:pages.json、条件编译、组件)

  3. 仿写实战项目(新闻资讯、电商、社交)

  4. 多平台测试(真机调试、不同分辨率适配)

  5. 性能优化(分包加载、图片懒加载、骨架屏)

官方资源


八、总结

uni-app的核心价值

  1. 效率提升:一套代码覆盖6+平台,开发效率提升3-5倍

  2. 生态丰富:插件市场有大量现成组件,开箱即用

  3. 学习成本低:基于Vue语法,前端开发者快速上手

  4. 社区活跃:文档完善,遇到问题容易找到解决方案

适合场景

  • 中小型创业公司快速MVP验证

  • 企业内外部管理工具

  • 内容型、工具型小程序/H5应用

不适合场景

  • 对性能要求极高的3D游戏

  • 需要大量原生交互的复杂应用

一句话总结:学会uni-app,你就拿到了通往全栈+跨端开发的入场券。一套代码,多端覆盖,这就是未来前端开发的效率之道。

Logo

一站式 AI 云服务平台

更多推荐