需求:将 webpack 做成 cli 并发布到 www.npmjs.com
何时需要自定义 CLI
了解自定义 CLI 的应用场景
虽然市面上存在很多如 vue-cli 等流行且好用的 CLI 工具,但这些工具面向的是全体开发者,所以必须做的很通用,也正因如此,它们也就不可能包含一些和个人/企业强关联的私有化个性化功能。
比如:
- 通过命令行生成业务代码:如一个业务数据的整套增、删、改、查代码
- 通过命令行生成企业中已经非常成熟、复用度很高的基础框架代码,在开发新项目时能节约很多时间
webpack的结构
了解 webpack 的宏观结构,以便理解如何去开发基于 webpack 的命令行工具
核心内容:
- webpack 本身的结构
webpack 这个工具其实划分成三部分:
- 核心 API - 实现 webpack 具体功能的代码
- 命令行接口 - 即 webpack 自己提供的命令行工具(如:webpack build、webpack watch 等)
- Node接口 - 开放给其他外部 node.js 程序调用的接口,让它们集成和调用 webpack 来完成打包相关的功能
开发最简单的 CLI
使用 Node.js 开发一个最简单的 CLI 程序。
它的功能是:在命令行打印接受到的参数
核心内容:
- CLI 的概念
- 使用 Node.js 编写 CLI 程序
CLI 就是 Command-Line Interface,即命令行界面,也称为 CUI(Character User Interface,字符用户界面)。它是在 GUI (图形用户界面)普及前被广泛使用的计算机与用户间的交互界面,俗称”黑窗口“。
具体步骤:
-
创建一个新目录,比如:
my-cli -
创建
src/index.js,编写 CLI 程序真正的业务逻辑
function echo(message) {
console.log(`提示信息:${message}`)
}
module.exports = { echo }
- 创建
bin/echo-cli.js,CLI 可执行程序的入口代码
#!/usr/bin/env node
const { echo } = require('../src/index')
// 获取命令行携带的第一个参数
// process.argv是一个数组:
// - process.argv[0] 是 node 程序的路径
// - process.argv[1] 是本代码的路径
// - process.argv[2] 从该位置开始的元素,是命令行上指定的一个或多个参数
const msg = process.argv[2]
// 调用 echo 函数
echo(msg)
所有用脚本语言编写的 CLI 程序代码的开头第一行,都要加上 #! 脚本解释器名称 。
比如 Node.js 编写的 CLI 程序就要加上: #! /usr/bin/env node
以上这个由井号和叹号构成的字符序列 ,在计算领域中称为 Shebang 或 Hashbang。
文件中存在 Shebang 的情况下,Unix/Linux 操作系统的程序加载器会分析 Shebang 后的内容,将这些内容作为解释器指令来调用。
- 创建
package.json,并添加bin配置:
{
// ...
"bin": {
"myecho": "./bin/echo-cli.js"
}
}
bin 的作用是:当 CLI 程序发布到 npmjs.com 之后:
- 如果通过 npm 局部安装该包,npm 会将
echo-cli.js放到node_modules/.bin/myecho中 - 如果通过 npm 全局安装该包,则会产生一个软连接放到系统的全局程序目录中(Unix/Linux 上为
/usr/local/bin/myecho,windows 上为 C:\windows\system32\myecho)
安装后就能局部或全局调用该命令了( myecho )。
- 开发阶段的测试方法
开发阶段,如果频繁发布到 npmjs.com 上进行测试,肯定非常不便。可以采用下列方式:
首先,为 echo-cli.js 添加可执行权限,比如 Unix/Linux 系统上为:
chmod +x ./bin/echo-cli.js
然后就可以直接在命令行执行该程序文件:
./bin/echo-cli.js hello,cli
执行效果:
以上是开发 CLI 程序所要具备的最基础知识。但在实际开发中,如果用这种方式开发稍微复杂点的 CLI 程序的话,会需要自行处理很多细节,花费大量时间。
开发标准体验的 CLI 程序
让开发出来的命令行程序更符合 Unix/Linux 等的命令行程序的使用体验
核心内容:
- 标准 CLI 的用户体验惯例及 Node.js 命令行工具库介绍
- 使用
命令行程序的交互形式是纯文字的,但即使是纯文字交互,也能可以为用户提供尽可能好好的体验,比如:
- 提供命令的使用帮助信息
- 提供对重要信息的高亮显示
- 提供选项列表、文字输入等功能
- 提供处理进度信息
如系列图片所示:
开源社区已将这类交互都封装成了工具包,供所有 CLI 程序开发者使用。
Node.js 中常用的库:
- commander - 完整的 node.js 命令行解决方案,能方便的创建自定义命令行
- inquirer - 实现命令行输入提示的交互界面工具库
- chalk - 实现对命令行文字进行着色的工具库
- shelljs - 实现了一套常用 Unix Shell 命令功能的工具库
- ora - 实现在命令行中显示 loading 信息的工具库
具体步骤:
- 创建新项目目录,并安装
commander
npm i commander --save
- 创建
bin/hm.js
#! /usr/bin/env node
const { program } = require('commander')
// 设定我们的命令行程序的版本
program.version('0.0.1')
// 定义一个命令:init
program
// 命令名称
.command('init')
// 命令别名
.alias('new')
// 命令描述
.description('create a new project')
// 命令具体做的事情
.action(() => {
console.log('初始化新项目...')
})
// 定义另一个命令:serve
program
// 命令名称
.command('serve')
// 命令描述
.description('run the code with the dev server')
// 命令选项
.option('-p, --port <port>', 'server port', '8080')
// 命令具体做的事情
.action((args) => {
console.log('运行开发服务器...', args)
})
// 通过 commander 来解析命令行传递的参数信息【非常重要】
program.parse(process.argv)
- 配置
package.json中的bin选项
{
"name": "hm-cli",
"bin": {
"hm": "./bin/hm.js"
},
// ...
}
- 通过 npm link 功能,让本地开发中的 CLI 程序变成系统全局命令,方便开发测试
npm link
然后执行以下命令, 查看帮助信息:
hm --help
更进一步,执行 init 和 serve 两个命令:
hm init
hm serve -p 8888
实现简易版 Vue-CLI
开发一个类似 vue-cli 的 CLI 程序,可以生成项目骨架,并调用 webpack 实现 dev server 的启动和 build 打包
核心内容:
- 按上一章节介绍的标准体验 CLI 编写方式,编写一个 CLI 程序结构
- 实现三个 CLI 功能:创建项目骨架、使用开发服务器运行项目、打包项目
本简易版 vue-cli 将实现以下命令:
- hm init
- hm serve
- hm build
推荐目录结构:
/my-webpack-cli
├─┬ bin
│ └── hm.js # CLI 主入口
├─┬ src
│ ├── init.js # hm init 功能代码
│ ├── serve.js # hm serve 功能代码
│ └── build.js # hm build 功能代码
├─┬ template
│ ├── framework # 骨架代码模板
│ └── webpack # webpack 配置文件
└── package.json
具体步骤:
一、开发初始化项目命令(hm init)
- 创建目录
my-webpack-cli,并按推荐结构创建各目录和空代码文件;并且下载模板文件到template目录
- 安装
commander,并编写 CLI 入口代码bin/hm.js
npm i commander --save
在 bin/hm.js 中实现三个命令的最基本代码:
#! /usr/bin/env node
const { program } = require('commander')
const init = require('../src/init')
const serve = require('../src/serve')
const build = require('../src/build')
// 设定我们的命令行程序的版本
program.version('0.0.1')
// 定义一个命令:init
program
.command('init')
.description('create a new project')
.action(() => {
init()
})
program
.command('serve')
.option('-p, --port <port>', 'server port', '3000')
.description('run the project with dev server')
.action(args => {
serve(args)
})
program
.command('build')
.description('build for production')
.action(() => {
build()
})
// 通过 commander 来解析命令行传递的参数信息【非常重要】
program.parse(process.argv)
- 在
package.json中添加bin属性
{
"name": "hm-cli",
"bin": {
"hm": "./bin/hm.js"
},
// ...
}
- 实现
src/init.js:生成项目骨架的功能
需要安装的依赖包:inquirer 和 shelljs
npm i inquirer shelljs --save
代码:
const inquirer = require('inquirer')
const shell = require('shelljs')
const path = require('path')
module.exports = async () => {
// 1. 显示输入项目名称的提示
const input = await inquirer.prompt([
{
type: 'input',
name: 'name',
message: '新项目的名称',
default: 'myapp'
},
{
type: 'list',
name: 'packager',
message: '使用哪个包管理工具安装依赖?',
choices: ['yarn', 'npm'],
}
])
const projectName = input.name
// 2. 创建项目目录
shell.mkdir(projectName)
// 3. 复制模板代码(从本地目录复制,理想情况下可以从网络下载)
shell.cp('-R', path.join(__dirname, '../template/framework/*'), projectName)
// 4. 进入项目目录,并使用 yarn/npm 安装依赖
shell.cd(projectName)
if (input.packager === 'yarn') {
shell.exec('yarn')
} else {
shell.exec('npm i')
}
}
- 通过 npm link 功能,让本地开发中的 CLI 程序变成系统全局命令,方便开发测试
npm link
在 CLI 项目下执行以上命令后,就能直接在命令行界面中执行 package.json中由 bin 指定的 hm 命令:
hm init
接着,按提示信息生成一个新的骨架项目:
二、开发 dev server 命令(hm serve)
- 在
template/webpack 目录下创建webpack.config.default.js` :
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const { VueLoaderPlugin } = require('vue-loader')
module.exports = {
// 生成 Source Map 文件
devtool: 'eval-source-map',
// 入口文件
entry: './src/main.js',
// 打包输出
output: {
// 注意需要使用 process.cwd() 获取到当前的工作目录
path: path.join(process.cwd(), 'dist'),
filename: 'app.[contenthash].js'
},
module: {
rules: [
// 处理纯 css 样式文件
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, 'css-loader']
},
// 处理 sass|scss 样式文件
{
test: /\.(scss|sass)$/i,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
},
// 处理图片和字体文件
{
test: /\.(png|jpe?g|gif|svg|eot|ttf|woff|woff2)$/i,
type: "asset"
},
// 处理 ES6+ 语法
{
test: /\.m?js$/i,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
plugins: ['@babel/plugin-transform-runtime']
}
}
},
{
test: /\.vue$/i,
use: 'vue-loader'
}
]
},
plugins: [
// 生成 index.html
new HtmlWebpackPlugin({
template: './public/index.html'
}),
// 抽取样式
new MiniCssExtractPlugin(),
// 打包清理
new CleanWebpackPlugin(),
// 处理 Vue 组件
new VueLoaderPlugin()
],
resolve: {
alias: {
// 为 src 目录设置别名 @
'@': path.resolve(process.cwd(), 'src')
}
}
}
并安装需要的依赖包:
npm i clean-webpack-plugin html-webpack-plugin mini-css-extract-plugin vue-loader vue-template-compiler --save
【注意】
以上使用
process.cwd()的目的是获取新生成的骨架项目的目录!(用户工作目录)
- 实现 vue-cli 中的
vue.config.js的功能(用户配置)
先安装 webpack、webpack-merge、webpack-dev-server:
npm i webpack webpack-merge webpack-dev-server --save
编写src/serve.js:
const path = require('path')
const webpack = require('webpack')
const { merge } = require('webpack-merge')
const WebpackDevServer = require('webpack-dev-server')
const defaultConfig = require('../template/webpack/webpack.config.default')
module.exports = async (args) => {
// 读取用户配置文件
const userConfig = require(path.join(process.cwd(), 'hm.config')) || {}
// 组装 webpack 配置对象(将默认配置和命令相关联配置合并)
const config = merge(defaultConfig, {
mode: 'development'
})
// 创建 webpack 编译器实例
const compiler = webpack(config)
// 创建 dev server 实例
const server = new WebpackDevServer({
static: {
directory: path.join(process.cwd(), 'dist')
},
// 从命令行参数获取服务器端口
port: args.port,
// 从配置文件 hm.config.js 中获取其他配置信息
...userConfig.devServer
}, compiler)
// 启动 dev server
await server.start()
}
- 在之前生成的骨架项目中该
serve命令,启动开发服务器
三、开发打包命令(hm build)
- 编写
src/build.js
const path = require('path')
const webpack = require('webpack')
const { merge } = require('webpack-merge')
const defaultConfig = require('../template/webpack/webpack.config.default')
module.exports = () => {
// 读取用户配置文件
const userConfig = require(path.join(process.cwd(), 'hm.config')) || {}
// 组装 webpack 配置对象(将默认配置和命令相关联配置合并)
const config = merge(defaultConfig, {
mode: 'production',
// 从用户配置中获取 devtool 的值
devtool: userConfig.devtool || 'source-map'
})
// 创建 webpack 编译器实例
const compiler = webpack(config)
// 执行打包
compiler.run((err, stats) => {
if (err) {
console.log('build error:', err)
}
// 销毁编译器实例
compiler.close((err, result) => {
if (err) {
console.log('close error:', err)
}
})
})
}
随后即可使用 build 命令在生成的骨架项目下进行打包。
值得一提的知识
- 哪些包放到 cli 的 dependencies 中?哪些包放到模板的 devDependencies 中?
- cli 程序中直接引用到的包,放到 cli 的 dependencies 中
- 其他统统放到模板项目代码的 devDependencies 中(比如各种 webpack loader)
- 由于在生成的骨架项目的
package.json中配有快捷命令:
"scripts": {
"serve": "hm serve",
"build": "hm build"
},
因此也可以用 npm run serve 和 npm run build 来启动服务器和打包。
发布到 npmjs.com
将开发完成的简易版 vue-cli 发布到 npmjs.com
核心内容:
-
npmjs.com 介绍
-
发布要用到的 npm 命令:npm login 和 npm publish
具体步骤:
-
注册一个 npmjs.com 的账号
-
在命令行登录 npmjs.com
npm login
- 上传发布我们的代码包
npm publish
发布时可能会遇到各种问题,最常遇到的就是由 package.json 中字段设置错误引起的。 以下这些字段一定要检查是否已正确设置:
name包名,且不能和 npm 上已有包发生重名!version版本号
- 发布成功后,即可通过 npm 从 npmjs.com 下载安装我们的 CLI 程序
npm i -g 最终发布时的包名