发布时间:2023-03-02 09:30
学习笔记
参考文章:https://juejin.cn/post/6854573217336541192
项目地址:https://gitee.com/cjperfect/webpack–core-theorem
直接打开html文件,发现报错:Uncaught SyntaxError: Cannot use import statement outside a module,因为浏览器无法识别importES6语法,除非
,添加一个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\' //表示我们要解析的是ES模块
});
console.log(ast.program.body); // 它的内容在属性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\");
// 我们需要 遍历AST,将用到的依赖收集起来。什么意思呢?其实就是将用import语句引入的文件路径收集起来。我们将收集起来的路径放到deps里。
const traverse = require(\"@babel/traverse\").default;
// 分析模块的主要任务是 将获取到的模块内容 解析成AST语法树,这个需要用到一个依赖包@babel/parser
const parser = require(\"@babel/parser\");
const getModuleInfo = (file) => {
const body = fs.readFileSync(file, \"utf-8\");
const ast = parser.parse(body, {
sourceType: \"module\", //表示我们要解析的是ES模块
});
console.log(ast.program.body)
const deps = {};
/*
ImportDeclaration方法代表的是对type类型为ImportDeclaration的节点的处理。
console.log(ast.program.body); // 打印的结果中存在type: \'ImportDeclaration\',
这个函数就是这个类型的节点做处理操作
*/
traverse(ast, {
// 对语法树中特定的节点进行操作 参考@babel/types (特定节点类型)
// ImportDeclaration特定节点
ImportDeclaration({ node }) {
const dirname = path.dirname(file);
// console.log(ast.program.body); 打印结果中type为ImportDeclaration的value,
// 也就是入口文件import的路径\"./add\", \"./minus\"
const importPath = node.source.value;
const abspath = \"./\" + path.join(dirname, importPath); // 拼接
deps[importPath] = abspath; // 收集入口文件中, 所有import的文件对应的地址
},
});
// console.log(deps); // { \'./add\': \'./src\\\\add\', \'./minus\': \'./src\\\\minus\' }
};
@babel/traverse 可以用来
遍历更新
@babel/parser生成的AST
官方网址:https://www.babeljs.cn/docs/babel-traverse
需要把获得的ES6的AST转化成ES5
所需要的依赖
yarn add @babel/core @babel/preset-env
更新代码
const fs = require(\"fs\");
const path = require(\"path\");
// 我们需要 遍历AST,将用到的依赖收集起来。什么意思呢?其实就是将用import语句引入的文件路径收集起来。我们将收集起来的路径放到deps里。
const traverse = require(\"@babel/traverse\").default;
// 分析模块的主要任务是 将获取到的模块内容 解析成AST语法树,这个需要用到一个依赖包@babel/parser
const parser = require(\"@babel/parser\");
// 把获得的ES6的AST转化成ES5
const babel = require(\"@babel/core\");
const getModuleInfo = (file) => {
const body = fs.readFileSync(file, \"utf-8\");
const ast = parser.parse(body, {
sourceType: \"module\", //表示我们要解析的是ES模块
});
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; // 收集入口文件中, 所有import的文件对应的地址
},
});
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\");
// 我们需要 遍历AST,将用到的依赖收集起来。什么意思呢?其实就是将用import语句引入的文件路径收集起来。我们将收集起来的路径放到deps里。
const traverse = require(\"@babel/traverse\").default;
// 分析模块的主要任务是 将获取到的模块内容 解析成AST语法树,这个需要用到一个依赖包@babel/parser
const parser = require(\"@babel/parser\");
// 把获得的ES6的AST转化成ES5
const babel = require(\"@babel/core\");
const getModuleInfo = (file) => {
const body = fs.readFileSync(file, \"utf-8\");
const ast = parser.parse(body, {
sourceType: \"module\", //表示我们要解析的是ES模块
});
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; // 收集入口文件中, 所有import的文件对应的地址
},
});
const { code } = babel.transformFromAst(ast, null, {
presets: [\"@babel/preset-env\"],
});
// 该模块的路径(file),该模块的依赖(deps),该模块转化成es5的代码
const moduleInfo = { file, deps, code };
return moduleInfo;
};
/**
* 递归获取所有依赖
* @param {*} file
*/
const parseModules = (file) => {
const entry = getModuleInfo(file);
const allModuleInfo = [entry]; // 所有模块信息
const depsGraph = {};
allModuleInfo.forEach((module) => {
// { \'./add\': \'./src\\\\add\', \'./minus\': \'./src\\\\minus\' },
const deps = module.deps;
if (deps) {
for (const key in deps) {
allModuleInfo.push(getModuleInfo(deps[key]));
}
}
});
// console.log(allModuleInfo); // 所有文件路径,所需要的依赖,对应的文件代码
allModuleInfo.forEach((moduleInfo) => {
depsGraph[moduleInfo.file] = {
deps: moduleInfo.deps,
code: moduleInfo.code,
};
});
// 使用对象存储,是为了后面文件中require的参数是一个地址,我们就可以通过这个地址从对象找出对应文件信息
// 该模块的依赖(deps),该模块转化成es5的代码
return depsGraph;
// console.log(depsGraph);
};
我们现在是不能执行index.js这段代码的,因为浏览器不会识别执行require和exports。
不能识别的原因就是没有定义这require函数
,和exports对象
。那我们可以自己定义。
更新代码
...代码
/*
* 生成一个bundle.js文件,也就是打包后的一个文件。其实思路很简单,就是把index.js的内容和它的依赖模块整合起来。然后把代码写到一个新建的js文件。
* 处理两个关键字,require和export, 浏览器无法识别
*/
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) {
/* 例如传进来./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]);
}
(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) {
/* 例如传进来./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})
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
<script src=\"./src/index.js\">script> 替换成 <script src=\"./dist/bundle.js\">script>
const fs = require(\"fs\");
const path = require(\"path\");
// 我们需要 遍历AST,将用到的依赖收集起来。什么意思呢?其实就是将用import语句引入的文件路径收集起来。我们将收集起来的路径放到deps里。
const traverse = require(\"@babel/traverse\").default;
// 分析模块的主要任务是 将获取到的模块内容 解析成AST语法树,这个需要用到一个依赖包@babel/parser
const parser = require(\"@babel/parser\");
// 把获得的ES6的AST转化成ES5
const babel = require(\"@babel/core\");
const getModuleInfo = (file) => {
const body = fs.readFileSync(file, \"utf-8\");
const ast = parser.parse(body, {
sourceType: \"module\", //表示我们要解析的是ES模块
});
const deps = {};
/*
ImportDeclaration方法代表的是对type类型为ImportDeclaration的节点的处理。
console.log(ast.program.body); // 打印的结果中存在type: \'ImportDeclaration\',这个函数就是这个类型的节点做处理操作
*/
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(file);
// console.log(ast.program.body); type为ImportDeclaration的value ,也就是入口文件import的路径\"./add\", \"./minus\"
const importPath = node.source.value;
const abspath = \"./\" + path.join(dirname, importPath); // 拼接
deps[importPath] = abspath; // 收集入口文件中, 所有import的文件对应的地址
},
});
// console.log(deps); // { \'./add\': \'./src\\\\add\', \'./minus\': \'./src\\\\minus\' }
const { code } = babel.transformFromAst(ast, null, {
presets: [\"@babel/preset-env\"],
});
// 该模块的路径(file),该模块的依赖(deps),该模块转化成es5的代码
const moduleInfo = { file, deps, code };
return moduleInfo;
};
/**
* 递归获取所有依赖
* @param {*} file
*/
const parseModules = (file) => {
const entry = getModuleInfo(file);
const allModuleInfo = [entry]; // 所有模块信息
const depsGraph = {};
allModuleInfo.forEach((module) => {
// { \'./add\': \'./src\\\\add\', \'./minus\': \'./src\\\\minus\' },
const deps = module.deps;
if (deps) {
for (const key in deps) {
allModuleInfo.push(getModuleInfo(deps[key]));
}
}
});
// console.log(allModuleInfo); // 所有文件路径,所需要的依赖,对应的文件代码
/* [
{
file: \"./src\\\\add.js\",
deps: {},
code: \"\",
},
]; */
allModuleInfo.forEach((moduleInfo) => {
depsGraph[moduleInfo.file] = {
deps: moduleInfo.deps,
code: moduleInfo.code,
};
});
// 使用对象存储,是为了后面文件中require的参数是一个地址,我们就可以通过这个地址从对象找出对应文件信息
// 该模块的依赖(deps),该模块转化成es5的代码
console.log(depsGraph);
return depsGraph;
// console.log(depsGraph);
};
/*
* 生成一个bundle.js文件,也就是打包后的一个文件。其实思路很简单,就是把index.js的内容和它的依赖模块整合起来。然后把代码写到一个新建的js文件。
* 处理两个关键字,require和export, 浏览器无法识别
*/
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})`;
};
// getModuleInfo(\"./src/index.js\");
// parseModules(\"./src/index.js\");
const content = bundle(\"./src/index.js\");
/* 创建文件,写入打包后的内容 */
fs.mkdirSync(\"./dist\");
fs.writeFileSync(\"./dist/bundle.js\", content);