乐趣区

关于javascript:前端模块化介绍

一、模块化介绍

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); // 5
console.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.js
const {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.js
module.exports.ss = 'ss1';
setTimeout(()=>{
    module.exports.ss = 'ss2';
    console.log('module.exports', module.exports);
},2000);
// 执行后果
ss ss1
lib {ss: 'ss1'}
lib module.exports {ss: 'ss2'}
ss ss1
lib {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.js
const 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.js
export function getName() {...}
//my_module
import {getName} from './person.js';

----------------- 比照 ---------------------

//person.js
export default function getName(){...}
//my_module
import getName from './person.js';

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

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

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

//my_module
import 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 模块是动静援用,并且不会缓存值。

四 总结

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

退出移动版