简介:这份小程序商城源码包面向计算机相关专业学生、课程设计与毕业设计参与者,以及希望学习微信小程序商城完整实现思路的初中级前端人员,可用于搭建商品展示、购物车、结算、订单与用户管理等核心业务场景。压缩包共68个文件,约3.32MB,主要由30个png与4个jpg界面素材、9个js业务脚本、7个wxss样式、7个wxml页面结构及7个json配置文件组成,兼顾页面资源、交互逻辑与配置说明。已有651人学习,说明其作为教学案例与项目参考具有一定认可度。读者可据此梳理小程序商城的前后端职责划分、页面组织方式与常见业务模块,借助现成素材和源码理解商品管理、支付对接、营销活动及客服入口等设计,并在此基础上完成二次开发、课程作品改造或毕业设计功能补全。
1. 小程序商城源码的第一次拆箱:一套能跑的前后端工程长什么样
拿到一个写着「小程序-商城(源码)」的压缩包,解压之后十有八九是两个目录:一个塞满 pages 和 components 的小程序前端,一个带 package.json 或者 index.php 的后端服务。新手最容易翻车的地方,是直接把整个解压目录拖进小程序开发者工具,然后被一连串「找不到 app.json」的报错劝退——因为工具只认前端那一层的根目录。这类小程序商城源码一般覆盖商品列表、分类、商品详情、购物车、下单和个人中心,适合做课程设计、毕业设计,也适合想把电商链路完整走一遍的人。先分清前端入口和后端入口在哪,再决定怎么跑,能省下大半天。
2. 技术选型与目录结构:原生小程序前端配 Node 后端的双端布局
2.1 前端选型:原生小程序与 uni-app 的差别
判断一套源码用的是哪种前端方案,不用翻文档,看根目录三个文件就够了。有app.json、app.wxss、project.config.json的,基本是原生小程序写法,页面用 WXML + WXSS + JS + JSON 四件套;有pages.json、manifest.json、main.js的,是 uni-app,用 Vue 语法编译到多端。两者在运行方式上差别不大,但改起来手感完全不同:原生方案的组件和生命周期是平台原生的,uni-app 那套更接近写 Vue 项目。
课程设计类源码里原生方案占比更高,原因也简单——不用装 HBuilderX,不用配编译链,开发者工具打开就能跑,交作业时评阅人也不用折腾环境。下面这张表是我自己判断时的对照,实际拆包时按它扫一眼就能定性:
| 判别文件 | 指向方案 | 编译方式 | 改造成本 |
|---|---|---|---|
app.json+app.wxss | 原生小程序 | 无,工具直读 | 低,改完即生效 |
pages.json+manifest.json | uni-app | 需 cli 或 HBuilderX | 中,要重新编译 |
app.js里有App({}) | 原生小程序 | 无 | 低 |
main.js里有createApp | uni-app | 需编译 | 中 |
选型上没有绝对优劣。原生方案胜在零构建、调试直观,热重载是工具原生支持的;uni-app 胜在能一套代码编译到多个端,如果后续想加 H5 版本,省一次重写。但就毕业设计这个场景,原生方案更稳,因为少一层编译,出错时日志直接指到源码行号,不用猜是哪一步转换出的问题。
2.2 后端选型:Express + MySQL 的经典组合
后端这块,课程设计和毕业设计的商城源码里出现频率最高的是 Node.js + Express + MySQL,其次是 PHP + ThinkPHP,再次是 Java + Spring Boot。判别方法同样简单:根目录有package.json且依赖里能看到express、mysql2的,就是 Node 方案;有composer.json的是 PHP;有pom.xml的是 Java。Node 方案对新手最友好,因为前端本来就是 JS,不用再学一门语言,接口调试也能用同一套工具。
我一般会先看后端入口文件,常见命名是app.js、server.js或index.js,里面通常能看到端口监听和路由挂载这两段。再往下看routes/目录,商品、购物车、订单、用户这几个路由文件如果都在,说明这套后端是完整链路,不是只给前端做样子的 mock。真正需要留神的是数据库连接配置,它经常被硬编码在某个db.js或config.js里,账号密码是作者本地环境的,拿到手必须先改,不然一启动就是连接超时。
一个典型的后端目录大致长这样:
server/ ├── app.js # 入口,挂路由、起服务 ├── config/ │ └── db.js # MySQL 连接池配置 ├── routes/ │ ├── goods.js # 商品列表、详情 │ ├── cart.js # 购物车增删改查 │ ├── order.js # 下单、订单查询 │ └── user.js # 登录、用户信息 ├── controllers/ # 业务逻辑 ├── models/ # 表结构映射 ├── sql/ │ └── init.sql # 建表 + 初始数据 └── package.json这张目录树里最值得先打开的是sql/init.sql,它决定了数据库要建几张表、字段长什么样、有没有预置商品数据。很多源码跑不起来,问题就出在这份脚本没执行,或者执行时字符集没设对,中文商品名全变成问号。
2.3 一份可对照的目录树与关键文件说明
前端这一侧的目录结构,原生小程序基本是固定套路。pages/下按业务分文件夹,每个文件夹里四个同名文件;components/放复用组件,比如商品卡片、数量选择器;utils/放请求封装和格式化函数;images/或static/放静态资源。判断页面数量看app.json的pages数组长度就行,一个功能完整的商城通常在 10 到 15 个页面之间。
{ "pages": [ "pages/index/index", "pages/category/category", "pages/detail/detail", "pages/cart/cart", "pages/order/order", "pages/user/user" ], "window": { "navigationBarTitleText": "商城", "navigationBarBackgroundColor": "#ffffff" }, "tabBar": { "list": [ { "pagePath": "pages/index/index", "text": "首页" }, { "pagePath": "pages/category/category", "text": "分类" }, { "pagePath": "pages/cart/cart", "text": "购物车" }, { "pagePath": "pages/user/user", "text": "我的" } ] } }这段app.json里,pages数组的第一项就是启动页,改成别的页面就能换首页,调试时很实用。tabBar的list最多五项,每项必须对应pages里已注册的路径,少写一个就是白屏。window里的标题和颜色是全局默认值,单个页面可以在自己的 JSON 里覆盖。这些字段都不复杂,但改动时容易漏配,尤其是新增页面忘了往pages里加,工具会直接报「未找到页面」。
2.4 请求链路:前端怎么和后端对上
前端调后端靠的是一个统一的request封装,几乎所有商城源码都有这么一层,位置通常在utils/request.js。它的作用是把域名、超时、错误提示、token 注入这些重复逻辑收拢到一处,页面里只写接口路径和参数。下面这段是常见写法,做了三件事:拼接 baseUrl、带上登录态、统一处理非 200 的业务码。
// utils/request.js const BASE_URL = 'http://127.0.0.1:3000/api'; // 本地调试地址,上线要换成备案域名 function request(options) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data || {}, header: { 'content-type': 'application/json', 'Authorization': wx.getStorageSync('token') || '' // 每次请求带登录态 }, success(res) { if (res.statusCode === 200 && res.data.code === 0) { resolve(res.data.data); // 业务成功,直接抛数据 } else { wx.showToast({ title: res.data.msg || '请求失败', icon: 'none' }); reject(res.data); } }, fail(err) { wx.showToast({ title: '网络异常', icon: 'none' }); reject(err); } }); }); } module.exports = { request };这段封装里,BASE_URL是第一个要改的东西,本地跑就指向127.0.0.1,真机预览或部署后要换成后端实际地址。Authorization那行读取本地存的 token,登录页登录成功后写进去,退出时清掉,这是最简的登录态方案。code === 0是业务约定,有的源码用code === 200,改的时候要跟后端返回结构对齐,不然页面会一直提示请求失败。fail分支只提示网络异常,实际项目里通常会在这里补一个重试或跳登录的逻辑,课程设计阶段可以先不动。
3. 商品模块拆解:列表渲染、SKU 规格选择与图片加载的真实写法
3.1 商品列表页:分页加载与下拉刷新
商品列表是整个商城的门面,也是新手最先改的页面。它的核心逻辑就三件事:初始加载第一页、上拉加载下一页、下拉重置回第一页。很多源码把这三个状态塞在一个loadGoods函数里,用参数区分,读起来乱但能跑;我更倾向于拆成loadFirst和loadMore两个函数,边界清楚,后面加分页参数、加筛选条件都方便。
// pages/index/index.js Page({ data: { goodsList: [], page: 1, pageSize: 10, hasMore: true, loading: false }, onLoad() { this.loadFirst(); }, // 首屏加载,重置分页 loadFirst() { this.setData({ page: 1, hasMore: true, goodsList: [] }); this.fetchList(1); }, // 上拉加载,页码自增 loadMore() { if (!this.data.hasMore || this.data.loading) return; // 没有更多或正在加载时直接返回 this.fetchList(this.data.page + 1); }, fetchList(page) { this.setData({ loading: true }); const { request } = require('../../utils/request'); request({ url: '/goods/list', data: { page, pageSize: this.data.pageSize } }).then(list => { const next = page === 1 ? list : this.data.goodsList.concat(list); this.setData({ goodsList: next, page, hasMore: list.length === this.data.pageSize, // 不足一页说明到底了 loading: false }); }).catch(() => { this.setData({ loading: false }); }); }, // 下拉刷新在页面 JSON 里开启 enablePullDownRefresh 后触发 onPullDownRefresh() { this.loadFirst(); wx.stopPullDownRefresh(); } });这段逻辑里,hasMore的判断依据是「返回条数是否等于 pageSize」,这是最省事也最常见的一种分页到底判断,缺点是如果后端每次都返回固定条数、最后一页刚好满页,会多请求一次空列表,能接受但不完美。loading是防抖用的,上拉事件触发频率很高,不加这个标志会连续发好几个重复请求。onPullDownRefresh必须在页面 JSON 里开"enablePullDownRefresh": true才会触发,这一点经常被忘,表现为「下拉没反应」,其实不是代码问题。
页面上就是scroll-view或者普通view配合wx:for渲染,商品卡片通常抽成组件,避免首页和分类页各写一份。渲染时要注意wx:key必须写且唯一,一般用商品 id,不写会警告,写了重复 id 会导致列表更新错位。
3.2 商品详情与 SKU 规格选择
商品详情页的难点不在展示,在 SKU 选择。单规格商品简单,一个价格一个库存;多规格商品就要处理规格组合、库存联动、价格变化。课程设计类源码常见两种实现:一种是笛卡尔积生成所有组合,选中规格后查对应组合;另一种是两层循环,规格一选完再筛规格二。前者代码量大但可扩展,后者写起来快但加第三个规格就崩。
我一般会要求数据结构里 SKU 是独立的一张表或一个数组,每项带specs、price、stock、skuId。选中规格时用specs做匹配,而不是靠下标拼字符串,因为下标顺序一变就全错。
// 根据已选规格拼出当前 SKU // specs: ['红色', 'XL'] selected: { 颜色: '红色', 尺码: 'XL' } function matchSku(skuList, selected) { return skuList.find(sku => { // sku.specs 形如 ['红色', 'XL'],顺序必须和规格名顺序一致 return Object.values(selected).every((val, idx) => sku.specs[idx] === val); }); } // 页面里选择规格时的处理 onSelectSpec(e) { const { name, value } = e.currentTarget.dataset; const selected = { ...this.data.selected, [name]: value }; const sku = matchSku(this.data.skuList, selected); this.setData({ selected, currentPrice: sku ? sku.price : this.data.basePrice, currentStock: sku ? sku.stock : 0, canBuy: !!(sku && sku.stock > 0) }); }matchSku依赖Object.values的顺序和sku.specs的顺序一致,这是一个隐性约定。稳妥点的做法是让每个 SKU 带一个specMap,用对象比较而不是数组下标,但代码会多个几行。selected用对象存,键是规格名如「颜色」「尺码」,值是选中的规格值,这样加规格不用改结构。canBuy控制加购按钮是否可用,库存为 0 时置灰,这个细节很多源码没做,用户点进去才发现买不了。
规格数据的来源通常是后端详情接口一次返回,包含specList和skuList两部分。如果源码里只有specList没有skuList,那大概率是单规格商品,价格库存直接挂在商品上,这种情况下页面逻辑可以砍掉一大半。
3.3 商品图片的加载策略
商城页面图多,图片处理不好,首屏能卡到怀疑人生。源码里常见的原始图路径直接塞进image标签,一张图两三兆,列表十条就是几十兆。合格的开发会在上传阶段就做压缩,或者在图片地址后面拼参数让图床返回缩略图。小程序这边image组件本身支持mode和lazy-load,用好了能省不少流量。
<!-- 列表里的商品图,开启懒加载,指定裁剪模式 --> <image src="{{item.cover}}" mode="aspectFill" lazy-load="{{true}}" class="goods-cover" />mode="aspectFill"保证图片按比例填满容器、超出部分裁掉,比默认的scaleToFill拉伸变形好看。lazy-load只在scroll-view或页面滚动到可视区时才真正加载,列表长的时候效果明显。真正要命的是图片本身太大,这两个属性救不了,还是得在上传或图床层做处理。我自己的做法是后端存原图、返回时拼一个缩略参数,比如?w=400,前端列表用小图、详情用大图,同一张图两个尺寸。
另外要注意image的默认宽高是 320×240,不写样式就按这个来,很多页面错位就是忘了给宽高。用aspectFill时容器高度必须定死,不然图片会把容器撑开,滚动时视觉跳动。
4. 购物车到订单:本地状态管理、库存扣减与支付回调的状态机
4.1 购物车数据结构与本地持久化
购物车这块,源码里常见两种存法:一种是纯前端存wx.setStorage,下单时再提交;另一种是登录后同步到后端,换设备也在。课程设计为了简单多用前者,但会带来一个问题——用户清缓存购物车就空了。判断源码用哪种,看加购那段代码有没有调接口,只操作this.data.cart的就一定是本地存。
本地购物车的数据结构要设计好,不然加减数量和勾选逻辑会写得很痛苦。我一般用商品 id 加规格做联合键,每项存商品快照、数量、是否勾选、规格信息,这样商品下架或改价时还能显示加购时的信息。
// 购物车项结构示例 const cartItem = { key: '1001_红色_XL', // 商品id + 规格,唯一标识 goodsId: 1001, name: '示例商品', cover: '/images/demo.png', price: 99.00, stock: 20, count: 1, checked: true, specs: ['红色', 'XL'] }; // 加购:已存在则数量+1,不存在则追加 function addToCart(cart, item) { const idx = cart.findIndex(c => c.key === item.key); if (idx > -1) { cart[idx].count += item.count; } else { cart.push(item); } wx.setStorageSync('cart', cart); // 每次变更都落盘,防止页面切换丢失 return cart; }用key而不是goodsId做唯一键,是因为同一商品不同规格是不同的购物车项,只用goodsId会合并错。每次变更都写一次setStorageSync看起来笨,但商城购物车操作频率不高,这个开销可以忽略,换来的是页面来回跳不丢数据。真正要注意的是数量上限,加减时得跟stock比对,超过库存就提示,别等下单才报错。
4.2 下单接口与库存扣减
下单是整条链路里最容易出问题的一步,因为它同时动了订单表和库存表。源码里如果写成「先查库存、判断够不够、再减库存、再插订单」四步分离,那基本就是没考虑并发,两个人同时买最后一件会都成功。正确做法是把扣减和判断合成一条带条件的 UPDATE,靠数据库的行锁保证原子性。
-- 扣减库存:库存足够才减,返回影响行数判断是否成功 UPDATE goods_sku SET stock = stock - ? WHERE sku_id = ? AND stock >= ?; -- 插入订单主表 INSERT INTO orders (order_no, user_id, total_amount, status, created_at) VALUES (?, ?, ?, 'pending', NOW()); -- 插入订单明细 INSERT INTO order_item (order_id, sku_id, price, count) VALUES (?, ?, ?, ?);这三条语句必须放在同一个事务里,第一条的stock >= ?是关键,它让「判断」和「扣减」变成一次原子操作,影响行数为 0 就说明库存不足,直接回滚。status初始值设成pending,支付成功后再改成paid,这个状态字段后面会一直用到。订单号order_no一般用时间戳加随机数生成,别用自增 id 直接暴露给前端,不然订单量一目了然。
事务代码在后端通常长这样:开事务、执行三条 SQL、任何一步失败就rollback、全成功再commit。Node 里如果用mysql2,就是conn.beginTransaction()和conn.commit()配对。这里最常见的错误是忘了在catch里rollback,异常一抛连接不释放,跑几次就把连接池耗尽了。
4.3 支付回调与订单状态流转
小程序的支付流程是:前端调下单接口拿到支付参数,拉起支付,用户付款后平台回调后端,后端验签、改订单状态。课程设计阶段经常没有真实商户号,源码里要么把支付做成模拟的,要么留一个notify接口等着接。不管哪种,订单状态机得先理清楚,不然改支付方式时到处补丁。
| 状态值 | 含义 | 触发动作 | 可跳转到 |
|---|---|---|---|
pending | 待支付 | 创建订单 | paid/cancelled |
paid | 已支付 | 支付回调成功 | shipped/refunded |
shipped | 已发货 | 后台发货 | completed |
completed | 已完成 | 用户确认收货 | 无 |
cancelled | 已取消 | 超时未付或主动取消 | 无 |
回调接口要做三件事:验签、幂等、改状态。验签是防伪造,幂等是防重复回调——支付平台可能因为网络问题重发通知,同一个订单号处理两次不能扣两次库存。
// 支付回调处理(简化版) async function handlePayNotify(payload) { // 1. 验签,失败直接返回,不处理 if (!verifySign(payload)) return { code: 'FAIL', msg: 'sign error' }; const { orderNo, tradeNo } = payload; // 2. 幂等:已处理过的订单直接返回成功 const order = await db.query('SELECT status FROM orders WHERE order_no = ?', [orderNo]); if (!order.length) return { code: 'FAIL', msg: 'order not found' }; if (order[0].status !== 'pending') return { code: 'SUCCESS' }; // 3. 改状态 + 记流水,放在一个事务里 await db.transaction(async conn => { await conn.query('UPDATE orders SET status = ?, trade_no = ? WHERE order_no = ? AND status = ?', ['paid', tradeNo, orderNo, 'pending']); await conn.query('INSERT INTO pay_log (order_no, trade_no, amount, created_at) VALUES (?, ?, ?, NOW())', [orderNo, tradeNo, payload.amount]); }); return { code: 'SUCCESS' }; }这里UPDATE ... WHERE status = 'pending'又是一次条件更新,它同时承担了幂等判断,比先查再改更稳。verifySign的具体实现跟支付渠道有关,源码里通常有对应工具函数,改的时候别自己重写。返回给支付平台的成功标识必须是它要求的格式,写错了平台会一直重发,日志里全是重复回调,看着像 bug 其实是格式问题。
5. 跑起来与排查:开发者工具导入、数据库初始化与五类常见翻车点
5.1 导入开发者工具与基础配置
前端跑起来的第一步是导入正确的目录。前面说过,开发者工具要的是前端那层根目录,不是解压后的总目录,判断标准是这一层能看到app.json。导入时填的 AppID 可以选「测试号」,课程设计阶段不需要真实注册,测试号能跑除支付外的绝大部分功能。
导入后先别急着编译,打开project.config.json看两眼。miniprogramRoot字段如果指向一个子目录,说明前端代码在更深一层,工具会自动按这个路径找;setting里的urlCheck建议关掉,否则本地127.0.0.1的接口会被域名校验拦下来,报「不在合法域名列表」。
{ "miniprogramRoot": "miniprogram/", "setting": { "urlCheck": false, "es6": true, "postcss": true }, "appid": "touristappid" }urlCheck关掉只对开发阶段有效,真机预览和上线还是得配合法域名,这一步绕不过去。es6和postcss保持开启,代码里用了箭头函数、模板字符串、新 CSS 属性都靠它转译。appid填touristappid就是测试号,换成自己的也行,但没必要。
前端配置改完,回到utils/request.js把BASE_URL指到本地后端端口,一般是http://127.0.0.1:3000/api,端口以app.js里listen的为准。这两处对不上,页面就是一片空白加请求失败提示。
5.2 后端启动与数据库初始化
后端启动顺序不能乱:先建库建表,再装依赖,最后起服务。建表脚本在sql/init.sql,用命令行或者数据库客户端执行都行,关键是字符集要选utf8mb4,不然中文和 emoji 都会出问题。
# 1. 创建数据库并导入表结构 mysql -u root -p -e "CREATE DATABASE mall DEFAULT CHARACTER SET utf8mb4;" mysql -u root -p mall < sql/init.sql # 2. 安装依赖 cd server && npm install # 3. 改数据库连接配置后启动 node app.jsCREATE DATABASE那条里的utf8mb4别省,默认字符集在某些版本里是latin1,导入中文直接乱码。npm install如果卡住,换国内镜像源,这个不算敏感操作,纯粹是网络问题。启动成功的标志是控制台打出监听端口,看到这行再去点前端页面,顺序反了会以为是接口没写。
数据库连接配置一般在config/db.js,四个字段:host、user、password、database。host 本地就是127.0.0.1,password 填自己 MySQL 的密码,database 填刚建的库名。这四项错任何一项,启动时报的错误都不一样,连接被拒是密码错,库不存在是库名错,超时是 host 或端口错,按错误信息对号入座比瞎试快。
5.3 五类常见翻车点:现象、原因、解决
第一类:页面白屏,控制台报「未找到 app.json」。现象是导入后编译直接失败,什么都不显示。原因几乎都是导入目录选错,选到了压缩包解压后的总目录,而app.json在下一层。解决办法是重新导入,目录选到能看到app.json的那一层,或者在project.config.json里把miniprogramRoot指对。
第二类:接口全部 404 或跨域报错。现象是页面能渲染,但商品列表是空的,Network 里请求全是红叉。原因有两个:一是BASE_URL的端口和后端实际监听端口不一致,二是开发者工具没关域名校验。先核对端口,再看project.config.json里urlCheck是不是false,两步都不行就看后端路由前缀,有的源码路由挂的是/api,有的没挂,request.js里要对应调整。
第三类:中文商品名显示成问号或乱码。现象是列表能出来,但标题是一堆???。原因是建库时字符集用了默认值,或者导入 SQL 时没指定编码。解决是把库和表都改成utf8mb4,重新导入一次脚本;已经导过的可以用ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4补救,但初始数据得重导。
第四类:下单成功但库存没变,或者库存扣了订单没生成。现象是点了下单,提示成功,回头一看数据对不上。原因是下单那几条 SQL 没放进同一个事务,中间某步失败没有回滚。解决是检查后端下单逻辑有没有beginTransaction和commit配对,catch里有没有rollback,缺一个就会出现半成功状态。这类问题在单机测试时不容易发现,并发一压就暴露。
第五类:真机预览时图片不显示、接口请求失败。现象是工具里一切正常,扫码到手机上就残了。原因是真机访问不到127.0.0.1,那是手机自己的本地环回地址,不是电脑的。解决是把BASE_URL改成电脑在局域网里的 IP,手机和电脑连同一个网络,同时后端要监听0.0.0.0而不是127.0.0.1,否则外部设备连不进来。图片如果是本地路径,真机上也要确认路径是相对小程序根目录的,不能带盘符。
6. 二次开发改造:从课程设计作业到能交付的小商城
源码跑通只是起点,真正花时间的是改。我拿这类商城练手时,改造顺序一般是「先换皮、再补功能、最后加校验」,每一步都能独立验证,不至于一次改太多导致全线崩。
换皮是最容易见效的一步。把app.wxss里的主色变量改掉,商品卡片圆角和阴影调一调,tabBar 图标换一套,整个项目的观感就变了。这一步不用碰逻辑,改完编译看一眼就行,翻车概率低。但要注意rpx和px别混用,源码里如果全是rpx,你新加的样式也跟着用rpx,混着写在不同机型上缩放比例会不一致。
补功能建议从「后台管理」切入。课程设计的商城往往只有用户端,没有管理端,商品是写死在数据库里的。加一个简单的管理页面,能增删改商品、能看订单列表,这个项目就从「演示」变成「能用」了。实现上不用另起前端,直接在现有后端加一组带权限校验的接口,前端加几个页面,复用已有的request封装即可。
加校验是最后一步,也是最容易被跳过的一步。前端要校验数量不能为负、不能超库存、必填项不能空;后端要校验参数类型、权限归属、金额是否被篡改。尤其是金额,绝对不能信前端传过来的总价,必须后端按商品单价和数量重新算一遍,这一步不做,改个请求参数就能一分钱下单。
验证方法我一般分三层:单页面手动点一遍主流程,从列表到详情到加购到下单;用工具构造异常请求,比如库存传负数、订单号传别人的,看后端拦不拦;最后清一次缓存、换一个账号,确认登录态和购物车隔离没问题。三层都过,才敢说这套改造是站得住的。
血泪经验是别在原始代码上直接改。我第一次拆这类源码时图省事,在原目录上动刀,改到一半想回退发现没有版本记录,只能重新解压一遍,之前调好的配置全丢。从那以后我每次拆新项目,第一件事就是git init提交一个原始版本,再拉一个dev分支,改坏了随时能对照原始代码看差异,这个习惯比任何调试技巧都值钱。希望帮到你。
本文还有配套的精品资源,点击获取