- webpack是一个打包工具,它可以根据模块的依赖关系,结合指定的规则,生成对应的静态文件
webpack打包的文件
toStringTag
- 通常用来表示该对象的自定义类型标签
let obj = {};
Object.defineProperty(obj, Symbol.toStringTag, { value: "Module" });
console.log(Object.prototype.toString.call(obj)); // [Object Module]
打包后bundle.js基本结构
(function (modules) {
var installedModules = {}; // 模块的缓存
function __webpack_require__(moduleId) { // webpack自定义的__webpack_require__函数
if (installedModules[moduleId]) {
return installedModules[moduleId].exports; //检测模块是否在缓存中存在,如果存在则直接返回模块的的exports,否则创建新的模块
}
//创建一个新的模块并且放到模块的缓存中
var module = (installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {},
});
//执行模块函数
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__) {
var title = __webpack_require__("./src/title.js");
console.log(title);
},
"./src/title.js": function (module, exports) {
module.exports = "title";
},
});
- 打包的结果是一个自执行函数,参数是代码块所以来的模块,返回是入口模块的导出对象
- 定义一个自己实现的__webpack_require__函数
- 先加载入口模块
- 判断模块在不在缓存中,如果在,则直接返回缓存模块的module.exports,
- 如果不在,创建一个新的模块,并且让对应的函数执行
- 将模块设置为已经加载
- 返回模块的module.exports
__webpack_require__下的各种方法
// 在打包文件中的结构
function __webpack_require__() {}
__webpack_require__.o = function (object, property) {
...
};
__webpack_require__.d = function (exports, name, getter) {
....
};
...
webpack_require.o判断对象身上是否存在某属性
function __webpack_require__() {}
__webpack_require__.o = function (object, property) {
return Object.prototype.hasOwnProperty.call(object, property);
};
'注意':object.hasOwnProperty(property)会存在覆盖的情况,因此使用原型
...
webpack_require.d定义getter函数
function __webpack_require__() {}
__webpack_require__.d = function (exports, name, getter) {
if (!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter });
}
};
let obj = {};
__webpack_require__.d(obj, "name", function () {
return "xxx";
});
...
webpack_require.r为模块添加__esmodule: true
__webpack_require__.r = function() {
if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
}
Object.defineProperty(exports, "__esModule", { value: true }); //定义__esModule属性
}
webpack_require.n获取默认导出的函数
__webpack_require__.n = function (module) {
var getter =
module && module.__esModule // 判断是否有__esmodule属性
? function getDefault() {
return module["default"];
}
: function getModuleExports() {
return module;
};
__webpack_require__.d(getter, "a", getter); // 给getter上定义a 的getter函数。如果是esModule,返回module['default'],否则返回module
return getter;
};
测试:
1. let obj = {name: 'xxx'}
let result = __webpack_require__.n(obj)
console.log(result.a) // {name: 'xxx'}
2. let obj = {__esmodule: true, default: {name: 'xxx'}}
let result = __webpack_require__.n(obj)
console.log(result.a) // {name: 'xxx'}
- 对于es6模块的默认导出,会放在module.exports['default'],其他放在module.exports对象上
webpack_require.t创建一个命名对象,把模块转换为es6模块
/*
* mode & 1 value是模块ID直接用__webpack_require__加载
* mode & 2 把所有的属性合并到命名空间ns上
* mode & 4 已经是ns对象了,可以直接返回值
* mode & 8|1 行为类似于require
import('xxxx').then(r => r)之后一定是个es6模块,
进行互相组合
*/
__webpack_require__.t = function (value, mode) {
if (mode & 1) value = __webpack_require__(value); // 是一个模块ID,需要引入
if (mode & 8) return value;
if (mode & 4 && typeof value === "object" && value && value.__esModule) // 如果value是一个esmodule,直接返回
return value;
var ns = Object.create(null); //如果不是es6转换成es6模块,定义一个空对象
__webpack_require__.r(ns); 给ns定义__esmodule属性为true
Object.defineProperty(ns, "default", { enumerable: true, value: value }); // 让ns.default属性为value
if (mode & 2 && typeof value != "string") // 将value上的属性定义在ns上
for (var key in value)
__webpack_require__.d(
ns,
key,
function (key) {
return value[key];
}.bind(null, key)
);
return ns; // 返回ns
};
- 如果mode&1,说明value是一个模块ID,先倒入该模块
- 如果是4,说明value已经是个es6模块了,直接返回value
- 定义一个空对象作为命名对象
- 为ns定义__esmodules属性
- 设置ns.default,并将值设面得到的value
- 如果mode&2,将value上的属性,映射到ns上并且返回ns
模块打包处理
- 在 webpack 打包模块中,默认import和require是一样的,最终都是转化成__webpack_require__。
commonJS引commonJS
// title.js 加载commonJS
exports.name = " v";
exports.age = "18";
// index.js采取commonJS
let title = require("./title");
console.log(title.name);
console.log(title.age);
/*
打包的bundle.js,因为webpack打包之后,将模块都转为commonJS
将commonJS的require换成自己实现的__webpack_require__,按照commonJS的包装,其他不变
*/
{
"./src/index.js":
(function(module, exports, __webpack_require__) {
var title = __webpack_require__("./src/title.js");
console.log(title.name);
console.log(title.age);
}),
"./src/title.js":
(function(module, exports) {
exports.name = 'title_name';
exports.age = 'title_age';
})
}
- 打包的bundle.js,因为webpack打包之后,将模块都转为commonJS
- 将commonJS的require换成自己实现的__webpack_require__,按照commonJS的包装,其他不变
commonJS加载es6
// title.js,采取es6模块
export default 'xxx'
export const age = 18
// index.js commonJS
const title = require('./title.js')
console.log(title.name);
console.log(title.age);
//bundle.js
{
"./src/index.js":
(function(module, exports, __webpack_require__) {
var title = __webpack_require__("./src/title.js");
console.log(title["default"]);
console.log(title.age);
}),
"./src/title.js":
(function(module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);//__esModule=true
__webpack_require__.d(__webpack_exports__, "age", function() { return age; });
__webpack_exports__["default"] = 'title_name';
var age = 'title_age';
}
- 打包后的commonJS模块,将原来的require转为__webpack_require__
- 针对es6模块
- 通过__webpack_require__.r给模块添加__esmodule属性为true,标记为es6Module
- 批量导出,通过__webpack_require__.d定义对应的属性getter
- 对于默认导出,将变量放在__webpack_require['default']上
es6加载es6
// title.js
export const age = 18
export default 'xxx'
// index.js
import name, { age } from './title.js'
console.log(name) // 'xxx'
console.log(age) // 18
// bundle.js
{
"./src/index.js":
(function(module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);//__esModule=true // 标记为es6模块
var _title__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/title.js");
console.log(_title__WEBPACK_IMPORTED_MODULE_0__["default"]); // 默认导出从导出对象的‘default’属性取值
console.log(_title__WEBPACK_IMPORTED_MODULE_0__["age"]); // 批量导出直接从导出对象中取值
}),
"./src/title.js":
(function(module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);//__esModule=true // 标记为es6模块
__webpack_require__.d(__webpack_exports__, "age", function() { return age; }); // 对于export,批量导出采用__webpack_require__.d在导出对象上定义属性的getter函数
__webpack_exports__["default"] = 'title_name'; // 默认导出在__webpack_require__['default']上赋值
var age = 'title_age';
})
}
import
- 针对es6模块,遇到import,先使用__webpack_require__.r标记模块为es6模块
- 然后通过__webpack_require__引入相应的模块
- 从导出对象或者['default']属性取值
export
- 将模块标记为es6模块
- 批量导出采用__webpack_require__.d在导出对象上定义属性的getter函数
- 默认导出在__webpack_require__['default']上赋值
es6模块加载commonJS
// title.js
module.exports = {
name: xxx
age: 18,
};
// index.js
import name, { age } from "./title";
console.log(name); // {name: "xxx", age: 18} 相当于默认导出
console.log(age); //
// 打包bundle.js
{
"./src/index.js": function (module,__webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
var _title__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
"./src/title.js"
);
var _title__WEBPACK_IMPORTED_MODULE_0___default = __webpack_require__.n(_title__WEBPACK_IMPORTED_MODULE_0__);
console.log(_title__WEBPACK_IMPORTED_MODULE_0___default.a); //因为要取default,所以要用n包装一下
console.log(_title__WEBPACK_IMPORTED_MODULE_0__["age"]); //age是正常取属性就可以
},
"./src/title.js": function (module, exports) {
module.exports = {
name: "title_name",
age: "title_age",
};
},
"./src/title.js": function (module, exports) {
module.exports = {
name: "title_name",
age: "title_age",
};
},
}
- 针对es6模块
- 先标记为es6模块
- 通过__webpack_require__()拿到对应文件的导出对象
- 通过__webpack_require__.n加工导出对象,因为加载的模块不是esModule,所以指甲返回module,拿到的是{ name: "title_name", age: "title_age", },默认导出
- 如果要获取通过{age}引入的属性,就可以在导出对象上直接取值
- name就要通过n包装,得到引入的module.exports
- commonJS模块几乎不变
为什么es6加载commonJS需要经过n方法处理
- n最主要的原因是处理 esModule的default + common引入common,没有default的问题,esModule引入esModule,default在各自模块内部处理,使用的时候直接 moduleName.default就可以了。common引入esModule和es引入es一样。只有es引入common,commonJs中是没有default的,但是在es中使用import的变量时,我们会默认为它就是 default,所以才用 .n处理了一下
模块的异步加载
- webpack ensure有人称他为异步加载,也有人称为代码切割,他其实就是将js模块给独立导出一个.js文件,然后使用这个模块的时候,webpack会构造script dom元素,由浏览器异步请求这个js文件,然后写个回调函数,让请求到的js文件做一些业务操作
准备
var installedModules = {};
var installedChunks = { main: 0 }; // 用来存放加载过的和加载中的代码块
/*
* 0已经加载完成
* undefined,未加载
* promise 加载中
* null 预加载
*/
webpack_require.e
- 异步加载主要通过__webpack_require__.e实现
function jsonpScriptSrc(chunkId) {
return __webpack_require__.p + "" + chunkId + ".bundle.js";
}
__webpack_require__.e = function requireEnsure(chunkId) {
var promises = [];
var installedChunkData = installedChunks[chunkId];
if (installedChunkData !== 0) {
if (installedChunkData) {
promises.push(installedChunkData[2]);// 模块正在加载
} else { // 未加载
var promise = new Promise(function (resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject]; // installedChunkData是一个数组,第一个是成功的回调,第二个是的回调
});
promises.push((installedChunkData[2] = promise)); // 将新生成的promise放入promises队列中,并将新生成的promise当作installedChunkData数组的第三个参数
var script = document.createElement("script"); // 创建script标签,给script标签添加标签,并将script标签插入head中去请求其他文件
script.charset = "utf-8";
script.timeout = 120;
script.src = jsonpScriptSrc(chunkId);
document.head.appendChild(script);
}
}
return Promise.all(promises);
};
- 传入一个chunkId,在installedChunks中找到当前模块的状态
- 如果状态是0,说明模块已经加载成功,直接让promise成功
- 如果状态不是0
- 状态是个promise,则说明模块正在加载中
- 如果是undefined,则表示模块未加载,此时创建一个promise,放入promises队列中,并将生成的promise作为installedChunkData的第三项,此时installedChunkData存放的是[reslove, reject, promise]
- 创建script标签,去请求文件(Jsonp)
- 返回promise
总结: webpack_require.e做的事情就是,根据传入的chunkId,去加载这个chunkId对应的异步 chunk 文件,它返回一个promise。通过jsonp的方式使用script标签去加载。这个函数调用多次,还是只会发起一次请求 js 的请求。若已加载完成,这时候异步的模块文件已经被注入到立即执行函数的入参modules变量中了,这个时候和同步执行import调用__webpack_require__的效果就一样了(这个注入由webpackJsonpCallback函数完成)。此时,在promise的回调中再调用__webpack_require__.bind(null, "./src/c.js"") 就能拿到对应的模块,并且执行相关逻辑了。
JSONP返回的文件
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([
["title"],
{
"./src/title.js": function (module, exports) {
exports.name = "title_name";
exports.age = "title_age";
},
},
]);
对照上边的返回
(function (modules) {
...
function webpackJsonpCallback(data) {
var chunkIds = data[0]; // 拿到chunkids: title
var moreModules = data[1]; //' moreModules = {"./src/title.js": fn}'
var moduleId,
chunkId,
i = 0,
resolves = [];
for (; i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if (installedChunks[chunkId]) { // installedChunks = [reslove, reject, promise]
resolves.push(installedChunks[chunkId][0]); // 将对应的reslove放在数组中
}
installedChunks[chunkId] = 0; // 将模块状态设置为0
}
for (moduleId in moreModules) {
modules[moduleId] = moreModules[moduleId]; // 将额外的模块合并到之前的modules,之前只有一个main.js
}
while (resolves.length) {
resolves.shift()(); // 让所有的promise编程成功态,并且触发回调,这里代表__webpack_require__.e方法中的promise.all成功,就可以接着使用then
}
}
var jsonpArray = (window["webpackJsonp"] = window["webpackJsonp"] || []); // 创建一个数组
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback; // (window["webpackJsonp"] = window["webpackJsonp"] || []).push赋值一个函数
jsonpArray = jsonpArray.slice();
for (var i = 0; i < jsonpArray.length; i++)
webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;
return __webpack_require__((__webpack_require__.s = "./src/main.js"));
})
- 在__webpack_require__中重新定义定义(window["webpackJsonp"] = window["webpackJsonp"] || [])的push方法(webpackJsonpCallback)
- 让script请求文件回来,会执行webpackJsonpCallback,去加载额外的模块
- 将额外的模块合并到入口的modules,并且让所有的promise变成成功态,让代码块的状态变成0,至此使得__webpack_require_.e返回的promise.all成功,接着进行then时通过__webpack_require__就可以对对应的模块进行获取了