首先我们看一下webpack官方文档中对模块的描述:
在模块化编程中,开发人员将程序分解为离散的功能块,并将它们称为模块。
每个模块的接触面比整个程序更小,使验证、调试和测试变得轻而易举。编写良好的模块提供了可靠的抽象和封装边界,以便应用程序中的每个模块都有连贯的设计和明确的目的。
webpack 的核心概念之一是一切都是模块。 webpack在项目中的作用是分析项目的结构,找到浏览器不能直接运行的JavaScript模块和其他扩展语言(less、scss、typescript),并将其打包成适合浏览器使用的格式。它从一个项目的主文件开始,根据引用路径找到它所依赖的所有文件,同时对这些文件进行处理(各种加载器解析、编译、处理和浏览浏览器不能直接使用的文件),然后打包成一个或多个浏览器可以识别的JavaScript文件。
本文不会详细描述webpack构建过程。毕竟官网已经详细解释过了。这里主要分析webpack打包的文件,文件是如何打包的,以及如何使用模块。 webpack最早支持的代码模块化方式是CommonJS,后来逐渐支持ES6、AMD等,无论使用哪种方式,webpack都可以解析并打包。本文中的示例使用 CommonJS 规范。更多规格介绍请查看官方文档。
示例
为了方便后面的指令,先创建一个项目,即先创建一个文件夹webpack-test(自己命名),然后在里面新建一个package.json文件,用于npm指令。在 webpack 中 - 使用测试文件夹中的命令:
npm init
执行命令后,会问一些问题,一路回车即可。然后使用以下命令安装webpack依赖包:
npm install --save-dev webpack
再创建几个文件:
1。在项目根目录新建文件夹app存放业务代码,文件夹public存放打包文件;
2、在app中新建入口文件main.js;
3、在app中新建功能模块hello.js、bye.js、to.js;
4、在项目根目录下,创建index.html文件;
然后依次填写这些文件的以下内容:
// webpack-test/app/hello.js const to = require('./to.js'); module.exports = function() { var hello = document.createElement('div'); hello.textContent = "向你问好" + www.sychzs.cn; 回复你好; };
// webpack-test/app/bye.js const to = require('./to.js'); module.exports = function() { var bye = document.createElement('div'); bye.textContent = "说再见" + www.sychzs.cn; 返回再见; };
// webpack-test/app/to.js module.exports = {name: "小明"};
// webpack-test/app/main.js const hello = require('./hello.js'); const bye = require('./bye.js'); document.querySelector("#root").appendChild(hello()).appendChild(bye());;
// webpack-test/index.htmlWebpack 测试项目 //bundle.js文件是我们稍后将app中的文件打包后生成的结果文件。
业务模块hello.js和bye.js执行各自的操作,引用公共文件to.js;主文件main.js引用并执行模块hello.js和bye.js; index.html 文件引入了main.js 打包后的最终文件bundle.js。
包装
接下来,进行包装操作。首先确保 webpack 已全局安装。否则执行时需要指定webpack的路径。例如4.0以下版本,使用node_modules/.bin/webpack ./app/main.js ./public/bundle.js;
如果您使用的是 webpack4.0+ 并使用 webpack ./app/main.js ./public/bundle.js 命令,您可能会收到以下错误:
配置中的警告
尚未设置“mode”选项,webpack 将回退到该值的“生产”。将“模式”选项设置为“开发”或“生产”以启用每个环境的默认值。
您还可以将其设置为“无”以禁用任何默认行为。了解更多:https://www.sychzs.cn/concepts/mode/多个 ./app/main.js ./public/bundle.js
中出现错误 找不到模块:错误:无法解析“/Users/zhaohaipeng/soyoung-project/webpack-test”中的“./public/bundle.js”
@ multi ./app/main.js ./public/bundle.js main[1]
webpack 4.0+以后,第一次报错,需要指定环境--modedevelopment;第二个错误是因为我们没有使用配置文件来打包,而是直接使用命令指定的打包输出位置,所以需要声明输出文件。综上所述,正确的命令如下:
webpack app/main.js --output public/bundle.js --modedevelopment
执行结果:
➜ webpack-test webpack app/main.js --output public/bundle.js --modedevelopment
哈希值:a4e2f9ecc51b64891624
版本:webpack 4.25.1
时间:90ms
建成于:2018-11-08 17:11:01
资产大小块块名称
bundle.js 5.16 KiB main [已发出] main
入口点 main = bundle.js
[./app/bye.js] 165 字节 {main} [内置]
[./app/hello.js] 173 字节 {main} [内置]
[./app/main.js] 144 字节 {main} [内置]
[./app/to.js] 30 字节 {main} [内置]
➜ webpack-测试
在浏览器中打开index.html文件,您将看到结果
向小明问好
跟小明说再见
但是webpack是一个可以简化我们的开发并且易于使用的工具。显然,像上面那样敲很多命令来打包并不方便,所以我们再用配置文件来打包一下:
在根目录下创建webpack.config.js文件并配置打包入口和出口:
// webpack-test/webpack.config.js 模块. 导出 = { mode: "development",//需要在webpack.0之后声明环境 entry: __dirname + "/app/main.js",//唯一入口文件 输出: { path: __dirname + "/public",//打包文件存放目录 filename: "bundle.js"//打包后输出文件名 } }
再次打包时,只需要使用命令webpack即可。 Webpack默认读取当前路径下的webpack.config.js文件。
最终打包的bundle.js文件去掉了多余的注释,并调整了代码格式。内容如下:
// 自执行函数,参数由所有模块组成,格式为key:value,key为模块名 (function(modules) { // webpackBootstrap // 已加载模块的缓存记录了模块的加载状态,也是为了避免重复打包,节省资源。 var 安装模块 = {}; // webpack使用require方法加载模块(模拟ConmmonJS reqiure()),根据传入的模块id处理对应的模块,添加加载的缓存,执行,标记,返回exports 函数 __webpack_require__(moduleId) { // moduleId是模块路径 // 检查模块是否已加载。如果已经加载,则直接返回该模块。 if(installedModules[moduleId]) { 返回installedModules[moduleId].exports; } // 如果当前模块未加载,则创建一个新模块并将其存储在缓存中。var 模块=已安装的模块[模块Id] = { i:模块ID, l:假的, 导出:{} }; // 执行当前模块的exports下的模块代码,即模块内部,高亮范围 模块[moduleId].call(module.exports, module, module.exports, __webpack_require__); // 标记模块已加载 模块.l = true; // 返回模块导出的内容 返回模块.exports; } //挂载该模块的属性(__webpack_modules__) __webpack_require__.m = 模块; //挂载属性,模块加载缓存 __webpack_require__.c = 安装的模块; // 这段代码没有执行,暂时不分析。 //在exports中定义getter方法 __webpack_require__.d = 函数(导出、名称、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中调用) // 在导出时定义 __esModule __webpack_require__.r = 函数(导出){ if(typeof Symbol !== '未定义' && Symbol.toStringTag) {Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); } Object.defineProperty(exports, '__esModule', { value: true }); }; // 这段代码没有执行,暂时不分析。 //创建一个伪命名空间对象 // 创建一个假的命名空间对象 // mode & 1: value是模块id,需要它 // 模式&2:将value的所有属性合并到ns中 // mode & 4: 当已经是 ns 对象时返回值 // mode & 8|1: 行为类似于 require __webpack_require__.t = 函数(值,模式){ if(模式 & 1) 值 = __webpack_require__(值); if(mode & 8) 返回值; if((mode & 4) && typeof value === 'object' && value && value.__esModule) 返回值; 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)); 返回 ns; }; // 这段代码没有执行,暂时不分析。// getDefaultExport 函数用于与非 Harmony 模块兼容 __webpack_require__.n = 函数(模块){ var getter = 模块 && module.__esModule ? 函数 getDefault() { return module['default']; }: 函数 getModuleExports() { 返回模块; }; __webpack_require__.d(getter, 'a', getter); 返回吸气剂; }; // www.sychzs.cn // 判断某个属性是否是在对象本身定义的而不是从原型链继承的 __webpack_require__.o = function(object, property) { return www.sychzs.cn(object, property); } }; // __webpack_public_path__ __webpack_require__.p = ""; //加载入口模块main.js并返回exports,从而从入口文件开始执行,递归执行并返回所有依赖 返回 __webpack_require__(__webpack_require__.s = "./app/main.js"); })({ “./app/bye.js”:(函数(模块,导出,__webpack_require__){eval("const to = __webpack_require__(/*! ./to.js */ "./app/to.js");nmodule.exports = function() {n var bye = document.createElement('div'); n bye.textContent = "对 " + www.sychzs.cn 说再见;n return bye;n};nn//# sourceURL=webpack:///./app/bye.js?"); }), “./app/hello.js”:(函数(模块,导出){ eval("module.exports = function() {n var hello = document.createElement('div');n hello.textContent = "问好!";n return hello;n};nn//# sourceURL=webpack: ///./app/hello.js?"); }), “./app/main.js”:(函数(模块,导出,__webpack_require__){ eval("const hello = __webpack_require__(/*! ./hello.js */ "./app/hello.js");nconst bye = __webpack_require__(/*! ./bye.js */ "./app/bye .js");nndocument.querySelector("#root").appendChild(hello()).appendChild(bye());;nn//# sourceURL=webpack:///./app/main.js?" ); }), “./app/to.js”:(函数(模块,导出){ eval("module.exports = {name: "小明"};nn//# sourceURL=webpack:///./app/to.js?"); }) });
分析
webpack的运行过程可以分为:读取配置参数、实例化插件、模块解析处理(loader)、输出打包文件;上面的例子中只使用了JavaScript引用,并没有使用CSS、less等插件和组件。 、图片等需要loader处理的模块,所以在上面的例子中,流程只涉及读取配置、识别入口及其引用的模块、打包几个步骤,生成最终的bundle.js文件。
简单描述一下webpack在此过程中的执行过程:读取配置文件中的entry,如果配置了plugins参数,则此时实例化插件并绑定钩子函数;模块解析,也是加载器添加的时刻。从入口文件开始,根据入口文件对其他模块的依赖关系,结合配置文件中不同类型文件所使用的加载器(loader)指令,对这些模块进行一一解析。要么压缩,要么转义,生成浏览器可以直接识别的内容;最后将所有模块打包并输出打包文件。上面的代码中,bundle.js的内容已经被注释掉了。我们来分析一下bundle.js的执行流程:
1。自执行功能
最终输出文件bundle.js是一个JavaScript文件,它本身实际上是一个自执行函数
(函数(参数){})(参数)。
2,参数
自执行方法的参数是所有模块组成的对象,key是各个模块的路径,value是各个模块内部的执行代码。观察参数里面的代码,对比打包前的源码。可以发现,所有的 require 都变成了 webpack 自定义模块调用方法 __webpack_require__ ,源代码中的相对路径变成了最终执行位置的文件的相对路径。
{ “./app/bye.js”:(函数(模块,导出,__webpack_require__){eval("const to = __webpack_require__(/*! ./to.js */ "./app/to.js");nmodule.exports = function() {n var bye = document.createElement('div'); n bye.textContent = "对 " + www.sychzs.cn 说再见;n return bye;n};nn//# sourceURL=webpack:///./app/bye.js?"); }), “./app/hello.js”:(函数(模块,导出){ eval("module.exports = function() {n var hello = document.createElement('div');n hello.textContent = "问好!";n return hello;n};nn//# sourceURL=webpack: ///./app/hello.js?"); }), “./app/main.js”:(函数(模块,导出,__webpack_require__){ eval("const hello = __webpack_require__(/*! ./hello.js */ "./app/hello.js");nconst bye = __webpack_require__(/*! ./bye.js */ "./app/bye .js");nndocument.querySelector("#root").appendChild(hello()).appendChild(bye());;nn//# sourceURL=webpack:///./app/main.js?" ); }), “./app/to.js”:(函数(模块,导出){ eval("module.exports = {name: "小明"};nn//# sourceURL=webpack:///./app/to.js?"); }) }
3、执行
(1)自执行文件开始执行后,到自执行函数最底层,首先从入口文件
加载return __webpack_require__(__webpack_require__.s = "./app/main.js");
(2)调用__webpack_require__函数,传入参数./app/main.js,
函数 __webpack_require__(moduleId) { // moduleId 是 ./app/main.js // 第一次进来,未加载,模块尚未缓存 if(installedModules[moduleId]) { 返回installedModules[moduleId].exports; } // 创建一个新的./app/main.js模块并将其存储在缓存中 var 模块=已安装的模块[模块Id] = { i:模块ID, l:假的, 导出:{} }; 模块[moduleId].call(module.exports, module, module.exports, __webpack_require__); // 标记模块已加载 模块.l = true; // 输出模块的内容 返回模块.exports; }
此时执行modules[moduleId].call(module.exports, module, module.exports,__webpack_require__);该方法中相当于在名为 ./app/main.js 的模块中执行以下代码:
(函数(模块,导出,__webpack_require__){eval("const hello = __webpack_require__(/*! ./hello.js */ "./app/hello.js");nconst bye = __webpack_require__(/*! ./bye.js */ "./app/bye .js");nndocument.querySelector("#root").appendChild(hello()).appendChild(bye());;nn//# sourceURL=webpack:///./app/main.js?" ); })()
由于引用关系,__webpack_require__方法会再次执行两次,分别传递模块路径./app/hello.js和./app/bye.js;
(3) 执行第一个__webpack_require__进程。除了参数不同以及执行的模块不同之外,与第二步基本相同。再次找到依赖模块to.js,再次调用__webpack_require__。
“./app/hello.js”:(函数(模块,导出,__webpack_require__){ eval("const to = __webpack_require__(/*! ./to.js */ "./app/to.js");nmodule.exports = function() {n var hello = document.createElement('div'); n hello.textContent = "向 " 打招呼 + www.sychzs.cn;n return hello;n};nn//# sourceURL=webpack:///./app/hello.js?"); }),
(4)执行第二个__webpack_require__时,在bye.js中发现了对to.js的依赖,因此会继续调用__webpack_require__方法,但传递的参数变成了./app/to。 js,到达终点。
“./app/bye.js”:(函数(模块,导出,__webpack_require__){ eval("const to = __webpack_require__(/*! ./to.js */ "./app/to.js");nmodule.exports = function() {n var bye = document.createElement('div'); n bye.textContent = "对 " + www.sychzs.cn 说再见;n return bye;n};nn//# sourceURL=webpack:///./app/bye.js?"); })
(5) 至此,从入口文件开始的整个依赖模块的分析已经完成,并且所有的js代码都已经被引用并放置在bundle.js中。
总结
这里可以看到,webpack对js的封装就是将其封装成各个方法。参考这些方法,实现了模块化的效果;而封装的过程就是查找、解析、封装这些方法。单线程的过程,当项目较大或者模块之间的依赖关系复杂时,webpack打包会比较耗时。
以上是对webpack4中js模块处理的简单理解。关于ES6和AMD的模块化方式、代码分割等,我们稍后会补充。希望对大家的学习有所帮助,也希望大家支持来客网。