普通视图

发现新文章,点击刷新页面。
今天 — 2025年7月7日技术

vite调试node_modules下面插件

2025年7月7日 14:19

在使用vite进行开发的时候,我们可能想要修改node_modules中插件的源码.特别是集成一个SDK,需要调试去判断问题时,或者研究第三方源码时后;

vite默认是走缓存的,所以当修改后不会看到你打印的日志,这个时候有几种方法可以选择;

方式1

手动删除 node_modules/.vite 目录

方式2

npm run dev --force ;运行指令的时候添加--force;

方式3

在package.json 中添加--force "serve": "vite --force --mode development",

浏览器

收起日志Console面板

在这里插入图片描述

全勾上

在这里插入图片描述

uniapp使用h5的map(已弃用)

作者 Mr_汪
2025年7月7日 14:07

目前已经启用,当前是前期调用的,要求使用h5本地map宣绒,后放弃了,目前只是写了一个简单的demo,组要是h5和本地的交互

本地html

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>本地网页</title>
<style type="text/css">
.btn {
display: block;
margin: 20px auto;
padding: 5px;
background-color: #007aff;
border: 0;
color: #ffffff;
height: 40px;
width: 200px;
}

.btn-red {
background-color: #dd524d;
}

.btn-yellow {
background-color: #f0ad4e;
}

.desc {
padding: 10px;
color: #999999;
}

#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
</style>
</head>
<body>
<div id="map"></div>

<p class="desc">web-view 组件加载本地 html 示例,仅在 App 环境下生效。点击下列按钮,跳转至其它页面。</p>

<p class="desc">网页向应用发送消息。注意:小程序端应用会在此页面后退时接收到消息。</p>
<div class="btn-list">
<button class="btn btn-red" type="button" id="postMessage">postMessage</button>
</div>
<!-- uni 的 SDK -->
<script type="text/javascript" src="https://unpkg.com/@dcloudio/uni-webview-js@0.0.1/index.js"></script>

<script src="http://webmap.sf-express.com/api/map?v=3.1&ak=申请的ak"></script>


<script type="text/javascript">
var map = ''
// 接收数据的全局函数(与 uni-app 中 evalJS 调用的名称一致)
function receiveDataFromApp(data) {
console.log('收到 App 数据1:', data);

// 可选:发送数据回 uni-app
uni.postMessage({
data: {
response: 'Data received!'
}
});
//范围自适应
if (map) {
var coords = [
[113.945899, 22.525566],
[113.940964, 22.529943],
[113.93914, 22.527132],
[113.942594, 22.525029]
];
new SFMap.Polygon({
coordinates: coords,
paint: {
"fill-color": "#088",
"fill-opacity": 0.8
}
}).addTo(map);
// 计算bounds
var bounds = coords.reduce(function(rtn, item) {
rtn.extend(item);
return rtn;
}, new SFMap.LngLatBounds());

// 地图fitBounds
if (!bounds.isEmpty()) {
map.fitBounds(bounds, {
padding: {
top: 0,
bottom: 0,
left: 300,
right: 0,
},
maxZoom: 14
});
}
}
}
map = new SFMap.Map({
container: 'map',
center: [113.99709, 22.56859],
zoom: 9 // starting zoom
});
document.addEventListener('UniAppJSBridgeReady', function() {

console.log("map", map)

uni.postMessage({
data: {
action: 'load'
}
});

document.querySelector('.btn-list').addEventListener('click', function(evt) {
var target = evt.target;
if (target.tagName === 'BUTTON') {
var action = target.getAttribute('data-action');
switch (action) {
case 'switchTab':
uni.switchTab({
url: '/pages/tabBar/API/API'
});
break;
case 'reLaunch':
uni.reLaunch({
url: '/pages/tabBar/API/API'
});
break;
case 'navigateBack':
uni.navigateBack({
delta: 1
});
break;
default:
uni[action]({
url: '/pages/component/button/button'
});
break;
}
}
});
document.querySelector("#postMessage").addEventListener('click', function() {

uni.postMessage({
data: {
action: 'message'
}
});
})
});
</script>
</body>
</html>

uniapp本地测试页面

<template>
<view>
<web-view style="height: 200px;width: 100%;" src="/hybrid/html/local.html" ref="myWebview" :webview-styles="webviewStyles" @message="getMessage"
id="webviewId" @load="webLoad"></web-view>
<button type="default" style="position: fixed;botttom:0;margin-top: 300px;" @click="sendData">发送消息给html</button>
</view>
</template>

<script>
export default {

data() {
return {
currentWebview: '',
webviewStyles: {
progress: {
color: '#ffffff'
},
}
}
},
onReady() {
var that = this


setTimeout(function() {
that.initWebView()
}, 1000); //如果是页面初始化调用时,需要延时一下

},
methods: {

initWebView() {
if (!this.currentWebview) {
// #ifdef APP-HARMONY
this.currentWebview = uni.createWebviewContext('webviewId', this);
// #endif  
// #ifdef APP-PLUS  
this.currentWebview = this.$scope.$getAppWebview().children()[0];
// #endif  

// #ifdef APP-PLUS
this.currentWebview.setStyle({
top: 0,
height: 300
})
// #endif
}

},

webLoad() {
//微信小程序、支付宝小程序、抖音小程序、QQ小程序、H5
console.log("-------webLoad----------")
},
getMessage(e) {
console.log("-------getMessage--------")
uni.showModal({
content: JSON.stringify(e.detail),
showCancel: false
})
this.initWebView()
},
sendData() {
var data = {
log: '1',
lat: '2'
}
if (this.currentWebview) {
if (this.currentWebview.evalJS) {
this.currentWebview.evalJS(`receiveDataFromApp(${JSON.stringify(data)})`);
} else {
console.error(" evalJS 方法不存在");
}

} else {
console.error("WebView 实例 不存在");
}
// 方式1:直接调用 WebView 中的全局函数(推荐)
// webview.evalJS(`receiveDataFromApp(${JSON.stringify(data)})`);

// 方式2:通过 window 对象传递
// webview.evalJS(`window.appData = ${JSON.stringify(data)}`);
}

}
}
</script>

<style>

</style>

markdown预览自定义扩展实现

作者 gnip
2025年7月7日 13:58

概述

在CSDN文章和AI问答的产生的回答,都是markdown格式的文本,如果浏览器需要渲染对应的markdown文件,需要能够正确将markdown文件转化HTML字符串,这样才能被浏览器识别,比较常见的转化markdown文件的库有诸如:markedjs、markdown-it等,这些库能够将markdown转化为浏览器能够识别的标签内容,但是里面的代码块样式需要我们自定义处理,可以结合highlight.js进行高亮和主题定制。如果需要在markdown文件转化过程中,自定义部分响应内容,需要使用到对应的扩展。

效果

实现类似CSDN的代码块复制功能,如下在原markdown的文本基础上,扩展自定义的一部分内容,比如点击下面复制,可以复制对应代码块的内容

演示效果.gif

实现

需要使用到的库

  • marked:转化markdown文本
  • marked-highlight:marked的扩展,用于定制高亮样式
  • highlight.js:高亮代码块

具体实现

这里结合vue代码来实现,代码块上面的头部,我们定义成组件,通过组件挂载的形式注入到最后生成的markdown渲染的html片段中去。 App.vue

<script setup lang="ts">
import { Marked, marked } from "marked";
import { markedHighlight } from "marked-highlight";
import { ref, createApp, nextTick, h } from "vue";
import HeaderCode from "./components/HeaderCode.vue";
import { str, codeStr } from "./test.js";
import { v1 } from "uuid";
import hljs from "highlight.js";
import "highlight.js/styles/github-dark.min.css";

window.myNamespace.age = 12;

const markdownContent = ref(null);


function handleTest() {
    //高亮主题配置
  const marked = new Marked(
    markedHighlight({
      emptyLangClass: "hljs",
      langPrefix: "hljs language-",
      highlight(code, lang, info) {
        console.log("lang", lang, info);
        const language = hljs.getLanguage(lang) ? lang : "plaintext";
        return hljs.highlight(code, { language }).value;
      },
    })
  );
  //自定义代码块的markdown渲染逻辑
  const renderer = {
    code(data) {
      const app = createApp({
        render() {
          return h(HeaderCode,{data});
        },
      });
      //拦截code返回的内容,然后自定义注入头部组件
      const parentNodeId = "code-header-" + v1();
      nextTick(() => {
        const container = document.querySelector(`#${parentNodeId}`);
        console.log("container", container);
        app.mount(container);
      });
    //返回渲染的内容
      return `
        <div id="${parentNodeId}" class="code-header">
        </div>
      <pre><code class="hljs language-js">${data.text}</code></pre>
      `;
    },
  };

  marked.use({ renderer });

  const parseStr = marked.parse(str);
  markdownContent.value.innerHTML = parseStr;
}
</script>

<template>
  <el-button @click="handleTest">模拟</el-button>

  <div class="markdown-content" ref="markdownContent"></div>
</template>

<style>
.code-header {
  justify-content: space-between;
  align-items: center;
  width: 100%;
  padding: 6px 14px 6px 6px;
  display: flex;
  background-color: #f5f5f5;
}
.copy-code {
  cursor: pointer;
}

#app {
  height: 100%;
  padding: 10px;
}

body {
  width: 100%;
  height: 100%;
  display: block;
}
</style>

HeaderCode.vue

<script setup lang="ts">
import { ElMessage } from "element-plus";

const props = defineProps<{ data: any }>();
const downLoad = () => {
  console.log("downLoad", props.data);

  const content = props.data.raw.replace(/```/g, "");

  console.log("content", content);

  const reg = new RegExp(props.data.lang);

  console.log("res--code", content.replace(reg, ""));
  ElMessage.success("复制成功");
};
</script>
<template>
  <div class="code-title">{{ props.data.lang }}</div>
  <div class="copy-code" @click="downLoad">复制</div>
</template>

总结

上面只拦截了code块的自定义内容,其他的,比如想要自定义标题、列表、图片等等的渲染逻辑,可以通过对应扩展进行处理。

相关链接

vue学习路线(11.watch对比computed)

2025年7月7日 13:55

一、通过案例对比computedwatch

姓名案例(输入框分别输入姓和名,全名展示姓名的组合)

image.png

1.通过computed实现

  <body>
    <div id="app">
      姓:<input v-model="firstName" /><br /><br />
      名:<input v-model="lastName" /><br /><br />
      全名:<span>{{fullName}} </span>
    </div>
    <script type="text/javascript">
      const vm = new Vue({
        el: "#app",
        data() {
          return {
            firstName: "张",
            lastName: "三",
          };
        },
        computed: {
          fullName() {
            return this.firstName + "-" + this.lastName;
          },
        },
      });
    </script>
  </body>

2.通过watch实现

  <body>
    <div id="app">
      姓:<input v-model="firstName" /><br /><br />
      名:<input v-model="lastName" /><br /><br />
      全名:<span>{{fullName}} </span>
    </div>
    <script type="text/javascript">
      const vm = new Vue({
        el: "#app",
        data() {
          return {
            firstName: "张",
            lastName: "三",
            fullName: "",
          };
        },
        watch: {
          firstName(val) {
            this.fullName = val + "-" + this.lastName;
          },
          lastName(val) {
            this.fullName = this.firstName + "-" + val;
          },
        },
      });
    </script>
  </body>

二、总结computedwatch之间的区别

    1. computed能完成的功能,watch都可以完成。
    1. watch能完成的功能,computed不一定能完成,例如:watch可以进行异步操作。

三、两个重要的小原则

    1. 所被Vue管理的函数,最好写成普通函数,这样this的指向才是vm 或 组件实例对象。
    1. 所有不被vue所管理的函数 (定时器的回调函数、ajax的回调函数、Promise的回调函数等) ,最好写成箭头函数。这样this的指向才是vm或组件实例对象。

Zustand 状态管理库完全指南 - 进阶篇

作者 旧时光_
2025年7月7日 13:40

🐻 Zustand 状态管理库完全指南 - 进阶篇

深入掌握 Zustand 的高级特性和最佳实践

📖 前言

在基础篇中,我们学习了 Zustand 的基本概念、安装使用、核心 API 和基础用法。现在是时候深入了解 Zustand 的高级特性了!

进阶篇将带你掌握异步操作、中间件系统、错误处理、性能优化等高级技巧,让你能够在实际项目中游刃有余地使用 Zustand。

📚 目录

进阶篇

  1. 异步操作处理
  2. 中间件系统深入
  3. 高级用法和技巧
  4. 错误处理和调试
  5. 最佳实践指南
  6. 与其他状态管理库对比
  7. 完整实战案例
  8. 常见问题解答

1. 异步操作处理

异步操作是现代 Web 应用的核心组成部分。无论是 API 调用、文件上传、还是数据库操作,我们都需要优雅地处理异步逻辑。Zustand 在异步操作方面提供了出色的支持,让复杂的异步状态管理变得简单直观。

异步操作的常见挑战

  1. 加载状态管理:用户需要知道操作正在进行
  2. 错误处理:网络失败、服务器错误等需要妥善处理
  3. 数据同步:确保 UI 显示的数据是最新的
  4. 竞态条件:避免过时的请求覆盖新的数据
  5. 用户体验:提供及时的反馈和重试机制

Zustand 的异步优势

  • 🎯 直观的 API:直接使用 async/await,无需额外的库
  • 🚀 内置状态管理:loading、error、data 状态的统一管理
  • 🔄 灵活的更新策略:支持乐观更新、悲观更新等模式
  • 🛡️ 错误恢复:内置的错误处理和恢复机制

1. 基础异步操作

在 Zustand 中处理异步操作非常简单,你可以直接在 store 的方法中使用 async/await。让我们从一个用户管理系统开始,逐步学习异步操作的最佳实践。

定义异步状态的数据结构

首先,我们需要定义清晰的类型结构来管理异步状态:

interface User {
  id: string
  name: string
  email: string
  avatar?: string
}

interface UserState {
  // 数据状态
  user: User | null
  users: User[]
  
  // 异步状态
  loading: boolean
  error: string | null
  
  // 异步方法
  fetchUser: (id: string) => Promise<void>
  fetchUsers: () => Promise<void>
  createUser: (userData: Omit<User, 'id'>) => Promise<void>
  updateUser: (id: string, userData: Partial<User>) => Promise<void>
  deleteUser: (id: string) => Promise<void>
}

异步状态设计要点

  • 数据分离:将实际数据(user, users)与异步状态(loading, error)分开
  • 统一错误处理:使用统一的 error 字段管理所有错误
  • 加载状态loading 字段提供用户反馈
  • 类型安全:使用 TypeScript 确保类型正确性
实现基础的异步操作

现在让我们实现基础的 CRUD 操作:

const useUserStore = create<UserState>((set, get) => ({
  // 初始状态
  user: null,
  users: [],
  loading: false,
  error: null,
  
  // 获取单个用户
  fetchUser: async (id: string) => {
    // 第一步:设置加载状态
    set({ loading: true, error: null }, false, 'fetchUser/start')
    
    try {
      const response = await fetch(`/api/users/${id}`)
      
      // 检查响应状态
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: 获取用户失败`)
      }
      
      const user = await response.json()
      
      // 第二步:成功时更新数据
      set({ 
        user, 
        loading: false 
      }, false, 'fetchUser/success')
      
    } catch (error) {
      // 第三步:失败时设置错误状态
      set({ 
        error: error instanceof Error ? error.message : '获取用户失败',
        loading: false 
      }, false, 'fetchUser/error')
    }
  },
}))

异步操作的三个关键步骤

  1. 开始阶段:设置 loading: true,清除之前的错误
  2. 成功阶段:更新数据,设置 loading: false
  3. 失败阶段:设置错误信息,设置 loading: false

这种模式确保了用户始终能够了解操作的当前状态,避免了界面卡死或用户不知道发生了什么的情况。通过统一的错误处理,我们可以在一个地方管理所有可能出现的异常情况。

获取列表数据

列表数据的获取是最常见的异步操作,也是大多数应用的核心功能。与获取单个数据不同,列表获取需要考虑更多的边界情况,比如空列表、分页、排序等。让我们看看如何正确实现:

// 在 useUserStore 中继续添加
fetchUsers: async () => {
  set({ loading: true, error: null }, false, 'fetchUsers/start')
  
  try {
    const response = await fetch('/api/users')
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: 获取用户列表失败`)
    }
    
    const users = await response.json()
    
    set({ 
      users, 
      loading: false 
    }, false, 'fetchUsers/success')
    
  } catch (error) {
    set({ 
      error: error instanceof Error ? error.message : '获取用户列表失败',
      loading: false 
    }, false, 'fetchUsers/error')
  }
},

列表获取的注意事项

  • 数据验证:确保返回的数据格式正确
  • 空状态处理:考虑列表为空的情况
  • 分页支持:大型列表应该考虑分页

在实际项目中,列表数据往往是用户最常接触的内容。一个好的列表加载体验应该包括:加载骨架屏、错误重试机制、空状态提示等。这些细节决定了用户体验的好坏。

创建新数据

创建操作是用户与应用交互的重要环节,它不仅要保证数据的正确性,还要提供良好的用户反馈。在 Zustand 中,我们可以采用乐观更新的策略,让用户感觉操作响应迅速。创建操作需要特别注意状态的更新方式:

createUser: async (userData: Omit<User, 'id'>) => {
  set({ loading: true, error: null }, false, 'createUser/start')
  
  try {
    const response = await fetch('/api/users', {
      method: 'POST',
      headers: { 
        'Content-Type': 'application/json',
        // 可以添加认证头
        // 'Authorization': `Bearer ${token}`
      },
      body: JSON.stringify(userData)
    })
    
    if (!response.ok) {
      const errorData = await response.json()
      throw new Error(errorData.message || '创建用户失败')
    }
    
    const newUser = await response.json()
    
    // 重要:更新现有列表,而不是重新获取
    set((state) => ({
      users: [...state.users, newUser],
      loading: false
    }), false, 'createUser/success')
    
  } catch (error) {
    set({ 
      error: error instanceof Error ? error.message : '创建用户失败',
      loading: false 
    }, false, 'createUser/error')
  }
},

创建操作的最佳实践

  • 乐观更新:立即更新本地状态,无需重新获取列表
  • 错误处理:解析服务器返回的具体错误信息
  • 数据验证:确保新创建的数据符合预期格式

乐观更新是现代 Web 应用的重要特性,它让用户感觉应用响应迅速。但同时也要处理好失败的情况,确保数据的一致性。在创建操作中,我们通常会立即将新数据添加到本地列表,然后发送请求到服务器。如果服务器返回错误,我们需要将本地状态回滚到之前的状态。

更新和删除操作

更新和删除操作比创建操作更复杂,因为它们涉及到现有数据的修改。这些操作需要考虑数据的一致性、并发修改、以及失败时的回滚策略。更新和删除操作需要特别注意状态同步:

// 更新用户信息
updateUser: async (id: string, userData: Partial<User>) => {
  set({ loading: true, error: null }, false, 'updateUser/start')
  
  try {
    const response = await fetch(`/api/users/${id}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(userData)
    })
    
    if (!response.ok) {
      const errorData = await response.json()
      throw new Error(errorData.message || '更新用户失败')
    }
    
    const updatedUser = await response.json()
    
    // 同时更新列表和单个用户状态
    set((state) => ({
      users: state.users.map(user => 
        user.id === id ? updatedUser : user
      ),
      user: state.user?.id === id ? updatedUser : state.user,
      loading: false
    }), false, 'updateUser/success')
    
  } catch (error) {
    set({ 
      error: error instanceof Error ? error.message : '更新用户失败',
      loading: false 
    }, false, 'updateUser/error')
  }
},

// 删除用户
deleteUser: async (id: string) => {
  set({ loading: true, error: null }, false, 'deleteUser/start')
  
  try {
    const response = await fetch(`/api/users/${id}`, {
      method: 'DELETE'
    })
    
    if (!response.ok) {
      throw new Error('删除用户失败')
    }
    
    // 从所有相关状态中移除用户
    set((state) => ({
      users: state.users.filter(user => user.id !== id),
      user: state.user?.id === id ? null : state.user,
      loading: false
    }), false, 'deleteUser/success')
    
  } catch (error) {
    set({ 
      error: error instanceof Error ? error.message : '删除用户失败',
      loading: false 
    }, false, 'deleteUser/error')
  }
},

更新和删除的关键点

  • 状态一致性:确保所有相关状态都得到正确更新
  • 级联更新:删除用户时,要清理所有相关引用
  • 原子操作:整个操作要么全部成功,要么全部失败

在实际应用中,更新和删除操作往往涉及到多个相关的数据结构。比如删除一个用户时,可能需要同时清理该用户的所有相关数据。这就要求我们在设计状态结构时就要考虑到这些关联关系,确保操作的完整性。

2. 在组件中使用异步操作

将异步操作集成到 React 组件中是状态管理的重要环节。组件不仅要展示数据,还要处理加载状态、错误状态、用户交互等多种情况。一个设计良好的组件应该能够优雅地处理所有这些状态变化。组件中使用异步操作需要处理多种状态和用户交互:

import React, { useEffect, useState } from 'react'

function UserList() {
  const { 
    users, 
    loading, 
    error, 
    fetchUsers, 
    deleteUser 
  } = useUserStore()
  
  // 组件挂载时获取数据
  useEffect(() => {
    fetchUsers()
  }, [fetchUsers])
  
  const handleDelete = async (id: string) => {
    if (confirm('确定要删除这个用户吗?')) {
      await deleteUser(id)
    }
  }
  
  // 处理加载状态
  if (loading) {
    return (
      <div className="loading-container">
        <div className="spinner"></div>
        <p>加载中...</p>
      </div>
    )
  }
  
  // 处理错误状态
  if (error) {
    return (
      <div className="error-container">
        <h3>❌ 出现错误</h3>
        <p>{error}</p>
        <button onClick={fetchUsers} className="retry-button">
          🔄 重试
        </button>
      </div>
    )
  }
  
  return (
    <div className="user-list">
      <h2>用户列表</h2>
      
      {users.length === 0 ? (
        <div className="empty-state">
          <p>暂无用户数据</p>
          <button onClick={fetchUsers}>刷新</button>
        </div>
      ) : (
        <div className="user-grid">
          {users.map(user => (
            <div key={user.id} className="user-card">
              <h3>{user.name}</h3>
              <p>{user.email}</p>
              {user.avatar && (
                <img src={user.avatar} alt={user.name} />
              )}
              <div className="actions">
                <button 
                  onClick={() => handleDelete(user.id)}
                  className="delete-button"
                >
                  删除
                </button>
              </div>
            </div>
          ))}
        </div>
      )}
    </div>
  )
}

组件异步处理的最佳实践

  • 状态驱动渲染:根据 loading、error、data 状态渲染不同 UI
  • 用户反馈:提供清晰的加载和错误提示
  • 重试机制:允许用户在失败时重试操作
  • 确认操作:对于危险操作(如删除)提供确认机制

这个示例展示了一个完整的异步数据处理流程。注意我们如何根据不同的状态渲染不同的 UI,这种模式被称为"状态驱动渲染"。通过清晰地区分加载、错误和成功状态,用户始终能够了解当前的操作状态,从而提供更好的用户体验。

3. 处理并发请求

并发请求是现代 Web 应用中的常见场景,特别是在用户快速操作或网络状况不稳定的情况下。想象一个搜索场景:用户在搜索框中快速输入,每次输入都会触发一个 API 请求。如果网络延迟不同,后发出的请求可能先返回,导致显示错误的搜索结果。在现代 Web 应用中,用户可能会快速触发多个异步请求。如果不正确处理,可能会导致数据不一致或显示过时的信息。

并发请求的问题

典型场景:用户在搜索框中快速输入,每次输入都触发一个 API 请求。如果网络延迟不同,后发出的请求可能先返回,导致显示错误的结果。

请求 ID 跟踪方案

最常用的解决方案是为每个请求分配唯一 ID,只处理最新请求的结果:

interface DataState {
  data: any[] | null
  loading: boolean
  error: string | null
  currentRequestId: string | null
  
  fetchData: (params: Record<string, string>) => Promise<void>
  cancelRequest: () => void
}

const useDataStore = create<DataState>((set, get) => ({
  data: null,
  loading: false,
  error: null,
  currentRequestId: null,
  
  fetchData: async (params) => {
    // 第一步:生成唯一请求 ID
    const requestId = `req_${Date.now()}_${Math.random()}`
    
    set({ 
      loading: true, 
      error: null,
      currentRequestId: requestId
    }, false, 'fetchData/start')
    
    try {
      const response = await fetch(`/api/data?${new URLSearchParams(params)}`)
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`)
      }
      
      const data = await response.json()
      
      // 第二步:检查是否仍是最新请求
      const { currentRequestId } = get()
      if (currentRequestId === requestId) {
        set({ 
          data, 
          loading: false 
        }, false, 'fetchData/success')
      } else {
        console.log('忽略过时的请求结果:', requestId)
      }
      
    } catch (error) {
      // 错误处理也要检查请求 ID
      const { currentRequestId } = get()
      if (currentRequestId === requestId) {
        set({ 
          error: error instanceof Error ? error.message : '请求失败', 
          loading: false 
        }, false, 'fetchData/error')
      }
    }
  },
}))

请求 ID 方案的优点

  • 简单有效:只需要一个 ID 字段就能解决并发问题
  • 性能友好:不需要取消网络请求,只是忽略结果
  • 状态清晰:总是显示最新请求的状态

请求 ID 方案是处理并发请求最简单直接的方法。它的核心思想是为每个请求分配一个唯一标识符,然后只处理最新请求的结果。这种方法的优点是实现简单,不需要复杂的取消逻辑,但缺点是过时的请求仍然会消耗网络资源。

使用 AbortController 取消请求

对于需要节省网络资源的场景,我们可以使用 AbortController API 来真正取消网络请求。这是一个更高级但也更高效的方案,特别适合于数据量大或请求频繁的场景。更高级的方案是使用 AbortController 真正取消网络请求:

const useAdvancedDataStore = create((set, get) => ({
  data: null,
  loading: false,
  error: null,
  abortController: null as AbortController | null,
  
  fetchData: async (params: Record<string, string>) => {
    // 取消之前的请求
    const { abortController } = get()
    if (abortController) {
      abortController.abort()
    }
    
    // 创建新的 AbortController
    const newController = new AbortController()
    
    set({ 
      loading: true, 
      error: null,
      abortController: newController
    }, false, 'fetchData/start')
    
    try {
      const response = await fetch(`/api/data?${new URLSearchParams(params)}`, {
        signal: newController.signal  // 关键:传入 signal
      })
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`)
      }
      
      const data = await response.json()
      
      set({ 
        data, 
        loading: false,
        abortController: null
      }, false, 'fetchData/success')
      
    } catch (error) {
      // 检查是否是因为取消导致的错误
      if (error.name === 'AbortError') {
        console.log('请求已被取消')
        return
      }
      
      set({ 
        error: error instanceof Error ? error.message : '请求失败', 
        loading: false,
        abortController: null
      }, false, 'fetchData/error')
    }
  },
  
  // 手动取消请求
  cancelRequest: () => {
    const { abortController } = get()
    if (abortController) {
      abortController.abort()
      set({ 
        loading: false, 
        abortController: null 
      }, false, 'fetchData/cancel')
    }
  },
}))

AbortController 方案的优点

  • 节省资源:真正取消网络请求,节省带宽
  • 更快响应:新请求不需要等待旧请求完成
  • 用户体验:可以提供取消按钮给用户

AbortController 是现代浏览器提供的标准 API,它允许我们主动取消正在进行的网络请求。这对于移动端应用特别重要,因为移动网络环境更不稳定,用户也更关心流量消耗。通过及时取消无用的请求,我们可以显著提升应用的性能和用户体验。

4. 缓存和去重

缓存是现代 Web 应用性能优化的核心策略之一。合理的缓存策略可以减少网络请求、提升响应速度、改善用户体验。在 Zustand 中实现缓存相对简单,我们可以利用 JavaScript 的 Map 或普通对象来存储缓存数据。缓存是提高应用性能的重要手段,特别是对于不经常变化的数据。去重则可以避免重复的网络请求。

基础缓存实现

首先实现一个简单的内存缓存:

interface CacheItem {
  data: any
  timestamp: number
  expiresAt: number
}

interface CacheState {
  cache: Map<string, CacheItem>
  pendingRequests: Map<string, Promise<any>>
  
  fetchWithCache: (url: string, options?: RequestInit) => Promise<any>
  clearCache: () => void
  clearExpiredCache: () => void
}
缓存逻辑实现
const useApiStore = create<CacheState>((set, get) => ({
  cache: new Map(),
  pendingRequests: new Map(),
  
  fetchWithCache: async (url: string, options = {}) => {
    const { cache, pendingRequests } = get()
    
    // 第一步:检查缓存
    if (cache.has(url)) {
      const cachedItem = cache.get(url)!
      const now = Date.now()
      
      // 检查缓存是否过期
      if (now < cachedItem.expiresAt) {
        console.log('从缓存返回数据:', url)
        return cachedItem.data
      } else {
        // 删除过期缓存
        set((state) => {
          const newCache = new Map(state.cache)
          newCache.delete(url)
          return { cache: newCache }
        })
      }
    }
    
    // 第二步:检查是否有正在进行的请求(去重)
    if (pendingRequests.has(url)) {
      console.log('返回正在进行的请求:', url)
      return pendingRequests.get(url)
    }
    
    // 第三步:创建新请求
    const requestPromise = this.createRequest(url, options)
    
    // 存储正在进行的请求
    set((state) => {
      const newPendingRequests = new Map(state.pendingRequests)
      newPendingRequests.set(url, requestPromise)
      return { pendingRequests: newPendingRequests }
    })
    
    return requestPromise
  },
  
  // 创建请求的辅助方法
  createRequest: async (url: string, options: RequestInit) => {
    try {
      const response = await fetch(url, options)
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`)
      }
      
      const data = await response.json()
      
      // 成功后更新缓存
      set((state) => {
        const newCache = new Map(state.cache)
        const newPendingRequests = new Map(state.pendingRequests)
        
        // 添加到缓存(5分钟过期)
        newCache.set(url, {
          data,
          timestamp: Date.now(),
          expiresAt: Date.now() + 5 * 60 * 1000
        })
        
        // 清理待处理请求
        newPendingRequests.delete(url)
        
        return {
          cache: newCache,
          pendingRequests: newPendingRequests
        }
      })
      
      return data
      
    } catch (error) {
      // 失败时清理待处理请求
      set((state) => {
        const newPendingRequests = new Map(state.pendingRequests)
        newPendingRequests.delete(url)
        return { pendingRequests: newPendingRequests }
      })
      
      throw error
    }
  },
}))
高级缓存功能
// 扩展缓存功能
const useAdvancedCacheStore = create((set, get) => ({
  cache: new Map(),
  pendingRequests: new Map(),
  
  // 带自定义过期时间的缓存
  fetchWithCustomCache: async (url: string, expirationMs = 5 * 60 * 1000) => {
    // 实现逻辑...
  },
  
  // 清理过期缓存
  clearExpiredCache: () => {
    const { cache } = get()
    const now = Date.now()
    
    set((state) => {
      const newCache = new Map()
      
      for (const [key, item] of state.cache) {
        if (now < item.expiresAt) {
          newCache.set(key, item)
        }
      }
      
      return { cache: newCache }
    })
  },
  
  // 预加载数据
  preload: async (urls: string[]) => {
    const promises = urls.map(url => get().fetchWithCache(url))
    await Promise.allSettled(promises)
  },
  
  // 获取缓存统计
  getCacheStats: () => {
    const { cache } = get()
    const now = Date.now()
    let validCount = 0
    let expiredCount = 0
    
    for (const item of cache.values()) {
      if (now < item.expiresAt) {
        validCount++
      } else {
        expiredCount++
      }
    }
    
    return { validCount, expiredCount, totalSize: cache.size }
  },
  
  // 完全清理缓存
  clearCache: () => {
    set({ 
      cache: new Map(),
      pendingRequests: new Map()
    })
  },
}))

缓存策略的最佳实践

  • 合理的过期时间:根据数据更新频率设置过期时间
  • 内存管理:定期清理过期缓存,避免内存泄漏
  • 缓存键设计:使用有意义的键,考虑参数变化
  • 错误处理:缓存失败不应影响正常功能

缓存系统的设计需要在性能和数据新鲜度之间找到平衡。过期时间太短会导致频繁的网络请求,过期时间太长则可能显示过时的数据。在实际项目中,我们通常会根据不同类型的数据设置不同的缓存策略,比如用户信息可以缓存较长时间,而实时数据则需要较短的缓存时间。

5. 乐观更新

乐观更新是现代用户界面设计的重要理念,它基于"大多数操作都会成功"的假设来提升用户体验。这种技术让用户感觉应用响应迅速,即使在网络较慢的情况下也能提供流畅的交互体验。但是,乐观更新也带来了复杂性,我们需要处理失败情况下的状态回滚。乐观更新是一种用户体验优化技术,它假设操作会成功,先更新 UI,然后再发送请求。如果操作失败,则回滚到之前的状态。

乐观更新的原理

传统方式:用户操作 → 发送请求 → 等待响应 → 更新 UI 乐观更新:用户操作 → 立即更新 UI → 发送请求 → 如果失败则回滚

添加数据的乐观更新
interface Todo {
  id: string
  text: string
  completed: boolean
  pending?: boolean  // 标记是否为临时数据
}

interface OptimisticState {
  todos: Todo[]
  
  addTodoOptimistic: (todoText: string) => Promise<void>
  deleteTodoOptimistic: (id: string) => Promise<void>
  updateTodoOptimistic: (id: string, updates: Partial<Todo>) => Promise<void>
}

const useOptimisticStore = create<OptimisticState>((set, get) => ({
  todos: [],
  
  addTodoOptimistic: async (todoText: string) => {
    // 第一步:生成临时 ID 和乐观数据
    const tempId = `temp-${Date.now()}-${Math.random()}`
    const optimisticTodo: Todo = {
      id: tempId,
      text: todoText,
      completed: false,
      pending: true  // 标记为待处理状态
    }
    
    // 第二步:立即更新 UI
    set((state) => ({
      todos: [...state.todos, optimisticTodo]
    }), false, 'addTodo/optimistic')
    
    try {
      // 第三步:发送实际请求
      const response = await fetch('/api/todos', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ text: todoText })
      })
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: 添加失败`)
      }
      
      const realTodo = await response.json()
      
      // 第四步:用真实数据替换临时数据
      set((state) => ({
        todos: state.todos.map(todo =>
          todo.id === tempId 
            ? { ...realTodo, pending: false } 
            : todo
        )
      }), false, 'addTodo/confirmed')
      
    } catch (error) {
      // 第五步:失败时回滚
      set((state) => ({
        todos: state.todos.filter(todo => todo.id !== tempId)
      }), false, 'addTodo/rollback')
      
      // 通知用户失败
      console.error('添加待办事项失败:', error)
      // 可以使用 toast 或其他方式通知用户
    }
  },
}))

乐观添加的关键点

  • 临时 ID:使用唯一的临时 ID 标识乐观数据
  • pending 标记:用于 UI 显示加载状态
  • 原子回滚:失败时完全移除临时数据
删除数据的乐观更新
deleteTodoOptimistic: async (id: string) => {
  const { todos } = get()
  const todoToDelete = todos.find(todo => todo.id === id)
  
  // 检查待删除的项是否存在
  if (!todoToDelete) {
    console.warn('要删除的待办事项不存在:', id)
    return
  }
  
  // 第一步:立即从 UI 中移除
  set((state) => ({
    todos: state.todos.filter(todo => todo.id !== id)
  }), false, 'deleteTodo/optimistic')
  
  try {
    // 第二步:发送删除请求
    const response = await fetch(`/api/todos/${id}`, {
      method: 'DELETE'
    })
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: 删除失败`)
    }
    
    // 第三步:删除成功,无需额外操作
    console.log('待办事项删除成功:', id)
    
  } catch (error) {
    // 第四步:失败时恢复数据
    set((state) => {
      // 将删除的项插入回原位置
      const newTodos = [...state.todos, todoToDelete]
      // 按某种规则排序,保持列表顺序
      newTodos.sort((a, b) => {
        // 假设按创建时间排序
        return new Date(a.createdAt || 0).getTime() - new Date(b.createdAt || 0).getTime()
      })
      return { todos: newTodos }
    }, false, 'deleteTodo/rollback')
    
    console.error('删除待办事项失败:', error)
  }
},
更新数据的乐观更新
updateTodoOptimistic: async (id: string, updates: Partial<Todo>) => {
  const { todos } = get()
  const originalTodo = todos.find(todo => todo.id === id)
  
  if (!originalTodo) {
    console.warn('要更新的待办事项不存在:', id)
    return
  }
  
  // 第一步:立即更新 UI
  set((state) => ({
    todos: state.todos.map(todo =>
      todo.id === id 
        ? { ...todo, ...updates, pending: true }
        : todo
    )
  }), false, 'updateTodo/optimistic')
  
  try {
    // 第二步:发送更新请求
    const response = await fetch(`/api/todos/${id}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(updates)
    })
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: 更新失败`)
    }
    
    const updatedTodo = await response.json()
    
    // 第三步:用服务器返回的数据替换
    set((state) => ({
      todos: state.todos.map(todo =>
        todo.id === id 
          ? { ...updatedTodo, pending: false }
          : todo
      )
    }), false, 'updateTodo/confirmed')
    
  } catch (error) {
    // 第四步:失败时恢复原始数据
    set((state) => ({
      todos: state.todos.map(todo =>
        todo.id === id ? originalTodo : todo
      )
    }), false, 'updateTodo/rollback')
    
    console.error('更新待办事项失败:', error)
  }
},

乐观更新的最佳实践

  • 保存原始数据:删除和更新操作前要保存原始数据用于回滚
  • 视觉反馈:使用 pending 状态提供视觉反馈
  • 错误处理:优雅地处理失败情况,提供用户友好的错误信息
  • 数据一致性:确保回滚后的数据与原始状态完全一致
在组件中使用乐观更新
function TodoList() {
  const { todos, addTodoOptimistic, deleteTodoOptimistic, updateTodoOptimistic } = useOptimisticStore()
  const [newTodoText, setNewTodoText] = useState('')
  
  const handleAddTodo = async () => {
    if (newTodoText.trim()) {
      await addTodoOptimistic(newTodoText.trim())
      setNewTodoText('')
    }
  }
  
  const handleToggleTodo = async (id: string, completed: boolean) => {
    await updateTodoOptimistic(id, { completed: !completed })
  }
  
  return (
    <div>
      <div>
        <input 
          value={newTodoText}
          onChange={(e) => setNewTodoText(e.target.value)}
          placeholder="添加新待办事项"
        />
        <button onClick={handleAddTodo}>添加</button>
      </div>
      
      <ul>
        {todos.map(todo => (
          <li key={todo.id} className={todo.pending ? 'pending' : ''}>
            <input 
              type="checkbox"
              checked={todo.completed}
              onChange={() => handleToggleTodo(todo.id, todo.completed)}
            />
            <span>{todo.text}</span>
            {todo.pending && <span className="spinner"></span>}
            <button onClick={() => deleteTodoOptimistic(todo.id)}>
              删除
            </button>
          </li>
        ))}
      </ul>
    </div>
  )
}

乐观更新的优势

  • 即时响应:用户操作后立即看到结果
  • 流畅体验:减少等待时间,提升用户体验
  • 网络容错:在网络较慢时仍能提供良好体验

2. 中间件系统深入

中间件是 Zustand 生态系统的重要组成部分,它们扩展了 Zustand 的核心功能,提供了状态持久化、开发工具集成、不可变更新等高级特性。理解和掌握中间件系统对于构建复杂应用至关重要。

什么是中间件?

中间件是一种设计模式,它允许你在状态更新的过程中插入自定义逻辑。在 Zustand 中,中间件可以:

  1. 拦截状态更新:在状态改变前后执行自定义逻辑
  2. 增强功能:为 store 添加新的能力(如持久化、调试等)
  3. 组合使用:多个中间件可以组合使用,形成强大的功能链
  4. 保持简洁:不改变核心 API,保持 Zustand 的简洁性

Zustand 官方中间件

  • 🛠️ devtools:Redux DevTools 集成
  • 💾 persist:状态持久化
  • 🔄 immer:不可变更新简化
  • 📝 subscribeWithSelector:选择器订阅

中间件的优势

  • 功能扩展:无需修改核心代码即可添加新功能
  • 🔧 模块化设计:每个中间件专注于特定功能
  • 🎯 按需使用:只加载需要的中间件,保持包体积小
  • 🔄 可组合性:多个中间件可以灵活组合

1. DevTools 中间件详解

DevTools 中间件是开发过程中最有用的工具之一,它让你能够在 Redux DevTools 中调试 Zustand 状态,提供了时间旅行、状态检查、动作回放等强大功能。

Redux DevTools 是前端开发者必备的调试工具,它原本是为 Redux 设计的,但通过 Zustand 的 DevTools 中间件,我们也可以享受到同样强大的调试体验。这个工具可以帮助我们:

  • 可视化状态变化:清楚地看到每次状态更新的详细信息
  • 时间旅行调试:可以回到任何一个历史状态,方便定位问题
  • 动作追踪:每个状态变化都会显示对应的动作名称
  • 状态导入导出:可以保存和恢复特定的应用状态
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'

// 基础 DevTools 配置
const useCounterStore = create<CounterState>()(
  devtools(
    (set, get) => ({
      count: 0,
      
      // 在 DevTools 中显示动作名称
      increment: () => set(
        (state) => ({ count: state.count + 1 }),
        false,  // 不替换整个状态
        'increment'  // 动作名称
      ),
      
      decrement: () => set(
        (state) => ({ count: state.count - 1 }),
        false,
        'decrement'
      ),
      
      // 复杂操作的调试
      complexOperation: () => {
        const { count } = get()
        
        set(
          { count: count * 2 },
          false,
          'complexOperation/double'
        )
        
        // 可以发送多个动作
        setTimeout(() => {
          set(
            (state) => ({ count: state.count + 10 }),
            false,
            'complexOperation/addTen'
          )
        }, 1000)
      },
    }),
    {
      name: 'counter-store',  // DevTools 中显示的名称
      // 可选配置
      serialize: {
        options: {
          undefined: true,  // 序列化 undefined 值
          function: true,   // 序列化函数
        }
      }
    }
  )
)

2. 持久化中间件详解

状态持久化是 Web 应用中的常见需求,它让用户在刷新页面或重新打开应用时能够保持之前的状态。Zustand 的 Persist 中间件提供了强大而灵活的持久化功能,支持多种存储后端和自定义配置。

持久化的核心价值在于提升用户体验:

  • 保持用户偏好:主题设置、语言选择等用户配置
  • 恢复工作状态:表单数据、页面状态等临时信息
  • 离线支持:缓存重要数据,支持离线访问
  • 性能优化:减少重复的数据获取

Persist 中间件可以将状态持久化到 localStorage、sessionStorage 或其他存储中:

import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'

// 基础持久化配置
const useSettingsStore = create<SettingsState>()(
  persist(
    (set, get) => ({
      theme: 'light',
      language: 'zh-CN',
      fontSize: 14,
      notifications: {
        email: true,
        push: false,
        sms: false
      },
      
      // 操作方法
      setTheme: (theme) => set({ theme }),
      setLanguage: (language) => set({ language }),
      setFontSize: (fontSize) => set({ fontSize }),
      updateNotifications: (key, value) => set((state) => ({
        notifications: {
          ...state.notifications,
          [key]: value
        }
      })),
      
      // 重置设置
      resetSettings: () => set({
        theme: 'light',
        language: 'zh-CN',
        fontSize: 14,
        notifications: { email: true, push: false, sms: false }
      }),
    }),
    {
      name: 'app-settings',  // localStorage 中的键名
      
      // 只持久化特定字段
      partialize: (state) => ({
        theme: state.theme,
        language: state.language,
        fontSize: state.fontSize,
        notifications: state.notifications
        // 不持久化方法
      }),
      
      // 自定义存储
      storage: createJSONStorage(() => localStorage),
      
      // 版本控制和迁移
      version: 1,
      migrate: (persistedState, version) => {
        if (version === 0) {
          // 从版本 0 迁移到版本 1
          return {
            ...persistedState,
            fontSize: 14  // 添加新字段
          }
        }
        return persistedState
      },
      
      // 合并策略
      merge: (persistedState, currentState) => ({
        ...currentState,
        ...persistedState,
        // 确保方法不被覆盖
        setTheme: currentState.setTheme,
        setLanguage: currentState.setLanguage,
        // ... 其他方法
      }),
    }
  )
)

这个配置展示了 Persist 中间件的强大功能。通过 partialize 选项,我们可以选择性地持久化状态的某些部分,避免将函数或敏感信息存储到本地。版本控制和迁移功能让我们能够安全地更新数据结构,而不会破坏用户的现有数据。

3. 使用 sessionStorage

sessionStorage 与 localStorage 的主要区别在于生命周期:sessionStorage 中的数据只在当前浏览器标签页中有效,关闭标签页后数据就会被清除。这使得它非常适合存储临时的会话数据,比如表单的临时保存、页面的滚动位置等。

const useSessionStore = create<SessionState>()(
  persist(
    (set) => ({
      sessionData: null,
      tempSettings: {},
      
      setSessionData: (data) => set({ sessionData: data }),
      updateTempSettings: (settings) => set({ tempSettings: settings }),
    }),
    {
      name: 'session-data',
      storage: createJSONStorage(() => sessionStorage),  // 使用 sessionStorage
    }
  )
)

4. 自定义存储适配器

有时候,localStorage 和 sessionStorage 可能无法满足我们的需求。比如,我们可能需要将数据存储到 IndexedDB 以支持更大的存储容量,或者存储到远程服务器以实现跨设备同步。Zustand 允许我们创建自定义的存储适配器来满足这些特殊需求。

自定义存储适配器需要实现三个基本方法:getItemsetItemremoveItem。这些方法都应该返回 Promise,以支持异步存储操作。

// 创建自定义存储适配器(例如:使用 IndexedDB)
const indexedDBStorage = {
  getItem: async (name: string): Promise<string | null> => {
    // 从 IndexedDB 获取数据
    return new Promise((resolve) => {
      const request = indexedDB.open('zustand-db', 1)
      request.onsuccess = () => {
        const db = request.result
        const transaction = db.transaction(['store'], 'readonly')
        const store = transaction.objectStore('store')
        const getRequest = store.get(name)
        
        getRequest.onsuccess = () => {
          resolve(getRequest.result?.value || null)
        }
      }
    })
  },
  
  setItem: async (name: string, value: string): Promise<void> => {
    // 保存到 IndexedDB
    return new Promise((resolve) => {
      const request = indexedDB.open('zustand-db', 1)
      request.onsuccess = () => {
        const db = request.result
        const transaction = db.transaction(['store'], 'readwrite')
        const store = transaction.objectStore('store')
        store.put({ name, value })
        
        transaction.oncomplete = () => resolve()
      }
    })
  },
  
  removeItem: async (name: string): Promise<void> => {
    // 从 IndexedDB 删除
    return new Promise((resolve) => {
      const request = indexedDB.open('zustand-db', 1)
      request.onsuccess = () => {
        const db = request.result
        const transaction = db.transaction(['store'], 'readwrite')
        const store = transaction.objectStore('store')
        store.delete(name)
        
        transaction.oncomplete = () => resolve()
      }
    })
  },
}

// 使用自定义存储
const useIndexedDBStore = create<State>()(
  persist(
    (set) => ({
      // ... 状态和方法
    }),
    {
      name: 'indexed-db-store',
      storage: indexedDBStorage,
    }
  )
)

5. Immer 中间件详解

Immer 是一个革命性的库,它让我们能够以可变的方式编写不可变的更新逻辑。在传统的 React 状态管理中,我们需要小心地创建新的对象和数组来避免直接修改状态,这在处理深层嵌套的数据结构时会变得非常繁琐。

Immer 的核心思想是"写起来像可变,运行起来是不可变"。它使用 Proxy 技术来跟踪我们对状态的修改,然后自动生成新的不可变状态。这大大简化了状态更新的代码,特别是对于复杂的数据结构。

Immer 中间件让你可以直接"修改"状态,而不需要手动创建不可变更新:

import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'

interface ComplexState {
  user: {
    profile: {
      name: string
      email: string
      preferences: {
        theme: string
        notifications: {
          email: boolean
          push: boolean
          sms: boolean
        }
      }
    }
    posts: Array<{
      id: string
      title: string
      content: string
      tags: string[]
      likes: number
    }>
  }
}

const useComplexStore = create<ComplexState>()(
  immer((set) => ({
    user: {
      profile: {
        name: '',
        email: '',
        preferences: {
          theme: 'light',
          notifications: {
            email: true,
            push: false,
            sms: false
          }
        }
      },
      posts: []
    },
    
    // 使用 Immer 可以直接"修改"嵌套状态
    updateUserName: (name: string) => set((state) => {
      state.user.profile.name = name  // 直接修改,Immer 会处理不可变性
    }),
    
    // 更新深层嵌套的状态
    toggleNotification: (type: 'email' | 'push' | 'sms') => set((state) => {
      state.user.profile.preferences.notifications[type] = 
        !state.user.profile.preferences.notifications[type]
    }),
    
    // 处理数组操作
    addPost: (post: Omit<Post, 'id'>) => set((state) => {
      state.user.posts.push({
        ...post,
        id: Date.now().toString()
      })
    }),
    
    // 更新数组中的特定项
    updatePost: (id: string, updates: Partial<Post>) => set((state) => {
      const post = state.user.posts.find(p => p.id === id)
      if (post) {
        Object.assign(post, updates)
      }
    }),
    
    // 删除数组项
    deletePost: (id: string) => set((state) => {
      const index = state.user.posts.findIndex(p => p.id === id)
      if (index !== -1) {
        state.user.posts.splice(index, 1)
      }
    }),
    
    // 为帖子添加标签
    addTagToPost: (postId: string, tag: string) => set((state) => {
      const post = state.user.posts.find(p => p.id === postId)
      if (post && !post.tags.includes(tag)) {
        post.tags.push(tag)
      }
    }),
    
    // 点赞帖子
    likePost: (postId: string) => set((state) => {
      const post = state.user.posts.find(p => p.id === postId)
      if (post) {
        post.likes += 1
      }
    }),
  }))
)

6. 组合多个中间件

import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
import { immer } from 'zustand/middleware/immer'

// 组合多个中间件
const useAdvancedStore = create<AdvancedState>()(
  // 中间件的顺序很重要
  devtools(
    persist(
      immer((set, get) => ({
        // 状态
        data: [],
        loading: false,
        error: null,
        
        // 方法
        addItem: (item) => set((state) => {
          state.data.push(item)
        }),
        
        removeItem: (id) => set((state) => {
          const index = state.data.findIndex(item => item.id === id)
          if (index !== -1) {
            state.data.splice(index, 1)
          }
        }),
        
        setLoading: (loading) => set((state) => {
          state.loading = loading
        }),
      })),
      {
        name: 'advanced-store',
        partialize: (state) => ({
          data: state.data  // 只持久化 data
        }),
      }
    ),
    {
      name: 'advanced-store-devtools',
    }
  )
)

7. 创建自定义中间件

理解了 createset 的完整参数后,我们可以创建更强大的自定义中间件:

// 创建增强的日志中间件,利用 set 的所有参数
const enhancedLogger = (config) => (set, get, api) =>
  config(
    (partial, replace, actionName) => {
      console.group(`🔄 Action: ${actionName || 'Unknown'}`)
      console.log('Previous state:', get())
      console.log('Update payload:', partial)
      console.log('Replace mode:', replace)
      
      // 调用原始的 set 函数
      set(partial, replace, actionName)
      
      console.log('New state:', get())
      console.groupEnd()
    },
    get,
    api
  )

// 创建性能监控中间件
const performance = (config) => (set, get, api) =>
  config(
    (...args) => {
      const start = performance.now()
      set(...args)
      const end = performance.now()
      console.log(`State update took ${end - start} milliseconds`)
    },
    get,
    api
  )

// 使用自定义中间件
const useLoggedStore = create(
  enhancedLogger(
    performance(
      (set) => ({
        count: 0,
        increment: () => set(
          (state) => ({ count: state.count + 1 }),
          false,
          'increment'
        ),
        reset: () => set(
          { count: 0 },
          false,
          'reset'
        ),
        fullReset: () => set(
          { count: 0 },
          true,  // 完全替换
          'fullReset'
        ),
      })
    )
  ),
  {
    name: 'logged-counter-store'
  }
)

// 实际项目中的高级用法示例
const useAdvancedProjectStore = create(
  (set, get, api) => ({
    // 应用状态
    user: null,
    theme: 'light',
    notifications: [],
    
    // 高级方法:使用所有参数
    login: async (credentials) => {
      set(
        { user: { ...credentials, isLoading: true } },
        false,
        'login/start'
      )
      
      try {
        const user = await authAPI.login(credentials)
        set(
          { user },
          false,
          'login/success'
        )
      } catch (error) {
        set(
          { user: null },
          false,
          'login/error'
        )
      }
    },
    
    // 使用 API 对象进行高级操作
    setupGlobalListeners: () => {
      // 监听主题变化
      api.subscribe(
        (state) => state.theme,
        (theme) => {
          document.documentElement.setAttribute('data-theme', theme)
        }
      )
      
      // 监听用户状态
      api.subscribe(
        (state) => state.user,
        (user) => {
          if (user) {
            console.log(`用户 ${user.name} 已登录`)
          }
        }
      )
    },
    
    // 批量状态重置
    logout: () => {
      set(
        {
          user: null,
          notifications: [],
          // 保留主题设置
        },
        false, // 不完全替换,保留其他状态
        'logout'
      )
    },
    
    // 完全重置应用状态
    resetApp: () => {
      set(
        {
          user: null,
          theme: 'light',
          notifications: [],
        },
        true, // 完全替换
        'resetApp'
      )
    },
  }),
  {
    name: 'advanced-project-store'
  }
)

3. 高级用法和技巧

1. Store 切片和模块化

随着应用规模的增长,单一的大型 Store 会变得难以维护和理解。Store 切片是一种将复杂状态分解为更小、更专注的模块的技术。这种模式借鉴了微服务架构的思想,每个切片专注于特定的业务领域。

切片化的好处包括:

  • 职责分离:每个切片只关注特定的功能领域
  • 代码复用:切片可以在不同的 Store 中重复使用
  • 团队协作:不同的开发者可以独立开发不同的切片
  • 测试简化:小的切片更容易进行单元测试

当应用变得复杂时,你可以将大的 store 拆分成多个切片:

// 用户切片
const createUserSlice = (set, get) => ({
  user: null,
  isAuthenticated: false,
  
  login: async (credentials) => {
    const user = await authAPI.login(credentials)
    set({ user, isAuthenticated: true })
  },
  
  logout: () => {
    set({ user: null, isAuthenticated: false })
  },
})

// 主题切片
const createThemeSlice = (set) => ({
  theme: 'light',
  primaryColor: '#007bff',
  
  setTheme: (theme) => set({ theme }),
  setPrimaryColor: (color) => set({ primaryColor: color }),
})

// 通知切片
const createNotificationSlice = (set, get) => ({
  notifications: [],
  
  addNotification: (notification) => set((state) => ({
    notifications: [...state.notifications, {
      id: Date.now(),
      ...notification
    }]
  })),
  
  removeNotification: (id) => set((state) => ({
    notifications: state.notifications.filter(n => n.id !== id)
  })),
})

// 组合所有切片
const useAppStore = create((set, get) => ({
  ...createUserSlice(set, get),
  ...createThemeSlice(set, get),
  ...createNotificationSlice(set, get),
}))

这个例子展示了如何将不同的功能模块组合成一个完整的 Store。每个切片都有自己的状态和方法,但它们可以通过组合模式统一管理。这种方法既保持了代码的模块化,又提供了统一的访问接口。

2. 跨 Store 通信

在大型应用中,我们通常会有多个独立的 Store,每个 Store 负责不同的业务领域。但有时候,这些 Store 之间需要进行通信和数据同步。比如,用户登录状态的变化可能需要影响购物车、通知等多个模块。

跨 Store 通信的常见场景包括:

  • 用户状态变化:登录/登出影响其他模块
  • 权限更新:权限变化需要更新 UI 状态
  • 数据同步:一个模块的数据变化需要通知其他模块
  • 事件传播:全局事件需要被多个模块处理
// Store A
const useStoreA = create((set, get) => ({
  dataA: [],
  
  updateDataA: (data) => {
    set({ dataA: data })
    
    // 通知其他 Store
    const storeB = useStoreB.getState()
    storeB.onDataAChanged(data)
  },
}))

// Store B
const useStoreB = create((set, get) => ({
  dataB: [],
  relatedData: [],
  
  onDataAChanged: (dataA) => {
    // 根据 Store A 的变化更新自己的状态
    const relatedData = dataA.filter(item => item.category === 'related')
    set({ relatedData })
  },
  
  // 或者使用订阅模式
  subscribeToStoreA: () => {
    const unsubscribe = useStoreA.subscribe(
      (state) => state.dataA,
      (dataA) => {
        const { onDataAChanged } = get()
        onDataAChanged(dataA)
      }
    )
    return unsubscribe
  },
}))

这个例子展示了两种跨 Store 通信的方式:直接调用和订阅模式。直接调用适合简单的通知场景,而订阅模式更适合需要持续监听变化的场景。选择哪种方式取决于具体的业务需求和数据流的复杂度。

3. 计算属性和派生状态

计算属性是基于现有状态计算出来的值,它们不直接存储在状态中,而是在需要时动态计算。这种模式类似于 Vue.js 的计算属性或 Excel 的公式,当依赖的状态发生变化时,计算属性会自动更新。

派生状态的优势包括:

  • 数据一致性:计算属性总是基于最新的状态计算
  • 内存效率:不需要存储冗余的计算结果
  • 自动更新:依赖变化时自动重新计算
  • 逻辑集中:计算逻辑集中在一个地方,便于维护
const useShoppingCartStore = create((set, get) => ({
  items: [],
  discountRate: 0,
  
  // 基础操作
  addItem: (item) => set((state) => ({
    items: [...state.items, item]
  })),
  
  setDiscountRate: (rate) => set({ discountRate: rate }),
  
  // 计算属性(通过选择器实现)
  getItemCount: () => get().items.length,
  
  getSubtotal: () => {
    const { items } = get()
    return items.reduce((sum, item) => sum + item.price * item.quantity, 0)
  },
  
  getDiscountAmount: () => {
    const { getSubtotal, discountRate } = get()
    return getSubtotal() * (discountRate / 100)
  },
  
  getTotal: () => {
    const { getSubtotal, getDiscountAmount } = get()
    return getSubtotal() - getDiscountAmount()
  },
}))

// 使用计算属性的组件
function CartSummary() {
  const store = useShoppingCartStore()
  
  // 这些值会在相关状态改变时自动更新
  const itemCount = store.getItemCount()
  const subtotal = store.getSubtotal()
  const discount = store.getDiscountAmount()
  const total = store.getTotal()
  
  return (
    <div>
      <p>商品数量: {itemCount}</p>
      <p>小计: ¥{subtotal.toFixed(2)}</p>
      <p>折扣: -¥{discount.toFixed(2)}</p>
      <p>总计: ¥{total.toFixed(2)}</p>
    </div>
  )
}

4. 状态机模式

状态机是计算机科学中的一个重要概念,它描述了系统在不同状态之间的转换规则。在前端开发中,状态机模式特别适合管理具有明确状态转换的复杂业务逻辑,比如数据加载、表单提交、用户认证等场景。

状态机模式的核心优势:

  • 状态明确:任何时候系统都处于一个明确的状态
  • 转换可控:状态之间的转换有明确的规则和条件
  • 错误预防:避免了无效的状态转换
  • 调试友好:状态变化清晰可追踪
  • 业务对齐:状态机往往能很好地映射业务流程

在数据获取的场景中,我们通常有四个状态:空闲(idle)、加载中(loading)、成功(success)和错误(error)。这四个状态之间有明确的转换规则,使用状态机模式可以让我们更好地管理这些状态。

// 使用状态机模式管理复杂状态
type LoadingState = 'idle' | 'loading' | 'success' | 'error'

interface StateMachineStore {
  state: LoadingState
  data: any[]
  error: string | null
  
  // 状态转换
  startLoading: () => void
  loadSuccess: (data: any[]) => void
  loadError: (error: string) => void
  reset: () => void
  
  // 状态检查
  isIdle: () => boolean
  isLoading: () => boolean
  isSuccess: () => boolean
  isError: () => boolean
  
  // 操作
  fetchData: () => Promise<void>
}

const useStateMachineStore = create<StateMachineStore>((set, get) => ({
  state: 'idle',
  data: [],
  error: null,
  
  // 状态转换
  startLoading: () => set({ state: 'loading', error: null }),
  loadSuccess: (data) => set({ state: 'success', data, error: null }),
  loadError: (error) => set({ state: 'error', error, data: [] }),
  reset: () => set({ state: 'idle', data: [], error: null }),
  
  // 状态检查
  isIdle: () => get().state === 'idle',
  isLoading: () => get().state === 'loading',
  isSuccess: () => get().state === 'success',
  isError: () => get().state === 'error',
  
  // 操作
  fetchData: async () => {
    const { state, startLoading, loadSuccess, loadError } = get()
    
    // 防止重复请求
    if (state === 'loading') return
    
    startLoading()
    
    try {
      const response = await fetch('/api/data')
      const data = await response.json()
      loadSuccess(data)
    } catch (error) {
      loadError(error.message)
    }
  },
}))

这个状态机的实现展示了如何将复杂的异步逻辑组织得更加清晰。通过明确的状态检查方法(如 isLoading()isError() 等),我们可以在组件中编写更加可读和可维护的条件逻辑。状态机还防止了一些常见的错误,比如在已经加载中的状态下重复发起请求。

5. 条件渲染和状态依赖

条件渲染是 React 开发中的核心概念,它让我们能够根据应用的状态动态地显示不同的 UI。结合 Zustand 的状态管理,我们可以创建响应式的用户界面,根据数据的加载状态、用户的权限、设备的特性等条件来渲染不同的内容。

良好的条件渲染策略应该考虑:

  • 用户体验:为每种状态提供合适的 UI 反馈
  • 性能优化:避免不必要的组件渲染
  • 错误处理:优雅地处理错误状态
  • 加载体验:提供有意义的加载提示
  • 空状态处理:当没有数据时显示合适的提示

在实际开发中,我们经常需要根据多个状态条件来决定渲染什么内容。状态机模式让这种条件渲染变得更加清晰和可维护。

function DataComponent() {
  const { 
    state, 
    data, 
    error, 
    fetchData, 
    reset,
    isIdle,
    isLoading,
    isSuccess,
    isError 
  } = useStateMachineStore()
  
  // 根据状态条件渲染
  if (isIdle()) {
    return (
      <div>
        <button onClick={fetchData}>加载数据</button>
      </div>
    )
  }
  
  if (isLoading()) {
    return <div>加载中...</div>
  }
  
  if (isError()) {
    return (
      <div>
        <p>错误: {error}</p>
        <button onClick={fetchData}>重试</button>
        <button onClick={reset}>重置</button>
      </div>
    )
  }
  
  if (isSuccess()) {
    return (
      <div>
        <h3>数据加载成功</h3>
        <ul>
          {data.map(item => (
            <li key={item.id}>{item.name}</li>
          ))}
        </ul>
        <button onClick={fetchData}>刷新</button>
      </div>
    )
  }
  
  return null
}

这个组件展示了基于状态机的条件渲染模式。每个状态都有对应的 UI 表现,用户始终能够了解当前的应用状态。这种模式不仅提升了用户体验,也让代码逻辑更加清晰,便于测试和维护。

6. 时间旅行和撤销/重做

时间旅行是一个强大的概念,它允许用户在应用的历史状态之间自由穿梭。这个功能在很多应用中都非常有用,比如文本编辑器的撤销/重做、图像编辑软件的历史记录、游戏的存档系统等。

撤销/重做功能的价值:

  • 用户信心:用户知道可以撤销错误操作,会更勇于尝试
  • 错误恢复:快速从误操作中恢复
  • 探索性操作:支持用户的试验性操作
  • 工作流程:支持复杂的编辑工作流程

实现时间旅行需要维护三个状态数组:过去(past)、现在(present)和未来(future)。当用户执行新操作时,当前状态被推入过去数组,新状态成为现在,未来数组被清空。撤销操作将当前状态推入未来数组,从过去数组中取出最近的状态。

interface HistoryState<T> {
  past: T[]
  present: T
  future: T[]
}

const createHistoryStore = <T>(initialState: T) => {
  return create<HistoryState<T> & {
    set: (newState: T) => void
    undo: () => void
    redo: () => void
    canUndo: () => boolean
    canRedo: () => boolean
    clear: () => void
  }>((set, get) => ({
    past: [],
    present: initialState,
    future: [],
    
    set: (newState: T) => {
      const { present, past } = get()
      set({
        past: [...past, present],
        present: newState,
        future: []  // 清空 future
      })
    },
    
    undo: () => {
      const { past, present, future } = get()
      if (past.length === 0) return
      
      const previous = past[past.length - 1]
      const newPast = past.slice(0, past.length - 1)
      
      set({
        past: newPast,
        present: previous,
        future: [present, ...future]
      })
    },
    
    redo: () => {
      const { past, present, future } = get()
      if (future.length === 0) return
      
      const next = future[0]
      const newFuture = future.slice(1)
      
      set({
        past: [...past, present],
        present: next,
        future: newFuture
      })
    },
    
    canUndo: () => get().past.length > 0,
    canRedo: () => get().future.length > 0,
    
    clear: () => set({
      past: [],
      present: initialState,
      future: []
    }),
  }))
}

// 使用历史记录 Store
const useCounterWithHistory = createHistoryStore(0)

function CounterWithHistory() {
  const { 
    present: count, 
    set, 
    undo, 
    redo, 
    canUndo, 
    canRedo,
    clear 
  } = useCounterWithHistory()
  
  return (
    <div>
      <p>计数: {count}</p>
      <button onClick={() => set(count + 1)}>+1</button>
      <button onClick={() => set(count - 1)}>-1</button>
      <button onClick={undo} disabled={!canUndo()}>撤销</button>
      <button onClick={redo} disabled={!canRedo()}>重做</button>
      <button onClick={clear}>清空历史</button>
    </div>
  )
}

7. 响应式计算和自动更新

const useReactiveStore = create((set, get) => ({
  width: 100,
  height: 100,
  
  // 自动计算的面积
  get area() {
    const { width, height } = get()
    return width * height
  },
  
  // 自动计算的周长
  get perimeter() {
    const { width, height } = get()
    return 2 * (width + height)
  },
  
  setWidth: (width) => set({ width }),
  setHeight: (height) => set({ height }),
  
  // 批量更新
  setDimensions: (width, height) => set({ width, height }),
}))

// 使用响应式计算
function ReactiveComponent() {
  const { width, height, area, perimeter, setWidth, setHeight } = useReactiveStore()
  
  return (
    <div>
      <div>
        <label>宽度: 
          <input 
            type="number" 
            value={width} 
            onChange={(e) => setWidth(Number(e.target.value))} 
          />
        </label>
      </div>
      <div>
        <label>高度: 
          <input 
            type="number" 
            value={height} 
            onChange={(e) => setHeight(Number(e.target.value))} 
          />
        </label>
      </div>
      <div>面积: {area}</div>
      <div>周长: {perimeter}</div>
    </div>
  )
}

4. 错误处理和调试

错误处理是构建健壮应用的关键环节。在状态管理中,错误可能来自多个源头:网络请求失败、数据验证错误、用户输入错误、系统异常等。良好的错误处理策略不仅能提升用户体验,还能帮助开发者快速定位和解决问题。

现代应用的错误处理应该是多层次的:

  • 预防性错误处理:在错误发生前进行验证和检查
  • 捕获性错误处理:捕获并优雅地处理运行时错误
  • 恢复性错误处理:提供错误恢复机制
  • 用户友好的错误提示:向用户展示易懂的错误信息
  • 开发者友好的错误信息:提供详细的调试信息

1. 错误边界和错误处理

错误边界是 React 的一个重要概念,它可以捕获组件树中的 JavaScript 错误,记录错误信息,并显示备用 UI。结合 Zustand,我们可以创建一个全局的错误处理系统,统一管理应用中的各种错误。

错误处理的最佳实践包括:

  • 分类管理:将不同类型的错误分别处理
  • 上下文保存:记录错误发生时的应用状态
  • 用户通知:以用户友好的方式展示错误信息
  • 自动恢复:对于可恢复的错误,提供自动重试机制
  • 日志记录:记录错误信息用于后续分析和改进
// 错误状态管理
interface ErrorState {
  errors: Record<string, string>
  globalError: string | null
  
  setError: (key: string, error: string) => void
  clearError: (key: string) => void
  setGlobalError: (error: string) => void
  clearGlobalError: () => void
  clearAllErrors: () => void
  hasErrors: () => boolean
  getError: (key: string) => string | null
}

const useErrorStore = create<ErrorState>((set, get) => ({
  errors: {},
  globalError: null,
  
  setError: (key, error) => set((state) => ({
    errors: { ...state.errors, [key]: error }
  })),
  
  clearError: (key) => set((state) => {
    const newErrors = { ...state.errors }
    delete newErrors[key]
    return { errors: newErrors }
  }),
  
  setGlobalError: (error) => set({ globalError: error }),
  clearGlobalError: () => set({ globalError: null }),
  
  clearAllErrors: () => set({ errors: {}, globalError: null }),
  
  hasErrors: () => {
    const { errors, globalError } = get()
    return Object.keys(errors).length > 0 || globalError !== null
  },
  
  getError: (key) => get().errors[key] || null,
}))

// 错误处理工具函数
const withErrorHandling = (fn: Function, errorKey: string) => {
  return async (...args: any[]) => {
    const { setError, clearError } = useErrorStore.getState()
    
    try {
      clearError(errorKey)
      return await fn(...args)
    } catch (error) {
      setError(errorKey, error.message)
      throw error
    }
  }
}

// 使用错误处理
const useUserStore = create((set, get) => ({
  users: [],
  loading: false,
  
  fetchUsers: withErrorHandling(async () => {
    set({ loading: true })
    
    const response = await fetch('/api/users')
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`)
    }
    
    const users = await response.json()
    set({ users, loading: false })
  }, 'fetchUsers'),
  
  createUser: withErrorHandling(async (userData) => {
    const response = await fetch('/api/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(userData)
    })
    
    if (!response.ok) {
      throw new Error('创建用户失败')
    }
    
    const newUser = await response.json()
    set((state) => ({
      users: [...state.users, newUser]
    }))
  }, 'createUser'),
}))

上面的代码展示了一个完整的错误处理系统。withErrorHandling 高阶函数可以包装任何可能抛出错误的函数,自动处理错误并更新错误状态。这种模式让我们能够在应用的任何地方统一处理错误,而不需要在每个组件中重复错误处理逻辑。

2. 调试工具和技巧

调试是开发过程中不可避免的环节。Zustand 提供了多种调试方式,从简单的控制台输出到复杂的中间件系统。有效的调试策略可以帮助我们快速定位问题,理解应用的行为,并优化性能。

调试的层次包括:

  • 状态变化追踪:监控状态的变化过程
  • 性能监控:测量状态更新的性能开销
  • 错误追踪:捕获和分析错误发生的上下文
  • 用户行为分析:理解用户操作对状态的影响

中间件是 Zustand 调试的核心工具。通过创建自定义中间件,我们可以在状态更新的各个阶段插入调试逻辑,获得应用运行时的详细信息。

// 调试中间件
const debugMiddleware = (config) => (set, get, api) => {
  const originalSet = set
  
  return config(
    (...args) => {
      console.group('🐻 Zustand State Update')
      console.log('Previous state:', get())
      console.log('Update args:', args)
      
      const result = originalSet(...args)
      
      console.log('New state:', get())
      console.groupEnd()
      
      return result
    },
    get,
    api
  )
}

// 性能监控
const performanceMiddleware = (config) => (set, get, api) => {
  let updateCount = 0
  
  return config(
    (...args) => {
      const start = performance.now()
      updateCount++
      
      const result = set(...args)
      
      const end = performance.now()
      console.log(`Update #${updateCount} took ${(end - start).toFixed(2)}ms`)
      
      return result
    },
    get,
    api
  )
}

// 使用调试中间件
const useDebugStore = create(
  debugMiddleware(
    performanceMiddleware(
      (set) => ({
        count: 0,
        increment: () => set((state) => ({ count: state.count + 1 })),
      })
    )
  )
)

这些调试中间件展示了如何在不修改核心逻辑的情况下添加调试功能。debugMiddleware 提供了详细的状态变化日志,而 performanceMiddleware 则帮助我们监控状态更新的性能。在开发环境中,这些工具可以帮助我们快速发现和解决问题。

3. 开发环境调试

开发环境的调试工具应该更加强大和全面。我们可以创建专门的开发环境调试功能,包括将 store 暴露到全局作用域、添加状态变化监听、集成浏览器开发者工具等。

开发环境调试的特点:

  • 全局访问:将 store 暴露到全局,方便在控制台中直接操作
  • 详细日志:记录所有状态变化和操作
  • 性能分析:提供详细的性能指标
  • 可视化:通过图表等方式展示状态变化
  • 热重载支持:在代码变更时保持调试状态
// 开发环境专用的调试功能
const createDebugStore = (config, name) => {
  const store = create(config)
  
  if (process.env.NODE_ENV === 'development') {
    // 将 store 暴露到全局,方便调试
    window[`__${name}Store__`] = store
    
    // 添加状态变化监听
    store.subscribe(
      (state) => state,
      (state) => {
        console.log(`[${name}] State changed:`, state)
      }
    )
  }
  
  return store
}

// 使用调试 Store
const useAppStore = createDebugStore(
  (set) => ({
    user: null,
    theme: 'light',
    setUser: (user) => set({ user }),
    setTheme: (theme) => set({ theme }),
  }),
  'App'
)

这种开发环境调试方案让我们能够在开发过程中更好地理解应用的行为。通过将 store 暴露到全局,我们可以在浏览器控制台中直接操作状态,这对于调试复杂的状态逻辑非常有用。

4. 错误恢复策略

错误恢复是健壮应用的重要特征。当错误发生时,应用应该能够优雅地处理错误,并尽可能地恢复到正常状态。这包括自动重试、降级处理、用户引导等多种策略。

错误恢复的核心原则:

  • 优雅降级:当某个功能出错时,不影响其他功能的正常使用
  • 自动重试:对于网络错误等临时性问题,提供自动重试机制
  • 用户引导:为用户提供明确的错误信息和解决建议
  • 状态保护:确保错误不会破坏应用的核心状态
  • 快速恢复:提供快速恢复到正常状态的方法
const useResilientStore = create((set, get) => ({
  data: [],
  backupData: [],
  error: null,
  
  // 安全更新:失败时自动恢复
  safeUpdate: (updateFn) => {
    const currentState = get()
    const backup = { ...currentState }
    
    try {
      updateFn(set, get)
    } catch (error) {
      // 恢复到备份状态
      set(backup)
      set({ error: error.message })
      console.error('State update failed, restored from backup:', error)
    }
  },
  
  // 创建备份
  createBackup: () => {
    const { data } = get()
    set({ backupData: [...data] })
  },
  
  // 从备份恢复
  restoreFromBackup: () => {
    const { backupData } = get()
    set({ data: [...backupData], error: null })
  },
  
  // 验证状态完整性
  validateState: () => {
    const state = get()
    const isValid = Array.isArray(state.data) && 
                   state.data.every(item => item.id && item.name)
    
    if (!isValid) {
      console.warn('Invalid state detected:', state)
      return false
    }
    
    return true
  },
}))

5. 最佳实践指南

最佳实践是从实际开发经验中总结出的指导原则,它们可以帮助我们避免常见的陷阱,提高代码质量,并构建更可维护的应用。在 Zustand 的使用过程中,遵循这些最佳实践可以让我们的状态管理更加高效和可靠。

良好的实践包括:

  • 架构设计:合理的 Store 结构和职责划分
  • 性能优化:避免不必要的重新渲染和计算
  • 类型安全:充分利用 TypeScript 的类型系统
  • 代码组织:清晰的文件结构和命名约定
  • 测试策略:确保状态管理逻辑的正确性

1. Store 设计原则

Store 的设计是状态管理的基础。一个好的 Store 设计应该遵循单一职责原则,保持状态结构的简洁性,并提供清晰的 API。这不仅有利于代码的维护,也有助于团队协作和功能扩展。

设计原则的核心思想:

  • 职责分离:每个 Store 只负责一个特定的业务领域
  • 状态扁平化:避免过度嵌套的状态结构
  • 命名规范:使用清晰、一致的命名约定
  • 接口设计:提供直观、易用的 API
  • 扩展性:考虑未来的功能扩展需求
单一职责原则
// ❌ 不好:一个 Store 处理太多职责
const useBadStore = create((set) => ({
  // 用户相关
  user: null,
  userLoading: false,
  
  // 产品相关
  products: [],
  productsLoading: false,
  
  // 订单相关
  orders: [],
  ordersLoading: false,
  
  // 主题相关
  theme: 'light',
  
  // 通知相关
  notifications: [],
}))

// ✅ 好:按职责分离
const useUserStore = create((set) => ({
  user: null,
  loading: false,
  error: null,
  // 用户相关的方法...
}))

const useProductStore = create((set) => ({
  products: [],
  loading: false,
  error: null,
  // 产品相关的方法...
}))

const useThemeStore = create((set) => ({
  theme: 'light',
  primaryColor: '#007bff',
  // 主题相关的方法...
}))
状态扁平化
// ❌ 不好:过度嵌套
const useBadStore = create((set) => ({
  user: {
    profile: {
      personal: {
        name: '',
        email: '',
        preferences: {
          theme: 'light',
          notifications: {
            email: true,
            push: false
          }
        }
      }
    }
  }
}))

// ✅ 好:扁平化结构
const useUserStore = create((set) => ({
  userName: '',
  userEmail: '',
  theme: 'light',
  emailNotifications: true,
  pushNotifications: false,
  
  // 方法名清晰明确
  updateUserName: (name) => set({ userName: name }),
  updateUserEmail: (email) => set({ userEmail: email }),
  updateTheme: (theme) => set({ theme }),
  toggleEmailNotifications: () => set((state) => ({ 
    emailNotifications: !state.emailNotifications 
  })),
}))

单一职责原则的应用让我们的代码更加模块化和可维护。通过将不同的业务逻辑分离到不同的 Store 中,我们可以独立地开发、测试和维护每个模块。状态扁平化则避免了复杂的嵌套更新逻辑,让状态管理变得更加直观。

2. 性能优化策略

性能优化是现代 Web 应用开发的重要环节。在状态管理中,性能问题通常表现为不必要的重新渲染、复杂的计算重复执行、内存泄漏等。通过合理的优化策略,我们可以显著提升应用的响应速度和用户体验。

性能优化的关键领域:

  • 渲染优化:减少不必要的组件重新渲染
  • 计算优化:避免重复的复杂计算
  • 内存优化:及时清理不需要的数据和监听器
  • 网络优化:合理的数据缓存和请求策略
  • 代码分割:按需加载状态管理逻辑
选择器优化

选择器是 Zustand 性能优化的核心工具。通过精确的选择器,我们可以确保组件只在真正需要的时候重新渲染。这对于大型应用来说尤其重要,因为不必要的重新渲染会累积成显著的性能问题。

// ✅ 好:使用精确的选择器
function UserProfile() {
  // 只选择需要的字段
  const userName = useUserStore((state) => state.userName)
  const userEmail = useUserStore((state) => state.userEmail)
  
  return (
    <div>
      <h1>{userName}</h1>
      <p>{userEmail}</p>
    </div>
  )
}

// ✅ 好:使用 shallow 比较
import { shallow } from 'zustand/shallow'

function UserCard() {
  const { userName, userEmail, userAvatar } = useUserStore(
    (state) => ({
      userName: state.userName,
      userEmail: state.userEmail,
      userAvatar: state.userAvatar
    }),
    shallow
  )
  
  return (
    <div>
      <img src={userAvatar} alt={userName} />
      <h2>{userName}</h2>
      <p>{userEmail}</p>
    </div>
  )
}

这些选择器优化技巧展示了如何精确控制组件的重新渲染。通过只选择组件实际需要的状态片段,我们可以避免因为无关状态变化而导致的不必要渲染。shallow 比较特别适用于需要选择多个状态字段的场景。

批量更新

批量更新是另一个重要的性能优化策略。当我们需要同时更新多个状态字段时,批量更新可以确保只触发一次重新渲染,而不是每个字段更新都触发一次。这不仅提高了性能,也避免了中间状态的闪烁。

const useOptimizedStore = create((set) => ({
  items: [],
  selectedItems: [],
  filters: {},
  
  // ✅ 好:批量更新
  updateMultiple: (items, filters) => {
    set({
      items,
      filters,
      selectedItems: []  // 重置选择
    })
  },
  
  // ❌ 不好:多次更新
  updateSeparately: (items, filters) => {
    set({ items })
    set({ filters })
    set({ selectedItems: [] })
  },
}))

批量更新的好处不仅仅是性能提升,还包括状态的一致性保证。当多个相关的状态需要同时更新时,批量更新确保了这些状态始终保持同步,避免了中间状态可能导致的 UI 异常。

3. 类型安全最佳实践

TypeScript 的类型系统是现代 JavaScript 开发的重要工具,它可以帮助我们在编译时发现错误,提供更好的代码提示,并让代码更加自文档化。在 Zustand 中,充分利用 TypeScript 的类型系统可以让我们的状态管理更加安全和可维护。

类型安全的价值:

  • 编译时错误检测:在开发阶段就发现类型错误
  • 更好的开发体验:IDE 可以提供准确的代码补全和提示
  • 代码自文档化:类型定义就是最好的文档
  • 重构安全:类型系统可以帮助我们安全地重构代码
  • 团队协作:类型定义让团队成员更容易理解代码
// 定义严格的类型
interface User {
  id: string
  name: string
  email: string
  role: 'admin' | 'user' | 'guest'
}

interface UserState {
  user: User | null
  users: User[]
  loading: boolean
  error: string | null
}

interface UserActions {
  setUser: (user: User) => void
  addUser: (user: User) => void
  updateUser: (id: string, updates: Partial<User>) => void
  removeUser: (id: string) => void
  clearError: () => void
}

// 使用联合类型确保类型安全
type UserStore = UserState & UserActions

const useUserStore = create<UserStore>((set, get) => ({
  // 状态
  user: null,
  users: [],
  loading: false,
  error: null,
  
  // 操作
  setUser: (user) => set({ user }),
  
  addUser: (user) => set((state) => ({
    users: [...state.users, user]
  })),
  
  updateUser: (id, updates) => set((state) => ({
    users: state.users.map(user =>
      user.id === id ? { ...user, ...updates } : user
    )
  })),
  
  removeUser: (id) => set((state) => ({
    users: state.users.filter(user => user.id !== id)
  })),
  
  clearError: () => set({ error: null }),
}))

这个类型安全的示例展示了如何在 Zustand 中充分利用 TypeScript 的类型系统。通过定义清晰的接口和联合类型,我们可以确保状态管理的类型安全,同时获得更好的开发体验。

4. 测试最佳实践

测试是确保代码质量和可靠性的重要手段。对于状态管理逻辑,我们需要测试状态的更新、副作用的执行、错误处理等各个方面。良好的测试策略可以让我们更有信心地重构和扩展代码。

测试的关键原则:

  • 隔离性:每个测试应该是独立的,不依赖其他测试
  • 可重复性:测试结果应该是可重复的,不受外部环境影响
  • 覆盖性:测试应该覆盖主要的业务逻辑和边界情况
  • 可读性:测试代码应该清晰易懂,就像文档一样
  • 快速性:测试应该快速执行,不影响开发效率
// 创建可测试的 Store
export const createUserStore = (initialState = {}) => create((set, get) => ({
  user: null,
  loading: false,
  error: null,
  ...initialState,
  
  setUser: (user) => set({ user }),
  setLoading: (loading) => set({ loading }),
  setError: (error) => set({ error }),
}))

// 默认 Store
export const useUserStore = createUserStore()

// 测试用例
describe('UserStore', () => {
  it('should set user correctly', () => {
    const store = createUserStore()
    const user = { id: '1', name: 'John' }
    
    store.getState().setUser(user)
    
    expect(store.getState().user).toEqual(user)
  })
  
  it('should handle loading state', () => {
    const store = createUserStore()
    
    store.getState().setLoading(true)
    expect(store.getState().loading).toBe(true)
    
    store.getState().setLoading(false)
    expect(store.getState().loading).toBe(false)
  })
})

这种测试方法的优势在于它让 Store 变得可测试。通过工厂函数创建 Store,我们可以为每个测试创建独立的实例,避免测试之间的相互影响。这种模式也让我们能够轻松地为测试提供初始状态。

5. 代码组织结构

良好的代码组织结构是大型项目成功的关键。合理的文件结构不仅让代码更易于查找和维护,还能促进团队协作和代码复用。在 Zustand 项目中,我们应该按照功能模块来组织代码,保持清晰的依赖关系。

src/
├── stores/
│   ├── index.ts          # 导出所有 stores
│   ├── userStore.ts      # 用户相关状态
│   ├── productStore.ts   # 产品相关状态
│   ├── cartStore.ts      # 购物车状态
│   └── themeStore.ts     # 主题状态
├── hooks/
│   ├── useUser.ts        # 用户相关的自定义 hooks
│   └── useCart.ts        # 购物车相关的自定义 hooks
├── types/
│   ├── user.ts           # 用户相关类型
│   ├── product.ts        # 产品相关类型
│   └── store.ts          # Store 相关类型
└── utils/
    ├── storage.ts        # 存储工具
    └── api.ts           # API 工具

这种文件组织结构遵循了关注点分离的原则,让不同类型的代码有明确的归属。通过这种结构,开发者可以快速找到相关的代码,新成员也能更容易地理解项目的架构。

6. 自定义 Hooks 封装

自定义 Hooks 是 React 中代码复用的重要方式。通过封装自定义 Hooks,我们可以将复杂的状态逻辑抽象成简单的接口,让组件代码更加清晰。在 Zustand 中,自定义 Hooks 可以帮助我们封装常用的状态选择器和操作。

自定义 Hooks 的价值:

  • 逻辑复用:将常用的状态逻辑封装成可复用的 Hook
  • 接口简化:为组件提供简洁的状态管理接口
  • 关注点分离:将状态逻辑从组件中分离出来
  • 测试友好:可以独立测试 Hook 的逻辑
  • 类型安全:提供类型安全的状态访问接口
// hooks/useUser.ts
export const useUser = () => {
  const user = useUserStore((state) => state.user)
  const isAuthenticated = useUserStore((state) => !!state.user)
  const loading = useUserStore((state) => state.loading)
  const error = useUserStore((state) => state.error)
  
  const { login, logout, updateProfile } = useUserStore()
  
  return {
    user,
    isAuthenticated,
    loading,
    error,
    login,
    logout,
    updateProfile,
  }
}

// hooks/useCart.ts
export const useCart = () => {
  const items = useCartStore((state) => state.items)
  const total = useCartStore((state) => 
    state.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
  )
  const itemCount = useCartStore((state) => 
    state.items.reduce((sum, item) => sum + item.quantity, 0)
  )
  
  const { addItem, removeItem, updateQuantity, clearCart } = useCartStore()
  
  return {
    items,
    total,
    itemCount,
    addItem,
    removeItem,
    updateQuantity,
    clearCart,
  }
}

这些自定义 Hooks 的封装展示了如何将复杂的状态逻辑抽象成简单的接口。通过这种封装,组件只需要关心业务逻辑,而不需要了解底层的状态管理细节。这种模式特别适合团队开发,可以让不同的开发者专注于自己的领域。

6. 与其他状态管理库对比

了解 Zustand 与其他状态管理库的差异,有助于在项目中做出正确的技术选择。不同的状态管理库都有其特定的设计理念和适用场景,选择合适的工具可以显著提升开发效率和代码质量。

技术选型的考虑因素:

  • 项目规模:不同规模的项目适合不同的状态管理方案
  • 团队经验:团队对不同技术栈的熟悉程度
  • 性能要求:应用对性能的具体需求
  • 维护成本:长期维护和扩展的成本考虑
  • 生态系统:工具链和第三方库的支持情况

1. Zustand vs Redux

Redux 是 React 生态系统中最成熟的状态管理库之一,它基于 Flux 架构,提供了可预测的状态管理模式。虽然 Redux 功能强大,但其复杂的样板代码和陡峭的学习曲线让很多开发者望而却步。

Redux 的特点:

  • 成熟稳定:经过多年发展,生态系统完善
  • 可预测性:严格的单向数据流,状态变化可追踪
  • 调试友好:强大的开发者工具支持
  • 中间件系统:丰富的中间件生态
  • 社区支持:大量的学习资源和最佳实践
代码量对比

让我们通过一个具体的例子来看看两者在代码量上的差异:

Redux 实现计数器

// types.ts
export const INCREMENT = 'INCREMENT'
export const DECREMENT = 'DECREMENT'

// actions.ts
export const increment = () => ({ type: INCREMENT })
export const decrement = () => ({ type: DECREMENT })

// reducer.ts
const initialState = { count: 0 }
export const counterReducer = (state = initialState, action) => {
  switch (action.type) {
    case INCREMENT:
      return { count: state.count + 1 }
    case DECREMENT:
      return { count: state.count - 1 }
    default:
      return state
  }
}

// store.ts
import { createStore } from 'redux'
export const store = createStore(counterReducer)

// 组件中使用
import { useSelector, useDispatch } from 'react-redux'
function Counter() {
  const count = useSelector(state => state.count)
  const dispatch = useDispatch()
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => dispatch(increment())}>+</button>
    </div>
  )
}

Zustand 实现计数器

// 一个文件搞定
const useCounterStore = create(set => ({
  count: 0,
  increment: () => set(state => ({ count: state.count + 1 })),
  decrement: () => set(state => ({ count: state.count - 1 })),
}))

// 组件中使用
function Counter() {
  const { count, increment } = useCounterStore()
  return (
    <div>
      <p>{count}</p>
      <button onClick={increment}>+</button>
    </div>
  )
}

对比总结

  • 代码量:Zustand 减少 80% 的样板代码,显著提升开发效率
  • 学习成本:Redux 需要理解 actions、reducers、store 等概念,而 Zustand 只需要掌握 create 函数
  • 开发效率:Zustand 可以更快地实现功能,特别适合快速原型开发
  • 维护成本:Zustand 的代码更简洁,维护成本更低
  • 团队协作:Zustand 的简单性让新团队成员更容易上手

2. Zustand vs Context API

Context API 是 React 内置的状态管理方案,它解决了 props 逐层传递的问题。虽然 Context API 使用简单,但在性能和复杂状态管理方面存在一些限制。

Context API 的特点:

  • 内置支持:React 原生支持,无需额外依赖
  • 简单易用:API 简单,容易理解
  • 组件耦合:需要 Provider 包装组件
  • 性能问题:容易导致不必要的重新渲染
  • 嵌套地狱:多个 Context 会导致组件嵌套过深
性能对比

性能是 Context API 的主要痛点,让我们看看具体的问题:

Context API 的问题

// Context 方案 - 所有消费者都会重新渲染
const AppContext = createContext()

function AppProvider({ children }) {
  const [user, setUser] = useState(null)
  const [theme, setTheme] = useState('light')
  
  // 当任何值改变时,所有使用 AppContext 的组件都会重新渲染
  return (
    <AppContext.Provider value={{ user, setUser, theme, setTheme }}>
      {children}
    </AppContext.Provider>
  )
}

Zustand 的优势

// Zustand 方案 - 精确的重新渲染控制
const useAppStore = create(set => ({
  user: null,
  theme: 'light',
  setUser: user => set({ user }),
  setTheme: theme => set({ theme }),
}))

// 只有当 theme 改变时,这个组件才会重新渲染
function ThemeComponent() {
  const theme = useAppStore(state => state.theme)
  return <div>Theme: {theme}</div>
}

// 只有当 user 改变时,这个组件才会重新渲染
function UserComponent() {
  const user = useAppStore(state => state.user)
  return <div>User: {user?.name}</div>
}

这个对比清楚地展示了 Zustand 在性能方面的优势。Context API 的问题在于它采用的是"发布-订阅"模式,所有订阅者都会收到更新通知。而 Zustand 采用的是"选择器"模式,只有当选择的状态发生变化时,组件才会重新渲染。

3. Zustand vs Recoil

Recoil 是 Facebook 开发的实验性状态管理库,它引入了原子化状态管理的概念。虽然 Recoil 在某些方面很先进,但其复杂的概念模型和实验性质让很多开发者持观望态度。

Recoil 的特点:

  • 原子化状态:将状态分解为最小的原子单位
  • 依赖图:自动管理状态之间的依赖关系
  • 异步支持:内置对异步操作的支持
  • 并发安全:支持 React 的并发特性
  • 实验性质:仍在快速发展中,API 可能会变化
复杂度对比

让我们比较一下两者在概念复杂度上的差异:

Recoil 的概念

// Recoil 需要理解 atoms 和 selectors
const countState = atom({
  key: 'countState',
  default: 0,
})

const doubleCountState = selector({
  key: 'doubleCountState',
  get: ({get}) => {
    const count = get(countState)
    return count * 2
  },
})

function Counter() {
  const [count, setCount] = useRecoilState(countState)
  const doubleCount = useRecoilValue(doubleCountState)
  
  return (
    <div>
      <p>Count: {count}</p>
      <p>Double: {doubleCount}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  )
}

Zustand 的简洁性

// Zustand 更直观
const useCounterStore = create(set => ({
  count: 0,
  increment: () => set(state => ({ count: state.count + 1 })),
  get doubleCount() {
    return this.count * 2
  },
}))

function Counter() {
  const { count, doubleCount, increment } = useCounterStore()
  
  return (
    <div>
      <p>Count: {count}</p>
      <p>Double: {doubleCount}</p>
      <button onClick={increment}>+</button>
    </div>
  )
}

从这个对比可以看出,Zustand 的 API 更加直观和简洁。Recoil 虽然在某些高级场景下很强大,但其概念模型的复杂性可能会增加学习成本和开发复杂度。

4. 选择指南

选择合适的状态管理库需要综合考虑项目的具体需求、团队的技术背景、以及长期的维护成本。没有一种方案是万能的,关键是要找到最适合当前项目的解决方案。

何时选择 Zustand

✅ 适合 Zustand 的场景

  • 中小型项目:状态管理需求不太复杂
  • 快速开发:需要快速原型或 MVP 开发
  • 团队新手:团队成员对状态管理经验不足
  • 包体积敏感:对应用体积有严格要求
  • 简单异步:异步操作相对简单
  • TypeScript 项目:需要良好的类型支持
何时选择 Redux

✅ 适合 Redux 的场景

  • 大型项目:复杂的状态管理需求
  • 团队协作:多人协作开发
  • 可预测性要求:需要严格的状态管理规范
  • 丰富的中间件:需要复杂的异步处理
  • 时间旅行调试:需要强大的调试能力
  • 成熟的生态系统:依赖丰富的第三方库
  • 大型企业级项目
  • 需要时间旅行调试
  • 复杂的状态逻辑
  • 团队已有 Redux 经验
  • 需要丰富的生态系统
何时选择 Context API

✅ 适合 Context API 的场景

  • 简单的主题切换
  • 用户认证状态
  • 不需要频繁更新的全局状态
  • 希望使用 React 原生方案

7. 完整实战案例

让我们构建一个完整的任务管理应用,展示 Zustand 在实际项目中的应用。这个案例将涵盖状态管理的各个方面,包括数据获取、用户交互、状态持久化、错误处理等。通过这个完整的示例,你将看到如何在真实项目中组织和使用 Zustand。

实战案例的特点:

  • 完整的业务逻辑:包含增删改查等常见操作
  • 多个状态管理:任务状态、用户状态、UI 状态的协调
  • 中间件应用:使用 devtools、persist、immer 等中间件
  • 性能优化:合理的状态选择和更新策略
  • 错误处理:完善的错误处理和用户反馈
  • 类型安全:完整的 TypeScript 类型定义

1. 项目结构设计

良好的项目结构是大型应用成功的基础。我们将按照功能模块来组织代码,确保代码的可维护性和可扩展性。

task-manager/
├── src/
│   ├── stores/
│   │   ├── index.ts
│   │   ├── taskStore.ts
│   │   ├── userStore.ts
│   │   └── uiStore.ts
│   ├── components/
│   │   ├── TaskList.tsx
│   │   ├── TaskForm.tsx
│   │   └── UserProfile.tsx
│   ├── hooks/
│   │   └── useTasks.ts
│   └── types/
│       └── index.ts

这个项目结构遵循了关注点分离的原则,将不同类型的代码分别放置在对应的目录中。这样的组织方式让代码更容易维护和扩展。

2. 类型定义

类型定义是 TypeScript 项目的重要组成部分,它不仅提供了类型安全,还起到了文档的作用。我们将定义应用中的核心数据结构。

// types/index.ts
export interface Task {
  id: string
  title: string
  description: string
  completed: boolean
  priority: 'low' | 'medium' | 'high'
  dueDate: string
  createdAt: string
  updatedAt: string
  userId: string
}

export interface User {
  id: string
  name: string
  email: string
  avatar?: string
}

export interface Filter {
  status: 'all' | 'completed' | 'pending'
  priority: 'all' | 'low' | 'medium' | 'high'
  search: string
}

这些类型定义清晰地描述了应用的数据结构。通过使用联合类型和可选属性,我们可以确保数据的一致性和完整性。

3. 任务状态管理

任务状态管理是这个应用的核心。我们将使用多个中间件来增强 store 的功能,包括开发者工具、状态持久化和不可变更新。

// stores/taskStore.ts
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
import { immer } from 'zustand/middleware/immer'

interface TaskState {
  tasks: Task[]
  filter: Filter
  loading: boolean
  error: string | null
  
  // 任务操作
  fetchTasks: () => Promise<void>
  addTask: (task: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>) => Promise<void>
  updateTask: (id: string, updates: Partial<Task>) => Promise<void>
  deleteTask: (id: string) => Promise<void>
  toggleTask: (id: string) => Promise<void>
  
  // 过滤操作
  setFilter: (filter: Partial<Filter>) => void
  clearFilter: () => void
  
  // 计算属性
  getFilteredTasks: () => Task[]
  getTaskStats: () => { total: number; completed: number; pending: number }
}

export const useTaskStore = create<TaskState>()(
  devtools(
    persist(
      immer((set, get) => ({
        tasks: [],
        filter: {
          status: 'all',
          priority: 'all',
          search: ''
        },
        loading: false,
        error: null,
        
        fetchTasks: async () => {
          set((state) => {
            state.loading = true
            state.error = null
          })
          
          try {
            const response = await fetch('/api/tasks')
            const tasks = await response.json()
            
            set((state) => {
              state.tasks = tasks
              state.loading = false
            })
          } catch (error) {
            set((state) => {
              state.error = error.message
              state.loading = false
            })
          }
        },
        
        addTask: async (taskData) => {
          const tempId = `temp-${Date.now()}`
          const optimisticTask: Task = {
            ...taskData,
            id: tempId,
            createdAt: new Date().toISOString(),
            updatedAt: new Date().toISOString(),
          }
          
          // 乐观更新
          set((state) => {
            state.tasks.push(optimisticTask)
          })
          
          try {
            const response = await fetch('/api/tasks', {
              method: 'POST',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify(taskData)
            })
            
            const realTask = await response.json()
            
            set((state) => {
              const index = state.tasks.findIndex(t => t.id === tempId)
              if (index !== -1) {
                state.tasks[index] = realTask
              }
            })
          } catch (error) {
            // 回滚
            set((state) => {
              state.tasks = state.tasks.filter(t => t.id !== tempId)
              state.error = error.message
            })
          }
        },
        
        updateTask: async (id, updates) => {
          const originalTask = get().tasks.find(t => t.id === id)
          
          // 乐观更新
          set((state) => {
            const task = state.tasks.find(t => t.id === id)
            if (task) {
              Object.assign(task, updates, { updatedAt: new Date().toISOString() })
            }
          })
          
          try {
            await fetch(`/api/tasks/${id}`, {
              method: 'PUT',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify(updates)
            })
          } catch (error) {
            // 回滚
            if (originalTask) {
              set((state) => {
                const index = state.tasks.findIndex(t => t.id === id)
                if (index !== -1) {
                  state.tasks[index] = originalTask
                }
                state.error = error.message
              })
            }
          }
        },
        
        deleteTask: async (id) => {
          const taskToDelete = get().tasks.find(t => t.id === id)
          
          set((state) => {
            state.tasks = state.tasks.filter(t => t.id !== id)
          })
          
          try {
            await fetch(`/api/tasks/${id}`, { method: 'DELETE' })
          } catch (error) {
            if (taskToDelete) {
              set((state) => {
                state.tasks.push(taskToDelete)
                state.error = error.message
              })
            }
          }
        },
        
        toggleTask: async (id) => {
          const task = get().tasks.find(t => t.id === id)
          if (task) {
            await get().updateTask(id, { completed: !task.completed })
          }
        },
        
        setFilter: (newFilter) => {
          set((state) => {
            Object.assign(state.filter, newFilter)
          })
        },
        
        clearFilter: () => {
          set((state) => {
            state.filter = {
              status: 'all',
              priority: 'all',
              search: ''
            }
          })
        },
        
        getFilteredTasks: () => {
          const { tasks, filter } = get()
          
          return tasks.filter(task => {
            // 状态过滤
            if (filter.status === 'completed' && !task.completed) return false
            if (filter.status === 'pending' && task.completed) return false
            
            // 优先级过滤
            if (filter.priority !== 'all' && task.priority !== filter.priority) return false
            
            // 搜索过滤
            if (filter.search && !task.title.toLowerCase().includes(filter.search.toLowerCase())) {
              return false
            }
            
            return true
          })
        },
        
        getTaskStats: () => {
          const tasks = get().tasks
          return {
            total: tasks.length,
            completed: tasks.filter(t => t.completed).length,
            pending: tasks.filter(t => !t.completed).length
          }
        },
      })),
      {
        name: 'task-store',
        partialize: (state) => ({
          tasks: state.tasks,
          filter: state.filter
        })
      }
    ),
    { name: 'task-store' }
  )
)

这个任务状态管理的实现展示了现代应用中的几个重要概念:

  1. 乐观更新:在网络请求完成前先更新 UI,提升用户体验
  2. 错误回滚:当请求失败时,回滚到之前的状态
  3. 状态计算:提供计算属性来获取派生状态
  4. 过滤功能:实现复杂的数据过滤逻辑
  5. 中间件组合:使用多个中间件来增强功能

4. 用户状态管理

用户状态管理处理用户的认证和个人信息。这个 store 相对简单,主要负责登录、登出和用户信息的管理。

// stores/userStore.ts
export const useUserStore = create<UserState>()(
  devtools((set, get) => ({
    user: null,
    isAuthenticated: false,
    loading: false,
    error: null,
    
    login: async (email: string, password: string) => {
      set({ loading: true, error: null })
      
      try {
        const response = await fetch('/api/auth/login', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ email, password })
        })
        
        if (!response.ok) {
          throw new Error('登录失败')
        }
        
        const { user, token } = await response.json()
        
        localStorage.setItem('token', token)
        set({ user, isAuthenticated: true, loading: false })
      } catch (error) {
        set({ error: error.message, loading: false })
      }
    },
    
    logout: () => {
      localStorage.removeItem('token')
      set({ user: null, isAuthenticated: false })
    },
    
    updateProfile: async (updates: Partial<User>) => {
      const originalUser = get().user
      
      // 乐观更新
      set((state) => {
        if (state.user) {
          state.user = { ...state.user, ...updates }
        }
      })
      
      try {
        const response = await fetch('/api/user/profile', {
          method: 'PUT',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(updates)
        })
        
        const updatedUser = await response.json()
        set({ user: updatedUser })
      } catch (error) {
        // 回滚
        set({ user: originalUser, error: error.message })
      }
    },
  }), { name: 'user-store' })
)

用户状态管理展示了认证相关的典型模式。注意这里也使用了乐观更新和错误回滚的策略,确保用户体验的流畅性。

5. 自定义 Hooks

自定义 Hooks 将复杂的状态逻辑封装成简单的接口,让组件代码更加清晰。这个 Hook 不仅提供了数据,还包含了计算属性和操作方法。

// hooks/useTasks.ts
export const useTasks = () => {
  const store = useTaskStore()
  
  const filteredTasks = useMemo(() => {
    return store.getFilteredTasks()
  }, [store.tasks, store.filter])
  
  const stats = useMemo(() => {
    return store.getTaskStats()
  }, [store.tasks])
  
  return {
    // 数据
    tasks: filteredTasks,
    allTasks: store.tasks,
    filter: store.filter,
    stats,
    loading: store.loading,
    error: store.error,
    
    // 操作
    addTask: store.addTask,
    updateTask: store.updateTask,
    deleteTask: store.deleteTask,
    toggleTask: store.toggleTask,
    setFilter: store.setFilter,
    clearFilter: store.clearFilter,
    fetchTasks: store.fetchTasks,
  }
}

这个自定义 Hook 展示了如何将状态管理逻辑从组件中抽离出来。通过使用 useMemo 来缓存计算结果,我们可以避免不必要的重新计算,提升性能。

6. 组件实现

组件实现展示了如何在实际的 React 组件中使用我们的状态管理逻辑。注意组件只关心 UI 的渲染,所有的状态逻辑都被封装在了 Hook 中。

// components/TaskList.tsx
export function TaskList() {
  const { tasks, stats, loading, error, toggleTask, deleteTask } = useTasks()
  
  if (loading) {
    return <div className="loading">加载中...</div>
  }
  
  if (error) {
    return <div className="error">错误: {error}</div>
  }
  
  return (
    <div className="task-list">
      <div className="stats">
        <span>总计: {stats.total}</span>
        <span>已完成: {stats.completed}</span>
        <span>待完成: {stats.pending}</span>
      </div>
      
      {tasks.map(task => (
        <div key={task.id} className={`task-item ${task.completed ? 'completed' : ''}`}>
          <input
            type="checkbox"
            checked={task.completed}
            onChange={() => toggleTask(task.id)}
          />
          <div className="task-content">
            <h3>{task.title}</h3>
            <p>{task.description}</p>
            <span className={`priority ${task.priority}`}>{task.priority}</span>
          </div>
          <button onClick={() => deleteTask(task.id)}>删除</button>
        </div>
      ))}
    </div>
  )
}

这个组件实现展示了现代 React 开发的最佳实践:组件专注于 UI 渲染,状态管理逻辑被抽象到自定义 Hook 中。这种分离让代码更容易测试和维护。

8. 常见问题解答

在使用 Zustand 的过程中,开发者经常会遇到一些常见的问题。这个章节收集了最常见的问题和解决方案,帮助你更好地使用 Zustand。

1. 状态管理相关

状态管理是 Zustand 的核心功能,但在实际使用中,开发者经常会遇到性能问题、状态共享问题等。让我们来看看如何解决这些问题。

Q: 如何避免状态更新时的性能问题?

A: 使用精确的选择器

// ❌ 不好:选择整个 store
const store = useStore()

// ✅ 好:只选择需要的数据
const user = useStore(state => state.user)
const theme = useStore(state => state.theme)

// ✅ 更好:使用 shallow 比较
const { user, theme } = useStore(
  state => ({ user: state.user, theme: state.theme }),
  shallow
)
Q: 如何在 store 之间共享数据?

A: 使用订阅或者共享状态

// 方法1:订阅其他 store
const useStoreB = create((set, get) => ({
  data: [],
  
  init: () => {
    // 订阅 store A 的变化
    useStoreA.subscribe(
      state => state.data,
      (data) => {
        set({ relatedData: data.filter(item => item.important) })
      }
    )
  }
}))

// 方法2:创建共享状态
const useSharedStore = create(() => ({
  sharedData: null,
  updateSharedData: (data) => set({ sharedData: data })
}))

这些状态管理的问题和解决方案展示了如何在实际项目中优化 Zustand 的使用。状态共享的两种方法各有优缺点,选择哪种取决于具体的使用场景。

2. 异步操作相关

异步操作是现代 Web 应用的重要组成部分,但也是容易出问题的地方。并发请求、数据缓存、错误处理等都需要仔细考虑。

Q: 如何处理多个并发请求?

A: 使用请求 ID 或 AbortController

并发请求的问题在于可能会出现竞态条件,即后发起的请求可能先完成,导致数据不一致。我们可以使用请求 ID 来确保只处理最新请求的结果。

const useStore = create((set, get) => ({
  data: null,
  currentRequestId: null,
  
  fetchData: async (params) => {
    const requestId = Date.now().toString()
    set({ currentRequestId: requestId })
    
    try {
      const data = await api.fetchData(params)
      
      // 只处理最新请求的结果
      if (get().currentRequestId === requestId) {
        set({ data })
      }
    } catch (error) {
      if (get().currentRequestId === requestId) {
        set({ error: error.message })
      }
    }
  }
}))

这种请求 ID 的方案简单有效,可以避免大部分的竞态条件问题。对于需要真正取消网络请求的场景,建议使用 AbortController。

Q: 如何实现数据缓存?

A: 使用 Map 或对象存储缓存

数据缓存可以显著提升应用性能,减少不必要的网络请求。这里展示了一个简单但实用的缓存实现:

const useApiStore = create((set, get) => ({
  cache: new Map(),
  
  fetchWithCache: async (url) => {
    const cached = get().cache.get(url)
    if (cached && Date.now() - cached.timestamp < 5 * 60 * 1000) {
      return cached.data
    }
    
    const data = await fetch(url).then(r => r.json())
    
    set(state => {
      const newCache = new Map(state.cache)
      newCache.set(url, { data, timestamp: Date.now() })
      return { cache: newCache }
    })
    
    return data
  }
}))

这个缓存实现考虑了缓存过期时间,确保数据的新鲜度。在实际项目中,你可能还需要考虑缓存大小限制、缓存清理策略等。

3. TypeScript 相关

TypeScript 的类型系统是 Zustand 的重要优势之一。正确的类型定义不仅能提供类型安全,还能改善开发体验。

Q: 如何正确定义 store 的类型?

A: 分离状态和操作的类型定义

分离状态和操作的类型定义让代码更清晰,也更容易维护。这种模式在大型项目中特别有用:

interface State {
  count: number
  user: User | null
}

interface Actions {
  increment: () => void
  setUser: (user: User) => void
}

type Store = State & Actions

const useStore = create<Store>((set) => ({
  count: 0,
  user: null,
  increment: () => set(state => ({ count: state.count + 1 })),
  setUser: (user) => set({ user }),
}))

这种类型定义方式的优势在于它将状态和操作分开,让类型定义更加清晰。在团队开发中,这种模式也有助于代码审查和维护。

4. 调试相关

调试是开发过程中的重要环节。Zustand 提供了多种调试方式,从简单的日志输出到集成的开发者工具。

Q: 如何调试 Zustand 状态?

A: 使用 DevTools 中间件

DevTools 中间件让我们可以在浏览器的 Redux DevTools 中查看和调试 Zustand 状态:

import { devtools } from 'zustand/middleware'

const useStore = create(
  devtools(
    (set) => ({
      count: 0,
      increment: () => set(
        state => ({ count: state.count + 1 }),
        false,
        'increment'  // 动作名称
      ),
    }),
    { name: 'counter-store' }
  )
)

通过为每个动作提供名称,我们可以在 DevTools 中清楚地看到状态变化的历史,这对于调试复杂的状态逻辑非常有用。

Q: 如何在开发环境中监控状态变化?

A: 创建调试中间件

自定义的调试中间件可以提供更详细的状态变化信息,帮助我们理解应用的行为:

const logger = (config) => (set, get, api) =>
  config(
    (...args) => {
      console.log('Previous state:', get())
      set(...args)
      console.log('New state:', get())
    },
    get,
    api
  )

const useStore = create(logger((set) => ({
  // store 定义
})))

这种调试中间件在开发环境中非常有用,可以帮助我们追踪状态变化的详细过程。记住在生产环境中移除这些调试代码以避免性能影响。

5. 最佳实践相关

最佳实践是从实际项目经验中总结出来的指导原则,它们可以帮助我们避免常见的陷阱,构建更好的应用。

Q: 什么时候应该拆分 store?

A: 遵循单一职责原则

Store 的拆分是一个重要的架构决策。一般来说,当一个 Store 变得过于复杂或者包含不相关的状态时,就应该考虑拆分:

// ✅ 好:按功能拆分
const useUserStore = create(() => ({ /* 用户相关 */ }))
const useCartStore = create(() => ({ /* 购物车相关 */ }))
const useThemeStore = create(() => ({ /* 主题相关 */ }))

// ❌ 不好:所有状态放在一起
const useAppStore = create(() => ({
  user: null,
  cart: [],
  theme: 'light',
  products: [],
  orders: [],
  // ... 太多不相关的状态
}))

Store 拆分的好处包括更好的代码组织、更容易的测试、更清晰的职责划分。但也要避免过度拆分,导致状态管理变得碎片化。

Q: 如何组织大型项目的状态?

A: 使用模块化的文件结构

良好的文件组织结构对于大型项目的维护至关重要。这里推荐一种实用的组织方式:

stores/
├── index.ts          # 导出所有 stores
├── slices/           # 状态切片
│   ├── userSlice.ts
│   ├── cartSlice.ts
│   └── themeSlice.ts
├── middleware/       # 自定义中间件
│   └── logger.ts
└── types/           # 类型定义
    └── index.ts

📖 结语

恭喜你完成了 Zustand 进阶篇的学习!

如果这篇文章对你有帮助,请点赞收藏支持一下!有问题欢迎在评论区讨论。 🎉

# Bsin-App Uni:面向未来的跨端开发框架深度解析

作者 boleixiongdi
2025年7月7日 13:38

引言

在当今移动互联网时代,跨端开发已经成为开发者的刚需。如何在保证开发效率的同时,确保应用在不同平台上的一致性体验,一直是前端开发者面临的核心挑战。今天要为大家介绍的 Bsin-App Uni 正是为解决这一痛点而生的现代化跨端开发框架。

什么是 Bsin-App Uni?

Bsin-App Uni 是一个基于 uni-app 构建的跨终端开发框架,专为开发者打造面向未来的开发体验。它不仅仅是一个简单的模板,而是一个完整的开发解决方案,集成了现代前端开发的最佳实践。

核心特性

  • 🔥 最新技术栈:基于 Vite5、Vue3、TypeScript 构建
  • 🎨 现代化UI:集成 Wot-Design-UI 组件库,提供丰富的UI组件
  • 🚀 开发体验:内置 UnoCss 原子化CSS,开发效率倍增
  • 📱 真正跨端:支持 H5、iOS、Android、微信小程序等多端发布
  • 🛠️ 工程化完备:完整的代码规范、构建流程、自动化部署

技术栈深度解析

1. 构建工具:Vite5

Bsin-App Uni 选择了最新的 Vite5 作为构建工具,相比传统的 webpack:

// vite.config.js 关键配置
export default defineConfig({
  plugins: [
    uni(),
    Unocss(),
    AutoImport({
      imports: ['vue', 'uni-app'],
      dts: true,
    }),
  ],
  build: {
    sourcemap: process.env.NODE_ENV === 'development',
    rollupOptions: {
      external: ['vue', 'uni-app'],
    },
  },
})

优势:

  • 冷启动时间减少 10+ 倍
  • 热更新速度提升 5+ 倍
  • 更好的 Tree-shaking 支持

2. 前端框架:Vue3 + TypeScript

框架采用了 Vue3 Composition API + TypeScript 的组合:

// 典型的页面组件结构
<template>
  <view class="page-container">
    <wd-button @click="handleSubmit">{{ t('submit') }}</wd-button>
  </view>
</template>

<script setup lang="ts">
import { useI18n } from '@/hooks'

interface FormData {
  name: string
  email: string
}

const { t } = useI18n()
const formData = ref<FormData>({
  name: '',
  email: ''
})

const handleSubmit = () => {
  // 类型安全的处理逻辑
}
</script>

优势:

  • 更好的类型提示和错误检查
  • 更清晰的代码结构
  • 更好的 IDE 支持

3. 样式方案:UnoCss + Wot-Design-UI

UnoCss 原子化CSS

<template>
  <!-- 传统方式 -->
  <view class="container">
    <view class="card">
      <text class="title">标题</text>
    </view>
  </view>

  <!-- UnoCss 方式 -->
  <view class="min-h-100vh flex items-center justify-center bg-gradient-to-r from-blue-500 to-purple-600">
    <view class="bg-white rounded-lg shadow-xl p-6 max-w-md">
      <text class="text-2xl font-bold text-gray-800">标题</text>
    </view>
  </view>
</template>

Wot-Design-UI 组件库

<template>
  <view class="demo-container">
    <!-- 表单组件 -->
    <wd-form ref="form" :model="model">
      <wd-input v-model="model.name" label="姓名" placeholder="请输入姓名" />
      <wd-picker v-model="model.city" label="城市" :columns="cityColumns" />
      <wd-button type="primary" @click="handleSubmit">提交</wd-button>
    </wd-form>
    
    <!-- 弹窗组件 -->
    <wd-toast ref="toast" />
    <wd-loading :loading="loading" />
  </view>
</template>

4. 路由管理:Uni Mini Router

// 路由配置示例
const router = useRouter()

// 页面跳转
router.push({ name: 'userDetail', params: { id: '123' } })

// 路由守卫
router.beforeEach((to, from, next) => {
  // 权限验证
  if (to.meta.requireAuth && !isLoggedIn()) {
    next({ name: 'login' })
  } else {
    next()
  }
})

架构设计与最佳实践

1. 目录结构设计

src/
├── components/          # 公共组件
├── pages/              # 页面组件
├── layouts/            # 布局组件
├── store/              # 状态管理
├── services/           # API服务
├── utils/              # 工具函数
├── hooks/              # 自定义Hook
└── types/              # TypeScript类型

