热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

javascript模块化开发之路

问题的由来万维网诞生之初,只有HTML(超文本标记语言),为了增加一些动态能力,1995年由Netscape公

问题的由来


万维网诞生之初,只有HTML(超文本标记语言),为了增加一些动态能力,1995年由Netscape公司的Brendan Eich设计了Javascript。这种小脚本语言很快流行起来,实现登录验证、显示动态内容等功能,与浏览器的紧密结合使它成为网页的“默认编程语言”。这时的很多网页都嵌入Javascript,或者使用



这样的方式引入外部js文件。

随着网站逐渐变成"互联网应用程序",嵌入网页的Javascript代码越来越庞大,越来越复杂。外部js文件也越来越多,诞生了很多库和框架,相互间还有依赖关系和加载顺序的要求。网页越来越像桌面程序,以Javascript代码为主题,网页只是展现模板,越来越需要团队分工协作、进度管理、单元测试等等......开发者不得不使用软件工程的方法,管理网页的业务逻辑。

Javascript模块化编程,已经成为一个迫切的需求。理想情况下,开发者只需要实现核心的业务逻辑,其他都可以加载别人已经写好的模块。





模块化的初步尝试


用过python的人知道python有很好的模块系统,一句import语句就可以导入任何标准模块。但是,Javascript不是一种模块化编程语言,它不支持"类"(class),更遑论"模块"(module)了。(正在制定中的ECMAScript标准第六版,将正式支持"类"和"模块",但还需要很长时间才能投入实用。)
Javascript社区做了很多努力,通过写法上的约定,在现有的运行环境中,实现"模块"的效果。


一、原始写法
模块就是实现特定功能的一组方法。我们能想到的最简单的写法,就是把程序的功能分解成各个函数。
只要把不同的函数(以及记录状态的变量)简单地放在一起,就算是一个模块。
  function m1(){
    //...
  }
  function m2(){
    //...
  }
上面的函数m1()和m2(),组成一个模块。使用的时候,直接调用就行了。
这种做法的缺点很明显:"污染"了全局变量,无法保证不与其他模块发生变量名冲突,而且模块成员之间看不出直接关系。
二、对象写法
为了解决上面的缺点,可以把模块写成一个对象,所有的模块成员都放到这个对象里面。
  var module1 = new Object({
    _count : 0,
    m1 : function (){
      //...
    },
    m2 : function (){
      //...
    }
  });
上面的函数m1()和m2(),都封装在module1对象里。使用的时候,就是调用这个对象的属性。
  module1.m1();

三、立即执行函数写法
使用"立即执行函数"(Immediately-Invoked Function Expression,IIFE),可以达到不暴露私有成员的目的。
  var module1 = (function(){
    var _count = 0;
    var m1 = function(){
      //...
    };
    var m2 = function(){
      //...
    };
    return {
      m1 : m1,
      m2 : m2
    };
  })();
使用上面的写法,外部代码无法读取内部的_count变量。
  console.info(module1._count); //undefined
module1就是Javascript模块的基本写法。下面,再对这种写法进行加工。


四、放大模式
如果一个模块很大,必须分成几个部分,或者一个模块需要继承另一个模块,这时就有必要采用"放大模式"(augmentation)。
  var module1 = (function (mod){
    mod.m3 = function () {
      //...
    };
    return mod;
  })(module1);
上面的代码为module1模块添加了一个新方法m3(),然后返回新的module1模块。


五、宽放大模式(Loose augmentation)
在浏览器环境中,模块的各个部分通常都是从网上获取的,有时无法知道哪个部分会先加载。如果采用上一节的写法,第一个执行的部分有可能加载一个不存在空对象,这时就要采用"宽放大模式"。
  var module1 = ( function (mod){
    //...
    return mod;
  })(window.module1 || {});
与"放大模式"相比,"宽放大模式"就是"立即执行函数"的参数可以是空对象。
六、输入全局变量
独立性是模块的重要特点,模块内部最好不与程序的其他部分直接交互。
为了在模块内部调用全局变量,必须显式地将其他变量输入模块。
  var module1 = (function ($, YAHOO) {
    //...
  })(jQuery, YAHOO);
