使 Next.js SWR

September 24, 2020

Pokedex

( Demo || Code )

Next.js 当前最火最热的 React 框架!

SWR 时下最强最有潜力的 React Hooks 库!

我们结合使用 Next.js 和 SWR 动手开发 宝可梦图鉴 这个小项目来学习它是如何运作的

🔧 创建项目

首先用命令行工具新建一个项目,很简单,只需要一行代码

npx create-next-app
# or
yarn create next-app

🔌 PokeAPI

PokeAPI 是一个免费提供宝可梦数据的 API 对于我们的 宝可梦图鉴 项目是必不可少的,阅读官方文档大致了解如何使用这个接口

👉🏻 初始页面

在刚刚创建完成的项目目录中,我们找到 pages/index.js 然后编辑它

import Head from "next/head"
import useSWR from "swr"

const fetcher = url => fetch(url).then(res => res.json())

function HomePage() {
  const { data } = useSWR(`https://pokeapi.co/api/v2/pokemon/`, fetcher)

  if (!data) return <h1>Loading...</h1>
  const { results } = data

  return (
    <>
      <Head>        <link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet" />      </Head>      <section className="container py-6 mx-auto">
        <h1 className="text-4xl text-center mb-2">寶可夢圖鑑</h1>
        <div>
          {results.map(pokemon => {
            return <div key={pokemon.name}>{pokemon.name}</div>
          })}
        </div>
      </section>
    </>
  )
}

export default HomePage

注意:这里直接通过 CDN 使用 Tailwind 只是为了便捷,在实际项目中,这种方法并非使用 Tailwind 的最佳方式!

➕ 加载更多

参考 SWR 文档以及示例,我们对 pages/index.js 做如下修改

import Head from "next/head";
import { useSWRInfinite, SWRConfig } from "swr";

const fetcher = (url) => fetch(url).then((res) => res.json());
const PAGE_SIZE = 20;

function CatchEmAll() {
  const { data, error, size, setSize } = useSWRInfinite(    (index) =>
      `https://pokeapi.co/api/v2/pokemon/?offset=${
        PAGE_SIZE * index
      }&limit=${PAGE_SIZE}`,
    fetcher
  );

  const pokemonList = data ? [].concat(...data) : [];
  const isLoadingInitialData = !data && !error;
  const isLoadingMore =
    isLoadingInitialData ||
    (size > 0 && data && typeof data[size - 1] === "undefined");
  const isEmpty = data?.[0]?.length === 0;
  const isReachingEnd =
    isEmpty || (data && data[data.length - 1]?.length < PAGE_SIZE);

  return (
    <>
      <Head>
        <link
          href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css"
          rel="stylesheet"
        />
        <title>寶可夢圖鑑</title>
      </Head>
      <section className="container py-6 mx-auto">
        <h1 className="text-4xl text-center mb-2">寶可夢圖鑑</h1>
        <div className="flex flex-wrap">
          {pokemonList.map((pokemon) => {
            return pokemon.results.map((result) => (
              <div
                className="p-2 w-full sm:w-1/2 md:w-1/2 lg:w-1/3 xl:w-1/4"
                key={result.name}
              >
                <div className="rounded-md shadow-md w-full p-2 bg-gray-500">
                  <div className="poke-name flex justify-between items-center px-1">
                    {result.name}
                  </div>
                </div>
              </div>
            ));
          })}
        </div>
        <div className="mx-auto py-10 w-1/2 text-center">
          <button
            className="inline-flex items-center px-4 py-2 border border-transparent text-base leading-6 font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none"
            disabled={isLoadingMore || isReachingEnd}
            onClick={() => setSize(size + 1)}
          >
            {isLoadingMore
              ? Loading
              : isReachingEnd
              ? "No More Pokémon"
              : "Load More Pokémon"}
          </button>
        </div>
      </section>
    </>
  );
}

function HomePage() {
  return (
    <SWRConfig value={{ fetcher }}>
      <CatchEmAll />
    </SWRConfig>
  );
}

export default HomePage;

这里通过 useSWRInfinite 中的 setSize 函数向 PokeAPI 请求新的数据,实现加载更多这个功能

📇 宝可梦卡片

上一步我们实现了加载更多,但是仅仅获取了宝可梦的英文名。现在我们希望展示宝可梦的图片和中文名,要怎么做呢?

新建 components 文件夹并创建 pokemon.js

import useSWR from "swr";

function Pokemon({ name }) {
  const { data: pokemon } = useSWR(`https://pokeapi.co/api/v2/pokemon/${name}`);  const { data: pokemonSpecies } = useSWR(() => pokemon.species.url);
  const bgc = pokemonSpecies ? pokemonSpecies.color.name : "gray"; // background-color
  const names = pokemonSpecies ? [].concat(...pokemonSpecies.names) : [];  const lan = names.filter((obj) => {    return obj.language.name === "zh-Hant"; // 宝可梦的繁体中文名  });  const theName = pokemonSpecies ? lan[0].name : name;
  return (
    <div className="p-2 w-full sm:w-1/2 md:w-1/2 lg:w-1/3 xl:w-1/4">
      <article className={`rounded-md shadow-md w-full p-3 bg-${bgc}-500`}>
        {pokemon ? (
          <div className={`poke-name id-${pokemon.id} flex justify-between items-center px-1`}>
            <div>
              <h2 className="text-lg capitalize mb-2">{theName}</h2>
              <div>
                {pokemon.types.map((type) => (
                  <span
                    key={type.type.name}
                    className="inline-block bg-gray-400 bg-opacity-25 rounded-lg px-2 text-sm text-gray-700 mr-2 mb-2"
                  >
                    {type.type.name}
                  </span>
                ))}
              </div>
            </div>
            <div className="w-16 h-16 ml-1">
              <img
                src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${pokemon.id}.png`}
                alt={name}
              />
            </div>
          </div>
        ) : (
          <p className="font-bold text-l capitalize">Loading {name}...</p>
        )}
      </article>
    </div>
  );
}

export default Pokemon;

这里首先获取 pokemon 的数据,然后通过已获取数据 pokemon.species.url 使用 useSWR() 获取 pokemonSpecies

pokemonSpecies.names 是一个包含宝可梦多国语言名称的数组,我们用 filter 函数筛选出我们需要的语言。

接下来我们回到 pages/index.js 引入 Pokemon 部件,代码调整如下

import Head from "next/head";
import { useSWRInfinite, SWRConfig } from "swr";
import Pokemon from "../components/pokemon";
...

return (
    <>
      <Head>
        <title>寶可夢圖鑑</title>
      </Head>
      <section className="container py-6 mx-auto">
        <h1 className="text-4xl text-center mb-2">寶可夢圖鑑</h1>
        <div className="flex flex-wrap">
          {pokemonList.map((pokemon) => {
            return pokemon.results.map((result) => (
              <Pokemon key={result.name} name={result.name} />            ));
          })}
        </div>
        <div className="mx-auto py-10 w-1/2 text-center">
          <button
            className="inline-flex items-center px-4 py-2 border border-transparent text-base leading-6 font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none"
            disabled={isLoadingMore || isReachingEnd}
            onClick={() => setSize(size + 1)}
          >
            {isLoadingMore
              ? Loading
              : isReachingEnd
              ? "No More Pokémon"
              : "Load More Pokémon"}
          </button>
        </div>
      </section>
    </>
  );
}

( Demo || Code )


Comments