Preact、Unistore、およびPreactルーターを使用してSSRアプリを構築する

提供:Dev Guides
移動先:案内検索

序章

シングルページアプリは、最新のWebアプリケーションを構築するための一般的な方法です。 SPAに関しては、アプリのコンテンツをユーザーにレンダリングする方法が2つあります。クライアント側のレンダリングとサーバー側のレンダリングです。

クライアント側のレンダリングでは、ユーザーがアプリを開くたびに、レイアウト、HTML、CSS、およびJavaScriptをロードするためのリクエストが送信されます。 アプリケーションのコンテンツがJSスクリプトの正常なロードの完了に依存している場合、これは問題になる可能性があります。 これは、スクリプトのロードが完了するのを待つ間、ユーザーはプリローダーを表示することを余儀なくされることを意味します。

サーバーサイドレンダリングの動作は異なります。 SSRを使用すると、最初のリクエストで最初にページ、レイアウト、CSS、JavaScript、コンテンツが読み込まれます。 SSRは、レンダリング時にデータが適切に初期化されることを確認します。 サーバー側のレンダリングは、検索エンジン最適化にも適しています。

このチュートリアルでは、Preactを使用してサーバー側でレンダリングされたアプリを構築する方法について説明します。 preact-router はルーティングに使用され、 unistore は状態管理に使用され、WebpackはJSバンドリングに使用されます。 Preact、Unistore、およびWebpackに関する既存の知識が必要になる場合があります。

テクノロジー

このチュートリアルでは、次のテクノロジーを使用してサーバー側レンダリングアプリを構築します。

  • Preact-同じAPIでReactの代替手段。 PropTypesやChildrenなどの一部の機能は削除されていますが、Reactと同様の開発エクスペリエンスを提供することを目的としています。
  • Unistore-ReactとPreactのコンポーネントバインディングを備えた一元化された状態コンテナー。
  • Preactルーター-Preactアプリケーションでルートを管理するのに役立ちます。 URLがパスと一致したときに子を条件付きでレンダリングする<Router />コンポーネントを提供します。
  • Webpack-ブラウザーで使用するためにJavaScriptファイルをバンドルするのに役立つバンドラー。

Preactを使用したSSRアプリの構築

このアプリの構築は2つのセクションに分かれます。 まず、NodeとExpressに含まれるコードのサーバー側を構築します。 その後、コードのPreact部分をコーディングします。

アイデアは、Preactアプリをそのまま作成し、preact-render-to-stringパッケージを使用してノードサーバーに接続することです。 これにより、JSXおよびPreactコンポーネントをHTML文字列にレンダリングして、サーバーで使用できるようになります。 これは、srcフォルダーにPreactコンポーネントを作成し、それをノードサーバーファイルに接続することを意味します。

最初に行うことは、プロジェクトのディレクトリと必要なさまざまなフォルダを作成することです。 preact-unistore-ssrという名前のフォルダーを作成し、フォルダー内でコマンドnpm init --yを実行します。 これにより、最小限のpackage.jsonとそれに付随するpackage-lock.jsonが作成されます。

次に、このプロジェクトで使用するツールのいくつかをインストールします。 package.jsonファイルを開き、以下のコードで編集してから、npm iコマンドを実行します。

{
  "name": "preact-unistore-ssr",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "babel-cli": "^6.26.0",
    "babel-core": "^6.26.0",
    "babel-loader": "^7.1.2",
    "babel-plugin-transform-react-jsx": "^6.24.1",
    "babel-preset-env": "^1.6.1",
    "file-loader": "^1.1.11",
    "url-loader": "^1.0.1",
    "webpack": "^3.11.0",
    "webpack-cli": "^2.0.13"
  },
  "dependencies": {
    "express": "^4.16.2",
    "preact": "^8.2.6",
    "preact-render-to-string": "^3.7.0",
    "preact-router": "^2.6.0",
    "unistore": "^3.0.4"
  }
}

