初涉模块化

作者: DHFE | 来源:发表于2018-07-26 00:52 被阅读37次

    早期的JavaScript发展初期只是为了少量的页面交互逻辑,且功能(逻辑)简单,代码量少,甚至于早期的Web是没有前端这个说法的,后端顺便一写JS。
    随着时间发展,进入Web2.0时代,CPU等硬件性能的提升也使得浏览器性能得到了提升,很多页面交互逻辑迁移到了客户端(浏览器),加上新技术不断涌现(Ajax),JQuery等前端库层出不穷,代码量日益膨胀。

    这时候JS作为动态语言的定位就显得捉襟见肘,没有类的概念,没有模块,简单的代码组织不足以驾驭如此大规模的代码。

    模块

    从最简单的开始:

    function step1() {
    
    };
    function step2() {
    
    };
    step1();
    step2();
    

    这是上古时期的JS书写方式,亦或者初学JavaScript的新手书写JS的方法,当然也属于面向过程式的。

    当逻辑交互增多,一个JS文件显然不够了,需要引入多个JS文件,并且这种书写的JS问题也很明显。

    1. 函数都是在global下定义,别人可以随意的修改操控这些全局函数,污染了全局变量
    2. 如果有人要是在另一个文件的人也定义了一个step函数会如何?会发生命名冲突

    为了解决如上问题,对象的写法应运而生,可以把所有的模块成员封装在一个对象中。

    var Moudle = {
        value1: 1,
        value2: 2,
        method1: function() {
            /*do something*/
        },
        method2: function() {
            /*do somethinlg*/
        }
    }
    

    调用时,只要保证模块名唯一即可。
    当然,这并没有从根本上解决这个问题,外部依然可以随意修改内部成员。

    Moudle.value1 = 10;
    

    这样会产生安全问题。


    后来又有人使用立即执行的函数表达式,也叫IIFE模式。

    var Moudle = (function() {
        var a = 1;
        var b = 2;
        function method1() {
            console.log(a);
        };
        function method2() {
            console.log(b);
        };
    
        return {
            method1: method1,
            method2: method2
        }
    })();
    
    Moudle.method1();       // 1
    Moudle.method2();       // 2
    

    这样在模块外部无法修改我们没有暴露出来的变量、函数。
    上述做法就是我们模块化的基础


    AMD/CMD/CommonJS是什么?

    CommonJS / Node.js

    2009年,美国程序员Ryan Dahl创造了node.js项目,将javascript语言用于服务器端编程。这标志"Javascript模块化编程"正式诞生。
    为什么JS可以在服务器端运行就标志着模块化编程到来呢?因为,在浏览器下,没有模块也不是很致命的问题,毕竟网页程序的复杂度有限。但对于服务端就不一样了,如果没有模块与操作系统底层或者其他应用程序互动,根本无法编程。

    Node.js是CommonJS规范的实现

    node.js的模块系统,就是参照CommonJS规范实现的。在CommonJS中,有一个全局性方法require(),用于加载模块。

    1. 根据CommonJS规范,一个单独的文件就是一个模块。每一个模块都是一个单独的作用域,也就是说,在该模块内部定义的变量,无法被其他模块获取,除非定义为global对象的属性。
    2. 模块输出:模块只有一个出口,module.exports对象,我们需要把模块希望输出的内容放入对象。
    3. 加载模块:加载模块使用require()方法,该方法获取一个文件并执行,返回文件内部的module.exports对象。

    模块定义a.js

    var myModule = {
        a: 10,
        sayA: function() {
            return this.a;
        }
    }
    module.exports = myModule;
    

    加载模块

    var myModule = require("./a.js");
    console.log(myModule.sayA());    // 10
    

    CommonJS定义的模块分为:{模块引用(require)} {模块定义(exports)} {模块标识(module)}
    require()用来引入外部模块;exports对象用于导出当前模块的方法或变量,唯一的导出口;module对象就代表模块本身。


    AMD / require.js
    背景

    在CommonJS规范的Node.js诞生后,服务端的模块化概念已经形成,很自然的,大家就想要客户端(浏览器)模块。而且最好两者都兼容,一个模块不用改,在两端都可以运行。
    但是有一个缺陷,使得CommonJS规范不适用于浏览器环境。

    var myModule = require("./a.js");
    console.log(myModule.sayA());    // 10
    

    第二行的sayA()方法,在第一行的a模块之后运行,必须得等到a.js加载完成后才能使用方法,如果无法加载或者加载没有完成那就会造成阻塞,直到a.js加载完成后续的代码才会执行。即:它们是同步的

    同步加载对于服务器端不是难事,因为资源都存放在服务器端,即用即取,完全可以同步加载,等待的时间就是服务器的硬盘读取时间,这个时间肯定比浏览器快的多。
    受到网速限制,如果等待很长时间都未能加载,页面就会“假死”,这对用户来说是相当不友好的。

    因此,基于这样的特殊背景,浏览器端的模块,不能采用同步加载,只能采用异步加载,这就是AMD规范的诞生。

    require.js是AMD规范的实现

    AMD介绍

    AMD(Asynchronous Module Definition),中文名异步模块定义,是一个在浏览器端模块化开发的规范。

    由于不是JavaScript原生支持,使用AMD规范进行页面开发需要用到对应的库函数,也就是大名鼎鼎的RequireJS,实际上AMD是requireJS在推广过程中对模块定义的规范化的产出。

    requireJS解决了什么问题呢?

    • 实现JS文件的异步加载,避免网页失去相应。
    • 管理模块之间的依赖性,便于代码的编写和维护。
      <script src="1.js"></script>
      <script src="2.js"></script>
      <script src="3.js"></script>
      <script src="4.js"></script>
      <script src="5.js"></script>
      <script src="6.js"></script>
    

    这样的代码,很多人都应该写过,繁多复杂,并且,还要体现依赖性,如照这样写,说明1.js必定被2,3,4,5,6所依赖。如果依赖关系在复杂一点,可读性,维护将变得很差。

    还有一个缺点,就是script标签加载时的阻塞问题。

    使用require.js,首先需要引入它。
    requireJS

        <script src="./lib/require.js" defer async="true"></script>
    
    

    当然,加载这个文件也可能使网页失去响应,要么放在body下方;要么写上async属性表明这个文件需要异步加载,避免网页失去响应,IE不支持这个属性,所以写上defer。

    加载require.js之后,下一步就是加载自己的代码了,假定我们的代码文件存放在app下,那么写成这样就好了。

        <script src="./lib/require.js" defer async="true" data-main="./app/a.js"></script>
    
    

    data-main属性的作用是,指定网页程序的主模块,如上代码,就是app目录下的a.js。这个文件会第一个被require.js加载。由于require.js默认的文件后缀名的js,所以可以把main.js简写成main。

    主模块写法

    a.js,把它称为“主模块”,意思是整个网页的入口代码,类似C语言里的main()函数,所有代码都从这儿开始运行。

    怎么写a.js?

    如果你的a.js不依赖任何模块,那么直接写就行了。

    alert("加载成功!");
    

    但很显然存在模块,并且主模块a.js依赖于其他模块,比如你的tab模块、轮播图模块等等。
    这时就要用到AMD规范定义的require()函数。

    注意,这个不是CommonJS里的require函数了。

      require(['moduleA', 'moduleB', 'moduleC'], function (moduleA, moduleB, moduleC){
    
        // some code here
    
      });
    

    require()函数接受两个参数。

    • 第一个参数:表示所依赖的模块,例子中就是['moduleA','moduleB','moduleC'],即主模块依赖这三个模块;
    • 第二个参数:是一个回调函数,只有前面的所依赖的模块加载完成,回调函数才会被调用,加载的模块会以参数的形式传入函数,从而在回调函数中使用这些模块。

    require()异步加载moduleA,moduleB和moduleC,浏览器不会失去响应;它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题。

    注意: 默认情况下,require.js会假设这三个模块与主模块目录相同,关于目录指定,可以查看官方文档

    假设现在主模块依赖moduleA、moduleB、moduleC这三个模块,那么主模块可以这样写:


    require(["moduleA","moduleB","moduleC"],function(moduleA,moduleB,moduleC) {
        var sum = moduleA.str + moduleA.str + moduleC.str;
        console.log(sum);       // " module A module A module C"
    });
    
    

    在CommonJS中,模块的出口完全靠module.exports,在requireJS中,我们又如何定义模块呢?

    在之前的例子中,A,B,C三个模块存放在app目录下,require.js假定这三个模块与主模块(a.js)在同一个目录中,然后就可以自动加载他们了。

    RequireJS以一个相对于baseUrl的地址来加载所有的代码。 页面顶层<script>标签含有一个特殊的属性data-mainrequire.js使用它来启动脚本加载过程,而baseUrl一般设置到与该属性相一致的目录。

    因为我们显式指定了data-main属性,那么baseUrl就会与data-main属性所在目录一制,自动的从目录内加载文件。

    当然,我们也可以对模块的加载行为进行自定义。require.config()就写在主模块(a.js)的头部。参数就是一个对象,这个对象的paths属性指定在各个模块的加载路径。

    假设现在有两个依赖模块在lib目录下,那么我们可以在主模块头部直接指定每个模块的路径。

    require.config({
        paths: {
            "moduleA": "../lib/moduleA",
            "moduleB": "../lib/moduleB",
            "moduleC": "moduleC",
        }
    })
    
    require(["moduleA","moduleB","moduleC"],function(moduleA,moduleB,moduleC) {
        var sum = moduleA.str + moduleA.str + moduleC.str;
        console.log(sum);       // " module A module A module C"
    });
    
    

    注意,不要写成../lib/moduleA.js

    如果写成了,解析目录就会变成你的HTML页面所在的直接目录下。

    当然,你也可以指定主机名,也就是直接指定它的绝对URL。

      require.config({
    
        paths: {
    
          "jquery": "https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min"
    
        }
    
      });
    

    require.js要求,每个模块是一个单独的js文件。这样的话,如果加载多个模块,就会发出多次HTTP请求,会影响网页的加载速度。因此,require.js提供了一个优化工具,当模块部署完毕以后,可以用这个工具将多个模块合并在一个文件中,减少HTTP请求数。

    模块如何定义
    require.js加载的模块,采用AMD规范,也就是说,模块必须按照AMD的规范来写。

    模块必须采用特定的define()函数来定义,如果一个模块不依赖其他模块,那么直接定义了define()函数之中。

    比如我们新建一个math模块 —— math.js。

    define(function() {
        var add = function(x,y) {
            return x+y;
        };
        return {
            add: add
        };
    });
    

    加载方法:

    require(["math"],function(math) {
        console.log(math.add(1,1));     // 2
    });
    

    如果这个模块还依赖其他模块,那么define()函数的第一个参数,必须是一个数组,指明该模块的依赖性。

    // math.js
    define(["myLib"],function(myLib) {
        function foo() {
            console.log(myLib.num);
        }
        return {
            foo: foo
        }
    });
    

    主模块a.js依赖math模块,math模块依赖myLib模块
    AMD中文网

    CMD / Sea.js

    CMD(Common Module Definition通过模块定义),CMD规范是国内发展出来的,比如AMD有个requireJS,CMD有个SeaJS,SeaJS要解决的问题

    Sea.js推崇一个模块一个文件,遵循统一的写法。

    • 一个文件一个模块,所以经常就用文件名作为模块id
    • CMD推崇依赖就近,所以一般不在define的参数中写依赖,在factory中写。

    CMD是懒加载,虽然会一开始就并行加载js文件,但是不会执行,而是在需要的时候才执行

    define
    define(id?, deps?, factory);

    参数factory也有三个参数

    function(reuqire, exports, module);
    
    • require:一个方法,接受模块标识作为唯一参数,用来获取其他模块提供的接口。
    • exports:一个对象,用来向外提供模块接口。
    • module:上面存储了与当前模块相关联的一些属性和方法。
    // 定义模块  myModule.js
    define(function(require, exports, module) {
      var $ = require('jquery.js')
      $('div').addClass('active');
    });
    
    // 加载模块
    seajs.use(['myModule.js'], function(my){
    
    });
    

    总结:

    规范实现

    CommonJS / Node.js
    AMD / Require.js
    CMD / Sea.js

    针对端

    AMD / CMD主要针对浏览器端
    CommonJS主要针对服务端

    加载区别
    // CMD
    define(function (require, exports, module) {
        var a = require("./a");
        a.doSomething();
        var b = require("./b");
        b.doSomething();
    })
    
    // AMD
    define(["./a", "./b"], function (a, b) {
        a.doSomething();
        b.doSomething();
    })
    
    主要区别:执行时机 处理不同,注意不是加载的时机或者方式不同。

    CMD推崇依赖就近,AMD推崇依赖前置
    CMD是延迟执行,AMD是提前执行
    加载模块时,都是异步加载

    • AMD因为依赖前置,当所有模块加载好后,就会立即执行,进入require回调函数,执行主逻辑。因为异步原因,模块加载和执行不一定一致,如a模块和b模块,b模块先加载完成,那么会先执行b模块,但是,主逻辑一定是在所有依赖模块加载后才执行的。

    • CMD因为依赖就近,模块加载好后并不执行,只是下载而已,当所有依赖模块加载完成后进入主逻辑,遇到require语句时才会执行对应模块,这样模块执行顺序和书写顺序一致。


    js模块化编程之彻底弄懂CommonJS和AMD/CMD!
    AMD/CMD/CommonJs到底是什么?它们有什么区别?
    前端模块化开发的价值
    MODULE?
    JavaScript AMD 与 CMD 规范

    相关文章

      网友评论

        本文标题:初涉模块化

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