天天看點

好程式員web前端教程之前端子產品化開發

  1. 命名沖突

  首先從一個簡單的習慣開始。

  由于以前一直做 JavaEE 開發的緣故,在 JavaScript 開發中,我已經習慣将項目中的一些通用功能抽象出來,形成一個個的獨立函數,以便于實作代碼複用,如:

  function css(element, attr) { // 擷取 element 元素的 attr 對應的 CSS 屬性值

  // ...

  }

  function offset(element) { // 擷取 element 元素在文檔中的位置坐标

  并把這些封裝的函數放在統一的 tools.js 檔案中。

  如果頁面功能實作需要使用到這些函數,則直接通過 引入即可。

  前期感覺一切都好,大家也都覺得寫這樣的工具檔案對開發來說很友善,直到使用越來越多,頁面功能越來越複雜,大家要實作的需求也越來越多樣。

  這時有人就抱怨,因為引入了 tools.js 檔案,如果要定義一個能夠設定 css 屬性值的函數,那麼就隻有取另外的函數名稱(如 setCss )而不能再使用 css 這個函數名稱了,同樣如果要設定一個元素在整個文檔中的定位坐标,也不能再使用 offset 這個函數名稱,因為那樣的話,就與 tools.js 檔案中已定義的函數名稱沖突了。

  既然問題出現了,就需要解決。

  在 Java 中有一個非常實用的技術——package,它将邏輯上相關的代碼組織在一起使用“包”來進行管理,這相當于檔案系統中的檔案夾。在檔案系統中,檔案夾内是相對獨立的一個空間,不用擔心一個檔案夾和另一個檔案夾中檔案命名的沖突。在“包”中也一樣,可以解決檔案命名沖突問題,如果要在包外部再使用到包内的資源,直接通過 import 導入相關的 package 即可。類似包這樣的概念,在其它的語言(如 C#)中也稱為命名空間。

  JavaScript 中并沒有提供原生的包或命名空間的支援,但可以使用其它的方法(如對象、閉包)來實作類似的效果。

  參照 Java 的方式,我使用 JavaScript 中的對象來簡單修改 tools.js 檔案:

  var Util = {

  css : function(element, attr) {

  },

  offset : function(element) {

  };

  這樣,當引入 tools.js 檔案後,要擷取 CSS 樣式或擷取元素的文檔坐标,就通過類似 Util.css()/Util.offset() 的方法來實作。css 與 offset 的作用域是在對象 Util 下,再全局或是新對象中定義 css 屬性是不受影響的。

  Util 這個名稱也具有通用性,通常用作輔助工具定義的時候會使用到這個名稱,為了展現該名稱的唯一性,可以繼續借鑒 Java 中 package 的命名規範(域名倒置):

  var com = {};

  com.github = {};

  com.github.itrainhub = {};

  com.github.itrainhub.Util = {

  要擷取 CSS 樣式值,則可使用 com.github.itrainhub.Util.css() 方法。但這樣的寫法增加了記憶難度,YUI 中關于這一點有比較好的解決方案,先按下暫且不表。

  使用對象的寫法可解決命名沖突問題,但這種寫法也會暴露對象的所有成員,使對象内部狀态可以在對象外部被改寫。比如在對象内部存在計數器:

  _count : 0

  在對象外部可以通過 Util._count = 18; 修改該計數器的值,這是不安全的。

  像計數器這樣的變量,通常可能是作為對象的私有成員存在,不希望在對象外部還能繼續修改其值,這時,可以使用 IIFE(立即執行函數)來設計:

  var Util = (function(){

  var _count = 0;

  function _css(element, attr) {

  function _offset(element) {

  return {

  css : _css,

  offset : _offset

  })();

  這樣,在外部就不能再直接修改 _count 的值了。

  通過命名空間,的确可以解決命名沖突的問題,我們可以暫時松一口氣了。

  2. 檔案依賴

  接着 tools.js 繼續開發。

  在 tools.js 的基礎上,可以開發出一些 UI 層通用的元件,如放大鏡、輪播圖之類的,這樣各個項目中要使用這些功能的時候就不用重複造輪子了。

  通常情況下,每個 UI 元件都是以獨立的 js 檔案存在的,比如放大鏡,可以将它放到一個 zoom.js 的檔案中,當要使用到放大鏡元件時,通過 引入即可。

  但很多時候,在使用 zoom.js 之前忘記了引入 tools.js,則使用 zoom.js 就會報錯,無法保證它的正常執行。

  zoom.js 的正常執行依賴于 tools.js 的使用,上述的問題都還是比較容易解決的,但随着團隊越來越大,業務需求越來越複雜,項目中元件間的依賴關系也會變得越來越複雜。比如:

  某一天,我擴充了 zoom.js 元件的功能,但除了使用到 tools.js 外,還使用到另一個工具 js 元件:helper.js。如果項目中已有 N 個地方之前使用到了 zoom.js 元件,我就隻好全局搜尋每個引用 zoom.js 的地方,再加上對 helper.js 的引用。

  再想想,随着項目推進,我們會繼續修改 tools.js,添加更多的元件 component_1.js、component_2.js……某些元件中隻使用到 tools.js,某些隻使用到 helper.js,而某些元件既使用到了 tools.js 又使用到了 helper.js。那麼關于元件間依賴關系的維護,工作量可想而知,如果以人肉的方式來保證依賴關系的維護,簡直就要崩潰掉了。

  為什麼維護元件間的依賴關系這麼費神呢,因為 JavaScript 中天生缺少了引入其它 js 檔案的文法。在 Java 中可以通過 import 引入依賴元件,在 CSS 中也有 @import 指令去引入其它的 CSS 檔案,而 js 中卻不能自動管理依賴。

  除了檔案間的依賴關系維護不便外,如果在頁面中引入的元件非常多,我們還得保證引用元件的路徑及先後順序不能出錯,一旦出錯,又得花時間查找錯誤,可想而知工作量是很可觀了,再加上元件引入過多,又是以同步的方式加載各元件,也可能導緻浏覽器假死的現象。

  要解決這些問題,子產品化開發的價值就展現出來了。

  3. 子產品化開發

  3.1 子產品化

  所謂子產品化,就是把一個相對獨立的功能,單獨形成一個檔案,可輸入指定依賴、輸出指定的函數,供外界調用,其它都在内部隐藏實作細節。這樣即可友善不同的項目重複使用,也不會對項目造成額外的影響。

  前端使用子產品化載發主要的作用是:

  • 異步加載 js,避免浏覽器假死

  • 管理子產品間依賴關系,便于子產品的維護

  有了子產品,我們就可以更友善地使用别人的代碼,想要什麼功能,就加載什麼子產品。

  但要使用子產品的前提,是必然要形成可遵循的開發規範,使得開發者和使用者都有據可尋,否則你有你的寫法,我有我的寫法,大家沒辦法統一,也就不能很好的互用了。

  目前通用的規範是,伺服器端使用 CommonJS 規範,用戶端使用 AMD/CMD 規範。

  3.2 CommonJS

  CommonJS 規範出現是在 2009 年,Node.js 就是該規範的實作。CommonJS 規範中是這樣加載子產品的:

  var gulp = require("gulp");

  gulp.task(/ 任務 /);

  子產品的加載是同步的,這種寫法适合伺服器端,因為在伺服器讀取的子產品都是在本地磁盤,加載速度很快,可同步加載完成。但是如果在用戶端浏覽器中,因為子產品是放在伺服器端的,子產品加載取決于網絡環境,以同步的方式加載子產品時有可能出現“假死”狀況。

  今天我主要介紹針對浏覽器程式設計,不針對 Node.js 内容,是以在此關于 CommonJS 規範就不作深究,知道 require() 用于加載子產品即可。

  3.3 AMD

  由于在浏覽器端,子產品使用同步方式加載可能出現假死,那麼我們采用異步加載的方式來實作子產品加載,這就誕生了 AMD 的規範。

  AMD 即 Asynchronous Module Definition 的簡稱,表示“異步子產品定義”的意思。AMD 規範:

https://github.com/amdjs/amdjs-api/wiki/AMD

  AMD 采用異步方式加載子產品,子產品的加載不影響它後面語句的運作。所有依賴所加載子產品的語句,都被定義在一個回調函數中,等到子產品加載完畢後,回調函數才會執行。

  AMD 也采用 require() 來加載子產品,文法結構為:

  require([module], callback);

  module 是數組參數,表示所加載子產品的名稱;callback 是回調函數參數,所有子產品加載完畢後執行該回調函數。如:

  require(["jquery"], function($){

  $("#box").text("test");

  });

  3.4 CMD

  CMD 即 Common Module Definition 的簡稱,表示“通用子產品定義”的意思。CMD 規範:

https://github.com/cmdjs/specification/blob/master/draft/module.md

  CMD 規範明确了子產品的基本書寫格式和基本互動規則,該規範是在國内發展出來的,由玉伯在推廣 SeaJS 過程中規範産出的。

  SeaJS 實作了 CMD 規範。SeaJS 要解決的問題和 RequireJS 一樣,隻不過在子產品定義方式和子產品加載(運作、解析)時機上有所不同。

  3.5 AMD 與 CMD 的差別

  AMD 是 RequireJS 在推廣過程中對子產品定義的規範化産出。

  CMD 是 SeaJS 在推廣過程中對子產品定義的規範化産出。

  二者主要差別如下:

  1 對于依賴的子產品,AMD 是提前執行,CMD 是延遲執行。

  2 CMD 推崇依賴就近,AMD 推崇依賴前置。

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

  當然還有一些其它細節上的差別,具體看規範的定義就好。

  本文由好程式員web前端教育訓練學員總結。