Preact、Unistore、およびPreactルーターを使用してSSRアプリを構築する
序章
シングルページアプリは、最新の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
コンポーネントの子にします。 次に、path
のprop
を各コンポーネントに設定して、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();
上記のコードブロックでは、express
やpath
などのノードサーバーに必要なパッケージをインポートします。 また、preact
、Provider
コンポーネントを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.js
はdist
フォルダー内にあります。
次に、app.get(**)
を使用してアプリに着信するリクエストのルートを設定します。 この最初に行うことは、ストアとその状態を初期化してから、状態の値を保持する変数を作成することです。
その後、preact-render-to-string
(render
としてインポートされた)を使用して、ルートを保持する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-loader
とcss-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で表示できます。