前兩篇簡單討論了requirejs+angular和requirejs+backbone的架構,這兩個架構,估計也是國內最熱門的做法。
淺談HTML5單頁面架構(一)——requirejs + angular + angular-route
淺談HTML5單頁面架構(二)——backbone + requirejs + zepto + underscore
不過,這一篇,我想進一步探討一下這兩個框架的優缺點,另外,再進一步,拋開這兩個框架,回到本真,自己搞個簡單的路由一樣可以實現單頁面。
這個對於剛做前端開發的新同學來說就最好不過了,如果一來到崗位就一大堆angular、backbone、requirejs,看資料都看一兩周。其實大家最熟悉的東西還是那個美元$,用美元能解決的問題,就不要麻煩到angular、backbone大爺了。
事先說明,由於我的業務范圍窄,不一定能把angular和backbone的功能都用一遍,所以以下的分析可能以偏概全,歡迎大家討論。
angular優點:
- 強大的數據雙向綁定
- View界面層組件化
- 內置的強大服務(例如表單校驗)
- 路由簡單
angular缺點:
- 引入的js較大,對移動端來說有點吃不消
- 語法復雜,學習成本高
backbone優點:
- 引入的js較小
- 清晰MVC分層
- Model層事件機制
- 路由簡單而且便於擴展
backbone缺點:
- MVC有點死板,有時候覺得累贅
- 沒有雙向綁定,界面修改只能靠自己
- view切換時,沒有足夠便捷的事件通知(要自己監聽route)
其實,這兩個框架都非常優秀,但是,在實際業務中,不一定百試百靈,因為有一些移動端的單頁面web,業務就很簡單,只是路由分別切換到幾個子模塊,每個子模塊基本都是拉一次數據,展示給用戶,很少用戶交互從而修改數據,改變視圖的功能。
對於這種情況,使用angular未免有點殺雞用牛刀的感覺,而backbone雖然小巧了不少,但是模型的功能也是浪費的。
所以,在這里,我想探討一下,能否拋開這兩個框架,只索取我們基本所需,建立一個更簡單的架構呢?
經驗看來,一些類庫是必不可少的:
- requirejs:模塊划分
- zepto:移動端的jquery
- underscore:便捷的基礎方法,包括模版template、each、map等等
- 路由庫:這里先使用director.js,然而這玩意並沒有backbone和angular的路由好用,文章最后再來探討這個問題
自己做一套最簡單的架構,思想非常簡單:
- 啟動程序
- 監聽路由
- 路由變化,映射到對應的處理邏輯,加載對應的模塊
- 模塊加載完成,修改dom,也就是視圖
- 頁面跳轉時,移除上一個模塊,加載下一個模塊,也就是回到第3點
簡單的思路,讓架構非常簡潔明了,新團隊成員來到能夠輕松上手,而angular和backbone的架構,少說得2、3天才能融入一個已有項目中去。
接下來,我們具體看看怎么做。
第一步,還是index.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Underscore & Director & Requirejs</title> </head> <body> <div id="container"></div> <script data-baseurl="./" data-main="main.js" src="libs/require.js" id="main"></script> </body> </html>
這個跟前兩篇沒什么差別。requirejs引入main.js作為程序入口
第二步,main.js配置requirejs的依賴關系,並啟動webapp
(function (win) { //配置baseUrl var baseUrl = document.getElementById('main').getAttribute('data-baseurl'); /* * 文件依賴 */ var config = { baseUrl: baseUrl, //依賴相對路徑 paths: { //如果某個前綴的依賴不是按照baseUrl拼接這么簡單,就需要在這里指出 director: 'libs/director', zepto: 'libs/zepto.min', underscore: 'libs/underscore', text: 'libs/text' //用於requirejs導入html類型的依賴 }, shim: { //引入沒有使用requirejs模塊寫法的類庫。 underscore: { exports: '_' }, zepto: { exports: '$' }, director: { exports: 'Router' } } }; require.config(config); require(['zepto', 'router', 'underscore'], function($, router, _){ win.appView = $('#container'); //用於各個模塊控制視圖變化 win.$ = $; //暴露必要的全局變量,沒必要拘泥於requirejs的強制模塊化 win._ = _; router.init(); //開始監控url變化 }); })(window);
director.js沒有AMD寫法,還是按照shim的方式引入。另外,由於$和_的使用率太高,所以這里直接公開為全局變量。
除此之外,還加了appView變量,目的是方便各個子模塊修改界面。
第三步,router.js配置路由
這里使用的路由類庫是director(https://github.com/flatiron/director),相對精簡的路由,但其實對於我們這個程序來說,貌似還不夠精簡。先湊合着吧。
director官網給出的示例也相當簡單,就是“路徑”對應“函數”,非常清晰而且實用的方式。
var author = function () { console.log("author"); }; var books = function () { console.log("books"); }; var viewBook = function (bookId) { console.log("viewBook: bookId is populated: " + bookId); }; var routes = { '/author': author, '/books': [books, function() { console.log("An inline route handler."); }], '/books/view/:bookId': viewBook }; var router = Router(routes); router.init();
來看看我們自己的版本:
define(['director', 'underscore'], function (Router, _) { //先設置一個路由信息表,可以由html直出,純字符串配置 var routes = { 'module1': 'module1/controller1.js', 'module2/:name': 'module2/controller2.js' //director內置了普通必選參數的寫法,這種路由,必須用路徑“#module2/kenko”才能匹配,無法缺省 // 'module2/?([^\/]*)/?([^\/]*)': 'module2/controller2.js' //可缺省參數的寫法,其實就是正則表達式,括號內部分會被抽取出來變成參數值。backbone做得比較好,把這個語法簡化了 // “ /?([^\/]*) ” 這樣的一段表示一個可選參數,接受非斜杠/的任意字符 }; var currentController = null; //用於把字符串轉化為一個函數,而這個也是路由的處理核心 var routeHandler = function (config) { return function () { var url = config; var params = arguments; require([url], function (controller) { if(currentController && currentController !== controller){ currentController.onRouteChange && currentController.onRouteChange(); } currentController = controller; controller.apply(null, params); }); } }; for (var key in routes) { routes[key] = routeHandler(routes[key]); } return Router(routes); });
這里把director的路由配置修改了一下,原來只能接受<String, Function>這樣的key value對,但參考之前backbone篇,更好方式應該是讓路由表盡量只有字符串配置,不要寫邏輯(函數)。
所以,上述代碼中,多了一個routeHandler,目的就是建立閉包,把string(配置)轉換為一個閉包函數。
結果,運行效果就是,遇到一個路由,就根據配置加載對應的子模塊代碼。后續實際執行什么,由子模塊自己決定。這樣main/router就能徹底跟子模塊解耦。
第四步,建立一個模塊
tpl.html
<div> Here is module 1. My name: <%=name %><br> <a href="#module2/fromModule1">turn to module 2</a> </div>
controller1.js
define(['text!module1/tpl.html'], function (tpl) { var controller = function () { appView.html(_.template(tpl, {name: 'kenko'})); }; return controller; });
我覺得能實現業務邏輯的前提下,越簡單的架構就越好,便於傳承和維護。
controller就是這個子模塊要做的邏輯,appView是整個視圖根節點,想怎么玩就怎么玩,這對於不熟悉angular、backbone的同學最爽不過了。
這里重點是利用了requirejs做模塊化和依賴加載,並用了underscore的模版庫template。
第五步,再做一個模塊,加上一些銷毀接口
tpl.html
<div> Here is module 2. My name: <%=name %><br> <button>click me!</button> <a href="#module1">turn to module 1</a> </div>
controller2.js
define(['text!module2/tpl.html'], function (tpl) { var controller = function (name) { appView.html(_.template(tpl, {name: name?name:'vivi'})); $('button').on('click', function clickHandler() { alert('hello'); }); controller.onRouteChange = function () { console.log('change'); //可以做一些銷毀工作,例如取消事件綁定 $('button').off('click'); //解除所有click事件監聽 }; }; return controller; });
至此,整個簡單的框架就完成了。
大道至簡,我非常喜歡這樣簡單的架構。希望對新手朋友有所幫助。
最后,關於director的路由,要吐槽一下,這個並沒有backbone那些這么好用,它沒有內置的缺省參數寫法,需要自己理解正則表達式,寫復雜的([?*。參照上邊router.js的代碼。
路由匹配的本質,其實是正則表達式的exec匹配和提取參數。我后續會再整一個簡單好用的路由,參考backbone的模式,猛擊這里:http://www.cnblogs.com/kenkofox/p/4650824.html
本文代碼:https://github.com/kenkozheng/HTML5_research/tree/master/UnderscoreRequireJS