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 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:
- require([module], callback);
- 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
為什麼要這麼做,主要是兩點:
- 效能,在編譯階段即完成所有模組匯入,如果在執行時進行會降低速度
- 更好的檢查錯誤,比如對變數型別進行檢查
各種匯入和匯出方式總結
總結一下,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的模組化程序
核心思想:
- 一切皆模組:
正如js檔案可以是一個“模組(module)”一樣,其他的(如css、image或html)檔案也可視作模 塊。因此,你可以require(‘myJSfile.js’)亦可以require(‘myCSSfile.css’)。這意味著我們可以將事物(業務)分割成更小的易於管理的片段,從而達到重複利用等的目的。 - 按需載入:
傳統的模組打包工具(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
- bundle.js是以模組 id 為記號,通過函式把各個檔案依賴封裝達到分割效果,如上程式碼 id 為 0 表示 entry 模組需要的依賴, 1 表示 util1模組需要的依賴
- require資原始檔 id 表示該檔案需要載入的各個模組,如上程式碼
_webpack_require__(1)
表示 util1.js 模組,__webpack_require__(2)
表示 util2.js 模組 exports.util1=util1
模組化的體現,輸出該模組