前言
不管是前端老司机还是刚接触前端的"菜鸟"。模块化想必在每天工作中,或多或少都会接触到。尤其针对一些针对React
、Vue
开发的同学来说,那就是每天都会脱口而出的一个必备术语。并且在很多技术文档中,都常常看到AMD
、UMD
、COMMONJS
还有ES6中的module
。
但是,模块化的本质是什么!前端是如何从"茹毛饮血"的<script>
到现在es6的module
的呢。
今天我们就来唠唠这段鲜为人知的故事。
模块化本质
何为模块化。其实就是功能的单一化或者说功能的切片化编程。更直白一点就是,每一个独立的功能都有自己独立的作用域。
让我们看看针对模块的英文定义
Modules are an integral piece of any robust application's architecture and typically help in keeping the units of code for a project both cleanly separated and organized.
而JS在实现模块代码有如下方式:
- 对象字面量
- 设计模式中的模块模式
- AMD
- CommonJS
- ES6 module
让我们针对每一个方式来一一说明
对象字面量
由于JS语法本身没有块级作用域的概念(es6之前),所以是没法直接利用{}
来将指定的代码进行封装。如果想将特定用于处理类似功能的代码合并到一起。对象字面量不失为一个很好的方式。(有人会说,用函数封装也可以啊,记住JS中一切皆对象)
Talk is cheap ,show you the code
var myModule = { myProperty: "北宸", // 对象字面量可以包含属性和方法 // 我们还可以为该模块定义配置信息: myConfig: { useCaching: true, language: "en" }, // saySomething: function () { console.log( "你好啊,世界" ); }, // 基于配置信息输出一些信息 reportMyConfig: function () { console.log( "缓存: " + ( this.myConfig.useCaching ? "可用" : "禁用") ); }, // 重新配置信息 updateMyConfig: function( newConfig ) { if ( typeof newConfig === "object" ) { this.myConfig = newConfig; console.log( this.myConfig.language ); } } }; // 你好啊,世界 myModule.saySomething(); // 缓存可用 myModule.reportMyConfig(); // fr myModule.updateMyConfig({ language: "fr", useCaching: false }); // 缓存禁用 myModule.reportMyConfig(); 复制代码
从上述代码中,可以看到,将一些操作和数据进行了封装。实现了,功能切片化处理。但是如果细想,感觉利用字面量来封装数据和方法,感觉有点鸡肋。因为这个对象是一个单例。如果只是在一个地方使用和操作,那完全没有问题,但是如果是多个地方都用到呢,同时多个地方都需要对指定的属性进行修改。这就是牵一发而动全身的操作。
同时,我们可以看到利用字面量进行数据和方法封装。这些属性都是对外友好的。都是能在外部访问到的。没有丝毫的隐私,用传统OOP编程术语来讲。这些属性都是public
的。也就是说,无法实现属性私有化。
模块模式
为了解决字面量无法进行属性私有化。模块模式应用而生。这也是JS语言在早起比较常用的模块化处理方式。
Talk is cheap ,show you the code
var MODULE = (function () { var my = {}, privateVariable = 1; function privateMethod() { // ... } my.moduleProperty = 1; my.moduleMethod = function () { // ... }; return my; }()); 复制代码
从代码上看到,一个IIFE赫然映入眼帘。偷偷的告诉大家,模块模式
就是利用IIFE
实现的。
为了不占用很大篇幅来讲解这个实现。特定为大家准备了饭后甜点。JS_Module模式深入了解一下
AMD
其实AMD(Asynchronous Module Definition)是一种为浏览器环境书写模块的模式。 而能够实现异步加载的关键就在于RequireJS。
RequireJS
是在ES6module
没出现之前,常用的前端模块解决方案。RequireJS
将加载的每一个独立模块作为<script>
,并利用head.appendChild()
追加到文档中。
RequireJS等待所有依赖模块加载,将该模块需要的额外模块进行排序,并在依赖模块加载完之后,调用本模块的定义函数。
Talk is cheap,show you the code
在项目中存在如下结构,我们用cart.js
、inventory.js
来构建一个shirt.js
- my/cart.js
- my/inventory.js
- my/shirt.js
define(["./cart", "./inventory"], function(cart, inventory) { //返回一个对象用于定义"my/shirt"模块 return { color: "blue", size: "large", addToCart: function() { inventory.decrement(this); cart.add(this); } } } ); 复制代码
当然,上述中的本地模块cart.js
也可以换成JQuery
等现成的模块。
如果想对AMD
有一个更深的了解,或者想知道如何定义一个AMD
模块。可以先移步RequireJS官网。
CommonJS
JS有一条定律:Atwood's Law
any application that can be written in JavaScript, will eventually be written in JavaScript.
JS是可以在服务端存在,所以出现了CommonJS
(A Module Format Optimized For The Server),使得JS不仅仅在浏览器端应用,而且在服务端开始发光发热。
CommonJS
是专注于
- 服务端应用
- 公共工具方法
- 基于GUI的桌面程序
- 混合应用 (Titanium, Adobe AIR)
和AMD
不是一个服务层面。
Talk is cheap ,show you the code
a.js
var x = 5; var addX = function (value) { return value + x; }; module.exports.x = x; module.exports.addX = addX; 复制代码
上面代码通过module.exports输出变量x和函数addX。
b.js
var example = require('./a.js'); console.log(example.x); // 5 console.log(example.addX(1)); // 6 复制代码
commonJS模块特点
- 每个文件就是一个模块,它有自己的作用域(不会污染全局作用域)。在文件里面定义的变量、函数、类,都是这个模块的私有的,对外不可见。
- 模块加载顺序,按照代码执行顺序。也就是说,是同步加载的。
- 模块可以重复加载,但是会在加载第一次的时候,就将该模块缓存起来,后面再次加载将从缓存中获取该模块。(注意:如果要重新加载模块,需要清空缓存)
ES6 module
在ES6中,从语法层面就提供了模块化的功能。然而受限于浏览器的实现程度,如果想要在浏览器中运行,还是需要通过Babel等转译工具进行编译。
person-module.js
var firstName = '北宸'; var lastName = '范'; export { firstName, lastName }; 复制代码
test-module.js
import {firstName,lastName} from './person-module.js'; console.log(`${lastName}${firstName}`)//范北宸 复制代码
具体细节请参考Module的用法
各个模块比较
模块化方案 | 加载 | 同步/异步 | 浏览器 | 服务端 | 模块定义 | 模块引入 |
Module Pattern | 取决于代码 | 取决于代码 | 支持 | 支持 | IIFE | 命名空间 |
AMD | 提前预加载 | 异步 | 支持 | 构建工具r.js | define | require |
Common | 值拷贝,运行时加载 | 同步 | 原生不支持 | 需要使用browserify提前打包编译 | module.exports | require |
ES Modules(ES6) | 实时绑定,动态绑定,编译时输出 | 同步 | 支持 | 需用babel转译 | export | import |
本文参考链接: