Vite基本原理

69 阅读3分钟
const fs = require('fs');  // 导入 Node.js 的文件系统模块
const path = require('path');  // 导入 Node.js 的路径处理模块
const utils = require('./utils');  // 导入自定义的工具模块
const config = require('./config');  // 导入自定义的配置模块

// 获取入口文件的绝对地址、以及所在文件夹的绝对地址
const entryAbsPath = path.resolve(__dirname, config.entry);  // 计算入口文件的绝对路径
const entryDirname = path.dirname(entryAbsPath);  // 获取入口文件所在文件夹的绝对路径

/**
 * @description 路由逻辑中间件
 */
const router = (ctx) => {  // 定义路由中间件函数,接受 Koa 的上下文对象 ctx
  const { url = '' } = ctx;  // 从上下文中获取请求的 URL

  if (url === '/') {  // 如果请求的是根路径
    handleRootRouter(ctx);  // 调用处理根路径的函数
  } else if (utils.getFileExtname(url) === '.js') {  // 如果请求的是 JavaScript 文件
    handleJsRouter(ctx);  // 调用处理 JavaScript 文件的函数
  } else if (url.startsWith('/node_modules')) {  // 如果请求的是位于 node_modules 文件夹中的文件
    handleLibRouter(ctx);  // 调用处理库文件的函数
  } else if (utils.getFileExtname(url) === '.vue') {  // 如果请求的是 Vue 文件
    handleVueRouter(ctx);  // 调用处理 Vue 文件的函数
  }
};

/**
 * @description 匹配根路径,返回入口文件,没错,vite的入口文件不是js,而是一个html文件。在html中利用srcipt type='module'开启浏览器的esm模块加载方式
 */
const handleRootRouter = (ctx) => {  // 处理根路径的函数
  const entryCodeStr = fs.readFileSync(entryAbsPath, 'utf-8');  // 读取入口文件的内容
  ctx.type = 'text/html; charset=utf-8';  // 设置响应类型为 HTML
  utils.setStrongCache(ctx);  // 设置强缓存(TODO:入口文件设置为强缓存合适吗?)
  ctx.body = entryCodeStr;  // 设置响应内容为入口文件的内容
};

/**
 * @description 匹配.js路径文件,并做两件事情:
 *  - 1. 将js中的esm裸模块加载方式替换为`/`、`./`、`../`
 *  - 2. 将cjs模块加载方式替换为esm方式
 */
const handleJsRouter = ctx => {  // 处理 JavaScript 文件的函数
  const fileAbsPath = path.resolve(entryDirname, `.${ctx.url}`);  // 计算请求的文件的绝对路径
  const jsCodeStr = fs.readFileSync(fileAbsPath, 'utf-8');  // 读取请求的文件的内容
  const transformCodeStr = utils.transformPath(jsCodeStr);  // 转换文件中的模块加载方式
  ctx.type = 'application/javascript';  // 设置响应类型为 JavaScript
  ctx.body = transformCodeStr;  // 设置响应内容为转换后的文件内容
};

/**
 * @description 加载node_modules里的库文件
 * 这里初版没有用vite的esbuild预编译方法,而是利用库文件的package.json的modul字段,来找到库打包输出的bundle文件
 */
const handleLibRouter = (ctx) => {  // 处理库文件的函数
  const libAbsPath = path.resolve(entryDirname, `../${ctx.url}`);  // 计算库文件的绝对路径
  const { module: libBundlePath } = require(`${libAbsPath}/package.json`);  // 获取库文件的打包输出路径
  const libBundleAbsPath = `${libAbsPath}/${libBundlePath}`;  // 计算库文件的打包输出文件的绝对路径
  const libBundleStr = fs.readFileSync(libBundleAbsPath, 'utf-8');  // 读取库文件的打包输出内容
  const transformCodeStr = utils.transformPath(libBundleStr);  // 转换库文件中的模块加载方式
  utils.setStrongCache(ctx);  // 设置强缓存
  ctx.type = 'application/javascript';  // 设置响应类型为 JavaScript
  ctx.body = transformCodeStr;  // 设置响应内容为转换后的文件内容
};

/**
 * @description 处理.vue文件,将.vue文件变为.js文件返回
 */
const handleVueRouter = (ctx) => {  // 处理 Vue 文件的函数
  const url = ctx.url.split('?')[0];  // 获取请求的 Vue 文件的 URL
  const fileAbsPath = path.resolve(entryDirname, `.${url}`);  // 计算请求的 Vue 文件的绝对路径
  const vueCodeStr = fs.readFileSync(fileAbsPath, 'utf-8');  // 读取请求的 Vue 文件的内容
  const { jsCodeStr, transformRenderModuleStr: renderModuleStr } = utils.parseVue(url, vueCodeStr);  // 解析 Vue 文件内容
  // .vue文件有分两种情况,type==='tempalte',返回render module,否则返回主逻辑script
  const { type } = ctx.query;  // 获取请求参数中的 type
  let body = '';  // 响应内容的变量
  if (type === 'template') {  // 如果 type 为 'template'
    body = renderModuleStr;  // 设置响应内容为渲染模块部分
  } else {  // 否则
    body = jsCodeStr;  // 设置响应内容为主逻辑的 script 部分
  }

  ctx.type = 'application/javascript';  // 设置响应类型为 JavaScript
  ctx.body = body;  // 设置响应内容
};

