《紅包日歷》ios版本——基于vue全家桶的webapp單頁應(yīng)用

項目介紹:制作一個網(wǎng)賺應(yīng)用
平臺:ios手機
技術(shù)選型:vue + vuex + vue-router + vue-resource + webpack + es6 + sass + postcss
最終效果圖:更多可到App Store下載“悅動music”


首頁
tipLayer

背景

畢業(yè)之后首個用新技術(shù)單獨完成的項目,項目是從2015.10月開始的,當(dāng)時vue還沒有2.0。

產(chǎn)品需求

業(yè)務(wù)角度(不展開說,有興趣私聊)
用戶做任務(wù)(下載應(yīng)用)-->檢測有效性-->給分給用戶

技術(shù)角度

  1. 可快速迭代
  2. 獲取ios手機上的一些信息
  3. 用戶體驗友好

技術(shù)架構(gòu)

出于以下的考量:
a. 獲取ios手機上的一些信息,只能通過ios客戶端來實現(xiàn),這是業(yè)務(wù)的重心
b. ios客戶端需要上傳到App Store,每次迭代都需要至少2天的審核,這樣對可快速迭代不利
c. 因為a的一些實現(xiàn)跟App Store的規(guī)則有打擦邊球的嫌疑,有時候會突然下架

參考競品,以及綜合分析,然后我們定出了這樣的架構(gòu):
Web App + Native App + 后端


架構(gòu)

這里稍微說一下
黑線路徑,要經(jīng)過客戶端,發(fā)揮客戶端的優(yōu)勢,譬如說加密、一些客戶端的功能(譬如說截圖、第三方軟件分享、登錄)
藍色路徑,只要前端跟客戶端拿到token,就可以直接跟后端通訊,免除每次都經(jīng)過客戶端

前端技術(shù)選型分析

  1. 客觀角度
    問題來了,作為多頁Web App,需要考慮的地方(更多點擊這里):
    i. 狀態(tài)管理
    譬如說,多頁面應(yīng)用下,A頁面跳去B頁面,在B頁面提交了數(shù)據(jù),返回A的時候,我想利用B頁面的數(shù)據(jù),是不可以的,因為進行了刷新,又或者說這兩個頁面沒有可以通訊的中介。其實我們可以用localStorage、url參數(shù)來作為中介,這兩個頁面之間的通訊還好說,但是如果是兩個以上的,就很難維護了。
    ii. 喪失入口、路徑控制權(quán)限

  2. 主觀角度
    2015年10月,vue還沒出1.0,第一次接觸mvvm框架,也聽說過當(dāng)時很熱的Angular、React。當(dāng)時是抱著越簡單越好的心態(tài)去選,然后就無意中挑選了vue。關(guān)于mvvm的框架對比,可以看看這里。
    而且也只有我一個人負(fù)責(zé),所以更大膽地用新框架。途中經(jīng)歷過入職答辯,以及跟網(wǎng)友聊天。才發(fā)現(xiàn)自己在技術(shù)選型上是隨意的,我問了網(wǎng)友一個關(guān)于vue-resource的跨域問題,網(wǎng)友問了我一個問題:為什么選擇vue-resource,有什么特別之處嗎?為什么不用普通的ajax?我這才從懵逼中醒悟過來,技術(shù)為需求而生。

單頁面應(yīng)用架構(gòu),有以下特點:
1)在一個頁面下切換視圖,而不需要重新加載整個網(wǎng)頁,這樣一來就減輕了加載資源的負(fù)載、縮短了用戶的等待時間;
2)路由控制視圖切換
3)組件化開發(fā),利于分治、復(fù)用
4)MV*,免掉繁重的dom操作
5)方便共享數(shù)據(jù)

Vue是前端MVVM框架,它實現(xiàn)了組件化、模板渲染等功能
VueRouter可以控制路由,從而切換視圖
VueResource封裝了Promise的寫法以及對Restful API更友好

最后不謀而合,我們就選取了Vue全家桶來制作單頁應(yīng)用

當(dāng)然單頁應(yīng)用也有缺點:
1)首次加載比較慢
2)對SEO不友好
3)瀏覽器本身的歷史回退

JUST DO IT √

  • 構(gòu)建項目
    vue-cli:目錄結(jié)構(gòu)
    webpack gulp:構(gòu)建項目,壓縮代碼,自動化腳本,打包代碼
    npm:包管理

  • 開發(fā)flow
    git flow
     搭建開發(fā)/測試環(huán)境:webpack-dev-server hot-reload webpack.conf
    webpack打包大小優(yōu)化:code splitting、壓縮
    webpack本地構(gòu)建優(yōu)化:把第三方庫放在vendor或者externals等等

  • 功能區(qū)分以及開發(fā)
    utils
    config
    mixins
    公用組件
    view

  • 布局
    z軸上,采用weui的規(guī)范
    區(qū)分公用組件和view進行布局
    自適應(yīng)布局flexible.js + rem + flex布局
    -webkit-overflow-scrolling : touch造成的堆疊上下文

  • 其他
    官網(wǎng)上百度搜索,zhanzhang.baidu.com


vue的功能:(√ 表示項目中用到的)

  • 數(shù)據(jù)驅(qū)動更新視圖 √
  • 試圖切換&過渡效果 √
  • 路由 √
  • 組件之間的通訊※ √
  • 狀態(tài)管理 √
  • vdom
  • 單元測試
  • 后端渲染

制作的過程中,我覺得組件間通訊比較重要:
vue1.x:
方法①broadcast、dispatch(父子、兄弟組件通訊)vue2.x廢棄該方法
方法②this.$root、this.$children(父子、兄弟組件通訊)
方法③prop、emit(父子組件通訊)vue1.x prop支持雙向綁定
方法④this.$refs(父->子單向通訊)
vue2.x:
方法①event bus(兄弟組件通訊)
方法②prop、emit(父子組件通訊)vue2.x prop不支持雙向綁定


遇到的問題

1. 跨域cookie共享
因為需要知道用于的登錄狀態(tài),所以使用token來作為標(biāo)示,前端傳送的http頭部如果帶有token的cookie,后端檢驗token通過,就代表該用戶有效且處于登錄狀態(tài)。但是跨域傳輸cookie需要配置一些東西。

如果不跨域,前端直接使用document.cookie,發(fā)送請求到后端,會自動帶上cookie;
如果跨域,默認(rèn)是不帶cookie的,如果需要跨域帶上cookie需要做以下步驟:
1)前端設(shè)置cookie的domain為后端的域名

document.cookie = "key=value;domain=backend.website.com;path=/"

如果前端域名和后端域名的主域名是相同的,可以直接設(shè)置為主域名,譬如這個項目是前后端分離的,瀏覽器訪問html文件,是m.hongbaorili.com,這個域名對應(yīng)的是前端的文件,而后端的接口是ios.hongbaorili.com,他們有相同的部分hongbaorili.com,直接把cookie的domain設(shè)置為這個也可以。

document.cookie = "token=xxx;domain=hongbaorili.com;path=/"

2)前端設(shè)置xhr.withCredentials=true
3)后端設(shè)置http返回頭部

// 設(shè)置允許跨域的域名,注意如果是跨域傳送cookie,是不能設(shè)置為*的,必須指定域名
Access-Control-Allow-Origin: http://m.hongbaorili.com
// 設(shè)置允許跨域共享cookie
Access-Control-Allow-Credentials: true

2. http的簡單請求和非簡單請求(preflight)
因為使用vue-resource來進行處理請求,其實它主要就是使用了promise包了一層ajax,當(dāng)然還有設(shè)置了一些勾子讓用戶靈活設(shè)置,譬如我在這里提問的問題。
它還默認(rèn)對post、get方法設(shè)置了一些方法和html頭部,其中的post方法(vue-resource v0.9.3)默認(rèn)設(shè)置了HTTP頭部Content-Type:"application/json;charset=utf-8。當(dāng)我嘗試使用vue-resource的方法的時候,會失敗,抓包一看狀態(tài)是403(method not allow),它發(fā)送了一個method為OPTION的包,我當(dāng)時是想著有沒有方法不走OPTION直接走post方法,就查到了相關(guān)的資料:

