🚀 从零到一:用React打造一个修仙主题的问卷调查应用

162 阅读8分钟

引言:当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状态并展示。页面还提供了"再来一次"按钮,让用户可以重新答题。

页面展示:(题目内容无需在意😜)

react-questionnaire.gif

🎨 样式设计: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问卷应用,掌握了以下修仙秘籍:

  1. React 19 + TypeScript:构建现代化、类型安全的前端应用
  2. Redux Toolkit:简化状态管理,让数据流清晰可控
  3. React Router v7:实现页面间的无缝导航
  4. rem适配:打造完美适配各种移动设备的响应式界面
  5. Sass:编写结构化、可维护的CSS代码
  6. 异步数据获取:与后端API交互的最佳实践

这个看似简单的问卷应用,其实包含了现代前端开发的方方面面。希望本文能帮助你在React的修仙之路上更进一步!


附录:项目结构

├── src/
│   ├── App.tsx          # 应用入口组件
│   ├── main.tsx         # 渲染入口
│   ├── index.css        # 全局样式
│   ├── lib/rem.js       # rem适配
│   ├── assets/          # 静态资源
│   ├── pages/           # 页面组件
│   │   ├── home/        # 首页
│   │   ├── question/    # 问卷页
│   │   └── result/      # 结果页
│   └── store/           # Redux状态管理
│       ├── index.ts     # 仓库入口
│       └── modules/     # 子模块