likes
comments
collection
share

研究vite心得

作者站长头像
站长
· 阅读数 8

前言

vite作为我们前端开发工具利器,在开发时,作为开发服务器时提供极速的启动体验、调试接口时提供代理服务器功能,开发时提供HMR能力支持修改无感刷新,提供兼容rollup的打包生态,提供方便扩展的插件系统等等,这些是怎么做的呢,本文在学习了文档&源码后与大家探讨一下 vite 以下功能的实现思路:

  • 开发服务器
  • proxy代理
  • 预构建
  • hmr
  • 插件系统

前置知识

  1. 浏览器原生支持 原生esm,当碰到 import 一个(非本地)模块时,浏览器会发一个请求(服务器收到请求后怎么处理都完全可控),如下:

研究vite心得

  1. 熟悉使用vite-plugin-inspect插件,可以方便查看开发时的代码处理中间产物及最终返回

  2. nodejs 简单 http.createServer/express/koa demo基础,了解中间件

开发服务器

开发-服务器,其实就是一个简单的服务器,想一下express demo,能接收请求,自定义返回(这就为所欲为了啊);vite中直接用node:http/s启一个服务,用 connect 做中间件容器,处理浏览器的请求:

研究vite心得

搜下 middlewares.use,主要的各种处理逻辑入口找到了:

研究vite心得

其中主要负责处理文件请求的是transformMiddleware中间件,里面主要的逻辑是:【解析路径-resolve -> 加载文件-load -> 代码转译-transform(完全可控)】(这里会定好处理工作流,做插件系统,后面说到)-> 返回处理结果(主要返回浏览器支持的es6模块,不要被文件后缀名迷惑,浏览器是根据Content-Type响应头(可控)来处理响应的)

研究vite心得

研究vite心得

开发服务器之代理proxy

代理有正向代理和反向代理之分,代理客户端的是正向代理,代理服务器的是反向代理(不清楚的再问问ai)。

开发服务器配置代理主要是解决开发时前后端调试接口时碰到cors报错(欢迎指教),因为cors只会在浏览器上起作用,在服务端发请求没有效果。

vite用的代理是用http-proxy模块开启正向代理,简单demo如下:

研究vite心得

vite 中代理逻辑主要在proxyMiddleware中间件里,用法如下(关注红圈部分):

研究vite心得

在vite中也可以访问http-proxy实例(如下),在需要修改请求或响应头时有用,比如对接第三方时返回302重定向到location头,这时可以改写location头为开发环境地址。

研究vite心得

看了下http-proxy源码,本质上就是从node端发http请求(用啥都行axios/fetch等等),http-proxy这里用的是 http 模块的request方法发起代理请求)

依赖预构建

vite服务启动时先执行,在特定情况重新执行预构建,只在开发服务时执行。

原因:

  1. cjs转esm:把commonjs/umd模块的依赖转为esm模块
  2. 提高后续页面的加载性能:把具有多个依赖的esm依赖项打包成单个模块,例如 lodash-es

思路原理:

  1. 先找出依赖,并解析出依赖入口地址,一般是bare import,比如import { createApp } from "vue";这种:先从html入口开始,解析出bare import格式的导入,vite中是用esbuild.context打包来获取bare import的,因为打包过程会涉及解析所有关联的import语句,vite通过编写一个esbuil插件,用onResolve钩子,在打包过程中,用filter过滤出bare import,然后记录下来bare import 的解析结果信息(能提取到就行,你用正则匹配或者其他办法也行,关键要快); 研究vite心得

研究vite心得

模块解析:有多种模块解析算法,也可以看看typescript的介绍,vite的开发node服务器中应该是和nodejs一样

  1. 把依赖入口地址作为入口,bundle(打包)输出一个esm格式的文件:通过esbuild.build方法打包,format设置为'esm',bundle设置为true(这样会把入口引用的模块打包到产物中)等等,打包结果写到node_modules/.vite目录下
  2. 重写依赖的导入路径:通过 import-analysis 插件改写模块路径为真实路径

研究vite心得

hmr

思路:

  1. 监听到变化的模块:用 chokidar
  2. 当一个模块更新时,找出需要更新的模块(找出热更新边界、例如导入该模块的模块也更新)
  3. 通知客户端更新模块
    1. 怎么通知:用websocket通知需要更新的模块的信息(能通知到就行,用啥不重要)
    2. 怎么更新:首先要发请求获取更新后的模块(客户端收到websocket通知就可以发请求啦,浏览器请求后会自动执行),然后我们来脑暴下更新的模块有哪几种情况,对于每一种情况需要怎么更新,下面以vue demo项目中的模块为例:
      1. js模块

        1. 有导出:这种情况需要更新导入该模块的模块
        2. 没有导出,导入运行即可:这种情况直接请求更新后的模块即可
      2. 资源文件:在vite中静态资源的import,一般会把静态资源的路径导出返回,这时更新导入该资源的模块即可 研究vite心得

      3. sfc文件:这个模块更新是最重要的,因为上面的模块通常都是被sfc文件导入,而sfc文件会被编译为导出render函数的js模块,本身不会执行全局的逻辑,如下(关注红线框),那重新请求更新的模块后,还得应用才行 研究vite心得

下面看看vite中是怎么应用sfc模块的更新:先随便改下文件保存,看下浏览器network里的新请求的initiator,这更新的路径就出来了,点进去定位到源码:

研究vite心得

研究vite心得

接下来打个断点,再随便改点东西保存,然后会发现流程和我们推想的差不多

  1. 第一步先发请求获取最新模块,这时模块代码会运行,通过import.meta.hot.accept注册回调(import.meta.hot.accept代码是vite-vue插件注入的)
  2. 第二步触发import.meta.hot.accept注册的回调,sfc组件的更新也就是在这里更新的

研究vite心得

研究vite心得

可以看到最终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心得

研究vite心得

插件系统

在vite开发/构建过程中,某些特定时机触发的函数集合(钩子)(类比vue生命周期钩子,但不单单是切面独立的逻辑,插件钩子可接收特定的参数,并产生特定的结果影响最终结果)

要理解插件(钩子集),就要理解vite整体工作流水线,主体大概就是

  1. 起一个 开发-服务器(就是一个简单的服务器,想一下express demo,能接收请求,自定义返回),然后根据浏览器的请求信息(定好的格式)开始 -> 2.(解析路径resolve、加载文件 load、代码转译transform)(完全可控) ->
  2. 返回处理结果(浏览器支持的es6模块/资源,不要被文件后缀名迷惑,浏览器是根据Content-Type响应头(可控)来处理响应的);
  • 然后各种钩子就是作者在这个流水线中把有意义的点暴露出来,对于同一种钩子,可能有很多个,那么执行顺序呢,简单的话可以 按添加顺序-全部-执行,但根据钩子功能,也可以考虑异步-并行/串行,不全部-执行(比如解析路径的钩子,有结果就行,不需要全部执行)(如何管理不同的钩子和相同钩子的执行模式,Tapable就是专门提供插件钩子支持的)等等

同理的话,所有库插件的编写也如此,对于作者,最重要的是理清工作流水线(要怎么干),暴露好钩子;对于开发者,最重要的是理解工作流水线(是干什么的)

参考

转载自:https://juejin.cn/post/7355431482687619123
评论
请登录