点击查看更新记录

更新记录

2021-06-30:初步讲解可能用到的api

  1. hexo过滤器(Filter)API用法
  2. hexo生成器(Generator)API用法
  3. hexo注入器(Injector)API用法
  4. hexo辅助函数(Helper)API简单用法
点击查看参考教程
参考方向教程原贴
高情商:有效治疗低血压患者Hexo API文档
参考了页面生成插件的写法
参考了页面植入式插件的写法,以及hexo api的具体应用
编译stylus文件stylus官方文档
编译pug文件PUG官方文档
店长的碎碎念

本文讨论的npm插件化,针对的是那种可以放在单独的页面魔改,或者代码可以剥离出来,通过某个特定的页面容器进行挂载的植入式魔改方案。

例如糖果屋的gitcalendar,页面轮播图,以及所有的侧栏魔改就是植入式插件。而信封式留言板,朋友圈前端页面则是页面式插件。糖果屋微调合集基本上是不可能写成插件了。倒不是说理论上不可行。而是时间成本不对等。毕竟改10行代码的事情非要去写几十行的插件,得不偿失。

它们的共同特点就是高内聚低耦合。除了必要的挂载容器以及适配样式和主题相关以外,理论上可以把它们迁移到任何其他主题。
这就给魔改方案普及化提供了可能性。只需要更换挂载容器或者附加一些样式补丁,我们就能很轻易的在其他主题也用上这些方案。
本文提供的详细的教程拆解和具体示例。

NPM插件的发布

此部分内容已经在NPM图床的使用技巧中进行过详细描述。关于账户注册和插件发布的部分本帖不会再详细展开。以下仅针对本地开发流程进行阐述。

教程拆解

