js 的模块化

历史上,JavaScript一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。其他语言都有这项功能,比如 Ruby 的 require、Python 的 import ,甚至就连 CSS 都有 @import ,c 语言中的 include,但是 JavaScript 任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。

JavaScript 模块化开发

模块化介绍

当你的网站开发越来越复杂的时候,会经常遇到什么问题?

1、模块化思想可以让开发更高效
2、实现模块化需要解决一个任务,这个任务就是依赖关系
3、浏览器端 js 是天然不能实现模块
4、有一些库弥补了浏览器端 js 的一些缺陷,实现了模块化并解决了依赖关系

将这种库称为模块加载器,RequireJS、SeaJS

这些模块加载器定义了自己的规范,必须尊早这些规范才能正常工作。

以 SeaJS 为例子:

  • 通过 define() 方法来定义模块
  • 通过 use() 方法来加载/执行模块
  • 通过 require() 方法来引入模块
  • 通过 exports/module.exports 暴漏模块功能

Sea.js 可以解决命名空间污染、文件依赖的问题。

  • 模块的作用就是:私有空间
  • 可以加载,可以导出

  • 什么是模块化

    • 模块化是指解决一个复杂问题时自顶向下逐层把系统划分成若干模块的过程,有多种属性,分别反映其内部特性。
    • 解决复杂问题的一种方式而已
    • 电脑:CPU、主板、显示器、内存、硬盘、输入与输出设备
  • 使用模块化开发的方式带来的好处

    • 生产效率高
    • 可维护性高

模块化开发演变

全局函数

  • 污染了全局变量
  • 模块成员之间看不出直接关系

命名空间

  • 理论意义上减少了变量冲突
  • 缺点1:暴露了模块中所有的成员,内部状态可以被外部改写,不安全
  • 缺点2:命名空间会越来越长

私有空间

  • 私有空间的变量和函数不会影响全局作用域
  • 公开公有方法,隐藏私有属性

模块的维护和扩展

  • 开闭原则
  • 可维护性好

模块的第三方依赖

  • 保证模块的独立性
  • 模块之间的依赖关系变得明显

总结

以后如果不使用第三方规范的情况下,如果写模块可以采用下面这种方式:

1
2
3
4
5
6
7
8
// 1. 分号是什么意思
// 2. 为什么要给你的代码加一个匿名自执行函数
// 3. 为什么要把使用的依赖作为参数传递进来
;(function (形参模块名, 依赖项, 依赖项) {
// 通过 形参模块名 修改模块
// 如果需要,可以通过给 window 对象挂载属性对外暴露内部成员
window.模块名 = 形参模块名
})(window.模块名 || {}, 依赖项, 依赖项)

模块化规范

模块系统理解

自然界生态系统、计算机操作系统、软件办公系统,还有教育系统、金融系统、网络系统、理论系统等等。究竟什么是系统呢?

简单来说,系统有两个基本特性:

  1. 系统由个体组成
  2. 个体之间有关联,按照规则协同完成任务

系统之间的个体可以成为系统成员,要构建一个系统,最基本的层面需要做两件事:

  1. 定义系统成员:确定成员是什么
    • 模块是一个 JavaScript 文件
    • 每一个模块都使用 define 函数去定义
  2. 约定系统通讯:确定成员之间如何交互,遵循的规则是什么
    • 一个 SeaJS 模块默认就是私有作用域
    • 如果想要被外部文件模块所访问,就必须把要公开的属性挂载给 module.exports 对象接口
    • 使用 require 函数可以加载一个指定的模块,得到该模块代码中暴露的接口对象
  3. 如何启动整个模块系统
    • 在 html 页面中使用 seajs.use() 方法,指定一个入口文件模块

Sea.js 是一个适用于 Web 浏览器端的模块加载器。在 Sea.js 里,一切皆是模块,所有模块协同构建成模块系统。Sea.js 首要要解决的是模块系统的基本问题:

  1. 模块是什么?
  2. 模块之间如何交互?

在前端开发领域,一个模块,可以是JS 模块,也可以是 CSS 模块,或是 Template 等模块。而 Sea.js 则专注于 JS 文件模块:

  1. 模块是一段 JavaScript 代码,具有统一的 基本书写格式
  2. 模块之间通过基本 交互规则 ,能彼此引用,协同工作

