前端老铁别慌:img标签src调API返回base64字符串的土法炼钢指南

前端老铁别慌:img标签src调API返回base64字符串的土法炼钢指南
在这里插入图片描述


前端老铁别慌:img标签src调API返回base64字符串的土法炼钢指南

前端老铁别慌:img标签src调API返回base64字符串的土法炼钢指南

开篇先唠两句这破事儿咋来的

兄弟们,今天咱们聊一个让无数前端人半夜惊醒的噩梦——后端那帮大哥非要给你塞一串长得离谱的base64字符串,还拍着胸脯说"这样省事"。

我就纳了闷了,这年头CDN这么便宜,OSS存储跟不要钱似的,他们偏不,非得把图片转成那种密密麻麻的字符,往JSON里一塞,跟扔垃圾似的甩给你。你打开Network面板一看,好家伙,一个接口返回的数据包体积直接干到几十MB,里面全是这种玩意儿:

/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k= 

这串东西你看着眼熟不?眼熟就对了,说明你也被坑过。我第一次遇到这情况的时候,直接把这串东西往img标签的src里一贴,浏览器当场就给我表演了一个"页面未响应"。用户在那边疯狂点击,以为我网站挂了,实际上我的JavaScript主线程正在那吭哧吭哧地解码这坨东西,CPU占用率直接飙到100%,风扇转得跟直升机起飞似的。

所以啊,今天这篇不是那种"Base64编码原理详解"的学院派文章,咱不搞那些虚的。我就把我这些年踩过的坑、摔过的跤、背过的锅,一股脑儿全倒出来。从"这玩意儿到底是个啥"到"怎么让它不卡成PPT",再到"出问题了怎么排查",最后送你几个能让代码看起来不那么像新手写的骚操作。全程手把手上代码,注释给你写得明明白白,复制粘贴就能跑,跑不通你来找我(反正你也找不到)。

咱就是说,看完这篇,下次再遇到后端给你扔base64,你至少能淡定地泡杯咖啡,慢悠悠地说:“哦,这个啊,我熟。”

这玩意儿到底是个啥妖魔鬼怪

base64这编码的底裤,咱给它扒了

先别急着写代码,咱得搞清楚这妖孽的本质。base64说白了就是一种编码方式,把二进制数据(比如图片、音频、视频)转换成ASCII字符集中的64个字符(A-Z、a-z、0-9、+、/)再加上一个填充符=。为啥要这么干?因为有些系统或者协议(比如早期的邮件系统)只支持文本传输,二进制数据过去就乱码,所以得先"翻译"成文本。

一张图片在计算机里存的是啥?是一堆0和1的二进制数据。base64就是把这些0和1按6个一组,每组对应一个ASCII字符,最后拼成一串看起来像是乱码的文本。这个过程会让数据体积膨胀约33%,也就是说,原本100KB的图片,转成base64后就变成133KB左右。

// 来,咱们手动模拟一下base64的膨胀率const originalSize =100*1024;// 100KB的二进制数据const base64Size = Math.ceil(originalSize /3)*4;// base64是3个字节转4个字符const expansionRate =((base64Size - originalSize)/ originalSize *100).toFixed(2); console.log(`原始大小: ${originalSize} bytes`); console.log(`Base64后大小: ${base64Size} bytes`); console.log(`膨胀率: ${expansionRate}%`);// 输出大概是33.33%

看到没,这就是为啥我说base64"看着省事实则坑爹"的原因之一。数据量变大了,传输时间变长了,用户流量哗哗地烧,尤其是在4G或者信号不好的地方,体验直接崩盘。

img标签的src属性,它到底想吃啥?

咱们天天写<img src="xxx">,但你真的了解src这个属性吗?它其实是个"杂食动物",啥都能吃:

  1. 绝对URLhttps://example.com/image.jpg
  2. 相对URL./assets/logo.png
  3. Data URIdata:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==

那个Data URI就是咱们今天的主角。它的格式是固定的:

data:[MIME类型];base64,[base64编码的字符串] 

MIME类型告诉浏览器这是啥玩意儿,常见的有:

  • image/png - PNG图片
  • image/jpegimage/jpg - JPEG图片
  • image/gif - GIF动图
  • image/webp - WebP格式(现在挺流行的)
  • image/svg+xml - SVG矢量图(这个有时候也转base64,虽然没必要)
<!-- 正确的打开方式 --><imgsrc="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="><!-- 错误的打开方式(漏了MIME类型) --><imgsrc="data:base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==">

看到上面那个错误的例子没?我就见过有后端返回的数据里,只给了base64字符串,没给MIME类型,前端小哥直接拼了个data:base64,xxx,结果图片死活显示不出来,在那调试了俩小时,最后发现是漏了image/png这茬。这种低级错误,说出去都丢人,但谁没年轻过呢对吧?

为啥有时候直接拼前缀能行,有时候又摆烂?

这事儿得看后端给的数据质量。理想情况下,后端应该给你返回一个完整的对象:

{"imageName":"avatar.png","mimeType":"image/png","base64Data":"iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="}

但现实往往是残酷的,后端可能只给你:

{"image":"iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="}

甚至有时候,这串base64里面还混进了换行符\n、回车符\r或者空格,因为有些后端框架为了"美观",会自动格式化JSON,把一长串base64折成好几行。浏览器解析的时候可不认这些,它要的是纯的base64字符,一旦遇到非法字符,直接给你显示个裂开的图标,或者控制台报错"Invalid character"。

// 假设这是后端返回的"脏数据"const dirtyBase64 ="iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbybl\nAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO\n9TXL0Y4OHwAAAABJRU5ErkJggg==";// 直接拼接到src里,图片显示不出来const imgSrc =`data:image/png;base64,${dirtyBase64}`;// 得先"洗洗澡",把非法字符干掉const cleanBase64 = dirtyBase64.replace(/[\s\r\n]+/g,'');const correctImgSrc =`data:image/png;base64,${cleanBase64}`;

看到没,这就是为啥有时候你明明觉得代码写得没问题,图片就是不显示。不是浏览器抽风,是数据本身带"病"。

手把手教你把乱码变美图

好了,理论基础打完了,咱们上干货。我总结了三种常见的处理方式,从"土得掉渣"到"稍微能看点",总有一款适合你。

第一种土法:直接在HTML里硬拼字符串

这种方法适合那种特别小的图标,比如16x16的favicon,或者几个像素的小圆点。大图千万别这么干,否则你的HTML文件体积会爆炸,编辑器都能卡死。

<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><title>Base64硬拼示例</title><style>.tiny-icon{width: 16px;height: 16px;}/* 稍微大一点的图,用CSS控制一下,别让它原尺寸显示 */.small-logo{width: 100px;height: auto;}</style></head><body><h2>这方法只适用于超小图标</h2><!-- 一个红色的1x1像素点,用来做占位或者透明gif的替代品 --><imgclass="tiny-icon"src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"alt="1x1透明gif"><!-- 一个稍微复杂点的小图标,比如一个红色的圆 --><imgclass="tiny-icon"src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="alt="红点"><p>看到上面那个src了吗?那么长一串,要是大图的话,这一行能占满整个屏幕</p></body></html>

这种写法的优缺点很明显:

优点

  • 简单粗暴,不用写JS
  • 省了一次HTTP请求(因为数据就在HTML里)
  • 小图标确实加载快,几乎是瞬间显示

缺点

  • HTML文件体积暴增,影响首屏加载
  • 无法缓存(除非缓存整个HTML页面)
  • 维护困难,你想改个图?得重新生成base64再替换
  • 编辑器里看着头大,满屏乱码

所以啊,这种方法就用来应急,或者那种万年不变的小图标。千万别在生产环境的大图上用,否则性能优化的时候有你哭的。

第二种稍微洋气点:用JS动态创建Image对象

这种方法适合需要根据条件动态显示图片的场景。比如用户点击按钮后才加载图片,或者要根据屏幕大小加载不同尺寸的图片。

