阅读视图
栗子前端技术周刊第 94 期 - React Native 0.81、jQuery 4.0.0 RC1、Bun v1.2.20...
🌰栗子前端技术周刊第 94 期 (2025.08.11 - 2025.08.17):浏览前端一周最新消息,学习国内外优秀文章视频,让我们保持对前端的好奇心。
📰 技术资讯
-
React Native 0.81:React Native 0.81 版本新增了对 Android 16 的支持,提升了 iOS 构建速度,并进行了一系列稳定性改进。
-
jQuery 4.0.0 RC1:jQuery 4.0.0 首个候选版本已发布,团队认为它已接近完成,希望开发者进行广泛测试,若未发现需修复的问题,将直接推出正式版本 jQuery 4.0.0。新版本主要变化与亮点包括:不再支持 IE 11 以下版本、删减遗留代码与弃用 API、引入精简版本等等。
-
Bun v1.2.20:Bun v1.2.20 修复了 141 个问题,并带来了显著的性能提升,包括降低空闲状态下的 CPU 使用率,以及将
AbortSignal.timeout
的速度提升 40 倍。
📒 技术文章
-
What we learned from creating PostCSS:那些从创建 PostCSS 中所学到的经验 - 12 年前,作者创建了 PostCSS —— 一款 CSS 自动化工具,其月下载量达 4 亿次,被谷歌、维基百科、Tailwind 以及 38% 的开发者所使用。在本文中,作者将分享在维护这个热门开源项目的漫长历程中所学到的经验。
-
How to Use innerHTML, innerText, and textContent Correctly in JavaScript:如何在 JavaScript 中正确使用
innerHTML
、innerText
和textContent
- 本文将解释 JavaScript 中三个 DOM 属性的区别:innerHTML
返回包含标签的完整 HTML 内容,innerText
仅返回受 CSS 规则影响的可见样式文本,而textContent
则返回所有文本内容,无论其在 CSS 中是否可见。这三个属性在 DOM 操作中适用于不同的使用场景。 -
前端必学-完美组件封装原则:此文总结了作者多年组件封装经验,以及拜读
antd
、element-plus
、vant
、fusion
等多个知名组件库所提炼的完美组件封装的经验;是一个开发者在封装项目组件,公共组件等场景时非常有必要遵循的一些原则。
🔧 开发工具
- ReactJS Cheatsheet:一份简洁且对开发者友好的 ReactJS 速查表,汇总了核心概念、必备 Hooks、路由、性能优化技巧以及 React 18 的新特性。
- vue-scan:让你的组件在每次更新时都闪现红色边框,帮助你排查性能问题。
- react-json-view:react-json-view(简称 rjv)是一个用于展示和编辑 JavaScript 数组及 JSON 对象的 React 组件。
🚀🚀🚀 以上资讯文章选自常见周刊,如 JavaScript Weekly 等,周刊内容也会不断优化改进,希望你们能够喜欢。
💖 欢迎关注微信公众号:栗子前端
React状态更新踩坑记:我是这样优雅修改参数的
大家好,我是小杨,一名有6年经验的前端开发工程师。在React开发中,状态(State)和参数(Props)的修改是最基础但也最容易踩坑的部分。今天我就来分享几种常见的React参数修改方法,以及我在项目中总结的最佳实践,避免大家走弯路。
1. 直接修改State?大忌!
新手常犯的一个错误是直接修改state
,比如:
// ❌ 错误示范:直接修改state
this.state.count = 10;
React的state
是不可变(Immutable) 的,直接修改不会触发重新渲染。正确的做法是使用setState
(类组件)或useState
的更新函数(函数组件)。
2. 类组件:setState的正确姿势
在类组件里,修改状态必须用setState
:
class Counter extends React.Component {
state = { count: 0 };
increment = () => {
// ✅ 正确方式:使用setState
this.setState({ count: this.state.count + 1 });
};
render() {
return <button onClick={this.increment}>Count: {this.state.count}</button>;
}
}
注意:setState
是异步的,如果依赖前一个状态,应该用函数式更新:
this.setState((prevState) => ({ count: prevState.count + 1 }));
3. 函数组件:useState + 不可变更新
在函数组件里,我们使用useState
,同样要遵循不可变原则:
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
// ✅ 正确方式:使用useState的更新函数
setCount(count + 1);
};
return <button onClick={increment}>Count: {count}</button>;
}
如果新状态依赖旧状态,推荐使用函数式更新:
setCount((prevCount) => prevCount + 1);
4. 修改对象或数组:避免引用突变
React要求状态更新必须是不可变的,所以直接修改对象或数组的属性是不行的:
const [user, setUser] = useState({ name: 'Alice', age: 25 });
// ❌ 错误:直接修改对象
user.age = 26;
setUser(user); // 不会触发更新!
// ✅ 正确:创建新对象
setUser({ ...user, age: 26 });
数组的更新也要遵循不可变原则:
const [todos, setTodos] = useState(['Learn React', 'Write Blog']);
// ✅ 正确:使用展开运算符或map/filter
setTodos([...todos, 'New Task']); // 添加
setTodos(todos.filter((todo) => todo !== 'Learn React')); // 删除
5. 性能优化:useState vs useReducer
如果状态逻辑较复杂,useState
可能会变得臃肿,这时可以用useReducer
:
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error('Unknown action');
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
</div>
);
}
useReducer
适合管理复杂状态逻辑,比如表单、全局状态等。
6. 常见坑点 & 解决方案
① 连续setState不会立即更新
// ❌ 连续调用setState,count只会+1
setCount(count + 1);
setCount(count + 1);
// ✅ 使用函数式更新
setCount(prev => prev + 1);
setCount(prev => prev + 1); // 现在会+2
② useEffect依赖问题
如果useEffect
依赖state
,但忘记加进依赖数组,可能导致闭包问题:
useEffect(() => {
console.log(count); // 可能拿到旧值
}, []); // ❌ 缺少依赖
useEffect(() => {
console.log(count); // ✅ 正确
}, [count]); // 依赖正确
总结
- 不要直接修改
state
,使用setState
或useState
的更新函数 - 对象/数组更新时,创建新引用
- 复杂状态逻辑用
useReducer
- 连续更新用函数式
setState
useEffect
依赖要写全
⭐ 写在最后
请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.
✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式
✅ 认为我部分代码过于老旧,可以提供新的API或最新语法
✅ 对于文章中部分内容不理解
✅ 解答我文章中一些疑问
✅ 认为某些交互,功能需要优化,发现BUG
✅ 想要添加新功能,对于整体的设计,外观有更好的建议
✅ 一起探讨技术加qq交流群:906392632
最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!
vite和webpack打包结构控制
概述
在工程化项目中,Vite 和 Webpack 作为当前最流行的两大构建工具,它们在打包输出目录结构的配置上各有特点,webpack和vite默认打包构建的输出目录结构可能不满足我们的需求,因此需要根据实际情况进行控制。
默认输出结构对比
webpack
默认情况下,基本上都不会根据情况进行分块,所有资源都是默认被打包到了一个文件中。
dist/
├── main.js
├── index.html
|—— .....
vite
dist/
├── assets/
│ ├── index.[hash].js
│ ├── vendor.[hash].js
│ └── style.[hash].css
└── index.html
Vite目录结构精细控制
文件指纹策略
Webpack 提供了多种 hash 类型:
-
[hash]
: 项目级hash -
[chunkhash]
: chunk级hash -
[contenthash]
: 内容级hash
基础配置方案
// vite.config.js
export default {
build: {
outDir: 'dist',
assetsDir: 'static',
emptyOutDir: true
}
}
Rollup 输出配置
由于vite内部打包使用rollup,因此打包输出相关配置需参考rollup的配置
export default {
build: {
rollupOptions: {
output: {
//资源块输出目录配置
chunkFileNames: 'static/js/[name]-[hash].js',
//入口文件输出目录配置
entryFileNames: 'static/js/[name]-[hash].js',
//静态输出目录配置(图片、音频、字体)
assetFileNames: ({ name }) => {
const ext = name.split('.').pop()
//函数形式动态返回文件输出名及其位置
return `static/${ext}/[name]-[hash].[ext]`
}
}
}
}
}
webpack 目录结构精细控制
基础输出配置
// webpack.config.js
module.exports = {
output: {
path: path.resolve(__dirname, 'build'), // 修改输出目录
filename: 'js/[name].[contenthash:8].js', // JS文件输出路径
chunkFilename: 'js/[name].[contenthash:8].chunk.js', // 异步chunk
assetModuleFilename: 'media/[name].[hash:8][ext]', // 静态资源
clean: true // 构建前清空目录
}
}
高级资源管理
使用 mini-css-extract-plugin
控制 CSS 输出:
module: {
rules: [
{ test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"] },
],
},
Webpack 5 引入了资源模块类型,取代了传统的 file-loader/url-loader,用来处理之前繁琐的配置
module: {
rules: [
{
test: /.(png|jpe?g|gif|svg)$/i,
type: 'asset/resource' // 替换 file-loader
},
{
test: /.(mp4|webm|ogg)$/i,
type: 'asset/resource'
}
],
},
四种资源模块类型
类型 | 作用 | 等价 loader |
---|---|---|
asset/resource |
导出单独文件并生成 URL | file-loader |
asset/inline |
导出资源的 Data URI | url-loader |
asset/source |
导出资源的源代码 | raw-loader |
asset |
自动选择 resource 或 inline | url-loader + 限制 |
总结
上面列举的部分配置,更多的详细配置,可以查阅官网解析。
前端必会:如何创建一个可随时取消的定时器
一、原生的取消方式
JavaScript 原生就提供了取消定时器的方法。setTimeout
和 setInterval
在调用时都会返回一个数字类型的 ID,我们可以将这个 ID 传递给 clearTimeout
或 clearInterval
来取消它。
// 1. 设置一个定时器
const timerId: number = setTimeout(() => {
console.log("这个消息可能永远不会被打印");
}, 2000);
// 2. 在它触发前取消它
clearTimeout(timerId);
常见痛点:
-
timerId
变量需要被保留在组件或模块的作用域中,状态分散。 - 启动、暂停、取消的逻辑是割裂的,代码可读性和可维护性差。
二、封装一个可取消的定时器类
我们可以简单的封装一个 CancellableTimer
类,将定时器的状态和行为内聚在一起。后续可以扩展,把项目中的所有定时器进行统一管理。
// 定义定时器ID类型
type TimeoutId = ReturnType<typeof setTimeout>;
class CancellableTimer {
private timerId: TimeoutId | null = null;
constructor(private callback: () => void, private delay: number) {}
public start(): void {
// 防止重复启动
if (this.timerId !== null) {
this.cancel();
}
this.timerId = setTimeout(() => {
this.callback();
// 执行完毕后重置 timerId
this.timerId = null;
}, this.delay);
}
public cancel(): void {
if (this.timerId !== null) {
clearTimeout(this.timerId);
this.timerId = null;
}
}
}
// 使用示例
console.log('定时器将在3秒后触发...');
const myTimer = new CancellableTimer(() => {
console.log('定时器任务执行!');
}, 3000);
myTimer.start();
// 模拟在1秒后取消
setTimeout(() => {
console.log('用户取消了定时器。');
myTimer.cancel();
}, 1000);
三、实现可暂停和恢复的定时器
在很多场景下,我们需要的不仅仅是取消,还有暂停和恢复。
要实现这个功能,我们需要在暂停时记录剩余时间。
type TimeoutId = ReturnType<typeof setTimeout>;
class AdvancedTimer {
private timerId: TimeoutId | null = null;
private startTime: number = 0;
private remainingTime: number;
private callback: () => void;
private delay: number;
constructor(callback: () => void, delay: number) {
this.remainingTime = delay;
this.callback = callback;
this.delay = delay;
}
public resume(): void {
if (this.timerId) {
return; // 已经在运行
}
this.startTime = Date.now();
this.timerId = setTimeout(() => {
this.callback();
// 任务完成,重置
this.remainingTime = this.delay;
this.timerId = null;
}, this.remainingTime);
}
public pause(): void {
if (!this.timerId) {
return;
}
clearTimeout(this.timerId);
this.timerId = null;
// 计算并更新剩余时间
const timePassed = Date.now() - this.startTime;
this.remainingTime -= timePassed;
}
public cancel(): void {
if (this.timerId) {
clearTimeout(this.timerId);
}
this.timerId = null;
this.remainingTime = this.delay; // 重置
}
}
// 使用示例
console.log('定时器启动,5秒后执行...');
const advancedTimer = new AdvancedTimer(() => console.log('Done!'), 5000);
advancedTimer.resume();
setTimeout(() => {
console.log('2秒后暂停定时器');
advancedTimer.pause();
}, 2000);
setTimeout(() => {
console.log('4秒后恢复定时器 , 应该还剩3秒');
advancedTimer.resume();
}, 4000);
总结
如果你喜欢本教程,记得点赞+收藏!关注我获取更多JavaScript/TypeScript开发干货
Oxc 最新 Transformer Alpha 功能速览! 🚀🚀🚀
拯救你的app/小程序审核!一套完美避开审核封禁的URL黑名单机制
app/微信小程序审核又双叒叕被拒了?因为一个历史遗留页面导致整个小程序被封禁搜索?别让精心开发的app/小程序毁在几个不起眼的URL上!本文将揭秘我们在多次惨痛教训后总结出的终极解决方案。
前言:每个小程序开发者都经历过的噩梦
凌晨两点,微信审核通知:"您的小程序因存在违规页面,搜索功能已被限制"。看着辛苦运营的用户量断崖式下跌,排查三天才发现是因为一个早已下架但还能访问的历史页面。这不是假设,而是真实发生的灾难场景。
在经历多次微信审核失败后,我们意识到:必须有一套灵活、实时的URL黑名单机制,能够在app/微信审核发现问题前,快速屏蔽任何违规页面。这套系统需要:
-
分钟级响应:新发现的违规URL,1分钟内全局生效
-
精准打击:既能拦截整个页面,也能封禁特定参数组合
-
零误杀:确保正常页面不受影响
-
优雅降级:被拦截用户跳转到友好提示页,且可一对一设置兜底页。
下面是我们用血泪教训换来的完整解决方案,已成功帮助我们通过n多次app审核。
app/微信小程序审核的致命陷阱:你未必意识到的风险点
真实审核失败案例
-
案例1:三年前的活动页仍可通过直接URL访问(违反现行规则)
-
案例2:用户生成内容包含敏感关键词(UGC页面)
-
案例3:第三方合作伙伴的H5页面突然变更内容
-
最致命案例:历史页面被微信爬虫索引,导致整个小程序搜索功能被封禁
核心需求清单(微信审核视角)
-
实时封堵能力:无需发版即可封禁任意URL
-
精准匹配:支持完整URL和带参数的URL匹配
-
全类型覆盖:原生页面 + H5页面统一处理
-
优雅降级:被封禁用户看到友好提示而非404
-
安全兜底:系统异常时自动放行,不影响正常业务
系统架构设计:三重防护盾
核心流程:
-
所有跳转请求经过黑名单检查
-
命中规则则跳转到兜底页
-
系统异常时降级放行
-
后台配置秒级生效
核心技术实现
参数级精准打击 - 只封禁违规内容
// 黑名单配置
["pages/user/content?type=sensitive"]
// 结果:
"pages/user/content?type=normal" => 放行 ✅
"pages/user/content?type=sensitive" => 拦截 ⛔
微信审核场景:当只有特定参数组合违规时,最小化业务影响
匹配规则详解:如何应对app审核
场景1:紧急封禁整个页面(后台配置示例)
{
"YourBlackList": [
{
"nowUrl": "https://baidu.com",
"ToUrl": "www.juejin.cn"
}
]
}
只要命中 baidu.com 无论实际跳转页面后面参数是什么,都命中了黑名单,直接跳转到自己的兜底页](url)
场景2:精准封禁违规内容
// 配置黑名单
{
"YourBlackList": [
{
"nowUrl": "pages/news/detail?id=12345",
"ToUrl": "www.baidu.com"
}
]
}
// 效果:
仅拦截id=12345的新闻,如果命中,则跳转到百度(你设置的兜底页)。其他正常展示
场景3:批量处理历史内容
// 配置黑名单
{
"YourBlackList": [
{
"nowUrl": "pages/history/?year=2020",
"ToUrl": "www.baidu.com"
}
]
}
// 效果:
拦截2020年的所有历史页面,其他年份正常
实际应用:拯救审核的最后一公里
在路由跳转处拦截
async function myNavigateTo(url) {
const { isBlocked, ToUrl } = checkUrlBlacklist(url);
if (isBlocked) {
console.warn('审核风险页面被拦截:', url);
// 跳转到安全页
return wx.navigateTo({ url: ToUrl });
}
// 正常跳转逻辑...
}
性能与安全:双保险设计
二重保障机制:
- 性能优化:黑名单为空时短路返回
if (!blackUrlList.length) return { isBlocked: false };
- 频率控制:避免相同URL重复解析
更新时机
app/小程序初始化时,如果想更精细一些,可以监听app/小程序后台切到前台onShow时
// 获取阿波罗接口配置
const resp = await request({
url: 'https://你的后台配置接口',
});
// 这里blackUrlInfoList需要保存在全局,可以放在本地存储下
blackUrlInfoList = res.blackUrlInfoList || []
校验时机:每次跳转时。
具体判断逻辑在此不做阐述,
总结:从此告别审核噩梦
通过实施这套URL黑名单系统,我们实现了:
-
审核通过率从63% → 98%
-
问题响应时间从2天 → 5分钟
-
搜索封禁事故0发生
关键收获:
-
提前拦截比事后补救更重要
-
参数级控制最大化保留正常功能
-
实时配置能力是应对审核的关键
现在点击右上角收藏本文,当app审核再次亮红灯时,你会感谢今天的自己!
分享你的审核故事:你在微信/app审核中踩过哪些坑?欢迎在评论区分享你的经历和解决方案!
nextjs项目build导出静态文件
只需要在next.config.ts中配置output为export即可,可以配置三种模式。
undefined:默认的构建输出,.next目录,适用于生产模式下的next start或Vercel等托管提供商
“standalone”:一个独立的构建输出,.next/stalone目录,仅包含必要的文件/依赖项。适用于Docker容器中的自托管。
'export':导出的构建输出,输出目录,仅包含静态HTML/CSS/JS。适用于没有Node.js服务器的自托管
配置完成后,再build:
JavaScript 日期的奇妙冒险:当 UTC 遇上 el-date-picker
前情提要:程序员小明在使用 Element Plus 的日期选择器时,发现同一个时间用不同方式设置默认值,居然显示出了三个不同的日期。这到底是闹哪样?
🌍 UTC:时间界的"标准答案"
首先,我们来认识一下 UTC(协调世界时),它就像是时间界的"标准答案"。
想象一下,如果全世界的手表都不统一,那该多乱:
- 北京的程序员说:"我们下午 3 点开会"
- 纽约的程序员回复:"哪个下午 3 点?"
- 伦敦的程序员插话:"我这里已经晚上了..."
为了避免这种尴尬,聪明的人类发明了 UTC:
- UTC = 全世界公认的"标准时间"
- 可以理解为"0 时区"的时间
- 所有其他时区都是 UTC ± 几小时
// UTC 就像是时间界的"普通话"
const utcTime = new Date().toISOString(); // "2023-12-25T08:30:45.123Z"
// 末尾的 Z 就是 UTC 的标志,意思是"Zulu Time"(军事术语)
🎭 三种日期格式的"变脸"表演
第一幕:toISOString().split('T')[0]
- "我是个好孩子"
const format1 = new Date().toISOString().split('T')[0];
console.log(format1); // "2023-12-25"
这家伙的内心独白:
"我看起来很无辜,就是个普通的日期字符串。但是!我其实是基于 UTC 时间的。如果你在东八区,而现在是北京时间凌晨 2 点,我可能会给你显示昨天的日期。惊不惊喜?意不意外?"
问题所在:看起来人畜无害的 "2023-12-25"
,但 el-date-picker 会按本地时区解释,可能出现日期偏移。
第二幕:toUTCString()
- "我很正式但很难懂"
const format2 = new Date().toUTCString();
console.log(format2); // "Mon, 25 Dec 2023 08:30:45 GMT"
这家伙的内心独白:
"我是正宗的 UTC 格式,很正式很标准。但是 el-date-picker 看到我就懵了:'这是什么鬼格式?Mon?Dec?GMT?我该怎么解析你?'"
问题所在:格式太"学术",el-date-picker 可能无法正确解析或解析结果不符合预期。
第三幕:dayjs().format("DD MMM YYYY HH:mm:ss")
- "我很个性"
const format3 = dayjs(new Date()).format("DD MMM YYYY HH:mm:ss");
console.log(format3); // "25 Dec 2023 16:30:45"
这家伙的内心独白:
"我很有个性,用的是自定义格式。我基于本地时间,没有时区信息。el-date-picker 看到我:'你到底是哪个时区的?算了,我随便猜一个吧。'"
问题所在:自定义格式 + 没有时区信息 = 解析结果不可预测。
🎪 时区的"魔术表演"
让我们看看时区是如何"变魔术"的:
// 假设现在是北京时间 2023-12-25 16:30:45 (UTC+8)
const now = new Date();
console.log('北京时间:', now.toString());
// "Mon Dec 25 2023 16:30:45 GMT+0800 (中国标准时间)"
console.log('UTC时间:', now.toUTCString());
// "Mon, 25 Dec 2023 08:30:45 GMT" <- 注意:比北京时间早8小时!
console.log('ISO格式:', now.toISOString());
// "2023-12-25T08:30:45.123Z" <- Z表示UTC时间
时区换算公式:
- 北京时间 = UTC + 8 小时
- 纽约时间 = UTC - 5 小时(冬令时)
- 伦敦时间 = UTC + 0 小时
🚑 救命稻草:el-date-picker 的正确喂食方法
经过一番折腾,我们总结出了 el-date-picker 的正确"喂食"方法:
🥇 金牌方案:直接喂 Date 对象
// 最简单粗暴,不会出错
const dateValue = ref(new Date());
为什么好用:
- Date 对象自带时区信息
- el-date-picker 天生就认识它
- 不需要任何格式转换
🥈 银牌方案:完整的 ISO 字符串
// 保留完整的时区信息
const dateValue = ref(new Date().toISOString());
为什么靠谱:
- ISO 格式是国际标准
- 包含完整的时区信息(那个 Z)
- 大部分组件都认识
🥉 铜牌方案:dayjs 标准格式
// 使用标准的日期格式
const dateValue = ref(dayjs().format('YYYY-MM-DD'));
适用场景:
- 只需要日期,不需要时间
- 想要更多控制权
🏆 特别奖:精确控制方案
// 我要的就是今天的开始时间!
const dateValue = ref(dayjs().startOf('day').toDate());
// 注意:dayjs().toDate() 本质上也是 Date 对象,和 new Date() 一样安全
console.log(dayjs().toDate() instanceof Date); // true
🎯 实战代码
<template>
<div>
<h3>正确的日期选择器使用姿势</h3>
<!-- 推荐方式 -->
<el-date-picker
v-model="dateValue"
type="date"
placeholder="选择日期"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
import dayjs from 'dayjs'
// 🎉 最推荐:简单直接,不会出错
const dateValue = ref(new Date())
// 其他可行方案:
// const dateValue = ref(dayjs().startOf('day').toDate()) // 精确控制
// const dateValue = ref(new Date().toISOString()) // 完整 ISO
</script>
🤔 思考题
看完这篇文章,你能回答这个问题吗:
如果现在是北京时间 2023-12-25 凌晨 02:00,new Date().toISOString().split('T')[0]
会返回什么?
答案是:"2023-12-24"
为什么?因为:
- 北京时间 2023-12-25 02:00 - 8小时 = UTC 时间 2023-12-24 18:00
-
toISOString()
返回"2023-12-24T18:00:00.000Z"
-
split('T')[0]
取日期部分:"2023-12-24"
所以虽然你的本地时间已经是 25 号了,但这个方法返回的还是 24 号!
🎊 总结
记住这个简单的口诀:
日期选择器要喂饱,
Date 对象是最好。
时区问题很头疼,
UTC 标准要记牢!
最后,如果你还在纠结用哪种格式,就直接用 new Date()
吧。简单粗暴,永不出错!
Happy Coding! 愿你的日期永远准确,时区永远正确! 🎈
也是用上webworker了
React 16.8,我自己写的的足球应用,问题是模态框没有立即弹出,反而是等了一会才弹出。
useImperativeHandle(ref, () => ({
showModal: (id: string) => {
const newState = {
...state,
id,
teamCount: teamOddList.length,
tableLoading: true,
open: true,
};
setState(newState);
setAddedItems(new Map());
// 初始化已存在的奖金项目
initializeAddedItems(id);
getOddResultList(newState, teamOddList);
},
}));
我开始以为是 React 响应式设计导致的,因为此时 setState 的是异步的,虽然 showModal 中设置了 open 为 true,后续处理不当,还是会导致 open 隐式设置为 false
仔细检查一番发现没有,并且我已经把 newSate 传递过去了,通过注释代码发现,是getOddResultList
导致的,其实在之前的写法中我是加了setTimeout
的,只不过时间太久了忘记了为什么加。
如今再写一来觉得setTimeout
这种解决方式并不好。而且延迟 20ms 是没用的,看起来像是在等计算完成后再显示模态框。
于是我问了下 Trace,他说是因为getOddResultList
有大量的同步计算,建议我用 webworker 来处理。
- 多重嵌套循环 :对每个球队的赔率信息进行多维度组合计算(胜平负、让球、比分、进球数、半场等)
- 指数级复杂度 :通过递归函数 getTeamCombinationList 和 againForEach 生成所有可能的投注组合
- 大量数据处理 :每个组合都需要计算赔率乘积,并进行排序
- JavaScript 是单线程的,同步计算会完全阻塞主线程
- 模态框的 open 状态虽然已设置为 true ,但 React 无法进行重新渲染
- 用户界面会出现"卡顿",模态框无法立即显示
这个函数确实会消耗大量的时间,因为有 4 只球队会产生 9 百万种结果。
使用 webworker 的话,需要将 js 文件放到 public 目录下,通过onmessage
来接收消息,通过postMessage
来发送消息。
self.onmessage = function (e) {
const { teamOddList } = e.data;
try {
const result = calculateOddResultList(teamOddList);
self.postMessage({ success: true, data: result });
} catch (error) {
self.postMessage({ success: false, error: error.message });
}
};
在 jsx 文件中使用 webworker
useEffect(() => {
// 创建 Web Worker
workerRef.current = new Worker('/oddResultWorker.js');
workerRef.current.onmessageerror = (e) => {
console.error('Worker message error:', e);
};
// 监听 Worker 消息
workerRef.current.onmessage = (e) => {
const { success, data, error } = e.data;
if (success) {
allOddResultListRef.current = data;
setState((preState) => ({
...preState,
total: data.length,
tableLoading: false,
oddResultList: data.slice(
(preState.currentPage - 1) * pageSize,
preState.currentPage * pageSize
),
}));
} else {
console.error('Worker error:', error);
}
};
return () => {
if (workerRef.current) {
workerRef.current.terminate();
}
};
}, []);
调用时
showModal: (id: string, teamOddList: Array<NFootball.ITeamRecordOdds>) => {
...
setAddedItems(new Map());
initializeAddedItems(id);
workerRef.current.postMessage({ teamOddList });
},
React 牵手 Ollama:本地 AI 服务对接实战指南
在这个 AI 大模型如雨后春笋般涌现的时代,让前端应用与本地大模型来一场 “亲密接触”,就像给你的 React 应用装上一个 “本地智囊团”。今天,我们就来实现一个看似高深实则简单的需求:用 React 对接本地 Ollama 服务。这就好比教两个素未谋面的朋友打招呼,Ollama 是守在本地的 “AI 达人”,React 则是活泼的 “前端信使”,我们要做的就是搭建它们之间的沟通桥梁。
底层原理:通信的奥秘
在开始编码前,我们得先搞明白这两个 “朋友” 是如何交流的。Ollama 作为本地运行的大模型服务,会在你的电脑上开启一个 “通信窗口”—— 也就是 HTTP 服务器,默认情况下这个窗口的地址是 http://localhost:11434。而 React 应用要做的,就是通过 HTTP 协议向这个窗口发送 “消息”(请求),并等待 “回复”(响应)。
这就像你去餐厅吃饭,Ollama 是后厨的厨师,React 是前厅的服务员,http://localhost:11434 就是厨房的传菜口。服务员把顾客的订单(请求)通过传菜口递给厨师,厨师做好菜后再通过传菜口把菜(响应)送回给服务员。
准备工作:工具就位
在正式开始前,我们需要准备好 “食材” 和 “厨具”:
- 安装 Ollama:去 Ollama 官网下载并安装,这一步就像把厨师请到厨房里。安装完成后,打开命令行,输入 ollama run llama3 来启动一个基础模型,这里我们用 llama3 作为示例,你也可以换成其他喜欢的模型。
- 创建 React 应用:如果你还没有 React 项目,可以用 Create React App 快速创建一个,命令是 npx create-react-app ollama-demo,这就像搭建好前厅的场地。
代码实现:搭建沟通桥梁
一切准备就绪,现在我们来编写核心代码,实现 React 与 Ollama 的通信。
首先,我们需要一个发送请求的函数。在 React 组件中,我们可以用 fetch API 来发送 HTTP 请求到 Ollama 的 API 端点。Ollama 的聊天接口是 http://localhost:11434/api/chat,我们需要向这个接口发送包含模型名称和消息内容的 JSON 数据。
import { useState } from 'react';
function OllamaChat() {
const [message, setMessage] = useState('');
const [response, setResponse] = useState('');
const sendMessage = async () => {
try {
// 构建请求体,指定模型和消息
const requestBody = {
model: 'llama3',
messages: [{ role: 'user', content: message }],
stream: false // 不使用流式响应,等待完整回复
};
// 发送 POST 请求到 Ollama 的聊天接口
const response = await fetch('http://localhost:11434/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
});
// 解析响应数据
const data = await response.json();
// 提取并显示 AI 的回复
if (data.message && data.message.content) {
setResponse(data.message.content);
}
} catch (error) {
console.error('与 Ollama 通信出错:', error);
setResponse('抱歉,无法连接到 AI 服务,请检查 Ollama 是否正在运行。');
}
};
return (
<div style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
<h2>React × Ollama 聊天 Demo</h2>
<div style={{ marginBottom: '20px' }}>
<input
type="text"
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="输入你的问题..."
style={{ width: '70%', padding: '8px', marginRight: '10px' }}
/>
<button onClick={sendMessage} style={{ padding: '8px 16px' }}>
发送
</button>
</div>
<div style={{ border: '1px solid #ccc', padding: '10px', borderRadius: '4px' }}>
<h3>AI 回复:</h3>
<p>{response}</p>
</div>
</div>
);
}
export default OllamaChat;
代码解析:庖丁解牛
让我们来仔细看看这段代码的工作原理,就像拆解一台精密的机器。
- 状态管理:我们用 useState 钩子创建了两个状态变量,message 用来存储用户输入的消息,response 用来存储 AI 的回复。这就像两个储物盒,分别存放要发送的消息和收到的回复。
- 发送消息函数:sendMessage 是核心函数,它通过 fetch 发送请求到 Ollama。请求体中指定了要使用的模型(llama3)和用户的消息。这里的 stream: false 表示我们希望一次性收到完整的回复,而不是逐字接收。
- 处理响应:当 Ollama 处理完请求后,会返回一个 JSON 格式的响应。我们从中提取出 AI 的回复内容,并更新 response 状态,这样页面上就会显示出 AI 的回答了。
- 错误处理:如果通信过程中出现错误(比如 Ollama 没有运行),我们会捕获错误并显示友好的提示信息。
运行测试:见证奇迹的时刻
现在,让我们来测试一下这个 Demo 是否能正常工作。
- 确保 Ollama 正在运行:打开命令行,输入 ollama run llama3,等待模型加载完成。
- 启动 React 应用:在项目目录下运行 npm start,打开浏览器访问 http://localhost:3000。
- 发送消息:在输入框中输入一个问题,比如 “你好,Ollama!”,然后点击 “发送” 按钮。稍等片刻,你应该就能看到 AI 的回复了。
如果一切顺利,你会看到 React 应用和 Ollama 成功 “牵手”,完成了一次愉快的对话。如果遇到问题,先检查 Ollama 是否正在正常运行,模型名称是否正确,网络连接是否通畅。
进阶思考:拓展可能性
这个简单的 Demo 只是一个开始,就像我们只是搭建了一座简陋的小桥。你可以基于这个基础进行很多拓展:
- 实现流式响应:将 stream 设置为 true,然后处理流式响应,让 AI 的回复像打字一样逐字显示,提升用户体验。
- 增加聊天历史:用状态管理存储聊天记录,让对话可以上下文连贯。
- 切换不同模型:在界面上增加模型选择功能,让用户可以根据需要切换不同的 Ollama 模型。
- 优化错误处理:增加更详细的错误提示,帮助用户排查问题。
总结:本地 AI 的魅力
通过这个 Demo,我们展示了 React 对接本地 Ollama 服务的全过程。相比于调用云端的 AI 服务,本地部署的 Ollama 具有隐私性好、响应速度快、无需网络连接等优点,就像把 AI 助手请到了自己家里,随时可以交流。
希望这篇文章能帮助你理解 React 与本地 AI 服务对接的原理和方法。现在,你可以基于这个基础,开发出更强大、更有趣的本地 AI 应用了。让我们一起探索前端与 AI 结合的无限可能吧!
Next.js 全栈开发基础:在 pages/api/*.ts 中创建接口的艺术
在 Web 开发的世界里,前端与后端就像一对需要默契配合的舞者。前端负责优雅地展示数据,后端则默默在幕后准备数据,而接口就是它们之间传递信号的乐谱。在 Next.js 的舞台上,pages/api/*.ts就是谱写这份乐谱的最佳创作室。今天,我们就来揭开在 Next.js 中创建接口的神秘面纱,用 TypeScript 为你的全栈应用搭建起高效的数据桥梁。
接口的本质:数据交换的高速公路
在深入技术细节之前,让我们先理解接口的本质。想象你在餐厅点餐,你(前端)告诉服务员(接口)想要什么,服务员把需求传达给厨房(数据库 / 业务逻辑),然后把做好的食物(数据)端给你。这个过程中,服务员就是接口,负责规范请求格式、处理业务逻辑并返回结果。
在计算机科学中,接口本质上是客户端与服务器之间约定的数据交换格式和规则。Next.js 的 API 路由之所以强大,是因为它允许我们在同一个项目中同时编写前端页面和后端接口,就像在同一个屋檐下同时拥有餐厅大堂和厨房,大大提高了开发效率。
初探 pages/api:Next.js 的接口魔法
Next.js 的 API 路由基于一个简单而强大的约定:在pages/api目录下创建的文件会自动成为 API 接口。这个机制背后其实是 Next.js 的文件系统路由在起作用,当服务器启动时,它会扫描pages/api目录下的所有文件,为每个文件创建对应的路由端点。
比如我们创建pages/api/hello.ts文件,访问http://localhost:3000/api/hello就能调用这个接口。这种设计就像给每个接口分配了独立的办公室,它们互不干扰又能协同工作。
第一个接口:Hello World 的进阶版
让我们从经典的 Hello World 开始,创建一个能返回个性化问候的接口。在pages/api目录下新建greet.ts文件,输入以下代码:
export default function handler(req, res) {
// 从请求中获取查询参数name
const { name = "World" } = req.query;
// 设置响应状态码为200(成功)
res.status(200).json({
message: `Hello, ${name}!`,
timestamp: new Date().toISOString()
});
}
这个接口做了三件事:
- 从请求的查询参数中获取 name,如果没有提供则默认使用 "World"
- 设置 HTTP 响应状态码为 200,表示请求成功
- 返回一个 JSON 对象,包含问候消息和当前时间戳
运行你的 Next.js 应用,访问http://localhost:3000/api/greet?name=Next.js,你会看到类似这样的响应:
{
"message": "Hello, Next.js!",
"timestamp": "2025-08-17T12:34:56.789Z"
}
处理不同的 HTTP 方法:接口的多面手
一个健壮的接口应该能处理不同的 HTTP 方法,就像一个多才多艺的演员能胜任不同的角色。常见的 HTTP 方法有 GET(获取数据)、POST(创建数据)、PUT(更新数据)和 DELETE(删除数据)。
让我们创建一个简单的任务管理接口,支持 GET 和 POST 方法:
// pages/api/tasks.ts
let tasks = [
{ id: 1, title: "学习Next.js", completed: false },
{ id: 2, title: "创建API接口", completed: true }
];
export default function handler(req, res) {
// 获取请求方法
const { method } = req;
switch (method) {
case 'GET':
// 处理GET请求:返回所有任务
res.status(200).json(tasks);
break;
case 'POST':
// 处理POST请求:创建新任务
const { title } = req.body;
// 验证请求数据
if (!title) {
return res.status(400).json({ error: "任务标题不能为空" });
}
// 创建新任务
const newTask = {
id: tasks.length + 1,
title,
completed: false
};
// 添加到任务列表
tasks.push(newTask);
// 返回创建的任务,状态码201表示资源创建成功
res.status(201).json(newTask);
break;
default:
// 处理不支持的方法
res.setHeader('Allow', ['GET', 'POST']);
res.status(405).end(`方法 ${method} 不被允许`);
}
}
这个接口展示了如何根据不同的 HTTP 方法执行不同的操作:
- 当使用 GET 方法访问时,它返回所有任务列表
- 当使用 POST 方法并发送包含 title 的 JSON 数据时,它创建一个新任务
- 当使用不支持的方法(如 PUT 或 DELETE)时,它返回 405 错误
你可以使用工具如 Postman 或 curl 来测试这个接口:
# 测试GET请求
curl http://localhost:3000/api/tasks
# 测试POST请求
curl -X POST -H "Content-Type: application/json" -d '{"title":"新任务"}' http://localhost:3000/api/tasks
接口参数处理:精准获取请求数据
在实际开发中,我们经常需要从不同位置获取请求数据。Next.js 的 API 路由提供了多种方式来获取这些数据,就像有多个入口可以进入一个建筑:
- 查询参数(Query Parameters) :位于 URL 中?后面的键值对,通过req.query获取
- 路径参数(Path Parameters) :URL 路径中的动态部分,通过文件名中的[param]定义
- 请求体(Request Body) :POST、PUT 等方法发送的数据,通过req.body获取
让我们创建一个支持路径参数的接口,用于获取单个任务:
// pages/api/tasks/[id].ts
// 假设tasks数组与前面的例子相同
let tasks = [
{ id: 1, title: "学习Next.js", completed: false },
{ id: 2, title: "创建API接口", completed: true }
];
export default function handler(req, res) {
const { id } = req.query;
// 将id转换为数字
const taskId = parseInt(id, 10);
// 验证id是否有效
if (isNaN(taskId)) {
return res.status(400).json({ error: "无效的任务ID" });
}
// 查找任务
const task = tasks.find(t => t.id === taskId);
if (task) {
res.status(200).json(task);
} else {
res.status(404).json({ error: "任务不存在" });
}
}
现在,访问http://localhost:3000/api/tasks/1会返回 ID 为 1 的任务,而访问http://localhost:3000/api/tasks/99会返回 404 错误。
错误处理:接口的安全网
就像现实生活中需要应急预案一样,接口也需要完善的错误处理机制。一个好的错误处理策略应该:
- 返回适当的 HTTP 状态码
- 提供清晰的错误信息
- 避免暴露敏感信息
让我们改进前面的任务接口,添加更完善的错误处理:
// pages/api/tasks/[id].ts(改进版)
let tasks = [
{ id: 1, title: "学习Next.js", completed: false },
{ id: 2, title: "创建API接口", completed: true }
];
export default function handler(req, res) {
try {
const { id } = req.query;
const taskId = parseInt(id, 10);
if (isNaN(taskId)) {
// 400 Bad Request:请求参数无效
return res.status(400).json({
error: "无效的任务ID",
details: "ID必须是数字"
});
}
const task = tasks.find(t => t.id === taskId);
if (task) {
// 200 OK:请求成功
res.status(200).json(task);
} else {
// 404 Not Found:资源不存在
res.status(404).json({
error: "任务不存在",
details: `没有ID为${taskId}的任务`
});
}
} catch (error) {
// 500 Internal Server Error:服务器内部错误
console.error("处理请求时出错:", error);
res.status(500).json({
error: "服务器内部错误",
details: "请稍后再试"
});
}
}
这个改进版接口使用 try-catch 块捕获可能的错误,并为不同类型的错误返回相应的状态码和详细信息,同时避免将内部错误直接暴露给客户端。
接口的性能考量:让数据流动更快
随着应用规模的增长,接口的性能变得越来越重要。以下是一些提高 API 路由性能的小贴士:
- 数据缓存:对于不经常变化的数据,可以使用缓存减少重复计算
- 请求验证:尽早验证请求数据,避免不必要的处理
- 分页处理:对于大量数据,使用分页减少数据传输量
- 异步处理:对于耗时操作,考虑使用异步处理避免阻塞
让我们实现一个带分页功能的任务列表接口:
// pages/api/tasks/paginated.ts
let tasks = [
// 假设这里有很多任务...
{ id: 1, title: "任务1", completed: false },
{ id: 2, title: "任务2", completed: true },
// ...更多任务
];
export default function handler(req, res) {
try {
// 获取分页参数,默认页码为1,每页10条
const { page = 1, limit = 10 } = req.query;
const pageNum = parseInt(page, 10);
const limitNum = parseInt(limit, 10);
// 验证分页参数
if (isNaN(pageNum) || isNaN(limitNum) || pageNum < 1 || limitNum < 1) {
return res.status(400).json({
error: "无效的分页参数",
details: "页码和每页数量必须是正整数"
});
}
// 计算总页数
const totalPages = Math.ceil(tasks.length / limitNum);
// 计算起始索引
const startIndex = (pageNum - 1) * limitNum;
// 获取当前页的任务
const paginatedTasks = tasks.slice(startIndex, startIndex + limitNum);
res.status(200).json({
data: paginatedTasks,
pagination: {
total: tasks.length,
page: pageNum,
limit: limitNum,
totalPages
}
});
} catch (error) {
console.error("分页查询出错:", error);
res.status(500).json({ error: "服务器内部错误" });
}
}
这个接口支持通过page和limit参数控制返回的数据量,减轻了服务器和网络的负担。
部署与注意事项:让接口飞向生产环境
当你的接口准备好部署到生产环境时,有几个重要的注意事项:
- 环境变量:敏感信息如数据库连接字符串应该使用环境变量,而不是硬编码在代码中
- CORS 设置:如果你的前端和后端不在同一个域名下,需要配置跨域资源共享(CORS)
- 速率限制:为了防止滥用,考虑添加速率限制功能
- 日志记录:添加适当的日志记录以便调试和监控
在 Next.js 中配置 CORS 非常简单,你可以使用cors中间件:
// pages/api/with-cors.ts
import cors from 'cors';
// 初始化cors中间件
const corsMiddleware = cors({
origin: process.env.NEXT_PUBLIC_FRONTEND_URL || '*',
methods: ['GET', 'POST', 'PUT', 'DELETE']
});
// 辅助函数:将中间件转换为Promise
function runMiddleware(req, res, fn) {
return new Promise((resolve, reject) => {
fn(req, res, (result) => {
if (result instanceof Error) {
return reject(result);
}
return resolve(result);
});
});
}
export default async function handler(req, res) {
// 应用CORS中间件
await runMiddleware(req, res, corsMiddleware);
// 处理请求
res.status(200).json({ message: "这个接口支持跨域请求!" });
}
总结:接口开发的艺术与科学
在 Next.js 中创建 API 接口就像在构建一座连接前端和后端的桥梁,它需要扎实的技术基础,也需要对用户需求的深刻理解。通过pages/api/*.ts文件,我们可以快速创建功能完善的接口,处理各种 HTTP 方法,获取不同来源的请求数据,并返回结构化的响应。
记住,一个好的接口应该是清晰、健壮、高效且安全的。它不仅要能正确处理正常情况,还要能优雅地应对错误;不仅要能满足当前需求,还要为未来的扩展留有余地。
随着你对 Next.js API 路由的深入了解,你可以尝试更高级的功能,如数据库集成、身份验证、文件上传等。全栈开发的世界充满了可能性,而接口就是打开这个世界的钥匙。现在,拿起这把钥匙,开始构建你的全栈应用吧!
p5.js 3D 形状 "预制工厂"——buildGeometry ()
点赞 + 关注 + 收藏 = 学会了
如果你已经会用box()
、sphere()
画简单 3D 形状,想组合它们做出复杂模型,又担心画面卡顿,那么buildGeometry()
就是你的 "性能救星"。这个函数能把多个简单形状 "焊接" 成一个自定义 3D 模型,让绘制效率飙升。
什么是 buildGeometry ()?
buildGeometry()
是 p5.js 中用于组装复杂 3D 模型的工具函数。它的核心作用就像 "预制构件厂":
- 把多个简单 3D 形状(比如
box()
、sphere()
)组合成一个完整的p5.Geometry
对象(可以理解为 "自定义 3D 零件"); - 这个 "零件" 只需要在程序启动时制作一次,之后每次绘制直接调用即可,大幅减少重复计算;
- 必须在WebGL 模式下使用(和所有 3D 函数一样)。
buildGeometry()
就是来解决这个问题的:它能把多个简单 3D 形状 “打包” 成一个p5.Geometry
对象,只需创建一次,之后反复绘制都不会卡顿。就像快递打包,把多个小包裹捆成一个大包裹,搬运起来更高效~
基础用法
用buildGeometry
打包一个球体,然后绘制它。
let myShape; // 存储打包好的3D对象
function setup() {
// 开启WebGL模式(3D绘图必备)
createCanvas(400, 400, WEBGL);
// 用buildGeometry创建3D对象,回调函数是makeShape
myShape = buildGeometry(makeShape);
}
function draw() {
background(200); // 灰色背景
orbitControl(); // 允许鼠标拖动旋转视角
lights(); // 添加光照(3D物体需要光照才看得见)
model(myShape); // 绘制打包好的3D对象
}
// 回调函数:定义要打包的形状
function makeShape() {
sphere(50); // 画一个半径50的球体
}
旋转的几何花朵
用buildGeometry
组合多个锥体,形成一朵 “花”,然后让它随时间旋转并变色,展示高性能复杂 3D 动画的实现。
let flower;
let hueValue = 0; // 色相值(用于颜色变化)
function setup() {
createCanvas(600, 600, WEBGL);
// 创建几何花朵
flower = buildGeometry(makeFlower);
}
function draw() {
background(0); // 黑色背景
orbitControl(); // 允许鼠标旋转视角
lights(); // 光照
// 颜色随时间变化(HSB模式:色相、饱和度、亮度)
colorMode(HSB);
fill(hueValue % 360, 80, 90);
hueValue += 0.5;
// 整体旋转(X和Y轴同时转,更有动感)
// rotateX(frameCount * 0.005);
rotateY(frameCount * 0.008);
model(flower); // 绘制花朵
}
// 构建花朵形状的回调函数
function makeFlower() {
// 中心球体
sphere(15);
// 周围的“花瓣”:12个锥体
for (let i = 0; i < 12; i++) {
push();
// 绕Y轴均匀分布(360度/12=30度一个)
rotateY(i * PI / 6);
// 沿Z轴向外移动
translate(0, 0, 40);
// 锥体:底面半径10,高30,朝上
cone(10, 30);
pop();
}
}
以上就是本文的全部内容啦,想了解更多 P5.js 用法欢迎关注 《P5.js中文教程》。
也可以➕我 green bubble 吹吹水咯
点赞 + 关注 + 收藏 = 学会了
手把手教你入门 MCP:模型上下文协议与 Trae IDE 中的实践
✨点击上方关注☝️,追踪不迷路!
一、什么是 MCP?
MCP(Model Context Protocol)是一种开放协议,它标准化了应用程序如何向大型语言模型(LLMs)提供上下文。可以将 MCP 想象成 AI 应用程序的 USB-C 接口——正如 USB-C 提供了标准化的方式来连接设备与各种外设和配件一样,MCP 提供了标准化的方式来连接 AI 模型与不同的数据源和工具。
MCP 使你能够在 LLM 之上构建代理和复杂工作流,并将你的模型与外部世界连接起来。它提供了一套标准规则,让 AI 模型能够理解如何获取上下文信息、如何与外部工具交互,以及如何处理和利用返回的结果。
二、为什么要学习 MCP?
MCP 为 AI 生态系统带来了许多重要价值:
- 丰富的预构建集成:MCP 提供了一系列预构建的集成,你的 LLM 可以直接接入使用
- 标准化的自定义集成方式:为 AI 应用程序提供了标准化的方式来构建自定义集成
- 开放的协议:每个人都可以自由实现和使用 MCP 协议
- 灵活性:允许在不同应用之间切换并携带你的上下文
对于开发者和 AI 用户来说,学习 MCP 意味着你可以充分利用 AI 模型与外部工具的协同能力,构建更加强大和实用的 AI 应用。
三、MCP 的基本工作原理
MCP 协议的核心工作原理是建立 AI 模型与外部数据源、工具之间的标准化通信桥梁。以下是 MCP 的基本工作流程:
- 上下文获取:MCP 服务从各种数据源收集上下文信息
- 标准化处理:将上下文信息转换为标准格式
- 模型交互:将标准化的上下文传递给 AI 模型
- 工具调用:根据模型需求,通过 MCP 协议调用外部工具
- 结果返回:将工具执行结果格式化后返回给模型
在 Trae IDE 这样的开发环境中,AI 助手正是通过 MCP 协议来调用代码搜索、文件编辑、命令执行等各种功能,从而更好地理解代码库上下文,为开发者提供更精准的帮助。
四、如何在 Trae IDE 中配置 Gitee MCP 服务
Gitee 提供了 MCP 服务,允许 Trae IDE 通过 MCP 协议与 Gitee 平台进行交互。下面是在 Trae 中配置 Gitee MCP 服务的步骤:
1. 获取 Gitee 访问令牌
首先,你需要在 Gitee 上创建一个访问令牌,用于 Trae IDE 与 Gitee MCP 服务之间的身份验证:
- 登录你的 Gitee 账号
- 进入「设置」→「安全设置」→「私人令牌」
- 点击「生成新令牌」,选择需要的权限(如
repo
、user_info
等) - 保存生成的令牌,这将用于后续配置
2. 在 Trae IDE 中配置 Gitee MCP 服务
在 Trae IDE 中,你可以通过以下 JSON 配置来连接 Gitee MCP 服务:
{
"mcpServers": {
"gitee": {
"url": "https://api.gitee.com/mcp",
"headers": {
"Authorization": "Bearer <私人令牌>"
}
}
}
}
其中,<私人令牌>
是你在第一步中获取的 Gitee 访问令牌。你可以将此配置添加到 Trae IDE 的 MCP 服务配置文件中。
3. 测试 Gitee MCP 服务连接
在 Trae IDE 中测试 Gitee MCP 服务连接的实际操作步骤如下:
- 打开 Trae IDE 的 MCP 服务配置面板
- 选择你刚刚配置的 Gitee MCP 服务
- 点击服务配置旁边的「开启连接」按钮
- Trae IDE 会自动向 Gitee MCP 服务发送一个简单的请求来验证连接是否正常
- 观察测试结果:如果连接成功,你会看到服务会有一个绿色勾,并可能显示一些基本的用户信息摘要;如果连接失败,会有红色感叹号,点击会显示具体的错误信息
通过这种方式,你可以确认 Gitee MCP 服务配置是否正确,以及能否正常与 Gitee 平台通信。
4. 开始使用
在trae对话中输入@切换到对应的builder with MCP 智能体,并输入“帮我查询我的 gitee 有多少个仓库?” 如下图
五、在 Trae IDE 中配置 MCP 服务
在 Trae IDE 中,你可以通过两种方式添加和配置 MCP 服务:从市场添加或手动添加。下面将详细介绍这两种配置方法。
5.1 从市场添加 MCP 服务
Trae IDE 提供了 MCP 市场,其中包含了社区中热门的 MCP 服务,你可以直接从中选择并添加:
操作步骤:
- 在 AI 侧边对话框的右上角,点击「设置」图标
- 选择「MCP」页签
- 点击「+ 添加 MCP Servers」按钮;或点击右上角的「+ 添加」按钮,然后在菜单中选择「从市场添加」
- 在打开的 MCP 市场中,浏览并找到你需要的 MCP 服务
- 点击该服务右侧的「+」按钮
- 在弹出的配置窗口中,填写必要的配置信息
注意事项:
- 对于标记为「Local」的 MCP 服务,需要在本地安装 NPX 或 UVX 后才能使用
- 配置内容中的环境变量信息(如 API Key、Token、Access Key 等字段)须替换为你的真实信息
- 填写完成后,点击「确认」按钮保存配置
5.2 手动添加 MCP 服务
如果你需要添加的 MCP 服务不在市场中,或者你想使用自己开发的 MCP 服务,可以通过手动添加的方式:
操作步骤:
- 在 AI 侧边对话框的右上角,点击「设置」图标,然后在菜单中选择「MCP」
- 进入 MCP 配置界面后,点击右上角的「+ 添加」按钮,然后在菜单中选择「手动添加」
- 在弹出的「手动配置」窗口中,填写 MCP 服务的详细配置
配置建议:
- 优先使用 NPX 或 UVX 配置方式
- 如果你希望添加一个全新的 MCP 服务,可以参考 MCP 官方文档中的协议规范
- 配置完成后,点击「确认」按钮保存设置
5.3 配置系统环境
为确保 MCP 服务能够正常启动和运行,你可能需要安装以下工具:
5.3.1 安装 Node.js
NPX 工具依赖于 Node.js 环境(版本需大于等于 18):
- 前往 Node.js 官网,下载并安装 Node.js 18 或更高版本
- 安装完成后,在终端中运行以下命令确认安装成功:
node -v npx -v
- 若安装成功,终端会输出相应的版本号
- 重启 Trae IDE,使 Node.js 环境生效
5.3.2 安装 uvx(Python 工具)
uvx 是基于 Python 的快速执行工具,用于运行 Python 脚本的 MCP 服务:
- 前往 Python 官网,下载并安装 Python 3.8 或更高版本
- 安装完成后,在终端中运行以下命令确认安装成功:
python --version
- 执行以下命令安装 uv(包含 uvx):
- macOS / Linux 系统:
curl -LsSf https://astral.sh/uv/install.sh | sh
- Windows 系统(PowerShell):
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
- macOS / Linux 系统:
- 安装完成后,运行以下命令验证:
uvx --version
5.3.3 安装 Docker(可选)
如果你需要使用 GitHub MCP Server,则需要安装 Docker:
macOS 系统:
- 手动安装:前往 Docker 官方网站,下载适用于 macOS 的 Docker Desktop 安装包(.dmg 文件)
- 使用 Homebrew 安装:
brew install --cask docker
Windows 系统:
- 前往 Docker 官方网站,下载并安装 Docker Desktop,确保勾选「Use the WSL 2 based engine」选项
验证安装:
docker --version
docker info
5.4 MCP 服务传输类型
在 Trae IDE 中,MCP Server 支持三种传输类型:
- stdio 传输:通过标准输入/输出进行通信,适用于本地运行的 MCP 服务
- SSE 传输:Server-Sent Events 传输,支持服务端向客户端推送事件
- Streamable HTTP 传输:支持流式 HTTP 通信的传输方式
根据你选择的 MCP 服务类型,Trae IDE 会自动选择合适的传输方式。
六、MCP 开发资源
如果你想深入了解 MCP 协议或开发自己的 MCP 服务,可以参考以下资源:
- 官方 SDK:MCP 提供了多种语言的官方 SDK,包括 Python、JavaScript、Java 等,这些 SDK 处理了协议细节,让你可以专注于构建功能
- 协议规范文档:详细说明了 MCP 协议的请求和响应格式、数据类型等技术细节
- 示例代码库:包含了各种 MCP 服务和客户端的示例实现
- 社区论坛:可以与其他开发者交流 MCP 开发经验和问题
七、总结
MCP(模型上下文协议)是连接 AI 模型与外部世界的重要桥梁,它通过标准化的方式,让 AI 模型能够获取上下文信息并调用各种外部工具。在 Trae IDE 中,AI 助手正是通过 MCP 协议来提供代码搜索、文件编辑、命令执行等强大功能。
通过本文的介绍,你应该已经了解了 MCP 的基本概念、工作原理以及在 Trae IDE 中配置和使用 Gitee MCP 服务的方法。随着 AI 技术的不断发展,MCP 协议的重要性将会越来越凸显,它将为 AI 应用提供更广阔的可能性。
如果你想进一步学习 MCP,可以访问官方文档或加入 MCP 社区,与其他开发者一起探索 MCP 的更多可能性。
延伸阅读:
最后,创作不易请允许我插播一则自己开发的小程序广告,感兴趣可以访问体验:
【「合图图」产品介绍】
-
主要功能为:本地添加相册图片进行无限长图高清拼接,各种布局拼接等
-
安全:无后台服务无需登录,全程设备本地运行,隐私100%安全;
-
高效:自由布局+实时预览,效果所见即所得;
-
高清:秒生高清拼图,一键保存相册。
-
立即体验 →合图图 或微信小程序搜索「合图图」
如果觉得本文有用,欢迎点个赞👍和收藏⭐支持我吧!
借助CSS实现自适应屏幕边缘的tooltip
欢迎关注我的公众号:前端侦探
tooltip
是一个非常常见的交互,一般用于补充文案说明。比如下面这种(蓝色边框表示屏幕边缘)
通常tooltip
都会有一个固定的方向,比如top
表示垂直居中向上。
但是,如果提示文案比较多,提示区域右比较靠近屏幕边缘,就可能出现这种情况
直接超出屏幕!这很显然是不能接受的。
你可能会想到改变一下对齐方向,比如top-right
,但是这里的文案可能是不固定的,也就是会出现这样
嗯...感觉无论怎么对齐都会有局限。那么如何解决呢?一起看看吧
一、理想中的自适应对齐
我们先想想,最完美的对齐是什么样的。
其实没那么复杂,就分两种情况,一个居左,一个居右
1.居左
正常情况下,就是垂直居中朝上
如果提示文本比较多,那就靠左贴近文本容器对齐
如果提示文本继续增加,那就整行换行,并且不超过文本容器
2. 居右
正常情况下,也是垂直居中朝上
如果提示文本比较多,那就靠右贴近文本容器对齐
如果提示文本继续增加,也是整行换行,并且不超过文本容器
那么如何实现这样的对齐方式呢?
二、左自适应对齐的思路
我们先看第一种情况,看似好像有3种对齐方式,而且还要监测是否到了边界,好像挺复杂。其实换个角度,其实是这样一种规则
- 当内容较少时,居中对齐
- 当内容较多时,居左对齐
- 当内容多到换行时,有一个最大宽度
既然涉及到了对齐,那就有对齐的容器和被对齐的对象。
我们可以想象一个虚拟容器,以对齐中心(下图问号图标)向两边扩展,一直到边界处,如下所示(淡蓝色区域)
假设HTML
如下
<span class="tooltip" title="提示"></span>
当气泡文本比较少时,可以通过文本对齐实现居中,气泡可以直接通过伪元素实现
.tooltip{
width: 50px; /*虚拟容器宽度,暂时先固定 */
text-align:center;
}
.tooltip::before{
content: attr(title);
display: inline-block;
color: #fff;
background-color: #000;
padding: .5em 1em;
border-radius: 8px;
box-sizing: border-box;
}
/*居中箭头*/
.tooltip::after{
content: '';
position: absolute;
width: 1em;
height: .6em;
background: #000;
clip-path: polygon(0 0, 100% 0, 50% 100%);
top: 0;
left:0;
right:0;
margin: 0 auto;
transform: translateY(-150%)
}
使用文本居中,也就是text-align: center
有个好处,当文本不超过容器时,居中展示,就如同上图展示一样。
当文本比较多时,默认会换行,效果如下
这样应该很好理解吧。
我们需要气泡里的文本在多行时居左,可以直接给气泡设置居左对齐
.tooltip::before{
/*...*/
text-align: left;
}
效果如下
这样就实现了单行居中,多行居左的效果了。
现在还有一个问题,如何在气泡文本较多时,不被对齐容器束缚呢?
首先可以想到的是禁止换行,也就是
.tooltip::before{
/*...*/
white-space: nowrap
}
这样在文本不超过一行时确实可以
看,已经突破了容器束缚。但是文本继续增加时,也会出现无法换行的问题
我们可以想一想,还有什么方式可以控制换行呢?
这里,我们需要设置宽度为最大内容宽度,相当于文本有多少,文本容器就有多宽
.tooltip::before{
/*...*/
width: max-content
}
看似好像和不换行一样
实则不然,我们并没用禁止换行。只要给一个最大宽度,立马就换行了
.tooltip::before{
/*...*/
width: max-content;
max-width: 300px;
}
效果如下
是不是几乎实现了我们想要的效果了?
不过,这里涉及了两个需要动态计算的宽度,一个是虚拟容器宽度,还有一个是外层最大宽度,
下面看如何实现
三、借助JS计算所需宽度
现如今,外层的最大宽度倒是可以通过容器查询获得,但内部的虚拟容器宽度还无法直接获取,只能借助JS
了。
不过我们这里可以先只计算左侧偏移,也就是一半的宽度
具体实现如下
//问号中心到左侧距离
const x = this.offsetLeft - 8
// 问号的宽度
const w = this.clientWidth
// 外层整行文本容器宽度
const W = this.offsetParent.clientWidth - 32
// 左侧偏移
this.style.setProperty('--x', x + 'px')
// 外层文本容器宽度(气泡最大宽度)
this.style.setProperty('--w', W + 'px')
然后给前面待定的宽度绑定这些变量就行了
.tooltip{
/*...*/
width: calc(var(--x) * 2);
}
.tooltip::before{
/*...*/
max-width: var(--w);
}
这样左侧就完全实现自适应了,无需实时计算,仅需初始化一次就好了
四、完全自适应对齐
前面是左侧,那右侧如何判断呢?我们可以比较左侧距离的占比,如果超过一半,就表示现在是居右了
这里用一个属性表示
this.tooltip.dataset.left = x/W < 0.5 //是否居左
然后就右侧虚拟容器的宽度了,和左侧还有有点不一样
前面我们已经算出了左侧距离,由于超过了一半,所以需要先减然后再乘以二
.tooltip[data-left="false"]::before{
/*...*/
width: calc( (var(--w) - var(--x)) * 2);
max-width: var(--w);
}
其实这里还是有个小问题的,当气泡文字比较长时,仍然是朝右突破了边界,如下所示
这是因为默认的语言流向造成的(从左往右),解决这个问题也非常简单,仅需改变语言方向就可以了,要用到direction:rtl
,如下
.tooltip[data-left="false"]::before{
/*...*/
width: calc( (var(--w) - var(--x)) * 2);
max-width: var(--w);
direction: rtl;
}
这样就完美了
现在来看一下所有边界情况的演示
你也可以访问在线demo真实体验:codepen.io/xboxyan/pen…
如果你是 vue3 项目,可以直接用这段封装好的组件(其实没几行代码,大部分自适应都是CSS
完成的)
<!-- 极度自适应的tooltips -->
<script setup lang="ts">
const props = defineProps({
text: String,
gap: {
type: Number,
default: 12,
},
})
const show = ref(false)
const pos = reactive({
x: 0,
w: 0,
top: 0,
gap: 0,
isLeft: true,
})
const click = (ev: MouseEvent) => {
// console.log()
// if (ev.target) {
// ev.stopPropagation()
// }
const target = ev.target as Element | null
console.log('xxxxxxxxxxx', target)
if (target) {
const { x, y, width } = target.getBoundingClientRect()
pos.top = y + window.scrollY
pos.gap = props.gap
pos.x = x + width / 2 - props.gap
pos.w = window.innerWidth - props.gap * 2
show.value = true
}
}
const wrap = ref<HTMLElement>()
document.body.addEventListener('touchstart', (ev) => {
// 没有点击当前触发对象就隐藏tooltips
if (!(wrap.value && ev.target && wrap.value.contains(ev.target as Node))) {
show.value = false
}
})
</script>
<template>
<span class="wrap" ref="wrap" @click="click">
<slot></slot>
</span>
<Teleport to="body">
<div
class="tooltip"
v-show="show"
:data-title="text"
:data-left="pos.x / pos.w < 0.5"
:style="{
'--x': pos.x + 'px',
'--top': pos.top + 'px',
'--gap': pos.gap + 'px',
'--w': pos.w + 'px',
}"
></div>
</Teleport>
</template>
<style>
.wrap {
display: contents;
}
.tooltip {
position: absolute;
top: var(--top);
text-align: center;
pointer-events: none;
}
.tooltip[data-left='true'] {
width: calc(var(--x) * 2);
left: var(--gap);
}
.tooltip[data-left='false'] {
width: calc((var(--w) - var(--x)) * 2);
right: var(--gap);
direction: rtl;
}
.tooltip::before {
content: attr(data-title);
display: inline-block;
color: #fff;
background-color: #191919;
padding: 0.5em 0.8em;
border-radius: 8px;
transform: translateY(calc(-100% - 0.5em));
width: max-content;
max-width: var(--w);
box-sizing: border-box;
text-align: left;
}
.tooltip::after {
content: '';
position: absolute;
width: 1.2em;
height: 0.6em;
background: #000;
clip-path: polygon(0 0, 100% 0, 50% 100%);
top: 0;
left: 0;
right: 0;
margin: 0 auto;
transform: translateY(calc(-100% - 0.2em));
}
</style>
五、推荐一个开源库
其实市面上有一个库可以完成类似的交互,叫做 float-ui
这个是专门做popover
这类交互的,其中有一个shift
属性,可以做这种跟随效果
不过对于大部分情况,引入一个单独的库还是成本偏大,建议还是纯原生实现。
这样一个极度自适应的气泡组件,你学会了吗,赶紧在项目中用起来吧~最后,如果觉得还不错,对你有帮助的话,欢迎点赞、收藏、转发 ❤❤❤
事件委托的深层逻辑:当冒泡不够时⁉️
前言
在项目不断扩大之时,管理用户交互变的越来越重要,为每个交互元素附加一个事件监听器是一种糟糕的做法,因为它会导致代码混乱、内存消耗增加以及性能瓶颈。这时,事件委托就派上用场了。
认识dom事件传播
三个阶段
当事件在 DOM 元素上触发时,它不会简单地到达目标并停止。相反,它会经历以下阶段:
-
捕获阶段旅程从
window
级别开始,沿着 DOM 树向下移动,经过每个祖先元素,直到到达目标的父级。带有(中的第三个参数)的事件监听器在此触发useCapture = true``addEventListener
- 目标阶段在此阶段,事件到达预期的目标元素。所有直接附加到此元素的监听器都会被触发
-
冒泡阶段命中目标后,事件会沿着 DOM 向上“冒泡”,从目标的父元素到祖父元素,依此类推,直到到达目标
window
。默认情况下,大多数事件监听器都在此阶段运行
事件在dom树中流动过程
< div id = "grandparent" >
< div id = "parent" >
< button id = "child" >点击我</ button >
</ div >
</ div >
如果您单击,则事件流程如下:<button id="child">
click
-
捕获 -
window
->document
-><html>
-><body>
-><div id="grandparent">
-><div id="parent">
-
目标-
<button id="child">
-
冒泡 -
<button id="child">
-><div id="parent">
-><div id="grandparent">
-><body>
-><html>
->document
->window
什么是事件委托
事件委托是一种将事件监听器添加到多个子元素的父元素上,而不是分别添加到每个子元素上的方法。当子元素上发生事件时,它会触发父元素上的监听器,父元素会检查是哪个子元素触发了该事件。
假设一个<ul>
包含<li>
以下项目的简单列表:
< ul id = "myList" >
< li >项目 1 </ li >
< li >项目 2 </ li >
< li >项目 3 </ li >
< li >项目 4 </ li >
</ ul >
而不是为每个添加一个点击监听器<li>
:
const listItems = document . querySelectorAll ( '#myList li' );
listItems . forEach ( item => {
item . addEventListener ( 'click' , ( event ) => { console . log ( `点击于: $ { event . target . textContent } ` );
});
});
通过事件委托,你可以将一个监听器附加到<ul>
父级:
onst myList =文档. getElementById ( 'myList' );
myList . addEventListener ( 'click' , ( event ) => {
// 检查点击的元素是否为 <li>
if ( event . target . tagName === 'LI' ) {
console . log ( ` Clicked on : $ { event . target . textContent } ` );
}
});
在此示例中,当<li>
点击任意一个时,click
事件都会冒泡到。然后,myList
上的单个事件监听器会检查是否是触发了该事件,并采取相应的措施:myList``event.target.tagName``<li>
为什么事件委托如此重要
- 无需添加数百或数千个监听器,只需几个父容器就足够了,从而大大减少内存占用
- 更少的监听器可以提高浏览器整体系统内存的使用率,并减少 JavaScript 引擎在事件管理和调度方面的工作量
- 它支持动态创建元素,这非常实用。假设在页面加载后(例如,在 API 调用后)
<li>
添加了新元素,监听器仍然有效。无需重新连接监听器。#myList``#myList
事件委托中常见的误区
event.target vs event.currentTarget
-
event.target
是触发事件的特定元素。 -
event.currentTarget
是事件监听器实际附加到的元素。
stopPropagation ()和stopImmediatePropagation ()
-
event.stopPropagation()
– 此方法仅允许事件停止沿 DOM 树向上或向下冒泡或捕获。如果在子元素的事件处理程序中执行此方法,则其祖先元素上的任何委托监听器都将无法访问该事件 -
event.stopImmediatePropagation()
这不stopPropagation()
的复制粘贴。它的相似之处仅限于添加了这个效果:它阻止进一步的事件传播,并阻止绑定到同一元素的任何其他监听器被执行。
在某些情况下,它们会破坏委托处理程序,例如:子元素的事件处理程序调用stopPropagation
将导致位于 DOM 层次结构中更高层级的任何委托监听器的功能失效。委托监听器将无法接收事件。这对于分析、集中式 UI 逻辑或可访问的自定义控件功能尤其麻烦。
非冒泡事件
最突出的非冒泡事件包括:
-
focus
– 当元素获得焦点时触发 -
blur
– 当元素失去焦点时触发 -
mouseenter
– 当指针进入元素时触发 -
mouseleave
– 当指针离开元素时触发
为什么它们不起泡
由于浏览器的工作方式以及过去的兼容性问题,通常无法触发此类事件。focus
和blur
旨在在获得或失去焦点的特定元素上触发,因此不存在冒泡。mouseenter
和mouseleave
与mouseover
和 mouseout
配对(它们会产生冒泡);但是与mouseover
和 mouseout
不同,mouseenter
和 mouseleave
仅在指针位于元素上(而不是其子元素上)时触发。
对于非冒泡事件只能通过自定义冒泡事件来替代
总结
事件委托通过将单个监听器附加到父元素来简化事件处理。当子元素触发事件时,它会向上冒泡到父元素,从而减少内存占用并简化代码。
这种技术在管理大量相似元素(例如列表项或按钮)时非常有效,尤其是在它们动态生成的情况下。父级监听器无需额外配置即可处理新添加元素的事件。
并非所有事件都会冒泡 focus
、blur
、mouseleave
等是例外。对于这些事件,可以用 focusin
、focusout
或自定义冒泡事件等替代方法。
全面解析this-理解this指向的原理
参考资料
- 《你不知道的JavaScript》- this全面解析
this 是什么
- this 是一个代词,代指一个对象
- this 提供了一种更优雅的方式来隐式的传递一个对象引用,可以让代码更加简洁易于复用
调用位置
调用位置就是函数在代码中被调用的位置(而不是声明的位置)。调用位置决定了 this 的绑定
比如下面代码:
function baz() {
// 当前调用栈是:baz
// 因此,当前调用位置是全局作用域
console.log("baz");
bar(); // <-- bar 的调用位置
}
function bar() {
// 当前调用栈是 baz -> bar
// 因此,当前调用位置在 baz 中
console.log("bar");
foo(); // <-- foo 的调用位置
}
function foo() {
// 当前调用栈是 baz -> bar -> foo // 因此,当前调用位置在 bar 中
console.log("foo");
}
baz(); // <-- baz 的调用位置
可以使用浏览器的开发工具查看调用栈
|
|
|
---|
this的绑定规则
this的指向有以下四条特性/规则
- 默认绑定
- 隐式绑定
- 显式绑定
- new 绑定
默认绑定
当函数被独立调用时,函数的 this 指向 window
独立调用就是像这样:
foo()
的调用
比如
function foo() {
console.log(this.a);
}
var a = 2;
foo(); // 2
如果使用严格模式(strict mode),那么全局对象将无法使用默认绑定,因此 this 会绑定到 undefined
隐式绑定
当函数引用有上下文对象 且被该对象调用时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
// 这里就是隐式绑定,foo函数的this绑定到了obj上
需要注意的是,对象属性引用链中只有最顶层或者说最后一层会影响调用位置
比如
function foo() {
console.log(this.a);
}
var obj2 = {
a: 42,
foo: foo,
};
var obj1 = {
a: 2,
obj2: obj2,
};
obj1.obj2.foo(); // 42
// 相当于 obj2.foo(),因为只有最后一层会影响调用位置
隐式丢失
隐式绑定的函数可能会丢失绑定对象,也就是说它会应用默认绑定
隐式丢失的几种情况:
- 函数别名
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo,
};
// 注意这里的函数别名,会导致隐式绑定丢失,导致foo函数的this指向全局
var bar = obj.foo; // 函数别名!
var a = "oops, global"; // a 是全局对象的属性
bar(); // "oops, global"
- 函数作为参数传入,并调用时
function foo() {
console.log(this.a);
}
function doFoo(fn) {
// fn 其实引用的是 foo
fn(); // <-- 调用位置!
}
var obj = {
a: 2,
foo: foo,
};
var a = "oops, global"; // a 是全局对象的属性
doFoo(obj.foo); // "oops, global"
参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一个例子一样
显式绑定
-
fn.call(obj, x, x, ...)
将fn函数的this指向obj,并调用,call的剩余参数是fn需要的参数
function foo(aram) {
console.log("foo", param.a); // foo 1
function bar(x, y) {
console.log("bar", this.a, x, y); // bar 1 2 3
}
bar.call(param, 2, 3);
}
foo({ a: 1 });
-
fn.apply(obj, [x, x, ...])
将fn函数的this指向obj,并调用,apply的第二个参数是一个数组
function foo(param) {
console.log("foo", param.a); // foo 1
function bar(x, y) {
console.log("bar", this.a, x, y); // bar 1 2 3
}
bar.apply(param, [2, 3]);
}
foo({ a: 1 });
-
fn.bind(obj, x, x, ...)(x, x, ...)
将fn函数的this指向obj,bind会返回一个新的函数,新的函数也可以传递参数
function foo(param) {
console.log("foo", param.a); // foo 1
function bar(x, y) {
console.log("bar", this.a, x, y); // bar 1 2 3
}
return bar.bind(param, 2);
}
const bar = foo({ a: 1 });
bar(3);
new 绑定
new 绑定 - this会绑定到新创建的对象上
function Person(name, age) {
// this 指向新创建的对象
this.name = name;
this.age = age;
// 通过new调用构造函数时,this指向新创建的对象
// 直接调用构造函数时,应用默认绑定规则,this指向全局或undefined
console.log(this);
}
// 使用 new 关键字调用构造函数
const person1 = new Person("张三", 25);
console.log(person1.name); // "张三"
console.log(person1.age); // 25
// 不使用 new 调用,this 会指向全局对象(非严格模式)或 undefined(严格模式)
const person2 = Person("李四", 30); // this 不会指向新对象
console.log(person2); // undefined (因为没有显式返回)
箭头函数
- 箭头函数没有自己的this 指向,它需要继承外层函数的this指向
- 箭头函数即使是new也无法改变this指向,因此箭头函数不能用于编写构造函数
var a = 1;
function foo() {
var obj1 = {
a: 2,
bar: function () {
console.log("bar", this.a);
var obj2 = {
a: 3,
baz: () => {
console.log("baz", this.a);
},
};
// 箭头函数不会创建自己的 this,它会捕获外层函数的 this
obj2.baz(); // baz 2
},
};
console.log("foo", this.a);
obj1.bar(); // bar 2
}
foo(); // foo 1
神奇魔法类:使用 createMagicClass 增强你的 JavaScript/Typescript 类
什么是神奇魔法类?
神奇魔法类(Magic Class)是一种特殊的类包装器,它通过createMagicClass
函数创建,能够赋予普通 JavaScript/TypeScript 类超能力。这个工具让你的类既可以作为构造函数使用,又可以作为函数调用来配置选项,大大增强了类的灵活性和可用性。
神奇特性
1. 双重身份
魔法类具有双重身份,可以:
- 作为普通类使用用于继承:
class MyClass extends MagicClass
- 作为函数调用来传入选项用于构建类:
class MyClass extends MagicClass({<options>})
2. 生命周期钩子
提供完整的生命周期钩子,让你能够精确控制实例的创建过程:
-
onBeforeInstance
: 实例创建前触发,可以阻止实例创建或修改类 -
onAfterInstance
: 实例创建后触发,可以对实例进行后处理 -
onErrorInstance
: 实例创建出错时触发,可以进行错误处理
使用示例
基本用法
import { createMagicClass } from "flex-tools/classs";
type UserCreateOptions = {
prefix?: string;
x?: number;
};
// 定义一个普通类
class User {
name: string;
prefix: string = "";
constructor(name: string) {
this.name = name;
this.prefix = getMagicClassOptions<UserCreateOptions>(this)?.prefix!;
}
get title() {
return `${this.prefix}${this.name}`;
}
toString() {
return `${this.constructor.name}<${this.name}>`;
}
}
// 创建魔术类
const MagicUser = createMagicClass<typeof User, UserCreateOptions>(User, {
prefix: "Hi,", // 默认配置
x: 1,
onBeforeInstance: (cls, args, _options) => {},
onAfterInstance: (inst, _options) => {},
});
// 直接作为类使用
class Admin extends MagicUser {}
class Guest extends MagicUser({ x: 2, prefix: "欢迎," }) {}
class Customer extends MagicUser({ prefix: "尊贵的" }) {}
const user = new User("用户");
const admin = new Admin("管理员");
const guest = new Guest("访客");
const customer = new Customer("客户");
高级用法:拦截实例创建
const ValidatedPerson = createMagicClass(Person, {
onBeforeInstance: (cls, args, options) => {
const name = args[0];
// 验证名称
if (!name || name.length < 2) {
throw new Error("Name must be at least 2 characters long");
}
// 可以修改参数
args[0] = name.charAt(0).toUpperCase() + name.slice(1);
// 返回false会阻止实例创建
// 返回一个对象会使用该对象作为实例
// 返回一个类会使用该类创建实例
},
onErrorInstance: (error, cls, options) => {
console.error("Failed to create person:", error.message);
},
});
class SitePerson extends ValidatedPerson {}
// 这将抛出错误,因为名称太短
try {
const invalid = new SitePerson("A");
} catch (e) {
console.log(e.message); // "Name must be at least 2 characters long"
}
// 这将成功,并且名称首字母会被自动大写
const valid = new SitePerson("bob"); // 实际名称将是 "Bob"
获取实例配置
你可以使用getMagicClassOptions
函数获取实例的配置选项:
import { createMagicClass, getMagicClassOptions } from "flex-tools/classs";
const MagicPerson = createMagicClass(Person, { version: "1.0" });
const ConfiguredPerson = MagicPerson({ theme: "dark" });
const person = new ConfiguredPerson("Alice");
const options = getMagicClassOptions(person);
console.log(options); // { version: '1.0', theme: 'dark' }
总结
神奇魔法类通过createMagicClass
函数提供了一种优雅而强大的方式来增强 JavaScript/TypeScript 类的能力。它不仅保留了原始类的所有功能,还添加了配置选项、生命周期钩子和灵活的实例化方式,使你的代码更加灵活、可配置且易于维护。
无论你是构建复杂的 UI 组件、可配置的工具类,还是需要精细控制实例创建过程的系统,神奇魔法类都能为你提供强大而灵活的解决方案。
详见flex-tools。
一个前端开发者的救赎之路-JS基础回顾(三)-Function函数
函数的声明
- 赋值式:var func = function() {}
- 声明式:function func() {}
- 箭头函数:()=>{}
- 三者之间的区别
a. 声明式可以在函数声明前去调用,赋值式不可以
b. 箭头函数,他们从定义自己的环境继承上下文,而不是像以其他方式定义的函数那样定义自己的调用上下文
c. 箭头函数没有prototype属性,所以它不能作为新类的构造函数
函数的调用
- 作为函数:
- 注意这个说法,函数是通过调用表达式被作为函数或方法调用的。
- 调用表达式包括求值为函数对象的函数表达式,后跟一对圆括号,圆括号中是逗号分隔的零或多个参数表达式列表
- this指向:
- 非严格模式:全局对象
- 严格模式:undefined
-
注意
: 箭头函数又有不同:它们总是继承自身定义所在环境的this值(这里即使是严格模式,this也是window) - 下面的代码可以用来判断是不是处于严格模式
// 定义并调用函数,以确定当前是不是严格模式 const strict = (function() { return !this }())
- 我有一篇文章,这里面有一个自己遇到的有趣的this指向和作用域的问题
- 这里注意条件式调用:
- 在Es2020中,可以在函数表达式的后面、圆括号的前面插入
?.
,从而只在函数不是null或undefined的情况下调用。 - 在没有副作用的前提下:f?.()<=>(f !== null && f !== undefined ? f() : undefined)
- 在Es2020中,可以在函数表达式的后面、圆括号的前面插入
- 作为方法:
-
this关键字不具有变量那样的作用域机制,除了箭头函数,嵌套函数不会继承包含函数的this值。如果嵌套函数被当做方法来用,那它的this就是调用它的对象。如果嵌套函数(不是箭头函数)被当做函数来调用,则它的this值要么是全局对象(非严格模式),要么是undefined(严格模式)
let o = { a: 1, b: 2, m: function() { let self = this; console.log(this.a); // 1 f(); function f() { console.log(this.b); // undefined } const g = () => { console.log(this.b); // 2 } g(); } } o.m();
-
- 作为构造函数
- 作为构造函数调用,上下文是这个新对象
- 通过call()或apply()方法调用
- 这两个方法允许我们指定调用时的this,这意味着可以将任意函数作为任意对象的方法来调用
- call()方法使用自己的参数列表作为函数的参数,而apply()方法则期待数组值作为参数。
- 通过JavaScript语言隐式调用
- 这里在开发中排查bug,很关键,就是我们引用了别人的三方库或者代码都有可能出现这个隐式调用
函数的参数
- 函数调用时所传的实参,可以少于形参也可以多于形参,
- 函数定义式可以用一个剩余参数来接收多余的参数,
剩余参数必须作为最后一个参数
,并且是...args
这种形式,剩余参数永远是一个数组,不会是undefined,即使没有传对应的实参,因此不需要给剩余参数默认值
JS预解析
- 对于函数的预解析和普通变量不一样,函数预解析是直接把整个函数提到顶部作用域,在预解析时会提前定义,只是不会立即执行。
- 普通变量只是把声明提升到所在作用域顶部,而不进行初始化。
- 因此,上面函数定义的时候,如果使用字面量的方式会把这个变量提升到顶部作用域并赋值undefined,所以在定义前调用的时候会报错。
-
let
和const
会提升(hoisting),但由于 TDZ(暂时性死区) 机制,在声明前访问会报错。
作用域
- 作用域在定义的时候,一个函数就会形成一个内部作用域,后来引入了let/const又有了块级作用域
- 作用域在访问的时候,是从下往上找,一直找到顶级作用域,找不到就报is not defined
- 在赋值的时候不是,如果一直到最顶级都未声明,那他就是直接在全局定义域声明并且赋值这个变量。所以即使在vue中的某一个函数直接使用xxx=来进行操作,也是会在全局作用域添加一个全局变量xxx(
内存泄漏
),千万不要这样干
函数的方法和属性
- length属性:返回声明时声明的形参的个数(不包含剩余参数),只读
- name属性:表示定义函数时使用的名字(如果是用名字定义的),如果是未命名的函数,表示在第一次创建这个函数时赋给该函数的变量名或属性名。这个属性主要用于记录调试或排错信息。只读
- prototype属性:箭头函数没有
- call()和apply()方法:
箭头函数的this不会被修改
- bind()方法:bind()方法主要目的是把函数绑定到对象,
箭头函数不起作用
- toString()方法:
- ECMAScript规定返回一个符合函数声明语句的字符串
- 实际上,多数(不是全部),都是返回函数完整源代码
- 内置函数返回的字符串中通常包含“[native code]”,表示函数体
- Function()构造函数:
- Function()构造函数可以接收
任意多个
字符串参数,其中最后一个参数
是函数体的文本。这个函数体 文本中可以包含任意JavaScript语句,相互以分号分隔。传给构造函数的其他字符串参数,将作为这个新函数的的形参。 - 注意:Function()构造函数
不接受任何指定新函数名字的参数
。与函数字面量一样,Function()构造函数创建的也是匿名函数
- Function()构造函数可以接收