The last time, I have learnedjavascript
【THE LAST TIME】一直是我想写的一个系列,旨在厚积薄发,重温前端。css
也是对本身的查缺补漏和技术分享。html
欢迎你们多多评论指点吐槽。前端
系列文章均首发于公众号【全栈前端精选】,笔者文章集合详见GitHub 地址:Nealyang/personalBlog。目录和发文顺序皆为暂定java
随着互联网的发展,前端开发也变的愈来愈复杂,从一开始的表单验证到如今动不动上千上万行代码的项目开发,团队协做就是咱们不可避免的工做方式,为了更好地管理功能逻辑,模块化的概念也就渐渐产生了。node
好的书籍📚会分章节,好的代码得分模块。jquery
JavaScript 在早期的设计中就没有模块、包甚至类的概念,虽然 ES6
中有了 class
关键字,那也只是个语法糖。随意随着项目复杂度的增长,开发者必然须要模拟类的功能,来隔离、封装、组织复杂的 JavaScript 代码,而这种封装和隔离,也被被咱们称之为模块化。git
模块就是一个实现特定功能的文件 or 代码块。随着前端工程体系建设的愈发成熟,或许模块化的概念已经在前端圈子里已经耳熟能详了。es6
可是对于不少开发者而言,ES6 中的 export
、import
,nodejs
中的 require
、exports.xx
、module.exports
到底有什么区别?为何又有 CommonJS
,又有 AMD
,CMD
,UMD
?区别是什么?甚至咱们在编写 ts 文件的时候,还须要在配置文件里面说明什么模块方式,在项目中使用的时候,咱们又是否真正知道,你用的究竟是基于哪种规范的模块化?github
本文对你写代码没有一点帮助,可是若是你还对上述的问题存有疑惑或者想了解JavaScript 模块化的前世古今,那么咱们开始吧~
公众号回复【xmind2】获取源文件
所谓的模块化,粗俗的讲,就是把一大坨代码,一铲一铲分红一个个小小坨。固然,这种分割也必须是合理的,以便于你增减或者修改功能,而且不会影响总体系统的稳定性。
我的认为模块化具备如下几个好处:
npm
上找 package
的时候,是在干啥?对于某一工程做业或者行为进行定性的信息规定。主要是由于没法精准定量而造成的标准,因此,被称为规范。在模块化尚未规范肯定的时候,咱们都称之为原始模块化。
回到咱们刚刚说的模块的定义,模块就是一个实现特定功能的文件 or 代码块(这是我本身给定义的)。专业定义是,在程序设计中,为完成某一功能所需的一段程序或子程序;或指能由编译程序、装配程序等处理的独立程序单位;或指大型软件系统的一部分。而函数的一个功能就是实现特定逻辑的一组语句打包。而且 JavaScript 的做用域就是基于函数的。因此最原始之处,函数必然是做为模块化的第一步。
//函数1
function fn1(){
//statement
}
//函数2
function fn2(){
//statement
}
复制代码
其实就是把变量名塞的深一点。。。
let module1 = {
let tag : 1,
let name:'module1',
fun1(){
console.log('this is fun1')
},
fun2(){
console.log('this is fun2')
}
}
复制代码
咱们在使用的时候呢,就直接
module1.fun2();
复制代码
IIFE
就是当即执行函数,咱们能够经过匿名闭包的形式来实现模块化
let global = 'Hello, I am a global variable :)';
(function () {
// 在函数的做用域中下面的变量是私有的
const myGrades = [93, 95, 88, 0, 55, 91];
let average = function() {
let total = myGrades.reduce(function(accumulator, item) {
return accumulator + item}, 0);
return 'Your average grade is ' + total / myGrades.length + '.';
}
let failing = function(){
let failingGrades = myGrades.filter(function(item) {
return item < 70;});
return 'You failed ' + failingGrades.length + ' times.';
}
console.log(failing());
console.log(global);
}());
// 控制台显示:'You failed 2 times.'
// 控制台显示:'Hello, I am a global variable :)'
复制代码
这种方法的好处在于,你能够在函数内部使用局部变量,而不会意外覆盖同名全局变量,但仍然可以访问到全局变量
相似如上的 IIFE
,还有很是多的演进写法
好比引入依赖:
// module.js文件
(function(window, $) {
let data = 'www.baidu.com'
//操做数据的函数
function foo() {
//用于暴露有函数
console.log(`foo() ${data}`)
$('body').css('background', 'red')
}
function bar() {
//用于暴露有函数
console.log(`bar() ${data}`)
otherFun() //内部调用
}
function otherFun() {
//内部私有的函数
console.log('otherFun()')
}
//暴露行为
window.myModule = { foo, bar }
})(window, jQuery)
复制代码
// index.html文件
<!-- 引入的js必须有必定顺序 -->
<script type="text/javascript" src="jquery-1.10.1.js"></script>
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript"> myModule.foo() </script>
复制代码
还有一种所谓的揭示模块模式 Revealing module pattern
var myGradesCalculate = (function () {
// 在函数的做用域中下面的变量是私有的
var myGrades = [93, 95, 88, 0, 55, 91];
var average = function() {
var total = myGrades.reduce(function(accumulator, item) {
return accumulator + item;
}, 0);
return'Your average grade is ' + total / myGrades.length + '.';
};
var failing = function() {
var failingGrades = myGrades.filter(function(item) {
return item < 70;
});
return 'You failed ' + failingGrades.length + ' times.';
};
// 将公有指针指向私有方法
return {
average: average,
failing: failing
}
})();
myGradesCalculate.failing(); // 'You failed 2 times.'
myGradesCalculate.average(); // 'Your average grade is 70.33333333333333.'
复制代码
这和咱们以前的实现方法很是相近,除了它会确保,在全部的变量和方法暴露以前都会保持私有.
public
和 private
的概念上述的全部解决方案都有一个共同点:使用单个全局变量来把全部的代码包含在一个函数内,由此来建立私有的命名空间和闭包做用域。
虽然每种方法都比较有效,但也都有各自的短板。
随着大前端时代的到来,常见的 JavaScript 模块规范也就有了:CommonJS
、AMD
、CMD
、UMD
、ES6
原生。
CommonJS
是 JavaScript 的一个模块化规范,主要用于服务端Nodejs 中,固然,经过转换打包,也能够运行在浏览器端。毕竟服务端加载的模块都是存放于本地磁盘中,因此加载起来比较快,不须要考虑异步方式。
根据规范,每个文件既是一个模块,其内部定义的变量是属于这个模块的,不会污染全局变量。
CommonJS
的核心思想是经过 require
方法来同步加载所依赖的模块,而后经过 exports
或者 module.exprots
来导出对外暴露的接口。
CommonJS
的规范说明,一个单独的文件就是一个模块,也就是一个单独的做用域。而且模块只有一个出口,module.exports
/exports.xxx
// lib/math.js
const NAME='Nealayng';
module.exports.author = NAME;
module.exports.add = (a,b)=> a+b;
复制代码
加载模块使用 require
方法,该方法读取文件而且执行,返回文件中 module.exports
对象
// main.js
const mathLib = require('./lib/math');
console.log(mathLib.author);//Nealyang
console.log(mathLib.add(1,2));// 3
复制代码
因为浏览器不支持 CommonJS
规范,由于其根本没有 module
、exports
、require
等变量,若是要使用,则必须转换格式。Browserify是目前最经常使用的CommonJS格式转换的工具,咱们能够经过安装browserify
来对其进行转换.可是咱们仍然须要注意,因为 CommonJS 的规范是阻塞式加载,而且模块文件存放在服务器端,可能会出现假死的等待状态。
npm i browserify -g
复制代码
而后使用以下命令
browserify main.js -o js/bundle/main.js
复制代码
而后在 HTML 中引入使用便可。
有一说一,在浏览器中使用 CommonJS 的规范去加载模块,真的不是很方便。若是必定要使用,咱们可使用browserify编译打包,也可使用require1k,直接在浏览器上运行便可。
其实在 nodejs 中模块的实现并不是彻底按照 CommonJS 的规范来的,而是进行了取舍。
Node 中,一个文件是一个模块->module
源码定义以下:
function Module(id = '', parent) {
this.id = id;
this.path = path.dirname(id);
this.exports = {};
this.parent = parent;
updateChildren(parent, this, false);
this.filename = null;
this.loaded = false;
this.children = [];
}
复制代码
//实例化一个模块
var module = new Module(filename, parent);
复制代码
CommonJS 的一个模块,就是一个脚本文件。require命令第一次加载该脚本,就会执行整个脚本,而后在内存生成一个对象。
{
id: '...',
exports: { ... },
loaded: true,
...
}
复制代码
上面代码就是 Node 内部加载模块后生成的一个对象。该对象的id属性是模块名,exports属性是模块输出的各个接口,loaded属性是一个布尔值,表示该模块的脚本是否执行完毕。其余还有不少属性,这里都省略不介绍了。
之后须要用到这个模块的时候,就会到exports属性上面取值。即便再次执行require命令,也不会再次执行该模块,而是到缓存之中取值。也就是说,CommonJS 模块不管加载多少次,都只会在第一次加载时运行一次,之后再加载,就返回第一次运行的结果,除非手动清除系统缓存。
再去深究具体的实现细节。。那就。。。下一篇分享吧~
Asynchronous Module Definition:异步模块定义。
也就是解决咱们上面说的 CommonJS 在浏览器端致命的问题:假死。
CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操做。AMD规范则是异步加载模块,容许指定回调函数。
因为其并不是原生 js 所支持的那种写法。因此使用 AMD 规范开发的时候就须要大名鼎鼎的函数库 require.js
的支持了。
关于 require.js 的更详细使用说明能够参考官网 api:requirejs.org/docs/api.ht…
require.js
主要解决两个问题:
define(id,[dependence],callback)
复制代码
id
,一个可选参数,说白了就是给模块取个名字,可是倒是模块的惟一标识。若是没有提供则取脚本的文件名dependence
,以来的模块数组callback
,工厂方法,模块初始化的一些操做。若是是函数,应该只被执行一次。若是是对象,则为模块的输出值require([moduleName],callback);
复制代码
moduleName
,以来的模块数组callback
,即为依赖模块加载成功以后执行的回调函数(前端异步的通用解决方案),<script src="scripts/require.js" data-main="scripts/app.js"></script>
复制代码
data-main
指定入口文件,好比这里指定 scripts
下的 app.js
文件,那么只有直接或者间接与app.js
有依赖关系的模块才会被插入到html中。
经过这个函数能够对requirejs
进行灵活的配置,其参数为一个配置对象,配置项及含义以下:
baseUrl
——用于加载模块的根路径。paths
——用于映射不存在根路径下面的模块路径。shims
——配置在脚本/模块外面并无使用RequireJS的函数依赖而且初始化函数。假设underscore并无使用 RequireJS
定义,可是你仍是想经过RequireJS来使用它,那么你就须要在配置中把它定义为一个shimdeps
——加载依赖关系数组require.config({
//默认状况下从这个文件开始拉去取资源
baseUrl:'scripts/app',
//若是你的依赖模块以pb头,会从scripts/pb加载模块。
paths:{
pb:'../pb'
},
// load backbone as a shim,所谓就是将没有采用requirejs方式定义
//模块的东西转变为requirejs模块
shim:{
'backbone':{
deps:['underscore'],
exports:'Backbone'
}
}
});
复制代码
|-js
|-libs
|-require.js
|-modules
|-article.js
|-user.js
|-main.js
|-index.html
复制代码
// user.js文件
// 定义没有依赖的模块
define(function() {
let author = 'Nealyang'
function getAuthor() {
return author.toUpperCase()
}
return { getAuthor } // 暴露模块
})
复制代码
//article.js文件
// 定义有依赖的模块
define(['user'], function(user) {
let name = 'THE LAST TIME'
function consoleMsg() {
console.log(`${name} by ${user.getAuthor()}`);
}
// 暴露模块
return { consoleMsg }
})
复制代码
// main.js
(function() {
require.config({
baseUrl: 'js/', //基本路径 出发点在根目录下
paths: {
//映射: 模块标识名: 路径
article: './modules/article', //此处不能写成article.js,会报错
user: './modules/user'
}
})
require(['article'], function(alerter) {
article.consoleMsg()
})
})()
复制代码
// index.html文件
<!DOCTYPE html>
<html>
<head>
<title>Modular Demo</title>
</head>
<body>
<!-- 引入require.js并指定js主文件的入口 -->
<script data-main="js/main" src="js/libs/require.js"></script>
</body>
</html>
复制代码
若是咱们须要引入第三方库,则须要在 main.js 文件中引入
(function() {
require.config({
baseUrl: 'js/',
paths: {
article: './modules/article',
user: './modules/user',
// 第三方库模块
jquery: './libs/jquery-1.10.1' //注意:写成jQuery会报错
}
})
require(['article'], function(alerter) {
article.consoleMsg()
})
})()
复制代码
关于 require.js 的使用,仔细看文档,其实仍是有不少知识点的。可是鉴于咱们着实如今使用很少(我也不熟),因此这里也就参考网上优秀文章和本身实践,抛砖引玉。
CMD是阿里的玉伯提出来的(大神的成长故事可在公众号回复【大佬】),js 的函数为 sea.js
,它和 AMD 其实很是的类似,文件即为模块,可是其最主要的区别是实现了按需加载。推崇依赖就近的原则,模块延迟执行,而 AMD 所依赖模块式提早执行(requireJS 2.0
后也改成了延迟执行)
//AMD
define(['./a','./b'], function (a, b) {
//依赖一开始就写好
a.test();
b.test();
});
//CMD
define(function (requie, exports, module) {
//依赖能够就近书写
var a = require('./a');
a.test();
...
//按需加载
if (status) {
var b = requie('./b');
b.test();
}
});
复制代码
准确的说 CMD
是 SeaJS
在推广过程当中对模块定义的规范化产物。
也能够说SeaJS
是一个遵循 CMD
规范的 JavaScript
模块加载框架,能够实现 JavaScript 的 CMD 模块化开发方式。
SeaJS
只是实现 JavaScript的模块化和按需加载,并未扩展 JavaScript 语言自己。SeaJS
的主要目的是让开发人员更加专一于代码自己,从繁重的 JavaScript 文件以及对象依赖处理中解放出来。
绝不夸张的说,咱们如今详情页就是 SeaJS+Kissy。。。(即将升级)
Seajs
追求简单、天然的代码书写和组织方式,具备以下核心特性:
Sea.js
遵循 CMD
规范,能够像 Node.js 通常书写模块代码。Sea.js 还提供经常使用插件,很是有助于开发调试和性能优化,并具备丰富的可扩展接口。
examples/
|-- sea-modules 存放 seajs、jquery 等文件,这也是模块的部署目录
|-- static 存放各个项目的 js、css 文件
| |-- hello
| |-- lucky
| `-- todo `-- app 存放 html 等文件
|-- hello.html
|-- lucky.html
`-- todo.html 复制代码
咱们从 hello.html 入手,来瞧瞧使用 Sea.js 如何组织代码。
在 hello.html 页尾,经过 script 引入 sea.js 后,有一段配置代码
// seajs 的简单配置
seajs.config({
base: "../sea-modules/",
alias: {
"jquery": "jquery/jquery/1.10.1/jquery.js"
}
})
// 加载入口模块
seajs.use("../static/hello/src/main")
复制代码
sea.js 在下载完成后,会自动加载入口模块。页面中的代码就这么简单。
这个小游戏有两个模块 spinning.js 和 main.js,遵循统一的写法:
// 全部模块都经过 define 来定义
define(function(require, exports, module) {
// 经过 require 引入依赖
var $ = require('jquery');
var Spinning = require('./spinning');
// 经过 exports 对外提供接口
exports.doSomething = ...
// 或者经过 module.exports 提供整个接口
module.exports = ...
});
复制代码
上面就是 Sea.js 推荐的 CMD 模块书写格式。若是你有使用过 Node.js,一切都很天然。
以上实例,来源于官网 Example。更多 Demo 查看:github.com/seajs/examp…
UMD 其实我我的仍是以为很是。。。。不喜欢的。ifElse
就 universal
了。。。。
UMD
是 AMD
和 CommonJS
的综合产物。如上所说,AMD
的用武之地是浏览器,非阻塞式加载。CommonJS 主要用于服务端 Nodejs 中使用。因此人们就想到了一个通用的模式 UMD
(universal module definition)。来解决跨平台的问题。
没错!就是 ifElse
的写法。
核心思想就是:先判断是否支持Node.js的模块(exports
)是否存在,存在则使用Node.js模块模式。
在判断是否支持AMD(define
是否存在),存在则使用AMD
方式加载模块。
(function (window, factory) {
if (typeof exports === 'object') {
module.exports = factory();
} else if (typeof define === 'function' && define.amd) {
define(factory);
} else {
window.eventUtil = factory();
}
})(this, function () {
//module ...
});
复制代码
关于 UMD 更多的example 可移步github:github.com/umdjs/umd
若是你一直读到如今,那么恭喜你,咱们开始介绍咱们最新的模块化了!
经过上面的介绍咱们知道,要么模块化依赖环境,要么须要引入额外的类库。说到底就是社区找到的一种妥协方案而后获得了你们的承认。可是归根结底不是官方呀。终于,ECMAScript 官宣了模块化的支持,真正的规范。
在ES6中,咱们可使用 import
关键字引入模块,经过 export
关键字导出模块,功能较之于前几个方案更为强大,也是咱们所推崇的,可是因为ES6目前没法在全部浏览器中执行,因此,咱们还需经过babel将不被支持的import
编译为当前受到普遍支持的 require
。
ES6 的模块化汲取了 CommonJS
和AMD
的优势,拥有简洁的语法和异步的支持。而且写法也和 CommonJS 很是的类似。
关于 ES6 模块的基本用法相比你们都比较熟悉了。这里咱们主要和 CommonJS 对比学习。
两大差别:
// lib/counter.js
var counter = 1;
function increment() {
counter++;
}
function decrement() {
counter--;
}
module.exports = {
counter: counter,
increment: increment,
decrement: decrement
};
// src/main.js
var counter = require('../../lib/counter');
counter.increment();
console.log(counter.counter); // 1
复制代码
在 main.js 当中的实例是和本来模块彻底不相干的。这也就解释了为何调用了 counter.increment() 以后仍然返回1。由于咱们引入的 counter 变量和模块里的是两个不一样的实例。
因此调用 counter.increment() 方法只会改变模块中的 counter .想要修改引入的 counter 只有手动一下啦:
counter.counter++;
console.log(counter.counter); // 2
复制代码
而经过 import 语句,能够引入实时只读的模块:
// lib/counter.js
export let counter = 1;
export function increment() {
counter++;
}
export function decrement() {
counter--;
}
// src/main.js
import * as counter from '../../counter';
console.log(counter.counter); // 1
counter.increment();
console.log(counter.counter); // 2
复制代码
由于 CommonJS
加载的是一个对象(module.exports
),对象只有在有脚本运行的时候才能生成。而 ES6 模块不是一个对象,只是一个静态的定义。在代码解析阶段就会生成。
ES6 模块是编译时输出接口,所以有以下2个特色:
// a.js
console.log('a.js')
import { foo } from './b';
// b.js
export let foo = 1;
console.log('b.js 先执行');
// 执行结果:
// b.js 先执行
// a.js
复制代码
// a.js
import { foo } from './b';
console.log('a.js');
export const bar = 1;
export const bar2 = () => {
console.log('bar2');
}
export function bar3() {
console.log('bar3');
}
// b.js
export let foo = 1;
import * as a from './a';
console.log(a);
// 执行结果:
// { bar: undefined, bar2: undefined, bar3: [Function: bar3] }
// a.js
复制代码
“循环加载”(circular dependency)指的是,a脚本的执行依赖b脚本,而b脚本的执行又依赖a脚本。
// a.js
var b = require('b');
// b.js
var a = require('a');
复制代码
循环加载若是处理很差,还可能致使递归加载,使得程序没法执行,所以应该避免出现。
在 CommonJS 中,脚本代码在 require
的时候,就会所有执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。
// a.js
exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 执行完毕');
复制代码
// b.js
exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 执行完毕');
复制代码
// main.js
var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
复制代码
输出结果为:
在 b.js 之中,a.done = false
b.js 执行完毕
在 a.js 之中,b.done = true
a.js 执行完毕
在 main.js 之中, a.done=true, b.done=true
复制代码
从上面咱们能够看出:
b.js
之中,a.js
没有执行完毕,只执行了第一行。main.js
执行到第二行时,不会再次执行b.js
,而是输出缓存的b.js
的执行结果,即它的第四行ES6 处理“循环加载”与 CommonJS 有本质的不一样**。ES6 模块是动态引用**,若是使用import从一个模块加载变量(即import foo from 'foo'),那些变量不会被缓存,而是成为一个指向被加载模块的引用,须要开发者本身保证,真正取值的时候可以取到值。
// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar);
export let foo = 'foo';
// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo);
export let bar = 'bar';
复制代码
运行结果以下:
b.mjs
ReferenceError: foo is not defined
复制代码
上面代码中,执行a.mjs之后会报错,foo变量未定义.
具体的执行结果以下:
解决这个问题的方法,就是让b.mjs运行的时候,foo已经有定义了。这能够经过将foo写成函数来解决。
// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar());
function foo() { return 'foo' }
export {foo};
// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo());
function bar() { return 'bar' }
export {bar};
复制代码
最后执行结果为:
b.mjs
foo
a.mjs
bar
复制代码
关于 ES6 详细的模块的介绍,强烈推荐阮一峰的 ES6 入门和深刻理解 ES6 一书
公众号【全栈前端精选】 | 我的微信【is_Nealyang】 |
---|---|
![]() |
![]() |