带你用 Vue 全家桶和 Node.js 完成一个聚合应用

5,993 阅读1分钟

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

项目地址

项目技术栈

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

后端

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

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 回传

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)

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

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

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

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 他会在进入路由前调用。

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 即可,到这里路由部分结束。

export default router

Vuex

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

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

new Vue({
  el: '#app',
  router,
  store,
  render: h => h(App)
})

Vue

先看一下复用组件的代码

// 内容组件
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 实例

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()
    }

子组件给父组件通信

// 当点击上一页或下一页时调用
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!