/** * 动态创建Image对象加载base64图片 * @param {string} base64String - 纯base64字符串(不含data URI前缀) * @param {string} mimeType - MIME类型,默认image/png * @param {HTMLElement} container - 要挂载到的容器 * @param {object} options - 配置选项 */functionloadBase64Image(base64String, mimeType ='image/png', container, options ={}){// 先洗数据,去掉可能存在的换行符和空格const cleanBase64 = base64String.replace(/[\s\r\n]+/g,'');// 拼接完整的Data URIconst dataUri =`data:${mimeType};base64,${cleanBase64}`;// 创建Image对象,这样可以监听load和error事件const img =newImage();// 设置图片属性if(options.width) img.width = options.width;if(options.height) img.height = options.height;if(options.alt) img.alt = options.alt;if(options.className) img.className = options.className;// 加载成功的回调 img.onload=function(){ console.log('图片加载成功啦!尺寸:', img.naturalWidth,'x', img.naturalHeight);// 如果传了成功回调,执行一下if(options.onLoad &&typeof options.onLoad ==='function'){ options.onLoad(img);}// 挂载到DOMif(container && container.appendChild){ container.appendChild(img);}};// 加载失败的回调 img.onerror=function(error){ console.error('图片加载失败了,检查一下base64数据:', error);// 如果传了失败回调,执行一下if(options.onError &&typeof options.onError ==='function'){ options.onError(error);}// 可以显示一个占位图或者错误提示if(container){ container.innerHTML ='<p>图片加载失败,可能是base64数据有问题</p>';}};// 设置src,触发加载 img.src = dataUri;return img;// 返回img对象,方便外部操作}// 使用示例const base64Data ="iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";// 找到容器const container = document.getElementById('image-container');// 调用函数,加点配置loadBase64Image(base64Data,'image/png', container,{width:200,alt:'这是动态加载的图片',className:'responsive-img',onLoad:function(img){ console.log('老板,图片加载完毕,可以交差了!');// 这里可以加个淡入动画啥的 img.style.opacity ='0';setTimeout(()=>{ img.style.transition ='opacity 0.5s'; img.style.opacity ='1';},10);},onError:function(err){ console.log('完蛋,出错了,准备背锅');}});

看到没,这种方法的好处是:

  • 可以监听加载状态,成功失败都知道
  • 可以在加载前显示loading,加载后加动画
  • 错误处理更优雅,不会直接显示裂开的图标
  • 可以动态控制图片尺寸和样式

但本质上还是把base64塞给src,大图依然会卡,这点要注意。

第三种针对异步接口:axios或者fetch拿到数据后怎么处理

这是最常见的场景了,后端给你个接口,返回一堆base64数据,你得等请求回来才能处理。这里最容易踩的坑就是"undefined"或者"null"被拼进了src里。

// 用axios举例,fetch也大同小异import axios from'axios';/** * 从API获取base64图片并显示 * @param {string} apiUrl - 接口地址 * @param {HTMLElement} targetElement - 目标img标签或者容器 */asyncfunctionfetchAndDisplayBase64Image(apiUrl, targetElement){try{// 显示loading状态if(targetElement.tagName ==='IMG'){ targetElement.src ='loading.gif';// 或者显示个骨架屏}else{ targetElement.innerHTML ='<p>图片加载中...</p>';}// 发请求const response =await axios.get(apiUrl);// 这里要注意!后端返回的数据结构可能千奇百怪// 可能是 response.data.base64// 可能是 response.data.image// 可能是 response.data.data.imageBase64// 得看接口文档,或者console.log出来看看 console.log('后端返回的数据:', response.data);// 假设后端返回的是 { code: 200, data: { image: "xxx", type: "png" } }const{ data }= response.data;// 防御性编程,万一后端抽风返回了undefinedif(!data ||!data.image){thrownewError('后端返回的数据格式不对,找不到image字段');}const base64String = data.image;const mimeType = data.type ?`image/${data.type}`:'image/png';// 再次检查,防止是空字符串if(typeof base64String !=='string'|| base64String.length ===0){thrownewError('base64数据是空的,后端在搞什么飞机');}// 清洗数据const cleanBase64 = base64String.replace(/[\s\r\n]+/g,'');// 拼接Data URIconst dataUri =`data:${mimeType};base64,${cleanBase64}`;// 设置到img标签if(targetElement.tagName ==='IMG'){ targetElement.src = dataUri;}else{// 如果是容器,创建img元素塞进去const img = document.createElement('img'); img.src = dataUri; img.style.maxWidth ='100%'; targetElement.innerHTML =''; targetElement.appendChild(img);} console.log('图片加载成功,base64长度:', cleanBase64.length);}catch(error){ console.error('加载图片失败:', error);// 显示错误占位图或者提示if(targetElement.tagName ==='IMG'){ targetElement.src ='error-placeholder.png'; targetElement.alt ='图片加载失败';}else{ targetElement.innerHTML =` <div> <p>图片加载失败: ${error.message}</p> <button onclick="location.reload()">重试</button> </div> `;}}}// 批量加载多个base64图片(比如列表页)asyncfunctionloadMultipleBase64Images(apiUrls, container){// 创建一个DocumentFragment,减少DOM操作次数const fragment = document.createDocumentFragment();// 用Promise.allSettled,即使某个失败了也不影响其他的const results =await Promise.allSettled( apiUrls.map(url=> axios.get(url))); results.forEach((result, index)=>{const wrapper = document.createElement('div'); wrapper.className ='image-item';if(result.status ==='fulfilled'){const{ data }= result.value.data;if(data && data.image){const img = document.createElement('img');const cleanBase64 = data.image.replace(/[\s\r\n]+/g,''); img.src =`data:image/${data.type ||'png'};base64,${cleanBase64}`; img.alt =`图片-${index}`; img.style.width ='200px'; img.style.margin ='10px'; wrapper.appendChild(img);}else{ wrapper.innerHTML =`<p>图片${index}数据格式错误</p>`;}}else{ wrapper.innerHTML =`<p>图片${index}加载失败: ${result.reason.message}</p>`;} fragment.appendChild(wrapper);}); container.appendChild(fragment);}// 使用示例fetchAndDisplayBase64Image('/api/user/avatar', document.getElementById('avatar-img'));// 批量加载loadMultipleBase64Images(['/api/image/1','/api/image/2','/api/image/3'], document.getElementById('gallery'));

看到上面那段代码了吗?我写了多少防御性判断?if (!data || !data.image)if (typeof base64String !== 'string'),这些都是血泪教训啊。后端接口说变就变,今天返回data.image,明天可能就改成data.base64Image,你不做判断,直接data.image.replace(),万一data是undefined,直接报错,页面白屏,用户截图发群里,老板@你,这酸爽…

还有那个Promise.allSettled,批量加载的时候一定要用,别用Promise.all。后者只要有一个失败,全部凉凉,前者至少能保证其他的图片正常显示。

这方案看着香其实全是坑

优点嘛,确实有,但不能只看表面

base64方案最大的卖点就是"减少HTTP请求"。传统的图片加载是这样的:

  1. 浏览器下载HTML
  2. 解析到img标签的src
  3. 发起HTTP请求去下载图片
  4. 下载完成后解码显示

而用base64的话,步骤变成了:

  1. 浏览器下载HTML(或者JSON接口数据,里面已经包含了base64)
  2. 解析到Data URI,直接解码显示

省掉了那次HTTP请求,对于小图标来说,确实快。因为HTTP请求是有开销的,建立连接、发送请求头、等待响应,这些时间加起来可能比下载一个小图标还长。所以对于那些只有几百字节的图标,base64确实香。

// 来做个简单的性能对比(伪代码,实际要看Network面板)// 方案A:传统URL// 请求头开销大约500字节 + 图片本身200字节 = 700字节,外加RTT时间// 方案B:base64// 图片200字节 -> base64后约267字节,直接包含在HTML里,没有额外请求// 结论:小图(<1KB)用base64确实划算// 大图(>10KB)就不划算了,base64膨胀33%,而且无法利用浏览器缓存

缺点得好好吐槽,全是泪

1. 体积膨胀,流量刺客

前面说了,base64会让数据膨胀33%。一张1MB的图片,转成base64后就变成1.33MB。用户如果是用流量访问,这多出来的330KB就是真金白银啊。而且这330KB是文本数据,gzip压缩对它的效果不如二进制数据好。

2. 解码性能,CPU杀手

浏览器拿到base64字符串后,要先解码成二进制,再解码成图片像素。这个过程是在主线程上进行的,如果图片很大,或者数量很多,会直接阻塞UI,页面卡顿、掉帧,用户以为你网站挂了。

// 来,咱们写个简单的性能测试functiontestBase64Performance(){// 生成一个较大的base64字符串(模拟500KB的图片)const size =500*1024;// 500KBconst binaryData =newUint8Array(size);for(let i =0; i < size; i++){ binaryData[i]= Math.floor(Math.random()*256);}// 转成base64(浏览器提供的btoa只能处理字符串,得先转)const base64 =btoa(String.fromCharCode.apply(null, binaryData)); console.log('开始解码测试,base64长度:', base64.length); console.time('decodeTime');// 创建图片并设置srcconst img =newImage(); img.onload=()=>{ console.timeEnd('decodeTime');// 看看用了多久 console.log('图片尺寸:', img.naturalWidth,'x', img.naturalHeight);}; img.src =`data:image/jpeg;base64,${base64}`;}// 在控制台跑一下这个,看看解码500KB的base64要多久// 如果超过16ms(一帧的时间),用户就能感知到卡顿

3. 缓存机制,基本等于没有

用URL加载的图片,浏览器会自动缓存,下次再访问直接从缓存拿,304都省了。但base64图片的数据是直接写在HTML或者JS里的,除非整个HTML/JS被缓存,否则每次都要重新下载、重新解码。而且就算HTML被缓存了,base64解码的过程每次都要重新来一遍,CPU该烧还是烧。

4. 内存占用,移动端杀手

base64字符串是文本,存在JavaScript的堆内存里。一张1MB的图片,base64后1.33MB,解码成Image对象后,还要占用宽高×4字节的内存(RGBA四个通道)。一张1920×1080的图片,解码后就要占用约8MB内存(1920×1080×4)。如果页面里有十几张这样的图,移动端浏览器直接崩溃给你看,尤其是iOS的WebView,内存限制特别严格。

5. 开发和维护成本

你想啊,代码里全是这种:

const imgSrc ="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIA...(此处省略一万字)...Jggg==";

这谁看得懂?想改个图,得重新生成base64,找对应的字符串替换,一不小心就改错了。而且代码审查的时候,这坨东西根本没法review,只能相信生成它的人没搞错。

真实项目里那些让人头秃的场景

列表页一次性加载几十张base64,浏览器直接假死

这是我踩过最大的坑,没有之一。当时做的是个电商后台管理系统,商品列表页要显示商品缩略图。后端大哥图省事,直接把图片转成了base64塞在接口里返回。我想着,缩略图嘛,能有多大?结果一测试,列表一页显示50条数据,每条带一张图,页面直接卡死。

为啥?50张图,每张就算100KB,base64后130KB,总共6.5MB的文本数据。接口返回就要好几秒,然后浏览器解析这6.5MB的JSON,再解码50张图片,主线程直接被占满,页面假死十几秒。用户在那疯狂点击,以为网站挂了,实际上浏览器正在那吭哧吭哧地干活。

// 错误的示范:一次性渲染所有base64图片functionrenderProductList(products){const container = document.getElementById('product-list');let html ='';// 千万别这么干!如果products有50个,直接卡死 products.forEach(product=>{ html +=` <div> <img src="data:image/jpeg;base64,${product.base64Image}" alt="${product.name}"> <h3>${product.name}</h3> <p>¥${product.price}</p> </div> `;}); container.innerHTML = html;// 这一行执行的时候,浏览器要解码所有图片}// 稍微好点的方案:虚拟滚动 + 懒加载functionrenderProductListSmart(products){const container = document.getElementById('product-list');// 只渲染视口内的图片,其他的等滚动到再说const viewportHeight = window.innerHeight;const itemHeight =200;// 每个商品卡片高度const visibleCount = Math.ceil(viewportHeight / itemHeight)+2;// 多渲染2个缓冲let html =''; products.slice(0, visibleCount).forEach((product, index)=>{ html +=` <div> <!-- 先用占位图 --> <img src="placeholder.jpg" alt="${product.name}"> <h3>${product.name}</h3> <p>¥${product.price}</p> </div> `;}); container.innerHTML = html;// 懒加载逻辑:当图片进入视口时再加载base64const observer =newIntersectionObserver((entries)=>{ entries.forEach(entry=>{if(entry.isIntersecting){const img = entry.target;const base64 = img.getAttribute('data-base64');if(base64){// 延迟一点加载,避免同时解码太多setTimeout(()=>{ img.src =`data:image/jpeg;base64,${base64}`; img.removeAttribute('data-base64');}, Math.random()*500);// 随机延迟0-500ms,分散解码压力} observer.unobserve(img);}});}); document.querySelectorAll('.lazy-img').forEach(img=> observer.observe(img));}

看到没,解决方案就是不要一次性渲染所有base64图片。用虚拟滚动(virtual scrolling)只渲染视口内的,或者用Intersection Observer做懒加载,等图片要显示了再解码。而且加了个随机延迟,避免同时解码多张图导致卡顿。

上传预览功能,本地转base64展示没问题,一提交给后端就报错

文件上传前的本地预览,用base64很方便。FileReader的readAsDataURL方法直接就能拿到Data URI,塞给img标签就能预览。但这里有个大坑:前端拿到的base64是带data:image/jpeg;base64,前缀的,而后端可能只需要后面的纯base64字符串。

// 文件上传预览的经典代码 document.getElementById('file-input').addEventListener('change',function(e){const file = e.target.files[0];if(!file)return;const reader =newFileReader(); reader.onload=function(event){// 这里拿到的result是完整的Data URIconst dataUri = event.target.result; console.log('预览用的Data URI:', dataUri);// 输出:data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD...// 设置到img标签预览,没问题 document.getElementById('preview').src = dataUri;// 但是提交给后端的时候,得把前缀去掉!const base64String = dataUri.split(',')[1];// 取逗号后面的部分// 提交给后端uploadToServer(base64String);}; reader.readAsDataURL(file);});asyncfunctionuploadToServer(base64String){try{const response =awaitfetch('/api/upload',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({image: base64String,fileName:'avatar.jpg'})});const result =await response.json(); console.log('上传结果:', result);}catch(error){ console.error('上传失败:', error);}}

还有一个坑是文件大小限制。前端转base64的时候,大文件会导致浏览器卡顿。而且后端接收base64数据时,JSON解析也有大小限制,有些服务器配置默认最多接收1MB的JSON,你传个5MB的base64过去,直接413 Payload Too Large。

// 加个文件大小检查,避免转太大的base64functionhandleFileSelect(file){constMAX_SIZE=2*1024*1024;// 2MB限制if(file.size >MAX_SIZE){alert('文件太大,请选择2MB以下的图片');return;}// 还可以先压缩一下再转base64compressImage(file,800,600).then(compressedFile=>{// 压缩后再转base64,体积会小很多returnfileToBase64(compressedFile);}).then(base64=>{// 预览和上传...});}// 图片压缩函数(用canvas实现)functioncompressImage(file, maxWidth, maxHeight){returnnewPromise((resolve)=>{const img =newImage();const url =URL.createObjectURL(file); img.onload=function(){URL.revokeObjectURL(url);// 释放内存let{ width, height }= img;// 等比例缩放if(width > maxWidth){ height =(height * maxWidth)/ width; width = maxWidth;}if(height > maxHeight){ width =(width * maxHeight)/ height; height = maxHeight;}const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height;const ctx = canvas.getContext('2d'); ctx.drawImage(img,0,0, width, height);// 转成blob,质量0.8的JPEG canvas.toBlob((blob)=>{resolve(newFile([blob], file.name,{type:'image/jpeg'}));},'image/jpeg',0.8);}; img.src = url;});}

混合开发里,H5页面嵌在App里,base64图片太大导致WebView直接崩溃

做Hybrid App的老铁肯定懂这个痛。iOS的WKWebView和Android的WebView都有内存限制,尤其是iOS,单个App的内存占用超过一定阈值(比如几百MB),系统直接给你杀掉,用户看到的就是App闪退。

如果H5页面里用了大量base64图片,内存占用会飙升。因为base64字符串存在JS的堆里,解码后的Image对象又占一份内存,WebView的渲染层还要存一份位图数据,一份图片三份内存,这谁顶得住?

// 在Hybrid App里的优化方案// 1. 尽量用原生图片组件,而不是H5的img标签// 2. 如果必须用H5,控制base64图片的数量和大小// 内存管理:当图片离开视口时,释放内存functionsetupMemoryManagement(){const imageCache =newMap();// 缓存已经加载的图片const observer =newIntersectionObserver((entries)=>{ entries.forEach(entry=>{const img = entry.target;if(entry.isIntersecting){// 进入视口,加载图片const base64 = img.getAttribute('data-base64');if(base64 &&!imageCache.has(img)){ img.src =`data:image/jpeg;base64,${base64}`; imageCache.set(img,true);}}else{// 离开视口,释放内存(把src设为空或者占位图)if(imageCache.has(img)){ img.src ='data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';// 1x1透明gif imageCache.delete(img);// 强制垃圾回收(虽然JS没有直接GC的API,但这样可以释放引用)if(img.dataset){delete img.dataset.base64;}}}});},{rootMargin:'100px'// 提前100px开始加载,延迟100px释放}); document.querySelectorAll('img[data-base64]').forEach(img=>{ observer.observe(img);});}// 页面不可见时(比如切换到其他Tab),暂停所有图片加载 document.addEventListener('visibilitychange',()=>{if(document.hidden){// 页面隐藏,可以暂停一些非关键的图片解码 console.log('页面隐藏,节省内存');}else{ console.log('页面显示,恢复加载');}});

出问题了别只会重启大法好

图片显示个裂开的图标?赶紧查查前缀

这是最常见的问题,控制台可能还没报错,但图片就是显示不出来,是个裂开的图标或者空白。这时候先右键检查元素,看看src属性是不是完整的Data URI。

常见问题:

  1. 漏了MIME类型data:base64,xxx 应该是 data:image/png;base64,xxx
  2. MIME类型写错了:后端给的是jpg,你写成了png,有时候浏览器能自动识别,有时候就不行
  3. base64字符串不完整:被截断了,或者复制的时候漏了开头结尾
// 调试工具:检查base64字符串是否合法functionvalidateBase64(base64String, expectedMimeType ='image/png'){const issues =[];// 检查是否为空if(!base64String || base64String.length ===0){ issues.push('base64字符串为空');return issues;}// 检查长度(base64长度应该是4的倍数)if(base64String.length %4!==0){ issues.push(`长度不是4的倍数,当前长度:${base64String.length}`);}// 检查是否有非法字符(base64只能包含A-Z a-z 0-9 + / =)const invalidChars = base64String.match(/[^A-Za-z0-9+/=]/g);if(invalidChars){ issues.push(`包含非法字符: ${[...newSet(invalidChars)].join(', ')}`);}// 检查填充符=的位置(只能在末尾,最多两个)const paddingMatch = base64String.match(/=+$/);const paddingCount = paddingMatch ? paddingMatch[0].length :0;if(paddingCount >2){ issues.push(`填充符=超过2个,当前:${paddingCount}`);}if(base64String.includes('=')&&!base64String.endsWith('=')){ issues.push('填充符=不在末尾');}// 尝试解码(在浏览器环境里)try{atob(base64String);}catch(e){ issues.push(`解码失败: ${e.message}`);}if(issues.length >0){ console.error('base64验证失败:', issues);}else{ console.log('base64格式正确,长度:', base64String.length);}return issues;}// 使用const problematicBase64 ="iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbybl\nAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";validateBase64(problematicBase64);// 会提示包含非法字符(换行符)

控制台报"Invalid source"?多半是字符串脏了

如果控制台报错"Failed to load resource: net::ERR_INVALID_URL"或者"Invalid source",多半是base64字符串里混进了换行符、空格或者其他不可见字符。有些后端语言(比如Java)在生成JSON的时候,为了可读性会自动给长字符串加换行,或者有些数据库字段类型是TEXT,存进去的时候没问题,取出来就带了换行。

// 清洗函数,专治各种脏数据functionsanitizeBase64(dirtyBase64){if(typeof dirtyBase64 !=='string'){ console.warn('传入的不是字符串:',typeof dirtyBase64);return'';}// 第一步:去掉所有空白字符(空格、制表符、换行、回车)let clean = dirtyBase64.replace(/\s+/g,'');// 第二步:去掉URL安全base64的替代字符(有些后端会用-和_代替+和/)// 如果是标准base64,这步可以省略// clean = clean.replace(/-/g, '+').replace(/_/g, '/');// 第三步:检查并修复填充符const paddingNeeded =4-(clean.length %4);if(paddingNeeded !==4){// 长度不是4的倍数,补= clean +='='.repeat(paddingNeeded); console.warn('base64长度不是4的倍数,已自动补全');}// 第四步:去掉可能存在的Data URI前缀(如果后端不小心把前缀也塞进来了)if(clean.includes('base64,')){ clean = clean.split('base64,')[1]; console.warn('base64字符串包含Data URI前缀,已自动去除');}return clean;}// 测试const dirty ="iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbybl\nAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg== "; console.log('清洗前长度:', dirty.length); console.log('清洗后长度:',sanitizeBase64(dirty).length);

页面卡顿严重?打开Performance面板看看

如果页面卡成PPT,别瞎猜,打开Chrome DevTools的Performance面板,录个性能分析看看。重点看Main线程的CPU占用,如果看到一长条的"Decode Image"或者"Parse HTML",那就是base64解码在搞事情。

// 优化方案:把解码放到Web Worker里(如果浏览器支持)// 注意:Web Worker里不能操作DOM,只能解码后把ImageBitmap传回来// worker.js self.onmessage=function(e){const{ base64, mimeType, id }= e.data;// 在Worker里解码base64(这里只是示例,实际解码图片需要更复杂的逻辑)// 实际上浏览器解码图片是在渲染进程的主线程,Worker里没法直接解码成ImageBitmap// 但我们可以把base64转成Blob,然后用createImageBitmap(这个API可以在Worker里用)const byteCharacters =atob(base64);const byteNumbers =newArray(byteCharacters.length);for(let i =0; i < byteCharacters.length; i++){ byteNumbers[i]= byteCharacters.charCodeAt(i);}const byteArray =newUint8Array(byteNumbers);const blob =newBlob([byteArray],{type: mimeType });createImageBitmap(blob).then(imageBitmap=>{ self.postMessage({ id, imageBitmap },[imageBitmap]);});};// 主线程代码const worker =newWorker('worker.js');functiondecodeBase64InWorker(base64, mimeType){returnnewPromise((resolve)=>{const id = Date.now()+ Math.random(); worker.onmessage=function(e){if(e.data.id === id){resolve(e.data.imageBitmap);}}; worker.postMessage({ base64, mimeType, id });});}// 使用decodeBase64InWorker(base64String,'image/png').then(imageBitmap=>{// 在Canvas上绘制,避免创建img元素(省内存)const canvas = document.createElement('canvas'); canvas.width = imageBitmap.width; canvas.height = imageBitmap.height;const ctx = canvas.getContext('2d'); ctx.drawImage(imageBitmap,0,0);// 如果非要显示在img标签里,可以转成blob URL(但这样又多了一步) canvas.toBlob(blob=>{const url =URL.createObjectURL(blob); document.getElementById('img').src = url;});});

说实话,Web Worker这个方案有点过度设计了,大部分场景用不到。但如果你在做图片编辑器那种需要处理大量大图的应用,可以考虑。

几个让代码看起来不那么菜的骚操作

写个工具函数自动判断MIME类型

别硬编码image/png,万一后端返个gif或者webp,你写死了就抓瞎。可以根据base64字符串的特征来判断,或者让后端把MIME类型也返回。

/** * 自动检测base64图片的MIME类型 * 通过文件头的magic number来判断 */functiondetectMimeType(base64String){// 取前几个字符解码,看文件头const header =atob(base64String.substring(0,20));const bytes = header.split('').map(c=> c.charCodeAt(0));// 文件签名(magic numbers)const signatures ={'image/png':[0x89,0x50,0x4E,0x47],// ‰PNG'image/jpeg':[0xFF,0xD8,0xFF],// ÿØÿ'image/gif':[0x47,0x49,0x46],// GIF'image/webp':[0x52,0x49,0x46,0x46],// RIFF(webp也是RIFF格式)'image/bmp':[0x42,0x4D],// BM'image/svg+xml':[0x3C,0x73,0x76,0x67]// <svg(这个不太准,base64编码后可能不是这个)};for(const[mime, signature]of Object.entries(signatures)){let match =true;for(let i =0; i < signature.length; i++){if(bytes[i]!== signature[i]){ match =false;break;}}if(match)return mime;}// 默认返回pngreturn'image/png';}// 更实用的方案:根据后缀名或者后端返回的类型functiongetMimeTypeFromFilename(filename){const ext = filename.split('.').pop().toLowerCase();const mimeTypes ={'png':'image/png','jpg':'image/jpeg','jpeg':'image/jpeg','gif':'image/gif','webp':'image/webp','svg':'image/svg+xml','bmp':'image/bmp','ico':'image/x-icon'};return mimeTypes[ext]||'image/png';}// 终极工具函数:智能处理各种情况functioncreateDataUri(base64String, options ={}){// 清洗数据let cleanBase64 = base64String.replace(/[\s\r\n]+/g,'');// 如果已经有data URI前缀了,直接返回if(cleanBase64.startsWith('data:')){return cleanBase64;}// 确定MIME类型let mimeType = options.mimeType;if(!mimeType && options.filename){ mimeType =getMimeTypeFromFilename(options.filename);}if(!mimeType){ mimeType =detectMimeType(cleanBase64);}return`data:${mimeType};base64,${cleanBase64}`;}// 使用const dataUri =createDataUri(base64String,{filename:'avatar.png'});// 或者const dataUri2 =createDataUri(base64String,{mimeType:'image/jpeg'});

加个阈值判断,超过多少KB坚决不转base64

前面说了,base64适合小图,大图走CDN。那多大的图算"大图"?一般来说,10KB以下的可以考虑base64,10KB以上的坚决走URL。这个阈值可以根据项目调整,但别超过50KB,否则就是跟自己过不去。

/** * 智能选择图片加载方式 * @param {string} base64String - base64字符串 * @param {string} fallbackUrl - 如果base64太大,使用的备用URL * @param {number} threshold - 阈值,默认10KB */functionsmartImageLoad(base64String, fallbackUrl, threshold =10*1024){// 计算base64大概的原始大小(base64长度*0.75,因为4个字符代表3个字节)const estimatedSize = base64String.length *0.75;if(estimatedSize > threshold){ console.warn(`图片大小${(estimatedSize/1024).toFixed(2)}KB超过阈值${threshold/1024}KB,建议使用URL`);return{type:'url',src: fallbackUrl,reason:'too_large'};}const mimeType =detectMimeType(base64String);return{type:'base64',src:`data:${mimeType};base64,${base64String}`,size: estimatedSize };}// 在React组件里使用functionSmartImage({ base64Data, fallbackUrl, alt, threshold =10*1024}){const imageInfo =smartImageLoad(base64Data, fallbackUrl, threshold);if(imageInfo.type ==='url'){// 如果base64太大,显示一个提示,或者直接用URLreturn(<div><img src={fallbackUrl} alt={alt} loading="lazy"/><small style={{color:'gray'}}>大图已优化加载</small></div>);}return<img src={imageInfo.src} alt={alt}/>;}

利用CSS变量或者预处理器搞点批量处理

如果你在用Vue或者React,模板里全是data:image/png;base64,xxx这种,看着就烦。可以用CSS变量或者组件化的方式封装一下。

// SCSS里定义一些常用的base64小图标(比如1x1的透明图、loading图) $transparent-gif: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; // 使用 .placeholder { background-image: url($transparent-gif); } // Vue组件封装 // Base64Image.vue <template> <img :src="dataUri" :alt="alt" :class="['base64-img', { 'is-loading': loading }]" @load="onLoad" @error="onError" /> </template> <script> export default { name: 'Base64Image', props: { base64: { type: String, required: true }, mimeType: { type: String, default: 'image/png' }, alt: { type: String, default: '' }, // 是否自动检测MIME类型 autoDetect: { type: Boolean, default: true } }, data() { return { loading: true, error: false }; }, computed: { dataUri() { let cleanBase64 = this.base64.replace(/[\s\r\n]+/g, ''); // 如果已经有前缀,直接返回 if (cleanBase64.startsWith('data:')) { return cleanBase64; } let mime = this.mimeType; if (this.autoDetect) { // 简单的检测逻辑,实际项目中可以完善 if (cleanBase64.charAt(0) === '/') mime = 'image/jpeg'; else if (cleanBase64.charAt(0) === 'i') mime = 'image/png'; else if (cleanBase64.charAt(0) === 'R') mime = 'image/gif'; } return `data:${mime};base64,${cleanBase64}`; } }, methods: { onLoad() { this.loading = false; this.$emit('load'); }, onError(e) { this.error = true; this.loading = false; console.error('Base64图片加载失败:', e); this.$emit('error', e); } } }; </script> <style scoped> .base64-img { transition: opacity 0.3s; } .is-loading { opacity: 0; background: #f0f0f0; } </style> 

这样封装后,使用的时候就很清爽了:

<Base64Image:base64="user.avatar"mimeType="image/jpeg"alt="用户头像"@load="handleLoad"/>

最后啰嗦一句保命秘籍

好了,说了这么多,最后总结几句掏心窝子的话:

1. 别啥图都转base64,那是十年前的玩法

现在带宽这么便宜,CDN这么普及,WebP格式压缩率这么高,还抱着base64不放干啥?base64只适合那种特别小的图标(<1KB),或者必须内联的场景(比如HTML邮件、单文件离线应用)。其他的,老老实实走URL,让浏览器缓存去干活。

2. 要是老板非逼着你全转,就把这篇甩给他看

有些产品经理或者老板,听风就是雨,看到某个文章说"base64减少HTTP请求能优化性能",就要求全站图片转base64。这时候你别硬刚,把这篇转给他,让他看看"减少HTTP请求"的代价是什么。数据说话,把页面加载时间、内存占用、流量消耗的对比数据甩他脸上,告诉他这是拿用户体验在换所谓的"简洁"。

3. 后端要是坚持给你返base64,让他至少把MIME类型带上

最烦那种只返一串base64,啥说明都没有的接口。你问他这是啥格式,他说"你试试不就知道了"。试试?我试你个大头鬼!规范的后端接口应该长这样:

{"image":{"data":"iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==","mimeType":"image/png","size":167,// 原始字节数"filename":"dot.png"}}

4. 实在不行就摸鱼吧

如果这需求改起来是无底洞,后端不配合,老板不理解,用户还天天骂,那…不如早点下班去吃火锅。身体是自己的,bug是改不完的,base64是杀不尽的。留得青山在,不怕没柴烧,明天又是新的一天(和新的bug)。

行了,就唠到这儿。希望下次你再遇到base64的坑,能淡定地泡杯咖啡,慢悠悠地说:"哦,这个啊,我熟。"然后复制粘贴上面的代码,搞定收工。

毕竟,咱们前端工程师的终极目标,不就是让代码能跑,让自己能下班嘛。


(全文完,字数统计:约7200字,应该够你交差了吧?)

欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。
推荐:DTcode7的博客首页。
一个做过前端开发的产品经理,经历过睿智产品的折磨导致脱发之后,励志要翻身农奴把歌唱,一边打入敌人内部一边持续提升自己,为我们广大开发同胞谋福祉,坚决抵制睿智产品折磨我们码农兄弟!

专栏系列(点击解锁)学习路线(点击解锁)知识定位
《微信小程序相关博客》持续更新中~结合微信官方原生框架、uniapp等小程序框架,记录请求、封装、tabbar、UI组件的学习记录和使用技巧等
《AIGC相关博客》持续更新中~AIGC、AI生产力工具的介绍,例如stable diffusion这种的AI绘画工具安装、使用、技巧等总结
《HTML网站开发相关》《前端基础入门三大核心之html相关博客》前端基础入门三大核心之html板块的内容,入坑前端或者辅助学习的必看知识
《前端基础入门三大核心之JS相关博客》前端JS是JavaScript语言在网页开发中的应用,负责实现交互效果和动态内容。它与HTML和CSS并称前端三剑客,共同构建用户界面。
通过操作DOM元素、响应事件、发起网络请求等,JS使页面能够响应用户行为,实现数据动态展示和页面流畅跳转,是现代Web开发的核心
《前端基础入门三大核心之CSS相关博客》介绍前端开发中遇到的CSS疑问和各种奇妙的CSS语法,同时收集精美的CSS效果代码,用来丰富你的web网页
《canvas绘图相关博客》Canvas是HTML5中用于绘制图形的元素,通过JavaScript及其提供的绘图API,开发者可以在网页上绘制出各种复杂的图形、动画和图像效果。Canvas提供了高度的灵活性和控制力,使得前端绘图技术更加丰富和多样化
《Vue实战相关博客》持续更新中~详细总结了常用UI库elementUI的使用技巧以及Vue的学习之旅
《python相关博客》持续更新中~Python,简洁易学的编程语言,强大到足以应对各种应用场景,是编程新手的理想选择,也是专业人士的得力工具
《sql数据库相关博客》持续更新中~SQL数据库:高效管理数据的利器,学会SQL,轻松驾驭结构化数据,解锁数据分析与挖掘的无限可能
《算法系列相关博客》持续更新中~算法与数据结构学习总结,通过JS来编写处理复杂有趣的算法问题,提升你的技术思维
《IT信息技术相关博客》持续更新中~作为信息化人员所需要掌握的底层技术,涉及软件开发、网络建设、系统维护等领域的知识
《信息化人员基础技能知识相关博客》无论你是开发、产品、实施、经理,只要是从事信息化相关行业的人员,都应该掌握这些信息化的基础知识,可以不精通但是一定要了解,避免日常工作中贻笑大方
《信息化技能面试宝典相关博客》涉及信息化相关工作基础知识和面试技巧,提升自我能力与面试通过率,扩展知识面
《前端开发习惯与小技巧相关博客》持续更新中~罗列常用的开发工具使用技巧,如 Vscode快捷键操作、Git、CMD、游览器控制台等
《photoshop相关博客》持续更新中~基础的PS学习记录,含括PPI与DPI、物理像素dp、逻辑像素dip、矢量图和位图以及帧动画等的学习总结
日常开发&办公&生产【实用工具】分享相关博客》持续更新中~分享介绍各种开发中、工作中、个人生产以及学习上的工具,丰富阅历,给大家提供处理事情的更多角度,学习了解更多的便利工具,如Fiddler抓包、办公快捷键、虚拟机VMware等工具

吾辈才疏学浅,摹写之作,恐有瑕疵。望诸君海涵赐教。望轻喷,嘤嘤嘤

非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。愿斯文对汝有所裨益,纵其简陋未及渊博,亦足以略尽绵薄之力。倘若尚存阙漏,敬请不吝斧正,俾便精进!
在这里插入图片描述

Read more

Vue入门到精通:从零开始学Vue

Vue入门到精通:从零开始学Vue

目录 一、第一个Vue程序 第一步 Vue构造函数的参数:options template配置项 第二步 模板语句的数据来源 Template配置项 Vue实例和容器 二、Vue模板语法 Vue 插值 Vue 指令 v-bind指令 v-model指令 三、MVVM分层思想 四、VM defineProperty 五、数据代理机制 Vue数据代理机制对属性名的要求 手写Vue框架数据代理的实现 六、解读Vue框架源代码 data(函数) 七、Vue事件处理 事件绑定 Vue事件绑定 事件回调函数中的this methods实现原理 八、事件修饰符 按键修饰符 九、计算属性 反转字符串methods实现 反转字符串计算属性实现 计算属性用法 十、侦听属性 比较大小的案例watch实现 computed实现

基于C++11手撸前端Promise

基于C++11手撸前端Promise

文章导航 * 引言 * 前端Promise的应用与优势 * 常见应用场景 * 并发请求 * Promise 解决的问题 * 手写 C++ Promise 实现 * 类结构与成员变量 * 构造函数 * resolve 方法 * reject 方法 * then 方法 * onCatch 方法 * 链式调用 * 使用示例 * `std::promise` 与 `CProimse` 对比 * 1. 基础功能对比 * 2. 实现细节对比 * (1) 状态管理 * (2) 回调注册与执行 * (3) 异步支持 * (4) 链式调用 * 3. 代码示例对比 * (1) `CProimse` 示例 * (2) `std::promise` 示例 * 4.

Android WebView 版本升级方案详解

Android WebView 版本升级方案详解 目录 1. 问题背景 2. WebViewUpgrade 项目介绍 3. 升级方法详解 4. 替代方案对比 5. 接入与使用步骤 6. 注意事项与限制 7. 总结与建议 问题背景 WebView 版本差异带来的问题 Android 5.0 以后,WebView 升级需要去 Google Play 安装 APK,但即使安装了也不一定能正常工作。像华为、Amazon 等特殊机型的 WebView 的 Chromium 版本一般比较低,只能使用它自己的 WebView,无法使用 Google 的 WebView。 典型问题场景 H.265 视频播放问题: