使用同一份應用代碼,同時提供瀏覽器環境和服務器環境下的應用,解決傳統瀏覽器單頁應用的兩個頑固問題:javascript
接下來咱們一塊兒從零開始搭建基於Webpack的React同構應用腳手架。css
mkdir react-ssr-example
cd react-ssr-example
yarn init -y
yarn add webpack webpack-cli webpack-dev-server -D # 安裝Webpack
yarn add react react-dom react-router-dom # 安裝React
yarn add @types/react @types/react-dom @types/react-router-dom -D # 安裝React聲明文件
yarn add express # 安裝express
yarn add css-loader sass-loader node-sass mini-css-extract-plugin # 安裝CSS相關模塊
yarn add ts-loader typescript # 安裝TypeScript
yarn add html-webpack-plugin # 安裝HTML處理插件
複製代碼
腳手架的完整目錄以下:(這些文件一步步都會有)html
|----build # 構建結果目錄
|----styles # 樣式
|----main.css
|----bundle.ssr.js # SSR應用文件
|----bundle.web.js # Web應用文件
|----index.html # Web應用入口HTML
|----src # 應用源碼
|----home # 首頁組件
|----index.scss # 首頁SCSS
|----index.tsx # 首頁組件
|----signin # 登陸頁組件
|----index.scss # 登陸頁SCSS
|----index.tsx # 登陸頁組件
|----App.tsx # 應用路由設置
|----index.html # Web應用入口HTML
|----main.ssr.tsx # SSR入口文件
|----main.web.tsx # Web入口文件
|----index.js # express服務器入口
|----package.json
|----tsconfig.json # TypeScript配置文件
|----webpack.config.js # Web應用webpack配置
|----webconfig.ssr.config.js # SSR應用Webpack配置
複製代碼
1.TypeScript配置,新建tsconfig.jsonjava
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"jsx": "react",
"strict": true,
"lib": [
"DOM"
],
"esModuleInterop": true
},
"include": [
"./src/**/*.ts",
"./src/**/*.tsx"
],
"exclude": [
"node_modules"
]
}
複製代碼
主要是添加了jsx設置和include設置node
2.Web環境webpack配置,新建webpack.config.jsreact
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssPlugin = require('mini-css-extract-plugin');
module.exports = {
entry: './src/main.web', // 入口文件
output: {
path: path.resolve(__dirname, 'build'), // 輸出目錄
filename: 'bundle.web.js' // 輸出文件
},
module: {
rules: [
{
test: /\.tsx?$/, // ts文件處理
use: 'ts-loader'
},
{
test: /\.scss$/, // scss文件處理
use: [MiniCssPlugin.loader, 'css-loader', 'sass-loader']
},
{
test: /\.css$/, // css文件處理
use: [MiniCssPlugin.loader, 'css-loader']
}
]
},
plugins: [
new HtmlWebpackPlugin({
chunks: ['main'], // chunk名稱,entry是字符串類型,所以chunk爲main
filename: 'index.html', // 輸出到build目錄的文件名
template: 'src/index.html' // 模板路徑
}),
new MiniCssPlugin({
filename: 'styles/[name].[contenthash:8].css', // 輸出的CSS文件名
chunkFilename: 'styles/[name].[contenthash:8].css'
})
],
resolve: {
extensions: ['.ts', '.tsx', '.js', '.json'] // 添加ts和tsx後綴
}
};
複製代碼
3.SSR環境Webpack配置,新建webpack.ssr.config.jswebpack
const path = require('path');
const MiniCssPlugin = require('mini-css-extract-plugin');
module.exports = {
entry: './src/main.ssr',
target: 'node', // 必須指定爲Node.js,不然會打包Node.js內置模塊
output: {
path: path.resolve(__dirname, 'build'),
filename: 'bundle.ssr.js',
libraryTarget: 'commonjs2' // 打包爲CommonJs模塊才能被Node.js加載
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader'
},
{
test: /\.scss$/,
use: [MiniCssPlugin.loader, 'css-loader', 'sass-loader']
},
{
test: /\.css$/,
use: [MiniCssPlugin.loader, 'css-loader']
}
]
},
plugins: [
new MiniCssPlugin({
filename: 'styles/[name].[contenthash:8].css',
chunkFilename: 'styles/[name].[contenthash:8].css'
})
],
resolve: {
extensions: ['.ts', '.tsx', '.js', '.json']
}
};
複製代碼
4.package.json添加npm命令git
{
"scripts": {
"build": "webpack",
"start": "webpack-dev-server",
"build-ssr": "webpack --config webpack.ssr.config.js"
}
}
複製代碼
src/home/index.tsxgithub
import React from 'react';
import './index.scss';
export default class Home extends React.Component {
render() {
return (
<div className="main">首頁</div>
)
}
}
複製代碼
src/home/index.scssweb
.main {
color: red;
}
複製代碼
src/signin/index.tsx
import React from 'react';
import { withRouter } from 'react-router-dom';
function SignIn(props: any) {
return (
<button onClick={() => props.history.replace('/')}>登陸</button>
)
}
export default withRouter(SignIn);
複製代碼
src/App.tsx
import React from 'react';
import { Switch, Route, Link } from 'react-router-dom'; // router
// 導入頁面組件
import Home from './home';
import SignIn from './signin';
// 導出路由組件配置
export default function App() {
return (
<Switch>
<Route path="/signin" component={SignIn} />
<Route path="/" component={Home} />
</Switch>
)
}
複製代碼
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello World</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
複製代碼
src/main.ssr.tsx
import React from 'react';
import { StaticRouter, Link } from 'react-router-dom';
import { renderToString } from 'react-dom/server';
import App from './App'; // 將路由組件導入進來
export function render(req: any) { // 導出一個渲染函數,根據請求連接進行分發
const context = {};
const html = renderToString(
<StaticRouter location={req.url} context={context}>
<header>
<nav>
<ul>
<li><Link to="/">首頁</Link></li>
<li><Link to="/signin">登陸</Link></li>
</ul>
</nav>
</header>
<App />
</StaticRouter>
);
return [html, context]; // 導出context和html渲染結果
}
複製代碼
src/main.web.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Link } from 'react-router-dom';
import App from './App';
ReactDOM.render( // 渲染路由
<BrowserRouter>
<header>
<nav>
<ul>
<li><Link to="/">首頁</Link></li>
<li><Link to="/signin">登陸</Link></li>
</ul>
</nav>
</header>
<App />
</BrowserRouter>, document.querySelector('#app'))
複製代碼
index.js
const express = require('express'); // 加載express
const { render } = require('./build/bundle.ssr'); // 加載ssr
const app = express();
app.use(express.static('.')) // 靜態資源配置
app.get('/*', (req, res) => { // 全部請求都走這裏處理,必須加*
const [html, context] = render(req)
console.log(context) // context目前沒發現啥用處
res.send(` <html> <head> <meta charset="UTF-8"> <title>SSR</title> <link href="build/styles/main.8f173ff5.css" rel="stylesheet"> </head> <body> <div id="app">${html}</div> <script src="build/bundle.web.js"></script> </body> </html> `);
console.log(context)
});
app.listen(8080)
複製代碼
注意:
npm run build # 構建Web
npm run build-ssr # 構建SSR
node index.js # 啓動Express服務器
複製代碼
首頁樣式
首頁代碼
登陸頁樣式
登陸頁代碼