美文网首页前端进阶之路Web前端之路让前端飞
高级前端人员的进阶之路 - 模块化编程

高级前端人员的进阶之路 - 模块化编程

作者: 果汁凉茶丶 | 来源:发表于2017-11-19 20:13 被阅读66次

     Javascript不是一种模块化编程语言,它不支持""(class),更遑论"模块"(module),然而模块化编程,已经成为一个迫切的需求。

    * 模块化的写法

    基本写法

    一:原始写法

     模块就是实现特定功能的一组方法,只要把不同的函数(以及记录状态的变量)简单地放在一起,就算是一个模块。

    function f1() {...}
    function f2() {...}
    

     上面的函数f1()和f2(),组成一个模块。使用的时候,直接调用就行了。
     这种做法的缺点很明显:

    1. "污染"了全局变量;
    2. 无法保证不与其他模块发生变量名冲突;
    3. 而且模块成员之间看不出直接关系。

    二:对象写法

      为了解决上面的缺点,可以把模块写成一个对象,所有的模块成员都放到这个对象里面.

    var module = new Object {
      _count: 0;
      f1: function(){
        // some code
      }
      f2: function() {
        // some code
      }
    }
    

    这样,我们调用方式就变成了

    module.f1();
    

     这么写还有缺点:暴露了所有的模块成员,内部的状态可以被外部改变。比如

    module._count = 5;
    

    三:立即执行函数写法

     使用【立即执行函数】(Immediately-Invoked Function Expression,IIFE),可以达到不暴露成员变量的目的

    var module = (function() {
      var _count = 0;
      var f1 = function(){
        // some code
      };
      var f2 = function(){
        // some code
      };
      
      return {
        f1: f1,
        f2: f2
      }                    
    })();
    console.log(module._count);   // undefined
    

     这样,外部代码无法读取内部的_count变量

    放大模式

    如果一个模块很大,必须分成几个部分,或者一个模块需要继承另一个模块,这时就有必要采用"放大模式"(augmentation)。

     为module1模块添加了一个新方法m3(),然后返回新的module1模块

    var module = function(mod){
      var mod.f3 = function(){
        // some code
      }
      return mod;
    })(module);
    

     在浏览器环境中,模块的各个部分通常都是从网上获取的,有时无法知道哪个部分会先加载。如果采用上一节的写法,第一个执行的部分有可能加载一个不存在空对象,为了更严谨一点,这时就要采用"宽放大模式"。

    var module = (function(mod){
      // some code
      return mod;
    })(window.module || {});
    

     这样,立即执行函数"的参数就可以是空对象

    输入全局变量

    1. 独立性是模块的重要特点,模块内部最好不与程序的其他部分直接交互。
    2. 为了在模块内部调用全局变量,必须显式地将其他变量输入模块
    var module = (function($, YAHOO){
      // some code
    })(jQuery,YAHOO);
    

     上面的module1模块需要使用jQuery库和YUI库,就把这两个库(其实是两个模块)当作参数输入module1。这样做除了保证模块的独立性,还使得模块之间的依赖关系变得明显。

    * 模块化的规范

    为什么模块很重要?因为有了模块,我们就可以更方便地使用别人的代码,想要什么功能,就加载什么模块。但是,这样做有一个前提,那就是大家必须以同样的方式编写模块,否则你有你的写法,我有我的写法,岂不是乱了套!

    CommonJs规范

     2009年,美国程序员Ryan Dahl创造了 node.js 项目,将javascript语言用于服务器端编程。这标志"Javascript模块化编程"正式诞生.老实说,在浏览器环境下,没有模块也不是特别大的问题,毕竟网页程序的复杂性有限;但是在服务器端,一定要有模块,与操作系统和其他应用程序互动,否则根本没法编程。

     node.js的 模块系统 ,就是参照 CommonJS 规范实现的。在CommonJS中,有一个全局性方法require(),用于加载模块。假定有一个数学模块math.js,就可以像下面这样加载。

    var math = require('math');
    

    然后就可以调用模块定义的方法:

    math.add(2, 3);
    

    require()就是用于加载模块的。

    AMD规范

    AMD 是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义",它采用异步方式加载模块。
     模块的加载不影响它后面语句的运行。我们将“后面的语句”分两部分:

    1. 与该模块无关的语句,受异步模块定义,不受模块加载的影响;
    2. 所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

    AMD采用 require() 加载模块,他有两个参数:

    require([modules], callback);
    

    第一个参数 [modules] 是一个数组,里面的成员是要加载的模块,第二个参数是callback, 表示加载之后的回调函数
    所以,我们的模块化代码书写方式按照AMD规范就变成如下形式:

    require([math], function(math){
      math.add(2, 3)
    })
    

     math.add()与math模块加载不是同步的,浏览器不会发生假死。所以很显然,AMD比较适合浏览器环境

    * 模块化的使用 - require.js

    一: 为什么要用 require.js

     最早的时候,所有Javascript代码都写在一个文件里面,只要加载这一个文件就够了。后来,代码越来越多,一个文件不够了,必须分成多个文件,依次加载。相信很多小伙伴都这么写过:

    <html>
      <head>
      ...
      </head>
      <body>
      ...
        <script src="jquery.js"></script>
        <script src="base.js"></script>
        <script src="core.js"></script>
        <script src="bootstrap.js"></script>
        <script src="component.js"></script>
        <script src="dialog.js"></script>
        <script src="event.js"></script>
      </body>
    </html>
    

    这段代码依次加载多个js文件。这样的写法有很大的缺点:

    1. 加载的时候,浏览器会停止网页渲染,加载文件越多,网页失去响应的时间就会越长;
    2. 由于js文件之间存在依赖关系,因此必须严格保证加载顺序(比如上例的jquery.js要在bootstrap.js的前面),依赖性最大的模块一定要放到最后加载,当依赖关系很复杂的时候,代码的编写和维护都会变得困难。

    模块化 require.Js的诞生就是为了解决这两个问题:

    1. 实现js文件的异步加载,避免浏览器假死
    2. 管理模块之间的依赖,便于模块的维护

    二: require.js 的加载

      在官网下载最新版本,将其放在js子目录下,index.html文件引入

    <script src="js/require.js"></script>
    

    加载这个问价同样会面临页面假死的现象,所以,我们通常这么做:

    1. 将加载语句放在网页(<body>)后面加载;
    2. 增加 defer anysc="true" 属性,如下
    <script src="js/require.js" defer async="true"></script>
    

    async属性表明这个文件需要异步加载,避免网页失去响应。IE不支持这个属性,只支持defer,所以把defer也写上。

    三:主模块的加载与写法

     require.js已经加载完成,下一步就是要加载我们自己的代码。假如文件为main.js,存放在js子目录下,我们用data-main属性引入:

    <script src="js/require.js" data-main="js/main"></script>
    

     这里的 main.js,我们称之为“主模块”;意思就是整个网页的入口,它有点像C语言的main()函数,所有代码都从这儿开始运行。
     如果我们的代码不依赖任何其他模块,那么可以直接写入javascript代码。

    // main.js
    alert("load success");
    

     但如果真这样,就没必要使用require.js了。真正常见的情况是,主模块依赖于其他模块,这时就要使用AMD规范定义的的require()函数。试写一个如下:

    // main.js
    require(['moduleA', 'moduleB', 'moduleC'], function(moduleA, moduleB, moduleC){
      // some code
    })
    

     这样,在主模块main.js中,依赖了三个模块 moduleA,moduleB,moduleC当这些模块加载完成之后,调起回调函数,我们将这些模块以参数的形式传入回调函数,从而在回调函数中就可以使用这些模块了。
    require()异步加载moduleA,moduleBmoduleC,浏览器不会失去响应;它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题。

    一个实例
     假定主模块依赖jqueryunderscorebackbone这三个模块,main.js就可以这样写:

    // main.js
    require(['jquery', 'underscore', 'backbone'], function($, _, Backbone){
      // some code
    })
    

    require.js会先加载jQuery、underscorebackbone,然后再运行回调函数。主模块的代码就写在回调函数中。

    四:模块的加载

    1. 默认加载行为

     我们引入jqueryunderscorebackbone之后,require.js会默认为在与main.js的同级目录下有jquery.jsunderscore.jsbackbone.js这几个文件,然后自动加载。如果找不到,就会加载失败,引发异常。然而我们常引入且使用的是.min.js文件,那么就需要我们自定义。

    2. 自定义加载行为

     使用require.config()方法,我们可以对模块的加载行为进行自定义。require.config()就写在主模块(main.js)的头部。参数就是一个对象,这个对象的paths属性指定各个模块的加载路径。另外,require.config() 提供了baseUrl来索引文件,默认的就是main.js所在路径。

    1. 默认以main.js的相对路径baseUrl加载
    // main.js 头部
    require.config({
      paths: {
        "jquery": "juery.min",
        "underscore": "lib/underscore.min",
        "backbone": "lib/backbone.min"
      }
    
    });
    

     在上方的代码中,jquery.min,js的路径与main.js在同一个目录(js子目录),underscore.min.jsbackbone.min.js则在js目录的子目录lib中。

    1. 直接修改基目录baseUrl
    // main.js 头部
    require.config({
      baseUrl: "js/lib",
      paths: {
        "jquery": "../juery.min",
        "underscore": "underscore.min",
        "backbone": "backbone.min"
      }
    
    });
    
    1. 如果某个模块在另一台主机上,也可以直接指定它的网址
    // main.js 头部
    require.config({
      paths: {
        "jquery": "https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min"
      }
    })
    

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

    五:AMD模块的写法

     require.js加载的模块,采用AMD规范。具体来说,就是模块必须采用特定的define()函数来定义

    1. 新增一个模块,它不依赖其他模块,那么可以直接在define()函数中定义
    // math.js
    define(function(){
      var add = function(x, y){
        return x + y;
      }
      return {
        add: add
      };
    });
    

     使用加载方式如下:

    require(['math'],function(math){
      alert(math.add(1, 2));
    });
    
    1. 如果新增的模块需要依赖其他模块
    define(['myLib'], function(myLib){
      function f1(){
        myLib.doSomething();
      }
      
      return {
        f1: f1
      }
    })
    

     当require()函数加上上面这个模块的时候,就会先加载myLib.js文件

    六:加载非规范的模块

     理论上,require.js加载的模块,必须是按照AMD规范、用define()函数定义的模块,但是实际上,虽然已经有一部分流行的函数库(比如jQuery)符合AMD规范,更多的库并不符合。
     加载这样的库,需要先用require.config()方法定义它们的一些特性。在本文中提到的underscorebackbone这两个库,就没有采用AMD来编写,使用时我们必须先定义它们的特性:

    require.config({
      shim: {
        'underscore': {
          exports: '_'
        },
    
        'backbone': {
          deps: ['underscore', 'jquery'],
          exports: 'Backbone'
        }
      }
    })
    

    require.config()接受一个配置对象,这个对象除了有前面说过的paths属性之外,还有一个shim属性,专门用来配置不兼容的模块

    每个模块都至少要定义:

    1. exports值(输出的变量名),定义这个模块外部调用时的名称;
    2. deps数组,表明该模块的依赖列表。 当然,如果没有依赖可以省略。

    如下,我们可以这样定义一个jquery的依赖模块

    require.config({
      shim: {
        'jquery.scroll': {
          deps: ['jquery'],
          exports: 'jQuery.fn.scroll'  
        }
      }
    })
    

    七:require.js的插件

    require.js还提供一系列 插件,实现一些特定的功能。
     例如:domready插件,可以让回调函数在页面DOM结构加载完成后再运行。

    require(['domready!'], function(doc){
      // called once the DOM is ready
    })
    

    textimage插件,则是允许require.js加载文本和图片文件。

    require(['text!review.txt', 'image!cat.jpg'], function(review, cat){
      console.log(review);
      document.body.appendChild(cat);
    })
    

    类似的插件还有jsonmdown,用于加载json文件和markdown文件。

    相关文章

      网友评论

        本文标题:高级前端人员的进阶之路 - 模块化编程

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