跳转到主要内容
Chal1ce blog

我的项目:AutoPodcast(下:能学到什么)

给大家分享一下我之前的练手小项目:自动化生成播客

实现一个简单的AI播客生成工具(知识篇)

在上一篇文章中我分享了一篇如何用nextjspython来构建一个AI播客生成工具的文章,里面谈到了整个Web程序的框架和一些关键功能的代码实现,这一篇文章我想要聊一聊在提到的这个项目中,可以学习到一些什么样的知识。

前端部分

React 和 Next.js 前端开发:

  • 使用 React hooks 进行状态管理和副作用处理

  • Next.js 的客户端渲染和动态导入

  • 组件化开发和代码复用

客户端渲染

Next.js 是一个基于 React 的框架,它提供了许多强大的功能,包括服务器端渲染(SSR)、静态站点生成(SSG)以及客户端渲染(CSR)。而在上篇文章提到的项目中,我们使用的是客户端渲染(CSR)。

1、什么是客户端渲染?

客户端渲染(CSR)是指在浏览器中通过 JavaScript 动态生成页面内容的过程。与服务器端渲染(SSR)不同,CSR 的页面内容在初始加载时通常是空的,然后通过 JavaScript 在客户端进行填充。

2、为什么使用客户端渲染?

  • 更好的用户体验:客户端渲染可以实现更流畅的用户体验,因为页面内容可以在不刷新整个页面的情况下进行更新。

  • 减少服务器负载:客户端渲染将部分工作转移到客户端,减轻了服务器的负担。

  • 动态内容:对于需要频繁更新的内容(如实时数据、用户交互等),客户端渲染更为合适。

3、项目哪里体现了是客户端渲染?

在项目中,page.js文件中使用 'use client' 指令,该项目还有其他几个方面体现了客户端渲染:

  • 使用 React hooks:项目大量使用了 React 的 useState 和 useRef hooks,这些都是客户端渲染的特征。

  • 事件处理:项目中包含了许多事件处理函数,如 handleDrop、handleChange、handleRoleChange 等,这些都是在客户端执行的。

  • 动态导入:在 app/page.js 中使用了动态导入,这是一种客户端优化技术。

  • 状态管理和 UI 更新:项目中有大量的状态更新和基于状态的 UI 渲染,这些都是在客户端进行的。

  • 浏览器 API 的使用:项目使用了一些只在浏览器环境中可用的 API,如 URL.createObjectURL 和 document.createElement。

  • 异步操作:项目中包含了多个异步操作,如文件上传和 API 请求,这些通常在客户端执行

动态导入

在项目中,由于我们有两种生成对话文本的模式,一种是根据用户输入的主题来生成对话文本,另一种是上传文件来生成对话文本.

在选择前者时,我们用不到FileUpload组件,当我们选择上传文件的模式时,我们才需要导入FileUpload组件,因此我们使用了Next.js的dynamic函数来动态导入FileUpload组件。

// 动态导入 FileUpload 组件
const FileUpload = dynamic(() => import('./components/FileUpload'), { ssr: false });

这种动态导入方式允许将FileUpload组件的加载推迟到运行时,只有在需要时才会加载,从而优化了应用的初始加载性能。

条件渲染

1、什么是条件渲染?

条件渲染是指根据某些条件来决定是否渲染某个组件或元素。这在处理用户权限、显示错误信息、切换视图等场景中非常有用。

2、如何进行条件渲染?

在 React 中,可以使用 JavaScript 的条件语句(如 if、else、&&、? : 等)来进行条件渲染。

在项目里的选择生成对话文本的模式中,就用到了条件渲染,根据选择的选项(主题生成对话 或 上传文件生成对话)来渲染不同的组件,例如选择了上传文件生成对话,则渲染FileUpload组件,提高了代码的复用性和灵活性。

          <div className="content-container">
            {selectedOption === 'prompt' && (
              <>
                <Form 
                  topic={topic}
                  setTopic={setTopic}
                  format={format}
                  setFormat={setFormat}
                />
                <div className="mt-6">
                  <GenerateDialog 
                    topic={topic} 
                    format={format} 
                    setGeneratedText={setGeneratedText}
                    setIsLoading={setIsLoading}
                    model={getModelEndpoint(selectedModel, modelMode === 'local')}
                  />
                </div>
                <ResponseDisplay 
                  generatedText={generatedText}
                  isLoading={isLoading}
                  setGeneratedText={setGeneratedText}
                  ttsModelMode={ttsModelMode}
                  selectedTtsModel={selectedTtsModel}
                />
                    <span className="model-select-label">TTS 模式</span>
                <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-6">
                  <div>
                    <span className="model-select-label">TTS 模式</span>
                    <ToggleButton
                      options={ttsModelModes}
                      selectedOption={ttsModelMode}
                      onSelect={(mode) => {
                        setTtsModelMode(mode);
                        setSelectedTtsModel(mode === 'api' ? 'reecho' : 'coquitts');
                      }}
                    />
                  </div>
                    <ToggleButton
                  <div>
                    <span className="model-select-label">TTS 模型</span>
                    <ToggleButton
                      options={ttsModelMode === 'api' ? ttsApiModels : ttsLocalModels}
                      selectedOption={selectedTtsModel}
                      onSelect={setSelectedTtsModel}
                    />
                  </div>
                </div>
              </>
            )}
                model={getModelEndpoint(selectedModel, modelMode === 'local')}
            {selectedOption === 'file' && 
              <FileUpload 
                model={getModelEndpoint(selectedModel, modelMode === 'local')}
                initialTtsModelMode={ttsModelMode}
                initialSelectedTtsModel={selectedTtsModel}
              />

3、哪些场景下会用到条件渲染?

  • 用户权限:根据用户的权限显示不同的内容。

  • 错误处理:在发生错误时显示错误信息。

  • 加载状态:在数据加载时显示加载动画,加载完成后显示内容。

React hooks

React Hooks 是 React 16.8 版本引入的新特性,它允许你在不编写类组件的情况下使用状态和其他 React 特性。Hooks 的出现极大地简化了函数组件的功能,使得代码更加简洁、易读。

1. 为什么需要 Hooks?

HooksReact 提供的一组函数,它们可以让你在函数组件中“钩入” React 的状态和生命周期特性。Hooks 的出现解决了以下几个问题:

  • 状态管理:在函数组件中使用状态。

  • 生命周期管理:在函数组件中使用生命周期方法。

  • 代码复用:通过自定义 Hooks 实现代码复用。

那么,在提到的AutoPodcast项目中,使用到了哪一些Hooks呢?项目中使用到的Hooks有:

1)、useState-用于在函数组件中管理状态。

举个例子, useState 返回一个数组,第一个元素是当前状态值,第二个元素是更新状态的函数。你可以多次调用 useState 来管理多个状态,在下方代码中,使用useState来管理点击次数的状态,初始点击次数为0,即初始状态为0,每点击一次,则为点击次数增加1:

import React, { useState } from 'react';

function Example() {
  // 声明一个名为 "count" 的状态变量,初始值为 0
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

2)、useRef-用于在函数组件中创建一个可变的引用,类似于类组件中的 this.ref

在项目代码中,使用useRef来创建一个音频元素的引用,并将其赋值给audioRef,在handlePlay函数中,通过audioRef.current来访问音频元素,并调用其play方法来播放音频:

const handlePlay = () => {
    if (audioRef.current) {
        audioRef.current.play();
    }
};

2、还有什么其他常见的Hooks?

1)、useEffect- 用于在函数组件中执行副作用操作,比如数据获取、订阅、手动修改 DOM 等。

useEffect 接收两个参数:第一个参数是一个函数,第二个参数是一个依赖数组。

如果依赖数组为空,useEffect 只会在组件挂载和卸载时执行。

如果依赖数组中有值,useEffect 会在这些值变化时执行。

import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  // 类似于 componentDidMount 和 componentDidUpdate
  useEffect(() => {
    // 更新文档标题
    document.title = `You clicked ${count} times`;
  }, [count]); // 仅在 count 变化时执行

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

2)、useContext 允许你使用 React 的 Context API,避免通过层层传递 props 来传递数据。

useContext 接收一个 Context 对象,并返回该 Context 的当前值。

当 Context 的值发生变化时,使用 useContext 的组件会重新渲染。

import React, { useContext } from 'react';

const ThemeContext = React.createContext('light');

function Example() {
  return (
    <ThemeContext.Provider value="dark">
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return (
    <button style={{ background: theme }}>
      I am styled by theme context!
    </button>
  );
}

3)、useCallbackuseMemo

useCallbackuseMemo 用于优化性能,避免不必要的重新渲染。这两个函数的功能有所不同:

  • useCallback 返回一个缓存的函数,避免在每次渲染时创建新的函数。

  • useMemo 返回一个缓存的值,避免在每次渲染时重新计算。

import React, { useState, useCallback, useMemo } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  // 使用 useCallback 缓存函数
  const increment = useCallback(() => {
    setCount(count + 1);
  }, [count]);

  // 使用 useMemo 缓存计算结果
  const doubleCount = useMemo(() => {
    return count * 2;
  }, [count]);

  return (
    <div>
      <p>Count: {count}</p>
      <p>Double Count: {doubleCount}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

4)、useReducer - 用于在函数组件中管理复杂的状态逻辑,它是useState的替代方案,适用于复杂的状态逻辑。

  • useReducer 接收一个 reducer 函数和一个初始状态,返回当前状态和一个 dispatch 函数。

  • dispatch 函数用于发送 action,reducer 函数根据 action 更新状态。

import React, { useReducer } from 'react';

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </>
  );
}

Props 传递

1、什么是 Props?

Props(Properties 的缩写)是 React 组件之间传递数据的一种方式。你可以将数据从一个组件传递到另一个组件,就像传递参数给函数一样。

2、如何传递 Props?

假设你有一个父组件 ParentComponent,它需要将数据传递给子组件 ChildComponent。你可以通过在父组件中定义 ChildComponent 时传递属性来实现。

而在本项目中,各个组件或多或少都用到了Props,例如:

  • Form组件中,接收topic、setTopic、format和setFormat作为props,

  • ToggleButton组件接收options、selectedOption和onSelect作为props,

  • FileUpload组件接收model、initialTtsModelMode和initialSelectedTtsModel作为props

  • 等等

3、为什么要在项目中使用到Props?

使用Props的优点是:

  • 状态提升:将状态提升到父组件中,使得多个子组件可以共享和操作相同的状态。

  • 单向数据流:数据从父组件流向子组件,使得数据流更加可预测和易于管理。

  • 关注点分离:让使用到的组件只负责渲染和用户交互,而不需要关心状态的存储和管理。

  • 可重用性:组件变得更加通用,可以在不同的场景下重用,只需传入不同的props。

这种模式使得组件之间的通信更加清晰,也使得状态管理更加集中和可控。

后端部分

FastAPI 后端开发:

  • 创建 API 路由和处理 HTTP 请求

  • 使用 Pydantic 进行数据验证

  • 异步编程和错误处理

1、什么是 FastAPI?

FastAPI 是一个基于 Python 3.7+ 的现代 Web 框架,旨在提供高性能的 API 开发体验。它结合了 Starlette(一个轻量级的 ASGI 框架)和 Pydantic(一个数据验证和设置管理库),使得开发者能够以类型安全的方式快速构建 API。

2、FastAPI有哪些特点?

  • 高性能:基于 ASGI(异步服务器网关接口),FastAPI 能够处理大量并发请求,性能接近 Node.js 和 Go。

  • 类型安全:通过 Pydantic,FastAPI 能够在运行时进行数据验证和序列化,减少错误并提高代码的可维护性。

  • 自动生成文档:FastAPI 能够自动生成交互式的 API 文档(基于 Swagger UI 和 ReDoc),方便开发者进行测试和调试。

聊聊FastAPI生成的API文档

在我对后端的开发过程中,我一般会先在API文档中进行测试,也就是后端启动时所给的文档链接(默认是127.0.0.1:8000/docs),在API文档中,可以看到每个API的请求方法、请求参数、请求体、返回值等信息,然后我会先在API文档中将各个API测试一遍,确保接口函数能够跑通,FastAPI的调试对于新手而言,还是比较方便的。

FastAPI的路由

在 FastAPI 中,路由是通过装饰器来定义的。FastAPI 提供了多个装饰器,对应不同的 HTTP 方法,如 @app.get()、@app.post()、@app.put()、@app.delete() 等。而在这个项目中,我用得最多的是@app.post(),用来定义POST请求的路由,并且根据模型和接口的不同,定义不同的请求路径,最后再main文件中直接对所有的路径进行导入,并且使用app.include_router来包含所有的路径。

from api.deepseek import deepseek_router
from api.OpenAI import openai_router
from api.Yi import yi_router

app.include_router(deepseek_router, prefix="/deepseek", tags=["deepseek"])
app.include_router(openai_router, prefix="/openai", tags=["openai"])
app.include_router(yi_router, prefix="/yi", tags=["yi"])

pydantic

Pydantic主要用于数据验证和设置管理。它用于定义请求和响应的模型,通过类型提示和运行时检查,确保数据在输入和输出时的一致性和有效性

Pydantic 的核心是模型(Model),它是一个继承自 BaseModel 的类。模型定义了数据的结构和验证规则。它在模型实例化时自动进行数据验证。如果数据不符合模型的定义,会抛出 ValidationError 异常,以此在后端开发过程中,我将其用于验证接口所收到的前端发送过来的数据,验证接收的数据的格式其是否符合预期所设定的后端能够处理的格式

除此之外,它还有一些高级功能,例如:字段约束,允许你为字段添加各种约束,如最小值、最大值、正则表达式等,还有嵌套模型,允许你定义复杂的数据结构,例如:

class Address(BaseModel):
    street: str
    city: str
    zip_code: str

class User(BaseModel):
    id: int
    name: str
    email: str
    address: Address

不仅如此,它还支持自定义验证、自定义行为等等,不过这些高级功能在我当前的开发过程中没有用到。

说到后端接收前端发送的数据,就不得不提前后端的交互,在项目中,我使用的是Fetch API来发送HTTP请求,Fetch API是现代浏览器中用于发送HTTP请求的标准方法,它返回一个Promise对象,可以方便地处理异步请求。

fetch(url, {
  method: 'POST',
  body: JSON.stringify(data)
})

而在处理文件上传时,在前端我使用的是FormData对象,FormData对象用于将表单数据编码为键值对,并可以方便地添加文件,后端则是用到了FastAPI的FileUploadFile。File 是一个用于定义文件字段的类,而 UploadFile 是一个用于处理上传文件的类。

File(...) 的参数可以用来验证上传文件的类型,不过我在前端已经做了一步文件类型的验证,所以在后端就没有对其进行过多的验证。

并且由于前端可以一次性发送多个文件,所以在后端我也用上了List[UploadFile]来定义多个文件字段,接收前端发送过来的文件。