知乎日报项目开发
知乎日报是由知乎开发的一款资讯类阅读 APP, 每日提供来自知乎社区精选的回答或者专栏文章, 本项目将使用 Vue 和 webpack 等相关技术, 利用知乎日报的接口开发一个 Web App.
分析与准备
webpack 的基础配置参考 GitHub
日报是一个单页的应用, 由 3 部分组成, 如图所示:
左侧是惨淡, 分为 “每日推荐” 和 “主题日报” 两个类型, 中间是文章列表, 右侧是文章正文和评论. 其中每日推荐按日期排列, 比如途中显示为 5 月 2 日的推荐文章, 中间栏滚动至底部时. 自动加载前一天的推荐内容.
主题日报有 “日常心理学” 等 10 多个子分类, 分类列表默认是收起的, 点击 “主题日报” 菜单时切换展开和收起的状态. 点击某个子分类后, 中间栏切换为该项目下的文章列表, 不再按时间排列. 点击文章列表中的某一项, 在右侧渲染对应文章的内容和评论.
知乎日报的接口地址前缀为 http://news-at.zhihu.com/api/4/, 图片地址前缀为 https://pic1.zhimg.com, 由于两者都开启了跨域限制, 无法在前端直接调用, 因此要开发一个代理.
我们使用基于 Node.js 的 request 库来做代理, 通过 NPM 安装 request:
npm install request –save-dev
在 daily 目录下新建一个 proxy.js 的文件, 写入以下内容:
1 | // daily/proxy.js |
监听了两个端口: 8010 和 8011. 8010 用于接口代理, 8011 用于图片代理. 比如请求的真实接口为 http://news-at.zhihu.com/api/4/news/3892357, 开发时改写为 http://127.0.0.1:8080/news/3892357; 图片的真实地址为 https://pic4.zhimg.com/v2-b44636ccd2affac97ccc0759a0f64f7f.jpg, 开发时改写为 http://127.0.0.1:8011/img/https://pic4.zhimg.com/v2-b44636ccd2affac97ccc0759a0f64f7f.jpg.
代理的核心是在返回的头部 (response header) 中添加一项 Access-Control-Allow-Origin 为 “*”, 也就是允许所有的域访问.
最后在终端使用 Node 启动代理服务:
node proxy.js
如果成功, 就会在终端显示两行日志:
接口代理运行在 http://127.0.0.1:8010/ 和 图片代理运行在 http://127.0.0.1:8011/
对于接口的 Ajax 请求, 前端有多种实现方案; Vue 官方提供了 vue-resource 插件, 但是不在维护了, 而是推荐使用 axios.
axios 是基于 Promise 的 HTTP 库, 同时支持前端和 Node.js. 首先使用 npm 安装 axios:
npm install axios –save
在 daily 目录下新建目录 libs, 并在 libs 下新建 util.js 文件, 项目中使用的工具函数可以在这里封装. 比如对 axios 封装, 写入请求地址的前缀, 在业务中只用写相对路径, 这样可以灵活控制. 另外, 可以全局拦截 axios 返回的内容, 简单处理, 只需返回我们需要的数据. 其代码如下:
1 | // libs/util.js |
做好这些准备后, 就可以开始日报应用的开发了.
推荐列表与分类
搭建基本结构
项目中使用的 css 样式不多, 所以直接写在 daily/style.css, 并在 main.js 中导入:
1 | // main.js |
日报是单页应用, 没有路由, 只有一个入口组件 app.vue. 应用结构如图所示.
这里需要插入一张图片
应用分左、中、右 3 栏, 3 栏都可以滚动. 对左栏和中栏使用 fixed 固定, 并使用 overflow: auto 滚动, 而右栏高度自适应, 使用浏览器默认的 body 区域滚动即可. 基本的 HTML 和 CSS 结构如下.
1 | // app.vue |
主题日报
“主题日报” 下有子类列表, 默认是收起的, 点击主题日报可以切换展开和收起的状态, 使用数据 showThemes 来控制, 并用 themes 来循环渲染子类目:
1 | // app.vue |
themeId 会在点击子类时设置, 稍后会介绍.
应用初始化时, 获取主题日报的分类列表:
1 | // app.vue |
主题日报类目列表为数组, 每一项的结构示例如下:
1 | "others": [ |
点击子类目时, 将菜单 type 切换为 “主题日报” 高亮点击的子类, 然后加载该类目下的文章列表:
1 | // app.vue |
文章列表 list 为数组, 每一项的结构示例如下:
1 | "stories": [ |
文章列表中的 id 字段是文章的 id, 请求文章内容和评论列表时会用到, title 为标题, images 为封面图片, 没有 images 字段就不显示封面图片.
每日推荐
应用初始化和点击 “每日推荐” 菜单时请求推荐的文章列表. 推荐列表的 API 相对地址为 news/before/20170503, before 后面是查询的日期, 这个日期比要查询的真实日期多一天. 每日推荐可以无限次地向前一天查询, 为方便操作日期, 在 libs/util.js 内定义两个时间长发:
1 | // libs/util.js |
Util.prevDay 的参数为前一天的时间戳, 计算前一天的时间戳只需以今天 0 点的时间戳为基础, 也就是通过 Util.getTodayTime 获取的时间戳减去 86400000. 这种方法要比直接判断前一天的日期简单得多, 因为每个月的日期是不固定的, 另外还需要特殊处理闰年.
推荐文章的列表获取相关代码如下:
1 | // app.vue |
recommendList 为推荐文章列表的数据, 在初始化和每次点击 “每日推荐” 菜单时都会请求数据. dailyTime 默认获取今天 0 点的时间戳, 请求时需要多加一天. 因为推荐列表可能通过 “主题日报” 的子类切换而来, 需要重新获取一遍数据, 所以 handleToRecommend 方法每次都需要清空列表并重新设置 dailyTime.
推荐列表的数据结构和主题日报基本一致, 不同的是多了一个 date 字段来表示请求列表的日期.
两个文章列表 (list、recommendList)的每一项都用一个组件 item.vue 来展示, 在 daily/components 目录下新建 item.vue 文件, 内容如下:
1 | // components/item.vue |
prop: data 里可能没有 images 字段, 所以列表会显示两种模式, 即含封面图和不含封面图.
Item 组件会用到文章列表里, type 为 recommend 和 daily 两种类型下, 渲染会稍有不同. recommend 会显示每天的日期, daily 则没有. 对应的代码如下:
1 | // app.vue |
自动加载更多推荐类表
在 “每日推荐” 类型下, 中栏的文章列表滚动到底部会自动加载前一天的推荐列表, 所以要监听中栏 (.daily-list) 的滚动事件, 并在合适的时机触发加载请求:
1 | // app.vue |
$list (.daily-list) 的 CSS 使用了 overflow: auto, 所以它具备滚动的能力, 进而可以监听滚动事件. 直接操作 DOM 在 Vue 中很少见, 但示例的场景和一些对 window、document 对象监听事件的场景还是有的, 使用监听时要注意在 beforeDestroy 生命周期使用 removeEventListener 移除. ¥list 的 scroll 是标准 DOM 事件, 所以也可以用 Vue 的 v-on 指令. 比如上例也可以改写为:
1 | <template> |
文章详情页
加载内容
右侧的文章内容区域封装成了一个组件. 在 components 目录下新建 daily-article.vue 组件, 它接收唯一的一个 prop:id, 也就是文章的 id, 如果 id 变化了, 就说明切换了文章, 需要请求新的文章内容.
早 app.vue 中导入 daily-article.vue 组件, 并在文章类表的 Item 组件上绑定查看文章事件:
1 | // app.vue |
Item 是组件, 绑定原生事件时要带指定事件修饰符 .native, 否则会认为监听的是来自 Item 组件的自定义事件 click.
dailyArticle 组件在监听到 id 改变时请求文章内容:
1 | // components/daily-artivle.vue |
数据的 data 结构如下:
1 | { |
这里只用了 title 和 body, 其中 body 的格式为 html, 需要用 v-html 指令直接显示. 用户 可能会在某篇文章阅读到一定位置时切换了别的文章, 这时文章的滚动条仍停留在上次浏览的位置, 使用 window.scollTo(0,0) 可以返回页面的顶端. 需要注意的是, .daily-article.vue 并没有使用 overflow: auto 滚动, 而是自然高度, 所以这里是让页面返回顶端, 而不能设置 .daily-article 的 scrollTop 为 0.
加载评论
每篇文章底部要加载评论, 评论的数据结构为:
每条评论要显示发表时间, 源数据格式为时间戳, 需要前端转为相对时间. 在 daily 目录下创建 directives 目录, 并创建 time.js 文件, 内容如下:
1 | // directives/time.js |
评论列表在获取完文章内容后在获取.
1 | // components/daily-article.vue |
至此, 知乎日报的所有核心代码分析完毕
总结
日报项目以单页面的形式呈现, 基本覆盖了 Vue 和 webpack 的核心功能, 他们包括:
- Vue 的单文件组件用法
- Vue 的基本指令、自定义指令.
- 数据的获取、整理、可视化.
- prop、事件、子组件索引
- ES6 模块
日报项目是一个较独立的单页小应用, 没有使用路由和大规模状态管理插件 Vuex, 在工程上并不算复杂, 比较适合刚入手 Vue 的联系项目. 虽然看似简单, 但它覆盖了业务中很多场景, 对代码进行了组织和模块化, 很接近真实的生产项目. 项目对代码维护和扩展性也有考虑, 比如对 Ajax 的封装, 通用工具函数的提取、组件的解耦等, 这些细节都是在实际项目中要考虑的.