这篇文章是为谁写的?
*这篇文章是为那些已经知道如何用Gatsby.js+Netlify建立网站的基本知识的人而写的。
我们要怎么做
由于这是一个关于设计模式和原则的网站,我将向你展示一种使用非常花哨的生成器模式来构建这样的东西的方法,这种方法不觉得花哨,但感觉很强大。
高水平的解释
在我的网站上,我把我所有的博客文章组织成Articles 。
每个Article 有一个Category ,可以有很多Tags 。
我想做的是查看我所有其他文章的类别和标签,并计算出与当前文章的相似度分数,以确定哪些文章与它最相似。
从Graphql查询中获取文章
我已经把一个SimilarArticles.js 文件放在一起,该文件暴露了一个同名的查询(1.)。
这个查询会返回我网站上的每一篇文章,从文章(或者像我在模板-url中包含的那样,blog-post )的markdown文件中提取我需要的所有属性来渲染一篇文章,包括Category 和Tags 。
// SimilarArticles.js
import React from 'react'
import PropTypes from 'prop-types'
import { StaticQuery, graphql } from "gatsby"
import { getPostsFromQuery } from '../../../../utils/blog'
import ArticleCard from './ArticleCard'
import { SimilarArticlesFactory } from './SimilarArticlesFactory'
import "../styles/SimilarArticles.sass"
const SimilarArticlesComponent = ({ articles }) => (
<section className="similar-articles">
{articles.map((article, i) => (
<ArticleCard {...article.article} key={i}/>
))}
</section>
)
// (1.) Query for articles
export default (props) => (
<StaticQuery
query={graphql`
query SimilarArticles {
posts: allMarkdownRemark(
sort: { order: DESC, fields: [frontmatter___date] }
filter: {
frontmatter: {
templateKey: { eq: "blog-post" }
published: { eq: true }
}
}
limit: 1000
) {
edges {
node {
fields {
slug
readingTime {
text
}
}
frontmatter {
title
date
description
tags
category
image
}
}
}
}
}
`}
render={data => {
const { category, tags, currentArticleSlug } = props;
// (2.) Marshall the response into articles
const articles = getPostsFromQuery(data.posts);
// (3.) Use a SimilarArticlesFactory to get my similar articles
const similarArticles = new SimilarArticlesFactory(
articles, currentArticleSlug
)
.setMaxArticles(4)
.setCategory(category)
.setTags(tags)
.getArticles()
// (4.) Render it
return (
<SimilarArticlesComponent
articles={similarArticles}
/>
)
}}
/>
)
在我得到所有的查询之后,我把它们全部(2.)汇集成实际的文章。
Graphql的回应有一点嵌套。因为我经常查询文章,所以我写了一个实用的函数来从查询中剥离文章。
用SimilarArticleFactory对文章进行排名
在(3.),事情变得有趣了。
除了这篇文章的currentArticleSlug ,我还传入了所有的articles 。
从返回的内容中,我能够在调用setMaxArticles(num: number) 、setCategories(category: string) 、setTags(tags: string[]) ,然后再调用getArticles() 。
这就是所谓的 "构建者模式"。
它的工作原理是在调用setter后返回this 。
这是一种创建模式,它允许你以一种更加声明的方式创建对象。
让我们看一下SimilarArticlesFactory.js 。
// SimilarArticlesFactory.js
import { includes, orderBy } from 'lodash'
export class SimilarArticlesFactory {
// (1.) Create by passing in articles, currentSlug
constructor (articles, currentArticleSlug) {
// (2.) Don't include the current article in articles list
this.articles = articles.filter(
(aArticle) => aArticle.slug !== currentArticleSlug);
this.currentArticleSlug = currentArticleSlug;
// (3.) Set default values
this.maxArticles = 3;
this.category = null;
this.tags = []
}
// (4.) Builder pattern usage
setMaxArticles (m) {
this.maxArticles = m;
return this;
}
setCategory (aCategory) {
this.category = aCategory;
return this;
}
setTags (tagsArray) {
this.tags = tagsArray;
return this;
}
getArticles () {
const { category, tags, articles, maxArticles } = this;
// (5.) We use an Identity Map to keep track of score
const identityMap = {};
if (!!tags === false || tags.length === 0) {
console.error('SimilarArticlesFactory: Tags not provided, use setTags().')
return [];
}
if (!!category === false) {
console.error('SimilarArticlesFactory: Category not provided, use setCategory().')
return [];
}
function getSlug (article) {
return article.slug;
}
function addToMap (article) {
const slug = getSlug(article);
if (!identityMap.hasOwnProperty(slug)) {
identityMap[slug] = {
article: article,
points: 0
}
}
}
// (7.) For category matches, we add 2 points
function addCategoryPoints (article, category) {
const categoryPoints = 2;
const slug = getSlug(article);
if (article.category === category) {
identityMap[slug].points += categoryPoints;
}
}
// (8.) For tags matches, we add 1 point
function addTagsPoints (article, tags) {
const tagPoint = 1;
const slug = getSlug(article);
article.tags.forEach((aTag) => {
if (includes(tags, aTag)) {
identityMap[slug].points += tagPoint;
}
})
}
function getIdentityMapAsArray () {
return Object.keys(identityMap).map((slug) => identityMap[slug]);
}
// (6.) Map over all articles, add to map and add points
for (let article of articles) {
addToMap(article);
addCategoryPoints(article, category);
addTagsPoints(article, tags)
}
// (9.) Convert the identity map to an array
const arrayIdentityMap = getIdentityMapAsArray();
// (10.) Use a lodash utility function to sort them
// by points, from greatest to least
const similarArticles = orderBy(
arrayIdentityMap, ['points'], ['desc']
)
// (11. Take the max number articles requested)
return similarArticles.splice(0, maxArticles);
}
}
这个类中最有趣的部分是设置器,它返回this ,这允许我们先前看到的那种漂亮的方法链,以及getArticles() 方法。
我们提到,我们想根据相似度对文章进行评分,对吗?
那么,这是我们可以做到的一个方法。
我把所有的文章(6.)映射出来,并把它们添加到一个身份映射(这是一个哈希表或JavaScript对象的花哨术语)。
身份映射最终看起来有点像这样。一个以slugs为键的对象。
我们识别两篇文章的方法是通过它们的slug 。
当我在做这个的时候,我也会看一下每篇文章的类别(7.)。
如果当前的文章与我们正在循环浏览的这篇文章有共同的类别,那么我们就给它2分。
我们对标签也做同样的事情(8.),但我们给这篇文章的每个标签加1分,这个标签也在当前文章中。
最后,我们把整个东西变成一个数组(9.),然后把所有的文章(10.使用lodash)从最多到最少的分数进行排序,然后把要求的maxArticles ,也就是4分。