两个尝试

  • 代码风格校验
  • 使用脚本创建新组件模板

代码风格校验

背景

团队中每个人的编辑器配置不一样,格式化工具配置不一样,很多文件一旦使用编辑器格式化,往往 format 的与原来面目全非。如何使大家的代码保持一样的格式?正好看到了奇舞周刊的一套工具链使用,于是就想参考一下,在团队的项目中使用,然而一拖再拖,到现在很多东西还是折腾的不是特别的清楚。

实践过程中发现这套工具链不仅仅是做到了代码格式的一致,实际上是通过程序去强制约束成员遵守编码规范,一些社区的规范比如 Airbnb 的 ESLint 校验规则远远不止是格式上的校验,规则中融入了很多编码理念与思考,让我联想到了这个仓库 clean-code-javascript

下图是腾讯教育团队的编码规范,截自 GMTC 大会分享的 PPT《腾讯在线教育小程序开发实践之路》

组件库项目工程化的两个尝试 - 图1

推广这套工具的最终的目的是形成一套属于我们自己的编码规范,让开发流程更加规范化,标准化,提高代码质量。

工具链

对于每一个工具,我们只需要弄懂两个问题,What & Why?
官方的描述是我们这个问题最好的解答。

EditorConfig

组件库项目工程化的两个尝试 - 图2

解决的痛点是同一个项目,用不同的编辑器或 IDE 开发造成的风格不同的问题,比如换行有 LF 和 CRLF,缩进可以是空格 或者 tab,文件的编码方式等。

看到它的配置项就可以联想到 VSCode 右下角的快捷配置。

组件库项目工程化的两个尝试 - 图3

配置项很简单,下一个。

Prettier

组件库项目工程化的两个尝试 - 图4

组件库项目工程化的两个尝试 - 图5

代码格式化工具,特点是简单粗暴。

节约调整代码格式的时间,确保大家代码格式一致,code review 无需关心格式问题。

列举 80% 的配置:
组件库项目工程化的两个尝试 - 图6

ESLint

组件库项目工程化的两个尝试 - 图7

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

组件库项目工程化的两个尝试 - 图8
目前基本思路是慢慢删减规则,与医生端基于 @dxy/eslint-config-dxy-base 的规则上分开来走,实验一下哪种更好。

疑问:回头再看一看 Prettier 的配置项,如果与 eslint 规则有冲突可怎么办?

组件库项目工程化的两个尝试 - 图9

组件库项目工程化的两个尝试 - 图10

stylelint

组件库项目工程化的两个尝试 - 图11
就是 CSS 版的 ESLint

目前的规则还没有仔细研究,基本上来说是参考 Vant 与 Ant-Design 的配置。

commitlint

校验 commit message 是否符合规范,为了方便大家提交,引入了 commitizen 这个工具,可以通过 yarn run commit 通过交互式命令行来提交 commit message。

husky

组件库项目工程化的两个尝试 - 图12

疑问:什么是 Git Hooks,有哪些 Git Hooks?

通过配置的形式,在特定的 hook 执行命令:

  1. "husky": {
  2. "hooks": {
  3. "pre-commit": "lint-staged",
  4. "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
  5. }
  6. }

lint-staged

组件库项目工程化的两个尝试 - 图13

疑问:什么是 Stage

组件库项目工程化的两个尝试 - 图14

  1. "lint-staged": {
  2. "*.{ts}": [
  3. "eslint --fix",
  4. "prettier --write",
  5. "git add"
  6. ],
  7. "*.scss": [
  8. "stylelint",
  9. "prettier --write",
  10. "git add"
  11. ],
  12. "*.{json,md}": [
  13. "prettier --write",
  14. "git add"
  15. ]
  16. }

疑问:为什么命令的最后需要加 git add

组件库项目工程化的两个尝试 - 图15

参考

使用脚本创建新组件模板

创作背景

灵感:来源于用户端小程序的 create-file 脚本,实现很简单,组件库的创建文件存在同样的痛点,预期:几十行代码解决问题。

痛点:新创建一个组件(需要预览页面),需要创建 9 个新文件,并涉及 处配置文件的改动,一共需要手动操作共计 11 个文件。

组件库项目工程化的两个尝试 - 图16

  • 组件 5 个文件
  • 预览页面 4 个文件
  • app.json 与 config.js 两处配置

