LoginSignup
1
3

従来型Webページの一部としてReactを埋め込む方法

Last updated at Posted at 2023-05-25

はじめに

Reactのコンポーネントを従来型Webページの一部に組み込むにはどうすればいいのか?と思い調べてみましたが、意外にも具体的な手順が見つかりませんでした

そこで、コンポーネントのビルド、ページ内の部品として表示、コンポーネント外部とのやり取りの3点に絞って手順をまとめてみます

  1. Reactコンポーネントを外部から利用できるようにビルドする手順
  2. ReactのコンポーネントをWebページの一部として表示する手順
  3. Webページ側とReactコンポーネント間でやり取りを行う(初期値設定、相互の呼び出し)

概要

create-react-appを使ってひな形を生成し、おなじみCounterコンポーネントを作ってから、外部で利用できるようにしていきます

  1. Reactコンポーネントを外部から利用できるようにビルド(rollup.jsを利用)する
    • 従来型Webアプリではモジュール形式のJSを利用してないことが多いので、UMD形式(グローバル変数経由でReactコンポーネントを公開)でビルドする
    • 埋め込むコンポーネントはReactでおなじみのCounterコンポーネントを利用
  2. ReactのコンポーネントをWebページの一部として表示する
  3. Webページ側とReactコンポーネント間でやり取りを行う
    • Reactコンポーネントへの初期値設定
    • Reactコンポーネント側のイベント処理から、Webページ側の処理を呼び出す
    • Webページ側のイベント処理から、React内部の処理を呼び出す

Counterコンポーネントを部品として表示するコード(のイメージ)

  • MyBundleはコンポーネントを公開するためのグローバル変数(UMD形式でビルドする際に、指定した変数名)
  • ReactDOMClient.createRoot()rootを作成し、コンポーネントをrenderします
  <body>
    <h2>Webページの一部にReactコンポーネントを表示する</h2>
    <div id="root"></div>
  </body>
  <script src="/dist/lib.umd.js"></script>
  <script>
    const {ReactDOMClient, React, Counter} = MyBundle;
    const container = document.getElementById('root');
    const root = ReactDOMClient.createRoot(container);
    root.render(React.createElement(Counter));
  </script>

ESModule形式でビルドした場合はこのようになります

  <script type="module">
    import {ReactDOMClient, React, Counter} from '/dist/lib.esm.js';
    const container = document.getElementById('root');
    const root = ReactDOMClient.createRoot(container);
    root.render(React.createElement(Counter));
  </script>

準備:Counterコンポーネントを作成

  • create-react-appでReactのひな形を作成
$ npx create-react-app react-with-conventional-webapp --template typescript
$ cd react-with-conventional-webapp
  • styled-componentsをインストール(Reactコンポーネントを目立だたせるため、borderを表示する)
$ npm i styled-components@5.3.10

$ npm i -D @types/styled-components
  • 下記のエラーが出た場合はnpm i styled-components@5.3.10でインストール可能 (StackOverflowの解説)
  $ npm i styled-components
  npm ERR! Cannot read properties of null (reading 'edgesOut')
  • Counterコンポーネント(ボタンを押すとカウントを加算)を追加
$ touch ./src/Counter.tsx

Counter.tsx

import { FC, useState } from 'react';
import styled from 'styled-components';
const Content = styled.div`
  border: solid 1px black;
  width: fit-content;
  padding: 4px;
`;

type propType = {
  initVal?: number;
};
const Counter: FC<propType> = ({ initVal = 0 }) => {
  const [count, setCount] = useState(initVal);
  const handleClick = () => setCount((n) => n + 1);
  return (
    <Content>
      <div>Count: {count}</div>
      <button onClick={handleClick}>Increment</button>
    </Content>
  );
};
export default Counter;

動作確認

  • App.tsxファイルを書き換えてからnpm run startで実行する
import './App.css';
import Counter from './Counter';

function App() {
  return (
    <div className="App">
      Counterコンポーネント動作確認
      <Counter initVal={0} />
    </div>
  );
}

export default App;

このような画面が表示されればOK

img10.png

①Reactコンポーネントを外部から利用できるようにビルド(rollup.jsを利用)する

