開篇概述
Ajax技術在Gmail中的成功應用,和高性能的V8引擎的推出,使得編寫Web應用變得流行起來,使用前端技術也可以編寫具有復雜交互的應用。相對於原生應用,Web應用程式具有以下優點:
跨平台,開發和維護成本低;
升級和發布方便,沒有版本的概念,隨時隨地發布,用戶沒有感知,不需要安裝;
響應式設計(Responsive Design)讓Web應用可以跨平台,同一份程式碼自適應各種螢幕大小
即使最終不採用Web應用方案,也很適合開發原型
當然,Web應用也不是沒有缺點。由於不同平台和廠商的瀏覽器並不完全一樣,跨平台也有一些相容成本。另外,Web應用的效能不如native應用,互動有時候不是很流暢, 再加上HTML5的API上的限制,讓有些功能採用Web應用不太適合。由於這些原因,結合兩者優點的混合方案變得流行起來(例如微信、手機QQ和手機QQ瀏覽器中會嵌入一 些Web頁面)。
根據筆者的開發經驗,以下總結一些Web應用開發過程中的要面臨的幾個問題。
模組化程式設計
模組化程式設計是編寫大規模應用必不可少的一個特性,與其它主流的程式語言相比Javascript沒有對模組提供直接的支持,更不用說維護模組之間的依賴關係,這使得維護Javascript程式碼變得異常困難,在<script>標籤中包含程式碼的順序需要人工維護。 </script>
要支援模組化程式必須解決兩個問題:
支援編寫模組並為模組命名,防止名字衝突和全域變數的使用;
支援顯示指定模組之間的依賴關係,並在程式執行時自動加載依賴的模組。
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> <script src="base.js" type="text/javascript"></script> <script src="app.js" type="text/javascript"></script> </head>
其缺點是:
載入和解析是順序是同步執行的,先下載base.js然後解析和執行,然後再下載 app.js;
載入腳本時還會阻塞對<script>之後的DOM元素的渲染。 </script>
為了緩解這些問題,現在的普遍做法是將<script>放在<body>的底部。 </script>
<script src="base.js" type="text/javascript"></script> <script src="app.js" type="text/javascript"></script> </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之前執行,如果它們之間有依賴關係這將導致錯誤。
講到這裡從開發者角度來看我們其實需要的是這些特性:
異步下載,不要阻塞DOM的渲染;
按照模組的依賴關係解析和執行腳本。
所以腳本的載入其實需要與模組化程式設計問題結合起來解決。 RequireJS不僅記錄了模 區塊之間的依賴關係,並且提供了根據依賴關係的按需載入和執行(詳情請參考 RequireJS官方文件)。
關於腳本載入的更多方案請看 這裡.
靜態資源檔案的部署
這裡的靜態資源檔案是指CSS、Javascript和CSS需要的一些圖片檔案。它們的部署需 要考慮兩個問題:
下載速度
版本管理
靜態資源檔案的一個特點變更不頻繁,且與使用者身分無關(即與Cookie無關),因此很適合快取。另一方面,一旦靜態資源檔案變更時,瀏覽器必須從網路伺服器下載最新 的版本。當發布新版本的網路應用程式時,並不是所有使用者馬上就用上新版本,舊版和新版本將會共存,這就牽涉到版本比對問題。舊版的應用程式需要下載舊版的CSS和 Javascript,新版本的應用程式需要下載新版本的靜態資源。
為了防止版本不一致,每當發布新版本的應用時靜態資源文件都需要改名,讓舊的 HTML引用舊的靜態文件,新的HTML引用新的靜態文件。一個常見辦法就是在檔案名稱 中加上時間戳記;
為了防止懸掛引用,資源檔案應該比HTML先發布。
上述方案可以解決版本問題,這樣每個靜態檔案的快取時間可以設定得任意大,防止重複下載,同時在新版本發佈時瀏覽器將及時更新。
為解決下載速度問題,可以考慮以下幾個方案:
合併靜態檔案以免檔案數量過多,過多的檔案需要更多的連線來下載,瀏覽器通常對同一個網域的連線數量有限制;
壓縮靜態檔案;為了可讀性,CSS和Javascript通常有很多空行、縮排和註釋,這些在發佈時都可以去掉;
靜態檔案通常與Cookie沒有關係,所以為了減小傳輸大小和增加快取命中率(快取的key需要考慮Cookie),靜態檔案最好託管在沒有Cookie的網域上;
最後也是最重要的,要使上述流程自動化。
MVC程式設計模型
Web應用程式採用的是事件驅動程式設計模型,與native應用是相同的,差異僅在於基礎架構提供的API不一樣。 UI程式設計通常採用MVC設計模式,以流行的 Backbone.js為例包含以下部分:
Model
資料的唯一來源
負責取得和儲存資料
可提供快取機制
資料變更時透過事件通知其它物件
View
負責渲染
監聽UI事件和Model事件並重繪UI
渲染結果取決於兩類資料:Model和UI互動狀態
UI的互動狀態通常存在View物件中,有時為了存在View物件中,有時為了存在View方便也存在DOM樹節點中
為了降低渲染成本,盡量減少需要渲染的區域,每次當資料變化時只渲染受影響的區域
Router
負責監聽URL的變化,並通知對應的View對象渲染頁
為了有效地使用MVC,有幾個問題要注意。
Model應與View完全隔離
Model僅提供資料的訪問,不應該依賴View,因此Model不應該知道View的存在。所以 Model不能持有對任何View物件的引用。 Model的資料變更時只能透過事件通知View.
View在初始化時採用委派方式監聽UI事件
這裡有兩個關鍵點:
在初始化時監聽事件var View = Backbone.View.extend( { initialize: function () { this.$el.on('click', '#id', function () { // … }); } });
除了一些特殊情況外(見下文),所有UI事件都應該在View初始化時初始化,防止同一個事件被綁定多次。即使有些事件是動態監聽的(有時候需要監聽,有時候有不需要監聽,例如有些按鈕有時候是有效的,有時又無效),也需要在初始化時監聽,然後在事件回呼函數裡判斷是否需要處理。這樣邏輯更簡單,也更容易維護。
採用委派方式監聽UI事件
關於委派方式監聽請參考jQuery文檔.
上面已強調要在初始化時監聽事件,但是初始化時需要監聽的DOM節點可能還不存在, 所以沒法直接綁定事件,只能採用委派方式。不過採用委派方式要求事件可以冒泡。
對於那些沒法冒泡的事件(例如的load事件)只能在保證其存在的情況下直 接綁定,而不一定要在初始化時綁定。
複雜的View組織成樹形層次結構
函數太大了需要拆分成幾個子函數。同樣,View的邏輯如果過於複雜也應根據頁面結構拆成幾個子View:
父View透過引用訪問子View,但是子View不應該持有父View的引用;
子View只負責自己區域的渲染,其它區域由父View負責渲染;
父View透過函數呼叫存取子View的功能,子View透過事件與父View通訊;
子View之間無法直接通訊。
其它技巧可查看 Backbone技巧與模式.
離線應用緩存
為使Web應用體驗更加流暢,可考慮使用HTML5離線應用緩存,不過有以下幾點需要注意:
不要將離線應用緩存與HTTP快取機制搞混淆,前者是HTML5引入的新特性,與HTTP快取機制是相互獨立並存的;
Cache manifest文件不应被HTTP缓存太久(通过HTTP头Cache-Control控制缓存 时间),否则发布新版后浏览器不会及时监测到变化并下载新文件;
在Cache manifest文件的NETWORK节放一个*,否则没有列在这个文件的资源不 会被请求;
不适合缓存的请求最好都放在NETWORK节;
如果之前使用过离线应用缓存现在不想再使用了,从100db36a723c770d327fc0aef2ce13b1删除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.