且其中有 9 处改动可以通过模板替换来完成,并且一部分公共的代码(如公共样式)可以放在其中,减少一部分粘贴复制的工作量。

需求分析

将需要处理的文件分为两类

  • 页面/组件文件 进行模板替换即可
  • 配置文件 读文件,改内容即可

需要从命令行读4个参数

  • 组件英文名
  • 组件中文名
  • 是否需要预览页面
  • 组件分类

技术方案

命令行传参

疑问:如何传参到 Node 程序中?

  • process.argv 这种方案实现最简单,但对用户输入不太友好,参数较多,需要按照一定的格式,且对输入内容处理起来较为麻烦

有没有更优雅的方式?

我想到了使用过的一些命令行工具,比如 vue-cli,会弹出一个交互式表单来给用户填写,怎么做呢?

Inquirer.js-A collection of common interactive command line user interfaces.

组件库项目工程化的两个尝试 - 图17
这就是我想要的效果!!!

脚本第一步就是用这玩意把参数读进来,给用户一个表单来填写,Inquirer.js 的仓库中提供了许多 demo,照着 demo 很快实现了这个表单,两个输入表单,一个 confirm,一个单选,可见 Inquirer.js 提供了非常多的交互形式,可满足大部分表单类需求,包含表单验证与条件判断。

组件/页面文件操作

这一类文件的操作流程如下

  • 根据路径以文本形式读取模板文件
  • 进行特定模板的字符串替换
  • 将处理后的文本写入到目标路径

fs 的操作选用 fs-extra,API Promise化,并提供了更为便捷的 fs API。

其中字符串替换这一步展开来说一下字符串替换,如果匹配文本是一个动态输入的,可以通过构造函数创建正则对象,来实现动态替换。

  1. /**
  2. * 处理内容
  3. * @param {*} content
  4. */
  5. function processContent(content, answerFormat) {
  6. Object.keys(answerFormat).forEach(key => {
  7. const origin = `{{ ${key} }}`;
  8. const target = answerFormat[key];
  9. const pattern = new RegExp(origin, 'g');
  10. content = content.replace(pattern, target);
  11. });
  12. return content;
  13. }

写文件使用
组件库项目工程化的两个尝试 - 图18

配置文件操作

一共有两个配置文件 app.jsonconfig.js

app.json

  1. {
  2. "window": {
  3. "navigationBarBackgroundColor": "#f8f8f8",
  4. "navigationBarTitleText": "DXDesign",
  5. "navigationBarTextStyle": "black",
  6. "backgroundTextStyle": "dark",
  7. "backgroundColor": "#f8f8f8"
  8. },
  9. "pages": [
  10. "pages/dashboard/index",
  11. "pages/tag/index" // 这里需要插入
  12. ],
  13. "usingComponents": {
  14. "dxd-search": "../../dist/search/index",
  15. "dxd-tag": "../../dist/tag/index" // 这里需要插入
  16. },
  17. "sitemapLocation": "sitemap.json"
  18. }

对 JSON 文件的操作非常便捷,使用 fs-extra 提供的 readJsonwriteJson 方法。

config.js

  1. export default [
  2. {
  3. groupName: '基础组件',
  4. list: [
  5. {
  6. path: '/cell',
  7. title: 'Cell 单元格'
  8. } // 在这后面新插入
  9. ],
  10. },
  11. {
  12. groupName: '表单组件',
  13. list: [
  14. {
  15. path: '/input',
  16. title: 'Input 输入框'
  17. } // 在这后面新插入
  18. ],
  19. },
  20. {
  21. groupName: '反馈组件',
  22. list: [
  23. {
  24. path: '/toast',
  25. title: 'Toast 提示'
  26. } // 在这后面新插入
  27. ],
  28. },
  29. ];

疑问:.js 文件该怎么处理呢 🤔

同样是分三步走

  • 读文件
  • 编辑文件
  • 写文件

先来看读文件,最理想的情况是拿到可以操作的 JS 对象,之后通过最熟悉的 JS API 去操作这个对象,最后再格式化成文本输出到文件。

读的时候就出现了一些问题,CommonJS 和 ESModule。

疑问:Node 如何读取 ESmodule?

方法是有的,将文件扩展名改为 .mjs,使用 --experimental-modules 参数启动 Node。

看一个小例子

export.mjs

  1. let a = 1;
  2. export const getChange = () => {
  3. console.log("getChange" + a);
  4. a = 2;
  5. console.log("getChange" + a);
  6. };
  7. export const get = () => {
  8. console.log("get" + a);
  9. };
  10. export const set = () => {
  11. a = 3;
  12. };

import.mjs

  1. import {getChange, get, set} from './export.mjs'
  2. getChange()
  3. get()
  4. set()
  5. get()

组件库项目工程化的两个尝试 - 图19

组件库项目工程化的两个尝试 - 图20

实验特性加上需要修改文件扩展名,这条路走不通!!!

那还有什么办法呢,如果读进来的不是 JS,只能以文本的形式来处理,可以使用正则匹配,这么处理不是很优雅,咨询了一番之后,决定尝试使用 AST 来处理。

AST 一听就觉得很“高深”,离我很遥远,好像不是我这个阶段应该折腾的东西,搜了几篇文章实践下来发现其实没有那么“高深”,结合社区的工具上手起来非常快。

组件库项目工程化的两个尝试 - 图21

借助 AST分析工具 来看一下构成。

Tree,熟悉的名词,在浏览器渲染过程中有 DOM Tree,CSSOM Tree,Render Tree 等概念,之后我们会用 DOM Tree 来进行类比。

Tree 即方便开发者处理的结构化数据,是一个中间产物,将源文件读成 Tree,以 DOM Tree 为例,我们对节点进行增删改查,与 CSSOM Tree 共同构成 Render Tree,最终由浏览器生成视图,同样对于 AST,我们也是进行增删改查,之后将 Tree 转化为目标文本。

祭出工具—-大家非常熟悉的 BABEL

与以前不同的是这次我们关注的点在这里:

组件库项目工程化的两个尝试 - 图22

组件库项目工程化的两个尝试 - 图23

不得不感叹前端现在的包拆分的有多细。。。。。。

让我们来对照这代码看一看是需求如何实现的

首先需要在提问模块中的最后一个问题是让用户选择组件类型,第一步读取文件并解析出语法树。

  1. // 获取 config.js 的AST
  2. const getConfigAst = async () => {
  3. try {
  4. let configPromise = null;
  5. return (async function() {
  6. if (configPromise) {
  7. return await configPromise;
  8. } else {
  9. return new Promise(async resolve => {
  10. const filePath = path.resolve(__dirname, '../example', 'config.js');
  11. const content = await fs.readFile(filePath, 'utf8');
  12. // 解析抽象语法树
  13. const ast = parse(content, {
  14. sourceType: 'module',
  15. });
  16. return resolve(ast);
  17. })
  18. }
  19. })();
  20. } catch (error) {
  21. console.log(error);
  22. }
  23. }

第二步,我们需要获得 typeIdentifiername"groupName" 的节点,获取节点之后我们找到父节点,在找到父节点的 value 节点,成功获取到了我们要的数据。

组件库项目工程化的两个尝试 - 图24

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

组件库项目工程化的两个尝试 - 图25

  1. // 获取组件类型
  2. const getTypeArr = async () => {
  3. try {
  4. const ast = await getConfigAst();
  5. const typeArr = [];
  6. traverse(ast, {
  7. Identifier(path) {
  8. const { node } = path;
  9. if (node.name === 'groupName') {
  10. const { parent: { value: { value } } } = path;
  11. typeArr.push(value);
  12. }
  13. }
  14. });
  15. return typeArr;
  16. } catch (error) {
  17. console.log(error);
  18. }
  19. }

实际在代码中执行,会有更多的节点信息

组件库项目工程化的两个尝试 - 图26

第三步,我们需要在特定的位置新增节点

思路还是一样,在分析工具上分析节点类型,在 @babel/types 的文档上找到相应的 API 方法来创建节点。

第四步,将编辑完的 AST 转换成文本,这一步目前发现了一个坑还没解决,新增的中文字符转换成文本之后变成了 unicode。

组件库项目工程化的两个尝试 - 图27

之后就是常规的写文件操作,仍然使用 outputFile

AST 相关操作到此完成。

原本计划 50 行的代码,最后用了 300+ 行才实现。

参考文档