一、模块化介绍

1 模块化由来

  • 问题

    - 多人合作造成变量命名抵触问题- 代码凌乱不好保护

    基于以上问题的呈现,有了模块化的解决方案。

  • 后果

    - 能够把简单的代码拆分成小的模块,方便管理代码和保护- 每个模块间接的内容都是互相独立的,互不影响

2 模块化历史

2.1 最晚期模块化形式

1 单例模式
如果两个开发者都有同一个变量a,能够用以下这种形式加以辨别。

var name1 = {  a: 1}var name2 = {  a: 2}

然而这种形式也并没有齐全解决问题,毕竟name1 和name2 也须要不同命名,并且这种形式调用起来不是很不便
2 自执行函数

function(){  var a = 1}()function(){  var a = 1}()

每个函数有本人的作用域,所以以上两个函数外部的a变量不会呈现抵触。然而这种解决形式也不雅观

2.2 已过期模块化形式

1 AMD模块标准
AMD——异步模块加载标准,就是模块加载过程中即便require的模块还没有获取到,也不会影响前面代码的执行。
RequireJS——AMD标准的实现。其实也能够说AMD是RequireJS在推广过程中对模块定义的规范化产出。
示例如下:

//独立模块定义define({  a: function() {}  b: function() {}}); // 非独立模块定义define(['f1', 'f2'], function(f1, f2){  a: function() {}  b: function() {}});// 模块援用require(['m1', 'm2'], function(m1, m2){  m1.a();  m2.b();})

2 CMD模块标准
CMD——通用模块标准,由国内的玉伯提出。
SeaJS——CMD的实现,其实也能够说CMD是SeaJS在推广过程中对模块定义的规范化产出。
用法示例:

