开篇

一年多前折腾Sakurairo主题的时候,写了一篇关于添加自定义表情文章:Sakurairo主题评论区增加表情包,将原神的表情包添加进评论表情里。又从kanokano那里拿了一些kano的表情包,一起加了进去,她真的好可爱!

当初折腾的时候也意识到了一个问题,维护更新很麻烦。每更新一次主题,就需要手动修改一次主题源代码,添加自定义表情的过程也异常繁琐,需要先把所有表情包的文件名构建成一个数组,再进行调用输出到评论区里。虽然当时写了一个小PHP脚本用来处理表情包文件,一来二去,还是感觉麻烦诸多。最后这个表情也没有继续再维护了。

当时我就想着,能不能给Sakurairo主题添加一个自定义表情的功能,让用户能够使用自定义的表情。奈何技术力有限,最终不了了之。

前几天有个群友提起这个事情,问我主题的自定义表情怎么做,之前参考我的那篇文章做好了,后面更新主题后就炸了,现在想整回来。我听着还挺开心,原来我写的文章还是有人看的啊,这大概就是价值被认可的快乐?

随后将以前做的那篇文章发给了TA,不知现在整得咋样了。

我重温了一遍这篇文章,评论区不乏赞扬之词,死去的记忆突然向我潮涌而来,又想起了折腾主题那段快乐的时光。

然后,我决定要给它画一个句号。

前置条件

我想起了一年多前的想法,想着要不试试就在今天把它实现吧!看了一遍源代码,发现主题的表情系统不算复杂,照猫画虎应该能够搞定。随即思考实现方案,有两点尤为重要:

  • 需要有简单的使用方法
  • 不能影响页面的加载性能

需要有简单的使用方法:如果一个功能使用起来很麻烦,用户肯定不愿去使用。比如需要输入一个包含表情包的数组,又或者手动输入文件名才能正常使用,这是相当反人类的。需要用户做的,只有一个输入文件夹名称的操作就够了。

不能影响页面的加载性能:如果启用了自定义表情功能,页面渲染时间增加了1秒钟,这绝对是一个灾难!性能优化是一门高深的学问,我不认为自己能做好。我希望能尽量让用户无感知渲染时间的增加。众所周知的是,不管对于哪个系统来说,遍历一个文件夹以及子文件夹下的所有文件,这个过程是非常耗时间的,尤其是在文件数量众多的情况下。所以表情文件应该按需遍历,好在WordPress的Transients API可以用来解决这一个问题。可以用Transients将遍历文件夹后的数据存储起来,这是一种临时的存储手段,类似于一个缓存。

开始创建

思路大致理清后,可以上手干活了。

设置选项说明

Sakurairo后台设置选项使用的是Codestar Framework框架,在文档的帮助下,还是比较容易上手的。一共4个设置选项,其中两个是可选项。

评论区表情

这个选项用于控制显示在评论区域面板的表情类别,该项目可多选。如果用户对内置的三种表情不感冒,或者想扩充表情类别,这时自定义表情功能就派上用场了。

如果4种表情都不勾选,则会关闭评论区面板上的表情输入功能,历史评论中的表情仍然能够正常显示,只是新的评论不能再添加表情。

自定义表情栏目名称

顾名思义,就是表情的类别名称,会显示在评论区域表情类别选择栏目上。

上图是以原神作为表情栏目名称

上图是以kano作为表情栏目名称

从最佳实践上看,在启用所有表情类别的情况下,4个汉字的栏目名称,可能会导致移动端横向空间不足。

如果你发现这一问题,这时需要做的是减少一个类别的启用或者缩短表情类别名称,以保证横向空间。桌面端宽度足够,未发现类似问题。

image-20230915030705571

自定义表情的路径

需要明确的是,受制于权限要求,主题能够访问到的文件夹是有限的,这里填入的必须是WordPress wp-content文件夹里uploads文件夹下的目录,填写相对路径。

假如表情文件夹的目录是:/web/site/kanochan.net/wp-content/uploads/smilies

那么需要填写的自定义表情路径是:/smilies

举一个例子:

假如你有一张表情图片可以通过这个URL访问:https://kanochan.net/wp-content/uploads/smilies/ys_bixin.png

那么你需要填写的自定义表情路径是:/smilies

再举一个例子:

假如你的文件夹结构如下所示,表情文件放置在smilies文件夹里

uploads
└─ sakurairo_vision
   └─ @2.4
      └─ smilies
         ├─ kanopng
         │  ├─ kano_awsl.png
         │  ├─ kano_biezaiyi.png
         │  └─ kano_bixin.png
         ├─ yspng
         │  ├─ ys_aaaaaa.png
         │  ├─ ys_anxiang.png
         │  └─ ys_anzhongguancha.png
         ├─ ys_yiwen.png
         ├─ ys_zhenjing.png
         └─ ys_zuomeng.png

那么需要填写的自定义表情路径是:/sakurairo_vision/@2.4/smilies

其它一些注意事项:

  • 表情文件夹里可以建立子文件夹,方便对表情进行归类存放
  • 表情文件夹(包括子文件夹)下所有文件的文件名,都不能存在重名(即使是扩展名不同)
  • 支持的文件格式有:jpgjpegpnggifsvgavifwebp,其他的文件格式,都不会被收录进表情列表中
  • 建议表情图片的长宽像素比例为1:1、像素80px * 80px以上,可以带来更好的展示效果

自定义表情代理地址

即常说的CDN地址,本地服务器的目录结构与CDN服务器的目录结构需要一致,否则会404。以下是一个示例:

假如你本地服务器上的表情文件URL是这样的:https://kanochan.net/wp-content/uploads/smilies/ys_bixin.png

你将smilies表情文件夹放到了CDN服务器,获取到的文件路径是这样的:https://cdn.kanochan.net/smilies/ys_bixin.png

那么你需要填入的自定义表情代理地址是:https://contents.kanochan.net

更新自定义表情列表

这个小功能藏在设置选项的介绍里。

image-20230915035154317

基于性能考虑,自定义表情列表一旦建立后,主题就会将其缓存,除非手动更新,否则列表是不会变化的。这个特性带来了一个问题,若是我增加了一些表情包,如何进行列表更新?

更新的原理不复杂,清除已缓存的Transients即可。最初我打算在后台设置面板添加一个按钮,点击后可以通过AJAX来清除Transients,并返回清除结果。可是看了好久Codestar Framework文档,也没能做出来,这是一个不足之处,在看文档的大佬有啥想法吗,恳请指点一二。

当然也可以通过Transients Manager插件清除指定的Transients。但是通过安装插件解决这个问题,与“简单”的理念背道而驰,增加用户的使用门槛,自然不在考虑范围。

退而求次,Codestar Framework的选项描述是可以加入超链接的,可以利用这个功能通过GET调用函数来达到清除指定Transients的目的。当然需要限制在管理员页面,并且验证用户权限。就我个人体验来说,属于能用,但算不上不友好,将就着用吧。

function update_custom_smilies_list() {

    if (!is_admin() || !current_user_can('manage_options')) {
        return;
    }

    if (!isset($_GET['update_custom_smilies'])) {
        return;
    }

    $transient_name = sanitize_key($_GET['update_custom_smilies']);

    if ($transient_name === 'true') {
        delete_transient("custom_smilies_list");
        $custom_smilies_list = get_custom_smilies_list();
        $much = count($custom_smilies_list);
        echo '自定义表情列表更新完成!总共有' . $much . '个表情。';
    }
}
update_custom_smilies_list();

点击更新自定义表情列表的超链接后,会跳转到一个新建页面,展示处理结果。因为过程中不涉及到用户手动输入参数,故省去了一些异常处理。

image-20230915135441806

当然这个页面也可以输出一个精美的HTML,列出目前所有的自定义表情,但感觉没什么必要去做。

一些注意事项:

  • 每次更改了自定义表情文件夹后,都需要更新列表
  • 可以在自定义表情文件夹里的任何地方随意添加表情,但添加完一定要记得更新列表,否则新加入的表情会不显示,被删除的表情会404。
  • 添加自定义表情后,我不建议进行删除某个表情图片的操作,除非迫不得已。
  • 更改本地自定义表情文件夹后,如果启用了CDN代理,需要把本地的更改的文件同步到CDN服务器中,保证文件结构一致。

设置选项源代码

语言还没有进行国际化,为了方便理解,先填了中文。

      array(
        'id'       => 'smilies_list',
        'type'     => 'button_set',
        'title'    => '评论区表情',
        'desc' => __('选择要在评论区输入框显示的表情,全不选为关闭评论输入框表情功能。','sakurairo_csf'),
        'multiple' => true,
        'options'  => array(
          'bilibili'   => 'BILIBILI表情',
          'tieba'   => '贴吧表情',
          'yanwenzi' => '颜文字',
          'custom' => '自定义表情',
        ),
        'default'  => array( 'bilibili', 'tieba', 'yanwenzi' )
      ),

      array(
        'id'         => 'smilies_name',
        'type'       => 'text',
        'title'      => '自定义表情栏目名称',
        'desc' => __('建议输入少于4个汉字长度,以免引起移动端兼容异常。','sakurairo_csf'),
        'dependency' => array( 'smilies_list', 'any', 'custom', '', 'true' ),
        'default' => 'custom'
      ),

      array(
        'id'         => 'smilies_dir',
        'type'       => 'text',
        'title'      => '自定义表情的路径',
        'desc' => __('点击<a href="./admin.php?update_custom_smilies=true" target="_blank">这里</a>更新表情包列表。具体用法参考:','sakurairo_csf'),
        'dependency' => array( 'smilies_list', 'any', 'custom', '', 'true' )
      ),

      array(
        'id'         => 'smilies_proxy',
        'type'       => 'text',
        'title'      => '自定义表情代理地址',
        'desc' => __('填写表情图片的CDN地址,留空则不启用CDN代理功能。','sakurairo_csf'),
        'dependency' => array( 
                              array('smilies_list', 'any', 'custom', '', 'true' ),
                              array('smilies_dir', '!=', '', '', 'true')
                            ),
      ),

评论区面板