これにより、このアプリケーションに必要なすべてのパッケージがインストールされます。 devDependenciesオブジェクトには、ES6コードのトランスパイルに役立ついくつかのbabelパッケージがあります。 file-loaderおよびurl-loaderは、ファイル、アセット、モジュールなどのインポートに役立つWebpackプラグインです。

dependenciesオブジェクトでは、Express、Preact、preact-render-to-string、preact-router、unistoreなどのパッケージをインストールします。

次に、Webpack構成ファイルを作成します。 プロジェクトのルートにwebpack.config.jsという名前のファイルを作成し、以下のコードで編集します。

const path = require("path");

module.exports = {
    entry: "./src/index.js",
    output: {
        path: path.join(__dirname, "dist"),
        filename: "app.js"
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                loader: "babel-loader",
            }
        ]
    }
};

上記のwebpack設定では、エントリポイントをsrc/index.jsに定義し、出力をdist/app.jsに定義しました。 Babelを使用するためのルールも設定します。 エントリポイントファイルはまだ存在しませんが、後で作成します。

Babelを使用しているため、プロジェクトのルートに.babelrcファイルを作成し、設定を行う必要があります。

//.babelrc
{
    "plugins": [
        ["transform-react-jsx", { "pragma": "h" }]
    ],
    "presets": [
        ["env", {
            "targets": {
                "node": "current",
                "browsers": ["last 2 versions"]
            }
        }]
    ]
}

Preactアプリの構築

次に、Preact側のファイルの作成を開始します。 srcフォルダーを作成し、その中に次のファイルを作成します。

  • store/store.js
  • About.js
  • App.js
  • index.js
  • router.js

これで、必要なコードを使用してファイルを編集できます。 store.jsファイルから始めます。 これには、ストアデータとアクションが含まれます。

import createStore from 'unistore'

export let actions = store => ({
  increment(state) {
    return { count: state.count + 1 }
  },
  decrement(state) {
    return { count: state.count - 1 }
  }
})

export default initialState => createStore(initialState)

上記のコードブロックでは、countの値を1ずつインクリメントおよびデクリメントする一連のアクションをエクスポートします。 アクションは常に最初のパラメーターとしてstateを受け取り、他のパラメーターは次に来る可能性があります。 Unistoreのストアを初期化するために使用されるcreateStore関数もエクスポートされます。

次に、router.jsファイルを編集します。 これには、アプリで使用するルートの設定が含まれています。

import { h } from 'preact'
import Router from 'preact-router'

import { App } from "./App";
import { About } from "./About";

export default () => (
  <Router>
    <App path="/" />
    <About path="/about" />
  </Router>
)

このコードは、preact-routerを使用してルートを定義します。 これを行うには、ルートをインポートして、Routerコンポーネントの子にします。 次に、pathpropを各コンポーネントに設定して、preact-routerがルートに使用するコンポーネントを認識できるようにします。

アプリケーションには、ホームルートとして機能するApp.jsコンポーネントと、アバウトページとして機能するAbout.jsコンポーネントの2つの主要なルートがあります。

次に、About.jsを次のように編集します。

import { h } from "preact";
import { Link } from "preact-router/match";

export const About = () => (
    <div>
        <p>This is a Preact app being rendered on the server. It uses Unistore for state management and preact-router for routing.</p>
        <Link href="/">Home</Link>
    </div>
);

これは、簡単な説明と、ホームルートにつながるLinkコンポーネントを持つコンポーネントです。

App.jsはホームルートとして機能します。 そのファイルを開き、必要なコードで編集します。

import { h } from 'preact'
import { Link } from 'preact-router'
import { connect } from 'unistore/preact'

import { actions } from './store/store'

export const App = connect('count', actions)(
    ({ count, increment, decrement }) => (
      <div class="count">
        <p>{count}</p>
        <button class="increment-btn" onClick={increment}>Increment</button>
        <button class="decrement-btn" onClick={decrement}>Decrement</button>
        <Link href="/about">About</Link>
      </div>
    )
  )

このコードでは、connect関数と、actions関数がインポートされます。 Appコンポーネントでは、count状態値と、incrementおよびdecrementアクションが公開されます。 incrementアクションとdecrementアクションはどちらも、onClickイベントハンドラーを使用して異なるボタンに接続されています。

index.jsファイルはWebpackのエントリポイントです。 これは、Preactアプリの他のすべてのコンポーネントの親コンポーネントとして機能します。 ファイルを開き、以下のコードで編集します。

// index.js
import { h, render } from 'preact'
import { Provider } from 'unistore/preact'
import Router from './router'

import createStore from './store/store'

const store = createStore(window.__STATE__)

const app = document.getElementById('app')

render(
  <Provider store={store}>
    <Router />
  </Provider>,
  app,
  app.lastChild
)

上記のコードブロックでは、Providerコンポーネントがインポートされます。 PreactまたはReactの場合は、作業環境を指定することが重要です。 Routerコンポーネントもrouter.jsファイルからインポートし、createStore関数もstore.jsファイルからインポートします。

const store = createStore(window.__STATE__)行は、SSRアプリを構築しているため、サーバーからクライアントに初期状態を渡すために使用されます。

最後に、render関数で、RouterコンポーネントをProviderコンポーネント内にラップして、すべての子コンポーネントでストアを使用できるようにします。

これでクライアント側の作業は完了です。 次に、アプリのサーバー側に移動します。

ノードサーバーの構築

server.jsファイルを作成することから始めます。 これには、サーバー側のレンダリングに使用されるNodeアプリが格納されます。

// server.js
const express = require("express");
const { h } = require("preact");
const render = require("preact-render-to-string");
import { Provider } from 'unistore/preact'
const { App } = require("./src/App");
const path = require("path");

import Router from './src/router'
import createStore from './src/store/store'

const app = express();

const HTMLShell = (html, state) => `
    <!DOCTYPE html>
    <html>
        <head>
            <meta charset="utf-8">
            <meta name="viewport" content="width=device-width, initial-scale=1">
            <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.2/css/bulma.min.css">
            <title> SSR Preact App </title>
        </head>
        <body>
            <div id="app">${html}</div>
      <script>window.__STATE__=${JSON.stringify(state).replace(/<|>/g, '')}</script>
            <script src="./app.js"></script>
        </body>
    </html>`

app.use(express.static(path.join(__dirname, "dist")));

app.get('**', (req, res) => {
  const store = createStore({ count: 0, todo: [] })

  let state = store.getState()

  let html = render(
    <Provider store={store}>
      <Router />
    </Provider>
  )

  res.send(HTMLShell(html, state))
})

app.listen(4000);

これを分解してみましょう:

const express = require("express");
const { h } = require("preact");
const render = require("preact-render-to-string");
import { Provider } from 'unistore/preact'
const { App } = require("./src/App");
const path = require("path");

import Router from './src/router'
import createStore from './src/store/store'

const app = express();

上記のコードブロックでは、expresspathなどのノードサーバーに必要なパッケージをインポートします。 また、preactProviderコンポーネントをunistoreからインポートし、最も重要なのは、サーバー側のレンダリングを可能にするpreact-render-to-stringパッケージをインポートすることです。 ルートとストアもそれぞれのファイルからインポートされます。

const HTMLShell = (html, state) => `
    <!DOCTYPE html>
    <html>
        <head>
            <meta charset="utf-8">
            <meta name="viewport" content="width=device-width, initial-scale=1">
            <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.2/css/bulma.min.css">
            <title> SSR Preact App </title>
        </head>
        <body>
            <div id="app">${html}</div>
      <script>window.__STATE__=${JSON.stringify(state).replace(/<|>/g, '')}</script>
            <script src="./app.js"></script>
        </body>
    </html>`

