当前位置:编程学堂 > webpack4.x CommonJS模块化简析

webpack4.x CommonJS模块化简析

  • 发布:2023-10-01 08:13

首先我们看一下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.html




Webpack 测试项目

//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的模块化方式、代码分割等,我们稍后会补充。希望对大家的学习有所帮助,也希望大家支持来客网。

相关文章

热门推荐