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 的模块化方式,代码分割的等,后续再进行补充。希望对大家的学习有所帮助,也希望大家多多支持脚本之家。