点击折叠教程内容
  1. 新建文件夹,在里面运行npm init以后,按照指示,初始化npm插件。我们首先会获得一个package.json。以下是我的package.json内容。此处除了初始化默认生成的内容外,我还指定了pug依赖的版本,没有特殊版本需求的话,不写其实也没有关系。
    {
    "name": "hexo-butterfly-artitalk-pro",
    "version": "0.0.1",
    "description": "A talk plugin for theme-butterfly based on artitalk",
    "main": "index.js",
    "scripts": {
    "eslint": "eslint ."
    },
    "directories": {
    "lib": "./lib"
    },
    "files": [
    "lib/",
    "index.js"
    ],
    "keywords": [
    "hexo",
    "filter",
    "artitalk",
    "butterfly",
    "theme-butterfly",
    "sidebar",
    "plugins"
    ],
    "author": "akilarlxh",
    "license": "Apache-2.0",
    "dependencies": {
    "pug": "^3.0.0"
    },
    "repository": {
    "type": "git",
    "url": "https://github.com/Akilarlxh/hexo-butterfly-artitalk-pro.git"
    }
    }
  2. 然后是新建三个文件(或者文件目录),
    • index.js,这是插件的主要脚本,负责处理各类数据和进行插件植入操作。
    • README.md,这是插件文档,你可以把它认为是你插件的说明书。出于开发者的专业素养,建议详细编写你的插件文档。
    • lib/,这个目录下我们预计存放一些静态资源。当然你也不一定非要在这里放静态资源。放在根目录也同样可以。专门建个资源目录只是为了便于管理而已。
  3. 在编写一款hexo插件之前,我们应当确认会用到哪些配置项。

    以容器挂载型插件为例,必要的内容有插件开关,过滤器优先权,应用页面,挂载容器、屏蔽页面

    config_name:
    enable: true # 开关
    priority: 5 #过滤器优先权
    enable_page: all # 应用页面
    exclude:
    - /posts/ #需要配合abbrlink插件
    layout: # 挂载容器类型
    type: class
    name: sticky_layout
    index: 0

    以页面生成型插件为例,必要的内容有插件开关,页面生成路径,front_matter

    config_name:
    enable: true #开关
    path: #生成的页面路径
    front_matter: #页面的front_matter
    title:
    description:

  4. 在确定变量以后,我们就可以开始在index.js中编写插件的具体内容了。
    • 以容器挂载型为例。首先是整体的文件目录结构
      |__lib
      |__html.pug #插件的主要dom结构
      |__style.css #可能用到的自定义样式
      |__script.js #可能用到的自定义脚本
      |__index.js
      |__package.json
    • 打开index.js,开始编写脚本。请详细阅读以下代码中的注释。我会尽量逐行解释。为了便于区分,我们约定不再变动的为常量,用const声明。会变动的为变量,用var声明。(事实上在这个index.js中,constvar的生命周期没多大意义了。就算全部用var也没事)

      考虑到以下代码内容较多,建议复制到自己的编辑器内进行查看。您也可以将以下代码作为通用模板进行修改,来编写属于您自己的插件。

      'use strict'
      // 全局声明插件代号,插件的名字随你定
      // 此处是为了便于区分多个插件的注入函数名,确保不重名。
      const pluginname = 'plugin_name'
      // 全局声明依赖,此处用到的都是hexo的api或者pug插件的api
      const pug = require('pug')
      const path = require('path')
      const urlFor = require('hexo-util').url_for.bind(hexo)
      const util = require('hexo-util')

      hexo.extend.filter.register('after_generate', function () {
      // 首先获取整体的配置项名称,
      // 这行的写法可以保证不管是写在主题配置文件里
      // 还是站点配置文件里,都能读取到配置项。
      const config = hexo.config.config_name || hexo.theme.config.config_name
      // 如果配置开启
      if (!(config && config.enable)) return
      // 集体声明配置项,将我们用到的配置项全部封装到data里
      // 之后方便我们统一调用。
      // 同时活用三元运算符,给配置项设置默认配置内容。
      const data = {
      enable_page: config.enable_page ? config.enable_page : "all",
      exclude: config.exclude,
      layout_type: config.layout.type,
      layout_name: config.layout.name,
      layout_index: config.layout.index ? config.layout.index : 0
      }
      // 渲染页面,此处调用了pug的api,具体写法可以查看最上方的参考教程。
      const temple_html_text = config.temple_html ? config.temple_html : pug.renderFile(path.join(__dirname, './lib/html.pug'),data)
      //cdn资源声明,来引用必要的依赖或者样式。
      //样式资源
      const css_text = `<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/package_name/lib/style.css">`
      //脚本资源
      const js_text = `<script src="https://cdn.jsdelivr.net/npm/package_name/lib/script.js"></script>`
      //注入容器声明,用来判断注入容器类型。
      // ==========start=============
      var get_layout
      //若指定为class类型的容器
      // 因为各个配置项内容已经封装在data数据集里
      // 我们就需要用data.name的方式来访问
      if (data.layout_type === 'class') {
      //则根据class类名及序列获取容器
      get_layout = `document.getElementsByClassName('${data.layout_name}')[${data.layout_index}]`
      }
      // 若指定为id类型的容器
      else if (data.layout_type === 'id') {
      // 直接根据id获取容器
      get_layout = `document.getElementById('${data.layout_name}')`
      }
      // 若未指定容器类型,默认使用id查询
      else {
      get_layout = `document.getElementById('${data.layout_name}')`
      }
      // =============end===============
      //挂载容器脚本,用来注入上方编译好的插件dom结构
      //挂载容器脚本
      var user_info_js = `<script data-pjax>
      function ${pluginname}_injector_config(){
      var parent_div_git = ${get_layout};
      var item_html = '${temple_html_text}';
      console.log('已挂载${pluginname}')
      parent_div_git.insertAdjacentHTML("afterbegin",item_html)
      }
      //屏蔽项判定
      var elist = '${data.exclude}'.split(',');
      var cpage = location.pathname;
      var epage = '${data.enable_page}';
      var flag = 0; //标记值
      //若命中屏蔽规则,则标记值加一
      for (var i=0;i<elist.length;i++){
      if (cpage.includes(elist[i])){
      flag++;
      }
      }
      // 对全站生效时才会判断屏蔽项
      if ((epage ==='all')&&(flag == 0)){
      ${pluginname}_injector_config();
      }
      // 对单一页面生效时无需判断屏蔽项
      else if (epage === cpage){
      ${pluginname}_injector_config();
      }
      </script>`
      // 使用hexo提供的注入器API,将对应的html代码片段注入到对应的位置
      // 此处利用挂载容器实现了二级注入,也就是所谓的套娃,
      // 注入用户脚本 将上文的容器脚本注入到body标签的结束符之前。
      hexo.extend.injector.register('body_end', user_info_js, "default");
      // 注入脚本资源,将上文的容器脚本注入到body标签的结束符之前。
      hexo.extend.injector.register('body_end', js_text, "default");
      // 注入样式资源,将上文的容器脚本注入到head标签的结束符之前。
      hexo.extend.injector.register('head_end', css_text, "default");
      },
      // 此处利用hexo提供的辅助函数来获取主题配置文件的配置内容,
      // 如果不用辅助函数的话,最多只能读取到站点配置文件的内容。
      hexo.extend.helper.register('priority', function(){
      // 过滤器优先级,priority 值越低,过滤器会越早执行,默认priority是10
      const pre_priority = hexo.config.config_name.priority || hexo.theme.config.config_name.priority
      // 此处设置过滤器优先级的预设值
      const priority = pre_priority ? pre_priority : 10
      // 返回最终的过滤器优先级数值
      return priority
      })
      )

    页面生成型的代码基本完全复用了由@butterfly主题作者jerry开发的主题插件结构。以下仅做了具体注释。

    • 以页面生成型为例。首先是整体的文件目录结构
      |__lib
      |__html.pug #插件的主要dom结构
      |__style.css #可能用到的自定义样式
      |__script.js #可能用到的自定义脚本
      |__index.js
      |__package.json
    • 打开index.js,开始编写脚本。请详细阅读以下代码中的注释。我会尽量逐行解释。为了便于区分,我们约定不再变动的为常量,用const声明。会变动的为变量,用let或者var声明。(事实上在这个index.js中,constletvar的生命周期没多大意义了。就算全部用var也没事)

      考虑到以下代码内容较多,建议复制到自己的编辑器内进行查看。您也可以将以下代码作为通用模板进行修改,来编写属于您自己的插件。

      'use strict'
      // 全局声明依赖,此处用到的都是hexo的api或者pug插件的api
      const pug = require('pug')
      const path = require('path')
      const urlFor = require('hexo-util').url_for.bind(hexo)
      const util = require('hexo-util')
      // 调用hexo的生成器API
      // 此处的pathname指生成的页面名称
      hexo.extend.generator.register('pathname', function (locals) {
      // 首先获取整体的配置项名称,
      // 这行的写法可以保证不管是写在主题配置文件里
      // 还是站点配置文件里,都能读取到配置项。
      const config = hexo.config.config_name || hexo.theme.config.config_name
      // 如果配置开启
      if (!(config && config.enable)) return
      // 集体声明配置项,将我们用到的配置项全部封装到data里
      // 之后方便我们统一调用。
      // 同时活用三元运算符,给配置项设置默认配置内容。
      const data = {
      //考虑到空页面没啥可参考的配置项,
      // 这里用hexo-butterfly-envelope的一些配置项作为写法示例
      author: hexo.config.author,
      cover: config.cover ? urlFor(config.cover) : "https://ae01.alicdn.com/kf/U5bb04af32be544c4b41206d9a42fcacfd.jpg",
      message: config.message ? config.message : ["有什么想问的?","有什么想说的?","有什么想吐槽的?","哪怕是有什么想吃的,都可以告诉我哦~"],
      bottom: config.bottom ? config.bottom : "自动书记人偶竭诚为您服务",
      height: config.height ? config.height : "1050px"
      }
      // 渲染页面,此处调用了pug的api,具体写法可以查看最上方的参考教程。
      const content = pug.renderFile(path.join(__dirname, './lib/html.pug'), data)
      // 页面生成的访问路径
      const pathPre = config.path || 'pathname'
      // 获取主题默认生成页面的基本配置内容,样式。
      let pageDate = {
      content: content
      }
      // 获取页面的front_matter
      if (config.front_matter) {
      pageDate = Object.assign(pageDate, config.front_matter)
      }

      return {
      path: pathPre + '/index.html',
      data: pageDate,
      layout: ['page', 'post']
      }
      })
  5. 编写完毕以后我们就可以通过在插件文件夹根目录下执行npm publish指令来发布插件。
  6. 发布成功后,在hexo博客根目录[Blogroot]下运行安装插件指令安装你的插件。
    npm install package_name --save
  7. 到主题配置文件_config.butterfly.yml或者_config.yml里添加配置项。运行hexo cl && hexo g && hexo s即可。
  8. 当然以上说的都是理想状态。一般我们都会遇到各种各样的报错。这时候我们不用着急去修改插件版本并重新发布、重新安装。可以直接在hexo博客的[Blogroot]/node_modules目录下找到我们的插件文件夹,然后在此处进行修改,并进行本地调试。等到在hexo博客处调整通过了。再将代码逐一复制到插件开发文件夹处。发布正式版本。

具体示例

点击折叠教程内容

hexo-butterfly-artitalk-pro为示例,这款插件结合了页面生成和侧栏插件注入的内容。源码公开在github。诸位开发者可以比对源码和上文的教程拆解以及下方的开发思路进行阅读。

以下是我的开发流程。

  1. 首先还是要先确定我们要用到的配置项。作为容器植入和页面生成并用的插件,自然要用到上文讨论的所有配置项。然后考虑到artitalk使用的api,还应该按照artitalk官方文档设置必要配置项。再就是,若两种形式的插件并存,因为artitalk是根据id挂载的,势必需要一个屏蔽项exclude,确保页面版的时候不会加载侧栏版。于是得出我们的配置项雏形
    artitalk:
    enable:
    card: true # 侧边栏开关
    page: true #页面开关
    # 侧栏相关配置项
    priority: 5 #过滤器优先权
    enable_page: all # 应用页面
    layout: # 挂载容器类型
    type: class
    name: sticky_layout
    index: 0
    # 页面相关配置项
    path: artitalk
    front_matter:
    title: 碎碎念
    # 公共配置项
    appId: ***************************
    appKey: ****************************
    exclude:
    - /artitalk/
    js: https://cdn.jsdelivr.net/npm/artitalk
    option:
  2. 确定文件目录树,这个保持之前的开发模式即可。

    此处我用到了stylus作为css预编译语言。使用方法是全局安装stylus插件,然后使用它提供的指令将styl编译成css即可。

    # 在本地全局安装stylus
    npm install stylus -g
    # cd到.lib目录下
    cd /.lib
    # 使用stylus的编译指令进行编译
    stylus -w card.styl -o card.css

    |__.lib
    |__card_visual.js #控制侧栏内容显隐的自定义脚本
    |__card.css #调整侧栏和页面适配的补丁样式表
    |__card.styl #预编译,主要是开发者本地使用
    |__card.pug #侧栏的dom结构
    |__page.pug #页面的dom结构
    |__index.js #主要脚本,处理数据和控制插入
    |__package.json #记录插件基本信息
  3. 此处需要特别讲解的是card.pugpage.pug,我们需要注意它们在插件中的写法和在主题魔改源码时的写法的不同。主要表现在变量的获取写法。因为插件版的变量是在index.js就预先处理好的,所以可以直接调用。而源码魔改则是可以利用主题自带的一些辅助函数。考虑到多主题适配,显然插件版要摒弃对某一主题自带的辅助函数的依赖。
    • card.pug
      .card-widget.card-shuo
      .card-content(style='height:auto;min-height:280px;')
      .item-headline
      i.fas.fa-comments
      if theme.artitalk.page_enable
      span
      a(href=url_for(theme.artitalk.exclude) title="artitalk page link") 碎碎念
      else
      span 碎碎念
      a#cardVisual(style='cursor:pointer;float:right' onclick='cardVisual()') 编辑
      #artitalk_main(style='width:100%;height:100%;padding:1px')
        .card-widget.card-shuo
      .card-content(style='height:auto;min-height:280px;')
      .item-headline
      i.fas.fa-comments
      - if theme.artitalk.page_enable
      + if page_enable
      span
      - a(href=url_for(theme.artitalk.exclude) title="artitalk page link") 碎碎念
      + a(href=exclude title="artitalk page link") 碎碎念
      else
      span 碎碎念
      a#cardVisual(style='cursor:pointer;float:right' onclick='cardVisual()') 编辑
      #artitalk_main(style='width:100%;height:100%;padding:1px')
      .card-widget.card-shuo
      .card-content(style='height:auto;min-height:280px;')
      .item-headline
      i.fas.fa-comments
      if page_enable
      span
      a(href=exclude title="artitalk page link") 碎碎念
      else
      span 碎碎念
      a#cardVisual(style='cursor:pointer;float:right' onclick='cardVisual()') 编辑
      #artitalk_main(style='width:100%;height:100%;padding:1px')
    • page.pug
      #artitalk_main
      .js-pjax
      script.
      (()=>{
      const init = () => {
      new Artitalk(Object.assign({
      appId: '!{theme.artitalk.appId}',
      appKey: '!{theme.artitalk.appKey}',
      }, !{theme.artitalk.option} ))
      }

      if (typeof Artitalk === 'function') {
      init()
      } else {
      getScript('!{theme.CDN.artitalk}').then(init)
      }
      })()
        #artitalk_main
      .js-pjax
      script.
      (()=>{
      const init = () => {
      new Artitalk(Object.assign({
      - appId: '!{theme.artitalk.appId}',
      - appKey: '!{theme.artitalk.appKey}',
      - }, !{theme.artitalk.option} ))
      + appId: '#{appId}',
      + appKey: '#{appKey}',
      + }, !{option} ))
      }

      if (typeof Artitalk === 'function') {
      init()
      } else {
      - getScript('!{theme.CDN.artitalk}').then(init)
      + getScript('!{js}').then(init)
      }
      })()
      #artitalk_main
      .js-pjax
      script.
      (()=>{
      const init = () => {
      new Artitalk(Object.assign({
      appId: '#{appId}',
      appKey: '#{appKey}',
      }, !{option} ))
      }

      if (typeof Artitalk === 'function') {
      init()
      } else {
      getScript('!{js}').then(init)
      }
      })()
  4. 然后就是关键的index.js了。

    考虑到以下代码内容较多,建议复制到自己的编辑器内进行查看。您也可以将以下代码作为通用模板进行修改,来编写属于您自己的插件。

    'use strict'
    // 全局声明侧栏插件代号
    const pluginname = 'card_artitalk'
    // 全局声明依赖
    const pug = require('pug')
    const path = require('path')
    const urlFor = require('hexo-util').url_for.bind(hexo)
    const util = require('hexo-util')

    // 先来编写侧栏插件,使用容器注入式开发模板

    hexo.extend.filter.register('after_generate', function (locals) {
    // 首先获取整体的配置项名称
    const config = hexo.config.artitalk || hexo.theme.config.artitalk
    // 如果配置开启
    if (!(config && config.enable.card)) return
    // 集体声明配置项
    const card_data = {
    page_enable: config.enable.page ? config.enable.page : false,
    enable_page: config.enable_page ? config.enable_page : "all",
    layout_type: config.layout.type,
    layout_name: config.layout.name,
    layout_index: config.layout.index ? config.layout.index : 0,
    path: config.path ? config.path : "artitalk",
    exclude: config.exclude ? config.exclude : "/artitalk/",
    appId: config.appId,
    appKey: config.appKey,
    option: config.option ? JSON.stringify(config.option) : false,
    js: config.js ? urlFor(config.js) : 'https://cdn.jsdelivr.net/npm/artitalk'
    }
    // 渲染页面
    const temple_html_text = config.temple_html ? config.temple_html : pug.renderFile(path.join(__dirname, './lib/card.pug'),card_data)

    //cdn资源声明
    //样式资源
    const css_text = `<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/hexo-butterfly-artitalk-pro/lib/card.css" media="defer" onload="this.media='all'">`
    //脚本资源
    const js_text = `<script async src="https://cdn.jsdelivr.net/npm/hexo-butterfly-artitalk-pro/lib/card_visual.js"></script>`

    //注入容器声明
    var get_layout
    //若指定为class类型的容器
    if (card_data.layout_type === 'class') {
    //则根据class类名及序列获取容器
    get_layout = `document.getElementsByClassName('${card_data.layout_name}')[${card_data.layout_index}]`
    }
    // 若指定为id类型的容器
    else if (card_data.layout_type === 'id') {
    // 直接根据id获取容器
    get_layout = `document.getElementById('${card_data.layout_name}')`
    }
    // 若未指定容器类型,默认使用id查询
    else {
    get_layout = `document.getElementById('${card_data.layout_name}')`
    }
    // 挂载容器脚本
    // 此处还在挂载脚本后面附上了artitalk初始化函数。
    var user_info_js = `<script data-pjax>
    function ${pluginname}_injector_config(){
    var parent_div_git = ${get_layout};
    var item_html = '${temple_html_text}';
    console.log('已挂载${pluginname}');
    parent_div_git.insertAdjacentHTML("afterbegin",item_html);
    (()=>{
    const init = () => {
    new Artitalk(Object.assign({
    appId: '${card_data.appId}',
    appKey: '${card_data.appKey}',
    }, ${card_data.option} ))
    }
    if (typeof Artitalk === 'function') {
    init()
    } else {
    getScript('${card_data.js}').then(init)
    }
    })()
    }
    var elist = '${card_data.exclude}'.split(',');
    var cpage = location.pathname;
    var epage = '${card_data.enable_page}';
    var flag = 0;

    for (var i=0;i<elist.length;i++){
    if (cpage.includes(elist[i])){
    flag++;
    }
    }

    if ((epage ==='all')&&(flag == 0)){
    ${pluginname}_injector_config();
    }
    else if (epage === cpage){
    ${pluginname}_injector_config();
    }
    </script>`
    // 注入用户脚本
    // 此处利用挂载容器实现了二级注入
    hexo.extend.injector.register('body_end', user_info_js, "default");
    // 注入脚本资源
    hexo.extend.injector.register('body_end', js_text, "default");
    // 注入样式资源
    hexo.extend.injector.register('head_end', css_text, "default");
    },
    hexo.extend.helper.register('priority', function(){
    // 过滤器优先级,priority 值越低,过滤器会越早执行,默认priority是10
    const pre_priority = hexo.config.artitalk.priority || hexo.theme.config.artitalk.priority
    const priority = pre_priority ? pre_priority : 10
    return priority
    })
    )

    // 再是编写页面版插件,使用页面生成式模板
    // 此处直接复用hexo-butterfly-artitalk的原代码
    hexo.extend.generator.register('artitalk', function (locals) {
    const config = hexo.config.artitalk || hexo.theme.config.artitalk

    if (!(config && config.enable.page)) return

    const page_data = {
    appId: config.appId,
    appKey: config.appKey,
    option: config.option ? JSON.stringify(config.option) : false,
    js: config.js ? urlFor(config.js) : 'https://cdn.jsdelivr.net/npm/artitalk'
    }

    const content = pug.renderFile(path.join(__dirname, './lib/page.pug'), page_data)

    const pathPre = config.path || 'artitalk'

    let pageDate = {
    content: content
    }

    if (config.front_matter) {
    pageDate = Object.assign(pageDate, config.front_matter)
    }

    return {
    path: pathPre + '/index.html',
    data: pageDate,
    layout: ['page', 'post']
    }
    })
  5. 之后通过npm publish发布即可。不过个人建议不要忘记编写README.md,从我个人的开发经验来看,后续维护的时候,这个文档会很好的帮到自己。

更多已开发插件

以下是一些已经完成的插件源码。权且作为参考。开发模式基本同本帖所讨论的模板方案。在涉及一些辅助函数的时候也有详细注释。各位开发者可以选择适当的内容作为参照。

插件仓库类型参考方向推荐
容器植入式、页面生成式侧栏魔改方案插件化,页面生成方案插件化,初始化脚本动态生成。
容器植入式侧栏魔改方案插件化
容器植入式依赖添加,补丁添加,通过配置项给页面dom动态添加class,外挂标签插件植入
页面生成式单独页面生成模板。动态生成css
页面生成式单独页面生成模板
容器植入式结合文章front-matter进行筛选,实现与post的front-matter联动
容器植入式多主题适配,读取主题,自动加载容器

TO DO

讲解可能用到的一些api

具体案例

已开发插件源码示例