令人耳目一新的Vite

1,636 阅读7分钟

Vite初探

85897c14eb3190625e91a13c0bac5180.jpeg

最近公司开发的项目用到了Vite+vue3+TypeScript,作为一个“优秀”的前端攻城狮那还是得紧跟公司步伐,工作之余还是还是得kubi的来学习学习尤大的新东西,当然咱们也是从零开始搞这个东西,都说Viteimage.png,那我得看看到底 image.png在了那里。 话不多说首先打开Vite.js中文官网,毕竟是尤大开发的,官方文档对国人还是很友好的👌。 image.png

好家伙,打开就看到这醒目的大字:下一代前端开发与构建工具,那我就要紧跟🐮🍺,做🐮🍺的前端攻城狮!!!

首先Vite由一个模块热更新(HMR)开发服务器和使用 Rollup 打包代码,预配置输出高度优化的静态资源用于生产的一套构建指令。Vite 意在提供更开箱即用的配置,同时它的 插件 APIJavaScript API 带来了高度的可扩展性,并完全支持类型化。

浏览器支持

  • 开发环境中:Vite 需要在支持 原生 ES 模块动态导入 的浏览器中使用,所以低级浏览器就赶紧拜拜吧。
  • 生产环境中:默认支持的浏览器需要支持 通过脚本标签来引入原生ES模块 。可以通过官方插件 @vitejs/plugin-legacy 支持旧浏览器。

搭建项目并运行

使用npm

$ npm init @vitejs/app myViteApp && cd myViteApp && npm run dev

使用yarn

$ yarn create vite-app myViteApp && cd myViteApp && yarn && yarn dev

image.png

目录结构

|-- public
|-- src
| |-- assets
| |-- components
| |-- App.vue
| |-- index.css
| |-- main.js
|-- .gitgnore
|-- index.html
|-- package.json  

Vite 帮我们生成的目录结构很简洁,主要文件和 vue-cli 的文件都是一样的,vite简单、高效、强大, 在学习 vue3 的时候 就不用各种搭环境了

Vite 也支持多个 .html 作入口点的 多页面应用模式

命令行接口

在安装了 Vite 的项目中,可以在 npm scripts 中使用 vite 可执行文件,或者直接使用 npx vite 运行它。下面是通过脚手架创建的 Vite 项目中默认的 npm scripts:

{
  "scripts": {
    "dev": "vite", // 启动开发服务器
    "build": "vite build", // 为生产环境构建
    "serve": "vite preview" // 本地预览生产构建产物
  }
}

可以指定额外的命令行选项,如 --port 或 --https。运行 npx vite --help 获得完整的命令行选项列表。

Vite原理分析

首先看一下Vite帮我们做什么事情,怎么样做的。

先看看index.html

image.png

再看一下src/main.ts,我这里在创建时选的是TS,用js的小伙伴创建时选择一下就可以了。

image.png

index.html中引入了src/main.tsmain.ts引入App.vue并挂在到html中,流程简单的不行,打开浏览器组件也确实渲染出来了。

这一步的实现 离不开Esmodules , 浏览器通过<script module>,为每个导入生成HTTP请求, vitedev服务拦截http请求,并把代码做一些转换之后返回给浏览器进行渲染。

image.png

这是通过vite构建后请求的main.ts文件

简单来说就是 Vite通过node编译静态资源 返回给浏览器渲染

为什么他会启动的会快?

在过去的 WebpackRollup 等构建工具的时代,我们所写的代码一般都是基于 ES Module 规范,在文件之间通过 import export 形成一个很大的依赖图。

这些构建工具在本地开发调试的时候,也都会提前把你的模块先打包成浏览器可读取的 js bundle,虽然有诸如路由懒加载等优化手段,但懒加载并不代表懒构建,Webpack 还是需要把你的异步路由用到的模块提前构建好。

当你的项目越来越大的时候,启动也难免变的越来越慢,甚至可能达到分钟级别。而 HMR 热更新也会达到好几秒的耗时。

Vite 则别出心裁的利用了浏览器的原生 ES Module 支持,直接在 html 文件里写诸如这样的代码:

// index.html
<div id="app"></div>
<script type="module">
  import { createApp } from 'vue'
  import Main from './Main.vue'

  createApp(Main).mount('#app')
</script>

Vite 会在本地帮你启动一个服务器,当浏览器读取到这个html文件之后,会在执行到 import 的时候才去向服务端发送 Main.vue 模块的请求,Vite 此时在利用内部的一系列黑魔法,包括 Vuetemplate 解析,代码的编译等等,解析成浏览器可以执行的 js 文件返回到浏览器端。

这就保证了只有在真正使用到这个模块的时候,浏览器才会请求并且解析这个模块,最大程度的做到了按需加载。

Vite 官网上的图来解释,传统的 bundle 模式是这样的:

image.png

而基于 ESM 的构建模式则是这样的:

image.png

灰色部分是暂时没有用到的路由,甚至完全不会参与构建过程,随着项目里的路由越来越多,构建速度也不会变慢。

依赖预编译

依赖预编译,其实是 Vite 2.0 在为用户启动开发服务器之前,先用 esbuild 把检测到的依赖预先构建了一遍。

也许你会疑惑,不是一直说好的 no-bundle 吗,怎么还是走启动时编译这条路线了?尤老师这么做当然是有理由的,我们先以导入lodash-es这个包为例。

当你用 import { debounce } from 'lodash' 导入一个命名函数的时候,可能你理想中的场景就是浏览器去下载只包含这个函数的文件。但其实没那么理想,debounce 函数的模块内部又依赖了很多其他函数,形成了一个依赖图。

当浏览器请求 debounce 的模块时,又会发现内部有 2 个 import,再这样延伸下去,这个函数内部竟然带来了 600 次请求,耗时会在 1s 左右。

这当然是不可接受的,于是尤老大想了个折中的办法,正好利用 Esbuild 极快的构建速度,让你在没有感知的情况下在启动的时候预先帮你把 debounce 所用到的所有内部模块全部打包成一个传统的 js bundle。

Esbuild 使用 Go 编写,并且比以 JavaScript 编写的打包器预构建依赖快 10-100 倍。

image.png

在 httpServer.listen 启动开发服务器之前,会先把这个函数劫持改写,放入依赖预构建的前置步骤,Vite 启动服务器相关代码。

// server/index.ts
const listen = httpServer.listen.bind(httpServer)
httpServer.listen = (async (port: number, ...args: any[]) => {
  try {
    await container.buildStart({})
    // 这里会进行依赖的预构建
    await runOptimize()
  } catch (e) {
    httpServer.emit('error', e)
    return
  }
  return listen(port, ...args)
}) as any

而 runOptimize 相关的代码则在 Github optimizer 中。

首先会根据本次运行的入口,来扫描其中的依赖:

let deps: Record<string, string>, missing: Record<string, string> if (!newDeps) { ;({ deps, missing } = await scanImports(config)) }

{
  "lodash-es": "node_modules/lodash-es"
}

之后再根据分析出来的依赖,使用 Esbuild 把它们提前打包成单文件的 bundle。

const esbuildService = await ensureService()
await esbuildService.build({
  entryPoints: Object.keys(flatIdDeps),
  bundle: true,
  format: 'esm',
  external: config.optimizeDeps?.exclude,
  logLevel: 'error',
  splitting: true,
  sourcemap: true,
  outdir: cacheDir,
  treeShaking: 'ignore-annotations',
  metafile: esbuildMetaPath,
  define,
  plugins: [esbuildDepPlugin(flatIdDeps, flatIdToExports, config)]
})

在浏览器请求相关模块时,返回这个预构建好的模块。这样,当浏览器请求 lodash-es 中的 debounce 模块的时候,就可以保证只发生一次接口请求了。

你可以理解为,这一步和 Webpack 所做的构建一样,只不过速度快了几十倍。

在预构建这个步骤中,还会对 CommonJS 模块进行分析,方便后面需要统一处理成浏览器可以执行的 ES Module。

比较

和 Vite 同时期出现的现代化构建工具还有:

  • Snowpack - The faster frontend build tool
  • preactjs/wmr: 👩‍🚀 The tiny all-in-one development tool for modern web apps.
  • Web Dev Server: Modern Web

Snowpack

Snowpack 和 Vite 比较相似,也是基于 ESM 来实现开发环境模块加载,但是它的构建时却是交给用户自己选择,整体的打包体验显得有点支离破碎。

而 Vite 直接整合了 Rollup,为用户提供了完善、开箱即用的解决方案,并且由于这些集成,也方便扩展更多的高级功能。

WMR

WMR 则是为 Preact 而生的,如果你在使用 Preact,可以优先考虑使用这个工具。

@web/dev-server

这个工具并未提供开箱即用的框架支持,也需要手动设置 Rollup 构建配置,不过这个项目里包含的很多工具也可以让 Vite 用户受益。

更具体的比较可以参考Vite 文档 —— 比较

总结

Vite作为一个尤老大提出来要代替webpack的一个充满魔力的构建工具当然有他独到之处,技术的日新月异早已不是能令人奇怪的了,掌握前沿技术优化项目解决方案是我们所必备的。但是Webpack 在上个世代也是一个贡献很大的构建工具,只是由于新特性的出现,有了可以解决它的诟病的解决方案。

目前我个人觉得,一些轻型的项目(不需要一些特别奇怪的依赖构建)完全可以开始尝试 Vite,比如: 轻量级的企业项目(比如本猿公司正在开发的项目)各种框架、库中的展示 demo 项目。

在最后,欢迎各位大佬批评指正,积极认错(死不悔改🤪)。