define(function(require, exports, module){  //依赖模块a  var a = require('./a');  //调用模块a的办法  a.method();})

与AMD标准的次要区别在于定义模块和依赖引入的局部。AMD须要在申明模块的时候指定所有的依赖,通过形参传递依赖到模块内容中。CMD模块更靠近于Node对CommonJS标准(前面会着重讲)的定义,CMD反对动静引入,require、exports和module通过形参传递给模块,在须要依赖模块时,随时调用require( )引入即可。与AMD相比,CMD推崇依赖就近,AMD推崇依赖前置。

3 UMD通用模块标准
所谓的兼容模式是将几种常见模块定义形式都兼容解决。

(function (global, factory) {   typeof exports === 'object' && typeof module !== 'undefined'        ? module.exports = factory()          // Node , CommonJS       : typeof define === 'function' && define.amd           ? define(factory)                   //AMD CMD         : (global.CodeMirror = factory());  //模块挂载到全局}(this, (function () {    ...})

接下来咱们将介绍目前最支流的前端模块化的计划。

二 node中的模块

Node 利用由模块组成,采纳 CommonJS 模块标准。
每个文件就是一个模块,有本人的作用域。在一个文件外面定义的变量、函数、类,都是公有的,对其余文件不可见。

2.1 CommonJs简略介绍

CommonJS标准规定,每个模块外部,module变量代表以后模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。

// 被援用模块文件var x = 5;var addX = function (value) {  return value + x;};module.exports.x = x;module.exports.addX = addX;// 加载模块文件var example = require('./example.js');console.log(example.x); // 5console.log(example.addX(1)); // 6

2.2 module对象

2.2.1 模块实现
Node外部提供一个Module构建函数。所有模块都是Module的实例。每个模块外部,都有一个module对象,代表以后模块。

function Module(id, parent){  this.id = id;  this.exports = {};  this.parent = parent;  if(parent && parent.children) {    parent.children.push(this);  }  this.filename = null;  this.loaded = false;  this.children = [];}
  • module.id 模块的辨认符,通常是带有绝对路径的模块文件名。
  • module.filename 模块的文件名,带有绝对路径。
  • module.loaded 返回一个布尔值,示意模块是否曾经实现加载。
  • module.parent 返回一个对象,示意调用该模块的模块。
  • module.children 返回一个数组,示意该模块要用到的其余模块。
  • module.exports 示意模块对外输入的值。

2.2.2 module.exports与exports
module.exports属性示意以后模块对外输入的接口,其余文件加载该模块,实际上就是读取module.exports变量。
为了不便,Node为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令。
var exports = module.exports;
如果你感觉,exports与module.exports之间的区别很难分清,一个简略的解决办法,就是放弃应用exports,只应用module.exports。

2.3 require命令

Node应用CommonJS模块标准,内置的require命令用于加载模块文件。
require命令的基本功能是,读入并执行一个JavaScript文件,而后返回该模块的exports对象。如果没有发现指定模块,会报错。

2.3.1 node中模块分类
1.外围模块/内置模块 (fs http path等)
2.第三方模块须要装置
3.自定义模块须要通过绝对路径或者相对路径进行引入
2.3.2 模块分类
在Node模块的引入过程中,个别要通过一下三个步骤

  1. 路径分析
  2. 文件定位
  3. 编译执行

外围模块会省略文件定位和编译执行这两步,并且在路径分析中会优先判断,加载速度比个别模块更快。
文件模块——就是内部引入的模块如node_modules里通过npm装置的模块,或者咱们我的项目工程里本人写的一个js文件或者json文件。文件模块引入过程以上三个步骤都要经验。
2.3.3 路径分析
不管外围模块还是文件模块都须要经验路径分析这一步。
node反对如下几种模式的模块标识符

//外围模块require('http')----------------------------//文件模块//以.结尾的相对路径,(能够不带扩展名)require('./a.js')//以..结尾的相对路径,(能够不带扩展名)require('../b.js')//以/开始的绝对路径,(能够不带扩展名)require('/c.js')//内部模块名称require('express')//内部模块某一个文件require('codemirror/addon/merge/merge.js');

那么对于这个都是字符串的引入形式,Node 会优先去内存中查找匹配外围模块,如果匹配胜利便不会再持续查找
(1)比方require http 模块的时候,会优先从外围模块里去胜利匹配
如果外围模块没有匹配胜利,便归类为文件模块
(2)以.、..和/结尾的标识符,require都会依据以后文件门路将这个相对路径或者绝对路径转化为实在门路,也就是咱们平时最常见的一种门路解析
(3)非门路模式的文件模块 如下面的'express' 和'codemirror/addon/merge/merge.js',这种模块是一种非凡的文件模块,个别称为自定义模块。

自定义模块的查找最费时,因为对于自定义模块有一个模块门路,Node会依据这个模块门路顺次递归查找。

模块门路——Node的模块门路是一个数组,模块门路寄存在module.paths属性上。模块门路的生成规定如下:

  1. 以后路文件下的node_modules目录
  2. 父目录下的node_modules目录
  3. 父目录的父目录下的node_modules目录
  4. 沿路径向上逐级递归,直到根目录下的node_modules目录

2.3.4 文件定位
扩展名剖析
咱们在应用require的时候有时候会省略扩展名,那么Node怎么定位到具体的文件呢?

这种状况下,Node会顺次依照.js、.json、.node的秩序一次匹配。(.node是C++扩大文件编译之后生成的文件)

若扩展名匹配失败,则会将其当成一个包来解决,我这里间接了解为npm包

包解决
对于包Node会首先在以后包目录下查找package.json(CommonJS包标准)通过JSON.parse( )解析出包形容对象,依据main属性指定的入口文件名进行下一步定位。

如果文件短少扩展名,将依据扩展名剖析规定定位。

若main指定文件名谬误或者压根没有package.json,Node会将包目录下的index当做默认文件名。

再顺次匹配index.js、index.json、index.node。

若以上步骤都没有定位胜利将,进入下一个模块门路——父目录下的node_modules目录下查找,直到查找到根目录下的node_modules,若都没有定位到,将抛出查找失败的异样。
2.3.5 模块编译
.js文件——通过fs模块同步读取文件后编译执行
.node文件——用C/C++编写的扩大文件,通过dlopen( )办法加载最初编译生成的文件。
.json——通过fs模块同步读取文件后,用JSON.parse( ) 解析返回后果。
其余扩展名文件。它们都是被当做.js文件载入。

2.4 CommonJS模块加载机制

网上很多中央都在说:CommonJS模块的加载机制是,输出的是被输入的值的拷贝。这句话是谬误的。
以上面这段代码为例:

// index.jsconst { ss } = require('./lib');const lib = require('./lib');console.log('ss', ss);console.log('lib', lib);setTimeout(()=>{    console.log('ss', ss);    console.log('lib', lib);},3000);
// lib.jsmodule.exports.ss = 'ss1';setTimeout(()=>{    module.exports.ss = 'ss2';    console.log('module.exports', module.exports);},2000);
//执行后果ss ss1lib { ss: 'ss1' }lib module.exports { ss: 'ss2' }ss ss1lib { ss: 'ss2' }

从执行后果能够看出

commonjs 导出的是module.exports这个对象,导出值给这个对象增加新的属性会影响导入值。
const { ss } = require('./lib'); 相当于 const { ss } = {ss:'ss1'}; 解构赋值,相当于const ss = 'ss1';所以导出对象批改ss不能使导入对象ss也变成2。

三 ESModule

ES6在语言规格层面上实现了模块性能,是编译时加载,齐全能够取代CommonJS和AMD标准,能够成为浏览器和服务器通用的模块解决方案.

3.1 ES6模块应用——export

// 导出变量export var name = 'pengpeng';// 导出一个函数export function foo(x, y){}// 举荐罕用导出形式// person.jsconst name = 'dingman';const age = '18';const addr = '卡尔斯特森林';export { name, age, addr };// as 用法const s = 1;export {  s as t,  s as m, }

3.2 ES6模块应用——import

// 个别用法import { name, age } from './person.js';// As用法import { name as personName } from './person.js';//整体加载import * as person from './person.js';console.log(person.name);console.log(person.age);

3.3 ES6模块应用——export default

其实export default,在我的项目里用的十分多,个别一个Vue组件或者React组件咱们都是应用export default命令,须要留神的是应用export default命令时,import是不须要加{}的。而不应用export default时,import是必须加{},示例如下:

//person.jsexport function getName() { ...}//my_moduleimport {getName} from './person.js';-----------------比照---------------------//person.jsexport default function getName(){ ...}//my_moduleimport getName from './person.js';

export default其实是导出一个叫做default的变量,所以其前面不能跟变量申明语句。

值得注意的是咱们能够同时应用export 和export default

//person.jsexport name = 'dingman';export default function getName(){  ...}//my_moduleimport getName, { name } from './person.js';

3.4 ES6模块与CommonJS模块加载区别

ES6模块的设计思维,是尽量的动态化,使得编译时就能确定模块的依赖关系,以及输出和输入的变量。所以说ES6是编译时加载,不同于CommonJS的运行时加载(理论加载的是一整个对象),ES6模块不是对象,而是通过export命令显式指定输入的代码,输出时也采纳动态命令的模式:

//ES6模块import { basename, dirname, parse } from 'path';//CommonJS模块let { basename, dirname, parse } = require('path');

以上这种写法与CommonJS的模块加载有什么不同?

当require path模块时,其实 CommonJS会将path模块运行一遍,并返回一个对象,并将这个对象缓存起来,这个对象蕴含path这个模块的所有API。当前无论多少次加载这个模块都是取这个缓存的值,也就是第一次运行的后果,除非手动革除。
ES6会从path模块只加载3个办法,其余不会加载,这就是编译时加载。ES6能够在编译时就实现模块加载,当ES6遇到import时,不会像CommonJS一样去执行模块,而是生成一个动静的只读援用,当真正须要的时候再到模块里去取值,所以ES6模块是动静援用,并且不会缓存值。

四 总结

以上介绍了模块化的一些常识,欢送大家批评指正!