June 01, 2020

VuePress + TailwindCss + Netlify 重写个人独立博客

#博文 #VuePress

建立自己的个人独立博客已经将近七年时间。从一开始的 WordPress 到更换轻量级的 Ghost 博客,又到 JAMStackVuePressxlbd.me 也见证了我从一名后端开发者转型成为一名前端开发者。

时至2020,我仍然对前端技术有着痴迷的热情和动力驱使我去发现和学习更有趣、更前沿的前端新技术。

新的博客主题折腾了几个月了,才于今天(2020-06-01)正式发布。送给我自己这个大孩子的礼物🎁。拖延的最主要原因可能是我的思维比较跳跃,写着写着发现好多 idea 都可以写成plugin(瞎折腾),于是就先写插件去了。不过还算有所收获,尝试了编写 VuePress插件、TailwindCss 插件和部署 Netlify,都是些新鲜的尝试。

新博客 https://xlbd.me 欢迎访问

# Why VuePress?

Vue 技术栈狂热者的利器

采用 VuePress 跟我使用的技术栈有很大关系了。工作中使用的是 Vue ,写 React 相对较少,如果我比较熟悉 React,那么我可能就使用 Gatsby 了。之前写过的《前端技术栈月刊》就是基于 VuePress 搭建的。

VuePress 1.x 开始,就支持了自定义主题的功能,社区也层出不穷出现了很多 VuePress 主题,使用 VuePress 写博客主题对于熟悉 Vue 技术栈的开发者来说,真的是太舒服了。VuePress 主题开发给开发者提供了很多内置的方法可以调用。VuePress 本身就是一个 Vue 应用,可以使用编写组件的方式开发博客主题,甚至编写 VuePress 插件。

# 从 Ghost 迁移至 VuePress

Ghost 3.x 发布之初,就发布了一篇文章 Working With VuePress ,讲解了如何通过 Ghost Content API 将 Ghost 作为 Headless CMS,VuePress 作为 静态页面生成器,将内容输出为 .md 文件的方法。

教程已经详细讲解,这里就不展开了。

# VuePress 插件

在写博客主题的时候,Post 页面展示的是博文列表,卡片形式。想使用一种能自动生成的图案(pattern)代替没有博客主图的博文卡片,重要的是可以根据不同的 seed 生成和 seed 绑定的唯一图案。于是找到了两种生成 svg pattern 作为背景图片的库:

于是乎造轮子开始

# vuepress-plugin-geopattern

geopattern 是我为 VuePress 写的第一个插件,VuePress Plugin 官方文档上有好四种插件可以去编写,geopattern 其实就是一个全局UI组件。

# vuepress-plugin-hero-pattern

hero-pattern 同样是一个VuePress 全局UI组件,它是基于 Hero Pattern Plain Svg 编写的插件,插件将可复用、可重复的 SVG,通过 mini-svg-data-uri 工具转化为 background-image 所需的 data-uri 实现背景图功能。

# vuepress-plugin-svg-sprite

这是一个尝试失败的插件,我想将 SVG Sprite 功能迁移到 VuePress 上,本地当然是可用的。但是想做成一个成熟的插件遇到了一点点困难,可能我还是没有找到好的办法。

  • 方案一

    我想使用 svg-sprite-loader 插件自动注入 SVG Sprite,并暴露一个全局UI组件 SvgIcon ,但是插件的路径传参数是个问题。

  • 方案二

    不借助 svg-sprite-loader 的情况下,将 svg icon 生成 SVG Sprite,插入到 dom 结构,然而尝试后发现,转换 sprite 的类库使用 svgo 将SVG 优化后,会丢掉一些图形。导致 icon 变得难看,甚至就不显示了。

不过我相信这个插件后续会写好的。👀下面介绍在 VuePress 如何正确使用 SVG Sprite

# VuePress 如何使用 SVG Sprite?

VuePress 编写UI插件,还算比较好实现的,主要概念在 Option Api → enhanceAppFiles,还允许你像使用熟悉的 vue-cli 3/4 脚手架配置文件 vue.config.js 的方式去修改 webpack 配置,Option Api -> chainWebpack,有了这两点,就可以将 SVG Sprite 功能迁移过来。

# 配置 webpack

// .vuepress/plugins/svg-sprite/index.js
const path = require('path')
const fs = require('fs')
const resolve = dir => path.join(__dirname, dir)