这个似乎没啥好介绍的。

    $smilies_panel = '';
    $bilibili_smilies = '';
    $tieba_smilies = '';
    $menhera_smilies = '';
    $custom_smilies = '';
    $bilibili_push_smilies = '';
    $tieba_push_smilies = '';
    $menhera_push_smilies = '';
    $custom_push_smilies = '';
    $smilies_list = iro_opt('smilies_list');
    if ($smilies_list) {
        if (in_array('bilibili', $smilies_list)) {
            $bilibili_smilies = '<th onclick="motionSwitch(\'.bili\')" class="bili-bar">bilibili~</th>';
            $bilibili_push_smilies = '<div class="bili-container motion-container"  style="display:none;">' . push_bili_smilies() . '</div>';
        }
        if (in_array('tieba', $smilies_list)) {
            $tieba_smilies = '<th onclick="motionSwitch(\'.tieba\')" class="tieba-bar">Tieba</th>';
            $tieba_push_smilies = '<div class="tieba-container motion-container" style="display:none;">' . push_tieba_smilies() . '</div>';
        }
        if (in_array('yanwenzi', $smilies_list)) {
            $menhera_smilies = '<th onclick="motionSwitch(\'.menhera\')" class="menhera-bar">(=・ω・=)</th>';
            $menhera_push_smilies = '<div class="menhera-container motion-container" style="display:none;">' . push_emoji_panel() . '</div>';
        }
        if (in_array('custom', $smilies_list)) {
            $custom_smilies = '<th onclick="motionSwitch(\'.custom\')" class="custom-bar"> '. iro_opt('smilies_name') .'</th>';
            $custom_push_smilies = '<div class="custom-container motion-container" style="display:none;">' . push_custom_smilies() . '</div>';
        }
        switch ($smilies_list[0]) {
            case "bilibili" :
                $bilibili_smilies = '<th onclick="motionSwitch(\'.bili\')" class="bili-bar on-hover">bilibili~</th>';
                $bilibili_push_smilies = '<div class="bili-container motion-container"  style="display:block;">' . push_bili_smilies() . '</div>';
                break;
            case "tieba" :
                $tieba_smilies = '<th onclick="motionSwitch(\'.tieba\')" class="tieba-bar on-hover">Tieba</th>';
                $tieba_push_smilies = '<div class="tieba-container motion-container" style="display:block;">' . push_tieba_smilies() . '</div>';
                break;
            case "yanwenzi" :
                $menhera_smilies = '<th onclick="motionSwitch(\'.menhera\')" class="menhera-bar on-hover">(=・ω・=)</th>';
                $menhera_push_smilies = '<div class="menhera-container motion-container" style="display:block;">' . push_emoji_panel() . '</div>';
                break;
            case "custom" :
                $custom_smilies = '<th onclick="motionSwitch(\'.custom\')" class="custom-bar on-hover"> '. iro_opt('smilies_name') .'</th>';
                $custom_push_smilies = '<div class="custom-container motion-container" style="display:block;">' . push_custom_smilies() . '</div>';
                break;
        }

        $smilies_panel = '<p id="emotion-toggle" class="no-select">
                                <span class="emotion-toggle-off">' . __("Click me OωO", "sakurairo")/*戳我试试 OωO*/ . '</span>
                                <span class="emotion-toggle-on">' . __("Woooooow ヾ(≧∇≦*)ゝ", "sakurairo")/*嘿嘿嘿 ヾ(≧∇≦*)ゝ*/ . '</span>
                            </p>
                            <div class="emotion-box no-select">
                                <table class="motion-switcher-table">
                                    <tr>
                                    '. $bilibili_smilies .'
                                    '. $tieba_smilies .'
                                    '. $menhera_smilies .'
                                    '. $custom_smilies .'
                                    </tr>
                                </table>
                                ' . $bilibili_push_smilies . '
                                ' . $tieba_push_smilies . '
                                ' . $menhera_push_smilies . '
                                ' . $custom_push_smilies . '            
                            </div>';

    };

获取自定义表情列表

/**
 * 通过文件夹获取自定义表情列表,使用Transients来存储获得的列表,除非手动清除,数据永不过期。
 * 数据格式如下:
 * Array
 * (
 *     [0] => Array
 *         (
 *             [path] => C:\xampp\htdocs\wordpress/wp-content/uploads/sakurairo_vision/@2.4/smilies\bilipng\emoji_2233_chijing.png
 *             [little_path] => /sakurairo_vision/@2.4/smilies\bilipng\emoji_2233_chijing.png
 *             [file_url] => http://192.168.233.174/wordpress/wp-content/uploads/sakurairo_vision/@2.4/smilies\bilipng\emoji_2233_chijing.png
 *             [name] => emoji_2233_chijing.png
 *             [base_name] => emoji_2233_chijing
 *             [extension] => png
 *         )
 *     ...
 * )    
 *
 * @return array
 */
function get_custom_smilies_list() {

    $custom_smilies_list = get_transient("custom_smilies_list"); // 检查Transient缓存

    if ($custom_smilies_list !== false) {
        return $custom_smilies_list; // 缓存存在,返回缓存数据
    }

    $custom_smilies_list = array();
    $custom_smilies_dir = iro_opt('smilies_dir');

    if (!$custom_smilies_dir) {
        return $custom_smilies_list; // 用户没有输入自定义表情路径,返回空数组
    }

    $custom_smilies_extension = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'avif','webp']; // 限制文件类型
    $custom_smilies_path = wp_get_upload_dir()['basedir'] . $custom_smilies_dir;

    if (!is_dir($custom_smilies_path)) {
        return $custom_smilies_list; // 拼接出来的路径不是一个文件夹,返回空数组
    }

    $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($custom_smilies_path), RecursiveIteratorIterator::LEAVES_ONLY); // 迭代自定义表情目录文件
        foreach ($files as $file) {
            if ($file->isFile()) {
                $file_name = $file->getFilename(); //完整的文件名
                $file_base_name = pathinfo($file_name, PATHINFO_FILENAME); // 基本文件名
                $file_extension = pathinfo($file_name, PATHINFO_EXTENSION); // 文件扩展名
                $file_path = $file->getPathname(); // 文件绝对路径
                $file_little_path = str_replace(wp_get_upload_dir()['basedir'], '' , $file_path); // 文件相对路径,相对于uploads文件夹
                $file_url = wp_get_upload_dir()['baseurl'] . $file_little_path; // 本地文件的URL路径
                if (in_array($file_extension, $custom_smilies_extension)) { // 限制文件类型
                    $custom_smilies_list[] = array( // 存储数据
                        'path' => $file_path,
                        'little_path' => $file_little_path,
                        'file_url' => $file_url,
                        'name' => $file_name,
                        'base_name' => $file_base_name,
                        'extension' => $file_extension
                    );
                }            
            }
        }
    set_transient("custom_smilies_list", $custom_smilies_list); // 配置Transient缓存

    return $custom_smilies_list;
}

