
简介一套基于Vue与uniapp的微信小程序前端初版源码面向微信小程序开发者与前端初学者解决从零搭建小程序基础架构、组件与样式体系的问题。压缩包共173个文件大小仅1.29MB包含93个Vue组件、46个JavaScript逻辑文件、10个SCSS样式文件以及PNG/GIF图片、JSON配置、ESLint规则、LICENSE许可等完整工程文件兼顾界面呈现、功能逻辑与项目规范。目前已有464人学习下载。源码目录结构清晰组件与工具函数分层明确既有iconfont、weCropper、MpHtmlParser等实用模块也包含H5模板与跨平台适配配置便于快速理解小程序常见场景的实现方式。适合用于课程设计参考、初始版本迭代或uniapp跨端开发学习能帮助节省前期搭建成本直接在此基础上按需扩展业务功能。1. 初版 uniapp 源码不是“不能跑”是“先给你看边界”拿到一份 174 个文件的 uniapp 小程序源码我的第一反应不是找入口而是先数 Vue 组件和 JS 文件的配比。93 个 Vue 组件加 46 个 JavaScript 文件说明页面渲染和业务逻辑已经分开不是把所有东西塞在一个大 page 里的玩具工程。它适合两类人刚进入微信小程序开发、想通过完整工程理解 uniapp 编译链路的前端开发者以及要在既有代码上快速出第一款小程序、不想从零搭脚手架的团队。初版源码的价值在于边界清晰哪些是组件、哪些是配置、哪些是逻辑打开目录就能看出来。下面按结构、配置、功能、编译四条线把这套源码拆开讲。2. 从 174 个文件拆解 uniapp 项目的模块边界uniapp 的工程目录和标准 Vue 项目很像区别主要在src/pages.json和src/manifest.json这两个文件上。初版项目把 174 个文件按类型分得很清楚93 个 Vue 组件、46 个 JavaScript 文件、10 个 SCSS 样式、3 个 JSON 配置。这个比例对小程序开发来说偏重组件化意味着后续增加页面时大部分逻辑可以复用已有组件而不是复制页面代码。具体文件组成可以整理成一张表后续排查问题时会很有用。分类数量作用Vue 组件93页面、自定义组件、列表项、tab 栏等视图层JavaScript46页面逻辑、请求封装、工具函数、API 桥接PNG 图片11tab 图标、默认展示图、业务静态图SCSS 样式10全局变量、基础样式、组件样式JSON 配置3pages.json 路由、manifest.json 应用配置、项目级配置其他若干ESLint 配置、LICENSE、ico、template.h5.html2.1 Vue 组件页面与自定义组件的划分初版项目里最容易混淆的是 pages 目录下的页面组件和 components 目录下的自定义组件。标准 uniapp 工程中页面组件必须注册到pages.json的pages数组里编译后每个页面才有一个独立入口而自定义组件只需要在页面里import引入或者通过easycom规则自动注册。看到 93 个 Vue 组件时先分清哪些是页面、哪些是复用组件后面读代码的速度会快很多。例如在页面中使用一个goods-list自定义组件写法如下template view classorder-page goods-list :itemsorderList selecthandleSelect/goods-list /view /template script import GoodsList from /components/goods-list/goods-list.vue; export default { components: { GoodsList }, data() { return { orderList: [] }; }, methods: { handleSelect(item) { console.log(选择了, item); } } }; /script/components/goods-list/goods-list.vue是路径别名指向项目src/components目录在jsconfig.json或vite.config.js里配置。看到这样的路径说明项目已经避免了../../这种难以维护的相对路径引用。如果页面属性传参时发现子组件不更新先检查父组件里items有没有做深拷贝小程序端对直接在原数组上 push 或 splice 的响应式追踪并不总是可靠配合this.$set修改数组元素索引会更稳。2.2 JavaScript 逻辑层46 个 JS 文件在生命周期里承担什么微信小程序把逻辑层和视图层分开uniapp 在编译时会把 Vue 页面转换成小程序原生页面。每个 Vue 页面里的生命周期会映射到小程序生命周期onLoad对应页面创建onShow对应页面从后台切回前台onReady对应首次渲染完成。初版项目里 46 个 JS 文件大部分就是用来承载这类页面级逻辑的。一个常见的用户列表加载逻辑写在onLoad里export default { data() { return { list: [], page: 1, loading: false }; }, onLoad() { this.fetchList(); }, async fetchList() { if (this.loading) return; this.loading true; try { const res await request.get(/api/orders, { page: this.page }); this.list this.page 1 ? res.data : this.list.concat(res.data); } finally { this.loading false; } } };这里的request.get是封装后的异步请求方法后面会展开讲。需要注意的是小程序逻辑层的data并不是浏览器里的 DOM 对象uniapp 会通过 diff 把this.list的修改同步到视图层。如果直接修改数组下标比如this.list[0].name x视图可能不刷新应该用this.$set(this.list, 0, { ...this.list[0], name: x })。初版项目里最容易踩的响应式坑就在这里尤其是从数组末尾追加数据后页面经常出现数据变了但 UI 不动的现象。2.3 SCSS 与 uni.scss一套样式如何适配多端初版项目里 10 个 SCSS 文件大概率包含uni.scss、common.scss或variables.scss。uni.scss是 uniapp 的特殊文件它会被自动注入到每个 Vue 组件的style langscss中不需要手动import。所以在uni.scss里定义主题变量所有组件都能直接使用这是 uniapp 在样式层面对 Vue 单文件组件最重要的增强。共享样式文件可以包含常用的混合宏和覆盖小程序默认样式例如// uni.scss 中的主题变量与通用 mixin $theme-color: #2b85e4; $bg-color: #f5f5f5; $font-size-sm: 24rpx; $font-size-base: 28rpx; mixin text-ellipsis($lines: 1) { overflow: hidden; text-overflow: ellipsis; if $lines 1 { white-space: nowrap; } else { display: -webkit-box; -webkit-line-clamp: $lines; -webkit-box-orient: vertical; } }在页面中使用时$theme-color、$font-size-base这些变量直接生效不需要重复 import。如果想让 H5 和微信小程序表现一致避免使用 Chrome 才支持的 CSS 属性rpx 单位会由 uniapp 按设计稿宽度自动换算。初版项目里的 11 张 PNG 多用于 tab 图标或默认展示位替换时注意保持设计稿中的图标标准避免被小程序自动缩放导致边缘模糊。敲代码时我习惯在uni.scss里把主题色、主字号、间距统一成一到两组变量改视觉风格时只动这一份文件比全局搜索替换颜色值可靠得多。3. 配置层实战pages.json、manifest.json 与代码规范3.1 pages.json 的页面注册与顶部导航栏高度适配pages.json 是整个 uniapp 项目的路由和窗口配置中心。初版源码里 3 个 JSON 文件中pages.json 决定了小程序有哪些页面、每个页面的标题和导航样式。微信小程序运行时顶部导航栏的高度受机型状态栏高度影响如果初版使用了自定义导航就需要在代码里动态计算而不是写死一个像素值。一个典型的页面配置片段长这样{ pages: [ { path: pages/index/index, style: { navigationBarTitleText: 首页, navigationBarBackgroundColor: #2b85e4, navigationBarTextStyle: white } } ], globalStyle: { navigationBarTextStyle: black, navigationBarTitleText: 初始小程序, navigationBarBackgroundColor: #ffffff, backgroundColor: #f5f5f5 }, tabBar: { list: [ { pagePath: pages/index/index, text: 首页 }, { pagePath: pages/mine/mine, text: 我的 } ] } }pages数组的第一项就是小程序启动后展示的首页。如果需要修改刚进入的加载页面直接把这一项的 path 换掉。tabBar的pagePath必须与pages中的 path 完全一致否则微信开发者工具会直接报错这个错误在初版项目里非常常见。对自定义导航栏需要获取状态栏高度和右上角胶囊按钮位置const systemInfo uni.getSystemInfoSync(); const menuButton uni.getMenuButtonBoundingClientRect(); export function getNavBarHeight() { const statusBarHeight systemInfo.statusBarHeight || 20; const capsuleHeight menuButton.height || 32; const marginTop menuButton.top - statusBarHeight; return { statusBarHeight, navBarHeight: capsuleHeight marginTop * 2, menuButton }; }获取后把navBarHeight设置为页面自定义导航容器的height把statusBarHeight设置为安全区顶部占位高度才能在 iPhone 14 Pro 和普通 Android 机型上对齐返回按钮与胶囊位置。很多初版项目在这里直接写死44px换一批机型就会出现顶部大块空白或按钮被状态栏遮挡。提示uni.getMenuButtonBoundingClientRect在微信小程序端有效H5 端会返回空对象使用前要做兼容判断避免页面在浏览器打开时 js 报错。3.2 manifest.json 配置微信小程序 AppID 与权限声明manifest.json 是 uniapp 的项目配置文件负责告诉编译器当前应用面向哪些平台、采用什么 AppID、需要哪些原生权限。初版源码下载后manifest.json里mp-weixin节点默认是空 appid需要改成自己在微信公众平台申请的小程序 AppID否则运行到微信开发者工具时会提示 invalid appid。{ name: 初版小程序, appid: , mp-weixin: { appid: wx1234567890abcdef, setting: { urlCheck: false }, usingComponents: true, permission: { scope.userLocation: { desc: 用于展示附近门店的位置信息 } } } }urlCheck: false会跳过微信的合法域名校验方便开发环境连本地后端上线前必须改为 true并在微信公众平台配置 request 合法域名。permission字段的desc会出现在微信的位置授权弹窗里初版项目如果涉及定位、相册这类隐私接口提前把文案写清楚否则真机弹窗会显示默认说明容易被用户拒绝授权。很多开发者在这里只顾着填 appid忽略 permission 描述结果上线后用户反馈定位失败。3.3 .eslintignore 与 ESLint初版项目也能做静态检查项目里带着.eslintignore和.eslintrc说明作者在初版阶段就引进了代码规范校验。.eslintignore通常用来排除构建产物和第三方库比如dist unpackage node_modules *.min.js有了这个文件npm run lint时 ESLint 不会去检查编译输出的unpackage目录避免每次构建后产生海量误报。如果初版项目是用 vue-cli 创建的 uniapp 工程package.json里可能已经配好了 lint 脚本npm run lint我在处理这类源码时会先跑一次 lint把 no-unused-vars、undef 这类能自动修复的问题一次性清理掉再开始改业务代码。ESLint 报的错不要直接用// eslint-disable压掉尤其是组件引入后未使用、console.log残留这类的 warning会直接影响后续别人接手时对代码质量的判断。初版源码自带 ESLint比大多数从模板生成后就没跑过 lint 的项目要可靠。4. 登录、分享、拖拽与保存小程序功能逻辑怎么落地4.1 用 code 换 token封装 request 的完整链路微信小程序不能直接拿到用户身份常规流程是uni.login拿到临时code把code发给后端后端用 code 换 openid 和 session_key再返回业务 token。初版源码里如果没有这个能力我也会建议先把它补上因为几乎所有业务请求都要在 header 里带 token。常见做法是封装一个request.js统一处理 baseURL、header、401 刷新逻辑// utils/request.js const BASE_URL https://api.example.com; let isRefreshing false; let waitQueue []; function refreshToken() { // 用旧 token 换取新 token具体接口由后端定义 return new Promise((resolve, reject) { uni.request({ url: ${BASE_URL}/auth/refresh, method: POST, data: { refreshToken: uni.getStorageSync(refreshToken) }, success: (res) { uni.setStorageSync(token, res.data.token); resolve(res.data.token); }, fail: reject }); }); } function request(options) { return new Promise((resolve, reject) { uni.request({ url: ${BASE_URL}${options.url}, method: options.method || GET, data: options.data || {}, header: { Authorization: Bearer ${uni.getStorageSync(token)}, ...options.header }, success: (res) { if (res.statusCode 401) { if (!isRefreshing) { isRefreshing true; refreshToken() .then((token) { isRefreshing false; waitQueue.forEach((cb) cb(token)); waitQueue []; resolve(request(options)); }) .catch((err) { isRefreshing false; uni.navigateTo({ url: /pages/login/login }); reject(err); }); } else { waitQueue.push(() { resolve(request(options)); }); } } else { resolve(res.data); } }, fail: reject }); }); } export default { get: (url, data) request({ url, method: GET, data }), post: (url, data) request({ url, method: POST, data }) };这段代码最关键的不是uni.request本身而是 401 之后的等待队列。如果同时发出三个请求三个都返回 401只刷新一次 token其余请求等待新 token 后重放避免后端的 refresh token 被并发刷新失效。Authorization: Bearer ...是常见的认证头格式如果你的后端使用自定义 header比如X-Token把这里替换成X-Token: ${token}即可。真实项目中还需要给request增加重试次数标记防止登录态失效后刷新令牌仍然 401造成死循环。至于从 code 换 token可以把它放在登录页面里调用uni.login({ provider: weixin, success: async (loginRes) { const res await request.post(/auth/login, { code: loginRes.code }); uni.setStorageSync(token, res.data.token); uni.setStorageSync(refreshToken, res.data.refreshToken); } });provider在微信小程序里固定为weixin。其他端登录方式不同但uni.login的 API 基本保持一致所以这段逻辑也可以复用到 App 端。4.2 自定义分享好友onShareAppMessage 与生命周期绑定小程序右上角菜单会默认带“转发”按钮但分享出去的卡片标题、图片、路径都要自定义。在 uniapp 的 Vue 页面里直接写onShareAppMessage生命周期即可不需要从 Vue methods 里调用。export default { data() { return { shareLink: /pages/index/index?id1024 }; }, onShareAppMessage() { return { title: 这个初版小程序有点意思, path: this.shareLink, imageUrl: /static/share-card.png }; }, onShareTimeline() { return { title: 分享到朋友圈的标题 }; } };path必须是标准小程序页面路径如果带参数要放在?后面。imageUrl建议使用 5:4 比例的图片微信会按卡片比例裁剪如果图片比例不对会被剪掉关键内容。设置了onShareTimeline后用户还能分享到朋友圈这是两个独立生命周期都要写在同一页面才能同时生效。初版项目如果分享按钮是自定义的记得在按钮上绑定open-typeshare否则点击不会触发转发。4.3 长按拖拽滚动与图片保存到 wx.env.user_data_path小程序里长按拖拽排序是高频需求uniapp 最直接的做法是用movable-area和movable-view。movable-view需要设置directionall并绑定x、y来控制位置。如果只是列表内长按拖拽我会用longpress进入拖拽态再用touchmove计算偏移template movable-area classdrag-area movable-view v-for(item, index) in items :keyitem.id classdrag-item directionall :xitem.x :yitem.y touchstartonTouchStart(index) longpressstartDrag(index) touchmoveonDragMove(index, $event) touchendendDrag {{ item.name }} /movable-view /movable-area /templatemethods: { startDrag(index) { this.draggingIndex index; this.startX this.items[index].x; this.startY this.items[index].y; }, onTouchStart(event) { this.startPageX event.touches[0].pageX; this.startPageY event.touches[0].pageY; }, onDragMove(index, event) { if (this.draggingIndex ! index) return; const dx event.touches[0].pageX - this.startPageX; const dy event.touches[0].pageY - this.startPageY; this.$set(this.items, index, { ...this.items[index], x: this.startX dx, y: this.startY dy }); }, endDrag() { this.draggingIndex null; } }movable-view在真机上表现与开发者工具不完全一致尤其是嵌套 ScrollView 时纵向滚动手势会冲突。实际落地时更稳的方案是用longpress配合uni.createSelectorQuery计算当前触摸位置与列表项坐标然后交换数组顺序而不是依赖movable-view的绝对坐标。图片保存到本地需要用到wx.env.user_data_path这个路径表示小程序内部用户文件目录适合存放临时下载的附件。用 uniapp 封装一段保存到用户目录的代码export function downloadFileAndSave(url) { return new Promise((resolve, reject) { uni.downloadFile({ url, success: (res) { if (res.statusCode ! 200) { reject(new Error(下载失败)); return; } uni.saveFile({ tempFilePath: res.tempFilePath, success: (saveRes) { resolve(saveRes.savedFilePath); }, fail: reject }); }, fail: reject }); }); }uni.downloadFile下载的是临时文件临时目录会被系统清理uni.saveFile保存后的文件会长期存在于wx.env.user_data_path下。如果只是想临时查看只调用uni.downloadFile并拿到tempFilePath即可不用落盘。这个接口在小程序端有路径限制H5 端则可以用URL.createObjectURL临时生成地址同样一段代码不要期待两端行为完全一致。5. 从 uni 到微信开发者工具编译差异与踩坑验证5.1 H5 与小程序构建差异template.h5.html 与播放 m3u8源码里的template.h5.html是 uniapp 项目跑在浏览器端时的 HTML 模板里面通常只有div idapp/div和引入编译后的 JS/CSS 的容器。小程序端并不使用这个文件修改 H5 的 title、favicon 都在这里与小程序无关。如果页面里有视频播放比如 vue 播放 m3u8 流在小程序端要使用video组件并设置src为 m3u8 地址H5 端则可以用原生video或 vue-video-player因为 m3u8 在 iOS Safari 下原生支持Android Chrome 不一定。uniapp 对这些平台差异不会自动抹平所以在写播放器组件时需要判断平台// #ifdef MP-WEIXIN const inWeixin true; // #endif // #ifdef H5 const inWeixin false; // #endif条件注释是 uniapp 特有的预处理写法编译到对应平台时未命中的代码会被剔除。对初版项目来说不要试图在公共组件里做太多平台兼容优先保证微信小程序端正常H5 端出问题再用条件编译单独修。5.2 运行到微信开发者工具时的常见报错排查初版源码下载后最怕运行不起来。先在项目根目录安装依赖并启动编译npm install npm run dev:mp-weixin编译成功后用微信开发者工具导入项目目录选择dist/dev/mp-weixin。常见的报错有现象排查方向app.json: 未找到开发者工具导入目录选错了应该选dist/dev/mp-weixininvalid appidmanifest.json 中 mp-weixin.appid 未配置请求地址不在合法域名列表开发者工具右上角详情勾选“不校验合法域名”组件引入后白屏检查页面路径是否以pages/开头文件大小写是否一致样式错乱、宽高异常检查 scoped 样式和 rpx 是否被误写成 px构建后布局异常检查 manifest.json h5 节点配置和 publicPath 路径这些坑普遍存在于初版项目里。尤其页面白屏不是编译失败而是路径或组件名大小写不匹配微信开发者工具比 uniapp 的编译器更严格。5.3 初版改造前先做的三件事第一把package.json里的 script 全部读一遍。dev:mp-weixin、build:mp-weixin、lint这些命令决定了当前工程的开发姿势。初版项目往往没有 README 说明这时要自己补一份把启动命令、后端接口地址、测试账号写进去。第二确认是 vue2 还是 vue3 的 uniapp 工程。如果main.js用的是Vue.use(...)是 vue2 风格如果用的是createSSRApp是 vue3 风格。vue2 转 vue3 时Filters 会被移除$on、$off等实例方法不再支持组件选项式 API 改成组合式 API 需要逐个文件处理。初版项目如果已经是 vue3后续维护会更轻松。第三给项目建立 git 基线。先把.eslintignore、manifest.json里的敏感配置检查一遍然后执行git init git add . git commit -m chore: import initial version这一步不花多少时间但能让你接下来的所有改动都有回退点。如果你拿到的是改造任务优先改pages.json里的启动页和manifest.json里的微信小程序 AppID而不是急着调样式。初版源码的可读性通常还在先跑通编译、再改业务比从头写一个工程更节省时间。本文还有配套的精品资源点击获取