在这篇文章中,我们将使用React和Firebase构建一个健身追踪器网络应用,这两种技术使我们能够高效率地开发网络应用。
这篇文章将使你能够自己用React和Firebase构建全栈应用。如果你知道React的基础知识,你应该可以胜任。否则,我建议先解决这些问题。
注意,你可以在这里找到完成的应用程序,在这里可以找到这个项目的源代码。
设置项目
让我们先用craco npm包覆盖一个新的Create React App设置。Tailwind需要craco包来覆盖默认的Create React App配置。
让我们也来设置路由。我们将给我们的路由一个额外的参数,叫做layout
,这样它们就可以用正确的布局来包装页面。
function RouteWrapper({ page: Page, layout: Layout, ...rest }) {
return (
<Route
{...rest}
render={(props) => (
<Layout {...props}>
<Page {...props} />
</Layout>
)}
/>
);
}
复制代码
我们将在本文的后面添加认证和其他路由。在App.js
,我们将返回我们的路由。让我们继续讨论用户会话。
用React Context认证用户会话
我们想知道一个用户是否在我们应用程序的任何地方通过了认证,而不需要通过多个组件传递这个信息。为了达到这个目的,我们将使用React的上下文API。我们将用认证上下文来包装我们的整个应用程序,这样我们就可以从我们应用程序的任何地方访问当前认证的用户。
首先,让我们创建一个认证上下文。
我们可以通过调用创建一个新的上下文。
const AuthContext = createContext()
复制代码
然后,我们将它提供给其他组件,像这样。
<AuthContext.Provider value={user}>{children}</AuthContext.Provider>
复制代码
在我们的例子中,我们想订阅已认证的Firebase用户。我们通过调用我们导出的Firebase auth函数的onAuthStateChanged()
方法来做到这一点。
auth.onAuthStateChanged(user => { … });
复制代码
这将给我们提供当前认证的用户。如果用户的状态发生变化,比如签入或签出,我们要相应地更新我们的上下文提供者。为了处理这种变化,我们将使用useEffect
钩子。
我们的AuthContext.jsx
,然后看起来像这样。
...
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((user) => {
setUser(user);
setLoading(false);
});
return unsubscribe;
}, []);
const value = {
user,
};
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
);
}
复制代码
我们可以调用useAuth()
钩子,从我们应用程序的任何地方返回当前的上下文值。我们现在提供的上下文值将在以后包含更多的功能,如登录和退出。
创建签入和签出表单
为了能够签入用户,我们需要使用一个叫做signInWithEmailAndPassword()
的方法,这个方法存在于我们在Firebase文件中导出的auth对象上。
我们可以不直接访问这个方法,而是把它添加到我们的AuthContext
提供者中,这样我们就可以很容易地把认证方法与认证用户结合起来。我们将把signIn()
函数添加到我们的AuthContext
提供者,像这样。
function signIn(email, password) {
return auth.signInWithEmailAndPassword(email, password);
}
const value = {
user,
signIn,
};
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
);
复制代码
在我们的登录页面上,我们现在可以用我们的useAuth()
钩子轻松访问signIn()
方法。
const { signIn } = useAuth();
复制代码
如果用户成功登录,我们将把他们重定向到仪表板,它位于主路由器路径上。为了检查这个,我们将使用一个try-catch块。
你现在应该收到一条错误信息,说没有找到用户,因为我们还没有签到。如果是这样,那太好了!这意味着我们的Firebase连接是正常的。这意味着我们的Firebase连接正在工作。
启用谷歌认证
首先,在Firebase控制台中启用Google认证。然后,在认证上下文中添加signInWithGoogle
函数。
function signInWithGoogle() {
return auth.signInWithPopup(googleProvider);
}
复制代码
接下来,我们将从我们的Firebase文件中导入googleProvider
。
export const googleProvider = new firebase.auth.GoogleAuthProvider();
复制代码
回到我们的登录页面,我们将添加以下代码使之工作。
const handleGoogleSignIn = async () => {
try {
setGoogleLoading(true);
await signInWithGoogle();
history.push("/");
} catch (error) {
setError(error.message);
}
setGoogleLoading(false);
};
复制代码
让我们继续建立我们的健身应用。
在健身应用中创建锻炼选项
选择一个运动
让我们创建一个实际的组件,叫做SelectExercise
。我们想在这个组件中完成两件事。首先,我们要呈现一个用户创建的、可以添加到他们的锻炼中的锻炼列表。第二,我们要让用户选择创建一个新的练习。
锻炼还原器用锻炼状态来包装我们的整个应用程序,这样我们就可以从我们应用程序的任何地方访问它。每次用户改变他们的锻炼时,localStorage
,以及所有订阅该状态的组件都会被更新。
我们将锻炼与调度提供者分开,因为有些组件只需要访问状态或调度。
const WorkoutStateContext = createContext();
const WorkoutDispatchContext = createContext();
export const useWorkoutState = () => useContext(WorkoutStateContext);
export const useWorkoutDispatch = () => useContext(WorkoutDispatchContext);
export const WorkoutProvider = ({ children }) => {
const [workoutState, dispatch] = useReducer(rootReducer, initializer);
// Persist workout state on workout update
useEffect(() => {
localStorage.setItem("workout", JSON.stringify(workoutState));
}, [workoutState]);
return (
<WorkoutStateContext.Provider value={workoutState}>
<WorkoutDispatchContext.Provider value={dispatch}>
{children}
</WorkoutDispatchContext.Provider>
</WorkoutStateContext.Provider>
);
};
复制代码
现在我们可以向我们的reducer调度动作了。这是一个来自Redux的概念–它只是意味着我们希望锻炼状态随着我们提供的值而改变。
我们的addExercise
函数看起来像这样。
const exercise = {
exerciseName,
sets: { [uuidv4()]: DEFAULT_SET },
};
dispatch({
type: "ADD_EXERCISE",
payload: { exerciseId: uuidv4(), exercise },
});
复制代码
它把ADD_EXERCISE
动作分派给我们的还原器,还原器会把给定的锻炼添加到我们的状态中。使用Immer,我们的还原器将看起来像这样。
export const rootReducer = produce((draft, { type, payload }) => {
switch (type) {
...
case ACTIONS.ADD_EXERCISE:
draft.exercises[payload.exerciseId] = payload.exercise;
break;
…
复制代码
对于我们的练习,我们将使用对象_的对象_,而不是使用一个对象_的_数组。
case ACTIONS.UPDATE_WEIGHT:
draft.exercises[payload.exerciseId].sets[payload.setId].weight =
payload.weight;
break;
复制代码
这比我们每次更新状态时过滤一个数组要有效得多,因为还原器确切地知道要更新哪个项目。
SelectExercise
还必须能够向数据库添加练习。因此,我们首先需要访问我们的Firestore数据库。
下面是保存一个新练习到数据库的功能。
const { user } = useAuth();
...
const saveExercise = async () => {
if (!exerciseName) {
return setError("Please fill in all fields");
}
setError("");
try {
await database.exercises.add({
exerciseName,
userId: user.uid,
createdAt: database.getCurrentTimestamp(),
});
toggleShowCreateExercise();
} catch (err) {
setError(err.message);
}
};
复制代码
我们还想检索存储在数据库中的用户所创建的练习列表。我们不想用这些练习来包装我们的整个应用程序,所以我们将把它保存在我们的SelectExercise
组件中。
为了检索数据库中的练习,我们不需要上下文API。为了便于学习,我们将创建一个自定义的钩子,使用useReducer
钩子来管理状态。这样一来,我们就有了高效的状态管理,以便在用户每次请求时都能检索到最新的练习列表。
function useWorkoutDb() {
const [workoutDbState, dispatch] = useReducer(reducer, initialState);
const { user } = useAuth();
useEffect(() => {
dispatch({ type: ACTIONS.FETCHING_EXERCISES });
return database.exercises
.where("userId", "==", user.uid)
.onSnapshot((snapshot) => {
dispatch({
type: ACTIONS.SET_EXERCISES,
payload: snapshot.docs.map(formatDocument),
});
});
}, [user]);
useEffect(() => {
dispatch({ type: ACTIONS.FETCHING_WORKOUTS });
return database.workouts
.where("userId", "==", user.uid)
.onSnapshot((snapshot) => {
dispatch({
type: ACTIONS.SET_WORKOUTS,
payload: snapshot.docs.map(formatDocument),
});
});
}, [user]);
return workoutDbState;
}
复制代码
你可能会注意到与我们其他useReducer
,我们使用对象的对象和Immer来突变状态的区别。
现在你应该能够添加一个练习并在列表中看到它们。棒极了!让我们继续讨论锻炼定时器的问题。
构建锻炼定时器
对于定时器,我们将创建一个自定义的钩子,叫做useTimer
。我们将每秒钟设置一个间隔,以更新secondsPassed
数字变量。我们的停止和暂停函数会清除间隔,重新从0
开始。每隔一秒,我们也会更新用户的localStorage
里面的时间,这样用户就可以刷新屏幕,并且仍然可以正常运行计时器。
function useTimer() {
const countRef = useRef();
const [isActive, setIsActive] = useState(false);
const [isPaused, setIsPaused] = useState(false);
const [secondsPassed, setSecondsPassed] = useState(
persist("get", "timer") || 0
);
useEffect(() => {
const persistedSeconds = persist("get", "timer");
if (persistedSeconds > 0) {
startTimer();
setSecondsPassed(persistedSeconds);
}
}, []);
useEffect(() => {
persist("set", "timer", secondsPassed);
}, [secondsPassed]);
const startTimer = () => {
setIsActive(true);
countRef.current = setInterval(() => {
setSecondsPassed((seconds) => seconds + 1);
}, 1000);
};
const stopTimer = () => {
setIsActive(false);
setIsPaused(false);
setSecondsPassed(0);
clearInterval(countRef.current);
};
const pauseTimer = () => {
setIsPaused(true);
clearInterval(countRef.current);
};
const resumeTimer = () => {
setIsPaused(false);
startTimer();
};
return {
secondsPassed,
isActive,
isPaused,
startTimer,
stopTimer,
pauseTimer,
resumeTimer,
};
}
复制代码
计时器现在应该开始工作了。让我们继续进行实际的锻炼方案。
在React中创建锻炼方案
在我们的应用程序中,我们希望用户能够。
- 添加和删除练习
- 添加和删除组数
- 对于每组,添加重量和次数
- 对于每一组,标记为完成或未完成
我们可以通过向我们之前制作的减速器派发动作来更新我们的锻炼。为了更新重量,我们将派发以下动作。
dispatch({
type: "UPDATE_WEIGHT",
payload: {
exerciseId,
setId,
newWeight,
},
});
复制代码
然后,我们的还原器将相应地更新状态。
case ACTIONS.UPDATE_WEIGHT:
draft.exercises[payload.exerciseId].sets[payload.setId].weight =
payload.weight;
复制代码
由于我们给了它exerciseId
和setId
,所以还原器知道要更新哪条记录。
<Button
icon="check"
variant={isFinished ? "primary" : "secondary"}
action={() =>
dispatch({
type: "TOGGLE_FINISHED",
payload: {
exerciseId,
setId,
},
})
}
/>
复制代码
创建仪表盘
仪表板由两个图表组成:总锻炼量和每天消耗的卡路里。我们还想显示今天、这周和这个月的锻炼总量和卡路里。
这意味着我们要从数据库中检索所有的锻炼,我们可以从我们的自定义useWorkoutDb()
钩子中获得。
const { isFetchingWorkouts, workouts } = useWorkoutDb();
复制代码
我们已经可以显示锻炼的总量了。
{isFetchingWorkouts ? 0: workouts.length}
复制代码
每天、每周和每月消耗的卡路里
如果锻炼有变化并且至少有一项锻炼,我们要重新计算卡路里。
useEffect(() => {
if (!isFetchingWorkouts && workouts.length) {
calcCalories();
}
}, [workouts])
复制代码
对于每个锻炼,我们将检查日期是否与今天、本周或本月相同。
const formattedDate = new Date(createdAt.seconds * 1000);
const day = format(formattedDate, "d");
复制代码
如果是,我们将相应地更新卡路里,将其与该锻炼的分钟数相乘。
const newCalories = CALORIES_PER_HOUR * (secondsPassed / 3600);
if (dayOfYear === day) {
setCalories((calories) => ({
...calories,
today: calories.today + newCalories,
}));
}
}
复制代码
锻炼图表
我们想要一个简单的线形图,X轴上是月份,Y轴上是卡路里数。对线下的区域进行样式设计也很好,所以我们将使用rechartsAreaChart
组件。我们简单地传递给它一个数据数组。
<AreaChart data={data}>
复制代码
接下来让我们对数据数组进行格式化。为了让recharts知道它需要使用x轴的月份,我们将在我们的AreaChart
内添加<XAxis dataKey="month" />
。
为了使其发挥作用,我们需要使用这种格式。
[{ month: "Feb", amount: 13 }, ...]
复制代码
我们想显示过去三个月的锻炼量,即使这些月份没有锻炼。因此,让我们使用date-fns
,用过去三个月的数据填充一个数组,并将金额设置为0
。
const [data, setData] = useState([]);
let lastMonths = [];
const addEmptyMonths = () => {
const today = new Date();
for (let i = 2; i >= 0; i--) {
const month = format(subMonths(today, i), "LLL");
lastMonths.push(month);
setData((data) => [...data, { month, amount: 0 }]);
}
};
复制代码
创建卡路里图表
对于卡路里图表,我们想显示过去一周每天的卡路里数量。与WorkoutChart
,我们用一个数组来填充我们的数据,即上周的天数,每天的卡路里0
。
let lastDays = [];
const addEmptyDays = () => {
const today = new Date();
for (let i = 6; i >= 0; i--) {
const day = format(subDays(today, i), "E");
lastDays.push(day);
setData((data) => [...data, { day, calories: 0 }]);
}
};
复制代码
对于每次锻炼,我们将检查它是否发生在过去七天内。如果是的话,我们将计算出该次锻炼所消耗的卡路里数,并将其加入我们的数据数组。
const addCaloriesPerDay = () => {
for (const { createdAt, secondsPassed } of workouts) {
const day = format(new Date(createdAt.seconds * 1000), "E");
const index = lastDays.indexOf(day);
if (index !== -1) {
const calories = CALORIES_PER_HOUR * (secondsPassed / 3600);
setData((data) => {
data[index].calories = data[index].calories + parseInt(calories);
return data;
});
}
}
};
复制代码
如果你保存了一个新的锻炼,你现在应该看到你的仪表板统计数据和图表正在更新。
祝贺你,你已经建立了你的React健身应用!谢谢你跟随这个教程。
你可以在这里找到完成的应用程序:fitlife-app.netlify.app。源代码可以在这里找到:github.com/sanderdebr/…。
The postCreate a fitness tracker with React and Firebaseappeared first onLogRocket Blog.