上面的module1模块需要使用jQuery库和YUI库,就把这两个库(其实是两个模块)当作参数输入module1。这样做除了保证模块的独立性,还使得模块之间的依赖关系变得明显。这方面更多的讨论,参见Ben Cherry的著名文章《Javascript Module Pattern: In-Depth》。




模块和规范


什么是模块?
模块是一段 Javascript 代码,具有统一的基本书写格式。
模块之间通过基本交互规则,能彼此引用,协同工作。
把上面两点中提及的基本书写格式和基本交互规则描述清楚,就能构建出一个模块系统。对书写格式和交互规则的详细描述,就是模块定义规范(Module Definition Specification)。
为什么模块很重要?
因为有了模块,我们就可以更方便地使用别人的代码,想要什么功能,就加载什么模块。
但是,这样做有一个前提,那就是大家必须以同样的方式编写模块,否则你有你的写法,我有我的写法,岂不是乱了套!考虑到Javascript模块现在还没有官方规范,这一点就更重要了。
比如 CommonJS 社区的 Modules 1.1.1 规范,以及 NodeJS 的 Modules 规范,还有 RequireJS 提出的 AMD 规范等等。目前,通行的Javascript模块规范有有三大阵营:CommonJS、AMD、CMD,对应的模块实现分别是node.js,Requre.js和Sea.js。





CommonJS模块规范及其NodeJS实现


CommonJS规范

2009年,美国程序员Ryan Dahl创造了node.js项目,将Javascript语言用于服务器端编程。


这标志"Javascript模块化编程"正式诞生。因为老实说,在浏览器环境下,没有模块也不是特别大的问题,毕竟网页程序的复杂性有限;但是在服务器端,一定要有模块,与操作系统和其他应用程序互动,否则根本没法编程。

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

  var math = require('math');

然后,就可以调用模块提供的方法: 

 var math = require('math');

  math.add(2,3); // 5

是不是很简单?这个Require方法是一个函数,它用来识别外部模块(其实就是对象)的ID并返回其暴露的API。如果你要做的项目全部JS库都在本地,用node.js的模块系统就足够了。

当有了CommonJS(http://www.commonjs.org)规范的出现,其目标是为了构建Javascript在包括Web服务器,桌面,命令行工具,及浏览器方面的生态系统。


CommonJS制定了解决这些问题的一些规范,而Node.js就是这些规范的一种实现。Node.js自身实现了require方法作为其引入模块的方法,同时NPM也基于CommonJS定义的包规范,实现了依赖管理和模块自动安装等功能。这里我们将深入一下Node.js的require机制和NPM基于包规范的应用。


Node.js的模块用法

一,Node.JS模块包管理:


1.编写及加载使用模块


在Node.JS中编写及安装一个js的模块是很简单的。


首先写编写一个test.js,内容如下:


exports.myfunc= function () {

  console.log('hello,world');


};


也可以写成(推荐这种写法):


var myfunc = function () {


  console.log('hello,world');


};


exports.myfunc = myfunc;


保存这个文件就可以在你的别的js代码里调用了,在这里我写一个app.js内容如下:


var test = require('./test.js');

console.log( 'test my function:\n ' + test.myfunc());


在node.js的命令行中运行:


node app.js


你会发现打印出:


test my function:


hello world


分析一下上述的行为,在你用node app.js时,有如下后台动作:


首先node 用module对象来加载你的app.js.


然后会把你的js内容进行封装调用。原来app.js内从变成如下内容:


(function (exports, require, module, __filename, __dirname) {


   var test = require('./test.js');

console.log( 'test my function:\n ' + test.myfunc());


})


再进行执行。在执行到require('./test.js')时,会按着路径进行搜索test.js


而其实这个require方法实际调用的就是module对象的load方法。load方法在载入、编译、缓存了module后,返回module的exports对象。这就是test.js文件中只有定义在exports对象上的方法才能被外部调用的原因。


其实require加载模块有一个复杂的逻辑。


这里简单说一下,


第一种情况。当require中的参数为没有路径,没有文件后缀的一个字符串时比如require('httpd');


Node.JS搜索顺序如下:


a.从缓存中加载。b.从原生模块加载.c.从文件加载。从文件加载也有是很一个顺序:


Node.JS运行一个js程序比如刚才通过node app.js时,module加载时会形成一个当前的有效路径,这是一个路径集合。


首先是当前路径,然后是当前路径的父目录,再是父目录的父目录,只到根目录。


搜索模块首先从当前路径中的node_modules子目录去寻找,找不到就找这个目录中的package.json文件中的main参数指定的目录,然后就是父目录的node_modules子目录,一直找到根目录,再没找到。就找NODE_PATH中设定的目录下去寻找。


第二种情况:require参数是相对或绝对路径下的模块名比如 require('./test.js') 或者require('/home/joezhang/nodejs/test')这个不用搜索直接加载,因为有绝对路径麻:)


