前言
在 《一篇带你用 VuePress + Github Pages 搭建博客》中,我们使用 VuePress 搭建了一个博客,最终的效果查看:TypeScript 中文文档。
如果我们浏览过 TypeScript 官方文档,我们会发现一个很好用的功能,那就是很多代码块,在悬浮上去的时候都会出现一个 Try 按钮:

点击就会跳转到对应的 Playground,比如图示的按钮跳转的就是这个链接,我们可以在这个 Playground 修改并验证代码效果。
如果我们要实现这样的功能,该怎么实现呢?
思考
我们很容易想到,写一个 VuePress 插件来实现它,这个效果看起来有点像代码复制插件,但细细一想,又非如此。
代码复制插件的实现方式,参考 《从零实现一个 VuePress 插件》,可以在页面渲染完成后,遍历每一个代码块然后插入一个复制按钮,点击复制的时候将代码写入剪切板,但是代码块跳转就不一样了,代码跳转需要我们先写入一个链接地址,然后再渲染按钮,问题是这个链接的地址写在哪里呢?要知道,我们能写的只是一个普通的 markdown 文件呀……
于是我们就想到,是否可以拓展 markdown 的语法呢?就比如正常的代码块写法是:
1```typescript 2const message = "Hello World!"; 3```
为了实现这个效果,我们是否可以这样写:
1```typescript 2// try-link: https://www.baidu.com 3const message = 'Hello World!'; 4```
但是渲染的时候,并不渲染 try-link 这行注释,而是变成这样的效果:

当点击 Try 的时候,跳转到对应的链接。
当然效果更好的话,可以在鼠标悬浮在代码块上方的时候,才显示这个 Try 按钮,类似于这种效果:

markdown-it
查阅 VuePress 的官方文档,我们可以知道:VuePress 使用 markdown-it来渲染 Markdown,那markdown-it是什么呢?查阅 markdown-it 的 Github 仓库,可以看到这样一段介绍:
Markdown parser done right. Fast and easy to extend.
简单的来说,markdown-it就是一个 markdown 渲染器,可以将 markdown 渲染成 html 等,而且 markdown-it 支持写插件拓展功能,实际上,VuePress 项目中的 markdown 文件为什么能支持写 Vue 组件,就是因为 VuePress 写了插件支持了 Vue 语法,那我们是不是也可以拓展 markdown 的语法呢?
还好在 VuePress 文档里,提供了配置,可以自定义 markdown-it 插件:
VuePress 使用 markdown-it (opens new window)来渲染 Markdown,上述大多数的拓展也都是通过自定义的插件实现的。想要进一步的话,你可以通过 .vuepress/config.js 的 markdown 选项,来对当前的 markdown-it 实例做一些自定义的配置:
1module.exports = { 2 markdown: { 3 // markdown-it-anchor 的选项 4 anchor: { permalink: false }, 5 // markdown-it-toc 的选项 6 toc: { includeLevel: [1, 2] }, 7 extendMarkdown: md => { 8 // 使用更多的 markdown-it 插件! 9 md.use(require('markdown-it-xxx')) 10 } 11 } 12}
引入的方法知道了,但怎么写这个 markdown-it 插件呢?
markdown-it 插件
查阅 markdown-it 的Github 仓库代码和文档,我们可以大致了解到 markdown-it的工作原理,其转换过程类似于 Babel,先转换成抽象语法树,然后生成对应的代码,简单的概括就是分为 Parse 和 Render 两个过程。
这点我们查看源码也可以看到:
1MarkdownIt.prototype.render = function (src, env) { 2 env = env || {}; 3 return this.renderer.render(this.parse(src, env), this.options, env); 4};
所以这里我们解决问题的思路有两个,一个是在 Parse 过程中处理,一个在 Render 过程中处理,为了简单起见,我决定直接处理 Render 过程,查看 Render 的源码,我们可以看到 Render 里其实已经根据一些固定的类型写了默认 Rules(渲染规则),就比如关于代码块:
1default_rules.fence = function (tokens, idx, options, env, slf) { 2 var token = tokens[idx], 3 info = token.info ? unescapeAll(token.info).trim() : '', 4 langName = '', 5 langAttrs = '', 6 highlighted, i, arr, tmpAttrs, tmpToken; 7 8 if (info) { 9 // ... 10 } 11 12 if (options.highlight) { 13 highlighted = options.highlight(token.content, langName, langAttrs) || escapeHtml(token.content); 14 } else { 15 highlighted = escapeHtml(token.content); 16 } 17 18 if (highlighted.indexOf('<pre') === 0) { 19 return highlighted + '\n'; 20 } 21 22 if (info) { 23 //... 24 } 25 26 27 return '<pre><code' + slf.renderAttrs(token) + '>' 28 + highlighted 29 + '</code></pre>\n'; 30};
我们可以覆盖这个规则,参照 markdown-it 提供的插件编写原则,我们可以这样写:
1# 获取 md 实例后 2md.renderer.rules.fence = function (tokens, idx, options, env, self) { 3 // ... 4};
为了再省事一点,我准备直接获取最后渲染的 HTML 结果,它是一个字符串,然后匹配 //try-link: xxx生成的 HTML,替换成一个 <a>链接,我们查看下 //try-link: xxx这句注释生成的 HTML:

修改下 config.js文件:
1module.exports = { 2 markdown: { 3 extendMarkdown: md => { 4 md.use(function(md) { 5 const fence = md.renderer.rules.fence 6 md.renderer.rules.fence = (...args) => { 7 let rawCode = fence(...args); 8 rawCode = rawCode.replace(/<span class="token comment">\/\/ try-link https:\/\/(.*)<\/span>\n/ig, '<a href="$1" class="try-button" target="_blank">Try</a>'); 9 return `${rawCode}` 10 } 11 }) 12 } 13 } 14}
这里为了简洁,我没有将 <a>链接的样式直接内联写入其中,而是加了一个类,那在哪里写这个类的样式呢?
VuePress 提供了 docs/.vuepress/styles/index.styl文件,作为将会被自动应用的全局样式文件,会生成在最终的 CSS 文件结尾,具有比默认样式更高的优先级。
所以我们在 index.styl文件下写入样式:
1// 默认样式 2.try-button { 3 position: absolute; 4 bottom: 1em; 5 right: 1em; 6 font-weight: 100; 7 border: 1px solid #719af4; 8 border-radius: 4px; 9 color: #719af4; 10 padding: 2px 8px; 11 text-decoration: none; 12 transition-timing-function: ease; 13 transition: opacity .3s; 14 opacity: 0; 15} 16 17// hover 样式 18.content__default:not(.custom) a.try-button:hover { 19 background-color: #719af4; 20 color: #fff; 21 text-decoration: none; 22}
有的时候,自动编译可能不会生效,我们可以重新运行 yarn run docs:dev。
此时已经可以正常显示按钮了(默认样式透明度为 0,这里为了截图强行设置透明度为 1):

接下来我们要实现,鼠标悬浮在代码块的时候,才显示这个按钮,这里我们可以借助 《从零实现一个 VuePress 插件》中的方法,在页面 mounted 的时候,获取所有的代码块元素,然后添加事件,我们再修改下 config.js文件:
1module.exports = { 2 plugins: [ 3 (options, ctx) => { 4 return { 5 name: 'vuepress-plugin-code-try', 6 clientRootMixin: path.resolve(__dirname, 'vuepress-plugin-code-try/index.js') 7 } 8 } 9 ], 10 markdown: { 11 extendMarkdown: md => { 12 md.use(function(md) { 13 const fence = md.renderer.rules.fence 14 md.renderer.rules.fence = (...args) => { 15 let rawCode = fence(...args); 16 rawCode = rawCode.replace(/<span class="token comment">\/\/ try-link https:\/\/(.*)<\/span>\n/ig, '<a href="$1" class="try-button" target="_blank">Try</a>'); 17 return `${rawCode}` 18 } 19 }) 20 } 21 } 22}
然后在同级目录config.js下新建一个 vuepress-plugin-code-try目录,然后新建一个 index.js文件:
1export default { 2 mounted () { 3 setTimeout(() => { 4 document.querySelectorAll('div[class*="language-"] pre').forEach(el => { 5 if (el.querySelector('.try-button')) { 6 el.addEventListener('mouseover', () => { 7 el.querySelector('.try-button').style.opacity = '1'; 8 }) 9 el.addEventListener('mouseout', () => { 10 el.querySelector('.try-button').style.opacity = '0'; 11 }) 12 } 13 }) 14 }, 100) 15 } 16}
此时,再运行项目,我们就实现了最初想要的效果:

系列文章
博客搭建系列是我至今写的唯一一个偏实战的系列教程,讲解如何使用 VuePress 搭建博客,并部署到 GitHub、Gitee、个人服务器等平台。
- 一篇带你用 VuePress + GitHub Pages 搭建博客
- 一篇教你代码同步 GitHub 和 Gitee
- 还不会用 GitHub Actions ?看看这篇
- Gitee 如何自动部署 Pages?还是用 GitHub Actions!
- 一份前端够用的 Linux 命令
- 一份简单够用的 Nginx Location 配置讲解
- 一篇从购买服务器到部署博客代码的详细教程
- 一篇域名从购买到备案到解析的详细教程
- VuePress 博客优化之 last updated 最后更新时间如何设置
- VuePress 博客优化之添加数据统计功能
- VuePress 博客优化之开启 HTTPS
- VuePress 博客优化之开启 Gzip 压缩
- 从零实现一个 VuePress 插件
微信:「mqyqingfeng」,加我进冴羽唯一的读者群。
如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。
