最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 前端模块化的理解以及主要区别

    正文概述 掘金(poder)   2021-01-24   637

    前言

    javascipt 在发展初期,并没有模块化的概念,Node 的诞生促进了 CommonJS 的诞生,但该标准只适用于服务端模块。为了在游览器端也能使用, AMD 和 CMD 诞生了,后来又为了兼容前三者,UMD 诞生了,随后的官方标准 ESM 吸收前辈的各个优点。除此之外,各个打包器,如 webpack,rollup,也打包出了符合各个标准,供游览器使用的代码。

    1. 模块化的理解

    1.1 什么是模块

    • 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件),并进行组合在一起
    • 块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信

    1.2 模块的优点

    • 避免命名冲突(减少命名空间污染)
    • 更好的分离, 按需加载
    • 更高复用性
    • 高可维护性

    1.3 模块化的进化过程

    • **全局函数模式:**将不同的功能封装成不同的全局函数

      • 缺点: 污染全局命名空间, 容易引起命名冲突或数据不安全,而且模块成员之间看不出直接关系
      function module1(){ //... }
      function module2(){ //... }  
      
    • **命名空间模式:**简单对象封装

      • 优点:减少了全局变量,减少了命名冲突
      • 缺点:数据不安全(外部可以直接修改模块内部数据)
      const myModule = {
          name: 'laixiaotao',
          run() {
              console.log(`名字是${this.name}`)
          }
      };
      myModule.name = 'zhouxingxing';
      myModule.run();
      
    • **IIFE 模式:**匿名函数自调用,将数据和行为封装到一个函数内部, 通过给 window 添加属性来向外暴露接口

      • 优点:数据是私有的, 外部只能通过暴露的方法操作
      • 缺点:无法当前模块依赖另一个模块问题
      // index.html文件
      <script type="text/javascript" src="module.js"></script>
      <script type="text/javascript">
      	myModule.run();
          console.log(myModule.name);
      </script>
      
      // module.js文件
      (function (window) {
          name: 'laixiaotao',
          run() {
              console.log(`名字是${this.name}`);
          }
          window.myModule = { run }; // 注入window中,对外暴露myModule
      })(window)
      
    • **IIFE 模式增强:**引入依赖,这是现代模块实现的基石。

      • 优点:将依赖作为参数传入,不仅保证了模块的独立性,还使得模块之间的依赖关系变得明显
      // index.html文件
      <!-- 引入的js必须有一定顺序 -->
      <script type="text/javascript" src="jquery-1.10.1.js"></script>
      <script type="text/javascript" src="module.js"></script>
      <script type="text/javascript" src="module1.js"></script>
      <script type="text/javascript">
      	myModule.run();
          console.log(myModule.name);
      </script>
      
      // module.js文件
      (function (window, jquery) {
          name: 'laixiaotao',
          run() {
              console.log(`名字是${this.name}`);
              $('body').css('background', 'red'); // 使用引入的依赖
          }
          window.myModule = { run }; // 注入window中,对外暴露myModule
      })(window, jquery||{}, mudule1||{})
      

    1.4 引入多个 script 标签问题

    IIFE 模式增强模式虽然解决了模块化隔离问题,但是如果简单的使用,会产生引入多个 script 标签问题。

    • **请求过多:**如果依赖过多,也意味着需要更多的请求
    • **依赖模糊:**依赖之间关系混乱,无法理清彼此这件的加载关系,加载顺序
    • **难以维护:**以上两种原因就导致了很难维护,很可能出现牵一发而动全身的情况导致项目出现严重的问题

    因此,为了更好解决这个问题,以及其他问题。模块化规范,通过异步加载,按需加载/懒加载/延迟加载等手段消除了手动引入依赖的弊端,通过自动管理依赖来消除隐患。

    2. 模块化规范

    • 共同点

      • 所有代码都运行在模块作用域,不会污染全局作用域
      • 单例模式,模块可以多次加载,但是只会在第一次加载时执行一次,之后就被缓存到内存中了。之后再加载,就直接读取缓存结果。要想模块重新执行就必须清除缓存。但是不同的规范的单例模式有点区别,尤其要重点注意 CommonJS 和 ESM 的区别
    • 不同点

      • 执行机制:参见 3.1

    2.1 CommonJS

    2.1.1 概述

    Node 模块采用 CommonJS 规范,单个文件就是一个模块,有着属于自己的作用域。文件里定义的变量、函数、类、对象、类等都是私有的,除了文件暴露的,均不可访问。在服务器端,模块的加载是运行时同步加载执行的;在浏览器端,模块需要提前编译打包处理。主要的实现有 Node 和 Webpack

    2.1.2 语法

    • **导出模块:**module.exports = value 或 (module.)exports.xxx = value
    • **导入模块:**require(路径/内置模块名/node_modules模块名)

    2.1.3 简单原理

    // 引入的hello.js文件
    (function (){
        module.exports.a = 3;
    })();
    
    // 实现
    var module = {
        id: '31231231',
        exports: {}
    }
    var load = function (module){
        // 读取的hello.js代码
        module.exports.a = 3;
        // hello.js代码结束
        return module.exports;
    }
    var exported = load(module);
    save(module, exported)
    

    (1)module 变量是在 Node 加载前就准备好的一个变量,实际为一个构造函数 Module 的一个实例,new 使用

    (2)然后在加载文件时,module 作为一个函数参数传给对应的文件使用,然后给这个 module 装上 exports

    (3)其他模块 require,实际上是 require 该模块的 module.exports 属性,require 命令的基本功能是,读入并执行一个 JavaScript 文件,然后返回该模块的exports对象。如果没有发现指定模块,会报错

    2.2 AMD

    2.2.1 概述

    CommonJS 加载模块是同步加载执行的,会阻塞被调用的文件,而AMD 则是非同步加载执行模块的,允许指定回调函数。由于 Node.js 主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以 CommonJS 规范比较适用。但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。此外 AMD 规范比 CommonJS 规范在浏览器端实现要来着早。主要的实现是 RequireJS。

    2.2.2 语法

    • 引入 require.js,并且指明 js 主文件入口

      // 这样就可以直接使用require.js暴露出来的全局变量
      <script type="text/javascript" data-main="js/main" src="js/libs/require.js"></script> 
      
    • 导出导入模块

      // math.js,定义没有依赖的模块,只有导出
      define(function(){
          let count = 1;
         	function add() {
              count++;
          }
          return {count, add} // 暴露模块
      })
      // use.js,提前声明拥有依赖的模块,这叫做依赖前置,既有导出又有导入
      define(['math', 'jquery'], function(math, $){
          const count = math.count;
          const showCount = function() {
              console.log(count);
          }
          // 并不是运行到这里才加载执行module,而是会在整个define开头和math、jquery一起加载执行
          // 注意此require不同与CommonJS、CMD执行机制
          require([module], function(module){
              console.log(module)
          })
          $('body').css('background', 'green');
          return {showCount} // 暴露模块
      })
      
    • 配置文件

      // main.js
      (function() {
         require.config({
             baseUrl: 'js/', // 基本路径
             paths:{
                 // 映射: 模块标识名: 路径
                 math: './modules/math', // 不能写成math.js,会报错,最后生成的路径是 js/modules/math.js
                 use: './modules/use',
                 jquery:'jquery.min.js'
             }
         }),
         require(['use'], function(use) {
             use.showCount();
         })
      })()
      /* 目录结构
       * js
       *  |-modules-math.js
       *  |-main.js
       */
      

    2.3 CMD

    2.3.1 概述

    CMD 规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。CMD 规范整合了 CommonJS 和 AMD 规范的特点。主要的实现是 SeaJS。使用比 AMD 简单许多。已停止使用更新

    2.3.2 语法

    • 引入 sea.js 并调用对应函数

      <script type="text/javascript" src="sea.js"></script>
      <script type="text/javascript">
        seajs.use('./js/modules/main')
      </script>
      
    • 导出导入模块

      // math.js模块
      define(function(require, exports, module) {
          // 跟 CommonJS 导出一样
          exports.xxx = 1;
          module.exports.yyy = 2;
      })
        
      // main.js模块
      define(function(require, exports, module) {
          // 引入模块-同步,会阻塞,运行时才加载,故叫做就近依赖
          const math = require('./module/math')
          // 引入模块-异步,不会阻塞,运行时才加载,故叫做就近依赖
          const asyncModule = require.async('./xx', function(asyncModule) {
              console.log(asyncModule)
          })
      })
      /* 目录结构
       * js
       *  |-modules-math.js
       *  |-main.js
       */
      

    2.4 UMD

    一种 JS 通用模块定义规范,目的是用一种规范同时容纳兼容 CommonJS、AMD、CMD 模块,可以导入导出任意环境下可以使用的模块。但是由于 ESM 的流行,现已不常用,@详细学习

    2.5 ESM

    2.5.1 概述

    ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS、AMD、CMD 模块,都只能在运行时确定这些东西。因为他们都是用内存形成的单例模式,而 ESM 是由 JS 引擎去做静态编译。

    2.5.2 特点

    • import/export 需要声明 type="module",import() 不需要
    • import/export 必须在顶层作用域,import() 不需要,但它返回的是一个 promise
    • 单例模式,每个模块只加载到内存运行一次,注意和 CommonJS 区别
    • import 属性是只读的,不能赋值,但可以设置别名

    2.5.3 语法

    • 导出模块:export let/var/const 变量名 = 值 或 export default 值
    • 导入模块:import ... from ‘路径‘
    • 动态加载:import()

    3. 不同模块化规范对比

    3.1 CommonJS、AMD、CMD 对比

    CommonJSAMDCMD
    主要实现node、webpack<=4require.jssea.js文件 VS 模块1. node:文件即是模块
    2. webpack:文件可以多模块
    文件即模块,单个 JS 文件只有第一个define 生效文件即模块,单个 JS 文件只有第一个define 生效html 文件引用webpack:打包的同时,会在 html 中生成 script异步加载,通过 head.appendChild 注入异步加载,通过 head.appendChild 注入模块调用requirerequire、definerequire、require.async执行机制顺序加载执行依赖,会阻塞调用文件执行提前加载执行 define 和 require 引用依赖(先加载好先执行),不会阻塞调用文件执行require:顺序加载执行依赖,会阻塞调用文件执行
    require.async:异步加载执行依赖,不会阻塞调用文件执行
    • **webpack 文件:**webpack 打包后的单个文件可以只有一个模块,如各种 chunk.xxx.js;也可以单个文件多个模块,如 chunk-vendors.xxx.js 包含多个第三方模块

    • 依赖前置 vs 依赖就近

      • **依赖前置:**在定义模块的时候就要声明其依赖的模块。AMD 无论是 define、require 都会在执行调用文件前,提前加载执行 define 和 require 引用依赖(先加载好先执行),因此 requireJS 作者推崇依赖前置。
      • **就近依赖:**只有在用到某个模块的时候再去 require,由于会阻塞调用文件的执行,因此 CMD 推崇就近执行,CommonJS 随意。
      // 如下模块通过SeaJS/RequireJS来加载,参考:https://www.douban.com/note/283566440/
      define(function(require, exports, module){
          console.log(“这是主模块”);
          var module1 = require("./module1");
          module1.say();
          var module2 = require("./module2");
          module2.say();
          return {
      		hello: funtion() { console.log("Hello, main") }
          }
      })
        
      // AMD执行结果:这是模块1、这是模块2、这是主模块、Hello,module1、Hello,module2、Hello、main
      // CMD执行结果:这是主模块、这是模块1、Hello,module1、这是模块2、Hello,module2、Hello、main
      

    3.2 CommonJS、ESM 对比

    3.2.1 ESM babel

    • ESMBable
      const { run } = obj;
      run();
      var run = obj.run;
      run();

      对象解构出来的变量,等价于重新拿个同名变量去赋值,相当等号,该怎样就怎样。

    • ESMBabel
      export let count;
      export default {};
      Object.defineProperty(exports, "__esModule", {
      value: true
      });
      exports.default = exports.count = void 0;
      var count;
      exports.count = count;
      var _default = {};
      exports.default = _default;
      import a from 'a.js';var _a = _interopRequireDefault (require("a.js"));
      function _interopRequireDefault (obj) {
      return obj && obj.__esModule ? obj : { default: obj };
      }

      (1)_esModule 标识挂载在 exports,这表明它是一个由 ESM 模块转换而来的 CommonJS 模块

      (2)ESM 模块本身是可以同时支持声明导出和默认导出的,Babel 时,如果简单地用 CommonJS 模拟 ESM 的默认导出,会出现问题,如下面代码,因此,Babel 在 exports 上使用了一个 default 变量来承载 ESM 的默认导出;这样就可以同时兼容声明导出和默认导出了。

      module.exports.xxx1 = 1;
      exports.xxx2 = 2;		 // 内部实现:var exports = module.exports
      module.exports = {};     // module.exports重新指向,导致上面两行全部失效,无法导出xxx1,xxx2
      module.exports.yyy1 = 1;
      exports.yyy2 = 1;        // 失效,因为mdule.exports重新指向,导致和exports联系中断
      

      (3)如果没有 Babel 转译,CommonJS 和 ESM 模块当然是不可以随意混用的,但由于 ESM 模块的 Babel 是采用了 CommonJS,这也意味着只要源文件需要通过 Babel,那么可以使用 ESM import 或 CommonJS require 引入 ESM export。但是两者是有差别的,从第(2)点可以看出,由于需要用 exports.default 承载 ESM 的默认导出,ESM export 时,会自动去掉 default,但是如果采用 ComonJS require 硬生生地导入。会有 default 残留。这也是为什么 Typesciprt 会有 import x = require('x') 写法,就是为了兼容引入两者。

      // module.js
      export default {}
        
      // index.js ESM 导入,node>=9 可以直接执行index.js,也可以执行babel后的index.js   
      import m from './module.js';
      console.log(m); // {}
      // index.js CommonJS 导入,node 直接执行当然会报错,但可以执行babel后的index.js 
      const m = require('./module.js');
      console.log(m); // {default:{}}
      

      (4)void 0 是一个函数,它的值是 undefined,因此 exports.default = exports.count = undefined

    3.2.2 两者区别

    • CommonJS 模块输出的是一个值的拷贝,ESM 模块输出的是值的引用

      其实就把 commomJS 和 ESM 当做普通的看待就行了,所谓的 commomJS 值传递是因为 exports 和对应的 value 之间的关系就是一个普通对象赋值关系,该怎样还是怎样,而 ESM 的所谓引用传递是因为它不仅获得了值,它还把对应的引用就是那些 let var const 和变量也传出去了,这也从他的 babel 可以看出,如下代码,从这个角度出发 export default 并没有引用传递能力,譬如此@错误用例,下面 exports.count = count = count + 1 语句可以看出 add 被调用时,不仅改变外面的 count 也改变了自身的 count,然而 babel 模拟存在@局限性

      ESMBabel
      export let count = 1;
      export function add() {
      count++;
      }
      export function get() {
      return count;
      }
      export default {};
      Object.defineProperty(exports, "__esModule", {
      value: true
      });
      exports.default = exports.count = void 0;

      var count = 1;
      exports.count = count;

      exports.add = add;
      function add() {
      exports.count = count = count + 1; // 重点语句
      }

      exports.get = get;
      function get() {
      return count;
      }

      var _default = {};
      exports.default = _default;
    • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口

      (1)CommonJS、AMD、CMD 运行时加载:都是加载到内存中,通常为单例模式,只能在运行时确定这些东西,也就是说代码执行到对应的 require 会加载文件

      // CommonJS模块
      let { stat, exists, readFile } = require('fs');
        
      // 等同于
      let _fs = require('fs');
      let stat = _fs.stat;
      let exists = _fs.exists;
      let readfile = _fs.readfile;
      

      **解析:**上面代码的实质是整体加载 fs 模块(即加载 fs 的所有方法),生成一个对象(_fs),然后再从这个对象上面读取 3 个方法。这种加载称为 “运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。

      (2)ESM 编译时加载:ESM 不是对象,而是通过 export 命令显式指定输出的代码,再通过 import 命令输入,它是 JS 引擎做的工作。

      import { stat, exists, readFile } from 'fs';
      

      **解析:**上面代码的实质是从 fs 模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高,因此 import/export 必须位于顶层作用域,而且还可以做@树摇的原因

    • CommonJS 模块的 require 是同步加载模块,ES6 模块的 import 命令是异步加载,有一个独立的模块依赖的解析阶段

    4. 不同环境使用模块化对比

    4.1 Node 如何处理模块?

    • **标准:**CommonJS、ESM;前者是 Node 的官方标准,后者是 JS 官方标准,需要兼容实现

    • **使用方法:**与 package.json type 字段有关,官方推荐包开发者明确指定 type 字段值

      • 无论 type 是何值,.mjs 文件按 ESM 模块处理,.cjs 文件按照 CommonJS 模块处理
      • type 值为 CommonJS 或没有定义,.js 文件按照 CommonJS 模块处理
      • type 值为 module,.js 文件按照 ESM 模块处理,并且强制默认使用严格模式
    • **混用:**按照第二点后的 CommonJS 模块或 ESM 模块处理的文件随意混用导入导出时,会出现报错。

      • **CommonJS 文件中加载 ESM 模块:**由于 CommonJS 是运行时加载,因此只能使用 import()

        // main.cjs 文件
        (async ()=>{
        	await import('./module.mjs')
        })()
        
      • **ESM文件中加载 CommonJS 模块:**由于 CommonJS 是整体加载,因此不能只加载单一项

        // main.mjs 文件
        // 正确
        import module from './module.cjs'
        const { run } = module
        // 报错
        import { run } from './module.cjs'
        

    4.2 游览器如何处理模块?

    借助打包器如 rollup、Browserify 打包出各种适合游览器加载的 dist 文件,可参考 @vue各个打包版本

    4.3 webpack 如何处理模块?

    不同于 rollup 只是打包出不同环境下使用的单纯的 js 文件,webpack 是 web 的整体解决方案,可以打包出完整网站目录,可以对 html、css 文件做各种操作

    参考文章

    1. CommonJS规范-阮一峰

    2. Node CommonJS模块源码解析

    3. 前端模块化详解(完整版)

    4. CommonJS 与 esm 的区别

    5. node.js 如何处理模块


    起源地下载网 » 前端模块化的理解以及主要区别

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元