前端工程化(二)

模塊化開發(fā)

--- 當(dāng)下最重要的前端開發(fā)范式之一
所謂模塊化,只是思想或者理論,不是具體的某個(gè)特定的實(shí)現(xiàn)

模塊化的演變過程

  • 第一階段:文件劃分方式

    早起的模塊化完全依賴約定

    • 缺點(diǎn)

      • 污染全局作用域

      • 命名沖突

      • 無法管理模塊的依賴關(guān)系

    <!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>Modular evolution stage 1</title>
      </head>
      <body>
        <h1>模塊化演變(第一階段)</h1>
        <h2>基于文件的劃分模塊的方式</h2>
        <p>
          具體做法就是將每個(gè)功能及其相關(guān)狀態(tài)數(shù)據(jù)各自單獨(dú)放到不同的文件中,
          約定每個(gè)文件就是一個(gè)獨(dú)立的模塊,
          使用某個(gè)模塊就是將這個(gè)模塊引入到頁面中,然后直接調(diào)用模塊中的成員(變量 /
          函數(shù))
        </p>
        <p>
          缺點(diǎn)十分明顯:
          所有模塊都直接在全局工作,沒有私有空間,所有成員都可以在模塊外部被訪問或者修改,
          而且模塊一段多了過后,容易產(chǎn)生命名沖突,
          另外無法管理模塊與模塊之間的依賴關(guān)系
        </p>
        <script src="module-a.js"></script>
        <script src="module-b.js"></script>
        <script>
          // 命名沖突
          method1();
          // 模塊成員可以被修改
          name = 'foo';
        </script>
      </body>
    </html>
    
    // module a 相關(guān)狀態(tài)數(shù)據(jù)和功能函數(shù)
    var name = 'module-a';
    
    function method1() {
      console.log(name + '#method1');
    }
    
    function method2() {
      console.log(name + '#method2');
    }
    
    // module a 相關(guān)狀態(tài)數(shù)據(jù)和功能函數(shù)
    var name = 'module-a';
    
    function method1() {
      console.log(name + '#method1');
    }
    
    function method2() {
      console.log(name + '#method2');
    }
    
    // module b 相關(guān)狀態(tài)數(shù)據(jù)和功能函數(shù)
    var name = 'module-b';
    
    function method1() {
      console.log(name + '#method1');
    }
    
    function method2() {
      console.log(name + '#method2');
    }
    
  • 第二階段:命名空間方式

每個(gè)模塊掛載到對(duì)象上

  • 缺點(diǎn)

    • 內(nèi)部的所有成員任然可以被修改和訪問

    • 無法管理模塊的依賴關(guān)系

<!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>Modular evolution stage 2</title>
  </head>
  <body>
    <h1>模塊化演變(第二階段)</h1>
    <h2>每個(gè)模塊只暴露一個(gè)全局對(duì)象,所有模塊成員都掛載到這個(gè)對(duì)象中</h2>
    <p>
      具體做法就是在第一階段的基礎(chǔ)上,通過將每個(gè)模塊「包裹」為一個(gè)全局對(duì)象的形式實(shí)現(xiàn),
      有點(diǎn)類似于為模塊內(nèi)的成員添加了「命名空間」的感覺。
    </p>
    <p>
      通過「命名空間」減小了命名沖突的可能,
      但是同樣沒有私有空間,所有模塊成員也可以在模塊外部被訪問或者修改,
      而且也無法管理模塊之間的依賴關(guān)系。
    </p>
    <script src="module-a.js"></script>
    <script src="module-b.js"></script>
    <script>
      moduleA.method1();
      moduleB.method1();
      // 模塊成員可以被修改
      moduleA.name = 'foo';
    </script>
  </body>
</html>
// module a 相關(guān)狀態(tài)數(shù)據(jù)和功能函數(shù)

var moduleA = {
  name: 'module-a',

  method1: function () {
    console.log(this.name + '#method1');
  },

  method2: function () {
    console.log(this.name + '#method2');
  },
};
// module b 相關(guān)狀態(tài)數(shù)據(jù)和功能函數(shù)

var moduleB = {
  name: 'module-b',

  method1: function () {
    console.log(this.name + '#method1');
  },

  method2: function () {
    console.log(this.name + '#method2');
  },
};
  • 第三階段:立即執(zhí)行函數(shù)

每個(gè)模塊放在一個(gè)立即執(zhí)行函數(shù)中,將外部要用到的對(duì)象掛載到全局邊梁上
優(yōu)點(diǎn):

  • 避免了大量的對(duì)象被掛載到全局,防止私有成員被訪問

  • 可以通過參數(shù)傳遞依賴

缺點(diǎn)

  • 掛載的時(shí)候還是會(huì)有命名沖突