浏览器环境、AMD规范和Require.js


有了服务器端模块以后,很自然地,大家就想要客户端模块。而且最好两者能够兼容,一个模块不用修改,在服务器和浏览器都可以运行。

但是,由于一个重大的局限,使得CommonJS规范不适用于浏览器环境。还是上一节的代码,如果在浏览器中运行,会有一个很大的问题,你能看出来吗?

  var math = require('math');

  math.add(2, 3);

第二行math.add(2, 3),在第一行require('math')之后运行,因此必须等math.js加载完成。也就是说,如果加载时间很长,整个应用就会停在那里等。

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

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


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

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

  require([module], callback);

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

  require(['math'], function (math) {

    math.add(2, 3);

  });

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

目前,主要有两个Javascript库实现了AMD规范:require.js和curl.js。最火的是Require.js。

Require.js中可以使用如下方式加载模块:

  

其中,require.js是需要加载的require.js库,main是你自己的main.js文件,都放在js路径下。data-main属性的作用是,指定网页程序的主模块。在上例中,就是js目录下面的main.js,这个文件会第一个被require.js加载。由于require.js默认的文件后缀名是js,所以可以把main.js简写成main。

main.js怎么写呢?常见的情况是,主模块依赖于其他模块,这时就要使用AMD规范定义的的require()函数。

  // main.js

  require(['moduleA', 'moduleB', 'moduleC'], function (moduleA, moduleB, moduleC){

    // some code here

  });

require()函数接受两个参数。第一个参数是一个数组,表示所依赖的模块,上例就是['moduleA', 'moduleB', 'moduleC'],即主模块依赖这三个模块;第二个参数是一个回调函数,当前面指定的模块都加载成功后,它将被调用。加载的模块会以参数形式传入该函数,从而在回调函数内部就可以使用这些模块。

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


CMD规范优秀的模块加载器——SeaJS


CMD模块规范


Sea.js 是一个适用于 Web 浏览器端的模块加载器。在 Sea.js 里,一切皆是模块,所有模块协同构建成模块系统。Sea.js 基于CMD规范。

CMD规范是折中了CommonJS和AMD而发展出来的,它规定了模块系统的最基本约定:模块必须是单独的文件;模块中不能引入新的自由变量;模块的执行必须是调用才执行(lazy)。

在CMD规范中模块的基本写法是define(factory),factory可以是一个函数或一个对象。如果是函数(工厂函数),则头三个参数必须是require,exports,module。因此,多数情况下定义一个模块要写成

define(function(require, exports, module) {


// The module code goes here


});

require接受模块的名称,返回模块暴露的API,有require.async模式可用于异步加载,接受一系列模块名称和一个可选的回调函数。export对象可用于在模块执行时动态加载API,module参数可有三项内容:module.uri接受模块的路径,module.dependencies列出模块的依赖,module.exports暴露模块的接口(同上面的exports功能)。

实例如下:

A typical sample


//math.js

define(function(require, exports, module) {

exports.add = function() {

var sum = 0, i = 0, args = arguments, l = args.length;

while (i

sum += args[i++];

}

return sum;

};

});



//increment.js

define(function(require, exports, module) {

var add = require('math').add;

exports.increment = function(val) {

return add(val, 1);

};

});



//program.js

