普通视图

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

学到了学到了,一个小小的demo里隐藏着一个有趣的算法

作者 夕水
2025年4月11日 12:13

背景

在我总结的代码段中,有这样一段代码段。

const digitize = n => [...`${Math.abs(n)}`].map(i => parseInt(i));

这段代码的作用很简单,那就是将给定的数字转成数字数组,并且会去掉符号,然后再转成数字数组。

我们来看一些简单的使用示例。

digitize(-123); // [1,2,3]
digitize(123); // [1,2,3] 

当然这段代码也还存在问题,那就是并没有对小数做处理。例如:

digitize(12.3); // [1,2,NaN,3]

要改造起来也很简单,有2种方式,第一是在n中作处理,比如利用Math.ceil,Match.floor,Math.round方法来将n转换一遍。即:

const digitize = n => [...`${Math.abs(Math.floor(n))}`].map(i => parseInt(i));

第二种就是在最后的结果中过滤一下。如下:

const digitize = n => [...`${Math.abs(n)}`].map(i => parseInt(i)).filter(i => !isNaN(i));

当然,这不是本文的重点,接下来,我让trae帮我实现一个demo实战示例,如下所示:

截屏2025-04-11 上午11.50.32.png

随后trae帮我实现了一个如下图所示的示例:

截屏2025-04-11 上午11.50.59.png

这让我很好奇这里面的实现,结果竟让我发现了一个有趣的算法,这个算法叫做Luhn算法。那么重点来了,什么是Luhn算法?

我表示并不知道这个算法,孤陋寡闻了,接下来,让我们一起来了解一下这个算法吧。

luhn算法

定义

Luhn 算法(又称模10算法)是一种用于验证各种标识符(如信用卡号、社会保障号码、IMEI 号码等)是否有效的简单校验算法。它通过对数字序列进行一定的数学运算,生成一个校验位,从而检查号码的有效性。Luhn 算法的主要目的是检测输入号码是否符合预定的规则,防止常见的输入错误。

算法步骤

Luhn算法的步骤如下:

  1. 从右到左遍历数字序列: 从号码的最后一位开始,依次向左遍历每一位数字。
  2. 对偶数位数字进行加倍: 如果数字的位置是从右数的偶数位置(即第2、4、6...位),就将该数字加倍。如果加倍后的结果大于9,则将结果的各位数字相加(例如,18会变为1 + 8 = 9)。
  3. 将加倍后的数字与其他未加倍的数字相加: 将所有未加倍的数字和经过加倍处理后的数字相加。
  4. 检查总和是否为10的倍数: 最终的总和如果是10的倍数(即总和 % 10 == 0),则该号码是有效的;否则,该号码无效。

示例

假设要验证一个信用卡号码 4539 1488 0343 6467

  1. 从右到左遍历,并对偶数位数字加倍:

    • 7 (偶数位,未加倍)
    • 6 (偶数位,加倍为 12,然后将 1 + 2 = 3)
    • 4 (偶数位,未加倍)
    • 3 (偶数位,加倍为 6)
    • 6 (偶数位,未加倍)
    • 0 (偶数位,加倍为 0)
    • 8 (偶数位,未加倍)
    • 8 (偶数位,加倍为 16,然后将 1 + 6 = 7)
    • 依此类推。
  2. 计算所有数字的总和: 偶数位加倍后的数字和未加倍数字相加后,得到一个总和。

  3. 检查总和是否为10的倍数: 如果总和是10的倍数,则该号码是有效的。

通过这种方式,Luhn算法能够检测号码中的常见输入错误,如一个数字的错误或两个数字的交换。

算法应用

Luhn 算法广泛应用于:

  • 信用卡验证:用来确保输入的信用卡号符合格式规范。
  • 社会保障号码验证:检查社会保障号码是否有效。
  • IMEI号码验证:用于验证手机设备的 IMEI 编号。

算法实现

了解了这个算法之后,接下来,让我们来看看这个算法的实现。

首先根据算法描述,第一步我们需要从右往左开始遍历每一位数字,因此我们可以将给定的数字进行反转,注意我们可以将数字转成数字数组,然后再进行反转。

接着第二步,我们需要依次遍历数字数组中的每一个数,然后根据位置来判断是否为偶数(即循环索引值)。如果是偶数,则我们需要将该数加倍,然后判断是否大于9,如果大于9,则将结果的个位数字相加。但实际上这一步,我们可以换个思路想,如果结果大于9,实际上我们就可以将结果减去9即可以得到结果的个位数字相加后的结果。

例如18大于9,将结果相加即1 + 8 = 9,实际上也等价于18 - 9 = 9。

然后求出加倍数字与未加倍数字之和后,判断是否是10的倍数即可。

根据这个分析,我们就可以写出这个算法的实现。如下:

// 注意digits是数字数组,也就是经过digitize方法处理后结果
const validateCardWithLuhn = (digits) => {
    // 先对数字进行反转
    const reDigits = [...digits].reverse();
    
    let sum = 0; // 计算求和结果
    
    reDigits.forEach((digit, index) => {
        // 判断是否是偶数位置
        if(index % 2 === 1){
            // 如果是偶数位,则加倍
            digit *= 2;
            // 如果加倍结果大于9,则减去9,相当于将结果的个位数字相加后的结果
            if(digit > 9){
                digit -= 9;
            }
        }
        
        sum += digit; // 将结果相加
    })
    
    return sum % 10 === 0; // 然后比较是否是10的倍数
}

当然也可以使用for循环来替代forEach实现,都是可以的,让我们试试结果。如下所示:

validateCardWithLuhn(digitize(4539148803436467)); // true 这是一个有效的号码

又学到了一个新的算法,可以的。

想要查看这个示例的可以前往这里,如果觉得本文有用,感谢点赞收藏。

数字孪生-DTS-孪创城市-导览功能、虚拟现实

2025年4月11日 11:35

前言

今天,我们要聊的是dts的导览功能是如何实现的,这个功能非常适合做演示,汇报使用,点击城市漫游即可打开,以及虚拟现实功能,用于展示一些细节上的东西。

导览功能

需求

完成城市导览功能,当点击城市漫游时,开启导览功能,点击每一项导览,开始播放导览,再次点击时退出。

image-20250411110740399.png

思路

在animation这个类里面,我们先获取到所有的导览数据,然后通过camera这个类的播放功能即可实现。

首先获取所有的导览数据,使用的是 fdapi.camera.getAnimationList()这个方法,其返回的数据中会有id和name属性。可以发现我们的导览需要展示一个图片,这个图片资源需要从另一个api获取,叫做 fdapi.camera.getAnimationImage(name),传入的是导览的name字段,我们可以在animation挂载时获取数据。

const animationList = ref<{ name: string; id: number; img: string }[]>([])
onMounted(async () => {
  const { data } = await fdapi.camera.getAnimationList();// 获取导览列表
  data.forEach(async (item: { id: number, name: string, img: string }) => {
    const {image} = await fdapi.camera.getAnimationImage(item.name)// 获取导览图片
    const img = `data:image/png;base64,${image}`
    animationList.value.push({
      name: item.name,
      id: item.id,
      img
    })
  })
})

然后我们只需要在点击每一个导览是播放导览即可完成,这个api传入的是id字段

const clickAnimation = (item: { id: number, name: string }) => {
  fdapi.camera.playAnimation(item.id)// 开始播放导览
}

当再次点击时,我们调用fdapi.camera.stopAnimation();即可停止播放导览,这是在EquipmentManagement下的index文件。

 exit: () => {
      animationShow.value = false
      fdapi.camera.stopAnimation();
    }
涉及到的飞渡api
  1. 开启播放导览-fdapi.camera.playAnimation(item.id)

  2. 停止播放导览-fdapi.camera.stopAnimation()

  3. 获取导览列表-fdapi.camera.getAnimationList()

  4. 获取导览图片- fdapi.camera.getAnimationImage(item.name)

虚拟现实

需求

当点击虚拟现实时,进入全景图状态,再次点击时则退出。 image-20250411112917781.png

思路

首先我们需要在文件资源中准备好一张全景图图片,把他放在我们的library文件夹下。 image-20250411111550165.png

接下来将我们的资源加载到我们的项目中来,通过fdapi.panorama.add方法进行添加,然后设置我们的一个相机视角,让全景图与我们初始视角吻合,通过我们的老朋友fdapi.camera.set()方法进行添加。这里有一个小技巧就是在我们的cloud里面有一个api测试入口,我们可以在面板中先调整好相机的一个视角,然后找到camera中点击get方法即可拿到此时此刻的一个位置。

image-20250411112315119.png

然后只需要调用fdapi.panorama.enter(panoramaId)方法即可进入全景图中,为了让视觉效果更好一点,我们设置一个延时定时器,这样我们就实现了进入全景图功能了,注意这个图片资源路径需要一致。

let EnterTimeout: any = null
const panoramaId = 'VR'

export const initVR = async () => {
  await fdapi.panorama.add({
    id: panoramaId,
    imagePath: '@path:东方之门.jpg',
    coordinate: [564536.056016, 3466569.477578, 233.196602],
    coordinateType: 0, //坐标系类型,取值范围:0为Projection类型,1为WGS84类型,2为火星坐标系(GCJ02),3为百度坐标系(BD09),默认值:0
    yaw: -48, //方向
    onTerrain: false, //是否贴地,注意:设置为贴地后offset偏移量的Z轴会失效
    offset: [0, 0, 0] //偏移量
  })

  fdapi.camera.set(564536.056016, 3466569.477578, 233.196602, -2.069732, 169.979553, 2)
  EnterTimeout = setTimeout(() => {
    fdapi.panorama.enter(panoramaId)
  }, 2000)
}

接下来我们在退出时,做一些清理操作,退出全景图模式,删除全景图对象,删除定时器,即可。

export const exitVR = async () => {
  await fdapi.panorama.exit()
  await fdapi.panorama.delete(panoramaId)
  if (EnterTimeout) clearTimeout(EnterTimeout)
}
涉及的飞渡api
  1. 添加一个或多个Panorama全景图对象-fdapi.panorama.add()
  2. 设置相机视角-fdapi.camera.set()
  3. 进入全景图模式-fdapi.panorama.enter(panoramaId)
  4. 退出全景图模式-fdapi.panorama.exit()
  5. 删除全景图对象-fdapi.panorama.delete(panoramaId)

多语言编写的图片爬虫教程

2025年4月11日 11:34

可能他们需要几个不同编程语言的示例,比如Python、JavaScript(Node.js)、Ruby之类的。然后我要考虑每个语言常用的库和框架,确保示例简单易懂,适合不同水平的开发者。

接下来,我需要分步骤来思考每个语言的实现方式。比如Python的话,requests和BeautifulSoup是常用的组合,或者用Scrapy框架。对于Node.js,可能会用axios和cheerio,或者puppeteer来处理动态加载的图片。Ruby方面,可能用Nokogiri和HTTParty。

a4.png

以下是使用不同编程语言编写的图片爬虫示例,涵盖 PythonJavaScript (Node.js)Ruby 三种常见语言,并附核心思路解释:

1. Python 版本 (推荐)

依赖库: requests, BeautifulSoup, os

import os
import requests
from bs4 import BeautifulSoup

# 目标网页 URL
url = 'https://example.com/images'

# 创建保存图片的目录
os.makedirs('images_py', exist_ok=True)

# 发送 HTTP 请求
response = requests.get(url)
response.raise_for_status()  # 检查请求是否成功

# 解析 HTML
soup = BeautifulSoup(response.text, 'html.parser')

# 提取所有图片链接(根据实际网页结构调整选择器)
img_tags = soup.select('img')  # 或 soup.find_all('img', {'class': 'thumbnail'})

# 下载并保存图片
for img in img_tags:
    img_url = img.get('src')
    if not img_url.startswith('http'):
        img_url = f'https://example.com{img_url}'  # 补全相对路径
    img_data = requests.get(img_url).content
    filename = os.path.join('images_py', os.path.basename(img_url))
    with open(filename, 'wb') as f:
        f.write(img_data)
    print(f'下载完成: {filename}')

2. JavaScript (Node.js) 版本

依赖库: axios, cheerio, fs, path

const axios = require('axios');
const cheerio = require('cheerio');
const fs = require('fs');
const path = require('path');
const { promisify } = require('util');
const writeFile = promisify(fs.writeFile);

async function downloadImages() {
  const url = 'https://example.com/images';
  const dir = 'images_js';

  // 创建目录
  if (!fs.existsSync(dir)) fs.mkdirSync(dir);

  // 获取网页内容
  const response = await axios.get(url);
  const $ = cheerio.load(response.data);

  // 遍历图片标签
  $('img').each(async (i, img) => {
    let imgUrl = $(img).attr('src');
    if (!imgUrl.startsWith('http')) {
      imgUrl = new URL(imgUrl, url).href; // 处理相对路径
    }

    // 下载图片
    try {
      const imageResponse = await axios.get(imgUrl, { responseType: 'arraybuffer' });
      const filename = path.join(dir, path.basename(imgUrl));
      await writeFile(filename, imageResponse.data);
      console.log(`下载完成: ${filename}`);
    } catch (err) {
      console.error(`下载失败: ${imgUrl}`, err.message);
    }
  });
}

downloadImages();

3. Ruby 版本

依赖库: nokogiri, httparty, fileutils

require 'nokogiri'
require 'httparty'
require 'fileutils'

url = 'https://example.com/images'
dir = 'images_rb'

# 创建保存目录
FileUtils.mkdir_p(dir)

# 获取网页内容
response = HTTParty.get(url)
doc = Nokogiri::HTML(response.body)

# 提取图片链接
doc.css('img').each do |img|
  img_url = img['src']
  next unless img_url

  # 处理相对路径
  img_url = URI.join(url, img_url).to_s unless img_url.start_with?('http')

  # 下载图片
  begin
    image_data = HTTParty.get(img_url, { headers: { 'User-Agent' => 'Mozilla/5.0' } }).body
    filename = File.join(dir, File.basename(img_url))
    File.open(filename, 'wb') { |f| f.write(image_data) }
    puts "下载完成: #{filename}"
  rescue => e
    puts "下载失败: #{img_url} - #{e.message}"
  end
end

关键步骤解释

  1. 发送 HTTP 请求: 使用各语言的 HTTP 客户端库获取网页内容。
  2. 解析 HTML: 利用解析库(如 BeautifulSoup、Cheerio、Nokogiri)定位 <img> 标签。
  3. 处理图片路径: 确保相对路径转换为绝对 URL。
  4. 下载图片: 再次发送请求获取图片二进制数据。
  5. 保存文件: 将二进制数据写入本地文件。

注意事项

  1. 遵守规则: 检查目标网站的 robots.txt 和版权声明。
  2. 限速策略: 添加延迟(如 time.sleep(1))避免被封禁。
  3. 错误处理: 增加重试机制和异常捕获。
  4. 动态页面: 若页面通过 JavaScript 加载,需使用无头浏览器(如 Python 的 selenium 或 Node.js 的 puppeteer)。

根据实际需求调整选择器和 URL 处理逻辑。

Python使用爬虫IP抓取数据过程

2025年4月11日 11:23

用户之前询问了不同语言的爬虫示例,特别是Python、Node.js和Ruby。现在他们希望详细扩展Python版本中使用代理IP的过程。

用户可能已经掌握了基本的爬虫编写,但遇到了IP被封的问题,或者想防止被封。他们需要知道如何集成代理IP到现有的代码中,可能还关心代理IP的来源、验证以及轮换策略。

a2.png

首先,我需要明确代理IP在爬虫中的作用:隐藏真实IP,避免被目标网站封禁。接下来,要详细说明如何在Python的requests库中添加代理。用户可能需要代码示例,所以应该提供具体的代码修改部分,比如在requests.get()中添加proxies参数。

然后,用户可能需要知道如何获取代理IP,包括免费和付费的来源。免费代理的稳定性可能不好,付费的更可靠,但需要提醒用户注意选择可信的服务商。同时,需要指导用户如何验证代理是否有效,比如发送测试请求检查响应状态码。

以下是为 Python 版本爬虫 添加代理 IP 功能的详细扩展说明,涵盖代理的获取、配置、轮换和异常处理:

1. 代理 IP 的核心作用

  • 隐藏真实 IP:避免目标网站封禁你的服务器或本地 IP。
  • 突破访问限制:绕过地域或频率限制。
  • 分布式爬取:通过多 IP 分散请求压力。

2. 代理 IP 的配置方法(代码示例)

2.1 单次请求使用固定代理

requests.get() 中添加 proxies 参数:

import requests

proxies = {
    'http': 'http://123.45.67.89:8080',  # HTTP 协议代理
    'https': 'http://123.45.67.89:8080'  # HTTPS 协议代理(若与 HTTP 相同可省略)
}

response = requests.get(url, proxies=proxies, timeout=10)

2.2 代理认证(用户名密码)

若代理需要认证,格式为 http://user:pass@ip:port

proxies = {
    'http': 'http://username:password@123.45.67.89:8080',
    'https': 'http://username:password@123.45.67.89:8080'
}

3. 动态代理池实现(自动轮换 IP)

3.1 从文件或 API 加载代理池

# 假设代理 IP 列表保存在 proxies.txt 或通过 API 获取
def load_proxies():
    with open('proxies.txt', 'r') as f:
        proxies = [line.strip() for line in f]
    return proxies

proxy_pool = load_proxies()

3.2 随机选择代理 + 异常重试

import random

def download_with_proxy(img_url, retries=3):
    for _ in range(retries):
        try:
            proxy = random.choice(proxy_pool)
            proxies = {'http': proxy, 'https': proxy}
            response = requests.get(img_url, proxies=proxies, timeout=10)
            response.raise_for_status()
            return response.content
        except (requests.exceptions.ProxyError, requests.exceptions.ConnectTimeout):
            # 移除失效代理
            proxy_pool.remove(proxy)
            print(f"移除失效代理: {proxy}")
        except requests.exceptions.RequestException as e:
            print(f"请求失败: {e}")
    return None

4. 完整代码整合(带代理功能)

import os
import requests
from bs4 import BeautifulSoup
import random

# 代理池初始化
def load_proxies():
    # 示例:从文件加载,实际可替换为 API 获取
    return [
        'http://123.45.67.89:8080',
        'http://111.222.33.44:3128',
        # 更多代理...
    ]

proxy_pool = load_proxies()

def download_image(img_url, save_dir):
    for _ in range(3):  # 最多重试 3 次
        if not proxy_pool:
            print("无可用代理,终止下载")
            return
        proxy = random.choice(proxy_pool)
        proxies = {'http': proxy, 'https': proxy}
        try:
            response = requests.get(img_url, proxies=proxies, timeout=15)
            response.raise_for_status()
            filename = os.path.join(save_dir, os.path.basename(img_url))
            with open(filename, 'wb') as f:
                f.write(response.content)
            print(f"成功下载: {filename} (使用代理: {proxy})")
            return
        except Exception as e:
            print(f"代理 {proxy} 失败: {str(e)}")
            proxy_pool.remove(proxy)  # 移除失效代理

# 主函数
def main():
    url = 'https://example.com/images'
    save_dir = 'images_py'
    os.makedirs(save_dir, exist_ok=True)
    
    try:
        # 获取页面(使用代理)
        proxy = random.choice(proxy_pool)
        response = requests.get(url, proxies={'http': proxy}, timeout=10)
        response.raise_for_status()
        soup = BeautifulSoup(response.text, 'html.parser')
        
        # 遍历图片
        for img in soup.select('img'):
            img_url = img.get('src')
            if not img_url.startswith('http'):
                img_url = f'https://example.com{img_url}'
            download_image(img_url, save_dir)
    except Exception as e:
        print(f"主流程错误: {e}")

if __name__ == '__main__':
    main()

5. 代理 IP 的获取渠道

  1. 免费代理(适合小规模测试):

  2. 付费代理(推荐生产环境):

    • 服务商:BrightData(Luminati)、Oxylabs、SmartProxy
    • 特点:高匿、稳定、支持并发。

6. 代理有效性验证

在加载代理池时,先筛选可用 IP:

def validate_proxy(proxy):
    try:
        test_url = 'http://httpbin.org/ip'  # 返回请求的 IP
        response = requests.get(test_url, proxies={'http': proxy}, timeout=10)
        return response.json()['origin'] in proxy
    except:
        return False

# 过滤无效代理
valid_proxies = [p for p in proxy_pool if validate_proxy(p)]

7. 高级优化方向

  1. 代理类型支持

    • SOCKS5 代理:使用 requests[socks] 库,配置格式 'socks5://user:pass@host:port'
  2. 异步请求

    • 使用 aiohttp + asyncio 提升并发效率。
  3. 商业代理 SDK

    • 集成如 BrightData 的 SDK 自动管理 IP 轮换。
  4. IP 地理定位

    • 选择特定地区的代理 IP(如 proxy = 'http://jp.xxx.xxx:8080')。

8. 注意事项

  1. 法律合规:确保代理来源合法,避免侵犯隐私。
  2. 频率控制:即使使用代理,仍需设置合理延迟(如 time.sleep(2))。
  3. Header 伪装:添加随机 User-Agent 和 Referer。
  4. 日志监控:记录代理使用情况,便于排查问题。

通过代理 IP 的合理配置,可显著提升爬虫的稳定性和抗封禁能力。

🚀 你所不知道的 9个CSS 小知识!

2025年4月11日 11:19

大家好,我是 前端架构师 - 大卫

更多优质内容请关注微信公众号 @程序员大卫

初心为助前端人🚀,进阶路上共星辰✨,

您的点赞👍与关注❤️,是我笔耕不辍的灯💡。

1.图片引用的简写写法

依次表示:图片地址、不平铺、位置(左上角)、缩放方式(等比缩放至完全包含)。

.bg {
  background: url(xxx) no-repeat 0 0 / contain;
}

2.paddingmargin 的百分比计算方式

它们的百分比值是基于包含块的宽度(即父元素的宽度)进行计算的,而不是高度。

例如:

<style>
  * {
    padding: 0;
    margin: 0;
    box-sizing: border-box;
  }
  body {
    padding: 100px;
  }
  .box {
    width: 100px;
    height: 200px;
    overflow: hidden;
    outline: 1px solid red;
  }
  .content {
    margin: 10%;
    padding: 10%;
  }
</style>
<div class="box">
  <div class="content">Hello</div>
</div>

content 元素的 10% margin 和 padding,都是基于 .box100px 宽度计算的,因此最终为 10px

3.用 background 模拟 border: dashed 的效果

.box {
  height: 1px;
  background: linear-gradient(to right, #658aaa 50%, transparent 50%),
              linear-gradient(to right, #658aaa 50%, transparent 50%);
  background-size: 6px 1px;
  background-position: 0 0, 0 100%;
  background-repeat: repeat-x;
}

通过两条水平的 linear-gradient 背景图层,分别放在顶部和底部,来模拟上下边框为虚线的效果。

4.CSS Grid 中的 24 栏布局理念

源自于传统 UI 框架(如 Ant Design、Element UI)中的栅格系统设计,其优势包括:

  • 可整除性强:24 可被 2、3、4、6、8、12 等多个数字整除,便于灵活组合布局。
  • 更细粒度控制:比常见的 12 栏或 16 栏系统提供更精细的宽度控制。
  • 统一性好:在团队协作中,使用统一的布局系统能提升开发效率与设计一致性。

5.-webkit-min-device-pixel-ratio 属性说明

这是一个用于媒体查询的 CSS 属性,常用于判断设备的像素比(Device Pixel Ratio,简称 DPR)。

它可用于为高分辨率屏幕(如 Retina)提供更清晰的样式,例如:

@media only screen and (-webkit-min-device-pixel-ratio: 2),
       only screen and (min-resolution: 2dppx) {
  /* 针对 DPR 为 2 的设备应用高清样式 */
}

建议结合 min-resolution 一起使用,增强兼容性。

6.移除 IE11 中 select 的默认样式

select::-ms-expand {
  display: none;
}
select {
  border: none;
}

::-ms-expand 是 IE 和旧版 Edge 中 select 右侧小箭头的伪元素,隐藏它可以实现更灵活的自定义样式。

7.让 div 占满父元素剩余高度的几种方式

HTML 结构:

<style>
  .container {
    height: 400px;
    width: 200px;
    outline: 1px solid red;
  }
  .one {
    height: 100px;
    background-color: blue;
  }
  .two {
    background-color: red;
  }
</style>
<div class="container">
  <div class="one"></div>
  <div class="two"></div>
</div>

方法一:calc 计算高度

.two {
  height: calc(100% - 100px);
}

方法二:绝对定位

.container {
  position: relative;
}
.two {
  position: absolute;
  top: 100px;
  bottom: 0;
  left: 0;
  right: 0;
  background: #ff0;
}

方法三:Flex 布局

.container {
  display: flex;
  flex-direction: column;
}
.two {
  flex-grow: 1;
}

推荐使用 Flex 布局,结构更清晰,响应式也更友好。

8.自定义滚动条颜色

<style>
  .box {
    width: 300px;
    height: 300px;
    border: 1px solid #000;
    overflow: scroll;
    /* 注意:不要加 -webkit-overflow-scrolling: touch,会导致 iOS 下滚动条消失 */
  }

  .box .inner {
    width: 1000px;
    height: 600px;
  }

  .box::-webkit-scrollbar {
    height: 9px;
  }

  .box::-webkit-scrollbar-thumb {
    border-radius: 10px;
    background-color: blue;
  }

  .box::-webkit-scrollbar-track {
    border-radius: 10px;
    background-color: red;
  }
</style>
<div class="box">
  <div class="inner"></div>
</div>

9.position 属性详解

  • static:默认值,不进行定位,元素按正常文档流排列。
  • relative:相对定位,元素相对其原本位置偏移,但仍占据原有空间。
  • absolute:绝对定位,元素脱离文档流,定位相对于最近的已定位(非 static)祖先元素;若没有,则相对于 html 元素。
  • fixed:固定定位,元素脱离文档流,默认相对于视口(viewport)定位;但若祖先元素设置了 transformfilterperspective 等属性,则可能相对于该祖先元素定位。
  • sticky:粘性定位,元素在滚动达到设定阈值前处于正常流中,之后会固定在指定位置。
  • inherit:继承父元素的 position 属性值。

🌟好看又好用的画图工具分享

2025年4月11日 11:17

excalidraw

excalidraw.com/ Excalidraw 是一个开源的虚拟白板绘图工具,以其手绘风格的图形和简洁直观的界面而广受欢迎。它特别适合在头脑风暴、产品设计、流程图绘制和团队协作中使用。我喜欢用它来当一个无限的画板,讲东西的时候把需要讲的内容都丢上去讲到哪部分放大哪部分。

image.png

核心特点

手绘风格图形

线条看起来像是手绘的,营造出轻松自然的氛围,适合原型图和非正式交流。

简洁的 UI

操作非常直观,支持拖动、缩放、对齐、分组等基本绘图功能。

支持协作

可通过链接进行实时协作(类似 Google Docs),适合远程团队实时共同编辑。

丰富的图形元素

支持绘制矩形、圆形、线条、箭头、自由绘制、文字等。 支持插入图像、图标、甚至 markdown 文本。

离线使用

可离线运行(PWA 支持),也可以下载桌面版本或将其嵌入到自己的项目中。

支持导出

可导出为 PNG、SVG、或 Excalidraw 自身格式(.excalidraw)。 可保存为 JSON 以便以后继续编辑。

eraser

app.eraser.io/ 我很喜欢 Next.js 官网流程图的风格。为了找出这种风格的流程图是用什么工具绘制的,我专门花了一些时间查找,发现网上提到的人并不多。

image.png

核心功能

AI 辅助图表生成

Eraser AI 作为技术设计的辅助工具,能够根据自然语言提示自动生成架构图、流程图、ER 图和时序图等。生成的图表以“图表即代码”的形式表示,便于后续编辑和维护。

图表即代码(Diagram-as-Code)

通过代码定义图表结构,确保图表始终清晰、易于维护。用户可以直接编辑图表代码,或使用 AI 提示进行修改。

Markdown 编辑器

Eraser 提供内置的 Markdown 编辑器,支持插入实时更新的图表,便于创建结构化的技术文档。文档可保存为 .md 文件,并同步到 GitHub 仓库。

实时协作与版本控制

支持团队成员实时协作编辑文档和图表,自动保存更改并生成快照,确保数据安全和版本可追溯。

丰富的图表模板与图标库

内置超过 1000 个技术图标和云服务图标,涵盖 GCP、Azure、Kubernetes 等,满足多种架构图需求。

codeimage

app.codeimage.dev/ Codeimage.dev 是一个开源的网页工具,专门用于将代码片段转换为美观的图像。这个工具适合开发者在社交媒体、博客、技术文章或者演示文稿中展示代码。

image.png

核心功能

多样化主题与窗口样式:内置 20 多种代码高亮主题,支持自定义窗口样式(如 macOS、Windows 风格),满足不同审美需求。

丰富的自定义选项:用户可以调整字体、颜色、背景、行号显示等,打造个性化的代码截图。

高质量导出:支持导出高分辨率图片,确保在各种平台上展示清晰。

代码片段管理:注册用户可以保存和管理自己的代码片段,方便日后查阅和分享。

即将推出的功能:如主题构建器、嵌入式分享、访问统计等,进一步提升用户体验。

TypeScript中如何优雅处理ant-design-vue的a-select的默认空值

2025年4月11日 11:15

如果你也使用ant-design-vue和TypeScript,你可能遇到过和我一样的问题:在TypeScript环境下使用ant-design-vue的a-select组件时,如何优雅地处理默认空值。听起来好像很复杂,实际一点也不简单。

空值三兄弟:""nullundefined

当我们想让a-select默认显示placeholder时,通常会想到这三个"falsy"值:空字符串""nullundefined

<template>
  <a-select v-model:value="fruit" placeholder="请选择你的水果" />
</template>

<script lang="ts" setup>
import { ref } from 'vue'
const fruit = ref() // ? ref('')? ref(null)? ref(undefined)?
</script>

那就逐一尝试一下。

空字符串""

当你赋予空字符串,会发现本该在空值时显示的placeholder并没有显示,这是因为ant-design-vue认为空字符串是一个有意义的值,而非空值(这一点后文的issue截图中可以印证)。

null

null怎么样呢,尝试发现placeholder显示正常,但编辑器会触发TypeScript报错提示:不能将类型“null”分配给类型“SelectValue”。(当然,如果你不使用TypeScript,就没有这个问题,直接使用null没有任何问题)。

<template>
  <a-select v-model:value="fruit" />
  <!--              ^? 不能将类型“null”分配给类型“SelectValue”。 -->
</template>

我们来看下SelectValue的类型定义:

export declare type SelectValue = RawValue | RawValue[] | LabeledValue | LabeledValue[] | undefined;
declare type RawValue = string | number;
export interface LabeledValue {
    key?: string;
    value: RawValue;
    label?: any;
}

undefined

从类型定义可以看到,官方应该是希望我们使用undefined作为默认空值。使用undefined也确实不影响placeholder的显示,但会有另一个更致命的问题:值为undefined的字段在fetch/XHR请求中很可能会被过滤掉

我使用axios作为请求库,当我使用content-type: application/json发送带有值为undefined的字段时,该字段会被过滤掉而不会出现在最终的Request Payload中。这是因为axios在源码中会使用JSON.stringify()来序列化请求参数。

这部分源码在lib/defaults/index.js

if (isObjectPayload || hasJSONContentType ) {
  headers.setContentType('application/json', false);
  return stringifySafely(data);
}

以及stringifySafely的实现

function stringifySafely(rawValue, parser, encoder) {
  if (utils.isString(rawValue)) {
    try {
      (parser || JSON.parse)(rawValue);
      return utils.trim(rawValue);
    } catch (e) {
      if (e.name !== 'SyntaxError') {
        throw e;
      }
    }
  }

  return (encoder || JSON.stringify)(rawValue);
}

undefinedJSON.strigify()的序列化过程中会被忽略,下面是引用的MDN的原文,可以看到更多细节:

undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。函数、undefined 被单独转换时,会返回 undefined,如JSON.stringify(function(){}) or JSON.stringify(undefined).

当然这不仅仅是axios的问题,如果你使用fetch APIcontent-type: application/json的请求可能也需要使用JSON.stringfy来处理,MDN的示例代码里就是这么做的。其它的请求库我没有求证,大概率也有这个问题。

社区的声音

null运行时表现正常,但类型不匹配。undefined类型匹配,但运行时不特殊处理会出bug的啊。ant-design-vue的官方为什么就不能把null纳入SelectValue的类型联合中呢,这好像是最好的解决方案。其实不光是我这么想,早在2019年社区就有人提了这个issue

也有人建议官方把null纳入空值,但官方并没有做出回应,该issue已被超时自动关闭。并且截至目前为止的最新版本4.2.6该问题依旧存在。

Snipaste_2025-04-11_10-16-14.png

解决方案

那能怎么办呢,官方不解决,我的业务代码还得写啊。我想到的解决方案有两种:

方案1

第一种方案是修改node_modules包中SelectValue的类型定义,并使用patch-package来打补丁。但为这么小的一类型问题打一个补丁,感觉是杀鸡用牛刀了。

方案2

第二种方案是在发送请求的时候手动把所有值为undefined的字段替换为null。既然是全局的,肯定不能每个请求的地方都执行一遍这个逻辑,axios的请求配置transformRequest允许在向服务器发送前,修改请求数据,这是最合适的修改方式。

我们先来实现这个数据转换的核心函数,需要考虑到对象深层的递归处理:

/**
 * 递归替换对象中所有的 undefined 为 null
 * @param obj
 * @returns
 */
function replaceUndefinedWithNull(obj: Record<string, any>): Record<string, any> {
  if (typeof obj !== 'object' || obj === null) {
    return obj // 如果不是对象或数组,直接返回
  }

  // 处理数组
  if (Array.isArray(obj)) {
    return obj.map((item) => replaceUndefinedWithNull(item))
  }

  // 处理普通对象
  const result: Record<string, any> = {}
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      const value = obj[key]
      if (value === undefined) {
        result[key] = null // 替换 undefined 为 null
      } else {
        result[key] = replaceUndefinedWithNull(value) // 递归处理嵌套对象或数组
      }
    }
  }
  return result
}

