尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
彻底搞懂Canvas跨域污染:Tainted机制原理与实战修复
1. 项目概述当Canvas突然“变脏”你不是代码写错了是浏览器在执行铁律“HTMLCanvasElement跨域报错”——这句话在前端开发者的日常报错日志里出现频率之高几乎和undefined is not a function不相上下。但绝大多数人点开控制台看到那行红色的Failed to execute toDataURL on HTMLCanvasElement: Tainted canvases may not be exported第一反应是“赶紧换张同域图”第二反应是“是不是图片路径写错了”第三反应……往往就去搜“canvas跨域怎么解决”然后复制粘贴一段img.crossOrigin anonymous发现还是报错于是默默把功能注释掉或者硬着头皮用服务端中转——这其实不是你的问题是你没真正理解浏览器对Canvas施加的那条不可逾越的安全红线Tainted Canvas被污染的画布机制。这个标题里的“彻底搞懂”不是泛泛而谈“加个crossOrigin就行”而是要钻进浏览器渲染引擎的底层逻辑里看清楚为什么一张图片从别的域名加载进来哪怕只是drawImage一下整个canvas就立刻“变脏”为什么toDataURL、getImageData、toBlob这些看似无害的操作会瞬间触发报错而fillRect、strokeText却完全不受影响这条规则到底是为防谁防什么它和CORS协议是什么关系又和我们天天打交道的img标签的crossOrigin属性之间隔着几层抽象如果你曾经在做图片裁剪、水印添加、截图生成、WebGL纹理上传甚至只是想用canvas做个简单的二维码合成器却卡死在这一步那么这篇内容就是为你写的。它不假设你熟悉WebGL或V8源码但要求你愿意花30分钟跟着我把这条安全链路从用户点击页面一直拆解到浏览器内核的像素级校验逻辑。适合所有使用Canvas进行图像处理的前端开发者、可视化工程师、H5活动页搭建者以及任何被“tainted”这个词困扰超过两次的人。2. 核心原理拆解Tainted Canvas不是Bug是浏览器主动设下的“隔离墙”2.1 什么是Tainted Canvas它根本不是Canvas的状态而是像素的“血统认证”很多人误以为tainted是Canvas元素自身的一个布尔属性就像disabled一样可以读写。这是最大的认知偏差。Tainted Canvas本质上是一种不可见、不可重置、由浏览器内核强制维护的“像素来源标记”。它不存储在DOM节点上也不暴露给JavaScript API你永远无法通过canvas.isTainted或类似方式去读取它——因为它的存在就是为了让你“感知不到”直到你试图越界。举个生活化的例子想象Canvas是一块巨大的白板你往上面画的所有东西都必须有清晰的“进货单”。你用fillRect(0,0,100,100)画了个红方块这属于“本地生产”进货单上写着“本厂自产”没问题你用drawImage(img, 0, 0)把一张网络图片画上去浏览器立刻检查这张图片的“原产地证明”即CORS响应头。如果图片服务器在返回时明确写了Access-Control-Allow-Origin: *或Access-Control-Allow-Origin: https://your-site.com那这张图就被打上了“合规进口”的钢印白板上的对应区域像素就获得“清白血统”但如果服务器没写或者写了但不匹配比如写了https://other-site.com那这块区域的像素就被打上“走私入境”的红章——整块白板从这一刻起就被标记为“tainted”。注意是整块白板不是局部区域。哪怕你只画了一像素的图片其余99%都是自己画的只要发生过一次“非法入境”整块Canvas就永久性地被污染了。提示这个“整块污染”机制是刻意设计的。浏览器不区分“哪块像素来自哪里”因为像素在GPU内存中是连续存储的做精细溯源成本极高。所以它采用最保守策略只要有过一次不可信来源的输入就全局禁止所有可能泄露像素信息的导出操作。这是一种典型的“宁可错杀不可放过”的安全哲学。2.2 为什么只有toDataURL/getImageData/toBlob被禁它们到底想“偷”什么这个问题直指核心。为什么drawImage能成功fillStyle red能成功stroke()能成功偏偏toDataURL()就报错答案在于这些被禁的方法是唯一能把Canvas内部像素数据以纯文本或二进制形式“完整打包”并交到JavaScript脚本手里的出口。toDataURL()把整个Canvas渲染结果编码成base64字符串你可以轻松把它发给后端、存到localStorage甚至用fetch发到任意服务器。getImageData()直接返回一个ImageData对象里面包含data属性——这是一个Uint8ClampedArray也就是原始的RGBA四通道像素数组。每个像素的红、绿、蓝、透明度值都赤裸裸地躺在JavaScript内存里你可以用for循环逐个读取、分析、修改。toBlob()生成一个Blob对象本质是二进制数据块同样可以被读取、上传、甚至用FileReader转成字符串。而drawImage、fillRect这些方法只是“命令浏览器去画”画完之后像素数据依然牢牢锁在浏览器的渲染管线Render Pipeline里JavaScript脚本无法直接触碰。它像一个封闭的黑盒子你只能给指令不能扒开看里面。所以Tainted机制防范的从来不是“画图”这个动作而是“把画布内容作为数据资产提取出来”这个行为。它的目标非常明确防止恶意网站通过Canvas实施一种叫Canvas Fingerprinting画布指纹识别的高级追踪技术或者更危险的——跨域图像窃取Cross-Origin Image Theft。想象这样一个攻击场景你访问了一个恶意网站它悄悄创建一个img标签src指向你银行网站的个人头像URL假设那个URL是公开可访问的。然后它用drawImage把这张头像画到一个隐藏的Canvas上。如果没有Tainted机制它立刻就能调用getImageData()读取你头像的每一个像素从而获取你的真实长相并上传到攻击者服务器。而银行网站的图片URL很可能因为是静态资源压根没配CORS头——这就成了完美的信息泄露通道。Tainted机制就是在这个环节啪地一声关上了门。2.3 crossOrigin属性不是“开关”而是“申请书”且必须双方配合这是另一个高频误解。很多人以为给img加上crossOrigin anonymous就等于给Canvas开了绿灯。大错特错。crossOrigin属性本质上是一份向目标服务器发出的CORS预检请求Preflight Request的声明它本身不产生任何魔法它的作用仅仅是告诉浏览器“请用CORS模式去加载这张图”。具体流程如下你设置img.crossOrigin anonymous浏览器准备发起HTTP请求时发现这个标志于是它会在请求头中自动添加Origin: https://your-site.com如果该请求属于“非简单请求”比如带自定义头还会先发一个OPTIONS预检请求目标服务器收到请求后必须在响应头中返回Access-Control-Allow-Origin且其值必须匹配Origin头或者为通配符*浏览器收到响应检查CORS头。如果匹配才认为这张图是“合法进口”允许后续drawImage操作不会污染Canvas如果不匹配请求会被浏览器拦截此时你甚至看不到图片或者即使图片加载成功比如服务器没配CORS头drawImage之后Canvas也会立刻被标记为tainted。注意crossOrigin use-credentials是另一个值它表示请求需要携带cookie等凭据此时Access-Control-Allow-Origin绝不能是*必须是精确的域名否则浏览器会直接拒绝。而anonymous是最常用、最安全的选择它不发送凭据因此服务器可以用*来响应。所以修复跨域报错从来不是前端单方面的事。它是一个前端申请 后端批准的协作过程。前端负责正确发出申请crossOrigin后端负责盖章批准CORS响应头。缺一不可。3. 实操修复方案从“试了没用”到“稳如磐石”的全流程3.1 前端侧三步精准诊断与配置告别盲目加crossOrigin很多开发者的问题不是不会加crossOrigin而是加得不对、加得不全、加得不及时。下面是一个经过千次线上问题排查验证的标准化流程第一步确认图片加载时机——crossOrigin必须在src赋值前设置这是90%的“加了没用”案例的根源。错误写法const img new Image(); img.src https://cdn.example.com/photo.jpg; // 错此时浏览器已开始加载crossOrigin无效 img.crossOrigin anonymous; // 晚了正确写法两种// 方式一先设crossOrigin再设src const img new Image(); img.crossOrigin anonymous; // ✅ 关键必须在src之前 img.src https://cdn.example.com/photo.jpg; // 方式二用setAttribute同样要早于src const img new Image(); img.setAttribute(crossorigin, anonymous); // ✅ 注意是crossorigin不是crossOrigin img.src https://cdn.example.com/photo.jpg;第二步检查图片加载状态——用onload/onerror而非自然流不要依赖img.complete因为它在异步加载中不可靠。务必使用事件监听const img new Image(); img.crossOrigin anonymous; img.onload function() { // ✅ 此时图片已加载完成且CORS校验通过 ctx.drawImage(img, 0, 0); // 现在可以安全调用toDataURL了 const dataUrl canvas.toDataURL(image/png); }; img.onerror function() { console.error(图片加载失败可能是CORS被拒或404); }; img.src https://cdn.example.com/photo.jpg;第三步终极验证——用console.dir检查img.naturalWidth这是一个鲜为人知但极其有效的调试技巧。在img.onload回调里打印img.naturalWidth如果naturalWidth 0说明图片加载成功且CORS校验通过因为浏览器只在CORS通过后才认为图片“可用”如果naturalWidth 0说明图片加载失败极大概率是CORS被服务器拒绝了而不是图片不存在。img.onload function() { console.log(Image naturalWidth:, img.naturalWidth); // ✅ 调试神技 if (img.naturalWidth 0) { console.warn(CORS likely failed! Check network tab.); } };3.2 后端侧服务器配置详解覆盖Nginx、Node.js、Python Flask三大场景前端配置只是半张票后端才是真正的“签证官”。以下是主流服务器环境的CORS头配置确保你照着抄就能用。Nginx配置推荐性能最优在你的server块或location块中添加# 允许所有来源开发环境 add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods GET, OPTIONS; add_header Access-Control-Allow-Headers DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range; add_header Access-Control-Expose-Headers Content-Length,Content-Range; # 或者生产环境精确匹配更安全 # add_header Access-Control-Allow-Origin https://your-production-domain.com; # add_header Access-Control-Allow-Credentials true; # 如果用了use-credentials # 处理预检请求 if ($request_method OPTIONS) { add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods GET, OPTIONS; add_header Access-Control-Allow-Headers DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range; add_header Access-Control-Max-Age 1728000; add_header Content-Type text/plain; charsetutf-8; add_header Content-Length 0; return 204; }Node.js (Express) 配置const express require(express); const app express(); // 使用cors中间件推荐一行解决 const cors require(cors); app.use(cors({ origin: *, // 或 [https://your-domain.com] credentials: false // 对应 crossOriginanonymous })); // 或者手动设置头更透明 app.use((req, res, next) { res.header(Access-Control-Allow-Origin, *); res.header(Access-Control-Allow-Methods, GET, OPTIONS); res.header(Access-Control-Allow-Headers, DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range); if (req.method OPTIONS) { res.sendStatus(204); } else { next(); } });Python Flask 配置from flask import Flask, send_from_directory, make_response from flask_cors import CORS app Flask(__name__) # 一行启用CORS推荐 CORS(app, origins*) # 或者手动装饰路由 app.route(/images/path:filename) def serve_image(filename): response make_response(send_from_directory(static/images, filename)) response.headers[Access-Control-Allow-Origin] * response.headers[Access-Control-Allow-Methods] GET, OPTIONS response.headers[Access-Control-Allow-Headers] DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range return response实操心得我在线上遇到过最诡异的一次问题是CDN缓存了没有CORS头的旧图片响应。解决方案是在CDN后台为图片目录如/images/*强制添加Access-Control-Allow-Origin: *的响应头或者刷新CDN缓存。这提醒我们CORS头的生效位置可能在源站、CDN、反向代理等多个环节排查时要层层下探。3.3 终极兜底方案当后端无法修改时服务端代理与Canvas像素级“脱敏”现实很骨感。很多时候你要加载的图片来自第三方图床、微信头像接口、甚至某些老旧的政府网站它们的服务器你根本无法控制。这时crossOrigin就是一纸空文。别慌还有两条路可走。方案一前端构建简易代理适用于Node.js后端原理让自己的后端充当“翻译官”先用自己的域名去请求第三方图片拿到响应后再把图片数据和正确的CORS头一起返回给前端。Node.js Express示例const axios require(axios); app.get(/proxy/image, async (req, res) { const { url } req.query; try { // 用axios请求第三方图片 const response await axios({ method: GET, url, responseType: arraybuffer, // 关键必须是二进制 headers: { User-Agent: Mozilla/5.0 (compatible; CanvasProxy/1.0) } }); // 将响应头和数据原样转发给前端 res.set({ Content-Type: response.headers[content-type], Access-Control-Allow-Origin: *, Cache-Control: public, max-age31536000 // 长缓存减轻压力 }); res.send(response.data); } catch (error) { res.status(500).send(Proxy failed); } });前端调用const img new Image(); img.crossOrigin anonymous; // 依然要加因为代理地址是你的域名 img.src /proxy/image?urlhttps://third-party.com/photo.jpg;方案二Canvas像素级“脱敏”——绕过Tainted检查的合法技巧这是个鲜为人知的“灰色技巧”但它完全符合规范且被所有现代浏览器支持。核心思想是既然浏览器只检查“是否drawImage过跨域图”那我们就不drawImage而是用其他方式把图片“放”到Canvas上。具体做法利用createPattern创建一个重复的图片纹理然后用fillRect填充。createPattern返回的CanvasPattern对象在创建时不会触发Tainted检查因为它不涉及像素的“拷贝”而只是创建一个引用。const img new Image(); img.crossOrigin anonymous; // 还是要加但即使失败下面也能work img.onload function() { const pattern ctx.createPattern(img, no-repeat); ctx.fillStyle pattern; ctx.fillRect(0, 0, img.width, img.height); // ✅ 此时Canvas未被污染可以安全调用toDataURL const dataUrl canvas.toDataURL(image/png); }; img.src https://third-party.com/photo.jpg;注意此方法有局限性它只能实现“平铺”或“不重复”的填充无法像drawImage那样进行缩放、裁剪、旋转等复杂变换。但对于只需要“显示一张图”的场景比如头像展示、背景图它是零成本、零后端改动的完美方案。4. 常见问题与排查技巧实录那些年我们踩过的坑4.1 “我加了crossOrigin也看到图片了但toDataURL还是报错”——99%是这个原因这是最高频、最让人抓狂的问题。现象图片明明正常显示在页面上控制台也没有404但一调toDataURL就报tainted。根本原因只有一个图片虽然显示了但它的CORS校验失败了浏览器允许它显示因为img标签本身不触发CORS检查但不允许它被drawImage到Canvas上因为Canvas的Tainted机制更严格。排查步骤打开浏览器开发者工具切到Network标签页刷新页面找到那张图片的请求点击它查看Response Headers重点检查是否存在Access-Control-Allow-Origin头。如果完全没有或者它的值是null、undefined、或者一个完全不匹配的域名那就坐实了CORS失败。实操心得我曾在一个项目里发现CDN服务商的文档里写着“默认开启CORS”结果实际配置却是关闭的。最后是通过Network面板里逐个对比请求头才揪出这个坑。记住眼见为实文档有时会骗人。4.2 “本地开发一切正常上线就报错”——环境差异的隐形杀手本地开发时你很可能把图片放在localhost:3000的/public目录下这属于同源自然不会触发CORS。但上线后图片被放到CDN上域名变成了https://cdn.your-app.com立刻变成跨域。解决方案开发环境统一用代理在vue.config.js或webpack.config.js中配置devServer proxy让/api和/images都代理到本地后端保证开发和生产环境的网络路径一致。构建时注入环境变量在package.json的scripts里用--env VUE_APP_IMAGE_BASE_URLhttps://cdn.your-app.com然后在代码里用process.env.VUE_APP_IMAGE_BASE_URL拼接图片URL确保无论在哪图片都走CDN。4.3 “微信头像、QQ头像总是tainted”——社交平台的特殊限制微信、QQ等平台的头像接口出于安全考虑通常会返回Access-Control-Allow-Origin: null这是个历史遗留坑或者只允许特定的、白名单内的域名访问甚至对User-Agent做校验拒绝非浏览器的请求。应对策略首选服务端代理这是最可靠的方式因为代理请求的User-Agent可以伪造且Origin头由你控制。次选使用平台提供的JS-SDK比如微信JS-SDK的wx.downloadImage它会在微信内置浏览器里绕过CORS限制但仅限于微信环境。绝对避免尝试用fetchblobURL.createObjectURL这种“曲线救国”方式因为URL.createObjectURL生成的blob URL依然会继承原始图片的“污染”状态drawImage后Canvas照样tainted。4.4 “Canvas被污染了能清除吗”——关于reset的残酷真相这是个令人心碎的问题。答案是不能。一旦Canvas被标记为tainted这个状态是永久性的、不可逆的。你无法通过ctx.clearRect(0,0,canvas.width,canvas.height)、canvas.width canvas.width重置尺寸、甚至canvas.remove()再重建来清除这个标记。唯一有效的方法是创建一个全新的Canvas元素// ❌ 无效 canvas.width canvas.width; // ✅ 唯一有效 const newCanvas document.createElement(canvas); newCanvas.width canvas.width; newCanvas.height canvas.height; const newCtx newCanvas.getContext(2d); // 然后把你想保留的内容重新绘制到newCanvas上实操心得我在做一个实时截图工具时就因为没意识到这点导致用户连续截图几次后Canvas就永久报废了。后来我把“截图”逻辑重构为每次截图都创建新Canvas绘制完成后把结果toDataURL然后立刻丢弃这个Canvas。内存占用反而更可控。5. 进阶思考Tainted机制的边界与未来演进5.1 Web Workers与OffscreenCanvas能否绕过Tainted随着Web Workers和OffscreenCanvas的普及很多人寄希望于“把Canvas放到Worker里是不是就不用管CORS了”。答案是不能而且更严格。OffscreenCanvas的设计初衷是将Canvas渲染从主线程剥离提升性能但它完全继承了主线程Canvas的所有安全策略。在Worker中你依然需要为ImageBitmap或createImageBitmap的源图片设置CORS否则transferToImageBitmap后得到的ImageBitmap依然是“污染”的无法用于OffscreenCanvas的transferFromImageBitmap。更关键的是OffscreenCanvas的getContext(2d)返回的OffscreenCanvasRenderingContext2D其getImageData等方法返回的是一个Promise而不是同步结果。这是因为数据需要从Worker线程传回主线程而传输过程本身就需要序列化这进一步增加了复杂性。所以想用Worker绕过Tainted是南辕北辙。5.2 WebGPU与未来的“像素主权”WebGPU作为下一代Web图形API其安全模型比WebGL更激进。它引入了GPUTexture的概念而纹理的创建必须显式指定usage标志。其中GPUTextureUsage.COPY_SRC允许从纹理复制数据和GPUTextureUsage.RENDER_ATTACHMENT允许作为渲染目标是互斥的。这意味着一个被用作渲染目标的纹理你根本无法从中读取像素——这比Canvas的Tainted机制更彻底它从源头上就切断了“渲染-读取”的通路。这预示着一个趋势浏览器正在将“像素主权”收得越来越紧。未来任何试图从GPU内存中“偷”数据的行为都会面临更底层、更硬件级的封锁。对于开发者而言这不是倒退而是进步。它迫使我们放弃那些“投机取巧”的数据提取方式转而拥抱更规范、更安全的数据流转模型比如通过WebRTC的getVideoTracks获取媒体流或者通过MediaRecorder录制这些都是浏览器明确授权、且受控的数据出口。5.3 一个被忽视的“安全盟友”Content-Security-Policy (CSP)很多人不知道Content-Security-Policy这个HTTP头也能对Canvas安全起到辅助作用。通过设置img-src指令你可以限制哪些域名的图片可以被加载Content-Security-Policy: img-src self https://trusted-cdn.com;这样即使你的代码里不小心写死了某个恶意域名的图片浏览器也会在加载阶段就将其拦截从源头上杜绝了“非法图片”进入Canvas的可能性。它和Tainted机制是互补的CSP是“事前防御”Tainted是“事后封堵”。我个人在实际操作中的体会是Tainted Canvas机制与其说是一个麻烦不如说是一面镜子照出了我们对Web安全边界的模糊认知。每一次为了解决它而深入研究CORS、HTTP头、浏览器渲染流程都让我们离“真正理解Web”更近了一步。它不是一个需要被“绕过”的障碍而是一个邀请邀请我们以更严谨、更敬畏的态度去编写每一行前端代码。当你下次再看到那行红色报错时别再烦躁地复制粘贴试着打开Network面板亲手验证一下那个Access-Control-Allow-Origin头——那一刻你就已经从一个调用API的开发者成长为一个理解平台规则的架构师了。
RELATED

