使用 Vue 构建 RSS 阅读器:第一部分

Avatar of Raymond Camden
Raymond Camden

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 200 美元的免费额度!

在探索、学习,最重要的是, Vue.js 的过程中,我一直在构建不同类型的应用程序,以此来练习并提高我的使用技巧。几周前,我读到关于 关闭 Digg 的 RSS 阅读器,虽然存在很棒的替代方案,但我认为用 Vue 构建自己的阅读器会很有趣。在这篇文章中,我将解释我是如何将其组合在一起的,以及它有什么问题。我知道在开始构建时需要做出一些妥协,所以计划是在后续文章中使用更漂亮的版本来跟进此版本。

文章系列

  1. 设置和第一次迭代 (本文)
  2. 改进和最终版本

让我们首先看看这个应用程序并解释各个组件。

查看应用程序

打开应用程序时,会显示一些基本说明和添加新 RSS 提要的提示。

单击按钮将打开一个模态窗口,允许您输入提要

添加按钮后,将显示该提要的博客条目

注意颜色。我将其设置成每个提要都有一个独特的颜色,以便更容易区分不同网站的内容。例如,以下是添加更多提要后的外观。

左侧的面板允许您通过单击提要进行筛选。不幸的是,您还无法删除提要,因此,如果您需要删除某些内容,则需要打开 DevTools 并编辑缓存的值。

让我们回顾一下技术栈!

组件

首先是 Vue 库本身。我*没有*为此应用程序使用 webpack——只是一个简单的脚本包含,没有构建过程。

UI 是全部 Vuetify,一个非常棒的材质设计框架,易于使用。我还在学习它,所以可以肯定地说我的设计可以更好,尽管我现在对它的外观非常满意。

持久性通过localStorage完成。我存储从 RSS 提要中检索到的提要元数据。这通常包括网站名称、主要 URL 和描述等信息。我没有存储提要项目,这意味着每次加载站点时,我都会重新获取项目。下一个版本将使用 IndexedDB 在本地保留项目。

那么,我如何加载提要信息呢?我可以直接向 URL 发出网络请求,但大多数 RSS 提要都没有使用 CORS,这意味着浏览器将被阻止加载它。为了解决这个问题,我使用 Webtask编写了一个快速无服务器函数。它既处理创建对 CORS 友好的端点,也处理将提要的 XML 解析成友好的 JSON。

现在我已经介绍了应用程序的各个部分,让我们开始查看代码吧!

布局

让我们从布局开始。正如我所说,我正在使用 Vuetify 作为 UI。我一开始使用的是 黑暗示例布局。这就是创建用于菜单的标题、页脚和左侧列的方式。

应用程序模板

我使用了 卡片组件 用于单个提要项目。我对这里的布局不太满意。例如,我还没有呈现发布日期,因为我很难找到一种好的呈现方式。我决定简单地跳过它,等到下一个版本,我们将在**本系列的第 2 部分**中看到它。

与其一次性将所有源代码都放到您面前,不如让我们看看各个部分。首先,这是在任何提要添加之前显示的介绍/帮助文本

<div v-if="showIntro">
  <p>
    Welcome to the RSS Reader, a simple way to manage RSS feeds and read content.  To begin using the RSS Reader, add your first feed by clicking the button below.
  </p>
  <p>
    <v-btn color="primary" large @click="addFeed">
      <v-icon>add</v-icon>
      Add Feed
    </v-btn>
  </p>
</div>

当您拥有提要时,项目将显示为卡片列表

<v-container fluid grid-list-lg>
  <v-layout row wrap>
    <v-flex xs12 v-for="item in items">
      <v-card :color="item.feedColor">
        <v-card-title primary-title>
          <div class="headline">{{item.title}}</div>
        </v-card-title>
        <v-card-text>
          {{item.content | maxText }}
        </v-card-text>
        <v-card-actions>
        <v-btn flat target="_new" :href="item.link">Read on {{item.feedTitle}}</v-btn>
        </v-card-actions>
      </v-card>
    </v-flex>
  </v-layout>