define(function(require, exports, module) {

var inc = require('increment').increment;

var a = 1;

inc(a); // 2


module.id == "program";

});

Wrapped modules with non-function factory


//object-data.js

define({

foo: "bar"

});

array-data.js


define([

'foo',

'bar'

]);

string-data.js


define('foo bar');


为什么是SeaJS

SeaJS 是一个国人开发的适用于浏览器端的 Javascript 模块加载器,是一个开源项目,目前由阿里、腾讯等公司共同维护。

相比与RequireJS还可以作为文件加载器。SeaJS 推荐用组合的思路解决问题:LABjs + SeaJS = Javascript 文件和模块加载器。

上面是 SeaJS 的狭义定位,SeaJS 还有一个广义定位:SeaJS 是浏览器端的 NodeJS.


RequireJS和SeaJS两者的区别如下:


定位有差异。RequireJS 想成为浏览器端的模块加载器,同时也想成为 Rhino / Node 等环境的模块加载器。SeaJS 则专注于 Web 浏览器端,同时通过 Node 扩展的方式可以很方便跑在 Node 服务器端。


遵循的规范不同。RequireJS 遵循的是 AMD(异步模块定义)规范,SeaJS 遵循的是 CMD (通用模块定义)规范。规范的不同,导致了两者 API 的不同。SeaJS 更简洁优雅,更贴近 CommonJS Modules/1.1 和 Node Modules 规范。


社区理念有差异。RequireJS 在尝试让第三方类库修改自身来支持 RequireJS,目前只有少数社区采纳。SeaJS 不强推,采用自主封装的方式来“海纳百川”,目前已有较成熟的封装策略。


代码质量有差异。RequireJS 是没有明显的 bug,SeaJS 是明显没有 bug。


对调试等的支持有差异。SeaJS 通过插件,可以实现 Fiddler 中自动映射的功能,还可以实现自动 combo 等功能,非常方便。RequireJS 无这方面的支持。


插件机制不同。RequireJS 采取的是在源码中预留接口的形式,源码中留有为插件而写的代码。SeaJS 采取的插件机制则与 Javascript 语言以及Node 的方式一致:开放自身,让插件开发者可直接访问或修改,从而非常灵活,可以实现各种类型的插件。


还有不少细节差异就不多说了。总之,SeaJS 从 API 到实现,都比 RequireJS 更简洁优雅。因此推荐采用SeaJS作为前端的模块加载器。




SeaJS的用法


首先在页面引入sea.js

然后写它的配置文件:

seajs.config({

base: "../module/",

alias: {

"jquery": "jquery/jquery/1.10.1/jquery.js"

}

});

这是为了配置基础路径,设置别名之类。

最后使用模块即可:

seajs.use("examples/hello/1.0.0/main");


官网seajs.org,里面说得非常详细,可下载示例研究。当然,所有模块的写法都要遵照上面CMD规范的要求。


这里提供一个不错的社区贡献的 API 文档:http://yslove.net/seajs/,列举了 Sea.js 的常用 API。只要掌握这些用法,就可以娴熟地进行模块化开发。


与Node.js的兼容

---------------

Sea.js 的初衷是为了让 CommonJS Modules/1.1 的模块能运行在浏览器端,但由于浏览器和服务器的实质差异,实际上这个梦无法完全达成,也没有必要去达成。更好的一种方式是,Sea.js 专注于 Web 浏览器端,CommonJS 则专注于服务器端,但两者有共通的部分。对于需要在两端都可以跑的模块,可以 有便捷的方案来快速迁移。


让 Sea.js 的模块跑在 Node 上


非常简单。首先需要安装 seajs 的 Node 模块:


$ npm install seajs -g

安装好后,在需要调用 Sea.js 模块的入口文件里,require 下 seajs :


a.js


define(function(require, exports) {

exports.name = 'A';

});


main.js


require('seajs');


var a = require('./a');

console.log(a.name);

这样就可以在 Node 环境中运行 Sea.js 的模块了:




