category icon
2022-08-18
JavaScript - Nuxt3

Nuxt3 で Monaco Editor を使う方法

nuxt
3.0.0-rc.8
monaco-editor
0.34.0
profile
hikaru
Software Developer / DIY'er

VS Code のエディタ部分をブラウザで動かせちゃう Microsoft 製の Monaco Editor を Nuxt3 で使う方法です。

(ぶっちゃけ、今回のコードにはシックリきていなくて… "とりあえずこれで動くぞー" といった感じのレベルになってます。もっとスマートな方法があればゴメンナサイ!)

なお、コーディングスタイルは Nuxt3 で標準的である、Vue3 Composition API 形式で統一しています。

まずはプロジェクトの準備

(1) Nuxt3 プロジェクトの作成

Nuxt3 の公式情報に従い、プロジェクトを作成します。

shell
# nuxt3の新規プロジェクトを作成
$ npx nuxi init nuxt3-monaco-example

# カレントディレクトリを移動
$ cd nuxt3-monaco-example

# 各種パッケージのインストール
$ npm i

# 開発環境で実行 (Ctrl+Cで停止)
$ npm run dev

(2) Monaco Editor パッケージをインストール

Monaco Editor に必要な公式パッケージを追加します。

shell
# Monaco Editorに必要な公式パッケージを追加
$ npm install monaco-editor

この記事を執筆している時点での最新バージョンである nuxt:3.0.0-rc.8 と monaco-editor:0.34.0 がインストールされました。

実装コード

(1) Monaco Editor 用のコンポーネントを作成する

Vue3 Composition API 形式で、Monaco Editor を内包するコンポーネントを作成します。
components/AppMonacoEditor.vue ファイルを作成して、以下のように実装します。

./components/AppMonacoEditor.vue
<script lang="ts" setup>
import * as monaco from "monaco-editor";

/** コンポーネントのプロパティ */
const props = defineProps({
  modelValue: String, // v-model用
  language: { type: String, default: `javascript` },
});

/** 親コンポーネントに発行するイベント定義 */
const emits = defineEmits([`change`, `update:modelValue`]);

/** テンプレート参照: MonacoEditor用のDOM要素 */
const appMonacoEditorEl = ref<HTMLElement>();

/** 生成したMonacoEditorのインスタンス */
let monacoEditor: monaco.editor.IStandaloneCodeEditor | undefined = undefined;

// modelValue(v-model)の監視設定
watch(
  () => props.modelValue,
  (newVal, oldVal) => {
    // エディタの初期化済みチェック
    if (!monacoEditor) {
      return;
    }

    // MonacoEditor側に反映する
    if (monacoEditor.getValue() !== newVal) {
      monacoEditor.setValue(newVal ?? ``);
    }
  }
);

onMounted(() => {
  // MonacoEditorを生成する
  monacoEditor = monaco.editor.create(appMonacoEditorEl.value, {
    language: props.language,
    value: props.modelValue,
    automaticLayout: true, // 親要素のレイアウト変更に追従させる
  });

  // MonacoEditorのテキスト変更イベントを登録
  monacoEditor.onDidChangeModelContent((event) => {
    // エディタ上のテキストを取得
    const value = monacoEditor.getValue();

    // 変更イベントを発行
    if (props.modelValue !== value) {
      emits(`change`, value, event);
      emits(`update:modelValue`, value, event);
    }
  });
});
</script>

<template>
  <div ref="appMonacoEditorEl" style="width: 100%; height: 100%"></div>
</template>

注意点として、monaco-editor パッケージを静的 import しているこのコンポーネントは、サーバー上でレンダリングできません。(SSR や SSG モードだとサーバーでレンダリングされる)

このコンポーネントを呼び出す親コンポーネント内で ClientOnly + Lazy(遅延読み込み) を行うことで、SSR や SSG モードでもブラウザ上でのみレンダリングするようにする必要があります。

(2) app.vue の書き換え

親コンポーネントにあたる app.vue を以下のように書き換えます。

./app.vue
<script lang="ts" setup>
const textRef = ref(`# はじめに\n\n- これは箇条書き\n- 2つ目の箇条書き\n`);
</script>

<template>
  <div style="width: 640px; height: 480px; border: 1px solid">
    <ClientOnly>
      <LazyAppMonacoEditor
        language="markdown"
        v-model="textRef"
      ></LazyAppMonacoEditor>
    </ClientOnly>
  </div>
</template>

LazyAppMonacoEditor コンポーネントは、Lazy(遅延読み込み)を指定した AppMonacoEditor コンポーネントです。このように記載することで Nuxt3 が勝手に AppMonacoEditor コンポーネントを遅延読み込みしてくれます。

ClientOnly は SSR と SSG モードの時にサーバーではレンダリングせず、ブラウザでのみレンダリングするようにする指定です。

完成・動作確認

Monaco Editor に適当な Markdown テキストを張り付けてみた

完成するとこんな感じ。ブラウザ上で VSCode (Monaco Editor) が動作するようになります。

なお、ブラウザのデベロッパーツールに表示されている Could not create web worker(s). Falling back to loading web worker code in main thread, which might cause UI freezes. って警告は、『Web ワーカー作成できんかったからメインスレッドで代用するね~。UI フリーズするかもよ』って内容です。

これ、解決できなくて・・・
Monaco Editor のドキュメント を読んでもうまく解決できませんでした。とりあえずメインスレッドでも動作できるし、プロダクトに影響がなければこのまま使用しようかなと思います。

おわり。

おすすめ本コーナー

最近、DIY ブログ用に Amazon アソシエイトを登録しました~!DIY で使った商品リンクを載せて収益を得ようという魂胆だったのですが、『この記事のような技術記事にも本載せられるやん』って気が付いたので、さっそくのおすすめ本コーナーを作ってみました✨

(1) 個人開発をはじめよう!クリエイター25人の実践エピソード

個人開発を楽しんでる人におすすめ。読み物としても面白いし、気を張らずに気楽に読めます。
自分自身の開発モチベーションを上げるためにも使ってます。

(2) Vue.js3やさしい入門書[Vite/Vue Router/Pinia 対応]

Vue 3 から追加された Composition API での解説や <script setup> 構文を前提に解説している本。
ほかの入門書では Vue2.0 での書き方である Option API で書かれていたり、<script setup> 構文を使わないものが多いため、ほかの書籍にはないメリットだと思います。