从零开始创建一个hexo主题

前言

本文将从零开始记录一个 hexo 主题的开发过程,目前使用过的主题从设计感或者功能上或多或少都有问题,因此不如借此机会尝试一下 hexo 主题的编写。我目前已经完成了Chic 主题的编写,在写下这篇文章时已经提交 pull request。本文将记录编写主题过程中的各个环节,欢迎阅读本文的同学自己动手创建属于自己的专属主题。

2019.6.25 更新:主题已经发布到 hexo theme list 啦!点击查看

编写主题前需要了解的一些预备知识:

Chic 主题使用的是 Stylus + Ejs,本文将在这两个库基础上进行编写。

构建目录结构

根据官方文档的说法,在theme文件夹中创建任意名称(其实是主题名称)的文件夹,即为主题文件夹,一般包含以下目录:

.
├── _config.yml # 主题配置文件
├── languages # 国际化语言
├── layout # 页面模板
├── scripts # 脚本
└── source # 资源文件夹

具体解释在官方文档中有说明。主题 | Hexo

这部分也可以使用generator-hexo-theme主题生成器生成一个基本目录结构。

这里特别需要注意的是:

  • scripts 脚本文件夹。在启动时,Hexo 会载入此文件夹内的 JavaScript 文件。
    script 是指 hexo 的插件,在 hexo 命令运行时加载,页面插件请加入 source/js 下
  • source 资源文件夹,除了模板以外的 Asset,例如 CSS、JavaScript 文件等,都应该放在这个文件夹中。文件或文件夹开头名称为 _(下划线)或隐藏的文件会被忽略。如果文件可以被渲染的话,会经过解析然后储存到 public 文件夹,否则会直接拷贝到 public 文件夹。

原理

hexo 使用的 ejs 模板是组件化开发的思路,使静态资源 markdown 文件根据 source 目录中的页面模板结构结合 config 中变量动态渲染出静态 html 页面。

这里如果有组件化前端开发经验的话这里会比较好理解,简单的说就是 hexo 开发不是点击链接切换一个页面,而是刷新并替换局部的模板,我们根据页面元素划分出一些页面部件进行替换即可。

页面结构 是以 ejs 语法为基础声明,开发时使用hexo s命令,hexo 会自动渲染 ejs 文件并支持热更新。此处注意调用的主题 config 是支持热更新的,但全局 config 不支持热更新,更改全局 config 需要重启 hexo 服务器。

页面样式 推荐使用 CSS 预处理器,当然原生 CSS 也可以,这点没有什么区别,但 css 预处理器支持根据组件划分样式表,继承元素样式会节省很多重复代码。hexo 默认支持 stylus 预处理器,sass、less 等需要额外添加依赖。同样开发过程中 hexo 的样式也支持热更新。

页面设计

想自己写主题的理由就是 hexo 中主题大部分没有封面,个人觉得博客作为个人主页需要一个封面来引入内容。有封面的主题如我用过的 typing、fexo 都很不错,typing 主题的 markdown 渲染样式很适合中文但不太适合代码;fexo 有点过于简单。而基于 go 语言的 hugo 生成器的社区主题的设计感就好很多。本文参考 hugo 的部分主题设计编写一个 hexo 主题,使其达到简洁同时典雅大方,阅读体验良好。

Chic 主题完成了这样的任务,我制作了封面作为 home page,有头像、昵称、简介、外部链接等,同时参考 hugo 部分主题设计了两套样式,一键切换 light/dark 模式。

文章阅读样式需要考虑易读性、清晰性。我结合读过的版式设计的书籍给出的设计方案是这样:

  • 文章页面以一种淡色为主题色,结合大部分浏览器黑底白字营造出双色阅读的体验;
  • 常用四级标题分别加入前缀区分结构;
  • 行高设为字符大小 2 倍可以使行间距更加清晰;
  • 代码高亮方案可更换;
  • 中英文混合排版,本来有typo.css这个中英文混编样式库,但它的字体太纤细了感觉还是不舒服,后更改字体方案,中文字符以 Microsoft Jhenghei, Lantinghei SC (微软正黑、方正兰亭)为主要方案,针对苹果显示器使用苹方字体;西文字体选用非衬线体,这样混合排版不会突兀也不会交错,长时间阅读会产生沉浸感且不累眼睛。

