前言
本文主要介绍Webpack打包流程和其中的工作细节,会使用到Babel、TypeScript等相关技术
有关Webpack基本使用请参考文章Webpack基本使用
环境搭建
- 初始化项目,执行命令
npm init -y
tsc --init
- 根目录下新建
webpack.config.ts,作为webpack的配置文件,内容如下
import path from "node:path";
export default {
entry: "./main.js",
output: {
filename: "bundle.js",
path: path.resolve(process.cwd(), "dist")
}
}
1、使用
ESM导入模块(导入node模块会存在代码提示),因此使用process.cwd()而非__dirname2、若导入
path模块报错:Cannot find module 'node:path' or its corresponding type declarations.,执行npm install @types/node -D安装其类型声明文件即可,其他同类报错同理
- 根目录下新建
main.js和app.js用例文件
// main.js
import add from "./app.js";
var a = 1;
add(a, 2); // 3
// app.js
var a = 1;
const add = (a, b) => {
return a + b;
}
export default add;
- 根目录下新建
lib/index.ts文件 - 修改
package.json
{
"name": "webpack_source_code",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "nodemon lib/index.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/node": "^20.12.7"
}
}
Webpack Lib库
搭建框架
- 新建
lib/build/index.ts用于编写打包相关的函数
import fs from "node:fs";
import path from "node:path";
export function createAssets(filePath: string) {
console.log("Creating assets for", filePath);
const content = fs.readFileSync(path.resolve(process.cwd(), filePath), "utf-8");
console.log(content);
}
- 编写
lib/index.ts文件
import config from "../webpack.config";
import { createAssets } from "./build";
createAssets(config.entry);
- 可以读取到配置文件内容和入口文件内容
处理依赖关系
- 使用
Babel处理依赖关系
Babel又名巴别塔,目的是统一所有语言,主要作用是提供一个过程,例如JS AST --> Transform --> Generate
- 安装依赖
@babel/parser、@babel/traverse,
// 分别用于生成 AST抽象语法树 和 遍历AST
npm install @babel/parser -D
npm install @babel/traverse -D
- 修改
lib/build/index.ts文件处理入口文件及其依赖
import fs from "node:fs";
import path from "node:path";
import { parse } from "@babel/parser";
import traverse from "@babel/traverse";
function createAssets(filePath: string) {
const content = fs.readFileSync(
path.resolve(process.cwd(), filePath),
"utf-8"
);
// sourceType: "module" is required for ES module syntax
const ast = parse(content, { sourceType: "module" });
const deps: string[] = [];
traverse(ast, {
ImportDeclaration: ({ node }) => {
deps.push(node.source.value);
}
});
return {
filePath,
deps
};
}
// 数据结构 使用图的方式来表示
export function createGraph(entry: string) {
const graph = createAssets(entry);
const queue = [graph];
for (let assets of queue) {
assets.deps.forEach((dep: string) => {
const child = createAssets(dep);
queue.push(child);
});
}
return queue;
}
构建输出文件模板
思考:若入口文件及全部依赖都打包到一个输出文件bundle.js中,会发生什么问题?
显而易见,变量重名是最直接的问题,此时可以使用函数来解决,大致模板如下:
function main() {
import add from "./app.js";
var a = 1;
add(a, 2); // 3
}
function app() {
var a = 1;
const add = (a, b) => {
return a + b;
}
export default add;
}
OK的,这样我们就解决了变量重名冲突的问题,同时接踵而来的问题是import只可以用于文件顶部,很显然不可以放在函数体内
而cjs(CommonJS)可以帮助我们解决这个问题,所以接下来的工作是将依赖文件变为函数并且将esm变为cjs
- 根据上述思路,编写如下代码,具体思路是重写
require函数、并使用立即执行函数执行
!(function (modules) {
function require(path) {
const fn = modules[path];
const module = { exports: {} };
fn(require, module, module.exports);
return module.exports;
}
require("./main.js");
})({
"./main.js": function (require, module, exports) {
const add = require("./app.js");
var a = 1;
add(a, 2); // 3
},
"./app.js": function app(require, module, exports) {
var a = 1;
const add = (a, b) => {
return a + b;
}
module.exports = add;
}
});
- 编写模板
由于模板使用ejs编写,所以需要安装依赖ejs,执行命令
npm install ejs -D
模板如下:
!(function (modules) {
function require(path) {
const fn = modules[path];
const module = { exports: {} };
fn(require, module, module.exports);
return module.exports;
}
require("<%- entry %>");
})({
<% data.forEach(function(item) { %>
"<%- item.filePath %>": function (require, module, exports) {
<%- item.code %>
},
<% }); %>
});
- 由于上述模板需要使用变量
data、entry等,所以需要更改lib/build/index.ts文件并在lib/index.ts中执行函数生成模板
新建lib/types/index.ts
export interface Graph {
filePath: string;
deps: string[];
code?: string;
}
export interface Config {
entry: string;
output: {
filename: string;
path: string;
}
}
修改lib/build/index.ts文件,添加build函数用于生成模板并输出到配置文件的指定目录下,同时需要获取到依赖文件的代码并将其转为cjs,此时需要使用到依赖@babel/core和@babel/preset-env,需要提前安装
npm install @babel/core -D
npm install @babel/preset-env -D
更新后的代码如下:
import fs from "node:fs";
import path from "node:path";
import { parse } from "@babel/parser";
import traverse from "@babel/traverse";
import { transformFromAstSync } from "@babel/core";
import ejs from "ejs";
import { Graph, Config } from "../types";
function createAssets(filePath: string) {
const content = fs.readFileSync(
path.resolve(process.cwd(), filePath),
"utf-8"
);
// sourceType: "module" is required for ES module syntax
const ast = parse(content, { sourceType: "module" });
const deps: string[] = [];
traverse(ast, {
ImportDeclaration: ({ node }) => {
deps.push(node.source.value);
},
});
// 获取文件代码并且 esm --> cjs
const source = transformFromAstSync(ast, undefined, {
presets: ["@babel/preset-env"]
});
return {
filePath,
deps,
code: source?.code as string
};
}
// 其它代码...
export function build(graph: Graph[], config: Config) {
const template = fs.readFileSync(path.resolve(process.cwd(), "lib/template/bundle.ejs"), "utf-8");
const content = ejs.render(template, { data: graph, entry: config.entry });
const filename = config.output.filename;
const outputDir = config.output.path;
fs.mkdirSync(outputDir, { recursive: true });
fs.writeFileSync(path.resolve(outputDir, filename), content);
}
修改lib/index.ts执行build函数
import config from "../webpack.config";
import { createGraph, build } from "./build";
const graph = createGraph(config.entry);
build(graph, config);
console.log(graph);
优化输出文件模板
OK的,目前可以进行初步的打包了,但当前模板中的modules依赖映射是使用相对路径或者绝对路径来表示的(取决于编码者),相对路径会存在隐患,所以要将映射部分进行优化
优化后模板如下:
!(function (modules) {
function require(path) {
const [ fn, mapping ] = modules[path];
const module = { exports: {} };
function localRequire(relativePath) {
return require(mapping[relativePath]);
}
fn(localRequire, module, module.exports);
return module.exports;
}
require("<%- entry %>");
})({
<% data.forEach(function(item) { %>
"<%- item.id %>": [function (require, module, exports) {
<%- item.code %>
}, <%- JSON.stringify(item.mapping) %>],
<% }); %>
});
其它代码请自行查阅源代码部分
Loader
Loader 实质上是一个函数,主要在打包前被调用
import { Config } from "../types";
export function Loader(source: string, config: Config) {
const module = config.rules.module;
module.forEach((item) => {
if (Array.isArray(item.use)) {
item.use.reverse().forEach((fn) => {
source = fn(source);
});
} else {
source = item.use(source);
}
})
return source;
}
注意:若
Loader的use部分是函数数组,需要按照从后往前的顺序执行,并且将前一次的结果传给下一次