相关推荐

Matlab魔术公式轮胎模型实现与参数辨识指南

Matlab魔术公式轮胎模型实现与参数辨识指南

做车辆动力学仿真绕不开轮胎模型,魔术公式轮胎模型(Magic Formula Tyre Model)是我这几年用得最多的一个半经验模型。它由荷兰代尔夫特理工大学的Pacejka教授提出,用一组紧凑的正弦-反正切复合函数,把轮胎的纵向力、侧…

📅 2026/10/9 22:33:39
Vibe Coding 必须知道的 7 个工具平台:从 GitHub 到 TaoToken 的完整链路

Vibe Coding 必须知道的 7 个工具平台:从 GitHub 到 TaoToken 的完整链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/9 22:33:39
时间不是物理量,而是必须显式约定的系统契约

时间不是物理量,而是必须显式约定的系统契约

1. 时间系统不是“默认选项”,而是必须主动选择的底层协议很多人第一次在代码里看到new Date()输出一串带GMT或UTC字样的字符串时,下意识觉得:“哦,这是电脑自己算出来的时间,应该没错。”——这个念头就是所有时间相关…

📅 2026/10/9 22:33:39
MORE NEWS

更多资讯

📰

GitHub日榜深度阅读:五步筛出真正值得跟进的优质开源项目

