时隔三个月, 终于有时间写脚手架系列第二篇文章了, 在北京上班确实比天津忙多了, 都没时间摸鱼. 如果你没看过本系列的第一篇文章手把手教你写一个脚手架 https://juejin.cn/post/6932610749906812935 , 建议先看一遍再来阅读本文, 效果更好.
mini-cli 项目 GitHub 地址: https://github.com/woai3c/mini-cli
v3 版本的代码在 v3 分支, v4 版本的代码在 v4 分支.
第三个版本 v3
第三个版本主要添加了两个功能:
将项目拆分为 monorepo 的组织方式.
新增 add 命令, 可以通过 mvc add xxx 命令的方式来添加插件.
monorepo
首先来简单了解一下 monorepo 和 multirepo. 它们都是项目管理的一种方式, multirepo 就是将不同的项目放在不同的 Git 仓库维护, 而 monorepo 将多个项目放在同一个 Git 仓库中维护. 在 v3 版本里, 我要将 mini-cli 改造成 monorepo 的方式, 把不同的插件当成一个个独立的项目来维护.
在将项目改造成 monorepo 后, 目录如下所示:
├─packages
│ ├─@mvc
│ │ ├─cli # 核心插件
│ │ ├─cli-plugin-babel # babel 插件
│ │ ├─cli-plugin-linter # linter 插件
│ │ ├─cli-plugin-router # router 插件
│ │ ├─cli-plugin-vue # vue 插件
│ │ ├─cli-plugin-vuex # vuex 插件
│ │ └─cli-plugin-webpack # webpack 插件
└─scripts # commit message 验证脚本 和项目无关 不需关注
│─lerna.JSON
|─package.JSON
monorepo 改造过程
全局安装 lerna
NPM install -g lerna
创建项目
Git init mini-cli
初始化
- cd mini-cli
- lerna init
创建 package
lerna create xxx
由于 cli 是脚手架核心代码, 在这里需要调用其他插件, 因为要将其他插件添加到 @mvc/cli 的依赖项
- # 如果是添加到 devDependencies, 则需要在后面加上 --dev
- # 下载第三方依赖也是同样的命令
- lerna add @mvc/cli-plugin-babel --scope=@mvc/cli
改造成 monorepo-repo 后的脚手架功能和第二版没有区别, 只是将插件相关的代码独立成一个单独的 repo, 后续可以将插件单独发布到 NPM.
使用 monorepo 的优点
如果采用 multirepo 的方式开发, 在本地调试时如果需要调用其他插件, 则需要先执行 NPM i 安装, 才能使用. 采用 monorepo 则没有这种烦恼, 可以直接调用在 packages 目录里的其他插件, 方便开发调试.
如果多个插件都进行了修改, 执行 lerna publish 时可以同时发布已经修改过的插件, 不用每个单独发布.
add 命令
将项目改造成 monorepo-repo 的目的就是为了后续方便做扩展. 例如生成的项目原来是不支持 router 的, 在中途突然想加入 router 功能, 就可以执行命令 mvc add router 添加 vue-router 依赖以及相关的模板代码.
先来看一下 add 命令的代码:
- const path = require('path')
- const inquirer = require('inquirer')
- const Generator = require('./Generator')
- const clearConsole = require('./utils/clearConsole')
- const PackageManager = require('./PackageManager')
- const getPackage = require('./utils/getPackage')
- const readFiles = require('./utils/readFiles')
- async function add(name) {
- const targetDir = process.cwd()
- const pkg = getPackage(targetDir)
- // 清空控制台
- clearConsole()
- let answers = {}
- try {
- const pluginPrompts = require(`@mvc/cli-plugin-${name}/prompts`)
- answers = await inquirer.prompt(pluginPrompts)
- } catch (error) {
- console.log(error)
- }
- const generator = new Generator(pkg, targetDir, await readFiles(targetDir))
- const pm = new PackageManager(targetDir, answers.packageManager)
- require(`@mvc/cli-plugin-${name}/generator`)(generator, answers)
- await generator.generate()
- // 下载依赖
- await pm.install()
- }
- module.exports = add
由于 v3 版本仍然是在本地开发的, 所以没有将相关插件发布到 NPM 上, 因为可以直接引用插件, 而不需执行 NPM i 安装. 在 v2 版本执行 create 命令创建项目时, 所有的交互提示语都是放在 cli 插件下的, 但是 add 命令是单独添加一个插件, 因此还需要在每个插件下添加一个 prompts.JS 文件 (如果不需要, 可以不加), 里面是一些和用户交互的语句. 例如用 add 命令添加 router 插件时, 会询问是否选择 history 模式.
- const chalk = require('chalk')
- module.exports = [
- {
- name: 'historyMode',
- type: 'confirm',
- message: `Use history mode for router? ${chalk.yellow(`(Requires proper server setup for index fallback in production)`)}`,
- description: `By using the html5 History API, the URLs don't need the'#' character anymore.`,
- },
- ]
从 add 命令的代码逻辑可以看出来, 如果新加的插件有 prompts.JS 文件就读取代码弹出交互语句. 否则跳过, 直接进行下载.
第四个版本 v4
v4 版本主要将 webpack 的 dev 和 build 功能做成了动态, 原来的脚手架生成的项目是有一个 build 目录, 里面是 webpack 的一些配置代码. v4 版本的脚手架生成的项目是没有 build 目录的.
这一个功能通过新增的 mvc-cli-service 插件来实现, 生成的项目中会有以下两个脚本命令:
- scripts: {
- serve: 'mvc-cli-service serve',
- build: 'mvc-cli-service build',
- },
当运行 NPM run serve 时, 就会执行命令 mvc-cli-service serve. 这一块的代码如下:
- #!/usr/bin/env node
- const webpack = require('webpack')
- const WebpackDevServer = require('webpack-dev-server')
- const devConfig = require('../lib/dev.config')
- const buildConfig = require('../lib/pro.config')
- const args = process.argv.slice(2)
- if (args[0] === 'serve') {
- const compiler = webpack(devConfig)
- const server = new WebpackDevServer(compiler)
- server.listen(8080, '0.0.0.0', err => {
- console.log(err)
- })
- } else if (args[0] === 'build') {
- webpack(buildConfig, (err, stats) => {
- if (err) console.log(err)
- if (stats.hasErrors()) {
- console.log(new Error('Build failed with errors.'))
- }
- })
- } else {
- console.log('error command')
- }
原理如下 (NPM scripts 使用指南):
NPM 脚本的原理非常简单. 每当执行 NPM run, 就会自动新建一个 Shell, 在这个 Shell 里面执行指定的脚本命令. 因此, 只要是 Shell(一般是 Bash) 可以运行的命令, 就可以写在 NPM 脚本里面.
比较特别的是, NPM run 新建的这个 Shell, 会将当前目录的 node_modules/.bin 子目录加入 PATH 变量, 执行结束后, 再将 PATH 变量恢复原样.
上述代码对执行的命令进行了判断, 如果是 serve, 就 new 一个 WebpackDevServer 实例启动开发环境. 如果是 build, 就用 webpack 进行打包.
vue-cli 的 webpack 配置是动态的, 使用了 chainwebpack 来动态添加不同的配置, 我这个 demo 是直接写死的, 主要是没时间, 所以没有再深入研究.
发布到 NPM 后
下载 mini-cli 脚手架, 其实下载的只是核心插件 mvc-cli. 如果这个插件需要引用其他插件, 则需要先进行安装, 再调用. 因此对 create add 命令需要做一些修改. 下面看一下 create 命令代码的改动:
- answers.features.forEach(feature => {
- if (feature !== 'service') {
- pkg.devDependencies[`mvc-cli-plugin-${feature}`] = '~1.0.0'
- } else {
- pkg.devDependencies['mvc-cli-service'] = '~1.0.0'
- }
- })
- await writeFileTree(targetDir, {
- 'package.json': JSON.stringify(pkg, null, 2),
- })
- await pm.install()
- // 根据用户选择的选项加载相应的模块, 在 package.JSON 写入对应的依赖项
- // 并且将对应的 template 模块渲染
- answers.features.forEach(feature => {
- if (feature !== 'service') {
- require(`mvc-cli-plugin-${feature}/generator`)(generator, answers)
- } else {
- require(`mvc-cli-service/generator`)(generator, answers)
- }
- })
- await generator.generate()
- // 下载依赖
- await pm.install()
上面的代码就是新增的逻辑, 在用户选择完需要的插件后, 将这些插件写入到 pkg 对象, 然后生成 package.JSON 文件, 再执行 NPM install 安装依赖. 安装完插件后, 再读取每个插件的 generator 目录 / 文件代码, 从而生成模板或再次添加不同的依赖. 然后再执行一次安装.
发布遇到的坑
v3 版本的插件有一个前缀 @mvc, 由于带有 @ 前缀的 NPM 包会默认作为私人包, 因此遇到了一些坑. 花费了挺长的时间, 后来懒得弄了, 干脆将所有的插件重新改了前缀名, 变成 mvc 开头的前缀.
参考资料
lerna 多包管理实践 https://juejin.cn/post/6844904194999058440
https://github.com/vuejs/vue-cli
NPM scripts 使用指南
来源: https://segmentfault.com/a/1190000040053861