当前位置: 首页 > news >正文

实现一个小程序分享图 wxml2canvas

我们经常会遇上动态生成海报的需求,而在小程序中,生成图片非Canvas莫属。但是在实际工作当中,为了追求效率,我们会不可避免地去使用一些JS插件,而 wxml-to-canvas 就是一款官方推荐且非常优秀的插件,它可以轻松地帮你将HTML代码转换成Canvas,进而生成可保存分享的图片。

但是wxml-to-canvas是通过静态模板和样式绘制 canvas ,进而导出图片,需要单独写一份静态模板用于编译,对于很多场景还是有些限制,比如有时需要将图文混排的富文本内容生成分享图,对于这种长度不定,内容动态变化的图片生成需求,直接利用官方的canvas接口绘制是十分困难的,包括但不限于文字换行、表情文字图片混排、文字加粗、子标题等元素都需要一一绘制

我们的目标是实现一个通过wxml节点标记,收集元素从而进行编译转换,仅依赖wxml直出需要绘制的canvas进而快速实现图片分享,为此学习了 wxml2canvas npm包,实现了基础功能的wxml2canvas

  • 此文暂不讨论图片类型的wxmlcanvas

先看结果🌰

我们针对简单的demo进行处理,包含了对块级元素行内元素背景色、等简单样式的转换,达到 wxml->canvas->image 的一次性处理,且不需要重复书写静态代码模板进行编译

下面让我们一步步探求如何实现这个tiny版的wxml2canvas

实现

小程序提供了如下特性,可供我们便捷使用:

  • measureText接口能直接测量出文本的宽度;
  • SelectorQuery可以查询到节点对应的computedStyle。

同时小程序也存在一些弊端,比如:

  • canvas属于原生组件,在移动端会置于最顶层;
  • 通过SelectorQuery只能拿到节点的style,而无法获取文本节点的内容

所以我们第一步获取元素就面临两个问题:

1.如何获取需要转成canvas的元素
2.如何拿到获取元素的对应属性(样式、节点、内容…)

当获取到待收集的元素后,就可以将元素绘制到指定的canvas上,也就实现了wxml2canvas

所以初始化时,需要传入如下参数

/*** element:需要渲染的canvas节点* class:查找所有类名为exc-c的节点,并进行加入绘制队列* limit:限定相对位置 ***/
wxml2Canvas({element: "over-canvas",options: {class: ".exc-c",limit: ".limit-r",},
}) 

在wxml中需要绘制的元素需添加 exc-c 类名,方便对元素进行查找,并且如果需要限定相对位置,也需要在父级添加 limit-r 类名,例如,一个文本的位置(left, top) = (50, 80),class为panel的节点的位置为(left, top) = (20, 40),则文本canvas上实际绘制的位置(x, y) = (50 - 20, 80 -40) = (30, 40)。如果不传入limit,则以实际的位置(x, y) = (50, 80) 绘制

对于 wxml 而言,如有需要渲染的文本,也需要将文本内容通过 data-text="xxx" 属性的方式进行挂载,因为 SelectorQuery 无法获取文本节点的内容但是可以获取到节点的 dataset 属性,从而拿到文本内容

wxml 代码如下:

 <view class="content">Wxml:</view><view class="-box limit-r"><view class="-line exc-c" data-type="inline-text" data-text="这是{{txt}}">这是{{txt}}</view><view class="-line -blue exc-c" data-type="inline-text" data-text="这是蓝色行内文字试试">这是蓝色行内文字试试</view><view class="-line exc-c" data-type="inline-text" data-text="这是换行的行内行内行内行内行内行内行内行内行内行行内行内内文字">这是换行的行内行内行内行内行内行行内行内内行内行内行内行内文字</view><view class="-line -red exc-c" data-type="inline-text" data-text="这是红色行内红色行内红色行内红色行内文字试试">这是红色行内红色行内红色行内红色行内文字试试</view><view class="-il exc-c" data-type="text" data-text="这是无边距文字">这是无边距文字</view><view class="-il-2 exc-c" data-type="text" data-text="这是margin文字">这是margin文字</view><view class="-il-3 exc-c" data-type="text" data-text="这是padding文字">这是padding文字</view><view class="-il-4 exc-c" data-type="text" data-text="这是居中有背景文字">这是居中有背景文字</view><view class="-il-5 exc-c" data-type="text" data-text="这是背景文字" data-background="rgba(255, 0, 0, 0.4)" data-padding="0 0 0 0">这是背景文字</view></view><view class="content">Canvas:</view><canvas canvas-id="over-canvas" class="-box"></canvas> 

