Vue.js のサイト上での使用は NG だけど、Vue.js での開発はあきらめなかった話

半分ネタですが、切実な必要があり試行錯誤した結果です。


## どういうこと？

比較的ページ数のあるサイトの作成において、

0. 汎用パーツのコーディング品質担保のため、コンポーネントベースの開発が理想的だった
0. Vue.js のコンポーネントを利用した開発を検討 
0. **さまざまな理由で**、サイトでの Vue.js での利用が **NG** になった :scream::scream::scream: 
0. 開発は Vue のコンポーネントを利用し、プリレンダリング出力した静的 HTML と CSS をプロダクション環境に使うことにした :sunglasses: 


通常プリレンダリングは SEO 対策のために実施するものですが、今回はあくまでもコンポーネントベースでの開発、および静的 HTML と CSS の生成を目的としています。

## サンプル

サンプルコード一式は、こちらになります。
:octocat: [https://github.com/megurock/vue-as-template-engine](https://github.com/megurock/vue-as-template-engine)



## こういう感じでコーディングします

特に意味はありませんが、 HelloWorld コンポーネントを [v-for](https://jp.vuejs.org/v2/api/#v-for) と Pug の [while 構文](https://pugjs.org/language/iteration.html#while) でそれぞれ書いてみた例です。この比較では v-for の方がシンプルで使い勝手が良いですが、プリレンダリングされてしまえば、どちらにも差異はありません。


```pug:index.pug
section
  h2 v-for でループ
  hello-world(msg="Hello, Vue!" v-for="n in 2")

section 
  h2 Pug の while 構文でループ
  - var count = 0
    while ++count <= 2
      hello-world(msg="Hello, Pug!!")
```


こちらは [単一ファイルコンポーネント](https://jp.vuejs.org/v2/guide/single-file-components.html) で作成した　HelloWorld コンポーネントです。msg プロパティで受け取った文字列を、`<p class="hello-world">` として出力します。

```vue:HelloWorld.vue
<template lang="pug">
p.hello-world {{ msg }}
</template>

<script lang="ts">
export default {
  props: {
    msg: { type: String, default: 'Hello, World!' }
  }
}
</script>

<style lang="scss">
.hello-world { color: #42b983; }
</style>
```


## プリレンダーするとこうなります :smile: 

プリレンダーを実行することで、以下の html と css が出力されます。

```bash
$ yarn prerender
```

```html:prerendered/index.html
<section>
  <h2>v-for でループ</h2>
  <p class="hello-world">Hello, Vue!</p>
  <p class="hello-world">Hello, Vue!</p>
</section>
<section>
  <h2>Pug の while 構文でループ</h2>
  <p class="hello-world">Hello, Pug!!</p>
  <p class="hello-world">Hello, Pug!!</p>
</section>
```

```css:components.css
.hello-world { color: #42b983; }
```

## プリレンダリングについて

Vue.js のプリレンダリングには、[Prerender SPA Plugin](https://github.com/chrisvfritz/prerender-spa-plugin#readme) や、[Nuxt の generate](https://ja.nuxtjs.org/api/configuration-generate) を使った実装が有名だと思いますが、これらは SPA（1 ページにひとつの単一ファイルコンポーネント）を想定しているようなので、今回は [prerenderer プラグイン](https://github.com/Tribex/prerenderer#readme) を使いました。

[プリレンダーのスクリプト](https://github.com/megurock/vue-as-template-engine/blob/720717ab864ec83a3f138cad2ed8eeb83519132f/prerender.js#L37-L62)は、ほぼ [サンプルコード](https://github.com/Tribex/prerenderer#example-prerenderer-usage) のままです。Puppeteer と JSDOM のいずれかを使ったレンダラ―を選択できるのですが、JSDOM 実装の方は高速で、一方 Puppeteer の方は信頼度が高いとのことです。

ちょっとした注意点なのですが、prerenderer プラグインはまだ動作が安定していないとのことらしく、実際に大量のページを対象にプリレンダリングを実行すると、Node の非同期処理でタイムアウトエラーを起こしてしまうことがありました。回避策として、対象ページの指定に [glob](https://github.com/isaacs/node-glob)  を適用し、適宜ファイル数を絞りつつ実行できるようにしました。

### ポストプロセス

プロダクション環境には、Vue.js や コンポーネント用のスクリプトは必要ないので、プリレンダリングされた HTML タグから `<script>` タグの記述を削除してからファイルを書き出ししています。

```js:prerender.js
/**
 * <script> タグをすべて削除
 * （必要な JS がある場合は、もう少し丁寧な正規表現を書いてください）
 */
function postProcessHtml(html) {
  return html
    .replace(/\s*<script[^<]*<\/script>\s*/igm, '')
    .trim()
}
```

## JS はどうするのか？

鋭い人は「HTML と CSS は良いとしても、JS はどうするの？」と思ったかもしれませんが、単一ファイルコンポーネントの `<script>` ではタグから受け取るプロパティと、必要であれば算出プロパティを定義するだけで、コンポーネントの振舞いは書きません。潔くあきらめて**別途** JS を書きましょう。:grin: 

以上になります。

---
[:christmas_tree: FORK Advent Calendar 2018](/advent-calendar/2018/fork)
:arrow_left: 13日目 [Google Apps Scriptでの申請フォームの作成とハングアウトチャットとの連携](/pino_dmdm/items/b5f0adf88bec1a31c7e2) @pino_dmdm
:arrow_right: 15日目 [Photoshopで画像をスライスするJS](/re_sai/items/8eb316def2f85cda7f1b) @re_sai