手写webpack核心原理

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

打包主要流程
  1. 读取入口文件内容
  2. 根据入口文件,递归读取引入文件所依赖的文件内容,生成AST语法树
  3. 根据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语法树
疑问:

  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

输出结果:
在这里插入图片描述

收集入口文件依赖

将用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);
};

输出结果:
在这里插入图片描述

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

输出结果:
在这里插入图片描述

处理两个关键字(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

输出结果:
在这里插入图片描述
修改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);



  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值