- 投稿日:2020-03-10T21:38:37+09:00
GROWI に draw.io 連携機能を PR (Pull Request)して v3.7.0-RC としてリリースされた話
はじめに
GROWI に draw.io 連携機能を実装して Pull Request し、無事マージされて、バージョン 3.7.0 RC(Release Candidate) としてリリースされました!
(今は docker image のみ提供されてます)
weseek/growi Add draw.io Integration #1685
使い方のドキュメントはこちらです。
draw.io で様々な図を作成する | GROWI Docs
この記事では簡単な機能紹介と実装のモチベーション、どうやって実装したかなどをお話しようと思います。
GROWI って何?
株式会社 WESEEK が OSS として開発している Markdown で書ける Wiki です。
- 公式 HP
- ドキュメント
- クラウド(SaaS)版
技術スタックは下記の通り、わりと鉄板な構成です
- React
- webpack
- Express
- MongoDB
- etc ...
どういうことができるようになったの?
PR に掲載しているアニメーション GIF を置いておきます。
端的に言うと下記の項目が一通りできるようになりました。
- 編集画面から、ツールバーの draw.io アイコンをクリックすることで図を追加できる
- 追加した draw.io の図がページ表示画面で表示できる
- draw.io の図を2通りの方法で編集できる
- ページ表示画面で「編集」ボタンから図の編集ができる
- 編集画面で画面左側にあるエディタ上で Base64 エンコードされた箇所にカーソルを合わせて、ツールバーの draw.io アイコンをクリックすることで既存の図が編集できる
作ったモチベーション
GROWI には元々 PlantUML で UML 図を描画できる機能 や blockdiag で図を描画できる機能 が搭載されていました。
「これでめっちゃドキュメント書き放題やんけ!」と思っていたのですが、やはり記法を覚えないといけなかったり、自分で図のカスタムを行いたいときの不自由さが目につきました。
また、 Twitter や Qiita で GROWI の反応を見ると、作図機能を使おうと試行錯誤してくれている方は結構居るものの「使いづらいかもしれん ... (´・ω・`)」という気持ちが湧きました。といいつつ、blockdiag の図を生成する機能を追加したのも自分なんですが。 (PR はこちら)
アレコレ考えるうちに、「図はやっぱり見たまま編集できる (WYSIWYG) 方法が一番だろ!」という至極シンプルな理由で実装することを決めました。
また、実装中何度も心が折れかけましたが、 Qiita で draw.io の使い方の解説記事が公開されて「これは実装しきるしかない!」と後押しされた、というのも最後まで完成させる大きなモチベーションになりました。
また、 draw.io の図が GROWI で管理できるようになると、ページの「更新履歴」タブから過去の図を復元できるため、GROWI の仕組みを上手く活かせるのも嬉しいポイントですね
完成に至るまでの道のり
ここからはどのように実装していったかの経緯を書いておきます。
1. draw.io の図をテキスト形式で表現できないか調べる
GROWI の編集画面は WYSIWYG 形式ではなく、テキスト形式を想定していますので、Markdown 内にバイナリデータを埋め込もうとしても、その表現方法がありません。
そのため、テキストで埋め込む形式をまず調べる必要があります。Google さんにお尋ねしてもあまりいい答えは返ってこなかったため、 draw.io を提供している jgraph の github を調べることにしました。そこで、jgraph/drawio-integration という目的に合ったものが見つかったため、このソースコードを見つつ、テキスト形式で表現できることが分かりました。
コードでいうと ここらへん
大分ざっくり話しますが、バイナリ形式を Base64 化した形式で表現していることが分かります。
2. draw.io の仕組みについて調べる
draw.io の図のサンプルを見ながら、データ形式を探っていきます。
さらにデータ形式を解釈して図として変換してくれる JS が draw.io のコードのどこに該当するかを見つけるために、コードを読み進めていきます。 GraphViewer.js というものがデータを解釈して、図として変換してくれることが分かったため、この JS を使ってプレビューを行うことが分かりました。
実際にビルドされたソースは viewer.min.js という最適化された形の JS です。
ここまで分かれば「特定の HTML タグに Base64 化されたデータを埋め込んで、 viewer.min.js に食わせれば
表示ができる」というところまで到達できます。(実際もっと途方も無い時間がかかってますが)その後に小さなプロトタイプを HTML と JS で作り、自分の仮定が上手く行っていることを確認しました。
3. markdown-it が draw.io のデータを食って、特定のタグを吐けるような npm パッケージ を作る
プロトタイプ実装後、 GROWI にすべての担当をお任せすることはコード量が増えることを懸念し、 「draw.io のデータを食わせると、特定のタグを吐かせる markdown-it のプラグインを担当」する npm パッケージを作りました。かなり端折ってるので、詳しく知りたい方は質問していただけると。
- ソースコード: https://github.com/kaishuu0123/markdown-it-drawio-viewer
- 実行サンプル: https://runkit.com/embed/c8zmotx8yypi
入力
::: drawio (ここに draw.io のデータ(Base64 エンコードされたもの)が入る) :::出力
<div class="drawio-viewer-index-0 markdownItDrawioViewer" data-begin-line-number-of-markdown="2" data-end-line-number-of-markdown="6"> <div class="mxgraph" style="max-width: 100%; border: 1px solid transparent" data-mxgraph="{"editable":false,"highlight":"#0000ff","nav":false,"toolbar":null,"edit":null,"resize":true,"lightbox":"open","xml":"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<mxfile userAgent=\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36\" version=\"6.8.9\" editor=\"www.draw.io\" type=\"atlas\"><mxAtlasLibraries/><diagram name=\"Page-1\" id=\"bac0fdb4-bee4-c619-222c-02e826218fb6\">1VtNk5s4EP01vqYQAhsfY2eS3UMqqfJu7eaowQooAeQI4Y/99SsGgXELJw7GoPHBhRrRkl4/ulste4bX6fGDILv4I9/SZOY62+MMv5u5brBcqO9ScKoEfhBUgkiwbSVCZ8GG/Ue10NHSgm1pftFRcp5ItrsUhjzLaCgvZEQIfrjs9pUnl6PuSKRHdM6CTUgSanT7h21lrJflt3r/QVkU1yMjR995JuH3SPAi0+PNXPz15VPdTkmtS/fPY7Llh5YIP83wWnAuq6v0uKZJCW0NW/Xc+yt3m3kLmslbHnCrB/YkKfTS3zESCZLq6clTDckhZpJudiQs2wdl9Rle5VLw7w1CWElimSbqEqlLcyp6dnsqJD22RHpqHyhPqRQn1UXf9eYaJk0jTzcPZ5ugGsm4ZY/6MaJpEDWaz1CoC41GNzLYQGYjRRHKQtCZO1d2VVzFK5vQat4ejRYaEy7PgGtFY7JnvBCWwhVMCZdvwLVOSJ4rkU0QLS8Rcv0REZqbCPF0x3OFQotQtr+TgGM4GBHBhYHgZxWdyshnJ1YYTwhWYAZCukv4KS3XYydeaDEhXsvu1zOzF67Ge03hzWo/0MLr0/O3MnW1EywPTQkWMsB6G0q2Z/JkKVx+MCVcZhL/ZyapIAoznlmK2AJPiZiZ3P+dq7DorElua3QMJqWYmd5vJCkzMecjCWOW2YoaciblmZnmb+iPgmahrXhBRzZqToE6c/60yFhIXpErGxczM8vvdv6f1IL2jB4sBRG6t3FBNLP/v1jKsshSsAyvNi5aZu5voEK3Ed3oJk2e+eHpLFi9CNSNcsHq1VbjrF5KprQcwLkRrZwXItSj6XAuiYio7qWnVM7jp4i2AXM6AGuEgibKCe0vC8RdMOoxPnNWboWulFKavVutoVqOfuhsC0MPAoqgngoDQ8+LTZtl32Tmmj72mNmz38w+LJm5Pe08h4SBigY0tLndmtjQvmlobJmhYVqJ+77PQBGCiq7Y+a0Q5NTqtis75Nfni2D14/JgSF1UGnuTyNyE3k0iemTy35JCb3zd+qIJpcgjTq1bZfOLZth91Fu+Ouo15xb3Ug/qGYh5xinC0MwzN/MTu6+6dmYzifyh8hG/Vz7yuyTCiweTyCxwTEyiuf0cgo5oOZAjgnqGCoGPdkRmtedVhsDAfuYBS/q9mQcUeQ+innH2NDT1zMrZ/dT7CcGu0/I+6i3spx50erCi0tfpQT1DBU7vwcwz648TB8769LBNIs8yEvngh21u0JNFc6AIQUUD0WgBJzw0jcwK7NQ06qg22UYjD54J9o2D8NjHSOUGotESTnhoGj2gND1FHKyP9G0mn3/tZPW3fRgMqVDRQOQL4IQHJl+t/tWTr+M4xTryOQN5vrkzkueDP6gamnzWFfFRRxW/9mvW8MiDO8m+Tgwe/Hg3OrE+pnatM3VHsco6U89B7uH33bktgCLvxq1bH1PbV9vu2KLbZ2o0lKnReKa2rgKNOgqB1pka+l2/904aOvAbd9K/NrVqnv/QV3U//2kSP/0P</diagram></mxfile>"}"></div> </div>4. GROWI に実装
この実装を行う際には GROWI のメンテナである @yuki-takei さんに GROWI でのレンダリングの仕組みなどを教えていただきつつ、実装を行いました。ありがとうございます
- draw.io エディタの起動方法を調べて、iframe 内で draw.io エディタを表示して
position: fixedで既存画面に被るように表示し、 iframe と GROWI 間でデータが受け渡せるように実装したり- 不用意に再レンダリングが走らないように GROWI の lsx プラグインなどと同様に draw.io の図を React コンポーネント化したり
- Table 編集機能と同じようにページ表示部分から「編集」ボタンを押すことで、図の更新ができるようにしたり
- 既存の仕組みに乗っかるようなコード構成に大修正したり
と GROWI に組み込む際にもそこそこのコード量になりました。( PR の内容 )
アドバイスもあり、とてもシンプルな形で機能を追加できたんじゃないかなと思っています。
今後の課題
- やっぱり図が大きくなればなるほど、レンダリングが遅くなるんじゃないか、という不安
- これは実際に使ってみてもらってから様子を見ればいいかな、と思っています
- 最悪、図を他のページに分割して保存するという方法で回避していただく方向で ...
- エディタ画面に Base64 エンコードした文字列がそのまま書かれるのは分かりづらい
- これはエディタの折りたたみ機能を使って、なるべく Base64 エンコードされた文字列が意識されないようにできるといいかなぁと思っています。
- 本当は添付ファイルを CRUD できる機能が GROWI にあれば、 draw.io の図は画像みたいな取り扱いができるっちゃできるんですが、それはそれで実装コストが高すぎる気がするので
最後に
draw.io の仕組みを理解するまでに結構な時間がかかりましたが、 Markdown に draw.io の図を埋め込める機能が作れた経験は貴重でした
また、 draw.io に関する様々なノウハウを得ることができました。(データ構造や viewer.min.js の仕組みなどは今回の記事ではめちゃくちゃ端折っています)Visual Studio Code の拡張機能に応用できたりするかもしれません。
皆さんも是非今回実装した機能を使ってみてください
- 投稿日:2020-03-10T20:55:29+09:00
ReactのHOCでModalを実装
ReactでModalを実装するときHigher Order Componentを用いたのでその備忘録になります。
Higher Order Component(HOC)って
公式サイトも分かりやすいですが、qiitaの記事を参考にするならこの記事がわかりやすかったです。「React の Higher-order Components の利用方法」
注意してほしいのは公式にも書かれているとおり、componentを受け取って新規のcomponenを返す、単なる関数です。設計パターンです、
なので、なにかライブラリを使うわけではないです。高階コンポーネント (higher-order component; HOC) はコンポーネントのロジックを再利用するための React における応用テクニックです。HOC それ自体は React の API の一部ではありません。HOC は、React のコンポジションの性質から生まれる設計パターンです。
具体的には、高階コンポーネントとは、あるコンポーネントを受け取って新規のコンポーネントを返すような関数です。
const enhance = compose<ImplProps, ImplOuterProps>(ModalHoc); const Sample = enhance(SampleModal);こんな感じでHOCの引数に毎回中身が違うmodalのcomponentを渡して、毎回同じ処理をするHOC側でModalのcomponentを作るということです。
このときポイントとなるのが下記になると思います。
- HOC側でReduxの処理をする
- HOCを呼ぶ側のcomponentにはpropsを取得するだけで処理を分離させる
実際に書いてみた
- Material-uiのModalを使ってます。
- TypeScriptで書いてます
毎回変わるModalの中身のコンポーネント
- modalの中身に渡したいdataをpropsのmodalValueで管理しています。たとえばUserIDとか
Sample.tsxinterface ImplProps { modalValue?: { userId: string; }; hideModal: () => void; } const SampleModal: FC<ImplProps> = props => { const { modalValue, hideModal } = props; return ( <> <h2 id="transition-modal-title">タイトルが入るよ</h2> <p id="transition-modal-description"> {modalValue ? modalValue.userId : 'IDないってよ'} </p> <Button variant="contained" color="secondary" type="button" onClick={hideModal}> Close </Button> </> ); }; const enhance = compose<ImplProps, ImplOuterProps>(ModalHoc); const Sample = enhance(SampleModal); export default Sample;HOCとは関係ないのですが、composeの型を定義するのに四苦八苦しました。
compose<InnerProps, OuterProps>で指定します。
InnerPropsはこのSampleコンポーネントの型を指定。OuterPropsはHOCコンポーネントの型を指定となります。このSampleコンポーネントが毎回変わるModalの中身を書くコンポーネントなのでHOCのことは気にせずにガシガシ書いていけばいいわけです。
propsのhideModal関数でmodalを閉じる処理が行われます。ModalのHOCコンポーネント
- このコンポーネントにReduxでデータを取得、dispatchする処理を書いていきます。
- それらの処理を子コンポーネント(Sampleコンポーネント)に渡します
- Material-UIの記述部分を省略しています。
ModalHoc.tsxexport interface ImplOuterProps { modalName: string; } interface ImplInnerProps { isOpen: boolean; modalValue?: {}; handleClose: () => void; } type ImplProps = ImplInnerProps & ImplOuterProps; const ModalHoc = (ComposedComponent: ReactNode | any): ComponentClass<{}> => { const ModalWrapper: FC<ImplProps | any> = props => { const { modalName, isOpen, modalValue, handleClose } = props; return ( <Modal open={isOpen} onClose={handleClose} <Fade in={isOpen}> <div> <ComposedComponent modalValue={modalValue} hideModal={handleClose} /> </div> </Fade> </Modal> ); }; const mapStateToProps = ( state: ImplState, { modalName }: { modalName: string } ) => { const targetModal = state.modal[modalName]; return { isOpen: targetModal ? targetModal.isOpen : false, modalValue: targetModal ? targetModal.modalValue : {} }; }; const mapDispatchToProps = ( dispatch: Dispatch<ImplModalAction>, { modalName }: { modalName: string } ) => ({ handleClose: () => dispatch(hideModal(modalName)) }); const enhance = compose<ImplProps, {}>( connect(mapStateToProps, mapDispatchToProps) ); return enhance(ModalWrapper); }; export default ModalHoc;const ModalHoc = (ComposedComponent: ReactNode | any)この
ComposedComponentが先程書いたSampleコンポーネントになります。
Sampleコンポーネントのpropsに渡したいデータ(modalValue)、処理(modalを閉じる処理)を書いています。<ComposedComponent modalValue={modalValue} hideModal={handleClose} />ちょっと難解なのがこのHOCのなかで
ModalWrapperというコンポーネントを作っています。そしてそれをenhanceでwrapした関数を返したもののがHOCになります。
あとは先程説明したようにこのHOC側でmapStateToProps,mapDispatchToPropsを使ってstateのdataを取得したり、reducerの処理を行っています。Modalを配置するコンポーネント
- Sampleコンポーネントを呼ぶ
pages/index.tsx<Sample modalName={SAMPLE_MODAL} />ここでのポイントはここで指定したpropsがHOCコンポーネントのpropsに入ることですね。
以上になります。
HOCを使うとコンポーネントを再利用できるのと、Reducer処理とただコーディングするだけとで分けられるからいいですね。
- 投稿日:2020-03-10T04:56:57+09:00
Next.js 9.3: 新世代の静的サイト生成、Built-in Sassのサポート
本日、Next.js 9.3 がリリースされました。本リリースの特徴は次のものです。
- Static Site Genration のサポート
- プレビューモードのサポート
- ビルトインの Sass サポート
- 404 ページの静的化
- ランタイムの縮小
各トピックごとに簡単に説明します。詳しく知りたい場合は公式のリリースノートが公開されているのでご確認ください。
Static Site Generation のサポート
Next.js では 9.0 から Automatic Static Optimization というコンセプトを打ち出し、
getInitialPropsでデータを取得しない場合はビルド時に HTML ファイルを生成していました。(また、getInitialPropsでのデータ取得はnext exportで生成した場合であっても、クライアントのルーティング変更時に実行されるため、完璧な静的サイトとは言い難いものでした。)
しかし、データを取得しつつ、HTML をビルド時に生成したいケースが増えてきています。Headless CMS を使ったマーケティングブログがよい例です。そこでコミュニティ内で討論したできたのが本機能です。Next.js 9.3 では
getStaticPropsとgetServerSidePropsという 2 つのデータ取得方法が用意されています。また、動的なルーティングを実現する際に、パラメータを付与するgetStaticPathsも追加されています。
getStaticPropsビルド時にデータを取得するgetStaticPathsデータに基づいて動的ルーティングを指定するgetServerSidePropsリクエストごとにデータを取得する後方互換性も担保されており、今までの
getInitialPropsも使うこともできますが、新しい方法が推奨されます。公式ブログのリリースノートには各メソッドの要点がまとまっています。
またドキュメントも大きく書き直されているので一読をおすすめします。
プレビューモードのサポート
Static Generation は Headless CMS から取得するのに役立ちますが、下書き段階には理想的ではありませんでした。
Static Generation で生成されるのは静的なプレビューなため、変更を反映するには再生成する必要があるからです。そこで用意されたのがプレビューモードです。静的に生成されたページを通して、下書き状態のページを SSR することができます。
Preview Mode のドキュメントも用意されているのでそちらをご覧ください。
ビルトインの Sass サポート
Next.js 9.2 では Built-in CSS がサポートされましたが、Next.js 9.3 では Sass のサポートも追加されました。
Built-in CSS と同様に、Global Stylesheet と CSS Modules がサポートされています。簡単なコード例をお見せします。// Global Styles import "global.scss" // CSS Modules // 注意: 拡張子はmodule.scssにすること import styles from "./Button.module.scss" const Button = () => { return <button className={styles.success}>Save</button> }CSS Modules ではコード分割も行われるので、global ではなくなるべく CSS Modules の方に寄せるといいでしょう。1
今まで
@zeit/next-sassを使っていた方は、この機能は自動的に無効化されます。Built-in の Sass に乗り換えたい方は@zeit/next-sassを外しましょう。404 ページの静的化
上記で紹介した Automatic Static Optimization というコンセプトはありましたが、404 ページは静的にレンダリングされたページではありませんでした。
今までは
pages/_error.jsにおいたファイルがエラーページとなっていましたが、Next.js 9.3 からはpages/404.jsというファイルを置くと 404 ページとして扱われます。
この 404 ページを静的に保つことでエラーページを表示する度にレンダリングされなくなり、サーバーへの負荷が減ります。こちらもドキュメントがあるので読みましょう!
まとめ
ここ最近の Next.js は今までとひと味違う動きを見せているので要注目です。
繰り返しにはなりますが、本記事では概要のみを扱っています。詳しく知りたいと思った方は公式のリリースノートをご覧ください。
私はお仕事のプロジェクトでも移行しましたが、module.scss では IE 対応のセレクタが壊れて大変でした。 ↩
- 投稿日:2020-03-10T00:29:28+09:00
nanorouterでReactのルーティング処理を簡単に自作する
nanorouterというURLパスのパターンマッチングだけをやってくれるライブラリがあるので、それを使ってReactアプリケーションのルーティング部分を自作します。History APIとかそんなものは知らん。
まずはルーティングの実装。
nextComponent変数がイミュータブルじゃないのが気になってしょうがない。Route.jsimport React from 'react'; import nanorouter from "nanorouter" export const Route = routes => { const router = nanorouter({ default: '/' }) let nextComponent = null Object.keys(routes).forEach(path => { router.on(path, param => { nextComponent = routes[path] }) }) router.emit(window.location.pathname) return ( <div className="container"> {nextComponent} </div> ) }Routeを使うAppコンポーネント
App.jsimport React from 'react' import { Route } from './Route' import { PageA } from './PageA' import { PageB } from './PageB' function App() { return Route({ '/hello': <PageA />, '/world': <PageB />, }) } export default App;ルーティングするページふたつ
PageA.jsimport React from 'react'; export const PageA = () => { return ( <div>This is page A</div> ) }PageB.hsimport React from 'react'; export const PageB = () => { return ( <div>This is page B</div> ) }nanorouterは機能が少なくて使いやすいが、設計がイミュータブルじゃないのが若干微妙。
パスに該当するデータを返す関数みたいな動きをするルータがあればいいんだが。
