前端模塊化開發

1. 前言

現在的前端開發, 通常是一個單頁面應用,每一個視圖通過異步的方式加載,這導致頁面初始化和使用過程中會加載越來越多的 JS 代碼,如何在開發環境組織好這些碎片化的代碼和資源,并且保證他們在瀏覽器端快速、優雅的加載和更新,就需要一個模塊化系統。

1.1 最簡單的模塊

其實我們曾把函數作為模塊,但會污染全局變量,并且模塊成員之間沒什么關系。這個時候我們可以運用面向對象思想,使用立即執行函數實現閉包,可以避免變量污染,同時同一模塊內的成員也有了關系,在模塊外部無法修改我們沒有暴露出來的變量、函數,這就是簡單的模塊。但是這樣處理起來麻煩,并且遠遠不夠。

1.2 期望的模塊系統

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

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

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

2. 模塊系統

2.1 script

  • 全局作用域下容易造成變量沖突
  • 文件只能按照 <script> 的書寫順序進行加載
  • 開發人員必須主觀解決模塊和代碼庫的依賴關系
  • 在大型項目中各種資源難以管理,長期積累的問題導致代碼庫混亂不堪

2.2 CommonJS

服務器端的 Node.js 遵循 CommonJS 規范,該規范的核心思想是允許模塊通過 require 方法來同步加載所要依賴的其他模塊,然后通過 exportsmodule.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 萬個可以使用模塊包
  • 簡單并容易使用

缺點:

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

2.3 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');
});

優點:

  • 適合在瀏覽器環境中異步加載模塊
  • 可以并行加載多個模塊

缺點:

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

2.4 CMD

define(function(require, exports, module) {
  var $ = require('jquery');
  var Spinning = require('./spinning');
  exports.doSomething = ...
  module.exports = ...
})

優點:

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

缺點:

  • 依賴 SPM 打包,模塊的加載邏輯偏重

2.5 UMD (暫未接觸)

2.6 ES6 模塊

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

import "jquery";
export function doStuff() {}
module "localModule" {}

優點:

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

缺點:

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

實現:

3. 模塊系統/規范對比

3.1 AMD 與 CMD

從前有兩個規范,一個是 AMD,一個是 CMD。RequireJS 是 AMD 規范的實現,SeaJS 是 CMD 規范的實現。一個主張提前加載依賴,一個主張延遲加載依賴。后來出現了 CommomJS 規范,CommomJS 是服務端規范,node 就是采用這個規范,他是同步加載,畢竟服務端不用考慮異步。

3.2 AMD 與 CommonJs

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

define(['firstModule'], function(module) {
  //your code...
  return anotherModule;
});

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

var firstModule = require('firstModule');
//your code...
module.export = anotherModule;

其實我們單比較寫法,就知道 CommonJs 是更為優秀的。它是一種同步的寫法,友好而且代碼也不會繁瑣臃腫。但更重要的原因是,隨著 npm 成為主流的 JS 組件發布平臺,越來越多的前端項目也依賴于 npm 上的項目,或者自身就會發布到 npm 平臺。所以我們對如何可以使用 npm 包中的模塊是我們的一大需求。

3.3 browserify 與 webpack

browserify 工具支持我們直接使用 require()的同步語法去加載 npm 模塊,使用不多這里就不做介紹。

  1. Webpack 其實就是一個打包工具,他的思想就是一切皆模塊,css 是模塊,js 是模塊,圖片是模塊。并且提供了一些列模塊加載(各種-loader)來編譯模塊。官方推薦使用 commonJS 規范,但是也支持 CMD 和 AMD。無論是node應用模塊,還是webpack配置 ,均是采用CommonJS模塊化規范。

  2. webpack 支持哪些功能特性:

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

4. 相關知識

4.1 ES6 模塊

4.1.1 對象的導出

1. export default{
        add(){}
 }
2. export fucntion add(){} 相當于 將add方法當做一個屬性掛在到exports對象

4.1.2 對象的導入

如果導出的是: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'

4.2 Node 模塊

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

  1. 命名沖突
  2. 文件依賴

4.2.2 前端標準的模塊化規范

  1. AMD - requirejs
  2. CMD - seajs

4.2.3 服務器端的模塊化規范

CommonJS - Node.js

4.2.4 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);

4.3 webpack

4.3.1 模塊打包器

根據模塊的依賴關系進行靜態分析,然后將這些模塊按照指定的規則生成對應的靜態資源。如何在一個大規模的代碼庫中,維護各種模塊資源的分割和存放,維護它們之間的依賴關系,并且無縫的將它們整合到一起生成適合瀏覽器端請求加載的靜態資源。市面上已經存在的模塊管理和打包工具并不適合大型的項目,尤其單頁面 Web 應用程序。最緊迫的原因是如何在一個大規模的代碼庫中,維護各種模塊資源的分割和存放,維護它們之間的依賴關系,并且無縫的將它們整合到一起生成適合瀏覽器端請求加載的靜態資源。

這些已有的模塊化工具并不能很好的完成如下的目標:

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

4.3.2 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 能夠以令人難以置信的速度快速增量編譯。

4.3.3 webpack 是什么?

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

4.3.4 為什么使用 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 很快且在增量編譯上更加快

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 前端模塊化開發簡介 歷史上,JavaScript 一直沒有模塊(module)體系,無法將一個大程序拆分成互相依賴...
    榮兒飛閱讀 4,364評論 0 6
  • 在JavaScript發展的初期是為了實現簡單的頁面交互邏輯, 就這么一句話. 如今, 瀏覽器性能得到極大的提高,...
    HelloJames閱讀 651評論 0 2
  • CommonJS 服務器端的 Node.js 遵循 CommonJS規范,該規范的核心思想是允許模塊通過 requ...
    LiLi原上草閱讀 188評論 0 0
  • 前端模塊化開發 常見的三大模塊化框架。 CommonJS: 1.根據CommonJS規范,一個單獨的文件就是一個模...
    一長亭閱讀 351評論 0 2
  • 概念 模塊化開發,一個模塊就是一個實現特定功能的文件,有了模塊我們就可以更方便的使用別人的代碼,要用什么功能就加載...
    biu丶biubiu閱讀 248評論 0 0