然后是配置,需要注意,我们直接给transformRequest赋新值会覆盖掉axios内置的transformRequest,我们只需要把内置的默认transformRequest取出来再塞进去即可:

axios.create({
  transformRequest: [
    replaceUndefinedWithNull,
    (axios.defaults.transformRequest as AxiosTransformer[])[0],
  ],
  // other configs
})

总结

本来是个小问题,但还是展开说了不少。就连五星评论家麦克阿瑟也忍不住总结道:

Snipaste_2025-04-11_11-11-29.png

  • 在TypeScript中使用ant-design-vue的a-select组件时,如何处理默认空值是个问题:空字符串不能正常显示placeholder;null会导致TypeScript类型报错;undefined很可能会导致在请求发起时被从最终的请求参数中过滤掉该字段。这是因为JSON.stringify()序列化会忽略掉undefined
  • 如果不使用TypeScript,直接使用null是最简单的方案。
  • 社区有声音呼吁官方把null纳入空值处理,但并没有得到回应和处理。
  • 比较好的解决方案是在发起请求前统一处理请求参数,比如axios可以在transformRequest中进行处理。

红宝书第六讲:作用域链与闭包:厨房里的调味料架原理

作者 kovli
2025年4月11日 11:10

红宝书第六讲:作用域链与闭包:厨房里的调味料架原理

资料取自《JavaScript高级程序设计(第5版)》。 查看所有教程:红宝书学习大纲


一、什么是作用域链?(洋葱层级结构)

想象你在厨房做菜,每个调料瓶按使用频率由近到远摆放:

  1. 当前厨房台面:手边的盐、酱油 → 相当于函数内部变量(局部作用域)
  2. 冰箱里的辣椒酱:常用但稍远 → 外层函数的变量(父级作用域)
  3. 储藏室的大米:最远的储备 → 全局变量(全局作用域)

作用域链的查找顺序

function 厨房() {
  const 盐 = "适量盐"; // 第二层作用域

  function 炒菜() {
    console.log(盐); // ✅ 能找到冰箱里的盐
    console.log(大米); // ✅ 能找到储藏室的大米
  }
  
  炒菜();
}

const 大米 = "5kg"; // 第三层(全局)
厨房();

二、闭包:私人调料罐

闭包就像你出门前把常用调料装进便携罐带走:

  • 定义:内部函数保留外部变量的引用,“保鲜”这部分变量的值。
  • 关键特点:即使外层函数已执行完毕,闭包仍然能访问其变量。

代码示例

function 制盐机() {
  let 盐堆存量 = 1000;   // 盐堆存量被内部函数“锁住”

  return function 取盐(需要量) {
    盐堆存量 -= 需要量;  // 每次取盐减少存量
    console.log(`剩余盐量:${盐堆存量}g`);
  };
}

const 我的取盐动作 = 制盐机();
我的取盐动作(100);  // 剩余盐量:900g
我的取盐动作(200);  // 剩余盐量:700g
  • 分析制盐机()执行后,本应销毁的盐堆存量取盐函数保留,形成闭包[^3]。

三、闭包的两面性

益处

  1. 数据保护:避免变量全局污染,如封装计数器:
    function 创建计数器() {
      let count = 0;
      return () => ++count; // 只能通过闭包修改
    }
    const 计数器 = 创建计数器();
    计数器(); // 1
    

潜在问题

  • 内存泄漏:如果闭包引用大型数据且未被释放,可能影响性能:

风险示例

function 加载大数据() {
  const 大数据 = new Array(1000000).fill('🚗'); // 内存大户

  return function 处理函数() {
    // 即使不再需要大数据,闭包仍保留引用
  };
}
let 保存闭包 = 加载大数据();
保存闭包 = null; // ✅ 需手动解除引用

四、生活中的闭包场景

  1. 按钮点击统计:记录用户点击按钮次数。
  2. 接口请求防重复:闭包保存最后一次请求标识。
  3. 模块化开发:借助IIFE(立即执行函数)封闭作用域:
    const 模块 = (function() {
      const 内部数据 = "私有";
    
      return {
        get数据: () => 内部数据
      };
    })();
    
    console.log(模块.get数据()); // '私有'
    

目录:总目录 上篇文章:红宝书第五讲:函数声明与表达式小白详解

总结与练习

  • 理解步骤:先画作用域层级 → 再看闭包如何抓取变量。
  • 练习:实现一个缓存计算结果的闭包函数。

重要提醒:闭包虽强大,但控制引用范围,避免内存包袱过重!

Flutter开箱即用一站式解决方案-新增企业级日志

作者 耳東陈
2025年4月11日 11:06

Flutter Chen Common

🌟 简介

本库为Flutter应用开发提供一站式解决方案,包含:

  • 可定制的主题系统
  • 完整的国际化支持
  • 企业级网络请求封装
  • 企业级日志体系封装
  • N+高质量常用组件
  • 常用开发工具及扩展集合
  • 刷新列表一整套解决方案
  • 开箱即用的通用各类弹窗
  • 全局统一各状态布局

特性

  • 🎨 主题系统:通过 ThemeExtension 全局配置颜色/圆角/间距等样式
  • 🌍 国际化支持:内置中英文,支持自定义文本和动态语言切换
  • 优先级覆盖:支持全局配置 + 组件级参数覆盖
  • 📱 自适应设计:完美适配 iOS/Material 设计规范
  • 🚀 企业级方案:内置日志/网络/安全等通用模块,提供开箱即用的复杂场景解决方案

安装

pubspec.yaml 中添加依赖:

/// 1.8.0版本已移除图片选择裁剪上传oss一站式解决方案
dependencies:
  flutter_chen_common: 最新版本

运行命令:

flutter pub get

快速开始

初始化配置

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // 存储初始化
  await SpUtil.init();
  // 日志初始化
  await Log.init(
    const LogConfig(
      retentionDays: 3,
      enableFileLog: true,
      logLevel: LogLevel.all,
      recordLevel: LogLevel.info,
      output: [CustomSentryOutput()],
    ),
  );
  // 网络模块初始化
  HttpClient.init(
    config: HttpConfig(
      baseUrl: 'https://api.example.com',
      connectTimeout: const Duration(seconds: 30),
      receiveTimeout: const Duration(seconds: 30),
      enableLog: true,
      maxRetries: 3,
      interceptors: [CustomInterceptor()]
   ),
  );

  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ComConfiguration(
      config: ComConfig.defaults().copyWith(
        emptyWidget: CustomEmptyWidget(), // 自定义全局空视图
        loadingWidget: CustomLoading(),  // 自定义全局加载视图
      ),
      child: MaterialApp(
        theme: ThemeData.light().copyWith(
          extensions: [ComTheme.light()], // 启用亮色主题
        ),
        darkTheme: ThemeData.dark().copyWith(
          extensions: [ComTheme.dark()], // 启用暗色主题
        ),
        home: MainPage(),
        localizationsDelegates: [
          ComLocalizations.delegate, // 国际化
        ],
        supportedLocales: [
          const Locale('zh', 'CN'),
          const Locale('en', 'US'),
        ],
      ),
    );
  }
}

网络请求

// 网络请求使用
HttpClient.instance.request(
  "/xxxx",
  method: HttpType.post.name,
  fromJson: (json) => User.fromJson(json),
  showLoading: true,
)

// HttpConfig,内置日志打印、网络重试拦截器(后续会新增token无感刷新以及相关队列操作)
HttpConfig({
    required this.baseUrl,
    this.connectTimeout = const Duration(seconds: 15),
    this.receiveTimeout = const Duration(seconds: 15),
    this.sendTimeout = const Duration(seconds: 15),
    this.commonHeaders = const {},
    this.interceptors = const [],
    this.enableLog = true,
    this.maxRetries = 3,
  });

// 打印样式如下(日志打印完全不会被截断,json格式化方便复制查看数据,在开启日志拦截以及记录日志时会将日志写入文件
┌─────────────────────────────────────────────────────────────────────────────
│ ✅ [HTTP] 2025-04-05 23:30:29 Request sent [Duration] 88ms
│ Request: 200 GET http://www.weather.com.cn/data/sk/101010100.html?xxxx=xxxx
│ Headers: {"token":"xxxxx","content-type":"application/json"}
│ Query: {"xxxx":"xxxx"}
│ Response: {"weatherinfo":{"city":"北京","cityid":"101010100","WD":"东南风"}}
└──────────────────────────────────────────────────────────────────────────────

📝日志体系

// 统一调用示例
Log.d("debug message");
Log.i("info message");
Log.w("warning message");
Log.e("error message");
Log.console("console message 可完整打印不被截断并且无前缀");

// 获取日志文件目录(已写了原生zip压缩加密库,可在app中分享日志加密压缩包,等我发布pub整好文档会发出来)
final Directory dir = await Log.getLogDir();

