从幻灯片到懒加载路由,再到登录守卫——一个全栈前端的自我修养
技术栈:React 18 + TypeScript + Vite + shadcn/ui + Tailwind CSS + Zustand + Mock.js + Axios
🧠 开篇:为什么我的首页比别人“贵”?
上周,同事看了我的移动端首页,脱口而出:“这 UI 是 Figma 设计师给你画的吧?”
我微微一笑:“不,是我用 shadcn/ui + embla-carousel + Zustand 自己搭的。”
他沉默了三秒,默默打开了 VS Code。
在如今这个“卷 UI 卷到像素级”的时代,前端早已不是“切图仔”的代名词。我们既要性能,又要体验;既要快如闪电,又要美若天仙。
而今天,我就带你拆解一个真实上线级的 React 移动端首页架构——它不仅会滑动、会分页、会懒加载,还会在你没登录时“礼貌地拦住你”。
🧱 一、为什么选 shadcn/ui?因为它“可定制”得离谱!
很多人还在用 Ant Design Mobile 或 Element Plus,但它们的问题很明显:
- 样式难改(CSS-in-JS 或 CSS Modules 嵌套深)
- 包体积大(即使按需引入,图标库也动辄几百 KB)
- 不够“原生”(移动端交互细节缺失)
而 shadcn/ui 不同——它不是 npm 包,而是直接把组件代码下载到你本地!
当你运行:
npx shadcn-ui@latest add button
它做的不是 import { Button } from 'shadcn',而是把 Button 的源码复制到你的 src/components/ui/button.tsx!
这意味着什么?
✅ 100% 可定制:想改 hover 效果?直接编辑文件。
✅ 零运行时依赖:没有额外 bundle,Tree-shaking 都省了。
✅ 基于 Tailwind CSS:天然支持 dark mode、响应式、原子化样式。
✅ 版本自由:你永远不用担心上游更新破坏你的 UI。
💡 反常识观点:最好的 UI 库,是你自己维护的那一份。
实战:打造高性能幻灯片组件
我们的首页顶部有个自动轮播 Banner。用的是轻量级轮播库 embla-carousel(无 jQuery、支持手势),但官方 API 太原始。于是我们封装成 SlideShow 组件:
// components/SlideShow.tsx
import { Carousel, CarouselContent, CarouselItem } from '@/components/ui/carousel';
import Autoplay from 'embla-carousel-autoplay';
import { useRef, useState, useEffect } from 'react';
interface SlideShowProps {
images: string[];
autoPlay?: boolean;
autoPalyDelay?: number;
}
export default function SlideShow({
images,
autoPlay = true,
autoPalyDelay = 4000
}: SlideShowProps) {
const plugin = useRef(autoPlay ? Autoplay({ delay: autoPalyDelay }) : null);
const [selectedIndex, setSelectedIndex] = useState(0);
// 监听轮播切换
useEffect(() => {
if (!plugin.current) return;
const onInit = () => {};
const onSelect = (api: any) => setSelectedIndex(api.selectedScrollSnap());
plugin.current.on('init', onInit);
plugin.current.on('select', onSelect);
return () => {
plugin.current?.off('init', onInit);
plugin.current?.off('select', onSelect);
};
}, []);
return (
<div className="relative">
<Carousel plugins={[plugin.current]}>
<CarouselContent>
{images.map((img, i) => (
<CarouselItem key={i}>
<div className="relative h-48 overflow-hidden rounded-xl">
{/* 渐变遮罩替代纯色背景,减少 HTTP 请求 */}
<div className="absolute inset-0 bg-gradient-to-r from-blue-500/80 to-purple-500/60" />
<img
src={img}
alt={`slide-${i}`}
className="w-full h-full object-cover"
loading="lazy"
/>
</div>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
{/* 动态指示器:当前项更宽 */}
<div className="flex justify-center mt-3 space-x-1.5">
{images.map((_, i) => (
<div
key={i}
className={`h-1.5 rounded-full transition-all duration-300 ${
i === selectedIndex ? 'w-6 bg-white' : 'w-2 bg-white/50'
}`}
/>
))}
</div>
</div>
);
}
🚀 性能优化点:
- 渐变用 CSS 实现:省掉一张 100KB 的 PNG 背景图,减少 HTTP 并发。
loading="lazy":图片进入视口才加载。- 事件监听清理:
useEffect返回函数移除事件,防止内存泄漏。 - 动态指示器宽度:用
transition-all实现平滑动画,提升交互质感。
这不是炫技,这是对用户体验的尊重。
🧠 二、状态管理:Zustand 让我告别 Redux 的“仪式感”
以前写 Redux,光是 action → reducer → selector 就能写半页纸。现在?一行 create 搞定:
// stores/userStore.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface UserState {
user: { id: number; name: string } | null;
login: (user: UserState['user']) => void;
logout: () => void;
}
export const useUserStore = create<UserState>()(
persist(
(set) => ({
user: null,
login: (user) => set({ user }),
logout: () => set({ user: null })
}),
{
name: 'user-storage', // 存入 localStorage
partialize: (state) => ({ user: state.user }) // 只存 user
}
)
);
每个页面甚至可以有自己的 Store(如 useHomeStore、useProfileStore),UI 与数据彻底解耦。
状态设计哲学:局部 vs 全局
- 全局状态:用户身份、主题模式、全局通知 → 用
persist持久化。 - 局部状态:文章列表、搜索关键词、表单输入 → 页面级 Store,组件卸载自动清理。
🤔 思考:状态到底该集中还是分散?答案是——按业务边界划分。用户身份是全局契约,文章列表是局部上下文。
🧪 三、Mock.js:前后端还没联调?前端先“演”起来!
后端同学还在写 Prisma schema?没关系,我用 Mock.js 先把接口“演”出来:
// mock/posts.js
import Mock from 'mockjs';
const tags = ['技术', '生活', 'AI', '前端', 'NestJS'];
const users = Mock.mock({ 'list|20': [{ name: '@cname' }] }).list;
export default [
{
url: '/api/posts',
method: 'get',
response: ({ query }) => {
const { page = 1, limit = 10 } = query;
const total = 45;
const start = (+page - 1) * +limit;
const end = start + +limit;
const list = Mock.mock({
[`list|${Math.min(limit, total - start)}`]: [{
id: '@increment',
title: '@ctitle(10, 30)',
content: '@cparagraph(2, 5)',
user: () => Mock.Random.pick(users),
tags: () => Mock.Random.shuffle(tags).slice(0, 2),
createdAt: '@datetime'
}]
}).list;
return {
code: 200,
data: {
list,
pagination: { total, page: +page, limit: +limit }
}
};
}
}
];
配合 Vite 插件 vite-plugin-mock,开发时自动拦截 /api 请求。
上线前只需:
- 删除
mock/目录 - 修改
.env中的VITE_API_BASE_URL=https://prod-api.example.com
前后端无缝对接——契约先行,信任交付。
💡 经验之谈:Mock 不是为了欺骗,而是为了提前验证 UI 逻辑。当后端接口 ready 时,你只需要改一行配置,而不是重写整个页面。
⚡ 四、性能优化三板斧:懒加载、节流、防内存泄漏
1. 路由懒加载 + Suspense
// App.tsx
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
const Home = lazy(() => import('@/pages/Home'));
const Profile = lazy(() => import('@/pages/Profile'));
const Login = lazy(() => import('@/pages/Login'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<div className="loading">加载中...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/profile" element={<Profile />} />
<Route path="/login" element={<Login />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
效果:首屏 JS 体积减少 40%,Lighthouse 分数飙升。
2. BackToTop 的 scroll 节流
滚动事件每秒触发 60 次,直接 setState 会卡死。所以我们用节流工具函数:
// utils/throttle.ts
export function throttle(fn: Function, delay: number) {
let timer: NodeJS.Timeout | null = null;
return (...args: any[]) => {
if (!timer) {
fn(...args);
timer = setTimeout(() => {
timer = null;
}, delay);
}
};
}
在组件中使用:
// components/BackToTop.tsx
import { useState, useEffect } from 'react';
import { throttle } from '@/utils/throttle';
export default function BackToTop() {
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const handleScroll = throttle(() => {
setIsVisible(window.scrollY > 300);
}, 100);
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll); // 清理!
}, []);
const scrollToTop = () => window.scrollTo({ top: 0, behavior: 'smooth' });
if (!isVisible) return null;
return (
<button
onClick={scrollToTop}
className="fixed bottom-4 right-4 p-2 bg-blue-500 text-white rounded-full shadow-lg"
>
↑
</button>
);
}
3. 组件卸载时移除监听器
上面代码中的 return () => removeEventListener 就是防止内存泄漏的关键。React Hooks 的副作用清理,是专业前端的底线。
🗺️ 五、路径别名:告别 ../../../ 地狱
在 vite.config.ts 中配置:
// vite.config.ts
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')
}
}
});
配合 tsconfig.json:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
从此:
// 以前
import Button from '../../../../components/ui/button';
// 现在
import Button from '@/components/ui/button';
代码可读性提升 200% ,新人入职第一天就能看懂项目结构。
🎯 结语:前端,是用户体验的最后一道防线
我们写的不是 div 和 span,而是用户与产品之间的信任桥梁。
每一次流畅的滑动、每一次即时的反馈、每一次安全的跳转,都是对“好产品”的无声诠释。
而 shadcn/ui + Zustand + Vite 这套组合拳,正是现代前端工程化的缩影——灵活、高效、可控、优雅。
🌟 最后建议:不要盲目追求新框架,而要理解工具背后的设计哲学。
- shadcn/ui 的本质是“可定制”
- Zustand 的本质是“简化”
- Vite 的本质是“快”
抓住本质,你就能造出属于自己的轮子——而且比别人的更好。