構建一個簡約博皮的過程

前置

由於之前構建的皮膚 reacg 偏二次元風,儘管提供了大量配置(包括幾乎任何顏色、插件等的配置),依然有人吐槽花里胡哨,遂重新構建了一款簡約風格的部落格園皮膚, 正如你所見。下文我將從零介紹它的構建過程,構建它最快花費一個小時到幾個小時。由於之前做了大量工作,所以現在按照流程走一遍就完事了。

準備工作

  • 環境:node & npm
  • git clone //gitee.com/guangzan/awescnb.git
  • 運行 npm install 安裝依賴
  • 在 themes 文件夾下新建 simple 文件夾、simple/index.js

配置

config/options.js,這是 webpack 的配置:

module.exports = {
    themeName: 'simple',
    template: 'index',
    eslint: true,
    sourceMap: false,
    openAnalyzer: true,
    cssExtract: false,
    openBrowser: false,
    // ...
}

看個人所需,我在這裡簡單配置如下:

  • themeName: ‘simple’ 對應 themes/simple 文件夾
  • template: 運行 npm start 打開部落格園首頁模板
  • 開啟 eslint
  • 開啟 Analyzer,運行 npm run build 時我需要檢查最終構建的體積大小
  • cssExtract 先不分離 css,構建完成直接推送到遠端,直接切換線上版本

開始

  1. simple/style 下存放樣式文件
  • index.scss
  • index.m.scss
  • markdown.scss
  • reset.scss
  • tools.scss

最終只在入口文件 index.js 中導入 index.scss,其他 scss 由 index.scss 引入,由於之前編好了樣式程式碼,所以需要新編寫的樣式極少。

  1. simple/plugins/index.js

引入構建好的部落格園插件,不需要寫任何功能程式碼,及其樣式。

import footer from '@plugins/footer'
import highlight from '@plugins/highlight'
import copy from '@plugins/copy'
import linenumbers from '@plugins/linenumbers'
import imagebox from '@plugins/imagebox'
import commentsAvatars from '@plugins/commentsAvatars'
import dragMenu from '@plugins/dragMenu'
import donation from '@plugins/donation'
import emoji from '@plugins/emoji'
import player from '@plugins/player'
import postMessage from '@plugins/postMessage'
import postSignature from '@plugins/postSignature'
import postTopimage from '@plugins/postTopimage'
import notice from '@plugins/notice'

const plugins = () => {
    footer()
    highlight()
    copy()
    linenumbers()
    imagebox()
    commentsAvatars()
    donation()
    dragMenu()
    emoji()
    player()
    postMessage()
    postSignature()
    postTopimage()
    notice()
}

module.exports = plugins
  • 構建 footer 鏈接
  • mac 樣式程式碼高亮
  • 程式碼行號
  • 圖片燈箱
  • 顯示評論區頭像
  • 捐增二維碼
  • 按鈕工具條(返回頂部,推薦,收藏。。。)
  • 評論框表情
  • 播放器
  • 在文章頭部構建文章資訊
  • 構建文章簽名資訊
  • 文章頭圖
  • 通知功能

由於這個皮膚的基調是簡約,所以只引入一些常用的功能模組。花里胡哨的就不考慮了。

  1. simple/build/index.js

index.js 引入了一些其他 JavaScript 用來做一些調整,例如 simple/build 文件夾下我還寫了

  • catalog(文章目錄)
  • header(頭部導航邏輯)
  • scroll(滾動控制)
  • side (側邊欄邏輯)

catalog/index.js

程式碼有些長,我將它摺疊
import './index.scss'
import {
    pageName,
    userAgent,
    hasPostTitle,
    getClientRect,
    throttle,
} from '@tools'

const { enable } = window.opts.catalog

// 構建目錄
function build() {
    let $catalogContainer = $(
        `<div id="catalog">
            <div class='catalog-title'><h3>目錄</h3></div>
        </div>`,
    )
    const $ulContainer = $('<ul></ul>')
    const titleRegExp = /^h[1-3]$/

    $('#cnblogs_post_body')
        .children()
        .each(function() {
            if (titleRegExp.test(this.tagName.toLowerCase())) {
                if ($(this).text().length === 0) return // 如果標題為空 只有 #
                let id
                let text

                if (this.id !== '') {
                    id = this.id
                    text =
                        this.childNodes.length === 2
                            ? this.childNodes[1].nodeValue
                            : this.childNodes[0].nodeValue
                } else {
                    if (this.childNodes.length === 2) {
                        const value = this.childNodes[1].nodeValue
                        text = value ? value : $(this.childNodes[1]).text()
                    } else {
                        const value = this.childNodes[0].nodeValue
                        text = value ? value : $(this.childNodes[0]).text() // 處理標題被 span 包裹的情況
                    }
                    id = text.trim()
                    $(this).attr('id', id)
                }

                const title = `
                            <li class='${this.nodeName.toLowerCase()}-list'>
                                <a href='#${id}'>${text}</a>
                            </li>
                        `

                $ulContainer.append(title)
            }
        })

    const $catalog = $($catalogContainer.append($ulContainer))
    $('#sidebar_news').after($catalog)
}

function noCatalog() {
    if (pageName() !== 'post') return
    // to do something
}

