開發無框架單頁面應用 — 老碼農的祖傳祕方

老碼農發表於2015-01-21

什麼是單頁面應用(SPA)?

維基百科上的描述是這樣的:

也就是說,單頁面應用是僅包含單個網頁的應用,目的是為了提供類似於本地應用的流暢使用者體驗。

需不需要框架?

要實現單頁面應用,現在已經有很多現成的框架了,比如AngularJSEmber.jsBackbone.js等等。它們都是很全面的開發平臺,為單頁面應用開發提供了必需的頁面模板、路徑解析和處理、後臺服務api訪問、DOM操作等功能。

事實上,現代的web應用開發基本都離不開一個甚至多個框架,開發無框架應用的想法聽起來蠻不靠譜的,對吧?

但是我總覺得現在是時候拋棄框架了。前兩年我都在用AngularJS做開發,可以說已經比較熟悉它了,我的第一個單頁面應用就是在AngularJS的啟發下做出來的。框架曾經是我的摯愛。

但是現在每次看著它們那龐大臃腫的身軀和晦澀的語法,我都會想到諸葛亮的那句名言:“好累,感覺不會再愛了”。還有不同框架下各種工具、外掛難以混用的現狀,讓我不得不經常需要自己寫原生程式碼解決很多問題。時間長了,我自然冒出一個想法:“為啥不乾脆拋棄框架,直接寫原生程式碼呢?畢竟,框架也是原生程式碼寫出來的嘛。”

怎麼實現無框架SPA?

在微博裡表達了這個想法之後,有不少朋友提出了各種意見和建議,我非常感謝。其中還有個小朋友評論道:“我看到了一個從大型機到web的大叔,在摳效能[偷笑]這是職業病嘛。”。看到這條評論,我含笑不語。

這種職業病在我們從90年代過來的老碼農裡還是比較普遍的,當年記憶體64K,磁碟360K,必須精打細算才能過日子。1個byte要掰成2個4位用,連結串列要自己實現,每一K記憶體裡放了啥都門清。後來工作了,在ES/9000上做開發,系統資源也是非常金貴的。

記得有一次我們單位因為某個資料庫應用系統吃記憶體太厲害,找IBM加了128K記憶體,一下子就花了60多萬人民幣,60多萬哪!當時我的心在滴血:“把錢給我一半,我幫你們優化一下,省下這些記憶體行不?”。後來有機會瞻仰了一下那個系統的程式碼,我滴個媽呀,無數的join操作,當時罵孃的心都有了,但程式碼是我們部門一位元老寫的,我一個新來的菜鳥惹不起…

總之,那時寫程式碼是藝術,現在有的同學動不動就把一堆東西全load到記憶體裡,反正記憶體不夠了就加,這不是敗家子麼!哼!(老碼農倚老賣老,不能算新聞)

好了,一不小心扯遠了,還是說單頁面應用的事情。

總之,無框架單頁面應用看似可行,但難度有多大?我還是心裡沒底,需要一點理論依據給自己壯膽。所以我就在網上到處尋摸了一番,偶然找到了這篇 Google 工程師 Joe Gregorio 寫的文章《別再用JS框架了》,裡面的分析有一種與我心有慼慼的感覺,看完還給它翻譯成中文了。

不過,他提出的方法是更超前的,例如 imports 和 Polymer,我曾經試過,印象中只有 Google 的 Chrome Canary 才有支援,而且要先在選項中開啟一些試驗功能,瀏覽器會變得不那麼穩定。而 X-Tag 和 Bosonic 也要依賴於一個小的庫。而我想做的是現在的瀏覽器就已經能支援的功能,用原生程式碼來實現。所以他這篇文章只能讓我堅定方向,但是具體的做法還得靠自己去發現。

後來又看了幾篇比較偏學術的文章,例如這篇 Mixu 寫的《Single page apps in depth》,對我也不太適用。他的模板都需要先編譯為JS物件存放,和 AngularJS 的方法類似,但我覺得在一個小規模應用裡應該有更加優雅的實現方法。

找了好幾天文件,我突然意識到自己浪費了不少時間。所謂理論依據應該是高層次的,解決可行性的問題,剩下的就是自己去想辦法實現了。可行性不是明擺著的嘛,那麼多框架不也是用原生程式碼實現的麼?

想到這兒,我就開始自己嘗試了。前後一共只花了兩三天時間,寫出來一共一百多行JS,就基本解決了問題。其實把程式碼寫完了回顧一下,這些方法都算不上什麼創新,都是標準的東西而已。肯定有別人也這麼做了,只是我不知道而已吧。

可能有讀者看到這兒不耐煩了:“Talk is cheap. Show me the code.”好吧,下面就是程式碼的描述。

老碼農的實現方法

基礎物件

首先是定義預設的兩個頁面片段(預設頁面和出錯頁面,這兩個頁面是基礎功能,所以放在庫裡)相關程式碼,對每個片段對應的url(例如home)定義一個同名的物件,裡面存放了對應的 html 片段檔案路徑、初始化方法。

隨後是全域性變數,包含了 html 片段程式碼的快取、區域性重新整理所在 div 的 DOM 物件和向後端服務請求返回的根資料(rootScope,初始化時未出現,在後面的方法中才會用到):

