前言

《一篇带你用 VuePress + Github Pages 搭建博客》中,我们使用 VuePress 搭建了一个博客,最终的效果查看:TypeScript 中文文档

如果我们浏览过 TypeScript 官方文档,我们会发现一个很好用的功能,那就是很多代码块,在悬浮上去的时候都会出现一个 Try 按钮:

点击就会跳转到对应的 Playground,比如图示的按钮跳转的就是这个链接,我们可以在这个 Playground 修改并验证代码效果。

如果我们要实现这样的功能,该怎么实现呢?

思考

我们很容易想到,写一个 VuePress 插件来实现它,这个效果看起来有点像代码复制插件,但细细一想,又非如此。

代码复制插件的实现方式,参考 《从零实现一个 VuePress 插件》,可以在页面渲染完成后,遍历每一个代码块然后插入一个复制按钮,点击复制的时候将代码写入剪切板,但是代码块跳转就不一样了,代码跳转需要我们先写入一个链接地址,然后再渲染按钮,问题是这个链接的地址写在哪里呢?要知道,我们能写的只是一个普通的 markdown 文件呀……

于是我们就想到,是否可以拓展 markdown 的语法呢?就比如正常的代码块写法是:

```typescript
const message = "Hello World!";
```

为了实现这个效果,我们是否可以这样写:

```typescript
// try-link: https://www.baidu.com
const message = 'Hello World!';
```

但是渲染的时候,并不渲染 try-link 这行注释,而是变成这样的效果:

当点击 Try 的时候,跳转到对应的链接。

当然效果更好的话,可以在鼠标悬浮在代码块上方的时候,才显示这个 Try 按钮,类似于这种效果:

markdown-it

查阅 VuePress 的官方文档,我们可以知道:VuePress 使用 markdown-it来渲染 Markdown,那markdown-it是什么呢?查阅 markdown-it 的 Github 仓库,可以看到这样一段介绍:

简单的来说,markdown-it就是一个 markdown 渲染器,可以将 markdown 渲染成 html 等,而且 markdown-it 支持写插件拓展功能,实际上,VuePress 项目中的 markdown 文件为什么能支持写 Vue 组件,就是因为 VuePress 写了插件支持了 Vue 语法,那我们是不是也可以拓展 markdown 的语法呢?

还好在 VuePress 文档里,提供了配置,可以自定义 markdown-it 插件:

module.exports = {
  markdown: {
    // markdown-it-anchor 的选项
    anchor: { permalink: false },
    // markdown-it-toc 的选项
    toc: { includeLevel: [1, 2] },
    extendMarkdown: md => {
      // 使用更多的 markdown-it 插件!
      md.use(require('markdown-it-xxx'))
    }
  }
}

引入的方法知道了,但怎么写这个 markdown-it 插件呢?

markdown-it 插件

查阅 markdown-itGithub 仓库代码文档,我们可以大致了解到 markdown-it的工作原理,其转换过程类似于 Babel,先转换成抽象语法树,然后生成对应的代码,简单的概括就是分为 Parse 和 Render 两个过程。

这点我们查看源码也可以看到:

MarkdownIt.prototype.render = function (src, env) {
  env = env || {};
  return this.renderer.render(this.parse(src, env), this.options, env);
};

所以这里我们解决问题的思路有两个,一个是在 Parse 过程中处理,一个在 Render 过程中处理,为了简单起见,我决定直接处理 Render 过程,查看 Render 的源码,我们可以看到 Render 里其实已经根据一些固定的类型写了默认 Rules(渲染规则),就比如关于代码块:

default_rules.fence = function (tokens, idx, options, env, slf) {
  var token = tokens[idx],
      info = token.info ? unescapeAll(token.info).trim() : '',
      langName = '',
      langAttrs = '',
      highlighted, i, arr, tmpAttrs, tmpToken;

  if (info) {
    // ...
  }

  if (options.highlight) {
    highlighted = options.highlight(token.content, langName, langAttrs) || escapeHtml(token.content);
  } else {
    highlighted = escapeHtml(token.content);
  }

  if (highlighted.indexOf('<pre') === 0) {
    return highlighted + '\n';
  }

  if (info) {
    //...
  }


  return  '<pre><code' + slf.renderAttrs(token) + '>'
        + highlighted
        + '</code></pre>\n';
};

我们可以覆盖这个规则,参照 markdown-it 提供的插件编写原则,我们可以这样写:

# 获取 md 实例后
md.renderer.rules.fence = function (tokens, idx, options, env, self) {
    // ...
};

