阅读视图

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

MTGR:美团外卖生成式推荐Scaling Law落地实践

美团外卖推荐算法团队基于HSTU提出了MTGR框架以探索推荐系统中Scaling Law。MTGR对齐传统模型特征体系,并对多条序列利用Transformer架构进行统一建模。通过极致的性能优化,样本前向推理FLOPs提升65倍,推理成本降低12%,训练成本持平。MTGR离在线均取得近2年迭代最大收益,且于2025年4月底在外卖推荐场景全量。本文系相关工作的实践与经验总结,希望能给从事相关方向研究的同学带来一些帮助。

三角形类型

方法一:数学

首先将 $\textit{nums}$ 按照从小到大的顺序进行排序,然后依次进行以下判断:

  • 如果 $\textit{nums}[0] + \textit{nums}[1] \le \textit{nums}[2]$,那么返回 $``\text{none}''$。

  • 如果 $\textit{nums}[0] = \textit{nums}[2]$,那么返回 $``\text{equilateral}''$。

  • 如果 $\textit{nums}[0] = \textit{nums}[1]$ 或 $\textit{nums}[1] = \textit{nums}[2]$,那么返回 $``\text{isosceles}''$。

  • 以上条件都不满足,返回 $``\text{scalene}''$。

###C++

class Solution {
public:
    string triangleType(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        if (nums[0] + nums[1] <= nums[2]) {
            return "none";
        } else if (nums[0] == nums[2]) {
            return "equilateral";
        } else if (nums[0] == nums[1] || nums[1] == nums[2]) {
            return "isosceles";
        } else {
            return "scalene";
        }
    }
};

###Go

func triangleType(nums []int) string {
    sort.Ints(nums)
    if nums[0] + nums[1] <= nums[2] {
        return "none"
    } else if nums[0] == nums[2] {
        return "equilateral"
    } else if nums[0] == nums[1] || nums[1] == nums[2] {
        return "isosceles"
    } else {
        return "scalene"
    }
}

###Python

class Solution:
    def triangleType(self, nums: List[int]) -> str:
        nums.sort()
        if nums[0] + nums[1] <= nums[2]:
            return "none"
        elif nums[0] == nums[2]:
            return "equilateral"
        elif nums[0] == nums[1] or nums[1] == nums[2]:
            return "isosceles"
        else:
            return "scalene"

###Java

class Solution {
    public String triangleType(int[] nums) {
        Arrays.sort(nums);
        if (nums[0] + nums[1] <= nums[2]) {
            return "none";
        } else if (nums[0] == nums[2]) {
            return "equilateral";
        } else if (nums[0] == nums[1] || nums[1] == nums[2]) {
            return "isosceles";
        } else {
            return "scalene";
        }
    }
}

###JavaScript

var triangleType = function(nums) {
    nums.sort((a, b) => a - b);
    if (nums[0] + nums[1] <= nums[2]) {
        return "none";
    } else if (nums[0] === nums[2]) {
        return "equilateral";
    } else if (nums[0] === nums[1] || nums[1] === nums[2]) {
        return "isosceles";
    } else {
        return "scalene";
    }
};

###TypeScript

function triangleType(nums: number[]): string {
    nums.sort((a, b) => a - b);
    if (nums[0] + nums[1] <= nums[2]) {
        return "none";
    } else if (nums[0] === nums[2]) {
        return "equilateral";
    } else if (nums[0] === nums[1] || nums[1] === nums[2]) {
        return "isosceles";
    } else {
        return "scalene";
    }
}

###C

int cmp(const void* a, const void* b) {
    return (*(int*)a - *(int*)b);
}

char* triangleType(int* nums, int numsSize) {
    qsort(nums, numsSize, sizeof(int), cmp);
    if (nums[0] + nums[1] <= nums[2]) {
        return "none";
    } else if (nums[0] == nums[2]) {
        return "equilateral";
    } else if (nums[0] == nums[1] || nums[1] == nums[2]) {
        return "isosceles";
    } else {
        return "scalene";
    }
}

###C#

public class Solution {
    public string TriangleType(int[] nums) {
        Array.Sort(nums);
        if (nums[0] + nums[1] <= nums[2]) {
            return "none";
        } else if (nums[0] == nums[2]) {
            return "equilateral";
        } else if (nums[0] == nums[1] || nums[1] == nums[2]) {
            return "isosceles";
        } else {
            return "scalene";
        }
    }
}

###Rust

impl Solution {
    pub fn triangle_type(mut nums: Vec<i32>) -> String {
        nums.sort();
        if nums[0] + nums[1] <= nums[2] {
            "none".to_string()
        } else if nums[0] == nums[2] {
            "equilateral".to_string()
        } else if nums[0] == nums[1] || nums[1] == nums[2] {
            "isosceles".to_string()
        } else {
            "scalene".to_string()
        }
    }
}

复杂度分析

  • 时间复杂度:$O(1)$。

  • 空间复杂度:$O(1)$。

原子化的未来?了解一下全面进化的CSS attr函数

欢迎关注我的公众号:前端侦探

CSS attr函数相信大家都用过了吧,通常会配合伪元素content动态生成内容,比如一个简易的tooltip

<span class="css-tips" data-title="我是tooltip" >提示上</span>

通过attr动态生成

.css-tips[data-title]:after {
  content: attr(data-title);
  /*...*/
}

效果如下

image-20250516200454818

你可以访问这个链接查看完整demo: codepen.io/xboxyan/pen…

不过,之前仅仅支持字符串形式,对于数字、颜色等都无法识别,例如

<div w="10"></div>
<style>
  div{
    width: attr(w) /**不生效/
  }
</style>

现在,CSS attr迎来了全面进化(chrome 133+),很多问题都得到了很好的解决,一起看看吧~

一、快速上手

比如这样一个结构,是不是看着有些眼熟?

<div w="100" h="100"></div>
<style>
  div{
    background: royalblue;
  }
</style>

那么,如何让属性上的尺寸传递应用到实际的宽高上呢?你可以这样

[w]{
  width: attr(w px)
}
[h]{
  height: attr(h px)
}

来看看效果

image-20250516170508419

我们可以用之前的规则,将尺寸通过content显示出来

div:before{
  content: attr(w) '*' attr(h);
  color: white;
  font-size: 14px;
}

效果如下

image-20250516170636866

更为关键的是,这些完全是自动获取的,你可以设置多个任意尺寸

<div w="100" h="100"></div>
<div w="200" h="100"></div>
<div w="300" h="100"></div>

效果如下

image-20250516170814615

是不是非常灵活?

二、语法详解

现在来看看语法规则

attr(<attr-name> <attr-type>? , <fallback-value>?)

其实相比之前的规则,多了两个可选参数,一个是attr-type,表示属性类型,完整类型可以参考

developer.mozilla.org/en-US/docs/…

还有一个是allback-value,表示回退值,一些写法如下

/* Basic usage */
attr(data-count)
attr(href)

/* With type */
attr(data-width px)
attr(data-size rem)
attr(data-name raw-string)
attr(id type(<custom-ident>))
attr(data-count type(<number>))
attr(data-size type(<length> | <percentage>))

/* With fallback */
attr(data-count type(<number>), 0)
attr(data-width px, inherit)
attr(data-something, "default")

前面的例子其实带类型的值,除了使用px,还可以使用任何已有的CSS单位,比如

<div w="100" h="100" rotate="45"></div>

这里定义了一个旋转角度,可以直接加上角度单位deg

[rotate]{
  rotate: attr(rotate deg)
}

效果如下

image-20250516172610893

但是,有些值其实是不带单位的,比如颜色,并没有什么后缀单位,比如

<div w="100" h="100" rotate="45" bg="red"></div>

这时,可以采用type来手动指定

[bg]{
  background: attr(bg type(<color>));
}

效果如下

image-20250516181308281

有些属性可能不止一种类型,比如background,支持颜色,也支持渐变,还支持图像,这里其实也能定义多种类型

[bg]{
  background: attr(bg type(<color>|<image>));
}

我们换成渐变试试

<div w="100" h="100" rotate="45" bg="linear-gradient(orange,red)"></div>

也能完美适配

image-20250516182249509

多个值写起来可能比较麻烦,可以用通配符来代替,相当于传入什么,读取的就是什么

[bg]{
  background: attr(bg type(*));
}

最后就是回退值,非常类CSS变量,当属性不存在时(注意不能是空),采用回退值,比如

div{
  background: attr(bg type(*), royalblue);
}

现在去除bg属性

<div w="100" h="100" rotate="45"></div>

就回到了默认的宝蓝色

image-20250516183003948

你也可以访问在线demo真实体验: codepen.io/xboxyan/pen…

三、带数字显示的进度条

下面来看一个案例

image-20250516185648485

在过去,如果想用单个标签、单一变量来实现,通常会用到CSS变量,就像这样

<div class="progress" style="--value:30"></div>
<div class="progress" style="--value:42.5"></div>
<div class="progress" style="--value:50"></div>
<div class="progress" style="--value:90"></div>

进度很好办,直接用这个变量计算就好了,那后面的数字怎么办呢?直接使用变量是不行的

::before{
  content: var(--value) /*不生效*/
}

其实可以用计数器来实现,类似于这样

.progress::before {
  --value: 50;
    counter-reset: progress var(--value);
    content: counter(value);
}

有兴趣可以查看张老师的这篇文章: 小tips: 如何借助content属性显示CSS var变量值

不过计数器在正常场景下不支持小数,导致有些场景受限

如果需要展示小数可以参考这篇文章: 如何让CSS计数器支持小数的动态变化?

现在有了attr,可以直接用属性来实现,实现更方便

<div class="progress" value="30"></div>
<div class="progress" value="42.5"></div>
<div class="progress" value="50"></div>
<div class="progress" value="90"></div>

直接通过渐变绘制进度attr(value %)

.progress {
  color: royalblue;
  width: 300px;
  height: 20px;
  background: linear-gradient(currentColor, currentColor) 0 0 / attr(value %) 100% no-repeat #ccc;
  border-radius: 2px;
  position: relative;
}
.progress::after {
  content: attr(value);
  position: absolute;
  top: 50%;
  left: 100%;
  transform: translate(10px, -50%);
  font-size: 20px;
}

你也可以访问在线demo真实体验:codepen.io/xboxyan/pen…

四、原子化的未来?

回头再来看看这种写法,是不是非常类似现在流行的原子化CSS?

<div w="100" h="100"></div>
<div w="200" h="100"></div>
<div w="300" h="100"></div>

嗯...等到兼容性没有问题后,现在的原子化框架都得革新了 ,只需要极少部分原子CSS即可适配大量的样式,而不是这样生成大量用到的样式

image-20250516195921515

attr可能就两行,类似这样

[fs]{
  font-size: attr(fs type(<length>))
}
p{
  padding: attr(p type(*))
}

是不是可以节省大量CSS代码?

五、优势和局限

其实很多特性和CSS变量还是比较相似,不过相比而言还是有不少优势的

  1. 支持content内容生成
  2. html结构更直观,个人觉得CSS变量放在style上有些冗余
  3. 天然原子化,比现在框架生成要高效的多

然后有一个局限性,那就是不支持链接格式,比如

<div src="xxx.png"></div>

如果直接这样使用,是不会生效的

div{
  background: url(attr(src)); /*无效*/
}

只能用这种形式,其实和现在CSS变量差不多了

<div src="url(xxx.png)"></div>

官方说明是为了安全考虑,不能用于动态构造 URL

😭太可惜了,一直想用这个功能能实现自定义 img 标签,将图片转成背景图片,这样就能做更多事情了

<img src="xxx.png">
<style>
  img{
    background: url(attr(src));
  }
</style>

总之,这是一个未来非常有潜力的新特性,敬请期待吧。最后,如果觉得还不错,对你有帮助的话,欢迎点赞、收藏、转发 ❤❤❤

别再写一堆 if-else 了:用状态模式优雅管理状态行为

前言

介绍了状态模式(State Pattern)的概念及其在管理对象动态行为中的应用。今日文章由 @Maxim Gorin 分享,前端早读课@飘飘翻译。

状态设计模式(State Design Pattern)是一种行为型软件设计模式,它允许对象在其内部状态发生改变时调整自身的行为。简单来说,状态模式让对象根据当前的状态表现出不同的行为,而不用在代码中堆满 if/else 或 switch 语句。

在我们之前的文章《为什么命令模式比你想象的更有用》中,我们探讨了如何将动作封装为对象以提升代码的灵活性。状态模式采取了类似的思路,不过它专注于将 “状态” 和 “行为” 封装为独立的对象。和命令模式一样,状态模式有助于我们减少大量条件语句,并遵循良好的设计原则 —— 但它解决的是另一类问题。

图片

一个现实生活中的类比:手机的通知模式

想象一下,你有一部智能手机,它有多个通知模式:正常、振动、静音。

  • 在 “正常” 模式下,来电会响铃;
  • 在 “振动” 模式下,手机不会响铃,而是震动;
  • 在 “静音” 模式下,既不会响铃也不会震动 —— 可能只是记录一个未接来电。

你(作为手机的使用者)可能会根据不同场景(比如上班、开会、看电影等)手动切换这些模式,而手机的行为会随之改变,而无需你每次都去修改手机内部的响铃机制。

这个例子就是状态模式的一个贴切类比:

  • 手机是那个行为会根据状态变化的对象;
  • 当前的模式(正常 / 振动 / 静音)就是手机的内部状态;
  • 每种状态定义了手机在特定操作下应该如何响应(比如接到电话时怎么做);
  • 切换模式,其实就是更换内部的状态对象,从而改变了手机的行为。

那为什么不用 if-else 或枚举呢?你当然可以用简单的 if 或 switch 来处理手机的行为:

if(mode ==NORMAL){
   响铃
}else if(mode ==VIBRATE){
   震动
}else if(mode ==SILENT){
   保持安静
}

这个方式在模式不多的时候确实没问题。但想象一下,如果手机有十几种模式,每种模式还要影响多个行为(比如来电、短信、闹钟、通知等),那么条件分支会迅速增多,而且每种模式的逻辑会分散在代码中的各个 if 语句里,维护起来就非常容易出错。

而状态模式提供了一种更清晰的方案:将每种模式都作为一个独立的状态类,里面包含各自的逻辑。手机只需要持有一个状态对象的引用(比如 SilentState 或 VibrateState 的实例),并将行为委托给这个对象。当你切换模式时,实际上就是更换状态对象,这样就避免了大量的条件判断,而是依靠多态来实现 —— 每个状态类知道在该状态下该如何处理具体的动作。

状态模式的工作原理

状态模式包含几个关键组成部分:

  • 上下文(Context)- 就是那个拥有动态内部状态的主要对象。在前面的类比中,手机就是上下文。
  • 状态接口(或抽象类)- 定义所有状态类共有的接口,声明了上下文想要委托给状态对象的一些操作方法。例如,一个 PhoneState 接口可能会声明一个 handleIncomingCall() 方法。
  • 具体状态类(Concrete State Classes)- 这些类代表特定的状态,每个类实现状态接口,并提供该状态下的具体行为,比如 NormalStateVibrateStateSilentState 分别定义了接电话时的不同响应方式。
  • 状态切换 - 上下文通常会有一个方法用于更换当前状态。这种切换可能是外部触发的(比如用户手动换模式),也可以是内部逻辑决定的(某个状态对象决定切换到其他状态)。

图片

在状态模式中,当上下文对象接收到某个请求(比如 phone.receiveCall()),它并不会自己处理这个请求,而是将其委托给当前的状态对象(比如 currentState.handleIncomingCall())。由于每个状态对象对这个方法的实现都不同,所以即便调用的是同一个方法,结果也会因当前状态的不同而有所区别。

这就是多态在起作用:一个方法调用,根据具体的状态对象不同,表现出不同的行为。

避免条件语句过于复杂

使用状态模式的主要动机之一,就是为了消除代码中重复且分散的条件逻辑。如果一个对象的行为会根据状态变化而改变,你可能会倾向于用枚举或布尔标志来跟踪状态,然后在每个需要根据状态处理的地方写 switch 或 if 语句。这种做法会让代码变得臃肿、难以维护。

状态模式的做法是把每种状态下的逻辑封装在独立的类中:

  • 每种状态的逻辑都放在自己专属的类中(比如 “静音模式” 的所有逻辑都放在 SilentState 里)。
  • 上下文(Context)对象的代码会变得更简单,不再需要处理大段的条件判断逻辑。
  • 增加新的状态或修改已有状态,不需要在多个地方修改庞大的 switch 语句 —— 只需新增或修改一个状态类。

经典定义中提到:“当某些操作包含大量依赖于对象状态的条件语句时,状态模式会将每个条件分支封装在独立的类中,把状态当作一个独立的对象来看待。”

这种封装方式符合开闭原则(Open/Closed Principle):我们可以在不修改原有上下文或其他状态类的情况下引入新的状态。同时也符合单一职责原则(Single Responsibility Principle),因为每个状态类只负责处理一种状态下的行为。

什么时候该使用(或不使用)状态模式

适合使用状态模式的场景:
  • 当一个对象的行为依赖其当前状态,并且它在运行时需要根据状态改变行为时。如果你发现自己在多个地方都写着 “如果状态是 X 做这个,状态是 Y 做那个”,那可能就适合用状态模式。
  • 当一个对象有多个行为逻辑,并且这些逻辑可以明确地按状态划分。例如,手机的响铃、震动、静音记录等行为都可以独立处理。
  • 想要避免状态判断逻辑重复出现在多个方法中。使用状态模式后,这些行为被集中封装在状态类中,不再重复。
  • 预计未来可能会增加新的状态,或每个状态下的逻辑会变得更复杂。状态模式的结构更容易扩展(新增一个状态类)或修改(只需改动一个类的代码)。
不适合使用状态模式的情况或需谨慎使用:
  • 如果对象只有一两个状态,而且每种状态下的行为差异非常简单,那么使用状态模式可能就有点小题大做了。用普通的条件判断反而更清晰。
  • 如果状态切换很少发生,或者每种状态的逻辑基本不会变,那用状态模式引入的一堆类可能并不值得。
  • 如果状态数量固定且逻辑简单明确,使用枚举加 switch 语句可能就足够了。状态模式适用于那些状态复杂且易变的场景。

可以这样想:一个只有两个状态的小状态机,用 if 来管理也没问题。但如果是一个有十种状态、状态之间还有复杂切换逻辑的状态机,那用状态模式结构化处理会更好维护。

为什么状态模式比枚举和标志变量更好?

一开始,很多人会选择用枚举或布尔标志来表示状态,比如:

enum Mode { normal, vibrate, silent }

然后用类似这样的逻辑处理行为:

if(mode == Mode.normal){
// 响铃
}else if(mode == Mode.vibrate){
// 震动
}else if(mode == Mode.silent){
// 保持静音
}

这种方式起初是可行的,但随着程序变复杂,会出现以下问题:

  • 逻辑分散 - 如果多个行为都依赖状态判断,你就会在很多方法里看到类似的 if/else 或 switch,例如 handleCall()notifyMessage()alarmRing() 等等。状态行为稍有改动,就得到处找这些条件语句并改动。
  • 违反开闭原则 - 比如你想新增一个 “请勿打扰” 模式(Do Not Disturb),就得修改所有相关的 switch 语句。每次修改都有可能引入 bug,影响原有功能。
  • 维护困难 - 状态和条件越来越多,代码就越难阅读和维护,容易变成一个嵌在业务逻辑中的 “巨型状态机”。

状态模式通过封装各个状态的行为,解决了这些问题。你不再需要一个大函数来处理各种分支,而是有多个小类,各自处理自己的状态行为。这样结构更清晰:

  • Phone 类(上下文)无需了解各个模式的具体行为,它只需把行为委托给当前的状态对象。
  • 想增加一个 “请勿打扰” 模式,只需新增一个 DoNotDisturbState 类,定义好它的行为。Phone 类可能只需做一点小改动,甚至不改(如果状态可以通过 setter 或工厂设置)。
  • 删除或修改某个状态行为,只需要修改该状态类,不会影响到其他代码,降低了出错风险。

简而言之:在复杂场景中,状态模式比枚举 / 标志 + 条件判断更健壮、更灵活。它让代码模块化,符合设计原则,也更方便多个开发者(前端、后端、移动端等)理解和协作,不用去翻那些密密麻麻的条件语句。

状态模式的优缺点

像所有设计模式一样,状态模式也有优劣之分。我们来具体看一下:

优点:
  • 代码结构更清晰 - 每种状态对应一个独立的类,满足单一职责原则。每个状态类只负责一种状态下的行为。
  • 消除复杂的条件语句 - 上下文对象不再被 if/else 或 switch 语句包围,结构更简单,维护起来更轻松。
  • 更符合开闭原则 - 添加新状态无需修改原有代码,特别是上下文对象或其他状态类,扩展性好。
  • 状态切换逻辑集中管理 - 可以在状态类或上下文中集中处理状态之间的转换,流程更易管理和理解。
  • 支持多态行为 - 通过运行时替换状态对象来改变行为,其他系统部分不需要感知变化,降低出错率。
缺点:
  • 类数量增加,结构变复杂 - 每种状态都需要一个类,对于简单场景来说,这可能是 “用大炮打蚊子”,过度设计。
  • 状态爆炸问题 - 如果一个对象有很多种状态,就会出现大量状态类,状态间的切换也变得复杂。(解决办法:可以分组、分层,或重新思考是否真有那么多必要的状态)
  • 状态与上下文之间耦合 - 状态类通常需要知道上下文对象的情况,甚至可能要知道其他状态类的信息,这会引入耦合。好在这种耦合是局部且可控的,通常是可以接受的权衡。
  • 学习成本 - 对一些不熟悉设计模式的开发者来说,“对象中还有另一个对象来处理逻辑” 这个思路可能不太直观。对比直观的 if 条件语句,状态模式可能需要点时间适应。
  • 内存 / 性能开销 - 在某些语言中,频繁创建状态对象可能会有一点性能损耗(不过大多数情况下可以忽略)。如果状态对象包含大量从上下文复制的数据,可能会导致效率问题。但通常状态类都很轻量,甚至可以使用单例模式来复用,因此一般不会成为瓶颈。
应对这些缺点的建议:
  • 担心类太多?可以将状态类写成内部类,甚至匿名类(如果语言支持),这样可以把它们和上下文放在一起。
  • 担心对象创建成本?可以复用状态实例,状态模式并不要求每次都新建对象。你可以用单例或无状态对象。
  • 担心别人看不懂?使用清晰的命名和注释,让每个状态类的职责一目了然,降低理解门槛。

用 Dart 实现状态模式(以手机模式为例)

为了加深理解,我们用 Dart 来实现前面讲到的智能手机通知模式的例子。我们将创建一个简单的模拟程序,模拟手机在不同模式下接到电话的行为。代码是完整可运行的,可以在 Dart 在线编辑器或其他环境中直接运行并在控制台查看输出。

示例设计结构:
  • 我们会创建一个抽象类 PhoneState,定义当手机接到来电时应该执行的方法(onReceiveCall)。
  • 创建三个具体的状态类:NormalStateVibrateState 和 SilentState,分别继承 PhoneState 并实现各自不同的来电响应逻辑。
  • Phone 类是上下文类,它持有一个 PhoneState 类型的状态属性。所有的来电处理都会委托给当前状态对象,同时它也提供方法来切换状态(setState())。
  • 我们将模拟手机在不同模式下接听电话的过程,以观察行为变化。

以下是 Dart 示例代码:

// 状态接口(在 Dart 中用抽象类表示)
 abstract class PhoneState{
  void onReceiveCall(Phone context);
}

// 具体状态类:正常模式(响铃)
class NormalState implements PhoneState{
   @override
   void onReceiveCall(Phone context){
      print("来电:铃铃铃!📢(正常模式)");
      // 正常模式下手机响铃,不会自动切换状态
   }
}

// 具体状态类:振动模式
class VibrateState implements PhoneState{
   int _vibrateCount =0;// 模拟内部状态,例如振动次数

   @override
   void onReceiveCall(Phone context){
     _vibrateCount++;
     print("来电:嗡嗡嗡…… 🤫(振动模式)");
     // 如果振动次数过多,自动切换为静音模式(只是示例,现实中不会这样)
    if(_vibrateCount >=3){
       print("连续振动 $_vibrateCount 次未接听,切换为静音模式。");
       context.setState(SilentState());
    }
  }
}

// 具体状态类:静音模式
classSilentStateimplementsPhoneState{
   @override
   void onReceiveCall(Phone context){
      print("来电:(静音模式,无声音)🤐");
      print("手机保持静音,稍后可能会看到未接来电。");
   }
}

// 上下文类:手机
class Phone{
   // 默认从正常模式开始
   PhoneState _state =NormalState();

   void setState(PhoneState newState){
     _state = newState;
     // 可以在这里打印或记录模式变更(可选)
   }

  void receiveCall(){
     // 将行为委托给当前状态对象
     _state.onReceiveCall(this);
  }

   // 可选:获取当前状态名称用于打印日志
   String get modeName => _state.runtimeType.toString();
}

void main(){
   Phone phone =Phone();
   print("手机当前模式:${phone.modeName}");

   // 模拟来电(正常模式)
   phone.receiveCall();// 响铃

   // 切换到振动模式
   phone.setState(VibrateState());
   print("\n手机当前模式:${phone.modeName}");
   phone.receiveCall();// 第一次振动
   phone.receiveCall();// 第二次振动
   phone.receiveCall();// 第三次振动,触发自动切换为静音

   // 此时应自动进入静音模式
   print("\n手机当前模式:${phone.modeName}");
   phone.receiveCall();// 静音,不响铃

   // 手动切换回正常模式
   phone.setState(NormalState());
   print("\n手机当前模式:${phone.modeName}");
   phone.receiveCall();// 再次响铃
}
上面的代码要点:
  • Phone 类(上下文)并不知道来电时具体该做什么,它只是调用 _state.onReceiveCall(this),由当前状态对象处理行为。这正是状态模式的核心。
  • 每个状态类只处理一种模式下的行为。比如 SilentState 只处理静音模式的来电情况。
  • VibrateState 中加入了一个小彩蛋:如果连续接到 3 个电话都没有应答,它会自动切换为静音模式。这是为了演示状态对象内部可以触发状态切换的能力。
  • 切换状态的方式很简单:调用 phone.setState(SomeState()) 即可。你可以想象这种行为由用户操作或程序逻辑触发。

运行这段代码,会看到类似如下的输出:

 手机当前模式:NormalState
 来电:铃铃铃!📢(正常模式)

 手机当前模式:VibrateState
 来电:嗡嗡嗡…… 🤫(振动模式)
 来电:嗡嗡嗡…… 🤫(振动模式)
 来电:嗡嗡嗡…… 🤫(振动模式)
 连续振动 3 次未接听,切换为静音模式。

 手机当前模式:SilentState
 来电:(静音模式,无声音)🤐
 手机保持静音,稍后可能会看到未接来电。

 手机当前模式:NormalState
 来电:铃铃铃!📢(正常模式)

可以看到,每次状态变化后,手机的行为也随之改变。而这些行为的逻辑并不在 Phone 类中,而是被封装在各自的状态类中。这体现了状态模式的强大之处。

局限性和权衡取舍

虽然状态模式功能强大,但也并非万能:

复杂性 vs 简洁性

在使用前要权衡问题的复杂度。只有当状态模式能降低整体复杂度时才值得引入。如果感觉它反而增加了不必要的层级结构,那也许应该退一步,采用更简单的方案。一个常用的经验法则是:当你有两三个以上的行为分支,并且这些行为可能会扩展或变化时,就可以考虑使用状态模式。

状态切换逻辑的归属问题

设计时一个常见的挑战是:状态切换的逻辑应放在哪?在我们的示例中,是由 VibrateState 自己决定切换到 SilentState。在其他设计中,可能会让 Phone(上下文)来判断是否切换,这取决于具体业务。状态模式对此并没有硬性规定,你可以根据可读性和清晰度来决定。若切换逻辑太复杂,建议写好注释或简化切换规则。

状态数量管理

如果你预计状态会爆炸式增长,那需要重新思考是否每个状态都值得用一个类。有时候看起来像 “多个状态” 的,其实可以合并处理,或者通过数据而非类来管理。比如手机的 10 级音量,不需要写 10 个状态类,而是把音量值作为一个参数放到 “正常模式” 类中就行。只有当状态间行为本质上不同时,才值得使用不同的状态类。

尽管存在这些考虑因素,状态模式仍是一种久经考验的工具。它使代码保持灵活且易于扩展。许多框架和库在内部使用状态模式或类似的概念(例如,用户界面组件通常具有启用 / 禁用 / 悬停等状态,这些状态在幕后通过状态对象或状态模式来实现)。

总结

状态设计模式能让对象更具灵活性、更易维护,它通过将状态相关的行为封装到独立的类中,实现了 “行为与状态解耦” 的目标。我们的手机模式示例展示了:一个设备如何通过切换内部状态对象,改变自己的行为(响铃、振动、静音),而无需在主类中堆积大量条件语句。

如果你正在开发一个系统,它涉及 “模式、阶段、条件行为” 的切换,状态模式是你值得掌握的一种工具。虽然初期设置可能略显麻烦,但随着项目增长,你会越来越体会到它带来的好处:关注点分离、逻辑清晰、可扩展性强。

关于本文
译者:@飘飘
作者:@Maxim Gorin
原文:maxim-gorin.medium.com/stop-writin…

MCP Server 的三种实现

MCP 规范定义了三种标准的传输机制,也就对应了三种 MCP Server 的实现方式

本文对三种数据传输方式及 Server 的实现进行了实践,代码已上传 GitHub 猛击访问

image.png

MCP 三种标准的传输机制

标准输入和标准输出的通信 STDIO

专为本地 MCP 连接设计,比如通过 node index.js 执行 MCP Server 进行交互

服务器发送事件 SSE

目前大多数远程 MCP 客户端都支持,但预计随着时间的推移将被流式 HTTP 取代。

它需要两个端点:一个用于发送请求,另一个用于接收流式响应。

可流式传输 HTTP

2025 年 3 月引入的新传输方法。它通过使用单个 HTTP 端点简化了双向通信。

目前,它正在被远程 MCP 客户端采用,预计未来将成为标准的传输方式。

为什么要替换可以看 Replace HTTP+SSE with new "Streamable HTTP" transport GitHub 上的这个 RFC

MCP 三种 Server 的实现

创建一个 server

三种实现方式只是数据传输的方式不同, server 实现是一样的

创建一个简单的 MCP server 导出

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { z } from 'zod'
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc.js'
import timezone from 'dayjs/plugin/timezone.js'

dayjs.extend(utc)
dayjs.extend(timezone)

const server = new McpServer({
  name: 'mcp-server-time',
  version: '1.0.0'
})

// 获取当前时间的工具
server.tool(
  'get_current_time',
  '获取当前时间',
  {
    timezone: z.string().optional(),
  },
  async ({ timezone }) => {
    const tz = timezone || process.env.LOCAL_TIMEZONE || 'Asia/Shanghai';
    const currentTime = dayjs().tz(tz).format('YYYY-MM-DD HH:mm:ss');
    return {
      content: [{ type: "text", text: JSON.stringify({ currentTime }, null, 2) }],
    };
  }
)

// 日期时间转换工具
server.tool(
  'convert_time',
  '在时区之间转换日期时间',
  {
    source_timezone: z.string(),
    datetime: z.string().regex(/^\d{4}-\d{1,2}-\d{1,2} ([01]\d|2[0-3]):([0-5]\d):([0-5]\d)$/, '日期时间格式无效,应为 YYYY-MM-DD HH:mm:ss'),
    target_timezone: z.string(),
  },
  async ({ source_timezone, datetime, target_timezone }) => {
    const sourceTime = dayjs.tz(datetime, source_timezone);
    const convertedTime = sourceTime.clone().tz(target_timezone).format('YYYY-MM-DD HH:mm:ss');
    return {
      content: [{ type: "text", text: JSON.stringify({ convertedTime }, null, 2) }],
    };
  }
)

server.tool(
  'get_text',
  '返回测试文本',
  {},
  async () => {
    const text = '在这个充满变化的时代,每一天都带来了新的机遇与挑战。科技的发展不仅改变了我们的生活方式,也让我们的思维方式不断更新。面对未知,我们或许会感到迷茫,但正是这种探索精神推动着社会不断进步。从传统的理念到现代的创新,每一次转变都蕴含着无限可能。测试文本的存在,正是为了验证系统的生成和处理能力。相信在不断的尝试中,我们能够找到更好的解决方案,为未来的发展铺平道路。'
    return {
      content: [{ type: "text", text: text }],
    };
  }
)

export default server

标准输入和标准输出的通信 STDIO

#!/usr/bin/env node
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import server from './mcpServerTest.js'

// 启动服务器
async function runServer() {
  // 使用 StdioServerTransport 标准输入输出
  const transport = new StdioServerTransport()
  await server.connect(transport)
  console.error('获取当前时间和时区转换的 MCP 服务器已在 stdio 上启动')
}

runServer().catch((error) => {
  console.error('启动服务器时出错:', error)
  process.exit(1)
})

测试

执行 npx @modelcontextprotocol/inspector node transports/dist/stdio.js 启动测试页面

image.png

访问 http://127.0.0.1:6274

image.png

注意这里的配置,确认没有问题点击连接即可

iShot_2025-05-14_11.40.50.gif

服务器发送事件 SSE

这是一个简单的 mcp sse 服务的实现,在生产中可能需要继续完善代码比如对跨域、鉴权的处理

import express from "express";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import server from './mcpServerTest.js'
import {addInfoLog, addWarnLog, addSuccessLog} from './utils.js'

const app = express();
app.use(express.json());

// 存储连接
const connections = new Map<string, SSEServerTransport>();

app.get('/sse', async (req, res) => {

  addSuccessLog('客户端连接参数:', req.query)

  // 创建 sse 传输
  const transport = new SSEServerTransport('/messages', res);
  const sessionId = transport.sessionId

  addInfoLog(`新的 SSE 连接建立: ${sessionId}`)  

  // 注册连接
  connections.set(sessionId, transport);
  
  res.on("close", () => {
    connections.delete(sessionId);
    addInfoLog(`SSE 连接关闭: ${sessionId}`)  
  });
  
  // 将传输对象与MCP服务器连接
  await server.connect(transport);
  addSuccessLog(`MCP 服务器连接成功: ${sessionId}`)  
});

// 旧消息端点
app.post('/messages', async (req, res) => {
  const sessionId = req.query.sessionId as string;

  addInfoLog(`收到客户端消息: ${sessionId}`)  
  
  console.log('query',req.query, '\r\n')
  console.log('body',req.body, '\r\n')
  console.log('params',req.params, '\r\n')

  // 获取连接
  const transport = connections.get(sessionId)
  if (transport) {
    await transport.handlePostMessage(req, res, req.body);
  } else {
    addWarnLog(`未找到活跃的 ${sessionId} 连接`) 
    res.status(400).send(`未找到活跃的 ${sessionId} 连接`);
  }
});


// 启动服务器
const port = process.env.PORT || 9001;
app.listen(port, () => {
  addSuccessLog(`MCP SSE 服务器已启动:`, `http://localhost:${port}`)
  addInfoLog('SSE 连接端点:', `http://localhost:${port}/sse`)
  addInfoLog('SSE 消息处理端点:', ` http://localhost:${port}/messages`)

  console.log('=========================== success ===========================\r\n')
});

测试

执行 node dist/sse.js 启动服务

image.png

执行 npx @modelcontextprotocol/inspector http://127.0.0.1 启动测试页面

image.png

访问 http://127.0.0.1:6274

image.png

这里需要选择 sse 和确认 sse 服务的地址及端口,确认没有问题后点击连接即可

iShot_2025-05-14_13.44.16.gif

可流式传输 HTTP

这是一个简单的 Streamable HTTP mcp 服务实现,其实是官方仓库的示例代码 (看起来有点绕🤪) 加了一些注释便于理解

import express from "express";
import { randomUUID } from "node:crypto";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"
import server from './mcpServerTest.js'
import {addInfoLog, addSuccessLog} from './utils.js'
import pc from 'picocolors'

const app = express();
app.use(express.json());

/**
 * POST 请求:创建新的传输实例并保存到 transports 映射(以 sessionId 为键)。
 * GET 请求(SSE):通过已保存的传输实例推送数据流到客户端。
 * DELETE 请求:通过已保存的传输实例终止会话,断开连接。
 */

// 保存 会话 id 到 传输实例的映射 
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};