主程式

下面就是主程式了,所有的公用方法打包放到一個物件miniSPA中,這樣可以避免汙染名稱空間:

然後是 changeUrl 方法,對應在index.html中有如下觸發定義:

onhashchange是在location.hash發生改變的時候觸發的事件,能夠通過它獲取區域性 url 的改變。在index.html中定義瞭如下的連結:

每個 url 都以#號開頭,這樣就能被onhashchange事件抓取到。最後的 div 就是區域性重新整理的 html 片段嵌入的位置。

上面的程式碼先獲取改變後的 url,先通過window[url]找到對應的物件(類似於最上部定義的homenotfound),如物件不存在(無定義的路徑)則轉到404處理,否則通過ajaxRequest方法獲取window[url].partial中定義的 html 片段並載入到區域性重新整理的div,並執行window[url].init初始化方法。

ajaxRequest方法主要是和後端的服務進行互動,通過XMLHttpRequest傳送請求(GETPOST),如果獲取的是 html 片段就把它快取到settings.partialCache[url]裡,因為 html 片段是相對固定的,每次請求返回的內容不會變化。如果是其他請求(比如向 Github 的 markdown 服務 POST 一個字串)就不能快取了。

對於不支援XMLHttpRequest的瀏覽器(主要是 IE 老版本),本來是可以在 else 里加上xmlhttp = new ActiveXObject(‘Microsoft.XMLHTTP’);的,不過,我手頭也沒有那麼多老版本 IE 用於測試,而且老版本 IE 本來就是我深惡痛絕的東西,憑什麼要支援它啊?所以就乾脆直接給個alert完事。

render方法一般在每個片段的初始化方法中呼叫,它會設定全域性變數中的根物件,並通過refresh方法渲染 html 片段。

獲取後端資料後,如何渲染 html 片段是個比較複雜的問題。這就是 DOM 操作了。總體思想就是從 html 片段的根部入手,遍歷 DOM 樹,逐個替換屬性和文字中的佔位變數(例如<img src="emojis.value"><p>{{emojis.key}}</p>),匹配和替換是在feedData方法中完成的。

這裡最麻煩的是data-repeat屬性,這是為了批量渲染格式相同的一組元素用的。比如從 Github 獲取了全套的 emoji 表情,共計 888 個(也許下次升級到1000個),就需要渲染 888 個元素,把 888 個圖片及其說明放到 html 片段中去。而 html 片段中對此只有一條定義:

等 888 個 emoji 表情來了之後,就要自動把<li>元素擴充套件到 888 個。這就需要先clone定義好的元素,然後根據後臺返回的資料逐個替換元素中的佔位變數。

從上面的程式碼可以看到,refresh方法是一個遞迴執行的函式,每次處理當前 node 之後,還會遞迴處理所有的孩子節點。通過這種方式,就能把模板中定義的所有元素的佔位變數都替換為真實資料。

feedData用來替換文字節點中的佔位變數。它通過正規表示式獲取{{...}}中的內容,並把多級屬性(例如data.map.value)切分開,逐級迴圈處理,直到最底層獲得相應的資料。

initFunc方法的作用是解析片段對應的初始化方法,判斷其型別是否為函式,並執行它。這個方法是在changeUrl方法裡呼叫的,每次訪問路徑的變化都會觸發相應的初始化方法。

最後是miniSPA庫自身的初始化。很簡單,就是先獲取404.html片段並快取到settings.partialCache.notfound中,以便在路徑變化時使用。當路徑不合法時,就會從快取中取出404片段並顯示在區域性重新整理的 div 中。

好了,核心的程式碼就是這麼多。整個 js 檔案才區區 155 行,比起那些動輒幾萬行的框架是不是簡單得不能再簡單了?

有了上面的miniSPA.js程式碼以及配套的404.htmlhome.html,並把它們打包放在lib目錄下,下面就可以來看我的應用裡有啥內容。

應用程式碼

說到應用那就更簡單了,app.js一共30行,實現了一個GET和一個POST訪問。

首先是getEmoji物件,定義了一個 html 片段檔案路徑和一個初始化方法。初始化方法中分別呼叫了miniSPA中的ajaxRequest方法(用於獲取 Github API 提供的 emoji 表情資料, JSON格式)和render方法(用來渲染對應的 html 片段)。

然後是postMD物件,它除了 html 片段檔案路徑和初始化方法(因為初始化不需要獲取外部資料,所以只需要呼叫render方法就可以了)之外,重點在於submit方法。submit會把使用者提交的輸入文字和其他兩個選項打包 POST 給 Github 的 markdown API,並獲取後臺解析標記返回的 html。

這兩個物件對應的 html 片段如下:

getEmoji.html :

postMD.html :

演示地址

以上程式碼的線上演示可以在我的 Github 專案頁面看到。

以上演示程式碼已經在Chrome,Firefox,SafariOpera較新版本上測試過。IE 9以上版本估計也可以,不過沒測過。

另外,這些程式碼還有不少值得優化的地方,不過時間有限,主要是為了達到演示目的,所以暫時就不去改它了。

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

任選一種支付方式

開發無框架單頁面應用 — 老碼農的祖傳祕方 開發無框架單頁面應用 — 老碼農的祖傳祕方

相關文章