推荐阅读
  • VScode格式化文档换行或不换行的设置方法
    本文介绍了在VScode中设置格式化文档换行或不换行的方法,包括使用插件和修改settings.json文件的内容。详细步骤为:找到settings.json文件,将其中的代码替换为指定的代码。 ... [详细]
  • Linux重启网络命令实例及关机和重启示例教程
    本文介绍了Linux系统中重启网络命令的实例,以及使用不同方式关机和重启系统的示例教程。包括使用图形界面和控制台访问系统的方法,以及使用shutdown命令进行系统关机和重启的句法和用法。 ... [详细]
  • Java实战之电影在线观看系统的实现
    本文介绍了Java实战之电影在线观看系统的实现过程。首先对项目进行了简述,然后展示了系统的效果图。接着介绍了系统的核心代码,包括后台用户管理控制器、电影管理控制器和前台电影控制器。最后对项目的环境配置和使用的技术进行了说明,包括JSP、Spring、SpringMVC、MyBatis、html、css、JavaScript、JQuery、Ajax、layui和maven等。 ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • Android Studio Bumblebee | 2021.1.1(大黄蜂版本使用介绍)
    本文介绍了Android Studio Bumblebee | 2021.1.1(大黄蜂版本)的使用方法和相关知识,包括Gradle的介绍、设备管理器的配置、无线调试、新版本问题等内容。同时还提供了更新版本的下载地址和启动页面截图。 ... [详细]
  • Voicewo在线语音识别转换jQuery插件的特点和示例
    本文介绍了一款名为Voicewo的在线语音识别转换jQuery插件,该插件具有快速、架构、风格、扩展和兼容等特点,适合在互联网应用中使用。同时还提供了一个快速示例供开发人员参考。 ... [详细]
  • 本文介绍了前端人员必须知道的三个问题,即前端都做哪些事、前端都需要哪些技术,以及前端的发展阶段。初级阶段包括HTML、CSS、JavaScript和jQuery的基础知识。进阶阶段涵盖了面向对象编程、响应式设计、Ajax、HTML5等新兴技术。高级阶段包括架构基础、模块化开发、预编译和前沿规范等内容。此外,还介绍了一些后端服务,如Node.js。 ... [详细]
  • 小程序自动授权和手动接入的方式及操作步骤
    本文介绍了小程序支持的两种接入方式:自动授权和手动接入,并详细说明了它们的操作步骤。同时还介绍了如何在两种方式之间切换,以及手动接入后如何下载代码包和提交审核。 ... [详细]
  • 使用eclipse创建一个Java项目的步骤
    本文介绍了使用eclipse创建一个Java项目的步骤,包括启动eclipse、选择New Project命令、在对话框中输入项目名称等。同时还介绍了Java Settings对话框中的一些选项,以及如何修改Java程序的输出目录。 ... [详细]
  • Java和JavaScript是什么关系?java跟javaScript都是编程语言,只是java跟javaScript没有什么太大关系,一个是脚本语言(前端语言),一个是面向对象 ... [详细]
  • 从零基础到精通的前台学习路线
    随着互联网的发展,前台开发工程师成为市场上非常抢手的人才。本文介绍了从零基础到精通前台开发的学习路线,包括学习HTML、CSS、JavaScript等基础知识和常用工具的使用。通过循序渐进的学习,可以掌握前台开发的基本技能,并有能力找到一份月薪8000以上的工作。 ... [详细]
  • 单页面应用 VS 多页面应用的区别和适用场景
    本文主要介绍了单页面应用(SPA)和多页面应用(MPA)的区别和适用场景。单页面应用只有一个主页面,所有内容都包含在主页面中,页面切换快但需要做相关的调优;多页面应用有多个独立的页面,每个页面都要加载相关资源,页面切换慢但适用于对SEO要求较高的应用。文章还提到了两者在资源加载、过渡动画、路由模式和数据传递方面的差异。 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • Python如何调用类里面的方法
    本文介绍了在Python中调用同一个类中的方法需要加上self参数,并且规范写法要求每个函数的第一个参数都为self。同时还介绍了如何调用另一个类中的方法。详细内容请阅读剩余部分。 ... [详细]
  • IjustinheritedsomewebpageswhichusesMooTools.IneverusedMooTools.NowIneedtoaddsomef ... [详细]
author-avatar
gauss
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有