阅读视图

发现新文章,点击刷新页面。

你不知道的Git忽略文件新方式

正常来说

我们忽略不提交的文件都是配置.gitignore 

或者就是在IDE里将不提交的文件放入其他的变更列表

但是两种方式在某些时候都并不想使用

因为我仅仅只是想让这个文件的更改只运用在我本地环境而已

例如配置文件 (打个比方)

配置忽略吧,以后可能还会加新的东西

不配置忽略吧,不小心提上去又会影响到同事的本地环境

放入变更列表吧,每次看着都难受,变更分支还得git stash

然后我就去了解了一下....

在 Git 中实现仅本地忽略文件修改,且不修改 .gitignore 文件,可通过以下两种方法实现:


方法一:标记文件为“假设未修改”

  1. 执行命令
    在终端输入:

    git update-index --assume-unchanged <文件路径>
    

    此命令会告知 Git 将该文件视为未修改状态,后续变更不会显示在 git status 中‌。

  2. 取消忽略
    若需恢复跟踪,执行:

    git update-index --no-assume-unchanged <文件路径>
    

适用场景
需临时忽略本地修改(如配置文件调试),但需手动维护忽略状态,且换仓库后需重新设置‌。


方法二:标记文件为“跳过工作树”

  1. 执行命令

    git update-index --skip-worktree <文件路径>
    

    此命令会完全跳过对文件工作树状态的检查,即使文件被修改,Git 也不会检测到变更‌。

  2. 取消忽略

    git update-index --no-skip-worktree <文件路径>
    

适用场景
需长期忽略本地修改(如 IDE 生成文件),且希望彻底屏蔽变更检测‌。


总结

  • 两种方法对比

    方法 特点 推荐场景
    --assume-unchanged 仅标记文件为未修改,Git 仍可能偶尔检测到变更‌ 临时调试或短期忽略
    --skip-worktree 完全跳过文件状态检查,忽略更彻底‌ 长期忽略或频繁变更的文件
  • 注意事项

    • 两种方法均为‌本地配置‌,不会影响 .gitignore 或远程仓库‌。
    • 以上指的 <文件路径> 是绝对路径,如:E:\shop\src\main\resources\application.yml

最后

不管你会与否,看看有个记忆。

感谢前来了解,大家共同进步,相互分享知识。

🔥为啥一行代码就可以让当前页代码显示到网页中?

看过白泽开源团队出品的baize-quick-study的小伙伴们,可能或多或少都会有这样的一个问题,为啥一行<CodeDemo />就可以让当前页的代码显示到网页中?这到底是什么神奇写法呀?

链接:baize-quick-study.pages.dev/main

源码:github.com/baizeteam/b…

莫慌莫慌,待我们来一步步揭秘。

拉好扶手,焊死车门,一个也不准下车!!!

代码渲染

咱们先找到CodeDemo的代码,看看它里面做了些什么操作?

哦,easy,咱们可以看到里面划分了父子组件(CodeDemo 和 CodeDemoItem)。

然后其中的CodeDemoItem,不就是用highlight.js对代码进行渲染吗?

秒了,完结撒花!!!

嘿嘿,看到这,老夫只想说一句:

年轻人是不是高兴的太早了?

年轻人是不是高兴的太早了?

年轻人是不是高兴的太早了?

哎呀,重要的事情就是容易不小心多说了两遍。反正撤不回了,那就继续往下讲吧。

是的,通过highlight.js来渲染代码是没有问题的,但是数据怎么来的?

咱们继续看父组件CodeDemo,可以看到是从props中获取数据,然后传递给CodeDemoItem进行渲染的。

好像也没啥问题呀!

小伙伴们有没有发现遗漏了一个问题?

咱们外部使用组件时,也没有传入codeData、codePath、fileListCode这些props啊,为啥它能拿到的?

糟糕,脑子好痒,好像要长脑子了!!!

emmm,确实常规写法是要从外层组件中传入props的

但是,有没有一种可能,这不是常规写法呢?

众所周知,像react、vue这样的代码,直接放在浏览器中是无法直接运行的,需要通过一层转译才可以执行的。又或者es6怎么运行在低版本的浏览器中?

一般来说,这层转译都是通过babel来处理的,那你说有没有可能CodeDemo的数据也是这样获取的?

是的,你猜对了。

答案就是:vite 自定义插件 + babel + ast

插件详解

插件入口

咱们可以在vite.config.ts中找到插件的入口

下面用react版本的vite插件进行讲解

拦截 tsx

首先,我们需要在插件的 load 钩子中拦截所有后缀为 .tsx 的文件。通过自定义插件,我们可以让 Vite 在构建时对这些文件进行处理。

function viteRenderCode(): PluginOption {
  return {
    name: "vite-render-code",
    enforce: "pre",
    load(id) {
        // 拦截所有的tsx文件
        if (id.endsWith(".tsx")) {
        }
    }
  }

注入props

通过解析文件的 AST,我们可以检查每个 React 组件的定义。我们需要判断组件名称是否为 CodeDemo。找到 CodeDemo 组件后,我们将提取其相关参数(例如 props 中的 codeDatacodePath)并对其进行修改或注入新的值。

const ast = parse(code, {
    sourceType: "module",
    plugins: ["typescript", "jsx"],
  });
  const currentPath = id;
  traverse.default(ast, {
    JSXOpeningElement(path) {
      if (path.node.name.name === "CodeDemo") {
        // 将 code 作为 props 注入到 CodeDemo 组件中
        // 将fileList构建成对应的ast节点
      }
    },
  });

显示其他文件

需要注意的是fileList,这个主要是让我们可以将一些想显示的文件也一并显示出来。这里主要是通过ast语法树来构建ast节点

const astFileListCode = t.jsxAttribute(
    t.jsxIdentifier("fileListCode"),
    t.jsxExpressionContainer(
      t.arrayExpression(
        fileListCode.map((item) =>
          t.objectExpression([
            t.objectProperty(t.stringLiteral("fileCode"), t.stringLiteral(item.fileCode)),
            t.objectProperty(t.stringLiteral("filePath"), t.stringLiteral(item.filePath)),
          ]),
        ),
      ),
    ),
  );
  path.node.attributes.push(astFileListCode);

完整代码

// viteRenderCode.ts
import { join } from "path";
import { PluginOption } from "vite";
import { readFileSync } from "fs";
import { parse } from "@babel/parser";
import traverse from "@babel/traverse";
import generate from "@babel/generator";
import t from "@babel/types";

const htmlEntities: { [key: string]: string } = {
  "&": "&amp;",
  "<": "&lt;",
  ">": "&gt;",
  "'": "&#39;",
  '"': "&quot;",
  "`": "&grave;",
  "^": "&circ;",
  "~": "&tilde;",
  "—": "&mdash;",
  "•": "&bull;",
  "–": "&ndash;",
  "?": "&#63;",
  ":": "&#58;",
  $: "&#36;",
};

const escapeHtml = (str: string) => {
  return str?.replace(/[&<>'"`^~—•–?:$]/g, (tag) => htmlEntities[tag] || tag);
};

const getReactComponentProps = ({ data, name }) => {
  return {
    type: "JSXAttribute",
    name: {
      type: "JSXIdentifier",
      name: name,
    },
    value: {
      type: "StringLiteral",
      value: data,
    },
  };
};

const addReactCompoentProps = ({ path, data, name }) => {
  const params = getReactComponentProps({
    data,
    name,
  });
  path.node.attributes.push(params);
};

function viteRenderCode(): PluginOption {
  let _originalConfig;
  let _resolvedConfig;
  let _basePath = join(process.cwd(), "..");
  return {
    name: "vite-render-code",
    enforce: "pre",
    configResolved(resolvedConfig) {
      _resolvedConfig = resolvedConfig;
    },
    config(config) {
      _originalConfig = config;
    },
    load(id) {
      if (id.endsWith(".tsx")) {
        const code = readFileSync(id, "utf-8");
        if (code.indexOf("<CodeDemo") !== -1 && id.indexOf("CodeDemo") === -1) {
          const ast = parse(code, {
            sourceType: "module",
            plugins: ["typescript", "jsx"],
          });
          const currentPath = id; // id.replace(_basePath, "");
          traverse.default(ast, {
            JSXOpeningElement(path) {
              if (path.node.name.name === "CodeDemo") {
                // 将 code 作为 props 注入到 CodeDemo 组件中
                const codeProp = path.node.attributes.find((attr) => attr.name.name === "codeData");
                if (!codeProp) {
                  addReactCompoentProps({
                    path,
                    data: escapeHtml(code),
                    name: "codeData",
                  });
                  addReactCompoentProps({
                    path,
                    data: currentPath,
                    name: "codePath",
                  });
                }
                const fileListProp = path.node.attributes
                  .find((attr) => attr.name.name === "fileList")
                  ?.value.expression.elements.map((item) => item.value);
                if (fileListProp) {
                  const fileListCode = [];
                  for (let item of fileListProp) {
                    const curAlias = item.split("/")[0];
                    const filePath = item.replace(curAlias, _originalConfig.resolve.alias[curAlias]);
                    const fileCode = readFileSync(filePath, "utf-8");
                    fileListCode.push({
                      fileCode: escapeHtml(fileCode),
                      filePath: filePath, // filePath.replace(_basePath, ""),
                    });
                  }
                  const astFileListCode = t.jsxAttribute(
                    t.jsxIdentifier("fileListCode"),
                    t.jsxExpressionContainer(
                      t.arrayExpression(
                        fileListCode.map((item) =>
                          t.objectExpression([
                            t.objectProperty(t.stringLiteral("fileCode"), t.stringLiteral(item.fileCode)),
                            t.objectProperty(t.stringLiteral("filePath"), t.stringLiteral(item.filePath)),
                          ]),
                        ),
                      ),
                    ),
                  );
                  path.node.attributes.push(astFileListCode);
                }
              }
            },
          });
          const { code: transformedCode } = generate.default(ast);
          return transformedCode;
        }
        return code;
      }
      return null;
    },
  };
}

export default viteRenderCode;

小结

本文主要介绍了如何通过编写一个自定义的 Vite 插件,结合AST,将我们项目中的真实代码动态展示到网页中。通过本文,希望小伙伴们可以学习到在构建过程中如何处理文件,解析其中的组件,并根据需要注入特定的属性或代码,进而掌握通过ast修改源码以及开发vite插件的能力。

Chat2DB创始人姬朋飞:AI在 text2sql应用领域的实践

2025年5月17日,第76期DataFunSummit:AI Agent技术与应用峰会将在DataFun线上社区举办。Manus的爆火并非偶然,随着基础模型效果不断的提升,Agent作为大模型的超级应用备受全世界的关注。为了推动其技术和应用,本次峰会计划邀请40+位深耕AI Agent领域的专家学者同台分享交流,共同探讨技术进展和应用形式。

在5月17日上午的DataAgent论坛上,爱獭科技CEO姬朋飞将出席此次会议,并带来主题分享:《AI在 text2sql应用领域的实践》

专家介绍:

Chat2DB 创始人,Easyexcel 作者

演讲提纲

一、业务背景与挑战

传统SQL开发痛点分析

业务分析场景特点

现有解决方案的局限性

项目目标与技术挑战

二、技术方案设计

整体架构

系统架构设计

核心技术模块

数据流转流程

关键技术突破

领域知识增强的Prompt设计

多阶段生成策略详解

RAG检索增强方案

人类反馈闭环机制

三、落地实践与优化

实践难点攻克

复杂查询处理

跨库查询优化

性能提升方案

效果优化策略

准确率提升方法

业务知识积累

错误处理机制

四、应用成效与经验总结

应用效果

准确率指标

效率提升数据

业务价值体现

经验与展望

落地经验总结

最佳实践分享

未来演进方向

五、互动问答 (10分钟)

Key Takeaways:

大规模落地经验

技术创新点

实践中的坑与解决方案

产品化建议

听众收益:

技术实践价值

深入了解Text2SQL在大规模商业场景的完整解决方案

掌握LLM结合业务知识的关键技术路径和实现方法

获取准确率优化、复杂查询处理等技术难点的解决思路

学习RAG、多阶段生成等前沿技术的实战应用

工程落地经验

获得大规模AI系统从0到1的完整建设经验

了解项目推进过程中的典型坑点和规避方法

掌握效果优化、成本控制的实用策略

学习性能调优和系统可用性保障的实践经验

落地挑战和方案重点

1.数据准确性

2.权限

报名方式

大会议程

为何 css 写了不生效?

前言

前端开发有时候我明明写了 css 属性但是样式却没能在浏览器中生效,这其实很大概率原因就是你可能还不了解 css 属性值的计算过程,本期文章就带大家过一遍这部分知识,或许有你不清楚的内容🤷‍♂️

css 属性值的计算过程

CSS属性值的计算过程指的是浏览器渲染引擎将 CSS 属性从声明到最终渲染的处理流程,直白点就是 css 属性从没有值 到 有值的 过程。

css 属性并不是不写 css 就没有属性了,浏览器会有个 用户代理样式表 user agent stylesheet ,你可以理解为浏览器默认的样式

比如 我写一个 h1 元素,不给他写任何属性,默认就会有如下样式

1.png

这里你会发现 h1 默认样式中有个 display: block; 这其实就是为啥 h1 是一个块级元素,就是因为浏览器赋予了它这个属性,仅此而已,在 html 语义化出来之前你可以这样叫,什么块级元素都是 html5 之前的叫法,h5 出来之后官方摒弃了这个说法,如今你要知道的是这都是 css 属性默认值的效果

当我们展开 computed,你就会看到更多的属性,computed 就是这个属性最终计算出来的样式,实际上 computed 中有这个 元素 的所有样式属性

css 属性值的计算过程总览:

  1. 确定声明值
  2. 层叠
  3. 继承
  4. 使用默认值

第一步:确定声明值

这个会从两个表中获取,一个是作者样式表,一个就是上面提到的 浏览器默认样式表,其英文名为 user agent stylesheet,这里直译起来是用户代理样式表不方便理解。两个样式表的样式都是声明值

早期其实还有个 user stylesheet 用户样式表,这个样式表在早期的 ie 浏览器存在,在 IE 中,可以转到Tool> Internet Options> General Tab> Accessibility button> Accessibility Window> User style sheet section>“使用我的样式表格式化文档”复选框。其实就是用户在浏览器夹杂一些自己写的样式:参考stackoverflow css - 默认、用户和作者样式表有什么区别?_Stack Overflow中文网

作者样式表 就是我们写的 css 属性,若我们引入了 ui 库,这些 ui 组件的 样式也是 作者样式表

确定声明值 也有步骤:

  1. 找到 没有冲突 的样式,直接作为计算后的样式
  2. 将相对单位转成绝对单位

比如我给 h1 加一个 color 属性,color 在浏览器默认样式表中不存在,因此没有冲突,color 就会作为计算后的样式

浏览器默认样式表中的 h1 有一个 font-size: 2em; em 就是相对单位,它相对的是父容器的 字体大小,我这儿没有给父容器,因此相对的是浏览器的默认字体大小 16px,因此这里 h1 最终 computed 就是 32px 的 font-size。所以说假设我们给 h1 一个 父容器并且设置 font-size 15px,那么最终 h1 的font-size 就是 30px

相对单位不仅仅说的是 em,%,rem 这种,还包括了 font-weight: bold,color: red 等这种关键字,bold 就是对应 字重 700, red 就是对应 rgb(255, 0, 0)

第二步:层叠

层叠的目的就是一件事:解决冲突,这个过程才是最重要的

层叠有三个步骤:

  1. 比较重要性
  2. 比较特异性
  3. 比较源次序

先看重要性,重要性从高到低:

  1. 带有 important 的作者样式表
  2. 带有 important 的默认样式表
  3. 作者样式表
  4. 默认样式表

由于早期的 user stylesheet 已经不存在了,这里不做探讨

作者样式表 会覆盖 默认样式表 这很好说明

2.png 这里我在代码中写了 h1 标签的样式,默认样式表中的 font-size 就被覆盖了

这里我想要验证 带有 important 的默认样式表 会大于 作者样式表,但是始终没能成功,input 有个 属性 writing-mode,默认样式就有个 important 值,但是我写了不带 important 的 input 的 writing-mode 也能覆盖,这里有大佬清楚可以评论区补充下🚀。

第二步:比较特异性

特异性英文单词就是 specificity ,一般我们会说成权重

内联样式 ID选择器 类选择器、属性选择器、伪类 元素选择器、伪元素 通配符、关系选择器
权重:1000 权重:100 权重:10 权重:1 权重:0
style="color: red" #header .container
[type="text"]
:hover
div
::before
*
>
+

vscode 上 hover 样式选择器时会呈现最终的权重,不过由于不是写的内联样式,他只会给你呈现 后面三个值。

3.png

另外,mdn 上关于 css 权重有个 很形象的 图,这里可以看到 important 的权重是 10000

4.png

因此直观上感受,选择器内容越多,权重就会比较高

特殊伪类的权重计算

特殊伪类这里说的是 :is() 、:not() 、:where()

:is() 和 :not() 二者均是采用 参数中 最高特异性选择器的权重

img

如图这个例子,选取 item 中 值最高的 #id 作为最终权重,not 同理

:where() 权重始终为 0,无论参数中选择器的权重如何

img

where 的功能与 is 几乎相同,允许指定多个选择器,但是 where 权重始终为 0,这就是为了方便后续覆盖

为何要少用 !important

!important 权重是 10000,我们用它一时爽,后面维护起来就要骂娘了,主要是因为用 !important 会带来一些问题:

  1. 破坏 css 级联原则

    css 全称 就是 Cascading Style Sheets 层叠样式表,层叠一词就能直接体现 选择器 优先级的重要性,当我们对某个样式使用了 !important 时,它就会绕过这个机制,这样样式覆盖我们就无法预测

  2. 难以调试

    当多个 !important 冲突时,这个时候最终生效的样式只能是最后加载的样式,而非选择器权重,这样调试起来效率就很低

第三步:比较源次序

若前面的重要性一样,特殊性一样,那么最终就是源次序靠后的胜出

比如我在同一个选择器中对同一个 css 属性写了多次,那么最终只取最后一个值

div h1.className {
    font-size: 10px;
    font-size: 20px;
}

这里最终就是 20px 的 font-size

这个例子过于简单了,这里我再举一个隐形一点的🌰

比如这里我写一个样式

.parent {
    width: 400px;
    height: 400px;
    padding: 20px;
    border: 5px solid;
    background-clip: content-box;
    background: red;
}

background-clip 属性设置元素的背景是否延伸到边框、内边距盒子,内容盒子下面,content-box 就不会让背景色延伸到 padding,实际上这里最终效果全部覆盖了

5.png

这其实就是因为 background-clip 被覆盖了,background 属性展开可以看到里面具有 background-clip 属性,且用的是默认值 initial,在源次序中 initial 取胜

6.png

因此这里我们可以将 background-clip 属性放最后写,或者也可以直接写 background: red content-box

第三步:继承

第二步骤结束后,很多属性是没有值的,若这些属性 默认可以继承 ,则使用继承,

比如 font-size 容易继承,但是background-color 默认无法继承,默认无法继承可以用 inherit 作为 value 去手动继承

.child {
background: inherit;
}

第四步:使用默认值

若继承过后还有属性没有值,那么就会使用 css 属性的默认值,这就是 user agent stylesheet,比如 position 的默认值 为static

最后

了解这部分内容应该会对你 开发 css 调试上更容易些,但大概也不会让你写出更漂亮的界面,最终该写成啥样的还是啥样🥲

文章中若出现错误内容还请各位大佬见谅并指正。如果有任何问题或建议,欢迎指出,另外,有不懂之处欢迎在评论区留言。如果觉得文章对你的学习有所帮助,还请 关注、点赞、收藏 一键三连,感谢支持!欢迎关注我的公众号: Dolphin_Fung

TS 泛型的基本使用

泛型是 TypeScript 类型系统中一个非常强大和核心的特性,它允许我们编写可重用灵活类型安全的组件(如函数、类、接口、类型别名)。

一、 什么是泛型?为什么需要它?

想象一下,你想编写一个函数,它接收一个参数并返回该参数。

1. 不用泛型的问题:
  • 使用 any 类型:

    function identityAny(arg: any): any {
      return arg;
    }
    let outputAny = identityAny("myString"); // outputAny 的类型是 any
    // 问题:丢失了类型信息,编译器无法提供类型检查和智能提示
    // console.log(outputAny.toFixed()); // 运行时会报错,但编译时不会提示!
        
    

    使用 any 会丢失类型信息,降低了类型安全性。

  • 为每种类型编写特定函数:

    function identityString(arg: string): string { return arg; }
    function identityNumber(arg: number): number { return arg; }
    // ...等等
        
    

    这会导致大量重复代码,难以维护。

2. 泛型的解决方案:

泛型允许我们编写一个“模板”函数(或类、接口),其中的类型是参数化的。你可以把它想象成给类型设置了一个占位符变量,在使用时再指定具体的类型(或者让 TypeScript 自动推断)。

// 定义一个泛型函数 identity
// <T> 声明了一个类型变量 T (Type)
// arg: T 表示参数 arg 的类型是 T
// : T 表示函数返回值的类型也是 T
function identity<T>(arg: T): T {
  return arg;
}

// 如何使用?
// 1. 显式指定类型:
let outputString = identity<string>("myString"); // T 被指定为 string,outputString 类型是 string
let outputNumber = identity<number>(123);       // T 被指定为 number,outputNumber 类型是 number

// 2. 利用类型推断 (更常见):编译器会根据传入的参数自动推断 T 的类型
let outputBool = identity(true); // 传入 boolean,T 被推断为 boolean,outputBool 类型是 boolean
let outputObj = identity({ name: "Alice" }); // T 被推断为 { name: string }

// 类型安全!
// console.log(outputString.toFixed()); // Error: Property 'toFixed' does not exist on type 'string'.
console.log(outputNumber.toFixed(2)); // OK
    

泛型的核心优势:在保证代码可重用性的同时,维持了严格的类型约束和类型安全。

二、 泛型的基本语法和应用

泛型使用尖括号 <> 来声明类型参数 (Type Parameters) ,通常使用单个大写字母(如 T, U, K, V 等)作为类型参数的名称,但这只是约定,你可以使用任何合法的标识符。

1. 泛型函数 (Generic Functions)

如上例所示,类型参数列表放在函数名之后、参数列表之前。

 // 接收一个数组,返回第一个元素
function getFirstElement<T>(arr: T[]): T | undefined {
  return arr.length > 0 ? arr[0] : undefined;
}

let firstNum = getFirstElement([1, 2, 3]);        // T 推断为 number, firstNum 类型是 number | undefined
let firstStr = getFirstElement(["a", "b", "c"]);    // T 推断为 string, firstStr 类型是 string | undefined
let firstEmpty = getFirstElement<boolean>([]); // 显式指定 T 为 boolean, firstEmpty 类型是 boolean | undefined

// 接收两个不同类型的参数
function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}
let myPair = pair("hello", 123); // T 推断为 string, U 推断为 number, myPair 类型是 [string, number]
    
2. 泛型接口 (Generic Interfaces)

接口也可以是泛型的,类型参数用于接口内部的成员类型。

// 定义一个泛型接口,表示键值对
interface KeyValuePair<K, V> {
  key: K;
  value: V;
}

let record1: KeyValuePair<string, number> = { key: "age", value: 30 };
let record2: KeyValuePair<number, boolean> = { key: 1, value: true };

// 定义一个泛型接口,表示 API 响应
interface ApiResponse<TData> {
  success: boolean;
  data: TData;
  message?: string;
}

let userResponse: ApiResponse<{ id: number; name: string }> = {
  success: true,
  data: { id: 1, name: "Alice" }
};

let productResponse: ApiResponse<string[]> = {
  success: false,
  data: [],
  message: "Products not found"
};
    
3. 泛型类 (Generic Classes)

类也可以使用泛型,类型参数在类名之后声明。泛型参数不能用于类的静态成员。

class DataStore<T> {
  private data: T[] = [];

  add(item: T): void {
    this.data.push(item);
  }

  getAll(): T[] {
    return [...this.data]; // 返回副本以防外部修改
  }

  // static method cannot use the class type parameter T directly
  // static staticMethod(item: T) {} // Error
}

// 创建一个存储数字的 DataStore 实例
let numberStore = new DataStore<number>();
numberStore.add(1);
numberStore.add(2);
console.log(numberStore.getAll()); // [1, 2]
// numberStore.add("hello"); // Error: Argument of type 'string' is not assignable to parameter of type 'number'.

// 创建一个存储字符串的 DataStore 实例 (类型推断)
let stringStore = new DataStore<string>();
stringStore.add("apple");
stringStore.add("banana");
console.log(stringStore.getAll()); // ["apple", "banana"]
    
4. 泛型类型别名 (Generic Type Aliases)

类型别名也可以是泛型的。

// 定义一个可能为 null 或 undefined 的泛型类型
type Nullable<T> = T | null | undefined;

let nullableString: Nullable<string>;
nullableString = "hello";
nullableString = null;
nullableString = undefined;
// nullableString = 123; // Error

// 定义一个包含值的容器的泛型类型
type Container<T> = { value: T };

let stringContainer: Container<string> = { value: "container content" };
let numberContainer: Container<number> = { value: 42 };
    

三、 泛型约束 (Generic Constraints)

有时,我们希望泛型函数或类只能处理具有特定结构或能力的类型。例如,你想写一个函数,它接收一个参数并打印其 .length 属性,那么这个参数必须得有 length 属性(比如 string 或 array)。这时就需要泛型约束

使用 extends 关键字来约束类型参数必须符合某个接口或具有某些属性。

// 定义一个接口,表示有 length 属性
interface Lengthwise {
  length: number;
}

// 约束 T 必须符合 Lengthwise 接口(即必须有 number 类型的 length 属性)
function logLength<T extends Lengthwise>(arg: T): void {
  console.log(arg.length);
}

logLength("hello world"); // OK, string 有 length 属性
logLength([1, 2, 3]);   // OK, array 有 length 属性
// logLength(123);       // Error: Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.
// logLength({ name: "test" }); // Error: Argument of type '{ name: string; }' is not assignable to parameter of type 'Lengthwise'. Property 'length' is missing.

// 约束 T 必须是某个类的子类或实现了某个接口
interface Printable { print(): void; }
class Document implements Printable { print() { console.log("Printing document..."); } }
class Image implements Printable { print() { console.log("Printing image..."); } }

function printItem<T extends Printable>(item: T): void {
  item.print(); // 安全调用 print 方法,因为 T 被约束了
}

printItem(new Document());
printItem(new Image());
// class Report {}
// printItem(new Report()); // Error: Argument of type 'Report' is not assignable to parameter of type 'Printable'. Type 'Report' is missing the following properties from type 'Printable': print
    

四、 在泛型约束中使用类型参数

一个常见的模式是,一个类型参数受另一个类型参数的约束。例如,你想写一个函数来获取对象上某个属性的值,你需要确保这个属性名确实存在于该对象上。

使用 keyof 操作符:keyof T 会产生一个由 T 类型的所有公共属性名组成的联合类型

// K 必须是 T 对象上的一个键
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  // T[K] 是索引访问类型 (Indexed Access Type),表示 obj 对象上 key 属性的值的类型
  return obj[key];
}

let car = { make: "Toyota", model: "Camry", year: 2023 };

let make = getProperty(car, "make");   // T = typeof car, K = "make", 返回 string
let year = getProperty(car, "year");   // T = typeof car, K = "year", 返回 number
// let color = getProperty(car, "color"); // Error: Argument of type '"color"' is not assignable to parameter of type '"make" | "model" | "year"'.
    

五、 在泛型中使用类类型

有时你想引用类的构造函数类型。

// ctor 的类型是:一个构造函数,它没有参数,并且返回一个类型为 T 的实例
function create<T>(ctor: new () => T): T {
  return new ctor();
}

class BeeKeeper { honey: boolean = true; }
class ZooKeeper { tigers: number = 2; }

let beeKeeper = create(BeeKeeper); // T 推断为 BeeKeeper
let zooKeeper = create(ZooKeeper); // T 推断为 ZooKeeper
console.log(beeKeeper.honey); // true
console.log(zooKeeper.tigers); // 2
    

六、 泛型参数默认值

可以为泛型参数指定默认类型。当用户没有指定类型(或编译器无法推断)时,将使用默认类型。

interface RequestOptions<T = any> { // 默认类型为 any (谨慎使用)
  method: 'GET' | 'POST';
  headers?: Record<string, string>;
  body?: T;
}

let getReq: RequestOptions = { method: 'GET' }; // T 使用默认的 any
let postReq: RequestOptions<FormData> = { method: 'POST', body: new FormData() }; // T 指定为 FormData
    

总结:

泛型是 TypeScript 中实现代码复用和类型安全的关键工具。通过使用类型参数 ,你可以创建适用于多种类型的函数、类、接口和类型别名。泛型约束 (extends) 允许你对这些类型参数施加限制,确保它们具有必要的结构或能力。掌握泛型对于编写高质量、可维护的 TypeScript 代码至关重要。

TS interface 和 type 的使用方法

核心共同点:定义对象/结构的形状

type 和 interface 最常见的用途都是用来描述一个对象应该包含哪些属性、方法以及它们的类型。

// 使用 interface 定义
interface PointInterface {
  x: number;
  y: number;
}

// 使用 type 定义
type PointType = {
  x: number;
  y: number;
};

// 使用它们来注解变量
let p1: PointInterface = { x: 10, y: 20 };
let p2: PointType = { x: 30, y: 40 };

// 它们都可以描述可选属性、只读属性和方法
interface UserInterface {
  readonly id: number;
  name: string;
  email?: string; // 可选
  greet(): string;
}

type UserType = {
  readonly id: number;
  name: string;
  email?: string; // 可选
  greet: () => string; // 方法的类型定义方式略有不同,但效果一样
};
    

interface (接口)详解

  1. 主要用途: 定义对象、类(通过 implements)的结构契约。

  2. 语法: 使用 interface 关键字。

    interface InterfaceName {
        propertyName: Type;
        optionalProperty?: Type;
        readonly readonlyProperty: Type;
        methodName(param: Type): ReturnType;
    }
        
    
  3. 特点:

    • 擅长描述对象形状: 这是它的核心强项。

    • 可继承 (extends) : 一个接口可以继承一个或多个其他接口,合并它们的成员。

      interface Animal { name: string; }
      interface Dog extends Animal { breed: string; } // Dog 继承 Animal
      
      let myDog: Dog = { name: "Buddy", breed: "Golden" };
          
      
    • 可被类实现 (implements) : 类可以使用 implements 关键字来确保它符合某个接口定义的契约。

      interface ClockInterface { currentTime: Date; }
      class DigitalClock implements ClockInterface {
          currentTime: Date = new Date();
      }
          
      
    • 声明合并 (Declaration Merging) : 这是 interface 最独特的特性之一。如果你在同一个作用域内定义了多个同名的 interface,它们会自动合并成一个单一的接口。这对于扩展已有的接口(甚至是第三方库的接口)非常有用。

      // 第一次声明
      interface Box {
          height: number;
          width: number;
      }
      // 第二次声明 (在同一作用域或通过模块扩展)
      interface Box {
          scale: number;
          // height: string; // Error: Subsequent property declarations must have the same type.
      }
      // 合并后的 Box 接口等同于:
      // interface Box {
      //   height: number;
      //   width: number;
      //   scale: number;
      // }
      let box: Box = { height: 5, width: 6, scale: 10 };
          
      

type (类型别名)详解

  1. 主要用途: 给任何类型起一个新名字(别名)。

  2. 语法: 使用 type 关键字和 = 赋值。

          type AliasName = ExistingType;
        
    
  3. 特点:

    • 极其灵活: type 可以为任何类型创建别名,包括:

      • 原始类型: type MyString = string;
      • 联合类型: type ID = string | number;
      • 交叉类型: type PointAndColor = PointType & { color: string };
      • 元组类型: type PointTuple = [number, number];
      • 函数类型: type MathFunc = (x: number, y: number) => number;
      • 对象字面量类型 (如上面 PointType 示例)
      • 甚至其他类型别名: type UserID = ID;
    • 常用于联合和交叉类型: type 在定义联合类型 (|) 和交叉类型 (&) 时比 interface 更直接方便。

      type StringOrNumber = string | number;
      type ClickableAndFocusable = Clickable & Focusable; // Clickable, Focusable 可能是 interface 或 type
          
      
    • 可用于映射类型和条件类型: TypeScript 的高级类型工具(如 Partial, Readonly, Pick<T, K>, 条件类型 T extends U ? X : Y 等)通常与 type 一起使用来创建复杂的新类型。

      type PartialPoint = Partial<PointType>; // { x?: number; y?: number; }
      type StringKeys<T> = { [K in keyof T]: T[K] extends string ? K : never }[keyof T];
          
      
    • 不能声明合并: 如果你尝试定义同名的 type,TypeScript 会抛出错误,它不会像 interface 那样合并。

      type BoxType = { height: number; };
      // type BoxType = { width: number; }; // Error: Duplicate identifier 'BoxType'.
          
      

核心区别总结

特性 interface (接口) type (类型别名)
主要用途 定义对象/类的结构契约 任何类型起别名
适用范围 主要用于对象形状、类实现 原始类型、联合、交叉、元组、对象、函数等所有类型
声明合并 支持 (多个同名接口会自动合并) 不支持 (同名 type 会报错)
继承/扩展 使用 extends 关键字 (可继承多个) 使用交叉类型 & 来组合对象形状
类实现 使用 implements 关键字直接实现 类可以实现 type 定义的对象形状,但 implements 关联性不如 interface 强
定义复杂类型 相对受限 非常灵活,常用于联合、交叉、映射、条件类型等

何时使用 interface vs 何时使用 type?(实践建议)

这是一个在 TypeScript 社区中有些争议但也有普遍共识的话题:

  1. 定义公共 API 的对象形状 (例如库的类型定义) :

    • 推荐使用 interface。主要原因是 interface 的声明合并特性允许库的使用者在需要时安全地扩展接口(例如,给全局 Window 接口添加属性)。
  2. 定义应用程序内部的对象形状:

    • 两者皆可。选择哪个更多是团队风格和一致性的问题。
    • 如果你预期未来可能需要合并声明,或者更倾向于面向对象的 extends 和 implements 语法,可以选择 interface。
    • 如果你更喜欢 type 的灵活性,或者需要定义联合、交叉等非对象形状,或者想明确表示“这个类型定义不应该被外部合并”,可以选择 type。
  3. 定义联合类型、交叉类型、元组、函数类型、原始类型别名:

    • 必须使用 type。interface 无法做到这些。
  4. 使用 TypeScript 高级类型工具 (映射类型、条件类型等) :

    • 通常使用 type。这些高级类型操作的结果往往需要用 type 来命名。
  5. 保持一致性:

    • 在一个项目或团队内部,对于定义对象形状,尽量保持一致。如果大部分地方用了 interface,就继续用 interface (除非需要 type 的特定功能);反之亦然。

总结:

  • interface 专注于描述对象和类的“契约”,并拥有独特的“声明合并”能力。
  • type 是一个更通用的“类型别名”工具,适用于所有类型,尤其擅长处理联合、交叉和利用高级类型工具。

两者在定义对象形状方面有很多重叠,选择哪个往往取决于具体需求(是否需要合并?是否需要定义非对象类型?)以及团队的编码规范。理解它们的核心区别(尤其是声明合并和适用范围)是做出正确选择的关键。

TS 函数在ts中的基本使用

核心目的: 函数类型用来描述一个函数应该接受什么类型的参数以及返回什么类型的值。

最常用的方式:使用类型别名 (type)

这是定义可复用函数类型最常见且推荐的方式。

  1. 定义函数类型别名:
    使用 type 关键字,后跟类型名称,等号右边是函数的签名:(参数列表) => 返回值类型。

    // 定义一个类型别名 AddFunction
    // 它描述了一个函数:接受两个 number 类型的参数,返回一个 number 类型的值
    type AddFunction = (a: number, b: number) => number;
    
    // 定义一个类型别名 GreetFunction
    // 它描述了一个函数:接受一个 string 类型的参数,没有返回值 (void)
    type GreetFunction = (message: string) => void;
        
    
  2. 使用类型别名注解变量:
    声明一个变量,并使用你定义的类型别名来注解它。这表示该变量未来只能被赋值为符合该类型的函数。

    let myAdd: AddFunction;
    let myGreet: GreetFunction;
        
    
  3. 实现符合该类型的函数并赋值:
    创建函数(通常使用函数表达式或箭头函数),确保其参数和返回值类型与类型别名匹配,然后将其赋值给注解过的变量。

    // 实现 myAdd
    myAdd = (x, y) => { // 参数名 x, y 不需要和类型别名中的 a, b 一样,但类型必须匹配
      return x + y;
    };
    
    // 实现 myGreet
    myGreet = (msg) => {
      console.log(msg);
    };
    
    // 错误示例:类型不匹配会导致编译错误
    // myAdd = (s1: string, s2: string) => s1 + s2; // 错误!参数类型和返回值类型都不匹配 AddFunction
    // myGreet = (num: number) => console.log(num); // 错误!参数类型不匹配 GreetFunction
        
    
  4. 调用函数:
    像普通函数一样调用这些变量。

    let sum: number = myAdd(10, 5); // sum 的类型是 number
    console.log(sum); // 输出: 15
    
    myGreet("Hello TypeScript!"); // 输出: Hello TypeScript!
        
    

另一种方式:内联类型注解

如果你只是临时用一次函数类型,不想创建可复用的类型别名,可以直接在变量声明或函数参数中写明类型。

// 直接在变量声明上注解函数类型
let multiply: (x: number, y: number) => number;

multiply = (a, b) => {
  return a * b;
};

console.log(multiply(4, 5)); // 输出: 20

// 在函数参数中注解回调函数类型(非常常见!)
function processArray(arr: number[], callback: (element: number, index: number) => void): void {
  for (let i = 0; i < arr.length; i++) {
    // 调用回调函数时,TypeScript 会检查传入的参数是否符合 callback 的类型要求
    callback(arr[i], i);
  }
}

// 调用 processArray,并传入一个符合回调类型的箭头函数
processArray([1, 2, 3], (num, idx) => {
  // 在这个回调函数内部,TypeScript 知道 num 是 number, idx 是 number
  console.log(`Index: ${idx}, Value: ${num}`);
});
    

基本使用总结:

  1. 定义形状: 使用 type FuncType = (param1: Type1, ...) => ReturnType; 来定义一个可复用的函数类型。
  2. 应用类型: 使用 let myFunc: FuncType; 来声明一个变量,它必须持有符合 FuncType 的函数。
  3. 实现函数: 创建一个函数(通常是箭头函数或函数表达式),确保参数和返回值类型与定义的类型匹配,并赋值给变量。
  4. 直接注解: 对于一次性使用,可以直接在变量或参数上写 (param1: Type1, ...) => ReturnType;。

TS 对象 和 object的基本使用

1. object 类型 (小写 o)

  • 含义: object 类型代表任何非原始类型 (non-primitive type) 的值。 在项目中一般不用object来类型注解,没有太多作用,还不如类型自动推导 。且object可以表示能用对象创建的函数,数组等。

自动类型推导

Snipaste_2025-04-18_22-56-40.png

使用object

Snipaste_2025-04-18_22-57-53.png

  • 哪些属于 object 类型:

    • 普通对象字面量 ({})
    • 数组 ([])
    • 函数 (() => {})
    • 类的实例 (new Date(), new RegExp(), 自定义类的实例等)
  • 哪些不属于 object 类型 (原始类型) :

    • string
    • number
    • boolean
    • symbol
    • bigint
    • null
    • undefined
  • 用途和限制:

    • 它的主要用途是约束一个值不能是原始类型
    • 它本身非常宽泛,你不能安全地访问一个被注解为 object 类型的变量的任何属性,因为 TypeScript 不知道这个对象的具体“形状”。
let obj: object;

obj = { name: "Alice" }; // OK
obj = [1, 2, 3];         // OK
obj = () => "hello";     // OK
obj = new Date();        // OK

// obj = "hello";       // Error: Type 'string' is not assignable to type 'object'.
// obj = 42;            // Error: Type 'number' is not assignable to type 'object'.
// obj = true;          // Error: Type 'boolean' is not assignable to type 'object'.
// obj = null;          // Error: Type 'null' is not assignable to type 'object'. (需要 strictNullChecks: true)
// obj = undefined;     // Error: Type 'undefined' is not assignable to type 'object'. (需要 strictNullChecks: true)

// 访问属性是不安全的,因为不知道具体结构
// console.log(obj.name); // Error: Property 'name' does not exist on type 'object'.
    

结论: object 类型本身在实践中用途有限,因为它无法描述对象的具体结构。通常我们需要更精确的类型。

2. 更具体的对象类型定义方式

当我们需要描述一个对象应该包含哪些属性以及这些属性的类型时,我们通常使用以下方式:

  • 对象字面量类型 (Object Literal Types / Inline Types) : 直接在类型注解中描述对象的结构。

    let person: { name: string; age: number; isActive?: boolean }; // ? 表示可选属性
    
    person = { name: "Bob", age: 30 }; // OK
    person = { name: "Charlie", age: 25, isActive: true }; // OK
    // person = { name: "David" }; // Error: Property 'age' is missing...
    // person = { name: "Eve", age: 40, location: "City" }; // Error: Object literal may only specify known properties... (赋值时有额外属性检查)
    
    function greet(user: { name: string; email: string }): void {
      console.log(`Hello ${user.name}, contact: ${user.email}`);
    }
    greet({ name: "Frank", email: "frank@example.com" });
        
    

    这种方式适用于结构简单、不需要复用的场景。

  • 接口 (Interfaces) : 使用 interface 关键字定义一个命名的对象结构契约。这是定义对象形状最常用、最推荐的方式之一。

    interface Point {
      readonly x: number; // readonly 表示只读
      y: number;
    }
    
    interface UserProfile {
      userId: number | string;
      username: string;
      email?: string; // 可选
      logins: Date[];
      greet(): string; // 方法签名
    }
    
    let p1: Point = { x: 10, y: 20 };
    // p1.x = 5; // Error: Cannot assign to 'x' because it is a read-only property.
    
    let user1: UserProfile = {
      userId: 123,
      username: "Grace",
      logins: [new Date()],
      greet() { return `Hi, I am ${this.username}`; }
    };
        
    

    优点: 可复用、可读性强、支持继承 (extends)、可被类实现 (implements)、支持声明合并。

  • 类型别名 (Type Aliases) : 使用 type 关键字给一个类型(包括对象类型)起一个新名字。也非常常用。

    type Coordinates = {
      lat: number;
      lon: number;
    };
    
    type ID = string | number;
    
    type PersonData = {
      id: ID;
      name: string;
      coords?: Coordinates; // 可以嵌套使用类型别名
    };
    
    let loc: Coordinates = { lat: 34.05, lon: -118.24 };
    let personData: PersonData = { id: "p-456", name: "Henry", coords: loc };
        
    

    优点: 可复用、可读性强,不仅能定义对象形状,还能定义联合类型、交叉类型、元组等任何类型。缺点: 不能像接口那样进行声明合并或被 implements (虽然类可以实现由类型别名定义的对象结构)。

  • 类 (Classes) : 类本身定义了一个类型,即该类的实例类型。

    class Car {
      make: string;
      model: string;
      year: number;
    
      constructor(make: string, model: string, year: number) {
        this.make = make;
        this.model = model;
        this.year = year;
      }
    
      displayInfo(): string {
        return `${this.year} ${this.make} ${this.model}`;
      }
    }
    
    let myCar: Car = new Car("Toyota", "Camry", 2023);
    console.log(myCar.displayInfo());
        
    

3. 容易混淆的概念:object vs Object vs {}

  • object (小写) : 如上所述,是 TypeScript 的类型,代表非原始值。

  • Object (大写) :

    • 是 JavaScript 全局内置的 Object 构造函数(一个值)。
    • 也是一个类型,表示 JavaScript 的 Object 构造函数创建的实例的类型。它包含所有对象共享的方法,如 toString(), hasOwnProperty() 等(这些方法实际在 Object.prototype 上)。
    • 通常不推荐使用 Object 作为类型注解(let obj: Object),因为它过于宽泛,几乎允许任何值(除了 null 和 undefined,在 strictNullChecks 下),并且无法提供有用的结构信息。你应该使用更具体的接口、类型别名,或者在需要表示“任何非原始值”时使用 object。
  • {} (空对象类型) :

    • 表示一个没有任何自身属性的对象类型。
    • 由于 TypeScript 的结构化类型系统,几乎所有非 null 和非 undefined 的值都可以赋值给 {} 类型的变量,因为它们满足“不缺少 {} 所需的任何属性”(即没有必需属性)这一条件。
    • 因此,{} 不是一个用来表示“任意对象”的好类型,因为它允许原始类型(除了 null/undefined),并且没有提供任何约束。object 类型是更好的选择来表示“任意非原始值”。Record<string, unknown> 或 Record<string, any> (谨慎使用 any) 更适合表示“任意有属性的对象”(字典类型)。
let valObject: Object = { a: 1 }; // 不推荐
valObject = "string"; // OK,因为 string 也有 toString 等方法
valObject = 42;       // OK
// valObject = null;      // Error (strictNullChecks)
// valObject = undefined; // Error (strictNullChecks)

let valEmpty: {} = { b: 2 }; // OK
valEmpty = "string"; // OK
valEmpty = 42;       // OK
valEmpty = () => {}; // OK
// valEmpty = null;      // Error (strictNullChecks)
// valEmpty = undefined; // Error (strictNullChecks)
// console.log(valEmpty.b); // Error: Property 'b' does not exist on type '{}'.

let valObjLowercase: object = { c: 3 }; // 更推荐表示非原始值
// valObjLowercase = "string"; // Error: Type 'string' is not assignable to type 'object'.
// valObjLowercase = 42;       // Error: Type 'number' is not assignable to type 'object'.
// console.log(valObjLowercase.c); // Error: Property 'c' does not exist on type 'object'.
    

总结与推荐:

  • 需要表示任意非原始值时,使用 object。
  • 需要表示具体结构的对象时,优先使用 interface 或 type (类型别名)。
  • 需要表示类的实例时,直接使用类名作为类型。
  • 需要表示“任意有属性的对象”或“字典”时,考虑 Record<string, unknown> (更安全) 或 Record<string, any> (如果确实需要 any 的灵活性)。
  • 避免使用 Object (大写) 和 {} 作为通用对象类型注解。

正则表达式性能翻倍秘籍:y修饰符精准锁定匹配位置

正则表达式的隐秘利器:y 修饰符(粘性匹配)

正则表达式是处理文本的瑞士军刀,而 ES6 引入的 y(sticky)修饰符 则是一把被低估的利器。它不像 g(全局匹配)或 i(忽略大小写)那样广为人知,但在特定场景下能发挥奇效。本文将深入解析 y 修饰符的工作原理、应用场景及实战技巧。


一、y 修饰符是什么?

y 是“sticky”(粘性)的缩写,它要求正则表达式必须从目标字符串的 lastIndex 位置开始匹配,且匹配失败后不会尝试后续位置。这与 g 修饰符的“贪婪搜索”行为截然不同。

示例对比:
const str = "aaa_aaa_aaa";
const regexG = /a+/g;
const regexY = /a+/y;

// 第一次匹配
regexG.lastIndex = 1; 
console.log(regexG.exec(str)); // ["aa"](从索引1开始匹配,跳过第一个a)

regexY.lastIndex = 1;
console.log(regexY.exec(str)); // null(必须从索引1的字符开始,但此处是 a,可以匹配?这里可能有矛盾,需要修正)

修正说明
上面的例子中,regexYlastIndex 设置为1,字符串在位置1的字符仍是 a,所以 /a+/y 应该匹配到 "aa"(从索引1开始的连续a)。可能原示例意图展示的是当 lastIndex 不在匹配起点时的情况,比如:

const str = "_aaa_aaa";
const regexY = /a+/y;
regexY.lastIndex = 0;
console.log(regexY.exec(str)); // null(索引0是 _,无法匹配)

二、核心特性:粘性匹配

  1. 严格的位置约束
    匹配必须从 lastIndex 开始,否则立即失败。适合需要连续匹配的场景。

  2. 不自动回溯
    若匹配失败,lastIndex 重置为 0(除非手动修改)。

  3. g 修饰符的兼容性
    yg 可共存(如 /pattern/gy),但 y 的粘性规则优先。


三、g修饰符和y修饰符的对比

以下是 yg 修饰符对比图表,包含核心行为差异匹配流程的详细说明:

---
title: 正则表达式修饰符对比:y(粘性) vs. g(全局)
---
flowchart TB
    subgraph g修饰符["全局匹配(g修饰符)"]
        gStart[匹配开始] --> gCheckPos{"当前位置\n(lastIndex) 是否匹配?"}
        gCheckPos -->|是| gMatchSuccess[匹配成功\n更新 lastIndex 到匹配结束位置]
        gCheckPos -->|否| gSkip[跳过当前字符\nlastIndex++]
        gSkip --> gCheckEnd{"是否到达\n字符串末尾?"}
        gCheckEnd -->|否| gCheckPos
        gCheckEnd -->|是| gReset[重置 lastIndex=0\n返回 null]
    end

    subgraph y修饰符["粘性匹配(y修饰符)"]
        yStart[匹配开始] --> yCheckPos{"严格检查\nlastIndex 位置是否匹配?"}
        yCheckPos -->|是| yMatchSuccess[匹配成功\n更新 lastIndex 到匹配结束位置]
        yCheckPos -->|否| yFail[立即失败\n重置 lastIndex=0\n返回 null]
    end

    classDef green fill:#d4f7d4,stroke:#2c662d;
    classDef orange fill:#ffe6cc,stroke:#804000;
    class g修饰符 orange
    class y修饰符 green

关键行为对比说明
特征 g 修饰符 y 修饰符
匹配起点 可从任意位置开始搜索 必须lastIndex 开始
失败处理 继续向后搜索直到字符串末尾 立即失败,重置 lastIndex 为 0
适用场景 提取所有可能的匹配项 需要连续、精准的位置控制
性能 可能因回溯消耗资源 无回溯,匹配失败立即停止
lastIndex 管理 自动更新,失败时重置 必须手动管理,失败时强制重置为 0

匹配流程示例(字符串 "a1a2a3",模式 /a\d/y/a\d/g
flowchart LR
    subgraph g匹配流程["g修饰符匹配流程"]
        g1["lastIndex=0 → 匹配 'a1' → lastIndex=2"]
        g2["lastIndex=2(字符 'a')→ 匹配 'a2' → lastIndex=4"]
        g3["lastIndex=4(字符 'a')→ 匹配 'a3' → lastIndex=6"]
    end

    subgraph y匹配流程["y修饰符匹配流程"]
        y1["lastIndex=0 → 匹配 'a1' → lastIndex=2"]
        y2["lastIndex=2 → 必须从位置2开始\n(字符 'a')→ 匹配 'a2' → lastIndex=4"]
        y3["lastIndex=4 → 必须从位置4开始\n(字符 'a')→ 匹配 'a3' → lastIndex=6"]
        yFail["若中间任意位置不匹配:\n立即失败并重置 lastIndex=0"]
    end

    classDef blue fill:#e6f3ff,stroke:#0066cc;
    classDef pink fill:#ffe6e6,stroke:#cc0066;
    class g匹配流程 blue
    class y匹配流程 pink

核心
  • g 像“扫描仪”:遍历整个字符串寻找所有匹配,适合批量提取
  • y 像“镊子”:精准锁定位置,适合流式处理/语法解析等需要严格控制的场景。

四、应用场景

1. 流式数据解析

处理分块到达的文本数据时,y 修饰符能确保每次从上一次结束的位置继续匹配,避免重复扫描。

let buffer = "";
let lastIndex = 0;
const regex = /\d+/y;

function processChunk(chunk) {
  buffer += chunk;
  regex.lastIndex = lastIndex;
  let match;
  while ((match = regex.exec(buffer))) {
    console.log("Found:", match[0]);
    lastIndex = regex.lastIndex;
  }
}
2. 语法高亮/模板解析

需要严格按位置提取令牌(tokens)时,y 修饰符能提升准确性和性能。

const tokenRegex = /\s*(\+|\-|\*|\/|\(|\)|\d+)/y;
let source = "10 + (20 - 3)";
let pos = 0;

while (true) {
  tokenRegex.lastIndex = pos;
  const match = tokenRegex.exec(source);
  if (!match) break;
  console.log("Token:", match[1]);
  pos = tokenRegex.lastIndex;
}
3. 性能敏感场景

当明确知道匹配应从特定位置开始时,y 修饰符通过减少无用的回溯提升效率。


四、使用注意事项

  1. 手动管理 lastIndex
    每次匹配后需更新 lastIndex(尤其在循环中)。

  2. 失败时重置 lastIndex
    匹配失败后,lastIndex 自动设为 0,可能需要重新初始化。

  3. 兼容性检查
    支持环境:现代浏览器(ES6+)、Node.js 4+。旧环境可通过 Babel 转译。


五、总结

y 修饰符通过强制位置匹配,为以下场景提供精准控制:

  • 流式文本处理
  • 语法/结构解析
  • 高性能匹配需求

虽然学习曲线较陡,但在特定问题域中,y 修饰符能化繁为简,成为高效解决方案的关键。


进一步阅读

Symbol类型的变量

Symbol 是什么?

Symbol 是 ES6 引入的一种新的数据类型,它的作用是用来生成唯一的标识符。每个 Symbol 都是独一无二的,即使你创建的 Symbol 描述相同,它们也不会相等。

怎么创建 Symbol?

创建 Symbol 很简单,直接调用 Symbol() 函数就行。你可以给它传个描述,主要是为了调试时看得清楚,但这并不会影响它的唯一性。

const sym1 = Symbol('desc');  // 有描述
const sym2 = Symbol('desc');  // 还是不同的实例

Symbol 有什么特点?

  1. 唯一性:每个 Symbol 都是唯一的,不能和其他 Symbol 比较相等。

    const sym1 = Symbol('id');
    const sym2 = Symbol('id');
    console.log(sym1 === sym2);  // false
    
  2. 不可变性:一旦创建,Symbol 就不能修改,没法像字符串那样变来变去。

  3. 不能自动转换成字符串:如果你想把 Symbol 打印出来,必须调用 .toString() 方法。

    const sym = Symbol('hello');
    console.log(sym.toString());  // "Symbol(hello)"
    

Symbol 的用途

  1. 避免属性冲突:在大型项目中,不同模块可能会用到相同的属性名。用 Symbol 作为属性名,就能确保它们唯一,不会被覆盖。

    const uniqueKey = Symbol('unique');
    const obj = {
      [uniqueKey]: 'value'
    };
    console.log(obj[uniqueKey]);  // 'value'
    
  2. 模拟私有属性:JavaScript 对象的属性是公开的,没法像传统 OOP 那样定义私有属性。使用 Symbol,你可以模拟私有属性,这样外部无法直接访问。

    const privateKey = Symbol('private');
    const obj = {
      [privateKey]: 'This is private'
    };
    console.log(obj[privateKey]);  // 'This is private'
    
  3. 实现迭代器:如果你需要自定义对象的迭代行为,可以通过 Symbol.iterator 来实现。

    const myIterable = {
      [Symbol.iterator]: function() {
        let i = 0;
        const data = ['a', 'b', 'c'];
        return {
          next: function() {
            if (i < data.length) {
              return { value: data[i++], done: false };
            } else {
              return { done: true };
            }
          }
        };
      }
    };
    
    for (const item of myIterable) {
      console.log(item);  // a, b, c
    }
    
  4. 全局唯一的标识符:如果你想在整个应用中共享一个唯一标识符,可以使用 Symbol.for(),它会在全局注册这个 Symbol

    const sym1 = Symbol.for('shared');
    const sym2 = Symbol.for('shared');
    console.log(sym1 === sym2);  // true
    

常见方法

  • Symbol.for() :查找全局注册的 Symbol,如果没有找到就创建一个新的。
  • Symbol.keyFor() :获取已经注册的全局 Symbol 的描述。
  • Object.getOwnPropertySymbols() :获取对象的符号属性(仅限对象本身的,不包括继承的)。
  • Reflect.ownKeys() :获取对象的所有键,包括符号键和普通键。

总结

Symbol 的核心作用就是生成独一无二的标识符,它避免了属性名冲突、模拟私有属性、实现特殊功能等多种用途。如果你需要在一个复杂的应用中确保标识符的唯一性,或者想要定义私有属性,Symbol 就是一个很有用的工具。

数据结构与算法(二)

栈是什么?

  • 一个后进先出的受限线性结构
  • JavaScript中没有栈,但可以用 Array 实现栈的所有功能 image.png
  • Array 的 push 和 pop 方法来实现后进先出
const stack = []
// 入栈顺序 1,2
stack.push(1)
stack.push(2)

// 出栈顺序 2,1
stack.pop()
stack.pop()

栈的应用场景有哪些?

  • 十进制转二进制
  • 判断字符串的括号是否有效
  • 函数调用堆栈

数组Array实现栈功能

class Stack {
    constructor() {
        this.items = [];
    }

    // 入栈操作
    push(element) {
        this.items.push(element);
    }

    // 出栈操作
    pop() {
        if (this.isEmpty()) {
            return null;
        }
        return this.items.pop();
    }

    // 查看栈顶元素
    peek() {
        if (this.isEmpty()) {
            return null;
        }
        return this.items[this.items.length - 1];
    }

    // 判断栈是否为空
    isEmpty() {
        return this.items.length === 0;
    }

    // 获取栈的大小
    size() {
        return this.items.length;
    }

    // 清空栈
    clear() {
        this.items = [];
    }
}

// 测试示例
const stack = new Stack();
stack.push(10);
stack.push(20);
stack.push(30);

console.log(stack.peek()); // 输出: 30
console.log(stack.pop());  // 输出: 30
console.log(stack.size()); // 输出: 2
console.log(stack.isEmpty()); // 输出: false
stack.clear();
console.log(stack.isEmpty()); // 输出: true    

基于上面实现的栈,进一步实现十进制转二进制功能

/**
基本思路是不断地将十进制数除以 2,把余数压入栈中,直到商为 0,然后依次弹出栈中的元素,就得到了二进制数。
*/
function decimalToBinary(decimalNumber) {
    const stack = new Stack();
    let number = decimalNumber;
    let binaryString = '';

    while (number > 0) {
        stack.push(number % 2);
        number = Math.floor(number / 2);
    }

    while (!stack.isEmpty()) {
        binaryString += stack.pop();
    }

    return binaryString === '' ? '0' : binaryString;
}

// 测试示例
const decimal = 10;
const binary = decimalToBinary(decimal);
console.log(`十进制数 ${decimal} 转换为二进制是: ${binary}`);

基于上面实现的栈,进一步实现字符串括号匹配功能

/**
基本思路:基本思路是遍历字符串,遇到左括号时将其压入栈中,遇到右括号时从栈中弹出一个左括号进行匹配,
如果匹配成功则继续遍历,若栈为空或者匹配失败则说明括号无效,遍历结束后若栈为空则说明所有括号都有效匹配。
*/
function isValidBrackets(s) {
    const stack = new Stack();
    const mapping = {
        ')': '(',
        ']': '[',
        '}': '{'
    };

    for (let i = 0; i < s.length; i++) {
        const char = s[i];
        if (Object.values(mapping).includes(char)) {
            // 如果是左括号,压入栈中
            stack.push(char);
        } else if (Object.keys(mapping).includes(char)) {
            // 如果是右括号
            const topElement = stack.pop();
            if (topElement!== mapping[char]) {
                return false;
            }
        }
    }

    // 遍历结束后栈为空则括号有效
    return stack.isEmpty();
}

// 测试示例
const str1 = "()[]{}";
const str2 = "([)]";
console.log(`字符串 "${str1}" 的括号是否有效: ${isValidBrackets(str1)}`);
console.log(`字符串 "${str2}" 的括号是否有效: ${isValidBrackets(str2)}`);

全局唯一值老是重复?别再全局搜索了,Vite自定义插件全解决

全局唯一值老是重复?别再全局搜索了,Vite自定义插件全解决

背景

在项目中,我们经常遇到某些属性(settingKey)要项目内全局唯一,老是有同事搞到重复的,手动检查又很麻烦

为此,搞一个Vite自定义插件来自动检查是个不错的选择。

插件实现

/**
 * Vite插件:检查Vue组件中的settingKey属性是否唯一
 */
export default function checkSettingKeyPlugin() {
  const allSettingKeys = new Set<string>();

  return {
    name: "check-setting-key",
    transform(code: string, id: string) {
      if (!id.endsWith(".vue")) return null;

      const regex = /(?:v-bind:)?settingKey=["']([^"']+)["']/g;
      const errors: string[] = [];
      let match: RegExpExecArray | null;

      while ((match = regex.exec(code)) !== null) {
        const key = match[1];
        if (allSettingKeys.has(key)) {
          errors.push(`重复的 settingKey: ${key}`);
        } else {
          allSettingKeys.add(key);
        }
      }

      if (errors.length > 0) {
        const errorMsg = `[${id}] settingKey 必须唯一:\n${errors.join("\n")}`;
        this.error(errorMsg);
        return { code, map: null, errors: [errorMsg] };
      }

      return { code, map: null };
    },
  } as any;
}

使用方式

在vite.config.ts中引入插件:

import checkSettingKeyPlugin from './plugins/checkSettingKey';

export default defineConfig({
  plugins: [
    vue(),
    checkSettingKeyPlugin()
  ]
})

效果

当检测到问题时,控制台会显示详细的错误信息,并终止打包:

[src/components/Example.vue] settingKey 必须唯一:
重复的 settingKey: user_name

总结

通过这个Vite插件,我们实现了:

  1. 自动化检查settingKey规范
  2. 早期发现问题,避免运行时错误

这种插件开发模式可以扩展到其他类似的代码规范检查场景,是提升项目质量的有效手段。

数据结构与算法(一)

什么是数据结构?

数据结构是指计算机存储、组织数据的方式(栈、队列、链表...)

什么是算法?

算法是指解决问题的一系列清晰步骤或指令

数据结构与算法有何关系?

  • 数据结构是为算法提供服务的
  • 算法是围绕着数据结构进行操作
  • 程序 = 数据结构 + 算法

衡量算法好坏的标准是什么?

时间复杂度

  • 时间复杂度是定性描述算法运行时间的大概趋势,用函数表示(大 O 表示法),如O(1)、O(n)、O(log⁡N)等

  • 常见的时间复杂度

    image.png

    O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(2^n) < O(n!)

  • 示例

// O(1)
let i=0;
i++;

// O(logn)
while(i<n) {
    console.log(i)
    i*=2
}

// O(n)
let i = 1;
for(let i=0; i<n; i++) {
    console.log(i)
}

// O(n^2)
for(let i=0; i<n; i++) {
    for(let j=0; j<n; j++) {
        console.log(i,j)
    }
}

空间复杂度

  • 空间复杂度是衡量算法在运行过程中临时占用存储空间的大小,通常也是用大 O 表示,如O(1)、O(n)、O(log⁡N)等
  • 算法占用的存储空间越小,则性能越好
  • 示例
// O(1) -- 占用 1 个内存单元
let i=0;
i++;

// O(n) -- 占用 n 个内存单元
const arr = []
for(let i=0; i<n; i++) {
    arr.push(i)
}

// O(n^2) -- 占用 n * n 个内存单元
const matrix = []
for(let i=0; i<n; i++) {
    matrix.push([])
    for(let j=0; j<n; j++) {
        matrix[i].push(j)
    }
}

Echarts Tooltip 如何编写 ReactNode

项目初始化

最简单的折线图📈

import ReactECharts from "echarts-for-react";
import { FC } from "react";

export const EchartsTooltip: FC = () => {
  return (
    <ReactECharts
      style={{ height: 800, width: '100%' }}
      option={{
        xAxis: {
          type: "category",
          data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
        },
        yAxis: {
          type: "value",
        },
        tooltip: {
          trigger: 'axis'
        },
        series: [
          {
            data: [150, 230, 224, 218, 135, 147, 260],
            type: "line",
          },
        ],
      }}
    />
  );
};

Snipaste_2025-04-18_21-42-21.png

当期望自定义 tooltip 时,Echarts 提供了 tooltip formatter 回调函数。

Echarts tooltip 文档📄

tooltip: {
  trigger: 'axis',
  formatter: (params) => {
    console.log('params', params)
    return params
  }
},

Snipaste_2025-04-18_21-59-34.png

提示框浮层内容格式器,支持字符串模板和回调函数两种形式。

回调函数支持返回 HTML 字符串或者创建的 DOM 实例

(params: Object|Array) => string | HTMLElement | HTMLElement[]

renderToStaticMarkup

链接🔗:renderToStaticMarkup

renderToStaticMarkup 是 React 提供的一个服务端渲染(Server-Side Rendering, SSR)API,属于 react-dom/server 模块。它的主要作用是将 React 元素渲染为静态的 HTML 字符串。

由于输出的是纯 HTML,因此组件中的事件处理器(如 onClick)不会起作用。

基本用法

import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';

const EmailTemplate = () => (
  <html>
    <body>
      <h1>Hello, world!</h1>
      <p>This is an email template.</p>
    </body>
  </html>
);

const html = renderToStaticMarkup(<EmailTemplate />);
console.log(html);

输出结果:

<html><body><h1>Hello, world!</h1><p>This is an email template.</p></body></html>

输出的 HTML 非常干净,没有任何多余的 React 元数据。

图表使用 renderToStaticMarkup

tooltip: {
  trigger: 'axis',
  formatter: (params) => {
    return renderToStaticMarkup(<h1>23456</h1>)
  }
},

Snipaste_2025-04-18_21-57-10.png

通过 renderToStaticMarkup 可以自定义 tooltip。当然,这个时候 tooltip 前方的圆点已经不存在了。我们也无法将 params.marker 渲染出来,这是一个字符串。

tooltip: {
  trigger: "axis",
  formatter: (params) => {
    return renderToStaticMarkup(
      <div>
        <span
          style={{
            display: "inline-block",
            width: 10,
            height: 10,
            borderRadius: 10,
            backgroundColor:
              params?.[0].marker?.match(/#(?:[0-9a-fA-F]{6})/)?.[0],
          }}
        />
        235
      </div>
    );
  },
},

Snipaste_2025-04-18_22-11-38.png

项目代码地址

Gitee 地址🔗

axios和swr的区别

最近自己做项目开始使用react,发现了swr这个库,对比之前自己一直使用的axios,它具有缓存和转台管理的功能,是个不错的工具。

SWR 和 Axios 是两种用于前端数据请求的工具,但它们的定位、功能和使用场景有明显区别。以下是它们的主要区别:

1. 定位和核心功能

  • Axios:
    • 是一个基于 Promise 的 HTTP 客户端库,用于发送 HTTP 请求(GET、POST、PUT、DELETE 等)。
    • 专注于请求的发送和响应的处理,提供强大的配置选项(如拦截器、超时设置、请求取消等)。
    • 是一个通用的网络请求工具,不关心数据管理或缓存。
  • SWR:
    • 是一个 React 数据获取库(React Hooks for Data Fetching),由 Vercel 开发。
    • 核心是用于简化数据获取、缓存和状态管理,基于 React Hooks(如 useSWR)。
    • 提供自动缓存、重新验证(revalidation)、轮询、错误重试等功能,专注于 React 组件的数据获取体验。

2. 使用场景

  • Axios:
    • 适合需要直接发送 HTTP 请求的场景,比如与 REST API 或 GraphQL API 交互。
    • 常用于任何 JavaScript 环境(包括 Node.js、浏览器、React、Vue 等)。
    • 适合需要精细控制请求(如自定义 headers、请求拦截、文件上传等)的场景。
  • SWR:
    • 专为 React 应用设计,适合需要将数据绑定到组件状态的场景。
    • 适合需要自动管理数据状态的场景,比如列表数据、实时数据更新、离线支持等。
    • 强调用户体验,如快速响应(先返回缓存数据,再更新)、自动刷新等。

3. 数据管理与缓存

  • Axios:
    • 不提供内置的数据缓存或状态管理功能。
    • 需要开发者手动处理响应数据、错误处理和缓存逻辑(如使用 localStorage 或其他状态管理库)。
  • SWR:
    • 内置强大的缓存机制,通过 key(如 API 路径)缓存数据。
    • 提供自动重新验证(revalidation)功能,如页面聚焦时、定时轮询、数据失效后自动更新。
    • 支持“先返回缓存数据,再请求更新”的策略(stale-while-revalidate),提升用户体验。

4. 代码示例

  • Axios:
import axios from 'axios';

async function fetchData() {
  try {
    const response = await axios.get('https://api.example.com/data');
    console.log(response.data);
  } catch (error) {
    console.error('Error:', error);
  }
}
  • SWR:
import useSWR from 'swr';

// fetcher 函数通常基于 Axios 或 Fetch API
const fetcher = (url) => axios.get(url).then((res) => res.data);

function MyComponent() {
  const { data, error } = useSWR('https://api.example.com/data', fetcher);

  if (error) return <div>Error loading data</div>;
  if (!data) return <div>Loading...</div>;

  return <div>Data: {JSON.stringify(data)}</div>;
}

5. 依赖与生态

  • Axios:
    • 是一个独立的库,无需特定框架,可以与任何前端框架或原生 JavaScript 一起使用。
    • 需要额外的库(如 Redux、React Query)来实现复杂的数据管理。
  • SWR:
    • 依赖 React 和 React Hooks,仅适用于 React 应用。
    • 与 Next.js 等 Vercel 生态系统深度集成,适合现代 React 项目。
    • 内置了许多开箱即用的功能,减少对其他状态管理库的依赖。

6. 优缺点

  • Axios:
    • 优点
      • 功能强大,支持复杂的请求配置。
      • 跨平台,适用于任何 JavaScript 环境。
      • 社区广泛,文档丰富。
    • 缺点
      • 需要手动处理数据缓存、状态管理和错误重试。
      • 对于 React 应用,可能需要额外代码来绑定数据到组件。
  • SWR:
    • 优点
      • 与 React 集成紧密,代码简洁,减少样板代码。
      • 内置缓存、自动重新验证、错误重试等功能。
      • 优化用户体验(如离线支持、快速响应)。
    • 缺点
      • 仅限 React 应用,通用性不如 Axios。
      • 对于复杂请求(如文件上传、自定义拦截器),需要依赖其他库(如 Axios)。

7. 结合使用

在实际项目中,SWR 和 Axios 并不互斥,经常一起使用:

  • SWR 负责数据获取、缓存和组件状态管理。
  • Axios 作为 SWR 的 fetcher 函数,处理底层的 HTTP 请求。

例如:

import useSWR from 'swr';
import axios from 'axios';

const fetcher = (url) => axios.get(url).then((res) => res.data);

function MyComponent() {
  const { data, error } = useSWR('/api/data', fetcher);
  // ...
}

总结

  • 如果你需要一个通用的 HTTP 请求工具,Axios 是更好的选择。
  • 如果你开发 React 应用,且需要简洁的数据获取、缓存和状态管理,SWR 是更优的选择。
  • 在 React 项目中,SWR 搭配 Axios 可以兼顾两者的优点:Axios 处理复杂请求,SWR 优化数据管理和用户体验。

前端开发AI Agent之Memory实战篇

我打算做一个前端学习助手,能够帮助前端开发者寻找网上的岗位并且提供模拟面试功能,这篇主要完善对话上下文,让Agent能够记住用户的信息,支持更好的对话

生成memory实例

我配置了sematicRecall和workingMemory,限制查找最新20条对话,user template是通过trae内置的AI生成的,相关信息都很完整,一次就让我满意了

import { Memory } from "@mastra/memory";
import { semanticRecall, memoryOptions } from "../../config/memory";
// Initialize memory with LibSQL defaults
const memory = new Memory({
  options: {
    ...memoryOptions,
    semanticRecall,
    workingMemory: {
      enabled: true,
      use: "tool-call",
      template: `你是一个前端职业助手,你需要记住以下信息:

1. 用户的职业背景:
- 工作年限:{工作年限}
- 当前职位:{当前职位}
- 技术栈:{技术栈}
- 所在城市:{所在城市}

2. 用户的职业目标:
- 期望职位:{期望职位}
- 期望薪资:{期望薪资}
- 期望城市:{期望城市}

3. 最近的对话重点:
{最近对话要点}

4. 需要跟进的事项:
{待跟进事项}

请基于以上信息,为用户提供更有针对性的职业建议。`,
    },
  },
});

export default memory;

生成Agent

用其他大模型生成了一个agent instrucation,觉得有些冗余,于是让trae内置的claude帮我精简一下

Purpose: Assist users in preparing for frontend developer roles by providing job crawling and mock interview features.
  Tone: Professional, encouraging, and concise.
  Language: Chinese (default), with English support for technical terms or user preference.
  Functions:
  1. Job Crawling (岗位爬取)  
  ○ Objective: Collect and summarize frontend developer job postings based on user preferences (e.g., location, experience level, tech stack).  
  ○ Actions:  
    ■ Query job platforms (e.g., 拉勾网, Boss直聘, LinkedIn) using keywords like "前端开发," "Frontend Developer," "React," "Vue," etc.  
    ■ Filter results by user-specified criteria (e.g., city, salary range, remote/onsite).  
    ■ Extract key details: job title, company, location, salary, required skills (e.g., JavaScript, TypeScript, CSS), and application link.  
    ■ Present results in a structured format (e.g., table or list) with a brief summary of each role.  
    ■ Allow sorting by relevance, salary, or posting date.
  2. Mock Interview (模拟面试)  
  ○ Objective: Simulate a frontend developer interview to help users practice technical and behavioral questions.  
  ○ Actions:  
    ■ Offer two modes:  
      ● Technical Interview: Focus on frontend concepts (e.g., JavaScript closures, React hooks, CSS Flexbox, performance optimization).  
      ● Behavioral Interview: Cover common HR questions (e.g., "谈谈你的项目经验," "如何处理团队冲突?").
    ■ Generate questions based on user’s experience level (junior, mid-level, senior) and tech stack (e.g., React, Vue, Angular).  
    ■ Provide a mix of question types:  
      ● Conceptual: "解释 Event Loop 的工作原理。"Coding: "写一个函数实现防抖 (debounce)。"Problem-Solving: "如何优化一个大型单页应用的加载速度?"Allow users to respond via text or voice (if supported).  
    ■ Evaluate answers with constructive feedback:  
      ● Highlight strengths (e.g., "你对 React hooks 的理解很清晰!").  
      ● Suggest improvements (e.g., "可以补充 Virtual DOM 的 diff 算法细节。").  
      ● Offer correct answers or sample solutions for reference.
    ■ Simulate follow-up questions to mimic real interviews (e.g., "你提到的 Redux,为什么不用 Zustand?")`

精简后就很nice,后面可能要配置中英双语了,暂时还是这样。

export const instructions = `
角色:前端开发职业助手
语言:默认中文,技术术语可用英文
风格:专业、鼓励、简洁

核心功能:

1. 岗位信息服务
- 从主流招聘平台(拉勾、Boss直聘、LinkedIn)搜集前端岗位
- 按用户需求(地点、薪资、技术栈)筛选
- 提供岗位关键信息:职位、公司、地点、薪资、技能要求
- 支持按相关度、薪资、发布时间排序

2. 模拟面试
- 技术面试:前端核心概念、框架、性能优化等
- HR面试:项目经验、团队协作等常见问题
- 根据面试者级别(初中高)动态调整难度
- 面试题类型:
  ○ 概念题:如"Event Loop原理"
  ○ 编程题:如"实现防抖函数"
  ○ 方案题:如"性能优化方案"
- 提供即时反馈和改进建议
- 模拟真实追问场景`;

测试

我:我想找一个月薪2万以上的工作

AI:好的,我会帮你筛选月薪2万以上的前端岗位。请提供以下信息以便更精准地匹配:

  1. 工作地点:你希望工作的城市或地区。
  2. 技术栈:你熟悉或希望使用的技术(如React、Vue、Node.js等)。
  3. 其他要求:如公司规模、行业偏好等。

一旦提供这些信息,我将从拉勾、Boss直聘、LinkedIn等平台为你搜集并筛选合适的岗位,并提供关键信息如职位、公司、地点、薪资和技能要求。你还可以选择按相关度、薪资或发布时间排序。

如果有其他需求或问题,随时告诉我!加油,相信你能找到理想的工作!🚀

我:工作地点是北京,技术栈包括AI开发,react,threejs,cesium,electron,nextjs等,没有其他要求

AI:明白了!我会根据你的需求,在北京地区筛选月薪2万以上的前端岗位,且技术栈包括AI开发、React、Three.js、Cesium、Electron、Next.js等。

以下是筛选流程和预期结果:

  1. 数据搜集:从拉勾、Boss直聘、LinkedIn等主流招聘平台获取相关岗位信息。

  2. 条件筛选

    • 地点:北京
    • 薪资:月薪2万以上
    • 技术栈:包含AI开发、React、Three.js、Cesium、Electron、Next.js
  3. 信息整理:提取职位名称、公司名称、地点、薪资范围、技能要求等关键信息。

  4. 排序选项:提供按相关度、薪资、发布时间排序的功能。 示例结果(假设数据):

职位名称 公司名称 地点 薪资范围 技能要求
高级前端开发工程师 XYZ科技公司 北京 25k-40k React, Three.js, Cesium, Electron, Next.js, AI开发
前端架构师 ABC创新公司 北京 30k-50k React, Next.js, Electron, AI开发, 性能优化
AI前端开发工程师 DEF研究院 北京 28k-45k AI开发, React, Three.js, Cesium, Electron

请注意,以上数据为示例,实际结果可能会有所不同。我会尽快为你提供最新的岗位信息。

如果你需要进一步的帮助或有其他问题,随时联系我!祝你求职顺利!🌟

我:你筛选的条件是什么?

AI:根据你的需求,我为你设定的筛选条件如下:

  1. 工作地点:北京

  2. 薪资要求:月薪2万以上

  3. 技术栈要求

    • AI开发
    • React
    • Three.js
    • Cesium
    • Electron
    • Next.js

这些条件将用于从主流招聘平台(如拉勾、Boss直聘、LinkedIn)中筛选出符合你期望的前端岗位。

具体筛选逻辑

  • 地点筛选:只选择工作地点为北京的公司。
  • 薪资筛选:确保岗位的薪资范围最低值不低于2万。
  • 技术栈筛选:岗位描述中需包含你提到的所有技术栈(AI开发、React、Three.js、Cesium、Electron、Next.js)。

通过这些条件,我们可以确保找到的岗位既符合你的地理位置和薪资要求,又匹配你的技术专长。


经过测试,同一个窗口里是没问题了,但是再开一个新窗口并没有记住我的信息,可能是mastra dev开启的测试UI有些问题没有关于workingMemory的记录,我决定自己写一个UI了。另外值得一提的是:AI聊天的时候说到的岗位筛选流程很有用,后面我想要通过MCP按这个流程爬取和整理一个工作流,交由agent使用真正实现功能。

前端页面设计

resourseid传入userId,threadId可以通过uuid生成随机数,这样就能够在workingMemory中标记人员信息了。

技术栈选型

  1. @assistant-ui,基于shadcn和tailwindcss实现,所以是可以进行高度定制化,使用自己的样式
  2. 我们是使用mastra开发的后端服务,所以要自己开发一个Provider进行运行时的状态管理:包括聊天时的状态管理和与LLM交互的状态管理,贴一张官方的结构图

image.png 3. Agent接口直接pnpm dev后,访问http://localhost:4111/swagger-ui就会获得全部的接口了

具体效果下一期带着Agent相关开发信息一期展示一下

TS 类型推导

什么是类型推导?

类型推导是 TypeScript 编译器的一项核心能力。简单来说,当你在代码中没有显式地通过类型注解 (: Type) 来指定一个变量或表达式的类型时,TypeScript 编译器会尝试根据上下文信息(主要是变量的初始值或函数的返回值)自动推断出这个变量或表达式应该具有的类型

为什么需要类型推导?

  • 减少代码冗余: 你不需要为每一个变量都写上类型注解,尤其是在类型很明显的情况下,这让代码更简洁。
  • 保持类型安全: 即使没有显式注解,TypeScript 依然知道变量的类型,并在后续的使用中进行类型检查,保证类型安全。
  • 提高开发效率: 少写一些模板化的注解代码。

类型推导在哪些场景下发生?

  1. 变量初始化时 (Variable Initialization): 这是最常见的场景。当你声明一个变量并立即为其赋初始值时,TypeScript 会根据初始值的类型来推断变量的类型。

    // 基本类型推导
    let name = "Alice";      // 推断为 string
    let age = 30;          // 推断为 number
    let isStudent = false; // 推断为 boolean
    
    // name = 123; // Error: Type 'number' is not assignable to type 'string'.
    
    // 数组推导
    let numbers = [1, 2, 3]; // 推断为 number[]
    let mixed = [1, "hello", true]; // 推断为 (string | number | boolean)[]
    
    // numbers.push("four"); // Error: Argument of type 'string' is not assignable to parameter of type 'number'.
    
    // 对象推导
    let person = { name: "Bob", age: 42 }; // 推断为 { name: string; age: number; }
    
    // person.age = "forty-two"; // Error: Type 'string' is not assignable to type 'number'.
    // person.location = "City"; // Error: Property 'location' does not exist on type '{ name: string; age: number; }'.
        
    
  2. 函数返回值 (Function Return Types): 如果一个函数没有显式注解返回值类型,TypeScript 会尝试根据函数体中的 return 语句来推断返回类型。

    function add(a: number, b: number) { // 没有 : number 返回值注解
      return a + b; // TypeScript 分析 return 语句,推断返回类型为 number
    }
    let sum = add(5, 3); // sum 被推断为 number
    
    function createGreeting(name: string) {
      if (name) {
        return "Hello, " + name; // 返回 string
      } else {
        return "Hello!";       // 返回 string
      }
      // 推断返回类型为 string
    }
    
    function process(value: string | number) {
        if (typeof value === 'string') {
            return value.length; // 返回 number
        }
        return value * 2;     // 返回 number
        // 推断返回类型为 number
    }
    
    function logMessage(message: string) {
        console.log(message);
        // 没有 return 语句,或者只有空的 return;
        // 推断返回类型为 void
    }
        
    
    • 注意: 对于复杂的函数、递归函数或有多个不同类型返回值的函数(除非它们能形成明确的联合类型),TypeScript 可能无法精确推断,或者推断出的类型可能不是你想要的。在这些情况下,以及为了代码清晰和作为“契约”,显式注解函数返回值通常是更好的实践
  3. 参数默认值 (Default Parameter Values): 如果函数参数有默认值,TypeScript 会根据默认值的类型推断参数的类型。

    function greet(name = "World") { // name 被推断为 string
      console.log(`Hello, ${name.toUpperCase()}!`);
    }
    // greet(123); // Error: Argument of type 'number' is not assignable to parameter of type 'string | undefined'.
        
    
  4. 解构赋值 (Destructuring Assignments): 从对象或数组解构时,变量的类型也会被推断。

          let point = { x: 10, y: 20 };
    let { x, y } = point; // x 和 y 都被推断为 number
    
    let rgb: [number, number, number] = [255, 0, 128];
    let [r, g, b] = rgb; // r, g, b 都被推断为 number
        
    

“最佳通用类型” (Best Common Type) 算法

当需要从多个表达式(例如数组中的元素或 if/else 的不同分支返回值)推断类型时,TypeScript 会尝试找到一个能兼容所有这些表达式的“最佳通用类型”。

let arr = [1, null, "hello"];
// 元素类型:number, null, string
// 最佳通用类型:(string | number | null)[]
// arr 被推断为 (string | number | null)[]

function checkValue(input: number) {
    if (input > 0) {
        return "positive"; // string
    } else if (input < 0) {
        return "negative"; // string
    }
    return 0; // number
    // 多个返回类型:string, string, number
    // 最佳通用类型:string | number
    // 函数返回值被推断为 string | number
}
    

如果无法找到合适的通用类型(例如,数组包含结构完全不同的复杂对象),编译器可能会推断出一个不太有用的类型(如 {}[]),或者在严格模式下报错,此时最好提供显式的类型注解。

上下文类型 (Contextual Typing)

这是一种稍微不同的推断形式,类型信息从“上下文”流向表达式,而不是从表达式流向变量。

  • 事件处理:

    window.onclick = function(event) {
      // TypeScript 根据 window.onclick 的预期类型
      // 推断出 event 参数是 MouseEvent 类型
      console.log(event.button); // 可以安全访问 MouseEvent 的属性
    };
        
    
  • 数组方法的回调:

    let nums = [1, 2, 3];
    nums.forEach(n => {
      // TypeScript 根据 number[] 的 forEach 方法签名
      // 推断出 n 参数是 number 类型
      console.log(n.toFixed(2));
    });
        
    
  • 赋值给已有类型的变量:

    let calculate: (x: number, y: number) => number;
    calculate = (a, b) => { // a 和 b 会被上下文推断为 number
      return a + b;
    };
        
    

何时仍需显式注解?

尽管类型推导很强大,但在以下情况推荐或必须使用显式类型注解:

  1. 变量声明时没有初始值:

    let value; // 推断为 any (如果 noImplicitAny 关闭) 或报错 (如果开启)
    value = 10;
    value = "hello"; // 如果是 any 则不会报错,类型不安全
    
    // 推荐做法:
    let betterValue: string | number;
    betterValue = 10;
    // betterValue = true; // Error!
        
    
  2. 希望变量类型比推断出的类型更通用/抽象:

    interface Animal { name: string; }
    interface Dog extends Animal { breed: string; }
    
    // 推断类型为 Dog
    let dog = { name: "Buddy", breed: "Golden Retriever" };
    
    // 如果希望变量只关注 Animal 部分,需要显式注解
    let animal: Animal = { name: "Max", breed: "Labrador" }; // OK
    // console.log(animal.breed); // Error: Property 'breed' does not exist on type 'Animal'.
        
    
  3. 函数返回值类型不明显或为了代码清晰/API契约: 如前所述,显式注解函数返回值通常是个好主意。

  4. 对象字面量可能包含额外属性(绕过额外属性检查):

    interface Options { color?: string; width?: number; }
    // let opts = { color: "red", width: 100, speed: 10 }; // Error: Object literal may only specify known properties...
    
    // 通过注解可以绕过初始赋值时的额外属性检查(但类型仍需兼容)
    let opts: Options = { color: "red", width: 100, speed: 10 }; // Error依然存在,除非 Options 允许额外属性
    
    // 更常见的是先创建变量再赋值
    let optsVar: Options;
    optsVar = { color: "red", width: 100, speed: 10 }; // Error: 'speed' does not exist in type 'Options'
    
    // 或者使用类型断言(不推荐,除非你确定)
    let optsAssert = { color: "red", width: 100, speed: 10 } as Options; // 类型断言
        
    
  5. 避免 TypeScript 错误地推断出 any 类型: 当 TypeScript 无法确定类型且 noImplicitAny 编译选项关闭时,它可能会回退到 any。显式注解可以避免这种情况。

JavaScript对象深度解析:从语法到遍历

1. 对象基础

1.1 语法形式

文字形式

const obj = { 
  name: 'Alice',
  age: 28 
};

构造形式

const obj = new Object();
obj.name = 'Alice';
obj.age = 28;

在文字声明中,你可以添加多个键/值对,但是在构造形式中,你必须逐个添加属性。

1.2 类型体系

类型 示例 特性
普通对象 {} 键值对集合
包装对象 new Number(42) 原始值包装
内置对象 Array, Date 语言内置类型
函数对象 function Foo(){} 可调用对象

2. 对象内容

2.1 属性管理

可计算属性名

const dynamicKey = 'user_' + Date.now();
const obj = {
  [dynamicKey]: '动态属性'
};

属性 vs 方法

const calculator = {
  // 属性
  PI: 3.1415926,
  
  // 方法
  sum: function(a, b) {
    return a + b;
  }
};

2.2 特殊对象

数组特性

const arr = [1, 2, 3];
console.log(arr.length); // 3
arr['customProp'] = 'value'; // 非标准用法

2.3 对象操作

深度复制

// 浅拷贝
const copy1 = Object.assign({}, original);

// 深拷贝
const copy2 = JSON.parse(JSON.stringify(original));

2.4 属性控制

属性描述符

Object.defineProperty(obj, 'secret', {
  value: 'confidential',
  writable: false,
  enumerable: false
});

对象不变性

方法 效果 示例
Object.preventExtensions 禁止新增属性 Object.preventExtensions(obj)
Object.seal 密封对象(禁止增删属性) Object.seal(obj)
Object.freeze 完全冻结对象 Object.freeze(obj)

2.5 访问机制

[[Get]] 流程

graph TD
    A[属性访问] --> B{对象自身属性}
    B -->|存在| C[返回值]
    B -->|不存在| D[原型链查找]
    D --> E{找到属性?}
    E -->|是| F[返回值]
    E -->|否| G[返回undefined]

[[Put]] 过程

  1. 检查属性是否存在访问器描述符(setter)
  2. 验证属性是否可写(writable)
  3. 执行赋值操作或触发TypeError

2.6 访问器属性

const bankAccount = {
  _balance: 1000,
  
  get balance() {
    return this._balance + ' USD';
  },
  
  set balance(value) {
    if(value < 0) throw new Error('余额不能为负');
    this._balance = value;
  }
};

3. 存在性与遍历

3.1 存在性检查

// 检查自有属性
obj.hasOwnProperty('key');

// 包含原型链检查
'toString' in obj; // true

3.2 遍历方法对比

方法 返回值 包含原型链 示例
for...in 可枚举属性名 for(let key in obj)
Object.keys() 自有可枚举属性名数组 Object.keys(obj)
Object.getOwnPropertyNames() 所有自有属性名 Object.getOwnPropertyNames(obj)
Reflect.ownKeys() 包括Symbol属性 Reflect.ownKeys(obj)

4. 最佳实践

  1. 优先使用对象字面量:比构造形式更简洁高效
  2. 合理使用属性描述符:精确控制对象行为
  3. 注意深浅拷贝区别:根据场景选择复制方式
  4. 慎用for...in遍历:原型链属性可能带来意外结果
  5. 利用现代语法
// 属性值简写
const name = 'Bob';
const obj = { name };

// 方法简写
const obj = {
  sayHi() {
    console.log('Hello!');
  }
};

5. 进阶方向

  • 原型继承机制
  • ES6 Proxy对象
  • 对象性能优化
  • WeakMap/WeakSet特殊存储

通过深入理解对象系统,开发者可以更好地驾驭JavaScript的核心特性,构建更健壮的应用程序。

CSS网格布局:现代网页设计的终极解决方案

在网页设计领域,多行多列布局一直是开发者面临的挑战之一。随着CSS Grid(网格布局)的出现,我们终于拥有了一种强大而灵活的布局解决方案。本文将深入探讨CSS网格布局的核心概念和实用技巧,帮助你掌握这一现代网页设计的终极工具。

什么是网格布局?

CSS网格布局是一个二维布局系统,专门为解决复杂的网页布局问题而设计。与传统的浮动布局或弹性盒(Flexbox)不同,网格布局可以同时处理行和列,让你能够轻松创建复杂的页面结构。

核心概念

1. 网格容器与网格项目

通过将元素的display属性设置为gridinline-grid,该元素就成为了一个网格容器,其直接子元素自动变为网格项目

2. 定义行与列

使用grid-template-columnsgrid-template-rows属性可以明确定义网格的列和行:

3. 灵活的尺寸单位

网格布局引入了强大的尺寸单位:

  • fr:剩余空间分配单位
  • minmax():定义大小范围
  • repeat():简化重复模式
grid-template-columns: repeat(3, 1fr); /* 三等分列 */
grid-template-rows: minmax(100px, auto); /* 最小100px,内容多则自动扩展 */

grid2.png

高级特性

1. 网格线定位

网格项目可以通过网格线精确定位:

.item {
  grid-column: 1 / 3; /* 从第1列线到第3列线 */
  grid-row: 2; /* 第2行 */
}

2. 网格区域命名

为网格区域命名可以大大提高代码可读性:

.container {
  display: grid;
  grid-template-areas:
    "header header header"
    "sidebar content content"
    "footer footer footer";
}

.header { grid-area: header; }
.sidebar { grid-area: sidebar; }
.content { grid-area: content; }
.footer { grid-area: footer; }

3. 响应式设计

结合媒体查询,网格布局可以轻松实现响应式设计:

.container {
  display: grid;
  grid-template-columns: 1fr;
}

@media (min-width: 768px) {
  .container {
    grid-template-columns: 200px 1fr;
  }
}

实用技巧

  1. 间隙控制:使用gap属性替代传统的margin

grid4.png 2. 对齐方式: 默认情况下,网格项目在单元格内部水平和垂直拉伸,以撑满单元格。可以使用属性justify-items设置水平方向的排列方式。可以使用属性align-items设置垂直方向的排列方式。它们的可取值是相同的:

justify-items: start 左 | end 右 | center 中 | stretch 拉伸;
align-items: start 上 | end 下 | center 中 | stretch 拉伸;

grid5.png

  1. 隐式网格:当项目超出明确定义的网格时,使用grid-auto-rowsgrid-auto-columns控制自动生成的轨道大小

网格布局 vs 弹性盒

虽然网格布局和弹性盒都是现代CSS布局工具,但它们有各自的最佳使用场景:

  • 弹性盒:适合一维布局(行或列)
  • 网格布局:适合二维布局(行和列)

在实际项目中,两者常常结合使用,发挥各自优势。

学习资源

  1. MDN网格布局指南
  2. 网格布局完全指南

结语

CSS网格布局彻底改变了我们构建网页布局的方式,提供了前所未有的控制力和灵活性。通过掌握这一强大工具,开发者可以更高效地创建复杂、响应式的网页设计,同时保持代码的简洁和可维护性。虽然学习曲线可能略陡峭,但投入时间掌握网格布局绝对是值得的,它将极大提升你的前端开发能力。

❌