我很懒,什么都没留下系列 之 教你上手React服务端渲染(React SSR) HMR
技術(shù)棧:webpack3.9.1+webpack-dev-server2.9.5+React16.x + express4.x
前言
(好慌!可能是因?yàn)槲液軕?/strong>,導(dǎo)致...,然后,好吧,我比較懶,沒(méi)有然后了。。。切入正題ing,let's do it!!!)
網(wǎng)上關(guān)于React的SSR也很多,但都不夠詳細(xì),有的甚至讓初學(xué)者一頭霧水。不過(guò)這篇文章我將一步步詳細(xì)的介紹,從0開始配置React SSR,讓每個(gè)看到文章的人都能上手。
SSR的概念
Server Slide Rendering,縮寫為 SSR,即服務(wù)器端渲染,因?yàn)槭侵案鉰ava出身,也明白是怎么回事,其實(shí)SSR主要針對(duì) SPA應(yīng)用,目的大概有以下幾個(gè):
單頁(yè)應(yīng)用頁(yè)面大部分主要的 HTML并不是服務(wù)器返回,服務(wù)器只是返回一大串的腳本,頁(yè)面上看到的大部分內(nèi)容都是由腳本生成,對(duì)于一般網(wǎng)站影響不大,但是對(duì)于一些依賴搜索引擎帶來(lái)流量的網(wǎng)站來(lái)說(shuō)則是致命的,搜索引擎無(wú)法抓取頁(yè)面相關(guān)內(nèi)容,也就是用戶搜不到此網(wǎng)站的相關(guān)信息,自然也就無(wú)流量可言。
因?yàn)轫?yè)面 HTML由服務(wù)器端返回的腳本生成,一般來(lái)說(shuō)這種腳本的體積都不會(huì)太小,客戶端下載需要時(shí)間,瀏覽器解析以生成頁(yè)面元素也需要時(shí)間,這必然會(huì)導(dǎo)致頁(yè)面的顯示速度比傳統(tǒng)服務(wù)器端渲染得要慢,很容易出現(xiàn)首頁(yè)白屏的情況,甚至如果瀏覽器禁用了 JS,那么將直接導(dǎo)致頁(yè)面連基本的元素都看不到。
React中如何使用服務(wù)端渲染
react-dom是React專門為web端開發(fā)的渲染工具。我們可以在客戶端使用react-dom的render方法渲染組件,而在服務(wù)端,react-dom/server提供我們將react組件渲染成html的方法。
瀏覽器渲染與服務(wù)端渲染對(duì)比如下:(其中紅色框內(nèi)就是服務(wù)端渲染,很顯然比起瀏覽器渲染快了很多)
項(xiàng)目搭建
項(xiàng)目結(jié)構(gòu)圖如下:
build文件夾 用來(lái)配置webpack環(huán)境
- webpack.config.base.js是基礎(chǔ)配置
- webpack.config.client.js是客戶端打包配置
- webpack.config.server.js是用來(lái)打包服務(wù)器渲染的配置
package.json:
{"name": "juejin-reactssr","version": "1.0.0","description": "","main": "index.js","scripts": {"build:client": "webpack --config build/webpack.config.client.js","build:server": "webpack --config build/webpack.config.server.js","clear": "rimraf dist","build": "npm run clear && npm run build:client && npm run build:server","start":"node server/server.js"},"author": "Jerry","license": "ISC","dependencies": {"express": "^4.16.3","react": "^16.2.0","react-dom": "^16.2.0","react-router": "^4.2.0","react-router-dom": "^4.2.2"},"devDependencies": {"babel-core": "^6.26.0","babel-loader": "^7.1.2","babel-plugin-transform-decorators-legacy": "^1.3.4","babel-preset-es2015": "^6.24.1","babel-preset-es2015-loose": "^8.0.0","babel-preset-react": "^6.24.1","babel-preset-stage-1": "^6.24.1","cross-env": "^5.1.1","file-loader": "^1.1.5","html-webpack-plugin": "^2.30.1","http-proxy-middleware": "^0.17.4","memory-fs": "^0.4.1","react-hot-loader": "^3.1.3","rimraf": "^2.6.2","uglifyjs-webpack-plugin": "^1.1.2","webpack": "^3.9.1","webpack-dev-server": "^2.9.5","webpack-merge": "^4.1.2"} }webpack.config.base.js:```javascript const path = require('path') module.exports = {output: {path: path.join(__dirname, '../dist'),publicPath: '/public/',},devtool:"source-map",module: {rules: [{test: /.(js|jsx)$/,loader: 'babel-loader',exclude: [path.resolve(__dirname, '../node_modules')]}]}, }復(fù)制代碼webpack.config.server.js: ```javascript //此js用來(lái)將client/server-entry.js 打包成node能夠執(zhí)行的文件 const path = require('path') const webpackMerge = require('webpack-merge') const baseConfig = require('./webpack.config.base')const config=webpackMerge(baseConfig,{target: 'node',//打包成node端執(zhí)行entry: {app: path.join(__dirname, '../client/server-entry.js'),},output: {filename: 'server-entry.js',libraryTarget: 'commonjs2'//使用配置方案 commonjs2}, })module.exports = config復(fù)制代碼client文件夾 客戶端用來(lái)打包上線
app.js:
import React from 'react' import ReactDOM from 'react-dom' import App from './App.jsx'ReactDOM.render(<App/>, document.getElementById('root'))復(fù)制代碼App.jsx:
import React from 'react' export default class App extends React.Component{render(){return (<div>App</div>)} } 復(fù)制代碼server-entry.js:此文件用來(lái)生成服務(wù)器渲染所需模板
//服務(wù)端用來(lái)渲染的模板 import React from 'react' import App from './App.jsx' export default <App/> 復(fù)制代碼template.html:
<!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"><title>Title</title> </head> <body> <div id="root"><!-- app --></div> </body> </html> 復(fù)制代碼server文件夾 對(duì)應(yīng)服務(wù)端
const express = require('express') const ReactSSR = require('react-dom/server') const serverEntry = require('../dist/server-entry') const app = express()app.get('*', function (req, res) {//ReactDOMServer.renderToString則是把React實(shí)例渲染成HTML標(biāo)簽let appString = ReactSSR.renderToString(serverEntry.default);//返回給客戶端res.send(appString); }) app.listen(3000, function () {console.log('server is listening on 3000 port'); }) 復(fù)制代碼接下來(lái)
我們運(yùn)行 npm start ,打開瀏覽器輸入http://localhost:3000/ 我們發(fā)現(xiàn)服務(wù)器返回渲染的模板 ,到這里為止我們達(dá)到了最簡(jiǎn)單的SSR的目的(但是這還不是我們的最終目的,因?yàn)檫@里單單返回的只有渲染的模板,我們需要返回整個(gè)頁(yè)面,頁(yè)面中可能還引用其他的js等文件)
繼續(xù)完善
我們回到server端,改進(jìn)我們的server.js, + 所在行表示新增的內(nèi)容
const express = require('express') const ReactSSR = require('react-dom/server') const serverEntry = require('../dist/server-entry') + const fs=require('fs') + const path=require('path') const app = express()// 引入npm run build生成的index.html文件 + const template=fs.readFileSync(path.join(__dirname,'../dist/index.html'),'utf8') app.get('*', function (req, res) {//ReactDOMServer.renderToString則是把React實(shí)例渲染成HTML標(biāo)簽let appString = ReactSSR.renderToString(serverEntry.default);//<!--App-->位置 就是我們渲染返回的結(jié)果插入的位置+ appString=template.replace('<!--App-->',appString);//返回給客戶端res.send(appString); }) app.listen(3000, function () {console.log('server is listening on 3000 port'); }) 復(fù)制代碼控制臺(tái) npm start ,打開瀏覽器輸入http://localhost:3000/ 發(fā)現(xiàn),頁(yè)面引用的app.js文件也同樣返回的是整個(gè)頁(yè)面,這顯然不是我們所想要的
那是因?yàn)槲覀僺erver.js中 app.get('*', function (req, res) {}這個(gè)是對(duì)所有請(qǐng)求都是一樣的處理返回整個(gè)頁(yè)面 ,所以我們要對(duì)靜態(tài)頁(yè)面單獨(dú)處理,我們加上static中間件j就可以了
const express = require('express') const ReactSSR = require('react-dom/server') const serverEntry = require('../dist/server-entry') const fs=require('fs') const path=require('path') const app = express() //處理靜態(tài)文件 凡是通過(guò) /public訪問(wèn)的都是靜態(tài)文件 + app.use('/public',express.static(path.join(__dirname,"../dist"))) const template=fs.readFileSync(path.join(__dirname,'../dist/index.html'),'utf8') app.get('*', function (req, res) {//ReactDOMServer.renderToString則是把React實(shí)例渲染成HTML標(biāo)簽let appString = ReactSSR.renderToString(serverEntry.default);//<!--App-->位置 就是我們渲染返回的結(jié)果插入的位置appString=template.replace('<!-- app -->',appString);//返回給客戶端res.send(appString); }) app.listen(3000, function () {console.log('server is listening on 3000 port'); }) 復(fù)制代碼這樣app.js返回的就是對(duì)應(yīng)的js內(nèi)容了,而不是整個(gè)頁(yè)面了
以上就是我們服務(wù)端ssr的整個(gè)流程(PS:當(dāng)然目前還有個(gè)不好的地方就是,我們都直接命令行啟動(dòng)webpack進(jìn)行打包,就可以滿足我們的需求。但畢竟計(jì)劃趕不上變化,有時(shí)候你會(huì)發(fā)現(xiàn)用命令行啟動(dòng)webpack變得不是那么方便。比如我們?cè)谡{(diào)試react的服務(wù)端渲染的時(shí)候,我們不可能每次有文件更新,等著webpack打包完輸出到硬盤上某個(gè)文件,然后你重啟服務(wù)度去加載這個(gè)新的文件,因?yàn)檫@太浪費(fèi)時(shí)間了,畢竟開發(fā)時(shí)你隨時(shí)都可能改代碼,而且改動(dòng)可能還很小。)
那么要解決這個(gè)問(wèn)題怎么辦呢?我們可以在啟動(dòng)nodejs服務(wù)的時(shí)候,順帶啟動(dòng)webpack打包服務(wù),這樣我們可以在nodejs的執(zhí)行環(huán)境中拿到webpack打包的上下文,就可以不重啟服務(wù)但每次文件更新都可以拿到最新的bundle。
這個(gè)問(wèn)題我們先放在這里 (todo...)
接下來(lái),我們先來(lái)看看wepack-dev-server 以及 模塊熱替換(Hot Module Replacement 或 HMR)是 webpack 提供的最有用的功能之一。它允許在運(yùn)行時(shí)更新各種模塊,而無(wú)需進(jìn)行完全刷新。)
wepack-dev-server 和 HMR 不適用于生產(chǎn)環(huán)境,這意味著它應(yīng)當(dāng)只在開發(fā)環(huán)境使用,接下來(lái)我們來(lái)配置開發(fā)環(huán)境
webpack-dev-server配置
首先,package.json
"scripts": {"build:client": "webpack --config build/webpack.config.client.js","build:server": "webpack --config build/webpack.config.server.js",+ "dev:client":"cross-env NODE_ENV=development webpack-dev-server --config build/webpack.config.client.js","clear": "rimraf dist","build": "npm run clear && npm run build:client && npm run build:server","start":"node server/server.js"} 復(fù)制代碼webpack.config.client.js
const path = require('path') const webpackMerge = require('webpack-merge') const baseConfig = require('./webpack.config.base') + const webpack=require('webpack') const HTMLWebpackPlugin = require('html-webpack-plugin')//判斷當(dāng)前是不是開發(fā)環(huán)境 + const isDev = process.env.NODE_ENV === 'development'const config=webpackMerge(baseConfig,{entry: {app: path.join(__dirname, '../client/app.js'),},output: {filename: '[name].[hash].js',},plugins: [new HTMLWebpackPlugin({template: path.join(__dirname, '../client/template.html')})] })// localhost:8888/filename + if (isDev) {config.entry = {app: ['react-hot-loader/patch',path.join(__dirname, '../client/app.js')]}config.devServer = {host: '0.0.0.0',//代表任何方式進(jìn)行訪問(wèn) 本地ip localhost都可以compress: true,port: '8888',contentBase: path.join(__dirname, '../dist'),//告訴服務(wù)器從哪里提供內(nèi)容。只有在你想要提供靜態(tài)文件時(shí)才需要hot: true,//開啟HMR模式overlay: {errors: true //是否顯示錯(cuò)誤},publicPath: '/public',historyApiFallback: {//404 對(duì)應(yīng)的路徑配置index: '/public/index.html'}}config.plugins.push(new webpack.NamedModulesPlugin(),new webpack.HotModuleReplacementPlugin()) }module.exports = config 復(fù)制代碼app.js:
import React from 'react' import ReactDOM from 'react-dom' + import {AppContainer} from 'react-hot-loader' import App from "./App.jsx"; + const root=document.getElementById('root'); + const render=Component=>{ReactDOM.render(<AppContainer><Component/></AppContainer>,root)} + render(App); + if(module.hot){module.hot.accept('./App.jsx',()=>{const NextApp =require('./App.jsx').default;render(NextApp);}) } 復(fù)制代碼以上,devServer以及HMR已經(jīng)配置完成
修改App.jsx內(nèi)容 可以看到頁(yè)面無(wú)刷新就改變內(nèi)容了
回到之前未完待續(xù)的地方 (完成開發(fā)時(shí)的服務(wù)端渲染工作)
在server.js中我們區(qū)分環(huán)境變量
const express = require('express') const ReactSSR = require('react-dom/server')const fs = require('fs') const path = require('path') const app = express()+ const isDev = process.env.NODE_ENV === 'development' + if (!isDev) {//生產(chǎn)環(huán)境 直接到生成的dist目錄讀取文件const serverEntry = require('../dist/server-entry')//處理靜態(tài)文件 凡是通過(guò) /public訪問(wèn)的都是靜態(tài)文件app.use('/public', express.static(path.join(__dirname, "../dist")))const template = fs.readFileSync(path.join(__dirname, '../dist/index.html'), 'utf8')app.get('*', function (req, res) {//ReactDOMServer.renderToString則是把React實(shí)例渲染成HTML標(biāo)簽let appString = ReactSSR.renderToString(serverEntry.default);//<!--App-->位置 就是我們渲染返回的結(jié)果插入的位置appString = template.replace('<!-- app -->', appString);//返回給客戶端res.send(appString);}) } else {//開發(fā)環(huán)境 我們從內(nèi)存中直接讀取 減去了寫到硬盤上的時(shí)間const devStatic = require('./util/dev-static')devStatic(app); }app.listen(3000, function () {console.log('server is listening on 3000 port'); }) 復(fù)制代碼server目錄下新建dev-static.js 用來(lái)處理開發(fā)時(shí)候的服務(wù)端渲染
const axios = require('axios') const webpack = require('webpack') const path = require('path') const serverConfig = require('../../build/webpack.config.server') const ReactSSR = require('react-dom/server') const MemoryFs = require('memory-fs') const proxy = require('http-proxy-middleware')//getTemplate用來(lái)獲取打包后的模板(內(nèi)存中) const getTemplate = () => {return new Promise((resolve, reject) => {//http去獲取dev-server中的index.htmlaxios.get('http://localhost:8888/public/index.html').then(res => {resolve(res.data)}).catch(reject)}) }const Module = module.constructor;//node環(huán)境中啟動(dòng)一個(gè)webpack 來(lái)獲取打包后的server-entry.js const mfs = new MemoryFs//服務(wù)端使用webpack const serverCompiler = webpack(serverConfig); serverCompiler.outputFileSystem = mfs let serverBundle serverCompiler.watch({}, (err, stats) => {if (err) throw errstats = stats.toJSON()stats.errors.forEach(err => console.error(err))stats.warnings.forEach(warn => console.warn(warn))// 獲取bundle文件路徑const bundlePath = path.join(serverConfig.output.path,serverConfig.output.filename)const bundle = mfs.readFileSync(bundlePath, 'utf8')const m = new Module()m._compile(bundle, 'server-entry.js')serverBundle = m.exports.default })module.exports = function (app) { //http 代理:所有通過(guò)/public訪問(wèn)的 都代理到http://localhost:8888app.use('/public', proxy({target: 'http://localhost:8888'}))app.get('*', function (req, res) {getTemplate().then(template => {let content = ReactSSR.renderToString(serverBundle);res.send(template.replace('<!-- app -->', content));})}) } 復(fù)制代碼同時(shí),npm scripts配置如下:
"scripts": {"build:client": "webpack --config build/webpack.config.client.js","build:server": "webpack --config build/webpack.config.server.js","dev:client": "cross-env NODE_ENV=development webpack-dev-server --config build/webpack.config.client.js","dev:server": "cross-env NODE_ENV=development node server/server.js","clear": "rimraf dist","build": "npm run clear && npm run build:client && npm run build:server"}, 復(fù)制代碼運(yùn)行 npm run dev:client 和npm run dev:server,修改App.jsx的內(nèi)容 瀏覽器無(wú)刷新更新
以上就是最基礎(chǔ)的React SSR和HMR的配置,但還未涉及到數(shù)據(jù)以及路由等情況,接下來(lái)有時(shí)間我會(huì)在這個(gè)基礎(chǔ)上為大家?guī)?lái)mobx和react-router等整個(gè)項(xiàng)目的配置和部署,github 歡迎大家follow
總結(jié)
以上是生活随笔為你收集整理的我很懒,什么都没留下系列 之 教你上手React服务端渲染(React SSR) HMR的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 我的春招求职经验分享(已拿阿里京东网易等
- 下一篇: Redis 和 I/O 多路复用