myblog
myblog copied to clipboard
Vite笔记
认识javascript modules
Vite
主要是用了 js modules
,在认识 vite
之前,我们先简单学习一下 javascript modules
。 MDN 的 javascript modules
文档 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#aside_%E2%80%94_.mjs_versus_.js
简单的说,支持javascript modules
的浏览器,可以通过script
标签引入一个 esm
模块,并且支持其中的import
和 export
语法,在解析到对应的语法后会在浏览器中动态加载对应的js文件。
我们写一个简单的测试应用来理解什么是 javascript modules
,代码都在这里 https://github.com/lihongxun945/javascript-modules-test
入口文件 index.html
,通过script
标签加载一个模块:
<html>
<body>
<script type="module" src="./index.js"></script>
</body>
</html>
index.js
引用了另一个模块,并初始化:
import Person from './person.js';
const person = new Person();
person.js
文件直接导出一个 Person类:
export default function Person () {
console.log('person');
}
启动一个静态服务器,直接访问 index.html
,可以发现上述代码无需任何编译,在浏览器中可以直接运行。且浏览器会通过网络请求分别加载 index.js
和 person.js
认识vite
为什么需要vite
浏览器已经原生支持了 javascript modules
,为什么还要vite
呢?有以下几个原因:
- 对非JS文件,比如 css、图片等的支持
- 对非
esm
语法的支持 - 通过
bundle
优化性能,可以把多个小文件合并成大文件避免浏览器加载成百上千个文件 当然还有一些其他原因,比如生产环境打包、HMR、chunks等
vite 目录结构
vite
默认把index.html
放在了项目的根目录,这和我们的webpack
项目放在public
中有挺大区别。vite
官网上对这个设计做了解释,总结一下主要原因是对于使用 javascript modules
的项目来说,index.html
本来就是编译入口文件也是Server的根路径,也就是说既应该放在 src
也应该放在 public
中,所以干脆直接放在根目录,这样也不用写 PUBLIC_URL
之类的代码,既符合已有规范还方便,所以就这么写了。
那webpack
为啥不这样做? 因为 webpack
的编译入口文件其实是 index.js
而不是index.html
,而Server的根路径其实是编译后的 index.html
,所以就没这么设计。
加载js和CSS
示例中的 index.html
通过如下代码加载 main.tsx
:
<script type="module" src="/src/main.tsx"></script>
这直接用了 javascript modules
能力
加载的 main.tsx
源码是这样的:
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
)
TS显然无法被浏览器识别,而在浏览器中加载的文件已经经过了 vite的编译,编译后的代码如下:
var _jsxFileName = "/Users/hongxun.lhx/github/my-vue-app/src/main.tsx";
import __vite__cjsImport0_react from "/node_modules/.vite/react.js?v=8ca9e3e0"; const React = __vite__cjsImport0_react.__esModule ? __vite__cjsImport0_react.default : __vite__cjsImport0_react;
import __vite__cjsImport1_reactDom from "/node_modules/.vite/react-dom.js?v=8ca9e3e0"; const ReactDOM = __vite__cjsImport1_reactDom.__esModule ? __vite__cjsImport1_reactDom.default : __vite__cjsImport1_reactDom;
import "/src/index.css";
import App from "/src/App.tsx";
import __vite__cjsImport4_react_jsxDevRuntime from "/node_modules/.vite/react_jsx-dev-runtime.js?v=8ca9e3e0"; const _jsxDEV = __vite__cjsImport4_react_jsxDevRuntime["jsxDEV"];
ReactDOM.render(/* @__PURE__ */ _jsxDEV(React.StrictMode, {
children: /* @__PURE__ */ _jsxDEV(App, {}, void 0, false, {
fileName: _jsxFileName,
lineNumber: 8,
columnNumber: 5
}, this)
}, void 0, false, {
fileName: _jsxFileName,
lineNumber: 7,
columnNumber: 3
}, this), document.getElementById("root"));
我们来看一看代码是怎么编译的。
main.tsx
依赖的 React
和 ReactDOM
经过了 vite
的编译,且缓存在了 node_modules/.vite
目录中。这样做好处是只需要加载两个文件,如果不处理那么浏览器需要加载很多React
的依赖。根据官网的说明,vite启动时会自动分析node_modules
依赖并把他们都打包,所以并不会因为这些依赖太多导致浏览器加载大量js。
由于打包是在本地进行的,冷启动显然会受到影响,经过自己本地实际测试,冷启动有打包 React
相关依赖,热启动无需打包,启动速度分别是:
- 冷启动
435ms
- 热启动
236ms
虽然冷启动确实慢了一些,但是不得不说esbuild
打包React
只多用了100ms
,相比webpack
依然有大幅提升。
index.css
显然也会被编译成 JS,否则 import
会报错,我们看看编译后的 index.css
:
import { createHotContext as __vite__createHotContext } from "/@vite/client";
import.meta.hot = __vite__createHotContext("/src/index.css");
import { updateStyle, removeStyle } from "/@vite/client"
const id = "/Users/hongxun.lhx/github/my-vue-app/src/index.css"
const css = "body {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\n sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\n monospace;\n}\n"
updateStyle(id, css)
import.meta.hot.accept()
export default css
import.meta.hot.prune(() => removeStyle(id))
编译的结果和我们想象的差不多,这里有一个 updateStyle
方法,起作用就是通过style
标签把CSS插入到文档中,方法的实现如下:
function updateStyle(id, content) {
let style = sheetsMap.get(id);
{
if (style && !(style instanceof HTMLStyleElement)) {
removeStyle(id);
style = undefined;
}
if (!style) {
style = document.createElement('style');
style.setAttribute('type', 'text/css');
style.innerHTML = content;
document.head.appendChild(style);
}
else {
style.innerHTML = content;
}
}
sheetsMap.set(id, style);
}
因为我们是 import index.css
的写法,所以export default css
实际 并没啥用,如果是用CSS Module写法就有用了。
回到 main.tsx
,JSX
被编译成了JS是常规操作,对 App.tsx
的处理也和上面的逻辑类似。
静态资源
App.tsx
中加载了一张图片:
import logo from './logo.svg'
根据用Webpack
的经验,显然 logo.svg
也会被编译成JS,并返回文件地址,实际也确实是这样:
export default "/src/logo.svg"
Vite快在哪里?
认识了Vite
的基本原理之后,就可以明白vite为什么在本地开发那么快了。主要是基于以下几点:
- 本地只有冷启动的时候有资源打包,热启动完全无任何打包编译操作,只是启动了一个服务器,所以通常 0.2 秒就能完成启动
- 浏览器直接通过
esmodule
加载src文件,按需编译单个文件。 -
node_modules
依赖被打包成大文件,避免了浏览器加载多个js。 - 公共依赖开启缓存缓存,根本不用请求到 server。
- 最后也是最重要的:
esbuild
打包速度无敌快。
那么esbuild
为什么这么快呢?
官方是有解释的,可以看这里:https://esbuild.github.io/faq/#why-is-esbuild-fast
总结一下几个主要原因:
- GO语言的优势:esbuild是用go语言写的,是编译型语言,且针对多线程进行优化,而webpack是用JS写的,解释型且主要是单线程的特性注定他的性能比不过GO。
- 良好的并行优化:多线程优化,尽可能用到全部的CPU核心,且多线程可以共享内存数据。
- 从底层实现:自己实现了底层逻辑,没有依赖三方库。比如TS解析的时候如果用TS官方的编译器,需要检查类型,因此就会变慢;GO的静态类型速度也快于JS中的动态类型。
- 更小的内存使用:更小的内存数据,更少次数对JS进行遍历。
esbuild
有这么多优点,那么有没有缺点呢?
当然有的,esbuild
的快其实来源于两部分:一部分是 GO语言和多线程带来的优势,另一个部分其实是舍弃了一些特性换来的速度提升。比如 esbuild
省略了语法检查,官方文档中明确说明了esbuild
没有做TS类型检查,实际我测试发现JS语法检查也没做;没有生产环境需要用到的代码分割等特性(但有计划做)。因为这些不完善的地方,在打包生产环境代码的时候,vite依然用的是 rollup。