2024年7月

作者:vivo 互联网服务器团队-  Wang Zhi

Caffeine 作为一个高性能的缓存框架而被大量使用。本文基于Caffeine已有的基础进行定制化开发实现可视化功能。

一、背景

Caffeine缓存是一个高性能、可扩展、内存优化的 Java 缓存库,基于 Google 的 Guava Cache演进而来并提供了接近最佳的命中率。

Caffeine 缓存包含以下
特点

  1. 高效快速
    :Caffeine 缓存使用近似算法和并发哈希表等优化技术,使得缓存的访问速度非常快。

  2. 内存友好
    :Caffeine 缓存使用一种内存优化策略,能够根据需要动态调整缓存的大小,有效地利用内存资源。

  3. 多种缓存策略
    :Caffeine 缓存支持多种缓存策略,如基于容量、时间、权重、手动移除、定时刷新等,并提供了丰富的配置选项,能够适应不同的应用场景和需求。

  4. 支持异步加载和刷新
    :Caffeine 缓存支持异步加载和刷新缓存项,可以与 Spring 等框架无缝集成。

  5. 清理策略
    :Caffeine 使用 Window TinyLFU 清理策略,它提供了接近最佳的命中率。

  6. 支持自动加载和自动过期
    :Caffeine 缓存可以根据配置自动加载和过期缓存项,无需手动干预。

  7. 统计功能
    :Caffeine 缓存提供了丰富的统计功能,如缓存命中率、缓存项数量等,方便评估缓存的性能和效果。

正是因为Caffeine具备的上述特性,Caffeine作为项目中本地缓存的不二选择,越来越多的项目集成了Caffeine的功能,进而衍生了一系列的业务视角的需求。

日常使用的需求之一希望能够实时评估Caffeine实例的内存占用情况并能够提供动态调整缓存参数的能力,但是已有的内存分析工具MAT需要基于dump的文件进行分析无法做到实时,这也是整个事情的起因之一。

二、业务的技术视角

  • 能够对项目中的Caffeine的缓存实例能够做到近实时统计,实时查看缓存的实例个数。

  • 能够对Caffeine的每个实例的缓存配置参数、内存占用、缓存命中率做到实时查看,同时能够支持单个实例的缓存过期时间,缓存条目等参数进行动态配置下发。

  • 能够对Caffeine的每个实例的缓存数据做到实时查看,并且能够支持缓存数据的立即失效等功能。

基于上述的需求背景,结合caffeine的已有功能和定制的部分源码开发,整体作为caffeine可视化的技术项目进行推进和落地。

三、可视化能力

Caffeine可视化项目目前
已支持功能
包括:

  • 项目维度的全局缓存实例的管控。

  • 单缓存实例配置信息可视化、内存占用可视化、命中率可视化。

  • 单缓存实例的数据查询、配置动态变更、缓存数据失效等功能。

3.1 缓存实例的全局管控

图片

说明:

  • 以应用维度+机器维度展示该应用下包含的缓存实例对象,每个实例包含缓存设置中的大小、过期策略、过期时间、内存占用、缓存命中率等信息。

  • 单实例维度的内存占用和缓存命中率支持以趋势图进行展示。

  • 单实例维度支持配置变更操作和缓存查询操作。

3.2 内存占用趋势

图片

说明:

  • 内存占用趋势记录该缓存实例对象近一段时间内存占用的趋势变化。

  • 时间周期目前支持展示近两天的数据。

3.3 命中率趋势

图片

说明:

  • 命中率趋势记录该缓存实例对象近一段时间缓存命中的变化情况。

  • 时间周期目前支持展示近两天的数据。

3.4 配置变更

图片

说明:

  • 配置变更目前支持缓存大小和过期时间的动态设置。

  • 目前暂时支持单实例的设置,后续会支持全量生效功能。

3.5 缓存查询

图片

说明:

  • 单实例维度支持缓存数据的查询。

  • 目前支持常见的缓存Key类型包括String类型、Long类型、Int类型。

四、原理实现

4.1 整体设计框架

  • Caffeine框架功能整合

图片

说明:

  • 沿用Caffeine的基础功能包括Caffeine的缓存功能和Caffeine统计功能。

  • 新增
    Caffeine内存占用预估功能,该功能主要是预估缓存实例对象占用的内存情况。

  • 新增
    Caffeine实例命名功能,该功能是针对每个实例对象提供命名功能,是全局管控的基础。

  • 新增
    Caffeine实例全局管控功能,该功能主要维护项目运行中所有的缓存实例。

Caffeine可视化框架

图片

说明:

  • 【项目工程侧】:Caffeine的可视化框架基于Caffeine框架功能整合的基础上增加通信层进行数据数据上报和配置的下发。

  • 【管控平台侧】:负责缓存数据上报的接收展示,配置变更命令的下发。

  • 【通信层支持push和pull两种模式】,push模式主要用于统计数据的实时上报,pull模式主要用于配置下发和缓存数据查询。

4.2 源码实现

业务层-缓存对象的管理

static Cache<String, List<String>> accountWhiteCache = Caffeine.newBuilder()
            .expireAfterWrite(VivoConfigManager.getInteger("trade.account.white.list.cache.ttl", 10), TimeUnit.MINUTES)
            .recordStats().maximumSize(VivoConfigManager.getInteger("trade.account.white.list.cache.size", 100)).build();
常规的Caffeine实例的创建方式
 
 
static Cache<String, List<String>> accountWhiteCache = Caffeine.newBuilder().applyName("accountWhiteCache")
            .expireAfterWrite(VivoConfigManager.getInteger("trade.account.white.list.cache.ttl", 10), TimeUnit.MINUTES)
            .recordStats().maximumSize(VivoConfigManager.getInteger("trade.account.white.list.cache.size", 100)).build();
支持实例命名的Caffeine实例的创建方式

说明:

  • 在Caffeine实例创建的基础上增加了缓存实例的命名功能,通过.applyName("accountWhiteCache")来定义缓存实例的命名。

public final class Caffeine<K, V> {
 
  /**
   * caffeine的实例名称
   */
  String instanceName;
 
  /**
   * caffeine的实例维护的Map信息
   */
  static Map<String, Cache> cacheInstanceMap = new ConcurrentHashMap<>();
 
  @NonNull
  public <K1 extends K, V1 extends V> Cache<K1, V1> build() {
    requireWeightWithWeigher();
    requireNonLoadingCache();
 
    @SuppressWarnings("unchecked")
    Caffeine<K1, V1> self = (Caffeine<K1, V1>) this;
    Cache localCache =  isBounded() ? new BoundedLocalCache.BoundedLocalManualCache<>(self) : new UnboundedLocalCache.UnboundedLocalManualCache<>(self);
 
    if (null != localCache && StringUtils.isNotEmpty(localCache.getInstanceName())) {
      cacheInstanceMap.put(localCache.getInstanceName(), localCache);
    }
 
    return localCache;
  }
}

说明:

  • 每个Caffeine都有一个实例名称instanceName。

  • 全局通过cacheInstanceMap来维护Caffeine实例对象的名称和实例的映射关系。

  • 通过维护映射关系能够通过实例的名称查询到缓存实例对象并对缓存实例对象进行各类的操作。

  • Caffeine实例的命名功能是其他功能整合的基石。

业务层-内存占用的预估

import jdk.nashorn.internal.ir.debug.ObjectSizeCalculator;
 
public abstract class BoundedLocalCache<K, V> extends BLCHeader.DrainStatusRef<K, V>
    implements LocalCache<K, V> {
 
  final ConcurrentHashMap<Object, Node<K, V>> data;
 
  @Override
  public long getMemoryUsed() {
    // 预估内存占用
    return ObjectSizeCalculator.getObjectSize(data);
  }
}

说明:

  • 通过ObjectSizeCalculator.getObjectSize预估内存的缓存值。

  • data值是Caffeine实例用来保存真实数据的对象。

业务层-数据上报机制

public static StatsData getCacheStats(String instanceName) {
 
    Cache cache = Caffeine.getCacheByInstanceName(instanceName);
 
    CacheStats cacheStats = cache.stats();
    StatsData statsData = new StatsData();
 
    statsData.setInstanceName(instanceName);
    statsData.setTimeStamp(System.currentTimeMillis()/1000);
    statsData.setMemoryUsed(String.valueOf(cache.getMemoryUsed()));
    statsData.setEstimatedSize(String.valueOf(cache.estimatedSize()));
    statsData.setRequestCount(String.valueOf(cacheStats.requestCount()));
    statsData.setHitCount(String.valueOf(cacheStats.hitCount()));
    statsData.setHitRate(String.valueOf(cacheStats.hitRate()));
    statsData.setMissCount(String.valueOf(cacheStats.missCount()));
    statsData.setMissRate(String.valueOf(cacheStats.missRate()));
    statsData.setLoadCount(String.valueOf(cacheStats.loadCount()));
    statsData.setLoadSuccessCount(String.valueOf(cacheStats.loadSuccessCount()));
    statsData.setLoadFailureCount(String.valueOf(cacheStats.loadFailureCount()));
    statsData.setLoadFailureRate(String.valueOf(cacheStats.loadFailureRate()));
 
    Optional<Eviction> optionalEviction = cache.policy().eviction();
    optionalEviction.ifPresent(eviction -> statsData.setMaximumSize(String.valueOf(eviction.getMaximum())));
 
    Optional<Expiration> optionalExpiration = cache.policy().expireAfterWrite();
    optionalExpiration.ifPresent(expiration -> statsData.setExpireAfterWrite(String.valueOf(expiration.getExpiresAfter(TimeUnit.SECONDS))));
 
    optionalExpiration = cache.policy().expireAfterAccess();
    optionalExpiration.ifPresent(expiration -> statsData.setExpireAfterAccess(String.valueOf(expiration.getExpiresAfter(TimeUnit.SECONDS))));
 
    optionalExpiration = cache.policy().refreshAfterWrite();
    optionalExpiration.ifPresent(expiration -> statsData.setRefreshAfterWrite(String.valueOf(expiration.getExpiresAfter(TimeUnit.SECONDS))));
 
    return statsData;
}

说明:

  • 通过Caffeine自带的统计接口来统计相关数值。

  • 统计数据实例维度进行统计。

public static void sendReportData() {
 
    try {
        if (!VivoConfigManager.getBoolean("memory.caffeine.data.report.switch", true)) {
            return;
        }
 
        // 1、获取所有的cache实例对象
        Method listCacheInstanceMethod = HANDLER_MANAGER_CLASS.getMethod("listCacheInstance", null);
        List<String> instanceNames = (List)listCacheInstanceMethod.invoke(null, null);
        if (CollectionUtils.isEmpty(instanceNames)) {
            return;
        }
 
        String appName = System.getProperty("app.name");
        String localIp = getLocalIp();
        String localPort = String.valueOf(NetPortUtils.getWorkPort());
        ReportData reportData = new ReportData();
        InstanceData instanceData = new InstanceData();
        instanceData.setAppName(appName);
        instanceData.setIp(localIp);
        instanceData.setPort(localPort);
 
        // 2、遍历cache实例对象获取缓存监控数据
        Method getCacheStatsMethod = HANDLER_MANAGER_CLASS.getMethod("getCacheStats", String.class);
        Map<String, StatsData> statsDataMap = new HashMap<>();
        instanceNames.stream().forEach(instanceName -> {
 
            try {
                StatsData statsData = (StatsData)getCacheStatsMethod.invoke(null, instanceName);
 
                statsDataMap.put(instanceName, statsData);
            } catch (Exception e) {
 
            }
        });
 
        // 3、构建上报对象
        reportData.setInstanceData(instanceData);
        reportData.setStatsDataMap(statsDataMap);
 
        // 4、发送Http的POST请求
        HttpPost httpPost = new HttpPost(getReportDataUrl());
        httpPost.setConfig(requestConfig);
 
        StringEntity stringEntity = new StringEntity(JSON.toJSONString(reportData));
        stringEntity.setContentType("application/json");
        httpPost.setEntity(stringEntity);
 
        HttpResponse response = httpClient.execute(httpPost);
        String result = EntityUtils.toString(response.getEntity(),"UTF-8");
        EntityUtils.consume(response.getEntity());
 
        logger.info("Caffeine 数据上报成功 URL {} 参数 {} 结果 {}", getReportDataUrl(), JSON.toJSONString(reportData), result);
    } catch (Throwable throwable) {
        logger.error("Caffeine 数据上报失败 URL {} ", getReportDataUrl(), throwable);
    }
}

说明:

  • 通过获取项目中运行的所有Caffeine实例并依次遍历收集统计数据。

  • 通过http协议负责上报对应的统计数据,采用固定间隔周期进行上报。

业务层-配置动态下发

public static ExecutionResponse dispose(ExecutionRequest request) {
    ExecutionResponse executionResponse = new ExecutionResponse();
    executionResponse.setCmdType(CmdTypeEnum.INSTANCE_CONFIGURE.getCmd());
    executionResponse.setInstanceName(request.getInstanceName());
 
    String instanceName = request.getInstanceName();
    Cache cache = Caffeine.getCacheByInstanceName(instanceName);
 
    // 设置缓存的最大条目
    if (null != request.getMaximumSize() && request.getMaximumSize() > 0) {
        Optional<Eviction> optionalEviction = cache.policy().eviction();
        optionalEviction.ifPresent(eviction ->eviction.setMaximum(request.getMaximumSize()));
    }
 
    // 设置写后过期的过期时间
    if (null != request.getExpireAfterWrite() && request.getExpireAfterWrite() > 0) {
        Optional<Expiration> optionalExpiration = cache.policy().expireAfterWrite();
        optionalExpiration.ifPresent(expiration -> expiration.setExpiresAfter(request.getExpireAfterWrite(), TimeUnit.SECONDS));
    }
 
    // 设置访问过期的过期时间
    if (null != request.getExpireAfterAccess() && request.getExpireAfterAccess() > 0) {
        Optional<Expiration> optionalExpiration = cache.policy().expireAfterAccess();
        optionalExpiration.ifPresent(expiration -> expiration.setExpiresAfter(request.getExpireAfterAccess(), TimeUnit.SECONDS));
    }
 
    // 设置写更新的过期时间
    if (null != request.getRefreshAfterWrite() && request.getRefreshAfterWrite() > 0) {
 
        Optional<Expiration> optionalExpiration = cache.policy().refreshAfterWrite();
        optionalExpiration.ifPresent(expiration -> expiration.setExpiresAfter(request.getRefreshAfterWrite(), TimeUnit.SECONDS));
    }
 
    executionResponse.setCode(0);
    executionResponse.setMsg("success");
 
    return executionResponse;
}

说明:

  • 通过Caffeine自带接口进行缓存配置的相关设置。

业务层-缓存数据清空

/**
     * 失效缓存的值
     * @param request
     * @return
     */
    public static ExecutionResponse invalidate(ExecutionRequest request) {
 
        ExecutionResponse executionResponse = new ExecutionResponse();
        executionResponse.setCmdType(CmdTypeEnum.INSTANCE_INVALIDATE.getCmd());
        executionResponse.setInstanceName(request.getInstanceName());
 
        try {
            // 查找对应的cache实例
            String instanceName = request.getInstanceName();
            Cache cache = Caffeine.getCacheByInstanceName(instanceName);
 
            // 处理清空指定实例的所有缓存 或 指定实例的key对应的缓存
            Object cacheKeyObj = request.getCacheKey();
 
            // 清除所有缓存
            if (Objects.isNull(cacheKeyObj)) {
                cache.invalidateAll();
            } else {
                // 清除指定key对应的缓存
                if (Objects.equals(request.getCacheKeyType(), 2)) {
                    cache.invalidate(Long.valueOf(request.getCacheKey().toString()));
                } else if (Objects.equals(request.getCacheKeyType(), 3)) {
                    cache.invalidate(Integer.valueOf(request.getCacheKey().toString()));
                } else {
                    cache.invalidate(request.getCacheKey().toString());
                }
            }
 
            executionResponse.setCode(0);
            executionResponse.setMsg("success");
        } catch (Exception e) {
            executionResponse.setCode(-1);
            executionResponse.setMsg("fail");
        }
 
        return executionResponse;
    }
}

业务层-缓存数据查询

public static ExecutionResponse inspect(ExecutionRequest request) {
 
    ExecutionResponse executionResponse = new ExecutionResponse();
    executionResponse.setCmdType(CmdTypeEnum.INSTANCE_INSPECT.getCmd());
    executionResponse.setInstanceName(request.getInstanceName());
 
    String instanceName = request.getInstanceName();
    Cache cache = Caffeine.getCacheByInstanceName(instanceName);
 
    Object cacheValue = cache.getIfPresent(request.getCacheKey());
    if (Objects.equals(request.getCacheKeyType(), 2)) {
        cacheValue = cache.getIfPresent(Long.valueOf(request.getCacheKey().toString()));
    } else if (Objects.equals(request.getCacheKeyType(), 3)) {
        cacheValue = cache.getIfPresent(Integer.valueOf(request.getCacheKey().toString()));
    } else {
        cacheValue = cache.getIfPresent(request.getCacheKey().toString());
    }
 
    if (Objects.isNull(cacheValue)) {
        executionResponse.setData("");
    } else {
        executionResponse.setData(JSON.toJSONString(cacheValue));
    }
 
    return executionResponse;
}

说明:

  • 通过Caffeine自带接口进行缓存信息查询。

通信层-监听服务

public class ServerManager {
 
    private Server jetty;
 
    /**
     * 创建jetty对象
     * @throws Exception
     */
    public ServerManager() throws Exception {
 
        int port = NetPortUtils.getAvailablePort();
 
        jetty = new Server(port);
 
        ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
        context.setContextPath("/");
        context.addServlet(ClientServlet.class, "/caffeine");
        jetty.setHandler(context);
    }
 
    /**
     * 启动jetty对象
     * @throws Exception
     */
    public void start() throws Exception {
        jetty.start();
    }
}
 
 
public class ClientServlet extends HttpServlet {
 
    private static final Logger logger = LoggerFactory.getLogger(ClientServlet.class);
 
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doGet(req, resp);
    }
 
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
 
        ExecutionResponse executionResponse = null;
        String requestJson = null;
        try {
            // 获取请求的相关的参数
            String contextPath = req.getContextPath();
            String servletPath = req.getServletPath();
            String requestUri = req.getRequestURI();
            requestJson = IOUtils.toString(req.getInputStream(), StandardCharsets.UTF_8);
 
            // 处理不同的命令
            ExecutionRequest executionRequest = JSON.parseObject(requestJson, ExecutionRequest.class);
 
            // 通过反射来来处理类依赖问题
            executionResponse = DisposeCenter.dispatch(executionRequest);
 
        } catch (Exception e) {
            logger.error("vivo-memory 处理请求异常 {} ", requestJson, e);
        }
 
        if (null == executionResponse) {
            executionResponse = new ExecutionResponse();
            executionResponse.setCode(-1);
            executionResponse.setMsg("处理异常");
        }
 
        // 组装相应报文
        resp.setContentType("application/json; charset=utf-8");
        PrintWriter out = resp.getWriter();
        out.println(JSON.toJSONString(executionResponse));
        out.flush();
    }
}

说明:

  • 通信层通过jetty启动http服务进行监听,安全考虑端口不对外开放。

  • 通过定义ClientServlet来处理相关的请求包括配置下发和缓存查询等功能。

通信层-心跳设计

/**
 * 发送心跳数据
 */
public static void sendHeartBeatData() {
 
    try {
 
        if (!VivoConfigManager.getBoolean("memory.caffeine.heart.report.switch", true)) {
            return;
        }
 
        // 1、构建心跳数据
        String appName = System.getProperty("app.name");
        String localIp = getLocalIp();
        String localPort = String.valueOf(NetPortUtils.getWorkPort());
 
        HeartBeatData heartBeatData = new HeartBeatData();
        heartBeatData.setAppName(appName);
        heartBeatData.setIp(localIp);
        heartBeatData.setPort(localPort);
        heartBeatData.setTimeStamp(System.currentTimeMillis()/1000);
 
        // 2、发送Http的POST请求
        HttpPost httpPost = new HttpPost(getHeartBeatUrl());
        httpPost.setConfig(requestConfig);
 
        StringEntity stringEntity = new StringEntity(JSON.toJSONString(heartBeatData));
        stringEntity.setContentType("application/json");
        httpPost.setEntity(stringEntity);
 
        HttpResponse response = httpClient.execute(httpPost);
        String result = EntityUtils.toString(response.getEntity(),"UTF-8");
        EntityUtils.consume(response.getEntity());
 
        logger.info("Caffeine 心跳上报成功 URL {} 参数 {} 结果 {}", getHeartBeatUrl(), JSON.toJSONString(heartBeatData), result);
    } catch (Throwable throwable) {
        logger.error("Caffeine 心跳上报失败 URL {} ", getHeartBeatUrl(), throwable);
    }
}

说明:

  • 心跳功能上报项目实例的ip和端口用来通信,携带时间戳用来记录上报时间戳。

  • 实际项目中因为机器的回收等场景需要通过上报时间戳定时清理下线的服务。

五、总结

vivo技术团队在Caffeine的使用经验上曾有过多次分享,可参考公众号文章《如何把 Caffeine Cache 用得如丝般顺滑》,此篇文章在使用的基础上基于使用痛点进行进一步的定制。

目前Caffeine可视化的项目已经在相关核心业务场景中落地并发挥作用,整体运行平稳。使用较多的功能包括项目维度的caffeine实例的全局管控,单实例维度的内存占用评估和缓存命中趋势评估。

如通过单实例的内存占用评估功能能够合理评估缓存条目设置和内存占用之间的关系;通过分析缓存命中率的整体趋势评估缓存的参数设置合理性。

期待此篇文章能够给业界缓存使用和监控带来一些新思路。

使用 canvasRenderer 渲染

上一章分析了一下 Sprite 在默认 webgl 渲染器上的渲染,这章让我们把目光聚集到 canvasRenderer 上

image

使用 canvas 渲染器渲染图片的 demo

要使用 canvas 作为渲染器,我们需要引用 pixi-legacy.js

/bundles/pixi.js-legacy/dist/pixi-legacy.js

像下面这样先建一个简单的 demo 用于测试:

<script src="/bundles/pixi.js-legacy/dist/pixi-legacy.js"></script>
<script type="text/javascript">
const app = new PIXI.Application({ width: 800, height: 600 , forceCanvas: true});  
document.body.appendChild(app.view);  

const rectangle = PIXI.Sprite.from('logo.png');  
rectangle.x = 100;  
rectangle.y = 100;  
rectangle.anchor.set(0.5);  
rectangle.rotation = Math.PI / 4;  
app.stage.addChild(rectangle);  
</script>

同样创建一个简单的加载显示 logo 的 demo

运行它应该可以看到在第一章
simple.html
中一模一样的一张 logo 图被渲染在了网页上

在 Application.ts 的 constructor 函数内,即 78 行添加
console.log(this.renderer);
输出当前的渲染器看看

constructor(options?: Partial<IApplicationOptions>)
    {
        // The default options
        options = Object.assign({
            forceCanvas: false,
        }, options);

        this.renderer = autoDetectRenderer<VIEW>(options);
        console.log(this.renderer);
        // install plugins here
        Application._plugins.forEach((plugin) =>
        {
            plugin.init.call(this, options);
        });
    }

image

图 3-1

图 3-1 中可以发现输出了一个 _CanvasRenderer2 的而不是 CanvasRenderer 实例,是因为其实在 demo 中加载的 pixi.js 是经过 rollup 编译后的。

demo
https://github.com/willian12345/blogpost/tree/main/analysis/PixiJS/pixijs-dev/examples/sprite-canvas.html

Sprite 类

Sprite 是 webgl 渲染器和 canvas 渲染器共用的

注意
Sprite.ts 类本身并不做渲染,保存了 Sprite 的基本信息

在此处最终渲染到 canvas 上用的是
CanvasSpriteRenderer
渲染类

我们在直接使用 html 的 canvas 绘制图像时,是直接调用
context.drawImage
方法,并传递一个“图像源”

但在 pixi.js 内,这个图像源并不是直接的图像或 canvas,而是先封装成了一个 texture 即纹理对象,统一管理

找到 /packages/CanvasSpriteRenderer.ts 的第 37 - 40 行

static extension: ExtensionMetadata = {
    name: 'sprite',
    type: ExtensionType.CanvasRendererPlugin,
};

可以看到 CanvasSpriteRenderer 是一个渲染器的插件,当需要渲染一个 sprite 的时候调用的是此插件

最终被调用的 sprite 渲染方法, 即绘制图片或路径等到 canvas 上

render(sprite: Sprite): void
{
    const texture = sprite._texture;
    const renderer = this.renderer;
    const context = renderer.canvasContext.activeContext;
    const activeResolution = renderer.canvasContext.activeResolution;

    if (!texture.valid)
    {
        return;
    }

    const sourceWidth = texture._frame.width;
    const sourceHeight = texture._frame.height;

    let destWidth = texture._frame.width;
    let destHeight = texture._frame.height;

    if (texture.trim)
    {
        destWidth = texture.trim.width;
        destHeight = texture.trim.height;
    }

    let wt = sprite.transform.worldTransform;
    let dx = 0;
    let dy = 0;

    const source = texture.baseTexture.getDrawableSource();

    if (texture.orig.width <= 0 || texture.orig.height <= 0 || !texture.valid || !source)
    {
        return;
    }

    renderer.canvasContext.setBlendMode(sprite.blendMode, true);

    context.globalAlpha = sprite.worldAlpha;

    // If smoothingEnabled is supported and we need to change the smoothing property for sprite texture
    const smoothingEnabled = texture.baseTexture.scaleMode === SCALE_MODES.LINEAR;
    const smoothProperty = renderer.canvasContext.smoothProperty;

    if (smoothProperty
        && context[smoothProperty] !== smoothingEnabled)
    {
        context[smoothProperty] = smoothingEnabled;
    }

    if (texture.trim)
    {
        dx = (texture.trim.width / 2) + texture.trim.x - (sprite.anchor.x * texture.orig.width);
        dy = (texture.trim.height / 2) + texture.trim.y - (sprite.anchor.y * texture.orig.height);
    }
    else
    {
        dx = (0.5 - sprite.anchor.x) * texture.orig.width;
        dy = (0.5 - sprite.anchor.y) * texture.orig.height;
    }

    if (texture.rotate)
    {
        wt.copyTo(canvasRenderWorldTransform);
        wt = canvasRenderWorldTransform;
        groupD8.matrixAppendRotationInv(wt, texture.rotate, dx, dy);
        // the anchor has already been applied above, so lets set it to zero
        dx = 0;
        dy = 0;
    }

    dx -= destWidth / 2;
    dy -= destHeight / 2;

    renderer.canvasContext.setContextTransform(wt, sprite.roundPixels, 1);
    // Allow for pixel rounding
    if (sprite.roundPixels)
    {
        dx = dx | 0;
        dy = dy | 0;
    }

    const resolution = texture.baseTexture.resolution;

    const outerBlend = renderer.canvasContext._outerBlend;

    if (outerBlend)
    {
        context.save();
        context.beginPath();
        context.rect(
            dx * activeResolution,
            dy * activeResolution,
            destWidth * activeResolution,
            destHeight * activeResolution
        );
        context.clip();
    }

    if (sprite.tint !== 0xFFFFFF)
    {
        if (sprite._cachedTint !== sprite.tintValue || sprite._tintedCanvas.tintId !== sprite._texture._updateID)
        {
            sprite._cachedTint = sprite.tintValue;

            // TODO clean up caching - how to clean up the caches?
            sprite._tintedCanvas = canvasUtils.getTintedCanvas(sprite, sprite.tintValue);
        }

        context.drawImage(
            sprite._tintedCanvas,
            0,
            0,
            Math.floor(sourceWidth * resolution),
            Math.floor(sourceHeight * resolution),
            Math.floor(dx * activeResolution),
            Math.floor(dy * activeResolution),
            Math.floor(destWidth * activeResolution),
            Math.floor(destHeight * activeResolution)
        );
    }
    else
    {
        context.drawImage(
            source,
            texture._frame.x * resolution,
            texture._frame.y * resolution,
            Math.floor(sourceWidth * resolution),
            Math.floor(sourceHeight * resolution),
            Math.floor(dx * activeResolution),
            Math.floor(dy * activeResolution),
            Math.floor(destWidth * activeResolution),
            Math.floor(destHeight * activeResolution)
        );
    }

    if (outerBlend)
    {
        context.restore();
    }
    // just in case, leaking outer blend here will be catastrophic!
    renderer.canvasContext.setBlendMode(BLEND_MODES.NORMAL);
}

我想 sprite render 方法估计是在使用 pixi 时用的最多的方法

在此 render 方法内 打一个 debugger 后:

image

图 3-2

看一下方法的调用栈 从 图 3-2 中的红色向上箭头可以看到 _tick 函数一级一级往 render 方法内调用

render 函数做了什么

render 方法大致做了以下几步:

  1. 接受一个 sprite 对象实例,获取到这个 sprite 的当前 "激活的canvas2d上下文" activeContext

    当前激活的上下文不是固定的“根上下文” rootContext 而是可变的,因为可以并允许创建多个 canvas 的情况存在比如 “离屏渲染,用新的canvas缓存图片” 等


    /packages/canvas-render/CanvasContextSystem.ts
    文件的第 79 行
    init()
    初始化方法内可以看到
    this.activeContext = this.rootContext;
    默认就是“根上下文”

  2. 接下来是确定当前 canvas context 的渲染模式
    renderer.canvasContext.setBlendMode(sprite.blendMode, true);

    即根据传递进来的 sprite 的 blendMode 确定当前 canvas context 的渲染模式, blendMode 是一个枚举值

    blendMode 对应的是 可以查看
    /packages/canvas-render/src/utils/mapCanvasBlendModesToPixi.ts
    中的生成并存储的 CanvasRenderingContext2D.globalCompositeOperation 值

    具体值所对应的效果可查看
    https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation

    除了做特效外,碰到过最多blendMode 的应用场景是在一些 html5 做那种刮奖效果

  3. 生成当前 context 上下文的变幻矩阵(transform)

    根据传递进来的 sprite 的 texture 确定绘制“图形”的尺寸,旋转信息,转换成当前上下文的变幻矩阵(transform)

    render 方法内的 'wt' 变量(word transform) , 就是这一句
    renderer.canvasContext.setContextTransform(wt, sprite.roundPixels, 1);

  4. 根据 outerBlend 确定是否需要上下文进行 clip 裁剪

    其实就是是否要用就遮罩效果 相关信息可查看
    https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Compositing

  5. 调用上下文的 canvas 原生方法 drawImage 开始真正的绘制工作, 这里的 tint 值比较有意思,后面再详细介绍它

    绘制前先判断之前是否有缓存过图形,如果之前绘制过就直接绘制缓存的图形以提高性能

    此处作者还注释了一句,
    // TODO clean up caching - how to clean up the caches?
    , 充分说明了写程序肯定不是一蹴而就的
    _
    !

用 tint 给显示对象(DisplayObject)上色

tint 属性用于改变显示对象的颜色。

它通过混合原始颜色与指定的颜色来给显示对象

这里有几个关键点来理解 tint 属性的工作方式:

  1. 颜色混合:tint接受一个十六进制颜色值,这个值用来与对象原有的颜色进行混合。混合操作不是简单地替换颜色,而是基于色彩理论进行的,因此可以得到不同的视觉效果。

  2. 透明度影响:tint操作同时影响颜色和alpha(透明度)。这意味着,即使不直接改变对象的透明度,颜色的变化也可能影响其视觉上的透明程度。

  3. 全白或全透明不受影响:如果一个像素是完全白色(#FFFFFF)或完全透明,那么tint不会改变它。这是因为全白像素可以吸收任何颜色的混合,而全透明像素则不显示颜色变化。

  4. 多边形和纹理:对于包含纹理的显示对象(如Sprite),tint会影响整个纹理的颜色。而对于矢量图形(如通过Graphics绘制的形状),颜色混合会直接应用于线条或填充颜色。

  5. 性能考虑:与直接更换纹理或颜色相比使用 tint 在很多情况下更高效,因为它避免了重新加载或创建新的纹理资源。

在这里判断是否需要处理 tint 缓存比较有意思,如果你将 render 内的 sprite.tint 用 console.log 输出会得到值
16777215

而用
console.log(0xFFFFFF)
输出的也是
16777215
, 都会转换成十进制

以我们这个
sprite-canvas.html
为例,它是不缓存的,所以会直接走最下面的直接绘制逻辑

如果把我们的 sprite-canvas.html 代码修改一下,加一句
rectangle.tint = 'red';
如下:

<script type="text/javascript">
const app = new PIXI.Application({ width: 800, height: 600 , forceCanvas: true});  
document.body.appendChild(app.view);  

const rectangle = PIXI.Sprite.from('logo.png');  
rectangle.x = 100;  
rectangle.y = 100;  
rectangle.anchor.set(0.5);  
rectangle.rotation = Math.PI / 4;  
rectangle.tint = 'red';
app.stage.addChild(rectangle);  
</script>

你可以看到,整个 PixiJS 的 logo 变成了绝色

image

图 3-3

更准确的说应该说是混合成了红色

在 render 函数内的
sprite._tintedCanvas = canvasUtils.getTintedCanvas(sprite, sprite.tintValue);


/packages/canvas-render/src/canvasUtils.ts
源文件 50 行找到 的
getTintedCanvas
方法

此方法内会调用 canvasUtils.tintMethod(texture, color, canvas);

canvasUtils.tintMethod = canvasUtils.canUseMultiply ? canvasUtils.tintWithMultiply : canvasUtils.tintWithPerPixel;

最后根据能不能使用 multiply 确定使用哪种 tint 方法,优先使用 tintWithMultiply 方法

/packages/canvas-render/src/canvasUtils.ts
源文件 110 - 159 行:

tintWithMultiply: (texture: Texture, color: number, canvas: ICanvas): void =>
{
    const context = canvas.getContext('2d');
    const crop = texture._frame.clone();
    const resolution = texture.baseTexture.resolution;

    crop.x *= resolution;
    crop.y *= resolution;
    crop.width *= resolution;
    crop.height *= resolution;

    canvas.width = Math.ceil(crop.width);
    canvas.height = Math.ceil(crop.height);

    context.save();
    context.fillStyle = Color.shared.setValue(color).toHex();

    context.fillRect(0, 0, crop.width, crop.height);

    context.globalCompositeOperation = 'multiply';

    const source = texture.baseTexture.getDrawableSource();

    context.drawImage(
        source,
        crop.x,
        crop.y,
        crop.width,
        crop.height,
        0,
        0,
        crop.width,
        crop.height
    );

    context.globalCompositeOperation = 'destination-atop';

    context.drawImage(
        source,
        crop.x,
        crop.y,
        crop.width,
        crop.height,
        0,
        0,
        crop.width,
        crop.height
    );
    context.restore();
},

在 tintWithMultiply 这个方法通过设置 context 上下文的 fillStyle 结合 globalCompositeOperation 绘制一个矩形框叠加到图像源(source)上 实现变色,当然这里使用的肯定是独立于 rootContext 的 canvas

context.fillStyle = Color.shared.setValue(color).toHex();

context.fillRect(0, 0, crop.width, crop.height);

context.globalCompositeOperation = 'multiply';

如果不支持 Multiply 则调用性能消耗更高的 tintWithPerPixel 方法

tintWithPerPixel: (texture: Texture, color: number, canvas: ICanvas): void =>
{
    const context = canvas.getContext('2d');
    const crop = texture._frame.clone();
    const resolution = texture.baseTexture.resolution;

    crop.x *= resolution;
    crop.y *= resolution;
    crop.width *= resolution;
    crop.height *= resolution;

    canvas.width = Math.ceil(crop.width);
    canvas.height = Math.ceil(crop.height);

    context.save();
    context.globalCompositeOperation = 'copy';
    context.drawImage(
        texture.baseTexture.getDrawableSource(),
        crop.x,
        crop.y,
        crop.width,
        crop.height,
        0,
        0,
        crop.width,
        crop.height
    );
    context.restore();

    const [r, g, b] = Color.shared.setValue(color).toArray();
    const pixelData = context.getImageData(0, 0, crop.width, crop.height);

    const pixels = pixelData.data;

    for (let i = 0; i < pixels.length; i += 4)
    {
        pixels[i + 0] *= r;
        pixels[i + 1] *= g;
        pixels[i + 2] *= b;
    }

    context.putImageData(pixelData, 0, 0);
},

注意 tintWithPerPixel 这个方法内是先绘制源图像,再利用 getImageData 和 putImageData 像素级操作实现的变色效果,所以传统比较消耗性能

在 render 方法的最后一句
renderer.canvasContext.setBlendMode(BLEND_MODES.NORMAL);
将上下文的渲染模式恢复为普通值,以免影响全局的渲染

至上 canvas-sprite 渲染流程算是走完了

本章小节

果然 canvas 的渲染比起 webgl 的渲染容易理解一些,虽然都是顺序执行的命令行,但是 webgl 的渲染模式需要绘制到 GPU 之前需要收集的命令比 canvas 渲染要多出许多步骤

下一章让我们聚焦到最重要的事件交互上,PixiJS 是如何在 canvas 上实现交互事件的,如何处理最典型的鼠标点击事件并响应点击

还有,如果你到现在还是没能在你本地把调式项目跑起来,那么首先参考这个系列文章的第一章,然后直接下载我这个
https://github.com/willian12345/blogpost/tree/main/analysis/PixiJS/pixijs-dev
调式项目


注:转载请注明出处博客园:王二狗Sheldon池中物 (willian12345@126.com)

ComfyUI插件:IPAdapter_plus(新版)节点

前言:

学习ComfyUI是一场持久战,而IPAdapter_plus是常用且便捷有效的风格迁移模型,可以通过提供参考图像去进行图像的生成,比如风格迁移,风格融合,人物脸部模拟等各种工作,请大家重点关注本篇内容,深刻理解节点用法!!祝大家学习顺利,早日成为ComfyUI的高手!

目录

一、IPAdapter Advanced/ IPAdapter/ IPAdapter Batch (Adv.)节点

二、IPAdapter Unified Loader FaceID/ IPAdapter Unified Loader/ IPAdapter Unified Loader Community节点

三、IPAdapter FaceID/ IPAdapter FaceID Batch节点

四、IPAdapter Tiled/ IPAdapter Tiled Batch节点

五、IPAdapter Embeds/ IPAdapter Combine Embeds/ IPAdapter Encoder节点

六、IPAdapter Noise/ Prep Image For ClipVision节点

七、“参考人物换装更换”示例工作流

一、IPAdapter Advanced/ IPAdapter/ IPAdapter Batch (Adv.)节点

这三个节点用来将IPAdapter模型的效果注入到大模型中去引导扩散。

1

输入:

model → 模型

ipadapter → IP适配器

image → 图像

image_negative → 负图像

attn_mask → 注意力掩码

clip_vision → CLIP视觉

参数:

weight → 权重

weight_type → 权重类型

combine_embeds → 合并嵌入 可选项为concat、add、subtract、average、norm average

start_at → 起始位置

end_at → 结束位置

embeds_scaling → 嵌入缩放;默认为'V only',可选项为'V only'、'K+V'、'K+V w/ C penalty'、'K+mean(V) w/ C penalty'

'V only':仅对视觉嵌入(V)进行缩放操作,保持其他嵌入不变。

'K+V':对键(K)和视觉嵌入(V)进行缩放操作,保持其他嵌入不变。

'K+V w/ C penalty':对键(K)和视觉嵌入(V)进行缩放操作,并在缩放过程中对C惩罚,保持其他嵌入不变。

'K+mean(V) w/ C penalty':对键(K)和视觉嵌入(V)进行缩放操作,但是在缩放视觉嵌入时使用均值进行,同时对C进行惩罚,保持其他嵌入不变。

输出:

MODEL → 输出嵌入信息之后的模型

示例:如下图所示,使用SDXL的大模型,对weight_type的四种变化方式进行了测试,可以看出权重类型的不同变换方式对最终图像的生成有很大的影响。

2

示例:如下图所示,对四种不同的嵌入方式进行了一一实验,大家可自行对比择优选择。

3

功能:

①IPAdapter Advanced 节点提供了更多高级选项和参数,以实现对图像的精细控制。该节点通常用于需要更精确调整和优化图像的情况。主要功能包括:

· 高级图像处理选项:提供一系列高级图像处理算法和参数,可以对图像进行详细的调整和优化。

· 多种滤镜和效果:允许用户应用多种滤镜和效果,以实现特定的图像风格或增强效果。

· 自定义参数:提供更灵活的自定义参数设置,用户可以根据具体需求调整每个处理步骤的参数。

②IPAdapter 节点是基本的图像处理和调整节点,提供了常用的图像处理功能。适用于大多数普通图像处理任务。主要功能包括:

· 基本图像处理功能:如裁剪、调整大小、旋转、颜色调整等。

· 预设滤镜和效果:包含一些常用的滤镜和效果,用户可以快速应用这些预设来调整图像。

· 参数设置:允许用户对基本参数进行调整,以实现所需的图像效果。

③IPAdapter Batch (Adv.) 节点专为处理大量图像而设计,适用于批量处理场景。主要功能包括:

· 批量处理:支持一次处理多张图像,提高处理效率。

· 高级处理选项:提供与IPAdapter Advanced相同的高级选项和参数,确保批量处理时仍然可以进行精细控制。

· 自动化工作流程:支持自动化设置和参数调整,减少手动操作,提高工作效率。

二、IPAdapter Unified Loader FaceID/ IPAdapter Unified Loader/ IPAdapter Unified Loader Community节点

这三个节点是用来自动加载IPAdapter和其对应的CLIP version模型的,不需要自己对应加载。

4

输入:

model → 要应用模型的对象

ipadapter → IPAdapter 模型的配置信息

参数:

preset → 预设的模型配置

lora_strength → LoRA 模型的强度参数

provider → InsightFace 模型的提供程序

输出:

MODEL → 输出选择的模型

ipadapter → IPAdapter 模型的配置信息

示例:如下图所示,给出了IPAdapter Unified Loader FaceID的一般用法,该节点可以自动加载对应的lora,clip version和insightface模型,不需要自己进行指定,同理另外两个节点也可以自行适配不需额外的指定。

5

功能:

①IPAdapter Unified Loader FaceID 节点专为处理人脸识别和人脸数据加载设计,适用于需要对人脸数据进行统一处理的应用场景。主要功能包括:

· 人脸识别加载:能够从数据集中加载人脸图像,确保图像数据格式一致。

· 特征提取:提取人脸特征用于进一步处理或分析。

· 人脸对齐:自动对齐人脸图像,以便后续处理步骤中更容易处理。

②IPAdapter Unified Loader 节点是一个通用的图像数据加载节点,适用于需要统一处理和格式化图像数据的场景。主要功能包括:

· 统一数据加载:从各种来源加载图像数据,并将其格式化为统一的数据格式。

· 基本预处理:包括图像裁剪、调整大小、颜色调整等基本预处理功能。

· 数据规范化:确保所有加载的图像数据都符合统一的规范和标准,方便后续处理。

③IPAdapter Unified Loader Community 节点专为社区数据集设计,适用于加载和处理来自社区的数据集。主要功能包括:

· 社区数据集支持:支持从各种社区来源加载图像数据,例如开源数据集、公共数据集等。

· 数据整合:将来自不同社区的数据集整合为统一格式,便于统一处理。

· 社区特定预处理:提供针对社区数据集的特定预处理选项,确保处理流程优化。

三、IPAdapter FaceID/ IPAdapter FaceID Batch节点

这两个节点是用来识别人物的脸部,然后控制生成的图像跟参考图像的脸部相似,Batch节点可以批量的输入参考图像。

6

输入:

model → 要应用模型的对象

ipadapter → IPAdapter 模型的配置

image → 图像输入

image_negative → 负图像输入

attn_mask → 注意力掩码

clip_vision → CLIP 视觉模型

insightface → InsightFace 模型

参数:

weight → 权重参数

weight_faceidv2 → FaceIDv2 权重参数

weight_type → 权重类型

combine_embeds → 嵌入组合方式

start_at → 起始位置

end_at → 结束位置

embeds_scaling → 嵌入缩放方式

输出:

MODEL → 输出选择的模型

示例:使用faceID之前,对图片要进行预处理,处理成为224*224的正方形才能提供给视觉编码模型,如果在原始图像中画蒙版,那么蒙版区域与处理后的图像并不贴合,所以会导致生成的图像并不像原图。

7

示例:ipadapter faceID batch节点可以同时批量处理图像
8

功能:

①IPAdapter FaceID 节点专为单张人脸图像的处理和识别设计,适用于需要对单个人脸图像进行识别和处理的场景。主要功能包括:

· 人脸检测:识别人脸图像中的面部区域。

· 特征提取:从人脸图像中提取独特的面部特征用于识别。

· 匹配和识别:将提取的面部特征与已有数据库中的特征进行匹配,以识别身份或验证身份。

· 图像预处理:包括图像裁剪、调整大小、对齐和颜色调整等基本预处理操作。

②IPAdapter FaceID Batch 节点专为批量处理人脸图像设计,适用于需要一次处理多张人脸图像的场景。主要功能包括:

· 批量处理:支持一次处理多个图像,极大提高了处理效率。

· 人脸检测和特征提取:与IPAdapter FaceID相同,进行人脸检测和特征提取,但能够同时处理多个图像。

· 批量匹配和识别:对多个图像中的人脸特征进行匹配和识别,适用于大规模人脸识别任务。

· 自动化处理:提供自动化处理选项,减少手动操作,适用于需要高效处理大量人脸图像的场景。

四、IPAdapter Tiled/ IPAdapter Tiled Batch节点

这两个节点可以输入非正方形的图像,然后对整体分块进行参考,让最终生成的图像参考原图的全部信息。

9

输入:

model → 要应用模型的对象

ipadapter → IPAdapter 模型的配置

image → 图像输入

image_negative → 负图像输入

attn_mask → 注意力掩码

clip_vision → CLIP 视觉模型

参数:

weight → 权重参数

weight_type → 权重类型

combine_embeds → 嵌入组合方式

start_at → 起始位置

end_at → 结束位置

sharpening → 锐化程度

embeds_scaling → 嵌入缩放方式

输出:

MODEL → 输出最终的模型信息

mask → 如果输入有蒙版,会输出分块后的蒙版信息

tiles → 输出分块的结果

示例:

10

示例:如下图所示,在原图中绘制蒙版,然后将蒙版传入Tiled节点之后,节点会对蒙版也进行相应的裁剪和原始图像一一对应,最终生成的图像只参考蒙版区域进行模仿。

11

功能:

①IPAdapter Tiled节点用于处理单张大分辨率图像,通过将图像分成多个较小的块进行处理。主要功能包括:

· 图像分块:将大分辨率图像分成多个小块,以便更高效地处理和分析。

· 分块处理:对每个图像块单独进行处理,例如滤镜应用、特征提取等。

· 重组图像:处理完成后将各个图像块重新组合,恢复为完整的图像。

· 处理优化:通过分块处理减少内存消耗和处理时间,适用于处理高分辨率图像或复杂图像处理任务。

②IPAdapter Tiled Batch节点用于批量处理多个分块的图像,适用于需要一次处理多张大分辨率图像的场景。主要功能包括:

· 批量图像分块:支持一次将多张图像分成多个小块进行处理。

· 批量分块处理:对每个图像块进行批量处理,适用于大规模图像处理任务。

· 批量重组图像:处理完成后将多个图像的块重新组合,恢复为完整的图像。

· 高效处理:通过批量和分块处理,显著提高处理效率和资源利用率,适用于需要处理大量高分辨率图像的应用场景。

五、IPAdapter Embeds/ IPAdapter Combine Embeds/ IPAdapter Encoder节点

这三个节点组合使用,分别是对原始图像进行编码处理,合并编码后的结果,编码结果来影响模型的扩散。

12

输入:

model → 要应用模型的对象

ipadapter → IPAdapter 模型的配置

pos_embed → 正向嵌入

neg_embed → 负向嵌入

image → 图像输入

attn_mask → 注意力掩码

clip_vision → CLIP 视觉模型

embedx → 嵌入的条件信息

参数:

weight → 权重参数

weight_type → 权重类型

combine_embeds → 嵌入组合方式

start_at → 起始位置

end_at → 结束位置

输出:

MODEL → 输出最终的模型信息

embed → 嵌入编码信息

示例:

13

功能:

①IPAdapter Embeds节点用于从图像中提取特征嵌入,生成用于后续处理或分析的特征向量。主要功能包括:

· 特征提取:从输入图像中提取重要特征,生成特征向量(嵌入)。

· 图像表示:将图像数据转换为低维度的特征向量,以便于后续的处理和分析。

· 多种模型支持:支持使用不同的预训练模型来提取特征嵌入,适用于各种图像处理任务。

②IPAdapter Combine Embeds节点用于将多个特征嵌入进行组合,生成一个新的综合特征嵌入。主要功能包括:

· 特征组合:将多个输入的特征嵌入进行融合,生成一个综合的特征向量。

· 多种组合方法:支持多种特征组合方法,如加权平均、拼接等,适应不同的处理需求。

· 增强特征表示:通过组合多个特征嵌入,提高特征表示的丰富性和鲁棒性,适用于复杂图像分析任务。

③IPAdapter Encoder节点用于对图像进行编码,将图像数据转换为特征嵌入。主要功能包括:

· 图像编码:将输入图像通过编码器模型,转换为特征嵌入。

· 模型训练支持:支持使用预训练模型或自定义训练的编码器模型进行编码。

· 特征表示优化:通过编码过程,优化图像特征的表示,便于后续处理或分析。

六、IPAdapter Noise/ Prep Image For ClipVision节点

这两个节点,一个是用来将原图加上噪声,并可以设置一定的控制方法,比如shuffle,第二个节点是将原始图像进行裁剪去生成适合视觉编码的图像。

14

输入:图像信息

参数:

type → 噪声类型

strength → 强度

blur → 模糊程度

interpolation → 插值方式

crop_position → 裁剪位置

sharpening → 锐化程度

输出:处理后的图像信息

示例:

15

功能:

①IPAdapter Noise 节点用于添加或处理图像中的噪声,常用于数据增强或图像处理的特定需求。主要功能包括:

· 添加噪声:在图像中添加随机噪声,以增强数据的多样性,通常用于训练深度学习模型时的数据增强。

· 噪声类型:支持多种噪声类型,如高斯噪声、椒盐噪声等,用户可以根据需要选择合适的噪声类型。

· 噪声强度:允许用户调整噪声的强度,以控制添加噪声的量级和影响。

②Prep Image For ClipVision 节点用于将图像数据预处理为适合ClipVision模型输入的格式和规范。主要功能包括:

· 图像调整:调整图像的尺寸和分辨率,使其符合ClipVision模型的输入要求。

· 图像归一化:对图像数据进行归一化处理,以匹配ClipVision模型的输入规范,通常包括将像素值缩放到特定范围(如0-1或-1到1)。

· 颜色处理:调整图像的颜色通道顺序或进行颜色归一化,以确保输入图像与ClipVision模型的训练数据一致。

七、“参考人物换装更换”示例工作流

熟练使用以上节点,你就可以搭建“参考人物换装更换”的工作流了。

16

该工作流输入两张图像,一张原始人物图像用来参考脸部和发型,一张一幅图像,用来作为衣服参考,衣服参考图像通过segment anything节点去抠图生成衣服对应的蒙版然后进行embed,人物直接进行embed,人物图像会加入噪声编码后去作为neg embed,然后去控制采样器出图,最终结果如下:

17

**孜孜以求,方能超越自我。坚持不懈,乃是成功关键。**

一晃距
C# 9
发布已经4年了,对于
record
关键字想必大家都不陌生了,不过呢发现还是有很多同学不屑于使用这个
语法糖
,确实,本质上 record 就是 class 的封装,能用 record 书写的类,那100%都是可以自己手撸出来的,但是呢有没有考虑 别人可能一分钟写好的代码你可能会需要数分钟才能完成.因此为了能有更多时间
摸鱼
,强烈推荐不屑一顾的同学也能用起来!

下面我简略聊一聊 record 的好处和最佳场景:

  1. 简化语法

我们只需要一行代码就可以定义完成,这个是最直观节省编码的方式,我们不需要编写一堆枯燥的
get;set;
也不需要编写构造函数等样板代码:

public record Person(string FirstName, string LastName);

那么有同学会有疑问,如果Person有很多的属性咋整,不就意味着主构造函数会很冗长,其实呢,这个和封装传参的方式是一样的,我们可以把同质的属性封装成其他的record或者class,比如:

public record ExtraInfomation(string Address,string Email,int Age);
public record Person(string FirstName, string LastName, ExtraInfomation ExtraInfo);
  1. 自动生成一些对我们有用的成员函数.
  • 构造函数:根据定义的属性自动生成构造函数。
  • 属性:自动生成只读属性。
  • Deconstruct 方法:用于解构记录对象,对于习惯写
    TS
    的小伙伴相当友好。
  • Equals 和 GetHashCode 方法:基于属性值的相等性比较。
  • ToString 方法:提供友好的字符串表示,对于调试输出特别友好。
  1. 基于值的相等性语法.

我们很多时候有这种需求就是比较一个类的所有属性来判断逻辑.如果使用 record 的话 我们只需要
==
或者
Equals
就能判断,

  1. 非破坏性复制值

对于一个 class 的浅表复制,我们可能需要实现
ICloneable
,亦或者 new 一个对象逐个属性赋值,当然还有其他的方法,但是呢肯定是没有 record 来的这么简单直接.我们仅需要一个
with
关键字就干完了

public record Person(string FirstName, string LastName, int Age);
var person1 = new Person("vip", "wan", 18);
var person2 = person1 with { Age = 30 };
Console.WriteLine(person1); // 输出: Person { FirstName = vip, LastName = wan, Age = 18 }
Console.WriteLine(person2); // 输出: Person { FirstName = vip, LastName = wan, Age = 30 }
  1. 解构的支持

record 类型自动生成 Deconstruct 方法,允许你轻松地解构记录对象,对于全栈的同学书写就是手到擒来!

var person = new Person("vip", "wan", 18);
var (firstName, lastName, age) = person;
Console.WriteLine(firstName); // 输出: vip
Console.WriteLine(lastName);  // 输出: wan
Console.WriteLine(age);       // 输出: 18
  1. 结合模式匹配

record 类型与模式匹配功能很好地集成在一起,使得在模式匹配中使用记录对象更加方便。

public record Person(string UserName, int Age);
public string GetPersonInfo(Person person) => person switch
{
    { Age: < 18 } => "Minor",
    { Age: >= 18 } => "Adult",
    _ => "Unknown"
};
  1. 填充既有类

嗯当前 C# 语言是真的突飞猛进,年底就要发布
C# 13
了,小伙伴们都
直呼学不动了
!,当然也有同学肯定也尝鲜了
主构造函数
了吧, 如果想要对主构造函数进一步了解可以
点击链接
对于注入的服务又能少撸不少的代码!

那么既然 class 都有了
主构造函数
,是不是意味着 record 就失去意义了呢?!,嗯?!你忘了上面的那些糖的
甜度
了吗?

因此我们如果需要对既有的 class 支持到 record 的特性我们只需要在class前加上 record 即可.

public record class User {
  public string UserName{ get; set;}
  public int Age { get; set;}
}
var user1 = new User { UserName = "vipwan" , Age = 18};
var user2 = user1 with { };
var user3 = user1 with { Age = 30 };
user1 == user2 // true;
user3.ToString() // "User { Name = vipwan, Age = 30 }"
总结

使用 record 类型的主要好处包括简洁的语法、自动生成的成员、基于值的相等性、非破坏性复制、解构支持、继承支持和与
模式匹配
的良好集成。这些特性使得 record 类型非常适合用于不可变数据对象(DTO,VO等),提高了代码的
可读性

可维护性

开发效率

0x01 代码优化与压缩

(1)HTML

移除不必要的空白字符、注释和冗余标签,以减少文件大小

  1. 使用命令
    npm install html-minifier -g
    安装
    HTML Minifier

  2. 使用命令
    html-minifier -V
    确认安装成功

  3. 在 Node.js 环境中配置 index.js

    // 引入 HTML Minifier
    const minify = require("html-minifier").minify;
    
    // 处理 HTML 文本
    let result = minify('<p title="blah" id="moo">foo</p>', {
      removeAttributeQuotes: true,
    });
    
    // 输出处理结果
    console.log(result);
    
  4. 使用命令
    node .\index.js
    运行,输出结果为:
    <p title=blah id=moo>foo</p>

  5. 详细参考:
    https://www.npmjs.com/package/html-minifier
    在线使用:
    https://kangax.github.io/html-minifier/

(2)CSS

精简样式表,避免使用冗余或过时的属性,合理组织选择器以减少计算复杂度

  1. 使用命令
    npm install cssnano postcss postcss-cli --save-dev
    安装 PostCSS 与 CSSNaNo

  2. 在 Node.js 环境中配置 postcss.config.js

    module.exports = {
      plugins: [
        require("cssnano")({
          preset: "default",
        }),
      ],
    };
    
  3. 使用命令
    npx postcss input.css > output.css
    运行,生成优化后的结果 output.css

  4. 详细参考:
    https://www.cssnano.cn/docs/introduction/

  5. 其他工具:


    1. PostCSS:
      https://www.postcss.com.cn/
    2. PurgeCSS:
      https://www.purgecss.cn/

(3)JavaScript

使用工具(如 Webpack 等)或在线服务对代码进行压缩,移除空格、注释和不必要的字符

  1. 使用命令
    npm install terser-webpack-plugin --save-dev
    安装 terser-webpack-plugin

  2. 在 Node.js 环境中配置 webpack.config.js

    const TerserPlugin = require("terser-webpack-plugin");
    
    module.exports = {
      optimization: {
        minimize: true,
        minimizer: [new TerserPlugin()],
      },
    };
    
  3. 使用命令
    npx webpack
    运行 Webpack 打包工具,并优化 JavaScript 代码

(4)合并文件

将多个 CSS、JavaScript 文件合并为一个,减少 HTTP 请求的数量

  • 在 Webpack 中,可以通过配置
    entry

    output
    选项来自动合并多个模块到一个文件中
  • 在 Gulp 中,可以使用
    gulp-concat
    插件来合并文件

0x02 静态资源优化

(1)压缩图片

(2)图片懒加载

a. 原生 JavaScript

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

<head>
  <meta charset="UTF-8" />
  <style>
    img {
      width: 1000px;
      height: 700px;
      background-color: wheat;
      object-fit: cover;
      object-position: center;
    }
  </style>
</head>

<body>
  <img src="" data-src="./images/1.jpg" alt="" />
  <img src="" data-src="./images/2.jpg" alt="" />
  <img src="" data-src="./images/3.jpg" alt="" />
  <img src="" data-src="./images/4.jpg" alt="" />
  <img src="" data-src="./images/5.jpg" alt="" />
  <script>
    /**
     * 初始化图片懒加载功能。
     * 该函数通过监听窗口的滚动事件,来实现图片的延迟加载。当图片进入视口时,将其src属性设置为真正的图片源URL,从而实现懒加载的效果。
     */
    const imageLazyLoad = () => {
      // 获取页面中所有带有data-src属性的图片元素
      const imgs = document.querySelectorAll("img");
      // 定义计算函数,用于检查图片是否进入视口
      const calc = () => {
        imgs.forEach((img) => {
          // 检查图片是否进入视口:如果图片的顶部位置小于等于窗口的底部位置,则图片已进入视口,可以加载
          if (img.offsetTop <= window.innerHeight + window.scrollY)
            // 设置图片的src属性为data-src属性的值,真正开始加载图片
            img.src = img.dataset.src;
          else
            // 如果图片未进入视口,则返回,不进行加载
            return;
        });
      };
      // 监听窗口的滚动事件,以便在滚动时触发图片的加载
      window.addEventListener("scroll", () => calc());
      // 初次加载页面时,立即计算并加载可视区域内的图片
      calc();
    };
    // 调用函数,初始化图片懒加载
    imageLazyLoad();
  </script>
</body>

</html>

b. Intersection Observer API

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

<head>
  <meta charset="UTF-8" />
  <style>
    img {
      width: 1000px;
      height: 700px;
      background-color: wheat;
      object-fit: cover;
      object-position: center;
    }
  </style>
</head>

<body>
  <img src="" data-src="./images/1.jpg" alt="" />
  <img src="" data-src="./images/2.jpg" alt="" />
  <img src="" data-src="./images/3.jpg" alt="" />
  <img src="" data-src="./images/4.jpg" alt="" />
  <img src="" data-src="./images/5.jpg" alt="" />
  <script>
    /**
     * 初始化图片懒加载
     * 该函数通过IntersectionObserver API来实现图片的懒加载。只有当图片进入视口时,才会真正加载图片资源
     */
    const imageLazyLoad = () => {
      // 获取所有需要懒加载的图片元素
      const imgs = document.querySelectorAll("img");

      // 创建IntersectionObserver实例,用于观察图片是否进入视口
      const observer = new IntersectionObserver((entries) => {
        // 遍历所有观察到的条目
        entries.forEach((entry) => {
          // 如果图片进入视口
          if (entry.isIntersecting) {
            let img = entry.target;
            // 设置图片的src属性为data-src属性的值,即真正的图片源地址
            img.src = img.dataset.src;
            // 停止观察该图片,因为它已经加载
            observer.unobserve(img);
          }
        });
      });

      // 对所有需要懒加载的图片元素启用IntersectionObserver观察
      imgs.forEach((img) => {
        observer.observe(img);
      });
    };
    imageLazyLoad();
  </script>
</body>

</html>

c. 第三方库

(3)CDN

  • 部署内容分发网络(CDN),将静态资源托管在地理位置接近用户的边缘节点上,减少延迟
  • 步骤:
    1. 选择 CDN 服务提供商,如 Amazon、Cloudflare 等
    2. 上传静态资源
    3. 配置 DNS
    4. 测试并优化
  • 优点:减少访问延迟、提高可用性、减轻源服务器负载

(4)缓存机制

  • 步骤:
    1. 设置 Cache-Control 头部:响应头字段,表示在缓存有效期内直接从本地缓存中加载这些资源,而不是向服务器发送请求
    2. 使用 ETag:响应头字段,表示资源的特定版本
      • 当浏览器再次请求资源时,会将 ETag 值发送给服务器,如果资源未更改(即 ETag 值相同),服务器将返回 304 Not Modified 响应,告诉浏览器使用本地缓存的版本。
    3. 配置 Expires 头部:兼容旧版浏览器
  • 优点:减少服务器负载、加快加载速度、改善用户体验

0x03 渲染性能优化

(1)减少 DOM 元素数量

避免不必要的 DOM 元素,以减少渲染和重绘的时间

  1. 使用 CSS 替代 DOM 元素

    举例:

    <ul>
      <li><img src="icon1.png" alt="Icon 1"><span>Item 1</span></li>
      <li><img src="icon2.png" alt="Icon 2"><span>Item 2</span></li>
    </ul>
    

    优化为

    <ul>
      <li class="item">Item 1</li>
      <li class="item">Item 2</li>
    </ul>
    
    <style>
    .item::before {
      content: "";
      display: inline-block;
      width: 20px;
      height: 20px;
      background-image: url(icon-based-on-class.png);
      background-size: cover;
      margin-right: 5px;
    }
    </style>
    
  2. 引入 Flex 布局与 Grid 布局

    举例:

    <div class="container">
      <div class="row">
        <div class="col">Item 1</div>
        <div class="col">Item 2</div>
      </div>
    </div>
    

    优化为

    <div style="display: flex;">
      <div>Item 1</div>
      <div>Item 2</div>
    </div>
    
  3. 多个动态内容采用 DocumentFragment 添加

    举例:

    for (let i = 0; i < 100; i++) {
      let li = document.createElement("li");
      li.textContent = `Item ${i}`;
      document.querySelector("ul").appendChild(li);
    }
    

    优化为

    let fragment = document.createDocumentFragment();
    for (let i = 0; i < 100; i++) {
      let li = document.createElement("li");
      li.textContent = `Item ${i}`;
      fragment.appendChild(li);
    }
    document.querySelector("ul").appendChild(fragment);
    

    DocumentFragment 是一个轻量级的文档对象,可以包含节点和子节点,但不会成为文档树的一部分

(2)事件委托

通过事件委托来减少与 DOM 的交互次数,提高性能

  • 事件委托:一种事件处理的技术,它利用
    事件冒泡
    的原理,只在父元素上设置一个事件监听器,而不是在每个子元素上分别设置。当子元素上发生事件时,该事件会冒泡到父元素,父元素上的事件监听器会检查事件源(即触发事件的子元素),并据此执行相应的操作

  • 举例

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
      <meta charset="UTF-8" />
    </head>
    
    <body>
      <ul>
        <li>Item 1</li>
        <li>Item 2</li>
      </ul>
      <button>Add item</button>
      <script>
        const ul = document.querySelector("ul");
    
        ul.addEventListener("click", (e) => {
          if (e.target.tagName === "LI") {
            console.log("e.target.textContent", e.target.textContent);
            e.target.remove();
          }
        });
    
        const button = document.querySelector("button");
        button.addEventListener("click", () => {
          const newItem = document.createElement("li");
          newItem.textContent = `Item ${ul.children.length + 1}`;
          ul.appendChild(newItem);
        });
      </script>
    </body>
    
    </html>
    

(3)优化 DOM 操作

使用批量更新、虚拟 DOM 等技术减少重绘和回流

  1. 批量更新
    :将多个 DOM 操作合并成一个批次,然后一次性执行(参考 DocumentFragment)
  2. 虚拟 DOM
    :一种编程概念,用 JavaScript 对象来表示 DOM 树
    • 当应用程序的状态发生变化时,虚拟 DOM 树会首先进行更新,然后使用高效的算法(如 diff 算法)来比较新旧虚拟 DOM 树之间的差异,并只将这些差异应用到真实的 DOM 上
    • 即检测发生变化的 DOM 元素,并仅对变化的 DOM 元素操作,减少不必要的 DOM 操作
    • 一般在 React、Vue 等前端框架中广泛应用

(4)CSS 放在顶部

  • 将CSS放在
    <head>
    标签内,以确保页面在加载时能够优先渲染样式

    <head>
      <link rel="stylesheet" href="style.css" />
    </head>
    

(5)异步加载 JavaScript

  • 将非首屏必需的 JS 脚本放在文档末尾或使用
    async

    defer
    属性,避免阻塞渲染

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
      <!-- 同步加载首屏必需脚本 -->
      <script src="critical.js"></script>
    </head>
    
    <body>
      <!-- 页面内容 -->
    
      <!-- 异步加载非首屏且无依赖的脚本 -->
      <script async src="non-critical-async.js"></script>
    
      <!-- 异步加载但按顺序执行的非首屏脚本 -->
      <script defer src="non-critical-defer.js"></script>
    
      <!-- 将非首屏脚本放在底部(适用于不支持 async/defer 的老旧浏览器) -->
      <!-- <script src="non-critical-old-browser.js"></script> -->
    </body>
    
    </html>
    
  • async

    defer


    • 两者都用于异步加载脚本
    • async
      :完全异步,脚本的加载和执行不会阻塞文档的解析,但多个异步脚本之间执行顺序不一定,不建议用于具有依赖关系的脚本
    • defer
      :等到整个文档都被解析和显示之后,再按照脚本在文档中出现的顺序来执行

0x04 网络性能优化

(1)启用 HTTP/2 或 HTTP/3

  • 启用 HTTP/2 或 HTTP/3,利用多路复用、头部压缩等特性提升请求效率
  • 一般在 Web 服务器中,通过 Nginx、Apache 等配置并启用

(2)TLS/SSL

(3)预加载和预读取

a. 预加载

  • 预加载:一种资源提示,它告诉浏览器这个资源对于当前导航
    立即
    需要,并且应该被
    优先
    下载和解析


    • 如字体、CSS 和关键 JavaScript 脚本
  • 使用
    <link rel="preload">
    预加载关键资源

    <head>
      <!-- 预加载字体 -->
      <link rel="preload" href="fonts/myfont.woff2" as="font" type="font/woff2" crossorigin="anonymous">
    
      <!-- 预加载CSS -->
      <link rel="preload" href="styles/main.css" as="style">
    
      <!-- 其他头部标签... -->
    </head>
    

b. 预读取

  • 预读取:一种资源提示,它告诉浏览器这个资源
    可能
    会在将来的导航中被用到,但不像预加载那样具有紧迫性


    • 浏览器可以选择在空闲时间下载这些资源,以便在用户实际需要它们时能够更快地加载
  • 使用
    <link rel="prefetch">
    预读取可能需要的未来资源

    <link rel="prefetch" href="details/product-images.jpg">
    <link rel="prefetch" href="details/product-details.js"> 
    

(4)本地缓存

  • 存储限制:存储大小限制在 4KB 左右,且存储数量有限制
  • 缺点:
    1. 每次都会将 Cookie 数据携带在 HTTP 请求中,可能带来
      性能问题

      占用带宽
    2. 安全性较低
  • 由于浏览器的跨域限制,客户端和服务端必须保证同源原则
  • 场景:跟踪用户会话信息,如用户登录状态、购物车信息等
  • Session:以键值对的方式将缓存数据保存在服务器中,并把键值(Session ID)作为 Cookie 返给浏览器
  • Token:应对移动互联网不提供 Cookie 的解决方案,将数据哈希加密并保存在移动端的存储系统中
    • JWT 是一种广泛应用的 Token 标准

b. LocalStorage

  • HTML5 提供的一种新的本地缓存方案,用于存储数据在用户的浏览器中
  • 存储限制:
    • 长久保存,无有效期,直到手动删除或浏览器清理缓存为止
    • 存储空间一般可以达到 5MB 及以上(不同的浏览器有所区别)
  • 场景:存储需要在多个页面或会话中持久保存的数据,如用户偏好设置、游戏进度等

c. SessionStorage

  • 大体与 LocalStorage 类似,在存储限制上,数据仅在当前会话期间有效,浏览器关闭或标签页关闭后数据即被清除
  • 场景:存储仅在当前会话中需要的数据,如临时状态信息、表单数据等

d. IndexedDB

  • 一个低级的 API,允许进行复杂的查询、事务处理和数据库管理操作,提供索引功能
  • 存储限制:存储空间相对较大,可以存储大量数据
  • 场景:存储大量结构化数据并需要进行复杂查询,如离线应用、游戏数据存储等

e. Cache API

  • 一种用于存储和检索网络请求的响应的接口

  • 可以与 Service Workers 结合使用,实现离线应用和性能优化


    Service Workers:在 Web 浏览器中运行的脚本,具备在后台独立于网页运行的能力

    • 提供很多高级功能,如离线内容缓存、推送通知、背景数据同步等
  • 场景:精确控制缓存策略和资源缓存,如构建 PWA(Progressive Web Apps)时

0x05 框架与库的选择与优化

(1)框架

  • 轻量级


    • Preact
      :3kb 大小的 React 轻量、快速替代方案,拥有相同的现代 API
    • Vue3
      :在 Vue2 基础上,应用
      Tree-shaking
      ,允许在构建过程中自动移除未使用的代码
  • 按需加载


    • 采用
      代码分割

      懒加载
      技术,将应用拆分成多个小块,并在需要时才加载它们

    • 以 Vue Router 为例,Vue Router 支持动态导入

      const routes = [
        {
          path: "/products",
          name: "Products",
          // 使用动态导入来懒加载组件
          component: () =>
            import(/* webpackChunkName: "products" */ "./views/Products.vue"),
        },
        // 其他路由...
      ];
      

(2)第三方库

  • 仅引入必要的库,避免过时或冗余的库,定期检查更新以利用性能优化

0x06 性能监控与优化工具

(1)性能分析工具

  • Lighthouse:Google 开发的一款开源自动化工具,集成在 Chrome DevTools 中
  • PageSpeed Insights:Google 提供的一款免费工具,
    官网链接
  • Chrome DevTools Performance 面板:集成在 Chrome DevTools 中

(2)用户性能监控

采用集成 RUM(Real User Monitoring)工具收集实际用户的加载性能数据

  • Google Analytics:使用插件 Google Tag Manager 来部署 RUM 脚本,该脚本将收集用户交互数据并发送到 Google Analytics 进行处理和分析
  • SpeedCurve:
    https://www.speedcurve.com/

0x07 其他优化策略

(1)首屏内容优化

  • 确保首屏加载时立即展示关键内容,避免用户看到空白或加载指示器过久
  • 举例:电商网站首屏优化操作
    • 精简首屏内容
      :保留最关键的内容,如网站Logo、欢迎语、轮播图、商品推荐
    • 优化图片和脚本
      :压缩和优化所有首屏加载的图片,合并和压缩 CSS 和 JavaScript 脚本进行,减少HTTP请求次数
    • 异步加载非关键内容
      :非首屏关键内容设置为异步加载,即用户滚动到相应位置时再加载这些内容
    • 使用CDN加速
      :将网站内容分发到全球多个 CDN 节点,根据用户地理位置选择最近的节点进行加载
    • 预加载和缓存
      :利用浏览器的预加载和缓存机制,提前加载和存储一些常用的资源文件

(2)语义化 HTML

  • 使用合理 HTML 标记以及其特有的属性去格式化文档内容,提高内容可理解性

  • 详细方法参考:
    HTML语义化 | CSDN-北航程序员小陈

  • 举例:

    <!DOCTYPE html>
    <html lang="zh-CN">
    
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>新闻文章标题</title>
    </head>
    
    <body>
      <header>
        <h1>新闻文章标题</h1>
        <p>由 <a href="/author-profile">作者姓名</a> 发表于 <time datetime="2023-04-01">2023年4月1日</time></p>
      </header>
    
      <main>
        <article>
          <h2>引言</h2>
          <p>这里是引言部分的内容,简要介绍文章的主题和背景。</p>
    
          <h2>正文标题</h2>
          <p>这里是正文的第一段,详细阐述文章的主要观点或故事。</p>
    
          <h3>小节标题</h3>
          <p>这里是文章中的一个小节,进一步细化或支持主要观点。</p>
    
          <!-- 可以继续添加更多的h2、h3、p等元素来构建文章内容 -->
    
          <footer>
            <p>文章结束。</p>
          </footer>
        </article>
      </main>
    
      <aside>
        <h2>相关文章</h2>
        <ul>
          <li><a href="/article1">相关文章1</a></li>
          <li><a href="/article2">相关文章2</a></li>
          <!-- 更多相关文章链接 -->
        </ul>
      </aside>
    
      <footer>
        <p>版权所有 &copy; 2023 网站名称</p>
      </footer>
    </body>
    
    </html>
    

(3)元数据

  • 设置
    <title>

    <meta>

    <link rel="canonical">
    等 SEO 相关标签

  • 举例:

    <!DOCTYPE html>
    <html lang="zh-CN">
    
    <head>
      <!-- 页面编码,用于处理不同语言的字符串 -->
      <meta charset="UTF-8" />
    
      <!-- 设置视口,确保网页在不同设备上正确显示 -->
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    
      <!-- 页面标题,显示在浏览器标签和搜索结果中 -->
      <title>页面标题 - 网站名称</title>
    
      <!-- 页面描述,显示在搜索结果中,用于概括页面内容 -->
      <meta name="description" content="这里是页面的简短描述,应包含关键词并吸引用户点击。" />
    
      <!-- 页面关键词,虽然现代搜索引擎对keywords标签的重视程度降低,但仍可作为参考 -->
      <meta name="keywords" content="关键词1, 关键词2, 关键词3" />
    
      <!-- 指定页面的规范URL,有助于防止内容重复被搜索引擎索引 -->
      <link rel="canonical" href="https://www.example.com/your-page-url" />
    
      <!-- 其他可能的元数据 -->
      <meta name="author" content="作者姓名或组织" /> <!-- 添加作者信息 -->
      <meta name="robots" content="index, follow" /> <!-- 指示搜索引擎索引并跟踪页面上的链接 -->
    
      <!-- 对于响应式网站,可以使用meta标签来适应不同设备 -->
      <meta name="HandheldFriendly" content="true" /> <!-- 告诉移动设备,页面适合于手机浏览 -->
      <meta name="MobileOptimized" content="320" /> <!-- 指定移动设备的屏幕宽度,以适应响应式设计 -->
    
      <!-- 引入CSS样式 -->
      <link rel="stylesheet" href="style.css" />
    </head>
    
    <body>
      <!-- 页面内容 -->
      <!-- 引入JavaScript脚本 -->
      <script src="script.js"></script>
    </body>
    
    </html>
    

(4)结构化数据

  • 结构化数据:一种使用特定格式(如 JSON-LD、Microdata 或 RDFa)来标记网页内容的方式,以便搜索引擎和其他机器能够更容易地理解和处理这些信息

  • 举例:以下是采用 Schema.org 和JSON-LD 格式的结构化数据

    <!DOCTYPE html>
    <html lang="zh-CN">
    
    <head>
      <meta charset="UTF-8" />
      <title>电影《星际穿越》</title>
      <script type="application/ld+json">
        {  
          "@context": "https://schema.org/",  
          "@type": "Movie",  
          "name": "星际穿越",  
          "image": "https://example.com/movie-poster.jpg",  
          "director": {  
            "@type": "Person",  
            "name": "克里斯托弗·诺兰"  
          },  
          "genre": ["科幻", "剧情", "冒险"],  
          "actor": [  
            {  
              "@type": "Person",  
              "name": "马修·麦康纳希"  
            },  
            {  
              "@type": "Person",  
              "name": "安妮·海瑟薇"  
            }  
          ],  
          "datePublished": "2014-11-07",  
          "description": "一队探险家利用他们针对虫洞的新发现,超越人类对于太空旅行的极限,从而开始在广袤的宇宙中进行星际航行的故事。",  
          "aggregateRating": {  
            "@type": "AggregateRating",  
            "ratingValue": "8.7",  
            "reviewCount": "123456"  
          }  
        }  
        </script>
    </head>
    
    <body>
      <!-- 网页内容 -->
      <h1>电影《星际穿越》</h1>
      <p>导演:克里斯托弗·诺兰</p>
      <p>主演:马修·麦康纳希, 安妮·海瑟薇</p>
      <p>类型:科幻, 剧情, 冒险</p>
      <p>上映日期: 2014年11月7日</p>
      <p>剧情简介:...(详细描述)</p>
    </body>
    
    </html>
    

(5)无障碍性(a11y)

  • 无障碍性:确保网站对所有用户,包括残障用户,都能够友好地访问和使用
  • WCAG 标准
    :Web 内容无障碍指南
  • 举例:
    1. 非文本内容的文本替代
      • 如:
        <img alt="这是一张图片" />
    2. 键盘可访问
    3. 清晰和一致的导航
    4. 足够的颜色对比度
    5. 字幕和音频描述

-End-