lihongxun945 / myblog

言川的博客-前端工程师的笔记
1.41k stars 128 forks source link

Vite笔记 #50

Open lihongxun945 opened 2 years ago

lihongxun945 commented 2 years ago

认识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 模块,并且支持其中的importexport语法,在解析到对应的语法后会在浏览器中动态加载对应的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.jsperson.js

认识vite

为什么需要vite

浏览器已经原生支持了 javascript modules,为什么还要vite呢?有以下几个原因:

  1. 对非JS文件,比如 css、图片等的支持
  2. 对非 esm 语法的支持
  3. 通过 bundle 优化性能,可以把多个小文件合并成大文件避免浏览器加载成百上千个文件 当然还有一些其他原因,比如生产环境打包、HMR、chunks等

vite 目录结构

modules 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依赖的 ReactReactDOM 经过了 vite 的编译,且缓存在了 node_modules/.vite 目录中。这样做好处是只需要加载两个文件,如果不处理那么浏览器需要加载很多React的依赖。根据官网的说明,vite启动时会自动分析node_modules依赖并把他们都打包,所以并不会因为这些依赖太多导致浏览器加载大量js。 由于打包是在本地进行的,冷启动显然会受到影响,经过自己本地实际测试,冷启动有打包 React相关依赖,热启动无需打包,启动速度分别是:

  1. 冷启动 435ms
  2. 热启动 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.tsxJSX被编译成了JS是常规操作,对 App.tsx的处理也和上面的逻辑类似。

静态资源

App.tsx中加载了一张图片:

import logo from './logo.svg'

根据用Webpack的经验,显然 logo.svg也会被编译成JS,并返回文件地址,实际也确实是这样:

export default "/src/logo.svg"

Vite快在哪里?

认识了Vite的基本原理之后,就可以明白vite为什么在本地开发那么快了。主要是基于以下几点:

  1. 本地只有冷启动的时候有资源打包,热启动完全无任何打包编译操作,只是启动了一个服务器,所以通常 0.2 秒就能完成启动
  2. 浏览器直接通过esmodule加载src文件,按需编译单个文件。
  3. node_modules依赖被打包成大文件,避免了浏览器加载多个js。
  4. 公共依赖开启缓存缓存,根本不用请求到 server。
  5. 最后也是最重要的:esbuild打包速度无敌快。

那么esbuild 为什么这么快呢? modules 官方是有解释的,可以看这里:https://esbuild.github.io/faq/#why-is-esbuild-fast 总结一下几个主要原因:

  1. GO语言的优势:esbuild是用go语言写的,是编译型语言,且针对多线程进行优化,而webpack是用JS写的,解释型且主要是单线程的特性注定他的性能比不过GO。
  2. 良好的并行优化:多线程优化,尽可能用到全部的CPU核心,且多线程可以共享内存数据。
  3. 从底层实现:自己实现了底层逻辑,没有依赖三方库。比如TS解析的时候如果用TS官方的编译器,需要检查类型,因此就会变慢;GO的静态类型速度也快于JS中的动态类型。
  4. 更小的内存使用:更小的内存数据,更少次数对JS进行遍历。

esbuild有这么多优点,那么有没有缺点呢? 当然有的,esbuild的快其实来源于两部分:一部分是 GO语言和多线程带来的优势,另一个部分其实是舍弃了一些特性换来的速度提升。比如 esbuild 省略了语法检查,官方文档中明确说明了esbuild没有做TS类型检查,实际我测试发现JS语法检查也没做;没有生产环境需要用到的代码分割等特性(但有计划做)。因为这些不完善的地方,在打包生产环境代码的时候,vite依然用的是 rollup。