序言:相信大家在写javascript代码的时候,或多或少都听过CommonJS, AMD, RequireJs, ES6,不过肯定没有怎么深入了解各个概念的区别,在此我给大家详细介绍其原理和区别,方便大家不会再听到这些概念的时候处于懵懵的状态。
Javascript—— 需要解决的问题
JavaScript的发展史有20多年,为什么近几年javascript特别的火,各种框架如春笋般爆发,大把程序员高呼学不动了的情况出现。其中一个比较重要的原因我觉得就是NodeJS的出现,并且引出了CommonJs制定的规范。
在10年前,jquery特别盛行的时候,相信大家在运用一个基于jquery的library(包)的时候,必然会遇到一个问题,就是一定要把该包的引用放在jquery之后。 例如下方代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Home</title>
</head>
<body>
<script src='/jquery.min.js'></script> <!-- !important -->
<script src='/bootstrap.min.js'></script> <!-- !important -->
</body>
</html>
如果我把bootstrap.min.js的引用放到了jquery的前面,那么就会发生$ is undefined的错误。 为什么呢? 下面我用一套简单的代码原理给大家解释一下:
1. IIFE (Immediately invoked function expression)
(function(){
var name = '1+'
})() // 立即执行函数
定义立即函数的其中一个重要目的是,当我在函数里面定义任何变量,都不会污染全局变量,并且不会被随意篡改。
// library A - wrong way
var name = '1+';
(function() {
setTimeout(function(){
console.log(name) // print 1-
})
})();
// library B - wrong way
var name = '1-';
(function(){
setTimeout(function(){
console.log(name) // print 1-
})
})();
从上面的代码中,library-A 和library-B分别定义了name并且值不一样,当我在立即执行函数中使用它的时候,发现最终值打印的都是1-,这就是name被污染了。而下面的代码就不会出现上述的情况
// library A - right way
(function() {
var name = '1+';
setTimeout(function(){
console.log(name) // print 1+
})
})();
// library B - right way
(function(){
var name = '1-';
setTimeout(function(){
console.log(name) // print 1-
})
})();
理解了IIFE的作用后,就可以解释下jquery和bootstrap的引用为什么必须要按顺序了,请看如下简易代码:
// fake-jquery
(function () {
window.$ = function (element){
console.log('This is a jquery' + element)
}
})();
// fake-bootstrap
(function () {
console.log(window.$('#id'))
})();
相信大家应该知道为什么jquery必须要放到bootstrap之前了,因为jquery把其接口定义到了window上面,而bootstrap就是使用window.$的,如果不先执行jquery的library,那么bootstrap肯定会报错。
随着这种方式的使用,大家发现了几个棘手的问题,第一个,我要引用库的时候,如果有依赖,就必须按顺序加载。第二个毕竟程序员的创造力是强大的,很有可能两个library的命名冲突,如果都放到window上面就会出现覆盖的问题。
发现上面的问题后,就需要方法去解决,这个时候NodeJS横空出世,使用了一套新的模块化规范,那就是CommonJS
CommonJS—— 服务端的模块化
CommonJS主要用于服务端或者桌面应用,如NodeJS和Electron。而其不使用于浏览器中。简单介绍下其使用方法:
// bar.js
exports.name = 'This is a bar'
// foo.js
exports.name = 'This is a foo'
// index.js
console.log(require.main.children.length) // 0
const bar = require('./bar')
const foo = require('./foo')
console.log(require.main.children.length) // 2
console.log(bar.name,foo.name) // This is a bar This is a foo
CommonJs的核心之一就是exports和require,它使用了一套自己的处理机制,将require的module绑定到require object上,这样当我要再次使用的时候就可以直接从require的main里找,有的话就不需要重新引用了。
AMD—— Asynchronous Module Definition
有了服务端的CommonJs,那么自然在web浏览器上我们也要实现该功能,这个时候,就有了AMD的出现,异步模块化定义,其原理可以简单的用以下函数表示:
define(id?, dependencies?, factory);.
定义一个以id为名的module,然后可以使用引用其需要的依赖dependencies,当我的依赖全部加载完毕后即可执行factory方法。
根据以上的定义方式,我们可以无需担心任何引用的顺序,以及互相依赖的问题,因为只有dependencies全部加载完毕后才会执行我的factory里面的方法,从而避免报错。
将上述理论运用起来的有以下几个库
- RequireJs
- Curl
- Lsjs
- Dojo 1.7+
而本次我来介绍一下比较火的RequireJs
RequireJs
主要该库的使用方法如下
1. 在HTML中进行引用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Require JS</title>
<script data-main='index' src='require.js'></script>
</head>
<body>
</body>
</html>
在引用完毕require.js后,我们需要使用主导入文件index.js,相关代码如下:
// index.js
require.config({
paths: {
fooRename: 'foo',
bar: 'bar'
}
}); // 定义各个依赖的文件路径,可以是本地文件也可以是线上文件
// 引用依赖,并且执行相关方法
require(['bar', 'fooRename'], function (bar, foo) {
console.log(foo.name, bar.name) // This is foo This is bar
})
// foo.js
define({
name: 'This is foo'
})
// bar.js
define({
name: 'This is bar'
})
那么以上就是require.js简单的使用方法了,如果大家想更深入的去使用,可以参考其官方网站 https://requirejs.org/
了解了其使用方法后,我们得看看源码,它到底是怎么实现的。
主要是两个方法:
1. 异步加载js文件
通过create script element并且设置其url后,插入到document的头部中,当加载成功完毕后,会执行一段代码从而告知requireJs已经加载完成,你可以执行factory里面的方法了
2. 轮询和绑定已加载的dependency
我们会将已加载的dependency绑定到requirejs的object中,并且如果要加载新的dependency的时候,我们会开启一个轮询,查看是否加载完毕,如果超时那么就会阻止继续加载,并且提示错误信息。
ES6 —— Webpack
ES6是编写javascript的一种新的规范,它通过import 和 export 方式解决了模块化的问题,所以也受广大开发者的喜爱。但是现代浏览器并不完全兼容ES6的大部分语法,那么这个时候就有相关的打包工具将其代码转化为大部分浏览器能够运行的代码。
首先给大家简单介绍下ES6的import和export写法,如下:
// app.js
import foo from './foo';
import bar from './bar';
console.log(bar, foo);
// bar.js
export default 'This is a bar'
// foo.js
export default 'This is a foo'
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: './app.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'main.bundle.js'
},
plugins: [
new HtmlWebpackPlugin({ template: './index.html' })
]
};
webpack解析根文件 app.js 然后将其编译成,接下来我来介绍其简易代码:
(function(modules){
// handle modules in here
})(
{
'./app.js': (function(){eval('xxxx')})
},
{
'./bar.js': (function(){eval('xxxx')})
},
{
'./foo.js': (function(){eval('xxxx')})
}
)
从上述简化后的代码可以看出,webpack就是定义了一个IIFE方法,然后传入的modules参数是由我定义的三个文件,以文件名为属性,以其可执行代码为值来实现的。
以上就是webpack处理es6的方式。当然这个是将所有文件都导入到一个main.bundle文件中,当我们文件越来越多的时候,main.bundle就会非常大,这个时候就需要用到按需加载,其代码如下:
// app.js
import('./foo').then((foo) => console.log(foo))
import('./bar').then((bar) => console.log(bar))
其编译后的原理和require.js类似,即创建script的node然后异步加载进html内中,根据监听的事件来实现then以后的方法。
综上所述:以上各个概念都是为了解决javascript模块化的问题,而大部分的原理也比较简单,就是通过create node并插入到html头部后监听其完成的事件,从而执行factory方法。而复杂的部分就在于如何缓存处理和处理加载后的问题了。
鸣谢:我是一名来自盛安德的Shinetecher,感谢盛安德公司及同事们对IT技术的支持,分享和热情,让我有时间和动力完成此博文。
联系:欢迎各位朋友有任何问题和建议留言至此博客下,或者邮件联系:liyijia428@126.com 进行沟通交流学习