我用微信小程序做了一个工具箱:从 0 到上线的完整记录
没有用任何框架,纯原生 + npm 包,9 个实用工具,一套墨蓝仪表盘 UI。本文完整记录从需求分析到发布的全过程,附带核心代码。文末有体验入口,欢迎扫码试用。
为什么做这个小程序?
事情很简单——我做了 15 年前端开发,日常工作中总有一些小需求反复出现:
- 客户发来一段 JSON,需要快速格式化看看结构对不对
- 要把一个链接转成二维码,发到群里让别人扫
- 前端收到后端的时间戳,要看看对应什么时间
- 吃饭 AA 分账,算每人该出多少钱
这些需求不需要装 App,但每次都要打开某个网页、等加载、甚至有时候网络不好还打不开。
我就想:能不能在微信里一键解决? 不用下载、不用注册、用完即走——这正是微信小程序的天然优势。
于是就有了这个「瑞宸工具箱」。目前已经上线,包含 9 个工具,分 3 大类:
图像工具: 图片压缩、二维码生成
开发工具: 时间戳转换、JSON 格式化、Base64 编解码、MD5/SHA 哈希
其他工具: AA 分账、日期计算、单位换算
一、技术选型:为什么坚持用原生?
市面上做小程序的方案很多:Uni-app、Taro、Remax……但我最终选了纯原生微信小程序开发。
原因很实际:
- 工具类小程序没有复杂状态管理——不需要 Vuex/Redux 那套东西
- 每个工具页独立运行——页面之间几乎没有数据共享需求
- 性能优先——原生 Canvas 2D API 做二维码渲染,没有中间层损耗
- 包体积控制——没有框架运行时开销,整个小程序打包后非常轻量
项目结构也很干净:
workjob/
├── app.js # 最小入口
├── app.json # 页面注册 + TabBar 配置
├── app.wxss # CSS 变量设计令牌中心
├── pages/
│ ├── index/ # 首页(工具箱导航)
│ ├── resume/ # 关于我(个人主页)
│ ├── compress/ # 图片压缩
│ ├── qrcode/ # 二维码生成
│ ├── timestamp/ # 时间戳转换
│ ├── jsonfmt/ # JSON 格式化
│ ├── base64/ # Base64 编解码
│ ├── hash/ # MD5/SHA 哈希
│ ├── split/ # AA 分账
│ ├── datecalc/ # 日期计算
│ └── unitconvert/ # 单位换算
└── utils/
├── resume-data.js # 简历数据源(数据驱动)
└── weapp-qrcode.js # 二维码生成库
一个原则:每个工具一个页面目录,结构统一,维护成本极低。
二、UI 设计:墨蓝仪表盘风格
大多数工具箱小程序都长一个样——白底、圆角卡片、蓝色按钮。我想做点不一样的。
我的日常工作是在能源行业做前端,做的是充电站运营平台、物联网数据大屏。那种墨蓝底色 + 电光橙高亮的仪表盘界面看多了,觉得很酷——为什么工具箱不能是这个风格?
CSS 变量设计令牌
所有颜色集中在 app.wxss 里,用 CSS 变量管理:
page {
--ink: #1B2A4A; /* 墨蓝:标题、导航栏 */
--ink-2: #24365E; /* 墨蓝浅档 */
--paper: #F4F6F9; /* 冷调纸白:页面底色 */
--card: #FFFFFF; /* 内容卡片 */
--volt: #FF5A1F; /* 电光橙:唯一强调色 */
--volt-2: #FFB100; /* 琥珀:渐变起点 */
--mist: #8A94A6; /* 灰蓝:次要文字 */
--line: #E3E8EF; /* 分隔线 */
--font-num: "DIN Alternate", "Bahnschrift", monospace;
--font-body: -apple-system, "PingFang SC", sans-serif;
}
这样做的好处:
- 改一处,全局生效——要调色调只改变量
- 暗色模式好扩展——后面加
@media (prefers-color-scheme: dark)覆盖变量就行 - 团队协作友好——看到
--volt就知道是强调色,不用猜#FF5A1F是什么
字体分工
数字用 DIN Alternate(工业仪表盘感),中文用苹方/思源。在 CSS 里做了强制分工:
.din {
font-family: var(--font-num);
}
需要显示数字的地方套一个 .din 类,自动切到数字字体。
三、首页设计:数据驱动的导航
首页是用户进来的第一屏。我的想法是:分类清晰、一触即达、支持后续扩展。
数据结构
工具列表不走硬编码,而是用一个数组结构来渲染:
// pages/index/index.js
Page({
data: {
categories: [
{
title: '图像工具',
tools: [
{ name: '图片压缩', icon: '/images/tit1_ico1.png' },
{ name: '二维码生成', icon: '/images/tit1_ico2.png' }
]
},
{
title: '开发工具',
tools: [
{ name: '时间戳转换', icon: '/images/tit2_ico4.png' },
{ name: 'JSON 格式化', icon: '/images/tit2_ico5.png' },
{ name: 'Base64 编解码', icon: '/images/tit2_ico6.png' },
{ name: 'MD5/SHA 哈希', icon: '/images/tit2_ico7.png' }
]
},
{
title: '其他工具',
tools: [
{ name: 'AA分账', icon: '/images/tit2_ico1.png' },
{ name: '日期计算', icon: '/images/tit2_ico2.png' },
{ name: '单位换算', icon: '/images/tit2_ico3.png' }
]
}
]
}
})
新增工具只需要在 categories 数组里加一项,不需要改布局模板。
跳转逻辑
点击工具后,根据 name 判断跳转到哪个页面:
onToolTap(e) {
const name = e.currentTarget.dataset.name
const routeMap = {
'图片压缩': '/pages/compress/index',
'二维码生成': '/pages/qrcode/index',
'时间戳转换': '/pages/timestamp/index',
'JSON 格式化': '/pages/jsonfmt/index',
'Base64 编解码': '/pages/base64/index',
'MD5/SHA 哈希': '/pages/hash/index',
'AA分账': '/pages/split/index',
'日期计算': '/pages/datecalc/index',
'单位换算': '/pages/unitconvert/index',
}
if (routeMap[name]) {
wx.navigateTo({ url: routeMap[name] })
} else {
wx.showToast({ title: '敬请期待', icon: 'none' })
}
}
用路由映射表替代一长串 if-else,新增工具时只要在 routeMap 里加一行。
💡 实际代码中我最初用的是 if-else,后来重构成了映射表。这也是一个很实用的优化——当你发现条件分支超过 5 个,就该考虑用对象映射替代了。
底部 TabBar
两个 Tab:首页(工具箱) + 关于我(个人主页)。
{
"tabBar": {
"color": "#8A94A6",
"selectedColor": "#FF5A1F",
"backgroundColor": "#1B2A4A",
"list": [
{ "pagePath": "pages/index/index", "text": "首页" },
{ "pagePath": "pages/resume/resume", "text": "关于我" }
]
}
}
TabBar 底色用墨蓝,选中色用电光橙,跟整体风格统一。
四、核心工具开发实录
挑几个有代表性的工具,讲讲实现细节和踩坑。
工具 1:二维码生成 —— Canvas 2D 的坑
这是我最满意的一个工具。支持自定义颜色、Logo、一键保存。
技术难点: 微信小程序的 Canvas 经历过从 Canvas 1.0 到 Canvas 2.0 的大改。旧版 wx.createCanvasContext 已经废弃,新版要用 Canvas 2D 原生 API。
我用的 weapp-qrcode 库,但它的内部实现还是基于旧版 Canvas API。怎么兼容?
方案:拦截绘制指令,用新 API 重绘。
const QR = require('../../utils/weapp-qrcode.js')
Page({
onGenerate() {
const that = this
const query = wx.createSelectorQuery()
query.select('#qrCanvas').fields({ node: true }).exec((res) => {
if (!res || !res[0] || !res[0].node) {
wx.showToast({ title: 'Canvas 未就绪', icon: 'none' })
return
}
const canvas = res[0].node
const ctx = canvas.getContext('2d')
// 第一步:用假 ctx 收集所有绘制指令
const commands = []
let currentColor = that.data.backgroundColor
const fakeCtx = {
setFillStyle(c) { currentColor = c },
fillRect(x, y, w, h) {
commands.push({ x, y, w, h, color: currentColor })
},
drawImage(src, dx, dy, dw, dh) {
commands.push({ type: 'img', src, dx, dy, dw, dh })
},
draw(_, cb) { if (cb) cb() }
}
// 调用库生成 QR 矩阵(但不真正绘制)
QR({
ctx: fakeCtx,
width: size, height: size,
text: that.data.text.trim(),
correctLevel: 0,
foreground: that.data.foregroundColor,
background: that.data.backgroundColor,
image: that.data.hasLogo ? that.data.logoPath : ''
})
// 第二步:用 Canvas 2D API 真正重绘
canvas.width = size
canvas.height = size
ctx.fillStyle = that.data.backgroundColor
ctx.fillRect(0, 0, size, size)
for (const cmd of commands) {
if (cmd.type === 'img') {
// 处理 Logo 图片
const img = canvas.createImage()
img.onload = () => {
ctx.drawImage(img, cmd.dx, cmd.dy, cmd.dw, cmd.dh)
that._exportQR(canvas, size)
}
img.src = cmd.src
} else {
ctx.fillStyle = cmd.color
ctx.fillRect(cmd.x, cmd.y, cmd.w, cmd.h)
}
}
})
}
})
这个指令拦截 + 重绘的方案是核心亮点:
- 不修改第三方库源码
- 兼容新旧 Canvas API
- 可以任意扩展(比如加圆角、渐变、阴影)
配色预设: 我做了 6 套配色方案,用户可以一键切换:
const COLOR_PRESETS = [
{ fg: '#000000', bg: '#FFFFFF', label: '经典' },
{ fg: '#1B2A4A', bg: '#F4F6F9', label: '墨蓝' },
{ fg: '#FF5A1F', bg: '#FFFFFF', label: '电光橙' },
{ fg: '#000000', bg: '#FFB100', label: '琥珀' },
{ fg: '#3DDC84', bg: '#000000', label: '赛博' },
{ fg: '#6366F1', bg: '#FFFFFF', label: '紫罗兰' }
]
生成的二维码可以直接保存到手机相册,方便转发。
工具 2:JSON 格式化 —— 容错的艺术
开发者最常用到的工具。除了常规的美化和压缩,我加了一个关键特性:JS 对象语法兼容。
痛点: 开发中经常遇到这种情况:后端或同事发来的 JSON 是 JS 对象格式——单引号、无引号属性名、尾逗号,直接 JSON.parse 会报错。
// 这不是合法 JSON,但开发者经常遇到
{ name: 'test', tags: ['a', 'b',], }
解决方案: 四层降级解析
_format(beautify) {
const text = this.data.inputText.trim()
let obj = null
let usedFallback = false
// 第一层:标准 JSON.parse
try {
obj = JSON.parse(text)
} catch (e) {
// 第二层:JS 对象语法转 JSON 后再解析
try {
const jsonText = this._jsToJson(text)
obj = JSON.parse(jsonText)
usedFallback = true
} catch (e2) {
// 第三层:尝试用 {} 包裹后解析
try {
obj = JSON.parse('{' + text + '}')
usedFallback = true
} catch (e3) {
// 第四层:{} 包裹 + JS 转换
try {
obj = JSON.parse(this._jsToJson('{' + text + '}'))
usedFallback = true
} catch (e4) {
this.setData({ errorMsg: this._formatError(e.message, text) })
return
}
}
}
}
const output = beautify ? JSON.stringify(obj, null, 2) : JSON.stringify(obj)
// 渲染输出...
}
四层降级解析策略: 标准 JSON → JS 转换 → 自动包裹 → JS 转换 + 包裹。能处理绝大多数"不规范 JSON"的情况。
_jsToJson 转换逻辑:
_jsToJson(text) {
let s = text
// 移除注释
s = s.replace(/\/\/.*$/gm, '')
s = s.replace(/\/\*[\s\S]*?\*\//g, '')
// 单引号转双引号
s = s.replace(/'([^'\\]*(?:\\.[^'\\]*)*)'/g, '"$1"')
// 无引号属性名加双引号
s = s.replace(/([{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/g, '$1"$2":')
// 移除尾逗号
s = s.replace(/,\s*([}\]])/g, '$1')
// 移除 undefined / function(JSON 不支持)
s = s.replace(/:\s*undefined\b/g, ': null')
s = s.replace(/:\s*function\s*\([^)]*\)\s*\{[^}]*\}/g, ': null')
return s
}
附加功能:JSON 路径查询
格式化后可以直接查询某个字段的值:
onQuery() {
const path = this.data.queryPath.trim()
// 将 categories[0].tools[0].name 转为 categories.0.tools.0.name
const normalizedPath = path.replace(/\[(\d+)\]/g, '.$1')
const keys = normalizedPath.split('.')
let current = this.data.parsedObj
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
if (Array.isArray(current)) {
current = current[parseInt(key)]
} else if (typeof current === 'object') {
current = current[key]
}
}
const result = typeof current === 'object'
? JSON.stringify(current, null, 2)
: String(current)
this.setData({ queryResult: result })
}
输入 data.categories[0].tools[0].name 直接拿到 图片压缩。
工具 3:AA 分账 —— 最接地气的工具
这个工具使用场景很简单:朋友聚餐,多人分摊费用。
核心逻辑:
- 输入总金额、人数 → 自动计算每人金额
- 支持有人先付了部分金额 → 算出谁该补给谁
这个工具虽然逻辑简单,但是拉新利器——每次聚餐大家都会用到,用完还会分享给同桌的人。
五、架构设计亮点
1. 数据驱动
简历页面的内容全部抽到 utils/resume-data.js:
module.exports = {
name: '周老师',
title: '前端开发工程师',
city: '重庆',
// ... 所有简历内容
}
改内容只改这一个文件,不动页面代码。 这也是我在企业级项目中的惯用做法——数据和视图分离。
2. 包体积控制
整个项目只用了一个 npm 依赖(weapp-qrcode),没有引入任何 UI 框架或工具库。原因:
- 工具类小程序不需要复杂组件库
- 每个工具的 UI 结构差异很大,强行抽象反而增加耦合
- 原生 API 已经够用(
wx.setClipboardData、wx.navigateTo等)
3. 安全考虑
- 所有计算都在本地完成——没有网络请求,不上传用户数据
- 二维码生成纯 Canvas 渲染——内容不经过任何服务器
- MD5/SHA 哈希本地计算——敏感信息不会泄露
这对于开发者工具来说是加分项——你不需要担心代码片段被传到某个服务器上。
六、踩坑记录
坑 1:Canvas 2D API 兼容
如前面所说,wx.createCanvasContext 已废弃,必须用 canvas.node.getContext('2d')。但很多第三方库还在用旧 API。我用的指令拦截方案完美解决了这个问题。
坑 2:rpx 适配
微信小程序用 rpx 做响应式单位(750rpx = 屏幕宽度)。但在 iPhone 上 env(safe-area-inset-bottom) 的处理要注意——底部操作栏必须保留安全距离:
.fixed-bar {
padding-bottom: env(safe-area-inset-bottom);
}
坑 3:小程序包大小限制
单个包不能超过 2MB。虽然我的项目很小,但如果后续加更多工具,需要考虑分包加载(subpackages)。目前 9 个工具 + 图片资源还在 1MB 以内,暂时不需要。
七、从 0 到上线的完整流程
1. 需求分析(1 天)
列出最常用的工具,按优先级排序。我选了上面 9 个作为第一版。
2. 设计 UI(2 天)
- 确定墨蓝 + 电光橙的配色方案
- CSS 变量设计令牌
- 首页导航布局
3. 开发工具(5 天)
每个工具 0.5-1 天,从简单到复杂:
时间戳转换(最简单)→ Base64 → JSON 格式化 → 二维码生成 → 图片压缩
4. 测试与优化(2 天)
- 微信开发者工具模拟器测试
- 真机预览(不同型号手机)
- 包体积检查
5. 提交审核(1-3 天)
小程序提交审核,微信团队审核通过后发布。
八、后续规划
计划加入的工具:
- 颜色选择器 / 色值转换(HEX ↔ RGB ↔ HSL)
- 正则表达式测试
- 图片转 Base64
- Cron 表达式解析
计划加的功能:
- 分享功能——每个工具页加一键转发按钮
- 最近使用记录——快速回到上次用的工具
- 搜索工具——工具多了以后按名字搜索
九、总结
做这个小程序的体会:
- 微信小程序做工具类应用天然合适——不用安装、不用注册、即用即走
- 原生开发在简单场景下是最佳选择——不需要框架的过度抽象
- CSS 变量是设计系统的基石——所有颜色走变量,改一处全局生效
- 容错设计决定用户体验——JSON 格式化能处理不规范输入,比严格报错好用 10 倍
扫码体验
用微信搜索「瑞宸工具箱」或扫描下方二维码,即可使用:
包含 9 个实用工具: 二维码生成、JSON 格式化、Base64 编解码、时间戳转换、MD5/SHA 哈希、图片压缩、AA 分账、日期计算、单位换算。
全部本地计算,不上传任何数据。
作者: 周老师,15 年前端开发经验,现任职于新能源行业。接项目开发合作(微信小程序 / Vue 管理后台 / H5 / 数据可视化),欢迎扫码联系。