在本文中,我们将:
使用 Next.js 引导一个 React.js Typescript 项目。
设置 Apollo GraphQL 客户端并将其集成到我们的项目中。
设置 GraphQL Codegen 以生成我们可以在整个应用程序中使用的类型、类型安全查询和自定义挂钩。
创建一个索引页面,其中包含我们所有帖子的列表。
为我们所有的帖子生成单独的静态博客页面。
我们不会:
进入 Next.js 的基础知识。
做很多造型。
当新帖子添加到您的 Hashnode 时,设置一个 webhook 来重建我们的网站。
你会需要:
包含博客文章的 Hashnode 帐户
能够运行引导的 Next.js 项目的开发环境。
引导项目和安装依赖项。
第 1 步 - 初始化一个新的 Next.js 项目
我们首先使用 TypeScript 和默认设置引导一个新的 Next.js 项目。
yarn create next-app
之后,我们可以安装必要的依赖项来实现所有的魔法。
第 2 步 - 初始化并安装 Apollo 客户端
yarn add @apollo/client graphql
之后,我创建了一个名为lib根目录的文件夹,并在其中创建了一个名为apollo.ts. 该文件将包含创建可在服务器端使用的新客户端的逻辑,因为我们不需要任何客户端获取数据。所有页面都将是静态的或服务器端呈现的。
如果您不知道它们之间的区别,我建议您阅读数据获取部分中的 Next.js 文档 👉数据获取:概述👈
lib/apollo.ts
import { ApolloClient, InMemoryCache } from"@apollo/client";exportconst createClient = () => {
const client = new ApolloClient({
uri: "https://api.hashnode.com/",
cache: new InMemoryCache(),
});return client;
};
这是他们的文档中描述的 Apollo Client 的默认初始化。在这里。唯一的区别是我将客户端指向 Hashnode API 端点,https://api.hashnode.com因为这是我们要获取帖子的地方。
第 3 步 - 安装 GraphQL CodeGen
多亏了 GraphQL 和 TypeScript 的魔力,我们可以在我们的应用程序和服务器/API 之间生成端到端类型安全的数据获取。这是一次性设置,将在未来加速开发,消除类型错误,并验证我们的模式。
运行以下命令来安装实现此目的所需的软件包:
yarn add -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-graphql-request @graphql-codegen/typescript-operations
这将安装以下软件包 devDepencencies
@graphql-codegen/cli
@graphql-codegen/typescript
@graphql-codegen/typescript-graphql-request
@graphql-codegen/typescript-operations
我建议在npmjs-website上查看它们☕
第 4 步 - 配置 GraphQL Codegen
在项目的根目录中创建一个名为 的文件graphql.codegen.yml,该文件将保存我们代码生成的配置。
schema:-"https://api.hashnode.com"documents:-"./graphql/**/*.graphql"generates:./generated/graphql.ts:plugins:-typescript-typescript-operations
配置文件执行以下操作:
schema这指向我们用于获取 API 模式映射的 GraphQL 端点。
documents这告诉 GraphQL Codegen 在哪里寻找我们的模式文件
generates这告诉 GraphQL Codegen 在哪里创建和存储我们生成的代码。
plugins指定它应使用的 GraphQL Codegen 插件。
我们需要做的是将代码生成脚本添加到我们的package.json
在里面scripts添加这一行:
{
"scripts": {
..."generate": "graphql-codegen --config ./graphql.config.yml",
...
}
}
此时如果我们尝试运行生成脚本,我们将得到以下输出:
yarn generate
yarn run v1.22.15
$ graphql-codegen --config graphql.codegen.yml
(node:42491) ExperimentalWarning: stream/web is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
✔ Parse Configuration
⚠ Generate outputs
❯ Generate to ./generated/graphql.ts
✔ Load GraphQL schemas
✖
Unable to find any GraphQL type definitions for the following pointers:
- ./graphql/**/*.graphql
◼ Generate
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
即使我们在这里抛出错误,也意味着我们已经成功安装了代码生成器。错误是因为我们还没有在文件夹中定义任何 graphql 模式graphql。
第 5 步 - 添加我们的第一个 GraphQL Schema
如果您前往,https://api.hashnode.com您可以使用他们的 API playground 来编写您需要的模式代码。它有文档,您可以在其中浏览可用字段等。
您还可以测试架构并查看结果。这就是它在浏览器中的样子。
我正在抓取我在 playground 中创作的模式,并将其添加到一个名为位于项目根目录中的get-blog-posts.query.graphql文件夹内的文件中。graphql
如果您现在运行生成脚本,您将看到以下输出:
yarn generate
yarn run v1.22.15
$ graphql-codegen --config graphql.codegen.yml
(node:42632) ExperimentalWarning: stream/web is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
✔ Parse Configuration
✔ Generate outputs
✨ Done in 3.30s.
此外,您会看到在您的根目录中弹出一个新文件夹,其中包含generated一个名为graphql.ts. 这个漂亮的文件现在包含了我们继续构建应用程序所需的所有类型注释。
例如,我们可以查看Post如下所示的 -type:
exporttype Post = {
__typename?: 'Post';
_id: Scalars['ID'];
author?: Maybe<User>;
bookmarkedIn?: Maybe<Array<Maybe<Scalars['String']>>>;
brief?: Maybe<Scalars['String']>;
contentMarkdown?: Maybe<Scalars['String']>;
contributors?: Maybe<Array<Maybe<Contributor>>>;
coverImage: Scalars['String'];
cuid?: Maybe<Scalars['String']>;
dateAdded?: Maybe<Scalars['String']>;
dateFeatured?: Maybe<Scalars['String']>;
dateUpdated?: Maybe<Scalars['String']>;
followersCount?: Maybe<Scalars['Int']>;
isActive?: Maybe<Scalars['Boolean']>;
isAnonymous?: Maybe<Scalars['Boolean']>;
numUniqueUsersWhoReacted?: Maybe<Scalars['Int']>;
partOfPublication?: Maybe<Scalars['Boolean']>;
poll?: Maybe<Poll>;
popularity?: Maybe<Scalars['Float']>;
reactions?: Maybe<Array<Maybe<Reaction>>>;
reactionsByCurrentUser?: Maybe<Array<Maybe<Reaction>>>;
replyCount?: Maybe<Scalars['Int']>;
responseCount?: Maybe<Scalars['Int']>;
slug?: Maybe<Scalars['String']>;
tags?: Maybe<Array<Maybe<Tag>>>;
title?: Maybe<Scalars['String']>;
totalReactions?: Maybe<Scalars['Int']>;
type: Scalars['String'];
};
每次我们运行代码生成脚本时,该文件都会更新。
我们现在准备开始构建包含所有帖子列表的索引页面。
使用 getStaticProps 获取博客文章
由于我们正在构建一个静态块,因此我们将用于getStaticProps通过道具将帖子传递到我们的 React 组件中。经验丰富的 Next.js 用户将确切了解未来的工作流程。
我现在正在清除 Next.js 在目录index.tsx中的文件中为我们生成的默认代码pages。此外,我在底部通过导出一个async名为getStaticProps.
完成后它看起来像这样。
pages/index.tsx
import Head from"next/head";
import styles from"@/styles/Home.module.css";
import { createClient } from"@/lib/apollo";
import {
GetBlogPostsDocument,
GetBlogPostsQuery,
GetBlogPostsQueryVariables,
Post,
} from"@/generated/graphql";
import { NextPage } from"next";
import Link from"next/link";type Props = {
posts: Post[];
};const HomePage: NextPage<Props> = ({ posts }) => (
<>
<Head>
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
<div style={{ padding: "2rem" }}>
<h1>My Hashnode Blog</h1>
<p>Powered by React, Typescript and, GraphQL</p>
</div>
<div>
{posts.map((post) => (
<div key={post._id} style={{ padding: "2rem" }}>
<p>{post.title}</p>
<Link
target="_blank"
href={`/${post.slug}`}
style={{ textDecoration: "underline", color: "pink" }}
>
Read more 🚀
</Link>
</div>
))}
</div>
</main>
</>
);exportdefault HomePage;exportconst getStaticProps = async () => {
const client = createClient();const { data } = await client.query<
GetBlogPostsQuery,
GetBlogPostsQueryVariables
>({ query: GetBlogPostsDocument });const posts = data.user?.publication?.posts ?? [];return {
props: {
posts,
},
};
};
看!端到端类型安全,我们没有手动输入任何类型 🤩 在更大的项目中,这将节省大量时间来解决错误。
这在我的浏览器中看起来像这样:
现在我们有了我们的帖子列表,我们需要开始生成我们的个人页面,这是通过使用 Next.js 中非常常见的技术通过使用getStaticProps+ getStaticPaths+ 动态路由模式来完成的。
准备从 Hashnode API 获取单个帖子。
首先,让我们再次前往 Hashnode API playground 并验证我们获取单个帖子所需的模式。
这是我从他们的操场上抓取的模式:
之后,我在我们的graphql文件夹中创建了一个名为的新文件get-blog-post.query.graphql,我正在修改它以便我可以将变量传递给它。
query GetBlogPost($slug: String!, $hostname: String!) {
post(slug: $slug, hostname: $hostname) {
title
coverImage
slug
contentMarkdown
dateAdded
}
}
其中slug是你在Hashnode网站上发帖的slug,hostname是你在Hashnode上的主地址。找到它的一种简单方法是打开顶部导航栏中的配置文件上下文菜单。
在我的例子中,主机名是carlw.hashnode.dev. 我们正在以火箭速度取得进步🚀
不要忘记通过运行以下命令来验证您的模式并生成类型:
yarn generate
为每篇博文生成单独的静态页面
pages首先,在目录中创建一个文件,[slug].tsx这就是我们告诉 Next.js 内容应根据url
在此处阅读官方文档getStaticPaths-docs。
代码将像这样运行:
首先,该getStaticPaths函数将获取我们博客文章的所有 slug,并告诉 Next.js 我们要为我们返回的每个路径生成静态页面。
其次,该getStaticProps函数将获取我们创建静态页面所需的所有数据。
一旦实现,它在我的代码中看起来像这样:
pages/[slug].tsx
import { NextPage } from"next";
import { createClient } from"@/lib/apollo";import {
GetBlogPostDocument,
GetBlogPostQuery,
GetBlogPostQueryVariables,
GetBlogPostsDocument,
GetBlogPostsQuery,
GetBlogPostsQueryVariables,
PostDetailed,
} from"@/generated/graphql";type Props = {
post: PostDetailed;
};const BlogPage: NextPage<Props> = ({ post }) => {
return (
<div>
<main>
<h1>{post.title}</h1>
<p>{post.dateAdded}</p>
{post.tags?.map((tag, index) => (
<p key={`${tag ?? index}`}>{tag?.__typename}</p>
))}
<p>{post.contentMarkdown}</p>
</main>
</div>
);
};exportdefault BlogPage;type Path = {
params: {
slug: string;
};
};type StaticPaths = {
paths: { params: { slug: string | null | undefined } }[] | undefined;
fallback: boolean;
};exportconst getStaticPaths = async (): Promise<StaticPaths> => {
const client = createClient();const { data } = await client.query<
GetBlogPostsQuery,
GetBlogPostsQueryVariables
>({ query: GetBlogPostsDocument });let paths;
const posts = data.user?.publication?.posts;if (posts) {
paths = data.user?.publication?.posts?.map((post) => {
return {
params: {
slug: post?.slug,
},
};
});
}return {
paths,
fallback: false,
};
};exportconst getStaticProps = async ({ params }: Path) => {
const client = createClient();const { data } = await client.query<
GetBlogPostQuery,
GetBlogPostQueryVariables
>({
query: GetBlogPostDocument,
variables: {
slug: params.slug,
hostname: "carlw.hashnode.dev",
},
});const post = data.post;return {
props: {
post,
},
};
};
当访问其中一个页面时,这将产生一个看起来像这样的漂亮汤:(我在开始时说过我不打算进入造型👀)
如果您运行 Next.js 构建脚本,您将看到以下输出:
yarn build
yarn run v1.22.15
$ next build
info - Linting and checking validity of types
info - Creating an optimized production build
info - Compiled successfully
info - Collecting page data
info - Generating static pages (9/9)
info - Finalizing page optimization
Route (pages) Size First Load JS
┌ ● / (2379 ms) 2.75 kB 75.9 kB
├ └ css/ad31fca617ac0c09.css 1.33 kB
├ /_app 0 B 73.1 kB
├ ● /[slug] (11386 ms) 406 B 73.5 kB
├ ├ /react-functional-components-const-vs-function (2450 ms)
├ ├ /react-native-getting-user-device-timezone-and-converting-utc-time-stamps-using-the-offset (2357 ms)
├ ├ /tutorial-write-a-re-useable-react-native-component-and-test-it-with-jest (2263 ms)
├ ├ /helping-developers-find-remote-jobs-since-2019 (1459 ms)
├ ├ /publishupdate-npm-packages-with-github-actions (1429 ms)
├ └ /adding-animations-to-your-react-project-using-lottie (1428 ms)
├ ○ /404 181 B 73.3 kB
└ λ /api/hello 0 B 73.1 kB
+ First Load JS shared by all 73.8 kB
├ chunks/framework-2c79e2a64abdb08b.js 45.2 kB
├ chunks/main-f11614d8aa7ee555.js 26.8 kB
├ chunks/pages/_app-891652dd44e1e4e1.js 296 B
├ chunks/webpack-8fa1640cc84ba8fe.js 750 B
└ css/876d048b5dab7c28.css 706 B
λ (Server) server-side renders at runtime (uses getInitialProps or getServerSideProps)
○ (Static) automatically rendered as static HTML (uses no initial props)
● (SSG) automatically generated as static HTML + JSON (uses getStaticProps)
✨ Done in 17.63s.
如您所见,它在路由下生成了多个页面[slug],这些页面现在是静态的,将作为您的其他页面托管。
成功🚀
我现在已经演示了如何开始构建您自己的博客或将博客页面添加到现有的 Next.js 项目。使用 Hashnode API 验证类型安全和模式。酷吧!
作为参考,您可以在 GitHub 上查看完整的源代码🤓
https://github.com/ugglr/nextjs-hashnode-graphql-blog