<!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>Modular evolution stage 3</title>
  </head>
  <body>
    <h1>模塊化演變(第三階段)</h1>
    <h2>
      使用立即執(zhí)行函數(shù)表達(dá)式(IIFE:Immediately-Invoked Function
      Expression)為模塊提供私有空間
    </h2>
    <p>
      具體做法就是將每個(gè)模塊成員都放在一個(gè)函數(shù)提供的私有作用域中,
      對(duì)于需要暴露給外部的成員,通過掛在到全局對(duì)象上的方式實(shí)現(xiàn)
    </p>
    <p>
      有了私有成員的概念,私有成員只能在模塊成員內(nèi)通過閉包的形式訪問。
    </p>
    <script src="module-a.js"></script>
    <script src="module-b.js"></script>
    <script>
      moduleA.method1();
      moduleB.method1();
      // 模塊私有成員無法訪問
      console.log(moduleA.name); // => undefined
    </script>
  </body>
</html>
// module a 相關(guān)狀態(tài)數(shù)據(jù)和功能函數(shù)

(function () {
  var name = 'module-a';

  function method1() {
    console.log(name + '#method1');
  }

  function method2() {
    console.log(name + '#method2');
  }

  window.moduleA = {
    method1: method1,
    method2: method2,
  };
})();
// module b 相關(guān)狀態(tài)數(shù)據(jù)和功能函數(shù)

(function () {
  var name = 'module-b';

  function method1() {
    console.log(name + '#method1');
  }

  function method2() {
    console.log(name + '#method2');
  }

  window.moduleB = {
    method1: method1,
    method2: method2,
  };
})();

模塊化規(guī)范

歷史

早期的模塊化規(guī)范,Commonjs 規(guī)范不適合瀏覽器,在 node 中同步加載模塊依賴。

- 一個(gè)文件就是一個(gè)模塊
- 每個(gè)模塊都有單獨(dú)的作用域
- 通過 module.exports 導(dǎo)出模塊
- 通過 require 函數(shù)載入模塊

于是在瀏覽器中又提出了 AMD(Asyncchronous Module Definition)

Require.js 實(shí)現(xiàn)了這個(gè)規(guī)范。目前大多數(shù)的對(duì)三方庫支持 AMD 規(guī)范缺缺點(diǎn):

  • 使用起來很復(fù)雜

  • 當(dāng)我們模塊劃分很細(xì)的時(shí)候 js 文件就會(huì)請(qǐng)求的很頻繁

AMD 規(guī)范是前端模塊化演進(jìn)道路上的一步,的歷史長(zhǎng)河中進(jìn)了一步,是一種妥協(xié)的實(shí)現(xiàn)方式,不算是最終的姐解決方案。除此之外,同一時(shí)期,淘寶推出了 Sea.js 庫,實(shí)現(xiàn)的是 CMD 標(biāo)準(zhǔn),但是后面讓 Require.js 兼容了

<!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>Modular evolution stage 5</title>
  </head>
  <body>
    <h1>模塊化規(guī)范的出現(xiàn)</h1>
    <h2>Require.js 提供了 AMD 模塊化規(guī)范,以及一個(gè)自動(dòng)化模塊加載器</h2>
    <script src="lib/require.js" data-main="main"></script>
  </body>
</html>
require.config({
  paths: {
    // 因?yàn)?jQuery 中定義的是一個(gè)名為 jquery 的 AMD 模塊
    // 所以使用時(shí)必須通過 'jquery' 這個(gè)名稱獲取這個(gè)模塊
    // 但是 jQuery.js 并不一定在同級(jí)目錄下,所以需要指定路徑
    jquery: './lib/jquery',
  },
});

require(['./modules/module1'], function (module1) {
  module1.start();
});
// 兼容 CMD 規(guī)范(類似 CommonJS 規(guī)范)
define(function (require, exports, module) {
  // 通過 require 引入依賴
  var $ = require('jquery');
  // 通過 exports 或者 module.exports 對(duì)外暴露成員
  module.exports = function () {
    console.log('module 2~');
    $('body').append('<p>module2</p>');
  };
});
// 因?yàn)?jQuery 中定義的是一個(gè)名為 jquery 的 AMD 模塊
// 所以使用時(shí)必須通過 'jquery' 這個(gè)名稱獲取這個(gè)模塊
// 但是 jQuery.js 并不在同級(jí)目錄下,所以需要指定路徑
define('module1', ['jquery', './module2'], function ($, module2) {
  return {
    start: function () {
      $('body').animate({ margin: '200px' });
      module2();
    },
  };
});

最佳實(shí)踐

隨著技術(shù)的發(fā)展,JavaScript 標(biāo)準(zhǔn)逐漸完善,模塊化被統(tǒng)一成了瀏覽器端的 ES Module,nodejs 中遵循 CommonJs 規(guī)范

