mini-bundle 实现:loader

前言

在上篇文章 mini-bundler 的简易实现中,我们实现了 mini-bundler模块解析依赖图构建代码转换,实际上已经是一个能执行的 bundler 了。

但目前我们的 mini-bundler 仅仅只能解析 js 文件,这其实是很局限的,毕竟项目中会有各式各样的文件,如 tsjsx、静态资源等文件。

一般来说,我们需要一个文件模块解析器,将不同的文件能够解析成 js 代码,从而能够支持 mini-bundler 流程。

webpack 中,loader 承接了这些操作。所以,本文我们继续参考了 webpackloader 的概念来进行实现。

在实现之前,我们先来介绍 loader

如何理解 loader

Out of the box, webpack only understands JavaScript and JSON files. Loaders allow webpack to process other types of files and convert them into valid modules that can be consumed by your application and added to the dependency graph. - webpack-concets

定义

webpack 的定义中,我们可以发现它是一个转换器,负责将源文件(如 .js, .css, .html)转换成 Webpack 能够处理的模块。

作用

  • 代码转换:Loader 允许开发者将文件从一种语言或格式(例如 TypeScript、Sass)转换成标准的 JavaScript 和 CSS,这些转换后的文件可以被浏览器解析。

  • 自定义处理流程 :开发者可以通过配置一个或多个 Loader 来定义一个处理流程,例如,可以使用 babel-loader 将 ES6 代码转换为兼容的 JavaScript,使用 style-loadercss-loader 处理样式文件。

  • 链式传递:Loader 可以链式调用,每个 Loader 只需专注于完成一种特定的任务。例如,一组 Loader 可以先将 SASS 转为 CSS,再将 CSS 转为 JavaScript 模块。

所以我们可以简单地将 loader 看成一个转化代码的函数。

举个例子,比如 tsx/jsx -> js

于是,我们的方向即为在提取文件源代码的同时,加入一个机制,对文件进行转译

思路

上面已经确定了方向,在提取文件源代码的同时,加入一个机制,对文件进行转译 ,这里我们需要关注的主要两个点,即时机转译机制

时机

提取文件源代码的时机,我们放在创建依赖图的时候。

在构建依赖图时执行,主要是因为:

  • 转换代码:Loader 将非标准 JavaScript 代码(如 TypeScriptSass)转换为标准 JavaScript,这对构建正确的依赖图是必要的。

  • 解析依赖:在依赖图构建期间,bundler 需要理解和解析文件之间的依赖关系,loader 帮助它处理非 JavaScript 资源,确保这些依赖被正确识别。

转译机制:

由于一个文件可能会有多个 loader 来执行,所以我们可以抽一个 Record<string, Array<Loader>> 来处理。

其实在 webpack 中应该还有一套优先级的处理方式,但在这里就先不列出来了。

具体实现

思路大概定了,那么我们就具体看看实现

loaders 数据结构

上文也讲到了,我们直接用 Record<string, Array<Loader>> 来做简易的实现。

Loader 本质上 是一个文件内容转文件内容,即 string -> string。

所以我们的类型定义为

ts 复制代码
export type Loader = (content: string, filePath: string) => string;

type LoaderMap = Record<string, Loader[]>

时机触发

上文说到,我们从提取文件代码的时候处理即可。之前我们其实已经提供了 createModule 函数,所以我们直接在这个函数做一下改造即可,即我们加入一个 applyLoaders 方法来专门走文件模块解析。

applyLoaders

ts 复制代码
import * as path from "path";
export type Loader = (content: string, filePath: string) => string;

export const applyLoaders = (
  content: string,
  filePath: string,
  loaderMap: Record<string, Loader[]>
): string => {
  const extension = path.extname(filePath);
  const loaders = loaderMap[extension] || [];
  return loaders.reduce(
    (content, loader) => loader(content, filePath),
    content
  );
};

createModule

我们只需要在创建模块的时候,加入 applyLoaders 即可

ts 复制代码
export function createModule(
  filePath: string,
  loaderMap?: Record<string, Loader[]>
): Module {
  const realFilePath = resolveModule(filePath);

  const fileExtension = path.extname(realFilePath);
  let content = fs.readFileSync(realFilePath, "utf-8") || "";

  const dependencies: string[] = [];

  const mapping: Record<string, string> = {};

  if (loaderMap?.[fileExtension]) {
    content = applyLoaders(content, realFilePath, loaderMap);
  }

  const ast = parser.parse(content, {
    sourceType: "module",
  });

  const { code } =
    transformFromAstSync(ast, content, {
      presets: ["@babel/preset-env"],
    }) || {};

  content = code || content;

  traverse(ast, {
    ImportDeclaration({ node }) {
      dependencies.push(node.source.value);
    },
    CallExpression({ node }) {
      if (node.callee.type === "Identifier" && node.callee.name === "require") {
        if (BabelType.isStringLiteral(node.arguments[0])) {
          dependencies.push(node.arguments[0].value);
        }
      }
    },
  });

  dependencies.forEach((dependency) => {
    const dependencyPath = isRelativeOrAbsolutePath(dependency)
      ? path.resolve(filePath, "..", dependency)
      : dependency;

    const absoluteDependencyPath = resolveModule(dependencyPath);
    mapping[dependency] = absoluteDependencyPath;
  });

  return {
    id: path.resolve(realFilePath),
    filePath,
    content,
    dependencies,
    mapping,
  };
}

