|
转转内部脚手架的Webpack部分,是基于@vue/cli进行二次封装的。选择二次封装而不是自己搞一套Webpack配置,是为了减少维护的成本。比如最近新出的Vue2.7版本,如果自行维护Webpack配置,可能还要对vue-loader进行一些调整。遇到重难点问题,还需要去看@vue/cli的源码作为参考,重新实现一遍它里面的逻辑。在看任何开源库的源码之前,必须先了解它有哪些功能,这样才能针对性地分模块阅读源码。根据@vue/cli的文档,它大体上分为两块功能:项目模板生成npm包@vue/cli是这个功能的入口包,提供了vuecreate命令生成项目模板。开发阶段功能这个功能对应npm包@vue/cli-service,包含了一些开发时实用的命令。vue-cli-serviceserve用于启动开发服务器。vue-cli-servicelint命令对代码进行lint。vue-cli-serviceinspect查看被所有cli插件修改后的Webpack配置。文章将分为两个部分,第一个部分是对@vue/cliplugin和preset的介绍,第二个部分是@vue/cli的关键部分源码实现,包括插件系统实现,Webpack配置处理等内容。plugin插件cli插件的组成@vue/cli设计了插件系统,一个插件是一个npm包,总共由generator(模板)和service(服务)两个部分组成。一个简单的插件目录是这样的:.├── generator.js├── index.js├── package.json├── pnpm-lock.yamlgenerator.js文件对应上文的generator部分,负责说明该插件希望对生成的模板做出哪些改动。index.js文件对应上文的service部分,可以为vue-cli-service这个主命令注册新的副命令,或者对@vue/cli自带的一些命令做出修改。cli生成项目模板流程@vue/cli在生成项目时,会在目标目录下新建一个package.json文件,并在devDependencies中列出所有使用到的cli插件。此时会执行第一次npminstall,来安装cli插件,@vue/cli会调用这些插件的generator.js,得到最终输出到目标目录的项目结构,并写入硬盘。由于cli插件会向package.json中声明一些新的依赖(比如vue、vue-router),所以此时@vue/cli会执行第二次npminstall,确保这些依赖被全部安装。此时项目已经基本上创建完成,@vue/cli调用每个插件tempalte部分注册的onCreateComplete钩子函数,执行一些项目创建完成后的逻辑。创建流程到此就结束了。generator.js-generator部分接下来简单介绍generator.js该怎么写,它的签名如下:/** * @type {import('@vue/cli').GeneratorPlugin} */module.exports = function generator(api, pluginOptions, preset) { // 这里写插件的代码}generator.js文件的逻辑很简单,只需要导出一个函数即可。@vue/cli为generator.js提供了三个参数。api是@vue/cli内部一个名为GeneratorAPI的类的实例,提供了各种操作模板的方法。pluginOptions@vue/cli有一个预设的概念,预设可以指定每个cli插件的选项。详见下文preset部分。preset预设对象,详见下文preset部分。其中api这个参数最为关键。它是一个对象,常用的属性和方法有:api.extendPackage()对package.json文件进行扩展和修改。api.render()将一个文件夹render到创建项目的目录。可以简单地理解为,将一个文件夹复制到目标文件夹中。与复制不同的是,render的对象支持ejs语法,可以在里面写部分JS逻辑。比如希望根据pluginOptions选项,来render不同的内容。index.js-service部分与generator.js类似,index.js同样导出一个函数。/** * @type {import('@vue/cli-service').ServicePlugin} */module.exports = function service(api, projectOptions) {}api@vue/cli内部名为ServiceAPI的类的实例。需要注意与GeneratorAPI进行区分。projectOptions即vue.config.js文件中的选项。api参数常用的方法有:api.configureWepback使用webpack-merge修改Webpack配置api.chainConfig使用webpack-chain修改Webpack配置api.registerCommand为vue-cli-service注册新的命令。preset预设在使用vuecreate命令创建项目时,需要使用者做出几个选择,包含Vue版本、是否使用TS和Babel等选项。这些选项会被合并成一个对象,@vue/cli将这个对象称为preset。如果你曾经使用@vue/cli创建过项目,并选择将选项保存为一个预设,那么可以通过cat~/.vuerc命令来找到保存的配置。这个配置一般长这样:{ // 是否使用淘宝源 "useTaobaoRegistry": false, // 使用cli创建项目时,使用哪个包管理器安装依赖。 "packageManager": "npm", // 被保存的 cli 预设 "presets": { "vue3-preset": { "useConfigFiles": true, // 创建模板时,使用哪些cli插件。 "plugins": { //key为插件的名称,value是插件的配置。 "@vue/cli-plugin-babel": {}, "@vue/cli-plugin-typescript": { "classComponent": false, "useTsWithBabel": true }, "@vue/cli-plugin-router": { "historyMode": false }, "@vue/cli-plugin-vuex": {}, "@vue/cli-plugin-eslint": { "config": "prettier", "lintOn": [ "save" ] } }, // 新项目使用vue2还是vue3 "vueVersion": "3", // 新项目使用什么css预处理器 "cssPreprocessor": "less" } }, // @vue/cli 的最新版本 "latestVersion": "5.0.8", // 上次检查 @vue/cli 最新版本的时间 "lastChecked": 1657541617415}如果~/.vuerc文件中保存了历史预设,下次使用vuecreate时,就可以选择这些预设,跳过一堆问题的选择。如果希望对预设有更深入的定制,可以仿照.vuerc文件的格式,将预设的内容写在一个json文件中。比如这样一份文件:{ "useConfigFiles": true, "plugins": { // 为了自定义 @vue/cli 而编写的插件 "@zz-common/vue-cli-plugin-zz": { "version": "^0.0.7" }, "@vue/cli-plugin-babel": {}, "@vue/cli-plugin-typescript": { "classComponent": false, "useTsWithBabel": true }, "@vue/cli-plugin-router": { "historyMode": true }, "@vue/cli-plugin-vuex": {}, "@vue/cli-plugin-eslint": { "config": "prettier", "lintOn": ["save"] } }, "vueVersion": "2", "cssPreprocessor": "dart-sass"}假设这个文件的名字是vueCliPreset.json,那么可以通过vuecreate
--preset./vueCliPreset.json命令,来使用这个预设文件,创建对应的项目模板。@vue/cli运行流程仓库概览vue-cli是一个基于yarn的monorepo,核心包都位于packages/@vue文件夹下,包含:@vue/cli核心包@vue/cli-service核心包@vue/cli-plugin-babel插件@vue/cli-plugin-typescript插件@vue/cli-plugin-vuex插件@vue/cli-plugin-router插件其中,@vue/cli对外暴露了vue命令,并统筹各个cli插件的运作,可以将其称为入口包。它负责命令行交互、预设存取、插件统筹等工作。@vue/cli包含这些功能:vuecreate创建一个模板项目vueinvoke调用某个cli插件的generator部分vueadd添加一个cli插件vueupgrade升级一个cli插件vueinspect查看当前项目的Webpack配置@vue/cli-service包含这些功能:vue-cli-serviceservevue-cli-servicebuild由于篇幅的原因,这里仅介绍vuecreate和vue-cli-servicebuild/serve这两个核心功能。vuecreate通常使用vuecreate
来创建一个新的项目。Creator类vue/cli包中使用commander这个包,声明了create命令和对应的参数。项目创建由名为Creator的class负责,Creator实例中的create方法,接收了commander传递的所有命令行选项,进行项目的创建。prompt:在上文提到,vuecreate支持使用一个json文件作为预设。如果使用json文件预设,那么create命令的prompt询问阶段会被跳过,否则会问使用者一些问题,来生成preset对象。包管理器:@vue/cli支持使用命令行参数指定包管理器。如果没有指定,则会依次降级到.vuerc文件、yarn、pnpm、npm。另外由于创建项目的过程中,与包管理器相关的命令调用非常多,且需要抹平不同包管理器之间的区别,所以源码中使用PackageManager这个类来封装包管理器操作。这是值得学习的一点。第一次安装依赖:在一次项目创建的过程中,需要使用各种官方和非官方的插件,所以cli会首先根据preset对象中指定的插件名,来创建package.json文件,并使用PackageManager.install方法,来进行第一次安装。调用cli插件的generator部分:在第一次安装完成后,所有的cli插件都被安装了。@vue/cli此时会调用所有插件的generator部分,生成最终需要输出到硬盘的文件内容。调用hook函数:cli会调用插件注册的一些函数,在项目创建完成后运行。完成创建在这个流程中,最值得关注的是@vue/cli与cli插件的交互部分。在一个拥有插件系统的设计中,有插件容器和插件两个部分。容器需要将上下文内容和用户选项,提供给插件,让插件实现它的功能。所以于上下文和用户选项的整合尤为关键。以插件的generator部分为例,@vue/cli使用单独的类GeneratorAPI,为插件提供render文件夹、扩展package.json等各种实用的功能。@vue/cli使用files对象来记录最终输出到硬盘的文件内容,key是文件路径,value是文件内容。GeneratorAPI.render作用是将插件指定的文件夹,render到最终生成的项目中去。这个API实质是在读取render方法指定的文件夹,使用ejs模板引擎处理源文件内容,并将处理后的内容记录在files对象中。最后只需要根据files对象,将文件一一写入硬盘即可。除了files对象,cli中还有一个pkg对象来记录package.json中的内容,当插件调用GeneratorAPI.extendPackage时,实际上是在修改pkg对象。之所以pkg不在files对象中,是因为package.json与其它文件差异较大,cli插件需要对它有更细粒度的操作。总结下插件的交互部分,一共有三个关键点:合适的数据结构在@vue/cli是两个对象,也可视情况采用SetMapWeakMapWeakSet等。操作数据结构的接口这个情景下对应的是GeneratorAPI,其中的方法都是在用不同的方式操作数据结构。事实上这里有两种选择,一种是直接将files对象暴露给插件,让插件自由发挥。优点是插件的上限更高,可以完成更复杂的功能;缺点是操作不便,files对象的value是字符串,通常需要使用正则或者ast来操作。如果希望使用ast,还需要根据字符串的内容,来选择不同的parser。另一种是对files对象操作的方法进行封装,就像GeneratorAPI这个类一样。这样做的优点是,插件的代码量大大下降,出bug的几率更低,行为更加统一。@vue/cli则同时采用了这两种做法,向插件传递GeneratorAPI实例的同时,也暴露了files对象。hook设计理论上来说,插件容器暴露的hook数量越多,插件的上限就越高。@vue/cli中插件暴露的hook并不多,比如eslint等配置文件的转换、项目创建结束后的hook等。原因之一是创建工程模板这个需求的复杂度不够高,也就不需要过多的hook。良好的插件容器,需要将内部所有关键流程的都暴露给插件,比如配置的合并策略、插件的执行顺序、数据结构的便捷修改方法、原始数据结构等。vue-cli-servicebuild/servebuild与serve的原理是类似的,它们都由@vue/cli-service这个包实现。@vue/cli-service是一个官方的@vue/cli插件,它通过ServiceAPI.registerCommand注册了serve和build命令,处理Webpack相关的操作。这同时体现了插件系统的好处,可以将打包逻辑提取到单独的插件中,不必与@vue/cli的代码放在同一个包中。build的主体逻辑比较简单,加载vue.config.js文件,调用cli插件,得到修改后的Webpack配置,并使用Webpack进行打包。有一个点是,@vue/cli支持modern模式的构建。当modern模式开启时,它会进行两次构建,第一次构建会通过script标签进行模块加载,第二次构建基于浏览器模块系统(type="module"VSnomodule)。@vue/cli的不足之处@vue/cli是一个优秀的脚手架,但仍有一些令人遗憾的设计存在。比如它对JSAPI的支持度较差,配置与vue.config.js文件强绑定。在进行modern模式的打包时,它的内部使用子进程的形式,递归地调用自身来完成功能。这会导致通过JSAPI传入的参数,被vue.config.js文件内容覆盖,造成意料外的行为。vue.config.js不支持ts写法,需要使用类型注释,来获得类型提示。如果希望使用esm格式,需要使用.mjs后缀,且通过环境变量传入vue.config.mjs,来覆盖默认的文件名。另外,插件的service函数部分,返回的Promise没有被await。基于Promise的API都不适合在插件的service部分使用,比如fs.readFilefs.writeFile,需要使用同步版本的API代替。如果这个部分使用cjs代码编写,依赖了一个esm格式的库,那么这个库需要使用import()函数来导入。由于toplevelawait的存在,import()函数是一个异步函数,可能导致一部分cli插件代码,实际上被没有被执行完,但@vue/cli却误认为它已经执行完了,从而产生报错。并且报错的信息通常和Webpack相关,不容易注意到这是一个异步相关的问题。最后尽管文章中提到了@vue/cli的一些设计缺陷,但多少有些吹毛求疵的成分。如果将时间倒回到@vue/cli被创建的时间点,这样一个拥有插件系统采用了最佳实践的同时,仍保留高度自定义Webpack配置的能力的脚手架,给Vue开发者一个方便、快捷启动项目的途径,已经十分优秀且值得借鉴了。本文是笔者在实现公司内部脚手架的Webpack部分时,看@vue/cli源码的一些心得。若有不足之处,欢迎在评论中指出。
|
|