简介:基于Vue.js与uniapp打造的微信小程序前端初版设计源码,面向小程序入门开发者或有跨端项目需求的工程师,提供一套可直接借鉴的前端工程骨架与组件化开发思路,能帮助快速理解uniapp项目的目录组织与基本开发流程。压缩包共173个文件,体积约1.29MB,以93个Vue组件、46个JavaScript文件、10个SCSS样式为主体,并包含JSON配置、PNG/GIF图标、HTML模板及许可文件等;其中Vue组件承载页面模块,JS文件处理逻辑与交互,SCSS负责统一样式,文档配置则支撑工程运行与规范管理。资源已有464人浏览/学习,适合用作调研uniapp项目结构或快速搭建初版微信小程序时的参考蓝本。从内容预览看,内含异步校验、图片裁剪、富文本解析等常用工具模块,并带有移动端图标和跨端HTML模板,可减少重复造轮子;加之ESLint忽略文件与开源许可证,工程约束清晰,便于在此基础上按业务需求扩展和维护。
1. 基于Vue的uniapp微信小程序前端初版本,动手前先分清楚三层边界
接到“初版本”需求时,通常意味着一个月后要有一个能演示、能体验、能收集反馈的版本,后端接口可能只定了一部分。选择基于 Vue 的 uni-app 做微信小程序,是因为一份代码能同时产出微信小程序和 H5,前端团队不用为两个端各维护一套。但初版最容易翻车的不是页面样式,而是三件事:登录态链路没闭环、请求层到处重复写、H5 与微信端的能力差异到联调时才暴露。这篇内容按“工程初始化 → 壳配置 → 数据通道 → 功能坑位 → 打包自检”的顺序,把一套能直接复用的初版搭法讲清楚。适合正在搭建小程序第一版的前端,也适合要把 Vue2 老项目迁到 Vue3 的团队参考。
2. 技术选型与初始化:Vue3、Vue2 与 uni-app CLI 怎么定
2.1 Vue3 与 Vue2 在 uni-app 里的差异,影响初版的代码组织方式
uni-app 同一套代码可以运行在小程序端、H5 端与 App 端,底层是把 Vue 组件编译成各端可执行的代码。编译链路决定了一件事:Vue2 版本走的是 webpack,Vue3 版本默认走 vite,两者在依赖处理、构建速度和 HMR 体验上差别明显。初版新项目,没有历史包袱时直接用 Vue3 组合式 API,原因是页面逻辑可以按业务维度拆成函数,例如把登录、获取用户信息、刷新 token 收敛到同一个模块里,比选项式分散写更贴近小程序的页面组织方式。
团队如果从 Vue2 迁过来,uniapp vue2转vue3方法并不是把 package.json 里的 vue 版本改掉就行。下面这张表列了最容易踩的四个差异点,迁移时逐条对照:
| 环节 | Vue2 习惯写法 | Vue3 需要调整 |
|---|---|---|
| 响应式数据 | data 返回对象,this.xxx 访问 | ref/reactive 定义,模板里自动解包,逻辑里要 .value |
| 生命周期 | onLoad/onShow 直接挂在组件选项里 | 从 @dcloudio/uni-app 按需 import |
| 自定义事件 | this.$emit('eventName', payload) | defineEmits 声明后在 setup 里调用 |
| 过滤器 filter | 模板中直接使用 | 已移除,改用 computed 或方法 |
初版没有历史包袱时,建议直接选用 Vue3 分支。Vue2 的选项式写法在小程序多页面场景下,状态分散在 data、computed、watch 里,页面一多维护成本明显上升;Vue3 组合式 API 可以把一个页面的请求、渲染、交互集中到 setup 区块,逻辑跳转时少翻几个文件。
2.2 用 CLI 创建 uni-app 工程并安装依赖,避免 IDE 版本锁定
工程创建用官方的模板仓库拉取即可,常见做法是:
# 拉取 Vue3 + vite 分支模板,项目名自定义 npx degit dcloudio/uni-preset-vue#vite my-uniapp-app cd my-uniapp-app # 安装依赖 npm install # 启动微信小程序端编译 npm run dev:mp-weixindegit 的作用是直接下载 GitHub 模板并去掉 git 历史,比 git clone 少一步清理成本的环节。vite 分支对应 Vue3,webpack 分支对应 Vue2,选错分支会导致后续按 Vue3 语法写却编译报错。npm install 装依赖阶段,常见的问题是 node 版本过高导致 esbuild 或 sass 编译失败,优先把 Node 切到 16 或 18 的 LTS 版本再装。启动 dev:mp-weixin 后,产物输出在 dist/dev/mp-weixin,微信开发者工具里导入该目录即可看到页面。
package.json 里默认脚本对应四个常用动作:
{ "scripts": { "dev:mp-weixin": "uni -p mp-weixin", "build:mp-weixin": "uni build -p mp-weixin", "dev:h5": "uni", "build:h5": "uni build" } }带 dev 前缀的是监听模式,改代码会增量编译;build 是产物构建,发布时使用。-p 指定平台,不传默认编译到 H5。这里要提醒一句:微信开发者工具导入的是 dist 目录,不是项目根目录。
提示:npm install 阶段遇到 node-sass 之类报错,先看 Node 版本,官方模板对 Node 16/18 兼容最好。
2.3 目录约定与页面传参,初版就该定死的两个规范
工程初始化后,src 下的目录结构建议直接按职责划分,避免所有代码堆在 pages 里:
src/ ├── pages/ # 页面目录,一个子目录对应一个页面 ├── components/ # 公共组件,配合 easycom 免 import ├── api/ # 接口定义,按模块拆文件 ├── utils/ # request 封装与工具函数 ├── store/ # 全局状态 ├── static/ # 图片、字体等静态资源 ├── App.vue # 应用入口,globalData 与全局生命周期 ├── main.js # 挂载入口 ├── pages.json # 路由、tabBar、导航栏配置 └── manifest.json # appid、权限说明等应用配置页面之间的跳转参数接收方式,与 vue-router 的 query 类似:
// 列表页发起跳转,参数拼在 url 上 uni.navigateTo({ url: '/pages/detail/detail?id=1001&title=' + encodeURIComponent('初版需求清单') }) // 详情页接收 export default { onLoad(options) { console.log(options.id) // '1001' console.log(options.title) // '初版需求清单' } }单向传参场景用 URL 即可,但只能传字符串,对象需要先 JSON.stringify 再用 encodeURIComponent 包一层,否则特殊字符会把参数截断。超过几百字的内容或需要多处共享的数据,不要走 URL,放 store 或 Storage 更合适。这个小细节也是前端面试里经常追问的传参场景。
3. manifest.json 与 pages.json:把微信小程序的壳先配对
3.1 manifest.json 的 mp-weixin 节点:appid、用户授权说明与校验开关
uniapp manifest配置是所有平台公共配置的入口,微信小程序相关的配置集中在 mp-weixin 节点。新建工程后先要填的是 appid,否则微信开发者工具里预览、真机调试、上传都会报 appid 不合法。测试阶段可以先不填,用测试号顶替,但涉及定位、支付、订阅消息这些能力时必须换成正式 appid。
{ "mp-weixin": { "appid": "wx1234567890abcdef", "setting": { "urlCheck": false, "es6": true, "minified": true }, "usingComponents": true, "permission": { "scope.userLocation": { "desc": "用于展示离你最近的可用门店" } } } }urlCheck 开发期设为 false,可以跳过 request 合法域名校验,方便直连本地后端调试。上传体验版前必须改回 true,并在微信公众平台配置 request 合法域名,否则线上接口全部被拦。permission 里的 desc 会在定位授权弹窗中直接展示给用户,微信要求必须写清楚用途,留空会直接拒绝授权。es6 和 minified 保持默认即可,分别控制 ES6 转译与代码压缩。
3.2 pages.json 路由与 tabBar:启动页、导航栏与文字 tab 一次配好
pages.json 同时承担路由表、窗口样式、tabBar 三份职责,微信小程序的页面注册顺序直接决定启动页是谁。初版通常结构简单,配置一个首页加一个“我的”页就够了:
{ "pages": [ { "path": "pages/index/index", "style": { "navigationBarTitleText": "首页" } }, { "path": "pages/mine/mine", "style": { "navigationBarTitleText": "我的" } } ], "globalStyle": { "navigationBarTextStyle": "black", "navigationBarBackgroundColor": "#ffffff", "navigationBarTitleText": "初版小程序", "backgroundColor": "#f5f5f5" }, "tabBar": { "color": "#999999", "selectedColor": "#07c160", "backgroundColor": "#ffffff", "list": [ { "pagePath": "pages/index/index", "text": "首页" }, { "pagePath": "pages/mine/mine", "text": "我的" } ] } }pages 数组第一项就是小程序启动页,修改启动页顺序直接调整这个数组。tabBar 的 pagePath 必须在 pages 中存在,否则编译直接报“tabBar 页面不存在”。tabBar 列表项支持 iconPath 配置图标,初版设计资源还没到位时可以像示例一样只放文字,微信允许文字 tab。导航栏要自定义时,在页面的 style 里加 "navigationStyle": "custom",原生导航栏会消失,页面顶部需要用状态栏高度手动补一块占位。
关于“修改刚进入的加载页面”:小程序冷启动时的品牌加载页是微信后台配置的,前端改不了;前端能改的是首屏内容渲染时机,常见做法是在 App.vue 的 onLaunch 里做登录态检查,配合页面骨架屏减少白屏感,这部分在第 6 章展开。
3.3 easycom 组件规则与 uni.scss 全局样式变量
初版本会用到大量 uni-ui 或自定义组件,如果每个页面都手动 import 一遍,组件一多代码会很啰嗦。easycom 的机制是:只要组件文件路径符合规则,页面模板里直接用标签名即可,无需 import 也无需注册。
{ "easycom": { "autoscan": true, "custom": { "^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue" } } }autoscan 会对 components 目录按“components/组件名/组件名.vue”的约定自动扫描;custom 里把 uni- 开头的标签映射到 uni-ui 包内路径。配好后,页面里直接写<uni-list>、<uni-card>就能用。样式方面,uni.scss 中定义的变量在任意页面 style 里直接可用,公共颜色、圆角、间距都放这里,比每页复制颜色值可靠。注意 App.vue 的全局样式中标签选择器在小程序端部分场景不生效,不要用它覆盖组件内部样式。
4. request 封装与 code 换 token:初版数据通道一次跑通
4.1 基于 uni.request 的请求层:统一 baseURL、头部与错误收敛
初版最常见的失败模式是每个页面直接调 uni.request:token 怎么带写三遍,401 怎么处理写三遍,改 baseURL 要全局搜索。正确做法是先封装一个 request 函数,把 header 注入、超时、业务码判断、错误提示全部收敛到一处。
// utils/request.js const BASE_URL = 'https://api.example.com' let isRedirecting = false // 防止 401 时重复跳转登录页 export function request(options) { return new Promise((resolve, reject) => { uni.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data || {}, header: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + (uni.getStorageSync('token') || '') }, timeout: 15000, success: (res) => { if (res.statusCode === 200 && res.data.code === 0) { resolve(res.data.data) } else if (res.statusCode === 401 || res.data.code === 401) { handleUnauthorized() reject(new Error('登录已过期')) } else { uni.showToast({ title: res.data.message || '请求失败', icon: 'none' }) reject(new Error(res.data.message)) } }, fail: (err) => { uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' }) reject(err) } }) }) } function handleUnauthorized() { uni.removeStorageSync('token') uni.removeStorageSync('userInfo') if (isRedirecting) return isRedirecting = true uni.navigateTo({ url: '/pages/login/login', complete: () => (isRedirecting = false) }) }业务约定是 code 为 0 时请求成功,401 表示登录失效。token 每次请求从 Storage 读取而不是存内存变量,是为了避免 storage 被外部清除后内存里还残留旧 token,造成请求头不一致。isRedirecting 是跳转锁,防止多个接口同时 401 时连续跳转登录页把页面栈打爆。fail 分支统一提示网络异常,具体网络错误细节打印到 console 便于排查。
4.2 微信小程序用 code 换 token:一次性凭证的正确用法
微信小程序的登录态链路是固定的:前端调 wx.login 拿到临时 code,把 code 交给后端,后端拿 code 调微信的 code2session 接口换取 openid 和 session_key,再生成业务 token 返回前端。这个 code 的有效期为五分钟且只能使用一次,所以它只承担“请求换 token”的角色,不能当作登录凭证保存。
// pages/login/login.vue 中的登录方法 uni.login({ provider: 'weixin', success: async (loginRes) => { const { code } = loginRes try { const data = await request({ url: '/auth/login', method: 'POST', data: { code } }) uni.setStorageSync('token', data.token) uni.setStorageSync('userInfo', data.userInfo) uni.switchTab({ url: '/pages/index/index' }) } catch (e) { console.error('登录失败', e) } } })code 的获取必须通过 uni.login 而不是手动拼参数,provider 固定为 weixin。后端接口返回的 token 要落 Storage,再次冷启动时 App.vue 的 onLaunch 里先检查 token 是否存在,存在则跳过登录页,不存在才引导登录。这里有一个前端面试高频追问点:token 有效期内的静默续期怎么做。常见做法是请求层捕获 401 后,调用刷新 token 接口拿到新 token 再重放原请求,初版本可以先降级为“401 直接回登录页”,等用户量上来再补续期逻辑。
提示:code2session 的调用必须放在服务端,前端直接调用会暴露小程序的 secret,属于严重的安全事故。
登录态相关的故障,初版联调中常见的有三种:
| 故障现象 | 直接原因 | 处理方式 |
|---|---|---|
| 每次冷启动都被弹回登录页 | token 没存 Storage,或后端返回字段名与前端读取不一致 | 全局检查 setStorageSync 与 getStorageSync 的 key 是否统一 |
| 多个接口同时报 401,页面反复跳转 | 401 处理没有防重入 | 加 isRedirecting 跳转锁 |
| H5 端 uni.login 无响应 | 非微信小程序环境不支持 wx.login | 按平台分流,H5 走账号密码或手机号验证码 |
4.3 请求体格式与参数编码的前后端对齐
请求层封装好后,还要和后端对齐两个细节。第一是 Content-Type:上面封装里默认 application/json,后端如果按表单解析就会拿到一串空 body;后端要求表单格式时,header 改成 application/x-www-form-urlencoded,data 用 URLSearchParams 或查询串。第二是 GET 参数的编码:uni.request 会把 GET 的 data 自动拼到 query 上,但中文和特殊字符仍然建议手动 encodeURIComponent 一次,避免后端收到被截断的参数。这两处对齐越早做,联调阶段的返工越少。
5. 初版功能坑位自查:m3u8 播放、H5 获取定位与分享配置
5.1 vue 播放 m3u8 的跨端方案:微信原生 video 与 hls.js 条件编译
初版涉及视频流播放时,m3u8 格式是绕不开的,HLS 协议在微信小程序和 H5 端的支持程度不同。微信小程序的原生 video 组件底层是微信播放器,直接支持 m3u8 地址;H5 端的 video 标签原生不支持 HLS,必须借助 hls.js 把流转成 MSE 可以喂给 video 的数据。
<template> <view class="player"> <video v-if="isMpWeixin" :src="videoSrc" controls autoplay object-fit="contain" @error="onVideoError" /> <view v-else id="hlsVideoContainer" class="hls-video"></view> </view> </template>// #ifdef H5 import Hls from 'hls.js' // #endif export default { data() { return { videoSrc: 'https://example.com/live/stream.m3u8', isMpWeixin: false } }, onLoad() { // #ifdef MP-WEIXIN this.isMpWeixin = true // #endif }, onReady() { // #ifdef H5 this.initH5Player() // #endif }, methods: { // #ifdef H5 initH5Player() { const container = document.getElementById('hlsVideoContainer') const video = document.createElement('video') video.controls = true video.autoplay = true video.style.width = '100%' container.appendChild(video) if (Hls.isSupported()) { const hls = new Hls({ maxBufferLength: 30 }) hls.loadSource(this.videoSrc) hls.attachMedia(video) hls.on(Hls.Events.ERROR, (event, data) => { console.error('HLS 播放错误', data) }) } }, // #endif onVideoError() { uni.showToast({ title: '视频加载失败,请重试', icon: 'none' }) } } }条件编译的 #ifdef 指令在编译期生效,H5 代码只会打进 H5 产物,小程序产物不会包含 hls.js,体积上不会浪费。maxBufferLength 控制缓冲时长,网络差时调小能降低延迟,网络好时调大可减少卡顿。autoplay 在部分浏览器会被拦截,移动端普遍要求先静音或用户手势触发后才允许自动播放,初版调试时如果发现点开不播,先检查浏览器拦截而不是怀疑 hls.js 配置。微信端 video 组件遇到 m3u8 拉流失败时,onVideoError 里 catch 住错误,给出重试按钮即可。
5.2 uniapp 开发 H5 嵌入微信公众号中获取定位:三套路径按环境选
uniapp 的 uni.getLocation 在小程序端是直通微信定位能力,但在 H5 端被嵌入微信公众号里时,表现并不一致。定位这个功能在 H5 与小程序端要按环境分三套处理:小程序端优先用 uni.getLocation;H5 在微信内置浏览器里优先用微信 JS-SDK 的 wx.getLocation,需要后端提供签名;普通 H5 浏览器里则可以尝试 HTML5 Geolocation。
// 小程序与普通 H5 环境下的通用调用 uni.getLocation({ type: 'gcj02', isHighAccuracy: true, highAccuracyExpireTime: 3000, success: (res) => { console.log('纬度', res.latitude) console.log('经度', res.longitude) }, fail: (err) => { console.error('定位失败', err) } })type 参数要按地图服务商选:wgs84 是 gps 原始坐标,gcj02 是国测局加密坐标,国内地图 SDK(微信内置地图、高德、腾讯)都要求 gcj02,传错会导致标点偏移几百米。isHighAccuracy 开启后定位精度更高但更费电,初版测试建议开启,上线前根据场景决定是否降级。manifest 里 permission 的 scope.userLocation 配置在 3.1 节还没配的话,小程序端调用 getLocation 会直接失败。
H5 嵌入微信公众号里的定位,核心坑在授权链路上:公众号要先配置 JS 接口安全域名,后端再根据当前页面的 URL 生成签名,前端 wx.config 成功后才能调 wx.getLocation。签名依赖后端参与,初版排期时要把后端配合时间计入。微信公众号内不配置 JS-SDK 而直接调 uni.getLocation,很多安卓机子上会静默失败,这也是 H5 端定位“时好时坏”的主要原因。
5.3 自定义分享好友与顶部导航栏高度计算
微信小程序的转发能力通过 onShareAppMessage 配置,初版如果连一个分享入口都不留,后续推广时还得返工。页面里声明了该方法,右上角菜单才会出现“转发”按钮,方法返回值决定分享卡片内容。
export default { onShareAppMessage() { return { title: '初版小程序,看看里面有什么', path: '/pages/index/index?from=share', imageUrl: '/static/share.png' } } }path 要带上前置页面需要的参数,imageUrl 分享图尺寸建议 5:4,缺省时默认截取当前页面顶部作为封面。如果用了自定义导航栏,分享按钮和胶囊按钮都要在自定义区域内预留布局空间,胶囊按钮的位置需要动态计算:
// #ifdef MP-WEIXIN const systemInfo = uni.getSystemInfoSync() const menuButton = uni.getMenuButtonBoundingClientRect() const navBarHeight = (menuButton.top - systemInfo.statusBarHeight) * 2 + menuButton.height const statusBarHeight = systemInfo.statusBarHeight // #endif自定义导航栏的总高度等于状态栏高度加上导航内容区高度,导航内容区通常按照胶囊按钮的垂直居中位置反推。这组数值在 iPhone 全面屏和安卓机型上差异较大,不要在 css 里写死,按上面公式计算后动态绑定到内联 style。顶部导航栏里如果有搜索框或标题,水平方向要避让胶囊按钮的左侧位置,否则会被挡住。
6. 打包前自检清单:跨端差异一次性抹平
6.1 条件编译语句,按平台隔离差异源码
跨端差异源码统一用条件编译包裹,而不是运行时 if 判断。编译期就能把不需要的代码从产物里剔除,H5 包不会残留小程序 API 调用,小程序包也不会有 document 操作。
// #ifdef MP-WEIXIN uni.login({ provider: 'weixin' }) // #endif // #ifdef H5 doH5LoginByPhone() // #endif6.2 微信端与 H5 端的差异自检清单
发布前把下面这张表逐项过一遍,大部分线上问题能提前拦下:
| 能力 | 微信小程序端 | H5 端 |
|---|---|---|
| 登录 | wx.login 换 code | OAuth 或账号密码/验证码 |
| 定位 | uni.getLocation 直接可用 | 公众号内需 JS-SDK 签名 |
| m3u8 播放 | video 原生支持 | 依赖 hls.js |
| 分享好友 | onShareAppMessage 生效 | 不支持,需引导复制链接 |
| 本地存储 | uni.setStorageSync | localStorage 兼容层,隐私模式会失效 |
6.3 从构建到上架:小程序包与 H5 包的产物去向
微信小程序正式包用 build 命令产出后,微信开发者工具导入 dist/build/mp-weixin,在工具里完成预览、上传,再到公众平台提审。H5 包 build 后是纯静态文件,部署到任意 https 服务器即可。uniapp ios 打包需要 p12 证书与描述文件,在 HBuilderX 云打包界面配置 Bundle Identifier 后生成安装包;上架安卓应用市场时要注意 targetSdk 版本与各市场的签名要求,包名和签名证书确定后不要再改,否则无法覆盖安装。最后一件事:build 前全局搜索 localStorage、document 等 H5 专属调用,确认都被条件编译包裹或替换为 uni API,再提交测试。
本文还有配套的精品资源,点击获取