美文网首页
彻底弄懂Module及其进化过程

彻底弄懂Module及其进化过程

作者: 大脸猫的前端之路 | 来源:发表于2019-12-30 22:44 被阅读0次

    ES6的出现,使得我们对Module的理解停留在exportimport的使用上,但整个JavaScript历史长河里,Module是怎样一个演变过程呢,下面就Module的进化过程做一个简单介绍,涉及的内容较多,每个模块涉入不深,供大家简单的了解。文中有什么说的不对的地方,欢迎提出指正。觉得内容还行的欢迎点赞,您的鼓励,是我前进的动力。

    模块化诞生的初衷:web系统的庞大、复杂、团队的分工协作,使得后期维护的成本越来越高,而模块化是用于保持代码块之间相互独立而普遍使用的设计模式。

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

    历史上,JavaScript一直没有模块体系,直到CommonJSAMD的出现,下面来看下在ES6之前,是怎样将大程序拆分成互相依赖的小文件的。

    模块化进化史

    1. 全局function模式 ----- 将不同的功能封装成不同的函数
    // module1.js
    let data = 'module1';
    function foo() {
        console.log(`foo():${data}`);
    }
    function bar() {
        console.log(`bar():${data}`);
    }
    
    // module2.js
    let data2 = 'module2';
    function foo() {  //与另一个模块中的函数冲突了
        console.log(`foo():${data2}`);
    }
    // index.html
    <body>
      <script type="text/javascript" src="module1.js"></script>
      <script type="text/javascript" src="module2.js"></script>
      <script type="text/javascript">
        foo(); // foo():module2    foo()函数值的输出与js文件的加载顺序有关
        bar(); // bar():module1
    </script>
    </body>
    

    缺点:global变量被污染,容易造成命名冲突。

    2.namespace模式 ----- 简单对象封装,减少了全局变量

    // module1.js
    let myModule1 = {
      data: 'module1.js',
      foo() {
        console.log(`foo() ${this.data}`)
      },
      bar() {
        console.log(`bar() ${this.data}`)
      }
    }
    
    // module2.js
    let myModule2 = {
      data: 'module2.js',
      foo() {
        console.log(`foo() ${this.data}`)
      },
      bar() {
        console.log(`bar() ${this.data}`)
      }
    }
    
    // index.html
    <body>
      <script type="text/javascript" src="module1.js"></script>
      <script type="text/javascript" src="module2.js"></script>
      <script type="text/javascript">
          myModule1.foo()
          myModule1.bar()
    
          myModule2.foo()
          myModule2.bar()
    
          myModule1.data = 'other data' //能直接修改模块内部的数据
          myModule1.foo();   //  输出other data
    </script>
    </body>
    

    缺点:模块数据缺乏独立性,外部可更改内部数据。

    3.IIFE模式(立即调用函数表达式)-----匿名函数自调用
    优点:数据是私有的,外部只能通过暴露的方法操作
    问题:如果当前模块依赖另一个模块怎么办?

     // module.js
        (function (window) {
            let data = 'module';   // 数据
            // 操作数据的函数
            function foo() { // 用于暴露有函数
                console.log(`foo():${data}`);
            }
            function bar() { // 用于暴露有函数
                console.log(`bar():${data}`)
                otherFun(); // 内部调用
            }
            function otherFun() { //内部私有的函数
                console.log('otherFun()');
            }
            //暴露行为
            window.myModule = {foo, bar}
        })(window)
    
    // index.html
    <body>
        <script type="text/javascript" src="module.js"></script>
        <script type="text/javascript">
            myModule.foo();             // foo():module
            myModule.bar();             // bar():module otherFun()
            //myModule.otherFun()       // TypeError:myModule.otherFun is not a function
            console.log(myModule.data); // undefined:不能访问模块内部数据
            myModule.data = 'xxxx';     // 不能修改的模块内部的data
            myModule.foo();             // foo():module,没有改变
        </script>
    </body>
    

    4.IIFE增强模式----引入依赖(现代模块化实现的基石)

    // module.js
        (function (window, $) {
            let data = 'NBA'; // 数据
            // 操作数据的函数
            function foo() { // 用于暴露有函数
                console.log(`foo():${data}`);
                $('body').css('background', 'red');
            }
            function bar() { // 用于暴露有函数
                console.log(`bar() ${data}`);
                otherFun(); // 内部调用
            }
            function otherFun() { // 内部私有的函数
                console.log('otherFun()');
            }
            // 暴露行为
            window.myModule = {foo, bar};
        })(window, jQuery)
    
    // index.html
    <body>
        // 引入的js必须有一定顺序
        <script type="text/javascript" src="jquery.js"></script>
        <script type="text/javascript" src="module.js"></script>
        <script type="text/javascript">
            myModule.foo()
        </script>
    </body>
    

    常见的模块化规范

    ES6之前,社区制定了一些模块加载方案,最主要的有CommonJSAMD两种。前者用于服务器,后者用于浏览器。由于ES6模块化的出现,CommonJSAMD规范渐渐很少被人使用。下面简单了解下两种规范如何使用。
    1.CommonJS----用于服务器端,模块的加载是运行时同步加载的

    • 定义暴露模块:exports
      (1) exports.xxx = value;
      (2) module.exports = value;
    • 引入模块:require
      (1) 第三方模块:var module = require('xxx模块名');
      (2) 自定义模块:var module = require('模块文件相对路径');

    2.AMD--- 专门用于浏览器端,模块的加载是异步的

    • 定义暴露模块:define()
      (1) 定义没有依赖的模块
    define(function() {
         // 代码块
        return 模块;
    })
    

    (2) 定义有依赖的模块

    define(['module1', 'module2'], function(m1,m2) {
            // 代码块
            return 模块;
    })
    
    • 引入使用的模块
    require(['module1', 'module2'], function(m1, m2) {
            // 使用模块1和模块2
     })
     // 或者使用requirejs引入模块
     requirejs(['module1', 'module2'], function(m1, m2) {
            // 使用模块1和模块2
     })
    

    ES6模块化

    ES6模块的设计思想是尽量静态化,使得在编译时就能确定模块之间的依赖关系,以及输入和输出的变量。ES6模块功能主要有两个命令构成:exportimport
    下面简单介绍下这两个命令的使用方式及注意事项,更多详细介绍请参考阮一峰Module的语法

    export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。
    ES6模块中默认采用严格模式,不管你头部有没有写"use strict"

    1.export
    导出export:作为一个模块,它可以选择性地给其它模块暴露(提供)自己的属性和方法,供其它模块使用。

    // profile.js
    // 写法一
    export var firstName = 'zxy';
    export var yesr = '2020';
    //写法二  推荐写法
    var firstName = 'zxy';
    var year = '2020';
    export {firstName, year}
    

    注意事项:

    • export命令除了输出变量,也可输出函数或类;
    • export输出的变量就是本来的名字,也可通过as关键字重命名;
    function v1() { ... }
    function v2() { ... }
    export {
      v1 as streamV1,
      v2 as streamV2,
      v2 as streamLatestVersion
    };
    
    • export语句输出的接口与其对应的值是动态绑定关系,即通过该接口可以取到模块内部实时的值。
    • export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错。import命令亦是如此。这是因为处于条件代码块之中,就没法做静态优化了,违背了ES6模块的设计初衷。
    export var foo = 'bar'; 
    setTimeout(() => foo = 'baz', 500); // 输出变量foo,值为bar,500ms之后变为baz
    
    function foo() {
        export default 'bar';  //  SyntaxError
    }
    

    2.import
    导入import:作为一个模块,可以根据需要,引入其它模块提供的属性或者方法,供自己模块使用。

    // main.js
    import { firstName, year } from './profile.js';
    function setName(element) {
      element.textContent = firstName ;
    }
    
    • 大括号中的变量名必须与被导入模块(profile.js)对外接口的名称相同,位置顺序无要求。
    • import后面的from指定模块文件的位置,可以是相对路径,也可以是绝对路径
    • 如果想为输入的变量重新取一个名字,要在import命令中使用as关键字,将输入的变量重命名。
    • import命令具有提升效果,会提升到整个模块的头部并首先执行。这种行为的本质是,import命令是编译阶段执行的,在代码运行之前。
    import {firstName as name} from './profile.js';
    
    foo();
    import {foo} from 'module';
    
    • 由于import是静态执行的,所以不能使用表达式和变量,只有在运行时才能得到结果的的语法结构。
    // 报错
    import { 'f' + 'oo' } from 'my_module';
    // 报错
    let module = 'my_module';
    import { foo } from module;
    // 报错
    if (x === 1) {
      import { foo } from 'module1';
    } else {
      import { foo } from 'module2';
    }
    
    • 如果多次重复执行同一句import语句,那么只会执行一次,而不会执行多次。
      import 'loadsh';
      import loadsh;
    • 导入不存在的变量,值为undefined
    // module1.js
    export var name = 'jack';  
    
    // module2.js
    import {height} from './module1.js';
    console.log(height); // 输出结果:undefined
    
    
    • 声明的变量,对外都是只读的。请注意下方解释
    // module1.js
    export var name = 'jack';  
    
    // module2.js
    import {name} from './module1.js';
    name="修改字符串变量";   // 亲试,不会报错,输出:修改字符串变量
    

    这里的对外只读并不是指import声明的变量都是只读的,引入后不可改写,而是当定义的接口或变量为只读时(通过Object.defineProperty()),import引入的变量就不可被更改。

    3.模块的整体加载

    • 除了指定加载某个输出值,还可以使用整体加载(即星号*)来指定一个对象,所有输出值都加载在这个对象上。
    //  moduleA.js
    var name='zxy';
    var age = '18';
    var say = function() {
     console.log('say hello');
    }
    export {name, age, say}
    
    // moduleB.js
    import * as obj from './moduleA.js';
    obj.name;   // zxy
    obj.age;   // 18
    obj.say();  // say hello
    

    4.export default命令
    export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default命令只能使用一次。所以import命令后面不用加大括号,因为只可能对应一个方法。

    // moduleA.js
    export default function() {
          console.log('zxy')
    }
    
    // moduleB.js
    import sayDefault from './moduleA.js';
    sayDefault();  // zxy
    
    • export default就是输出一个叫作default的变量或方法,然后系统允许我们为它取任意名字。使用as关键字。
    • 因为export default命令其实是输出一个叫default的变量,因此它后面不能跟变量声明语句。
    export var a = 1;  // 正确
    export default var a = 1;  // 错误
    var a = 1;  export default a;  // 正确
    

    同样地,因为export default命令的本质是将后面的值,赋给default变量,所以可以直接将一个值写在export default之后。

    export default 1;  // 正确
    export 1;   // 错误
    

    上面代码中,后一句报错是因为没有指定对外的接口,而前一句指定对外接口为default

    1. import()方法
      -import()函数用途完成动态加载。import()返回一个Promise对象。
    function initEntity() {
        return Promise.all([
            import('./A.js'),
            import('./B.js');  
    ]).then([transDefine] => {
            // code
    })
    
    • 按需加载
    btn.addEventListener('click', event =>{
            import('./dialogBox.js');
    }).then(dialogBox => {
            dialogBox.open();
    }).catch(error => {
          // code
    })
    
    • 条件加载
    if(condition) {
          import('moduleA');
    } else {
          import('moduleB');
    }
    

    相关文章

      网友评论

          本文标题:彻底弄懂Module及其进化过程

          本文链接:https://www.haomeiwen.com/subject/yeqmoctx.html