SSF0SSF0
首页
前端
  • Node
  • Go
  • Python
  • MySql
  • Bash
  • Git
  • Docker
  • VuePress
  • CI/CD
  • 服务器
  • 网站
  • 学习资料
  • 软件
  • 开源项目
Timeline
Github
标签
分类
首页
前端
  • Node
  • Go
  • Python
  • MySql
  • Bash
  • Git
  • Docker
  • VuePress
  • CI/CD
  • 服务器
  • 网站
  • 学习资料
  • 软件
  • 开源项目
Timeline
Github
标签
分类
  • HTML

    • html1
    • html2
  • CSS

    • Flex 布局常见问题与解决方案
  • JavaScript

    • 数据类型及引用问题
    • 处理 Blob 类型文件下载问题总结
    • localStorage 与 sessionStorage 区别
    • JavaScript 中的 script 标签问题详解
    • JavaScript 中的 this 指向问题详解
    • SessionStorage踩坑记录
    • 动态加载 JS 脚本方法对比
    • 浏览器页面关闭场景下的数据上报
    • HTTP流式接口详解与封装实践
    • JavaScript 中 this 绑定与解构:为什么 Pinia Store 可以解构,但 Class 实例方法不行?
  • es6

    • Promise
    • es6 模块导出方式全解析
    • reduce
    • forEach 中使用 async/await 的异步陷阱
  • Vue2

    • created VS mounted 发起异步请求
    • vue2-2
  • Vue3

    • Vite + Vue3 修改 element-plus 源码
    • Vue v-if 与 v-show
    • Vue3 ref 获取组件
    • Vue3 路由传参
    • 父子组件与组件里 Hooks 加载顺序
    • 第三方组件传入参数TS提示
    • Vue3 Props 在 Hooks 中的响应性处理
    • Vue Router 的两种历史模式及部署配置详解
    • 在Vue 3项目中顺利集成Tailwind CSS的完整指南
    • Vue Scoped CSS 深度解析
    • Vue 3 Teleport 组件完全指南:从原理到实战
    • Vant List 组件中双向绑定 loading 的注意事项
    • Vue 3 + Vite 中的路径别名与静态资源处理深度解析
    • 获取图片高度
    • 深入理解 Pinia:storeToRefs vs toRefs - 避开 TypeScript 类型推断的坑
  • webSocket

    • WebSocket 鉴权入门(实战版):握手票据 / 首帧鉴权 + 连接级 cid
  • Electron

    • 快速构建 electron + vue3 项目
  • TS

    • TS 泛型
    • 记录模板使用断言的问题
    • type 与 interface
  • WebPack

    • Webpack 介绍
  • Vite

    • Vite CLI 常见命令
    • vite 与 webpack 比较
    • Vite base 配置与 public 静态资源路径问题
  • 客户端数据库

    • IndexedDB 索引陷阱:字段重命名后索引返回旧数据
    • IndexedDB 事务陷阱与最佳实践:TransactionInactiveError 完全解析
  • UniApp

    • uni-app + Monorepo + 微信小程序热更新失效问题排查与结论总结
    • Monorepo + uni-app 开发中监听 / 热更新机制差异与解决方案
    • uni-app 运行与编译原理梳理
    • 为什么 uni-app 在 Monorepo 中必须做「环境区分」,而 Web 项目却不用?
  • 项目工程

    • 前端代码风格
    • Vue3 项目规范
    • npm 镜像问题
    • 包管理工具
    • 使用 engines 限制开发环境依赖
    • 打包与shell交互指定模式
    • Monorepo示例
    • PC 项目兼容 uni-app:基于 pnpm Monorepo 的工程化重构实践
    • Vue3 + Vite + pnpm Monorepo 迁移问题指南
    • 前端工程化:深入理解 npm 依赖管理机制
    • pnpm 修改依赖源码打包报错
    • 代码片段
  • 移动端

    • 浏览器跳转外部应用时请求状态码问题
  • 记录一下小程序
  • 控制浏览器正确保存网站账号密码的技巧
  • 信息泄露
  • 使用Gulp实现前端项目哈希化压缩打包
  • 前端缓存策略与文件哈希化的关联解析
  • 浏览器刷新方式与 Vue 生命周期、缓存行为解析
  • 浏览器消息铃声实现记录

