平时会经常浏览一些网站充电,但是老是需要切换网站也很麻烦,所以就有了做这个小项目的想法。通过爬虫抓取一些网站,然后整合在一个应用中。虽然是个简单应用,但是五脏六腑俱全,适合 Vue 的新手学习。

项目地址

项目技术栈

  • Vue 全家桶
  • 语言:ES6
  • UI:这里使用了Element-ui,毕竟小项目,不花时间在 UI 上
  • 后台:express + superagent + cheerio

后端

使用 express 做接口,在请求接口时,同时使用 superagent 请求对应的地址抓取数据

1
2
3
4
5
6
7
8
app.get('/api/zaoduke', function (req, res, next) {
// 请求接口附带的参数
let page = req.query.page
superagent.get(`https://toutiao.io/subjects/11907?f=new&page=${page}`)
.end(function (err, sres) {
...
});
});

然后通过 cheerio 解析返回的网页源代码 (类似 jqury) 的写法,获得自己需要的数据,然后通过 express 回传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var $ = cheerio.load(sres.text)
var items = []
var data = {
items: items,
hasMore: true
}
if ($('.post').length < 30) {
data.hasMore = false
}
$('.post').each(function (idx, element) {
var $element = $(element)
// cheerio 没有 innterText 的方法,所以通过 nodeType 去取只属于这个元素的文本
var $author = $element.find('.meta').contents().filter(function () {
return this.nodeType === 3;
});
// 这里取数据的方式和jqury是一样的
items.push({
title: $element.find('.title>a').text().trim(),
href: 'https://toutiao.io' + $element.find('.title>a').attr('href'),
author: $author.text().trim()
})
})

res.send(data)

前端

路由相关

在 Vue 中使用插件必须调用 Vue.use(xxx)

路由懒加载,当然小项目不做异步也完全没问题。当项目大了,可以使用这个功能分割不同路由组件,这个可以做到访问才加载路由。

1
const Lists = type => () => import('../page/Lists.js').then(m => m.default(type))

因为主体内容样式是一样的,所以我通过使用不同 type 的方式来复用代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let router = new Router({ 
// 想使用 scrollBehavior 必须用这个 mode
mode: 'history',
// 切换路由时内容滑动到底部
scrollBehavior: () => ({
y: 0
}),
routes: [{
path: '/',
redirect: '/zaoduke'
},
// 以下就是通过不同类型的 type 去复用代码
{
path: '/raywenderlich/:page(\\d+)?',
component: Lists('raywenderlich')
},
{
path: '/csstricks/:page(\\d+)?',
component: Lists('csstricks')
}
...
]
})

项目使用 Vuex 去管理数据,但是当手动刷新浏览器的时候,因为 Vuex 是全局变量,所以变量被销毁,不能获取正确的数据了。有种方式是将 state 存到 localStorage 中,在这里我使用了还有种方法。Vue route 有个全局钩子 beforeEach 他会在进入路由前调用。

1
2
3
4
5
6
7
8
router.beforeEach((to, from, next) => {
// 去获取路由当前的页码,用于请求数据
store.state.page = to.params.page || 1
// 获取 type
store.state.type = to.path.match(/[a-zA-Z0-9]+/)[0]
// 这里必须调用 next,否则进不了路由
next()
})

接下来导出 router 即可,到这里路由部分结束。

1
export default router

Vuex

使用 Vuex 还是要注意下场景,毕竟使用这个插件还是很繁琐的。在这个项目中其实不使用 Vuex 也是可以的,直接使用浏览器内置的方式去管理数据即可。因为代码中 Vuex 用的很少,就一笔略过了

PS: 在使用 Vuex 和 Vue route 要注意必须在 Vue 的实例中传入。

1
2
3
4
5
6
new Vue({
el: '#app',
router,
store,
render: h => h(App)
})

Vue

先看一下复用组件的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 内容组件
import List from './ray.vue'

export default function createListView(type) {
return {
// 通过 type 渲染,这个 type 需要在 List 中的 props 定义
render(h) {
return h(List, {
props: {
type
}
})
}
}
}

List 组件中包含了三个子组件,分别为顶部进度条,分页组件,单个 li 组件。具体的 HTML 和 CSS 代码大家可以自己看一下,没有任何难度。

在页面中请求数据的代码我选择放在 mounted 中,因为只有在这个钩子及以后才可以访问到 $refs 实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
async getData(page = 1) {
// 已经在加载了,就返回
if (this.isLoad) {
return
}
this.isLoad = true
// 调用进度条的 start 方法
this.$refs.progress.start()
// 这种异步请求方法写起来简单
let data = await getRaywenderlichData(this.type, page)
// 给数据赋值
this.list = data.items
// 判断下一页还有没有数据,没有的话禁用下一页按钮
if (!data.hasMore) {
this.noMore = true
} else {
this.noMore = false
}
this.isLoad = false
this.$refs.progress.finish()
}

子组件给父组件通信

1
2
3
4
5
6
7
8
9
10
11
12
13
// 当点击上一页或下一页时调用
routerPush() {
// 改变路由
this.$router.push(`/${this.type}/${this.pageIndex}`)
// 发送消息
this.$emit('changePage', this.pageIndex)
}
// 然后在父组件使用
<pageBreak class="pageBreak"
:type="type"
:isLoad="isLoad"
:noMore="noMore"
@changePage="changePage">

后记

大家有兴趣的可以在这个 issus 中回复觉得不错的网站,我会添加进项目。

如果在项目中发现了有什么不解或者发现了 bug,欢迎提交 PR 或者 issue,欢迎大神们多多指点小弟🙏🙏🙏

项目地址,如果喜欢这个项目,欢迎 Star!