脚手架 命令
vue-cli2 vue init webpack hello
vue-cli4(5) vue create hello-world
cra npx create-react-app my-app
vite yarn create vite
umi yarn create umi appName
umi mkdir myapp && cd myapp npx @umijs/create-umi-app

2. vue-cli2

2.1 初始化项目

// 因为是老版本了指定版本 可以使用npx来执行
npm install -g vue-cli@2 --force
vue init webpack hello-vue-cli2

2.2 基本流程

downloading template...
// 1. 选择一些选项
? Project name
vue-cli · Generated "hello-vue-cli2".
// 2. 安装依赖
Installing project dependencies ...
// 3. Project initialization finished!

2.3 基本思路

// 1. 注册命令 vue init
// 2. 下载模板到缓存目录
// 3. 获取用户交互数据 生成项目 
// 4.安装依赖

2.4 简单实现

// https://github.com/vuejs/vue-cli/tree/v2
mkdir 1.vue-cli2 && cd 1.vue-cli2
npm init -y
// 安装一些依赖
npm install commander chalk ora inquirer download-git-repo metalsmith handlebars consolidate async minimatch
// https://www.npmjs.com/package/download-git-repo 下载git上仓库代码
const download = require('download-git-repo') 
// https://www.npmjs.com/package/commander 命令行
const program = require('commander')
// https://www.npmjs.com/package/ora 显示loading效果
const ora = require('ora')
// https://www.npmjs.com/package/inquirer 用户交互
const inquirer = require('inquirer')
// https://www.npmjs.com/package/chalk 输出彩色文字
const chalk = require('chalk')
// https://www.npmjs.com/package/metalsmith 可插拔的静态站点生成器
const Metalsmith = require('metalsmith')
// https://www.npmjs.com/package/handlebars 模板引擎
const Handlebars = require('handlebars')
// https://www.npmjs.com/package/consolidate 模板引擎的结合体。包括了常用的jade和ejs
const render = require('consolidate').handlebars.render
// https://www.npmjs.com/package/async for use with Node-style callbacks
const async = require('async')
// https://www.npmjs.com/package/minimatch 匹配文件
const match = require('minimatch')
// 在package.json文件中增加bin 
"bin": {
  "vue-init": "bin/vue-init",
  • vue-init.js
  • // 根据用户选项下载指定模板
    // 1. 解析命令行参数
    program
      .usage('<template-name> [project-name]')
    program.parse(process.argv)
    // template-name(webpack) vue init webpack hello
    const template = program.args[0]
    // 项目名 hello
    const rawName = program.args[1]
    // /Users/liu/.vue-templates/webpack 会在.vue-templates目录下看是否有缓存
    const tmp = path.join(home, '.vue-templates', template.replace(/[\/:]/g, '-'))
    
    // 2. 拉取template代码 这里只是将template模板拉取下来了 还没有生成我们的项目
    const spinner = ora('downloading template')
    spinner.start()
    const officialTemplate = 'vuejs-templates/' + template
    // https://github.com/vuejs-templates/webpack
    // 下载模板 webpack 将webpack git clone 到 .vue-templates/webpack中
    download(officialTemplate, tmp, { clone }, err => {
      spinner.stop()
      generate(rawName, tmp, to, err => {
        logger.success('Generated "%s".', name)
    
    // 3. 生成项目 generate 用户交互 处理template目录中的内容
    // 最简单的就是不处理用户的选项 直接将整个项目copy到我们的目录中
    function generate(name, src, dest, done) {
      // 1. 获取选项 
      const js = path.join(src, "meta.js"); // 找到 vuejs-templates/webpack下的meta.js文件
      const req = require(path.resolve(js));
      // {metalsmith:{ before: addTestAnswers}, prompts, filters}
      const opts = req;
      // 2. metalsmith
      const metalsmith = Metalsmith(path.join(src, "template"));
      const data = Object.assign(metalsmith.metadata(), {
        destDirName: name,
        inPlace: dest === process.cwd(),
        noEscape: true,
      // 3. 处理用户交互
      metalsmith.use(askQuestions(opts.prompts))
      // 4. 根据用户的选项过滤一些文件
      // vue-cli是filter文件 vue-cli4提供的插件一般是提供最基础的template新增一些文件
      .use(filterFiles(opts.filters))
      // 5. build成最终的文件
      metalsmith
        .clean(false)
        .source(".") // start from template root instead of `./src` which is Metalsmith's default for `source`
        .destination(dest)
        .build((err, files) => {
          // console.log(err, files)
          done(err);
          // installDependencies
          if (typeof opts.complete === 'function') {
            opts.complete(data, {chalk})
      return data;
    
    // 3. 处理用户交互
    metalsmith
      // .use(askQuestions(opts.prompts))
      // 获取用户的选项
      .use((files, metalsmith, done) => {
        // ask(opts.prompts, metalsmith.metadata(), done)
        async.eachSeries(
          Object.keys(opts.prompts),
          (key, next) => {
            // 会在metadata中添加属性
            prompt(metalsmith.metadata(), key, opts.prompts[key], next);
    
    // 4. 根据用户的选项过滤一些文件
    // vue-cli是filter文件 vue-cli4提供的插件一般是提供最基础的template新增一些文件
    metalsmith.use((files, metalsmith, done) => {
      // filter(files, opts.filters, metalsmith.metadata(), done)
      const fileNames = Object.keys(files);
      Object.keys(opts.filters).forEach((glob) => {
        fileNames.forEach((file) => {
          if (match(file, glob, { dot: true })) {
            const condition = opts.filters[glob];
            const fn = new Function(
              "data",
              "with (data) { return " + condition + "}"
            if (!fn(metalsmith.metadata())) {
              delete files[file];
      done();
    
  • meta.js
  • // 我们有很多的逻辑是要meta.js中处理的
    // https://github.com/vuejs-templates/webpack/blob/develop/meta.js
    module.exports = {
      metalsmith: {
        // When running tests for the template, this adds answers for the selected scenario
        before: addTestAnswers
      prompts: {},
      filters: {}
      // 完成之后安装依赖
      complete: function(data, { chalk }) {
        const green = chalk.green
        sortDependencies(data, green)
        const cwd = path.join(process.cwd(), data.destDirName)
        // runCommand(executable, ['install'], {cwd})
        // 使用node的spawn const spawn = require('child_process').spawn
        // spawn()
        installDependencies(cwd, data.autoInstall, green)
          .then(() => {
            return runLintFix(cwd, data, green)
          .then(() => {
            printMessage(data, green)
          .catch(e => {
            console.log(chalk.red('Error:'), e)
    
    // 我们通过template的命令去拉取模板文件 但是当我们本地存在的时候可以直接使用 .vue-template中的资源
    if (exists(templatePath)) {
      // 当存在的时候就直接 generate而不会去download
      generate()
    5. gitee
    ```js
    // 如果我们需要自己下载git仓库的模板 gitee的api文档
    const gitUrl = `https://gitee.com/api/v5/orgs/${org}/repos`;
    // gitee可以使用 个人觉得实现不够友好 是对download-git-repo的扩展
    // 期待gitee 王圣松 大佬提供更好的npm包
    npm install zdex-downloadgitrepo
    // 简单实现
    // https://gitee.com/vue-next/vue3-cli/tree/v2.0
    

    2.5 npm包发布

    // 1. 调试
    npm link
    ibox-vue-init webpack hello
    // 2. 发布npm包
    npm login (npm whoami)
    npm publish
    // You must sign up for private packages
    npm publish --access public
    

    3. vite

    3.1 初始化项目

    // https://cn.vitejs.dev/
    // vite不在本文考虑范围内 我们只是分析 vite中 create-vite包的实现
    // vite采用了monorepo的管理方式 vite的实现在构建篇分析 这里不分析具体的实现
    // 指令会先安装create-vite 执行
    yarn create vite
    

    3.2 基本流程

    // 1. 输出项目名称
    // 2. 选择框架
    // 3. 选择variant 是否需要ts
    

    3.3 基本思路

    // https://github.com/vitejs/vite/tree/main/packages/create-vite
    // create-vite的实现比较简单 就是根据选项直接从项目中拉取文件
    template-vue-ts
    template-vue
    

    3.4 简单实现

  • 初始化项目
  • // 我们只是简单实现 crete-vite就不使用 lerna初始化项目了
    npm init -y
    // 增加bin字段
    "bin": {
      "create-box-vite": "index.js",
      "ibox-cva": "index.js"
    // 注意 ⚠️ 我们要加上files字段
    // 安装依赖
    npm install minimist prompts kolorist
    // https://www.npmjs.com/package/minimist 轻量级的命令行参数解析 commander
    const argv = require("minimist")(process.argv.slice(2));
    // https://www.npmjs.com/package/prompts 用户交互 inquirer.prompt
    const prompts = require("prompts");
    // https://www.npmjs.com/package/kolorist 轻量命令行输出工具  chalk
    const { blue } = require("kolorist");
    
  • index.js
  • // 1. 定义我们支持的框架列表 简化为只支持vue和react
    const FRAMEWORKS = [
        name: 'vue',
        color: green,
        variants: [
            name: 'vue',
            display: 'JavaScript',
            color: yellow
            name: 'vue-ts',
            display: 'TypeScript',
            color: blue
        name: 'react',
        color: cyan,
        variants: [
            name: 'react',
            display: 'JavaScript',
            color: yellow
            name: 'react-ts',
            display: 'TypeScript',
            color: blue
    // [ 'vue', 'vue-ts', 'react', 'react-ts' ] 
    const TEMPLATES = FRAMEWORKS.map(
      (f) => (f.variants && f.variants.map((v) => v.name)) || [f.name]
    ).reduce((a, b) => a.concat(b), [])
    async function init() {
      // 2. 等待用户选择结果 projectName framework variant
      let result = await prompts([])
      const { framework, packageName, variant } = result
      template = variant || framework || template
      // 3.根据用户的选项找到 对应的 template 模板文件
      const templateDir = path.join(__dirname, `template-${template}`)
      // 4.将文件遍历copy到我们的目录中
      const files = fs.readdirSync(templateDir)
      for (const file of files.filter((f) => f !== 'package.json')) {
        write(file)
      // 单独处理package.json文件 要添加name属性
      // .gitignore 文件需要 renameFiles .开头的文件会出问题
    
  • template文件
  • // 我们可以用自己的最佳实践 直接拷贝vite的内容
    

    3.5 npm包发布

    // i-box/create-vite@0.0.1
    // 包名是 @i-box/create-box-vite 不需要create yarn add做了什么?
    // 执行 yarn create @i-box/box-vite
    

    4. cra

    4.1 初始化项目

    // https://github.com/facebook/create-react-app
    npx create-react-app react-demo
    

    4.2 基本流程

    // 1. 创建项目 mkdir my-app
    Creating a new React app in /Users/liu/my-app.
    // 2.初始化npm按照依赖 
    // cd my-app npm init -y
    // yarn add react react-dom react-scripts cra-template
    Installing packages. This might take a couple of minutes.
    Installing react, react-dom, and react-scripts with cra-template...
    yarn add v1.22.10
    [1/4] 🔍  Resolving packages...
    [2/4] 🚚  Fetching packages...
    [3/4] 🔗  Linking dependencies...
    [4/4] 🔨  Building fresh packages...
    success Saved lockfile.
    ├─ cra-template@1.1.2
    ├─ react-dom@17.0.2
    ├─ react-scripts@4.0.3
    └─ react@17.0.2
    info All dependencies
    ├─ cra-template@1.1.2
    ├─ immer@8.0.1
    ├─ react-dev-utils@11.0.4
    ├─ react-dom@17.0.2
    ├─ react-scripts@4.0.3
    ├─ react@17.0.2
    └─ scheduler@0.20.2Done in 20.65s.
    // 3.初始化git仓库 git init
    Initialized a git repository.
    // 4.使用yarn安装模板 cra-template
    Installing template dependencies using yarnpkg...
    yarn add v1.22.10
    [1/4] 🔍  Resolving packages...
    [2/4] 🚚  Fetching packages...
    [3/4] 🔗  Linking dependencies...
    [4/4] 🔨  Building fresh packages...
    success Saved lockfile.
    success Saved 17 new dependencies.
    // 5.安装模版之后将cra-template remove掉
    Removing template package using yarnpkg...
    yarn remove v1.22.10
    [1/2] 🗑  Removing module cra-template...
    [2/2] 🔨  Regenerating lockfile and installing missing dependencies...
    success Uninstalled packages.
    ✨  Done in 7.42s.
    // 6.创建git的commit
    Created git commit.
    // 成功
    Success! Created my-app at /Users/liu/my-app
    Inside that directory, you can run several commands:
    // 7. 可以运行的命令 build start
      yarn start
        Starts the development server.
      yarn build
        Bundles the app into static files for production.
    We suggest that you begin by typing:
      cd my-app
      yarn start
    Happy hacking!
    

    4.3 实现基本思路

    // 1. 安装 react react-dom react-scripts cra-template
    // 2. 安装 cra-template  模板文件
    // 3. 卸载 cra-template
    

    4.4 简单实现

  • 项目初始化
  • // 使用lerna初始化
    lerna init
    // 创建子项目
    // 创建项目 主要分析这个包
    lerna create @i-box/create-ibox-react-app
    // 模板文件
    lerna create @i-box/ibox-cra-template
    // 脚本 我们执行 "start": "react-scripts start", webpack最佳实践 暂不分析改部分
    lerna create @i-box/ibox-react-scripts
    // 在package.json中添加
    "workspaces": [
      "packages/*"
    
  • cra源码调试
  • // 1. 下载源码  https://github.com/facebook/create-react-app
    // 2. 执行yarn 按照依赖 创建链接
    // 3. .vscode\launch.json中配置
      "version": "0.2.0",
      "configurations": [
          "name": "Launch via NPM",
          "request": "launch",
          "runtimeArgs": ["run-script", "create"],
          "runtimeExecutable": "npm",
          "skipFiles": ["<node_internals>/**"],
          "type": "pwa-node"
    // 4. 在package.json中新增脚本
    "create": "node ./packages/create-react-app/index.js hello-world",
    
  • create-ibox-react-app
  • // 主要实现这个包
    
  • cra-template
  • // 这是一个模板文件 我们可以直接拷贝 cra仓库中的代码
    template目录和template.json文件
    // 我们需要在package.json中指定files
    "files": [
      "template",
      "template.json"
    
  • react-script
  • // 这是一个比较核心的包 和vue中的vue-cli-service类似
    // webpack最佳实践 但是好像不支持插件系统
    // react-app-rewired是如何修改webpack配置的
    

    4.5 create-ibox-react-app

  • 项目初始化
  • // 1. 在package.json中新增bin目录
    "bin": { 
      "ibox-react-app": "./index.js",
      "ibox-cra": "./index.js" 
    // 2. 安装依赖
    yarn workspace @i-box/create-ibox-react-app add  chalk  commander fs-extra cross-spawn
    
  • index.js
  • #!/usr/bin/env node
    const { init } = require("./createReactApp");
    init();
    
  • createReactApp
  • function init() {
      // 1. 解析参数
      const program = new commander.Command(packageJson.name)
        .version(packageJson.version)
        .arguments("<project-directory>")
        .usage(`${chalk.green("<project-directory>")} [options]`)
        .action((name) => {
          projectName = name;
        .parse(process.argv);
      createApp(
        projectName,
        program.verbose,
        program.scriptsVersion,
        program.template,
        program.useNpm,
        program.usePnp
    
  • createApp
  • async function createApp(name, verbose, version, template, useNpm, usePnp) {
      const root = path.resolve(name);
      const appName = path.basename(root);
      fs.ensureDirSync(name); // // mkdir my-app
      console.log(`Creating a new React app in ${chalk.green(root)}.`);
      const packageJson = {
        name: appName,
        version: '0.1.0',
        private: true,
      // 写入package.json文件
      fs.writeFileSync(
        path.join(root, 'package.json'),
        JSON.stringify(packageJson, null, 2) + os.EOL
      const originalDirectory = process.cwd();
      // 切换目录
      process.chdir(root);
      // 执行run方法 主要就是安装四个包
      await run(root, appName, originalDirectory);
    
    async function run(root, appName, originalDirectory) {
      // 我们先使用官方的包
      const scriptName = "react-scripts";
      const templateName = "cra-template"; // 模版其实是可以配置的
      const allDependencies = ["react", "react-dom", scriptName, templateName];
      console.log("Installing packages. This might take a couple of minutes.");
      console.log(
        `Installing ${chalk.cyan("react")}, ${chalk.cyan(
          "react-dom"
        )}, and ${chalk.cyan(scriptName)} with ${chalk.cyan(templateName)}`
      await install(root, allDependencies);
      // 安装完之后 create-react-app包的功能基本就完成了
      // 接下来要去react-scripts包去执行相关逻辑
      let data = [root, appName, true, originalDirectory, templateName];
      let source = `
        var init = require('react-scripts/scripts/init.js');
        init.apply(null, JSON.parse(process.argv[1]));
      // 执行node命令  react-script包中scripts目录下的init.js
      await executeNodeScript({ cwd: process.cwd() }, data, source);
      process.exit(0);
    // 执行 react-scripts中的脚本文件 后面的步骤也是在这里面处理的
    async function executeNodeScript({ cwd }, data, source) {
      return new Promise((resolve) => {
        const child = spawn(
          process.execPath,
          ["-e", source, "--", JSON.stringify(data)],
          { cwd, stdio: "inherit" }
        child.on("close", resolve);
    
  • install
  • function install(root, allDependencies) {
      return new Promise((resolve) => {
        const command = "yarnpkg";
        // 拼接参数
        const args = ["add", "--exact", ...allDependencies, "--cwd", root];
        // 执行yarn add spawn跨平台的开启子进程的包
        const child = spawn(command, args, { stdio: "inherit" });
        child.on("close", resolve);
    

    4.6 react-scripts

  • 初始化项目
  • // react-script分为两部分来实现
    // 1.继续create-react-app run方法中install之后的逻辑 执行 scripts中的init.js
    // 2.作为命令来使用 react-scripts start (yarn start) 暂不分析
    // 安装依赖
    yarn workspace @i-box/ibox-react-scripts add  react react-dom
    yarn workspace @i-box/ibox-react-scripts add  cross-spawn fs-extra chalk webpack webpack-dev-server babel-loader babel-preset-react-app html-webpack-plugin  open
    // 在package.json文件中配置bin和scripts脚本
    "bin": {
      "rs": "./bin/react-scripts.js"
    "files": [
      "bin",
      "scripts"
    "scripts": {
      "build": "node ./bin/react-scripts build",
      "start": "node ./bin/react-scripts start"
    
  • scripts中的init.js方法
  • // 我们需要完成事
    // 修改package.json内容(增加脚本命令)
    // 拷贝cra-template内容 
    // 初始化仓库
    // 安装cra-template中依赖 template.json文件
    // 卸载cra-template
    module.exports = function (
      appPath, // path.resolve(appName)
      appName, // my-app
      verbose,
      originalDirectory, // process.cwd()
      templateName // cra-template
      // 拿到项目中的package.json文件
      const appPackage = require(path.join(appPath, "package.json"));
      // 先拿到模板中的package.json文件 将package.json的内容做一个合并 merge
      const templatePath = path.dirname(
        require.resolve(`${templateName}/package.json`, { paths: [appPath] })
      // 在package.json文件中新增几个命令 start build test eject
      appPackage.scripts = Object.assign({
        start: "react-scripts start",
        build: "react-scripts build",
      // 其他一系列的设置 eslint config browsers list appPackage.xxx = xx
      // 写入package.json文件内容
      fs.writeFileSync(
        path.join(appPath, "package.json"),
        JSON.stringify(appPackage, null, 2) + os.EOL
      // 拷贝文件 拿到cra-template下的template中的内容
      const templateDir = path.join(templatePath, "template");
      fs.copySync(templateDir, appPath); // 将template中的内容拷贝过来
      // template中的gitignore文件 写入到.gitignore中
      const data = fs.readFileSync(path.join(appPath, "gitignore"));
      fs.appendFileSync(path.join(appPath, ".gitignore"), data);
      fs.unlinkSync(path.join(appPath, "gitignore"));
      // 初始化仓库 require("child_process").execSync
      execSync("git init", { stdio: "ignore" });
      console.log("Initialized a git repository.");
      // 安装cra-template中的依赖 template.json
      let command = "yarnpkg",
        remove = "remove",
        args = ["add"];
      // args = args.concat(["react", "react-dom"]); // old CRA cli
      console.log(`Installing template dependencies using ${command}...`);
      // 执行安装命令 主要是处理template.json中的文件
      const proc = spawn.sync(command, args, { stdio: "inherit" });
      // 移除cra-template
      console.log(`Removing template package using ${command}...`);
      const proc1 = spawn.sync(command, [remove, templateName], {
        stdio: "inherit",
      // 提交git commit
      execSync("git add -A", { stdio: "ignore" });
      execSync('git commit -m "Initialize project using Create React App"', {
        stdio: "ignore",
      // 完成
      console.log(`Success! Created ${appName} at ${appPath}`);
    

    4.7 发布

    lerna publish 
    // 需要先提交git
    

    5. vue-cli4

    1. 初始化项目

    // https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue
    // 现在已经是 vue-cli5的beta版本了 主要用了webpack5
    vue create hello-world
    

    2. 基本流程

    // 1. 选择预设
    ? Please pick a preset: Manually select features
    // 选择手动模式之后选择特性
    ? Check the features needed for your project: 
      Choose Vue version, Babel, Router, Vuex, Linter
    // 选择了模式之后会多一些选项
    ? Choose a version of Vue.js that you want to start the project with 3.x
    // 2.创建项目  mkdir hello-worldCreating project in /Users/xueshuai.liu/hello-world.
    // 3.初始化git仓库
    🗃  Initializing git repository...
    // 4.安装plugins
     ⚙️  Installing CLI plugins. This might take a while...
    // 5. 调用生成器 这里是核心 插件
    🚀  Invoking generators...
    // 6.按照额外的依赖
     📦  Installing additional dependencies...
    // 7. 运行编辑的钩子hooksRunning completion hooks...
    // 8.生成README.md文件
     🚀  Generating README.md...
    // 9. get start
    🎉  Successfully created project hello-world. 
    👉  Get started with the following commands:
    

    3. 简单实现

  • 初始化项目
  • // vue-cli4现在是基于插件系统的
    lerna init
    // 执行yarn安装依赖
    // 修改package.json文件
    "workspaces": ["packages/*"]
    // 在lerna.json中新增
    "useWorkspaces": true,
    // 初始化子项目 
    lerna create @i-box/cli
    lerna create @i-box/cli-service
    lerna create @i-box/cli-shared-utils
    // 插件全部使用官方提供的
    lerna create @i-box/cli-plugin-vuex
    // yarn vs lerna
    yarn 用来处理依赖
    lerna用来初始化和发布
    

    3.1 cli

    // 我们主要实现 cli包的逻辑
    cd packages/cli
    // 增加bin命令
    "bin": {
      "ibox-vue": "bin/vue.js"
    // 安装依赖
    yarn workspace @i-box/cli add minimist commander fs-extra inquirer isbinaryfile ejs vue-codemod
    

    1. vue.js

    // 注册命令
    program.command("create <app-name>").action((name, options) => {
      require("../lib/create.js")(name, options);
    program.parse(process.argv);
    

    2. create.js

    async function create(projectName, options) {
      const cwd = options.cwd || process.cwd()
      const inCurrent = projectName === '.'
      const name = inCurrent ? path.relative('../', cwd) : projectName
      const targetDir = path.resolve(cwd, projectName || '.')
      // 获取弹出选项的结果 手动模式下选择的选项 vueVersion babel router vuex等
      let promptModules = getPromptModules();
      // 初始化 Creator
      const creator = new Creator(name, targetDir, promptModules)
      // 调用create方法
      await creator.create(options)
    

    2.1 getPromptModules

    // 我们手动模式下选择的特性
    const getPromptModules = () => {
      return [
        'vueVersion',
        'babel',
        'typescript',
        'pwa',
        'router',
        'vuex',
        'cssPreprocessors',
        'linter',
        'unit',
        'e2e'
      ].map(file => require(`../promptModules/${file}`))
    

    3. Creator

  • 初始化 new Creator
  • // 手动模式
    const isManualMode = answers => answers.preset === '__manual__'
    module.exports = class Creator extends EventEmitter {
      // 初始化
      constructor(name, context, prompModules) {
        super()
        this.name = name
        this.context = process.env.VUE_CLI_CONTEXT = context
        // 获取到选择的预设和特性 如果是default会有默认值
        const { presetPrompt, featurePrompt } = this.resolveIntroPrompts()
        // 手动模式 最后一步保存配置的位置和是否保存在文件中 会保存在 .vuerc 中
        this.outroPrompts = this.resolveOutroPrompts()
        // 预设选项
        this.presetPrompt = presetPrompt
        // 特性的选项 例如我们选择了 eslint会让我们选项哪种规范
        this.featurePrompt = featurePrompt
        // 选项的特征之后加入的选项
        this.injectedPrompts = []
        // 完成的回调
        this.promptCompleteCbs = []
        // 当我们选项了不同的特性 会通过api给我们增加对应的选项
        const promptAPI = new PromptModuleAPI(this)
        // 遍历特性 promptAPI提供一个注入选择特性 一个选择之后的回调(注入一些插件)
        promptModules.forEach((m) => m(promptAPI));
      // 执行create方法
      async create (cliOptions = {}, preset = null) {}
    
  • resolveIntroPrompts 获取预设和特性
  • function resolveIntroPrompts() {
      // 我们保存的选项会存在 .vuerc 中
      // const savedOptions = loadOptions()
      // const preset = Object.assign({}, savedOptions.presets, defaults.presets)
      // 默认的预设
      const defaultPreset = {
        useConfigFiles: false,
        cssPreprocessor: undefined,
        plugins: {
          '@vue/cli-plugin-babel': {},
          '@vue/cli-plugin-eslint': {
            config: 'base',
            lintOn: ['save']
      const presets = {
        'default': Object.assign({ vueVersion: '2' }, defaultPreset),
        '__default_vue_3__': Object.assign({ vueVersion: '3' }, defaultPreset)
      // 预设的选项 显示使用的
      const presetChoices = Object.entries(presets).map(([name, preset]) => {
        let displayName = name
        if (name === 'default') {
          displayName = 'Default'
        } else if (name === '__default_vue_3__') {
          displayName = 'Default (Vue 3)'
        return {
          // Default ([Vue 2] babel, eslint)
          // Default (Vue 3) ([Vue 3] babel, eslint) 
          // name: `${displayName} (${formatFeatures(preset)})`,
          name: `${displayName}`,
          value: name
      const presetPrompt  = {
        name: 'preset',
        type: 'list',
        message: `Please pick a preset:`,
        choices: [
          ...presetChoices,
          // 加上一个手动选择的模式
            name: 'Manually select features',
            value: '__manual__'
      // 特性
      const featurePrompt = {
        name: 'features',
        when: isManualMode, // 手动模式下才会有
        type: 'checkbox',
        message: 'Check the features needed for your project:',
        choices: [], // 保存我们的选项
        pageSize: 10
      return {
        presetPrompt,
        featurePrompt
    
  • resolveOutroPrompts
  • // 是否保存本次的选择结果 会在 .vuerc 中保存 什么时候保存的? 在create过程中 promptAndResolvePreset 
    function  resolveOutroPrompts() {
      return [
          name: 'useConfigFiles',
          when: isManualMode,
          type: 'list',
          message: 'Where do you prefer placing config for Babel, ESLint, etc.?',
          // 是在单独的文件中还是保存在package.json文件中
          choices: [
              name: 'In dedicated config files',
              value: 'files'
              name: 'In package.json',
              value: 'pkg'
          name: 'save',
          when: isManualMode,
          // 是否保存
          type: 'confirm',
          message: 'Save this as a preset for future projects?',
          default: false
          name: 'saveName',
          // 如果保存输入保存的name
          when: answers => answers.save,
          type: 'input',
          message: 'Save preset as:'
    
  • PromptModuleAPI
  • // 给特性注册一些api 方便选择之后做一些处理
    class PromptModuleAPI {
      constructor(creator) {
        this.creator = creator
      // 提供一些api来修改选项 还有完成选择之后的回调
      // 1. 插入一些新的特性 例如vuex vue的版本
      injectFeature (feature) {
        this.creator.featurePrompt.choices.push(feature)
      // 2. 加入新的选择 vueVersion选择了之后会要选择vue2还是vue3
      injectPrompt (prompt) {
        this.creator.injectedPrompts.push(prompt)
      // 3. 加入一些选项 我们可以自定义一些插件来提供选项
      injectOptionForPrompt (name, option) {
        this.creator.injectedPrompts.find(f => {
          return f.name === name
        }).choices.push(option)
      // 4. 选项完成之后的回调
      onPromptComplete (cb) {
        this.creator.promptCompleteCbs.push(cb)
    

    4. create

    // 调用create方法 creator.create
      // 执行create方法
      async create(cliOptions = {}, preset = null) {
        // 1. 获取用户的选项
        let preset = await this.promptAndResolvePreset();
        // 2. 注入核心的服务 @vue/cli-service 是一个核心的特殊的插件
        preset.plugins["@vue/cli-service"] = Object.assign(
            projectName: name,
          preset
        // 3.开始创建项目
        log(`✨  Creating project in ${chalk.yellow(context)}.`);
        // 4. 根据插件的依赖生成package.json写入 会处理版本的问题
        const pkg = {
          name,
          version: "0.1.0",
          private: true,
          devDependencies: {},
          // ...resolvePkg(this.context)
        // 在选项完成的回调中我们会增加 plugins的属性 options.plugins['@vue/cli-plugin-vuex'] = {}
        const deps = Object.keys(preset.plugins);
        deps.forEach((dep) => {
          pkg.devDependencies[dep] = "latest";
        await writeFileTree(context, {
          "package.json": JSON.stringify(pkg, null, 2),
        // 5. 初始化仓库
        log(`🗃  Initializing git repository...`);
        // execa 执行命令
        await this.run("git init");
        // 6.安装依赖
        log(`⚙\u{fe0f}  Installing CLI plugins. This might take a while...`);
        await run("npm install");
        // 7. 核心 生成器插件
        log(`🚀  Invoking generators...`);
        const plugins = await this.resolvePlugins(preset.plugins, pkg);
        const generator = new Generator(context, {
          plugins,
          afterInvokeCbs,
          afterAnyInvokeCbs,
        await generator.generate({
          extractConfigFiles: preset.useConfigFiles,
        // 8.安装额外的依赖 初始化readme文件 完成
        await writeFileTree(context, { "README.md": "README.md" });
        await run("git add -A");
        log(`🎉  Successfully created project ${chalk.yellow(name)}.`);
    
  • promptAndResolvePreset
  • // 获取用户的选项 解析预设
    function promptAndResolvePreset(answers = null) {
      if (!answers) {
        // await clearConsole(true)
        this.injectedPrompts.forEach(prompt => {
          const originalWhen = prompt.when || (() => true)
          prompt.when = answers => {
            return isManualMode(answers) && originalWhen(answers)
        let prompts = [
          this.presetPrompt,
          this.featurePrompt,
          ...this.injectedPrompts,
          ...this.outroPrompts
        // 获取用户选项
        // answers = await inquirer.prompt(this.resolveFinalPrompts())
        answers = await inquirer.prompt(prompts)
      // 是否保存到package.json文件中
      // if (answers.packageManager) {
      //   // fs.writeFile()
      //   saveOptions({
      //     packageManager: answers.packageManager
      //   })
      let preset
      if (answers.preset && answers.preset !== '__manual__') {
        // 不是手动模式下的 可能是默认的default 也可能是上次保存的
        // preset = await this.resolvePreset(answers.preset)
        preset = defaults.presets
      } else {
        // 手动模式
        preset = {
          useConfigFiles: answers.useConfigFiles === 'files',
          plugins: {}
        answers.features = answers.features || []
        // 运行模块注册的回调来完成预设 options.vueVersion = answers.vueVersion
        this.promptCompleteCbs.forEach(cb => cb(answers, preset))
      // 保存配置信息到.vuerc文件
      // if(answers.save) {}
      return preset
    
  • resolvePlugins
  • // 解析插件 插件系统的实现 加载 generator 模块 增加apply方法
    //  { id: options } => [{ id, apply, options }] 增加apply方法
    async  resolvePlugins(rawPlugins, pkg) {
      // 保证 @vue/cli-service 是第一个插件 这个很重要
      // 因为其他的插件是基于里面的template来做修改的
      rawPlugins = sortObject(rawPlugins, ['@vue/cli-service'], true)
      const plugins = []
      for (const id of Object.keys(rawPlugins)) {
        // 加载插件下的 generator 模块
        // 插件一般是有一个 generator目录的
        // require() Module.createRequire
        const apply = loadModule(`${id}/generator`, this.context) || (() => {})
        let options = rawPlugins[id] || {}
        if (options.prompts) {
          let pluginPrompts = loadModule(`${id}/prompts`, this.context)
          if (pluginPrompts) {
            const prompt = inquirer.createPromptModule()
            if (typeof pluginPrompts === 'function') {
              pluginPrompts = pluginPrompts(pkg, prompt)
            if (typeof pluginPrompts.getPrompts === 'function') {
              pluginPrompts = pluginPrompts.getPrompts(pkg, prompt)
            log()
            log(`${chalk.cyan(options._isPreset ? `Preset options:` : id)}`)
            options = await prompt(pluginPrompts)
        // 增加一个apply方法 什么时候执行的?
        plugins.push({ id, apply, options })
      return plugins
    

    5. Generator

    // 生成器
    class Generator {
      constructor(context, { pkg = {}, plugins = [], files = {} } = {}) {
        this.pkg = pkg
        this.files = {}
        this.fileMiddlewares = []; // 插件数组
        // load all the other plugins
        this.allPlugins = this.resolveAllPlugins()
        // @vue/cli-service 插件非常特殊 单独处理
        const cliService = plugins.find((p) => p.id === "@vue/cli-service");
        this.rootOptions = rootOptions; // 根选项
    
  • resolveAllPlugins
  • function resolveAllPlugins() {
      const allPlugins = [];
      Object.keys(this.pkg.dependencies || {})
        .concat(Object.keys(this.pkg.devDependencies || {}))
        .forEach((id) => {
          if (!isPlugin(id)) return;
          const pluginGenerator = loadModule(`${id}/generator`, this.context);
          if (!pluginGenerator) return;
          allPlugins.push({ id, apply: pluginGenerator });
      return sortPlugins(allPlugins);
    
  • generate方法
  • async generate ({ extractConfigFiles = false, checkExisting = false} = {}) {
      // 1. 初始化插件 遍历执行apply方法
      await this.initPlugins()
      const initialFiles = Object.assign({}, this.files)
      // 单独的配置文件
      // this.extractConfigFiles(extractConfigFiles, checkExisting)
      // 解析file 执行插件的方法 修改files
      await this.resolveFiles()
      // 排序
      // this.sortPkg()
      // 重新生成package.json 插件可能修改了内容
      this.files['package.json'] = JSON.stringify(this.pkg, null, 2) + '\n'
      // 写入文件书
      await writeFileTree(this.context, this.files, initialFiles, this.filesModifyRecord)
    
  • initPlugins
  • // 初始化插件
    function initPlugins() {
      const { rootOptions } = this
      const pluginIds = this.plugins.map(p => p.id)
      for (const plugin of this.plugins) {
        const { id, apply } = plugin;
        // 给插件提供的一些api方法
        const api = new GeneratorAPI(id, this, {}, rootOptions);
        // 执行apply方法 
        await apply(api, options, rootOptions, invoking)
    
  • GeneratorAPI
  • // 提供一些api给插件使用
    // render渲染内容  extendPackage扩展依赖 
    // injectImports增加import语句
    // injectRootOptions 注入根option等
    class GeneratorAPI {
      constructor(id, generator, options, rootOptions) {
        this.id = id;
        this.generator = generator;
        this.options = options;
        this.rootOptions = rootOptions; // 根选项
        this.pluginsData = generator.plugins
          .filter(({ id }) => id !== `@vue/cli-service`)
          .map(({ id }) => ({
            // eslint babel
            name: toShortPluginId(id),
        // 插件需要往entryFile中添加内容
        this._entryFile = undefined;
      // readeOnly
      get entryFile() {
        if (this._entryFile) return this._entryFile;
        // 这里就指明了 我们的入口是main
        const file = fs.existsSync(this.resolve("src/main.ts"))
          ? "src/main.ts"
          : "src/main.js";
        return (this._entryFile = file);
      // api.extendPackage({
      //   dependencies: {
      //     'vue': '^3.0.4'
      //   },
      //   devDependencies: {
      //     '@vue/compiler-sfc': '^3.0.4'
      //   }
      // })
      // 插件的功能  修改配置  render渲染内容
      extendPackage(fields) {
        // 做一个配置的合并 修改package.json文件中的依赖
        const pkg = this.generator.pkg;
        const toMerge = isFunction(fields) ? fields(pkg) : fields;
        for (const key in toMerge) {
          const value = toMerge[key];
          const existing = pkg[key];
            isObject(value) &&
            (key === "dependencies" || key === "devDependencies")
            pkg[key] = mergeDeps(existing || {}, value);
          } else {
            pkg[key] = value;
      // vue-cli-service中的使用
      // api.render('./template', {
      //   doesCompile: api.hasPlugin('babel') || api.hasPlugin('typescript'),
      //   useBabel: api.hasPlugin('babel')
      // })
      // 这一步是没有往files中添加的
      render(source, additionalData = {}) {
        const baseDir = extractCallDir(); // 提取目录
        if (isString(source)) {
          source = path.resolve(baseDir, source);
          // 添加到fileMiddlewares中
          this._injectFileMiddleware(async (files) => {
            // 解析额外的数据
            const data = this._resolveData(additionalData);
            const globby = require("globby"); // 匹配文件
            const _files = await globby(["**/*"], { cwd: source, dot: true });
            for (const rawPath of _files) {
              const targetPath = rawPath
                .split("/")
                .map((filename) => {
                  if (filename.charAt(0) === "_" && filename.charAt(1) !== "_") {
                    return `.${filename.slice(1)}`;
                  return filename;
                .join("/");
              const sourcePath = path.resolve(source, rawPath);
              // 使用ejs模版渲染
              const content = renderFile(sourcePath, data);
              // 如果是buffer
              if (Buffer.isBuffer(content) || /[^\s]/.test(content)) {
                files[targetPath] = content;
        } else if (isObject(source)) {
        } else if (isFunction(source)) {
          this._injectFileMiddleware(source);
      // 往fileMiddlewares中添加middle 这个时候只是添加了 还没有执行的
      // 修改的是fileMiddlewares和pkg files还没有修改
      // 执行resolveFiles的时候才会真正执行这些middleware
      _injectFileMiddleware(middleware) {
        this.generator.fileMiddlewares.push(middleware);
      // vue-server-cli会使用
      hasPlugin(id) {
        return this.generator.hasPlugin(id);
      _resolveData(additionalData) {
        return Object.assign(
            options: this.options,
            rootOptions: this.rootOptions,
            plugins: this.pluginsData,
          additionalData
      // Add import statements to a file. vuex vue-router
      injectImports(file, imports) {
        const _imports =
          this.generator.imports[file] ||
          (this.generator.imports[file] = new Set());
        (Array.isArray(imports) ? imports : [imports]).forEach((imp) => {
          _imports.add(imp);
      transformScript(file, codemod, options) {
        // debugger;
        const normalizedPath = this._normalizePath(file);
        this._injectFileMiddleware((files) => {
          if (typeof files[normalizedPath] === "undefined") {
            error(`Cannot find file ${normalizedPath}`);
            return;
          // files[] =
          files[normalizedPath] = runTransformation(
              path: this.resolve(normalizedPath),
              source: files[normalizedPath],
            codemod,
            options
      // vue2使用的 注入根选项
      injectRootOptions() {
        const _options =
          this.generator.rootOptions[file] ||
          (this.generator.rootOptions[file] = new Set());
        (Array.isArray(options) ? options : [options]).forEach((opt) => {
          _options.add(opt);
      // 处理文件
      postProcessFiles(cb) {
        this.generator.postProcessFilesCbs.push(cb);
      _normalizePath(p) {
        if (path.isAbsolute(p)) {
          p = path.relative(this.generator.context, p);
        return p.replace(/\\/g, "/");
      resolve(..._paths) {
        return path.resolve(this.generator.context, ..._paths);
    
  • resolveFiles
  • // 执行render方法 增加import语句
    async resolveFiles () {
      const files = this.files
      for (const middleware of this.fileMiddlewares) {
        // 执行插件的render方法 使用ejs模版渲染
        await middleware(files, ejs.render)
      // 格式化/和\
      // normalizeFilePaths(files)
      // handle imports and root option injections
      Object.keys(files).forEach(file => {
        let imports = this.imports[file]
        imports = imports instanceof Set ? Array.from(imports) : imports
        if (imports && imports.length > 0) {
          files[file] = runTransformation(
            { path: file, source: files[file] },
            require('./util/codemods/injectImports'),
            { imports }
        let injections = this.rootOptions[file]
        injections = injections instanceof Set ? Array.from(injections) : injections
        if (injections && injections.length > 0) {
          files[file] = runTransformation(
            { path: file, source: files[file] },
            require('./util/codemods/injectOptions'),
            { injections }
    
  • injectImports
  • // 插件vuex需要插入 import store from './store' // api.injectImports(api.entryFile, `import store from './store'`) // vue-cli-serve机车的template如下 我们需要插入 import语句 操作ast // import { createApp } from 'vue' // import App from './App.vue' // createApp(App).mount('#app') function injectImports (fileInfo, api, { imports }) { const j = api.jscodeshift const root = j(fileInfo.source) const toImportAST = i => j(`${i}\n`).nodes()[0].program.body[0] const toImportHash = node => JSON.stringify({ specifiers: node.specifiers.map(s => s.local.name), source: node.source.raw const declarations = root.find(j.ImportDeclaration) const importSet = new Set(declarations.nodes().map(toImportHash)) const nonDuplicates = node => !importSet.has(toImportHash(node)) const importASTNodes = imports.map(toImportAST).filter(nonDuplicates) if (declarations.length) { declarations .at(-1) // a tricky way to avoid blank line after the previous import .forEach(({ node }) => delete node.loc) .insertAfter(importASTNodes) } else { // no pre-existing import declarations root.get().node.program.body.unshift(...importASTNodes) return root.toSource()

    3.4 cli-shared-utils

    // 提供一些公共的方法
    yarn workspace @i-box/cli-shared-utils add chalk execa semver chalk strip-ansi readline events ora module
    

    3.3 vue-cli-service

    1. 作为核心插件使用

    // 根据上面的插件我们可以知道 插件中需要一个generator目录
    // 做两件事 一个是render一个是extendPackage
    module.exports = (api.options) => {
      // 渲染模版
      api.render('./template', {})
      // 扩展
      api.extendPackage({
        // 增加脚本文件
        scripts: {
          'serve': 'vue-cli-service serve',
          'build': 'vue-cli-service build'
        // 增加依赖
        dependencies: {
          vue: "^3.0.4",
        devDependencies: {
          "@vue/compiler-sfc": "^3.0.4",
    // 渲染了 template 所以我们还需要一个template目录在存放文件用来渲染
    

    2. 作为命令使用

    // 增加命令 暂不分析
    "bin": {
      "vue-cli-service": "bin/vue-cli-service.js"
    // vue-cli-service serve webpack启动服务
    // vue-cli-service build 打包
    
  • Service
  • class Service {
      constructor(context, { plugins, pkg, inlineOptions, useBuiltIn } = {}) {
        // 5.0新增的 beat已经用了webpack
        checkWebpack(context)
        // 获取package.json内容
        this.pkg = this.resolvePkg(pkg);
        // 插件 会将service的插件放在里面 命令相关的serve build webpack相关的config
        this.plugins = this.resolvePlugins(plugins, useBuiltIn)
        // vue.config.js中配置chainWebpack 也可以调用api来修改
        this.webpackChainFns = [];
        // configureWebpack
        this.webpackRawConfigFns = [];
        // 注册的命令 我们编写的插件就是注册命令
        this.commands = {};
      async run() {}
      async init(){}
      resolveWebpackConfig(){}
    // 主要是获取package.json和解析插件
    // 找到package.json文件
    resolvePkg(inlinePkg, context = this.context) {
      // 和之前一样也是用require 找context的package.json文件
      Module.createRequire(path.resolve(context, "package.json")).resolve(context)
    // 解析插件
    resolvePlugins(inlinePlugins) {
      // 和cli中的插件保持一样 增加apply方法
      const idToPlugin = (id, absolutePath) => ({
        id: id.replace(/^.\//, "built-in:"),
        apply: require(absolutePath || id),
      let plugins
      const builtInPlugins = [
        // 命令
        "./commands/serve", "./commands/build", "./commands/inspect",
        // webpack配置相关的
        "./config/base","./config/assets", "./config/css","./config/prod",
      ].map((id) => idToPlugin(id))
      // 找到package.json文件中的plugin
      const projectPlugins = Object.keys(this.pkg.devDependencies || {})
          .concat(Object.keys(this.pkg.dependencies || {}))
          .filter(isPlugin)
          .map((id) => idToPlugin(id, resolveModule(id, this.pkgContext)));
      return builtInPlugins.concat(projectPlugins)
    
    async run() {
      // 初始化 加载env load用户配置(vue.config.js) 应用插件
      await this.init(mode);
      // 执行对应的命令 server中注册命令
      let command = this.commands[name];
      const { fn } = command;
      return fn(args, rawArgv)
    
    // init方法主要三个 加载.env文件 加载用户vue.config.js配置文件 应用插件(apply方法提供一个api方法)
    async init() {
      // 加载.env 
      // 用户配置的参数必须是 VUE_APP_开头的 webpack中会通过正则匹配 webpack.DefinePlugin定义成全局的变量
      this.loadEnv();
      // 用户配置 vue.config.js
      const userOptions = this.loadUserOptions()
      // 应用插件 执行apply方法  config是webpack相关的 混入一些配置
      // commands是注册一些命令 加入到this.commands中 run的时候执行对应的命令
      const loadedCallback = (loadedUserOptions) => {
        this.plugins.forEach(({ id, apply }) => {
          // 执行apply方法 传入一些api方法给插件来使用
          apply(new PluginAPI(id, this), this.projectOptions);
        // webpack相关 vue.config.js中配置
        this.webpackChainFns.push(this.projectOptions.chainWebpack)
        this.webpackRawConfigFns.push(this.projectOptions.configureWebpack)
      return loadedCallback(userOptions)
      resolveWebpackConfig()
    
  • PluginAPI
  • // 这些api我们在开发插件的时候可以使用到
    // 插件api一个是注册命令 一个是修改配置
    class PluginAPI {
      // 注册命令 serve build都是这样注册的
      registerCommand(name, opts, fn) {
        this.service.commands[name] = { fn, opts: opts || {} };
      // 修改webpack配置
      chainWebpack() {
        this.service.webpackChainFns.push(fn);
      configureWebpack() { }
      configureDevServer(fn) {}
      resolveWebpackConfig() {}
      resolveChainableWebpackConfig(){}
    

    4. 插件

    program.command("add <plugin> [pluginOptions]").action((plugin) => {
      require("../lib/add")(plugin, minimist(process.argv.slice(3)));
    
    // vue add native-ui
    function add (pluginToAdd, options = {}, context = process.cwd()) {
      // 如果有未提交的代码直接return
      if (!(await confirmIfGitDirty(context))) {  return }
      const packageName = resolvePluginId(pluginName)
      // yarn add xxx
      await pm.add(`${packageName}@${pluginVersion}`)
      const generatorPath = resolveModule(`${packageName}/generator`, context)
      // const generator = new Generator(
      // await generator.generate()
      invoke(pluginName, options, context)
    
    // GeneratorAPI 给我们提供一些api
    // https://github.com/vuejs/vue-cli/blob/dev/packages/%40vue/cli/lib/GeneratorAPI.js
    // 插件一般是用 修改package.json文件 增加命令(修改scripts 增加依赖项) 动态注入import render渲染 模版文件等
    
  • 实现一个简单的插件
  • // 将官方提供的vuex做简单的修改即可
    module.exports = (api, options = {}, rootOptions = {}) => {
      api.injectImports(api.entryFile, `import native from './plugins/native-ui';`);
      api.transformScript(api.entryFile, require("./injectUseNativeUi"));
      // 加一个依赖
      api.extendPackage({
        dependencies: {
          "naive-ui": "^2.11.4",
      // 渲染template下的文件
      api.render("./template", {});
      // 提供选项给用户 部分导入还是全部导入
      // if (options.import === "partial") {
    

    6. create-umi

    1. 初始化项目

    // yarn create umi appName
    // https://github.com/umijs/create-umi
    

    2. 基本流程

    // 借用官方
    $ yarn create umi
    ? Select the boilerplate type (Use arrow keys)
      ant-design-pro  - Create project with a layout-only ant-design-pro boilerplate, use together with umi block.
    ❯ app             - Create project with a simple boilerplate, support typescript.
      plugin          - Create a umi plugin.
    ? Do you want to use typescript? (y/N)
    ? What functionality do you want to enable? (Press <space> to select, <a> to toggle all, <i> to invert selection)
    ❯◯ antd
     ◯ dva
     ◯ code splitting
     ◯ dll
      create abc/package.json
      create abc/.gitignore
      create abc/.editorconfig
      create abc/.env
      create abc/.eslintrc
      create abc/.prettierignore
      create abc/.prettierrc
      create abc/.umirc.js
      create abc/mock/.gitkeep
      create abc/src/assets/yay.jpg
      create abc/src/global.css
      create abc/src/layouts/index.css
      create abc/src/layouts/index.tsx
      create abc/src/pages/index.css
      create abc/src/pages/index.tsx
      create abc/tsconfig.json
      create abc/typings.d.ts
     📋  Copied to clipboard, just use Ctrl+V
     ✨  File Generate Done
    

    3. 基本流程

    // 和其他cli的思路大概一致
    // 获取用户的选项 然后根据用户的选项 生成项目
    // 如果选择的是 antd 就去 https://github.com/ant-design/ant-design-pro 拉取代码
    // 如果不是就在 generators 目录下 的 templates 目录下的 代码 writeFiles
    

    4. 简单实现

  • 初始化项目
  • npm init -y
    // 在package.json中增加命令
    "files": [
      "cli.js",
      "index.js",
      "lib"
    "bin": {
      "create-umi2": "cli.js"
    // 安装依赖
    npm install chalk inquirer mkdirp execa glob fs-extra semver typescript yargs-parser yeoman-environment yeoman-generator
    
  • cli.js
  • // 解析命令行参数
    const args = require('yargs-parser')(process.argv.slice(2))
    const run = require('./lib/run');
    // 获取项目名称
    const name = args._[0] || ''
    const {type} = args
    // 执行run方法
    (async () => {
      await run(name, type, args)
    
  • run.js
  • const run = async (config) => {
      let { type } = config;
      // 选择类型 plugin antd 还是 plugin
      const answers = await inquirer.prompt([
          name: "type",
          message: "Select the boilerplate type",
          type: "list",
          // generators 目录下的meta.json
          choices: generators,
      type = answers.type;
      // 2. 生成器 不同的type对应不同的逻辑
      return runGenerator(`./generators/${type}`, config);
    
  • generators
  • // app plugin an-design-pro
    const generators = fs
      .readdirSync(`${__dirname}/generators`)
      .filter(f => !f.startsWith('.'))
      .map(f => {
        return {
          name: `${f.padEnd(15)} - ${chalk.gray(require(`./generators/${f}/meta.json`).description)}`,
          value: f,
          short: f,
    
  • runGenerator
  • const runGenerator = async(generatorPath, { name = '', cwd = process.cwd(), args = {} }) => {
      return  new Promise(resolve => {
        if(name) {
          // 创建一个目录
          mkdirp.sync(name);
          cwd = path.join(cwd, name);
        // require('./generators/app')
        const Generator = require(generatorPath); 
        // env
        const env = yeoman.createEnv([], {
        // 实例化 generator
        const generator = new Generator({
          name,
          resolved: require.resolve(generatorPath),
          args,
        // https://yeoman.github.io/generator/Generator.html#run
        // run方法开始调度任务
        return generator.run(() => {
          resolve(true)
    
  • basic
  • // https://yeoman.github.io/generator/
    // https://www.npmjs.com/package/yeoman-generator 为终端用户生产文件
    const Generator = require("yeoman-generator");
    // 按优先级依次执行定义的方法 run方法是开始执行队列方法
    class BasicGenerator extends Generator {
      constructor(opts) {
        super(opts);
        this.opts = opts;
        this.name = basename(opts.env.cwd);
      initianlizing() {
        //获取当前项目状态,获取基本配置参数等
      prompting() {
        //向用户展示交互式问题收集关键参数
      configuring() {
        //保存配置相关信息且生成配置文件(名称多为'.'开头的配置文件,例如.editorconfig)
      default() {
        //未匹配任何生命周期方法的非私有方法均在此环节*自动*执行
      writing() {
        //依据模板进行新项目结构的写操作
      conflicts() {
        //处理冲突(内部调用,一般不用处理)
      install() {
        //使用指定的包管理工具进行依赖安装(支持npm,bower,yarn)
      end() {
        //结束动作,例如清屏,输出结束信息,say GoodBye等等
      // Prompt user to answer questions. The signature of this method is the same as Inquirer.js
      prompt(questions) {
        // 子进程通信的?
        process.send && process.send({ type: 'prompt' });
        process.emit('message', { type: 'prompt' });
        return super.prompt(questions);
      isTsFile(f) {
        return f.endsWith('.ts') || f.endsWith('.tsx') || !!/(tsconfig\.json)/g.test(f);
      // 写文件
      writeFiles({ context, filterFiles = noop }) {
        // const welcomeImages = glob.sync('**/assets/welcomeImgs/*', {})
        glob.sync('**/*', {  cwd: this.templatePath(),  dot: true,})
          .filter(filterFiles)
          .filter(file => !file.includes('welcomeImgs'))
          .forEach(file => {
            const filePath = this.templatePath(file);
            if (statSync(filePath).isFile()) {
              // 复制模板文件 模版的默认定义是在 ./templates 目录的
              this.fs.copyTpl(
                this.templatePath(filePath),
                this.destinationPath(file.replace(/^_/, '.')),
                context,
    
  • ant-design-pro
  • // 核心就是git clone
    class AntDesignProGenerator extends BasicGenerator {
      // 增加用户选项
      prompting() {
        const prompts = [
            name: 'language',
            type: 'list',
            message: '🤓 Which language do you want to use?',
            choices: ['TypeScript', 'JavaScript'],
            default: 'TypeScript',
            name: 'allBlocks',
            type: 'list',
            message: '🚀 Do you need all the blocks or a simple scaffold?',
            choices: ['simple', 'complete'],
            default: 'simple',
        // super.prompt() Inquirer.js
        return this.prompt(prompts).then(props => {
          this.prompts = props;
      async writing() {
        const projectName = this.opts.name || this.opts.env.cwd;
        // 需要子啊空文件夹中使用
        // const projectPath = path.resolve(projectName);
        // const githubUrl = await getGithubUrl();
        // 'https://github.com/ant-design/ant-design-pro'
        const githubUrl = `https://gitee.com/ant-design/ant-design-pro`
        // git clone xxx --depth=1
        const gitArgs = [`clone`, githubUrl, `--depth=1`, projectName];
        // execa
        await exec(`git`,gitArgs)
        // 修改package.json copy readme 判断是否选择了ts
        // 清楚一些无用的文件
    
    // app和plugin都是读取 templates 模版文件
    class Generator extends BasicGenerator {
      // 创建用户交互 自动将该函数列入到所在优先级队列中
      prompting() {
        const prompts = [
            name: "isTypeScript",
            type: "confirm",
            message: "Do you want to use typescript?",
            default: false,
            name: "reactFeatures",
            message: "What functionality do you want to enable?",
            type: "checkbox",
            choices: [
              { name: "antd", value: "antd" },
              { name: "dva", value: "dva" },
              { name: "code splitting", value: "dynamicImport" },
              { name: "dll", value: "dll" },
              { name: "internationalization", value: "locale" },
        return this.prompt(prompts).then((props) => {
          this.prompts = props;
      writing() {
        // 写入文件
        this.writeFiles({
          context: {
            name: this.name,
            ...this.prompts,
          // 根据用户的选择过滤一些文件
          filterFiles: f => {
            const { isTypeScript, reactFeatures } = this.prompts;
            if (isTypeScript) {
              if (f.endsWith('.js')) return false;
              if (!reactFeatures.includes('dva')) {
                if (f.startsWith('src/models') || f === 'src/app.ts') return false;
              if (!reactFeatures.includes('locale')) {
                if (f.startsWith('src/locales') || f.includes('umi-plugin-locale')) return false;
            } else {
              if (this.isTsFile(f)) return false;
              if (!reactFeatures.includes('dva')) {
                if (f.startsWith('src/models') || f === 'src/app.js') return false;
              if (!reactFeatures.includes('locale')) {
                if (f.startsWith('src/locales') || f.includes('umi-plugin-locale')) return false;
            return true;
    

    7. create-umi-app

    1. 初始化项目

    mkdir myapp && cd myapp 
    npx @umijs/create-umi-app 
    

    2. 简单实现

  • 项目初始化
  • // create-umi-app 现在在umi的仓库下 使用 monorepo 管理
    lerna init
    // 创建子项目
    lerna create @i-box/create-ibox-umi-app
    // package.json
    "workspaces": [
      "packages/*"
    // lerna.json
    "useWorkspaces": true,
    // 安装依赖
    yarn workspace @i-box/create-ibox-umi-app add yargs-parser yeoman-generator yeoman-environment glob
    // create-ibox-umi-app 项目中
    // 增加bin 和 files
     "bin": {
      "create-ibox-umi-app": "bin/create-umi-app.js"
    "files": [
      "lib",
      "bin",
      "templates"
    
  • create-umi-app.js
  • #!/usr/bin/env node
    require('../lib/cli');
    
    // 项目是使用ts写的 源码是在src下的
    const args = require('yargs-parser')(process.argv.slice(2));
    // const args = yParser(process.argv.slice(2))
    const yeoman = require("yeoman-environment");
    // This generator requires an environment.
    const env = yeoman.createEnv([], {
      cwd: process.cwd(),
    require('./').default({
      cwd: process.cwd(),
      args,
    // index.js
    const AppGenerator = require('./AppGenerator')
    module.exports = async({cwd, args}) =>  {
      const generator = new AppGenerator({
        args,
      await generator.run();
    
  • AppGenerator
  • // import { Generator } from '@umijs/utils';
    // 就是拷贝 template文件
    class AppGenerator extends Generator {
      async writing() {
        this.copyDirectory({
          context: {
            version: require('../package').version,
            conventionRoutes: this.args.conventionRoutes,
          path: join(__dirname, '../templates'),
          target: this.cwd,
    复制代码
  • 19小时前
    私信
  •