React服务端渲染原理解析与实践


Posted in Javascript onMarch 04, 2021

关于服务端渲染也就是我们说的SSR大多数人都听过这个概念,很多同学或许在公司中已经做过服务端渲染的项目了,主流的单页面应用比如说Vue或者React开发的项目采用的一般都是客户端渲染的模式也就是我们说的CSR。

但是这种模式会带来明显的两个问题,第一个就是TTFP时间比较长,TTFP指的就是首屏展示时间,同时不具备SEO排名的条件,搜索引擎上排名不是很好。所以我们可以借助一些工具来进行改良我们的项目,将单页面应用编程服务器端渲染项目,这样就可以解决掉这些问题了。

目前主流的服务器端渲染框架也就是SSR框架有针对于Vue的Nuxt.js和针对React的Next.js这两个。这里我们并不使用这些SSR框架,而是从零开始完整搭建一套SSR框架,来熟悉他的底层原理。

服务器端编写 React 组件

如果是客户端渲染,浏览器首先会向浏览器发送请求,服务器返回页面的html文件,然后html中再向服务器发送请求,服务器返回js文件,js文件在浏览器中执行绘制出页面结构渲染到浏览器完成页面渲染。

如果是服务器端渲染这个流程就不同了,浏览器发送请求,服务器端运行React代码生成页面,然后服务器将生成好的页面返回给浏览器,浏览器进行渲染。这种情况下React代码就是服务器的一部分而不是前端部分了。

这里我们进行代码的演示,首选需要npm init初始化项目,然后安装react,express,webpack,webpack-cli,webpack-node-externals。

我们首先编写一个React的组件。 .src/components/Home/index.js, 因为我们这个js是在node环境执行的所以我们要遵循CommonJS规范,使用require和module.exports进行导入导出。

const React = require('react');

const Home = () => {
  return <div>home</div>
}

module.exports = {
  default: Home
};

我们这里开发的Home组件是不能直接在node中运行的,需要借助webpack工具将jsx语法打包编译成js语法,让nodejs可以争取的识别,我们需要创建一个webpack.server.js文件。

在服务器端使用webpack需要添加一个target为node的键值对。我们知道在服务器端如果使用path路径是不需要打包到js中的,如果在浏览器端使用了path是需要打包到js中的,所以在服务器端和在浏览器端需要编译出来的js是完全不同的。所以我们在打包的时候要告诉webpack打包的是服务器端的代码还是浏览器端的代码。

entry入口文件就是我们node的启动文件,这里我们写成./src/index.js,输出的output文件名称为bundle,目录在跟目录的build文件夹中。

const Path = require('path');
const NodeExternals = require('webpack-node-externals'); // 服务端运行webpack需要运行NodeExternals, 他的作用是将express这类node模块不被打包到js里。

module.exports = {
  target: 'node',
  mode: 'development',
  entry: './src/server/index.js',
  output: {
    filename: 'bundle.js',
    path: Path.resolve(__dirname, 'build')
  },
  externals: [NodeExternals()],
  module: {
    rules: [
      {
        test: /.js?$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        options: {
          presets: ['react', 'stage-0', ['env', {
            targets: {
              browsers: ['last 2 versions']
            }
          }]]
        }
      }
    ]
  }
}

安装依赖模块

npm install babel-loader babel-core babel-preset-react babel-preset-stage-0 babel-preset-env --save

接着我们这里基于express模块来编写一个简单的服务。./src/server/index.js

var express = require('express');
var app = express();
const Home = require('../Components/Home');
app.get('*', function(req, res) {
  res.send(`<h1>hello</h1>`);
})

var server = app.listen(3000);

运行webpack使用webpack.server.js配置文件来执行。

webpack --config webpack.server.js

打包之后在我们的目录下会出现一个bundle.js,这个js就是我们打包生成的最终可以运行的代码。我们可以使用node运行这个文件, 就启动了一个3000端口的服务器。我们访问127.0.0.1:3000可以访问这个服务,看到浏览器输出Hello。

