Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

手把手教你写一个脚手架(二) #23

Open
woai3c opened this issue May 24, 2021 · 6 comments
Open

手把手教你写一个脚手架(二) #23

woai3c opened this issue May 24, 2021 · 6 comments

Comments

@woai3c
Copy link
Owner

woai3c commented May 24, 2021

时隔三个月,终于有时间写脚手架系列第二篇文章了,在北京上班确实比天津忙多了,都没时间摸鱼。如果你没看过本系列的第一篇文章手把手教你写一个脚手架,建议先看一遍再来阅读本文,效果更好。

mini-cli 项目 github 地址:https://github.com/woai3c/mini-cli

v3 版本的代码在 v3 分支,v4 版本的代码在 v4 分支。

第三个版本 v3

第三个版本主要添加了两个功能:

  1. 将项目拆分为 monorepo 的组织方式。
  2. 新增 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 的优点

  1. 如果采用 multirepo 的方式开发,在本地调试时如果需要调用其他插件,则需要先执行 npm i 安装,才能使用。采用 monorepo 则没有这种烦恼,可以直接调用在 packages 目录里的其他插件,方便开发调试。
  2. 如果多个插件都进行了修改,执行 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 开头的前缀。

参考资料

@cookiepool
Copy link

大佬牛皮,最近刚好在看vue/cli的源码,你的文章给我解了许多疑惑,现在vue/cli集成了许多能力,要拆分出来看源码确实比较难,看你的精简版理解就比较容易!!!

@woai3c
Copy link
Owner Author

woai3c commented Jun 3, 2021

大佬牛皮,最近刚好在看vue/cli的源码,你的文章给我解了许多疑惑,现在vue/cli集成了许多能力,要拆分出来看源码确实比较难,看你的精简版理解就比较容易!!!

有帮助就行

@zhenglll
Copy link

zhenglll commented Jan 5, 2022

感谢分享 :)

@frontendsmallAnt
Copy link

牛逼,原版vuecli的源码太绕了,始终串联不起来,看了你的文章豁然开朗

@yangzl96
Copy link

大佬,问个问题,Vuecli中使用 vue ui 创建的可视化界面去创建项目,关于这一块他的cli选的配置是怎么和页面联动的啊

@woai3c
Copy link
Owner Author

woai3c commented Apr 25, 2023

大佬,问个问题,Vuecli中使用 vue ui 创建的可视化界面去创建项目,关于这一块他的cli选的配置是怎么和页面联动的啊

这块代码我还没看,暂时帮不了你。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants