JavaScript 模块化开发
1. 模块化介绍
掌握模块化基本概念以及使用模块化带来的好处
当你的网站开发越来越复杂的时候,会经常遇到什么问题?
- 恼人的命名冲突
- 繁琐的文件依赖
历史上,JavaScript一直没有模块(module)体系, 无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。 其他语言都有这项功能,比如Ruby的 require
、Python的 import
, 甚至就连CSS都有 @import
, 但是JavaScript任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。
如何解决不通过 script 标签就能加载 JavaScript 文件代码。
-
什么是模块化
- 一块儿一块儿的:JavaScript 也是一块儿一块儿
- 模块化就是一种将复杂事物按照模块的方式简单化实现
- 模块与模块之间互相协作构成了模块系统
-
使用模块化开发的方式带来的好处
- 生产效率高
- 可维护性高
眼见他起高楼,眼见他宴宾客,眼见他楼塌了。
2. 模块化开发演变
体验刀耕火种的模块化解决方式
2.1 全局函数
- 污染了全局变量
- 模块成员之间看不出直接关系
2.2 命名空间
- 理论意义上减少了变量冲突
- 缺点1:暴露了模块中所有的成员,内部状态可以被外部改写,不安全
- 缺点2:命名空间会越来越长
2.3 私有空间
- 私有空间的变量和函数不会影响全局作用域
- 公开公有方法,隐藏私有属性
2.4 模块的维护和扩展
- 开闭原则
- 可维护性好
2.5 模块的第三方依赖
- 保证模块的独立性
- 模块之间的依赖关系变得明显
2.6 总结
以后如果不使用第三方规范的情况下, 如果写模块可以采用下面这种方式
```js ; (function (形参模块名, 依赖项, 依赖项) { // 通过 形参模块名 修改模块
window.模块名 = 形参模块名 })(window.模块名 || {}, 依赖项, 依赖项) ```
下面是一个关于模块化的面试题,一起观察和分析这段代码的作用:
js var UTIL = (function (parent, $) { // 动态的给一个不存在的对象挂载一个子对象 var my = parent.ajax = parent.ajax || {} my.get = function (url, params, callback) { return $.getJSON(url, params, callback) } return parent }(UTIL || {}, jQuery))
3. 模块化规范
3.1 模块系统理解
自然界生态系统、计算机操作系统、软件办公系统,还有教育系统、金融系统、网络系统、 理论系统等等。究竟什么是系统呢?
维基百科:系统泛指由一群有关连的个体组成,根据预先编排好的规则工作, 能完成个别元件不能单独完成的工作的群体。 系统分为自然系统与人为系统两大类。
简单来说,系统有两个基本特性:
- 系统由个体组成
- 个体之间有关联,按照规则协同完成任务
系统之间的个体可以成为系统成员,要构建一个系统,最基本的层面需要做两件事:
- 定义系统成员:确定成员是什么
- 模块是一个 JavaScript 文件
- 每一个模块都使用 define 函数去定义
- 约定系统通讯:确定成员之间如何交互,遵循的规则是什么
- 一个 SeaJS 模块默认就是私有作用域
- 如果想要被外部文件模块所访问,就必须把要公开的属性挂载给 module.exports 对象接口
- 使用 require 函数可以加载一个指定的模块,得到该模块代码中暴露的接口对象
- 如何启动整个模块系统
- 在 html 页面中使用 seajs.use() 方法,指定一个入口文件模块
Sea.js 是一个适用于 Web 浏览器端的模块加载器。 在 Sea.js 里,一切皆是模块,所有模块协同构建成模块系统。 Sea.js 首要要解决的是模块系统的基本问题:
- 模块是什么?
- 模块之间如何交互?
在前端开发领域,一个模块,可以是JS 模块,也可以是 CSS 模块,或是 Template 等模块。 而 Sea.js 则专注于 JS 文件模块:
- 模块是一段 JavaScript 代码,具有统一的 基本书写格式
- 模块之间通过基本 交互规则 ,能彼此引用,协同工作
把上面两点中提及的基本书写格式和基本交互规则描述清楚,就能构建出一个模块系统。 对书写格式和交互规则的详细描述,就是模块定义规范(Module Definition Specification)。
比如 CommonJS 社区的 Modules 1.1.1
规范, 以及 NodeJS 的 Modules
规范, 还有 RequireJS 提出的 AMD
规范等等。
Sea.js 遵循的是 CMD 规范。
3.2 常见的 JavaScript 模块化规范
- CommonJS
- Node.js
- AMD
- RequireJS
- CMD Common Module Definition
- CMD 就是 SeaJS 这个模块加载器在推广的过程中定义的一个模块规范
- ECMAScript
- ECMAScript 6
- UMD
CMD、AMD、CommonJS 都是社区制定出来的模块规范, 他们的目的都是为了解决 JavaScript 没有模块化系统的问题。 他们都有如何定模块成员,以及模块成员之间如何进行通信交互的规则。
AngularJS Module 也是 ng 框架本身提供的一个模块系统解决方案。 ng定义模块需要起名,即便你去加载或者依赖一个模块的时候, 这个被依赖的模块也需要通过 script 标签引入到 html 页面中。
2015 年 9 月份,ECMAScript 官方推出了 ECMAScript 6 语言标准。 在最新的 ES6 语言规范标准中,已经制定了 JavaScript 模块化规范。
export import
前端界的风气:都喜欢追赶新技术,甚至把没有推出来的标准直接干到了生产环境。
这个社区太浮躁了。
4. SeaJS
A Module Loader for the Web, Enjoy the fun of programming.
- 提供简单、极致的模块化开发体验
- A Module Loader for the Web
- JavaScript 模块加载器
- 可以实现 在 JavaScript 代码中去加载另一个 JavaScript 代码。
4.1 SeaJS 介绍
-
关于 SeaJS
- SeaJS 是一个适用于浏览器环境的 JavaScript 模块加载器
- 一个库文件,类似于 jQuery
- 使用这个库提供的规范的模块化的方式来编写 JavaScript 代码
- 只关心 JavaScript 文件代码模块如何组织
- 只关心 JavaScript 文件之间如何相互协议、引用、依赖
- SeaJS 的作者是阿里巴巴支付宝前端架构师,花名:玉伯,玉伯也叫射雕
- Sea.js创始人玉伯的前端开发之路
- SeaJS
- SeaJS -github
-
为什么学习和使用 SeaJS ?
- 简单友好的模块定义规范:SeaJS 遵循 CMD 规范,可以像 Node 一样书写模块代码
- 自然直观的代码组织方式:依赖的自动加载、配置简洁清晰,可以让我们更多的享受编码的乐趣
- SeaJS兼容性非常好,几乎可以运行在任何浏览器引擎上
- 注1:SeaJS 只是实现模块化开发的一种方式或者说一种工具而已,重在模块化思想的理解
- 注2:因为 SeaJS 采用的 CMD 模块规范和 Node 中的 CommonJS 模块规范非常一致,所以有利于我们学习 Node 中的模块化编程
-
谁在用?
- 淘宝网、支付宝、京东、爱奇艺。。。
-
SeaJS 适用场景
- 没有使用任何框架,例如 AngularJS
- 例如 只写写 原生 JavaScript 或者用了一些第三方库
- SeaJS 不提供任何功能性 API,只提供了解决 JavaScript 代码的命名污染和文件依赖的问题
4.2 快速上手(Getting Started)
- 下载 sea.js 库文件
- SeaJS - Release
bower install seajs
npm install seajs
- 在页面中引入 sea.js
- 使用
define
函数定义模块 - 使用
require
函数加载模块 - 使用
module.exports
对外暴露接口对象 - 使用
seajs.use
函数启动模块系统
4.3 API 详解
4.3.1 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
中的模块代码里
4.3.2 define(factory)
define
是一个全局函数,用来定义模块。
define
接受 factory
参数,factory
可以是一个函数,也可以是一个对象或字符串。
factory
为对象、字符串时,表示模块的接口就是该对象、字符串。
-
factory 是一个对象时
define({})
-
factory 是一个字符串时
define('hello')
-
factory 是一个函数时
define(function(require, exports, module){})
4.3.3 require
require 用来加载一个 js 文件模块, require 用来获取指定模块的接口对象 module.exports
。
require 在加载和执行的时候,js 会按照同步的方式和执行。
使用注意:
- 正确拼写
- 模块 factory 构造方法的第一个参数
必须
命名为 require
- 模块 factory 构造方法的第一个参数
- 不要修改
- 不要重命名 require 函数,或在任何作用域中给 require 重新赋值
- 使用字符串直接量
- require 的参数值 必须 是字符串直接量
Tips: 把 require
看做是语法关键字就好啦
4.3.4 模块标识
模块标识是一个字符串,用来标识模块。
-
模块标识可以不包含文件后缀名,比如
.js
- seajs 推荐不加 .js 文件模块后缀
-
模块标识可以是 相对 或 顶级 标识
-
相对标识
相对标识以 .
开头,永远相对于当前模块所处的路径来解析。
- 顶级标识
顶级标识不以 .
或 /
开始,会相对模块系统的基础路径(base路径,默认是 sea.js 文件所属的路径)。 可以手动配置 base 路径。
js seajs.config({ base: './js' })
- 普通路径
除了相对和顶级标识之外的标识都是普通路径。 普通路径的解析规则,会相对当前页面解析。
```js // 假设当前页面是 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
中的相对路径始终相对当前页面来解析。
4.3.5 module
module 是一个对象,上面存储了与当前模块相关联的一些属性和方法。
module.id
- 模块的唯一标识,可以通过
define
方法的第一个参数来指定,默认为该模块文件的绝对路径
- 模块的唯一标识,可以通过
module.uri
- 模块的绝对路径
module.dependencies
- dependencies 是一个数组,表示当前模块的依赖
module.exports
- 当前模块对外提供的接口对象
- 相当于每个模块内部最终都执行了这么一句话:
return module.exports
- 模块与模块之间的通信接口
4.3.6 exports
exports 仅仅是 module.exports 的一个引用。 也就是说修改了 exports 就相当于修改了 module.exports。
但是一旦在 factory 内部给 exports 重新赋值,并不会改变 module.exports 的值。 因此给 exports 赋值是无效的。
4.4 exports 和 module.exports 的区别
- 每个模块内部对外到处的接口对象始终都是
module.exports
- 可以通过修改
module.exports
或给它赋值改变模块接口对象 exports
是module.exports
的一个引用,就好比在每一个模块定义最开始的地方写了这么一句代码:var exports = module.exports
关于这俩哥们儿的区别请分析一下代码:
```js var module = { exports: {} }
function changeExports (exports, module) { // var exports = module.exports exports.foo = 'bar'
// 这里赋值拿不到,不要使用使用 // exports = function () {} return module.exports }
changeExports(module.exports, module) ```
那为啥要有 exports
?
为了开发体验,API更友好,使用 exports 的时候,可以少写一个点儿。
如果你实在分不清楚 exports
和 module.exports
之间的区别,就只记得 module.exports
就可以了。