Web應用開發中的幾個問題

Spockwang發表於2014-01-20

 Introduction

  由於Ajax技術在Gmail中的成功應用和高效能的V8引擎的推出使得編寫Web應用變得流行 起來,使用前端技術也可以編寫具有複雜互動的應用。相對於native應用,Web應用具 有如下優點:

  • 跨平臺,開發和維護成本低;
  • 升級和釋出方便,沒有版本的概念,隨時隨地釋出,使用者沒有感知,不需要安裝;
  • 響應式設計(Responsive Design)使得Web應用可以跨平臺,同一份程式碼自適應各種 螢幕大小
  • 即使最終不採用Web應用方案,也很適合開發原型

  當然,Web應用也不是沒有缺點。由於不同平臺和廠商的瀏覽器並不完全一樣,跨平臺 也有一些相容成本。另外,Web應用的效能不如native應用,互動有時候不是很流暢, 再加上HTML5的API上的限制,使得有些功能採用Web應用不太合適。由於這些原因,結 合兩者優點的混合方案變得流行起來(比如微信、手機QQ和手機QQ瀏覽器中會嵌入一 些Web頁面)。

  根據筆者的開發經驗,下面總結一些Web應用開發過程中的要面臨的幾個問題。

 模組化程式設計

  模組化程式設計是編寫大規模應用必不可少的一個特性,與其它主流的程式語言相比 Javascript沒有對模組提供直接的支援,更不用說維護模組之間的依賴關係,這使得維 護Javascript程式碼變得異常困難,在<script>標籤中包含程式碼的順序需要人工維護。

  要支援模組化程式設計必須解決兩個問題:

  1. 支援編寫模組併為模組命名,防止名字衝突和全域性變數的使用;
  2. 支援顯示指定模組之間的依賴關係,並在程式執行時自動載入依賴的模組。

  Douglas Crockford在”Javascript: The Good Parts”一書中提出的Module Pattern利 用Javascript的閉包技術來模擬模組的概念,防止名字衝突和全域性變數的使用。這解 決了第一個問題。

var moduleName = function () {
    // Define private variables and functions
    var private = ...

    // Return public interface.
    return {
        foo: ...
    };
}();

  為了解決第二個問題CommonJS組織定義了 AMD規範方便 開發者顯示指定模組之間的依賴關係,並在需要時載入依賴的模組。 RequireJS是AMD規範的一個比較流行的實現。

  首先我們在a.js中定義模組A.

define(function () {
    return {
        color: "black",
        size: 10
    };
});

  然後定義模組B依賴模組A.

define(["a"], function (A) {
    // ...
});

  當模組B執行時RequireJS保證模組A已被載入。具體細節可參考RequireJS官方文 檔。

 指令碼載入

  最簡單的指令碼載入方式是放在<head>載入。

<head>
  <script src="base.js" type="text/javascript"></script>
  <script src="app.js" type="text/javascript"></script>
</head>

  其缺點是:

  1. 載入和解析是順序是同步執行的,先下載base.js然後解析和執行,然後再下載 app.js;
  2. 載入指令碼時還會阻塞對<script>之後的DOM元素的渲染。

  為了緩解這些問題,現在的普遍做法是將<script>放在<body>的底部。

  <script src="base.js" type="text/javascript"></script>
  <script src="app.js" type="text/javascript"></script>
</body>

  但並不是所有的指令碼都可以放在<body>的底部,比如有些邏輯要在頁面渲染時執行, 不過大多數指令碼沒有這樣的要求。

  將指令碼放在<body>底部仍然沒有解決順序下載的問題,一些瀏覽器廠商也意識到了 這個問題並開始支援非同步下載。HTML5也提供了標準的解決方案:

<script src="base.js" type="text/javascript" async></script>
<script src="app.js" type="text/javascript" async></script>

  標上async屬性的指令碼表明你沒有在裡面使用document.write之類的程式碼。瀏覽器 將非同步下載和執行這些指令碼,並且不會組織DOM樹的渲染。但是這會導致另一個問題: 由於是非同步執行,app.js可能在base.js之前執行,如果它們之間有依賴關係這將 導致錯誤。

  講到這裡從開發者角度來看我們其實需要的是這些特性:

  1. 非同步下載,不要阻塞DOM的渲染;
  2. 按照模組的依賴關係解析和執行指令碼。

  所以指令碼的載入其實需要與模組化程式設計問題結合起來解決。RequireJS不僅記錄了模 塊之間的依賴關係,並且提供了根據依賴關係的按需載入和執行(詳情請參考 RequireJS官方文件)。

  關於指令碼載入的更多方案請看 這裡.

 靜態資原始檔的部署

  這裡的靜態資原始檔是指CSS、Javascript和CSS需要的一些圖片檔案。它們的部署需 要考慮兩個問題:

  1. 下載速度
  2. 版本管理

  靜態資原始檔的一個特點變化不頻繁,且與使用者身份無關(即與Cookie無關),因此 很適合快取。另一方面,一旦靜態資原始檔變化時,瀏覽器必須從Web伺服器下載最新 的版本。當釋出新版本的Web應用時,並不是所有使用者馬上就用上新版本,老版本和新 版本將會共存,這就涉及到版本匹配問題。老版本的應用需要下載老版本的CSS和 Javascript,新版本的應用需要下載新版本的靜態資源。

  1. 為了防止版本不一致,每當釋出新版本的應用時靜態資原始檔都需要改名,讓舊的 HTML引用舊的靜態檔案,新的HTML引用新的靜態檔案。一個常見辦法就是在檔名 中加時間戳;
  2. 為了防止懸掛引用,資原始檔應該比HTML先發布。

  上述方案可以解決版本問題,這樣每個靜態檔案的快取時間可以設定得任意大,防止 重複下載,同時在新版本釋出時瀏覽器將及時更新。

  為解決下載速度問題,可以考慮以下幾個方案:

  1. 合併靜態檔案以免檔案數量過多,過多的檔案需要更多的連線來下載,瀏覽器通常 對同一個域名的連線數量有限制;
  2. 壓縮靜態檔案;為了可讀性,CSS和Javascript通常有很多空行、縮排和註釋,這 些在釋出時都可以去掉;
  3. 靜態檔案通常與Cookie沒有關係,所以為了減小傳輸大小和增加快取命中率(快取 的key需要考慮Cookie),靜態檔案最好託管在沒有Cookie的域名上;

  最後也是最重要的,要使上述過程自動化。

 MVC程式設計模型

  Web應用採用的是事件驅動程式設計模型,與native應用是一樣的,區別僅在於基礎設施提 供的API不一樣。UI程式設計通常採用MVC設計模式,以流行的 Backbone.js為例包括如下部分:

  1. Model
    • 資料的唯一來源
    • 負責獲取和儲存資料
    • 可提供快取機制
    • 資料變化時通過事件通知其它物件
  2. View
    • 負責渲染
    • 監聽UI事件和Model事件並重繪UI
    • 渲染結果取決於兩類資料:Model和UI互動狀態
    • UI的互動狀態通常存在View物件中,有時候為了方便也存在DOM樹節點中
    • 為了降低渲染成本,儘量減少需要渲染的區域,每次當資料變化時只渲染受影響 的區域
  3. Router
    • 負責監聽URL的變化,並通知相應的View物件渲染頁面

  為了有效地使用MVC,有幾個問題需要注意。

  Model應與View完全隔離

  Model僅提供資料的訪問,不應該依賴View,因此Model不應該知道View的存在。所以 Model不能持有對任何View物件的引用。Model的資料發生變化時只能通過事件通知 View.

  View在初始化時採用委派方式監聽UI事件

  這裡有兩個關鍵點:

  1. 在初始化時監聽事件var View = Backbone.View.extend({ initialize: function () { this.$el.on(‘click’, ‘#id’, function () { // … }); } });

  除了一些特殊情況外(請看下文),所有UI事件都應該在View初始化時初始化,防止同 一個事件被繫結多次。即使有些事件是動態監聽的(有時候需要監聽,有時候有不需要 監聽,比如有些按鈕有時候是有效的,有時候又無效),也需要在初始化時監聽,然後 在事件回撥函式裡判斷是否需要處理。這樣邏輯更簡單,更容易維護。

  1. 採用委派方式監聽UI事件

  關於委派方式監聽請參考jQuery文件.

  上面已強調要在初始化時監聽事件,但是初始化時需要監聽的DOM節點可能還不存在, 所以沒法直接繫結事件,只能採用委派方式。不過採用委派方式要求事件可以冒泡。

  對於那些沒法冒泡的事件(比如<img>的load事件)只能在保證其存在的情況下直 接繫結,而不一定要在初始化時繫結。

  複雜的View組織成樹形層次結構

  函式太大了需要拆分成幾個子函式。同樣,View的邏輯如果過於複雜也應根據頁面結 構拆成幾個子View:

  1. 父View通過引用訪問子View,但是子View不應該持有父View的引用;
  2. 子View只負責自己區域的渲染,其它區域由父View負責渲染;
  3. 父View通過函式呼叫訪問子View的功能,子View通過事件與父View通訊;
  4. 子View之間不能直接通訊。

  其它技巧可檢視 Backbone技巧與模式.

 離線應用快取

  為使Web應用體驗更加流暢,可考慮使用HTML5離線應用快取,不過有以下幾點需要注 意:

  1. 不要將離線應用快取與HTTP快取機制搞混淆,前者是HTML5引入的新特性,與HTTP緩 存機制是相互獨立並存的;
  2. Cache manifest檔案不應被HTTP快取太久(通過HTTP頭Cache-Control控制快取 時間),否則釋出新版後瀏覽器不會及時監測到變化並下載新檔案;
  3. 在Cache manifest檔案的NETWORK節放一個*,否則沒有列在這個檔案的資源不 會被請求;
  4. 不適合快取的請求最好都放在NETWORK節;
  5. 如果之前使用過離線應用快取現在不想再使用了,從<html>刪除manifest屬性, 併傳送404響應給manifest檔案請求。僅僅刪除manifest屬性是沒有效的。

 線上錯誤報告

  Javascript是一個動態語言,許多檢查都是在執行時執行的,所以大多數錯誤只有執 行到的時候才能檢查到,只能在釋出前通過大量測試來發現。即使這樣仍可能有少數 沒有執行到的路徑有錯誤,這隻能通過線上錯誤報告來發現了。

window.onerror = function (errorMsg, fileLoc, linenumber) {
    var s = 'url: ' + document.URL + '\nfile:  ' + fileLoc
        + '\nline number: ' + linenumber
        + '\nmessage: ' + errorMsg;
    Log.error(s);       // 發給伺服器統計監控
    console.log(s);
};

  通常線上的Javascript都是經過了合併和壓縮的,上報的檔名和行號基本上沒法對 應到原始碼,對查錯幫助不是很大。不過最新版的Chrome支援在onerror的回撥函式 中獲取出錯時的棧軌跡:window.event.error.stack.

相關文章