概览
获取配置 根据配置信息启动webpack,执行构建
- 从入口模块开始分析,有哪些依赖,转换代码;
- 递归的分析其他依赖模块,有哪些依赖,转换代码;
- 生成可以在浏览器端执行的bundle文件。
自己实现一个bundle.js
模块分析
读取入口文件,分析代码:
let fs = require("fs");
let build = entryFile => {
let content = fs.readFileSync(entryFile, "utf-8");
};
build("./index.js");
生成AST抽象语法树
这里我们推荐使用@babel/parser,这是babel7 的工具,来帮助我们分析内部的语法,包括es6,返回一个ast抽象语法树
//安装@babel/parser
yarn add @babel/parser -D
let fs = require("fs");
const parser = require("@babel/parser");
let build = entryFile => {
let content = fs.readFileSync(entryFile, "utf-8");
const Ast = parser.parse(content, {
sourceType: "module"
});
};
build("./index.js");
通过parser.parse方法,可以将我们的index.js文件内容转换成AST抽象语法树
//AST抽象语法树
{
type: 'File',
start: 0,
end: 155,
loc: SourceLocation {
start: Position { line: 1, column: 0 },
end: Position { line: 6, column: 0 }
},
errors: [],
program: Node {
type: 'Program',
start: 0,
end: 155,
loc: SourceLocation { start: [Position], end: [Position] },
sourceType: 'module',
interpreter: null,
body: [ [Node], [Node], [Node], [Node] ],
directives: []
},
comments: []
}
拿到文件中的依赖
接下来我们就可以根据AST里面的分析结果,遍历出所有的引入模块,但是比较麻烦,这里还是推荐babel推荐的一个模块,@babel/traverse,来帮我们处理。
//安装@babel/parser
yarn add @babel/traverse -D
const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
let build = entryFile => {
let content = fs.readFileSync(entryFile, "utf-8");
const Ast = parser.parse(content, {
sourceType: "module"
});
const dependencies = {}; //可以保留相对路径和根路径两种信息
traverse(ast, {
ImportDeclaration({ node }) {
// dependencies.push(node.source.value); 相对路径
//分析ast抽象语法树,返回依赖模块的绝对路径。
const dirname = path.dirname(entryFile);
const newPath = "./" + path.join(dirname, node.source.value);
dependencies[node.source.value] = newPath;
},
});
};
build("./index.js");
在index.js中依赖模块是同级目录下的hello.js,
import { say } from "./hello.js";
let str = "hello" + say("webpack");
document.body.innerHTML = `<h1>${str}</h1>`;
console.log("hello" + say("webpack"));
//dependencies值:
{ './hello.js': './src\\hello.js' }
转换成浏览器可以识别的代码
把代码处理成浏览器可运行的代码,需要借助@babel/core,和@babel/preset-env,把ast语法树转换成合适的代码。
//安装@babel/preset-env和@babel/core。 yarn add @babel/preset-env -D yarn add @babel/core -D
const { transformFromAst } = require("@babel/core");
const { code } = transformFromAst(Ast, null, {presets: ["@babel/preset-env"]});
将以上三个方法放置在一个解析文件里面:
const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const { transformFromAst } = require("@babel/core");
module.exports = {
//分析模块 获得AST
getAst: (fileName) => {
let content = fs.readFileSync(fileName, "utf-8");
let ast = parser.parse(content, {
sourceType: "module",
});
return ast;
},
//获取依赖
getDependencies: (ast, fileName) => {
const dependencies = {}; //可以保留相对路径和根路径两种信息
traverse(ast, {
ImportDeclaration({ node }) {
// dependencies.push(node.source.value); 相对路径
const dirname = path.dirname(fileName);
const newPath = "./" + path.join(dirname, node.source.value);
dependencies[node.source.value] = newPath;
},
});
console.log(dependencies);
return dependencies;
},
//转换代码
getCode: (ast) => {
const { code } = transformFromAst(ast, null, {
presets: ["@babel/preset-env"],
});
return code;
},
};
返回的信息
{
fileName: './src/index.js',
dependencies: { './hello.js': './src\\hello.js' },
code: '"use strict";\n' +
'\n' +
'var _hello = require("./hello.js");\n' +
'\n' +
'var str = "hello" + (0, _hello.say)("webpack");\n' +
'document.body.innerHTML = "<h1>".concat(str, "</h1>");\n' +
'console.log("hello" + (0, _hello.say)("webpack"));'
}
在Complier编译文件里面引入这三个方法
const fs = require("fs");
const path = require("path");
const { getAst, getDependencies, getCode } = require("./parser");
module.exports = class Complier {
constructor(options) {
this.entry = options.entry;
this.output = options.output;
this.modules = [];
}
run() {
const info = this.build(this.entry);
this.modules.push(info);
for (let i = 0; i < this.modules.length; i++) {
const item = this.modules[i];
const { dependencies } = item;
if (dependencies) {
// 通过循环遍历所有的模块,生成所有模块的信息放入modules里面
for (let j in dependencies) {
this.modules.push(this.build(dependencies[j]));
}
}
}
//转换数据结构
const obj = {};
this.modules.forEach((item) => {
obj[item.fileName] = {
dependencies: item.dependencies,
code: item.code,
};
});
//生成代码文件
this.file(obj);
}
//在build方法里面引入这三个方法,发挥一个对象,包含了文件路径,依赖模块,可执行代码
build(fileName) {
let ast = getAst(fileName);
let dependencies = getDependencies(ast, fileName);
let code = getCode(ast);
return {
fileName,
dependencies,
code,
};
}
file(code) {
//获取输出信息 .../dist/main.js
const filePath = path.join(this.output.path, this.output.filename);
const newCode = JSON.stringify(code);
const bundle = `(function(graph){
function require(module){
function localRequire(relativePath){
return require(graph[module].dependencies[relativePath])
}
var exports = {};
(function(require,exports,code){
eval(code)
})(localRequire,exports,graph[module].code)
return exports;
}
require('${this.entry}') //./src/index.js
})(${newCode})`;
fs.writeFileSync(filePath, bundle, "utf-8");
}
};
生成webpack.js文件模仿webpack命令,生成可以在浏览器端执行的bundle文件
const Complier = require("./lib/complier");
const options = require("./webpack.config.js");
new Complier(options).run();
以下为webpack.config.js文件
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
filename: "main.js",
path: path.resolve(__dirname, "./dist")
}
};
执行node webpack.js命令,生成的main.js文件
(function (graph) {
function require(module) {
function localRequire(relativePath) {
return require(graph[module].dependencies[relativePath]);
}
var exports = {};
(function (require, exports, code) {
eval(code);
})(localRequire, exports, graph[module].code);
return exports;
}
require("./src/index.js"); //./src/index.js
})({
"./src/index.js": {
dependencies: { "./hello.js": "./src\\hello.js" },
code:
'"use strict";\n\nvar _hello = require("./hello.js");\n\nvar str = "hello" + (0, _hello.say)("webpack");\ndocument.body.innerHTML = "<h1>".concat(str, "</h1>");\nconsole.log("hello" + (0, _hello.say)("webpack"));',
},
"./src\\hello.js": {
dependencies: {},
code:
'"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports.say = say;\n\nfunction say(str) {\n return str;\n}',
},
});
到这里,webpack整体流程执行完毕,webpack的核心是通过AST抽象语法树将各个模块紧密关联起来,生成一个紧密结合的文件,供浏览器执行。