代码片段

片段 1

enterprises.value = res.data
  .map((item: IEnterprise) => {
    const { authorizationType, accessScope, workName, id } = item; // 解构赋值

    if (
      authorizationType === 1 ||
      (authorizationType === 2 && accessScope.includes("企微客服"))
    ) {
      return {
        label: workName,
        value: id,
        authorizationType,
      };
    }

    return null;
  })
  .filter(Boolean); // 过滤掉 null 值
  1. 使用解构赋值,减少变量名

  2. 使用 Boolean 转换(去除 falsy 值,包括 null、undefined、''、0、false、NaN)

var arr = ["1", null, "2", "", undefined, "3.jpg", "  ", "0"];

只去除 null 和 undefined

var newArr = arr.filter((v) => v != null); // 等价于 v !== null && v !== undefined
console.log(newArr); // ['1', '2', '', '3.jpg', '  ', '0']

使用 Boolean(去除所有 falsy 值)

var newArr = arr.filter(Boolean);
console.log(newArr); // ['1', '2', '3.jpg', '  ', '0']

filter(Boolean) 会移除以下值:

  • false , 0 ,""(空字符串),null,undefined,NaN

但不会移除 " "(空格字符串),因为它是 truthy。

去除 null、undefined、空字符串(包括纯空格)

var newArr = arr.filter((v) => v && v.trim() !== "");
console.log(newArr); // ['1', '2', '3.jpg', '0']

自定义函数封装

function cleanArray(arr) {
  return arr.filter((v) => v != null && String(v).trim() !== "");
}
console.log(cleanArray(arr)); // ['1', '2', '3.jpg', '0']

注重性能(当前的 reduce 方案),但可读性较差

enterprises.value = res.data.reduce((acc: IEnterprise[], item: IEnterprise) => {
  const { authorizationType, accessScope, workName, id } = item;

  if (
    authorizationType === 1 ||
    (authorizationType === 2 && accessScope.includes("企微客服"))
  ) {
    acc.push({
      label: workName,
      value: id,
      authorizationType,
    });
  }

  return acc;
}, []);

reduce 的实现与运用

片段 2

三元表达式处理接口

const params: IContactPerson = buildXXXParams(); // 构建参数

const res = await(isEdit.value ? updateContactPerson : addContactPerson)(
  params
); // 三元表达式处理添加/编辑操作
// 多个接口使用对象映射,不过具体看场景,复杂场景建议单一接口,遵循单一职责原则
const operation = {
  add: addContactPerson,
  edit: updateContactPerson,
  // ...
};

const res = await operation[isEdit.value ? "edit" : "add"](params); // 三元表达式处理添加/编辑操作

三元表达式嵌套处理空格

原本的写法:

<template>
  <div v-for="item in list" :key="item.id">
    {{ item.value ? (item.value === " " ? "false" : "true") : "false" }}
    <!-- 在vue模板中处理空格,使用嵌套三元表达式 -->
  </div>
</template>

最佳的写法:

<!--  数据源处理(最佳) -->
<script setup lang="ts">
interface ListItem {
  id: string;
  value: string;
  displayText: string; // 预处理的显示文本
}

const list = ref<ListItem[]>([]);

// 在获取数据时处理,避免在模板中处理
const processListData = (rawData: ListItem[]) => {
  return rawData.map((item) => ({
    ...item,
    displayText: getDisplayText(item.value),
  }));
};

// 处理空格
const getDisplayText = (value: string) => {
  if (!value || value === " ") return "false";
  return "true";
};
</script>

