vite 的启动链路以及背后的部分原理

上文我们说到 plugin,那么有哪些 plugin 呢?它们分别是:

  • 用户注入的 plugins —— 自定义 plugin

  • hmrPlugin —— 处理 hmr

  • htmlRewritePlugin —— 重写 html 内的 script 内容

  • moduleRewritePlugin —— 重写模块中的 import 导入

  • moduleResolvePlugin ——获取模块内容

  • vuePlugin —— 处理 vue 单文件组件

  • esbuildPlugin —— 使用 esbuild 处理资源

  • assetPathPlugin —— 处理静态资源

  • serveStaticPlugin —— 托管静态资源

  • cssPlugin —— 处理 css/less/sass 等引用

我们来看 plugin 的实现方式,开发一个用来拦截 json 文件 plugin 可以这么实现:

interface ServerPluginContext {

root: string

app: Koa

server: Server

watcher: HMRWatcher

resolver: InternalResolver

config: ServerConfig

}

type ServerPlugin = (ctx:ServerPluginContext)=> void;

const JsonInterceptPlugin:ServerPlugin = ({app})=>{

app.use(async (ctx, next) => {

await next()

if (ctx.path.endsWith(‘.json’) && ctx.body) {

ctx.type = ‘js’

ctx.body = export default json

}

})

}

vite 背后的原理都在 plugin 里,这里不再一一解释每个 plugin 的作用,会放在下文背后的原理中一并讨论。

build

这部分代码在 node/build/index.ts 中,build 目录的结构虽然与 server 相似,同样导出一个 build 方法,同样也有许多 plugin,不过这些 plugin 与 server 中的用途不一样,因为 build 使用了 rollup ,所以这些 plugin 也是为 rollup 打包的 plugin ,本文就不再多提。

NO.4

vite 运行原理

ES module

要了解 vite 的运行原理,首先要知道什么是 ES module,目前流览器对其的支持如下:

主流的浏览器(IE11除外)均已经支持,其最大的特点是在浏览器端使用 export import 的方式导入和导出模块,在 script 标签里设置 type="module" ,然后使用模块内容。

当 html 里嵌入上面的 script 标签时候,浏览器会发起 http 请求,请求 htttp server 托管的 bar.js ,在 bar.js 里,我们用 named export 导出 bar 变量,在上面的 script 中能获取到 bar 的定义。

// bar.js

export const bar = ‘bar’;

在 vite 中的作用

打开运行中的 vite 项目,访问 view-source 可以发现 html 里有段这样的代码:

从这段代码中,我们能 get 到以下几点信息:

  • 从 http://localhost:3000/@modules/vue 中获取 createApp 这个方法

  • 从 http://localhost:3000/App.vue 中获取应用入口

  • 使用 createApp 创建应用并挂载节点

createApp 是 vue3.X 的 api,只需知道这是创建了 vue 应用即可,vite 利用 ES module,把 “构建 vue 应用” 这个本来需要通过 webpack 打包后才能执行的代码直接放在浏览器里执行,这么做是为了:

  1. 去掉打包步骤

  2. 实现按需加载

去掉打包步骤

打包的概念是开发者利用打包工具将应用各个模块集合在一起形成 bundle,以一定规则读取模块的代码——以便在不支持模块化的浏览器里使用。

为了在浏览器里加载各模块,打包工具会借助胶水代码用来组装各模块,比如 webpack 使用 map存放模块 id 和路径,使用 __webpack_require__  方法获取模块导出。

vite 利用浏览器原生支持模块化导入这一特性,省略了对模块的组装,也就不需要生成 bundle,所以打包这一步就可以省略了。

实现按需打包

前面说到,webpack 之类的打包工具会将各模块提前打包进 bundle 里,但打包的过程是静态的——不管某个模块的代码是否执行到,这个模块都要打包到 bundle 里,这样的坏处就是随着项目越来越大打包后的 bundle 也越来越大。

开发者为了减少 bundle 大小,会使用动态引入 import() 的方式异步的加载模块( 被引入模块依然需要提前打包),又或者使用 tree shaking 等方式尽力的去掉未引用的模块,然而这些方式都不如 vite 的优雅,vite 可以只在需要某个模块的时候动态(借助 import() )的引入它,而不需要提前打包,虽然只能用在开发环境,不过这就够了。