<!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>ES Module - 模塊的特性</title>
  </head>
  <body>
    <!-- 通過給 script 添加 type = module 的屬性,就可以以 ES Module 的標(biāo)準(zhǔn)執(zhí)行其中的 JS 代碼了 -->
    <script type="module">
      console.log('this is es module');
    </script>

    <!-- 1. ESM 自動(dòng)采用嚴(yán)格模式,忽略 'use strict' -->
    <script type="module">
      console.log(this);
    </script>

    <!-- 2. 每個(gè) ES Module 都是運(yùn)行在單獨(dú)的私有作用域中 -->
    <script type="module">
      var foo = 100;
      console.log(foo);
    </script>
    <script type="module">
      console.log(foo);
    </script>

    <!-- 3. ESM 是通過 CORS 的方式請(qǐng)求外部 JS 模塊的 -->
    <!-- <script type="module" src="https://unpkg.com/jquery@3.4.1/dist/jquery.min.js"></script> -->

    <!-- 4. ESM 的 script 標(biāo)簽會(huì)延遲執(zhí)行腳本 等同于defer屬性 -->
    <script type="module" src="demo.js"></script>
    <p>需要顯示的內(nèi)容</p>
  </body>
</html>

ES Module 核心功能

import export 注意

  • 默認(rèn)導(dǎo)出的是字面量對(duì)象,非默認(rèn)導(dǎo)出則不是,且必須為{}中的成員

  • 導(dǎo)入的語法中的{}為固定的語法,并非解構(gòu)

  • 導(dǎo)出到外部的都是內(nèi)存地址的引用,并非值的拷貝

  • 導(dǎo)出的成員都是只讀的

<!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>ES Module 導(dǎo)出與導(dǎo)入 - 注意事項(xiàng)</title>
  </head>
  <body>
    <script type="module" src="app.js"></script>
  </body>
</html>
// CommonJS 中是先將模塊整體導(dǎo)入為一個(gè)對(duì)象,然后從對(duì)象中結(jié)構(gòu)出需要的成員
// const { name, age } = require('./module.js')

// ES Module 中 { } 是固定語法,就是直接提取模塊導(dǎo)出成員
import { name, age } from './module.js';

console.log(name, age);

// 導(dǎo)入成員并不是復(fù)制一個(gè)副本,
// 而是直接導(dǎo)入模塊成員的引用地址,
// 也就是說 import 得到的變量與 export 導(dǎo)入的變量在內(nèi)存中是同一塊空間。
// 一旦模塊中成員修改了,這里也會(huì)同時(shí)修改,
setTimeout(function () {
  console.log(name, age);
}, 1500);

// 導(dǎo)入模塊成員變量是只讀的
// name = 'tom' // 報(bào)錯(cuò)

// 但是需要注意如果導(dǎo)入的是一個(gè)對(duì)象,對(duì)象的屬性讀寫不受影響
// name.xxx = 'xxx' // 正常
var name = 'jack';
var age = 18;

// var obj = { name, age }

// export default { name, age }

// 這里的 `{ name, hello }` 不是一個(gè)對(duì)象字面量,
// 它只是語法上的規(guī)則而已
export { name, age };

// export name // 錯(cuò)誤的用法

// export 'foo' // 同樣錯(cuò)誤的用法

setTimeout(function () {
  name = 'ben';
}, 1000);

import 額外注意

  • 導(dǎo)入模塊的時(shí)候 import from 后的路徑必須帶文件后綴,不支持自動(dòng)定位 index

  • 導(dǎo)入本地模塊的時(shí)候 import from 后面的相對(duì)路徑必須是'./' or '../'; 絕對(duì)路徑支持從項(xiàng)目跟目錄查找 以'/'開頭; 或者完整的 url,不然會(huì)被按照導(dǎo)入三方模塊處理

  • 加載這個(gè)模塊并不提取任何成員

import {} from './module.js';
// 簡(jiǎn)寫
import './module.js';
  • 全部導(dǎo)出成員
import * as mod from './module.js';
  • import from 后面不支持變量
var modulePath = './module.js'
import {name} from modulePath; // 報(bào)錯(cuò)
  • 塊級(jí)作用域下不能使用 import
if (true) {
  import { name } from './module.js'; // 報(bào)錯(cuò)
}
  • 動(dòng)態(tài)導(dǎo)入需要使用全局提供的 import 函數(shù)
import('./module.js').then(module => {
  console.log(module);
});
  • 直接導(dǎo)入導(dǎo)出
export { for, bar } from './module.js';
  • ES Module 瀏覽器環(huán)境 Polyfill
