本文记录了本犬在构建一个移动端React应用框架过程中的学习心得,重点分享了路由管理、布局设计和移动端适配的核心实现方案,之后会继续跟进项目,分享一些项目亮点,技能点
项目概述
最近我正在开发一个移动端旅游应用,核心功能包括首页、特惠专区、收藏、行程和个人账户等模块。项目基于Vite构建,使用React作为前端框架,并采用了React Router进行路由管理。特别值得一提的是,我选择React Vant作为UI组件库,它提供了丰富的移动端组件,极大提升了开发效率。
路由架构设计
双层布局方案
在路由设计上,我采用了双层布局策略,根据页面需求灵活切换不同的布局容器:
import {
lazy,
Suspense
} from 'react'
import {
Routes,
Route,
Navigate
} from 'react-router-dom'
import MainLayout from '@/components/MainLayout'
import BlankLayout from '@/components/BlankLayout'
// 页面组件懒加载
const Home = lazy(() => import('@/pages/Home'))
const Search = lazy(() => import('@/pages/Search'))
const Discount = lazy(() => import('@/pages/Discount'))
const Collection = lazy(() => import('@/pages/Collection'))
const Trip = lazy(() => import('@/pages/Trip'))
const Acount = lazy(() => import('@/pages/Acount'))
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
{/* 带有Tabbar的主布局 */}
<Route path='/' element={<MainLayout />}>
<Route path='/' element={<Navigate to='/home' />} />
<Route path='/home' element={<Home />} />
<Route path='/discount' element={<Discount />} />
<Route path='/collection' element={<Collection />} />
<Route path='/trip' element={<Trip />} />
<Route path='/acount' element={<Acount />} />
</Route>
{/* 空白布局(无Tabbar) */}
<Route path='/' element={<BlankLayout />}>
<Route path='/search' element={<Search />} />
</Route>
</Routes>
</Suspense>
)
}
这种设计的优势在于:
- 布局复用:共享的UI元素(如底部导航栏)只需在一个地方维护
- 灵活扩展:新增页面时只需选择合适的布局容器
- 代码解耦:布局逻辑与页面内容分离,便于维护
布局组件实现
主布局(带底部导航栏)
import { useEffect, useState } from 'react';
import { Tabbar } from 'react-vant';
import { HomeO, Search, FriendsO, SettingO, UserO } from '@react-vant/icons';
import { Outlet, useNavigate, useLocation } from 'react-router-dom'
const tabs = [
{ icon: <HomeO />, title: '首页', path: '/home' },
{ icon: <Search />, title: '特惠专区', path: '/discount' },
{ icon: <FriendsO />, title: '我的收藏', path: '/collection' },
{ icon: <SettingO />, title: '行程', path: '/trip' },
{ icon: <UserO />, title: '我的账户', path: '/acount' }
]
const MainLayout = () => {
const [active, setActive] = useState(0)
const navigate = useNavigate();
const location = useLocation();
useEffect(() => {
// 根据当前路径高亮对应的Tab
const index = tabs.findIndex(tab =>
location.pathname.startsWith(tab.path)
)
setActive(index)
}, [location.pathname])
return (
<>
<Outlet />
<Tabbar value={active} onChange={(key) => {
setActive(key);
navigate(tabs[key].path)
}}>
{tabs.map((tab, index) => (
<Tabbar.Item key={index} icon={tab.icon}>
{tab.title}
</Tabbar.Item>
))}
</Tabbar>
</>
)
}
在这个组件中,我使用了几个关键技术点:
- 动态Tab高亮:通过
useLocation获取当前路径,使用findIndex和startsWith方法确定激活的Tab项 - 路由跳转:点击Tab时使用
useNavigate进行路由切换 - Outlet组件:作为子路由的渲染出口
使用useLocation + findIndex和startsWith方法是为了防止刷新后,状态丢失,我们利用es6新特性,相当于炫技了,就是就和location.pathname===tab.path效果一致,可以看到效果,刷新后页面高亮不再回到默认的第一个tab栏,这里的tabbar组件是利用的react-van这个第三方组件库,图标也是用的@react-vant/icons,非常方便,不需要我们去做额外的工作,这是 react-van的操作文档
空白布局(无导航栏)
import { Outlet } from "react-router-dom";
const BlankLayout = () => {
return (
<>
<Outlet />
</>
)
}
空白布局非常简洁,仅作为内容容器使用,适合不需要底部导航的页面(如搜索页)。现在只是搭建了一个框架,可以实现路由的跳转,具体的页面内容还得后续实现
页面组件实现
页面组件保持简洁,专注于内容呈现:
// Home.jsx
import useTitle from "@/hooks/useTitle";
const Home = () => {
useTitle('奶龙首页'); // 自定义Hook设置页面标题
return <>Home</>
}
export default Home;
// Discount.jsx
const Discount = () => {
return <>特惠专区</>
}
export default Discount;
// Collection.jsx
const Collection = () => {
return <>我的收藏</>
}
export default Collection;
// Trip.jsx
const Trip = () => {
return <>行程</>
}
export default Trip;
// Acount.jsx
const Acount = () => {
return <>我的账户</>
}
export default Acount;
// Search.jsx
const Search = () => {
return <>搜索</>
}
export default Search;
还是上述说的,现在是项目刚刚开始,页面还待完善,现在只是实现了路由的成功跳转,页面并没有什么内容
移动端适配方案
移动端适配是项目的重要环节,我采用了阿里成熟的lib-flexible方案:
// main.jsx
import { createRoot } from 'react-dom/client'
import 'lib-flexible' // 引入移动端适配方案
import './index.css'
import App from './App.jsx'
import { BrowserRouter as Router } from 'react-router-dom'
createRoot(document.getElementById('root')).render(
<Router>
<App />
</Router>
)
适配原理详解
lib-flexible的核心思想是动态设置根元素字体大小,使页面元素能够根据屏幕尺寸自适应:
-
基准设置:1rem = 屏幕宽度 / 10
- 在750px宽的设计稿上,1rem = 75px
- 在375px宽的设备上,1rem = 37.5px
-
设计稿换算:
- 设计稿尺寸:750px(iPhone标准尺寸)
- 设计稿元素宽度260px → 实际开发中应设置为:260 / 75 = 3.4667rem
自动化转换方案
为了避免繁琐的手动计算,我们可以在Vite中配置PostCSS插件自动完成px到rem的转换:
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
plugins: [react()],
resolve: {
alias: { '@': path.resolve(__dirname, './src') }
},
css: {
postcss: {
plugins: [
require('postcss-pxtorem')({
rootValue: 75, // 设计稿宽度/10
propList: ['*'], // 转换所有属性
selectorBlackList: ['.norem'] // 忽略带有norem类的元素
})
]
}
}
})
这样配置后,在CSS中直接使用设计稿的像素值,构建时会自动转换为rem单位:
/* 设计稿尺寸:260px */
.element {
width: 260px; /* 构建后自动转换为 260/75 = 3.4667rem */
}
我们只需要按照设计稿的要求来写,看到什么写什么,不需要自己去频繁地转换单位,非常方便
技术亮点总结
1. ES6特性的应用
在路由高亮逻辑中,我充分利用了ES6的数组和字符串方法:
// 查找当前激活的Tab项
const index = tabs.findIndex(tab =>
location.pathname.startsWith(tab.path)
)
- findIndex:高效查找匹配条件的数组元素
- startsWith:检查路径是否以指定字符串开头
- 箭头函数:简化回调函数写法
这些现代JavaScript特性让代码更简洁、可读性更强。我们上述也说了其实location.pathname.startsWith(tab.path)的作用就相当于location.pathname===tab.path,而findIdex是返回满足条件的索引
2. React Vant组件库的优势
选择React Vant作为UI组件库带来了显著优势:
- 丰富的移动端组件:如Tabbar组件开箱即用
- 良好的视觉体验:遵循移动端设计规范
- 主题定制能力:支持灵活的风格调整
- 社区支持:活跃的社区和持续更新
3. 路由懒加载优化
通过React的lazy函数和Suspense组件实现路由懒加载:
const Home = lazy(() => import('@/pages/Home'))
// ...
<Suspense fallback={<div>Loading...</div>}>
{/* 路由配置 */}
</Suspense>
这种方式可以:
- 减少首屏加载时间:只加载当前需要的资源
- 优化用户体验:显示加载状态避免空白页面
- 提高应用性能:按需加载代码块
开发经验分享
布局设计的思考过程
在设计双层布局时,我经历了以下思考:
- 分析页面需求:哪些页面需要底部导航?哪些不需要?
- 确定布局类型:主布局(带导航)和空白布局(纯内容)
- 设计路由结构:嵌套路由实现布局继承
- 实现布局组件:使用Outlet作为内容插槽
- 处理边界情况:如默认重定向、未匹配路由处理
移动端适配的挑战
在适配不同设备时,遇到了几个关键问题:
- 单位选择:px固定不变,rem/vw相对灵活
- 设计稿转换:手动计算容易出错
- 第三方组件适配:确保组件库也遵循适配方案
解决方案:
- 采用rem作为主要单位
- 使用PostCSS插件自动化转换
- 检查组件库是否支持rem或提供尺寸配置
性能优化实践
在开发过程中,我实施了以下性能优化措施:
- 路由懒加载:减少首屏资源大小
- 组件复用:避免重复代码
- 图标按需引入:减小打包体积
- 生产环境构建:Vite自动优化生产包
移动端开发是一个持续学习的过程,随着项目的深入,本犬将继续探索更多优化方案和最佳实践,分享一些项目的亮点和难点
参考文档: