1. 程式人生 > >6 JS的模組化 ES6模組化及webpack打包

6 JS的模組化 ES6模組化及webpack打包

轉自:https://blog.csdn.net/u014168594/article/details/77198729

js的模組化程序

現在前端技術日新月異,對於同一個問題痛點,各個時段有各自的解決方案,這就帶來了很大差異。今天我就打算梳理js模組化的歷史程序,講一講這些方案要做什麼,怎麼做。

js模組化程序的起因

現今的很多網頁其實可以看做是功能豐富的應用,它們擁有著複雜的JavaScript程式碼和一大堆依賴包。當一個專案開發的越來越複雜的時候,你會遇到一些問題:命名衝突(變數和函式命名可能相同),檔案依賴(引入外部的檔案數目、順序問題)等。

JavaScript發展的越來越快,超過了它產生時候的自我定位。這時候js模組化就出現了。

什麼是模組化

模組化開發是一種管理方式,是一種生產方式,一種解決問題的方案。他按照功能將一個軟體切分成許多部分單獨開發,然後再組裝起來,每一個部分即為模組。當使用模組化開發的時候可以避免剛剛的問題,並且讓開發的效率變高,以及方便後期的維護。

js模組化程序

一、早期:script標籤

這是最原始的 JavaScript 檔案載入方式,如果把每一個檔案看做是一個模組,那麼他們的介面通常是暴露在全域性作用域下,也就是定義在 window 物件中。

缺點: 
1.汙染全域性作用域 
2.只能按script標籤書寫順序載入 
3.檔案依賴關係靠開發者主觀解決

二、發展一:CommonJS規範

允許模組通過require方法來同步載入

(同步意味阻塞)所要依賴的其他模組,然後通過module.exports來匯出需要暴露的介面。

// module add.js
module.exports = function add (a, b) { return a + b; }

// main.js
var {add} = require('./math');
console.log('1 + 2 = ' + add(1,2);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

CommonJS 是以在瀏覽器環境之外構建JavaScript 生態系統為目標而產生的專案,比如在伺服器和桌面環境中。

三、發展二:AMD/CMD

(1)AMD

AMD 是 RequireJS 在推廣過程中對模組定義的規範化產出(非同步

模組定義)。

AMD標準中定義了以下兩個API:

  1. require([module], callback);
  2. define(id, [depends], callback);

require介面用來載入一系列模組,define介面用來定義並暴露一個模組。

    define(['./a', './b'], function(a, b) {  
        // 依賴必須一開始就寫好   
        a.add1()    
        ...  
        b.add2()    
        ...
    }) 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

優點: 
1、適合在瀏覽器環境中非同步載入模組 2、可以並行載入多個模組

(2)CMD

CMD 是 SeaJS 在推廣過程中對模組定義的規範化產出。(在CommomJS和AMD基礎上提出)

define(function (requie, exports, module) { 
    //依賴可以就近書寫 
    var a = require('./a'); 
    a.add1(); 
    ... 
    if (status) { 
        var b = requie('./b'); 
        b.add2(); 
    } 
}); 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

優點: 
1、依賴就近,延遲執行 2、可以很容易在伺服器中執行

(3)AMD 和 CMD 的區別

AMD和CMD起來很相似,但是還是有一些細微的差別:

1、對於依賴的模組,AMD是提前執行,CMD是延遲執行。

2、AMD推崇依賴前置;CMD推崇依賴就近,只有在用到某個模組的時候再去require。

3、AMD 的 API 預設是一個當多個用,CMD 的 API 嚴格區分,推崇職責單一

四、發展三:ES6模組化

EcmaScript6 標準增加了JavaScript語言層面的模組體系定義。

在 ES6 中,我們使用export關鍵字來匯出模組,使用import關鍵字引用模組。

// module math.jsx
export default class Math extends React.Component{}

// main.js
import Math from "./Math";
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

目前很少JS引擎能直接支援 ES6 標準,因此 Babel 的做法實際上是將不被支援的import翻譯成目前已被支援的require。

ES6詳解八:模組(Module)

基本用法

命名匯出(named exports)

可以直接在任何變數或者函式前面加上一個 export 關鍵字,就可以將它匯出。 
這種寫法非常簡潔,和平時幾乎沒有區別,唯一的區別就是在需要匯出的地方加上一個 export 關鍵字。 
比如:

export const sqrt = Math.sqrt;
export function square(x) {
    return x * x;
}
export function diag(x, y) {
    return sqrt(square(x) + square(y));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

然後在另一個檔案中這樣引用:

import { square, diag } from 'lib';
console.log(square(11)); // 121
console.log(diag(4, 3));
  • 1
  • 2
  • 3
  • 4

你可能會注意到這個奇怪的語法 { square, diag } 不就是前面講過的 destructing嗎。所以你會以為還可以這樣寫:

 import lib from 'lib';
 square = lib.square;
  • 1
  • 2
  • 3

但是其實這樣是錯的,因為 import { square, diag } from 'lib’; 是import的特有語法,並不是 destructing 語法,所以其實import的時候並不是直接把整個模組以物件的形式引入的。

如果你希望能通過 lib.square 的形式來寫,你應該這樣匯入:

 import * as lib from 'lib';
 square = lib.square;
  • 1
  • 2
  • 3

不過值得注意的一點是,如果你直接用babel編譯,執行是會報錯的。因為 babel 並不會完全編譯 modules,他只是把 ES6 的modules語法編譯成了 CMD 的語法,所以還需要用 browserify 之類的工具再次編譯一遍。 
如果你發現 browserify 找不到 lib,可以改成 from ‘./lib’ 試試。

預設匯出

大家會發現上面的寫法比較麻煩,因為必須要指定一個名字。其實很多時候一個模組只匯出了一個變數,根本沒必要指定一個名字。 
還有一種用法叫預設匯出,就是指定一個變數作為預設值匯出:

 //------ myFunc.js ------
export default function () { ... };

//------ main1.js ------
import myFunc from 'myFunc';
myFunc();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

預設匯出的時候不需要指定一個變數名,它預設就是檔名。 
這裡的區別不僅僅是不用寫名字,而是 匯出的預設值就是模組本身,而不是模組下面的一個屬性,即是 import myFunc from 'myFunc’; 而不是 import {myFunc} from 'myFunc’;

命名匯出結合預設匯出

預設匯出同樣可以結合命名匯出來使用:

export default function (obj) {
    ...
};
export function each(obj, iterator, context) {
    ...
}
export { each as forEach };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

上面的程式碼匯出了一個預設的函式,然後由匯出了兩個命名函式,我們可以這樣匯入:

 import _, { each } from 'underscore';
  • 1
  • 2

注意這個逗號語法,分割了預設匯出和命名匯出

其實這個預設匯出只是一個特殊的名字叫 default,你也可以就直接用他的名字,把它當做命名匯出來用,下面兩種寫法是等價的:

import { default as foo } from 'lib';
import foo from 'lib';
  • 1
  • 2
  • 3

同樣的,你也可以通過顯示指定 default 名字來做預設匯出, 下面兩種寫法是一樣的:

 //------ module1.js ------
export default 123;

//------ module2.js ------
const D = 123;
export { D as default };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

僅支援靜態匯入匯出

ES6規範只支援靜態的匯入和匯出,也就是必須要在編譯時就能確定,在執行時才能確定的是不行的,比如下面的程式碼就是不對的:

//動態匯入
var mylib;
if (Math.random()) {
    mylib = require('foo');
} else {
    mylib = require('bar');
}
//動態匯出
if (Math.random()) {
    exports.baz = ...;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

為什麼要這麼做,主要是兩點:

  1. 效能,在編譯階段即完成所有模組匯入,如果在執行時進行會降低速度
  2. 更好的檢查錯誤,比如對變數型別進行檢查

各種匯入和匯出方式總結

總結一下,ES6提供瞭如下幾種匯入方式:

// Default exports and named exports
import theDefault, { named1, named2 } from 'src/mylib';
import theDefault from 'src/mylib';
import { named1, named2 } from 'src/mylib';

// Renaming: import named1 as myNamed1
import { named1 as myNamed1, named2 } from 'src/mylib';

// Importing the module as an object
// (with one property per named export)
import * as mylib from 'src/mylib';

// Only load the module, don’t import anything
import 'src/mylib';
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

如下幾種匯出方式:

 //命名匯出
export var myVar1 = ...;
export let myVar2 = ...;
export const MY_CONST = ...;

export function myFunc() {
    ...
}
export function* myGeneratorFunc() {
    ...
}
export class MyClass {
    ...
}
// default 匯出
export default 123;
export default function (x) {
    return x
};
export default x => x;
export default class {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
};
//也可以自己列出所有匯出內容
const MY_CONST = ...;
function myFunc() {
    ...
}

export { MY_CONST, myFunc };
//或者在匯出的時候給他們改個名字
export { MY_CONST as THE_CONST, myFunc as theFunc };

//還可以匯出從其他地方匯入的模組
export * from 'src/other_module';
export { foo, bar } from 'src/other_module';
export { foo as myFoo, bar } from 'src/other_module';

淺談webpack打包原理

模組化機制

webpack並不強制你使用某種模組化方案,而是通過相容所有模組化方案讓你無痛接入專案。有了webpack,你可以隨意選擇你喜歡的模組化方案,至於怎麼處理模組之間的依賴關係及如何按需打包,webpack會幫你處理好的。

關於模組化的一些內容,可以看看我之前的文章:js的模組化程序

核心思想:

  1. 一切皆模組: 
    正如js檔案可以是一個“模組(module)”一樣,其他的(如css、image或html)檔案也可視作模 塊。因此,你可以require(‘myJSfile.js’)亦可以require(‘myCSSfile.css’)。這意味著我們可以將事物(業務)分割成更小的易於管理的片段,從而達到重複利用等的目的。
  2. 按需載入: 
    傳統的模組打包工具(module bundlers)最終將所有的模組編譯生成一個龐大的bundle.js檔案。但是在真實的app裡邊,“bundle.js”檔案可能有10M到15M之大可能會導致應用一直處於載入中狀態。因此Webpack使用許多特性來分割程式碼然後生成多個“bundle”檔案,而且非同步載入部分程式碼以實現按需載入。

檔案管理

  • 每個檔案都是一個資源,可以用require/import匯入js
  • 每個入口檔案會把自己所依賴(即require)的資源全部打包在一起,一個資源多次引用的話,只會打包一份
  • 對於多個入口的情況,其實就是分別獨立的執行單個入口情況,每個入口檔案不相干(可用CommonsChunkPlugin優化)

打包原理

把所有依賴打包成一個bundle.js檔案,通過程式碼分割成單元片段並按需載入。

如圖,entry.js是入口檔案,呼叫了util1.js和util2.js,而util1.js又呼叫了util2.js。

打包後的bundle.js例子

/******/ ([
/* 0 */     //模組id
/***/ function(module, exports, __webpack_require__) {

    __webpack_require__(1);     //require資原始檔id
    __webpack_require__(2);

/***/ },
/* 1 */
/***/ function(module, exports, __webpack_require__) {
    //util1.js檔案
    __webpack_require__(2);
    var util1=1;
    exports.util1=util1;

/***/ },
/* 2 */
/***/ function(module, exports) {
    //util2.js檔案
    var util2=1;
    exports.util2=util2;

/***/ }
...
...
/******/ ]);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  1. bundle.js是以模組 id 為記號,通過函式把各個檔案依賴封裝達到分割效果,如上程式碼 id 為 0 表示 entry 模組需要的依賴, 1 表示 util1模組需要的依賴
  2. require資原始檔 id 表示該檔案需要載入的各個模組,如上程式碼_webpack_require__(1) 表示 util1.js 模組,__webpack_require__(2) 表示 util2.js 模組
  3. exports.util1=util1 模組化的體現,輸出該模組