普通视图

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

HarmonyOSNext 端云一体化(4)

作者 万少
2025年1月18日 00:54

HarmonyOSNext 端云一体化(4)

在上一章节我们讲了数据库数据表的一些基本操作。如query、upsert、delete和calculateQuery。这一章节主要来讲解各种查询条件操作。如 查询班级年龄大于30的同学等。

查询条件解释

谓词,用来代替或者展示其客体性质、特征或者客体之间关系的词项

这些查询条件在端云一体中解释中叫做谓词。云数据库中提供丰富的谓词查询来构建查询条件。根据谓词查询方法构造自己的

DatabaseQuery对象。

查询条件谓词一览

关键字 说明
equalTo 表示等于的条件判断,用于查询中筛选出与指定值相等的数据
notEqualTo 表示不等于的条件判断,筛选出与指定值不相等的数据
beginsWith 表示以某个值开头,用于查询开头匹配特定字符串的数据
endsWith 表示以某个值结尾,用于查询结尾匹配特定字符串的数据
contains 表示包含某个值,用于查询包含特定字符串的数据
greaterThan 表示大于,用于数值类型数据的比较,筛选出大于指定值的数据
greaterThanOrEqualTo 表示大于或等于,筛选出大于或等于指定值的数据
lessThan 表示小于,用于数值类型数据的比较,筛选出小于指定值的数据
lessThanOrEqualTo 表示小于或等于,筛选出小于或等于指定值的数据
in 用于判断某个值是否在指定的集合内,常用于查询符合多个值中某一个的数据
isNull 用于判断某个字段是否为空值
isNotNull 用于判断某个字段是否不为空值
orderByAsc 按升序排列,用于对查询结果按照指定字段进行从小到大的排序
orderByDesc 按降序排列,用于对查询结果按照指定字段进行从大到小的排序
limit 限制查询结果返回的数量
beginGroup 开始一个逻辑分组,用于将多个条件组合在一起作为一个逻辑单元
endGroup 结束一个逻辑分组
or 逻辑或,用于连接多个条件,只要其中一个条件满足则整个逻辑表达式为真
and 逻辑与,用于连接多个条件,只有所有条件都满足时整个逻辑表达式才为真

谓词使用示例

equalTo 查询id为20的数据

this.condition.equalTo("id", 20) 

notEqualTo 查询id不等于20的数据

this.condition.notEqualTo("id", 20) 

beginsWith 查询name字段以b开头的数据

this.condition.beginsWith("name", "b") 

endsWith 查询name字段以k结尾的数据

this.condition.endsWith("name", "k") 

contains 查询name字段包含k的数据

this.condition.contains("name", "k") 

greaterThan 查询price字段大于30的数据

this.condition.greaterThan("price", 30) 

greaterThanOrEqualTo 查询price字段大于或者等于30的数据

this.condition.greaterThanOrEqualTo("price", 30) 

lessThan 查询price字段小于30的数据

this.condition.lessThan("price", 30) 

lessThanOrEqualTo 查询price字段小于或者等于30的数据

this.condition.lessThanOrEqualTo("price", 30) 

in 查询name字段包含在["book","aaaa","bbbb"]的中数据

this.condition.in("name", ["book", "aaaa", "bbbb"]) 

isNull 查询name字段是否为null

this.condition.isNull("name") 

isNotNull 查询name字段是否非null

this.condition.isNotNull("name") 

orderByAsc 根据id,进行升序

this.condition.orderByAsc("id") 

orderByDesc 根据id,进行降序

this.condition.orderByDesc("id") 

limit 查询2条数据,从第1条开始

this.condition.limit(2, 1) 

or 逻辑或,查询name=book 或者 price>30的数据

this.condition.equalTo("name", "book").or().greaterThan('price', 30) 

and 逻辑与,查询name=book123 并且 price>30的数据

this.condition.equalTo("name", "book123").and().greaterThan('price', 30) 

beginGroup 和 endGroup 表示一对逻辑分组

 // 条件1:   name=book并且price>30

  // 条件2:   id=20或者price>30

  // 需求: 查询 条件1 和 条件2 同时满足的数据
  this.condition
    .beginGroup()
    .equalTo('name', 30)
    .and()
    .greaterThan('price', 30)
    .endGroup()
    .and()
    .beginGroup()
    .equalTo('id', 20)
    .or()
    .greaterThan('price', 30)
    .endGroup();

加强

上面的谓词,也是根据实际语义搭配一起使用。比如:查询name=book的前2条数据

总结

本章主要介绍了HarmonyOSNext端云一体化中的数据库查询条件操作:

  1. 介绍了查询条件中的谓词概念,它用于展示或描述数据的性质、特征或关系

  2. 详细列举了常用的查询谓词,包括:

    • 比较类:equalTo、notEqualTo、greaterThan、lessThan等
    • 字符串匹配:beginsWith、endsWith、contains
    • 空值判断:isNull、isNotNull
    • 集合操作:in
    • 排序限制:orderByAsc、orderByDesc、limit
    • 逻辑组合:and、or、beginGroup、endGroup
  3. 通过具体示例展示了各种谓词的使用方法,包括基本查询和复杂的组合查询

  4. 说明了谓词可以根据实际需求灵活组合使用,以实现更复杂的查询功能


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

每日一题-求出数组中最大序列值🔴

2025年1月18日 00:00

给你一个整数数组 nums 和一个  整数 k 。

定义长度为 2 * x 的序列 seq 的  为:

  • (seq[0] OR seq[1] OR ... OR seq[x - 1]) XOR (seq[x] OR seq[x + 1] OR ... OR seq[2 * x - 1]).

请你求出 nums 中所有长度为 2 * k 的 子序列最大值 。

 

示例 1:

输入:nums = [2,6,7], k = 1

输出:5

解释:

子序列 [2, 7] 的值最大,为 2 XOR 7 = 5 。

示例 2:

输入:nums = [4,2,5,6,7], k = 2

输出:2

解释:

子序列 [4, 5, 6, 7] 的值最大,为 (4 OR 5) XOR (6 OR 7) = 2 。

 

提示:

  • 2 <= nums.length <= 400
  • 1 <= nums[i] < 27
  • 1 <= k <= nums.length / 2

jsonp解决前端跨域问题

2025年1月17日 23:45

各位大佬我们今天聊聊前端热门八股-跨域问题

那什么叫从一个域到另一个域呢?

在 Web 开发中,"域"(或"源")是指一个特定的协议、域名和端口的组合。浏览器的同源策略限制了从一个源加载的文档或脚本与另一个源的资源进行交互。理解域的概念对于理解跨域资源共享(CORS)非常重要。

域的组成

一个域由以下三个部分组成:

  • 协议:如 http 或 https。

  •  域名:如 example.com。

  • 端口:如 80(HTTP 的默认端口)或 443(HTTPS 的默认端口)。

比如掘金的域

image.png

同源策略

同源策略是浏览器的一种安全机制,限制了从一个源加载的文档或脚本与另一个源的资源进行交互。只有当协议、域名和端口都相同时,两个 URL 才被认为是同源的。

但是为什么浏览器要阻止这种访问资源的行为,我们来说几个常见的跨域安全问题

1. 防止跨站请求伪造(CSRF)

  • 跨站请求伪造是一种攻击方式,攻击者诱导用户的浏览器在用户不知情的情况下执行不当的操作。例如,用户登录到银行网站后,攻击者可能会诱导用户访问一个恶意网站,该网站会在用户不知情的情况下向银行网站发送请求,执行转账等操作。

2. 防止跨站脚本攻击(XSS)

  • 跨站脚本攻击允许攻击者在其他网站的上下文中执行恶意脚本。通过限制跨域请求,浏览器可以减少攻击者在用户访问的其他网站上执行恶意脚本的机会。

3. 保护用户隐私

  • 浏览器的同源策略保护用户的敏感信息不被恶意网站访问。例如,用户的会话信息、登录状态和其他敏感数据通常存储在 cookies 中,限制跨域请求可以防止这些信息被不可信的来源访问。

4. 防止数据泄露

  • 如果没有同源策略,恶意网站可以轻松地从其他网站获取数据,可能导致数据泄露。例如,攻击者可以从用户访问的其他网站窃取个人信息、交易记录等。

5. 确保数据完整性

  • 同源策略确保数据的完整性,防止恶意网站在用户不知情的情况下修改或操纵数据。

跨域请求

当一个网页尝试从不同的源请求资源时,就会发生跨域请求。例如:

我们用fetch模拟一下跨域访问

首先初始化一个后端项目,并且运行在3000端口

//http 服务启动
// commonjs模块规范node早期 引入http模块
const http=require('http');
const server=http.createServer((req,res)=>{
    //异步回调 
    //当请求来到服务器后,该函数会被执行 req请求对象被解析,res响应对象被创建 http结束

    //发送响应体
    res.end('hello world');
});
    console.log('服务已启动,端口号:3000');
});

我们使用nodemon热更新让后端跑在3000端口,使用live'server启动前端在5500端口我们前端用fecth请求3000端口会发现报错了,浏览器报了一个跨域访问出错,所以即使在同一个局域网,端口号不同的情况下,这算一个跨域请求

我们今天用jsonp来实现这跨域资源访问

 <ul id="list"></ul>
<script src=''http://localhost:3000''></script>
<script>
function callback(data) {
 list.innerHTML = data.map(user => `<li>${user.id+user.name}</li>`).join('');
}
</script>
                      

后端

//http 服务启动
// commonjs模块规范node早期 引入http模块
const http=require('http');
const user=[
    {
    id:1,
    name:'张三'
    },
    {
    id:2,
    name:'李四'
    }]
const server=http.createServer((req,res)=>{
    //异步回调 
    //当请求来到服务器后,该函数会被执行 req请求对象被解析,res响应对象被创建 http结束

    //发送响应体
    res.end('callback('+JSON.stringify(user)+')');
});

server.listen(3000,()=>{
    console.log('服务已启动,端口号:3000');
});

首先我们把script的源设置在与后端同一个端口,这里小编设置在3000端口,我们把后端返回的数据使用js的api挂载在ul标签上,这段代码的script标签从3000端口加载资源,看似浏览器能正常解析后端返回的js数据但是

各位大佬先别运行这段代码,我们先来想想这段代码的致命问题是什么,不错script标签阻塞

image.png

image.png 我们在浏览器启动前端会发现控制台报了一个callback没定义的问题,其实这段代码按顺序执行的时候,浏览器先解析了上面的script标签,从3000端口加载资源,但是此时的前端执行了返回的callback函数,<script src="http://localhost:3000"> 直接加载了这个脚本,而没有定义 callback 函数,导致浏览器无法正确执行返回的 JavaScript 代码。所以我们引出今天的跨域解决方案jsonp

我们把回调函数,与跨域的src封装在jsonp函数中这样只要改动函数中的src便能正确的处理后端返回的数据

前端

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

</head>
<body>
    <ul id="list"></ul>
    <script>
            

        let list = document.getElementById('list');
        // function callback(data) {
        //     list.innerHTML = data.map(user => `<li>${user.id+user.name}</li>`).join('');
        // }
        // fetch('http://127.0.0.1:3000').then(res => res.json())
        // fetch('http://localhost:3000')
        let jsonp=(url,callback)=>{
            let oScript=document.createElement('script')
            oScript.src=url          
            document.body.appendChild(oScript)
            window.callback=callback
        }
        jsonp('http://localhost:3000',(user)=>{ list.innerHTML = user.map(user => `<li>${user.id+user.name}</li>`).join('');})
    </script>
</body>
</html>

我们把后端callback函数挂载到window上

  • 动态创建 

  • 浏览器允许从不同源加载脚本文件,因此可以利用 

  • 回调函数:

  • 服务器返回的数据被包装在一个回调函数中。客户端在请求时指定回调函数的名称,服务器将数据作为参数传递给这个回调函数。

  • 数据传输:

  • 服务器返回的响应是一个 JavaScript 文件,其中包含对回调函数的调用,并将数据作为参数传递。

后端

//http 服务启动
// commonjs模块规范node早期 引入http模块
const http=require('http');
const user=[
    {
    id:1,
    name:'张三'
    },
    {
    id:2,
    name:'李四'
    }]
const server=http.createServer((req,res)=>{
    //异步回调 
    //当请求来到服务器后,该函数会被执行 req请求对象被解析,res响应对象被创建 http结束

    //发送响应体
    res.end('callback('+JSON.stringify(user)+')');
});

server.listen(3000,()=>{
    console.log('服务已启动,端口号:3000');
});

我们重新打开浏览器我们发现后端返回的数据正确的被浏览器解析到页面上,这样我们便使用jsonp实现了简单的跨域资源访问

Rnote:Star 8.6k,github上的宝藏项目,手绘与手写画图笔记,用它画图做笔记超丝滑,值得尝试!

2025年1月17日 23:36

嗨,大家好,我是小华同学,关注我们获得“最新、最全、最优质”开源项目和高效工作学习方法

Rnote是一款开源的基于矢量的绘图应用,专为学生、教师以及绘图板用户设计。它支持草图绘制、手写笔记以及对文档和图片进行注释。Rnote提供了PDF和图片的导入导出功能、无限画布以及适应不同屏幕尺寸的UI界面。

image.png

功能特点

