手写webpack核心原理

发布时间:2023-03-02 09:30

学习笔记
参考文章:https://juejin.cn/post/6854573217336541192
项目地址:https://gitee.com/cjperfect/webpack–core-theorem

打包主要流程

  1. 读取入口文件内容
  2. 根据入口文件,递归读取引入文件所依赖的文件内容,生成AST语法树
  3. 根据AST语法树,生成浏览器能够运行的代码

项目目录和基础代码\"手写webpack核心原理_第1张图片\"

获取模板内容(入口文件内容)

直接打开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\");

输出结果:
\"手写webpack核心原理_第2张图片\"

分析模块

分析模板主要任务是将获取到的模板内容解析成AST语法树
疑问:

  1. 什么要将获取到的模块内容 通过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

输出结果:
\"手写webpack核心原理_第3张图片\"

收集入口文件依赖

将用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转成ES5

需要把获得的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);
};

输出结果:
\"手写webpack核心原理_第4张图片\"

@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);
};

输出结果:
\"手写webpack核心原理_第5张图片\"

处理两个关键字(require,exports)

我们现在是不能执行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}defaultfunction _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

输出结果:
\"手写webpack核心原理_第6张图片\"
修改index.html引入路径

<script src=\"./src/index.js\">script>  替换成 <script src=\"./dist/bundle.js\">script>

访问index.html文件
\"在这里插入图片描述\"

所有代码

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);



ItVuer - 免责声明 - 关于我们 - 联系我们

本网站信息来源于互联网,如有侵权请联系:561261067@qq.com

桂ICP备16001015号