- Published on
vite-1.0模块重写插件解读
- Authors
- Name
- 祝你好运
serverPluginModuleRewrite
文件是vite 1.0里面非常重要的一个文件,我们需要仔细分析一下,整体结构如下: 
可以看到,除了红色箭头的地方,别的都很简单,我们展开这个if
的代码块,发现它跟上一篇文章中的那个HTML插件一样,调用到了rewriteImports
,用它的返回值作为res
的body
返回了。我们先看下它的参数:
参数
root: string,
source: string,
importer: string,
resolver: InternalResolver,
timestamp?: string
逐个分析下:
root
是我们要打包的项目的根目录,比如我们要解析文件,肯定是要从根目录触发的source
这个就是文件的内容,当程序执行到这里的时候,我们一定是已经拿到文件内容了。第三方库(node_modules
下面)是通过serverPluginModuleResolve
解析的,非第三方库是通过serverPluginServeStatic
来解析的。importer
指的是请求的url对应的文件。比如我在main.jsx
中引入了App.jsx
,那这里的main.jsx
就是importer
。resolver
是一个辅助我们用来解析的工具类,它可以把请求路径对应到文件,可以把文件对应到请求路径。timestamp
是一个可选参数,这个在HMR的时候会由客户端传过来一个时间,普通的请求里面不会有这个,在分析HMR的时候再看这个。
主结构

try
里面。第134行的#806
代表的是修复了编号为806的bug,原链接:UTF-8 with BOM files cause vite to fail rewrite import。这是一种好习惯,这样方便别人和后来的自己明白为什么要那么写。try里面的主结构

这里先简单提一下HMR和导入记录importeeMap
,比如我们有A
模块,导入了B
和C
,然后C
又导入了D
,当用户修改了D
的时候,前端页面需要如何替换组件?它当然是分析都谁导入了它,所有导入它的模块都得刷新。更详细的细节后面会再分析,vite-1.0 HMR插件解读。
还有就是这个env
,这个是给用户提供注入变量用的,细节可以参考CHANGELOG.md
和README.md
(注意要看1.0.0-rc.13这个tag处的代码)。我们第一遍分析代码的时候,可以不用扣这个细节。
imports替换过程

imports.length
为真。比如说是文件中有import 'src/user/login.js'
。hasHMR
为真。这个解释起来会稍微麻烦点,简单点,比如我的根组件App.jsx
,在vite处理之后会被注入HMR相关的代码的,它的hasHMR
就为真,因为当它的子组件App.js
更新的时候,需要它来动态的替换子组件。hasEnv
为真。当用户注入变量的时候会出现这种情况。具体可以看上面提到的文件。
第158行是用的magic-string来修改字符串,这个修改很简单,这个库的作用就是它可以生成source map,而且不像recast或者Babel这么复杂。
然后我们要区分一下importer
和importee
,比如A
引入了B
和C
,那么A
就是importer
,B
和C
就是importee
,那importerMap
和importeeMap
的定义都是Map<string, Set<string>>
。它是用来方便查找引入关系的,importerMap
是用来查找importee
为key的集合,也就是都有哪些文件引入了我这个文件,这方便查找HMR中需要更新的文件。importeeMap
是用来查找importer
为key的集合,也就是一个文件都引入了哪些文件。

第165行就开始进入for循环来遍历所有的imports
,首先第173上先拿到id
,这里举几个例子:
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
拿到的importer
和id
分别是:
// 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。

(?:)
是为了不捕获,这里讲解的很好:JS正则表达式之非捕获分组用法实例分析。如果你不了解正则表达式的捕获,可以先看下js正则表达式之捕获组。 再回到我们这个正则表达式上面,它是为了在182行拿到这个被导入的模块id。if
语句。第191行我们先不展开讲,这个函数调用就是为了拿到被解析后的id
,比如第三方库react
会被解析成/@modules/react.js
,我们自己写的文件./App
被解析成/src/App.jsx
。这个函数我们后面再详细分析。 
我们再看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行会根据是否有替换来返回不同的源代码。然后整个重写的主流程我们已经看过一遍了,下面就要去深挖一些分支。

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

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

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。