您的当前位置:首页正文

webpack4.x CommonJS模块化浅析

2020-11-27 来源:爱go旅游网

webpack4.0+之后,针对第一个报错,需要指定环境 --mode development;第二个报错,是因为我们没有使用配置文件的方式打包,而是直接使用的命令指定的打包输出位置,所以需要声明输出文件,综上,正确的命令如下:

webpack app/main.js --output public/bundle.js --mode development

执行结果:

➜  webpack-test webpack app/main.js --output public/bundle.js --mode development
Hash: a4e2f9ecc51b64891624
Version: webpack 4.25.1
Time: 90ms
Built at: 2018-11-08 17:11:01
    Asset      Size  Chunks             Chunk Names
bundle.js  5.16 KiB    main  [emitted]  main
Entrypoint main = bundle.js
[./app/bye.js] 165 bytes {main} [built]
[./app/hello.js] 173 bytes {main} [built]
[./app/main.js] 144 bytes {main} [built]
[./app/to.js] 30 bytes {main} [built]
➜  webpack-test

浏览器打开 index.html 文件,即可看到结果

Say Hello to 小明
Say Bye to 小明

但是 webpack 作为一个能简化我们开发难度和使用便捷的工具,显然像上面那样通过敲很多命令来打包,并不方便,所以下面采用配置文件的方式再来一次:

根目录创建 webpack.config.js 文件,并配置下打包入口和出口:

// webpack-test/webpack.config.js
module.exports = {
 mode: "development",//webpack.0之后需要声明环境
 entry: __dirname + "/app/main.js",//唯一入口文件
 output: {
 path: __dirname + "/public",//打包后的文件存放目录
 filename: "bundle.js"//打包后
输出文件名 } }

再次打包的时候,只需要使用命令 webpack 就可以了,webpack 默认读取当前路径下的 webpack.config.js 文件。

最终打包好的 bundle.js 文件,去除了多余注释,调整了代码格式,内容如下:

// 自执行函数,参数为所有模块组成的,形势为key:value,key是模块名
(function(modules) { // webpackBootstrap
 // 已加载模块的缓存,记录模块的加载情况,也是为了避免重复打包,节省资源
 var installedModules = {};

 // webpack 使用 require 方式加载模块的方法(模拟ConmmonJS reqiure()),作用为根据传进来的模块id来处理对应的模块,加入已加载缓存,执行,标记,返回exports
 function __webpack_require__(moduleId) {
 // moduleId 为模块路径
 // 检测模块是否已加载,已加载的话直接返回该模块
 if(installedModules[moduleId]) {
 return installedModules[moduleId].exports;
 }
 // 当前模块未加载的话,新建,并存于缓存
 var module = installedModules[moduleId] = {
 i: moduleId,
 l: false,
 exports: {}
 };

 // 在当前模块的 exports 下,也就是模块的内部执行模块代码,突出作用域
 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

 // 标记模块已经加载
 module.l = true;

 // 返回模块的导出内容
 return module.exports;
 }


 // 挂载属性,该模块 (__webpack_modules__)
 __webpack_require__.m = modules;

 // 挂载属性,模块加载缓存
 __webpack_require__.c = installedModules;

 // 本代码中未执行,暂时不分析
 // 在 exports 中定义 getter 方法
 __webpack_require__.d = function(exports, name, getter) {
 // 当 name 属性不是定义在对象本身,而是继承自原型链,则在在 exports 中定义 getter 方法
 if(!__webpack_require__.o(exports, name)) {
 Object.defineProperty(exports, name, { enumerable: true, get: getter });
 }
 };

 // 本代码中未执行,暂时不分析
 // 在 exports 中定义 __esModule,定义key为Symbol的属性(在__webpack_require__.t中被调用)
 // define __esModule on exports
 __webpack_require__.r = function(exports) {
 if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
 Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
 }
 Object.defineProperty(exports, '__esModule', { value: true });
 };

 // 本代码中未执行,暂时不分析
 // 创建一个伪命名空间的对象
 // create a fake namespace object
 // mode & 1: value is a module id, require it
 // mode & 2: merge all properties of value into the ns
 // mode & 4: return value when already ns object
 // mode & 8|1: behave like require
 __webpack_require__.t = function(value, mode) {
 if(mode & 1) value = __webpack_require__(value);
 if(mode & 8) return value;
 if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
 var ns = Object.create(null);
 __webpack_require__.r(ns);
 Object.defineProperty(ns, 'default', { enumerable: true, value: value });
 if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
 return ns;
 };

 // 本代码中未执行,暂时不分析
 // getDefaultExport function for compatibility with non-harmony modules
 __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;
 };

 // Object.prototype.hasOwnProperty.call
 // 判断一个属性是定义在对象本身而不是继承自原型链
 __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };

 // __webpack_public_path__
 __webpack_require__.p = "";


 // 加载入口模块 main.js ,返回 exports,从而从入口文件开始执行,以递归的方式,将所有依赖执行并返回
 return __webpack_require__(__webpack_require__.s = "./app/main.js");
})({

 "./app/bye.js": (function(module, exports, __webpack_require__) {

 eval("const to = __webpack_require__(/*! ./to.js */ \"./app/to.js\");\nmodule.exports = function() {\n var bye = document.createElement('div');\n bye.textContent = \"Say Bye to \" + to.name;\n return bye;\n};\n\n//# sourceURL=webpack:///./app/bye.js?");

 }),

 "./app/hello.js": (function(module, exports) {

 eval("module.exports = function() {\n var hello = document.createElement('div');\n hello.textContent = \"Say Hello!\";\n return hello;\n};\n\n//# sourceURL=webpack:///./app/hello.js?");

 }),

 "./app/main.js": (function(module, exports, __webpack_require__) {

 eval("const hello = __webpack_require__(/*! ./hello.js */ \"./app/hello.js\");\nconst bye = __webpack_require__(/*! ./bye.js */ \"./app/bye.js\");\n\ndocument.querySelector(\"#root\").appendChild(hello()).appendChild(bye());;\n\n//# sourceURL=webpack:///./app/main.js?");

 }),

 "./app/to.js": (function(module, exports) {

 eval("module.exports = {name: \"小明\"};\n\n//# sourceURL=webpack:///./app/to.js?");

 })

});

