摘要: 在看Hexo
文档的时候发现了关于Hexo
中的插件的相关知识,于是就去找了一些插件的源码,了解了一下大概的插件编写的格式,本文是记录一下自己开始尝试Hexo
插件的第一步。
1.Hexo
的插件 在Hexo
中,有强大的插件系统,使开发者能偶轻松扩展功能而不用修改核心模块的源码。它的插件分为两种:脚本(Scripts
)和插件(Packages
)。 如果自己的代码很简单,可以编写脚本,这个时候只需要把自己编写的JavaScript
文件放到[blogRoot]/scripts
文件夹,整个文件夹一般默认是没有的,所以需要自己在站点根目录下创建,整个文件夹中的脚本文件在启动时就会自动载入。 另一种则适合自己的代码较为复杂的情况,或是自己想要发布到npm
上,首先,在node_modules
文件夹中建立文件夹,文件夹名称开头必须为hexo-
,如此一来Hexo
才会在启动时载入,否则Hexo
将会忽略它。文件夹内至少要包含2
个文件:一个是主程序,另一个是 package.json
,描述插件的用途和所依赖的插件。
2.Hexo
插件的加载流程 这里列出的相关内容,是在自己还未掌握JavaScript
的情况下,后边对JavaScript
有了更深入的了解后,会将此部分错误或者不合适的地方进行更新。
2.1脚本(Scripts
)的加载 在阅读源码的过程中,有这么一部分代码,大致理解一下,就是使用此函数完成了根目录下scripts
文件夹和主题文件目录下的scripts
文件夹中的各个脚本文件的加载。
JavaScript [blogRoot]/node_modules/hexo/lib/hexo/load_plugins.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function loadScripts (ctx ) { const baseDirLength = ctx.base_dir.length; function displayPath (path ) { return magenta(path.substring(baseDirLength)); } return Promise .filter([ ctx.theme_script_dir, ctx.script_dir ], scriptDir => { return scriptDir ? exists(scriptDir) : false ; }).map(scriptDir => listDir(scriptDir).map(name => { const path = join(scriptDir, name); return ctx.loadPlugin(path).then(() => { ctx.log.debug('Script loaded: %s' , displayPath(path)); }).catch(err => { ctx.log.error({err}, 'Script load failed: %s' , displayPath(path)); }); })); }
2.1插件(Packages
)的加载 plugins
的加载是通过以下函数完成的。
JavaScript [blogRoot]/node_modules/hexo/lib/hexo/load_plugins.js 1 2 3 4 5 6 7 8 9 10 11 12 function loadModules (ctx ) { return loadModuleList(ctx).map(name => { const path = ctx.resolvePlugin(name); return ctx.loadPlugin(path).then(() => { ctx.log.debug('Plugin loaded: %s' , magenta(name)); }).catch(err => { ctx.log.error({err}, 'Plugin load failed: %s' , magenta(name)); }); }); }
3.Hexo
相关函数 在使用插件之前,肯定要先了解一下基本的函数啦,这一节的内容大部分来自于Hexo
官方文档。
3.1过滤器(Filter
)
过滤器用于修改特定文件,Hexo
将这些文件依序传给过滤器,而过滤器可以针对文件进行修改。
JavaScript [blogRoot]/node_modules/hexo/lib/extend/filter.js 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 class Filter { constructor ( ) { this .store = {}; } register (type, fn, priority ) { if (!priority) { if (typeof type === 'function' ) { priority = fn; fn = type; type = 'after_post_render' ; } } if (typeof fn !== 'function' ) throw new TypeError ('fn must be a function' ); type = typeAlias[type] || type; priority = priority == null ? 10 : priority; const store = this .store[type] || []; this .store[type] = store; fn.priority = priority; store.push(fn); store.sort((a, b ) => a.priority - b.priority); } }
JavaScript 1 2 3 4 5 6 7 8 9 10 hexo.extend.filter.register(type, function ( ) { const { config } = this ; if (config.external_link.enable) const { config : themeCfg } = this .theme; if (themeCfg.fancybox) }, priority);
type :为过滤器列表,它应该是一个字符串数据,使用的时候要加上''
,可以是以下值:
type的值
说明
before_post_render
在文章开始渲染前执行。
after_post_render
在文章渲染完成后执行。
before_exit
在 Hexo 即将结束时执行,也就是在 hexo.exit 被调用后执行。
before_generate
在生成器解析前执行。
after_generate
在生成器解析后执行。
template_locals
修改模板的局部变量 。
after_init
在 Hexo 初始化完成后执行,也就是在 hexo.init 执行完成后执行。
new_post_path
用来决定新建文章的路径,在建立文章时执行。
post_permalink
用来决定文章的永久链接。
after_render
在渲染 后执行。
server_middleware
新增服务器的 Middleware。app 是一个Connect 实例。
priority :是过滤器的优先级,priority
值越低,过滤器会越早执行,默认的 priority
是 10。建议提供配置选项如 hexo.config.your_plugin.priority
,让用户自行决定过滤器的优先级。
3.2注入器(Injector
)
注入器被用于将静态代码片段注入生成的HTML
的<head></head>
或<body></body>
中。Hexo
将在 after_render:html
过滤器 之前 完成注入。
JavaScript [blogRoot]/node_modules/hexo/lib/extend/injector.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Injector { constructor ( ) { this .store = { head_begin: {}, head_end: {}, body_begin: {}, body_end: {} }; register (entry, value, to = 'default' ) { if (!entry) throw new TypeError ('entry is required' ); if (typeof value === 'function' ) value = value(); const entryMap = this .store[entry] || this .store.head_end; const valueSet = entryMap[to] || new Set (); valueSet.add(value); entryMap[to] = valueSet; } }
JavaScript 1 hexo.extend.injector.register(entry, value, to)
entry :字符串类型数据,表示代码片段注入的位置,接受以下值:head_begin
: 注入在 <head>
之后(默认)head_end
: 注入在 </head>
之前body_begin
: 注入在 <body>
之后body_end
: 注入在 </body>
之前
value :字符串,或者支持返回值为字符串的函数,表示需要注入的代码片段。to :字符串类型数据,需要注入代码片段的页面类型,接受以下值:default
: 注入到每个页面(默认值)home
: 只注入到主页(is_home()
为 true
的页面)post
: 只注入到文章页面(is_post()
为 true
的页面)page
: 只注入到独立页面(is_page()
为 true
的页面)archive
: 只注入到归档页面(is_archive()
为 true
的页面)category
: 只注入到分类页面(is_category()
为 true
的页面)tag
: 只注入到标签页面(is_tag()
为 true
的页面) 其他自定义 layout
名称,自定义 layout
参考写作 - 布局(Layout
) 实例。
JavaScript 1 2 3 4 5 6 7 8 9 10 11 12 const css = hexo.extend.helper.get('css' ).bind(hexo);const js = hexo.extend.helper.get('js' ).bind(hexo);hexo.extend.injector.register('head_end' , () => { return css('https://cdn.jsdelivr.net/npm/aplayer@1.10.1/dist/APlayer.min.css' ); }, 'music' ); hexo.extend.injector.register('body_end' , '<script src="https://cdn.jsdelivr.net/npm/aplayer@1.10.1/dist/APlayer.min.js">' , 'music' ); hexo.extend.injector.register('body_end' , () => { return js('/js/jquery.js' ); });
3.3辅助函数(Helper
)
辅助函数帮助我们在模板中快速插入内容,我们可以把复杂的代码放在辅助函数而非模板中。
JavaScript [blogRoot]/node_modules/hexo/lib/extend/helper.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Helper { constructor ( ) { this .store = {}; } register (name, fn ) { if (!name) throw new TypeError ('name is required' ); if (typeof fn !== 'function' ) throw new TypeError ('fn must be a function' ); this .store[name] = fn; } }
JavaScript 1 2 hexo.extend.helper.register(name, function ( ) { });
示例如下,此示例的作用是将js
封装成一个函数,只需要自己填写要调用的脚本文件名称,就可以调用特定路径下的相应文件。
JavaScript 1 2 3 hexo.extend.helper.register('js' , function (path ) { return '<script src="' + path + '"></script>' ; });
函数注册通过辅助函数注册后,可以通过以下格式调用。
nunjuck 1 2 3 4 5 //在nunjuck模板中使用格式 {{- js('script.js') }} //渲染之后为以下语句 <script src="script.js"></script>
4.尝试Hexo
中最基本的插件 首先呢需要初始化一个新的站点目录,不做任何修改,以便于测试自己的写的基本插件是否生效。然后就可以开始编写插件啦😄。
shell 1 2 3 4 5 # 初始化一个新文件夹作为站点 hexo init npm-test # 进入该站点文件夹, cd npm-test
4.1脚本(Scripts
)测试
在站点根目录下新建scripts
文件夹,并新建一个test.js
文件,添加以下内容,在编写环境控制台输出一段提示字符。
JavaScript 1 2 3 4 hexo.extend.filter.register('before_exit' , function ( ) { console .log(`测试程序加载成功(ฅ>ω<*ฅ)` ) }, priority);
在scripts/test.js
文件中添加以下内容,由于要实现的效果程序较长,还请点击折叠部分查看。该测试程序实现的效果是在所有页面加载一个点击产生礼花炸开效果的脚本,并且在渲染后页面的控制台输出一段提示字符。该段程序可以也可以放在过滤器中。
JavaScript 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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 var user_info_js = ` <script> class Circle { constructor({ origin, speed, color, angle, context }) { this.origin = origin this.position = { ...this.origin } this.color = color this.speed = speed this.angle = angle this.context = context this.renderCount = 0 } draw() { this.context.fillStyle = this.color this.context.beginPath() this.context.arc(this.position.x, this.position.y, 2, 0, Math.PI * 2) this.context.fill() } move() { this.position.x = (Math.sin(this.angle) * this.speed) + this.position.x this.position.y = (Math.cos(this.angle) * this.speed) + this.position.y + (this.renderCount * 0.3) this.renderCount++ } } class Boom { constructor ({ origin, context, circleCount = 16, area }) { this.origin = origin this.context = context this.circleCount = circleCount this.area = area this.stop = false this.circles = [] } randomArray(range) { const length = range.length const randomIndex = Math.floor(length * Math.random()) return range[randomIndex] } randomColor() { const range = ['8', '9', 'A', 'B', 'C', 'D', 'E', 'F'] return '#' + this.randomArray(range) + this.randomArray(range) + this.randomArray(range) + this.randomArray(range) + this.randomArray(range) + this.randomArray(range) } randomRange(start, end) { return (end - start) * Math.random() + start } init() { for(let i = 0; i < this.circleCount; i++) { const circle = new Circle({ context: this.context, origin: this.origin, color: this.randomColor(), angle: this.randomRange(Math.PI - 1, Math.PI + 1), speed: this.randomRange(1, 6) }) this.circles.push(circle) } } move() { this.circles.forEach((circle, index) => { if (circle.position.x > this.area.width || circle.position.y > this.area.height) { return this.circles.splice(index, 1) } circle.move() }) if (this.circles.length == 0) { this.stop = true } } draw() { this.circles.forEach(circle => circle.draw()) } } class CursorSpecialEffects { constructor() { this.computerCanvas = document.createElement('canvas') this.renderCanvas = document.createElement('canvas') this.computerContext = this.computerCanvas.getContext('2d') this.renderContext = this.renderCanvas.getContext('2d') this.globalWidth = window.innerWidth this.globalHeight = window.innerHeight this.booms = [] this.running = false } handleMouseDown(e) { const boom = new Boom({ origin: { x: e.clientX, y: e.clientY }, context: this.computerContext, area: { width: this.globalWidth, height: this.globalHeight } }) boom.init() this.booms.push(boom) this.running || this.run() } handlePageHide() { this.booms = [] this.running = false } init() { const style = this.renderCanvas.style style.position = 'fixed' style.top = style.left = 0 style.zIndex = '999999999999999999999999999999999999999999' style.pointerEvents = 'none' style.width = this.renderCanvas.width = this.computerCanvas.width = this.globalWidth style.height = this.renderCanvas.height = this.computerCanvas.height = this.globalHeight document.body.append(this.renderCanvas) window.addEventListener('mousedown', this.handleMouseDown.bind(this)) window.addEventListener('pagehide', this.handlePageHide.bind(this)) } run() { this.running = true if (this.booms.length == 0) { return this.running = false } requestAnimationFrame(this.run.bind(this)) this.computerContext.clearRect(0, 0, this.globalWidth, this.globalHeight) this.renderContext.clearRect(0, 0, this.globalWidth, this.globalHeight) this.booms.forEach((boom, index) => { if (boom.stop) { return this.booms.splice(index, 1) } boom.move() boom.draw() }) this.renderContext.drawImage(this.computerCanvas, 0, 0, this.globalWidth, this.globalHeight) } } const cursorSpecialEffects = new CursorSpecialEffects() cursorSpecialEffects.init() console.log('点击效果程序加载成功(ฅ>ω<*ฅ)') </script> ` hexo.extend.injector.register('body_end' , user_info_js, "default" )
4.2插件(Packages
)测试 上边介绍了脚本(Scripts
)的测试程序,准备写插件的测试程序的时候发现,插件的测试程序其实和上边的测试程序是一模一样的,只不过是一个直接建立脚本文件,一个是另外生成一个插件,那这里就梳理一下从编写测试插件到安装插件然后运运行的过程吧。
shell 1 2 3 4 # 新建文件夹 mkdir hexo-plugins-test # 进入插件文件夹并初始化 cd hexo-plugins-test/ && npm init
创建并初始化相应文件夹后,会生成package.json
文件,文件内容如下,基本都是默认的,这个main
要注意,这里的index.js
为Hexo
使用该插件的时候入口脚本,所以插件中的主程序要写在该文件中。
json 1 2 3 4 5 6 7 8 9 10 11 12 { "name" : "hexo-plugins-test" , "version" : "0.0.0" , "description" : "我的插件测试程序" , "main" : "index.js" , "scripts" : { "test" : "echo \"Error: no test specified\" && exit 1" }, "author" : "qidaink" , "license" : "ISC" }
由于是使用的Vscode
软件作为编程环境,所以直接创建该文件,并添加以下程序。
此测试程序是将鼠标点击效果脚本添加到了过滤器中,在生成器解析后执行该脚本文件,出现的效果应该为在生成器解析后在编程环境的控制台输出一段测试程序加载成功的提示,随后通过启动本地预览,在那个页面点击都会有礼花炸开的效果,并且页面的控制台也会输出一个效果加载成功的提示。
JavaScript 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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 hexo.extend.filter.register('after_generate' , function ( ) { console .log(`测试程序加载成功(ฅ>ω<*ฅ)` ) var user_info_js = ` <script> class Circle { constructor({ origin, speed, color, angle, context }) { this.origin = origin this.position = { ...this.origin } this.color = color this.speed = speed this.angle = angle this.context = context this.renderCount = 0 } draw() { this.context.fillStyle = this.color this.context.beginPath() this.context.arc(this.position.x, this.position.y, 2, 0, Math.PI * 2) this.context.fill() } move() { this.position.x = (Math.sin(this.angle) * this.speed) + this.position.x this.position.y = (Math.cos(this.angle) * this.speed) + this.position.y + (this.renderCount * 0.3) this.renderCount++ } } class Boom { constructor ({ origin, context, circleCount = 16, area }) { this.origin = origin this.context = context this.circleCount = circleCount this.area = area this.stop = false this.circles = [] } randomArray(range) { const length = range.length const randomIndex = Math.floor(length * Math.random()) return range[randomIndex] } randomColor() { const range = ['8', '9', 'A', 'B', 'C', 'D', 'E', 'F'] return '#' + this.randomArray(range) + this.randomArray(range) + this.randomArray(range) + this.randomArray(range) + this.randomArray(range) + this.randomArray(range) } randomRange(start, end) { return (end - start) * Math.random() + start } init() { for(let i = 0; i < this.circleCount; i++) { const circle = new Circle({ context: this.context, origin: this.origin, color: this.randomColor(), angle: this.randomRange(Math.PI - 1, Math.PI + 1), speed: this.randomRange(1, 6) }) this.circles.push(circle) } } move() { this.circles.forEach((circle, index) => { if (circle.position.x > this.area.width || circle.position.y > this.area.height) { return this.circles.splice(index, 1) } circle.move() }) if (this.circles.length == 0) { this.stop = true } } draw() { this.circles.forEach(circle => circle.draw()) } } class CursorSpecialEffects { constructor() { this.computerCanvas = document.createElement('canvas') this.renderCanvas = document.createElement('canvas') this.computerContext = this.computerCanvas.getContext('2d') this.renderContext = this.renderCanvas.getContext('2d') this.globalWidth = window.innerWidth this.globalHeight = window.innerHeight this.booms = [] this.running = false } handleMouseDown(e) { const boom = new Boom({ origin: { x: e.clientX, y: e.clientY }, context: this.computerContext, area: { width: this.globalWidth, height: this.globalHeight } }) boom.init() this.booms.push(boom) this.running || this.run() } handlePageHide() { this.booms = [] this.running = false } init() { const style = this.renderCanvas.style style.position = 'fixed' style.top = style.left = 0 style.zIndex = '999999999999999999999999999999999999999999' style.pointerEvents = 'none' style.width = this.renderCanvas.width = this.computerCanvas.width = this.globalWidth style.height = this.renderCanvas.height = this.computerCanvas.height = this.globalHeight document.body.append(this.renderCanvas) window.addEventListener('mousedown', this.handleMouseDown.bind(this)) window.addEventListener('pagehide', this.handlePageHide.bind(this)) } run() { this.running = true if (this.booms.length == 0) { return this.running = false } requestAnimationFrame(this.run.bind(this)) this.computerContext.clearRect(0, 0, this.globalWidth, this.globalHeight) this.renderContext.clearRect(0, 0, this.globalWidth, this.globalHeight) this.booms.forEach((boom, index) => { if (boom.stop) { return this.booms.splice(index, 1) } boom.move() boom.draw() }) this.renderContext.drawImage(this.computerCanvas, 0, 0, this.globalWidth, this.globalHeight) } } const cursorSpecialEffects = new CursorSpecialEffects() cursorSpecialEffects.init() console.log('点击效果程序加载成功(ฅ>ω<*ฅ)') </script> ` hexo.extend.injector.register('body_end' , user_info_js, "default" ) }, 98 );
由于之前已经登陆过,所以直接执行以下命令。
shell
出现以下提示信息代表发布成功。
shell 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 npm notice npm notice package: hexo-plugins-test@0.0.0 npm notice === Tarball Contents === npm notice 5.5kB index.js npm notice 244B package.json npm notice === Tarball Details === npm notice name: hexo-plugins-test npm notice version: 0.0.0 npm notice package size: 1.8 kB npm notice unpacked size: 5.8 kB npm notice shasum: 50ce71a91babf482f12b0f54a05a7ad48c3d8298 npm notice integrity: sha512-EZwg7y6/fCmv/[...]oUQJ/Y0pKTR9g== npm notice total files: 2 npm notice + hexo-plugins-test@0.0.0
shell 1 2 3 4 # 进入自己的站点根目录(blogRoot代表站点根目录) cd [blogRoot]/ # 安装插件 npm install hexo-plugins-test
若无报错,即可进行下一步,启动本地预览,查看效果。
shell 1 hexo cl && hexo g && hexo s