搜索全站
建站 15 分钟阅读

WordPress多端内容同步:PC、移动端与小程序的一处维护方案

一份内容要在 PC 站、移动站和小程序上重复发三遍?本文讲清以 WordPress 为唯一内容源的架构方案,给出 REST API 字段扩展、缓存与鉴权处理,以及小程序端请求的实现要点。

U
UFO_PF 2016-12-08 · 阅读 15 分钟
Wo 建站
阅读提示
本文约 5,797 字,预计阅读 15 分钟。文中命令与配置均经过实测,可直接参考执行。

引言:三个后台,三份内容,永远不一致

最常见的多端困局是这样的:公司有一个 WordPress 官网,后来又做了小程序,运营还要在微信公众号里发图文。于是同一篇新品介绍要在三个后台各发一遍,改一个价格要改三处。三个月后,PC 站上写”库存充足”,小程序上还是”预售中”,用户在小程序下单后发现发货时间和官网说的不一样,投诉就来了。

内容的”多处维护”是运营成本黑洞,也是数据不一致的根源。正确架构只有一个原则:WordPress 作为唯一的内容源(Single Source of Truth),其他端全部通过接口读取。WordPress 自 4.7 起内置的 REST API 就是为这件事准备的,它把文章、分类、媒体、用户都暴露成标准的 JSON 接口,任何能发 HTTP 请求的客户端都能消费。本文讲清这套架构怎么落地,以及实操中会遇到的字段不够、鉴权、缓存三个具体问题。

原理分析:REST API 是”读”,同步的本质是”拉取”而非”推送”

REST API 是 WordPress 核心自带的,无需插件即可通过 /wp-json/wp/v2/posts 拿到文章列表。它默认是只读的(写入需要鉴权),这对多端同步反而是好事:内容源只有一处,其他端只是不同形式的”视图”,天然不会产生冲突。

要理解的关键点是数据流向:小程序/App 主动拉取 WordPress 的数据,而不是 WordPress 推给它们。这意味着两件事:一是 WordPress 端不需要知道有哪些客户端,新增一个端不需要改服务端;二是各端要自己做缓存,不能每次打开都去打接口。

方案 适用场景 一致性 维护成本
各端各自维护 内容完全不重叠 差 高
REST API 拉取 官网 + 小程序 + App 好 低
小程序 webview 套网页 只需展示、无原生能力 最好 最低
数据库直连 高定制化后端 好 高、风险大

选择时先看需求:如果小程序只是”把官网内容搬过去”,用 web-view 嵌 H5 页面是最省事的(但微信对 web-view 有业务域名配置要求,且体验一般);如果需要原生列表、下拉刷新、分享卡片,就走 REST API。数据库直连是最不推荐的——跨端直连数据库会把安全边界彻底打散。

保姆级实操

  1. 确认接口可访问并测试基础查询

浏览器打开 https://你的域名/wp-json/wp/v2/posts?per_page=5,能返回 JSON 就说明接口可用。常用参数:per_page(每页条数,最大 100)、page、_fields(只返回指定字段,减少传输量)、search、categories、orderby=date。X-WP-TotalPages 响应头里有总页数。

  1. 扩展接口字段:把特色图、摘要、阅读量一起返回

默认接口不含特色图片 URL 和自定义字段,逐个再发请求去取会让小程序端请求数翻倍。用 register_rest_field() 一次性附加:

<?php
add_action( 'rest_api_init', function () {
    register_rest_field( 'post', 'cover', array(
        'get_callback' => function ( $post ) {
            $id = get_post_thumbnail_id( $post['id'] );
            if ( ! $id ) {
                return '';
            }
            // 返回中等尺寸,避免小程序拉原图浪费流量
            $src = wp_get_attachment_image_src( $id, 'medium_large' );
            return $src ? $src[0] : '';
        },
        'schema' => array( 'type' => 'string', 'context' => array( 'view' ) ),
    ) );

    register_rest_field( 'post', 'cat_name', array(
        'get_callback' => function ( $post ) {
            $terms = get_the_terms( $post['id'], 'category' );
            if ( empty( $terms ) || is_wp_error( $terms ) ) {
                return '';
            }
            return $terms[0]->name;
        },
        'schema' => array( 'type' => 'string', 'context' => array( 'view' ) ),
    ) );
} );

// 给小程序用的精简列表接口:只返回必要字段,减少解析成本
add_action( 'rest_api_init', function () {
    register_rest_route( 'app/v1', '/feed', array(
        'methods'             => 'GET',
        'permission_callback' => '__return_true',
        'args'                => array(
            'per_page' => array( 'default' => 10, 'sanitize_callback' => 'absint' ),
            'cat'      => array( 'default' => 0,  'sanitize_callback' => 'absint' ),
        ),
        'callback' => function ( WP_REST_Request $req ) {
            $q = new WP_Query( array(
                'post_type'      => 'post',
                'post_status'    => 'publish',
                'posts_per_page' => min( (int) $req['per_page'], 30 ),
                'cat'            => (int) $req['cat'],
                'no_found_rows'  => true,
            ) );

            $data = array();
            foreach ( $q->posts as $p ) {
                $data[] = array(
                    'id'      => $p->ID,
                    'title'   => html_entity_decode( get_the_title( $p ), ENT_QUOTES, 'UTF-8' ),
                    'excerpt' => wp_strip_all_tags( get_the_excerpt( $p ) ),
                    'cover'   => get_the_post_thumbnail_url( $p, 'medium_large' ) ?: '',
                    'date'    => get_the_date( 'Y-m-d', $p ),
                    'link'    => get_permalink( $p ),
                );
            }
            return rest_ensure_response( $data );
        },
    ) );
} );
  1. 在小程序端请求并缓存

小程序用 wx.request 拉取,域名必须是已备案且在小程序后台加入 request 合法域名的 HTTPS 地址。列表首次请求后写入本地缓存,下拉刷新时再拉一次:

// pages/index/index.js
const API = 'https://mimangfei.com/wp-json/app/v1/feed';
const CACHE_KEY = 'feed_cache';

Page({
  data: { list: [], loading: true },

  onLoad() {
    const cached = wx.getStorageSync(CACHE_KEY);
    if (cached && Date.now() - cached.t < 10 * 60 * 1000) {
      this.setData({ list: cached.data, loading: false }); // 10 分钟内用缓存
    }
    this.fetch();
  },

  fetch() {
    wx.request({
      url: API,
      data: { per_page: 10 },
      success: (res) => {
        const list = Array.isArray(res.data) ? res.data : [];
        this.setData({ list, loading: false });
        wx.setStorageSync(CACHE_KEY, { t: Date.now(), data: list });
      },
      fail: () => this.setData({ loading: false })
    });
  },

  onPullDownRefresh() {
    this.fetch();
    wx.stopPullDownRefresh();
  }
});
  1. 移动端:不要做独立的移动站

现在很少需要单独建 m.xxx.com。响应式主题 + 同一个 URL 是最省事也最利于收录的做法。如果已有独立移动站,正确做法是让它 301 到 PC 站的对应 URL,而不是两边各发一份内容——这一点和 网站改版时的 301 重定向总表制作 里的思路一致。

  1. 处理缓存与更新延迟

WordPress 端开了页面缓存后,接口也可能被缓存,导致小程序端看到旧内容。把 /wp-json/ 开头的请求排除出缓存规则(Nginx 里加一条 location,或在缓存插件里排除该路径)。同时给接口响应加 Cache-Control,并在文章更新时清理对应缓存。

  1. 加一层鉴权与限流

公开内容用只读接口没问题,但如果要通过接口写入(比如小程序提交表单回写到 WordPress),就必须用 Application Passwords(WordPress 后台「用户 → 个人资料」里生成)或 JWT 插件做鉴权,并且用 permission_callback 严格限制——绝不能把写入接口设成 __return_true。

常见踩坑

  • 小程序请求的域名未加入合法域名列表:开发工具里能请求(勾了”不校验合法域名”),真机一律失败。
  • 案例:某站点把 /wp-json/ 也纳入了整页缓存,小程序端长期显示三天前的内容,运营以为同步失效,反复手动刷新无效。排查后发现是缓存插件把接口响应也缓存了,加入排除规则后立刻恢复。
  • 接口一次返回 100 条带全文的数据:单次响应几 MB,小程序端解析卡顿。用 _fields 限制字段,或自建精简接口。
  • 写入接口的 permission_callback 写成 '__return_true':等于把后台写权限对全网开放,任何人都可发文章。
  • 特色图片 URL 是 http 开头:小程序要求 HTTPS(https 强制),图片全部加载失败。
  • 正文里的相对链接在小程序中打不开:接口返回 post_content 需把站内相对链接补成绝对地址,或用 web-view 打开。
  • 每打开一次页面就请求一次接口:没做本地缓存,接口被打爆,主机 CPU 飙升。

总结

多端同步的判断标准只有一条:改一处,三端同时生效;做不到就是架构错了。以 WordPress 为唯一内容源、其他端通过 REST API 拉取,是成本最低、一致性最好的方案。落地时三个必做项:用 register_rest_field() 或自建路由精简返回字段、把 /wp-json/ 排除出页面缓存、写入接口必须鉴权。内容在多端展现的形态差异可以参考 网站移动端适配的验收标准,而接口返回的内容质量本身仍取决于 网站内容填充的效率方法 打下的底子。

U
UFO_PF作者

专注 WordPress 生态的实战作者,内容以「可复制、可验证」为第一原则。

留下你的想法

邮箱地址不会被公开。带 * 的为必填项。