// 設置目錄活躍標題樣式
function setActiveCatalogTitle() {
    $(window).scroll(
        throttle(
            function() {
                for (let i = $('#catalog ul li').length - 1; i >= 0; i--) {
                    const titleId = $($('#catalog ul li')[i])
                        .find('a')
                        .attr('href')
                        .replace(/[#]/g, '')
                    const postTitle = document.querySelector(
                        `#cnblogs_post_body [id='${titleId}']`,
                    )
                    if (getClientRect(postTitle).top <= 10) {
                        if (
                            $($('#catalog ul li')[i]).hasClass('catalog-active')
                        )
                            return
                        $($('#catalog ul li')[i]).addClass('catalog-active')
                        $($('#catalog ul li')[i])
                            .siblings()
                            .removeClass('catalog-active')
                        return
                    }
                }
            },
            50,
            1000 / 60,
        ),
    )
}

function setCatalogToggle() {
    $(window).scroll(
        throttle(
            function() {
                if ($('#catalog ul').css('display') === 'none') return
                const bottom = getClientRect(
                    document.querySelector('#sideBarMain'),
                ).bottom
                if (bottom <= 0) {
                    $('#catalog').addClass('catalog-sticky')
                } else {
                    $('#catalog').removeClass('catalog-sticky')
                }
            },
            50,
            1000 / 60,
        ),
    )
}

function toggle() {
    $('.catalog-title').click(function() {
        $('#catalog ul').toggle('fast', 'linear', function() {
            $(this).css('display') === 'none'
                ? $('.catalog-title').removeClass('is-active')
                : $('.catalog-title').addClass('is-active')
        })
    })
}

function catalog() {
    if (
        enable &&
        hasPostTitle() &&
        pageName() === 'post' &&
        userAgent() === 'pc'
    ) {
        build()
        setActiveCatalogTitle()
        setCatalogToggle()
        toggle()
    } else {
        noCatalog()
    }
}

module.exports = catalog

header/index.js

import './index.scss'
import { pageName, userAgent } from '@tools'

// header右側按鈕容器
const buildHeader = () => {
    const gitee = window.opts.gitee
    $('#navList').after(`<div class="navbar-end"></div>`)
    $('#blog_nav_newpost').appendTo('.navbar-end')
    $(
        `<a href="//guangzan.gitee.io/awescnb-docs/" id="header-awescnb">構建新皮膚</a>`,
    ).appendTo('.navbar-end')
    $(`<a href="${gitee.url}" id="header-gitee">開源主頁</a>`).appendTo(
        '.navbar-end',
    )
}

// 構建header昵稱
const headerNickname = () => {
    $('#Header1_HeaderTitle').text($('#profile_block a:first').text())
}

// header頭像
const buildAva = () => {
    const { avatar } = window.opts.theme
    $('#blogLogo').attr('src', `${avatar}`)
}

// 隨筆頁構建文章題目
const headerInnerPostTitle = () => {
    if (pageName() !== 'post') return
    if (userAgent() !== 'pc') return
    let title = $('.post .postTitle')
        .text()
        .replace(/\s*/g, '')
    const titleLength = title.length

    let offset = ''
    if (0 <= titleLength && titleLength < 10) offset = '-180%'
    if (10 <= titleLength && titleLength < 15) offset = '-140%'
    if (15 <= titleLength && titleLength < 20) offset = '-100%'
    if (20 <= titleLength && titleLength < 25) offset = '-65%'
    if (25 <= titleLength && titleLength < 28) offset = '-60%'
    if (titleLength >= 28) {
        title = title.substring(0, 28) + '...'
        offset = '-60%'
    }
    $('#navList').append(`<span class='header-posttitle'>${title}</span>`)
    $('head').append(
        `<style>
               .header-posttitle {transform: translate3d(${offset}, 300%, 0);}
                #header.is-active .header-posttitle {transform: translate3d(${offset}, 0, 0);}
        </style>`,
    )
}

// header移動端菜單
const headerBtn = () => {
    const ele = `<div id="navbarBurger" class="navbar-burger burger" data-target="navMenuMore">
      <span></span>
      <span></span>
      <span></span>
    </div>`
    $('#blogTitle').append(ele)
    $('#navbarBurger').click(function() {
        $(this).toggleClass('is-active')
        $('#navigator').toggleClass('is-active')
    })
}

// 創建自定義圖標容器及其圖標
const customLinks = () => {
    const github = window.opts.github
    // wrap
    $('.navbar-end').prepend(`<div class="custom-links"></div>`)
    $('#blogTitle h2').after(`<div class="custom-links"></div>`)
    // github icon
    if (github.enable) {
        $('.custom-links').append(`<a class="github" href="${github.url}"></a>`)
    }
    // qq
    // $('.custom-links').append(`<a class="qq"></a>`)
    // 知乎
    $('.custom-links').append(`<a class="zhihu"></a>`)
}

// 首頁 header 不要上下翻滾
const preventHeaderChange = () => {
    if (pageName() !== 'index') return
    $('#header').addClass('navlist-fix')
}

const header = () => {
    headerNickname()
    buildHeader()
    buildAva()
    headerBtn()
    customLinks()
    headerInnerPostTitle()
    preventHeaderChange()
}

module.exports = header

scroll/index.js

// import './index.scss'
import { userAgent } from '@tools'
// 只觸發一次向上或向下
// 如果又重新反向滾動則再觸發一次
function scrollOnce() {
    function scrollFunc() {
        let scrollDirection
        if (!scrollAction) {
            scrollAction = window.pageYOffset
        }
        let diff = scrollAction - window.pageYOffset
        if (diff < 0) {
            scrollDirection = 'down'
        } else if (diff > 0) {
            scrollDirection = 'up'
        } else {
            // First scroll event
        }
        scrollAction = window.pageYOffset
        return scrollDirection
    }
    let scrollAction, originalDir

    $(window).scroll(function() {
        if (userAgent() !== 'pc') return
        let direction = scrollFunc()
        if (direction && originalDir != direction) {
            if (direction == 'down') {
                $('#header').addClass('is-active')
                $('#catalog').addClass('catalog-scroll-up')
                $('#catalog').removeClass('catalog-scroll-down')
            } else {
                $('#header').removeClass('is-active')
                $('#catalog').removeClass('catalog-scroll-up')
                $('#catalog').addClass('catalog-scroll-down')
            }
            originalDir = direction
        }
    })
}

function scroll() {
    scrollOnce()
    // ...
}

module.exports = scroll

side/index.js

import './index.scss'
import { poll } from '@tools'
import { actions } from '@constants/element'

const sideItemToggle = () => {
    for (const { title, content } of actions) {
        if (!title.length) continue
        $(title).click(function() {
            $(content).toggle('fast', 'linear', function() {
                $(this).css('display') === 'none'
                    ? $(title).removeClass('is-active')
                    : $(title).addClass('is-active')
            })
        })
    }
}

const addCalendarTitle = () => {
    $('#blog-calendar').prepend(`<div id="blog-calendar-title">部落格日曆</div>`)
}

const side = () => {
    addCalendarTitle()
    setTimeout(() => {
        poll($('#blog-sidecolumn').length, sideItemToggle)
    }, 0)
}

module.exports = side
  1. simple/index.js

唯一需要寫的樣板程式碼:

import './style/index.scss'
import AwesCnb from '@awescnb'

class Simple extends AwesCnb {
    constructor() {
        super()
        super.init(this.init)
    }

    init() {
        require('./build')()
        require('./plugins')()
    }
}

new Simple()

構建

  • 運行 npm start 分別對首頁、隨筆詳情頁、標籤頁等調整
  • 運行 npm run build 打包

皮膚 simple 所有樣式和邏輯加起來有 130+kb,沒有異常。如果想擁有更好的體驗可以將 css 分離,在文章開頭的配置介紹中提供了這個選項。最後推送上去就能在部落格園切換到新皮膚了。

鏈接