前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【前端】:模块化与Webpack

【前端】:模块化与Webpack

作者头像
WEBJ2EE
发布2020-07-14 16:55:58
8090
发布2020-07-14 16:55:58
举报
文章被收录于专栏:WebJ2EE
代码语言:javascript
复制
1. 什么是模块化?
2. 模块化极简史
  2.1. 无模块化时代
  2.2. 传统模块化阶段
  2.3. 百家争鸣:CommonJS、AMD、CMD
  2.4. 一统天下,ES6 Module
3. Webpack模块化原理
  3.1. 基本原理
  3.2. webpack 怎么对待 CommonJS 模块?
  3.3. webpack 怎么对待 AMD 模块?
  3.4. webpack 怎么对待 CMD 模块?
  3.5. webpack 怎么对待 ES6 模块?
  3.6. webpack 怎么对待 动态 模块?

1. 什么是模块化?

模块化开发就是封装细节,提供使用接口,彼此之间互不影响,每个模块都是实现某一特定的功能。

——《软件工程》

在模块化编程中,开发者将程序分解成离散功能块(discrete chunks of functionality),并称之为模块。

每个模块具有比完整程序更小的接触面,使得校验、调试、测试轻而易举。精心编写的模块提供了可靠的抽象和封装界限,使得应用程序中每个模块都具有条理清楚的设计和明确的目的。

——《Webpack官网》

有啥好处?

a. 避免命名冲突(不占全局命名空间);

b. 便于依赖管理(无须手动组织JS文件顺序);

c. 利于性能优化(异步模块加载);

e. 提高可维护性;

f. 利于代码复用;

2. 模块化极简史

2.1. 无模块化时代

最初,大家只是把项目中的功能,以文件为单位进行划分;这么干的结果是.....所有的变量、函数都暴露在全局作用域;多人协作开发时,极易出现命名冲突,也容易为了避免命名冲突,硬造一些稀奇古怪的名字....时间越长,麻烦越多...维护成本也越高...

2.2. 传统模块化阶段

这一阶段,WEB开发人员主要是利用JS语言的闭包、原型、函数作用域等特性,减少对全局命名空间的污染;方式方法各有不同,但结果都差不多,比较混乱...

A. “对象”型模块

B. “仿Java类”型模块

C. “立即执行函数(IIFE)”型模块

D. “全局变量输入”型模块

注:上面仅列举了传统模块化方法中的几种常见代码组织形式,还有“放大型”、“宽松放大型”等其它方法,这里就不一一列举了,有兴趣可以看看下面这篇文章... http://www.adequatelygood.com/JavaScript-Module-Pattern-In-Depth.html

传统模块化方法中

基本做到了让模块更独立、减少模块间冲突

但还有个更重要的问题没解决掉

如何清晰地描述模块间依赖?

小结一下:

优点: 传统模块化相比于无模块化时代,显然是进步的:减少了命名冲突,增强了模块的独立性; 缺点: 1. 污染全局作用域:虽然我们通过各种手段尽力避免,但实际未从根本上解决; 2. 依赖关系不明显:对于大型项目,模块数量巨大,开发人员必须手动解决模块间依赖,这在复杂项目中极易出错维护成本高

2.3. 百家争鸣:CommonJS、AMD、CMD

JavaScript在语言层面迟迟不推出模块化功能,这个背景下,各“民间组织”提出了CommonJSAMDCMD模块化规范.....

——《高手在民间》

A. CommonJS

Node.js的诞生,使JavaScript扩展到了服务器端, 为了让JavaScript在服务器端能跟Java、Phyton一样编写大型程序,于是有了CommonJS模块化规范;

(1). CommonJS是针对服务器端(非浏览器环境)的JavaScript开发,是Node.js的默认模块化规范; (2). CommonJS是一种只适用于JavaScript静态模块化规范注:只适用于JavaScript,意味着它无法把CSS等前端资源纳入模块化管理范围,但显然CSS也是组成前端模块的重要部分; 注:静态模块化规范,意味着它无法实现按需加载; (3). CommonJS所有模块均是同步阻塞式加载,无法实现异步加载; 注:服务器端加载模块是从硬盘直接读取,时间消耗和忽略不计;但浏览器端需要经网络下载,时间消耗取决于网速,同步加载策略容易出现“假死”,因此“同步阻塞式”加载策略不适用于浏览器环境