origin.png

原始设计稿

production.png

完成稿

article.png

文章样式

布局

本主题采用三栏布局,即 header、body、footer 组成,三部分组件分别开发并以局部模板的形式塞到页面中。

这里会用到的重要函数:

  • partial()函数用于导入一个模块,有点类似 ES6 的 import 函数。就是将一个页面模板导入进来。
    partial 函数的第二个参数为 Object 类型,用于父子组件的值传递和参数格式化,具体参考实际代码和文档。(文档中并没有示例代码,这里推荐看已有的主题代码实例,比如 landscape)
  • <%- %>为 Ejs 一个标记,输出非转义的数据到模板。具体参考 ejs 文档。

页面框架

Hexo 博客首先根据themes/mytheme/layout/layout.ejs进行最初的渲染,后续所有结构都是在这个文件中替换形成的。示例代码如下:

layout/layout.ejs

<!DOCTYPE html>
<html lang="<%= theme.language %>">
  <head>
    <!--  head  -->
    <%- partial('_partial/head') %>
  </head>
  <body>
    <div class="container">
      <!--  header  -->
      <%- partial('_partial/header') %>
      <!--  index.ejs  -->
      <%- body %>
      <!--  footer  -->
      <%- partial('_partial/footer') %>
    </div>
  </body>
</html>

然后是局部模板替换,themes/mytheme/layout/_partial/head.ejs为 head 中需要声明的 html 页面头等信息

layout/_partial/head.ejs

<meta charset="UTF-8" />
<meta
  name="viewport"
  content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/>
<meta http-equiv="X-UA-Compatible" content="ie=edge" />

themes/mytheme/layout/_partial/header.ejs为 header 替换模板

layout/_partial/header.ejs

<header id="header" class="header">header</header>

themes/mytheme/layout/_partial/footer.ejs为 footer 替换模板

layout/_partial/footer.ejs

<footer id="footer" class="footer">footer</footer>

Content

body 模板自动加载同目录下的 index.ejs,该模板将作为首页的页面模板。当网页跳转到 category、tag 页面会替换载入同级目录下的同名 ejs 文件形成网页。官方文档说明点这里

themes/mytheme/layout/index.ejs示例代码

layout/index.ejs

<p>body!</p>

此时运行hexo s效果:

效果图

到这里页面布局就完成了。

配置文件

完成了页面框架结构的创建,就可以针对每个部分进行单独开发了,为了使页面中元素可以定制化,我们采用配置文件,也就是将需要引入的部分变量化,灵活的控制页面显示元素。这个文件就是 theme 目录下的_config.yml文件。

举例来说这个变量怎么使用:

themes/Chic/layout/_partial/header.ejs

<nav class="navbar">
  <div class="container">
    <div class="navbar-header header-logo">
      <a href="<%- config.root %>"><%= theme.navname %></a>
    </div>
    <div class="menu navbar-right">
      <% for (var i in theme.nav){ %>
      <a class="menu-item" href="<%- url_for(theme.nav[i]) %>"><%= i %></a>
      <% } %>
      <input id="switch_default" type="checkbox" class="switch_default" />
      <label for="switch_default" class="toggleBtn"></label>
    </div>
  </div>
</nav>

这是 header 组件,theme.nav是一个 list 结构,对这个变量枚举即可。

也就是说访问变量时,theme.obj是访问主题变量,page.obj是页面变量,site.obj为全局变量。

变量参数表可以看hexo 文档 - 变量

样式

基础样式

页面写到这里已经有了结构层,现在需要加入样式层。

themes\[themeName]\source\css中编写 stylus 文件,这里写原生 css 也是可以的,但为了编写的简便性和便于组织代码,推荐使用预处理器完成。