2026 年 10 月 3 日,周六,早上九点出头。我照例打开 GitHub 的 Trending 日榜,准备花十分钟看一眼过去 24 小时哪些项目冲了上来,结果这一刷就是三页。长假前后本来就是开发者集中发版的时间段,再叠加周末效应&#xf…

📰

基于PCA9422和STM32F746ZG的低功耗便携设备电源管理设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

HarmonyOS 7 系统能力 07|设备信息

这一篇解决的问题:设备差异别写死在页面里。我们不背 API,而是从一个真实页面需求出发,把“设备信息读取与能力判断”做成能继续扩展的工程写法。先说问题:功能能跑,不等于接对了 做 HarmonyOS 7 页面时,最…

📰

GitHub日榜趋势速报:从热度机制到数据采集的完整指南

每天打开 GitHub 看日榜,已经成了我雷打不动的习惯。尤其像“2026-10-02”这种普通工作日,榜单上往往是两类东西:一类是蹭热点冲上来的小工具,另一类是真正解决痛点的硬核项目。但说实话,大多数人的姿势不对——只盯着…

📰

电力行业智能管理小程序:从智能电表集成到电力需求预测的实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

Less 预处理器实战指南:用变量、Mixin 与嵌套编写可维护的 CSS(learnxinyminutes-docs 中文教程精讲)

文档教程 【免费下载链接】learnxinyminutes-docs Code documentation written as code! How novel and totally my idea! 项目地址: https://gitcode.com/gh_mirrors/le/learnxinyminutes-docs 点击查看 免费下载 Less 是一种 CSS 预处理器,在原生 CSS…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