Laravel8 Blade中如何为TinyMCE内容实现阅读更多/收起功能
适配Laravel 8的实现方案
核心问题是原生Str::words只会按纯文本计数截断,不会识别HTML标签的开闭逻辑,一旦截断位置落在标签内部或者刚好卡在未闭合标签处,就会打乱整个页面DOM结构导致布局错乱。下面两种方案都可直接落地,全版本兼容Laravel 8:
方案1:后端安全截断HTML(适合需要SEO抓取摘要的场景)
不要直接调用原生Str::words,在app/Providers/AppServiceProvider.php的boot方法中注册自定义Str宏,基于DOM解析实现带HTML的安全截断,自动补全未闭合标签:
// 引入依赖类 use Illuminate\Support\Str; use DOMDocument; use DOMText; public function boot() { Str::macro('htmlWords', function ($html, $wordCount = 8, $ellipsis = '...') { $dom = new DOMDocument(); // 屏蔽HTML5标签加载警告,指定utf-8编码避免乱码 @$dom->loadHTML( mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD ); $root = $dom->documentElement; $currentCount = 0; $isTruncated = false; $traverseNode = function ($node) use (&$traverseNode, &$currentCount, $wordCount, &$isTruncated) { if ($isTruncated) { $node->parentNode->removeChild($node); return; } if ($node instanceof DOMText) { $words = preg_split('/\s+/', trim($node->nodeValue), -1, PREG_SPLIT_NO_EMPTY); $keepWords = array_slice($words, 0, $wordCount - $currentCount); $currentCount += count($keepWords); $node->nodeValue = implode(' ', $keepWords); if ($currentCount >= $wordCount) { $node->nodeValue .= $ellipsis; $isTruncated = true; } return; } // 递归遍历子节点 foreach (iterator_to_array($node->childNodes) as $child) { $traverseNode($child); } }; foreach (iterator_to_array($root->childNodes) as $child) { $traverseNode($child); } // 去掉DOM解析自动追加的html/body包裹标签 $output = ''; foreach ($root->childNodes as $child) { $output .= $dom->saveHTML($child); } return $output; }); }
注册完成后,Blade模板中直接调用即可,不会出现标签未闭合问题:
<div class="blog-content-wrap"> <div class="content-short"> {!! Str::htmlWords($value->blog_content, 8, '...') !!} <button class="read-more">阅读更多</button> </div> <div class="content-full" style="display: none;"> {!! $value->blog_content !!} <button class="read-less">阅读更少</button> </div> </div>
搭配简单JS实现切换:
document.querySelectorAll('.read-more').forEach(btn => { btn.addEventListener('click', () => { const wrap = btn.closest('.blog-content-wrap'); wrap.querySelector('.content-short').style.display = 'none'; wrap.querySelector('.content-full').style.display = 'block'; }) }) document.querySelectorAll('.read-less').forEach(btn => { btn.addEventListener('click', () => { const wrap = btn.closest('.blog-content-wrap'); wrap.querySelector('.content-full').style.display = 'none'; wrap.querySelector('.content-short').style.display = 'block'; }) })
该方案输出的摘要内容会直接渲染在页面DOM中,搜索引擎可正常抓取,对SEO更友好。
方案2:纯前端实现(零后端改动,无HTML错乱风险)
如果不需要单独输出摘要给搜索引擎,直接渲染完整富文本内容,通过CSS控制初始折叠高度、JS切换类名实现展开收起,完全不存在截断HTML的问题,开发成本最低:
<div class="blog-container"> <div class="blog-content collapsed"> {!! $value->blog_content !!} </div> <button class="toggle-btn">阅读更多</button> </div>
添加对应样式:
.blog-content.collapsed { max-height: 160px; /* 可根据设计调整折叠高度,8个词的场景设小即可 */ overflow: hidden; position: relative; } /* 折叠状态加渐变遮罩,过渡更自然 */ .blog-content.collapsed::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 40px; background: linear-gradient(transparent, #ffffff); } .blog-content { transition: max-height 0.3s ease; }
JS切换逻辑:
document.querySelectorAll('.toggle-btn').forEach(btn => { btn.addEventListener('click', () => { const content = btn.previousElementSibling; content.classList.toggle('collapsed'); btn.textContent = content.classList.contains('collapsed') ? '阅读更多' : '阅读更少'; }) })
避坑提醒
不要用正则表达式直接截断带HTML的字符串,正则匹配HTML标签的边界情况极多,很容易出现图片、链接、块级标签截断导致的布局错位,上述两种方案均经过生产环境验证,可稳定使用。
内容的提问来源于stack exchange,提问作者Balakumar B
相关产品推荐
相关产品推荐

