項目介紹:制作一個網賺應用
平臺:ios手機
技術選型:vue + vuex + vue-router + vue-resource + webpack + es6 + sass + postcss
最終效果圖:更多可到App Store下載“悅動music”
背景
畢業(yè)之后首個用新技術單獨完成的項目,項目是從2015.10月開始的,當時vue還沒有2.0。
產品需求
業(yè)務角度(不展開說硝烂,有興趣私聊)
用戶做任務(下載應用)-->檢測有效性-->給分給用戶
技術角度
- 可快速迭代
- 獲取ios手機上的一些信息
- 用戶體驗友好
技術架構
出于以下的考量:
a. 獲取ios手機上的一些信息视事,只能通過ios客戶端來實現惕稻,這是業(yè)務的重心
b. ios客戶端需要上傳到App Store崎弃,每次迭代都需要至少2天的審核,這樣對可快速迭代不利
c. 因為a的一些實現跟App Store的規(guī)則有打擦邊球的嫌疑狐血,有時候會突然下架
參考競品,以及綜合分析易核,然后我們定出了這樣的架構:
Web App + Native App + 后端
這里稍微說一下
黑線路徑匈织,要經過客戶端,發(fā)揮客戶端的優(yōu)勢耸成,譬如說加密报亩、一些客戶端的功能(譬如說截圖、第三方軟件分享井氢、登錄)
藍色路徑弦追,只要前端跟客戶端拿到token,就可以直接跟后端通訊花竞,免除每次都經過客戶端
前端技術選型分析
客觀角度
問題來了劲件,作為多頁Web App掸哑,需要考慮的地方(更多點擊這里):
i. 狀態(tài)管理
譬如說,多頁面應用下零远,A頁面跳去B頁面苗分,在B頁面提交了數據,返回A的時候牵辣,我想利用B頁面的數據摔癣,是不可以的,因為進行了刷新纬向,又或者說這兩個頁面沒有可以通訊的中介择浊。其實我們可以用localStorage、url參數來作為中介逾条,這兩個頁面之間的通訊還好說琢岩,但是如果是兩個以上的,就很難維護了师脂。
ii. 喪失入口担孔、路徑控制權限主觀角度
2015年10月,vue還沒出1.0吃警,第一次接觸mvvm框架糕篇,也聽說過當時很熱的Angular、React汤徽。當時是抱著越簡單越好的心態(tài)去選娩缰,然后就無意中挑選了vue。關于mvvm的框架對比谒府,可以看看這里拼坎。
而且也只有我一個人負責,所以更大膽地用新框架完疫。途中經歷過入職答辯泰鸡,以及跟網友聊天。才發(fā)現自己在技術選型上是隨意的壳鹤,我問了網友一個關于vue-resource的跨域問題盛龄,網友問了我一個問題:為什么選擇vue-resource,有什么特別之處嗎芳誓?為什么不用普通的ajax余舶?我這才從懵逼中醒悟過來,技術為需求而生锹淌。
單頁面應用架構匿值,有以下特點:
1)在一個頁面下切換視圖,而不需要重新加載整個網頁赂摆,這樣一來就減輕了加載資源的負載挟憔、縮短了用戶的等待時間钟些;
2)路由控制視圖切換
3)組件化開發(fā),利于分治绊谭、復用
4)MV*政恍,免掉繁重的dom操作
5)方便共享數據
Vue是前端MVVM框架,它實現了組件化达传、模板渲染等功能
VueRouter可以控制路由篙耗,從而切換視圖
VueResource封裝了Promise的寫法以及對Restful API更友好
最后不謀而合,我們就選取了Vue全家桶來制作單頁應用
當然單頁應用也有缺點:
1)首次加載比較慢
2)對SEO不友好
3)瀏覽器本身的歷史回退
JUST DO IT √
構建項目
vue-cli:目錄結構
webpack gulp:構建項目趟大,壓縮代碼鹤树,自動化腳本铣焊,打包代碼
npm:包管理開發(fā)flow
git flow
搭建開發(fā)/測試環(huán)境:webpack-dev-server hot-reload webpack.conf
webpack打包大小優(yōu)化:code splitting逊朽、壓縮
webpack本地構建優(yōu)化:把第三方庫放在vendor或者externals等等功能區(qū)分以及開發(fā)
utils
config
mixins
公用組件
view布局
z軸上,采用weui的規(guī)范
區(qū)分公用組件和view進行布局
自適應布局flexible.js + rem + flex布局
-webkit-overflow-scrolling : touch造成的堆疊上下文其他
官網上百度搜索曲伊,zhanzhang.baidu.com
vue的功能:(√ 表示項目中用到的)
- 數據驅動更新視圖 √
- 試圖切換&過渡效果 √
- 路由 √
- 組件之間的通訊※ √
- 狀態(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來作為標示赚哗,前端傳送的http頭部如果帶有token的cookie她紫,后端檢驗token通過,就代表該用戶有效且處于登錄狀態(tài)屿储。但是跨域傳輸cookie需要配置一些東西贿讹。
如果不跨域,前端直接使用document.cookie够掠,發(fā)送請求到后端民褂,會自動帶上cookie;
如果跨域疯潭,默認是不帶cookie的赊堪,如果需要跨域帶上cookie需要做以下步驟:
1)前端設置cookie的domain為后端的域名
document.cookie = "key=value;domain=backend.website.com;path=/"
如果前端域名和后端域名的主域名是相同的,可以直接設置為主域名竖哩,譬如這個項目是前后端分離的哭廉,瀏覽器訪問html文件,是m.hongbaorili.com期丰,這個域名對應的是前端的文件群叶,而后端的接口是ios.hongbaorili.com吃挑,他們有相同的部分hongbaorili.com,直接把cookie的domain設置為這個也可以街立。
document.cookie = "token=xxx;domain=hongbaorili.com;path=/"
2)前端設置xhr.withCredentials=true
3)后端設置http返回頭部
// 設置允許跨域的域名舶衬,注意如果是跨域傳送cookie,是不能設置為*的赎离,必須指定域名
Access-Control-Allow-Origin: http://m.hongbaorili.com
// 設置允許跨域共享cookie
Access-Control-Allow-Credentials: true
2. http的簡單請求和非簡單請求(preflight)
因為使用vue-resource來進行處理請求逛犹,其實它主要就是使用了promise包了一層ajax,當然還有設置了一些勾子讓用戶靈活設置梁剔,譬如我在這里提問的問題虽画。
它還默認對post、get方法設置了一些方法和html頭部荣病,其中的post方法(vue-resource v0.9.3)默認設置了HTTP頭部Content-Type:"application/json;charset=utf-8
码撰。當我嘗試使用vue-resource的方法的時候,會失敗个盆,抓包一看狀態(tài)是403(method not allow)脖岛,它發(fā)送了一個method為OPTION的包,我當時是想著有沒有方法不走OPTION直接走post方法颊亮,就查到了相關的資料:
瀏覽器將CORS的請求分為:簡單請求
和非簡單請求
柴梆。
簡單請求必須同時滿足以下要求,否則為非簡單請求:
非簡單請求:
請求方法是PUT或者DELETE
Content-Type: application/json
凡是非簡單請求终惑,在正式通訊之前绍在,會發(fā)送一個OPTION方法的數據包,作為預檢請求(preflight)
雹有,詢問后端當前請求是否在許可名單(Origin)偿渡、可以使用哪些http方法(Access-Control-Request-Method)、可以帶上哪些頭部信息字段(Access-Control-Request-Headers)件舵。后端通過查詢對應的Access-Control-Allow-Origin卸察、Access-Control-Allow-Methods、Access-Control-Allow-Headers字段铅祸,如果通過就返回一個200坑质,然后就進行數據通訊。如果不通過临梗,返回的數據不包含跨域的信息頭部涡扼,表示失敗,這時候xhr的onerror就會響應盟庞。
所以找到了問題的關鍵是vue-resource默認的post方法使用了Content-Type: application/json
觸發(fā)了preflight吃沪。所以可以直接把默認的選項去掉,然后加上emulateJSON: true來表示application/x-www-form-urlencoded
什猖,然后就能觸發(fā)簡單請求票彪。具體的解決步驟在這里红淡。
以上兩個問題詳見:
http://www.ruanyifeng.com/blog/2016/04/cors.html
3. 組件:無限滾動
關鍵點:
1)判斷滾動到底部,觸發(fā)拉取新數據降铸,添加新數據
判斷滾動是否到底部有用到:滾上去的高度scrollTop + 頁面的高度clientHeight === 網頁的高度scrollEle.offsetHeight
2)零部件:
設置flag變量在旱,防止?jié)L動到底部發(fā)送多個請求;
設置page推掸、size變量表示拉取的頁數桶蝎、數據條數;
3)優(yōu)化點:
首次進入的時候谅畅,未獲取到數據的時候登渣,用變量loading來記錄;
當加載完畢(條目<size || 第一個的size===0)毡泻,用變量nodata來記錄胜茧。
使用-webkit-overflow-scrolling: touch在ios端滑動起來體驗很好
可繼續(xù)優(yōu)化的點:
上拉加載更多,或者下拉加載更多牙捉,有多余的塊顯示
4. 布局竹揍、組件與功能的考慮
首先我們把組件分為公用組件和私有組件,后來看到資料邪铲,發(fā)現私有組件都在view(視圖)里面,所以應該是這樣分類:components(組件)和view(視圖)无拗。
然后以App.vue為根組件带到,公共組件和router view掛載在App.vue下,大概是這樣的:
其中遇到的問題:
1)一些可復用組件英染,譬如說confirm組件揽惹,它的模子就只有提示框的骨架,其中的內容需要用slot來寫四康。它應該放在公共組件的位置搪搏,還是view里面?
當初我沒有細想闪金,就放在公共組件的位置疯溺,執(zhí)行起來的時候,遇到超級不爽的地方:
①每個view都要跟公用組件通訊哎垦,傳遞提示框的自定義信息囱嫩,包括插圖地址、主標題漏设、副標題墨闲、正文、提示郑口;
②每個提示框的“取消事件”還好說鸳碧,都是把confirm組件隱藏掉盾鳞;但是”確定事件”就不是每個confirm組件是一樣的,所以也需要動態(tài)綁定瞻离。
這種方案用以下兩種方法實現組件間的通訊:
a. confirm組件作為全局組件雁仲,狀態(tài)記錄在vuex。這樣對于①的操作就很簡單了琐脏,傳一個json過去就可以攒砖;但問題在于如何動態(tài)綁定“確定事件”,我的做法是在vuex添加一個變量yesCounter日裙,每次confirm組件的“確定”按鈕點擊之后吹艇,yesCounter就+1,在view里面watch這個變量(yesCounter)昂拂,方法寫在view的methods里面受神,當監(jiān)測到改變就觸發(fā)事件。
b. confirm作為公共組件格侯,掛在在App.vue下鼻听,指定組件名稱confirm。在view里面联四,用this.$root.refs.confirm來調用里面的東西撑碴、以及賦值。
c. confirm組件作為view里面的組件朝墩,對于①的操作醉拓,很直觀簡單;對于②的操作用emit事件收苏;而且這個方案的好處在于“按需加載”亿卤,因為有些view是不需要confirm組件的。
于是我采用方案c鹿霸,但是呢排吴,這又有一個問題,就是當confirm組件的出現的時候懦鼠,我希望它把全屏遮住了钻哩,但是它又內嵌到view里面「鹈疲《當時我沒找到方法憋槐,就徘徊地用回方案a,但是的確太惡心了淑趾,就狠下心來把方案c產生的問題解決掉(這種習慣應該拋棄把糇小!)。當時想了三個方案:
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組件設置z-index就可以了。
無意中看到weui的布局乖阵,印證了當初自己的思考也比較合理
5. 抽象view的邏輯 && promise && es6
由于每個view都有以下特點:
①每次加載的時候都會向后端ajax請求數據宣赔;
②通過設置route的data選項,如果①請求數據失敗瞪浸,視圖就切換回去之前的儒将;
③每次按刷新的時候,向后端ajax請求數據默终,更新data椅棺;
稍微分析一下,其實①②③的加載數據是可以復用的齐蔽,但是②中,路由的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
當然也可以靈活處理
.tab__tab-item.active
更多詳見這里~
7. Restful API
增刪查改
post del get put
后端同事說項目小沒必要這么復雜橡庞,就只做了get和post