封面：アスナ（Pixiv ID：29328488）介绍emoji 即表情符号，来自日语词汇“絵文字”（假名为“えもじ”，读音即 emoji），最初由栗田穰崇创作，目前已被纳入 Unicode 编码并得到广泛支持。⭐Emojipedia 收录了目前所有的 emoji。🎈尽管如此，emoji 在部分老设备、旧系统的支持性并不好，甚至可能显示为类似“□”的无法显示字符。这当然是不好的。为了优化展现，目前常用的做法是渲染时用图片替换 emoji 字符。相关的项目有 GitHub 正在使用的 g-emoji-element 以及 Twitter 的 twemoji 等。本文将要介绍的就是引入 twemoji 来优化展现。🍻使用方法引入 twemoji.jstwemoji 官方默认提供 MaxCDN 作为加载源，但该 CDN 在国内似乎可用性并不好。因此，笔者还是一贯地选择 jsDelivr。当然，你也可以从官方仓库下载并制作你自己的源。twemoji.js 从 NPM 拉取，使用： 将上述代码插入到网页中适当的位置，即可引入 twemoji。此时 window 下会有 twemoji 对象以便调用。💡调用 parse 方法twemoji.parse() 方法的详细用法在 README 中有很科学💎的说明，所以笔者在这里只介绍整体替换的思路。这个方法接受两个参数：第一个参数是解析的对象，直接使用 document.body 即可；第二个参数是一个 Object，包含一些选项：{ callback: Function, // default the common replacer attributes: Function, // default returns {} base: string, // default MaxCDN ext: string, // default ".png" className: string, // default "emoji" size: string|number, // default "72x72" folder: string // in case it's specified // it replaces .size info, if any }callback 是每次替换的回调，返回值将作为 的 src 属性，即对应替换图片的 URL。attributes 是另一个回调，返回值应该是一个 Object，这个 Object 将被整合到 img 的 attributes，例如返回 {test: 'test'} 就会使 多出一个 test="test" 的属性。base 是 emoji 替换图片的加载源，默认是 MaxCDN，稍后笔者将会给出 jsDelivr 的对应参数值。⚠️ext 指定 emoji 替换图片的扩展名，默认是 .png，也可以使用 .svg 或其他类型（需要 size 或 folder 配合）。className 指定 的 class 属性，默认是 emoji，如果与现有 class 有冲突则需要改变。size 指定“图片尺寸”，实质上是指定一个目录名。不建议修改。folder 指定 emoji 替换图片的目录名，将会覆盖 size 的设置。⚠️ 使用 jsDelivr 时，emoji 替换图片不能直接从 NPM 拉取，而是要从 GitHub 拉取，应该把 base 设置为 https://cdn.jsdelivr.net/gh/twitter/twemoji@13.0.0/assets/（末尾的 / 不可省略，原因后述）。如果没有指定 callback，组装 URL 的规则默认是 {base}{size|folder}/{icon}{ext}，其中 icon 作为 callback 的第一个参数传入。所以，当使用默认 callback 时，如果 base 的末尾缺少 /，会直接导致 URL 拼接非法。同理，ext 也一定要以 . 开头。👌综上，解析的代码如下：twemoji.parse(document.body, { base: 'https://cdn.jsdelivr.net/gh/twitter/twemoji@13.0.0/assets/' });handsome 主题handsome 是 Typecho 主题，所以首先需要确保 Typecho 已经开启 emoji 支持，即数据库编码已经调整至 utf8mb4，否则可能会出现数据损坏的严重后果。调整的方法可以参考：使 Typecho 支持 emoji 的显示😂😂😂 以及 如何让Typecho支持emoji的存储。调整完毕后，可以发送几条包含 emoji 的评论测试配置是否有效。handsome 主题配置中可以直接加入 twemoji 的设定：第一步，依次展开 开发者设置 -> 自定义输出body 尾部的HTML代码，在末尾加入： 第二步，依次展开 开发者设置 -> 自定义 JavaScript，同样在末尾加入：$(function() { twemoji.parse(document.body, { attributes() {return {nogallery: 'nogallery'};}, base: 'https://cdn.jsdelivr.net/gh/twitter/twemoji@13.0.0/assets/' }); });第三步，依次展开 开发者设置 -> 自定义 CSS，还是在末尾加入：img.emoji { height: 1em !important; width: 1em !important; margin: 0 .05em 0 .1em !important; vertical-align: -0.1em !important; border-radius: 0 !important; border: 0 !important; display: inline-block !important; padding: 0 !important; }为了处理样式冲突，不得不加了很多 !important（话说这也太多了吧😂）如果你还开启了 PJAX，那么在 PJAX -> PJAX回调函数 末尾也需要加入第二步中的代码。这样，我们就成功引入了 twemoji，emoji 在旧设备、旧系统也能很漂亮了~💯
handsome 主题自带“引用其他文章”和“手动指定摘要内容”功能，但两者似乎并不适配。笔者个人认为“手动指定摘要内容”功能更应该交由插件实现（挂 excerpt 钩子），但这超出了本文讨论范围，不详细展开。直接在文章中插入“引用其他文章”的代码，会解析出文章原本的内容并截取作为引用块的摘要，感觉有些不协调。笔者出于强迫症，考虑从代码层面一次性根本解决这个问题。定位到“引用其他文章”功能实现的核心方法 Content::quoteOtherPostCallback()（libs/Content.php），代码就不贴了，可以看到此处是没有被引用文章的上下文的，为了获取文章的信息需要执行 SQL 进行查询。这里作者虽然查了头图字段，但是却并没有查自定义摘要字段。这里贴一个小技巧（来自 Typecho 源代码），如何优雅地根据 cid 查询自定义字段：首先肯定是要查一次字段表的，也就是 prefix_fields 表，这里的 prefix_ 是安装时设置的数据表前缀。第一个问题就是如何获取这个前缀，但实际上 Typecho 包装的数据库驱动可以使用 table.xxx 代替 prefix_xxx，那么就不再需要手动获取这个前缀，即：$db->fetchAll($db->select()->from('table.fields')->where('cid = ?', $cid));这样一句话将会返回 $cid 文章的所有字段，但是这还不够，对其进行一下加工：$fields = array(); $rows = $db->fetchAll($db->select()->from('table.fields')->where('cid = ?', $cid)); foreach ($rows as $row) { $fields[$row['name']] = $row[$row['type'] . '_value']; } $fields = new Typecho_Config($fields);这样处理之后得到的 $fields（显而易见，是一个 Typecho_Config 对象）对应的成员就保存着对应自定义字段的值。例如 $fields->xxx。回到手动指定摘要，该字段对应的内部名称为 customSummary，这时我们就用 $fields->customSummary 来访问它。if (trim($fields->customSummary) != "") { $targetSummary = $fields->customSummary; } else { $targetSummary = Content::excerpt(Markdown::convert($result['text']), 60); }那么修改后的完整代码就是这样： /** * 一篇文章中引用另一篇文章正则替换回调函数 * @param $matches * @return bool|string */ public static function quoteOtherPostCallback($matches){ $options = mget(); // 不解析类似 post 双重括号的代码 if ( $matches[1] == '[' && $matches[6] == ']' ) { return substr($matches[0], 1, -1); } //对$matches[3]的url如果被解析器解析成链接，这些需要反解析回来 $matches[3] = preg_replace("/(.*?)<\/a>/",'$1',$matches[3]); $attr = htmlspecialchars_decode($matches[3]);//还原转义前的参数列表 $attrs = self::shortcode_parse_atts($attr);//获取短代码的参数 //这里需要对id做一个判断，避免空值出现错误 $cid = @$attrs["cid"]; $url = @$attrs['url']; $cover = @$attrs['cover'];//封面 $targetTitle = "";//标题 $targetUrl = "";//链接 $targetSummary ="";//简介文字 $targetImgSrc = "";//封面图片地址 if (!empty($cid)){ $db = Typecho_Db::get(); $prefix = $db->getPrefix(); $posts = $db->fetchAll($db ->select()->from($prefix.'contents') ->orWhere('cid = ?',$cid) ->where('type = ? AND status = ? AND password IS NULL', 'post', 'publish')); //这里需要对id正确性进行一个判断，避免查找文章失败 if (count($posts) == 0){ $targetTitle = "文章不存在，或文章是加密、私密文章"; }else{ $result = Typecho_Widget::widget('Widget_Abstract_Contents')->push($posts[0]); $fields = array(); $rows = $db->fetchAll($db->select()->from('table.fields')->where('cid = ?', $cid)); foreach ($rows as $row) { $fields[$row['name']] = $row[$row['type'] . '_value']; } $fields = new Typecho_Config($fields); if ($cover == "") { $targetImgSrc = Content:: whenSwitchHeaderImgSrc(0, 2, null, $result['text'], @$fields->thumb, 1); } else { $targetImgSrc = $cover; } if (trim($fields->customSummary) != "") { $targetSummary = $fields->customSummary; } else { $targetSummary = Content::excerpt(Markdown::convert($result['text']), 60); } $targetTitle = $result['title']; $targetUrl = $result['permalink']; } }else if (empty($cid) && $url !=""){ $targetUrl = $url; $targetSummary = @$attrs['intro']; $targetTitle = @$attrs['title']; $targetImgSrc = $cover; }else{ $targetTitle = "文章不存在，请检查文章CID"; } $imageHtml = ""; $noImageCss = ""; if (trim($targetImgSrc) != ""){ $imageHtml = ' '; }else{ $noImageCss = 'style="margin-left: 10px;"'; } return <<<EOF {$imageHtml} {$targetTitle} {$targetSummary} EOF; }因为本来就进行了一次对字段表的 SQL 查询，修改也只是对其逻辑进行合并，SQL 查询的次数并没有增加。[heimu]（其实如果算期望，还是增加了的，原本可能只执行一次，取决于 cover 参数设置与否，现在必然执行两次）[/heimu]
MathJax 目前已经推出了全新的 v3 版本，使用现代化的编程技巧重写了整个代码，包括 TS、ES6、Promises 等，同时全面采用 jsDelivr 作为默认 CDN 取代原先的 cdn.mathjax.org，在速度上有较大提升（国内尤为明显）。咕咕咕了很久之后我终于下定决心将其更新到了 v3 版本。这篇笔记就来讲解一下从 v2 升级到 v3 的一些快捷方法和注意事项。[scode type="blue"]这篇笔记针对 Typecho 的 handsome 主题编写，对于其他程序/主题的 MathJax 更新可能仅仅只有启发作用。[/scode]生成 v3 配置MathJax 有一个配置选项，在 v2 系列版本中书写在 我的写法是： 并没有什么明显的区别。插入部署完毕后，来到前端页面 F12 并在 Console 中运行 MathJax.version，就可以看到 3.x.x 的返回值咯 ~参考MathJax Configuration ConverterTypesetting and Converting Mathematics — MathJax 3.0 documentationmathjax/MathJax-demos-web: A repository with examples using mathjax-v3
引入什么是“黑幕效果”呢？[heimu]像这样[/heimu]，正常状态下被一个黑条遮挡，只有当鼠标放在上面（移动端是长按）后才显示出内容的，富有交互性的有趣功能样式，就被成为“黑幕效果”。它可以让文章更加活泼，内容更加有趣，[heimu]以及表达一些特殊的语气~[/heimu]这样，你就对黑幕效果有了一个基本的认知[heimu]，阅读气氛也活跃起来了[/heimu]，下面我们就来看一看小黑条的背后有哪些核心技术。 ::majsoul:028::CSS部分虽然大家都知道了，但还是简单提一句，CSS（Cascading Style Sheets，层叠样式表）是现代互联网技术中一个重要的组成部分，在前端开发中有着至关重要的地位。通过这种东西，我们可以方便地为网页中的内容添加丰富多彩的样式。黑幕效果需要哪些样式呢？ ::majsoul:034:: 我们再来看一下黑幕效果的呈现：[heimu]我是小黑条~[/heimu]它的背景是黑色的，文字颜色与背景相同，所以正常状态下看不见文字内容。背景颜色在CSS中通过background-color属性控制，而前景色（也就是文字颜色啦）通过color属性控制，为了方便，我们定义一个.heimu类，这样所有class="heimu"的元素都可以自动获得我们规定的这些属性。就像这样：（现在请暂时不要记笔记，需要记笔记的时候我会提醒你哦）.heimu { background-color: #000; color: #000; }这个东西怎么用呢？我们写一个测试页面看看： heimu test This is heimu text. （蓝色部分是我用鼠标选中造成的）还行，有那种感觉了，但是黑幕效果怎么变成了刮刮乐啊？这是因为我们还没有处理鼠标移入（或移动端长按）时这个样式的变化。这部分我们使用:hover伪类实现，这个伪类将会在鼠标放上去的时候起作用，改变样式。.heimu:hover, .heimu:active { color: #FFF; }实际使用时，我还使用了:active伪类，但它在此处作用不明显[heimu]，仅仅是我出于心理安慰加上的[/heimu]。在测试页面中加入这部分内容，我们就基本完成啦~实际使用时，不一定要用完完全全的黑色，只需要前景色与背景色一致即可；我们还可以加入渐变（用transition实现）。下面放出我使用的CSS部分内容，同学们可以记笔记啦~.heimu:active, .heimu:hover { color: #fff } .heimu { background-color: #252525; color: #252525; text-shadow: none; transition: color 0.3s; }JS部分通过前面的样式准备工作，我们已经可以通过小黑条~这样的方式来调用我们的黑幕效果了，但是这样未免冗长，而且大部分都是重复内容。我们规定一个自定义语法[heimu]xxx[/heimu]，把一对这样的闭合标记解析为xxx，中间内容不变，这样就可以大大降低我们调用时的工作量。这个工作可以交给插件实现，也可以交给js实现。出于性能考虑，我用js写了一个不算完整的实现。用js进行替换的原理是正则匹配，在整个文章内容中按照DOM树遍历一次，完成整个替换。之所以没有直接替换是因为要规避一些特殊标记内的关键文本，例如数学公式和元素。这样同时带来了一个缺陷，即黑幕效果只能覆盖简单文本，复杂文本（例如有链接的情况）当前版本是无法解析的。这里直接给出代码如下：function icore_init() { var t = new RegExp("\\[heimu\\](.*?)\\[/heimu\\]","g"); $("div.entry-content,div#morphing-content>div.page").children("p,blockquote,div.tip").each(function() { var n = ""; $(this).contents().each(function() { var i = this.outerHTML; null == i && (i = $(this).text()), null != i && ((1 === this.nodeType || 3 === this.nodeType) && "IMG" != this.tagName && "PRE" != this.tagName && "CODE" != this.tagName && "SCRIPT" != this.tagName && "BR" != this.tagName ? -1 != $.inArray("light-link", this.classList) || function(t=this.classList) { if (null == t || null == t) return !1; for (var n = 0; n < t.length; n++) if (-1 != t[n].indexOf("MathJax")) return !0; return !1 }() ? n += i : (i = i.replace(t, function() { return '' + arguments[1] + "" }), n += i) : n += i) }), $(this).html(n) }); } icore_init();[heimu]可以看到内容极其紊乱，其实是我混淆后找不到原文件了（悲[/heimu]只对handsome主题5.x系列版本做了适配，不保证不出现未知问题，如有情况可以评论联系我，但不保证解决。如果开启pjax，请在pjax回调函数中加入icore_init();，否则无刷新加载后不会进行解析。（handsome主题的该配置项位于“PJAX->PJAX回调函数”中，如果其中已经有代码，加入在末尾即可）title属性可以自定义，该属性规定的是鼠标放上去后显示的文本。至此，黑幕效果背后的黑科技已经完全呈现在你眼前了~ ::majsoul:077:: 顺便说一句，这部分内容也包括在我的自定义增强效果iCore（不过暂时没有开源，因为内容太少了）中哦~ ::majsoul:102::
最近给博客增加了船新的雀魂表情包，由于从导出表情到重新配置OwO.json都是完全由自己来完成，不像上次扩展贴吧表情的时候有现成的东西可以直接拿来用，所以记录一下以便之后需要时留作参考。 ::majsoul:001:: 并没有尝试一组表情中能否多种格式图片并存，这次统一采用了png格式，避免给自己找锅。本次新增的一组表情共计151张，需要在OwO.json中对它们一一进行声明。手写是不太可能的，因为是大量的重复工作，懒惰如我显然是宁可写一个Generator也不会去一条条加。gen.py的内容如下：#!/usr/bin/python3 OWO_NAME='雀魂' OWO_SLUG='majsoul' PICTURE_COUNT=151 f=open("OwO.json","w",encoding='utf-8') f.write(''' "%s": { "name": "%s", "type": "image", "container": [ '''%(OWO_NAME,OWO_SLUG)) for i in range(1,PICTURE_COUNT+1): f.write(''' { "icon": "%03d", "text": "%03d" }'''%(i,i)) if i<PICTURE_COUNT: f.write(',') f.write('\n') f.write(''' ] }''') f.close()其中OWO_NAME为这组表情的显示名称，OWO_SLUG为表情的内部别名，PICTURE_COUNT为图片总数。若需要生成其他表情包声明，改动这些值即可。执行后将在当前目录下生成一个OwO.json，内容是需要插入到完整OwO.json中的这个表情包的声明。 ::majsoul:029:: [scode type="yellow"]这里生成的并不是完整的json文件，需要手动将其插入完整的OwO.json中。[/scode]如果执行正确，将会生成形如下方代码块中所示的一段文本： "雀魂": { "name": "majsoul", "type": "image", "container": [ { "icon": "001", "text": "001" }, { "icon": "002", "text": "002" } ] }为方便起见，我只列出了PICTURE_COUNT=2的情况。插入位置如下图所示，需要视情况在后方补一个半角逗号。应用json检测工具检测通过后即可部署。由于上述代码是针对Python3编写的，考虑到有时并不方便获取Python3执行环境，补充一个JavaScript版本：(function(){ 'use strict'; const OWO_NAME="雀魂"; const OWO_SLUG="majsoul"; const PICTURE_COUNT=151; const html_encode=str=>{ let s=""; if (str.length==0)return""; s=str.replace(/&/g,"&"); s=s.replace(/</g,"<"); s=s.replace(/>/g,">"); s=s.replace(/ /g," "); s=s.replace(/\'/g,"'"); s=s.replace(/\"/g,"""); s=s.replace(/\n/g," "); return s; } let output=` "${OWO_NAME}": { "name": "${OWO_SLUG}", "type": "image", "container": [ `; for(let i=1,num;i<=PICTURE_COUNT;i++){ num=(Array(3).join('0')+i).slice(-3); output+=` { "icon": "${num}", "text": "${num}" }`; if(i<PICTURE_COUNT)output+=','; output+='\n'; } output+=` ] }`; document.write(`${html_encode(output)}`); })();在浏览器中新建标签页，按下F12键，在开发人员工具的Console选项卡中粘贴并执行上述代码即可生成在网页上，复制后插入即可。
魔改Lazyload前台调用代码实现异步加载图片
图片传输效率又是一大性能瓶颈，有钱人们选择用对象储存解决，我就没法解决。当然是不可能的。我使用的handsome主题自带lazyload功能，但是默认关闭，且作者并不推荐使用，称或将带来卡顿（？）。当然，即使你的网站没有lazyload，你也可以自行引入，这里不再赘述。什么是Lazyload一个可以实现图片懒加载的插件。所谓图片懒加载，即“你不看我就不加载”，最经典的操作便是监听一个scroll事件，当『快要看到这张图片』时，开始一个异步请求进行图片的加载。不难发现，其中最重要，也最微妙的就是对于『快要看到这张图片』时机的把握。大部分经典的实例，是通过一个距离来控制，也就是lazyload中的threshold参数。它用于设置一个临界值，当屏幕下端距离屏幕外的图片距离小于等于这个值的时候，lazyload开始load。大概类似于早自习犯困的你拜托同桌当老师来的时候提醒你一下（addEventListener），当负责任又好心的同桌提醒你时（trigger），你就赶紧爬起来认真早读。为何使用Lazyload我注意到，当网络状况差，图片无法立即加载时，大部分关键的js都处于蓄势待发状态，需等待图片加载完成（此时才会触发document的ready）才会开始执行，极大地影响了用户[heimu]我自己[/heimu]体验。事实上，图片加不加载好对我js的执行并没有影响，完全可以让图片加载自个在后面慢慢拖，而让document的ready提前，从而尽可能缩短js不执行的糟糕真空期。这时，就轮到lazyload大显身手啦~使用handsome主题的typecho用户可以通过打开“后台->控制台->外观->设置外观->主题增强功能->增强功能开关”中的“延迟加载图片（lazyload）”选项来打开lazyload。通用的方法是在body结束前、jQuery加载后手动引入lazyload.js 同时对需要进行lazyload的元素属性作一定的调整，设置data-original为实际加载地址，并设置一个占位图片，在这里不再赘述。问题与改进在使用过程中，我发现了新的问题，即我的图片加载实在是太 慢 了，以至于根本无法在滚动到图片位置时完成加载。就好像虽然你的同桌提醒了你，但你实在是太困，一直磨蹭到老师站在你面前了也没把书拿出来。事实上，我们也不是真的希望这个图片“懒”，只是希望它不要给大家拖后腿，别卡住document的ready事件。那么我们就完全可以跳出常规思路，不再使用默认的scroll作为触发事件。lazyload提供了一个参数event允许我们自定义触发事件。只需要给这个参数随便传一个不冲突的事件名字，在设置完成lazyload之后立即trigger我们的自定义事件，即可瞬间开始图片的异步请求，而不必等待滚动，如此一来就能进一步优化体验。栗子：$("img").lazyload({ effect: "fadeIn", event: "display" // 替换默认的触发事件 }); $("img").trigger("display"); // 立即触发仅仅是这样似乎还不够，lazyload似乎仍然是滚动优先触发。虽然做到这个程度已经差不多了，但我不想让它的触发机制复杂起来，不妨设想有时需要点击后再开始加载，这时滚动触发就没法让它按照预期去呈现。所以我们再给它补一个threshold参数混淆视听，设置一个绝对值很大的负数，滚动触发基本上就没什么戏了。这里我选择了-0x7fffffff，下面是我最终使用的代码：$("img").lazyload({ threshold: -0x7fffffff, // 让不需要的部分见鬼去 effect: "fadeIn", event: "display" // 替换默认的触发事件 }); $("img").trigger("display"); // 立即触发
BUG简述近期更新了handsome 5.2.0，后台写文章时发现编辑器工具栏上贴心地增加了增强功能的快捷按钮，于是稍微试了试。今天的主角就是我们的“调用其他文章”功能。方便起见，在本地搭建typecho+handsome测试环境。复现如下：emm，原来你管这叫随机……修复方法众所周知出锅需要补。根据常识，对增强功能的解析在libs/Content.php中完成，于是定位到该文件相关位置：自postContent方法开始跟踪，相关代码如下： /** * 输入内容之前做一些有趣的替换+输出文章内容 * * @param $obj * @param $status * @return string */ public static function postContent($obj,$status){ $content = $obj->content; $options = mget(); //... //中略 //... $content = Content::parseContentPublic($content); } return trim($content); }注意到解析外部文章调用逻辑存在于parseContentPublic方法中，定位到该方法继续跟踪，相关代码如下： /** * 一些公用的解析，文章、评论、时光机公用的，与用户状态无关 * @param $content * @return null|string|string[] */ public static function parseContentPublic($content){ $options = mget(); //... //中略 //... //调用其他文章页面的摘要 if ( strpos( $content, '[[post')!== false) {//提高效率，避免每篇文章都要解析 $pattern = self::get_shortcode_regex(array('post')); $content = preg_replace_callback("/$pattern/",array('Content','quoteOtherPostCallback'), $content); } //... //后略 //...显然，具体内容替换已经交由Content::quoteOtherPostCallback()处理，定位至该方法，相关代码如下： /** * 一篇文章中引用另一篇文章正则替换回调函数 * @param $matches * @return bool|string */ public static function quoteOtherPostCallback($matches){ $options = mget(); // 不解析类似 post] 双重括号的代码 if ( $matches[1] == '[' && $matches[6] == ']' ) { return substr($matches[0], 1, -1); } //对$matches[3]的url如果被解析器解析成链接，这些需要反解析回来 $matches[3] = preg_replace("/(.*?)<\/a>/",'$1',$matches[3]); $attr = htmlspecialchars_decode($matches[3]);//还原转义前的参数列表 $attrs = self::shortcode_parse_atts($attr);//获取短代码的参数 //这里需要对id做一个判断，避免空值出现错误 $cid = @$attrs["cid"]; $url = @$attrs['url']; $cover = @$attrs['cover'];//封面 $targetTitle = "";//标题 $targetUrl = "";//链接 $targetSummary ="";//简介文字 $targetImgSrc = "";//封面图片地址 if (!empty($cid)){ $db = Typecho_Db::get(); $prefix = $db->getPrefix(); $posts = $db->fetchAll($db ->select()->from($prefix.'contents') ->orWhere('cid = ?',$cid) ->where('type = ? AND status = ? AND password IS NULL', 'post', 'publish')); //这里需要对id正确性进行一个判断，避免查找文章失败 if (count($posts) == 0){ $targetTitle = "文章不存在，或文章是加密、私密文章"; }else{ $result = Typecho_Widget::widget('Widget_Abstract_Contents')->push($posts[0]); if ($cover == ""){ $thumbArray = $db->fetchAll($db ->select()->from($prefix.'fields') ->orWhere('cid = ?',$cid) ->where('name = ? ', 'thumb')); $targetImgSrc = Content:: whenSwitchHeaderImgSrc(0,2,null,$result['text'],@$thumbArray[0]['str_value']); }else{ $targetImgSrc = $cover; } $targetSummary = Content::excerpt(Markdown::convert($result['text']),60); $targetTitle = $result['title']; $targetUrl = $result['permalink']; } }else if (empty($cid) && $url !=""){ $targetUrl = $url; $targetSummary = @$attrs['intro']; $targetTitle = @$attrs['title']; $targetImgSrc = $cover; }else{ $targetTitle = "文章不存在，请检查文章CID"; } $imageHtml = ""; $noImageCss = ""; if (trim($targetImgSrc) != ""){ $imageHtml = ' '; }else{ $noImageCss = 'style="margin-left: 10px;"'; } return <<<EOF {$imageHtml} {$targetTitle} {$targetSummary} EOF; }该方法中头图又由Content::whenSwitchHeaderImgSrc()获取，跟踪： /** * 处理具体的头图显示逻辑：当有头图时候，显示随机图片还是第一个附件还是一张图片还是thumb字段 * @param int $index * @param $howToThumb 显示缩略图的方式，0，1，2，3 * @param $attach 文章的第一个附件 * @param $content 文章内容 * @param $thumbField thumb字段 * @return string */ public static function whenSwitchHeaderImgSrc($index =0,$howToThumb,$attach,$content,$thumbField){ $options = mget(); $randomNum = unserialize(INDEX_IMAGE_ARRAY); // 随机缩略图路径 $random = THEME_URL . 'usr/img/sj/' . @$randomNum[$index] . '.png';//如果有文章置顶，这里可能会导致index not undefined $pattern = '/\<img.*?src\=\"(.*?)\"[^>]*>/i'; $patternMD = '/\!\[.*?\]\((http(s)?:\/\/.*?(jpg|png))/i'; $patternMDfoot = '/\[.*?\]:\s*(http(s)?:\/\/.*?(jpg|png))/i'; if ($howToThumb == '0'){ return $random; }elseif ($howToThumb == '1' || $howToThumb == '2'){ if (!empty($thumbField)){ return $thumbField; }elseif ($attach!=null && isset($attach->isImage) && $attach->isImage == 1){ return $attach->url; }else{ if (preg_match_all($pattern, $content, $thumbUrl)){ $thumb = $thumbUrl[1][0]; }elseif (preg_match_all($patternMD, $content, $thumbUrl)){ $thumb = $thumbUrl[1][0]; }elseif (preg_match_all($patternMDfoot, $content, $thumbUrl)){ $thumb = $thumbUrl[1][0]; }else{//文章中没有图片 if ($howToThumb == '1'){ return ''; }else{ return $random; } } return $thumb; } }elseif ($howToThumb == '3'){ if (!empty($thumbField)){ return $thumbField; }else{ return $random; } } }由于无论几次调用，传入的$index始终为0（仅在文章列表显示逻辑中该参数发挥作用，具体暂未作分析），而随机头图的逻辑是将整个随机头图集随机乱序从中取索引值为$index的一项进行输出，且乱序操作仅在开始前进行一次，故此时当前文章大头图（如果随机）、文章内引用的其他文章展现头图（如果随机）均为一致项目。个人认为展现效果较差，尤其是当这篇文章是随机头图，而引用处又距离大头图较近，将产生单调感。出于稳定性和维护方便考虑不操作$index参数，而是考虑在whenSwitchHeaderImgSrc方法中新建static变量，每次随机调用时将该变量+1，从而达到显示不同的随机头图效果。先放出代码，然后进行必要解释。 /** * 处理具体的头图显示逻辑：当有头图时候，显示随机图片还是第一个附件还是一张图片还是thumb字段 * @param int $index * @param $howToThumb 显示缩略图的方式，0，1，2，3 * @param $attach 文章的第一个附件 * @param $content 文章内容 * @param $thumbField thumb字段 * @param $quote 是否在处理调用外部文章 0没有 1正在处理 2复位 * @return string */ public static function whenSwitchHeaderImgSrc($index =0,$howToThumb,$attach,$content,$thumbField,$quote=0){ static $used=1;//(ucw)解决引用其他文章随机头图重复问题 //(ucw)附注：存在潜在bug，随机头图集的size需要足够大 if($quote==2){$used=1;return '';} $options = mget(); $randomNum = unserialize(INDEX_IMAGE_ARRAY); // 随机缩略图路径 $random = THEME_URL . 'usr/img/sj/' . @$randomNum[$quote?$used:$index] . '.png';//如果有文章置顶，这里可能会导致index not undefined $pattern = '/\<img.*?src\=\"(.*?)\"[^>]*>/i'; $patternMD = '/\!\[.*?\]\((http(s)?:\/\/.*?(jpg|png))/i'; $patternMDfoot = '/\[.*?\]:\s*(http(s)?:\/\/.*?(jpg|png))/i'; if ($howToThumb == '0'){ if($quote)$used++; return $random; }elseif ($howToThumb == '1' || $howToThumb == '2'){ if (!empty($thumbField)){ return $thumbField; }elseif ($attach!=null && isset($attach->isImage) && $attach->isImage == 1){ return $attach->url; }else{ if (preg_match_all($pattern, $content, $thumbUrl)){ $thumb = $thumbUrl[1][0]; }elseif (preg_match_all($patternMD, $content, $thumbUrl)){ $thumb = $thumbUrl[1][0]; }elseif (preg_match_all($patternMDfoot, $content, $thumbUrl)){ $thumb = $thumbUrl[1][0]; }else{//文章中没有图片 if ($howToThumb == '1'){ return ''; }else{ if($quote)$used++; return $random; } } return $thumb; } }elseif ($howToThumb == '3'){ if (!empty($thumbField)){ return $thumbField; }else{ if($quote)$used++; return $random; } } }我新建了一个integer型参数$quote，作为是否正在处理引用的标志。static变量$used标记目前可用（未重复）的首个随机头图索引，当且仅当需要返回随机结果且正在处理引用情况时，使$used++。$used初始化为1防止与当前文章的随机头图重复。这样做有一个潜在bug：当随机头图的数量小于或等于引用文章的数目时，数组的对应位置不存在，此时会出现显示问题。（由于php的变量声明真的很随意，这只是一个warning级别的错误，最终输出的url是一个不存在的地址，前台访问时会404导致图片无法显示）我忽视这个bug的原因是我为自己配置了44张随机头图（具体来源信息参见左侧边栏“友情链接->总览”），而我不太可能一次性引用44篇文章。修复这个潜在bug也非常简单，只需要将$used对随机头图数目取模。上述的修复方法有些绕弯子，甚至增加了参数。但我考虑到实装阅读模式（在我的blog，它是“启动魔眼”）后，postContent方法不止调用一次，替换后的内容应该保证对同一次生成的内容是一致的，不应该出现阅读模式下头图与常规模式下有差异的情况，且这样会需要更大的随机头图集，容易带来不可预料的结果，所以我额外编写了重置$used变量的逻辑，在每次调用parseContentPublic方法时： /** * 一些公用的解析，文章、评论、时光机公用的，与用户状态无关 * @param $content * @return null|string|string[] */ public static function parseContentPublic($content){ $options = mget(); //... //中略 //... //调用其他文章页面的摘要 if ( strpos( $content, '[[post')!== false) {//提高效率，避免每篇文章都要解析 Content::whenSwitchHeaderImgSrc(0,0,0,0,0,2);//* 这里 $pattern = self::get_shortcode_regex(array('post')); $content = preg_replace_callback("/$pattern/",array('Content','quoteOtherPostCallback'), $content); } //... //后略 //...首先进行复位，在几乎不影响效率的前提下保证内容的稳定性。同时，调用时加上最后一个参数： /** * 一篇文章中引用另一篇文章正则替换回调函数 * @param $matches * @return bool|string */ public static function quoteOtherPostCallback($matches){ $options = mget(); // 不解析类似 post] 双重括号的代码 if ( $matches[1] == '[' && $matches[6] == ']' ) { return substr($matches[0], 1, -1); } //对$matches[3]的url如果被解析器解析成链接，这些需要反解析回来 $matches[3] = preg_replace("/(.*?)<\/a>/",'$1',$matches[3]); $attr = htmlspecialchars_decode($matches[3]);//还原转义前的参数列表 $attrs = self::shortcode_parse_atts($attr);//获取短代码的参数 //这里需要对id做一个判断，避免空值出现错误 $cid = @$attrs["cid"]; $url = @$attrs['url']; $cover = @$attrs['cover'];//封面 $targetTitle = "";//标题 $targetUrl = "";//链接 $targetSummary ="";//简介文字 $targetImgSrc = "";//封面图片地址 if (!empty($cid)){ $db = Typecho_Db::get(); $prefix = $db->getPrefix(); $posts = $db->fetchAll($db ->select()->from($prefix.'contents') ->orWhere('cid = ?',$cid) ->where('type = ? AND status = ? AND password IS NULL', 'post', 'publish')); //这里需要对id正确性进行一个判断，避免查找文章失败 if (count($posts) == 0){ $targetTitle = "文章不存在，或文章是加密、私密文章"; }else{ $result = Typecho_Widget::widget('Widget_Abstract_Contents')->push($posts[0]); if ($cover == ""){ $thumbArray = $db->fetchAll($db ->select()->from($prefix.'fields') ->orWhere('cid = ?',$cid) ->where('name = ? ', 'thumb')); $targetImgSrc = Content:: whenSwitchHeaderImgSrc(0,2,null,$result['text'],@$thumbArray[0]['str_value'],1); // * 这里！ 小小的“,1”作用极大 }else{ $targetImgSrc = $cover; } $targetSummary = Content::excerpt(Markdown::convert($result['text']),60); $targetTitle = $result['title']; $targetUrl = $result['permalink']; } }else if (empty($cid) && $url !=""){ $targetUrl = $url; $targetSummary = @$attrs['intro']; $targetTitle = @$attrs['title']; $targetImgSrc = $cover; }else{ $targetTitle = "文章不存在，请检查文章CID"; } $imageHtml = ""; $noImageCss = ""; if (trim($targetImgSrc) != ""){ $imageHtml = ' '; }else{ $noImageCss = 'style="margin-left: 10px;"'; } return <<<EOF {$imageHtml} {$targetTitle} {$targetSummary} EOF; }至此，修复工作全部完成。最终效果
主题现已更新至handsome 5.2.0。于是鸽子久违地需要动工了。以上就是这个项目更新的背景信息。5.2.0版本的bug的确存在，目前我能发现的就是阅读模式下几个显示和输出上的问题了，下面将问题清单列出以便后续查阅。[collapse status="false" title="目前能够发现的bug"]阅读模式下文章发布日期存在格式问题；阅读模式下本应显示文章发布日期处显示了当前日期；阅读模式内容中将自动替换a标签target="_blank"。（特性）[/collapse]好的，下面进入正题。[post cid="28" /]前情参见↑目前已完成更新，由于主题更新了阅读模式，导致原本的黑幕在阅读模式中无法渲染，于是稍作变更适配了一下；[heimu]同时更新了控制台彩蛋~pc端f12即可体验；[/heimu]终于优化了展现效果，现在给黑幕加上了渐变（基于transition实现）。关于[heimu]控制台彩蛋[/heimu]~
优化侧栏显示：实现更加美观的分类展示
[scode type="yellow"]提示：由于主题最新版本与撰写本文参考的版本存在差异，请预先了解本文更新指导部分。[/scode]引言本博客使用的handsome主题十分美观，遗憾的是其默认情况下对多级分类的显示并不那么友好，优化的想法也是由来已久，昨日终于克服惰性写了简单实现。先看看修改之前的样式：简洁，但多重分类下展现并不是那么好，多重分类中父级分类显得很朴素，在侧栏中比重也相对小了，但我希望分类能够占据侧栏中的优势地位，而且加上图标也不失为一个Great idea，于是就进行简单修改了。修改之后：代码实现对于小技巧、小功能的毫无保留，有助于不成熟技术的发展和微小功能的扩大化，进而不断提升自我；对低级操作的故步自封只能导致开发的停滞。 ——我自己以下讨论全部以handsome模板根目录为当前目录，Typecho环境下为usr/themes/handsome/，特此说明。分析aside.php文件位置：component/aside.php功能：侧栏布局显示结合开发人员工具定位到相关输出位置（第101行至第186行）： <?php if (!$hideHomeItem): ?> <a href="<?php $this->options->rootUrl(); ?>/" class="auto"> <?php _me("首页") ?> <?php endif; ?> <?php echo @$asideItemsOutput ?> <?php if (@!in_array('component',$this->options->asideSetting)): ?> <?php _me("组成") ?> <?php _me("分类") ?> <?php _me("分类") ?> <?php $this->widget('Widget_Metas_Category_List')->to($categorys); echo Content::returnCategories($categorys) ?> <?php _me("页面") ?> <a data-no-instant> <?php _me("页面") ?> <?php $this->widget('Widget_Contents_Page_List')->to($pages); ?> <?php while($pages->next()): ?> <?php if ($pages->fields->navbar == "hide") continue; ?> <a href="<?php $pages->permalink(); ?>"><?php $pages->title(); ?> <?php endwhile; ?> <?php _me("友链") ?> <a data-no-instant> <?php _me("友链") ?> <?php $mypattern1 = " {name} "; Links_Plugin::output($mypattern1, 0, "ten");?> <?php endif; ?>在这里，你可以了解侧栏的基本组成方式，以及其样式的写法。我们可以暂且记住 生成分割线等特性，准备之后的修改使用。不难发现分类结构的输出由Content::returnCategories($categorys)方法生成，不同主题输出分类结构不一定相同，所以它不会是Typecho自带方法，猜测为libs/Content.php所实现，不出意料。改写returnCategories方法文件位置：libs/Content.php功能：使用PHP方法输出内容第2262行至第2301行： /** * @param $categories * @return string */ public static function returnCategories($categories){ $html = ""; $options = mget(); while($categories->next()){ if ($categories->levels === 0){//父亲分类 $children = $categories->getAllChildren($categories->mid);//获取当前父分类所有子分类 //print_r($children); //var_dump(empty($children)); if (!empty($children)){//子分类不为空 $html .= ' permalink.'"> '.$categories->name.''; //循环输出子分类 $childCategoryHtml = ' '; //有子分类判断是否输出父分类 if (!in_array('noShowParentCategory',$options->featuresetup)){ $childCategoryHtml .= ' permalink.'">'.$categories->count.''.$categories->name.' '; } foreach ($children as $mid){ $child = $categories->getCategory($mid); $childCategoryHtml .= ' '.$child['count'].''.$child['name'].' '; } $childCategoryHtml .= ' '; $html .= $childCategoryHtml; $html .= " "; }else{//没有子分类 $html .= ' permalink.'">'.$categories->count.''.$categories->name.' '; } } } return $html; }[scode type="lblue"]目标：将父级分类参与侧栏构成修改为一级列表，同时加上自定义图标。[/scode]作者的逻辑比较清晰，Modify起来可以省很多事。先捡软柿子捏，首先，第2281行删除其child-nav样式，将子项目显示等级提高（否则左侧会有不协调的空白）；然后，参考aside.php中了解到的样式写法，检查该方法的返回数据。非常幸运，这些数据已经可以直接作为一级侧栏列表项使用了，并不需要结构上的修改。下面就是比较难以描述的自定义图标实现了。我的思路是定义一个$icons索引数组，将分类的slug（英文名称）作为索引，结合本博客实际情况，如下：$icons = array( //$slug => 'icon', 'algorithm-oi' => 'iconfont icon-kinds', 'project-share' => 'fa fa-tasks', 'acgn' => 'fa fa-gift', 'words' => 'fa fa-heartbeat', 'files' => 'fa fa-folder', );[scode type="green"]方便起见，我已经手动引入Font Awesome图标库。[/scode]写好了图标映射表，下一步就是将它插入到返回的分类数据中去。图标我们一般使用来实现。插入的过程不好描述，贴出代码，请自行领悟： /** * @param $categories * @return string */ public static function returnCategories($categories){ $html = ""; $options = mget(); $icons = array( //$slug => 'icon', 'algorithm-oi' => 'iconfont icon-kinds', 'project-share' => 'fa fa-tasks', 'acgn' => 'fa fa-gift', 'words' => 'fa fa-heartbeat', 'files' => 'fa fa-folder', ); while($categories->next()){ if ($categories->levels === 0){//父亲分类 $children = $categories->getAllChildren($categories->mid);//获取当前父分类所有子分类 //print_r($children); //var_dump(empty($children)); if (!empty($children)){//子分类不为空 $html .= ' permalink.'"> slug].'">'.$categories->name.'';//* 这里 //循环输出子分类 $childCategoryHtml = ' '.$categories->name.' '; //有子分类判断是否输出父分类 if (!in_array('noShowParentCategory',$options->featuresetup)){ $childCategoryHtml .= ' permalink.'">'.$categories->count.'slug].'">'.$categories->name.' ';//* 这里 } foreach ($children as $mid){ $child = $categories->getCategory($mid); $childCategoryHtml .= ' '.$child['count'].''.$child['name'].' ';//* 这里 } $childCategoryHtml .= ' '; $html .= $childCategoryHtml; $html .= " "; }else{//没有子分类 $html .= ' permalink.'">'.$categories->count.'slug].'">'.$categories->name.' ';//* 这里 } } } return $html; }上述代码中插入图标元素的位置均已使用//* 这里标示。[scode type="yellow"]如果你希望实现类似效果，请不要直接复制粘贴，希望在理解其思想后进行自定义修改，再进行部署。欢迎在评论区和我交流相关问题~[/scode]至此，关于难啃的硬骨头Content.php中returnCategories方法的修改已经全部完成。修改aside.php文件位置及功能参考“分析aside.php”中叙述。原始代码不再贴出，稍作讲解后将贴出修改后代码。刚刚已经提到，returnCategories方法的返回值现在已经可以直接作为一级列表项使用，于是删除外层套着的 元素，并在适当位置添加分割线（还记得吗，分析时已经看到的）即可。对于页面项目的修改同理可以完成，不再赘述。这里介绍一个小技巧，在后台编辑页面的“自定义字段”中，新增一条名为navbar的字段，将其值设置为hide，即可在侧栏中隐藏该页面。[scode type="green"]本博客视情况隐藏了“友情链接”页面，自定义在了一级列表项“友情链接”下的“总览”中。[/scode] <?php if (!$hideHomeItem): ?> <a href="<?php $this->options->rootUrl(); ?>/" class="auto"> <?php _me("首页") ?> <?php endif; ?> <?php echo @$asideItemsOutput ?> <?php if (@!in_array('component',$this->options->asideSetting)): ?> <?php _me("分类") ?> <?php $this->widget('Widget_Metas_Category_List')->to($categorys); echo Content::returnCategories($categorys) ?> <?php _me("页面") ?> <?php $icons = array( //$slug => 'icon', 'articles' => 'fa fa-archive', 'board' => 'fa fa-commenting', 'cross' => 'fa fa-coffee', ); ?> <?php $this->widget('Widget_Contents_Page_List')->to($pages); ?> <?php while($pages->next()): ?> <?php if ($pages->fields->navbar == "hide") continue; ?> <a href="<?php $pages->permalink(); ?>"><i class="<?php _me($icons[$pages->slug]); ?>"><?php $pages->title(); ?> <?php endwhile; ?> <?php _me("友情链接") ?> <a data-no-instant> <?php _me("友情链接") ?> 总览 <?php $mypattern1 = " {name} "; Links_Plugin::output($mypattern1, 0, "ten");?> <?php endif; ?>由于我比较懒，在输出总览时直接输出了永久链接，而没有动态获取，需要注意。部署后即可实现目标效果。更新指导Update on 2019-07-22已更新至handsome 5.2.0，上述内容基本适用，但行号稍有偏差，请联系具体代码上下文确定新版本文件应修改的位置。Update on 2020-05-10对于 handsome 6.x 以及后续不再提供“不输出父分类”选项的版本，使用以下代码： /** * @param $categories * @return string */ public static function returnCategories($categories){ $html = ""; $options = mget(); $icons = array( //$slug => 'icon', 'algorithm-oi' => 'iconfont icon-kinds', 'project-share' => 'fa fa-tasks', 'acgn' => 'fa fa-gift', 'words' => 'fa fa-heartbeat', 'files' => 'fa fa-folder', ); while($categories->next()){ if ($categories->levels === 0){//父亲分类 $children = $categories->getAllChildren($categories->mid);//获取当前父分类所有子分类 //print_r($children); //var_dump(empty($children)); if (!empty($children)){//子分类不为空 $html .= ' permalink.'"> slug].'">'.$categories->name.'';//* 这里 //循环输出子分类 $childCategoryHtml = ' '.$categories->name.' '; foreach ($children as $mid){ $child = $categories->getCategory($mid); $childCategoryHtml .= ' '.$child['count'].''.$child['name'].' ';//* 这里 } $childCategoryHtml .= ' '; $html .= $childCategoryHtml; $html .= " "; }else{//没有子分类 $html .= ' permalink.'">'.$categories->count.'slug].'">'.$categories->name.' ';//* 这里 } } } return $html; }
根据我之前随手填的博客开始日期来看，今天（2019-02-09）应该是这个博客启动满百日的日子，应该祭一下，暂且写作百日祭好了。[heimu]反正我的博客关关开开折腾估计能有三倍以上的时间XD[/heimu][heimu]“XD”的意思也许应该顺时针旋转90°来理解，大概等价于（笑），但其表情更加“狰狞”，抒发感情效果更好。[/heimu]目前使用handsome主题，因个人定制和优化需要，启动iCore项目，意为“我的核心”，同时也是“Improved Core”（进阶版核心XD）。本身网络上对于handsome主题的各类玩花样式脚本也是数不胜数，但出于我对于这个blog的定位（技术栈60%~70%，二次元40%~30%[heimu]，瞎整175%，蒟蒻日志325%[/heimu]，合计100%），我并不太追求花里胡哨还能把浏览器卡出天际的牛逼操作（实则咱也搞不来惹），于是就慢慢形成自己的定制core好了XD。经过一下午的随便乱搞，已经支持下面两个功能（真少XD）：左上角头像hover特效（PC端可以试试，手机端就算了）文章内[heimu][/heimu]自定义标记解析下面详细说说第二条。这玩意叫“黑幕”，萌娘百科的惯用操作，但是被我砍了不少，只支持黑幕（连续的）文本，如果中途来个a标记它就不解析，而且孤立的[heimu]标记未闭合它也是不管的。本身这东西写成php比较好处理，为什么要写js呢，答案是减轻服务器压力[heimu]（这能减轻个锤子XD）[/heimu]，把这种O(N)的处理都扔你们客户端最舒服不过XD（等死）。它主要负责娱乐功能，所以搞成黑幕就是为了吸引你把鼠标放上去（手机端仍可以通过点击查看），像这个：[heimu]这里什么都没有[/heimu]。既然项目开了，那么不鸽着真是对不起它，日后也不会慢慢维护吧……