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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM