普通视图

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

网页作品惊艳亮相!这个浪浪山小妖怪网站太治愈了!

作者 姑苏洛言
2025年8月21日 22:59

大家好呀!今天要给大家分享一个超级治愈的网页作品——浪浪山小妖怪

主题网站!这个纯原生开发的项目不仅颜值在线,功能也很能打哦~

至于灵感来源的话,要从一部动画说起。最近迷上了治愈系动画,就想做一个温暖人心的网站!浪浪山小妖怪的世界观超级可爱——每个小妖怪都有独特的性格和能力,住在云雾缭绕的奇幻山林里~

然后,我设计的初衷是,我希望用户一进入网站就能感受到浪浪山的温暖氛围。

一、网站亮点

  1. 5个完整页面:首页/电影介绍/小妖怪图鉴/幕后故事/关于我们;

  2. 治愈系配色:主色调米白+淡橙棕,看着超舒服!

  3. **[全响应式设计

    ](zhida.zhihu.com/search?cont…

  4. 纯原生代码:没用任何框架,基础前端技能拉满;

二、超用心功能

小妖怪筛选系统:可以按类型查看不同小妖怪;
时间线设计:用CSS打造高颜值制作历程;
悬停动画:卡片、按钮都有细腻的交互效果;
移动端菜单:小屏幕自动变成汉堡菜单;

三、技术三件套

四、核心技术揭秘

1. 响应式布局魔法

/* 移动端优先 */
.character-card {
  width: 100%;
}

/* 平板适配 */
@media (min-width: 768px) {
  .character-card {
    width: 48%;
  }
}

/* PC端完美呈现 */
@media (min-width: 992px) {
  .character-card {
    width: 30%;
  }
}

2. 小妖怪筛选系统

// 筛选功能
filterBtns.forEach(btn => {
  btn.addEventListener('click', () => {
    const filter = btn.dataset.filter;
    // 筛选逻辑...
  });
});

3. 丝滑的悬停动画

.card {
  transition: all 0.3s ease;
}
.card:hover {
  transform: translateY(-5px);
  box-shadow: 0 10px 20px rgba(0,0,0,0.1);
}

五、作品展示

六、收获总结

在网页设计与制作中,其实,移动优先的设计思维很重要。这次,我主要是基于原生JS实现复杂交互,而CSS动画则是提升体验的关键。当然啦,性能优化也是需要持续关注的。

对了,关于未来升级计划,我再稍微透露一下,其实我还想加入:

  • 3D角色展示;

  • 用户收藏功能;

  • 暗黑模式;

  • 更丰富的交互动效;

大家觉得这个作品最吸引你的是什么?是治愈的画风,还是实用的技术实现?欢迎在评论区留言讨论哦!

nvm 安装pnpm的异常解决

作者 字节逆旅
2025年8月21日 22:56

关于nvm

如果你是用nvm来管理node版本,正常情况下你的nvm文件夹下是有多个node版本的,像这样:

image.png

一般我们通过nvm ls命令就可以查到当前系统中安装的node版本,如果是要切换node版本就用nvm use 18.19.0类似这样的命令。

这里加一个知识点,记住,后面要考。

node的环境变量

现在问题来了,我要安装pnpm,但pnpm只能安装在18.12以上版本,否则它会提示这个:


This version of pnpm requires at least Node.js v18.12

安装pnpm

首先你要切到node版本v18.12以上,然后全局安装就好了:


npm install -g pnpm

通过pnpm -v检查一下是否安装成功。

遇到的问题

正常情况下按上述步骤我是可以用的,结果安装失败,我的环境变量配置的是c:\Program Files\nodejs\node_global\node_modules,但我在安装pnpm时npm install -g pnpm总是安装在这个位置D:\Program Files\nodejs\node_global,通过AI找了下原因,原来是由于环境变量的配置和 npm 的全局安装路径设置之间存在冲突或不一致导致的。一般来说,npm 会根据 npm config 中的配置来决定全局包的安装位置。你可以通过以下步骤检查和修正这个问题:

1、检查 npm 的配置:使用以下命令查看当前的全局安装路径:


npm config get prefix

结果显示的是 D:\Program Files\nodejs\node_global,说明 npm 的全局安装路径被设置为该位置。你可以通过以下命令修改它:


npm config set prefix "C:\Program Files\nodejs\node_global"

2、检查环境变量:在 Windows 中,你的环境变量 PATH 中应该包含正确的 Node.js 和 npm 安装路径,确保 npm 可以从正确的目录找到全局包。可以检查下 PATH 变量是否有多个 Node.js 相关路径。

3、重新安装pnpm: 在更新了 npm config 之后,再次尝试安装 pnpm:


npm install -g pnpm

4、确认路径: 安装完成后,可以通过 npm list -g pnpm 来确认 pnpm 是否正确安装在你期望的目录。

新的问题

现在pnpm已经安装在了C:\Program Files\nodejs\node_global下面,但是我在用pnpm i命令时,仍然提示:


pnpm i pnpm : 无法加载文件 C:\Program Files\nodejs\node_global\pnpm.ps1,因为在此系统上禁止运行脚本。有关详细信息,请参阅 https:/go.microsoft.com/fwlink/?LinkID=135170 中的 about_Execution_Policies。

这个错误是因为 PowerShell 的执行策略(Execution Policy)默认情况下不允许运行脚本,特别是未签名的脚本。你可以通过修改 PowerShell 的执行策略来允许脚本的运行。

image.png

我看了一眼vscode,果然新开的控制台是powershell,来吧,办它!

解决步骤

1、打开 PowerShell 以管理员身份

  • 在 Windows 开始菜单中,搜索 “PowerShell”。

  • 右键点击 “Windows PowerShell”,选择 “以管理员身份运行”。

2、检查当前执行策略:在 PowerShell 中输入以下命令,查看当前的执行策略:


Get-ExecutionPolicy

如果返回值是 Restricted,表示不允许执行任何脚本。

3、修改执行策略

将执行策略更改为 RemoteSigned ByPass,这两者允许执行本地脚本,并且只有从互联网下载的脚本需要签名。运行以下命令:


Set-ExecutionPolicy RemoteSigned

或者:


Set-ExecutionPolicy ByPass

选择 RemoteSigned 是较为安全的选择,它允许本地脚本执行,但需要从网上下载的脚本进行签名。

4、确认更改

执行该命令时,会提示确认更改执行策略,输入 Y 并按回车确认。

5、重新运行 pnpm 命令

修改执行策略后,关闭 PowerShell 窗口并重新打开一个新的 PowerShell 窗口。然后,再次尝试运行 pnpm i 命令:


pnpm i

这下终于丝滑了,顺利下载依赖,nice!

2025年 两院院士 增选有效候选人名单公布

作者 GIS之路
2025年8月21日 22:22

2025年8月20日两院院士增选有效候选人名单分别在中国科学院和中国工程院官网公布。

一、2025年中国科学院院士增选有效候选人名单

2025年中国科学院院士增选推荐工作已经结束。经中国科学院学部主席团审议,中国科学院党组审定,确认2025年中国科学院院士增选有效候选人639人。根据《中国科学院院士增选工作实施办法(试行)》的规定,现将有效候选人名单予以公布。

共639人,分专业学部按姓氏拼音排序

中国科学院.png

二、2025年中国工院院士增选有效候选人名单

中国工程院2025年院士增选提名工作已经结束。经中国工程院第八届主席团第十六次会议审议,中国工程院党组审定,确认中国工程院2025年院士增选有效候选人660人。根据《中国工程院院士增选工作实施办法》的规定,现将有效候选人名单予以公布。

共660人,分学部按姓氏拼音排序

中国工程院.png

前端只会写业务,不会nginx部署和提高性能?Trae老师来教教

2025年8月21日 22:00

前言

作为一个天天只写业务的前端攻城狮,还不知道怎么部署前端项目,部署之后怎么性能优化也不知道,只知道压缩打包的体积,这样就是全部优化手段了?

nginx是不是听着很耳熟,很高级的一个东西,是不是还不知道怎么安装使用?

今天就让Trae来教教怎么安装部署,以及优化前端性能吧~

image.png

首先先让Trae帮我输出一份配置来看看,这样的前端优化是不是合理的

性能优化配置,适用于静态网站前端

image.png

gzip压缩,常见的体积压缩,会有优雅降级,不支持gzip的浏览器会请求对应的html、css、js文件,不至于访问不了浏览器

gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types
        text/plain
        text/css
        text/xml
        text/javascript
        application/javascript
        application/xml+rss
        application/json;

缓存配置,对应的静态资源配置时效性,保证浏览器在时间内会走缓存,不至于每次都请求服务器,导致加载速度缓慢

map $sent_http_content_type $expires {
        default 1M;
        text/html 1h;
        text/css 1M;
        application/javascript 1M;
        ~image/ 1M;
        ~font/ 1M;
    }

安全请求头也不要忘记配置

    # 安全头配置
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

server是配置服务,配置服务器的根目录,可以在根目录新增文件夹,然后在域名后面添加上新增的文件夹就可以访问到对应的网页

 server {
        listen 80;
        listen [::]:80;
        server_name localhost;
        
        # 根目录配置
        root /usr/share/nginx/html;
        index index.html index.htm;
    }

静态资源优化

location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
            expires 1M;
            add_header Cache-Control "public, immutable";
            add_header Vary Accept-Encoding;
        }

api代理,前端的跨域也可以通过这种方式来解决,也是主流的解决方式

location /api/ {
            proxy_pass http://backend;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection 'upgrade';
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_cache_bypass $http_upgrade;
        }

ssl证书配置,如果没有这个,你的服务器就相当于是在裸奔,知道你的ip就可以轻松攻击你的服务器

server {
        listen 443 ssl http2;
        listen [::]:443 ssl http2;
        server_name localhost;
        
        ssl_certificate /etc/nginx/ssl/cert.pem;
        ssl_certificate_key /etc/nginx/ssl/key.pem;
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384;
        ssl_prefer_server_ciphers off;
        ssl_session_cache shared:SSL:10m;
        ssl_session_timeout 10m;
        
        include /etc/nginx/conf.d/default.conf;
    }

现在的腾讯云和阿里云的免费域名都是3个月有时效性,到期就得手动更换,如果你不想麻烦,可以去github搜索开源项目,可以自动续签域名,让你一劳永逸,不在为域名过期而烦恼,以及错失成交的机会

总结

前端的优化手段,很大一部分都可以靠nginx进行优化,有时候前端工程化的webpack或vite无法再优化时,可以考虑使用nginx的配置来优化,优化方式千千万,适合自己的才是真理,切记不要盲目优化,记得先分析是哪里出现性能瓶颈再下手优化哦

trae的优化也是很专业

image.png

Trae给出的预期也是可以达到的

image.png

Next.js Server Actions 详解: 无缝衔接前后端的革命性技术(八)

2025年8月21日 21:39

前言背景

构建一个网站,需要处理一个简单的用户反馈表单。在过去,可能需要经历这样一套标准的“仪式”:

  1. 在前端,精心编写一个表单组件。
  2. 在后端,创建一个专门的 API 路由(比如 /api/feedback)。
  3. 前端通过 fetch 函数,将表单数据打包发送到这个 API 端点。
  4. 后端接收数据,进行验证,然后与数据库“沟通”。
  5. 最后,后端向前端返回一个结果,告诉它:“嘿,我处理完了!”

这套流程虽然行得通,但就像是为了寄一封信,却不得不自己开车去邮政总局,绕了好大一圈。如果有一种方式,能让你在写前端代码的地方,直接“喊一嗓子”,服务器就能听到并行动,那该多好?

Server Actions,就是 Next.js 为你提供的这趟“直达快车”。

它允许你直接在你的 React 组件中定义和调用能在服务器上安全执行的函数。这不仅仅是语法上的简化,更是对传统前后端分离开发模式的一次优雅革命。如果你曾经为了处理一个简单的表单提交而创建了一个完整的 API 路由,那么 Server Actions 将会让你重新思考 Web 开发的方式。

什么是 Server Actions?它不是什么?

简单来说,Server Actions 就是一个普通的异步函数,但它被赋予了在服务器环境执行的“超能力”。你可以在定义它时,通过添加 "use server" 这个特殊的“标记”,来告诉 Next.js:“这个函数,请在服务器上运行!”

为了让你有更直观的感受,我们来看一个对比:

传统方式(API 路由):

// app/api/likes/route.js
import { NextResponse } from 'next/server';

export async function POST(request) {
  const { postId } = await request.json();
  // ... 更新数据库点赞数 ...
  console.log(`给文章 ${postId} 点赞`);
  return NextResponse.json({ message: '点赞成功!' });
}

// app/components/LikeButton.js
'use client';

const LikeButton = ({ postId }) => {
  const handleLike = async () => {
    await fetch('/api/likes', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ postId }),
    });
  };

  return <button onClick={handleLike}>点赞</button>;
};

Server Actions 方式:

// app/components/LikeButton.js
'use client';

import { likePost } from '../actions/likeActions'; // 假设 Server Action 在这个文件里

const LikeButton = ({ postId }) => {
  return (
    <form action={likePost}>
      <input type="hidden" name="postId" value={postId} />
      <button type="submit">点赞</button>
    </form>
  );
};


// app/actions/likeActions.js
'use server';

export async function likePost(formData) {
  const postId = formData.get('postId');
  // ... 更新数据库点赞数 ...
  console.log(`给文章 ${postId} 点赞`);
}

看到了吗?Server Actions 的方式省去了创建 API 路由的步骤,代码更内聚,逻辑也更清晰。前端组件似乎直接调用了一个能在服务器上操作数据库的函数。

但请注意,Server Actions 不是:

  • 取代所有 API 路由:对于需要被外部服务(如 webhook、第三方应用)调用的场景,传统的 API 路由依然是最佳选择。
  • 一个不安全的后门:Next.js 在底层做了大量工作来确保 Server Actions 的安全。所有的数据传输都经过了严格的序列化和加密,并且只有你的代码可以调用它们。你可以把它想象成一个只对你的应用内部开放的、受保护的“快速通道”。

核心优势:为什么你应该拥抱 Server Actions?

  1. 极致的开发体验:将前后端逻辑写在同一个地方,心智负担大大降低。你不再需要在多个文件和路由之间来回切换,开发流程如丝般顺滑。
  2. 减少客户端 JavaScript:由于大部分逻辑在服务器上执行,发送到浏览器的 JavaScript 代码量会减少,这意味着更快的页面加载速度和更好的性能。
  3. 渐进式增强:即使在用户的浏览器禁用了 JavaScript,基础的表单提交(如上面的点赞按钮)依然可以工作。这是因为 Server Actions 与 HTML 的 <form> 标签天然集成。
  4. 内置数据变更(Mutation)和缓存管理:Server Actions 与 Next.js 的缓存机制深度集成。你可以轻松地在操作完成后,让相关页面数据重新生效(revalidation),告别手动管理缓存的烦恼。

现在,你已经对 Server Actions 有了一个初步的印象。接下来,我们将深入其工作原理,并用更丰富的示例,带你一步步掌握这门强大的技术。

与表单共舞:Server Actions 的天作之合

Server Actions 和 HTML 的 <form> 标签是天生的好搭档。它们的结合,让处理用户输入变得前所未有的简单和优雅。我们将从一个最简单的表单开始,逐步为你揭示其中的奥秘。

第一步:最纯粹的表单

让我们从一个没有任何客户端 JavaScript 的场景开始。想象一个商品详情页,我们想添加一个“加入购物车”的按钮。

// app/actions/cartActions.js
'use server';

import { revalidatePath } from 'next/cache';

// 一个模拟的购物车
const cart = [];

export async function addToCart(formData) {
  const productId = formData.get('productId');
  console.log(`产品 #${productId} 已添加到购物车`);
  cart.push(productId);
  console.log('当前购物车:', cart);
  
  // 提示:在实际应用中,你可能想更新特定组件或页面
  // revalidatePath('/cart'); // 例如,让购物车页面数据重新生效
}
// app/products/[id]/page.js
import { addToCart } from '@/app/actions/cartActions';

export default function ProductPage({ params }) {
  const productId = params.id;

  return (
    <div>
      <h1>产品 #{productId}</h1>
      <p>这是一个很棒的产品。</p>
      
      <form action={addToCart}>
        <input type="hidden" name="productId" value={productId} />
        <button type="submit">加入购物车</button>
      </form>
    </div>
  );
}

发生了什么?

  1. 我们将 addToCart 这个 Server Action 直接传递给了 <form>action 属性。
  2. 当用户点击按钮时,浏览器会像提交传统表单一样,将表单内的数据(这里是隐藏的 productId)发送出去。
  3. Next.js 拦截了这个请求,并在服务器上执行了 addToCart 函数,将表单数据作为 FormData 对象传递给它。

最妙的是,即使用户的浏览器禁用了 JavaScript,这个功能依然可以工作! 这就是所谓的“渐进式增强”,是现代 Web 开发追求的黄金标准。

第二步:给用户一点反馈 (useFormStatus)

上面的例子很酷,但用户点击按钮后,页面会刷新,而且没有任何加载提示。在现代应用中,我们希望体验更流畅。当操作正在进行时,我们应该禁用按钮并显示一个加载指示器。

这时,useFormStatus Hook 就派上用场了。它是一个专门为 Server Actions 表单设计的 Hook,可以获取到父级 <form> 的提交状态。

重要提示useFormStatus 必须在作为 <form> 子组件的组件中使用,它不能和使用它的 <form> 在同一个组件里。

让我们创建一个可复用的 SubmitButton 组件:

// app/components/SubmitButton.js
'use client';

import { useFormStatus } from 'react-dom';

export function SubmitButton({ children }) {
  const { pending } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? '处理中...' : children}
    </button>
  );
}

现在,我们来更新一下产品页面的代码:

// app/products/[id]/page.js (更新后)
import { addToCart } from '@/app/actions/cartActions';
import { SubmitButton } from '@/app/components/SubmitButton';

export default function ProductPage({ params }) {
  const productId = params.id;

  return (
    <div>
      <h1>产品 #{productId}</h1>
      <p>这是一个很棒的产品。</p>
      
      <form action={addToCart}>
        <input type="hidden" name="productId" value={productId} />
        <SubmitButton>加入购物车</SubmitButton>
      </form>
    </div>
  );
}

现在,当用户点击按钮后,按钮会显示“处理中...”并且被禁用,直到服务器完成操作。这大大提升了用户体验,而且我们只做了一点小小的改动。

第三步:与用户对话 (useFormState)

我们已经能处理加载状态了,但如果操作完成后,我们想给用户一个明确的反馈,比如“添加成功!”或者“库存不足!”,该怎么办呢?

useFormState Hook 就是为此而生的。它允许你的 Server Action 返回一个状态,并在客户端进行响应。

让我们来改造一下 addToCart Action 和表单。

// app/actions/cartActions.js (更新后)
'use server';

// ...

export async function addToCart(previousState, formData) {
  const productId = formData.get('productId');

  // 模拟一个可能会失败的场景
  if (productId === '2') {
    return { message: '抱歉,该产品库存不足!', success: false };
  }

  console.log(`产品 #${productId} 已添加到购物车`);
  // ...
  return { message: `产品 #${productId} 已成功添加到购物车!`, success: true };
}

注意:使用了 useFormState 后,Server Action 的第一个参数会变成 previousState,即上一次的状态。

现在,我们来更新客户端组件,让它能够处理和显示这个返回的状态。

// app/products/[id]/page.js (最终版)
'use client'; // 因为要使用 Hook,所以需要标记为客户端组件

import { useFormState } from 'react-dom';
import { addToCart } from '@/app/actions/cartActions';
import { SubmitButton } from '@/app/components/SubmitButton';

const initialState = {
  message: '',
  success: false,
};

export default function ProductPage({ params }) {
  const productId = params.id;
  
  // useFormState 接收 action 和初始状态
  const [state, formAction] = useFormState(addToCart, initialState);

  return (
    <div>
      <h1>产品 #{productId}</h1>
      <p>这是一个很棒的产品。</p>
      
      <form action={formAction}>
        <input type="hidden" name="productId" value={productId} />
        <SubmitButton>加入购物车</SubmitButton>
      </form>

      {state.message && (
        <p style={{ color: state.success ? 'green' : 'red' }}>
          {state.message}
        </p>
      )}
    </div>
  );
}

发生了什么?

  1. 我们用 useFormState “包装”了我们的 addToCart Action。它返回了一个新的 formAction<form> 使用,以及一个 state 对象来存放 Action 的返回值。
  2. 当表单提交后,state 会被更新为 addToCart 函数返回的对象。
  3. 我们在 UI 中根据 state.messagestate.success 来显示相应的反馈信息。

通过这三步,我们从一个最基础的 HTML 表单,逐步构建起一个交互友好、状态明确的现代化 Web 表单。这就是 Server Actions 的威力所在:它尊重 Web 的基础,同时又提供了强大的现代化工具,让你能用最少的代码,创造出最棒的用户体验。

数据交互实战:构建一个简单的留言板

理论和简单的表单已经掌握,现在是时候进入真实世界的数据交互了。我们将通过构建一个简单的留言板,来完整地展示 Server Actions 在处理数据创建(Create)、读取(Read)和删除(Delete)等操作时的威力。

准备工作:数据存储和 Action 文件

首先,我们创建一个地方来存放我们的留言数据,并建立对应的 Action 文件。

// data/guestbook.js
// 在真实世界中,这里应该是你的数据库,比如 PostgreSQL, MongoDB 等。
// 为了简单起见,我们使用一个内存数组来模拟。
export const messages = [
  { id: 1, text: '你好,这是第一条留言!' },
  { id: 2, text: 'Server Actions 真的太酷了!' },
];
// app/actions/guestbookActions.js
'use server';

import { messages } from '@/data/guestbook';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';

export async function addMessage(formData) {
  const messageText = formData.get('message');
  
  if (!messageText) {
    // 在真实应用中,你应该返回一个错误状态,就像我们之前用 useFormState 做的那样
    return; 
  }

  const newMessage = {
    id: messages.length + 1,
    text: messageText,
  };

  messages.push(newMessage);

  // 这是关键!
  // 当我们添加了新留言后,需要告诉 Next.js 重新验证(即重新获取)
  // ‘/guestbook’ 路径的数据。
  revalidatePath('/guestbook');
}

export async function deleteMessage(messageId) {
    const index = messages.findIndex(msg => msg.id === messageId);
    if (index > -1) {
        messages.splice(index, 1);
    }

    // 同样,删除后也要更新UI
    revalidatePath('/guestbook');
}

构建留言板页面

现在,我们来创建显示留言和提交新留言的页面组件。

// app/guestbook/page.js
import { messages } from '@/data/guestbook';
import { addMessage, deleteMessage } from '@/app/actions/guestbookActions';
import { SubmitButton } from '@/app/components/SubmitButton'; // 复用我们之前创建的按钮

export default function GuestbookPage() {
  return (
    <div>
      <h1>留言板</h1>

      {/* 添加新留言的表单 */}
      <form action={addMessage} className="mb-8">
        <input
          type="text"
          name="message"
          placeholder="留下你的足迹..."
          className="border p-2 mr-2"
        />
        <SubmitButton>提交</SubmitButton>
      </form>

      {/* 显示所有留言 */}
      <ul>
        {messages.map((msg) => (
          <li key={msg.id} className="flex items-center justify-between mb-2">
            <span>{msg.text}</span>
            {/* 
              删除按钮的逻辑有些特别。
              我们不能直接在 form 的 action 里调用 deleteMessage(msg.id),
              因为 action 需要的是一个函数引用,而不是函数调用。
              
              这里我们使用 .bind(null, msg.id) 来创建一个新的函数,
              这个新函数在被调用时,会自动将 msg.id 作为第一个参数传给 deleteMessage。
            */}
            <form action={deleteMessage.bind(null, msg.id)}>
              <button type="submit" className="text-red-500 ml-4">删除</button>
            </form>
          </li>
        ))}
      </ul>
    </div>
  );
}

发生了什么?

  1. 数据读取 (Read):页面首次加载时,直接从 data/guestbook.js 导入 messages 数组并渲染出来。这是一个标准的服务器组件数据获取过程。
  2. 数据创建 (Create)addMessage Action 被绑定到留言表单的 action。当用户提交表单,Action 在服务器执行,向 messages 数组中添加新项。
  3. 数据删除 (Delete):每个留言旁边都有一个独立的删除表单。我们使用 deleteMessage.bind(null, msg.id) 这个技巧,为每个删除按钮创建了一个“定制版”的 Action,它在执行时已经知道了要删除哪条留言的 ID。
  4. UI 自动更新:无论是 addMessage 还是 deleteMessage,它们在完成数据库操作(这里是数组操作)后,都调用了 revalidatePath('/guestbook')。这个函数是 Server Actions 的核心魔法之一。它会清除服务器上关于 /guestbook 路径的数据缓存,并触发一次“重新渲染”。React 会智能地计算出 UI 的最小差异并更新浏览器中的内容,整个过程无需刷新页面,体验如丝般顺滑。

这个小小的留言板,已经完整地展示了 Server Actions 在数据驱动应用中的核心工作流程。它清晰、直接,并且将数据操作和 UI 更新紧密地联系在了一起。

进阶指南:让你的 Action 更上一层楼

掌握了基础之后,我们来看看如何让你的 Server Actions 变得更健壮、更高效。我们将简要地探讨三个重要主题:错误处理、性能优化和一些高级技巧。

优雅地处理错误

真实世界的代码充满了意外。网络问题、数据库错误、用户输入不合法……我们必须妥善处理这些情况。

在 Server Actions 中,处理错误的常用方法是结合我们之前学到的 useFormState。当错误发生时,返回一个包含错误信息的状态对象。

核心思想:

  1. 使用 try...catch:在你的 Action 函数中,将主要逻辑包裹在 try...catch 块中。
  2. 返回明确的错误状态:在 catch 块中,捕获到错误后,返回一个带有错误信息的对象,例如 { success: false, error: '数据库连接失败,请稍后再试。' }
  3. 在客户端显示错误:在你的组件中,通过 useFormState 获取这个状态,并在 UI 上清晰地展示错误信息给用户。
// app/actions/guestbookActions.js (增强版)
'use server';

// ...

export async function addMessage(previousState, formData) {
  try {
    const messageText = formData.get('message');
    if (!messageText || messageText.trim().length === 0) {
      return { success: false, error: '留言内容不能为空!' };
    }

    // ... (数据库操作逻辑)

    revalidatePath('/guestbook');
    return { success: true, message: '留言成功!' };

  } catch (e) {
    // 可能是数据库错误或其他意外
    console.error(e);
    return { success: false, error: '发生未知错误,请联系管理员。' };
  }
}

通过这种方式,你可以为用户提供清晰、友好的错误反馈,而不是让他们面对一个崩溃的页面。

性能优化:乐观更新 (Optimistic Updates)

当你点击“赞”或“发布评论”时,你可能注意到有些网站的界面会立刻发生变化,即使数据还在发送到服务器的路上。这种“先相信操作会成功”并立即更新 UI 的技术,就叫做“乐观更新”。它能极大地提升应用的感知速度。

React 提供了 useOptimistic Hook 来帮助我们轻松实现这一点。

核心思想:

  1. 定义乐观状态:使用 useOptimistic Hook,它会接收当前状态和一个“更新函数”。
  2. 立即更新UI:当用户执行操作时(例如提交表单),你立即调用 useOptimistic 返回的函数,并传入一个你“期望”的新状态。React 会用这个新状态来渲染 UI。
  3. 等待真实结果:与此同时,你的 Server Action 正常在后台执行。
  4. 同步最终状态:当 Server Action 完成后,Next.js 会像往常一样重新验证数据并返回最终的、真实的状态。React 会自动用这个真实状态替换掉之前的“乐观”状态,确保最终一致性。

这个技巧相对高级,但在追求极致用户体验的场景下非常有用。你可以查阅 Next.js 官方文档了解其具体用法。

总结与最佳实践

让我们最后回顾一下关键点和最佳实践:

  • Server Actions 是函数,不是 API 端点:转变你的思维方式。你正在调用一个可以直接访问后端的函数。
  • 拥抱渐进增强:尽可能从一个纯粹的 HTML <form> 开始,然后逐步添加 useFormStatususeFormState 来增强用户体验。
  • revalidatePathrevalidateTag 是你的好朋友:它们是连接数据操作和 UI 更新的桥梁,善用它们来保持数据同步。
  • 安全第一:始终在服务器端验证用户输入和权限。永远不要相信来自客户端的任何数据。
  • 明确职责:让 Server Actions 专注于处理数据和业务逻辑。UI 相关的状态管理(如加载、错误提示)则交给客户端的 Hooks。
  • 不是所有场景都适用:对于需要复杂、实时双向通信(如聊天应用)或纯粹的数据拉取(GET 请求),传统的 API 路由或 Next.js 15 的 fetch 仍然是更好的选择。

Server Actions 是 Next.js App Router 架构下的一个里程碑式的创新。它极大地简化了全栈开发模型,让我们能够用更少的代码、更统一的语言,构建出更快速、更健壮的 Web 应用。

解决flutter 在控制器如controller 无法直接访问私有类方法的问题

2025年8月21日 18:28

正常如果是getx 状态管理都是组件互相通信,不会有访问的问题 ,但是如果在不同的写法中,如最原始的私有类,里面通信确实遇到有这种问题,比如这个类是不挂在到weiget树上的,那么就相当于没有注册进树中,是不受管理的,如果是一个单独的私有类,不需要挂在树,那么就需要以下进行解决 1,先创建一个事件控制器 ,2段注册

class EventBusList {
  static final EventBusList _instance = EventBusList._internal();
  final _streamController = StreamController<String>.broadcast(); // 明确类型+广播

  factory EventBusList() => _instance; // 单例模式

  EventBusList._internal(); // 私有构造

  Stream<String> on() => _streamController.stream; // 直接返回stream

  void fire(String event) => _streamController.add(event);

  void dispose() => _streamController.close();
}

2,在控制器中直接调用传参

EventBusList().fire("lgoin");