ビルドを行うための手順

  • rollup.jsの導入
  • ビルド用スクリプトrollup.config.jsを作成
  • rollup.jsでトランスパイルを行うため、tsconfig.rollup.jsonを追加
  • package.jsonに出力ファイル名を追加(rollup.config.jsで利用)
  • 公開するコンポーネントをエントリーポイントファイル(src/lib.ts)でexportする
  • package.jsonscriptsにビルド用のコマンドbuild-libを追加して動作確認を行う

rollup.jsの導入

ライブラリとしてパッケージングするため、rollup.jsと必要なプラグインを導入します

  • rollup.jsは複数のモジュールやファイルを1つのファイルにまとめてくれる、軽量で高速なバンドルツールです
npm i -D rollup rollup-plugin-delete rollup-plugin-peer-deps-external rollup-plugin-typescript2 @rollup/plugin-commonjs @rollup/plugin-node-resolve @rollup/plugin-replace

ビルド用スクリプトrollup.config.jsの作成

続いて、ビルド用のスクリプトを作成します。rollup.config.jsをプロジェクトのルートに作成し、下記の内容を記載します

import peerDepsExternal from 'rollup-plugin-peer-deps-external';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import replace from '@rollup/plugin-replace';
import typescript from 'rollup-plugin-typescript2';
import del from 'rollup-plugin-delete';
const packageJson = require('./package.json');

// eslint-disable-next-line import/no-anonymous-default-export
export default {
  input: 'src/lib.ts',
  output: [
    {
      // esmodule
      file: packageJson.module,
      format: 'esm',
      sourcemap: true,
    },
    { // UMD形式
      file: packageJson.umd,
      format: 'umd',
      name: 'MyBundle',
      sourcemap: true,
    },
  ],
  plugins: [
    del({ targets: 'dist/*' }),
    peerDepsExternal(),
    resolve(),
    commonjs(),
    replace({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
      preventAssignment: true,
    }),
    typescript({
      tsconfig: 'tsconfig.rollup.json',
      useTsconfigDeclarationDir: true,
    }),
  ],
};
  • input: 'src/lib.ts'を起点にimportされているファイルを読み込み、バンドルします(1つのファイルにまとめる)。公開したいコンポーネントをexportするファイルです
  • ES Modules形式、UMD形式に分けて2種類のファイル出力します
  • file:~は出力するファイル名です。package.jsonで定義した値を利用します(この後追加)

rollup.jsで利用するプラグインについて

  • peerDepsExternal

    package.jsonに記載されたpeerDependenciesパッケージをバンドル対象から除外して、バンドルサイズを削減する

  • resolve

    インポートするモジュールの依存関係の解決とファイルパスの特定を行う

  • commonjs

    require()を解析してCommonJS形式の依存関係を特定する

  • replace

    ビルド時に置換処理を行う。実行時にnodeの環境変数を参照する箇所がエラーとなるため、ビルド時に置換する

  • typescript

    トランスパイルを行う。設定ファイルはtsconfig.rollup.jsonを利用する。また型定義ファイル(d.ts)を出力するため、useTsconfigDeclarationDir: trueを指定する。出力先はtsconfigファイルのdeclarationDir

rollup.jsでトランスパイルを行うため、tsconfig.rollup.jsonを追加

rollup.js用トランスパイル設定ファイルtsconfig.rollup.jsonをプロジェクトルートに作成します

{
  "extends": "./tsconfig",
  "compilerOptions": {
    "outDir": "dist",
    "declaration": true,
    "declarationDir": "dist",
  },
  "exclude": [
    "node_modules",
    "dist",
    "build",
  ]
}
  • "extends": "./tsconfig",

    create-react-appで作成されたtsconfig.jsonの設定を継承します

  • "outDir": "dist",

    出力先の指定

  • "declaration": true, "declarationDir": "dist"

    型定義ファイルの出力を有効にして、出力先をdistにします

package.jsonに出力ファイル名を追加(rollup.config.jsで利用)

rollup.jsで出力するファイル名の設定を追加します(ESModule用と、UMD用の2つ)

