技术栈:webpack3.9.1+webpack-dev-server2.9.5+React16.x + express4.x

前言

(好慌!可能是因为我很懒,导致...,然后,好吧,我比较懒,没有然后了。。。切入正题ing,let's do it!!!)

网上关于React的SSR也很多,但都不够详细,有的甚至让初学者一头雾水。不过这篇文章我将一步步详细的介绍,从0开始配置React SSR,让每个看到文章的人都能上手。

SSR的概念

Server Slide Rendering,缩写为 SSR,即服务器端渲染,因为是之前搞java出身,也明白是怎么回事,其实SSR主要针对 SPA应用,目的大概有以下几个:

  1. 解决单页面应用的 SEO
    单页应用页面大部分主要的 HTML并不是服务器返回,服务器只是返回一大串的脚本,页面上看到的大部分内容都是由脚本生成,对于一般网站影响不大,但是对于一些依赖搜索引擎带来流量的网站来说则是致命的,搜索引擎无法抓取页面相关内容,也就是用户搜不到此网站的相关信息,自然也就无流量可言。
  2. 解决渲染白屏
    因为页面 HTML由服务器端返回的脚本生成,一般来说这种脚本的体积都不会太小,客户端下载需要时间,浏览器解析以生成页面元素也需要时间,这必然会导致页面的显示速度比传统服务器端渲染得要慢,很容易出现首页白屏的情况,甚至如果浏览器禁用了 JS,那么将直接导致页面连基本的元素都看不到。

React中如何使用服务端渲染

react-dom是React专门为web端开发的渲染工具。我们可以在客户端使用react-dom的render方法渲染组件,而在服务端,react-dom/server提供我们将react组件渲染成html的方法。

浏览器渲染与服务端渲染对比如下:(其中红色框内就是服务端渲染,很显然比起浏览器渲染快了很多)

项目搭建

项目结构图如下:

build文件夹 用来配置webpack环境

  • webpack.config.base.js是基础配置
  • webpack.config.client.js是客户端打包配置
  • webpack.config.server.js是用来打包服务器渲染的配置

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')]}]},
}复制代码
webpack.config.server.js:
```javascript
//此js用来将client/server-entry.js 打包成node能够执行的文件
const path = require('path')
const webpackMerge = require('webpack-merge')
const baseConfig = require('./webpack.config.base')const config=webpackMerge(baseConfig,{target: 'node',//打包成node端执行entry: {app: path.join(__dirname, '../client/server-entry.js'),},output: {filename: 'server-entry.js',libraryTarget: 'commonjs2'//使用配置方案 commonjs2},
})module.exports = config复制代码

client文件夹 客户端用来打包上线

app.js:

import React from 'react'
import ReactDOM from 'react-dom'
import App from './App.jsx'ReactDOM.render(<App/>, document.getElementById('root'))复制代码

App.jsx:

import React from 'react'
export default class App extends React.Component{render(){return (<div>App</div>)}
}
复制代码

server-entry.js:此文件用来生成服务器渲染所需模板

//服务端用来渲染的模板
import React from 'react'
import App from './App.jsx'
export default <App/>
复制代码

template.html:

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body>
<div id="root"><!-- app --></div>
</body>
</html>
复制代码

server文件夹 对应服务端

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实例渲染成HTML标签let appString = ReactSSR.renderToString(serverEntry.default);//返回给客户端res.send(appString);
})
app.listen(3000, function () {console.log('server is listening on 3000 port');
})
复制代码

接下来

我们运行 npm start ,打开浏览器输入http://localhost:3000/ 我们发现服务器返回渲染的模板 ,到这里为止我们达到了最简单的SSR的目的(但是这还不是我们的最终目的,因为这里单单返回的只有渲染的模板,我们需要返回整个页面,页面中可能还引用其他的js等文件)

继续完善

我们回到server端,改进我们的server.js, + 所在行表示新增的内容

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实例渲染成HTML标签let appString = ReactSSR.renderToString(serverEntry.default);//<!--App-->位置 就是我们渲染返回的结果插入的位置+ appString=template.replace('<!--App-->',appString);//返回给客户端res.send(appString);
})
app.listen(3000, function () {console.log('server is listening on 3000 port');
})
复制代码

控制台 npm start ,打开浏览器输入http://localhost:3000/ 发现,页面引用的app.js文件也同样返回的是整个页面,这显然不是我们所想要的

那是因为我们server.js中 app.get('*', function (req, res) {}这个是对所有请求都是一样的处理返回整个页面 ,所以我们要对静态页面单独处理,我们加上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()
//处理静态文件 凡是通过 /public访问的都是静态文件
+ 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实例渲染成HTML标签let appString = ReactSSR.renderToString(serverEntry.default);//<!--App-->位置 就是我们渲染返回的结果插入的位置appString=template.replace('<!-- app -->',appString);//返回给客户端res.send(appString);
})
app.listen(3000, function () {console.log('server is listening on 3000 port');
})
复制代码

这样app.js返回的就是对应的js内容了,而不是整个页面了

以上就是我们服务端ssr的整个流程(PS:当然目前还有个不好的地方就是,我们都直接命令行启动webpack进行打包,就可以满足我们的需求。但毕竟计划赶不上变化,有时候你会发现用命令行启动webpack变得不是那么方便。比如我们在调试react的服务端渲染的时候,我们不可能每次有文件更新,等着webpack打包完输出到硬盘上某个文件,然后你重启服务度去加载这个新的文件,因为这太浪费时间了,毕竟开发时你随时都可能改代码,而且改动可能还很小。)

那么要解决这个问题怎么办呢?我们可以在启动nodejs服务的时候,顺带启动webpack打包服务,这样我们可以在nodejs的执行环境中拿到webpack打包的上下文,就可以不重启服务但每次文件更新都可以拿到最新的bundle。

这个问题我们先放在这里 (todo...)

接下来,我们先来看看wepack-dev-server 以及 模块热替换(Hot Module Replacement 或 HMR)是 webpack 提供的最有用的功能之一。它允许在运行时更新各种模块,而无需进行完全刷新。)

wepack-dev-server 和 HMR 不适用于生产环境,这意味着它应当只在开发环境使用,接下来我们来配置开发环境

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"}
复制代码

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')//判断当前是不是开发环境
+ 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',//代表任何方式进行访问 本地ip localhost都可以compress: true,port: '8888',contentBase: path.join(__dirname, '../dist'),//告诉服务器从哪里提供内容。只有在你想要提供静态文件时才需要hot: true,//开启HMR模式overlay: {errors: true //是否显示错误},publicPath: '/public',historyApiFallback: {//404 对应的路径配置index: '/public/index.html'}}config.plugins.push(new webpack.NamedModulesPlugin(),new webpack.HotModuleReplacementPlugin())
}module.exports = config
复制代码

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);})
}
复制代码

以上,devServer以及HMR已经配置完成

修改App.jsx内容 可以看到页面无刷新就改变内容了

回到之前未完待续的地方 (完成开发时的服务端渲染工作)

在server.js中我们区分环境变量

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) {//生产环境 直接到生成的dist目录读取文件const serverEntry = require('../dist/server-entry')//处理静态文件 凡是通过 /public访问的都是静态文件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实例渲染成HTML标签let appString = ReactSSR.renderToString(serverEntry.default);//<!--App-->位置 就是我们渲染返回的结果插入的位置appString = template.replace('<!-- app -->', appString);//返回给客户端res.send(appString);})
} else {//开发环境 我们从内存中直接读取 减去了写到硬盘上的时间const devStatic = require('./util/dev-static')devStatic(app);
}app.listen(3000, function () {console.log('server is listening on 3000 port');
})
复制代码

server目录下新建dev-static.js 用来处理开发时候的服务端渲染

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用来获取打包后的模板(内存中)
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环境中启动一个webpack 来获取打包后的server-entry.js
const mfs = new MemoryFs//服务端使用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 代理:所有通过/public访问的 都代理到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));})})
}
复制代码

同时,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"},
复制代码

运行 npm run dev:client 和npm run dev:server,修改App.jsx的内容 浏览器无刷新更新

以上就是最基础的React SSR和HMR的配置,但还未涉及到数据以及路由等情况,接下来有时间我会在这个基础上为大家带来mobx和react-router等整个项目的配置和部署,github 欢迎大家follow

我很懒,什么都没留下系列 之 教你上手React服务端渲染(React SSR) HMR相关推荐

  1. 我是个程序员,每天敲敲打打,哪天电脑崩溃了会发现我这辈子啥都没留下

    2019独角兽企业重金招聘Python工程师标准>>> 我尽量用平和一点的口吻跟你说说关于程序员的那点事儿. 1. 我在一个叫XXXX的公司上班,那地方有50%的人整天干的事情就是催 ...

  2. SSO单点登录系列2:cas客户端和cas服务端交互原理动画图解,cas协议终极分析

    落雨 cas 单点登录 一.用户第一次访问web1应用. ps:上图少画了一条线,那一条线,应该再返回来一条,然后再到server端,画少了一步...谢谢提醒.而且,重定向肯定是从浏览器过去的.我写的 ...

  3. twisted系列教程十一 — 一个twisted 的服务端

    A Twisted Poetry Server 既然我们已经学了这么多twisted client 的编写,现在让我们来用twisted来重新实现一下我们的poetry server 吧.我们要多谢谢 ...

  4. Zookeeper系列(十)zookeeper的服务端启动详述

    作者:leesf    掌控之中,才会成功:掌控之外,注定失败.出处:http://www.cnblogs.com/leesf456/p/6105276.html尊重原创,大家功能学习进步: 一.前言 ...

  5. Vue2系列教程——SSR服务端渲染

    Vue2 SSR服务端渲染 概念:ssr(server side render)服务端渲染 优点: 有利于搜索引擎的SEO操作,由于搜索引擎爬取的是完全渲染出来的页面. 对于网络慢或运行慢的设备,可提 ...

  6. 计算机的用户终端,计算机终端、客户端、服务端都是什么概念,他们之间的区别是什么?谢谢,大家,小弟是菜鸟...

    终端也称终端设备,是计算机网络中处于网络最外围的设备.客户端或称为用户端,是指与服务器相对应,为客户提供本地服务的程序.服务端是为客户端服务的,服务的内容诸如向客户端提供资源,保存客户端数据.终端.客 ...

  7. ROS摄像机的标定(这里很好的一点就是给出了标定结果的各个参数的含义,这个很多都没讲)

    这里很好的一点就是给出了标定结果的各个参数的含义,这个很多都没讲 转载自:https://blog.csdn.net/artista/article/details/51125560 ROS摄像机的标 ...

  8. 【答学员问】面试谈的很好,为什么最后都没下offer

    提问: 为什么每次面试自己喜欢的公司,谈得也挺好的,怎么最后都没通知呢? 招聘行业有个共识,那就是如果没有给通知一般就是没有通过, 有的学员会问,为什么不打电话通知一下呢? 我猜测,有一方面的原因是怕 ...

  9. 网上看到的!!很值得欣赏~~(没耐…

    引子寒假在家,一人去那久违的田野.似乎就是伸手可及的地方,蓝天缓缓的伸展.脚下的路,就在田野里蜿蜒而远去.顺着望,一片麦绿色中,几处消瘦的身影,便是冬日中静默的白杨树了.再远处,地是愈加平坦的.隐约一 ...

最新文章

  1. 最后期限已至,高通收购恩智浦全剧终!中国一刀切断高通物联网全局梦!
  2. yolov5检测完不显示框和标注
  3. 程序员也要多读些专业之外的书
  4. 关于Python的编译
  5. 'Request' object has no attribute 'META'报错解决
  6. 同步数据库仅在Worker内,目前只有Chrome6支持
  7. java之父_java之父:被下载达7000万次的编程视频教程,你还没有看过?
  8. 福建二级计算机考试12,福建农林大学2016年12月计算机二级考试通知
  9. 基础编程题目集 6-5 求自定类型元素的最大值 (10 分)
  10. 中国最让人脸红的节目:爆火14年的湿身诱惑,为何还没被叫停?
  11. python怎么用字符画_用Python把图片变成字符画
  12. Golang 协程的使用方法
  13. 找不到MSVCR120.dll,无法执行代码 ——问题解决方案
  14. Pascal基础教程
  15. pionner软件操作笔记
  16. matlab制作水印,怎么在含有水印的图像中提取出水印
  17. 悲!企业软件被360误认木马病毒!
  18. 互联网广告行业(01)------ 初识了解DSP、SSP、ADX
  19. Excel 按照某一列不同内容插入分页符号
  20. 五一放一天不调休,你能接受吗?

热门文章

  1. VC6解决托盘菜单不消失
  2. flex 设置换行flex-wrap
  3. Spring(十二)之JDBC框架
  4. Introduction to Mathematical Thinking - Week 3
  5. 常用正则表达式大全!
  6. HashTable 解决碰撞(冲突)的方法 —— 分离链接法(separate chaining)
  7. 单例模式 GetInstance()
  8. 一种在MVC3框架里面设置模板页的方法,不使用_ViewStart
  9. 10天学安卓-第六天
  10. 内存地址对齐提升程序性能