Skip to content

feat: CSR to SSR #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
"prepare": "husky install",
"commit": "cross-env git-cz",
"dev": "cross-env NODE_ENV=development webpack serve",
"build": "cross-env NODE_ENV=production webpack",
"build-server": "cross-env NODE_ENV=production BUILD_SERVER=true webpack --config ./webpack/server.config.js",
"start-debug-server": "node --inspect-brk ./dist/server.js",
"start-server": "node ./dist/server.js",
"build-client": "cross-env NODE_ENV=production webpack",
"build": "npm run build-client && npm run build-server",
"preview": "npx serve -s dist",
"clean": "rm dist",
"lint:file": "cross-env eslint . --ext .js,.jsx,.ts,.tsx --fix",
Expand Down
15 changes: 15 additions & 0 deletions server/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import express from 'express';
import { DIST, PUBLIC_PATH } from '../webpack/constants';
import { serverRenderer } from './renderer';

const app = express();
const PORT = 3000;

app.use(PUBLIC_PATH, express.static(DIST));

app.get('*', serverRenderer);

app.listen(PORT, () => {
// eslint-disable-next-line no-console
console.log(`SSR Server is listening on http://localhost:${PORT}`);
});
40 changes: 40 additions & 0 deletions server/renderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouterContext } from 'react-router';
import { readFileSync } from 'fs';
import { resolve } from 'path';
import { App } from '../src/app/ui/app/app';
import { history } from '../src/shared/router';
import { DIST } from '../webpack/constants';

function getIndexHTMLTemplate() {
return readFileSync(resolve(DIST, 'index.html'), {
encoding: 'utf-8',
});
}

export function serverRenderer(req, res) {
const context: StaticRouterContext = {};

const reqUrl = req.url;
history.push(reqUrl);

const markup = renderToString(<App />);

// eslint-disable-next-line no-console
console.log(`markup=${markup.substring(0, 100)}`);

if (context.url) {
// eslint-disable-next-line no-console
console.log(`context?.url=${context?.url}`);
// 某处渲染了 `<Redirect />` 组件
res.redirect(301, context.url);
} else {
res.send(
getIndexHTMLTemplate().replace(
'<div id="root"></div>',
`<div id="root">${markup}</div>`,
),
);
}
}
2 changes: 1 addition & 1 deletion src/app/ui/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useState, useCallback } from 'react';
import type { FallbackProps } from 'react-error-boundary';
import { ErrorBoundary } from 'react-error-boundary';
import { hot } from 'react-hot-loader/root';
import { Router, Route, Switch } from 'react-router-dom';
import { Route, Router, Switch } from 'react-router-dom';
import { useGate } from 'effector-react';
import { QueryParamProvider } from 'use-query-params';
import ArticlePage from '@/pages/article';
Expand Down
6 changes: 4 additions & 2 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { StrictMode } from 'react';
import { render } from 'react-dom';
import { hydrate } from 'react-dom';
import { App } from './app';

