普通视图

发现新文章,点击刷新页面。
今天 — 2026年1月10日掘金 前端

uni-app实现本地MQTT连接

作者 李剑一
2026年1月9日 22:27

最近接到安卓端的需求,要求使用MQTT连接实现设备信息的收发。

可能有兄弟不太清楚 MQTT协议 是什么,简单地说它是一种轻量级的、基于发布/订阅模式的消息传输协议,广泛用于物联网(IoT)领域。

常见的操作就是连接后有N个设备订阅了主题A,这时候任意一台设备对主题A发布了一条信息,则当前N个设备都能收到这条消息。

至于其他的也不用知道的太清楚,反正知道一般是这么玩的就行。

假如你想知道的更为详细,可以询问 Trae

image.png

需求

  1. 实现MQTT连接
  2. 实现主题(Topic)订阅
  3. 能够发送消息
  4. 能够接收消息

实现方案

首先是安装 mqtt.js 插件和 uuid.js 插件。

npm i mqtt@3.0.0 uuid

注意: 这里一定要安装 mqtt v3.0 版本,我之前装了一个 v5.x 版本,有比较大的 API 改动。项目上时间紧张,遂没有再尝试。(有兴趣的朋友可以考虑用最新版实现)

uuid 只是起到生成 连接ID 和 消息ID 的作用,这里也可以直接使用时间戳的方式,但是为了保证不重复,这里便不再更换了。

// mqttClient.js 对 MQTT 的简单封装
let client = null;
/**
 * 初始化 MQTT 连接
 * @param {Object} options
 * @param {string} options.host - 服务器地址(不带协议)
 * @param {number} options.port - 端口(通常 WebSocket 是 8083、8084 或 443)
 * @param {string} options.clientId - 客户端 ID(建议唯一)
 * @param {string} [options.username] - 用户名
 * @param {string} [options.password] - 密码
 * @param {boolean} [options.useWss=false] - 是否使用 wss(HTTPS)
 */
export function initMqtt(options) {
const {
host,
port,
clientId,
username = '',
password = '',
useWss = false
} = options;

// 根据平台选择协议(App 和 H5 用 ws/wss,小程序用 wxs/wss)
let protocol = 'ws';
if (useWss) protocol = 'wss';

// #ifdef MP-WEIXIN
protocol = useWss ? 'wxs' : 'wxs';
// #endif

const url = `${protocol}://${host}:${port}/mqtt`; // 注意:路径可能是 / 或 /mqtt,根据你的 broker 配置

const connectOptions = {
clientId: clientId,
username: username,
password: password,
keepalive: 60, // 心跳间隔(秒)
reconnectPeriod: 5000, // 自动重连间隔(毫秒)
connectTimeout: 10000, // 连接超时
clean: true // 是否清除会话
};

// 引入 mqtt(必须用 min 版本,否则小程序报错)
const mqtt = require('mqtt/dist/mqtt.min.js');

client = mqtt.connect(url, connectOptions);

// 监听连接事件
client.on('connect', () => {
console.log('MQTT 连接成功');
});

client.on('reconnect', () => {
console.log('MQTT 正在重连...');
});

client.on('error', (err) => {
console.error('MQTT 连接错误:', err);
});

client.on('close', () => {
console.log('MQTT 连接已关闭');
});

return client;
}

/**
 * 订阅主题
 * @param {string} topic
 * @param {Function} callback - (topic, message) => {}
 */
export function subscribe(topic, callback) {
if (!client) {
console.warn('MQTT 客户端未初始化');
return;
}

client.subscribe(topic, (err) => {
if (!err) {
console.log(`订阅主题成功: ${topic}`);
} else {
console.error('订阅失败:', err);
}
});

// 监听消息(全局监听,需配合 topic 过滤)
client.on('message', (receivedTopic, message) => {
// if (receivedTopic === topic) {
try {
// 尝试解析 JSON
const payload = JSON.parse(message.toString());
callback(receivedTopic, payload);
} catch (e) {
// 非 JSON 消息
callback(receivedTopic, message.toString());
}
// }
});
}

/**
 * 发布消息
 * @param {string} topic
 * @param {string|object} message
 */
export function publish(topic, message) {
if (!client) {
console.warn('MQTT 客户端未初始化');
return;
}

const payload = typeof message === 'object' ? JSON.stringify(message) : message;
client.publish(topic, payload, {
qos: 1
}, (err) => {
if (err) {
console.error('发布失败:', err);
} else {
console.log(`发布成功: ${topic}`, payload);
}
});
}

/**
 * 断开连接
 */
export function disconnect() {
if (client) {
client.end(true); // true 表示强制断开
client = null;
console.log('MQTT 已断开');
}
}

我让 Trae 基于网上的封装简单进行了修改,生成了上述封装Js文件。

