脚手架实现原理
简介:回答三个问题就掌握了脚手架原理:命令名从哪来、全局安装做了什么、为什么 JS 文件能直接执行
-
问题一:为什么全局安装
@vue/cli后,命令叫vue-
命令名由
package.json的bin字段决定,与包名、入口文件名无关 -
bin的 key 是命令名(软链接名),value 是实际执行的文件#{
"name": "@vue/cli",
"version": "4.5.9",
"bin": {
"vue": "bin/vue.js"
}
}{
#{
"name": "@vue/cli",
"version": "4.5.9",
"bin": {
"vue": "bin/vue.js"
}
} "name": "@vue/cli",
#{
"name": "@vue/cli",
"version": "4.5.9",
"bin": {
"vue": "bin/vue.js"
}
} "version": "4.5.9",
#{
"name": "@vue/cli",
"version": "4.5.9",
"bin": {
"vue": "bin/vue.js"
}
} "bin": {
#{
"name": "@vue/cli",
"version": "4.5.9",
"bin": {
"vue": "bin/vue.js"
}
} "vue": "bin/vue.js"
#{
"name": "@vue/cli",
"version": "4.5.9",
"bin": {
"vue": "bin/vue.js"
}
} }
#{
"name": "@vue/cli",
"version": "4.5.9",
"bin": {
"vue": "bin/vue.js"
}
}}
-
-
问题二:全局安装
@vue/cli时发生了什么

-
红色是生成
vue命令的关键一步,安装日志里能直接看到这条链接#[@vue/cli@4.5.9] link ~/.nvm/versions/node/v12.11.1/bin/vue@ -> ~/.nvm/versions/node/v12.11.1/lib/node_modules/@vue/cli/bin/vue.js[@vue/cli@4.5.9] link ~/.nvm/versions/node/v12.11.1/bin/vue@ -> ~/.nvm/versions/node/v12.11.1/lib/node_modules/@vue/cli/bin/vue.js
-
问题三:
vue指向一个 JS 文件,为什么能直接执行-
执行
vue等价于直接执行which vue找到的那个文件 -
普通 JS 文件不能直接执行
- 直接
./test.js:报Permission denied,没有可执行权限 chmod 777 test.js后再执行:报syntax error,系统不知道用什么解释器- JS 必须由解释器 node 执行,就像
.py要用 python、.java要用 java
- 直接
-
关键是入口文件第一行的
#!/usr/bin/env node- 告诉操作系统:到环境变量中找
node命令,再用 node 执行当前文件
#!/usr/bin/env node
console.log('hello cli')#!/usr/bin/env node
#!/usr/bin/env nodeconsole.log('hello cli')
#!/usr/bin/env nodeconsole.log('hello cli')console.log('hello cli')
- 告诉操作系统:到环境变量中找
-
-
下面三条命令等价
| 写法 | 说明 |
|---|---|
./test.js |
文件首行有 #!/usr/bin/env node 时可直接执行 |
/usr/bin/env node test.js |
env 在环境变量中找到 node 再执行 |
node test.js |
直接用 node 执行 |
- 两种首行写法对比
| 写法 | 含义 | 问题 |
|---|---|---|
#!/usr/bin/env node |
在环境变量中查找 node |
无,推荐 |
#!/usr/bin/node 或写死绝对路径 |
直接执行固定路径下的 node |
换台电脑路径不同就报 bad interpreter: No such file or directory |
-
自定义命令
-
在 node 的 bin 目录(在
$PATH中)创建软链接,指向自己的 JS 文件 -
Windows 可以手动创建快捷方式,效果相同
ln -s 目标文件 链接名
mycli # 输出 hello clicd ~/.nvm/versions/node/v12.11.1/bin/
ln -s 目标文件 链接名
mycli # 输出 hello cliln -s /Users/xxx/Desktop/vue-test/test.js mycli # ln -s 目标文件 链接名
ln -s 目标文件 链接名
mycli # 输出 hello climycli # 输出 hello cli
-
-
注意点
ln -s的参数顺序是先写目标文件、后写链接名,写反会报File exists- 首行统一写
#!/usr/bin/env node,不要写死 node 的安装路径
脚手架原理进阶
简介:脚手架本质是 node 这个操作系统客户端在执行,命令执行有三个关键节点
-
为什么说脚手架本质是操作系统的客户端
- 脚手架和 Web 应用本质上没有关系,只是都用 JavaScript 编写
- 真正的客户端是 node,不是我们写的 JS 文件
- node 本身是可执行文件(macOS 上约 42MB,Windows 上是
node.exe),预设的执行逻辑都打包在里面 - JS 文件只是传给 node 的参数,
node -e "console.log('hello')"直接传字符串也能执行
- node 本身是可执行文件(macOS 上约 42MB,Windows 上是
- 和 PC 上安装的软件本质没有区别
- 区别只是普通软件提供 GUI,node 通过命令行传参执行
- node 想展示 GUI 也可以,调用操作系统的 GUI 绘制 API 即可
-
如何为脚手架命令创建别名
-
软链接可以嵌套:别名 → 原命令 → 实际文件
mycli2 -> mycli -> test.js
mycli2 # 输出 hello clicd ~/.nvm/versions/node/v12.11.1/bin/
mycli2 -> mycli -> test.js
mycli2 # 输出 hello cliln -s ./mycli mycli2 # mycli2 -> mycli -> test.js
mycli2 -> mycli -> test.js
mycli2 # 输出 hello climycli2 # 输出 hello cli
-
-
脚手架命令执行全过程

- 实线是正常执行路径,虚线指向三个关键节点各自的失败表现(橙色),绿色是执行成功
- 排查思路
| 节点 | 报错 | 排查方法 |
|---|---|---|
| 查环境变量 | command not found |
which vue、echo $PATH |
| 查实际链接文件 | No such file or directory |
ls -l 看软链接指向是否正确 |
| 用 node 执行 | bad interpreter 等 |
检查首行写法、which node |
脚手架开发流程与难点
简介:开发流程只有五步,但要让脚手架真正好用,还要解决分包、命令注册、参数解析、帮助文档等难点
- 开发流程

-
红色两步是脚手架能被当作命令执行的关键
-
使用流程
全局安装
your-own-cli # 使用npm install -g your-own-cli # 全局安装
全局安装
your-own-cli # 使用your-own-cli # 使用
-
开发难点
| 难点 | 说明 |
|---|---|
| 分包 | 把复杂系统拆成若干模块,再组合成完整系统 |
| 命令注册 | 如 vue create、vue add、vue invoke,命令要绑定到对应方法 |
| 参数解析 | 解析 vue command [options] <params> |
| 帮助文档 | 没有帮助文档,使用者很难理解开发者的设计意图 |
| 其他 | 命令行交互、日志打印、命令行文字变色、网络通信(HTTP / WebSocket)、文件处理 |
-
参数解析
- options 全称:两个横线加完整单词,如
--version、--help - options 简写:一个横线加单个字母,如
-V、-h - 带 params 的 options:如
--path /Users/xxx/Desktop/vue-test
- options 全称:两个横线加完整单词,如
-
帮助文档
-
global help:主命令的帮助,包含 Usage、Options、Commands,如
vue -h -
command help:子命令的帮助,包含 Usage、Options,如
vue create --helpUsage: vue
[options] Options:
-V, --version output the version number
-h, --help output usage informationCommands:
create [options]create a new project powered by vue-cli-service
add [options]install a plugin and invoke its generator
...
-
快速入门第一个脚手架
简介:创建项目、写入口文件、配置 bin、发布 npm,四步完成一个能全局执行的命令
-
创建项目
#mkdir my-cli && cd my-cli
npm init -y
mkdir bin && touch bin/index.jsmkdir my-cli && cd my-cli
#mkdir my-cli && cd my-cli
npm init -y
mkdir bin && touch bin/index.jsnpm init -y
#mkdir my-cli && cd my-cli
npm init -y
mkdir bin && touch bin/index.jsmkdir bin && touch bin/index.js -
入口文件
bin/index.js#!/usr/bin/env node
console.log('welcome my-cli')#!/usr/bin/env node
#!/usr/bin/env nodeconsole.log('welcome my-cli')
#!/usr/bin/env nodeconsole.log('welcome my-cli')console.log('welcome my-cli')
-
配置
package.json的bin#{
"name": "my-cli",
"version": "1.0.0",
"bin": {
"my-cli": "bin/index.js"
}
}{
#{
"name": "my-cli",
"version": "1.0.0",
"bin": {
"my-cli": "bin/index.js"
}
} "name": "my-cli",
#{
"name": "my-cli",
"version": "1.0.0",
"bin": {
"my-cli": "bin/index.js"
}
} "version": "1.0.0",
#{
"name": "my-cli",
"version": "1.0.0",
"bin": {
"my-cli": "bin/index.js"
}
} "bin": {
#{
"name": "my-cli",
"version": "1.0.0",
"bin": {
"my-cli": "bin/index.js"
}
} "my-cli": "bin/index.js"
#{
"name": "my-cli",
"version": "1.0.0",
"bin": {
"my-cli": "bin/index.js"
}
} }
#{
"name": "my-cli",
"version": "1.0.0",
"bin": {
"my-cli": "bin/index.js"
}
}} -
发布和安装
登录 npm
npm publish # 发布
npm install -g my-cli # 全局安装
my-cli # 输出 welcome my-cli
which my-cli # 指向 node 的 bin/my-clinpm login # 登录 npm登录 npm
npm publish # 发布
npm install -g my-cli # 全局安装
my-cli # 输出 welcome my-cli
which my-cli # 指向 node 的 bin/my-clinpm publish # 发布登录 npm
npm publish # 发布
npm install -g my-cli # 全局安装
my-cli # 输出 welcome my-cli
which my-cli # 指向 node 的 bin/my-clinpm install -g my-cli # 全局安装登录 npm
npm publish # 发布
npm install -g my-cli # 全局安装
my-cli # 输出 welcome my-cli
which my-cli # 指向 node 的 bin/my-climy-cli # 输出 welcome my-cli登录 npm
npm publish # 发布
npm install -g my-cli # 全局安装
my-cli # 输出 welcome my-cli
which my-cli # 指向 node 的 bin/my-cliwhich my-cli # 指向 node 的 bin/my-cli -
注意点
- 当前目录下存在同名的项目目录时,
npm install -g my-cli会直接软链接到这个本地目录,改代码实时生效 - 想安装远程版本,要先离开这个目录再安装,否则装的一直是本地代码
- 当前目录下存在同名的项目目录时,
脚手架本地调试
简介:脚手架和库文件发布前,用 npm link 在本地建立软链接调试,避免反复发版
-
调试单个脚手架
- 方式一:在脚手架目录执行
npm link - 方式二:在脚手架的上级目录执行
npm install -g my-cli(利用同名目录软链接) - 移除全局脚手架:
npm remove -g my-cli
- 方式一:在脚手架目录执行
-
分包调试:脚手架
my-cli引用本地库my-cli-lib-
库还没发布时,直接
npm install my-cli-lib会报404 Not Found -
库文件
my-cli-lib/lib/index.js#module.exports = {
sum(a, b) {
return a + b
},
mul(a, b) {
return a * b
},
}module.exports = {
#module.exports = {
sum(a, b) {
return a + b
},
mul(a, b) {
return a * b
},
} sum(a, b) {
#module.exports = {
sum(a, b) {
return a + b
},
mul(a, b) {
return a * b
},
} return a + b
#module.exports = {
sum(a, b) {
return a + b
},
mul(a, b) {
return a * b
},
} },
#module.exports = {
sum(a, b) {
return a + b
},
mul(a, b) {
return a * b
},
} mul(a, b) {
#module.exports = {
sum(a, b) {
return a + b
},
mul(a, b) {
return a * b
},
} return a * b
#module.exports = {
sum(a, b) {
return a + b
},
mul(a, b) {
return a * b
},
} },
#module.exports = {
sum(a, b) {
return a + b
},
mul(a, b) {
return a * b
},
}}
-
-
库的
package.json要把main指向真实入口#{
"name": "my-cli-lib",
"version": "1.0.0",
"main": "lib/index.js"
}{
#{
"name": "my-cli-lib",
"version": "1.0.0",
"main": "lib/index.js"
} "name": "my-cli-lib",
#{
"name": "my-cli-lib",
"version": "1.0.0",
"main": "lib/index.js"
} "version": "1.0.0",
#{
"name": "my-cli-lib",
"version": "1.0.0",
"main": "lib/index.js"
} "main": "lib/index.js"
#{
"name": "my-cli-lib",
"version": "1.0.0",
"main": "lib/index.js"
}} -
脚手架中引用
#!/usr/bin/env node
const lib = require('my-cli-lib')
console.log(lib.sum(1, 2)) // 3
console.log(lib.mul(3, 4)) // 12
console.log('welcome my-cli')#!/usr/bin/env node
#!/usr/bin/env nodeconst lib = require('my-cli-lib')
console.log(lib.sum(1, 2)) // 3
console.log(lib.mul(3, 4)) // 12
console.log('welcome my-cli')
#!/usr/bin/env nodeconst lib = require('my-cli-lib')
console.log(lib.sum(1, 2)) // 3
console.log(lib.mul(3, 4)) // 12
console.log('welcome my-cli')const lib = require('my-cli-lib')
#!/usr/bin/env nodeconst lib = require('my-cli-lib')
console.log(lib.sum(1, 2)) // 3
console.log(lib.mul(3, 4)) // 12
console.log('welcome my-cli')console.log(lib.sum(1, 2)) // 3
#!/usr/bin/env nodeconst lib = require('my-cli-lib')
console.log(lib.sum(1, 2)) // 3
console.log(lib.mul(3, 4)) // 12
console.log('welcome my-cli')console.log(lib.mul(3, 4)) // 12
#!/usr/bin/env nodeconst lib = require('my-cli-lib')
console.log(lib.sum(1, 2)) // 3
console.log(lib.mul(3, 4)) // 12
console.log('welcome my-cli')console.log('welcome my-cli') -
link 后的链接关系