render(
// 把 <BrowserRouter /> 声明在 <APP /> 外层,
// 避免 serverRenderer 端执行渲染,因为没有浏览器API,导致报错
hydrate(
<StrictMode>
<App />
</StrictMode>,
Expand Down
10 changes: 5 additions & 5 deletions src/pages/home/pages/global-feed/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useEffect } from 'react';
import { Redirect } from 'react-router-dom';
// import { Redirect } from 'react-router-dom';
import { useQueryParam, withDefault, NumberParam } from 'use-query-params';
import * as article from '@/entities/article';
import * as visitor from '@/entities/visitor';
import { ROUTES } from '@/shared/router';
// import * as visitor from '@/entities/visitor';
// import { ROUTES } from '@/shared/router';
import { Pagination } from '@/shared/ui';
import * as model from './model';

Expand All @@ -12,7 +12,7 @@ type Props = Readonly<{
}>;

const GlobalFeedPage = ({ pageSize = 10 }: Props) => {
const isAuth = visitor.selectors.useIsAuthorized();
// const isAuth = visitor.selectors.useIsAuthorized();
const [page, setPage] = useQueryParam('page', withDefault(NumberParam, 1));
const loading = model.selectors.useGetFeedLoading();
const isEmpty = model.selectors.useIsEmptyFeed();
Expand All @@ -28,7 +28,7 @@ const GlobalFeedPage = ({ pageSize = 10 }: Props) => {

return (
<>
{isAuth ? null : <Redirect from={ROUTES.globalFeed} to="/" />}
{/* {isAuth ? null : <Redirect from={ROUTES.globalFeed} to="/" />} */}
<article.Feed
articlesStore={model.$articles}
isEmpty={isEmpty}
Expand Down
4 changes: 2 additions & 2 deletions src/pages/home/ui/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Page, Row } from '@/shared/ui';
import { LogoutBanner } from './logout-banner';
// import { LogoutBanner } from './logout-banner';
import { Sidebar } from './sidebar';
import { Tabs } from './tabs';

Expand All @@ -10,7 +10,7 @@ type Props = Readonly<{
export const Layout = ({ children }: Props) => {
return (
<div className="home-page">
<LogoutBanner />
{/* <LogoutBanner /> */}
<Page>
<Row>
<main className="col-md-9">
Expand Down
2 changes: 1 addition & 1 deletion src/pages/settings/model/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ split({
});

visitor.logoutClicked.watch(() => {
history.push(ROUTES.root);
history.push(ROUTES.login);
});

export const $error = restore(changeUserDataFx.failData, {
Expand Down
11 changes: 6 additions & 5 deletions src/shared/router/model/store.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { createEvent, restore } from 'effector';
import { createBrowserHistory, Location } from 'history';
import { createMemoryHistory, createBrowserHistory, Location } from 'history';

// http://localhost:4100/home/
export const history = createBrowserHistory({
basename: '/home',
});
const isBrowser = typeof window !== 'undefined';

export const history = isBrowser
? createBrowserHistory()
: createMemoryHistory();

export const locationUpdated = createEvent<Location>();
export const $location = restore(locationUpdated, history.location);
Expand Down
35 changes: 21 additions & 14 deletions webpack/common.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,26 @@ const FaviconsWebpackPlugin = require('favicons-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const { SRC, FAVICON } = require('./constants');

const configPlugins = [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
defaultSizes: 'gzip',
openAnalyzer: false,
}),
new FaviconsWebpackPlugin({
logo: FAVICON,
}),
];

if (!process.env.BUILD_SERVER) {
configPlugins.push(
new HtmlWebpackPlugin({
template: 'index.html',
inject: 'body',
}),
);
}

module.exports = {
context: SRC,
entry: ['react-hot-loader/patch', './index.tsx'],
Expand All @@ -12,20 +32,7 @@ module.exports = {
// 'react-dom': '@hot-loader/react-dom',
// },
},
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
defaultSizes: 'gzip',
openAnalyzer: false,
}),
new HtmlWebpackPlugin({
template: 'index.html',
inject: 'body',
}),
new FaviconsWebpackPlugin({
logo: FAVICON,
}),
],
plugins: configPlugins,
module: {
rules: [
{
Expand Down
3 changes: 2 additions & 1 deletion webpack/constants.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const { resolve } = require('path');

exports.SRC = resolve(__dirname, '../src');
exports.DIST = resolve(__dirname, '../dist');
exports.FAVICON = resolve(__dirname, '../public', 'favicon.png');
exports.DIST = resolve(__dirname, '../dist/client');
exports.PUBLIC_PATH = '/static';
44 changes: 44 additions & 0 deletions webpack/cssModuleRules.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
}),
],
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
importLoaders: 1,
},
},
'postcss-loader',
],
exclude: /\.module\.css$/,
},
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
importLoaders: 1,
modules: {
localIdentName: '[local]-[hash:base64:10]',
},
},
},
'postcss-loader',
],
include: /\.module\.css$/,
},
],
},
};
52 changes: 7 additions & 45 deletions webpack/production.config.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
const { resolve } = require('path');
const { DefinePlugin } = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const { DIST, SRC } = require('./constants');
const { merge } = require('webpack-merge');
const { DIST, SRC, PUBLIC_PATH } = require('./constants');
const cssModuleRules = require('./cssModuleRules');

module.exports = {
module.exports = merge(cssModuleRules, {
mode: 'production',
output: {
path: DIST,
publicPath: '',
publicPath: PUBLIC_PATH,
filename: '[name].[contenthash].js',
},
optimization: {
Expand All @@ -24,7 +25,7 @@ module.exports = {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
chunks: 'all', // 指定vendors区块包含同步、异步加载的2类模块。
},
},
},
Expand All @@ -47,47 +48,8 @@ module.exports = {
concurrency: 100,
},
}),
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
}),
new DefinePlugin({
'process.env': JSON.stringify(process.env),
}),
],
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,

{
loader: 'css-loader',
options: {
importLoaders: 1,
},
},
'postcss-loader',
],
exclude: /\.module\.css$/,
},
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
importLoaders: 1,
modules: {
localIdentName: '[local]-[hash:base64:10]',
},
},
},
'postcss-loader',
],
include: /\.module\.css$/,
},
],
},
};
});
14 changes: 14 additions & 0 deletions webpack/server.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const path = require('path');
const { merge } = require('webpack-merge');
const common = require('./common.config');
const cssModuleRules = require('./cssModuleRules');

module.exports = merge(common, cssModuleRules, {
mode: 'development', // 便于开发调试,排查报错堆栈
entry: path.resolve(__dirname, '../server/index.tsx'),
output: {
filename: 'server.js',
path: path.resolve(__dirname, '../dist'),
},
target: 'node', // 目标环境为 Node.js
});