侧边栏壁纸
博主头像
前端自习室博主等级

折腾是进步的阶梯

  • 累计撰写 30 篇文章
  • 累计创建 0 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

从vue-cli3迁移到vite谈起(一):ES发展与vite初步使用

lumozx
2022-04-28 / 0 评论 / 0 点赞 / 33 阅读 / 103934 字

注:本文使用的vite版本为2.9.14

模块化一直驱动着前端的发展,而前端的发展,也产生了一系列的模块化标准和规范,改革了以模块化作为底层的构建工具,从而加速了前端的发展。

一、模块化的刀耕火种到ESM

在很久很久以前,久到Jquery都算是一个新型框架的年代 ,那个时候受其他语言的影响,同时也为了满足自身的需求,前端也萌生了模块化思想,但受制于javascript能力的弱势,没有统一的标准,只能自己去实现。

总体来说,共三个手段:

  1. 文件划分

  2. 命名空间

  3. 私有作用域

文件划分:

在那个没有太多打包、构建工具的年代,往同一个html引入不同的script标签都是一种中等操作,因此,开发者就产生了一种想法:如果把js文件看做是模块,那么不就可以实现类模块化了吗?

// module1
function test1 () {
  console.log("i am module1");
},
// module2
function test2 () {
  console.log("i am module2");
},
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script src="./module1.js"></script>
    <script src="./module2.js"></script>
    <script>
      window.test1()
    </script>
  </body>
</html>

但这样有2个致命的问题

  1. 变量声明都是全局定义,模块化仿佛是在骗自己,文件之间的变量很容易冲突

  2. 无法管理加载顺序,以及依赖加载的问题

命名空间

命名空间跟上面文件划分类似的解决办法,不过,将它们统一包裹在不同的对象之中,这样为了解决变量声明是全局定义的问题。

这样一来,每个模块都会挂载到对应的命名空间,可以在一定程度上解决命名冲突的问题。

但依然存在问题:

  1. 内部变量都是共有的,外接很容易访问到

  2. 文件划分一样,无法管理加载顺序。

  3. 可能存在重名问题

立即执行函数

立即执行函数由于作用域的关系,有了自己的私有变量,于是较为彻底地实现了变量的问题,同时,产生的闭包也可能让开发者自行定义模块名称

但是,依然存在一个无法回避的问题,那就是无法解决模块只见的加载顺序,以及模块依赖的问题,也就是模块加载的问题。

随着前端的发展,工程越来越大,模块依赖已经是很常见的事情,模块加载的需求日益迫切,非标准化模块规范并不能得到满足,因此也催生了各种各样的模块化规范。

例如:

  1. CommonJs规范

  2. AMD规范

  3. CMD规范

  4. UMD规范

CommonJS

CommonJs是最早提出的模块化规范,一开始主要用于服务端,后来Node.js采用了这个规范。

CommonJS 规范是一个文件就是一个模块,加载模块使用require方法,改方法读取文件并执行,最后返回文件内部的exports对象。

由于CommonJS是服务器规范,并且模块加载器是Node.js提供,所以CommonJS模块直接放在浏览器是无法执行,不过社区也有阉割版的打包工具。

CommonJS 加载模块是同步的,只有加载完成之后,才能执行之后的操作,只在服务器本地是没有太大问题的,但放到浏览器,就受制于网络影响了,JS是单线程,模块请求会阻塞JS的解析。导致页面卡顿。

所以,CommonJS不太适合浏览器运行,那么就需要一个浏览器端的模块规范了。

AMD

AMD是RequireJs推广过程中,对模块定义的规范化,全称Asynchronous Module Definition翻译过来是异步模块定义规范,在这个规范中浏览器中的模块会被异步加载,也就不会产生页面阻塞的情况了。

在AMD规范中,可以使用define来定义、加载一个模块。

如果第一个参数是函数,那么就是无依赖定义模块 ,也就是此函数return出去的成员,就是需要导出的成员。

如果第一个参数是数组,那么就是有依赖定义模块,先加载对应依赖,然后根据依赖在第二个参数执行相应逻辑,同时可以return形式,导出本模块需要导出的成员。

require也可以来加载模块,但不能定义模块

其实可以从以上的代码可以看出来,如果一个模块依赖另一个模块,那么就需要在一开始把对应的依赖写好,同时提起加载所有可能用到的模块(RequireJs2.0也可以延迟加载)。也就是说AMD推崇依赖前置

CMD

