一、前言
最近學了react
,一直想做一個專案,沒有什麼好的主意。因為自己也要租房住,就想到了租房App
這個idea,參考豆瓣租房小程式,著手了這樣一個簡陋的前後端專案?。
?線上demo點選這裡
?專案原始碼點選這裡,你可以下載在本地執行,如果對你有幫助可以點一下star
哈?
// 你可以使用npm或yarn
yarn install
執行你的資料庫 // 必須!!!
yarn server // 執行伺服器, 連線的資料庫在server目錄下的config.js裡配置
yarn start // 執行專案
複製程式碼
二、前端
- 專案技術棧
react+react-router+react-redux
,用create-react-app
腳手架生成。UI方面採用Ant Design Mobile
?官方地址。 css
方案是css-in-js,採用style-jsx
,? github地址,可參考掘金上的一篇文章?點選這裡。- 由於是移動端,避免不了
適配
問題,採用vm/vh
適配,具體同樣可以參考掘金的?這篇文章。 - 在結合2、3兩點時,由於要新增配置項,但我不想在專案中
run eject
彈出,於是用了react-app-rewired
改寫配置,這樣就不用彈出命令了。? github地址 - 許可權路由。思路是根據遍歷路由配置表,需要許可權的走許可權路由,不需要的走原來的路由。具體可看專案中的router部分。
- 圖示採用
iconfont SVG
處理
具體用法檢視官方地址。
遇到的問題:
- (未解決)在dev開發環境下修改scss中的css,不會實時編譯更新
- (未解決)IOS下通過
focus
事件不能喚起鍵盤,安卓下可以。為了有個較好的使用者體驗,我在登入,搜尋
頁面開啟時,讓輸入框自動focus
喚起鍵盤,經IOS
真機實踐,只能觸發focus
事件,但是不會喚起鍵盤,安卓
正常。查閱資料後是IOS
做的限制,(IOS
還有和音訊、視訊不能自動播放的限制)。需要使用者主動點選輸入框後,才可以喚起鍵盤。下一次重新開啟就能自動喚起鍵盤了,很坑的一點?!目前無解? - (解決)從首頁列表點選詳情時候,返回到首頁,會重新請求載入,並且滾動位置丟失,使用者體驗十分不好。這裡為了學習
redux
我用redux
(也可用react
的新context api
)解決,因此在路由方面也用了react-redux-router
,但已不維護,改為connect-react-router
github點這裡。思路是首次獲取房源列表,然後存入redux
中,下次開啟的時候,從redux
中獲取。 - (解決)
熱載入
後不能儲存redux
中的狀態。解決方法:在store
中,新增以下程式碼, 詳細看這裡
if (module.hot) {
// Reload reducers
module.hot.accept('./reducers', () => {
store.replaceReducer(connectRouter(history)(rootReducer));
});
}
複製程式碼
- (解決??) 用了
react-loadble
載入專案中的搜尋頁面,會有搜尋input
的placeholder
顯示不全的問題,初次開啟會有問題,第二次開啟沒有問題,如下圖。 在dev
環境下不能重現,生產環境
下會有問題。該元件為Ant Design Mobile
的searchBar
。 上圖我們可以看到是寬度的問題,正常應該為110px
,而錯誤的時候則才80px
。暫時解決方法:移除該路由懶載入,直接載入?
專案優化:
- 路由懶載入,方案: react-loadable,新增
loading
提示 - 圖片懶載入,方案:lazyload
ajax
視情況新增loading
提示,新增CSS3
動畫,使互動更加友好。
三、後端
- 採用
koa2
+koa-router
+mongodb
+jsonWebToken
。最主要的是需要注意非同步和異常處理的問題。 - 資料庫方面用了
Mongoose
來操作。Mongoose
是在node.js非同步環境下對mongodb
進行便捷操作的物件模型工具。更多詳細說明請看官方文件:?點選這裡
3.1 爬取豆瓣小組資料
- 用到的
http
庫是axios
。 - 定時任務庫,
node-schedule
。github:?點選這裡 - 爬蟲庫
cheerio
,它的用法十分簡單。
const cheerio = require('cheerio')
const $ = cheerio.load('<h2 class="title">Hello world</h2>')
複製程式碼
這裡我們就可以通過$(selector)
,像jquery
一樣的方式取到頁面的元素。官方文件:?點選這裡
整個爬取的流程:
- 初始化的時候判斷是否大於最大儲存的資料長度(此專案中設定了資料庫最多儲存
5000
條資料),如果超過,則執行刪除,反之跳過。 - 開啟一個定時任務,每天的
0.00am
開始爬取=>
爬取列表頁面=>
存入資料庫=>
如果失敗,不會爬取該條tid - 的詳情頁,反之繼續爬取詳情頁。
- 爬蟲提取資訊用到了一些正規表示式,提取房租、聯絡方式、房型、所在地區等等。具體程式碼:?點選這裡。其中參考了?這篇文章中的一些正規表示式。
注意:豆瓣會限制一個時段內Ip的訪問次數,因此需要我們做一些調整。
- 列表頁面每一頁、 詳情頁每一條資料的爬取的間隔時間保證是不同的。(定時器+隨機數時間)(貌似沒什麼卵用?)
// sleep
function sleep(time = 0) {
return new Promise(resolve => {
setTimeout(resolve, time);
});
}
// 更新資料庫函式
async updateTopic(tid, resolve, reject) {
// 睡眠
await sleep(Math.ceil(Math.random() * 50 * 1000) + 5000);
// 開始更新
await this.fetchDetail(tid).then(houseInfo => {...});
}
複製程式碼
- 改變請求頭的
user-agent
。專案中是有個user-agent
列表?檢視程式碼,每次請求都帶上隨機中的一個。
3.2 存入資料庫
這裡我是一次性插入多條資料,用到的api
如下
db.Houses.insertMany([your array data])
複製程式碼
3.3 寫介面(路由)
需要注意的是部分路由(需要使用者登入後才可以訪問的介面)header
中需要傳遞token
才能訪問,因此新增路由中介軟體校驗,通過校驗
後才允許訪問。詳細程式碼檢視這裡。關鍵程式碼如下?
const jwt = require('jsonwebtoken');
const token = ctx.header['x-token'];
if(token){
解析token得到使用者資訊
進入下一個中介軟體
}else {
返回錯誤需要傳遞token
}
複製程式碼
四、資料庫mogodb相關
4.1 修改資料庫相關結構
開始設計資料庫的時候,設定價格欄位prices
是陣列,後覺得字串就可以了。於是在原資料庫的基礎上修改資料格式及欄位名prices
=>price
- 批量更新某個欄位
db.getCollection('houses').find().forEach(function(item){
db.getCollection('houses').update({_id:item._id},{$set:{prices: ''+item.prices}})
})
複製程式碼
- 更改欄位名
// 如將欄位"prices"改為"price"
db.getCollection('houses').update({},{$rename:{'prices':'price'}}, false, true)
複製程式碼
4.2 附上一些api.
- 資料庫複製。如複製douban-house資料庫到douban-test
// db.copyDatabase(<from_dbname>, <to_dbname>, <from_hostname>)
db.copyDatabase('douban-house', 'douban-test')
複製程式碼
- 查詢資料庫中陣列長度大/小於n的資料
// 大於 exists=1 小於exists=0
db.getCollection('houses').find({'imgs.n':{'$exists':1}})
複製程式碼
- 查詢資料庫中某個欄位不為null的資料
// $ne=> not equal
db.getCollection('houses').find({'contact':{$ne:null}})
複製程式碼
- 查詢資料庫中多條某個欄位的資料
db.getCollection('houses').find({'tid':{$in:['這裡是陣列','例如id1','2']}})
複製程式碼
另外:插入欄位數字Number Int
型別的資料會儲存為Double
型別,會帶有小數點,例如存的是10
,存進資料庫之後會變成10.0
,可以用NumberInt
或者NumberLong
來儲存
db.houses.insert({"tid": NumberInt(666)})
複製程式碼
4.3 遇到的問題
爬蟲爬取貼子的時候,會爬到相同的貼子,而我們是不需要這些重複的。這裡的問題是在插入重複值的時候,出現錯誤之後不會繼續插入剩下的資料,這是很坑的一點。下面是解決方法:
- 先設定
mongodb
的唯一索引值,在設定的時候也遇到不少的坑,查了很多資料,總結相關的api
const housesSchema = new mongoose.Schema({ tid: String, //我這裡設定的唯一索引是每條貼子的id號 ...省略 }) housesSchema.index({ tid: 1 }, { unique: true }); 複製程式碼
- 這裡設定好之後,當插入重複的tid時,資料庫會返回錯誤,不插入該條資料。特別需要說的大坑是插入的api無論是
insert
還是insertMany
, 他們的api
如下
db.collection.insert(
<document or array of documents>,
{
writeConcern: <document>,
ordered: <boolean>
}
)
複製程式碼
這裡需要注意的是`ordered`這個引數, 這是一個可選引數,官方解釋如下
複製程式碼
Optional. A boolean specifying whether the mongod instance should perform an ordered or unordered insert. Defaults to true.
大意就是指定mongod例項是否應執行有序插入。預設為```true```。
**重點是:**當有序插入的時候,如果出現了錯誤,程式會停下當前的插入,不執行插入剩餘的資料。只有當無序插入,也就是設定了```ordered: false```,當出現錯誤之後,才會把剩下的繼續插入。官方說明如下:
> Excluding Write Concern errors, ordered operations stop after an error, while unordered operations continue to process any remaining write operations in the queue.
官方文件連結:?[點選這裡](http://docs.mongodb.com/manual/reference/method/db.collection.insertMany)
複製程式碼
五、部署相關(跨域處理)
- 開發階段
可在專案中的
package.json
中新增proxy
欄位, 這裡假設http://localhost:3003
就是我們的後臺伺服器,http://localhost:3000
是react開發時候的伺服器 如:在專案中訪問http://localhost:3000/api/house/125048127
就會代理到http://localhost:3003/api/house/125048127
, 就沒有跨域問題了
"proxy": {
"/api": {
"target": "http://localhost:3003"
}
}
複製程式碼
- 線上環境 nginx 配置代理 ?參考這裡
location /api/ {
proxy_pass http://localhost:3003;
}
複製程式碼
六、git相關
有時候提交了錯誤的程式碼又想回退版本,就需要回退遠端git
倉庫的程式碼,再重新提交。 ?更多用法參考這裡
git reflog // 檢視提交列表, 如我需要撤回到第二條提交記錄,也就是紅線下的那條
git reset --soft 3a2a12d // 這裡的引數--soft表示保留本地修改記錄, --hard 代表儲存本地的記錄,如果是--hard 則會清空本地修改記錄,也就是你修改的都沒有了!!切記!!!
git push -f //強制推送到遠端分支
複製程式碼