module.exports = router;  // 导出路由中间件函数

const path = require('path');  // 导入 Node.js 的路径处理模块
const parser = require('@babel/parser');  // 导入 @babel/parser 用于解析 JavaScript 代码为 AST
const { default: traverse } = require('@babel/traverse');  // 导入 @babel/traverse 用于遍历 AST
const generator = require("@babel/generator");  // 导入 @babel/generator 用于将 AST 转换回代码
const compilerSFC = require('@vue/compiler-sfc');  // 导入 @vue/compiler-sfc 用于处理 Vue 单文件组件
const compilerDOM = require('@vue/compiler-dom');  // 导入 @vue/compiler-dom 用于编译 Vue 模板成渲染函数

/**
 * @description 转换浏览器不认识的路径为认识路径
 * @param {string} source 输入源代码字符串
 * @returns {string} 转换后的源代码字符串
 */
const transformPath = (source = '') => {
  // 将源代码解析为 AST
  const ast = parser.parse(source, {
    sourceType: "module",
  });

  // 重写路径的函数
  const rewritePath = (node) => {
    const esmPath = node?.source?.value;  // 获取模块的路径
    if (!isLegalEsmPath(esmPath) && esmPath) {
      node.source.value = `/node_modules/${esmPath}`;  // 改写路径为 /node_modules/...
    }
  };

  // 遍历 AST,对 ImportDeclaration、ExportAllDeclaration 和 ExportNamedDeclaration 进行路径改写
  traverse(ast, {
    ImportDeclaration({node}) {
      rewritePath(node);
    },
    ExportAllDeclaration({node}) {
      rewritePath(node);
    },
    ExportNamedDeclaration({node}) {
      rewritePath(node);
    }
  });

  // 将改写后的 AST 转换为源代码并返回
  const transformSource = generator.default(ast, {}, source).code;
  return transformSource;
};

/**
 * @description 判断是否是合法的 esm 路径
 * @param {string} esmPath esm 路径
 * @returns {boolean} 是否是合法的 esm 路径
 */
const isLegalEsmPath = esmPath => {
  const legalEsmPath = ['/', './', '../'];
  let isLegalEsmPath = false;
  legalEsmPath.some(path => {
    if (esmPath?.startsWith(path)) {
      isLegalEsmPath = true;
      return true;
    }
  });
  return isLegalEsmPath;
};

/**
 * @description 解析 Vue 文件的单文件组件
 * @param {string} url 请求的 URL
 * @param {string} source Vue 单文件组件的源代码字符串
 * @returns {object} 包含 transformRenderModuleStr 和 jsCodeStr 的对象
 */
const parseVue = (url, source) => {
  const ast = compilerSFC.parse(source);
  const { template, script } = ast.descriptor;

  // 转换 script 部分的代码
  const scriptCode = script.content.replace(/export default/, 'const script =');
  const transformScriptCode = transformPath(scriptCode);

  // 将 template 转换为 render 渲染函数
  const { code: renderModule } = compilerDOM.compile(template.content, {mode: 'module'});
  const transformRenderModuleStr = transformPath(renderModule);

  // 组装 js 代码字符串
  const jsCodeStr = `
    ${transformScriptCode}
    import { render } from '${url}?type=template';
    script.render = render;
    export default script;
  `;

  return {
    transformRenderModuleStr,
    jsCodeStr
  };
};

/**
 * @description 获取请求文件的后缀名
 * @param {string} urlPath 文件路径
 * @returns {string} 文件后缀名,例如 .js、.vue、.css
 */
const getFileExtname = (urlPath = '') => {
  const filePath = urlPath.split('?')[0];
  const extname = path.extname(filePath);
  return extname;
};

/**
 * @description 设置强缓存
 * @param {object} ctx Koa 的上下文对象
 */
const setStrongCache = (ctx) => {
  ctx.set('Cache-Control', 'max-age=31536000,immutable'); // 设置强缓存时间
};

/**
 * @description 设置协商缓存
 */
const setConsultCache = () => {
  ctx['no-cache'] = true; // 不走强缓存
  // 需要设置 ETag hash 值,且需要对比 If-None-Match 的 hash 值
};

module.exports = {
  transformPath,
  parseVue,
  getFileExtname,
  setStrongCache,
  setConsultCache,
};

参考

github.com/classmatewu…