Javascript 模块化管理的来世此生

Javascript 模块化管理的来世此生javascript

[TOC]html

模块管理这个概念其实在前几年前端度过了刀耕火种年代以后就一直被提起,那么咱们有思考过这个模块管理具体指的是什么东西?什么样子的展示形式?历史由来?如今是什么样的一个状态?前端

直接回想起来的就是 cmd amd commonJS 这三大模块管理的印象。可是你们清楚 cmd amd commonJS 为何会出现么?接下来,咱们就一块儿来瞅瞅这具体是啥状况。java

感受本身在每个阶段,对于同一个技术的理解都不同。jquery

1、什么是模块化开发

为了让一整个庞大的项目看起来整整齐齐,规规整整而出现的模块化管理,咱们常见的三种模块化管理的库: requireJS、seaJS、commonJS规范 ( 注意,这里的commonJS不是一个库,而是一种规范) 逐渐的在咱们平常的开发中出现。 同时依赖于 这三类规范而出现的一些构建工具,但最后都败给了 webpack 。这是一篇介绍 webpack 基本入门的文章,能够结合着这篇文章来进行解读。 《前端之路》之 webpack 4.0+ 的应用构建 webpack

1-一、模块化第一阶段

在这个阶段中,咱们经常会把很是多复杂的功能 封装成一个个的函数:git

function f1() {
	// todo
}

function f2() {
	// todo
}
.
.
.
复制代码

可是当 整个项目变大了之后,就会遇到不少问题,都是定义的全局变量,造成了比较严重的污染,还有可能会出现由于重命名致使的各类问题。因此这些是须要进化的。因此就会进入到模块化的第二阶段: 对象。程序员

1-二、封装到对象

到了第二个阶段为了不全局变量的污染,咱们会将单个模块封装到一个对象内部。以下:github

const module = {
	_number: 10,
	f1: () => { 
		console.log(123)
	},
	f2: () => { 
		console.log(456)
	},
	.
	.
	.
}
复制代码

这样咱们就没个模块定义一个对象,在须要的时候直接调用就行了,可是这样也会存在一个问题 这样写的话会暴露所有的对象内部的属性,内部状态能够被外部改变. 例如:web

module._number = 100
复制代码

若是咱们支付相关模块这样子来写的话。咱们随意的来改变支付的金额,那样就会出现比较危险的状况。

1-三、 对象的优化

后来,聪明的人类就想到了利用 当即执行函数 来达到 不暴露私有成员的目的

const module2 = (function() {
	let _money = 100
	const m1 = () => {
		console.log(123)
	}
	const m2 = () => {
		console.log(456)
	}
	return {
		f1: m1,
		f2: m2
	}
})()
复制代码

经过当即执行函数,让外部根本没有时间从外部去修改内部的属性,从而达到必定的防护做用。

以上就是模块化开发的基础中的基础。 没有库,没有规范,一切的维护都是靠人力,一切的创新,都是来源于 解放生产力。

2、模块化管理的发展历程

2-一、CommonJS

CommonJS 的出发点: JS 没有完善的模块系统,标准库较少,缺乏包管理工具。(虽然这些东西,在后端语言中已是 早就被玩的不想玩的东西了) 伴随着 NodeJS 的兴起,能让 JS 能够在任何地方运行,特别是服务端,以达到 也具有 Java、C#、PHP这些后台语言具有开发大型应用的能力,因此 CommonJS 应运而生。

2-1-一、 CommonJS常见规范

  • 一个文件就是一个模块,拥有单独的做用域
  • 普通方式定义的 变量、函数、对象都属于该模块内
  • 经过 require 来加载模块
  • 经过 exportsmodule.exports 来暴露模块中的内容

咱们经过编写一个 Demo 来尝试写这个规范

Demo 1 : 经过 module.exports 来导出模块

// module.js
module.exports = {
  name: "zhang",
  getName: function() {
    console.log(this.name);
  },
  changeName: function(n) {
    this.name = n;
  }
};

// index.js
const module = require("./module/index");
console.log(module)	// {name: "zhang", getName: ƒ, changeName: ƒ} "commons"
复制代码

Demo 2 : 经过 exports 来导出模块

// module1.js
const getParam = () => {
  console.log(a);
};
let a = 123;
let b = 456;

exports.a = a;
exports.b = b;
exports.getParam = getParam;

// index.js
const module1 = require("./module/index1");
consoel.log(module1, "commons1")	// {a: 123, b: 456, getParam: ƒ} "commons1"
复制代码

Demo 3 : 同时存在 exports 和 module.exports 来导出模块

// module2.js
let a = 123;

const getSome = () => {
  console.log("yyy");
};

const getA = () => {
  console.log(a);
};

exports.getSome = getSome;
module.exports = getA;

// index.js
const module2 = require("./module/index2");
consoel.log(module2, "commons2")	// function getA() {...}
复制代码

总结 : 经过这样的一个对比的例子就能够比较清晰的对比出 exports 和 module.exports 的区别: 一、当 exports 和 module.exports 同时存在的时候,module.exports 会盖过 exports 二、当模块内部所有是 exports 的时候, 就等同于 module.exports 三、最后 咱们就能够认定为 exports 其实就是 module.exports 的子集。

以上就是咱们对于 CommonJS 的一个初级的介绍。 还有一个硬性的规范,这里咱们只是作一下列举,就不作详细的 Demo 演示了。

2-1-二、 CommonJS 规范 --- 加载、做用域

全部代码都运行在模块做用域,不会污染全局做用域;模块能够屡次加载,但只会在第一次加载的时候运行一次,而后运行结果就被缓存了,之后再加载,就直接读取缓存结果;模块的加载顺序,按照代码的出现顺序是同步加载的;

2-1-三、 CommonJS 规范 --- __dirname、__filename

__dirname表明当前模块文件所在的文件夹路径,__filename表明当前模块文件所在的文件夹路径+文件名;

以上就是关于 CommonJS 规范 相关的介绍,更下详细的 文档,能够查阅 CommonJS 规范 官方文档。

2-二、CommonJS 与 ES6(ES2015) 的 import export

在 ES2015 标准为出来以前,最主要的是CommonJS和AMD规范。上文中咱们已经介绍了 CommonJS 规范(主要是为了服务端 NodeJS 服务),那么当 ES6标准的出现,为浏览器端 模块化作了一个很是好的补充。

这里,咱们仍是比较详细的介绍下 ES6 的 import export 的系列特性。

2-2-一、 ES6 的 export

export用于对外输出本模块(一个文件能够理解为一个模块)变量的接口

Demo 1 export { xxx }

// export/index.js
const a = "123";
const fn = () => window.location.href;

export { fn };

// show/index.js
const 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.js
const a = "123";
const fn = () => window.location.href;
export default fn;


// show/index1.js
const 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-二、 ES6 的 import

这里就同上面 Demo 中的例子同样了得出的结论就是

import {fn} from './xxx/xxx' ( export 导出方式的 引用方式 ) import fn from './xxx/xxx1' ( export default 导出方式的 引用方式 ,能够不用在乎导出模块的模块名)

总结: 以前对于 模块的导出、引用 的概念都比较的魔幻,此次经过知识的梳理终于搞清楚了。😄

Demo 例子传送门: 模块化》》》

2-三、AMD 的 RequireJS

Asynchronous Module Definition,中文名是异步模块。它是一个在浏览器端模块化开发的规范,因为不是js原生支持,使用AMD规范进行页面开发须要用到对应的函数库,也就是大名鼎鼎的RequireJS,实际上AMD是RequireJS在推广过程当中对模块定义的规范化的产出。


requireJS主要解决两个问题:

  • 一、 多个js文件可能有依赖关系,被依赖的文件须要早于依赖它的文件加载到浏览器。
  • 二、 js加载的时候浏览器会中止页面渲染,加载文件愈多,页面失去响应的时间愈长。
  • 三、 异步前置加载!(什么意思? 后面咱们在原理那个章节进行介绍)

咱们经过一个 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-四、CMD 的 SeaJS

define(id, deps, factory)

由于CMD推崇一个文件一个模块,因此常常就用文件名做为模块id;
CMD推崇依赖就近,因此通常不在define的参数中写依赖,而是在factory中写。

factory有三个参数:
function(require, exports, module){}

一,require
require 是 factory 函数的第一个参数,require 是一个方法,接受 模块标识 做为惟一参数,用来获取其余模块提供的接口;

二,exports
exports 是一个对象,用来向外提供模块接口;

三,module
module 是一个对象,上面存储了与当前模块相关联的一些属性和方法。

demo
// 定义模块 myModule.js
define(function(require, exports, module) {
  var $ = require('jquery.js')
  $('div').addClass('active');
});

// 加载模块
seajs.use(['myModule.js'], function(my){

});
复制代码

2-五、AMD 的 RequireJS 和 CMD 的 SeaJS 的差别

AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块. CMD推崇就近依赖,只有在用到某个模块的时候再去require.


你们说:

AMD和CMD最大的区别是对依赖模块的执行时机处理不一样,注意不是加载的时机或者方式不一样

不少人说requireJS是异步加载模块,SeaJS是同步加载模块,这么理解其实是不许确的,其实加载模块都是异步的,只不过AMD依赖前置,js能够方便知道依赖模块是谁,当即加载,而CMD就近依赖,须要使用把模块变为字符串解析一遍才知道依赖了那些模块,这也是不少人诟病CMD的一点,牺牲性能来带来开发的便利性,实际上解析模块用的时间短到能够忽略。

3、模块化框架原理

3-一、 实现原理

(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、总结

4-一、 为何会有这个东西?

方便组织你的代码,提升项目的可维护性。一个项目的可维护性高不高,也体现一个程序员的水平,在现在愈来愈复杂的前端项目,这一点尤其重要。

4-二、 为何不用requirejs,seajs等

它们功能强大,可是文件体积是个问题,此外还有就是业务有时候可能没那么复杂。

4-三、 适用场景

移动端页面,将js注入到html页面,这样就不用考虑模块加载的问题,从而节省了不少的代码,在实现上也更为的简单。 若是是多文件加载的话,须要手动执行文件加载顺序,那么其实最好用库来进行依赖管理会好一点。

4-四、 现实状况

webpack + commonJS + ES6 (import + export )

这样来 实现模块管理,实现 较大项目的管理。 好了,模块化管理就先介绍到这里了,欢迎一块儿探讨


关于 前端 Javascript 模块化管理的来世此生 的文章就介绍到这里了,欢迎一块儿来论道~

GitHub 地址:(欢迎 star 、欢迎推荐 : )

前端 Javascript 模块化管理的来世此生

相关文章
相关标签/搜索