tohokuaikiのチラシの裏

技術的ネタとか。

CoreUIの管理画面テンプレートをLaravel11+React+Inertia.jsに組み込みたかった。

前の記事の続き。ただし、「CoreUIのCSSを読み込む 」のところは後述するように変更している。

CoreUIでLaravelの管理画面作るのにテンプレートが必要で、それはnpmで一発インストールできるわけじゃないんですね。見本にあるのをちゃちゃっと行けると思ったんだけど、そんな甘くなかった。

CoreUIはパーツで管理画面はコンポーネント?みたいな。どっちかというとレイアウトか。

CoreUI謹製のLaravelテンプレートもあったんだけど、もう丸っと4年更新が止まっててLaravel7に対応したよってのが最後らしいので自力でガンバるか。 github.com

この記事が「あ、できるんだな」って気持ちにさせてくれるレベルで参考になりました。 techshack-creatives.medium.com

Laravelのセットアップ

前の記事の通り。Breezeを入れて、coreuiをnpmでインストールする。

CoreUI管理画面テンプレートのデータをダウンロードしてZIPを展開

ということでここからデータをダウンロードする。メールを登録するとそこにダウンロードURLが送られてくる。ありがとうございます。

選んだのは、Reactの管理画面テンプレート。 coreui.io

手に入れたZIPファイル( coreui-free-react-admin-template-main.zip)を展開するとこんな感じ。

$ tree .
.
├── LICENSE
├── README.md
├── index.html
├── package.json
├── public
│   ├── favicon.ico
│   └── manifest.json
├── src
│   ├── App.js
│   ├── _nav.js
│   ├── assets
│   │   ├── brand
│   │   │   ├── logo.js
│   │   │   └── sygnet.js
│   │   └── images
│   │       ├── angular.jpg
│   │       ├── avatars
│   │       │   ├── 1.jpg
│   │       │   ├── 2.jpg
│   │       │   ├── 3.jpg
│   │       │   ├── 4.jpg
│   │       │   ├── 5.jpg
│   │       │   ├── 6.jpg
│   │       │   ├── 7.jpg
│   │       │   ├── 8.jpg
│   │       │   └── 9.jpg
│   │       ├── react.jpg
│   │       └── vue.jpg
│   ├── components
│   │   ├── AppBreadcrumb.js
│   │   ├── AppContent.js
│   │   ├── AppFooter.js
│   │   ├── AppHeader.js
│   │   ├── AppSidebar.js
│   │   ├── AppSidebarNav.js
│   │   ├── DocsCallout.js
│   │   ├── DocsExample.js
│   │   ├── DocsLink.js
│   │   ├── header
│   │   │   ├── AppHeaderDropdown.js
│   │   │   └── index.js
│   │   └── index.js
│   ├── index.js
│   ├── layout
│   │   └── DefaultLayout.js
│   ├── routes.js
│   ├── scss
│   │   ├── _custom.scss
│   │   ├── _theme.scss
│   │   ├── _variables.scss
│   │   ├── examples.scss
│   │   ├── style.scss
│   │   └── vendors
│   │       └── simplebar.scss
│   ├── store.js
│   └── views
│       ├── base
│       │   ├── accordion
│       │   │   └── Accordion.js
│       │   ├── breadcrumbs
│       │   │   └── Breadcrumbs.js
│       │   ├── cards
│       │   │   └── Cards.js
│       │   ├── carousels
│       │   │   └── Carousels.js
│       │   ├── collapses
│       │   │   └── Collapses.js
│       │   ├── index.js
│       │   ├── jumbotrons
│       │   │   └── Jumbotrons.js
│       │   ├── list-groups
│       │   │   └── ListGroups.js
│       │   ├── navbars
│       │   │   └── Navbars.js
│       │   ├── navs
│       │   │   └── Navs.js
│       │   ├── paginations
│       │   │   └── Paginations.js
│       │   ├── placeholders
│       │   │   └── Placeholders.js
│       │   ├── popovers
│       │   │   └── Popovers.js
│       │   ├── progress
│       │   │   └── Progress.js
│       │   ├── spinners
│       │   │   └── Spinners.js
│       │   ├── tables
│       │   │   └── Tables.js
│       │   ├── tabs
│       │   │   └── Tabs.js
│       │   └── tooltips
│       │       └── Tooltips.js
│       ├── buttons
│       │   ├── button-groups
│       │   │   └── ButtonGroups.js
│       │   ├── buttons
│       │   │   └── Buttons.js
│       │   ├── dropdowns
│       │   │   └── Dropdowns.js
│       │   └── index.js
│       ├── charts
│       │   └── Charts.js
│       ├── dashboard
│       │   ├── Dashboard.js
│       │   └── MainChart.js
│       ├── forms
│       │   ├── checks-radios
│       │   │   └── ChecksRadios.js
│       │   ├── floating-labels
│       │   │   └── FloatingLabels.js
│       │   ├── form-control
│       │   │   └── FormControl.js
│       │   ├── input-group
│       │   │   └── InputGroup.js
│       │   ├── layout
│       │   │   └── Layout.js
│       │   ├── range
│       │   │   └── Range.js
│       │   ├── select
│       │   │   └── Select.js
│       │   └── validation
│       │       └── Validation.js
│       ├── icons
│       │   ├── brands
│       │   │   └── Brands.js
│       │   ├── coreui-icons
│       │   │   └── CoreUIIcons.js
│       │   ├── flags
│       │   │   └── Flags.js
│       │   └── index.js
│       ├── notifications
│       │   ├── alerts
│       │   │   └── Alerts.js
│       │   ├── badges
│       │   │   └── Badges.js
│       │   ├── index.js
│       │   ├── modals
│       │   │   └── Modals.js
│       │   └── toasts
│       │       └── Toasts.js
│       ├── pages
│       │   ├── login
│       │   │   └── Login.js
│       │   ├── page404
│       │   │   └── Page404.js
│       │   ├── page500
│       │   │   └── Page500.js
│       │   └── register
│       │       └── Register.js
│       ├── theme
│       │   ├── colors
│       │   │   └── Colors.js
│       │   └── typography
│       │       └── Typography.js
│       └── widgets
│           ├── Widgets.js
│           ├── WidgetsBrand.js
│           └── WidgetsDropdown.js
└── vite.config.mjs

