找回密码
 会员注册
查看: 33|回复: 0

深入了解vue-cli

[复制链接]

8

主题

0

回帖

25

积分

新手上路

积分
25
发表于 2024-9-20 02:46:00 | 显示全部楼层 |阅读模式
转转内部脚手架的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源码的一些心得。若有不足之处,欢迎在评论中指出。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2024-12-25 13:58 , Processed in 0.492713 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表