普通视图

发现新文章,点击刷新页面。
今天 — 2025年6月8日首页

从原生 JS 到 React:手把手带你开启 React 业务开发之旅

作者 然我
2025年6月8日 12:30

一、前端开发演进:从切图崽到全干工程师的成长路径

在开始React业务开发之前,让我们先了解前端工程师的成长路线:

  1. 前端切图崽:掌握HTML+CSS+基础JS,能实现静态页面
  2. 前端开发工程师:掌握Vue/React等框架,能开发复杂Web应用
  3. 全栈开发工程师:掌握Node.js+数据库,实现前后端一体化
  4. 跨平台应用开发:掌握React Native等,开发Android/iOS应用
  5. AI应用开发:整合AI能力,开发智能化应用
  6. 全干工程师:掌握全技术栈,独立完成项目全流程

React作为现代前端开发的三大框架之一,让开发者得以脱离底层 DOM 操作,聚焦于业务逻辑,是我们进阶路上的重要里程碑。

二、React 项目初始化:从 vite 模板开始

1. 核心工具:npm 与 vite 的作用

  • npm(Node Package Manager) :作为 Node.js 的包管理器,负责安装 React 等开发依赖,如通过npm install react react-dom引入核心库。
  • vite:新一代前端构建工具,相比传统 webpack,具有极速冷启动、按需编译的特点,尤其适合 React 项目的工程化搭建。

2. 快速创建项目的 4 步流程

# 1. 初始化vite项目(选择react模板语言选择js)
npm init vite@latest my-react-app -- --template react

# 2. 进入项目目录
cd my-react-app

# 3. 安装依赖
npm install

# 4. 启动开发服务器(默认端口5173)
npm run dev

执行完成后,浏览器访问http://localhost:5173即可看到 React 欢迎页面

image.png

项目结构如下:

image.png

三、React 初体验:组件化开发的核心逻辑

1. 组件:HTML+CSS+JS 的封装单元

React 组件是完成开发任务的最小单元,它将 HTML、CSS、JS 逻辑封装在一个函数中,通过 组件组合 构建完整页面。以下是一个基础的 App 组件示例,演示如何通过函数组件渲染静态数据:

import { useState } from 'react';
import './App.css';

// 函数组件:接收参数并返回 JSX 结构
function App() {
  const staticTodos = ['吃饭', '睡觉', '打豆豆']; 
  return (
    <>
      <table>
        <thead>
          <tr>
            <th>序号</th>
            <th>任务</th>
          </tr>
        </thead>
        <tbody>
          {/* 在 JSX 中通过 {} 嵌入 JS 表达式,使用 map 遍历渲染列表 */}
          {staticTodos.map((item, index) => (
            <tr key={index}> {/* 列表渲染必须指定唯一 key,此处仅为演示,实际应使用数据唯一标识 */}
              <td>{index + 1}</td>
              <td>{item}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </>
  );
}

export default App;

image.png

关键知识点:

  • JSX 语法:允许在 JS 中直接书写类 HTML 结构,{} 用于插入 JS 表达式(如变量、函数调用等)。
  • 列表渲染:通过 Array.map() 遍历数据并生成多个 JSX 元素,需为每个列表项指定唯一 key(React 用于追踪组件更新的标识)。
  • 组件结构:函数组件必须有一个根返回值(如 <> 空标签或 <div> 等容器元素)。

2. 响应式数据:useState Hook 实现数据驱动视图

当数据需要动态变更并触发视图更新时,需使用 React 的 状态(State)  机制。通过 useState Hook 声明响应式数据,数据变更时 React 会自动重新渲染组件。

示例:动态更新标题与任务列表

import { useState } from 'react';
import './App.css';

function App() {
  // 声明响应式状态:todos(任务列表)和 title(页面标题)
  const [todos, setTodos] = useState(['吃饭', '睡觉', '打豆豆']);
  const [title, setTitle] = useState('今天你吃饭了吗'); 

  // 模拟异步操作:5 秒后更新
  setTimeout(() => {
    setTitle('今天你完成任务了吗');
    setTodos([...todos, '养鱼']); 
  }, 5000);

  return (
    <div>
      <h1 className="title">{title}</h1> {/* 绑定响应式数据 title */}
      <table>
        <thead>
          <tr>
            <th>序号</th>
            <th>任务</th>
          </tr>
        </thead>
        <tbody>
          {todos.map((item, index) => (
            <tr key={index}>
              <td>{index + 1}</td>
              <td>{item}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

export default App;

CSS 样式(App.css):

* {
  margin: 0;
  padding: 0;
}

.title {
  background-color: aqua;
  color: aliceblue;
  padding: 10px;
  margin-bottom: 20px;
}

image.png

五秒后更新为: image.png

核心概念解析:

  • useState 用法

    • 调用 useState(initialValue) 返回一个数组,第一项是状态值(如 todos),第二项是更新状态的函数(如 setTodos)。
    • 状态更新函数(如 setTitle)的调用会触发组件重新渲染,确保视图与数据同步。
  • 异步更新:即使在定时器、Promise 等异步场景中调用 setState,React 也能正确捕获变更并更新视图。

  • 数据不可变性:更新数组或对象状态时,需通过 [...todos] 或 {...obj} 创建新对象,避免直接修改原数据导致 React 无法检测变更。

3. 组件组合:拆解复杂页面为可复用单元

React 推荐将页面拆分为多个小组件,通过嵌套组合实现复杂功能。以下是对示例代码的扩展思路 :

// 拆分标题组件
function Title({ title }) {
  return <h1 className="title">{title}</h1>;
}

// 拆分任务列表组件
function TodoTable({ todos }) {
  return (
    <table>
      <thead>
        <tr>
          <th>序号</th>
          <th>任务</th>
        </tr>
      </thead>
      <tbody>
        {todos.map((item, index) => (
          <tr key={index}>
            <td>{index + 1}</td>
            <td>{item}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

function App() {
  const [todos, setTodos] = useState(['吃饭', '睡觉', '打豆豆']);
  const [title, setTitle] = useState('今天你吃饭了吗');

  setTimeout(() => setTitle('今天你完成任务了吗'), 5000);

  return (
    <div>
      <Title title={title} /> {/* 父组件向子组件传递数据 via props */}
      <TodoTable todos={todos} /> {/* 复用列表组件,传入不同数据 */}
    </div>
  );
}

组件通信规则:

  • 父传子:通过 props 向子组件传递数据(如 Title 组件的 title 属性)。
  • 子传父:子组件通过回调函数(如 onClick={handleClick})向父组件传递事件。
  • 跨组件通信:状态提升(将共享状态提升至共同父组件)或使用 Context 实现全局状态管理。

4. 对比原生 JS:React 如何简化开发

场景 原生 JS 实现方式 React 实现方式
渲染列表 使用 createElement 或 innerHTML 拼接 DOM 字符串 通过 JSX + map 直接声明视图结构
数据更新 手动查找 DOM 节点并更新 textContent/innerHTML 调用 setState 自动触发视图更新
组件复用 复制粘贴代码或封装函数返回 DOM 节点 定义函数组件并通过 props 传递差异化数据

核心优势:React 通过声明式编程(描述 “是什么” 而非 “怎么做”)和组件化架构,让开发者聚焦业务逻辑而非底层 DOM 操作,极大提升开发效率与代码可维护性。

四、实战案例:构建响应式待办事项应用

1. 功能拆解

我们将通过实际代码实现一个包含以下功能的待办应用:

  • 输入框添加新任务(表单组件TodoForm
  • 任务列表展示(列表组件Todos
  • 数据持久化(暂存于组件状态,后续可扩展 localStorage)

2. 核心组件实现

首先我们先在src目录下创建一个components文件夹,存放各个组件

(1)表单组件TodoForm:双向绑定与提交处理

import { useState } from 'react';

function TodoForm(props) {
  // 从 props 中获取父组件传递的回调函数(用于接收新增任务)
  const onAdd = props.onAdd;
  // 声明本地状态:text 存储输入框内容,初始值为"打豆豆"
  const [text, setText] = useState('打豆豆');

  // 表单提交处理函数
  const handleSubmit = (e) => {
    // 阻止表单默认提交行为(避免页面跳转)
    e.preventDefault();
    // 调用父组件回调,传递当前输入的文本
    onAdd(text);
    console.log(e.target.value);
  };

  // 输入框内容变化处理函数
  const handleChange = (e) => {
    // 更新本地状态,实现双向绑定(输入框值与 state 同步)
    setText(e.target.value);
  };

  return (
    <form action="http://www.baidu.com" onSubmit={handleSubmit}>
      <input
        type="text"
        placeholder="请输入待办事项"
        value={text}          // 绑定 state 到输入框 value
        onChange={handleChange} // 监听输入变化并更新 state
      />
      <button type="submit">添加</button>
    </form>
  );
}

export default TodoForm;

在这个组件中,通过useState定义text状态,实现输入框的双向绑定。handleChange函数监听输入变化更新状态,handleSubmit函数阻止表单默认提交,并调用父组件传递的onAdd回调函数,将输入内容传递出去。

(2)列表组件Todos:数据渲染与 key 的重要性

function Todos(props) {
  console.log(props, '/////');
  // 解构 props 中的 todos 数据
  const todos = props.todos;

  return (
    <ul>
      {
        // 遍历 todos 数组,渲染为列表项
        todos.map((todo) => (
          // key 是 React 识别列表项的唯一标识,必须使用稳定的唯一值(如数据 ID)
          <li key={todo.id}>{todo.text}</li>
        ))
      }
    </ul>
  );
}

export default Todos;

该组件接收父组件传递的props.todos数据,使用map方法遍历数据并渲染为列表项。为每个列表项设置唯一的key属性(这里使用todo.id),这能帮助 React 高效地更新列表,避免在数据变化时出现渲染错误。

(3)核心逻辑组件TodoList:状态管理与组件组合

import { useState } from 'react';
import '../Todo.css'; // 引入组件级样式(需确保路径正确)
import TodoForm from './TodoForm'; // 引入表单组件(子组件)
import Todos from './Todos'; // 引入列表组件(子组件)

function TodoList() {
  const [hi, setHi] = useState('haha');
  const [title, setTitle] = useState('Todo List');
  const [todos, setTodos] = useState([
    {
      id: 1,          // 唯一标识(必填,用于列表渲染和更新)
      text: '吃饭',  
      completed: false 
    }
  ]);

  // 添加任务的回调函数(供子组件调用)
  const handleAdd = (text) => {
    // 更新 todos 状态:使用展开运算符保留原有数据,新增任务项
    setTodos([
      ...todos,
      {
        id: todos.length + 1, 
        text,                // 解构参数作为任务文本
        completed: false      
      }
    ]);
  };

  return (
    <div className="container">
      <h1 className="title">
        {title}{hi} {/* 组合显示标题和演示文本 */}
      </h1>
      <TodoForm onAdd={handleAdd} /> {/* 向子组件传递回调函数,实现子传父通信 */}
      <Todos todos={todos} />       {/* 向子组件传递数据,实现父传子通信 */}
    </div>
  );
}

export default TodoList;

TodoList组件作为整个待办应用的核心,通过useState定义了多个状态,如titletodoshandleAdd函数用于处理添加任务的逻辑,当调用setTodos更新任务列表状态时,React 会自动重新渲染包含Todos组件的部分,实现数据驱动视图更新。同时,通过propshandleAdd函数传递给TodoForm组件,完成子传父的通信。

(4)根组件App:整合应用

import { useState } from 'react'
import './App.css'
import TodoList from './components/TodoList'

function App() {
  return (
    <>
      <div>
      </div> 
 {/* 渲染待办事项核心组件 */}
      <TodoList />
    </>
  )
}

export default App

App组件作为应用的入口,只负责引入并渲染TodoList组件,将整个待办应用集成到项目中。

image.png

通过这个完整的待办事项应用案例,我们能更清晰地看到 React 中组件化开发、数据驱动视图、组件间通信等核心概念的实际应用。后续还可以在此基础上,添加任务删除、任务状态切换等功能,进一步深化对 React 开发的理解 。

学习 React 之路道阻且长,唯有持续深耕

从原生 JS 到 React,前端开发变得更加高效和便捷。React 的组件化开发和响应式数据管理,让开发者能够聚焦于业务逻辑,而无需过多关注底层的 DOM 操作。随着技术的不断发展,相信前端开发还会迎来更多的变革和创新。

昨天以前首页

你以为的 Tailwind 并不高效,看看这些使用误区

作者 ErpanOmer
2025年6月6日 13:54

“Tailwind 写得越多,越觉得混乱”“组件样式重复一堆”“设计师完全看不懂这坨 className”…

这些反馈你是否也听说过?

Tailwind CSS 被誉为“实用优先的 CSS 框架”,然而在实际项目中,很多团队用了 Tailwind,效率却不升反降
不是因为 Tailwind 本身不行,而是——你可能正踩在这些使用误区上

本文将围绕 6 个常见误区,逐一剖析:

  1. 把 Tailwind 当成原子 CSS 的“组合器”
  2. 滥用 @apply 和组件式提取
  3. 不配置 Design Token,直接用默认色板
  4. 缺少抽象语义类的规范
  5. 无视团队协作中的语义歧义
  6. 不懂插件生态和可配置性,白白造轮子

🎯 误区一:把 Tailwind 当成“低配版 SCSS”来写

有些团队习惯了 BEM 或 SCSS 的写法,迁移 Tailwind 后陷入一个坑:

“我只会把 .title {} 改写成 <div class="title">,然后 .title@apply text-lg font-bold 合成”

结果就是:

  • 每个组件依然维护 .xxx { @apply ... }
  • 每个 class 被重复“硬编码”,没有复用价值
  • 真正的 Tailwind 优势(原子化组合)彻底失效

这其实是把 Tailwind 当成 SCSS 的语法糖在用,不仅没提高效率,反而多了一层“拼贴工”。

建议:

  • 组件中直接使用原子类组合,不要回退为传统 class 命名法
  • 通过抽象语义 class(例如 btn-primary)统一复杂样式,避免 @apply 滥用

🔥 误区二:滥用 @apply,导致样式复用变得更难维护

Tailwind 支持 @apply,但很多人误解它是“推荐方式”。结果就是:

.btn {
  @apply px-4 py-2 rounded-md bg-blue-500 text-white;
}
.btn-secondary {
  @apply btn bg-gray-500;
}

这本质上已经偏离了 Tailwind 的精神。问题在于:

  • @apply 语义缺失,又回到了传统 CSS 的“找 class 写样式”流程
  • 一旦 .btn 修改,影响范围不可控(链式引用)
  • 无法动态响应状态(例如 hover:bg-blue-600dark:bg-blue-400

正确方式:

  • 抽象出来的 class 应该直接写在 class="" 中,比如 class="btn btn-primary"
  • 配合 UnoCSS 或 Tailwind plugin,使用 语义化原子类 实现动态组合(见下文)

🎨 误区三:直接使用 Tailwind 默认色板,导致主题难以统一

许多初学者习惯直接写:

<div class="bg-blue-500 text-gray-800">按钮</div>

看起来没毛病,但:

  • “blue-500” 具体代表什么品牌色?设计稿里用的是 #378AFF 你知道吗?
  • 一旦品牌换主色,100 个组件都要人工搜索替换?
  • 多人项目中,“每个人对 text-sm 的认知都不同”

这不是视觉认知问题,而是你没有建立设计 token 体系

推荐配置方式(tailwind.config.ts):

theme: {
  colors: {
    brand: {
      DEFAULT: '#378AFF',
      dark: '#2563EB',
      light: '#93C5FD'
    }
  },
  fontSize: {
    base: '16px',
    sm: '14px',
    lg: '18px'
  }
}

然后组件中统一用 bg-brand text-sm,做到真正的“设计系统驱动”。


🧱 误区四:class 混乱、语义缺失,导致组件难复用

很多初级 Tailwind 项目里的组件看起来像这样:

<div class="px-4 py-2 rounded-md bg-blue-500 text-white text-sm shadow-md hover:bg-blue-600">
  提交
</div>

逻辑没问题,但:

  • 这个组件到底是按钮?标签?还是 Toast?
  • 想要复用时只能 copy-paste,一改就坏
  • 业务迭代时,10 个类似组件居然样式不同(改过一点 padding、换了一个 shadow)

Tailwind 推荐语义化命名 + utility 组合方式,例如:

<button class="btn btn-primary">提交</button>

然后在 tailwind.config.ts 里添加:

plugins: [
  require('@tailwindcss/forms'),
  function ({ addComponents }) {
    addComponents({
      '.btn': {
        @apply px-4 py-2 rounded-md text-white text-sm;
      },
      '.btn-primary': {
        @apply bg-brand hover:bg-brand-dark;
      }
    })
  }
]

不仅可复用,还能集中管理样式变更。


🧠 误区五:团队协作不统一,样式风格“各写各的”

当团队使用 Tailwind 却没有协作规范时,经常出现以下现象:

  • A 开发写 text-sm, B 写 text-xs,页面出现 4 种字号
  • 有的写 rounded, 有的写 rounded-md,风格混乱
  • 多人维护组件时,一改颜色就影响全局,因为写死在组件里

Tailwind 鼓励显式地“写出来”,但这不代表可以无限自由拼接

解决方式:

  • 建立 token 规范:颜色、字体、尺寸统一配置
  • 使用 @shadcn/ui 作为组件层抽象(推荐组合 Tailwind + Radix + shadcn)
  • 使用 ESLint 插件强制校验 Tailwind class 的顺序和规范(如 eslint-plugin-tailwindcss

⚙️ 误区六:没用插件、没开 JIT、错失生态红利

很多团队用了 Tailwind,但配置还停留在最基本阶段:

  • 没开 JIT 模式(Just In Time 构建)
  • 没启用 dark mode(Tailwind 支持类选择器控制 dark 样式)
  • 没接入 tailwind-variants / clsx / cva 等原子类组合库
  • 对 UnoCSS 完全不了解(其实比 Tailwind 更自由,兼容性好)

Tailwind 的能力远不止于“写几个 class”——它已经是一个完整的样式编程语言了

举个例子:

你可以使用 tailwind-variants 这样写组件样式组合:

const button = tv({
  base: 'inline-flex items-center justify-center font-medium',
  variants: {
    intent: {
      primary: 'bg-brand text-white hover:bg-brand-dark',
      secondary: 'bg-gray-100 text-gray-800 hover:bg-gray-200'
    },
    size: {
      sm: 'text-sm py-1 px-2',
      md: 'text-base py-2 px-4'
    }
  }
})

然后在组件中使用:

<button className={button({ intent: 'primary', size: 'md' })}>按钮</button>

写起来干净,组合灵活,完全符合设计系统的思想。


✅ 正确的 Tailwind 使用思维:构建语义原子设计系统

Tailwind 的真正优势不在于“快”,而在于:

构建一套“视觉样式与设计语言一致”的原子组件体系。

好的实践是:

  • 所有颜色、字号、间距抽象为 Design Token
  • 所有组件样式统一在配置文件或 plugin 中抽象复用
  • 使用 shadcn/ui + tailwind-variants + clsx 构建语义组件库
  • 使用 prettier-plugin-tailwindcss + ESLint 强化规范
  • 页面代码中只使用 最小必要 class + 语义化 class
  • 样式变化驱动来自配置层变动,而非组件层硬改

🧩 最后

Tailwind 并不是魔法,它只是一个极致实用主义的 CSS 工具包。

真正的工程实践中,Tailwind 的效率取决于你是否理解并规避以下误区:

误区 正确思路
把 Tailwind 当 SCSS 用 原子化组合 + 抽象语义 class
滥用 @apply 提炼插件组件 + 原子类组合方式
使用默认色板无品牌感 建立 Design Token 体系
样式写死、组件无法复用 抽象组件语义 class + 统一配置管理
多人协作风格混乱 使用 lint、格式化插件强制规范
没使用生态工具,错失红利 熟悉 tailwind-variants / UnoCSS 等替代方案
❌
❌