Highlight.js のサイズを抑える
- 公開日:
- 更新日:
単に highlight.js 全体を require してしまうと、自分が記事で扱わないであろう言語のハイライト用コードが大量にバンドルされてしまい、JS ファイルが肥大化するだけでなく、言語の自動検出機能の精度も下がります。
必要な言語のみ処理させるには、highlight.js のコアを require して、使う言語を一つ一つ登録します。
const hljs = require('highlight.js/lib/core')
hljs.registerLanguage('xml', require('highlight.js/lib/languages/xml'))
hljs.registerLanguage('scss', require('highlight.js/lib/languages/scss'))
hljs.registerLanguage('javascript', require('highlight.js/lib/languages/javascript'))
hljs.registerLanguage('shell', require('highlight.js/lib/languages/shell'))
hljs.registerLanguage('bash', require('highlight.js/lib/languages/bash'))
hljs.registerLanguage('ini', require('highlight.js/lib/languages/ini'))
subLanguage に注意
登録する言語ファイルには、subLanguage が指定されていることがあります。例えば、シェル用の highlight.js/lib/languages/shell.js
を見ると、subLanguage として bash
が指定されています。
shell.js
は文頭の「$」「#」などに色を付けるだけで、その他の部分は bash.js
に丸投げしているので、bash も登録しておく必要があります。
$ #ここから .bash クラスが付いているので bash 扱い
別名の登録
SCSS 用の scss.js
は subLanguage を指定しておらず、単体で CSS と SCSS のハイライトができます。
しかし、HTML, XML 用の xml.js
では、subLanguage として css
を指定しているため、以下のようなコードのハイライトが上手くいきません。
<style>
/* (別名を登録済みなのでハイライトされてるけど) */
/* ここから */
.foo {
background-color: black;
color: white;
}
/* ここまでハイライトされない*/
</style>
そこで、hljs.registerAliases()
を使って、scss
の別名として css
を登録します。第一引数は文字列の配列にすることもできます。
hljs.registerAliases('css', { languageName: 'scss' })
これで、HTML コード内の CSS もハイライトされるようになります。
css.js
と scss.js
は 430 行目ぐらいまで全く同じ内容で、擬似セレクタや CSS プロパティなどのリストが定義されています。css.js
のバンドルを避けることで、6.6 KB ほど削減することができました。
そもそもバンドルしない
Jamstack なブログでは JavaScript の実行結果をそのまま配信できるので、記事データを取得して Highlight.js で処理したものを payload に渡せば、そもそも Highlight.js をバンドルしなくて済みます。
処理内容を統一するため、モジュール化します。
// assets/js/filterPost.js
const hljs = require('highlight.js/lib/core')
hljs.registerLanguage('xml', require('highlight.js/lib/languages/xml'))
hljs.registerLanguage('scss', require('highlight.js/lib/languages/scss'))
hljs.registerLanguage('javascript', require('highlight.js/lib/languages/javascript'))
hljs.registerLanguage('shell', require('highlight.js/lib/languages/shell'))
hljs.registerLanguage('bash', require('highlight.js/lib/languages/bash'))
hljs.registerLanguage('ini', require('highlight.js/lib/languages/ini'))
hljs.registerAliases('css', { languageName: 'scss' })
const { decodeHTML } = require('entities')
exports.filterPost = post => {
post.content = post.content.replace(/<pre><code>(.+?)<\/code><\/pre>/gims, (match, p1) => {
const { language, value } = hljs.highlightAuto(decodeHTML(p1))
const langClass = language ? ` lang-${language}` : ''
return `<pre><code class="hljs${langClass}">${value}</code></pre>`
})
}
記事用テンプレートでは、開発時のみこのスクリプトを require します。generate 時は payload からデータを受け取ります。
<!-- pages/_postId/index.vue -->
<template>
...
</template>
<script>
let asyncData = ({ payload }) => payload
if (process.env.NODE_ENV !== 'production') {
const { filterPost } = require('~/assets/js/filterPost')
asyncData = async ({ $api, params }) => {
const { 'data': post } = await $api.get(`posts/${params.postId}`)
filterPost(post)
return { post }
}
}
export default {
scrollToTop: true,
asyncData,
head() {
return {
title: this.post.title,
}
},
}
</script>
記事のプレビューページでは、mounted で記事データを取ってきて、その場でハイライトすることになるので、Highlight.js をバンドルする必要があります。
データには created でも触れるのですが、開発時にサーバーとクライアントで描画内容が違うと怒られがちなので、mounted での取得で良いと思います。
<script>
const { filterPost } = require('~/assets/js/filterPost')
export default {
scrollToTop: true,
data() {
return {
post: {},
loading: true,
}
},
mounted() {
const { id, draftKey } = this.$route.query
this.$axios.get('/api/preview', { params: { id, draftKey }})
.then(({ 'data': post }) => {
filterPost(post)
this.post = post
this.loading = false
})
.catch(({ response }) => {
this.$nuxt.error({
statusCode: response.status,
message: response.statusText,
})
})
},
head() {
return {
title: this.post.title,
}
},
}
</script>
Nuxt.js ではページごとにスクリプトが分割されるので、preview ページにアクセスしない限り、Highlight.js 入りのスクリプトがダウンロードされることはありません。実質バンドルしてないみたいな感じになります。