3,在私有类中进行监听就可以,--这里如果不提前注册也就是相当于挂在到树中,那么就要打开初始化执行监听,那么就可以得到监听的数据---这个问了ai,ai是暂时没有做到很好的解决的,ai的回复都是先挂载(注册类) 其实很多场景不用注册挂载,都是临时调用和销毁

截屏2025-08-21 上午3.26.39.png

瀑布流布局的完整实现:从原理到实践

作者 晴空雨
2025年8月21日 18:27

前言

瀑布流布局(Waterfall Layout)是一种流行的网页布局方式,特别适用于展示不同高度的内容卡片,如图片画廊、商品列表等。本文将深入分析一个完整的瀑布流实现,从核心算法到响应式设计,带你全面理解瀑布流的实现原理。

👻 Talk is cheap, show me the code:

核心设计思路

本篇文章旨在提供思路,因此代码整体上会比较潦草且随意,勿怪勿怪!

1. 整体架构

这个瀑布流实现采用了面向对象的设计模式,主要包含两个核心类:

  • WaterfallOptions: 负责配置管理,包括列数和间距的响应式处理;
  • Waterfall: 继承自 WaterfallOptions,负责布局计算和渲染逻辑。

2. 响应式断点系统

const BREAKPOINTS = ["sx", "md", "lg", "xl", "2xl", "default"];
const BREAKPOINTS_MAP = {
  sx: 640,
  md: 768,
  lg: 1024,
  xl: 1280,
  "2xl": 1536,
};

这套断点系统支持多种屏幕尺寸的适配,用户可以为不同断点设置不同的列数和间距。

核心算法解析

1. 列高度追踪算法

瀑布流的核心是维护每列的高度,并始终将新元素放置到最短的列中:

__columnsHeight = Array(this.__columnsCount).fill(0);
__minColumnIndex = 0;

// 找到最短列的索引
this.__minColumnIndex = this.__columnsHeight.indexOf(
  Math.min(...this.__columnsHeight)
);

2. 元素定位计算

每个元素的位置通过以下公式计算:

// 垂直位置:当前最短列的高度
item.top = this.__columnsHeight[this.__minColumnIndex];

// 水平位置:列索引 × (元素宽度 + 间距)
item.left = this.__minColumnIndex * (this.__itemWidth + this.__gap);

3. 宽度自适应计算

元素宽度根据容器宽度和列数动态计算:

this.__itemWidth = 
  (this.__containerWidth - (this.__columnsCount - 1) * this.__gap) / 
  this.__columnsCount;

图片加载处理

异步加载策略

由于图片的高度需要在加载完成后才能确定,实现了智能的异步加载处理:

__paintChildWithImages($images, child) {
  let imageCount = 0;
  Array.from($images).forEach(($image) => {
    let $proxy = new Image();
    $proxy.src = $image.src;
    $proxy.onload = (event) => {
      // 根据图片原始尺寸计算显示高度
      child.height += (this.__itemWidth / event.target.naturalWidth) * 
                      event.target.naturalHeight;
      
      imageCount++;
      if (imageCount === $images.length) {
        child.hasLoaded = true;
        child.ratio = child.height / child.width;
        this.__paintChild(child);
      }
    };
  });
}

通过图片的原始尺寸和显示宽度,计算出准确的显示高度,确保布局的精确性。

CSS 配合实现

CSS 变量动态更新

JavaScript 通过 CSS 自定义属性与样式层进行通信:

this.__container.style.setProperty("--gap", `${this.__gap}px`);
this.__container.style.setProperty("--columns-count", this.__columnsCount);
this.__container.style.setProperty("--height", `${Math.max(...this.__columnsHeight)}px`);

流畅的动画效果

.waterfall-item {
  transform: translate(var(--left, 0), var(--top, 0)) translateZ(0);
  transition: box-shadow 0.2s linear, opacity 1s linear, transform 0.1s linear;
  will-change: transform, opacity, visibility;
}

使用 translateZ(0) 开启硬件加速,will-change 属性优化动画性能。

响应式配置系统

灵活的配置选项

支持多种配置方式,既可以设置固定值,也可以为不同断点设置不同的值:

// 使用示例
const waterfall = new Waterfall($container, {
  columns: {
    sx: 3,
    md: 4,
    lg: 5,
    xl: 6,
    "2xl": 8,
    default: 10
  },
  gap: {
    sx: '0.5rem',
    md: '1em', 
    lg: '12px',
    xl: 16,
    "2xl": 24,
    default: '2%',
  },
});

单位转换处理

支持多种 CSS 单位,并能正确转换为像素值:

__extractGapNumberValue(gap) {
  if (/^\d+(.\d+)?(rem|em|%)$/.test(gap)) {
    const $div = document.createElement("div");
    $div.style.width = gap;
    document.body.appendChild($div);
    const { width } = $div.getBoundingClientRect();
    document.body.removeChild($div);
    return width;
  }
  return parseInt(gap);
}

性能优化策略

文档片段优化

使用 DocumentFragment 减少 DOM 操作次数:

const $fragment = document.createDocumentFragment();
Array.from(images).map((image, index) => {
  const child = this.__initItem(image, this.__children.length + index);
  this.__children.push(child);
  $fragment.appendChild(child.$dom);
});
this.__container.appendChild($fragment);

延迟显示

通过 CSS 类控制元素的显示时机,提供更好的用户体验:

item.timer = setTimeout(() => {
  item.$dom.classList.remove("waterfall-item__pending");
}, 300);

使用方式

// 创建瀑布流实例
const waterfall = new Waterfall(container, options);

// 添加内容
waterfall.append(images);

// 销毁实例
waterfall.destroy();

总结

这个瀑布流实现的亮点在于:

  1. 完整的响应式支持 - 支持多断点配置,适应各种屏幕尺寸
  2. 智能的图片加载处理 - 异步计算图片高度,确保布局准确性
  3. 优秀的性能表现 - 使用多种优化策略,保证流畅的用户体验
  4. 灵活的配置系统 - 支持多种单位和配置方式
  5. 现代化的实现方式 - 使用 ES6+ 语法,代码结构清晰

通过这种实现方式,我们可以创建出既美观又高性能的瀑布流布局,为用户提供优秀的浏览体验。无论是图片画廊、商品展示还是内容聚合页面,这套方案都能很好地满足需求。

AI知识管理软件推荐:九大解决方案与企业应用

作者 探码科技
2025年8月21日 18:27

引言

几乎所有企业都能从知识库中受益。而人工智能(AI)的出现,正将传统的知识管理流程推向全新高度,为支持团队和组织创造更大价值。

企业既可以在内部使用知识库,也可以面向客户构建外部资源。这类面向客户的知识库通常被称为自助服务(Self-Service)或自助解决方案。客户服务团队、客户体验(CX)团队、IT服务管理(ITSM)团队以及外包供应商都能从中受益。

根据 Salesforce 的数据,89%的客户在获得积极的客户服务后会保持忠诚,其中就包括使用自助工具、知识内容和AI驱动的聊天机器人。事实证明,AI工具与软件能够显著提升知识库内容的创建、维护和上下文关联,因此,AI知识管理已经成为企业的必然选择。

本文将定义什么是AI知识管理,并重点介绍市场上九款领先的软件解决方案。

什么是AI知识管理?

AI知识管理是指利用人工智能工具来收集、创建、整理和优化面向内部和客户的知识库与资源。AI在以下几个方面发挥作用:

收集

AI工具能够自动收集信息。过去人工客服难以完整记录客户交互中的所有对话或内容,而早期的自动化系统也仅限于存储数据,仍需人工整理才能转化为知识文章。如今,借助AI,这些过程可以全自动完成,并且大规模提升数据质量。

创建

在客户互动和工单数据被自动采集后,具备生成式AI能力的工具可以直接生成知识内容。通过自然语言处理(NLP)、机器学习等技术,AI能够帮助企业快速构建新的知识资源,更高效地支持客户需求。

内容管理

所有知识都需要分类、整理与去重,以确保最相关的文档、文章和内容能够被有效管理。AI简化了这一过程,能够在大规模数据中自动识别并整合有价值的信息。

可发现性

如果没有智能的知识发现与搜索工具,用户在需要时仍然难以快速找到内容。优秀的AI知识管理平台通常具备强大的智能搜索与推荐功能,确保用户能快速定位所需信息,从而显著提升体验。

与其尝试从零构建内部AI知识管理系统,大多数企业会发现使用现成的SaaS解决方案更具成本效益。以下,我们将基于功能与应用场景,介绍九款顶级AI知识管理软件。。

9 款顶级人工智能知识管理解决方案

1.Genesys

Genesys是一款基于人工智能的呼叫中心客户服务代理 SaaS 平台,现已包含基于人工智能的知识管理工具和功能。

功能概述:

帮助客服人员、客户体验团队负责人及其他管理人员更轻松地自动记录知识管理数据与信息,并将其转化为知识库问答内容。

优点:

  1. 帮助呼叫中心和中小企业(SMBs)改善全渠道客户互动体验

  2. AI工具可自动记录客户交互并转化为内部知识管理资源

  3. 具备可扩展的解决方案和全球服务能力

不足:

  • 以系统配置和管理复杂著称

  • 新团队需要较陡峭的学习曲线来掌握使用方法

  • 对于预算有限的初创公司或内部团队可能成本过高

定价: 每位坐席每月75美元起

2.AIGEN

AIGEN是来自印度的软件供应商,提供基于SaaS的定制化AI知识管理工具。

核心功能:

与列表中的其他工具类似,提供基础的知识管理和AI集成功能。

优势:

  • 通过API与其他客户体验软件集成

  • 支持多种格式的客服数据和知识管理数据导入导出

  • 可处理海量数据

不足:

  • 功能先进性不及列表中其他工具

  • 相比竞品定价偏高

  • 客户服务质量有待提升

定价: 定制化报价

3.Lucy.ai

Lucy是一款以AI为核心的知识抓取与更新应用。

核心功能:

定位为一站式知识管理解决方案,具备AI驱动的知识捕获与更新功能的SaaS知识管理解决方案。

优势:

  • 易于设置

  • 可无缝集成客户体验和IT服务管理团队常用系统

  • 专为企业组织知识构建统一入口,尤其适合客户体验和ITSM团队

不足:

  • 对中小企业和初创公司价格偏高

**定价:**年度预付套餐起价49,500美元,此为最低定制报价

4.Starmind

Starmind是配备实用AI功能的智能知识管理目录系统。

核心功能:

被定义为"唯一基于AI的实时全组织专家网络",适用于客户服务、销售及支持内部AI解决方案(如大语言模型)等多种场景。

优势:

  • 部署使用简便

  • 支持匿名提问

  • 提供移动端应用,适合远程办公场景

不足:

  • 回答准确性存在波动

  • 无法设置查询优先级

**定价:**每位用户每月6美元起

5.Guru

Guru致力于让内部信息、企业数据和知识管理文档对所有需求者更易获取。

它能做什么?

AI知识管理助手,也称为AI驱动的机器人,用于内部知识库。

优点:

  • 与团队使用的其他软件和应用程序集成时,是一个有用的资源

  • 对新员工入职很有帮助

  • 可以生成现有文档的摘要

缺点:

  • 目前AI驱动的答案仍处于测试阶段,可能不如您的客户体验(CX)或ITSM代理所需的准确

  • 设计虽然直观,但仍需改进

  • 平台内的知识有效性取决于团队更新文章的频率

价格: 每位用户每月12美元起

5.Korra

Korra是一款AI工具,帮助客户服务和ITSM代理快速找到所需信息的答案。

它能做什么?

Korra被描述为GPT的进化版,即OpenAI的ChatGPT-4,作为客户服务、客户体验(CX)和ITSM团队的支持平台。

优点:

  • 易于设置和实施

  • 使用自然语言处理(NLP)和AI生成标签和支持文档

  • 有帮助的客户支持

缺点:

  • 生成的结果并不总是非常优化,可能是由于依赖GPT-4创建内容

  • 除非以非原生格式(如PDF)存储,否则无法与Google Docs或其他Google Workspace产品集成

  • 难以针对每个用例进行定制

价格: 商业计划每月99美元起

6.Capacity

Capacity是一款能够互联并整合您所有客户体验(CX)和IT服务管理(ITSM)技术栈的AI支持工具,类似于Zapier。

它有什么功能?

“Capacity的知识库智能存储能够改变您组织的知识。”它是一个AI驱动的知识库和知识管理系统,适用于CX和ITSM,同时对销售、营销和人力资源(HR)等其他团队也很有用。

优点:

  • 适用于自动化以前手动记录的功能

  • 跨职能,因此可以在整个组织中部署

  • 随着输入和数据的增加,它会变得更智能

缺点:

  • 在大型团队中使用时成本较高

  • 产品仍在学习和进化,因此存在一些限制

  • 对于没有使用过AI工具的人来说,学习曲线较陡

价格: 从每位用户每月49美元起

7.Tettra

Tettra是另一款强大的企业级AI知识管理应用。

它有什么功能?

Tettra是一款AI工具,允许您“通过Tettra的简单编辑器创建新的内部文档,或利用现有内容(如Google Docs、Notion、本地文件等)快速构建您的知识库。”

优点:

  • 包含多种功能,如AI知识管理和AI知识库

  • 支持Slack集成,AI可以实时回答团队问题

  • 保存可重复使用的答案,并将其转化为知识库文章

缺点:

  • 目前集成有限

  • 基于提供信息生成的答案并不总是准确

定价: 每位用户每月5美元起(最低需10人起订,即每月50美元)

8.ChatGPT-4

OpenAI的ChatGPT-4无需赘述,正是它掀起了这场AI革命。微软是其主要合作伙伴和投资者,数以千计的创业者正基于GPT-4开发AI驱动的SaaS工具。

核心功能

与列表中的其他工具不同,GPT-4并非专为知识管理设计。但通过基于GPT-4开发或集成该模型的应用程序,您可将其用于知识管理场景。

优势:

  • 高度通用的LLM大语言模型,可适配包括知识管理在内的多种场景

  • 大量应用通过集成GPT-4实现知识管理功能(本列表中就有多个案例)

不足:

  • 若无配套集成和应用,直接使用GPT-4存在技术门槛,需通过API或第三方平台实现

  • 虽然功能强大但仍存在局限,且存在众所周知的数据隐私隐患

定价: 每位用户每月20美元起

9.体验Baklib的不同之处

是时候让您的企业体验Baklib的独特之处了吗?通过云端客户服务软件提升您的客户支持能力。

简洁纯净的设计。将普通客户转化为忠实粉丝、品牌大使和传播者。

Baklib已推出基于AI的新功能,您现在就能在我们的软件解决方案中享受AI带来的优势。立即试用Baklib

Baklib 是新一代 AI 知识库于数字体验管理平台,托管超过1000 家企业的网站和在线文档。其流行源于出色的灵活性和开源主题生态系统,使用户能够根据多样化需求定制网站、在线文档和知识库系统。Baklib独创的资源库+知识库+体验库三层架构设计,一方面满足企业一体化数字内容管理,另一方面又满足企业构建多场景的应用网站。无论是跨国多语言站点构建,还是内外部知识库建设,客户帮助中心,产品手册搭建,都在一个地方完成。选择了Baklib作为其内容管理平台,主要因其卓越的优化能力。