</v-container>

请注意,用于阅读提要项目的按钮使用target在新标签页中打开它。

为了显示提要,我使用了一个列表组件

<v-list dense>
  <v-list-tile @click="allFeeds">
    <v-list-tile-action>
      <v-icon>dashboard</v-icon>
    </v-list-tile-action>
    <v-list-tile-content>
      <v-list-tile-title>All Feeds</v-list-tile-title>
    </v-list-tile-content>
  </v-list-tile>
  <v-list-tile @click="filterFeed(feed)" v-for="feed in feeds" :value="feed == selectedFeed">
    <v-list-tile-action>
     <v-icon :color="feed.color">bookmark</v-icon>
    </v-list-tile-action>
    <v-list-tile-content>
      <v-list-tile-title>{{ feed.title }} </v-list-tile-title>
    </v-list-tile-content>
  </v-list-tile>
  <v-list-tile @click="addFeed">
    <v-list-tile-action>
      <v-icon>add</v-icon>
    </v-list-tile-action>
    <v-list-tile-content>
      <v-list-tile-title>Add Feed</v-list-tile-title>
    </v-list-tile-content>
  </v-list-tile>
</v-list>

最后,这是模态布局

<v-dialog v-model="addFeedDialog" max-width="500px">
  <v-card>
    <v-card-title>Add Feed</v-card-title>
    <v-card-text>
      Add the RSS URL for a feed below, or the URL for the site and I'll try to 
      auto-discover the RSS feed.
      <v-text-field v-model="addURL" label="URL" :error="urlError"
      :rules="urlRules"></v-text-field>
    </v-card-text>
    <v-card-actions>
      <v-btn color="primary" @click.stop="addFeedAction">Add</v-btn>
      <v-btn color="primary" flat @click.stop="addFeedDialog=false">Close</v-btn>
    </v-card-actions>
  </v-card>
</v-dialog>

逻辑

现在到了有趣的部分——JavaScript!和之前一样,我不会一次性把整个文件都放到您面前。相反,让我们一点一点地解决它。

在启动时,我加载可能已定义的任何现有提要,然后根据需要显示介绍文本

created() {
  this.restoreFeeds();
  if (this.feeds.length === 0) this.showIntro = true;
},

restoreFeeds方法处理检查 LocalStorage 中是否存在现有提要。

restoreFeeds() {
  let feeds = localStorage.getItem('feeds');
  if (feeds) {
    this.feeds = JSON.parse(feeds);
    this.feeds.forEach((feed,idx) => {
      feed.color = colors[idx % (colors.length-1)];
      this.loadFeed(feed);
    });
  }
},

请注意,此方法处理分配颜色(这是一个简单的数组),然后加载提要数据。

说到这一点,我如何处理加载 RSS 信息?目前有两种情况会发生这种情况。第一种是当您最初添加提要时,第二种是当您重新加载应用程序并且提要已定义时。在这两种情况下,我都调用一个 URL——使用 Webtask 定义的无服务器任务。此任务将返回所有内容——有关提要的元数据和项目本身。我仅在第一次调用时关心元数据,理论上,我可以通过在服务器端删除元数据并将其剔除来使代码更快一些,但这似乎不值得付出努力。

该无服务器函数非常简单

'use strict';

const Parser = require('rss-parser');
const parser = new Parser();

module.exports = function(context, cb) {
  let url = '';
  if(context.body && context.body.url) url = context.body.url;
  if(context.query && context.query.url) url = context.query.url;
  if(url === '') cb(new Error('URL parameter not passed.'));
  console.log('gonna parse '+url);
  
  parser.parseURL(url)
  .then(feed => {
    console.log(feed);
    cb(null, {feed:feed});
  })
  .catch(e => {
    cb(e);
  });
        
}

