ssr
ssr与csr的区别
<body>
</body>
<script>
document.querySelector('body').innerHTML = `<div>111</div>`
</script>
<body>
<div>111</div>
</body>
- 明显的区别就是:
- csr的逻辑为请求到html,然后获取js,js执行渲染dom
- ssr为请求到的html内dom已经存在渲染了
ssr解决了什么问题
- 随着需求的迭代spa项目编译出来的包的体积会越来越大,那么获取页面获取下来后,是一个空架子,这时候获取js的时间比较长的话,会造成很明显的白屏效果
- 比如官网运营类项目需要考虑seo,而百度爬虫或自己产品线爬虫识别不出来js渲染的内容,无法达到预期的效果,比如检索、推广等
实现一个ssr项目
- 分为2部分,serve和client
- 我们需要了解一个内容:以react的ssr项目举例,服务端渲染出html骨架后,客户端接收到,会将获取的js和html骨架进行
水合
,也就是注入逻辑
client
- 为客户端渲染部分
- 新建src目录,创建main.tsx
import { createRoot, hydrateRoot } from 'react-dom/client';
import App from './App';
if (process.env.NODE_ENV == 'development') {
const root = createRoot(document.querySelector('#root') as any);
root.render(<App />);
} else {
hydrateRoot(document.querySelector('#root') as any, <App />);
}
import { useState, Suspense, lazy } from 'react';
import Style from './styles/App';
import A from '@/components/A';
import B from '@/components/B';
import { DatePicker, Button } from 'antd';
import { Helmet } from 'react-helmet';
const App = () => {
const [show, setShow] = useState(false);
return (
<Style>
<Helmet>
<title>服务标题</title>
<meta name="keywords" content="关键字关键字" />
<meta name="description" content="描述描述"></meta>
<link
rel="shortcut icon"
href="//mapopen-website-wiki.cdn.bcebos.com/LOGO/lbsyunlogo_icon.ico"
type="image/x-icon"
/>
</Helmet>
<DatePicker />
<Button>11</Button>
<div>父级容器</div>
<button
onClick={() => {
setShow(!show);
}}
>
取反
</button>
{show ? <A /> : <B />}
</Style>
);
};
export default App;
工程化部分
- 统一放在config目录下,便于管理
- 抽离出公用逻辑,便于复用
const path = require('path');
const webpack = require('webpack');
module.exports = {
resolve: {
extensions: ['.tsx', '.jsx', '.ts', '.js'],
alias: {
'@': path.resolve('src'),
},
},
module: {
rules: [
{
test: /\.(js|ts|tsx|jsx)/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
['@babel/preset-react', { runtime: 'automatic' }],
'@babel/preset-typescript',
],
},
},
},
],
},
plugins: [
new webpack.DefinePlugin({
NODE_ENV: process.env.NODE_ENV,
}),
],
};
- 客户端编译
- 我们需要了解到
每个模块之间的依赖关系
,便于后续我们对资源进行导入,所以使用assets-webpack-plugin来记录,它会生成一个json
- react和react-dom使用cdn引入,减少包体积和编译时长
const base = require('./base');
const { merge } = require('webpack-merge');
const AssetsPlugin = require('assets-webpack-plugin');
module.exports = merge(base, {
entry: './src/main.tsx',
externals: {
react: 'React',
'react-dom': 'ReactDOM',
},
output: {
clean: true,
filename: '[name].js?[contenthash]',
publicPath: '/',
},
plugins: [
new AssetsPlugin({
entrypoints: true,
integrity: true,
prettyPrint: true,
includeFilesWithoutChunk: true,
includeAuxiliaryAssets: true,
includeDynamicImportedAssets: true,
}),
],
});
- 客户端编译
- 代码里使用了es module语法,默认是不识别的,所以编译下
- node自带包不需要编译进去,使用webpack-node-externals给它排除掉,也减少包体积和编译时间
const path = require('path');
const base = require('./base');
const nodeExternals = require('webpack-node-externals');
const { merge } = require('webpack-merge');
module.exports = merge(base, {
mode: 'production',
target: 'node',
entry: path.resolve(__dirname, '../serve/index.js'),
output: {
filename: 'serve.js',
},
externals: nodeExternals(),
});
- 服务端和客户端都有了,但是他们都是生成环境使用的,我本地开发不需要这么麻烦,所以再来一个dev环境
const path = require('path');
const HtmlPlugin = require('html-webpack-plugin');
const base = require('./base');
const { merge } = require('webpack-merge');
module.exports = merge(base, {
mode: 'development',
entry: './src/main.tsx',
externals: {
react: 'React',
'react-dom': 'ReactDOM',
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
output: {
clean: true,
filename: '[name].js?[contenthash]',
publicPath: '/',
},
plugins: [
new HtmlPlugin({
template: './public/index.html',
}),
],
devServer: {
historyApiFallback: true,
hot: true,
},
});
服务端部分
- 统一放在serve目录下
- 用express起了一个本地服务
import React from 'react';
import { createCache, extractStyle, StyleProvider } from '@ant-design/cssinjs';
import { renderToString } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom/server';
import { ServerStyleSheet } from 'styled-components';
import App from '../src/App';
import html from './client/html';
import getJs from './client/getJs';
import getHead from './client/getHead';
import { Helmet } from 'react-helmet';
const fs = require('fs');
const path = require('path');
const assets = JSON.parse(
fs.readFileSync(path.resolve(__dirname, '../webpack-assets.json'), 'utf-8')
);
let jsStr = getJs(assets['main'].js);
const express = require('express');
const app = express();
app.use(express.static('dist'));
app.get('*', (req, res) => {
const cache = createCache();
const sheet = new ServerStyleSheet();
const helmet = Helmet.renderStatic();
let body = renderToString(
sheet.collectStyles(
<StaticRouter>
<StyleProvider cache={cache}>
<App />
</StyleProvider>
</StaticRouter>
)
);
const styles = sheet.getStyleTags();
const styleText = extractStyle(cache);
res.send(
html({ styles: styles + styleText, body, head: getHead(helmet), js: jsStr })
);
});
app.listen(3000, () => {
console.log('http://127.0.0.1:3000');
});
const getHead = (helmet) => {
return (
helmet.title.toString() +
helmet.meta.toString() +
helmet.link.toString() +
helmet.style.toString()
);
};
export default getHead;
const getJs = (strOrarg) => {
if (typeof strOrarg === 'string')
return `<script src="${strOrarg}"></script>`;
return strOrarg.map((str) => `<script src="${str}"></script>`).join('');
};
export default getJs;
const Html = ({ body, styles, head, js }) => `
<!DOCTYPE html>
<html>
<head>
${head}
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
${styles}
</head>
<body><div id="root">${body}</div></body>
</html>
${js}
`;
export default Html;
统一安装下依赖
{
"name": "ssr",
"version": "1.0.0",
"description": "react的ssr方案",
"main": "index.js",
"scripts": {
"build": "webpack --config config/client.js --mode=production",
"dev": "cross-env NODE_ENV=develpoment webpack serve -c config/dev.js",
"serve:build": "webpack -c config/serve.js",
"serve:preview": "node dist/serve.js",
"preview": "npm-run-all --sequential build serve:**"
},
"keywords": [
"ssr",
"react-ssr"
],
"author": "cuishoulong",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.20.12",
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.18.6",
"@types/node": "^18.11.18",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"@types/react-helmet": "^6.1.6",
"@types/react-router": "^5.1.20",
"@types/react-router-dom": "^5.3.3",
"@types/styled-components": "^5.1.26",
"@typescript-eslint/eslint-plugin": "^5.50.0",
"@typescript-eslint/parser": "^5.50.0",
"assets-webpack-plugin": "^7.1.1",
"babel-loader": "^9.1.2",
"cross-env": "^7.0.3",
"css-loader": "^6.7.3",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.32.2",
"html-webpack-plugin": "^5.5.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.8.3",
"style-loader": "^3.3.1",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.11.1",
"webpack-merge": "^5.8.0",
"webpack-node-externals": "^3.0.0"
},
"dependencies": {
"antd": "^5.1.7",
"prism-react-renderer": "^1.3.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-helmet": "^6.1.0",
"react-router": "^6.8.0",
"react-router-dom": "^6.8.0",
"styled-components": "^5.3.6"
}
}
增加ts类型提示
- 现在使用的是
@babel/preset-typescript
来编译的ts文件,它相比ts-loader快上不少
,但缺乏ts的校验
,那么我们加一个tsconfig.json来进行语法提示,但不阻塞编译
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"noImplicitAny": false,
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src"]
}
一些注意点
- react的服务端渲染这里使用了
renderToString
seo
- seo部分使用了
react-helmet
,在gethead函数里可以注意到,它的使用方法就是实例.标签.toString()
,目前支持所有的head原生标签类型
css
- 当浏览器渲染html骨架的时候,我们期望它将css加载,这样页面就不会有闪动的效果,也可以看到更美观的页面,所以需要在服务端处理css
- 使用
styled-components
来写样式,提取使用ServerStyleSheet
方法
- antd的5版本使用了css in js版本,默认支持了按需引入,所以以前的
babel-plugin-import在antd里没有意义了
- 提取antd的css使用
createCache, extractStyle, StyleProvider
,
官方更新文档
启动项目
- 本地开发 npm run dev
- 本地编译 npm run build
- 预览 npm run preview