序章

シングルページアプリは、最新の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アプリケーションでルートを管理するのに役立ちます。 を提供します <Router /> URLがパスと一致したときに子を条件付きでレンダリングするコンポーネント。
  • Webpack-ブラウザーで使用するためにJavaScriptファイルをバンドルするのに役立つバンドラー。

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

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

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

最初に行うことは、プロジェクトのディレクトリと必要なさまざまなフォルダを作成することです。 名前の付いたフォルダを作成します 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-loaderurl-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 最初のパラメータと他のパラメータが次に来る可能性があるため。 The createStore Unistoreのストアを初期化するために使用される関数もエクスポートされます。

次に、 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 成分。 次に、を設定できます proppath 各コンポーネントに preact-router ルートに使用するコンポーネントを認識しています。

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

次に、 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 状態値と同様に公開されます incrementdecrement 行動。 The incrementdecrement アクションは両方とも異なるボタンに接続されています onClick イベントハンドラ。

The index.js fileは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 ファイル。

The const store = createStore(window.__STATE__) lineは、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 セクション。 The HTMLShell 関数は2つのパラメーターを受け入れます。 The 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);

コードの最初の行で、Expressに dist 静的ファイルを提供する場合。 前述のように、 app.js 中にあります dist フォルダ。

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

その後、 preact-render-to-string (としてインポートされました render)は、クライアント側のPreactアプリを Router、ルートを保持し、 Provider、すべての子コンポーネントにストアを提供します。

これで、最終的にアプリを実行して、どのように表示されるかを確認できます。 その前に、以下のコードブロックをに追加してください 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 アプリに追加する必要があります。 次のコマンドを実行して、両方をインストールできます。

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

インストールが完了したら、 webpack.config.js ファイルを作成し、以下のコードを rules 配列。

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

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

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で表示できます。