上記のコードブロックでは、アプリに使用されるベースHTMLを作成します。 HTMLコードでは、状態はscriptセクションで初期化されます。 HTMLShell関数は2つのパラメーターを受け入れます。 htmlパラメーターは、preact-render-to-stringから受信した出力になり、htmlがHTMLコード内に挿入されます。 2番目のパラメーターは状態です。

app.use(express.static(path.join(__dirname, "dist")));

app.get('**', (req, res) => {
  const store = createStore({ count: 0})

  let state = store.getState()

  let html = render(
    <Provider store={store}>
      <Router />
    </Provider>
  )

  res.send(HTMLShell(html, state))
})

app.listen(4000);

コードの最初の行で、静的ファイルを提供するときにdistを使用するようにExpressに指示します。 前述のように、app.jsdistフォルダー内にあります。

次に、app.get(**)を使用してアプリに着信するリクエストのルートを設定します。 この最初に行うことは、ストアとその状態を初期化してから、状態の値を保持する変数を作成することです。

その後、preact-render-to-stringrenderとしてインポートされた)を使用して、ルートを保持するRouter、およびProviderとともに、クライアント側のPreactアプリをレンダリングします。 ]、すべての子コンポーネントにストアを提供します。

これで、最終的にアプリを実行して、どのように表示されるかを確認できます。 その前に、以下のコードブロックをpackage.jsonファイルに追加してください。

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start:client": "webpack -w",
    "start:server": "babel-node server.js",
    "dev": "npm run start:client & npm run start:server"
  },

これらは、アプリを起動して実行できるようにするスクリプトです。 端末でコマンドnpm run devを実行し、http://localhost:4000に移動します。 アプリが起動して実行されているはずです。次のような表示が表示されます。

CSSスタイリングの追加

これでビューが完了し、クライアントがサーバーに接続されたので、アプリにスタイルを追加できます。 CSSファイルをバンドルする必要があることをWebpackに通知する必要があります。

そのためには、style-loadercss-loaderをアプリに追加する必要があります。 次のコマンドを実行して、両方をインストールできます。

npm i css-loader style-loader --save-dev

インストールが完了したら、webpack.config.jsファイルに移動し、rulesアレイ内に以下のコードを追加します。

{
  test: /\.css$/,
  use: [ 'style-loader', 'css-loader' ]
}

これで、srcフォルダー内にindex.cssファイルを作成し、次のコードで編集できます。

body {
  background-image: linear-gradient(to right top, #2b0537, #820643, #c4442b, #d69600, #a8eb12);
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
}
a {
  display: block;
  color: white;
  text-decoration: underline;
}
p {
  color: white
}
.count p {
  color: white;
  font-size: 60px;
}
button:focus {
  outline: none;
}
.increment-btn {
  background-color: #1A2C5D;
  border: none;
  color: white;
  border-radius: 3px;
  padding: 10px 20px;
  font-size: 14px;
  margin: 0 10px;
}
.decrement-btn {
  background-color: #BC1B1B;
  border: none;
  color: white;
  border-radius: 3px;
  padding: 10px 20px;
  font-size: 14px;
  margin: 0 10px;
}

index.jsファイルで、ファイルの先頭に次のコードを追加します。

import './index.css';`
...

これで、ページが定型化されます。

結論

このチュートリアルでは、サーバー側でレンダリングされたPreactアプリを作成し、サーバー側でレンダリングされたアプリを構築する利点を探りました。 また、基本的な状態管理にUnistoreを使用し、window.__STATE__を使用してサーバーからフロントエンドに状態を接続しました。

これで、サーバー上でPreactアプリをレンダリングする方法についてのアイデアが得られたはずです。 要約すると、アイデアは最初にサーバー最初でアプリをレンダリングし、次にブラウザーでコンポーネントをレンダリングすることです。

このチュートリアルのコードは、GitHubで表示できます。