2. 状态管理:Pinia

// store/user.ts
export const useUserStore = defineStore('user', () => {
  const userInfo = ref<UserInfo | null>(null)
  const isLoggedIn = computed(() => !!userInfo.value)

  const login = async (credentials: LoginCredentials) => {
    try {
      const response = await authService.login(credentials)
      userInfo.value = response.data
      return response
    } catch (error) {
      throw new Error('登录失败')
    }
  }

  const logout = () => {
    userInfo.value = null
    uni.clearStorageSync()
  }

  return {
    userInfo,
    isLoggedIn,
    login,
    logout
  }
})

3. API 服务管理

// services/api/user.ts
import { request } from '../request'

export interface LoginCredentials {
  username: string
  password: string
}

export interface UserInfo {
  id: string
  username: string
  email: string
}

export const userAPI = {
  login: (data: LoginCredentials) => 
    request.post<UserInfo>('/auth/login', data),
  
  getUserInfo: () => 
    request.get<UserInfo>('/user/profile'),
  
  updateProfile: (data: Partial<UserInfo>) => 
    request.put<UserInfo>('/user/profile', data)
}

开发体验优化

1. 自动导入

// 无需手动导入
const router = useRouter()  // 自动导入
const store = useUserStore()  // 自动导入
const { t } = useI18n()  // 自动导入

2. 代码规范

框架内置了完整的代码规范配置:

{
  "scripts": {
    "lint": "eslint . --fix",
    "format": "prettier --write .",
    "prepare": "husky install"
  }
}

3. Git 提交规范

# 使用交互式提交
pnpm cz

# 自动生成规范的提交信息
✨ feat: 添加用户登录功能
🐞 fix: 修复表单验证问题
📃 docs: 更新API文档

实战案例:构建一个完整的用户模块

让我们通过一个实际案例来展示框架的强大功能:

1. 创建用户信息页面

<route lang="json5">
{
  style: { navigationBarTitleText: '用户信息' },
  name: 'userProfile'
}
</route>

<template>
  <view class="user-profile">
    <!-- 用户头像 -->
    <view class="avatar-section">
      <image class="avatar" :src="userInfo?.avatar || defaultAvatar" />
      <text class="username">{{ userInfo?.username }}</text>
    </view>

    <!-- 用户信息表单 -->
    <wd-form ref="form" :model="formData" :rules="rules">
      <wd-input 
        v-model="formData.email" 
        label="邮箱" 
        placeholder="请输入邮箱"
        :disabled="!isEditing"
      />
      <wd-input 
        v-model="formData.phone" 
        label="手机号" 
        placeholder="请输入手机号"
        :disabled="!isEditing"
      />
      <wd-textarea 
        v-model="formData.bio" 
        label="个人简介" 
        placeholder="请输入个人简介"
        :disabled="!isEditing"
      />
    </wd-form>

    <!-- 操作按钮 -->
    <view class="action-buttons">
      <wd-button 
        v-if="!isEditing" 
        type="primary" 
        @click="startEdit"
      >
        编辑
      </wd-button>
      <template v-else>
        <wd-button @click="cancelEdit">取消</wd-button>
        <wd-button type="primary" @click="saveChanges">保存</wd-button>
      </template>
    </view>
  </view>
</template>

<script setup lang="ts">
import { userAPI } from '@/services/api/user'
import { useUserStore } from '@/store/user'

interface FormData {
  email: string
  phone: string
  bio: string
}

const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)

const isEditing = ref(false)
const formData = ref<FormData>({
  email: '',
  phone: '',
  bio: ''
})

const rules = {
  email: [
    { required: true, message: '请输入邮箱' },
    { pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: '邮箱格式不正确' }
  ],
  phone: [
    { required: true, message: '请输入手机号' },
    { pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确' }
  ]
}

const startEdit = () => {
  isEditing.value = true
  formData.value = {
    email: userInfo.value?.email || '',
    phone: userInfo.value?.phone || '',
    bio: userInfo.value?.bio || ''
  }
}

const cancelEdit = () => {
  isEditing.value = false
  formData.value = {
    email: '',
    phone: '',
    bio: ''
  }
}

const saveChanges = async () => {
  try {
    await userAPI.updateProfile(formData.value)
    await userStore.refreshUserInfo()
    isEditing.value = false
    uni.showToast({
      title: '保存成功',
      icon: 'success'
    })
  } catch (error) {
    uni.showToast({
      title: '保存失败',
      icon: 'error'
    })
  }
}

onMounted(() => {
  if (!userInfo.value) {
    userStore.fetchUserInfo()
  }
})
</script>

<style lang="scss" scoped>
.user-profile {
  padding: 32rpx;
  background: var(--bsin-bgPrimary);
  min-height: 100vh;
}

.avatar-section {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-bottom: 60rpx;
}

.avatar {
  width: 160rpx;
  height: 160rpx;
  border-radius: 50%;
  margin-bottom: 20rpx;
}

.username {
  font-size: 36rpx;
  font-weight: bold;
  color: var(--bsin-textPrimary);
}

.action-buttons {
  display: flex;
  gap: 20rpx;
  margin-top: 60rpx;
}
</style>

2. 集成主题切换

// hooks/useTheme.ts
export const useTheme = () => {
  const themeStore = useThemeStore()
  const { theme, followSystem } = storeToRefs(themeStore)

  const toggleTheme = () => {
    themeStore.setTheme(theme.value === 'light' ? 'dark' : 'light')
  }

  const setFollowSystem = (follow: boolean) => {
    themeStore.setFollowSystem(follow)
  }

  // 监听系统主题变化
  watch(followSystem, (newVal) => {
    if (newVal) {
      uni.onThemeChange?.((res) => {
        themeStore.setTheme(res.theme)
      })
    }
  })

  return {
    theme,
    followSystem,
    toggleTheme,
    setFollowSystem
  }
}

性能优化策略

1. 代码分割

// 路由级别的代码分割
const routes = [
  {
    path: '/user',
    component: () => import('@/pages/user/index.vue')
  },
  {
    path: '/order',
    component: () => import('@/pages/order/index.vue')
  }
]

2. 组件懒加载

<template>
  <view>
    <AsyncComponent v-if="showComponent" />
  </view>
</template>

<script setup lang="ts">
const AsyncComponent = defineAsyncComponent(() => import('@/components/HeavyComponent.vue'))
</script>

3. 图片优化

// utils/image.ts
export const getOptimizedImageUrl = (url: string, options: {
  width?: number
  height?: number
  quality?: number
} = {}) => {
  const { width = 750, height, quality = 80 } = options
  
  // 根据不同平台返回优化后的图片URL
  // #ifdef H5
  return `${url}?imageView2/2/w/${width}/h/${height}/q/${quality}`
  // #endif
  
  // #ifdef MP-WEIXIN
  return `${url}?x-oss-process=image/resize,w_${width},h_${height}/quality,q_${quality}`
  // #endif
  
  return url
}

部署与发布

1. 多环境配置

# 开发环境
npm run dev:h5
npm run dev:mp-weixin

# 测试环境
npm run build:h5:test
npm run build:mp-weixin:test

# 生产环境
npm run build:h5
npm run build:mp-weixin

2. CI/CD 配置

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
      - name: Install dependencies
        run: pnpm install
      - name: Build
        run: pnpm build:h5
      - name: Deploy to OSS
        run: |
          # 部署到阿里云 OSS
          npm run deploy:oss

总结

Bsin-App Uni 作为一个现代化的跨端开发框架,具有以下核心优势:

  1. 技术先进:基于最新的前端技术栈,确保项目的前瞻性
  2. 开发效率:完整的工程化配置,让开发者专注于业务逻辑
  3. 跨端一致:真正的一次开发,多端发布
  4. 扩展性强:模块化设计,易于定制和扩展
  5. 最佳实践:内置行业最佳实践,新手也能快速上手

对于希望在跨端开发领域提升效率、保证质量的开发者和团队来说,Bsin-App Uni 无疑是一个值得深入了解和使用的优秀框架。


项目地址: gitee.com/s11e-DAO/bs…

pixijs 的填充渲染错误,如何处理?

2025年7月7日 13:36

大家好,我是前端西瓜哥。

经常用 pixijs 的小伙伴或许注意到了,在 pixijs 下绘制多边形并使用填充,在很多情况下渲染是正确的,如果多边形是自交的,填充就会有问题。

这里使用的 pixi.js 版本为 7.4.2

我们写一段 pixijs 代码,绘制一个自交多边形。

import * as PIXI from'pixi.js';

const app = new PIXI.Application({
background: '#eee',
antialias: true,
});

const polygonPoints = [
  { x: 200, y: 27 },
  { x: 200, y: 200 },
  { x: 233, y: 110 },
  { x: 176, y: 181 },
  { x: 98, y: 138 },
  { x: 95, y: 68 },
  { x: 130, y: 27 },
];

const polygon = new PIXI.Graphics()
  .beginFill(0xff0000, 0.3)
  .lineStyle(2, 0x000000)
  .drawPolygon(polygonPoints);

app.stage.addChild(polygon);

document.body.appendChild(app.view);

渲染结果:

图片

我们来看看是什么会导致这个原因。

在线示例

这里提供一个在线示例,读者可以修改多边形的点,观测不同三角剖分的在各种场景下的区别。

codesandbox.io/p/sandbox/2…

ear cutting 三角剖分

首先需要理解 pixijs 的图形是怎么渲染的?

pixijs 是使用 WebGL 进行渲染的,WebGL 渲染的常见方式是绘制三角形,大量的三角形进行组合,便绘制出了图形。

对于 pixijs,其图形的填充和描边,分别都需要 通过三角剖分为顶点,并设置颜色信息,然后提供给 WebGL 调用绘制。

三角剖分(triangulation)是一种将图形转为多个三角形表达的算法,是计算机图形学中的经典课题。比如矩形可以用两个三角形表达,圆形可以用一圈的三角形表达。

pixijs 选择了使用 earcut 的第三方库来队多边形进行三角剖分。

github.com/mapbox/earc…

earcut 库实现了一种改进的耳切(ear cutting)算法。

所谓耳切算法,就是遍历多边形上的点不断地将突出的三角形 “耳朵” 切掉,直到切完整个多边形,这些切出来的三角形就是我们想要的最终结果。

如下图,连续的点 2、3、4 构成的三角形不会包含其他的点,把它作为 “耳朵” 切割出来。

图片

earcut 库的优点是快且小(压缩后 3KB),因为牺牲了质量,对于 pixijs 这种追求高性能高速度的渲染引擎,是合适的选择。

但问题在于 ear cutting 算法限制很大,只支持简单多边形,像是自交的多边形,渲染结果可能不符合预期,这便是 pixijs 的填充效果错误的原因。

简单多边形效果是正确的。

const polygon = [  { x: 200, y: 27 },  { x: 180, y: 100 },  { x: 233, y: 110 },  { x: 176, y: 181 },  { x: 98, y: 138 },  { x: 95, y: 68 },  { x: 130, y: 27 },];

earcut(polygon.map((p) => [p.x, p.y]).flat(), []2);
// 结果如下,三个一组构成三角形
// [
//   5, 6, 0, 
//   1, 2, 3, 
//   3, 4, 5, 
//   5, 0, 1, 
//   1, 3, 5
// ]

图片但如果多边形自交了,渲染明显会相当怪异,不符合预期。

一般我们需要 even-odd 或 non-zero。

图片

即使不用这两种填充规则,用另外的填充规则,比如 figma 的将所有封闭区域都填充,ear cutting 这种把内容填充到多边形外也是很奇怪的

const polygon = [  { x: 200, y: 27 },  { x: 200, y: 200 },  { x: 233, y: 110 },  { x: 176, y: 181 },  { x: 98, y: 138 },  { x: 95, y: 68 },  { x: 130, y: 27 },];

earcut(polygon.map((p) => [p.x, p.y]).flat(), []2);
// 结果如下,三个一组构成三角形
// [
//   5, 6, 0,
//   2, 3, 4,
//   4, 5, 0,
//   2, 4, 0
// ]

图片

tess 三角剖分

为此我们需要换个合适的,能处理复杂情况的算法。

我们可以看看 pixijs 的一个第三方库 @pixi-essentials/svg,这个库的作用是将 svg 转为 pixijs 的图形进行渲染。

github.com/ShukantPal/…

所以需要保持 svg 原有的正确填充效果,这个库使用了 libtess 来做三角剖分。

github.com/brendankenn…

libtess 是一个多边形镶嵌(polygon tesselation)库,从 OpenGL Utility Library(GLU)移植过来,用 javascript 重写。

对于前面用到的示例,我们换成 libtess 来试试。

因为是基于 GLU 迁移过来的,所以 API 挺复杂的,出于易用性,对 libtess 做了一层封装。

下面是 libtess 的示例中一个封装好的方法。

import * as libtess from'libtess';

const tessy = (function initTesselator() {
// function called for each vertex of tesselator output
function vertexCallback(data, polyVertArray) {
    polyVertArray[polyVertArray.length] = data[0];
    polyVertArray[polyVertArray.length] = data[1];
  }
function begincallback(type) {
    if (type !== libtess.primitiveType.GL_TRIANGLES) {
      console.log('expected TRIANGLES but got type: ' + type);
    }
  }
function errorcallback(errno) {
    console.log('error callback');
    console.log('error number: ' + errno);
  }
// callback for when segments intersect and must be split
function combinecallback(coords, data, weight) {
    // console.log('combine callback');
    return [coords[0], coords[1], coords[2]];
  }
function edgeCallback(flag) {
    // don't really care about the flag, but need no-strip/no-fan behavior
    // console.log('edge flag: ' + flag);
  }

var tessy = new libtess.GluTesselator();
// tessy.gluTessProperty(libtess.gluEnum.GLU_TESS_WINDING_RULE, libtess.windingRule.GLU_TESS_WINDING_POSITIVE);
  tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_VERTEX_DATA, vertexCallback);
  tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_BEGIN, begincallback);
  tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_ERROR, errorcallback);
  tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_COMBINE, combinecallback);
  tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_EDGE_FLAG, edgeCallback);

return tessy;
})();

function triangulate(contours) {
// libtess will take 3d verts and flatten to a plane for tesselation
// since only doing 2d tesselation here, provide z=1 normal to skip
// iterating over verts only to get the same answer.
// comment out to test normal-generation code
  tessy.gluTessNormal(001);

var triangleVerts = [];
  tessy.gluTessBeginPolygon(triangleVerts);

for (var i = 0; i < contours.length; i++) {
    tessy.gluTessBeginContour();
    var contour = contours[i];
    for (var j = 0; j < contour.length; j += 2) {
      var coords = [contour[j], contour[j + 1]0];
      tessy.gluTessVertex(coords, coords);
    }
    tessy.gluTessEndContour();
  }

// finish polygon (and time triangulation process)
  tessy.gluTessEndPolygon();

return triangleVerts;
}

下面是简单多边形的效果,这里还对比了 earcut 的效果。

const arr = polygon.map((p) => [p.x, p.y]).flat();
triangulate([arr]);
// 返回的不是索引,是图形的点位置,
// 两个为一个点,三个点为一个三角形
// [
//   130, 27,
//   98, 138,
//   95, 68,
//
//   98, 138,
//   130, 27,
//   176, 181,
//
//   176, 181,
//   130, 27,
//   180, 100,
//
//   180, 100,
//   130, 27,
//   200, 27,
//
//   233, 110,
//   176, 181,
//   180, 100
// ]

图片

然后是自交多边形,这次渲染对了。

图片

除了 libtess 库,还有其他的第三方库也迁移了 GLU tesselator。有:

具体不同库的效果谁更好我没有研究过,但这里暂且列出一些以供读者选择和测试。

pixijs 上如何改造?

下面我们看如何给 pixijs 用上这个新的三角剖分算法。

说实在的,pixijs 的改造方式算不上很优雅,但还是可以改的。

pixijs 暴露了 graphicsUtils 对象,该对象下放置了不同图形的三角化逻辑。

declare const graphicsUtils: {
  // 多边形
  buildPoly: IShapeBuildCommand;
  buildCircle: IShapeBuildCommand;
  buildRectangle: IShapeBuildCommand;
  buildRoundedRectangle: IShapeBuildCommand;
  buildLine: typeof buildLine;
  // ...
};

我们需要改一下 graphicsUtils.buildPoly.triangulate,设置为我们的新的  triangulate 方法。

这里用的是 tess2 库。

import { graphicsUtils } from'pixi.js';
import * as Tess2 from'tess2';

// 重写原来的 buildPoly.triangulate 方法
graphicsUtils.buildPoly.triangulate = triangulate;

function triangulate(graphicsData, graphicsGeometry) {
let points = graphicsData.points;
const holes = graphicsData.holes;
const verts = graphicsGeometry.points;
const indices = graphicsGeometry.indices;

if (points.length >= 6) {
    const holeArray = [];
    for (let i0; i < holes.length; i++) {
      const hole = holes[i];

      holeArray.push(points.length / 2);
      points = points.concat(hole.points);
    }

    // Tesselate
    const res = Tess2.tesselate({
      contours: [points],
      windingRule: Tess2.WINDING_ODD,
      elementType: Tess2.POLYGONS,
      polySize: 3,
      vertexSize: 2,
    });

    if (!res.elements.length) {
      return;
    }

    const vrt = res.vertices;
    const elm = res.elements;

    const vertPos = verts.length / 2;

    for (var i0; i < elm.length; i++) {
      indices.push(elm[i] + vertPos);
    }

    for (let i0; i < vrt.length; i++) {
      verts.push(vrt[i]);
    }
  }
}

这样就用上了 tess2 的三角剖切,复杂的多边形也能正确渲染了。

这次填充渲染就对了。

图片

如果你想要多种多边形三角剖分都同时存在,可能需要考虑继承一下 Graphics 类,然后在 renderer 的时候设置好 buildPoly.triangulate

class DefGraphics extends PIXI.Graphics {
  render(renderer) {
    PIXI.graphicsUtils.buildPoly.triangulate = defTriangulator;
    super.render(renderer);
  }
}

class TessGraphics extends PIXI.Graphics {
  render(renderer) {
    PIXI.graphicsUtils.buildPoly.triangulate = triangulate;
    super.render(renderer);
  }
}

在线示例:

codesandbox.io/p/sandbox/n…

最后

pixijs 为了提高渲染速度,在质量上做了妥协,如果你的项目对质量有要求,就需要进行特殊的调整,就像这里的三角剖分的算法一样,希望对你有所帮助。

我是前端西瓜哥,关注我,学习更多图形渲染知识。


相关阅读,

PixiJS 源码解读:绘制矩形的渲染过程讲解

用 Pixi.js 写 WebGL

Cursor 排查 eslint 问题全过程记录

作者 windliang
2025年7月7日 13:04

Cursor 使用 Claude Sonnet 4 (Thinking) 排查 eslint 问题全流程记录。

背景

问题:一个微信小程序项目的 wxml 文件,某个自定义 rules 规则不希望被检查,但添加 eslint-disable 后依旧被检查出 error。

我:对 eslint 了解甚微,完全无思路。

