使用Preact、Unistore和PreactRouter构建SSR应用程序
介绍
单页应用程序是构建现代 Web 应用程序的一种流行方式。 对于 SPA,您可以通过两种方式将应用程序的内容呈现给用户:客户端呈现或服务器端呈现。
通过客户端渲染,每当用户打开应用程序时,都会发送一个请求以加载布局、HTML、CSS 和 JavaScript。 如果应用程序的内容依赖于成功加载 JS 脚本的完成,这可能是个问题。 这意味着用户将被迫在等待脚本完成加载时查看预加载器。
服务器端渲染的操作方式不同。 使用 SSR,您的初始请求将首先加载页面、布局、CSS、JavaScript 和内容。 SSR 确保数据在渲染时正确初始化。 服务器端渲染也更适合搜索引擎优化。
在本教程中,您将探索如何使用 Preact 构建服务器端渲染应用程序。 preact-router 将用于路由,unistore 用于状态管理,Webpack 用于 JS 捆绑。 可能需要一些现有的 Preact、Unistore 和 Webpack 知识。
技术
在本教程中,您将使用以下技术来构建服务器端渲染应用程序:
- Preact - 使用相同 API 替代 React。 它旨在提供类似于 React 的开发体验,尽管剥离了一些功能,例如 PropTypes 和 Children。
- Unistore - 一个集中的状态容器,带有 React 和 Preact 的组件绑定。
- Preact Router - 帮助管理 Preact 应用程序中的路由。 提供一个
<Router />
组件,当 URL 与其路径匹配时有条件地呈现其子级。 - Webpack - 一个捆绑器,可帮助捆绑 JavaScript 文件以在浏览器中使用。
使用 Preact 构建 SSR 应用
这个应用程序的构建将分为两个部分。 您将首先在 Node 和 Express 中构建代码的服务器端。 之后,您将对代码的 Preact 部分进行编码。
这个想法是创建一个 Preact 应用程序,并使用 preact-render-to-string
包将其连接到 Node 服务器。 它允许将 JSX 和 Preact 组件渲染为 HTML 字符串,然后可以在服务器中使用。 这意味着我们将在 src
文件夹中创建 Preact 组件,然后将其连接到 Node 服务器文件。
首先要做的是为项目创建目录和您需要的不同文件夹。 创建一个名为 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
对象中,有一些 babel 包可以帮助转译 ES6 代码。 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
文件并放入 config.xml 文件中。
//.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中用于初始化store的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
组件,用作关于页面。
接下来使用以下内容编辑 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.js
文件中导入 Router
组件,并且 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();
在上面的代码块中,您导入了 Node 服务器所需的包,例如 express
和 path
。 您还可以从 unistore
导入 preact
、Provider
组件,以及最重要的 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
函数接受两个参数。 html
参数将是从 preact-render-to-string
接收的输出,然后将 html
注入到 HTML 代码中。 第二个参数是状态。
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
和 [X157X ],它为每个子组件提供存储。
完成后,您终于可以运行该应用程序并查看它的外观。 在你这样做之前,将下面的代码块添加到 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 样式
现在视图已经完成并且客户端连接到服务器,您可以向应用程序添加一些样式。 你需要让 Webpack 知道它需要捆绑 CSS 文件。
为此,需要将 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上查看。