vue-cli插件安装过程源码解析

728 阅读5分钟

什么是vue-cli脚手架

vue-cli是Vue官方提供的一个脚手架,他能够帮助开发者快速搭建一个基于vue框架的前端项目。
开发者通过npm install -g @vue/cli可以将vue-cli安装到本地,然后通过vue create <projectName>去初始化一个前端项目,该项目基于webpack进行构建,并可以手动选择是否在项目中安装vuex,vueRouter,typeScript,esLint...等配置。

什么是vue-cli插件

上面说到vue-cli可以快速帮助我们去构建一个前端项目,但是他只能去默认安装一些公共的vue依赖包进去,对于定制化是不足的。在日常开发中,每个团队都会沉淀一些公用的业务代码。当我们初始化项目时,如果希望将这些业务代码自动安装的新项目去时,vue-cli默认是做不到。
这是vue-cli插件就可以解决这个痛点,vue-cli可以通过vue add,vue invoke命令去通过安装插件的形式,将我们的自定义代码安装到我们的前端工程中去。

如何开发vue-cli插件

简单来说,就是我们需要暴露一个函数去给vue-cli调用,当我们执行vue add命令时候。vue-cli就会去调用指定路径(generator/index)文件。vue-cli在调用的过程中,会传入一个GeneratorAPI对象实例,这个对象实例拥有很多方法(如renderextendPackage,injectImports)等,我们在函数中可以调用该实例的方法对前端项目就行修改。
vue-cli插件开发指南详细的说明了如何去开发一个vue-cli插件,不熟悉的同学可以去看一下,本文由于是做源码解析,对如何开发不多赘述。

案例展示

module.exports = (api) => {
  // 渲染template模板
  api.render('./template');
  // 入口文件插入两条Import语句
  api.injectImports(api.entryFile, [
    'import \'./api\';',
    'import \'./plugins/axios\';',
  ]);
  // 修改依赖包
  api.extendPackage({
    dependencies: {
      axios: '^0.19.2',
      'vue-axios': '^2.1.5',
    },
    // 修改vue.config.js配置
    vue: {
      devServer: {
        proxy: {
          '^/api': {
            target: 'https://api.example.com',
            ws: true,
            changeOrigin: true,
            pathRewrite: {
              '^/api': '',
            },
          },
        },
      },
    },
  });
};

当我们执行vue add axios命令(私库),就会执行以下操作

  • 渲染template下的文件到我们执行vue add的项目下
  • 在项目的入口文件插入两条import语句,引入./api,./plugins/axios
  • 修改package.json添加axios/vue-axios依赖
  • 修改vue.config.js,将devServer配置信息写入

前置的一些知识点

node的process.cwd()返回的是当前执行上下文,也就是我们需要安装插件的项目路径。插件中如果访问相对路径就是访问的插件包里的文件。如果修改项目,则一版使用path.resolve(process.cwd(), path)来访问项目路径。

文件路径

上面介绍了什么是vue-cli插件,以及为什么需要vue-cli插件。但当我们执行vue add <packageName>的时候,具体做了什么呢?
如果你已经将vue-cli安装到了本地,不妨去你本地的node仓库里去查看一下vue-cli的源代码。可以在命令行工具中执行where vue,就可以看到具体的安装目录了

image.png

vue.cmd是一个bin命令软链,具体的包是放在node_modules下

image.png

使用编辑器打开该项目,通过package.json查看包的具体信息就会发现,他的bin指向了入口文件bin/vue.js

image.png

源码解读部分

带着这个案例的操作,我们进入源码阅读环境。上面已经说得到了源码的路径,可以直接进入@vue/cli目录下,查看package.json,阅读包信息

image.png

可以看到包的命令放在bin/vue.js文件下,接着去阅读vue.js文件。可以看到vue-cli的所有命令都在这个文件里面定义,他是依赖commander这个包来进行命令的创建,commander文档。因为我们这里只用到了add命令,所以只需要看这里

image.png add接收一个必填项<plugin>也就是插件名称,和一个可选项[pluginOptions]也就是插件配置。可以从description看到这个命令的介绍:install a plugin and invoke its generator in an already created project(安装一个插件并在已经创建的项目中调用它的generator)。通过action函数可以发现,这个命令会去执行../lib/add这个文件下的代码。

