使用同一份應用代碼,同時提供瀏覽器環境和服務器環境下的應用,解決傳統瀏覽器單頁應用的兩個頑固問題: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服務器
首頁樣式
首頁代碼
登陸頁樣式
登陸頁代碼
Https://github.com/xialeistud...