Rnote以其强大的功能和灵活的用户体验著称,以下是它的一些核心特性:

  • 自适应UI:专注于手写笔输入,提供流畅的绘图体验。
  • 压力感应:支持不同配置的笔触样式,实现压力感应输入。
  • 形状工具:创建多种不同的形状。
  • 编辑工具:移动、旋转、缩放和修改现有内容。
  • 文档布局:提供多种文档扩展布局选项。
  • 背景定制:自定义背景颜色、图案和大小。
  • 页面格式:自定义页面格式。
  • 声音效果:可选的笔触声音。
  • 快捷键配置:可重新配置的手写笔按钮快捷键。
  • 工作区浏览器:集成的工作区浏览器,快速访问相关文件。
  • 拖放与剪贴板支持:支持拖放和剪贴板功能。
  • 图片导入:支持PDF、位图和SVG图片导入。
  • 文档导出:文档可以导出为SVG、PDF和Xopp格式,文档页面和选择可以导出为SVG、PNG和JPEG格式。
  • 文件格式:保存和加载文档使用原生.rnote文件格式。
  • 多文档工作:使用标签在多个文档之间工作。
  • 自动保存与打印:支持自动保存和打印功能。

官方网站

Rnote拥有一个项目网站:rnote.flxzt.net

安装指南

Rnote支持多种操作系统,以下是各个平台的安装方法:

Linux

在Flahub上下载官方的Flatpak版本:点击这里

在Flahub上下载

在Flahub上下载

MacOS

感谢@dehesselle,Rnote现在可以在MacOS上作为应用包使用。查看仓库,最新版本可以在这里下载:点击这里

下载MacOS应用包

下载MacOS应用包

Windows

从最新发布中下载Windows安装程序:点击这里

下载Windows安装程序

下载Windows安装程序

也可以使用Winget安装:

winget install flxzt.rnote

降级

由于文件格式的不稳定性,有时可能需要降级到特定版本。

列出Flahub上所有可用的旧版本:

flatpak remote-info --log flathub com.github.flxzt.rnote

选择所需版本的提交,并使用以下命令降级:

sudo flatpak update --commit=<commit-hash> com.github.flxzt.rnote

降级后,可以使用以下命令固定或取消固定Flahub版本:

$ flatpak mask com.github.flxzt.rnote
$ flatpak mask --remove com.github.flxzt.rnote

要再次更新到最新版本,取消固定并运行flatpak update

截图预览

以下是Rnote的一些截图,展示了应用的不同功能和界面:

image.png

image.png

image.png

image.png

image.png

image.png

image.png

常见问题和已知问题

