vue-cli create执行流程

无论是vue还是react项目,官方都提供了一个脚手架用来创建项目.但是我想要一个可以创建任何类型的项目.官方脚手架创建的项目启动页面都是显示一个图标加一些链接,这个模版我也想自定义.

但又对脚手架的实现知之甚少,在网上也没找到具体一些的vue-cli源码解析,所以只能自己努努力先从 vue-cli create 命令看起

主要使用到的第三方库 commander inquirer execa ejs 内置模块 path fs

  1. vue-cli是通过 lerna 管理多包项目.在packages文件夹下 cli 包,找到其下的 package.json中bin 指向的文件(bin/vue.js),create 命令在此注册.
js 复制代码
program
  .command('create <app-name>')
  .description('create a new project powered by vue-cli-service')
  .option('-m, --packageManager <command>', 'Use specified npm client when installing
  //...
  .option('-f, --force', 'Overwrite target directory if it exists')
  .action((name, options) => {
    //...
    require('../lib/create')(name, options) //进入这个文件
  })

这里会判断文件名是否合法,不合法退出并弹出提示.接着检查工作目录下是否已经存在相同名字的文件,如果有弹出提示

在这里选择 overwrite 或在create命令后加了 -f ,会直接删除之前的文件.

之后利用 creator 类创建一个对象,并调用其 create 方法.先看 getPromptModules返回了什么.

js 复制代码
  const creator = new Creator(name, targetDir, getPromptModules())
  await creator.create(options)

这里不就是自定义模板时选择的 插件

js 复制代码
exports.getPromptModules = () => {
  return [
    'vueVersion',
    'babel',
    'typescript',
    'pwa',
    'router',
    'vuex',
    'cssPreprocessors',
    'linter',
    'unit',
    'e2e'
  ].map(file => require(`../promptModules/${file}`))
}

拿其中的路由举例.他会导出一个函数,在函数内部他会调用传入参数的方法.

  • injectFeature 注入功能,让我们记住选了这个插件
  • injectPrompt 选择完路由之后,还要选择模式,需要这个函数,所以上面有些文件没有这个函数
  • onPromptComplete 选择完之后的回调

现在可以看creator的构造函数了.

  • presetPrompt 选择预设的交互,选项包括用户自己的预设(预设信息存在一个名为 .vuerc 的文件),脚手架内置的两个预设和自定义选项
  • featurePrompt 选择插件的交互(现在里面的插件列表还是空的)
  • outroPrompts 三个交互,这是最后询问我们 Where do you prefer placing config...,是否将其设为预设,如果保存还要输入名字
js 复制代码
  constructor (name, context, promptModules) {
    super()

    this.name = name //项目名
    this.context = process.env.VUE_CLI_CONTEXT = context //路径
    const { presetPrompt, featurePrompt } = this.resolveIntroPrompts()

    this.presetPrompt = presetPrompt
    this.featurePrompt = featurePrompt
    this.outroPrompts = this.resolveOutroPrompts()
    this.injectedPrompts = []
    this.promptCompleteCbs = []
    this.afterInvokeCbs = []
    this.afterAnyInvokeCbs = []

    this.run = this.run.bind(this)

    const promptAPI = new PromptModuleAPI(this)
    promptModules.forEach(m => m(promptAPI))
  }

看上面最后两行,这里又new了一个 PromptModuleAPI 对象,它里面的函数跟promptModules中调用的函数相同.

循环之后
featurePrompt 里的插件列表就有了
injectedPrompts 就是选择完插件还需要选择配置的交互也收集起来了
promptCompleteCbs 表示插件相关的交互完成之后的回调也收集起来.

紧接着执行creator对象的 create 函数,再执行其中的 promptAndResolvePreset

在这个函数里利用 inquirer库来做交互. resolveFinalPrompts 函数会返回一个数组,包含创建项目的一系列交互.例如选择预设,如果是自定义下个问题就是选择插件.

js 复制代码
 answers = await inquirer.prompt(this.resolveFinalPrompts())

选择完这些就可以进入下一步,下一步当然解析用户的选项,先声明一个变量 preset

js 复制代码
 this.promptCompleteCbs.forEach(cb => cb(answers, preset))

执行之前的回调数组,会根据用户的选项给 preset 添加信息,也就是.

js 复制代码
preset : {
  useConfigFiles: true,
  plugins: {
    "@vue/cli-plugin-babel": {
    },
    "@vue/cli-plugin-router": {
      historyMode: false,
    },
    "@vue/cli-plugin-vuex": {
    },
    "@vue/cli-plugin-eslint": {
      config: "base",
      lintOn: [
        "save",
      ],
    },
  },
  vueVersion: "3",
}
 //最后还要加上下面这个插件
 // inject core service
    preset.plugins['@vue/cli-service'] = Object.assign({
      projectName: name
    }, preset)

接着又会new一个PackageManager对象,这个暂时不看

js 复制代码
 const pm = new PackageManager({ context, forcePackageManager: packageManager })

现在需要创建package.josn文件了,里面的内容 pkg 初始化是

js 复制代码
  const pkg = {
      name,
      version: '0.1.0',
      private: true,
      devDependencies: {},
      ...resolvePkg(context) //如果已经存在package.json,也就是之前选择merge的情况下,会合并之前的package.json和现在用户选择的插件等信息.
    }

现在根据之前的preset变量可以给pkg添加依赖,添加之后的结果是

js 复制代码
{
  name: "projectname",
  version: "0.1.0",
  private: true,
  devDependencies: {
    "@vue/cli-plugin-babel": "~5.0.0",
    "@vue/cli-plugin-router": "~5.0.0",
    "@vue/cli-plugin-vuex": "~5.0.0",
    "@vue/cli-plugin-eslint": "~5.0.0",
    "@vue/cli-service": "~5.0.0",
  },
}

根据pkg可以生成我们的package.json文件,这里是使用 writeFileTree 函数,其中使用node的fs模块就可以生成文件了.

js 复制代码
 // write package.json
    await writeFileTree(context, {
      'package.json': JSON.stringify(pkg, null, 2)
    })

现在需要下载依赖,调用上面的PackageManager对象 pm 的install进行依赖下载 其实也就是利用第三方库execa开启子进程执行命令npm install,这里vue默认是pnpm.

依赖下载完毕之后,需要生成项目目录文件.进入下一个最为核心的环节 Generator

js 复制代码
    const plugins = await this.resolvePlugins(preset.plugins, pkg)
    const generator = new Generator(context, {
      pkg,
      plugins,
      afterInvokeCbs,
      afterAnyInvokeCbs
    })
    await generator.generate({
      extractConfigFiles: preset.useConfigFiles
    })

第一步:resolvePlugins,接收两个参数,一个是选择的预设插件数组,第二个就是上面生成package.josn的数据了.这个函数的作用是把插件的数据格式对象变成一个数组, { id: options } => [{ id, apply, options }],多了一个apply,他是从什么地方来呢?

js 复制代码
 const apply = loadModule(`${id}/generator`, this.context) || (() => {})

apply就是拿到下载的依赖node_modules/@vue下模块导出的方法,这个以后要用先跟对应的插件存在一起.

第二步new一个Generator类,构造函数里传入了 pkg,plugins 等.执行generator上的generate方法.在这个函数里执行initPlugins初始化插件信息.

js 复制代码
initPlugins(){
 //...
 // apply generators from plugins
    for (const plugin of this.plugins) {
      const { id, apply, options } = plugin
      const api = new GeneratorAPI(id, this, options, rootOptions)
      await apply(api, options, rootOptions, invoking)
//...
    }
    }

这里又new了一个GeneratorAPI对象,将他作为参数传递给之前保存的 apply 方法

拿其中的cli-service举个例子,执行apply函数其中有调用api,也就是GeneratorAPI的实例中的render方法,再执行其中的 _injectFileMiddleware 注册middleware.

再执行extendPackage,他的作用是扩展package.json中相应的配置,执行完毕之后退出插件初始化完毕.

接着 resolveFiles 也就是循环执行middleware会根据插件名称找到node_modules对应插件下的template内容保存下来

writeFileTree 会渲染新的package.json和模版文件,最后再次下载依赖.

现在就可以知道模版不是从git仓库拿到的,而是在下载的插件提供的,并且还会拓展package.json的内容

最后的最后会生成readme文件,初始化git,打印log.

总结

首先判断项目名是否合法,是否已经存在同名文件 --> 在.vuerc文件获取用户预设和脚手架自带两个预设以及自定义预设供用户选择 --> 这里选择自定义 --> 出现选择插件的交互,选择之后还有与插件配置相关的交互,全部选择完成之后收集到用户数据answer --> 拿到answer生成package.json --> 利用execa库执行下载依赖的命令 --> 根据选择的插件名找到下载的node_modules中对应的插件,执行其下的index.js文件会添加package.josn中的相关内容还会保存其中的template文件 --> 生成新的package.son文件和模版文件 --> 再次下载依赖 --> 最后会生成readme文件,初始化git,打印log.

相关推荐
zqx_716 分钟前
随记 前端框架React的初步认识
前端·react.js·前端框架
惜.己33 分钟前
javaScript基础(8个案例+代码+效果图)
开发语言·前端·javascript·vscode·css3·html5
什么鬼昵称1 小时前
Pikachu-csrf-CSRF(get)
前端·csrf
长天一色1 小时前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
NiNg_1_2342 小时前
npm、yarn、pnpm之间的区别
前端·npm·node.js
秋殇与星河2 小时前
CSS总结
前端·css
BigYe程普2 小时前
我开发了一个出海全栈SaaS工具,还写了一套全栈开发教程
开发语言·前端·chrome·chatgpt·reactjs·个人开发
余生H2 小时前
前端的全栈混合之路Meteor篇:关于前后端分离及与各框架的对比
前端·javascript·node.js·全栈
花花鱼2 小时前
@antv/x6 导出图片下载,或者导出图片为base64由后端去处理。
vue.js
程序员-珍2 小时前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发