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


背景
畢業(yè)之后首個用新技術(shù)單獨完成的項目,項目是從2015.10月開始的,當(dāng)時vue還沒有2.0。
產(chǎn)品需求
業(yè)務(wù)角度(不展開說,有興趣私聊)
用戶做任務(wù)(下載應(yīng)用)-->檢測有效性-->給分給用戶
技術(shù)角度
- 可快速迭代
- 獲取ios手機上的一些信息
- 用戶體驗友好
技術(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 + 后端

這里稍微說一下
黑線路徑,要經(jīng)過客戶端,發(fā)揮客戶端的優(yōu)勢,譬如說加密、一些客戶端的功能(譬如說截圖、第三方軟件分享、登錄)
藍色路徑,只要前端跟客戶端拿到token,就可以直接跟后端通訊,免除每次都經(jīng)過客戶端
前端技術(shù)選型分析
客觀角度
問題來了,作為多頁Web App,需要考慮的地方(更多點擊這里):
i. 狀態(tài)管理
譬如說,多頁面應(yīng)用下,A頁面跳去B頁面,在B頁面提交了數(shù)據(jù),返回A的時候,我想利用B頁面的數(shù)據(jù),是不可以的,因為進行了刷新,又或者說這兩個頁面沒有可以通訊的中介。其實我們可以用localStorage、url參數(shù)來作為中介,這兩個頁面之間的通訊還好說,但是如果是兩個以上的,就很難維護了。
ii. 喪失入口、路徑控制權(quán)限主觀角度
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

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就可以了。


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

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