普通视图

发现新文章,点击刷新页面。
今天 — 2025年7月6日首页

Xcode16报错: SDK does not contain 'libarclite' at the path '/Applicati

作者 90后晨仔
2025年7月5日 11:28

xcode 16运行项目报如下错误:

SDK does not contain 'libarclite' at the path '/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/arc/libarclite_iphonesimulator.a'; try increasing the minimum deployment target

解决方案:

  • 一、错误原因是在这个路径下边缺少一个libarclite_iphonesimulator.a文件,那就command + G打开这个路径看一下,结果发现这个目录下边没有arc这个文件夹。如下图所示:
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/

Snip20250705_1.png

  • 二、点击这里下载arc文件下载下来放到这个路径下边再次运行就不报错了。这里需要注意下,就是必须是command + C + command + V复制粘贴arc这个目录下,不能拖拽,拖拽的是快捷方式不是真实的文件。

Snip20250705_2.png

下载地址我已经放到github了,需要的可以自行下载。

谈一谈iOS线程管理

作者 finger24480
2025年7月4日 21:51

前言

iOS 线程管理是一个老生常谈的话题,也是新人绕不过去的一道难关。用好多线程不容易,下面我简单谈一谈我的体会,希望对屏幕前的你有所帮助。

一、什么时候需要多线程

首先,要知道线程不是越多越好,创建线程和切换线程都有一定的开销,线程使用的不当也容易造成崩溃。那么什么时候需要使用多线程呢?一个主要的衡量标准是这个操作是否耗时,比如读写文件、网络请求、加解密等。特别是IO密集的操作,一定是要多线程的,否则会阻塞当前线程。

其次,线程和队列有着紧密的联系(ios里面特指GCD队列),如果某些操作需要按照一定的时序来执行并且对执行的时间不是那么敏感的话,那么最好就是放在一个串行队列里,比如写缓存。如果这些操作对执行时间敏感,且不是很讲究顺序的话,那么放在并行队列里比较合适,比如从分批下载视频片段(例如dash和hls)。如果是对执行时间敏感,并且又有一定的执行顺序,那么可以考虑NSOperationQueue,或者用dispatch_group、dispatch_semaphore来管理多个线程及其依赖关系。如果对这些都不讲究,那就用不着多线程了。

二、同步还是异步

一般情况下,能用异步还是用异步,除非是需要等待结果返回的才用同步。这主要是因为同步操作会阻塞线程,弄的不好还会导致死锁。编写同步代码的话,主要是用在同步读取某些属性这种场景,比如以下这个方法

- (BOOL)hasSubscribeTopic:(NSString*)topic {

        __block BOOL subscribed = NO;

        dispatch_sync(self.syncQueue, ^{

            subscribed = [self.subscribedTopics containsObject:topic];

        });

        return subscribed;


}

但是这样写有一个问题,就是如果别的方法在syncQueue对应的线程上调用了hasSubscribeTopic这个方法,就会导致死锁,所以正确的方式应该是这样

static const void * kDispatchQueueSpecificKey = &kDispatchQueueSpecificKey;
//init方法中调用
dispatch_queue_set_specific(_syncQueue, kDispatchQueueSpecificKey, (__bridge void *)self, NULL);

- (BOOL)hasSubscribeTopic:(NSString*)topic {

    void* value = dispatch_get_specific(kDispatchQueueSpecificKey);

    if (value == (__bridge void *)(self)) {

        return [self.subscribedTopics containsObject:topic];

    }else{

        __block BOOL subscribed = NO;

        dispatch_sync(self.syncQueue, ^{

            subscribed = [self.subscribedTopics containsObject:topic];

        });

        return subscribed;

    }

}

有些第三方库没有注意这方面,比如SDWebImage的SDImageCache,使用的时候就需要尤其注意

- (void)storeImageDataToDisk:(nullable NSData *)imageData

                      forKey:(nullable NSString *)key {

    if (!imageData || !key) {

        return;

    }

    

    dispatch_sync(self.ioQueue, ^{

        [self _storeImageDataToDisk:imageData forKey:key];

    });

}

三、串行还是并行

这个如前所述,主要看对执行时间的敏感程度和有无顺序要求。一般使用dispatch_create创建的队列以串行为主(swift的dispatchQueue默认就是串行的)。并行队列使用global_queue就可以了,但是有一个需要特别注意的是,不管是dispatch_get_global_queue还是dispatch_create分配的线程都是有上限的,如果超出上限,系统要么就是等待前面的线程执行完成(iOS模拟器),要么就会因为分配资源过多而导致崩溃(iOS真机)。通过下面这段代码,可以测试出系统最多能分配多少个线程,在iphone 15的模拟器上我测试得到的是global_queue能分配64个左右线程,而dispatch_create相对多一点,100多不到200个。

dispatch_queue_t syncQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);

    for (int i=0;i<1000;i++) {

        dispatch_async(dispatch_get_global_queue(0, 0), ^{

            pthread_t tid = pthread_self();

            printf("1 tid=%lu\n",(unsigned long)tid);

            dispatch_sync(syncQueue, ^{

                NSLog(@"2");

            });

            NSLog(@"3");

        });

    }

还有一个问题就是,使用dispatch_get_global_queue创建的线程看似64个也够用了,但如果在这些线程里面使用了同步操作等待串行队列执行完成的话,就会造成阻塞,最终超出线程数量上限而崩溃。比如将上面代码中的NSLog(@"2") 改为一个写缓存之类的耗时操作。

四、线程池

由于线程数量是有上限的,并且线程切换比较耗时,所以对于性能要求较高的程序需要有线程池来管理多线程。iOS是没有系统自带的线程池的,一般都是自己实现(推荐使用dispatch_semaphore或NSOperationQueue,具体实现可以参考java的executor相关代码。需要注意的是,什么时候切换到线程池是有讲究的,一般规则是逻辑层的代码尽早切换到线程池,特别是有些逻辑可能会创建多个线程的时候,比如多个图片的下载和缓存。

五、线程的同步

线程的同步也是一个比较经典的话题了,我在这里就不想赘述了,大家可以在网上随便搜一搜,我只提一下,一般线程间同步就几种方式:

  1. 加锁
  2. 条件变量 3.信号量
  3. 串行队列+同步读异步写
  4. 内存屏障
  5. CAS原子操作

个人比较推荐的是加锁(性能要求没那么高)和条件变量(性能要求较高,逻辑相对简单的场景)。串行队列如果管理不当可能会创建多个线程,因此不做推荐。内存屏障和CAS原子操作比较底层,使用起来也没那么方便,除非是对时序和性能要求极高。

六、线程间通信

除了使用C语言的pthrea_create和pthread_join来进行线程创建和销毁时的通信外,iOS还可以使用NSMachPort和NSThread的performSelectorOnThread来做线程间通信。前者跟runloop结合,在runloop的生命周期内注册一个特定的事件来定期检查并执行,后者类似于pthread_create,在创建线程时传递一个参数。 除了这种系统提供的方法外,还有一种通用的方式,就是在线程内维护一个事件队列,外部需要给这个线程发消息时,就往队列插入一个事件,然后该线程在一个循环内定时去取事件执行。有点类似runloop的感觉,如果要跨平台的话可以考虑使用libevent(一般用来做网络通信)来实现。

结语

不管是在iOS还是其他的平台上,多线程管理都是一个复杂的话题。要用好多线程,除了要掌握一些常见的方法外,最主要还是平时编程的时候多思考,什么时候应该用多线程,以及怎么样做好线程同步和队列的选择,在追求高性能的同时保证安全性。

美团即时零售日订单已突破1.2亿,餐饮订单占比过亿

2025年7月5日 23:34
据美团内网公布信息显示,截至22时54分,美团即时零售当日订单已经突破了1.2亿单,其中,餐饮订单已超过1亿单。值得注意的是,就在当晚20时45分,美团内网曾显示即时零售日订单突破了1亿。这也意味着,在2个小时的时间内,美团已产生了超过2000万笔订单。考虑到周末夜宵时段仍然是外卖高峰期,这一数据目前仍在增长中。7月5日是暑期消费旺季正式开启后的第一个周末,也是外卖消费的传统促销季。公开报道显示,去年夏天美团的订单峰值超过了9000万单。
昨天 — 2025年7月5日首页

美团即时零售日订单突破1亿,较去年峰值提前33天

2025年7月5日 21:55
7月5日晚,美团向内部员工推送的战报显示,截至20时45分,美团即时零售日订单量突破1亿单。今年美团订单峰值比去年提前了33天,去年8月7日美团日订单超过了9000万。据媒体报道,今年6月以来美团的日订单连续保持在9000万以上。这一峰值是在外卖迎来激烈竞争的局面下取得的。在这次的内部战报中,美团多次提��了“用户体验”,强调“继续为用户创造最佳体验”。

顺德农商行撤回IPO申请

2025年7月5日 18:20
7月4日,深交所发行上市审核信息公开网站显示,因广东顺德农村商业银行股份有限公司、保荐人撤回发行上市申请,深交所决定终止其发行上市审核。截至目前,处于A股上市“候场区”的银行仅剩5家,分别是湖州银行、江苏昆山农商银行、湖北银行、广东南海农商银行、东莞银行,其中仅湖州银行处于“已问询”状态。(界面)

盘古团队最新声明:严格遵循开源要求

2025年7月5日 18:19
36氪获悉,7月5日下午,华为诺亚方舟实验室在官方平台发布最新声明称,盘古Pro MoE开源模型是基于昇腾硬件平台开发、训练的基础大模型,并非基于其他厂商模型增量训练而来。上述团队表示,“盘古Pro MoE开源模型部分基础组件的代码实现参考了业界开源实践,涉及其他开源大模型的部分开源代码。我们严格遵循开源许可证的要求,在开源代码文件中清晰标注开源代码的版权声明。这不仅是开源社区的通行做法,也符合业界倡导的开源协作精神。”

用Sass循环实现炫彩文字跑马灯效果

作者 小old弟
2025年7月5日 18:03

1.gif

今天我们来学习一个非常酷炫的文字动画效果——通过Sass循环和CSS动画实现的彩色跑马灯。这个效果看起来复杂,但实际上原理非常简单,让我们一步步拆解。

效果预览

我们有一组字母"C O L O R F U L",每个字母会依次从浅色变为亮粉色,形成波浪式的颜色变化效果,就像跑马灯一样流动。

HTML结构

首先看HTML部分,非常简单:

<div>
  <span>C</span>
  <span>O</span>
  <span>L</span>
  <span>O</span>
  <span>R</span>
  <span>F</span>
  <span>U</span>
  <span>L</span>
</div>

每个字母都被包裹在<span>标签中,这样我们可以单独控制每个字母的样式。

基础样式

body {
  font-family: 'Roboto Mono';
}

span {
  color: #faebd7; /* 初始颜色 - 米白色 */
  animation: colorChange 1s infinite alternate;
}

我们设置了统一的字体,并为所有span元素定义了初始颜色和动画效果。

关键动画定义

@keyframes colorChange {
  to {
    color: #ff0266; /* 目标颜色 - 亮粉色 */
  }
}

这个动画非常简单,只是让颜色从初始色变为目标色。但有几个关键点:

  1. infinite表示动画无限循环
  2. alternate让动画在完成一次后反向播放,形成平滑的往返效果

没有alternate时,动画会突然跳回初始状态,造成闪烁效果。加上后,动画会平滑地往返变化。

Sass循环 - 实现延迟的关键

@for $i from 1 through 8 {
  span:nth-child(#{$i}) {
    animation-delay: ($i - 1) * 0.2s;
  }
}

这是整个效果最精彩的部分!我们使用Sass的@for循环为每个字母设置不同的动画延迟:

  1. $i是循环变量,从1到8(因为我们有8个字母)
  2. nth-child(#{$i})选择第i个span元素
  3. ($i - 1) * 0.2s计算延迟时间:
    • 第一个字母延迟0s(立即开始)
    • 第二个字母延迟0.2s
    • 第三个字母延迟0.4s
    • 以此类推...

这样每个字母的动画都会比前一个晚0.2秒开始,形成连续的波浪效果。

为什么这样设计?

  1. 简化代码:手动为8个字母写延迟会很冗长,Sass循环让我们用几行代码就搞定
  2. 易于调整:只需修改循环中的数字或延迟时间,就能轻松改变效果
  3. 可扩展性:字母数量变化时,只需调整循环次数,无需重写大量代码

实际应用场景

这种技术非常适合:

  • 网站标题或标语的特殊效果
  • 按钮悬停时的动态反馈
  • 引导用户注意力的提示元素
  • 任何需要增加视觉趣味性的文字元素

总结

通过这个例子,我们学到了:

  1. CSS动画的基本创建方法
  2. alternate属性如何让动画更平滑
  3. Sass循环如何大幅简化重复性样式代码
  4. 动画延迟如何创造序列效果

试着调整动画时间、延迟间隔或颜色值,看看能创造出什么不同的效果吧!

1.gif

Playwright 中特定的 Fixtures

2025年7月5日 17:33

Playwright 为 pytest 提供了一组专门的 fixtures,用于简化浏览器自动化测试的编写。这些 fixtures 管理浏览器、上下文和页面的生命周期,让测试更加简洁高效。

常用 Playwright Fixtures 列表

以下是 Playwright 最常用的 pytest fixtures:

  1. playwright - Playwright 实例
  2. browser - 浏览器实例
  3. browser_name - 当前浏览器名称
  4. browser_channel - 浏览器渠道
  5. context - 浏览器上下文
  6. page - 页面实例
  7. is_webkit, is_firefox, is_chromium - 浏览器类型检查

安装和配置

首先需要安装必要的包:

pip install pytest-playwright
playwright install

各 Fixture 详解及示例

1. playwright fixture

作用:提供 Playwright 的初始实例

示例

def test_with_playwright(playwright):
    chromium = playwright.chromium
    browser = chromium.launch()
    # 测试代码...
    browser.close()

原理:管理 Playwright 的启动和清理,是其他浏览器相关 fixtures 的基础

2. browser fixture

作用:提供已启动的浏览器实例

示例

def test_with_browser(browser):
    page = browser.new_page()
    page.goto("https://example.com")
    assert "Example" in page.title()

原理

  • 测试开始时自动启动浏览器
  • 测试结束后自动关闭浏览器
  • 默认使用 Chromium,可通过命令行参数更改

3. browser_name fixture

作用:获取当前测试使用的浏览器名称

示例

def test_browser_name(browser, browser_name):
    print(f"Running test on {browser_name}")
    assert browser_name in ["chromium", "firefox", "webkit"]

4. context fixture

作用:提供浏览器上下文实例

示例

def test_with_context(context):
    page = context.new_page()
    page.goto("https://example.com")
    assert page.url == "https://example.com/"

原理

  • 浏览器上下文相当于一个独立的浏览器会话
  • 隔离 cookies 和本地存储
  • 测试结束后自动关闭

5. page fixture (最常用)

作用:提供已创建的页面实例

示例

def test_example(page):
    page.goto("https://example.com")
    heading = page.get_by_role("heading", name="Example Domain")
    assert heading.is_visible()

原理

  • 每个测试自动创建新页面
  • 测试结束后自动关闭页面
  • 包含所有常用的页面操作方法

高级配置示例

自定义浏览器选项

conftest.py 中:

import pytest

@pytest.fixture(scope="session")
def browser_context_args(browser_context_args):
    return {
        **browser_context_args,
        "viewport": {"width": 1920, "height": 1080},
        "ignore_https_errors": True
    }

多浏览器测试

运行不同浏览器的测试:

pytest --browser chromium --browser firefox --browser webkit

对应的测试文件:

def test_multi_browser(page, browser_name):
    page.goto("https://example.com")
    if browser_name == "firefox":
        # Firefox 特定的断言
        pass
    elif browser_name == "webkit":
        # WebKit 特定的断言
        pass

Fixture 作用域控制

Playwright fixtures 默认作用域是"function"(每个测试函数一个实例),但可以调整:

@pytest.fixture(scope="module")
def shared_page(page):
    # 这个 page 将在整个模块中共享
    yield page

运行原理

  1. 初始化阶段

    • playwright fixture 初始化 Playwright 实例
    • browser fixture 使用 Playwright 实例启动浏览器
    • context fixture 创建浏览器上下文
    • page fixture 在上下文中创建新页面
  2. 测试执行阶段

    • 测试函数接收配置好的 page 或其他 fixtures
    • 执行测试逻辑
  3. 清理阶段

    • 按照创建的反向顺序自动清理资源
    • 先关闭 page,然后 context,最后 browser

最佳实践

  1. 大多数测试只需使用 page fixture
  2. 需要特殊配置时才使用底层 fixtures
  3. 使用 browser_context_args 定制默认上下文设置
  4. 对于需要登录的测试,可以创建自定义 fixture 重用认证状态
@pytest.fixture
def authenticated_page(page):
    page.goto("/login")
    page.fill("#username", "testuser")
    page.fill("#password", "password")
    page.click("text=Sign in")
    # 确保登录成功
    assert page.url == "/dashboard"
    return page

def test_dashboard(authenticated_page):
    authenticated_page.goto("/dashboard")
    # 测试代码...

safari 浏览器的一次问题定位

作者 kingsley
2025年7月5日 17:27

背景

批量上传资源型文件, 需要先获取对应每份文件的 ID 标识进而上传。 简而言之, 上传链路为: 并行获取标识 -- 并行触发上传。

问题

chrome 浏览器上链路正常, safari 浏览器并行获取标识请求被缓存后只会进行单次触发。

问题定位

  1. 针对触发链路进行断点 debug , 不得不说, safari 的 debug 机制实在是不友好。 发现断点确实命中多次, 说明代码链路正常执行。
  2. 查看接口请求

ff72b0f97f5eda5b5fc84ddc333a0bbe.jpg

image.png

查看标头相关字段, 并未发现缓存机制相关字段 (Cache-Control), 结合 www.raymondcamden.com/2015/07/16/… 猜测是请求被 safari 浏览器缓存。

问题解决

  1. 给该请求添加随机数 ID (nanoid 生成)标识不同请求 例如
// 在 URL 后添加时间戳参数
const url = `/api/data?t=${Date.now()}`;

// 或使用随机数
const url = `/api/data?r=${Math.random().toString(36).substring(2)}`;

2. 设置请求的缓存方式

Cache-Control: no-cache, no-store, must-revalidate
Pragma: no-cache
Expires: 0

Cache-Control 的一些用法说明

维度 请求头(Request Headers) 响应头(Response Headers)
控制方 客户端(浏览器) 服务器
作用对象 当前请求的处理方式 返回的资源的缓存策略
核心目的 声明浏览器对缓存的偏好(如是否强制验证) 规定资源如何被缓存(如有效期、存储位置)
优先级 可临时覆盖响应头的缓存策略(如强制刷新) 主导缓存行为,是缓存规则的基础

请求头中的 Cache-Control

浏览器在发送请求时添加,用于 声明本次请求的缓存处理意愿
常见指令及作用

指令 含义 典型场景
no-cache 跳过强缓存,直接向服务器验证缓存有效性(触发协商缓存)。 强制刷新(Ctrl+F5
no-store 完全忽略缓存:不读取缓存,且要求服务器返回原始数据(不缓存响应)。 敏感数据请求(如银行页面)
max-age=0 等效于 no-cache,强制验证缓存有效性。 普通刷新(F5
max-stale=300 允许使用过期缓存,但过期时间不超过 300 秒。 弱网环境下容忍旧缓存

响应头中的 Cache-Control

服务器在返回响应时添加,用于 规定资源如何被缓存
常见指令及作用

指令 含义 典型场景
max-age=3600 资源有效期 3600 秒(强缓存),期间浏览器直接使用缓存。 静态资源(JS/CSS/图片)
no-cache 可缓存,但每次使用前必须向服务器验证(强制协商缓存)。 频繁变化的资源(如实时数据)
no-store 禁止任何缓存(浏览器和代理均不可存储)。 敏感数据(如登录页)
public 允许浏览器和代理服务器(CDN)缓存资源。 公共资源(如图片库)
private 仅允许浏览器缓存,禁止代理服务器缓存。 用户私有数据(如个人中心)
immutable 资源永不变,浏览器直接使用缓存而不验证(适用于哈希命名的静态文件)。 文件名带哈希的静态资源

Promise 基础使用

作者 code_YuJun
2025年7月5日 17:26

Promise 是什么?

  1. 抽象表达:
    1. Promise 是一门新的技术(ES6 规范)
    2. Promise 是 JS 中进行异步编程的新解决方案
      备注:旧方案是单纯使用回调函数
  2. 具体表达:
    1. 从语法上来说: Promise 是一个构造函数
    2. 从功能上来说: promise 对象用来封装一个异步操作并可以获取其成功/失败的结果值

promise 的状态改变

  1. pending 变为 resolved/fullfilled
  2. pending 变为 rejected
    说明: 只有这 2 种, 且一个 promise 对象只能改变一次,无论变为成功还是失败, 都会有一个结果数据,成功的结果数据一般称为 value, 失败的结果数据一般称为 reason

promise 的基本流程

image.png

promise 的基本使用

        const btn = document.querySelector('#btn');
        //绑定单击事件
        btn.addEventListener('click', function(){
            //Promise 形式实现
            // resolve 解决  函数类型的数据
            // reject  拒绝  函数类型的数据
            const p = new Promise((resolve, reject) => {
                setTimeout(() => {
                    //30%  1-100  1 2 30
                    //获取从1 - 100的一个随机数
                    let n = rand(1, 100);
                    //判断
                    if(n <= 30){
                        resolve(n); // 将 promise 对象的状态设置为 『成功』
                    }else{
                        reject(n); // 将 promise 对象的状态设置为 『失败』
                    }
                }, 1000);
            });
            //调用 then 方法
            // value 值
            // reason 理由
            p.then((value) => {
                alert('恭喜恭喜, 奖品为 10万 RMB 劳斯莱斯优惠券, 您的中奖数字为 ' + value);
            }, (reason) => {
                alert('再接再厉, 您的号码为 ' + reason);
            });
        });

如何使用 Promise

  1. Promise 构造函数: Promise (excutor) {}
    1. executor 函数: 执行器 (resolve, reject) => {}
    2. resolve 函数: 内部定义成功时我们调用的函数 value => {}
    3. reject 函数: 内部定义失败时我们调用的函数 reason => {}

说明: executor 会在 Promise 内部立即同步调用,异步操作在执行器中执行

        let p = new Promise((resolve, reject) => {
            console.log(111);
        });
        console.log(222); // 111 222
  1. Promise.prototype.then 方法: (onResolved, onRejected) => {}

    1. onResolved 函数: 成功的回调函数 (value) => {}

    2. onRejected 函数: 失败的回调函数 (reason) => {}

说明: 指定用于得到成功 value 的成功回调和用于得到失败 reason 的失败回调,返回一个新的 promise 对象

        let p = new Promise((resolve, reject) => {
            // resolve(111);
            reject(222)
        });
        p.then((value) => {
            console.log(value) // 111
        },(reason) => {
            console.log(reason) // 222
        })
  1. Promise.prototype.catch 方法: (onRejected) => {}
    1. onRejected 函数: 失败的回调函数 (reason) => {} 说明: then()的语法糖, 相当于: then(undefined, onRejected)
        let p = new Promise((resolve, reject) => {
            reject(222)
        });
        p.catch((reason)=>{
            console.log(reason) // 222
        })
  1. Promise.resolve 方法: (value) => {}

    1. value: 成功的数据或 promise 对象,说明: 返回一个成功/失败的 promise 对象
        //如果传入的参数为 非Promise类型的对象, 则返回的结果为成功promise对象
        let p1 = Promise.resolve(521);
        p1.then((res) => {
            console.log(res) // 521
        })
        
        //如果传入的参数为 Promise 对象, 则参数的结果决定了 resolve 的结果
        let p2 = Promise.resolve(new Promise((resolve, reject) => {
            resolve('OK');
        }));
        console.log(p2);
        p2.then(reason => {
            console.log(reason); // OK
        })
  1. Promise.reject 方法: (reason) => {}
    1. reason: 失败的原因,说明: 返回一个失败的 promise 对象
        let p = Promise.reject(521);
        console.log(p) // Promise {<rejected>: 521}
        let p3 = Promise.reject(new Promise((resolve, reject) => {
            resolve('OK');
        }));
        console.log(p3);

image.png

  1. Promise.all 方法: (promises) => {}

    1. promises: 包含 n 个 promise 的数组

说明: 返回一个新的 promise, 只有所有的 promise 都成功才成功, 只要有一个失败了就直接失败

        let p1 = new Promise((resolve, reject) => {
            resolve('OK');
        })
        let p2 = Promise.resolve('Success');
        let p3 = Promise.resolve('Oh Yeah');
        const result = Promise.all([p1, p2, p3]);
        console.log(result);

image.png

        let p1 = new Promise((resolve, reject) => {
            resolve('OK');
        })
        let p2 = Promise.reject('Error');
        let p3 = Promise.resolve('Oh Yeah');
        const result = Promise.all([p1, p2, p3]);
        console.log(result);

image.png

  1. Promise.race 方法: (promises) => {}
    1. promises: 包含 n 个 promise 的数组 说明: 返回一个新的 promise, 第一个完成的 promise 的结果状态就是最终的结果状态
        let p1 = new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve('OK');
            }, 1000);
        })
        let p2 = Promise.resolve('Success');
        let p3 = Promise.resolve('Oh Yeah');

        //调用
        const result = Promise.race([p1, p2, p3]);
        console.log(result);

image.png

promise 的几个关键问题

  1. 如何改变 promise 的状态?
    1. resolve(value): 如果当前是 pending 就会变为 resolved
    2. reject(reason): 如果当前是 pending 就会变为 rejected
    3. 抛出异常: 如果当前是 pending 就会变为 rejected
        let p = new Promise((resolve, reject) => {
            //1. resolve 函数
            // resolve('ok'); // pending   => fulfilled (resolved)
            //2. reject 函数
            // reject("error");// pending  =>  rejected 
            //3. 抛出错误
            // throw '出问题了';
        });
  1. 一个 promise 指定多个成功/失败回调函数, 都会调用吗?

    当 promise 改变为对应状态时都会调用

        let p = new Promise((resolve, reject) => {
            resolve('OK');
        });
        ///指定回调 - 1
        p.then(value => {
            console.log(value);
        });
        //指定回调 - 2
        p.then(value => {
            alert(value);
        });
  1. 改变 promise 状态和指定回调函数谁先谁后?

    1. 都有可能, 正常情况下是先指定回调再改变状态, 但也可以先改状态再指定回调
    2. 如何先改状态再指定回调?
      ① 在执行器中直接调用 resolve()/reject()
      ② 延迟更长时间才调用 then()
    3. 什么时候才能得到数据?
      ① 如果先指定的回调, 那当状态发生改变时, 回调函数就会调用, 得到数据
      ② 如果先改变的状态, 那当指定回调时, 回调函数就会调用, 得到数据
  2. promise.then()返回的新 promise 的结果状态由什么决定?

    1. 简单表达: 由 then()指定的回调函数执行的结果决定
    2. 详细表达:
      ① 如果抛出异常, 新 promise 变为 rejected, reason 为抛出的异常
      ② 如果返回的是非 promise 的任意值, 新 promise 变为 resolved, value 为返回的值
      ③ 如果返回的是另一个新 promise, 此 promise 的结果就会成为新 promise 的结果\
        let p = new Promise((resolve, reject) => {
            resolve('ok');
        });
        //执行 then 方法
        let result = p.then(value => {
            // console.log(value);
            //1. 抛出错误
            // throw '出了问题';
            //2. 返回结果是非 Promise 类型的对象
            // return 521;
            //3. 返回结果是 Promise 对象
            // return new Promise((resolve, reject) => {
            //     // resolve('success');
            //     reject('error');
            // });
        }, reason => {
            console.warn(reason);
        });
        console.log(result);
  1. promise 如何串连多个操作任务?
    1. promise 的 then()返回一个新的 promise, 可以开成 then()的链式调用
    2. 通过 then 的链式调用串连多个同步/异步任务
        let p = new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve('OK');
            }, 1000);
        });
        p.then(value => {
            return new Promise((resolve, reject) => {
                resolve("success");
            });
        }).then(value => {
            console.log(value); // success
        }).then(value => {
            console.log(value); // undefined
        })
  1. promise 异常传透?

    1. 当使用 promise 的 then 链式调用时, 可以在最后指定失败的回调,
    2. 前面任何操作出了异常, 都会传到最后失败的回调中处理
        let p = new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve('OK');
            }, 1000);
        });
        p.then(value => {
            throw '失败啦!';
        }).then(value => {
            console.log(222);
        }).then(value => {
            console.log(333);
        }).catch(reason => {
            console.warn(reason); // 失败啦
        });
  1. 中断 promise 链?
    1. 当使用 promise 的 then 链式调用时, 在中间中断, 不再调用后面的回调函数
    2. 办法: 在回调函数中返回一个 pendding 状态的 promise 对象
        let p = new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve('OK');
            }, 1000);
        });
        p.then(value => {
            console.log(111);
            //有且只有一个方式
            return new Promise(() => {});
        }).then(value => {
            console.log(222);
        }).then(value => {
            console.log(333);
        }).catch(reason => {
            console.warn(reason);
        });

