1. 程式人生 > >前端模組化IIFE,commonjs,AMD,UMD,ES6 Module規範超詳細講解

前端模組化IIFE,commonjs,AMD,UMD,ES6 Module規範超詳細講解

[TOC] ## 為什麼前端需要模組化 在沒有模組化的時候,多個指令碼引入頁面,會造成諸多問題,比如: - 多人協同開發的時候,系統中可能會引入很多js指令碼,這些js會定義諸多全域性變數,這時候很容易出現變數名覆蓋的問題 ```html ``` 上面的例子中可以看到 第一個js中定義的變數info的值被第二個js中的變數所覆蓋 - 當指令碼之間存在依賴關係的時候,單純的引用script看不出js之間的依賴,可讀性很差 ```html ``` 如果第一個指令碼沒有引入,那麼執行就會拋錯,也就是說第二個指令碼是依賴第一個指令碼的,但是這個依賴關係這裡看不出來 ## 什麼是模組 模組我理解為就是一個區域性作用域,這個區域性作用域內部定義了一些區域性變數和方法,然後對外提供介面供外部呼叫,比如: ```javascript var moduleA = { name : "A" } var moduleB = { name : "B" } console.log(moduleA.name); // A ``` 這裡就可以看成是定義了兩個最簡單的模組,我們可以通過模組去訪問各自的變數 ## 是什麼IIFE IIFE(Immediately Ivoked Function Expression),即立即執行函式表示式,所謂立即執行,就是宣告一個函式,宣告完了立即執行 ```javascript var IIFE = function(){ // ... } IIFE(); ``` 這樣是立即執行但是肯定會有一個問題,函式名衝突了怎麼辦?==所以有了我們最常見的寫法,宣告一個自執行匿名函式== ```javascript (function(){ // ... })() ``` 如果看過jquery的一些外掛的原始碼的話經常能看到這樣的程式碼 ```javascript (function($){ // ... })(jQuery) ``` 這裡其實就是表明 這個模組依賴了jquery ### 舉個栗子 定義模組A和模組B,模組B依賴模組A - js檔案 ```javascript // 模組A moduleA.js (function(window){ var name = "module A"; // 對外暴露物件moduleA window.moduleA = { getName(){ return name; } } })(window) // 模組B moduleB.js (function(window, moduleA){ // 對外暴露物件moduleB window.moduleB = { showFirstModuleName(){ console.log(moduleA.getName()); } } })(window, moduleA) // main.js (function(moduleB){ console.log(moduleB.showFirstModuleName()); })(moduleB) ``` - html檔案中 ```html ``` 上述例子展示瞭如何用IIFE來定義模組,這樣寫有幾個**缺點**: - 定義了3個模組,那麼就引入了3個js指令碼,那如果==有更多模組呢,那就意味著很頁面載入時會像伺服器發起多次http請求==,這是不好的 - ==html中script的標籤順序是固定的==,因為模組main依賴moduleB,moduleB依賴moduleA,所以moduleA必須先宣告,這樣在moduleB的IIFE執行時候才能正常,不然會拋處ReferenceError ## 模組化標準 ### Commonjs nodejs採用的模組化標準,commonjs使用方法**require**來引入模組,這裡require()接收的引數是==模組名或者是模組檔案的路徑==,如果是模組名的話,require會到node_modules中去找對應名稱的模組來載入 ```javascript const _ = require("lodash"); ``` 這裡就引入了一個名為lodash的模組,那麼一個模組應該如何對外提供介面呢? commonjs提供兩種方式對外暴露介面 ```javascript // 第一種module.exports const name = "張三"; module.exports = { getName(){ return name } } // 第二種 const name = "張三" exports.getName = function(){ return name; } ``` 其實本質上,模組對外暴露的就是==exports==這個物件,*module.exports =*這種寫法,相當於直接給exports物件賦值,而*export. name*這種寫法其實就是給exports物件上添加了一個名為"name"的方法 #### 特徵 - ==在node執行時執行== - require是對值的拷貝 ```javascript // moduleA.js let count = 1; // 非同步讓count++ setTimeout(()=>{ count++; }); exports.count = count; // main.js const {count} = require("./moduleA.js"); // 同步列印count console.log(count); // 列印值為1 // 非同步列印count setTimeout(()=>{ console.log(count); // 列印值為1 }); ``` 可見改變了moduleA中的count,並不影響main.js中引入的值 - **不做特殊處理(webpack打包)commonjs只能執行在node環境**,瀏覽器環境不能直接使用,window上沒有定義require這個方法,所以解釋指令碼的時候就會拋處ReferenceError - **commonjs是同步載入模組**,在node環境中**require**引入一個模組的時候,這個過程是同步的,必須等模組載入完才能繼續後續操作 #### IIFE中的例子用commonjs實現 上述IIFE中的例子,用commonjs來實現就看起來就更清晰: ```javascript // 模組A moduleA.js const name = "module A" module.exports = { getName(){ return name; } } // 模組B moduleB.js const {getName} = require("./moduleA.js"); // 引入moduleA exports.showFirstModuleName = function(){ console.log(getName()); } // main.js const moduleB = require("./moduleB.js"); moduleB.showFirstModuleName(); // module A ``` 上文中講commonjs的特性的時候提到過,不能直接在瀏覽器中執行,所以我們==需要先使用打包用具(webpack等工具,以後的文章中會寫)把js打包處理成瀏覽器能直接執行的bundle.js==,在引入到html中 ```html ``` 或者直接在用node執行main: ```shell -> node main.js ``` ### AMD和RequireJS 全稱Asynchronous Module Definition非同步模組定義,與commonjs不同AMD是完全針對瀏覽器的模組化定義,AMD載入模組是非同步的 #### 如何定義一個模組 AMD規範中定義模組用到方法define,還是以之前的例子來舉例,先來定義一個沒有依賴的模組moduleA ```javascript // 定義一個moduleA.js define(function(){ var name = "module A" return { getName(){ return name } } }) ``` 這裡define只接受了一個回撥函式作為引數,這個回撥是不是與IIFE有點相似,再來定義一個依賴moduleA的moduleB ```javascript // 定義一個moduleB.js define(["moduleA"], function(moduleA){ return { showFirstModuleName(){ console.log(moduleA.getName()); } } }); ``` 這裡define的第一個引數是一個數組,數組裡面放的是==當前定義的模組所依賴的模組的名字,而後面回撥函式接收的引數就是對應的模組了==,也許看到這裡你會想問,為什麼這裡只寫一個模組名“moduleA”就能找到對應的moduleA.js的檔案了呢?後面會講 #### 如何在入口檔案引入模組 我們已經實現了moduleA.js和moduleB.js接下來要實現入口main.js,==AMD的標準中,引入模組需要用到方法require==,看到這你可能會有疑問,前面說commonjs的時候,不是說了window物件上沒定義require嗎?這裡就不得不提到一個庫,那就是[RequireJS](https://requirejs.org) >RequireJS is a JavaScript file and module loader. 官網介紹RequireJS是一個js檔案和模組的載入器,提供了載入和定義模組的api,當在頁面中引入了RequireJS之後,我們便能夠在全域性呼叫define和require,下面來實現main.js ```javascript // 實現main.js require(["moduleB"], function(moduleB){ moduleB.showFirstModuleName(); }); ``` ==三個js檔案都寫好了,我們該如何引入到頁面中呢==?檢視[RequireJS](https://requirejs.org)官網已經給出了答案 ```html ``` 要通過script引入requirejs,然後需要為標籤加一個屬性==data-main==來指定入口檔案 #### 使用RequireJS需要的配置 前面介紹用define來定義一個模組的時候,直接傳“模組名”似乎就能找到對應的檔案,這一塊是在哪實現的呢?其實在使用RequireJS之前還需要為它做一個配置 ```javascript // main.js require.config({ paths : { // key為模組名稱, value為模組的路徑 "moduleA" : "./moduleA", "moduleB" : "./moduleB" } }); require(["moduleB"], function(moduleB){ moduleB.showFirstModuleName(); }); ``` 這個配置中的屬性paths應該說是一目瞭然,看了就能明白,為什麼引入的時候只寫模組名就能找到對應路徑了吧,==不過這裡有一項要注意的是,路徑後面不能跟.js檔案字尾名==,更多的配置項請參考[RequireJS](https://requirejs.org)官網 ### ES6 module es6提出了新的模組化方案,這個方案應該也是現在最流行的。通過關鍵字==export value==來暴露模組,通過==import moduleName from path==來引入模組,是不是看起來很簡單?但是其實這裡還有很多細節 #### 如何執行 - ==瀏覽器端是不能直接執行的==,需要先用babel將es6語法轉譯成es5(把import轉譯成了require),然後再使用打包工具打包,最後在頁面中引入 - node端在某個版本後有辦法直接運行了(抱歉沒查是哪個版本),首先js檔案的==字尾名都要改成.mjs==,然後再命令列直接執行==node --experimental-modules main.mjs== #### 多次暴露 模組可以多次呼叫export來暴露任何值 ```javascript // moduleA.mjs // 暴露一個變數 export let name = "張三" // 暴露一個方法 export function getName(){ return name; } export function setName(newName){ name = newName; } // main.mjs import {name, getName, setName} from "./moduleA"; console.log(name); // 張三 setName("李四"); console.log(getName()); // 李四 ``` 這裡import後面必須跟結構賦值如果寫成下面這樣,會輸出**undefined** ```javascript import moduleA from "./moduleA" console.log(moduleA); // undefined;在node環境下執行會報錯 ``` 那如果模組分別暴露的方法有很多怎麼辦呢,這時候結構賦值不是要寫很多個方法?其實還可以這樣引入 ```javascript import * as moduleA from "./moduleA"; console.log(moduleA.name); // 張三 moduleA.setName("李四"); console.log(moduleA.getName()); // 李四 ``` #### 預設暴露 es6還提供了一種暴露方法叫預設暴露,預設暴露即==export default value==這裡的value可以是任何值,為什麼上面舉得**import**的反例,引入結果會是undefined呢,再看一個例子 ```javascript // moduleA.mjs export default { name : 張三, setName(newName){ this.name = newName; }, getName(){ return this.name; } } // main.mjs import moduleA from "./moduleA" console.log(moduleA); // { name: '張三', setName: [Function: setName], getName: [Function: getName] } ``` 這裡其實就能看出來,直接引入給moduleA賦值的其實是==export default value==後面的value ### UMD UMD全稱為*Universal Module Definition*,也就是==通用模組定義==,為什麼叫通用呢,我們怎麼描述一個模組是通用的呢?舉個例子,假如現在我的專案使用的是**amd**模組規範,那麼現在我引入了一個用**commonjs**規範寫的模組,能正常執行嗎?肯定不行的,而UMD就是解決了這個問題。 #### 特點 ==umd所謂的通用,就是相容了commonjs和amd規範==,這意味著無論是在commonjs規範的專案中,還是amd規範的專案中,都可以直接引用umd規範的模組使用(牛逼!) #### 原理 原理其實就是在模組中去判斷全域性是否存在exports和define,如果存在exports,那麼以commonjs的方式暴露模組,如果存在define那麼以amd的方式暴露模組 ```javascript (function(window, factory){ if(typeof exports === "objects"){ // commonjs module.exports = factory(); }else if(typeof define === "function" && define.amd){ // amd define(factory); }else{ window.moduleA = factory(); } })(window, function(){ // 返回module let modlueA = { name : "張三", setName(newName){ thie.name = newName; }, getName(){ return this.name; } } return modlueA;