6

什么是模块化

1、模块化

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

2、模块

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

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

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

前端为什么需要模块化(模块的发展)

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

1.1语法

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

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

1.2不足

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

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

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、对象封装

一个对象就是一个模块,所有模块成员都在其中

3.1语法

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

3.2不足

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

4、结合对象封装与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已经加载完
全局命名空间污染:仍然需要暴露一个全局变量

5、模块化规范的出现
(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 的内核开发者已经废弃了该规范。

1、定义

每个文件是一个模块,有自己的作用域。在一个文件里定义的变量、函数等都是私有的,对其他文件不可见。

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

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

2、语法

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

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

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

2.2 模块定义 :module.exports

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

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

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

3、特点

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

4、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规范比较适用。但是如果是浏览器环境,要从服务器端加载资源,这时就必须采用非同步模式。

1、模块定义

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(){})   //  依赖必须一开始就写好

2、模块导入

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

3、特点

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

4、RequireJS

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

CMD 与 seajs

1、模块定义

在依赖示例部分,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"; 
});

2、模块导入

require('路径')  

3、特点

依赖就近书写:一般不再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

1、模块导出 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。

2、模块输入 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';

3、特点

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

各规范总结

1、 CommonJS

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

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

2、AMD

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

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

3、CMD

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

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

4、UMD

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

5、ES6 module

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

导入 :import 
导出:export、 export default

各规范的区别提炼

1、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模块输出的是值的拷贝,所以当模块内值变化时,不会影响到输出的值

2、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/...


二丽
280 声望8 粉丝

下一篇 »
babel-preset-env