vite——纵享丝滑开发体验

avatar
@智云健康

作者:maxin,未经授权禁止转载。

简介

vite发音[veet],在法语中是轻量、轻快的意思

开发环境通过koa启动本地服务,生产环境通过Rollup打包构建

特点

  • dev server启动时间不超过300ms
  • 模块热替换(HMR)更新不超过100ms
  • 通过 esbuild 来支持TS/JSX的转化
  • 按需加载

esbuild

esbuild底层使用的golang进行编写的,在对比传统web构建工具的打包速度上,具有明显的优势。编译Typescript的速度远超官方的tsc

对比vue-cli

vue3和包括10个组件的前提下,vite在开发服务器启动时间上相较于vue-cli速度提升幅度较大

传统打包工具的dev-server

  • 本地运行前需要加载所有模块文件并导出bundle才能展示页面
    • 包括对每个文件导入/导出关系的解析
    • 将各个模块排序、重写、关联
  • 应用越大,开发服务的启动速度也越慢
  • 代码分割只针对于生产环境构建

基于浏览器ES module的dev-server

  • <script type="module">
  • 浏览器可以解析ES module的import并发送http请求
  • dev server拦截浏览器对模块的请求并返回处理后的结果 <script type="module"> ES Module的特点:
  • 模块代码只在加载后执行
  • 模块引用相同js只加载一次
  • 模块是单例
  • 模块可以请求加载其他模块
  • 支持循环依赖
  • 默认在严格模式下执行
  • 不共享全局命名空间
  • 模块顶级this的值是undefined
  • 模块var声明不会添加到window

目录结构

$ tree -L 3 -I 'node_modules' ./src
./src
├── client # 客户端运行代码,主要是客户端的 socket 通信以及 HMR 相关
│   ├── client.ts
│   ├── env.d.ts
│   ├── tsconfig.json
│   └── vueJsxCompat.ts
├── hmrPayload.ts # HMR 类型定义
└── node # 服务端运行代码
    ├── build # vite build 命令运行代码
    │   ├── buildPluginAsset.ts
    │   ├── buildPluginCss.ts
    │   ├── buildPluginEsbuild.ts
    │   ├── buildPluginHtml.ts
    │   ├── buildPluginManifest.ts
    │   ├── buildPluginReplace.ts
    │   ├── buildPluginResolve.ts
    │   ├── buildPluginWasm.ts
    │   └── index.ts
    ├── cli.ts
    ├── config.ts
    ├── esbuildService.ts # esbuild 相关代码
    ├── index.ts
    ├── optimizer # 预优化
    │   ├── entryAnalysisPlugin.ts
    │   ├── index.ts
    │   └── pluginAssets.ts
    ├── resolver.ts # 模块加载逻辑
    ├── server # koa服务使用的一些中间件
    │   ├── index.ts
    │   ├── serverPluginAssets.ts
    │   ├── serverPluginClient.ts
    │   ├── serverPluginCss.ts # 处理css和其他css预处理器文件
    │   ├── serverPluginEnv.ts
    │   ├── serverPluginEsbuild.ts
    │   ├── serverPluginHmr.ts # 服务端热模块替换相关
    │   ├── serverPluginHtml.ts
    │   ├── serverPluginJson.ts
    │   ├── serverPluginModuleResolve.ts # 处理 /@modules/:id 开头请求的文件
    │   ├── serverPluginModuleRewrite.ts
    │   ├── serverPluginProxy.ts 
    │   ├── serverPluginServeStatic.ts # koa静态服务器
    │   ├── serverPluginSourceMap.ts
    │   ├── serverPluginVue.ts # 处理.vue文件
    │   ├── serverPluginWasm.ts
    │   └── serverPluginWebWorker.ts
    ├── transform.ts
    ├── tsconfig.json
    └── utils
        ├── babelParse.ts
        ├── createCertificate.ts
        ├── cssUtils.ts
        ├── fsUtils.ts
        ├── index.ts
        ├── openBrowser.ts
        ├── pathUtils.ts
        ├── resolveVue.ts
        ├── shims.d.ts
        └── transformUtils.ts

vite做了些什么?

  • $ vite
  • 执行package.json的bin字段对应的js
  • createServer
  • 初始化koa、watcher、resolver
  • 读取配置,合并到koa的context上
  • 通过koa middlewares的方式使用一堆plugins
  • 预优化用户项目里dependencies依赖的模块文件

koa中间件

vite中使用koa中间件的模式来处理请求到的不同类型文件,中间件的执行顺序类似于下图洋葱模型

来看一个简单的例子

执行顺序如下:

// 第一层洋葱 - 开始
// 第二层洋葱 - 开始
// 第三层洋葱 - 开始
// 第三层洋葱 - 结束
// 第二层洋葱 - 结束
// GET / - 2ms
// 第一层洋葱 - 结束
// 第一层洋葱 - 开始

请求拦截

  • vite中使用的koa中间件
  • 每个plugin都是一个函数
  • 执行后use一个或多个中间件
  • 在请求、响应的时候对资源文件进行解析、重写处理
  • 返回给浏览器可直接执行的文件

如果import后面的路径不是以 ../../开头,浏览器不认识怎么办?

浏览器访问的时候,路径前缀已经被处理成//@module/:id

开启--debug模式,可以看到vite:rewrite处理的路径重写

moduleRewritePlugin的作用:

  • 对js文件进行拦截,包括*.vue文件中的<template><script>
  • 把文件通过可读流转化成字符串内容并读取
  • 使用esbuild提供的es-module-lexer进行词法分析
  • 使用magic-string来替换import中无法识别的路径
    • 裸模块导入 vue -> @module/vue.js
    • 相对路径导入 ./App.vue -> /src/App.vue
  • 重写后的字符串拼到ctx.body上返回给下一个中间件
  • 将ctx.path和重写后的文件拼接作为rewriteCache的key,文件作为value进行缓存
  • 改写后的路径会再次向服务器拦截请求
  • 下次path+file没变的话直接返回file
  • rewriteCache是lru-cache生成的类似Map的数据结构

rewriteImports方法

resolveImport方法

解析node_modules

moduleResolvePlugin的作用:

  • 中间件拦截/@modules/开头的ctx.path
  • 若用户项目dependencies没有vue,就用vite的dependencies里的@vue
  • 若moduleIdToFileMap里有id对应的文件路径,读取并返回文件
  • 若预优化目录node_modules/.vite_opt_cache有id,读取并返回文件
  • 都没有的话从node_modules下解析模块id里package.json的module属性对应的文件路径,读取并返回文件

*.vue文件怎么处理?

  • 通过vuePlugin中间件来解析处理
  • 使用@vue/compiler-sfc解析vue文件,重写成新的script以es module形式导出
  • 浏览器解析后再次异步请求带有query参数的请求处理template
  • 使用@vue/compiler-dom编译,导出render函数处理style
  • 借助热更新updateStyle并导出JSON字符串形式的css内容

处理后的style文件

处理后的template文件

解析后的结果可以直接在createApp方法中进行使用

vite.config.js

目前vite文档还没有确定具体怎么配置config文件,不过我们可以从代码中看到

大致包括proxyconfigureServer

HMR

当request.path 路径是 /vite/client 时、请求得到对应的客户端代码,在客户端中创建了一个websocket服务并与服务端建立了连接,通过chokidar这个库创建watcher实例,监听文件变化,不同的消息触发一些事件做到浏览器端的即时热模块更换

一些优化

启动服务前的预优化

启动服务前,会进行预优化,把用户项目的npm依赖打包到node_module下的.vite_opt_cache目录缓存下来,这些js文件里不存在import,不用发起多次请求,利用http缓存可以提高读取node_modules里模块的加载速度

执行vite命令后直接安装npm包,无需重启服务。

先启动服务器,再安装lodash-es,跳过预优化,观察network发现有600多次请求

启动服务器前安装lodash-es,预优化后请求数明显减少

ctx.read方法会从内存中读取已缓存文件

node_modules模块第一次解析后缓存到内存中,不用每次从磁盘中读取

http2/https

执行 vite --https 会开启http2协议,多个请求都是通过一个 TCP 连接并发完成

总结

vite在浏览器端使用 export import 的方式导入和导出模块,同时实现了按需加载。vite高度依赖module script特性。目前还是rc版本,项目还在飞速迭代中,可以自己先体验起来了。