vite 如何处理 ESM

既然 vite 使用 ESM 在浏览器里使用模块,那么这一步究竟是怎么做的?

上文提到过,在浏览器里使用 ES module 是使用 http 请求拿到模块,所以 vite 必须提供一个 web server 去代理这些模块,上文中提到的 koa 就是负责这个事情,vite 通过对请求路径的劫持获取资源的内容返回给浏览器,不过 vite 对于模块导入做了特殊处理。

@modules 是什么?

通过工程下的 index.html 和开发环境下的 html 源文件对比,发现 script 标签里的内容发生了改变,由

变成了

vite 对 import 都做了一层处理,其过程如下:

  1. 在 koa 中间件里获取请求 body

  2. 通过 es-module-lexer 解析资源 ast 拿到 import 的内容

  3. 判断 import 的资源是否是绝对路径,绝对视为 npm 模块

  4. 返回处理后的资源路径:"vue" => "/@modules/vue"

这部分代码在 serverPluginModuleRewrite 这个 plugin 中,

为什么需要 @modules?

如果我们在模块里写下以下代码的时候,浏览器中的 esm 是不可能获取到导入的模块内容的:

import vue from ‘vue’

因为 vue 这个模块安装在 node_modules 里,以往使用 webpack,webpack遇到上面的代码,会帮我们做以下几件事:

  • 获取这段代码的内容

  • 解析成 AST

  • 遍历 AST 拿到 import 语句中的包的名称

  • 使用 enhanced-resolve 拿到包的实际地址进行打包,

但是浏览器中 ESM 无法直接访问项目下的 node_modules,所以 vite 对所有 import 都做了处理,用带有 @modules 的前缀重写它们。

从另外一个角度来看这是非常比较巧妙的做法,把文件路径的 rewrite 都写在同一个 plugin 里,这样后续如果加入更多逻辑,改动起来不会影响其他 plugin,其他 plugin 拿到资源路径都是 @modules ,比如说后续可能加入 alias 的配置:就像 webpack alias 一样:可以将项目里的本地文件配置成绝对路径的引用。

怎么返回模块内容

在下一个 koa middleware 中,用正则匹配到路径上带有 @modules 的资源,再通过 require('xxx') 拿到 包的导出返回给浏览器。

以往使用 webpack 之类的打包工具,它们除了将模块组装到一起形成 bundle,还可以让使用了不同模块规范的包互相引用,比如:

  • ES module (esm) 导入 cjs
  • CommonJS (cjs) 导入 esm
  • dynamic import 导入 esm
  • dynamic import 导入 cjs

关于 es module 的坑可以看这篇文章(https://zhuanlan.zhihu.com/p/40733281)。

起初在 vite 还只是为 vue3.x 设计的时候,对 vue esm 包是经过特殊处理的,比如:需要 @vue/runtime-dom 这个包的内容,不能直接通过 require('``@vue/runtime-dom')得到,而需要通过 require('@vue/runtime-dom/dist/runtime-dom.esm-bundler.js' 的方式,这样可以使得 vite 拿到符合 esm 模块标准的 vue 包。

目前社区中大部分模块都没有设置默认导出 esm,而是导出了 cjs 的包,既然 vue3.0 需要额外处理才能拿到 esm 的包内容,那么其他日常使用的 npm 包是不是也同样需要支持?答案是肯定的,目前在 vite 项目里直接使用 lodash 还是会报错的。

不过 vite 在最近的更新中,加入了 optimize 命令,这个命令专门为解决模块引用的坑而开发,例如我们要在 vite 中使用 lodash,只需要在 vite.config.js (vite 配置文件)中,配置 optimizeDeps 对象,在 include 数组中添加 lodash。

// vite.config.js

module.exports = {

optimizeDeps: {

include: [“lodash”]

}

}

这样 vite 在执行 runOptimize 的时候中会使用 roolup 对 lodash 包重新编译,将编译成符合 esm 模块规范的新的包放入 node_modules 下的 .vite_opt_cache 中,然后配合 resolver 对 lodash 的导入进行处理:使用编译后的包内容代替原来 lodash 的包的内容,这样就解决了 vite 中不能使用 cjs 包的问题,这部分代码在 depOptimizer.ts 里。

不过这里还有个问题,由于在 depOptimizer.ts 中,vite 只会处理在项目下 package.json 里的 dependencies 里声明好的包进行处理,所以无法在项目里使用

import pick from ‘lodash/pick’

的方式单使用 pick 方法,而要使用

import lodash from ‘lodash’

lodash.pick()

的方式,这可能在生产环境下使用某些包的时候对 bundle 的体积有影响。

返回模块的内容的代码在:serverPluginModuleResolve.ts 这个 plugin 中。

vite 如何编译模块

最初 vite 为 vue3.x 开发,所以这里的编译指的是编译 vue 单文件组件,其他 es 模块可以直接导入内容。

SFC

vue 单文件组件(简称 SFC) 是 vue 的一个亮点,前端届对 SFC 褒贬不一,个人看来,SFC 是利大于弊的,虽然 SFC 带来了额外的开发工作量,比如为了解析 template 要写模板解析器,还要在 SFC 中解析出逻辑和样式,在 vscode 里要写 vscode 插件,在 webpack 里要写 vue-loader,但是对于使用方来说可以在一个文件里可以同时写 template、js、style,省了各文件互相跳转。

与 vue-loader 相似,vite 在解析 vue 文件的时候也要分别处理多次,我们打开浏览器的 network,可以看到:

1 个请求的 query 中什么都没有,另 2 个请求分别通过在 query 里指定了 type 为 style 和 template。

先来看看如何将一个 SFC 变成多个请求,我们从第一次请求开始分析,简化后的代码如下:

function vuePlugin({app}){

app.use(async (ctx, next) => {

if (!ctx.path.endsWith(‘.vue’) && !ctx.vue) {

return next()

}

const query = ctx.query

// 获取文件名称

let filename = resolver.requestToFile(publicPath)

// 解析器解析 SFC

const descriptor = await parseSFC(root, filename, ctx.body)

if (!descriptor) {

ctx.status = 404

return

}

// 第一次请求 .vue

if (!query.type) {

if (descriptor.script && descriptor.script.src) {

filename = await resolveSrcImport(descriptor.script, ctx, resolver)

}

ctx.type = ‘js’

// body 返回解析后的代码

ctx.body = await compileSFCMain(descriptor, filename, publicPath)

小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频

如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注:前端)
img

总结

三套“算法宝典”

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

28天读完349页,这份阿里面试通关手册,助我闯进字节跳动

算法刷题LeetCode中文版(为例)

人与人存在很大的不同,我们都拥有各自的目标,在一线城市漂泊的我偶尔也会羡慕在老家踏踏实实开开心心养老的人,但是我深刻知道自己想要的是一年比一年有进步。

最后,我想说的是,无论你现在什么年龄,位于什么城市,拥有什么背景或学历,跟你比较的人永远都是你自己,所以明年的你看看与今年的你是否有差距,不想做咸鱼的人,只能用尽全力去跳跃。祝愿,明年的你会更好!

由于篇幅有限,下篇的面试技术攻克篇只能够展示出部分的面试题,详细完整版以及答案解析,有需要的可以关注
)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频

如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注:前端)
[外链图片转存中…(img-2RSd8Jzx-1710694256474)]

总结

三套“算法宝典”

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

28天读完349页,这份阿里面试通关手册,助我闯进字节跳动

算法刷题LeetCode中文版(为例)

人与人存在很大的不同,我们都拥有各自的目标,在一线城市漂泊的我偶尔也会羡慕在老家踏踏实实开开心心养老的人,但是我深刻知道自己想要的是一年比一年有进步。

最后,我想说的是,无论你现在什么年龄,位于什么城市,拥有什么背景或学历,跟你比较的人永远都是你自己,所以明年的你看看与今年的你是否有差距,不想做咸鱼的人,只能用尽全力去跳跃。祝愿,明年的你会更好!

由于篇幅有限,下篇的面试技术攻克篇只能够展示出部分的面试题,详细完整版以及答案解析,有需要的可以关注

  • 18
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值