cmd是SeaJS推广过程中,对模块定义的规范化,全称Common Module Definition ,翻译过来就是通用模块定义规范。在这个规范中,模块与模块的的依赖也可以就近执行,而不用提前定义。


在CMD规范中,我们需要使用define来定义模块、加载一个模块。

第一个参数一直都是一个函数,这个函数暴露三个方法,分别是require,exports,module

require是一个方法,接受模块标识作为唯一参数,用来获取其他模块提给的接口。

exports是一个对象,用来提供向外的模块接口,同时,亦可以如同AMD一样,直接return出需要导出的成员。

module是一个对象,上面存储了当前模块相关联的属性与方法,例如exports的引用,也可以通过module.exports来获取。

可以看出,CMD的API 严格区分,并且模块并不需要提前写好、加载,也就是CMD推崇依赖就近

UMD

AMD是浏览器规范,异步加载模块,CommonJS是服务器规范,同步加载模块,并且模块无需包装。

那么两个规范相互柔和,就成为了UMD,全称Universal Module Definition,翻译过来也是通用模块定义规范。

在这个规范中,会先判断是否支持Node.js模块,如果支持那么启用Node.js模块模式,再判断是否支持AMD,存在就使用AMD的方式加载模块。

从上述代码中可以看出,UMD并非一个新规范,而是兼容了ADM和CommonJS,同时运行浏览器和Node.js环境的规范,也就是说,UMD具有了跨平台能力。

同时也意味着,前端模块进化出了跨平台能力

ES6 Module

ES6 Module也被叫为ES Module或者ESM

经过这么长时间的迭代,ECMAScript官方也提出了模块化规范,在提出后,很快得到了浏览器的支持。

ESM具有以下特性:

  1. 浏览器可以直接加载ESM,也是使用script标签,但需要加入type="module"属性

  2. 自动采用严格模式

  3. 输出的是值的引用

  4. 编译时输出接口

  5. 可以import()异步加载

  6. Node.js 12.20版本支持ESM,因此有跨平台能力

ESM主要两个命令构成:export, importexport用于规定模块对外接口,import用于引入其他模块。

由于原生支持,因此使用起来比之前的规范方便、简单得多。

二、vite

之所以讲述前置知识,是因为接下来的认识VIte的过程需要用到以上知识。

vite与webpack

如何评价一款构建工具的优劣,主要从四个方面来进行判断:

  1. 模块化方案:指提供模块加载方案,以及对不同模块的规范做兼容

  2. 语法转译:sass、typesript的转移,以及对图片、字体、worker的加载、集成

  3. 产物质量:指打包好的文件的压缩、混淆、tree shaking、以及语法降级

  4. 开发效率: HMR,构建速度等

vue-cli是基于webpack的构建、开发脚手架。

webpack具有强大的社区生态,各种开发业务需求大部分能够覆盖到。但缺点也很明显:冷启动和热更新的时间会随着项目的扩增而增加

导致这个原因是因为:

  1. 项目冷启动需要递归打包整个依赖树

  2. 构建使用JavaScript构建,JavaScript本身需要转义为字节码,然后才转化成机器码

这样改动后无法立即看到效果,开发体验就会越来越差。

Vite则是基于EsbuildRollup的构建、开发框架工具。它具有以下特点。

  1. 在开发阶段,Vite基于浏览器原生ESM实现了no-bundle,同时借助Esbuild实现了第三方库的构建和ts/jsx的语法编译,同时Esbuild基于Go语言实现,直接转化成机器码,速度提升飞快。

  2. 模块化方面,由于基于浏览器原生ESM,因此可以使用ESM实现模块加载,通也可以将开发环境、生产环境中的模块产物转换为ESM

  3. 语法转译,内置对typescriptJSXsass等高级语法支持,也能加载图片、字体、worker等

  4. 产物质量方面,基于成熟的Rollup实现生产环境打包,同时也能配合TerserBabel等工具链,也能保证产物质量。

可以说Vite属于人有我优的前端框架,Vite不是第一个no-bundle构建工具,在之前还有Snowpack,但Vite的出现补齐了Dev Server能力,比如HMR

Snowpack自身有一套插件机制,但并不兼容任何打包工具,如需打包,需要调用其他打包API,自身没有打包能力

Vite生态方面根植Rollup,意味着Vite从诞生起就有一个比较高的起点,产物质量和稳定性都相对成熟,更有利于工具的推广和发展。

vite的双引擎是如何工作的

关于Vite的的双引擎,大部分人都能说出大概:开发阶段用Esbuild,生产环境用Rollup,但是真的这么简单吗?通过查阅不少资料,发现并没有想的那么简单。

下面是一张画的比较完善的Vite的架构图。

vite.png预构建Esbuild

可以从架构图上注意到,Vite在开发环境,Esbuild主要参与的是有一个预构建的阶段。那么什么是预构建阶段呢?

前文不断地强调:Vite是一个no-bundle构建工具,并且能按需编译,但实际上:Vite并非一个纯粹的no-bundle构建工具。

我们的代码分为两部分,项目中的源代码,也就是业务代码,另一部分是第三方依赖,也就是node_modules那一些第三方包。

针对这些第三方包,Vite依然会进行打包,所谓的no-bundle是对于业务代码来说的。

给第三方包转化为ESM,并合并到一起的过程,称为预构建过程。

这一过程在Vite1.X的时候,是由Rollup来进行预构建,在2.X更换为Esbuild,编译速度提升100倍。

如果手头有一个Vite项目,那么当它在开发环境启动成功的时候,我们可以通过node_modules/.vite看到一堆打包产物,这就是Esbuild的预构建产物。

如果我们使用浏览器进入项目网页,我们可以从Network看到第三方依赖的请求结果,使用了强缓存,并且过期时间为一年。

当然,Vite并非一直使用缓存文件,如果下面三处进行改动,那么缓存将失效

  1. poackage.jsondependencies字段

  2. 包管理器的lock文件

  3. vite.config.jsoptimizeDeps配置

之前提到过,Esbuildgo语言开发,但它性能极高还有其它因素:

  1. 同时多核并行,内部步骤尽可能并行,也得益于go多线程共享内存的优势

  2. 几乎没有使用任何第三方库,一切手撸

  3. 高效内存利用,众所周知wabpack的AST和babel的AST是不共用的,但Esbuild从头到尾尽可能复制一份AST,而不用传统打包软件那样频繁转换AST造成打量内存浪费。

下图为Esbuild官网上,它与传统打包工具的对比

看到这里,可能觉得Esbuild应该是一个超级打包工具,秒天秒地的那种,但实际上,它自身也有很多缺点:

  1. 不支持降级到ES5,意味着低端浏览器基本告别Esbuild

  2. 不支持操作打包产物的接口,比如RolluprenderChunk

  3. 支持自定义的代码分割,降低拆包的灵活性

这么看下来,Esbuild具有很大的局限性,作为独立打包工具的话,很难跟Rollup以及webpack进行抗衡.

但是,Esbuild实在是太快了!

单文件编译Esbuild

前文中提到,ESbuild参与了预构建环境,但也提到Esbuild也实现了ts/jsx的编译。同时还提到了,Esbuild高效地利用AST,那么,把它跟babel、typescript以及swc对比,会如何呢?

以下结果对比来源:

https://datastation.multiprocess.io/blog/2021-11-13-benchmarking-esbuild-swc-typescript-babel.html

上图为50MB文件平均编译速度。是的,依然非常快

当然,缺点还是有的:Esbuild没有实现TS的类型检查,仅仅在编译的时候抹除类型相关代码

这个方法可以通过编辑器插件,在开发阶段进行类型检查,以及借用tsc来实现类型检查功能

代码压缩Esbuild

回顾下架构图,我们已经讲了两处Esbuild参与过程,还剩下一处,那就是架构图右上角的生产环境中,Esbuild也参与其中。

Esbuild通过插件的形式,参与到Rollup的打包流程,根据上面两小节的阅读,我们已经能大概猜到是什么原因了,那就是它的压缩效率是太高了。

传统方式是使用Terser这种基于javascript的压缩器来实现,比如在webpack或者Rollip作为一个插件来完成压缩混淆工作

  1. 既然是基于javascript,那么它的速度就没有go的速度快

  2. EsbuildAST共享是Terser望尘莫及的

以下结果来源:

https://github.com/privatenumber/minification-benchmarks

虽然Esbuild时间不是每次都是第一,但在这个数量级上还能兼顾压缩质量,那么可以把它称为性能利器因此Vite将其作为默认的压缩方案。

同时需要注意的是,在生产环境使用Esbuild并不会让代码无法降级到ES5,因为生产环境使用了babel进行语法降级。

构建与Vite插件

之前都是讲Esbuild的,但另一个引擎Rollup却很少提到,但这并非不重要,它反而贯彻了Vite始终。

虽然Vite开发环境实现了no-bundle,但生产环境该打包还是得打包,否则会有网络问题(巨量的请求带来巨量的网络开销),那么既然用到Rollup,那么Vite也基于Rollup做了扩展和优化。

  1. CSS打码分割,如果某个异步模块用了一些CSS,Vite会自动抽取成为单独文件,提高缓存复用率

  2. 自动预加载Vite为入口chunk的依赖生成预加载标签<link rel="moduelpreload">

  3. 异步chunk加载优化,如果两个chunk有相同的依赖A,那么并不会按正常加载逻辑加载chunk的过程中依赖A,而是在请求第一个chunk的时候就自动预加载那个A。

同时,在之前讲过,Vite根植于Rollup的插件机制生态,简单点来说就是:

不论开发环境还是生产环境Rollup的插件,不少是可以直接用到Vite里面的,而Vite的插件写法,是完全兼容Rollup的。

Vite实现了Rollup的的调度逻辑,生产环境中奖所有Vite插件传入Rollup也是没有问题的。

兼容插件可以从一下网址寻找

https://vite-rollup-plugins.patak.dev/

小结

在Vite中,双引擎各司其职,然后VIte本身参与其中并且调度,这个框架才会顺利运行起来。

Esbuild负责将node_modules包编译为ESM,同时业务文件的ts/jsx也由Esbuild编译,同时它还作为打包时候的代码压缩默认方案参与其中。

Rollup负责将node_modules和业务文件进行打包

Vite负责将业务代码转成ESM,同时做好调用Esbuild编译完的第三方包的准备,以及使用兼容Rollup的插件进行代码处理,也就是说,Vite本身以及Vite插件是接触不到node_modules真正第三方包的,而是接触的是node_modules/.vite已经被转译的第三方包的ESM,换句话说,Vite以及vite插件的作用域是node_modules/.vite和业务代码

但是vite.config.js也是可以配置Esbuild插件的,此插件属于Esbuild范畴。

三、迁移

之前的理论知识已经积累的差不多了,那么接下来就应该是迁移了。

index.html处理

首先是index.html的改装,将index.htmlpublic放到根目录下,同时在body中增加

<script type="module" src="/src/main.js"></script>


这个地方设置为项目的入口文件,同样是也可以是ts文件。

然后,我们类似<%= VUE_APP_TITLE %>的模板变量,统统改成硬解码值,也就是具体的值。

虽然我们可以通过vite-plugin-html这个插件实现相同的效果,但是在迁移初期,尽可能保持少的插件安装,然后根据情况一步步增加插件。

Vue文件引入处理

将所有引入vue文件的地方,增加.vue扩展名,这个是设计如此,即使在extensions中进行配置['.vue'],也会报错,必须显式引入。

安装Vite

  • 首先我们将@vue/cli-开头的插件全部卸载,如果项目中使用了ts,包含了@vue/cli-plugin-typescript,也一并卸载,卸载后编辑器会有报错,这个我们之后解决

  • sass-loader也可以删除vite内置了css预处理器

  • 安装vite-plugin-vue2vite,因为vite需要认识vue文件,所以需要安装vite-plugin-vue2,让vite认识vue2文件,如果是vue3的话,插件需要更换为@vitejs/plugin-vue

  • 删除vue-template-compiler,因为vite-plugin-vue2自带对应功能

  • 删除babel-eslintcore-js,因为我们开发仅支持现代浏览器,因此这俩个包就需要删除,毕竟没有用了,后续旧浏览器的兼容我们之后解决

  • 既然我们删除了babel-eslint,如果我们在.eslintrc用到了babel-eslint,我们也需要移除对应字段

  • 然后我们在.eslintrc中,把node:true替换es2021:true

  • 如果可以的话,更新我们的eslint到最新版

  • pacckage.json增加以下三个命令,并移除之前的vue-cli的命令

  1. "dev": "vite",

  2. "build": "vite build",

  3. "serve": "vite preview",

  • 如果使用了lint,可自行编写eslint的命令比如

  1. "lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src"

替换环境变量

vue-cli的环境变量文件与vite环境变量文件名称相同,可以不作调整。

但环境变量规范却不同。

vue-cli中,用户自定义的环境变量以VUE_APP_开头,同时暴露了BASE_URLNODE_ENV,通过webpack.DefinePlugin嵌入到业务代码中。

Vite中,用户自定义的环境变量以VITE_开头,但没有暴露BASE_URL字段,虽然在业务代码依然可以获取BASE_URL的值,但此值是通过配置文件base字段获取,而非通过环境变量文件自定义的。如果配置了NODE_ENV字段,则会被包装为VITE_USER_NODE_ENV字段。

