跟着双越老师的划水AI项目学习记录,从Vue转向React+Next.js的踩坑经验分享。
核心工具快速了解
UI相关库
- lucide-react:1000+矢量图标,即取即用
- shadcn/ui:零依赖组件库,代码复制到项目完全可控
- @radix-ui:shadcn/ui底层依赖,提供无障碍基础组件
样式工具
- class-variance-authority (CVA):管理组件变体样式
- tailwind-merge:智能合并Tailwind类名,解决冲突
- clsx:条件性组合类名的微型工具
import { twMerge } from 'tailwind-merge';
import clsx from 'clsx';
function Button({ variant, size, className, children, ...props }) {
return (
<button
className={twMerge(
clsx(
'inline-flex items-center justify-center rounded-md font-medium',
{
'bg-blue-600 text-white': variant === 'primary',
'bg-gray-200 text-gray-800': variant === 'secondary',
'h-9 px-3 text-sm': size === 'sm',
'h-10 px-4 text-base': size === 'md',
},
className // 外部传入的样式会正确覆盖默认样式
)
)}
{...props}
>
{children}
</button>
);
}
功能库
- Next.js:全栈React框架,文件系统路由
- next-themes:主题切换(暗色/亮色模式)
- react-resizable-panels:可拖拽调整的面板布局
- react-hook-form:高性能表单处理和验证
- ...
Vue vs React 核心差异
1. 快速适应技巧
- 从"指令"到"表达式":
v-if → {condition && <div/>}
- 从"双向绑定"到"单向数据流":手动控制状态更新
- 从"选项式"到"函数式":一切都是函数和Hook
2. 常用Hook对照
| Vue Composition API | React Hook |
|---|
ref() | useState() |
computed() | useMemo() |
watch() | useEffect() |
onMounted() | useEffect([], []) |
3. Next.js特色功能
- 服务器组件:默认在服务器渲染,性能更好
- 客户端组件:添加
'use client'才能使用浏览器API
- 文件路由:
app/work/[id]/page.tsx = /work/123
4.具体代码对比
1. 组件结构
<!-- Vue:三段式分离 -->
<template>
<div>{{ title }}</div>
</template>
<script>
const title = ref('标题')
</script>
<style scoped>
.title { color: red; }
</style>
export default function Component() {
const [title, setTitle] = useState('标题');
return <div className="text-red-500">{title}</div>;
}
2. 数据绑定
| Vue | React |
|---|
{{ message }} | {message} |
v-model="input" | value={input} onChange={setInput} |
@click="handle" | onClick={handle} |
v-if="show" | {show && <div/>} |
v-for="item in list" | {list.map(item => <div/>)} |
3. 状态管理
<!-- Vue -->
<script setup>
const count = ref(0);
const user = reactive({ name: '张三' });
</script>
const [count, setCount] = useState(0);
const [user, setUser] = useState({ name: '张三' });
setUser(prev => ({ ...prev, age: 20 }));
4. 路由参数获取
const id = this.$route.params.id;
export default async function Page({ params }) {
const { id } = await params;
return <div>项目 {id}</div>;
}
'use client';
import { useParams } from 'next/navigation';
const params = useParams();
5. 条件样式处理
<!-- Vue -->
<div :class="{ active: isActive, error: hasError }">内容</div>
import clsx from 'clsx';
<div className={clsx('base-style', {
'active': isActive,
'error': hasError
})}>内容</div>
import { cva } from 'class-variance-authority';
const cardVariants = cva('p-4 rounded', {
variants: {
variant: {
default: 'bg-white',
primary: 'bg-blue-500 text-white'
}
}
});
<div className={cardVariants({ variant: 'primary' })}>卡片</div>
6. 生命周期
onMounted(() => {})
onUpdated(() => {})
onUnmounted(() => {})
useEffect(() => {
return () => {
};
}, []);
7. 表单处理
<!-- Vue -->
<input v-model="email" />
<input v-model="password" type="password" />
const [email, setEmail] = useState('');
<input value={email} onChange={(e) => setEmail(e.target.value)} />
import { useForm } from 'react-hook-form';
const { register, handleSubmit } = useForm();
<input {...register('email', { required: '邮箱必填' })} />