CommonJS模块示例:

CommonJS是针对服务器端JavaScript的规范

但不适用于浏览器端

于是衍生出针对浏览器端的

AMD和CMD规范

B. AMD

AMD(Asynchronous Module Definition),异步模块定义;

实现:RequireJS; 特性:依赖前置,提前执行;

AMD模块示例:

C. CMD(Common Module Definition)

CMD(Common Module Definition),通用模块定义;CMD与AMD很类似,只是在模块的运行、解析时机上有所不同;

实现:SeaJS; 特性:依赖就近,延迟执行;

CMD模块示例:

注:CommonJS、AMD、CMD都是语言规范缺失背景下的产物,了解即可;

2.4. 一统天下,ES6 Module

ES6在语言规格的层面上实现了模块功能,而且实现的相当简单,完全可以取代现有的CommonJS、AMD和CMD规范,成为浏览器和服务器通用的模块解决方案

特点:

语言级、静态模块化规范; 实现按需加载的dynamic import语法提案现处于stage3阶段; 完全可替代CommonJS、AMD、CMD;

ES6 Module示例1(静态):

ES6 Module示例2(动态):

注:ES6模块规范和dynamic-import提案,各浏览器还不能完美支持,仍需要JavaScript的编译器babel帮忙...

小结一下

传统模块化手段:通过JS的闭包、对象、自执行函数等语言特性,避免模块间的命名冲突,提高模块的内聚性,但无统一编程标准,也无法把模块间的依赖关系描述清晰; CommonJS:Node.js让JavaScript延伸到“服务端”领域,促使针对“服务端”的JavaScript静态模块化规范CommonJS诞生,但此规范的“同步阻塞式”模块加载策略不适用于浏览器端环境; AMD,CMD:CommonJS规范的衍生品,支持模块“异步并行加载”,适用浏览器环境;AMD推崇“依赖前置”、CMD则是“依赖后置”;AMD规范的产物为RequireJS,CMD是SeaJS; ES6 Module:官方模块化标准,是语言的一部分,无需额外引入第三方库;ES6 Module同CommonJS一样,也是静态模块化规范,无法实现“按需加载”;但目前有一份处于stage3阶段的 dynamic import(tc39/proposal-dynamic-import)提案可用于动态模块加载;ES6完全可以取代CommonJS、AMD、CMD,成为浏览器和服务器端通用的模块化解决方案;

3. Webpack 模块化原理

3.1. 基本原理

先来观察最、最、最简单的 webpack 打包示例。

webpack.config.js:

代码语言:javascript
复制
// "webpack": "^4.42.1",
module.exports = {
    mode: 'development',
    devtool: false,
    entry: './src/entry.js'
};

src/entry.js:【只有这么一个文件】

代码语言:javascript
复制
alert("This is entry!");

dist/main.js:【webpack 打包输出】

代码语言:javascript
复制
/******/ (function(modules) { // webpackBootstrap
/******/   // The module cache
/******/   var installedModules = {};
/******/
/******/   // The require function
/******/   function __webpack_require__(moduleId) {
/******/
/******/     // Check if module is in cache
/******/     if(installedModules[moduleId]) {
/******/       return installedModules[moduleId].exports;
/******/     }
/******/     // Create a new module (and put it into the cache)
/******/     var module = installedModules[moduleId] = {
/******/       i: moduleId,
/******/       l: false,
/******/       exports: {}
/******/     };
/******/
/******/     // Execute the module function
/******/     modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/     // Flag the module as loaded
/******/     module.l = true;
/******/
/******/     // Return the exports of the module
/******/     return module.exports;
/******/   }
/******/
/******/
/******/   // expose the modules object (__webpack_modules__)
/******/   __webpack_require__.m = modules;
/******/
/******/   // expose the module cache
/******/   __webpack_require__.c = installedModules;
/******/
/******/   // define getter function for harmony exports
/******/   __webpack_require__.d = function(exports, name, getter) {
/******/     if(!__webpack_require__.o(exports, name)) {
/******/       Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/     }
/******/   };
/******/
/******/   // 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 = "";
/******/
/******/
/******/   // Load entry module and return exports
/******/   return __webpack_require__(__webpack_require__.s = "./src/entry.js");
/******/ })
/************************************************************************/
/******/ ({

/***/ "./src/entry.js":
/*!**********************!*\
  !*** ./src/entry.js ***!
  \**********************/
/*! no static exports found */
/***/ (function(module, exports) {

alert("This is entry!");


/***/ })

/******/ });
  1. 总体结构
  1. __webpack_require__
  1. 再看看其他辅助函数

3.2. webpack 怎么对待 CommonJS 模块?

webpack.config.js:

代码语言:javascript
复制
module.exports = {
    mode: 'development',
    devtool: false,
    entry: './src/entry.js'
};

src/cjs.js:【CommonJS模块】

代码语言:javascript
复制
module.exports.m1 = function(){
    console.log("cjs-m1");
};

module.exports.m2 = function(){
    console.log("cjs-m2");
};

src/entry.js:【CommonJS模块】

代码语言:javascript
复制
const cjs = require("./cjs");
cjs.m1();
cjs.m2();

dist/main.js:【打包结果】

3.3. webpack 怎么对待 AMD 模块?

webpack.config.js:

代码语言:javascript
复制
module.exports = {
    mode: 'development',
    devtool: false,
    entry: './src/entry.js'
};

src/amd.js:【AMD模块】

代码语言:javascript
复制
define(["./amd1"], function ([amd1]) {
    amd1.m3();
    amd1.m4();

    return {
        m1: function(){
            console.log("amd-m1");
        },
        m2: function(){
            console.log("amd-m2");
        }
    }
});

src/amd1.js:【AMD模块】

代码语言:javascript
复制
define(function () {
    return {
        m3: function(){
            console.log("amd-m3");
        },
        m4: function(){
            console.log("amd-m4");
        }
    }
});

src/entry.js:【CommonJS模块】

代码语言:javascript
复制
const amd = require("./amd");
amd.m1();
amd.m2();

dist/main.js:【打包结果】

3.4. webpack 怎么对待 CMD 模块?

webpack.config.js:

代码语言:javascript
复制
module.exports = {
    mode: 'development',
    devtool: false,
    entry: './src/entry.js'
};

src/cmd.js:【CMD模块】

代码语言:javascript
复制
define(function(require, exports, module) {
    const m = require('./cmd1');

    m.m3();
    m.m4();

    exports.m1 = function(){
        console.log("cmd-m1");
    };

    exports.m2 = function(){
        console.log("cmd-m2");
    }
});

src/cmd1.js:【CMD模块】

代码语言:javascript
复制
define(function(require, exports, module) {
    exports.m3 = function(){
        console.log("cmd-m3");
    };

    exports.m4 = function(){
        console.log("cmd-m4");
    }
});

src/entry.js:【CommonJS模块】

代码语言:javascript
复制
const cmd = require("./cmd");
cmd.m1();
cmd.m2();

dist/main.js:【打包结果】

3.5. webpack 怎么对待 ES6 模块?

webpack.config.js:

代码语言:javascript
复制
module.exports = {
    mode: 'development',
    devtool: false,
    entry: './src/entry.js'
};

src/es.js:【ES6模块】

代码语言:javascript
复制
import es1default, {m1 as es1m1, m2 as es1m2} from "./es1"

es1default();
es1m1();
es1m2();

export default function m(){
    console.log('es-default');
}

export function m1(){
    console.log('es-m1');
}

export function m2(){
    console.log('es-m2');
}

src/es1.js:【ES6模块】

代码语言:javascript
复制
export default function m(){
    console.log('es1-default');
}

export function m1(){
    console.log('es1-m1');
}

export function m2(){
    console.log('es1-m2');
}

src/entry.js:【CommonJS模块】

代码语言:javascript
复制
const es= require("./es");

es.default();
es.m1();
es.m2();

dist/main.js:【打包结果】

3.6. webpack 怎么对待 动态 模块?

webpack.config.js:

代码语言:javascript
复制
module.exports = {
    mode: 'development',
    devtool: false,
    entry: './src/entry.js'
};

src/folder/es2.js:【ES6模块】

代码语言:javascript
复制
export default function m(){
    console.log('es2-default');
}

export function m1(){
    console.log('es2-m1');
}

export function m2(){
    console.log('es2-m2');
}

src/folder/es3.js:【ES6模块】

代码语言:javascript
复制
export default function m(){
    console.log('es3-default');
}

export function m1(){
    console.log('es3-m1');
}

export function m2(){
    console.log('es3-m2');
}

src/entry.js:【ES6模块】

代码语言:javascript
复制
import (/* webpackChunkName:"dynamic" */ "./folder/es2").then((module)=>{
    module.default();
    module.m2();
    module.m1();
});
import (/* webpackChunkName:"dynamic" */ "./folder/es3").then((module)=>{
    module.default();
    module.m2();
    module.m1();
});

dist/main.js:【打包结果-入口模块】

代码语言:javascript
复制
/******/ (function(modules) { // webpackBootstrap
/******/   // install a JSONP callback for chunk loading
/******/   function webpackJsonpCallback(data) {
/******/     var chunkIds = data[0];
/******/     var moreModules = data[1];
/******/
/******/
/******/     // add "moreModules" to the modules object,
/******/     // then flag all "chunkIds" as loaded and fire callback
/******/     var moduleId, chunkId, i = 0, resolves = [];
/******/     for(;i < chunkIds.length; i++) {
/******/       chunkId = chunkIds[i];
/******/       if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
/******/         resolves.push(installedChunks[chunkId][0]);
/******/       }
/******/       installedChunks[chunkId] = 0;
/******/     }
/******/     for(moduleId in moreModules) {
/******/       if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
/******/         modules[moduleId] = moreModules[moduleId];
/******/       }
/******/     }
/******/     if(parentJsonpFunction) parentJsonpFunction(data);
/******/
/******/     while(resolves.length) {
/******/       resolves.shift()();
/******/     }
/******/
/******/   };
/******/
/******/
/******/   // The module cache
/******/   var installedModules = {};
/******/
/******/   // object to store loaded and loading chunks
/******/   // undefined = chunk not loaded, null = chunk preloaded/prefetched
/******/   // Promise = chunk loading, 0 = chunk loaded
/******/   var installedChunks = {
/******/     "main": 0
/******/   };
/******/
/******/
/******/
/******/   // script path function
/******/   function jsonpScriptSrc(chunkId) {
/******/     return __webpack_require__.p + "" + ({"dynamic":"dynamic"}[chunkId]||chunkId) + ".js"
/******/   }
/******/
/******/   // The require function
/******/   function __webpack_require__(moduleId) // 没变;
/******/
/******/   // This file contains only the entry chunk.
/******/   // The chunk loading function for additional chunks
/******/   __webpack_require__.e = function requireEnsure(chunkId) {
/******/     var promises = [];
/******/
/******/
/******/     // JSONP chunk loading for javascript
/******/
/******/     var installedChunkData = installedChunks[chunkId];
/******/     if(installedChunkData !== 0) { // 0 means "already installed".
/******/
/******/       // a Promise means "currently loading".
/******/       if(installedChunkData) {
/******/         promises.push(installedChunkData[2]);
/******/       } else {
/******/         // setup Promise in chunk cache
/******/         var promise = new Promise(function(resolve, reject) {
/******/           installedChunkData = installedChunks[chunkId] = [resolve, reject];
/******/         });
/******/         promises.push(installedChunkData[2] = promise);
/******/
/******/         // start chunk loading
/******/         var script = document.createElement('script');
/******/         var onScriptComplete;
/******/
/******/         script.charset = 'utf-8';
/******/         script.timeout = 120;
/******/         if (__webpack_require__.nc) {
/******/           script.setAttribute("nonce", __webpack_require__.nc);
/******/         }
/******/         script.src = jsonpScriptSrc(chunkId);
/******/
/******/         // create error before stack unwound to get useful stacktrace later
/******/         var error = new Error();
/******/         onScriptComplete = function (event) {
/******/           // avoid mem leaks in IE.
/******/           script.onerror = script.onload = null;
/******/           clearTimeout(timeout);
/******/           var chunk = installedChunks[chunkId];
/******/           if(chunk !== 0) {
/******/             if(chunk) {
/******/               var errorType = event && (event.type === 'load' ? 'missing' : event.type);
/******/               var realSrc = event && event.target && event.target.src;
/******/               error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
/******/               error.name = 'ChunkLoadError';
/******/               error.type = errorType;
/******/               error.request = realSrc;
/******/               chunk[1](error);
/******/             }
/******/             installedChunks[chunkId] = undefined;
/******/           }
/******/         };
/******/         var timeout = setTimeout(function(){
/******/           onScriptComplete({ type: 'timeout', target: script });
/******/         }, 120000);
/******/         script.onerror = script.onload = onScriptComplete;
/******/         document.head.appendChild(script);
/******/       }
/******/     }
/******/     return Promise.all(promises);
/******/   };
/******/
/******/   // expose the modules object (__webpack_modules__)
/******/   __webpack_require__.m = modules;
/******/
/******/   // expose the module cache
/******/   __webpack_require__.c = installedModules;
/******/
/******/   // define getter function for harmony exports
/******/   __webpack_require__.d = // 没变;
/******/
/******/   // define __esModule on exports
/******/   __webpack_require__.r = // 没变;
/******/
/******/   // 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 = // 没变;
/******/
/******/   // getDefaultExport function for compatibility with non-harmony modules
/******/   __webpack_require__.n = // 没变;
/******/
/******/   // Object.prototype.hasOwnProperty.call
/******/   __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/   // __webpack_public_path__
/******/   __webpack_require__.p = "";
/******/
/******/   // on error function for async loading
/******/   __webpack_require__.oe = function(err) { console.error(err); throw err; };
/******/
/******/   var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
/******/   var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
/******/   jsonpArray.push = webpackJsonpCallback;
/******/   jsonpArray = jsonpArray.slice();
/******/   for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
/******/   var parentJsonpFunction = oldJsonpFunction;
/******/
/******/
/******/   // Load entry module and return exports
/******/   return __webpack_require__(__webpack_require__.s = "./src/entry.js");
/******/ })
/************************************************************************/
/******/ ({

/***/ "./src/entry.js":
/*!**********************!*\
  !*** ./src/entry.js ***!
  \**********************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {

__webpack_require__.e(/*! import() | dynamic */ "dynamic").then(__webpack_require__.bind(null, /*! ./folder/es2 */ "./src/folder/es2.js")).then((module)=>{
    module.default();
    module.m2();
    module.m1();
});
__webpack_require__.e(/*! import() | dynamic */ "dynamic").then(__webpack_require__.bind(null, /*! ./folder/es3 */ "./src/folder/es3.js")).then((module)=>{
    module.default();
    module.m2();
    module.m1();
});


/***/ })

/******/ });

dist/dynamic.js:【打包结果-动态模块】

几个关键点:

  1. 对于待加载的动态模块,由 installedChunks 区域存储。
  1. __webpack_require__e 方法负责通过 JSONP 模式,加载动态模块。
  1. 对打包后的动态模块来说,它只需要知道,它必须把自己 push 进 window["webpackJsonp"] 数组即可。
  2. 但是 window["webpackJsonp"] 的 push 方法被 “劫持”,hook 了 webpackJsonpCallback 函数。
  1. webpackJsonpCallback 负责将加载完成的的模块,加入 modules 中,已被未来 __webpack_require_ 使用。

参考:

传统模块化编程方法: http://www.adequatelygood.com/JavaScript-Module-Pattern-In-Depth.html AMD规范:https://github.com/amdjs/amdjs-api/wiki RequireJS:http://requirejs.org/ CMD规范:https://github.com/seajs/seajs/issues/242 SeaJS:http://www.zhangxinxu.com/sp/seajs/ 《前端工程化体系设计与实践》 avaScript Module Systems Showdown: CommonJS vs AMD vs ES2015: https://auth0.com/blog/javascript-module-systems-showdown/


本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-07-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 WebJ2EE 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
命令行工具
腾讯云命令行工具 TCCLI 是管理腾讯云资源的统一工具。使用腾讯云命令行工具,您可以快速调用腾讯云 API 来管理您的腾讯云资源。此外,您还可以基于腾讯云的命令行工具来做自动化和脚本处理,以更多样的方式进行组合和重用。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档