NodeJS のプロジェクトを作りたいので、package.jsonがまだの場合はyarn init -ypackage.jsonを作っておきます。

依存インストール

次にnextをインストールします。nextreactreact-domが共にインストールされていることを期待しているのでその2つもインストールします。

yarn add next react react-dom

設定ファイル作成

🎉next@^9からこのファイルは作成しなくても良くなりました。

next.config.js"Hello from NextJS"

// next.config.js
module.exports = {};
2019年10月以前の内容というファイルを作ります。とはいっても今はただと表示させたいだけなので設定内容はただの空オブジェクトです。~~

ページ作成

NextJS ではsrc/pagesディレクトリに実際のページとなるファイルを作成していきます。src/pages/index.jsxindex.htmlというような意味になるのでこのファイルを作成しましょう。

// /pages/index.jsx
export default () => 

Hello from NextJS

ES で書かれていますが、 NextJS は逆に ES でしか書くことができないのでこれで大丈夫です。

ページ確認

./node_modules/.bin/next dev -p 3333と実行しましょう。サーバーが立ち上がり、localhost:3333へアクセスして"Hello from NextJS"と表示されれば大丈夫です。

この結果のリポジトリです。

public/ディレクトリへ静的なファイルを置くとそれがそのまま配信されます。例えばpublic/foo.pngのようなファイルを置いたとすると、/foo.pngでアクセスできるようになります。

これは必ずpublic/でなければなりません。src/publicでは配信されないので注意しましょう。

.
├── public
│   └── cats.jpeg
└── src
    └── pages
        └── index.js
        

またnext@<9(バージョンが9より小さい)頃は、これはstatic/ディレクトリの役割でした。この2つの違いはstatic/では/static/foo.pngのようなファイルにアクセスするには/static/foo.pngとそのまま書かなければなりませんでした。

関連

pages/以下に置いたファイルは規則によって自動的にルーティング設定されます。例えば、

  • pages/index.js/

  • pages/products/index.js/products

  • pages/products/related.jsは/products/related`

のような感じです。見ての通りファイル構造そのままでルート設定されます。またindex.*な時はディレクトリ名がそのファイルのルートになります。

クエリ付きツート

[ ]で囲んだファイル名・ディレクトリ名(例えばpages/products/[productId].js)にすると、対象のファイルは動的に内容を切り替えられるようになります。その仕組はpages/からの絶対パスの中で[ ]で囲まれた部分はすべてgetInitialPropsqueryプロパティに格納されて来るからです。

例えばpages/products/[productId]/[productColor].jsのようなページの場合、

{
  productId: 123,
  productColor: 'orange'
}

のようなクエリオブジェクトを動的に取得できるので、これを使って結果ページを切り替えることができます。

🎉nexts@^9.1(2019-11)からデフォルトでpages/の代わりにsrc/pages/ディレクトリでも大丈夫なようになりました。#

よって以下の内容は古くなりました。

nextコマンドの引数の最後はdirです。これはデフォルトでは.が使われて、これで./pagesがページとして処理される形になっています。
なのでsrc/pagesに変更したい場合は、このように実行するだけです。

next src
next build src
next export src

他のコマンドでも同じようにsrc指定が必要です。ちなみにnext.config.jsdirを設定でいいのではと思うかもしれませんが、これを書いてる時点のnext@^8.0.3では何故か効きません。

dir (string) where the Next project is located - default '.'

らしいですが。。

react-transition-group_app.jsxの中で使います。_appファイルで定義したコンポーネントはクライアント側で唯一マウントし続けるコンポーネントで、このコンポーネントが受け取るプロパティのComponentに対象のpages/*.jsxファイルのコンポーネントが入る形で来ます。

以下にフェードイン・アウトを想定した例です。( TypeScript ) router.routeCSSTransitionkeyとして使っています。これはページ移動した時にrouter.routeが切り替わり、古いkeyのものはexitに向かい、新しいkeyのページコンポーネントはenterに向かうような形になります。Componentは1つしかないはずですが、TransitionGroupを使うことでアニメーション動作中に限りそれら新古のページコンポーネントを共存させられます。アニメーション中はTransitionGroupが生かしてくれる為です。

export default class extends App {
  addEventListener = (node: HTMLElement, done: () => void) => {
    const handle = () => {
      node.removeEventListener('transitionend', handle);
      done();
    };

    node.addEventListener('transitionend', handle);
  };

  render() {
    const {Component, pageProps, router} = this.props;

    return (
      
        
          
            
              
                
              
            
          
        
      
    );
  }
}

ちなみにFadestyled-componentsになります。これで.fade-<suffix>クラス名を使いアニメーションを実装します。

import styled from 'styled-components';

const Fade = styled.div`
  .fade-enter {}
  .fade-enter-active {}
  .fade-enter-done {}
  
  .fade-exit {}
  .fade-exit-active {}
  .fade-exit-done {}
`;

NextJS のドキュメントを見ると以下のように使われている例が載っています。このasが何に使われているのかです。

href と asの違い

href

hrefに設定する値は NextJS が見る URL です。
NextJS では /pages 以下にページファイルを作っていきます。例えば /foo という URL にページを作りたいなら/pages/foo.jsxというファイルを作り、Linkhref属性には/fooを設定すればいいです。

as

asはブラウザが見る URL です。 例えば、nantoka.com/hoge/以下で NextJS で作ったサイトを公開したい場合、<Link href="/foo" />ではnantoka.com/foo/にページ遷移してしまい、リロードすると NextJS 外の URL なので正常にサイトを表示できなくなってしまいます。
そのような場合にas属性に/hoge/fooと指定してあげることで、ブラウザの URL は nantoka.com/hoge/fooとしながら、表示するページは/foo(/pages/foo)とすることができます。

Styled JSX を TypeScript と共に使うには<style>タグの属性タイプにjsxglobalを追加する作業から始めなければならないかもしれません。それは以下でできます。

 import * as React from 'react';
 
 declare module 'react' {
   interface StyleHTMLAttributes extends React.HTMLAttributes {
     jsx?: boolean;
     global?: boolean;
   }
 }

jsx での<style>の属性はReact.StyyleHTMLAttributesによって定義されている為、そこへ TypeScript の同 Interface 名のプロパティ型はマージされる機能を使い、追記しています。

例えばこのサイトだとIntersectionObserverを使っていて、これはスマホなどでまだ対応しておらず見れないことが多いので含めています。

なぜ Webpack の entry に含めたいのか

<script>での読み込みは Lighthouse のスコアを下げる

HTMLで以下の Polyfil を読み込むことで、IntersectionObserverを対応していないサイトでも使えるようにできます。

<script src="https://polyfill.io/v2/polyfill.min.js?features=IntersectionObserver"></script>

しかしこれは、Minimize Critical Requests Depthとして引っかかってしまいます。

NextJS の entry ファイルのルートで Polyfil を読み込む

上のnpm版である、intersection-observerを使います。

Polyfil は基本エントリーの最初に読み込むファイルの先頭にimportを記載すれば使えるので、_app.[jt]sx?をその形にすれば使えそうです。しかし、この Polyfil は中で window オブジェクトへアクセスしている部分があり NextJS ではその_appファイルも SSR で使われるので、 「windowundefinedだよ」なエラーが起こってしまうのでこれも駄目です。

// _app.tsx
import 'intersection-observer';

NextJS の Webpack-Config の entry に含める

next.config.jsからwebpack設定を自身の設定で上書きする方法です。

next.config.jsのデフォルトentry() => Promise<{[x: string]: string[]}>な形をしていて、このオブジェクトを上書きすれば良さそうです。以下は戻り値の例です。

Promise {
  { 'main.js': [],
    'static/runtime/main.js':
     [ '/Users/nju33/github/blog/node_modules/next/dist/client/next-dev' ],
    'static/development/pages/_app.js': [ './pages/_app.tsx' ],
    'static/development/pages/_error.js':
     [ '/Users/nju33/github/blog/node_modules/next/dist/pages/_error.js' ] } }

このmain.jsへ含めてます。

{
  webpack: config => {
    const originalEntry = config.entry;
    config.entry = () => {
      return originalEntry().then(entry => {
        entry['main.js'] = 'intersection-observer';
        return entry;
      }
    }
  }
}

ただまだこれだと、サーバーサイド用のビルドファイルにもintersection-observer Polyfil が含まれてしまい、上記と同じエラーが起きてしまうので、含めるのは Client 用のビルドファイルだけにする必要があります。
幸いにも、next.config.jswebpack関数は第二引数にoptions.isServerというフラグを渡してくれます。これは Client 用のビルド時にはfalseになってくれるフラグです。

つまり、

  • options.isServertrueのときは、デフォルトのままのentryを返す

  • falseのときは、intersection-observerを含める

とすれば解決できそうです。config.entryを以下のように改良してみます。

{
  webpack: (config, {isServer}) => {
    const originalEntry = config.entry;
    config.entry = () => {
      if (isServer) {
        return originalEntry;
      }
      
      return originalEntry().then(entry => {
        entry['main.js'] = 'intersection-observer';
        return entry;
      }
    }
  }
}

上のように変更したところ Lighthouse のスコアがあがり、かつIntersectionObserverに対応していない端末も対応することができました。

以下では、getInitialPropsfoo: 1という値で返しているので、this.props.fooには1が入ってきます。が、以下のようにconnectでバインド同じプロパティに対してバインドしてしまうとconnectで渡したmapStateToPropsの値で上書きされてしまいfoo: 9になってしまいます。

class FooBase extends React.Component {
  static getInitialProps() {
    return {
      foo: 1
    }
  }

  render() {
    console.log(this.props.foo)
    // output: 9

    return 
foo
} } connect(() => ({foo: 9})(FooBase)

少し考えれば当たり前な気もしますが、少しハマりました。ステート構造を見直すなどするといいかもしれません。

getInitialPropsスタティックメソッドは React のライフサイクルのメソッド群のようにサーバーサイドでもクライアントサイドでも呼び出されます。サーバーサイドで返した値はwindow.__NEXT_DATA__から取得できるので、クライアントサイドではここから値を取得することで通信なしでいける場合があります。

req か res を使ってサーバーサイドでのみ実行される処理を書く

(追記@2019-05-26) 以下のような判定をしなくてもprocess.browserという変数が設定されていました。名前から察せれるようにサーバー側ではfalseでブラウザ側ではtrueになるのでこれを使ったほうが楽です。next@^8.1.0での確認です。

typescript process.browser にアクセスできる型定義

processNodeJS.Processを見ているのでこれを拡張してあげます。

declare namespace NodeJS {
  interface Process {
    browser: boolean;
  }
}

というよりnext@8ぐらいからgetInitialPropsはサーバーでしか動かなくなったかもしれないので以下は不要かもしれません。

このgetInitialPropsに渡ってくるコンテキストのreqresというプロパティはサーバーサイドだけでしか渡ってこない値です。ですのでこれを使ってサーバーサイド判定ができます。

以下は例コードです。reqがある場合はサーバーサイドなのでデータを取得する処理を行い、ない場合(クライアントサイド)ではサーバーサイドで返した値をただ渡すことでクライアントサイドでの通信を無くしています。。

class extends React.Component {
  static async getInitialProps({req}) {
    let data;
    if (req) {
      data = await axios.get('https://xxx.com/xxx')
    } else {
      data = window.__NEXT_DATA__.props.pageProps.data;
    }
    
    return {data};
  }
}

JavaScript で飯食べたい歴約 9 年、 純( nju33 ) によるノートサイトです。

このサイトではドリンク代や奨学金返済の為、広告などを貼らせて頂いてますがご了承ください。

Change Log