上方就基本实现了 loader 的解析机制了。下面,我们具体来实战下。

自定义 loader 示范

为了降低理解成本,所以这里我们只实现对 cssts 支持。

所以我们写一个 cssts 的 loader 吧。

注意:由于本次我们的核心在于 loader 机制,而并不关心 loader 的具体实现,所以这么不会描述具体 loader 的实现细节。

ts-loader

ts 复制代码
import * as ts from "typescript";
import * as babel from "@babel/core";
import { Loader } from "../../../loader";

export const tsLoader: Loader = (content, filePath) => {
  // 使用 TypeScript 编译器 API 编译代码
  const transpiled = ts.transpileModule(content, {
    compilerOptions: {
      module: ts.ModuleKind.ES2015,
      target: ts.ScriptTarget.ES5,
    },
  });
  return transpiled.outputText;
};

export const tsLoaderUserBabel: Loader = (content, filePath) => {
  const res = babel.transformSync(content, {
    // Babel 配置,您可以在这里指定预设、插件等
    presets: [
      "@babel/preset-env",
      "@babel/preset-typescript",
      "@babel/preset-react",
    ],
    filename: filePath,
  });

  return res?.code || "";
};

export const getTSLoader = (options: TsLoaderOptions) => {
  return options.useBabel ? tsLoaderUserBabel : tsLoader;
};

interface TsLoaderOptions {
  useBabel?: boolean;
}

css-loader

ts 复制代码
import { Loader } from "../../../loader";

export const cssLoader: Loader = (content, filePath) => {
  const escaped = content.replace(/\n/g, "").replace(/"/g, '\\"');
  return `const style = document.createElement('style');
          style.innerText = "${escaped}";
          document.head.appendChild(style);`;
};

接入

入口文件

ts 复制代码
import { add } from "./add";
import { sub } from "./sub";
import "./index.css";

const value = add(1, 4) + sub(3, 2);

document.body.innerHTML = `<div id="app">with ts loader and css loader: ${value}</div>`;

配置

ts 复制代码
import { run } from "../../src/index";
import * as path from "path";
import HtmlPlugin from "../../src/tool-kit/plugins/html-plugin";
import { getTSLoader } from "../../src/tool-kit/loaders/ts-loader";
import { cssLoader } from "../../src/tool-kit/loaders/css-loader";

const tsLoader = getTSLoader({ useBabel: true });

const filePath = path.resolve(__dirname, "./code/index.ts");

const outputPath = path.resolve(__dirname, "./dist/bundle.js");

run({
  rootPath: path.resolve(__dirname, "./code"),
  entry: filePath,
  output: outputPath,
  loaders: {
    ".css": [cssLoader],
    ".ts": [tsLoader],
  },
  plugins: [new HtmlPlugin()],
  devServer: {
    rootPath: path.resolve(__dirname),
    hot: true,
  },
  watch: true,
});

效果

从效果来看确实正确解析了 tscss 文件

总结

通过 loader 能力,我们可以去解析 tsximagecss 等文件。本质上是做模块转化

由于技术细节的实现差异,以及 mini-bundler 的定位,所以是不支持 webpack 生态已有的插件。 这里的定位,更多是为了让读者能够了解这个流程。

最后,你也可以实现自己的 loader 机制。

参考

相关推荐
李高钢5 小时前
Python Flask 框架入门:从零搭建你的第一个 Web 应用
前端·python·flask
特立独行的猫A5 小时前
用仓颉语言写 Coding Agent:cjh 是怎么实现的
前端
特立独行的猫A5 小时前
cjh:基于华为仓颉语言的原生 Coding Agent Harness 实践与设计思考
前端
mONESY5 小时前
LangChain 结构化输出三兄弟:ToolCall、OutputParser、withStructuredOutput 彻底讲透
javascript
晚安日记wanna5 小时前
大文件分片上传的五层追问从 Blob 切片到秒传与弱网容错
前端·面试
Htr_5 小时前
Anysite.io 使用指南:把整个 Web 变成 AI 智能体的数据库
前端·数据库·人工智能
知兀5 小时前
【前端】受控和非受控组件
前端·javascript·react
玉宇夕落5 小时前
别再把 SSE 当普通流式了!从 EventSource 到 LangChain,一次讲透实时推送与流式输出
前端
CappuccinoRose5 小时前
专项事件体系
前端·学习·网络状态·表单事件·剪贴板事件·拖放事件·页面可见性
李剑一5 小时前
各位产品经理求你们优化一下吧,暗黑模式最大的BUG!文中附解决方案
前端