// Handle POST requests for client-to-server communication
app.post('/mcp', async (req, res) => {
  // Check for existing session ID
  const sessionId = req.headers['mcp-session-id'] as string | undefined;
  let transport: StreamableHTTPServerTransport;

  if (sessionId && transports[sessionId]) {
    // 如果传输实例已经存在则直接使用
    transport = transports[sessionId];
  } else if (!sessionId && isInitializeRequest(req.body)) {
    // isInitializeRequest 判断 是否是一个合法的 mcp 的请求
    // 创建一个新的传输实例
    transport = new StreamableHTTPServerTransport({
      sessionIdGenerator: () => randomUUID(),
      onsessioninitialized: (sessionId) => {
        // 通过会话 id 保存传输实例
        transports[sessionId] = transport;
        addInfoLog(`创建传输实例成功 ${sessionId}`)
      }
    });

    // 接收到 DELETE 请求关闭连接
    transport.onclose = () => {
      if (transport.sessionId) {
        delete transports[transport.sessionId];
      }
    };

    await server.connect(transport);
    addSuccessLog(`MCP 服务器连接成功: ${sessionId}`)  
  } else {
    // 无效的请求
    res.status(400).json({
      jsonrpc: '2.0',
      error: {
        code: -32000,
        message: '错误请求:未提供有效的会话 ID',
      },
      id: null,
    });
    return;
  }

  // 处理请求 这里 根据 get、delete 做对应的处理
  // 如果请求是 GET,通常会被解释为服务器推送通知(SSE)。
  // 如果请求是 DELETE,通常会被解释为终止会话(断开连接)。
  await transport.handleRequest(req, res, req.body);
});

// 处理 get 和 delete 请求
async function handleSessionRequest(req: express.Request, res: express.Response) {
  const sessionId = req.headers['mcp-session-id'] as string | undefined;
  if (!sessionId || !transports[sessionId]) {
    res.status(400).send('会话 ID 无效或缺失');
    return;
  }
  
  // 获取传输实例执行对应的的操作
  const transport = transports[sessionId];
  await transport.handleRequest(req, res);
}

// 通过 SSE 处理服务器到客户端通知的 GET 请求
app.get('/mcp', handleSessionRequest);

// 处理会话终止的 DELETE 请求
app.delete('/mcp', handleSessionRequest);

