• javascript模块化编程思想、实现与规范


    随着BS架构的发展,网站逐渐变成了互联网应用程序,嵌入网络的JavaScript代码越来越庞大,越来越复杂(业务逻辑处理或用户交互很多写在前端)。网页越来越像桌面程序,需要一个团队分工协作、进度管理、单元测试等。。开发者不得不使用软件工程的方法,管理网页的业务逻辑。因此JavaScript模块化编程已经成了一个迫切的需求,理想的情况下是开发者只需要实现核心的业务逻辑,其他业务处理都可以加载别人已经写好的模块,做到明确分工而不会相互影响。

    但是,JavaScript却不是一种模块化编程语言,它不支持类(class),更别说模块(module)了。虽然ECMAScipt正在谋划支持和推广类和模块的概念,但要实际投入生产还是遥遥无期,只能自己另外想办法。为此JavaSript社区做了很多努力,努力在现有的运行环境中,利用现有的资源,实现模块化的效果。

    模块化的原始写法

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

    function f1() {
        // doSomething
    }
    
    function f2() {
        // doSomething
    }

    上面的函数f1()和f2()共同组成了一个模块,使用的时候直接通过函数名调用就行了。这种做法的缺点很明显,既污染了全局变量(f1和f2处在全局的上下文栈中,属于window的属性),也无法保证不与其他模块发生变量名冲突,而且模块成员之间看不出直接的关系。

    模块化的对象写法

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

    var module1 = {
        status: 233,
        
        f1: function() {
            // doSomething
        },
        
        f2: function() {
            // doSomething
        }
    }

    上面的函数f1()和函数f2()都封装在了module1对象里,使用的时候就是通过访问module1对象的属性。

    module1.f1();

    但是,这样的写法会暴露所有的模块成员,且内部的状态可以被外部改写。

    module1.status = 666;

    模块化的立即执行函数写法

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

    var module1 = (function() {
        var status = 233;
        
        var f1 = function() {
            // doSomething
        };
    
        var f2 = function() {
            // doSomething
        };
    })();

    使用这样的写法,外部的代码就无法读取到内部的变量。

    console.log(module1.status); // undefined

    这种写法,就是JavaScript模块化的基本写法,后面的实现基本上都是依照这个思路。

    模块化的放大模式

    如果一个模块很大,就会要拆分成几个小的模块,或者一个模块需要继承另一个模块,这个时候就要采用放大模式(Augmentation)。

    var module1 = (function(mod) {
        mod.f3 = function() {
            // doSomething
        };
    
        return mod;
    })(module1);

    这里为module1模块添加了一个新函数f3(),然后返回新的module1模块。也就是把旧的对象传进来,给这个对象添加属性,然后返回添加了属性后的对象,相当于扩展。

    模块化的宽放大模式

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

    var module1 = (function(mod) {
        // doSomething
    
        return mod;
    })(window.module1 || {});

    与放大模式相比,宽放大模式就是立即执行函数的参数可以是空对象。

    模块化的全局变量输入

    独立性是模块化的重要特点,模块内部最好不与程序的其他部分直接交互。

    为了在模块内部调用全局变量,必须显式地将其他变量输入模块。

    var module1 = (function($, YAHOO) {
        // doSomething
    })(jQuery, YAHOO);

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

    模块化的几种规范

    模块化的前提是要遵循同一套规范,否则模块之间的调用会十分困难。

    JavaScript官方没有模块化的规范,目前通用的民间规范主要有CommonJS(服务端js模块化的规范,NodeJS是这种规范的实现)、AMD(Asynchronous Module Definition异步模块定义,RequireJS遵循此规范)和CMD(Common Module Definition,通用模块定义,SeaJS遵循此规范)。

    模块化在服务端的规范:CommonJS

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

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

    var math = require('math');
    math.add(2, 3); // 5

    更多的用法这里就不说了,只需要知道CommonJS是使用require()函数加载模块就行了。

    模块化规范从服务端到客户端的发展

    有了服务端的模块化之后,大家就想要客户端的模块化了。而且最好两者能兼容,一个模块不用修改,在服务器和浏览器都可以运行。但是由于一个重大局限,使得CommonJS规范不适用于浏览器环境。这是因为,在上面的代码中,调用math的方法必须要在math.js加载完成。也就是说,如果加载时间很长,整个应用就会停在那里等。

    这对于服务器端不是一个问题,因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是对于浏览器来说,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,可能会导致浏览器处于假死状态。

    因此浏览器端的模块化不能使用同步加载(Synchronous),只能使用异步加载(Asynchronous)。这就是AMD规范诞生的背景。

    模块化在客户端的规范:AMD

    AMD(Asynchronous Module Definition,异步模块定义)采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,直到加载完成之后,这个回调函数才会运行。

    AMD也采用require()语句加载模块,不同于CommonJS的是,它要求两个参数:

    require([module], callback);

    第一个参数[moudle],是一个数组,里面的成员就是要加载的模块;第二个参数callback,则是加载成功之后的回调函数。如果将前面的代码改写成AMD形式,就是这样:

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

    这样,math.add()与math模块的加载就不是同步的,浏览器也不会发生假死的状况。所以很显然地是AMD比较适合浏览器环境。应用AMD规范的主要有require.js。

    模块化在客户端的规范:CMD

    CMD是SeaJS在推广过程中对模块定义的规范化产出。

    AMD和CMD的区别:

    1.对于依赖的模块,AMD是提前执行,CMD是延迟执行。CMD推崇的是as lazy as possible,即尽可能得懒加载(延迟加载),即在需要得时候才加载。

    2.CMD推崇依赖就近,AMD推崇依赖前置。

    // 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();
    }) 

    3.AMD的API默认是一个当多个用,CMD的API则是严格区分,推崇职责单一。比如在AMD里,require分全局和局部,而在CMD里则没有全局require,而是根据模块系统的完备性,提供seajs.use来实现模块系统的加载启动。CMD里,每个API都简单存粹。

    模块化的优点总结

    1.解决了命名的冲突问题。多人开发的场景下,容易出现命名冲突,模块化通过内部封装与外部隔离能有效防止命名冲突的问题。

    2.解决了文件的依赖问题,使文件易于管理。如果有很多js文件相互依赖,依赖关系和加载顺序都是让人头冷的问题。使用模块化就可以很好地实现依赖管理(使用依赖都要提前声明)。

    3.提高代码的可读性。各个模块各自完成自己的功能,专司其职,除了问题也会便于维护。

    4.提高代码的复用性。可以抽提特定的通用功能作为一个通用的模块。

    "可是怎么办,想起你的时候,心还是会疼。"

  • 相关阅读:
    CSS3弹性盒布局方式
    Vue知识点(面试常见点)
    h5新增加的存储方法
    前端常用插件
    Git 及 GitHub 使用
    Express 框架
    angular.js 教程 -- 实例讲解
    Windows右键添加VSCode启动
    Windows平台SSH登录Linux并使用图形化界面
    10_Linux yum命令
  • 原文地址:https://www.cnblogs.com/yanggb/p/10798646.html
Copyright © 2020-2023  润新知