MIS.W 公式ブログ

早稲田大学公認、情報系創作サークル「早稲田大学経営情報学会」(MIS.W)の公式ブログです!

Next.jsとEditor.jsでいい感じのブロックエディタを作る方法【カウントダウンカレンダー2022冬2日目】

挨拶

55代プログラミング研究会 わせりんです。Web系イキれたら楽しいだろうなと思っています。

Next.jsとEditor.jsでブロックエディタを作る時に難しかったことが色々あったので、後継の役に立てばと思ってこの記事を生成しました。

対象者

Notionに惚れ込んでたり、HackMDが大好きな人 兼 自作してみたいという人。

この記事を読めば多分1時間くらいでそれっぽいエディタがnext.jsで作れるようになります。

完成図

こんな感じのイケてるエディタが出来上がる予定です。かっこいいですね。

Editorjsでの完成図

公式のドキュメントはこちら。英語読めちゃうよって人はこっちのほうが早いです。 Editor.js

お断り

今回はwindows10, node version 14.18.1です。それ以外のバージョンはうまくいかないかもしれません。環境によっては動かない可能性があります。

また、解説の内容は一部厳密性を欠いていることがあります。ご留意ください。

Next.jsとは?

この記事を読む方はおそらく知っていますが、少しだけ解説します。

webアプリを作るフレームワークの一つで、Reactの拡張的な存在です。サーバー機能を持っていることもあり、フロント側だけで気楽にwebアプリが作れることもあります。

ポケモン最新作のホームページはNext.jsを使っていたりします。

Next.jsの特徴は主に3つです。 1. 初期データを埋め込んだHTMLをブラウザに送信するので、無駄にJSファイルを実行せずにすみます。つまり、読み込み時間が早いです。 2. ファイルの配置によってURLルーティングができます。 3. 設定がある程度自動でやってくれます。 つまり、描画が早くて開発が簡単になります。とっても便利ですね。 しかし、Next.jsのサーバーサイドで構築するという機能には問題もあります。これが今回のメインポイントなので後ほど解説します。

1について細かく知りたい人はこのサイトがとても良いでしょう。 Server Side Renderingについて知るべきこと。Server Side Renderingとは何か? それによって何が改善されるのか?(前編) ng-japan 2017 - Publickey

Editor.jsとは?

Notionっぽいブロックエディタを簡単に使えるJavaScriptのライブラリです。 出力がJSON形式で汎用なデータが取れます。 カスタマイズ性が高く、自分好みのエディタが使えます。 大まかなイメージは完成図を参照してみてください。

詳細はこちらから。

シンプルで使ってみたいWYSIWYGエディター Editor.js | アールエフェクト

作り方

next jsの環境構築

コマンドラインツールから任意フォルダ上で

create-next-appコマンドを実行するとnext.jsのプロジェクトフォルダが作成されます。

※next-appには任意のプロジェクト名を設定します。

npx create-next-app next-app

作成したフォルダ内で npm run dev コマンドを実行するとローカル上でアプリが起動します。

npm run dev

これで、このページが確認できれば一旦完成です。

詳細はこちらで確認してください。

Next.jsで始めるReact開発環境の構築手順

editor jsの環境構築

Editor.jsをインストールします。 npm i @editorjs/editorjs — save

これでひとまずEditor.jsは使えます。 次に初期設定をします。 Editorの設定を行うファイルとEditorを表示するindex.jsファイルに分けて作ります。使うファイルの構成は以下のとおりです。

/components - Editor.js - tool.js /pages - index.js

next.jsがメインで表示するIndex.jsは以下のコードで実装します。 ここでは主に、Next.jsの特徴であるSSRを無効にしつつ、Editorを埋め込んで居ます。

import dynamic from "next/dynamic";

const CustomEditor = dynamic(() => import("../components/Editor.js"), {
  ssr: false,
});

const Home = () => {
  const [data, setData] = useState();

  return (
    <>
      <h1>Editor</h1>
      <CustomEditor
        data={data}
        onChange={setData}
        holder="editorjs-container"
      />
    </>
  );
};

export default Home;

このコードでは、ブロックエディタ画面をサーバーサイドでのレンダリング後に読みこむためにdynamicメソッドを利用しています。ssrは falseにしないとバグります。 では、ブロックエディタで入力された情報を受け渡しつつ、holderではエディターの画面を表示するためのフックを入れています。

Indexで表示するエディター画面は、下記のコードで作ります。 ここでは、hooksをつかって変更内容を保存し、index.jsにわたす内容を作成しています。