-
绿色是最终指向的本地源码目录,所以改本地代码立即生效
-
本地 link 标准流程
链接本地脚手架
cd your-cli-dir
npm link链接本地库文件
cd your-lib-dir
npm link
cd your-cli-dir
npm link your-lib取消链接本地库文件
cd your-lib-dir
npm unlink
cd your-cli-dir
npm unlink your-lib
rm -rf node_modules # 链接已失效时,删除后重新安装
npm install# 链接本地脚手架链接本地脚手架
cd your-cli-dir
npm link链接本地库文件
cd your-lib-dir
npm link
cd your-cli-dir
npm link your-lib取消链接本地库文件
cd your-lib-dir
npm unlink
cd your-cli-dir
npm unlink your-lib
rm -rf node_modules # 链接已失效时,删除后重新安装
npm installcd your-cli-dir链接本地脚手架
cd your-cli-dir
npm link链接本地库文件
cd your-lib-dir
npm link
cd your-cli-dir
npm link your-lib取消链接本地库文件
cd your-lib-dir
npm unlink
cd your-cli-dir
npm unlink your-lib
rm -rf node_modules # 链接已失效时,删除后重新安装
npm installnpm link链接本地脚手架
cd your-cli-dir
npm link链接本地库文件
cd your-lib-dir
npm link
cd your-cli-dir
npm link your-lib取消链接本地库文件
cd your-lib-dir
npm unlink
cd your-cli-dir
npm unlink your-lib
rm -rf node_modules # 链接已失效时,删除后重新安装
npm install链接本地脚手架
cd your-cli-dir
npm link链接本地库文件
cd your-lib-dir
npm link
cd your-cli-dir
npm link your-lib取消链接本地库文件
cd your-lib-dir
npm unlink
cd your-cli-dir
npm unlink your-lib
rm -rf node_modules # 链接已失效时,删除后重新安装
npm install# 链接本地库文件链接本地脚手架
cd your-cli-dir
npm link链接本地库文件
cd your-lib-dir
npm link
cd your-cli-dir
npm link your-lib取消链接本地库文件
cd your-lib-dir
npm unlink
cd your-cli-dir
npm unlink your-lib
rm -rf node_modules # 链接已失效时,删除后重新安装
npm installcd your-lib-dir链接本地脚手架
cd your-cli-dir
npm link链接本地库文件
cd your-lib-dir
npm link
cd your-cli-dir
npm link your-lib取消链接本地库文件
cd your-lib-dir
npm unlink
cd your-cli-dir
npm unlink your-lib
rm -rf node_modules # 链接已失效时,删除后重新安装
npm installnpm link链接本地脚手架
cd your-cli-dir
npm link链接本地库文件
cd your-lib-dir
npm link
cd your-cli-dir
npm link your-lib取消链接本地库文件
cd your-lib-dir
npm unlink
cd your-cli-dir
npm unlink your-lib
rm -rf node_modules # 链接已失效时,删除后重新安装
npm installcd your-cli-dir链接本地脚手架
cd your-cli-dir
npm link链接本地库文件
cd your-lib-dir
npm link
cd your-cli-dir
npm link your-lib取消链接本地库文件
cd your-lib-dir
npm unlink
cd your-cli-dir
npm unlink your-lib
rm -rf node_modules # 链接已失效时,删除后重新安装
npm installnpm link your-lib链接本地脚手架
cd your-cli-dir
npm link链接本地库文件
cd your-lib-dir
npm link
cd your-cli-dir
npm link your-lib取消链接本地库文件
cd your-lib-dir
npm unlink
cd your-cli-dir
npm unlink your-lib
rm -rf node_modules # 链接已失效时,删除后重新安装
npm install链接本地脚手架
cd your-cli-dir
npm link链接本地库文件
cd your-lib-dir
npm link
cd your-cli-dir
npm link your-lib取消链接本地库文件
cd your-lib-dir
npm unlink
cd your-cli-dir
npm unlink your-lib
rm -rf node_modules # 链接已失效时,删除后重新安装
npm install# 取消链接本地库文件链接本地脚手架
cd your-cli-dir
npm link链接本地库文件
cd your-lib-dir
npm link
cd your-cli-dir
npm link your-lib取消链接本地库文件
cd your-lib-dir
npm unlink
cd your-cli-dir
npm unlink your-lib
rm -rf node_modules # 链接已失效时,删除后重新安装
npm installcd your-lib-dir链接本地脚手架
cd your-cli-dir
npm link链接本地库文件
cd your-lib-dir
npm link
cd your-cli-dir
npm link your-lib取消链接本地库文件
cd your-lib-dir
npm unlink
cd your-cli-dir
npm unlink your-lib
rm -rf node_modules # 链接已失效时,删除后重新安装
npm installnpm unlink链接本地脚手架
cd your-cli-dir
npm link链接本地库文件
cd your-lib-dir
npm link
cd your-cli-dir
npm link your-lib取消链接本地库文件
cd your-lib-dir
npm unlink
cd your-cli-dir
npm unlink your-lib
rm -rf node_modules # 链接已失效时,删除后重新安装
npm installcd your-cli-dir链接本地脚手架
cd your-cli-dir
npm link链接本地库文件
cd your-lib-dir
npm link
cd your-cli-dir
npm link your-lib取消链接本地库文件
cd your-lib-dir
npm unlink
cd your-cli-dir
npm unlink your-lib
rm -rf node_modules # 链接已失效时,删除后重新安装
npm installnpm unlink your-lib链接本地脚手架
cd your-cli-dir
npm link链接本地库文件
cd your-lib-dir
npm link
cd your-cli-dir
npm link your-lib取消链接本地库文件
cd your-lib-dir
npm unlink
cd your-cli-dir
npm unlink your-lib
rm -rf node_modules # 链接已失效时,删除后重新安装
npm installrm -rf node_modules # 链接已失效时,删除后重新安装链接本地脚手架
cd your-cli-dir
npm link链接本地库文件
cd your-lib-dir
npm link
cd your-cli-dir
npm link your-lib取消链接本地库文件
cd your-lib-dir
npm unlink
cd your-cli-dir
npm unlink your-lib
rm -rf node_modules # 链接已失效时,删除后重新安装
npm installnpm install
| 命令 | 作用 |
|---|---|
npm link |
将当前项目链接到 node 全局 node_modules 中作为一个库文件,并解析 bin 配置创建可执行文件 |
npm link your-lib |
将当前项目 node_modules 下指定的库文件链接到 node 全局 node_modules 下的库文件 |
npm unlink |
将当前项目从 node 全局 node_modules 中移除 |
npm unlink your-lib |
将当前项目中的库文件依赖移除 |
- 常见问题
| 问题 | 原因 | 解决 |
|---|---|---|
Cannot find module ... valid "main" entry |
库的 main 指向的 index.js 不存在 |
main 改为 lib/index.js |
| 发布后依赖装不上 | package.json 没写 dependencies |
手动添加 "my-cli-lib": "^1.0.0",或 npm install -S my-cli-lib |
unlink 或 install 报 ENOENT |
软链接指向的目录已不存在 | 删除 node_modules 后重新 npm install |
| 改了库代码不生效 | 当前装的是远程版本,没有链接到本地 | 重新 npm link your-lib |
- 注意点
npm link不会写入dependencies,发布前必须手动添加依赖npm unlink your-lib会同时把该依赖从dependencies中移除- 库文件上线前一定先本地调试,否则发现
main等配置错误就要反复发新版本
面试/考试记忆点
- 脚手架命令名由
package.json的bin字段决定 npm install -g做两件事:把包下载到 node 的lib/node_modules,再按bin配置在 node 的bin目录创建软链接- 入口文件首行
#!/usr/bin/env node让系统在环境变量中找到 node 来执行该文件 #!/usr/bin/env node比写死 node 路径可靠,换机器也能运行- 脚手架本质是操作系统客户端,真正的客户端是 node,JS 文件只是 node 的参数
- 用
ln -s可以创建命令和别名,软链接可以嵌套 - 命令执行三个关键节点:
$PATH查命令、软链接找实际文件、node 执行文件,任一环节失败命令都执行不了 - 脚手架开发难点:分包、命令注册、参数解析、帮助文档(global help / command help)
npm link链接本地项目到全局,npm link your-lib链接本地库,npm unlink取消链接