1. package.json
{
"name": "react-server",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "npm-run-all --parallel dev:**",
"dev:build:client": "webpack --config config/webpack.client.js --watch",
"dev:build:server": "webpack --config config/webpack.server.js --watch",
"dev:start": "nodemon --watch dist --exec node \"./dist/bundle.js\""
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.0.0",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-env": "^7.22.20",
"@babel/preset-react": "^7.22.15",
"@babel/preset-stage-0": "^7.8.3",
"@babel/preset-typescript": "^7.23.0",
"@reduxjs/toolkit": "^1.9.7",
"@types/react": "^18.2.27",
"@types/react-dom": "^18.2.12",
"antd": "^5.10.0",
"autoprefixer": "^9.7.3",
"axios": "^1.5.1",
"babel-core": "^7.0.0-bridge.0",
"babel-loader": "7",
"clean-webpack-plugin": "^3.0.0",
"cross-env": "^7.0.3",
"css-loader": "5.0.0",
"eslint-loader": "^4.0.2",
"file-loader": "^5.0.2",
"happypack": "^5.0.1",
"html-webpack-plugin": "^3.2.0",
"less": "^3.10.3",
"less-loader": "5.0.0",
"lodash": "^4.17.15",
"mini-css-extract-plugin": "^0.8.0",
"moment": "^2.24.0",
"node-sass": "^9.0.0",
"nodemon": "^3.0.1",
"npm-run-all": "^4.1.5",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"postcss": "^8.4.31",
"postcss-loader": "^3.0.0",
"postcss-pxtorem": "5.0.0",
"react-activation": "^0.12.4",
"react-redux": "^8.1.3",
"recoil": "^0.7.7",
"redux": "^4.2.1",
"redux-persist": "^6.0.0",
"sass": "^1.69.3",
"sass-loader": "5.0.0",
"style-loader": "^1.0.1",
"terser-webpack-plugin": "^2.2.2",
"thread-loader": "^4.0.2",
"typescript": "^5.2.2",
"url-loader": "^3.0.0",
"webpack": "^4.41.2",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.9.0",
"webpack-merge": "^4.2.2",
"webpack-node-externals": "^3.0.0",
"webpack-parallel-uglify-plugin": "^1.1.2",
"yarn": "^1.22.19"
},
"dependencies": {
"express": "^4.18.2",
"path": "^0.12.7",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
2. 新建.babelrc文件
{
"presets": [
"@babel/preset-react",
"@babel/preset-typescript"
],
"plugins": []
}
3. 新建tsconfig.json文件
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"noImplicitAny": false
},
"include": [
"src"
]
}
4. config目录
paths.js
const path = require('path');
const srcPath = path.resolve(__dirname, '..', 'src');
const distPath = path.resolve(__dirname, '..', 'dist');
module.exports = {
srcPath,
distPath
}
webpack配置文件
(1) webpack.base.js提取公共配置代码
module.exports = {
// 打包的规则
module: {
rules: [
{
test: /\.[jt]sx?$/, // 检测文件类型
loader: 'babel-loader', // 注意下载babel-loader babel-core
exclude: /node_modules/, // node_modules目录文件不编译
}
]
}
}
(2) webpack.server.js服务器配置
使用webpack-merge合并公共配置代码
const nodeExternals = require('webpack-node-externals');
const { distPath, srcPath } = require('./paths');
const path = require('path');
const merge = require('webpack-merge');
const config = require('./webpack.base.js');
const serverConfig = {
mode: 'production', // 也可以写development
target: 'node', // 告诉webpack打包的代码是服务器端文件
entry: './src/server/index.js',
output: { // 打包生成的文件应该放到哪儿去
filename: 'bundle.js',
path: distPath,
},
resolve: {
extensions: ['.tsx', '.ts', '.jsx', '.js'],
// 针对npm中的第三方模块优先采用jsnext中指向的es6模块语法的文件
mainFields: ['jsnext:main', 'brower', 'main'],
alias: {
"components": path.resolve(srcPath, "containers/components"), //配置样式简单的路径
"@": srcPath // 把 src 这个常用目录修改为 @
},
},
externals: [
/**
* 1、如何让webpackexternals不影响测试环境?
由于webpackexternals将部分库文件排除在打包范围之外,这样在某些情况下可能会影响单元测试的运行,可以使用webpack-node-externals来排除node_modules目录下的所有依赖项。
*/
nodeExternals(),
],
}
module.exports = merge(config, serverConfig);
(3) webpack.client.js客户端配置
使用webpack-merge合并公共配置代码
const { distPath, srcPath, publicPath } = require('./paths');
const path = require('path');
const merge = require('webpack-merge');
const config = require('./webpack.base.js');
const clientConfig = {
mode: 'production', // 也可以写development
entry: './src/client/index.js',
output: { // 打包生成的文件应该放到哪儿去
filename: 'index.js',
path: publicPath,
},
resolve: {
extensions: ['.tsx', '.ts', '.jsx', '.js'],
// 针对npm中的第三方模块优先采用jsnext中指向的es6模块语法的文件
mainFields: ['jsnext:main', 'brower', 'main'],
alias: {
"components": path.resolve(srcPath, "containers/components"), //配置样式简单的路径
"@": srcPath // 把 src 这个常用目录修改为 @
},
},
}
module.exports = merge(config, clientConfig);
5. src/containers/组件页面目录
routes.tsx
import React from 'react'
import { Route } from 'react-router-dom'
import Home from './containers/home'
import Login from './containers/login'
export default (
<div>
<Route path='/' component={Home} exact />
<Route path='/login' component={Login} exact />
</div>
)
home/index.tsx
import React from 'react'
const Home = () => {
return <div>home !!!!</div>
}
export default Home
login/index.tsx
import React from 'react'
import Header from '../components/header';
const Login = () => {
return <div>
<Header />
<div>login</div>
</div>
}
export default Login;
components/公共组件目录
header/index.tsx
import React from 'react'
import {Link} from 'react-router-dom'
// 同构--同一套react代码, 在服务器执行一次, 在客户端再执行一次
const Header = () => {
return <div>
<Link to="/">HOME</Link>
<Link to="/login">LOGIN</Link>
</div>
}
export default Header
6. src/server/服务端代码目录
index.js
import express from 'express';
import React from 'react';
import {render} from './utils'
var app = express();
/**
* 客户端渲染
* react代码在浏览器上执行, 消耗的是用户浏览器的性能
*
* 服务器渲染
* react代码在服务器上执行, 消耗的是服务器端的性能(或者资源)
* 报错信息查询网站: stackoverflow.com
*/
// 只要是静态文件, 都到public目录找
app.use(express.static('public'));
// * => 任意路径都能走到下列的方法
app.get('*', function (req, res) {
res.send(render(req))
})
var server = app.listen(2000);
utils.js
添加script标签是因为模板字符串渲染成dom, onClick等事件没有反应, 所以script标签再同构一下
import React from 'react';
import { renderToString } from "react-dom/server"
import { StaticRouter } from 'react-router-dom'
import Routes from '../routes'
export const render = (req) => {
// 虚拟dom是真实dom的一个JavaScript对象的映射
const content = renderToString((
<StaticRouter location={req.path} context={{}}>
{Routes}
</StaticRouter>
))
return (
`
<html>
<head>
<title>ssr</title>
</head>
<body>
<div id="root">${content}</div>
<script src="/index.js"></script>
</body>
</html>
`
)
}
7. src/client/客户端代码目录
index.js
import React from "react";
import ReactDOM from "react-dom";
import Home from '../containers/home';
import {BrowserRouter} from 'react-router-dom'
import Routes from '../routes'
const App = () => {
return <BrowserRouter>
{Routes}
</BrowserRouter>
}
ReactDOM.hydrate(<App />, document.getElementById('root'));
8. 因为npm-run-all, 所以执行yarn dev就能运行代码并监听组件是否修改了
修改home组件, 刷新浏览器就行了