Published on

vite-1.0模块重写插件解读

Authors
  • avatar
    Name
    祝你好运
    Twitter
这个serverPluginModuleRewrite文件是vite 1.0里面非常重要的一个文件,我们需要仔细分析一下,整体结构如下: serverPluginModuleRewrite.ts

可以看到,除了红色箭头的地方,别的都很简单,我们展开这个if的代码块,发现它跟上一篇文章中的那个HTML插件一样,调用到了rewriteImports,用它的返回值作为resbody返回了。我们先看下它的参数:

参数

  root: string,
  source: string,
  importer: string,
  resolver: InternalResolver,
  timestamp?: string

逐个分析下:

  1. root是我们要打包的项目的根目录,比如我们要解析文件,肯定是要从根目录触发的
  2. source这个就是文件的内容,当程序执行到这里的时候,我们一定是已经拿到文件内容了。第三方库(node_modules下面)是通过serverPluginModuleResolve解析的,非第三方库是通过serverPluginServeStatic来解析的。
  3. importer指的是请求的url对应的文件。比如我在main.jsx中引入了App.jsx,那这里的main.jsx就是importer
  4. resolver是一个辅助我们用来解析的工具类,它可以把请求路径对应到文件,可以把文件对应到请求路径。
  5. timestamp是一个可选参数,这个在HMR的时候会由客户端传过来一个时间,普通的请求里面不会有这个,在分析HMR的时候再看这个。

主结构

rewrite-imports-main-structure 我们可以看出来,我们重点要看的就是在try里面。第134行的#806代表的是修复了编号为806的bug,原链接:UTF-8 with BOM files cause vite to fail rewrite import。这是一种好习惯,这样方便别人和后来的自己明白为什么要那么写。

try里面的主结构

rewrite-imports-try-main-structure 简单来说,就是通过es-module-lexer词法分析器来拿到这个模块的导入项,如果没有导入,就不处理。如果有,就需要记录里面的导入记录,也就是谁导入了谁,这个导入记录会在HMR的时候用到。并且会修改这导入,最后返回修改之后的源代码。

这里先简单提一下HMR和导入记录importeeMap,比如我们有A模块,导入了BC,然后C又导入了D,当用户修改了D的时候,前端页面需要如何替换组件?它当然是分析都谁导入了它,所有导入它的模块都得刷新。更详细的细节后面会再分析,vite-1.0 HMR插件解读

还有就是这个env,这个是给用户提供注入变量用的,细节可以参考CHANGELOG.mdREADME.md(注意要看1.0.0-rc.13这个tag处的代码)。我们第一遍分析代码的时候,可以不用扣这个细节。

imports替换过程

rewrite-imports-l-156 我们从156行开始,一行一行来看,一共有三种情况会走进去,因为走进去的话,就需要分析源代码然后修改源代码,所以才做了合并。举一些例子
  1. imports.length为真。比如说是文件中有import 'src/user/login.js'
  2. hasHMR为真。这个解释起来会稍微麻烦点,简单点,比如我的根组件App.jsx,在vite处理之后会被注入HMR相关的代码的,它的hasHMR就为真,因为当它的子组件App.js更新的时候,需要它来动态的替换子组件。
  3. hasEnv为真。当用户注入变量的时候会出现这种情况。具体可以看上面提到的文件。

第158行是用的magic-string来修改字符串,这个修改很简单,这个库的作用就是它可以生成source map,而且不像recast或者Babel这么复杂。

然后我们要区分一下importerimportee,比如A引入了BC,那么A就是importerBC就是importee,那importerMapimporteeMap的定义都是Map<string, Set<string>>。它是用来方便查找引入关系的,importerMap是用来查找importee为key的集合,也就是都有哪些文件引入了我这个文件,这方便查找HMR中需要更新的文件。importeeMap是用来查找importer为key的集合,也就是一个文件都引入了哪些文件。

第160,161,162以及下面这几行,都是用来更新这些导入关系的。 importee-importer-update-code

第165行就开始进入for循环来遍历所有的imports,首先第173上先拿到id,这里举几个例子:

import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'

拿到的importerid分别是:

// importer: /src/main.jsx, id: react
// importer: /src/main.jsx, id: react-dom
// importer: /src/main.jsx, id: ./index.css
// importer: /src/main.jsx, id: ./App

第174行的hasViteIgnore判断这个id是否符合那个正则表达式。

第176行的dynamicIndex是表明这个import是否是dynamic import,举例如下:

// STATIC
import './a.js'
import b from './b.js'
import { c } from './c.js'

// DYNAMIC
import('./a.js').then(() => {
  console.log('a.js is loaded dynamically')
})

第178行是为了删掉备注,具体可以参考第998号问题。

第179行是通过正则表达式拿到那个被导入的id,比如import './a.js';,那id就是'./a.js'。我们先来看一下这个正则表达式,简单点可以通过可视化正则表达式辅助分析,regulex

literal-id-regular-expression 这里额外多说一句,(?:)是为了不捕获,这里讲解的很好:JS正则表达式之非捕获分组用法实例分析。如果你不了解正则表达式的捕获,可以先看下js正则表达式之捕获组。 再回到我们这个正则表达式上面,它是为了在182行拿到这个被导入的模块id。
我们再来看185行,大意就是如果有静态导入或者动态导入,就进入if语句。第191行我们先不展开讲,这个函数调用就是为了拿到被解析后的id,比如第三方库react会被解析成/@modules/react.js,我们自己写的文件./App被解析成/src/App.jsx。这个函数我们后面再详细分析。 module-rewrite-replace-import

我们再看201行的isOptimizedCjs,这个函数就是判断id代表的文件是不是我们已经优化过的第三方库,这个提前优化,我们之前提过,也就是说,第三方库我们是不会修改的,所以可以先提前打包优化下,那后面运行的时候,就直接加载优化之后的结果,这个是放到node_modules/.vite_opt_cache下面的文件夹中的,这个文件夹在src/node/optimizer/index.ts的第62行有定义。

然后这个201行的if判断还有对应的else,其实就是,如果是优化过的,是一种替换方法(第208行);如果不是,就是另外一种替换方法(第215行)。

225行到234行可以通过注释看懂,就是更新导入链。

第242行就是针对HMR做重写。第248行是针对那些注入的环境变量的,会在文件末尾附加一些语句。第259行会更新导入依赖关系。最后第274行会根据是否有替换来返回不同的源代码。然后整个重写的主流程我们已经看过一遍了,下面就要去深挖一些分支。

module-rewrite-replace-hmr

resolveImport

首先resolveImport会预处理id,如果是第三方库,就加上/@modules/前缀。如果是用户自己写的代码,就让它们变成指向具体文件的绝对路径。具体判断是否是第三方库,是通过正则/^[^\/\.]/来判断的,这个正则表示的就是不以/.开头的,中括号里面的^表示非。剩余的代码通过注释就可以看懂,具体的函数调用会在下面继续分析。

module-rewrite-resolve-import-id

第312行,会尝试给非代码的导入加上import的参数,这个参数会在pathUtils.ts的72行用到,到那里看下注释就明白了,简单来说就是为了后面区分<link>import "...",后者会被加上?import,前者不会,前者不需要vite做额外的处理,浏览器就可以解析,后者就需要再进一步处理为ES Module。

module-rewrite-append-import

transformCjsImport

这个就是把从CommonJS里import的语句重写一下。举个例子来说,

import React, { useState } from 'react'

会被替换为:

import $viteCjsImport2_react from '/@modules/react.js'
const React = $viteCjsImport2_react
const useState = $viteCjsImport2_react['useState']

这么做是因为react.js打包出来的是CommonJS。