在 WordPress 块编辑器中使用 Markdown 和本地化

Avatar of Leonardo Losoviz
Leonardo Losoviz

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费积分!

如果我们需要直接在 WordPress 编辑器中向用户显示文档,最好的方法是什么?

由于块编辑器基于 React,我们可能会倾向于使用 React 组件和 HTML 代码来编写文档。这正是我在我之前的文章中采用的方法,该文章演示了一种 在模态窗口中显示文档 的方法。

但此解决方案并非完美无缺,因为通过 React 组件和 HTML 代码添加文档可能会变得非常冗长,更不用说难以维护了。例如,上面图像中的模态窗口包含一个 React 组件中的文档,如下所示

const CacheControlDescription = () => {
  return (
    <p>The Cache-Control header will contain the minimum max-age value from all fields/directives involved in the request, or <code>no-store</code> if the max-age is 0</p>
  )
}

使用 Markdown 代替 HTML 可以使工作更轻松。例如,上面的文档可以从 React 组件中移出,并放入一个 Markdown 文件中,例如 /docs/cache-control.md

The Cache-Control header will contain the minimum max-age value from all fields/directives involved in the request, or `no-store` if the max-age is 0

与纯 HTML 相比,使用 Markdown 的优缺点是什么?

优点缺点
✅ 编写 Markdown 比 HTML 更轻松快捷❌ 文档不能包含 React 组件
✅ 文档可以与块的源代码分开保存(甚至在单独的仓库中)❌ 我们无法使用 __ 函数(它通过 .po 文件帮助本地化内容)来输出文本
✅ 校对员可以修改文档,而不用担心破坏代码
✅ 文档代码不会添加到块的 JavaScript 资产中,从而可以更快地加载

关于缺点,至少对于简单的文档而言,无法使用 React 组件可能不是问题。但是,缺少本地化是一个主要问题。通过 JavaScript __ 函数添加到 React 组件中的文本可以 提取并使用来自 POT 文件的翻译进行替换。Markdown 中的内容无法访问此功能。

支持文档本地化是强制性的,因此我们需要弥补这一点。在本文中,我们将追求两个目标

  • 使用 Markdown 编写文档(由 WordPress 编辑器的块显示)
  • 将文档翻译成用户的语言

让我们开始吧!

加载 Markdown 内容

创建了一个 Markdown 文件 /docs/cache-control.md 后,我们可以导入其内容(已渲染为 HTML)并将其注入到 React 组件中,如下所示

import CacheControlDocumentation from '../docs/cache-control.md';


const CacheControlDescription = () => {
  return (
    <div
      dangerouslySetInnerHTML={ { __html: CacheControlDocumentation } }
    />
  );
}

此解决方案依赖于 webpack,它是 WordPress 编辑器核心中的模块打包器。

请注意,WordPress 编辑器目前使用 webpack 4.42。但是,webpack 网站上显示的前端文档对应于版本 5(它 仍处于测试阶段)。版本 4 的文档位于 子站点

内容通过 webpack 的 加载器 从 Markdown 转换为 HTML,为此,块需要 自定义其 webpack 配置,添加使用 markdown-loaderhtml-loader 的规则。

为此,在块的根目录下添加一个文件 webpack.config.js,其中包含以下代码

// This is the default webpack configuration from Gutenberg
const defaultConfig = require( '@wordpress/scripts/config/webpack.config' );


// Customize adding the required rules for the block
module.exports = {
  ...defaultConfig,
  module: {
    ...defaultConfig.module,
    rules: [
      ...defaultConfig.module.rules,
      {
        test: /\.md$/,
        use: [
          {
            loader: "html-loader"
          },
          {
            loader: "markdown-loader"
          }
        ]
      }
    ],
  },
};

并安装相应的包

npm install --save-dev markdown-loader html-loader

趁此机会,让我们应用一个小改进。docs 文件夹可以包含项目中任何位置的组件文档。为了避免必须从每个组件计算到该文件夹的相对路径,我们可以在 webpack.config.js 中添加一个 别名 @docs,以解析到文件夹 /docs

const path = require( 'path' );
config.resolve.alias[ '@docs' ] = path.resolve( process.cwd(), 'docs/' )

现在,导入操作得到了简化

import CacheControlDocumentation from '@docs/cache-control.md';

就是这样!现在我们可以将来自外部 Markdown 文件的文档注入到 React 组件中。

将文档翻译成用户的语言

我们无法通过 .po 文件为 Markdown 内容翻译字符串,但有一种替代方法:为不同的语言生成不同的 Markdown 文件。然后,我们可以为每种语言创建一个文件,而不是只有一个文件(/docs/cache-control.md),每个文件都存储在其对应的语言代码下

  • /docs/en/cache-control.md
  • /docs/fr/cache-control.md
  • /docs/zh/cache-control.md
  • 等等。

我们还可以支持语言和地区的翻译,以便美式英语和英式英语可以有不同的版本,并在未提供地区的翻译时默认为仅语言版本(例如,"en_CA""en" 处理)

  • /docs/en_US/cache-control.md
  • /docs/en_GB/cache-control.md
  • /docs/en/cache-control.md

为了简化问题,我将只解释如何支持不同的语言,而不支持地区。但是代码基本相同。

本文中演示的代码也可以在 我制作的 WordPress 插件的源代码 中看到。

将用户的语言提供给块

WordPress 中用户的语言可以通过 get_locale() 获取。由于语言环境包括语言代码和地区(例如 "en_US"),因此我们对其进行解析以单独提取语言代码

function get_locale_language(): string 
{
  $localeParts = explode( '_', get_locale() );
  return $localeParts[0];
}

通过 wp_localize_script(),我们将语言代码提供给块,作为全局变量(在本例中为 graphqlApiCacheControl)下的 userLang 属性

// The block was registered as $blockScriptRegistrationName
wp_localize_script(
  $blockScriptRegistrationName,
  'graphqlApiCacheControl',
  [
    'userLang' => get_locale_language(),
  ]
);

现在,用户的语言代码在块中可用

const lang = window.graphqlApiCacheControl.userLang; 

动态导入

我们只能在运行时知道用户的语言。但是,import 语句是静态的,而不是动态的。因此,我们无法执行以下操作

// `lang` contains the user's language
import CacheControlDocumentation from '@docs/${ lang }/cache-control.md';

也就是说,webpack 允许我们通过 import 函数动态加载模块,该函数默认将请求的模块拆分为一个单独的块(即它不包含在主编译的 build/index.js 文件中),以便延迟加载。

此行为适用于在模态窗口中显示文档,该窗口由用户操作触发,而不是预先加载。import 必须接收有关模块所在位置的一些信息,因此此代码有效

import( `@docs/${ lang }/cache-control.md` ).then( module => {
  // ...
});

但这段看似相似的代码无效

const dynamicModule = `@docs/${ lang }/cache-control.md`
import( dynamicModule ).then( module => {
  // ...
});

文件中的内容可在导入对象的 default 键下访问

const cacheControlContent = import( `@docs/${ lang }/cache-control.md` ).then( obj => obj.default )

我们可以将此逻辑概括为一个名为 getMarkdownContent 的函数,并传递 Markdown 文件的名称以及语言

const getMarkdownContent = ( fileName, lang ) => {
  return import( `@docs/${ lang }/${ fileName }.md` )
    .then( obj => obj.default )
} 

管理块

为了保持块资产的组织性,让我们将文档块分组到 /docs 子文件夹中(将在 build/ 文件夹内创建),并为其提供描述性文件名。

然后,在三种语言(英语、法语和中文)中包含两个文档(cache-control.mdcache-purging.md),将生成以下块

  • build/docs/en-cache-control-md.js
  • build/docs/fr-cache-control-md.js
  • build/docs/zh-cache-control-md.js
  • build/docs/en-cache-purging-md.js
  • build/docs/fr-cache-purging-md.js
  • build/docs/zh-cache-purging-md.js

这是通过在 import 参数之前使用 魔法注释 /* webpackChunkName: "docs/[request]" */ 来实现的