注意:在监听消息的部分,如果你的 Topic 是固定的,则请将 if (receivedTopic === topic)注释打开,如果不是,则注释掉,后续有详解。

现在进入到 page/index/index.vue 文件中

import {initMqtt, subscribe, publish, disconnect} from './mqttClient.js';
export default {
    data() {
        return {
            host: 'broker.emqx.io',
            messages: []
        }
    },
    mounted() {
        this.connect()
    },
    beforeDestroy() {
        this.disconnect()
    },
    methods: {
        // 连接
        connect() {
            const uuid = uuidv4();
            const client = initMqtt({
                host: this.host,
                port: 8083,
                clientId: uuid,
                username: '',
                password: '',
                useWss: false // 如果是 HTTPS/WSS,设为 true
            });
            if (client) {
                this.subscribeTopic();
            }
        },
        // 订阅 test/topic 主题
        subscribeTopic() {
            subscribe('test/topic', (topic, message) => {
                // 接收到的消息
                const msg = JSON.stringify(message);
                this.messages.unshift(msg);
            });
        },
        // 订阅 带通配符 的主题
        subscribeTopics() {
            subscribe('+/topic', (topic, message) => {
                // 接收到的消息
                const msg = JSON.stringify(message);
                this.messages.unshift(msg);
            });
        },
        // 发送消息到 test/topic 主题
        publishMessage() {
            publish('test/topic', {
                name: 'uni-app',
                time: new Date().toISOString(),
                msg: '测试信息'
            });
        },
        disconnect() {
            disconnect();
        }
    }
}

注意:在卸载页面的时候建议将 MQTT连接 断开,或者在 uni-app 中使用也可以使用 unload 方法断开连接。

另外订阅主题可以采用常规的,确定性的 Topic 主题名称,也可以采用 通配符(+) 的主题名称。

Trae还提醒我一定要注意,假如你的 Topic 为:

123/456/report 则你的通配符主题名称为 +/+/report

假如你的 Topic 为:

/123/456/report 则你的通配符主题名称为 +/+/+/report,因为他的第一位是 空字符串

结论

在开发 IoT 设备时这个协议用的比较广泛,如果不接触这个方面一般不会用上这个。

另外 MQTT 本身自带心跳自动重连机制,大致上相当于 WebSocket,这个部分一般不需要做特殊开发。

GDAL 实现空间分析

作者 GIS之路
2026年1月9日 22:26

^ 关注我,带你一起学GIS ^

前言

空间分析基于空间关系,常见的如叠加、求差、取反等。在GIS开发中,空间分析作为其灵魂支柱,具有重要意义,是每一个GISer都必须要掌握的技能。

在之前的文章中讲了如何使用GDAL或者ogr2ogr工具将txt以及csv文本数据转换为Shp格式,本篇教程在之前一系列文章的基础上讲解如何使用GDAL 实现空间分析

如果你还没有看过,建议从以上内容开始。

1. 开发环境

本文使用如下开发环境,以供参考。

时间:2025年

系统:Windows 11

Python:3.11.7

GDAL:3.11.1

2. 空间分析

定义一个方法SpatialAnalysis用于判断要素间空间关系,该方法接收两个参数,其中sourcePath指向源数据路径,resultPath指向结果文件路径。

"""说明:GDAL 空间分析操作参数:    -sourcePath:Shp 数据路径    -resultPath:生成结果数据路径"""def SpatialAnalysis(sourcePath,resultPath):

接着是基础的常规操作,添加数据驱动,获取数据源,获取目标图层。

# 检查文件是否存在checkFilePath(sourcePath)# 添加数据驱动shpDriver = ogr.GetDriverByName("ESRI Shapefile")# 获取数据源shpDs = shpDriver.Open(sourcePath)# 获取图层shpLayer = shpDs.GetLayer(0)srs = shpLayer.GetSpatialRef()geomType = shpLayer.GetGeomType()

方法checkFilePath用于检查文件路径是否正常,接受一个路径参数。

"""说明:检查文件路径是否正常参数:    -filePath:文件数据路径"""def checkFilePath(filePath):    if os.path.exists(filePath):        print("文件数据路径存在")    else:        print("文件数据路径不存在,请检查!")

本文基于以下数据进行空间分析操作。对涉及面的操作分别获取面0和面1两个要素。GDAL中的几何处理操作,如叠加、缓冲、求差等均返回一个新的几何对象。

"""获取第一个图层要素"""# feature = shpLayer.GetNextFeature()# 根据FId获取指定要素feature1 = shpLayer.GetFeature(0)print(f"n面状要素~~~~~Id:{feature1.GetField('Id')}n")# 获取要素IdfeatId1 = feature1.GetField("Id")# 获取几何要素geom1 = feature1.GetGeometryRef()# print(f"面状几何:{geom1}")"""    获取第二个要素"""feature2 = shpLayer.GetFeature(1)geom2 = feature2.GetGeometryRef()featId2 = feature2.GetField("Id")

3. 空间叠加

Geometry对象上具有一个方法Intersection用于空间叠加操作,该方法接收另一个几何对象,并返回一个新的Geometry对象。

"""    空间操作之【叠加】"""resultGeom = geom1.Intersection(geom2)# print(f"面要素【{featId1}】与 面要素【{featId2}】相交结果:{resultGeom}")

如下面0和面1相交结果为图中高亮部分。

4. 空间缓冲

Geometry对象上具有一个方法Buffer用于空间缓冲区生成,该方法接收另一个几何对象,并返回一个新的Geometry对象。

"""    空间操作之【缓冲】"""resultGeom = geom1.Buffer(1)# print(f"面要素【{featId1}】缓冲结果:{resultGeom}")

如下面0缓冲结果为图中高亮部分。

如下线段缓冲结果为图中高亮部分。

如下点缓冲结果为图中高亮部分。

5. 空间联合

Geometry对象上具有一个方法Union用于合并几何对象,该方法接收另一个几何对象,并返回一个新的Geometry对象。

"""    空间操作之【联合】"""resultGeom = geom1.Union(geom2)# print(f"面要素【{featId1}】联合结果:{resultGeom}")

如下面0和面1联合结果为图中高亮部分。

6. 空间求差

Geometry对象上具有一个方法Difference用于求取目标几何对象差集,该方法接收另一个几何对象,并返回一个新的Geometry对象。

"""    空间操作之【求差】"""resultGeom = geom1.Difference(geom2)# print(f"面要素【{featId1}】联合结果:{resultGeom}")

如下面0和面1求差结果为图中高亮部分。

7. 对称差集

Geometry对象上具有一个方法SymmetricDifference用于几何对象交集取反操作,该方法接收另一个几何对象,并返回一个新的Geometry对象。

"""    空间操作之【求对称差集】"""resultGeom = geom1.SymmetricDifference(geom2)print(f"面要素【{featId1}】联合结果:{resultGeom}")

如下面0和面1求对称差集结果为图中高亮部分。

还有一些几何操作,如求等、求距离等内容本教程并未涉及,留给感兴趣的读者自行实践。

8. 创建新图层

几何运算完成之后,需要将分析结果保存到新的Shp图层。通过数据驱动方法CreateDataSource创建Shp数据源,再使用数据源方法CreateLayer创建图层即可。

"""    创建新图层"""targetDs = shpDriver.CreateDataSource(resultPath)targetLayer = targetDs.CreateLayer("result_layer",srs,ogr.wkbPolygon)featureDefn = shpLayer.GetLayerDefn()tarFeat = ogr.Feature(featureDefn)fieldCount = featureDefn.GetFieldCount()tarFeat.SetGeometry(resultGeom)for i in range(fieldCount):    fieldDefn = featureDefn.GetFieldDefn(i)    name = fieldDefn.GetName()    value = feature1.GetField(i)    print(f"字段名称:{name},字段值:{value}")    # tarFeat.SetField(i,value)    tarFeat.SetField(name,value)targetLayer.CreateFeature(tarFeat)

9. 注意事项

windows开发环境中同时安装GDALPostGIS,其中投影库PROJ的环境变量指向PostGIS的安装路径,在运行GDAL程序时,涉及到要素、几何与投影操作时会导致异常。具体意思为GDAL不支持PostGIS插件中的投影库版本,需要更换投影库或者升级版本。

RuntimeError: PROJ: proj_identify: D:Program FilesPostgreSQL13sharecontribpostgis-3.5projproj.db contains DATABASE.LAYOUT.VERSION.MINOR = 2 whereas a number >= 5 is expected. It comes from another PROJ installation.

解决办法为修改PROJ的环境变量到GDAL支持的版本或者在GDAL程序开头添加以下代码:

os.environ['PROJ_LIB'] = r'D:\Programs\Python\Python311\Libsite-packages\osgeo\data\proj'

视频效果

GDAL 实现空间分析操作

OpenLayers示例数据下载,请在公众号后台回复:ol数据

全国信息化工程师-GIS 应用水平考试资料,请在公众号后台回复:GIS考试

GIS之路 公众号已经接入了智能 助手,可以在对话框进行提问,也可以直接搜索历史文章进行查看。

都看到这了,不要忘记点赞、收藏 + 关注

本号不定时更新有关 GIS开发 相关内容,欢迎关注 


    

GeoTools 开发合集(全)

OpenLayers 开发合集

GDAL 实现空间分析操作

GDAL 空间关系解析

GDAL 实现数据空间查询

GDAL 实现数据属性查询

GDAL 实现创建几何对象

GDAL 实现自定义数据坐标系

GDAL 实现矢量数据读写

GDAL 数据类型大全

GDAL 实现 GIS 数据读取转换(全)

昨天 — 2026年1月9日掘金 前端

JavaScript 如何准确判断数据类型?5 种方法深度对比

2026年1月9日 19:31

在写js的时候,很容易因为数据类型没判断好而出错,比如这样的代码:

function calculate(a, b) {
    return a + b;
}
// 我以为是 10 + 20 = 30
calculate(10, 20); // 结果 30 对的 

// 实际上用户输入的是字符串和数字
calculate("10", 20); // 结果为 "1020" 

所以为了避免这种情况的出现,我们还是要去判断好数据类型。

JavaScript中,有几种方式来判断数据类型,以下是常用的方法:

1. typeof 操作符

最常用的类型判断方法,但有一些局限性:

typeof 42;           // "number"
typeof "hello";      // "string"
typeof true;         // "boolean"
typeof undefined;    // "undefined"
typeof null;         // "object" (历史遗留问题)
typeof {};           // "object"
typeof [];           // "object"
typeof function(){}; // "function"
typeof Symbol();     // "symbol"
typeof 42n;          // "bigint"

局限性

  • 无法区分数组、对象和 null
  • 函数返回 function
  • typeof 适合判断基本类型,但遇到对象类型就力不从心了

2. instanceof 操作符

用于检测构造函数的 prototype 属性是否出现在对象的原型链中:

[] instanceof Array;           // true
{} instanceof Object;          // true
new Date() instanceof Date;    // true
function(){} instanceof Function; // true

// 继承关系,数组也是对象
[] instanceof Object;          // true

instanceof 的局限性:

// 基本类型用不了
42 instanceof Number;          // false
"hello" instanceof String;     // false

在跨 iframe 或不同 window 环境下可能失效(因为构造函数不同)


3. Object.prototype .toString.call()

这是最准确、最可靠的方法,能识别所有内置类型!

Object.prototype.toString.call(42);           // "[object Number]"
Object.prototype.toString.call("hello");      // "[object String]"
Object.prototype.toString.call(true);         // "[object Boolean]"
Object.prototype.toString.call(null);         // "[object Null]"
Object.prototype.toString.call(undefined);    // "[object Undefined]"
Object.prototype.toString.call([]);           // "[object Array]"
Object.prototype.toString.call({});           // "[object Object]"
Object.prototype.toString.call(function(){}); // "[object Function]"
Object.prototype.toString.call(Symbol());     // "[object Symbol]"
Object.prototype.toString.call(42n);          // "[object BigInt]"

我们封装一个实用的工具函数:

function getRealType(value) {
    return Object.prototype.toString.call(value)
        .slice(8, -1)          // 截取"[object "和"]"之间的内容
        .toLowerCase();         // 转为小写,更友好
}

console.log(getRealType([]));        // "array"
console.log(getRealType(null));      // "null"
console.log(getRealType({}));        // "object"
console.log(getRealType(new Date())); // "date"

4. 专用方法

对于一些特殊类型,JavaScript提供了专门的判断方法:

判断数组:Array.isArray()

Array.isArray([]);     // true
Array.isArray({});     // false
Array.isArray("123");  // false

判断NaN:Number.isNaN()

// 注意区别!
isNaN("hello");        // true  ← 字符串不是数字,但这样判断容易误解
Number.isNaN("hello"); // false ← 更准确:只有真正的NaN才返回true
Number.isNaN(NaN);     // true

判断有限数字:Number.isFinite()

Number.isFinite(42);     // true
Number.isFinite(Infinity); // false  ← 无穷大不是有限数字
Number.isFinite("42");   // false  ← 字符串不是数字

编写健壮的函数

在实际开发中的应用:

场景1:安全的数字相加

function safeAdd(a, b) {
    // 确保两个参数都是数字类型
    if (typeof a !== 'number' || typeof b !== 'number') {
        throw new Error('参数必须是数字');
    }
    return a + b;
}

safeAdd(1, 2);     // 3
safeAdd(1, "2");   // 报错:参数必须是数字

场景2:处理多种数据类型

function processData(data) {
    // getRealType方法在第3点Object.prototype.toString.call()中有写
    const type = getRealType(data); 
    
    switch(type) {
        case 'array':
            return data.map(item => item * 2);
        case 'object':
            return Object.keys(data).length;
        case 'string':
            return data.toUpperCase();
        case 'number':
            return data * 2;
        default:
            return '不支持的数据类型';
    }
}

console.log(processData([1, 2, 3]));    // [2, 4, 6]
console.log(processData("hello"));      // "HELLO"
console.log(processData({a: 1, b: 2})); // 2