import wxml from 'eslint-plugin-wxml';
import wxmlParser from "@wxml/parser";
import globals from 'globals';
import mpeMiniprogram from "@mpe/eslint-plugin-miniprogram";
const OFF = 0;
const WARN = 1;
const ERROR = 2;
export default[
  {  
    files: ['**/*.wxml'],
    plugins:{wxml, "@mpe/miniprogram": mpeMiniprogram},
    languageOptions:{
     parser:wxmlParser,
    },
    rules:{
      // 禁用标签名列表,持续迭代
      'wxml/forbid-tags': [
        ERROR,
        {
          forbid: [],
        },
      ],

看了下使用了 eslint-plugin-wxml@wxml/parser ,github 上也没看到相关问题。

寄托于 AI 了。

Demo1 eslint-test

尝试复现问题。

现在创建项目 AI 会主动先创建一些测试文件保证功能正常:

然后自己测试:

先学到一个知识点禁用整个文件 eslint 检查不能用 // eslint-disable ,应该用 /* eslint-disable */

写 demo 前和 ai 聊了下(聊天记录不知道为啥没了),当时以为是自定义规则有问题导致 eslint-disable 不生效,所以思路是看自定义规则是不是执行来推导是否符合预期。

先问了下 ai 哪个函数执行。

得出了结论各个生命周期都还会执行,只是 context.report 调用被过滤了,继续追问怎么观察出是否被过滤:

AI 列了个表格分析了一波:

还是只是说了会被丢弃,但没有讲细节。先不追问了,让他写个 wxml 看能不能复现现场。

三下五除二搞完了:

再让他搞个自定义规则:

执行过程中自己发现了一个问题,只检测了 wxs:

然后自己又写了写代码修复了:

但我此时在文件添加了 <!-- eslint-disable --> 后,生效了,错误不再检查,复现失败。

看了下不同之处,demo 用的 eslint 8 ,但项目是 9,让 AI 升级下:

升级的很顺利,但是 <!-- eslint-disable --> 依旧生效了。

现在的不同之处就是 wxml 的那两个库了,让 AI 换下库:

然后 <!-- eslint-disable --> 就不生效了,复现成功。

询问 AI 原因:

AI 思考了很多,尝试了很多,最后给出了结论,

还顺带翻 node_modules 源码发现刚自定义的规则可以用自带的,不需要自己写:

就是说是这个插件 eslint-plugin-wxml 的锅。那能绕过吗?

通过自定义 processor 还真解决了:

但我看了下它写的代码,只考虑了 <abc> 这一种情况,让他生成个通用方案

一顿操作还真给写出来了,但懒得去读代码了,直接问他实现的原理是什么:

一句话说明就是在 preprocess 阶段将不需要检查的代码注释掉了。虽然解决了,但还是怪怪的,想知道 eslint 原生是怎么处理 eslint-disable 的。

列了一个图,但依旧是表象,还是没说出 eslint 是怎么干的。

Demo2 eslint-wxml-test

Demo1 项目代码比较乱了,再写一个纯粹的项目来二次确认是 eslint-plugin-wxml 出了问题。

忘记指定版本了,让他升级下

成功复现,可以确认是 plugin 的问题了,顺便问问怎么解决:

让我们忽略掉 相应的文件,当然不是我们想要的。

还记得 html 的 plugin 生效,问他为什么:

又回到 插件的 postprocess 了,再问问他刚没明白的原生的:

说的有道理,但不涉及细节。

目前排除了自定义规则的问题,聚焦到 eslint-plugin-wxml 这个 node 包,可以通过 preprocess(检查前)注释代码或者 postprocess(检查后) 过滤错误来解决。

Demo3 eslint-html-test

一开始使用的 html 的 plugin 是正常的,让 AI 再创建一个 html 的 demo 研究下。

测试文件加一个 eslint-disable ,然后直入主题:

AI 猜测了一下说是 postprocess 处理的,移除了禁用的错误。

直觉上不太像,让他去 node_modules 里翻翻源码:

破案了,不是 eslint-plugin-wxml postprocess 阶段做的,而是 @wxml/parser 做的,解析过程中收集了 comment,有了 comment 之后 Eslint 会自己判断。

有了方案了,可以回到 demo2 了。

最终方案

告诉它方案之后,也直接解决了。但代码看上去非常啰嗦。

原库导出了两个方法 parse 和 parseForESLint 。

import { parse as cstParse } from "./cst";
import { buildAst } from "./ast/build-ast";

function parse(code: string) {
  const { cst, tokenVector, lexErrors, parseErrors } = cstParse(code);
  return buildAst(cst, tokenVector, lexErrors, parseErrors);
}

function parseForESLint(code: string) {
  const { cst, tokenVector, lexErrors, parseErrors } = cstParse(code);
  return {
    ast: buildAst(cst, tokenVector, lexErrors, parseErrors, true),
    services: {},
    scopeManager: null,
    visitorKeys: {
      Program: ["errors", "body"],
    },
  };
}

export { parse, parseForESLint };

parse 相对底层,它基于 parse 来重写了 parseForEslint 。但直觉上应该直接基于 parseForEslint 重写 parseForEslint 就行。

image-20250703142518488

整体思路上清晰了很多。

import wxmlParser from '@wxml/parser';

/**
 * 增强版 WXML Parser,基于 @wxml/parser 的 parseForESLint 方法
 * 主要增强:收集注释节点并转换为 ESLint 标准格式,支持 eslint-disable 功能
 */
export class EnhancedWXMLParser {
  
  parseForESLint(code, options = {}) {
    // 使用原始 parser 的 parseForESLint 方法
    const result = wxmlParser.parseForESLint(code, options);
    
    // 收集所有注释节点并转换为 ESLint 格式
    const comments = this.collectCommentsFromAST(result.ast, code);
    
    // 将注释添加到 AST 中
    result.ast.comments = comments;
    
    // 确保 AST 符合 ESLint 要求
    this.ensureESLintCompatibility(result.ast);
    
    return result;
  }
  
  /**
   * 从 AST 中收集所有 WXComment 节点并转换为 ESLint 格式
   */
  collectCommentsFromAST(ast, code) {
    const comments = [];
    
    // 递归遍历 AST 查找 WXComment 节点
    this.traverseAST(ast, (node) => {
      if (node.type === 'WXComment') {
        const comment = this.convertWXCommentToESLintComment(node, code);
        if (comment) {
          comments.push(comment);
        }
      }
    });
    
    // 按位置排序
    return comments.sort((a, b) => a.range[0] - b.range[0]);
  }
  
  /**
   * 将 WXComment 节点转换为 ESLint 标准注释格式
   */
  convertWXCommentToESLintComment(wxComment, code) {
    if (!wxComment.loc) return null;
    
    // 计算字符范围
    const startOffset = this.getOffsetFromLocation(code, wxComment.loc.start);
    const endOffset = this.getOffsetFromLocation(code, wxComment.loc.end);
    
    return {
      type: "Block",
      value: wxComment.value || "",  // WXComment.value 已经去掉了 <!-- 和 -->
      range: [startOffset, endOffset],
      loc: {
        start: { line: wxComment.loc.start.line, column: wxComment.loc.start.column },
        end: { line: wxComment.loc.end.line, column: wxComment.loc.end.column }
      }
    };
  }
  
  /**
   * 递归遍历 AST 节点
   */
  traverseAST(node, callback, visited = new Set()) {
    if (!node || typeof node !== 'object' || visited.has(node)) {
      return;
    }
    
    visited.add(node);
    callback(node);
    
    // 遍历可能包含子节点的属性
    const childProperties = ['body', 'children', 'elements', 'properties'];
    
    childProperties.forEach(prop => {
      if (node[prop] && Array.isArray(node[prop])) {
        node[prop].forEach(child => this.traverseAST(child, callback, visited));
      }
    });
    
    // 遍历其他可能的单个子节点
    ['startTag', 'endTag', 'value'].forEach(prop => {
      if (node[prop] && typeof node[prop] === 'object') {
        this.traverseAST(node[prop], callback, visited);
      }
    });
  }
  
  /**
   * 确保 AST 符合 ESLint 兼容性要求
   */
  ensureESLintCompatibility(ast) {
    // 确保根节点有必要的属性
    if (!ast.comments) ast.comments = [];
    if (!ast.tokens) ast.tokens = [];
    if (!ast.range) ast.range = [0, 0];
    if (!ast.loc) {
      ast.loc = {
        start: { line: 1, column: 0 },
        end: { line: 1, column: 0 }
      };
    }
    if (!ast.sourceType) ast.sourceType = "module";
    
    // 递归处理所有节点,确保它们有必要的属性
    this.processNodeForESLint(ast);
  }
  
  /**
   * 处理 AST 节点,确保符合 ESLint 要求
   */
  processNodeForESLint(node, visited = new Set()) {
    if (!node || typeof node !== 'object' || visited.has(node)) {
      return;
    }
    
    visited.add(node);
    
    // 确保每个节点都有 range 和 loc(如果它们还没有的话)
    if (!node.range && node.loc) {
      // 可以从 loc 推断 range,但这里我们保持简单
      node.range = [0, 0];
    }
    if (!node.loc && node.range) {
      node.loc = {
        start: { line: 1, column: 0 },
        end: { line: 1, column: 0 }
      };
    }
    
    // 递归处理子节点
    const childProperties = ['body', 'children', 'elements', 'properties'];
    childProperties.forEach(prop => {
      if (node[prop] && Array.isArray(node[prop])) {
        node[prop].forEach(child => this.processNodeForESLint(child, visited));
      }
    });
  }
  
  /**
   * 从位置信息计算字符偏移量
   */
  getOffsetFromLocation(code, location) {
    const lines = code.split('\n');
    let offset = 0;
    
    // 计算到目标行之前的所有字符
    for (let i = 0; i < location.line - 1; i++) {
      offset += lines[i].length + 1; // +1 for the newline character
    }
    
    // 添加目标行中的列偏移
    offset += location.column;
    
    return offset;
  }
}

// 导出单例
const parserInstance = new EnhancedWXMLParser();

// ESLint 期望的 parser 格式
export const enhancedWXMLParser = {
  parseForESLint: (code, options) => parserInstance.parseForESLint(code, options),
  parse: (code, options) => parserInstance.parseForESLint(code, options).ast
};

// 兼容原有的接口
export default enhancedWXMLParser;

但看起来还是很多细节冗余,一点点的 review 代码让它优化:

1

 // 确保 AST 符合 ESLint 要求
  this.ensureESLintCompatibility(result.ast);

2

3

4

5

6

  traverseAST(node, callback, visited = new Set()) {
    if (!node || typeof node !== 'object' || visited.has(node)) {
      return;
    }
    
    visited.add(node);
    callback(node);
    
    // 遍历可能包含子节点的属性
    const childProperties = ['body', 'children', 'elements', 'properties'];
    
    childProperties.forEach(prop => {
      if (node[prop] && Array.isArray(node[prop])) {
        node[prop].forEach(child => this.traverseAST(child, callback, visited));
      }
    });
    
    // 遍历其他可能的单个子节点
    ['startTag', 'endTag', 'value'].forEach(prop => {
      if (node[prop] && typeof node[prop] === 'object') {
        this.traverseAST(node[prop], callback, visited);
      }
    });
  }

看到遍历是写死了几个标签名,让他优化下:

变成了更通用的方法:

 traverseAST(node, callback, visited = new Set()) {
    if (!node || typeof node !== 'object' || visited.has(node)) {
      return;
    }
    
    visited.add(node);
    callback(node);
    
    // 不再硬编码属性名,而是动态检查所有属性
    Object.keys(node).forEach(key => {
      const value = node[key];
      
      // 跳过非子节点的属性
      if (this.isNonTraversableProperty(key, value)) {
        return;
      }
      
      if (Array.isArray(value)) {
        // 遍历数组中的每个元素
        value.forEach(child => {
          if (child && typeof child === 'object') {
            this.traverseAST(child, callback, visited);
          }
        });
      } else if (value && typeof value === 'object') {
        // 遍历单个对象
        this.traverseAST(value, callback, visited);
      }
    });
  }

当然也不是每次质疑都成功:

7

看到它返回 parse 也调用了 parserInstance.parseForESLint,想着是不是不应该重写 parse 就可以。

parse: (code, options) => parserInstance.parseForESLint(code, options).ast

8

尝试引导它加一些兜底

但过于谨慎了,超时都整上了,最终没有采用。

最终版本:

import wxmlParser from '@wxml/parser';

/**
 * 增强版 WXML Parser,基于 @wxml/parser 的 parseForESLint 方法
 * 主要增强:收集注释节点,支持 eslint-disable 功能
 */
export class EnhancedWXMLParser {
  
  parseForESLint(code, options = {}) {
    // 使用原始 parser 的 parseForESLint 方法
    const result = wxmlParser.parseForESLint(code, options);
    
    // 收集所有注释节点,保持原始 WXComment 类型
    const comments = this.collectCommentsFromAST(result.ast);
    
    // 将注释添加到 AST 中
    result.ast.comments = comments;
    
    return result;
  }
  
  /**
   * 从 AST 中收集所有 WXComment 节点,保持原始格式
   * 注释在遍历过程中已经是按位置排序的,无需额外排序
   */
  collectCommentsFromAST(ast) {
    const comments = [];
    // 递归遍历 AST 查找 WXComment 节点
    this.traverseAST(ast, (node) => {
      if (node.type === 'WXComment') {
        // WXComment 已经是正确格式,直接使用
        comments.push(node);
      }
    });
    
    // 注释已经按位置自然排序,直接返回
    return comments;
  }
  
  /**
   * 通用的 AST 遍历方法 - 动态发现所有子节点属性,未来兼容
   */
  traverseAST(node, callback, visited = new Set()) {
    if (!node || typeof node !== 'object' || visited.has(node)) {
      return;
    }
    
    visited.add(node);
    callback(node);
    
    // 不再硬编码属性名,而是动态检查所有属性
    Object.keys(node).forEach(key => {
      const value = node[key];
      
      // 跳过非子节点的属性
      if (this.isNonTraversableProperty(key, value)) {
        return;
      }
      
      if (Array.isArray(value)) {
        // 遍历数组中的每个元素
        value.forEach(child => {
          if (child && typeof child === 'object') {
            this.traverseAST(child, callback, visited);
          }
        });
      } else if (value && typeof value === 'object') {
        // 遍历单个对象
        this.traverseAST(value, callback, visited);
      }
    });
  }
  
  /**
   * 判断属性是否不应该被遍历
   * 这些属性虽然是对象或数组,但不包含 AST 子节点
   */
  isNonTraversableProperty(key, value) {
    // 位置信息对象,不包含子节点
    if (key === 'loc' || key === 'start' || key === 'end') {
      return true;
    }
    
    // 范围数组,只是数字不是节点
    if (key === 'range' && Array.isArray(value) && 
        value.length === 2 && typeof value[0] === 'number') {
      return true;
    }
    
    // 空数组,没必要遍历
    if (Array.isArray(value) && value.length === 0) {
      return true;
    }
    
    // 包含基础类型的数组(如 offset)
    if (Array.isArray(value) && value.every(item => typeof item !== 'object')) {
      return true;
    }
    
    // 字符串、数字、布尔值等基础类型
    if (typeof value !== 'object') {
      return true;
    }
    
    return false;
  }
}

// 导出单例
const parserInstance = new EnhancedWXMLParser();

// ESLint 期望的 parser 格式
export const enhancedWXMLParser = {
  parseForESLint: (code, options) => parserInstance.parseForESLint(code, options),
  parse: (code, options) => parserInstance.parseForESLint(code, options).ast
};

// 兼容原有的接口
export default enhancedWXMLParser; 

从怀疑自定义 eslint 规则,到 eslint-plugin-wxml 这个 node 包通过 preprocess(检查前)或者 postprocess(检查后) 解决,之后转到了一个更优的方案 @wxml/parser 解析过程中收集 comment,最终一步一步再优化代码解决了这个问题。

一些感受:

  • AI 目前可以完成事情,但需要人为的引导可以把事情做的更好。未来如何让 Cursor 一次就写好是个值得探索的方向,除了等 ai 模型更强,另一个就是从 Cursor rules 入手了。
  • 目前看来过去积累的编程直觉还是有一定作用的,不然无法引导 ai。也就是常说的解决问题的能力,原来是思路 + 执行,现在思路我们来引导,执行交给 ai 就可以了。
  • 有问题都先尝试着去问一下 AI,不断的熟悉 AI 的能力边界。
  • AI 写 demo 现在就是一句话的事,找 bug 完全可以先构造一个最小场景复现后继续排查。
  • 当下试错成本非常低,有想法直接让 AI 实现即可,大不了就是推翻重来。
  • 解决问题的流程变了,以往是先学习 -> 再尝试解决,现在变成了问题解决了 -> 再去学习了解。

深入浅出 JavaScript 闭包:从核心概念到框架实践

2025年7月7日 12:51

深入浅出 JavaScript 闭包:从核心概念到框架实践

如果你写过 JavaScript,无论是否意识到,你其实一直在使用闭包。它是许多强大编程模式背后的“秘密武器”,也是 Vue、React 等现代前端框架的基石。本文将带你走近闭包,从核心概念到框架实践,将这个看似复杂的主题,转变为你代码工具箱中的一把利器。

1. 到底什么是闭包?

闭包 = 函数 + 定义它时的词法作用域。

简单来说,当一个函数能够“记住”并访问它在被创建时所处的环境(作用域)中的变量,即使它在当前环境之外被调用,一个闭包就产生了。

一个直观的比喻:带背包的函数

你可以把闭包想象成一个特殊的函数,这个函数随身携带了一个“背包”。背包里装着它被创建时,周围环境中所有的变量。无论这个函数走到哪里去执行,它都可以随时打开这个背包,使用里面的东西。

经典示例
function outer() {
  let count = 0; // 外部函数的变量,即将被闭包“记住”

  function inner() { // 内部函数
    count++; // 访问并修改外部变量
    console.log(count);
  }

  return inner; // 返回内部函数
}

const closureFn = outer(); // outer() 执行完毕,但其变量 count 被 inner 的闭包捕获,并未销毁

closureFn(); // 输出 1
closureFn(); // 输出 2(count 的状态被完整保留)
快速判断闭包
  1. 函数嵌套:是否存在一个函数在另一个函数内部定义?
  2. 内部引用外部:内部函数是否引用了外部函数的变量?
  3. 外部调用内部:内部函数是否在定义它的函数之外被调用?

2. 闭包的威力:常见应用场景

场景一:封装与模块化 - 创建“私有变量”

在闭包出现之前,JavaScript 没有真正的私有变量。但通过闭包,我们可以模拟出私有状态,只暴露我们想提供的接口。

const createCounter = () => {
  let count = 0; // 私有变量,外界无法直接访问

  // 返回一个对象,包含了操作私有变量的方法
  return {
    increment: () => count += 1,
    getCount: () => count,
    reset: () => count = 0
  };
};

const counter = createCounter();
counter.increment();
console.log(counter.getCount()); // 输出: 1
console.log(counter.count); // 输出: undefined (无法直接访问)

✅ 应用价值:实现状态的私有化,避免全局命名冲突和状态污染,这是现代前端组件化和模块化的基石。

场景二:函数式编程的利器 - 防抖与节流

防抖(Debounce)和节流(Throttle)是优化高频触发事件(如窗口大小调整、输入框搜索)的常用手段。闭包在其中扮演了保存状态(如定时器ID)的关键角色。

const debounce = (fn, delay) => {
  let timer; // 这个timer被闭包持久化,不会在每次调用时重置

  return (...args) => {
    clearTimeout(timer); // 清除上一个定时器
    timer = setTimeout(() => fn(...args), delay); // 创建新的
  };
};

// 使用
window.addEventListener('input', debounce(() => {
  console.log('向服务器发送搜索请求...');
}, 500));
场景三:解决异步循环中的陷阱

这是一个经典的面试题,也是闭包大显身手的舞台。

// 经典问题:循环中创建异步操作
for (var i = 0; i < 5; i++) {
  setTimeout(() => {
    console.log(i) // 输出5个5
  }, 100);
}
// 原因:setTimeout是异步的。当它执行时,循环已经结束,此时的i是全局的,值为5。

// 闭包解决方案 (IIFE: 立即执行函数表达式)
for (var i = 0; i < 5; i++) {
  (function(j) { // 创建一个新的函数作用域
    setTimeout(() => console.log(j), 100); // 这里的j是每次循环传入的i的值
  })(i);
} // 输出 0,1,2,3,4

// ES6 `let` 的解决方案
// for (let i = 0; i < 5; i++) {
//   setTimeout(() => console.log(i), 100);
// }
// `let`会为每次循环创建一个新的块级作用域,其行为类似于闭包。

3. 现代框架中的闭包实践

闭包并非古老的屠龙之技,它正是现代前端框架实现其核心功能的基石。

Vue 3:组合式 API 的灵魂

Vue 3 的组合式 API (Composition API) 是闭包的重度使用者。setup 函数本身就创建了一个巨大的闭包。

<script setup>
import { ref, onMounted } from 'vue'

// setup脚本块本身就是一个闭包环境
const count = ref(0); // `count` 变量被下面的函数和钩子“记住”

function increment() {
  count.value++; // 闭包使得increment可以访问和修改count
}

onMounted(() => {
  // 生命周期钩子也通过闭包访问到最新的状态
  console.log(`组件挂载时,count 的值为 ${count.value}`);
});
</script>

✅ 闭包价值:实现了组件内部的状态隔离和逻辑复用。每个组件实例调用 setup 都会创建一个独立的闭包环境,保证了状态的独立性。自定义组合式函数(Composables)更是将闭包的能力发挥到极致。

Pinia:更优雅的状态管理

Pinia 的 defineStore 设计巧妙地利用了闭包来创建单例、响应式的全局状态。

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCounterStore = defineStore('counter', () => {
  // 这个函数只在第一次使用时执行一次,其内部环境形成一个持久的闭包
  const count = ref(0);
  const name = ref('Eduardo');

  const doubleCount = computed(() => count.value * 2);

  function increment() {
    count.value++;
  }

  // 暴露的API都通过闭包访问内部状态
  return { count, name, doubleCount, increment };
});

✅ 闭包价值:以组合式函数的形式定义 Store,天然地实现了状态的封装和隔离,只暴露想被外部使用的接口。

React:Hooks 与“陈旧闭包”陷阱

React Hooks 的工作方式也深度依赖闭包来在多次渲染间保持状态。但这也带来了著名的“陈旧闭包”(Stale Closure)问题。

function Counter() {
  const [count, setCount] = useState(0);
  
  const handleAlert = () => {
    // 这个函数在定义时,捕获了当时的 count 值
    setTimeout(() => {
      alert("你点击时的计数值是: " + count); // 这个count是旧值!
    }, 3000);
  };
  
  return (
    <div>
      <p>当前计数: {count}</p>
      <button onClick={() => setCount(count + 1)}>增加</button>
      <button onClick={handleAlert}>显示计数值</button>
    </div>
  );
}

问题分析:当 handleAlert 函数被创建时(即组件渲染时),它在闭包中捕获了当时的 count 值。即使你之后点击按钮更新了 count,那个旧的 handleAlert 函数的闭包里 count 还是旧值。

解决方案:

  1. 函数式更新:给 setState 传递一个函数,React 会确保将最新的 state 传入,从而绕过陈旧闭包。
    const handleAlertFixed = () => {
        setTimeout(() => {
          // 不直接用外面的count, 而是通过回调获取最新值
          setCount(currentCount => {
              alert("当前计数: " + currentCount);
              return currentCount; // 别忘了返回
          });
        }, 3000);
    };
    
  2. useRefuseRef 返回一个可变的 ref 对象,其 .current 属性在组件的整个生命周期内保持不变。我们可以用它来手动追踪最新状态。
    const countRef = useRef(count);
    useEffect(() => {
      countRef.current = count; // 每次count更新,都同步到ref中
    }, [count]);
    
    const handleAlertWithRef = () => {
        setTimeout(() => {
          alert("当前计数: " + countRef.current);
        }, 3000);
    };
    

4. 内存管理与性能优化

闭包是把双刃剑。它带来了强大功能,但也可能导致内存泄漏,如果不正确管理的话。

核心原理:只要闭包存在,它所引用的外部变量就不会被垃圾回收机制(GC)回收。

常见陷阱与规避策略
  1. 被遗忘的事件监听器 当一个 DOM 元素上绑定了事件处理函数(一个闭包),而这个元素后来被移除了,但事件监听没有被显式移除,那么闭包及其引用的所有变量(包括对该 DOM 元素的引用)都将留在内存中。

    ✅ 最佳实践:组件销毁时,务必清理事件监听和定时器。现代框架的生命周期钩子(如 Vue 的 onUnmounted 或 React useEffect 的返回函数)是执行这类清理操作的理想位置。

    function setup() {
      const element = document.getElementById('my-btn');
      const handler = () => console.log('clicked');
      element.addEventListener('click', handler);
    
      // 在组件销毁时
      onUnmounted(() => {
        element.removeEventListener('click', handler);
      });
    }
    
  2. 避免创建巨大的闭包 只让闭包捕获必要的信息。

    // 不好:闭包捕获了整个 `hugeData` 数组
    function bigClosure() {
      const hugeData = new Array(100000).fill('data');
      return () => console.log(hugeData.length); // 整个数组被引用
    }
    
    // 改进:只捕获需要的数据
    function optimizedClosure() {
      const hugeData = new Array(100000).fill('data');
      const length = hugeData.length; // 提前取出
      return () => console.log(length); // 闭包只引用了 `length` 这个数字
    }
    
如何检测内存泄漏?

Chrome DevTools 是你的好朋友:

  • Memory -> Heap Snapshot (堆快照):可以拍摄应用在不同时间点的内存快照。搜索 "Closure" 或你的函数名,可以查看哪些闭包占用了内存,以及它们引用了哪些变量。
  • Performance Monitor:实时监控 JS 堆内存(JS Heap Size)的变化,如果内存持续增长且不下降,可能存在泄漏。

5. 总结

闭包是 JavaScript 中一个强大且基础的概念,它赋予了我们:

  • 创建私有状态和实现数据封装的能力。
  • 持久化状态,是函数式编程和许多设计模式的基础。
  • 构建现代前端框架的核心机制,如组件化、响应式和状态管理。

深入理解闭包的工作原理和潜在陷阱,不仅能帮助我们写出更优雅、更健壮的代码,更是从“会用”到“精通”现代 JavaScript 开发的必经之路。在日常开发中,我们应当拥抱闭包带来的灵活性,同时警惕其可能导致的内存问题,做到收放自如。

JavaScript闭包深度解析:从作用域到实战应用

2025年7月7日 12:30

前言

作为前端开发者,闭包绝对是你绕不过去的一个核心概念。无论是面试还是实际开发,闭包都是考察重点。但很多人对闭包的理解还停留在"函数嵌套函数"的表面,今天我们就来深入剖析闭包的本质和应用场景。

从作用域说起

要理解闭包,首先得搞清楚作用域的概念。JavaScript中有三种作用域:

// 全局作用域
var n = 999;

function f1() {
    // 没有使用var声明,意外创建了全局变量
    b = 123;
    
    // 函数作用域
    {
        // 块级作用域
        let a = 1;
    }
    console.log(n); // 可以访问全局变量
}

f1();
console.log(b); // 123 - 意外的全局变量

作用域链的核心规则:内部可以访问外部,外部无法访问内部。

这里有个坑要注意:不使用varletconst声明的变量会意外成为全局变量。这在《JavaScript语言精粹》中被归类为"糟粕部分"(The Bad Parts)。

闭包的本质

那么问题来了:函数外部真的无法读取函数内的局部变量吗?

闭包就是打破这个规则的"桥梁"。

最简单的闭包示例

// 让局部变量可以在全局访问
function f1() {
    // 局部变量
    var n = 999; // 自由变量
    function f2(){
        console.log(n);
    }
    return f2;
}

var result = f1();
result(); // 999

这里的f2就是一个闭包函数,它可以访问外部函数f1的局部变量n

闭包的"记忆"能力

闭包最神奇的地方在于它能"记住"外部函数的变量状态:

function f1(){
    var n = 999;
    // 修改自由变量的函数
    nAdd = function(){
        n += 1;
    }
    function f2() {
        console.log(n);
    }
    return f2;
}

var result = f1();
result(); // 999
nAdd();   // 修改n的值
result(); // 1000

看到了吗?即使f1已经执行完毕,n这个变量依然存在于内存中,而且可以被修改!

深入理解:为什么闭包变量不会被销毁?

这涉及到JavaScript的垃圾回收机制。JavaScript使用引用计数的方式进行垃圾回收:

  1. f1执行完毕后,按理说n应该被销毁
  2. 但是f2函数还在引用着n(这个n被称为"自由变量")
  3. 由于存在引用,垃圾回收器不会回收n
  4. 所以n会一直保持在内存中

闭包也被形象地称为"背包",因为它背着外部函数的变量不放手。

实战应用:解决this指向问题

来看一个经典的闭包应用场景:

var name = 'The Window';
var object = {
    name: "My Object",
    getNameFunc: function () {
        var that = this; // 保存this引用
        return function() {
            return that.name; // 使用闭包访问外部this
        }
    }
}

console.log(object.getNameFunc()()); // "My Object"

这里利用闭包解决了this指向问题:

  • 外部函数的this指向object
  • 通过that变量保存这个引用
  • 内部函数通过闭包访问that,获得正确的name

闭包的两大用途

根据阮一峰老师的总结,闭包主要有两个用途:

1. 读取函数内部的变量

让外部代码可以访问函数内部的私有变量,实现数据封装。

2. 让变量的值始终保持在内存中

通过闭包,可以创建持久化的变量状态,这在很多场景下非常有用。

闭包的注意事项

内存泄漏风险

闭包会阻止垃圾回收,如果使用不当容易造成内存泄漏:

function createHandler() {
    var largeData = new Array(1000000); // 大数组
    
    return function(element) {
        // 即使不使用largeData,它也不会被回收
        element.onclick = function() {
            console.log('clicked');
        };
    };
}

解决方案:

  • 在退出函数前,将不使用的局部变量设为null
  • 使用delete操作符删除不需要的属性

变量值的不确定性

闭包会在父函数外部改变父函数内部变量的值,这种"自由"带来了不确定性:

function createFunctions() {
    var result = [];
    for (var i = 0; i < 3; i++) {
        result[i] = function() {
            return i; // 闭包引用的是同一个i
        };
    }
    return result;
}

var funcs = createFunctions();
console.log(funcs[0]()); // 3,不是0!

这就是闭包中"自由变量"的自由性——它的生命周期和值都可能超出预期。

总结

闭包是JavaScript中一个强大但需要谨慎使用的特性:

核心概念:

  • 函数嵌套函数(作用域链的嵌套)
  • 内部函数引用外部函数的变量
  • 内部函数被返回或传递到外部

主要特点:

  • 将函数内部和外部连接起来的桥梁
  • 让变量在函数执行完毕后仍然存在
  • 可以创建私有变量和方法

注意事项:

  • 谨防内存泄漏
  • 注意变量值的不确定性
  • 及时清理不需要的引用

掌握闭包,不仅能帮你写出更优雅的代码,在面试中也会让你脱颖而出。记住:闭包就是将函数内部和函数外部链接起来的桥梁

Java后端项目前端基础Vue(二)

作者 阑梦清川
2025年7月7日 12:28

1.编写App组件

1.1main.ts文件

下面的这个就是我们的App组件的编写的这个流程,其实就是对于我们的这个src里面的这个内容有一个更加深刻的这个理解罢了;

首先编写的是我们的main.ts文件,这个文件里面的内容和我们最开始的这个初始化的这个内容没有很大的这个区别;

mount相当于指定的就是我们的这个花盆的摆放的这个位置,这个里面的参数和我们的这个index.html里面的这个内容是相互对应的;

image-20250703160735890

1.2App.vue文件

对于初学者而言,可能不知道这个vue文件里面的内容的含义以及我们的这个项目里面的这个具体的不同的文章的目录代表的含义以及他们之间的这个关联关系,这个时候,尚硅谷的这个老师的做法我非常同意,就是使用的自己动手实现的方式去帮助我们理解这个里面的奥秘;

在一个简单的这哦vue文件里面,可以有下面的是3个不同部分的这个内容,分别是我们的这个template杭哥script和我们的这个style标签;

下面的这个就是简单的模版,版喊了我们谈及的这三个组成的部分,这个和我们最开始学习的这个css的相关的语法是非常的类似的,在我们的这个template这个标签里面,包含的这个内容是我们的这个页面想要展示的这个具体的内容;

在这个style这个标签里面,显示的就是我们的我们的这个内容标签的样式,页边距,颜色之类的这个具体的呈现的样式,在我们的这个script标签里面就是这个具体的展示的方法,我们需要把这个组件暴露出去,使用的是下面的这个default也就是默认的这个暴露的方式;

name表示的就是我们想要暴露的这个组件的名字;

image-20250703161026032

1.3组件的运行的效果

image-20250703161804020

2.一个简单的效果

2.1新建文件

上面的这个仅仅是一个非常简单的组件,相当于我们的这个花盆的基本的效果,接下来我们开始进行这个基本的枝叶的开发,这个时候我们就需要在这个原来的src下面去新建这个components文件夹存放我们的这个相关的修饰的部分;

image-20250703163624944

在这个新建的文件夹里面我们新建这个person文件,这个时候我们就是想要编写一个和这个人相关的这个界面,但是之前的这个界面我们也是不需要删除掉的;

之前的那个App组件里面的代码,我们可以直接cv过来,因为这个基本的vue文件里面的这个组成部分,都是style和script和我们的template三个部分,因此这个是没有很大的这个区别的;

我们把这个代码cv之后,直接进行修改即可,这个大的框架是没有发生任何的这个变化的;

可以看看下面的这个代码:

1) 我们在这个name里面进行修改,这个data就是我们想要显示的这个数据的具体的内容;

2)我们的这个div里面定义了这个showTel的这个方法,因此这个需要展示我们的具体的联系方式,我们使用alert进行弹框显示即可;

3)style里面还是我们的这个标签的相关的属性;

image-20250703164439173

2.2具体的效果

因为我们的这个person是在原来的这个基础上面添加的新文件,因此这个原来的样式还是保留的,只不过之前的那个文本消失了,这个也是很容易理解的;

点击这个查看联系方式,这个内容进行弹框进行显示

image-20250703164726087

至于我们的这个具体的信息,在这个页面也是可以显示出来的:

可以明显的看到这个APP下面存在着这个person,这个person里面定义的这个具体的内容这个也是可以显示出来的;

image-20250703164934558

2.3关于这个开发者工具

我们可以在这个浏览器里面添加下面的这个开发者工具的插件,方便我们进行这个开发者工具里面的这个相关的代码的调试的过程;

但是后来我发现这个东西其实没有很大的这个用处,其实这个好像没有我们也是可以查看这个具体的效果的;

image-20250703163716622

下面的这个就是我们浏览器里面的按钮,一个显示页面,一个就是隐藏,这个界面,我们使用这个自带的即可,好像是无需使用这个插件的,也是没有问题的;

3.配置项API和组合式API

下面的这个就是我们的配置项API:

image-20250703165452520

当我们添加新的功能的时候,我们的这个新东西需要添加到每一个模块里面去;

但是我们的组合式API解决了上面的这个弊端:

就是相同的这个东西都放到一起,集中管理,每一个集中的都作为一个函数,一个函数包含了某一个东西的所有的内容,直接集中进行修改即可;

深入理解useState:批量更新与非函数参数支持

作者 snakeshe1010
2025年7月7日 12:10

深入理解useState:批量更新与非函数参数支持

在构建React类库时,useState Hook的实现是核心挑战之一。本文将深入探讨如何优化useState实现,支持批量更新和非函数参数,从而更贴近React的实际行为。

useState基础实现回顾

在之前的实现中,我们已经建立了useState的基本结构:

function useState(initial) {
    let currentFiber = wipFiber;
    const oldHook = currentFiber.alternate?.stateHooks[stateHookIndex]

    const stateHook = {
        state: oldHook ? oldHook.state : initial,
        queue: oldHook ? oldHook.queue : []
    }

    stateHook.queue.forEach(action => {
        stateHook.state = action(stateHook.state)
    })

    stateHook.queue = []

    stateHookIndex++
    stateHooks.push(stateHook)

    currentFiber.stateHooks = stateHooks

    function setState(action) { 
        stateHook.queue.push(action)
        wipRoot = {
            ...currentFiber,
            alternate: currentFiber
        }
        nextWorkOfUnit = wipRoot
    }
    return [stateHook.state, setState]
}

这个实现已经支持了状态管理的基本功能,但存在两个重要问题:

  1. 无法处理直接传入值而非函数的setState调用
  2. 每次setState都会立即触发重新渲染,缺乏批量更新机制

优化一:支持非函数参数

React的setState既可以接受函数也可以直接接受值:

// 函数形式
setCount(prev => prev + 1)

// 直接值形式
setCount(5)

为了支持这两种形式,我们需要修改setState的实现:

function setState(action) { 
    // 将非函数参数转换为函数形式
    const newAction = typeof action === 'function' ? action : () => action
    stateHook.queue.push(newAction)
    
    // 触发重新渲染
    wipRoot = {
        ...currentFiber,
        alternate: currentFiber
    }
    nextWorkOfUnit = wipRoot
}

这个优化使得API更加灵活,开发者可以根据情况选择最适合的更新方式。

为什么需要转换?

在状态更新处理中,我们使用队列机制:

stateHook.queue.forEach(action => {
    stateHook.state = action(stateHook.state)
})

这里需要每个队列项都是函数(接收旧状态,返回新状态)。通过统一转换为函数形式,我们可以:

  1. 保持队列处理逻辑的一致性
  2. 支持多种调用方式
  3. 简化内部实现

优化二:批量更新机制

在React中,多次setState调用会被批量处理,而不是每次调用都立即触发渲染。这是性能优化的重要机制。

当前实现的问题

当前代码中,每次调用setState都会:

wipRoot = {
    ...currentFiber,
    alternate: currentFiber
}
nextWorkOfUnit = wipRoot

这会导致每次setState调用都会启动一个新的渲染流程。如果连续多次调用setState,会造成不必要的性能开销。

批量更新的实现思路

要实现批量更新,我们需要:

  1. 收集更新:在事件处理函数或生命周期中调用的多个setState应该被收集起来
  2. 延迟执行:等到所有更新收集完毕后,再统一执行一次渲染
  3. 合并更新:在渲染时一次性应用所有更新

在React中,这是通过"批量更新"(batching)机制实现的。虽然完整实现需要更复杂的调度器,但我们可以简化实现:

// 标记是否在批量更新中
let isBatching = false
// 收集需要更新的fiber
let batchedUpdates = new Set()

function setState(action) {
    const newAction = typeof action === 'function' ? action : () => action
    stateHook.queue.push(newAction)
    
    if (isBatching) {
        // 批量更新模式:收集fiber
        batchedUpdates.add(currentFiber)
    } else {
        // 直接触发更新
        scheduleUpdate(currentFiber)
    }
}

function scheduleUpdate(fiber) {
    wipRoot = {
        ...fiber,
        alternate: fiber
    }
    nextWorkOfUnit = wipRoot
}

// 模拟React的事件系统
function withBatching(fn) {
    return function(...args) {
        isBatching = true
        fn(...args)
        isBatching = false
        
        // 执行所有收集的更新
        batchedUpdates.forEach(fiber => scheduleUpdate(fiber))
        batchedUpdates.clear()
    }
}

如何使用批量更新

在事件处理中包装批量更新:

// 原始事件处理
const handleClick = () => {
    setCount(c => c + 1)
    setCount(c => c + 1)
}

// 包装后
element.addEventListener('click', withBatching(handleClick))

这样,在handleClick中连续调用两次setCount,只会触发一次重新渲染。

完整优化后的useState

结合上述两个优化,我们得到完整的useState实现:

let isBatching = false;
let batchedUpdates = new Set();

function useState(initial) {
    let currentFiber = wipFiber;
    const oldHook = currentFiber.alternate?.stateHooks[stateHookIndex]
    
    const stateHook = {
        state: oldHook ? oldHook.state : initial,
        queue: oldHook ? oldHook.queue : []
    }
    
    // 应用所有更新
    stateHook.queue.forEach(action => {
        stateHook.state = action(stateHook.state)
    })
    stateHook.queue = []
    
    stateHookIndex++
    stateHooks.push(stateHook)
    currentFiber.stateHooks = stateHooks
    
    function setState(action) {
        const newAction = typeof action === 'function' 
            ? action 
            : () => action;
        
        stateHook.queue.push(newAction)
        
        if (isBatching) {
            batchedUpdates.add(currentFiber)
        } else {
            scheduleUpdate(currentFiber)
        }
    }
    
    return [stateHook.state, setState]
}

function scheduleUpdate(fiber) {
    wipRoot = {
        ...fiber,
        alternate: fiber
    }
    nextWorkOfUnit = wipRoot
}

function withBatching(fn) {
    return function(...args) {
        isBatching = true
        fn(...args)
        isBatching = false
        
        batchedUpdates.forEach(scheduleUpdate)
        batchedUpdates.clear()
    }
}

实际应用示例

function Counter() {
    const [count, setCount] = useState(0)
    
    const increment = () => {
        setCount(count + 1)  // 直接值形式
        setCount(c => c + 1) // 函数形式
    }
    
    return (
        <div>
            <h1>{count}</h1>
            <button onClick={withBatching(increment)}>
                增加 (+2)
            </button>
        </div>
    )
}

在这个示例中:

  1. 点击按钮触发increment函数
  2. 连续两次setCount调用被批量处理
  3. 最终count增加2,但只触发一次渲染

性能对比

场景 优化前 优化后
连续3次setState 3次渲染 1次渲染
事件处理中的状态更新 立即渲染 延迟批量渲染
异步操作中的状态更新 立即渲染 立即渲染

总结

通过优化useState的实现,我们:

  1. 支持非函数参数:通过将值转换为函数,统一处理逻辑
  2. 实现批量更新:减少不必要的渲染,提高性能
  3. 更贴近React行为:提供更符合开发者预期的API

这些优化使得我们的React实现更加强大和高效。批量更新机制尤其重要,它能显著提升复杂应用的性能,避免"渲染风暴"问题。

在后续开发中,还可以考虑:

  1. 实现更精细的调度机制
  2. 增加优先级控制
  3. 支持并发模式

理解这些底层实现原理,不仅能帮助我们构建更好的框架,也能提升我们作为React开发者的技术水平。

总结一期正则表达式

作者 gnip
2025年7月6日 14:44

概述

正则表达式(Regular Expression)是编程语言中不可或缺的强大工具,无论是表单验证、字符串处理还是数据提取,正则表达式都能大显身手。本文将全面介绍正则表达式在前端开发中的应用,从基础语法到高级技巧,帮助开发者掌握这一利器。

一、正则表达式基础

1.1 什么是正则表达式

正则表达式是一种用于匹配字符串中字符组合的模式。在JavaScript中,正则表达式也是对象,可以用于RegExpexectest方法,以及Stringmatchreplacesearchsplit方法。

// 创建正则表达式的两种方式
const regex1 = /pattern/;          // 字面量形式
const regex2 = new RegExp('pattern'); // 构造函数形式

1.2 基本匹配规则

正则表达式由普通字符(如字母a到z)和特殊字符(称为"元字符")组成。以下是一些最基本的匹配规则:

  • . - 匹配除换行符之外的任何单个字符
  • \d - 匹配数字,等价于[0-9]
  • \D - 匹配非数字字符,等价于[^0-9]
  • \w - 匹配字母、数字或下划线,等价于[A-Za-z0-9_]
  • \W - 匹配非字母、数字、下划线字符
  • \s - 匹配空白字符(空格、制表符、换行符等)
  • \S - 匹配非空白字符
// 示例:匹配手机号码
const phoneRegex = /1\d{10}/;
console.log(phoneRegex.test('13800138000')); // true

二、正则表达式进阶语法

2.1 量词与重复

量词用于指定某个模式出现的次数:

  • * - 匹配前一个表达式0次或多次
  • + - 匹配前一个表达式1次或多次
  • ? - 匹配前一个表达式0次或1次
  • {n} - 匹配前一个表达式恰好n次
  • {n,} - 匹配前一个表达式至少n次
  • {n,m} - 匹配前一个表达式至少n次,最多m次
// 匹配QQ号(5-11位数字)
const qqRegex = /^[1-9]\d{4,10}$/;
console.log(qqRegex.test('12345')); // true
console.log(qqRegex.test('012345')); // false(不能以0开头)

2.2 字符集合与范围

使用方括号[]可以定义一个字符集合,匹配其中任意一个字符:

  • [abc] - 匹配a、b或c中的任意一个
  • [a-z] - 匹配a到z之间的任意小写字母
  • [^abc] - 匹配除了a、b、c之外的任意字符
// 匹配16进制颜色值
const colorRegex = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
console.log(colorRegex.test('#fff')); // true
console.log(colorRegex.test('#ffffff')); // true
console.log(colorRegex.test('#ggg')); // false

2.3 分组与捕获

使用圆括号()可以创建捕获组,匹配的内容会被记住,可以在后面引用:

  • (pattern) - 匹配并记住匹配项
  • (?:pattern) - 匹配但不记住匹配项(非捕获组)
  • \n - 引用第n个捕获组(n为正整数)
// 匹配日期并提取年、月、日
const dateRegex = /(\d{4})-(\d{2})-(\d{2})/;
const match = dateRegex.exec('2023-05-20');
console.log(match[1]); // "2023"(年)
console.log(match[2]); // "05"(月)
console.log(match[3]); // "20"(日)

三、正则表达式在前端中的应用

3.1 表单验证

表单验证是正则表达式在前端中最常见的应用场景之一。

// 邮箱验证
function validateEmail(email) {
  const regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
  return regex.test(email);
}

// 密码强度验证(至少8位,包含大小写字母和数字)
function validatePassword(password) {
  const regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$/;
  return regex.test(password);
}

// 身份证号验证(简单版)
function validateIDCard(id) {
  const regex = /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/;
  return regex.test(id);
}

3.2 字符串处理

正则表达式可以高效地进行复杂的字符串操作:

// 千分位分隔数字
function formatNumber(num) {
  return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
console.log(formatNumber(1234567.89)); // "1,234,567.89"

// 驼峰命名转连字符命名
function camelToHyphen(str) {
  return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
}
console.log(camelToHyphen('backgroundColor')); // "background-color"

// 去除HTML标签
function stripTags(html) {
  return html.replace(/<[^>]+>/g, '');
}

3.3 URL解析

使用正则表达式可以方便地从URL中提取各种信息:

function parseURL(url) {
  const regex = /^(https?:\/\/)?([^\/\?:]+)(:(\d+))?([\/\?:][^#]*)?(#.*)?$/;
  const match = regex.exec(url);
  
  return {
    protocol: match[1] || 'http://',
    host: match[2],
    port: match[4] || (match[1] && match[1].includes('https') ? '443' : '80'),
    path: match[5] || '/',
    hash: match[6] || ''
  };
}

console.log(parseURL('https://www.example.com:8080/path/to/page?query=string#hash'));

四、JavaScript中的正则表达式API

4.1 RegExp对象方法

  • test() - 测试字符串是否匹配正则表达式,返回布尔值
  • exec() - 执行正则表达式匹配,返回匹配结果数组或null
const regex = /hello (\w+)/;
console.log(regex.test('hello world')); // true

const result = regex.exec('hello world');
console.log(result[0]); // "hello world"(完整匹配)
console.log(result[1]); // "world"(第一个捕获组)

4.2 String对象方法

  • match() - 检索字符串中与正则表达式匹配的结果
  • search() - 测试字符串是否匹配正则表达式,返回匹配位置的索引
  • replace() - 替换字符串中与正则表达式匹配的部分
  • split() - 使用正则表达式分割字符串
// match示例
const str = 'The quick brown fox jumps over the lazy dog';
console.log(str.match(/[A-Z]/g)); // ["T"]

// replace示例
console.log('2023-05-20'.replace(/(\d{4})-(\d{2})-(\d{2})/, '$2/$3/$1')); // "05/20/2023"

// split示例
console.log('a,b, c , d'.split(/\s*,\s*/)); // ["a", "b", "c", "d"]

五、高级正则表达式技巧

5.1 零宽断言

零宽断言(lookaround assertions)用于指定匹配位置需要满足的条件,但不消耗字符:

  • x(?=y) - 正向肯定查找,匹配x仅当x后面跟着y
  • x(?!y) - 正向否定查找,匹配x仅当x后面不跟着y
  • (?<=y)x - 反向肯定查找,匹配x仅当x前面是y
  • (?<!y)x - 反向否定查找,匹配x仅当x前面不是y
// 提取价格数字
const priceStr = 'Price: $123.45, $67.89';
const prices = priceStr.match(/(?<=\$)\d+\.\d\d/g);
console.log(prices); // ["123.45", "67.89"]

// 匹配不以https开头的URL
const urlRegex = /^(?!https?:\/\/).+/;
console.log(urlRegex.test('www.example.com')); // true
console.log(urlRegex.test('http://example.com')); // false

5.2 非贪婪匹配

默认情况下,量词是"贪婪的",会尽可能多地匹配字符。在量词后添加?可以使其变为"非贪婪的":

const html = '<div>content1</div><div>content2</div>';

// 贪婪匹配
console.log(html.match(/<div>.*<\/div>/)[0]); 
// "<div>content1</div><div>content2</div>"

// 非贪婪匹配
console.log(html.match(/<div>.*?<\/div>/)[0]); 
// "<div>content1</div>"

5.3 递归匹配

使用(?R)(?1)等可以实现递归匹配,适合处理嵌套结构:

// 匹配嵌套的圆括号(最多支持3层)
const nestedParens = /\(([^()]|\((?:[^()]|\([^()]*\))*\))*\)/;
console.log(nestedParens.test('(a(b(c)d)e)')); // true

六、性能优化与最佳实践

6.1 正则表达式性能优化

  1. 预编译正则表达式:对于频繁使用的正则表达式,应该预先编译并保存:
// 不好的做法:每次调用都创建新的正则表达式
function testSomething(input) {
  return /pattern/.test(input);
}

// 好的做法:预编译正则表达式
const pattern = /pattern/;
function testSomething(input) {
  return pattern.test(input);
}
  1. 避免回溯灾难:复杂的正则表达式可能导致性能问题:
// 有问题的正则(可能导致回溯灾难)
const badRegex = /(x+x+)+y/;

// 改进版本
const goodRegex = /x+y/;
  1. 使用具体字符集:尽可能使用具体的字符集代替通配符:
// 不好的做法
const slowRegex = /.*abc.*/;

// 好的做法
const fastRegex = /[^abc]*abc[^abc]*/;

6.2 可读性与维护性

  1. 添加注释:对于复杂的正则表达式,可以使用x标志添加注释:
const complexRegex = new RegExp(
  `^                  # 字符串开始
  (\\d{3})           # 3位区号
  [\\s-]?            # 可选的分隔符(空格或短横线)
  (\\d{3})           # 3位前缀
  [\\s-]?            # 可选的分隔符
  (\\d{4})           # 4位线路号
  $                  # 字符串结束`, 
  'x'
);
  1. 模块化复杂正则:将复杂的正则表达式拆分为多个部分:
// 匹配URL的正则表达式
const protocol = '(https?:\\/\\/)?';
const domain = '([^\\/\\?:]+)';
const port = '(:\\d+)?';
const path = '([\\/\\?:][^#]*)?';
const hash = '(#.*)?';

const urlRegex = new RegExp(`^${protocol}${domain}${port}${path}${hash}$`);

七、常见问题与解决方案

7.1 常见正则表达式问题

  1. 多行匹配:默认情况下,.不匹配换行符,可以使用[^][\s\S]匹配任意字符:
const multiLineText = 'line1\nline2\nline3';
console.log(multiLineText.match(/line1.*line3/)); // null
console.log(multiLineText.match(/line1[^]*line3/)); // 匹配成功
  1. Unicode字符匹配:使用u标志正确处理Unicode字符:
console.log(/^.$/.test('𠮷')); // false
console.log(/^.$/u.test('𠮷')); // true
  1. 全局匹配的状态问题:带有g标志的正则表达式会记住上次匹配的位置:
const regex = /a/g;
const str = 'abcabc';

console.log(regex.test(str)); // true
console.log(regex.test(str)); // true
console.log(regex.test(str)); // false(需要重置lastIndex)
regex.lastIndex = 0;
console.log(regex.test(str)); // true

7.2 实用正则表达式示例

  1. 提取Markdown链接
function extractMarkdownLinks(text) {
  const regex = /\[([^\]]+)\]\(([^)]+)\)/g;
  const links = [];
  let match;
  
  while ((match = regex.exec(text)) !== null) {
    links.push({
      text: match[1],
      url: match[2]
    });
  }
  
  return links;
}
  1. 验证信用卡号(Luhn算法):
function validateCreditCard(cardNumber) {
  // 去除所有非数字字符
  const cleaned = cardNumber.replace(/\D/g, '');
  
  // 检查基本格式
  if (!/^[0-9]{13,16}$/.test(cleaned)) {
    return false;
  }
  
  // Luhn算法验证
  let sum = 0;
  let shouldDouble = false;
  
  for (let i = cleaned.length - 1; i >= 0; i--) {
    let digit = parseInt(cleaned.charAt(i), 10);
    
    if (shouldDouble) {
      digit *= 2;
      if (digit > 9) {
        digit -= 9;
      }
    }
    
    sum += digit;
    shouldDouble = !shouldDouble;
  }
  
  return (sum % 10) === 0;
}

八、工具与资源推荐

8.1 在线测试工具

  1. Regex101 (regex101.com/) - 功能强大的正则表达式测试工具,支持多种语言
  2. RegExr (regexr.com/) - 学习、构建和测试正则表达式的工具
  3. Debuggex (www.debuggex.com/) - 正则表达式可视化工具

8.2 学习资源

  1. 《精通正则表达式》 - 深入讲解正则表达式的经典书籍
  2. MDN正则表达式指南 (developer.mozilla.org/zh-CN/docs/…) - Mozilla的正则表达式文档
  3. RegexOne (regexone.com/) - 交互式正则表达式学习教程

8.3 常用正则表达式库

  1. validator.js (github.com/validatorjs…) - 常用的字符串验证库
  2. xregexp (xregexp.com/) - 扩展的JavaScript正则表达式库
  3. regexgen (github.com/devongovett…) - 根据输入字符串生成正则表达式

结语

正则表达式是前端开发中一项强大而灵活的技能,虽然学习曲线较陡峭,但一旦掌握,可以极大地提高开发效率和代码质量。本文从基础到高级全面介绍了正则表达式在前端中的应用。

正则表达式的世界博大精深,本文只是抛砖引玉。更多高级实用技巧有待发掘。

Nx带来极致的前端开发体验——借助CDD&TDD开发提效

作者 西陵
2025年7月6日 13:32

首发于公众号 code进化论,欢迎关注。

依托CDD的开发提效

什么是CDD?

组件驱动开发(Component-Driven Development, CDD)是一种以组件为中心构建用户界面的开发方法。它从最小的 UI 单元入手,将页面拆解为多个可独立开发、复用、测试的小组件,并逐步组合成复杂的应用。这一开发模式提升了模块化、复用性,并且能让团队更高效地协作开发。

现状

随着前端项目规模的增长,组件数量也会随之增加。成熟的项目可能包含数百个组件,进而可能产生数千种离散的变体,而这些变体又纠缠于各自的业务场景,这就会出现下面两个问题:

  • 效果验证等待时间长

    一般本地开发项目都需要先本地启动项目,项目编译构建完之后最终才能在浏览器中进行预览,对于大型单体项目来说这个过程会推项目的体积变大而变长,除此之外频繁修改组件后需要重新编译整个应用,最后刷新页面才能看到效果,这会导致开发者等待时间过长。

  • 效果验证流程长

    在组件的开发过程中,一些组件可能需要在特定的路由页面或者特定交互下才会渲染,这些交互可能还存在数据的请求,这意味着开发者在组件开发阶段无法轻松验证其效果,开发者往往需要切换到实际页面或者手动进行交互才能验证组件效果。

因此如何在实际项目中真正发挥 CDD 的效果是至关重要的。

Storybook

Storybook 是一个开源工具,用于构建和展示UI 组件的独立开发环境。它让开发者可以在不依赖于项目启动的情况下开发、测试和调试组件,并将组件以不同状态和场景的形式展示。Storybook 支持多种前端框架(如 React、Vue、Angular 等),能够有效的提高组件驱动开发(CDD)的效率和体验:

  • Storybook 提供了一个独立的环境,使得开发者可以专注于组件的开发,而无需考虑项目的其他依赖。这种独立性避免了项目编译和启动的时间浪费,提升开发效率。
  • Storybook 让开发者可以直观地查看所有已开发的组件,并以故事(Story)的形式展示它们的不同状态。这种可视化库有助于团队了解现有组件的功能和样式,避免重复开发。
  • 开发者可以在 Storybook 中预设组件的各种场景,并在这些场景下测试组件的表现。这减少了开发者在页面级调试时的等待时间,并确保组件能在各种状态下正确工作。

Nx如何集成Storybook

为了帮助开发者在项目中快速集成 Storybook,Nx 提供了相应的代码生成器,@nx/react、@nx/vue、@nx/angular 等插件都提供了对应框架的 Storybook 生成器,帮助开发者生成最佳实践的 Storybbok 配置,下面以 react 项目为例。

安装插件

pnpm add @nx/react -D

生成 Storybook 配置

首先需要打开 Nx Console 并选中 Generate(UI)中的 @nx/react -Storybook Configuration。

只需要在 project 选项中选择对应的库名称,然后点击生成代码的按钮,创建完成之后会增加如下配置文件:

libs/shop/
├──.storybook/
│   ├── main.ts
│ ├── preview.ts
├── src/
│   └── lib/
│       └── shop.stories.tsx
└── tsconfig.storybook.json
│       
└── vite.config.ts

这里需要关注的是 main.ts 即 storybook 编译运行配置:

import type { StorybookConfig } from '@storybook/react-vite';

const config: StorybookConfig = {
  stories: [
    '../src/lib/**/*.@(mdx|stories.@(js|jsx|ts|tsx))'
  ],
  addons: [],
  framework: {
    name: '@storybook/react-vite',
    options: {
      builder: {
        viteConfigPath: 'vite.config.ts',
      },
      
    },
  },
};

export default config;

  • stories 表示 storybook 会在配置的路径中寻找 story 文件。
  • framework 表示 storybook 运行的框架,对于 react 项目默认使用的 @storybook/react-vite,并读取当前库下面的 vite.config.ts 配置。

最终运行 stroybook 的启动命令就能在浏览器中进行预览:

接下来对于 shop 库的开发我们只需要启动 storybook 就能快速完成开发。

依托TDD的开发提效

什么是TDD?

TDD 是测试驱动开发 (Test-Driven Development)的英文简称,旨在开发者在编写代码之前,首先编写测试代码,测试代码确定开发者对功能的预期,之后再编写实际功能代码,直到所有的功能代码都通过测试用例。

当前前端开发都以数据驱动为主,开发者通过声明式的方式编写 UI 代码,即开发者描述“渲染成什么样”,而不需要详细描述“如何渲染”,最终通过数据来控制和驱动用户界面的生成、更新和变化。而数据层作为应用的核心逻辑,承担了数据的获取、处理、存储和验证等多项职责,因此在数据层进行 TDD 开发是非常必要。

作者认为以 TDD 的方式开发数据层有几大核心作用:

  • 前端开发者脱离面向 UE/UI 开发,转而面向功能逻辑开发。
  • 避免头重脚轻的情况,即数据层逻辑少,UI层逻辑多。
  • 确保代码的可测试性,提高代码质量和可维护性,长期减少回归 bug。

TDD工具现状

有效的测试框架对于构建可靠的 javascript 应用程序至关重要,可以帮助开发者最大限度的减少错误并尽早发现错误,而选择正确的测试框架可节省数小时的配置并改善开发者的体验。下面主要介绍当前使用率较高的三个测试框。

当前前端流行的测试框架有 jest、vitest、cypress 等,在 vitest 官方文档中对这几个框架都做了详细的介绍和对比

Jest Vs Vitest

下面会从开发者体验、社区和生态系统方面比较一下 Jest 和 Vitest 两个测试框架,由于 jest 和 vitest 的重点区分点不在性能上而且官方也没有给出各自的对比数据,所以就不过多介绍了。

开发体验

Jest 和 Vitest 都拥有全面、组织良好且易于搜索的文档,大大降低了开发者的上手门槛,同时两者提供功能和 API 非常相近,为了便于开发者从 Jest 迁移到 Vitest,Vitest 提供了对大多数 Jest API 和生态系统库的兼容性,在大多数项目中,它应该可以直接替换 Jest 使用。

作者认为在开发体验上两者最大的差别还是在于对 ESM 的支持,Vitest 的 ES 模块支持使其比 Jest 具有显著优势,而 Jest 仅提供对 ES 模块的实验性支持,因此在 Jest 中使用 ESM 会存在很大的风险。

因此开发者在使用 Jest 时需要先使用Babel将 JavaScript ESM 模块转换为 CommonJS,这可以借助 @babel/plugin-transform-modules-commonjs插件,但是在默认情况下,Babel 会将第三方依赖排除在外,如果开发者使用了仅支持 ESM 的依赖库,例如 react-markdown ,在这种情况下,Jest 会给开发者进行错误提示:

SyntaxError: Unexpected token 'export'

要解决此问题,需要在 jest.config.js 文件中的配置 transformIgnorePatterns 来指定那些依赖需要由 Babel 进行转译。除此之外 Jest 对于 TypeScript 的支持也需要进行额外的配置。

总体而言,Jest 需要一些配置才能与 ESM 模块和 TypeScript 配合使用,而 Vitest 是直接支持 ESM 模块和 TypeScript ,配置越少,开发人员就越开心。

社区生态

从统计数据来看,虽然 npm 下载量显示 Jest 要高于 Vitest,但是 Vitest 的增长趋势在迅速增长。根据JS 2023 年调查,Vitest 在 2021 年至 2023 年期间人气和正面评价迅速上升。相比之下,Jest 在 2016 年至 2020 年期间人气和正面评价迅速上升,但这种势头在 2021 年至 2023 年期间有所放缓,人们的看法也变得更加褒贬不一。这种转变可能是由于开发人员采用了 Vitest,它解决了 Jest 的主要痛点之一:ESM 模块支持。

Nx如何集成Vitest/Jest/Cypress

Nx 为了帮助开发者更好地在项目中搭建单测环境,为 Vitest、Jest、Cypress 都提供了相应的代码生成器,下面会以 Vitest 为例为项目添加单测配置。

使用代码生成器搭建Vitest环境

首先需要打开 Nx Console 并选中 Generate(UI)中的 @nx/vite - vitest。

在配置页开发者可以选择是否自动生成 vite 配置、运行环境(node/jsdom/happy-dom)、ui框架等:

最终 Nx 会在当前 package 下生成一个 vite.config.ts 文件,之后开发者就能正常的在项目中创建单测文件:

运行测试

运行 vitest 的测试 case 有两种方式,一种是通过 Nx 的命令去启动测试任务,开发者可以直接通过 Nx Console 运行指定 package 的 test 命令:

另一种则是直接使用 vitest 提供的 vscode 插件来执行测试 case,在插件中可以可视化的查看每个 package 下的测试用例:

总结

本章主要介绍了如何借助 CDD 和 TDD 来提高代码开发效率,CDD 能够帮助开发者在不依赖业务逻辑的情况下快速开发验证组件,而 TDD 能够让开发者的将关注点从 ui 层转移到逻辑层,同时保证逻辑层代码的可测性。Nx 为了支持项目 CDD 和 TDD 开发,提供了相应的代码生成器。

探索 AI + MCP 渲染前端 UI

作者 YongGit
2025年7月6日 12:55

🙋 前言

正如标题所说 AI + MCP 渲染前端 UI,通过 AI 结合 MCP 以一种方式渲染前端的 UI。会有同学问:“这样做有什么好处?” 大家不妨想一下,目前 AI 已经逐渐普及,对于非大模型研究员而言的程序员,大致方向都是利用 AI 创造,而不是研究大模型对吧。

身为前端程序员的我,方向也与 👆 一致,在思考我可以用 AI 创造些什么。 前端接触最多的就是web 网页 , 相信大家不难发现,越来越多的网站都接入了业务知识库的智能助手,这正是前端网页在拥抱 AI 带来的变化,AI 同时也在赋能业务的体现。

💡 灵感

知识库智能助手的能力 -> 偏业务性的回答你的问题,更多的是它告诉你什么,而后需要你自己根据它回复的内容而后我们再去执行。例如在订票网站中,我询问 “3.15 9点从深圳到达长沙的高铁票有哪些?” 这时智能助手会输出 车次 1、 车次 2、车次 3。这时候你还需要复制这些车次去网站 UI 中进行搜索车次,而后再进行购票流程。

我在想,使用 AI 的目的是简化人为的路径操作,上述确实减少了用户条件查询翻找的操作路径。但我觉得还不够,我发现 UI 的操作路径还能更加简化,例如当我询问 “3.15 9点从深圳到达长沙的高铁票有哪些?”,智能助手直接给我输出这三个车次,并且还附带上了购票按钮,那我就直接可以在聊天框完成购票了 nice!

再举一个直观的例子,比如快递地址的输入,当我在聊天助手中说添加一个收货地址的时候,这时聊天框会输出一个地址表单 UI 供你填入信息。节省了点击 “我的 -> 收货地址 -> 添加收货地址” 的路径。

🤔 思考

❓ 提问

那有同学问了:缩短 UI 操作路径和 AI 结合 MCP 有什么关系呢?

AI llm 大模型通过识别自然语言获取 MCP tool description 来进行工具调用。那如果我在现有的智能助手的 llm 中接入 MCP 机制,我在每个 MCP server 中定义当符合我的 tool description 的时候我就返回某个 UI 组件到前端,然后前端进行组件渲染就可以。

好处

这样做的好处就是 UI 渲染的能力完全由我们自己决定、并且在 mcp server 后端中也可以调用业务接口,进行逻辑处理。

方向

这个时候我的步骤方向就很清晰了

  1. 前端发送信息给聊天助手。
  2. 聊天助手根据信息调用指定 mcp server tool。
  3. mcp server tool 返回指定结构。
  4. 前端接收到对应结构,前端渲染 UI 处理。

目标

期望能够以最小成本在前端现有项目中接入上述的方案。假设你的公司正在使用智能助手,也可以很轻松的在项目中接入这套方案,实现 UI 的渲染。不仅如此,UI 也伴随着 JS 的运行,同时 JS 也能根据业务需要去运行,因此这里畅想的空间很大。

🚗 浅谈实现思路

1、前端发送消息给聊天助手

这一步骤是比较常规的,比如后端提供的接口为 /message 接口,只需要前端调用接口,即可获取流式数据进行渲染。

2、聊天助手后端调用 MCP

这里稍微偏后端一些,实现原理是大模型通过 function calling 去获取指定参数,例如获取到 mcp server name、mcp server arg ,然后你再手动调用 mcp host 通过指定 mcp client 调用其对应 mcp server。 不熟悉概念的小伙伴可以看看官网介绍 modelcontextprotocol.io/docs/concep…

例如,关键点是 tools 参数

static async createChatCompletion(messages: any[], tools?: any[]) {
    return await openai.chat.completions.create({
      messages: messages || [
        { role: "system", content: "You are a helpful assistant." },
      ],
      model: "deepseek-chat",
      tools: tools ?? [],
    });
  }

这里的 mcpHost 是我实现的 mcp 客户端,后续会提到哈。当然了后端内容是可以完全定制的,这里只是举个例子。

  static async getAvailableTools() {
    const tools = await mcpHost.getTools();
    const toolsList = tools;
    const availableTools = toolsList?.reduce((pre, cur) => {
      if (cur?.tools?.length) {
        // @ts-ignore
        cur.tools.forEach((item) => {
          console.log(`${cur.server_name}_${item.name}`);
          // 确保 inputSchema 有效
          const inputSchema = item.inputSchema || {};
          const schemaType = inputSchema.type || "object";

          pre.push({
            type: "function",
            function: {
              name: `${cur.server_name}_${item.name}`,
              description: item?.description || "",
              parameters: {
                type: schemaType,
                required: inputSchema.required || [],
                properties: inputSchema.properties || {},
              },
            },
          });
        });
      }
      return pre;
    }, [] as any[]);
    return availableTools;
  }
  
  
      const toolCall = content.message.tool_calls[0];
      const toolName = toolCall.function.name;
      const toolArgs = toolCall.function.arguments;
      const serverName = toolName.split("_")[0];
      // 第一个 _ 后面的内容,可能存在多个 _
      const functionName = toolName.split("_").slice(1).join("_");

      console.log("serverName", serverName);
      console.log("functionName", functionName);
      console.log("toolArgs", toolArgs);

      const toolResult = await MCPService.callTool(serverName, functionName, JSON.parse(toolArgs));

3、MCP server tool 返回指定结构

举个 mcp server 中的例子。可以看到当 tool 为 RecommendBook 会执行推荐逻辑。content 为展示到输出内容,_meta 是携带到前端的字段。可以看到内部包含 props, 这个 props 就是需要注入到组件的数据,为什么有这个属性?我们后面会提到。

 case 'RecommendBook': {
        const { title, author } = args;
        let recommendBookList = []
        if (!title && !author) {
          recommendBookList = books.sort(() => Math.random() - 0.5).slice(0, 3)
        } else if (title && !author) {
          // 模糊的查找
          recommendBookList = books.filter((book) => book.title.incsludes(title))
        } else if (!title && author) {
          recommendBookList = books.filter((book) => book.author.includes(author))
        } else {
          recommendBookList = books.filter((book) => book.title.includes(title) || book.author.includes(author))
        }
        return {
          content: [
            { type: "text", text: "show book list" },
          ],
          _meta: {
            aiOutput: {
              type: "text",
              content: `Recommend book list is starting to render...`,
            },
            props: {
              recommendedBooks: recommendBookList,
            },
          },
        };
      }

4. 前端接收到对应结构

前端在接收到 _meta 后如何渲染 UI 呢

可以通过动态组件的能力进行渲染。以 react 为例使用 lazy 进行渲染

 lazy(() => import("path")),

总不能让用户去定义 mapping 然后做匹配吧,可以,但没必要。可以通过构建工具添加来实现这层 mapping

 /**
 * @mcp-comp RecommendBook
 * @mcp-prop-path recommendedBooks
 */
export interface IncludeBook {
  id: string;
  /**
   * @mcp-input-optional book title
   */
  title: string;
  /**
   * @mcp-input-optional book author
   */
  author: string;
  cover: string;
  price: number;
}
/**
 * @mcp-comp RecommendBook
 * @mcp-description recommend book for user
 * @mcp-server-name mcp-component-render
 */
interface RecommendListProps {
  recommendedBooks: IncludeBook[];
}

const RecommendList: React.FC<RecommendListProps> = ({ recommendedBooks }) => {

构建工具通过注释生成 mapping 关系图。

✅ 至此思路完成!

可以答疑一下上面提到的 props ,没错,就是为了注入到前端的组件中,为什么是后端生成的呢?

比如展示我的个人档案,有两种方法,前端 UI 通过 id 查找,你也需要注入 id props,一种是后端查到档案 UI 组件的 props 然后注入。

🎁 不仅于此

作者根据上面提到的实现了一整套方案。后续我将继续和大家探讨揭露更多技术细节 ♥️

目前文档还在完善中 ✍️

文档地址:mcpsynergy.github.io/docs/

如果你觉得对你有帮助,不妨点个 star 🌟 吧

github.com/McpSynergy/…

github.com/McpSynergy/…

前端

目前作者只实现了 react 框架下的前端 sdk。 vue 的后续会支持。

1、react 动态可校验组件 sdk @mcp-synergy/react

2、vite 插件生成组件 mapping 关系 @mcp-synergy/vite-plugin-comp

后端

目前只实现了 node 下的 mcp 客户端方便你像 cursor 一样管理 mcp。

1、nodejs mcp 客户端sdk 包 @mcp-synergy/host

其余的后端逻辑以各业务为主。

Demo

启动 client 、server 项目中的 demo 后可以看到我实现的 demo。 github.com/McpSynergy/…

前端 Source Map 原理与结构详解

作者 CAD老兵
2025年7月6日 11:27

在现代前端开发中,源码通常需要经过打包、压缩、转译等多个构建步骤,最终输出的 JavaScript 代码往往与原始代码相去甚远。这虽然对性能有利,却带来了一个问题:调试困难

为了解决这个问题,浏览器引入了 Source Map 技术,可以将压缩或转译后的代码映射回原始源代码。本文将深入介绍 source map 的原理、文件结构、编码方式,并通过一个实际例子进行详细分析,甚至手动解码其中关键字段。


一、什么是 Source Map?

Source Map 是一种 映射文件格式,用于建立“编译后代码”与“原始源代码”之间的对应关系。借助它,浏览器可以将压缩、合并、转译后的 JS 还原回我们熟悉的 TypeScript、ES6+、Vue 或 JSX 等原始源码。

主要用途

  • 让调试工具显示源码而非压缩后的代码
  • 支持断点、变量查看、调用栈追踪等调试功能
  • 保留开发体验,同时不牺牲生产代码性能

二、Source Map 文件结构概览

一个典型的 source map 文件是一个 JSON 格式的 .map 文件,结构大致如下:

{
  "version": 3,
  "file": "example.min.js",
  "sources": ["../example.js"],
  "sourcesContent": ["function add(a, b) {\n  return a + b;\n}\n\nconsole.log(add(2, 3));"],
  "names": ["add", "a", "b", "console", "log"],
  "mappings": "AAAA,SAASA,IAAI,CAACC,CAAC,EAAEC,CAAC,CACnB,OAAOD,CAAC,GAAGC,CAAC,CAAC,CAAC,CAClB,EAAEC,OAAOC,IAAI,CAACH,GAAG,CAAC,CAAC"
}

字段说明

字段名 含义
version Source map 格式版本(目前固定为 3)
file 对应的输出文件(通常是压缩后的 JS 文件)
sources 映射所依赖的源文件路径数组(可多个)
sourcesContent 源文件的完整内容(用于 DevTools 显示)
names 映射中使用到的变量、函数、方法名
mappings 最核心字段:记录每段代码的位置映射,采用 VLQ 编码

三、Source Map 的工作原理

调试工具(如 Chrome DevTools)的工作流程大致如下:

  1. 浏览器加载 JS 文件
  2. 检测末尾是否存在 //# sourceMappingURL=xxx.map 注释
  3. 解析 .map 文件,获取 mappings 信息
  4. 将编译/压缩后的代码定位映射到 sourcessourcesContent 提供的原始代码
  5. 实现调试器中源码展示、断点调试、堆栈还原等功能

四、实际示例:JS 文件生成 Source Map

示例源码:example.js

function add(a, b) {
  return a + b;
}

console.log(add(2, 3));

使用 esbuild 对其压缩并生成 source map:

esbuild example.js --minify --sourcemap --outfile=dist/example.min.js

生成两个文件:

  • example.min.js
  • example.min.js.map

压缩后代码:

function add(n,d){return n+d}console.log(add(2,3));
//# sourceMappingURL=example.min.js.map

五、mappings 字段详解

mappings 是 source map 中最复杂的字段,它采用VLQ(Variable-Length Quantity)编码来压缩大量的位置信息,确保文件体积小、解析速度快。

mappings 的结构

  • 使用 ; 分隔 目标文件中的行
  • 每行中使用 , 分隔不同片段(segment)
  • 每个 segment 使用 VLQ 编码,表示源文件中对应的行列信息

segment 的含义(最多五个字段):

字段序号 含义
1 生成代码中的列号(相对于前一个 segment)
2 源文件索引(对应 sources 数组下标)
3 源文件行号(相对上一个 segment)
4 源文件列号(相对上一个 segment)
5(可选) 变量名索引(在 names 中的位置)

六、进阶解析:手动解码 mappings 字段

示例 segment:AAAA

A = base64 0 → VLQ 解码值 0
A = 0
A = 0
A = 0

解码结果:[0, 0, 0, 0]

表示:

  • 压缩文件第 0 行第 0 列
  • 源文件 ../example.js(索引 0)
  • 源文件第 0 行第 0 列
  • 没有变量名索引

示例 segment:CAAC

C = 2 → +1
A = 0
A = 0
C = 2 → +1

相对于前一个 segment [0, 0, 0, 0],此段表示:

  • 目标代码列 +1 → 第 1 列
  • 源文件索引不变
  • 源文件行不变
  • 源文件列 +1

七、Source Map 的类型

类型 描述
External .js 文件末尾有注释,指向 .map 文件(最常见)
Inline 将 source map 用 base64 内嵌进 JS 文件
Hidden 生成 map 文件但不加入注释,适合线上调试
Eval 开发时使用 eval() 动态生成源码映射,用于热更新等

八、辅助工具推荐


九、常见问题

Source Map 会暴露源码吗?

是的。建议:

  • 不在生产环境部署 .map 文件
  • 或配置访问权限
  • 或使用 hidden 类型

浏览器没加载 Source Map 的原因?

  • 没有 sourceMappingURL 注释
  • .map 文件路径错误或未部署
  • DevTools 设置未开启 Source Map

十、总结

内容 描述
什么是 Source Map 编译后代码和源码的映射表
mappings 字段 使用 VLQ 编码压缩位置关系
手动解码 可用于插件开发和调试排错
生产部署建议 谨慎暴露 .map 文件
实用工具 Chrome DevTools、可视化工具等

6个你必须掌握的「React Hooks」实用技巧✨

作者 Sun_light
2025年7月7日 11:27

在前端开发的世界里,React 一直以其组件化、声明式的特性深受开发者喜爱。而自从 React 16.8 推出 Hooks(钩子函数)以来,函数组件的能力得到了极大提升,开发体验也变得更加丝滑。今天,我们就一起来深入了解 React Hooks 的核心用法,结合具体例子,轻松掌握它们的精髓,让你的代码更优雅、更高效!


一、什么是 Hooks?为什么要用 Hooks?

Hooks 是 React 官方为函数组件提供的一组“钩子”API,让你无需编写 class 组件,也能拥有状态管理、生命周期等强大功能。它们让代码更简洁、逻辑更清晰,极大提升了开发效率。

简单来说:

  • 以前:只有 class 组件才能用 state、生命周期
  • 现在:函数组件 + Hooks = 一切皆有可能!

二、常用 Hooks 的详解

1. useState —— 让你的变量“活”起来

作用:为函数组件引入响应式状态变量。

用法

import React, { useState } from 'react';

function Counter() {
  // count 是当前状态,setCount 是修改状态的方法
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>当前计数:{count}</p>
      <button onClick={() => setCount(count + 1)}>点我+1</button>
    </div>
  );
}

小结

  • useState 返回一个数组,包含当前状态和修改方法。
  • 每次调用 set 方法,组件会自动重新渲染。

小贴士

  • 可以用来管理表单输入、开关状态等各种场景。

2. useEffect —— 副作用管理小能手

作用:处理副作用,比如数据请求、订阅、手动操作 DOM 等。

用法

import React, { useState, useEffect } from 'react';

function DataLoader() {
  const [data, setData] = useState([]);

  useEffect(() => {
    // 模拟请求数据
    fetch('http://localhost:3000/data')
      .then(res => res.json())
      .then(json => setData(json));
  }, []); // 依赖数组为空,表示只在组件挂载时执行一次

  return (
    <ul>
      {data.map(item => <li key={item.id}>{item.name}</li>)}
    </ul>
  );
}

小结

  • 依赖数组为空时,只在组件首次加载时执行。
  • 依赖数组有值时,只有依赖变化才会执行。
  • 可以返回一个清理函数,用于组件卸载时执行。

小贴士

  • 用于数据请求、事件监听、定时器等场景。

3. useLayoutEffect —— 同步副作用的利器

作用:与 useEffect 类似,但它会在 DOM 更新后、浏览器绘制前同步执行。

用法

import React, { useLayoutEffect, useRef } from 'react';

function LayoutDemo() {
  const divRef = useRef();

  useLayoutEffect(() => {
    divRef.current.style.color = 'red';
  }, []);

  return <div ref={divRef}>我是红色的文字</div>;
}

小结

  • 适合需要同步读取布局、强制同步修改 DOM 的场景。
  • 一般情况下,优先用 useEffect,只有特殊需求才用 useLayoutEffect

4. useReducer —— 复杂状态管理的好帮手

作用:当状态逻辑复杂或多个状态相互关联时,推荐用 useReducer

用法

import React, { useReducer } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });

  return (
    <div>
      <p>计数:{state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>加1</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>减1</button>
    </div>
  );
}

