引言:当React遇上修仙
大家好,我是你们的前端道友!今天我们要一起修炼一个React问卷应用——没错,就是那种能测出你修仙等级的神奇问卷!想象一下,当用户答完最后一题,屏幕上金光一闪:"恭喜道友,您的修仙等级为元婴期!"——是不是很有画面感?
这个项目虽然看似简单,却蕴含了现代React开发的诸多精髓:从Redux状态管理到React Router路由控制,从TypeScript类型安全到移动端rem适配,每一个环节都是前端修炼路上的重要关卡。话不多说,让我们祭出VS Code,开始今天的修仙之旅!
🏗️ 项目初始化:搭建修仙道场
我们的修仙道场(项目)采用了最新的React技术栈,让我们先看看package.json中的核心依赖:
"dependencies": {
"@reduxjs/toolkit": "^2.8.2",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-redux": "^9.2.0",
"react-router-dom": "^7.6.3"
}
可以看到,我们使用了React 19、Redux Toolkit和React Router v7,这些都是当前React生态中的顶尖法宝。构建工具则选用了Vite,它的热更新速度快如闪电,让我们的开发效率事半功倍。
Vite配置:简单即美
Vite配置文件vite.config.ts非常简洁,只需要引入React插件即可:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
})
这种简洁的配置正是Vite的魅力所在,让我们可以专注于业务逻辑而非构建工具的配置。
📱 移动端适配:响应式大法
作为一个现代Web应用,移动端适配是必不可少的。我们的项目采用了rem适配方案,核心代码在src/lib/rem.js中:
// 根据用户屏幕尺寸来设置页面根字体大小
(function(win, doc) {
// 获取用户屏幕宽度
const screenWidth = window.screen.width
// 设置页面根字体大小
doc.documentElement.style.fontSize = screenWidth / 18.75 + 'px'
// 监听用户屏幕尺寸变化
win.addEventListener('resize', () => {
// 获取用户屏幕宽度
const screenWidth = window.screen.width
// 设置页面根字体大小
doc.documentElement.style.fontSize = screenWidth / 18.75 + 'px'
})
})(window, document)
这个适配方案的原理是将屏幕宽度平均分成18.75份,1rem就等于其中一份的宽度。例如,在375px宽度(iPhone SE的屏幕宽度)下,1rem = 375 / 18.75 = 20px。这种方案让我们可以使用相对单位rem来编写样式,实现不同屏幕尺寸的自适应。
全局样式则定义在src/index.css文件中,主要用于重置默认样式:
*{
margin: 0;
padding: ;
box-sizing: border-box;
}
ul li {
list-style: none;
}
🗺️ 路由设计:问卷之旅的地图
我们的问卷应用包含三个主要页面:首页、问卷页和结果页。路由配置在src/App.tsx中:
import Home from "./pages/home/Home"
import Question from "./pages/question/Index"
import Result from "./pages/result/Index"
import { BrowserRouter, Routes, Route } from 'react-router-dom'
export default function App() {
return (
<div>
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/question" element={<Question />} />
<Route path="/result" element={<Result />} />
</Routes>
</BrowserRouter>
</div>
)
}
这种路由结构清晰明了,用户从首页开始,进入问卷页答题,最后在结果页查看自己的修仙等级。
🧠 状态管理:Redux修炼秘籍
状态管理是这个问卷应用的核心,我们使用Redux Toolkit来管理问卷数据、用户答案和最终结果。状态定义在src/store/modules/questional.ts中:
initialState: {
questions: [],
answersId: [] as number[],
result: {
grade: 0,
desc: ''
}
}
我们定义了三个主要状态:
- questions:存储问卷题目数据
- answersId:存储用户选择的答案ID
- result:存储最终的修仙等级结果
Redux Slice:状态操作的艺术
Redux Toolkit的createSlice函数让我们可以轻松定义状态和操作:
const questional = createSlice({
name: 'questional',
initialState: {...},
reducers: {
setQuestions(state, action) {
state.questions = action.payload
},
setAnswersId(state, action:PayloadAction<number>) {
state.answersId.push(action.payload);
},
getResult(state) {
// 计算结果的逻辑
}
}
})
这里有个小细节:setAnswersId使用了TypeScript的PayloadAction泛型来指定action的类型,这就是TypeScript带来的类型安全保障。
结果计算:修仙等级评定
getResult函数是整个应用的灵魂所在,它根据用户的答案计算出对应的修仙等级:
getResult(state) {
const resultGrades = [
{ grade: 0, desc: '炼气都摸不着边' },
{ grade: 40, desc: '筑基门槛刚够着' },
{ grade: 60, desc: '金丹期算稳当了' },
{ grade: 80, desc: '元婴期显神通了' },
{ grade: 100, desc: '化神境独一档啊' }
];
// 计算答对的题目数量
const rightArray = [] as number[]
questions.forEach(item => {
const right = item.topic_answer?.find(answer => answer.is_standard_answer === 1)
if (right) {
rightArray.push(right.topic_answer_id)
}
});
const rightCount = rightArray.filter(id => answersId.includes(id)).length
const grade = parseFloat(((rightCount / total) * 100).toFixed(1))
// 找到最接近的等级描述
let closestResult = resultGrades[0];
for (const item of resultGrades) {
if (Math.abs(item.grade - grade) < Math.abs(closestResult.grade - grade)) {
closestResult = item;
}
}
state.result = {
grade: grade,
desc: closestResult.desc
};
}
这段代码的逻辑是:先找出所有正确答案,然后统计用户答对的数量,计算得分,最后根据得分匹配最接近的修仙等级描述。这种设计既直观又有趣,让用户的答题结果变得生动起来。
📝 问卷页面:核心业务组件
问卷页面是用户交互的主要场所,位于src/pages/question/Index.tsx。让我们来看看它的核心逻辑:
数据获取:从后端获取问卷
const getData = async () => {
const response = await fetch('https://xxxxxxxx/question-naire', {
method: 'GET',
})
const data = await response.json()
dispatch(setQuestions(data.questions)) // 存数据到仓库
setQues(data.questions) // 存给页面使用
}
useEffect(() => { // 组件初次加载
getData()
}, [])
在组件挂载时,我们通过fetch API从后端获取问卷数据,并将数据同时存入Redux和组件状态。这种双存储策略既方便了当前组件使用,也为结果页面提供了数据支持。
答案选择:用户交互逻辑
const selectAnswer = (item: Answer) => { // 选择答案
setSelectedAnswer(item)
setIsSelected(true)
}
const nextTopic = () => {
setIsSelected(false)
if (!isSelected) {
alert('请选择答案')
return
}
if (num === ques.length) { // 最后一题
if (selectedAnswer) {
dispatch(setAnswersId(selectedAnswer.topic_answer_id))
}
navigate('/result') // 跳转到结果页
} else {
if (selectedAnswer) {
dispatch(setAnswersId(selectedAnswer.topic_answer_id))
}
setNum(num + 1)
}
}
这段代码实现了答案选择和题目切换的逻辑,包括未选择答案的提示、答案存入Redux以及页面导航等功能。
进度条:直观的用户反馈
问卷页面还实现了一个简单但实用的进度条:
<div className="question-container-hd-progress">
<div className="question-container-hd-progress-bar" style={{ width: `${num / ques.length * 100}%` }}>
</div>
</div>
通过动态计算width样式,实现了进度条随答题进度变化的效果,给用户直观的反馈。
🎉 结果页面:修仙等级展示
结果页面位于src/pages/result/Index.tsx,它的主要功能是展示用户的修仙等级:
export default function Index() {
const dispatch = useDispatch()
const result = useSelector((state: RootState) => state.questional.result)
const navigate = useNavigate()
useEffect(() => {
dispatch(getResult())
}, [])
const handleRetry = () => {
navigate('/question')
}
return (
<div className='result-container'>
<div className="result-container-hd">测试结果</div>
<div className="result-container-bd">
<div className="result-container-bd-result">
<div className="result-container-bd-result-grade">得分:{result.grade}</div>
<div className="result-container-bd-result-desc">{result.desc}</div>
</div>
<div className="result-container-bd-share">
<img src={share} alt="分享" />
炫耀一下
</div>
</div>
<div className="result-container-ft">
<div className="retry-button" onClick={handleRetry}>
<img src={retry} alt="重试" />
再来一次
</div>
</div>
</div>
)
}
在组件挂载时,我们调用getResult action来计算结果,然后通过useSelector获取Redux中的result状态并展示。页面还提供了"再来一次"按钮,让用户可以重新答题。
页面展示:(题目内容无需在意😜)
🎨 样式设计:Sass的魔法
我们的项目使用Sass(SCSS语法)来编写样式,以问卷页面的样式为例:
.question-container{
height: 100vh;
background-color: #F0F7FF;
&-hd{
padding: 0.7rem;
background-color: #fff;
&-title{
font-size: 0.65rem;
color: #4B5563;
text-align: center;
}
// 更多嵌套样式...
}
// 更多区块样式...
}
使用Sass的嵌套语法,我们可以编写更具结构性和可读性的样式代码,&符号的使用让嵌套选择器更加直观。
🛡️ TypeScript:类型安全的护盾
作为一个现代化的React项目,TypeScript是必不可少的。我们定义了清晰的类型接口:
export interface Answer{
answer_name: string,
is_standard_answer: number,
topic_answer_id: number,
topic_id:number
}
export interface Ques {
topic_name?: string,
topic_answer?: object[]
}
这些接口定义了答案和问题的数据结构,让我们在开发过程中获得更好的IDE支持和类型检查。
TypeScript配置文件tsconfig.app.json中启用了严格模式:
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
这些严格的类型检查选项帮助我们在编译阶段就发现潜在的错误,提高代码质量。
🚀 项目优化:飞升之路
虽然我们的项目已经可以正常运行,但作为追求极致的前端道友,我们还可以从以下几个方面进行优化:
1. 错误处理:修仙路上的劫难
当前代码中缺少完善的错误处理,特别是在数据获取部分:
// 改进前
getData().then(...)
// 改进后
try {
const response = await fetch(url);
if (!response.ok) throw new Error('网络错误');
const data = await response.json();
} catch (error) {
console.error('获取数据失败:', error);
// 显示错误提示给用户
}
2. 加载状态:渡劫中的等待
添加加载状态可以提升用户体验:
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
try {
setLoading(true);
// 获取数据
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
3. 状态重置:重修一次
当前"再来一次"功能只是简单跳转,没有重置状态,这可能导致数据混乱:
// 在Redux中添加重置状态的action
resetState(state) {
state.answersId = [];
state.result = { grade: 0, desc: '' };
}
// 在结果页调用
const handleRetry = () => {
dispatch(resetState());
navigate('/question');
}
📝 总结:修仙有成
恭喜你,道友!通过本文,我们一起修炼了一个完整的React问卷应用,掌握了以下修仙秘籍:
- React 19 + TypeScript:构建现代化、类型安全的前端应用
- Redux Toolkit:简化状态管理,让数据流清晰可控
- React Router v7:实现页面间的无缝导航
- rem适配:打造完美适配各种移动设备的响应式界面
- Sass:编写结构化、可维护的CSS代码
- 异步数据获取:与后端API交互的最佳实践
这个看似简单的问卷应用,其实包含了现代前端开发的方方面面。希望本文能帮助你在React的修仙之路上更进一步!
附录:项目结构
├── src/
│ ├── App.tsx # 应用入口组件
│ ├── main.tsx # 渲染入口
│ ├── index.css # 全局样式
│ ├── lib/rem.js # rem适配
│ ├── assets/ # 静态资源
│ ├── pages/ # 页面组件
│ │ ├── home/ # 首页
│ │ ├── question/ # 问卷页
│ │ └── result/ # 结果页
│ └── store/ # Redux状态管理
│ ├── index.ts # 仓库入口
│ └── modules/ # 子模块