第二章 - 04: Javascript 模块化管理的来世今生
模块管理这个概念其实在前几年前端度过了刀耕火种年代之后就一直被提起,那么我们有思考过这个模块管理具体指的是什么东西?什么样子的展现形式?历史由来?现在是什么样的一个状态?
直接回想起来的就是 cmd amd commonJS 这三大模块管理的印象。但是大家清楚 cmd amd commonJS 为什么会出现么?接下来,我们就一起来瞅瞅这具体是啥情况。
感觉自己在每一个阶段,对于同一个技术的理解都不一样。
一、什么是模块化开发
为了让一整个庞大的项目看起来整整齐齐,规规整整而出现的模块化管理,我们常见的三种模块化管理的库: requireJS、seaJS、commonJS规范 ( 注意,这里的commonJS不是一个库,而是一种规范) 逐渐的在我们日常的开发中出现。 同时依赖于 这三类规范而出现的一些构建工具,但最后都败给了 webpack 。这是一篇介绍 webpack 基本入门的文章,可以结合着这篇文章来进行解读。
《前端之路》之 webpack 4.0+ 的应用构建
1-1、模块化第一阶段
在这个阶段中,我们常常会把非常多复杂的功能 封装成一个个的函数:
function f1() { // todo}function f2() { // todo} . . .
但是当 整个项目变大了以后,就会遇到很多问题,都是定义的全局变量,形成了比较严重的污染,还有可能会出现因为重命名导致的各种问题。所以这些是需要进化的。所以就会进入到模块化的第二阶段: 对象。
1-2、封装到对象
到了第二个阶段为了避免全局变量的污染,我们会将单个模块封装到一个对象内部。如下:
const module = { let _number = 10, f1: () => { console.log(123) }, f2: () => { console.log(456) }, . . . }
这样我们就没个模块定义一个对象,在需要的时候直接调用就好了,但是这样也会存在一个问题
这样写的话会暴露全部的对象内部的属性,内部状态可以被外部改变
. 例如:
module._number = 100
如果我们支付相关模块这样子来写的话。我们随意的来改变支付的金额,那样就会出现比较危险的情况。
1-3、 对象的优化
后来,聪明的人类就想到了利用 立即执行函数 来达到 不暴露私有成员的目的
const module2 = (function() { let _money = 100 const m1 = () => { console.log(123) } const m2 = () => { console.log(456) } return { f1: m1, f2: m2 } })()
通过立即执行函数,让外部根本没有时间从外部去修改内部的属性,从而达到一定的防御作用。
以上就是模块化开发的基础中的基础。 没有库,没有规范,一切的维护都是靠人力,一切的创新,都是来源于 解放生产力。
二、模块化管理的发展历程
2-1、CommonJS
CommonJS 的出发点: JS 没有完善的模块系统,标准库较少,缺少包管理工具。(虽然这些东西,在后端语言中已经是 早就被玩的不想玩的东西了)
伴随着 NodeJS 的兴起,能让 JS 可以在任何地方运行,特别是服务端,以达到 也具备 Java、C#、PHP这些后台语言具备开发大型应用的能力,所以 CommonJS 应运而生。
2-1-1、 CommonJS常见规范
一个文件就是一个模块,拥有单独的作用域
普通方式定义的 变量、函数、对象都属于该模块内
通过
require
来家在模块通过
exports
和module.exports
来暴露模块中的内容
我们通过编写一个 Demo 来尝试写这个规范
Demo 1 : 通过 module.exports 来导出模块
// module.jsmodule.exports = { name: "zhang", getName: function() { console.log(this.name); }, changeName: function(n) { this.name = n; } };// index.jsconst module = require("./module/index");console.log(module) // {name: "zhang", getName: ƒ, changeName: ƒ} "commons"
Demo 2 : 通过 exports 来导出模块
// module1.jsconst getParam = () => { console.log(a); };let a = 123;let b = 456; exports.a = a; exports.b = b; exports.getParam = getParam;// index.jsconst module1 = require("./module/index1"); consoel.log(module1, "commons1") // {a: 123, b: 456, getParam: ƒ} "commons1"
Demo 3 : 同时存在 exports 和 module.exports 来导出模块
// module2.jslet a = 123;const getSome = () => { console.log("yyy"); };const getA = () => { console.log(a); }; exports.getSome = getSome;module.exports = getA;// index.jsconst module2 = require("./module/index2"); consoel.log(module2, "commons2") // function getA() {...}
总结 : 通过这样的一个对比的例子就可以比较清晰的对比出 exports 和 module.exports 的区别:
1、当 exports 和 module.exports 同时存在的时候,module.exports 会盖过 exports
2、当模块内部全部是 exports 的时候, 就等同于 module.exports
3、最后 我们就可以认定为 exports 其实就是 module.exports 的子集。
以上就是我们对于 CommonJS 的一个初级的介绍。
还有一个硬性的规范,这里我们只是做一下列举,就不做详细的 Demo 演示了。
2-1-2、 CommonJS 规范 --- 加载、作用域
所有代码都运行在模块作用域,不会污染全局作用域;模块可以多次加载,但只会在第一次加载的时候运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果;模块的加载顺序,按照代码的出现顺序是同步加载的;
2-1-3、 CommonJS 规范 --- __dirname、__filename
__dirname代表当前模块文件所在的文件夹路径,__filename代表当前模块文件所在的文件夹路径+文件名;
以上就是关于 CommonJS 规范 相关的介绍,更下详细的 文档,可以查阅 CommonJS 规范 官方文档。
2-2、CommonJS 与 ES6(ES2015) 的 import export
在 ES2015 标准为出来之前,最主要的是CommonJS和AMD规范。上文中我们已经介绍了 CommonJS 规范(主要是为了服务端 NodeJS 服务),那么当 ES6标准的出现,为浏览器端 模块化做了一个非常好的补充。
这里,我们还是比较详细的介绍下 ES6 的 import export 的系列特性。
2-2-1、 ES6 的 export
export用于对外输出本模块(一个文件可以理解为一个模块)变量的接口
Demo 1 export { xxx }
// export/index.jsconst a = "123";const fn = () => window.location.href;export { fn };// show/index.jsconst ex = require("./export/index");import x from "./export/index";import { fn } from "./export/index";console.log(ex, "export1"); // {fn: ƒ, __esModule: true} "export1"console.log(x, "export-x"); // undefined "export-x"console.log(fn, "export-fn"); // function() { return window.location.href; } "export-x"
Demo 2 export default xxx
// export/index1.jsconst a = "123";const fn = () => window.location.href;export default fn;// show/index1.jsconst ex1 = require("./export/index1");import x from "./export/index1";console.log(ex1, "export1"); // {default: ƒ, __esModule: true} "export1"console.log(x, "export2"); // ƒ fn() {return window.location.href;} "export2"
通过 Demo 1 和 Demo 2 我们可以很好的 对比了下 export 和 export default 的区别
export
可以导出的是一个对象中包含的多个 属性,方法。export default
只能导出 一个 可以不具名的 对象。
import {fn} from './xxx/xxx'
( export 导出方式的 引用方式 )import fn from './xxx/xxx1'
( export default 导出方式的 引用方式 )
同时,我们发现 可以直接使用 require 来引用
这个也能引用其实有点神奇的。但是功能和 import 一样。(原因就是我这里起了 webpack 的 server 相关)
2-2-2、 ES6 的 import
这里就同上面 Demo 中的例子一样了得出的结论就是
import {fn} from './xxx/xxx'
( export 导出方式的 引用方式 )import fn from './xxx/xxx1'
( export default 导出方式的 引用方式 ,可以不用在意导出模块的模块名)
总结: 之前对于 模块的导出、引用 的概念都比较的魔幻,这次通过知识的梳理终于搞清楚了。
Demo 例子传送门: 模块化》》》
2-3、AMD 的 RequireJS
Asynchronous Module Definition,中文名是异步模块。它是一个在浏览器端模块化开发的规范,由于不是js原生支持,使用AMD规范进行页面开发需要用到对应的函数库,也就是大名鼎鼎的RequireJS,实际上AMD是RequireJS在推广过程中对模块定义的规范化的产出。
requireJS主要解决两个问题:
1、 多个js文件可能有依赖关系,被依赖的文件需要早于依赖它的文件加载到浏览器。
2、 js加载的时候浏览器会停止页面渲染,加载文件愈多,页面失去响应的时间愈长。
3、 异步前置加载!(什么意思? 后面我们在原理那个章节进行介绍)
我们通过一个 Demo 来介绍下 RequireJS 的语法:
// 定义模块define(['myModule'],() => { var name = 'Byron'; function printName(){ console.log(name); } return { printName:printName } })// 加载模块require(['myModule'],function(my){ my.printName(); })
requireJS 语法:
define(id,dependencies,factory)
——id
可选参数,用来定义模块的标识,如果没有提供该参数,脚本文件名(去掉拓展名)
——dependencies
是一个当前模块用来的模块名称数组
——factory
工厂方法,模块初始化要执行的函数或对象,如果为函数,它应该只被执行一次,如果是对象,此对象应该为模块的输出值。
在页面上使用require函数加载模块;require([dependencies], function(){});
require()
函数接受两个参数:
——第一个参数是一个数组
,表示所依赖的模块
;
——第二个参数是一个回调函数
,当前面指定的模块都加载成功后,它将被调用
。加载的模块会以参数形式传入该函数,从而在回调函数内部就可以使用这些模块
2-4、CMD 的 SaeJS
define(id, deps, factory) 因为CMD推崇一个文件一个模块,所以经常就用文件名作为模块id; CMD推崇依赖就近,所以一般不在define的参数中写依赖,而是在factory中写。 factory有三个参数:function(require, exports, module){} 一,requirerequire 是 factory 函数的第一个参数,require 是一个方法,接受 模块标识 作为唯一参数,用来获取其他模块提供的接口; 二,exports exports 是一个对象,用来向外提供模块接口; 三,modulemodule 是一个对象,上面存储了与当前模块相关联的一些属性和方法。 demo// 定义模块 myModule.jsdefine(function(require, exports, module) { var $ = require('jquery.js') $('div').addClass('active'); });// 加载模块seajs.use(['myModule.js'], function(my){ });
2-5、AMD 的 RequireJS 和 CMD 的 SaeJS 的差异
AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块.
CMD推崇就近依赖,只有在用到某个模块的时候再去require.
大家说:
AMD和CMD最大的区别是对依赖模块的执行时机处理不同,注意不是加载的时机或者方式不同
很多人说requireJS是异步加载模块,SeaJS是同步加载模块,这么理解实际上是不准确的,其实加载模块都是异步的,只不过AMD依赖前置,js可以方便知道依赖模块是谁,立即加载,而CMD就近依赖,需要使用把模块变为字符串解析一遍才知道依赖了那些模块,这也是很多人诟病CMD的一点,牺牲性能来带来开发的便利性,实际上解析模块用的时间短到可以忽略。
三、模块化框架原理
3-1、 实现原理
(function(global){ var modules = {}; var define = function (id,factory) { if(!modules[id]){ modules[id] = { id : id, factory : factory }; } }; var require = function (id) { var module = modules[id]; if(!module){ return; } if(!module.exports){ module.exports = {}; module.factory.call(module.exports,require,module.exports,module); } return module.exports; } global.define = define; global.require = require; })(this);
使用实例:
define('Hello',function(require,exports,module){ function sayHello() { console.log('hello modules'); } module.exports = { sayHello : sayHello } });var Hello = require('Hello'); Hello.sayHello();
四、总结
4-1、 为什么会有这个东西?
方便组织你的代码,提高项目的可维护性。一个项目的可维护性高不高,也体现一个程序员的水平,在如今越来越复杂的前端项目,这一点尤为重要。
4-2、 为什么不用requirejs,seajs等
它们功能强大,但是文件体积是个问题,此外还有就是业务有时候可能没那么复杂。
4-3、 适用场景
移动端页面,将js注入到html页面,这样就不用考虑模块加载的问题,从而节省了很多的代码,在实现上也更为的简单。
如果是多文件加载的话,需要手动执行文件加载顺序,那么其实最好用库来进行依赖管理会好一点。
4-4、 现实情况
webpack + commonJS + ES6 (import + export )
这样来 实现模块管理,实现 较大项目的管理。 好了,模块化管理就先介绍到这里了,欢迎一起探讨