企业级前端脚手架开发总结
# 为什么我们需要前端脚手架
脚手架是为了保证各施工过程顺利进行而搭设的工作平台。
相信很多前端童鞋都用过vue-cli
、create-react-app
这类优秀的前端脚手架了,这些工具在一定程度上提升了我们项目初始化的效率,减少了很多重复性的工作,但这些工具仅仅是基础通用型的,跟公司业务以及内部制定的规范无关,可控性不高,远远不能满足公司的需求。所以,为了满足公司内部的研发需求,规范项目结构以及提高每个前端的同学的工作效率,便需要有一套内部的前端脚手架来支持,服务每个开发人员,赋能公司所有的项目🚀
# 需要考虑什么
目前项目上存在以下这些痛点:
- 前端项目越来越重,一些工具集成很繁琐、耗时,如
commitlint
- 项目结构不够统一,仅仅靠
vuecli
来初始化项目远远不够 - 不想每次项目初始化都
yarn add
一堆依赖 - 前端项目类型多,不同项目配置不同,管理成本高
- 单靠
git clone
基线这些方式不方便
解决这些痛点最核心的一个方案就是,通过定制多套模板,然后用脚手架去拉取模板进行项目初始化。但是要做好这套方案,也有很多细节需要讨论的,比如:
- 模板支持拓展
- 零成本接入新模板
- 根据用户选择,生成定制化模板
- 友好的界面交互
- ...
除此之外,拉取项目只是脚手架的一个功能而已,我们期望脚手架是一个效率工具,而不只是用来拉取项目模板这么简单,我们希望脚手架提供更多的功能支持,比如:
- 集成项目上和各个前端团队已有的
node script
- 在旧项目上一键初始化vscode开发环境
- 在旧项目上集成
cz-customizable
- 脚手架升级提示
- ...
所以,脚手架更像是一套命令集,使用nodejs
提供的api,快速接入各种自定义命令,调用便可达到各种目的。
# xcli的诞生
开会讨论,确定好当前需求,我就开始动工了。要做的第一件事就是,这个脚手架叫什么名字呢?想到我所有在团队叫xdata,就打算取xdata-cli
,后来跟宋哥讨论了下,决定改叫xcli
了,因为用起来可以更快速,少敲几个字嘛😀。目前公司这套脚手架整体架构已经搭建完毕,上面提到的需求也已基本实现,下面就来跟大家聊聊这套脚手架技术方面的内容。
# 技术选型
开发语言这边使用typeScript
,ts的好处不用多说了,况且还有vscode的加持,写起来体验非常nice。下面是具体选型:
- node.js >= 10
- typeScript
- eslint
- prettier
- ts-jest
- husky
- lint-staged
除此之外,nodejs生态提供了很多优秀的开源工具库,这些库都非常适合用来脚手架的开发,其实vuecli
这些大名鼎鼎的开源库,也是依赖这类开源库进行开发的。下面是使用的开源库
commander (解析命令)
fs-extra (fs模块拓展)
execa (better child_process)
download-git-repo (下载git仓库工具)
boxen (终端中方框输出)
cfonts (终端炫酷的字体输出)
chalk (给文字增加色彩)
ora (loading工具)
handlebars (模板插值)
module-alias (模板别名映射)
inquirer (交互式询问并记录结果)
listr (多任务串行执行,并提供loading效果)
有了这些开源库的支持,脚手架开发起来真的方便多了
# 项目结构
目前的项目结构是这样的
├── bin
│ └── index.js 入口文件
├── dist 生产文件
├── src
│ ├── utils 工具集
│ ├── commit xcli commit
│ ├── create xcli create
│ ├── initenv xcli initenv
│ ├── script xcli script
│ ├── update xcli update
│ ├── types
│ ├── unExpectInput.ts xcli
│ ├── updateNotify.ts 通知更新
│ ├── commands.ts 注册命令入口
│ └── index.ts
├── tests 单元测试
├── tsconfig.json
├── jest.config.js
├── rmrf.js
├── package.json
└── yarn.lock
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
上文已经提到脚手架其实就是命令的集合,一切的功能使用都离不开命令相关操作,所以这边就采用平铺的方式来管理每个命令集,也就是用文件夹的方式来管理每个命令,比如create
、commit
等等这些,这些就可以把具体实现的文件都放到对应的文件夹进行维护和迭代开发了。
# 具体实现
# bin文件
所谓的bin入口文件,也就是可执行的文件,在package.json
配置之后,npm会找到对应的可执行文件的位置,然后在node_modules/.bin
目录下建立对应的符合链接。比如我们在安装某些库的时候,在node_modules
里会看到新增的符合链接文件
这样就可以通过node_modules/.bin/xxx
方式直接使用某个命令。当用npm i -g xxx
方式安装全局包的话,全局包会被安装到{prefix}/lib/node_modules/
中,也就是npm
全局node_modules
,其中,npm
还会在{prefix}/bin/
文件夹中创建一个符合链接,在使用xxx
全局命令的时候,也会在这个bin
文件夹进行命令的查找和执行,例如我们熟悉的vuecli
使用方式,原理也是一样,我们在安装完vuecli
后,在全局bin
文件夹中确实能找到vue
的符合链接
那么问题又来了,怎么在package.json
中进行bin文件配置呢?
很简单
{
"bin": {
"xcli": "bin/index.js"
}
}
2
3
4
5
再执行下yarn link
或者npm link
就可以把xcli
的bin文件链接到全局npm
bin文件夹上啦,这样在终端上跑xcli
就能执行bin/index.js
文件了!
# index.js
上文说了,xcli
执行实际上就是跑的bin/index.js
文件,先来看看index.js
文件做了什么
#!/usr/bin/env node
const currentNodeVersion = process.versions.node;
const semver = currentNodeVersion.split('.');
const major = semver[0];
const Cfonts = require('cfonts');
// Node版本校验
if (+major < 10) {
console.log(
'您在使用 Node ' +
currentNodeVersion +
'.\n' +
'xcli 需要 Node >= 10.0 版本. \n' +
'请升级 Node 版本!.'
);
process.exit(1);
}
Cfonts.say('xcli', {
font: 'block', // define the font face
align: 'left', // define text alignment
colors: ['system'], // define all colors
background: 'transparent', // define the background color, you can also use `backgroundColor` here as key
letterSpacing: 1, // define letter spacing
lineHeight: 1, // define the line height
space: true, // define if the output text should have empty lines on top and on the bottom
maxLength: '0', // define how many character can be on one line
gradient: ['blue', 'red'], // define your two gradient colors
independentGradient: false, // define if you want to recalculate the gradient for each new line
transitionGradient: false, // define if this is a transition between colors directly
env: 'node',
});
require('../dist/src/index');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
其中可以看到,在初始化的时候会对nodejs
的版本进行一次校验,如果node
版本小于10,那么就直接退出程序。下面还用了Cfonts
做了层比较炫酷的文字效果,让初次使用的用户能有眼前一亮的效果,不至于觉得平乏无味😁,下面是使用xcli
的演示效果
请注意上面代码最后一句require('../dist/src/index');
,这里直接拿了dist
下的入口文件跑了,其实这个就是ts
编译后的代码,因为node
是跑不了ts
代码的,除非使用ts-node
,但是这边暂时不考虑使用,所以就直接用了这种方式。
dist/src/index.js
其实就是对应着src/index.ts
文件,在这块,主要是负责注册命令、检测版本和更新通知的模块执行。
# 注册commands
注册commands
其实是通过commander这个开源库进行命令注册,使用方式看文档,这边不做具体介绍。
代码实现:
import program from 'commander';
export default function commands(): void {
/**
* 创建项目
*/
program
.command('create [projectName]') // <xxx> 必填,[xxx] 可填)
.alias('c')
.description('创建项目')
.action((name) => {
import('@/create').then((m) => m.default(name));
});
// 省略ininenv、commit等...
/**
* 什么参数都不输入,直接输入 xcli
*/
program
.allowUnknownOption()
.action(() => import('@/unExpectInput').then((m) => m.default()));
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
可以看到,这边主要是用了commander
进行了命令注册,非常简洁方便。其中,注册的模板用了按需加载方式进行加载,而不是在注册前就把模板加载进来,用按需加载的好处就是可以加快脚手架启动速度,用户使用到了某个命令再去加载对应的模块代码,比如我使用了xcli create
再去加载create文件夹下的相关代码。
除此之外,这边使用了program.allowUnknownOption()
来支持输入xcli
直接启动。这样的好处一是可以少打点字快速启动,二是可以让用户看看这个脚手架能干嘛,也不用记住每个具体的命令了,这种方式比设置命令别名更加方便。
# xcli create
当时我做个脚手架,第一个功能就是要实现拉取项目模板。xcli create
便是这个需求的具体实现。下面是这个功能的流程图
这边主要的核心实现,其实就是用inquirer
库来实现交互式询问效果,这个库文档详细,官方还提供了很多示例,可以在这进行学习。
拉取模板的话,会使用gitlab api
去拉取模板组,然后取出数组提供给用户进行选择下载,这样就非常便于模板的拓展,如果以后有了新的模板,那么也不需要修改脚手架代码了。
使用了listr
管理任务队列,支持promise
const tasks = new Listr([
{
title: '拉取项目模板',
task: () => download(answer),
},
{
title: '处理模板插值',
task: () => interpolationAppConfigJson(answer),
},
{
title: '安装项目依赖',
task: () => setupNodeModules(answer),
},
]);
await tasks.run();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
这样,把三大块任务用listr
进行管理,这样代码看起来也就十分清晰了。
# xcli commit
xcli commit
,为什么要做这个呢,其实完全是个人的想法。先来介绍这个功能,首先,我们现在的国土空间项目,写git message
都是用yarn commit
这种方式,啥是yarn commit
呢,其实就是使用git-cz
这里不会介绍git-cz的用法,有兴趣的可以google学习下。这个主要是配合commitlint
来使用的,能生成符合规范的git message
,比如这种feat(公共模块): 新增一个功能
的git message
,是符合Angular团队规范的,看起来非常舒服,也方便记录查找。但是,我发现了一个问题,配置git-cz
和commitlint
这些需要安装好多的依赖库,很耗时,时间久了也容易忘记。虽然可以在模板上配置,然后在新项目中使用,但是在旧项目中配置就非常繁琐了,所以我想到了这完全可以使用脚手架来接管这件事。
经过一番研究,发现cz-customizable
这个库里面提供了standalone.js
模块支持独立运行,这样,这个功能完全可以使用这个模块来实现以上需求。
查看了standalone.js
模块的源码,发现是需要提供.cz-config.js
文件才可以运行,本来以为这个库有函数支持使用配置对象来执行,就不强制用户提供了,但是看了源码是没有的,可能作者觉得用文件来管理比较好吧。于是,我这边就做了两种选择:
- 如果用户项目根目录中提供了
.cz-config.js
,则直接运行standalone.js
- 如果用户项目根目录没有
.cz-config.js
文件,则会提示是否需要自动帮用户创建
export default async function commit(): Promise<void> {
const isFileExists = await checkConfigFileExists();
// 校验.cz.config.js文件是否存在
if (isFileExists) {
// 跑standalone.js
return runCz();
}
log.error(`项目根目录下没有找到 ${hlText(CONFIGFILENAME)} 配置文件!`);
const { wantCreateFile } = await inquirer.prompt({
type: 'confirm',
name: 'wantCreateFile',
message: `是否需要帮您创建 ${hlText(CONFIGFILENAME)} 文件?`,
default: true,
});
if (wantCreateFile) {
await createConfigFile();
log.success(`创建 ${hlText(CONFIGFILENAME)} 配置文件成功!`);
return runCz();
} else {
log(
`🎃 请手动创建 ${hlText(CONFIGFILENAME)} 配置文件,再使用 ${hlText(
'xcli commit'
)}!`
);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# xcli script
xcli script
主要是用跑各种已有的功能脚本,因为之前在各个团队都产出了一些脚本,所以这边就要把这些脚本都集成进来。目前这些脚本都是用js
写的,不过没关系,ts
是可以加载js
文件的,在tsconfig.json
中开启allowJs
即可
{
"compilerOptions": {
"allowJs": true
}
}
2
3
4
5
当前已经集成了3个脚本,使用时可通过inquirer
进行询问
const questions: ListQuestionOptions = {
type: 'list',
name: 'scripts',
message: '选择执行功能脚本',
choices: [
{
name: '生成一个vue组件',
value: 'gen-comp',
},
{
name: '生成运维权限路由json',
value: 'gen-route-json',
},
{
name: '生成rollup.config.js配置文件',
value: 'gen-rollup-config',
},
],
};
export default async function scripts() {
const answer = await inquirer.prompt([questions]);
const { scripts } = answer;
return scripts && scriptsMap[scripts]();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
这块主要就是提供个平台用来存放和执行各种ts
或者js
的node脚本,相关脚本这边就不作具体讲解了。
# xcli update
xcli update
顾名思义,就是用来更新xcli
的。当用户启动脚手架的时候,就会去检测是否需要更新,然后把提示信息打印出来,现在很多脚手架都是有这个功能的,所以我这边也顺便实现了下。
先来抛出个问题,怎么检测更新?
原理很简单,其实npm
有个命令是支持的,即npm show @dist/xcli version
可以拿到npm私有仓库上的最新版本。这样,拿到了最新版本,和本地package.json
的version
值比较下,就知道是否需要更新了。
import pkg from '../../package.json';
export async function checkUpdate(
needVersionInfo = false
): Promise<CheckUpdateInfo | boolean> {
const isConfigCorrect = await checkNpmConfigCorrect(); // 检查 npm 配置是否正确
if (!isConfigCorrect) return false;
const { stdout } = await execa.command(`npm show @dist/xcli version`);
const { version } = pkg;
if (needVersionInfo) {
return {
shouldUpdate: stdout > version ? true : false,
curVersion: version,
newVersion: stdout,
} as CheckUpdateInfo;
}
return stdout > version ? true : false;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
如果检测到了需要更新,则会给用户提示
升级的具体实现
export default async function update(): Promise<void> {
// 修改正在升级状态,不 log 提示升级框
// 设置锁
setUpdating(true);
const shouldUpdate = await checkUpdate();
if (shouldUpdate) {
const spinner = ora('正在升级脚手架版本...').start();
try {
await execa.command(`npm i -g @dist/xcli`);
spinner.succeed('脚手架版本升级成功!');
} catch (error) {
log(error);
spinner.fail('脚手架版本升级失败!');
log(`请手动输入 ${hlText('npm i -g @dist/xcli')} 升级`);
setUpdating(false);
}
} else {
setUpdating(false);
ora().info('xcli已经是最新版本!');
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# xcli initenv
xcli initenv
主要是用于初始化开发环境,目前主要用于快速生成vscode的settings.json
文件,基础模板是已经配好settings.json
的,但是很多旧项目是没有配好的,所以这个命令可以在旧项目中一键生成配置信息。
代码实现也比较简单,只是把内置的settings.json
拷贝过去即可
import settinsJson from './settings.json';
const SETTINGSJSONTARGET = path.resolve(
process.cwd(),
'./.vscode/settings.json'
);
async function initVscodeSettings() {
const spinner = ora('正在初始化项目vscode配置信息').start();
try {
await fs.outputFile(
SETTINGSJSONTARGET,
JSON.stringify(settinsJson, null, 2),
'utf8'
);
spinner.succeed('初始化项目vscode配置信息成功');
} catch (error) {
console.log(error);
spinner.fail('初始化项目vscode配置信息失败');
}
}
export default async function initEnv(): Promise<void> {
await initVscodeSettings();
// 未来将会支持更多功能...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 总结
可以发现,其实开发一个脚手架一点都不难,只需要熟悉一些nodejs
和一些shell
命令就能搞出来。每个人可以根据个人习惯,写个自用版的脚手架出来,具体要做什么,完全看你怎么发挥啦,一切都是为了提效(偷懒)。