不是教程,未来的开发主题时参考,记录 Cattila 主题开发初期的过程及掉进的各种坑,愿引以为戒
起因
许是查过太多网页开发相关问题,Quora 给我推了篇文章,建博客平台对比。早就看过太多类似的文章,但出于好奇还是点了进去。
和大多数博客平台对比文章没什么不同,介绍了 Wordpress,Drupal,Wix 等等,但另一个名字引起了我的注意。
Ghost,基于 Node.js
的静态博客程序,主题简洁,不吃服务器配置因此加载非常快。之前对 Ghost 也有所耳闻,但并未深度了解过。如今英文博客 Nightingale 那几乎十秒的后台加载速度让人颇为头疼,便生了换平台的念头,这 Ghost 作为一个功能略少但适合文字发表的简单系统正和我意。
看了看 Demo 网站,不得不说,Ghost 主题的不少设计与 Medium 博客平台在设计上都有同曲同工异曲同工之妙,简直狠狠戳在我的审美上,心里默念英文博客非 Ghost 不用。
第一步,生成一个 Node.js
的 App。在 cPanel 找到 Node.js
部分,却发现没有命令行无法启动 Node.js
App,忙去问老薛的技术支持,得到个心灰意冷的答案…
虚拟主机的配置跑不起 Ghost。
为了几个个人博客和学校网站租个 VPS?孩子租不起!但这 Ghost 主题太香了舍不得放弃,那就移植到 Z-Blog 上吧。后台编辑页面有些简陋的 Ueditor 虽不如 Ghost 的 WYSIWYG 编辑器,但好歹能有个惊艳的前台。
这并不是一时兴起,移植主题到 Z-Blog 的主要原因有以下几点:
-
这是个我熟悉的博客系统
-
Z-Blog 程序算是超轻量级的,前台快,后台甚至比前台更快,用着舒心
-
基于 PHP,便于折腾
-
目前应用中心还没有见过适合英文写作发表内容的主题(当初也是因这个原因,英文博客选择了 WordPress),或许这个独一无二的移植主题能吸引写英文博客的个人博主
Cattila 主题的 Ghost -> Z-Blog 移植 + 踩坑路便这样开始了,愿小白一路顺利。
对了,重戳我审美的主题大名 Attila,移植主题既然是我 cc 子的亲生闺女,就叫你 Cattila 吧。
主题结构
/path/zb_users/theme/demoTheme │ screenshot.png [必需]缩略图 300*240像素, 横向; │ theme.xml [必需]自述文件; │ main.php [可选]应用内置管理页,在创建主题时填写才会生成; │ include.php [可选]应用嵌入页,在创建主题时填写才会生成; │ ├─compile [废弃]旧版 z-blog 用于放置模板编译文件,可直接删除; ├─include [可选]主题自带「文件模块」,使用{module:abc}「嵌入调用」该目录下的abc.php文件; ├─script [可选]JS目录; ├─style [必需]样式目录, 内存样式表及所需图片; │ style.css [必需]不限于这个文件名,一套主题也可以拥有多个样式(各自独立使用); │ ├─css [可选]并不会自动创建,用于不应该放在style文件夹中的样式内容; └─template 用于存放模板文件;建议优先确立以下 6 个模板文件及内容; index.php 首页及列表页 single.php 文章页(单页) search.php 搜索结果页,不存在时使用index.php header.php 公共头部文件 footer.php 公共尾部文件 404.php 建议设置
开始的时候以为需要跟着这个格式自己从 0 开始写,后来发现不知什么原因,自己写的插件即使上传到服务器也无法开启,还弹出了一些应用 ID 错误以及数据库 bug,兜兜转转大半天发现,Z-Blog 程序的应用中心有这玩意:
居然是填写完相关信息后全自动生成,孩子抱头痛哭……
估算阅读时长
阅读时长是基于文章中文字、图片与代码的数量来计算的,平均而言一个人的阅读速度是每分钟 275 个单词。我 JS 方面菜菜的,于是从 GitHub 挑了个估算阅读时常的工具 Reading Time 直接用了,在页面加入 reading-time.js
即可,最终结果如下:
背景图模板与自定义背景图
先在 include.php
中挂载接口,然后定义函数即可。设置后发现编辑页面并没有出现提示用户输入背景图地址与上传文件的框框,排查了老半天,近乎抓狂之时意识到是函数的命名问题。在 Z-Blog 官方 Doc 里有看到函数的命名规范,本以为是为了阅读与 debug 上的便利,未曾想过会影响实际上功能的使用。
//挂载接口 Add_Filter_Plugin('Filter_Plugin_Edit_Response5','cattila_article_AddCover'); //定义函数 function cattila_article_AddCover(){ global $zbp,$article; echo "<div id='alias' class='editmod'><label for='meta_cover_url' class='editinputname'>大背景图:</label><input class='ue-url' style='width: 55%' placeholder='输入图片URL或者上传图片(需选择使用大背景图文章模板)' type='text' name='meta_cover_url' value='".htmlspecialchars($article->Metas->cover_url)."'/><input class='ue-image-upload' type='button' value='上传图片'/></div>"; if ($zbp->CheckPlugin('UEditor')) { echo "<script type=\"text/javascript\" src=\"{$zbp->host}zb_users/theme/cattila/plugin/lib.upload.js\"></script>"; } }
最终效果:
后期可能会加上推荐图片供用户选择,提供已 CDN 加速过的图片,比较如此之大的背景图加载过慢能毁掉整个网页的体验。
后台美化
一直觉得,主题的作用是美化前端样式,而插件既可以起到美化作用,也可以实现各种功能,并且能作用于前后端,极为实用。现在却发现,原来主题似乎也能做到这些。看着 1.6 版本 Zblog 有些不太美观后台设计,便起了主题自带后台美化的想法。既然是借鉴 Ghost 主题,后台也参考 Ghost 主题吧。
吃了上次命名问题的亏,这次先按规范命名函数,挂载个接口:
//将cattila_backstage_Css函数放在 Add_Filter_Plugin('Filter_Plugin_Admin_Header','cattila_backstage_Css'); Add_Filter_Plugin('Filter_Plugin_Login_Header','cattila_backstage_Css'); Add_Filter_Plugin('Filter_Plugin_Other_Header','cattila_backstage_Css');
然后用函数在页面中引用美化页面的 css:
function cattila_backstage_Css() { global $zbp; echo '<link rel="stylesheet" type="text/css" href="'. $zbp->host .'zb_users/theme/cattila/style/backstage.css"/>' . "\r\n"; }
注意,这里 CSS 文件的路径中的域名部分必须用 $zbp->host
而不是模板文件了常用的 <?php echo $host; ?>
或 {$host}
,具体原因小白也不太懂(摊手),毕竟 $host
变量不知道是哪里赋值的,可能这个文件里没有吧。
登录页美化效果:
后台美化效果:
自定义颜色
Cattila 是个简约的主题,和 Medium 一样,改变页面强调色不但更有个性,且怎么变都依旧简洁美观。这么一来,后台设置中自定义颜色功能必须得有。小白按 ghost 后台设置里的样子画了个颜色选择器:
然后问题马上来了,输入框里的颜色代码和左侧颜色选择器里的对不上。
这本在我眼里不是个难题,如此简单的功能,不是 HTML 颜色选择器自带的么?
答案是,不是,默认颜色选择器真就和它的名字一样,只负责选择。至于选择了什么,是不会显示在旁边的。
<input placeholder="<?php echo $zbp->Config('cattila')->accent_color ?>" id="accent-input" oninput="setColor('accent-input', 'accent-picker')" name="accent_color" value="<?php echo $zbp->Config('cattila')->accent_color;?>" autocorrect="off" maxlength="6" class="gh-input" type="text"> <!--accent-input是右边的框框,accent-picker是左边的选色器--> <script> function setColor(fromId, toId) { var fromObject = document.getElementById(fromId).value; var toObject = document.getElementById(toId); toObject.value = '#'.concat(fromObject); } </script>
问题不大,用 JS 把选择到的颜色放入框里就好。但这时,我还没有意识到问题的严重性。
直到写好 JS 自恋地选来选去,突然再次发现问题——选择器能控制右侧 Hex 颜色代码,但输入 Hex 不能改变选择器的颜色!甚至乎在输入后,选择器就不能改变 Hex 了!头疼子。
于是乎就把 JS 里的函数改成了双向的,给函数多加了一个参数决定其方向(其实也可以变成两个函数),1 为选色器到输入框,0 为输入框到选色器。
... oninput="setColor('accent-picker', 'accent-input', 1)" ... <script> function setColor(fromId, toId, toInput = 0) { var fromObject = document.getElementById(fromId).value; if(toInput){ fromObject = fromObject.substring(1); var toObject = document.getElementById(toId); toObject.setAttribute('value', fromObject); }else{ if(validate(fromObject, fromId)){ //为了便利用户无需输入#号,如果输入了#则validate结果为false,不执行以下而是将框框变成红色的(参考ghost设计) //从选色器到输入框因此需要减掉#,从输入框到选色器要加上# var toObject = document.getElementById(toId); toObject.setAttribute('value', '#'.concat(fromObject)); } } } </script>
这里踩了一个巨大的坑,第三行中获取选色器或输入框的 value
用的是这个:
var fromObject = document.getElementById(fromId).value;
而在后面使用的则是 getAttribute
与 setAttribute
,如:
toObject.setAttribute('value', fromObject);
这就直接导致了一个严重的问题:第一次在选色器上换颜色时,输入框里的文字会改变,但一旦在输入框中进行修改,左侧的选色器便不再起作用。同样,第一次在输入框中输入可以改变选色器颜色,但在换了选色器的颜色,不但输入框的文字不会因此改变,输入框也不再能控制选色器。
这些难道不都是获取一个 object 的 value 并改变它么?为什么有时候有作用,有时候却没有?
感谢这篇文章,完全解惑:
Difference between Element.value and Element.getAttribute(“value”)
getAttribute
& setAttribute
和 object.value
还真的不一样。
这一系列问题的产生归根结底是由于 properties 和 attributes 之间的混淆(这俩极为不同,但中文翻译都是属性,这里就用英文替代,知乎上还有一专门的帖子讨论这一问题 😂)。
从一些回答总结:
-
HTMLElements
都是 JavaScript 对象,因此都有 properties -
object.value="red"
定义了 object 的 property,<object value="red"/>
或object.setAttribute("value", "red")
定义了 object 的 attribute
-
Properties 描述的是一个对象的基本特征
-
Attributes 是属于 HTML 的,作用是描述对象的额外信息。只要你想,加什么都行
-
当 HTML 被解析为 DOM 节点时,对应该节点的标准 attribute 会被转换为相应 property,而不标准 attributes 则不会
-
标准 attribute 或 property 被改变时,它对应的 property 或 attribute 同时也会被改变(如果
value
也是如此,那问题则不会发生) -
value
非常特殊,它 attribute 和 property 之间的同步是单向的 —— 只允许 attribute → property,反之则不可以
<input> <script> let input = document.querySelector('input'); // attribute => property input.setAttribute('value', 'text'); alert(input.value); // text // NOT property => attribute input.value = 'newValue'; alert(input.getAttribute('value')); // text (没变!) </script>
.value
(property)的变化无法改变作为 attribute 的 value
,另一个名为 .defaultValue
,也就默认值的属性却可以。
神奇的是,当一个 input
元素没有被用户修改过时,.defaultValue
等同于 .value
,两者是同步的。但一旦用户对 input
做出了改动,这种同步便终止了。
回到刚刚的双向 JavaScript 代码,在函数第一行,我们获取了选色器或输入框的 value
property,即最开始的一个被用户改变的值。
接着我们进行 .setAttribute
。这时,因为 .defaultValue
永远与 value
的 attribute 同步,改变 attribute 中的 value
等于设置 .defaultValue
的 property。又因为用户只碰了选色器与输入框的其中一个,没有对另一个做出改动,.defaultValue
与 .value
两个 property 的同步仍在继续,可以认为,attribute value
= .defaultValue
= .value
。
一切正常,直到用户改变了选色器与输入框中的另外一个(刚才没有改动的,例如之前在改选色器,现在在输入框中输入了 Hex 代码)。这时,.defaultValue
与 .value
的同步终止。第一行代码中用的 .value
仍旧可以实时获得用户正在改变的值,但在后面的 setAttribute
中,用户控制的是作为 attribute 的 value
,间接改变作为 property 的 .defaultValue
,但这已经不能改变 .value
了,因此和应被控制的对象长什么样子没有一点关系了。
因此,既然需要被控制的只是对象的 property .value
,何必要通过改变 attribute 这种间接的方式呢?不如全部直接控制 .value
:
function setColor(fromId, toId, toInput = 0) { var fromObject = document.getElementById(fromId).value; //直接获取 value property if(toInput){ fromObject = fromObject.substring(1); var toObject = document.getElementById(toId); toObject.value = fromObject; //直接改变 value property }else{ if(validate(fromObject, fromId)){ var toObject = document.getElementById(toId); toObject.value = '#'.concat(fromObject); //直接改变 value property } } }
问题解决!
最后吐槽
本就被 Ghost 平台的 UI 设计与其主题的简约美感吸引,在扒模板的过程中更深地了解了 Ghost 与 Attila 主题,更是被深深震撼。什么时候我大 Z-Blog 能有那样的设计(绝不是嫌弃 Z-Blog 喷子勿喷,只是个人吐槽),就从这代码块开始吧,害,对 Ghost 用户有些小嫉妒呢。
Z-Blog 设计:
Ghost 设计(啊呲溜呲溜太香了诶嘿嘿嘿):
化悲愤为动力!努力移植嘻嘻嘻嘻💪
评论列表
咕了一个月,发出来了~