学习笔记 参考文章:https://juejin.cn/post/6854573217336541192 项目地址:https://gitee.com/cjperfect/webpack–core-theorem
打包主要流程
- 读取入口文件内容
- 根据入口文件,递归读取引入文件所依赖的文件内容,生成AST语法树
- 根据AST语法树,生成浏览器能够运行的代码
项目目录和基础代码
获取模板内容(入口文件内容)
直接打开html文件,发现报错:Uncaught SyntaxError: Cannot use import statement outside a module,因为浏览器无法识别importES6语法,除非<script src="./src/index.js" type="module"></script> ,添加一个type="module"属性,浏览器才能识别。
创建bundle.js文件,里面包含所有打包逻辑。
const fs = require("fs");
const getModuleInfo = (file) => {
const body = fs.readFileSync(file, "utf-8");
console.log(body);
}
getModuleInfo("./src/index.js");
输出结果:
分析模块
分析模板主要任务是将获取到的模板内容解析成AST语法树 疑问:
- 什么要将获取到的模块内容 通过babel解析成 AST 语法树
Babel 是一个 JS 编译器,概括起来讲,它有三个运行代码的阶段:解析阶段、转换阶段、生成阶段。 我们给 Babel 一些 JS 代码,他会修改并且生成新的代码,它如何修改代码?确切的来说,Babel 通过构建 AST,然后遍历 AST,根据应用的插件对其进行修改,然后从修改的 AST 中生成新的代码。(目的就是将浏览器无法识别的代码,转成可以识别的)
所需要依赖包
yarn add @babel/parser
更新代码
const fs = require('fs');
const parser = require('@babel/parser');
const getModuleInfo = (file)=>{
const body = fs.readFileSync(file,'utf-8');
const ast = parser.parse(body,{
sourceType:'module'
});
console.log(ast.program.body);
}
getModuleInfo("./src/index.js");
babelParser.parse(code, [options]) sourceType: 指示分析代码的模式。可以是"script", “module"或"unambiguous"之一。默认为"script”。 “unambiguous"将使@babel/parser尝试根据存在的ES6导入或导出语句进行猜测。带有ES6 import和export的文件被视为"module”,否则是"script"。
官方网址:https://babeljs.io/docs/en/babel-parser
输出结果:
收集入口文件依赖
将用import语句引入的文件路径收集起来。我们将收集起来的路径放到deps里。
所需要的依赖
yarn add @babel/traverse
更新代码
const fs = require("fs");
const path = require("path");
const traverse = require("@babel/traverse").default;
const parser = require("@babel/parser");
const getModuleInfo = (file) => {
const body = fs.readFileSync(file, "utf-8");
const ast = parser.parse(body, {
sourceType: "module",
});
console.log(ast.program.body)
const deps = {};
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(file);
const importPath = node.source.value;
const abspath = "./" + path.join(dirname, importPath);
deps[importPath] = abspath;
},
});
};
@babel/traverse 可以用来遍历更新 @babel/parser生成的AST
官方网址:https://www.babeljs.cn/docs/babel-traverse
ES6转成ES5
需要把获得的ES6的AST转化成ES5
所需要的依赖
yarn add @babel/core @babel/preset-env
更新代码
const fs = require("fs");
const path = require("path");
const traverse = require("@babel/traverse").default;
const parser = require("@babel/parser");
const babel = require("@babel/core");
const getModuleInfo = (file) => {
const body = fs.readFileSync(file, "utf-8");
const ast = parser.parse(body, {
sourceType: "module",
});
const deps = {};
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(file);
const importPath = node.source.value;
const abspath = "./" + path.join(dirname, importPath);
deps[importPath] = abspath;
},
});
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"],
});
console.log(code);
};
输出结果:
@babel/preset-env 根据指定的执行环境提供语法转换 所以我们需要指定执行环境 Browserslist, Browserslist 的配置有几种方式,并按下面的优先级使用:
- @babel/preset-env 里的 targets
- package.json 里的 browserslist 字段
- .browserslistrc 配置文件
官方网址: https://www.babeljs.cn/docs/babel-preset-env
递归获取所有依赖
入口文件import对应的文件,里面可能也存在import,所以需要递归找个每个文件所需要的依赖文件(import)
更新代码
const fs = require("fs");
const path = require("path");
const traverse = require("@babel/traverse").default;
const parser = require("@babel/parser");
const babel = require("@babel/core");
const getModuleInfo = (file) => {
const body = fs.readFileSync(file, "utf-8");
const ast = parser.parse(body, {
sourceType: "module",
});
const deps = {};
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(file);
const importPath = node.source.value;
const abspath = "./" + path.join(dirname, importPath);
deps[importPath] = abspath;
},
});
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"],
});
const moduleInfo = { file, deps, code };
return moduleInfo;
};
const parseModules = (file) => {
const entry = getModuleInfo(file);
const allModuleInfo = [entry];
const depsGraph = {};
allModuleInfo.forEach((module) => {
const deps = module.deps;
if (deps) {
for (const key in deps) {
allModuleInfo.push(getModuleInfo(deps[key]));
}
}
});
allModuleInfo.forEach((moduleInfo) => {
depsGraph[moduleInfo.file] = {
deps: moduleInfo.deps,
code: moduleInfo.code,
};
});
return depsGraph;
};
输出结果:
处理两个关键字(require,exports)
我们现在是不能执行index.js这段代码的,因为浏览器不会识别执行require和exports。 不能识别的原因就是没有定义这require函数 ,和exports对象 。那我们可以自己定义。
更新代码
...代码
const bundle = (file) => {
const depsGraph = JSON.stringify(parseModules(file));
return `(function (graph) {
function require(file) {
function absoluteRequire(realPath) {
return require(graph[file].deps[realPath]);
}
const exports = {};
(function (require, exports, code) {
eval(code);
})(absoluteRequire, exports, graph[file].code);
return exports;
}
require('${file}');
})(${depsGraph})`;
};
解析返回的代码:
======================================第一步开始:======================================
(function (graph) {
function require(file) {
(function (code) {
eval(code)
})(graph[file].code)
}
require(file)
})(depsGraph)
1. 将depsGraph,传入一个立即执行函数。也就是上一张截图的内容
2. 将入口文件的路径传入require函数执行
3. 执行require函数,会调用立即执行函数,传递code。(在js中require就是加载指定路径对应文件)
4. 执行eval(code),相当于执行了入口文件的代码
======================================第一步结束:======================================
******index.js代码******
"use strict";\n' +
'\n' +
'var _add = _interopRequireDefault(require("./add.js"));\n' +
'\n' +
'var _minus = require("./minus.js");\n' +
'\n' +
'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +
'\n' +
'var sum = (0, _add["default"])(1, 2);\n' +
'var division = (0, _minus.minus)(2, 1);\n' +
'console.log(sum);\n' +
'console.log(division);
======================================第二步开始:======================================
(function (graph) {
function require(file) {
function absoluteRequire(realPath) {
return require(graph[file].deps[realPath]);
}
(function (require, code) {
eval(code);
})(absoluteRequire, graph[file].code);
return exports;
}
require(file);
})(depsGraph)
执行代码时候require的参数,是相对路径,需要转换成绝对路径
1. 执行eval,也就是执行index.js代码
2. 执行过程过遇到require函数
3. 这时候就会调用传入进来的require(也就是absoluteRequire函数,这个会返回一个绝对路径)
======================================第二步结束:======================================
======================================第三步,最终代码开始:======================================
******add.js******
'"use strict";\n' +
'\n' +
'Object.defineProperty(exports, "__esModule", {\n' +
' value: true\n' +
'});\n' +
'exports["default"] = void 0;\n' +
'\n' +
'var _default = function _default(a, b) {\n' +
' return a + b;\n' +
'};\n' +
'\n' +
'exports["default"] = _default;'
第三步,最终代码:
(function (graph) {
function require(file) {
function absoluteRequire(realPath) {
return require(graph[file].deps[realPath]);
}
const exports = {};
(function (require, exports, code) {
eval(code);
})(absoluteRequire, exports, graph[file].code);
return exports;
}
require('${file}');
})(${depsGraph})
1. 从上面的截图可以看出exports其实就是一个对象,但是我们没有定义,因此需要定义个新的对象exports
2. 在执行代码的时候会往这个对象上挂载内容
3. 执行完add.js
exports = {
__esModule:{ value: true},
default:function _default(a, b) { return a + b;}
}
4. index.js文件中 var _add = _interopRequireDefault(require("./add.js"))
5. return出去的值,被_interopRequireDefault接收,_interopRequireDefault再返回default这个属性给_add,因此_add = function _default(a, b) { return a + b;}
======================================第三步,最终代码结束:======================================
生成文件,将打包的代码写入
...代码
const content = bundle("./src/index.js");
fs.mkdirSync("./dist");
fs.writeFileSync("./dist/bundle.js", content);
运行
node bundle.js
输出结果: 修改index.html引入路径
<script src="./src/index.js"></script> 替换成 <script src="./dist/bundle.js"></script>
访问index.html文件
所有代码
const fs = require("fs");
const path = require("path");
const traverse = require("@babel/traverse").default;
const parser = require("@babel/parser");
const babel = require("@babel/core");
const getModuleInfo = (file) => {
const body = fs.readFileSync(file, "utf-8");
const ast = parser.parse(body, {
sourceType: "module",
});
const deps = {};
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(file);
const importPath = node.source.value;
const abspath = "./" + path.join(dirname, importPath);
deps[importPath] = abspath;
},
});
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"],
});
const moduleInfo = { file, deps, code };
return moduleInfo;
};
const parseModules = (file) => {
const entry = getModuleInfo(file);
const allModuleInfo = [entry];
const depsGraph = {};
allModuleInfo.forEach((module) => {
const deps = module.deps;
if (deps) {
for (const key in deps) {
allModuleInfo.push(getModuleInfo(deps[key]));
}
}
});
allModuleInfo.forEach((moduleInfo) => {
depsGraph[moduleInfo.file] = {
deps: moduleInfo.deps,
code: moduleInfo.code,
};
});
console.log(depsGraph);
return depsGraph;
};
const bundle = (file) => {
const depsGraph = JSON.stringify(parseModules(file));
return `(function (graph) {
function require(file) {
function absoluteRequire(realPath) {
/* 例如传进来./src/index.js,
deps: { './add.js': './src\\add.js', './minus.js': './src\\minus.js' }
这个文件中有require('./add.js') ==> graph[file].deps[realPath] = ./src/add.js
*/
return require(graph[file].deps[realPath]);
}
const exports = {};
// require加载指定路径对应文件的代码, eval('xxxxx')
(function (require, exports, code) {
eval(code); // code中有require函数的调用,此时调用的就是传入进来的absoluteRequire
})(absoluteRequire, exports, graph[file].code);
return exports;
}
require('${file}');
})(${depsGraph})`;
};
const content = bundle("./src/index.js");
fs.mkdirSync("./dist");
fs.writeFileSync("./dist/bundle.js", content);
|