最近想重新系统地整理一下前端的知识,因此写了这个专栏。我会尽量写一些业务相关的小技巧和前端知识中的重点内容,核心思想。
前言
工程化是前端开发者进阶的一个大标志,我们会开始研究开发构建,构建优化,已经服务开发等领域。而工程化的这最早代表,就是模块化的诞生。今天我们就来探讨一下前端模块化。
模块化的由来
我们都知道前端页面由HTML,CSS和JavaScript组成。随着页面的功能逐渐强大,页面逻辑越来越复杂。JavaScript代码就不可避免的变得臃肿起来。而早期为了分割js代码,开发者会把js分成几个不同的文件,通过scritpt标签引入。
// 的同时我们需要把引入的工具函数挂载到页面上。
(function(window){
function tool(){
// ... do something
}
window.tool = tool;
})(window)
观察上方的代码我们可以看到最早的引用js的时候我们会有2个考虑:
- 为了在不污染全局变量,js内容本身需要用闭包包起来。
- 由于工具需要在页面上引用,所以要把window传入闭包然后在内部把js挂载。
不难发现,通过这种方式引入js。开发者会面临一个问题,就是这些js工具之间的依赖关系无法控制。如果一个页面一共用到了10个js工具,这些工具之类又需要相互引用。这样的话在html引用script标签的时候需要做好充分的考虑安排。
为了解决这个问题,前端开发者走上了模块化的探索道路。
AMD
【AMD】(Asynchromous Module Definition)规范可以说是最早被广泛使用的前端模块化方案。它是由require.js所提出实现的。更多关于require.js的内容可以查阅官网:requirejs.org/
先来看一下它的用法:
// 在入口文件配置整个项目用到的工具
require.config({
baseUrl: "js/lib",
paths: {
"jquery": "jquery.min", //实际路径为js/lib/jquery.min.js
"underscore": "underscore.min",
}
});
// 在定义工具时,需要用提供的define函数
define('myModule',['jquery','lodash'],function($,_){
return {
// 这里可以用$引用jq,并不需要考虑依赖的引用顺序。
getInputValue:function(){
var input = $('#input');
return input.val();
}
}
})
观察上方代码,我们可以了解到AMD的思路:
- 有一个项目整体的配置,主要是确认工具的别名和路径。
- 当需要定义新工具的时候,我们需要用require.js提供的define函数,传入需要定义的工具的别名。依赖的工具,以及工具本身的内容。
- 这样在工具中就可以直接使用依赖工具了。
由于AMD的加载是异步操作,不会阻塞同步代码。等工具依赖的内容加载完了,才会触发自身的回调。这样我们可以不用担心依赖之间的引用顺序问题了。
commonJS
commonJS是nodeJs默认的模块方式。从时间点上来说它跟AMD是同期的作品,但commonJS更多的是用在nodeJS上,并没有迅速地改变前端开发。
在nodeJs的思想中,每个文件都是一个模块。模块之间通过require来引用,模块自身用module.exports导出。nodeJs的模块是在启动的时候就全部一起加载,并不需要在执行过程中加载,这一点跟浏览器有点区别。
用法:
// a.js
var printSolgan = function(){
console.log('hello world');
}
module.exports = printSolgan;
//b.js
var printSolgan = require('./a.js');
printSolgan(); // hello world
可以看到作为nodeJs自带的模块方案,commonJS在使用已经十分便捷了。nodejs天然地把每个文件都当作是一个模块,省去了一些闭包之类的额外处理。与AMD不同的是commonJS的引用是同步操作,在首次引用时会执行模块,当第二次引用时,nodejs会把从缓存里拿出文件,并不会二次执行。
CMD
【CMD】(Common Module Definition)规范可以说是AMD的一种优化,在commonJs规范出来之后,前端开发者不得不记住2种不同的引用方式(浏览器,与nodeJs环境)。如果可以让同一种写法兼容2个环境,对前端开发者来说会是一个很大的帮助。而淘宝的前端团队推出的sea.js 就解决这个问题(后来require.js也把他整合兼容了)。
更多关于sea.js的内容可以查阅:seajs.github.io/seajs/docs
用法:
define(function(require,exports,module){
var $ = require('jqurey');
var getInputValue = function(){
var input = $('#input');
return input.val();
}
module.exports = getInputValue;
})
通过上述代码可以看出,CMD自己实现了require,module等nodejs的原生内容,当然模块的定义还是需要调用define函数,函数内的部分就跟nodeJs环境很相似了。与AMD最大的不同是,AMD需要在定义的函数执行前,把用到的依赖全部加载完之后才执行。而CMD则是通过require动态引用。也就是说没有被执行执行到的代码,CMD是可以避免那部分的引用的。
// AMD
define(["a", "b"], function(a, b) {
// 只要是定义了的依赖,就会执行
a.doSomething();
if (false) {
// 即便没用到b,但b已经在前面执行了
b.doSomething()
}
});
/** CMD **/
define(function(require, exports, module) {
// 在使用的时候执行
var a = require('./a');
a.doSomething();
if (false) {
// 不会执行
var b = require('./b');
b.doSomething();
}
});
ES6 Modules
ES模块方式是在ES 2015版本后推出的官方模块化方案,也是现在使用最广的方式。包括nodeJs环境中也很常用babel,或者typescript编译等方式来写ES模块。
用法:
// a.js
var printSolgan = function(){
console.log('hello world');
}
export default printSolgan;
//b.js
import printSolgan from './a.js';
printSolgan();
// hello world
可以看出ES Module 是使用import export 来导入导出模块的。ES6的模块不是对象,import命令会被 JavaScript 引擎静态分析,在编译时就引入模块代码,而不是在代码运行时加载,所以无法实现条件加载。也正因为这个,使得静态分析成为可能。
ES6 模块的特征:
- 严格模式:ES6 的模块自动采用严格模式
- import read-only特性: import的属性是只读的,不能赋值,类似于const的特性
- export/import提升: import/export必须位于模块顶级,不能位于作用域内;其次对于模块内的import/export会提升到模块顶部,这是在编译阶段完成的
浏览器中使用
值得注意的是在浏览器中使用es module时,需要在script标签中加上'type="module"'
<script type="module">
var foo = 100;
console.log(foo);//100
</script>
UMD模块类型兼容
到了ES6 moudule之后,其实前端模块化可以说已经趋向稳定了。大多数新开发的项目都会首选用ES模块,包括一些早期开发的工具库也会更新es模块可以使用的新版本。开发者们现在面临的问题是,如何处理过去那么多不同的模块方案。在编写一个工具库的时候,开发者无法知道使用者的项目用的是哪种模块方案。为此我们需要一个兼容所有模块的方式。那就是UMD。
所谓【UMD】 (Universal Module Definition),就是一种javascript通用模块定义规范,让你的模块能在javascript所有运行环境中发挥作用。那么他是怎么实现的呢?
实现
我们需要先明确,UMD是一个在定义函数的时候的规范,而不是引用的时候的操作。所以UMD不是一个新的模块方案,它只是一个兼容方案。使用了UMD之后,函数大致长这样:
// root: 全局变量
// factory: 函数内容
(function(root, factory) {
// 如果当前环境有module 和 module.exports 说明可以用commonJS
if (typeof module === 'object' && typeof module.exports === 'object') {
module.exports = factory();
// 如果当前环境有define 和 define.amd 说明可以用commonJS
} else if (typeof define === 'function' && define.amd) {
define(factory())
// 如果当前环境有define 和 define.cmd 说明可以用commonJS
} else if (typeof define === 'function' && define.cmd) {
define(function(require, exports, module) {
module.exports = factory()
})
} else {
// 没有模块环境,直接挂载在全局对象上
root.umdModule = factory();
}
// this会根据运行环境而定,浏览器是window,nodejs是global
}(this, function() {
// 具体函数内容
return {
name: '我是一个umd模块'
}
}))
通过示例我们可以看出,UMD的思路就是根据各种模块的特别,来判断当前运行环境用的是什么模块,再用对应的模块加载函数。目前大部分的主流工具库都会用umd格式文件,一般以 umd.js 结尾。
有兴趣的朋友可以看一下jquery库,其实也用了umd兼容。jquery 3.6的CDN地址:cdn.bootcdn.net/ajax/libs/j…