<template>
  <div v-for="item in list" :key="item.id">
    {{ item.displayText }}
  </div>
</template>

片段 3

嵌套循环的判断优化

const checkSkipLink = (): boolean => {
  let hasWarning = false;
  if (pageData.value.type === "2") {
    formData.value.generateData.forEach((item: IWidgetComponents) => {
      if (item.type === "button" && item.options.skipLink) {
        if (!item.options.skipLink.startsWith("https://")) {
          ElMessage.error("跳转企微链接需是完整链接,包含https://");

          hasWarning = true;
        } else if (!item.options.skipLink.includes("qq.com")) {
          hasWarning = true;

          ElMessage.error("跳转企微链接必须包含域名qq.com");
        }
      } else if (item.type === "group") {
        item.cols.forEach((arrItem: IWidgetComponents[]) => {
          arrItem.forEach((item: IWidgetComponents) => {
            if (item.type === "button" && item.options.skipLink) {
              if (!item.options.skipLink.startsWith("https://")) {
                ElMessage.error("跳转企微链接需是完整链接,包含https://");

                hasWarning = true;
              } else if (!item.options.skipLink.includes("qq.com")) {
                hasWarning = true;

                ElMessage.error("跳转企微链接必须包含域名qq.com");
              }
            }
          });
        });
      } else if (item.type === "groupBox") {
        const widget = item as IGroupBoxWidget;
        widget.childList.forEach((item: IWidgetComponents) => {
          if (item.type === "button" && item.options.skipLink) {
            if (!item.options.skipLink.startsWith("https://")) {
              ElMessage.error("跳转企微链接需是完整链接,包含https://");

              hasWarning = true;
            } else if (!item.options.skipLink.includes("qq.com")) {
              hasWarning = true;

              ElMessage.error("跳转企微链接必须包含域名qq.com");
            }
          }
        });
      }
    });
  }

  return !hasWarning;
};

优化后:

const checkSkipLink = (): boolean => {
  // 将配置提取到方法内部
  const VALIDATION_CONFIG = {
    REQUIRED_PROTOCOL: "https://",
    REQUIRED_DOMAIN: "qq.com",
    TARGET_PAGE_TYPE: "2",
  } as const;

  // 将错误消息提取
  const ERROR_MESSAGES = {
    INVALID_PROTOCOL: "跳转企微链接需是完整链接,包含https://",
    INVALID_DOMAIN: "跳转企微链接必须包含域名qq.com",
  } as const;

  let isSuspend = false;

  // 链接验证逻辑,职责更单一
  const validateSkipLink = (link: string): boolean => {
    if (!link.startsWith(VALIDATION_CONFIG.REQUIRED_PROTOCOL)) {
      ElMessage.error(ERROR_MESSAGES.INVALID_PROTOCOL);
      return true;
    }

    if (!link.includes(VALIDATION_CONFIG.REQUIRED_DOMAIN)) {
      ElMessage.error(ERROR_MESSAGES.INVALID_DOMAIN);
      return true;
    }

    return false;
  };

  // 处理单个组件的验证
  const processItem = (item: IWidgetComponents) => {
    if (item.type === "button" && item.options.skipLink) {
      isSuspend = validateSkipLink(item.options.skipLink) || isSuspend;
    }
  };

  // 递归处理组合型组件
  const processGroupItem = (item: IWidgetComponents) => {
    if (item.type === "group") {
      item.cols.forEach((arrItem: IWidgetComponents[]) => {
        arrItem.forEach(processItem);
      });
    } else if (item.type === "groupBox") {
      const widget = item as IGroupBoxWidget;
      widget.childList.forEach(processItem);
    } else {
      processItem(item);
    }
  };

  // 主逻辑:只有特定页面类型才需要检查
  if (pageData.value.type === VALIDATION_CONFIG.TARGET_PAGE_TYPE) {
    formData.value.generateData.forEach(processGroupItem);
  }

  return isSuspend;
};

片段 4

接口拆分

当前端数据属性与接口需要传入属性不一致的(很多的)情况,使用映射关系进行处理

/**
 * 设置ai组件数据
 * @param data 数据,为低代码组件list,包含aiUtils、groupBox等组件
 * @param aiCustomerServices ai组件数据,为接口返回数据(多个ai组件数据)
 */
const setAiComponentData = (
  data: IWidgetComponents[],
  aiCustomerServices: IAiConversationParams[]
) => {
  data.forEach((item) => {
    if (item.type === "aiUtils") {
      const aiUtils = item as IAiUtilsWidget;
      aiCustomerServices.find((aiCustomerService) => {
        if (
          aiCustomerService.componentKey === aiUtils.key &&
          aiCustomerService.type === "aiConversation"
        ) {
          aiUtils.options.customerDesc =
            aiCustomerService.accurateCustomerDescription;
          aiUtils.options.unCustomerDesc =
            aiCustomerService.nonTargetCustomerDescription;
          aiUtils.options.roleDefinition = aiCustomerService.roleCustomization;
          aiUtils.options.welcomeContent = aiCustomerService.welcomeMessage;
          aiUtils.options.knowledgeBase =
            aiCustomerService.relatedKnowledgeBase;
          return true;
        } else if (
          aiCustomerService.componentKey === aiUtils.key &&
          aiCustomerService.type === "aiAssistant"
        ) {
          aiUtils.options.assistantName = aiCustomerService.one;
          aiUtils.options.greeting = aiCustomerService.two;
          aiUtils.options.capabilities = aiCustomerService.three;
          return true;
        }
        // 等等等...
      });
    } else if (item.type === "groupBox") {
      const groupBox = item as IGroupBoxWidget;
      setAiComponentData(groupBox.childList, aiCustomerServices);
    }
  });
};

优化后:

/**
 * 设置ai组件数据
 * @param data 数据
 * @param aiCustomerServices ai组件数据
 */
const setAiComponentData = (
  data: IWidgetComponents[],
  aiCustomerServices: IAiConversationParams[]
) => {
  const serviceMap = new Map(
    aiCustomerServices.map((service) => [service.componentKey, service])
  );

  // 配置不同类型的属性映射
  const AI_SERVICE_HANDLERS = {
    // 添加 aiConversation 的映射配置
    aiConversation: {
      mappings: {
        accurateCustomerDescription: "customerDesc",
        nonTargetCustomerDescription: "unCustomerDesc",
        roleCustomization: "roleDefinition",
        welcomeMessage: "welcomeContent",
        relatedKnowledgeBase: "knowledgeBase",
      },
    },
    // 后续的扩展,aiAssistant 的映射配置
    // aiAssistant: {
    //   mappings: {
    //     roleCustomization: "assistantName",
    //     welcomeMessage: "greeting",
    //     relatedKnowledgeBase: "capabilities"
    //   },
    // }
  } as const;

  const processAiComponent = (component: IAiUtilsWidget) => {
    const service = serviceMap.get(component.key);
    if (!service) {
      // 找不到数据源问题 serviceMap 生成有误
      console.error(`AI组件缺少服务配置: componentKey="${component.key}"`, {
        availableServices: Array.from(serviceMap.keys()),
        component,
        possibleCauses: [
          "后端未返回此组件的服务配置",
          "组件key与服务componentKey不匹配",
          "aiCustomerServices数据缺失",
        ],
      });
      return;
    }

    const handler =
      AI_SERVICE_HANDLERS[service.type as keyof typeof AI_SERVICE_HANDLERS];
    if (!handler) {
      // 找不到映射配置问题 AI_SERVICE_HANDLERS 生成有误
      console.error(`AI服务类型缺少映射配置: serviceType="${service.type}"`, {
        availableHandlers: Object.keys(AI_SERVICE_HANDLERS),
        service,
        solution: `请在 AI_SERVICE_HANDLERS 中添加 "${service.type}" 类型的映射配置`,
      });
      return;
    }

    // 正常的映射逻辑
    const updates = Object.entries(handler.mappings).reduce(
      (acc, [sourceKey, targetKey]) => {
        const value = service[sourceKey as keyof IAiConversationParams];
        if (value !== undefined) {
          acc[targetKey] = value;
        }
        return acc;
      },
      {} as any
    );

    Object.assign(component.options, updates);
  };

  const processComponents = (components: IWidgetComponents[]) => {
    components.forEach((item) => {
      if (item.type === "aiUtils") {
        processAiComponent(item as IAiUtilsWidget);
      } else if (item.type === "groupBox") {
        const groupBox = item as IGroupBoxWidget;
        processComponents(groupBox.childList);
      }
    });
  };

  processComponents(data);
};

