淺談模組化開發

majun發表於2019-03-04

現在的前端開發

現在的前端開發, 不僅僅是完成瀏覽的基本需求,並且通常是一個單頁面應用,每一個檢視通過非同步的方式載入,這導致頁面初始化和使用過程中會載入越來越多的 JavaScript 程式碼. 如何在開發環境組織好這些碎片化的程式碼和資源,並且保證他們在瀏覽器端快速、優雅的載入和更新,就需要一個模組化系統

什麼是模組化

把函式作為模組
缺陷: 汙染全域性變數 模組成員之間沒什麼關係
物件導向思想 並使用立即執行函式 實現閉包
避免了變數汙染 同時同一模組內的成員也有了關係 在模組外部無法修改我們沒有暴露出來的變數、函式
這就是簡單的模組

期望的模組系統

模組的載入和傳輸,我們首先能想到兩種極端的方式,一種是每個模組檔案都單獨請求,另一種是把所有模組打包成一個檔案然後只請求一次。顯而易見,每個模組都發起單獨的請求造成了請求次數過多,導致應用啟動速度慢;一次請求載入所有模組導致流量浪費、初始化過程慢。這兩種方式都不是好的解決方案,它們過於簡單粗暴。

分塊傳輸,按需進行懶載入,在實際用到某些模組的時候再增量更新,才是較為合理的模組載入方案。要實現模組的按需載入,就需要一個對整個程式碼庫中的模組進行靜態分析、編譯打包的過程。

在上面的分析過程中,我們提到的模組僅僅是指JavaScript模組檔案。然而,在前端開發過程中還涉及到樣式、圖片、字型、HTML 模板等等眾多的資源。如果他們都可以視作模組,並且都可以通過require的方式來載入,將帶來優雅的開發體驗,那麼如何做到讓 require 能載入各種資源呢?在編譯的時候,要對整個程式碼進行靜態分析,分析出各個模組的型別和它們依賴關係,然後將不同型別的模組提交給適配的載入器來處理。Webpack 就是在這樣的需求中應運而生。

模組系統

script

  • 全域性作用域下容易造成變數衝突
  • 檔案只能按照 <script> 的書寫順序進行載入
  • 開發人員必須主觀解決模組和程式碼庫的依賴關係
  • 在大型專案中各種資源難以管理,長期積累的問題導致程式碼庫混亂不堪

CommonJS

伺服器端的 Node.js 遵循 CommonJS規範,該規範的核心思想是允許模組通過 require 方法來同步載入所要依賴的其他模組,然後通過 exports 或 module.exports 來匯出需要暴露的介面。

require("module");
require("../file.js");
exports.doStuff = function() {};
module.exports = someValue;
複製程式碼

// moduleA.js
module.exports = function( value ){
    return value * 2;
}
複製程式碼
// moduleB.js
var multiplyBy2 = require(`./moduleA`);
var result = multiplyBy2(4);
複製程式碼

優點:

  • 伺服器端模組便於重用
  • NPM 中已經有將近20萬個可以使用模組包
  • 簡單並容易使用

缺點:

  • 同步的模組載入方式不適合在瀏覽器環境中,同步意味著阻塞載入,瀏覽器資源是非同步載入的
  • 不能非阻塞的並行載入多個模組

AMD

define(id?, dependencies?, factory),它要在宣告模組的時候指定所有的依賴 dependencies,並且還要當做形參傳到 factory 中,對於依賴的模組提前執行,依賴前置。

define("module", ["dep1", "dep2"], function(d1, d2) {
  return someExportedValue;
});
require(["module", "../file"], function(module, file) { /* ... */ });
複製程式碼

一些用例:
定義一個名為 myModule 的模組,它依賴 jQuery 模組:

define(`myModule`, [`jquery`], function($) {
    // $ 是 jquery 模組的輸出
    $(`body`).text(`hello world`);
});
// 使用
define([`myModule`], function(myModule) {});
複製程式碼

注意:在 webpack 中,模組名只有區域性作用域,在 Require.js 中模組名是全域性作用域,可以在全域性引用。
定義一個沒有 id 值的匿名模組,通常作為應用的啟動函式:

define([`jquery`], function($) {
    $(`body`).text(`hello world`);
});
複製程式碼

依賴多個模組的定義:

define([`jquery`, `./math.js`], function($, math) {
    // $ 和 math 一次傳入 factory
    $(`body`).text(`hello world`);
});
複製程式碼

模組輸出:

define([`jquery`], function($) {

    var HelloWorldize = function(selector){
        $(selector).text(`hello world`);
    };

    // HelloWorldize 是該模組輸出的對外介面
    return HelloWorldize;
});
複製程式碼

在模組定義內部引用依賴:

define(function(require) {
    var $ = require(`jquery`);
    $(`body`).text(`hello world`);
});
複製程式碼

優點:

  • 適合在瀏覽器環境中非同步載入模組
  • 可以並行載入多個模組

缺點:

  • 提高了開發成本,程式碼的閱讀和書寫比較困難,模組定義方式的語義不順暢
  • 不符合通用的模組化思維方式,是一種妥協的實現

4.CMD

define(function(require, exports, module) {
  var $ = require(`jquery`);
  var Spinning = require(`./spinning`);
  exports.doSomething = ...
  module.exports = ...
})
複製程式碼

優點:

  • 依賴就近,延遲執行
  • 可以很容易在 Node.js 中執行

缺點:

  • 依賴 SPM 打包,模組的載入邏輯偏重

5.UMD

6.ES6模組

ES6 模組的設計思想,是儘量的靜態化,使得編譯時就能確定模組的依賴關係,以及輸入和輸出的變數。CommonJS 和 AMD 模組,都只能在執行時確定這些東西。

import "jquery";
export function doStuff() {}
module "localModule" {}
複製程式碼

優點:

  • 容易進行靜態分析
  • 面向未來的 EcmaScript 標準

缺點:

  • 原生瀏覽器端還沒有實現該標準
  • 全新的命令字,新版的 Node.js才支援

實現:

AMD、CMD、webpack的區別:

從前有兩個規範,一個是AMD 一個是CMD 
RequireJS是AMD規範的實現,SeaJS是CMD規範的實現, 
一個主張提前載入依賴,一個主張延遲載入依賴
後來出現了 commonjs規範 
webpack就是支援commonjs規範的 
目前可以說是主導了前端構建格局。

CommomJS是服務端規範,node就是採用這個規範,他是同步載入,畢竟服務端不用考慮非同步。 
它是對通用的JavaScript模式的標準化嘗試,它包含有 AMD 定義 
AMD是非同步載入模組的縮寫,使用require引入模組,提倡依賴前置。 
CMD與AMD其實挺接近的,還因為有sea.js,中文資料還是比較親切的。 
還有就是AMD和CommomJS的中間者UMD 
Webpack其實就是一個打包工具,他的思想就是一切皆模組,css是模組,js是模組,圖片是模組。並且提供了一些列模組載入(各種-loader)來編譯模組。官方推薦使用commonJS規範,但是也支援CMD和AMD

無論是node應用模組,還是webpack 配置 ,均是採用CommonJS模組化規範

CommonJS 與 AMD 支援

Webpack 對 CommonJS 的 AMD 的語法做了相容, 方便遷移程式碼

不過實際上, 引用模組的規則是依據 CommonJS 來的

require(`lodash`) // 從模組目錄查詢
require(`./file`) // 按相對路徑查詢
複製程式碼

AMD 語法中, 也要注意, 是按 CommonJS 的方案查詢的

define (require, exports. module) ->
  require(`lodash`) # commonjs 當中這樣是查詢模組的 

複製程式碼

CommonJs與AMD

在一開始,我們先講一下它和以往我們所用的模組管理工具有什麼不一樣。在最開始的階段,Js並沒有這些模組機制,各種Js到處飛,得不到有效妥善的管理。後來前端圈開始制定規範,最耳熟能詳的是CommonJs和AMD。

CommonJs是應用在NodeJs,是一種同步的模組機制。它的寫法大致如下:

var firstModule = require("firstModule");
//your code...
module.export = anotherModule
複製程式碼

AMD的應用場景則是瀏覽器,非同步載入的模組機制。require.js的寫法大致如下:

define([`firstModule`], function(module){
   //your code...
   return anotherModule
})
複製程式碼

其實我們單比較寫法,就知道CommonJs是更為優秀的。它是一種同步的寫法,對Human友好,而且程式碼也不會繁瑣臃腫。但更重要的原因是,隨著npm成為主流的JavaScript元件釋出平臺,越來越多的前端專案也依賴於npm上的專案,或者自身就會發布到npm平臺。所以我們對如何可以使用npm包中的模組是我們的一大需求。所以browserify工具就出現了,它支援我們直接使用require()的同步語法去載入npm模組。

當然我們這裡不得不說的是,ES2015(ES6)裡也有了自己的模組機制,也就是說ES6的模組機制是官方規定的,我們通過babel(一種6to5的編譯器)可以使用比較多的新特性了,包括我們提到的模組機制,而它的寫法大致如下:

import {someModule} from "someModule";
// your codes...
export anotherModule;
複製程式碼

當然上面的寫法只是最基本的,還有其他的不同載入模組的寫法,可以看一下阮一峰老師的ECMAScript 6 入門或者babel的相關文件Learn ES2015。

功能特性

browserify的出現非常棒,但webpack更勝一籌!

我們來看看webpack支援哪些功能特性:

  • 支援CommonJs和AMD模組,意思也就是我們基本可以無痛遷移舊專案。
  • 支援模組載入器和外掛機制,可對模組靈活定製。特別是我最愛的babel-loader,有效支援ES6。
  • 可以通過配置,打包成多個檔案。有效利用瀏覽器的快取功能提升效能。
  • 將樣式檔案和圖片等靜態資源也可視為模組進行打包。配合loader載入器,可以支援sass,less等CSS前處理器。
  • 內建有source map,即使打包在一起依舊方便除錯。
  • 看完上面這些,可以想象它就是一個前端工具,可以讓我們進行各種模組載入,預處理後,再打包。之前我們對這些的處理是放在grunt或gulp等前端自動化工具中。有了webpack,我們無需藉助自動化工具對模組進行各種處理,讓我們工具的任務分的更加清晰。

回顧ES6模組

物件的匯出

1. export default{
        add(){}
 }
2. export fucntion add(){} 相當於 將add方法當做一個屬性掛在到exports物件
複製程式碼

物件的匯入

如果匯出的是:export default{ add(){}}
那麼可以通過  import obj from `./calc.js`
如果匯出的是:
export fucntion add(){} 
export fucntion substrict(){} 
export const PI=3.14
那麼可以通過按需載入 import {add,substrict,PI} from `./calc.js`
複製程式碼

回顧Node模組

傳統非模組化開發有如下的缺點

  1. 命名衝突
  2. 檔案依賴

前端標準的模組化規範

  1. AMD – requirejs
  2. CMD – seajs

伺服器端的模組化規範

CommonJS – Node.js

Node模組化相關的規則

  1. 如何定義模組:一個js檔案就是一個模組,模組內部的成員都是相互獨立
  2. 模組成員的匯出和引入:
    exports與module的關係:module.exports = exports = {};
    模組成員的匯出最終以module.exports為準
    如果要匯出單個的成員或者比較少的成員,一般我們使用exports匯出;如果要匯出的成員比較多,一般我們使用module.exports的方式;這兩種方式不能同時使用
var sum = function(a,b){
    return parseInt(a) + parseInt(b);
}
// 方法1
// 匯出模組成員
exports.sum = sum;
//引入模組
var module = require(`./xx.js`);
var ret = module.sum(12,13);

// 方法2
// 匯出模組成員
module.exports = sum;
//引入模組
var module = require(`./xx.js`);
module();

// // 方法1
// exports.sum = sum;
// exports.subtract = subtract;
// 
// var m = require(`./05.js`);
// var ret = m.sum(1,2);
// var ret1 = m.subtract(1,2);
// console.log(ret,ret1);
// 
// // 方法2
// module.exports = {
//     sum : sum,
//     subtract : subtract,
//     multiply : multiply,
//     divide : divide
// }
// 
// var m = require(`./05.js`);
// console.log(m);
複製程式碼

##回顧webpack

模組打包器

根據模組的依賴關係進行靜態分析,然後將這些模組按照指定的規則生成對應的靜態資源。如何在一個大規模的程式碼庫中,維護各種模組資源的分割和存放,維護它們之間的依賴關係,並且無縫的將它們整合到一起生成適合瀏覽器端請求載入的靜態資源。市面上已經存在的模組管理和打包工具並不適合大型的專案,尤其單頁面 Web 應用程式。最緊迫的原因是如何在一個大規模的程式碼庫中,維護各種模組資源的分割和存放,維護它們之間的依賴關係,並且無縫的將它們整合到一起生成適合瀏覽器端請求載入的靜態資源。
這些已有的模組化工具並不能很好的完成如下的目標:

  • 將依賴樹拆分成按需載入的塊
  • 初始化載入的耗時儘量少
  • 各種靜態資源都可以視作模組
  • 將第三方庫整合成模組的能力
  • 可以自定義打包邏輯的能力
  • 適合大專案,無論是單頁還是多頁的 Web 應用

Webpack 的特點

Webapck 和其他模組化工具有什麼區別呢?

  1. 程式碼拆分
    Webpack 有兩種組織模組依賴的方式,同步和非同步。非同步依賴作為分割點,形成一個新的塊。在優化了依賴樹後,每一個非同步區塊都作為一個檔案被打包。
  2. Loader
    Webpack 本身只能處理原生的 JavaScript 模組,但是 loader 轉換器可以將各種型別的資源轉換成 JavaScript 模組。這樣,任何資源都可以成為 Webpack 可以處理的模組。
  3. 智慧解析
    Webpack 有一個智慧解析器,幾乎可以處理任何第三方庫,無論它們的模組形式是 CommonJS、 AMD 還是普通的 JS 檔案。甚至在載入依賴的時候,允許使用動態表示式 require("./templates/" + name + ".jade")
  4. 外掛系統
    Webpack 還有一個功能豐富的外掛系統。大多數內容功能都是基於這個外掛系統執行的,還可以開發和使用開源的 Webpack 外掛,來滿足各式各樣的需求。
  5. 快速執行
    Webpack 使用非同步 I/O 和多級快取提高執行效率,這使得 Webpack 能夠以令人難以置信的速度快速增量編譯。

webpack是什麼?

CommonJS和AMD是用於JavaScript模組管理的兩大規範,前者定義的是模組的同步載入,主要用於NodeJS;而後者則是非同步載入,通過requirejs等工具適用於前端。隨著npm成為主流的JavaScript元件釋出平臺,越來越多的前端專案也依賴於npm上的專案,或者 自身就會發布到npm平臺。因此,讓前端專案更方便的使用npm上的資源成為一大需求。
web開發中常用到的靜態資源主要有JavaScript、CSS、圖片、Jade等檔案,webpack中將靜態資原始檔稱之為模組。 webpack是一個module bundler(模組打包工具),其可以相容多種js書寫規範,且可以處理模組間的依賴關係,具有更強大的js模組化的功能。Webpack對它們進行統 一的管理以及打包釋出

為什麼使用 webpack?

1. 對 CommonJS 、 AMD 、ES6的語法做了相容
2. 對js、css、圖片等資原始檔都支援打包
3. 串聯式模組載入器以及外掛機制,讓其具有更好的靈活性和擴充套件性,例如提供對CoffeeScript、ES6的支援
4. 有獨立的配置檔案webpack.config.js
5. 可以將程式碼切割成不同的chunk,實現按需載入,降低了初始化時間
6. 支援 SourceUrls 和 SourceMaps,易於除錯
7. 具有強大的Plugin介面,大多是內部外掛,使用起來比較靈活
8.webpack 使用非同步 IO 並具有多級快取。這使得 webpack 很快且在增量編譯上更加快

相關文章