我在这里所做的只是包装 npm 包 rss-parser,它为我处理所有转换工作。您在开头看到的if语句处理查找url参数。当调用我的 webtask 时,我可以传递查询字符串变量或将其作为 HTTP 主体的一部分发送。无论哪种方式,我都只是使用rss-parser模块并返回结果。

此函数的端点为

https://wt-c2bde7d7dfc8623f121b0eb5a7102930-0.sandbox.auth0-extend.com/getRss

欢迎您自己尝试。您可以在处理添加提要的方法中看到这一点

addFeedAction() {
  this.urlError = false;
  this.urlRules = [];
  //first, see if new
  if(this.feeds.findIndex((feed) => {
    return (feed.rsslink === this.addURL);
  }) >= 0) {
    this.urlError = true;
    this.urlRules = ["URL already exists."];
    return;
  } else {
    fetch(rssAPI+encodeURIComponent(this.addURL))
    .then(res => res.json())
    .then(res => {
      // ok for now, assume no error, cuz awesome
      this.addURL = '';

      //assign a color first
      res.feed.color = colors[this.feeds.length % (colors.length-1)];

      // ok, add the items (but we append the url as a fk so we can filter later)
      res.feed.items.forEach(item => {
        item.feedPk = this.addURL;
        item.feedColor = res.feed.color;
        this.allItems.push(item);
      });

      // delete items
      delete res.feed.items;

      // add the original rss link
      res.feed.rsslink = this.addURL;

      this.feeds.push(res.feed);
      this.addFeedDialog = false;

      //always hide intro
      this.showIntro = false;

      //persist the feed, but not the items
      this.storeFeeds();
    });
  }

},

此方法首先检查提要是否已存在,如果不存在,则会访问无服务器端点以获取详细信息。当存储项目时,我有一些数据重复。我不想将项目存储在提要对象“下”,而是使用全局 Vue 数据值allItems。因此,我将提要标识符和颜色复制到每个项目中。这样做的目的是为了便于以后进行项目显示和筛选。这对我来说感觉“不对”,但同样,这是我的第一个草稿。我正在为项目使用计算属性,您可以在此处看到逻辑

items:function() {
  if(this.allItems.length === 0) return [];
  // filter
  let items = [];
  if(this.selectedFeed) {
    console.log('filtered');
    items = this.allItems.filter(item => {
            return item.feedPk == this.selectedFeed.rsslink;
    });
  } else {
    items = this.allItems;
  }
  items = items.sort((a, b) => {
    return new Date(b.isoDate) - new Date(a.isoDate);
  });

  return items;
}

现在看来,我可以从每个提要中收集我的项目,而不是存储一个全局数组,尽管如果需要,我以后可以解决这个问题。我喜欢 Vue 为我提供了解决此类问题的选择。

下一步是什么?

当我开始撰写这篇文章时,我明确地认为*这* *是*第一个草稿。我已经在这里和那里指出了我喜欢和不喜欢的东西,但对于下一个版本,我到底计划做什么?

  • 我想将所有数据访问移动到 Vuex 中。Vuex 被描述为 Vue 的“状态管理模式 + 库”。如果您对此不太理解,请不要担心。一开始我也不知道这意味着什么。对我来说,Vuex 提供了一种以封装方式处理更复杂数据的方法。随着您开始构建更多需要共享数据的组件,这一点变得更加重要。
  • 说到组件,我应该考虑将“项目”制作成一个合适的 Vue 组件。这是一个简单的胜利。
  • 我想开始将提要项目存储在 IndexedDB 中,以便您在打开应用程序时立即获得内容。这将使应用程序的性能大大提高,并提供基本的离线支持。当然,如果您离线,则无法阅读完整的条目,但至少可以提供*某些*内容。
  • ……以及您提出的任何建议!查看代码,随时提出建议并指出错误!

敬请期待第二篇文章!