一、前言
最近学了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 //强制推送到远程分支
复制代码