
简介这是一套基于 Vue.js 与 uniapp 构建的微信小程序前端初版源码旨在解决多端重复开发的问题面向具备一定前端基础、希望快速上手跨端小程序开发的初学者或团队。压缩包共 173 个文件、约 1.29MB涵盖 93 个 Vue 组件、46 个 JavaScript 逻辑文件、10 个 SCSS 样式文件以及 JSON 配置、PNG/GIF 图片、LICENSE 许可等类型其中脚本文件承载页面逻辑与数据交互样式文件负责界面样式配置文件用于全局及页面配置目录划分清晰便于按模块查阅。项目完整呈现了组件化页面搭建、核心交互逻辑、全局与页面级配置、样式与静态资源组织等关键环节能直观展示 uniapp 在微信小程序端的开发范式同时保留了模板、配置文件与静态资源方便快速替换为实际业务页面。目前已有 464 人学习下载适合用作入门参考或初版原型尤其有助于理解 Vue 组件在小程序场景中的复用方式并快速扩展出更多业务功能。1. 基于Vue的uniapp微信小程序前端初版本先把工程骨架立起来很多团队做微信小程序第一反应是直接开微信开发者工具写原生代码但一旦涉及后续要扩展到App端或H5端原生小程序代码的复用率就非常低。基于Vue语法的uniapp框架本质上是一套编译到多端的方案业务代码用Vue单文件组件编写通过cli或HBuilderX编译成微信小程序、支付宝小程序、iOS和Android应用。初版本设计的重点不是功能堆砌而是把目录结构、请求层、登录态、路由与组件规范一次性定好避免后续每个页面都各写一套风格。这个初版本源码的价值在于它不只是一个能跑的Demo而是一套可继续叠业务的地基。你拿到的应该是包含pages、static、utils、api、components等标准目录的uniapp工程编译目标为微信小程序。适合正在从原生小程序转向跨端方案、或者第一次用uniapp搭建前端工程的人。接下来的内容我按一个可交付的初版本需要具备的模块逐步拆解每个环节都给出可以直接抄的代码和参数说明。2. 初版本工程结构设计与Vue组件的选型依据2.1 页面目录与组件划分决定这个版本的扩展上限初版本最忌讳的是把所有代码塞进App.vue和单个页面里。uniapp的pages.json相当于小程序原生的app.json页面注册、tabBar配置、窗口样式都在这一个文件里集中管理。我一般会按业务域划分目录而不是按页面类型划分例如pages/index、pages/login、pages/mine、pages/goods每个业务域下的页面只做路由和页面级状态管理公共逻辑下沉到components和utils。├── pages │ ├── index/index.vue │ ├── login/login.vue │ └── mine/mine.vue ├── components │ ├── list-empty.vue │ └── nav-bar.vue ├── api │ ├── request.js │ ├── user.js │ └── goods.js ├── utils │ ├── auth.js │ └── format.js ├── static │ └── logo.png ├── App.vue ├── main.js ├── manifest.json └── pages.json这个结构的核心逻辑是api目录集中管理所有接口请求页面不直接调用uni.request而是统一走api层封装components目录放可复用的业务组件utils目录放无业务状态的纯函数。初版本不需要引入vuex或pinia做全局状态管理登录态存storage页面间数据用路由参数或事件总线够用到十个页面以内。超过这个规模再考虑引入pinia反而更可控。2.2 为什么用Vue语法而不是直接写小程序原生标题里强调了“基于Vue”这一点的价值在于开发体验和代码可维护性。uniapp的Vue写法保留了响应式数据绑定、计算属性、生命周期钩子和Web端Vue开发几乎没有差别。比如你在Web Vue项目里习惯用v-for渲染列表、用watch监听数据变化、用computed做派生数据在uniapp里全部通用。对于团队里已经有Vue经验的人上手成本只需要熟悉uniapp的API和微信小程序的限制而不需要重新学一套WXML和WXSS。template view classgoods-card clickgoDetail(item.id) image :srcitem.cover modeaspectFill classcover / view classtitle{{ item.title }}/view view classprice¥{{ item.price }}/view /view /template script export default { props: { item: { type: Object, required: true } }, methods: { goDetail(id) { uni.navigateTo({ url: /pages/goods/detail?id${id} }) } } } /script组件的逻辑很直白父组件传入商品对象子组件负责展示并处理点击跳转navigateTo是uniapp提供的路由API传给它的url字符串里用参数拼接。注意这里的props类型校验在初版本就必须写上一是方便后来接手的人看懂数据结构二是小程序端对类型错误有更明确的告警。代码中modeaspectFill是image组件最常用的裁剪模式等价于Web端的object-fit: cover避免图片变形。2.3 manifest.json里的微信小程序配置项初版本必须设置对很多初版本跑不起来问题就出在manifest.json的配置上。uniapp在微信小程序端的配置分为两部分一部分是manifest.json里的mp-weixin节点一部分是project.config.json里的appid和项目设置。project.config.json是微信开发者工具识别工程用的而manifest.json里的设置会在编译时写进小程序配置。{ mp-weixin: { appid: 你的小程序appid, setting: { urlCheck: false, es6: true, minified: true }, usingComponents: true, permission: { scope.userLocation: { desc: 获取您的位置用于展示附近门店 } }, requiredPrivateInfos: [getLocation] } }urlCheck控制是否校验合法域名开发阶段设false方便调接口上线前必须改回true并配置request合法域名。requiredPrivateInfos是微信2022年后新增的隐私接口声明不声明的话调用uni.getLocation会直接报错。permission里的desc会显示在用户授权弹窗中文案需要说明授权用途否则审核会被驳回。这就是初版本里最容易忽略的配置项功能写了配置没声明真机预览直接白屏或弹错误。3. 微信小程序登录态与请求层code换token的完整链路3.1 登录流程为什么要先拿code再换token微信小程序登录和Web登录有一个关键区别小程序端拿不到用户密码腾讯的安全机制决定了前端只能获取一个临时的code这个code五分钟内有效只能使用一次需要发送到自己的后端服务器由后端调用微信的接口换取openid和session_key再下发自定义的token。很多初版本直接把code存storage或者把openid返回前端这是错误的做法openid是用户在小程序里的唯一标识泄露给前端虽然不至于直接丢数据但会放大伪造请求的风险。// api/auth.js import request from ./request export function wxLogin() { return new Promise((resolve, reject) { uni.login({ provider: weixin, success: async (loginRes) { try { const { code } loginRes const res await request({ url: /auth/wx-login, method: POST, data: { code } }) resolve(res) } catch (e) { reject(e) } }, fail: (err) reject(err) }) }) }这段代码把uni.login和业务接口调用封装在一个Promise里外部使用时只需要const res await wxLogin()就能拿到后端返回的token和用户信息。uni.login的成功回调里返回的loginRes.code就是临时凭证传给后端接口后由后端完成后续的openid获取和token签发。初版本里前端拿到token后下一步就是把token写入storage并在后续每次请求的头里带上。3.2 request.js统一封装处理token注入和401跳转请求层是一个前端工程的脊梁骨。初版本如果不做统一封装会出现每个页面各自uni.request、各自处理错误码的混乱局面。封装的核心职责有几个自动携带token、统一处理HTTP状态码和业务状态码、超时中断、文件上传时自动带header、以及401时统一跳转登录。我需要把这四件事合并到一个文件里。// api/request.js const BASE_URL https://api.example.com export default function 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 || {}, timeout: options.timeout || 10000, header: { Content-Type: application/json, Authorization: token ? Bearer ${token} : }, success: (res) { if (res.statusCode 401) { uni.removeStorageSync(token) uni.navigateTo({ url: /pages/login/login }) reject(res) return } if (res.statusCode 200 res.statusCode 300) { if (res.data.code 0) { resolve(res.data.data) } else { uni.showToast({ title: res.data.msg || 请求失败, icon: none }) reject(res.data) } } else { uni.showToast({ title: 服务异常${res.statusCode}, icon: none }) reject(res) } }, fail: (err) { uni.showToast({ title: 网络连接失败, icon: none }) reject(err) } }) }) }这段封装里BASE_URL是接口域名真实项目中应该区分开发环境、测试环境和生产环境初版本可以先用一个常量后续再用process.env.NODE_ENV做环境判断。Authorization请求头的格式需要和后端约定Bearer前缀是常见做法也可以用自定义的X-Token关键是前后端保持一致。401的处理我选择直接清token并跳转登录页而不是弹出提示再跳转因为token过期时用户感知到的是静默失效跳转登录页让用户重新走一遍授权流程更干净。3.3 用code换token时的几个常见报错排查初版本联调登录接口时大概率会遇到几个报错这里提前说明定位方向。第一是invalid code原因通常是code被使用过两次或者code生成时间和后端调用微信接口的时间间隔超过五分钟。前端需要检查是否存在重复调用wxLogin的情况。第二是appid mismatch排查方向是config里填的appid和后端后台配置的是否一致这个不一致在开发工具里很难发现因为工具本身能编译成功。第三是后端返回的token字段名不统一比如后端文档写的access_token前端代码取的token导致storage里永远存不上。4. 初版本核心页面与常用交互的落地实现4.1 首页列表的加载与渲染下拉刷新与触底分页首页是初版本的门面也是用户进入小程序后的第一个视觉落点。这个页面通常包含一个轮播图、一个商品列表或资讯列表以及对应的加载状态。列表页的核心是分页逻辑这里我用onReachBottom触底加载下一页用onPullDownRefresh做下拉刷新这两个是uniapp页面生命周期里专门为小程序场景提供的钩子。template view classhome swiper :indicator-dotstrue :autoplaytrue :interval4000 swiper-item v-for(banner, index) in banners :keyindex image :srcbanner.image classbanner-image / /swiper-item /swiper view v-foritem in list :keyitem.id classlist-item text{{ item.title }}/text /view view v-ifloading classloading-text加载中.../view view v-iffinished classfinished-text没有更多了/view /view /template script import { getBanners, getList } from /api/home export default { data() { return { banners: [], list: [], page: 1, pageSize: 10, loading: false, finished: false } }, onLoad() { this.loadBanners() this.loadList() }, onPullDownRefresh() { this.page 1 this.finished false this.loadList(() uni.stopPullDownRefresh()) }, onReachBottom() { if (!this.loading !this.finished) { this.page 1 this.loadList() } }, methods: { loadBanners() { getBanners().then(res { this.banners res }) }, loadList(callback) { this.loading true getList({ page: this.page, pageSize: this.pageSize }).then(res { const newList res.list || [] this.list this.page 1 ? newList : this.list.concat(newList) this.finished newList.length this.pageSize }).finally(() { this.loading false if (callback) callback() }) } } } /script分页逻辑里有个细节page 1时直接替换listpage 1时用concat追加这样下拉刷新和触底加载共用loadList这一个方法不需要写两套函数。onPullDownRefresh里先重置page和finished再加载数据加载完成回调里调用uni.stopPullDownRefresh结束动画。onReachBottom里加了this.loading的开关避免在请求还没返回时用户连续触发多次加载造成数据错乱。这个防重入的开关是初版本最容易漏掉的。4.2 微信小程序里播放m3u8视频流的HLS方案搜索热词里出现了“vue播放m3u8”这个问题在uniapp里同样常见。微信小程序原生video组件不支持m3u8格式的直播流或点播流需要先做一次转换。方案有两种一种是用腾讯云的TCPlayer另一种是用video.js的HLS插件但在uniapp里还需要考虑小程序的兼容性。最常见且稳定的做法是如果视频源是m3u8用hls.js库在Web端播放小程序端则使用wx video配合腾讯云的点播播放器。template video v-ifvideoUrl :srcplayUrl classvideo-player controls autoplay erroronVideoError / /template script export default { data() { return { playUrl: } }, props: { videoUrl: { type: String, required: true } }, watch: { videoUrl: { immediate: true, handler(url) { if (url url.includes(.m3u8)) { // 原生video组件无法直接播放m3u8需要转换 this.playUrl url } else { this.playUrl url } } } } } /script这段代码看起来简单实际使用中m3u8能否播放取决于微信小程序的video组件版本和服务器是否允许跨域。微信官方对video组件的播放格式支持有限我在实际项目中遇到m3u8播放不出来的情况最终的落地做法是让后端把m3u8转成mp4临时链接或者配置一个HLS代理地址。如果你在开发H5端可以用hls.js库做兼容如果你在小程序端遇到m3u8播放困难优先和后端确认是否能把ts切片放在同一个域名下微信小程序的网络请求不允许跨域域名这是硬限制。4.3 获取定位并在地图上标记兼容微信公众号H5场景另外一个高频场景是“uniapp开发h5嵌入微信公众号中获取定位”。uniapp的uni.getLocation在微信小程序里可以直接用但在H5端的微信公众号环境里需要引入微信JS-SDK用wx.getLocation来获取而且必须先通过后端获取签名。这个差异很多人踩坑初版本需要做一个封装层屏蔽掉平台差异。// utils/location.js export function getLocation() { return new Promise((resolve, reject) { // #ifdef MP-WEIXIN uni.getLocation({ type: gcj02, success: resolve, fail: reject }) // #endif // #ifdef H5 const jweixin require(jweixin-module) // 先调用后端接口获取签名 uni.request({ url: /wechat/js-sdk-config, method: POST, data: { url: window.location.href.split(#)[0] }, success: (res) { const config res.data.data jweixin.config({ debug: false, appId: config.appId, timestamp: config.timestamp, nonceStr: config.nonceStr, signature: config.signature, jsApiList: [getLocation] }) jweixin.ready(() { jweixin.getLocation({ type: gcj02, success: resolve, fail: reject }) }) }, fail: reject }) // #endif }) }// #ifdef MP-WEIXIN和// #ifdef H5是uniapp的条件编译注释编译时会自动保留对应平台的代码块去掉其它平台的代码。H5端引入jweixin-module需要在项目里先执行npm install jweixin-module。签名接口的url参数必须用window.location.href.split(#)[0]去掉hash部分因为微信JS接口安全域名校验是基于当前页面的完整URL进行签名带hash会导致签名无效。这个封装的思路值得沿用页面层只需要调用getLocation()不需要关心当前运行在哪个平台。4.4 长按拖拽排序和单选框组件的实现微信小程序里做长按拖拽滚动排序初版本如果不借助第三方库手写会比较繁琐。uniapp的movable-area和movable-view组合可以在一个区域内实现自由拖拽但要做列表内长按拖拽排序更简洁的思路是用scroll-view配合touchstart、touchmove和touchend事件自己实现一个简易版。template scroll-view scroll-y classsort-list view v-for(item, index) in sortedList :keyitem.id classsort-item :class{ dragging: dragIndex index } longpressstartDrag(index) touchmoveonDragMove touchendendDrag text{{ item.name }}/text /view /scroll-view /template script export default { data() { return { sortedList: [], dragIndex: -1, startY: 0 } }, methods: { startDrag(index) { this.dragIndex index this.startY 0 }, onDragMove(e) { if (this.dragIndex -1) return const currentY e.touches[0].clientY if (this.startY 0) { this.startY currentY return } const delta currentY - this.startY const moveOffset Math.round(delta / 44) if (moveOffset ! 0) { const targetIndex this.dragIndex moveOffset if (targetIndex 0 targetIndex this.sortedList.length) { const temp this.sortedList.splice(this.dragIndex, 1)[0] this.sortedList.splice(targetIndex, 0, temp) this.dragIndex targetIndex this.startY currentY } } }, endDrag() { this.dragIndex -1 this.startY 0 } } } /script代码里44是每个列表项的高度delta / 44计算出手指移动跨越了几个列表项每跨越一个就实时交换位置。这种做法的交互手感接近原生长按排序但性能上需要在每个item上添加transform: transition来让位移动画平滑。注意longpress事件在微信小程序里是原生支持的长按事件不需要自己用setTimeout模拟。5. 打包配置与初版本上线前的三个关键检查5.1 uniapp怎么打包成微信小程序两种途径对比打包这个话题在初版本阶段就需要理清因为开发完成后第一件事就是上传代码到微信公众平台。uniapp提供两种打包方式。第一种是HBuilderX里点菜单栏的“发行”选择“小程序-微信”会自动生成dist目录下的微信小程序工程然后用微信开发者工具打开这个目录填入自己的appid即可预览和上传。第二种是命令行方式在项目根目录执行npm run dev:mp-weixin或npm run build:mp-weixin输出同样在dist目录下。npm run dev:mp-weixin这条命令会在dist/dev/mp-weixin目录下生成编译后的微信小程序代码。官方推荐开发时用dev命令配合微信开发者工具的“打开项目”功能指向这个目录修改代码后工具会自动刷新。打包上线时用build命令build模式下会压缩代码、去console但初版本建议保留console到功能稳定后再关。两种方式殊途同归差异在于HBuilderX的“发行”菜单会在编译完成后自动打开微信开发者工具命令行的方式需要手动打开。5.2 manifest配置与隐私弹窗修改刚进入的加载页面“修改刚进入的加载页面”是一个具体且高频的需求。小程序默认从pages.json里第一个页面进入初始加载时微信会先展示一个空白页。如果想要一个独立的启动加载页比如展示logo和slogan不需要额外跳转只需要在pages.json里把启动页放在第一条路由然后在onLoad里做重定向。{ pages: [ { path: pages/launch/launch, style: { navigationBarTitleText: 加载中, navigationStyle: custom } }, { path: pages/index/index, style: { navigationBarTitleText: 首页 } } ] }navigationStyle设为custom会让导航栏变成透明启动页可以全屏展示自己的视觉。在launch页的onLoad里用uni.reLaunch跳转到主页注意这里不能用navigateTo因为navigateTo会保留当前页面用户按返回键会回到启动页体验很奇怪。reLaunch会关闭所有页面并打开指定页面符合启动页的语义。5.3 uniapp ios打包的签名和证书问题如果是初版本需要做iOS端的App打包需要注意uniapp在iOS端有两种打包方式一种是云打包在HBuilderX里配置好Apple开发者账号的证书文件p12和描述文件mobileprovision直接云端生成ipa另一种是离线打包需要iOS工程师用Xcode加载官方SDK编译。初版本建议直接用云打包。这里有个常见的问题manifest.json里配置的Bundle Identifier必须和描述文件里的保持一致不一致会导致打包成功后安装闪退。另外iOS端的NSLocationWhenInUseUsageDescription需要在manifest的App模块配置里补充隐私文案否则调用定位时会直接崩溃。5.4 vue2转vue3的差异点初版本就要想清楚热词里出现了“uniapp vue2转vue3方法”这个问题不是迁移时才会遇到而是初版本选型时就要决策。目前uniapp对Vue3的支持已经成熟但很多老旧插件和组件库还停留在Vue2语法所以选择前先确认自己的依赖是否支持Vue3。如果需要转换常见的差异有全局API从Vue.prototype变成了app.config.globalPropertiesfilter被废弃需要改成计算属性或方法$on和$off被移除事件总线需要自己实现或引入mitt。对于初版本如果团队熟悉Vue2不必强行上Vue3但新建项目建议直接Vue3因为微信小程序端Vue3的编译性能更优而且Vue2在2023年后进入维护期新特性不再加入。// Vue2写法 Vue.prototype.$baseUrl https://api.example.com // Vue3写法 const app createApp(App) app.config.globalProperties.$baseUrl https://api.example.com在uniapp的main.js里Vue2用Vue.prototype挂载全局变量Vue3用app.config.globalProperties这两种写法在模板里的使用时一致的都用this.$baseUrl。差异只在挂载环节。初版本如果选择Vue3还需要在main.js里去掉Vue.config.productionTip之类的Vue2专属配置否则控制台会提示无效属性。5.5 微信开发者工具里的sourceMap与本地调试技巧初版本调试时微信开发者工具里不能直接看到uniapp源码只能看到编译后的JS和WXML这给排查问题带来不小的困难。我的做法是在manifest.json的mp-weixin节点下把devtool设为source-map编译时会生成对应的sourceMap文件微信开发者工具的Sources面板里即可看到编译前的源文件进行断点调试。需要注意这仅用于开发阶段上线打包时务必关闭否则源代码直接暴露给用户。另一个技巧是使用uni.showToast和console.log结合#ifdef条件注释在小程序端只保留必要日志H5端的调试日志可以通过vconsole插件在真机预览时查看。本文还有配套的精品资源点击获取