とりあえず、この中の src ディレクトリを resources/js/coreui としてコピーする。resources/js/coreui/App.js とかができることになる。

必要なライブラリをインストール

laravelのpackage.jsonとcoreui-templateのpackage.jsonを比較してcoreui-templateで必要なライブラリをインストールする。自分の場合

npm i @coreui/chartjs @coreui/icons @coreui/icons-react @coreui/react-chartjs @coreui/utils chart.js classnames core-js prop-types react-redux react-router-dom redux simplebar-react
npm i -D @vitejs/plugin-react

だった。

Laravel側のファイルの書き換え

resources/js/coreui のコンポーネントファイルを.tsx拡張子にする

TypeScriptで書いているので、.jsなのを.tsxにする。ただし、まとめて読み込んでいるindex.jsやAssetなどは変えない。

  • coreui/components
  • coreui/layout
  • coreui/views

の中にある大文字から始まるファイルがJSXコンポーネントを含んでいるものなので、拡張子を.jsから.tsxに変更。renameコマンド使うならこんな感じ。

 find src/resources/js/coreui -type f -name "*.js" ! -name "index.js"|xargs -I{} rename "s/.js/.tsx/;" {}

レイアウトの resources/js/Layouts/AuthenticatedLayout.tsx (管理画面なので)

resources/js/coreui/App.js を参考にしてreturnを以下の通りにする。

import { Link, usePage } from '@inertiajs/react';
import React, { PropsWithChildren, ReactNode, Suspense, useState } from 'react';
import { CSpinner } from '@coreui/react';
import DefaultLayout from '@/coreui/layout/DefaultLayout';
import { Provider } from 'react-redux'
import { BrowserRouter, Route, Routes } from 'react-router-dom'
import store from '../coreui/store'

export default function Authenticated({
    header,
    children,
}: PropsWithChildren<{ header?: ReactNode }>) {

    const user = usePage().props.auth.user;

    const [showingNavigationDropdown, setShowingNavigationDropdown] =
        useState(false);

    return (
            <BrowserRouter>
                <Provider store={store}>
                    <Suspense
                        fallback={
                            <div className="pt-3 text-center">
                                <CSpinner color="primary" variant="grow" />
                            </div>
                        }
                    >
                        <Routes>
                            <Route path="*" element={<DefaultLayout>
                                {header && (
                                    <header className="bg-white shadow">
                                        <div className="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
                                            {header}
                                        </div>
                                    </header>
                                )}
                                {children}
                            </DefaultLayout>} />
                        </Routes>
                    </Suspense>
                </Provider>
            </BrowserRouter>
   )
}

CSSの読み込み

resources/css/app.scss に以下の行を追加。

@import "@coreui/coreui/scss/coreui";
@import "../js/coreui/scss/theme";

chartjs使いたかったら、@import "@coreui/chartjs/scss/coreui-chartjs"; も追加。

DefaultLayout コンポーネントでchildrenを出す。

これをやらないと、Dashboard.tsxで表示させたいコンテンツが表示されない。

import React, { ReactNode } from 'react'
import { AppContent, AppSidebar, AppFooter, AppHeader } from '../components/index'

const DefaultLayout = (props: {children: ReactNode}) => {
  
  const {children} = props;

  return (
    <div>
      <AppSidebar />
      <div className="wrapper d-flex flex-column min-vh-100">
        <AppHeader />
        <div className="body flex-grow-1">
          <AppContent />
          {children}
        </div>
        <AppFooter />
      </div>
    </div>
  )
}

export default DefaultLayout

SCSSのコンパイルでWarningが出まくるよ問題

CoreUIがSASSでDeprecatedなグローバル変数や@import、mixinを使いまくっているのでWarningが出まくってデバッグしづらい。

そもそも、コンパイルする時間が長い…

ってことで、もう最初からコンパイルしたものを@importだけでいいんじゃないかな?って思った。

管理画面テンプレートをbuildする。

全然別のディレクトリに coreui-free-react-admin-template-main.zip を持ってきて以下の作業をする。

unzip coreui-free-react-admin-template-main.zip
cd coreui-free-react-admin-template-main
npm i
npm run build

で、 build/assets/index-wUvqVzu6.css みたいなファイルができるのでこれをLaravelの resources/css/coreui-template.css とでもコピーして resources/css/app.scss で@importする。

@tailwind base;
@tailwind components;
@tailwind utilities;
 
/* coreuiコンポーネント */
@import './coreui-template.css';

ルーティングの問題

3つ考えなければならない。

  • Laravel側のルーティング
  • ReactのRoute登録
  • 管理メニューの変更

後者2つのCoreUIのテンプレートの2つは関連しているのかと思いきや、全然そうでもなかった。ReactのRouteの登録は、resources/js/coreui/routes.js で、左側管理メニューは resources/js/coreui/_nav.tsx で行っている。両方のPathを合わせてやるとDashboard.tsxからリンクを踏んだ時にページが表示される。まぁ、なかなか一発でimportされんくてlazyloadとかを直接importにしたり、ちょいちょいと変更しなければならないけど。

ただ、このReactのルーティングを行っても http://localhost:8000/admin/buttons/dropdowns直接アクセス すると404エラーになってしまう。Laravel側のルーティングが無いからね。

とういことで、LaravelのRoutingを少し変更。

<?php
Route::middleware(['auth', 'verified'])->name('admin')->any('/admin/{operation}/{target?}', function(){
    return Inertia::render('Dashboard', []);
});

どうせGETしか来ないから、->anyじゃなくて->getでもいいかも。

Reactのページコンポーネントが表示されるまでの道のり(ここまでのまとめ)

  1. routes/web.phpで任意のルーティングにはReactを起動させるようにする。
    Inertia::render('Dashboard', []); をreturnさせると、Dashboardコンポーネントをブラウザのファーストアクセス時に起動する。ただし、このDashboardコンポーネントのパスは後で決められる。
  2. app/Http/Middleware/HandleInertiaRequests.php で、HandleInertiaRequests::rootView()の返すbladeテンプレートを決める。返り値が admin だと、resources/views/admin.blade.php がそれになる。
  3. そのbladeテンプレートで使える変数、 $page['component'] が先ほどのInertia::render()の第一引数でReactのページコンポーネントである。render()の第二引数はそのコンポーネントのpropsに渡す値。
  4. bladeテンプレートでは、以下の3行でReactを呼び出す。
    @viteReactRefresh
    @vite(['resources/js/admin.tsx', "resources/js/Pages/Admin/{$page['component']}.tsx"])
    @inertiaHead
    2行目の配列は、1つ目がメインのInertia.jsを記述する。上記の場合は、 resources/js/admin.tsxとなる。これはBreezeをインストールされる時に自動生成されるのでコピーさせて増やせる。resolvePageComponent()で対象にするページコンポーネントのファイルを指定する。2つ目に最初のReactページコンポーネントを指定する。この場合、$page['component']はDashboardになる。ので、 resources/js/Pages/Admin/Dashboard.tsx が該当のページコンポーネントである。
  5. このDashboardコンポーネントなどのReactコンポーネントでは、HandleInertiaRequests::share()で返す値を usePage().props として取り出せる。

で、ここまで作って、管理者画面のDashboardコンポーネントはただの入り口ではなくすべてを受けるデフォルトページなんだと気づいた。ReactのRouteでいうところのってやつ。resources/js/Pages/Admin/Dashboard.tsxresources/js/Pages/Admin/Default.tsx と変更する。

で、色々とやったのをここに

github.com

置いておく。