上面是需要配置的代码,那下面让我看看如何具体实现:

1.配置基础的样式 如:字体大小、颜色、边距,并返回一个函数用来追加配置
2.解析元素 拿到所有需要绘制的元素以及相对的父级元素
3.按照是否涉及换行进行分类绘制 (块级元素 / 行内元素)
4.全部绘制结束时进行 resolve

const Wxml2Canvas = config => {return new Promise(async (resolve, reject) => {const appendSet = setInit(config); // 配置基础const [render, limit] = await getWxml(config.options); // 获取待绘制元素、限制区域const [block, inlineTmp] = sortListByTop(render); // 划分块级元素 \ 行内元素appendSet({ limit }); // 追加配置Promise.all([...drawB(block), drawL(inlineTmp)]).then(resolve).catch(reject);});
}; 

初始化

在上面代码中,通过 setInit 设置 canvas 的基本属性,并将可配置的默认参数(宽度、字体大小、字体颜色…)以及 ctx 维护成一个对外暴露的公共对象,最后返回一个函数,供使用者更新配置

const DEFAULT_CONFIG = {width: 340,TOP: "top",FONT_SIZE: "14",PADDING: "0 0 0 0",FONT_COL: "#454545",SHADOW_COL: "#ffffff",background: "#ffffff",font: "14px PingFang SC",STROKECOLOR: "white",
};export const CACHE_INFO = {};export const setInit = (config = {}) => {const info = { ...DEFAULT_CONFIG, ...config };const { background, font, width, height, TOP, STROKECOLOR } = info;CACHE_INFO.options = info;CACHE_INFO.ctx = wx.createCanvasContext(info.element, info._this);CACHE_INFO.ctx.font = font;CACHE_INFO.ctx.setTextBaseline(TOP);CACHE_INFO.ctx.setStrokeStyle(STROKECOLOR); // 设置基本样式drawRectToCanvas(0, 0, width, height, { fill: background });return arg => Object.keys(arg).forEach(i => (CACHE_INFO.options[i] = arg[i]));
}; 

获取元素

getWxml 中,通过小程序提供的 createSelectorQuery 获取到需要绘制元素的节点信息,同时也获取父级相对元素的节点信息,作为 limit 界定绘制元素的边界

/*** @param { object } item 待处理的节点* @returns array 解析后的节点信息*/
export const getWxml = item => {const { options } = CACHE_INFO;const { _this, width } = options;const query = _this? wx.createSelectorQuery().in(_this): wx.createSelectorQuery();const render = new Promise(resolve => {query.selectAll(item.class).fields({dataset: true,size: true,rect: true,computedStyle: COMPUT_STYLE,},res => resolve(res)).exec();});const limit = new Promise(resolve => {if (!item.limit) resolve({ top: 0, width });query.select(item.limit).fields({dataset: true,size: true,rect: true,},res => resolve(res)).exec();});return Promise.all([render, limit]);
}; 

其中 COMPUT_STYLE 规定了需要查找的元素样式名称:

export const COMPUT_STYLE = ["width","height","font","fontSize","fontFamily","fontWeight","fontStyle","textAlign","color","lineHeight","border","borderColor","borderStyle","borderWidth","verticalAlign","boxShadow","background","backgroundColor","backgroundImage","backgroundPosition","backgroundSize","paddingLeft","paddingTop","paddingRight","paddingBottom",
]; 

获取到相对父级元素,将获取的节点作为边界信息更新到配置中

appendSet({ limit }); // 追加配置 

获取到的元素可以分为两类 即:

  • 涉及文字换行的元素 (行内元素)
  • 不涉及文字换行的元素 (块级元素)

我们可以通过在书写 wxml 时,通过事先声明 data-type="inline-text" ,这样获取到节点之后,可以通过 dataset 获取元素的类型,如果为行内元素时,需要将行内元素通过高度进行分层,将同一行的元素进行归类

/*** @param { array } list 待处理的节点* @returns array*/
export const sortListByTop = list => {const [arrBlock, arrLine, lineTemp] = [[], [], {}];list.forEach(i => {if (i.dataset.type && i.dataset.type.indexOf("inline") == -1) {arrBlock.push(i);} else {arrLine.push(i);}});arrLine.forEach(i => {lineTemp[i.top] = lineTemp[i.top] || [];lineTemp[i.top].push(i);});return [arrBlock, lineTemp];
}; 

处理元素

通过 drawWxmlBlockdrawWxmlInline 分别对元素进行处理和绘制,在此之前还需要 drawAfter 方法将节点信息(文字、位置…) + 元素的样式信息(padding、width…) 转化为对应的 canvas 位置信息:

drawAfter:

/*** 返回节点的真实位置** @param { object } el 需要渲染的节点* @param {*} leftOffset 从左侧开始绘制的起点* @param {*} maxWidth 一行文本的最大宽度* @returns 返回canvas位置*/
const drawAfter = (el, leftOffset, maxWidth) => {const { options } = CACHE_INFO;const { left: limitLeft = 0, top: limitTop = 0 } = options.limit;const leftFix = +el.dataset.left || 0;const topFix = +el.dataset.top || 0;el.width = transferNum(el.width);el.height = transferNum(el.height);el.left = transferNum(el.left) - limitLeft + leftFix;el.top = transferNum(el.top) - limitTop + topFix;let padding = el.dataset.padding || options.PADDING;if (typeof padding === "string") {padding = transferPadding(padding);}const paddingTop = +el.paddingTop.replace("px", "") + +padding[0];const paddingRight = +el.paddingRight.replace("px", "") + +padding[1];const paddingBottom = +el.paddingBottom.replace("px", "") + +padding[2];const paddingLeft = +el.paddingLeft.replace("px", "") + +padding[3];el.padding = [paddingTop, paddingRight, paddingBottom, paddingLeft];const text = el.dataset.text || "";el.background = el.dataset.background || el.backgroundColor;return {text,x: leftOffset || el.left,y: el.top,originX: el.left,...(leftOffset && { leftOffset }),...(maxWidth && { maxWidth }),};
}; 

drawWxmlBlock:

对于块级元素,将转为 canvas 位置信息的节点依次绘制,通过 Promise 将绘制完成的结果进行返回

/*** 绘制块级元素* @param { array } block 需要绘制的块级元素*/
export const drawWxmlBlock = (block = []) => {return block.map(el =>new Promise((resolve, reject) => {const textData = drawAfter(el);drawText(textData, el, "text", resolve, reject,);}));
}; 

drawWxmlInline:

对于行内元素,首先通过同一个行元素的左右边距计算出一行的最大宽度,用于换行,并且记录距离左侧的边距 leftOffset ,每次绘制完更新下一次绘制的起点,从上次结束的位置继续绘制,全部绘制完成后进行 resolve (有些贪心算法的影子)

/*** 绘制行内元素* @param { object } inline 需要绘制的行内元素*/
export const drawWxmlInline = (inline = {}) => {let leftOffset = 0;return new Promise(resolve => {let maxWidth = 0;let minLeft = Infinity;let maxRight = -Infinity;Object.keys(inline).forEach(top => {inline[top].forEach(el => {minLeft = Math.min(el.left, minLeft);maxRight = Math.max(el.right, maxRight);});});// 找出同一top下的最小left和最大right,得到最大的宽度,用于换行maxWidth = Math.ceil(maxRight - minLeft);Object.keys(inline).forEach(top => {inline[top].forEach(el => {const textData = drawAfter(el, leftfOfset, maxWidth);const drawRes = drawText(textData, el, "inline");leftOffset = drawRes.leftOffset; // 每次绘制从上次结束地方开始});});resolve();});
}; 

渲染节点

无论是块级元素还是行内元素都是通过 drawText 方法最终渲染节点到指定的 canvas 中,只不过传参会发生变化,块级元素 每次绘制都是独立的,所以每次绘制成功之后会触发 resolve 的回调,但是行内元素,每次绘制需要返回当前绘制结束之后的定位,以此作为下次绘制的起始位置。

块级元素:

/*** 绘制文字 * @param { object } textData 节点位置* @param { object } el 节点信息* @param { string } type 渲染节点类型* @param { function } resolve 成功时候抛出* @param { function } reject 失败时候抛出*/
const DrawTxt = (textData, el, type, resolve, reject) => {const { ctx, options } = CACHE_INFO;try {...[x, y] = setTxtAlign(textData, el, textWidth, width);ctx.fillText(textData.text, x, y);ctx.draw(true);resolve();} catch (e) {reject && reject(e);}
}; 

以上对于块级元素时,我们只需要结合 textAlignpadding 就可以计算出 x 的值,通过 行数 * 行高padding 计算出 y 的值,最后通过 ctx.fillText 进行绘制

行内元素:

/*** 绘制文字 * @param { object } textData 节点位置* @param { object } el 节点信息*/
const DrawTxt = (textData, el) => {const { ctx, options } = CACHE_INFO;...// 元素换行的情况:const maxw = textData.maxWidth; // 最大宽度let lineNum = Math.max(Math.floor(textWidth / maxw), 1); // 最大行数const singleLength = Math.floor(text.length / lineNum); // 每行字数const widthOffset = textData.leftOffset - textData.originX; // 计算真实的左边距let [endIdx, fSingle, fsWidth] = getTextSingleLine(text, maxw, singleLength, 0, widthOffset);[x, y] = setTxtAlign(textData, el, fsWidth);ctx.fillText(fSingle, x, y); // 绘制leftOffset = x + fsWidth; // 更新左边距topOffset = y; // 更新右边距...ctx.draw(true);return { leftOffset, topOffset }
}; 

上面行内元素,需要关心的是 getTextSingleLine 方法,即计算截取换行文字的索引和位置,通过 offset 矫正每行的真实文本数,如果 真实文本距离 + 左边距 > 一行最大长度 则不断进行截取,并且更新一行的文本字数,直到不超过一行最大长度时,返回截取的文字索引、截取的文字、截取后的文本长度

getTextSingleLine:

/*** 当文本超过宽度时,计算每一行应该绘制的文本** @param {*} text 需要绘制的文字* @param {*} width 一行最大长度* @param {*} singleLength 每行实际字数* @param {*} currentIndex 文字的起始位置索引* @param {*} widthOffset 左边距*/
export const getTextSingleLine = ( text,width,singleLength,currentIndex = 0,widthOffset = 0 ) => {let offset = 0;let endIndex = currentIndex + singleLength + offset;let single = text.substring(currentIndex, endIndex);let singleWidth = measureWidth(single);while (Math.round(widthOffset + singleWidth) > width) {offset -= 1;endIndex = currentIndex + singleLength + offset;single = text.substring(currentIndex, endIndex);singleWidth = measureWidth(single);}return [endIndex, single, singleWidth];
}; 

最后

最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

相关文章:

实现一个小程序分享图 wxml2canvas

我们经常会遇上动态生成海报的需求&#xff0c;而在小程序中&#xff0c;生成图片非Canvas莫属。但是在实际工作当中&#xff0c;为了追求效率&#xff0c;我们会不可避免地去使用一些JS插件&#xff0c;而 wxml-to-canvas 就是一款官方推荐且非常优秀的插件&#xff0c;它可以…...

基于matlab设计x波段机载SAR系统

一、前言此示例说明如何设计在 X 波段工作的合成孔径雷达 &#xff08;SAR&#xff09; 传感器并计算传感器参数。SAR利用雷达天线在目标区域上的运动来提供目标区域的图像。当SAR平台在目标区域上空行进时&#xff0c;当脉冲从雷达天线发送和接收时&#xff0c;会产生合成孔径…...

WPF学习:Slider — 冒泡显示值

想做一个下图所示的Slider&#xff0c;以冒泡的方式显示其Value值&#xff0c;该怎么做呢&#xff1f; 功能要求&#xff0c;当鼠标放在滑块上的时候&#xff0c;冒“泡”显示值&#xff1b;当滑块移动的时候&#xff0c;“泡”跟随移动。 看似简单的功能&#xff0c;但要完美…...

Vue实战第4章:主页设计之中部内容设计

前言 本篇在讲什么 接上篇文章&#xff0c;我们制作了一个自定义的网页导航栏&#xff0c;本篇文章我们简单制作一个内容页 仅介绍简单的应用&#xff0c;仅供参考 本篇适合什么 适合初学Vue的小白 适合想要自己搭建网站的新手 适合没有接触过vue-router的前端程序 本篇…...

数据结构代码总结(C语言实现)

目录如何应对数据结构的代码题&#xff1f;采取的学习流程①首先对C语言的语法的熟悉②学习掌握基本代码的写法&#xff0c;做到熟练2.1插入排序2.2快速排序2.3二分查找2.4树的遍历③跟着网上视频开始熟悉对一些问题的解答④结合真题的代码&#xff0c;寻找其中的结题规律如何应…...

zookeeper 复习 ---- chapter04

zookeeper 复习 ---- chapter04zookeeper 的精髓是什么&#xff1f; 1&#xff1a;它有四个节点类型 持久无序的节点 临时无序的节点 持久有序的节点 临时有序的节点 临时的节点的特征&#xff1a;当客户端和服务器端断开连接&#xff0c;当前客户端创建的节点被服务器端自动删…...

thinkphp6.0连接MYSQL

目录8.连接多个数据库7.多级控制器不存在6.分页5.非法请求4.关于路由**3.初体验页面****2.加入fileheader添加注释****1.配置mysql0. 官方开发手册一些网址 http://127.0.0.1:8000/index 原桌面 http://127.0.0.1:8000/hello/fsh hello,fsh&#xff08;index中hello方法&#x…...

商家必读!超店有数分享,tiktok达人营销变现如何更快一步?

近几年来&#xff0c;“粉丝经济”发展越来越迅猛&#xff0c;“网红带货”已经成为了一种营销的方式。这种方式让商家能基于达人的影响下迅速抢占自己的私域流量池。消费者会基于对达人的信任&#xff0c;购买达人推荐的产品。达人效应可以助力品牌走出营销困境。如果商家想要…...

操作系统(day11)--快表,两级页表

具有快表的地址变换机构 时间局限性&#xff1a;会有大量连续的指令需要访问同一个内存块的数据的情况&#xff08;程序中的循环&#xff09; 空间局限性&#xff1a;一旦程序访问了某个存储单元&#xff0c;在不久之后&#xff0c;其附近的存储单元也很有可能被访问。&#xf…...

预告| 亮点抢先看!第四届OpenI/O启智开发者大会主论坛24日启幕!

2023年2月24日至25日&#xff0c;第四届OpenI/O启智开发者大会将在深圳隆重举行。“算网筑基、开源启智、AI赋能”作为今年大会的主题&#xff0c;吸引了全球业界关注的目光。大会集结中国算力网资源基座、开源社区治理及AI开源生态建设、国家级开放创新应用平台、NLP大模型等前…...

猪齿鱼(Choerodon UI )的通用提交的封装 —— 两种方案,A.使用dataSet的自身的submit,B.使用axios.post来提交

submit组件&#xff08;otherSubmit/axiosSubmit&#xff09; 一、背景与简介 1、首先我们申请表提交&#xff0c;分为【保存】提交与【其他】提交&#xff1b; 1.1【保存】提交&#xff0c;要求表单必须要有变更&#xff0c;DataToJSON默认为dirty&#xff08;只转换变更的…...

CISCN(Web Ezpentest)GC、序列化、case when

目录 REGEXP的一个点&#xff08;正则&#xff09; like&#xff08;默认不区分大小写&#xff09; 当禁用了空格 regexp&#xff0c;like的区分大小写的使用方法 [CISCN 2022 初赛]ezpentest 卡点 2022 HFCTF babysql 最近又学到了一道新知识&#xff0c;case when的错…...

OSG三维渲染引擎编程学习之五十七:“第六章:OSG场景工作机制” 之 “6.1 OSG访问器”

目录 第六章 OSG场景工作机制 6.1 OSG访问器 6.1.1 访问器模式 6.1.2 osg::NodeVisitor 6.1.3 访问器示例...

Python3 输入和输出实例及演示

在前面几个章节中&#xff0c;我们其实已经接触了 Python 的输入输出的功能。本章节我们将具体介绍 Python 的输入输出。 输出格式美化 Python两种输出值的方式: 表达式语句和 print() 函数。 第3种方式是使用文件对象的 write() 方法&#xff0c;标准输出文件可以用 sys.std…...

召回-回忆录(持续更新)

0.召回方法 词召回 swing、itemCF 缺点&#xff1a; 有冷启动问题不是全局召回&#xff0c;冷门活动难以得到召回结果容易召回过多的头部热门活动 向量召回 参考文献&#xff1a; 经典推荐算法学习&#xff08;七&#xff09;| Graph Embedding技术学习 | 从DeepWalk到No…...

1243. 糖果/状态压缩dp【AcWing】

1243. 糖果 糖果店的老板一共有 M种口味的糖果出售。 为了方便描述&#xff0c;我们将 M种口味编号 1∼M。 小明希望能品尝到所有口味的糖果。 遗憾的是老板并不单独出售糖果&#xff0c;而是 K颗一包整包出售。 幸好糖果包装上注明了其中 K颗糖果的口味&#xff0c;所以小…...

【Spring Cloud Alibaba】001-单体架构与微服务架构

【Spring Cloud Alibaba】001-单体架构与微服务 文章目录【Spring Cloud Alibaba】001-单体架构与微服务一、单体架构1、单体应用与单体架构2、单体应用架构图3、单体架构优缺点优点缺点二、微服务1、微服务的“定义”2、微服务的特性3、微服务架构图4、微服务的优缺点优点缺点…...

Renderer 使用材质分析:materials、sharedMaterials 及 MaterialPropertyBlock

一、materials 与 sharedMaterials 1.1 使用上的区别与差异 Unity 开发时&#xff0c;在 C# 中通过 Renderer 取材质操作是非常常见的操作&#xff0c;Renderer 有两种常规获取材质的方式&#xff1a; sharedMaterials&#xff1a;可以理解这个就是原始材质&#xff0c;所有使…...

java学习----网络编程

网络编程入门 网络编程概述 计算机网络 ​ 计算机网络是指地理位置不同的具有独立功能的计算机及其外部设备&#xff0c;通过通信线路连接起来&#xff0c;在网络操作系统&#xff0c;网络管理软件及网络通信协议的管理协调下&#xff0c;实现资源共享和信息传递的计算机系统…...

这些「误区」99%的研发都踩过

意识不到误区的存在最为离谱&#xff1b; 01生活中&#xff0c;职场上&#xff0c;游戏里&#xff0c;都少不了正面对喷过&#xff1a;意识太差&#xff1b; 在个人的认知中意识即思维&#xff0c;意识太差即思维中存在的误区比较多&#xff1b; 每个人或多或少都存在思维上的…...

装饰模式(Decorator Pattern)重构java邮件发奖系统实战

前言 现在我们有个如下的需求&#xff0c;设计一个邮件发奖的小系统&#xff0c; 需求 1.数据验证 → 2. 敏感信息加密 → 3. 日志记录 → 4. 实际发送邮件 装饰器模式&#xff08;Decorator Pattern&#xff09;允许向一个现有的对象添加新的功能&#xff0c;同时又不改变其…...

多模态2025:技术路线“神仙打架”,视频生成冲上云霄

文&#xff5c;魏琳华 编&#xff5c;王一粟 一场大会&#xff0c;聚集了中国多模态大模型的“半壁江山”。 智源大会2025为期两天的论坛中&#xff0c;汇集了学界、创业公司和大厂等三方的热门选手&#xff0c;关于多模态的集中讨论达到了前所未有的热度。其中&#xff0c;…...

ES6从入门到精通:前言

ES6简介 ES6&#xff08;ECMAScript 2015&#xff09;是JavaScript语言的重大更新&#xff0c;引入了许多新特性&#xff0c;包括语法糖、新数据类型、模块化支持等&#xff0c;显著提升了开发效率和代码可维护性。 核心知识点概览 变量声明 let 和 const 取代 var&#xf…...

【WiFi帧结构】

文章目录 帧结构MAC头部管理帧 帧结构 Wi-Fi的帧分为三部分组成&#xff1a;MAC头部frame bodyFCS&#xff0c;其中MAC是固定格式的&#xff0c;frame body是可变长度。 MAC头部有frame control&#xff0c;duration&#xff0c;address1&#xff0c;address2&#xff0c;addre…...

mongodb源码分析session执行handleRequest命令find过程

mongo/transport/service_state_machine.cpp已经分析startSession创建ASIOSession过程&#xff0c;并且验证connection是否超过限制ASIOSession和connection是循环接受客户端命令&#xff0c;把数据流转换成Message&#xff0c;状态转变流程是&#xff1a;State::Created 》 St…...

可靠性+灵活性:电力载波技术在楼宇自控中的核心价值

可靠性灵活性&#xff1a;电力载波技术在楼宇自控中的核心价值 在智能楼宇的自动化控制中&#xff0c;电力载波技术&#xff08;PLC&#xff09;凭借其独特的优势&#xff0c;正成为构建高效、稳定、灵活系统的核心解决方案。它利用现有电力线路传输数据&#xff0c;无需额外布…...

AtCoder 第409​场初级竞赛 A~E题解

A Conflict 【题目链接】 原题链接&#xff1a;A - Conflict 【考点】 枚举 【题目大意】 找到是否有两人都想要的物品。 【解析】 遍历两端字符串&#xff0c;只有在同时为 o 时输出 Yes 并结束程序&#xff0c;否则输出 No。 【难度】 GESP三级 【代码参考】 #i…...

页面渲染流程与性能优化

页面渲染流程与性能优化详解&#xff08;完整版&#xff09; 一、现代浏览器渲染流程&#xff08;详细说明&#xff09; 1. 构建DOM树 浏览器接收到HTML文档后&#xff0c;会逐步解析并构建DOM&#xff08;Document Object Model&#xff09;树。具体过程如下&#xff1a; (…...

大模型多显卡多服务器并行计算方法与实践指南

一、分布式训练概述 大规模语言模型的训练通常需要分布式计算技术,以解决单机资源不足的问题。分布式训练主要分为两种模式: 数据并行:将数据分片到不同设备,每个设备拥有完整的模型副本 模型并行:将模型分割到不同设备,每个设备处理部分模型计算 现代大模型训练通常结合…...

精益数据分析(97/126):邮件营销与用户参与度的关键指标优化指南

精益数据分析&#xff08;97/126&#xff09;&#xff1a;邮件营销与用户参与度的关键指标优化指南 在数字化营销时代&#xff0c;邮件列表效度、用户参与度和网站性能等指标往往决定着创业公司的增长成败。今天&#xff0c;我们将深入解析邮件打开率、网站可用性、页面参与时…...