DedeCMS手机端图片溢出修复:3种实战方案

DedeCMS文章页图片在手机端撑破屏幕怎么办?本文给出PHP动态清洗、批量改写数据库、纯前端JS兜底三种实战方案,并附srcset和LCP优化的完整代码。

更新 22 分钟阅读 1,462 阅读

我从2013年开始接触织梦DedeCMS,那会儿一边给本地中小企业做企业站,一边折腾自己的博客,前前后后给大概四十多个DedeCMS站点做过手机端适配。今天这篇我把处理「文章正文图片在手机端撑破屏幕」这个老大难问题的完整方案写下来,包含我踩过的坑、最终落地的代码、以及为什么这么写。

这篇文章面向的是用DedeCMS 5.7 SP2、UTF8或GBK版本搭建的站点,文章页通过{dede:field.body/}标签输出正文的场景。如果你的内容是在2018年以前用旧版CKEditor或FCKEditor录入的,几乎一定能在文章里搜到带硬编码width/heightimg标签——这正是这篇文章要解决的核心。

问题到底出在哪里:从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里硬编码的widthheight属性带来的问题。具体来说:

  • 如果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相关字段(seotitlekeywordsdescription)都是从主表读取,跟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确认。模板里的函数调用语法是一致的。

分享到
标签
版权声明

本文标题:《DedeCMS手机端图片溢出修复:3种实战方案》

本文链接:https://zhangwenbao.com/dedecms-mobile-picture-adaptation.html

版权声明:本文原创,转载请注明出处和链接。许可协议: CC BY-NC-SA 4.0

继续阅读
发表评论
分享到微信 或在下方手动填写
支持 Ctrl + Enter 提交