前言
- 文章结构采用【指出阶段目标,然后以需解决问题为入口,以解决思路为手段,小测试加深印象】达到本文目标,若使诸君稍有启发,不枉此文心力^-^
目标
- 了解webpack编译之后的dist中的main.js(一般是)的运行原理;
- 了解webpack的模块化是如何实现的,已经如何兼容多个模块化标准的; 如何打包在之后系列文章中会补上
关键点
模块化无非导入导出规则的制定,webpack是基于commonJs规范的,所以问题转为
- commonJS的导入导出实现
- es6等导入导出的兼容(hammary),即转为commonJS
commonJS的导入导出实现
先看使用
## title.js
module.export = {
a: 'title'
}
export.b = 'b'
## index.js
let title = require('title');
console.log(title);
核心问题
- 如何实现模块作用域上挂载module对象、epxorts对象、require函数
- 根据commonJS规范实现module对象、epxorts对象、require函数
- 实现require方法的缓存效果,即已加载过一次的模块再次加载只需要取之前的计算值
解决思路
-
实现模块函数的赋值
-
作用域自然想到函数, 那我们就可以用一个函数去包裹用户定义逻辑(根据require传进来的路径去获取文件内容,并用eval包裹作为函数体得到一个函数),
-
然后在模块安装时调用次模块对应函数,传递三个参数:
module , module.exports,__webpack_require__(也就是我们的require)
就实现了挂载
-
-
实现commonJS规范
-
规定module对象是一个Object类型,有三个属性
- i : 模块id
- l :模块是否已加载完成
- exports :模块导出对象
注意:传给webpack的模块(注意区分,上面讲的模块对象是指webpack内模块对象)在webpack不同版本有不同描述,在3.xx中是对象,其模块id是key,val是函数,4.xx中直接是函数,模块id是数组索引;本文采用对象版本进行描述,更好理解,思路是一致的;
-
epxorts其实是module.exports的一个引用,传值时直接传module.exports即可
-
require函数是核心的模块安装函数,核心是创建模块对象进行模块安装,还有缓存避免循环依赖问题等等,逻辑较为复杂下文补充;
-
-
实现模块缓存
- 我们为模块取一个模块id即可,当模块id在缓存对象(
var installedModules = {};
)中已存在,则直接取出;未存在则进行安装;- webpack内中定义的模块对象是一个Object类型,有三个属性
- i : 模块id
- l :模块是否已加载完成
- exports :模块导出对象
- webpack内中定义的模块对象是一个Object类型,有三个属性
- 我们为模块取一个模块id即可,当模块id在缓存对象(
注意,exports是module.exports的引用,也就意味着当引用关系改变,exports上赋值可能出现“赋值不上”的情况,此处故意含糊了点,因为后面我会提问加深印象,关键词:引用传递
实现逻辑:
首先,进入入口文件,其自然会存在require函数的执行,这个require我们前置中提到的_webpack_require,也是我们的核心方法,核心是生成对应模块对象,
然后,将module , module.exports,__webpack__require__传递给包裹着用户自定义逻辑的函数中,这也是我们“全局”有module、exports、require的原因;同时,逻辑中会为导出值赋值;
最后,__webpack__require__函数返回module.exports,这样,导入方也就拿到了依赖模块的导出值。
show the code(简化版打包结果)
(function(modules) { // webpackBootstrap webpack 启动脚本
// The module cache 模块缓存
var installedModules = {};
// The require function 就是我们用的require
function __webpack_require__(moduleId) {
// 1. 是否已经加载过 已加载就将加载过的返回
// Check if module is in cache
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
}
// 2. 未加载
var module = installedModules[moduleId] = {
i: moduleId, // 模块id
l: false, // 是否 已加载完成
exports: {} // 导出对象
};
// 执行传入对象中 moduleId对应的函数
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
module.l = true;
return module.exports;
}
return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
// 传参
({
"./src/index.js":
(function(module, exports, __webpack_require__) {
let title = __webpack_require__("./src/title.js");
console.log(title);
}),
"./src/title.js":
(function(module, exports) {
module.exports = {
a: 'title'
};
exports.b = 'b';
})
});
小测试
## index.js
let title = require('./title');
let {b} = title;
console.log(title,b);
## title.js
module.exports = {
a: '1'
}
exports.b = 2
控制台会打印什么?
hammary:es6
先看使用
## index.js
import title,{esB} from './title';
console.log(title,esB);
## title.js
export default {
esA: 1
}
export const esB = 2;
核心问题
- 模块来源的记录,便于引入方去区分加载方式
- 的导入导出转为commonJS的导入导出
解决思路
关键点:对于es和commonJs而言,最大的区别在于,es中的define语法导出值与直接export导出值并无关联;而在commonJs中其实只会导出一个export对象,其exports和module.exports是共享同一个对象(即地址值)的,所以在兼容时我们可以做如下处理
- 通过判断模块中是否有
import export define
等关键字判断当前是否为es6模块,对es6模块先记录(模块对象上加__esModule属性为true) - 导出时,模块对象的exports属性承载
export
语法的导出值,再在模块对象上添加default属性指向export default的导出值; - 导入时,对__esModule属性进行判断,如果是es6模块,则默认值取模块对象exports的default指向值;如果是commonJs,则直接是模块对象的exports本身
- 至此,我们就实现了模块化对es6的hammary(融合)
实现逻辑
前提
- 定义记录es模块函数,功能,传参导出值,将导出值标为es模块
/**
* 1. 改变exports在调用Object.prototype.toString.call时的值 表示这是一个模块对象
* 2. 增加__esModule属性 且值为true 用于表示此模块对象是es模块
* @param {*} exports
*/
__webpack_require__.r = function(exports) {
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
// 此语法是设置toString函数的返回值的
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
- 定义取值函数n,功能,根据模块导出值类型去区分取值,commonJS则返回export本身,es模块则返回export的default。
- 其中定义属性可以封装一个方法d,先判断有无此属性(函数o),无则添加;
// 判断对象上是否存在属性
__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
// definePropetry定义属性
__webpack_require__.d = function(exports, name, getter) {
if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter });
}
};
__webpack_require__.n = function(module) {
var getter = module && module.__esModule ?
function getDefault() { return module['default']; } :
function getModuleExports() { return module; };
__webpack_require__.d(getter, 'a', getter);
return getter;
};
实现
当模块含有import export define
时,在模块函数中
导出模块
当发现export关键字时调用
- 定义方法(webpack_require.r),两个作用
- 改变exports在调用Object.prototype.toString.call时的值 表示这是一个模块对象
- 增加__esModule属性 且值为true 用于表示此模块对象是es模块
- 定义方法(webpack_require.d),作用
- 将
export const b = 1;
类似语法转为 在模块对象的exports对象上定义属性名为b
,值为 1; - 将
export default
导出值挂载在 模块对象的exports对象的default属性上
- 将
导入模块
当发现import关键字时
- 调用__webpack_require__.r标明模块来源是es6
import xxx from
解析为从exports对象的default属性上取值
show the code(简化版打包结果,函数体相同,只是做了传入值的兼容转化,所以只加这块代码)
(function(modules){...})({
"./src/index.js":
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
// 将此模块标为es模块
__webpack_require__.r(__webpack_exports__);
var _title__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./title */ "./src/title.js");
// 从default属性上取值
console.log(_title__WEBPACK_IMPORTED_MODULE_0__["default"],_title__WEBPACK_IMPORTED_MODULE_0__["esB"]);
}),
"./src/title.js":
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
// 将此模块标为es模块
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, "esB", function() { return esB; });
// 将`export default`导出值定义在导出对象的default属性上
__webpack_exports__["default"] = ({
esA: 1
});
const esB = 2;
})})
小测试
// foo.js
const bar = require('./bar.js');
console.log('value of bar:', bar);
module.exports = 'This is foo.js';
// bar.js
const foo = require('./foo.js');
console.log('value of foo:', foo);
module.exports = 'This is bar.js';
// index.js
require('./foo.js');
思考:会打印出什么
答案:
value of foo: undefined
value of bar: This is bar.js