前言
在 《一篇帶你用 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 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 例項做一些自定義的配置:
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-it
的Github 倉庫程式碼和文件,我們可以大致瞭解到 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、個人伺服器等平臺。
- 一篇帶你用 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,對作者也是一種鼓勵。