# JavaScript 模块

# 模块风格介绍

伴随着移动互联的大潮,当今越来越多的网站已经从网页模式进化到了 Webapp 模式。它们运行在现代的高级浏览器里,使用 HTML5、 CSS3、 ES6 等更新的技术来开发丰富的功能,网页已经不仅仅是完成浏览的基本需求,并且webapp通常是一个单页面应用,每一个视图通过异步的方式加载,这导致页面初始化和使用过程中会加载越来越多的 JavaScript 代码,这给前端开发的流程和资源组织带来了巨大的挑战。

前端开发和其他开发工作的主要区别,首先是前端是基于多语言、多层次的编码和组织工作,其次前端产品的交付是基于浏览器,这些资源是通过增量加载的方式运行到浏览器端,如何在开发环境组织好这些碎片化的代码和资源,并且保证他们在浏览器端快速、优雅的加载和更新,就需要一个模块化系统,这个理想中的模块化系统是前端工程师多年来一直探索的难题。

在ES6之前,javscript并没有对模块做出任何定义,于是先驱者们创造出了各种各样的规范来完成这个任务。伴随着require.js的流行,AMD格式成为首选。 之后,随之而来的是CommonJS格式,在之后browerify的诞生,让浏览器也能使用这种格式。知道ES6出现,模块这个概念才真正有了语言特性的支持。

# js模块化方案

3个阶段

  • 全局变量 + 命名空间
  • AMD & CommonJS
  • ES6模块

# 全局变量 + 命名空间

基于同一个全局变量,例如window.foo,各个模块按照自己的命名空间进行挂载,例如Jquery

const foo = window.foo;
//export
foo.bar = xx;

使用时可以直接调用window.foo.bar使用

模块内部使用自执行函数实现局部作用域

(function(){
}){}

# AMD&CMD

详见后文

# ES6模块

详见后文

# js 组件化方案

4种:

  • 基于命名空间的多入口文件组件
  • 基于模块的多入口文件组件
  • 单js入口组件
  • web component

# 基于命名空间的多入口文件组件

基于第一种模块化方案,很典型的做法是使用时引入<script>标签和<link>标签,在script加载的js代码中,会将变量挂载到全局变量,方便之后直接调用。

# 基于模块的多入口文件组件

前端此时有了模块化的方案,可以组织js 的实现,把自己暴露为一个模块,但是样式和其他依赖资源(图片,字体)还未能纳入整体的模块化方案,此时的组件往往呈现为:

  • 一个AMD模块,js
  • 一个css,样式
  • 其他资源,往往不需要手动引入,组件会在其css实现中通过相对路径引入

我们在工程上:

  • 在js中require一些模块
  • 在样式代码中引入组件的样式内容

但是,虽然js模块化了,但是组件的实现和使用方法依然不便利

# 单js入口组件

browserify, webpack等现代的打包工具的出现为解决上一个方案的遗留问题。他允许我们将一般的资源视为js平等的模块,并以一致的方式加载,所以可以将一个组件组织为如下形式:

 header/
     - header.js
 body/
     - img/
     - main.js
     - style.less
 footer/
     - img/
     - footer.js

# Web component

2011年提出,它所期待的是向使用html标签一样使用组建,完全意义上的样式内容隔绝 4大特性:

  • 自定义元素
  • HTML模板
  • Shadow的DOM
  • HTML引入(HTML Import)

# 模块的详细介绍

  • <script> tag 无模块管理风格
  • AMD
  • CommonJS
  • CMD
  • ES6 modules

# tag 风格

传统的无模块系统的模块化代码方案

<script src="module1.js"></script>
<script src="module2.js"></script>
<script src="libraryA.js"></script>
<script src="module3.js"></script>

每一个模块都导出一个接口给全局的变量,例如 window。模块可以通过这个全局变量来读取依赖的接口。

常见的问题

  • 全局作用域下容易造成变量冲突
  • 文件只能按照 <script> 的书写顺序进行加载
  • 开发人员必须主观解决模块和代码库的依赖关系
  • 在大型项目中各种资源难以管理,长期积累的问题导致代码库混乱不堪

# CommonJS

CommonJS 是以在浏览器环境之外构建 JavaScript 生态系统为目标而产生的项目,(没有define函数的CommonJS模块【参考require.js 】是无法直接在浏览器中执行,浏览器环境中无法实现同Node.js 环境一样同步的require方法),比如在服务器和桌面环境中。 这个项目最开始是由 Mozilla 的工程师 Kevin Dangoor 在2009年1月创建的,当时的名字是 ServerJS。

CommonJS 规范是为了解决 JavaScript的作用域问题而定义的模块形式,可以使每个模块它自身的命名空间中执行。该规范的主要内容是,模块必须通过 module.exports/exports 导出对外的变量或接口,通过 require() 来导入其他模块的输出到当前模块作用域中。Commonjs本不适合在浏览器环境当中执行,但依赖现代打包工具的能力,Commonjs模块也可以经过转换后在浏览器中执行。加载时执行是 CommonJS 模块的重要特性,即脚本代码在 require 的时候就会执行模块中的代码。这个特性在服务端是没问题的,但如果引入一个模块就要等待它执行完才能执行后面的代码,这在浏览器端就会有很大的问题了。因此出现了AMD规范,以支持浏览器环境。

Commonjs 提供两个工具:

  • require() 函数,当前作用域下导入其他模块.
  • module 对象, 当前域到处变量和接口.

服务器端的 Node.js 遵循 CommonJS规范,该规范的核心思想是允许模块通过 require 方法来 同步加载所要依赖的其他模块,然后通过 exports 或 module.exports 来导出需要暴露的接口

require("module");
require("../file.js");
exports.doStuff = function() {};
module.exports = someValue;

特点:

  1. CommonJS 模块中 require 引入模块的位置不同会对输出结果产生影响,并且会生成值的拷贝
  2. CommonJS 模块重复引入的模块并不会重复执行,再次获取模块只会获得之前获取到的模块的拷贝

优点

  • 服务端的模块可重用
  • 已经有很多模块用这种风格 比如npm包
  • 非常简单易用

缺点

  • 网络请求是异步的,所以在网络请求上阻塞执行的不是很好
  • 不能非阻塞的并行加载多个模块

一些实现:

  • Node.js - 服务端
  • browserify
  • modules-webmake -编译成一个包
  • wreq - 服务端

# AMD: 异步加载

Asynchronous Module Definition规范其实只有一个主要接口 define(id?, dependencies?, factory),它要在声明模块的时候指定所有的依赖dependencies,并且还要当做形参传到 factory 中,对于依赖的模块提前执行,依赖前置。

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

AMD革命性的将Javascript模块化的方案带到了前端开发中,他解决了以下问题:

  • 仅仅需要在全局环境下定义require和define,不需要其他的全局变量
  • 通过文件的路径或模块自己声明模块名定位模块
  • 模块实现中声明依赖,依赖的加载与执行均由加载器操作
  • 提供了打包工具自动分析依赖并且合并
define("module", ["dep1", "dep2"], function(d1, d2) {
  return someExportedValue;
});

require(["module", "../file"], function(module, file) { /* ... */ });

优点:

  • 适合在浏览器环境中异步加载模块
  • 可以并行加载多个模块

缺点:

  • 提高了开发成本,代码的阅读和书写比较困难,模块定义方式的语义不顺畅
  • 不符合通用的模块化思维方式,是一种妥协的实现

实现:

  • RequireJS
  • curl

扩展Require.js

RequireJS项目本身是最流行的AMD规范实现,格式如下。

//hello.js
define(function(require){
module.exports = 'hello!';
})

AMD通过将模块的实现代码包在匿名函数内部(即AMD的工厂方法,factory)中实现作用域的隔离,通过文件路径作为天然的模块ID实现命名空间的控制,将模块的工厂方法作为参数传入全局的define(由模块加载器事先定义),是的工厂方法的执行时机可控,也就变现模拟出了同步的局部require,因而AMD的模块可以直接以原文件的形式在浏览器中加载执行并调试

# CMD

Common Module Definition 规范和 AMD 很相似,尽量保持简单,并与 CommonJS 和 Node.js 的 Modules 规范保持了很大的兼容性。

define(function(require, exports, module) {
  var $ = require('jquery');
  var Spinning = require('./spinning');
  exports.doSomething = ...
  module.exports = ...
})

优点:

  • 依赖就近,延迟执行
  • 可以很容易在 Node.js 中运行

缺点:

  • 依赖 SPM 打包,模块的加载逻辑偏重

实现:

  • Sea.js
  • coolie

# UMD

Universal Module Definition 规范类似于兼容 CommonJS 和 AMD 的语法糖,是模块定义的跨平台解决方案。

# ES6 modules

EcmaScript6 标准增加了 JavaScript 语言层面的模块体系定义。ES6 模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。在遇到模块加载命令import时,不会去执行模块,而是只生成一个引用。等到真的需要用到时,再到模块里面去取值。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。

import "jquery";
export function doStuff() {}
module "localModule" {}

特点:

  • 多次导入,调用一次
  • import和export命令只能在模块的顶层,不能在代码块之中
  • 动态import可以在代码块中执行
  • import 命令会被JavaScript引擎静态分析,优先于模块内的其他内容执行。
  • export 命令会有变量声明提前的效果。

优点:

  • 容易进行静态分析
  • 面向未来的 EcmaScript 标准

缺点:

  • 原生浏览器端还没有实现该标准
  • 全新的命令字,新版的 Node.js才支持

实现:

  • Babel

# 对比

CommonJS 规范主要用于服务端编程,加载模块是同步的,这并不适合在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了 AMD CMD 解决方案。 AMD 规范在浏览器环境中异步加载模块,而且可以并行加载多个模块。不过,AMD 规范开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅。 CMD 规范与 AMD 规范很相似,都用于浏览器编程,依赖就近,延迟执行,可以很容易在 Node.js 中运行。不过,依赖 SPM 打包,模块的加载逻辑偏重ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

# ES6 模块跟 CommonJS 模块的不同之处

  • ES6 模块输出的是值的引用,输出接口动态绑定,而 CommonJS 输出的是值的拷贝
  • ES6 模块编译时执行,而 CommonJS 模块总是在运行时加载

ES模块不会重复执行

# 期望的模块系统

可以兼容多种模块风格,尽量可以利用已有的代码,不仅仅只是 JavaScript 模块化,还有 CSS、图片、字体等资源也需要模块化。

# 参考

  1. 模块系统 (opens new window)
  2. 前端模块化详解 (opens new window)
  3. 深入理解 ES6 模块机制 (opens new window)
陕ICP备20004732号-3