const getMarkdownContent = ( fileName, lang ) => {
  return import( /* webpackChunkName: "docs/[request]" */ `@docs/${ lang }/${ fileName }.md` )
    .then(obj => obj.default)
} 

设置块的公共路径

webpack 知道在哪里获取块,这要归功于 publicPath 配置选项。如果未提供,则使用 WordPress 编辑器中的当前 URL /wp-admin/,因为块位于其他位置,因此会产生 404 错误。对于我的块,它们位于 /wp-content/plugins/graphql-api/blocks/cache-control/build/ 下。

如果块用于我们自己的用途,我们可以在 webpack.config.js 中硬编码 publicPath,或者通过 ASSET_PATH 环境变量提供它。否则,我们需要在运行时将公共路径传递给块。为此,我们计算块的 build/ 文件夹的 URL

$blockPublicPath = plugin_dir_url( __FILE__ ) . '/blocks/cache-control/build/';

然后,我们通过本地化块将其注入到 JavaScript 端

// The block was registered as $blockScriptRegistrationName
wp_localize_script(
    $blockScriptRegistrationName,
    'graphqlApiCacheControl',
    [
      //...
      'publicPath' => $blockPublicPath,
    ]
);

然后,我们将公共路径提供给 __webpack_public_path__ JavaScript 变量

__webpack_public_path__ = window.graphqlApiCacheControl.publicPath;

回退到默认语言

如果用户语言没有翻译,会发生什么?在这种情况下,调用getMarkdownContent将会抛出一个错误。

例如,当语言设置为德语时,浏览器控制台将显示以下内容

Uncaught (in promise) Error: Cannot find module './de/cache-control.md'

解决方法是捕获错误,然后返回默认语言的内容,该内容始终由代码块满足

const getMarkdownContentOrUseDefault = ( fileName, defaultLang, lang ) => {
  return getMarkdownContent( fileName, lang )
    .catch( err => getMarkdownContent( fileName, defaultLang ) )
}

请注意,当翻译不完整时,作为 React 组件内部的 HTML 和作为外部 Markdown 文件的代码文档的行为有所不同。在第一种情况下,如果一个字符串已被翻译,但另一个字符串尚未翻译(在.po文件中),那么 React 组件最终将显示混合语言。在第二种情况下,要么文档完全翻译,要么不翻译。 

将文档设置到模态框中

现在,我们可以从 Markdown 文件中检索文档。让我们看看如何在模态框中显示它。

我们首先包装 Gutenberg 的Modal组件,以将内容注入为 HTML

import { Modal } from '@wordpress/components';


const ContentModal = ( props ) => {
  const { content } = props;
  return (
    <Modal 
      { ...props }
    >
      <div
        dangerouslySetInnerHTML={ { __html: content } }
      />
    </Modal>
  );
};

然后我们从 Markdown 文件中检索内容,并使用名为page状态钩子将其作为 prop 传递到模态框中。动态加载内容是一个异步操作,因此我们还必须使用效果钩子在组件中执行副作用。我们需要仅读取 Markdown 文件中的内容一次,因此我们将空数组作为第二个参数传递给useEffect(否则钩子将持续触发)

import { useState, useEffect } from '@wordpress/element';

const CacheControlContentModal = ( props ) => {
  const fileName = 'cache-control'
  const lang = window.graphqlApiCacheControl.userLang
  const defaultLang = 'en'


  const [ page, setPage ] = useState( [] );


  useEffect(() => {
    getMarkdownContentOrUseDefault( fileName, defaultLang, lang ).then( value => {
      setPage( value )
    });
  }, [] );


  return (
    <ContentModal
      { ...props }
      content={ page }
    />
  );
};

让我们看看它是如何工作的。请注意,包含文档的块是如何延迟加载的(即,当编辑块时触发)

搞定了 🎉

编写文档可能不是你最喜欢的事情,但使编写和维护文档变得容易可以帮助你减轻痛苦。

使用 Markdown 而不是纯 HTML 当然是一种方法。我希望我们刚刚介绍的方法不仅可以改善你的工作流程,还可以为你的 WordPress 用户提供一个不错的增强功能。