Webpack解决的问题
Webpack做的事情简单来说就一件事情打包,先看下边这段简单的代码
这段代码不能在浏览器中直接运行,因为浏览器不支持运行带有import和export关键字的代码,现代浏览器可以通过<script type=module>来支持,但是ie浏览器不支持,当项目比较大的时候我们对于代码拆分为多个文件的需求有很多,所以我们对于这个问题急需要解决。
webpack就是提供给解决这个问题的一个方案:把关键字转译为普通代码,并把所有文件打包成一个文件。
babel原理
要将代码打包webpack就需要借助babel对代码进行解析、转译等等工作。
babel工作步骤
babel转译代码分为三个步骤:
- 解读代码生成ast树
- 遍历ast树修改树节点属性生成新的ast树
- 通过ast生成代码
简单案例
通过一个简单案例,来理解下babel转换的过程,这个例子是个简单的将let 转化为var的过程:
import { parse } from "[@babel](/user/babel)/core";
import traverse from "[@babel](/user/babel)/traverse";
import generate from "[@babel](/user/babel)/generator";
const code = `let a = 'let'; let b = 2`;
const ast = parse(code, { sourceType: "module" });
if (ast) {
traverse(ast, {
enter: (item) => {
if (item.node.type === 'VariableDeclaration') {
if (item.node.kind === "let") {
item.node.kind = "var";
}
}
},
});
const result = generate(ast, {}, code);
console.log(result.code);
}
先使用parse库将代码转换为ast树,然后通过traverse库遍历ast树,将所有类型声明语句中的类型从var改成let,最终结果为:
babel库
import的几个库就是对应babel的转换过程:
生成ast树 @babel/core
遍历ast树 @babel/traverse
ast树生成代码 @babel/generator
AST
AST是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,将上边的ast对象打印出来格式如下:
ast是一个对象格式,program就是对代码做的整体抽象,例子中有两条声明语句,所以这里body数组中就是两个节点对象。
type属性表示这个语句的类型,例子中是个变量声明,kind表示这个语句用的是var声明变量。declarations语句由是一个节点数组,因为一个变量声明语句可以声明多个变量,declarations数组中的节点结构如下图:
es6转es5案例
一个频繁使用babel的场景就是将js的新特性转换成低版本的js代码,要将新版本js转换成低版本js需要使用@babel/preset-env库,告诉babel新老版本js之间的转换关系。
import * as fs from "fs";
import { parse } from "[@babel](/user/babel)/core";
import * as babel from "[@babel](/user/babel)/core";
const code = fs.readFileSync("./test.js").toString();
const ast = parse(code, { sourceType: "module" });
if (ast) {
const result = babel.transformFromAstSync(ast, code, {
presets: ["[@babel](/user/babel)/preset-env"],
});
if (result?.code) {
fs.writeFileSync("./test.es5.js", result.code);
}
}
test.js原代码:
let a = "let";
let b = 2;
const c = 3;
function sum() {
let a = 1,
b = 1;
return a + b;
}
转义后的代码:
"use strict";
var a = "let";
var b = 2;
var c = 3;
function sum() {
var a = 1,
b = 1;
return a + b;
}
打包文件
处理文件依赖关系
案例
通过一个简单的例子来了解webpack是怎么将几个文件的代码打包成一个文件。
代码结构
project_1/index.js
import a from "./a.js";
import b from "./b.js";
console.log(a.value + b.value);
project_1/a.js
const a = {
value: 1,
}
export default a
project_1/b.js
const b = {
value: 2,
}
export default b
打包文件代码
import * as babel from "[@babel](/user/babel)/core";
import { parse } from "[@babel](/user/babel)/parser";
import traverse from "[@babel](/user/babel)/traverse";
import { readFileSync, writeFileSync } from "fs";
import { resolve, relative, dirname } from "path";
// 设置根目录
const projectRoot = resolve(__dirname, "project_2");
interface Dep {
key: string;
deps: string[];
code: string;
}
// 类型声明
type DepRelation = Dep[];
// 初始化一个空的 depRelation,用于收集依赖
const depRelation: DepRelation = []; // 数组!
// 将入口文件的绝对路径传入函数,如 D:\demo\fixture_1\index.js
collectCodeAndDeps(resolve(projectRoot, "index.js"));
console.log(depRelation);
console.log("done");
/**
*
* [@param](/user/param) filepath 文件绝对地址
*/
function collectCodeAndDeps(filepath: string) {
const key = getProjectPath(filepath); // 文件的项目路径,如 index.js
if (depRelation.find((i) => i.key === key)) {
// 注意,重复依赖不一定是循环依赖
return;
}
// 获取文件内容,将内容放至 depRelation
const code = readFileSync(filepath).toString();
// 初始化 depRelation[key]
// 将代码转为 AST
const transformCode = babel.transform(code, {
presets: ["[@babel](/user/babel)/preset-env"],
});
const es5Code = transformCode?.code;
if (!es5Code) {
return;
}
const item: Dep = { key, deps: [], code: es5Code };
depRelation.push(item);
const ast = parse(code, { sourceType: "module" });
// 分析文件依赖,将内容放至 depRelation
traverse(ast, {
enter: (path) => {
if (path.node.type === "ImportDeclaration") {
// path.node.source.value 往往是一个相对路径,如 ./a.js,需要先把它转为一个绝对路径
const depAbsolutePath = resolve(
dirname(filepath),
path.node.source.value
);
// 然后转为项目路径
const depProjectPath = getProjectPath(depAbsolutePath);
// 把依赖写进 depRelation
item.deps.push(depProjectPath);
collectCodeAndDeps(depAbsolutePath);
}
},
});
}
// 获取文件相对于根目录的相对路径
function getProjectPath(path: string) {
return relative(projectRoot, path).replace(/\\/g, "/");
}
代码思路
depRelation是依赖数组,存储所有的文件依赖,第一个元素是入口文件的依赖,depRelation数据项格式{ deps: [‘依赖的文件路径’], code: ‘文件的源码’ }
collectCodeAndDeps是处理文件依赖的主函数,主要步骤是:
- 判断文件路径是否已经存在依赖数组中,如果已经存在不再重复处理
- 根据文件路径读取源代码
- 将源代码准换ast树
- 通过@babel/preset-env将代码转义然后存在依赖数据项的code属性中
- 遍历ast树,找到import语句,存入deps属性中,然后取出import路径,递归调用collectCodeAndDeps
打包后代码执行
最终需要通过生成的依赖关系数组对象,生成可以执行的代码,生成最终代码的函数如下:
function generateCode() {
let code = "";
code +=
"var depRelation = [" +
depRelation
.map((item) => {
const { key, code, deps } = item;
return `{
key: ${JSON.stringify(key)},
deps: ${JSON.stringify(deps)},
code: function(require, module, exports){
${code}
}
}`;
})
.join(",") +
"];\n";
code += "var modules = {};\n";
code += `execute(depRelation[0].key)\n`;
code += `
function execute(key) {
if (modules[key]) { return modules[key] }
var item = depRelation.find(i => i.key === key)
if (!item) { throw new Error(\`\${item} is not found\`) }
var pathToKey = (path) => {
var dirname = key.substring(0, key.lastIndexOf('/') + 1)
var projectPath = (dirname + path).replace(\/\\.\\\/\/g, '').replace(\/\\\/\\\/\/, '/')
return projectPath
}
var require = (path) => {
return execute(pathToKey(path))
}
modules[key] = { __esModule: true }
var module = { exports: modules[key] }
item.code(require, module, module.exports)
return modules[key]
}
`;
return code;
}
- 通过依赖关系数组拼装依赖对象,将code替换为方法
- 使用modules对象存储各个文件export出去的内容
- 自定义require函数替换每个文件中require方法,用modules对象替换每个文件的export方法对象
- pathToKey方法处理"./"这种import
最终代码
import * as babel from "[@babel](/user/babel)/core";
import { parse } from "[@babel](/user/babel)/parser";
import traverse from "[@babel](/user/babel)/traverse";
import { readFileSync, writeFileSync } from "fs";
import { resolve, relative, dirname } from "path";
// 设置根目录
const projectRoot = resolve(__dirname, "project_2");
interface Dep {
key: string;
deps: string[];
code: string;
}
// 类型声明
type DepRelation = Dep[];
// 初始化一个空的 depRelation,用于收集依赖
const depRelation: DepRelation = []; // 数组!
// 将入口文件的绝对路径传入函数,如 D:\demo\fixture_1\index.js
collectCodeAndDeps(resolve(projectRoot, "index.js"));
function generateCode() {
let code = "";
code +=
"var depRelation = [" +
depRelation
.map((item) => {
const { key, code, deps } = item;
return `{
key: ${JSON.stringify(key)},
deps: ${JSON.stringify(deps)},
code: function(require, module, exports){
${code}
}
}`;
})
.join(",") +
"];\n";
code += "var modules = {};\n";
code += `execute(depRelation[0].key)\n`;
code += `
function execute(key) {
if (modules[key]) { return modules[key] }
var item = depRelation.find(i => i.key === key)
if (!item) { throw new Error(\`\${item} is not found\`) }
var pathToKey = (path) => {
var dirname = key.substring(0, key.lastIndexOf('/') + 1)
var projectPath = (dirname + path).replace(\/\\.\\\/\/g, '').replace(\/\\\/\\\/\/, '/')
return projectPath
}
var require = (path) => {
return execute(pathToKey(path))
}
modules[key] = { __esModule: true }
var module = { exports: modules[key] }
item.code(require, module, module.exports)
return modules[key]
}
`;
return code;
}
writeFileSync("dist_2.js", generateCode());
console.log(depRelation);
console.log("done");
/**
*
* [@param](/user/param) filepath 文件绝对地址
*/
function collectCodeAndDeps(filepath: string) {
const key = getProjectPath(filepath); // 文件的项目路径,如 index.js
if (depRelation.find((i) => i.key === key)) {
// 注意,重复依赖不一定是循环依赖
return;
}
// 获取文件内容,将内容放至 depRelation
const code = readFileSync(filepath).toString();
// 初始化 depRelation[key]
// 将代码转为 AST
const transformCode = babel.transform(code, {
presets: ["[@babel](/user/babel)/preset-env"],
});
const es5Code = transformCode?.code;
if (!es5Code) {
return;
}
const item: Dep = { key, deps: [], code: es5Code };
depRelation.push(item);
const ast = parse(code, { sourceType: "module" });
// 分析文件依赖,将内容放至 depRelation
traverse(ast, {
enter: (path) => {
if (path.node.type === "ImportDeclaration") {
// path.node.source.value 往往是一个相对路径,如 ./a.js,需要先把它转为一个绝对路径
const depAbsolutePath = resolve(
dirname(filepath),
path.node.source.value
);
// 然后转为项目路径
const depProjectPath = getProjectPath(depAbsolutePath);
// 把依赖写进 depRelation
item.deps.push(depProjectPath);
collectCodeAndDeps(depAbsolutePath);
}
},
});
}
// 获取文件相对于根目录的相对路径
function getProjectPath(path: string) {
return relative(projectRoot, path).replace(/\\/g, "/");
}