{
  "name": "react-with-conventional-webapp",
  "version": "0.1.0",
  "private": true,
+  "module": "dist/lib.esm.js",
+  "umd": "dist/lib.umd.js",

公開するコンポーネントをエントリーポイントファイル(src/lib.ts)でexportする

公開するコンポーネント(関数)をexportします

Counterだけではなく、React自体も一緒にexportしてパッケージングします(別途読み込み不要になる)

export { default as React } from 'react';
export { default as ReactDOM } from 'react-dom';
export { default as ReactDOMClient } from 'react-dom/client';
// Component
export { default as Counter } from './Counter';

package.jsonscriptsにビルド用のコマンドを追加して動作確認

パッケージビルド用のコマンドを追記します

  "scripts": {
+    "build-lib": "rollup -c"
  },

ビルドエラーが発生しないことを確認します

  • エラーがなければdist/lib.esm.js, dist/lib.umd.js の2ファイルが作成されます
$ npm run build-lib

> react-with-conventional-webapp@0.1.0 build-lib
> rollup -c


src/lib.ts → dist/lib.esm.js, dist/lib.umd.js...
created dist/lib.esm.js, dist/lib.umd.js in 4.1s

②ReactのコンポーネントをWebページの一部として表示する

動作確認のためhtmlファイルを作成します。webrootフォルダをルート直下に作成しその中に保存していきます

$ mkdir webroot
$ touch webroot/test1.html

test1.htmlに下記内容を書き込みます

  • <div id="root"></div>の部分に、Reactコンポーネント(Counter)を描画
<!DOCTYPE html>
<html>
  <body>
    <h2>Webページの一部にReactコンポーネントを表示する</h2>
    <div id="root"></div>
  </body>
  <script src="/dist/lib.umd.js"></script>
  <script>
    const {ReactDOMClient, React, Counter} = MyBundle;
    const container = document.getElementById('root');
    const root = ReactDOMClient.createRoot(container);
    root.render(React.createElement(Counter));
  </script>
</html>

動作確認のためにWebサーバーを起動します

$ npx http-server .

http://localhost:8080/webroot/test1.htmlを開きます。htmlの一部としてReactコンポーネントが表示されることを確認します

img20.png

  • ESModule形式(lib.esm.js)を利用する場合は以下の記述になります

test1_esm.html

<!DOCTYPE html>
<html>
  <body>
    <h2>Webページの一部にReactコンポーネントを表示する</h2>
    <div id="root"></div>
  </body>
  <script type="module">
    import {ReactDOMClient, React, Counter} from '/dist/lib.esm.js';
    const container = document.getElementById('root');
    const root = ReactDOMClient.createRoot(container);
    root.render(React.createElement(Counter));
  </script>
</html>

③Webページ側とReactコンポーネント間でやり取りを行う

Reactコンポーネントへの初期値設定

Counterコンポーネントは引数(属性)で初期値をセットできるようになっています

type propType = {
  initVal?: number;
};
const Counter: FC<propType> = ({ initVal = 0 }) => {

React.createElement()の引数に、initValを追加することで初期値を設定することができます

  • PHPなどサーバ側から初期値をセットして表示することが可能です

test2.html

<!DOCTYPE html>
<html>
  <body>
    <h2>Webページの一部にReactコンポーネントを表示する</h2>
    <div id="root"></div>
  </body>
  <script type="module">
    import {ReactDOMClient, React, Counter} from '/dist/lib.esm.js';
    const container = document.getElementById('root');
    const root = ReactDOMClient.createRoot(container);
    root.render(React.createElement(Counter,{initVal:12}));
  </script>
</html>

img30.png

Reactコンポーネント側のイベント処理から、Webページ側の処理を呼び出す

propsにcallback関数を追加して、React側から呼び出します(countを渡す)

CallbackCounter.tsx

import { FC, useState } from 'react';
import styled from 'styled-components';
const Content = styled.div`
  border: solid 1px black;
  width: fit-content;
  padding: 4px;
`;

type propType = {
  initVal?: number;
  callback: (num: number) => number;
};
const CallbackCounter: FC<propType> = ({ initVal = 0, callback }) => {
  const [count, setCount] = useState(initVal);
  const handleClick = () => {
    setCount((n) => n + 1);
    if (callback) {
      callback(count + 1);
    }
  };
  return (
    <Content>
      <div>Count: {count}</div>
      <button onClick={handleClick}>Increment</button>
    </Content>
  );
};
export default CallbackCounter;

CallbackCounterをexportします

lib.ts

export { default as React } from 'react';
export { default as ReactDOM } from 'react-dom';
export { default as ReactDOMClient } from 'react-dom/client';
// Component
export { default as Counter } from './Counter';
+ export { default as CallbackCounter } from './CallbackCounter';

html側でコンポーネントにcallback関数を渡します。callback関数は、引数経由で受け取ったcountの値を表示します

test3.html

<!DOCTYPE html>
<html>
  <body>
    <h2>React側のイベントをWebページ側に通知する</h2>
    <p>
      callback:<span id="callback">
    </p>
    <div id="root"></div>
  </body>
  <script src="/dist/lib.umd.js"></script>
  <script>
    const {ReactDOMClient, React, CallbackCounter} = MyBundle;
    const $ = (selectors) => document.querySelector(selectors);
    const root = ReactDOMClient.createRoot($('#root'));
    root.render(
      React.createElement(CallbackCounter,{
        callback: (val)=> $('#callback').innerText = `${val}`}
      )
    );
  </script>
</html>

ボタンをクリックすると、callback関数が呼び出されてcallback:の後ろのカウントが更新されます

img40.png

Webページ側のイベント処理から、React内部の処理を呼び出す

Webページ側から、Reactの処理を呼び出すためにカスタムイベントを利用します

1:カスタムイベントを受信するコンポーネントを作成します

  • ButtonClickという名前のカスタムイベントを受信する
  • イベントのデータ(日時)を受け取り、yyyyMMdd HHmmss形式にフォーマットして表示する

EventReceiver.tsx

import { useState, useEffect } from 'react';
import styled from 'styled-components';

// カスタムイベントの型設定
declare global {
  interface DocumentEventMap {
    ButtonClick: CustomEvent<Date>;
  }
}

const Content = styled.div`
  border: solid 1px black;
  width: fit-content;
  padding: 4px;
`;

const formatDate = (date: Date) => {
  return new Intl.DateTimeFormat('ja-jp', {
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit',
  }).format(date);
};

const EventReceiver = () => {
  const [log, setLog] = useState('');
  useEffect(() => {
    document.addEventListener('ButtonClick', appendEventLog);
    return () => {
      document.removeEventListener('ButtonClick', appendEventLog);
    };
  }, []);

  function appendEventLog(data: CustomEvent<Date>) {
    setLog((value) => value + formatDate(data.detail) + '\r\n');
  }

  return (
    <Content>
      <p>React Component</p>
      <textarea value={log} style={{ height: '10em' }}></textarea>
    </Content>
  );
};
export default EventReceiver;

EventReceiverをexportします

lib.ts

export { default as React } from 'react';
export { default as ReactDOM } from 'react-dom';
export { default as ReactDOMClient } from 'react-dom/client';
// Component
export { default as Counter } from './Counter';
export { default as CallbackCounter } from './CallbackCounter';
export { default as EventReceiver } from './EventReceiver';

2:Webページ側からカスタムイベントを配信する

ボタンのクリック時に、イベントを発生させて現在時刻を渡します

test4.html

<!DOCTYPE html>
<html>
  <body>
    <h2>Webページ側のイベントから、React側の処理を呼び出す</h2>
    <p>
      callback:<span id="callback">
    </p>
    <div id="root"></div>
  </body>
  <script src="/dist/lib.umd.js"></script>
  <script>
    const {ReactDOMClient, React, EventReceiver} = MyBundle;
    const $ = (selectors) => document.querySelector(selectors);
    $('#outerButton').addEventListener('click', () => {
        // イベントを配信
        const event = new CustomEvent('ButtonClick', {detail: new Date()});
        document.dispatchEvent(event);
    });

    const root = ReactDOMClient.createRoot($('#root'));
    root.render(
      React.createElement(EventReceiver)
    );
  </script>
</html>

ボタンをクリックすると、カスタムイベント経由でReactに通知できました

img50.png

参考ページ

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3