输出表情列表

这个是照着Sakurairo的表情系统写的,将数据循环写入即可。

$custom_smiliestrans = array();
/**
 * 输出表情列表
 *
 */
function push_custom_smilies() {

    global $custom_smiliestrans; // 用于替换评论、文章中的表情符号
    $custom_smilies_panel = '';  // 用于输出到评论区表情面板
    $custom_smilies_list = get_custom_smilies_list();

    if (!$custom_smilies_list) {
        $custom_smilies_panel = '<div style="font-size: 20px;text-align: center;width: 300px;height: 100px;line-height: 100px;">File does not exist!</div>';
        return $custom_smilies_panel; // 空数组,在评论表情面板提示文件不存在
    }

    $custom_smilies_cdn = iro_opt('smilies_proxy');
    foreach ($custom_smilies_list as $smiley) {

        if ($custom_smilies_cdn) {
            $smiley_url = $custom_smilies_cdn . $smiley['little_path']; //构建表情文件CDN地址
        } else {
            $smiley_url = $smiley['file_url'];
        }
        $custom_smilies_panel = $custom_smilies_panel . '<span title="' . $smiley['base_name'] . '" onclick="grin(' . "'" . $smiley['base_name'] . "'" . ',type = \'Math\')"><img loading="lazy" style="height: 60px;" src="' . $smiley_url . '" /></span>';
        $custom_smiliestrans['{{' . $smiley['base_name'] . '}}'] = '<span title="' . $smiley['base_name'] . '" ><img loading="lazy" style="height: 60px;" src="' . $smiley_url . '" /></span>';
    }

    return $custom_smilies_panel;
}

替换评论、文章中的表情符号

/**
 * 替换评论、文章中的表情符号
 *
 */
function custom_smilies_filter($content) {
    push_custom_smilies();
    global $custom_smiliestrans;
    $content =  str_replace(array_keys($custom_smiliestrans), $custom_smiliestrans, $content);
    return $content;
}
add_filter('the_content', 'custom_smilies_filter'); 
add_filter('comment_text', 'custom_smilies_filter'); 

JS端的配合

Sakurairo主题的JavaScript位于:Sakurairo_Scripts

image-20230915145950665

JS端修改的不多,只是做了一个判断,防止表情面板不完全选择输出时产生报错。

const motionEles = [".bili", ".menhera", ".tieba", ".custom"];
function motionSwitch(ele) {
    for (let i = 0; i < motionEles.length; i++) {
        let smilies = document.querySelector(motionEles[i] + '-bar');
        if (smilies !== null) {
            smilies.classList.remove('on-hover');
            document.querySelector(motionEles[i] + '-container').style.display = 'none';
        }
    }
    document.querySelector(ele + '-bar').classList.add("on-hover");
    document.querySelector(ele + '-container').style.display = 'block';
}

最后

源代码

这个功能预计会在2.6.3版本中合并到Sakurairo里

ea4bf2fe88a2e1888491e6bfa21db4d40e865d9a

bcdc8b0fbcb0b7e6bb077c8de8ebea0c37469f64

参考资料

この素晴らしい世界に祝福を!
最后更新于 2023-11-04