import React, { memo, useEffect, useRef } from "react";
import EditorJS from "@editorjs/editorjs";
import TOOLS from "./tools";

const CustomEditor = ({ data, onChange, holder }) => {
  //initialize editorjs
  const ref = useRef();

  useEffect(() => {
    //initialize editor if we don't have a reference
    if (!ref.current) {
      const editor = new EditorJS({
        holder: holder,
        tools: TOOLS,
        autofocus: true,
        data,
        placeholder: "Start writing your story...",
        async onChange(api) {
          const data = await api.saver.save();
          onChange(data);
        },
      });
      ref.current = editor;
    }
    //add a return function handle cleanup
    return () => {
      if (ref.current && ref.current.destroy) {
        ref.current.destroy();
      }
    };
  }, []);

  return (
    <>
      <div id={holder}></div>
    </>
  );
};

// 状態の変更をこのFCの変更によってのみ検知する。
export default memo(CustomEditor);

const ref = useRef();:

useRefを使用して初期化されます。このhookは、カスタムエディターの参照を保持するために使用されます。

useEffect(() => {...}, []);:

この行では、 useEffectを使用して、カスタムエディターを初期化するための関数を定義します。初回のレンダリング時のみ実行しています。

!ref.currentで refの中身が無い時にインスタンス生成するようにしています。

const editor = new EditorJS({...});: この行では、 EditorJSクラスの新しいインスタンスを作成します。このインスタンスには、次の引数が渡されます。

holder: この引数は、カスタムエディターが表示されるHTML要素のIDを指定します。

tools: この引数は、EditorJSに対応するカスタムツールを定義したJavaScriptファイルを指定します。 autofocus: この引数は、エディターを自動的にフォーカスするかどうかを指定します。

data: この引数は、エディターに表示する初期データを指定します。

placeholder: この引数は、エディターが空の場合に表示するプレースホルダー文字列を指定します。 async onChange(api) {...}: この引数は、エディターの内容が変更されたときに呼び出される非同期関数を指定します。この関数では、 api.saver.saveメソッドを使用して、エディターの内容を保存し、 onChange関数を呼び出して、変更を外部に通知します。

return () => {
      if (ref.current && ref.current.destroy) {
        ref.current.destroy();
      }

ここでは、すでに定義されているときに、2つ以上エディターを作らないようにしています。

最後に、

<>
      <div id={holder}></div>
</>

この部分がindex.jsで表示されるようになります。

Editorのプラグインをtools.jsにまとめて記載しています。これらのツールについては、全て npm i --save @editorjs/プラグイン名 でインストールしてから下記のように記載してみてください。

tool.js

import Header from "@editorjs/header";
import CheckList from "@editorjs/checklist";
import code from "@editorjs/code";
import Delimiter from "@editorjs/delimiter";
import Embed from "@editorjs/embed";
import InlineCode from "@editorjs/inline-code";
import LinkTool from "@editorjs/link";
import List from "@editorjs/list";
import Quote from "@editorjs/quote";
import { Paragraph } from "@editorjs/paragraph";

const TOOLS = {
    header: {
    class: Header,
    },
  },
  checklist: CheckList,
  code: code,
  delimiter: Delimiter,
  embed: Embed,
  inlineCode: InlineCode,
  linkTool: LinkTool,
  list: List,
  quote: Quote,
};

export default TOOLS;

toolsについては、書き方が固定されているので、このまま使ってください。基本的には、importしたプラグインの内容をTOOLS以下で呼び出しています。

Ultimate Guide on how to setup Editor.js with Next.js 12+ (Typescript) | ReactHustle

Next.js & Editor.js Complete Setup Guide | by Fazley Rabbi | Medium

注意点

  1. ssrをfalseにすること
  2. 一回初期化したら二回目以降を止める(useRefで状態が出来上がっているかどうかを検知) この点抜けていると、バグがおきるので気をつけてください。

拡張方法

前述したとおり、プラグインをtools.jsに追加すれば出来ます。

また、自分でプラグインを作成することも可能です。ここでは細かく言及しませんが、documentにて詳細な情報を載せています。その他には、このサイトを参照してみてください。

Editor.jsを使ってテキストエディタを作る - Qiita

まとめ

ここまでで、ブロックエディターをNext.js環境下で作成する方法をまとめました。色々欠けている点はありますが、所々で添付した参考サイトと合わせれば、自分だけの完璧なエディターを作れるようになります。

皆様のイチオシエディターを待っています

参考文献

神ドキュメント

editorjs.io

次回予告

明日は56代のなださんによる、「初心者向け顔の描き方講座」です。お楽しみに!