近期公司领导要求造轮子,以提升团队技术沉淀(0_0),所以要求团队内部人员都要去进行相应的技术基础建设,由于本人以前搞过简单的脚手架工具,所以cli这部分就交由我来进行开发。我个人对这部分内容也是比较感兴趣(出去面试能吹一会),本文将会沿着我个人的思路出发,从零搭建一个前端cli工具。
cli功能整理
接到任务,在调研内部外部多个cli工具后(借...鉴...)吸收百家之所长,结合公司当前开发流程,整理了如下优化点:
- 版本检测
- 所有命令执行前,统一检测工具版本,提示升级
- 初始化 - init
- 统一项目模版,多个项目模版可供选择
- 整合vue/cli(部门技术栈以vue为主)
- 统一eslint,部门代码风格统一
- 统一开发配置,针对部门特点的个性化配置
- 启动 - dev
- 检查项目配置
- 提供数据mock
- 打包 - build
- 符合公司打包规范
- 检查配置文件
- 静态资源路径替换
- 发布 - public (公司为什么没用自动构建啊)
- 检查发布所需配置文件
- 检查发布所需打包文件
- 发布到指定cdn
以上是我对cli工具功能的整理,比较符合我们当前的开发流程。功能整理完,开干!
前期准备
在正式开始写代码之前,我们有必要先找一些好用的插件,以提升我们的开发效率,cli相应的开发库百度一下你就知道,以下我列举了我的项目里用到的库。
node工具
- path:原生的路径工具
- fs:原生的文件读写工具
- child_process:node子进程包
- process: node进程信息
// path
// 生成一个规范化的绝对路径
path.resolve(__dirname, b)
// /User/Desktop/code/b
// - 路径连接
path.join(a,b,c)
// a/b/c
// fs
// 读取文件夹
fs.readdirSync
// 读取文件
fs.readFileSync
// 文件路径是否存在
fs.existsSync
// 返回文件/文件夹信息
const f = fs.statSync
f.isDirectory() // 是否是文件夹
f.isFile() // 是否是文件
// 写入数据
fs.writeFileSync
// 子进程
import {execSync, spawnSync} from 'child_process';
// 传参形式不同
execSync('npm run dev') // 返回执行结果 最大200k
spawnSync('npm', ['run', 'dev']) //返回执行结果 大小不限
import { cwd } from 'process';
// 返回当前执行命令目录绝对路径
cwd()
复制代码
第三方插件:
开始 - 创建项目
项目结构
脚手架的开发使用的是ts语法. 最终转译成js语法。
npm install -g typescript
npm init
tsc --init
npm i path fs ..... fontmin
复制代码
目录结构:
- src: 源代码
- index.ts 入口文件
- utils 工具函数
- actions 命令
- bin: 导出目录
- package.json
- tsconfig.json
package.json配置
// package.json
{
// 脚手架名称
name: "xxx-cli",
// 脚手架版本,必须三位,否则发布时会报错
version: "1.0.0",
// 脚手架执行名称 以及对应入口
bin: {
xxx: './bin/index.js'
},
......
// npm发布时需要的文件
files: [
"bin"
]
}
复制代码
配置tsconfig.json
{
"compilerOptions": {
"types": [
"node"
],
"typeRoots": [
"node_modules/@types"
],
"outDir": "bin",
"strict": true,
"allowSyntheticDefaultImports": true,
"module": "commonjs",
"paths": {
"src/*": [
"src/*"
]
}
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules"
]
}
复制代码
版本检测
检测本地工具的版本和npm最新版本,如果本地版本低于npm版本,提示升级,否则不执行动作
// src/actions/checkVersion
// 伪代码
const checkVersion = () => {
// 获取本地版本号
const local = new Promise((resolve) => {
let localStr = '';
const childProcesslocal = exec('xxx -V');
if (childProcesslocal.stdout) {
childProcesslocal.stdout.on('data', chunk => {
localStr += chunk;
});
childProcesslocal.stdout.on('end', () => {
childProcesslocal.kill();
resolve(localStr);
});
}
});
const latest = .....同上 exec('npm views xxx versions');
if (latest > local) {
// 升级
npm i -g xxx@latest
return true;
}
};
复制代码
入口文件
入口文件通过识别用户输入的命令,执行对应的操作。我们可以对命令进行优化,添加可选参数,对动作进行修改. 这里我们主要用到的是commender命令行工具,以下是init命令的伪代码,其他命令添加方式基本一致,按需使用。
src/index.js
// 用户执行 xxx init demo
// 执行 xxx init 工具的init命令不会生效
// <project-name> 相当于用户输入的第三个参数
program.command('init <project-name>')
.description(chalk.yellow('初始化项目'))
.action((projectName: string) => {
init(projectName);
});
// 另一种写法
// 当我们添加了option后,执行 xxx init -t
// 那么返回的options参数中template = true
// -t 参数是可选项,所以执行 xxx init 工具的init功能生效,
program.command('init')
.options('-t, --template', '初始化一个模版')
.description(chalk.yellow('初始化项目'))
.action((options) => {
// options = {template: }
if (checkVersion()) return;
init(options);
});
program.parse(process.argv);
复制代码
init命令解析
执行 xxx init 命令,我们需要向开发者提供多种模版的选项,我所在的公司部门的项目目前有三大类
- 小游戏
- 活动
- 宣传
我的思路是:
- xxx init
- 下载模版代码
- 提示用户选择模版
- 其他配置选项
- 初始化项目
- 复制模版到项目内
- 执行npm install
- done
模版代码库配置一个json文件,每做一个模版,就在json文件内进行配置,拉取模版之后,读取json文件,然后在向用户展示,这样的话我们以后新建模版,就不用对工具进行升级操作了。
项目统一配置也可以抽离出去,维护一个文件,放在代码库内,生成项目后,直接copy到项目内即可。
vue项目预设
关于初始化项目,因为我部门技术栈是vue,所以我们初始化时其实是用了vue create命令,这里有一个问题就是直接vue create 项目的话,会出现vue/cli的配置选项,如果配置错误,可能会影响我们项目统一的config配置,所以我们需要直接跳过vue/cli的配置项。这里vue/cli提供了一个命令
vue create app -p path
复制代码
我们可以指定一个预设模版(path),这样vue就会按照预设模版进行项目初始化,并且预设模版还可以让我们直接注入项目所需要的npm包。
// preset/preset.js
// vue/cli配置项
{
"useConfigFiles": true,
"cssPreprocessor": "sass",
"plugins": {
"@vue/cli-plugin-babel": {},
"@vue/cli-plugin-eslint": {
"config": "airbnb",
"lintOn": ["save", "commit"]
},
"@vue/cli-plugin-router": {},
"@vue/cli-plugin-vuex": {}
}
}
// preset/generator.js
// 初始化项目时在package.json内添加npm包
module.exports = (api) => {
api.extendPackage({
dependencies: {
xxxxxx,
xxxxx,
},
devDependencies: {
"webpack": "^4.3.2",
}
});
}
复制代码
Init模块
init模块实现大概如下:
// src/actions/init.js
export default function Init() {
// 下载模版
spawn('git', ['clone', 'git.xxx'], { cwd: cwd() });
// 读取模版
const config = fs.readFileSync('template.json');
// 用户控制台选择模版
inquirer.prompt([
{
type: 'list',
choices: config,
name: 'templateName'
}
])
// vue/cli初始化一个项目,并使用预设配置,
// 如果公司有自己的npm镜像,也可以设置一下
spawn('vue', ['create', projectName, '-p', presetPath, '-r', 'xxx.npm']);
// 复制模版
spawn('cp', ['-r', templatePath, projectPath]);
console.log('done');
}
复制代码
init部分的代码最主要的是对模版的下载以及模版注入到初始化项目,这里涉及git,shell,vue命令,并且各种路径的使用也比较复杂,以及对各种情况的判断,提高容错,基本上init做完后,其他命令就很简单了, 熟能生巧了属于是。
End
虽然是老板的任务,但是在开发过程中是有许多收获的,之前一直想写一些有意思的东西,但是总是被其他事所影响,中途放弃,这次算是在外力的影响下,逼着自己去做了一个对我个人而言有意义的事(有东西可以吹了)。以上代码以及开发思路只是对这次cli开发的一点总结,不够完善,但是对于之前没有开发过的人来说,是一次难忘的体验。后期还会再对其他比较有意思的功能进行一个总结。