前端模块化

什么是模块化

一、模块化

模块化是自顶向下逐层将系统划分红若干更好的可管理模块的方式,用来分割、组织和打包软件,达到高度解耦前端

二、模块

模块是可组合、分解、更换的单元;
每一个模块完成一个特定子功能,模块间经过某种方式组装起来,成为一个总体
模块间高度解耦,模块功能单一,可高度复用node

三、前端模块化解决的问题

一、消除全局变量,减小命名冲突
二、更好地代码组织结构和开发协做:经过文件拆分,更易于管理复杂代码库,更易于多人协做开发,下降文件合并时候冲突的发生几率,方便编写单元测试
三、依赖管理、按需加载:再也不须要手动管理脚本加载顺序
四、优化:
(1)代码打包:合并小模块,抽取公共模块,在资源请求数和浏览器缓存利用方面进行合适的取舍
(2)代码分割:按需加载代码(分路由、异步组件),解决单页面应用首屏加载缓慢的问题
(3)Tree Shaking :利用ES6模块的静态化特性。在构建过程当中分析出代码库中未使用的代码,从最终的bundle中 去除,从而减小JS Bundle的大小
(4)Scope Hoisting:ES6模块内容导入导出绑定是活动的,能够将多个小模块合并到一个函数当中去,对于重复变量名进行核实的重命名,从而减小Bundle的尺寸和提高加载速度。es6

前端为何须要模块化(模块的发展)

一、内嵌脚本---原始写法

1.1语法express

在 <script ></script>标记之间添加js代码 ,把不一样的函数等简单放在一块儿,就算是一个模块npm

function fn1(){....}
​
    function fn2(){....}

1.2不足编程

代码无重用性:其余页面须要该script标签中一些代码时,须要复制粘贴
全局命名空间污染:全部变量、方法等都定义在全局做用域中,也容易命名冲突json

二、外链脚本---原始写法

2.1语法数组

将js代码分红多个片断分别放入s文件中,使用<script src>引入浏览器

<script src="1.js"></script>
​
  <script src="2.js"></script>
​
  <script src="3.js"></script>
​
  <script src="4.js"></script>

2.2不足缓存

缺少依赖管理:文件之间讲究前后顺序,互相之间存在依赖关系
全局命名空间污染:全部变量、方法等都定义在全局做用域中

三、对象封装

一个对象就是一个模块,全部模块成员都在其中

3.1语法

var obj = new Object({
    fn1 : function (){},
    fn2 : function (){}
    .....
});

3.2不足

暴露了内部成员:因此内部成员都被暴露,在外不能够轻易被修改
缺少依赖管理:一个模块一个文件,文件顺序还须要手动控制
全局命名空间污染:仍然须要暴露一个全局变量

四、结合对象封装与IIFE(当即执行函数表达式)

4.1 语法

将每一个文件都封装成IIFE,内部定义的变量和方法只在IIFE做用域内生效,不会污染全局。而且经过将这些方法变量赋值给某个全局对象来公开 , 不暴露私有成员;

var module = (function(obj){
    let a =1;
    obj.fn1=function (){}
    return obj
})(module || {});

4.2 应用

Jquery库,公开一个全局对象$, 它中包含因此方法与属性

4.3 不足

缺少依赖管理:文件顺序还须要手动控制,例如使用jQuery的方法前,必须保证jQuery已经加载完
全局命名空间污染:仍然须要暴露一个全局变量

五、模块化规范的出现
(1) js引入服务器端后,出现的 CommonJS规范
(2)CommonJS的同步性限制了前端的使用,出现了 AMD
(3)UMD规范的统一
(4)ES6模块的定义

CommonJs 与 nodeJs服务端的模块化实现

CommonJS是除浏览器以外 构建js生态系统为目标而产生的规范,好比服务器和桌面环境等。最先 由Mozilla的工程师Kevin Dangoor在2009年1月建立。

2013年5月,Node.js 的包管理器 NPM 的做者 Isaac Z. Schlueter 说 CommonJS 已通过时,Node.js 的内核开发者已经废弃了该规范。

一、定义

每一个文件是一个模块,有本身的做用域。在一个文件里定义的变量、函数等都是私有的,对其余文件不可见。

在每一个模块内部,module变量表明当前模块,它的exports属性是对外的接口,加载某个模块(require)时,其实加载的是该模块的 exports属性

var x = 5;
var addX = function (value) {
  return value + x;
};
module.exports.x = x;
module.exports.addX = addX;

二、语法

CommonJS包含主要包含三部分:模块导入(加载),模块定义、模块标识

2.1 模块导入:require() ——返回该模块的exports属性

var  module1 = require('./module1.js');

2.2 模块定义 :module.exports

//module1.js
module.exports.fn1 = function (){}

2.3 模块标识:require()方法的参数

必须是字符串
能够是以./ ../开头的相对路径
能够是绝对路径
能够省略后缀名

三、特色

自动依赖管理:模块加载的顺序 依赖于其在代码中出现的顺序
不污染全局做用域:模块内部代码运行在本身的私有做用域
可屡次加载,但只执行一次:模块能够屡次加载,可是只会在第一次加载时运行一次,而后运行结果被缓存,之后再加载,直接读取缓存结果。若是想让模块再次执行,必须清楚缓存
同步加载模块:只有加载完成以后,才能执行后面的操做
运行时加载

四、nodejs中的实现

4.1 module对象

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.chilren =[];//返回数组,该模块要用到的其余模块
        }

​
    //实例化一个模块
    var module1 =new Module(filename,parent)

4.2 module.exports属性

module.exports属性表示当前模块对外输出的接口,其余文件加载该模块,实际上就是读取module.exports变量。

4.3 exports变量

node为每一个模块提供了exoprts变量,指向module.exports。等同于在每一个模块头部,有一行代码

var exports = module.exports;

在对外输出时,能够向exports对象添加方法

exports.fn1 =function(){}

不能直接将exports指向一个值,这样会切断exports与module.exports的联系

exports = function(x) {console.log(x)};

若是一个模块的module.exports是一个单一的值,不能使用exports输出,只能使用module.exports输出

//hello函数是没法对外输出的,由于module.exports被从新赋值了。

exports.hello = function() {
  return 'hello';
};
​
module.exports = 'Hello world';


4.4 node中的模块分类

node中模块分为两类:一类为mode提供的核心模块,另外一类为 用户编写的文件模块

4.4.1 核心模块

即node提供的内置模块如 http模块、url模块、fs模块等

核心模块在node源代码的编译过程当中被编译进了二进制文件,在node进程启动的时候,会被直接加载进内存,所以引用这些模块的时候,文件定位和编译执行这两步会被省略。

在路径分析中会优先判断核心模块,加载速度最快。

4.4.2 文件模块

即外部引入的模块 如node_modules中的模块,项目中本身编写的js文件等

在运行时动态加载,须要完整的路径分析,文件定位,编译执行这三部,加载速度比核心模块慢

4.5 路径分析、文件定位、编译执行

4.5.1路径分析

不论核心模块仍是文件模块都须要经历路径分析这一步,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',这种模块是一种特殊的文件模块,通常称为自定义模块。

4.5.1.1 模块路径

自定义模块的查找最费时,由于对于自定义模块有一个模块路径,Node会根据这个模块路径依次递归查找。
模块路径——Node的模块路径是一个数组,模块路径存放在module.paths属性上。
咱们能够找一个基于npm或者yarn管理项目,在根目录下建立一个test.js文件,内容为console.log(module.paths),以下:

//test.js
console.log(module.paths);
而后在根目录下用Node执行

node test.js
能够看到咱们已经将模块路径打印出来。

能够看到模块路径的生成规则以下:
● 当前路文件下的node_modules目录
● 父目录下的node_modules目录
● 父目录的父目录下的node_modules目录
● 沿路径向上逐级递归,直到根目录下的node_modules目录
对于自定义文件好比express,就会根据模块路径依次递归查找。
在查找同时并进行文件定位。

4.5.2文件定位

● 扩展名分析
咱们在使用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,若都没有定位到,将抛出查找失败的异常。

4.5.3模块编译

● .js文件——经过fs模块同步读取文件后编译执行
● .node文件——用C/C++编写的扩展文件,经过dlopen( )方法加载最后编译生成的文件。
● .json——经过fs模块同步读取文件后,用JSON.parse( ) 解析返回结果。
● 其他扩展名文件。它们都是被当作.js文件载入。

每个编译成功的文件都会将其文件路径做为索引缓存在Module._cache对象上,以提升二次引入的性能。
这里咱们只讲解一下JavaScript模块的编译过程,以解答前面所说的CommonJS模块中的require、exports、module变量的来源。

