vue-cli3 的快速插件开发

3,371 阅读7分钟

前言

不久前组内有大佬发布了一个 vue-cli3 的 dll 包,作为一个在 vue 项目内摸爬滚打的萌新,是时候该学习点儿新的技术了,于是在闲暇之余,我拷贝了一份代码,同时研究该如何从“零”开始编写一个 dll 包(该部分以 webpack 的 dllPlugin 作为例子)。

了解文档

通篇读完官网的文档,因为知识点比较多,并且没有详细的例子(指“傻瓜式教学式”),因此开发这第三方包的学习成本还是有一些的,特别是 webpack-chain 和 node 的部分知识,这里记录总结了一些关键的点。

插件命名

最开始命名文件夹时我并没有使用 vue-cli-plugin- 作为文件名前缀,结果可想而知,vue invoke 一直提示找不到该包的信息。于是我去看了看源码…… 在 @vue/cli/lib/invoke.js 内,其有一个关键的获取包 Id 的方法 resolvePluginId,该方法在 @vue/cli-shared-utils/lib/pluginResolution.js,源码如下:

exports.resolvePluginId = id => {
  // already full id
  // e.g. vue-cli-plugin-foo, @vue/cli-plugin-foo, @bar/vue-cli-plugin-foo
  if (pluginRE.test(id)) {
    return id
  }
  // scoped short
  // e.g. @vue/foo, @bar/foo
  if (id.charAt(0) === '@') {
    const scopeMatch = id.match(scopeRE)
    if (scopeMatch) {
      const scope = scopeMatch[0]
      const shortId = id.replace(scopeRE, '')
      return `${scope}${scope === '@vue/' ? `` : `vue-`}cli-plugin-${shortId}`
    }
  }
  // default short
  // e.g. foo
  return `vue-cli-plugin-${id}`
}

很明显,使用 vue invoke 时其只会寻找含有 vue-cli-plugin- 作为前缀的包,官网内在文档的最后部分有做对应的说明(这个是后来才看到的),原文如下:

为了让一个 CLI 插件能够被其它开发者使用,你必须遵循 vue-cli-plugin-<name> 的命名约定将其发布到 npm 上。

因此 package.jsonname 字段符合规则即可。

creator 和 service

官网开篇就介绍了两个主要的部分:@vue/cli@vue/cli-service,首先是 @vue/cli 部分,这里介绍了插件的目录结构,因此我们可以根据此来搭一个插件框架:

vue-cli-plugin-xxx
  ├── README.md
  ├── generator
  |  └── index.js
  ├── index.js
  ├── package.json
  ├── prompts
  |  └── index.js
  ├── service
  |  ├── config-file.js
  |  └── regist-command.js
  └── yarn.lock

接下来就详细分析一下各部分的作用。

generator

文档分析

文档中有提到,插件内的 generator 将会在两种场景下被调用:

  • 在一个项目的初始化创建过程中,如果 CLI 插件作为项目创建 preset 的一部分被安装
  • 插件在项目创建好之后通过 vue invoke 独立调用时被安装

由于开发的第三方插件使用场景多数在于更改已安装的项目配置,preset 使用场景不是很多(创建项目时一般还是手动配置,大多数情况不会选择去生成一个 ~/.vuerc),因此这边仅处理使用手动调用 generator 的情况。

触发 generator 的方法有两种:

  • vue invoke
  • vue add

下面就简单介绍一下这两个命令的区别。

vue invoke 指令

此指令的适用情况为已经通过 yarn 或者 npm 将包安装至项目内,此时仅需要调用 vue invoke 即可。

注意:这里的 packageName 为不包含 vue-cli-plugin- 部分的剩余包名,比如:发布的包名为 vue-cli-plugin-xxx,那么此时使用命令即 vue invoke xxx

vue add

此指令的使用情况为项目内还没有安装对应的包,使用方式同 vue invoke

注:如果包的源不对的话,请自己在后面加上包所在的 npm 源地址( --registry )

内容编写

分析了这么多,重点还是 generator 内我们应该写点什么,它影响的是什么。好了,让我们来继续看文档(● ˃̶͈̀ロ˂̶͈́)੭ꠥ⁾⁾

generator 有三个参数,这里就不细赘了,因为这里不关注 preset 的配置,所以对我们来说,有用的部分就只有第一个参数 api。首先我们需要改动的部分便是项目内的 package.json 了,使用方法 extendPackage 即可,例子如下:

// 修改 `package.json` 里的字段
// vue 部分的内容可以不要
module.exports = (api, options, rootOptions) => {
  // 修改 `package.json` 里的字段
  api.extendPackage({
    scripts: {
      test: 'vue-cli-service test'
    },
    vue: {
      pluginOptions: {
        test: {
          // 需要预打包的部分
          vendors: [],
          // 输出文件名
          outputName: 'vendor.dll.js',
          // 输出地址
          outputPath: './public/vendor',
          // 是否调用 cleanWebpackPlugin
          cleanCache: true
        }
      }
    }
  });
}

关于此处添加的 vue 字段在 invoke 后会自动补充至 vue.config.js 或者 package.json 内。

如果你配置了 promots 并且需要该部分的内容,那么可以使用第二个 options 参数去获取配置的内容。(配置 .vuerc 的方法没有尝试,因为解构 + 默认值 + prompts 已经足够了)

如果需要配置模板方面的参考官方源码,感觉配合 prompts 写个模板插件也不错。

友情提示:render() 函数内为你的 template 模板基于当前文件夹所在的路径。

prompts

该部分其实在这个项目内并没有涉及,但还是要提一下。官方文档对于此部分在内建插件有详细的说明(官方插件),第三方插件提到过一点:

这个文件应该导出一个用于 Inquirer.js 的问题的数组。这些被解析的答案对象会作为选项被传递给插件的 generator。

因此,如果需要的情况下,我们得通过数组的形式编写问答。例如:

module.exports = [
  {
    name: 'entry',
    message: "What's the output file's name?",
    type: 'input',
    default: 'vendor'
  }
]

此部分的配置结果会在 generator 部分的第二个参数捕捉到。

service

这部分就是配置的重点了,还是根据官网来吧,官网有提到 3 个命令: chainWebpackconfigureWebpackregisterCommand。直接更改原有项目的配置并不是很好(除非你很有信心),因此我们可以使用 configureWebpack 来合并变更。

这部分总的来说做三件事:

  1. 更改用户的 webpack 配置文件(也就是 vue.config.js)
  2. 向 cli-service 内注册指令
  3. 为注册的指令指定模式

这里从简单的部分开始一一说明吧~(顺序:3、1、2)

指定模式

嗯,这个最简单了,毕竟官网文档内有,理由也就不赘述了,代码如下:

module.exports.defaultModes = {
  <your direct>: <target mode>
}

其中 your direct 部分即先前的 generator 部分注册的 script 脚本内,在 vue-cli-service 后面的那个指,比如,先前写的是 test,那么这里注册的指令也是 direct,mode 就根据实际情况处理即可,一般使用 production 生产模式就没啥问题。

更改用户的配置文件

这里就需要有 webpack-chain 的知识了。先看看 pluginAPI 内有些啥(传送门),可能会用到的一些方法包括:

  • getCwd:获取当前的工作目录
  • resolve:相当于 path.resolve
  • registerCommand:注册指令(有三个参数!)
  • chainWebpack:链式调用 webpack
  • configureWebpack:用于合并 webpack
  • resolveChainableWebpackConfig:用于解析 webpack

此处我们使用 configureWebpack 来更改配置,同时使用 registerCommand 来注册我们的命令。这里刚好对应的就是我们的两个文件 config-fileregist-command.js 了,对应开发即可。

configureWebpack

参考源码的写法,我们可以通过 options 参数获取 invoke 生成的 vue.config.js 里面的 pluginOptions 字段内的对应配置内容,配合 api.configureWebpack 来注入我们的 webpack 配置。粗略的写法为:

api.configureWebpack(config => {
  // 增一个 plugins
  config.plugins.push(
    /* plugin 配置 */
  )
});
registerCommand

查看源码的写法,发现参数有三个,分别是:

  • api: pluginAPI 实例
  • options: 用来添加配置说明
  • fn: 回调函数,用于触发执行的内容(比如运行一个 webpack 配置)

官方的写法如下,我们可以据此模仿

api.registerCommand(
  "test",
  {
    description: "此为指令的说明", // 指令意义
    usage: "vue-cli-service test", // 命令怎么用
    options: {
      /* 参数说明 */
    }
  },
  async args => {
    /* 可以写个 chain-webpack 然后调用,或者干点其它的 */
  }
);

部分问题总结

1. demo 内的 dll 打包位置问题

使用默认的打包位置: public/vendor 会产生一个问题,就是 build 文件内会包含此 vendor 文件夹,我们注入的一些配置不生效。

解决方法:将默认的 public/vendor 打包位置改改,只要不是生成在 public 文件夹内的都没问题,可看 vue-cli 官网的 public 部分的解释。

2. webpack 函数

目前第二个回调参数的使用场景不是很明确(其实不用回调也可以,外层关闭对应的 log 即可)

3. configureWebpack 只能用 push?

我尝试过自己新创建一个 webpack-chain 然后返回(源码上好像会对返回值进行判断,如果有那么会调用 merge 方法),按理来说返回一个 config.toConfig() 应该 没问题,但是行不通,目前仍然用的是 push 方法

demo

传送门

参考文档

--by kazehaiya