module.exports = (options, context) => {
  // plugin options iconsDir, point to '.vuepress/public/icons'
  const { iconsDir = '.vuepress/public/icons' } = options
  const iconsPath = path.isAbsolute(iconsDir)
    ? iconsDir
    : path.resolve(context.sourceDir, iconsDir)

  if (!fs.existsSync(iconsPath)) {
    console.log(`svg-sprite: Folder ${iconsPath} does not exist`)
  }

  return {
    name: 'svg-sprite',
    enhanceAppFiles: [
      resolve('enhanceApp.js')
    ],
    chainWebpack (config) {
      config.module
        .rule('svg')
        .exclude.add(iconsPath)
        .end()
      config.module
        .rule('svg-sprite-loader')
        .test(/\.svg$/)
        .include.add(iconsPath)
        .end()
        .use('svg-sprite-loader')
        .loader('svg-sprite-loader')
        .options({
          symbolId: 'icon-[name]'
        })
        .end()
        .before('svg-sprite-loader')
        .use('svgo-loader')
        .loader('svgo-loader')
        .options({
          plugins: [
            { removeTitle: true },
            { convertColors: { shorthex: false } },
            { convertPathData: false }
          ]
        })
        .end()
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

# 编写 UI 组件

编写一个 functional Component 即可

// .vuepress/plugins/svg-sprite/enhanceApp.js
const importAllSvg = () => {
  // require.context api must provide a literal path, that's a limit for plugin
  // https://webpack.js.org/guides/dependency-management/#requirecontext
  const icons = require.context('../../public/icons', false, /\.svg$/)
  const importAll = r => r.keys().map(r)
  importAll(icons)
}

export default ({ Vue }) => {
  importAllSvg()

  // regisiter a svg-icon component
  Vue.component('svg-icon', {
    functional: true,
    props: {
      symbol: {
        type: String,
        required: true
      },
      className: {
        type: String,
        default: ''
      }
    },
    render: function (h, { data, props, children }) {
      return h(
        'svg',
        {
          ...data,
          class: [
            'svg-icon',
            `svg-icon-${props.className}`
          ],
          style: {
            width: '1em',
            height: '1em',
            'vertical-align': '-0.15em',
            fill: 'currentColor',
            overflow: 'hidden'
          },
          attrs: { 'aria-hidden': true }
        },
        [
          h('use', {
            attrs: {
              'xlink:href': `#icon-${props.symbol}`
            }
          })
        ]
      )
    }
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

# 引用插件

// .vuepress/config.js
const SvgSprite = require('./plugins/svg-sprite/index')

module.exports = {
  title: '小蘿蔔丁',
  ...
  plugins: [
    ...
    [
      SvgSprite
    ]
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 使用 icon

<svg-icon symbol="heart" />
1

# Why TailwindCSS?

“御风而行”的 CSS,开发体验极佳

# Tailwind CSS

TailwindCss 是一个很棒的CSS类库,与其说是类库,不如说是一个超大的样式类工具集合,如果你掌握甚至习惯了 Tailwind 的语法。你会爱上它的。

Tailwind 提供了功能类至上移动端优先多种CSS伪类变体自定义插件等强大的核心功能。

# 功能类至上

Tailwind 提供大量的甚至说庞大的样式类声明,使得我们在编写页面样式的时候,可以不用写一行 style 就能实现大部分场景,比如我们有一个div,想通过 flex 布局实现垂直居中功能,我们需要编写如下CSS:

.flex-center {
	display: flex;
	justify-content: center;
	align-items: center;
}
1
2
3
4
5

使用 Tailwind CSS 只需在元素 class 上声明如下:

<div class="flex justify-center items-center">I am a div</div>
1

不过让开发者在元素 class 上编写一堆 class 名字,稍微有点反人类,许多开发者也经常诟病于此,尤其是使用 Vue 写组件的时候,我们的组件会变得很难看,甚至丑出天际。

还好 Tailwind 还有另一种写法,使用 @apply 指令,通过 @apply 指令编写的 Tailwind 代码如下:

// 单行声明
.flex-center {
	@apply flex justify-center items-center
}

// 或者多行声明
.flex-center {
	@apply flex
	@apply justify-center
	@apply items-center
}
1
2
3
4
5
6
7
8
9
10
11

这样看上去好一些,把 Tailwind 提供的样式类编写在一个 class 声明中,代替 style 的正常写法,如果习惯了 Tailwind,它真的就是你快速开发页面原型的利器了。

# 移动端优先?

Tailwind 默认使用了类似 Bootstrap 的移动端优先的断点系统,在响应式页面设计里,我们应该优先编写移动端视口(最小的断点)样式,再去调试其他视口的样式。也就是说如果你是按着 PC 端大屏幕的样式开发完成页面的样式,切换到移动端视口下,页面的展现未必是你想要的样子。

Tailwind CSS 默认的断点设置:

/* Small (sm) */
@media (min-width: 640px) { /* ... */ }

/* Medium (md) */
@media (min-width: 768px) { /* ... */ }

/* Large (lg) */
@media (min-width: 1024px) { /* ... */ }

/* Extra Large (xl) */
@media (min-width: 1280px) { /* ... */ }
1
2
3
4
5
6
7
8
9
10
11

比如要让一个标题在不同的视口下展示的字体大小不一样:

<div class="text-lg sm:text-xl md:text-3xl lg:text-4xl xl:text-5xl">
	The Responsive Title
</div>
1
2
3

不同屏幕下看到的标题字体大小就会不同,响应式开发在 class 里就能完成,并不需要写在统一的媒体查询(@media)里了。这种开发体验是不是非常爽。

# CSS 伪类变体?

CSS 开发中我们比较熟悉的一些 CSS 伪类,比如:hoverfocusactivefirst-childlast-child 等,在 Tailwind 中都提供了更方便的 class 声明。

比如让一个按钮有 hover 效果,我们需要编写样式如下:

.btn {
	background-color: #f4a;
}

.btn:hover {
	background-color: #4af;
}
1
2
3
4
5
6
7

使用 Tailwind CSS 伪类变体,只需在 class 前加上 hover: 即可实现:

<button class="bg-transparent hover:bg-blue-500...">
  Hover me
</button>
1
2
3

这样是写法真的是大大减少了 Style 的编写量。

# VuePress + TailwindCSS

如何在 VuePress 项目里使用 TailwindCSS 呢,TailwindCSS Installation 官方文档上说的其实很清楚了。一共四个步骤:

# 1、安装 Tailwind

# Using npm
npm install tailwindcss

# Using Yarn
yarn add tailwindcss
1
2
3
4
5

# 2、在主题样式里引入 Tailwind

文件路径: .vuepress/theme/styles/index.styl

// .vuepress/theme/styles/index.styl
@tailwind base;
@tailwind components;
@tailwind utilities;
1
2
3
4

# 3、初始化 Tailwind 配置文件

项目根目录下:./tailwind.config.js

// tailwind.config.js
module.exports = {
  theme: {},
  variants: {},
  plugins: [],
}
1
2
3
4
5
6

# 4、使用 PostCSS 处理 Tailwind

VuePress 配置文件中有配置 postcss 的选项

// .vuepress/config.js
module.exports = {
	...
  postcss: {
    plugins: [
      require('tailwindcss'),
      require('autoprefixer')
    ]
  }
}
1
2
3
4
5
6
7
8
9
10

这样就可以使用 tailwind 的所有的样式类了。

# Tailwind Theme

Tailwind 可在 配置文件中声明主题(theme)的配置,基本上所有默认主题提供的设置都可以覆盖,这提供给我们很大的自由度,比如声明博客的色系,我们可以通过改变主题配置中的 colors 即可使用自己声明的颜色 class 了。

// tailwind.config.js

module.exports = {
  theme: {
    ...
    extend: {
      colors: {
        ...
        primary: {
          default: '#139ce7',
          hover: '#53BAED',
          dark: '#4799eb'
        }
        ...
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

colors 的配置 Tailwind 会帮你生成对应的 class 声明,colors 影响到所有使用颜色的类,比如 textbgborderdivideplaceholder ,使用颜色类时以 text 举例会这样声明:

<span class="text-gray-700 hover:text-primary">I am a span</span>
1

# Dark Mode

暗色风格页面都已经流行了一年多了。在苹果系统 macOS、iOS 生态里很多应用或者微信里都提供了暗色风格都UI页面,暗色风格UI已经成为了一个流行趋势,我的新博客也加上了暗色风格UI,采用的是 CSS 媒体查询特性 prefers-color-scheme ,博客主题会根据系统是否切换成深色来自动切换UI风格。

这个功能实现归功于 Tailwind,Tailwind 提供了优秀的配置主题扩展,只需声明一个媒体查询即可:

// tailwind.config.js

module.exports = {
  theme: {
    screens: {
			...
      dark: {
        raw: '(prefers-color-scheme: dark)'
      }
    }
	}
}
1
2
3
4
5
6
7
8
9
10
11
12

如何使用暗色风格的媒体查询,就像使用 smmdlgxl 这种响应式媒体查询一样:

<span class="text-gray-700 dark:text-gray-200">Color will change in dark mode</span>
1

PS:关于暗色风格UI,目前我还没有做成可随时切换的按钮,这也是计划中的功能。

# TailwindCSS 插件

都写过 VuePress 插件了。要不要也尝试一个 Tailwind 插件呢,其实也非常简单。比如博客中大量复用了渐变色文字,我完全可以写一个工具类,让这些可复用的样式变为一种模式。

插件代码如下:

const plugin = require('tailwindcss/plugin')

module.exports = plugin(function ({ addUtilities, theme }) {
  const colors = theme('colors', {})

  const newUtilities = {
    '.text-neon': {
      'background-image': `linear-gradient(90deg, ${colors.switchNeon.pink} 0px, ${colors.switchNeon.lightBlue} 100%)`,
      'background-clip': 'text',
      '-webkit-background-clip': 'text',
      '-webkit-text-fill-color': 'transparent'
    },
    '.bg-neon': {
      'background-image': `linear-gradient(90deg, ${colors.switchNeon.pink} 0px, ${colors.switchNeon.lightBlue} 100%)`
    }
  }

  addUtilities(newUtilities, {
    variants: ['responsive', 'hover', 'focus']
  })
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

在 Tailwind 配置文件中引入插件:

// tailwind.config.js

module.exports = {
  ...
  plugins: [
    require('./.vuepress/plugins/tailwind/gradients')
  ]
}
1
2
3
4
5
6
7
8

实际使用的时候,只需在 class 上添加上 text-neon 即可让文字变成渐变色,这也响应了 Tailwind 功能类优先的核心思想。

# You don't need TailwindCss?

原谅我这里标题党了,我不需要使用 TailwindCss 吗?不,我当然需要 TailwindCss 来开发我的新博客主题,我想说的是,你有没有想过 TailwindCss 的样式类,我们自己也是可以写出来的,比如使用预处理器 Sass/SCSSStylus 又或者是 Less 语法。

拿 CSS 中的 display 属性举例 ,在 TailwindCss 我们引用的源文件中,TailwindCss 生成的代码实际是这样的:

.block {
  display: block
}

.inline-block {
  display: inline-block
}

.inline {
  display: inline
}

.flex {
  display: flex
}

.inline-flex {
  display: inline-flex
}

.grid {
  display: grid
}

...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

所有的 display 属性,我们日常开发都能用到吗?其实并不是。不过它就声明在那里。所以 TailwindCss 最终生成的文件体积接近 **1 MB** 级别。这也是让很多开发者头疼的地方,一定要搭配 Webpack + PostCss 进行处理才能减少实际使用的代码体积。

其实想要得到上看的类声明很简单,就是提前定义好了类的声明,我们随时使用就好了。像这样简单的类声明,我们可以轻易的使用CSS 预处理工具去实现。比如使用 SCSS:

// loop display list
$displayList: ('block', 'inline-block', 'inline', 'flex', 'inline-flex', 'grid');

@each $display in $displayList {
  .#{$display} {
    display: #{$display};
  }
}
1
2
3
4
5
6
7
8

想要深入探索 TailwindCss 类生成的原理,其实很简单,这里不展开说明,我正在计划写另一篇文章,介绍 TailwindCss 工具类是如何生成的?

# VuePress + TailwindCSS Starter

VuePress + TailwindCSS 真的给了我很快捷的页面开发体验,不同于像使用 Element-UI、iView 这种 UI 库,编写出来的页面风格几乎千篇一律,我们需要自己实现一些页面样式设计,这样才是属于自己的独立博客 😎。

为了方便给自己写的 VuePress 插件有个调试环境,一开始是在自己的博客 Repo 上调试,后来自己的博客 Repo 用于测试已发布到 NPM 的 VuePress 插件。于是需要一个开发环境测试。

我建立了一个简洁的、VuePress + TailwindCSS 初始模版。同样适用于调试新写的 VuePress 或者 TailwindCss 插件,建议想要尝试的小伙伴看这里:

vuepress-tailwind-theme-starter

# Why Netlify?

Netlify 比你想象的还好用

Netlify 是一个一站式的网站构建平台,虽然 GitHub Page 足够支撑一个静态页面的发布了,但是 Netlify 提供了更高性能的 Jamstack 页面构建,同时我的新博客也是使用 Jamstack 构建的。Netlify 同时也支持绑定个人独立域名,还可以帮我的域名加上 https 证书。真是大爱。

# VuePress + Netlify (CI / CD)

VuePress 如何在 Netlify 上发布呢?VuePress 官方文档上已经有说明文档了。只需要两步:

  • 连接你的 GitHub 项目,设定配置项:

    Build Command: yarn build

    Publish directory: .vuepress/dist

  • 设定自动构建的分支,比如: develop ,点击 deploy 按钮

Netlify 会自动帮你生成一个可访问的网址,我们也可以改变站点的三级域名名称,比如改成 xlbd.netlify.app ,或者绑定自己的独立域名。

就这样每次我们向指定分支 push 代码的时候,Netlify 就会自动帮你构建并发布到域名上,完成自动部署。

# 绑定域名

xlbd.me 是使用 Godaddy 申请的域名,这里是使用的 Netlify DNS Nameservers 进行配置的域名解析

# Netlify DNS Nameservers

配置 Nameservers 后,Netlify 会自动生成四个 Netlify DNS Nameservers

dns1.p04.nsone.net
dns2.p04.nsone.net
dns3.p04.nsone.net
dns4.p04.nsone.net
1
2
3
4

只需将 Netlify DNS Nameservers 配置在 Godaddy 上。

# 配置 Godaddy DNS

这种配置方式有个缺点,可能之前你的域名解析的记录都不能使用了。

# HTTPS

令我欢喜的是,Netlify 提供了一键集成 https 的功能,使用的是 Let's Encrypt 提供证书,只要你的域名 DNS 解析成功后,https 域名就已经有证书能使用了。great!

现在就可以通过 https://xlbd.me 来访问我的博客了。

# Notion Image Hub

感谢 notion 最近发布的个人版免费计划,取消了1000个块的限制,notion 已经成为我日常工作的伙伴,notion 颠覆了我所有使用过的 markdown 软件,想要的功能基本都有,甚至可以构建一个静态页面,发布博客。

之前我一直在 Mac 上一直使用的是 MWeb,MWeb 也是一个非常棒的知识管理软件,尤其是对图床、发布媒体的支持,我之前把写好的博文,通过 MWeb 连接 Ghost 直接发布。也是方便的很。

这次我使用 notion 作为我的图床,可以在 notion 建立一个 database,上传一些博文图片,将图片连接作为我的博文图片输入。也是个不错的方法。

# Ghost Theme Kaldorei

Ghost 博客主题 ghost-theme-kaldorei 已经运行了四年多,也见证了 Ghost 从 1.x 版本到现在的 Ghost 3.x 版本,Ghost 确实在不断变化着,越变越好。不过 Ghost 的后台文本编辑器一直对中文支持不太好,一直让国内开发者所诟病,还好我有 MWeb 为我发布内容。

ghost-theme-kaldorei 现在仍然支持在最新版的 Ghost 3.x 中使用,4年来感谢🙏250+ star 的支持,在 GitHub Topics #ghost-theme 中,Kaldorei 以第九名排进前十。我会一直维护这个主题。虽然代码有点老。还是用的 jQuery!

我可能计划后续会迁移一个 vuepress-theme-kaldorei 版本到 vuepress 上,或者出现在使用 JAMStack 技术栈的其他方案上。延续 Kaldorei 的风格。

# 博客后续规划

  • Disqus Support
  • Book Corner
  • About Page
  • Switch Light / Dark Mode

前端技术的变化确实在快步向前,从刀耕火种到年代,到现代前端的模块化、工程化前端工具层出不穷时代。我们该感谢开源社区以及杰出优秀色的开发者们,是他们改变了历史、影响了很多人的事业以及人生也不为过,我就是其中一个幸运的被影响者 😄。