关于javascript:浅谈-Vite-20-原理依赖预编译插件机制是如何兼容-Rollup-的

前言

Hi,我是 ssh,春节过来了,躁动的心也该收一收,开始新一年的学习了。我目前就任于字节跳动的 Web Infra 团队,目前团队还很缺人(尤其是北京)。

为此我组建了一个气氛特地好的招聘社群,大家在外面纵情的探讨面试相干的想法和问题,也欢送你退出,随时投递简历给我。

在字节跳动,大家都会自发的钻研社区前沿的技术,尝试接入外部的我的项目并寻找晋升开发效率的可能性,这种谋求极致敢于创新也是被激励的,也有不少同学曾经在尝试把新一代的构建工具 Vite 和 snowpack 引入应用。

这篇文章我想简略的聊聊,让尤老师如此专一的 Vite 2.0 到底有哪些亮点。

前几天,尤雨溪在各个社交平台发表 Vite 2.0 公布了。

看得出他对 Vite 倾泻了很多感情,甚至都冷清了 Vue3,停更了两个多月。

相干的中文布告曾经有翻译了,能够在尤雨溪的知乎文章:Vite 2.0 公布了中查看。

这篇文章来谈谈 Vite 2.0 的公布中,几个让我比拟感兴趣的技术点。

Vite 原理

为什么会呈现 Vite?在过来的 Webpack、Rollup 等构建工具的时代,咱们所写的代码个别都是基于 ES Module 标准,在文件之间通过 importexport 造成一个很大的依赖图。

这些构建工具在本地开发调试的时候,也都会提前把你的模块先打包成浏览器可读取的 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 此时在利用外部的一系列黑魔法,包含 Vue 的 template 解析,代码的编译等等,解析成浏览器能够执行的 js 文件返回到浏览器端。

这就保障了只有在真正应用到这个模块的时候,浏览器才会申请并且解析这个模块,最大水平的做到了按需加载。

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

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

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

依赖预编译

依赖预编译,其实是 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 倍。

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))
}

scanImports 其实就是利用 Esbuild 构建时提供的钩子去扫描文件中的依赖,收集到 deps 变量里,在扫描到入口文件(比方 index.html)中依赖的模块后,造成相似这样的依赖门路数据结构:

{
  "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,第一反馈就是生态不够成熟,其余构建工具有那么多的第三方插件,提供了各种各样开箱即用的便捷性能,Vite 须要多久能力赶上呢?

Vite 从 preact 的 WMR 中失去了启发,把插件机制做成兼容 Rollup 的格局。

于是便有了这个相亲相爱的 LOGO:

目前和 vite 兼容或者内置的插件,能够查看vite-rollup-plugins。

简略的介绍一下 Rollup 插件,其实插件这个货色,就是 Rollup 对外提供一些机会的钩子,还有一些工具办法,让用户去写一些配置代码,以此染指 Rollup 运行的各个机会之中。

比方在打包之前注入某些货色,或者扭转某些产物构造,仅此而已。

而 Vite 须要做的就是基于 Rollup 设计的接口进行扩大,在保障 Rollup 插件兼容的可能性的同时,再退出一些 Vite 特有的钩子和属性来扩大。

举个简略的例子,@rollup/plugin-image 能够把图片模块解析成 base64 格局,它的源码其实很简略:

export default function image(opts = {}) {
  const options = Object.assign({}, defaults, opts)
  const filter = createFilter(options.include, options.exclude)

  return {
    name: 'image',

    load(id) {
      if (!filter(id)) {
        return null
      }

      const mime = mimeTypes[extname(id)]
      if (!mime) {
        // not an image
        return null
      }

      const isSvg = mime === mimeTypes['.svg']
      const format = isSvg ? 'utf-8' : 'base64'
      const source = readFileSync(id, format).replace(/[\r\n]+/gm, '')
      const dataUri = getDataUri({ format, isSvg, mime, source })
      const code = options.dom
        ? domTemplate({ dataUri })
        : constTemplate({ dataUri })

      return code.trim()
    }
  }
}

其实就是在 load 这个钩子,读取模块时,把图片转换成相应格局的 data-uri,所以 Vite 只须要在读取模块的时候,也去兼容执行相干的钩子即可。

尽管 Vite 很多行为和 Rollup 构建不同,但他们外部有很多类似的行为和机会,只有确保 Rollup 插件只应用了这些共有的钩子,就很容易做到插件的通用。

能够参考 Vite 官网文档 —— 插件局部

一般来说,只有一个 Rollup 插件合乎以下规范,那么它应该只是作为一个 Vite 插件:

  • 没有应用 moduleParsed 钩子。
  • 它在打包钩子和输入钩子之间没有很强的耦合。
  • 如果一个 Rollup 插件只在构建阶段有意义,则在 build.rollupOptions.plugins 下指定即可。

Vite 前面的指标应该也是尽可能和 Rollup 相干的插件生态买通,社区也会一起贡献力量,心愿 Vite 的生态越来越好。

比拟

和 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 我的项目。
  • 轻量级的一些企业我的项目。

也衷心祝福 Vite 的生态越来越好,独特迎接这个构建的新世代。

不过到那个时候,我可能还会挺思念从前 Webpack 思念构建的时候,那几分钟不苟言笑的摸鱼时刻 ????。

感激

欢送各路前端俊杰加我 sshsunlight 交个敌人,也欢送随时找我投递简历。

【腾讯云】轻量 2核2G4M,首年65元

阿里云限时活动-云数据库 RDS MySQL  1核2G配置 1.88/月 速抢

本文由乐趣区整理发布,转载请注明出处,谢谢。

您可能还喜欢...

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据