普通视图

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

Javascript的Iterator和Generator

作者 xun_xing
2025年11月14日 15:28

Iterator和Generator

Iterator

简介

遍历器:Iterator是一种机制。可以把它理解成一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

Iterator 的作用有三个:

  • 一是为各种数据结构,提供一个统一的、简便的访问接口;
  • 二是使得数据结构的成员能够按某种次序排列;
  • 三是 ES6 创造了一种新的遍历命令for...of循环,Iterator 接口主要供for...of消费。

机制

Iterator 的遍历过程是这样的。

(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。

(2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。

(3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。

(4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。

每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value和done两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。

下面是一个模拟next方法返回值的例子:

    const arr = [1, 2, 3, 4, 5];
    function myIterator(array) {
        let nextIndex = 0;
        return {
            next: function () {
                return nextIndex < array.length ? { value: arr[nextIndex++], done: false } : { value: undefined, done: true };
            }
        }
    }
    const it = myIterator(arr);
    it.next(); // {value: 1, done: false}
    it.next(); // {value: 2, done: false}
    it.next(); // {value: 3, done: false}
    it.next(); // {value: 4, done: false}
    it.next(); // {value: 5, done: false}
    it.next(); // {value: undefined, done: true}

当然 Iterator 只是把接口规格加到数据结构之上,所以,遍历器与它所遍历的那个数据结构,实际上是分开的。

默认 Iterator 接口

当使用for...of循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口。一种数据结构只要部署了 Iterator 接口,我们就称这种数据结构是“可遍历的”(iterable)。

ES6 的有些数据结构原生具备 Iterator 接口(比如数组),即不用任何处理,就可以被for...of循环遍历。原因在于,这些数据结构原生部署了Symbol.iterator属性,另外一些数据结构没有(比如对象)。凡是部署了Symbol.iterator属性的数据结构,就称为部署了遍历器接口。调用这个接口,就会返回一个遍历器对象。Symbol.iterator在任何作用域使用值都一样。

原生具备 Iterator 接口的数据结构如下:

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象

下面的例子是数组的Symbol.iterator属性:

    const arr = [1, 2, 3, 4, 5];
    let iter = arr[Symbol.iterator]();
    it.next(); // {value: 1, done: false}
    it.next(); // {value: 2, done: false}
    it.next(); // {value: 3, done: false}
    it.next(); // {value: 4, done: false}
    it.next(); // {value: 5, done: false}
    it.next(); // {value: undefined, done: true}

综上表现,本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。

实现一个 Iterator 接口

一个对象如果要具备可被for...of循环调用的 Iterator 接口,就必须在Symbol.iterator的属性上部署遍历器生成方法(原型链上的对象具有该方法也可)。

class RangeIterator {
  constructor(start, stop) {
    this.start = start;
    this.stop = stop;
  }
  [Symbol.iterator]() {
    return this;
  }
  next() {
    let start = this.start;
    if (start <= this.stop) {
      this.start ++;
      return { value: start, done: false };
    }
    return { value: undefined, done: true };
  }
}

const obj = new RangeIterator(1, 5);
const ite = obj[Symbol.iterator]();
ite.next(); // { value: 1, done: false }
ite.next(); // { value: 2, done: false }
ite.next(); // { value: 3, done: false }
ite.next(); // { value: 4, done: false }
ite.next(); // { value: 5, done: false }
ite.next(); // { value: 6, done: false }

遍历器对象的return、throw

遍历器对象除了具有next()方法,还可以具有return()方法和throw()方法。如果你自己写遍历器对象生成函数,那么next()方法是必须部署的,return()方法和throw()方法是否部署是可选的。

return()方法的使用场合是,如果for...of循环提前退出(通常是因为出错,或者有break语句),就会调用return()方法。如果一个对象在完成遍历前,需要清理或释放资源,就可以部署return()方法。

调用 Iterator 接口的场合

  • 数组和 Set 结构进行解构赋值时,会默认调用Symbol.iterator

  • 扩展运算符(...)也会调用默认的 Iterator 接口(某个数据结构部署了 Iterator 接口,就可以对它使用扩展运算符)

  • yield* 后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口

  • 数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口,如下:

  1. for...of
  2. Array.from()
  3. Map(), Set(), WeakMap(), WeakSet()(比如new Map([['a',1],['b',2]]))
  4. Promise.all()
  5. Promise.race()

Generator

简介

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

机制

形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。

function* testGenerator() {
  yield 1;
  yield 2;
  return 3;
}
let test = testGenerator();
test.next(); // { value: 1, done: false }
test.next(); // { value: 2, done: false }
test.next(); // { value: 3, done: true }
test.next(); // { value: undefined, done: true }

上面代码定义了一个 Generator 函数testGenerator,它内部有三个yield表达式(123),即该函数有四个状态:1,2,3 和 return 语句(结束执行)。

然后,Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是上面介绍的遍历器对象Iterator。下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。

yield 表达式

由于 Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。

遍历器对象的next方法的运行逻辑如下。

  • 遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。

  • 下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。

  • 如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。

  • 如果该函数没有return语句,则返回的对象的value属性值为undefined。

yield表达式只能用在 Generator 函数里面,用在其他地方都会报错。

function* testGenerator() {
  // 可以在for循环中,但是不是在forEach等函数中
  for (let i = 0;;i ++) {
    if (i === 3) {
      return i;
    }
    yield i;
  }
}
let test = testGenerator();
test.next(); // { value: 0, done: false }
test.next(); // { value: 1, done: false }
test.next(); // { value: 2, done: true }
test.next(); // { value: undefined, done: true }

yield表达式如果用在另一个表达式之中,必须放在圆括号里面。

function* testGenerator() {
  for (let i = 0;i < 3;i ++) {
    console.log('next:' + (yield i));
  }
}
let test = testGenerator();
test.next(); 
// 先:{ value: 0, done: false }
// 后:next:undefined

yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号。

function* testGenerator() {
  testFunc(yield 1);
  let a = yield 2; // { value: 2, done: false }
  console.log('next:', a); // next: undefined
  yield 3;
}
let test = testGenerator();
test.next(); // 先 { value: 1, done: false },后 test: undefined
test.next(); // { value: 2, done: false }
test.next(); // 先 next: undefined,后 { value: 3, done: false }

与 Iterator 接口的关系

任意一个对象的Symbol.iterator方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具有 Iterator 接口。

let obj = {};
obj[Symbol.iterator] = function* () {
  yield 1;
  yield 2;
  yield 3;
}
console.log([...obj]); // [1, 2, 3]
// 具有Symbol.iterator属性,即可使用拓展运算符

Generator 函数执行后,返回一个遍历器对象。该对象本身也具有Symbol.iterator属性,执行后返回自身。

function* test() {
  yield true;
}
const iter = test();
console.log(iter[Symbol.iterator]() === iter); // true

next 方法的参数

上面的示例中yield 2本身并没有返回值,即为undefined。next方法可以带一个参数,该参数就会被当作上一个状态yield表达式的返回值。

function* test() {
  let res1 = yield 1;
  console.log(res1);
  let res2 = yield 2;
  console.log(res2);
  let res3 = yield 3;
  console.log(res3);
}
const iter = test();
iter.next('a'); // { value: 1, done: false }
iter.next('b'); // 先 b,后 { value: 2, done: false }
iter.next('c'); // 先 c,后 { value: 3, done: false }
iter.next('d'); // 先 d,后 { value: undefined, done: true }

所以正常情况来说,一个Generator函数中,第一个yield传递参数是没有作用的,因为并没有上一个状态去接收它的参数。

for...of 循环

for...of循环可以自动遍历 Generator 函数运行时生成的Iterator对象,且此时不再需要调用next方法。

function* test() {
  yield 1;
  yield 2;
  yield 3;
}

for (let key of test()) {
  console.log(key); // 1 2 3
}

throw 和 return

Generator 函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。

function* test() {
  try {
    yield 1;
  } catch (error) {
    console.log('test:', error);
  }
  yield console.log(2);
}
const iter = test();
iter.next(); // { value: 1, done: false }

try {
  iter.throw(new Error('出错了!')); // test: Error: 出错了!   2
  iter.throw(new Error('出错了!')); // catch: Error: 出错了!
} catch (error) {
  console.log('catch:', error);
}

上面代码中,遍历器对象iter连续抛出两个错误。第一个错误被 Generator 函数体内的catch语句捕获(前提是必须至少执行过一次next方法)。iter第二次抛出错误,由于 Generator 函数内部的catch语句已经执行过了,不会再捕捉到这个错误了,所以这个错误就被抛出了 Generator 函数体,被函数体外的catch语句捕获。

throw方法可以接受一个参数,该参数会被catch语句接收,建议抛出Error对象的实例。throw方法被捕获以后,会附带执行下一条yield表达式。也就是说,会附带执行一次next方法。

function* test() {
  yield 1;
  yield 2;
  yield 3;
} 
const iter = test();

iter.next(); // { value: 1, done: false }
iter.return('end'); // { value: 'end', done: true }
iter.next(); // { value: undefined, done: true }

上面代码中,遍历器对象调用return()方法后,返回值的value属性就是return()方法的参数。并且,Generator 函数的遍历就终止了,返回值的done属性为true,以后再调用next()方法,done属性总是返回true.如果return()方法调用时,不提供参数,则返回值的value属性为undefined。

function* test() {
  yield 1;
  try {
    yield 'try';
  } finally {
    yield 'finally';
  }
  yield 3;
} 
const iter = test();

iter.next(); // { value: 1, done: false }
iter.next(); // { value: 'try', done: false }
iter.return('end'); // { value: 'finally', done: false }
iter.next(); // { value: 'end', done: true }

如果 Generator 函数内部有try...finally代码块,且正在执行try代码块,那么return()方法会导致立刻进入finally代码块,执行完以后,整个函数才会结束。

yield* 表达式

如果在 Generator 函数内部,调用另一个 Generator 函数。需要在前者的函数体内部,自己手动完成遍历。

如下:

function* a() {
  yield 1;
  yield 2;
}

function* b() {
  yield 'a';
  for (let i of a()) {
    console.log(i);
  }
  yield 'b';
}
for (let j of b()) {
  console.log(j);
}
// a 1 2 b

yield*表达式,作为解决办法,用来在一个 Generator 函数里面执行另一个 Generator 函数。简化上述代码

function* a() {
  yield 1;
  yield 2;
}

function* b() {
  yield 'a';
  yield* a();
  yield 'b';
}
for (let j of b()) {
  console.log(j);
}
// a 1 2 b

可以通过yield*实现多层数组的扁平化处理,如下:

function* iterTree(tree) {
  if (Array.isArray(tree)) {
    for(let item of tree) {
      yield* iterTree(item);
    }
  } else {
    yield tree;
  }
}
const arr = [1, 2, ['a', 'b'], 4, ['name', 'age']];
console.log([...iterTree(arr)]);
// [1, 2, 'a', 'b', 4, 'name', 'age'];

VNBarcodeObservation的结果中observation.boundingBox 是什么类型?

作者 1024小神
2025年11月14日 15:20

大家好,我的开源项目PakePlus可以将网页/Vue/React项目打包为桌面/手机应用并且小于5M只需几分钟,官网地址:pakeplus.com

observation.boundingBox 的类型是 CGRect

CGRect 结构

CGRect 是 Core Graphics 框架中的结构体,表示一个矩形区域:

public struct CGRect {
    public var origin: CGPoint
    public var size: CGSize
}

在 Vision 框架中的特性

在 Vision 框架中,boundingBox 使用归一化坐标系统

let barcodeRequest = VNDetectBarcodesRequest { request, error in
    guard let results = request.results as? [VNBarcodeObservation] else { return }
    
    for observation in results {
        let boundingBox: CGRect = observation.boundingBox
        print("boundingBox: \(boundingBox)")
        
        // 访问具体属性
        print("原点: \(boundingBox.origin)")      // CGPoint
        print("尺寸: \(boundingBox.size)")        // CGSize
        print("x: \(boundingBox.origin.x)")      // CGFloat
        print("y: \(boundingBox.origin.y)")      // CGFloat
        print("宽度: \(boundingBox.size.width)")   // CGFloat
        print("高度: \(boundingBox.size.height)")  // CGFloat
        
        // 其他便捷属性
        print("最小X: \(boundingBox.minX)")
        print("最小Y: \(boundingBox.minY)")
        print("最大X: \(boundingBox.maxX)")
        print("最大Y: \(boundingBox.maxY)")
        print("中心X: \(boundingBox.midX)")
        print("中心Y: \(boundingBox.midY)")
    }
}

归一化坐标系统

Vision 框架的 boundingBox 使用归一化坐标:

  • 坐标范围: (0.0, 0.0) 到 (1.0, 1.0)

  • 原点: 左下角 (与 UIKit 的左上角不同!)

  • x: 从左到右 (0.0 = 左边缘, 1.0 = 右边缘)

  • y: 从下到上 (0.0 = 下边缘, 1.0 = 上边缘)

// 示例 boundingBox 值
let exampleBox = CGRect(x: 0.2, y: 0.3, width: 0.4, height: 0.3)
// 表示:
// - 从图像左边 20% 的位置开始
// - 从图像底部 30% 的位置开始  
// - 宽度为图像的 40%
// - 高度为图像的 30%

坐标转换

由于归一化坐标和 UIKit 坐标系统不同,需要进行转换:

1. 转换为图像像素坐标

func convertToImageCoordinates(boundingBox: CGRect, imageSize: CGSize) -> CGRect {
    // Vision → 图像坐标 (左下角原点)
    let imageRect = VNImageRectForNormalizedRect(
        boundingBox, 
        Int(imageSize.width), 
        Int(imageSize.height)
    )
    return imageRect
}

2. 转换为 UIKit 视图坐标

func convertToViewCoordinates(boundingBox: CGRect, viewSize: CGSize) -> CGRect {
    // Vision → UIKit 坐标 (左上角原点)
    let viewRect = CGRect(
        x: boundingBox.origin.x * viewSize.width,
        y: (1 - boundingBox.origin.y - boundingBox.size.height) * viewSize.height,
        width: boundingBox.size.width * viewSize.width,
        height: boundingBox.size.height * viewSize.height
    )
    return viewRect
}

完整的使用示例

import Vision
import UIKit

class BarcodeDetector {
    func processBarcodeObservation(_ observation: VNBarcodeObservation, 
                                 imageSize: CGSize, 
                                 targetView: UIView) {
        
        let boundingBox: CGRect = observation.boundingBox
        
        // 1. 打印原始 boundingBox
        print("原始 boundingBox: \(boundingBox)")
        
        // 2. 转换为图像坐标
        let imageRect = VNImageRectForNormalizedRect(
            boundingBox,
            Int(imageSize.width),
            Int(imageSize.height)
        )
        print("图像坐标: \(imageRect)")
        
        // 3. 转换为视图坐标 (用于在屏幕上绘制)
        let viewRect = convertToViewRect(boundingBox: boundingBox, 
                                       viewSize: targetView.bounds.size)
        print("视图坐标: \(viewRect)")
        
        // 4. 在界面上绘制边界框
        drawBoundingBox(on: targetView, rect: viewRect)
    }
    
    private func convertToViewRect(boundingBox: CGRect, viewSize: CGSize) -> CGRect {
        return CGRect(
            x: boundingBox.origin.x * viewSize.width,
            y: (1 - boundingBox.origin.y - boundingBox.size.height) * viewSize.height,
            width: boundingBox.size.width * viewSize.width,
            height: boundingBox.size.height * viewSize.height
        )
    }
    
    private func drawBoundingBox(on view: UIView, rect: CGRect) {
        // 移除之前的边界框
        view.layer.sublayers?.removeAll(where: { $0.name == "boundingBox" })
        
        // 创建新的边界框图层
        let boxLayer = CAShapeLayer()
        boxLayer.name = "boundingBox"
        boxLayer.frame = rect
        boxLayer.borderColor = UIColor.green.cgColor
        boxLayer.borderWidth = 2.0
        boxLayer.backgroundColor = UIColor.clear.cgColor
        
        view.layer.addSublayer(boxLayer)
    }
}

重要注意事项

  1. 坐标系统差异: Vision 使用左下角原点,UIKit 使用左上角原点

  2. 归一化范围: 坐标值在 0.0-1.0 范围内

  3. 空矩形检查: 检查 boundingBox 是否有效

  4. 边界处理: 确保转换后的坐标在有效范围内

// 检查 boundingBox 是否有效
if boundingBox.isNull || boundingBox.isInfinite {
    print("无效的 boundingBox")
    return
}

// 检查是否在有效范围内
if boundingBox.minX < 0 || boundingBox.maxX > 1 || 
   boundingBox.minY < 0 || boundingBox.maxY > 1 {
    print("boundingBox 超出有效范围")
}

总结:observation.boundingBox 是 CGRect 类型,使用归一化坐标系统表示检测对象在图像中的位置和大小,需要进行适当的坐标转换才能在 UIKit 界面中使用。

大家好,我是1024小神,技术群 / 私活群 / 股票群 或 交朋友 都可以私信我。 如果你觉得本文有用,一键三连 (点赞、评论、关注),就是对我最大的支持~

函数柯里化(curry)是什么?🤔

2025年11月14日 15:08

什么是函数柯里化?

函数柯里化是一种将多参数函数转换为一系列单参数函数的技术。简单来说,柯里化后的函数不会立即求值,而是每次接受一个参数,并返回一个新函数来接收剩余参数,直到所有参数都被提供,最终返回结果。

基本示例

让我们通过一个简单的例子来理解柯里化:

// 普通函数
function add(a, b, c) {
    return a + b + c;
}

// 柯里化版本
function curriedAdd(a) {
    return function(b) {
        return function(c) {
            return a + b + c;
        };
    };
}

// 使用方式对比
console.log(add(1, 2, 3)); // 6
console.log(curriedAdd(1)(2)(3)); // 6

实现通用的柯里化函数

手动为每个函数编写柯里化版本显然不现实,我们可以创建一个通用的柯里化工具函数: 思路就是创建一个自动柯里化函数可以接收一个函数作为参数,然后返回一个它的柯里化后的函数。

//自动柯里化函数,接收一个函数的参数
const autoCurryFn = function(fn){
    //边界判断
    //是否是函数
    if(typeof fn !== 'function'){
        throw new Error('传进来的参数必须是一个函数')
    }

    //返回一个新的函数,接收新的参数,这里用gras剩余参数收集
    return function curryFn(...args){
        //如果收集的参数个数少于原fn函数的参数个数,则返回这个新函数继续收集
        if(args.length < fn.length){
            return function(...newGras){
                return curryFn(...args,...newGrgs)
            }
        }else{
            //如果收集的参数大于等于原函数的参数就可以执行原函数,并返回对应结果
            return fn(...args)
        }
    }
}

柯里化的实际应用场景

1. 参数复用

柯里化非常适合创建可复用的函数模板:

// 创建特定前缀的日志函数
function createLogger(prefix) {
    return function(message) {
        console.log('[' + prefix + '] ' + message);
    };
}

const infoLogger = createLogger('INFO');
const errorLogger = createLogger('ERROR');

infoLogger('系统启动成功'); // [INFO] 系统启动成功
errorLogger('数据库连接失败'); // [ERROR] 数据库连接失败

2. 延迟执行

柯里化允许我们分步提供参数,这在事件处理等场景中特别有用:

// 事件处理器工厂
function createEventHandler(eventType, element) {
    return function(handler) {
        element.addEventListener(eventType, handler);
    };
}

// 为特定元素创建点击事件处理器
const createClickHandler = createEventHandler('click', document.getElementById('myButton'));

// 稍后添加具体的处理逻辑
createClickHandler(function(event) {
    console.log('按钮被点击了!');
});

总结

函数柯里化其实就是将多参数函数转换为单参数函数序列,为我们提供了更灵活的函数组合方式和更高的代码复用性。

前端规范【五】biomejs自动化工具-ultracite

作者 在泡泡里
2025年11月14日 15:05

前言

感觉和autful/eslint-config 一样, biomejs也有了自己的自动化工具 不过有了新的东西 包括ai工具调用的钩子和 格式化的md 文档

安装

npx ultracite@latest init

然后根据提示 选择自己的配置

image-1.png

配置

如果你需要扩展biomejs 的配置 去继承 ultracite 提供的配置就行了

{
  "extends": ["ultracite/core", "ultracite/react"] 
}

image.png

集成

{
  "scripts": {
    "check": "npx ultracite check --diagnostic-level warn",
    "fix": "npx ultracite@latest fix --unsafe"
  }
}

vscode 插件 需要安装的插件

biomejs tailwindcss unocss

其他

执行命令可以帮你检查你的配置是否正确

npx ultracite doctor

支持

image-2.png

biomejs 也支持vue了 虽然是实验性的

哈哈 目前就等待oxlint + oxfmt 能一键配置了,不过还没支持vue

React项目实战 | 修复Table可展开行,点击一个全部展开

作者 刀疤
2025年11月14日 14:57

问题分析

Table 组件需要唯一的 rowKey 来正确管理展开状态

原因详解

1. 虚拟DOM diff 算法依赖

React 使用虚拟DOM diff算法来高效更新UI。当Table数据更新时,React需要:

  • 识别哪些行是新增的、哪些是删除的、哪些是更新的
  • 正确保持展开/收起状态
  • 如果没有唯一key,React无法准确追踪每行的状态

2. 展开状态管理

Table组件的展开状态是基于 rowKey 来管理的:

// 内部类似这样的结构
expandedRowKeys = ['id1', 'id3', 'id5']

如果没有唯一id,点击展开时可能出现:

  • 多个行同时展开(主包遇到的bug)
  • 展开状态错乱
  • 无法正确收起

3. 性能优化

唯一key帮助React:

  • 减少不必要的重渲染
  • 提高diff算法效率
  • 准确更新特定行的展开状态

解决方案对比

方案一:使用 uuid

import { v4 as uuidv4 } from 'uuid';

const dataWithId = recordData.list.map(item => ({
  ...item,
  id: uuidv4() // 全局唯一
}));

优点:绝对唯一

缺点:需要额外依赖包

方案二:时间戳+随机数

const dataWithId = recordData.list.map(item => ({
  ...item,
  id: `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
}));

优点:无依赖

缺点:极低概率重复(通常可接受)

方案三:使用数据中的唯一字段&字段拼接

// 如果数据本身有唯一字段
<Table rowKey="user_id" />

// 或者组合多个字段、data接口数据
const dataWithId = data.map(item => ({
  ...item,
  id: `${item.user_id}-${item.timestamp}`
}));

最佳实践

// 推荐:优先使用数据中的业务ID
// 
const dataWithId = recordData.list.map((item, index) => ({
  ...item,
  id: item.id || item.user_id || `row-${index}-${Date.now()}`
}));

<Table
  rowKey="id"
  columns={columns}
  expandable={{
    expandedRowRender: record => (
      <p style={{ margin: 0 }}>{record.change_content}</p>
    ),
    rowExpandable: record => record.change_content && record.change_content !== ''
  }}
  dataSource={dataWithId}
  pagination={false}
/>

正常表格数据都应有ID,没有去找后端要(狗头

那话又说回来了,真没招了就选择引入UUID或者时间戳+随机数的方法

总结

根本原因:Table组件依赖唯一的 rowKey 来正确管理每行的展开状态和进行高效的虚拟DOM diff。

修复核心:确保每行数据都有一个唯一标识,让React能够准确追踪和管理每行的状态。

CSS实现边框光点围绕特效

作者 小杨累了
2025年11月14日 14:51

在前端开发中,会遇到一些特殊的视觉效果需求,比如只在指定区域内显示内容。本文将介绍如何通过CSS实现一个"边框光点围绕"的特效,通过运用CSS的层叠上下文和z-index属性,实现只在特定区域显示内容的效果。

实现思路

  1. 创建一个容器作为显示区域
  2. 使用伪元素创建装饰性的边框效果
  3. 使用另一个伪元素作为遮罩,遮挡不需要显示的部分
  4. 通过精确控制z-index层级关系,实现只显示边框区域内容的效果

核心代码

HTML结构

<div class="bottomContentItem"> <div class="main-title">取信于客户 服务于客户</div> <div class="sub-title">讲时效、保质量、重合同、守信誉</div> </div> 

CSS实现

.bottomContentItem { display: flex; background-color: #404040; width: 50%; height: 240px; margin: 30px 0; flex-direction: column; justify-content: center; align-items: center; gap: 40px; color: #FFFFFF; position: relative; overflow: hidden; .main-title { font-size: 3rem; font-weight: bolder; letter-spacing: 0.2rem; opacity: 0; transform-origin: center; transform: scaleX(0); transition: transform 0.8s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.8s ease; z-index: 1; } .sub-title { font-size: 1.5rem; opacity: 0; transform: translateY(20px); transition: all 1s cubic-bezier(0.23, 1, 0.32, 1); transition-delay: 0.3s; z-index: 1; } &::before { content: ''; position: absolute; width: 120%; height: 3.125rem; background: linear-gradient(45deg, rgba(255, 213, 135, .9215686275), rgba(251, 0, 0, .2392156863)); top: 50%; left: 50%; transform: translate(-50%, -50%); animation: effect-btn-borderflow-rotation 6s linear infinite; z-index: 0; } &::after { content: ''; position: absolute; width: calc(100% - 2px); height: calc(100% - 2px); top: 1px; left: 1px; z-index: 0; background-color: #404040; } } 

实际效果

css_border.gif

流程解析

1. 层叠上下文的运用

在这个实现中,我们通过z-index创建了三个层级:

  • 文字内容(z-index: 1):位于最上层,确保用户可以看到
  • 装饰边框(z-index: 0):位于中间层,提供视觉效果
  • 遮罩背景(z-index: 0):与装饰边框同层级,但通过定位覆盖不需要显示的区域

2. 伪元素的使用

我们使用了两个伪元素:

  • ::before:创建旋转的装饰边框效果
  • ::after:创建遮罩,隐藏边框以外的装饰内容

3. 精确的定位控制

&::before { width: 120%; /* 比容器宽20%,确保旋转时边缘不露出 */ height: 3.125rem; /* 固定高度,形成边框效果 */ top: 50%; left: 50%; transform: translate(-50%, -50%); /* 精确居中 */ } &::after { width: calc(100% - 2px); /* 略小于容器,形成边框 */ height: calc(100% - 2px); top: 1px; left: 1px; } 

4. 动画效果

@keyframes effect-btn-borderflow-rotation { 0% { transform: translate(-50%, -50%) rotate(0deg); } 100% { transform: translate(-50%, -50%) rotate(360deg); } } 

实现原理详解

这种效果的实现原理基于以下几点:

  1. 遮罩技术:通过::after伪元素创建一个与容器背景色相同的遮罩层,覆盖整个容器
  2. 局部显示:由于::before伪元素只在垂直居中的窄条区域内显示,其他部分被::after遮罩遮挡
  3. 层级控制:通过z-index控制显示层级,确保文字内容始终可见

实际应用建议

  1. 兼容性考虑:确保目标浏览器支持所需的CSS特性
  2. 性能优化:对于复杂动画,考虑使用transformwill-change属性优化性能
  3. 响应式适配:根据不同屏幕尺寸调整边框高度和定位参数
  4. 可维护性:将颜色值和尺寸定义为CSS变量,便于统一管理

总结

通过运用CSS的层叠上下文、伪元素和定位技术,可以实现复杂的视觉效果。在实际项目中,可以根据需求调整颜色、尺寸和动画效果,创造出更多个性化的界面效果。

这种方法的优势在于:

  • 纯CSS实现,无需额外的JavaScript代码
  • 性能良好,利用浏览器的硬件加速
  • 易于维护,结构清晰
  • 可扩展性强,便于调整样式和效果

代码隔离革命:用 JavaScript Realm 安全运行不可信代码

作者 大知闲闲i
2025年11月14日 14:51

在多年的开发生涯中,我带领团队交付了无数中等规模的外包项目,遇到过各种棘手的技术挑战。但最近在调试一个复杂的多 iframe 应用时,我发现了一个被大多数开发者忽略的 JavaScript 特性,它彻底改变了我对代码安全性的认知。

======================================================================================================================

从一次生产事故说起

想象这个场景:你为客户的xx平台开发了一个插件系统,允许商家编写自定义 JavaScript 来增强店铺功能。一切都很美好,直到某个商家写了这样的代码:

// 某个"创新"商家的插件代码
Array.prototype.push = function() {
    console.log("哈哈,我重写了数组方法!");
    return "搞破坏了";
};

结果如何?整个xx平台的购物车、商品列表、订单系统全线崩溃。这就是典型的全局污染问题——当不可信代码与核心业务共享同一个执行环境时,灾难随时可能发生。

Realm:JavaScript 的隔离解决方案

什么是 Realm?

Realm 是 JavaScript 的隔离执行环境,可以理解为"一套全新的 JavaScript 宇宙"。每个 Realm 都拥有自己独立的内置对象和全局作用域。

// 主环境
console.log(Array); // [Function: Array]

// 创建 iframe(自动生成新 Realm)
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);

// iframe 中的 Array 是全新的构造器
const iframeArray = iframe.contentWindow.Array;

console.log(Array === iframeArray); // false!

看到这个结果时,我团队的小伙伴们都惊呆了。两个 Array 构造器功能完全相同,但却是完全独立的对象,这就是 Realm 的强大之处。

Realm 的组成要素

每个 Realm 都包含完整的运行环境:

  • 全局对象:浏览器中的 window 或 Node.js 中的 global

  • 内置构造器:Array、Object、Function、Error 等

  • 工具函数:setTimeout、fetch、JSON 等

  • 原型对象:Array.prototype、Object.prototype 等基础原型

实战:三种 Realm 实现方案

方案一:ShadowRealm(未来标准)

ShadowRealm 是 TC39 标准提案(Stage 3),专为代码隔离设计:

// 创建隔离环境
const realm = new ShadowRealm();

// 安全执行不可信代码
const result = realm.evaluate(`
    // 这里无法访问主环境的任何变量
    const sensitiveData = "这段数据很安全";
    2 + 2
`);

console.log(result); // 4
console.log(typeof sensitiveData); // undefined(完全隔离)

当前可用 polyfill:

npm install shadowrealm-api

import ShadowRealm from 'shadowrealm-api';

const realm = new ShadowRealm();
const userCodeResult = realm.evaluate(userSuppliedCode);

方案二:iframe 沙箱(生产环境首选)

对于需要立即上线的项目,iframe 是最可靠的解决方案:

function createSafeSandbox() {
    const frame = document.createElement('iframe');
    
    // 关键配置:限制权限
    frame.sandbox = [
        'allow-scripts',     // 允许执行脚本
        // 'allow-same-origin' // 谨慎使用:允许同源访问
    ].join(' ');
    
    frame.style.display = 'none';
    document.body.appendChild(frame);
    
    return {
        evaluate: (code) => frame.contentWindow.eval(code),
        destroy: () => frame.remove()
    };
}

// 使用示例
const sandbox = createSafeSandbox();
try {
    const result = sandbox.evaluate('2 + 2');
    console.log('安全计算结果:', result);
} finally {
    sandbox.destroy();
}

方案三:Web Worker(纯计算场景)

对于 CPU 密集型任务,Web Worker 提供良好的隔离性:

// 创建隔离的工作线程
const workerCode = `
    self.onmessage = function(e) {
        try {
            const result = eval(e.data.code);
            self.postMessage({ success: true, result });
        } catch (error) {
            self.postMessage({ success: false, error: error.message });
        }
    };
`;

const blob = new Blob([workerCode], { type: 'application/javascript' });
const worker = new Worker(URL.createObjectURL(blob));

worker.onmessage = (e) => {
    if (e.data.success) {
        console.log('Worker 计算结果:', e.data.result);
    } else {
        console.error('执行出错:', e.data.error);
    }
};

// 执行用户代码
worker.postMessage({
    code: 'Math.pow(2, 10)' // 用户提供的代码
});

真实案例:插件系统安全改造

我们最近为一家金融科技客户重构了他们的报表插件系统。改造前,第三方插件经常导致整个系统崩溃;改造后,即使插件代码存在问题,也只会影响自身运行。

改造前的问题代码:

// 老系统:直接执行插件代码
function runPlugin(pluginCode) {
    // 危险!插件可以访问和修改全局状态
    return eval(pluginCode);
}

改造后的安全方案:

class PluginSandbox {
    constructor() {
        this.iframe = document.createElement('iframe');
        this.iframe.sandbox = 'allow-scripts';
        document.body.appendChild(this.iframe);
    }
    
    runPlugin(pluginCode, api) {
        // 通过 postMessage 提供有限的 API
        this.iframe.contentWindow.postMessage({
            type: 'EXECUTE',
            code: pluginCode,
            api: api
        }, '*');
        
        return new Promise((resolve) => {
            const handler = (event) => {
                if (event.data.type === 'RESULT') {
                    window.removeEventListener('message', handler);
                    resolve(event.data.result);
                }
            };
            window.addEventListener('message', handler);
        });
    }
}

何时应该使用 Realm 技术?

根据我们的项目经验,以下场景强烈推荐使用 Realm:

  1. 用户代码执行:在线代码编辑器、教学平台

  2. 第三方插件:CMS 系统、电商平台的扩展功能

  3. A/B 测试:隔离不同版本的代码逻辑

  4. 单元测试:确保每个测试用例环境纯净

  5. 微前端架构:隔离不同团队开发的子应用

安全最佳实践

在多个金融级项目中,我们总结出这些安全准则:

// 安全配置示例
const SAFE_SANDBOX_CONFIG = [
    'allow-scripts',        // 必需:执行脚本
    // 'allow-forms',       // 谨慎:表单提交
    // 'allow-popups',      // 谨慎:弹出窗口
    // 'allow-same-origin', // 危险:同源访问
    // 'allow-top-navigation' // 危险:顶级导航
];

// 永远验证输入
function validateCode(code) {
    const blacklist = [
        'document.cookie',
        'localStorage',
        'XMLHttpRequest',
        'fetch',
        'window.parent'
    ];
    
    return !blacklist.some(unsafe => code.includes(unsafe));
}

未来展望

ShadowRealm 标准落地后,JavaScript 代码隔离将变得更加简单高效。我们团队正在密切关注相关进展,并已在几个实验性项目中开始使用 polyfill 版本。


在 Vision 框架中,request.results 是什么类型的数据

作者 1024小神
2025年11月14日 14:48

大家好,我的开源项目PakePlus可以将网页/Vue/React项目打包为桌面/手机应用并且小于5M只需几分钟,官网地址:pakeplus.com

在 Vision 框架中,request.results 的类型是 [VNObservation]?(可选的对象数组)。

基本类型

// request.results 的基本类型
let results: [VNObservation]? = request.results

具体的子类类型

根据不同的 Vision 请求,results 数组中的对象会是不同的 VNObservation 子类:

1. 条码检测 - VNDetectBarcodesRequest

let barcodeRequest = VNDetectBarcodesRequest { request, error in
    // 需要向下转型为具体的类型
    guard let results = request.results as? [VNBarcodeObservation] else { return }
    
    for barcode in results {
        print("条码类型: \(barcode.symbology.rawValue)")
        print("条码内容: \(barcode.payloadStringValue ?? "")")
        print("置信度: \(barcode.confidence)")
        print("边界框: \(barcode.boundingBox)")
    }
}

2. 文字识别 - VNRecognizeTextRequest

let textRequest = VNRecognizeTextRequest { request, error in
    guard let results = request.results as? [VNRecognizedTextObservation] else { return }
    
    for observation in results {
        // 获取识别到的文字
        let topCandidates = observation.topCandidates(1)
        if let recognizedText = topCandidates.first {
            print("识别到的文字: \(recognizedText.string)")
            print("置信度: \(recognizedText.confidence)")
        }
    }
}

3. 人脸检测 - VNDetectFaceRectanglesRequest

let faceRequest = VNDetectFaceRectanglesRequest { request, error in
    guard let results = request.results as? [VNFaceObservation] else { return }
    
    for face in results {
        print("人脸位置: \(face.boundingBox)")
        print("置信度: \(face.confidence)")
    }
}

4. 物体检测 - VNDetectRectanglesRequest

let rectangleRequest = VNDetectRectanglesRequest { request, error in
    guard let results = request.results as? [VNRectangleObservation] else { return }
    
    for rectangle in results {
        print("矩形位置: \(rectangle.boundingBox)")
        print("左上角: \(rectangle.topLeft)")
        print("右上角: \(rectangle.topRight)")
        print("左下角: \(rectangle.bottomLeft)")
        print("右下角: \(rectangle.bottomRight)")
    }
}

完整的类型处理示例

func handleVisionResults(request: VNRequest, error: Error?) {
    if let error = error {
        print("Vision 请求错误: \(error)")
        return
    }
    
    // 首先检查是否有结果
    guard let results = request.results, !results.isEmpty else {
        print("未检测到任何内容")
        return
    }
    
    // 根据请求类型处理不同的结果
    switch request {
    case is VNDetectBarcodesRequest:
        handleBarcodeResults(results as! [VNBarcodeObservation])
        
    case is VNRecognizeTextRequest:
        handleTextResults(results as! [VNRecognizedTextObservation])
        
    case is VNDetectFaceRectanglesRequest:
        handleFaceResults(results as! [VNFaceObservation])
        
    case is VNDetectRectanglesRequest:
        handleRectangleResults(results as! [VNRectangleObservation])
        
    default:
        print("未知的请求类型")
        // 通用处理
        for observation in results {
            print("检测到对象 - 置信度: \(observation.confidence), 位置: \(observation.boundingBox)")
        }
    }
}

安全处理类型转换

为了避免强制转型崩溃,建议使用安全的方式:

func safeHandleResults(request: VNRequest) {
    guard let results = request.results else { return }
    
    // 安全的方式:使用条件转型
    if let barcodeResults = results as? [VNBarcodeObservation] {
        handleBarcodes(barcodeResults)
    } else if let textResults = results as? [VNRecognizedTextObservation] {
        handleText(textResults)
    } else if let faceResults = results as? [VNFaceObservation] {
        handleFaces(faceResults)
    } else {
        // 通用处理
        for observation in results {
            print("基础观察对象: \(observation)")
        }
    }
}

VNObservation 的通用属性

所有 VNObservation 子类都有一些通用属性:

for observation in request.results ?? [] {
    print("UUID: \(observation.uuid)")
    print("置信度: \(observation.confidence)") // 0.0 到 1.0
    print("边界框: \(observation.boundingBox)") // 归一化坐标 (0,0 到 1,1)
    
    // 转换边界框到具体图像坐标
    let imageSize = CGSize(width: 1000, height: 800)
    let boundingBoxInPixels = VNImageRectForNormalizedRect(
        observation.boundingBox, 
        Int(imageSize.width), 
        Int(imageSize.height)
    )
    print("像素坐标: \(boundingBoxInPixels)")
}

总结

  • 基本类型[VNObservation]?

  • 需要向下转型 为具体的子类才能访问特定功能

  • 不同类型请求 返回不同的 VNObservation 子类

  • 总是可选类型,因为可能没有检测到任何内容

  • 包含通用属性 如置信度、边界框等

这种设计让 Vision 框架既保持了类型安全,又提供了统一的接口来处理各种计算机视觉任务。

大家好,我是1024小神,技术群 / 私活群 / 股票群 或 交朋友 都可以私信我。 如果你觉得本文有用,一键三连 (点赞、评论、关注),就是对我最大的支持~

swift中VNDetectBarcodesRequest VNImageRequestHandler 是什么?有什么作用?VN是什么意思

作者 1024小神
2025年11月14日 14:31

大家好,我的开源项目PakePlus可以将网页/Vue/React项目打包为桌面/手机应用并且小于5M只需几分钟,官网地址:pakeplus.com

在 Swift 中,VNDetectBarcodesRequest 和 VNImageRequestHandler 是 Vision 框架 中的类,用于计算机视觉任务。VN 是 Vision 的缩写。

Vision 框架概述

Vision 框架是 Apple 提供的用于执行计算机视觉任务的框架,包括:

  • 人脸检测

  • 条码识别

  • 文字识别

  • 图像分析

  • 目标跟踪等

VNImageRequestHandler - 图像请求处理器

作用:用于处理图像并执行 Vision 请求

import Vision
import UIKit

// 创建图像请求处理器
let image = UIImage(named: "barcode_image")
guard let cgImage = image?.cgImage else { return }

let requestHandler = VNImageRequestHandler(cgImage: cgImage)

// 也可以从其他来源创建
let requestHandlerFromURL = VNImageRequestHandler(url: imageURL)
let requestHandlerFromCIImage = VNImageRequestHandler(ciImage: ciImage)
let requestHandlerFromBuffer = VNImageRequestHandler(cvPixelBuffer: pixelBuffer)

// 执行请求
do {
    try requestHandler.perform([barcodeRequest, textRequest])
} catch {
    print("处理失败: \(error)")
}

VNDetectBarcodesRequest - 条码检测请求

作用:专门用于检测和识别图像中的条码

// 创建条码检测请求
let barcodeRequest = VNDetectBarcodesRequest { request, error in
    if let error = error {
        print("条码检测错误: \(error)")
        return
    }
    
    // 处理检测结果
    guard let results = request.results as? [VNBarcodeObservation] else { return }
    
    for observation in results {
        print("检测到条码:")
        print("类型: \(observation.symbology.rawValue)")
        print("内容: \(observation.payloadStringValue ?? "无内容")")
        print("位置: \(observation.boundingBox)")
        
        // 获取条码的角点坐标
        if let corners = observation.topLeft, 
           let topRight = observation.topRight,
           let bottomLeft = observation.bottomLeft,
           let bottomRight = observation.bottomRight {
            print("角点坐标: \(corners), \(topRight), \(bottomLeft), \(bottomRight)")
        }
    }
}

// 配置请求选项(可选)
barcodeRequest.revision = VNDetectBarcodesRequestRevision1
// 设置识别的条码类型
barcodeRequest.symbologies = [.QR, .code128, .EAN13]

完整使用示例

import Vision
import UIKit

class BarcodeScanner {
    func detectBarcodes(in image: UIImage) {
        guard let cgImage = image.cgImage else { return }
        
        // 创建条码检测请求
        let barcodeRequest = VNDetectBarcodesRequest { request, error in
            self.handleBarcodeResults(request: request, error: error)
        }
        
        // 配置条码类型
        barcodeRequest.symbologies = [.QR, .PDF417, .code128]
        
        // 创建图像处理器并执行请求
        let requestHandler = VNImageRequestHandler(cgImage: cgImage, options: [:])
        
        do {
            try requestHandler.perform([barcodeRequest])
        } catch {
            print("条码检测失败: \(error)")
        }
    }
    
    private func handleBarcodeResults(request: VNRequest, error: Error?) {
        if let error = error {
            print("处理错误: \(error)")
            return
        }
        
        guard let results = request.results as? [VNBarcodeObservation] else {
            print("未检测到条码")
            return
        }
        
        for barcode in results {
            print("""
            检测到条码:
            类型: \(barcode.symbology.rawValue)
            内容: \(barcode.payloadStringValue ?? "N/A")
            置信度: \(barcode.confidence)
            """)
            
            // 在真实应用中,这里可以处理条码数据
            if let payload = barcode.payloadStringValue {
                processBarcodePayload(payload, type: barcode.symbology)
            }
        }
    }
    
    private func processBarcodePayload(_ payload: String, type: VNBarcodeSymbology) {
        switch type {
        case .QR:
            print("QR码内容: \(payload)")
            // 处理 URL、联系方式等
        case .code128:
            print Code128 内容: \(payload)")
            // 处理商品编码等
        default:
            print("未知类型的条码: \(payload)")
        }
    }
}

支持的条码类型

Vision 框架支持多种条码类型:

let supportedSymbologies: [VNBarcodeSymbology] = [
    .Aztec,        // Aztec 码
    .code39,       // Code 39
    .code93,       // Code 93
    .code128,      // Code 128
    .dataMatrix,   // 数据矩阵码
    .EAN8,         // EAN-8
    .EAN13,        // EAN-13
    .PDF417,       // PDF417
    .QR,           // QR 码
    .UPCE,         // UPC-E
    .ITF14,        // ITF-14
    .codabar       // Codabar
]

其他常用的 Vision 请求

除了条码检测,Vision 框架还提供其他检测功能:

// 文字识别
let textRequest = VNRecognizeTextRequest { request, error in
    // 处理识别到的文字
}

// 人脸检测
let faceRequest = VNDetectFaceRectanglesRequest { request, error in
    // 处理检测到的人脸
}

// 物体检测
let objectRequest = VNDetectRectanglesRequest { request, error in
    // 处理检测到的矩形物体
}

// 同时执行多个请求
do {
    try requestHandler.perform([barcodeRequest, textRequest, faceRequest])
} catch {
    print("请求执行失败: \(error)")
}

优势特点

  1. 高性能: 利用设备的神经网络引擎

  2. 准确度高: 基于机器学习模型

  3. 易于使用: 简单的 API 设计

  4. 实时处理: 支持摄像头实时流处理

  5. 隐私保护: 在设备端处理,数据不上传

Vision 框架为 iOS/macOS 应用提供了强大的计算机视觉能力,让开发者可以轻松实现条码识别、文字识别等复杂功能。

大家好,我是1024小神,技术群 / 私活群 / 股票群 或 交朋友 都可以私信我。 如果你觉得本文有用,一键三连 (点赞、评论、关注),就是对我最大的支持~

React Native 新、旧架构集成原生模块方式

2025年11月14日 14:27

React Native新旧架构支持信息

ReactNative版本 最低支持Android版本 最低支持Ios版本 支持架构 备注
0.75及以下 sdk23(安卓6.0及以下) Ios 13.4 旧架构
0.76及以上 sdk24(安卓7.0及以上) Ios 15.4 新架构 到现在写文章的0.82版本

新、旧架构性能对比的文章挺多的就不多介绍了,性能优先肯定选新。

新架构集成原生模块

1、Expo原生模块

1.1 生成Expo方式的 app原生模块,运行下面命令,会在项目根目录下创建如下图所示目录:

# 创建Expo方式的 app原生模块
npx create-expo-module@latest --local

image.png

1.2 安装Expo模块

默认的Expo项目是没有安卓和Ios目录的,此时需要运行下面命令:

# 生成ios和安卓目录
npx expo prebuild --clean

如果已经prebuild出安卓和ios目录的,ios需要下面命令安装到pods内:

npx pod-install

1.3 此时ios打开expoapp.xcworkspace可以看到Development Pods内安装进来的模块,可以在这里进行修改,运行到手机或模拟器

image.png

2、社区cli搭建的项目方式

集成模块方式有几种,比如:Turbo ModuleNitro Module,可以使用官网推荐的社区模块模版,性能最好的是Nitro Module方式。

# 创建最新的模块lib
npx create-react-native-library@latest react-native-awesome-module

旧架构

1、创建旧架构的原生模块,选择 Legacy Native module

# 创建旧架构的模块lib,选择 `Legacy Native module `
npx create-react-native-library@0.49.8 test-mod

创建完成以后,会在项目根目录创建一个modules/你的模块目录,ios同上面Expo安装模块步骤一样。

image.png

2、旧架构如果想偷懒,甚至可以直接在AndroidIos目录内,直接创建对应的原生模块代码,暴露出来给RN使用,新时代了 原生模块代码可以使用AI生成,这样就不需要使用create-react-native-library库来创建模块了,下面是示例代码

import { NativeModules, Platform } from 'react-native';

const LINKING_ERROR =
  `The package 'TestNative' doesn't seem to be linked. Make sure: \n\n` +
  Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) +
  '- You rebuilt the app after installing the package\n' +
  '- You are not using Expo Go\n';

// 定义设备信息接口
export interface DeviceInfo {
  model: string;
  systemVersion: string;
  name: string;
  systemName: string;
}

// 定义原生模块接口
interface ITestNative {
  hello(input: string): Promise<string>;
  multiply(a: number, b: number): Promise<number>;
  getDeviceInfo(): DeviceInfo;
  delayedMessage(message: string, delay: number): Promise<string>;
  getCurrentTimestamp(): Promise<number>;
  formatDate(timestamp: number, format?: string): Promise<string>;
}

const TestNative: ITestNative = NativeModules.TestNative
  ? NativeModules.TestNative
  : new Proxy(
      {},
      {
        get() {
          throw new Error(LINKING_ERROR);
        },
      }
    ) as ITestNative;

/**
 * 调用iOS原生方法,返回问候语
 * @param input 输入字符串
 * @returns Promise<string> 返回格式化的问候语
 */
export function hello(input: string): Promise<string> {
  return TestNative.hello(input);
}

/**
 * 两个数字相乘
 * @param a 第一个数字
 * @param b 第二个数字
 * @returns Promise<number> 返回相乘结果
 */
export function multiply(a: number, b: number): Promise<number> {
  return TestNative.multiply(a, b);
}

/**
 * 获取设备信息(同步方法)
 * @returns 设备信息对象
 */
export function getDeviceInfo(): DeviceInfo {
  return TestNative.getDeviceInfo();
}

/**
 * 延迟发送消息
 * @param message 消息内容
 * @param delay 延迟时间(秒)
 * @returns Promise<string> 返回延迟后的消息
 */
export function delayedMessage(message: string, delay: number): Promise<string> {
  return TestNative.delayedMessage(message, delay);
}

/**
 * 获取当前时间戳
 * @returns Promise<number> 返回Unix时间戳
 */
export function getCurrentTimestamp(): Promise<number> {
  return TestNative.getCurrentTimestamp();
}

/**
 * 格式化日期
 * @param timestamp Unix时间戳
 * @param format 日期格式(可选,默认:'yyyy-MM-dd HH:mm:ss')
 * @returns Promise<string> 返回格式化后的日期字符串
 * 
 * @example
 * ```typescript
 * // 使用默认格式
 * const date1 = await formatDate(Date.now() / 1000);
 * // 输出: "2024-11-14 15:30:45"
 * 
 * // 使用自定义格式
 * const date2 = await formatDate(Date.now() / 1000, 'MM/dd/yyyy');
 * // 输出: "11/14/2024"
 * ```
 */
export function formatDate(timestamp: number, format?: string): Promise<string> {
  return TestNative.formatDate(timestamp, format);
}

// 导出原生模块供高级用户直接使用
export { TestNative };

相关文档链接

node/py实现 qwen多轮对话

2025年11月14日 14:27

大模型多轮对话需要3点

  1. llm
  2. prompt
  3. RunnableWithMessageHistory

注意 agent 多轮对话不在此范围

node版本

  • 依赖
"@langchain/anthropic": "^1.0.0",
"@langchain/community": "^1.0.2",
"@langchain/core": "^1.0.3",
"@langchain/langgraph": "^1.0.1",
"@langchain/langgraph-supervisor": "^1.0.0",
"@langchain/openai": "^1.0.0",
"axios": "^1.13.2",
"dotenv": "^17.2.3",
"langchain": "^1.0.2",
"langsmith": "^0.3.78",
"openai": "^6.8.1",
"zod": "^4.1.12"

实现

import { ChatOpenAI } from "@langchain/openai";
import "dotenv/config";
import * as readline from 'readline';
import { ChatPromptTemplate,MessagesPlaceholder } from "@langchain/core/prompts";

import { FileSystemChatMessageHistory } from "@langchain/community/stores/message/file_system";
import { RunnableWithMessageHistory } from "@langchain/core/runnables";
import { StringOutputParser } from "@langchain/core/output_parsers";


// llm
const model = new ChatOpenAI({
  model: "qwen-plus",
  apiKey: process.env.OPENAI_API_KEY,  //填写你token
  configuration: {
    baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
  },
});

// prompt
const prompt = ChatPromptTemplate.fromMessages([
  [
    "system",
    "你是一个全科目的专家, 尽你所能的帮助用户回答问题, 根据用户的语言回答问题",
  ],
  new MessagesPlaceholder("chat_history"), // ! 注意实现多轮对话 需要message占位符来插入历史消息, 请根据实际token 控制对话轮数
  ["human", "{input}"],
]);

console.log("输入exit或q退出");


// 构建输出
const chain = prompt.pipe(model).pipe(new StringOutputParser());


// Runnable 接口的api
const chainWithHistory = new RunnableWithMessageHistory({
  runnable: chain,
  inputMessagesKey: "input",
  historyMessagesKey: "chat_history",
  getMessageHistory: async (sessionId) => {
    console.log("sessionId:", sessionId);
    const chatHistory = new FileSystemChatMessageHistory({
      sessionId,
      userId: "444", // 根据实际情况实现uuid,默认路径在当前目录 也可修改文件路径 `./chat_history/${sessionId}.json`
    });
    return chatHistory;
  },
});



// 创建 readline 接口
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
});

function askQuestion() {
  rl.question('问题: ', async (userContent) => {
    if (userContent.toLowerCase() === "exit" || userContent.toLowerCase() === "q") {
      rl.close();
      return;
    }

    try {
      // 使用用户实际输入的问题
      const resp = await chainWithHistory.invoke(
        { input: userContent },  // 改为使用 userContent
        { configurable: { sessionId: "langchain-test-session" } }
      );

      console.log('---------------------------');
      // resp 已经是字符串,直接输出
      console.log(resp);
      console.log('---------------------------');

    } catch (error) {
      console.error('Error:', error.message);
    }
    // 继续提问
    askQuestion();
  });
}

// 开始提问循环
askQuestion();


python 版本


import uuid

from langchain_classic.agents.chat.prompt import HUMAN_MESSAGE

from langchain_core.output_parsers import StrOutputParser, JsonOutputParser
from langchain_community.chat_message_histories import ChatMessageHistory, FileChatMessageHistory
from langchain_core.runnables import RunnableWithMessageHistory, RunnableSequence

from app.agent.model.model import llm_qwen
from app.agent.prompts.multi_chat_prompt import multi_chat_prompt
from langchain_community.agent_toolkits.file_management import FileManagementToolkit
# from langgraph.prebuilt import create_react_agent
from langchain.agents import create_agent
from app.bailian.banlian_tools import add_tools

def get_session_history(session_id: str):
    # if session_id not in store:
    #     store[session_id] = ChatMessageHistory()
    return FileChatMessageHistory(f"{session_id}.json")

file_toolkit = FileManagementToolkit(root_dir="D:\code\ai-agent-test\.temp")
file_tools = file_toolkit.get_tools()

all_tools = file_tools + [add_tools]

agent = create_agent(model=llm_qwen, tools=all_tools)


llm_with_tools = llm_qwen.bind_tools(tools=all_tools)

chain = RunnableSequence(
    first=multi_chat_prompt,
    middle=[llm_with_tools],
    last=StrOutputParser()
)




# chat_history = ChatMessageHistory()
# chat_history.add_user_message("我是野猪佩奇,我们要做 浏览器自动化智能体项目(类似selenium自动化)")
# chat_history.add_ai_message("")
chain_with_history = RunnableWithMessageHistory(
    runnable=chain,
    get_session_history=get_session_history,
    input_messages_key="question",
    history_messages_key="chat_history",
)

# chat_session_id = uuid.uuid4()
chat_session_id = "1"

print(f"session_id: {chat_session_id}")


while True:
    user_input = input("用户:")
    if user_input.lower() == "exit" or user_input.lower() == "quit":
        break

    print("助理:", end="")
    for chunk in chain_with_history.stream(
        {"question": user_input},
        config={"configurable": {"session_id": chat_session_id}},
    ):
        print(chunk, end="")

    print("\n")


JavaScript 对象数组去重的几种方法

作者 juejin_cn
2025年11月14日 14:20

部分内容由大模型生成

根据对象的某个属性(如 id)对对象数组去重的几种方法。

Map

const arr = [
  { id: 'a', name: 'Object A' },
  { id: 'b', name: 'Object B' },
  { id: 'c', name: 'Object C' },
  { id: 'a', name: 'Object A (重复)' },
  { id: 'd', name: 'Object D' },
  { id: 'b', name: 'Object B (重复)' }
];

const unique = [...new Map(arr.map(item => [item.id, item])).values()]

console.log(unique)
// 输出: 
// [
//   {id: 'a', name: 'Object A (重复)'}
//   {id: 'b', name: 'Object B (重复)'}
//   {id: 'c', name: 'Object C'}
//   {id: 'd', name: 'Object D'}
// ]

Map 的键(key)唯一,map(item => [item.id, item]) 把每个对象用 id 当键,后面的重复项会覆盖。最后用 .values() 得到去重后的对象数组。

此方法会保留最后一个出现的 id 对应的对象。

filter & findIndex

const unique = arr.filter((item, index, self) =>
  index === self.findIndex(t => t.id === item.id)
)
// 输出: 
// [
//   {id: 'a', name: 'Object A'}
//   {id: 'b', name: 'Object B'}
//   {id: 'c', name: 'Object C'}
//   {id: 'd', name: 'Object D'}
// ]

findIndex 找到数组中首个 id 相同的元素位置,只有第一个匹配项会被保留。

reduce & some

const unique = arr.reduce((acc, cur) => {
  if (!acc.some(item => item.id === cur.id)) {
    acc.push(cur)
  }
  return acc
}, [])
// 输出: 
// [
//   {id: 'a', name: 'Object A'}
//   {id: 'b', name: 'Object B'}
//   {id: 'c', name: 'Object C'}
//   {id: 'd', name: 'Object D'}
// ]

累积构建一个新数组,遇到重复的 id 不再添加。

filter & Set

const seen = new Set()
const unique = arr.filter(item => !seen.has(item.id) && seen.add(item.id))
// 输出: 
// [
//   {id: 'a', name: 'Object A'}
//   {id: 'b', name: 'Object B'}
//   {id: 'c', name: 'Object C'}
//   {id: 'd', name: 'Object D'}
// ]

利用 Set 记录已出现的 id,第一次出现才添加。

从 Vue 构建错误到深度解析:::v-deep 引发的 CSS 压缩危机

作者 一川_
2025年11月14日 14:13

前言

在日常的前端开发中,我们经常会遇到各种构建错误,有些错误信息明确,容易定位;而有些则像迷宫一样,需要一步步排查。最近在开发一个 Vue 2 项目时,我就遇到了一个令人头疼的 CSS 压缩错误,经过多轮排查和尝试,最终找到了问题的根源和解决方案。本文将详细记录这个问题的排查过程,并深入分析相关的技术原理。

问题初现

那是一个普通的开发日,我正在为一个生产工单管理系统添加新功能。在完成代码编写后,我像往常一样执行构建命令:

npm run build:prod

然而,控制台却报出了令人困惑的错误:

ERROR Error: CSS minification error: Cannot read property 'trim' of undefined. File: static/css/chunk-25fbebba.59b3af06.css
Error: CSS minification error: Cannot read property 'trim' of undefined. File: static/css/chunk-25fbebba.59b3af06.css
    at C:\Users\wzh\Desktop\BatchProductionWorkOrderReport\node_modules@intervolga\optimize-cssnano-plugin\index.js:106:21

第一阶段:常规排查

尝试方案一:清除缓存和重新安装

面对构建错误,我的第一反应是清除缓存和重新安装依赖,这是前端开发中的"万能药":

# 清除 npm 缓存
npm cache clean --force

# 删除 node_modules 和 package-lock.json
rm -rf node_modules package-lock.json

# 重新安装依赖
npm install

# 重新构建
npm run build:prod

然而,这次"万能药"并没有奏效,同样的错误再次出现。

尝试方案二:更新相关依赖

注意到控制台有一个警告:"A new version of sass-loader is available",我尝试更新相关依赖:

# 更新 sass-loader
npm update sass-loader

# 更新 Vue CLI 和相关构建工具
npm update @vue/cli-service

# 更新 CSS 相关插件
npm update @intervolga/optimize-cssnano-plugin cssnano postcss

更新后重新构建,问题依旧。

第二阶段:深入分析

错误信息分析

仔细分析错误信息,有几个关键点:

  1. 错误位置@intervolga/optimize-cssnano-plugin/index.js:106:21
  2. 错误类型Cannot read property 'trim' of undefined
  3. 涉及文件chunk-25fbebba.59b3af06.css

这表明问题出现在 CSS 压缩阶段,某个 CSS 内容在压缩时变成了 undefined

代码审查

我开始审查项目中最近修改的代码,重点关注样式部分。发现问题出现在一个使用了 ::v-deep 的 Vue 组件中:

<style scoped>
.workorder-table {
  height: 100%;
}

::v-deep .el-table__body-wrapper {
  height: 100% !important;
  overflow-y: auto;
}

::v-deep .el-table th {
  background: #e3e9f3 !important;
  color: #1f1f1f !important;
  font-weight: 600;
  font-size: 13px;
  border-bottom: 2px solid #c3c9d4 !important;
  padding: 12px;
}

::v-deep .el-table td {
  padding: 12px;
}
</style>

第三阶段:技术原理探究

什么是 ::v-deep?

::v-deep 是 Vue.js 中用于样式穿透的伪类选择器。在 Vue 的 scoped CSS 中,样式默认只作用于当前组件,但有时候我们需要修改子组件的样式,这时就需要使用样式穿透。

Vue 2 和 Vue 3 中的差异

在排查过程中,我发现不同 Vue 版本对深度选择器的支持有所不同:

Vue 2 支持的形式:

  • >>>
  • /deep/
  • ::v-deep

Vue 3 支持的形式:

  • :deep()
  • ::v-deep(已弃用)

构建过程中的 CSS 处理流程

理解构建过程中 CSS 的处理流程对于解决问题至关重要:

  1. Vue Loader 处理:Vue Loader 解析 .vue 文件中的 <style> 块
  2. CSS 预处理:如果使用了 Sass/Less,会进行相应的预处理
  3. PostCSS 处理:应用各种 PostCSS 插件,包括 scoped CSS 处理
  4. CSS 提取:将 CSS 从 JavaScript 中提取出来
  5. CSS 压缩:使用 cssnano 等工具进行压缩

问题根源分析

经过深入分析,我发现问题的根源在于:

  1. 版本兼容性问题:项目中使用的 @intervolga/optimize-cssnano-plugin 版本与当前的 Vue CLI 版本存在兼容性问题
  2. 深度选择器解析:在某些情况下,::v-deep 在构建过程中可能被解析为空的 CSS 规则
  3. CSS 压缩异常:当遇到这些异常的 CSS 规则时,压缩插件无法正确处理,导致 undefined 错误

第四阶段:解决方案尝试

方案一:使用 /deep/ 替代 ::v-deep

这是最直接的解决方案,将所有的 ::v-deep 替换为 /deep/

<style scoped>
.workorder-table {
  height: 100%;
}

/deep/ .el-table__body-wrapper {
  height: 100% !important;
  overflow-y: auto;
}

/deep/ .el-table th {
  background: #e3e9f3 !important;
  color: #1f1f1f !important;
  font-weight: 600;
  font-size: 13px;
  border-bottom: 2px solid #c3c9d4 !important;
  padding: 12px;
}

/deep/ .el-table td {
  padding: 12px;
}
</style>

结果:构建成功!这是最快速的解决方案。

方案二:使用 CSS Modules

为了更彻底地解决问题,我尝试了 CSS Modules 方案:

<template>
  <div class="workorder-page">
    <el-table :class="$style.workorderTable">
      <!-- 表格内容 -->
    </el-table>
  </div>
</template>

<style module>
.workorderTable {
  width: 100%;
  min-width: 1400px;
}

.workorderTable :global(.el-table__body-wrapper) {
  height: 100% !important;
  overflow-y: auto;
}

.workorderTable :global(.el-table th) {
  background: #e3e9f3 !important;
  color: #1f1f1f !important;
  font-weight: 600;
  font-size: 13px;
  border-bottom: 2px solid #c3c9d4 !important;
  padding: 12px;
}

.workorderTable :global(.el-table td) {
  padding: 12px;
}
</style>

结果:构建成功,且代码更加规范。

方案三:配置 vue.config.js

如果必须使用 ::v-deep,可以通过配置 vue.config.js 来解决问题:

module.exports = {
  css: {
    loaderOptions: {
      css: {
        // 启用 CSS Modules 模式避免深度选择器问题
        modules: false
      },
      postcss: {
        plugins: [
          require('autoprefixer')
        ]
      }
    }
  },
  chainWebpack: config => {
    // 优化 CSS 压缩配置
    config.plugin('optimize-css').tap(args => {
      if (args[0] && args[0].cssnanoOptions) {
        args[0].cssnanoOptions.preset = ['default', {
          discardComments: {
            removeAll: true
          },
          normalizeWhitespace: false
        }]
      }
      return args
    })
  }
}

最终解决方案

综合考虑项目现状和长期维护性,我选择了方案二(CSS Modules) 作为最终解决方案,原因如下:

  1. 符合现代前端开发规范
  2. 更好的样式隔离
  3. 避免深度选择器的兼容性问题
  4. 便于代码维护和重构

技术深度解析

Vue Scoped CSS 原理

Vue 的 scoped CSS 是通过 PostCSS 插件实现的,工作原理如下:

  1. 为每个选择器添加属性选择器.example → .example[data-v-xxxxxx]
  2. 为模板元素添加属性<div class="example"> → <div class="example" data-v-xxxxxx>
  3. 样式仅限于带有相同 data-v 属性的元素

深度选择器的实现机制

深度选择器的工作原理是移除属性选择器:

/* 原始代码 */
::v-deep .child-component { color: red; }

/* 转换后 */
[data-v-xxxxxx] .child-component { color: red; }

CSS Modules 的优势

  1. 真正的局部作用域:通过类名哈希实现
  2. 无冲突的类名:每个模块的类名都是唯一的
  3. 显式依赖:明确知道样式在哪里被使用
  4. 代码压缩优化:类名可以被压缩得更短

经验总结

通过这次问题的排查和解决,我总结了以下几点经验:

1. 构建错误排查方法论

  • 从简单到复杂:先尝试清除缓存、重新安装等简单操作
  • 分析错误堆栈:仔细阅读错误信息,定位问题发生的具体位置
  • 版本兼容性检查:检查相关依赖的版本兼容性
  • 代码审查:重点关注最近修改的代码

2. Vue 样式开发最佳实践

  • Vue 2 项目:推荐使用 /deep/ 或 CSS Modules
  • Vue 3 项目:推荐使用 :deep() 选择器
  • 大型项目:优先考虑 CSS Modules 或 CSS-in-JS 方案

3. 预防措施

// 在 package.json 中固定关键依赖版本
{
  "dependencies": {
    "vue": "^2.6.14"
  },
  "devDependencies": {
    "@vue/cli-service": "^4.5.19",
    "sass-loader": "^10.2.1"
  }
}

结语

这次 CSS 压缩错误的排查过程,让我对 Vue 的样式系统有了更深入的理解。从前端的表面现象到底层的构建原理,从简单的样式编写到复杂的工程化问题,每一个技术细节都值得深入探究。

作为前端开发者,我们不仅要会使用框架提供的便利功能,更要理解其背后的原理和实现机制。只有这样,当遇到问题时,我们才能快速定位并找到最优解决方案。

希望这篇文章能帮助到遇到类似问题的开发者,也欢迎大家分享自己的问题和解决方案,共同进步!

qinkun的缓存机制也有弊端,建议官方个参数控制

作者 石小石Orz
2025年11月14日 13:54

公司前端基于qiankun架构,主应用通过qiankun加载子应用,子应用也可能通过qiankun继续加载子应用,反复套娃。经过测试,不断打开子应用后,会导致内存不断上上。通过快照分析,发现内存升高的元凶是qiankun内置的# import-html-entry

import-html-entry 的作用是什么

import-html-entry 是 qiankun / single-spa 微前端生态的核心模块之一,用来:

加载远程 HTML 入口文件(entry HTML),并提取出其中的 JS / CSS / 静态资源,然后按需执行或注入。

比如:

import { importEntry } from 'import-html-entry';

const { execScripts } = await importEntry('https://wwww.石小石.com/');
const exports = await execScripts(window);

简单来说,import-html-entry 负责做三件事:

下载远程 HTML 文件

  • 使用 fetch(url) 请求远程 HTML。
  • 解析 HTML 中的 <script><link> 标签。

提取资源并缓存

  • 提取脚本与样式资源 URL。
  • 通过自定义逻辑加载(并缓存)外部脚本与样式内容。
  • <link> 替换为内联 <style>,提升加载性能。

执行脚本

  • 通过 eval 在隔离作用域中执行 JS(防止污染主应用的 window)。
  • 支持 proxy 代理对象(qiankun 沙箱核心)。
  • 支持同步、异步脚本的加载与执行顺序。

核心源码

它的 源码中包含这些关键函数:

函数名 作用
importHTML(url, opts) 主入口,加载远程 HTML
processTpl 解析 HTML 模板,提取 script/link
_getExternalScripts 加载并缓存 JS
_getExternalStyleSheets 加载并缓存 CSS
_execScripts 按顺序执行脚本
getExecutableScript 包装脚本为沙箱可执行代码
evalCode 实际执行脚本(带缓存)

缓存机制

import-html-entry 内部维护了四个全局缓存对象, 这些缓存的目的是 在同一个浏览器会话中,当多个子应用或同一个子应用多次加载同一个 URL 时,避免重复网络请求,从而加快微前端加载速度。

var styleCache = {};    // 样式字符缓存
var scriptCache = {};   // js字符缓存
var embedHTMLCache = {};// html字符缓存
var evalCache = {};     // 编译后的脚本缓存

styleCache对应源码:

scriptCache对应源码:

embedHTMLCache对应源码:

evalCache对应源码:

缓存机制在子应用多开频繁销毁创建场景中的弊端

在单实例的 qiankun 架构 中,import-html-entry 的缓存仅会存在一份,对内存的占用影响有限,缓存带来的性能收益相对较高。
但如果系统存在大量qiankun加载子应用的场景,比如要频繁打开若干子应用(类似于菜单),子应用需要频繁打开销毁(tab切换等),同时其内部的部分功能模块又会再次通过 qiankun 动态加载子应用。这种嵌套加载结构会导致 import-html-entry 在多个层级重复缓存资源,即使资源内容相同,也会被多次存储。

因此,子应用的频繁打开与卸载,会导致内存占用持续增长,从而引发明显的性能下降(国产CPU可能更明显)。

因此,移除或禁用 import-html-entry 的缓存机制,能极大缓解内存泄漏问题,提升系统在复杂场景下的运行性能与稳定性

优化方案

缓存名 缓存内容 缓存目的 禁用影响
styleCache 每个 CSS 链接的内容(文本) 避免重复请求相同样式文件 每次重新请求 CSS(但浏览器会命中协商缓存,影响极小)
scriptCache 每个 JS 链接的内容(文本) 避免重复请求相同脚本文件 每次重新请求 JS(命中浏览器缓存,影响较小)
embedHTMLCache 整个 HTML 模板字符串 避免重复请求入口 HTML 文件 每次重新请求入口文件,性能略降
evalCache 每个脚本的已编译函数 (function(){...}) 避免多次 eval 编译同一脚本字符串,提升运行性能 每次都重新 eval 解析 JS 字符串,会略微影响性能(CPU 负担)

浏览器自带的协商缓存已能高效复用 HTML、JS、CSS 资源,因此禁用 import-html-entry 的缓存逻辑几乎不影响加载性能。
evalCache 的移除可能短暂增加 CPU 开销降低性能,但整体影响可能较小,需要综合评估。

通过在 qiankun 集成层中移除多余缓存,可有效降低内存占用,缓解泄漏问题,并显著提升系统性能与稳定性。

Vue3实现拖拽排序

2025年11月14日 13:54

Vue3 + Element Plus + SortableJS 实现表格拖拽排序功能

📋 目录

功能概述

在管理后台系统中,表格数据的排序功能是一个常见的需求。本文介绍如何使用 Vue3、Element Plus 和 SortableJS 实现一个完整的表格拖拽排序功能,支持:

  • ✅ 通过拖拽图标对表格行进行排序
  • ✅ 实时更新数据顺序
  • ✅ 支持数据过滤后的排序
  • ✅ 切换标签页时自动初始化
  • ✅ 优雅的动画效果

先看实现效果: 在这里插入图片描述

技术栈

  • Vue 3 - 渐进式 JavaScript 框架
  • Element Plus - Vue 3 组件库
  • SortableJS - 轻量级拖拽排序库
  • TypeScript - 类型安全的 JavaScript 超集

实现思路

1. 整体架构

用户拖拽表格行
    ↓
SortableJS 监听拖拽事件
    ↓
触发 onEnd 回调
    ↓
更新 Vue 响应式数据
    ↓
表格自动重新渲染

2. 关键步骤

  1. 安装依赖:引入 SortableJS 库
  2. 获取 DOM:获取表格 tbody 元素
  3. 初始化 Sortable:创建拖拽实例
  4. 处理回调:在拖拽结束时更新数据
  5. 生命周期管理:在适当时机初始化和销毁实例

代码实现

1. 安装依赖

npm install sortablejs
# 或
pnpm add sortablejs

2. 导入必要的模块

import { ref, nextTick, watch, onMounted } from "vue";
import Sortable from "sortablejs";
import { Operation } from "@element-plus/icons-vue";//图标

3. 定义数据结构

interface TypeItem {
  id: string;
  name: string;
  enabled: boolean;
  sortOrder: number;
}

const typeData = ref<TypeItem[]>([
  { id: "1", name: "楼宇性质1", enabled: true, sortOrder: 1 },
  { id: "2", name: "楼宇性质2", enabled: true, sortOrder: 2 },
  // ... 更多数据
]);

4. 模板结构

<template>
  <el-table ref="typeTableRef" :data="filteredTypeData" stripe row-key="id">
    <!-- 排序列:显示拖拽图标 -->
    <el-table-column label="排序" width="131">
      <template #default>
        <el-icon class="drag-handle">
          <Operation />
        </el-icon>
      </template>
    </el-table-column>
    
    <!-- 其他列 -->
    <el-table-column prop="name" label="名称" />
    <el-table-column prop="enabled" label="启用/禁用">
      <template #default="{ row }">
        <el-switch v-model="row.enabled" />
      </template>
    </el-table-column>
  </el-table>
</template>

5. 核心实现代码

// 表格引用
const typeTableRef = ref<InstanceType<typeof ElTable>>();

// Sortable 实例(用于后续销毁)
let sortableInstance: Sortable | null = null;

/**
 * 初始化拖拽排序功能
 */
const initSortable = () => {
  // 1. 销毁旧实例,避免重复创建
  if (sortableInstance) {
    sortableInstance.destroy();
    sortableInstance = null;
  }

  // 2. 等待 DOM 更新完成
  nextTick(() => {
    // 3. 获取表格的 tbody 元素
    const tbody = typeTableRef.value?.$el?.querySelector(
      ".el-table__body-wrapper tbody"
    );
    
    if (!tbody) return;

    // 4. 创建 Sortable 实例
    sortableInstance = Sortable.create(tbody, {
      // 指定拖拽手柄(只能通过拖拽图标来拖拽)
      handle: ".drag-handle",
      
      // 动画时长(毫秒)
      animation: 300,
      
      // 拖拽结束回调
      onEnd: ({ newIndex, oldIndex }) => {
        // 5. 更新数据顺序
        if (
          newIndex !== undefined &&
          oldIndex !== undefined &&
          filterStatus.value === "all" // 只在"全部"状态下允许排序
        ) {
          // 获取被移动的项
          const movedItem = typeData.value[oldIndex];
          
          // 从原位置删除
          typeData.value.splice(oldIndex, 1);
          
          // 插入到新位置
          typeData.value.splice(newIndex, 0, movedItem);
          
          // 更新排序字段
          typeData.value.forEach((item, index) => {
            item.sortOrder = index + 1;
          });
        }
      }
    });
  });
};

6. 生命周期管理

/**
 * 监听标签页切换,初始化拖拽
 */
const watchActiveTab = () => {
  if (activeTab.value === "type") {
    // 延迟初始化,确保表格已完全渲染
    setTimeout(() => {
      initSortable();
    }, 300);
  }
};

// 组件挂载时初始化
onMounted(() => {
  watchActiveTab();
});

// 监听标签页切换
watch(activeTab, () => {
  watchActiveTab();
});

// 监听过滤器变化,重新初始化拖拽
watch(filterStatus, () => {
  if (activeTab.value === "type") {
    setTimeout(() => {
      initSortable();
    }, 100);
  }
});

7. 样式定义

/* 拖拽手柄样式 */
.drag-handle {
  color: #909399;
  cursor: move;
  font-size: 18px;
  transition: color 0.3s;
}

.drag-handle:hover {
  color: #1890ff;
}

/* 表格样式 */
.type-table {
  margin-top: 0;
}

:deep(.type-table .el-table__header-wrapper) {
  background-color: #f9fafc;
}

:deep(.type-table .el-table th) {
  background-color: #f9fafc;
  font-size: 14px;
  font-weight: 500;
  color: #33425cfa;
  font-family: PingFang SC;
  border-bottom: 1px solid #dcdfe6;
}

核心要点

1. 实例管理

问题:如果不管理 Sortable 实例,切换标签页或过滤器时会创建多个实例,导致拖拽行为异常。

解决:使用变量保存实例引用,在创建新实例前先销毁旧实例。

let sortableInstance: Sortable | null = null;

const initSortable = () => {
  // 先销毁旧实例
  if (sortableInstance) {
    sortableInstance.destroy();
    sortableInstance = null;
  }
  // 再创建新实例
  // ...
};

2. DOM 获取时机

问题:如果直接获取 DOM,可能表格还未渲染完成,导致获取失败。

解决:使用 nextTick 等待 Vue 完成 DOM 更新,或使用 setTimeout 延迟执行。

nextTick(() => {
  const tbody = typeTableRef.value?.$el?.querySelector(
    ".el-table__body-wrapper tbody"
  );
  // ...
});

3. 拖拽手柄

问题:如果不指定拖拽手柄,整行都可以拖拽,可能与其他交互冲突(如点击编辑按钮)。

解决:使用 handle 选项指定只有拖拽图标可以触发拖拽。

Sortable.create(tbody, {
  handle: ".drag-handle", // 只允许通过 .drag-handle 元素拖拽
  // ...
});

4. 数据更新策略

问题:直接操作 DOM 顺序不会更新 Vue 的响应式数据。

解决:在 onEnd 回调中手动更新数据数组的顺序。

onEnd: ({ newIndex, oldIndex }) => {
  const movedItem = typeData.value[oldIndex];
  typeData.value.splice(oldIndex, 1);
  typeData.value.splice(newIndex, 0, movedItem);
  // 更新排序字段
  typeData.value.forEach((item, index) => {
    item.sortOrder = index + 1;
  });
}

5. 过滤状态处理

问题:当表格数据被过滤后,拖拽的索引可能不准确。

解决:只在"全部"状态下允许排序,或根据过滤后的数据计算正确的索引。

onEnd: ({ newIndex, oldIndex }) => {
  if (filterStatus.value === "all") {
    // 只在全部状态下允许排序
    // ...
  }
}

常见问题

Q1: 拖拽后数据没有更新?

A: 检查是否正确更新了响应式数据。SortableJS 只负责 DOM 操作,不会自动更新 Vue 数据。

Q2: 切换标签页后拖拽失效?

A: 需要在标签页切换时重新初始化 Sortable 实例,因为 DOM 已经重新渲染。

Q3: 拖拽时整行都可以拖,如何限制?

A: 使用 handle 选项指定拖拽手柄元素。

Q4: 拖拽动画不流畅?

A: 调整 animation 参数的值,通常 200-300ms 效果较好。

Q5: 如何保存排序结果?

A: 在 onEnd 回调中,将更新后的数据发送到后端 API。

onEnd: ({ newIndex, oldIndex }) => {
  // 更新本地数据
  // ...
  
  // 保存到后端
  saveSortOrder(typeData.value.map(item => ({
    id: item.id,
    sortOrder: item.sortOrder
  })));
}

完整示例代码

<template>
  <div class="type-setting">
    <!-- 过滤器 -->
    <div class="filter-actions">
      <el-button
        :type="filterStatus === 'all' ? 'primary' : ''"
        @click="filterStatus = 'all'"
      >
        全部
      </el-button>
      <el-button
        :type="filterStatus === 'enabled' ? 'primary' : ''"
        @click="filterStatus = 'enabled'"
      >
        启用
      </el-button>
    </div>

    <!-- 表格 -->
    <el-table
      ref="typeTableRef"
      :data="filteredTypeData"
      stripe
      row-key="id"
    >
      <el-table-column label="排序" width="131">
        <template #default>
          <el-icon class="drag-handle">
            <Operation />
          </el-icon>
        </template>
      </el-table-column>
      <el-table-column prop="name" label="名称" />
      <el-table-column prop="enabled" label="启用/禁用">
        <template #default="{ row }">
          <el-switch v-model="row.enabled" />
        </template>
      </el-table-column>
      <el-table-column label="操作">
        <template #default="{ row }">
          <el-button type="primary" link @click="handleEdit(row)">
            编辑
          </el-button>
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>

<script setup lang="ts">
import { ref, nextTick, watch, onMounted } from "vue";
import { ElTable } from "element-plus";
import Sortable from "sortablejs";
import { Operation } from "@element-plus/icons-vue";

interface TypeItem {
  id: string;
  name: string;
  enabled: boolean;
  sortOrder: number;
}

const typeData = ref<TypeItem[]>([
  { id: "1", name: "楼宇性质1", enabled: true, sortOrder: 1 },
  { id: "2", name: "楼宇性质2", enabled: true, sortOrder: 2 },
  { id: "3", name: "楼宇性质3", enabled: false, sortOrder: 3 },
]);

const filterStatus = ref<"all" | "enabled" | "disabled">("all");
const typeTableRef = ref<InstanceType<typeof ElTable>>();
let sortableInstance: Sortable | null = null;

const filteredTypeData = computed(() => {
  if (filterStatus.value === "all") return typeData.value;
  if (filterStatus.value === "enabled") {
    return typeData.value.filter(item => item.enabled);
  }
  return typeData.value.filter(item => !item.enabled);
});

const initSortable = () => {
  if (sortableInstance) {
    sortableInstance.destroy();
    sortableInstance = null;
  }

  nextTick(() => {
    const tbody = typeTableRef.value?.$el?.querySelector(
      ".el-table__body-wrapper tbody"
    );
    if (!tbody) return;

    sortableInstance = Sortable.create(tbody, {
      handle: ".drag-handle",
      animation: 300,
      onEnd: ({ newIndex, oldIndex }) => {
        if (
          newIndex !== undefined &&
          oldIndex !== undefined &&
          filterStatus.value === "all"
        ) {
          const movedItem = typeData.value[oldIndex];
          typeData.value.splice(oldIndex, 1);
          typeData.value.splice(newIndex, 0, movedItem);
          typeData.value.forEach((item, index) => {
            item.sortOrder = index + 1;
          });
        }
      }
    });
  });
};

onMounted(() => {
  setTimeout(() => initSortable(), 300);
});

watch(filterStatus, () => {
  setTimeout(() => initSortable(), 100);
});
</script>

<style scoped>
.drag-handle {
  color: #909399;
  cursor: move;
  font-size: 18px;
}

.drag-handle:hover {
  color: #1890ff;
}
</style>

总结

通过本文的介绍,我们实现了一个完整的表格拖拽排序功能。关键点包括:

  1. 正确的实例管理:避免重复创建和内存泄漏
  2. 合适的初始化时机:确保 DOM 已完全渲染
  3. 数据同步更新:手动更新 Vue 响应式数据
  4. 良好的用户体验:指定拖拽手柄,添加动画效果
  5. 完善的错误处理:处理边界情况

这个方案可以轻松应用到其他需要拖拽排序的场景,如菜单管理、分类排序等。希望本文对您有所帮助!


不只是设计师的工具:Photoshop在前端开发中的高频操作指南

2025年11月14日 13:48

很多刚入门前端的同学会有个疑问:“我是写代码的,为什么还要学Photoshop?” 事实上,PS 对于前端开发者来说,不仅是查看设计稿的工具,更是获取精确数据、提取资源和理解视觉实现的“瑞士军刀”。今天,我们就来聊聊那些前端工程师必须掌握的 PS 常见操作。

一、核心概念:为什么前端要懂点PS?

前端工程师的使命是将设计师的静态稿(通常是 PSD 或 Sketch 文件)转化为动态的、可交互的网页。在这个过程中,PS 能帮助我们:

  1. 获取精确尺寸与间距:精准测量元素的大小、边距、行高,实现“像素级”还原。
  2. 提取切图资源:获取页面中使用的图标、Logo、特殊按钮等图片素材。
  3. 拾取颜色值:获取设计稿中使用的精确颜色,包括不常用的渐变和阴影色。
  4. 分析复杂样式:理解图层混合模式、阴影、模糊等效果的参数,以便用 CSS 代码实现。

二、常见操作、代码示例与注意点

1. 获取尺寸与间距

这是最基础也是最常用的操作。

操作流程

  1. 在 PS 中打开设计稿。
  2. 选择 移动工具 (快捷键 V)。
  3. 在上方选项栏勾选 自动选择 并选择 图层
  4. 点击你想测量的元素,在图层面板中会自动定位到该图层。
  5. 使用 矩形选框工具 (M) 框选要测量的区域,信息面板 (F8) 会显示宽 (W) 和高 (H)。

CSS代码示例:假设我们测量到一个按钮的宽度为 120px,高度为 40px,距离左边元素 20px

.my-button {
  width: 120px;
  height: 40px;
  margin-left: 20px; /* 测量到的间距 */
}

2. 提取图片资源(切图)

当设计稿中有无法用 CSS 绘制的图片、图标或 Logo 时,我们需要将它们“切”出来。

  • 操作流程(传统方式)

    1. 使用 切片工具 (C)。
    2. 框选你需要导出的区域。
    3. 点击菜单栏 文件 -> 导出 -> 存储为 Web 所用格式 (旧版)。
    4. 选择格式(如 PNG-24, PNG-8, JPG),调整质量,点击存储,并选择“选中的切片”。
  • 操作流程(更高效的方式 - 导出为)

    1. 在图层面板,右键点击你需要导出的图层或图层组。
    2. 选择 导出为...
    3. 在弹出的对话框中选择格式(如 PNG, JPG, SVG)和大小(1x, 2x等)。
    4. 点击 导出全部

HTML代码示例:假设我们导出了一个名为 icon-search.png 的图标。

<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <!-- 使用切图资源 -->
  <button class="search-btn">
    <img src="images/icon-search.png" alt="搜索图标">
    搜索
  </button>
</body>
</html>

3. 获取颜色值

确保页面颜色与设计稿一致的关键。

  • 操作流程

    1. 使用 吸管工具 (快捷键 I)。
    2. 在设计稿上点击你想要取色的位置。
    3. 查看前景色块,点击它会弹出 拾色器 对话框,可以获取到 HEX、RGB 等格式的颜色值。

CSS代码示例:假设我们取到一个主题色为 #3a86ff,一个带透明度的黑色 rgba(0, 0, 0, 0.5)

:root {
  --primary-color: #3a86ff; /* 从PS拾色器获取的HEX值 */
  --overlay-color: rgba(0, 0, 0, 0.5); /* 从PS拾色器获取的RGBA值 */
}

.header {
  background-color: var(--primary-color);
}

.modal-overlay {
  background-color: var(--overlay-color);
}

4. 解析阴影效果

CSS 的 box-shadow 属性可以完美实现 PS 中的投影效果,但需要获取正确的参数。

  • 操作流程

    1. 在图层面板,双击想要查看效果的图层,打开 图层样式 对话框。
    2. 选择 投影
    3. 记录下 混合模式不透明度角度距离扩展大小 等参数。

CSS代码示例:假设我们从图层样式中看到参数为:不透明度 25%,角度 120度,距离 5px,扩展 0px,大小 10px。

.card {
  /* box-shadow: h-offset v-offset blur spread color; */
  box-shadow: 5px 5px 10px 0 rgba(0, 0, 0, 0.25);
  /* 
    解释:
    h-offset (水平偏移): 5px (根据角度和距离换算,这里简化为5px)
    v-offset (垂直偏移): 5px
    blur (模糊大小): 10px
    spread (扩展大小): 0px
    color: rgba(0, 0, 0, 0.25) (黑色,25%不透明度)
  */
}

注意:

PS 中的“角度”是一个全局光的角度,需要手动换算为 h-offset 和 v-offset。可以使用 距离 * sin(角度) 和 距离 * cos(角度) 来粗略计算,但通常直接手动调整 h-offset 和 v-offset 直到视觉上匹配即可。

总结

Photoshop 不是一个需要精通绘图的软件,而是一个  “数据提取器和视觉解析器”

  • 核心价值:在于精准地获取构建页面所需的数值(尺寸、间距)、资源(图片)和样式参数(颜色、阴影) 。
  • 工作流:可以概括为  “测量 -> 提取 -> 编码”  的循环。
  • 终极目标:通过与设计师的无缝协作,将这些视觉数据转化为高质量的 CSS 和 HTML 代码,最终实现与设计稿高度一致的网页。

Canvas绘图基础:坐标、线条与圆形的艺术

2025年11月14日 13:46

在Canvas绘图的世界里,理解坐标系统是绘制一切图形的基础。就像在地图上找位置需要经纬度一样,在Canvas中绘制图形也需要精确的坐标定位。今天,我们将深入探索Canvas的坐标系统,并学习如何利用这个系统绘制线条和圆形——这两种构成复杂图形的基本元素。

Canvas坐标系统

基本概念

Canvas使用基于左上角的二维笛卡尔坐标系,这与我们数学中常见的坐标系有所不同:

  • 原点(0,0) :位于画布的左上角
  • X轴:向右为正方向
  • Y轴:向下为正方向
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Canvas坐标系统演示</title>
    <style>
        canvas {
            border: 1px solid #333;
            background: #f9f9f9;
        }
    </style>
</head>
<body>
    <canvas id="coordinateCanvas" width="600" height="400"></canvas>

    <script>
        const canvas = document.getElementById('coordinateCanvas');
        const ctx = canvas.getContext('2d');
        
        // 绘制坐标轴
        ctx.strokeStyle = '#333';
        ctx.lineWidth = 1;
        
        // X轴
        ctx.beginPath();
        ctx.moveTo(0, 200);
        ctx.lineTo(600, 200);
        ctx.stroke();
        
        // Y轴
        ctx.beginPath();
        ctx.moveTo(300, 0);
        ctx.lineTo(300, 400);
        ctx.stroke();
        
        // 标记原点
        ctx.fillStyle = 'red';
        ctx.fillRect(298, 198, 4, 4);
        ctx.fillText('原点 (0,0)', 310, 190);
        
        // 标记坐标点示例
        ctx.fillStyle = 'blue';
        ctx.fillRect(100, 100, 4, 4); // 点(100,100)
        ctx.fillText('(100,100)', 110, 90);
        
        ctx.fillStyle = 'green';
        ctx.fillRect(400, 300, 4, 4); // 点(400,300)
        ctx.fillText('(400,300)', 410, 290);
    </script>
</body>
</html>

绘制线条

基本语法

绘制线条需要使用路径(Path)  API,遵循以下步骤:

  1. beginPath() - 开始新路径
  2. moveTo(x, y) - 移动到起点
  3. lineTo(x, y) - 绘制到终点
  4. stroke() - 描边路径

线条样式属性:

属性名 说明 可选值/示例
lineWidth 设置线条的宽度(单位:像素) 3510
strokeStyle 设置线条的颜色或样式 'red''#ff0000''rgba(255,0,0,0.5)'
lineCap 设置线条末端的样式 'butt'(平直)、'round'(圆形)、'square'(方形)
lineJoin 设置两条线段连接处的样式 'miter'(尖角)、'round'(圆角)、'bevel'(斜角)
setLineDash() 设置虚线模式 [5, 3](5px实线,3px空白)

绘制圆形和圆弧

基本语法

Canvas使用arc()方法绘制圆形和圆弧:

ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise);

属性说明:

参数名 说明 例子
x 圆心的X坐标 100
y 圆心的Y坐标 150
radius 圆的半径 50
startAngle 起始角度(弧度制) 0
endAngle 结束角度(弧度制) Math.PI * 2
anticlockwise 绘制方向 false

角度与弧度转换

Canvas使用弧度制而非角度制:

  • 180° = π 弧度
  • 360° = 2π 弧度
  • 转换公式:弧度 = 角度 * (Math.PI / 180)

代码示例:绘制完整圆形,圆形边框,半圆,四分之一圆,复杂圆弧(扇形),使用arcTo绘制圆角

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Canvas圆形绘制</title>
    <style>
        canvas {
            border: 1px solid #333;
            background: #fff;
        }
    </style>
</head>
<body>
    <canvas id="circleCanvas" width="600" height="400"></canvas>

    <script>
        const canvas = document.getElementById('circleCanvas');
        const ctx = canvas.getContext('2d');
        
        // 示例1:绘制完整圆形(填充)
        ctx.beginPath();
        ctx.arc(100, 100, 60, 0, Math.PI * 2); // 完整圆形
        ctx.fillStyle = 'rgba(52, 152, 219, 0.7)';
        ctx.fill();
        
        // 示例2:绘制圆形边框
        ctx.beginPath();
        ctx.arc(250, 100, 60, 0, Math.PI * 2);
        ctx.strokeStyle = '#e74c3c';
        ctx.lineWidth = 5;
        ctx.stroke();
        
        // 示例3:绘制半圆
        ctx.beginPath();
        ctx.arc(400, 100, 60, 0, Math.PI); // 0到π = 180度
        ctx.fillStyle = '#2ecc71';
        ctx.fill();
        
        // 示例4:绘制四分之一圆
        ctx.beginPath();
        ctx.arc(100, 250, 60, 0, Math.PI / 2); // 0到π/2 = 90度
        ctx.strokeStyle = '#f39c12';
        ctx.lineWidth = 5;
        ctx.stroke();
        
        // 示例5:绘制复杂圆弧(扇形)
        ctx.beginPath();
        ctx.moveTo(250, 250); // 移动到圆心
        ctx.arc(250, 250, 60, Math.PI / 4, Math.PI * 1.5); // 45度到270度
        ctx.closePath(); // 连接回圆心形成扇形
        ctx.fillStyle = 'rgba(155, 89, 182, 0.7)';
        ctx.fill();
        
        // 示例6:使用arcTo绘制圆角
        ctx.beginPath();
        ctx.moveTo(350, 200);
        ctx.arcTo(450, 200, 450, 300, 30); // 创建圆角
        ctx.lineTo(450, 300);
        ctx.strokeStyle = '#34495e';
        ctx.lineWidth = 3;
        ctx.stroke();
        
        // 添加说明文字
        ctx.fillStyle = '#333';
        ctx.font = '12px Arial';
        ctx.fillText('完整圆形', 80, 180);
        ctx.fillText('圆形边框', 230, 180);
        ctx.fillText('半圆', 385, 180);
        ctx.fillText('四分之一圆', 75, 330);
        ctx.fillText('扇形', 240, 330);
        ctx.fillText('arcTo圆角', 370, 330);
    </script>
</body>
</html>

圆弧绘制方法

  1. arc()  - 绘制标准圆弧,最常用
  2. arcTo()  - 通过两个控制点绘制圆弧,适合创建圆角
  3. ellipse()  - 绘制椭圆弧(可控制椭圆半径和旋转)
综合示例:绘制简单笑脸

屏幕截图 2025-11-08 212855.png

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Canvas笑脸绘制</title>
    <style>
        canvas {
            border: 1px solid #333;
            background: #fff;
        }
    </style>
</head>
<body>
    <canvas id="smileyCanvas" width="400" height="400"></canvas>

    <script>
        const canvas = document.getElementById('smileyCanvas');
        const ctx = canvas.getContext('2d');
        
        // 绘制脸部(圆形)
        ctx.beginPath();
        ctx.arc(200, 200, 150, 0, Math.PI * 2);
        ctx.fillStyle = '#FFD700'; // 金黄色
        ctx.fill();
        ctx.strokeStyle = '#D4AF37';
        ctx.lineWidth = 3;
        ctx.stroke();
        
        // 绘制左眼
        ctx.beginPath();
        ctx.arc(150, 160, 25, 0, Math.PI * 2);
        ctx.fillStyle = 'white';
        ctx.fill();
        ctx.strokeStyle = '#333';
        ctx.stroke();
        
        // 绘制左眼珠
        ctx.beginPath();
        ctx.arc(150, 160, 10, 0, Math.PI * 2);
        ctx.fillStyle = '#333';
        ctx.fill();
        
        // 绘制右眼
        ctx.beginPath();
        ctx.arc(250, 160, 25, 0, Math.PI * 2);
        ctx.fillStyle = 'white';
        ctx.fill();
        ctx.strokeStyle = '#333';
        ctx.stroke();
        
        // 绘制右眼珠
        ctx.beginPath();
        ctx.arc(250, 160, 10, 0, Math.PI * 2);
        ctx.fillStyle = '#333';
        ctx.fill();
        
        // 绘制嘴巴(微笑弧线)
        ctx.beginPath();
        ctx.arc(200, 220, 80, 0.1 * Math.PI, 0.9 * Math.PI); // 微笑弧度
        ctx.strokeStyle = '#333';
        ctx.lineWidth = 5;
        ctx.stroke();
    </script>
</body>
</html>

总结

Canvas绘图的三个核心基础:

  1. 坐标系统:理解基于左上角的坐标系是精确定位的基础
  2. 线条绘制:学会使用路径API创建直线、折线和虚线
  3. 圆形绘制:掌握使用arc()方法绘制圆形、圆弧和扇形

Trae 推出 Solo 模式:AI 开发的“一人一项目”时代来了?

作者 烟袅
2025年11月14日 13:31

“产品经理找程序员写原型?不需要了。产品经理就是程序员。”

最近,AI 开发平台 Trae 正式推出了全新功能 —— Solo 模式,彻底重构了传统软件开发流程。它不再只是“代码生成器”,而是进化为一个全栈 AI 开发工程师,从需求到交付,全程高能。

这不仅是工具的升级,更是一场开发范式的革命


🚀 什么是 Solo 模式?

Solo 模式 是 Trae 提出的全新 AI 驱动开发范式,旨在打造沉浸式的智能开发体验

你可以把它理解为:
👉 你只需提出一个想法,AI 就能独立完成从需求分析、产品设计、UI 构建、状态管理、API 对接、数据库设计、测试到部署的全过程。

换句话说:

✅ 只需一句话,就能让 AI 从零构建一个完整网站或应用。


💡 为什么叫“Solo”?

因为——一个人,一个想法,就能启动一个项目

过去,一个产品上线需要:

  • 产品经理写 PRD
  • UI 设计师出稿
  • 前后端开发协作
  • 测试、运维跟进

而现在,你只需要提出需求,剩下的交给 AI 完成


🎯 实战演示:用 Solo 模式创建“绘本岛”

我们来试试看,如何用 Trae 的 Solo 模式,快速搭建一个名为 “绘本岛” 的亲子阅读网站。

第一步:提出需求

创建一个名为“绘本岛”的亲子阅读网站。
品牌调性:温馨、童趣、色彩明亮。
核心功能:
- 用户注册/登录
- 分类浏览绘本(按年龄、主题)
- 在线阅读(支持翻页动画)
- 收藏与分享
- 每日推荐
视觉风格:扁平化 + 卡通插画风,主色调为蓝色和橙色。

👉 这些信息就像你在跟一位全能的 AI 工程师对话。


第二步:AI 自动生成完整方案

Trae 的 Solo 模式会自动完成以下步骤:

  1. 生成 PRD(产品需求文档)
  2. 输出设计稿(含交互逻辑)
  3. 产出技术方案(前端架构 + 后端接口设计)
  4. 自动生成数据库结构(如用户表、绘本表、收藏表)
  5. 编写前端代码(React/Vue + Tailwind)
  6. 搭建 API 服务(Node.js 或 Python)
  7. 配置测试用例
  8. 一键部署至云环境

⚡️ 所有过程无需人工干预,AI 自动规划、分步执行。


第三步:一键启动

点击「启动项目」按钮,整个系统自动运行,几分钟内即可看到可交互的原型页面

你甚至可以:

  • 直接在浏览器中预览
  • 修改文案或样式
  • 快速迭代功能

🌐 从此,非技术人员也能拥有自己的产品原型


🔁 传统流程 vs. Solo 模式对比

环节 传统方式 Solo 模式
需求沟通 多方协调,耗时长 一次输入,AI 理解并执行
原型设计 UI 设计师手绘 → 评审 → 修改 AI 自动生成交互原型
技术方案 开发团队讨论 → 写文档 AI 输出完整技术架构
代码实现 手动编码,易出错 AI 编写高质量代码
部署上线 手动部署,依赖运维 一键部署,自动完成

效率提升 10 倍以上,成本近乎归零。


🤔 谁会受益?

  • 产品经理:不用再找程序员写 demo,自己就能验证想法。
  • 创业者:快速验证 MVP,降低试错成本。
  • 非技术人员:轻松实现创意落地。
  • 开发者:专注复杂业务逻辑,不再重复造轮子。

💡 未来,人人都是产品经理,人人都是开发者


🔮 未来展望:AI Development,不只是 AI Coding

Trae 的目标是实现 AI Development(整体系统构建) ,而不仅仅是 AI Coding(代码生成)

这意味着:

  • AI 不再是辅助工具
  • 而是主导开发流程的智能体
  • 编辑器、终端、文档、浏览器……全部整合进 AI 工作流

🌐 我们正在进入一个“AI 主导开发”的新时代。


✅ 总结

Trae 的 Solo 模式,不是一次功能更新,而是一次开发模式的颠覆

它让我们看到:

  • 未来的开发,可能是“提需求 → 等结果”
  • 产品的边界,将由想法决定,而非资源限制

🚀 当你还在写 PRD 时,别人已经用 AI 把产品跑起来了。


📌 如果你也想体验这种“一人一项目”的开发快感,不妨关注 Trae 的官方动态,第一时间尝鲜 Solo 模式!

一文搞懂 CSS 定位:relative、absolute、fixed、sticky

作者 烟袅
2025年11月14日 13:27

在前端开发中,CSS 的 position 属性是布局的核心之一。理解不同定位方式的原理和使用场景,能让你轻松应对各种页面布局需求。

今天我们就来梳理一下常见的几种定位方式:relativeabsolutefixedsticky,以及它们与文档流的关系。


🌐 什么是文档流?

文档流是 HTML 元素默认的布局方式:

  • 块级元素垂直排列
  • 行内元素水平排列
  • 遵循从上到下、从左到右的自然顺序

当一个元素脱离了文档流,它不再占据原来的位置,后面的元素会“填补”它的空间。


🔹 1. position: relative —— 相对定位

position: relative;
  • 相对于自身原本位置进行偏移
  • 不会脱离文档流,原位置依然保留
  • 后续元素仍按标准流布局

✅ 适用于需要微调位置但不破坏布局的场景,比如配合 top/bottom/left/right 调整元素位置。


🔹 2. position: absolute —— 绝对定位

position: absolute;
  • 脱离文档流,不再占据空间
  • 相对于最近的 拥有定位属性的父元素 定位
  • 如果父元素没有定位,则以 body 或最近非 static 的祖先为参考

⚠️ 使用时注意:绝对定位的元素会“漂浮”在其他元素之上,需谨慎控制层级(z-index)。


🔹 3. position: fixed —— 固定定位

position: fixed;
  • 以浏览器窗口为参照物
  • 脱离文档流
  • 滚动页面时,元素位置固定不变

✅ 常用于顶部导航栏、侧边栏等需要“固定显示”的组件。


🔹 4. position: sticky —— 粘性定位

position: sticky;
  • 结合 relative 和 fixed 的特性
  • 默认行为像 relative
  • 当滚动到指定阈值(如 top: 0)时,变为 fixed,固定在视口某处

💡 例如:粘性标题、悬浮菜单,用户体验更友好。

.sticky-header {
  position: sticky;
  top: 0;
  background: #fff;
}

🔹 5. position: static —— 静态定位(默认)

position: static; /* 默认值 */
  • 元素按照正常文档流布局
  • topbottomleftright 无效
  • 一般不需要显式声明,除非要重置定位

🧩 总结对比表

定位方式 是否脱离文档流 参考对象 适用场景
relative ❌ 不脱离 自身原始位置 微调位置
absolute ✅ 脱离 最近定位父元素或 body 弹窗、遮罩层
fixed ✅ 脱离 浏览器窗口 固定导航、悬浮按钮
sticky 部分脱离 视口 + 文档流 粘性头部、侧边栏
static ❌ 不脱离 默认状态,无需设置

✅ 小贴士

  • 使用 absolute 时,记得给父容器设置 position: relative,避免定位混乱。
  • sticky 需要设置 top/bottom 等属性才生效。
  • display: none 会隐藏元素且不占空间,与定位无关,但常被混淆。
❌
❌