hexo 系统自带 stylus 预编译器,编译后会在public\css文件夹内生成静态 css。因此在 head 组件内引入这个样式资源。

<%# css list %>

<% if (theme.stylesheets !== undefined && theme.stylesheets.length > 0) { %>
    <!-- stylesheets list from _config.yml -->
    <% theme.stylesheets.forEach(url => { %>
    <link rel="stylesheet" href="<%- url_for(url) %>">
    <% }); %>
<% } %>

我的写法是将要包含的 css 写在 config 中根据这个列表引入页面。

themes\[themeName]\source\css中建立 style.styl 文件,这个文件作为所有 stylus 的入口,最后编译完成会只有一个 style.css 文件。

stylus 的语法、变量、辅助函数等在这里不做过多介绍,直接看文档即可。stylus 支持 css 与 styl 并存,可以将 css 引入 styl 中来,因此也可以使用第三方 css 库 如 bootstrap 等。

此处注意,style.styl 导入的 css 是有前后顺序的,后面的样式在同样选择器下会覆盖前面的,因此基础样式如 normalize、cssReset 等需要先引入。

// style.styl文件
// variable & init style
@import "variable.styl"
@import "normalize.styl"
// font
@import "font.styl"
// structure style
@import "base.styl"
@import "layout.styl"


// toggle button
@import "_lib/looper.css"
// icon
@import "../fonts/iconfont/iconfont.css"
// partial
@import "_partial/header.styl"
@import "_partial/footer.styl"
@import "_partial/paginator.styl"
// page
@import "_page/profile.styl"
@import "_page/archive.styl"
@import "_page/category.styl"
@import "_page/tag.styl"
@import "_page/page.styl"
// page post
@import "_page/_post/post_header.styl"
@import "_page/_post/post_content.styl"
@import "_page/_post/post_copyright.styl"
@import "_page/_post/post_code.styl"
@import "_page/_post/post_tags.styl"
@import "_page/_post/post_nav.styl"
@import "_page/_post/post_toc.styl"
//mobile
@import "media.styl"
// highlight
@import "_highlight/solarized-light.styl"
//@import "_highlight/solarized-dark.styl"
//// ==============================
//// Custom style
//// ==============================
//// You can override the variables in variable.styl to customize the style
@import "custom.styl"

文章样式

文章样式为 Hexo 对 Markdown 转换成的 HTML 结构的样式,例如 markdown 标签###会渲染为<h3>

合适的文章样式会更加适合屏幕显示与阅读。这里推荐的方法为看下其他主题中文章样式,对其可取部分进行保留。

个人觉得在这点上 Hugo 主题普遍比 Hexo 主题的阅读体验要好,因此可以参考下 Hugo 主题的样式。

图标

图标推荐的方式是 css 结合 font 字体,个人不推荐雪碧图等图片方案。

推荐的方式是以 i class 引入,相比 Unicode 形式更加语义化且方便维护。

脚本

开发 Chic 主题时遇到了document.body.classList出现 “Cannot read property 'classList' of null 错误。

这个错误很奇怪,因为明明 DOM 上有了 body,为什么会 undefined 呢?

后来明白了是页面加载顺序的原因。

我写的绑定 body 元素的函数为匿名函数,JavaScript 的加载和 html 构建 DOM 树在 V8 中为多线程进行,因此在执行匿名函数时页面 DOM 加载还未完成,这时 body 元素还未显示到文档中,因此 body 空指针。

所以提醒需要写脚本的主题开发者注意下这个问题,初学者可以引入JQuery直接调用$.ready()函数来完成在页面中需要的脚本。

子目录路径

这部分是解决在某个子目录下部署 hexo 的路径问题。

全局配置有这么个参数:

# URL
# If your site is put in a subdirectory, set url as 'http://yoursite.com/child' and root as '/child/'
url: https://yoursite.com
root: /

如果你的主题的外部资源(图片、图标等)是以相对目录形式引入的,例如图标

<!-- 有问题的写法 -->
<link rel="icon" href="<%- theme.favicon %>" />

这种写法在根目录下部署不会有问题,但如果root不是根目录'/'而是一个子目录,你的外部资源就会 404 file not found.也正是这个原因需要用到 hexo 中的辅助函数url_for()补全路径。

<!-- 正确的写法 -->
<link rel="icon" href="<%- url_for(theme.favicon) %>" />

这样就可以在子目录中也能正确的识别资源 url 了。我的预览站就是这样,可以参考https://siricee.github.io/hexo-theme-Chic/

国际化

还记得我们一开始创建的languages文件夹吗?没错,它是用来添加多种语言,用于 i18n 的。站点的语言设置为站点配置文件中的 language。

当该字段为空时,默认使用的是 languages/default.yml 这个文件。那么现在我们来添加这个文件,我们决定主题的默认语言是英文:

Menu:
  Home: Home
  Archives: Archives
  Github: Github

然后就需要更改组件中的变量语言。此处以 header 举例:

<header class="header">
  <div class="blog-title">
    <a href="<%- url_for() %>" class="logo"><%= config.title %></a>
  </div>
  <nav class="navbar">
    <ul class="menu">
      <% for (name in theme.menu) { %>
      <li class="menu-item">
        <a href="<%- url_for(theme.menu[name]) %>" class="menu-item-link"
          ><%- __('Menu.' + name) %></a
        >
      </li>
      <% } %>
    </ul>
  </nav>
</header>

中文语言的配置为

Menu:
  Home: 首页
  Archives: 归档
  Github: 代码托管

然后在全局配置中(config 文件)更改 language 值

language: en

即可达到切换语言的效果。其他语言即为更改该值为languages文件夹中的语言 YAML 文件名。

移动端适配

网站的全端适配主流做法都是媒体查询,缺点就是需要针对不同设备编写很多套 css 文件。

所以推荐的写法为 PC 等大屏使用 flex 弹性布局进行宽度自适应,移动端使用媒体查询根据页面宽度进行切换样式方案。

给出 Chic 主题中的媒体查询三种屏幕尺寸方案做举例:

// themes\Chic\source\css\media.styl

/* mobile phone and smart portable devices */
@media screen and (max-width: 479px) /* iPads (portrait and landscape) ----------- */ @media screen and (max-width: 1023px) .navbar-mobile display none /* Desktops and laptops ----------- */ @media screen and (min-width: 1024px) .navbar-mobile display none;

总结

刚接触 Hexo 主题编写的时候还一头雾水,写完了发现也就这么回事,看懂了渲染过程之后针对页面模板进行结构和样式的编写就简单了许多。Hexo 就是把那些 Markdown 文件按照不同的布局模板,填上对应的数据生成 HTML 页面,复制 source 中的到生成的 public 文件夹中,中间过程会把需要编译的 stylus/less/sass 等文件编译。

本文并没有对页面编写的细节做过多介绍,这部分就按自己的设计思路去编写就可以了。

最后在主题发布之前可以考虑使用 gulp、grunt 等自动化构建工具压缩下代码,使资源的加载速度能提升一些。

感谢阅读,希望对主题编写的你有所帮助,感受开发 hexo 主题的快乐。

参考链接

第三方库

中英文混编排版库 Typo.css - 中文网页重设与排版

样式初始化 Stylus version of normalize.css

代码高亮 highlight.js

Toggle 样式 Looper UI 轻量级响应式前端框架

作品展示效果图 Screely - Generate Beautiful Mockups

参考文章

从零开始制作 Hexo 主题 - Ahonn's Blog

Create an Hexo Theme - Part 1: Index - CodeBlocQ

Hexo 主题开发经验杂谈 - 前端 - 掘金

stylus 入门使用方法 - 小弟调调 - SegmentFault 思否

搭建 Hexo 博客进阶篇---主题自定义(三) - 简书