前言
vite作为我们前端开发工具利器,在开发时,作为开发服务器时提供极速的启动体验、调试接口时提供代理服务器功能,开发时提供HMR能力支持修改无感刷新,提供兼容rollup的打包生态,提供方便扩展的插件系统等等,这些是怎么做的呢,本文在学习了文档&源码后与大家探讨一下 vite 以下功能的实现思路:
- 开发服务器
- proxy代理
- 预构建
- hmr
- 插件系统
前置知识
- 浏览器原生支持 原生esm,当碰到 import 一个(非本地)模块时,浏览器会发一个请求(服务器收到请求后怎么处理都完全可控),如下:
-
熟悉使用vite-plugin-inspect插件,可以方便查看开发时的代码处理中间产物及最终返回
-
nodejs 简单 http.createServer/express/koa demo基础,了解中间件
开发服务器
开发-服务器,其实就是一个简单的服务器,想一下express demo,能接收请求,自定义 返回(这就为所欲为 了啊);vite中直接用node:http/s
启一个服务,用 connect 做中间件容器,处理浏览器的请求:
搜下 middlewares.use
,主要的各种处理逻辑入口找到了:
其中主要负责处理文件请求的是transformMiddleware
中间件,里面主要的逻辑是:【解析路径-resolve -> 加载文件-load -> 代码转译-transform(完全可控)】(这里会定好处理工作流,做插件系统,后面说到)-> 返回处理结果(主要返回浏览器支持的es6模块,不要被文件后缀名迷惑,浏览器是根据Content-Type响应头(可控)来处理响应的)
开发服务器之代理proxy
代理有正向代理和反向代理之分,代理客户端的是正向代理,代理服务器的是反向代理(不清楚的再问问ai)。
开发服务器配置代理主要是解决开发时前后端调试接口时碰到cors报错(欢迎指教),因为cors只会在浏览器上起作用,在服务端发请求没有效果。
vite用的代理是用http-proxy模块开启正向代理,简单demo如下:
vite 中代理逻辑主要在proxyMiddleware
中间件里,用法如下(关注红圈部分):
在vite中也可以访问http-proxy
实例(如下),在需要修改请求或响应头时有用,比如对接第三方时返回302重定向到location头,这时可以改写location头为开发环境地址。
看了下
http-proxy
源码,本质上就是从node端发http请求(用啥都行axios/fetch等等),http-proxy
这里用的是 http 模块的request方法发起代理请求)
依赖预构建
vite服务启动时先执行,在特定情况重新执行预构建,只在开发服务时执行。
原因:
- cjs转esm:把commonjs/umd模块的依赖转为esm模块
- 提高后续页面的加载性能:把具有多个依赖的esm依赖项打包成单个模块,例如 lodash-es
思路原理:
- 先找出依赖,一般是bare import,比如
import { createApp } from "vue";
这种:先从html入口开始,解析出bare import格式的导入(正则匹配或者ast遍历); - 解析出依赖在node_modules中的入口地址:有多种模块解析算法,也可以看看typescript的介绍,vite的开发node服务器中应该是和nodejs一样
- 把依赖入口地址作为入口,bundle(打包)输出一个esm格式的文件:通过esbuild.build方法打包,format设置为'esm',bundle设置为true(这样会把入口引用的模块打包到产物中)等等,打包结果写到node_modules/.vite目录下
- 重写依赖的导入路径:通过 import-analysis 插件改写模块路径为真实路径
hmr
思路:
- 监听到变化的模块:用 chokidar
- 当一个模块更新时,找出需要更新的模块(找出热更新边界、例如导入该模块的模块也更新)
- 通知客户端更新模块
- 怎么通知:用websocket通知需要更新的模块的信息(能通知到就行,用啥不重要)
- 怎么更新:首先要发请求获取更新后的模块(客户端收到websocket通知就可以发请求啦,浏览器请求后会自动执行),然后我们来脑暴下更新的模块有哪几种情况,对于每一种情况需要怎么更新,下面以vue demo项目中的模块为例:
-
js模块
- 有导出:这种情况需要更新导入该模块的模块
- 没有导出,导入运行即可:这种情况直接请求更新后的模块即可
-
资源文件:在vite中静态资源的import,一般会把静态资源的路径导出返回,这时更新导入该资源的模块即可
-
sfc文件:这个模块更新是最重要的,因为上面的模块通常都是被sfc文件导入,而sfc文件会被编译为导出render函数的js模块,本身不会执行全局的逻辑,如下(关注红线框),那重新请求更新的模块后,还得应用才行
-
下面看看vite中是怎么应用sfc模块的更新:先随便改下文件保存,看下浏览器network里的新请求的initiator,这更新的路径就出来了,点进去定位到源码:
接下来打个断点,再随便改点东西保存,然后会发现流程和我们推想的差不多
- 第一步先发请求获取最新模块,这时模块代码会运行,通过import.meta.hot.accept注册回调(import.meta.hot.accept代码是vite-vue插件注入的)
- 第二步触发import.meta.hot.accept注册的回调,sfc组件的更新也就是在这里更新的
可以看到最终sfc最终是通过(__VUE_HMR_RUNTIME__.rerender(updated.__hmrId, updated.render);)
重渲染组件的(rerender函数在vue源码的runtime-core/src/hmr.ts里,最终通过调用update函数重渲染组件(如下截图)-> update()函数通过重新执行响应式effect来触发更新-> effect里实际运行的是componentUpdateFn函数,里面调用了patch执行更新(vdom diff过程))
插件系统
在vite开发/构建过程中,某些特定时机触发的函数集合(钩子)(类比vue生命周期钩子,但不单单是切面独立的逻辑,插件钩子可接收特定的参数,并产生特定的结果影响最终结果)
要理解插件(钩子集),就要理解vite整体工作流水线,主体大概就是
- 起一个 开发-服务器(就是一个简单的服务器,想一下express demo,能接收请求,自定义 返回),然后根据浏览器的请求信息(定好的格式)开始 ->
2.(解析路径resolve、加载文件 load、代码转译transform)(完全可控) -> - 返回处理结果(浏览器支持的es6模块/资源,不要被文件后缀名迷惑,浏览器是根据Content-Type响应头(可控)来处理响应的);
- 然后各种钩子就是作者在这个流水线中把有意义的点暴露出来,对于同一种钩子,可能有很多个,那么执行顺序呢,简单的话可以 按顺序-全部-执行,但根据钩子功能,也可以考虑异步-并行/串行,不全部-执行(比如解析路径的钩子,有结果就行,不需要全部执行)等等
同理的话,所有库插件的编写也如此,对于作者,最重要的是理清 工作流水线(要怎么干),暴露好钩子;对于开发者,最重要的是理解工作流水线(是干什么的)