Open lihongxun945 opened 2 years ago
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
Vite
js modules
vite
javascript modules
简单的说,支持javascript modules的浏览器,可以通过script标签引入一个 esm 模块,并且支持其中的import和 export语法,在解析到对应的语法后会在浏览器中动态加载对应的js文件。
script
esm
import
export
我们写一个简单的测试应用来理解什么是 javascript modules,代码都在这里 https://github.com/lihongxun945/javascript-modules-test
入口文件 index.html,通过script标签加载一个模块:
index.html
<html> <body> <script type="module" src="./index.js"></script> </body> </html>
index.js引用了另一个模块,并初始化:
index.js
import Person from './person.js'; const person = new Person();
person.js文件直接导出一个 Person类:
person.js
export default function Person () { console.log('person'); }
启动一个静态服务器,直接访问 index.html,可以发现上述代码无需任何编译,在浏览器中可以直接运行。且浏览器会通过网络请求分别加载 index.js 和 person.js
浏览器已经原生支持了 javascript modules,为什么还要vite呢?有以下几个原因:
bundle
vite 默认把index.html放在了项目的根目录,这和我们的webpack项目放在public中有挺大区别。vite 官网上对这个设计做了解释,总结一下主要原因是对于使用javascript modules的项目来说,index.html 本来就是编译入口文件也是Server的根路径,也就是说既应该放在 src也应该放在 public 中,所以干脆直接放在根目录,这样也不用写 PUBLIC_URL 之类的代码,既符合已有规范还方便,所以就这么写了。
webpack
public
src
PUBLIC_URL
那webpack 为啥不这样做? 因为 webpack 的编译入口文件其实是 index.js而不是index.html,而Server的根路径其实是编译后的 index.html,所以就没这么设计。
示例中的 index.html通过如下代码加载 main.tsx:
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相关依赖,热启动无需打包,启动速度分别是:
React
ReactDOM
node_modules/.vite
node_modules
435ms
236ms
esbuild
100ms
index.css 显然也会被编译成 JS,否则 import 会报错,我们看看编译后的 index.css:
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插入到文档中,方法的实现如下:
updateStyle
style
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写法就有用了。
import index.css
export default css
回到 main.tsx,JSX被编译成了JS是常规操作,对 App.tsx的处理也和上面的逻辑类似。
JSX
App.tsx
App.tsx中加载了一张图片:
import logo from './logo.svg'
根据用Webpack的经验,显然 logo.svg也会被编译成JS,并返回文件地址,实际也确实是这样:
Webpack
logo.svg
export default "/src/logo.svg"
认识了Vite的基本原理之后,就可以明白vite为什么在本地开发那么快了。主要是基于以下几点:
esmodule
那么esbuild 为什么这么快呢? 官方是有解释的,可以看这里:https://esbuild.github.io/faq/#why-is-esbuild-fast 总结一下几个主要原因:
esbuild有这么多优点,那么有没有缺点呢? 当然有的,esbuild的快其实来源于两部分:一部分是 GO语言和多线程带来的优势,另一个部分其实是舍弃了一些特性换来的速度提升。比如 esbuild 省略了语法检查,官方文档中明确说明了esbuild没有做TS类型检查,实际我测试发现JS语法检查也没做;没有生产环境需要用到的代码分割等特性(但有计划做)。因为这些不完善的地方,在打包生产环境代码的时候,vite依然用的是 rollup。
认识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
标签加载一个模块:index.js
引用了另一个模块,并初始化:person.js
文件直接导出一个 Person类:启动一个静态服务器,直接访问
index.html
,可以发现上述代码无需任何编译,在浏览器中可以直接运行。且浏览器会通过网络请求分别加载index.js
和person.js
认识vite
为什么需要vite
浏览器已经原生支持了
javascript modules
,为什么还要vite
呢?有以下几个原因: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
:这直接用了
javascript modules
能力 加载的main.tsx
源码是这样的:TS显然无法被浏览器识别,而在浏览器中加载的文件已经经过了 vite的编译,编译后的代码如下:
我们来看一看代码是怎么编译的。
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
:编译的结果和我们想象的差不多,这里有一个
updateStyle
方法,起作用就是通过style
标签把CSS插入到文档中,方法的实现如下:因为我们是
import index.css
的写法,所以export default css
实际 并没啥用,如果是用CSS Module写法就有用了。回到
main.tsx
,JSX
被编译成了JS是常规操作,对App.tsx
的处理也和上面的逻辑类似。静态资源
App.tsx
中加载了一张图片:根据用
Webpack
的经验,显然logo.svg
也会被编译成JS,并返回文件地址,实际也确实是这样:Vite快在哪里?
认识了
Vite
的基本原理之后,就可以明白vite为什么在本地开发那么快了。主要是基于以下几点:esmodule
加载src文件,按需编译单个文件。node_modules
依赖被打包成大文件,避免了浏览器加载多个js。esbuild
打包速度无敌快。那么
esbuild
为什么这么快呢? 官方是有解释的,可以看这里:https://esbuild.github.io/faq/#why-is-esbuild-fast 总结一下几个主要原因:esbuild
有这么多优点,那么有没有缺点呢? 当然有的,esbuild
的快其实来源于两部分:一部分是 GO语言和多线程带来的优势,另一个部分其实是舍弃了一些特性换来的速度提升。比如esbuild
省略了语法检查,官方文档中明确说明了esbuild
没有做TS类型检查,实际我测试发现JS语法检查也没做;没有生产环境需要用到的代码分割等特性(但有计划做)。因为这些不完善的地方,在打包生产环境代码的时候,vite依然用的是 rollup。