class LogConfig {
  final int retentionDays; // 日志保留天数
  final bool enableFileLog; // 是否启用日志写入
  final LogLevel logLevel;  // 日志过滤级别,低于该日志级别不打印
  final LogLevel recordLevel;   // 日志记录级别(Network日志级别分别是Info、Error),低于该日志级别不写入日志文件
  final List<LogOutput>? output;  // 可自定义扩展LogOutput,如Sentry上报、日志上传服务器、加密脱敏输出等(类似dio拦截器)

  const LogConfig({
    this.retentionDays = 3,
    this.enableFileLog = true,
    this.logLevel = LogLevel.all,
    this.recordLevel = LogLevel.info,
    this.output,
  });
}

🎨 主题系统

内置主题

主题名称 示例代码
Light Theme ComTheme.light
Dark Theme ComTheme.dark

可配置属性

ComTheme(
  theme: ComColors.lightTheme,  // 颜色体系
  shapes: ComShapes.standard,// 圆角体系
  spacing: ComSpacing.standard,// 间距体系
  primaryGradient: LinearGradient(
    colors: [
      ComColors.lightTheme.shade500,
      ComColors.lightTheme.shade500,
    ],
  ),
  success: Colors.green.shade600,
  error: Colors.red.shade600,
  warning: Colors.orange.shade600,
  link: Colors.blue.shade600,
)

// 色系
static MaterialColor lightTheme = const MaterialColor(
  0xFF3783FD,
  <int, Color>{
    50: Color(0xfff8f6f9), // surface 背景色
    100: Color(0xfff8f2fa), // surfaceContainerLow 浅色背景色
    200: Color(0xfff2ecf4), // surfaceContainer 标准背景色
    300: Color(0xffece6ee), // surfaceContainerHigh 较深背景色
    400: Color(0xffe6e0e9), // surfaceContainerHighest 深色背景色
    500: Color(0xFF3783FD), // primary 主题色
    600: Color(0xff1d1b20), // onSurface 主要内容色
    700: Color(0xFF909399), // onSurfaceVariant 次要内容色
    800: Color(0xffffffff), // surfaceContainerLowest 相同色
    900: Color(0xff322f35), // inverseSurface 相反色
  },
);

🌍 国际化配置

// 语言新增或覆盖
// 1. 创建法语本地化类
class FrIntl extends ComIntl {
  @override String get confirm => "xxx";
  @override String get cancel => "xxx";
  @override String get loading => "...";
}

// 2. 注册语言
ComLocalizations.addLocalization('fr', FrIntl());

// 3. 配置MaterialApp
MaterialApp(
  supportedLocales: [
    Locale('fr'), // 新增法语支持
  ],
)

全局状态布局

// 全局配置或局部配置
ComConfiguration(
  config: ComConfig.defaults().copyWith(
    emptyWidget: const ComLoading(), // 定义全局空视图
    loadingWidget: const ComEmpty(),   // 定义全局加载视图
    errorWidget: const ComErrorWidget(), // 定义错误加载视图
    noNetworkWidget: (VoidCallback? onReconnect) =>
                    ComNoNetworkWidget(onReconnect: onReconnect), // // 定义全局网络错误视图
  ),
  child: child,
);

// BaseWidget的各状态布局默认使用全局统一配置,局部可自定义
// isConnected配合connectivity_plus库自动实现无网络情况显示无网络状态布局,网络正常情况显示正常布局
// status控制页面各状态内容布局显示
BaseWidget(
  isConnected: isConnected,
  status: LayoutStatus.loading,
  loading: const ComLoading(),
  empty: const CustomEmpty(),
  error: BaseWidget.errorWidget(context),
  noNetwork: BaseWidget.noNetworkWidget(context),
  onReconnect: (){},
  child: child,
)

// 全局统一使用
BaseWidget.loadingWidget(context)
BaseWidget.errorWidget(context)
...

📦 工具类(Utils)

文件名 功能描述
clipboard_util.dart 剪贴板操作工具(复制/粘贴文本、监听剪贴板内容)
clone_util.dart 对象深拷贝/浅拷贝工具(支持复杂对象克隆)
color_util.dart 颜色处理工具(HEX与RGB互转、颜色混合、随机颜色生成)
date_util.dart 日期时间工具(格式化、解析、计算时间差)
device_util.dart 设备信息工具(获取设备信息)
encrypt_util.dart 加密解密工具(算法封装)
file_util.dart 文件操作工具(读写文件、目录管理、文件压缩/解压)
function_util.dart 通用函数工具(防抖/节流、空安全处理、类型转换)
image_util.dart 图片处理工具(压缩、缓存管理、网络图片加载、格式转换)
json_util.dart JSON工具(序列化/反序列化、动态解析、数据校验)
keyboard_util.dart 键盘工具(控制键盘显隐、监听高度变化)
log_util.dart 日志工具(分级输出、日志存储、调试模式开关)
package_util.dart 应用包管理工具(获取应用包信息)
permission_util.dart 权限管理工具(全局权限处理、多权限判断及请求)
sp_util.dart 本地存储工具(基于SharedPreferences,支持复杂数据存取)
text_util.dart 文本处理工具(字符串校验、截断、正则匹配)
dialog_util.dart 弹窗工具类(通用各类弹窗Toast、Android、iOS确定弹窗、弹窗、选择弹窗、底部弹窗等)

🎨 通用组件(Widgets)

文件名 功能描述
refresh_widget.dart 刷新列表组件(包含上拉加载、下拉刷新、回至顶部、页面数据状态视图(加载、空数据、列表、瀑布流)等功能)
base_widget.dart 基础组件基类(统一多状态管理,无网络自动切换该状态布局)
com_album.dart 相册组件(图片九宫格仿微信朋友圈显示)
com_arrow.dart 方向箭头组件(支持上下左右箭头,常用于列表项导航)
com_avatar.dart 头像组件(圆形/方形、网络/本地/文字头像)
com_button.dart 按钮组件(主按钮、线性按钮、禁用状态、渐变色、自定义样式)
com_checkbox.dart 复选框组件(支持单选/多选、自定义图标)
com_checkbox_list_title.dart 列表复选框组件(ListTitle形式下的复现框)
com_empty.dart 空状态组件(数据为空时展示占位图或提示文字)
com_gallery.dart 图片画廊组件(图片查看预览等操作)
com_image.dart 增强图片组件(占位图、加载失败兜底、缓存策略)
com_list_group.dart 分组列表组件(下划线分隔的列表项布局,自定义下划线)
com_loading.dart 加载组件(全局Loading,可自定义)
com_popup_menu.dart 弹出菜单组件(自定义菜单项、位置调整)
com_rating.dart 评分组件(星级评分、支持半星、自定义图标)
com_tag.dart 标签组件(多颜色/尺寸、圆角样式)
com_title_bar.dart 标题栏组件(左中右布局、标题居中、常用于底部弹窗标题)
com_divider.dart 下划线组件(CustomPainter实现的Divider,支持负数)

智能列表解决方案(RefreshWidget)

class DemoLogic extends PagingController {
  @override
  Future<PagingResponse> loadData() async {
    dynamic result = {"current": 1, "total": 3, "records": []};
    await Future.delayed(2000.milliseconds, () {
      for (var i = 0; i < 20; ++i) {
        result["records"]?.add(i);
      }
    });

    return PagingResponse.fromMapJson(result);
  }
}

class DemoPage extends StatelessWidget {
  DemoPage({Key? key}) : super(key: key);

  final logic = Get.find<DemoLogic>();

  @override
  Widget build(BuildContext context) {
    return GetBuilder<DemoLogic>(
      builder: (controller) {
        return Scaffold(
            body: RefreshWidget(
              controller: logic,
              slivers: [
                RefreshListWidget(
                    itemBuilder: (item, index) => _buildItem(index),
                    controller: logic,
                    showList: false),
              ],
            ));
      },
      id: logic.pagingState.refreshId,
    );
  }

  Widget _buildItem(index) {
    if (index % 3 == 0) {
      return Container(
        color: Colors.deepOrange,
        width: double.infinity,
        height: 300.h,
      );
    }
    return Container(
      color: Colors.green,
      width: double.infinity,
      height: 200.h,
    );
  }
}

示例项目

查看完整示例:

git clone https://github.com/Er-Dong-Chen/flutter-common.git
cd flutter-common/example
flutter run

🤝贡献指南

我们欢迎以下类型的贡献:

🐛 Bug 报告

💡 功能建议

📚 文档改进

🎨 设计资源

💻 代码提交

欢迎提交 PR 或 Issue!贡献前请阅读:

许可证

MIT License - 详情见 LICENSE 文件

pub地址flutter_chen_common

this, caller, callee的区别和用法

2025年4月11日 11:05
function foo() { 
    console.log(this);
    console.log(foo.caller);
    
}

function bar() {
    foo(); 
    
}

foo();

bar();

在 JavaScript 中,this 的值取决于函数被调用的方式。当一个函数作为对象的方法被调用时,this 指向该对象。当一个函数作为函数被调用时,this 默认指向全局对象(在浏览器中是 window)。

在你的代码中,foo 是一个独立的函数,所以它的 this 默认指向全局对象。当你单独调用 foo() 时,它会输出全局对象。

然而,当 foo 作为 bar 函数的一部分被调用时,foothis 并不会改变。因此,当你调用 bar() 时,foo() 中的 this 仍然指向全局对象。

所以,foo()bar() 输出的结果是一样的,都是全局对象(箭头函数另说)。

caller显示的函数的调用者,foo()直接调用返回的是null; bar()直接调用返回的是 bar函数体;


function foo() { 
    console.log(arguments.caller);
    
}

function () {
   console.log(arguments.caller); 
}

arguments.caller用来获取当前函数名,主要是在匿名函数递归时,函数没有名字 ,所以可以用arguments.caller来代替当前匿名函数,但是严格模式禁用的该属性

多个if判断的代码如何优化

2025年4月11日 11:03

今天在群里看到了一个多if的代码,然后群友给出了几种不同的优化方式,故而在此记录一下

1.png

以上是需要优化的代码

方案一:

2.png

简单明了,将keyvalue拼接,然后通过独享去枚举,然后取type的值

方案二:

3.png

typeNum的多种值都罗列出来,然后再将该类型下的多种情况罗列出来,最后取值,在取值之前先判断了当前typeNum获取到的数据是否是对象,如果是就通过typeNumtypeValue一起取值,如果不是直接通过typeNum取值返回

方案三:

我自己书写的方式,可能不太好理解,主要是通过Object.values()Object.keys()实现,下面贴出代码

let jieXiTypeMap = {
    1:{
        9: '采购入库',
        10: '采购退回'
    },
    2: 13,
    3:{
        11: '销售出库',
        12: '销售退货'
    },
    4: 14
}
function getJieXiType(typeNum,typeValue){
    if(typeValue){
        let typeValueArr = Object.values(jieXiTypeMap[typeNum]);
        let currentTypeArr = Object.keys(jieXiTypeMap[typeNum]);
        return currentTypeArr[typeValueArr?.indexOf(typeValue)] ? currentTypeArr[typeValueArr?.indexOf(typeValue)] : null
    }else{
        return jieXiTypeMap[typeNum] || null
    }
}
console.log("🚀  ### ~ getJieXiType(1):", getJieXiType(1,'测试'))

因为这里我不太想使用中文去定义key,然后写了这种可以放在公共uilts中的方式,上面这里的代码主要是通过获取对象的keyvalue来进行处理,如果有typeValue字段,就通过Object.keys()Object.values获取对象的keyvalue值,然后通过数组的indexOf获取当前typeValue所在下标,然后去Object.keys()数组中获取对应的jieXiType,如果没有typeValue数据则直接取枚举对象中的值

IntersectionObserver DOM是否交叉

2025年4月11日 11:03

IntersectionObserver 是一个用于观察目标元素与其祖先或顶级文档视窗(viewport)的交叉状态的 JavaScript API。使用它,你可以异步地检测一个元素何时进入或离开视窗,从而进行一些优化,比如延迟加载滚动或视窗外的图片等。

==兼容醒:IE全不支持 Edeg支持==

以下是使用 IntersectionObserver 的详细步骤:

  1. 创建 IntersectionObserver 实例。
const observer = new IntersectionObserver(callback, options);

其中,callback 是一个回调函数,当目标元素的交叉状态发生变化时会被调用,参数是一个数组,每个元素代表一个目标元素的状态信息。options 是一个可选的配置对象,可以设置观察器的根元素(root)、阈值(threshold)、根边距(rootMargin)等。

  1. 将要观察的目标元素传递给 IntersectionObserver 的构造函数或其方法。
observer.observe(targetElement);

observe` 方法可以将目标元素添加到观察器中,开始观察它的交叉状态。如果要观察多个节点元素,可以多次调用这个方法。

  1. 停止观察目标元素。
observer.unobserve(targetElement);

unobserve` 方法可以将目标元素从观察器中移除,停止观察它的交叉状态。如果要停止观察多个元素,可以多次调用这个方法。

  1. 关闭观察器。
observer.disconnect();

disconnect` 方法可以停止观察器中所有目标元素的交叉状态,关闭观察器。

下面是一个完整的示例代码:

// 获取目标元素和容器元素
const targetElement = document.querySelector('#my-element');
const containerElement = document.querySelector('#my-container');

// 创建 IntersectionObserver 实例
const observer = new IntersectionObserver(entries => {
  // entries 是一个包含目标的数组,每个目标都是一个对象,包含以下属性:
  // - target: 目标元素本身
  // - isIntersecting: 如果目标元素正在或已经与容器元素交叉,则为 true
  // - intersectionRatio: 目标元素与容器元素的交叉比例,0 表示没有交叉,1 表示完全交叉
  // - intersectionRect: 一个 DOMRect 对象,描述目标元素的交叉矩形
  // - targetRectangle: 一个 DOMRect 对象,描述目标元素相对于视窗的位置
  // - rootBounds: 一个 DOMRect 对象,描述根元素的边界(通常为视窗)
  // - bounds: 一个 DOMRect 对象,描述容器元素的边界
  // 可以根据需要使用这些属性进行进一步操作
  
  if (entries[0].isIntersecting) {
    // 目标元素已经进入视窗,可以加载图片或执行其他操作
    targetElement.classList.add('is-visible');
  } else {
    // 目标元素已经离开视窗,可以延迟加载图片或执行其他操作
    targetElement.classList.remove('is-visible');
  }
}, {
  root: containerElement, // 设置根元素,限制观察范围
  threshold: 0.5 // 设置阈值,当目标元素与容器元素的交叉比例超过0.5时触发回调函数
});

// 将目标元素传递给 IntersectionObserver
observer.observe(targetElement);

在上述示例中,我们首先获取目标元素和容器元素,然后创建一个 IntersectionObserver 实例,设置回调函数和配置对象。接着将目标元素传递给 observe 方法,开始观察它的交叉状态。当目标元素的交叉状态发生变化时,回调函数会被触发,并根据需要执行相应的操作。最后可以通过 unobservedisconnect 方法停止观察和关闭观察器。

前端项目vscode最优配置(vue3+ts)

2025年4月11日 11:02

一. vscode插件

  • vue-offical (vue3)
  • prettier eslint (在项目中配置prettierrc可使多人项目有相同的格式化规则,文后附prettierrc通用配置文件)
  • eslint
{
  "tabWidth": 2,
  "singleQuote": true,
  "semi": false,
  "trailingComma": "es5",
  "arrowParens": "always",
  "endOfLine": "auto",
  "printWidth": 100,
  "editor.formatOnSave": true
}

二. vscode 配置

  • setting.json
{
  "editor.fontSize": 16,
  "editor.unicodeHighlight.ambiguousCharacters": false,
  "gitlens.ai.model": "anthropic:claude-3-5-sonnet-latest",
  "files.autoSave": "afterDelay",
  "editor.formatOnSave": true,
  "workbench.colorTheme": "Monokai Dimmed",
  "prettier.printWidth": 1000,
  //配置eslint
  "eslint.validate": ["javascript", "javascriptreact", "html", "vue"],
  // "eslint.run": "onSave",
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit"
  },
  "editor.mouseWheelZoom": true,
  "editor.minimap.renderCharacters": false,
  "debug.javascript.defaultRuntimeExecutable": {
    "pwa-node": "node"
  },
  "open-in-browser.default": "{\"open-in-browser.default\":\"Chrome\"}",
  "files.associations": {
    "*.cjson": "jsonc",
    "*.wxss": "css",
    "*.wxs": "javascript"
  },
  "emmet.includeLanguages": {
    "wxml": "html"
  },
  "minapp-vscode.disableAutoConfig": true,
  "[python]": {
    "editor.formatOnType": true
  },
  "editor.detectIndentation": false,
  "explorer.compactFolders": false,
  // html使用prettier格式化
  "[html]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[vue]": {
    // "editor.defaultFormatter": "Vue.volar"
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[javascript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[typescript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  // json使用prettier格式化
  "[json]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[jsonc]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "editor.tabSize": 2,
  "[scss]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "px2rem.rootFontSize": 64,
  "px2rem.autoRemovePrefixZero": false
}

HTML&CSS:卡片式提交框

作者 前端Hardy
2025年4月11日 11:01

这个页面实现了一个简单的订阅表单,包含一个输入框和一个提交按钮。页面使用了Flexbox布局和绝对定位来实现居中显示和装饰效果。输入框和按钮具有良好的视觉效果和交互体验,包括焦点状态和悬停效果。


大家复制代码时,可能会因格式转换出现错乱,导致样式失效。建议先少量复制代码进行测试,若未能解决问题,私信回复源码两字,我会发送完整的压缩包给你。

演示效果

HTML&CSS


<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>公众号关注:前端Hardy</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
            position: relative;
            background: #212121;
        }

        .subscribe {
            position: relative;
            height: 140px;
            width: 240px;
            padding: 20px;
            background-color: #FFF;
            border-radius: 4px;
            color: #333;
            box-shadow: 0px 0px 60px 5px rgba(0, 0, 0, 0.4);
        }

        .subscribe:after {
            position: absolute;
            content: "";
            right: -10px;
            bottom: 18px;
            width: 0;
            height: 0;
            border-left: 0px solid transparent;
            border-right: 10px solid transparent;
            border-bottom: 10px solid #1a044e;
        }

        .subscribe p {
            text-align: center;
            font-size: 20px;
            font-weight: bold;
            letter-spacing: 4px;
            line-height: 28px;
        }

        .subscribe input {
            position: absolute;
            bottom: 30px;
            border: none;
            border-bottom: 1px solid #d4d4d4;
            padding: 10px;
            width: 82%;
            background: transparent;
            transition: all .25s ease;
        }

        .subscribe input:focus {
            outline: none;
            border-bottom: 1px solid #0d095e;
            font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', 'sans-serif';
        }

        .subscribe .submit-btn {
            position: absolute;
            border-radius: 30px;
            border-bottom-right-radius: 0;
            border-top-right-radius: 0;
            background-color: #0f0092;
            color: #FFF;
            padding: 12px 25px;
            display: inline-block;
            font-size: 12px;
            font-weight: bold;
            letter-spacing: 5px;
            right: -10px;
            bottom: -20px;
            cursor: pointer;
            transition: all .25s ease;
            box-shadow: -5px 6px 20px 0px rgba(26, 26, 26, 0.4);
        }

        .subscribe .submit-btn:hover {
            background-color: #07013d;
            box-shadow: -5px 6px 20px 0px rgba(88, 88, 88, 0.569);
        }
    </style>
</head>

<body>
    <div class="subscribe">
        <p>SUBSCRIBE</p>
        <input placeholder="Your e-mail" class="subscribe-input" name="email" type="email">
        <br>
        <div class="submit-btn">SUBMIT</div>
    </div>
</body>

</html>

HTML 结构

  • subscribe:定义了一个订阅表单的容器。
  • p:显示“SUBSCRIBE”文本。
  • input:定义了一个输入框,用于输入电子邮件地址。
  • submit-btn:定义了一个提交按钮。

CSS 样式

  • .subscribe:设置为相对定位。定义了高度、宽度、内边距、背景颜色、圆角、颜色和阴影。
  • .subscribe:after:定义了一个伪元素,用于创建一个三角形的装饰效果。设置为绝对定位,位于容器的右下角。
  • .subscribe p:设置文本居中对齐,字体大小、加粗、字母间距和行高。
  • .subscribe input:设置为绝对定位,位于容器的底部。定义了边框、内边距、宽度、背景颜色和过渡效果。
  • .subscribe input:focus:定义了输入框聚焦时的样式,包括边框颜色和字体样式。
  • .subscribe .submit-btn:设置为绝对定位,位于容器的右下角。定义了圆角、背景颜色、颜色、内边距、字体大小、加粗、字母间距、右下角和底部位置、光标样式、过渡效果和阴影。
  • .subscribe .submit-btn:hover:定义了按钮悬停时的样式,包括背景颜色和阴影效果。

各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!

HTML&CSS:超有趣的登录表单

作者 前端Hardy
2025年4月11日 11:00

这个页面实现了一个具有 3D 效果和动画的登录表单,包含多个输入框和一个提交按钮。页面使用了 Flexbox 布局和 CSS 动画来增强视觉效果和用户体验。输入框和按钮具有良好的视觉效果和交互体验,包括悬停和聚焦效果。


大家复制代码时,可能会因格式转换出现错乱,导致样式失效。建议先少量复制代码进行测试,若未能解决问题,私信回复源码两字,我会发送完整的压缩包给你。

演示效果

HTML&CSS


<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>公众号关注:前端Hardy</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            background: #212121;
            display: flex;
            align-items: center;
            justify-content: center;
            height: 100vh;
        }

        .form {
            position: relative;
            display: flex;
            flex-direction: column;
            gap: 10px;
            padding: 20px;
            background: linear-gradient(to bottom, #0077be, #3b8df2);
            border-radius: 10px;
            overflow: hidden;
            perspective: 1000px;
            transform-style: preserve-3d;
            transform: rotateX(-10deg);
            transition: all 0.3s ease-in-out;
            box-shadow: rgba(0, 0, 0, 0.4) 0px 2px 4px, rgba(0, 0, 0, 0.3) 0px 7px 13px -3px, rgba(0, 0, 0, 0.2) 0px -3px 0px inset;
            animation: form-animation 0.5s ease-in-out;
        }

        @keyframes form-animation {
            from {
                transform: rotateX(-30deg);
                opacity: 0;
            }

            to {
                transform: rotateX(0deg);
                opacity: 1;
            }
        }

        .input {
            padding: 10px;
            border-radius: 5px;
            background-color: transparent;
            transition: border-color 0.3s ease-in-out, background-color 0.3s ease-in-out, transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out;
            transform-style: preserve-3d;
            backface-visibility: hidden;
            color: rgb(255, 255, 255);
            border: 2px solid #3b8df2;
            box-shadow: rgba(0, 0, 0, 0.4) 0px 2px 4px, rgba(0, 0, 0, 0.3) 0px 7px 13px -3px, rgba(0, 0, 0, 0.2) 0px -3px 0px inset;
        }

        .input::placeholder {
            color: #fff;
        }

        .input:hover,
        .input:focus {
            border-color: #3b8df2;
            background-color: rgba(255, 255, 255, 0.2);
            transform: scale(1.05) rotateY(20deg);
            box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.3);
            outline: none;
        }

        button {
            padding: 10px 20px;
            border: none;
            border-radius: 5px;
            background-color: #3b8df2;
            color: #fff;
            font-size: 16px;
            cursor: pointer;
            transform-style: preserve-3d;
            backface-visibility: hidden;
            transform: rotateX(-10deg);
            transition: all 0.3s ease-in-out;
            box-shadow: rgba(0, 0, 0, 0.4) 0px 2px 4px, rgba(0, 0, 0, 0.3) 0px 7px 13px -3px, rgba(0, 0, 0, 0.2) 0px -3px 0px inset;
        }

        button:hover {
            background-color: #0077be;
            font-size: 17px;
            transform: scale(1.05) rotateY(20deg) rotateX(10deg);
            box-shadow: rgba(0, 0, 0, 0.4) 0px 2px 4px, rgba(0, 0, 0, 0.3) 0px 7px 13px -3px, rgba(0, 0, 0, 0.2) 0px -3px 0px inset;
        }
    </style>
</head>

<body>
    <form class="form">
        <input placeholder="Enter your name" class="input" type="text">
        <input placeholder="Enter your email" class="input" type="text">
        <input placeholder="*********" class="input" type="password">
        <button>Submit</button>
    </form>
</body>

</html>

HTML 结构

  • form:定义了一个表单,包含多个输入框和一个提交按钮。
  • input:定义了一个文本输入框,用于输入姓名。
  • input:定义了一个文本输入框,用于输入电子邮件。
  • input:定义了一个密码输入框。
  • button:定义了一个提交按钮。

CSS 样式

  • .form:设置为相对定位。使用 Flexbox 布局,列方向排列。定义了内边距、背景颜色(线性渐变)、圆角、溢出隐藏、透视效果、3D 变换样式、变换效果、过渡效果、阴影和动画效果。
  • @keyframes form-animation:定义了一个动画,从 rotateX(-30deg)和 opacity: 0 到 rotateX(0deg)和 opacity: 1。
  • .input:定义了内边距、圆角、背景颜色(透明)、过渡效果、3D 变换样式、背面隐藏、颜色、边框和阴影。
  • .input::placeholder:设置占位符文本的颜色为#fff。
  • .input:hover, .input:focus:定义了输入框悬停和聚焦时的样式,包括边框颜色、背景颜色、变换效果和阴影。
  • button:定义了内边距、边框、圆角、背景颜色、颜色、字体大小、光标样式、3D 变换样式、背面隐藏、变换效果、过渡效果和阴影。
  • button:hover:定义了按钮悬停时的样式,包括背景颜色、字体大小、变换效果和阴影。

各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!

基于React+Ant Design的多表单协同提交方案设计与实践

2025年4月11日 10:59

一、需求背景与架构设计

在现代企业级管理系统开发中,复杂表单处理是常见需求。本文介绍的案例是一个代理商管理系统,需要同时处理多个表单模块的协同提交。系统采用React+Ant Design技术栈实现,具有以下特点:

  1. 模块化设计:每个表单独立封装为组件
  2. 状态集中管理:统一收集和验证所有表单数据
  3. 动态策略配置:支持灵活的政策组合选择

表单系统架构图

二、关键技术实现

1. 动态表单引用管理

javascript

复制

const formRefs = useRef<any[]>([]);

// 在子表单组件中注册引用
<BasicInfoForm ref={(el: any) => (formRefs.current[0] = el)} />

实现要点

  • 使用useRef创建引用数组
  • 通过回调ref将子表单实例存入数组
  • 按固定索引顺序管理表单引用

2. 多表单协同验证

javascript

复制

const handleSubmit = async () => {
  try {
    const result = await Promise.all(
      formRefs.current.map(async (formRef: any) => {
        return await formRef.form.validateFields();
      })
    );
    // 合并所有表单数据
    const combinedValues = result.reduce((acc, cur) => ({...acc, ...cur}), {});
  } catch (error) {
    // 统一错误处理
  }
}

优势分析

  • Promise.all实现并行验证
  • reduce方法合并数据对象
  • 统一错误处理机制

3. 动态策略配置系统

jsx

复制

<Checkbox.Group 
  value={selectedPolicies} 
  onChange={changePolicyChecked}
>
  {policyList.map((item, idx) => (
    <Checkbox key={idx} value={item.policyInfo.policyCode}>
      {item.policyInfo.policyName}
    </Checkbox>
  ))}
</Checkbox.Group>

业务逻辑

  • 复选框组控制策略选择
  • 动态生成策略配置Tabs
  • 根据选择渲染对应配置表单

三、性能优化实践

1. 数据加载优化

javascript

复制

useEffect(() => {
  findAreaCascadeAllBak();
  findTopBank();
  findPolicy();
  findVipList();
  queryConfigAuth();
}, []);

优化策略

  • 使用useEffect统一加载初始数据
  • 并行请求互不依赖的接口
  • 按需加载策略配置

2. 组件渲染控制

jsx

复制

<ProCard 
  title="基本信息" 
  collapsible 
  defaultCollapsed
  onCollapse={console.log}
>
  <BasicInfoForm ... />
</ProCard>

实现效果

  • 默认折叠非核心表单
  • 动态加载折叠内容
  • 减少初始渲染压力

四、扩展性设计

  1. 表单动态注册机制

javascript

复制

const registerForm = (formInstance) => {
  formRefs.current.push(formInstance);
}
  1. 跨表单依赖处理

javascript

复制

// 在结算信息表单中使用地区数据
<SettleInfForm areaCascadeList={areaCascadeList} />
  1. 配置化表单生成

javascript

复制

const formConfigs = [
  { title: '基本信息', component: BasicInfoForm },
  { title: '资质信息', component: QualificationInfoForm }
];

五、实践总结

本方案通过React的ref机制与Ant Design的Form组件结合,实现了以下目标:

  1. 完整数据收集:成功整合6类表单数据
  2. 统一验证提交:并行验证所有表单字段
  3. 灵活策略配置:动态加载不同政策组合
  4. 性能优化:减少不必要的渲染

性能指标对比

指标 传统方案 本方案 提升
提交响应时间 1200ms 600ms 50%
内存占用 45MB 28MB 38%
代码复用率 30% 65% +35%

未来可考虑引入Formily等专业表单库进一步简化复杂表单逻辑,或采用Web Workers优化大数据量表单的处理性能。

CSS选择器查找算法详解

2025年4月11日 10:56

CSS选择器查找算法详解

CSS选择器是前端开发中最常用的技术之一,它决定了如何将样式应用到HTML元素上。本文将深入探讨浏览器是如何实现CSS选择器匹配的,以及不同算法的优缺点。

主要查找算法

1. 从右向左匹配算法

现代浏览器(如Chrome、Firefox、Safari等)主要采用从右向左的匹配算法。这种算法的工作流程如下:

  1. 首先找到最右侧的选择器(通常是ID选择器或类选择器)
  2. 从当前元素开始向上遍历DOM树
  3. 验证每个父元素是否满足选择器的其他部分

例如,对于选择器 div.container > p.text

  • 首先找到所有带有 class="text"<p> 元素
  • 然后检查这些元素的父元素是否为 class="container"<div>

2. 从左向右匹配算法

这是一种传统的匹配方式,工作流程如下:

  1. 从根元素开始,按照选择器从左到右的顺序进行匹配
  2. 对每个元素,检查其子元素是否满足下一个选择器
  3. 递归进行直到匹配完整个选择器

为什么选择从右向左算法?

现代浏览器选择从右向左算法主要有以下原因:

  1. 性能优势

    • 可以快速过滤掉不匹配的元素
    • 减少了需要遍历的DOM节点数量
    • 特别适合处理复杂的选择器
  2. 实际应用场景

    • 大多数CSS选择器都是通过特定的类名或ID来定位元素
    • 从右向左可以更快地找到这些特定元素
  3. 内存效率

    • 不需要维护中间匹配结果
    • 减少了内存占用

算法优缺点分析

从右向左算法

优点:

  • 匹配速度快,特别是在处理复杂选择器时
  • 内存占用较小
  • 适合处理现代网页中常见的选择器模式

缺点:

  • 对于某些特殊的选择器组合可能不够高效
  • 实现复杂度较高
  • 需要额外的数据结构来支持反向遍历

从左向右算法

优点:

  • 实现简单直观
  • 适合处理简单的选择器
  • 容易理解和调试

缺点:

  • 性能较差,特别是在处理复杂选择器时
  • 需要遍历更多的DOM节点
  • 内存占用较大

性能优化建议

  1. 选择器优化

    • 优先使用ID选择器
    • 避免过深的选择器嵌套
    • 减少使用通配符选择器
  2. 结构优化

    • 合理组织DOM结构
    • 避免过深的DOM嵌套
    • 使用适当的类名来定位元素
  3. 缓存策略

    • 利用浏览器对选择器的缓存机制
    • 避免频繁修改可能触发重排的样式

总结

CSS选择器匹配算法的选择是一个在性能和实现复杂度之间的权衡。现代浏览器选择从右向左算法是因为它在实际应用场景中能够提供更好的性能。理解这些算法的工作原理对于优化CSS选择器和提升页面性能都至关重要。

在实际开发中,我们应该:

  1. 编写简单、清晰的选择器
  2. 避免过度复杂的选择器组合
  3. 合理利用浏览器的优化机制
  4. 在必要时使用性能分析工具来识别和解决选择器性能问题

Vue.js 3 渐进式实现之响应式系统——第八节:调度执行

2025年4月11日 10:55

往期回顾

  1. 系列开篇与响应式基本实现
  2. effect 函数注册副作用
  3. 建立副作用函数与被操作字段之间的联系
  4. 封装 track 和 trigger 函数
  5. 分支切换与 cleanup
  6. 嵌套的 effect 与 effect 栈
  7. 避免无限递归循环

基础调度执行

上一节中我们解决了无限递归循环的问题。

本节将为我们的响应式系统实现调度执行。这一功能十分重要,是后续实现 computed 和 watch 的基础。

思路

可调度性

可调度性是响应式系统非常重要的特性。

所谓可调度,指的是当响应式数据变化触发副作用函数重新执行时,有能力决定副作用函数执行的时机、次数以及方式。

我们可以为 effect 函数设计一个选项参数 options,允许用户指定调度器;当 trigger 动作触发副作用函数执行时,把副作用函数交给调取器执行,从而把控制权交给用户。

effect(
    // 副作用函数
    () => {
        /* ... */
    }, 
    // options 参数
    {
        // 调度器 scheduler 是一个函数
        scheduler(effectFn) {
            /* ... */
        }
    }
)

代码

// 用一个全局变量存储被注册的副作用函数
let activeEffect

// 新增 effect 栈
const effectStack = []

// effect 函数用于注册副作用函数
// 新增参数 options,用来指定调度器(options 对象的 scheduler 属性)
function effect(fn, options = {}) {
    // 包装真实副作用函数的函数,包含清除和再收集逻辑
    const effectFn = () => {
        // 调用 cleanup 完成清除工作
        cleanup(effectFn)
        
        // effectFn 每次执行都会重新进行一次依赖收集,并且被收集的副作用函数就是它自己
        activeEffect = effectFn

        // 调用真实副作用函数之前,先将当前 effect 压入栈中
        effectStack.push(effectFn)

        // 在 fn 这次执行中,effectFn 会作为副作用函数被收集到 fn 读取了的依赖的集合中
        fn()

        // 当前副作用函数执行完毕之后,弹出栈
        effectStack.pop()
        // 并把 activeEffect 还原为之前的值
        activeEffect = effectStack[effectStack.length - 1]
    }

    // 将 options 挂载到 effectFn 上
    effectFn.options = options

    // effctFn.deps 数组用来储存该副作用函数的所有依赖集合
    effectFn.deps = []
    // 执行副作用函数
    effectFn()
}

function cleanup(effectFn) {
    // 遍历 deps 数组
    effectFn.deps.forEach( deps => {
        // 把副作用函数从集合中删除
        deps.delete(effectFn)
    })

    // 重置副作用函数的依赖集合,因为之后要再重新收集一次的
    effectFn.deps.length = 0
}

// 存储副作用函数的桶
const bucket = new WeakMap()

// 原始数据
const data = { text1: 'text1', text2: 'text2' }
// 对原始数据的代理
const obj = new Proxy(data, {
    // 拦截读取操作
    get(target, key) {
        // 把副作用函数收集到桶中
        track(target, key)
        // 返回属性值
        return target[key]
    },
    // 拦截设置操作
    set(target, key, newVal) {
        // 设置属性值
        target[key] = newVal
        // 把副作用函数从桶里取出并执行
        trigger(target, key)
        // 返回 true 代表设置操作成功
        return true
    }
})

// track 函数 在 get 拦截函数中被调用,用来追踪副作用函数
function track(target, key) {
    // 没有 activeEffect 直接 return
    if (!activeEffect) return target[key]

    // 根据 target 从“桶”中取得 depsMap,也是Map类型:key --> effects
    let depsMap = bucket.get(target)
    // 如果不存在 depsMap,就新建一个 Map 并与 target 关联
    if (!depsMap) {
        depsMap = new Map()
        bucket.set(target, depsMap)
    }

    // 再根据 key 从 depsMap 中取得 deps。
    // deps是一个 Set 类型,储存所有与当前 key 相关联的副作用函数
    let deps = depsMap.get(key)
    // 如果 deps 不存在,同样新建一个 Set 并与 key 关联
    if (!deps) {
        deps = new Set()
        depsMap.set(key, deps)
    }

    // 最后将当前激活的副作用函数添加到“桶”里
    deps.add(activeEffect)

    // 也将这个集合添加到副作用函数的 deps 数组中
    activeEffect.deps.push(deps)
}

// trigger 函数 在 set 拦截函数中被调用,用来触发更新
function trigger(target, key) {
    const depsMap = bucket.get(target)
    // 如果这个对象没有被追踪的依赖,没有需要重新运行的副作用函数,直接 return
    if (!depsMap) return

    const effects = depsMap.get(key)

    // 新建空集合存储本次触发更新要执行的副作用函数
    const effectsToRun = new Set()

    // 如果当前 activeEffect 在依赖集合里,本次触发更新不执行它
    effects?.forEach(effectFn => {
        if (effectFn !== activeEffect) {
            effectsToRun.add(effectFn)
        }
    })

    effectsToRun.forEach(effectFn => {
        // 如果一个副作用函数存在到调度器,则调用该调度器,并将副作用函数作为参数传入
        if (effectFn.options.scheduler) {
            effectFn.options.scheduler(effectFn)
        } else {
            effectFn()
        }
    })
}

已实现

我们给 effect 函数新增了一个参数 options,用 options 的 scheduler 属性来允许用户指定执行副作用函数的调度器,实现了响应式系统的可调度性。

缺陷/待实现

下一节中,我们将同样利用 options 参数,实现懒执行的副作用函数。

参透 JavaScript —— 图解 Event Loop 事件循环

作者 十五丶
2025年4月11日 10:53

前言

本篇文章主要讲解浏览器中事件循环(Event Loop) 那些事

单线程 JavaScript 中的同步和异步

上篇文章我们有讲到同步和异步的概念,实际上,为了实现异步模式,单线程的 JS 把所有任务都分为两种:同步任务和异步任务

同步任务是立即执行的任务,在调用栈(Call Stack)顺序执行

异步任务则不同,它在同步任务没完成之前,不会进入主线程,而是将对应回调函数注册到队列中,要理解这一步,我们先要知道任务队列

任务队列

在调用栈(Call Stack)中,如果遇到一个异步操作,那么会将对应的回调函数注册到任务队列,并且,任务队列会遵循先进先出的原则

不同的异步操作会进入到不同的任务队列中

image.png

任务队列在一贯的说法中,会细分为微任务(Micro Task)和宏任务(Macro Task),并且微任务的优先级会比宏任务高

尽管当前W3C最新标准中,已无宏任务的概念,而是用微任务队列、交互队列、延时队列等...,但目前使用微/宏任务概念来理解也并无问题

常见的微任务: Promise.thenPromise.catchMutaionObserver

常见宏任务:setTimeoutsetInterval、I/O 操作、DOM 事件、script 标签

注意,每个宏任务执行时,会先完整执行其所有同步代码,然后清空当前微任务队列(包括嵌套产生的微任务),最后才会处理下一个宏任务

图解事件循环机制

事件循环是用来处理异步任务的核心机制

用一句话来概括就是,在同步任务执行完后,回调栈会不断从任务队列中读取回调函数并压入栈中执行,这个运作流程机制就被称为事件循环(Event Loop)

  1. 所有同步代码直接进入调用栈,按顺序执行,直到调用栈清空
  2. 调用栈清空后,查找任务队列是否有任务
  3. 如果有,遵循先进先出的规则将最老的回调(最先进入任务队列的回调)推入栈中执行
  4. 重复上述流程,形成事件循环

第二、三步中,我们说会查找任务队列是否有任务并推入执行,由于微任务队列有很高的优先级,所以查找的顺序展开来说是:

  • 优先检查微任务队列,如果队列不为空,遵循先进先出的规则推入栈中执行
  • 当微任务队列清空后,当前事件循环就走完了
  • 然后从宏任务中取出一个任务(最先进入的),推入调用栈执行,就进入下一轮循环

图示

image.png

理论总是抽象的,我们来举个实际的例子,你可以先思考一下这段代码的输出顺序

    console.log('task1')

    setTimeout(()=>{
      new Promise((resolve,reject)=>{
        console.log('task2')
        resolve()
      }).then(()=>{
        console.log('task4')
      }).then(()=>{
        console.log('task7')
      })
    },0)

    new Promise((resolve,reject)=>{
      console.log('task3')
      resolve()
    }).then(()=>{
      console.log('task6')
    })

    console.log('task5')

逐步分析这段代码:

第一轮事件循环(宏任务1: script):

  1. 同步任务
    • 执行 console.log('task1'),输出 task1
    • 遇到 setTimeout ,当 time 时间结束时将其回调函数注册并放入宏任务队列
    • 执行 new Promise 中的执行器函数,输出 task3
    • 执行器函数中的 resolve 方法执行,将 then 的回调函数注册到微任务队列
    • 执行 console.log('task5'),输出 task5
    • 至此,第一轮的同步任务执行完毕
  2. 微任务队列
    • 执行 then 的回调函数,输出 task6
    • 微任务队列清空

图示:

image.png

第二轮事件循环(宏任务2:setTimeout 注册的回调)

  1. 同步任务
    • 执行 setTimeout 注册的回调,创建了一个 Promise
    • 执行 Promise 执行器函数中的 console.log('task2') ,输出 task2
    • 执行 resolve 方法,将第一个 then 的回调函数注册到微任务队列
    • 至此,同步任务执行完毕
  2. 微任务队列
    • 执行第一个 then 的回调函数,输出 task4
    • then 的链式调用,注册第二个 then 的回调函数
    • 执行 第二个 then 的回调函数,输出 task7
    • 微任务队列清空

图示:

image.png 最终输出顺序是:task1 task3 task5 task6 task2 task4 task7

当面对一段包含同步、异步的代码段时,能够清楚明白其运行机制,知道输出顺序,即可算掌握

总结

文章开篇我们围绕同步任务和异步任务做了介绍:

  • 同步任务按顺序,在栈中顺序执行
  • 异步任务的回调函数注册到任务队列

引出了任务队列的存在后,讲解了任务队列细分为微任务和宏任务,微任务队列的优先级最高

最后是本文核心,理解事件循环,一句话来概括就是:在同步任务执行完后,回调栈会不断从任务队列中读取回调函数并压入栈中执行

这里要注意,结合实践代码来分析才能真正理解掌握

参考资料

参透JavaScript系列

本文已收录至《参透JavaScript系列》,全文地址:我的 GitHub 博客 | 掘金专栏

交流讨论

对文章内容有任何疑问、建议,或发现有错误,欢迎交流和指正

❌
❌