主要特点:

  • 强大的内容编辑能力,支持一键导入、导出,以及富文本和 Markdown格式编辑。

  • 开源的主题模板能力,方便企业高度定制化开发千站千面的前端界面。

  • 内置GEO/SEO优化工具,助力内容优化。

  • 内置 AI 私有知识库功能,包括 AI 自动化标签、AI 智能搜索和多轮会话。

核心结论:投资AI知识管理的时机已至

AI能更精准理解客户咨询、智能分配服务人员,为CX和ITSM节省时间成本,同时持续优化知识库质量。积极研究并应用这项新技术的企业将可以获得AI为知识管理带来的诸多优势。

每日一个知识点:JavaScript 箭头函数与普通函数比较

作者 Miracle_G
2025年8月21日 18:21
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>JS箭头函数 vs 普通函数</title>
    <style>
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        }
        
        body {
            background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
            color: #333;
            line-height: 1.6;
            padding: 20px;
            min-height: 100vh;
        }
        
        .container {
            max-width: 1200px;
            margin: 0 auto;
        }
        
        header {
            text-align: center;
            margin-bottom: 40px;
            padding: 20px;
        }
        
        h1 {
            font-size: 2.5rem;
            color: #2c3e50;
            margin-bottom: 10px;
        }
        
        .subtitle {
            font-size: 1.2rem;
            color: #7f8c8d;
        }
        
        .comparison {
            display: flex;
            flex-wrap: wrap;
            gap: 20px;
            margin-bottom: 40px;
        }
        
        .card {
            flex: 1;
            min-width: 300px;
            background: white;
            border-radius: 10px;
            box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
            padding: 25px;
            transition: transform 0.3s ease;
        }
        
        .card:hover {
            transform: translateY(-5px);
        }
        
        .card h2 {
            color: #2c3e50;
            margin-bottom: 20px;
            padding-bottom: 10px;
            border-bottom: 2px solid #3498db;
        }
        
        .difference {
            margin-bottom: 15px;
        }
        
        .difference h3 {
            color: #3498db;
            margin-bottom: 5px;
        }
        
        .code-example {
            background: #2c3e50;
            color: #f8f8f2;
            padding: 15px;
            border-radius: 5px;
            margin: 15px 0;
            overflow-x: auto;
            font-family: 'Consolas', monospace;
        }
        
        .keyword {
            color: #f92672;
        }
        
        .function {
            color: #66d9ef;
        }
        
        .comment {
            color: #75715e;
        }
        
        .usage {
            background: white;
            border-radius: 10px;
            box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
            padding: 25px;
            margin-bottom: 40px;
        }
        
        .usage h2 {
            color: #2c3e50;
            margin-bottom: 20px;
            padding-bottom: 10px;
            border-bottom: 2px solid #3498db;
        }
        
        .scenarios {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
            gap: 20px;
        }
        
        .scenario {
            background: #f8f9fa;
            padding: 15px;
            border-radius: 5px;
            border-left: 4px solid #3498db;
        }
        
        .scenario h3 {
            color: #2c3e50;
            margin-bottom: 10px;
        }
        
        .conclusion {
            background: white;
            border-radius: 10px;
            box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
            padding: 25px;
            text-align: center;
        }
        
        .conclusion h2 {
            color: #2c3e50;
            margin-bottom: 20px;
        }
        
        .highlight {
            background: linear-gradient(120deg, #a1c4fd 0%, #c2e9fb 100%);
            padding: 25px;
            border-radius: 10px;
            margin-top: 20px;
        }
        
        footer {
            text-align: center;
            margin-top: 40px;
            color: #7f8c8d;
        }
        
        @media (max-width: 768px) {
            .comparison {
                flex-direction: column;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <header>
            <h1>JavaScript 箭头函数与普通函数</h1>
            <p class="subtitle">全面比较两种函数类型的区别与适用场景</p>
        </header>
        
        <div class="comparison">
            <div class="card">
                <h2>普通函数</h2>
                
                <div class="difference">
                    <h3>this 绑定</h3>
                    <p>拥有自己的 <code>this</code> 上下文,取决于调用方式</p>
                    <div class="code-example">
                        <span class="keyword">function</span> <span class="function">Person</span>() {<br>
                        &nbsp;&nbsp;this.age = 0;<br>
                        &nbsp;&nbsp;setInterval(<span class="keyword">function</span> growUp() {<br>
                        &nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// 这里的this指向全局对象或undefined</span><br>
                        &nbsp;&nbsp;&nbsp;&nbsp;this.age++;<br>
                        &nbsp;&nbsp;}, 1000);<br>
                        }
                    </div>
                </div>
                
                <div class="difference">
                    <h3>构造函数</h3>
                    <p>可以用作构造函数,使用 <code>new</code> 关键字</p>
                    <div class="code-example">
                        <span class="keyword">function</span> <span class="function">Car</span>(make, model) {<br>
                        &nbsp;&nbsp;this.make = make;<br>
                        &nbsp;&nbsp;this.model = model;<br>
                        }<br><br>
                        const myCar = <span class="keyword">new</span> Car('Toyota', 'Camry');
                    </div>
                </div>
                
                <div class="difference">
                    <h3>arguments 对象</h3>
                    <p>有自己的 <code>arguments</code> 对象,包含所有传入参数</p>
                    <div class="code-example">
                        <span class="keyword">function</span> <span class="function">showArgs</span>() {<br>
                        &nbsp;&nbsp;console.log(arguments);<br>
                        &nbsp;&nbsp;<span class="comment">// 输出: [1, 2, 3, callee: ƒ, Symbol(...): ...]</span><br>
                        }<br><br>
                        showArgs(1, 2, 3);
                    </div>
                </div>
            </div>
            
            <div class="card">
                <h2>箭头函数</h2>
                
                <div class="difference">
                    <h3>this 绑定</h3>
                    <p>没有自己的 <code>this</code>,继承自父作用域</p>
                    <div class="code-example">
                        <span class="keyword">function</span> <span class="function">Person</span>() {<br>
                        &nbsp;&nbsp;this.age = 0;<br>
                        &nbsp;&nbsp;setInterval(() => {<br>
                        &nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// 这里的this继承自Person函数</span><br>
                        &nbsp;&nbsp;&nbsp;&nbsp;this.age++;<br>
                        &nbsp;&nbsp;}, 1000);<br>
                        }
                    </div>
                </div>
                
                <div class="difference">
                    <h3>构造函数</h3>
                    <p>不能用作构造函数,使用 <code>new</code> 会抛出错误</p>
                    <div class="code-example">
                        const Car = (make, model) => {<br>
                        &nbsp;&nbsp;this.make = make;<br>
                        &nbsp;&nbsp;this.model = model;<br>
                        };<br><br>
                        <span class="comment">// 抛出TypeError: Car is not a constructor</span><br>
                        const myCar = <span class="keyword">new</span> Car('Toyota', 'Camry');
                    </div>
                </div>
                
                <div class="difference">
                    <h3>arguments 对象</h3>
                    <p>没有自己的 <code>arguments</code> 对象,但可以访问外部函数的arguments</p>
                    <div class="code-example">
                        <span class="keyword">function</span> <span class="function">outer</span>(a, b) {<br>
                        &nbsp;&nbsp;const inner = () => {<br>
                        &nbsp;&nbsp;&nbsp;&nbsp;console.log(arguments);<br>
                        &nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// 输出: [1, 2, callee: ƒ, Symbol(...): ...]</span><br>
                        &nbsp;&nbsp;};<br>
                        &nbsp;&nbsp;inner();<br>
                        }<br><br>
                        outer(1, 2);
                    </div>
                </div>
            </div>
        </div>
        
        <div class="usage">
            <h2>使用场景建议</h2>
            <div class="scenarios">
                <div class="scenario">
                    <h3>使用箭头函数的情况</h3>
                    <ul>
                        <li>需要继承外部this上下文时</li>
                        <li>简短的回调函数</li>
                        <li>函数式编程(map、filter、reduce等)</li>
                        <li>不需要自己this绑定的函数</li>
                    </ul>
                </div>
                
                <div class="scenario">
                    <h3>使用普通函数的情况</h3>
                    <ul>
                        <li>需要作为构造函数使用时</li>
                        <li>需要可变的this上下文</li>
                        <li>需要访问arguments对象</li>
                        <li>对象的方法(通常需要访问对象本身)</li>
                        <li>生成器函数(function*)</li>
                    </ul>
                </div>
            </div>
            
            <div class="highlight">
                <h3>关键区别总结</h3>
                <p>箭头函数是ES6引入的语法糖,主要优点是更简洁的语法和词法作用域的this绑定。</p>
                <p>普通函数更适合需要动态上下文、构造函数或需要arguments对象的场景。</p>
            </div>
        </div>
        
        <div class="conclusion">
            <h2>总结</h2>
            <p>箭头函数和普通函数各有其用途,理解它们的区别对于编写正确的JavaScript代码至关重要。</p>
            <p>在大多数现代JavaScript开发中,箭头函数因其简洁性和更可预测的this绑定而更受欢迎,</p>
            <p>但在某些特定场景下,普通函数仍然是不可或缺的。</p>
        </div>
        
        <footer>
            <p>© 2023 JavaScript函数比较 | 设计用于教育目的</p>
        </footer>
    </div>
</body>
</html>

在 JavaScript 中,箭头函数(Arrow Function)是 ES6 引入的一种简化函数语法的特性,与普通函数(Function Declaration/Expression)在行为和特性上有显著差异。以下从多个维度对比两者的核心区别,并结合实际场景说明适用场景。

1. 语法简洁性

箭头函数的语法更简洁,省略了 function关键字(部分场景可省略大括号和 return),适合需要匿名函数的场景(如回调)。

普通函数示例

// 函数表达式
const add = function(a, b) {
  return a + b;
};

// 函数声明
function multiply(a, b) {
  return a * b;
}

箭头函数示例

const add = (a, b) => a + b; // 单行直接返回,省略大括号和 return
const logName = name => console.log(name); // 单个参数可省略参数括号

2. this的指向

这是两者最核心的差异。普通函数的 this动态绑定的(取决于调用方式),而箭头函数的 this词法绑定的(继承自外层作用域,定义时确定,无法修改)。

普通函数的 this:

  • 全局调用:this指向全局对象(浏览器中为 window,Node.js 中为 global)。

  • 对象方法调用:this指向调用该方法的对象。

  • 构造函数调用:this指向新创建的实例对象。

  • call/apply/bind调用:this被显式指定为目标对象。

示例

const obj = {
  name: "Alice",
  sayHello: function() {
    console.log(`Hello, ${this.name}`); // this 指向 obj
  }
};
obj.sayHello(); // 输出 "Hello, Alice"

箭头函数的 this:

  • 无独立 this,直接继承外层作用域的 this(无论是否通过 call/apply/bind修改)。

  • 常用于避免回调函数中 this丢失的问题(如定时器、事件监听器)。

示例

const obj = {
  name: "Alice",
  sayHello: function() {
    setTimeout(() => {
      console.log(`Hello, ${this.name}`); // 箭头函数的 this 继承自外层的 sayHello(this 指向 obj)
    }, 1000);
  }
};
obj.sayHello(); // 1秒后输出 "Hello, Alice"(普通函数 setTimeout 回调的 this 会指向 window)

3. 构造函数与 new

普通函数可以作为构造函数(通过 new调用),而箭头函数不能作为构造函数(调用 new会报错)。

普通函数作为构造函数:

function Person(name) {
  this.name = name;
}
const p = new Person("Bob");
console.log(p.name); // 输出 "Bob"(this 指向新实例 p)

箭头函数尝试 new:

const Animal = (name) => {
  this.name = name; // 箭头函数无独立 this,此处 this 指向外层作用域(如全局 window)
};
const a = new Animal("Cat"); // 报错:Animal is not a constructor

4. arguments对象

普通函数内部可通过 arguments对象访问所有传入的参数(类数组),而箭头函数没有自己的 arguments对象(但可通过剩余参数 ...args实现类似效果)。

普通函数使用 arguments:

function sum() {
  let total = 0;
  for (let i = 0; i < arguments.length; i++) {
    total += arguments[i];
  }
  return total;
}
sum(1, 2, 3); // 输出 6

箭头函数使用剩余参数替代:

const sum = (...args) => {
  return args.reduce((total, num) => total + num, 0);
};
sum(1, 2, 3); // 输出 6

5. yield与生成器函数

箭头函数不能使用 yield关键字(除非在嵌套函数中),因此无法作为生成器函数(Generator Function)。普通函数可以通过 function*声明生成器。

示例

// 普通生成器函数
function* gen() {
  yield 1;
  yield 2;
}
const g = gen();
console.log(g.next()); // { value: 1, done: false }

// 箭头函数尝试使用 yield(报错)
const badGen = () => {
  yield 1; // 语法错误:Unexpected token 'yield'
};

6. 原型与 prototype属性

普通函数拥有 prototype属性(用于存储实例方法),而箭头函数没有 prototype属性(因为无法作为构造函数)。

验证

function foo() {}
console.log(foo.prototype); // { constructor: foo }(存在 prototype)

const bar = () => {};
console.log(bar.prototype); // undefined(无 prototype)

7. 适用场景对比

总结

箭头函数是普通函数的语法糖,但其核心差异在于 this的绑定机制和设计目标。箭头函数更适合需要固定 this上下文的场景(如回调、函数式编程),而普通函数则适用于需要动态 this作为构造函数的场景。实际开发中需根据需求选择,避免因 this指向错误导致问题。

Error: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.32' not found

作者 unfetteredman
2025年8月21日 18:14

Vite在Linux环境中构建react项目失败

本地没问题, 发布测试环境有问题, 详细错误信息如下

`[17:05:58] > tsc -b && vite build`

`[17:05:58] `

`[17:06:03] /root/workspace/project/node_modules/.pnpm/rollup@4.47.0/node_modules/rollup/dist/native.js:64`

`[17:06:03]  throw new Error(`

`[17:06:03]        ^`

`[17:06:03] `

``[17:06:03] Error: Cannot find module @rollup/rollup-linux-x64-gnu. npm has a bug related to optional dependencies (https://github.com/npm/cli/issues/4828). Please try `npm i` again after removing both package-lock.json and node_modules directory.``

`[17:06:03]     at requireWithFriendlyError (/root/workspace/project/node_modules/.pnpm/rollup@4.47.0/node_modules/rollup/dist/native.js:64:9)`

`[17:06:03]     at Object.<anonymous> (/root/workspace/project/node_modules/.pnpm/rollup@4.47.0/node_modules/rollup/dist/native.js:73:76)`

`[17:06:03]     at Module._compile (node:internal/modules/cjs/loader:1554:14)`

`[17:06:03]     at Object..js (node:internal/modules/cjs/loader:1706:10)`

`[17:06:03]     at Module.load (node:internal/modules/cjs/loader:1289:32)`

`[17:06:03]     ... 2 lines matching cause stack trace ...`

`[17:06:03]     at wrapModuleLoad (node:internal/modules/cjs/loader:220:24)`

`[17:06:03]     at cjsLoader (node:internal/modules/esm/translators:262:5)`

`[17:06:03]     at ModuleWrap.<anonymous> (node:internal/modules/esm/translators:196:7) {`

``[17:06:03]   [cause]: Error: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.32' not found (required by /root/workspace/project/node_modules/.pnpm/@rollup+rollup-linux-x64-gnu@4.47.0/node_modules/@rollup/rollup-linux-x64-gnu/rollup.linux-x64-gnu.node)``

`[17:06:03]       at Object..node (node:internal/modules/cjs/loader:1732:18)`

`[17:06:03]       at Module.load (node:internal/modules/cjs/loader:1289:32)`

`[17:06:03]       at Function._load (node:internal/modules/cjs/loader:1108:12)`

`[17:06:03]       at TracingChannel.traceSync (node:diagnostics_channel:322:14)`

`[17:06:03]       at wrapModuleLoad (node:internal/modules/cjs/loader:220:24)`

`[17:06:03]       at Module.require (node:internal/modules/cjs/loader:1311:12)`

`[17:06:03]       at require (node:internal/modules/helpers:136:16)`

`[17:06:03]       at requireWithFriendlyError (/root/workspace/project/node_modules/.pnpm/rollup@4.47.0/node_modules/rollup/dist/native.js:46:10)`

`[17:06:03]       at Object.<anonymous> (/root/workspace/project/node_modules/.pnpm/rollup@4.47.0/node_modules/rollup/dist/native.js:73:76)`

`[17:06:03]       at Module._compile (node:internal/modules/cjs/loader:1554:14) {`

`[17:06:03]     code: 'ERR_DLOPEN_FAILED'`

`[17:06:03]   }`

`[17:06:03] }`

`[17:06:03] `

`[17:06:03] Node.js v22.14.0`

`[17:06:03]  ELIFECYCLE  Command failed with exit code 1.`

大致意思就是GLIBC_2.32, 找不到对应的 @rollup/rollup-linux-x64-gnu. 这个包,导致报错

先说解决方案

1、在package.json中加入如下代码

  "optionalDependencies": {
    "@rollup/rollup-linux-x64-gnu": "*"
  }

2、如果使用pnpm或者其他包安装工具, 重新install 一下更新一下lock文件即可

注意⚠️

如果不重新install, lock文件不更新还是会导致安装失败哦

写在最后

如果大家有更好的解决方案, 欢迎大家留言哦!

C++之模板函数

作者 long_run
2025年8月21日 18:12

你好!很高兴为你讲解C++模板函数。作为初学者,这是一个非常重要的概念,理解它会极大提升你的编程能力。我会用非常详细、循序渐进的方式为你解释。

1. 什么是模板函数?为什么需要它?

想象一个场景:你需要写一个函数来比较两个数的大小,并返回较大的那个。一开始你可能只需要比较int类型:

int max(int a, int b) {
    return (a > b) ? a : b;
}

但很快你发现,还需要比较double类型、float类型等等。如果没有模板,你可能需要写一大堆重复的代码:

int max(int a, int b) { /* ... */ }
double max(double a, double b) { /* ... */ }
float max(float a, float b) { /* ... */ }
// ... 为每种类型都写一个!

这太麻烦了!代码几乎一模一样,唯一的区别只是参数和返回值的类型不同。

模板函数(Function Template) 就是为了解决这个问题而生的。它允许你编写一个“函数蓝图”,其中的类型可以作为参数。编译器会根据你的使用情况,用具体的类型替换这个参数,自动为你生成对应类型的函数版本。

核心思想: 将数据类型参数化,实现代码复用。


2. 模板函数的基本语法

声明与定义

使用关键字 template 开头,后面跟着模板参数列表(用尖括号 <> 括起来),然后是普通的函数声明/定义。

template <typename T> // 或者 template <class T>
T myMax(T a, T b) {
    return (a > b) ? a : b;
}

让我们逐句解析:

  • template <typename T>:这告诉编译器“下面我要定义一个模板,其中有一个类型参数,我暂时叫它 T”。

    • typename TT 是一个占位符,代表某种数据类型。当你使用这个模板时,T 会被替换成实际的类型(如 int, double, string 等)。
    • 你也可以用 class T,在这里 typenameclass 是完全等价的,但 typename 更直观,因为它表示的是一个类型(Type),而不是一个类(Class)。现代C++中更推荐使用 typename
  • T myMax(T a, T b):这就是我们的函数。它的两个参数 ab 都是类型 T,返回值也是类型 T

如何使用(实例化)

使用模板函数非常简单,就像使用普通函数一样。编译器会自动推导类型。

#include <iostream>
#include <string>
using namespace std;

template <typename T>
T myMax(T a, T b) {
    return (a > b) ? a : b;
}

int main() {
    // 1. 用于 int 类型
    int i1 = 10, i2 = 20;
    cout << myMax(i1, i2) << endl; // 输出 20
    // 编译器看到int参数,会自动生成并调用 int myMax(int, int)

    // 2. 用于 double 类型
    double d1 = 3.14, d2 = 2.71;
    cout << myMax(d1, d2) << endl; // 输出 3.14
    // 编译器生成 double myMax(double, double)

    // 3. 用于 std::string 类型
    string s1 = "hello", s2 = "world";
    cout << myMax(s1, s2) << endl; // 输出 "world" (按字典序比较)
    // 编译器生成 string myMax(string, string)

    return 0;
}

你也可以显式指定类型,这在某些编译器无法自动推导的情况下很有用:

cout << myMax<int>(3, 7); // 显式告诉编译器 T 是 int

3. 模板参数推导

编译器非常智能,它通过查看你调用函数时传入的实参类型来推导模板参数 T 应该是什么。

在上面的例子中:

  • myMax(i1, i2) 传入两个 int,所以 T 被推导为 int
  • myMax(d1, d2) 传入两个 double,所以 T 被推导为 double

一个重要限制:自动推导要求所有参数的类型推导必须一致。

int a = 1;
double b = 2.5;
cout << myMax(a, b); // 错误!编译器懵了:T 到底是 int 还是 double?

解决这个错误有三种方法:

  1. 强制转换myMax(a, (int)b);
  2. 显式指定类型myMax<double>(a, b); // 告诉编译器 T 是 double,int 型的 a 会被隐式转换为 double
  3. 使用多个模板参数(见下一节)

4. 模板函数的进阶用法

a) 多个模板参数

一个模板可以有多个类型参数。这样就可以解决上面参数类型不一致的问题。

template <typename T1, typename T2> // 有两个类型参数:T1 和 T2
T1 myMixedMax(T1 a, T2 b) { // 注意返回值类型是 T1
    return (a > b) ? a : static_cast<T1>(b); // 需要将 b 转换为 T1 类型再比较
}

int main() {
    int a = 1;
    double b = 2.5;
    cout << myMixedMax(a, b) << endl; // 输出 2 (b被转成int后是2)
    cout << myMixedMax(b, a) << endl; // 输出 2.5
    return 0;
}

在这个例子中,T1T2 可以被推导成不同的类型。

b) 非类型模板参数

模板参数不一定非得是类型,也可以是整型、枚举或指针等

// 定义一个函数,将数组的所有元素都乘以一个因子
template <typename T, int Factor> // 一个类型参数 T,一个整型参数 Factor
void scale(T& array, int size) {
    for (int i = 0; i < size; ++i) {
        array[i] *= Factor;
    }
}

int main() {
    int arr[] = {1, 2, 3, 4};
    scale<int, 10>(arr, 4); // Factor 被替换为 10
    // 现在 arr 变成了 {10, 20, 30, 40}
    return 0;
}

注意:非类型模板参数必须是编译期常量。


5. 注意事项和常见问题

  1. 模板不是函数:模板是生成函数的蓝图。它本身不占用内存。只有在被使用(实例化)时,编译器才会根据模板生成具体的函数代码(这个过程叫实例化)。myMax<int>myMax<double> 是两个完全不同的函数。

  2. 模板定义必须可见:通常你会将模板的声明和定义都放在头文件(.hpp 或 .h) 里。这是因为编译器需要在编译时看到完整的模板定义才能为它实例化出具体版本的代码。这与普通函数(声明放.h,实现放.cpp)不同。

  3. 理解“泛型”:模板是泛型编程的基础。你写的 myMax 函数是“泛型”的,只要类型 T 支持函数体内用到的操作(例如 > 运算符),它就可以工作。这就是为什么它既能用于数字,也能用于 string(因为 string 重载了 > 运算符)。

  4. 编译错误信息可能很复杂:如果模板实例化失败(例如,你试图用 myMax 比较两个自定义类对象,但这个类没有定义 > 操作符),编译器报错信息可能会又长又难懂。这是使用模板的一个常见痛点,需要慢慢习惯。


总结

特性 解释
目的 编写与类型无关的通用代码,实现代码复用。
关键字 template <typename T>template <class T>
核心 类型参数化(用 T, U 等占位符表示)。
工作原理 编译器根据调用时传入的实际类型,自动实例化出具体的函数版本。
优点 1. 代码高度复用,减少重复。
2. 是STL(标准模板库)的基石,功能强大。
缺点 1. 编译错误信息不友好。
2. 可能导致代码膨胀(生成多个版本的函数)。
3. 定义通常需在头文件中。

希望这份详细的讲解能帮助你彻底理解C++模板函数!这是迈向C++高手之路的关键一步。多写多练,很快你就会熟练运用它了。如果有任何疑问,随时可以再问!

Dify x 腾讯云 COS MCP:自然语言解锁智能数据处理,零代码构建 AI 新世界

2025年8月21日 18:06

关于 Dify

1)Dify 是什么?

Dify 是一款开源的 大语言模型(LLM)应用开发平台。它融合了后端即服务(Backend as Service)和 LLMOps 的理念,使开发者可以快速搭建生产级的生成式 AI 应用。即使是非技术人员,也能参与到 AI 应用的定义和数据运营过程中。 由于 Dify 内置了构建 LLM 应用所需的关键技术栈,包括对数百个模型的支持、直观的 Prompt 编排界面、高质量的 RAG 引擎、稳健的 Agent 框架、灵活的流程编排,并同时提供了一套易用的界面和 API。这为开发者节省了许多重复造轮子的时间,使其可以专注在创新和业务需求上。

核心能力

  • 可视化编排 Prompt:通过界面化编写 prompt 并调试,只需几分钟即可发布一个 AI 应用;
  • 接入长文本(数据集):全自动完成文本预处理,使用您的数据作为上下文,无需理解晦涩的概念和技术处理;
  • 基于 API 开发:后端即服务。您可以直接访问网页应用,也可以接入 API 集成到您的应用中,无需关注复杂的后端架构和部署过程。
  • 数据标注与改进:可视化查阅 AI 日志并对数据进行改进标注,观测 AI 的推理过程,不断提高其性能。

2)Dify 怎么部署?

