Reactの勉強を兼ねて、スタートガイドの
クイックスタートをOracle APEXのアプリケーションに実装してみます。Oracle APEXではReactのJSXをそのまま扱うことはできないため、ブラウザ上でのJSXの実行とJSXファイルのコンパイルに
Babelを使用します。
最初に空のAPEXアプリケーションを作成します。名前はReact Quick Startとしました。
アプリケーションの作成を実行します。
アプリケーションが作成されます。
このアプリケーションのホーム・ページにReactのコンポーネントを埋め込みます。
作成予定のReactのコンポーネントの多くは、ローカルのPC上にファイルとして作成します。作成したJSXファイルをBabelを使ってJavaScriptのファイルにコンパイルし、APEXのアプリケーションに組み込みます。
JSXファイルを配置するフォルダを作成します。名前はquickstartとしました。
フォルダquickstartにreactおよびreact-domをインストールします。インストールするパッケージのバージョンを明示していますが、これはブラウザに設定するimportmapを、npmでインストールしたreact、react-domのバージョンと一致させるためです。
npm install react@18.3.1 react-dom@18.3.1
quickstart % npm install react@18.3.1 react-dom@18.3.1
added 5 packages in 250ms
quickstart %
Babelをインストールします。
npm install --save-dev @babel/core @babel/cli @babel/preset-react quickstart % npm install --save-dev @babel/core @babel/cli @babel/preset-react
npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
added 89 packages, and audited 95 packages in 804ms
8 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
quickstart %
JSXのコンパイルを行うため、.babelrcにプリセットとして@babel/preset-reactを設定します。ファイル.babelrcを作成し、以下の内容を記述します。
{
"presets": ["@babel/preset-react"]
}
quickstart % cat .babelrc
{
"presets": ["@babel/preset-react"]
}
quickstart %
VS Codeを起動し、フォルダquickstartを開きます。
Oracle APEXのセッション・オーバーライドを活用して、ローカルのPC上のファイルを静的アプリケーション・ファイルとして参照できるようにします。
フォルダquickstart直下にファイルindex.htmlを作成します。ファイルには以下を記述します。
<html>
<body>
Live Server is ready.
</body>
</html>
作成したファイルindex.htmlを保存します。コンテキスト・メニューを開き、Open with Live Serverを実行します。
ブラウザの画面が開き、Live Server is ready.と表示されます。アクセスしているURLは以下になります。
http://127.0.0.1:5500/index.html
これでセッション・オーバライドを使って、フォルダquickstart以下のファイルを静的アプリケーション・ファイルとして参照できるようになりました。
すでに静的アプリケーション・ファイルとして作成済みのアイコン・ファイルを、フォルダquickstart以下にコピーします。
共有コンポーネントの静的アプリケーション・ファイルを開きます。
Zipとしてダウンロードをクリックし、静的アプリケーション・ファイルをZip形式で手元のPCにダウンロードします。ダウンロードされるZipファイルの名前は以下の形式になります。
f<アプリケーションID>_static_application_files.zip
ダウンロードされたZipファイルをフォルダquickstart以下に展開します。
unzip ~/Downloads/f165_static_application_files.zip
quickstart % unzip ~/Downloads/f165_static_application_files.zip
Archive: /___/_______/Downloads/f165_static_application_files.zip
Implementation by Anton Scheffer
extracting: icons/app-icon-144-rounded.png
inflating: icons/app-icon-192.png
inflating: icons/app-icon-256-rounded.png
extracting: icons/app-icon-32.png
inflating: icons/app-icon-512.png
quickstart %
作成したAPEXアプリケーションを実行し、セッション・オーバーライドを設定します。
開発者ツール・バーのセッション・オーバーライドを開きます。
ファイル・パスの
アプリケーション・ファイルを
オンにします。
セッション・オーバーライドの有効化も同時に
オンに変わります。
アプリケーション・ファイルにLive Serverの待ち受けURLである、以下の値を設定します。
http://127.0.0.1:5500/
変更を保存します。
セッション・オーバーライドが有効になっているか確認するため、表示されているアイコンの画像アドレスのコピーを実行します。
コピーした画像アドレスが以下であれば、セッション・オーバーライドは有効です。
http://127.0.0.1:5500/icons/app-icon-512.png
これから、ホーム・ページにReactコンポーネントを実装していきます。
ホーム・ページのページ・プロパティのHTMLヘッダーに、Reactを読み込むための
importmapを記述します。
<script type="importmap">
{
"imports": {
"react": "https://cdn.jsdelivr.net/npm/react@18.3.1/+esm",
"react-dom": "https://cdn.jsdelivr.net/npm/react-dom@18.3.1/+esm"
}
}
</script>
reactとreact-domのバージョンは必ず一致させます。バージョンが未指定であったり、最新バージョンを選択するようにしていると、reactとreact-domのバージョンが一致しないことがあります。その場合、useStateなどのフックの利用時にエラーが発生することがあります。(例えばcannot read properties of null (reading 'useState')といったエラー)。
できるだけエラーを発生させないように、npmでinstallしたreactおよびreact-domのバージョンと、importmapのバージョンは一致させます。
また、Babelを呼び出すためにJavaScriptのファイルURLに以下を記述します。
https://cdn.jsdelivr.net/npm/@babel/standalone/babel.min.js
最初にJSXファイルを作成せず、APEXのリージョンに直接Reactコンポーネントを記述してみます。
静的コンテンツのリージョンとしてMyButtonを作成します。ソースのHTMLコードとして以下を記述します。SCRIPT要素のtypeとしてtext/babelを設定し、Babelを呼び出すことでJSXを解釈します。
アプリケーションを実行すると、以下のように表示されます。
同じコンポーネントを、今度は
App.jsxという名前のファイルとして作成します。作成したApp.jsxをBabelでJavaScriptファイルにコンパイルし、コンパイルされたApp.jsをAPEXアプリケーションに読み込みます。
App.jsxからJavaScriptファイルApp.jsを作成します。
npx babel App.jsx --out-file App.js
quickstart % npx babel App.jsx --out-file App.js
quickstart %
作成済みのリージョン
MyButtonを
コメント・アウトし、新たにリージョン
Appを作成して、
HTMLコードに以下を記述します。
<div id="container"></div>
<script type="text/babel" data-presets="react" data-type="module">
import React from 'react';
import ReactDOM from 'react-dom';
import App from "#APP_FILES#App.js";
/* render React component within an APEX region */
const container = document.getElementById("container");
const root = ReactDOM.createRoot( container );
root.render( <App /> );
</script>
ホーム・ページを実行すると、以下のように表示されます。表示自体は、先ほどのMyButtonリージョンと同じです。
次にProfile.jsxを作成します。コードは以下です。
Babelでコンパイルします。
npx babel Profile.jsx --out-file Profile.js
作成済みのリージョン
Appを
コメント・アウトし、新たにリージョン
Profileを作成して、
HTMLコードに以下を記述します。import文とrender文に指定されていたAppをProfileに置き換えています。
<div id="container"></div>
<script type="text/babel" data-presets="react" data-type="module">
import React from 'react';
import ReactDOM from 'react-dom';
import Profile from "#APP_FILES#Profile.js";
/* render React component within an APEX region */
const container = document.getElementById("container");
const root = ReactDOM.createRoot( container );
root.render( <Profile /> );
</script>
ホーム・ページを実行すると、以下のように表示されます。
CSSの指定が不足しているので、ページ・プロパティのCSSのインラインに以下を追記します。APEXでのクイックスタートの実装は、IDがcontainerのdiv要素をReactコンポーネントのrootとしているため、#contaierをスコープとしています。
@scope(#container) {
.avatar {
border-radius: 50%;
}
}
CSSを適用すると、Reactのクイックスタートと同じように表示されます。
ShoppingList.jsxを作成します。コードは以下です。
Babelでコンパイルします。
npx babel ShoppingList.jsx --out-file ShoppingList.js
作成済みのリージョン
Profileを
コメント・アウトし、新たにリージョン
ShoppingListを作成して、
HTMLコードに以下を記述します。
<div id="container"></div>
<script type="text/babel" data-presets="react" data-type="module">
import React from 'react';
import ReactDOM from 'react-dom';
import ShoppingList from "#APP_FILES#ShoppingList.js";
/* render React component within an APEX region */
const container = document.getElementById("container");
const root = ReactDOM.createRoot( container );
root.render( <ShoppingList /> );
</script>
ホーム・ページを実行すると、以下のように表示されます。
フックのuseStateを使ったボタンをクリックするサンプルを、MyApp.jsxとして作成します。
Babelでコンパイルします。
npx babel MyApp.jsx --out-file MyApp.js
作成済みのリージョンShoppingListをコメント・アウトし、新たにリージョンMyAppを作成して、HTMLコードに以下を記述します。
<div id="container"></div>
<script type="text/babel" data-presets="react" data-type="module">
import React from 'react';
import ReactDOM from 'react-dom';
import MyApp from "#APP_FILES#MyApp.js";
/* render React component within an APEX region */
const container = document.getElementById("container");
const root = ReactDOM.createRoot( container );
root.render( <MyApp /> );
</script>
ホーム・ページを実行すると、以下のように表示されます。
ボタンが横並びになっているのはさておき、ボタンをクリックするとAPEXのデフォルトの動作であるページの送信が行われてしまいます。
ページ・プロパティのJavaScriptのページ・ロード時に実行に以下のJavaScriptを記述し、Reactコンポーネント内でのボタンクリックでページの送信が行われないようにします。
ページ送信を抑制すると、useStateを使ったサンプルが想定通りに動作します。
コンポーネント間でデータを共有する実装を、MyApp2.jsxとして作成します。
Babelでコンパイルします。
npx babel MyApp2.jsx --out-file MyApp2.js
作成済みのリージョンMyAppをコメント・アウトし、新たにリージョンMyApp2を作成して、HTMLコードに以下を記述します。
<div id="container"></div>
<script type="text/babel" data-presets="react" data-type="module">
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
import MyApp from "#APP_FILES#MyApp2.js";
/* render React component within an APEX region */
const container = document.getElementById("container");
const root = ReactDOM.createRoot( container );
root.render( <MyApp /> );
</script>
ホーム・ページを実行すると、以下のように表示されます。
チュートリアルの三目並べを実装します。Game.jsxを作成します。
Babelでコンパイルします。
npx babel Game.jsx --out-file Game.js
作成済みのリージョンMyApp2をコメント・アウトし、新たにリージョンGameを作成して、HTMLコードに以下を記述します。
<div id="container"></div>
<script type="text/babel" data-presets="react" data-type="module">
import React from 'react';
import ReactDOM from 'react-dom';
import Game from "#APP_FILES#Game.js";
/* render React component within an APEX region */
const container = document.getElementById("container");
const root = ReactDOM.createRoot( container );
root.render( <Game /> );
</script>
ページ・プロパティのCSSのインラインに以下を追記します。ファイルを作成してインポートしても良いでしょう。
ホーム・ページを実行すると、以下のように表示されます。
チュートリアルのReactの流儀のセクションで紹介されているアプリケーションを実装します。App3.jsxを作成します。
Babelでコンパイルします。
npx babel App3.jsx --out-file App3.js
作成済みのリージョンGameをコメント・アウトし、新たにリージョンApp3を作成して、HTMLコードに以下を記述します。
<div id="container"></div>
<script type="text/babel" data-presets="react" data-type="module">
import React from 'react';
import ReactDOM from 'react-dom';
import App from '#APP_FILES#App3.js';
/* render React component within an APEX region */
const container = document.getElementById("container");
const root = ReactDOM.createRoot( container );
root.render( <App /> );
</script>
ホーム・ページを実行すると、以下のように表示されます。
Reactのクイックスタートを、一通りOracle APEXで動かせました。
Babelでコンパイルして作成したJavaScriptファイルを静的アプリケーション・ファイルとして保存することにより、ローカルのファイルを参照せずにReactコンポーネントを含むAPEXアプリケーションを実行できます。
共有コンポーネントの静的アプリケーション・ファイルを開きます。
ファイルの作成をクリックします。
コンテンツとして作成したJavaScriptファイル(App.js、Profile.js、ShoppingList.js、MyApp.js、MyApp2.js、Game.js、App3.js)をそれぞれ、作成します。
ファイルをすべてアップロードすると、以下のようになります。APEXの静的アプリケーション・ファイルとしてJavaScriptファイルを作成すると、自動的にミニファイされたファイルも作成されますが、ファイルをアップロードした場合はミニファイされません。
今回はミニファイされたファイルは使用していないため、ミニファイは行いません。
JavaScriptファイルをアップロードしたら、セッション・オーバーライドを外します。
セッション・オーバーライドを外すと、以下のエラーが発生します。
Failed to resolve module specifier "r/apexdev/165/files/static/v22/App3.js". Relative references must start with either "/", "./", or "../".
内容としては、import from の指定としてr/apexdev/165/... という形式ではなく、相対パスの場合は、/ や ./ や ../ で始まるように記述してください、と言われています。
import文を以下のように書き換えるとエラーは発生しなくなりますが、セッション・オーバーライドを使って、Live Serverを指定できなくなります。
import App from './#APP_FILES#App3.js';
今回はアプリケーション定義のユーザー・インターフェースの詳細の#APP_FILES#のパスを設定することで、このエラーを回避します。
./r/apexdev/165/files/static/v22/
./以降に付与される文字列はAPEXワークスペース名やアプリケーションIDが含まれるため、環境やアプリケーションごとに値は異なります。
以上の変更を実施することで、Reactのクイックスタートで作成しているアプリケーションをAPEXのアプリケーションに組み込むことができました。
今回作成したAPEXアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/react-quick-start.zip
Oracle APEXのアプリケーション作成の参考になれば幸いです。
完