1 min read

网页工具集:科学计算器、二维码与 34 个免费小工具

博客自建的 34 个纯浏览器端工具页:科学计算器、函数绘图、复利/房贷计算、二维码生成器(手写编码器)等,全部零第三方依赖。

博客导航栏里的 Calculator/calculators/standard/)是一个纯浏览器端运行的进阶计算器。它最初是四个选项卡挤在一个页面里,后来随着工具页体系(见 Tools)的建立被拆成了独立子页面——每个工具一个网址,更好分享、更好搜索:

旧地址 /calculator/ 会自动跳转到新页面。

一个注册表,34 个工具

这次拆分顺带建起了整套工具页体系(入口在导航栏的 Tools,共 34 个工具、四大分类):

分类 内容
Calculators 科学计算器、函数绘图、百分比、百分比变化、分数、比例、平均数与统计、单利
Converters 长度、重量、温度、面积、体积、速度、时间、数据大小——双向换算、一键交换
Finance 复利(含月定投、逐年明细)、等额本息月供与摊销表、房贷(含税险 HOA)、投资回报、ROI、折扣、年薪换算、个税
Tools 密码/UUID/随机数生成器、二维码生成器、字数统计、字符统计、JSON 格式化、Base64、颜色转换

几个值得一提的细节:

  • JSON 格式化报错不只说“Invalid JSON”,还给出精确的行列号——浏览器的 V8 错误消息里只有上下文片段没有位置,所以格式化前先跑一遍自己写的结构扫描器定位第一个错误
  • 三个生成器(密码/UUID/随机数)全部用 crypto.getRandomValues 并做拒绝采样,保证每个字符均匀分布——Math.random 做密码是不合格的
  • 二维码生成器的编码器是手写的(下文展开),支持版本 1–10、四档纠错、PNG 下载
  • 所有计算在浏览器本地完成,没有任何数据离开你的设备

功能总览

计算器

支持完整的数学表达式,而不是普通计算器的“一步一步”输入:

2 + 3 * 4        → 14
sin(pi/2)        → 1        (弧度制)
a = 5            → 定义变量
a^2 + 1          → 26
ans * 2          → 上一轮结果
  • 标准 / 科学键盘:右上角可切换。标准键盘是 4 列的日常布局;科学键盘多出 sin cos tan √ ln log π e x^y n! 等按键
  • DEG / RAD:科学键盘上可切换角度制与弧度制。注意与实体计算器一致的语义——角度制下 sin(pi/2) 会把 π/2 ≈ 1.57 当作 1.57° 来算
  • 变量名字 = 表达式 赋值(常量名和函数名不可占用),变量以 chips 形式列在输入框下方,点一下插入、点 × 删除。变量存进 localStorage,在绘图页里可以直接引用(如 a*x
  • 历史记录:每次按 = 的算式都进历史,点击任意一条可回填

函数绘图

最多同时画 4 条曲线(固定四色,色盲友好),支持 sin(x)x^2 - 3a*x(引用计算器变量)等:

  • 滚轮缩放(以鼠标位置为中心)、拖拽平移、触屏双指 pinch
  • 悬停显示十字线与各函数在该 x 处的取值
  • 自动网格与坐标轴刻度,1/x 这类渐近线会自动断笔
  • 修改科学计算器页里的变量,图形实时联动重绘
  • 暗色主题下画布配色自动跟随重绘(颜色从 CSS 变量读取,MutationObserver 监听主题切换)

实现原理

整个工具零第三方依赖——没有 math.js,没有 React,只有 TypeScript 和浏览器原生 API。

手写表达式引擎

核心是一条经典的编译流水线,位于 src/scripts/calculator/engine/

tokenizer → parser(递归下降) → AST → compile 为闭包
  1. tokenizer:把 "2+3*4" 切成数字、运算符、函数名、括号 token

  2. parser:按文法递归下降,处理优先级和结合性:

    expr  := term (('+'|'-') term)*
    term  := unary (('*'|'/'|'%') unary | 隐式乘法)*
    unary := ('-'|'+') unary | power
    power := postfix ('^' unary)?        // 右结合
    postfix := primary ('!' | '%')*
  3. compile:把 AST 编译成一个 (scope) => number 闭包。绘图时每帧要采样上千个点,解释执行 AST 太慢,预编译成闭包后绘图热路径就是纯函数调用

% 有一个刻意设计的歧义处理:跟在操作数后面且下一个 token 不是操作数时是百分号(50 + 10% = 55),否则是取模(10 % 3 = 1),与主流计算器一致。

Canvas 绘图

  • DPR 适配:画布物理尺寸 = CSS 尺寸 × devicePixelRatio,保证 2K/4K 屏不糊
  • ResizeObserver 监听容器尺寸变化自动重绘
  • 每像素列采样一个点;相邻两点屏幕跳跃超过两倍画布高且函数值变号时断笔(渐近线检测)
  • Pointer Events 统一处理鼠标/触摸的拖拽与 pinch

手写二维码编码器

二维码生成器是工具集里最硬核的一个——整条编码管线都是手写的,不到 400 行:

UTF-8 字节 → 位流(模式+长度+数据+填充)
          → Reed–Solomon 纠错码(GF(256),生成多项式 0x11D)
          → 分块交织 → 矩阵摆放(之字形扫描)
          → 8 种掩码按 ISO 罚分规则选最优 → 写入格式信息

几个关键点:

  • Reed–Solomon:在 GF(256) 上用对数/反对数表做乘法,移位寄存器式求余算出纠错码字,块结构按版本 × 纠错等级查表
  • 掩码:数据模块要 XOR 一个掩码图案避免出现大面积同色或类似定位图案的结构;8 种候选逐一评估四条罚分规则(连续同色、2×2 同色块、类定位图案、明暗比例),取罚分最低者
  • 验证方式:与 python qrcode 库强制相同掩码后逐模块 diff(128 例全部一致),再把输出交给 opencv 的解码器做解码往返(16/16 还原出原文)——证明生成的码是真实可扫的,不只是“看起来像”

拆分后的工程组织

工具页共用一套注册表驱动的架构:src/tools/registry.ts 里每个工具是一条数据(路径、名称、字段定义、compute 纯函数),四个动态路由 src/pages/{calculators,converters,finance,tools}/[slug].astro 据此静态生成全部子页面,客户端按 data-tool-kind 分发到对应的渲染器(表单、换算器、文本工具……)。新增一个计算器只需在注册表里加一条配置,路由、目录页、sitemap 全部自动跟上。

性能哲学

没有用 React 之类的框架:这类工具页交互密度低、无状态共享需求,vanilla TS 在加载(无框架运行时)与运行(无虚拟 DOM diff)两端都更快,打包体积只有几 KB。

技术栈小结

部分 方案
表达式求值 手写 tokenizer + 递归下降 parser + 闭包编译
绘图 原生 Canvas 2D + Pointer Events
二维码 手写编码器:RS 纠错 + 8 掩码 ISO 罚分选择
金融计算 等额本息/复利闭式解,摊销表按年分组
随机性 crypto.getRandomValues + 拒绝采样
状态持久化 localStorage(历史、变量、主题)
框架依赖 0