咱们还知道Node的每一个模块中都有filename、dirname 这两个变量,是怎么来的的呢?
其实JavaScript模块在编译过程当中,整个所要加载的脚本内容,被放到一个新的函数之中,这样能够避免污染全局环境。该函数的参数包括require、module、exports,以及其余一些参数。
Node对获取的JavaScript文件内容进行了头部和尾部的包装。在头部添加了(function (exports, require, module,filename, dirname){n,而在尾部添加了n}); 。

所以一个JS模块通过编译以后会被包装成下面的样子:

(function(exports, require, module, __filename, __dirname){
  var express = require('express') ;
  exports.method = function (params){
   ...
  };
});

4.6 模块加载机制

总体加载执行,导入的是被输出的值得拷贝,即 一旦输出一个值,模块内部的变化就影响不到这个值

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};
​
​
​
​
// main.js
var counter = require('./lib').counter;
var incCounter = require('./lib').incCounter;
​
console.log(counter);  // 3
incCounter();
console.log(counter); // 3 
//counter输出之后,lib.js模块内部的变化就影响不到counter了。

4.7 require 的内部处理流程

require不是一个全局命令,而是指向当前模块的module.require命令,module.require又调用node内部命令Module._load

require —>module.require——>Module._load

MOdule._load =function(require,parent,isMain){
    1.检查缓存Module._cache ,是否有指定模块
    2.若是缓存中没有,就建立一个新的MOdule实例
    3.将实例保存到缓存
    4.使用Module,load()加载指定的模块文件
    5.读取文件内容后,使用module.compile()执行文件代码
    6.若是加载/解析过程报错,就从缓存中删除该模块
    7.返回该模块的module.exports
}

Module.compile方法是同步执行的,全部Module.load 要等它执行完成,才会向用户返回 module.exports的值

AMD 与 requireJs

因为node主要用户服务端编程,模块文件通常都已经存在于本地硬盘,因此加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。可是若是是浏览器环境,要从服务器端加载资源,这时就必须采用非同步模式。

一、模块定义

define(id? dependencies?,factory)

id为string类型,表示模块标识
dependencies:为Array类型,表示须要依赖的模块
factory:为function或者Object,表示要进行的回调

1.1 独立模块(不须要依赖模块)

define({
    fn1:function(){}
 })
​
define(function(){
    return {
        fn1:function(){},   
    }
})

1.2 非独立模块(有依赖其余模块)

define(['module1','module2'],function(){})   //  依赖必须一开始就写好

二、模块导入

require(['a','b'],function(a,b){})

三、特色

依赖管理:被依赖的文件早于主逻辑被加载执行 ;
运行时加载;
异步加载模块:在模块的加载过程当中即便require的模块尚未获取到,也不会影响后面代码的执行,不会阻塞页面渲染

四、RequireJS

AMD规范是RequireJS在推广过程当中对模块定义的规范化产出

CMD 与 seajs

一、模块定义

在依赖示例部分,CMD支持动态引入,require、exports和module经过形参传递给模块,在须要依赖模块时,随时调用require( )引入便可,示例以下:

define(factory)

1.1 factory 三个参数

function(require,exports,module)
require用于导入其余模块接口
exports 用于导出接口
module存储了与当前模块相关联的一些属性与方法

1.2 例子

define(function(require ,exports,module) {
    //调用依赖模块increment的increment方法
    var inc = require('increment').increment;   // 依赖能够就近书写
    var a = 1;
    inc(a); 
    module.id == "program"; 
});

二、模块导入

require('路径')

三、特色

依赖就近书写:通常再也不define的参数中写依赖,就近书写
延迟执行

UMD通用规范

兼容CommonJS、AMD 、CMD、全局引用
写法:

(function(global,factory){
    typeof exports === 'object'&& typeof module!=='undefined'
    ?module.exports =factory()   //CommonJS
    :typeof define ==='fucntion' && define.amd
    ?define(factory)         //AMD CMD
    :(global.returnExports = factory()) //挂载到全局
}(this,function(){
    //////暴露的方法
    return fn1
}))

es6 module

一、模块导出 export

export 输入变量、函数、类等 与模块内部的变量创建一一对应关系

//写法一   
export var a=1;
//写法二
var a=1;
export {a}
//写法三  as进行重命名
var b=1; 
export {b as a}
//写法四
var a=1
export default a

export语句输出的接口,与其对应的值是动态绑定关系,即经过该接口,能够取到模块内部实时的值。

export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);

上面代码输出变量foo,值为bar,500 毫秒以后变成baz。

二、模块输入 import

2.1 写法一

