如何開(kāi)始一個(gè)模塊化可擴(kuò)展的Web App

雖然從沒(méi)有認(rèn)為自己是一個(gè)前端開(kāi)發(fā)者,但不知不覺(jué)中也積累下了一些前端開(kāi)發(fā)的經(jīng)驗(yàn)。正巧之前碰到一道面試題,于是就順便梳理了一下自己關(guān)于Web App的一些思路并整理為本文。

對(duì)于很多簡(jiǎn)單的網(wǎng)站或Web應(yīng)用來(lái)說(shuō),引入jQuery以及一些插件,在當(dāng)前頁(yè)面內(nèi)寫(xiě)入簡(jiǎn)單邏輯已經(jīng)可以滿足大部分需要。但是如果一旦多人開(kāi)發(fā),應(yīng)用的復(fù)雜程度上升,就會(huì)有很多問(wèn)題開(kāi)始暴露出來(lái):

  1. 數(shù)據(jù)源一般都與頁(yè)面分離,那么App啟動(dòng)一般都需要等待數(shù)據(jù)源讀入。
  2. UI交互復(fù)雜時(shí),需要將邏輯通過(guò)面向?qū)ο蟪橄蠛蟛拍芨玫膹?fù)用。
  3. 功能間一般都存在依賴(lài)關(guān)系,需要引入支持依賴(lài)關(guān)系的模塊加載器。

那么如何解決這些問(wèn)題,就以一個(gè)簡(jiǎn)單的訂餐App為例,從零開(kāi)始一個(gè)模塊化可擴(kuò)展Web App。

這個(gè)簡(jiǎn)單的App基于HTML5 Boilerplate、requireJS、jQuery Mobile、Underscore.js,后端邏輯用jStorage模擬實(shí)現(xiàn)。完成后的成品在此。所有代碼可以在github查看。下文將逐一介紹實(shí)現(xiàn)的思路與方法。

從選擇一個(gè)好模板開(kāi)始

開(kāi)始一個(gè)Web項(xiàng)目,HTML的書(shū)寫(xiě)總是重中之重,一個(gè)好的HTML能從根源上規(guī)避大量潛在問(wèn)題,所以Web App應(yīng)該全部應(yīng)用一個(gè)標(biāo)準(zhǔn)化的高質(zhì)量HTML模板,而不是將所有頁(yè)面交由開(kāi)發(fā)人員自由發(fā)揮。

這里推薦使用HTML5 Boilerplate項(xiàng)目作為App的默認(rèn)模板以及文件路徑規(guī)范,無(wú)論是網(wǎng)站或者富UI的App,都可以采用這個(gè)模板作為起步。

可以使用

git clone git://github.com/h5bp/html5-boilerplate.git

或者直接下載HTML5 Boilerplate項(xiàng)目代碼。HTML5 Boilerplate的文件結(jié)構(gòu)如下,

.
├── 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文件,并內(nèi)置了Normalize.css作為默認(rèn)CSS重置手段(其實(shí)Normalize.css不能算是CSS reset)。
  • doc 存放項(xiàng)目文檔
  • img 存放項(xiàng)目圖片
  • js 存放javascript文件,其中第三方類(lèi)庫(kù)推薦放在vendor
  • .htaccess 內(nèi)置了很多對(duì)于靜態(tài)文件在Apache下的優(yōu)化策略,如果Web服務(wù)器不是Apache則可以參考其他Web服務(wù)器配置優(yōu)化。
  • 404.html 默認(rèn)的404頁(yè)面,
  • index.html 項(xiàng)目模板
  • humans.txt 相對(duì)于面向機(jī)器人的robots.txt,humans.txt更像是小幽默,這在里可以寫(xiě)關(guān)于項(xiàng)目/團(tuán)隊(duì)的介紹,或者放置一些彩蛋給那些喜歡對(duì)你的應(yīng)用刨根問(wèn)底的用戶們。
  • robots.txt 用于告訴搜索引擎蜘蛛爬行規(guī)則
  • crossdomain.xml 用于配置Flash的跨域策略
  • favicon.ico apple-touch-icon.png等小圖標(biāo)。

如果是一個(gè)主要面向移動(dòng)設(shè)備,還有更具針對(duì)性的Mobile Boilerplate可供參考。

制定統(tǒng)一的編碼規(guī)范

在正式開(kāi)始編碼之前,無(wú)論是多大規(guī)模的應(yīng)用,多少人的團(tuán)隊(duì),一定要有一個(gè)統(tǒng)一的規(guī)范,才能保證后續(xù)的開(kāi)發(fā)不會(huì)亂套。

前端規(guī)范其實(shí)又要分為三部分:HTML、CSS、Javascript應(yīng)該分別有自己的規(guī)范。HTML/CSS主要約定id/class的命名規(guī)則、屬性的書(shū)寫(xiě)順序。JavaScript可能需要細(xì)化到縮進(jìn)、編碼風(fēng)格、面向?qū)ο髮?xiě)法等等。

最省事的方法當(dāng)然還是參考已有的規(guī)范,比如Google的HTML/CSS風(fēng)格指南Google的Javascript編碼指南等等。

HTML篇

HTML5 Boilerplate的模板核心部分不過(guò)30行,但是每一行都可謂千錘百煉,可以用最小的消耗解決一些前端的頑固問(wèn)題:

使用條件注釋區(qū)分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]-->

之所以要這樣寫(xiě)

  1. 可以使用class作為全局條件區(qū)分低版本的IE瀏覽器并進(jìn)行調(diào)整,這顯然要優(yōu)于使用CSS Hack。
  2. 可以避免IE6條件注釋引起的高版本IE文件阻塞問(wèn)題,原文的解決方法是在前面加一個(gè)空白的條件注釋?zhuān)沁@里顯然將原本無(wú)用空白的條件注釋變得有意義了。
  3. 仍然可以通過(guò)HTML驗(yàn)證。
  4. 與Modernizr等特征檢測(cè)類(lèi)庫(kù)使用相同的class,更具備通用性。

no-js標(biāo)簽是需要與Modernizr等類(lèi)庫(kù)配合使用的,如果你不想在項(xiàng)目中引入Modernizr,需要在Head部分加入一行使no-js標(biāo)簽變?yōu)?code>js,代碼來(lái)自Avoiding the FOUC

 <script>(function(H){H.className=H.className.replace(/\bno-js\b/,'js')})(document.documentElement)</script>

通過(guò)上面的條件注釋?zhuān)涂梢栽贑SS中針對(duì)不同情況分別處理

.lt-ie7 {} /* IE6等版本時(shí) */
.no-js {} /* JavaScript沒(méi)有啟用時(shí) */

meta標(biāo)簽的書(shū)寫(xiě)順序

為了讓瀏覽器識(shí)別正確的編碼,meta charset標(biāo)簽應(yīng)該先于title標(biāo)簽出現(xiàn)。

meta X-UA-Compatible標(biāo)簽可以指定IE8以上版本瀏覽器以最高級(jí)模式渲染文檔,同時(shí)如果已經(jīng)安裝Google Chrome Frame則直接使用Chrome Frame渲染。而指定渲染模式的meta X-UA-Compatible標(biāo)簽同樣需要優(yōu)先出現(xiàn)

<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title></title>

設(shè)置移動(dòng)設(shè)備顯示窗口寬度

<meta name="viewport" content="width=device-width">

這是移動(dòng)設(shè)備專(zhuān)屬的標(biāo)簽,具體設(shè)置需要根據(jù)項(xiàng)目實(shí)際情況調(diào)整。

使用Modernizr做瀏覽器差異檢測(cè)

Modernizr常做前端的應(yīng)該都不陌生。引入Modernizr后,html標(biāo)簽的no-js將會(huì)被自動(dòng)替換為js,同時(shí)Modernizr會(huì)向html標(biāo)簽添加代表版本檢測(cè)結(jié)果的class。

對(duì)于低版本瀏覽器的向上兼容需要根據(jù)項(xiàng)目實(shí)際需求處理,Modernizr也非常周到的給出的絕大多數(shù)HTML5功能的兼容方法。

CSS篇

CSS重置及增強(qiáng)功能

HTML5 Boilerplate選擇Normalize.css重置CSS。如果項(xiàng)目計(jì)劃引入Twitter Bootstrap、YUI 3這些前端框架的話則可以移除,因?yàn)檫@些框架已經(jīng)內(nèi)置了Normalize.css。

同時(shí)HTML5 Boilerplate又引入了一個(gè)main.css,內(nèi)置了一些基本的排版樣式以及打印樣式。

使用LESS或Sass生成CSS

在復(fù)雜應(yīng)用中,如果還手寫(xiě)CSS的話將是一件痛苦的事情,大量的class前綴,復(fù)用樣式需要來(lái)回copy等等。為了更好的擴(kuò)展性,這里建議在項(xiàng)目中引入LESSSass。這代表著:

  • 支持變量與簡(jiǎn)單運(yùn)算
  • 支持CSS片段復(fù)用
  • class/id樣式嵌套

等一些更像是編程語(yǔ)言的特性。這對(duì)于提高開(kāi)發(fā)效率是效果非常明顯的。

以LESS為例,簡(jiǎn)單介紹一下LESS在Windows下如何應(yīng)用到這個(gè)項(xiàng)目中:

  1. 下載Nodejs并安裝,nodejs會(huì)自動(dòng)將自己加入系統(tǒng)路徑。
  2. 在cmd運(yùn)行
    npm install -g less
  3. 然后就可以通過(guò)lessc指令將less源文件編譯為css
    lessc avnpc.less avnpc.css
  4. 如果不使用nodeJs作為后端,最好在寫(xiě)LESS時(shí)采用watch模式,每次保存自動(dòng)編譯為css。這里需要安裝一個(gè)輔助模塊recess:
    npm install -g recess
    然后運(yùn)行watch
    recess avnpc.less:avnpc.css --watch

Javascript篇

使用requireJS按需加載

模塊加載器的概念可能稍微接觸過(guò)前端開(kāi)發(fā)的童鞋都不會(huì)陌生,通過(guò)模塊加載器可以有效的解決這些問(wèn)題:

  1. JS文件的依賴(lài)關(guān)系。
  2. 通過(guò)異步加載優(yōu)化script標(biāo)簽引起的阻塞問(wèn)題
  3. 可以簡(jiǎn)單的以文件為單位將功能模塊化并實(shí)現(xiàn)復(fù)用

主流的JS模塊加載器有requireJS,SeaJS等,加載器之間可能會(huì)因?yàn)樽裱囊?guī)范不同有微妙的差別,從純用戶的角度出發(fā),之所以選requireJS而不是SeaJS主要是因?yàn)椋?/p>

  • 功能實(shí)現(xiàn)上兩者相差無(wú)幾,沒(méi)有明顯的性能差異或重大問(wèn)題。
  • 文檔豐富程度上,requireJS遠(yuǎn)遠(yuǎn)好于SeaJS,就拿最簡(jiǎn)單的加載jQuery和jQuery插件這回事,雖然兩者的實(shí)現(xiàn)方法相差無(wú)幾,但requireJS就有可以直接拿來(lái)用的Demo,SeaJS還要讀文檔自己慢慢折騰。一些問(wèn)題的解決上,requireJS為關(guān)鍵詞也更容易找到答案。

requireJS 加載jQuery + jQuery插件

可能對(duì)于一般Web App來(lái)說(shuō),引入jQuery及相關(guān)插件的概率是最大的,requireJS也親切的給出了相應(yīng)的解決方案及動(dòng)態(tài)加載jQuery及插件的文檔及實(shí)例代碼。

在最新的jQuery1.9.X中,jQuery已經(jīng)在最后直接將自己注冊(cè)為一個(gè)AMD模塊,即是說(shuō)可以直接被requireJS作為模塊加載。如果是加載舊版的jQuery有兩種方法:

1. 讓jQuery先于requireJS加載

