Web應用開發中的幾個問題——使用javascript開發需知
Ajax技術在Gmail中的成功應用,和高性能的V8引擎的推出,使得編寫Web應用變得流行起來,使用前端技術也可以編寫具有復雜交互的應用。相對于原生應用,Web應用具有如下優點:
跨平臺,開發和維護成本低;升級和發布方便,沒有版本的概念,隨時隨地發布,用戶沒有感知,不需要安裝;響應式設計(Responsive Design)使得Web應用可以跨平臺,同一份代碼自適應各種 屏幕大小即使最終不采用Web應用方案,也很適合開發原型當然,Web應用也不是沒有缺點。由于不同平臺和廠商的瀏覽器并不完全一樣,跨平臺也有一些兼容成本。另外,Web應用的性能不如native應用,交互有時候不是很流暢, 再加上HTML5的API上的限制,使得有些功能采用Web應用不太合適。由于這些原因,結合兩者優點的混合方案變得流行起來(比如微信、手機QQ和手機QQ瀏覽器中會嵌入一 些Web頁面)。
根據筆者的開發經驗,下面總結一些Web應用開發過程中的要面臨的幾個問題。
模塊化編程模塊化編程是編寫大規模應用必不可少的一個特性,與其它主流的編程語言相比 Javascript沒有對模塊提供直接的支持,更不用說維護模塊之間的依賴關系,這使得維 護Javascript代碼變得異常困難,在<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>加載。
<head> <script src='http://www.aoyou183.cn/bcjs/base.js' type='text/javascript'></script> <script src='http://www.aoyou183.cn/bcjs/app.js' type='text/javascript'></script></head>
其缺點是:
加載和解析是順序是同步執行的,先下載base.js然后解析和執行,然后再下載app.js;加載腳本時還會阻塞對<script>之后的DOM元素的渲染。為了緩解這些問題,現在的普遍做法是將<script>放在<body>的底部。
<script src='http://www.aoyou183.cn/bcjs/base.js' type='text/javascript'></script> <script src='http://www.aoyou183.cn/bcjs/app.js' type='text/javascript'></script></body>
但并不是所有的腳本都可以放在<body>的底部,比如有些邏輯要在頁面渲染時執行, 不過大多數腳本沒有這樣的要求。
將腳本放在<body>底部仍然沒有解決順序下載的問題,一些瀏覽器廠商也意識到了 這個問題并開始支持異步下載。HTML5也提供了標準的解決方案:
<script src='http://www.aoyou183.cn/bcjs/base.js' type='text/javascript' async></script><script src='http://www.aoyou183.cn/bcjs/app.js' type='text/javascript' async></script>
標上async屬性的腳本表明你沒有在里面使用document.write之類的代碼。瀏覽器 將異步下載和執行這些腳本,并且不會組織DOM樹的渲染。但是這會導致另一個問題: 由于是異步執行,app.js可能在base.js之前執行,如果它們之間有依賴關系這將導致錯誤。
講到這里從開發者角度來看我們其實需要的是這些特性:
異步下載,不要阻塞DOM的渲染;按照模塊的依賴關系解析和執行腳本。所以腳本的加載其實需要與模塊化編程問題結合起來解決。RequireJS不僅記錄了模 塊之間的依賴關系,并且提供了根據依賴關系的按需加載和執行(詳情請參考 RequireJS官方文檔)。
關于腳本加載的更多方案請看這里.
靜態資源文件的部署這里的靜態資源文件是指CSS、Javascript和CSS需要的一些圖片文件。它們的部署需 要考慮兩個問題:
下載速度版本管理靜態資源文件的一個特點變化不頻繁,且與用戶身份無關(即與Cookie無關),因此很適合緩存。另一方面,一旦靜態資源文件變化時,瀏覽器必須從Web服務器下載最新 的版本。當發布新版本的Web應用時,并不是所有用戶馬上就用上新版本,老版本和新版本將會共存,這就涉及到版本匹配問題。老版本的應用需要下載老版本的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對象中,有時候為了方便也存在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節點可能還不存在, 所以沒法直接綁定事件,只能采用委派方式。不過采用委派方式要求事件可以冒泡。
對于那些沒法冒泡的事件(比如<img>的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節;如果之前使用過離線應用緩存現在不想再使用了,從<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.
相關文章: