两个尝试
- 代码风格校验
- 使用脚本创建新组件模板
代码风格校验
背景
团队中每个人的编辑器配置不一样,格式化工具配置不一样,很多文件一旦使用编辑器格式化,往往 format 的与原来面目全非。如何使大家的代码保持一样的格式?正好看到了奇舞周刊的一套工具链使用,于是就想参考一下,在团队的项目中使用,然而一拖再拖,到现在很多东西还是折腾的不是特别的清楚。
实践过程中发现这套工具链不仅仅是做到了代码格式的一致,实际上是通过程序去强制约束成员遵守编码规范,一些社区的规范比如 Airbnb 的 ESLint 校验规则远远不止是格式上的校验,规则中融入了很多编码理念与思考,让我联想到了这个仓库 clean-code-javascript。
下图是腾讯教育团队的编码规范,截自 GMTC 大会分享的 PPT《腾讯在线教育小程序开发实践之路》

推广这套工具的最终的目的是形成一套属于我们自己的编码规范,让开发流程更加规范化,标准化,提高代码质量。
工具链
对于每一个工具,我们只需要弄懂两个问题,What & Why?
官方的描述是我们这个问题最好的解答。
EditorConfig

解决的痛点是同一个项目,用不同的编辑器或 IDE 开发造成的风格不同的问题,比如换行有 LF 和 CRLF,缩进可以是空格 或者 tab,文件的编码方式等。
看到它的配置项就可以联想到 VSCode 右下角的快捷配置。

配置项很简单,下一个。
Prettier


代码格式化工具,特点是简单粗暴。
节约调整代码格式的时间,确保大家代码格式一致,code review 无需关心格式问题。
列举 80% 的配置:
ESLint

主要工作是校验,虽然提供了 fix 功能,但实际用下来发现 fix 命令只能修复一小小小小小部分问题,大多报错需要手动去应对,规则非常的多,组件库参考了奇舞周刊中的用法。

目前基本思路是慢慢删减规则,与医生端基于 @dxy/eslint-config-dxy-base 的规则上分开来走,实验一下哪种更好。
疑问:回头再看一看 Prettier 的配置项,如果与 eslint 规则有冲突可怎么办?


stylelint

就是 CSS 版的 ESLint
目前的规则还没有仔细研究,基本上来说是参考 Vant 与 Ant-Design 的配置。
commitlint
校验 commit message 是否符合规范,为了方便大家提交,引入了 commitizen 这个工具,可以通过 yarn run commit 通过交互式命令行来提交 commit message。
husky

疑问:什么是 Git Hooks,有哪些 Git Hooks?
通过配置的形式,在特定的 hook 执行命令:
"husky": {"hooks": {"pre-commit": "lint-staged","commit-msg": "commitlint -E HUSKY_GIT_PARAMS"}}
lint-staged

疑问:什么是 Stage

"lint-staged": {"*.{ts}": ["eslint --fix","prettier --write","git add"],"*.scss": ["stylelint","prettier --write","git add"],"*.{json,md}": ["prettier --write","git add"]}
疑问:为什么命令的最后需要加 git add?

参考
使用脚本创建新组件模板
创作背景
灵感:来源于用户端小程序的 create-file 脚本,实现很简单,组件库的创建文件存在同样的痛点,预期:几十行代码解决问题。
痛点:新创建一个组件(需要预览页面),需要创建 9 个新文件,并涉及 两 处配置文件的改动,一共需要手动操作共计 11 个文件。

- 组件 5 个文件
- 预览页面 4 个文件
- app.json 与 config.js 两处配置
且其中有 9 处改动可以通过模板替换来完成,并且一部分公共的代码(如公共样式)可以放在其中,减少一部分粘贴复制的工作量。
需求分析
将需要处理的文件分为两类
- 页面/组件文件 进行模板替换即可
- 配置文件 读文件,改内容即可
需要从命令行读4个参数
- 组件英文名
- 组件中文名
- 是否需要预览页面
- 组件分类
技术方案
命令行传参
疑问:如何传参到 Node 程序中?
process.argv这种方案实现最简单,但对用户输入不太友好,参数较多,需要按照一定的格式,且对输入内容处理起来较为麻烦
有没有更优雅的方式?
我想到了使用过的一些命令行工具,比如 vue-cli,会弹出一个交互式表单来给用户填写,怎么做呢?
Inquirer.js-A collection of common interactive command line user interfaces.

