阅读视图

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

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

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

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 浏览器的一次问题定位

背景

批量上传资源型文件, 需要先获取对应每份文件的 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 基础使用

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 才是二进制处理的王者,面试常考

前端开发中,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: 变身一款丝滑的 图片查看器

最近收集了不少优质的图片,这些图片数量太多;为了能够快速浏览,自动播放;不用一个个的点击,上次用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 ---

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

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

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

什么是文档流?

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

  • 块级元素(如 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首要解决的问题。而九交模型的提出和实现为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开发 相关内容,欢迎关注 !

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

🔍 一、什么是 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 的身影。

OneCode自主UI设计体系:架构解析与核心实现

引言

OneCode平台的UI设计体系基于组件化思想构建,通过清晰的对象生命周期管理、灵活的样式模板系统和高效的事件处理机制,为开发者提供了强大的自主UI设计支持。本文将深入剖析OneCode UI组件的核心架构,重点讲解对象生命周期、样式模板配置和事件管理三大核心模块的设计与实现。

一、UI组件核心架构 overview

OneCode UI组件体系采用分层设计,构建了从基础对象到具体组件的完整继承链。核心基类关系如下:

┌────────────────┐      ┌────────────────┐
│   xui.absObj   │──────│ xui.absProfile │
└────────┬───────┘      └────────┬───────┘
         │                       │
┌────────▼───────┐      ┌────────▼───────┐
│  xui.Module    │      │ xui.UIProfile  │
└────────────────┘      └────────────────┘
  • xui.absObj: 所有可实例化对象的根类,提供基础的生命周期管理和属性配置能力
  • xui.absProfile: 配置文件基类,为模块和模板提供统一的配置管理
  • xui.Module: 业务模块基类,定义模块的生命周期和数据交互规范
  • xui.UIProfile: UI组件基类,提供渲染和样式管理的核心能力

组件化实现基础

OneCode使用xui.Class方法定义组件,典型结构如下:

xui.Class("xui.MQTT", "xui.absObj", {
    Constructor: function(properties, events) {
        // 调用父类构造函数
        this.upper(arguments);
        // 组件初始化逻辑
    },
    Instance: {
        // 实例方法和属性
    },
    Static: {
        // 静态方法和属性
    }
});

二、对象生命周期管理

OneCode组件的生命周期管理通过xui.ModuleFactory实现,提供了从组件创建到销毁的完整生命周期支持。

生命周期阶段

  1. 创建阶段
    • initialize: 初始化组件配置
    • beforeCreated: DOM创建前的准备工作
    • onCreated: DOM创建完成后的初始化
  2. 运行阶段
    • render: 组件渲染
    • refresh: 组件刷新
    • 事件响应与交互
  3. 销毁阶段
    • destroy: 组件销毁与资源释放

模块工厂实现

xui.ModuleFactory负责组件的实例化和缓存管理,核心方法包括:

// 获取或创建模块实例
getModule: function(id, onEnd, threadid, cached) {
    // cached=true: 从缓存获取实例
    // cached=false: 创建新实例
    if (cached && this._cache[id] && !this._cache[id].destroyed) {
        return this._cache[id];
    }
    // 模块创建逻辑
},

// 创建新模块实例
newModule: function(cls, onEnd, threadid, properties, events) {
    return this.getModule(cls, onEnd, threadid, false, properties, events);
}

生命周期管理示例

// 创建模块实例
xui.getModule('MyComponent', function(err, module) {
    if (!err) {
        module.show(); // 显示组件
    }
}, null, false);

// 销毁模块
module.destroy();

三、样式模板配置系统

OneCode的样式模板系统基于xui.Template类实现,提供了灵活的HTML模板解析和样式管理能力。

模板定义与渲染

模板系统支持变量替换、事件绑定和嵌套模板,示例如下:

// 设置模板
this.setTemplate({
    root: '<div class="container" [event]>{title}</div>'
});

// 渲染模板
render: function() {
    if (!this.renderId) {
        var div = xui.$getGhostDiv();
        div.innerHTML = this.toHtml();
        // 事件处理绑定
        // DOM节点注册
        this.renderId = div.firstChild.$xid;
    }
    return this;
}

样式管理机制

组件样式通过以下方式进行管理:

  1. 内置样式:组件默认样式定义
  2. 自定义样式:通过setCustomStyle方法覆盖默认样式
  3. 动态样式:运行时根据状态动态修改样式
// 设置自定义样式
component.setCustomStyle({
    'background-color': '#f5f5f5',
    'border': '1px solid #ddd'
});

模板变量与表达式

模板系统支持多种变量和表达式:

  • 简单变量替换: {property}
  • 事件绑定标记: [event]
  • 嵌套模板引用: 通过tpl_evkey实现

四、事件管理机制

OneCode的事件管理系统提供了组件内部、组件间以及全局事件的完整支持。

事件定义与绑定

组件通过setEvents方法定义事件处理逻辑:

this.setEvents({
    'click': function(e) {
        // 点击事件处理
    },
    'customEvent': function(data) {
        // 自定义事件处理
    }
});

事件触发与传播

通过_fireEvent方法触发事件,并支持事件冒泡和数据传递:

// 触发自定义事件
this._fireEvent('customEvent', {param1: 'value'});

全局事件总线

通过xui.message实现跨组件通信:

// 发送全局消息
xui.message('global.event', data);

// 订阅全局消息
xui.message.on('global.event', function(data) {
    // 处理消息
});

事件委托机制

模板系统支持事件委托,通过[event]标记自动绑定事件处理器:

// 模板中的事件标记
'<button [event]>{caption}</button>'

// 事件处理器自动绑定
// 在模板渲染时,系统会自动将事件处理器绑定到对应的DOM元素

五、组件通信机制

OneCode提供了灵活的组件通信方式,满足不同场景下的交互需求。

父子组件通信

  • 父组件通过setHost设置宿主关系
  • 子组件通过getParent获取父组件实例
// 父组件中设置子组件宿主
child.setHost(this, 'childAlias');

// 子组件中获取父组件
var parent = this.getParent();

跨层级组件通信

通过模块工厂和别名系统实现跨层级组件访问:

// 通过别名获取组件实例
var component = xui.getObjectByAlias('componentAlias');

六、OneCode UI设计优势

  1. 完整的组件化体系:从基础对象到具体组件的清晰继承关系
  2. 灵活的生命周期管理:精细控制组件从创建到销毁的全过程
  3. 强大的模板系统:支持动态渲染和复杂交互
  4. 高效的事件机制:满足组件内、组件间和全局事件需求
  5. 松耦合的组件通信:降低组件间依赖,提高复用性

结语

OneCode的自主UI设计体系通过精心设计的组件架构、生命周期管理、样式模板系统和事件机制,为开发者提供了强大而灵活的UI构建工具。这种设计不仅提高了开发效率,也保证了UI组件的一致性和可维护性,为企业级应用开发提供了坚实的技术支撑。

未来,OneCode UI体系将继续优化性能,扩展组件库,为开发者提供更加丰富的UI设计能力和更好的开发体验。

【纯前端推理】纯端侧 AI 对象检测:用浏览器就能跑的深度学习模型

🚀 纯端侧 AI 对象检测:用浏览器就能跑的深度学习模型

前言

随着 WebAssembly 和浏览器 GPU 加速技术的快速发展,在浏览器中直接运行深度学习模型已经成为现实。这一突破的关键在于 ONNX Web Runtime,它作为微软开源的跨平台机器学习推理引擎,能够将训练好的 ONNX 模型高效地运行在浏览器环境中。

ONNX Web Runtime 通过以下技术实现了浏览器端的高性能 AI 推理:

  • WebAssembly (WASM):提供接近原生性能的计算能力
  • WebGL:利用 GPU 进行并行计算加速
  • WebGPU:下一代 GPU 计算标准,性能更强劲
  • CPU 优化:针对不同架构的专门优化

基于这些底层技术,结合 Hugging Face Transformers.js 的高级封装,我们今天来实现一个完全运行在浏览器端的对象检测应用。整个过程无需服务器、无需上传数据,在保护用户隐私的同时,让每个人都能便捷地体验 AI 的强大能力。

🔬 技术原理:Transformers.js 在底层调用 ONNX Web Runtime,将 PyTorch/TensorFlow 训练的模型转换为 ONNX 格式,然后在浏览器中进行推理计算。

🎯 效果展示

效果图.png

上传一张图片,AI 立即识别出图中的对象,并用彩色边框标注出来

🔧 技术栈

  • 前端框架: TypeScript + Vite
  • AI 推理: Hugging Face Transformers.js
  • 模型: DETR ResNet-50 (Facebook 开源)

📖 端侧推理的优势

graph TB
    A[用户上传图片] --> B[浏览器加载模型]
    B --> C[本地推理计算]
    C --> D[返回检测结果]
    D --> E[Canvas可视化]

    F[传统方案] --> G[上传到服务器]
    G --> H[服务器推理]
    H --> I[返回结果]

    style A fill:#e1f5fe
    style E fill:#c8e6c9
    style F fill:#ffebee
    style I fill:#ffcdd2

端侧推理的优势:

  • 隐私保护:数据不离开设备
  • 实时响应:无网络延迟
  • 成本控制:无服务器费用
  • 离线可用:无需网络连接

端侧推理 VS 服务端推理:

特性 端侧推理 服务器推理
隐私保护 ✅ 完全本地 ❌ 需要上传
成本 ✅ 零服务器成本 ❌ 需要 GPU 服务器
离线使用 ✅ 支持离线 ❌ 需要网络
模型更新 ❌ 需要重新下载 ✅ 服务器更新
计算资源 ❌ 依赖设备性能 ✅ 专业 GPU
响应速度 ❌ 依赖端侧计算能力 ✅ 专业 GPU

🔨 核心代码实现

使用 Vite 新建项目

pnpm create vite

安装 transformers

pnpm i @huggingface/transformers

调用模型,实现推理 模型地址: huggingface.co/Xenova/detr…

/** 加载模型 */
const detector = await pipeline("object-detection", "Xenova/detr-resnet-50", {
  dtype: "auto",
});

/** 设置阈值 分数小于 0.9 的不展示 */
const output = await detector(imageUrl, { threshold: 0.9 });

console.log("Detection results:", output);

/** 推理的结果是一个由下面接口组成的数组 */
interface DetectionResult {
  label: string;
  score: number;
  box: {
    xmin: number;
    ymin: number;
    xmax: number;
    ymax: number;
  };
}

完整代码

import { pipeline } from "@huggingface/transformers";

interface CreateCanvasOptions {
  width: number;
  height: number;
}

interface DetectionResult {
  label: string;
  score: number;
  box: {
    xmin: number;
    ymin: number;
    xmax: number;
    ymax: number;
  };
}

type Detector = (
  image: string,
  options: { threshold: number }
) => Promise<DetectionResult[]>;

let detector: unknown | null = null;

async function initializeModel() {
  if (!detector) {
    console.time("Model loading");
    detector = await pipeline("object-detection", "Xenova/detr-resnet-50", {
      dtype: "auto",
    });
    console.timeEnd("Model loading");
  }

  return detector;
}

async function detectObjects(imageUrl: string) {
  const model = await initializeModel();
  const detector = model as Detector;

  console.time("Detection");
  const output = await detector(imageUrl, { threshold: 0.9 });
  console.timeEnd("Detection");

  console.log("Detection results:", output);

  return output.map((detection: DetectionResult) => ({
    label: detection.label,
    score: detection.score,
    box: {
      xmin: detection.box.xmin,
      ymin: detection.box.ymin,
      xmax: detection.box.xmax,
      ymax: detection.box.ymax,
    },
  }));
}

function loadImage(url: string) {
  return new Promise<HTMLImageElement>((resolve, reject) => {
    const image = new Image();
    image.crossOrigin = "anonymous";
    image.src = url;

    image.addEventListener("load", () => {
      resolve(image);
    });

    image.addEventListener("error", () => {
      reject(new Error("Failed to load image"));
    });
  });
}

function createCanvas(options: CreateCanvasOptions) {
  const canvas = document.createElement("canvas");
  const context = canvas.getContext("2d");

  if (!context) {
    throw new Error("Failed to get canvas context");
  }

  const devicePixelRatio = window.devicePixelRatio || 1;
  canvas.width = options.width * devicePixelRatio;
  canvas.height = options.height * devicePixelRatio;

  context.scale(devicePixelRatio, devicePixelRatio);

  return {
    canvas,
    context,
    devicePixelRatio,
  };
}

function drawDetections(
  context: CanvasRenderingContext2D,
  detections: DetectionResult[]
) {
  for (const [index, detection] of detections.entries()) {
    const { box, label, score } = detection;
    const colors = [
      "#FF6B6B",
      "#4ECDC4",
      "#45B7D1",
      "#96CEB4",
      "#FFEAA7",
      "#DDA0DD",
      "#98D8C8",
    ];
    const color = colors[index % colors.length];

    /** 画框 */
    const lineWidth = 2;
    const halfLineWidth = lineWidth / 2;
    const textPadding = 10;
    context.strokeStyle = color;
    context.lineWidth = lineWidth;
    context.strokeRect(
      box.xmin,
      box.ymin,
      box.xmax - box.xmin,
      box.ymax - box.ymin
    );

    /** 画标签 */
    context.fillStyle = color;
    const labelText = `${label.toUpperCase()} (${(score * 100).toFixed(1)}%)`;
    context.font = "16px Arial";
    const metrics = context.measureText(labelText);
    const textHeight =
      metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
    const textWidth = metrics.width;
    context.fillRect(
      box.xmin - halfLineWidth,
      box.ymin - textHeight - halfLineWidth - textPadding,
      textWidth + textPadding,
      textHeight + textPadding
    );

    /** 画标签文本 */
    context.fillStyle = "white";
    context.textAlign = "left";
    context.textBaseline = "top";
    context.fillText(
      labelText,
      box.xmin + textPadding / 2,
      box.ymin - textHeight - textPadding / 2
    );
  }
}

async function detectImage(url: string) {
  const image = await loadImage(url);
  const { canvas, context } = createCanvas({
    width: image.naturalWidth,
    height: image.naturalHeight,
  });

  context.drawImage(image, 0, 0, image.naturalWidth, image.naturalHeight);
  canvas.style.display = "block";
  canvas.style.margin = "50px auto";
  if (image.naturalWidth > image.naturalHeight) {
    canvas.style.width = `${window.innerWidth * 0.85}px`;
  } else {
    canvas.style.height = `${window.innerHeight * 0.85}px`;
  }

  document.body.appendChild(canvas);

  const detections = await detectObjects(image.src);

  drawDetections(context, detections);
}

async function selectImageAndCreateBlobUrl(options?: {
  accept?: string;
  multiple?: boolean;
}): Promise<string> {
  const { accept = "image/*", multiple = false } = options || {};

  return new Promise((resolve, reject) => {
    const fileInput = document.createElement("input");
    fileInput.type = "file";
    fileInput.accept = accept;
    fileInput.multiple = multiple;
    fileInput.style.display = "none";

    fileInput.onchange = (event) => {
      const target = event.target as HTMLInputElement;
      const file = target.files?.[0];

      if (!file) {
        reject(new Error("No file selected"));
        return;
      }

      // 验证文件类型
      if (!file.type.startsWith("image/")) {
        reject(new Error("Selected file is not an image"));
        return;
      }

      // 创建 blob URL
      const blobUrl = URL.createObjectURL(file);

      // 清理 DOM
      document.body.removeChild(fileInput);

      resolve(blobUrl);
    };

    fileInput.oncancel = () => {
      document.body.removeChild(fileInput);
      reject(new Error("File selection cancelled"));
    };

    // 添加到 DOM 并触发点击
    document.body.appendChild(fileInput);
    fileInput.click();
  });
}

const selectImageButton = document.getElementById("select-image")!;

selectImageButton.addEventListener("click", async () => {
  const blobUrl = await selectImageAndCreateBlobUrl();
  detectImage(blobUrl);
});

🎯 总结

通过 Hugging Face Transformers.js 和现代浏览器的强大能力,我们成功实现了完全运行在浏览器端的对象检测应用。这种方案具有以下优势:

  • 🔒 隐私安全:数据不离开设备
  • 💰 成本效益:无服务器费用
  • 📱 易于部署:静态网站即可

随着 WebGPU、WebAssembly 等技术的发展,端侧 AI 推理将会变得更加高效和普及。这不仅为开发者提供了新的可能性,也为用户带来了更好的隐私保护和使用体验。


🔗 相关资源

💡 提示:如果你觉得这篇文章对你有帮助,请点个赞支持一下!有任何问题欢迎在评论区讨论。

Tags: JavaScript TypeScript 机器学习 人工智能 Web开发 前端 Transformers.js 对象检测

github 集成CICD自动化部署

1设置阿里云密钥对

由于我使用的是阿里云,需要在阿里云控制面板设置密钥对,导出的私钥ssh_key2.pem文件先暂存在本地;由于我好几次使用 ssh-keygen -t rsa -b 2048生成密钥,配置在cicd环境变量中未生效,建议通过阿里云控制面板生成密钥对生成;必须是完整的私钥,包括头尾,例如:

  -----BEGIN OPENSSH PRIVATE KEY-----
  ...内容...
  -----END OPENSSH PRIVATE KEY-----

添加图片注释,不超过 140 字(可选)

同时还需要修改服务器ssh配置文件,/etc/ssh/sshd_config中应有:

  PubkeyAuthentication yes #允许密钥形式连接
  PermitRootLogin yes #允许root用户登录

修改后需重启 sshd:

  systemctl restart sshd

2 设置CICD环境变量

如图,直接在GitHub仓库settings中操作

添加图片注释,不超过 140 字(可选)

这里需要三个变量:

SERVER_SSH_KEY #导出的私钥ssh_key2.pem的内容,不要有多余的空格、换行或其他字符
USER_HOST #这里是服务器的ip
USER_NAME #我这里是root

3 配置action执行配置文件

添加图片注释,不超过 140 字(可选)

会在工程目录生成.github/workflows/main.yml文件,以下是具体配置,本地修改记得将文件从仓库pull到本地

name: Build app and deploy to Tencent
on:
  #监听push操作
  push:
    branches:
      # mian分支,你也可以改成其他分支
      - main
jobs:
  # 任务ID
  build:
    # 运行环境
    runs-on: ubuntu-latest
    # 步骤
    steps:
    # 使用别人的action
    - uses: actions/checkout@v2
    # 步骤名称
    - name: npm install
      # 步骤执行指令
      run: npm install
    - name: npm run build
      run: npm run build
    # - name: Install SSH Client
    #   run: sudo apt-get update && sudo apt-get install -y openssh-client
    - name: ls
      run: ls -al && pwd
    # 命名这个任务为发布Deploy
    - name: Deploy
      uses: easingthemes/ssh-deploy@v2.1.5
      env:
        # SSH_PRIVATE_KEY: ${{ secrets.SERVER_SSH_KEY2 }}
        REMOTE_HOST: ${{ secrets.USER_HOST }}
        SSH_PRIVATE_KEY: ${{ secrets.SERVER_SSH_KEY }}
        REMOTE_USER: ${{ secrets.USER_NAME }}
        REMOTE_PORT: 22
        SOURCE: "dist/"
        TARGET: "/home/www/dist"

SOURCE是我的vue工程打包目录,TARGET是目标服务器目录,/home/www是nginx静态文件目录,CICD执行完后就可以看到我的最新页面了。

第五款 HarmonyOS 上架作品 奇趣故事匣 来了

第五款 HarmonyOS 上架作品 奇趣故事匣 来了

前言

北京时间2025年7月4日 ,青蓝逐码组织第五款鸿蒙元服务 奇趣故事匣 也发布上架了,奇趣故事匣 是一个奇幻故事类的应用,目前包含的核心功能有 奇趣故事分类、浏览、语音播报,还包括有华为静默登录,它也是我们青蓝逐码组织的一个小伙伴独立开发前端+后端的作品,耗时一个多月的业余时间完成🎉。

效果一览

开发故事

这次作品是一个小伙伴独立完成开发并且上架的,作战能力很强,这里我们可以分享下他开发的历程故事。

建立作品选型

我们在立项前也会简单的做个技术沟通,确保目标和方向是正确并且可行的。


技术方案讨论

如果小伙伴不懂后端,但是执行力强,还可以推荐具体的技术,如端云


程序员的思维 - 记账类APP

传统的记账类太多,很容易引起生理反感

接入了华为的AI朗读

在商讨后期接入大模型,可以让用户自己AI创作故事

思想的碰撞会指引出具体的行动方向


头脑乱掉了,简单的业务给限制住了


实践得真知

开发日志

独立开发就是快,自己一个人噌噌噌的写和改。

上架历程

从提测到真正真正上架,也花了有差不多小半个月时间了

技术说明文档

项目概述

奇趣故事匣是一款基于HarmonyOS开发的故事阅读应用,采用原子化服务(Atomic Service)架构。应用提供故事浏览、分类查看、在线阅读、收藏管理、阅读历史等功能,支持华为账号授权登录。

基本信息

  • 应用名称: 奇趣故事匣
  • 应用类型: 原子化服务 (atomicService)
  • 版本: 1.0.0 (versionCode: 1000000)
  • 兼容SDK版本: 5.0.4(16)
  • 运行系统: HarmonyOS

技术架构

开发框架

  • 开发语言: ArkTS (TypeScript扩展)
  • UI框架: ArkUI
  • 架构模式: Stage模型
  • 组件化: ComponentV2 + @Local状态管理
  • 数据持久化: PersistenceV2
  • 导航: Navigation + NavPathStack

项目结构

FunStoryBox/
├── AppScope/                    ## 应用级配置
│   ├── app.json5               ## 应用配置文件
│   └── resources/              ## 应用级资源
├── entry/                      ## 主模块
│   ├── src/main/
│   │   ├── ets/                ## TypeScript源码
│   │   │   ├── components/     ## 可复用组件
│   │   │   ├── constants/      ## 常量定义
│   │   │   ├── entryability/   ## 应用入口
│   │   │   ├── models/         ## 数据模型
│   │   │   ├── pages/          ## 页面组件
│   │   │   └── utils/          ## 工具类
│   │   ├── module.json5        ## 模块配置
│   │   └── resources/          ## 资源文件
│   └── build-profile.json5     ## 构建配置
├── config/                     ## 签名证书配置
└── build-profile.json5         ## 项目构建配置

核心功能模块

1. 用户认证模块

  • 华为账号授权登录: 集成华为账号Kit,支持OAuth2.0授权流程
  • 自动登录: 应用前台时自动检测登录状态
  • 用户信息管理: 持久化存储用户基本信息

2. 故事内容模块

  • 故事浏览: 支持分类浏览、热门推荐、最新更新
  • 故事详情: 展示故事基本信息、章节列表、相关推荐
  • 在线阅读: 章节内容阅读,支持阅读进度记录
  • 搜索功能: 故事标题和内容搜索

3. 个人中心模块

  • 阅读历史: 记录和管理用户阅读记录
  • 收藏管理: 故事收藏和取消收藏
  • 个人信息: 用户资料展示和管理

4. 数据管理模块

  • 网络请求: 封装HTTP工具类,支持GET/POST/DELETE请求
  • 数据模型: 定义完整的业务数据结构
  • 状态管理: 使用@Local和PersistenceV2管理应用状态

主要页面说明

页面路由配置

应用使用Navigation组件进行页面管理,路由配置如下:

页面名称

文件路径

功能描述

Layout

pages/Layout.ets

主布局页面,包含底部导航

StoryDetail

pages/StoryDetail.ets

故事详情页面

ReadStory

pages/ReadStory.ets

故事阅读页面

RecommendList

pages/RecommendList.ets

推荐列表页面

CategoryDetail

pages/CategoryDetail.ets

分类详情页面

SearchStory

pages/SearchStory.ets

故事搜索页面

AuthorDetail

pages/AuthorDetail.ets

作者详情页面

ReadHistory

pages/ReadHistory.ets

阅读历史页面

FavoriteStory

pages/FavoriteStory.ets

收藏故事页面

核心页面架构

1. 主布局页面 (Layout.ets)
  • 底部Tab导航,包含:首页、发现、我的
  • 使用Tabs组件实现页面切换
  • 集成用户信息状态管理
2. 首页 (Home.ets)
  • 分类统计展示
  • 热门故事推荐
  • 最新故事列表
  • 最近阅读记录
  • 下拉刷新功能
3. 发现页面 (Discover.ets)
  • 故事分类浏览
  • 推荐内容展示
4. 个人中心 (Mine.ets)
  • 用户信息展示
  • 阅读历史入口
  • 收藏管理入口

技术特性

1. 组件化开发

  • 可复用组件: PageHeader、SectionTitle、StoryCard、StoryList
  • 组件通信: 使用@Local装饰器实现组件间状态共享
  • 生命周期管理: 合理使用aboutToAppear等生命周期方法

2. 状态管理

  • 全局状态: 使用AppStorageV2管理导航栈
  • 持久化状态: 使用PersistenceV2存储用户信息
  • 响应式更新: @Monitor装饰器监听状态变化

3. 网络通信

  • HTTP封装: 统一的网络请求工具类
  • 错误处理: 完善的网络错误处理机制
  • 数据格式: 支持JSON和form-urlencoded格式

4. 安全特性

  • 应用签名: 配置开发和发布环境的签名证书
  • 权限管理: 申请网络访问权限
  • 数据加密: 集成RSA加密工具

API接口设计

基础配置

  • 服务器地址: ad.qqgsj.lzzvx.com
  • 超时时间: 60秒
  • 请求格式: JSON / form-urlencoded

主要接口

接口类型

接口路径

功能描述

GET

/api/category/totalStoriesCount

获取分类故事统计

GET

/api/story/hot

获取热门故事

GET

/api/story/latest

获取最新故事

GET

/api/story/search

故事搜索

POST

/api/user/authorizeLogin

华为账号授权登录

GET

/api/user/read/history/latest

获取最近阅读记录

POST

/api/favorite/add

添加收藏

DELETE

/api/favorite

取消收藏

数据模型

核心实体

用户模型 (User)
故事模型 (Story)
章节模型 (Chapter)

总结

目前我们上架的作品也已经达到了五个。

关于青蓝逐码组织

如果你兴趣想要了解更多的鸿蒙应用开发细节和最新资讯甚至你想要做出一款属于自己的应用!欢迎在评论区留言或者私信或者看我个人信息,可以加入技术交流群。

vue3+element-plus el-table列的显隐、列宽 持久化

element-plus el-table列的显隐、列宽 持久化

1 工具组件

<template>
  <div class="top-right-btn" :style="style">
    <el-row>
      <el-tooltip class="item" effect="dark" :content="showSearch ? '隐藏搜索' : '显示搜索'" placement="top" v-if="search">
        <el-button circle icon="Search" @click="toggleSearch()" />
      </el-tooltip>
      <el-tooltip class="item" effect="dark" content="刷新" placement="top">
        <el-button circle icon="Refresh" @click="refresh()" />
      </el-tooltip>
      <el-tooltip class="item" effect="dark" content="显隐列" placement="top" v-if="columns">
        <el-button circle icon="Menu" @click="showColumn()" v-if="showColumnsType == 'transfer'"/>
        <el-dropdown trigger="click" :hide-on-click="false" style="padding-left: 12px" v-if="showColumnsType == 'checkbox'">
          <el-button circle icon="Menu" />
          <template #dropdown>
            <el-dropdown-menu>
              <!-- 全选/反选 按钮 -->
              <el-dropdown-item>
                <el-checkbox :indeterminate="isIndeterminate" v-model="isChecked" @change="toggleCheckAll"> 列展示 </el-checkbox>
              </el-dropdown-item>
              <div class="check-line"></div>
              <template v-for="item in columns" :key="item.key">
                <el-dropdown-item>
                  <el-checkbox v-model="item.visible" @change="checkboxChange($event, item.prop)" :label="item.label" />
                </el-dropdown-item>
              </template>
            </el-dropdown-menu>
          </template>
        </el-dropdown>
      </el-tooltip>
    </el-row>
    <el-dialog :title="title" v-model="open" append-to-body>
      <el-transfer
        :titles="['显示', '隐藏']"
        v-model="value"
        :data="columns"
        @change="dataChange"
      ></el-transfer>
    </el-dialog>
  </div>
</template>

<script setup>
const props = defineProps({
  /* 是否显示检索条件 */
  showSearch: {
    type: Boolean,
    default: true
  },
  /* 显隐列信息 */
  columns: {
    type: Array
  },
  /* 是否显示检索图标 */
  search: {
    type: Boolean,
    default: true
  },
  /* 显隐列类型(transfer穿梭框、checkbox复选框) */
  showColumnsType: {
    type: String,
    default: "checkbox"
  },
  /* 右外边距 */
  gutter: {
    type: Number,
    default: 10
  },
})

const emits = defineEmits(['update:showSearch', 'queryTable','updateStorage'])

// 显隐数据
const value = ref([])
// 弹出层标题
const title = ref("显示/隐藏")
// 是否显示弹出层
const open = ref(false)

const style = computed(() => {
  const ret = {}
  if (props.gutter) {
    ret.marginRight = `${props.gutter / 2}px`
  }
  return ret
})

// 是否全选/半选 状态
const isChecked = computed({
  get: () => props.columns.every(col => col.visible),
  set: () => {}
})
const isIndeterminate = computed(() => props.columns.some((col) => col.visible) && !isChecked.value)

// 搜索
function toggleSearch() {
  emits("update:showSearch", !props.showSearch)
}

// 刷新
function refresh() {
  emits("queryTable")
}

// 右侧列表元素变化
function dataChange(data) {
  for (let item in props.columns) {
    const key = props.columns[item].key
    props.columns[item].visible = !data.includes(key)
  }
}

// 打开显隐列dialog
function showColumn() {
  open.value = true
}

if (props.showColumnsType == 'transfer') {
  // 显隐列初始默认隐藏列
  for (let item in props.columns) {
    if (props.columns[item].visible === false) {
      value.value.push(parseInt(item))
    }
  }
}

// 单勾选
function checkboxChange(event, prop) {
  props.columns.filter(item => item.prop == prop)[0].visible = event
  emits("updateStorage")
}

// 切换全选/反选
function toggleCheckAll() {
  const newValue = !isChecked.value
  props.columns.forEach((col) => (col.visible = newValue))
  emits("updateStorage")
}
</script>

<style lang='scss' scoped>
:deep(.el-transfer__button) {
  border-radius: 50%;
  display: block;
  margin-left: 0px;
}
:deep(.el-transfer__button:first-child) {
  margin-bottom: 10px;
}
:deep(.el-dropdown-menu__item) {
  line-height: 30px;
  padding: 0 17px;
}
.check-line {
  width: 90%;
  height: 1px;
  background-color: #ccc;
  margin: 3px auto;
}
</style>


2 封装持久化

// src/utils/storageByUI.js
import localforage from 'localforage'

// 强制使用 IndexedDB 作为存储驱动
localforage.config({
  name: 'okFactura',
  storeName: 'ui_data',
  description: 'Local storage using IndexedDB',
  driver: localforage.INDEXEDDB
})

/** 设置数据 */
const setItem = (key, value) => {
  const raw = JSON.parse(JSON.stringify(value));  // 脱响应式
  return localforage.setItem(key, raw);
}

/** 获取数据 */
const getItem = (key) => {
  return localforage.getItem(key)
}

/** 删除数据 */
const removeItem = (key) => {
  return localforage.removeItem(key)
}

/** 清空数据 */
const clear = () => {
  return localforage.clear()
}

export default {
  setItem,
  getItem,
  removeItem,
  clear
}

3 实现显隐、列宽 持久化

<template>
  <div class="app-container">
      <!-- 1 引用工具组件,常用组件注册到主进程main.js -->
      <right-toolbar 
          v-model:showSearch="showSearch" 
          @queryTable="getList" 
          @updateStorage="updateStorageByColumns" 
          :columns="columns" 
      />
    ... ...
    <el-table class="table-container" v-loading="loading" :data="printConfigList" @selection-change="handleSelectionChange" 
        <!-- 2 border column-resizable 可调整列宽 -->
      size="small" stripe border column-resizable 
        <!-- 3 header-dragend 更改列宽触发 -->
      @header-dragend="handleColumnWidthChange"
    >
      <el-table-column type="selection" width="50" align="center" />
      <el-table-column label="序号" type="index" align="center" width="50" />
      <el-table-column label="模板类型" align="center" prop="templateType"
          <!-- 4 获取持久化列宽 -->
        :width="getColumnWidth(0)" 
          <!-- 5 获取持久化显隐状态 -->
        v-if="columns[0]?.visible"
       >
        <template #default="scope">
          <dict-tag :options="print_template_type" :value="scope.row.templateType"/>
        </template>
      </el-table-column>
      ... ...
    </el-table>
    
    ... ...

  </div>
</template>

<script setup name="PrintConfig">
... ...
// 引入封装的持久化
import storageByUI from "@/utils/storageByUI";
... ...

// ----------------------------------- 列宽设计 start -----------------------------------
const columns = ref([])
const columnsKey = "printConfigColumns"
// 6 prop-控制索引,visible-显隐状态,width-列宽
const initColumns = [
  { key: 0, label: `模板类型`, prop:'templateType', visible: true ,  width: 80 },
  { key: 1, label: `绑定的模板名称`, prop:'templateName', visible: true, width: 120 },
  { key: 2, label: `打印宽度`, prop:'templateWidth', visible: true , width: 80 },
  { key: 3, label: `打印高度`, prop:'templateHeight', visible: true , width: 80 },
  { key: 4, label: `更新时间`, prop:'updateTime', visible: true , width: 120 },
  { key: 5, label: `更新人`, prop:'updateBy', visible: true , width: 80 },
  { key: 6, label: `备注描述`, prop:'remark', visible: true , width: 120 }
]

/** 7 获取持久化列信息 */
function getColumns() { 
  storageByUI.getItem(columnsKey).then(storeColumns => { 
    if(storeColumns) { 
      columns.value = storeColumns;
    } else { 
      columns.value = initColumns;
    }
  })
}
getColumns()

// 8 更新持久化列信息
function updateStorageByColumns(){
  storageByUI.setItem(columnsKey, columns.value);
}

/** 9 修改列宽 */
function handleColumnWidthChange(newWidth, oldWidth, column, event){
  console.log("修改列宽......",column,newWidth)
  // 9.1 找到对应的列索引
  const columnIndex = columns.value.findIndex(col => col.prop === column.property);
  if (columnIndex !== -1) {
    // 9.2 更新列宽
    columns.value[columnIndex].width = newWidth;
    // 9.3 存储更新后的列配置
    updateStorageByColumns();
  }
};

// 10 获取列宽方法
const getColumnWidth = (index) => {
  if (columns.value[index] && columns.value[index].width) {
    return columns.value[index].width;
  }
  return undefined; // 返回undefined让element-ui使用默认宽度
};

// ----------------------------------- 列宽设计 end -----------------------------------
... ...

前端会用到的数据结构--堆(HEAP)

堆是什么?

  1. 完全二叉树

    1. 除最后一层外,每层都填满
    2. 用数组存储非常高效,父子节点下标有规律
  2. 堆顶始终是最大或最小的数,便于快速取出。

应用场景

1️⃣ 堆排序(Heap Sort)

原理:构建一个最大堆,每次取堆顶元素(最大),放到数组末尾,重复堆化。

时间复杂度:O(n log k)

📌 应用:

  • 在一万条数据里,取最大的10个数
  • React 内部 reconciler 中有对 fiber list 的优先级调整过程(虽然更复杂)

2️⃣ React 的 scheduler 中的最小堆队列

React 17+ 中的 scheduler 模块用于调度更新任务,其核心是一个时间驱动的优先级队列

源码地址:

js
复制编辑
const taskQueue = new HeapQueue();   // 按照 expirationTime 排序(时间优先)
const timerQueue = new HeapQueue();  // 按照 startTime 排序(定时任务)

这里使用的是 最小堆,以保证:

  • 取出最早过期或最早开始的任务是 O(1)
  • 插入/更新任务是 O(log n)

📌 相关概念:

  • expirationTime 决定任务的紧急程度(Lanes机制)
  • startTime 控制任务延迟开始(如 setTimeout

3️⃣ 前端动画调度器(如 requestIdleCallback polyfill)

浏览器的 requestIdleCallback 用于在浏览器空闲时间运行回调,类似的调度器内部也常用最小堆存储任务(按照预定的执行时间排序),例如:

js
复制编辑
[
  { callback: fn1, time: 158000001 },
  { callback: fn2, time: 158000003 }
]

每一帧取出堆顶执行任务(即将过期的),其余任务继续等待。


4️⃣ 优先级任务队列 / 限流工具

你实现一个并发请求池(如 5 并发),但需要支持优先级调度,可用最大堆实现优先级队列:

js
复制编辑
class Task {
  constructor(priority, callback) { ... }
}

堆顶是优先级最高的任务,先执行它。

📌 应用:

  • 文件上传任务队列(优先级)
  • 渲染任务(长列表懒加载)

5️⃣ 前端游戏 / 动画帧控制系统

在复杂的 WebGL、canvas 动画或游戏循环中,需要按执行时间调度任务帧 —— 最小堆非常适合做这种“时间轴排序”。

例如:

js
复制编辑
scheduleAt(time, callback)

内部将任务按时间压入最小堆,当前帧时间到了就从堆顶拿任务执行。


为什么前端常用“最小堆”?

  • 因为前端调度大多数以**“时间驱动”或“优先级最小者先执行”**为主:

    • 比如:任务最早执行、超时最早发生、优先级最小者最紧急
  • 所以最小堆正好满足这个模型:堆顶是最小值,取用只要 O(1)


如何实现堆?

  • peek函数: 查看堆的顶点, 也就是优先级最高的tasktimer.
  • pop函数: 将堆的顶点提取出来, 并删除顶点之后, 需要调用siftDown函数向下调整堆.
  • push函数: 添加新节点, 添加之后, 需要调用siftUp函数向上调整堆.
  • siftDown函数: 向下调整堆结构, 保证数组是一个最小堆.
  • siftUp函数: 当插入节点之后, 需要向上调整堆结构, 保证数组是一个最小堆.

总结

面试官问:堆在前端有什么应用?

在前端开发中,堆主要应用在需要快速获取“最小/最大值”的任务调度场景中。例如:

  • React 中的 scheduler 模块使用最小堆管理任务队列,以 O(1) 获取优先级最高的任务
  • requestIdleCallback 的 polyfill 实现中,也会用最小堆安排任务执行时间
  • 自定义的并发任务队列(如上传、抓取)也可用最大堆做优先级控制
  • 当然,堆排序本身也可以用来实现稳定的 O(n log k) 排序

因为堆能在 log(n) 时间内插入任务,O(1) 获取最小值,所以非常适合调度类系统。

深入理解JavaScript的Reflect API:从原理到实践

引言:为什么需要Reflect?

在JavaScript的发展历程中,ES6(ECMAScript 2015)引入了一个重要但容易被忽视的全局对象——Reflect。这个对象不是构造函数,不能使用new操作符创建实例,而是提供了一系列静态方法,用于操作对象、属性和函数调用等底层功能。

Reflect的出现并非偶然,它代表了JavaScript语言设计理念的演进:减少魔法,让代码更加透明。在ES5及之前版本中,很多操作(如deletein等)是作为运算符存在的,它们的行为难以预测且无法直接复用。ES6将这些底层操作提取为Reflect的API方法,使开发者能够以更规范、更可控的方式操作对象。

一、Reflect的核心概念

1.1 什么是Reflect?

Reflect是一个内置的JavaScript全局对象,它提供了一系列拦截JavaScript操作的方法。这些方法与Proxy对象的处理器方法一一对应,使得开发者能够更容易地自定义对象的基本操作。

Reflect的设计借鉴了其他语言的反射机制,因此得名。反射是指程序在运行时能够检查、修改自身状态和行为的能力。在JavaScript中,Reflect对象就是这种能力的体现。

1.2 Reflect的设计哲学

Reflect的出现基于三个核心设计原则:

  1. 统一对象操作方法:将分散的操作符(如deletein)和Object的方法统一到Reflect中
  2. 与Proxy完美配合:Reflect的方法与Proxy的handler方法一一对应,便于在代理中调用默认行为
  3. 更合理的返回值:Reflect方法返回布尔值表示操作是否成功,而非像操作符那样可能抛出异常
// 传统方式 vs Reflect方式
// 删除属性
delete obj.name; // 传统方式,无返回值
Reflect.deleteProperty(obj, 'name'); // Reflect方式,返回布尔值

// 检查属性
'name' in obj; // 传统方式
Reflect.has(obj, 'name'); // Reflect方式

二、Reflect的核心API详解

Reflect提供了13个静态方法,几乎涵盖了所有常见的对象操作。下面我们分类介绍这些方法的核心用法。

2.1 属性操作相关方法

2.1.1 Reflect.get(target, propertyKey[, receiver])

获取对象属性的值,类似于target[propertyKey],但提供了更灵活的控制。

const obj = { name: 'Alice' };
console.log(Reflect.get(obj, 'name')); // "Alice"

// 支持getter和receiver
const objWithGetter = {
  _name: 'Bob',
  get name() {
    return this._name;
  }
};
const receiver = { _name: 'Carol' };
console.log(Reflect.get(objWithGetter, 'name', receiver)); // "Carol"

2.1.2 Reflect.set(target, propertyKey, value[, receiver])

设置对象属性的值,类似于target[propertyKey] = value,但返回布尔值表示是否成功。

const obj = {};
Reflect.set(obj, 'name', 'Alice'); // true
console.log(obj.name); // "Alice"

// 数组操作
const arr = [];
Reflect.set(arr, 0, 'first'); // true
console.log(arr); // ["first"]

2.1.3 Reflect.has(target, propertyKey)

检查对象是否包含某属性,类似于propertyKey in target

const obj = { name: 'Alice' };
console.log(Reflect.has(obj, 'name')); // true
console.log(Reflect.has(obj, 'age')); // false

2.1.4 Reflect.deleteProperty(target, propertyKey)

删除对象属性,类似于delete target[propertyKey],但返回布尔值。

const obj = { name: 'Alice', age: 25 };
Reflect.deleteProperty(obj, 'age'); // true
console.log(obj); // { name: "Alice" }

2.1.5 Reflect.defineProperty(target, propertyKey, attributes)

定义或修改对象属性,类似于Object.defineProperty(),但返回布尔值而非抛出异常。

const obj = {};
const success = Reflect.defineProperty(obj, 'name', {
  value: 'Alice',
  writable: false
});
console.log(success); // true
console.log(obj.name); // "Alice"

2.2 函数调用相关方法

2.2.1 Reflect.apply(target, thisArgument, argumentsList)

调用函数并绑定this值和参数列表,类似于Function.prototype.apply.call(target, thisArgument, argumentsList)的简化版。

function greet(name) {
  return `Hello, ${name}!`;
}

const result = Reflect.apply(greet, null, ['Alice']);
console.log(result); // "Hello, Alice!"

// 数学函数应用
const max = Reflect.apply(Math.max, null, [1, 3, 2]);
console.log(max); // 3

2.2.2 Reflect.construct(target, argumentsList[, newTarget])

以构造函数方式创建对象,类似于new target(...argumentsList),但更灵活。

function Person(name) {
  this.name = name;
}

const p = Reflect.construct(Person, ['Alice']);
console.log(p instanceof Person); // true
console.log(p.name); // "Alice"

// 指定不同的newTarget
function Animal() {}
const personAsAnimal = Reflect.construct(Person, ['Alice'], Animal);
console.log(personAsAnimal instanceof Animal); // true

2.3 原型操作相关方法

2.3.1 Reflect.getPrototypeOf(target)

获取对象的原型,类似于Object.getPrototypeOf()

const parent = { foo: 'bar' };
const child = Object.create(parent);
console.log(Reflect.getPrototypeOf(child) === parent); // true

2.3.2 Reflect.setPrototypeOf(target, prototype)

设置对象的原型,类似于Object.setPrototypeOf(),但返回布尔值。

const obj = {};
const newProto = { foo: 'bar' };
Reflect.setPrototypeOf(obj, newProto); // true
console.log(Reflect.getPrototypeOf(obj) === newProto); // true

2.4 对象扩展性控制

2.4.1 Reflect.isExtensible(target)

检查对象是否可扩展,类似于Object.isExtensible()

const obj = {};
console.log(Reflect.isExtensible(obj)); // true
Reflect.preventExtensions(obj);
console.log(Reflect.isExtensible(obj)); // false

2.4.2 Reflect.preventExtensions(target)

阻止对象扩展,类似于Object.preventExtensions(),但返回布尔值。

const obj = { name: 'Alice' };
Reflect.preventExtensions(obj); // true
obj.age = 25; // 静默失败或TypeError(严格模式)
console.log(obj.age); // undefined

2.5 属性枚举相关方法

2.5.1 Reflect.ownKeys(target)

获取对象所有自身属性键(包括Symbol和不可枚举属性),比Object.keys()更全面。

const obj = {
  name: 'Alice',
  [Symbol('id')]: 123,
  age: 25
};

Object.defineProperty(obj, 'hidden', {
  value: 'secret',
  enumerable: false
});

console.log(Reflect.ownKeys(obj)); 
// ["name", "age", "hidden", Symbol(id)]

三、Reflect与Proxy的完美配合

Reflect API设计的一个重要目的就是与Proxy配合使用。Proxy可以拦截对象操作,而Reflect提供了恢复默认行为的方法。

3.1 基本配合模式

在Proxy的handler中,我们可以使用Reflect来执行默认操作:

const target = { name: 'Alice' };
const handler = {
  get(target, prop, receiver) {
    console.log(`Getting property ${prop}`);
    return Reflect.get(target, prop, receiver);
  },
  set(target, prop, value, receiver) {
    console.log(`Setting property ${prop} to ${value}`);
    return Reflect.set(target, prop, value, receiver);
  }
};

const proxy = new Proxy(target, handler);
proxy.name; // 日志: Getting property name
proxy.age = 25; // 日志: Setting property age to 25

3.2 实现数据验证

结合Proxy和Reflect可以实现强大的数据验证逻辑:

const validator = {
  set(target, prop, value, receiver) {
    if (prop === 'age') {
      if (typeof value !== 'number' || value <= 0) {
        throw new TypeError('Age must be a positive number');
      }
    }
    return Reflect.set(target, prop, value, receiver);
  }
};

const person = new Proxy({}, validator);
person.age = 25; // OK
// person.age = -5; // 抛出TypeError

3.3 实现日志记录

可以轻松记录对象的所有操作:

const logger = {
  get(target, prop, receiver) {
    console.log(`GET ${prop}`);
    return Reflect.get(target, prop, receiver);
  },
  deleteProperty(target, prop) {
    console.log(`DELETE ${prop}`);
    return Reflect.deleteProperty(target, prop);
  }
  // 可以添加其他trap...
};

const config = new Proxy({ apiKey: '123' }, logger);
config.apiKey; // 控制台输出: GET apiKey
delete config.apiKey; // 控制台输出: DELETE apiKey

四、Reflect的实用场景

4.1 统一的操作方式

Reflect提供了一致的API风格,避免了操作符和方法的混用:

// 不一致的传统方式
const obj = { name: 'Alice' };
'name' in obj; // 检查属性
delete obj.name; // 删除属性
Object.defineProperty(obj, 'age', { value: 25 }); // 定义属性

// 一致的Reflect方式
Reflect.has(obj, 'name');
Reflect.deleteProperty(obj, 'name');
Reflect.defineProperty(obj, 'age', { value: 25 });

4.2 更安全的操作

Reflect方法返回布尔值而不是抛出异常,使错误处理更优雅:

// 传统方式可能抛出异常
try {
  Object.defineProperty(obj, 'name', { value: 'Alice' });
} catch (e) {
  console.error('定义属性失败');
}

// Reflect方式更安全
const success = Reflect.defineProperty(obj, 'name', { value: 'Alice' });
if (!success) {
  console.error('定义属性失败');
}

4.3 元编程能力

Reflect为JavaScript提供了强大的元编程能力,可以在运行时检查和修改程序结构:

// 动态调用构造函数
function createInstance(Constructor, args) {
  return Reflect.construct(Constructor, args);
}

class Person {
  constructor(name) {
    this.name = name;
  }
}

const p = createInstance(Person, ['Alice']);
console.log(p.name); // "Alice"

4.4 框架开发

现代前端框架如Vue 3大量使用Reflect和Proxy实现响应式系统:

// 简化的响应式实现
function reactive(target) {
  const handler = {
    get(target, key, receiver) {
      track(target, key); // 追踪依赖
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      const success = Reflect.set(target, key, value, receiver);
      if (success) {
        trigger(target, key); // 触发更新
      }
      return success;
    }
  };
  return new Proxy(target, handler);
}

const state = reactive({ count: 0 });
// 当state.count变化时,自动触发相关更新

五、Reflect的注意事项

5.1 浏览器兼容性

虽然Reflect是ES6标准的一部分,但在一些老旧浏览器中可能需要polyfill。可以使用Babel等工具进行转译。

5.2 性能考虑

Reflect方法通常比直接操作稍慢,但在大多数场景下差异可以忽略。对于性能敏感的代码,应进行基准测试。

5.3 使用建议

  1. 与Proxy配合:在Proxy的trap中优先使用Reflect调用默认行为
  2. 错误处理:利用Reflect的布尔返回值进行错误处理,而非try/catch
  3. 代码一致性:在项目中统一使用Reflect或传统方式,避免混用
  4. 明确语义:选择最能表达意图的API,如Reflect.has()in操作符更明确

六、总结

Reflect API是JavaScript元编程的重要工具,它:

  1. 统一了对象操作方式,将分散的操作符和方法整合为一致的API
  2. 增强了代码的可控性,通过布尔返回值而非异常处理错误
  3. 与Proxy完美配合,为高级抽象和框架开发提供了基础
  4. 实现了真正的反射能力,使JavaScript具备了运行时检查和修改程序结构的能力

在现代JavaScript开发中,Reflect已经成为框架开发、高级抽象和元编程的基础工具。虽然日常业务代码中可能不常直接使用,但理解其原理和设计思想对于掌握JavaScript语言精髓至关重要。

随着JavaScript语言的演进,Reflect API可能会进一步扩展,为开发者提供更强大的元编程能力。建议开发者关注MDN文档和ECMAScript提案,及时了解最新发展。

七、Reflect API MDN

developer.mozilla.org/zh-CN/docs/…

前端实战优化:用语义化映射替代 if-else,告别魔法数字的心智负担

在一个主页面中(如 /workspace/detail),我们根据 URL 中的关键词或参数,动态渲染不同的子视图组件,而不是整个页面跳转或路由切换。采用的是组件映射渲染的方式,而不是 Vue Router 的路由注册机制。这种场景常见于:

  • 详情页中的多视图切换
  • 工作台/控制台页面根据 query 参数加载内容
  • 单页入口统一承载多个业务视图

我们经常会根据 URL 中的关键词来动态渲染不同的视图组件。比如:

  • 访问 /formDetail 显示表单详情组件
  • 访问 /documentPreview 显示文档预览组件
  • 访问 /publicDisplay 显示公示信息组件

最常见的实现方式,就是在 Vue 的逻辑中加上一串 if-else

if (url.includes('formDetail')) {
  this.flag = 2;
} else if (url.includes('documentPreview')) {
  this.flag = 5;
}

在模板中再通过 v-if="flag === x" 去判断显示哪个组件。这是一种很常见的做法,但随着业务页面的增加,这种结构会快速变得冗长、混乱且难以维护

这篇文章将带你从「flag 控制」这种初级做法出发,逐步升级到语义化配置驱动组件渲染逻辑,并深入探讨:为什么初学者会倾向于使用 flag,又该如何自然过渡到更合理的写法。

🧱 初始做法:flag 控制组件渲染

典型写法如下:

if (url.includes('formDetail')) {
  this.flag = 2;
} else if (url.includes('announcementDetail')) {
  this.flag = 3;
}
<form-detail v-if="flag === 2" />
<announcement-detail v-else-if="flag === 3" />

✅ 优点(表象):

  • 写起来简单直观
  • 逻辑线性,适合刚接触前端的程序员
  • “先跑起来”是第一需求

❌ 缺点(本质):

  • 没有语义性,flag = 2 代表什么?
  • 新增页面必须新增编号,并修改多个地方
  • 模板中分支越来越多
  • 不易测试、调试和复用

💡 升级思路:语义化映射配置替代魔法数字

我们不再用 flag = 2,而是直接根据 URL 匹配出一个组件名,用于 component :is="..." 方式动态渲染。

✅ 第一步:定义路由映射配置

const routeMap = [
  { match: ['documentPreview'], component: 'documentPreview' },
  { match: ['formDetail'], component: 'formDetail' },
  { match: ['announcementDetail'], exclude: ['intl'], component: 'announcementDetail' },
  { match: ['messageDetail'], component: 'messageDetail' },
  { match: ['approvalProcess'], component: 'approvalProcess' },
  { match: ['publicDisplay'], exclude: ['intl'], component: 'publicDisplay' },
  { match: ['publicDisplay', 'intl'], component: 'publicDisplayIntl' },
];

✅ 第二步:编写匹配函数

function resolveComponentFromUrl(url) {
  return (
    routeMap.find(rule => {
      const match = rule.match.every(k => url.includes(k));
      const exclude = !(rule.exclude?.some(k => url.includes(k)));
      return match && exclude;
    })?.component || 'mainLayout'
  );
}

✅ 第三步:在 Vue 中使用

data() {
  return {
    componentName: 'mainLayout',
  };
},
mounted() {
  this.componentName = resolveComponentFromUrl(window.location.href);
}

✅ 第四步:模板中动态渲染组件

<component :is="componentName" />

🧩 组件命名参考(中后台语义化)

组件功能 推荐组件名
表单详情页 formDetail
审批流程页 approvalProcess
文档预览页 documentPreview
公告详情页 announcementDetail
公示展示页 publicDisplay
国际化公示页 publicDisplayIntl
消息通知页 messageDetail
默认布局组件 mainLayout

⚖️ 方案对比总结

比较维度 flag 写法 语义映射配置写法
可读性 ❌ 低,需记住编号含义 ✅ 高,组件名即语义
扩展性 ❌ 差,每次加页面都改逻辑 ✅ 好,配置新增即可
维护成本 ❌ 高 ✅ 低,集中式配置
模板复杂度 ❌ 多个 v-if 分支 ✅ 一个 :is 动态渲染
类型提示 ❌ 无 ✅ 可使用 enum 做类型标注

🤔 番外思考:为什么很多初级程序员会用 flag?

这其实是个值得深挖的现象。

✅ 原因一:线性逻辑更符合新手的认知模型

新手写代码更习惯按步骤走:

判断 → 设置状态 → 由状态控制视图

而不是:

判断 → 直接映射视图组件

flag = 2 看似冗余,其实是对他们“掌控状态变化”的一种心理缓冲


✅ 原因二:flag 是“中介变量”,让流程显得更“清晰”

if (url.includes('xxx')) {
  this.flag = 3;
}

再在模板中:

<xxx-view v-if="flag === 3" />

这种拆两步的写法让新手觉得:

“我设置了状态,就像按了个按钮,然后组件就出现了。”

➡️ 它降低了“思维跳跃成本”,哪怕带来了冗余。


✅ 原因三:教程和业务代码中经常出现这种用法

很多低质量的教程会用 flag 教页面切换逻辑,久而久之,很多新手就以为这就是“正确方式”。


🧠 如何引导走出 flag 依赖?

  1. 先允许写 flag,但逐步加入语义常量替代魔法数字
const VIEW = {
  DETAIL: 'formDetail',
  PREVIEW: 'documentPreview',
};
this.componentName = VIEW.DETAIL;
  1. 展示 flag 写法带来的维护成本

    • 修改多处
    • 含义不明确
    • 不利调试和日志追踪
  2. 用映射配置逐步替代分支判断

📌 总结

flag 并不是错,而是一个阶段。

语义化映射配置是一种更高级的表达方式,真正实现了**“行为和数据分离”、“判断逻辑集中”、“组件渲染解耦”**的目标。

越早把这些逻辑结构整理清晰,就越容易避免业务规模扩大后的混乱。

🎯 结语

程序员成长的过程,就是不断摆脱“我能让它跑”→“我能让它维护好”的转变。
配置驱动、语义映射这些看似简单的技术手段,背后是你对代码结构、维护成本和团队协作的深度思考。

Vue 模板引用(ref)全面指南:从基础到高级应用

在 Vue 开发中,虽然声明性渲染模型为我们抽象了大部分 DOM 操作,但在某些场景下,我们仍然需要直接访问底层 DOM 元素或子组件实例。Vue 提供的 ref attribute 正是为解决这类需求而生,它允许我们在元素或组件挂载后获取其直接引用,实现更精细的控制。本文将深入探讨 Vue 模板引用的各种用法与最佳实践。

一、ref 的基本概念与核心作用

ref 是 Vue 中一个特殊的 attribute,类似于 v-for 中的 key,其核心功能是为 DOM 元素或子组件实例创建引用标识。当元素或组件被挂载到 DOM 后,我们可以通过这些引用执行以下操作:

  • 对 DOM 元素进行焦点设置、动画控制等底层操作

  • 初始化或操作第三方库(如 Chart.js 图表实例)

  • 访问子组件的实例方法与属性

  • 在 v-for 循环中批量获取元素引用

关键特性

  • 引用仅在组件挂载后可用(初次渲染前为 null
  • 支持字符串命名与函数绑定两种方式
  • 在组合式 API 中通过 useTemplateRef 函数获取引用
  • 子组件引用的访问受 <script setup> 私有性限制

二、在组合式 API 中使用 ref

2.1 获取 DOM 元素引用

在组合式 API 中,获取 DOM 元素引用的标准流程如下:

<script setup>
import { useTemplateRef, onMounted } from 'vue'

// 声明与模板中 ref 值匹配的引用
const inputRef = useTemplateRef('input-element')

// 组件挂载后访问引用
onMounted(() => {
  // 对输入框设置焦点
  inputRef.value.focus()
  
  // 操作 DOM 元素属性
  inputRef.value.placeholder = '已通过 ref 初始化'
})
</script>

<template>
  <input ref="input-element" type="text" />
</template>

2.2 TypeScript 类型推断

Vue 对 TypeScript 提供了良好支持,inputRef.value 的类型会根据匹配的元素自动推断:

<script setup lang="ts">
import { useTemplateRef, onMounted } from 'vue'

// TypeScript 自动推断 inputRef.value 为 HTMLInputElement 类型
const inputRef = useTemplateRef('input-element')

onMounted(() => {
  // 类型安全的操作
  inputRef.value.addEventListener('input', (e) => {
    console.log(e.target.value)
  })
})
</script>

三、模板引用的高级使用场景

3.1 监听引用变化

由于引用在组件挂载前为 null,且可能随组件卸载而消失,监听时需进行非空判断:

js

import { useTemplateRef, watchEffect } from 'vue'

const inputRef = useTemplateRef('input-element')

// 响应式监听引用变化
watchEffect(() => {
  if (inputRef.value) {
    // 元素已挂载,执行操作
    inputRef.value.style.border = '2px solid blue'
  } else {
    // 元素未挂载或已卸载
    console.log('元素状态变更,当前为 null')
  }
})

3.2 子组件引用与组件通信

当 ref 应用于子组件时,引用值为子组件实例,可访问其属性与方法:

<!-- 父组件 -->
<script setup>
import { useTemplateRef, onMounted } from 'vue'
import ChildComponent from './ChildComponent.vue'

// 获取子组件实例引用
const childRef = useTemplateRef('child')

onMounted(() => {
  // 调用子组件方法
  childRef.value.doSomething()
  
  // 访问子组件属性
  console.log(childRef.value.dataValue)
})
</script>

<template>
  <ChildComponent ref="child" />
</template>
<!-- 子组件(选项式 API) -->
<script>
export default {
  data() {
    return {
      dataValue: '子组件数据'
    }
  },
  methods: {
    doSomething() {
      console.log('子组件方法被调用')
    }
  }
}
</script>

3.3 <script setup> 组件的引用限制与暴露

使用 <script setup> 的组件默认私有,父组件无法直接访问其属性,需通过 defineExpose 显式暴露:

<!-- 子组件(<script setup>) -->
<script setup>
import { ref } from 'vue'

// 私有属性
const privateData = '不会被暴露'
const exposedData = ref('将被暴露的数据')

// 暴露属性与方法
defineExpose({
  exposedData,
  // ref 会自动解包为原始值
  getExposedValue() {
    return exposedData.value
  }
})
</script>

<template>
  <div>子组件内容</div>
</template>
<!-- 父组件访问暴露的子组件引用 -->
<script setup>
import { useTemplateRef } from 'vue'
import ExposedChild from './ExposedChild.vue'

const childRef = useTemplateRef('exposedChild')

// 组件挂载后访问暴露的属性
console.log(childRef.value.exposedData) // 输出: "将被暴露的数据"
console.log(childRef.value.getExposedValue()) // 输出: "将被暴露的数据"
</script>

四、v-for 中的模板引用(v3.5+)

Vue 3.5 及以上版本支持在 v-for 中获取元素引用数组,这在批量操作元素时非常实用:

<script setup>
import { ref, useTemplateRef, onMounted } from 'vue'

const items = ref([
  { id: 1, name: '项目一' },
  { id: 2, name: '项目二' },
  { id: 3, name: '项目三' }
])

// 获取 v-for 中所有 li 元素的引用数组
const itemRefs = useTemplateRef('listItems')

onMounted(() => {
  // itemRefs.value 是包含所有 li 元素的数组
  console.log('元素数量:', itemRefs.value.length)
  
  // 批量设置样式
  itemRefs.value.forEach((el, index) => {
    el.style.color = index % 2 === 0 ? 'red' : 'blue'
  })
})
</script>

<template>
  <ul>
    <li v-for="item in items" :key="item.id" ref="listItems">
      {{ item.name }}
    </li>
  </ul>
</template>

注意事项

  • ref 数组顺序不一定与源数组完全一致
  • 元素卸载时,对应引用会从数组中移除
  • 可结合 key 提升引用匹配的稳定性

五、函数式模板引用

除了字符串命名,ref 还可绑定为函数,在元素挂载 / 更新 / 卸载时触发:

<script setup>
import { ref } from 'vue'

// 声明 ref 变量存储元素引用
const inputEl = ref(null)

// 函数式 ref 绑定
const setInputRef = (el) => {
  // 元素挂载时 el 为 DOM 实例,卸载时为 null
  inputEl.value = el
  
  if (el) {
    // 元素已挂载,初始化操作
    el.placeholder = '函数式 ref 绑定'
  }
}
</script>

<template>
  <!-- 使用 v-bind:ref 绑定函数 -->
  <input v-bind:ref="setInputRef" type="text" />
  
  <!-- 或使用内联函数 -->
  <input :ref="(el) => { if (el) el.focus() }" />
</template>

函数式 ref 的优势

  • 更灵活的引用赋值逻辑
  • 可在元素卸载时执行清理操作
  • 适合动态绑定场景(如条件渲染元素)

六、注意事项

6.1 避免滥用 ref

  • 优先使用声明式编程:Vue 的核心优势在于声明式渲染,应尽量通过数据驱动视图,而非直接操作 DOM
  • 组件通信首选 props/emit:父子组件交互优先使用标准接口,仅在必要时使用 ref 访问子组件
  • 第三方库集成场景:当需要操作库实例(如表单验证库、图表库)时,ref 是合理的选择

6.2 性能考虑

  • 批量操作 DOM:使用 nextTick 确保在 DOM 更新完成后执行批量操作
  • 避免频繁访问 ref:在循环或高频事件中,可先缓存 ref.value 以提升性能
  • v-for 中慎用函数式 ref:函数式 ref 在每次更新时都会被调用,可能影响性能

6.3 类型安全(TypeScript)

  • 显式标注类型:在复杂场景中,可通过泛型为 useTemplateRef 标注精确类型
  • 使用 defineRef 与 ref:确保 ref 变量的类型推导正确
  • 参考官方类型声明:查看 Vue 类型定义文件,了解内置类型的使用方式

七、总结

ref 作为 Vue 中操作底层元素的重要接口,在保持声明式编程优势的同时,为开发者提供了必要的命令式控制能力。从基础的 DOM 焦点设置,到复杂的子组件通信与第三方库集成,ref 的灵活用法贯穿于各类开发场景。

在实际项目中,建议遵循 "声明式优先,命令式保底" 的原则,合理使用 ref 并结合组合式 API 的其他特性(如 watchEffectonMounted),构建既保持 Vue 特性又满足特定需求的应用。

京东小程序JS API仓颉改造实践

作者:薛猛

本次创新实践为京东小程序团队与华为鸿蒙突击队合作对京东小程序API调用过程进行解析,通过借力仓颉实现小程序性能提升和便捷的开发体验

  1. 背景介绍

京东小程序容器是京东及其关联App的重要组成部分,承载了多种内部和外部业务。其中近期热门的模块秒送外卖、以及常用的买菜、超市店铺、奢侈品店铺等均属于小程序。

编辑

  1. 小程序架构

京东鸿蒙版小程序框架整体如下所示:

编辑

小程序采用双线程架构。即同时存在JS逻辑线程和WebView线程。其中JS逻辑线程(简称JS线程)负责运行JS引擎,执⾏业务逻辑;Webview通常运行在UI主线程,主要包括页面的渲染任务、响应交互事件并发送给JS线程。两个线程可能会启动worker子线程来辅助处理任务。 JS Bridge作为桥梁层,负责处理JS API的调用与派发。整体JS API派发逻辑由Native实现(C++)。当一个JS API调用请求到来后,首先判断该API是否有Native实现,如果没有则调用ArkTS的派发逻辑来调用到原生API实现。


  1. 性能瓶颈分析

小程序页面加载/活跃过程中存在大量的JS API的调用。通常由JS线程发起API调用,经过主线程执行后生成结果数据,再将结果返回。因此,会出现如下性能瓶颈点。

  1. API如Storage(存储)、Systeminfo(系统信息)和Network(网络请求)在启动阶段被调用数十次。集中的API调用导致在主线程上出现排队情况,占用大量主线程时间,无法及时处理webview任务。
  2. 由于线程之间数据不共享,跨线程的数据传递需要序列化和反序列化操作,响应延迟增大
  3. JS Bridge在与原生交互时需要调用大量的nAPI接口,创建nAPI线程,处理效率较低,开发维护成本大。API有数百个,如果全部使用Native处理多线程共享问题,开发维护成本高,可行性极低

编辑

  1. 原生JS API调用过程拆解

本次选取京东小程序中执行频率最高的getSystemInfo API为例,展开拆解一次API调用的详细流程:

编辑

getSystemInfo目前是由ArkTS实现。调用由JS线程提交给主线程,主线程启用taskpool执行后再接受返回结果,等待过程中可以流水线执行下一个API。可以看到单次JS API调用会占据主线程时间来处理。当API被频繁调用时,主线程无法及时处理其他webview任务

  1. 场景分析

对京东部分小程序场景采集trace分析。以路易威登LV店铺为例,该店铺小程序在冷启动过程约2800ms,其中有900ms-1000ms的时间在webview任务中穿插这大量的getSystemInfo API的调用。经过检查代码,虽然已经根据页面hashcode缓存了该API的结果数据,但每次获取缓存值仍需要占据主线程时间,因此在该场景上可以进行仓颉API改造尝试

  1. 仓颉改造实践

仓颉的部分特性在该场景性能优化上起到了决定性作用,其中包括:

1.作为鸿蒙系统官方语言之一,具备完善的鸿蒙系统的API能力,实现1比1改写原生API的可行性
2.仓颉具备线程池能力。仓颉语言的JS API执行不占用主线程时间。仓颉线程间具备天然的内存共享能力,省去序列化和反序列化开销
3.仓颉具备高效的与C语言互操作的能力。C语言与仓颉代码互相调用只需要声明和使用,代码简单,执行快,不需要调用nAPI协议接口 
4.仓颉具备高效的与ArkTS互操作的能力,在需要时,可以获取到ArkTS语言的runtime,webview等信息,保证API执行的逻辑正确性

使用仓颉改写后的JS API执行流程如下图:

编辑

本次改造在小程序架构上重点涉及到两个模块。在JSBridge中加入仓颉API的派发逻辑,和在JS API中加入仓颉实现的JS API(getSystemInfo),如下图

编辑

  1. 仓颉穿刺优化效果

经过测试,仓颉改造后的getSystemInfo执行时间可缩短50%+,且不占用主线程时间。在LV小程序上进行冷启动端到端测试,启动性能可提升20%。 (约500ms+)

  1. 未来规划与展望

本次初步验证证明了在小程序场景,仓颉具备高效性能和开发潜力。本次验证也打造了仓颉JS API派发框架,未来基于此框架可以更快的扩展仓颉JS API。计划在以下方面进一步扩展验证仓颉:

1.更多API的仓颉化:尝试改造更多高频调用的JS API。并在京东近期更多热门场景(如秒送外卖)验证性能收益。

2.仓颉并发能力: 基于仓颉的高并发能力,尝试优化网络模块的性能,接入更多小程序

3.小程序其他阶段仓颉化探索: 除了JS API,探索小程序场景更多阶段的仓颉优化

4.在新场景使用仓颉开发:利用仓颉的性能和安全优势,将仓颉用于新模块新功能的开发。仓颉的高并发能力在AI、跨屏应用和高性能Web场景中具有广阔前景

5.跨平台支持:仓颉支持HarmonyOS、Android、iOS和PC平台,未来将完善京东跨端通用能力,降低多平台开发的复杂性。

❌