总结:选择合适的方法

场景 推荐方法 示例
判断基本类型 typeof typeof "hello" === "string"
判断数组 Array.isArray() Array.isArray([])
判断自定义对象 instanceof obj instanceof MyClass
精确类型判断 Object.prototype.toString.call() 见上文工具函数
特殊值判断 专用方法 Number.isNaN(), Number.isFinite()

本文首发于公众号:程序员大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!

📌往期精彩

写前端久了,我用 Node.js 给自己造了几个省力小工具

我也是写了很久 TypeScript,才意识到这些写法不对

ThreadLocal 在实际项目中的 6 大用法,原来可以这么简单

重构了20个SpringBoot项目后,总结出这套稳定高效的架构设计

用changeset来管理你的npm包版本

作者 稀饭52
2026年1月9日 18:05

简单介绍下,changeset是一个版本管理和生成更新日志的工具,超级适用于多包仓库,比如monorepo,可以在提交发布时,自动更新所有包的版本号,创建Tag,并且生成更新日志。

一、安装

进入项目之后执行命令,安装changeset,并且初始化

 pnpm add -D @changesets/cli
 pnpm changeset init

执行完之后,在项目中会新增一个.changeset目录,用于存放配置文件和临时的版本变更描述文件

.changeset/
 ├─ config.json
 └─ README.md

二、使用流程

在通用的版本管理流程中,通常会区分为:

  • 预发布版本(如alpha和beta)
  • 正式版本

预发布版本

预发布阶段的两级为alpha和beta,下面是大致区别:

维度 alpha(内测) beta(公测)
代码稳定性 随时可能大改 功能基本锁定,不会有大的调整
测试人群 团队内部 灰度用户
发布频率 每天/每周都能出包 节奏稍慢,某个阶段的版本
版本号示例 1.0.0-alpha.0 ➜ 1.0.0-alpha.1 … 1.0.0-beta.0 ➜ 1.0.0-beta.1 …
退出条件 达到功能完备 → 进入beta 连续几天无阻塞Bug → 发布正式版

相关命令:

pnpm changeset pre enter alpha   # 进入alpha模式,
pnpm changeset version          # 版本变成0.0.1-alpha.0

pnpm changeset pre enter beta   # 进入beta模式
pnpm changeset version          # 版本变成0.0.1-beta.0

# 结束预发布
pnpm changeset pre exit
pnpm changeset version          # 版本变成0.0.1

正式版本

当你完成某个包的开发,准备发版时,执行:

pnpm changeset

如果是多包仓库,终端会出现一个选择框,让你选择改过的包,

  1. 按空格选中你改过的包(有星号就算选中)→ 回车,

  2. 选包的更新级别,会依次出现major和minor,回车到下一步,如果都没选中,就默认为patch,输入本次更新的描述回车

    • patch:修复小bug(1.0.0→1.0.1)
    • minor:添加新功能(1.0.0→1.1.0)
    • major:破坏性的大版本调整,api级别的调整(1.0.0→2.0.0)

单包仓库就直接到了选择更新级别这一步,同样是输入描述,然后回车;

生成版本号,执行:

pnpm changeset version

你会发现.changeset文件夹中刚才生成的md文件都已经不见了,版本号也升好了。

发布

  • 登录npm
npm login
  • 发版
pnpm changeset publish

成功后,会在git创建对应的git tag,终端会给出每个包的版本号和 npm链接。

三、changeset总结

最后总结下,对changeset的整体感觉

优点

  1. 版本语义化,显式标注变更级别
  2. 每一次变更都会新建个文件,方便review
    • 列出受影响的包
    • 变更的级别,patch/minor/major
    • 变更的内容
  3. 自动生成tag、版本号changelog
  4. Monorepo依赖的自动推导,比如修改了a包,b包依赖了a包,那么b包也会更新版本

缺点

  1. 会有额外心智负担,
    • 每次变更都要执行changeset,会增加操作流程(其实我觉得这个不能算)
    • 思考版本类型
    • 流程容易漏
  2. 单包项目有点多余,没完全发挥作用

CSS 特殊符号 / 英文导致换行问题速查表

作者 卡尔特斯
2026年1月9日 18:02

一、最推荐通用写法(90% 场景)

.text {
  word-break: normal;
  overflow-wrap: break-word;
  white-space: normal;
}

适用:

  • 中文 + 英文混排
  • URL / 特殊符号
  • 不希望英文被拆成字母

二、常见需求对应写法

以下场景覆盖:不拆单词 / 允许拆单词 / 特殊符号提前换行 等真实业务需求

1️⃣ 不希望单词被拆开

.text {
  word-break: normal;
  overflow-wrap: normal;
}

2️⃣ 单词太长允许必要时换行(推荐)

