前端模塊化

什麼是模塊化

一、模塊化

模塊化是自頂向下逐層將系統劃分紅若干更好的可管理模塊的方式,用來分割、組織和打包軟件,達到高度解耦前端

二、模塊

模塊是可組合、分解、更換的單元;
每一個模塊完成一個特定子功能,模塊間經過某種方式組裝起來,成爲一個總體
模塊間高度解耦,模塊功能單一,可高度複用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/...

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息