本文首发于个人 Github,欢迎 issue / fxxk。
前言
相信大家都写过vue
,react
或者angular
的各位同学,也一定不会对以下库陌生:
体验过上述工具的同学,有没有发现他们都有一个共同点——提供了一个可供快速开发的样板文件(boilerplate)。本文就将从样板文件入来进行阐述。通过本文,你将学到:
- 一个
CLI
工具需要解决的问题; - 写一个
CLI
的基本思路; - 写一个
CLI
需要做哪些准备;
PS:由于脚手架的英文 scaffolding 太长,本文我将以更可爱的 cli 来代替。
预热
由于篇幅有限,本节将以create-react-app
和vue-cli
为例(真地很难说服大家我是ng
起家的...),回顾其使用过程:
首先是create-react-app
, 按照README,我们生成出一个基本项目后,打开目录,其目录结构如下:
my-app
├── README.md
├── node_modules
├── package.json
├── .gitignore
├── public
│ └── favicon.ico
│ └── index.html
│ └── manifest.json
└── src
└── App.css
└── App.js
└── App.test.js
└── index.css
└── index.js
└── logo.svg
└── registerServiceWorker.js
说一下其中两个有意思的点:
public/manifest.json
: 这是PWA 的一部分,用来描述应用相关的信息。以前开发cordova
的时候,这个还用来做过热更新。src/registerServiceWorker.js
: 安装Service Workers文件。
很久没用它,原来已经默认支持
PWA
了,nice, 还不会的同学赶紧学起来,这里就不展开了。
接着,我们打开package.json
,探一下究竟:
{
"name": "my-app",
"version": "0.1.0",
"private": true,
"dependencies": {
"react": "^16.2.0",
"react-dom": "^16.2.0",
"react-scripts": "1.1.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
}
非常简洁,但是有一个react-scripts
。看起来很陌生,但是如果我告诉你它的依赖中有babel
、webpack
、webpack-dev-server
和autoprefixer
这些常见的前端流氓,我想你也清楚了react-scripts
到底做了什么:
包装了
webpack
和webpack-dev-server
,提供一套默认的配置(内置css,file,svg等等各种loader),在react-scripts start
和react-scripts build
时直接运行这些配置。
一句话概括,就是为了帮你简化构建时的配置(Create React apps with no build configuration.😂)。
当然,零配置并非适用于大型定制项目的开发,react-scripts
就像一个巨大的黑盒一样,总有存在一些其未提供 API 或者指令的场景,让你无从下手。react-scripts
当然也不傻,还提供了一个比较fancy的指令:
react-scripts eject
: 将所有的工具(配置文件和 package.json 依赖库)解压到应用所在的路径。
厉害了😅,来运行看看:

当所有的依赖暴露出来是,果然package.json
瞬间爆炸性增长了,顺时有点怀念从前的美好了。
好了,说到这里,create-react-app 要解决的问题就是:
- 根据样板文件生成统一的项目骨架,让开发者快速投入开发;
- 预置了一个易于开发的
react-scripts
,让开发者省略痛苦的配置流程.(webpack
配置工程师看来要失业了😇)
接下来,再回顾一下 vue-cli
。截止本文书写日期, vue-cli@3.0 仍然处于 beta 阶段,因此本文将以 2.x.x 为例,我们创建一个名为 my-vue-app,模板为 webpack-simple 的 vue 项目:

可见,vue-cli在生成项目之前多了一个非常重要的一步 —— Prompt,也就是问询,根据询问的内容最终生成你的项目。两条FYI:
- vue-cli 的问询功能是使用 Inquirer.js 这个库完成的。
- webpack-simple 源码你可以在这里找到 webpack-simple 。
对本节做一下结,一个脚手架通常由以下基本几部分组成:
- 问询(Prompts)
- 样板文件(Boilerplate)
- 生成文件(Generate)
实战
经过上一节的洗礼,你可能已经有大致的思路了,然后,让我们以 vue-cli 为例,直接进入实战吧。
基本思路
- 定义一个模板包的规则,这里采用 vue-cli 的规则:template 为源文件,meta.js/meta.json 为配置入口文件。
my-first-package
├── meta.js
└── template
- 解析包的meta.js,获得要询问的问题(prompts),并运行它,将最终用户的答案分配到一个上下文对象中;
- 读取 template 中的内容,用刚刚获取的上下文对象去渲染它。
伪代码实现
以下是一个CLI的伪代码实现:
// 10行伪代码实现一个CLI
function CLI(packageSourcePath) {
const context = {}
const meta = require(path.join(packageSourcePath, 'meta.js'))
const templatePath = path.join(packageSourcePath, 'template')
const { prompts } = meta
return promptsRunner(prompts).then(anwsers => {
Object.assign(context, anwsers)
return generateFiles(templatePath, context)
})
.then(() => console.log('[OK]'))
.error(() => console.log('[Error]'))
}
其中,promptsRunner用来问询,generateFiles 用来渲染并生成文件。哇!原来这么简单。
技术选型
这是 vue-cli 的技术选型:
- 问询:Inquirer.js
- 命令行解析:commander.js
- 模板渲染:handlebars.js
- 文件生成:metalsmith
热衷于看这个世界的你,是否已经跃跃欲试了呢?
更多
当然,如果只是这样的一个 CLI,很显然只是玩具,你可以考虑支持以下可爱的特性:
- 在Context中注入默认的一些属性(如git的username)
- 支持文件过滤(根据Context过滤)
- 支持文件重命名(vue-cli默认不直接支持,但可以通过 metalsmith 的插件实现)
- 支持包管理(如包的生成,缓存,拉取,更新,删除,自动化测试)
- 提供一些生命周期的钩子(如beforePrompt,beforeRender,beforeExit等等)
- 动态的输出路径(这个对于我来说很有用)
当然,还有很多了,只要你想得到。
甜点时刻
是时候给大家介绍一些甜点了。
SAO
Github传送门:github.com/saojs/sao
一个听起来很骚气的名字,这是我们可爱的 EGOIST 写的一个库,基本上实现了上述我说的所有特性。
最为关键的是,SAO目前已经提供了大量的高质量的样板文件:
name | description |
---|---|
template | Template for scaffolding out an SAO template |
lass | Lass scaffolds a modern package boilerplate for node |
lad | Lad scaffolds a Koa webapp and API framework for node |
vue | Kickstart a Vue project with Poi |
gi | Generate .gitignore file in your project |
nm | Scaffold out a node module |
vue-webpack | Vue.js offcial webpack template (SAO port) |
basic | Basic project skeleton |
react | SAO template for react with vbuild |
micro-service | Scaffolding out a micro-service |
node-cli | Scaffold a node cli tool |
next | Scaffold out a Next.js project |
electron | Scaffold out an Electron project |
expo | Scaffold out an Expo app |
太强大了,这也大概就是我不得不爱 EGOIST 的原因了吧。
poz
Github传送门:github.com/ulivz/poz
由于在实际生产中需要支持 重命名 和 动态输出路径,我写了 POZ 这个库,在前人的基础上,基于完全不一样的实现,实现了一样的功能,并加了一点儿特效,这大概就是造轮子的乐趣吧。欢迎大家 Fxxk/Issue。
最近 POZ 也计划开始开发 1.0的稳定版本了,快来看看这个 RoadMap,太有野心了有木有。
alphax
Github传送门:github.com/ulivz/alpha…
这是 poz 底层使用的一个库,基于Stream,灵感来源于 metalsmith 和 SAO 底层的 majo,最近我彻底重写了该库,让其有了以下可爱的特性:
- 极简的API;
- 任务流控制;
- 中间件;
- 基于纯函数或JSON的的文件过滤;
- 基于纯函数或JSON的文件重命名;
- 支持不写入硬盘,易于测试;
它使用起来像这样:
alphax()
.src('**')
.task(task1)
.task(task2)
.task(task3)
.use(file => file.content += Date.now())
.rename(filepath => filepath.replace('{name}', name))
.rename(filepath => filepath.replace('{age}', age))
.transform(content => content.replace('{name}', name))
.filter(filepath => filepath.endWith('.js'))
.filter(filepath => !filepath.startWith('test'))
.dest('dist')
.then(files => console.log(files))
.catch(error => console.log(error))
如果你不喜欢函数,也可以用配置的方式来:
const config = {
tasks: [task1, task3, task3],
use: file => file.content += Date.now(),
rename: {
'{name}': name,
'{age}': age
},
filter: {
'app.js': true,
'test.js': false
},
transform(content) {
return content.replace('{name}', name)
}
}
alphax()
.src('**', config)
.dest('dist')
.then(files => console.log(files))
.catch(error => console.log(error))
相信这个lib已经解决掉你大部分的 meta.js 的API设计问题了😄。附上用可爱的 docute 写的文档地址: Documentation
总结
一个CLI归根到底是要解决的是生产力和统一性的问题,但是,对于create-react-app这种过度封装,和 vue-cli@2.x.x 的过度松散,似乎都不是最佳方案。配置少和拓展性高这本来就是两个互相矛盾的话题,从长远来看,选择怎样的CLI还是依赖于具体的场景,但作为一个CLI开发者,如果做到更好的平衡,还值得多多思考。
本文仅谈及了写一个CLI工具的第一部分,其基本思路较为简单,只是实现层面会有较多的优化点和 error catch 😅,Good luck!
下文,我将继续阐述类似于 create-react-app 中 react-scripts 的基本实现原理及其思路,实际上,这也是 vue-cli@3.x.x 和 poi 所具有的功能,敬请关注。
以上,全文终。)