小结

  • useReducer 接收 reducer 函数和初始 state。
  • 通过 dispatch 派发 action,reducer 返回新的 state。
  • 适合表单、复杂交互等场景。

5. useRef —— 获取 DOM 或保存变量

作用:获取 DOM 元素引用,或保存一个在组件生命周期内不会变化的变量。

用法

import React, { useRef } from 'react';

function InputFocus() {
  const inputRef = useRef();

  const focusInput = () => {
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} placeholder="点按钮聚焦我" />
      <button onClick={focusInput}>聚焦输入框</button>
    </div>
  );
}

小结

  • useRef 返回一个可变的 ref 对象,.current 属性指向 DOM。
  • 也可用于保存定时器 id、上一次的 props/state 等。

6. useContext —— 跨组件传值不再烦恼

作用:实现跨组件(祖孙、兄弟)间的数据共享,避免层层 props 传递。

用法

import React, { createContext, useContext } from 'react';

const ThemeContext = createContext('light');

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return <button style={{ background: theme === 'dark' ? '#333' : '#eee' }}>主题按钮</button>;
}

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <ThemedButton />
    </ThemeContext.Provider>
  );
}

小结

  • 先用 createContext 创建上下文,再用 Provider 提供数据。
  • 子组件用 useContext 获取数据,随时随地都能用!

三、实战小案例:数据列表的增删查

结合上面 Hooks,假如我们要做一个数据列表页面,支持加载、搜索、删除功能:

import React, { useState, useEffect } from 'react';

function DataList() {
  const [data, setData] = useState([]);
  const [keyword, setKeyword] = useState('');

  // 加载数据
  useEffect(() => {
    fetch(`http://localhost:3000/data?name=${keyword}`)
      .then(res => res.json())
      .then(json => setData(json));
  }, [keyword]);

  // 删除数据
  const handleDelete = id => {
    fetch(`http://localhost:3000/data/${id}`, { method: 'DELETE' })
      .then(() => setData(data.filter(item => item.id !== id)));
  };

  return (
    <div>
      <input
        value={keyword}
        onChange={e => setKeyword(e.target.value)}
        placeholder="搜索..."
      />
      <ul>
        {data.map(item => (
          <li key={item.id}>
            {item.name}
            <button onClick={() => handleDelete(item.id)}>删除</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

是不是很简单?只用 useStateuseEffect 就能搞定大部分需求啦!😄


四、总结

React Hooks 让函数组件变得更强大、更灵活。只要掌握了 useStateuseEffectuseLayoutEffectuseReduceruseRefuseContext 这 6 个常用钩子,你就能轻松应对绝大多数开发场景。建议大家多动手实践,遇到问题多查文档,慢慢你也会成为 Hooks 大师!💪

深度解析JavaScript中的call方法实现:从原理到手写实现的完整指南

2025年7月7日 11:23

前言:那些年我们踩过的this指向坑

大家在写JavaScript的时候,是不是经常被this指向搞得头疼?明明写的是同一个函数,在不同地方调用结果却完全不一样。今天我们就来彻底搞懂call方法的原理,并且手写一个完整的call实现。

相信看完这篇文章,你不仅能理解call的工作机制,还能在面试中自信地手写出来!

一、call方法到底是个什么东西?

1.1 call的本质:手动指定函数内部的this

var name = "Trump"
function gretting(...args){
    return `hello, I am ${this.name}.`;
}
const lj = {
    name: "王子"
}
console.log(gretting.call(lj)); // "hello, I am 王子."

核心理解: call方法让我们能够"借用"别人的方法,就像是给函数临时换了个身份证一样。

1.2 call、apply、bind的爱恨情仇

这三兄弟经常被放在一起比较,但它们各有特色:

方法 执行时机 参数传递方式 使用场景
call 立即执行 一个个传递 参数确定且不多时
apply 立即执行 数组形式传递 参数不确定或很多时
bind 延迟执行 一个个传递 需要保存函数供后续使用
// call: 参数一个个传
console.log(gretting.call(lj, 18, '抚州'));

// apply: 参数用数组包装
console.log(gretting.apply(lj, [18, '抚州']));

// bind: 返回新函数,延迟执行
const fn = gretting.bind(lj, 18, '抚州');
setTimeout(() => {
    console.log(fn()); // 1秒后执行
}, 1000);

二、深入call的工作原理

2.1 call是怎么改变this指向的?

很多人觉得call很神奇,其实原理很简单。我们先看一个例子:

var obj = {
    name: 'Tom',
    sayHello: function() {
        return `Hello, I am ${this.name}`;
    }
};

console.log(obj.sayHello()); // "Hello, I am Tom"

当我们调用obj.sayHello()时,this自然指向obj。call的原理就是:临时把函数挂载到目标对象上,调用完后再删除

2.2 严格模式下的特殊情况

"use strict"
function gretting() {
    return `hello, I am ${this.name}.`;
}

// 严格模式下,传入null或undefined会报错
console.log(gretting.call(null)); // TypeError
console.log(gretting.call(undefined)); // TypeError

重要提醒: 在严格模式下,如果传入null或undefined,this不会被转换为window对象,而是保持原值,这可能导致错误。

三、手写call方法:从零到一的实现过程

3.1 基础框架搭建

既然call是所有函数都能使用的方法,那它一定在Function.prototype上:

Function.prototype.myCall = function(context, ...args) {
    // 实现逻辑
};

3.2 参数处理:边界情况的优雅处理

Function.prototype.myCall = function(context, ...args) {
    // 处理context为null或undefined的情况
    if (context === null || context === undefined) {
        context = window; // 非严格模式下指向window
    }
    
    // 确保调用者是函数
    if (typeof this !== 'function') {
        throw new TypeError('Function.prototype.myCall was called on non-function');
    }
};

设计思考: 这里的边界处理体现了健壮性编程的重要性。在实际开发中,用户可能传入各种奇怪的参数,我们需要优雅地处理这些情况。

3.3 核心实现:Symbol的巧妙运用

Function.prototype.myCall = function(context, ...args) {
    if (context === null || context === undefined) {
        context = window;
    }
    
    if (typeof this !== 'function') {
        throw new TypeError('Function.prototype.myCall was called on non-function');
    }
    
    // 使用Symbol确保属性名唯一,避免覆盖原有属性
    const fnKey = Symbol('fn');
    
    // 将函数挂载到context上
    context[fnKey] = this;
    
    // 调用函数并收集结果
    const result = context[fnKey](...args);
    
    // 清理:删除临时添加的属性
    delete context[fnKey];
    
    // 返回执行结果
    return result;
};

技术亮点解析:

  1. Symbol的使用:ES6的Symbol类型确保了属性名的唯一性,避免了覆盖context原有属性的风险
  2. 动态属性添加:利用JavaScript对象的动态性,临时给context添加方法
  3. 及时清理:执行完毕后立即删除临时属性,避免污染原对象

3.4 完整测试:验证我们的实现

// 测试用例
function gretting(...args) {
    console.log(args); // 查看参数
    return `hello, I am ${this.name}.`;
}

var obj = {
    name: 'Tom',
    fn: function() {} // 已有属性,测试是否会被覆盖
};

// 测试我们的实现
console.log(gretting.myCall(obj, 1, 2, 3));
// 输出:[1, 2, 3]
// 输出:"hello, I am Tom."

// 验证原有属性没有被破坏
console.log(typeof obj.fn); // "function"

四、深度思考:为什么要这样设计?

4.1 JavaScript动态性的体现

我们的实现充分利用了JavaScript的动态特性:

  • 动态属性添加context[fnKey] = this
  • 动态方法调用context[fnKey](...args)
  • 动态属性删除delete context[fnKey]

这种设计让JavaScript具有了极大的灵活性,但也要求开发者更加小心地处理边界情况。

4.2 函数式编程思想的体现

// 函数作为一等公民
const boundFunction = gretting.bind(obj);

// 高阶函数的应用
function createBoundFunction(fn, context) {
    return function(...args) {
        return fn.myCall(context, ...args);
    };
}

设计哲学: call方法体现了JavaScript中"函数是一等公民"的设计理念,函数可以被传递、绑定、组合,这为函数式编程提供了基础。

五、实际应用场景:call在实战中的妙用

5.1 数组方法的借用

// 类数组对象借用数组方法
function example() {
    // arguments是类数组对象,没有数组方法
    const argsArray = Array.prototype.slice.call(arguments);
    console.log(argsArray); // 真正的数组
}

example(1, 2, 3); // [1, 2, 3]

5.2 继承中的应用

function Parent(name) {
    this.name = name;
}

function Child(name, age) {
    // 调用父类构造函数
    Parent.call(this, name);
    this.age = age;
}

const child = new Child('小明', 18);
console.log(child.name); // "小明"

5.3 函数柯里化的实现

function curry(fn) {
    return function curried(...args) {
        if (args.length >= fn.length) {
            return fn.call(this, ...args);
        } else {
            return function(...nextArgs) {
                return curried.call(this, ...args, ...nextArgs);
            };
        }
    };
}

六、性能考虑与最佳实践

6.1 性能对比

// 直接调用(最快)
obj.method();

// call调用(稍慢)
method.call(obj);

// apply调用(最慢,因为要处理数组参数)
method.apply(obj, args);

6.2 最佳实践建议

  1. 优先使用直接调用:如果可以直接调用,就不要使用call
  2. 参数少时用call:参数确定且不多时,call比apply性能更好
  3. 避免频繁使用:在性能敏感的场景中,避免在循环中频繁使用call
  4. 合理使用bind:如果需要多次调用同一个绑定函数,使用bind预先绑定

七、常见陷阱与调试技巧

7.1 箭头函数的特殊性

const arrowFunc = () => {
    console.log(this); // 箭头函数的this无法被call改变
};

const obj = { name: 'test' };
arrowFunc.call(obj); // this仍然是定义时的上下文

重要提醒: 箭头函数的this是词法绑定的,无法通过call、apply、bind改变。

7.2 调试技巧

Function.prototype.debugCall = function(context, ...args) {
    console.log('调用函数:', this.name || 'anonymous');
    console.log('绑定对象:', context);
    console.log('传入参数:', args);
    
    return this.myCall(context, ...args);
};

八、总结与展望

通过这篇文章,我们不仅理解了call方法的工作原理,还亲手实现了一个完整的call方法。这个过程让我们深入理解了:

  1. JavaScript的动态特性:对象属性的动态添加和删除
  2. 函数式编程思想:函数作为一等公民的体现
  3. 边界处理的重要性:健壮代码的必要条件
  4. Symbol的实际应用:解决属性名冲突的优雅方案

技术成长感悟: 手写call方法不仅仅是一个面试题,更是理解JavaScript核心机制的重要途径。当你能够从原理层面理解这些基础方法时,你对JavaScript的理解就上升到了一个新的层次。

这不仅仅是一个技术实现,更是一次深入JavaScript内核的探索之旅。希望这篇文章能够帮助你在前端开发的道路上走得更远、更稳!


相关代码示例都可以在项目中找到,建议大家动手实践,加深理解。记住,最好的学习方式就是动手写代码!

Vue 3 中的组件通信与组件思想详解

2025年7月7日 11:23

在现代前端开发中,Vue 作为一款渐进式 JavaScript 框架,凭借其简洁、高效和易上手的特性,深受广大开发者喜爱。尤其是 Vue 3 的推出,带来了 Composition API、性能优化以及更好的 TypeScript 支持,使得构建大型应用变得更加得心应手。

本文将围绕 Vue 3 中的组件通信方式组件化开发的思想 进行详细讲解,并结合生活中的实际案例,帮助你更深入地理解这些概念。


一、组件是什么?为什么需要组件?

1.1 组件的定义

在 Vue 中,组件是可复用的 UI 单元。它可以是一个按钮、一个输入框、一个导航栏,甚至是一个完整的页面结构。每个组件都拥有自己的模板(template)、逻辑(script)和样式(style)。

类比生活:你可以把组件想象成乐高积木。每一块积木都有固定的形状和功能,但通过不同的组合,可以拼出各种复杂的结构。组件也是一样,通过合理的拆分与组合,可以构建出复杂而灵活的用户界面。

1.2 组件化开发的优势

  • 可维护性高:组件独立性强,修改一处不影响全局。
  • 可复用性强:一个组件可以在多个地方重复使用。
  • 开发效率高:多人协作时,不同人可以负责不同的组件模块。
  • 结构清晰:代码结构层次分明,便于理解和调试。

二、Vue 3 中的组件通信方式详解

组件之间并不是完全孤立的,它们往往需要进行数据交互。Vue 提供了多种组件通信方式,适用于不同的场景。

我们将从最基础的父子组件通信讲起,逐步过渡到跨层级通信和全局状态管理。


2.1 父子组件通信:props + emits

这是最常见也是最基础的一种通信方式。

示例场景:

假设我们正在做一个“购物车”系统。父组件是 CartView,子组件是 CartItem,我们需要把商品信息传递给子组件,并且当用户点击删除按钮时,子组件要通知父组件删除该商品。

<!-- CartItem.vue -->
<template>
  <div class="cart-item">
    <span>{{ product.name }}</span>
    <button @click="removeProduct">删除</button>
  </div>
</template>

<script setup>
const props = defineProps({
  product: {
    type: Object,
    required: true
  }
})

const emit = defineEmits(['remove'])

function removeProduct() {
  emit('remove', product.id)
}
</script>
<!-- CartView.vue -->
<template>
  <CartItem v-for="item in cartItems" :key="item.id" :product="item" @remove="handleRemove" />
</template>

<script setup>
import { ref } from 'vue'
import CartItem from './CartItem.vue'

const cartItems = ref([
  { id: 1, name: '苹果' },
  { id: 2, name: '香蕉' }
])

function handleRemove(productId) {
  cartItems.value = cartItems.value.filter(item => item.id !== productId)
}
</script>

生活类比:

这就像父母告诉孩子要做某件事(比如“去拿快递”),孩子做完后会告诉父母:“我拿回来了”。


2.2 子传父:emits 的高级用法

除了基本的事件触发,还可以传递参数。例如上面例子中,子组件在 emit 时传入了 product.id,父组件就可以根据这个 ID 做进一步处理。


2.3 非父子组件通信:provide / inject

有时候我们需要跨越多个层级传递数据,比如主题色、用户信息等全局变量。这时我们可以使用 provideinject

示例场景:

我们有一个网站的主题配置(如深色/浅色模式),希望在整个应用中都能访问到这个配置。

<!-- App.vue -->
<script setup>
import { provide, ref } from 'vue'
const theme = ref('dark')
provide('theme', theme)
</script>
<!-- SomeChildComponent.vue -->
<script setup>
import { inject } from 'vue'
const theme = inject('theme')
</script>

注意:虽然 provide/inject 可以跨级传递数据,但它更像是“祖先传值”,不是响应式的绑定。如果希望实现响应式,建议使用 reactive 或者配合 watch 使用。

生活类比:

这就好比家里的 WiFi 密码写在一张纸上,家里每个人都可以看到并连接。不需要一个个通知,大家都知道怎么连。


2.4 全局状态管理:Pinia

对于大型项目来说,组件之间可能有复杂的通信需求,这时候就需要引入状态管理工具,比如 Vue 官方推荐的 Pinia

示例场景:

我们来模拟一个登录系统,用户登录之后,在多个组件中都需要显示用户名。

步骤一:创建 store
// stores/userStore.js
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    username: null
  }),
  actions: {
    login(name) {
      this.username = name
    },
    logout() {
      this.username = null
    }
  }
})
步骤二:在组件中使用
<!-- Login.vue -->
<script setup>
import { useUserStore } from '@/stores/userStore'
const userStore = useUserStore()

function handleLogin() {
  userStore.login('Tom')
}
</script>
<!-- Header.vue -->
<script setup>
import { useUserStore } from '@/stores/userStore'
const userStore = useUserStore()
</script>

<template>
  <div v-if="userStore.username">欢迎,{{ userStore.username }}</div>
  <div v-else>请登录</div>
</template>

生活类比:

这就像你家有个记事本,谁想记点什么都可以写上去,别人也能看到。Pinia 就像这个记事本,记录着整个家庭(应用)的状态信息。


2.5 自定义事件总线:mitt

如果你不想使用 Pinia,又需要非父子组件之间的通信,可以使用事件总线库,比如 mitt

安装 mitt:

npm install mitt

创建事件中心:

// eventBus.js
import mitt from 'mitt'
export default mitt()

发送事件:

import eventBus from '@/eventBus'

eventBus.emit('update-cart', newCartData)

接收事件:

import eventBus from '@/eventBus'

eventBus.on('update-cart', (data) => {
  console.log('接收到新的购物车数据:', data)
})

生活类比:

这就像小区广播站,谁想发消息就往广播里喊一声,其他人都能听到。适合轻量级的通信需求。


三、组件设计的最佳实践

3.1 单向数据流原则

  • 数据从父组件流向子组件,避免反向修改 props。
  • 如果子组件需要修改父组件的数据,应该通过 emit 事件通知父组件进行更新。

这就像老师布置作业给学生,学生不能擅自更改题目内容,而是应该反馈给老师说:“我觉得这个题太难了,能不能改一下?”老师再决定是否调整。


3.2 组件命名规范

  • 使用 PascalCase(如 UserProfileCard
  • 文件名建议使用 .vue 结尾,如 UserProfileCard.vue

3.3 组件职责单一原则

一个组件只做一件事。比如不要在一个组件里同时处理表单提交和数据展示,应该拆分成两个组件。


3.4 使用 slots 实现组件插槽

插槽允许我们在组件内部插入任意内容,非常灵活。

<!-- Card.vue -->
<template>
  <div class="card">
    <slot></slot>
  </div>
</template>
<!-- 使用 -->
<Card>
  <h2>标题</h2>
  <p>内容区域</p>
</Card>

生活类比:

这就像一个相框,你可以在里面放任何照片。组件提供的是框架,具体内容由使用者决定。


四、总结:组件化思维的本质

组件化不仅仅是技术上的拆分,更是一种思维方式的转变。它要求我们:

  • 把问题拆解成小块
  • 让每个部分专注做好一件事
  • 通过组合的方式解决大问题

这与生活中解决问题的方式非常相似:

想盖一栋房子?先准备好砖头、水泥、钢筋,然后一步步搭建。而不是直接从头开始垒墙,边垒边设计窗户位置。


五、结语

Vue 3 的组件化开发为我们提供了强大的工具和灵活的机制,使我们能够构建出结构清晰、易于维护、高度可复用的应用程序。掌握好组件通信的各种方式,并理解组件背后的设计哲学,是我们成为优秀 Vue 开发者的关键一步。

希望这篇文章能帮助你更好地理解 Vue 3 的组件通信机制与组件化思想。如果你觉得有用,不妨点赞收藏,也可以分享给你的朋友一起学习!

❌
❌