这就是我想要的效果!!!
脚本第一步就是用这玩意把参数读进来,给用户一个表单来填写,Inquirer.js 的仓库中提供了许多 demo,照着 demo 很快实现了这个表单,两个输入表单,一个 confirm,一个单选,可见 Inquirer.js 提供了非常多的交互形式,可满足大部分表单类需求,包含表单验证与条件判断。
组件/页面文件操作
这一类文件的操作流程如下
- 根据路径以文本形式读取模板文件
- 进行特定模板的字符串替换
- 将处理后的文本写入到目标路径
fs 的操作选用 fs-extra,API Promise化,并提供了更为便捷的 fs API。
其中字符串替换这一步展开来说一下字符串替换,如果匹配文本是一个动态输入的,可以通过构造函数创建正则对象,来实现动态替换。
/*** 处理内容* @param {*} content*/function processContent(content, answerFormat) {Object.keys(answerFormat).forEach(key => {const origin = `{{ ${key} }}`;const target = answerFormat[key];const pattern = new RegExp(origin, 'g');content = content.replace(pattern, target);});return content;}
写文件使用
配置文件操作
一共有两个配置文件 app.json 和 config.js
app.json
{"window": {"navigationBarBackgroundColor": "#f8f8f8","navigationBarTitleText": "DXDesign","navigationBarTextStyle": "black","backgroundTextStyle": "dark","backgroundColor": "#f8f8f8"},"pages": ["pages/dashboard/index","pages/tag/index" // 这里需要插入],"usingComponents": {"dxd-search": "../../dist/search/index","dxd-tag": "../../dist/tag/index" // 这里需要插入},"sitemapLocation": "sitemap.json"}
对 JSON 文件的操作非常便捷,使用 fs-extra 提供的 readJson 和 writeJson 方法。
config.js
export default [{groupName: '基础组件',list: [{path: '/cell',title: 'Cell 单元格'} // 在这后面新插入],},{groupName: '表单组件',list: [{path: '/input',title: 'Input 输入框'} // 在这后面新插入],},{groupName: '反馈组件',list: [{path: '/toast',title: 'Toast 提示'} // 在这后面新插入],},];
疑问:.js 文件该怎么处理呢 🤔
同样是分三步走
- 读文件
- 编辑文件
- 写文件
先来看读文件,最理想的情况是拿到可以操作的 JS 对象,之后通过最熟悉的 JS API 去操作这个对象,最后再格式化成文本输出到文件。
读的时候就出现了一些问题,CommonJS 和 ESModule。
疑问:Node 如何读取 ESmodule?
方法是有的,将文件扩展名改为 .mjs,使用 --experimental-modules 参数启动 Node。
看一个小例子
export.mjs
let a = 1;export const getChange = () => {console.log("getChange" + a);a = 2;console.log("getChange" + a);};export const get = () => {console.log("get" + a);};export const set = () => {a = 3;};
import.mjs
import {getChange, get, set} from './export.mjs'getChange()get()set()get()


实验特性加上需要修改文件扩展名,这条路走不通!!!
那还有什么办法呢,如果读进来的不是 JS,只能以文本的形式来处理,可以使用正则匹配,这么处理不是很优雅,咨询了一番之后,决定尝试使用 AST 来处理。
AST 一听就觉得很“高深”,离我很遥远,好像不是我这个阶段应该折腾的东西,搜了几篇文章实践下来发现其实没有那么“高深”,结合社区的工具上手起来非常快。

借助 AST分析工具 来看一下构成。
Tree,熟悉的名词,在浏览器渲染过程中有 DOM Tree,CSSOM Tree,Render Tree 等概念,之后我们会用 DOM Tree 来进行类比。
Tree 即方便开发者处理的结构化数据,是一个中间产物,将源文件读成 Tree,以 DOM Tree 为例,我们对节点进行增删改查,与 CSSOM Tree 共同构成 Render Tree,最终由浏览器生成视图,同样对于 AST,我们也是进行增删改查,之后将 Tree 转化为目标文本。
祭出工具—-大家非常熟悉的 BABEL。
与以前不同的是这次我们关注的点在这里:


不得不感叹前端现在的包拆分的有多细。。。。。。
让我们来对照这代码看一看是需求如何实现的
首先需要在提问模块中的最后一个问题是让用户选择组件类型,第一步读取文件并解析出语法树。
// 获取 config.js 的ASTconst getConfigAst = async () => {try {let configPromise = null;return (async function() {if (configPromise) {return await configPromise;} else {return new Promise(async resolve => {const filePath = path.resolve(__dirname, '../example', 'config.js');const content = await fs.readFile(filePath, 'utf8');// 解析抽象语法树const ast = parse(content, {sourceType: 'module',});return resolve(ast);})}})();} catch (error) {console.log(error);}}
第二步,我们需要获得 type 为 Identifier,name 为 "groupName" 的节点,获取节点之后我们找到父节点,在找到父节点的 value 节点,成功获取到了我们要的数据。

这里有一个便捷操作是鼠标点击左侧的位置,右侧语法树直接在相应节点高亮。

// 获取组件类型const getTypeArr = async () => {try {const ast = await getConfigAst();const typeArr = [];traverse(ast, {Identifier(path) {const { node } = path;if (node.name === 'groupName') {const { parent: { value: { value } } } = path;typeArr.push(value);}}});return typeArr;} catch (error) {console.log(error);}}
实际在代码中执行,会有更多的节点信息

第三步,我们需要在特定的位置新增节点
思路还是一样,在分析工具上分析节点类型,在 @babel/types 的文档上找到相应的 API 方法来创建节点。
第四步,将编辑完的 AST 转换成文本,这一步目前发现了一个坑还没解决,新增的中文字符转换成文本之后变成了 unicode。

之后就是常规的写文件操作,仍然使用 outputFile。
AST 相关操作到此完成。
原本计划 50 行的代码,最后用了 300+ 行才实现。
