React Todo 应用程序通常是非常基础的——事实上,如果你是 React 初学者,并且想要提升你的技能,构建这类项目是一个很好的练习方式。

但是,你是否曾经构建过一个 Todo 应用程序,用户可以在其中使用语音命令添加待办事项。这使它变得更加复杂和令人兴奋。

这就是我们在本教程中要做的。为了构建这个基于语音的 Todo 应用程序,我们将使用三个主要工具:

  • React - 用于用户界面
  • Firebase – 用于数据库
  • Alan AI – 用于执行语音命令

那么,让我们开始吧。

如何使用 React 创建 Todo 应用 UI

让我们首先创建一个 React 应用程序。只需输入以下命令:

npx create-react-app react-todo-alan-firebase

它将像这样初始化并创建我们的 React 应用程序。然后,我们将导航到该文件夹,并使用 npm start 启动应用程序。

现在让我们创建一个名为 components 的文件夹。它将包含我们的主要组件 Todo.js。 如下创建一个 Todo.js 文件。

import React from 'react'

export default function Todo() {
    return (
        <div>
            
        </div>
    )
}
Todo.js

为应用程序提供一个标题(或名称),例如基于语音的 Todo 应用程序或你选择的任何内容。

import React from 'react'

export default function Todo() {
    return (
        <div>
            <h2>Voice-based Todo Application</h2>
        </div>
    )
}

然后,将此组件导入你的 App.js 文件。

import './App.css';
import Todo from './components/Todo';

function App() {
  return (
    <div>
      <Todo />
    </div>
  );
}

export default App;
App.js

让我们把标题居中。因此,在 Todo.js 组件中为 h2 指定一个 header 的 classname。

import React from 'react'

export default function Todo() {
    return (
        <div>
            <h2 className="header">Voice-based Todo Application</h2>
        </div>
    )
}

然后我们将在 App.css 文件中添加一些样式,将标题居中。

.header{
  text-align: center;
}
Screenshot-2021-11-15-215217

你将在屏幕上看到上面的输出,标题居中。

现在,让我们创建一张包含待办事项的卡片。

import React from 'react'

export default function Todo() {
    return (
        <div className="todo-main">
            <h2 className="header">Voice-based Todo Application</h2>

            <div className="todo-card">

            </div>
        </div>
    )
}

创建一个 div 并使 classNametodo-card。你将看到最上级 div 的 classNametodo-main。这是因为我们需要一切内容都居中。

.todo-main {
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
}

.header {
  text-align: center;
}

.todo-card {
  border: 1px dashed #1f133d;
  height: 50vh;
  width: 50vh;
  border-radius: 20px;
}

将上述样式添加到 App.css 中。现在看起来像这样:

Screenshot-2021-11-15-220125

现在让我们添加列表。

import React from 'react'

export default function Todo() {
    return (
        <div className="todo-main">
            <h2 className="header">Voice-based Todo Application</h2>

            <div className="todo-card">
                <div className="todo-list">
                    <h3>
                        Wash the Clothes
                    </h3>
                </div>
                <div className="todo-list">
                    <h3>
                        Cook the Dinner
                    </h3>
                </div>
                <div className="todo-list">
                    <h3>
                        Code some software
                    </h3>
                </div>
            </div>
        </div>
    )
}

所以,我创建了一个 div,它包含 h3 标签中的项目。这些文本目前是静态的,但我们很快也会创建来自 Firebase 数据库的动态文本。

这是我们更新的样式:

.todo-main {
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
}

.header {
  text-align: center;
}

.todo-card {
  border: 1px dashed #1f133d;
  height: 50vh;
  width: 50vh;
  border-radius: 20px;
}

.todo-list{
  text-align: center;
}
Screenshot-2021-11-15-220710

这就是现在的输出,我们的列表中有三个项目。

现在,让我们添加一个关闭图标,它会在我们完成后删除每个项目。

为了添加图标,我们需要一个图标包。因此,让我们使用以下命令安装 React 图标:

npm install react-icons --save

安装后,从左侧边栏中选择任何图标包。

Screenshot-2021-11-15-220805

我正在使用 Feather Icons,所以我将导入它。

首先,让我们使用以下命令导入该包:

import { FiX } from "react-icons/fi";

然后,在 h3 标签后调用它。

import React from 'react'
import { FiX } from "react-icons/fi";
export default function Todo() {
    return (
        <div className="todo-main">
            <h2 className="header">Voice-based Todo Application</h2>

            <div className="todo-card">
                <div className="todo-list">
                    <h3>
                        Wash the Clothes
                    </h3>
                    <FiX />
                </div>
                <div className="todo-list">
                    <h3>
                        Cook the Dinner
                    </h3>
                    <FiX />
                </div>
                <div className="todo-list">
                    <h3>
                        Code some software
                    </h3>
                    <FiX />
                </div>
            </div>
        </div>
    )
}
Screenshot-2021-11-15-221145

上面,这就是我们的应用程序现在的样子。但是我们希望关闭图标和待办事项在同一行。

为 Icon 指定一个 className 为 close-icon

 <FiX className="close-icon" />

并在 App.css 中,添加以下样式:

.todo-list {
  display: flex;
  align-items: center;
  justify-content: center;
}

.close-icon {
  margin-left: 10px;
}

到目前为止,我们的 Todo.js 组件将具有以下最终代码:

import React from 'react'
import { FiX } from "react-icons/fi";
export default function Todo() {
    return (
        <div className="todo-main">
            <h2 className="header">Voice-based Todo Application</h2>

            <div className="todo-card">
                <div className="todo-list">
                    <h3>
                        Wash the Clothes
                    </h3>
                    <FiX className="close-icon" />
                </div>
                <div className="todo-list">
                    <h3>
                        Cook the Dinner
                    </h3>
                    <FiX className="close-icon" />
                </div>
                <div className="todo-list">
                    <h3>
                        Code some software
                    </h3>
                    <FiX className="close-icon" />
                </div>
            </div>
        </div>
    )
}

App.css 是这样的:

.todo-main {
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
}

.header {
  text-align: center;
}

.todo-card {
  border: 1px dashed #1f133d;
  height: 50vh;
  width: 50vh;
  border-radius: 20px;
}

.todo-list {
  display: flex;
  align-items: center;
  justify-content: center;
}

.close-icon {
  margin-left: 10px;
}

UI 看起来是这样:

Screenshot-2021-11-15-221826

如何把 Alan AI 添加到我们的 React 项目

访问 https://alan.app/,创建你的账号。

登录后,你可以创建一个项目。只需单击加号按钮。

Screenshot-2021-11-15-222106

但是在使用之前,我们需要先安装 Alan AI 包。因此,请访问 https://alan.app/docs/client-api/web/react,获取 React 文档。

使用以下命令安装 Alan Al:

npm install @alan-ai/alan-sdk-web --save

现在,让我们在主 App.js 文件中导入 Alan。

import alanBtn from "@alan-ai/alan-sdk-web";

然后,我们需要创建一个 useEffect Hook。每当我们的组件被挂载或加载时,它就会启动我们的 Alan 服务。

useEffect(() => {
    alanBtn({
        key: 'YOUR_KEY_FROM_ALAN_STUDIO_HERE',
        onCommand: (commandData) => {
            if (commandData.command === 'go:back') {
                // Call the client code that will react to the received command
            }
        }
    });
}, []);

这个 alanBtn 需要一个我们需要得到的密钥。因此,在你在 Alan 中创建的项目中,你应该会在顶部栏中看到一个 “Integrations” 按钮。 单击该按钮。

Screenshot-2021-11-15-222645

在那里你会得到你的密钥。

将该密钥粘贴到 React 应用程序中的 alanBtn 中,如下所示:

import './App.css';
import Todo from './components/Todo';
import alanBtn from "@alan-ai/alan-sdk-web";
import { useEffect } from 'react'
function App() {
  useEffect(() => {
    alanBtn({
      key: '86e866fbe49666abd385ee5c9f9cbf5c2e956eca572e1d8b807a3e2338fdd0dc/stage',
      onCommand: (commandData) => {

      }
    });
  }, []);
  return (
    <div>
      <Todo />
    </div>
  );
}

export default App;

现在,检查输出,你将看到一个麦克风按钮。

Screenshot-2021-11-15-222913

现在,我们需要在我们的 Alan 应用程序中创建一个 Intent。它将以添加命令开始,例如添加洗涤衣服、添加编写一些代码等。因此,让我们为此编写代码:

intent('Add $(item* (.*))', (p) => {
    if(p.item.value){
        p.play({ command: 'todoApp', data: p.item.value });
        p.play(`${p.item.value} added`);
    }
    else{
        p.play(`Cannot add Empty Item`);
    }
})

它还会将项目返回给我们,我们可以在 React 应用程序中看到。在这里,我们还有一个阻止我们添加任何空项目的检查。如果我们尝试,它会回复“无法添加空项目”。

现在,我们希望将语音项接收回我们的 React 应用程序。

import './App.css';
import Todo from './components/Todo';
import alanBtn from "@alan-ai/alan-sdk-web";
import { useEffect } from 'react'
function App() {
  useEffect(() => {
    alanBtn({
      key: '86e866fbe49666abd385ee5c9f9cbf5c2e956eca572e1d8b807a3e2338fdd0dc/stage',
      onCommand: (commandData) => {
        console.log(commandData)
      }
    });
  }, []);
  return (
    <div>
      <Todo />
    </div>
  );
}

export default App;

只需 console.log commandData,你将得到以下结果。不要忘记单击麦克风按钮并说点什么。你将在控制台中看到你所说的内容。

Screenshot-2021-11-15-223428

好了,我们的 Alan AI 现已准备就绪。

如何使用 Firebase 将数据发送到 Firestore 数据库

我们现在将这些数据发送到 Firebase。

但首先,我们需要安装它。访问 https://firebase.google.com/,并在那里创建一个项目。

要安装 Firebase,只需键入 npm install firebase

然后,在项目中创建一个应用程序,如下所示:

Screenshot-2021-11-15-223909

它会给我们一些配置数据。只需在 src 文件夹中创建一个文件,将其命名为 firebase-config.js,然后添加这些配置数据。

// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";

// Your web app's Firebase configuration
const firebaseConfig = {
    apiKey: "AIzaSyCP8qL8z9BorGF3NZJsGb4vSaWHYyCVfc8",
    authDomain: "todo-firebase-alan.firebaseapp.com",
    projectId: "todo-firebase-alan",
    storageBucket: "todo-firebase-alan.appspot.com",
    messagingSenderId: "892581913000",
    appId: "1:892581913000:web:dbe08ac753c3adaab87d9d"
};

// Initialize Firebase
export const app = initializeApp(firebaseConfig);

不要忘记导出 const 应用程序。

接下来,我们还需要访问 Firestore。所以,让我们将它导入到我们的 firebase-config.js 文件中。

import { getFirestore } from 'firebase/firestore'
export const database = getFirestore(app);

然后,在底部导出它。

// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";
import { getFirestore } from 'firebase/firestore'

// Your web app's Firebase configuration
const firebaseConfig = {
    apiKey: "AIzaSyCP8qL8z9BorGF3NZJsGb4vSaWHYyCVfc8",
    authDomain: "todo-firebase-alan.firebaseapp.com",
    projectId: "todo-firebase-alan",
    storageBucket: "todo-firebase-alan.appspot.com",
    messagingSenderId: "892581913000",
    appId: "1:892581913000:web:dbe08ac753c3adaab87d9d"
};

// Initialize Firebase
export const app = initializeApp(firebaseConfig);
export const database = getFirestore(app);

这是整个 firebase-config 代码。

现在,在 App.js 中,我们需要导入这个应用程序和数据库。

import { app, database } from './firebase-config';

接下来,我们需要创建与 Firebase Firestore 的连接。为此,我们需要 Firebase Firestore 的集合属性。此外,我们将导入 addDoc,我们将使用它向 Firestore 添加数据。

import { collection, addDoc } from 'firebase/firestore';

现在,让我们创建与数据库的连接。

创建一个变量,并将我们从 firebase-config 文件导入的数据库以及我们想要给集合的名称放入数据库中。由于我们希望集合是待办事项列表,所以可以添加以下内容:

const databaseRef = collection(database, 'todo-list');

要添加数据,我们需要 addDoc 属性。

addDoc 属性将采用两个参数。第一个是我们创建的连接,databaseRef。第二个是我们要添加的数据,作为一个对象。

将 addDoc 放入 useEffect Hook 中,如下所示:

useEffect(() => {
    alanBtn({
      key: '86e866fbe49666abd385ee5c9f9cbf5c2e956eca572e1d8b807a3e2338fdd0dc/stage',
      onCommand: (commandData) => {
        addDoc(databaseRef, { item: commandData.data })
      }
    });
  }, []);

目前,我们的 Firestore 是空的。

Screenshot-2021-11-15-225805

现在,让我们试试这个。从 add 命令开始对着麦克风说话,它会在 Firebase Firestore 中可见。

Screenshot-2021-11-15-225922

可以看到,我们所说的现在已添加到 Firebase 中。

现在,让我们尝试读取并显示这些数据。

databaseRef 作为 prop 传递给 Todo 组件。

<Todo databaseRef={databaseRef}/>

然后在 Todo 组件中接收它。

export default function Todo({databaseRef})

