【Vue.js】Element Plusで使用されているイケてる検証環境を作ってみる【@vue/repl】

# 概要
- `@vue/repl`はREPL（インタプリタ）でVueコードをブラウザ上で即時に変更/実行可能
- 変更したコードはURLに反映されるため簡単に共有可能
- サンプル紹介（コード、画面）

# @vue/replとは
[@vue/repl](https://github.com/vuejs/repl)はVue.jsのSFCをREPL（Read-Eval-Print Loop）で実行可能とするライブラリです。（REPL：インタプリタ形式でコードを実行）
つまり対話形式でVue.jsのコードを即時で実行可能とするライブラリですね。
画面イメージは下記のようになります。

![image1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/969561/470bc589-b48e-fcdd-1bc1-a9622b103921.png)

画像左のコードを修正すると右側に即時反映されるためデプロイすれば環境なしで手軽にVue.jsのコードを確認できますが、真価はそれだけではありません。記述したコードはURLのパラメータとして即時で反映される仕組みとなっています。つまり、**任意のコードをブラウザ上で記述した後にそのURLを共有するだけで同じコード/実行結果を確認することが可能**です。issueの確認などに便利！

実際にCSSフレームワークの[Element Plus](https://element-plus.org/en-US/)でも実行環境として使用されています。本記事で作成しているアプリケーションもElement Plusの処理を参考にしています。
（参考：[Element Plus Playground](https://github.com/element-plus/element-plus-playground)）

# 使用方法
使用方法はライブラリをnpmなどでインストールして以下のコードを記述するだけです。

```vue
<script setup>
import { Repl } from '@vue/repl'
import '@vue/repl/style.css'
</script>

<template>
  <Repl />
</template>
```

これでコードエディタのUIなども表示してくれますが、この状態では初期表示のコードの変更やライブラリの取り込みは出来ません。

# 拡張方法
```vue
<script setup lang="ts">
import { Repl, ReplStore } from '@vue/repl'
const store = new ReplStore()
</script>

<template>
  <Repl :store="store" />
</template>
```

`Repl`コンポーネントは`store`という形で状態を持っています。この`store`の中にファイル（コード）やVue.jsのランタイムの情報などを持っており、URLでコードを共有可能なのも`store`の情報の共有により実現しています。

全てのオプションではありませんが、今回使用する`store`の状態は以下の様になります。

```yaml
store:
  state:
    activeFile: File             # コードで初期表示しているファイル
    mainFile: string             # エントリとなるメインのファイル
    files:  Record<string, File> # コードで表示しているファイル群。相互参照可能
```

上記の`files`は非表示も可能なため必要な設定を行うファイルの追加と初期コードの変更などを行いそれを各項目に設定すれば任意の環境を作成することが可能です。Element Plusは`store`ごと作り替える方法で実現していますが、制御が複雑になるため今回は`store`のプロパティを直接いじって変更することとします。

# 実践
今回はVue.js Japanでお馴染みの[kazupon](https://github.com/kazupon)氏が作成された国際化ライブラリの[vue-i18n](https://github.com/intlify/vue-i18n-next)の実行環境をサンプルとして作成します。

サンプル画面（右の結果が表示されない場合は少し待機してみてください）

https://esttom.github.io/repl-test/

サンプルコード

https://github.com/esttom/repl-test

Replでvue-i18nを使用するためにはダウンロードすることと、Composition APIのセットアップ（インストール）が必要です。幸い、ライブラリの取得方法は既に用意されています。

```ts
import { ReplStore } from '@vue/repl'
const store = new ReplStore()
store.setImportMap({
  imports: {
    'vue-i18n': 'https://unpkg.com/vue-i18n@9/dist/vue-i18n.esm-browser.prod.js'
  }
})
```

画面上でも確認できますが、import-mapでCDNからビルド済みのJSファイルを取得可能です。

続いてvue-i18nのインストールとなります。通常Vueのインスタンス生成時に行うもののですが、既に作成されているため以下の様な形式でインストール関数を作成します。

```ts
import { getCurrentInstance } from 'vue'
import { createI18n } from 'vue-i18n'

let installed = false

export function i18nSetup() {
  if (installed) return
  // 通常のi18nの設定を行う
  const i18n = createI18n(...)

  // Vueのインスタンスを取得
  const instance = getCurrentInstance()
  // i18nのインストール
  instance.appContext.app.use(i18n)
  installed = true
}
```

作成したインストール関数はVueファイル内で呼び出せば良いのですが、削除されると動作しなくなる上に動作検証で見える必要はありません。そこで初期表示として見せるApp.vueをラップするVueファイルを作成します。

```vue
<script setup lang="ts">
import App from './App.vue'
import { i18nSetup } from './i18n.ts'
i18nSetup()
</script>

<template>
  <App />
</template>
```

このファイルでは先ほど作成したインストール関数（i18n.ts、命名は後述）の実行とApp.vueの内包を行います。これでApp.vueではvue-i18nを使用可能となります。

ここまでで作成したファイルはvue-i18nインストールファイル、インストール実行用Vueファイル、初期表示のApp.vueの3種類となりました。ここでstoreの設定を再掲します。

```yaml
store:
  state:
    activeFile: File             # コードで初期表示しているファイル
    mainFile: string             # エントリとなるメインのファイル
    files:  Record<string, File> # コードで表示しているファイル群。相互参照可能
```

対応関係としてはmainFileがインストール実行用のVueファイル、activeFileがApp.vue、filesが3種類のファイル全てとなります。設定は以下のとおりです（一部省略）。App.vueは初期表示のパターンとURLからパラメータを貰うパターンの二つが存在しますが、ここでは省略しています。必要な場合はサンプルコードを確認してください。（ユーザ記述のファイルを復元しているイメージ）

```ts
import { ReplStore, File, compileFile } from '@vue/repl'
import i18nCode from './i18n.ts?raw' // 文字列でも良いがファイルで取り込み
import mainCode from './main.vue?raw'
import welcomeCode from './App.vue?raw'

const store = new ReplStore()
// filesの設定。記述した名前で相互に呼び出しが可能。第三引数で非表示設定
store.state.files['i18n.ts'] = new File('i18n.ts', i18nCode, true)
store.state.files['Main.vue'] = new File('Main.vue', mainCode, true)
// 初期表示のファイル。URLのパラメータから復元するためには別途設定が必要
store.state.files['App.vue'] = new File('App.vue', welcomeCode)
// mainFileの指定
store.state.mainFile = 'Main.vue'
// 初回は明示でcompileする
for (const file of Object.values(store.state.files)) {
  compileFile(store, file)
}
// 初期表示ファイルの指定
store.setActive('App.vue')
```

以上となります。すべて組み合わせたものは上記のサンプルコードをご覧ください。

# 結果
無事vue-i18nの処理が実行できる環境が作成できました。今回は「hello」という文字列を日本語設定であれば「こんにちは」に変換する設定を入れています。

![image2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/969561/ec993403-7a54-f08a-59c0-43a3a4a55464.png)


初期表示では多言語の変換を行う`t`関数を使用していますが、vue-i18nの関数は設定の範囲内なら全て使用可能なため、多言語の設定有無の判定関数である`te`関数などの追加も可能です。

![image3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/969561/1320fb06-5c33-e074-3bb1-df8311d23ec2.png)

こちらのコード追加を行った結果を共有したい場合はURLごとコピーすれば他の方と共有も可能です。下記のリンクは上記のサンプル画面のリンクと同じに見えますがコードと実行結果が`te`関数を追加したものになっています。

https://esttom.github.io/repl-test/#eNqVVMGK1EAQ/ZUiCMnATMLedMiuyuJhFU8ejbBttiaT2U53091ZdxlyGH9hDyp68SaI+Af+TNj/sDqdZDLiIg4MpKpedVfVe13b4KlS8VWNwTJITa5LZcGgrRVwJorjLLAmC04yUVZKagtbqA2eHT0U0MBKywpCSl2U5AgzkUthHMTOweIcuMwZRwIeD0nRLBOZWNUit6UUgNelsS/w5nSN+WVkmS7QLsFYXYpiBttMAGgqRQs4v8Sb1w+2HtK8aXc/7358vPvyjVzYJ87gMYTt7kO7+9XuPoWw3Fuf2/e3YXOeiSYTaeKbpJbIsFgpziySBZCuj062VH0UrpFzGc6gadKEnJPgYcn/AZQF/oEzyDG3cLWo5AVymrSfVzdtoF8qVTelK8ZrpPCGUeg5U0ygwTTx0XuwKAj7TBS8NOsDKLXfXUtGmky6D+aBZ3hRMRVvjBSkh46BrA+QDJaeE+cbWHfOLFhbq8wySWqhLos4l1UyxJ88Si5oEKMdo6kWb7V8Z1DTNVngDiRaGleAi1tDF49iI15Pa61R2DOSFhO5k9OouzDTE2HmGqmXe7RJQPfnaKF0J3GOF6TLFeMGhyBedyeN8nSZr9xTiJwYCQBQriAa82e9On3Ii9/l0Ln7WqI+E4BjwfKbpb9zPno70kmsGxaOzgqNYQUaN/DeBbBhByZAJ75O5rck8Hb3vd19pZfRNesRzXgiAIp70r2GJ0nDZ//RzIYBjU0OXBz/hSB6435UvR0zpU6lsHht3WdMqyByU5rCejasrjsynBheslL8ey3t+act1pMeJ/1GO5THyOaojjjpFeeBe7adud8TffOTXdEVnrobk86YviQyg+Y3E27arA==

Element Plusでは実行するVueランタイムのバージョンも指定できるなどカスタマイズの幅はまだまだあります。issue管理や教育、簡易な実行環境として検討してみてはいかがでしょうか。

# 参考
- [vuejs/repl](https://github.com/vuejs/repl)
- [vue-i18n](https://github.com/intlify/vue-i18n-next)
- [element-plus-playground](https://github.com/element-plus/element-plus-playground)