3、展示层设计:View层的职责边界、模板引擎的选择(JSX vs Template)、无状态组件与有状态组件的划分

展示层,说白了就是用户能看到的那一层。很多新手容易犯一个错误——把所有的逻辑都塞进 View 里。我见过一个项目,一个组件文件写了 800 行,里面混着 API 调用、数据格式化、路由跳转,最后连原作者自己都看不懂了。

那 View 层的职责边界到底在哪?我个人习惯用一句话来定义:View 只负责“怎么画”,不负责“画什么”和“为什么画”。数据从哪来、什么时候更新、用户操作后要做什么——这些都不该是 View 管的。

3.1 View 层的职责边界

先看一个反面教材。我在项目中遇到过这样的代码:

// 反面示例:View 层做了太多事
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // View 层直接发请求
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        // View 层还做了数据转换
        const formatted = {
          ...data,
          fullName: `${data.firstName} ${data.lastName}`,
          age: calculateAge(data.birthday)
        };
        setUser(formatted);
        setLoading(false);
      });
  }, [userId]);

  // 渲染逻辑
  if (loading) return <Spinner />;
  return (
    <div>
      <h1>{user.fullName}</h1>
      <p>年龄:{user.age}</p>
    </div>
  );
}

这个组件的问题在哪?它把数据获取、数据转换、状态管理、UI 渲染全揉在一起了。你想想看,如果换一个页面也要展示用户信息,是不是得再写一遍同样的逻辑?

正确的做法应该是这样:

// 正确示例:View 层只做渲染
function UserProfile({ user, loading }) {
  if (loading) return <Spinner />;
  return (
    <div>
      <h1>{user.fullName}</h1>
      <p>年龄:{user.age}</p>
    </div>
  );
}

数据获取和转换交给 Service 层或 Store 层去做。View 层只接收已经处理好的数据,然后画出来。这样测试也方便——你只需要传不同的 props,看它渲染对不对就行。

View 层的核心原则:

  • 只接收 props,不直接发请求
  • 只做展示逻辑(循环、条件渲染),不做业务逻辑
  • 不直接操作 DOM(除非是第三方库的 ref 场景)
  • 不存储持久化状态(只存 UI 状态,比如折叠/展开)

3.2 模板引擎的选择:JSX vs Template

说到模板引擎,前端圈一直有两大阵营:JSX 派和 Template 派。我两个都用过,说说我的真实感受。

对比维度 JSX(React) Template(Vue/Angular)
表达能力 完整 JS 能力,任意逻辑都能写 受限于指令系统,复杂逻辑需用计算属性
学习曲线 需要 JS 基础,上手稍难 类似 HTML,新手友好
类型安全 天然支持 TypeScript 需要额外工具链支持
可测试性 组件即函数,测试简单 需要挂载实例,测试稍复杂
性能优化 手动控制渲染(memo、useMemo) 自动依赖追踪,细粒度更新

我个人更倾向于 JSX。为什么?因为 JSX 本质上就是 JS。你想想看,在 JSX 里写条件渲染:

// JSX:直接用 JS 语法
{isLoggedIn ? <Dashboard /> : <Login />}

而在 Template 里:

<!-- Template:需要学指令 -->
<Dashboard v-if="isLoggedIn" />
<Login v-else />

JSX 不需要额外学一套模板语法。你会的 JS 知识,直接就能用在 JSX 里。但 Template 也有它的好处——更干净,更接近 HTML 直觉。Vue 的模板对设计师很友好,他们可以直接看懂。

我的建议:

如果你的团队以 JS 工程师为主,选 JSX。如果团队里有大量 HTML/CSS 背景的成员,Template 可能更合适。没有绝对的好坏,只有合不合适。

3.3 无状态组件与有状态组件的划分

这个划分,说白了就是“这个组件需不需要自己管数据”。

无状态组件(也叫展示组件、纯组件):只依赖 props,没有自己的状态。给它什么数据,它就画什么。就像一个打印机——你塞进一张纸,它给你印出内容。

// 无状态组件:纯展示
function UserCard({ name, avatar, onClick }) {
  return (
    <div className="card" onClick={onClick}>
      <img src={avatar} alt={name} />
      <span>{name}</span>
    </div>
  );
}

有状态组件(也叫容器组件、智能组件):自己管理状态,比如用户输入、展开/折叠、定时器等。

// 有状态组件:管理 UI 状态
function SearchBox({ onSearch }) {
  const [keyword, setKeyword] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    onSearch(keyword);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={keyword}
        onChange={(e) => setKeyword(e.target.value)}
      />
      <button type="submit">搜索</button>
    </form>
  );
}

那怎么划分?我有个简单的判断标准:问自己一个问题——这个状态如果去掉,组件还能正常工作吗?

比如一个按钮的 loading 状态,去掉它按钮还能点,只是没有反馈。那这个状态就是 UI 状态,可以放在组件里。但如果是用户信息,去掉它页面就空了,那这个状态应该由上层管理。

我曾经踩过的坑:

我曾经把一个表单的校验逻辑全放在 View 层。结果表单有 20 个字段,校验规则复杂,组件文件膨胀到 600 行。后来重构时,我把校验逻辑抽到独立的 hooks 里,View 层只负责绑定值和展示错误信息。代码瞬间清爽了。

记住:View 层只存 UI 状态,不存业务状态。业务状态交给 Store 或 Service 层。

3.4 实战中的划分策略

在实际项目中,我一般这样划分:

  1. 页面级组件:有状态,负责组合子组件、管理页面级 UI 状态(如 tab 切换、弹窗开关)
  2. 业务组件:尽量无状态,通过 props 接收数据和回调。如果必须用状态,只存 UI 状态
  3. 通用组件:纯无状态,比如 Button、Card、Modal 等

举个例子,一个用户列表页面:

// 页面级组件:有状态
function UserListPage() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchUsers().then(data => {
      setUsers(data);
      setLoading(false);
    });
  }, []);

  return (
    <div>
      <SearchBox onSearch={handleSearch} />
      <UserTable users={users} loading={loading} />
    </div>
  );
}

// 业务组件:无状态
function UserTable({ users, loading }) {
  if (loading) return <Spinner />;
  return (
    <table>
      {users.map(user => (
        <UserRow key={user.id} user={user} />
      ))}
    </table>
  );
}

// 通用组件:无状态
function Spinner() {
  return <div className="spinner" />;
}

这样分层的好处很明显:

  • UserTable 可以单独测试,传不同的 users 数据看渲染效果
  • Spinner 可以复用到任何需要加载提示的地方
  • 如果以后要换数据源,只需要改 UserListPage,其他组件不用动

记住这个原则:

能写成无状态的就写成无状态的。无状态组件更容易测试、更容易复用、更容易理解。只有当你确实需要管理 UI 状态时,才把它变成有状态组件。

嗯,展示层设计就聊到这。说白了就是一句话:View 层要“傻”一点。它越傻,你的项目就越容易维护。下一章我们会聊数据层设计,到时候你会看到,把数据逻辑从 View 层剥离出来后,整个项目结构会变得多清晰。