学习笔记
参考文章: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);