模块化对于现在的开发人员来说并不陌生,可以说是相当熟悉了。但是如果问 JavaScript 中的模块发展过程,相信有挺多人都是不太了解的。接下来我们就来回顾一下 JavaScript 的发展历程。
凡事三个步骤进行:是什么、为什么、怎么做。
1.什么是模块化?
模块化其实是一种规范、约束。这种规范约束能让我们的代码更具可观性和后续维护性。这种方式会大大的提高我们的工作效率,同时减少了后面维护的时间。
2.为什么要模块化?
- 我们的前端代码越来越庞大
- 代码的复杂性越来越高
- 文件之间的耦合性太高
显然这些问题随着前端项目的持续迭代与发展,都是会遇到的。这时候就需要我们使用模块化来进行管理与约束了。
3.怎么实现模块化?
1、一开始
最早我们写的JS代码就是一个函数一个函数的往下写...
function foo(){
//...
}
function bar(){
//...
}
坏处:直接是写在全局中,变量容易被污染
2、Namespace 命名空间模式
Namespace 模式就是用个对象封装起来
var test = {
foo: function() {},
bar: function() {}
}
test.foo()
test.bar()
这种模式虽然减少了全局的变量数,但是本质上还是全局范围内的一个对象,不安全。
3、匿名闭包:IIFE 模式
var Module = (function(){
var test = "test";
var foo = function(){
console.log(test)
}
return {
foo: foo
}
})()
Module.foo();
Module.test; // undefined
每个闭包都是单独一个文件,利用闭包的方式来解决变量重名和环境污染问题
但是闭包同样会带来许多的问题:如内存泄露、this指向问题等等...
4、引入依赖
var Module = (function($){
var _$body = $("body"); // we can use jQuery now!
var foo = function(){
console.log(_$body); // 特权方法
}
// Revelation Pattern
return {
foo: foo
}
})(jQuery)
Module.foo();
这就是模块模式,也是现代模块实现的基石
5、LABjs - Script Loader
“ LABjs是一种动态脚本加载器,旨在以灵活且性能优化的替代API代替丑陋,性能不佳的 <script> 标记的使用。 ”
够以浏览器允许的速度并行加载所有JavaScript文件的功能
$LAB
.script("script1.js")
.script("script2.js")
.script("script3.js")
.wait(function(){ // 等待所有script加载完再执行这个代码块
script1Func();
script2Func();
script3Func();
});
6、YUI3 Loader
YUI的轻量级内核和模块化体系结构使其可扩展,快速且强大
结构上分四大类:种子、核心、组件框架、组件
最大特点: 动态按需加载、细粒度化设计
// YUI - 编写模块
YUI.add('dom', function(Y) {
Y.DOM = { ... }
})
// YUI - 使用模块
YUI().use('dom', function(Y) {
Y.DOM.doSomeThing();
// use some methods DOM attach to Y
})
创建自定义模块
// hello.js
YUI.add('hello', function(Y){
Y.sayHello = function(msg){
Y.DOM.set(el, 'innerHTML', 'Hello!');
}
},'3.0.0',{
requires:['dom']
})
// main.js
YUI().use('hello', function(Y){
Y.sayHello("hey yui loader");
})
你不必按固定顺序来包含脚本标签,加载和执行是分离的
script(src="/path/to/yui-min.js") // YUI seed
script(src="/path/to/my/module1.js") // add('module1')
script(src="/path/to/my/module2.js") // add('module2')
script(src="/path/to/my/module3.js") // add('module3')
YUI().use('module1', 'module2', 'module3', function(Y) {
// you can use all this module now
});
但是这样还有一个问题:Too much HTTP calls
YUI 组合
在单个请求中处理多个文件,但是需要服务器支持(alibaba/nginx-http-concat)
script(src="http://yui.yahooapis.com/3.0.0/build/yui/yui-min.js")
script(src="http://yui.yahooapis.com/3.0.0/build/dom/dom-min.js")
⬇️
script(src="http://yui.yahooapis.com/combo?
3.0.0/build/yui/yui-min.js&
3.0.0/build/dom/dom-min.js")
7、CommonJS
特点:
- 所有代码都运行带模块作用域、不会污染全局作用域
- 模块可以多次加载,但只会在第一次加载时运行一次,然后运行结果就被缓存了。想要模块再次运行,需要清除缓存
- 模块加载的顺序是按照其在代码中出现的顺序
module.exports 属性与 exports 变量
module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。
node 为每一个模块提供了一个exports变量(可以说是一个对象),指向 module.exports。这相当于每个模块中都有一句这样的命令 var exports = module.exports
既然两个不好区分,那就放弃 exports, 只用 module.exports 就好。
模块的定义与引用
// math.js
exports.add = function(a, b){
return a + b;
}
// main.js
var math = require('math') // ./math in node
console.log(math.add(1, 2)); // 3
8、AMD / CMD
RequireJS - AMD
AMD 规范是非同步加载模块,允许指定回调函数
定义
define(['package/lib'],function(lib){
function foo(){
lib.log('hello world!')
}
return{foo:foo}
})
Async 异步
//CommonJS Syntax
var Employee = require("types/Employee");
function Programmer (){
//do something
}
Programmer.prototype = new Employee();
//如果 require call 是异步的,那么肯定 error
//因为在执行这句前 Employee 模块根本来不及加载进来
Function Wrapping 功能封装
//AMD Wrapper
define(
["types/Employee"], //依赖
function(Employee){ //这个回调会在所有依赖都被加载后才执行
function Programmer(){
//do something
};
Programmer.prototype = new Employee();
return Programmer; //return Constructor
}
)
AMD vs CommonJS
1、相同点
都是为了模块化
2、不同点
加载上
AMD 规范是非同步加载模块,允许指定回调函数
CommonJS 规范加载模块是同步的
书写
// Module/1.0
var a = require("./a"); // 依赖就近
a.doSomething();
var b = require("./b")
b.doSomething();
// AMD recommended style
define(["a", "b"], function(a, b){ // 依赖前置
a.doSomething();
b.doSomething();
})
执行时机
// Module/1.0
var a = require("./a"); // 执行到此时,a.js 同步下载并执行
// AMD with CommonJS sugar
define(["require"], function(require){
// 在这里, a.js 已经下载并且执行好了
var a = require("./a")
})
SeaJS - CMD
SeaJS 遵循 CMD 规范模块化开发,依赖的自动加载、配置的简介清晰
非常像 CommonJS 的格式
加载单个依赖
//加载模块 main,并在加载完成时,执行指定回调
seajs.use('./main', function(main) {
main.init();
})
加载多个依赖
//并发加载模块 a 和模块 b,并在都加载完成时,执行指定回调
seajs.use(['./a', './b'], function(a, b) {
a.init();
b.init();
})
定义
// 所有模块都通过 define 来定义
define(function(require, exports, module) {
// 通过 require 引入依赖
var $ = require('jquery');
var Spinning = require('./spinning');
// 通过 exports 对外提供接口
exports.doSomething = ...
// 或者通过 module.exports 提供整个接口
module.exports = ...
// 模块是通过define()方法包装的,然后内部痛过require()方法引入需要的依赖文件(模块)。(也可以引入.css文件哦~)
});
AMD vs CMD
1、AMD 是提前执行, CMD 是延迟执行
2、CMD 推崇依赖就近,AMD 推崇依赖前置
// CMD
define(function(require, exports, module) {
var a = require('./a')
a.doSomething()
// ...
var b = require('./b') // 依赖可以就近书写
b.doSomething()
// ...
})
// AMD
define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好
a.doSomething()
// ...
b.doSomething()
...
})
3、AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一
9、Browserify / Webpack
1、Browserify
browserify 是用于为浏览器编译节点风格的 commonjs 模块的工具 。
您可以使用browserify来组织代码并使用第三方库,即使您不以其他任何身份使用node本身,除了使用npm捆绑和安装软件包之外。
browserify使用的模块系统与节点相同,因此发布到npm的程序包最初打算在节点中使用,但不适用于浏览器,也可以在浏览器中正常工作。
2、Webpack
引用 Webpack 官网的一句话:“webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。”
简单的CLI
# make sure your directory contains webpack.config.js
# Development: debug + devtool + source-map + pathinfo
webpack main.js bundle.js -d
# Production: minimize + occurence-order
webpack main.js bundle.js -p
# Watch Mode
webpack main.js bundle.js --watch
具体的 Webpack 知识,可以移步 <a href="https://www.webpackjs.com/">这里</a>
10、ES6 Module
ES6 模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。
如果想了解更多的 ES6 Module 知识,可以点击 <a href="http://caibaojian.com/es6/module.html">这里</a>
// CommonJS模块
let { stat, exists, readFile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat, exists = _fs.exists, readfile = _fs.readfile;
ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。
// ES6模块
import { stat, exists, readFile } from 'fs';
除了静态加载带来的各种好处,ES6 模块还有以下好处。
- 不再需要UMD模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。目前,通过各种工具库,其实已经做到了这一点。
- 将来浏览器的新 API 就能用模块格式提供,不再必要做成全局变量或者navigator对象的属性。
- 不再需要对象作为命名空间(比如Math对象),未来这些功能可以通过模块提供。
严格模式
ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict"
严格模式主要有以下限制。
- 变量必须声明后再使用
- 函数的参数不能有同名属性,否则报错
- 不能使用with语句
- 不能对只读属性赋值,否则报错
- ...
export 命令
模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。
// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;
export的写法,除了像上面这样,还有另外一种
// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export {firstName, lastName, year};
4.总结思考
本文着重介绍了 JavaScript 模块发展的历程,并简单的罗列了各个时期模块化解决方案中的一些小例子。
不同的模块化手段其实都是为了
- 为了让日后的代码更具有迭代性和维护性
- 解决文件依赖加载存在的问题
- 更方便服务于我们的日常业务开发...
如今到了 ES6 Module,虽然比起以前模块化的发展和规范已经日渐完善了,但并不会止步于此。随着业务的发展与迭代,后续肯定会继续出现更加符合业务开发维护的模块化管理方案。
我个人觉得:没有更完美,只有更符合
参考
<a href="https://www.webpackjs.com/concepts/">Webpack 官网</a>
<a href="http://caibaojian.com/es6/module.html">ES6 module</a>
<a href="https://huangxuan.me/2015/07/09/js-module-7day/">JavaScript 模块化七日谈</a>
<a href="https://juejin.cn/post/6844903581661790216">浅谈模块化开发</a>
<a href="https://www.cnblogs.com/xjchenhao/p/4021775.html/">快速上手seajs——简单易用Seajs</a>