这里以 Docker Compose 部署方式为例。更多场景参见部署社区版 - Dify Docs

(1)克隆 Dify 代码仓库

克隆 Dify 源代码至本地环境。

# 假设当前最新版本为 0.15.3
git clone https://github.com/langgenius/dify.git --branch 0.15.3

(2)启动 Dify

进入 Dify 源代码的 Docker 目录,复制环境配置文件,并根据环境需要可以修改 Dify 服务端口号。

cd dify/docker
cp .env.example .env

图片

根据你系统上的 Docker Compose 版本,选择合适的命令来启动容器。可以通过 $ docker compose version 命令检查版本,详细说明请参考:Docker 官方文档

# Docker Compose V2 版本
# 启动
docker compose up -d
# 检查容器运行情况
docker compose ps

# Docker Compose V1 版本
# 启动
docker-compose up -d
# 检查容器运行情况
docker-compose ps

运行命令后,可以看到类似以下的输出,显示所有容器的状态和端口映射: 图片

启动后可以检查是否所有容器都正常运行,检查后可看到包括 3 个业务服务 api / worker / web,以及 6 个基础组件 weaviate / db / redis / nginx / ssrf_proxy / sandbox 。 图片

通过这些步骤在本地成功安装了 Dify。

(3)访问 Dify

本地安装完成后,可以先前往管理员初始化页面设置设置管理员账户:

# 本地环境
http://localhost/install
 
# 服务器环境
http://your_server_ip/install

Dify 主页面:

# 本地环境
http://localhost
 
# 服务器环境
http://your_server_ip

关于 COS MCP Server

1)COS MCP Server 是什么?

**开放协议 MCP(Model Context Protocol)**通过建立通用型接口规范,有效打通了 AI 模型与功能插件的交互通道,为 AI 技术的规模化部署提供了关键性支撑。在 AI 与云原生技术深度融合的今天,开发者面临的核心挑战是如何让 AI 高效地调用、管理资源。对此,腾讯云对象存储 COS 与数据处理服务数据万象 CI 共同推出了基于 MCP 协议的开发接口服务 COS MCP Server,助力开发者们实现"自然语言驱动云端资源管理"的终极愿景。

使用时,开发者无需为 AI 业务编写 COS 的 SDK 或 API 适配代码,通过 COS MCP Server 可以直接以自然语言指令操作资源。

核心能力:

目前,COS MCP Server 提供了以下能力: (1)对象存储 COS 接口

  • 上传/下载对象
  • 获取对象列表

(2)数据万象 CI 接口

  • 文档转PDF
  • 图片文字水印
  • 图片二维码识别
  • 图片质量评分
  • 图片超分
  • 图片通用抠图
  • 图片智能裁剪
  • 智能检索 MetaInsight(文搜图、图搜图)
  • 视频智能封面 

2)COS MCP Server 怎么部署?

COS MCP Server 支持两种通信传输方式:SSE 模式和Command 模式(即stdio 模式)。 图片 可以通过npm或者使用源码进行安装部署,同时也支持在腾讯云开发者平台直接进行托管接入部署。

(1)通过npm 安装

# 安装
npm install -g cos-mcp@latest

启动

# 启动
# 运行开启 SSE 模式
cos-mcp --Region=yourRegion --Bucket=yourBucket --SecretId=yourSecretId --SecretKey=yourSecretKey --DatasetName=yourDatasetname --port=3001 --connectType=sse

# 或通过 JSON 配置
cos-mcp --cos-config='{"Region":"yourRegion","Bucket":"BucketName-APPID","SecretId":"yourSecretId","SecretKey":"yourSecretKey","DatasetName":"datasetName"}' --port=3001 --connectType=sse

# 参数说明: 
# connectType 代表连接方式,可以是stdio (本地) 或 sse (远程)
# port 代表监听端口(sse模式有用)
# SecretId 和 SecretKey 可以从腾讯云COS https://console.cloud.tencent.com/cam/capi 获取
# bucket 是存储的桶名称
# region 是存储桶所在的区域
# datasetName 是数据集名,非必填参数,数据智能检索操作需要此参数

(2)使用源码安装

# 克隆仓库
git clone https://github.com/tencent/cos-mcp.git
cd cos-mcp
#安装依赖
npm i
#构建项目
npm run build

配置 使用 .env 文件,在项目根目录创建 .env文件,参考 .env.example 模板

