使用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.jsAbout.jsApp.jsindex.jsrouter.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上查看。