所有文章 > 学习各类API > 为 Last.fm API 构建自定义 Contentful 应用程序
为 Last.fm API 构建自定义 Contentful 应用程序

为 Last.fm API 构建自定义 Contentful 应用程序

最近,我看了我的第一部音乐剧——舞台版的《回到未来》——我感到很惊讶!这是我见过的最美丽、最有趣的表演。就像我团队的其他成员一样,我成为了音乐剧的粉丝!

或许您从未有过这种乐趣,音乐剧是一种艺术表演形式,它结合了表演、舞蹈和音乐,并以歌曲的形式讲故事。世界上一些最受欢迎的音乐剧是《汉密尔顿》、《邪恶》和《狮子王》。

我已经成为了如此狂热的粉丝,以至于我从事的每个项目都想将它们与音乐剧联系起来!因此,我创建了一个 Contentful 应用程序,允许用户将配乐专辑详细信息添加到他们的内容中。当然,你也可以添加传统的专辑,但我最初的灵感来自对音乐剧的喜爱。

在本文中,我将引导您完成创建 Last.fm 应用程序的步骤,并帮助您在内容中添加一些音乐。那么 Marty McFly,让我们开始吧!

回到未来

什么是 Last.fm?

Last.fm是一个广受欢迎的在线音乐信息服务平台,您可以在这里获取艺术家、专辑、热门歌曲等详细信息。在创建用户资料后,该平台会根据您的收听习惯推荐歌曲。对于这个应用程序,您将使用Last.fm提供的专辑搜索API。

先决条件

要构建此应用程序,您需要满足以下条件:

  • Last.fm API 账户:注册 Last.fm API 账户并创建新应用程序。按照本文档中提到的说明创建应用程序并生成 API 密钥。
  • Contentful 空间:如果您还没有 Contentful 帐户,请在此处注册并创建一个至少具有一种内容类型的空间。如果您不确定如何创建内容类型,请按照本文档中提到的说明进行操作。
  • Node.js 和 npm:要在本地开发和运行应用程序,您需要 Node.js。安装 Node.js,它还会为你安装 npm。

快速开始

如果您想跳过详细信息并快速入门,您可以在 GitHub 上找到该应用程序。您可以直接将其安装在 Contentful 空间上,也可以在您的计算机上本地运行它。

要学习从头开始构建应用程序,请继续阅读。

引导应用程序

您可以为您的 Contentful 空间创建应用程序,以自定义编辑体验、集成外部服务等等。

要为您的 Contentful 空间创建应用程序,您将使用 App Framework。App Framework 提供了创建应用程序所需的所有工具。它提供的包允许您与 API 交互、与各种位置交互等。

您可以使用 CLI 在计算机上创建应用程序,方法是运行以下命令来引导应用程序。

npx create-contentful-app

注意: 默认情况下,上述命令将在 TypeScript 中引导项目。如果要使用 JavaScript,请使用 flag。--javascript

存储库准备就绪后,您可以通过从项目目录运行以下命令来启动本地服务器。

npm start

如果您导航到 localhost:3000,您将看到一条警告消息,指出您无法在 Contentful 之外查看应用程序。

在下一节中,您将学习如何在 Contentful 中创建应用程序,并在 Contentful 中与应用程序交互。

在 Contentful 中创建应用程序

既然您已经在本地运行了项目,下一步就是创建应用程序并在Contentful中配置其应用程序定义。应用程序定义将允许您选择位置、主机URL以及其他应用程序参数。

登录到 Contentful 空间,然后从 应用程序 下拉列表中选择 管理应用程序。接下来,单击左上角的 Manage app definitions,然后单击 Create app 按钮。

在 App name 字段中输入应用程序的名称,然后单击 Create app。这将创建一个新应用程序,并将您带到应用程序的定义页面。

由于您将在开发过程中在本地运行应用程序,因此在 App definition (应用程序定义) 屏幕上,在 frontend 字段中输入 http://localhost:3000。

接下来,在 Locations (位置) 下,选择 App configuration (应用程序配置) 屏幕。应用程序配置位置允许您从用户那里获取配置详细信息,例如,设置 Last.fm API 密钥。

您还需要选择 Entry field > JSON 对象。这是允许用户与应用程序交互并存储信息的位置。

单击 Save 按钮以保存应用程序定义。您的应用程序定义应与下图相同。

应用详细信息

要在 Contentful 空间上安装应用程序,请单击操作,然后选择安装到空间

从 Select a space 下拉列表中选择您的空间,并从 Select an environment 下拉列表中选择环境。授权应用程序,您将看到默认的 App config 页面。

在下一节中,您将了解如何自定义 App configuration location。

自定义 App 配置位置

如果您在应用程序定义中进行了配置,那么应用程序配置位置将是用户安装应用程序后与之交互的第一个屏幕。用户可以在此处提供详细信息,例如 API 密钥、他们想要获取的属性等。

要自定义 App configuration location(应用程序配置位置),请打开该文件。要添加表单,请将其中的代码替换为以下代码。src/locations/ConfigScreen.tsxreturn()

<Flex flexDirection="column" className={css({ margin: '80px', maxWidth: '800px' })}>
<Form>
<FormControl>
<FormControl.Label>API Key</FormControl.Label>
<TextInput
value={parameters.apiKey}
type='text'
onChange={(e) => setParameters({...parameters, apiKey:e.target.value})}
/>
</FormControl>
</Form>
</Flex>

上面的代码使用了 Forma 36 中的 、 、 和 组件。更新导入以使用这些组件。FlexFormFormControlTextInput

您还可以观察到 value 属性从 获取值。TextInputparameters.apiKey

parameters 是已为您定义的类型的状态。AppInstallationParameters

要充分利用 TypeScript,请更新 .你的应用 AppInstallationParameters 应如下所示。AppInstallationParameters

export interface AppInstallationParameters {
apiKey: string | undefined;
}

最终代码应如下所示。

import React, { useCallback, useState, useEffect } from 'react';
import { AppExtensionSDK } from '@contentful/app-sdk';
import {Form, FormControl, Flex, TextInput } from '@contentful/f36-components';
import { css } from 'emotion';
import { useSDK } from '@contentful/react-apps-toolkit';

export interface AppInstallationParameters {
apiKey: string | undefined;
}

const ConfigScreen = () => {
const [parameters, setParameters] = useState<AppInstallationParameters>({apiKey: ''});
const sdk = useSDK<AppExtensionSDK>();

const onConfigure = useCallback(async () => {
const currentState = await sdk.app.getCurrentState();

return {
parameters,
targetState: currentState,
};
}, [parameters, sdk]);

useEffect(() => {
sdk.app.onConfigure(() => onConfigure());
}, [sdk, onConfigure]);

useEffect(() => {
(async () => {
const currentParameters: AppInstallationParameters | null = await sdk.app.getParameters();

if (currentParameters) {
setParameters(currentParameters);
}
sdk.app.setReady();
})();
}, [sdk]);

return (
<Flex flexDirection="column" className={css({ margin: '80px', maxWidth: '800px' })}>
<Form>
<FormControl>
<FormControl.Label>API Key</FormControl.Label>
<TextInput
value={parameters.apiKey}
type='text'
onChange={(e) => setParameters({...parameters, apiKey:e.target.value})}
/>
</FormControl>
</Form>
</Flex>
);
};

export default ConfigScreen;

更深入地了解代码超出了本文的范围。如果您有兴趣了解更多信息,可以阅读官方文档。

但以下是代码作用的快速摘要:当应用程序首次安装时,用户会看到此表单。用户在此处输入其API密钥。当用户点击“安装”按钮时,API密钥会被存储在参数对象中。这样,您就可以从其他任何位置引用该值。

保存代码,您现在将在 App configuration (应用程序配置) 位置查看表单。输入您的 Last.fm API 密钥,然后点击 Install(安装)。

您的应用程序现已安装并可供使用!

Last.fm

在要添加唱片集数据的内容类型中,创建一个 JSON 对象类型的新字段。确保在 Appearance (外观) 选项卡中选择您的应用。

Last.fm

为该内容类型创建一个新条目,您应该会看到默认的 JSON 编辑器。您将在下一节中了解如何自定义此字段。

自定义 Entry Field 位置

在配置 App definition 时,您选择了 App configuration 作为 Entry 字段位置。

在上一节中,您自定义了 App configuration location(应用程序配置位置)。在本节中,您将自定义 Entry field location 以显示用户输入和渲染数据。

对于此应用程序,用户将输入他们想要添加的相册名称。该应用程序将从 API 返回所有相关相册,并在对话框组件中向用户显示它们。用户选择相册,应用程序保存相册信息。

与 App configuration location(应用程序配置位置)类似,您可以自定义文件中的 Entry field (src/locations/Field.tsx) 位置。打开文件并将 return 语句替换为以下代码。

return (
<>
<Form onSubmit={()=>openDialog()}>
<FormControl>
<FormControl.Label isRequired>Album name</FormControl.Label>

<TextInput type='text' onChange={(e) => setAlbumSearch(e.target.value)} isRequired/>
</FormControl>
<FormControl>
<Button type='submit' variant='primary'>Search</Button>
</FormControl>
</Form>
</>
)

更新导入以包括FormFormControlTextInputButton组件。

如果您仔细观察代码,则会看到一个albumSearch state 和一个 openDialogfunction。按如下方albumSearch式初始化状态。确保更新useState hook 的导入。

const [albumSearch, setAlbumSearch] = useState<string>('');

接下来,按如下方式定义openDialog函数。

const openDialog = async () => {
const album = await sdk.dialogs.openCurrentApp({
width: 700,
parameters: {
albumName: albumSearch
},
title: "Album Search",
allowHeightOverflow:true,
shouldCloseOnEscapePress: true,
shouldCloseOnOverlayClick: true
})
}

上述函数将打开一个对话框组件,该组件从 Last.fm API 返回结果。你正在传递albumName参数,该参数在Dialog组件中使用。

您的最终代码应如下所示。

import React, {useState} from 'react';
import { Form, FormControl, TextInput, Button } from '@contentful/f36-components';
import { FieldExtensionSDK } from '@contentful/app-sdk';
import { useSDK } from '@contentful/react-apps-toolkit';

const Field = () => {
const sdk = useSDK<FieldExtensionSDK>();
const [albumSearch, setAlbumSearch] = useState<string>('');
const openDialog = async () => {
const album = await sdk.dialogs.openCurrentApp({
width: 700,
parameters: {
albumName: albumSearch
},
title: "Album Search",
allowHeightOverflow:true,
shouldCloseOnEscapePress: true,
shouldCloseOnOverlayClick: true
})
}

return (
<>
<Form onSubmit={()=>openDialog()}>
<FormControl>
<FormControl.Label isRequired>Album name</FormControl.Label>

<TextInput type='text' onChange={(e) => setAlbumSearch(e.target.value)} isRequired/>
</FormControl>
<FormControl>
<Button type='submit' variant='primary'>Search</Button>
</FormControl>
</Form>
</>
)
};

export default Field;

保存代码,应用程序将热重载。您的字段现在将具有一个输入字段和一个类似于下图的按钮。

last.fm

尝试提交表单,对话框将呈现。Marty,相信你已经知道下一步是什么了— 自定义对话组件。

获取数据并自定义对话组件

在本节中,您将学习如何自定义对话组件。此组件将从 Last.fm API 获取数据,并将其呈现给用户。

打开src/locations/Dialog.tsx文件和以下 TypeScript 接口。

export interface Album {
 name:       string;
 artist:     string;
 url:        string;
 image:      Image[];
 streamable: string;
 mbid:       string;
}

export interface Image {
 "#text": string;
 size:    string;
}

接下来,声明 Album 类型的状态相册。您将 API 调用的结果存储在 album 状态中。您可以使用 API 密钥从 Last.fm 获取数据。由于您在安装应用程序时已经设置了 API 密钥,因此您将使用该 API 密钥。销毁apiKey fromsdk.parameters.installation

const [album,setAlbum] = useState<Album[] | undefined>();

const {apiKey} = sdk.parameters.installation;

现在,您已经拥有了状态和 API 密钥,您将定义一个函数,该函数将从 Last.fm API 获取数据。复制并粘贴以下函数代码。

const fetchData = async (albumName: string) => {
   const response = await fetch(`https://ws.audioscrobbler.com/2.0/?method=album.search&album=${albumName}&api_key=${apiKey}&format=json`)
   const {results} = await response.json();
   setAlbum(results.albummatches.album);
 }

在上面的代码中,您正在使用 fetch API 从 API 获取结果。在查询参数中,您传递了专辑名称 (albumNameapi) 和 API 密钥 (Key)。

从 API 获得响应后,您可以使用json()方法解析 promise 并销毁 results 数组。最后,使用所需的结果更新专辑状态的值。

该应用程序从您之前创建的输入字段中获取搜索查询。用户提交表单后,fetchData将呈现对话框。该函数应在组件渲染后立即执行。

因此,添加一个useEffect钩子,并调用从输入字段fetchData传递专辑名称的函数。下面是执行此操作的代码。

useEffect(()=>{
   // @ts-expect-error
   fetchData(sdk.parameters.invocation.albumName)
 },[sdk.parameters.invocation])

每次用户搜索新专辑时,您的组件都会获取数据。但是,该组件仍然不呈现数据。将 return 替换为以下代码,以在 dialog 组件中显示数据。

if(!album){
return <Spinner size="large" />
}
return (
<Stack fullWidth>
<EntityList style={{
width: '100%'
}}>
{
album.map((item,i)=>{
return(<EntityList.Item
key={i}
title={item.name}
thumbnailUrl={item.image[1]['#text']}
onClick={()=> sdk.close({
name: item.name,
image: item.image[2]['#text']
})}
/>)
})
}
</EntityList>
</Stack>
);

让我们了解一下上面的代码中发生了什么。使用 IF 语句,您首先检查相册是否包含任何值。如果应用程序正在获取数据,则专辑状态为 null,用户将看到一个微调器。但是一旦数据被获取,结果就会被渲染。

onClick组件的EntityList.Item属性将关闭对话框组件,并将所选影集的名称和图像信息发送回字段位置。

在继续下一部分之前,请将挂钩useAutoResizer()添加到对话组件中。这个 hook 处理组件的大小调整。您的对话组件应如下所示。

import React, { useEffect, useState } from 'react';
import { Spinner, Stack, EntityList } from '@contentful/f36-components';
import { DialogExtensionSDK } from '@contentful/app-sdk';
import { useAutoResizer, useSDK } from '@contentful/react-apps-toolkit';

export interface Album {
name: string;
artist: string;
url: string;
image: Image[];
streamable: string;
mbid: string;
}

export interface Image {
"#text": string;
size: string;
}

const Dialog = () => {
const sdk = useSDK<DialogExtensionSDK>();
useAutoResizer();

const [album,setAlbum] = useState<Album[] | undefined>();

const {apiKey} = sdk.parameters.installation;

const fetchData = async (albumName: string) => {
const response = await fetch(`https://ws.audioscrobbler.com/2.0/?method=album.search&album=${albumName}&api_key=${apiKey}&format=json`)
const {results} = await response.json();
setAlbum(results.albummatches.album);
}

useEffect(()=>{
// @ts-expect-error
fetchData(sdk.parameters.invocation.albumName)
},[sdk.parameters.invocation])

if(!album){
return <Spinner size="large" />
}
return (
<Stack fullWidth>
<EntityList style={{
width: '100%'
}}>
{
album.map((item,i)=>{
return(<EntityList.Item
key={i}
title={item.name}
thumbnailUrl={item.image[1]['#text']}
onClick={()=> sdk.close({
name: item.name,
image: item.image[2]['#text']
})}
/>)
})
}
</EntityList>
</Stack>
);
};

export default Dialog;

在 Contentful 中存储相册数据

使用您的应用,用户可以搜索他们最喜欢的专辑。用户从搜索结果中选择相册后,应用程序既不会向用户显示相册,也不会将数据存储在 Contentful 中。

在本节中,您将更新 Field 组件以呈现所选数据并将其存储在 Contentful 中。

打开src/locations/Field.tsx文件并声明接口Album

interface Album {
name: string,
image: string
}

接下来,从@contentful/react-apps-toolkit包中导入useFieldValue钩子,并按如下方式声明albumData

const [albumData, setAlbumData] = useFieldValue<Album | null>()

更新openDialog函数,并将albumData的值设置为album

const openDialog = async () => {
const album = await sdk.dialogs.openCurrentApp({
width: 700,
parameters: {
albumName: albumSearch
},
title: "Album Search",
allowHeightOverflow:true,
shouldCloseOnEscapePress: true,
shouldCloseOnOverlayClick: true
})
if(album){
setAlbumData(album);
}
}

当用户从列表中选择相册时,对话框将关闭,并返回相册名称和相册图像。上面的代码将此对象值存储在字段中。用户可能会关闭对话框而不选择相册。IF 语句可以解决这个问题。

最后一步是更新我们的组件以显示选定的相册。在 closing 标签的末尾添加以下Form代码。

{
       albumData && (
         <AssetCard
             type='image'
             title={albumData.name}
             src={albumData.image}
             actions={[
               <MenuItemkey="remove"onClick={()=>setAlbumData(null)}>Remove</MenuItem>
             ]}
           />
       )
     }

在这里,您再次检查albumData是否包含任何数据。如果它包含从对话框组件返回的数据,您将使用Forma 36的AssetCard组件来渲染它。您还添加了一个操作,允许用户删除选定的专辑。

您应该添加的最后一件事是useAutoResizer钩子。您的字段位置(Field location)的最终代码应该如下所示:

import React, {useState} from 'react';
import { Form, FormControl, TextInput, Button, AssetCard, MenuItem } from '@contentful/f36-components';
import { FieldExtensionSDK } from '@contentful/app-sdk';
import { useAutoResizer, useFieldValue, useSDK } from '@contentful/react-apps-toolkit';

interface Album {
name: string,
image: string
}

const Field = () => {
const sdk = useSDK<FieldExtensionSDK>();
useAutoResizer();
const [albumSearch, setAlbumSearch] = useState<string>('');
const [albumData, setAlbumData] = useFieldValue<Album | null>()
const openDialog = async () => {
const album = await sdk.dialogs.openCurrentApp({
width: 700,
parameters: {
albumName: albumSearch
},
title: "Album Search",
allowHeightOverflow:true,
shouldCloseOnEscapePress: true,
shouldCloseOnOverlayClick: true
})
if(album){
setAlbumData(album);
}
}

return (
<>
<Form onSubmit={()=>openDialog()}>
<FormControl>
<FormControl.Label isRequired>Album name</FormControl.Label>

<TextInput type='text' onChange={(e) => setAlbumSearch(e.target.value)} isRequired/>
</FormControl>
<FormControl>
<Button type='submit' variant='primary'>Search</Button>
</FormControl>
</Form>
{
albumData && (
<AssetCard
type='image'
title={albumData.name}
src={albumData.image}
actions={[
<MenuItem key="remove" onClick={()=>setAlbumData(null)}>Remove</MenuItem>
]}
/>
)
}
</>
)
};

export default Field;

尝试搜索其他影集。您的应用程序现在将提取数据、显示搜索结果并呈现所选相册。

专辑名称

下一步是什么?

在本文中,您创建了一个与外部 API 集成的 Contentful,并允许您添加内容。

现在,您的应用程序已准备就绪,请托管它以供您的团队使用。您可以将应用程序托管在 Contentful 或 Netlify、Vercel 等外部服务上。按照文档了解如何托管您的应用程序。

您可以在开发者展示中查看社区创建的大量应用程序。如果您创建的应用程序可能会使其他人受益,请考虑提交。

最后,请随时在 Twitter 上与我联系,分享您最喜欢的音乐剧、谈论 Contentful 或提出问题。

原文来源:https://www.contentful.com/blog/build-custom-contentful-app-last-fm-api/

#你可能也喜欢这些API文章!