前言
在前一篇文章的末尾,我们提了以下问题:
- 缺少插件机制
在实现插件的之前,我们需要有读取配置文件的能力。
本文的代码基于上一篇文章的分支开始写:github.com/blankzust/v… ,同学们也可以基于这个分支的代码编写本章的功能。
配置插件
在vite中,插件默认是在vite.config.ts或vite.config.js中定义的,如下所示:
export default {
plugins: [
{
name: '插件名称',
resolveId() {
// 钩子函数
}
}
]
}
在此,我们也需要实现读取配置文件的能力
解析配置文件路径
先来看一下vite的内部实现流程图
由于篇幅有限,这里仅定义mvite.config.js为我们的配置文件,流程简化为:
代码实现
// config.js const path = require('path'); function loadConfigFromFile(configEnv, configRoot = process.cwd()) { const configFile = path.resolve(configRoot, 'mvite.config.js'); if (!fs.existSync(configFile)) { return null; } // ...未完待续 }
ESM or CJS
判断vite.config.js是esm模块还是cjs模块
vite内部的实现流程图为:
由于我们这边写死了了文件名,流程简化为:
代码实现:
const path = require('path'); function loadConfigFromFile(configEnv, configRoot = process.cwd()) { const configFile = path.resolve(configRoot, 'mvite.config.js'); if (!fs.existSync(configFile)) { return null; } + let isESM = false; + const packagePath = path.resolve(configRoot, 'package.json'); + const packageJson = JSON.parse(fs.readFileSync(packagePath)); + isESM = packageJson.type === 'module'; }
读取配置文件并转换为可执行的js
先来看一下vite转换后的配置文件内容
// 转换前
// 这是一段简单的vite配置,定义alias配置
// 让代码中的import x from '/@'变为import x from '项目根目录/dir'
import path from 'node:path'
import { defineConfig } from 'vite'
// 打印全局变量import.meta.url,表示当前模块的路径
console.log(import.meta.url)
export default defineConfig({
resolve: {
alias: [
{ find: '/@', replacement: path.resolve(__dirname, 'dir') },
],
},
build: {
minify: false,
},
})
// 转换后
import path from "node:path";
// 第三方依赖转换为file://文件路径
import { defineConfig } from "file:///Users/shufeng/study/vite/packages/vite/dist/node/index.js";
// 注入使用到的全局变量__dirname和import.meta.url
var __vite_injected_original_dirname = "/Users/shufeng/study/vite/playground/alias";
var __vite_injected_original_import_meta_url = "file:///Users/shufeng/study/vite/playground/alias/vite.config.js";
// import.meta.url转换为注入的变量__vite_injected_original_import_meta_url
console.log(__vite_injected_original_import_meta_url);
var vite_config_default = defineConfig({
resolve: {
alias: [
// __dirname 转换为全局变量 __vite_injected_original_dirname
{ find: "/@", replacement: path.resolve(__vite_injected_original_dirname, "dir") }
]
},
build: {
minify: false
}
});
// 代码格式化
export {
vite_config_default as default
};
从变化的过程可以发现完成了以下几个操作:
- 第三方依赖转换为绝对路径
- 注入全局变量的定义并替换引用的地方
使用esbuild来完成代码的转换
async function bundleConfigFile(fileName, isESM, configRoot = process.cwd()) {
// 注入3个全局变量
const dirnameVarName = '__vite_injected_original_dirname'
const filenameVarName = '__vite_injected_original_filename'
const importMetaUrlVarName = '__vite_injected_original_import_meta_url'
const res = await esbuild.build({
absWorkingDir: process.cwd(),
entryPoints: [fileName],
outfile: 'out.js',
write: false,
target: ['node14.18', 'node16'],
platform: 'node',
bundle: true,
format: 'esm',
mainFields: ['main'],
sourcemap: 'inline',
metafile: true,
define: {
__dirname: dirnameVarName,
__filename: filenameVarName,
'import.meta.url': importMetaUrlVarName,
},
plugins: [
{
name: 'externalize-deps',
setup(build) {
// 过滤bare import
build.onResolve({
filter: BARE_IMPORT_RE,
}, ({ path: id, importer, kind }) => {
if (!isBuiltIn(id)) {
const modulePath = path.resolve(configRoot, `./node_modules/${id}`);
const modulePkgPath = path.resolve(modulePath, `./package.json`);
const pkgData = JSON.parse(fs.readFileSync(modulePkgPath).toString('utf-8'));
console.log(pkgData);
const exports = pkgData.exports;
Object.keys(exports).forEach(exportKey => {
console.log(path.resolve(modulePath, exportKey));
if (path.resolve(modulePath, exportKey) === modulePath) {
id = path.resolve(modulePath, exports[exportKey].import);
}
})
}
console.log(id, 'id');
return {
path: id,
external: true
}
})
}
},
{
name: 'inject-global-variables',
setup(build) {
build.onLoad({ filter: /\.[cm]?[jt]s$/ }, (args) => {
const contents = fs.readFileSync(args.path, { encoding: 'utf8' });
const injectValues =
`const ${dirnameVarName} = ${JSON.stringify(
path.dirname(args.path),
)};` +
`const ${filenameVarName} = ${JSON.stringify(args.path)};` +
`const ${importMetaUrlVarName} = ${JSON.stringify(
pathToFileURL(args.path).href,
)};`
return {
loader: args.path.endsWith('ts') ? 'ts' : 'js',
contents: injectValues + contents,
}
})
}
}
]
})
return {
code: res.outputFiles[0].text,
}
}
加载js并引入为模块
朴素的想法:把js代码转存为js文件,然后执行import(js文件地址)来加载模块,加载好后删除文件。
这个也是vite 4.3版本及之前版本实现的方案。
但是在vite4.4开始,使用import(data:text/javascript;base64,${文件内容转换为base64}
)的方式来加载模块
async function loadConfigFromFile(configEnv, configRoot = process.cwd()) {
const configFile = path.resolve(configRoot, 'mvite.config.js');
let isESM = false;
const packagePath = path.resolve(configRoot, 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packagePath));
isESM = packageJson.type === 'module';
const res = await bundleConfigFile(configFile, isESM, configRoot)
+ const configTimestamp = `mvite.config.js.timestamp:${Date.now()}-${Math.random()
+ .toString(16)
+ .slice(2)}`;
+ return (await import('data:text/javascript;base64,' +
+ Buffer.from(`${res.code}\n//${configTimestamp}`).toString(
+ 'base64',
+ )
+ )).default
}
成果
在express服务器启动代码中加入loadConfigFromFile的调用代码
#!/usr/bin/env node
const express = require('express')
const { vueMiddleware } = require('../middleware')
const app = express()
const root = process.cwd();
const path = require('path');
const prebundle = require('../prebundle');
+ const { loadConfigFromFile } = require('./config');
async function start() {
app.use(vueMiddleware())
app.use(express.static(path.join(root, './demo')))
+ const config = await loadConfigFromFile();
+ console.log(config.plugins, 'config')
app.listen(3003, async () => {
await prebundle(path.join(root, './demo'));
console.log('server running at http://localhost:3003')
})
}
start();
在项目根目录新增mvite.config.js
import * as path from 'path'
import { defineConfig } from 'vite'
export default defineConfig({
resolve: {
alias: [
{ find: '/@', replacement: path.resolve(__dirname, 'dir') },
],
},
build: {
minify: false,
},
plugins: [
{
name: 'test-plugin',
resolveId() {
console.log('resolveId钩子')
}
}
]
})
执行npm run dev
打印出[ { name: 'test-plugin', resolveId: [Function: resolveId] } ] config
符合预期,至此,我们已经能够读取mvite.config.js中定义的插件配置了。
下一章,我们将利用本章读取的配置实现插件的运行机制
链接文档
- 本文源码:github.com/blankzust/v…
- esbuild官方文档:esbuild.docschina.org/api/
- vite源码仓库:github.com/vitejs/vite
- 本文涉及的vite源码文件:
- /packages/vite/src/node/config.ts
- /packages/vite/src/node/utils.ts
- /packages/vite/src/node/plugins/resolve.ts
© 版权声明
文章版权归作者所有,未经允许请勿转载,侵权请联系 admin@trc20.tw 删除。
THE END