执行流程:

setAiComponentData 函数完整执行流程详解

// 假设传入的数据
const data = [
  { type: "aiUtils", key: "ai_001", options: {} },
  { type: "button", key: "btn_001" },
  {
    type: "groupBox",
    key: "group_001",
    childList: [
      { type: "aiUtils", key: "ai_002", options: {} },
      { type: "text", key: "txt_001" },
    ],
  },
];

const aiCustomerServices = [
  {
    componentKey: "ai_001",
    type: "aiConversation",
    accurateCustomerDescription: "精准客户描述内容",
    nonTargetCustomerDescription: "非目标客户描述内容",
    roleCustomization: "AI角色定制内容",
    welcomeMessage: "欢迎语内容",
    relatedKnowledgeBase: "相关知识库内容",
  },
  {
    componentKey: "ai_002",
    type: "aiConversation",
    // ... 其他属性
  },
];

第 1 步:创建服务映射表 - Map 工作原理

const serviceMap = new Map(aiCustomerServices.map((service) => [service.componentKey, service]))

// Map 构造过程详解:
// 1. .map() 生成二维数组:
const pairs = [
  ["ai_001", { componentKey: "ai_001", type: "aiConversation", accurateCustomerDescription: "精准客户描述内容", ... }],
  ["ai_002", { componentKey: "ai_002", type: "aiConversation", ... }]
]

// 2. new Map() 将二维数组转换为Map对象:
// Map {
//   "ai_001" => { componentKey: "ai_001", type: "aiConversation", ... },
//   "ai_002" => { componentKey: "ai_002", type: "aiConversation", ... }
// }

// 重要:Map 不是普通对象 {},必须用 .get(key) 访问值
const service = serviceMap.get("ai_001")  // 返回完整的service对象

第 2 步:定义映射配置

const AI_SERVICE_HANDLERS = {
  aiConversation: {
    mappings: {
      accurateCustomerDescription: "customerDesc",
      nonTargetCustomerDescription: "unCustomerDesc",
      roleCustomization: "roleDefinition",
      welcomeMessage: "welcomeContent",
      relatedKnowledgeBase: "knowledgeBase",
    },
  },
};

第 3 步:开始处理组件

processComponents(data); // 传入整个数据数组

第 4 步:遍历顶层组件

data.forEach((item) => {
  // item1: { type: "aiUtils", key: "ai_001", options: {} }
  // item2: { type: "button", key: "btn_001" }
  // item3: { type: "groupBox", key: "group_001", childList: [...] }
});

第 5 步:处理第一个 aiUtils 组件

// item.type === "aiUtils" 为 true
processAiComponent(item as IAiUtilsWidget)

// 在processAiComponent中:
const service = serviceMap.get("ai_001") // 找到完整的服务对象

const handler = AI_SERVICE_HANDLERS["aiConversation"] // 获取映射配置

第 6 步:属性映射核心逻辑

6.1 Object.entries() 转换映射配置

// Object.entries(handler.mappings)
const entries = [
  ["accurateCustomerDescription", "customerDesc"],
  ["nonTargetCustomerDescription", "unCustomerDesc"],
  ["roleCustomization", "roleDefinition"],
  ["welcomeMessage", "welcomeContent"],
  ["relatedKnowledgeBase", "knowledgeBase"],
];

