最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 一文搞懂脚手架

    正文概述 掘金(dream_99517)   2021-07-19   565

    1. 脚手架对比

    脚手架命令
    vue-cli2vue init webpack hellovue-cli4(5)vue create hello-worldcranpx create-react-app my-appviteyarn create vite

    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')
    
    1. 注册命令
    // 在package.json文件中增加bin 
    "bin": {
      "vue-init": "bin/vue-init",
    }
    
    1. 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);
          },
          done
        );
      })
    

    一文搞懂脚手架

    // 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();
    })
    

    一文搞懂脚手架

    1. 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)
          })
      },
    }
    
    1. 处理缓存
    // 我们通过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 简单实现

    1. 初始化项目
    // 我们只是简单实现 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");
    
    1. 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 .开头的文件会出问题
    }
    
    1. 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.2
    ✨  Done 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 简单实现

    1. 项目初始化
    // 使用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/*"
    ],
    
    1. 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",
    
    1. create-ibox-react-app
    // 主要实现这个包
    
    1. cra-template
    // 这是一个模板文件 我们可以直接拷贝 cra仓库中的代码
    template目录和template.json文件
    // 我们需要在package.json中指定files
    "files": [
      "template",
      "template.json"
    ],
    
    1. react-script
    // 这是一个比较核心的包 和vue中的vue-cli-service类似
    // webpack最佳实践 但是好像不支持插件系统
    // react-app-rewired是如何修改webpack配置的
    

    4.5 create-ibox-react-app

    1. 项目初始化
    // 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
    
    1. index.js
    #!/usr/bin/env node
    
    const { init } = require("./createReactApp");
    init();
    
    1. 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
      );
    }
    
    1. 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);
    } 
    
    1. run
    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);
      });
    }
    
    1. 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

    1. 初始化项目
    // 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"
    },
    
    1. 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-world
    ✨  Creating 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. 运行编辑的钩子hooks
     ⚓  Running completion hooks...
     
    // 8.生成README.md文件
     ?  Generating README.md...
     
    // 9. get start
    ?  Successfully created project hello-world. 
       
    ?  Get started with the following commands:
    
    

    一文搞懂脚手架

    3. 简单实现

    1. 初始化项目
    // vue-cli4现在是基于插件系统的
    lerna init
    // 执行yarn安装依赖
    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

    1. 初始化 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) {}
    }
    
    1. 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
      }
    }
    

    一文搞懂脚手架

    1. 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:'
        }
      ]
    }
    

    一文搞懂脚手架

    1. 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, {
          pkg,
          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)}.`);
      }
    
    1. 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
    }
    
    1. 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; // 根选项
      }
    }
    
    1. 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);
    }
    
    1. 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)
    }
    
    1. 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)
      }
    }
    
    1. 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];
          if (
            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);
      }
    }
    
    1. 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 }
          )
        }
      })
    }
    
    1. 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 打包
    
    1. 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)
    }
    
    1. run
    async run() {
      // 初始化 加载env load用户配置(vue.config.js) 应用插件
      await this.init(mode);
      // 执行对应的命令 server中注册命令
      let command = this.commands[name];
      const { fn } = command;
      return fn(args, rawArgv)
    }
    
    1. init
    // 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()
    }
    
    1. 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. 插件

    1. 注册命令
    program.command("add <plugin> [pluginOptions]").action((plugin) => {
      require("../lib/add")(plugin, minimist(process.argv.slice(3)));
    });
    
    1. add
    // 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)
    }
    
    1. api
    // GeneratorAPI 给我们提供一些api
    // https://github.com/vuejs/vue-cli/blob/dev/packages/%40vue/cli/lib/GeneratorAPI.js
    // 插件一般是用 修改package.json文件 增加命令(修改scripts 增加依赖项) 动态注入import render渲染 模版文件等
    
    1. 实现一个简单的插件
    // 将官方提供的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") {
      // }
    };
    

    起源地下载网 » 一文搞懂脚手架

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元