2. 對(duì)jQuery代碼稍做一點(diǎn)處理,在jQuery代碼包裹一句:

define(["jquery"], function($) {
    // $ is guaranteed to be jQuery now */
});

requireJS的示例中,直接將requireJS與jQuery合并為一個(gè)文件,如果是采用jQuery作為核心庫(kù)的話推薦這種做法。

同樣對(duì)于jQuery插件來(lái)說(shuō)也有兩種方法

1. 在插件外包裹代碼

define(["jquery"], function($){
     // Put here the plugin code. 
});

2. 在使用reuqireJS代碼加載前注冊(cè)插件(比如在main.js)中

requirejs.config({
    "shim": {
        "jquery-cookie"  : ["jquery"]
    }
});

requireJS加載第三方類(lèi)庫(kù)

在實(shí)例的App中還用到了jQuery以外的第三方類(lèi)庫(kù),如果類(lèi)庫(kù)不是一個(gè)標(biāo)準(zhǔn)的AMD模塊而又不想更改這些類(lèi)庫(kù)的代碼,同樣需要提前進(jìn)行定義:

require.config({
      paths: {
            'underscore': 'vendor/underscore'
      },
      shim: {
          underscore: {
              exports: '_'
          }
      }
});

CSS文件的模塊化處理

在requireJS中,模塊的概念僅限于JS文件,如果需要加載圖片、JSON等非JS文件,requireJS實(shí)現(xiàn)了一系列加載插件。

但是遺憾的是requireJS官方?jīng)]有對(duì)CSS進(jìn)行模塊化處理,而我們?cè)趯?shí)際項(xiàng)目中卻往往能遇到一些場(chǎng)景,比如一個(gè)輪播的圖片展示欄,比如高級(jí)編輯器等等。幾乎所有的富UI組件都會(huì)由JS與CSS兩部分構(gòu)成,而CSS之間也存在著模塊的概念以及依賴(lài)關(guān)系。

為了更好的與requireJS整合,這里采用require-css來(lái)解決CSS的模塊化與依賴(lài)問(wèn)題。

require-css是一個(gè)requireJS插件,下載后將css.jsnormalize.js放于main.js同級(jí)即可默認(rèn)被加載,比如在我們的項(xiàng)目中需要加載jQuery Mobile的css文件,那么可以直接這樣調(diào)用:

require(['jquery', 'css!../css/jquery.mobile-1.3.0.min.css'], function($) {
});

不過(guò)由于這個(gè)CSS本質(zhì)上是屬于jQuery Mobile模塊的一部分,更好的做法是將這個(gè)CSS文件的定義放在jQuery Mobile的依賴(lài)關(guān)系中,最終我們的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: '_'
          }
      }
});

在使用模塊時(shí),只需要:

require(['jquery', 'underscore', 'jquerymobile', 'jstorage'], function($, _) {
});

jQuery Mobile的CSS文件就會(huì)被自動(dòng)加載,這樣CSS與JS就被整合為一個(gè)模塊了。同理其他有復(fù)雜依賴(lài)關(guān)系的模塊也可以做類(lèi)似處理,requireJS會(huì)解決依賴(lài)關(guān)系的邏輯。

數(shù)據(jù)源的加載與等待

Web App一般都會(huì)動(dòng)態(tài)加載后端的數(shù)據(jù),數(shù)據(jù)格式一般可以是JSON、JSONP也可以直接是一個(gè)JS變量。這里以JS變量為例

var restaurants = [
    {
        "name": "KFC"
    },
    {
        "name": "7-11"
    },
    {
        "name": "成都小吃"
    }
]

載入這段數(shù)據(jù):

$.getScript('data/restaurants.json', function(e){
    var data = window.restaurants;
    alert(data[0].name); //KFC
});

單一的數(shù)據(jù)源確實(shí)很簡(jiǎn)單,但是往往一個(gè)應(yīng)用中會(huì)有多個(gè)數(shù)據(jù)源,比如在這個(gè)實(shí)例App中UI就需要載入用戶信息、餐廳信息、訂餐信息三種數(shù)據(jù)后才能工作。如果僅僅靠多層嵌套回調(diào)函數(shù)的話,可能代碼的耦合就非常重了。

