我从2013年开始接触织梦DedeCMS,那会儿一边给本地中小企业做企业站,一边折腾自己的博客,前前后后给大概四十多个DedeCMS站点做过手机端适配。今天这篇我把处理「文章正文图片在手机端撑破屏幕」这个老大难问题的完整方案写下来,包含我踩过的坑、最终落地的代码、以及为什么这么写。
这篇文章面向的是用DedeCMS 5.7 SP2、UTF8或GBK版本搭建的站点,文章页通过{dede:field.body/}标签输出正文的场景。如果你的内容是在2018年以前用旧版CKEditor或FCKEditor录入的,几乎一定能在文章里搜到带硬编码width/height的img标签——这正是这篇文章要解决的核心。
问题到底出在哪里:从HTML源头说起
先复现现象。我自己有个2014年上线的客户站,2018年之前几百篇老文章都是用旧版CKEditor录入的。作者插入图片时习惯直接拖入,编辑器会自动把图片真实像素宽高写进img标签,长这样:
<img src="/uploads/allimg/200101/1-200101120.jpg" width="800" height="533" alt="示例" />或者更糟糕——把宽高直接写进了内联style:
<img src="/uploads/allimg/200101/1-200101120.jpg" style="width:800px;height:533px;" />这两种写法在PC端没问题,正文容器一般有800px以上的可用宽度。但是切到手机端模板,正文容器宽度可能只有360px,浏览器看到width="800"这个硬编码值,就会按800像素渲染,结果就是图片把整个页面横向撑开,用户必须左右拖动才能看完,体验非常糟糕。
Google的Mobile-Friendly Test会直接判定这种页面Mobile Unfriendly,「点击元素彼此过近」「内容超出视口宽度」两条警告会同时触发。我那个站点在2019年上半年就因为这个问题,移动端关键词排名掉了一大截,后来才意识到原因。修复后两个月,移动端流量回升大约35%。
为什么纯CSS方案搞不定这个问题
很多教程会说「在手机端样式表里加img { max-width: 100%; height: auto; }就完事了」。这话只对一半。
这条CSS规则确实可以让图片宽度不超过容器,但它解决不了HTML里硬编码的width和height属性带来的问题。具体来说:
- 如果
img同时有width="800" height="533"属性和max-width:100%; height:auto样式,浏览器会按CSS缩放宽度到容器宽度,这部分没问题。 - 但
height属性会被当作原始尺寸约束,浏览器在等比缩放时,老版本Safari和某些国产浏览器会出现高度计算错误,图片被压扁或者拉长。 - 内联
style里的width:800px优先级高于外部CSS的max-width:100%,会直接覆盖掉移动端样式规则。 - 带
onload属性触发的JS缩放(旧版CKEditor的图片插件会写这种代码)会跟CSS冲突,部分浏览器表现为闪烁后还原成原尺寸。
我当年就吃过这个亏,加完CSS在Chrome模拟器里看着挺好,结果用户拿真机一访问,部分图片还是溢出。最后排查半天,发现问题就是内联style没被覆盖。
所以正确的做法是:从HTML源头把硬编码尺寸属性全部去掉,再配合CSS的max-width:100%,才能保证万无一失。
方案一:用PHP在输出阶段动态剥离尺寸属性(推荐)
这是我现在用的标准方案。它的好处是不动数据库里的存量内容,只在内容输出到页面时实时清洗。如果以后想回滚,把PHP逻辑去掉即可,原始内容毫发无伤。
具体实现:在DedeCMS的核心模板文件article_default.htm(或你正在用的文章模板)里,找到{dede:field.body/}这一行。它的作用是输出文章正文。我们要把它替换成一段PHP代码,对正文做正则清洗后再输出。
但DedeCMS模板里直接写PHP是行不通的——模板引擎会把它当成纯文本输出。需要先在/include/extend.func.php里定义一个自定义函数,再在模板里通过function调用。
打开/include/extend.func.php,在文件末尾追加:
<?php
function MobileImgFix($body) {
// 第一步:剥离 img 标签里的 width 和 height 属性
$body = preg_replace('/(<img[^>]*?)\s+width\s*=\s*["\']?\d+(?:px)?["\']?/i', '$1', $body);
$body = preg_replace('/(<img[^>]*?)\s+height\s*=\s*["\']?\d+(?:px)?["\']?/i', '$1', $body);
// 第二步:清理内联 style 里的 width 和 height 像素声明
$body = preg_replace_callback(
'/<img([^>]*?)style\s*=\s*["\']([^"\']*)["\']/i',
function ($m) {
$cleaned = preg_replace('/(?:^|;)\s*(width|height)\s*:\s*\d+(?:px|%|em|rem)?\s*(?=;|$)/i', '', $m[2]);
$cleaned = trim($cleaned, ' ;');
if ($cleaned === '') {
return '<img' . $m[1];
}
return '<img' . $m[1] . 'style="' . htmlspecialchars($cleaned, ENT_QUOTES) . '"';
},
$body
);
// 第三步:去掉旧编辑器留下的 onload="this.width=this.naturalWidth" 之类的脚本属性
$body = preg_replace('/(<img[^>]*?)\s+onload\s*=\s*["\'][^"\']*["\']/i', '$1', $body);
return $body;
}
?>保存文件后,回到模板。把原来的:
{dede:field.body/}替换成:
{dede:field name='body' function='MobileImgFix(@me)'/}这个语法是DedeCMS模板引擎支持的「函数过滤」用法,@me会被替换成body字段的当前值。整段流程下来,文章页输出时所有img标签里的硬编码尺寸属性都会被实时清洗。
清洗完成后,再在主题的全局CSS里加一行:
.article-body img {
max-width: 100%;
height: auto;
display: block;
margin: 12px auto;
}这样图片在任意屏幕宽度下都会等比缩放,不会撑破容器,也避免了老Safari的高度错算。
方案二:批量改写数据库里的存量数据
方案一不动存量数据,但有一个副作用:每次页面渲染都要跑一次正则,对页面缓存命中率有微弱影响(开了静态化可以忽略)。如果你的站点对性能极致敏感、且不打算回滚,可以选择方案二,直接把数据库里的存量内容改写一遍。
这个动作风险比方案一高,必须先备份。备份命令:
mysqldump -u 用户名 -p 数据库名 dede_addonarticle > addonarticle_backup_$(date +%Y%m%d).sql然后写一个PHP脚本,遍历dede_addonarticle表的body字段,套用上面MobileImgFix函数的逻辑后写回。脚本核心:
require_once 'include/common.inc.php';
$dsql->SetQuery("SELECT aid,body FROM `#@__addonarticle`");
$dsql->Execute();
$count = 0;
while ($row = $dsql->GetArray()) {
$cleaned = MobileImgFix($row['body']);
if ($cleaned !== $row['body']) {
$cleaned = addslashes($cleaned);
$dsql->ExecNoneQuery("UPDATE `#@__addonarticle` SET body='$cleaned' WHERE aid={$row['aid']}");
$count++;
}
}
echo "已清洗 $count 条文章";跑完后随便挑10篇文章打开看一眼源码,确认img标签里没有width/height属性即可。
这个方案的不可逆性需要警惕:一旦数据库里的内容被改写,原始尺寸属性就丢了,不能再恢复。所以备份SQL文件至少要保留3个月以上。
方案三:纯前端JS兜底(应急用)
如果你暂时不能改主题模板,也不愿意动数据库,还有一种纯前端兜底方案。在footer.htm底部加这段JS:
<script>
(function(){
var imgs = document.querySelectorAll('.article-body img');
for (var i = 0; i < imgs.length; i++) {
imgs[i].removeAttribute('width');
imgs[i].removeAttribute('height');
imgs[i].style.width = '';
imgs[i].style.height = '';
imgs[i].style.maxWidth = '100%';
imgs[i].style.height = 'auto';
}
})();
</script>这个方案的缺点很明显:在JS执行之前,浏览器已经按硬编码尺寸渲染了一遍,用户会看到一瞬间的「图片溢出再缩回」抖动。Google的Page Speed Insights也会扣分(CLS指标受影响)。所以这只是过渡方案,最终还是要切换到方案一或方案二。
不能忽略的细节:阿里云OSS和七牛云的图片处理
如果你的DedeCMS站点把图片放到了阿里云OSS、七牛云、又拍云这类对象存储,并且用了它们的图片处理功能,事情会更复杂一些。
这些云存储一般支持URL参数化的图片处理,比如阿里云OSS的?x-oss-process=image/resize,w_800会让OSS服务端返回宽800px的缩图。如果你之前是按PC尺寸预设了URL参数,手机端拿到的还是800px大图,浪费流量。
处理思路是:在MobileImgFix函数里加一段动态根据视口宽度调整URL参数的逻辑。但视口宽度只有客户端知道,所以这段逻辑要写在前端而不是后端。具体可以搭配srcset属性:
$body = preg_replace_callback(
'/<img\s+src=["\']([^"\']*?\.(jpg|jpeg|png|webp))["\'][^>]*?>/i',
function ($m) {
$base = $m[1];
$sep = (strpos($base, '?') !== false) ? '&' : '?';
$small = $base . $sep . 'x-oss-process=image/resize,w_400';
$medium = $base . $sep . 'x-oss-process=image/resize,w_800';
$large = $base . $sep . 'x-oss-process=image/resize,w_1200';
return sprintf(
'<img src="%s" srcset="%s 400w, %s 800w, %s 1200w" sizes="(max-width: 480px) 100vw, (max-width: 1024px) 80vw, 800px" loading="lazy" />',
$medium, $small, $medium, $large
);
},
$body
);加完之后浏览器会根据视口和DPR自动选最合适的图片版本,移动端流量能省掉一半以上。
七牛云的特殊坑:URL裁剪参数与CDN缓存
七牛云的图片处理参数是?imageView2/2/w/800这种格式,处理逻辑跟阿里云OSS不一样。如果你的站点早期用的是七牛旧版API(?imageView/2/w/800,不带imageView2),切换到新参数时要在七牛后台手动刷新一遍CDN缓存,否则会出现「同一张图URL变了但CDN还在返回旧版本」的奇怪现象。
我去年在一个客户的迁移项目里栽过这个跟头。改完模板上线,开发环境一切正常,结果生产环境的部分图片返回的还是旧URL的版本(CDN边缘节点没刷),以为代码写错了,排查了一晚上才反应过来。从那之后,凡是涉及七牛云URL变更的,我都在发布脚本最后加一行qrefresh命令把CDN刷干净。
移动端图片懒加载与LCP优化
解决了图片溢出之后,下一步要考虑的是首屏图片的Largest Contentful Paint(LCP)指标。Google Core Web Vitals对LCP的阈值是2.5秒,超过这个值会扣分。
对DedeCMS来说,常见做法是给所有非首屏图片加loading="lazy",让浏览器在用户滚动到附近时才发请求。但要注意:首屏图片绝对不能加loading="lazy",否则LCP指标会变得更糟,因为lazy load会引入额外的判断延迟。
我的处理方式是在MobileImgFix里只给「文章正文里的非第一张图」加lazy标记。判断方法是用一个静态变量在循环里跟踪是否是第一张img:
function MobileImgFix($body) {
static $isFirstInArticle = true;
$body = preg_replace_callback('/<img([^>]*?)>/i', function ($m) use (&$isFirstInArticle) {
$tag = '<img' . $m[1];
if (strpos($tag, 'loading=') === false) {
if ($isFirstInArticle) {
$tag .= ' fetchpriority="high"';
$isFirstInArticle = false;
} else {
$tag .= ' loading="lazy" decoding="async"';
}
}
return $tag . '>';
}, $body);
return $body;
}这样首屏图给浏览器fetchpriority="high"提示让它优先抓取,之后所有图都lazy load,LCP指标能压在1.5秒以内。
常见问题解答
方案一的MobileImgFix函数会不会影响PC端的图片显示?
不会。剥离width/height属性后,PC端的图片宽度会被CSS的max-width:100%限制在文章容器宽度(一般800px左右)以内,超过的会等比缩放,不超过的按原始尺寸显示。如果你希望PC端的图片显示比容器宽(撑出版心),可以把CSS改成.article-body img { max-width: 1024px; height: auto; },但要注意PC端最大宽度不要超过对象存储返回的最大版本,否则会糊。
用了{dede:field name='body' function='MobileImgFix(@me)'/}之后,DedeCMS的SEO插件还会正常工作吗?
会,因为function调用是在模板渲染阶段处理的,DedeCMS的SEO相关字段(seotitle、keywords、description)都是从主表读取,跟body字段无关。但要注意:如果你装了某些会扫描正文做关键词提取的插件,那些插件读取的可能是数据库原始内容(包含尺寸属性),不影响功能但会导致提取结果中有HTML残留。
为什么有时候清洗完img标签,图片还是会变形?
三个常见原因:第一,CSS里有更具体的选择器(比如.article-body table img)覆盖了通用规则,要把所有相关选择器都加上max-width:100%; height:auto;第二,浏览器缓存了清洗前的页面,强制刷新(Ctrl+F5)就好;第三,图片本身在对象存储侧被裁剪了非等比的版本,源图就是变形的,这种要回到对象存储后台检查。
方案二批量改写数据库时,能不能用LIKE做条件过滤减少更新量?
可以,能显著降低UPDATE量。在SELECT阶段加WHERE body LIKE '%width=%' OR body LIKE '%height=%' OR body LIKE '%style=%',只对真正包含尺寸属性的文章做处理。我自己跑过的一个站点1.2万篇文章,加这个过滤之后只命中了4300多篇,UPDATE耗时从15分钟降到5分钟以内。
开了PageCache插件之后,方案一的清洗会被缓存吗?
会被缓存,这是好事。PageCache一般是把渲染后的HTML缓存到磁盘或内存,下一次同一URL请求直接返回缓存,MobileImgFix只在缓存未命中时跑。所以即便正则有性能开销,对生产环境的实际影响几乎可以忽略。但要注意PageCache和移动端模板的配合——如果是PC和移动同URL不同模板,要确保PageCache的key包含设备类型,否则会出现「PC用户拿到了移动端缓存」的串读。
用srcset之后,DedeCMS的{dede:field.body/}调用方式还兼容旧主题吗?
兼容。srcset是HTML5标准属性,不支持srcset的老浏览器(IE11之前)会fallback到src属性指向的图片。也就是说,加了srcset对现代浏览器是优化,对老浏览器是无影响。但前提是src属性要保留并指向一个尺寸适中的版本(800px左右),不能光给srcset。
这套方案在DedeCMS的二次开发分支(如DedeBIZ、DedeCMSV6)上还能用吗?
能,模板语法和函数过滤机制在DedeBIZ和DedeCMSV6里都保留了。但要注意:DedeCMSV6改了部分核心常量名,extend.func.php路径可能从/include/extend.func.php改到/system/extend.func.php,定位时先find . -name extend.func.php确认。模板里的函数调用语法是一致的。