JavaScript 模組封裝

雲崖先生發表於2020-08-07

JavaScript 模組封裝

前言介紹

  在最早的時候JavaScript這門語言其實是並沒有模組這一概念,但是隨著時間的推移與技術的發展將一些複用性較強的程式碼封裝成模組變成了必要的趨勢。

  在這篇文章中主要介紹原生的 JavaScript封裝的幾種手段以及新增的 ES6 Module的語法,來實現模組封裝。

  並且會簡單的使用WebpackEs6程式碼向後相容。

 

引入問題

  以下有兩個Js檔案,如果不採取任何封裝手段直接匯入會導致window環境汙染。

  並且,如果檔案中有相同名字的變數或函式會發生命名衝突,因為它們都是放在全域性作用域window物件中的。

 

<script src="./js_m1.js"></script>
<script src="./js_m2.js"></script>
<script>"use strict";
​
    // 這是由於js_m2後引入,所以js_m1的同名變數以及函式都被覆蓋掉了。
​
    console.log(module_name);  // js_m2
​
    show();  // js_m2.show
</script>

 

var module_name = "js_m1";
​
function show(){
        console.log("js_m1.show");
}

 

var module_name = "js_m2";
​
function show(){
        console.log("js_m2.show");
}

 

image-20200807115438551

 

簡單解決

IIFE封裝


  針對上述問題,採取函式的閉包及作用域特性我們為每個模組封裝一個作用域。

 

  第一步:進行自執行函式包裹程式碼封裝出區域性作用域

  第二步:向外部暴露介面,為window物件新增新的物件

 

<script src="./js_m1.js"></script>
<script src="./js_m2.js"></script>
<script>"use strict";
​
    console.log(js_m1.module_name);  // js_m1
​
    js_m1.show();  // js_m1.show
​
    console.log(js_m2.module_name);  // js_m2
​
    js_m2.show();  // js_m2.show
</script>

 

(function () {
​
        var module_name = "js_m1";
​
        function show() {
                console.log("js_m1.show");
        }
​
        window.js_m1 = { module_name: module_name, show: show };
        // 在es6中,可簡寫為 { module_name , show }
}())

 

(function () {
​
        var module_name = "js_m2";
​
        function show() {
                console.log("js_m2.show");
        }
​
        window.js_m2 = { module_name: module_name, show: show };  
        // 在es6中,可簡寫為 { module_name , show }
}())

 

image-20200807120606342

 

Es6塊級封裝


  Es6之前,由於沒有出現塊級作用域的概念,那時候大家都使用上面的方式進行封裝。

  在當Es6的塊級作用域出現之後,又誕生出了新的封裝方式即塊級作用域封裝。

 

  IIFE封裝相同,都是利用作用域的特性進行封裝。

  注意一點,塊級作用域只對letconst宣告有效。

 

<script src="./js_m1.js"></script>
<script src="./js_m2.js"></script>
<script>"use strict";
​
    console.log(js_m1.module_name);  // js_m1
​
    js_m1.show();  // js_m1.show
​
    console.log(js_m2.module_name);  // js_m2
​
    js_m2.show();  // js_m2.show
</script>

 

{
        let module_name = "js_m1";
​
        let show = function () {
                console.log("js_m1.show");
        }
​
        window.js_m1 = { module_name, show };
​
}

 

{
        let module_name = "js_m2";

        let show = function () {
                console.log("js_m2.show");
        }

        window.js_m2 = { module_name, show };

}

 

image-20200807121327601

 

Es6 module 語法

  上面的兩種方式雖然都能達到模組封裝的效果,但是我們依然有更好的選擇。

  下面將介紹極力推薦的Es6 module語法進行匯入。

 

  學習Es6 module從以下三個方面來入手:

 

  1.模組標籤及其特性

  2.匯出

  3.匯入

 

模組標籤

  要想使用Es6 module語法匯入模組,必須使用模組標籤來引入Js檔案。

  模組標籤與普通的<script>標籤具有一些不太一樣的地方,下面會從各個方面逐一進行介紹。

 

宣告標籤


  <script>標籤新增上type="module"的屬性。

 

<script type="module"></script>

 

匯入路徑


  在瀏覽器中引用模組必須新增路徑如./ ,但在打包工具如webpack中則不需要,因為他們有自己的存放方式。

  總而言之,即使是在當前目錄也要新增上./,不可以進行省略。

  這也是推薦的一種引入檔案方式,不管是何種語言中都推薦引入檔案時不進行路徑省略。

 

  正確的匯入路徑

<script type="module" src="./js_m1.js"></script>
<script type="module" src="./js_m2.js"></script>

 

  錯誤的匯入路徑

<script type="module" src="js_m1.js"></script>  // 不可省略!省略就會丟擲異常
<script type="module" src="js_m2.js"></script>

 

延遲解析


  所謂延遲解析是指在模組標籤中的程式碼會提到HTML程式碼以及嵌入式的<script>標籤後才進行執行。

  注意看下面的示例,編碼時模組標籤在普通的<script>之上,但是結果卻相反。

 

<script type="module">

    console.log("<script type='module'> code run...");

</script>

<script>

    "use strict";

    console.log("<script> code run...");

</script>

 

image-20200807123803291

 

嚴格模式


  模組標籤中的所有程式碼都是按嚴格模式執行的,請注意變數名的宣告以及this指向問題,同時還有解構賦值等等。

 

<script type="module">

    username = "雲崖";  // 丟擲異常,未宣告

</script>

 

<script type="module">

    let obj = {
        show() {
            console.log(this); // {show: ƒ}
            (function () { console.log(this); }())  // undefined 嚴格模式下為undefined ,普通模式下為window物件
        }
    };

    obj.show();

</script>

 

作用域


  每個模組標籤中的程式碼都會為其建立一個專屬的作用域,禁止相互之間進行訪問。

  而普通的<script>標籤中的程式碼全部在全域性作用域下執行。

 

<script>

    let m1 = "m1...";

</script>

<script>

    console.log(m1);  // m1...

</script>

 

<script type="module">

    let m1 = "m1...";

</script>

<script type="module">

    console.log(m1);  // Uncaught ReferenceError: m1 is not defined

</script>

 

預解析


  模組在匯入時只執行一次解析,之後的匯入不會再執行模組程式碼,而使用第一次解析結果,並共享資料。

 

  可以在首次匯入時完成一些初始化工作

  如果模組內有後臺請求,也只執行一次即可

 

<script type="module" src="./js_m3.js"></script>
<script type="module" src="./js_m3.js"></script>
<script type="module" src="./js_m3.js"></script>

<!-- 匯入多次,也只執行一次程式碼  -->
<!-- 列印結果如下:import m3... -->

<!-- js_m3內容如下:

    console.log("import m3...");

-->

 

匯出模組

  ES6使用基於檔案的模組,即一個檔案一個模組。

  可以使用export來將模組中的介面進行匯出,匯出方式分為以下幾種:

 

  1.單個匯出

  2.預設匯出

  3.多個匯出

  4.混合匯出

  5.別名匯出

 

  另外,ES6的匯出是是以引用方式匯出,無論是標量還是物件,即模組內部變數發生變化將影響已經匯入的變數。

 

單個匯出


  下面將使用export來將模組中的介面進行單個單個的匯出。

 

export let module_name = "js_m3.js";

export function test(){
        console.log("測試功能");
}

export class User{
        constructor(username){
                this.username = username;
        }

        show(){
                console.log(this.username);
        }
}

 

預設匯出


  一個模組中,只能預設匯出一個介面。

 

  如果預設匯出的是一個類,那麼該類就可以不用起類名,此外函式同理。

export let module_name = "js_m3.js";

export function test(){
        console.log("測試功能");
}

export default class{  // 預設匯出
        constructor(username){
                this.username = username;
        }

        show(){
                console.log(this.username);
        }
}

 

多個匯出


  可以使用exprot{}的形式進行介面的批量多個匯出。

 

let module_name = "js_m3.js";

function test() {
        console.log("測試功能");
}

class User {
        constructor(username) {
                this.username = username;
        }

        show() {
                console.log(this.username);
        }
}

export { module_name, test, User };

 

混合匯出


  使用export default 匯出預設介面,使用 export {} 批量匯入普通介面

let module_name = "js_m3.js";

function test() {
        console.log("測試功能");
}

export default class {
        constructor(username) {
                this.username = username;
        }

        show() {
                console.log(this.username);
        }
}

export { module_name, test };

 

  同時也可以使用as來為一個匯出的介面取別名,如果該介面別名為default則將該介面當做預設匯出。

let module_name = "js_m3.js";

function test() {
        console.log("測試功能");
}

class User {
        constructor(username) {
                this.username = username;
        }

        show() {
                console.log(this.username);
        }
}

export { module_name, test, User as default };

 

別名匯出


  使用as來為匯出的export {}中的匯出介面起一個別名,當匯入時也應該使用匯出介面的別名進行接收。

  當一個介面的別名為default時,該介面將當做預設匯出。

 

let module_name = "js_m3.js";

function test() {
        console.log("測試功能");
}

class User {
        constructor(username) {
                this.username = username;
        }

        show() {
                console.log(this.username);
        }
}

export { module_name as m_name, test as m_tst, User as default };

 

匯入模組

  使用importfrom進行靜態的模組的匯入,注意匯入時必須將匯入語句放在頂層。

  模組的匯入分為以下幾部分:

 

  1.具名匯入

  2.批量匯入

  3.預設匯入

  4.混合匯入

  5.別名匯入

  6.動態匯入

 

具名匯入


  具名匯入應該注意與匯出的介面名一致。

 

  下面是模組匯出的程式碼:

let module_name = "js_m3.js";

function test() {
        console.log("測試功能");
}

class User {
        constructor(username) {
                this.username = username;
        }

        show() {
                console.log(this.username);
        }
}

export { module_name, test, User };

 

  使用具名匯入:

<script type="module">

    import { module_name, test, User} from "./js_m3.js"; 

    console.log(module_name);  // js_m3.js

    test(); // 測試功能

    let u1 = new User("雲崖");  

    u1.show(); // 雲崖

</script>

 

批量匯入


  如果匯入的內容過多,可使用*進行批量匯入,注意批量匯入後應該使用as來取一個別名方便呼叫。

 

  下面是模組匯出的程式碼:

let module_name = "js_m3.js";

function test() {
        console.log("測試功能");
}

class User {
        constructor(username) {
                this.username = username;
        }

        show() {
                console.log(this.username);
        }
}

export { module_name, test, User };

 

  使用批量匯入:

<script type="module">

    import * as m3 from "./js_m3.js";   // 別名為m3,下面使用都要以m3開頭

    console.log(m3.module_name);  // js_m3.js

    m3.test(); // 測試功能

    let u1 = new m3.User("雲崖");  

    u1.show(); // 雲崖

</script>

 

預設匯入


  使用預設匯入時不需要用{}進行接收,並且可以使用任意名字來接收預設匯出的介面。

 

  下面是模組匯出的程式碼:

let module_name = "js_m3.js";

function test() {
        console.log("測試功能");
}

class User {
        constructor(username) {
                this.username = username;
        }

        show() {
                console.log(this.username);
        }
}

export { module_name, test, User as default };

 

  使用預設匯入,我們只匯入預設匯出的介面,可以隨便取一個名字。

<script type="module">

    import m3U from "./js_m3.js";  

    let u1 = new m3U("雲崖");  

    u1.show(); // 雲崖

</script>

 

混合匯入


  當一個模組中匯出的又有預設匯出的介面,又有其他的匯出介面時,我們可以使用混合匯入。

  使用{}來接收其他的匯出介面,對於預設匯出的介面而言只需要取一個名字即可。

 

  下面是模組匯出的程式碼:

let module_name = "js_m3.js";

function test() {
        console.log("測試功能");
}

class User {
        constructor(username) {
                this.username = username;
        }

        show() {
                console.log(this.username);
        }
}

export { module_name, test, User as default };

 

  使用混合匯入:

<script type="module">

    import m3U, { module_name, test } from "./js_m3.js";  

    console.log(module_name);  // js_m3.js

    test();  // 測試功能

    let u1 = new m3U("雲崖");

    u1.show(); // 雲崖

</script>

 

別名匯入


  為了防止多個模組下介面名相同,我們可以使用as別名匯入,再使用時也應該按照別名進行使用。

 

  下面是m1模組匯出的程式碼:

let module_name = "js_m1";

let show = function () {
        console.log("js_m1.show");
}

export { module_name, show };

 

  下面是m2模組匯出的程式碼:

let module_name = "js_m2";

let show = function () {
        console.log("js_m2.show");
}

export { module_name, show };

 

  下面是使用別名匯入這兩個模組的介面並進行使用:

<script type="module">

    import { module_name as m1_name, show as m1_show } from "./js_m1.js";
    import { module_name as m2_name, show as m2_show } from "./js_m2.js";

    console.log(m1_name);  // js_m1
    console.log(m2_name);  // js_m2

    m1_show();  // js_m1.show
    m2_show();  // js_m2.show

</script>

 

動態匯入


  使用importfrom的匯入方式屬於靜態匯入,必須將匯入語句放在最頂層,如果不是則丟擲異常。

 

  這是模組中的匯出介面:

export function test() {
        console.log("測試功能");
}

 

  如果我們想在某種特定條件下才匯入並呼叫改介面,使用importfrom的方式會丟擲異常。

<script type="module">

    if (true) {

        import { test } from "./js_m3.js"; // Error

        test();  // 想在特定條件下執行模組中的測試功能
    }

</script>

 

  這個時候就需要用到動態匯入,使用 import() 函式可以動態匯入,實現按需載入,它返回一個 promise 物件。

<script type="module">

    if (true) {

        let m3 = import("./js_m3.js");

        m3.then((module)=> module.test()); // 測試功能

    }

</script>

 

  我們可以使用解構語法來將模組中的介面一個一個全部拿出來。

<script type="module">

    if (true) {

        let m3 = import("./js_m3.js");

        m3.then(({ test, }) => test()); // 拿出test介面

    }

</script>

 

合併使用

  如果有多個模組都需要被使用,我們可以先定義一個Js檔案將這些需要用到的模組中的介面做一個合併,然後再將該檔案匯出即可。

  合併匯出請將exportfrom結合使用。

 

// js_m1

export default class{  // 預設匯出
        static register(){
                console.log("註冊功能");
        }
}

 

// js_m2

export class Login{
        static login(){
                console.log("登入功能");
        }
}

export function test(){
        console.log("js_m2測試功能");
}

 

// index.js

// 合併匯出

import js_m1 from "./js_m1.js";

// js_m1中有介面是預設匯出,因此我們需要不同的匯出方式 , 注意這裡就匯出了一個介面,即js_m1的註冊類
export {default as js_m1_register} from "./js_m1.js";

// 匯出js_m2中的介面,共匯出兩個介面。登入類和測試函式。
export * as js_m2 from "./js_m2.js";

 

  匯入與使用:

<script type="module">

    import * as index from "./index.js";

    index.js_m1_register.register();  // 註冊功能

    index.js_m2.Login.login();  // 登入功能

    index.js_m2.test();  //  js_m2測試功能

</script>

 

指令總結

 

表示式說明
export function show(){} 匯出函式
export const name="Yunya" 匯出變數
export class User{} 匯出類
export default show 預設匯出

const name = "Yunya"

export {name}

匯出已經存在變數
export {name as m1_name} 別名匯出
import m1_default from './m1_js.js' 匯入預設匯出
import {name,show} from '/m1_js.js' 匯入命名匯出
Import {name as m1_name,show} from 'm1_js.js' 別名匯入
Import * as m1 from '/m1_js.js' 匯入全部介面

 

編譯打包

  由於module語法是Es6推出的,所以對老舊的瀏覽器相容不太友好,這個時候就需要用到打包工具進行打包處理使其能讓老舊的瀏覽器上進行相容。

 

  首先登入 https://nodejs.org/en/ 官網下載安裝Node.js,我們將使用其他的npm命令,npm用來安裝第三方類庫。

 

  在命令列輸入 node -v 顯示版本資訊表示安裝成功。

 

安裝配置


  cd到你的專案路徑,並使用以下命令生成配置檔案 package.json

 

npm init -y

 

  修改package.json新增打包命令

...
"main": "index.js",
"scripts": {
    "dev": "webpack --mode development --watch"  // 新增這一句
},
...

 

  安裝webpack工具包,如果安裝慢可以使用淘寶 cnpm 命令

npm i webpack webpack-cli --save-dev

 

目錄結構


 

index.html

--dist #壓縮打包後的檔案

--src

----index.js #合併入口

----style.js //模組

 

  index.html內容如下

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
  </head>
  <body>
    <script src="dist/main.js"></script>
  </body>
</html>

 

  index.js內容如下

import style from "./style";
new style().init();

 

  style.js

export default class User {
  constructor() {}
  init() {
    document.body.style.backgroundColor = "green";
  }
}

 

執行打包


  執行以下命令將生成打包檔案到 dist目錄,因為在命令中新增了 --watch引數,所以原始檔編輯後自動生成打包檔案。

npm run dev

 

相關文章