為了解決多個(gè)數(shù)據(jù)加載的問(wèn)題,我習(xí)慣的解決方法是構(gòu)造一個(gè)dataReady事件響應(yīng)機(jī)制。

var foodOrder = {

    //數(shù)據(jù)載入后要執(zhí)行的函數(shù)暫存在這里
    dataReadyFunc : []

    //數(shù)據(jù)源URL及載入狀態(tài)
    , 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 }
    ]

    //檢查數(shù)據(jù)源是否全部載入完畢
    , isReady : function(){
        var isReady = true;
        for(var key in this.dataSource){
            if(this.dataSource[key].ready !== true){
                isReady = false;
            }
        }
        return isReady;
    }

    //數(shù)據(jù)源全部加載完畢,則逐一運(yùn)行dataReadyFunc中存放的函數(shù)
    , callReady : function(){
        if(true === this.isReady()){
            for(var key in this.dataReadyFunc){
                this.dataReadyFunc[key]();
            }
        }
    }

    //供外部調(diào)用,會(huì)將外部輸入的函數(shù)暫存在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){
                //每次載入數(shù)據(jù)后,將數(shù)據(jù)存放于dataSource中,將ready狀態(tài)置為true,并調(diào)用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內(nèi)的alert將會(huì)在所有數(shù)據(jù)載入完畢后開(kāi)始執(zhí)行。

這段處理的邏輯并不復(fù)雜,將所有要執(zhí)行的方法通過(guò)dataReady暫存起來(lái),等待數(shù)據(jù)全部加載完畢后再執(zhí)行,更加復(fù)雜的場(chǎng)景此方法仍然通用。

使用JS模板引擎

數(shù)據(jù)載入后,最終都會(huì)以某種形式顯示在頁(yè)面上。簡(jiǎn)單情況,我們可能會(huì)這樣做:

$('body').append('<div>' + data.name + '</div>');

如果頁(yè)面邏輯一旦復(fù)雜,比如需要有if判斷或者多層循環(huán)時(shí),這種連接字符串的方式就相形見(jiàn)絀了,而這也就催生出了JS模板引擎。

主流的JS模板引擎有underscore.js,Jade,EJS等等,可以橫向?qū)Ρ纫幌逻@些JS模板引擎的優(yōu)缺點(diǎn)。

對(duì)于相對(duì)簡(jiǎn)單的頁(yè)面邏輯(只需要支持if和for/each)來(lái)說(shuō),我更傾向選用輕巧的underscore.js或者JavaScript Templates。

在當(dāng)前例子中,使用underscore.js生成列表就非常簡(jiǎn)單了,頁(yè)面模板為:

<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>

調(diào)用引擎

$("#tmpl-restaurants").replaceWith(
    _.template($("#tmpl-restaurants").html(), {
        data : restaurants
    })
);

面向?qū)ο笈c模塊化

通過(guò)上面這些工具的組合,我們有了模塊的概念,有了模板引擎,有數(shù)據(jù)的加載。最終還是要通過(guò)javascript將這一切組織在一起并加入應(yīng)用所需要的邏輯。為了能最大限度的復(fù)用代碼,用面向?qū)ο蟮姆绞饺ソM織內(nèi)容是比較好的選擇。

JavaScript雖然原生并不支持面向?qū)ο螅且廊豢梢酝ㄟ^(guò)很多方式模擬出面向?qū)ο蟮奶匦?。例子中采用了我個(gè)人比較喜歡的一種方式是:

var foodOrder = function(ui, options){
    //構(gòu)造函數(shù)
    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
});

將頁(yè)面的UI元素以及配置項(xiàng)目抽象出來(lái),在實(shí)際構(gòu)造對(duì)象時(shí)則可以通過(guò)入口參數(shù)復(fù)寫(xiě),可以分離整個(gè)項(xiàng)目的邏輯與UI,使處理的方式更加靈活。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容