在使用Rnote时,可能会遇到以下问题:

  • 拖放功能不工作:确保Rnote有权访问你拖放文件的位置。可以在Flatseal(Flahub权限管理器)中授权。

  • 当前文件位置奇怪:当标题栏中显示的目录类似于/run/user/1000/../时,Rnote可能没有权限访问该目录。同样,在Flatseal中授权可以解决这个问题。

  • 手写笔按钮移动画布/功能不正常:确保已安装并加载了xf86-input-wacom(X11驱动)、libinput(Wayland)和libwacom

  • 使用手写笔悬停时,某些屏幕区域的其他输入事件被阻止:这可能是手掌拒绝功能,但如果不需要,可以检查是否有左右手系统设置,并确保设置正确。Rnote无法禁用此功能。(讨论见#329

  • 手写笔按钮快捷方式映射不符合预期:在某些设备上,一个手写笔按钮被映射到专用的“橡皮擦”模式。在快捷方式设置中的按钮可能会不一致(次要/上按钮实际上是主要/下按钮,或相反)。要更改映射到此“橡皮擦”模式的工具,请按照以下步骤操作:

    • 将手写笔悬停在画布上,并按住被怀疑映射到“橡皮擦”模式的按钮
    • 在按住按钮的同时切换到所需的笔样式
    • 释放按钮时,它应该切换回之前的笔样式
    • “橡皮擦”模式中的笔样式现在应该被记住

字体

Rnote内置了以下字体:

  • Grape Nuts:Grape Nuts是一种简单的手写休闲字体。
  • OpenDyslexic-Regular:OpenDyslexic是一种针对一些常见阅读障碍症状设计的字体。
  • TT2020Base-Regular:TT2020是一款先进的开源超现实主义多语言打字机字体,适用于新的十年。
  • Virgil:Virgil是Excalidraw使用的字体。

同类项目比较

在开源社区中,还有其他一些类似的项目,如Excalidraw、Pizzara等。以下是它们的一些特点:

  • Excalidraw:一款简洁的在线绘图工具,支持手写笔输入,但功能相对单一。
  • Pizzara:一款创新的绘图应用,具有高级形状处理和无限缩放功能,但与Rnote相比,可能在文档注释方面稍显不足。
  • Inkscape:一个强大的矢量图形编辑器,适用于创建和编辑SVG文件。
  • Krita:一个专为概念艺术家、纹理画家、漫画家和插画家设计的开源绘画软件。
  • MyPaint:一个简约的绘画软件,专注于提供流畅的绘图体验。

总的来说,Rnote在功能丰富性、自定义选项和跨平台支持方面具有明显优势。

通过本文的介绍,相信你已经对Rnote有了更深入的了解。如果你是手写笔记和绘图的爱好者,不妨尝试一下这款开源应用,相信它会给你带来不一样的体验。

项目地址

https://github.com/flxzt/rnote

DP前后缀 + 枚举 ( 无合并 step by step)

2024年9月16日 20:25

Problem: 3287. 求出数组中最大序列值

思路

  1. 计算所有 下标 $j$ 前 $k$ 个数可能 $or$ 出的结果,记为$prefix[k-1][j+1][x]$
    因为$1 <= nums[j] < 2^7$,所以 $x < 2^7$
  2. 同理,计算所有 下标 $j$ 后 $k$ 个数可能 $or$ 出的结果,记为$suffix[k-1][j][x]$
  3. 枚举可能的分割位置 $i$ ,计算此位置前后的 $xor$ 值 = $prefix[i+1]$ ^ $suffix[i]$

Code

###Python3

class Solution:
    def maxValue(self, nums: List[int], k: int) -> int:
        n = len(nums)
        mx = reduce(or_, nums)
        prefix = [[[False] * (mx + 1) for i in range(n + 1)] for j in range(k + 1)]
        ans = -inf
        # 1. prefix or for k
        for i in range(n):
            prefix[0][i + 1] = prefix[0][i].copy()
            prefix[0][i + 1][nums[i]] = True
        for i in range(1, k):
            for j in range(i, n - k + 1):
                x = nums[j]
                # unselect
                prefix[i][j + 1] = prefix[i][j].copy()
                # select
                for h in range(0, mx + 1):
                    if prefix[i - 1][j][h]:
                        prefix[i][j + 1][h | x] = True
            # print( f[i])
        # 2. suffix or for k
        suffix = [[[False] * (mx + 1) for i in range(n + 1)] for j in range(k + 1)]
        for i in range(n - 1, -1, -1):
            suffix[0][i] = suffix[0][i + 1].copy()
            suffix[0][i][nums[i]] = True

        for i in range(1, k):
            for j in range(n - 1, k - 1, -1):
                x = nums[j]
                # unselect
                suffix[i][j] = suffix[i][j + 1].copy()
                # select
                for h in range(0, mx + 1):
                    if suffix[i - 1][j + 1][h]:
                        suffix[i][j][h | x] = True

        # 3. 枚举 所有 prefix xor suffix
        ans = -inf
        for i in range(k - 1, n - k + 1):  # [0, i] [i+1, i+k]
            pre = prefix[k - 1][i + 1]
            for j in range(1, mx + 1):
                if not pre[j]:
                    continue
                post = suffix[k - 1][i + 1]
                for h in range(1, mx + 1):
                    if post[h]:
                        ans = max(ans, j ^ h)
        return ans

复杂度

  • 时间复杂度: $O(nkU)$,$n$ 是 $nums$ 的长度,$U$ 是 $nums$ 所有元素的 $OR$,最多为 $2 ^ 7 -1$
  • 空间复杂度: $O(nkU)$

前后缀分解 + 二维 0-1 背包 + 优化所选元素个数 + 试填法(Python/Java/C++/Go)

作者 endlesscheng
2024年9月15日 08:45

题意

从 $\textit{nums}$ 中选一个长为 $2k$ 的子序列,计算其前一半的 OR,后一半的 OR,这两个 OR 再计算 XOR。

问:计算出的 XOR 最大能是多少?

核心思路

  • 想象有一根分割线,把 $\textit{nums}$ 分成左右两部分,左和右分别计算所有长为 $k$ 的子序列的 OR 都有哪些值。比如左边计算出的 OR 有 $2,3,5$,右边计算出的 OR 有 $1,3,6$,那么两两组合计算 XOR,其中最大值即为答案。
  • 枚举分割线的位置,把 $\textit{nums}$ 分割成一个前缀和一个后缀,问题变成:从前缀/后缀中选一个长为 $k$ 的子序列,这个子序列 OR 的结果能否等于 $x$?

把 OR 理解成一个类似加法的东西,转换成二维 0-1 背包。如果你不了解 0-1 背包,或者不理解为什么下面代码 $j$ 要倒序枚举,请看【基础算法精讲 18】

二维:指背包有两个约束,一个是所选元素的个数是 $k$,另一个是所选元素的 OR 是 $x$。

具体算法

计算后缀。对于 0-1 背包问题,我们定义 $f[i][j][x]$ 表示从 $\textit{nums}[i]$ 到 $\textit{nums}[n-1]$ 中选 $j$ 个数,这些数的 OR 能否等于 $x$。

设 $v=\textit{nums}[i]$,用刷表法转移:

  • 不选 $v$,那么 $f[i][j][x] = f[i+1][j][x]$。
  • 选 $v$,如果 $f[i+1][j][x]=\texttt{true}$,那么 $f[i][j+1][x\ |\ v]=\texttt{true}$。

刷表法:本题计算 $x = v\ |\ ?$ 中的 $?$ 是困难的,但计算 $x\ |\ v$ 是很简单的。也就是说,对于状态 $f[i][j][x]$ 而言,其转移来源是谁不好计算,但从 $f[i][j][x]$ 转移到的目标状态 $f[i][j+1][x\ |\ v]$ 是好计算的。在动态规划中,根据转移来源计算状态叫查表法,用当前状态更新其他状态叫刷表法。

初始值 $f[n][0][0]=\texttt{true}$。什么也不选,OR 等于 $0$。

对于每个 $i$,由于我们只需要 $f[i][k]$ 中的数据,把 $f[i][k]$ 复制到 $\textit{suf}[i]$ 中。这样做无需创建三维数组,空间复杂度更小。

代码实现时,$f$ 的第一个维度可以优化掉。

对于前缀 $\textit{pre}$ 的计算也同理。

最后,枚举 $i=k-1,k,k+1,\ldots,n-k-1$,两两组合 $\textit{pre}[i]$ 和 $\textit{suf}[i+1]$ 中的数计算 XOR,其中最大值即为答案。

小优化:如果在循环中,发现答案 $\textit{ans}$ 达到了理论最大值 $2^7-1$(或者所有元素的 OR),则立刻返回答案。

也可以用哈希集合代替布尔数组,见下面的 Python 优化代码。

具体请看 视频讲解 第三题,欢迎点赞关注~

优化前

###py

class Solution:
    def maxValue(self, nums: List[int], k: int) -> int:
        mx = reduce(or_, nums)
        n = len(nums)
        suf = [None] * (n - k + 1)
        f = [[False] * (mx + 1) for _ in range(k + 1)]
        f[0][0] = True
        for i in range(n - 1, k - 1, -1):
            v = nums[i]
            # 注意当 i 比较大的时候,循环次数应和 i 有关,因为更大的 j,对应的 f[j] 全为 False
            for j in range(min(k - 1, n - 1 - i), -1, -1):
                for x, has_x in enumerate(f[j]):
                    if has_x:
                        f[j + 1][x | v] = True
            if i <= n - k:
                suf[i] = f[k].copy()

        ans = 0
        pre = [[False] * (mx + 1) for _ in range(k + 1)]
        pre[0][0] = True
        for i, v in enumerate(nums[:-k]):
            for j in range(min(k - 1, i), -1, -1):
                for x, has_x in enumerate(pre[j]):
                    if has_x:
                        pre[j + 1][x | v] = True
            if i < k - 1:
                continue
            for x, has_x in enumerate(pre[k]):
                if has_x:
                    for y, has_y in enumerate(suf[i + 1]):
                        if has_y and x ^ y > ans:  # 手写 if
                            ans = x ^ y
            if ans == mx:
                return ans
        return ans

###py

# 使用 set 代替 bool list
class Solution:
    def maxValue(self, nums: List[int], k: int) -> int:
        n = len(nums)
        suf = [None] * (n - k + 1)
        f = [set() for _ in range(k + 1)]
        f[0].add(0)
        for i in range(n - 1, k - 1, -1):
            v = nums[i]
            for j in range(min(k - 1, n - 1 - i), -1, -1):
                f[j + 1].update(x | v for x in f[j])
            if i <= n - k:
                suf[i] = f[k].copy()

        mx = reduce(or_, nums)
        ans = 0
        pre = [set() for _ in range(k + 1)]
        pre[0].add(0)
        for i, v in enumerate(nums[:-k]):
            for j in range(min(k - 1, i), -1, -1):
                pre[j + 1].update(x | v for x in pre[j])
            if i < k - 1:
                continue
            ans = max(ans, max(x ^ y for x in pre[k] for y in suf[i + 1]))
            if ans == mx:
                return ans
        return ans

###java

class Solution {
    public int maxValue(int[] nums, int k) {
        final int MX = 1 << 7;
        int n = nums.length;
        boolean[][] suf = new boolean[n - k + 1][];
        boolean[][] f = new boolean[k + 1][MX];
        f[0][0] = true;
        for (int i = n - 1; i >= k; i--) {
            int v = nums[i];
            // 注意当 i 比较大的时候,循环次数应和 i 有关,因为更大的 j,对应的 f[j] 全为 false
            for (int j = Math.min(k - 1, n - 1 - i); j >= 0; j--) {
                for (int x = 0; x < MX; x++) {
                    if (f[j][x]) {
                        f[j + 1][x | v] = true;
                    }
                }
            }
            if (i <= n - k) {
                suf[i] = f[k].clone();
            }
        }

        int ans = 0;
        boolean[][] pre = new boolean[k + 1][MX];
        pre[0][0] = true;
        for (int i = 0; i < n - k; i++) {
            int v = nums[i];
            for (int j = Math.min(k - 1, i); j >= 0; j--) {
                for (int x = 0; x < MX; x++) {
                    if (pre[j][x]) {
                        pre[j + 1][x | v] = true;
                    }
                }
            }
            if (i < k - 1) {
                continue;
            }
            for (int x = 0; x < MX; x++) {
                if (pre[k][x]) {
                    for (int y = 0; y < MX; y++) {
                        if (suf[i + 1][y]) {
                            ans = Math.max(ans, x ^ y);
                        }
                    }
                }
            }
            if (ans == MX - 1) {
                return ans;
            }
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    int maxValue(vector<int>& nums, int k) {
        const int MX = 1 << 7;
        int n = nums.size();
        vector<array<int, MX>> suf(n - k + 1);
        vector<array<int, MX>> f(k + 1);
        f[0][0] = true;
        for (int i = n - 1; i >= k; i--) {
            int v = nums[i];
            // 注意当 i 比较大的时候,循环次数应和 i 有关,因为更大的 j,对应的 f[j] 全为 false
            for (int j = min(k - 1, n - 1 - i); j >= 0; j--) {
                for (int x = 0; x < MX; x++) {
                    if (f[j][x]) {
                        f[j + 1][x | v] = true;
                    }
                }
            }
            if (i <= n - k) {
                suf[i] = f[k];
            }
        }

        int ans = 0;
        vector<array<int, MX>> pre(k + 1);
        pre[0][0] = true;
        for (int i = 0; i < n - k; i++) {
            int v = nums[i];
            for (int j = min(k - 1, i); j >= 0; j--) {
                for (int x = 0; x < MX; x++) {
                    if (pre[j][x]) {
                        pre[j + 1][x | v] = true;
                    }
                }
            }
            if (i < k - 1) {
                continue;
            }
            for (int x = 0; x < MX; x++) {
                if (pre[k][x]) {
                    for (int y = 0; y < MX; y++) {
                        if (suf[i + 1][y]) {
                            ans = max(ans, x ^ y);
                        }
                    }
                }
            }
            if (ans == MX - 1) {
                return ans;
            }
        }
        return ans;
    }
};

###go

func maxValue(nums []int, k int) (ans int) {
const mx = 1 << 7
n := len(nums)
suf := make([][mx]bool, n-k+1)
f := make([][mx]bool, k+1)
f[0][0] = true
for i := n - 1; i >= k; i-- {
v := nums[i]
// 注意当 i 比较大的时候,循环次数应和 i 有关,因为更大的 j,对应的 f[j] 全为 false
for j := min(k-1, n-1-i); j >= 0; j-- {
for x, hasX := range f[j] {
if hasX {
f[j+1][x|v] = true
}
}
}
if i <= n-k {
suf[i] = f[k]
}
}

pre := make([][mx]bool, k+1)
pre[0][0] = true
for i, v := range nums[:n-k] {
for j := min(k-1, i); j >= 0; j-- {
for x, hasX := range pre[j] {
if hasX {
pre[j+1][x|v] = true
}
}
}
if i < k-1 {
continue
}
for x, hasX := range pre[k] {
if hasX {
for y, hasY := range suf[i+1] {
if hasY {
ans = max(ans, x^y)
}
}
}
}
if ans == mx-1 {
return
}
}
return
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(nkU + nU^2)$,其中 $n$ 是 $\textit{nums}$ 的长度,$U$ 是 $\textit{nums}$ 所有元素的 OR,本题至多为 $2^7-1$。DP 是 $\mathcal{O}(nkU)$ 的,计算 XOR 最大值是 $\mathcal{O}(nU^2)$ 的。
  • 空间复杂度:$\mathcal{O}(nU)$。

优化

例如 $x=1101_{(2)}$,我们至多要选几个 $\textit{nums}[i]$,就能 OR 得到 $x$?(前提是可以得到 $x$)

答案是 $3$ 个。考虑 $x$ 中的每个比特 $1$,它来自某个 $\textit{nums}[i]$。

设 $\textit{nums}$ 所有元素 OR 的二进制中的 $1$ 的个数为 $\textit{ones}$(本题数据范围保证 $textit{ones}\le 7$)。一般地,我们至多选 $\textit{ones}$ 个 $\textit{nums}[i]$,就能 OR 得到 $x$。

但是,本题要求(前缀/后缀)恰好选 $k$ 个元素。选的元素越多 OR 越大,那么某些比较小的 $x$ 可能无法 OR 出来。

为了判断(前缀/后缀)恰好选 $k$ 个元素能否 OR 出整数 $x$,定义:

  • $\textit{minI}[x]$,表示从 $0$ 开始遍历,至少要遍历到 $i$ 才有可能找到 $k$ 个数 OR 等于 $x$。如果无法得到 $x$ 那么 $\textit{minI}[x] = \infty$。
  • $\textit{maxI}[x]$,表示从 $n-1$ 开始遍历,至少要遍历到 $i$ 才有可能找到 $k$ 个数 OR 等于 $x$。如果无法得到 $x$ 那么 $\textit{maxI}[x] = 0$。

根据 从集合论到位运算,如果能 OR 得到 $x$,那么参与 OR 运算的元素都是 $x$ 的子集。换句话说,$x$ 是参与 OR 运算的元素的超集(superset)。

对于 $\textit{minI}[x]$ 的计算,我们可以在遍历 $\textit{nums}$ 的同时,用一个数组 $\textit{cnt}$ 维护 $\textit{nums}$ 元素的超集的出现次数。如果发现 $\textit{cnt}[x]=k$,说明至少要遍历到 $i$ 才有可能找到 $k$ 个数 OR 等于 $x$,记录 $\textit{minI}[x]=i$。对于 $\textit{maxI}[x]$ 的计算也同理。

对于两数异或最大值的计算,可以用试填法解决,原理请看【图解】421. 数组中两个数的最大异或值

###py

class Solution:
    def maxValue(self, nums: List[int], k: int) -> int:
        n = len(nums)
        mx = reduce(or_, nums)
        k2 = min(k, mx.bit_count())  # 至多选 k2 个数

        suf = [None] * (n - k + 1)
        f = [set() for _ in range(k2 + 1)]
        f[0].add(0)
        max_i = [0] * (mx + 1)
        cnt = [0] * (mx + 1)
        for i in range(n - 1, k - 1, -1):
            v = nums[i]
            for j in range(min(k2 - 1, n - 1 - i), -1, -1):
                f[j + 1].update(x | v for x in f[j])
            if i <= n - k:
                suf[i] = f[k2].copy()
            # 枚举 v 的超集
            s = v
            while s <= mx:
                cnt[s] += 1
                if cnt[s] == k:
                    # 从 n-1 开始遍历,至少要遍历到 i 才有可能找到 k 个数 OR 等于 s
                    max_i[s] = i
                s = (s + 1) | v

        ans = 0
        pre = [set() for _ in range(k2 + 1)]
        pre[0].add(0)
        min_i = [inf] * (mx + 1)
        cnt = [0] * (mx + 1)
        w = mx.bit_length()  # 用于 findMaximumXOR
        for i, v in enumerate(nums[:-k]):
            for j in range(min(k2 - 1, i), -1, -1):
                pre[j + 1].update(x | v for x in pre[j])
            # 枚举 v 的超集
            s = v
            while s <= mx:
                cnt[s] += 1
                if cnt[s] == k:
                    # 从 0 开始遍历,至少要遍历到 i 才有可能找到 k 个数 OR 等于 s
                    min_i[s] = i
                s = (s + 1) | v
            if i < k - 1:
                continue
            a = [x for x in pre[k2] if min_i[x] <= i]
            b = [x for x in suf[i + 1] if max_i[x] > i]
            ans = max(ans, self.findMaximumXOR(a, b, w))
            if ans == mx:
                return ans
        return ans

    # 421. 数组中两个数的最大异或值
    # 改成两个数组的最大异或值,做法是类似的,仍然可以用【试填法】解决
    def findMaximumXOR(self, a: List[int], b: List[int], w: int) -> int:
        ans = mask = 0
        for i in range(w - 1, -1, -1):  # 从最高位开始枚举
            mask |= 1 << i
            new_ans = ans | (1 << i)  # 这个比特位可以是 1 吗?
            set_a = set(x & mask for x in a)  # 低于 i 的比特位置为 0
            for x in b:
                x &= mask  # 低于 i 的比特位置为 0
                if new_ans ^ x in set_a:
                    ans = new_ans  # 这个比特位可以是 1
                    break
        return ans

###java

class Solution {
    private static final int BIT_WIDTH = 7;

    public int maxValue(int[] nums, int k) {
        final int MX = 1 << BIT_WIDTH;
        int n = nums.length;
        int k2 = Math.min(k, BIT_WIDTH); // 至多选 k2 个数

        boolean[][] suf = new boolean[n - k + 1][];
        boolean[][] f = new boolean[k2 + 1][MX];
        f[0][0] = true;
        int[] maxI = new int[MX];
        int[] cnt = new int[MX];
        for (int i = n - 1; i >= k; i--) {
            int v = nums[i];
            for (int j = Math.min(k2 - 1, n - 1 - i); j >= 0; j--) {
                for (int x = 0; x < MX; x++) {
                    if (f[j][x]) {
                        f[j + 1][x | v] = true;
                    }
                }
            }
            if (i <= n - k) {
                suf[i] = f[k2].clone();
            }
            // 枚举 v 的超集
            for (int s = v; s < MX; s = (s + 1) | v) {
                if (++cnt[s] == k) {
                    // 从 n-1 开始遍历,至少要遍历到 i 才有可能找到 k 个数 OR 等于 s
                    maxI[s] = i;
                }
            }
        }

        int ans = 0;
        boolean[][] pre = new boolean[k2 + 1][MX];
        pre[0][0] = true;
        int[] minI = new int[MX];
        Arrays.fill(minI, Integer.MAX_VALUE);
        Arrays.fill(cnt, 0);
        int[] a = new int[MX];
        int[] b = new int[MX];
        for (int i = 0; i < n - k; i++) {
            int v = nums[i];
            for (int j = Math.min(k2 - 1, i); j >= 0; j--) {
                for (int x = 0; x < MX; x++) {
                    if (pre[j][x]) {
                        pre[j + 1][x | v] = true;
                    }
                }
            }
            // 枚举 v 的超集
            for (int s = v; s < MX; s = (s + 1) | v) {
                if (++cnt[s] == k) {
                    // 从 0 开始遍历,至少要遍历到 i 才有可能找到 k 个数 OR 等于 s
                    minI[s] = i;
                }
            }
            if (i < k - 1) {
                continue;
            }
            int na = 0;
            int nb = 0;
            for (int x = 0; x < MX; x++) {
                if (pre[k2][x] && minI[x] <= i) {
                    a[na++] = x;
                }
                if (suf[i + 1][x] && maxI[x] > i) {
                    b[nb++] = x;
                }
            }
            ans = Math.max(ans, findMaximumXOR(a, na, b, nb));
            if (ans == MX - 1) {
                return ans;
            }
        }
        return ans;
    }

    // 421. 数组中两个数的最大异或值
    // 改成两个数组的最大异或值,做法是类似的,仍然可以用【试填法】解决
    private int findMaximumXOR(int[] a, int n, int[] b, int m) {
        int ans = 0;
        int mask = 0;
        boolean[] seen = new boolean[1 << BIT_WIDTH];
        for (int i = BIT_WIDTH - 1; i >= 0; i--) { // 从最高位开始枚举
            mask |= 1 << i;
            int newAns = ans | (1 << i); // 这个比特位可以是 1 吗?
            Arrays.fill(seen, false);
            for (int j = 0; j < n; j++) {
                seen[a[j] & mask] = true; // 低于 i 的比特位置为 0
            }
            for (int j = 0; j < m; j++) {
                int x = b[j] & mask; // 低于 i 的比特位置为 0
                if (seen[newAns ^ x]) {
                    ans = newAns; // 这个比特位可以是 1
                    break;
                }
            }
        }
        return ans;
    }
}

###cpp

class Solution {
    static constexpr int BIT_WIDTH = 7;

    // 421. 数组中两个数的最大异或值
    // 改成两个数组的最大异或值,做法是类似的,仍然可以用【试填法】解决
    int findMaximumXOR(vector<int>& a, vector<int>& b) {
        int ans = 0, mask = 0;
        vector<int> seen(1 << BIT_WIDTH);
        for (int i = BIT_WIDTH - 1; i >= 0; i--) { // 从最高位开始枚举
            mask |= 1 << i;
            int new_ans = ans | (1 << i); // 这个比特位可以是 1 吗?
            ranges::fill(seen, false);
            for (int x : a) {
                seen[x & mask] = true; // 低于 i 的比特位置为 0
            }
            for (int x : b) {
                x &= mask; // 低于 i 的比特位置为 0
                if (seen[new_ans ^ x]) {
                    ans = new_ans; // 这个比特位可以是 1
                    break;
                }
            }
        }
        return ans;
    }

public:
    int maxValue(vector<int>& nums, int k) {
        const int MX = 1 << BIT_WIDTH;
        int n = nums.size();
        int k2 = min(k, BIT_WIDTH); // 至多选 k2 个数

        vector<array<int, MX>> suf(n - k + 1);
        vector<array<int, MX>> f(k2 + 1);
        f[0][0] = true;
        int max_i[MX]{}, cnt[MX]{};
        for (int i = n - 1; i >= k; i--) {
            int v = nums[i];
            // 注意当 i 比较大的时候,循环次数应和 i 有关,因为更大的 j,对应的 f[j] 全为 false
            for (int j = min(k2 - 1, n - 1 - i); j >= 0; j--) {
                for (int x = 0; x < MX; x++) {
                    if (f[j][x]) {
                        f[j + 1][x | v] = true;
                    }
                }
            }
            if (i <= n - k) {
                suf[i] = f[k2];
            }
            // 枚举 v 的超集
            for (int s = v; s < MX; s = (s + 1) | v) {
                if (++cnt[s] == k) {
                    // 从 n-1 开始遍历,至少要遍历到 i 才有可能找到 k 个数 OR 等于 s
                    max_i[s] = i;
                }
            }
        }

        int ans = 0;
        vector<array<int, MX>> pre(k2 + 1);
        pre[0][0] = true;
        int min_i[MX];
        ranges::fill(min_i, INT_MAX);
        ranges::fill(cnt, 0);
        for (int i = 0; i < n - k; i++) {
            int v = nums[i];
            for (int j = min(k2 - 1, i); j >= 0; j--) {
                for (int x = 0; x < MX; x++) {
                    if (pre[j][x]) {
                        pre[j + 1][x | v] = true;
                    }
                }
            }
            // 枚举 v 的超集
            for (int s = v; s < MX; s = (s + 1) | v) {
                if (++cnt[s] == k) {
                    // 从 0 开始遍历,至少要遍历到 i 才有可能找到 k 个数 OR 等于 s
                    min_i[s] = i;
                }
            }
            if (i < k - 1) {
                continue;
            }
            vector<int> a, b;
            for (int x = 0; x < MX; x++) {
                if (pre[k2][x] && min_i[x] <= i) {
                    a.push_back(x);
                }
                if (suf[i + 1][x] && max_i[x] > i) {
                    b.push_back(x);
                }
            }
            ans = max(ans, findMaximumXOR(a, b));
            if (ans == MX - 1) {
                return ans;
            }
        }
        return ans;
    }
};

###go

const bitWidth = 7
const mx = 1 << bitWidth

func maxValue(nums []int, k int) (ans int) {
n := len(nums)
k2 := min(k, bitWidth) // 至多选 k2 个数
suf := make([][mx]bool, n-k+1)
f := make([][mx]bool, k2+1)
f[0][0] = true
maxI := [mx]int{}
cnt := [mx]int{}
for i := n - 1; i >= k; i-- {
v := nums[i]
for j := min(k2-1, n-1-i); j >= 0; j-- {
for x, hasX := range f[j] {
if hasX {
f[j+1][x|v] = true
}
}
}
if i <= n-k {
suf[i] = f[k2]
}
// 枚举 v 的超集
for s := v; s < mx; s = (s + 1) | v {
cnt[s]++
if cnt[s] == k {
// 从 n-1 开始遍历,至少要遍历到 i 才有可能找到 k 个数 OR 等于 s
maxI[s] = i
}
}
}

pre := make([][mx]bool, k2+1)
pre[0][0] = true
minI := [mx]int{}
for i := range minI {
minI[i] = math.MaxInt
}
cnt = [mx]int{}
for i, v := range nums[:n-k] {
for j := min(k2-1, i); j >= 0; j-- {
for x, hasX := range pre[j] {
if hasX {
pre[j+1][x|v] = true
}
}
}
// 枚举 v 的超集
for s := v; s < mx; s = (s + 1) | v {
cnt[s]++
if cnt[s] == k {
// 从 0 开始遍历,至少要遍历到 i 才有可能找到 k 个数 OR 等于 s
minI[s] = i
}
}
if i < k-1 {
continue
}
a := []int{}
b := []int{}
for x, has := range pre[k2] {
if has && minI[x] <= i {
a = append(a, x)
}
if suf[i+1][x] && maxI[x] > i {
b = append(b, x)
}
}
ans = max(ans, findMaximumXOR(a, b))
if ans == mx-1 {
return
}
}
return
}

// 421. 数组中两个数的最大异或值
// 改成两个数组的最大异或值,做法是类似的,仍然可以用【试填法】解决
func findMaximumXOR(a, b []int) (ans int) {
mask := 0
for i := bitWidth - 1; i >= 0; i-- { // 从最高位开始枚举
mask |= 1 << i
newAns := ans | 1<<i // 这个比特位可以是 1 吗?
seen := [mx]bool{}
for _, x := range a {
seen[x&mask] = true // 低于 i 的比特位置为 0
}
for _, x := range b {
x &= mask // 低于 i 的比特位置为 0
if seen[newAns^x] {
ans = newAns
break
}
}
}
return
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(nU\log U)$,其中 $n$ 是 $\textit{nums}$ 的长度,$U$ 是 $\textit{nums}$ 所有元素的 OR,本题至多为 $2^7-1$。
  • 空间复杂度:$\mathcal{O}(nU)$。

更多相似题目,见下面动态规划题单中的「§3.1 0-1 背包」和「专题:前后缀分解」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/最短路/最小生成树/二分图/基环树/欧拉路径)
  7. 动态规划(入门/背包/状态机/划分/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

枚举 & DP

作者 tsreaper
2024年9月15日 00:16

解法:枚举 & DP

一般这种一个序列分两半的题目,我们会尝试枚举分界点。

设 $f(i, j, x)$ 表示从前 $i$ 个元素中选出 $j$ 个,OR 起来能否得到 $x$;$g(i, j, x)$ 表示从第 $i$ 个元素到最后一个元素中选出 $j$ 个,OR 起来能否得到 $x$。我们枚举子序列左半边的结束点 $i$,左半边 OR 起来的值 $x$,以及右半边 OR 起来的值 $y$,那么答案就是

$$
\max\limits_{f(i, k, x) = \text{true and } g(i + 1, k, y) = \text{true}} x \oplus y
$$

枚举的复杂度是 $\mathcal{O}(n \times m^2)$,其中 $m = 2^7$ 是题目中提到的取值范围。

剩下的问题就是 $f$ 和 $g$ 怎么求。考虑是否选择第 $i$ 个元素,可以得到转移方程

f(i, j, x) -> f(i + 1, j, x) // 不选 nums[i]
f(i, j, x) -> f(i + 1, j + 1, x | nums[i]) // 选 nums[i]

初值 f(0, 0, 0) = true,其它位置初值为 false。$g$ 的求法类似。这一部分的复杂度为 $\mathcal{O}(nkm)$。

参考代码(c++)

###cpp

bool f[410][210][1 << 7], g[410][210][1 << 7];

class Solution {
public:
    int maxValue(vector<int>& nums, int K) {
        int n = nums.size();
        int m = 1 << 7;
        for (int i = 0; i <= n + 1; i++) for (int j = 0; j <= K; j++) for (int x = 0; x < m; x++)
            f[i][j][x] = g[i][j][x] = false;

        auto update = [&](bool &x, bool y) { x = x || y; };

        // DP 求子序列前半部分的情况
        f[0][0][0] = true;
        for (int i = 0; i < n; i++) for (int j = 0; j <= K; j++) for (int x = 0; x < m; x++) if (f[i][j][x]) {
            update(f[i + 1][j][x], f[i][j][x]);
            if (j < K) update(f[i + 1][j + 1][x | nums[i]], f[i][j][x]);
        }

        // DP 求子序列后半部分的情况
        g[n + 1][0][0] = true;
        for (int i = n + 1; i > 1; i--) for (int j = 0; j <= K; j++) for (int x = 0; x < m; x++) if (g[i][j][x]) {
            update(g[i - 1][j][x], g[i][j][x]);
            if (j < K) update(g[i - 1][j + 1][x | nums[i - 2]], g[i][j][x]);
        }

        int ans = 0;
        // 枚举子序列的分界点,以及前后半的值
        for (int i = K; i + K <= n; i++) for (int x = 0; x < m; x++) for (int y = 0; y < m; y++)
            if (f[i][K][x] && g[i + 1][K][y]) ans = max(ans, x ^ y);
        return ans;
    }
};
昨天 — 2025年1月17日首页

美最高法支持政府对TikTok强迫出售令

2025年1月17日 23:27
本周日(1月19日)是美国政府强行要求短视频社交平台TikTok出售或面临被禁的最后期限。当地时间今天(1月17日)上午10点,北京时间深夜11点,美国最高法院裁定,支持这一法律。TikTok在美国有约1.7亿用户。去年4月24日,美国总统拜登签署一项国会参众两院通过的法案,要求TikTok母公司字节跳动在270天内将TikTok出售给非中国企业,否则这款应用程序将在美国被禁用。(CCTV国际时讯)

Langchian.js |Embedding & Vector Store👈| 数据向量化后这样储存😱

作者 浪遏
2025年1月17日 23:01

前言

书接上文 , 学习了分割多个文档对象 , 这一次要学习

  • 如何将数据向量化 ? 😍
  • 向量化的数据持久化储存 ? 😍

也就是说 ,下面这张图 ,要 over 了 , 🤡👈

Embedding

langchain.js 在文本处理领域 ,不仅提供我前面所学的文本分割与转换 , 也为文本的向量化提供了支持 , 这不禁让应用开发者尖叫 ~ , 所谓文本的嵌入 , 其机制就是 : 将复杂文本数据转换为具有固定维度的向量 , 在机器学习和检索任务中十分 nice ~

Embedding 就是嵌入 , 他是 Langchain.js 的一个核心组件

主要作用是 , 为各种文本嵌入模型交互而设计 , 为许多的模型提供统一的 、标准化的接口 ; 说到这里 , 我们可以思考 : 其实 langchain 框架本身就是为了提供“统一化 、标准化的接口”而生 , 它是 LLM 的上层应用框架 , 成为开发层面的老大 , 底层调用各类模型 , 我们开发者只需要熟悉固定的语法 , 痛苦都交给了 langchain 🤡

langchain 支持的嵌入式模型如下 :

Name Description
Alibaba Tongyi The AlibabaTongyiEmbeddings class uses the Alibaba Tongyi API to gene...
Azure OpenAI [Azure
Baidu Qianfan The BaiduQianfanEmbeddings class uses the Baidu Qianfan API to genera...
Amazon Bedrock Amazon Bedrock is a fully managed
ByteDance Doubao This will help you get started with ByteDanceDoubao [embedding
Cloudflare Workers AI This will help you get started with Cloudflare Workers AI [embedding
Cohere This will help you get started with CohereEmbeddings [embedding
DeepInfra The DeepInfraEmbeddings class utilizes the DeepInfra API to generate ...
Fireworks This will help you get started with FireworksEmbeddings [embedding
Google Generative AI This will help you get started with Google Generative AI [embedding
Google Vertex AI Google Vertex is a service that
Gradient AI The GradientEmbeddings class uses the Gradient AI API to generate emb...
HuggingFace Inference This Embeddings integration uses the HuggingFace Inference API to gen...
IBM watsonx.ai This will help you get started with IBM watsonx.ai [embedding
Jina The JinaEmbeddings class utilizes the Jina API to generate embeddings...
Llama CPP Only available on Node.js.
Minimax The MinimaxEmbeddings class uses the Minimax API to generate embeddin...
MistralAI This will help you get started with MistralAIEmbeddings [embedding
Mixedbread AI The MixedbreadAIEmbeddings class uses the Mixedbread AI API to genera...
Nomic The NomicEmbeddings class uses the Nomic AI API to generate embedding...
Ollama This will help you get started with Ollama [embedding
OpenAI This will help you get started with OpenAIEmbeddings [embedding
Pinecone This will help you get started with PineconeEmbeddings [embedding
Prem AI The PremEmbeddings class uses the Prem AI API to generate embeddings ...
Tencent Hunyuan The TencentHunyuanEmbeddings class uses the Tencent Hunyuan API to ge...
TensorFlow This Embeddings integration runs the embeddings entirely in your brow...
TogetherAI This will help you get started with TogetherAIEmbeddings [embedding
HuggingFace Transformers The TransformerEmbeddings class uses the Transformers.js package to g...
Voyage AI The VoyageEmbeddings class uses the Voyage AI REST API to generate em...
ZhipuAI The ZhipuAIEmbeddings class uses the ZhipuAI API to generate embeddin...

参考自官网 :js.langchain.com/docs/integr…

这些模型支持嵌入式 , 即支持将文本向量化 ~

我将使用 openai 来演示 ,

  1. 首先加载 data 文件夹下的"少年中国说.txt"文件为 Document 对象
  2. 然后使用工具分割对象
  3. 使用嵌入式模型向量化第二步分割后的 chunk
import { load } from "dotenv";
import { OpenAIEmbeddings } from "@langchain/openai";
import { TextLoader } from "langchain/document_loaders/fs/text";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
const env = await load();
const process = {
    env
}
// 1.
const loader = new TextLoader("data/少年中国说.txt");
const docs = await loader.load();
// 2.
const splitter = new RecursiveCharacterTextSplitter({
    chunkSize: 100, // 切片大小
    chunkOverlap: 20,// 重叠部分
  });
const splitDocs = await splitter.splitDocuments(docs);
// 3.
const embeddings = new OpenAIEmbeddings()
const splitDoc = splitDocs[0].pageContent
const res = await embeddings.embedQuery(splitDoc)
console.log(res)

在向量化之前 , 打印splitDocs , 输出如下:

使用OpenAIEmbeddings 嵌入式模型 , 对上述输出向量化之后 , 变成如下 :

上述代码使用嵌入式模型将文本变成了向量

会经历一下过程 :

  1. 预处理
    分词:首先,文本需要被分割成更小的单元,如单词或子词(subword)。例如,中文文本通常会被切分成单个汉字或词语。
    标准化:去除标点符号、转换为小写等操作,以确保一致性。
  2. 词汇表构建
    模型会根据训练数据构建一个词汇表(vocabulary),其中每个词都对应一个唯一的索引。对于未出现在词汇表中的词,通常会有一个特殊的标记(如)来表示未知词。
  3. 词向量生成
    静态词向量:早期的方法如Word2Vec、GloVe等会为每个词生成一个固定长度的向量。这些向量是通过无监督学习从大量文本中训练得到的,能够捕捉到词与词之间的语义关系。
    动态词向量:现代模型如BERT、OpenAI的模型使用的是上下文敏感的词向量。这意味着同一个词在不同的句子中可能会有不同的向量表示,从而更好地捕捉其在特定上下文中的含义。
  4. 句子编码
    平均池化:一种简单的方法是将句子中所有词向量的平均值作为句子的向量表示。
    加权求和:可以对词向量进行加权求和,权重可以根据词的重要性(如TF-IDF)来确定。
    Transformer架构:现代模型如BERT、OpenAI的模型使用了自注意力机制(self-attention),能够更好地捕捉句子中的长距离依赖关系,并生成整个句子的向量表示。
  5. 嵌入层
    在神经网络中,嵌入层(Embedding Layer)负责将输入的词索引转换为对应的词向量。这个层通常是可训练的,可以在下游任务中进一步优化。
  6. 输出向量
    最终,模型会输出一个固定长度的向量,这个向量代表了输入文本片段的语义信息。这个向量可以用于各种自然语言处理任务,如相似度计算、分类等。

以上过程参考自网络

Vector Store

向量数据库主要由 LangChain 社区维护的第三方集成 , 即在@langchain/community 包下面

关于选取那个数据 ,请查阅:js.langchain.ac.cn/docs/integr…

下面介绍两种向量数据库

  • Chroma
  • FaissStore

Chroma

一个专门为嵌入式向量设计的基于 SQLite 的开源数据库 , 有如下特点

  • 容易用
  • 轻量
  • 智能

通过向量切分多个段落 , 并对每个段落独立进行 k-means 聚类 , Chroma 可以有效压缩数据 , 减少储存空间 , 提高查询效率

k-means 聚类是一种无监督学习算法。它将数据分为 k 个聚类,使得每个数据点都属于离它最近的聚类中心所属的聚类。 通过不断迭代更新聚类中心,直到达到某种收敛条件。 例如,在图像识别中,可以用 k-means 聚类对图像的颜色进行分类;在客户细分中, 可以根据客户的特征将客户分为不同的群体。

langchain.js 官网 : js.langchain.ac.cn/docs/integr…

Chroma 官网 : docs.trychroma.com/docs/overvi…

好家伙 , 只支持 python 和 ts 🤡

安装、使用 ,依照上面官网

FaissStore

Faiss 是一个用于高效相似性搜索和密集向量聚类的库。

LangChain.js 支持使用 Faiss 作为本地运行的向量存储,可以保存到文件。

它还提供从 LangChain Python 实现 读取保存的文件的能力。

我在官网上看到这段 , 从那一眼起 , 我就选择她了 , 可是让我无语的是 , 我熬夜到天亮改了一个很臭的 bug —— 使用 npm , yarn , pnpm ... , 从淘宝源到腾讯源 , 这个包总是下不下来 , 我就不断搜索 , 可惜我用的是 Edge , 全是 csdn , 直到我在 github 上搜到以下解决方案 ,非常 nice !

一言蔽之即 : 手动下载 realse 版本 , 将无法下载的文件 ,手动添加到 node_modules

愿以我之发 , 保倔友之发🤡

总结 : 不要使用诸如 Edge 之类的浏览器搜报错🤡👈

实战

package.json
{
  "name": "test-app-node",
  "private": true,
  "version": "0.0.0",
  "scripts": {
    "prepare-kong-faiss": "ts-node prepare-kong-faiss.ts",
    "load-kong-faiss": "ts-node load-kong-faiss.ts",
    "multiQueryRetriever": "ts-node multiQueryRetriever.ts",
    "LLMChainExtractor": "ts-node LLMChainExtractor.ts",
    "ScoreThresholdRetriever": "ts-node ScoreThresholdRetriever.ts",
    "prepare-qiu": "ts-node ./rag/prepare-qiu.ts",
    "rag-2": "ts-node ./rag/index.ts",
    "rag-server": "ts-node ./rag/server.ts",
    "rag-client": "ts-node ./rag/client.ts"
  },
  "type": "module",
  "dependencies": {
    "@langchain/community": "^0.0.27",
    "dotenv": "^16.4.7",
    "express": "^4.19.2",
    "faiss-node": "^0.5.1",
    "langchain": "^0.1.37",
    "typescript": "^5.7.3"
  },
  "main": "index.js",
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "devDependencies": {
    "ts-node": "^10.9.2"
  }
}
embedding

安装好上述包后 , 使用嵌入式模型 将向量化后的数据储存在 data/vector/ 下

import { TextLoader } from "langchain/document_loaders/fs/text";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
import { FaissStore } from "langchain/vectorstores/faiss";
import { OpenAIEmbeddings } from "@langchain/openai";
import "dotenv/config";


const run = async () => {
    const loader = new TextLoader("../data/少年中国说.txt");
    const docs = await loader.load();
  
    const splitter = new RecursiveCharacterTextSplitter({
      chunkSize: 100,
      chunkOverlap: 20,
    });
    const splitDocs = await splitter.splitDocuments(docs);
  
    const embeddings = new OpenAIEmbeddings();
    const vectorStore = await FaissStore.fromDocuments(splitDocs, embeddings);
  
    const directory = "../db/vector";
    await vectorStore.save(directory);
  };
  
  run();
  

运行上述文件后 , 生成 docstore.json 和二进制文件 faiss.index

docstore.json 中 , 即向量化后的数据 :

retriever

从向量数据库中检索 , 我提问 : 日本怎么称呼我们中国? , 将从向量数据中检索 ,

import { FaissStore } from "@langchain/community/vectorstores/faiss";
import { OpenAIEmbeddings } from "@langchain/openai";
import "faiss-node";
import dotenv from "dotenv";
dotenv.config();
async function f() {
  const directory = "../db/vector";
 
  const embeddings = new OpenAIEmbeddings(
    {  
      modelName: "text-embedding-ada-002", //指定模型的名称
      maxConcurrency: 10, //设置最大的并发数 , 意味着同负一时间最多可以并行处理10个请求 , 避免过多并发请求 , 导致系统过载和api限流
      maxRetries: 3, //设置最大的重试次数 , 当api调用失败的时候 , 程序会自动重试最多三次 , 这增加请求成功的概率 , 提高了系统的可靠性
    },
    {
      batchSize: 100, //设置批量处理的大小 , 每次调用api 最多处理100个文本片段 , 但同时也要注意api的限制和内存的使用
    }
  );
  //加载向量储存
  const vectorstore = await FaissStore.load(directory, embeddings);
  //从向量数据库中创建一个检索器
  const retriever = vectorstore.asRetriever(2);
  //使用Runnable API进行进行检索
  const res = await retriever.invoke("日本怎么称呼我们中国?");
  console.log(res);
}

f();

结果如下 :

总结

学到这里 , 我已经知道知识库从自然语言到向量的过程 , 从数据角度的话 , 经历了一下过程 :

  • 加载数据源
  • 分割数据
  • 向量化数据
  • 持久化数据

逐步走向 RAG ~

!!??所以是真的哦.gif

Vue 项目开发全攻略:从搭建到上线的技术盛宴

2025年1月17日 22:52

一、项目搭建

在开始开发 Vue 项目时,首先要进行项目搭建。这里我们选用 vite 来负责工程化,它能极大地提升项目构建和开发的效率。

使用 vite 搭建 Vue 项目非常简单,只需在命令行中输入 npm init vite 这一指令,就能快速初始化一个全新的 Vue 项目框架。vite 是新一代的前端构建工具,它基于 ES 模块导入,在开发环境下无需打包操作,可直接启动开发服务器,实现快速冷启动。在生产环境中,vite 又能利用 Rollup 进行高效的打包,为项目提供优化后的代码输出。通过这种方式,我们能轻松搭建起一个基础的 Vue 项目架构,为后续的开发工作奠定坚实的基础。

二、核心技术栈

2.1 Vue 核心语法

Vue 的核心语法是构建项目的基石 ,在本项目中,响应式原理通过ref和reactive两个函数来实现。例如,当需要创建一个简单的响应式数据时,使用ref函数:

import { ref } from 'vue';
const count = ref(0);

若要处理复杂的对象或数组,reactive则更为合适:

import { reactive } from 'vue';
const userInfo = reactive({
  name: 'John',
  age: 30
});

组件化开发让代码的可维护性和复用性大大提高。在项目里,我们将页面拆分成多个组件,每个组件都有独立的逻辑和视图。以一个按钮组件为例,其template部分定义了按钮的外观:

<template>
  <button>{{ buttonText }}</button>
</template>

script部分则负责组件的逻辑,如:

<script setup>
import { ref } from 'vue';
const buttonText = ref('点击我');
</script>

指令方面,v - if、v - show用于控制元素的显示与隐藏。v - for则常用于列表的渲染,假设我们有一个用户列表:

const userList = reactive([
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' }
]);

在模板中使用v - for进行渲染:

<template>
  <ul>
    <li v - for="user in userList" :key="user.id">{{ user.name }}</li>
  </ul>
</template>

通过这些核心语法的运用,我们能够构建出灵活且高效的 Vue 应用程序。

2.2 Vue - Router 路由

在 Vue - Router 的配置中,多级路由的设置让页面结构更加清晰。例如,我们有一个主页面Home,其下包含About和Contact两个子页面。在路由配置文件中可以这样定义:

import { createRouter, createWebHistory } from 'vue - router';
import Home from './views/Home.vue';
import About from './views/About.vue';
import Contact from './views/Contact.vue';
const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/home',
      component: Home,
      children: [
        { path: 'about', component: About },
        { path: 'contact', component: Contact }
      ]
    }
  ]
});

路由懒加载是提升性能的关键。我们使用import()函数来实现,当访问特定路由时,对应的组件才会被加载。比如:

const About = () => import('./views/About.vue');

添加路由守卫则能有效控制页面的访问权限。以登录验证为例,在全局前置守卫中可以这样实现:

router.beforeEach((to, from, next) => {
  const isLoggedIn = localStorage.getItem('token');
  if (to.meta.requiresAuth &&!isLoggedIn) {
    next('/login');
  } else {
    next();
  }
});

在需要验证的路由中,设置meta字段:

{
  path: '/dashboard',
  component: Dashboard,
  meta: { requiresAuth: true }
}

通过这样的配置,确保了只有登录用户才能访问受保护的页面。

2.3 Pinia 状态管理

Pinia 在项目中负责状态的管理,极大地简化了状态共享的过程。首先,安装 Pinia 并在main.js中进行配置:

import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
const app = createApp(App);
const pinia = createPinia();
app.use(pinia);
app.mount('#app');

接着,定义一个store来管理用户相关的状态。例如:

import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
  state: () => ({
    userInfo: null,
    isLoggedIn: false
  }),
  actions: {
    login(user) {
      this.userInfo = user;
      this.isLoggedIn = true;
      localStorage.setItem('token', 'valid - token');
    },
    logout() {
      this.userInfo = null;
      this.isLoggedIn = false;
      localStorage.removeItem('token');
    }
  }
});

在组件中使用该store时,只需引入并调用相应的方法:

import { useUserStore } from '@/stores/user';
const userStore = useUserStore();
userStore.login({ name: 'John', age: 30 });

Pinia 的优势在于其简洁的 API 和良好的模块化设计,使得状态管理变得轻松且高效。

2.4 Element3 UI 组件库

Element3 是一个功能强大的 UI 组件库,为项目提供了丰富的组件。在使用时,我们采用按需加载的方式来优化性能。首先,安装相关的插件:

npm install -D unplugin - vue - components unplugin - auto - import

然后,在vue.config.js中进行配置:

const AutoImport = require('unplugin - auto - import/webpack');
const Components = require('unplugin - vue - components/webpack');
const { ElementPlusResolver } = require('unplugin - vue - components/resolvers');
module.exports = {
  configureWebpack: {
    plugins: [
      AutoImport({
        resolvers: [ElementPlusResolver()]
      }),
      Components({
        resolvers: [ElementPlusResolver()]
      })
    ]
  }
};

这样,在组件中使用 Element3 组件时,如按钮组件,只需直接引入:

<template>
  <el - button type="primary">点击我</el - button>
</template>

Element3 组件以el -开头,通过按需加载,我们避免了引入不必要的组件,有效减少了项目的打包体积,提升了页面的加载速度。

2.5 Stylus CSS 预处理器

Stylus 作为 CSS 预处理器,为项目带来了诸多便利。它允许我们使用变量、混入、嵌套等功能,使 CSS 代码更加简洁和易于维护。例如,定义一个颜色变量:

$primaryColor = #1890ff

在样式中使用该变量:

button {
  background - color: $primaryColor;
  color: white;
}

混入功能可以复用一些常用的样式,如圆角样式:

border - radius() {
  border - radius: 5px;
}
.box {
  +border - radius();
}

样式的嵌套则让代码结构更加清晰,以导航栏为例:

.nav {
  display: flex;
  justify - content: space - between;
  li {
    list - style: none;
    a {
      text - decoration: none;
      color: #333;
      &:hover {
        color: $primaryColor;
      }
    }
  }
}

通过 Stylus 的这些特性,我们能够高效地编写和管理项目的样式。

2.6 Axios AJAX 请求封装库

Axios 用于与后端进行数据交互,我们对其进行了封装,以提高代码的复用性和可维护性。首先,创建一个api.js文件,设置基础 URL 和请求拦截器:

import axios from 'axios';
const service = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 5000
});
service.interceptors.request.use(
  config => {
    const token = localStorage.getItem('token');
    if (token) {
      config.headers['Authorization'] = `Bearer ${token}`;
    }
    return config;
  },
  error => {
    return Promise.reject(error);
  }
);

然后,封装常用的请求方法,如get和post:

export const get = (url, params = {}) => {
  return service.get(url, { params });
};
export const post = (url, data = {}) => {
  return service.post(url, data);
};

在组件中使用时,只需引入相应的方法:

import { get } from '@/api';
get('/user/info').then(response => {
  console.log(response.data);
}).catch(error => {
  console.error(error);
});

通过这样的封装,我们能够方便地进行各种 AJAX 请求,与后端进行稳定的数据交互。

三、项目亮点展示

3.1 ES6 风格的全面应用

在整个项目中,我们全面采用了 ES6 风格的代码编写方式,这使得代码在简洁性、易读性和易维护性上都有了显著提升。例如,在定义路由时,使用对象解构的方式简化了代码结构。原本需要完整书写routes: routes,现在直接写成routes即可。这种简洁的写法不仅减少了冗余代码,还让代码逻辑更加清晰,开发者能够一眼看清路由的配置关系。

在函数定义方面,ES6 的箭头函数也被广泛应用。比如在处理一些简单的回调函数时,箭头函数的使用使得代码更加紧凑。例如,在数组的map方法中,使用箭头函数可以快速对数组中的每个元素进行处理:

const numbers = [1, 2, 3, 4];
const squaredNumbers = numbers.map((number) => number * number);

相比于传统的函数定义方式,箭头函数的语法更加简洁,同时也避免了this指向的问题,让代码的维护更加轻松。

3.2 良好的注释与代码可读性

良好的注释是提高代码可读性的关键。在项目中,我们在关键的代码块、函数定义以及复杂的逻辑处都添加了详细的注释。例如,在路由守卫的代码中,我们添加了注释来说明其作用和逻辑:

// 全局前置守卫,用于验证用户是否登录
router.beforeEach((to, from, next) => {
  const isLoggedIn = localStorage.getItem('token');
  if (to.meta.requiresAuth &&!isLoggedIn) {
    // 如果目标路由需要登录且用户未登录,则重定向到登录页面
    next('/login');
  } else {
    // 否则,允许用户访问目标路由
    next();
  }
});

这样的注释使得其他开发者在阅读代码时,能够快速理解代码的意图和功能,降低了代码的理解成本。同时,对于一些自定义的函数和组件,我们也添加了注释来解释其输入参数、返回值以及功能用途,确保代码的每一部分都清晰易懂。

3.3 规范的 Git 提交记录和习惯

在项目开发过程中,我们始终保持着规范的 Git 提交记录和良好的提交习惯。每次提交都有明确的提交信息,描述本次提交所做的修改内容。例如,“修复登录页面的验证码验证问题”“优化首页的加载速度” 等。这样的提交信息使得项目的版本历史清晰可追溯,团队成员能够快速了解每个提交的目的和影响范围。

同时,我们遵循一定的分支管理策略,如使用master分支作为主分支,用于发布稳定版本;develop分支用于开发新功能,通过创建特性分支进行功能开发,开发完成后再合并到develop分支。这种规范的分支管理和提交习惯,不仅有助于团队协作开发,还能在出现问题时快速定位和解决,提高了项目的开发效率和质量。

四、实战技巧与注意事项

4.1 表单组件的使用

在项目中,表单组件的使用非常频繁。我们使用 :model来收集表单数据,这是一种双向数据绑定的方式,能够实时同步表单输入与数据模型。例如:

<el - form :model="formData">
  <el - form - item label="用户名">
    <el - input v - model="formData.username"></el - input>
  </el - form - item>
  <el - form - item label="密码">
    <el - input type="password" v - model="formData.password"></el - input>
  </el - form - item>
</el - form>

在上述代码中,formData是一个包含username和password字段的对象,通过v - model指令,表单输入框的值会实时更新到formData中,反之亦然。

通过ref可以获取表单实例,这在需要手动操作表单时非常有用。在模板中,使用ref标记表单组件:

<el - form ref="formRef" :model="formData">
  <!-- 表单内容 -->
</el - form>

在script部分,通过ref获取表单实例:

import { ref } from 'vue';
const formRef = ref(null);

当表单挂载后,formRef就会获取到实际的表单实例。此时,我们可以调用表单实例的方法,如validate方法进行表单校验:

formRef.value.validate((valid) => {
  if (valid) {
    // 校验通过,提交表单或执行其他操作
    console.log('表单校验通过');
  } else {
    // 校验失败,提示用户错误信息
    console.log('表单校验失败');
  }
});

表单的校验规则通过rules属性来定义。例如,对用户名和密码设置必填校验:

const formData = reactive({
  username: '',
  password: ''
});
const rules = {
  username: [
    { required: true, message: '用户名不能为空', trigger: 'blur' }
  ],
  password: [
    { required: true, message: '密码不能为空', trigger: 'blur' }
  ]
};

在表单组件中,将rules属性绑定到对应的form - item上:

<el - form :model="formData" :rules="rules">
  <el - form - item label="用户名" prop="username">
    <el - input v - model="formData.username"></el - input>
  </el - form - item>
  <el - form - item label="密码" prop="password">
    <el - input type="password" v - model="formData.password"></el - input>
  </el - form - item>
</el - form>

这样,当用户输入完成并离开输入框(blur事件触发)时,表单会根据设置的规则进行校验,并显示相应的错误提示信息。

4.2 布局组件的应用

布局组件在构建页面结构时起着关键作用。我们常用的布局组件包括Elcontainer、Elheader、ElAside、ElMain等。

以一个常见的后台管理页面布局为例,使用Elcontainer作为容器,将页面分为头部、侧边栏和主体内容区域:

<el - container>
  <el - header>
    <!-- 头部内容,如导航栏 -->
    <h1>后台管理系统</h1>
  </el - header>
  <el - container>
    <el - aside width="200px">
      <!-- 侧边栏菜单 -->
      <el - menu :default - active="activeIndex" class="el - menu - vertical - demo" @select="handleSelect">
        <el - menu - item index="1">菜单1</el - menu - item>
        <el - menu - item index="2">菜单2</el - menu - item>
      </el - menu>
    </el - aside>
    <el - main>
      <!-- 主体内容区域 -->
      <p>这里是主要内容</p>
    </el - main>
  </el - container>
</el - container>

在上述代码中,Elheader定义了页面的头部,通常包含导航栏等信息。Elaside作为侧边栏,设置了固定的宽度为200px,并在其中放置了菜单组件。Elmain则用于展示主体内容。

对于页面内的布局,ElRow和ElCol经常被用于实现栅格化布局。例如,将一行分为两列,左列占 8 格,右列占 4 格:

<el - row>
  <el - col :span="8">
    <p>左列内容</p>
  </el - col>
  <el - col :span="4">
    <p>右列内容</p>
  </el - col>
</el - row>

通过span属性可以灵活调整每列所占的比例,从而实现各种复杂的页面布局。

4.3 性能优化策略

在项目开发过程中,性能优化至关重要。我们采用了多种策略来提升项目的性能。

按需加载是其中一个重要的策略。在引入 Vue 组件库 Element3 时,我们通过配置实现了按需加载,避免一次性加载所有组件,从而减少了初始加载时间。在路由方面,也采用了懒加载技术,只有当用户访问特定路由时,对应的组件才会被加载。例如:

const Home = () => import('./views/Home.vue');
const About = () => import('./views/About.vue');
const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/home', component: Home },
    { path: '/about', component: About }
  ]
});

这样,在应用启动时,只有必要的路由组件会被加载,大大提高了应用的启动速度。

此外,还对静态资源进行了优化。通过使用 Webpack 插件对 JavaScript 和 CSS 文件进行压缩,减少了文件体积,加快了文件的传输速度。同时,合理利用缓存机制,对于不经常变化的静态资源设置较长的缓存时间,避免用户每次访问都重新下载。

在图片处理方面,对图片进行了压缩和格式优化,选择合适的图片格式(如 WebP 格式,在保证图片质量的前提下,文件体积更小),并根据不同的设备屏幕尺寸提供相应分辨率的图片,避免加载过大的图片资源,从而提升页面的加载速度和用户体验。

五、总结与展望

通过本次项目的开发,我收获了许多宝贵的经验。从项目搭建到技术栈的运用,再到项目亮点的打造和实战技巧的积累,每一个环节都让我对 Vue 开发有了更深入的理解。在项目中,我学会了如何高效地运用各种工具和技术,解决实际开发中遇到的问题。同时,也深刻体会到团队协作、代码规范以及性能优化的重要性。

展望未来,我希望能够进一步优化项目。在性能方面,持续探索更有效的优化策略,如进一步优化图片加载、减少 HTTP 请求等,以提升用户体验。在功能上,根据用户反馈和业务需求,不断添加新的功能模块,使项目更加完善。同时,也会关注 Vue 技术的发展动态,及时引入新的特性和最佳实践,保持项目的技术先进性。

Tauri(五)——实现托盘菜单和图标切换功能

2025年1月17日 22:47

前言

在桌面端应用中,托盘图标是常见的功能,本文将以 Tauri V2 框架为例,展示如何实现托盘菜单以及根据主题切换托盘图标的功能。以下是效果截图和详细实现步骤和代码说明。

image.png

1. 修改 Cargo.toml 添加依赖

首先,在 src-tauri/Cargo.toml 文件中添加如下依赖:

[dependencies]
tauri = { version = "2.0.6", features = ["tray-icon", "image-png"] }
  • tray-icon: 启用托盘图标功能。
  • image-png: 支持自定义 PNG 图标。

2. 实现托盘菜单功能

在 Rust 中,我们创建一个 enable_tray 函数,用于初始化托盘菜单及其事件。

enable_tray 函数

fn enable_tray(app: &mut tauri::App) {
    use tauri::{
        image::Image,
        menu::{MenuBuilder, MenuItem},
        tray::TrayIconBuilder,
    };
    
    // 退出按钮
    let quit_i = MenuItem::with_id(app, "quit", "Quit Coco", true, None::<&str>).unwrap();
    // 设置按钮
    let settings_i = MenuItem::with_id(app, "settings", "Settings...", true, None::<&str>).unwrap();
    // 打开按钮
    let open_i = MenuItem::with_id(app, "open", "Open Coco", true, None::<&str>).unwrap();
    // 关于按钮
    let about_i = MenuItem::with_id(app, "about", "About Coco", true, None::<&str>).unwrap();
    // 隐藏按钮
    let hide_i = MenuItem::with_id(app, "hide", "Hide Coco", true, None::<&str>).unwrap();
    // ......

    // 按照一定顺序 把按钮 放到 菜单里
    let menu = MenuBuilder::new(app)
        .item(&open_i)
        .separator() // 分割线
        .item(&hide_i)
        .item(&about_i)
        .item(&settings_i)
        .separator() // 分割线
        .item(&quit_i)
        .build()
        .unwrap();

    let _tray = TrayIconBuilder::with_id("tray")
        // .icon(app.default_window_icon().unwrap().clone()) // 默认的图片
        .icon(Image::from_bytes(include_bytes!("../icons/light@2x.png")).expect("REASON")) // 自定义的图片
        .menu(&menu)
        .on_menu_event(|app, event| match event.id.as_ref() {
            "open" => {
                handle_open_coco(app);  // 打开事件
            }
            "hide" => {
                handle_hide_coco(app);
            }
            "about" => {
                let _ = app.emit("open_settings", "about");
            }
            "settings" => {
                // windows failed to open second window, issue: https://github.com/tauri-apps/tauri/issues/11144 https://github.com/tauri-apps/tauri/issues/8196
                //#[cfg(windows)]
                let _ = app.emit("open_settings", "");

                // #[cfg(not(windows))]
                // open_settings(&app);
            }
            "quit" => {
                println!("quit menu item was clicked");
                app.exit(0);
            }
            _ => {
                println!("menu item {:?} not handled", event.id);
            }
        })
        .build(app)
        .unwrap();
}

功能说明

  • 菜单项创建:使用 MenuItem::with_id 方法创建菜单项并设置唯一 ID 和显示文本。
  • 菜单构建:通过 MenuBuilder 组合菜单项并添加分隔符。
  • 托盘图标构建:通过 TrayIconBuilder 设置图标、菜单及点击事件。
  • 事件监听:在 on_menu_event 中根据菜单项 ID 处理对应事件。

3. 注册托盘菜单

在 Tauri 应用启动时,调用 enable_tray 注册托盘菜单。

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    let mut ctx = tauri::generate_context!();

    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![
            switch_tray_icon, // 切换托盘图标
        ])
        .setup(|app| {
            init(app.app_handle());

            enable_tray(app); // 注册事件

            Ok(())
        })
        .run(ctx)
        .expect("error while running tauri application");
}

4. 实现托盘图标切换

为了根据主题切换托盘图标,我们需要创建一个 switch_tray_icon 命令。

switch_tray_icon 命令

#[tauri::command]
fn switch_tray_icon(app: tauri::AppHandle, is_dark_mode: bool) {
    let app_handle = app.app_handle();

    println!("is_dark_mode: {}", is_dark_mode);

    const DARK_ICON_PATH: &[u8] = include_bytes!("../icons/dark@2x.png");
    const LIGHT_ICON_PATH: &[u8] = include_bytes!("../icons/light@2x.png");

    // 根据 app 的主题切换 图标
    let icon_path: &[u8] = if is_dark_mode {
        DARK_ICON_PATH
    } else {
        LIGHT_ICON_PATH
    };

    // 获取托盘
    let tray = match app_handle.tray_by_id("tray") {
        Some(tray) => tray,
        None => {
            eprintln!("Tray with ID 'tray' not found");
            return;
        }
    };

    // 设置图标
    if let Err(e) = tray.set_icon(Some(
        tauri::image::Image::from_bytes(icon_path)
            .unwrap_or_else(|e| panic!("Failed to load icon from bytes: {}", e)),
    )) {
        eprintln!("Failed to set tray icon: {}", e);
    }
}

代码说明

  • 动态加载图标:根据 is_dark_mode 参数决定使用亮色或暗色图标。
  • 更新托盘图标:通过 set_icon 方法更新图标。
  • 错误处理:在托盘实例不存在或图标加载失败时记录错误日志。

5. 前端调用 Rust 命令

前端可以通过 Tauri 的 invoke API 调用 switch_tray_icon 命令。

示例代码

import { invoke } from "@tauri-apps/api/core";

async function switchTrayIcon(value: "dark" | "light") {
    try {
      // invoke  switch_tray_icon 事件名 isDarkMode 参数名
      await invoke("switch_tray_icon", { isDarkMode: value === "dark" });
    } catch (err) {
      console.error("Failed to switch tray icon:", err);
    }
  }

在主题切换时调用 switchTrayIcon 即可实现图标动态切换。

小结

通过本文的实现,我们完成了以下功能:

  1. 创建自定义托盘菜单。(更丰富的菜单内容可以自行扩展了)
  2. 响应托盘菜单事件。
  3. 根据主题动态切换托盘图标。(不仅仅可以主题切换图标,还可以依据 app 行为修改对应的图标)

这种方式为 Tauri 应用提供了更加友好的用户体验。如果有其他需求,可以在菜单事件中扩展更多功能。

参考

  1. v2.tauri.app/learn/syste…
  2. github.com/infinilabs/…

开源

最近,我正在基于 Tauri 开发一款项目,名为 Coco。目前已开源,项目仍在不断完善中,欢迎大家前往支持并为项目点亮免费的 star 🌟!

作为个人的第一个 Tauri 项目,开发过程中也是边探索边学习。希望能与志同道合的朋友一起交流经验、共同成长!

代码中如有问题或不足之处,期待小伙伴们的宝贵建议和指导!

非常感谢您的支持与关注!

Iterator迭代器(遍历器)

2025年1月17日 22:34

前言

平时的开发语言基本上都会用到迭代器,有的叫遍历器,有的叫枚举,都是一个意思,就是将我们的集合或者特定的结构,通过遍历器能够访问到其允许访问的所有成员,这就是遍历、迭代器了

本篇主要讲的是 js 中的迭代器(遍历、枚举),其与 generator 函数也有着不小的关系,仔细一看就能感觉到 generator 方法就是 Iterator 迭代器 的另一个版本哈

前面有介绍过 generator,参考 Generator函数与async函数介绍

Iterator

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

我们常见的支持遍历的对象有:Array、set、map等,甚至是 String 也提供了 Iterator遍历器 接口,方便我们遍历,一些结构还支持返回遍历器,方便遍历

遍历器常常伴随着 for ... of,其可以很好地访问我们的遍历器,而 forEach 等并不是遍历器标配,支持 Iterator 遍历的也不一定有 forEach 类似的方法哈,但是 for ... of 一定可以

尝试实现 Iterator

话不多说了,先来一组遍历器的使用

var it = makeIterator(["a", "b"]);

it.next(); // { value: "a", done: false }
it.next(); // { value: "b", done: false }
it.next(); // { value: undefined, done: true }

遍历器实现长这样

function makeIterator(array) {
    var nextIndex = 0;
    const len = array.length
    return {
        next: function () {
            return nextIndex < len
                ? { value: array[nextIndex++], done: false }
                : { value: undefined, done: true };
        },
    };
}

看到遍历器的实现,我们大概知道了遍历器的一些特征

  • 遍历器对象包含 next 方法
  • 每次遍历都会调用 next 方法,调用完毕后,指针会往后移,直到 len 时,返回固定的结果
  • 遍历器的 next 对象在没结束前返回 { value, done }结构,value 返回当前遍历的内容,done表示是否结束了,当结束时,done 返回 true

看了上面结构,我们可以稍微在简化一下 makeIterator,去掉多余的 done 和 value,也能够减少一部分内容

function makeIterator(array) {
    let nextIndex = 0;
    const len = array.length;
    return {
        next: function () {
            return nextIndex < len
                ? { value: array[nextIndex++] }
                : { done: true };
        },
    };
}

模仿 Iterator 遍历

模仿一下 for ... of 的遍历逻辑

var it = makeIterator(["a", "b"]);
let next = it.next()
while (!next.done) {
    //如果要封装,这里可以回调 item、index 等内容,index可以在枚举器返回,也可以这里记录
    console.log(next.value)
    next = it.next
}

[Symbol.iterator] 接口

Iterator 接口的目的,就是为所有数据结构,提供了一种统一的访问机制,即for...of循环

ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。Symbol.iterator属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名Symbol.iterator,它是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为 Symbol 的特殊值

下面我们给 arrayLike 对象实现一个遍历器吧,当然想更通用,可以直接创建一个类似 ArrayLike 类,实现原型函数即可,每次只需要new一个此类型的对象,就可以实现通用了

const arrayLike = { '0': 1, '1': 1, '2': 2,  length: 2 }
arrayLike[Symbol.iterator] = function() {
    let nextIndex = 0;
    let self = this
    const len = self.length;
    return {
        next: function () {
            return nextIndex < len
                ? { value: self['' + nextIndex++] }
                : { done: true };
        },
        return: function () {
            console.log(self[nextIndex])
            return {};
        }
    };
}

我们稍微改进一下,让其更通用,这样就 ok 了

class ArrayLike {
    constructor(obj) {
        this.obj = obj
    }
}

ArrayLike.prototype[Symbol.iterator] = function () {
    let nextIndex = 0;
    let self = this;
    const len = self.length;
    return {
        next: function () {
            return nextIndex < len
                ? { value: self["" + nextIndex++] }
                : { done: true };
        },
        return: function () {
            console.log(self[nextIndex]);
            return {};
        },
    };
};

//遍历一下发现成功了
const alike = new ArrayLike({ 0: 0, 1: 1, 2: 2, length: 2 });
for (const v of arrayLike) {
    console.log('arrayLike', v);
}

复用遍历器

我们发现 ArrayLike 和 Array 的遍历功能很相似,可以不用写,直接使用 Array 的遍历器即可

ArrayLike.prototype[Symbol.iterator]: Array.prototype[Symbol.iterator]

解构与遍历器

除了遍历,有时候我们也会使用 es6 语法解构、展开一些遍历器对象,此时就会自动调用迭代器

如果解构出错,有时候会报 Iterator 相关错误相信了解原因了

//集合解构也会默认调用 iterator
const set = new Set()
const list = [...set]
const [first, ...others] = list; //集合第一个给first,其他的给 others

遍历器 next、return、throw

遍历器除了 next,还有 return、throw 相关内容函数,调用 next 相当于 continue 功能,自动进入下一个,而 return 则相当于循环内的 break,throw 就不多说了 throw 一个异常了

return 和 throw 为一个 可选项,不实现也没啥影响,会自动使用默认功能,使用了也需要返回默认功能

function makeIterator(array) {
    let nextIndex = 0;
    const len = array.length;
    return {
        next: function () {
            return nextIndex < len
                ? { value: array[nextIndex++] }
                : { done: true };
        },
    };
}

实现一个 return,当for ... of 中出现 break 则会调用,我们需要返回一个对象结构(和next返回一样的结构),试了一下,返回一个{}, {done: false}都会正常 break,也就是这个方法监听break用的,平时基本不会用到, throw 也就不多介绍了

{
    next: function () {
        return nextIndex < len
            ? { value: self['' + nextIndex++] }
            : { done: true };
    },
    //我们可以通过 return 
    return: function () {
        console.log(self[nextIndex])
        return {done: true};
    }
};

最后

Iterator 和 generator 也挺像的,但就介绍到这里吧,想继续了解可以看 Generator函数与async函数介绍,本篇就不多介绍了,东西没多少,就是包含了 Iterator 的基础功能,一些注意不到的细节罢了

js中小知识 continue、break到外循环

2025年1月17日 22:34

js 看着简单,但是里面有很多很多的细节,多到我们学习过程中,很多的细节都接触不到,甚至一些有事还是挺重要的,甚至能够帮我们减少一些逻辑

有时网上看文章刷视频,总是能从一些前辈们口中得知一些小知识,每次学到都是欣喜万分,有些感觉挺有用的,因此一定要记录下来,因为不知道啥时候会用到,也不能让一些好用的 tips 失传是吧

比如前些日子看到的 continue、break 到外循环,这个有点像 c 语言的 goto 了,可能很多大佬不推荐新手使用才不让用的,实际上这个在一些场景挺好用的,尤其是刷算法需要多层遍历时,有时能减少不少麻烦

通过标签continue到外循环

//外循环前面加上一个外标签,outer: ,方便内存换continue到该标签这层
//里面循环只要两者索引加起来大于 15,也就是 idx > 6 时方可满足,也就是大于 6 时,不会打印 idx
outer: for (let idx = 0; idx < 10; idx++) {
    for (let jdx = 0; jdx < 10; jdx++) {
        if (jdx + idx > 15) {
            continue outer
        }
    }
    console.log(idx)
}

执行后,我们看看结果,也就是 continue 到外循环标签时,相当于直接跳转到 外循环 idx++那一步,然后走判断,再继续走循环

0
1
2
3
4
5
6

通过标签 break 到外循环

//外循环前面加上一个外标签,outer: ,方便内存换 break 到该标签这层
//里面循环只要两者索引加起来大于 10,也就是 idx > 1 时方可满足
outer: for (let idx = 0; idx < 10; idx++) {
    for (let jdx = 0; jdx < 10; jdx++) {
        if (jdx + idx > 10) {
            break outer;
        } 
    }
    console.log(idx);
}

打印结果如下,不多说,相对于 continue 继续执行,break直接结束了外循环

0
1

处理弹窗后页面滚动问题

2025年1月17日 22:33

前言

我们在封装弹窗时,有时会出现,我们弹窗后面的滚动页面仍然可以滚动,我们希望的是,弹层起来时,页面后端的滚动内容停止滚动,这样我们的弹窗效果看起来优先级更高,体验更好

我们可能会考虑监听滚动事件,将滚动事件阻止,取消弹层后取消监听事件

默认使用(问题初现)

function handler(e) {
    e.preventDefault();
}

window.addEventListener("wheel", handler);
window.removeEventListener('wheel', handler)

可是上面设置了之后,在一些浏览器中,会出现一堆错误事件,仍然无法阻止滚动的默认行为

此时就和浏览器的优化有关了,有些浏览器,会默认开启事件优化,因此会忽略用户的一些设置的一些行为,其中就包括阻止事件的 preventDefault 行为

解决方案(关闭游览器默认的事件优化)

那么我们怎么做呢?

只需要设置监听的第三个参数 passive: false 即可

这个参数有被动的优化的意思,passive 默认为 true,也就是默认被动开启浏览器事件优化,其会阻止一些用户行为

当设置为 passive: false 的时候,意味着取消被动优化,也就是开发者可以自行做一些操作,停止优化,此时被优化的的 preventDefault 取消滚动事件,就可以生效了

function handler(e) {
    e.preventDefault();
}

window.addEventListener("wheel", handler, {
    passive: true, //默认为true
});
window.removeEventListener('wheel', handler)

overflow方案:有时候我们也会采取更原始的方案解决问题,那就是设置 overflow,可以取消页面的滚动事件,此时就解决问题了(很多直接取body节点取消,弹窗结束恢复),但也😂有一个问题,就是取消 overflow 后滚动条会消失而闪动,追求细节的有的直接隐藏滚动条了

元素的尺寸

2025年1月17日 22:33

元素尺寸问题由来

前端开发中,由于页面的元素渲染要经过一些流程,通过不同的方法会获取到不同流程阶段的元素的信息(主要是尺寸信息),这部分信息由于随着渲染阶段的改变,信息也可能会发生改变(当然也可能不变),因此元素的尺寸就有必要介绍了

下面就从不同阶段介绍元素尺寸

渲染步骤

元素渲染过程中,会经过下面步骤

  1. dom 树,此时 dom 树的属性就是我们设置的初始属性,可以直接获取dom读取,dom.style.width, 例如:style="width: 200px"
  2. cssom 计算树,样式计算阶段,也就是 通过 getComputedStyle 获取样式计算后的数据,比如我们设置了 200px,但其是基准属性,在弹性盒子中,可能会被压缩,因此会发生变化
  3. layout tree 布局树,浏览器呈现出来的树,里面包含了布局出来的实际几何属性,可以通过 clientWidth(content+padding 元素宽度)、offsetWidth(content+padding+scroll+border内容+边界偏移)、scrollWidth(visibile+invisibile出现滚动内容,滚动内容)等,此时已经渲染,因此获取 dom 的这些信息也会强制渲染回流,由于开始布局,因此也会浪费性能,可以根据情况使用
  4. 展现阶段,此时才能看到展现到用户眼中的尺寸,layout tree 看不到变换后的效果,transform 不在布局树中,它属于变换,例如: transform: scale(2, 2); 变成了两倍,前面的 200px就变成了 400px,或者旋转后,包围盒子也会变宽等,使用 dom.getBoundingClientRect() 方法可以获取元素展现时的尺寸,由于要等到展现,所以已经会强制渲染回流,浪费性能,可以根据情况使用

前端逻辑属性调整排版方向 write-mode

2025年1月17日 22:33

我们平时的 text 文本基本上都是横向布局的,但是有些诗歌文艺范的内容,很多文本都是垂直布局的,那么我们怎么布局呢, css 中有个属性 write-mode 可以

writing-mode: horizontal-tb; //默认水平方向布局
writing-mode: vertical-lr; //垂直方向布局,从做往右分列延伸
writing-mode: vertical-rl; //垂直方向布局,从右往左分列延伸

效果就像下面那样 MDN-write-mode

image.png

前端小知识包含块

2025年1月17日 22:33

我们以前应用布局的时候,经常会参考父元素,因此写文章很多基本上也会提到相对于父元素,实际上很多我们严重的父元素,其更精确的概念应该是包含块,因为单纯的父元素他不精确,有些是浮动元素或者定位元素,就不是明显的父子关系了

包含块确定规则

  • 常规元素和浮动元素父元素的内容盒
  • 绝对定位 absolute: 第一个定位祖先的填充盒(padding + content)
  • 固定定位 fixed:
    • 无变形祖先-往上找找不到使用了 transform 的父元素,其就是包含块就是视口
    • 有变形祖先-包含块有的祖先使用了 transform,就是变形祖先,包含块就是变形祖先

ps:看了fixed 这些是不是感觉天塌了,以前学的定位不太准了,实际上不是的,只是我们接触到得更深了,也能避免更多问题了,下面的 fixed 变形祖先实际上平时根本接触不到,因此按照以前总结也没啥问题,就当牛顿和爱因斯坦理论在合适的地方使用就好了,碰到了我们也知道咋回事就行了

我么更新一下我们的知识点,父元素换成包含块就正确了,否则全是不准确的哈

  • 元素的 width 百分比相对的是 父元素 包含块 宽度
  • 元素的 height 百分比相对的是 父元素 包含块 高度
  • 元素的 margin 百分比相对的是 父元素 包含块 宽度
  • 元素的 padding 百分比相对的是 父元素 包含块 宽度
  • 元素的 left 百分比相对的是 父元素 包含块 左边缘
  • 元素的 top 百分比相对的是 父元素 包含块 右边缘

NestJS中的Aop面向切面编程。不侵入业务逻辑,高复用通用逻辑。

作者 jinzunqinjiu
2025年1月17日 22:28

哈喽哈喽,我是你们的金樽清酒。寒假在家一直在学习nest。现在是已经学完了nest基础部分,开始往进阶学习了。我是通过看神光的文章学习的。基础知识呢又是最重要的。今天呢?我带大家了解一下什么是nestJs中的Aop编程,也叫面向切面编程。

声明:这是看了神光的课学习到的。大家想要更深入的了解可以看他的课。如有侵权行为必删。

什么叫面向切面编程呢?

nestjs采用的是MVC架构。相比于现代前端MVVM不同。

截屏2025-01-17 17.38.23.png 以前我们介绍过controller层是处理参数和路由的。Module层里面的provider是处理业务逻辑的。然后返回view视图。

在后端开发中,经常会有一些通用型的逻辑。比如打印日志呀,权限校验,异常处理。 那这些通用型逻辑写在什么地方呢?写在service里面,或者repository里面不够通过,难道每次都要调用这些方法嘛。那就写在controller里面,写在controller里面也会侵入业务逻辑,复用性不高。为此,我们得跳出业务逻辑,也就是在controller之前或之后进行统一的处理。nestjs提供了一种新的方式,叫AOP,面向切面编程。

什么叫面向切面编程呢?我们来看一下这个图。

截屏2025-01-17 17.47.02.png

在controller的前后,切一刀,来处理通用的逻辑,这就是AOP编程。这样复用性高,也不会侵入业务代码。

AOP编程的几种实现方式

AOP编程的实现方式呢有五种。

截屏2025-01-17 17.49.52.png 这是nestjs的官方的。我们来一一的了解一下吧。

Middware(中间件)

截屏2025-01-17 17.54.23.png 我们可以看到官方的图解,Middleware是发生在controller之前的。它可以获取请求数据和和响应数据。并且有一个next()函数。

我们可以尝试一下Middeware的用法。其实Middleware是express里面的功能。Nest的底层默认不是express嘛,还能切换fastify。

首先我们先创建一个项目。

npx nest new Aop
cd aop
npm run start

截屏2025-01-17 18.10.30.png 安装完之后将项目启动。 访问localhost:3000可以看到成功启动服务。

截屏2025-01-17 18.13.46.png

然后我们在main.ts里面全局启用Middleware。

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Response, Request, NextFunction } from 'express';

    async function bootstrap() {
        const app = await NestFactory.create(AppModule);
        app.use(function (req: Request, res: Response, next: NextFunction) {
         console.log(req.url, 'before');
        next();
        console.log('after');
     });
        await app.listen(process.env.PORT ?? 3000);
    }
bootstrap();

在路由里面也打印一下

截屏2025-01-17 18.24.20.png 我们再重启一下项目,访问一下页面。

截屏2025-01-17 18.25.49.png

可以看到打印结果。在cotroller之前和之后执行。不侵入业务代码。

在app.controller里面新增两个路由,aaa和bbb,访问localhost:3000/aaa和localhost:3000/bbb 可以看到控制台里面的打印结果。中间件在aaa路由和bbb路由的前后也会触发,说明可以高度服用逻辑。

截屏2025-01-17 18.32.06.png

这是全局使用Middleware,还可以在路由级别使用。 我们用nest cli创建一个路由中间件。

nest g middleware log --no-spec --flat

--no-spec是不生成测试文件。--flat是平铺,不生成文件夹。

截屏2025-01-17 18.47.58.png 我们分析一下怎么实现的。原来是继承实现了NestMiddleware类,用了@Injectable声明说明可以当成依赖注入。可以放到ioc容器中自动创建组装。

我们拿到app.module里面去使用。

import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';]
import { LogMiddleware } from './log.middleware';

@Module({
    imports: [],
    controllers: [AppController],
    providers: [AppService],
    })
    export class AppModule implements NestModule {
    configure(consumer: MiddlewareConsumer) {
    consumer.apply(LogMiddleware).forRoutes('aaa*');
    }
}

这样只有aaa开头的路由会生效这个中间件。

Guard

Guard相当于路由守卫,返回true或false来判断用户是否登录,有什么权限。

截屏2025-01-17 19.32.59.png

我们来看看怎么用。

截屏2025-01-17 19.34.03.png

Guards可以访问ExecutionContext实例,因此确切地知道接下来要执行的内容。它们的设计与异常过滤器、管道和拦截器一样,允许您在请求/响应周期的正确点插入处理逻辑,并声明性地这样做。这是官方说的,guard呢必须实现一个CanActivate类。然后根据获取到的ExecutionContext判断是否符合权限,返回true或false确定是否可以启用该路由。

那ExecutionContext里面又能获取到什么信息呢?

截屏2025-01-17 19.55.29.png 我们可以创建一个Guard看一下。

nest g guard login --no-spec --flat

截屏2025-01-17 20.01.40.png

这是生成的guard文件。它实现了canActive方法,里面有个参数ExecutionContext拿到我们想要的信息,进行权限验证,返回true或false决定是否执行这个路由。打印一下checked。

我们在controller里面用一下。

截屏2025-01-17 20.04.32.png

由于我们返回的是true,说明能走这个路由,再看一下打印的信息。

截屏2025-01-17 20.00.43.png

我们可以拿到请求的方式Get,请求的路由’/‘等等。

我们返回false试一下。 再访问这个路由就访问不通啦。

截屏2025-01-17 20.06.57.png

这就是Guard的作用。

Guard这种验证规则肯定要全局应用的。在main.ts里面全局应用这个实例。

截屏2025-01-17 20.09.28.png 这样所有的路由都可以使用了。

当然也可以作为provider全局启用。毕竟使用@Injectable装饰的。可以放进ioc容器,让ioc容器帮我们实例化并组装。

截屏2025-01-17 20.13.30.png 这样也能全局应用。而且可以注入其他的依赖。

Interceptor

Interceptor是拦截器,可以在controller之前或之后执行一些逻辑。

截屏2025-01-17 20.36.23.png

截屏2025-01-17 20.37.33.png

官方说要用@Injectable装饰器装饰,并且实现NestInterceptor接口。 我们来创建一个interceptor。

nest intorceptor 

截屏2025-01-17 20.56.46.png

我们可以看到生成的代码实现了NestInterceptore接口。并且有两个参数,一个是上下文信息,跟guard里面是一样的。一个是执行目标路由的函数。我们在里面添加该路由执行时间的代码。并在app.controller里面应用。

截屏2025-01-17 21.01.25.png 然后访问这个路由。

截屏2025-01-17 21.02.19.png 记得把前面全局应用的Guard注释掉,因为我们返回的是false,访问就是404报错了,不会走这个路由。 当然main.ts里面的Guard如果你没注释也要注释掉。

截屏2025-01-17 21.05.09.png 可以看到打印出开来的time。

ofcurse,也能全局应用,也有两种方式。

手动实例化 截屏2025-01-17 21.07.52.png 在module里面声明依赖关系

截屏2025-01-17 21.09.46.png 有时候我们需要对参数进行一些校验转换的一些通用逻辑,这时候要用上另一种AOP,pipe。而且用的特别多。对传入的表单数据进行校验之类的。

Pipe

截屏2025-01-17 21.21.34.png pipe是管道的意思,就是对参数做一些验证和转换。 我们先创建一个pipe。

截屏2025-01-17 21.24.18.png 这是我们生成的代码。value呢是从路由获取的参数,我们改一下。

截屏2025-01-17 21.42.03.png

对获取的参数value进行判断,如果可以被数字就转成数字乘10,不能就报错。 然后在controller里面写路由,用@Query获取url后面携带的参数。

截屏2025-01-17 21.44.44.png

截屏2025-01-17 21.40.56.png 拿到21的结果。我在url后面是num = 2.显然乘了10倍。

截屏2025-01-17 21.46.50.png 然后一样的可以用于某个参数,也可以用于全局。 手动实例化 截屏2025-01-17 21.49.59.png

还可以在app.module里面声明依赖。

截屏2025-01-17 21.51.59.png

ExceptionFilter

截屏2025-01-17 21.55.31.png exceptionFilter是用来抛出异常的。像前面我们用pipe的时候打印出来的异常就是它返回的。给出用户友好的错误反馈。

我们创建一个Exception filters。

nest g fliter test --no-spec --flat

截屏2025-01-17 21.59.13.png

用@Catch捕获错误。 对代码进行修改

import {
ArgumentsHost,
BadRequestException,
Catch,
ExceptionFilter,
} from '@nestjs/common';
import { Response } from 'express';


@Catch(BadRequestException)
    export class TestFilter implements ExceptionFilter {
    catch(exception: BadRequestException, host: ArgumentsHost) {
    const response: Response = host.switchToHttp().getResponse();
        response.status(400).json({
        statusCode: 400,
        message: 'test: ' + exception.message,
    });
    }
}

在handler上使用

截屏2025-01-17 22.09.11.png

可以看到刚才pipe抛出的错误信息发生了变化。 截屏2025-01-17 22.08.55.png

ExceptionFilter可以在controler层使用

截屏2025-01-17 22.11.53.png

也可以全局应用。 手动实例化。 截屏2025-01-17 22.14.19.png

也可以声明模块

截屏2025-01-17 22.15.54.png

各种AOP的执行顺序

我们可以看到,每种AOP有自己的功能,它的执行顺序呀肯定也是按照它的功能来决定的。我们画个图来捋一捋。

截屏2025-01-17 22.22.40.png 先中间件在controller之前可以做一些逻辑,然后是身份校验是否访问路由,请求拦截,对参数进行验证。通过路由处理业务逻辑,响应拦截,对前面抛出的错误进行响应,然后做出响应。

总结

nestjs中的AOP面向切面编程。可以在不改变业务逻辑的情况下,对一些通用逻辑进行一些切面的增删。提高了对通用逻辑的复用,又不影响业务逻辑。

❌
❌