node ./build/bundile.js

上面的代码我们运行前会使用webpack进行编译,所以也就支持了ES Modules规范,不再强制使用CommonJS了。

src/components/Home/index.js

import React from 'react';

const Home = () => {
  return <div>home</div>
}

export default Home;

/src/server/index.js中我们可以使用Home组件,这里我们首先需要安装react-dom,借助renderToString将Home组件转换为标签字符串,当然这里需要依赖React所以我们需要引入React。

import express from 'express';
import Home from '../Components/Home';
import React from 'react';
import { renderToString } from 'react-dom/server';

const app = express();
const content = renderToString(<Home />);
app.get('*', function(req, res) {
  res.send(`
    <html>
      <body>${content}</body>
    </html>
  `);
})

var server = app.listen(3000);
# 重新打包
webpack --config webpack.server.js
# 运行服务
node ./build/bundile.js

这时候页面就显示出了我们React组件的代码。

React的服务端渲染是建立在虚拟DOM上的服务器端渲染,而且服务端渲染会让页面的首屏渲染速度大大加快。不过服务端渲染也有弊端,客户端渲染React代码在浏览器端执行,他消耗的是用户浏览器端的性能,但是服务器端渲染消耗的是服务器端的性能,因为React代码在服务器上运行。极大的消耗了服务器的性能,因为React代码是很消耗计算性能的。

如果你的项目完全没有必要使用SEO优化并且你的项目访问速度已经很快了的情况下,建议还是不要使用SSR的技术了,因为他的成本开销还是比较大的。

上面我们的代码每次修改之后都需要重新执行webpack打包和启动服务器,这样调试起来太过麻烦,为了解决这个问题我们需要做一下webpack的自动打包和node的重启。我们在package.json中加入build命令,并且通过--watch监听文件变化进行自动打包。

{
  ...
  "scripts": {
    "build": "webpack --config webpack.server.js --watch"
  }
  ...
}

只是重新打包还不够,我们还需要重启node服务器,这里我们需要借助nodemon模块,这里我们使用全局安装nodemon, 在package.json文件中添加一个start命令来启动我们的node服务器。使用nodemon监听build文件并且发生改变之后重新exec运行"node ./build/bundile.js", 这里需要保留双引号,转译一下就好了。

{
  ...
  "scripts": {
    "start": "nodemon --watch build --exec node \"./build/bundile.js\"",
    "build": "webpack --config webpack.server.js --watch"
  }
  ...
}

这时我们启动服务器,这里需要在两个窗口运行下面的命令,因为build后不允许再输入其他命令了。

npm run build
npm run start

这个时候我们修改代码之后页面就会自动更新了。

但是上面的流程还是有些麻烦,我们需要两个窗口来执行命令,我们想要一个窗口将两个命令执行完毕,我们需要借助一个第三方模块npm-run-all,可以全局安装这个模块。然后再package.json中来修改一下。

我们在打包和调试应该是在开发环境,我们创建一个dev命令, 里面执行npm-run-all, --parallel表示并行执行, 执行dev:开头的所有命令。我们将start和build前面追加一个dev:,这个时候我想启动服务器同时监听文件改变运行npm run dev就可以了。

{
  ...
  "scripts": {
    "dev": "npm-run-all --parallel dev:**",
    "dev:start": "nodemon --watch build --exec node \"./build/bundile.js\"",
    "dev:build": "webpack --config webpack.server.js --watch"
  }
  ...
}

什么叫做同构

比如下面的代码,我们给div绑定一个click事件,希望点击的时候可以弹出click提示。但是运行之后我们会发现这个事件并没有被绑定上,因为服务器端没办法绑定事件。

src/components/Home/index.js

import React from 'react';

const Home = () => {
  return <div onClick={() => { alert('click'); }}>home</div>
}

export default Home;

一般我们的做法是先将页面渲染出来,然后将相同的代码在浏览器端像传统的React项目一样再去运行一遍,这样的话这个点击事件就有了。

这就衍生出一个同构的概念,我的理解是一套React代码在服务器端执行一次,在客户端再执行一次。

同构就可以解决点击事件无效的问题,首先服务器端执行一次能够正常的展示页面,客户端再执行一次就可以绑定上事件。

我们可以在页面渲染的时候加载一个index.js, 使用app.use创建静态文件的访问路径, 这样访问的index.js就会请求到/public/index.js文件中。

app.use(express.static('public'));

app.get('/', function(req, res) {
  res.send(`
    <html>
      <body>
        <div id="root">${content}</div>
        <script src="/index.js"></script>
      </body>
    </html>
  `);
})

public/index.js

console.log('public');

基于这种情况我们就可以将React代码在浏览器中执行一次,我们这里新建一个/src/client/index.js。将客户端执行的代码帖进去。这里我们同构代码使用hydrate代替render。

import React from 'react';
import ReactDOM from 'react-dom';

import Home from '../Components/Home';

ReactDOM.hydrate(<Home />, document.getElementById('root'));

然后我们还需要在根目录创建一个webpack.client.js文件。入口文件为./src/client/index.js,出口文件到public/index.js

const Path = require('path');

module.exports = {
  mode: 'development',
  entry: './src/client/index.js',
  output: {
    filename: 'index.js',
    path: Path.resolve(__dirname, 'public')
  },
  module: {
    rules: [
      {
        test: /.js?$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        options: {
          presets: ['react', 'stage-0', ['env', {
            targets: {
              browsers: ['last 2 versions']
            }
          }]]
        }
      }
    ]
  }
}

package.json文件中添加一条打包client目录的命令

{
  ...
  "scripts": {
    "dev": "npm-run-all --parallel dev:**",
    "dev:start": "nodemon --watch build --exec node \"./build/bundile.js\"",
    "dev:build": "webpack --config webpack.server.js --watch",
    "dev:build": "webpack --config webpack.client.js --watch",
  }
  ...
}

这样我们启动的时候会编译client运行的文件。再去访问页面的时候就可以绑定好事件了。

下面我们对上面工程的代码进行整理,上面webpack.server.js和webpack.client.js文件有很多重复的地方,我们可以使用webpack-merge插件对内容进行合并。

webpack.base.js

module.exports = {
  module: {
    rules: [
      {
        test: /.js?$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        options: {
          presets: ['react', 'stage-0', ['env', {
            targets: {
              browsers: ['last 2 versions']
            }
          }]]
        }
      }
    ]
  }
}

webpack.server.js

const Path = require('path');
const NodeExternals = require('webpack-node-externals'); // 服务端运行webpack需要运行NodeExternals, 他的作用是将express这类node模块不被打包到js里。

const merge = require('webpack-merge');
const config = require('./webpack.base.js');

const serverConfig = {
  target: 'node',
  mode: 'development',
  entry: './src/server/index.js',
  output: {
    filename: 'bundle.js',
    path: Path.resolve(__dirname, 'build')
  },
  externals: [NodeExternals()],
}

module.exports = merge(config, serverConfig);

webpack.client.js

const Path = require('path');
const merge = require('webpack-merge');
const config = require('./webpack.base.js');

const clientConfig = {
  mode: 'development',
  entry: './src/client/index.js',
  output: {
    filename: 'index.js',
    path: Path.resolve(__dirname, 'public')
  }
};

module.exports = merge(config, clientConfig);

src/server中放置的是服务端运行的代码,src/client放置的是浏览器端运行的js。

到此这篇关于React服务端渲染原理解析与实践的文章就介绍到这了,更多相关React服务端渲染内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
JavaScript高级程序设计 DOM学习笔记
Sep 10 Javascript
Jquery提交表单 Form.js官方插件介绍
Mar 01 Javascript
JS性能优化笔记搜索整理
Aug 21 Javascript
jQuery实现简单倒计时功能的方法
Jul 04 Javascript
Vue非父子组件通信详解
Jun 12 Javascript
JavaScript之promise_动力节点Java学院整理
Jul 03 Javascript
微信小程序 五星评分的实现实例
Aug 04 Javascript
vue router使用query和params传参的使用和区别
Nov 13 Javascript
js传递数组参数到后台controller的方法
Mar 29 Javascript
vue实现手机端省市区区域选择
Sep 27 Javascript
js实现图片无缝循环轮播
Oct 28 Javascript
JS实现瀑布流效果
Mar 07 Javascript
vue打开新窗口并实现传参的图文实例
Mar 04 #Vue.js
Vue-router编程式导航的两种实现代码
Mar 04 #Vue.js
手写Vue2.0 数据劫持的示例
Mar 04 #Vue.js
vue3.0封装轮播图组件的步骤
Mar 04 #Vue.js
vue3.0 项目搭建和使用流程
Mar 04 #Vue.js
vue 数据双向绑定的实现方法
Mar 04 #Vue.js
JavaScript中跨域问题的深入理解
Mar 04 #Javascript
You might like
php 输出双引号&quot;与单引号'的方法
2010/05/09 PHP
php 阴历-农历-转换类代码
2012/01/16 PHP
php curl操作API接口类完整示例
2019/05/21 PHP
JavaScript 继承机制的实现(待续)
2010/05/18 Javascript
JavaScript 异步方法队列链实现代码分析
2010/06/05 Javascript
最简单的js图片切换效果实现代码
2011/09/24 Javascript
js使用数组判断提交数据是否存在相同数据
2013/11/27 Javascript
再谈javascript原型继承
2014/11/10 Javascript
使用pjax实现无刷新更改页面url
2015/02/05 Javascript
jQuery选择器源码解读(五):tokenize的解析过程
2015/03/31 Javascript
JavaScript中的条件判断语句使用详解
2015/06/03 Javascript
深入浅析javascript立即执行函数
2015/10/23 Javascript
浅析jQuery Ajax通用js封装
2016/06/22 Javascript
利用js获取下拉框中所选的值
2016/12/01 Javascript
Bootstrap基本组件学习笔记之导航(10)
2016/12/07 Javascript
详解三种方式解决vue中v-html元素中标签样式
2018/11/22 Javascript
详解vue 命名视图
2019/08/14 Javascript
Nodejs 数组的队列以及forEach的应用详解
2021/02/25 NodeJs
[46:55]Ti4 冒泡赛第二轮 LGD vs C9
2014/07/14 DOTA
[49:28]VP vs Optic 2018国际邀请赛小组赛BO2 第二场 8.16
2018/08/17 DOTA
[07:25]DOTA2-DPC中国联赛2月5日Recap集锦
2021/03/11 DOTA
Python编程实现线性回归和批量梯度下降法代码实例
2018/01/04 Python
Flask 让jsonify返回的json串支持中文显示的方法
2018/03/26 Python
python dataframe 输出结果整行显示的方法
2018/06/14 Python
python3实现网络爬虫之BeautifulSoup使用详解
2018/12/19 Python
利用Python对文件夹下图片数据进行批量改名的代码实例
2019/02/21 Python
对YOLOv3模型调用时候的python接口详解
2019/08/26 Python
pycharm 更改创建文件默认路径的操作
2020/02/15 Python
前端面试必备之CSS3的新特性
2017/09/05 HTML / CSS
大学生专科毕业生自我评价
2013/11/17 职场文书
营业员实习自我鉴定
2013/12/07 职场文书
村党支部群众路线教育实践活动对照检查材料
2014/09/26 职场文书
教师党员自我评价范文
2015/03/04 职场文书
给原生html中添加水印遮罩层的实现示例
2021/04/02 Javascript
python字典进行运算原理及实例分享
2021/08/02 Python
「睡美人」爱洛公主粘土人开订
2022/03/22 日漫