js模組化:詳解與面試

2022-01-12 21:00:01

JS模組化

1. 不得不說的歷史

背景

JS本身簡單的頁面設計:頁面動畫 + 表單提交
並無模組化 or 名稱空間的概念

但是因為JS的模組化需求日益增長

幼年期: 無模組化

  1. 開始需要在頁面中增加一些不同的js:動畫、表單、格式化
  2. 多種js檔案被分在不同的檔案中
  3. 不同的檔案又被同一個模板參照
  <script src="jquery.js"></script>
  <script src="main.js"></script>
  <script src="dep1.js"></script>
  //……

認可:
檔案分離是最基礎的模組化第一步
問題出現:

  • 汙染全域性作用域 => 不利於大型專案的開發以及多人團隊的共建

成長期: 模組化的雛形 - IIFE(語法側的優化)

作用域的把控

栗子:

  // 定義一個全域性變數
  let count = 0;
  // 程式碼塊1
  const increase = () => ++count;
  // 程式碼塊2
  const reset = () => {
    count = 0;
  }

  increase();
  reset();

利用函數塊級作用域

  (() => {
    let count = 0;
    // ……
  }

僅定義了一個函數,如果立即執行

  (() => {
    let count = 0;
    // ……
  }();

初步實現了一個最最最最簡單的模組

嘗試去定義一個最簡單的模組

const iifeModule = (() => {
  let count = 0;
  return {
    increase: () => ++count;
    reset: () => {
      count = 0;
    }
  }
})();

iifeModule.increase();
iifeModule.reset();

面試官可能會追問:有額外依賴時,如何優化IIFE相關程式碼

優化1: 依賴其他模組的IIFE

const iifeModule = ((dependencyModule1, dependencyModule2) => {
  let count = 0;
  return {
    increase: () => ++count;
    reset: () => {
      count = 0;
    }
  }
})(dependencyModule1, dependencyModule2);
iifeModule.increase();
iifeModule.reset();

面試1:瞭解早期jquery的依賴處理以及模組載入方案嗎?/ 瞭解傳統IIFE是如何解決多方依賴的問題
答:IIFE加傳參調配

實際上,jquery等框架其實應用了revealing的寫法:
揭示模式

const iifeModule = ((dependencyModule1, dependencyModule2) => {
  let count = 0;
  const increase = () => ++count;
  const reset = () => {
    count = 0;
  }

  return {
    increase, reset
  }
})(dependencyModule1, dependencyModule2);
iifeModule.increase();
iifeModule.reset();

成熟期:

CJS - Commonjs

node.js制定
特徵:

  • 通過module + exports 去對外暴露介面
  • 通過require來呼叫其他模組

模組組織方式
main.js 檔案

// 引入部分
const dependencyModule1 = require(./dependencyModule1);
const dependencyModule2 = require(./dependencyModule2);

// 處理部分
let count = 0;
const increase = () => ++count;
const reset = () => {
  count = 0;
}
// 做一些跟引入依賴相關事宜……

// 暴露介面部分
exports.increase = increase;
exports.reset = reset;

module.exports = {
  increase, reset
}

模組使用方式

  const { increase, reset } = require('./main.js');

  increase();
  reset();

可能被問到的問題

實際執行處理

  (function (thisValue, exports, require, module) {
    const dependencyModule1 = require(./dependencyModule1);
    const dependencyModule2 = require(./dependencyModule2);

    // 業務邏輯……
  }).call(thisValue, exports, require, module);
  • 優點:
    CommonJS率先在伺服器端實現了,從框架層面解決依賴、全域性變數汙染的問題
  • 缺點:
    主要針對了伺服器端的解決方案。對於非同步拉取依賴的處理整合不是那麼的友好。

新的問題 —— 非同步依賴

AMD規範

通過非同步載入 + 允許制定回撥函數
經典實現框架是:require.js

新增定義方式:

  // 通過define來定義一個模組,然後require進行載入
  /*
  define
  params: 模組名,依賴模組,工廠方法
   */
  define(id, [depends], callback);
  require([module], callback);

模組定義方式

  define('amdModule', ['dependencyModule1', 'dependencyModule2'], (dependencyModule1, dependencyModule2) => {
    // 業務邏輯
    // 處理部分
    let count = 0;
    const increase = () => ++count;
    const reset = () => {
      count = 0;
    }

    return {
      increase, reset
    }
  })

引入模組:

  require(['amdModule'], amdModule => {
    amdModule.increase();
  })

面試題2: 如果在AMDmodule中想相容已有程式碼,怎麼辦

  define('amdModule', [], require => {
    // 引入部分
    const dependencyModule1 = require(./dependencyModule1);
    const dependencyModule2 = require(./dependencyModule2);

    // 處理部分
    let count = 0;
    const increase = () => ++count;
    const reset = () => {
      count = 0;
    }
    // 做一些跟引入依賴相關事宜……

    return {
      increase, reset
    }
  })

面試題3: AMD中使用revealing

  define('amdModule', [], (require, export, module) => {
    // 引入部分
    const dependencyModule1 = require(./dependencyModule1);
    const dependencyModule2 = require(./dependencyModule2);

    // 處理部分
    let count = 0;
    const increase = () => ++count;
    const reset = () => {
      count = 0;
    }
    // 做一些跟引入依賴相關事宜……

    export.increase = increase();
    export.reset = reset();
  })

  define('amdModule', [], require => {
    const otherModule = require('amdModule');
    otherModule.increase();
    otherModule.reset();
  })

面試題4:相容AMD&CJS/如何判斷CJS和AMD
UMD的出現

  (define('amdModule', [], (require, export, module) => {
    // 引入部分
    const dependencyModule1 = require(./dependencyModule1);
    const dependencyModule2 = require(./dependencyModule2);

    // 處理部分
    let count = 0;
    const increase = () => ++count;
    const reset = () => {
      count = 0;
    }
    // 做一些跟引入依賴相關事宜……

    export.increase = increase();
    export.reset = reset();
  }))(
    // 目標是一次性區分CommonJSorAMD
    typeof module === "object"
    && module.exports
    && typeof define !== "function"
      ? // 是 CJS
        factory => module.exports = factory(require, exports, module)
      : // 是AMD
        define
  )
  • 優點: 適合在瀏覽器中載入非同步模組,可以並行載入多個模組
  • 缺點:會有引入成本,不能按需載入
CMD規範

按需載入
主要應用的框架 sea.js

  define('module', (require, exports, module) => {
    let $ = require('jquery');
    // jquery相關邏輯

    let dependencyModule1 = require('./dependecyModule1');
    // dependencyModule1相關邏輯
  })
  • 優點:按需載入,依賴就近
  • 依賴於打包,載入邏輯存在於每個模組中,擴大模組體積

面試題5:AMD&CMD區別
答:依賴就近,按需載入

ES6模組化

走向新時代

新增定義:
引入關鍵字 —— import
匯出關鍵字 —— export

模組引入、匯出和定義的地方:

  // 引入區域
  import dependencyModule1 from './dependencyModule1.js';
  import dependencyModule2 from './dependencyModule2.js';

  // 實現程式碼邏輯
  let count = 0;
  export const increase = () => ++count;
  export const reset = () => {
    count = 0;
  }

  // 匯出區域
  export default {
    increase, reset
  }

模板引入的地方

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

node中:

  import { increase, reset } from './esModule.mjs';
  increase();
  reset();

  import esModule from './esModule.mjs';
  esModule.increase();
  esModule.reset();

面試題6:動態模組
考察:export promise

ES11原生解決方案:

  import('./esModule.js').then(dynamicEsModule => {
    dynamicEsModule.increase();
  })
  • 優點(重要性):通過一種最統一的形態整合了js的模組化
  • 缺點(侷限性):本質上還是執行時的依賴分析

解決模組化的新思路 - 前端工程化

背景

根本問題 - 執行時進行依賴分析

前端的模組化處理方案依賴於執行時分析

解決方案:線下執行
grunt gulp webpack

  <!doctype html>
    <script src="main.js"></script>
    <script>
      // 給構建工具一個標識位
      require.config(__FRAME_CONFIG__);
    </script>
    <script>
      require(['a', 'e'], () => {
        // 業務處理
      })
    </script>
  </html>
  define('a', () => {
    let b = require('b');
    let c = require('c');

    export.run = () {
      // run
    }
  })
工程化實現

step1: 掃描依賴關係表:

  {
    a: ['b', 'c'],
    b: ['d'],
    e: []
  }

step2: 重新生成依賴資料模板

  <!doctype html>
    <script src="main.js"></script>
    <script>
      // 構建工具生成資料
      require.config({
        "deps": {
          a: ['b', 'c'],
          b: ['d'],
          e: []
        }
      })
    </script>
    <script>
      require(['a', 'e'], () => {
        // 業務處理
      })
    </script>
  </html>

step3: 執行工具,採用模組化方案解決模組化處理依賴

  define('a', ['b', 'c'], () => {
    // 執行程式碼
    export.run = () => {}
  })

優點:

  1. 構建時生成設定,執行時執行
  2. 最終轉化成執行處理依賴
  3. 可以拓展

最終: 完全體 webpack為核心的工程化 + mvvm框架元件化 + 設計模式