在 Todo.js 组件中创建一个 useEffect 钩子,并在 useEffect 钩子中创建函数 getData

useEffect(() => {
        const getData = async () => {
            
        }
        getData()
    }, [])

我们将使用 getDocs 属性从 Firebase Firestore 读取数据。我们还需要连接 databaseRef,之前将它作为 prop 传递。

let data = await getDocs(databaseRef);
const getData = async () => {
  let data = await getDocs(databaseRef);
  console.log(data.docs.map((item) => ({ ...item.data(), id: item.id })));
}

我们映射传入的数据以使其更具可读性,还添加了项目的唯一 ID,应用程序稍后将用它删除该项目。

现在让我们检查控制台:

Screenshot-2021-11-15-231500

我们就要完成项目啦。

现在,让我们将这些数据存储在 state 中,在 React 应用程序中显示。

导入 useState Hook,然后像这样创建一个数组状态:

 const [todoList, setTodoList] = useState([]);

并使用 setTodoList 函数设置数据:

setTodoList(data.docs.map((item) => ({ ...item.data(), id: item.id })));

现在,让我们映射 todoList 数组。

<div className="todo-main">
            <h2 className="header">Voice-based Todo Application</h2>

            <div className="todo-card">
                {todoList.map((todo) => {
                    return (
                        <div className="todo-list">
                            <h3>
                                {todo.item}
                            </h3>
                            <FiX className="close-icon" />
                        </div>
                    )
                })}
            </div>
        </div>

我们将在 React UI 中看到我们的数据,它看起来像这样:

Screenshot-2021-11-15-231945

现在,让我们更新它,使每个 Todo 都以大写字母开头。

设置 h3 标签的 classNametodo-items

<h3 className="todo-item">
 {todo.item}
</h3>

在 App.css 中,添加以下样式:

.todo-item{
  text-transform: capitalize;
}

你会看到每个 Todo 现在都大写了。

Screenshot-2021-11-15-232149

现在,如果我们通过语音命令添加任何内容,在 React 中的列表应该更新。因此,让我们将 useEffect 配置为每次我们对应用程序说话时运行。

在 App.js 文件中,创建一个状态。它将是一个布尔值,初始状态为 false。

const [update, setUpdate] = useState(false)

当我们说些什么,或者 App.js 中的 useEffect 运行时,这个状态会变成 true。

useEffect(() => {
    alanBtn({
      key: '86e866fbe49666abd385ee5c9f9cbf5c2e956eca572e1d8b807a3e2338fdd0dc/stage',
      onCommand: (commandData) => {
        addDoc(databaseRef, { item: commandData.data })
        .then(() => {
          setUpdate(true);
        })
      }
    });
  }, []);

然后,我们将在 Todo.js 中传递更新状态和更新状态的函数。

<Todo databaseRef={databaseRef} update={update} setUpdate={setUpdate}/>

并在 Todo 组件中接收这两个。

export default function Todo({ databaseRef, update, setUpdate })

然后,在 Todo.js 的 useEffect 中,一旦它从 Firebase Firestore 获取我们的数据,就使用 setUpdate 函数将更新设置为 false,然后将更新状态放入依赖数组中。

useEffect(() => {
        const getData = async () => {
            let data = await getDocs(databaseRef);
            setTodoList(data.docs.map((item) => ({ ...item.data(), id: item.id })));
        }
        getData()
        setUpdate(false)
    }, [update])

这可能有点令人困惑,让我来解释一下。

当我们说话时,更新状态从 false 变为 true。然后,当完成从 Firestore 获取数据时,它会从 true 更改为 false。这样,状态就会不断变化。 所以每次更新状态改变时 useEffect 都会更新。

让我们现在试试看。说点什么,列表会动态更新。

Screenshot-2021-11-15-233617

现在,让我们添加删除函数以从 Firebase Firestore 和我们的 React 应用程序中删除项目。

创建一个名为 deleteItems 的函数。

const deleteItems = () => {
        
}

并将此函数绑定到 close 图标,如下所示:

<FiX className="close-icon" onClick={() => deleteItems()}/>

当我们点击一个特定的关闭图标时,我们需要将该项目的 id 传递给函数,以删除该项目。

<FiX className="close-icon" onClick={() => deleteItems(todo.id)}/>

并在函数中接收它。

让我们尝试 console.log 我们的 id:

const deleteItems = (id) => {
  console.log(id)
}
Screenshot-2021-11-15-234631

我们将在控制台中获得该特定 id。

现在,在删除任何待办事项之前,我们需要指定要删除的待办事项。因此,我们将使用此 id 创建一个引用。我们将使用 Firestore 的 doc 属性。

因此,首先使用以下命令从 Firestore 导入文档:

import { getDocs, doc } from 'firebase/firestore';

然后,在函数 1deleteItems1 中,添加以下代码:

const data = doc(database, 'todo-list', id)

该文档具有三个参数——数据库、集合名称和 id。我们有这三样东西。

数据库已从 firebase-config 导入。待办事项列表是 Firestore 集合的名称,以及我们从关闭按钮点击中获得的 id。

要删除一个项目,我们需要来自 Firestore 的另一个名为 deleteDoc 的属性。

import { getDocs, doc, deleteDoc } from 'firebase/firestore';

然后,只需添加以下内容:

const deleteItems = (id) => {
   const data = doc(database, 'todo-list', id);
   deleteDoc(data)
}

立即尝试——单击关闭图标,然后检查 Firestore。该项目将被删除。

但是我们在添加和读取操作期间遇到了同样的问题:在我们删除一个项目后,React 应用程序没有得到更新。

因此,首先要做的是将 getData 函数移到 useEffect Hook 之外。别担心,它仍然有效。

const getData = async () => {
   let data = await getDocs(databaseRef);
   setTodoList(data.docs.map((item) => ({ ...item.data(), id: item.id })));
}

useEffect(() => {   
   getData()
   setUpdate(false)
}, [update])

而在 deleteDoc 函数中,我们需要再次调用 getData 函数,在用户删除一个 item 后获取更新的数据。

const deleteItems = (id) => {
    const data = doc(database, 'todo-list', id);
    deleteDoc(data)
    .then(() => {
       getData()
    })
}

这是 Todo.js 的全部代码:

import React, { useEffect, useState } from 'react'
import { FiX } from "react-icons/fi";
import { database } from '../firebase-config';
import { getDocs, doc, deleteDoc } from 'firebase/firestore';
export default function Todo({ databaseRef, update, setUpdate }) {
    const [todoList, setTodoList] = useState([]);
    const getData = async () => {
        let data = await getDocs(databaseRef);
        setTodoList(data.docs.map((item) => ({ ...item.data(), id: item.id })));
    }
    useEffect(() => {
        getData()
        setUpdate(false)
    }, [update])

    const deleteItems = (id) => {
        const data = doc(database, 'todo-list', id);
        deleteDoc(data)
            .then(() => {
                getData()
            })
    }

    return (
        <div className="todo-main">
            <h2 className="header">Voice-based Todo Application</h2>

            <div className="todo-card">
                {todoList.map((todo) => {
                    return (
                        <div className="todo-list">
                            <h3 className="todo-item">
                                {todo.item}
                            </h3>
                            <FiX className="close-icon" onClick={() => deleteItems(todo.id)} />
                        </div>
                    )
                })}
            </div>
        </div>
    )
}

这是 App.js 的代码:

import './App.css';
import Todo from './components/Todo';
import alanBtn from "@alan-ai/alan-sdk-web";
import { useEffect, useState } from 'react';
import { app, database } from './firebase-config';
import { addDoc, collection } from '@firebase/firestore';
function App() {
  const databaseRef = collection(database, 'todo-list');
  const [update, setUpdate] = useState(false)
  useEffect(() => {
    alanBtn({
      key: '86e866fbe49666abd385ee5c9f9cbf5c2e956eca572e1d8b807a3e2338fdd0dc/stage',
      onCommand: (commandData) => {
        addDoc(databaseRef, { item: commandData.data })
          .then(() => {
            setUpdate(true);
          })
      }
    });
  }, []);
  return (
    <div>
      <Todo databaseRef={databaseRef} update={update} setUpdate={setUpdate} />
    </div>
  );
}

export default App;

现在,我们可以使用语音命令添加项目,并将其存储在我们的 Firebase 数据库中。它也会出现在我们的 React 应用程序中。

还有最后一件事要做。在我们的 App.css 文件中,我们需要将卡片的高度设置为 auto,以防止文本溢出。

.todo-card {
  border: 1px dashed #1f133d;
  height: auto;
  width: 50vh;
  border-radius: 20px;
}

这只是一个简单的 UI 设计。如果需要,你可以设计自己喜欢的。来吧,构建这个很棒的应用程序。

你可以在我的 YouTube 频道查看视频 Let's build a Voice-Based Todo Application using React, Firebase, and Alan AI

谢谢阅读,祝你学习愉快!

原文:How to Build a Voice-Based Todo App using React, Firebase, and Alan AI,作者:Nishant Kumar