6.2 reduce() 逐项处理映射

// .reduce((acc, [sourceKey, targetKey]) => {...}, {} as any)
// 初始值:acc = {}

// === 第1次迭代 ===
// [sourceKey, targetKey] = ["accurateCustomerDescription", "customerDesc"]
const value = service["accurateCustomerDescription"]; // "精准客户描述内容"
if (value !== undefined) {
  // true
  acc["customerDesc"] = "精准客户描述内容";
}
// acc 现在是:{ customerDesc: "精准客户描述内容" }

// === 第2次迭代 ===
// [sourceKey, targetKey] = ["nonTargetCustomerDescription", "unCustomerDesc"]
const value = service["nonTargetCustomerDescription"]; // "非目标客户描述内容"
if (value !== undefined) {
  // true
  acc["unCustomerDesc"] = "非目标客户描述内容";
}

// ... 依次处理所有映射规则

// 最终 reduce 返回的 updates:
const updates = {
  customerDesc: "精准客户描述内容",
  unCustomerDesc: "非目标客户描述内容",
  roleDefinition: "AI角色定制内容",
  welcomeContent: "欢迎语内容",
  knowledgeBase: "相关知识库内容",
};

6.3 添加组件类型并批量更新

// 添加组件类型
updates.componentsType = "aiConversation";

// 批量更新组件属性
// 假设 component.options 原来是:{ someExistingProp: "原有属性" }
Object.assign(component.options, updates);

// 执行后 component.options 变成:
// {
//   someExistingProp: "原有属性",        // 保留原有属性
//   customerDesc: "精准客户描述内容",      // 新增
//   unCustomerDesc: "非目标客户描述内容",   // 新增
//   roleDefinition: "AI角色定制内容",     // 新增
//   welcomeContent: "欢迎语内容",        // 新增
//   knowledgeBase: "相关知识库内容",      // 新增
//   componentsType: "aiConversation"     // 新增
// }

第 8 步:处理 groupBox 组件(递归)

// item.type === "groupBox" 为 true

const groupBox = item as IGroupBoxWidget
processComponents(groupBox.childList) // 递归调用!

// 递归处理 groupBox.childList,重复上述流程:
// - 遇到 ai_002,调用 processAiComponent 处理
// - 遇到 text,跳过

关键技术点总结

1. Map 数据结构的优势

  • 创建: new Map([[key1, value1], [key2, value2]]) 从二维数组创建

  • 查找: map.get(key) 时间复杂度 O(1),比数组查找 O(n) 更高效

  • 返回值: .get(key) 返回完整的 value 对象

2. 配置化映射机制

  • Object.entries(): 将对象转为 [key, value] 数组

  • 数组解构: [sourceKey, targetKey] 同时获取源属性名和目标属性名

  • 动态属性访问: service[sourceKey] 用变量作为属性名访问

  • reduce 累积器: 从空对象 {} 开始,逐步构建最终的 updates 对象

3. 批量更新策略

  • Object.assign(): 一次性复制所有属性,比逐个赋值更高效

  • 属性合并: 保留原有属性,覆盖同名属性,添加新属性

4. 递归遍历设计

  • 深度优先: 遇到嵌套结构(groupBox)时递归处理

  • 类型判断: 通过 item.type 决定处理方式

  • 无限层级: 可以处理任意深度的组件嵌套

整体设计优势

  1. 高性能: Map 查找 O(1) + 批量更新

  2. 高扩展性: 配置化映射,新增 AI 类型只需添加配置

  3. 类型安全: TypeScript 类型约束确保代码健壮性

  4. 递归处理: 优雅处理复杂的嵌套组件结构

  5. 代码简洁: 函数式编程思维,逻辑清晰易懂

最后更新时间:
贡献者: 何风顺
上一页
pnpm 修改依赖源码打包报错