vite-react-ts原理解析

437 阅读4分钟

vite

一个基于浏览器原生 ES imports 的开发服务器。利用浏览器去解析 imports,在服务器端按需编译返回,完全跳过了打包这个概念,服务器随起随用。

为什么选择vite,和webpack有什么区别

传统打包方式

vite打包方式

ES模块

JavaScript代码重用的机制

webpack是如何支持es模块的

webpack简单打包两个文件,分别为index.js和m.js,内容如下

index.js
import bar, {foo} from './m';
bar();
foo();
m.js
export default function bar () {
    return 1;
}

export function foo () {
    return 2;
}

打包后(去除了注释):


(() => {
    "use strict";
    var __webpack_modules__ = ({
  
      "./index.js":
  
        ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
  
          __webpack_require__.r(__webpack_exports__);
          /* harmony import */
          var _m__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./m.js");
          (0, _m__WEBPACK_IMPORTED_MODULE_0__.default)();
          (0, _m__WEBPACK_IMPORTED_MODULE_0__.foo)();
        }),
  
      "./m.js":
  
        ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
          __webpack_require__.r(__webpack_exports__);
          __webpack_require__.d(__webpack_exports__,
            {
              "default": () => (/* binding */ bar),
              "foo": () => (/* binding */ foo)
            }
          );
          function bar() {
            return 1;
          }
          function foo() {
            return 2;
          }
        })
  
    });
  
    var __webpack_module_cache__ = {};
  
    function __webpack_require__(moduleId) {
      var cachedModule = __webpack_module_cache__[moduleId];
      if (cachedModule !== undefined) {
        return cachedModule.exports;
      }
      var module = __webpack_module_cache__[moduleId] = {
        exports: {}
      };
  
      __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
  
      return module.exports;
    }
  
  
    (() => {
      __webpack_require__.d = (exports, definition) => {
        for (var key in definition) {
          if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
            Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
          }
        }
      };
    })();
  
    (() => {
      __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
    })();
  
    (() => {
      __webpack_require__.r = (exports) => {
        if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
          Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
        }
        Object.defineProperty(exports, '__esModule', { value: true });
      };
    })();
  
    var __webpack_exports__ = __webpack_require__("./index.js");
  
  })();

webpack_require.r定义为es模块

webpack_require.d生成依赖

最后进行调用

所以webapck解析es模块其实就是生成一个exports对象,基于Object.defineProperty方法定义get属性,通过moduleId访问得时候,返回对应得exports。

vite是如何支持es模块

script标签上添加type=module属性,浏览器会对每个import发起请求,获取资源。

目前基于web标准的ES模块已经覆盖了超过90%的浏览器

细心得小伙伴就看出来了,main.ts得

import React from 'react'

变成了

import __vite__cjsImport0_react from "/node_modules/.vite/react.js?v=6976f472";

这个过程是怎么实现得?原理是什么?带着疑问我们一起往下看

从源码入口看起

createServer

返回了一个 node 原生的 http 模块创建的server 对象,紧接着调用了 server 的 listen 方法

createServer之前做了什么操作

获取了当前配置和插件,还有生成了模块依赖

初始化中间件

transformMiddleware

vite devServer 核心的中间件,负责拦截处理各种文件的请求并将其内容转换成浏览器能识别的正确代码。

中间件拦截请求

可以看出来这个时候ModuleGraph还没有对应得模块映射

transformMiddleware方法里面

依赖预构建

依赖得预构建,并且会对应生成.vite文件,optimizeDeps方法里面得scanImports 函数其实就是完成对 import 语句的扫描,并返回了需要构建的依赖 deps。

vite的拦截请求,处理内容核心部分

transformRequest

vite transformMiddleware中间件的一个核心方法 transformRequest 处理请求内容,并将结果发送至浏览器。

具体看下transformRequest做了什么

首先要获取id(文件路径),这里的PluginContainer.resolveId实际上就是createPluginContainer里面的resolveId

插件遍历的时候,有!plugin.resolveId条件,筛选出vite:pre-alias和vite:resolve插件,继续分析下这两个插件的作用

vite:pre-alias

请求url对应得id就出来了,id是先通过vite:pre-alias

tryOptimizedResolve

tryOptimizedResolve 会通过读取依赖构建阶段的缓存的依赖映射对象,拿到 react 对应的路径,并且加上对应得browerHash值。 没有依赖项得,会往路径前面加上/src

vite:resolve

(遍历配置里面得plugins,找到vite:reslove),path得模块加上原来得root根路径直接获取到对应得路径即id,可以看出来这个插件是处理依赖预构建其外的import引入

这里可以看出来resolveId方法通过两个 plugin:vite:pre-alias 及 vite:resolve,获取到了对应得文件路径

处理好路径后会调用moduleGraph.ensureEntryFromUrl

moduleGraph模块还未赋值,通过ensureEntryFromUrl方法进行Map.set

对Map进行赋值,只有路径没有内容

接下来通过pluginContainer.transform处理内容

通过!Plugin.tarnsform筛选出vite:import-analysis插件

vite:import-analysis 遍历插件列表,当为vite:import-analysis插件得时候,处理code里面import部分

通过resolveId方法内容里面import得路径

处理完请求内容所有import后,返回浏览器可以识别得部分,毕竟仅仅是react模块,浏览器识别不出来

回到transformRequest方法,这时候已经获取到得路径,处理好了对应得内容,返回浏览器 etag是做协商缓存用,提高浏览器请求效率

最终处理的结果

这里得response就是处理后得code内容

以上便是vite处理es模块核心部分

vite不是提倡No Bundle嘛,为什么还要依赖预构建

官方给出两点解释:

  1. 兼容 CommonJS 和 AMD 模块的依赖
  2. 减少模块间依赖引用导致过多的请求次数

vite 是如何完成预构建的同时保证构建速度的呢

这里vite运用了esbuild进行构建,比webpack打包快上100倍左右

1.js是单线程串行,esbuild是新开一个进程,然后多线程并行,充分发挥多核优势

2.go是纯机器码,肯定要比JIT快

3.不使用 AST,优化了构建流程

由此可见,vite会对需要得资源按需引入,并且利用浏览器缓存机制处理,开发起来速度极快,对比webpack,需要先打包所有得依赖,这也是vite速度极快得原因。