《vite技术揭秘、还原与实战》第6节--defineConfig引发的问题思考与解决方案分析

前言

上一节我们提供的svite.config.ts配置文件没有相应的TypeScript类型定义,这对开发者是不友好的

本节我们通过提供一个defineConfig函数来优化这个问题

源码获取

传送门

更新进度

公众号:更新至第15

博客:更新至第6

尝试

这似乎很简单,在packages\vite\src\node\config.ts中导出defineConfig函数,该函数用作向用户提供类型支持

ts 复制代码
export function defineConfig(config: UserConfig): UserConfig {
  return config;
}

为了达到这个目的,还需要在packages\vite\src\node\index.ts中导出UserConfig

ts 复制代码
export * from "./config";

接着在playground\config\svite.config.ts中导入并使用,理论上就大功告成了

ts 复制代码
import { defineConfig } from "svite";
export default defineConfig({
  server: {},
  root: "",
});

此时启用该用例,会发现如下报错

text 复制代码
Dynamic require of "fs" is not supported

原因分析

该报错说明我们正在esm模块的执行过程中使用require语法,这似乎有点不可思议,因为我们在packages\vite\rollup.config.ts文件中定义的output.format确实是esm格式,打包结果如下所示

ts 复制代码
import { resolve } from 'node:path';
import { existsSync } from 'node:fs';
import { build } from 'esbuild';

const DEFAULT_CONFIG_FILES = ["svite.config.ts"];

async function buildBoundle(fileName) {
    ...
}
async function loadConfigFromBoundled(code, resolvedPath) {
    ...
}
function defineConfig(config) {
    return config;
}
async function parseConfigFile(conf) {
    ...
}
async function resolveConfig(userConf) {
    ...
}

export { defineConfig, parseConfigFile, resolveConfig };
//# sourceMappingURL=index.js.map

如上,我们的三个import也都是符合esm语法的,其中fspath模块是node内置的,node 本身又支持esm,理论上来说不可能是它们导致的。那问题貌似出现在esbuild

我们找到node_modules下的esbuild文件夹,并根据package.json定位到入口为packages\vite\node_modules\esbuild\lib\main.js的文件,在其源码中发现了这两句代码

js 复制代码
...
var fs = require("fs");
var os = require("os");
...

还记得我们上一节是如何处理svite.config.ts文件的吗?我们为了能在文件中引入外部模块,使用esbuild进行了打包,而build行为会分析模块中的import并将其打包进一个boundle,这就意味着var fs = require("fs");这行代码将会被打包到我们最终的boundleCode

ts 复制代码
import * as any from "some-pkg";
const fs = require("fs");

而对于boundleCode我们使用的是new Function的形式,至此,真相大白

ts 复制代码
const dynamicImport = new Function("file", "return import(file)");

如何解决

既然原因是对esbuild进行了分析打包,那是否可以跳过对esbuild包的build行为呢?

首先,我们找到在packages\vite\package.json中的dependencies,如下

json 复制代码
"dependencies": {
    "esbuild": "^0.18.8",
    "magic-string": "^0.30.0",
    "rollup": "^3.21.0"
}

由于esbuilddependencies的一员,则意味着,我们一定能在用户项目的node_modules中找到该依赖包,这意味着使用import { defineConfig } from 'svite 时对入口的加载过程不会抛出错误

同时由于svite已经被打包过,因此如果我们能直接从node_modules引入则问题能够被解决,并且为了更安全的找到对应的包,我们使用绝对路径更为妥当

ts 复制代码
import { defineConfig } from 'XXX/node_modules/svite/dist/node/index.js'

那么问题就在于,svite如何被转换为XXX/node_modules/svite/dist/node/index.js呢?

源码分析

我们在原因分析中已经找到了问题出在打包处,并且也提出了解决方案:

1-将裸依赖从打包中排除

2-将裸依赖导出地址替换为绝对路径

故将代码定位到bundleConfigFile函数的名称为externalize-depsesbuild plugin中,源码简化如下

ts 复制代码
// packages/vite/src/node/config.ts

async function bundleConfigFile(
  fileName: string,
  isESM: boolean,
): Promise<{ code: string; dependencies: string[] }> {
  ...
  const result = await build({
    ...
    plugins: [
      {
        name: 'externalize-deps',
        setup(build) {
          ...
          // 排除裸依赖
          build.onResolve(
            { filter: /^[^.].*/ },
            async ({ path: id, importer, kind }) => {
              ...
              return {
                path: idFsPath,
                external: true,
              }
            },
          )
        },
      },
      ...
    ],
  })
  const { text } = result.outputFiles[0]
  return {
    code: text,
    ...
  }
}

如上,vite通过onResolve钩子来完成对模块路径的替换和模块的排除工作,这分别对应着返回对象的pathexternal属性,当为external设置为true后,esbuild将会自动将模块从打包结果中排除,因此,我们的重点是分析模块路径替换是怎么实现的 ,也即pathvalue值是如何生成的

ts 复制代码
// 获取裸模块的本地文件路径
idFsPath = resolveByViteResolver(id, importer, !isImport)
// 将本地文件路径转换为可用于网络访问的URL
idFsPath = pathToFileURL(idFsPath).href

进入resolveByViteResolver函数,并按顺序找到tryNodeResolve函数,如下是笔者简化后的代码

ts 复制代码
// packages/vite/src/node/plugins/resolve.ts

export function tryNodeResolve(...): PartialResolvedId | undefined {
  const { preserveSymlinks, packageCache,... } = options
  ...
  // 获取目录,即vite.config.ts所在的目录,一般为用户项目根目录
  const basedir = path.dirname(importer)
  // 获取裸模块包的信息
  const pkg = resolvePackageData(pkgId, basedir, preserveSymlinks, packageCache)
  ...
  // 模块导入函数
  const resolveId = deepMatch ? resolveDeepImport : resolvePackageEntry
  ...
  // 从模块信息中分析入口文件地址
  const resolved = resolveId(unresolvedId, pkg, targetWeb, options)
  ...
  return resolved
}

进入resolvePackageData函数,可以看到,vitenpm包的package.json开始查找,一般来说,在第一次while循环过程中就能找到,若实在找不到就依次到上一级

ts 复制代码
// packages/vite/src/node/packages.ts
export function resolvePackageData(...): PackageData | null {
  ...
  const originalBasedir = basedir
  while (basedir) {
    ...
    // 找到导入npm包的package.json文件
    const pkg = path.join(basedir, 'node_modules', pkgName, 'package.json')
    try {
      if (fs.existsSync(pkg)) {
        // 读取package.json
        const pkgPath = preserveSymlinks ? pkg : safeRealpathSync(pkg)
        const pkgData = loadPackageData(pkgPath)
        ...
        return pkgData
      }
    } catch {}
    // 进入上一级目录查找
    const nextBasedir = path.dirname(basedir)
    if (nextBasedir === basedir) break
    basedir = nextBasedir
  }

  return null
}

pkgPath这一行,根据preserveSymlinks取值来决定是否使用符号链接,符号链接其实类似于一种别名,通过读取A可直接获取源文件B,而非符号链接则必须找到实际的源文件B的路径才能进行内容的读取

safeRealpathSync的实现又根据操作系统的不同有差异

ts 复制代码
// packages/vite/src/node/utils.ts
export let safeRealpathSync = isWindows
  ? fs.realpathSync
  : fs.realpathSync.native

总而言之,vite获取到了一个指向npm包的绝对路径,下一步使用loadPackageData来进行文件内容的读取,如下,其本质上就是fs的文件读取操作

ts 复制代码
export function loadPackageData(pkgPath: string): PackageData {
  const data = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
  ...
  return data
}

现在我们返回tryNodeResolve函数

对于resolveId的取值,则分别对应如下两种导入方式

ts 复制代码
import xxx from 'x'
import yyy from 'x/y'

笔者不打算在此处对这两个函数进行展开,因为在svite的实现中笔者并不打算完全采用该方式 。感兴趣的读者可以自己按提示找到对应的文件定位到代码处进行查看。实际上其实现思路也很简单:找到package.json中的文件导出字段如exports,然后进行匹配拼接即可

相关推荐
王解3 小时前
速度革命:esbuild如何改变前端构建游戏 (1)
前端·vite·esbuild
景天科技苑1 天前
【vue3+vite】新一代vue脚手架工具vite,助力前端开发更快捷更高效
前端·javascript·vue.js·vite·vue项目·脚手架工具
niech_cn3 天前
vite + vue3 + ts解决别名引用@/api/user报错找不到相应的模块
vite
Amd7945 天前
Nuxt.js 应用中的 vite:compiled 事件钩子
自定义·vite·编译·nuxt·热更新·性能·钩子
黑色的糖果5 天前
npm上传自己封装的插件(vue+vite)
前端·vue.js·npm·vite
软件小伟6 天前
Vite是什么?Vite如何使用?相比于Vue CLI的区别是什么?(一篇文章帮你搞定!)
前端·vue.js·ecmascript·vite·vue vli
Amd7946 天前
Nuxt.js 应用中的 vite:serverCreated 事件钩子
中间件·开发·vite·日志·nuxt·跨域·钩子
亦世凡华、6 天前
React--》如何高效管理前端环境变量:开发与生产环境配置详解
react·vite·环境变量·env·env配置
19组清风7 天前
对于模块动态加载,Vite 内部做了哪些优化
前端·vite·前端工程化
Amd7947 天前
Nuxt.js 应用中的 vite:configResolved 事件钩子
vite·配置·nuxt·构建·钩子·动态·调整