cosConfig='{"Region":"yourRegion","Bucket":"BucketName-APPID","SecretId":"yourSecretId","SecretKey":"yourSecretKey","DatasetName":"datasetName"}'
connectType='sse'
port='3001'

启动

# 开启 stdio 模式
npm start 
# 开启 sse 模式
npm run start:sse
# 开启测试平台
npm run inspect

(3)在腾讯云开发者社区平台托管接入

腾讯云 COS MCP Server 页面右侧,配置相应的cos相关信息。 图片 配置完成后,点击连接Server,即可得到该托管mcp的服务侧地址。 图片

如何在 Dify 中使用 COS MCP Server?

1)准备工作

(1)创建 COS 存储桶

在腾讯云控制台中,进入对象存储(COS)服务,创建一个新的存储桶,并记录存储桶的名称和所属地域。在左侧导航中,单击存储桶列表。在存储桶列表页面,单击创建存储桶。在弹出的创建存储桶对话框中,配置如下信息。 图片

(2)获取 API 密钥

在腾讯云控制台的访问管理(CAM) 中,获取您的访问密钥 SecretId 和 SecretKey,这些信息将用于配置 COS MCP Server。关于获取 CAM 密钥,详情可参考访问管理 主账号访问密钥管理_腾讯云。 

2)在 Dify 配置 COS MCP Server

登录 Dify 后,在导航栏中依次点击 工具 → MCP,即可进入外部 MCP 服务器的管理页面。在这里可以统一管理所有为自身应用配置的 MCP 服务器。 图片 点击 添加 MCP 服务器(HTTP),即可集成新的外部工具服务。 图片 需要填写如下信息: 服务器 URL:COS MCP Server的 HTTP 接口地址。 名称与图标:自定义服务器名称,建议选择能清晰体现工具用途的名字。Dify 会自动尝试获取服务器域名的图标,也可以手动上传,比如"cos-mcp"。 服务器标识符:Dify 用于区分服务器的唯一 ID。规则:小写字母、数字、下划线或连字符,最多 24 个字符。 图片

3)在 Dify 使用 COS MCP Server

在 Dify 各类应用中使用 COS MCP Server 主要围绕以下主体链路进行设计。 自然语言输入:接收用户输入,例如“上传文件到 COS”、“查询文件列表”、“对图片添加水印”等。 解析识别:使用正则表达式或 LLM 解析用户指令,提取操作指令和参数。 处理操作:COS MCP Server 根据操作指令完成具体的处理调用操作。 结果返回:将操作结果根据需要按相应格式或渠道返回给用户。 图片 具体而言,当 COS MCP Server 配置完成后,在 Dify 各类型应用部署中,可以在节点中选择 工具 → cos-mcp,其下的具体接口会出现在下拉列表中,根据场景做按需使用。 图片

图片

图片

场景应用案例

这里基于 COS MCP Server 搭建了一个简易的 Dify 工作流示例,在该工作流链路中,入口侧输入自然语言描述,使用混元大模型解析具体指令及参数,接着调用 COS MCP Server 不同接口进行相应的操作处理,再次使用混元大模型对结果进行解析及自然语言组织,并配合企业微信机器人实现结果的实时回调通知。 示例工作流结构如下图所示。 图片

场景一:拉取 COS 文件列表

根据自然语言描述,拉取所配置bucket里指定路径下的文件列表,结果详情里会返回相应的文件列表及相关描述。 示例:“获取路径/下的文件列表”。 图片 企业微信机器人回调收到的消息如下图所示。 图片

场景二:获取图片详情信息

自然语言描述获取bucket里指定图片的详情,可以返回图片的具体信息,包括格式、分辨率、大小等信息。 示例:“获取一下图片test-pic.jpeg信息”。 图片

场景三:生成智能封面

智能分析视频,对输入视频提供一张最合适作为封面的截帧,会自动创建对应的处理任务,触发相应的处理。 示例:“创建一个智能封面任务,输入是test-video.mp4”。 图片

场景四:文档转pdf

将指定的输入文档智能转换成pdf。 示例:“创建文档转pdf任务,输入是test-doc.txt”。 图片

场景五:查询任务结果

查询指定任务的执行结果。 示例:“查询媒体处理任务,任务ID是j69e716e45ff811f098dc7b252fcf54cc”。 图片

场景六:图片质量评估及超分处理

获取图片的质量评估详情,并可以在此基础上对图片进行清晰度提高等处理。 示例:“对图片test-pic.jpeg做一下质量评估”。 图片 示例:“对test-pic.jpeg进行质量评估,如果质量低的话进行超分处理”。 图片

场景七:图片加水印

通过对指定图片加上指定水印内容,并返回结果图详情。 示例:“对图片test-pic.jpeg加水印,水印内容为'tencent'”。 图片

展望

在 Dify 中,基于 COS MCP Server,用户可以通过自然语言指定任意非结构化数据,并口语化地描述预期的处理流程及效果,结合 Dify 可视化界面轻松配置和调用 COS 和 CI,无需编写复杂代码即可完成复杂的智能数据处理任务。

未来,随着 COS MCP Server 功能覆盖度的不断提升,用户可以享受到更加无缝、高效的开发体验。无论是视频转码、图片处理,还是文档解析,用户只需通过自然语言描述需求,系统即可自动完成从解析到执行的全流程,真正实现智能化的处理、检索和存储一体化。 

相关指引

Dify 使用文档 COS MCP Server Github 主仓库 在 Dify 中使用 MCP

微信小程序环境变量设置方案

作者 山间板栗
2025年8月21日 17:54

遇到一个问题,小程序的域名写死在config.js中,根据自定义的变量去获取对应的请求域名。

那么多年相安无事,最近翻车了。被扫出来了,立正挨打。改成根据环境变量设置。

image.png

核心思想

通过Node.js脚本在构建时动态生成环境配置文件,确保最终打包的小程序代码中只包含目标环境的配置信息,从根本上避免测试环境域名被扫描到的安全风险。

1. 文件结构设计

📁 e-mini-feat/
├── 📄 package.json (新增构建脚本)
├── 📁 scripts/
│   └── 📄 set-env.js (环境配置脚本)
├── 📁 utils/
│   ├── 📄 config.js (改造后的配置文件)
│   └── 📄 env.js (动态生成,不进入版控)
└── 📄 .gitignore (新增 utils/env.js)

2. 核心改造点

原始问题:

  • utils/config.js 中硬编码了所有环境的域名
  • 测试环境域名暴露在生产代码中

解决方案:

  • 将环境配置提取到独立的动态文件 utils/env.js
  • 通过 Node.js 脚本根据构建环境生成对应配置
  • utils/config.js 从动态文件导入配置,保持API不变

3. 改造后的 utils/config.js

把原先写死的请求域名,删掉,改成从 动态文件 中获取

// 从动态生成的环境文件导入配置(使用CommonJS格式)
const { BASE_URL } = require('./env.js')

// 省略我的项目代码


const obj = {

  local: function() {
    return BASE_URL  // 直接使用动态导入的URL
  },

  isTest: function(){
    // 根据BASE_URL判断是否为测试环境
    return BASE_URL.includes('uat-traceable') || BASE_URL.includes('dev-traceable')
  },
 
}

module.exports = obj

4. 构建工作流程图

image.png

5. 核心脚本实现

const fs = require('fs')
const path = require('path')

// 配置不同环境下的环境变量值
const CONFIG = {
  dev: {
    BASE_URL: 'https://dev-traceable.ezcun.com/api/user-api/',
    ENV_NAME: 'development'
  },
  test: {
    BASE_URL: 'https://uat-traceable.ezcun.com/api/user-api/',
    ENV_NAME: 'testing'
  },
  prod: {
    BASE_URL: 'https://traceable.ezcun.com/api/user-api/',
    ENV_NAME: 'production'
  }
}

// 获取当前环境
const env = process.env.NODE_ENV?.replace(/'/g, '').trim() || 'dev'
console.log(`🔧 正在为 ${env} 环境生成配置...`)

// 获取当前环境对应的配置
const targetConfig = CONFIG[env]
if (!targetConfig) {
  console.error(`❌ 未知环境: ${env}`)
  process.exit(1)
}

// 生成配置文件内容(使用CommonJS格式,兼容小程序环境)
const configContent = `// 此文件由 scripts/set-env.js 自动生成,请勿手动修改
// 生成时间: ${new Date().toLocaleString()}
// 目标环境: ${env}

module.exports = {
  BASE_URL: '${targetConfig.BASE_URL}',
  ENV_NAME: '${targetConfig.ENV_NAME}'
}
`

// 确保目标目录存在
const utilsDir = path.join(process.cwd(), 'utils')
if (!fs.existsSync(utilsDir)) {
  fs.mkdirSync(utilsDir, { recursive: true })
}

// 写入配置文件
const envFilePath = path.join(utilsDir, 'env.js')
fs.writeFileSync(envFilePath, configContent, 'utf8')

console.log(`✅ 环境配置已生成: ${envFilePath}`)
console.log(`📍 当前环境: ${env}`)
console.log(`🌐 API地址: ${targetConfig.BASE_URL}`)

package.json

{
  "scripts": {
    "dev": "cross-env NODE_ENV=dev node scripts/set-env.js",
    "test": "cross-env NODE_ENV=test node scripts/set-env.js", 
    "prod": "cross-env NODE_ENV=prod node scripts/set-env.js",
    "build:dev": "npm run dev",
    "build:test": "npm run test",
    "build:prod": "npm run prod"
  },
  "devDependencies": {
    "cross-env": "^7.0.3"
  }
}

5. 版本控制配置

文件位置.gitignore

# 动态生成的环境配置文件
utils/env.js

# 其他忽略文件...
``

## 使用方式

### 手动执行方式

```bash
# 开发环境
npm run dev

# 测试环境  
npm run test

# 生产环境
npm run prod

6. 自动配置

image.png

Java Spring Boot 集成淘宝 SDK:实现稳定可靠的商品信息查询服务

2025年8月21日 17:53

 在电商系统开发中,对接淘宝 API 获取商品信息是一项常见需求。本文将详细介绍如何使用 Java Spring Boot 框架集成淘宝 SDK,构建一个稳定、高效的商品信息查询服务。通过合理的封装和设计,我们可以实现对淘宝 API 的可靠调用,为业务系统提供高质量的商品数据支持。

技术选型与优势

选择 Spring Boot 集成淘宝 SDK 具有以下技术优势:

  • Spring Boot 生态:提供自动配置、依赖管理和内嵌服务器,简化开发流程
  • 淘宝官方 SDK:封装了 API 签名、请求处理等底层细节,降低集成难度
  • 成熟的错误处理:可结合 Spring 的异常处理机制,提高服务稳定性
  • 易于扩展:可方便集成缓存、限流等中间件,满足高并发需求

核心依赖包括:

  • Spring Boot Starter Web:构建 RESTful API
  • 淘宝 Java SDK:taobao-sdk-java
  • Lombok:简化实体类代码
  • Spring Boot Starter Cache:实现数据缓存
  • FastJSON:JSON 数据处理

开发实战:商品信息查询服务

第一步:项目初始化与依赖配置

创建 Spring Boot 项目并在pom.xml中添加以下依赖:

xml

<dependencies>
    <!-- Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- 淘宝SDK -->
    <dependency>
        <groupId>com.taobao.api</groupId>
        <artifactId>taobao-sdk-java</artifactId>
        <version>2.0.0</version>
    </dependency>
    
    <!-- Lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    
    <!-- 缓存支持 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
    
    <!-- FastJSON -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.83</version>
    </dependency>
    
    <!-- 测试 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

第二步:配置淘宝 API 参数

application.yml中配置淘宝平台的相关参数:

yaml

taobao:
  api:
    appKey: your_app_key
    appSecret: your_app_secret
    serverUrl: http://gw.api.taobao.com/router/rest
    timeout: 5000

spring:
  cache:
    type: caffeine
    caffeine:
      spec: maximumSize=1000,expireAfterWrite=300s

创建配置类加载这些参数:

package com.example.taobaosdk.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Data
@Configuration
@ConfigurationProperties(prefix = "taobao.api")
public class TaobaoApiConfig {
    private String appKey;
    private String appSecret;
    private String serverUrl;
    private int timeout;
}

第三步:封装淘宝 SDK 客户端

创建淘宝 API 客户端工具类,封装 SDK 的调用细节:

package com.example.taobaosdk.client;

import com.example.taobaosdk.config.TaobaoApiConfig;
import com.taobao.api.DefaultTaobaoClient;
import com.taobao.api.TaobaoClient;
import com.taobao.api.TaobaoResponse;
import com.taobao.api.exception.TaobaoApiException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;

@Slf4j
@Component
public class TaobaoApiClient {

    @Resource
    private TaobaoApiConfig taobaoApiConfig;
    
    private TaobaoClient taobaoClient;
    
    @PostConstruct
    public void init() {
        // 初始化淘宝客户端
        taobaoClient = new DefaultTaobaoClient(
            taobaoApiConfig.getServerUrl(),
            taobaoApiConfig.getAppKey(),
            taobaoApiConfig.getAppSecret(),
            "json"
        );
        ((DefaultTaobaoClient) taobaoClient).setTimeout(taobaoApiConfig.getTimeout());
    }
    
    /**
     * 执行API请求
     * @param request API请求对象
     * @return 响应结果
     * @param <T> 响应类型
     */
    public <T extends TaobaoResponse> T execute(com.taobao.api.Request<T> request) {
        return execute(request, null);
    }
    
    /**
     * 执行带会话的API请求
     * @param request API请求对象
     * @param session 会话ID
     * @return 响应结果
     * @param <T> 响应类型
     */
    public <T extends TaobaoResponse> T execute(com.taobao.api.Request<T> request, String session) {
        try {
            long startTime = System.currentTimeMillis();
            T response = session == null ? 
                taobaoClient.execute(request) : 
                taobaoClient.execute(request, session);
            
            log.info("API调用完成,方法: {}, 耗时: {}ms, 结果: {}",
                request.getApiMethodName(),
                System.currentTimeMillis() - startTime,
                response.isSuccess() ? "成功" : "失败");
            
            if (!response.isSuccess()) {
                log.error("API调用失败,错误码: {}, 错误信息: {}",
                    response.getErrorCode(),
                    response.getMsg());
            }
            
            return response;
        } catch (TaobaoApiException e) {
            log.error("API调用异常,方法: {}", request.getApiMethodName(), e);
            throw new RuntimeException("淘宝API调用失败: " + e.getMessage(), e);
        }
    }
}

第四步:实现商品查询服务

创建商品查询服务接口及实现类,处理业务逻辑:

package com.example.taobaosdk.service;

import com.example.taobaosdk.dto.ProductDTO;

public interface ProductService {
    /**
     * 根据商品ID查询商品详情
     * @param numIid 商品ID
     * @return 商品详情DTO
     */
    ProductDTO getProductDetail(String numIid);
}

第五步:定义数据传输对象 (DTO)

创建商品信息的数据传输对象:

package com.example.taobaosdk.dto;

import lombok.Data;
import java.util.List;

@Data
public class ProductDTO {
    // 商品ID
    private Long numIid;
    // 商品标题
    private String title;
    // 商品主图URL
    private String pictUrl;
    // 商品价格
    private String price;
    // 商品原价
    private String originalPrice;
    // 商品描述
    private String description;
    // 商品销量
    private Integer sales;
    // 商品体积
    private Integer volume;
    // 商品图片URL列表
    private List<String> imageUrls;
    // 商品属性名称
    private String propsName;
    // SKU数量
    private Integer skuCount;
}

第六步:创建 RESTful API 接口

实现控制器对外提供 RESTful API:

package com.example.taobaosdk.controller;

import com.example.taobaosdk.dto.ProductDTO;
import com.example.taobaosdk.service.ProductService;
import com.example.taobaosdk.vo.ResultVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@Slf4j
@RestController
@RequestMapping("/api/v1/products")
public class ProductController {

    @Resource
    private ProductService productService;
    
    /**
     * 根据商品ID查询商品详情
     */
    @GetMapping("/{numIid}")
    public ResultVO<ProductDTO> getProductDetail(@PathVariable String numIid) {
        log.info("接收商品查询请求,商品ID: {}", numIid);
        ProductDTO product = productService.getProductDetail(numIid);
        return ResultVO.success(product);
    }
}

第七步:统一响应格式与异常处理

创建统一响应对象和全局异常处理器:

package com.example.taobaosdk.vo;

import lombok.Data;

@Data
public class ResultVO<T> {
    private int code;
    private String message;
    private T data;
    
    private ResultVO(int code, String message, T data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }
    
    public static <T> ResultVO<T> success(T data) {
        return new ResultVO<>(200, "success", data);
    }
    
    public static <T> ResultVO<T> error(int code, String message) {
        return new ResultVO<>(code, message, null);
    }
    
    public static <T> ResultVO<T> error(String message) {
        return new ResultVO<>(500, message, null);
    }
}

第八步:启动类

创建 Spring Boot 应用启动类:

package com.example.taobaosdk;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching // 启用缓存
public class TaobaoSdkApplication {
    public static void main(String[] args) {
        SpringApplication.run(TaobaoSdkApplication.class, args);
    }
}

服务优化与扩展

为了提高服务的稳定性和可靠性,可进行以下优化:

  1. 请求重试机制:对失败的 API 请求实现自动重试

java

运行

// 重试工具类示例
public <T> T retry(Supplier<T> supplier, int maxRetries, long delayMs) {
    int retryCount = 0;
    while (true) {
        try {
            return supplier.get();
        } catch (Exception e) {
            if (++retryCount >= maxRetries) {
                throw e;
            }
            try {
                Thread.sleep(delayMs * retryCount); // 指数退避
            } catch (InterruptedException ie) {
                Thread.currentThread().interrupt();
                throw e;
            }
        }
    }
}

  1. 限流控制:使用 Guava RateLimiter 或 Spring Cloud Gateway 实现 API 调用限流
  2. 熔断降级:集成 Resilience4j 实现服务熔断,防止级联失败
  3. 异步处理:对于批量查询需求,使用 Spring 的 @Async 实现异步处理
  4. 完善监控:集成 Spring Boot Actuator 和 Prometheus,监控 API 调用情况

总结

本文详细介绍了如何使用 Spring Boot 集成淘宝 SDK,构建商品信息查询服务。通过合理的封装和设计,我们实现了一个稳定可靠的服务,具有以下特点:

  • 封装了淘宝 SDK 的底层细节,提供简洁的 API 调用方式
  • 实现了数据缓存,减少重复 API 调用,提高响应速度
  • 统一的异常处理机制,保证服务的稳定性
  • 规范的代码结构,便于维护和扩展

在实际应用中,还需根据业务需求进一步完善功能,如添加更多 API 接口、实现更复杂的缓存策略、优化并发处理等。通过这种方式构建的服务,可以为电商平台提供高效、可靠的商品数据支持。

LangChain:大模型开发框架的全方位解析与实践

作者 pepedd864
2025年8月21日 17:44

参考资料

核心功能

  • 是什么?
    • LangChain 是一个开发大预言模型的框架。它可以将 LLM 与外部数据源、工具和各种组件便捷地连接起来,构建功能强大且实用的应用。
  • 特点
    • LangChain目前主要的功能
      • 模型接口 封装OpenAI、Claude、Cohere、Qwen、deepseek等模型统一调用方式
      • 输出结构化 自动从模型中解析JSON、Schema、函数签名、文档等
      • 内存****管理 Buffer, Summary, Entity, Conversation Memory
      • 工具接入 Web 搜索、SQL数据库、Python 执行器、API代理等
      • Agent 架构 ReAct、Self-Ask、OpenAl Function Agent 等调度机制
      • RAG 集成 多种 Retriever、Vector Store、文档拆分策略
  • 支持的语言
    • LangChain主要支持python语言,部分支持js语言,因此我们可以在我们的前端全栈项目中引入LangChain开发智能体
  • 应用-可以用于开发
    • AI智能体
    • 智能问答客服
    • 数据分析
    • AI工作流

接入模型

LangChain实现了标准化接口,实现了很多AI厂商的接入sdk,这里以deepseek为例,使用LangChain接入并调用一个简单工具

注意,LangChain不同版本差异较大,本文使用的版本是0.3.25

不同的模型使用不同的接入sdk即可,代码中就可以统一使用init_chat_model初始化模型

安装langchainlangchain-deepseek

pip install langchain langchain-deepseek

接入API并调用

from langchain.chat_models import init_chat_model

model = init_chat_model(
    model="deepseek-chat",
    model_provider="deepseek",
    base_url="https://api.deepseek.com",
    api_key="sk-xxx"
)

res = model.invoke('你好')

print(res.content)

链式调用

我们将大模型、相关工具等作为组件,链就是负责将这些组件按照某一种逻辑,顺序组合成一个流水线的方式

![image-20250821173542656](/Users/yangguo/Library/Application Support/typora-user-images/image-20250821173542656.png)

模版提示词

langchain内置了多种提示词组件,可以让AI按要求回答问题,比如常见的提示词组件有

  • ChatPromptTemplate
  • PromptTemplate
from ast import parse
from langchain.chat_models import init_chat_model
from langchain.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

model = init_chat_model(
    model="deepseek-chat",
    model_provider="deepseek",
    base_url="https://api.deepseek.com",
    api_key="sk-xxx"
)

prompt_template = ChatPromptTemplate([
    ("system", "你是一个乐意助人的助手,请根据用户的问题给出回答"),
    ("user", "这是用户的问题: {topic}, 请用 yes 或 no 来回答")
])

# 直接使用模型 + 输出解析器
bool_qa_chain = prompt_template | model | StrOutputParser()
# 测试
question = "请问 1 + 1 是否 大于 2?"
result = bool_qa_chain.invoke({'topic':question})
print(result)

上述简单代码,即可结构化输出LLM的结果

image-20250821173614150暂时无法在飞书文档外展示此内容

结构化解析

提到结构化解析,langChain内置的解析器包括

解析器名称 功能描述 类型
BooleanOutputParser 将LLM输出解析为布尔值 基础类型解析
DatetimeOutputParser 将LLM输出解析为日期时间 基础类型解析
EnumOutputParser 解析输出为预定义枚举值之一 基础类型解析
RegexParser 使用正则表达式解析LLM输出 模式匹配解析
RegexDictParser 使用正则表达式将输出解析为字典 模式匹配解析
StructuredOutputParser 将LLM输出解析为结构化格式 结构化解析
YamlOutputParser 使用Pydantic模型解析YAML输出 结构化解析
PandasDataFrameOutputParser 使用Pandas DataFrame格式解析输出 数据处理解析
CombiningOutputParser 将多个输出解析器组合为一个 组合解析器
OutputFixingParser 包装解析器并尝试修复解析错误 错误处理解析
RetryOutputParser 包装解析器并尝试修复解析错误 错误处理解析
RetryWithErrorOutputParser 包装解析器并尝试修复解析错误 错误处理解析
ResponseSchema 结构化输出解析器的响应模式 辅助类

比如使用BooleanOutputParser解析器,将上面的示例代码,解析为布尔值

from langchain.output_parsers import BooleanOutputParser

# 直接使用模型 + 输出解析器
bool_qa_chain = prompt_template | model | BooleanOutputParser()
# 测试
question = "请问 1 + 1 是否 大于 2?"
result = bool_qa_chain.invoke({'topic':question})
print(result)

使用更频繁的解析器有StructedOutputParser,他可以让AI从文本中提取结构化数据

from ast import parse
from langchain.chat_models import init_chat_model
from langchain.prompts import PromptTemplate
from langchain.output_parsers import StructuredOutputParser, ResponseSchema

model = init_chat_model(
    model="deepseek-chat",
    model_provider="deepseek",
    base_url="https://api.deepseek.com",
    api_key="sk-xxx"
)

schemas = [ # 构建结构化数据模板
    ResponseSchema(name="name", description="用户的姓名"),
    ResponseSchema(name="age", description="用户的年龄")
]

parser = StructuredOutputParser.from_response_schemas(schemas)

prompt = PromptTemplate(
  template="请根据以下内容提取用户信息,并返回 JSON 格式:\n{input}\n\n{output}"
)

chain = (
    prompt.partial(output=parser.get_format_instructions()) 
    | model
    | parser
)

result = chain.invoke({"input": "用户叫李雷,今年25岁,是一名工程师。"})
print(result) # {'name': '李雷', 'age': '25'}

image-20250821173635647

多条链组合

langChain还可以定义多个chain,形成工作流形式,处理数据

from ast import parse
from langchain.chat_models import init_chat_model
from langchain.prompts import PromptTemplate
from langchain.output_parsers import StructuredOutputParser, ResponseSchema

model = init_chat_model(
    model="deepseek-chat",
    model_provider="deepseek",
    base_url="https://api.deepseek.com",
    api_key="sk-xxx"
)

news_gen_prompt = PromptTemplate.from_template(
    "请根据以下标题撰写一段文章内容,包括时间地点事件具体信息:\n\n标题:{title}"
)

# 第一个子链:生成新闻内容
news_chain = news_gen_prompt | model

schemas = [
    ResponseSchema(name="time", description="事件发生的时间格式化为YYYY-MM-DD"),
    ResponseSchema(name="location", description="事件发生的地点"),
    ResponseSchema(name="event", description="发生的具体事件"),
]
parser = StructuredOutputParser.from_response_schemas(schemas)

summary_prompt = PromptTemplate.from_template(
    "请从下面这段文章内容中提取关键信息,并返回结构化JSON格式:\n\n{news}\n\n{output}"
)

# 第二个子链:生成新闻摘要
summary_chain = (
    summary_prompt.partial(output=parser.get_format_instructions())
    | model
    | parser
)

# 组合成一个复合 Chain
full_chain = news_chain | summary_chain

# 调用复合链
result = full_chain.invoke({"title": "小米公司在武汉总部发布小米16Pro,搭载骁龙8gen5"})
print(result)

image-20250821173649149

自定义组件

image-20250821173656734

如果我们需要在两个chain中间进行调试,就可以使用自定义组件

from langchain_core.runnables import RunnableLambda

def debug_print(x):
    print('中间结果(新闻正文):', x)
    return x

debug_node = RunnableLambda(debug_print)

# 组合成一个复合 Chain
full_chain = news_chain | debug_node | summary_chain

记忆功能

使用 MessagesPlaceholderChatPromptTemplate 中为“对话历史”留出一个占位符,把用户/助手的历史消息按顺序插入到系统消息之后,从而构建完整的聊天消息列表给模型

from langchain.chat_models import init_chat_model
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.output_parsers import StrOutputParser

model = init_chat_model(
    model="deepseek-chat",
    model_provider="deepseek",
    base_url="https://api.deepseek.com",
    api_key="sk-xxx"
)

prompt = ChatPromptTemplate.from_messages([
    SystemMessage(content="你是一个专业的前端开发工程师,你喜欢玩弄前端八股,时常会说出一些让人难以理解的技术名词"),
    MessagesPlaceholder(variable_name="messages"),
])

chain = prompt | model | StrOutputParser()

messages_list = []  # 初始化历史
while True:
    user_query = input("你:")

    # 1) 追加用户消息
    messages_list.append(HumanMessage(content=user_query))

    # 2) 调用模型
    assistant_reply=''
    print('AI:', end=' ')
    for chunk in chain.stream({"messages": messages_list}):
        assistant_reply+=chunk
        print(chunk, end="", flush=True)
    print()

    # 3) 追加 AI 回复
    messages_list.append(AIMessage(content=assistant_reply))

img

调用工具

langChain官方内置了很多工具,可以参考文档

python.langchain.com/docs/integr…

使用AI调用工具是一个相对比较麻烦的过程,首先需要给AI绑定工具,让AI知道有哪些工具可以调用

tools = [get_weather]

llm_with_tools = model.bind_tools(tools)

执行AI对话后,AI会返回内容,其中包含需要调用的工具

{
  "content": "我来帮您查询南昌今天的天气情况。",
   ... 省略其他内容
  "tool_calls": [
    {
      "name": "get_weather",
      "args": {
        "loc": "南昌"
      },
      "id": "call_0_15fd694f-a1d1-4187-b325-792794ca71ac",
      "type": "tool_call"
    }
  ],
  ... 省略其他内容
}

这个时候在代码中要判断AI返回结果是否有工具调用请求,如果有则需要找到对应的工具函数,调用,并在再次调用AI,并填入函数调用结果。

暂时无法在飞书文档外展示此内容

而langChain中的agents功能简化了AI调用工具的流程,他可以自动执行调用功能步骤,并自动调用AI回答

from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain_core.prompts import ChatPromptTemplate

import requests
import json

model = init_chat_model(
    model="deepseek-chat",
    model_provider="deepseek",
    base_url="https://api.deepseek.com",
    api_key="sk-xxx"
)

@tool
def get_weather(loc):
    """
        查询即时天气函数
        :param loc: 必要参数,字符串类型,用于表示查询天气的具体城市名称,\
        :return:心知天气 API查询即时天气的结果,具体URL请求地址为:"https://api.seniverse.com/v3/weather/now.json"
        返回结果对象类型为解析之后的JSON格式对象,并用字符串形式进行表示,其中包含了全部重要的天气信息
    """
    url = "https://api.seniverse.com/v3/weather/now.json"
    params = {
        "key": "xxx-xxx",
        "location": loc,
        "language": "zh-Hans",
        "unit": "c",
    }
    response = requests.get(url, params=params)
    temperature = response.json()
    return temperature['results'][0]['now']

tools = [get_weather]

prompt = ChatPromptTemplate.from_messages(
    [
        ("system", "你是天气助手,请根据用户的问题,给出相应的天气信息"),
        ("human", "{input}"),
        ("placeholder", "{agent_scratchpad}"), # 这部分agent提示符写法是写死的不可以修改
    ]
)
agent = create_tool_calling_agent(llm=model, tools=tools, prompt=prompt)

agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

response = agent_executor.invoke({"input": "南昌今天的天气如何?"})

print(json.dumps(response, indent=2, ensure_ascii=False))

img

Agent

上一节中,就通过Agent调用了我们自定义的工具

LangChain Agent API 在底层对 大模型调用工具做了优化,让我们在代码中也可以方便的使用AI并集成工具

除了create_tool_calling_agent这一通用的方法,LangChain还封装了许多不同的Agent实现形式,参考下表:

函数名 功能描述 适用场景
create_tool_calling_agent 创建使用工具的Agent 通用工具调用
create_openai_tools_agent 创建OpenAI工具Agent OpenAI模型专用
create_openai_functions_agent 创建OpenAI函数Agent OpenAI函数调用
create_react_agent 创建ReAct推理Agent 推理+行动模式
create_structured_chat_agent 创建结构化聊天Agent 多输入工具支持
create_conversational_retrieval_agent 创建对话检索Agent 检索增强对话
create_json_chat_agent 创建JSON聊天Agent JSON格式交互
create_xml_agent 创建XML格式Agent XML逻辑格式
create_self_ask_with_search_agent 创建自问自答搜索Agent 自主搜索推理

MCP调用

MCP(全称是Model Context Protocol,模型上下文协议)

MCP统一了AI调用工具的格式,也让AI能更好的拓展工具,常规的Function Calling工具是写在源码中不可变动的

通过MCP可以自由拓展AI的能力

在langChain中使用mcp,需要先安装依赖

pip install langchain-mcp-adapters

再调用高德地图mcp工具

import asyncio
from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain_core.prompts import ChatPromptTemplate
from langchain_mcp_adapters.client import MultiServerMCPClient

import json

mcp_config = json.load(open('./mcp.json', 'r', encoding='utf-8')).get('mcpServers', {})
mcp_client = MultiServerMCPClient(mcp_config)

model = init_chat_model(
    model="deepseek-chat",
    model_provider="deepseek",
    base_url="https://api.deepseek.com",
    api_key="sk-xxx",
)

prompt = ChatPromptTemplate.from_messages(
    [
        ("system", "你是一个地图工具,能够提供实时的地图信息和导航服务。"),
        ("human", "{input}"),
        ("placeholder", "{agent_scratchpad}"), # 这部分agent提示符写法是写死的不可以修改
    ]
)
async def run():
    tools = await mcp_client.get_tools() 
    agent = create_tool_calling_agent(llm=model, tools=tools, prompt=prompt)
    agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
    response = await agent_executor.ainvoke({"input": "南昌的天气怎么样"})
    print(json.dumps(response, indent=2, ensure_ascii=False))

asyncio.run(run())

img

KLineChart 绘制教程

作者 HANK
2025年8月21日 17:42

对前端开发者而言,KLineChart 是个不错的选择 —— 这款基于 HTML5 Canvas 的开源金融图表工具,零依赖压缩包仅 40K,轻量特性很适合集成到各类金融应用中。它不仅支持多数据源渲染 K 线图,还内置了丰富的交互功能和指标计算接口,高度可定制的特性能适配多数业务场景。

而 DolphinDB 作为高性能数据库,在数据存储与分析层面的优势无需多言,更关键的是其提供的 JavaScript API 封装了完整的数据库操作能力 —— 从连接节点、执行脚本到订阅流表等,让前端与数据库的对接变得直观高效。本文就以这两者的结合为例,聊聊如何基于 DolphinDB 存储的 K 线数据,用 KLineChart 快速实现前端可视化。

数据准备:从导入到类型对齐

数据导入

示例用的 K 线数据来自 candle_201801.csv,通过 DolphinDB 脚本导入并共享为可访问表:

t = loadText("<yourPath>/candle_201801.csv")
share t as jsTable  -- 共享为jsTable,供前端API调用

数据维度为单只股票的月度交易数据,包含开盘价、收盘价等核心字段。

类型适配要点

前端绘图的核心坑点往往在数据类型对齐上。KLineChart 对数据源有明确的字段规范:

{
  timestamp: number,  // 毫秒级时间戳,必填
  open: number,       // 开盘价,必填
  close: number,      // 收盘价,必填
  high: number,       // 最高价,必填
  low: number,        // 最低价,必填
  volume?: number,    // 成交量,可选
  turnover?: number   // 成交额,可选(EMV/AVP指标需此数据)
}

而 DolphinDB 数据类型与 JavaScript 存在默认映射关系,需重点关注:

DolphinDB 类型 JavaScript 类型 注意事项
TEMPORAL/STRING/SYMBOL 等 String 时间类型需手动转时间戳
LONG BigInt KLineChart 需 Number 类型,需转换
DOUBLE/FLOAT/INT/SHORT Number 无需额外处理

环境初始化

前端需引入两个核心资源:KLineChart 库和 DolphinDB JavaScript API:

<!-- 引入 KLineChart -->
<script src="https://cdn.dolphindb.cn/vendors/klinecharts/dist/umd/klinecharts.min.js"></script>
<!-- 模块化引入 DolphinDB API 并建立连接 -->
<script type="module">
  import { DDB } from 'https://cdn.dolphindb.cn/assets/api.js';
  const conn = new DDB('ws://ip:port');  // 替换为实际节点地址
</script>

历史数据绘图:从查询到渲染

数据查询与类型转换

用 DolphinDB API 的 execute 方法执行 SQL 查询,直接获取绘图所需字段:

// 执行查询:提取timestamp及价格、成交量等字段
const re = await conn.execute(`
  select unixTime as timestamp, open, high, low, close, volume, turnover from jsTable
`);

这里需注意 unixTime 字段 ——DolphinDB 中为 LONG 类型,默认转成 JavaScript 的 BigInt,但 KLineChart 要求 timestamp 为 Number。有两种解决方式:

  • 数据库端转换(推荐) :查询时直接转成 DOUBLE 类型,避免前端循环处理:
const re = await conn.execute(`
  select double(unixTime) as timestamp, open, high, low, close, volume, turnover from jsTable
`);
  • 前端转换:遍历数据手动转类型(大数据量下效率较低):
re.data.forEach(item => {
  item.timestamp = Number(item.timestamp);  // BigInt转Number
});

图表渲染实现

KLineChart 提供 applyNewData 接口处理全量数据,几行代码即可完成渲染:

// 初始化图表实例(绑定DOM节点)
const chart = klinecharts.init('k-line-chart');
// 传入数据渲染
chart.applyNewData(re.data);

完整示例代码(可直接复用):

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <script src="https://cdn.dolphindb.cn/vendors/klinecharts/dist/umd/klinecharts.min.js"></script>
</head>
<body>
  <div id="k-line-chart" style="height:800px;"></div>
  <script type="module">
    import { DDB } from 'https://cdn.dolphindb.cn/assets/api.js';
    const conn = new DDB('ws://<ip:port>');  // 替换节点地址
    const re = await conn.execute(`
      select double(unixTime) as timestamp, open, high, low, close, volume, turnover from jsTable
    `);
    klinecharts.init('k-line-chart').applyNewData(re.data);
  </script>
</body>
</html>

实时数据绘图:两种方案对比

当数据源为实时流时,前端需动态更新图表。根据场景不同,有两种实现思路:

方案 1:流数据订阅(推荐高实时性场景)

利用 DolphinDB API 的流订阅能力,后端增量推送数据,前端通过回调实时更新图表,无需全量拉取。

实现步骤:

  1. 后端准备流表:创建流表并共享,用于推送实时数据:
// 从历史数据生成示例流数据结构
t1 = select timestamp(unixTime) as ts, double(unixTime) as timestamp, 
  open, high, low, close, volume, turnover from jsTable
// 共享空流表st,供前端订阅
share streamTable(1:0, 
  ['ts', 'timestamp', 'open', 'high', 'low', 'close', 'volume', 'turnover'], 
  [TIMESTAMP, DOUBLE, DOUBLE, DOUBLE, DOUBLE, DOUBLE, INT, DOUBLE]
) as st
  1. 前端订阅与渲染:通过 API 订阅流表,在回调中处理数据:
<script type="module">
  import { DDB } from 'https://cdn.dolphindb.cn/assets/api.js';
  const chart = klinecharts.init('k-line-chart');
  
  // 初始化连接时配置流订阅
  const conn = new DDB('ws://<ip:port>', {
    autologin: true,
    username: 'admin',
    password: '123456',
    streaming: {
      table: 'st',  // 订阅流表st
      action: 'sub',
      handler(message) {
        // 全量更新:直接用缓存的所有数据刷新
        chart.applyNewData(message.window.data);
        
        // 增量更新:逐条追加(性能略低)
        // message.data.data.forEach(data => chart.updateData(data));
      }
    }
  });
  await conn.connect();
</script>
  1. 模拟实时数据:后端用 replay 函数回放数据,模拟实时流入:
// 按ts字段匀速回放,每秒推1条
replay(inputTables=t1, outputTables=st, dateColumn=`ts, replayRate=1000, absoluteRate=true);

