一、Lerna 入门指南
1.1 什么是 Lerna
Lerna 是各大开源项目常用的 npm 项目包管理工具,用于管理包含多个包的 JavaScript 项目。它可以帮助你执行安装、更新和发布依赖项等操作,同时保持各个包之间的独立性。
1.2 Lerna 核心命令
安装与初始化
# 全局安装 lerna
cnpm i -g lerna
# 项目初始化(记得添加.gitignore)
lerna init
# 创建 package 包
lerna create utils
依赖管理
# 为所有包添加依赖
lerna add lodash
# 为 package/core/ 添加 vue 依赖
lerna add vue package/core/
# 清空所有包的依赖
lerna clear
# 重新安装依赖
lerna bootstrap
# 在每个包的 package.json 中写好依赖关系及版本号,
# lerna link 会为每个 package 创建彼此之间的依赖关系
lerna link
命令执行
# lerna exec 为每个包执行命令
lerna exec -- rm -rf node_modules/
# 在指定的 @attack-i/core 包中执行命令
lerna exec --scope @attack-i/core -- rm -rf node_modules/
# lerna run 在每个包中执行 npm scripts
lerna run test
# 在指定的 @attack-i/core 包中 run script
lerna run --scope @attack-i/core test
版本与发布
# 版本管理
lerna version
# 查看依赖的所有变更
lerna changed
# 查看 diff
lerna diff
# 发布
lerna publish
更多命令及详情可以去 Lerna 官方文档 中查看
二、脚手架原理深入解析
2.1 脚手架的本质
脚手架本质上是一个操作系统的客户端,通过命令行执行特定任务。
2.2 核心价值
脚手架能够提升前端研发效能,主要体现在三个方面:
- 自动化:项目重复代码的拷贝、git 操作、发布上线操作
- 标准化:项目创建、git flow、发布流程、回滚流程
- 数据化:研发过程系统化、数据化,使得研发过程可量化
2.3 与自动化构建工具的区别
Jenkins、Travis 等自动化工具已经比较成熟,是否还需要自研脚手架?
| 对比项 | Jenkins/Travis | 自研脚手架 |
|---|---|---|
| 执行位置 | 服务端(git hooks 触发) | 本地开发环境 |
| 覆盖场景 | 构建、测试、部署 | 项目创建、本地 git 操作、开发辅助 |
| 定制难度 | 复杂(需后端语言) | 灵活(Node.js 生态) |
核心区别:
- Jenkins/Travis 通常在服务端执行,无法覆盖研发人员本地的功能
- Jenkins/Travis 定制过程复杂,需要使用后端语言
- 脚手架专注于本地开发体验的提升
2.4 从使用角度理解脚手架
vue create vue-test-app
# - 主命令:vue
# - command:create
# - command 的参数:vue-test-app
vue create vue-test-app --force -r https://regisetry.npm.taobao.org
命令结构解析:
| 部分 | 说明 | 示例 |
|---|---|---|
| 主命令 | 脚手架名称 | vue |
| command | 具体命令 | create |
| 参数 | command 的参数 | vue-test-app |
| options (全称) | 配置选项 | --force, --registry |
| options (简写) | 配置选项简写 | -f, -r |
--force 叫做 option,用来辅助脚手架确认在特定场景下用户的选择(可以理解为配置)。
-r 也叫做 option,与 --force 的区别在于使用 -,使用简写,-r 也可以替换为 --registry。
三、脚手架执行原理
3.1 常见问题
在深入讲解原理之前,先思考几个核心问题:
- 为什么全局安装
@vue/cli会添加vue命令? - 全局安装
@vue/cli发生了什么? - 执行
vue命令时发生了什么?为什么vue指向一个 js 文件,可以通过vue命令去执行它?
3.2 全局安装原理
全局安装 @vue/cli 时发生了什么:
- npm 在进行全局安装 vue-cli
- 在 vue-cli 的 package.json 中查看 bin 字段
- bin 中如果有指令,会在 node 安装目录的 bin 文件夹中添加 vue 的软链接
package.json 配置示例:
{
"name": "@vue/cli",
...
"bin": {
"vue": "bin/vue.js" // 终端中 vue 主命令从这里来的
}
}
npm 安装 vue 作为全局依赖的时候,为 vue 在 bin 目录中创建了软链接,并指向当前使用的 node 版本安装路径的
lib/node_modules目录中 vue-cli 中的vue.js
3.3 命令执行原理
执行 vue 命令,会查到 vue-cli 的 js 文件,但是为什么会被 node 执行呢?
关键代码:
#!/usr/bin/env node
// 上面这行会让操作系统在环境变量中寻找 node,
// nvm 可以切换 node,也就是修改了环境变量中的 PATH
console.log("hello world~")
3.4 如何在命令行创建自定义命令
# 先进入 node 的 bin 目录
cd ~/.nvm/versions/node/v16.13.1/bin
# 创建软链接
ln -s ~/pixel/source/_posts/backend/nodejs/cli/test.js attr
# 为命令添加别名,软连接是可以嵌套的
ln -s ./attr attr2
# 删除软链接
rm -rf attr attr2
脚手架之所以是客户端,因为 node 是客户端
3.5 完整执行流程
以 vue create vue-test-app 为例:
- 终端解析 vue 命令
- 在环境变量
$PATH中查询 vue 的软链接 - 执行 vue-cli 的 bin 目录中的 vue.js
- 终端利用 node 执行 vue.js
- vue.js 第一行声明
#!/usr/bin/env node,会让终端通过环境变量查找 node 客户端 - vue.js 用 node 执行
- vue.js 解析 command/options
- vue.js 执行 command
- 执行完毕,退出
四、脚手架开发实战
4.1 开发流程
- 创建 npm 项目:
npm init -y - 创建入口文件,最上方添加
#!/usr/bin/env node - 配置 package.json,添加 bin 属性
- 编写脚手架代码
- 将脚手架发布到 npm
- 安装脚手架:
npm install -g your-own-cli - 使用脚手架:
your-own-cli
4.2 开发难点解析
4.2.1 分包管理
将复杂的系统拆分成若干个模块。
4.2.2 命令注册
vue create
vue add
vue invoke
4.2.3 参数解析
命令格式:
vue command [options] <params>
Options 类型:
- options 全称:
--version、--help - options 简写:
-V、-h - 带 params 的 options:
--path /Users/pixel/Desktop/vue-test
4.2.4 帮助文档
Global Help:
- Usage
- Options
- Commands
Command Help: 例如:vue 的帮助信息
4.2.5 其他功能点
- 命令行交互
- 日志打印
- 命令行文字变色
- 网络通信:Http/WebSocket
- 文件处理
4.3 Node 根据路径执行文件
node -e "require('../cli/index.js')"
五、命令行参数解析库详解
Lerna 能够在命令行运行的原理:Lerna 其实是一个封装了 npm 命令的工具,在执行 lerna 命令时,其实是调用了 npm 命令。具体实现主要依赖两个模块:commander 和 yargs,这两个库都是用来解析命令行参数的。
5.1 Commander 使用详解
Commander 的作用是读取命令行参数。
#!/usr/bin/env node
const commander = require('commander')
const program = new commander.Command()
const pkg = require('../package.json')
program
.name(Object.keys(pkg.bin)[0])
.usage('<cmd> [option]')
.version(pkg.version)
.option('-d, --debug', 'start debug mode', false)
.option('-e --envName <envName>', 'fetch the environment', 'development')
// 定义 clone 命令
const clone = program.command('clone <source> [destination]');
clone
.description('clone an existing project')
.option('-f, --force', 'clone force')
.action((source, dest, cmdObj) => {
console.log('clone', source, dest, cmdObj.force);
})
// 定义 server 子命令
const server = new commander.Command('server')
server.command('start [port]')
.description('start server at specified port')
.action((port) => {
console.log(`start server at ${port} ...`);
})
server.command('stop')
.description("stop server")
.action(() => {
console.log("stop server");
})
// 定义 install 命令,委托给 npm 执行
program.command('install [name] [path]', 'install package at specified path', {
executableFile: 'npm', // 当前 install 命令执行,相当于 yarn add 执行
// isDefault: true, // 这里设置为 true,当执行脚手架时,默认就会执行 npm
hidden: true // 作为一个隐藏的命令
}).alias('i')
// 监听 --help 事件
program.on('--help', function () {
console.log('help info');
})
// debug 模式实现
program.on('--debug', function () {
process.env.LOG_LEVEL = program.debug ? 'verbose' : 'info';
console.log(process.env.LOG_LEVEL);
})
// 对未知命令监听
program.on('command:*', function (obj) {
console.error('unknown command', obj[0]);
const availableCommand = program.commands.map(cmd => cmd.name);
console.log('可用命令:' + availableCommand.join(","));
})
// 适用于所有命令的监听
program.arguments('<cmd> [options]')
.description('test command', {
cmd: 'command',
options: 'options for command',
})
.action((cmd, options) => {
console.log(cmd, options);
})
program.addCommand(server)
program.parse(process.argv)
5.2 Yargs 使用详解
Yargs 是一个命令行参数解析器,它可以帮助你快速构建一个命令行工具。
const yargs = require('yargs/yargs')
// 页脚格式化插件
const dedent = require('dedent')
const pkg = require('../package.json')
const cli = yargs()
const argv = process.argv.slice(2)
const context = {
cliVersion: pkg.version
}
cli
.usage('Usage: lio-imooc-test [command] <options>')
.demandCommand(1, 'A command is required. Pass --help to see all available commands and options. 最少要输入一个参数')
.strict()
.recommendCommands()
.fail((err, msg) => {
console.log('err =======>', err)
console.log('msg =======>', msg)
})
.alias('h', 'help')
.alias('v', 'version')
.wrap(cli.terminalWidth()) // 显示的宽度(撑满全屏)
.epilogue(dedent` hello
world`) // 去除缩进
.options({ // 对所有的 command 添加的 options 都有效
debug: { // 添加 debug 命令
type: 'boolean',
describe: 'Bootstrap debug mode',
alias: 'd'
}
})
.option('ci', {
type: 'string',
describe: 'Define global registry',
alias: 'r'
})
.group(['debug'], 'Dev Options:') // 把 debug 添加到 Dev Options 中
.group(['registry'], 'Extra Options:')
.command('init [name]', 'Do init a project', (yargs) => {
yargs
.option('name', {
type: 'string',
describe: 'Name of a project',
alias: 'n'
})
}, (argv) => {
console.log(argv)
})
.command({
command: 'list',
aliases: ['ls', 'la', 'll'],
describe: 'List local packages',
builder: (yargs) => {
},
handler: (argv) => { }
})
.parse(argv, context) // 将默认参数与配置参数合并
六、Node.js 相关技术参考
6.1 Node 原生方法和属性
在开发脚手架和命令行工具时,常用的 Node.js 原生方法和属性:
| 方法/属性 | 说明 |
|---|---|
fs.statSync / fs.lstatSync | 获取文件状态信息 |
fs.accessSync(path) | 检查文件是否可访问 |
fs.realpathSync() | 根据相对路径,返回绝对路径(如果是软链接,会一直寻到最终路径) |
fs.toRealPath() | 调用生成真实路径,判断是否存在 |
Module._nodeModulePaths(path) | 返回 path 各层级的 node_modules 路径数组 |
Module._resolveFilename(filename) | 返回文件的真实路径 |
NativeModule.canBeRequiredByUsers(request) | 是否是内置模块 |
Module._resolveLookupPaths(request, parent) | 返回一个数组,当前模块可能存在的所有路径,将当前模块文件路径各层级的 node_modules 目录数组和 node 环境变量中的 node_modules 数组合并,这是一个有顺序的数组,离 path 最近的一层目录为首位 |
process.cwd() | 当前进程运行目录 |
6.2 常用第三方库
在开发脚手架时,常用的第三方库:
| 库名 | 说明 |
|---|---|
import-local | 查询是否是本地的包 |
pkg-dir | 查找当前文件或者目录层级最近的 package.json |
find-up | 从当前目录向上查找文件 |
path-exists | 检查路径是否存在 |
locate-path | 定制路径查找逻辑 |
resolve-cwd | 解析当前工作目录的路径 |
七、总结
本文从 Lerna 的实际使用出发,深入讲解了脚手架和命令行工具的开发原理。通过学习本文,你应该能够:
- 熟练使用 Lerna 管理多包项目
- 理解脚手架的执行原理和工作机制
- 掌握 Commander 和 Yargs 的使用方法
- 了解 Node.js 在命令行工具开发中的关键 API
- 具备开发自定义脚手架的能力
脚手架是提升前端研发效能的重要工具,希望本文能够帮助你更好地理解和应用相关技术。