开篇
一年多前折腾Sakurairo主题的时候,写了一篇关于添加自定义表情文章:Sakurairo主题评论区增加表情包,将原神的表情包添加进评论表情里。又从kanokano那里拿了一些kano的表情包,一起加了进去,她真的好可爱!
当初折腾的时候也意识到了一个问题,维护更新很麻烦。每更新一次主题,就需要手动修改一次主题源代码,添加自定义表情的过程也异常繁琐,需要先把所有表情包的文件名构建成一个数组,再进行调用输出到评论区里。虽然当时写了一个小PHP脚本用来处理表情包文件,一来二去,还是感觉麻烦诸多。最后这个表情也没有继续再维护了。
当时我就想着,能不能给Sakurairo主题添加一个自定义表情的功能,让用户能够使用自定义的表情。奈何技术力有限,最终不了了之。
前几天有个群友提起这个事情,问我主题的自定义表情怎么做,之前参考我的那篇文章做好了,后面更新主题后就炸了,现在想整回来。我听着还挺开心,原来我写的文章还是有人看的啊,这大概就是价值被认可的快乐?
随后将以前做的那篇文章发给了TA,不知现在整得咋样了。
我重温了一遍这篇文章,评论区不乏赞扬之词,死去的记忆突然向我潮涌而来,又想起了折腾主题那段快乐的时光。
然后,我决定要给它画一个句号。
前置条件
我想起了一年多前的想法,想着要不试试就在今天把它实现吧!看了一遍源代码,发现主题的表情系统不算复杂,照猫画虎应该能够搞定。随即思考实现方案,有两点尤为重要:
- 需要有简单的使用方法
- 不能影响页面的加载性能
需要有简单的使用方法:如果一个功能使用起来很麻烦,用户肯定不愿去使用。比如需要输入一个包含表情包的数组,又或者手动输入文件名才能正常使用,这是相当反人类的。需要用户做的,只有一个输入文件夹名称的操作就够了。
不能影响页面的加载性能:如果启用了自定义表情功能,页面渲染时间增加了1秒钟,这绝对是一个灾难!性能优化是一门高深的学问,我不认为自己能做好。我希望能尽量让用户无感知渲染时间的增加。众所周知的是,不管对于哪个系统来说,遍历一个文件夹以及子文件夹下的所有文件,这个过程是非常耗时间的,尤其是在文件数量众多的情况下。所以表情文件应该按需遍历,好在WordPress的Transients API可以用来解决这一个问题。可以用Transients将遍历文件夹后的数据存储起来,这是一种临时的存储手段,类似于一个缓存。
开始创建
思路大致理清后,可以上手干活了。
设置选项说明
Sakurairo后台设置选项使用的是Codestar Framework框架,在文档的帮助下,还是比较容易上手的。一共4个设置选项,其中两个是可选项。
评论区表情
这个选项用于控制显示在评论区域面板的表情类别,该项目可多选。如果用户对内置的三种表情不感冒,或者想扩充表情类别,这时自定义表情功能就派上用场了。
如果4种表情都不勾选,则会关闭评论区面板上的表情输入功能,历史评论中的表情仍然能够正常显示,只是新的评论不能再添加表情。
自定义表情栏目名称
顾名思义,就是表情的类别名称,会显示在评论区域表情类别选择栏目上。
上图是以原神
作为表情栏目名称
上图是以kano
作为表情栏目名称
从最佳实践上看,在启用所有表情类别的情况下,4个汉字的栏目名称,可能会导致移动端横向空间不足。
如果你发现这一问题,这时需要做的是减少一个类别的启用或者缩短表情类别名称,以保证横向空间。桌面端宽度足够,未发现类似问题。
自定义表情的路径
需要明确的是,受制于权限要求,主题能够访问到的文件夹是有限的,这里填入的必须是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
其它一些注意事项:
- 表情文件夹里可以建立子文件夹,方便对表情进行归类存放
- 表情文件夹(包括子文件夹)下所有文件的文件名,都不能存在重名(即使是扩展名不同)
- 支持的文件格式有:
jpg
、jpeg
、png
、gif
、svg
、avif
、webp
,其他的文件格式,都不会被收录进表情列表中 - 建议表情图片的长宽像素比例为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
更新自定义表情列表
这个小功能藏在设置选项的介绍里。
基于性能考虑,自定义表情列表一旦建立后,主题就会将其缓存,除非手动更新,否则列表是不会变化的。这个特性带来了一个问题,若是我增加了一些表情包,如何进行列表更新?
更新的原理不复杂,清除已缓存的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();
点击更新自定义表情列表的超链接后,会跳转到一个新建页面,展示处理结果。因为过程中不涉及到用户手动输入参数,故省去了一些异常处理。
当然这个页面也可以输出一个精美的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
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
Comments 2 条评论
自定义表情实装啦