分析

webpack 的运行过程可分为:读取配置参数,实例化插件,模块解析处理(loader),输出打包文件;在上面例子中,仅为 JavaScript 的引用,没有使用插件和像CSS、less、图片之类需要loader处理的模块,所以上面的例子,过程只有读取配置,识别入口及其引用模块,打包几步,生成最终的 bundle.js 文件。

简单描述下 webpack 在这个过程中的执行流程:在配置文件中读取入口,如果有配置 plugins 参数,那么也是在此时进行插件的实例化和钩子函数的绑定;模块解析,也就是loader加入的时刻,从入口文件开始,根据入口文件对其他模块的依赖,结合配置文件中对不同种类型文件所使用的 loader(加载器) 说明,一个一个逐级对这些模块进行解析处理,或压缩,或转义,生成浏览器可以直接识别的内容;最后将所有模块进行打包,输出打包后的文件。在上面的代码中,已经对 bundle.js 内容进行了内容注释,下面我们来分析下 bundle.js 的执行过程:

1、自执行函数

最后的输出的文件 bundle.js 是一个 JavaScript 文件,其本身其实是一个自执行函数

(function(参数){})(参数)。

2、参数

自执行方法的参数为所有模块组成的对象,key 为各个模块的路径,值为各模块内部的执行代码,观察参数内部的代码,对比打包前的源码,可以发现凡是 require 都变成了__webpack_require__这个webpack自定义的模块调用方法,而且源码中的相对路径也变成了最终执行位置的文件的相对路径。

