uniapp 处理富文本中的媒体元素尺寸

308 阅读3分钟

功能与场景

在项目中会有文章内容或其他什么东西是后台富文本编辑的,需要前端展示富文本内容

正常来说是没什么问题的,但是总有客户喜欢上传1k+ 甚至是2k+ 像素的图片或视频,这就导致在移动端中渲染时无法完整的渲染出图片/视频

如下图

image.png

所以就封装了这个处理富文本中的媒体元素尺寸的功能

功能实现:

  • 富文本中媒体元素的宽度自适应当前设备的宽度
  • 如果媒体元素的宽度小于等于当前设备的宽度则不处理宽度
  • 如果媒体元素的宽度大于当前设备的宽度则进行等比例缩小
  • 如果媒体元素指定了高度,并且宽度大于当前设备的宽度,高度也会随着宽度的缩小比例进行相应的缩小,从而实现媒体元素等比例缩小

处理后效果如下图

image.png

代码

/**
 * 处理富文本中的媒体元素尺寸
 * @param {String} content 富文本内容
 * @param {Object} proxy 页面/组件实例
 * @param {String} symbol 富文本内容的父级元素选择器(例如:.class、#id)
 * @returns {String} 处理后的富文本内容
 */
export const handleRichText = async function (content, proxy, symbol) {
	// 获取content元素的宽度
	const query = uni.createSelectorQuery().in(proxy);
	const boxInfo = await new Promise((resolve) => {
		query
			.select(symbol)
			.boundingClientRect((data) => {
				resolve(data);
			})
			.exec();
	});
	let containerWidth = boxInfo.width;

	console.log('容器信息:', {
		选择器: symbol,
		容器宽度: containerWidth + 'px',
		容器完整信息: boxInfo,
	});

	// px 转 rpx 的比例
	const pxToRpxRatio = 2;

	// 处理图片
	content = content.replace(/<img[^>]*\/?\s*>/gi, (match) => {
		// 合并 style 和 data-original-style
		const styleMatch = match.match(/style="([^"]*)"/);
		const dataOriginalStyleMatch = match.match(/data-original-style="([^"]*)"/);
		let mergedStyle = '';
		if (styleMatch) mergedStyle += styleMatch[1] + ';';
		if (dataOriginalStyleMatch) mergedStyle += dataOriginalStyleMatch[1] + ';';

		// 提取宽度和高度(优先级:mergedStyle > width/height 属性 > data-width/data-height)
		let width, height;
		// 1. mergedStyle
		let widthStyleMatch = mergedStyle.match(/width:\s*([\d.]+)px/);
		let heightStyleMatch = mergedStyle.match(/height:\s*([\d.]+)px/);
		if (widthStyleMatch) width = parseFloat(widthStyleMatch[1]);
		if (heightStyleMatch) height = parseFloat(heightStyleMatch[1]);
		// 2. width/height 属性
		if (!width) {
			const attrWidthMatch = match.match(/\swidth="([\d.]+)"/);
			if (attrWidthMatch) width = parseFloat(attrWidthMatch[1]);
		}
		if (!height) {
			const attrHeightMatch = match.match(/\sheight="([\d.]+)"/);
			if (attrHeightMatch) height = parseFloat(attrHeightMatch[1]);
		}
		// 3. data-width/data-height
		if (!width) {
			const dataWidthMatch = match.match(/data-width="([\d.]+)px"/);
			if (dataWidthMatch) width = parseFloat(dataWidthMatch[1]);
		}
		if (!height) {
			const dataHeightMatch = match.match(/data-height="([\d.]+)px"/);
			if (dataHeightMatch) height = parseFloat(dataHeightMatch[1]);
		}

		// 提取图片地址
		const srcMatch = match.match(/src="([^"]*)"/);
		const imgSrc = srcMatch ? srcMatch[1] : '未知图片地址';

		console.log('处理图片(优化):', {
			图片地址: imgSrc,
			原始标签: match,
			合并style: mergedStyle,
			宽度: width,
			高度: height,
		});

		let newStyle = '';
		if (width) {
			let finalWidth = width;
			let finalHeight = height;
			let ratio = 1;
			if (width > containerWidth) {
				ratio = containerWidth / width;
				finalWidth = containerWidth;
				if (height) finalHeight = height * ratio;
			}
			const widthRpx = Math.floor(finalWidth * pxToRpxRatio);
			newStyle += `width: ${widthRpx}rpx; max-width: 100% !important;`;
			if (finalHeight) {
				const heightRpx = Math.floor(finalHeight * pxToRpxRatio);
				newStyle += ` height: ${heightRpx}rpx;`;
			}
		} else {
			// 没有宽度,强制最大宽度
			newStyle = 'max-width: 100% !important; height: auto;';
		}

		// 移除所有 width/height 属性、style 里的宽高
		let processedMatch = match
			.replace(/\swidth="[^"]*"/gi, '')
			.replace(/\sheight="[^"]*"/gi, '')
			.replace(/style="[^"]*"/gi, '')
			.replace(/data-original-style="[^"]*"/gi, '');

		// 添加新的 style
		if (/\s*\/?>$/.test(processedMatch)) {
			// 自闭合标签
			processedMatch = processedMatch.replace(/\s*\/?>$/, ` style="${newStyle}"/>`);
		} else {
			processedMatch = processedMatch.replace(/>$/, ` style="${newStyle}">`);
		}

		console.log('图片处理结果(优化):', {
			图片地址: imgSrc,
			处理后标签: processedMatch,
			处理后style: newStyle,
		});
		return processedMatch;
	});

	// 处理视频
	content = content.replace(/<video[^>]*\/?\s*>/gi, (match) => {
		// 合并 style 和 data-original-style
		const styleMatch = match.match(/style="([^"]*)"/);
		const dataOriginalStyleMatch = match.match(/data-original-style="([^"]*)"/);
		let mergedStyle = '';
		if (styleMatch) mergedStyle += styleMatch[1] + ';';
		if (dataOriginalStyleMatch) mergedStyle += dataOriginalStyleMatch[1] + ';';

		// 提取宽度和高度(优先级:mergedStyle > width/height 属性 > data-width/data-height)
		let width, height;
		// 1. mergedStyle
		let widthStyleMatch = mergedStyle.match(/width:\s*([\d.]+)px/);
		let heightStyleMatch = mergedStyle.match(/height:\s*([\d.]+)px/);
		if (widthStyleMatch) width = parseFloat(widthStyleMatch[1]);
		if (heightStyleMatch) height = parseFloat(heightStyleMatch[1]);
		// 2. width/height 属性
		if (!width) {
			const attrWidthMatch = match.match(/\swidth="([\d.]+)"/);
			if (attrWidthMatch) width = parseFloat(attrWidthMatch[1]);
		}
		if (!height) {
			const attrHeightMatch = match.match(/\sheight="([\d.]+)"/);
			if (attrHeightMatch) height = parseFloat(attrHeightMatch[1]);
		}
		// 3. data-width/data-height
		if (!width) {
			const dataWidthMatch = match.match(/data-width="([\d.]+)px"/);
			if (dataWidthMatch) width = parseFloat(dataWidthMatch[1]);
		}
		if (!height) {
			const dataHeightMatch = match.match(/data-height="([\d.]+)px"/);
			if (dataHeightMatch) height = parseFloat(dataHeightMatch[1]);
		}

		// 提取视频地址
		const srcMatch = match.match(/src="([^"]*)"/);
		const videoSrc = srcMatch ? srcMatch[1] : '未知视频地址';

		console.log('处理视频(优化):', {
			视频地址: videoSrc,
			原始标签: match,
			合并style: mergedStyle,
			宽度: width,
			高度: height,
		});

		let newStyle = '';
		if (width) {
			let finalWidth = width;
			let finalHeight = height;
			let ratio = 1;
			if (width > containerWidth) {
				ratio = containerWidth / width;
				finalWidth = containerWidth;
				if (height) finalHeight = height * ratio;
			}
			const widthRpx = Math.floor(finalWidth * pxToRpxRatio);
			newStyle += `width: ${widthRpx}rpx; max-width: 100% !important;`;
			if (finalHeight) {
				const heightRpx = Math.floor(finalHeight * pxToRpxRatio);
				newStyle += ` height: ${heightRpx}rpx;`;
			}
		} else {
			// 没有宽度,强制最大宽度
			newStyle = 'max-width: 100% !important; height: auto;';
		}

		// 移除所有 width/height 属性、style 里的宽高
		let processedMatch = match
			.replace(/\swidth="[^\"]*"/gi, '')
			.replace(/\sheight="[^\"]*"/gi, '')
			.replace(/style="[^"]*"/gi, '')
			.replace(/data-original-style="[^"]*"/gi, '');

		// 添加新的 style
		if (/\s*\/?>$/.test(processedMatch)) {
			// 自闭合标签
			processedMatch = processedMatch.replace(/\s*\/?>$/, ` style="${newStyle}"/>`);
		} else {
			processedMatch = processedMatch.replace(/>$/, ` style="${newStyle}">`);
		}

		console.log('视频处理结果(优化):', {
			视频地址: videoSrc,
			处理后标签: processedMatch,
			处理后style: newStyle,
		});
		return processedMatch;
	});

	return content;
};

使用

因为我是将其封装成一个工具函数,所以在页面导入函数使用即可

示例代码:

 <div class="content">
    			<rich-text :nodes="detail.content"></rich-text>
    		</div>

import { getCurrentInstance } from 'vue';
	const { proxy } = getCurrentInstance();
    import { handleRichText } from '@/utils/index';

    async function getDetail() {
    		try {
    			const res = await $getArticleDetail({ id: id.value });
    			res.content = await handleRichText(res.content, proxy, '.content');
    			detail.value = res;
    		} catch (error) {
    			console.log(error);
    		}
    	}
        

注意:

  • 在页面中需要给富文本渲染组件添加一个父元素,这样富文本渲染的媒体文件的宽度就不会超过父元素的宽度了
  • 需要传入富文本渲染组件父元素的元素选择器,这是为了获取父元素的宽度,从而限制富文本媒体元素的宽度/高度
  • 该方法是一个异步方法,所以需要使用await或then来等待函数处理完成