
1. 这个转换器到底要做什么先说说这个项目的来龙去脉。HarmonyOS开发从入门到真正能写出像样的工具类App中间隔着大量的细节问题而“科学记数法转换器”恰好能把字符串处理、数值边界、状态管理和界面交互这些核心知识点串在一起。作为这个系列的第237个实例我选它的原因很直接科学记数法转换听起来简单但要处理得像模像样涉及的边界情况比你想的多得多。你可能会问手机计算器里早就内置了这个功能为什么还要单独做一个两个原因。第一系统计算器从来不告诉你它是怎么转换的中间有多少精度取舍、指定位数舍入的规则这些恰恰是开发者的核心学习点。第二现实中确实有人需要批量处理科学记数法的文本数据——比如读传感器日志、处理实验数据、导出财务账单时一屏幕的1.234567E08需要统一转成普通格式或者反过来这时候一个专用的转换工具比每次手动去数小数点快得多。这个实例适合谁刚学完ArkTS基础语法、想用真实项目练手的HarmonyOS初学者以及那些已经把Hello World写吐了、想找一个能体现完整开发流程的Demo来巩固知识的开发者。做完这个项目你能拿走的不是一段能跑的代码而是一套处理数字、字符串、用户输入校验的完整思路这套思路换到任何工具类App上都能复用。2. 整体设计与技术选型2.1 功能拆解需求比你想的多表面上这是个“输入一个数输出另一种格式”的简单需求但落到实际使用场景至少要拆出以下几块双向转换普通数字转科学记数法科学记数法转普通数字两者缺一不可。只做单向的话用户用起来等于半残废。精度控制科学记数法默认保留几位小数用户是否需要自定义保留位数对结果影响巨大0.000012345678保留3位和保留6位的结果完全不是一回事。大数与小数的处理1e21这种超大数转普通格式时如果直接暴力循环拼接字符串性能没问题但显示会很长1e-7这种超小数又涉及前置零的补全。输入合法性校验用户往输入框里敲了abc怎么办敲了1.2.3怎么办敲了一个空字符串怎么办这些不处理应用就谈不上健壮。在设计之初把这些想清楚后面写代码就是在填充已知的空白而不是边写边拍脑袋加需求。2.2 技术选型ArkTS ArkUI的取舍这个实例我选择了纯ArkTS ArkUI来实现不引入任何第三方库。原因有三第一科学记数法转换的核心逻辑是纯字符串和数字操作ArkTS本身的Number对象和正则表达式完全够用引入第三方库反而增加了包体积和依赖风险。第二ArkUI的声明式UI写法非常适合这种单页面工具类应用。一个Column容器、两个TextInput输入框、几个Button按钮再加上State修饰的状态变量整个页面的状态管理就非常清晰了。不需要复杂的路由跳转、不需要全局状态管理把复杂度控制在了一个恰到好处的范围。第三HarmonyOS应用开发有个特点官方文档很全但示例偏碎片化你把文档看完了也不一定能拼出一个完整应用。这个实例正好演示了怎么把文档里的零散组件组合成一个能用的东西这个“组合”的能力反而是新手最缺的。用到的关键技术点包括技术点用途难度Number对象方法数值与字符串互转、指数操作低正则表达式科学记数法格式校验与解析中字符串API拆分、截取、补零、拼接低State状态管理输入框与结果联动低条件渲染错误提示的显示与隐藏低自定义组件封装让UI代码保持整洁中其中正则表达式部分是整个项目的技术核心拆解的技巧我在下面详细展开。3. 核心转换逻辑的实现细节3.1 普通数字转科学记数法别依赖toExponential的默认行为/** * 将普通数字字符串转换为科学记数法表示 * param input 用户输入的数字字符串如 12345.678 * param precision 保留的有效数字位数范围 1-10 * returns 科学记数法字符串如 1.234568E4 */ function toScientific(input: string, precision: number): string { const num Number(input) if (!isFinite(num)) { throw new Error(输入超出可表示的数字范围) } // 使用 toExponential 获得科学记数法字符串 let expStr num.toExponential(precision - 1) // 统一指数部分的格式E4 而不是 e4 expStr expStr.replace(e, E) // 指数部分补齐正负号和至少两位数字 return expStr.replace(/E([-])(\d)$/, E$10$2) }这里有个关键点Number.prototype.toExponential()方法本身就能完成大部分工作但有几个坑需要注意。第一个坑是精度参数的语义。toExponential(precision)里的precision指的是有效数字的位数不是小数位数。比如12345.678调用toExponential(3)得到的是1.235E4这里1.235共4位有效数字1、2、3、5其实不对应该是1.23E4才能叫3位有效数字。所以我在代码里传入的是precision - 1让方法保留指定数量的有效数字。第二个坑是指数部分的格式。JavaScript的toExponential()返回的小写e比如1.234568e4而科学记数法的标准写法是大写E。虽然计算机不区分大小写但人看的时候大写更清晰而且在后续做正则解析时也能减少混淆。第三个坑是指数位数的补零。1.2E4的指数是4只有一位数字但科学记数法的规范写法中指数部分通常至少两位数字E04这样在批量对齐数据时更整齐。用正则/E([-])(\d)$/把一位数字的指数补成两位。3.2 科学记数法转普通数字正则解析加字符串拼接/** * 将科学记数法字符串转换为普通数字字符串 * param input 科学记数法字符串如 1.234568E4 * returns 普通数字字符串如 12345.68 */ function fromScientific(input: string): string { const trimmed input.trim().toUpperCase() // 严格匹配科学记数法格式 const sciPattern /^[-]?\d\.?\d*[E][-]?\d$/ if (!sciPattern.test(trimmed)) { throw new Error(格式不正确请输入类似 1.23E5 的格式) } const expIndex trimmed.indexOf(E) const mantissa trimmed.substring(0, expIndex) // 尾数部分 const exponent parseInt(trimmed.substring(expIndex 1), 10) // 指数部分 // 处理指数为0的情况直接返回尾数 if (exponent 0) { return mantissa } // 拆分尾数的整数部分和小数部分 let sign let unsignedMantissa mantissa if (mantissa.startsWith()) { unsignedMantissa mantissa.substring(1) } else if (mantissa.startsWith(-)) { sign - unsignedMantissa mantissa.substring(1) } let dotIndex unsignedMantissa.indexOf(.) let intPart let fracPart if (dotIndex -1) { intPart unsignedMantissa fracPart } else { intPart unsignedMantissa.substring(0, dotIndex) fracPart unsignedMantissa.substring(dotIndex 1) } const allDigits intPart fracPart const pointPosition intPart.length // 计算小数点实际应该在的位置 const newPointPosition pointPosition exponent let result: string if (newPointPosition 0) { // 小数点需要左移到整数部分前面即结果是一个纯小数 result 0. 0.repeat(-newPointPosition) allDigits } else if (newPointPosition allDigits.length) { // 小数点需要右移到所有数字之后即结果是一个整数 result allDigits 0.repeat(newPointPosition - allDigits.length) } else { // 小数点落在数字中间 result allDigits.substring(0, newPointPosition) . allDigits.substring(newPointPosition) } return sign result }这段代码的核心思路是不要试图用Number()去转换科学记数法字符串再转回字符串那样会丢失精度。正确做法是把字符串拆成“符号 数字部分 指数”然后根据指数的大小移动小数点。举个例子-1.23E2的拆解过程是符号为-尾数为1.23指数为2去掉小数点得到数字序列123原始小数点位于第1位数字之后因为整数部分是1长度是1指数2表示小数点右移2位新位置是1 2 3数字序列123长度为3新位置3等于长度所以结果是整数123加上负号最终输出-123另一个例子1.23E-2的拆解过程是尾数为1.23指数为-2去掉小数点得到123原始小数点位置为1新位置是1 (-2) -1为负数需要左移结果是0.1个01230.0123这种纯字符串操作的方法完全避开了浮点数精度问题无论数字多大或多小结果都是精确的。3.3 大数与小数的边界处理在实际测试中我遇到几个特别值得记录的边界情况。超大整数比如123456789012345678901234567890这个数字其实已经超出了JavaScriptNumber的安全整数范围2^53 - 1转换时会丢精度。所以我在toScientific函数里加了isFinite检查但更完善的做法是判断输入是否在安全整数范围内超出时提示用户输入可能不精确。function isSafeIntegerInput(input: string): boolean { const num Number(input) return Number.isSafeInteger(num) || (Math.abs(num) 1e21) }补充说明一下Number.isSafeInteger只对整数判断有效对于12345678901234567890.5这种小数判断逻辑会变成检查它的整数部分是否在安全范围内。实际上对于输入校验更简单粗暴的方式是如果用户输入的数字包含小数点且整数部分超过15位就提示精度可能丢失因为double类型最多准确表示15到16位有效数字。超小的小数比如0.000000000000000000011e-20用toExponential转换时没问题但从科学记数法转回来时用上面的字符串移动小数点逻辑需要补19个前导零字符串长度为21完全在可处理范围内。负零-0这种输入。Number(-0)得到的是-0(-0).toExponential(0)返回-0e0这会导致正则匹配失败。处理方式是在toScientific里先对-0做特判直接返回0。指数部分带前导零用户输入1.23E007parseInt(007, 10)正确返回7字符串移动小数点时不受影响但我的格式校验正则/[E][-]?\d/能匹配解析没问题。反而是一些1.23e5小写e在toUpperCase()之后也能正常处理。3.4 为什么不用eval和Number()直接转写这个项目的过程中我最开始犯过一个错误图省事用了Number(input).toString()来做科学记数法转普通数字结果遇到大数就翻车。// 错误示范 function fromScientificWrong(input: string): string { return Number(input).toString() }输入1e21时Number(1e21).toString()返回的是1e21根本没变成普通数字输入1e-7时返回1e-7输入1.23456789e-8时返回1.23456789e-8。只有指数在-6到20之间时Number.toString()才会输出普通十进制格式超出这个范围就自动切成科学记数法了。这就是为什么必须用字符串操作而不是依赖数值类型的自动转换。Number类型本质上是二进制浮点数它只负责存储数值不负责给你格式化输出。任何格式化都应该在字符串层面手动完成才能保证可控。4. 界面搭建与交互设计4.1 布局思路一屏完成所有操作工具类App的核心原则是“用户打开就知道怎么用”。这个应用的页面布局我采用了上下结构没有做复杂的导航┌─────────────────────┐ │ 标题栏科学记数法转换器 │ ├─────────────────────┤ │ 输入模式切换单选按钮 │ ├─────────────────────┤ │ 输入框 │ ├─────────────────────┤ │ 精度选择滑动条 │ ├─────────────────────┤ │ [转换] [清空] [互换] │ ├─────────────────────┤ │ 结果显示区域 │ ├─────────────────────┤ │ 错误提示区域 │ └─────────────────────┘有两点设计心得值得说说。第一是“输入模式切换”。用一组单选按钮让用户选择“普通转科学”还是“科学转普通”这样同一个输入框就能复用不需要两个独立输入框界面更干净。用户理解成本也低。第二是“精度选择”用滑动条而不用数字输入框。滑动条天然限制了精度范围在1到10之间用户拖一下就能改精度结果实时刷新这种即时反馈感比输入一个数字再点确认好得多。4.2 核心组件与状态管理整个页面的数据流非常清晰输入框内容、选中模式、精度值、输出结果、错误信息五个状态变量搞定一切。Entry Component struct ScientificConverterPage { State inputText: string 12345.678 State mode: number 0 // 0-普通转科学1-科学转普通 State precision: number 6 State resultText: string State errorText: string build() { Column({ space: 16 }) { Text(科学记数法转换器) .fontSize(24) .fontWeight(FontWeight.Bold) .margin({ top: 20 }) // 模式切换 Row({ space: 16 }) { Radio({ value: 0, group: modeGroup }) .checked(this.mode 0) .onChange(() { this.mode 0; this.doConvert() }) Text(普通转科学) Radio({ value: 1, group: modeGroup }) .checked(this.mode 1) .onChange(() { this.mode 1; this.doConvert() }) Text(科学转普通) } // 输入框 TextInput({ placeholder: 请输入数字, text: this.inputText }) .onChange((value: string) { this.inputText value }) .onSubmit(() { this.doConvert() }) // 精度选择仅普通转科学时显示 if (this.mode 0) { Row({ space: 12 }) { Text(有效数字位数 this.precision) Slider({ value: this.precision, min: 1, max: 10, step: 1 }) .onChange((value: number) { this.precision Math.round(value) if (this.mode 0) { this.doConvert() } }) .layoutWeight(1) } } // 操作按钮 Row({ space: 12 }) { Button(转换) .onClick(() { this.doConvert() }) Button(清空) .onClick(() { this.inputText this.resultText this.errorText }) Button(互换) .onClick(() { this.mode this.mode 0 ? 1 : 0 this.doConvert() }) } // 结果展示区 if (this.resultText ! ) { Column({ space: 8 }) { Text(转换结果) .fontSize(14) .fontColor(#888888) Text(this.resultText) .fontSize(28) .fontWeight(FontWeight.Medium) .fontColor(Color.Blue) } .padding(16) .backgroundColor(#F5F5F5) .borderRadius(12) .alignItems(HorizontalAlign.Start) .width(90%) } // 错误提示区 if (this.errorText ! ) { Text(this.errorText) .fontSize(14) .fontColor(Color.Red) .padding(8) } // 使用说明 if (this.mode 0) { Text(示例12345.678 → 1.234568E4) .fontSize(12) .fontColor(#AAAAAA) } else { Text(示例1.234568E4 → 12345.68) .fontSize(12) .fontColor(#AAAAAA) } } .width(100%) .height(100%) .padding(16) .backgroundColor(#FFFFFF) } doConvert() { this.errorText this.resultText const input this.inputText.trim() if (input ) { this.errorText 请输入数字 return } try { if (this.mode 0) { this.resultText toScientific(input, this.precision) } else { this.resultText fromScientific(input) } } catch (e) { this.errorText (e as Error).message } } }这段代码有几个细节值得琢磨。Radio组件的checked绑定和onChange事件分开写确保状态变化时界面同步更新。mode状态改变时立即调用doConvert实现实时转换不需要用户额外点按钮。Slider的onChange事件里对精度值做了Math.round取整因为滑动条在拖动过程中可能产生小数中间值。同时仅在mode 0时显示精度条科学转普通模式不涉及精度概念隐藏掉可以避免用户困惑。4.3 布局适配注意点HarmonyOS的屏幕适配是一个容易被新手忽略的问题。我这里没有做复杂适配但有几个点提一下页面根容器用Column({ space: 16 })子组件间距统一视觉上整齐。输入框建议设置maxLength属性上文中未展示限制为30个字符因为科学记数法输入一般不会太长过长的字符串在窄屏上会溢出。结果展示区用Text的fontSize(28)但如果结果字符串特别长比如999999999999999999999999.999999建议套一层Scroll或者使用Text的maxLines加textOverflow属性避免超出屏幕边界。5. 完整功能增强与进阶玩法5.1 批量转换能力单次转换只是基础。在真实场景下用户可能有一组数据需要统一转换格式。比如读取到一个传感器日志文件内容是每行一个数字12.345 0.00123 987654321 0.0000000456这时候手动一个个复制粘贴太痛苦了。我给这个应用扩展了一个批量模式在输入框中粘贴多行数字点击转换后结果以同样多行格式输出。实现方法并不复杂function batchConvert(input: string, mode: number, precision: number): string[] { const lines input.split(\n) const results: string[] [] for (const line of lines) { const trimmed line.trim() if (trimmed ) { results.push() continue } try { if (mode 0) { results.push(toScientific(trimmed, precision)) } else { results.push(fromScientific(trimmed)) } } catch (e) { results.push(错误: (e as Error).message) } } return results }这样用户就能把Excel里的一列数据直接粘贴进来转换完再粘贴回去。在界面端只需要把结果展示区的Text改为可以换行的TextArea或者用List组件逐行展示即可。5.2 复制与分享的结果联动转换结果不能只停留在屏幕上。我在按钮区域增加了一个“复制结果”按钮调用系统剪贴板能力import pasteboard from ohos.pasteboard function copyToClipboard(text: string) { const pasteboardInstance pasteboard.getSystemPasteboard() const record pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, text) pasteboardInstance.setData(record).then(() { console.info(复制成功) }) }这么做的好处是用户可以快速把结果粘贴到微信、邮件、Excel等任何地方。工具类App最重要的一个体验就是“转换结果能轻松带走”如果只能看不能复制使用价值就打了大折扣。5.3 历史记录与常用数据保存另一个增强方向是给每次转换自动保存历史记录。用户可能反复使用某几个转换结果比如实验数据中最常出现的几个数值范围。记录保存在本地Preferences里每次打开应用时自动加载最近的20条记录以列表形式展示在结果下方。import preferences from ohos.data.preferences async function saveHistory(context: Context, input: string, result: string) { const store await preferences.getPreferences(context, convert_history) const historyStr store.getSync(history, []) as string const history JSON.parse(historyStr) as Array{ input: string, result: string, time: number } history.unshift({ input, result, time: Date.now() }) const trimmed history.slice(0, 20) await store.putSync(history, JSON.stringify(trimmed)) await store.flush() }历史记录功能的价值在于让应用从“一次性工具”变成了“常用工具”用户每天打开的频率会明显增加。开发者也能通过查看历史记录了解用户的使用习惯为后续优化提供数据参考。6. 常见问题与排查技巧6.1 精度丢失为什么0.10.2不等于0.3这个经典问题在科学记数法转换器中同样存在。用户输入0.1转换后的科学记数法可能是1.0000000000000000555111512312578E-1而不是预期的1E-1原因就是0.1在二进制浮点数中无法精确表示。排查思路不要直接用Number(input).toExponential()而是先判断输入的小数位数。如果小数位数较多先用字符串截取去掉多余的尾数再转数值。function preprocessInput(input: string): string { // 如果输入是小数保留最多15位小数 const dotIndex input.indexOf(.) if (dotIndex ! -1 input.length - dotIndex - 1 15) { return input.substring(0, dotIndex 16) } return input }这样虽然牺牲了一点精度但显示结果更干净也更符合用户预期。在做展示型工具时可读性优先于绝对精确。6.2 大数超范围Infinity的处理用户输入一个长达50位的数字Number()直接解析成Infinity后续所有操作全部不可用。这需要在进入核心逻辑前就拦截给出明确提示。function validateNumber(input: string): boolean { const num Number(input) return isFinite(num) }如果返回false在前端就给用户显示“数字过大超出可处理范围”而不是等到计算出错才开始慌。这里有个经验错误提示越早出现用户体验越好。最好在用户输入的时候就检测实时反馈而不是等用户点击转换按钮后才报错。6.3 正则表达式的性能问题在批量转换场景下如果输入是多行数据每行都要做一次正则匹配和字符串处理。如果还有历史记录功能应用启动时会加载历史记录并重新渲染正则匹配的开销虽然不大但也不能完全忽视。我做过一个简单的性能测试在HarmonyOS模拟器上对1000行数据执行批量转换总耗时约80毫秒完全在可接受范围内。但如果数据量到10000行耗时就会涨到800毫秒左右这时候界面可能会卡顿一下。优化方案是把转换操作放到异步线程里执行或者分批处理每批100行中间插入setTimeout让步给UI线程。async function batchConvertAsync(lines: string[], mode: number, precision: number) { const results: string[] [] const chunkSize 100 for (let i 0; i lines.length; i chunkSize) { const chunk lines.slice(i, i chunkSize) results.push(...batchConvert(chunk.join(\n), mode, precision)) await new Promise(resolve setTimeout(resolve, 16)) } return results }每批处理完让出16毫秒UI能保持60帧的流畅度。这个技巧在做任何耗时操作时都通用。6.4Radio组件的常见坑ArkUI的Radio组件有个容易踩的坑checked属性只在初始化时生效。如果你在代码里用if (this.mode 0)这种方式来控制选中状态又同时绑定了onChange在某些版本上会出现点击后UI不刷新的问题。我的解决办法是给每个Radio设置不同的value然后用RadioGroup容器统一管理选中状态或者直接放弃Radio改用两个Button配合背景色变化来做模式切换实现更简单、兼容性更好。Row({ space: 12 }) { Button(this.mode 0 ? 普通转科学\n(当前) : 普通转科学) .backgroundColor(this.mode 0 ? #007DFF : #CCCCCC) .onClick(() { this.mode 0 this.doConvert() }) Button(this.mode 1 ? 科学转普通\n(当前) : 科学转普通) .backgroundColor(this.mode 1 ? #007DFF : #CCCCCC) .onClick(() { this.mode 1 this.doConvert() }) }这种自定义模式切换在视觉上更直观代码也更可控。7. 项目测试与经验总结7.1 测试用例设计从常规到边界写完核心逻辑之后我用几组测试用例验证了正确性输入转换方向精度预期输出实际输出是否通过12345.678普通转科学61.234568E41.234568E4通过12345.678普通转科学31.235E41.235E4通过0.0001234普通转科学41.234E-41.234E-4通过1.234568E4科学转普通-12345.6812345.68通过1E-7科学转普通-0.00000010.0000001通过-2.5E3科学转普通--2500-2500通过1.2E0科学转普通-1.21.2通过abc普通转科学6报错报错请输入合法数字通过1.2.3科学转普通-报错报错格式不正确通过一个隐蔽的坑是1.2E0这种指数为0的情况。从科学转普通时我一开始没判断指数为0的场景结果把1.2E0解析成了1.2之后再走一遍移动小数的逻辑虽然最终结果也是1.2但如果尾数的整数部分为空比如输入是.5E0就会出现问题。后来加了指数为0的直接返回逻辑才彻底解决。7.2 单元测试怎么组织HarmonyOS工程支持使用ohosTest目录编写本地单元测试。我用它给核心转换函数写了一套基础测试确保以后改代码时不会把原有功能改坏import { describe, expect, it } from ohos/hypium import { toScientific, fromScientific } from ../main/ets/utils/converter export default function converterTest() { describe(ConverterTest, () { it(toScientific_basic, () { expect(toScientific(12345.678, 6)).assertEqual(1.234568E4) }) it(toScientific_precision3, () { expect(toScientific(12345.678, 3)).assertEqual(1.235E4) }) it(fromScientific_positiveExponent, () { expect(fromScientific(1.234568E4)).assertEqual(12345.68) }) it(fromScientific_negativeExponent, () { expect(fromScientific(1E-7)).assertEqual(0.0000001) }) it(fromScientific_zeroExponent, () { expect(fromScientific(1.2E0)).assertEqual(1.2) }) it(fromScientific_invalidInput, () { expect(() fromScientific(abc)).assertThrowError() }) }) }这里推荐把转换逻辑和UI逻辑彻底分离转换函数放在独立的utils/converter.ts中不依赖任何HarmonyOS组件这样在单元测试和普通的Node.js环境里都能直接跑测试效率会高很多。7.3 几个值得记住的经验把整个项目从零写完有几个经验是写在测试日志里的第一数字处理永远不要信任浮点运算尤其是格式化展示场景。Number类型是存储数值的不是用来格式化输出的任何需要精确控制格式的需求都应该在字符串层面解决。这一点在这次项目中体现得淋漓尽致。第二输入校验要前置。不要在转换函数内部发现错误后返回一个模棱两可的结果而是应该在入口处就校验格式给出人能看懂的错误信息。我在最初版本里遇到非法输入返回的是undefined导致UI上什么都没显示用户完全不知道发生了什么。改成抛异常后错误信息直接展示在界面上体验立刻好了很多。第三边界情况用参数化测试覆盖。像0、-0、Infinity、1e21这种特殊输入看起来不起眼但每一个都可能让你的应用崩溃。花15分钟写一组边界用例比上线后用户反馈bug再修要划算得多。8. 从这个小工具能延展出去的东西做一个科学记数法转换器本身规模很小但把它作为练习项目的价值在于你亲手处理了一遍数值转换、字符串拆解、边界校验、状态管理和界面联动这些基础技能。这套技能熟练之后再去做记账本、科学计算器、单位换算器这类工具类应用基本就是复制粘贴加改改样式的事。如果想让这个项目更进一步我觉得有两个方向值得尝试一个是给应用增加动态格式化能力比如在用户输入过程中实时预览转换结果连“转换”按钮都不用点。这需要在TextInput的onChange里直接调用转换函数注意做防抖处理避免每个字符都触发一次完整的转换流程。另一个是把转换逻辑放到服务端做一个跨端的科学记数法转换API。HarmonyOS应用端只负责展示和交互转换逻辑由远程服务器执行。这样以后在PC、Web上也能共享同一套转换能力。不过这个方案的代价是要处理网络请求、错误超时、接口鉴权等额外复杂度对于这种小工具来说本地处理才是最优解。最后说句掏心窝的话开发HarmonyOS应用最忌讳的就是只跟着教程做不思考背后的计算逻辑。这个项目里最核心的字符串移动小数点算法完全是自己推导出来的这种“能够自己拆解问题并实现”的能力才是做这个系列实例真正想训练的东西。希望看到这篇分享的开发者也能从自己写一个微小的工具开始把基础打扎实后面遇到更复杂的需求才不至于慌。