注意:流订阅默认从最新数据开始(offset=-1),需先启动前端再执行回放。

方案 2:SQL 定时轮询(适合低频次更新)

通过 setInterval 定时执行 SQL 查询,全量拉取最新数据后刷新图表。适合数据更新频率低、对实时性要求不高的场景。

实现步骤:

  1. 后端准备键值表:用键值表去重存储实时数据,避免重复:
// 创建流表st和键值表kt(按timestamp去重)
share streamTable(1:0, ['ts', 'timestamp', ...], [...]) as st
share keyedTable(`timestamp, 1:0, ['ts', 'timestamp', ...], [...]) as kt
// 订阅st,自动写入kt(自动去重)
subscribeTable(tableName="st", actionName="sub_st", handler=kt)
  1. 前端定时查询:封装异步查询函数,用定时器触发:
<script type="module">
  import { DDB } from 'https://cdn.dolphindb.cn/assets/api.js';
  const chart = klinecharts.init('k-line-chart');
  const conn = new DDB('ws://<ip:port>');
  await conn.connect();
  
  // 异步更新函数
  async function updateChart() {
    const re = await conn.execute('select * from kt');  // 查询去重后的kt表
    chart.applyNewData(re.data);  // 全量刷新
  }
  
  // 每秒执行一次(可按需调整)
  setInterval(updateChart, 1000);
</script>

总结:高效集成的核心逻辑

本质上,整个流程是 “数据层 - 接口层 - 渲染层” 的串联:DolphinDB 负责存储与推送 K 线数据,其 JavaScript API 作为中间层完成数据格式转换与传输,最后 KLineChart 基于标准化数据实现可视化。

对有前端经验的开发者而言,核心是把握两点:一是数据类型的对齐(尤其是时间戳的转换),二是实时场景下的方案选择 —— 流订阅适合高频实时数据,定时轮询适合低频次更新。借助这两个工具的封装能力,十多行核心代码就能完成从数据到图表的闭环,后续若需扩展指标,直接参考 KLineChart 官方文档的指标配置即可。

前端行情好起来了,但是我依然没拿到offer

2025年8月21日 17:24

大家好,我是前端小张同学,最近面试了一下,上海,杭州,宁波这几个城市的前端开发岗位,外面行情还是蛮好的,就是狼多肉少,并且还要求都是全日制本科学历,最近半个月上上下下也面试了10-15家公司吧,基本上面试通过率 70%,但是最后都是因为学历被卡下来了,下面就给大家介绍一下我面试的公司吧,做一个总结。

1:面试周期:2025-08-11 - 2025-02-21

2:面试经历

1:汉克时代(外包)

外派网易 , 一轮技术面

base 杭州

薪资:16 * 12

手写题:

手写 防抖 节流,函数

面试题

1:讲一下最近的项目以及难点亮点

2:讲一下 vue 和 react 的 区别?

3:讲一下 闭包 优缺点

4:为什么react useState 要用数组解构,而不是函数

5:讲一下react 生命周期

6:讲一下 react render的全流程

7:react协调算法是什么实现的,为什么需要Fiber

8:什么是Fiber 架构,空闲渲染有了解吗?


第一家基本上都是简单的八股文,面试结果 过,没发offer 因为简历上写的是本科,他们按照本科要求来,非全日制不行,要全日制大专。

2:上海 安脉盛 (16 * 14)

时长:40分钟

base 上海

自研,与宁德时代有合作,做核电项目,技术栈 react

面试题:

一面

1:介绍一下最近的项目有哪些难点亮点

2:弹幕怎么实现的,你在设计这个功能的时候 怎么架构的

3:讲一下它的实现原理以及里面具体的功能

4:打包优化怎么做的,你从哪方面考虑的

5:简历上写 优化字体闪烁问题 你是怎么优化的。

6:购买的校验流程是怎么实现的?高阶函数的作用是什么

7:从Vue2 升级到Vue3 整个团队的技术规范你是怎么落地的,怎么技术选型。

8:react useMemo的 作用是什么

9:能讲一下你这个完整的购买流程吗?从前端一直到你们最终交易经历了什么,服务端接口怎么设计的,如何交易防重。

二面

做自我介绍

也是问项目,以及场景,技术八股文问的不多。


面试结果 过,没发offer 要求全日制本科。

image.png

3:华讯网络(全栈工程师)

薪资:12-20 * 14

时长:44分钟

base 上海

面试题:

一面

1:为什么离开这个公司

2:看你稀土掘金上用到cursor,有什么比较好用的功能可以分享一下吗?

3:mcp 协议了解吗?配置过吗,写过 rules 吗?

4:团队代码codeReview 是怎么做的?

5:数据库 连接用过吗? 有哪几种连接? join

6:你的项目有上线过吗?有没有性能压力 在互联网上 有没有解决过实际的高并发的场景。

7:react hooks 有哪些? useMemo的作用。

8:vite中 配置代理怎么配?

9:弹幕库的实现原理?

10:http 和 https的区别 这个s是什么,加密怎么做的?

11: 描述一下快速排序的思想

12:无序数组 数组长度是N 找到第K大的元素 K小于N

13:前端怎么做性能优化,懒加载是怎么实现的

14:网页从请求到渲染之间经历了什么?

15:跨域是什么 有哪些解决方案

16:浏览器缓存了解吗?indexDB有没有用过?


面试结果 没过

4:天讯瑞达(外包)

薪资 15-18 base 上海

面试题:

八股文 没啥好说的

vue的响应式, js基础 react 反正就这些 一些基础面试八股文

面试过 ,学历不够,外派国企,可以非全本科,但没拿到毕业证 不可以入职

5:乌鸫科技

阿里巴巴子公司

base 杭州

薪资 17 具体没谈到多少薪就pass了

面试题:

手写深拷贝

1:虚拟dom的优势,为什么vue3还要依旧延续虚拟dom

2:react 渲染原理

3:工程架构怎么做的,技术选型怎么做的?

4:打包优化你怎么做的,思路是什么

5:react有没有封装过动态表单,怎么封装 思路是什么?

6:useEffect 的 四种场景分别在上面时候触发的

7:让你设计一个商城你怎么做,你应该考虑哪些方面

8:做过哪些性能优化

9:vite 和 webpack的区别 优缺点在哪里?

10:你的前端发展方向是什么?以后朝哪个方向发展?

具体问题 还有很多 记不太清楚了 ,忘记录音

面试结果 两轮 过 ,三轮 HR面坦白学历,拒收。

image.png

image.png

总结:面试过了 一大堆,offer 没拿到一个,还有很多公司我就不一一列举了,希望做前端的小伙伴们继续加油,外面行情好起来了,今天就到这里了。

❌
❌