/**
 *
 * @param {*} pluginToAdd 插件名称
 * @param {*} options 插件配置参数
 * @param {*} context 执行上下文环境(默认就是我们执行命令的项目路径)
 * @returns
 */

async function add (pluginToAdd, options = {}, context = process.cwd()) {
  //判断环境 忽略
  if (!(await confirmIfGitDirty(context))) {
    return
  }

  // for `vue add` command in 3.x projects
  // 3.x版本兼容 已经router/vuex插件安装 自定义插件可忽略
  const servicePkg = loadModule('@vue/cli-service/package.json', context)
  if (servicePkg && semver.satisfies(servicePkg.version, '3.x')) {
    // special internal "plugins"
    if (/^(@vue\/)?router$/.test(pluginToAdd)) {
      return addRouter(context)
    }
    if (/^(@vue\/)?vuex$/.test(pluginToAdd)) {
      return addVuex(context)
    }
  }

  const pluginRe = /^(@?[^@]+)(?:@(.+))?$/
  //拿到pluginName/pluginVersion等参数
  const [
    // eslint-disable-next-line
    _skip,
    pluginName,
    pluginVersion
  ] = pluginToAdd.match(pluginRe)
  const packageName = resolvePluginId(pluginName)

  log()
  log(`📦  Installing ${chalk.cyan(packageName)}...`)
  log()
  // 此处实例化PackageManager对象
  const pm = new PackageManager({ context })
  // 如果指定了版本plugin@verison
  if (pluginVersion) {
    await pm.add(`${packageName}@${pluginVersion}`)
  // 如果是vue-cli提供的插件
  } else if (isOfficialPlugin(packageName)) {
    const { latestMinor } = await getVersions()
    await pm.add(`${packageName}@~${latestMinor}`)
  // 安装自定义插件 我们就是走这一条判断去安装
  } else {
    await pm.add(packageName, { tilde: true })
  }

  log(`${chalk.green('✔')}  Successfully installed plugin: ${chalk.cyan(packageName)}`)
  log()
  // 去获取generator路径
  const generatorPath = resolveModule(`${packageName}/generator`, context)
  if (generatorPath) {
    invoke(pluginName, options, context)
  } else {
    log(`Plugin ${packageName} does not have a generator to invoke`)
  }
}

add函数主要依赖了PackageManager这个对象,PackageManager主要是对依赖的安装,去查看PackageManager会发现他会对当前项目的安装工具进行判断,支持pnpm,npm,yarn等。执行pm.add操作就回去拉取我们插件的代码包。

const PACKAGE_MANAGER_CONFIG = {
  npm: {
    install: ['install', '--loglevel', 'error'],
    add: ['install', '--loglevel', 'error'],
    upgrade: ['update', '--loglevel', 'error'],
    remove: ['uninstall', '--loglevel', 'error']
  },
  pnpm: hasPnpmVersionOrLater('4.0.0') ? PACKAGE_MANAGER_PNPM4_CONFIG : PACKAGE_MANAGER_PNPM3_CONFIG,
  yarn: {
    install: [],
    add: ['add'],
    upgrade: ['upgrade'],
    remove: ['remove']
  }
}
async runCommand (command, args) {
    const prevNodeEnv = process.env.NODE_ENV
    // In the use case of Vue CLI, when installing dependencies,
    // the `NODE_ENV` environment variable does no good;
    // it only confuses users by skipping dev deps (when set to `production`).
    delete process.env.NODE_ENV

    await this.setRegistryEnvs()
    await executeCommand(
      // this.bin只的是当前的安装工具
      this.bin,
      [
        ...PACKAGE_MANAGER_CONFIG[this.bin][command],
        ...(args || [])
      ],
      this.context
    )

    if (prevNodeEnv) {
      process.env.NODE_ENV = prevNodeEnv
    }
}
async add (packageName, {
    tilde = false,
    dev = true
  } = {}) {
    const args = dev ? ['-D'] : []
    if (tilde) {
      if (this.bin === 'yarn') {
        args.push('--tilde')
      } else {
        process.env.npm_config_save_prefix = '~'
      }
    }

    if (this.needsPeerDepsFix) {
      args.push('--legacy-peer-deps')
    }
    // 运行脚本 安装依赖
    return await this.runCommand('add', [packageName, ...args])
  }

执行完以上操作,我们的插件代码已经被安装到了node_modules下,并且package.json也会记录包信息。