共计 4861 个字符,预计需要花费 13 分钟才能阅读完成。
前言
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 标准,在文件之间通过 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 此时在利用外部的一系列黑魔法,包含 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 交个敌人,也欢送随时找我投递简历。