<!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>ES Module 瀏覽器環(huán)境 Polyfill</title>
  </head>
  <body>
    <script
      nomodule
      src="https://unpkg.com/promise-polyfill@8.1.3/dist/polyfill.min.js"
    ></script>
    <script
      nomodule
      src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"
    ></script>
    <script
      nomodule
      src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"
    ></script>
    <script type="module">
      import { foo } from './module.js';
      console.log(foo);
    </script>
  </body>
</html>

注意:script 標(biāo)簽的 nomodule 屬性可以在沒有 ES Module 的環(huán)境中執(zhí)行,動(dòng)態(tài)編譯,不推薦直接在生產(chǎn)環(huán)境使用

  • 在 node 中使用 ES module

    • 在 node 環(huán)境中運(yùn)行 ES module,把 js 文件后綴改為'.mjs',在命令行中執(zhí)行 node --experimental-modules index.mjs
    // 第一,將文件的擴(kuò)展名由 .js 改為 .mjs;
    // 第二,啟動(dòng)時(shí)需要額外添加 `--experimental-modules` 參數(shù);
    
    import { foo, bar } from './module.mjs';
    
    console.log(foo, bar);
    
    // 此時(shí)我們也可以通過 esm 加載內(nèi)置模塊了
    import fs from 'fs';
    fs.writeFileSync('./foo.txt', 'es module working');
    
    // 也可以直接提取模塊內(nèi)的成員,內(nèi)置模塊兼容了 ESM 的提取成員方式
    import { writeFileSync } from 'fs';
    writeFileSync('./bar.txt', 'es module working');
    
    // 對(duì)于第三方的 NPM 模塊也可以通過 esm 加載
    import _ from 'lodash';
    _.camelCase('ES Module');
    
    // 不支持,因?yàn)榈谌侥K都是導(dǎo)出默認(rèn)成員
    // import { camelCase } from 'lodash'
    // console.log(camelCase('ES Module'))
    
  • 在 ES module 中使用 Commonjs

    // es-module.mjs
    import mod from './commonjs.js'
    console.log(mod) // 正常打印
    
    // commonJS 始終只會(huì)導(dǎo)出一個(gè)默認(rèn)成員
    // commonjs.js
    module.export = {
      foo: 'commonjs exports value'
    }
    // 該語法為上面語法的簡(jiǎn)寫
    export.foo = 'commonjs exports value'
    
  • 不能直接提取 commonjs 的成員,import 不是解構(gòu)導(dǎo)出對(duì)象

import { foo } from './commonjs.js';
console.log(foo); // 報(bào)錯(cuò)
  • 不能在 CommonJS 模塊中通過 require 載入 ES Module
// es-module.js
export const foo = 'es module export value';
const mod = require('./es-module.mjs');
console.log(mod); // 報(bào)錯(cuò)
  • commonjs 和 ES Module 在 node 中區(qū)別

common js

// 加載模塊函數(shù)
console.log(require);

// 模塊對(duì)象
console.log(module);

// 導(dǎo)出對(duì)象別名
console.log(exports);

// 當(dāng)前文件的絕對(duì)路徑
console.log(__filename);

// 當(dāng)前文件所在目錄
console.log(__dirname);

ES Module

// ESM 中沒有模塊全局成員了

// // 加載模塊函數(shù)
// console.log(require)

// // 模塊對(duì)象
// console.log(module)

// // 導(dǎo)出對(duì)象別名
// console.log(exports)

// // 當(dāng)前文件的絕對(duì)路徑
// console.log(__filename)

// // 當(dāng)前文件所在目錄
// console.log(__dirname)

// -------------

// require, module, exports 自然是通過 import 和 export 代替

// __filename 和 __dirname 通過 import 對(duì)象的 meta 屬性獲取
// const currentUrl = import.meta.url
// console.log(currentUrl)

// 通過 url 模塊的 fileURLToPath 方法轉(zhuǎn)換為路徑
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
console.log(__filename);
console.log(__dirname);
  • 新版本 node 中進(jìn)一步支持 ESModule
// 在 package.json 中設(shè)置type,默認(rèn)以ES Module方式工作,不用在改擴(kuò)展名了 .mjs=>.js
{
  "type": "module"
}
//  如果要繼續(xù)支持commonjs規(guī)范,則可以把js文件后綴改為'.cjs'

  • 舊版本 node 中也可以通過 babel-node 支持 ES Module 特性
$ yarn add @babel/node @babel/core @babel/preset-env  -D
// or
$ yarn add @babel/node @babel/core @babel/plugin-transform-modules-commonjs  -D
$ yarn babel-node [文件名]
// .babelrc
{
  "plugins": [
    "@babel/plugin-transform-modules-commonjs"
  ]
}
// or
{
  "presets": ["@babel/preset-env"]
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容