Vite获取环境变量的方式也与vue-cli不同,在vue-cli中,获取环境变量直接可以通过process.env获取的,但VIte不同。

Vite在读取完配置文件之前,是默认不加载环境变量文件的,这是因为配置文件中的配置项会影响加载行为,比如envDir以及root,于是Vite提供了两种获取环境变量的方式。

浏览器环境,也就是业务代码部分(这时早就加载完环境变量文件),使用import.meta.env获取环境变量对象,这里需要注意的一点的是,Vite全局匹配了import.meta.env这个字符串,然后在对应文件顶部,定义这个对象,所以能获取环境变量对应的值。

import.meta.env并不是一个实时存在的内部对象,同时Vite仅仅匹配了’import.meta.env‘,也就是说,import.meta['env']这种形式是没有匹配到,Vite是不在文件头部定义这个对象,因此是获取不到环境变量的。

同时也可以注意到,import.meta.env不光暴露了我们自己定义环境变量,也附带了比如MODE,DEV等环境变量

Node.js环境中,最常见情况的是在Vite的配置文件中,由于之前提到过影响加载行文的问题,这里面是无法使用import.meta.env的,所以VIte暴露了一个API来获取对应的环境变量:loadEnv,这个我们将在下一节讲到。

需要提醒的一点是,环境变量文件.env,.env.local,.env.[mode],.env.[mode].local都会被加载,而非只匹配一个,环境变量的优先级跟vue-cli相同,按照从左到右优先级依次增大。

增加Vite配置

识别vue文件

如同vue.config.jsvue-cli一样,Vite也需要对应的配置文件:vite.config.js,以下是一个普通的vue2的Vite配置文件

如果用的是vue3,把vite-plugin-vue2替换为官方的@vitejs/plugin-vue即可。

base与环境变量

在上一节,我们提到了获取环境变量的API:loadEnv,那么如何使用这个API呢?以下是官方文档提给的例子

在这里,defineConfig接受的参数从之前的对象变为函数,然后通过这个函数的参数,我们可以获取启动vite的时候的两个信息 modecommand

  • mode在 执行npm脚本 ”vite“的时候,默认为development,执行"vite build" 与 "vite preview"的时候默认为production,如果在命令后面增加--mode XXX参数,就会变为XXX,同时环境变量文件也变为.env.XXX文件,在业务代码访问import.meta.env.MODE的时候,也显示为XXX,这一点与vue-cli类似。

  • command只有两个值,buildserve,在执行"vite build"的时候,会传入build,执行"vite"与"vite preview"的时候为serve

loadEnv接受三个参数,modeenvDir,以及可选参数prefixes

  • mode为当前环境,也可以认为是env文件的mode,意义与defineConfig的参数mode相同

  • envDir为项目工程中,env文件的存放处

  • prefixes可选,标识env环境中,环境变量的前缀,默认是VITE,env文件中有vue-cli的变量,使用VUE_APP也是可以匹配出来东西的

参数介绍完之后,大家会有什么想法呢?是不是看着这些参数感觉不像是一个经过vite高度封装的API,而是根据信息解析文件内容的小插件。

事实上的确如此的,分析下loadEnv的源码,大体也是这么写的,这个函数通过envDir寻找env文件地址,通过mode读取所需要的env文件合集,通过prefixes匹配文件中的字符串,然后按照优先级进行合并,最后返回来的就是我们想要的环境变量。

我们通过环境变量配置了base字段,这个字段类似vue-cli中的publicPath,可以用来定义项目的baseCDN

alias

别名alias不需要通过webpack引入,而是通过resolve.alias引入,如下图

改造项目中,由于使用了私有vue-cli插件,因此产生了多余引用引用,但到vite里面,由于不存在对应包,所以会在编译阶段报错,因此我们可以在alias中进行处理、屏蔽。

虽然aliasVite中配置无误,但编辑器与eslint并不认可,因此我们需要在在对应地方补充别名

  1. jsconfig.json/tsconfig.js中的paths字段,使用相对路径

  2. .eslintrc中的settings['import/resolver'],在这里面我们需要使用绝对路径,因为路径起点并非入口文件,而是每一个跑lint的文件,如果使用相对路径,那么很大一部分代码会报找不到路径的lint警告。正确配置如下图

同时也可以注意到,上面使用了node.extensions,这个是用来解决,引用ts文件被lint要求写后缀名,结果加上后缀名ts又提示需要去掉后缀名这个矛盾问题的。若出现此问题,可依照此法解决,若没有,可忽略此配置。

define

同样是使用了私有插件的原因,暴露了__DEV__ASSETS_URL_DEFINE等在浏览器环境中使用的环境变量,在vue-cli中,是通过使用webpack.DefinePlugin进行注入实现了,显然,vite需要使用其他方法,官方提供了denfine字段,来实现类似的功能。

这里需要注意的一点是,与define虽然与webpack.DefinePlugin实现了类似的功能,但具有本质的不同:那就是define接受一个JSON对象,而非webpack.DefinePlugin的可执行js代码,在原理上webpack.DefinePlugin是将对应目标变量替换为js语句,然后进行编译操作,而Vitedefine的替换不会进行任何语法分析,在构建时被静态替换。

在处理私有部署相关的环境变量getAssetsDefine的时候,因为使用的webpack的规范,将代码转为JSON字符串,因此我们需要在Vite使用之前,使用JSON.parse将其转成JSON对象。

注意:

  1. 目前很多包不支持ESM,因此编译之后的包依然残留process.env,所以我们可以通过这个功能来静态注入Node.js环境下的process.env

  2. 目前很多包也使用了global,但是经过实践,却发现无法同process.env一样使用define注入,因为global并非一个JSON对象,因此Vite无法格式化global,浏览器发生报错。好在,浏览器是有globalThis,所以解决方法是在index.html文件,使用script标签将globalThis指向global

<script>
  window.global = window.globalThis
</script>

inject

在私有插件中,我们可以通过使用webpack.ProvidePlugin,来让使用了自定义变量的文件,自动在头部引入一些文件。

Vite并没有提供对应功能,因此需要使用插件来达到此目的。

我们在之前提到过,Vite根植于Rollup生态,因此是可以使用部分Rollup插件的,而Rollup刚好存在与ProvidePlugin类似的插件:@rollup/plugin-inject

而使用方法同样与webpack.ProvidePlugin类似,如下代码:

如果检测到R_URL,那么就会在顶部动加入以下代码

import { default as R_URL } from 'XXXX/constants/url'

这里需要注意的是,地址是支持alias的。

同时,/constants/url/index.js文件也需要使用Vite的API,之前使用了require.context这个API来实现批量加载文件,但在Vite,批量加载的API是import.meta.globEager,他的底层加载逻辑是基于fast-glob实现的,因此可以参考fast-glob的文档来了解匹配规则。

https://github.com/mrmlnc/fast-glob#pattern-syntax

以下为更改后的获取API代码,同之前的逻辑类似,只不过更换成了Vite的批量引入API。

如果代码中require.context这个API较多的话,可以考虑使用@originjs/vite-plugin-require-context这个vite插件,这个插件会检查你的业务代码中的require-context,将其替换为依次引入。

需要注意的是@originjs/vite-plugin-require-context不会处理node_modules中的代码,因此无法处理第三方包使用了require-context的这种情况。

如果使用了defineinject,请记得一定要在.eslintrc以及.d.ts文件(如果使用了ts的话)注册对应的变量,否则会在编辑器以及命令行弹出警告。

css

vue-cli中,如果想更改css loader相关的配置,需要在css.loaderOptions.css或者css.loaderOptions.postcss等中进行配置,里面的参数,比如additionalData等,会传给对应的loader进行相应的处理。

Vite中,也有对应的配置项,但结构上有所不同,Vitepostcss的配置与css预处理器的配置进行了区分:

  • 用来传递给css的预处理器。Vite使用css.preprocessorOptions.less或者css.preprocessorOptions.scss进行配置,其中scssless为文件扩展名。

  • 用来传递给postcss的配置。Vite使用css.modulescss.postcss,其中css.postcsspostcss的配置项,而css.modulespostcss-modules的配置项。

在项目中,私有插件通过修改webpacksass-resources-loader,定义了common-resources.scss为公共变量scss文件,Vite显然没有sass-resources-loader,但这不妨碍我们使用Vite的api将其定义为公共变量scss文件。

我们使用css.preprocessorOptions.scss来处理公共变量文件,同时还处理了私有部署相关的scss变量,使用additionalData可以让一段sass/scss代码出现在实际入口文件之前。

这里需要注意的是,在vue.config.js中,我们配置的是css.loaderOptions.sass,而preprocessorOptions使用的是文件扩展名作为键值,因此是css.preprocessorOptions.scss

需要使用postcss插件的话,只需要在postcss.plugins使用即可。虽然在vue.config.js中,也可以使用css.loaderOptions.postcss.plugins挂载私有部署相关插件,但由于此处配置的插件,会在resolve-url-loader之前执行,导致resolve-url-loader无法获取到对应链接而报错。所以实际操作中,需要通过修改webpack中的loader执行顺序,然后才能挂载插件。

但在Vite中,并没有resolve-url-loader的相关逻辑,因此可以直接使用postcss插件。

server与preview

本地开发服务器配置字段servervue-cli中大体相似,因此可以直接复用,不过依然还是有些许不同。

server.force字段,如果为true,那么每次使用本地开发服务器都会强制预构建,而不使用缓存,一般情况下我们不会使用这个字段,但依然有少数场景,比如在调试某些包的预构建结果的时候,需要屏蔽一段时间缓存,那么就可以将server.force置为true,即可在使用本地开发服务器的时候,预构建一直是最新版本。

包括将server.force置为true,一共有三种方法强制启用预构建:

  1. 删除node_modules/.vite目录

  2. server.force置为true

  3. 命令行执行npx vite --force或者npx vite optimize

这里需要注意的是,Vite启动分为两部,第一步是预构建,第二步是启动Dev Server,因此npx vite optimizenpx vite --force不同之处在于npx vite optimize仅仅是进行了预构建,后者启动了Vite

除了serverVite还有一个类似的配置项,preview,可以将build的产物在本地开启一个服务器,进行预览,因此称为预览服务器配置。只不过区别在于,server的资源来源是项目工程,而preview的资源来自于build后的产物

server.https字段,在vue-cli中,使用的是webpackdev-server,如果想要启用http2,需要在Node.js 15.0.0以下版本使用,设置server.http2:true,因为所依赖的spdy包,不支持15.0.0以及之后的版本,webpack计划将dev-server迁移到Express,使用Node.js内置http2包,但由于牵扯太多,直到今天依然没有动手。

Vite中,使用的是http-proxy,如果想要启用httpshttp2,设置https:true即可,但仍然有一点需要注意,如果配置了server.proxy,那么Vite仅使用https

如果使用https浏览器报不安全的问题。

  1. vue-cli中我们可以通过使用node_modules/webpack-dev-server/ssl文件中的证书,通过添加信任即规避此问题。

  2. Vite中,我们可以使用mkcert,自主生产一个本地https证书

开启https后,Vite不会存在sockjs-node报错问题,因为是直接建立ws,而vue-cli并非这样,默认以内网ip为host,发起了一个http下允许跨域的请求,然后才会建立ws,但这个请求https下会因为没有证书,导致https请求跨域报错,所以会不断轮询、报错。如果规避此问题,需要配置sockhost,或者配置devServer.disableHostCheck: true,以及devServer.publiclocalhost + 端口,但这样就代表放弃了network调试能力。

(注:本文vue-cli指的是V4版本,V5版本基于webpack-dev-server V4,也已经废弃了sockhost和public)

亦或者使用mkcert自主生产一个内网ip的https证书,让第一次scokjs-nodehttps请求成功,建立ws链接,或者用更加“方便”的方法,一个项目启动两次,其中一次为http版本的,然后用proxy/sockjs-node/代理到http版本的项目中,成功后建立ws链接。

实际上vue-cli的默认ip行为本身是令人诟病的,如果较为完善解决,需要用到nginx,以下为关于此问题的讨论:vue-cli默认使用当前ip为常量

https://github.com/vuejs/vue-cli/issues/1616

如何使用mkcert:

其实,mkcert可以单独拎出来当做一篇文章来讲,但由于跟上文关系紧密,就放在一起讲了。

如果对上文开启https之后,sockjs-node报错处理云里雾里的话,那么这一节就是来讲如何干净利落地解决报错

其实解决报错的方法很简单,就是安装https证书,但是为什么按了证书还会报错呢?因为node_modules/webpack-dev-server/ssl 的证书是localhostsockjs-node是局域网ip,因此证书无法覆盖,所以发起这个接口是跨域、报错。

既然webpack-dev-server的证书覆盖有局限性,那么我们自己生产一个覆盖全面的证书就可以了。

使用

mkcert localhost 192.168.0.112

即可在当前目录生成cert和key

然后执行

mkcert -install

将证书注册到证书管理

