脚手架原理与本地调试:从 bin 软链接到 npm link

62 阅读6分钟

脚手架实现原理

简介:回答三个问题就掌握了脚手架原理:命令名从哪来、全局安装做了什么、为什么 JS 文件能直接执行

  • 问题一:为什么全局安装 @vue/cli 后,命令叫 vue
    • 命令名由 package.json 的 bin 字段决定,与包名、入口文件名无关
    • bin 的 key 是命令名(软链接名),value 是实际执行的文件
{
  "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 指向一个 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')
  • 下面三条命令等价
写法说明
./test.js文件首行有 #!/usr/bin/env node 时可直接执行
/usr/bin/env node test.jsenv 在环境变量中找到 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 可以手动创建快捷方式,效果相同
cd ~/.nvm/versions/node/v12.11.1/bin/
ln -s /Users/xxx/Desktop/vue-test/test.js mycli   # ln -s 目标文件 链接名
mycli                                             # 输出 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')" 直接传字符串也能执行
    • 和 PC 上安装的软件本质没有区别
      • 区别只是普通软件提供 GUI,node 通过命令行传参执行
      • node 想展示 GUI 也可以,调用操作系统的 GUI 绘制 API 即可
  • 如何为脚手架命令创建别名

    • 软链接可以嵌套:别名 → 原命令 → 实际文件
cd ~/.nvm/versions/node/v12.11.1/bin/
ln -s ./mycli mycli2   # mycli2 -> mycli -> test.js
mycli2                 # 输出 hello cli
  • 脚手架命令执行全过程

脚手架命令执行全过程

  • 实线是正常执行路径,虚线指向三个关键节点各自的失败表现(橙色),绿色是执行成功
  • 排查思路
节点报错排查方法
查环境变量command not foundwhich vue、echo $PATH
查实际链接文件No such file or directoryls -l 看软链接指向是否正确
用 node 执行bad interpreter 等检查首行写法、which node

脚手架开发流程与难点

简介:开发流程只有五步,但要让脚手架真正好用,还要解决分包、命令注册、参数解析、帮助文档等难点

  • 开发流程

脚手架开发流程

  • 红色两步是脚手架能被当作命令执行的关键
  • 使用流程
npm install -g 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
  • 帮助文档

    • global help:主命令的帮助,包含 Usage、Options、Commands,如 vue -h
    • command help:子命令的帮助,包含 Usage、Options,如 vue create --help
Usage: vue <command> [options]

Options:
  -V, --version                output the version number
  -h, --help                   output usage information

Commands:
  create [options] <app-name>  create a new project powered by vue-cli-service
  add [options] <plugin>       install a plugin and invoke its generator
  ...

快速入门第一个脚手架

简介:创建项目、写入口文件、配置 bin、发布 npm,四步完成一个能全局执行的命令

  • 创建项目
mkdir my-cli && cd my-cli
npm init -y
mkdir bin && touch bin/index.js
  • 入口文件 bin/index.js
#!/usr/bin/env node

console.log('welcome my-cli')
  • 配置 package.json 的 bin
{
  "name": "my-cli",
  "version": "1.0.0",
  "bin": {
    "my-cli": "bin/index.js"
  }
}
  • 发布和安装
npm login                # 登录 npm
npm publish              # 发布
npm install -g my-cli    # 全局安装
my-cli                   # 输出 welcome my-cli
which 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
  },
}
  • 库的 package.json 要把 main 指向真实入口
{
  "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')
  • link 后的链接关系

npm-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
命令作用
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 取消链接