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

脚手架实现原理

简介:回答三个问题就掌握了脚手架原理:命令名从哪来、全局安装做了什么、为什么 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 node

      console.log('hello cli')
      #!/usr/bin/env node

      console.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')" 直接传字符串也能执行
    • 和 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
  • 帮助文档

    • global help:主命令的帮助,包含 Usage、Options、Commands,如 vue -h

    • command help:子命令的帮助,包含 Usage、Options,如 vue create --help

      Usage: vue [options]

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

      Commands:
      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 node

    console.log('welcome my-cli')
    #!/usr/bin/env node

    console.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 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

    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')const lib = require('my-cli-lib')
    #!/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')console.log(lib.sum(1, 2)) // 3
    #!/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')console.log(lib.mul(3, 4)) // 12
    #!/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')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 取消链接
相关推荐
海码事务所1 小时前
Google Play 新个人开发者账号上架指南:12 人连续 14 天封闭测试怎么做?
前端
田威AI1 小时前
图片内文字翻译的规格:输入输出、保真、自动化、时间与费用
前端·计算机视觉
不可能片场1 小时前
命令行中文变问号 我用环境变量救了场
前端·electron
lerhxx2 小时前
AI 应用中的上下文管理 —— 从"上下文窗口"到"分层压缩"的工程实践
前端·javascript
骑着蜗牛撵大象3272 小时前
多 Agent 分治协作:MapReduce 模式下的并行扇出与结果归并
前端
用户55318297322 小时前
Flutter iOS 热更新深入 Dart VM:读懂函数入口、解释执行与调用桥接
前端
前端snow2 小时前
为什么大厂要用Postgres + mogodb的框架?
前端
运维有小邓12 小时前
2026年主流AD域管理软件有哪些?
前端
Mickey同学2 小时前
提示词注入:AI 时代的「SQL 注入」
前端