三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

小程序扫码解析网页:Jsoup服务端架构与HTML内容提取实践

小程序扫码解析网页:Jsoup服务端架构与HTML内容提取实践

1. 项目概述:从扫码到解析的完整链路

最近在做一个需要从线下物料导流到线上内容的小程序项目,核心需求很简单:用户用小程序扫一下宣传单或产品包装上的二维码,小程序就能自动识别出二维码里藏着的网址,然后把这个网页的内容“扒”下来,在小程序里干净、友好地展示出来。听起来像是“小程序版浏览器”,但实际做起来,你会发现这远不止调用一个wx.scanCode接口那么简单。整个链路涉及到小程序端、服务端以及一个关键的桥梁——Jsoup这个HTML解析库。

为什么不用小程序直接打开这个网址?很多时候,外部网页的样式在小程序WebView里表现不佳,或者我们只想提取其中的关键信息(如文章正文、商品价格、活动时间),进行二次排版和展示,以提供更统一、更沉浸的用户体验。这时,Jsoup就派上了用场。它就像一个精准的“外科手术刀”,能让我们从复杂的HTML结构中,轻松地提取出我们想要的“器官”(DOM元素)。

这个项目的技术栈非常清晰:前端是微信小程序,负责扫码和界面展示;后端需要有一个服务(可以用Java Spring Boot、Node.js、Python Flask等实现),接收小程序传来的网址,然后用Jsoup去抓取并解析目标网页,最后将结构化的数据(比如标题、正文、图片链接)返回给小程序渲染。难点不在于单个技术点,而在于如何将这条链路打通、做稳,并处理好网络、解析、安全、性能这一系列连锁问题。

2. 核心思路与架构设计

2.1 为什么选择“小程序+服务端+Jsoup”的方案?

最初构思时,你可能想过几个方案。比如,能否在小程序端用JavaScript直接解析HTML?理论上可以,但小程序沙箱环境对DOM操作支持很弱,且无法处理跨域请求。又比如,用云开发?云函数的网络环境可能受限,且对于复杂的HTML解析,Jsoup在Java生态中的成熟度是首选。

因此,“小程序扫码 -> 服务端中转抓取 -> Jsoup解析 -> 返回数据”成了最务实的选择。这个架构的职责分离非常清晰:

  1. 小程序端:纯交互层。调用摄像头扫码,将获得的URL发送给服务端,并优雅地展示解析后的数据。
  2. 服务端:核心数据处理层。承担网络抓取、HTML解析、数据清洗和接口提供的重任。这里是我们主要施展拳脚的地方。
  3. Jsoup:服务端的“利刃”。它不仅能以类似jQuery的语法方便地选择元素,还能处理不规范的HTML,防XSS攻击,功能强大。

这个方案的优势在于可控性强。服务端可以加入缓存(避免对同一URL频繁抓取)、设置超时与重试、处理各种编码问题、过滤广告和无关脚本,这些都是纯前端方案难以做到的。

2.2 技术选型背后的考量

  • 服务端语言选择:我选择了Java + Spring Boot。原因很简单,Jsoup是Java库,原生集成最顺畅。而且Spring Boot能快速搭建RESTful API,生态完善,后期加缓存、监控、日志都很方便。当然,如果你团队更熟悉Node.js,可以用cheerio;熟悉Python,可以用BeautifulSoup。原理相通。
  • 通信协议:小程序与服务端采用HTTPS通信。这是微信小程序的强制要求,也保证了数据传输的安全。
  • 数据格式:接口交互使用JSON。结构清晰,小程序端解析方便。
  • 关键依赖:在pom.xml中,核心就是引入Jsoup。
    <dependency> <groupId>org.jsoup</groupId> <artifactId>jsoup</artifactId> <version>1.17.2</version> <!-- 使用当前最新稳定版 --> </dependency>

3. 小程序端实现要点

3.1 扫码功能的最佳实践

小程序端的扫码功能主要依赖wx.scanCodeAPI。代码看似简单,但细节决定体验。