在上一节中,我们一般使用https的布尔值,其实它还可以接受一个对象,键为certkey,值为路径,用于加载对应证书。我们将生成的两个文件加入https中,即可解决证书覆盖不全的问题。

https:{
      cert: fs.readFileSync(path.join(__dirname, './localhost.pem'),'utf8'),
      key: fs.readFileSync(path.join(__dirname, './localhost-key.pem'),'utf8')
    },

第三方包的修改

从之前的内容来看,与Vite启动相关的内容基本都覆盖到了,文档也将快结束了,事实上也正是如此。但我们忽略了一个东西——我们都知道,Vite之所以很快,一部分的原因是Esbuild将第三方库转译为ESM,进行预构建,那么问题来了,如何确保Esbuild转译出来的代码,支持浏览器环境?

换句话说,如果第三方库的代码出现问题,包括但不限于使用了浏览器环境下不存在的API,应该怎么办?

举个例子,第三方包中有使用到了require.context,这个API显然在浏览器是没有的,那么应该怎么办呢?

修改源代码

这是一种十分狂野的做法,如果给不兼容的库提交代码,周期肯定会很长。如果在私有npm发布包的定制版本,那么后续包的更新上肯定也是个麻烦的事情,如果修改本地的node_modules,那么团队协作上就会有问题。

关于此问题,其实社区也给出了解决方法,那就是patch-package这个库,它可以把对本地node_modules的修改记录下来,随着git同步给协作的团队,然后在postinstall的时候根据修改记录,对目标包进行修改。

这里需要注意的是,我们需要锁定目标包的版本,防止自动更新到其他版本引起代码变化。

首先,我们安装patch-package

npm i patch-package -D

然后,我们将第三方包口文件中,将require.context替换为import.meta.globEager

npx patch-package XXX

这样,我们项目本地就会生成一个patches文件夹,记录了我们的更改

最后,我们在package.jsonscripts中增加如下内容:

{ 
  "scripts": { 
    "postinstall": "patch-package" 
  }
}

这样,我们就解决了改动第三方包所带来的的团队协同问题。

Esbuild 插件

我们都知道,预构建是Esbuild的工作,那么是不是可以通过Esbuild插件,来达到修改第三方源码的目的呢?答案是可行的。

Esbuild的配置主要集中在optimizeDeps.esbuildOptions中,关于optimizeDeps,我们之前提到过很多次了,它在Vite中被称作依赖优化选项,这也是为什么之前提到的optimizeDeps的改动会导致强制预构建的原因。

optimizeDeps.esbuildOptions.plugins中,接受一个数组,可以将我们自己写的替换插件,传入进去。

Esbuild的插件,被设计为一个对象,里面包括namesetup两个属性,其中,name是插件名称,setup是一个函数,接受一个build对象,这个对象上挂载了一些钩子,可以让我们实现一些自定义逻辑。

  1. onResolve是路径解析的时候触发的钩子

  2. onLoad是模块加载的时候触发的钩子,也是我们这次使用的钩子

  3. onStart构建开始的时候的钩子,包括触发watch或者serve模式下的重新构建

  4. onEnd是构建结束的钩子

这次我们主要将onLoad如何实现第三方包的修改。

onLoad需要传入两个参数第一个是一个对象options,第二个是回调函数callback。其中optionsonResolve一样,包含filternamespace两个属性。

filter是必传参数,为一个正则表达式,可以用它来匹配目标文件。

注意: 插件中的 filter 正则是使用 Go 原生正则实现的,为了不使性能过于劣化,规则应该尽可能严格。同时它本身和 JS 的正则也有所区别,不支持前瞻(?<=)、后顾(?=)和反向引用(\1)这三种规则。

namespace为选填参数,一般在onResolve钩子中的回调函数callback,返回namespace属性作为标识,然后我们可以在onLoad钩子中通过namespace,拿到对应模块。

callback每个钩子不相同,以下为onLoad使用的callback

有了这些API,那么我们实际上很快可以搞出一个删除目标引用的Esbuild插件

小结

我们了解了前端模块的发展历史,同时也了解了Vite的原理,然后,我们根据这些原理,将vue-cli的项目迁移过来,最后我们执行了build命令,成功打包,这一切都是曲折但成功的。

但是,这也只是开始,我们只是达到了他人使用Vite创建新项目的高度。

这次只是让整个项目流程跑通,还有山一样多的优化在等着我们,比如路由的懒加载、资源文件的合并、打包的优化等等等等。

需要学习的东西有很多,这并非我们的终点,而是我们新的起点。

0
博主关闭了所有页面的评论