// 启动服务器
const port = process.env.PORT || 9002;
app.listen(port, () => {
  addSuccessLog(`MCP Streamable 服务器已启动: ${pc.green(`http://localhost:${port}`)}`)

  console.log('=========================== success ===========================\r\n')
});

测试

执行 node dist/streamable.js 启动服务

image.png

执行 npx @modelcontextprotocol/inspector http://127.0.0.1 启动测试页面

image.png

访问 http://127.0.0.1:6274

这里需要选择 Streamable HTTP 和确认服务的地址及端口,确认没有问题后点击连接即可

image.png

iShot_2025-05-14_17.24.21.gif

server 同时兼容 SSE 与 Streamable 的写法

把 sse 和 Streamable HTTP 的 server 实现组合起来,没有什么魔法!

MCP 三种 Client 的实现

三种客户端的实现区别不大, MCP SDK 导出了三个文件对应三种 server 的实现

import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";

import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

STDIO

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { addInfoLog, addErrLog, addSuccessLog } from './utils.js'

async function createStdioClient() {
  // 创建 client
  const client = new Client({
    name: 'stdio-client',
    version: '1.0.0',
  });

  try {
    const sseTransport = new StdioClientTransport({
      command: 'node',
      args: ['../transports/dist/stdio.js']
    });

    // 连接到 MCP 服务
    await client.connect(sseTransport);

    addSuccessLog(' MCP 服务 连接成功!')

    // 获取 工具列表
    const toolInfo = await client.listTools()

    addInfoLog('MCP 工具信息', toolInfo)

    // 调用生成文本工具
    const callToolInfo = await client.callTool({
      "name": "get_text",
      "arguments": {}
    })

    addInfoLog('get_text 返回信息', callToolInfo)
  } catch (error) {
    addErrLog('MCP 客户端错误', error);
  }
}

createStdioClient()

image.png

SSE

这里需要先启动 sse serevr node ./dist/sse.js

image.png

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { addInfoLog, addErrLog, addSuccessLog } from './utils.js'

let client: Client | undefined = undefined
const baseUrl = new URL('http://localhost:9001/sse');

async function createSseClient() {
  client = new Client({
    name: 'sse-client',
    version: '1.0.0',
  });

  try {
    const sseTransport = new SSEClientTransport(baseUrl, {
      requestInit: {
        headers: {
          // 这里的参数可以在 messages req.headers 中获取
          'X-Custom-Param': 'custom_value'
        },
      }
    });

    // 连接到 MCP 服务
    await client.connect(sseTransport);

    addSuccessLog(' MCP 服务 连接成功!')

    // 获取 工具列表
    const toolInfo = await client.listTools()

    addInfoLog('MCP 工具信息', toolInfo)

    // 调用生成文本工具
    const callToolInfo = await client.callTool({
      "name": "get_text",
      "arguments": {}
    })

    addInfoLog('get_text 返回信息', callToolInfo)
  } catch (error) {
    addErrLog('MCP 客户端错误', error);
  }

  // 关闭连接
  // client.close()
}

createSseClient()

image.png

Streamable HTTP

实现与 SSE 几乎相同将 SSEClientTransport 替换为 StreamableHTTPClientTransport 即可

如果失败并出现 4xx 错误时尝试使用 SSE 客户端

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

let client: Client | undefined = undefined
const baseUrl = new URL('http://localhost:9001/sse');

async function createStreamableHttpClient() {
  try {
    client = new Client({
      name: 'streamable-http-client',
      version: '1.0.0'
    });

    const transport = new StreamableHTTPClientTransport(baseUrl);
    await client.connect(transport);

    console.log("Connected using Streamable HTTP transport");
  } catch (error) {
    // 如果失败并出现 4xx 错误,请尝试较旧的 SSE 传输 
    
    console.log("Streamable HTTP connection failed, falling back to SSE transport");

    // todo 使用 sse client 与 server 交互
  }
}

createStreamableHttpClient()

参考文章

modelcontextprotocol typescript-sdk

cloudflare / Agents 代理 / Transport 传输

开发 SSE 类型的 MCP 服务

前端周刊第414期(2025年5月12日–5月18日)

image.png

💬 推荐语

这周的前端更新节奏不减,CSS 世界持续花活创新,从 SVG 转形状到滚动驱动动画;React 则再掀讨论热度,RSC 的边界和“Context 渲染过多”的争议也被一一拆解;工具链方面,ESLint 终于开始原生支持 HTML lint,让代码质量保障更进一步。总之,如果你想知道 2025 年前端该怎么玩,这一周的内容,你值得仔细看看。

🧠 博主点评

从这期内容来看,前端生态进入了“工具整合与语义回归”的新阶段。像 html-eslint、Rslib、View Transitions 的持续演化,说明前端正往“表达更清晰、体验更细腻”的方向进化。而 React 圈依旧火热,RSC(React Server Component)与自定义渲染器的深水区讨论,表明框架开发者正在探索架构的下一个层级。

🗂️ 本期精选目录

Web 开发

🔹如何打造新的表单控件:从底层构建一个原生表单控件,完整介绍交互设计与可访问性实现。

🔹从 SplitText 到 MorphSVG:5 个免费的 GSAP 创意动画 Demo:动效爱好者不容错过,展示如何用 GSAP 插件制作高级动画。

🔹GitHub Copilot 入门:用 AI 构建 React 应用:GitHub 官方出品的 Copilot 教程,带你从 0 到 1 搭建 React 应用。

🔹如何用 Lighthouse 优化网页性能:深入解读 Lighthouse 报告并指导优化实践。

工具

🔹ESLint 支持 HTML 代码校验了!:通过 html-eslint 插件,HTML 文件也能纳入 ESLint 检查体系。

🔹Rslib:基于 Rspack 打包构建的库开发方案:来自 Rspack 团队的新工具,用于快速构建 JS/TS 库。

可访问性(Accessibility)

🔹HTML 邮件可访问性年度报告:全面分析 2025 年 HTML 邮件在可访问性方面的表现与改进方向。

🔹结合 Next.js 和 axe DevTools 打造可访问应用:实战指导,帮助开发者更容易集成无障碍功能。

🔹可访问性之道:用现代方式学习 React/Angular/Vue 的无障碍开发:一套系统性学习最佳实践的在线课程资源。

🔹三款工具助你解决常见 WCAG 2.2 可访问性错误:快速修复网页常见可访问性问题的利器。

CSS

🔹容器查询中的“元素外部空间”检测:扩展容器查询的用法边界,考虑“外部空间”布局。

🔹SVG 转 CSS 形状生成器:支持将 SVG 图形转为 clip-path 等 CSS 样式,提升创作效率。

🔹纯 CSS 实现无限滚动画廊:炫酷滚动展示画廊组件,无需 JS 支持。

🔹用 clip-path: shape() 绘制花朵图形:CSS 创意玩法再升级。

🔹百分比高度的谜团:CSS 高度是如何计算的:解析 percentage-based height 的常见误解。

🔹Scroll 驱动动画在 CSS 轮播图中的应用:结合 CSS Scroll Timeline 实现响应式动效。

🔹平滑动画 border-radius:View Transitions 应用探索:借助 View Transitions API 实现更自然的样式过渡。

🔹CSS Masking:为动画增加一维空间感:遮罩动画的高级进阶玩法。

🔹CSS 如何自动选择对比色:基于背景自动生成可读文本颜色,提升用户体验。

🔹初探 View Transitions:前端视觉过渡的实战尝试。

JavaScript & React

🔹我开始喜欢上 Generator 的使用体验了:作者谈为何 Generator 正逐渐变得顺手。

🔹JavaScript 正则表达式终极指南:详解语法、边界情况与调试技巧。

🔹JS 编译提示:是什么、什么时候用?:深入探讨编译优化 hint 与性能提升的关系。

🔹Angular Can I Use:像 CanIUse 一样检查 Angular API 在不同版本的支持情况。

🔹如何在 React 中集成 Rive 动效:带你了解 Rive 在实际项目中如何协作。

🔹React 真酷,你居然不知道?:一篇不拘一格的 React 使用体验文章,风格狂野但不失干货。

🔹RSC 的极限:一位开发者的实践历程:揭示 React Server Components 的优势与局限。

🔹如何构建自定义 React 渲染器:适合探索底层实现的开发者阅读。

🔹React Three Fiber:在 Web 上实现 3D 渲染:深入理解如何用 React 组织 WebGL 渲染。

🔹别再说 Context 导致频繁渲染了:深入分析 React Context 的性能表现与常见误解。

小结

这期内容可以看作是前端的“全生态热身”:从底层 HTML 与 ESLint 扩展,到高级动画、CSS 语义增强、React 渲染机制探索,一方面是性能与体验的持续优化,另一方面是对复杂架构和可访问性的新尝试。值得点赞的是,多个工具和文档都在“为开发者赋能”上下了不少功夫。

OK,以上就是本次分享,欢迎加我威 atar24,备注「前端周刊」,我会邀请你进交流群👇

🚀 每周分享技术干货
🎁 不定期抽奖福利
💬 有问必答,群友互助

10+ 代码案例快速掌握 CSS quotes 属性

CSS 的 quotes 属性用于设置浏览器应如何渲染引号,这些引号会自动添加到 HTML 的 <q> 标签中,或通过 CSS content 属性的 open-quotes、close-quotes 值(或使用 no-open-quote、no-close-quote 值省略)添加。

浏览器会在 <q> 标签的开头和结尾插入引号,并为 content 属性的 open-quoteclose-quote 值添加引号。每一对开始或结束引号都会根据嵌套深度,从 quotes 属性的值中选择对应的字符串进行替换;如果 quotes 显式设置为 auto 或最终解析为 auto ,则使用的引号取决于语言设置。

open-quoteclose-quote 设置为使用法语引号格式:

quotes: "«" "»";

设置两级引号样式:

quotes: "«" "»" "‹" "›";
/* 
第一级:« »
第二级:‹ ›
*/

none

quotes 属性的值为 none 时表示不添加引号。

即,不会为 <q> 标签添加引号。content 属性值为 open-quoteclose-quote 时,也不会添加引号,就像是 content 属性值为 no-open-quoteno-close-quote 时一样。

<style>
  .test {
    quotes: none;
  }
</style>

<div class="test">
  <q>你是人间四月天</q>
</div>

没有生成引号:

a8.png

<style>
  .test {
    quotes: none;
  }

  .quotes::before {
    content: open-quote;
  }
  .quotes::after {
    content: close-quote;
  }
</style>

<div class="test">
  <span class="quotes">你是人间四月天</span>
</div>

没有生成引号:

a9.png

[<string> <string>]+

quotes 属性的值类型:[<string> <string>]+

定义一组或多组用于开头引号和结尾引号的标记值。每组包含两个值:第一个值作为 open-quote 的标记,第二个值作为 close-quote 的标记。

第一组代表最外层的引号样式。若存在第二组,则代表第一级嵌套引号样式。后续组别依次用于更深层级的嵌套引号。若引号嵌套深度超过定义的组数,则重复使用最后一组标记值。

具体使用哪组引号标记取决于当前引号的深度(即嵌套层级),若深度为 0,则使用第一对引号;若深度为 1,则使用第二对引号,依此类推。

<style>
.test {
  quotes: "«" "»" "[" "]";
}
</style>

<div class="test">
  <!-- 若引号嵌套深度超过定义的组数,则重复使用最后一组标记值 -->
  <q>
    太空研究
    <q>
    正在进入一个新阶段,随着
    <q>
        火箭的负载越来越大,以及发射成本的降低和发射频率的提高,我们现在可以把真正大型
    </q>
    的仪器发射到
    </q>
    太空。
  </q>
</div>

a1.png

<!--
 具体使用哪组引号标记取决于当前引号的深度(即嵌套层级),
 若深度为 0,则使用第一对引号;若深度为 1,则使用第二对引号,依此类推 
-->
<style>
.test {
  quotes: "«" "»" "‹" "›" "[" "]";
}
</style>


<div class="test1">
  <q> <!-- 第 1 级 --><q> <!-- 第 2 级 --><q>人间</q> <!-- 第 3 级 -->
      四月
    </q></q>
</div>

a2.png

auto

CSS 的 quotes 属性的默认值是 auto 。浏览器会根据元素继承的语言(即通过父元素或祖先元素设置的 lang 属性),自动选用符合该语言排版规范的引号样式。

<style>
  q {
    quotes: auto;
  }
  li:not(:last-of-type) {
    border-bottom: 1px solid;
  }
</style>

<ul>
  <!-- 法语的引号 -->
  <li lang="fr">
    <q>Ceci est une citation française.</q>
  </li>
  <!-- 俄语的引号 -->
  <li lang="ru">
    <q>Это русская цитата</q>
  </li>
  <!-- 德语的银行 -->
  <li lang="de">
    <q>Dies ist ein deutsches Zitat</q>
  </li>
  <!-- 英语的引号 -->
  <li lang="en">
    <q>This is an English quote.</q>
  </li>
</ul>

如果 CSS 的 quotes 属性设置的值是非法的,则浏览器会将quotes 属性回退到 auto 来处理

<style>
  .test {
    /* 没有成对,是非法的 quotes 属性值 */
    quotes: "«" "»" "[";
  }
</style>

<div class="test">
  <q>
    太空研究
    <q>
      正在进入一个新阶段,随着
      <q>
        火箭的负载越来越大,以及发射成本的降低和发射频率的提高,我们现在可以把真正大型
      </q>
      的仪器发射到
    </q>
    太空。
  </q>
</div>

对于非法的 quotes 属性值,最终浏览器回退到了默认值,即 auto

a3.png

content 属性生成的内容

除了使用 <q> 标签添加引号外,也可通过为特定类名的元素内容前后添加 ::before::after 伪元素来实现引号的插入。

contentopen-quote 时,内容替换为 quotes 属性中定义的开引号

contentclose-quote 时,内容替换为 quotes 属性中定义的闭引号

<style>
  .quote {
    quotes: '"' '"' "'" "'";
  }
  .quote::before {
    content: open-quote;
  }
  .quote::after {
    content: close-quote;
  }
</style>

<p>
  <span class="quote">I should be using quotes</span>, I thought,
  <span class="quote">But why use semantic HTML elements when I can add classes to
    <span class="quote">ALL THE THINGS!</span>?
  </span>
</p>

a4.png

open-quoteclose-quote 值相对的,有 no-open-quoteno-close-quote

content 值为 no-open-quote 时,隐藏当前位置本应出现的开引号

content 值为 no-close-quote 时,隐藏当前位置本应出现的闭引号

<style>
  .quote {
    quotes: "《" "》" "〈" "〉";
  }
  .quote::before {
    content: open-quote;
  }
  .quote::after {
    content: close-quote;
  }
</style>

<div>
  正常引用
  <span class="quote">
    第一层<span class="quote">第二层</span>
  </span>
</div>

a5.png

隐藏开引号的例子:

<style>
  .quote {
    quotes: "《" "》" "〈" "〉";
  }
  .quote::before {
    content: open-quote;
  }
  .quote::after {
    content: close-quote;
  }
  .quote.no-open::before {
    content: no-open-quote;
  }
</style>

<div>
  正常引用
  <span class="quote no-open">
    第一层<span class="quote">第二层</span>
  </span>
</div>

a6.png

文本引号与空引号的使用

CSS 的 quotes 属性值也可以是非引号字符,比如使用 open-quote 标记说话者的身份,此时由于 open-quote 未设置为正常的开引号,因此,close-quote 通常会保持为空值。

<style>
  [data-speaker="karen" i] {
    quotes: "She said: " "";
  }
  [data-speaker="chad" i] {
    quotes: "He said: " "";
  }
  [data-speaker="pat" i] {
    quotes: "They said: " "";
  }
  [data-speaker] q {
    quotes: auto;
  }
</style>

<ul>
  <li><q data-speaker="karen">Hello</q></li>
  <li><q data-speaker="chad">Hi</q></li>
  <li><q data-speaker="karen">this conversation is not interesting</q></li>
  <li>
    <q data-speaker="pat"
      >OMG! <q>Hi</q>? Seriously? at least <q>hello</q> is five letters long.</q
    >
  </li>
</ul>

a7.png

上面的例子也可以用 content 属性来代替 <q> 标签实现:

<style>
  [data-speaker="karen" i] {
    quotes: "She said: " "";
  }
  [data-speaker="karen" i]::before {
    content: open-quote;
  }
  [data-speaker="karen" i]::after {
    content: close-quote;
  }

  [data-speaker="chad" i] {
    quotes: "He said: " "";
  }
  [data-speaker="chad" i]::before {
    content: open-quote;
  }
  [data-speaker="chad" i]::after {
    content: close-quote;
  }

  [data-speaker="pat" i] {
    quotes: "They said: " "";
  }
  [data-speaker="pat" i]::before {
    content: open-quote;
  }
  [data-speaker="pat" i]::after {
    content: close-quote;
  }

  [data-speaker] span {
    quotes: auto;
  }
  .quotes::before {
    content: open-quote;
  }
  .quotes::after {
    content: close-quote;
  }
</style>

<ul>
  <li><div data-speaker="karen">Hello</div></li>
  <li><div data-speaker="chad">Hi</div></li>
  <li><div data-speaker="karen">this conversation is not interesting</div></li>
  <li>
    <div data-speaker="pat"
      >OMG! 
      <span class="quotes">Hi</span>? Seriously? at least 
      <span class="quotes">hello</span> is five letters long.
    </div>
  </li>
</ul>

总结

CSS 的 quotes 属性在日常的开发中比较少用。

quotes 属性用于设置浏览器应如何渲染引号,这些引号会自动添加到 HTML 的 <q> 标签中,或通过 CSS content 属性的 open-quotesclose-quotes 值添加。

CSS content 属性值设置为 no-open-quoteno-close-quote 省略引号。

参考

quotes

JavaScript篇:for...in vs for...of:遍历JavaScript数据的正确姿势,你踩坑了吗?

        大家好,我是江城开朗的豌豆,一名拥有6年以上前端开发经验的工程师。我精通HTML、CSS、JavaScript等基础前端技术,并深入掌握Vue、React、Uniapp、Flutter等主流框架,能够高效解决各类前端开发问题。在我的技术栈中,除了常见的前端开发技术,我还擅长3D开发,熟练使用Three.js进行3D图形绘制,并在虚拟现实与数字孪生技术上积累了丰富的经验,特别是在虚幻引擎开发方面,有着深入的理解和实践。

        我一直认为技术的不断探索和实践是进步的源泉,近年来,我深入研究大数据算法的应用与发展,尤其在数据可视化和交互体验方面,取得了显著的成果。我也注重与团队的合作,能够有效地推动项目的进展和优化开发流程。现在,我担任全栈工程师,拥有CSDN博客专家认证及阿里云专家博主称号,希望通过分享我的技术心得与经验,帮助更多人提升自己的技术水平,成为更优秀的开发者。

大家好,我是前端开发工程师小杨。今天咱们来聊聊 JavaScript 里两个看似相似但完全不同的循环语句——for...in 和 for...of

你是不是也曾经纠结过:

  • for...in 和 for...of 到底该用哪个?
  • 为什么有时候遍历数组会得到奇怪的结果?
  • 遍历对象时,哪种方式更安全?

别急,看完这篇你就全懂了!


1. 一句话总结区别

  • for...in → 遍历对象的可枚举属性(包括原型链)
  • for...of → 遍历可迭代对象的值(Array、Map、Set等)

简单来说:

  • for...in 适合对象(但要注意原型链问题)
  • for...of 适合数组、字符串等可迭代结构

2. for...in:遍历对象的“家底”

for...in 会遍历对象的所有可枚举属性,包括继承自原型链的属性

🌰 举个栗子:遍历对象

const user = { name: '我', age: 25, role: 'developer' };

for (const key in user) {
  console.log(key, user[key]); 
}
// 输出:
// name 我
// age 25
// role developer

看起来没问题?但如果原型链上有属性呢?

Object.prototype.customProp = '来自原型链';

for (const key in user) {
  console.log(key); 
}
// 输出:
// name
// age
// role
// customProp (意外多了一个属性!)

解决方案:用 hasOwnProperty 过滤

for (const key in user) {
  if (user.hasOwnProperty(key)) { // 只遍历自己的属性
    console.log(key);
  }
}
// 输出:
// name
// age
// role

⚠️ 注意:for...in 不保证顺序!

  • 对象的属性遍历顺序不固定(尤其是数字键时)
  • 如果需要顺序,改用 Object.keys() + for...of

3. for...of:专治可迭代对象

for...of 专门用于遍历可迭代对象(Iterable) ,比如:
✅ Array
✅ String
✅ Map / Set
✅ NodeList(DOM 元素集合)
✅ arguments 对象

🌰 举个栗子:遍历数组

const skills = ['JS', 'CSS', 'React'];

for (const skill of skills) {
  console.log(skill);
}
// 输出:
// JS
// CSS
// React

比 for...in 更安全,不会遍历到奇怪的东西!

🌰 再举个栗子:遍历字符串

const name = '我';

for (const char of name) {
  console.log(char);
}
// 输出:
// 我

🚫 for...of 不能直接遍历普通对象!

const user = { name: '我' };

for (const val of user) { // ❌ 报错!
  console.log(val);
}
// TypeError: user is not iterable

解决方案:用 Object.values() / Object.entries()

for (const val of Object.values(user)) {
  console.log(val); // 输出:我
}

4. 对比总结

特性 for...in for...of
适用对象 普通对象(会遍历原型链) 可迭代对象(Array、Map、Set等)
返回值 键名(key) 值(value)
顺序保证 ❌ 不保证顺序(尤其数字键) ✅ 保证顺序
原型链问题 可能遍历到继承属性(需过滤) 不会遍历原型链
适用场景 遍历对象属性 遍历数组、字符串等

5. 最佳实践

✅ 用 for...in 时:

  • 一定要加 hasOwnProperty 检查,避免原型链污染
  • 不要用于数组(顺序不可控,可能遍历到非数字键)

✅ 用 for...of 时:

  • 优先用于数组、字符串、Map、Set
  • 普通对象先用 Object.keys() / Object.values() 转换

6. 终极选择指南

场景 推荐方式
遍历对象属性 for...in + hasOwnProperty
遍历数组值 for...of
遍历字符串字符 for...of
遍历Map/Set for...of
需要索引的数组遍历 for 循环 或 forEach

7. 总结

  • for...in → 遍历对象属性(小心原型链)
  • for...of → 遍历可迭代对象的值(数组、字符串等)
  • 普通对象想用 for...of  先用 Object.keys() / Object.values() 转换

🚀 现在你彻底搞懂了吧?下次写循环时别再选错了!

你在使用 for...in 或 for...of 时踩过什么坑?欢迎评论区分享!  👇

JavaScript篇:"三次握手和四次挥手:TCP 连接的‘恋爱仪式’全解析!"

        大家好,我是江城开朗的豌豆,一名拥有6年以上前端开发经验的工程师。我精通HTML、CSS、JavaScript等基础前端技术,并深入掌握Vue、React、Uniapp、Flutter等主流框架,能够高效解决各类前端开发问题。在我的技术栈中,除了常见的前端开发技术,我还擅长3D开发,熟练使用Three.js进行3D图形绘制,并在虚拟现实与数字孪生技术上积累了丰富的经验,特别是在虚幻引擎开发方面,有着深入的理解和实践。

        我一直认为技术的不断探索和实践是进步的源泉,近年来,我深入研究大数据算法的应用与发展,尤其在数据可视化和交互体验方面,取得了显著的成果。我也注重与团队的合作,能够有效地推动项目的进展和优化开发流程。现在,我担任全栈工程师,拥有CSDN博客专家认证及阿里云专家博主称号,希望通过分享我的技术心得与经验,帮助更多人提升自己的技术水平,成为更优秀的开发者。

大家好,我是前端开发工程师小杨。今天咱们来聊聊 TCP 连接的建立和断开,也就是传说中的三次握手(Three-way Handshake)四次挥手(Four-way Handshake)

你可能觉得这是后端的事情,但前端也得懂点网络知识,尤其是调试接口慢、连接异常时,知道底层原理才能更快定位问题!


1. TCP 连接:像谈恋爱一样严谨

TCP(传输控制协议)是一种可靠的传输协议,就像两个人谈恋爱,得先确认彼此心意(三次握手),分手时也要好好告别(四次挥手)。

🌰 举个栗子:我和服务器谈恋爱

  • 三次握手:我和服务器互相确认,建立稳定连接
  • 四次挥手:我和服务器和平分手,确保数据不丢失

2. 三次握手:TCP 的“确认眼神”

三次握手的目标是确保客户端和服务器都能正常收发数据

流程:

  1. 第一次握手(SYN)

    •  → 服务器:SYN=1, seq=x(“嗨,能听到我吗?”)
    • 进入 SYN_SENT 状态
  2. 第二次握手(SYN+ACK)

    • 服务器 → 我:SYN=1, ACK=1, seq=y, ack=x+1(“听到了!你也能听到我吗?”)
    • 服务器进入 SYN_RCVD 状态
  3. 第三次握手(ACK)

    •  → 服务器:ACK=1, seq=x+1, ack=y+1(“听到了!咱们开始聊天吧!”)
    • 双方进入 ESTABLISHED 状态,连接建立!

为什么是三次?两次不行吗?

  • 两次握手:服务器无法确认我是否能收到它的回复(可能我早就断开了)
  • 三次握手:确保双方都能正常收发数据,避免资源浪费

3. 四次挥手:TCP 的“和平分手”

TCP 连接断开时,需要确保数据全部传输完毕,不能突然“拉黑”。

流程:

  1. 第一次挥手(FIN)

    •  → 服务器:FIN=1, seq=u(“我要走了,拜拜!”)
    • 进入 FIN_WAIT_1 状态
  2. 第二次挥手(ACK)

    • 服务器 → 我:ACK=1, ack=u+1(“好的,等我处理完最后的数据”)
    • 服务器进入 CLOSE_WAIT 状态
    • 进入 FIN_WAIT_2 状态
  3. 第三次挥手(FIN)

    • 服务器 → 我:FIN=1, ACK=1, seq=v, ack=u+1(“我也准备好了,再见!”)
    • 服务器进入 LAST_ACK 状态
  4. 第四次挥手(ACK)

    •  → 服务器:ACK=1, ack=v+1(“好的,正式断开!”)
    • 进入 TIME_WAIT 状态(等待 2MSL 后彻底关闭)

为什么是四次?三次不行吗?

  • 服务器可能还有数据要发送,不能直接断开
  • 四次挥手确保双方都完成数据收发,避免数据丢失

4. 常见面试题

Q1:为什么 TIME_WAIT 要等 2MSL?

  • MSL(Maximum Segment Lifetime)  是数据包在网络中的最大存活时间

  • 等待 2MSL 是为了:

    • 确保最后一个 ACK 到达服务器(如果丢失,服务器会重发 FIN)
    • 让旧连接的数据包彻底消失,避免影响新连接

Q2:握手能变成两次吗?挥手能变成三次吗?

  • 握手不能两次(无法确认客户端能收到服务器的回复)
  • 挥手有时能三次(如果服务器没有数据要发,FIN 和 ACK 可以合并)

5. 总结

  • 三次握手:建立连接,确保双方都能通信
  • 四次挥手:断开连接,确保数据不丢失
  • TIME_WAIT:防止旧连接干扰新连接

🚀 前端也要懂点网络知识!  下次遇到接口慢、连接异常时,想想是不是 TCP 的“恋爱仪式”出了问题~

你有遇到过 TCP 相关的问题吗?欢迎评论区交流!  👇

栗子前端技术周刊第 81 期 - Parcel v2.15.0、Rslib、React Router 7.6...

🌰栗子前端技术周刊第 81 期 (2025.05.12 - 2025.05.18):浏览前端一周最新消息,学习国内外优秀文章视频,让我们保持对前端的好奇心。

📰 技术资讯

  1. Parcel v2.15.0:Parcel v2.15.0 版本带来两项重大更新:采用 Rust 重构 HTML/SVG 转换器与压缩器;依赖项精简 25%,node_modules 体积缩减 45%。

  2. Rslib:Rslib 由字节跳动 Web Infra 团队开发,能够帮助开发者以简单直观的方式创建 JavaScript 库和 UI 组件库,并享受 Rspack 和 Rsbuild 带来的极致开发体验。

  3. React Router 7.6:React Router 7.6 发布,更新内容包括:新增 routeDiscovery 配置项,支持更灵活地控制“懒路由发现”功能;类型系统智能升级,实现未来特性标志(future flags)开启下的自动类型推导等等。

📒 技术文章

  1. Categorize Your Dependencies:分类管理项目依赖项 - Anthony Fu 分享如何运用命名目录对依赖项进行分类。

  2. Best Practices for Creating a Modern npm Package:现代 npm 包开发最佳实践指南 - 这是一份与时俱进的分步教程,完整演示如何运用当下最新规范创建 npm 包。

  3. 编程常用的 MCP Server,用自然语言写代码:本文介绍了编程常用的 MCP Server,可借助 LLM + MCP 用自然语言完成编程相关操作。常用 MCP Server 包括 fileSystem MCP server、fetch MCP server、github MCP server 等等。

🔧 开发工具

  1. jsdiff 8.0:JavaScript 文本差异比对库。
image-20250517220447336
  1. Flatlogic Templates:提供免费的网页与移动端模板。
image-20250517223500154
  1. tscircuit:用 React 开发电子电路项目。

🚀🚀🚀 以上资讯文章选自常见周刊,如 JavaScript Weekly 等,周刊内容也会不断优化改进,希望你们能够喜欢。

💖 欢迎关注微信公众号:栗子前端

撸一个小程序运行容器

作者:蔡钧

一、背景

现在几大热门的APP都能运行小程序,每个APP都有自己的小程序开发工具(有谁懂。。。。。。),每个APP的小程序都有自己的“语法”,虽然有细微的差别但大差不差,那如果我希望自己写的APP也能拥有小程序运行能力的话应该怎么做呢?

叠甲:本文实现思路参考smallapp,在其基础上解决了一些bug和删减,能跑起一个demo,主要讲解实现思路,中间还有很多可以优化和拓展的东西就不展开了。

再叠:微信有自己的WMPF,它更偏向应用型,再拆一层就是我们要做的东西。

二、目标

构建一个 "可插拔" 的小程序容器,支持多平台运行(Web/App/Desktop),并且展示效果一致。

三、架构设计

容器层需要关注的核心

  • 数据-视图,正常渲染
  • 基础组件
  • 事件处理
  • 生命周期

四、容器层实现

小程序转换层

我们把webview作为运行容器的宿主,把小程序语法转换成html即可

画板

css

通过postcss和postcss-import打包成一个css文件

export class WxssFileNode extends FileNode {
  constructor(path, type, name, tag) {
    super(path, type, name, tag);
    this.type = "wxss";
  }
  async transform(input) {
    this.ast = input;
    const that = this;
    const res = await postcss([
      require("postcss-import")({
        resolve(id) {
          const url = resolve(that.path, "../", id);
          return url;
        },
      }),
    ]).process(input, {
      from: that.path,
      to: that.path,
    });
    this.code = res.css;
  }
}

js

通过esbuild把js文件打包成一个bundle

export class JsFileNode extends FileNode {
  constructor(path, type, name, tag) {
    super(path, type, name, tag);
    this.type = "js";
  }

  async transform() {
    const out = await build({
      entryPoints: [this.path],
      bundle: true,
      format: "esm",
      sourcemap: false,
      write: false,
      outdir: "out",
    });

    this.code = String.fromCharCode.apply(null, out.outputFiles[0].contents);
  }
}

wxml

逐行处理wxml内容

// 0. 原始状态
<view class="wrap">{{arr.length}}</view>
<view wx:for="{{arr}}">
    <text>{{item.a}}:{{item.b}}:{{index}}</text>
</view>
<view wx:if="{{arr.length > 5}}">show</view>
<view wx:elseif="{{arr.length > 2}}">show2</view>
<view wx:else>hidden</view>
<button bind:tap="add">add</button>

// 1. 状态处理后的WXML
<view class="wrap">{{state.arr.length}}</view>
<view wx:for="{{state.arr}}">
    <text>{{item.a}}:{{item.b}}:{{index}}</text>
</view>
<view wx:if="{{state.arr.length > 5}}">show</view>
<view wx:elseif="{{state.arr.length > 2}}">show2</view>
<view wx:else>hidden</view>
<button bind:tap="add">add</button>

// 2. 词法分析结果(tokens)
[
  { type: 'tag', value: 'view', attributes: { class: 'wrap' } },
  { type: 'text', value: '{{state.arr.length}}' },
  { type: 'tag', value: 'view', attributes: {}, closeStart: true },
  {
    type: 'tag',
    value: 'view',
    attributes: { 'wx:for': '{{state.arr}}' }
  },
  { type: 'tag', value: 'text', attributes: {} },
  { type: 'text', value: '{{item.a}}:{{item.b}}:{{index}}' },
  { type: 'tag', value: 'text', attributes: {}, closeStart: true },
  { type: 'tag', value: 'view', attributes: {}, closeStart: true },
  {
    type: 'tag',
    value: 'view',
    attributes: { 'wx:if': '{{state.arr.length > 5}}' }
  },
  { type: 'text', value: 'show' },
  { type: 'tag', value: 'view', attributes: {}, closeStart: true },
  {
    type: 'tag',
    value: 'view',
    attributes: { 'wx:elseif': '{{state.arr.length > 2}}' }
  },
  { type: 'text', value: 'show2' },
  { type: 'tag', value: 'view', attributes: {}, closeStart: true },
  { type: 'tag', value: 'view', attributes: { 'wx:else': '' } },
  { type: 'text', value: 'hidden' },
  { type: 'tag', value: 'view', attributes: {}, closeStart: true },
  { type: 'tag', value: 'button', attributes: { 'bind:tap': 'add' } },
  { type: 'text', value: 'add' },
  { type: 'tag', value: 'button', attributes: {}, closeStart: true }
]

// 3. 语法分析结果(AST)
{
  children: [
    {
      type: 'node',
      name: 'view',
      attributes: [Object],
      children: [Array]
    },
    ...
  ],
}

// 4. 生成jsx
{
  '0': '<comp.View class={`wrap`}>{String(state.arr.length)}</comp.View>',
  '1': '{$for(state.arr,(item, index) => (<comp.View ><comp.Text >{String(item.a)}:{String(item.b)}:{String(index)}</comp.Text></comp.View>))}',
  '2': '{state.arr.length > 5?<comp.View >show</comp.View>:state.arr.length > 2?<comp.View >show2</comp.View>:<comp.View >hidden</comp.View>}',
  '3': '<comp.Button  onClick={$handleEvent("add", "2", "bind:tap")} >add</comp.Button>'
}

举几个有意思的方法

在生成过程jsx中

把wx:开头的方法放进directives中

把bind方法都转换成on"Event"事件并绑定在$handleEvent触发下

bind:tap="add"

转换为

onClick={$handleEvent("add", pageid, "bind:tap")}

const generateProps = (node, state, asset) => {
  let code = "";
  for (let name in node.attributes) {
    const value = node.attributes[name];
    if (name.startsWith("wx:")) {
      node.directives = node.directives || [];
      node.directives.push([name, value]);
    } else if (name.startsWith("bind")) {
      if (state.methods.indexOf(value) < 0) {
        state.methods.push(value);
      }
      const key = wriedName(name);
      code += ` ${key}={$handleEvent("${value}", "${getId(
        asset
      )}", "${name}")} `;
    } else if (node.name === "import") {
      state.imports.push(value);
    } else {
      let compiled = compileExpression(value, node.type);
      code += `${name}=${compiled || "true"}`;
    }
  }
  return code + ">";
};

在生成direct的时候,我们把wx:for通过自定义方法$for包裹起来,把wx:if用三元的表达式转换

<view wx:for="{{arr}}">
    <text>{{item.a}}:{{item.b}}:{{index}}</text>
</view>

转换为

{$for(state.arr,(item, index) => (<comp.View><comp.Text>{String(item.a)}:{String(item.b)}:{String(index)}</comp.Text></comp.View>))}
<view wx:if="{{arr.length > 5}}">show</view>
    <view wx:elseif="{{arr.length > 2}}">show2</view>
<view wx:else>hidden</view>

转换为

{state.arr.length > 5?<comp.View >show</comp.View>:state.arr.length > 2?<comp.View show2</comp.View>:<comp.View >hidden</comp.View>}
const generateDirect = (node, code, next) => {
  for (let i = 0; i < node.directives.length; i++) {
    const [name, value] = node.directives[i];
    const compiled = compileExpression(value, "direct");
    if (code[0] === "{") {
      code = `<div>${code}</div>`;
    }
    if (name === "wx:for") {
      const item = findItem(node);
      code = `{$for(${compiled},(${item}, index) => (${code}))}`;
    }
    if (name === "wx:if") {
      ifcode += `{${compiled}?${code}:`;
      if (isElse(next)) {
        continue;
      } else {
        code = ifcode + "null}";
        ifcode = "";
      }
    }
    if (name === "wx:elseif") {
      ifcode += `${compiled}?${code}:`;
      if (isElse(next)) {
        continue;
      } else {
        code = ifcode + "null}";
        ifcode = "";
      }
    }
    if (name === "wx:else") {
      if (ifcode === "") {
        ifcode += `{!${compiled}?${code}:null}`;
      } else {
        ifcode += `${code}}`;
      }
      code = ifcode;
      ifcode = "";
    }
    return code;
  }
};

到这里我们已经可以转换好了每个页面对应的css,js和jsx,但wxml转换后的代码还需要被React包裹成组件才能执行,包裹一下就能得到完整的jsx文件代码。

export const packWxml = (fileNode) => {
  const code = `export default (props) => {
  const [state, setState] = React.useState(props.data)
  React.useEffect(()=>{
    setStates[${fileNode.parent.id}] = setState
  },[]);
  return <>${fileNode.out}</>
};\n`;
  return code;
};

app包

最终我们打包编译并生成一个app.js文件作为完整的构建完成文件

window.manifest = {
  origin: {
    pages: ["pages/page1/index", "pages/page2/index"],
    tabBar: {
      color: "#7A7E83",
      selectedColor: "#3cc51f",
      borderStyle: "rgb(200,200,200)",
      backgroundColor: "#ffffff",
      list: [
        {
          iconPath: "/public/icon_API.png",
          selectedIconPath: "/public/icon_API_HL.png",
          pagePath: "pages/page1/index",
          text: "组件",
        },
        {
          iconPath: "/public/icon_API.png",
          selectedIconPath: "/public/icon_API_HL.png",
          pagePath: "pages/page2/index",
          text: "组件2",
        },
      ],
    },
    window: {
      backgroundTextStyle: "light",
      navigationBarBackgroundColor: "#fff",
      navigationBarTitleText: "WeChat",
      navigationBarTextStyle: "black",
    },
  },
  pages: [
    {
      id: 2,
      info: { usingComponents: {} },
      scripts: [
        '// example/pages/page1/test.js\nvar test = () => {\n  console.log("test");\n};\n\n// example/pages/page1/index.js\nPage({\n  data: {\n    arr: [\n      {\n        a: 0,\n        b: 0\n      }\n    ]\n  },\n  onLoad(options) {\n    console.log(1, options);\n  },\n  add() {\n    this.setData({\n      arr: this.data.arr.concat([\n        {\n          a: this.data.arr.length,\n          b: this.data.arr.length * 2\n        }\n      ])\n    });\n  },\n  test() {\n    test();\n  }\n});\n\n',
        'var __defProp = Object.defineProperty;\nvar __getOwnPropDesc = Object.getOwnPropertyDescriptor;\nvar __getOwnPropNames = Object.getOwnPropertyNames;\nvar __hasOwnProp = Object.prototype.hasOwnProperty;\nvar __export = (target, all) => {\n  for (var name in all)\n    __defProp(target, name, { get: all[name], enumerable: true });\n};\nvar __copyProps = (to, from, except, desc) => {\n  if (from && typeof from === "object" || typeof from === "function") {\n    for (let key of __getOwnPropNames(from))\n      if (!__hasOwnProp.call(to, key) && key !== except)\n        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });\n  }\n  return to;\n};\nvar __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);\nvar stdin_exports = {};\n__export(stdin_exports, {\n  default: () => stdin_default\n});\nmodule.exports = __toCommonJS(stdin_exports);\nvar stdin_default = (props) => {\n  const [state, setState] = React.useState(props.data);\n  React.useEffect(() => {\n    setStates[2] = setState;\n  }, []);\n  return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(comp.View, { class: `wrap` }, String(state.arr.length)), $for(state.arr, (item, index) => /* @__PURE__ */ React.createElement(comp.View, null, /* @__PURE__ */ React.createElement(comp.Text, null, String(item.a), ":", String(item.b), ":", String(index)))), state.arr.length > 5 ? /* @__PURE__ */ React.createElement(comp.View, null, "show") : state.arr.length > 2 ? /* @__PURE__ */ React.createElement(comp.View, null, "show2") : /* @__PURE__ */ React.createElement(comp.View, null, "hidden"), /* @__PURE__ */ React.createElement(comp.Button, { onClick: $handleEvent("add", "2", "bind:tap") }, "add"));\n};\n',
      ],
      styles: ["/2.css"],
      path: "/pages/page1/index",
    },
    {
      id: 3,
      info: { usingComponents: {} },
      scripts: [
        '// example/pages/page2/index.js\nPage({\n  data: {\n    num: 0\n  },\n  async getBatteryInfo() {\n    const res = await wx.getBatteryInfo();\n    console.log(res);\n    this.setData({\n      num: res\n    });\n  },\n  onLoad() {\n    console.log("onload");\n  },\n  onShow() {\n    console.log("onshow");\n  }\n});\n\n',
        'var __defProp = Object.defineProperty;\nvar __getOwnPropDesc = Object.getOwnPropertyDescriptor;\nvar __getOwnPropNames = Object.getOwnPropertyNames;\nvar __hasOwnProp = Object.prototype.hasOwnProperty;\nvar __export = (target, all) => {\n  for (var name in all)\n    __defProp(target, name, { get: all[name], enumerable: true });\n};\nvar __copyProps = (to, from, except, desc) => {\n  if (from && typeof from === "object" || typeof from === "function") {\n    for (let key of __getOwnPropNames(from))\n      if (!__hasOwnProp.call(to, key) && key !== except)\n        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });\n  }\n  return to;\n};\nvar __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);\nvar stdin_exports = {};\n__export(stdin_exports, {\n  default: () => stdin_default\n});\nmodule.exports = __toCommonJS(stdin_exports);\nvar stdin_default = (props) => {\n  const [state, setState] = React.useState(props.data);\n  React.useEffect(() => {\n    setStates[3] = setState;\n  }, []);\n  return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(comp.Button, { onClick: $handleEvent("getBatteryInfo", "3", "bind:tap") }, "get"), /* @__PURE__ */ React.createElement(comp.Text, null, "\\u5F53\\u524D\\u8BBE\\u5907\\u7535\\u91CF\\uFF1A", String(state.num)));\n};\n',
      ],
      styles: ["/3.css"],
      path: "/pages/page2/index",
    },
  ],
};

运行层

代码已经生成了,想要把这个页面运行起来还需要完善前面埋下的一些全局变量以及Page方法

Page

将Page中的参数全都丢进自定义实体中,实现this.setData并实际调用对应组件的setState

const pages = manifest.pages;
const pageGraph = {};

var global = {
  modules: {},
  Page,
  $for,
  $handleEvent,
  useEffect: React.useEffect,
  setStates: {},
};

if (path === "/") {
  window.location.href = window.location.origin + pages[0].path;
} else {
  p = pages.find((i) => i.path === path);
}

const { scripts, styles, id } = p;

execScript(scripts[1], global); //new Function -> call 将编译后的组件挂在到global.modules下
execScript(scripts[0], global); //new Function -> call 将页面的方法挂在到pageGraph下

execStyles(styles); // 添加css的link

var Page = (option) => {
  pageGraph[p.id] = new _Page(option, p.id);
};

var _Page = class {
  constructor(option, id) {
    this.id = id;
    this.parent = null;
    this.eventMap = {};
    for (const key in option) {
      this[key] = option[key];
    }
  }
  setData(data) {
    this.data = { ...this.data, ...data };
    const setState = global.setStates[this.id];
    setState(this.data);
  }
};

comp

全局自定义组件,想要实现原生的组件就可以在这里编写

// components.js
import Button from "./Button";

var comp = {
  Button,
  View: "div",
  Text: "span",
};

window.comp = comp;


// Button.jsx
import "./index.less";
export default (props) => {
  const { onClick, children } = props;
  return (
    <button className="wx-button" onClick={onClick}>
      {children}
    </button>
  );
};

$for

遍历dom

function $for(arr, fn, key) {
  arr = arr || [];
  return arr.map((item, index) => {
    const vdom = fn(item, index);
    vdom.key = key || index;
    return vdom;
  });
}

$handleEvent

call触发

function $handleEvent(name, id, custom) {
  const ins = pageGraph[id];
  const method = ins[name] || (ins.methods || {})[name] || function () {};
  ins.eventMap[custom] = name;
  return (e) => {
    method.call(ins, e);
  };
}

渲染

执行过scripts[1]后拿到export出来的页面组件Comp并将其渲染

const Comp = global.modules[scripts[1]].default;

ReactDOM.render(
  React.createElement(wrap, {
    page: pageGraph[id],
    tabBar,
    path,
    manifest,
    Comp,
  }),
  document.body
);

// wrap.jsx
export const wrap = (props) => {
  const { page, manifest, tabBar, Comp, path } = props;

  const [show, setShow] = React.useState(false);

  React.useEffect(() => {
    page.onLoad && page.onLoad();
    return () => {
      page.onUnload && page.onUnload();
    };
  }, []);
  
  return (
    <>
      <Comp data={page.data} />
      {show && (
        <div
          style={{
            position: "fixed",
            display: "flex",
            justifyContent: "center",
            alignItems: "center",
            top: 0,
            left: 0,
            right: 0,
            padding: "8px 0 20px",
            height: "100vh",
            width: "100vw",
            fontSize: "20px",
            background: "rgba(0, 0, 0, 0.5)",
            color: "#fff",
          }}
        >
          <span>编译中</span>
        </div>
      )}
      {!manifest.origin.tabBar.custom && (
        <div
          style={{
            position: "fixed",
            display: "flex",
            bottom: 0,
            left: 0,
            right: 0,
            padding: "8px 0 20px",
            fontSize: "10px",
            backgroundColor: tabBar.backgroundColor,
            borderTop: `1px solid ${tabBar.borderStyle}`,
            color: tabBar.color,
          }}
        >
          {tabBar.list.map((item) => {
            const isSelect = "/" + item.pagePath === path;
            return (
              <div
                key={item.pagePath}
                style={{
                  display: "block",
                  width: "100%",
                  textAlign: "center",
                }}
                onClick={() => {
                  if (isSelect) return;
                  location.href = "/" + item.pagePath;
                }}
              >
                <img
                  src={isSelect ? item.selectedIconPath : item.iconPath}
                  style={{ width: "30px", height: "30px" }}
                />
                <div
                  style={{
                    color: isSelect ? tabBar.selectedColor : tabBar.color,
                  }}
                >
                  {item.text}
                </div>
              </div>
            );
          })}
        </div>
      )}
    </>
  );
}

node层

页面

我使用的是多页的方式实现,在node层启动了一个express并且注册了这些页面

export const server = (options) => {
  const express = require("express");

  const distdir = resolve(options.o);
  const appEntry = resolve(options.e, "app.json");
  const appJson = require(appEntry);

  const app = express()
    .use(express.static(distdir))
    .get("/", (_, res) => {
      getIndexHtmlCode().then((data) => {
        res.end(data);
      });
    });

  appJson.pages.forEach((page) => {
    app.get("/" + page, (_, res) => {
      getIndexHtmlCode().then((data) => {
        res.end(data);
      });
    });
  });

  
  app.listen(port, (err) => {
    if (err) throw err;
    console.log(`start:http://localhost:${port}`);
  });

  return app;
};


export async function getIndexHtmlCode() {
  return `<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>miniapp</title>
    <style>
    *{
        margin: 0;
        padding: 0;
    }
    </style>
    <link rel="stylesheet" href="/runtime.css">
</head>
<body>
    <script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
    <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
    <script src="/app.js"></script>
    <script src="/runtime.js"></script>
</body>
<script>
var wx = {}
if (window.JSBridge) {
  console.log("app env");
} else {
  console.log("brower env");
}
</script>
</html>`;
}

更新重新编译

node层&前端引入socket.io,监听入口文件

if (options.w) {
  chokidar
    .watch(resolve(options.e), {
      persistent: true,
      awaitWriteFinish: {
        stabilityThreshold: 500,
        pollInterval: 500,
      },
    })
    .on("change", async () => {
      ser.reloadStart?.();
      await rebuild(options);
      ser.reloadEnd?.();
    });
}
const http = require("http").createServer(app);
const io = require("socket.io")(http);
http.listen(8109, () => {
  console.log("socket.io listening on *:8109");
});
io.on("connection", (socket) => {
  socket.on("disconnect", () => {});
});

app.reloadEnd = () => {
  io.emit("reload-end");
};

app.reloadStart = () => {
  io.emit("reload-start");
};
const socketUrl = window.location.origin.replace(/:\d+/, ":8109");
const io = socket(socketUrl, {
  transports: ["websocket"],
  autoConnect: false,
  reconnection: false,
  forceNew: true,
  multiplex: false,
});

io.on("reload-start", () => {
  setShow(true);
});

io.on("reload-end", () => {
  setShow(false);
  window.location.reload();
});

五、应用层接入

在index.html里面我们判断是否含有JSBridge来判断宿主环境是app还是浏览器,所以只要在应用层接入时往webview的wx挂载一个变量并实现wx API即可

tauri实现

上层用tauri然后实现方法wx.getBatteryInfo

省事改下index.html的代码

<script>
var wx = {}
if (window.__TAURI_INTERNALS__) {
  const { invoke } = window.__TAURI_INTERNALS__;
  wx.getBatteryInfo = async () => {
    const res = await invoke('getBatteryInfo')
    return res;
  };
}
</script>
use battery::{units::ratio::percent, Manager, State};

#[tauri::command]
fn getBatteryInfo() -> u64 {
    let manager = Manager::new().unwrap();
    let batteries = manager.batteries().unwrap();
    let mut p = 0.0;

    for battery in batteries {
        let battery = battery.unwrap();
        p = battery.state_of_charge().value * 100.0;
        println!("设备电量: {:.1}%", battery.state_of_charge().value * 100.0);
        println!("电池状态: {:?}", battery.state());
    }

    return p as u64;
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_opener::init())
        .invoke_handler(tauri::generate_handler![getBatteryInfo])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

六、demo运行

微信开发者工具效果

七、总结

把这个容器的实现类比成乐高积木

  1. 拆解(编译转换)
    • 把wxml拆成ast
  2. 组装说明(运行时规则)
    • ast组装成jsx
    • 确定规则(数据更新/事件触发)
  3. 随处可玩(多平台接入)
    • 多平台可接入运行

还能怎么玩?

  • 安装电机(用编译型语言替换node实现/wasm加速)
  • 安装熊孩子保护罩(沙箱防护)
  • 拼接其他品牌的积木(从wxml -> ast -> jsx 提供直接从 ast ->jsx 的方法)

剖析RAG之父所说的:数据处理才是RAG系统的护城河!

RAG之父Douwe Kiela在一场演讲中,提到了这样的论点:内部知识是我们的竞争壁垒,数据处理是我们的护城河

"大规模处理企业数据的能力才是护城河,重点要放在让AI有效处理大规模、多元和含噪数据上,而非过度清洗数据。" Kiela格外强调了这句话。

这一点与我这几年的实战经验非常契合。

这是很多AI产品失败的原因之一:数据准备过度理想化

很多产研团队觉得他们在测试阶段测试数据的准确率等于生产环境的准确率!

这是一个很荒谬的误区,我们的测试数据通常都是标准的理想化的数据占比更多,杂乱的、喊噪音的数据占比更少,但是生成环境其实正相反:杂乱的、喊噪音的数据占比更多,反而标准的数据占比更少

就行Kiela在演讲中的案例:某银行的AI诈骗检测系统在试点阶段准确率达95%,但部署到生产环境后跌至62%

真实世界数据总是混乱的,能处理不完美数据的系统才是王道。构建处理真实世界数据的能力,比幻想完美数据集更实际。

RAG工程解决方案

下面给大家看一下我们是如何增强我们RAG系统的数据处理能力来解决的一些标准RAG无法解决的问题:

只能进行文本回复,无法提供相关的图片、视频等能力。

RAG系统想要加入图片、视频等能力,有两种方案可以解决:元数据描述

第一种方案:元数据是应用在结构化数据中,在结构化数据中添加有meta字段,meta字段代表着与我们的这条数据有关的其他数据。

当用户的query在知识库检索时检索到了我们这条数据,我们就可以根据meta字段进行相应的图片\视频等内容的输出。

例如,在售前场景中。我们有一批专利图片,我们要利用元数据的方案就要在我们的excel中加入图片地址信息:

meta.jpg

当用户询问专利相关问题时,我们的程序会获取到meta字段,还记得方才我们说的那个知识库响应内容content字段么?

meta字段就在里面,我们的程序拿到后就可以进行处理,返回给前端显示对应的专利图片。

第二种方案:描述是应用在非结构化的数据中,通过自然语言对图片\视频进行描述,然后把描述文案向量化之后添加到向量数据库中。

这样当用户的query和我们向量数据库中的内容进行匹配时,就可以匹配到一个图片信息,然后我们的大模型可以根据描述介绍图片,我们的程序可以根据信息返回图片。

还是刚才那个例子我们有一批专利图片,如果是使用描述的方案:

我们可以进行专利的描述,然后把描述文案加入到向量数据库中:

  • 企业申请的XXX专利。
  • 企业获取的XXX授权。
  • 企业的国际PCT专利。

当用户询问专利相关问题时,知识库就会检索到相关的内容,我们就可以进行图片化的回复了。

如果用户query不标准,问题不全,我们的知识库可能匹配不到内容。

这个问题的出现是因为:RAG系统本身是不支持上下文的:

例如,我们先问:你们企业有哪些授权? AI回复之后,我们又问:专利呢?

这时需要进入RAG系统参与匹配的应该是你们企业有哪些专业?而不是专利呢?

所以这时候就需要上下文分析的能力来分析上下文得到用户的真实问题你们企业有哪些专业?

我们的解决方案是使用提示词来进行上下文分析 + query改写

上下文分析提示词如下:

## 业务知识

【这里根据自己的业务场景,添加必须让大模型知道的业务知识,例如对某些名词的解释】

## 要求

- 根据user的上下文对话,分析出user本次对话的真实意图。
- 必要的知识放在【业务知识】中,查询业务知识的信息与user对齐概念。
- 把user最终的真实意图转化成与上下文文风一致的问题后直接输出,不要输出分析过程
- 输出格式为{user:真实意图}

## 上下文

question:【上一次分析的结果query】
answer:【回复内容】
question:【用户本次query】

## 输出

上下文分析 提示词用在解决使用RAG系统时,问题需要有上下文关联性的场景,可以帮助我们获得完整的用户意图。

query改写提示词如下:

## 业务知识

【这里根据自己的业务场景,添加必须让大模型知道的业务知识,例如对某些名词的解释】

## 要求

- 对问题进行一次改写,改写为一条新的问题。
- 需要保持语义一致性,核心意图不变,允许根据上下文扩展关联信息。
- 如果只有一个名词,用户的默认意图是需要解释。
- 用词保持简单,保证新问题是一句话,没有过多的冗余内容。
- 改写标准为主语 + 谓语 + 宾语的标准语法,不要使用倒装句等其他语法。
- 输出格式为{user:新问题}

## 上下文

question:【上一次分析的结果query】
answer:【回复内容】

## 问题

【用户本次query】

## 输出

query改写 提示词用在我们为了提高准确性,会通过改写query,对RAG系统进行多次匹配的场景。可以帮助我们把用户的非标准问题改写成更容易匹配我们的知识库的问题。

比如这个提示词,我们的知识库中全都是主谓宾组成的标准语法,那么我们就更希望把用户的问题也全部改成主谓宾的标准语法。

知识库中的内容仍然存在匹配错误的情况。例如:用户问A产品的价钱,我们知识库筛选出了B产品的价钱,然后回复给了用户。

这个问题的出现是因为:RAG系统是根据语义进行匹配的,虽然现在大家都是用混合检索了,但是在匹配的过程中还是会出现匹配错误的情况

我们当然是不允许把错误的信息返回给用户的。

所以在匹配到答案之后,需要再验证一次这个答案是否解决了用户的问题。

例如:用户问A会议的开始时间,我们拿到了B会议的信息,其中也有开始时间。如果不做相关性验证,就对用户造成了误导。

提示词如下:

## 业务知识
【这里根据自己的业务场景,添加必须让大模型知道的业务知识,例如对某些名词的解释】

## 资料
【知识库匹配出来的答案】

## 问题
【用户的query】

## 要求

- 必要的知识放在【业务知识】中,查询业务知识的信息与user对齐概念。
- 判断资料是否能够有效的回复user的问题。
- 如果资料是有效的,返回'''Y''',否则返回'''N''',不要输出任何其他内容。

## 输出

当相关性验证没有通过时,我们还可以调用query改写,再进行重试。或者把知识库匹配到的问题直接以相似问的形式返会给用户。

  • query改写的逻辑是把query的语法、用词等修改成和我们的知识库中存的数据相似性高一些的新query,以此来增加匹配度。
  • 相似问的逻辑是我们把问题抛回给用户,让用户自己进行选择他想问的问题或者重新提问。

经典的中文二义性问题。用户的问题可以用A来回答,也可以用B来回答,怎么办?

中文是具有的二义性问题的:

  • 能吃多少吃多少?,是多吃点还是少吃点?
  • 咬死了猎人的狗。,是要死了猎人的狗,还是猎人的狗被咬死了?

这些二义性问题还可以根据上下文来大概进行判断,但是还有一些二义性问题,就无法利用上下文了,例如:

Q1:清华大学怎么样? Q2:计算机专业怎么样?

用户是问计算机专业怎么样?还是清华大学的计算机专业怎么样?,这种问题结合上下文和不结合上下文是完全两个问题。

对于这种场景的问题,给大家分享一个我的解决方案,两个问题答案我们都回复给用户,利用结构化的表达方式,无论用户的真实意图是哪一个,都可以感受到被回答了。

回复示例:

【内容一:计算机专业介绍,回复计算机专业怎么样】
上下文衔接内容
【内容二:计算机专业的前景、发展等内容】
上下文衔接内容
【内容三:清华大学的计算机专业优势、介绍。】

既然回答哪个都有可能是错的,那么我们就全都要,全部回复即可。

结语

传统的RAG目前比较主流存在的问题有以下几个:

  • 知识库内容缺失:现有的文档其实回答不了用户的问题,系统有时被误导,给出的回应其实是“胡说八道”,理想情况系统应该回应类似“抱歉,我不知道”。
  • TopK截断有用文档:和用户查询相关的文档因为相似度不足被TopK截断,本质上是相似度不能精确度量文档相关性。
  • 上下文整合丢失:从数据库中检索到包含答案的文档,因为重排序/过滤规则等策略,导致有用的文档没有被整合到上下文中。
  • 有用信息未识别:受到LLM能力限制,有价值的文档内容没有被正确识别,这通常发生在上下文中存在过多的噪音或矛盾信息时。
  • 提示词格式问题:提示词给定的指令格式出现问题,导致大模型/微调模型不能识别用户的真正意图。
  • 准确性不足:LLM没能充分利用或者过度利用了上下文的信息,比如给学生找老师首要考虑的是教育资源的信息,而不是具体确定是哪个老师。另外,当用户的提问过于笼统时,也会出现准确性不足的问题。
  • 答案不完整:仅基于上下文提供的内容生成答案,会导致回答的内容不够完整。比如问“文档 A、B和C的主流观点是什么?”,更好的方法是分别提问并总结。

总的来看:

  • 问题1-3:属于知识库工程层面的问题,可以通过完善知识库、增强知识确定性、优化上下文整合策略解决。
  • 问题4-6:属于大模型自身能力的问题,依赖大模型的训练和迭代。
  • 问题7:属于RAG架构问题,更有前景的思路是使用Agent引入规划能力。

今天的内容主要是知识库工程层面的问题的解决方案,大模型自身能力的问题的解决方案可以交给微调来解决。而RAG架构问题目前大家也在不断提供新的解决方案例如Graph RAG之类的。

☺️你好,我是华洛,如果你对程序员转型AI产品负责人感兴趣,请给我点个赞。

你可以在这里联系我👉www.yuque.com/hualuo-fztn…

已入驻公众号【华洛AI转型纪实】,欢迎大家围观,后续会分享大量最近三年来的经验和踩过的坑。

精选专栏文章

# 从0到1打造企业级AI售前机器人——实战指南三:RAG工程的超级优化

# 从0到1打造企业级AI售前机器人——实战指南二:RAG工程落地之数据处理篇🧐

# 从0到1打造企业级AI售前机器人——实战指南一:根据产品需求和定位进行agent流程设计🧐

# 聊一下MCP,希望能让各位清醒一点吧🧐

# 实战派!百万PV的AI产品如何搭建RAG系统?

# 5000字长文,AI时代下程序员的巨大优势!

[Python3/Java/C++/Go/TypeScript] 一题一解:排序 + 分类讨论(清晰题解)

方法一:排序 + 分类讨论

我们先对数组进行排序,然后根据三角形的定义进行分类讨论即可。

  • 如果最小的两个数之和小于等于最大的数,那么无法构成三角形,返回 "none"。
  • 如果最小的数等于最大的数,那么是等边三角形,返回 "equilateral"。
  • 如果最小的数等于中间的数或者中间的数等于最大的数,那么是等腰三角形,返回 "isosceles"。
  • 否则,返回 "scalene"。

###python

class Solution:
    def triangleType(self, nums: List[int]) -> str:
        nums.sort()
        if nums[0] + nums[1] <= nums[2]:
            return "none"
        if nums[0] == nums[2]:
            return "equilateral"
        if nums[0] == nums[1] or nums[1] == nums[2]:
            return "isosceles"
        return "scalene"

###java

class Solution {
    public String triangleType(int[] nums) {
        Arrays.sort(nums);
        if (nums[0] + nums[1] <= nums[2]) {
            return "none";
        }
        if (nums[0] == nums[2]) {
            return "equilateral";
        }
        if (nums[0] == nums[1] || nums[1] == nums[2]) {
            return "isosceles";
        }
        return "scalene";
    }
}

###cpp

class Solution {
public:
    string triangleType(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        if (nums[0] + nums[1] <= nums[2]) {
            return "none";
        }
        if (nums[0] == nums[2]) {
            return "equilateral";
        }
        if (nums[0] == nums[1] || nums[1] == nums[2]) {
            return "isosceles";
        }
        return "scalene";
    }
};

###go

func triangleType(nums []int) string {
sort.Ints(nums)
if nums[0]+nums[1] <= nums[2] {
return "none"
}
if nums[0] == nums[2] {
return "equilateral"
}
if nums[0] == nums[1] || nums[1] == nums[2] {
return "isosceles"
}
return "scalene"
}

###ts

function triangleType(nums: number[]): string {
    nums.sort((a, b) => a - b);
    if (nums[0] + nums[1] <= nums[2]) {
        return 'none';
    }
    if (nums[0] === nums[2]) {
        return 'equilateral';
    }
    if (nums[0] === nums[1] || nums[1] === nums[2]) {
        return 'isosceles';
    }
    return 'scalene';
}

###cs

public class Solution {
    public string TriangleType(int[] nums) {
        Array.Sort(nums);
        if (nums[0] + nums[1] <= nums[2]) {
            return "none";
        }
        if (nums[0] == nums[2]) {
            return "equilateral";
        }
        if (nums[0] == nums[1] || nums[1] == nums[2]) {
            return "isosceles";
        }
        return "scalene";
    }
}

时间复杂度 $O(1)$,空间复杂度 $O(1)$。


有任何问题,欢迎评论区交流,欢迎评论区提供其它解题思路(代码),也可以点个赞支持一下作者哈😄~

每日一题-三角形类型🟢

给你一个下标从 0 开始长度为 3 的整数数组 nums ,需要用它们来构造三角形。

  • 如果一个三角形的所有边长度相等,那么这个三角形称为 equilateral 。
  • 如果一个三角形恰好有两条边长度相等,那么这个三角形称为 isosceles 。
  • 如果一个三角形三条边的长度互不相同,那么这个三角形称为 scalene 。

如果这个数组无法构成一个三角形,请你返回字符串 "none" ,否则返回一个字符串表示这个三角形的类型。

 

示例 1:

输入:nums = [3,3,3]
输出:"equilateral"
解释:由于三条边长度相等,所以可以构成一个等边三角形,返回 "equilateral" 。

示例 2:

输入:nums = [3,4,5]
输出:"scalene"
解释:
nums[0] + nums[1] = 3 + 4 = 7 ,大于 nums[2] = 5 
nums[0] + nums[2] = 3 + 5 = 8 ,大于 nums[1] = 4 。
nums[1] + nums[2] = 4 + 5 = 9 ,大于 nums[0] = 3 。
由于任意两边之和都大于第三边,所以可以构成一个三角形,因为三条边的长度互不相等,所以返回 "scalene"。

提示:

  • nums.length == 3
  • 1 <= nums[i] <= 100

模拟处理

Problem: 3024. 三角形类型 II

[TOC]

思路

直接模拟处理。

Code

执行用时分布38ms击败53.04%;消耗内存分布16.18MB击败99.58%

###Python3

class Solution:
    def triangleType(self, nums: List[int]) -> str:
        if nums[0] == nums[1] == nums[2]: return "equilateral"
        nums.sort()
        if nums[0] + nums[1] <= nums[2]: return "none"
        if len(set(nums)) == 2: return "isosceles"
        return "scalene"

您若还有不同方法,欢迎贴在评论区,一起交流探讨! ^_^

↓ 点个赞,点收藏,留个言,再划走,感谢您支持作者! ^_^

用排序简化代码逻辑(Python/Java/C++/C/Go/JS/Rust)

把 $\textit{nums}$ 从小到大排序,可以简化判断逻辑。

设排序后 $\textit{nums}=[a,b,c]$,那么有 $1\le a\le b\le c$。

  • 先判是否合法,即三角形任意两边之和必须大于第三边。由于排序后 $a+c > b$ 和 $b+c>a$ 自动成立(注意数组元素都是正数),所以只需判断 $a+b > c$ 是否成立。如果 $a+b\le c$,那么无法构成三角形。
  • 然后判等边:只需判断 $a=c$。注意已经排序了,如果 $a=c$,那么必然有 $a=b=c$。
  • 然后判等腰:判断 $a=b$ 或者 $b=c$。
  • 其他情况,三条边长度一定不相等,无需判断。

###py

class Solution:
    def triangleType(self, nums: List[int]) -> str:
        nums.sort()
        a, b, c = nums
        if a + b <= c:
            return "none"
        if a == c:
            return "equilateral"
        if a == b or b == c:
            return "isosceles"
        return "scalene"

###java

class Solution {
    public String triangleType(int[] nums) {
        Arrays.sort(nums);
        int a = nums[0];
        int b = nums[1];
        int c = nums[2];
        if (a + b <= c) {
            return "none";
        }
        if (a == c) {
            return "equilateral";
        }
        if (a == b || b == c) {
            return "isosceles";
        }
        return "scalene";
    }
}

###cpp

class Solution {
public:
    string triangleType(vector<int>& nums) {
        ranges::sort(nums);
        int a = nums[0], b = nums[1], c = nums[2];
        if (a + b <= c) {
            return "none";
        }
        if (a == c) {
            return "equilateral";
        }
        if (a == b || b == c) {
            return "isosceles";
        }
        return "scalene";
    }
};

###c

int cmp(const void* a, const void* b) {
    return *(int*)a - *(int*)b;
}

char* triangleType(int* nums, int numsSize) {
    qsort(nums, numsSize, sizeof(int), cmp);
    int a = nums[0], b = nums[1], c = nums[2];
    if (a + b <= c) {
        return "none";
    }
    if (a == c) {
        return "equilateral";
    }
    if (a == b || b == c) {
        return "isosceles";
    }
    return "scalene";
}

###go

func triangleType(nums []int) string {
slices.Sort(nums)
a, b, c := nums[0], nums[1], nums[2]
if a+b <= c {
return "none"
}
if a == c {
return "equilateral"
}
if a == b || b == c {
return "isosceles"
}
return "scalene"
}

###js

var triangleType = function(nums) {
    nums.sort((a, b) => a - b);
    const [a, b, c] = nums;
    if (a + b <= c) {
        return "none";
    }
    if (a === c) {
        return "equilateral";
    }
    if (a === b || b === c) {
        return "isosceles";
    }
    return "scalene";
};

###rust

impl Solution {
    pub fn triangle_type(mut nums: Vec<i32>) -> String {
        nums.sort_unstable();
        let (a, b, c) = (nums[0], nums[1], nums[2]);
        if a + b <= c {
            return "none".to_string();
        }
        if a == c {
            return "equilateral".to_string();
        }
        if a == b || b == c {
            return "isosceles".to_string();
        }
        "scalene".to_string()
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(1)$。
  • 空间复杂度:$\mathcal{O}(1)$。

附:哈希表做法

在三边长能构成三角形的情况下,用哈希表计算 $\textit{nums}$ 中有 $c$ 个不同元素,然后:

  • $c=1$ 即等边。
  • $c=2$ 即等腰。
  • $c=3$ 即边长互不相同。

###py

class Solution:
    def triangleType(self, nums: List[int]) -> str:
        nums.sort()
        if nums[0] + nums[1] <= nums[2]:
            return "none"
        return ("equilateral", "isosceles", "scalene")[len(set(nums)) - 1]

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

Canvas签名功能常见的几种问题

. 如何实现基础签名功能?


<!DOCTYPE html>
<canvas id="signature" width="500" height="300"></canvas>
<script>
  const canvas = document.getElementById('signature');
  const ctx = canvas.getContext('2d');
  let isDrawing = false;
  
  canvas.addEventListener('mousedown', startDrawing);
  canvas.addEventListener('mousemove', draw);
  canvas.addEventListener('mouseup', stopDrawing);
  canvas.addEventListener('mouseout', stopDrawing);

  function startDrawing(e) {
    isDrawing = true;
    ctx.beginPath();
    ctx.moveTo(e.offsetX, e.offsetY);
  }

  function draw(e) {
    if (!isDrawing) return;
    ctx.lineTo(e.offsetX, e.offsetY);
    ctx.stroke();
  }

  function stopDrawing() {
    isDrawing = false;
  }
</script>

1. 如何检测签名是否为空?

function isCanvasBlank(canvas) {
  // 获取画布像素数据
  const context = canvas.getContext('2d');
  const pixelBuffer = new Uint32Array(
    context.getImageData(0, 0, canvas.width, canvas.height).data.buffer
  );
  
  // 检查是否有非透明像素
  return !pixelBuffer.some(color => color !== 0);
}

2. 如何处理不同设备DPI问题?

function setupHighDPICanvas(canvas) {
  // 获取设备像素比
  const dpr = window.devicePixelRatio || 1;
  
  // 获取CSS显示尺寸
  const rect = canvas.getBoundingClientRect();
  
  // 设置实际尺寸为显示尺寸乘以像素比
  canvas.width = rect.width * dpr;
  canvas.height = rect.height * dpr;
  
  // 缩放上下文以匹配CSS尺寸
  const ctx = canvas.getContext('2d');
  ctx.scale(dpr, dpr);
  
  // 设置CSS尺寸保持不变
  canvas.style.width = `${rect.width}px`;
  canvas.style.height = `${rect.height}px`;
}

3. 如何实现撤销/重做功能?

class SignatureHistory {
  constructor(canvas) {
    this.canvas = canvas;
    this.ctx = canvas.getContext('2d');
    this.history = [];
    this.currentStep = -1;
  }
  
  saveState() {
    // 截取当前画布状态
    const imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
    
    // 如果当前不是最新状态,截断历史
    if (this.currentStep < this.history.length - 1) {
      this.history = this.history.slice(0, this.currentStep + 1);
    }
    
    this.history.push(imageData);
    this.currentStep++;
  }
  
  undo() {
    if (this.currentStep > 0) {
      this.currentStep--;
      this.ctx.putImageData(this.history[this.currentStep], 0, 0);
    }
  }
  
  redo() {
    if (this.currentStep < this.history.length - 1) {
      this.currentStep++;
      this.ctx.putImageData(this.history[this.currentStep], 0, 0);
    }
  }
}

4. 如何添加签名压力感应效果?

// 监听指针事件(支持压力感应设备)
canvas.addEventListener('pointerdown', startDrawing);
canvas.addEventListener('pointermove', drawWithPressure);
canvas.addEventListener('pointerup', stopDrawing);

function drawWithPressure(e) {
  if (!isDrawing) return;
  
  // 获取压力值(0-1),默认0.5用于鼠标
  const pressure = e.pressure || 0.5;
  
  // 根据压力调整线条宽度
  ctx.lineWidth = pressure * 10 + 2; // 2-12px范围
  
  ctx.lineTo(e.offsetX, e.offsetY);
  ctx.stroke();
  ctx.beginPath();
  ctx.moveTo(e.offsetX, e.offsetY);
}

5. 如何防止签名图片被篡改?

function generateSignatureHash(canvas) {
  // 获取画布数据
  const imageData = canvas.toDataURL('image/png');
  
  // 使用SHA-256生成哈希
  return crypto.subtle.digest('SHA-256', new TextEncoder().encode(imageData))
    .then(hash => {
      // 转换为十六进制字符串
      const hashArray = Array.from(new Uint8Array(hash));
      return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
    });
}

6. 如何加密存储签名数据?

async function encryptSignatureData(canvas) {
  // 获取画布数据
  const imageData = canvas.toDataURL('image/png');
  
  // 生成加密密钥
  const key = await crypto.subtle.generateKey(
    { name: 'AES-GCM', length: 256 },
    true,
    ['encrypt', 'decrypt']
  );
  
  // 加密数据
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const encrypted = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv },
    key,
    new TextEncoder().encode(imageData)
  );
  
  return {
    key,
    iv,
    encryptedData: Array.from(new Uint8Array(encrypted))
  };
}

7. 如何实现多人协同签名?

class CollaborativeSignature {
  constructor(canvas, socket) {
    this.canvas = canvas;
    this.ctx = canvas.getContext('2d');
    this.socket = socket;
    
    // 本地绘制事件
    canvas.addEventListener('mousedown', this.handleLocalDrawStart.bind(this));
    canvas.addEventListener('mousemove', this.handleLocalDrawing.bind(this));
    canvas.addEventListener('mouseup', this.handleLocalDrawEnd.bind(this));
    
    // 远程绘制事件
    socket.on('remote-draw-start', this.handleRemoteDrawStart.bind(this));
    socket.on('remote-drawing', this.handleRemoteDrawing.bind(this));
    socket.on('remote-d

Step - Invert

Task

Write a shader that splits the screen into two parts: the left half should be red and the right half should be black.

编写一个着色器,将屏幕分成两部分:左半部分应为红色,右半部分应为黑色。

Requirements

The shader should avoid using branching or conditional statements in its code, and instead rely on the step function to determine the color of each pixel.

着色器应避免在其代码中使用分支或条件语句,而是依靠step函数来确定每个像素的颜色。

Theory

您可以step通过从中减去阶跃函数结果来反转函数的结果1

如果输入值小于阈值,并且输入值大于或等于阈值,则函数返回。通过从中减去此结果,step可以有效地反转输出:0.0``1.0``1.0

  • • 如果原始结果是0.0,则反转的结果将是1.0
  • • 如果原始结果是1.0,则反转的结果将是0.0

例子

下面是一个示例代码片段,用于说明如何step在 GLSL 中反转函数的结果:

float invertedStep = 1.0 - step(threshold, value);

因此,如果大于或等于,invertedStep则为,否则为。0.0``value``threshold``1.0

Answer

uniform vec2 iResolution;

void main() {
  // Normalized pixel coordinates (from 0 to 1)
  vec2 uv = gl_FragCoord.xy / iResolution.xy;
  float result = step(0.5, 1.0 - uv.x);

  gl_FragColor = vec4(result, 0.0, 0.0, 1.0);
}

效果

image.png

练习

Step - Invert

最后

如果你觉得这篇文章有用,记得点赞、关注、收藏,学Shader更轻松!!

❌