// pages/scan/scan.js Page({ startScan() { const that = this; // 建议在用户触发动作(如点击按钮)后再调用,避免一进入页面就请求摄像头权限引起用户不适。 wx.scanCode({ onlyFromCamera: true, // 只允许从相机扫码,不显示相册选图,流程更专注 scanType: ['qrCode'], // 明确指定只识别二维码,提高识别速度和准确率 success(res) { console.log('扫码结果:', res.result); const scannedUrl = res.result; // 基础校验:检查是否是合法的URL if (scannedUrl && (scannedUrl.startsWith('http://') || scannedUrl.startsWith('https://'))) { that.fetchAndParseUrl(scannedUrl); } else { wx.showToast({ title: '未识别到有效网址', icon: 'none' }); } }, fail(err) { console.error('扫码失败:', err); // 失败原因细分处理 if (err.errMsg.includes('permission')) { wx.showModal({ title: '权限提示', content: '需要摄像头权限才能扫码,请在设置中开启', showCancel: false }); } else { wx.showToast({ title: '扫码失败,请重试', icon: 'none' }); } } }); }, fetchAndParseUrl(url) { wx.showLoading({ title: '解析中...' }); wx.request({ url: 'https://your-api-domain.com/api/parse', // 你的服务端接口地址 method: 'POST', data: { url: url }, header: { 'content-type': 'application/json' }, success(res) { wx.hideLoading(); if (res.statusCode === 200 && res.data.success) { // 解析成功,跳转到展示页或在本页渲染 const parsedData = res.data.data; wx.navigateTo({ url: `/pages/display/display?data=${encodeURIComponent(JSON.stringify(parsedData))}` }); } else { wx.showToast({ title: res.data.message || '解析失败', icon: 'none' }); } }, fail(err) { wx.hideLoading(); wx.showToast({ title: '网络请求失败', icon: 'none' }); } }); } })

注意事项与实操心得

  1. 权限引导wx.scanCode会触发摄像头权限申请。如果用户拒绝,再次调用会直接失败。更好的做法是,在首次失败时,用wx.openSetting引导用户去设置页开启权限,但要注意审核规范,不能默认强制跳转。
  2. 扫码性能:设置scanType: ['qrCode']能提升识别效率。对于复杂背景或受损的二维码,可以提示用户调整手机距离和角度,并保持环境光线充足。
  3. URL基础校验:在发送到服务端前做一次基础格式校验,能拦截明显无效的输入,减轻服务端压力。
  4. 加载状态管理:网络请求前后一定要配合wx.showLoadingwx.hideLoading,给用户明确的反馈。超时时间可以在wx.requesttimeout参数中设置。

3.2 解析数据的渲染策略

服务端返回的数据可能是富文本(HTML片段)或纯结构化数据。对于富文本,小程序可以用rich-text组件渲染,但要注意其有限的CSS支持。更常见的做法是,服务端解析后返回结构化的JSON,比如:

{ "title": "文章标题", "content": [ {"type": "text", "value": "这是一个段落。"}, {"type": "image", "value": "https://example.com/img1.jpg"}, {"type": "text", "value": "这是另一个段落。"} ], "images": ["https://example.com/img1.jpg"] }

小程序端则根据type动态渲染为<view><image>。这样样式完全可控,体验最佳。

4. 服务端核心:使用Jsoup进行解析

这是项目的核心。服务端接口接收到URL后,需要完成抓取、解析、清洗和返回。

4.1 基础解析流程与代码实现

首先,创建一个Spring Boot的Controller:

@RestController @RequestMapping("/api") public class ParserController { @PostMapping("/parse") public ApiResponse parseUrl(@RequestBody UrlRequest urlRequest) { String targetUrl = urlRequest.getUrl(); // 1. 基础验证 if (!isValidUrl(targetUrl)) { return ApiResponse.error("无效的URL"); } try { // 2. 连接并获取文档,设置超时和User-Agent模仿浏览器 Document doc = Jsoup.connect(targetUrl) .timeout(10000) // 10秒超时 .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") // 模拟浏览器,避免被屏蔽 .get(); // 3. 提取所需数据 String title = doc.title(); // 假设我们想提取正文,通常正文在 <article> 或 <div class=\"content\"> 等标签内 // 这里需要根据目标网站结构调整选择器,这是核心难点 Elements contentElements = doc.select("article, .content, .post-body, #main-content"); String contentHtml = contentElements.first() != null ? contentElements.first().html() : doc.body().html(); // 4. 进一步清洗内容(可选但重要) // 移除不需要的标签(如脚本、样式、广告) contentHtml = Jsoup.parse(contentHtml).select("script, style, iframe, .advertisement").remove().html(); // 相对路径转绝对路径 contentHtml = convertRelativeUrls(contentHtml, targetUrl); // 5. 构建返回数据 ParsedResult result = new ParsedResult(); result.setTitle(title); result.setContent(contentHtml); // 可以额外提取图片、描述等 result.setImages(extractImages(doc)); return ApiResponse.success(result); } catch (SocketTimeoutException e) { return ApiResponse.error("请求超时,网站响应过慢"); } catch (IOException e) { return ApiResponse.error("无法获取网页内容: " + e.getMessage()); } catch (Exception e) { return ApiResponse.error("解析过程发生错误"); } } private boolean isValidUrl(String url) { // 简单的URL格式验证 try { new URL(url); return true; } catch (MalformedURLException e) { return false; } } private String convertRelativeUrls(String html, String baseUri) { // 利用Jsoup的baseUri功能转换相对链接为绝对链接 Document dirtyDoc = Jsoup.parse(html, baseUri); dirtyDoc.outputSettings().prettyPrint(false); // 处理图片、链接等资源的src和href属性 for (Element img : dirtyDoc.select("img[src]")) { String src = img.attr("abs:src"); // 关键:使用abs:前缀获取绝对路径 img.attr("src", src); } for (Element a : dirtyDoc.select("a[href]")) { String href = a.attr("abs:href"); a.attr("href", href); } return dirtyDoc.body().html(); } private List<String> extractImages(Document doc) { // 提取文章中所有图片的绝对地址 return doc.select("article img, .content img").eachAttr("abs:src"); } }

代码解析与关键点

  1. 超时与UA设置timeout至关重要,防止抓取僵死网站时线程被长期占用。userAgent模拟真实浏览器,是绕过简单反爬策略的第一步。
  2. 选择器(Selector)doc.select(“article, .content”)是Jsoup的核心。它的语法和CSS选择器一模一样。如何找到正文所在标签?这没有通用解。你需要分析目标网站的HTML结构。常用的候选标签有<article><main>.post-content#content等。有时需要组合使用,如div.content > p
  3. 数据清洗:直接获取的HTML通常包含大量垃圾信息。用remove()方法可以剔除脚本、样式、广告iframe等。这一步能显著提升返回内容的质量和安全性。
  4. 相对路径转绝对路径:网页内的图片、链接常用相对路径(如./images/logo.png)。如果不处理,这些资源在小程序里会加载失败。attr(“abs:src”)是Jsoup提供的便捷方法,能自动根据baseUri(即原网页地址)转换为绝对路径。

4.2 应对复杂网站与反爬策略

不是所有网站都“友好”。你可能遇到:

  • 动态加载内容:很多现代网站用JavaScript渲染内容,Jsoup抓取到的初始HTML是空的。这时需要考虑使用无头浏览器工具,如Selenium或Puppeteer,但这会极大增加复杂度和资源消耗。对于小程序场景,建议优先与业务方约定,扫描的二维码指向的页面最好是服务端可渲染的(SSR)。
  • 反爬虫机制:如IP限制、验证码、请求头校验。
    • 策略一:完善请求头。除了User-Agent,还可以添加Referer(设为目标网站域名)、Accept-Language等。
    Document doc = Jsoup.connect(url) .header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") .header("Accept-Language", "zh-CN,zh;q=0.9") .header("Connection", "keep-alive") .get();
    • 策略二:使用代理IP池。对于大规模抓取有必要,但本项目通常为低频个人使用,可暂不考虑。
    • 策略三:遵守robots.txt。在抓取前,可以检查目标网站的robots.txt文件,尊重网站的爬虫协议,避免法律风险。

5. 高级技巧与数据清洗

5.1 智能正文提取

依赖固定的CSS选择器(如.content)非常脆弱,网站一改版就失效。我们可以实现一个简单的启发式算法来“猜”正文。

public Element guessMainContent(Document doc) { // 策略1:优先找<article>标签 Element article = doc.select("article").first(); if (article != null && article.text().length() > 100) { // 正文通常有一定长度 return article; } // 策略2:寻找文本密度最高的<div> Elements divs = doc.select("div"); Element bestDiv = null; int maxTextLength = 0; for (Element div : divs) { // 排除导航、页脚等常见非内容区域 if (div.id().matches(".*(nav|footer|sidebar|comment).*") || div.className().matches(".*(nav|footer|sidebar|comment).*")) { continue; } int textLen = div.text().length(); int childCount = div.children().size(); // 简单计算文本密度:文本长度 / (子元素数+1),避免只有一两个链接的div得分高 if (childCount > 0) { float density = (float) textLen / childCount; if (textLen > maxTextLength && density > 20) { // 密度阈值可调 maxTextLength = textLen; bestDiv = div; } } } return bestDiv != null ? bestDiv : doc.body(); // 保底返回body }

这个算法很基础,但比固定选择器健壮。更专业的方案可以集成第三方库,如boilerpipe,但会引入新的依赖。

5.2 内容安全过滤

绝对不能将未经处理的HTML直接返回给小程序rich-text组件,这有严重的XSS安全风险。即使经过Jsoup解析(Jsoup默认会清理一些危险标签),我们仍需进行白名单过滤。

import org.jsoup.safety.Safelist; public String safeClean(String html) { // 使用Jsoup提供的Safelist定义允许的标签和属性 Safelist safelist = Safelist.relaxed() // 基础宽松列表,包含a, b, blockquote, br, div... .addTags("section", "article", "header", "footer") // 添加HTML5语义标签 .addAttributes("a", "href", "title") // 允许a标签的href和title属性 .addAttributes("img", "src", "alt", "width", "height") // 允许img标签的属性 .addProtocols("a", "href", "http", "https") // 链接只允许http/https .addProtocols("img", "src", "http", "https", "data"); // 图片允许http/https和data URI // 清理HTML,不符合白名单的将被移除 String cleanHtml = Jsoup.clean(html, safelist); return cleanHtml; }

在解析流程中,在convertRelativeUrls之后,调用safeClean方法,确保输出的HTML是安全的。

6. 性能优化与缓存策略

频繁抓取同一网址是对目标网站和服务端资源的浪费。引入缓存至关重要。

6.1 基于内存或Redis的缓存

@Service public class PageCacheService { @Autowired private RedisTemplate<String, ParsedResult> redisTemplate; // 使用Redis // 或者使用简单的ConcurrentHashMap做内存缓存(适用于单机、数据量小) private static final String CACHE_PREFIX = "page:"; private static final long CACHE_EXPIRE_HOURS = 24; // 缓存24小时 public ParsedResult getOrParse(String url) { String cacheKey = CACHE_PREFIX + url.hashCode(); // 简单生成key // 1. 查缓存 ParsedResult cached = redisTemplate.opsForValue().get(cacheKey); if (cached != null) { return cached; } // 2. 缓存未命中,执行解析 ParsedResult freshResult = doParse(url); // 调用之前的解析逻辑 // 3. 存入缓存 if (freshResult != null) { redisTemplate.opsForValue().set(cacheKey, freshResult, CACHE_EXPIRE_HOURS, TimeUnit.HOURS); } return freshResult; } private ParsedResult doParse(String url) { // 原有的Jsoup解析逻辑... return result; } }

在Controller中,不再直接调用解析,而是调用cacheService.getOrParse(url)

缓存策略思考

  • 缓存键:直接用URL字符串可能太长,可以用其MD5或哈希值。但要小心,带不同查询参数的同一页面可能内容不同(如?page=1?page=2),是否需要区分取决于业务。
  • 过期时间:对于新闻类网站,缓存时间可以短一些(如1小时);对于不常变的公告页面,可以长一些(如1天)。甚至可以设置不同的缓存策略。
  • 缓存失效:如果发现某个页面解析结果错误(如网站改版),需要有手动或自动清理该URL缓存的能力。

6.2 异步处理与队列

如果解析非常耗时(比如需要调用无头浏览器),为了不阻塞HTTP请求,可以考虑异步模式。小程序端发起请求后,服务端立即返回一个taskId,然后通过WebSocket或轮询让小程序去获取结果。解析任务本身放入消息队列(如RabbitMQ、Kafka)中,由后台Worker处理。

@PostMapping("/async-parse") public ApiResponse asyncParse(@RequestBody UrlRequest request) { String taskId = UUID.randomUUID().toString(); // 1. 将任务(url, taskId)放入消息队列 messageQueue.send(new ParseTask(taskId, request.getUrl())); // 2. 立即返回taskId return ApiResponse.success(Collections.singletonMap("taskId", taskId)); } @GetMapping("/result/{taskId}") public ApiResponse getResult(@PathVariable String taskId) { // 3. 根据taskId从缓存(如Redis)中查询处理结果 ParsedResult result = resultCache.get(taskId); if (result == null) { return ApiResponse.of(202, "任务处理中", null); // 202 Accepted } return ApiResponse.success(result); }

这对于提升接口响应速度和用户体验很有帮助,但架构复杂度也上来了,需要根据实际业务量和性能要求权衡。

7. 异常处理与监控

一个健壮的服务必须能妥善处理各种异常。

7.1 定义清晰的异常类型和返回码

不要将所有错误都笼统地返回“解析失败”。定义一套错误码,帮助前端和小程序端定位问题。

public enum ErrorCode { SUCCESS(0, "成功"), INVALID_URL(1001, "URL格式无效"), NETWORK_TIMEOUT(1002, "网络请求超时"), HTTP_ERROR(1003, "目标网站访问错误"), PARSE_FAILED(1004, "内容解析失败"), CONTENT_EMPTY(1005, "未找到有效内容"), // ... 其他错误码 }

在ApiResponse中包含codemessagedata字段。

7.2 详细的日志记录

记录每一次解析请求的URL、耗时、结果状态、异常信息。这不仅是排查问题的依据,也能帮你分析哪些网站经常出问题,从而优化解析策略。

import lombok.extern.slf4j.Slf4j; @Slf4j @RestController public class ParserController { public ApiResponse parseUrl(...) { long startTime = System.currentTimeMillis(); try { // ... 解析逻辑 long cost = System.currentTimeMillis() - startTime; log.info("解析成功. url={}, cost={}ms, title={}", targetUrl, cost, title); return ApiResponse.success(result); } catch (SocketTimeoutException e) { log.warn("解析超时. url={}, error={}", targetUrl, e.getMessage()); return ApiResponse.error(ErrorCode.NETWORK_TIMEOUT); } catch (IOException e) { log.error("网络IO异常. url={}, error={}", targetUrl, e.getMessage()); return ApiResponse.error(ErrorCode.HTTP_ERROR); } catch (Exception e) { log.error("解析过程未知异常. url={}", targetUrl, e); // 记录完整堆栈 return ApiResponse.error(ErrorCode.PARSE_FAILED); } } }

7.3 设置合理的超时与重试

网络请求充满不确定性。除了在Jsoup连接时设置超时,还可以在Spring Boot的应用配置中,为整个HTTP客户端或RestTemplate设置连接超时和读取超时。

对于非幂等的POST请求(本例中是幂等的,因为只是获取数据),重试要谨慎。但对于临时性网络故障,可以加入简单的重试逻辑。

public Document fetchWithRetry(String url, int maxRetries) throws IOException { IOException lastException = null; for (int i = 0; i < maxRetries; i++) { try { return Jsoup.connect(url).timeout(10000).get(); } catch (SocketTimeoutException | ConnectException e) { lastException = e; log.warn("第{}次抓取失败,准备重试。url: {}", i+1, url); try { Thread.sleep(1000 * (i + 1)); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); } // 递增延迟 } } throw lastException; }

8. 部署与运维考量

8.1 服务端部署

将Spring Boot应用打包成JAR,部署到云服务器(如腾讯云CVM、阿里云ECS)或容器服务(如Docker + Kubernetes)。关键配置:

  • 服务器资源:解析服务是CPU和I/O密集型(网络请求)。建议选择至少2核4G的配置,并根据QPS监控进行扩容。
  • JVM参数:设置合适的堆内存(-Xmx-Xms),避免频繁GC影响解析性能。
  • 反向代理:使用Nginx作为反向代理,处理SSL卸载、负载均衡和静态资源服务。

8.2 小程序配置

确保小程序请求的服务器域名已在微信公众平台配置。在“开发管理” -> “开发设置” -> “服务器域名”中,将你的服务端API域名(如api.yourdomain.com)添加到request合法域名列表中。否则,小程序无法发起网络请求。

8.3 监控与告警

上线后,监控必不可少:

  1. 应用健康监控:使用Spring Boot Actuator暴露健康端点,配合Prometheus和Grafana监控应用状态、JVM内存、线程池等。
  2. 业务指标监控:记录解析成功率、平均耗时、热门URL等。成功率突然下降可能意味着某个常用网站改版了。
  3. 日志聚合:使用ELK(Elasticsearch, Logstash, Kibana)或类似工具集中收集和查询日志,方便排查问题。
  4. 告警:对解析失败率、服务响应时间设置阈值告警(可通过钉钉、企业微信机器人通知)。

9. 常见问题排查实录

在实际开发和线上运行中,我踩过不少坑,这里总结几个最典型的:

问题一:扫码后,小程序提示“解析失败”,服务端日志显示SSLHandshakeExceptionCertificateException

  • 原因:目标网站使用了过时的、不安全的SSL协议(如TLS 1.0)或自签名证书,而Java运行环境默认的安全策略较严格。
  • 解决:这不是一个建议的通用方案,仅在确认目标网站安全的情况下临时使用。可以在发起Jsoup连接前,设置忽略SSL证书验证(生产环境慎用)。
    import javax.net.ssl.*; // ... 在连接前执行 private static void ignoreSSL() throws Exception { TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() { public java.security.cert.X509Certificate[] getAcceptedIssuers() { return null; } public void checkClientTrusted(X509Certificate[] certs, String authType) { } public void checkServerTrusted(X509Certificate[] certs, String authType) { } }}; SSLContext sc = SSLContext.getInstance("SSL"); sc.init(null, trustAllCerts, new java.security.SecureRandom()); HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); HttpsURLConnection.setDefaultHostnameVerifier((hostname, session) -> true); }
    更安全的方式是,将目标网站的有效证书导入到服务器的Java信任库(keystore)中。

问题二:解析某些网站返回乱码。

  • 原因:网页的字符编码(如GBK)与Jsoup默认解析的编码(UTF-8)不一致。
  • 解决:在Jsoup连接时指定正确的字符集。
    Document doc = Jsoup.connect(url) .timeout(10000) .header("Accept-Charset", "utf-8") .execute() // 先执行请求 .charset("GBK") // 根据响应头或HTML meta标签判断,手动指定 .parse();
    也可以先获取响应体,然后根据HTML中的<meta charset="...">标签或HTTP响应头来动态判断编码。

问题三:小程序端显示“无法预览”,服务端日志看到403 Forbidden

  • 原因:目标网站有反爬虫机制,识别出我们的请求是来自Jsoup(非浏览器)并拒绝了。
  • 解决
    1. 完善请求头,尽可能模拟浏览器(参考4.2节)。
    2. 添加Referer头,有时需要设置为目标网站的同一域名下的页面。
    3. 如果网站依赖Cookie,可能需要先访问一次首页获取Cookie,再带着Cookie去访问目标页。这涉及到会话(Session)管理,复杂度较高。
    4. 如果上述都无效,可能需要评估是否触及了网站的使用条款。对于重要的业务源,考虑联系对方获取官方API或授权。

问题四:解析速度很慢,尤其是图片多的页面。

  • 原因:Jsoup在解析时会尝试加载并处理所有链接的资源吗?不会,它只解析HTML文本。慢的原因可能是网络延迟,或者是选择器过于复杂,遍历了巨大的DOM树。
  • 解决
    1. 优化选择器:尽量使用ID选择器(#id),它最快。避免使用通配符*或深层嵌套的选择器。
    2. 限制解析范围:如果只需要正文,就不要用doc.body().html(),而是用更精确的选择器获取特定元素。
    3. 异步处理图片:服务端可以只提取图片链接,返回给小程序,由小程序端异步加载图片,不阻塞主要内容展示。甚至可以先返回没有图片的文本内容,再通过另一个请求获取图片列表。

问题五:返回的HTML在小程序rich-text里样式错乱。

  • 原因:小程序rich-text组件支持的CSS样式有限,且不同标签的默认样式与浏览器不同。
  • 解决
    1. 服务端清洗样式:在safeClean时,可以使用Safelist.none().addTags(...)只保留标签,彻底移除所有style属性和<style>标签。然后由小程序端提供统一的CSS类进行渲染。
    2. 小程序端样式覆盖:在小程序的WXSS文件中,对rich-text内部的标签进行全局样式重置。
      /* pages/display/display.wxss */ rich-text { font-size: 16px; line-height: 1.6; color: #333; } rich-text img { max-width: 100%; height: auto; display: block; margin: 10px auto; } rich-text p { margin: 10px 0; }
    最根本的解决方案,还是如3.2节所述,服务端返回结构化数据,小程序端原生渲染,样式完全可控。

这个项目从简单的想法到稳定可用的服务,涉及了前端交互、网络通信、服务器端编程、HTML解析、性能优化和运维监控等多个环节。最大的体会是,** robustness(健壮性)** 比functionality(功能性)更难实现。处理好各种边界情况和异常,设计好降级和缓存策略,才能让这个小小的扫码解析功能,在真实的生产环境中可靠地运行下去。

← 返回列表