.text {
  overflow-wrap: break-word;
  word-break: normal;
}

3️⃣ 特殊符号(- _ / .)导致断行

.text {
  word-break: keep-all;
  overflow-wrap: break-word;
}

4️⃣ 完全不换行(一行显示)

.text {
  white-space: nowrap;
}

5️⃣ 一行显示,超出省略号

.text {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

6️⃣ URL / 长链接优雅断行

.text {
  word-break: normal;
  overflow-wrap: anywhere;
}

7️⃣ 代码 / hash / token 强制断行

.code {
  word-break: break-all;
  font-family: monospace;
}

8️⃣ 明确接受单词被拆开(强制任何位置换行)

.text {
  word-break: break-all;
}

适用:

  • 日志内容
  • 长 hash / token / ID
  • 空间极窄但必须完整展示

⚠️ 英文会被拆成字母,属于主动选择的行为


9️⃣ 特殊符号导致“提前换行”(如 - / _ .

.text {
  word-break: normal;
  overflow-wrap: normal;
}

说明:

  • 禁止在符号处断行
  • 让浏览器只在真正需要时换行

🔟 允许在特殊符号处优先换行(比拆字母更友好)

.text {
  overflow-wrap: anywhere;
  word-break: normal;
}

适用:

  • URL / 路径
  • aaa-bbb-ccc-ddd
  • 希望优先在符号处分行,而不是字母中间

---

## 三、Flex / Table 常见坑

### flex 子项内容被异常换行

```css
.item {
  min-width: 0;
  overflow-wrap: break-word;
}

四、不推荐写法(慎用)

⚠️ 以下写法不是不能用,而是必须明确知道后果

word-break: break-all;

❌ 会把英文拆成字母,仅适合日志 / code 场景


五、属性速记表

属性 作用
word-break 是否允许在单词内部断行
overflow-wrap 单词太长时是否允许换行(⭐推荐)
white-space 是否允许换行

📌 记住一句话:

优先用 overflow-wrap: break-word,避免 word-break: break-all

PowerShell 启动卡顿?内存飙升?原来是 800MB 的历史记录在作祟!

作者 林瞅瞅
2026年1月9日 17:30

PowerShell 启动卡顿?内存飙升?原来是 800MB 的历史记录在作祟!

最近在开发过程中遇到一个非常诡异的问题:PowerShell 一启动,电脑就像被施了定身法一样卡顿。 打开任务管理器一看,好家伙,PowerShell 进程的内存占用像坐火箭一样蹭蹭往上涨,甚至在没有任何手动操作的情况下也是如此。

经过一番侦探式的排查,终于揪出了幕后真凶。今天就把这个排查过程和解决方案分享给大家,如果你的终端也经常卡顿,不妨检查一下这个问题。

🧐 问题现场

现象非常简单粗暴:

  • 双击启动 PowerShell(或者在 VS Code 中打开终端)。
  • 系统明显卡顿,鼠标移动都变得迟缓。
  • 查看任务管理器,PowerShell 进程占用内存极高(甚至达到 GB 级别)。
  • 等待许久后,终端才勉强可以输入命令。

🕵️‍♂️ 抽丝剥茧:排查过程

为了找到原因,我并没有急着重装系统(虽然这是万能大法),而是决定深入系统内部看一看。

1. 进程分析

首先,我使用 Get-Process 命令查看了 PowerShell 进程的状态。果不其然,WorkingSet(工作集内存)数值异常的高。

2. 环境检查

我怀疑是不是某个 Profile 配置文件或者加载的模块有问题。检查了 $PROFILE,发现并没有什么特殊的启动脚本。接着检查已加载的模块,一切似乎都很正常。

3. 关键发现

在排查 PowerShell 的常用模块 PSReadLine 时,我注意到了一个细节。这个模块负责管理我们的命令行历史记录、语法高亮等功能。它会把我们敲过的所有命令都保存在一个文本文件里。

我顺藤摸瓜找到了这个文件,结果让我大吃一惊:

文件路径: %APPDATA%\Microsoft\Windows\PowerShell\PSReadLine\ConsoleHost_history.txt 文件大小: 832 MB

没错,你没看错,一个纯文本的历史记录文件,竟然有 800多兆!这意味着里面可能保存了数百万行的命令历史。

真相大白: PowerShell 在启动时,PSReadLine 模块会尝试加载这个巨大的历史记录文件,以便提供“向上箭头”查找历史命令的功能。这就好比让你一口气背诵一本字典,不卡才怪!

🛠️ 一键解决

既然找到了病灶,治疗方案就非常简单了:让 PowerShell 放弃加载这个文件。

为了保险起见(万一里面有重要的历史命令呢),我没有直接删除它,而是选择了重命名。

操作步骤:

打开你的文件资源管理器,或者直接在命令行执行以下操作:

# 进入 PSReadLine 目录
cd "$env:APPDATA\Microsoft\Windows\PowerShell\PSReadLine"

# 将巨大的历史文件重命名备份
Rename-Item .\ConsoleHost_history.txt -NewName ConsoleHost_history.txt.bak

效果立竿见影: 再次启动 PowerShell,秒开!内存占用恢复到了几十 MB 的正常水平。系统瞬间恢复了丝般顺滑。

💡 避坑指南

问题解决了,但为什么会有这么大的历史文件呢?通常有以下几个原因:

  1. 日积月累: 从来没有清理过,数年的操作记录都在里面。
  2. 自动化脚本: 某些自动化脚本如果在 PowerShell 环境下疯狂循环执行命令,这些命令也会被记录下来。

建议:

  • 定期检查一下 %APPDATA%\Microsoft\Windows\PowerShell\PSReadLine\ 目录下的文件大小。
  • 如果你发现自己不需要保留那么久远的历史记录,可以考虑定期清理。
  • 如果那个 800MB 的备份文件里没有你需要的“传家宝”代码,果断删掉它释放空间吧!

希望这篇文章能帮到遇到同样问题的你。Happy Coding! 🚀

保姆级教程:让 Cursor 编辑器突破地区限制,正常调用大模型(附配置 + 截图)

作者 袋鱼不重
2026年1月9日 16:07
Cursor 的大模型功能目前暂不支持中国地区直接访问,导致很多开发者打开后显示 “模型加载失败”。今天分享亲测有效的配置方法,帮你突破地区限制,丝滑使用 Opus、GPT 等模型~ 一、准备工作 先

第10章 SSE魔改

作者 XiaoYu2002
2026年1月9日 15:57

SSE(Server-Sent Events,服务器推送事件) 是一种基于标准HTTP协议的服务器到客户端的单向数据流技术。它允许服务器在建立初始连接后,通过一个持久的HTTP连接主动、连续地向客户端推送数据更新,而无需客户端重复发起请求。其核心机制是客户端使用 EventSource API 连接到指定端点后,服务器以 text/event-stream 格式持续发送事件流,每个事件由标识类型(event:)、数据(data:)和可选ID组成,客户端通过监听事件类型来实时处理数据,连接中断时还会借助最后接收的ID自动尝试重连。

与需要双向通信的WebSocket相比,SSE的典型优势在于协议轻量、天然支持自动重连与断点续传,且无需额外协议升级。它非常适合服务器主导的实时数据分发场景,如股市行情推送、新闻直播、社交媒体动态、任务进度通知等,浏览器兼容性广泛。但需要注意的是,SSE是单向通道(服务器→客户端),且主流实现中传输格式限于文本(二进制数据需编码),若需双向实时交互则仍需选择WebSocket。

通过以上对SSE的解释,我们可以想到现如今非常经典的例子,AI网站中输出文字的打字机效果(例如DeepSeek),一个字一个字的往外输出,这也是一种SSE。前端给后端发送一次消息,而后端可以给前端一直发消息。

10.1 初始化项目

我们采用Express来模拟SSE,因此需要如下3个步骤来初始化项目:

(1)创建index.html和index.ts文件。

(2)安装express和对应的声明文件,并引入index.ts文件中。

(3)安装cors和对应的声明文件,并引入index.ts文件中。

两个index文件用于展示效果以及编写SSE逻辑。

// 安装express
npm i express
// 安装 CORS(跨域资源共享中间件)
npm install cors
// 安装 Express 的 TypeScript 类型定义
npm install --save-dev @types/express
// 安装 CORS 的 TypeScript 类型定义
npm install --save-dev @types/cors

// 一次性安装所有依赖
npm install express cors @types/express @types/cors

对应的package.json文件中,将type字段设置为module,从而可以使用import引入写法。

// package.json
{
  "type": "module",
  "dependencies": {
    "@types/cors": "^2.8.19",
    "@types/express": "^5.0.6",
    "cors": "^2.8.5",
    "express": "^5.2.1"
  }
}

在index.ts文件使用ES模块语法引入依赖模块express和cors后,创建Express应用实例的app对象,然后在app对象中,通过use()方法挂载必要的全局中间件cors()和express.json(),用于处理跨域资源共享以及解析请求体中格式为JSON的数据。

// index.ts
import express from "express";
import cors from 'cors'

const app = express()
// 处理跨域
app.use(cors())
// 解析请求体中格式为JSON的数据
app.use(express.json())

最后,启动Express服务器,监听3000端口,完成SSE的项目初始化。

app.listen(3000, () => {
    console.log("Server is running on port 3000");
});

10.2 SSE逻辑实现

SSE要求接口必须是一个get请求,因此我们来定义一个get请求。

SSE的核心代码只有一行,将Content-Type设置为text/event-stream。通过将HTTP响应的内容类型明确声明为事件流格式,通知客户端(通常是浏览器)本次连接并非普通的请求-响应交互,而是一个需要保持开启、持续接收服务器推送事件的长连接通道。浏览器接收到这个特定头部后,会启动其内建的SSE处理机制(EventSource API),自动保持连接活性并准备以流式方式解析后续传入的数据。

返回数据的格式一定要遵循data: {实际数据}\n\n的形式。

// index.ts
app.get('/chat', (req, res) => {
  res.setHeader("Content-Type", "text/event-stream"); // 返回SSE
  // 不缓存
  res.setHeader("Cache-Control", "no-cache");
  // 持久化连接
  res.setHeader("Connection", "keep-alive");
  // 定时器,每秒返回一次时间,模拟后端连续地向客户端推送数据更新
  setInterval(() => {
    res.write(`data: ${new Date().toISOString()}\n\n`);
  }, 1000);
});

完成后端SSE的逻辑之后,前端需要如何接受后端传递过来的数据?通过浏览器内置的 EventSource API 来建立连接并接收后端SSE事件流数据就可以。

// index.html
const sse = new EventSource("http://localhost:3000/chat");
sse.onmessage = (event) => {
console.log(event.data);
};

此时启动后端服务器,打开index.html页面的控制台看流式输出时间,即前端可以实时接收后端返回的数据,如图10-1所示。

image-20251219033233783

图10-1 流式输出时间

此时打开网络选项卡,输出效果如图10-2所示。chat接口的EventStream会不断的接收message类型的消息,并展现对应的数据。

image-20251219033452742

图10-2 网络选项卡展示输出效果

10.3 SSE设置post请求

但一般在实际的项目中,是不会使用EventSource的,因为它必须是一个get请求,而在工作中经常使用的是post请求。那面对这种冲突的情况,应该如何去做?

如果我们只是在后端简单的将get请求直接改成post请求,然后重启服务去看效果的话,是无法生效的。

// index.ts
app.post('/chat', (req, res) => {
  //  省略...
});

chat接口修改成post请求如图10-3所示。请求方法依然为GET,网络状态则是404。

image-20251219034035386

图10-3 chat接口修改成post请求

面对后端chat接口修改为post请求不起效果的情况,我们只能在前端去魔改方案,不使用EventSource API来建立连接并接收后端SSE事件流数据。

我们在前端使用fetch()去接收chat接口返回的数据,此时从浏览器的的网络选项卡可以看到接通了,并且从响应选项中会不断打印出时间数据。

fetch("http://localhost:3000/chat", {
  headers: {
    "Content-Type": "application/json",
  },
  method: "POST",
  body: JSON.stringify({ message: "Hello, world!" }),
})
  .then(async response => {
    const reader = response.body.getReader(); // 获取流
    const decoder = new TextDecoder(); // 解码ASCII码值
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      console.log(value) // value是ASCII码
      const text = decoder.decode(value, { stream: true });
      console.log(text);
    }
  })
let a = [1]
let b = a[Symbol.iterator]();
console.log(b.next());
console.log(b.next());

现如今基本上都是通过fetch去魔改实现,将get请求修改成post请求也能传输数据。目前为止,没有更好的解决方法了。

在这段魔改的代码中,我们做了什么?

首先是设置请求头接收的内容类型以及请求方式。当接收到数据时,通过getReader()方法获取流,获取流得到的是一个Promise,因此需要通过await操作符去等待Promise兑现并获取它兑现之后值,通过read()方法去读取数据中的每一个流。

此时每一个流返回的是一个迭代器,迭代器是一个对象,内部有一个next()方法,该方法返回具有两个属性的对象:

(1)value:迭代序列的下一个值。

(2)done:如果已经迭代到序列中的最后一个值,则它为 true。如果 value 和 done 一起出现,则它就是迭代器的返回值。

所以迭代器对象可以通过重复调用next()方法显式地迭代。在while循环中持续调用reader.read()方法,这个方法返回的Promise在每次兑现时都提供一个类似迭代器next()方法的对象——包含value(当前数据块)和done(流是否结束)两个属性。通过循环判断done是否为false,我们可以持续读取Uint8Array格式的数据块,然后使用TextDecoder将其解码为可读文本,实现了对服务器推送数据流的实时逐块处理。

这种显式迭代的核心优势在于按需、增量地处理数据,避免了等待整个响应体完全到达才能开始处理。每次调用reader.read()都明确请求下一个数据块,直到done变为true表示流已结束。这与传统的一次性接收完整响应形成对比,特别适合处理SSE这种持续、长时间的数据流连接,确保了在处理服务器实时推送时内存使用的高效性和响应的即时性。

❌
❌