把上面两点中提及的基本书写格式和基本交互规则描述清楚,就能构建出一个模块系统。对书写格式和交互规则的详细描述,就是模块定义规范(Module Definition Specification)。

比如 CommonJS 社区的 Modules 1.1.1 规范,以及 NodeJS 的 Modules 规范,还有 RequireJS 提出的 AMD 规范等等。

Sea.js 遵循的是 CMD 规范。

常见的 JavaScript 模块化规范

规范其实就是这些库在推广的过程中逐渐形成的一套规则。

所谓的规范也就是:

  • 定义了模块的书写格式
  • 以及模块之间的交互规则

  • Node 环境

    • CommonJS
    • 这里先放在这里,Node 还没有学习,学到 Node 的时候,再说这个问题
  • 浏览器环境
    • AMD
      • RequireJS
    • CMD Common Module Definition
      • CMD 就是 SeaJS 这个模块加载器在推广的过程中定义的一个模块规范
  • ECMAScript
    • ECMAScript 6
  • UMD

CMD、AMD、CommonJS 都是社区制定出来的模块规范,他们的目的都是为了解决 JavaScript 没有模块化系统的问题。他们都有如何定义模块成员,以及模块成员之间如何进行通信交互的规则。

2015 年 9 月份,ECMAScript 官方推出了 ECMAScript 6 语言标准。在最新的 ES6 语言规范标准中制定了 JavaScript 模块化规范,通过 exportimport 两个关键字来作为交互规则。

ES6 才是未来的趋势,以后的大一统。

前端发展非常快,不是说出了新技术马上就用,而是这个破玩儿还没发布正式版,都已经怼到生产环境了。所有任何功能,都可以使用 js 来实现。

  • electron
    • 使用 HTML+CSS+JavaScript+Node 构建跨平台桌面应用程序

SeaJS

A Module Loader for the Web, Enjoy the fun of programming.

  • 提供简单、极致的模块化开发体验
  • A Module Loader for the Web
  • JavaScript 模块加载器
  • 可以实现 在 JavaScript 代码中去加载另一个 JavaScript 代码。

SeaJS 介绍

SeaJS 带来的最大好处是:提升代码的可维护性。如果一个网站的 JS 文件超过 3 个,就适合用 SeaJS 来组织和维护代码。涉及的 JS 文件越多,SeaJS 就越适合。

  • 关于 SeaJS

    • SeaJS 是一个适用于浏览器环境的 JavaScript 模块加载器
      • 一个库文件,类似于 jQuery
      • 使用这个库提供的规范的模块化的方式来编写 JavaScript 代码
      • 只关心 JavaScript 文件代码模块如何组织
      • 只关心 JavaScript 文件之间如何相互协议、引用、依赖
    • SeaJS 的作者是阿里巴巴支付宝前端架构师:玉伯
    • SeaJS
    • SeaJS -github
  • 为什么学习和使用 SeaJS ?

    • 简单友好的模块定义规范:SeaJS 遵循 CMD 规范,可以像 Node 一样书写模块代码
    • 自然直观的代码组织方式:依赖的自动加载、配置简洁清晰,可以让我们更多的享受编码的乐趣
    • SeaJS兼容性非常好,几乎可以运行在任何浏览器引擎上
    • 注1:SeaJS 只是实现模块化开发的一种方式或者说一种工具而已,重在模块化思想的理解
    • 注2:因为 SeaJS 采用的 CMD 模块规范和 Node 中的 CommonJS 模块规范非常一致,所以有利于我们学习 Node 中的模块化编程
  • 谁在用?

    • 淘宝网、支付宝、京东、爱奇艺。。。
  • SeaJS 使用场景

    • SeaJS 不提供任何功能性 API,只提供了解决 JavaScript 代码的命名污染和文件依赖的问题
    • 所以 SeaJS 可以和 jQuery、underscore 等库结合使用
    • 例如 只写写 原生 JavaScript 或者用了一些第三方库

快速上手(Getting Started)

  1. 下载 sea.js 库文件
  2. 在页面中引入 sea.js
  3. 使用 define 函数定义模块
  4. 使用 require 函数加载模块
  5. 使用 module.exports 对外暴露接口对象
  6. 使用 seajs.use 函数启动模块系统

API 详解

seajs.use 加载模块-普通路径

加载模块,启动模块系统。

  • 加载一个模块 seajs.use('id')
  • 加载一个模块,在加载完成时,执行回调 seajs.use('id', callback)
  • 加载多个模块,加载完成时,执行回调 seajs.use(['id1','id2',...],callback)

  • 注意:

    • 在调用 seajs.use 之前,需要先引入 sea.js 文件
    • seajs.use 与 DOM ready 事件没有任何关系。如果某些操作要确保在 DOM ready 后执行,需要使用 jquery 等类库来保证
    • seajs.use 理论上只用于加载启动,不应该出现在 define 中的模块代码里

define(factory)

  • define 是一个全局函数,用来定义模块。
  • define 接受 factory 参数,factory 可以是一个函数,也可以是一个对象或字符串。
  • factory 为对象、字符串时,表示模块的接口就是该对象、字符串。
  • factory 是一个对象
    • define({})
  • factory 是一个字符串时
    • define('hello')
  • factory 是一个函数时
    • define(function(require, exports, module){})

require 解决依赖

相对路径:相对于当前模块来说的

  • require 用来加载一个 js 文件模块,相对路径:相对于当前模块来说的
  • require 用来获取指定模块的接口对象 module.exports
  • require 在加载和执行的时候,js 会按照同步的方式和执行。

使用注意:

  • 正确拼写
    • 模块 factory 构造方法的第一个参数 必须 命名为 require
  • 不要修改
    • 不要重命名 require 函数,或在任何作用域中给 require 重新赋值
  • 使用字符串直接量
    • require 的参数值 必须 是字符串直接量

模块标识

模块标识是一个字符串,用来标识模块。

  • 模块标识可以不包含文件后缀名,比如 .js
    • seajs 推荐不加 .js 文件模块后缀
  • 模块标识可以是 相对顶级 标识
  • 相对标识

相对标识以 . 开头,永远相对于当前模块所处的路径来解析。

  • 顶级标识

顶级标识不以 ./ 开始,会相对模块系统的基础路径(base路径,默认是 sea.js 文件所属的路径)。可以手动配置 base 路径。

1
2
3
seajs.config({
base: './js';
})
  • 普通路径(相对于 html 路径来说的)

除了相对和顶级标识之外的标识都是普通路径。普通路径的解析规则,会相对当前页面解析。

1
2
3
4
5
6
7
8
9
10
11
12
// 假设当前页面是 http://example.com/path/to/page/index.html
// 绝对路径是普通路径:
require.resolve('http://cdn.com/js/a');
// => http://cdn.com/js/a.js
// 根路径是普通路径:
require.resolve('/js/b');
// => http://example.com/js/b.js
// use 中的相对路径始终是普通路径:
seajs.use('./c');
// => 加载的是 http://example.com/path/to/page/c.js
seajs.use('../d');
// => 加载的是 http://example.com/path/to/d.js

Tips:

  • 顶级标识始终相对 base 基础路径解析。
    • 如果不设置,base 路径默认就是 sea.js 库文件所属的路径
    • 可以通过 seajs.config({ base: '基础路径' }) 来配置基础路径
  • 绝对路径和根路径始终相对当前页面解析。
  • 相对标识永远相对于当前文件
  • seajs.use 中的相对路径始终相对当前页面来解析。

module

module 是一个对象,上面存储了与当前模块相关联的一些属性和方法。

  • module.id
    • 模块的唯一标识,可以通过 define 方法的第一个参数来指定,默认为该模块文件的绝对路径
  • module.uri
    • 模块的绝对路径
  • module.dependencies
    • dependencies 是一个数组,表示当前模块的依赖
  • module.exports
    • 当前模块对外提供的接口对象
    • 相当于每个模块内部最终都执行了这么一句话:return module.exports
    • 模块与模块之间的通信接口

exports

exports 仅仅是 module.exports 的一个引用。也就是说修改了 exports 就相当于修改了 module.exports。

但是一旦在 factory 内部给 exports 重新赋值,并不会改变 module.exports 的值。因此给 exports 赋值是无效的。

return 也是暴露,等于 module.exports

例如 jQuery 模块化

1
2
3
4
5
6
7
8
9
10
11
// support seajs
if ( typeof define === "function" && define.cmd ) {
define( function () { return jQuery; } );
}
// or
if ( typeof define === "function" && define.cmd ) {
define( function (require, exports, module) {
module.exports = jQuery;
});
}

jQuery 插件包装成模块

