53、现代Web开发:LyricsFinder V2实现与优化

现代Web开发:LyricsFinder V2实现与优化

1. 重访SongsList组件

1.1 修改Props接口

首先,回到 songs-list.tsx 文件,将 Props 接口修改为如下形式:

type Props = {
    songs: FindSongs_songs[];
    songSelected: (song: FindSongs_songs) => void;
};

同时添加导入语句:

import { FindSongs_songs } from '../generated/FindSongs';

1.2 渲染歌曲列表

使用 react-bootstrap ListGroup ListGroup.Item 组件来渲染歌曲列表。在 SongsList 组件内添加以下代码:

const songElements = props.songs
    .filter((song: FindSongs_songs) => song.hasLyrics)
    .map((song: FindSongs_songs) =>
        <ListGroup.Item key={song.id} className='lf-song' action
            style={{cursor: 'pointer'}}
            onClick={() => props.songSelected(song)}>{song.name}
        </ListGroup.Item>
    );

添加 ListGroup 的导入语句:

import { ListGroup } from 'react-bootstrap';

需要注意的点:
- 为每个元素设置 key 以优化重新渲染操作。
- 设置 action 属性将列表项标记为可操作项。
- 添加 onClick 事件处理程序,调用 songSelected 回调函数。
- 过滤操作可考虑移至 Home 组件。

最后,更新组件的返回表达式:

return (
  <Container className='lf-songs'>
      <h3>Songs</h3>
      <ListGroup>
          {songElements}
      </ListGroup>
  </Container>
);

1.3 更新Home视图

打开 frontend/src/views/home.tsx 文件,在 Home 组件内添加以下常量:

const songSelected = (selectedSong: FindSongs_songs) => {
    console.log('Home: song selected: ', selectedSong);
};

更新 Home 视图中的 SongsList 元素:

<SongsList songs={foundSongs} songSelected={songSelected} />

移除之前的 foundSongsList 常量和对应的 ul 标签。

2. 前端:加载歌词并跳转至歌词页面

2.1 处理歌曲选择事件

当用户选择一首歌曲时,我们要显示歌词页面并渲染歌词。由于已配置好 GraphQL 客户端,我们将在 Home 页面直接获取歌词,并通过路由参数传递歌曲和歌词。

2.2 修改Home页面

打开 frontend/src/pages/home.tsx 文件,进行以下操作:
1. 添加导入语句:

import { RouteComponentProps } from 'react-router';
  1. 声明 props 参数:
export const Home = (props: RouteComponentProps)

2.3 适配 songSelected 函数

import { FindLyricsQuery } from '../graphql/queries'
import { FindLyrics, FindLyricsVariables } from '../generated/FindLyrics';
...
const songSelected = (selectedSong: FindSongs_songs) => {
    console.log('Home: song selected: ', selectedSong);
    apolloClient.query<FindLyrics, FindLyricsVariables>({
        query: FindLyricsQuery,
        variables: {
            id: selectedSong.id,
        },
    }).then((result: ApolloQueryResult<FindLyrics>) => {
        const songLyrics = result.data.songLyrics;
        console.log(`Home: lyrics loaded for [${selectedSong.name}]: ${songLyrics.lyrics}`);
        props.history.push('/lyrics', {
            song: selectedSong,
            songLyrics,
        });
    }).catch((error: any) => {
        console.log('Home: error while loading lyrics: ', error);
    });
};

3. 前端:实现歌词页面

3.1 修改歌词页面

打开 frontend/src/pages/lyrics.tsx 文件,进行如下修改:

import React, { ReactElement } from "react";
import { RouteComponentProps } from 'react-router';
import Container from 'react-bootstrap/Container';
import { Link } from 'react-router-dom';
import { FindLyrics_songLyrics } from '../generated/FindLyrics';
import { FindSongs_songs } from '../generated/FindSongs';
import Card from 'react-bootstrap/Card';

type LyricsLocationState = {
    song: FindSongs_songs;
    songLyrics: FindLyrics_songLyrics;
};

interface LyricsProps extends RouteComponentProps<any, any, LyricsLocationState> {
}

export const Lyrics = (props: LyricsProps) => {
  const songLyrics = props.location.state.songLyrics;
  const songLyricsLines: ReactElement[] = [];
  if (songLyrics.lyrics) {
    songLyrics.lyrics.split("\n").forEach((line, i) => {
      songLyricsLines.push(
        <span key={i}>
          {line}
          <br />
        </span>
      );
    });
  }
    const song = props.location.state.song;
    return (
        <Container className='lf-lyrics'>
            <Card>
                <Card.Header>{song.name} (<Link to='/' title='Go back'>Go back</Link>)</Card.Header>
                <Card.Body>
                    <Card.Text>
                      <span>{songLyricsLines}</span>
                    </Card.Text>
                    <h4>Copyright:</h4>
                    <span>{songLyrics.copyright}</span>
                </Card.Body>
            </Card>
        </Container>
    );
};

3.2 代码解释

  • 定义 LyricsLocationState 类型和 LyricsProps 接口,明确 location 关联的状态类型。
  • 分割歌词字符串为多行 ReactElement
  • 使用 Card 组件渲染歌词,在卡片头部添加返回主页的链接。

3.3 安全考虑

避免使用 dangerouslySetInnerHTML 属性直接绑定歌词,以防止跨站脚本攻击(XSS)。

以下是整个过程的流程图:

graph TD;
    A[修改SongsList组件] --> B[更新Home视图];
    B --> C[处理歌曲选择事件];
    C --> D[实现歌词页面];

4. 后续优化建议

4.1 添加图标

可以引入 FontAwesome 图标库,为不同页面和组件添加图标。

4.2 创建ArtistsList组件

实现 ArtistsList 组件,并在 Home 页面并排渲染歌曲列表和艺术家列表,可使用手风琴或标签面板进行包裹。示例代码如下:

<Accordion>
    <Card>
        <Accordion.Toggle as={Card.Header} variant="link" eventKey="0">
            <h3>Artists</h3>
        </Accordion.Toggle>
        <Accordion.Collapse eventKey="0">
            <Card.Body>
                <ul>
                    {foundArtistsList}
                </ul>
            </Card.Body>
        </Accordion.Collapse>
        <Accordion.Toggle as={Card.Header} variant="link" eventKey="1">
            <h3>Songs</h3>
        </Accordion.Toggle>
        <Accordion.Collapse eventKey="1">
            <Card.Body>
                <SongsList songs={foundSongs} songSelected={songSelected}/>
            </Card.Body>
        </Accordion.Collapse>
    </Card>
</Accordion>

4.3 无歌曲或艺术家时显示消息

SongsList ArtistsList 组件中使用条件渲染,当没有元素可显示时显示消息。示例代码如下:

<Container className='lf-songs'>
    {songElements.length > 0? (
    <ListGroup>
        {songElements}
    </ListGroup>
    ) : (
       <span>No songs</span>
    )}
</Container>

4.4 添加加载指示器

添加状态变量来保存艺术家和歌曲的加载状态,并在 JSX 代码中使用条件渲染显示和隐藏加载指示器。示例代码如下:

const [artistsLoading, setArtistsLoading] = useState(false);
setArtistsLoading(true);
apolloClient.query<FindArtists, FindArtistsVariables>({
    ...
}).then((result: ApolloQueryResult<FindArtists>) => {
    ...
}).finally(() => setArtistsLoading(false));

在 JSX 中显示加载指示器:

<Accordion.Toggle as={Card.Header} variant="link" eventKey="0">
    <h3>Artists</h3>
    { artistsLoading &&
        <Spinner animation="border" role="status">
            <span className="sr-only">Loading...</span>
        </Spinner>
    }
</Accordion.Toggle>

通过这些优化,可以进一步提升应用的用户体验和功能完整性。

5. 回顾与总结

5.1 前端技术发展回顾

前端软件开发生态系统在过去几年发生了显著变化。ECMAScript 规范新的年度发布节奏推动了语言的快速发展,赋予开发者更多能力。我们从安装必要的开发工具开始,如 Visual Studio Code、Git、Node.js、NPM 和 TypeScript。

5.2 TypeScript 基础学习

学习了 TypeScript 的基础概念、基本类型,掌握了函数的编写方法。通过 Hello World 程序实践后,深入了解了更多特性,如 lambda 表达式、迭代器和循环、类型声明、类型推断、数组、null 特殊类型以及 as 类型转换运算符。

5.3 项目实践与知识应用

在开发 TodoIt 应用的过程中,了解了 npm 的工作原理,学会了编写和执行脚本、管理依赖项,掌握了 TypeScript 编译器的配置和构建模式。同时,利用现代浏览器的开发者工具进行调试,学习了如何生成和使用源映射文件。

5.4 面向对象编程与 TypeScript 特性

在第三章中,回顾了面向对象编程(OOP)的主要概念,包括封装、抽象、继承、多态、接口和类。同时,学习了 TypeScript 中与 OOP 相关的特性,如类和继承、字段和可见性、构造函数和访问器、接口、类型注解、自定义类型、结构类型、readonly 关键字、可选参数和默认值。并利用这些知识构建了新的 TodoIt 应用,体会到 OOP 对代码结构优化的重要性。

5.5 更多 TypeScript 特性与应用

在开发 MediaMan 应用时,接触到了更多 TypeScript 特性,如泛型、枚举、字符串字面量类型、联合和交叉类型、类型定义和 @types、装饰器、any 和 never 特殊类型、keyof 关键字和映射类型。同时,学习了现代浏览器的存储 API,如 LocalStorage、SessionStorage 和 IndexedDB,并使用了 localForage 和 class-transformer 等库。

5.6 模块化开发

在开发 WorldExplorer 应用时,重点关注了模块化开发。了解了 JavaScript 模块化的发展历程,从 Revealing Module 模式到 AMD/CommonJS、UMD 和 TypeScript 模块。学习了 TypeScript 中与模块化相关的概念,如导入和导出、模块、重新导出和桶、命名空间和模块解析。

以下是学习过程的关键知识点总结表格:
| 阶段 | 关键知识点 |
| ---- | ---- |
| 前端技术发展 | ECMAScript 规范、开发工具安装 |
| TypeScript 基础 | 基本类型、函数编写、特性学习 |
| 项目实践 | npm 使用、编译器配置、调试技巧 |
| 面向对象编程 | OOP 概念、TypeScript 相关特性 |
| 更多特性 | 泛型、枚举、存储 API |
| 模块化开发 | JavaScript 模块化历程、TypeScript 模块化概念 |

6. 未来学习方向

6.1 深入学习现有技术

虽然已经对 Angular、Vue.js、React、NestJS 和 GraphQL 有了一定的了解,但这些技术还有很多深入的内容值得学习。例如,在 React 中,可以进一步研究高阶组件、自定义 Hooks 等高级特性;在 GraphQL 方面,可以学习如何优化查询性能、处理复杂的关联关系等。

6.2 探索新的技术领域

随着技术的不断发展,新的前端框架和后端技术不断涌现。可以关注一些新兴的技术,如 Svelte、Deno 等,了解它们的特点和优势,为未来的项目选择合适的技术栈。

6.3 提升项目实践能力

通过参与更多的实际项目,将所学的知识应用到实际开发中,积累项目经验。可以尝试开源项目贡献,与其他开发者交流合作,提高自己的团队协作和问题解决能力。

6.4 关注行业动态

保持对行业动态的关注,阅读技术博客、参加技术会议和线上研讨会,了解最新的技术趋势和最佳实践。这有助于拓宽视野,不断提升自己的技术水平。

以下是未来学习方向的流程图:

graph LR;
    A[深入学习现有技术] --> B[探索新的技术领域];
    B --> C[提升项目实践能力];
    C --> D[关注行业动态];

通过不断学习和实践,我们可以在现代软件开发的道路上不断前进,掌握更多的技能和知识,为自己的职业生涯打下坚实的基础。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值