如何開始一個模組化可擴充套件的Web App
雖然從沒有認為自己是一個前端開發者,但不知不覺中也積累下了一些前端開發的經驗。正巧之前碰到一道面試題,於是就順便梳理了一下自己關於Web App的一些思路並整理為本文。
對於很多簡單的網站或Web應用來說,引入jQuery以及一些外掛,在當前頁面內寫入簡單邏輯已經可以滿足大部分需要。但是如果一旦多人開發,應用的複雜程度上升,就會有很多問題開始暴露出來:
- 資料來源一般都與頁面分離,那麼App啟動一般都需要等待資料來源讀入。
- UI互動複雜時,需要將邏輯通過物件導向抽象後才能更好的複用。
- 功能間一般都存在依賴關係,需要引入支援依賴關係的模組載入器。
那麼如何解決這些問題,就以一個簡單的訂餐App為例,從零開始一個模組化可擴充套件Web App。
這個簡單的App基於HTML5 Boilerplate、requireJS、jQuery Mobile、Underscore.js,後端邏輯用jStorage模擬實現。完成後的成品在此。所有程式碼可以在github檢視。下文將逐一介紹實現的思路與方法。
從選擇一個好模板開始
開始一個Web專案,HTML的書寫總是重中之重,一個好的HTML能從根源上規避大量潛在問題,所以Web App應該全部應用一個標準化的高質量HTML模板,而不是將所有頁面交由開發人員自由發揮。
這裡推薦使用HTML5 Boilerplate專案作為App的預設模板以及檔案路徑規範,無論是網站或者富UI的App,都可以採用這個模板作為起步。
可以使用
git clone git://github.com/h5bp/html5-boilerplate.git
或者直接下載HTML5 Boilerplate專案程式碼。HTML5 Boilerplate的檔案結構如下,
.
├── css
│ ├── main.css
│ └── normalize.css
├── doc
├── img
├── js
│ ├── main.js
│ ├── plugins.js
│ └── vendor
│ ├── jquery.min.js
│ └── modernizr.min.js
├── .htaccess
├── 404.html
├── index.html
├── humans.txt
├── robots.txt
├── crossdomain.xml
├── favicon.ico
└── [apple-touch-icons]
從上向下看
css
用於存放css檔案,並內建了Normalize.css作為預設CSS重置手段(其實Normalize.css不能算是CSS reset)。doc
存放專案文件img
存放專案圖片js
存放javascript檔案,其中第三方類庫推薦放在vendor
下.htaccess
內建了很多對於靜態檔案在Apache下的優化策略,如果Web伺服器不是Apache則可以參考其他Web伺服器配置優化。404.html
預設的404頁面,index.html
專案模板humans.txt
相對於面向機器人的robots.txt
,humans.txt更像是小幽默,這在裡可以寫關於專案/團隊的介紹,或者放置一些彩蛋給那些喜歡對你的應用刨根問底的使用者們。robots.txt
用於告訴搜尋引擎蜘蛛爬行規則crossdomain.xml
用於配置Flash的跨域策略favicon.ico
apple-touch-icon.png
等小圖示。
如果是一個主要面向移動裝置,還有更具針對性的Mobile Boilerplate可供參考。
制定統一的編碼規範
在正式開始編碼之前,無論是多大規模的應用,多少人的團隊,一定要有一個統一的規範,才能保證後續的開發不會亂套。
前端規範其實又要分為三部分:HTML、CSS、Javascript應該分別由自己的規範。HTML/CSS主要約定id/class的命名規則、屬性的書寫順序。JavaScript可能需要細化到縮排、編碼風格、物件導向寫法等等。
最省事的方法當然還是參考已有的規範,比如Google的HTML/CSS風格指南、Google的Javascript編碼指南等等。
HTML篇
HTML5 Boilerplate的模板核心部分不過30行,但是每一行都可謂千錘百煉,可以用最小的消耗解決一些前端的頑固問題:
使用條件註釋區分IE瀏覽器
<!DOCTYPE html>
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
之所以要這樣寫
- 可以使用class作為全域性條件區分低版本的IE瀏覽器並進行調整,這顯然要優於使用CSS Hack。
- 可以避免IE6條件註釋引起的高版本IE檔案阻塞問題,原文的解決方法是在前面加一個空白的條件註釋,但是這裡顯然將原本無用空白的條件註釋變得有意義了。
- 仍然可以通過HTML驗證。
- 與Modernizr等特徵檢測類庫使用相同的class,更具備通用性。
no-js
標籤是需要與Modernizr等類庫配合使用的,如果你不想在專案中引入Modernizr,需要在Head部分加入一行使no-js
標籤變為js
,程式碼來自Avoiding the FOUC:
<script>(function(H){H.className=H.className.replace(/\bno-js\b/,'js')})(document.documentElement)</script>
通過上面的條件註釋,就可以在CSS中針對不同情況分別處理
.lt-ie7 {} /* IE6等版本時 */
.no-js {} /* JavaScript沒有啟用時 */
meta標籤的書寫順序
為了讓瀏覽器識別正確的編碼,meta charset標籤應該先於title標籤出現。
meta X-UA-Compatible標籤可以指定IE8以上版本瀏覽器以最高階模式渲染文件,同時如果已經安裝Google Chrome Frame則直接使用Chrome Frame渲染。而指定渲染模式的meta X-UA-Compatible標籤同樣需要優先出現
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title></title>
設定移動裝置顯示視窗寬度
<meta name="viewport" content="width=device-width">
這是移動裝置專屬的標籤,具體設定需要根據專案實際情況調整。
使用Modernizr做瀏覽器差異檢測
Modernizr常做前端的應該都不陌生。引入Modernizr後,html標籤的no-js
將會被自動替換為js
,同時Modernizr會向html標籤新增代表版本檢測結果的class。
對於低版本瀏覽器的向上相容需要根據專案實際需求處理,Modernizr也非常周到的給出的絕大多數HTML5功能的相容方法。
CSS篇
CSS重置及增強功能
HTML5 Boilerplate選擇Normalize.css重置CSS。如果專案計劃引入Twitter Bootstrap、YUI 3這些前端框架的話則可以移除,因為這些框架已經內建了Normalize.css。
同時HTML5 Boilerplate又引入了一個main.css,內建了一些基本的排版樣式以及列印樣式。
使用LESS或Sass生成CSS
在複雜應用中,如果還手寫CSS的話將是一件痛苦的事情,大量的class字首,複用樣式需要來回copy等等。為了更好的擴充套件性,這裡建議在專案中引入LESS或Sass。這代表著:
- 支援變數與簡單運算
- 支援CSS片段複用
- class/id樣式巢狀
等一些更像是程式語言的特性。這對於提高開發效率是效果非常明顯的。
以LESS為例,簡單介紹一下LESS在Windows下如何應用到這個專案中:
- 下載Nodejs並安裝,nodejs會自動將自己加入系統路徑。
- 在cmd執行
npm install -g less
- 然後就可以通過lessc指令將less原始檔編譯為css
lessc avnpc.less avnpc.css
- 如果不使用nodeJs作為後端,最好在寫LESS時採用watch模式,每次儲存自動編譯為css。這裡需要安裝一個輔助模組recess:
npm install -g recess
然後執行watchrecess avnpc.less:avnpc.css --watch
Javascript篇
使用requireJS按需載入
模組載入器的概念可能稍微接觸過前端開發的童鞋都不會陌生,通過模組載入器可以有效的解決這些問題:
- JS檔案的依賴關係。
- 通過非同步載入優化script標籤引起的阻塞問題
- 可以簡單的以檔案為單位將功能模組化並實現複用
主流的JS模組載入器有requireJS,SeaJS等,載入器之間可能會因為遵循的規範不同有微妙的差別,從純使用者的角度出發,之所以選requireJS而不是SeaJS主要是因為:
- 功能實現上兩者相差無幾,沒有明顯的效能差異或重大問題。
- 文件豐富程度上,requireJS遠遠好於SeaJS,就拿最簡單的載入jQuery和jQuery外掛這回事,雖然兩者的實現方法相差無幾,但requireJS就有可以直接拿來用的Demo,SeaJS還要讀文件自己慢慢折騰。一些問題的解決上,requireJS為關鍵詞也更容易找到答案。
requireJS 載入jQuery + jQuery外掛
可能對於一般Web App來說,引入jQuery及相關外掛的概率是最大的,requireJS也親切的給出了相應的解決方案及動態載入jQuery及外掛的文件及例項程式碼。
在最新的jQuery1.9.X中,jQuery已經在最後直接將自己註冊為一個AMD模組,即是說可以直接被requireJS作為模組載入。如果是載入舊版的jQuery有兩種方法:
1. 讓jQuery先於requireJS載入 2. 對jQuery程式碼稍做一點處理,在jQuery程式碼包裹一句:
define(["jquery"], function($) {
// $ is guaranteed to be jQuery now */
});
requireJS的示例中,直接將requireJS與jQuery合併為一個檔案,如果是採用jQuery作為核心庫的話推薦這種做法。
同樣對於jQuery外掛來說也有兩種方法
1. 在外掛外包裹程式碼
define(["jquery"], function($){
// Put here the plugin code.
});
2. 在使用reuqireJS程式碼載入前註冊外掛(比如在main.js)中
requirejs.config({
"shim": {
"jquery-cookie" : ["jquery"]
}
});
requireJS載入第三方類庫
在例項的App中還用到了jQuery以外的第三方類庫,如果類庫不是一個標準的AMD模組而又不想更改這些類庫的程式碼,同樣需要提前進行定義:
require.config({
paths: {
'underscore': 'vendor/underscore'
},
shim: {
underscore: {
exports: '_'
}
}
});
CSS檔案的模組化處理
在requireJS中,模組的概念僅限於JS檔案,如果需要載入圖片、JSON等非JS檔案,requireJS實現了一系列載入外掛。
但是遺憾的是requireJS官方沒有對CSS進行模組化處理,而我們在實際專案中卻往往能遇到一些場景,比如一個輪播的圖片展示欄,比如高階編輯器等等。幾乎所有的富UI元件都會由JS與CSS兩部分構成,而CSS之間也存在著模組的概念以及依賴關係。
為了更好的與requireJS整合,這裡採用require-css來解決CSS的模組化與依賴問題。
require-css是一個requireJS外掛,下載後將css.js
與normalize.js
放於main.js同級即可預設被載入,比如在我們的專案中需要載入jQuery Mobile的css檔案,那麼可以直接這樣呼叫:
require(['jquery', 'css!../css/jquery.mobile-1.3.0.min.css'], function($) {
});
不過由於這個CSS本質上是屬於jQuery Mobile模組的一部分,更好的做法是將這個CSS檔案的定義放在jQuery Mobile的依賴關係中,最終我們的requireJS定義部分為:
require.config({
paths: {
'jquerymobile': 'vendor/jquery.mobile-1.3.0',
'jstorage' : 'vendor/jstorage',
'underscore': 'vendor/underscore'
},
shim: {
jquerymobile : {
deps: [
'css!../css/jquery.mobile-1.3.0.min.css'
]
},
underscore: {
exports: '_'
}
}
});
在使用模組時,只需要:
require(['jquery', 'underscore', 'jquerymobile', 'jstorage'], function($, _) {
});
jQuery Mobile的CSS檔案就會被自動載入,這樣CSS與JS就被整合為一個模組了。同理其他有複雜依賴關係的模組也可以做類似處理,requireJS會解決依賴關係的邏輯。
資料來源的載入與等待
Web App一般都會動態載入後端的資料,資料格式一般可以是JSON、JSONP也可以直接是一個JS變數。這裡以JS變數為例
var restaurants = [
{
"name": "KFC"
},
{
"name": "7-11"
},
{
"name": "成都小吃"
}
]
載入這段資料:
$.getScript('data/restaurants.json', function(e){
var data = window.restaurants;
alert(data[0].name); //KFC
});
單一的資料來源確實很簡單,但是往往一個應用中會有多個資料來源,比如在這個例項App中UI就需要載入使用者資訊、餐廳資訊、訂餐資訊三種資料後才能工作。如果僅僅靠多層巢狀回撥函式的話,可能程式碼的耦合就非常重了。
為了解決多個資料載入的問題,我習慣的解決方法是構造一個dataReady事件響應機制。
var foodOrder = {
//資料載入後要執行的函式暫存在這裡
dataReadyFunc : []
//資料來源URL及載入狀態
, dataSource : [
{ url : 'data/restaurants.json', ready : false, data : null },
{ url : 'data/users.json', ready : false, data : null },
{ url : 'data/foods.json', ready : false, data : null }
]
//檢查資料來源是否全部載入完畢
, isReady : function(){
var isReady = true;
for(var key in this.dataSource){
if(this.dataSource[key].ready !== true){
isReady = false;
}
}
return isReady;
}
//資料來源全部載入完畢,則逐一執行dataReadyFunc中存放的函式
, callReady : function(){
if(true === this.isReady()){
for(var key in this.dataReadyFunc){
this.dataReadyFunc[key]();
}
}
}
//供外部呼叫,會將外部輸入的函式暫存在dataReadyFunc中
, dataReady : function(func){
if (typeof func !== 'function') {
return false;
}
this.dataReadyFunc.push(func);
}
, init : function(){
var self = this;
var _initElement = function(key, url){
$.getScript(url, function(e){
//每次載入資料後,將資料存放於dataSource中,將ready狀態置為true,並呼叫callReady
self.dataSource[key].data = window[key];
self.dataSource[key].ready = true;
self.callReady();
});
}
for(var key in this.dataSource){
_initElement(key, this.dataSource[key].url);
}
}
}
用法為
foodOrder.dataReady(function(){
alert(1);
});
foodOrder.init();
dataReady內的alert將會在所有資料載入完畢後開始執行。
這段處理的邏輯並不複雜,將所有要執行的方法通過dataReady暫存起來,等待資料全部載入完畢後再執行,更加複雜的場景此方法仍然通用。
使用JS模板引擎
資料載入後,最終都會以某種形式顯示在頁面上。簡單情況,我們可能會這樣做:
$('body').append('<div>' + data.name + '</div>');
如果頁面邏輯一旦複雜,比如需要有if判斷或者多層迴圈時,這種連線字串的方式就相形見絀了,而這也就催生出了JS模板引擎。
主流的JS模板引擎有underscore.js,Jade,EJS等等,可以橫向對比一下這些JS模板引擎的優缺點。
對於相對簡單的頁面邏輯(只需要支援if和for/each)來說,我更傾向選用輕巧的underscore.js或者JavaScript Templates。
在當前例子中,使用underscore.js生成列表就非常簡單了,頁面模板為:
<ul data-role="listview" data-inset="true">
<script id="tmpl-restaurants" type="text/template">
<% _.each(data, function(restaurant) { %>
<li>
<a href="#" data-rel="back" data-value="<%- restaurant.name%>"><%- restaurant.name%></a>
</li>
<% }); %>
</script>
</ul>
呼叫引擎
$("#tmpl-restaurants").replaceWith(
_.template($("#tmpl-restaurants").html(), {
data : restaurants
})
);
物件導向與模組化
通過上面這些工具的組合,我們有了模組的概念,有了模板引擎,有資料的載入。最終還是要通過javascript將這一切組織在一起並加入應用所需要的邏輯。為了能最大限度的複用程式碼,用物件導向的方式去組織內容是比較好的選擇。
JavaScript雖然原生並不支援物件導向,但是依然可以通過很多方式模擬出物件導向的特性。例子中採用了我個人比較喜歡的一種方式是:
var foodOrder = function(ui, options){
//建構函式
this.init(ui, options);
}
foodOrder.prototype = {
defaultUI : {
form : '#form-order'
}
, defaultOptions : {
debug : false
}
, init : function(ui, options){
this.ui = $.extend({}, this.defaultUI, ui);
this.options = $.extend({}, this.defaultOptions, options);
}
}
var order = new foodOrder({
form : '#real-form'
}, {
debug : true
});
將頁面的UI元素以及配置專案抽象出來,在實際構造物件時則可以通過入口引數複寫,可以分離整個專案的邏輯與UI,使處理的方式更加靈活。
相關文章
- [譯] 探索 SMACSS:可擴充套件的模組化 CSS 框架MacCSS套件框架
- WPF如何封裝一個可擴充套件的Window封裝套件
- 如何開發一個 Notadd Administration 模組的前端擴充套件前端套件
- 如何開發一個 Notadd 擴充套件套件
- PHP擴充套件開發就是一個自己的PHP擴充套件PHP套件
- iOS一個靈活可擴充套件的開源Log庫iOS套件
- 智聯招聘的Web模組擴充套件落地方案Web套件
- 如何擴充套件Django使用者模組套件Django
- 可擴充套件性套件
- dubbo是如何實現可擴充套件的?套件
- 可擴充套件性筆記一套件筆記
- 推薦一個Dapper擴充套件CRUD基本操作的開源庫APP套件
- 從零開始做一個SLG遊戲(六):UI系統擴充套件遊戲UI套件
- 如何構建一個優雅擴充套件套件
- PHP擴充套件開發教程2 – 編寫第一個擴充套件 hello worldPHP套件
- 如何擴充套件 Create React App 的 Webpack 配置套件ReactAPPWeb
- 一個可擴充套件的報警系統Quick-Alarm套件UI
- dubbo是如何實現可擴充套件的?(二)套件
- 新增php的memcached擴充套件模組PHP套件
- 如何編寫一個獨立的 PHP 擴充套件PHP套件
- INFORMIX表的預設初始擴充套件、下一個擴充套件資料塊以及一個表允許的最大擴充套件數。ORM套件
- [外掛擴充套件]先佔樓做一個快遞的模組套件
- 易操作、可觀測、可擴充套件,EMQX如何簡化物聯網應用開發套件MQ
- 可擴充套件Web架構與分散式系統套件Web架構分散式
- 構建可擴充套件的應用(一) (轉)套件
- 可擴充套件的搜尋元件套件元件
- 可擴充套件、模組化CSS--主題樣式規則(翻譯文)套件CSS
- 可擴充套件、模組化CSS--佈局樣式規則(翻譯文)套件CSS
- 我的第一個Emacs擴充套件Mac套件
- 從零開始建立一個 PHP 擴充套件PHP套件
- 如何開發Chrome擴充套件程式Chrome套件
- 如何基於 PHP-X 快速開發一個 PHP 擴充套件PHP套件
- 如何擴充套件大規模Web網站的效能?套件Web網站
- Laravel-permission(一個許可權管理的擴充套件包) 的使用Laravel套件
- 編寫可擴充套件程式套件
- ZenML:可擴充套件的開源機器學習MLOps框架套件機器學習框架
- Chrome第一個擴充套件程式Chrome套件
- 一個開發中的 Laravel 關聯模型擴充套件Laravel模型套件