async 函数

  1. 函数的返回值是 promise 对象
  2. promise 对象的结果是由 async 函数执行的返回值决定。
  • 如果返回值是一个非 Promise 类型的数据,则结果是一个成功的 Promise
async function fun() {
    return 111
}
let result = fun()
console.log(result) // Promise {<fulfilled>: 111}
  • 如果返回值是一个 Promise 类型的数据,则结果由返回的 Promise 决定
async function fun() {
    return new Promise((resolve) => {resolve(123)})
}
let result = fun()
console.log(result)

image.png

async function fun() {
    return new Promise((resolve,reject) => {reject(123)})
}
let result = fun()
console.log(result) 

image.png

  • 抛出异常
async function fun() {
    throw '123'
}
let result = fun()
console.log(result) 

image.png

await 表达式

  1. await 右侧的表达式一般为 promise 对象,但也可以是其他值
  2. 如果表达式是 promise 对象,await 返回的是 promise 成功的值
  3. 如果表达式是其他值,直接将此值作为 await 的返回值
  4. 如果 awiat 的 promise 失败了,就会抛出异常,要通过 try...catch..捕获处理
async function fun(){
    let p = new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(111)
        }, 1000)
    })
    let result = await p
    console.log(result)
} 
fun() // 1秒后打印 111