import命令接受一对大括号,里面指定要加载指定模块,并从中输入变量

import {firstName, lastName, year} from './profile.js';
import { lastName as surname } from './profile.js';

大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同。
可使用as关键字,将输入的变量重命名。

2.2 写法二

import 后面写模块路径--------执行所加载的模块,但不输入任何值

import 'lodash';

上面代码仅仅执行lodash模块,可是不输入任何值。
若是屡次重复执行同一句import语句,那么只会执行一次,而不会执行屡次。

2.3 写法三

用星号(*)指定一个对象,总体加载全部对象到这个对象上 ——总体模块加载

import * as circle from './circle';

2.4 export default 与 import

export default 实际导出的为一个叫作 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';
​
​
//person.js
export name = 'dingman';
export default function getName(){
  ...
}
​
//my_module
import getName, { name } from './person.js';

三、特色

编译时加载:编译的时候就能够肯定模块的依赖关系,已经输入与输出的变量

各规范总结

一、 CommonJS

环境:服务器环境
特色:(1)同步加载;(2)运行时加载 (3)屡次加载,只第一次执行,之后直接读取缓存
应用: Nodejs
语法:

导入 :  require()
  导出:module.exports 或者 exports

二、AMD

环境:浏览器
特色:(1)异步加载 (2)运行时加载(3)管理依赖,依赖前置书写 (4)依赖提早执行(加载完当即执行)
应用:RequireJS
语法:

导入:require(['依赖模块'],fucntion(依赖模块变量引用){回调函数})
  导出(定义):define(id?def?factory(){return ///})

三、CMD

环境:浏览器
特色:(1)异步加载 (2)运行时加载 (3)管理依赖,依赖就近书写(4)依赖延迟执行 (require的时候才执行)
应用:SeaJS
语法:

导入:require()
导出: define(function(require,exports,module){})

四、UMD

环境:浏览器或服务器
特色:(1)兼容CommonJS AMD UMD 全局应用
语法:无导入导出,只是一种兼容写法

五、ES6 module

环境:浏览器或服务器
特色:(1)编译时加载(2)按需加载 (3)动态更新
应用:es6最新语法
语法:

导入 :import 
导出:export、 export default

各规范的区别提炼

一、CommonJS与ES6

1.1 是否动态更新

es6 :输出的值是动态绑定,会实时动态更新。
CommonJS :输出的是值的缓存,不存在动态更新

1.2 加载时机

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

es6 :

编译时加载,ES6能够在编译时就完成模块加载;
按需加载,ES6会从path模块只加载3个方法,其余不会加载。
动态引用,实时更新,当ES6遇到import时,不会像CommonJS同样去执行模块,而是生成一个动态的只读引用,当真正须要的时候再到模块里去取值,因此ES6模块是动态引用,而且不会缓存值。

CommonJS:

运行时加载
加载整个对象:require path模块时,其实 CommonJS会将path模块运行一遍,并返回一个对象,并将这个对象缓存起来,这个对象包含path这个模块的全部API。
使用缓存值,不会实时更新:之后不管多少次加载这个模块都是取第一次运行的结果,除非手动清除。由于CommonJS模块输出的是值的拷贝,因此当模块内值变化时,不会影响到输出的值

二、CMD 与 AMD

2.1 cmd依赖就近,AMD依赖前置

// CMD
define(function(require, exports, module) {
    var a = require('./a')
    a.doSomething()       
    var b = require('./b') // 依赖能够就近书写
    b.doSomething()
    // ... 
})
// AMD
define(['./a', './b'], function(a, b) {// 依赖必须一开始就写好
    a.doSomething()
    b.doSomething() 
    ...
})

2.2 CMD延迟执行,AMD提早执行

AMD

在加载模块完成后就当即执行该模块,
全部模块都加载执行完成后 才会进入require的回调函数,执行主逻辑
(会出现 那个依赖模块先下载完,哪一个就先执行,与执行顺序书写顺序不一致
AMD RequireJS 从 2.0 开始,也改为能够延迟执行(根据写法不一样,处理方式不一样)

CMD

加载完某个依赖模块后 并不执行,
当全部依赖模块加载完成后进入主逻辑,遇到require语句时才**执行对应依赖模块**。
(保证了**执行顺序与书写顺序的一致**)

参考:

http://es6.ruanyifeng.com/#do...
https://zhuanlan.zhihu.com/p/...

相关文章
相关标签/搜索
本站公众号
   欢迎关注本站公众号,获取更多信息