本文首发于个人 Github,欢迎 issue / fxxk。
前言
在系列的上一篇《如何开发一个可爱的CLI(一)》中,我给大家讲述了如何开发一个生成、渲染、转换样板文件(Boilerplate)的简单脚手架工具。本文,将是愉快的进阶环节 —— 如何基于webpack
写一个 “零配置” 的命令行工具(暂且命名为lovely-cli
.),实现以下功能:
lovely-cli dev # 以开发环境,webpack watch的模式启动一个应用
lovely-cli build # 以生产环境的模式构建应用
复制代码
首先,我要再次强调一个概念。尽管我在第一篇中我有备注:
“由于脚手架的英文 scaffolding 太长,本文我将以更可爱的 CLI 来代替。”
复制代码
但仍然有些同学提出疑问(有一位同学回复我是因为习惯性地滑的太快,所以没看到...),所以在第二篇的开始,我再次对CLI
、Scafollding
的概念全面阐述一次:
- CLI,其全称是 command-line interface,也就是命令行界面。你我常用的git,算是比较著名的一个命令行工具了(你若是用source tree党,当我没说):
- 脚手架,其英译是 Scafollding,关于它的概念,请首先看这张图:
没错,这就是脚手架,在建筑领域,无论是大工程还是小工程,都需要各式各样的脚手架,脚手架工程师首先搭好架子,然后工人们慢慢往里面堆砌砖头。
如果你只是建1层楼的平房,你可能只需要一个梯子足矣;如果是10层,上图的脚手架可能也足够了;但如果是50层、100层,你的脚手架对结构、承重、安全性将会有更多的考虑(PS:你至少得加个电梯吧。)
回到编程,为什么脚手架这个概念开始在前端领域兴起呢?说到底,都是源于工程化的崛起。随着经济的发展和人民生活水平的提高,富足的人民群众,对软件的体验需求越来越高,前端的页面也越来越复杂,十几年前几个JS文件能搞定的页面需求,现在可能需要几百,甚至几千个(哭)。后来,Node出现了,模块化出现了,任务调度和构建工具出现了,前端工程化也就诞生了,而脚手架在其中扮演的角色,和建筑工程中一样 —— 帮你搭建好基本的开发环境,其中包含生成基本项目结构、基本代码和开发流程的基本配置和脚本。
而 vue-cli,首先,它是一个 CLI, 其次,它才是一个 Scafollding,到 3.0 以后,它还算得上一个 build tool。
所以,不要再问我为什么标题不用 Scafollding 了。
分析
回到我们的需求:
lovely-cli dev # 以开发环境,webpack watch的模式启动一个应用
lovely-cli build # 以生产环境的模式构建应用
复制代码
根据需求,我们可以划分出如下子任务,并提出相应的疑问:
- 如何用node写一个全局可用的CLI命令 lovely-cli?
- 如何支持子命令(dev、build)?
- 如何让 dev 运行 webpack dev,让 build 运行 webpack build?
实施
前两条需求,我相信大多数前端/Node程序员都已经有一定了解或者非常熟悉了,这里放上阮老师的文章 《Node.js 命令行程序开发教程》 作为参考,忘记的同学快去复习吧。我快速过一下:
任务一
- 新建项目结构如下:
lovely-cli
├── package.json
└── src
└── index.js
复制代码
index.js :
#!/usr/bin/env node
console.log('I am a lovely CLI')
复制代码
package.json 的内容如下:
{
"name": "lovely-cli",
"version": "0.0.1",
"description": "A lovely CLI",
"bin": "src/index.js"
}
复制代码
sudo npm link 一下, 看到这个就说明全局注册成功了。
/usr/local/bin/lovely-cli -> /usr/local/lib/node_modules/lovely-cli/src/index.js
/usr/local/lib/node_modules/lovely-cli -> /Users/haolchen/Documents/__self__/lovely-cli
复制代码
OK,运行一下:
任务二
修改一下 index.js 如下:
#!/usr/bin/env node
const command = process.argv[2] // 不懂为什么这样写的同学不够可爱哦,请回去复习。
if (command === 'dev') {
console.log('Running in development mode.')
} else if (command === 'build') {
console.log('Running in production mode.')
} else {
console.log('I am a lovely CLI.')
}
复制代码
运行一下:
注:由于handle命令行参数不是本文的重点,本文的演示将均以 vanilla node.js 进行演示。你可能会使用 yargs、commander.js 等等这类元老库,而我推荐一下可爱的 egoist 出品的 cac。
任务三
这算是本文的重点了,我相信有心的同学看到这里,一定已经有思路了:
- 运行 lovely-cli dev => 用开发环境的配置,用 webpack-dev-server 运行起一个webpack app.
- 运行 lovely-cli build => 用生成环境的配置,用 webpack 直接 build.
首先 yarn 一下:
yarn add webpack webpack-dev-server -D
复制代码
接下来,Just show you the code:
// 20行伪代码实现一个 lovely-cli
#!/usr/bin/env node
const command = process.argv[2]
const Webpack = require('webpack')
const WebpackDevServer = require('webpack-dev-server')
const defaultDevConfig = {}
const defaultProdConfig = {}
if (command === 'dev') {
const compiler = Webpack(defaultDevConfig)
const devServerOptions = defaultDevConfig.devServer
const devServer = new WebpackDevServer(compiler, devServerOptions)
devServer.listen(8080, 'localhost', () => console.log('Starting server on http://localhost:8080'));
} else if (command === 'build') {
Webpack(defaultProdConfig)
} else {
console.log('I am a lovely CLI.')
}
复制代码
很简单吧,既然思路都有了,那我们来继续完善代码,写一个基础的可用版吧。
首先,完善项目结构:
lovely-cli
├── package.json
├── index.html (新增)
├── package.json
├── src
│ ├── default-webpack-config.js (新增)
│ ├── entry.js (新增)
│ └── index.js
└── dist (新增)
复制代码
其中,关于4个新增文件:
- index.html:页面入口,你也可以用 HtmlWebpackPlugin 解决;
- default-webpack-config.js:默认的 webpack 配置;
- entry.js:客户端代码入口;
- dist:输出目录。
首先看一下 default-webpack-config.js:
'use strict'
const { resolve } = require('path')
module.exports = {
entry: resolve(__dirname, 'entry.js'),
output: {
path: resolve(__dirname, '../dist'),
publicPath: '/dist/',
filename: 'bundle.js'
}
}
复制代码
再常规不过的配置了,其中,没用过 publicPath 作用的同学请参阅 官方文档。
接着是 index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Lovely CLI</title>
</head>
<body>
<div id="app"></div>
<script src="/dist/bundle.js"></script>
</body>
</html>
复制代码
以及打包入口文件 entry.js:
const app = document.querySelector('#app')
app.innerHTML = '<h1>Lovely CLI</h1>'
复制代码
在看 index.js 文件做了哪些修改之前,先来体验一下我们的成果吧:
- 运行 lovely-cli dev:
打开浏览器:
OK,一切按预料运行。
- 接着,运行 lovely-cli build:
混淆后的文件也已经生成:
最后,让我们再来看看 CLI 的核心入口文件做了哪些更改:
#!/usr/bin/env node
const command = process.argv[2]
const Webpack = require('webpack')
const WebpackDevServer = require('webpack-dev-server')
const defaultConfig = require('./default-webpack-config')
const defaultDevConfig = Object.assign({}, defaultConfig, { mode: 'development' })
const defaultProdConfig = Object.assign({}, defaultConfig, { mode: 'production' })
if (command === 'dev') {
const compiler = Webpack(defaultDevConfig)
const devServerOptions = defaultDevConfig.devServer
const devServer = new WebpackDevServer(compiler, devServerOptions)
devServer.listen(8080, 'localhost', () => {
console.log('[Lovely-CLI] Starting server on http://localhost:8080')
})
} else if (command === 'build') {
Webpack(defaultProdConfig, function (err, stats) {
if (err) {
throw err
}
if (stats.hasErrors()) {
console.log().log('[Lovely-CLI]', stats.toString());
}
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
}) + '\n\n')
})
} else {
console.log('I am a lovely CLI.')
}
复制代码
有实际运行以上代码的同学,会发现浏览器不会自动检测变化刷新,这也是我留给实际动手了的同学的一个思考题,正好趁此机会好好看看一点也不可爱的 webpack 的文档(建议英文)。
最终的代码已经放在 Github 上:
lovely-cli
说到这里,再回过头来看,上一篇文章中提到的 react-scripts, 你是不是也不陌生了呢?
当然,必须强调,这只是一个玩具(或者说连玩具都算不上),一个实际可用的工具,需要支撑足够多的场景,正如尤雨溪所说:
“判断一个开源库是不是玩具的一个参考是:这个库有没有因为实际生产中遇到的特殊情况而做出妥协。”
对于这样的一个轮子,你可以考虑支持:
- 完整的命令行功能,支持 -- 和 - 语法;
- 足够通用的默认配置(webpack4也明白了“约定优于配置”的道理。);
- 留出高拓展性的接口(plugin?preset?);
- 更漂亮的log页面(chalk又要登场啦...);
- 更优雅的reload(如:修改了webpack配置后根据新的配置重启dev-server);
当然,还有很多了,只要你想得到。
甜点
老惯例,是时候给大家介绍一些甜点了。
poi
这是官翻:用一个 .js
文件编写一个应用程序,无论是 vue 还是 react,poi 会帮你处理所有的开发设置,没有更多的配置。
poi设计的精妙之处在于提供了 presets 机制,preset可以理解为针对特定场景的一些配置的集合。
在每一个 poi 的 preset 中,可以通过基于 webpack-chain 设计的 API 来修改 webpack 的配置。所有的 preset 会在 app 运行前依次被调用。在你我都熟悉的 babel 中,不同的 preset 实际上就是不同的语法转换 plugin 的一个bundle。
值得开发 Vue 的同学注意的是,Vue、Vue JSX 以及 object-rest-spread 是内置的哦。
目前,poi支持以下预设:有没有你所熟悉的呢?
w7
w7的概念和poi不太一样,它更像是一个纯粹的dev server,没有任何配置,而做了一件简单的事,以某个特定端口serve某个html文件,并监听该文件的变化,当文件发生变化时,会自动刷新浏览器。或许很多人会提出疑问:这难道不是和前端工程化背道而驰吗?现在还会有人写纯html吗?
不,相反,我是一名狂热的前端工程化爱好者。诞生这个项目的缘由实际上还是来源于自己的需求,不知道细心的同学在有没有发现我博客仓库中,每个年份下都有一个tests目录,这是因为我经常会写一个相对独立的前端测试,它不隶属于某个项目,但我仍然想要它长期存在,我个人习惯以HTML的方式留存,而不是gist。
w7便解决了这个需求,当你只需要做一些简单的测试时,你并不需要任何构建工具,你只需要一个HTML文件。虽然只有一个HTML,但w7仍然可以帮你检测变化并刷新浏览器。
w7 app.html
复制代码
同时,w7还内置了 vue、react以及rxjs的样板文件哦,如果你只是想测试一下一个怎么用 vue 写一个计数器,为什么要花那么多时间下载 vue-cli,还留下一堆 node_modules 等待清理呢?
w7 init # Generate a simple html file with random filename (includes git user name.)
w7 init --lib vue # Generate a Counter boilerplate with vue.
w7 init --lib react # Generate a Counter boilerplate with React+JSX.
复制代码
总结
无论是一个从头开始的工具(如parcel),还是基于现有成熟方案开发的工具(如 react-scripts 和 poi ),一个好的构建工具,我认为是这样的:在将 API 设计得足够简单的同时,仍然保留其全部的拓展能力。从这一点上看,我认为egoist和她的poi做的相当好。
当然,也请别忘了向实际生产环境妥协。
CLI 系列到此就结束了。接下来几个月,我会专注在 vue 和 react 的源码、对比和思考中,同时也会告诉你如何写一个包含核心功能的 vue 或者 react 哦,所以下一个系列的标题,就暂定为《以更好的姿势看待 vue 和 react》吧,敬请关注。
以上,全文终。)