{

 "./app/bye.js": (function(module, exports, __webpack_require__) {

 eval("const to = __webpack_require__(/*! ./to.js */ \"./app/to.js\");\nmodule.exports = function() {\n var bye = document.createElement('div');\n bye.textContent = \"Say Bye to \" + to.name;\n return bye;\n};\n\n//# sourceURL=webpack:///./app/bye.js?");

 }),

 "./app/hello.js": (function(module, exports) {

 eval("module.exports = function() {\n var hello = document.createElement('div');\n hello.textContent = \"Say Hello!\";\n return hello;\n};\n\n//# sourceURL=webpack:///./app/hello.js?");

 }),

 "./app/main.js": (function(module, exports, __webpack_require__) {

 eval("const hello = __webpack_require__(/*! ./hello.js */ \"./app/hello.js\");\nconst bye = __webpack_require__(/*! ./bye.js */ \"./app/bye.js\");\n\ndocument.querySelector(\"#root\").appendChild(hello()).appendChild(bye());;\n\n//# sourceURL=webpack:///./app/main.js?");

 }),

 "./app/to.js": (function(module, exports) {

 eval("module.exports = {name: \"小明\"};\n\n//# sourceURL=webpack:///./app/to.js?");

 })

}

3、执行

(1)自执行文件开始执行后,到自执行函数最底部,首先从入口文件开始加载

return __webpack_require__(__webpack_require__.s = "./app/main.js");

(2)__webpack_require__函数被调用,传入参数 ./app/main.js,

function __webpack_require__(moduleId) {
 // moduleId 为 ./app/main.js
 // 首次进来,未加载,模块还没有缓存
 if(installedModules[moduleId]) {
 return installedModules[moduleId].exports;
 }
 // 新建 ./app/main.js 模块,并存于缓存
 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; }

此时方法中执行 modules[moduleId].call(module.exports, module, module.exports,__webpack_require__); 相当于在名为 ./app/main.js 的模块中执行如下代码:

(function(module, exports, __webpack_require__) {

 eval("const hello = __webpack_require__(/*! ./hello.js */ \"./app/hello.js\");\nconst bye = __webpack_require__(/*! ./bye.js */ \"./app/bye.js\");\n\ndocument.querySelector(\"#root\").appendChild(hello()).appendChild(bye());;\n\n//# sourceURL=webpack:///./app/main.js?");

})()

由于引用关系,接下来会再次执行两次__webpack_require__方法,分别传参模块路径 ./app/hello.js 和 ./app/bye.js;

(3)执行第一个__webpack_require__过程,除了传参不同、执行的模块不同,与第二步基本一致,再次找到了依赖模块 to.js,再次调用__webpack_require__。

"./app/hello.js": (function(module, exports, __webpack_require__) {

 eval("const to = __webpack_require__(/*! ./to.js */ \"./app/to.js\");\nmodule.exports = function() {\n var hello = document.createElement('div');\n hello.textContent = \"Say Hello to \" + to.name;\n return hello;\n};\n\n//# sourceURL=webpack:///./app/hello.js?");

}),

(4)执行第二个__webpack_require__时,在 bye.js 中找到了对于 to.js 的依赖,所以将继续调用__webpack_require__方法,只是传参变成了./app/to.js,达到终点。

"./app/bye.js": (function(module, exports, __webpack_require__) {

 eval("const to = __webpack_require__(/*! ./to.js */ \"./app/to.js\");\nmodule.exports = function() {\n var bye = document.createElement('div');\n bye.textContent = \"Say Bye to \" + to.name;\n return bye;\n};\n\n//# sourceURL=webpack:///./app/bye.js?");

})

(5)到此时,整个从入口文件的开始的针对所依赖模块的解析已经完成,所有的 js 代码也已经引用完毕且放到了 bundle.js 中。

总结

到这里可以看到,webpack对js的打包,就是封装为一个个单独的方法,通过对这些方法的引用,达到模块化的效果;而打包的过程,就是查找、解析、封装这些方法的过程,整个执行路径类似于一棵树,从主干出发,沿着树枝递归式的执行“require”方法,而且是直到这一根树枝走到尽头的时候才回头寻找其他的方法,由于node的单线程,当项目庞大或者模块间依赖错综复杂时,webpack打包会更加的耗费时间。

以上为对webpack4.x中针对js模块处理的简单理解,主要基于官方文档的介绍和打包后文件的分析,源码读起来还是比较难懂,暂时不敢照量。对于 ES6、AMD 的模块化方式,代码分割的等,后续再进行补充。希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

显示全文