async/await 结合

案例: 想要读取三个文件的内容,并组合在一起 image.png

image.png

这种写法好处是没有回调函数了。

别再只用 base64!HTML5 的 Blob 才是二进制处理的王者,面试常考

作者 然我
2025年7月5日 17:16

前端开发中,90%的人都不知道:掌握Blob对象处理二进制数据的能力,是突破技术瓶颈的关键!

你是不是还在只用 base64 处理图片?面试官问 “如何高效处理大文件上传” 时一脸懵?其实 HTML5 的 Blob 对象才是二进制处理的 “隐藏王者”—— 它能轻松搞定图片预览、大文件分片、PDF 生成等高级操作,也是前端面试的高频考点。

从一个面试题说起:为什么 base64 不适合大图片?🤦‍♀️

先看一个场景:前端需要预览用户上传的图片。很多人第一反应是转成 base64:

// 传统方式:图片转base64
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
  img.src = reader.result; // 类似 "data:image/png;base64,/9j/4AAQSkZJRg..."
};

但面试官追问:“如果用户上传 10MB 的图片,用 base64 有什么问题?”

答案藏在 base64 的原理里:base64 是把二进制数据转成字符串,会让数据体积增加 30% 。10MB 的图片转成 base64 后变成 13MB,不仅浪费带宽,还会让 JS 处理变慢(字符串操作比二进制操作低效)。

image.png

这时候,Blob 对象就要登场了 —— 它能直接操作二进制数据,处理大文件更高效,也是前端处理二进制的 “标准答案”。

Blob 是什么?二进制世界的 “万能容器”😍

Blob 的全称是Binary Large Object(二进制大对象) ,你可以把它理解成一个 “装二进制数据的文件袋”:

  • 里面可以装图片、视频、PDF 等任何二进制数据;
  • 有明确的 “文件类型”(比如image/png);
  • 支持切割、合并等操作,像 “拆快递盒” 一样灵活处理二进制。

对比 base64 和 Blob 的核心区别:

特性 base64 Blob
本质 字符串(二进制的文本编码) 二进制数据对象
体积 比原文件大 30% 和原文件体积相同
操作效率 低(字符串处理慢) 高(直接操作二进制)
适用场景 小图标、简单数据 大文件、复杂二进制处理

简单说:小数据用 base64 方便,大数据用 Blob 高效

实战:Base64图片转Blob全流程🤔

我们用一个 “图片处理” 案例,手把手教你用 Blob:把 base64 格式的图片转成 Blob 对象,再显示到页面上(这是面试常考的转换逻辑)

步骤 1:准备一个 base64 图片(模拟后端返回或本地存储的图片)

假设我们有一张 PNG 图片的 base64 编码(很长,这里简化表示):

const base64Str = "data:image/png;base64,UklGRiAHAABXRUJQVlA4IBQHAACwHACdASpQAFAAP...";

步骤 2:从 base64 中提取纯二进制字符串

base64 编码的格式是data:类型;base64,实际编码,第一步要去掉前缀,只保留后面的二进制编码部分:

// 去掉base64前缀("data:image/png;base64,")
const pureBase64 = base64Str.split(",")[1]; // 结果:"UklGRiAHAABXRUJQVlA4IBQHAACwHACd..."

步骤 3:用 atob () 解码 base64,得到二进制字符串

atob()是浏览器内置的解码函数,能把 base64 字符串转成二进制字符串(注意:这里的 “字符串” 是每个字符对应一个字节的二进制数据):

// 解码base64,得到二进制字符串
const binaryStr = atob(pureBase64);

打印出来将会是这样 image.png

步骤 4:把二进制字符串转成 Uint8Array(二进制数组)

二进制字符串不方便直接操作,需要转成TypedArray(类型化数组) —— 这里用Uint8Array(8 位无符号整数数组),每个元素代表一个字节的二进制数据:

// 创建一个和二进制字符串长度相同的Uint8Array
const uint8Array = new Uint8Array(binaryStr.length);

// 逐个字符转成对应的二进制数值(0-255)
for (let i = 0; i < binaryStr.length; i++) {
  uint8Array[i] = binaryStr.charCodeAt(i);
}

image.png

步骤 5:用 Uint8Array 创建 Blob 对象

有了二进制数组,就能创建 Blob 了,指定正确的 MIME 类型(比如image/png):

// 创建Blob对象(二进制数据+类型)
const blob = new Blob([uint8Array], { type: "image/png" });

image.png

步骤 6:用 URL.createObjectURL () 生成 Blob 的访问地址