瀏覽器將CORS的請求分為:簡單請求非簡單請求。
簡單請求必須同時滿足以下要求,否則為非簡單請求:

來源:阮一峰博客

非簡單請求:
請求方法是PUT或者DELETE
Content-Type: application/json
凡是非簡單請求,在正式通訊之前,會發(fā)送一個OPTION方法的數(shù)據(jù)包,作為預(yù)檢請求(preflight),詢問后端當(dāng)前請求是否在許可名單(Origin)、可以使用哪些http方法(Access-Control-Request-Method)、可以帶上哪些頭部信息字段(Access-Control-Request-Headers)。后端通過查詢對應(yīng)的Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers字段,如果通過就返回一個200,然后就進行數(shù)據(jù)通訊。如果不通過,返回的數(shù)據(jù)不包含跨域的信息頭部,表示失敗,這時候xhr的onerror就會響應(yīng)。

所以找到了問題的關(guān)鍵是vue-resource默認(rèn)的post方法使用了Content-Type: application/json觸發(fā)了preflight。所以可以直接把默認(rèn)的選項去掉,然后加上emulateJSON: true來表示application/x-www-form-urlencoded,然后就能觸發(fā)簡單請求。具體的解決步驟在這里。

以上兩個問題詳見:
http://www.ruanyifeng.com/blog/2016/04/cors.html

3. 組件:無限滾動
關(guān)鍵點:
1)判斷滾動到底部,觸發(fā)拉取新數(shù)據(jù),添加新數(shù)據(jù)
判斷滾動是否到底部有用到:滾上去的高度scrollTop + 頁面的高度clientHeight === 網(wǎng)頁的高度scrollEle.offsetHeight
2)零部件:
設(shè)置flag變量,防止?jié)L動到底部發(fā)送多個請求;
設(shè)置page、size變量表示拉取的頁數(shù)、數(shù)據(jù)條數(shù);
3)優(yōu)化點:
首次進入的時候,未獲取到數(shù)據(jù)的時候,用變量loading來記錄;
當(dāng)加載完畢(條目<size || 第一個的size===0),用變量nodata來記錄。
使用-webkit-overflow-scrolling: touch在ios端滑動起來體驗很好

可繼續(xù)優(yōu)化的點:
上拉加載更多,或者下拉加載更多,有多余的塊顯示

4. 布局、組件與功能的考慮
首先我們把組件分為公用組件和私有組件,后來看到資料,發(fā)現(xiàn)私有組件都在view(視圖)里面,所以應(yīng)該是這樣分類:components(組件)和view(視圖)。

然后以App.vue為根組件,公共組件和router view掛載在App.vue下,大概是這樣的:

我的布局

其中遇到的問題:
1)一些可復(fù)用組件,譬如說confirm組件,它的模子就只有提示框的骨架,其中的內(nèi)容需要用slot來寫。它應(yīng)該放在公共組件的位置,還是view里面?
當(dāng)初我沒有細想,就放在公共組件的位置,執(zhí)行起來的時候,遇到超級不爽的地方:
①每個view都要跟公用組件通訊,傳遞提示框的自定義信息,包括插圖地址、主標(biāo)題、副標(biāo)題、正文、提示;
②每個提示框的“取消事件”還好說,都是把confirm組件隱藏掉;但是”確定事件”就不是每個confirm組件是一樣的,所以也需要動態(tài)綁定。
這種方案用以下兩種方法實現(xiàn)組件間的通訊:
a. confirm組件作為全局組件,狀態(tài)記錄在vuex。這樣對于①的操作就很簡單了,傳一個json過去就可以;但問題在于如何動態(tài)綁定“確定事件”,我的做法是在vuex添加一個變量yesCounter,每次confirm組件的“確定”按鈕點擊之后,yesCounter就+1,在view里面watch這個變量(yesCounter),方法寫在view的methods里面,當(dāng)監(jiān)測到改變就觸發(fā)事件。

b. confirm作為公共組件,掛在在App.vue下,指定組件名稱confirm。在view里面,用this.$root.refs.confirm來調(diào)用里面的東西、以及賦值。

c. confirm組件作為view里面的組件,對于①的操作,很直觀簡單;對于②的操作用emit事件;而且這個方案的好處在于“按需加載”,因為有些view是不需要confirm組件的。

于是我采用方案c,但是呢,這又有一個問題,就是當(dāng)confirm組件的出現(xiàn)的時候,我希望它把全屏遮住了,但是它又內(nèi)嵌到view里面?!懂?dāng)時我沒找到方法,就徘徊地用回方案a,但是的確太惡心了,就狠下心來把方案c產(chǎn)生的問題解決掉(這種習(xí)慣應(yīng)該拋棄?。。?。當(dāng)時想了三個方案:
i) app__header、app__content放在同一個wrapper里面,控制content的高度,超出的范圍滾動條顯示。這種ok


方案 i

ii) app__header、app__content放在同一個wrapper里面,但是是使用flex布局的。這種方案肯定不可以,因為view怎么樣都覆蓋不了header的;

iii) app__header用fixed布局,app__content的高度是100%,header跟content的堆疊上下文是相同的,但是我需要把header置頂,所以直接把header的順序放在下面。然后放在app__content的confirm組件設(shè)置z-index就可以了。

App.vue
confirm

無意中看到weui的布局,印證了當(dāng)初自己的思考也比較合理

weui頁面層級

5. 抽象view的邏輯 && promise && es6
由于每個view都有以下特點:
①每次加載的時候都會向后端ajax請求數(shù)據(jù);
②通過設(shè)置route的data選項,如果①請求數(shù)據(jù)失敗,視圖就切換回去之前的;
③每次按刷新的時候,向后端ajax請求數(shù)據(jù),更新data;

稍微分析一下,其實①②③的加載數(shù)據(jù)是可以復(fù)用的,但是②中,路由的data勾子要傳入transition這個變量,用transition.next()和transition.abort()控制視圖切換,是否但是在①③不需要。綜合以上需求,就寫了一個mixin,如下:

export let routerDataMixin = {
    route: {
        data: function (transition) {
            var that = this;
            if (this.assist.token && this.fetchOption) {
                new Promise(function(resolve, reject) {
                    that.fetchData({resolve, reject})
                })
                .then(function(data){
                    transition.next();
                })
                .catch(function(error){
                    console.log(error);
                    transition.abort();
                })
            } else {
                transition.next();
            }
        }   
        //waitForData: true
    },
    methods: {
        fetchData: function(...rest) {  // 用上es6的rest,很方便
            if (!this.fetchOption) {
                return false;
            }

            this.$http.get(
                this.fetchOption.url,
                {
                    params: this.fetchOption.params || {},
                    credentials: /hongbaorili/g.test(this.fetchOption.url)
                }
            ).then(
            function (response) {
                if (response.data.c === 0) {
                    if ( this.fetchSuccess ) {
                        this.fetchSuccess(response);
                    } else {
                        this.userData = response.data.d
                    }
                    try {
                        rest[0].resolve();
                    } catch(e) {}
                } else if (response.data.c === -10000){
                } else {
                    if ( this.fetchAbnormal ) {
                        this.fetchAbnormal(response);
                    }

                    try {
                        rest[0].reject(new Error("fetchData: c!=0"))
                    } catch(e) {}
                }
                this.endProgress();
            },
            function (response) {
                if ( this.fetchFail ) {
                    this.fetchFail(response);
                } else {
                    this.showToast();
                }

                try {
                    rest[0].reject(new Error("fetchData: fail"));
                } catch(e) {}

                this.endProgress();
            });
        }
}

6. BEM類命名方法
.component-name__component-part_component-status
eg:
.tab__tab-item_active
當(dāng)然也可以靈活處理
.tab__tab-item.active
更多詳見這里

7. Restful API
增刪查改
post del get put
后端同事說項目小沒必要這么復(fù)雜,就只做了get和post

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

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

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