简易版webpack
实现思路:创建webpack.config.js,设置基础配置,webpack读取配置,从配置当中分析入口模块,以及其依赖模块,并且分析内容(@babel/parser),返回一个ast抽象语法树,最后编译内容(@babel/traverse),最后递归分析编译依赖的依赖,生成bundle.js(可以直接在浏览器执行的js)
ast 抽象语法树
ast 节点分析器:astexplorer.net/
准备插件
@babel/parser:帮助我们分析内部的语法,包括es6,返回⼀个ast抽象语法。
@babel/traverse:可以用来遍历更新@babel/parser生成的AST 进入节点(enter) 退出节点 (exit)
@babel/core:是把 js 代码分析成 ast ,方便各个插件分析语法进行相应的处理。 //有些新语法在低版本 js 中是不存在的,如箭头函数,rest 参数,函数默认值等,这种语言层面的不兼容只能通过将代码转为 ast,分析其语法后再转为低版本 js。
@babel/preset-env:(1) 将尚未被大部分浏览器支援的JavaScript 语法转换成能被浏览器支援的语法。(2) 让较旧的浏览器也能支援大部分浏览器能支援的语法,例如Promise、Map、Set等。
把代码处理成浏览器可运⾏的代码,需要借助@babel/core,和 @babel/preset-env,把ast语法树转换成合适的代码
流程解析
开始
webpack生成的编译入口:
(function (modules) {
var installedModules = {};
// 闭包
function __webpack_require__(moduleId) {
// 存在缓存 直接返回
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 没有缓存 则做一个缓存
var module = (installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {},
});
// 调用依赖时 给依赖传参
modules[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__
);
module.l = true;
return module.exports;
}
//默认moduleId = 0 后面自己传
return __webpack_require__((__webpack_require__.s = 0));
})([
/* 0 */
function (module, exports, __webpack_require__) {
var b = __webpack_require__(1);
b();
},
/* 1 */
function (module, exports) {
module.exports = function () {
console.log(11);
};
},
]);
//自执行函数 传入依赖参数
项目结构
npm init初始化项目
webpack.config.js
const path = require("path");
module.exports = {
entry: "./src/index.js",
mode: "development",
output: {
path: path.resolve(__dirname, "./dist"),
filename: "main.js"
}
}
bundle.js
const options = require('./webpack.config.js')
const Webpack = require('./lib/webpack.js')
new Webpack(options).run()
./lib/webpack.js (重点)
module.exports = class Webpack {
constructor(params) {
const { entry, output } = params;
this.output = output;
this.entry = entry;
this.modules = [];
}
}
run 方法
首先先解析入口文件,将所有依赖的子模块缓存起来。然后遍历这个数组再去收集子模块的依赖。通过扁平化收集到所有的依赖模块和对应的可执行的代码。
const info = this.parse(this.entry);
// console.log("Webpack -> run -> info", info)
this.modules.push(info);
//入口依赖的依赖 开始扁平化
for (let i = 0; i < this.modules.length; i++) {
const item = this.modules[i];
const { dependencies } = item;
if (dependencies) {
for (let j in dependencies) {
this.modules.push(this.parse(dependencies[j])); //依赖扁平化
}
}
}
const obj = {}
this.modules.forEach( item => {
obj[item.entryFile] = {
dependencies: item.dependencies,
code: item.code
}
})
console.log(obj)
//生成bundle.js bundle.js里面的代码可以直接在浏览器中运行
this.file(obj)
parse 方法
parse(entryFile) {
console.log("Webpack -> parse -> entryFile", entryFile)
//分析入口模块内容
//读取到入口文件中的编码内容 content 为./src/index.js的编码内容
const content = fs.readFileSync(entryFile, "utf-8");
// console.log(content);
//分析入口文件有哪些依赖 自己依赖路径
//把内容通过parse抽象成语法树便于分析 提取
const ast = parser.parse(content, {
sourceType: "module", //es module语法
});
// console.log(ast)
//每一行代码都会有一个node节点解析
//如果是import 语法 就是可以获取到 value 然后借助@babel/traverse处理 得到value
// console.log(ast.program.body);
//提取依赖
//依赖模块的缓存数组
const dependencies = {};
traverse(ast, {
//提取类型为ast.program.body打印出来的导入 ImportDeclaration 提取ImportDeclaration中的node
ImportDeclaration({ node }) {
//得到模块路径 这个路径不是最终 要./expo.js => ./src/expo.js
console.log(node.source.value);
// console.log(path.dirname(entryFile));
const sourceValue = node.source.value + '.js';
const newPathName =
"./" + path.join(path.dirname(entryFile), sourceValue);
//当前文件所有依赖的模块
dependencies[node.source.value] = newPathName.replace("\\", "/");
},
});
// 处理内容 转换ast
// 把代码处理成浏览器可运⾏的代码,需要借助@babel/core,和 @babel/preset-env,把ast语法树转换成合适的代码
//返回一段可执行的代码 但是现在不能直接在浏览器执行
const { code } = transformFromAst(ast, null, {
presets: ["@babel/preset-env"],
});
// console.log(code);
/**
* "use strict";
//浏览器不认识require
var _a = _interopRequireDefault(require("./a"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
console.log("hhhhh");
console.log(_a["default"].add(12, 1));
*/
return {
entryFile,
dependencies,
code,
};
}
ast 抽象语法树:content里面的每一行代码都会有一个node节点解析,如果是import 语法 就是可以获取到 value 然后借助@babel/traverse处理 得到value
./src/index.js
ast.program.body:./src/index.js有4行代码,第一行是表达式所以type是ExpressionStatement,二三两行是import所以type是ImportDeclaration
node.source.value
dependencies:
obj:
file 方法
//执行代码 自执行函数 实现require
// 生成bundle.js => ./dist/main.js (手动创建dist文件夹)
file(code){
// 生成bundle.js => ./dist/main.js
const filePath = path.join(this.output.path,this.output.filename )
//手动创建dist 目录
const newCode = JSON.stringify(code)
//graph 就是本模块的依赖和可执行代码对象 obj
const bundle = `(function(graph){
//module 模块名 如 ./src/index.js
function require(module){
//localRequire 引入模块的依赖
function localRequire(relativePath){
//obj['./src/index.js']
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
})(${newCode})`;
// 执行node bundle.js 会在dist目录下生成main.js,里面的代码内容为newCode 也就上面的obj内容
fs.writeFileSync(filePath,bundle,"utf-8")
}
执行node bunlde.js
生成的./dist/main.js
(function(graph){
function require(module){
function localRequire(relativePath){
//这边又调用了require, 比如code中的 require(\"./a\")
//就是调用localRequire('./a'),就是又开始了a的子模块的代码执 //行。
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
})({"./src/index.js":{"dependencies":{"./a":"./src/a.js","./b":"./src/b.js"},"code":"\"use strict\";\n\nvar _a = require(\"./a\");\n\nvar _b = _interopRequireDefault(require(\"./b\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log(\"hhhhh\");\nconsole.log((0, _a.add)(12, 1));"},"./src/a.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.minus = exports.add = void 0;\n\nvar add = function add(a, b) {\n console.log(a + b);\n};\n\nexports.add = add;\n\nvar minus = function minus(a, b) {\n console.log(a - b);\n};\n\nexports.minus = minus;"},"./src/b.js":{"dependencies":{"./c":"./src/c.js"},"code":"\"use strict\";\n\nvar _c = _interopRequireDefault(require(\"./c\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log(\"xixixiixbb\");"},"./src/c.js":{"dependencies":{},"code":"\"use strict\";\n\nconsole.log(\"hhhhhhhhh\");"}})
拿入口文件生成的code解析一下执行:
这个require就是localRequire方法,参数就是localRequire的relativePath,这样就引入子模块了。
"\"use strict\";
//这个require就是localRequire方法,参数就是localRequire的relativePath,这样就引入子模块了。
var _a = require(\"./a\");
var _b = _interopRequireDefault(require(\"./b\"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }
console.log(\"hhhhh\");
console.log((0, _a.add)(12, 1));"
复制到浏览器执行