Next.jsでIonic Frameworkを使う

この記事は、Next.js Advent Calendar 2020の8日目です。 Ionic Framework(@ionic/react)をNext.jsで使おうとすると、だいたいこういうエラーに遭遇します。 なにが […]

広告ここから
広告ここまで

目次

    この記事は、Next.js Advent Calendar 2020の8日目です。

    Ionic Framework(@ionic/react)をNext.jsで使おうとすると、だいたいこういうエラーに遭遇します。

    % yarn build 
    yarn run v1.22.5
    $ yarn --cwd packages/web-app build
    $ next build
    info  - Creating an optimized production build  
    info  - Compiled successfully
    
    > Build error occurred
    ReferenceError: HTMLElement is not defined

    なにがおきているのか

    IonicはWebComponent(Stencil)で作られたフレームワークです。そのため、各タグを利用するためには、defineCustomElements()を実行してカスタムエレメントを登録する必要があります。

    そして@ionic/reactでは、この事前準備系はimportするだけで内部的に実行されるように作られています。便利ですね。

    しかしこの内部的によしなにする部分がNext.jsやGatsbyで引っかかります。というのも、defineCustomElementsの引数はwindowだからです。これはNode.jsでは存在しないものですので、SSRまたはSSGを実行すると、Not Foundエラーが発生します。

    コードを見る限り、typeof window !== 'undefined'のチェックはあるのですが、どうも動かないのでどうしたものかなというところです。Issueなどを調べるといろいろと各自試行錯誤されている様子が窺えます。

    個人的解決方法: 思い切って@ionic/coreをつかう

    現在の本サイトがNext.js + Ionicなのですが、実はここでは@ionic/reactを使っていません。

    Next.jsのサンプルリポジトリを見ると、Stencilを使ったversionが用意されていました。
    https://github.com/vercel/next.js/tree/canary/examples/with-stencil

    これをみるに、Web Componentが動かないのではなく、defineCustomElementの実行場所次第でこけることがあるという理解が良さそうです。

    ということで、Ionicの本体でもある@ionic/coreそのものを使う形で実装します。

    Step by step

    Step1: Create Next.js application

    # Setup project
    $ npx create-next-app nextjs-ionic
    
    # Go to web root
    $ cd nextjs-ionic
    
    # Put tsconfig.json
    $ touch tsconfig.json
    
    # Install TypeScript packages
    $ yarn add -D typescript @types/react @types/node

    Step2: Install Ionic components

    @ionic/coreを使う場合、一緒にioniconsもインストールしましょう。

    $ yarn add @ionic/core ionicons

    Step3: Define Custom Elements Types

    JSXでIonicのタグを使う場合、JSXの型情報に追加登録してやる必要があります。

    pages/_app.tsx

    import { JSX as LocalJSX} from '@ionic/core'
    import {JSX as IoniconsJSX} from 'ionicons'
    import { HTMLAttributes, ReactText } from 'react'
    
    type ToReact<T> = {
      [P in keyof T]?: T[P] & Omit<HTMLAttributes<Element>, 'className'> & {
        class?: string;
        key?: ReactText;
      }
    }
    
    declare global {
      export namespace JSX {
        interface IntrinsicElements extends ToReact<LocalJSX.IntrinsicElements & IoniconsJSX.IntrinsicElements> {}
      }
    }

    Step4: Define Custom Element and import CSS

    続いでCSSとloaderを実装します。defineCustomElementsを実行する必要がありますので、こちらはuseEffectを使って副作用側で処理しましょう。

    pages/_app.tsx

    import React, { useEffect } from 'react'
    import { defineCustomElements as ionDefineCustomElements } from '@ionic/core/loader';
    /* Core CSS required for Ionic components to work properly */
    import '@ionic/core/css/core.css';
    
    /* Basic CSS for apps built with Ionic */
    import '@ionic/core/css/normalize.css';
    import '@ionic/core/css/structure.css';
    import '@ionic/core/css/typography.css';
    
    /* Optional CSS utils that can be commented out */
    import '@ionic/core/css/padding.css';
    import '@ionic/core/css/float-elements.css';
    import '@ionic/core/css/text-alignment.css';
    import '@ionic/core/css/text-transformation.css';
    import '@ionic/core/css/flex-utils.css';
    import '@ionic/core/css/display.css';
    
    function MyApp({ Component, pageProps }) {
      useEffect(() => {
        ionDefineCustomElements(window)
      })
      return <p>Hello</p>
    }
    
    export default MyApp
    

    Finally: Use Ionic component into the application

    あとはIonic Angularライクにコードを書くだけです。

    pages/home.tsx

    import Image from 'next/image'
    
    export default function Home() {
      return (
            <ion-refresher slot="fixed" closeDuration="10ms">
              <ion-refresher-content />
              <ion-card>
                <ion-card-header>
                  <ion-card-subtitle>Card Subtitle</ion-card-subtitle>
                  <ion-card-title>Card Title</ion-card-title>
                </ion-card-header>
    
                <ion-card-content>
                <Image
                  src="./images"
                  alt="Picture of the author"
                  width={500}
                  height={300}
                />
                  Keep close to Nature's heart... and break clear away, once in awhile,
                  and climb a mountain or spend a week in the woods. Wash your spirit clean.
                </ion-card-content>
              </ion-card>
            </ion-refresher>
      )
    }
    

    ioniconのSVGが表示されない場合は、webpack + リダイレクトで対応する

    再現性がある問題なのかまで掘り下げれていないのですが、ioniconのアイコンSVGが描画されない時がありました。

    SVGのパスが意図した通りにならない様子なので、自分のサイトの場合は、WebpackのCopyPluginを使ってコピーして、Netlifyのリダイレクトを使って調整しています。

    const CopyPlugin = require('copy-webpack-plugin')
    const path= require('path')
    
    module.exports = {
    ...
      pageExtensions: ['ts', 'tsx'],       
        webpack: (config) => {       
          config.plugins.push(      
            new CopyPlugin({                       
              patterns: [{      
                from: path.join(__dirname, 'node_modules/ionicons/dist/ionicons/svg'),       
                to: path.join(__dirname, 'public/svg')   
              }]       
            })       
          )       
          return config         
        }          
      }

    Netlify側はこんな感じ

    [[redirects]]      
        from = "/:lang/:slug/svg/:filename"    
        to = "/svg/:filename"       
        status = 200       
                    
    [[redirects]]    
        from = "/:slug/svg/:filename"    
        to = "/svg/:filename"      
        status = 200

    注意点など

    割と力技的解決ですが、簡単なブログサイトを作る程度であれば問題なく動いています。いまのところ。

    ただ、ionXXX系の挙動がちょっと怪しい気配があったり、@ionic/reactで未対応のものはやっぱり動かなかったりと、なかなか縛りプレイになることはご留意ください。

    サンプル

    広告ここから
    広告ここまで
    Home
    Search
    Bookmark