1
2
3
4
5
6
7
8
9
10
(function(factory){
if(typeof define === 'function' && define.cmd) {
define(function(require){
var $ = require('jquery');
factory($);
});
}
})(function($){
// jQuery 的插件代码
})

exports 和 module.exports 的区别

  • 每个模块内部对外到处的接口对象始终都是 module.exports
  • 可以通过修改 module.exports 或给它赋值改变模块接口对象
  • exportsmodule.exports 的一个引用,就好比在每一个模块定义最开始的地方写了这么一句代码:var exports = module.exports

分析下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
var module = {
exports: {}
}
function changeExports (exports, module) {
// var exports = module.exports
exports.foo = 'bar'
// 这里赋值拿不到,不要使用使用
// exports = function () {}
return module.exports
}
changeExports(module.exports, module)

如何将一个普通的模块文件改造为兼容 CMD 规范的模块

1
2
3
4
5
6
if (typeof define === "function" && define.cmd) {
// 有 Sea.js 等 CMD 模块加载器存在
define(function (require, exports, module) {
// 使用 module.exports 向外暴露接口对象
})
}

高级配置 seajs.config(options)

可以对 Sea.js 进行配置,让模块编写、开发调试更方便。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
seajs.config({
// 别名配置
alias: {
'es5-safe': 'gallery/es5-safe/0.9.3/es5-safe',
'json': 'gallery/json/1.0.2/json',
'jquery': 'jquery/jquery/1.10.1/jquery'
},
// 路径配置
paths: {
'gallery': 'https://a.alipayobjects.com/gallery'
},
// Sea.js 的基础路径
base: 'http://example.com/path/to/base/',
});

使用 SeaJS 开发计算器案例

  • index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SeaJS - 计算器案例</title>
</head>
<body>
<input type="text" id="x">
<select name="" id="opt">
<option value="0">+</option>
<option value="1">-</option>
<option value="2">*</option>
<option value="3">/</option>
</select>
<input type="text" id="y">
<button id="cal">=</button>
<input type="text" id="result">
<!--
模块系统:
1. 定义模块成员
2. 设定交互规则
0. 引包
1. 使用 define 定义一个模块,所有代码写到 define 回调函数中
2. 使用 module.exports 作为模块与模块之间的通信接口对象
3. 使用 requrie 函数加载模块,执行模块中的代码,得到模块中的 module.exports 接口对象
4. 使用 seajs.use 方法,启动模块系统,类似于电脑开机
-->
<script src="../node_modules/seajs/dist/sea.js"></script>
<script>
seajs.use('./js/main')
</script>
</body>
</html>
  • main.js 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
define(function (require, exports, module) {
var $ = require('../../node_modules/jquery/dist/jquery');
// 如果想要拿到 jQuery 接口对象,那么 jquery.js 文件内部必须显示的通过 moudle.exports 向外暴露
var cal = require('./cal/index');
var oX = document.querySelector('#x');
var oOpt = document.querySelector('#opt');
var oY = document.querySelector('#y');
var oResult = document.querySelector('#result');
var oCal = document.querySelector('#cal');
oCal.addEventListener('click', function (e) {
var opt = oOpt.value;
var result = 0;
var x = oX.value;
var y = oY.value;
switch (opt) {
case '0':
result = cal.add(x, y);
break;
case '1':
result = cal.sub(x, y);
break;
case '2':
result = cal.multiply(x, y);
break;
case '3':
result = cal.divide(x, y);
break;
}
oResult.value = result;
});
});
  • index.js
1
2
3
4
5
6
7
8
9
10
11
12
/**
* 每个文件模块中默认对外的接口对象就是 module.exports
* 同时 SeaJS 还提供了一个接口对象 exports
* 注意:exports 是 module.exports 接口对象的一个引用
* 也就是说:修改了 exports 相当于修改了 module.exports
* 但是,如果想要向外部暴露一个单独的变量、函数等成员,
* 那就必须通过给 module.exports 赋值才可以
*/
define(function (require, exports, module) {
exports.add = require('./add');
exports.sub = require('./sub');
});
  • add.js
1
2
3
4
5
define(function (require, exports, module) {
module.exports = function (x, y) {
return parseFloat(x) + parseFloat(y);
}
});
  • sub.js
1
2
3
4
5
define(function (require, exports, module) {
module.exports = function (x, y) {
return parseFloat(x) - parseFloat(y);
}
});

感谢您的支持!