mini-webpack 源码浅析_cory chase minipack web-

文章讲述了使用Babel解析JavaScript源代码,生成抽象语法树,分析依赖并创建依赖关系图,最终实现模块化的打包过程,类似于CommonJS模块,但存在重复引入和模块兼容性限制。
摘要由CSDN通过智能技术生成

}
require(0);
})({${modules}})
`
return result
}

module.exports = {
bundle,
createGraph
}


#### 模块依赖生成


具体步骤


* 给定入口文件
* 根据入口文件分析依赖(借助`bable`获取)
* 广度遍历依赖图获取依赖
* 根据依赖图生成`(模块id)key:(数组)value`的对象表示
* 建立require机制实现模块加载运行


### 一个简单的实例


**原始代码**:



// 入口文件 entry.js
import message from ‘./message.js’

console.log(message);

// message.js
import {name} from ‘./name.js’

export default hello ${name}!

// name.js
export const name = ‘world’


读取文件内容,分析依赖,第一步需要解析源码,生成抽象语法树。


* **第一步,读取入口文件,生成 AST,递归生成依赖关系对象 graph**。


其中,`createAsset` 函数是解析js文本,生成每个文件对应的一个对象,其中 `code` 的代码是经过`babel-preset-env`转换后可在浏览器中执行的代码。



const { code } = transformFromAst(ast, null, {
presets: [‘env’]
})


createGraph 函数生成依赖关系对象。



[
{ id: 0,
filename: ‘./example/entry.js’,
dependencies: [ ‘./message.js’ ],
code: ‘“use strict”;\n\nvar _message = require(“./message.js”);\n\nvar _message2 = _interopRequireDefault(_message);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nconsole.log(_message2.default);’,
mapping: { ‘./message.js’: 1 } },

{ id: 1,
filename: ‘example/message.js’,
dependencies: [ ‘./name.js’ ],
code: ‘“use strict”;\n\nObject.defineProperty(exports, “__esModule”, {\n value: true\n});\n\nvar _name = require(“./name.js”);\n\nexports.default = "hello " + _name.name + “!”;’,
mapping: { ‘./name.js’: 2 } },

{ id: 2,
filename: ‘example/name.js’,
dependencies: [],
code: ‘“use strict”;\n\nObject.defineProperty(exports, “__esModule”, {\n value: true\n});\nvar name = exports.name = ‘world’;’,
mapping: {} }

]


有了依赖关系图,下一步就是将代码打包可以在浏览器中运行的包。


首先**我们将依赖图解析成如下字符串**(其实是对象没用`{}`包裹的格式):  
 关键代码是这句:



modules += ${mod.id}: [ function (require, module, exports) { ${mod.code} }, ${JSON.stringify(mod.mapping)}, ],;


生成出来的代码如下:



0: [
function (require, module, exports) {
// -------------- mod.code --------------
“use strict”;
var _message = require(“./message.js”);
var _message2 = _interopRequireDefault(_message);
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}

    console.log(_message2.default);
    // --------------------------------------
  },
  {"./message.js":1},
],
1: [
  function (require, module, exports) {
    // -------------- mod.code --------------
    "use strict";
    Object.defineProperty(exports, "\_\_esModule", {
      value: true
    });
    var _name = require("./name.js");
    exports.default = "hello " + _name.name + "!";
    // --------------------------------------
  },
  {"./name.js":2},
],

2: [
  function (require, module, exports) {
    // -------------- mod.code --------------
    "use strict";
    Object.defineProperty(exports, "\_\_esModule", {
      value: true
    });
    var name = exports.name = 'world';
    // --------------------------------------
  },
  {},
],

依赖的图生成的文件可以简化为:



modules = {
0: [function code , {deps} ],
1: [function code , {deps} ]
}


这里,我们比较下源码:



// 入口文件 entry.js
import message from ‘./message.js’;

console.log(message);

// —
“use strict”;
var _message = require(“./message.js”);
var _message2 = _interopRequireDefault(_message);
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}

console.log(_message2.default);

// message.js
import {name} from ‘./name.js’;

export default hello ${name}!;

// —
“use strict”;
Object.defineProperty(exports, “__esModule”, {
value: true
});
var _name = require(“./name.js”);
exports.default = "hello " + _name.name + “!”;

// name.js
export const name = ‘world’;

// —
“use strict”;
Object.defineProperty(exports, “__esModule”, {
value: true
});
var name = exports.name = ‘world’;


可以看出,`babel`在转换原始`code`的时候,引入了`require`函数来解决模块引用问题。但是其实浏览器仍然是不认识的。因此还需要额外定义一个require函数(其实这部分和`requirejs`原理类似的模块化解决方案,其中原理其实也很简单)


得到这个字符串后,再最后拼接起来即最终结果。


最后,我们还需要定义一个自执行函数文本,并将上述字符串传入其中,拼接结果如下:



(function (modules) {
function require(id) {
const [fn, mapping] = modules[id];

	function localRequire(name) {
		return require(mapping[name]);
	}

	const module = { exports: {} };

	fn(localRequire, module, module.exports);

	return module.exports;
}

require(0);

})({
0: [
function (require, module, exports) {
“use strict”;
var _message = require(“./message.js”);
var _message2 = _interopRequireDefault(_message);
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}

		console.log(_message2.default);
	},
	{ "./message.js": 1 },
],
1: [
	function (require, module, exports) {
		"use strict";
		Object.defineProperty(exports, "\_\_esModule", {
			value: true
		});
		var _name = require("./name.js");
		exports.default = "hello " + _name.name + "!";
	},
	{ "./name.js": 2 },
],

2: [
	function (require, module, exports) {
		"use strict";
		Object.defineProperty(exports, "\_\_esModule", {
			value: true
		});
		var name = exports.name = 'world';
	},
	{},
],

})


我们执行最后的结果,会输出`"hello world"`。


那我们仔细分析下打包后的这段代码:


首先这是一个自执行函数,传入的字符串外面包裹上`{}`后是一个对象,形如`<moduleId>: <value>`的格式。


自执行函数的主体部分定义了一个require函数:



function require(id) {
// 1. 解构module,获取fn和当前module的依赖路径
const [fn, mapping] = modules[id];

// 2. 定义引入依赖函数
function localRequire(name) {
return require(mapping[name]);
}

// 3. 定义module变量,保存的是依赖模块导出的对象,存储在module.exports
const module = { exports: {} };

// 4. 递归执行,直到子module中不再执行传入的require函数
fn(localRequire, module, module.exports);

return module.exports;

}


在 require 方法中,传入模块 id,根据模块 id 获取隔离作用域的执行函数 fn 以及依赖信息 mapping,预处理一下 require 函数,目的是将模块中 require 函数入参的相对地址转为 id,然后传入执行 fn,最后返回 `module.exports` 对象给上级调用。


过程如下, 接收一个模块id,


* 第一步:解构`module`,获取`fn`和当前`module`的依赖路径
* 第二步:定义引入依赖函数(相对引用),函数体同样是获取到依赖`module`的`id`,`localRequire` 函数传入到`fn`中
* 第三步:定义`module`变量,保存的是依赖模块导出的对象,存储在`module.exports`中,`module`和`module.exports`也传入到fn中
* 第四步:递归执行,直到子`module`中不再执行传入的`require`函数


简单来说,模块之间通过`require`和`exports`联系,至于模块内部的实现,只在模块内可见。


第三步,我们模块代码会被执行。并且执行的结果会存储在`module.exports`中并接受三个参数 `require`, `module`, `module.exports`



> 
> 类似COMMONJS module会在模块闭包内注入exports, require, module, \_\_filename, \_\_dirname
> 
> 
> 


会在入口处对其代码进行require执行一遍。


### 总结


这个方法显然是有一些瑕疵的:


* 比如所有的模块执行 require 时都会执行一遍 require 模块的代码,可能会导致重复引入。
* 比如只支持 es6 模块收集,无法兼容 CommonJS/CMD/AMD 模块; 模块引用路径必须写后缀名等问题。


不过通过对 require 方法的简单实现,已经足够让我们理解 webpack 这类打包工具的本质:就是通过函数来划分作用域,通过 module 以及 module.exports 来共享数据。


通过上述分析,我们可以了解


* minipack的基本构造
	+ 从主入口中将代码转换为AST,
	+ 然后后找出主入口的依赖关系,
	+ 通过这个依赖关系可以构建依赖图,
	+ 最后通过依赖图转化为类commonjs模块的代码,打包在一块。
* 打包工具的基本形态
* 模块的一些问题


由此,可以看出,其实原理并不是很复杂,但是却很巧妙,要了解“打包”的原理,也需要了解“模块化”的一些知识。前端发展虽快,但是深入到基础,会发现其实是一脉相通的。


本文转载自下面的两篇文章:


* https://juejin.im/post/5bc846f7e51d450e7125a1e9
* https://segmentfault.com/a/1190000018949887






**前端资料汇总**

**[开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】](https://bbs.csdn.net/topics/618166371)**

![](https://img-blog.csdnimg.cn/img_convert/6e0ba223f65e063db5b1b4b6aa26129a.png)



我一直觉得技术面试不是考试,考前背背题,发给你一张考卷,答完交卷等通知。

首先,技术面试是一个 认识自己 的过程,知道自己和外面世界的差距。



更重要的是,技术面试是一个双向了解的过程,要让对方发现你的闪光点,同时也要 试图去找到对方的闪光点,因为他以后可能就是你的同事或者领导,所以,面试官问你有什么问题的时候,不要说没有了,要去试图了解他的工作内容、了解这个团队的氛围。
找工作无非就是看三点:和什么人、做什么事、给多少钱,要给这三者在自己的心里划分一个比例。
最后,祝愿大家在这并不友好的环境下都能找到自己心仪的归宿。
  • 20
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值