为了再省事一点,我准备直接获取最后渲染的 HTML 结果,它是一个字符串,然后匹配 //try-link: xxx生成的 HTML,替换成一个 <a>链接,我们查看下 //try-link: xxx这句注释生成的 HTML:

修改下 config.js文件:

module.exports = {
    markdown: {
      extendMarkdown: md => {
        md.use(function(md) {
          const fence = md.renderer.rules.fence
          md.renderer.rules.fence = (...args) => {
            let rawCode = fence(...args);
            rawCode = rawCode.replace(/<span class="token comment">\/\/ try-link https:\/\/(.*)<\/span>\n/ig, '<a href="$1" class="try-button" target="_blank">Try</a>');
            return `${rawCode}`
          }
              })
      }
    }
}

这里为了简洁,我没有将 <a>链接的样式直接内联写入其中,而是加了一个类,那在哪里写这个类的样式呢?

VuePress 提供了 docs/.vuepress/styles/index.styl文件,作为将会被自动应用的全局样式文件,会生成在最终的 CSS 文件结尾,具有比默认样式更高的优先级。

所以我们在 index.styl文件下写入样式:

// 默认样式
.try-button {
    position: absolute;
    bottom: 1em;
    right: 1em;
    font-weight: 100;
    border: 1px solid #719af4;
    border-radius: 4px;
    color: #719af4;
    padding: 2px 8px;
    text-decoration: none;
    transition-timing-function: ease;
    transition: opacity .3s;
    opacity: 0;
}

// hover 样式
.content__default:not(.custom) a.try-button:hover {
    background-color: #719af4;
    color: #fff;
    text-decoration: none;
}

有的时候,自动编译可能不会生效,我们可以重新运行 yarn run docs:dev

此时已经可以正常显示按钮了(默认样式透明度为 0,这里为了截图强行设置透明度为 1):

接下来我们要实现,鼠标悬浮在代码块的时候,才显示这个按钮,这里我们可以借助 《从零实现一个 VuePress 插件》中的方法,在页面 mounted 的时候,获取所有的代码块元素,然后添加事件,我们再修改下 config.js文件:

module.exports = {
    plugins: [
      (options, ctx) => {
        return {
          name: 'vuepress-plugin-code-try',
          clientRootMixin: path.resolve(__dirname, 'vuepress-plugin-code-try/index.js')
        }
      }
    ],
    markdown: {
      extendMarkdown: md => {
        md.use(function(md) {
          const fence = md.renderer.rules.fence
          md.renderer.rules.fence = (...args) => {
            let rawCode = fence(...args);
            rawCode = rawCode.replace(/<span class="token comment">\/\/ try-link https:\/\/(.*)<\/span>\n/ig, '<a href="$1" class="try-button" target="_blank">Try</a>');
            return `${rawCode}`
          }
              })
      }
    }
}

然后在同级目录config.js下新建一个 vuepress-plugin-code-try目录,然后新建一个 index.js文件:

export default {
  mounted () {
    setTimeout(() => {
        document.querySelectorAll('div[class*="language-"] pre').forEach(el => {
            if (el.querySelector('.try-button')) {
                el.addEventListener('mouseover', () => {
                    el.querySelector('.try-button').style.opacity = '1';
                })
                el.addEventListener('mouseout', () => {
                    el.querySelector('.try-button').style.opacity = '0';
                })
            }
        })
    }, 100)
  }
}

此时,再运行项目,我们就实现了最初想要的效果:

系列文章

博客搭建系列是我至今写的唯一一个偏实战的系列教程,讲解如何使用 VuePress 搭建博客,并部署到 GitHub、Gitee、个人服务器等平台。

  1. 一篇带你用 VuePress + GitHub Pages 搭建博客
  2. 一篇教你代码同步 GitHub 和 Gitee
  3. 还不会用 GitHub Actions ?看看这篇
  4. Gitee 如何自动部署 Pages?还是用 GitHub Actions!
  5. 一份前端够用的 Linux 命令
  6. 一份简单够用的 Nginx Location 配置讲解
  7. 一篇从购买服务器到部署博客代码的详细教程
  8. 一篇域名从购买到备案到解析的详细教程
  9. VuePress 博客优化之 last updated 最后更新时间如何设置
  10. VuePress 博客优化之添加数据统计功能
  11. VuePress 博客优化之开启 HTTPS
  12. VuePress 博客优化之开启 Gzip 压缩
  13. 从零实现一个 VuePress 插件

微信:「mqyqingfeng」,加我进冴羽唯一的读者群。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

03-05 23:08