- {
- "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",
- "express-http-proxy": "^2.0.0",
- "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",
- "react-router-config": "1.0.0-beta.4",
- "react-router-dom": "4.3.1"
- }
- }
- {
- "presets": [
- "@babel/preset-react",
- "@babel/preset-typescript"
- ],
- "plugins": []
- }
- {
- "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"
- ]
- }
- const path = require('path');
-
- const srcPath = path.resolve(__dirname, '..', 'src');
- const distPath = path.resolve(__dirname, '..', 'dist');
-
- module.exports = {
- srcPath,
- distPath
- }
- module.exports = {
- // 打包的规则
- module: {
- rules: [
- {
- test: /\.[jt]sx?$/, // 检测文件类型
- loader: 'babel-loader', // 注意下载babel-loader babel-core
- exclude: /node_modules/, // node_modules目录文件不编译
- }
- ]
- }
-
- }
使用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);
使用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);
- import React from "react";
- import Header from "components/header";
- import { renderRoutes } from 'react-router-config'
- import _ from 'lodash'
-
- const App = (props) => {
- return (
- <section>
- <Header />
- {/* 显示页面对应的内容 */}
- {renderRoutes(props.route.routes)}
- </section>
- );
- };
-
- export default App;
嵌套路由, 让head组件一直存在, head之下的页面作为子路由
- import React from 'react'
- import Home from './containers/home'
- import Login from './containers/login'
- import App from './App';
-
- const routes = [
- {
- path: '/',
- component: Home,
- loadData: Home.loadData,
- exact: true,
- key: 'home',
- },
- {
- path: '/login',
- component: Login,
- loadData: Login.loadData,
- exact: true,
- key: 'login',
- }
- ]
-
- export default [{
- path: '/',
- component: App,
- routes,
- }]
1) 引入需要的reducer1, reducer2...
2) 使用combineReducers组合引入的reducer1, reducer2..., 构成新的reducer
3) 使用createStore创建store实例, 传入reducer和中间件!!!
createstore语法:
- const store = createStore(reducer, [preloadedState], enhancer);
-
- /**
- *createStore接受3个参数:
- 第一个是reducer,
- 第二个是初始的state,
- 第三个enhancer是store的增强器。
- 执行本方法返回一个对象,
- 包含dispatch,subsribe,getState,replaceReducer,observable五个方法。
- */
a. 服务端获取store实例
const store = createStore(
reducer,
applyMiddleware(thunk.withExtraArgument(serverAxios)); // 注意传服务端的axios实例
);
b. 客户端获取store实例
// 拿服务端的数据作为客户端的数据
const defaultState = window.context.state
const store = createStore(
reducer,
defaultState,
// 改变客户端store的内容, 那么一定要使用clientAxios
applyMiddleware(thunk.withExtraArgument(clientAxios)) // 注意传客户端的axios实例
);
- import { legacy_createStore as createStore, applyMiddleware, combineReducers } from "redux";
- import thunk from 'redux-thunk';
- import { reducer as homeReducer } from '../containers/home/store'
- import clientAxios from '@/client/request'
- import serverAxios from '@/server/request'
-
- // 将各个模块的reducer组合一下
- const reducer = combineReducers({
- home: homeReducer,
- });
-
- // 导出getStore是因为想要每个用户的store都是独享的!!!
- export const getStore = () => {
- const store = createStore(
- reducer,
- // 改变服务器store的内容, 那么一定要使用serverAxios
- applyMiddleware(thunk.withExtraArgument(serverAxios))
- );
- return store
- }
-
- export const getClientStore = () => {
- // 拿服务端的数据作为客户端的数据
- const defaultState = window.context.state
- const store = createStore(
- reducer,
- defaultState,
- // 改变客户端store的内容, 那么一定要使用clientAxios
- applyMiddleware(thunk.withExtraArgument(clientAxios))
- );
- return store
- }
- import React, {useEffect} from "react";
- import { connect } from "react-redux";
- import { getHomeList } from './store/actions'
- import _ from 'lodash'
-
- // 同构--同一套react代码, 在服务器执行一次, 在客户端再执行一次
- const Home = (props) => {
- // 如果是class组件使用componentDidMount生命周期
- useEffect(()=> {
- // 避免首屏时服务端和客户端都各自调用了一次!!!
- if(!props.list.length) props.getHomeList()
- }, [])
-
- const getList = () => {
- return <ul>
- {_.map(props.list, (item) => {
- return <li key={item.uid}><span>{item.code}: </span>{item.type}</li>
- })}
- </ul>
- }
- return (
- <section>
- <div>this is {props.name}</div>
- {getList()}
- <button onClick={() => alert(11)}>click</button>
- </section>
- );
- };
-
- Home.loadData = (store) => {
- // 这个函数, 负责在服务器渲染之前, 把这个路由需要的数据提前加载好
- return store.dispatch(getHomeList())
- }
-
- const mapStateToProps = (state) => {
- return ({
- name: state.home.name,
- list: state.home.newsList,
- });
- }
-
- const mapDispatchToProps = (dispatch) => ({
- getHomeList() {
- console.log('test')
- dispatch(getHomeList())
- }
- });
- export default connect(mapStateToProps, mapDispatchToProps)(Home);
home/store/index.ts
- import reducer from './reducer'
-
- export { reducer }
home/store/actions.ts
- import { CHANGE_LIST } from './constants'
-
- const changeList = (list) => ({
- type: CHANGE_LIST,
- list,
- })
-
- export const getHomeList = (server) => {
- let url1 = `/app/mock/22915/api/code`
- // const url1 = 'http://rap2api.taobao.org/app/mock/22915/api/code'
- return (dispatch, getState, axiosInstance) => {
- return axiosInstance.post(url1, {
- rows: 10,
- page: 1
- })
- .then(res => {
- const list = res.data.rows;
- dispatch(changeList(list.slice(0, 10)))
- })
- }
- }
home/store/constants.ts
- // 定义常量
- export const CHANGE_LIST = 'HOME/CHANGE_LIST'
home/store/reducer.ts
- import { CHANGE_LIST } from './constants'
-
- const defaultState = {
- newsList: [],
- name: 'Tom Li-1'
- }
-
- const reducer = (state:any = defaultState, action) => {
- switch (action.type) {
- case CHANGE_LIST:
- return {
- ...state,
- newsList: action.list
- };
-
- default:
- return state;
- }
- };
-
- export default reducer
- import React from 'react'
- import Header from '../components/header';
-
- const Login = () => {
- return <div>
- <Header />
-
- <div>login</div>
- </div>
- }
-
- export default Login;
- 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
// 注意store要使用服务端定义的store
- import express from 'express';
- import React from 'react';
- import { render } from './utils'
- import routes from '../routes'
- import { getStore } from '../store'
- import { matchPath } from 'react-router-dom'
- import _ from 'lodash'
- import proxy from 'express-http-proxy'
-
- var app = express();
-
- /**
- * 客户端渲染
- * react代码在浏览器上执行, 消耗的是用户浏览器的性能
- *
- * 服务器渲染
- * react代码在服务器上执行, 消耗的是服务器端的性能(或者资源)
- * 报错信息查询网站: stackoverflow.com
- */
- // 服务器请求/api的时候, 做代理
- app.use('/api', proxy('http://rap2api.taobao.org', {
- proxyReqPathResolver(req) {
- console.log(req.url,'req.url=======')
- // const parts = req.url.split('?')
- // const queryString = parts[1]
- // const updatedPath = parts[0].replace(/test/, 'tent');
- return req.url
- }
- }));
-
- // 只要是静态文件, 都到public目录找
- app.use(express.static('public'));
-
- // * => 任意路径都能走到下列的方法
- app.get('*', function (req, res) {
- const store = getStore()
-
- // 如果在这里, 能够拿到异步数据并填充到store之中
- // store里面到底填充什么, 我们不知道, 我们需要结合当前用户请求地址, 和路由, 做判断
- // 如果用户访问根/路径, 我们就拿home组件的异步数据
- // 如果用户访问/login路径, 我们就拿login组件的异步数据
- // 根据路由的路径, 来往store里面加数据
- const matchedRoutes = [];
- function getRoutes(routes) {
- routes.some(route => {
- const match = matchPath(req.path, route);
- if (match) {
- matchedRoutes.push(route)
- }
- if (Array.isArray(_.get(route, 'routes')) && _.get(route, 'routes')) {
- getRoutes(_.get(route, 'routes'))
- }
- return match
- })
- }
- getRoutes(routes)
- // 让matchRoutes里面所有的组件, 对应的loadData执行一次
- const promises = []
- matchedRoutes.forEach(route => {
- try {
- promises.push(route.loadData(store))
- } catch (error) {
-
- }
- })
- console.log(matchedRoutes, 'matchedRoutes==')
- Promise.all(promises)
- .then(() => {
- res.send(render({store, routes, req}))
- })
- })
-
-
- var server = app.listen(2000);
添加script标签是因为模板字符串渲染成dom, onClick等事件没有反应, 所以script标签再同构一下
- import React from 'react';
- import { renderToString } from "react-dom/server"
- import { StaticRouter, Route } from 'react-router-dom'
- import { Provider } from 'react-redux'
- import _ from 'lodash'
- import { renderRoutes } from 'react-router-config'
-
- export const render = ({store, routes, req}) => {
-
- // 虚拟dom是真实dom的一个JavaScript对象的映射
- const content = renderToString((
- <Provider store={store}>
- <StaticRouter location={req.path} context={{}}>
- <div>
- {renderRoutes(routes)}
- </div>
- </StaticRouter>
- </Provider>
- ))
- // 当store数据更新完成再渲染dom
- return (
- `<html>
- <head>
- <title>ssr</title>
- <link rel="icon" href="/flower.jpg" />
- </head>
- <body>
- <div id="root">${content}</div>
- <script>
- window.context = ${JSON.stringify({state:store.getState()})}
- </script>
- <script src="/index.js"></script>
- </body>
- </html>`
- )
- }
导出服务端axios实例
- import axios from "axios";
-
- const instance = axios.create({
- baseURL: 'http://rap2api.taobao.org'
- })
-
-
- export default instance
// 注意store要使用客户端定义的store
- import React from "react";
- import ReactDOM from "react-dom";
- import { BrowserRouter, Route } from "react-router-dom";
- import routes from "../routes";
- import { Provider } from "react-redux";
- import { getClientStore } from '../store';
- import { renderRoutes } from 'react-router-config'
-
- const App = () => {
- return (
- <Provider store={getClientStore()}>
- <BrowserRouter>
- <div>
- {renderRoutes(routes)}
- </div>
- </BrowserRouter>
- </Provider>
- );
- };
-
- ReactDOM.hydrate(<App />, document.getElementById("root"));
导出客户端axios实例
- import axios from "axios";
-
- const instance = axios.create({
- baseURL: '/api'
- })
-
-
- export default instance
修改home组件, 刷新浏览器就行了