Blob 对象不能直接当src用,需要通过URL.createObjectURL()生成一个临时 URL(类似blob:http://localhost/xxx),浏览器能直接识别这个 URL:

// 生成Blob的临时URL
const blobUrl = URL.createObjectURL(blob);

// 显示图片(和用base64的方式一样简单,但更高效)
const img = document.getElementById("myImage");
img.src = blobUrl;

// 注意:用完后要释放URL,避免内存泄漏
img.onload = () => {
  URL.revokeObjectURL(blobUrl);
};

image.png

image.png 运行代码后,图片正常显示,但背后用的是 Blob 而非 base64—— 处理大图片时,你会明显感觉到加载更快、页面更流畅。

面试必背:Blob 的核心 API 和注意事项

1. 核心 API

  • new Blob(blobParts, options):创建 Blob 对象,blobParts是二进制数据数组(如[uint8Array, "字符串", anotherBlob]),options指定type(MIME 类型);
  • URL.createObjectURL(blob):生成 Blob 的临时 URL;
  • URL.revokeObjectURL(url):释放临时 URL,避免内存泄漏;
  • blob.slice(start, end, contentType):切割 Blob,返回新的 Blob(类似字符串的slice)。

2. 注意事项

  • 内存管理URL.createObjectURL()会占用内存,不用时必须用revokeObjectURL释放;
  • 兼容性:Blob 是 HTML5 标准 API,所有现代浏览器都支持(IE10+),不用担心兼容性问题;
  • 与 File 的关系File对象是Blob的子类,所以File能使用所有 Blob 的方法(比如slice)—— 这也是为什么我们能直接切割用户上传的File对象。

最后建议:Blob作为HTML5的底层能力,单独使用不足以成为亮点。但当你能结合文件处理、性能优化、音视频等场景展示其价值时,它将成为你技术深度的完美证明。记住:

"掌握Blob,就掌握了现代Web应用处理二进制数据的钥匙!"

当前端轮播图遇上Electron: 变身一款丝滑的 图片查看器

2025年7月5日 17:12

最近收集了不少优质的图片,这些图片数量太多;为了能够快速浏览,自动播放;不用一个个的点击,上次用html+JavaScript+css做了一个轮播图!

但是上次的轮播图还需要导入本地图片路径才能轮播;这次在上次的基础上,使用Electron包装下,竟然实现了一个win应用程序-图片查看器;直接在电脑上用该程序打开图片,就可以看图了!这里赶紧分享下!

项目简介

这是一款基于 Electron 打造的桌面图片查看器,支持 Windows 右键“用本应用打开”任意图片,自动浏览同目录下所有图片,支持上一张/下一张切换,还能一键轮播,以及调节轮播速度,体验如同刷短视频般顺滑!

动画17.gif

主要功能

  • 右键即用:在资源管理器中右键图片,选择该“图片查看器”打开,无需拖拽、无需繁琐操作。
  • 自动识别同目录图片:自动识别并加载图片所在文件夹下所有图片。
  • 支持多种图片格式:支持 jpg、png、gif、bmp、webp 等主流格式。
  • 快捷切换:支持上一张、下一张切换,底部实时显示“当前/总数”,让你不再迷失在图片海洋。
  • 轮播模式:一键自动播放,支持正序/倒序,速度随心调节,解放双手,享受视觉盛宴。
  • 美观 UI:现代化界面,暗色渐变背景,圆角卡片,操作按钮大而顺手,观感舒适。
  • 跨平台(理论上):主力支持 Windows,macOS 也能用(但右键集成需手动设置)。

技术亮点

  • Electron:桌面端跨平台神器,前端技术栈一把梭。
  • Node.js + fs:主进程自动遍历图片目录,图片列表一网打尽。
  • preload.js 安全桥:前后端通信,既安全又灵活。
  • 原生 IPC 通信:图片切换、窗口关闭等操作,主渲染进程无缝协作。
  • 自定义打包:electron-builder 配置文件关联,打包即集成右键“打开方式”。

快速上手

  1. 克隆或下载本项目,进入项目目录。
  2. 安装依赖:
    npm install
    
  3. 开发调试:
    npm start
    
  4. 打包发布:
    npm run dist
    
    生成的 exe 安装包在 dist/ 目录。
  5. 安装后,右键任意图片,选择“用 图片查看器 打开”,即可享受丝滑体验。

目录结构

图片查看器/
├── main.js         # Electron 主进程,负责窗口和图片列表逻辑
├── preload.js      # 预加载脚本,安全桥梁
├── carousel.html   # 轮播图主界面
├── package.json    # 项目配置与打包脚本
├── README.md       # 你正在看的这份说明
└── ...             # 其它资源

适用场景

  • 看壁纸、美图、批量预览摄影作品
  • 批量快速预览本地图片

最后

我把完整代码放到网盘了;里面还有往期的小demo;喜欢的可以下载体验下面是获取方法

Snipaste_2025-05-27_12-32-34.pngSnipaste_2025-07-05_17-08-43.png ---

前端必学:从盒模型到定位,一篇搞定页面布局核心 🧩

作者 呆呆的心
2025年7月5日 17:03

作为前端开发者,每天都在和 "盒子" 打交道,但你真的吃透盒模型和定位规则了吗?本文结合实例带你从基础到进阶,掌握页面布局的核心逻辑 ✨

一、文档流:页面布局的 "自然法则" 🌊

什么是文档流?

文档流就像自然界的水流,元素会按照特定规则有序排列:

  • 块级元素(如 div、p):从上到下垂直排列,独占一行

  • 行内元素(如 span、a):从左到右水平排列,空间不足时自动换行

  • 层级关系:HTML 标签从外到内嵌套,形成天然的层级结构(父盒子包含子盒子)

👉 关键:doctype声明决定了浏览器是否使用标准模式解析文档流,缺失时可能触发怪异模式,导致布局错乱!

二、盒模型:每个元素都是一个 "盒子" 📦

盒子的 4 大组成部分

每个 HTML 元素都可以看作一个盒子,由 4 部分构成:

  • 内容区(content)widthheight定义的区域,存放文本或子元素
  • 内边距(padding) :内容区与边框之间的空白,会影响盒子的视觉大小
  • 边框(border) :包裹内容区和内边距的线条,有宽度和样式属性
  • 外边距(margin) :盒子与其他盒子之间的空白,不影响盒子自身大小

两种盒模型计算方式(核心!)

盒模型有两种计算规则,用box-sizing属性切换:

1. 标准盒模型(content-box)

  • 盒子总宽度 = width + padding-left + padding-right + border-left + border-right

  • 例子:

    .box {
      box-sizing: content-box;
      width: 400px;
      padding: 5px;
      border: 10px solid red;
    }
    /* 实际占位宽度 = 400 + 5*2 + 10*2 = 430px */
    

2. IE 盒模型(border-box)

  • 盒子总宽度 = width(已包含 padding 和 border)

  • 例子:

    .box {
      box-sizing: border-box;
      width: 400px;
      padding: 5px;
      border: 10px solid red;
    }
    /* 实际占位宽度 = 400px(内容区宽度被压缩为 400-5*2-10*2=370px) */
    

💡 小贴士:移动端布局常用border-box,避免计算 padding 和 border 对总宽度的影响~

三、定位:打破文档流的 "魔法" 🧙‍♂️

元素默认在文档流中排列,但通过position属性可以改变其位置规则:

1. 绝对定位(position: absolute)

  • 元素脱离文档流,不再占据原位置

  • 相对于最近的已定位祖先元素(非 static)定位,若无则相对浏览器窗口

  • 例子:

     .inner {
       position: absolute;
       top: 0;
       left: 0; /* 相对于父元素main左上角定位 */
       width: 200px;
       height: 200px;
       border-radius: 50%; /* 圆形效果 */
     }
    

2. 相对定位(position: relative)

  • 元素不脱离文档流,原位置保留
  • 相对于自身在文档流中的原始位置偏移
  • 常用作绝对定位元素的 "定位容器"

3. z-index:控制元素层级关系 🏔️

  • 仅对已定位元素(非 static)有效

  • 值越大越靠上,但受 "堆叠上下文" 影响:

    • 父元素 z-index 会限制子元素层级(子元素无法超出父元素的堆叠层级)

    • 例子:

.box { position: relative; z-index: 1; } /* 父容器 */
.box3 { z-index: 2; } /* 虽然值大,但父元素无定位,可覆盖.box内元素 */

四、实战对比:两种盒模型的差异演示 🔍

假设两个盒子设置相同样式,仅box-sizing不同:

    /* 标准盒模型 */
    .box-standard {
      box-sizing: content-box;
      width: 200px;
      padding: 20px;
      border: 10px solid #000;
    }

    /* IE盒模型 */
    .box-ie {
      box-sizing: border-box;
      width: 200px;
      padding: 20px;
      border: 10px solid #000;
    }
  • 标准盒模型总宽度:200 + 202 + 102 = 260px
  • IE 盒模型总宽度:200px(内容区被压缩为 140px)

五、布局实战:组合技让页面 "活" 起来 🛠️

页面布局的核心公式:
页面显示 = 文档流 + 布局模式(flex/grid) + 盒模型 + 定位

经典布局:

  1. flex实现页面整体结构(header+container+footer)

  2. box-sizing控制盒子尺寸计算规则

  3. position: absolute实现局部元素的自由定位

  4. overflow: scroll处理内容溢出

    body {
      display: flex;
      flex-direction: column;
      height: 100vh; /* 全屏高度 */
    }
    .container {
      flex: 1; /* 占满剩余空间 */
      overflow: scroll; /* 内容溢出时显示滚动条 */
    }

总结:掌握这些,布局不再踩坑 🚀

  1. 盒模型是基础:根据需求选择content-boxborder-box

  2. 文档流是根本:理解元素默认排列规则,再谈脱离

  3. 定位是补充:absolute脱离流,relative保位置,z-index控层级

  4. 多练多试:用浏览器开发者工具实时调试盒子尺寸和定位

希望这篇文章能帮你理清盒模型和定位的逻辑!快去动手实践,打造更灵活的页面布局吧~💻

GIS 空间关系:九交模型

作者 GIS之路
2025年7月5日 16:56

前言

空间关系用于描述几何对象之间的拓扑结构,如何确定空间对象位置及其空间关系是GIS首要解决的问题。而九交模型的提出和实现为GIS空间关系的确定奠定了基础。

1. 九交模型起源

九交模型诞生于论文《Point-Set Topological Spatial Relations》,这篇由Max J. Egenhofer和Robert D. Franzosa于1991年发表在International Journal of Geographical Information Systems的奠基性论文,首次系统性地提出了九交模型(9-Intersection Model),为GIS空间关系分析建立了严格的数学框架。

2. 九交模型基本概念

九交模型英文叫做9-Intersection Model,它主要通过分析几何对象内部(Interior)、外部(Exterior)和边界(Boundary)交集情况来确定几何对象之间的空间拓扑关系。

使用简单几何要素描述其内部、外部及边界情况如下:

  • 要素:内部是点,边界是空集,外部是平面上除点以外的所有其他部分。
  • 线要素:内部、边界和外部不容易划分,内部是以端点为界限的线的那一部分,边界是线性要素的端点,外部是平面中除内部和边界外的所有其他部分。
  • 要素:内部是以环为边界的里面的那一部分;边界是环本身;外部是边界外的一切。

图片来源于Yukon(禹贡)数据库

为了能够唯一描述每一种简单几何对象的空间关系,空间实体的“补”作为空间实体的外部被引入空间关系描述框架 ,它同空间实体的边界、内部构成了简单空间实体的基本组件。假设空间实体 A 的边界为 A、内部为 A°、“补”为 A-,空间实体 B 的边界为 B、内部 B°、“补”为 B-,它们两两之间的交集就构成了空间关系描述的 9元组框架。

对于任意两个几何对象A和B,它们的拓扑关系可以用一个3×3的矩阵表示:

内部每一元素的取值都有空(∅)与非空(¬∅)两种可能。根据排列组合原理,9个元素项共有29= 512 种可能的取值,也就是说两个简单空间实体之间存在 512 种关系可能,当然,绝大部分空间关系可能没有意义。

对上图中九交矩阵元素含义表示如下:

  1. A°∩B°:两对象内部的交集
  2. A°∩∂B:A内部与B边界的交集
  3. A°∩B⁻:A内部与B外部的交集
  4. ∂A∩B°:A边界与B内部的交集
  5. ∂A∩∂B:两对象边界的交集
  6. ∂A∩B⁻:A边界与B外部的交集
  7. A⁻∩B°:A外部与B内部的交集
  8. A⁻∩∂B:A外部与B边界的交集
  9. A⁻∩B⁻:两对象外部的交集

而在实际应用中常见的空间关系模型有以下几种,这些关系为:

  1. 相离(Disjoint):是指两对象不相交,即在对象的内部、外部和边界均没有交集。
  2. 相接(Meet/Touch):是指两对象边界相交。
  3. 重叠(Overlap):是指两个对象存在部分交集。
  4. 相等(Equal):是指两个对象完全重合。
  5. 包含(Contains):是指其中一个对象是另一个对象的子集。
  6. 被包含(在内部)(Within):是指B包含A,即几何对象A在几何对象B的内部。
  7. 覆盖(Covers):是指两个对象A和B,其中对象B是对象A的子集。
  8. 被覆盖(CoveredBy):是指两个对象A和B,其中对象A是对象B的子集。

3. 九交模型现实意义

九交模型在GIS和空间数据库中具有重要的现实意义。它不仅为空间关系的精确描述提供了理论基础,还支持空间查询、分析、推理、数据质量检测、多源数据融合、自然语言表达、不确定性处理和标准化等应用。随着GIS技术的不断发展,九交模型的应用范围将进一步扩大,为更多领域的空间信息处理提供支持。

参考资料

  • 九交模型论文:https://www.tandfonline.com/doi/epdf/10.1080/02693799108927841?needAccess=true
  • Yukon(禹贡)数据库:https://yukon.supermap.io/2.0/demos/demo_de9im.html

OpenLayers示例数据下载,请回复关键字:ol数据

全国信息化工程师-GIS 应用水平考试资料,请回复关键字:GIS考试

【GIS之路】 已经接入了智能助手,欢迎关注,欢迎提问。

欢迎访问我的博客网站-长谈GIShttp://shanhaitalk.com

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

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

新AI模型助力更准确预测心源性猝死风险

2025年7月5日 16:50
美国研究人员开发出一款多模态人工智能(AI)模型,能显著提高识别心源性猝死高风险人群的准确性,有助于挽救生命,减少不必要的医疗干预。美国约翰斯·霍普金斯大学等机构的研究人员近日在《自然-心血管研究》杂志上发表论文说,他们新开发的AI模型名为“多模态AI室性心律失常风险分层系统(MAARS)”,可通过分析患者的心脏增强磁共振成像(MRI)及各种医疗数据,挖掘出此前未被识别的重要心脏健康信息,从而更准确预测由室性心律失常导致的心源性猝死风险。(新浪财经)

前端高手才知道的秘密:Blob 居然这么强大!

作者 小飞悟
2025年7月5日 16:43

🔍 一、什么是 Blob?

Blob(Binary Large Object)是 HTML5 提供的一个用于表示不可变的、原始二进制数据块的对象。

✨ 特点:

  • 不可变性:一旦创建,内容不能修改。
  • 可封装任意类型的数据:字符串、ArrayBuffer、TypedArray 等。
  • 支持 MIME 类型描述,方便浏览器识别用途。

💡 示例:

const blob = new Blob(['Hello World'], { type: 'text/plain' });

🧠 二、Base64 编码的前世今生

虽然名字听起来像是某种“64进制”,但实际上它是一种编码方式,不是数学意义上的“进制”。

📜 起源背景:

Base64 最早起源于电子邮件协议 MIME(Multipurpose Internet Mail Extensions),因为早期的电子邮件系统只能传输 ASCII 文本,不能直接传输二进制数据(如附件)。于是人们发明了 Base64 编码方法,把二进制数据转换成文本形式,从而安全地在网络上传输。

🧩 使用场景:

场景 说明
图片嵌入到 HTML/CSS 中 Data URI 方式减少请求
JSON 数据中传输二进制信息 如头像、加密数据等
WebSocket 发送二进制消息 避免使用 ArrayBuffer
二维码生成 将图像转为 Base64 存储

⚠️ 注意:Base64 并非压缩算法,它会将数据体积增加约 33%。


🔁 三、从 Base64 到 Blob 的全过程

1. Base64 字符串解码:atob()

JavaScript 提供了一个内置函数 atob(),可以将 Base64 字符串解码为原始的二进制字符串(ASCII 表示)。

const base64Data = 'SGVsbG8gd29ybGQh'; // "Hello world!"
const binaryString = atob(base64Data);

⚠️ 返回的是 ASCII 字符串,不是真正的字节数组。


2. 构建 Uint8Array(字节序列)

为了构造 Blob,我们需要一个真正的字节数组。我们可以用 charCodeAt() 将每个字符转为对应的 ASCII 数值(即 0~255 的整数)。

const byteArray = new Uint8Array(binaryString.length);

for (let i = 0; i < binaryString.length; i++) {
    byteArray[i] = binaryString.charCodeAt(i);
}

现在,byteArray 是一个代表原始图片二进制数据的数组。


3. 创建 Blob 对象

有了字节数组,就可以创建 Blob 对象了:

const blob = new Blob([byteArray], { type: 'image/png' });

这个 Blob 对象就代表了一张 PNG 图片的二进制内容。


4. 使用 URL.createObjectURL() 显示图片

为了让浏览器能够加载这个 Blob 对象,我们需要生成一个临时的 URL 地址:

const imageUrl = URL.createObjectURL(blob);
document.getElementById('blobImage').src = imageUrl;

这样,你就可以在网页中看到这张图片啦!


🛠️ 四、Blob 的核心功能与应用场景

功能 说明
分片上传 .slice(start, end) 方法可用于大文件切片上传
本地预览 Canvas.toBlob() 导出图像,配合 URL.createObjectURL 预览
文件下载 使用 a 标签 + createObjectURL 实现无刷新下载
缓存资源 Service Worker 中缓存 Blob 数据提升性能
处理用户上传 结合 FileReader 和 File API 操作用户文件

🧪 五、Blob 的高级玩法

1. 文件切片上传(分片上传)

const chunkSize = 1024 * 1024; // 1MB
const firstChunk = blob.slice(0, chunkSize);

2. Blob 转换为其他格式

  • FileReader.readAsText(blob) → 文本
  • FileReader.readAsDataURL(blob) → Base64
  • FileReader.readAsArrayBuffer(blob) → Array Buffer

3. Blob 下载为文件

const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'example.png';
a.click();

🧩 六、相关知识点汇总

技术点 作用
Base64 将二进制数据编码为文本,便于传输
atob() 解码 Base64 字符串,还原为二进制字符串
charCodeAt() 获取字符的 ASCII 值(0~255)
Uint8Array 构建字节数组,表示原始二进制数据
Blob 封装二进制数据,作为文件对象使用
URL.createObjectURL() 生成临时地址,让浏览器加载 Blob 数据

🧾 七、完整代码回顾

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Blob 实战</title>
</head>
<body>
  <img src="" id="blobImage" width="100" height="100" alt="Blob Image" />

  <script>
    const base64Data = 'UklGRiAHAABXRUJQVlA4IBQHAACwHACdASpQAFAAPok0lEelIyIhMziOYKARCWwAuzNaQpfW+apU37ZufB5rAHqW2z3mF/aX9o/ev9LP+j9KrqSOfp9mf+6WmE1P1yFc3gTlw8B8d/TebelHaI3mplPrZ+Aa0l5qDGv5N8Tt9vYhz3IH37wqm2al+FdcFQhDnObv2+WfpwIZ+K6eBPxKL2RP6hiC/K1ZynnvVYth9y+ozyf88Obh4TRYcv3nkkr43girwwJ54Gd0iKBPZFnZS+gd1vKqlfnPT5wAwzxJiSk+pkbtcOVP+QFb2uDqUhuhNiHJ8xPt6VfGBfUbTsUzYuKgAP4L9wrkT8KU4sqIHwM+ZeKDBpGq58k0aDirXeGc1Odhvfx+cpQaeas97zVTr2pOk5bZkI1lkF9jnc0j2+Ojm/H+uPmIhS7/BlxuYfgnUCMKVZJGf+iPM44vA0EwvXye0YkUUK...';

    const binaryString = atob(base64Data); // Base64 解码
    const byteArray = new Uint8Array(binaryString.length); // 创建 Uint8Array

    for (let i = 0; i < binaryString.length; i++) {
        byteArray[i] = binaryString.charCodeAt(i); // 填充字节数据
    }

    const blob = new Blob([byteArray], { type: 'image/png' }); // 创建 Blob
    const imageUrl = URL.createObjectURL(blob); // 生成 URL

    document.getElementById('blobImage').src = imageUrl; // 显示图片
  </script>
</body>
</html>

📚 八、扩展阅读建议


🧩 九、结语

Blob 是连接 JavaScript 世界与真实二进制世界的桥梁,是每一个想要突破瓶颈的前端开发者必须掌握的核心技能之一。 掌握了 Blob,你就拥有了操作二进制数据的能力,这在现代 Web 开发中是非常关键的一环。 下次当你看到一张图片在网页中加载出来,或者一个文件被顺利下载时,不妨想想:这一切的背后,都有 Blob 的身影。

❌
❌