vite no-bundle原理实现(三):读取配置

前言

前一篇文章的末尾,我们提了以下问题:

  • 缺少插件机制

在实现插件的之前,我们需要有读取配置文件的能力。

本文的代码基于上一篇文章的分支开始写:github.com/blankzust/v… ,同学们也可以基于这个分支的代码编写本章的功能。

配置插件

在vite中,插件默认是在vite.config.ts或vite.config.js中定义的,如下所示:

export default {
    plugins: [
        {
            name: '插件名称',
            resolveId() {
                // 钩子函数
            }
        }
    ]
}

更多配置见cn.vitejs.dev/config/

在此,我们也需要实现读取配置文件的能力

解析配置文件路径

先来看一下vite的内部实现流程图

vite流程.png

由于篇幅有限,这里仅定义mvite.config.js为我们的配置文件,流程简化为:

vite流程 (1).png

代码实现

// 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内部的实现流程图为:

vite流程 (2).png

由于我们这边写死了了文件名,流程简化为:

vite流程 (3).png

代码实现:

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
};


从变化的过程可以发现完成了以下几个操作:

  1. 第三方依赖转换为绝对路径
  2. 注入全局变量的定义并替换引用的地方

使用esbuild来完成代码的转换

vite流程 (5).png

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中定义的插件配置了。

下一章,我们将利用本章读取的配置实现插件的运行机制

链接文档

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYBE5M6v' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片