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 相关文章推荐
JS实多级联动下拉菜单类,简单实现省市区联动菜单!
May 03 Javascript
JavaScript中使用构造函数实现继承的代码
Aug 12 Javascript
extjs grid设置某列背景颜色和字体颜色的方法
Sep 03 Javascript
JavaScript 注册事件代码
Jan 27 Javascript
jQuery中操控hidden、disable等无值属性的方法
Jan 06 Javascript
javascript 实现 原路返回
Jan 21 Javascript
微信小程序开发一键登录 获取session_key和openid实例
Nov 23 Javascript
详解Vue中使用v-for语句抛出错误的解决方案
May 04 Javascript
JS实现延迟隐藏功能的方法(类似QQ头像鼠标放上展示信息)
Dec 28 Javascript
Vue波纹按钮组件制作
Apr 30 Javascript
vue路由组件按需加载的几种方法小结
Jul 12 Javascript
axios全局注册,设置token,以及全局设置url请求网段的方法
Sep 25 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出现内存位置访问无效错误问题解决方法
2014/08/16 PHP
PHP的Yii框架中行为的定义与绑定方法讲解
2016/03/18 PHP
JS 非图片动态loading效果实现代码
2010/04/09 Javascript
JQERY limittext 插件0.2版(长内容限制显示)
2010/08/27 Javascript
能说明你的Javascript技术很烂的五个原因分析
2011/10/28 Javascript
基于jquery的无限级联下拉框js插件
2011/10/29 Javascript
js获取浏览器基本信息大全
2014/11/27 Javascript
js实现iframe跨页面调用函数的方法
2014/12/13 Javascript
有效提高JavaScript执行效率的几点知识
2015/01/31 Javascript
JavaScript的Polymer框架中dom-repeat与VM的相关操作
2015/07/29 Javascript
javascript中的正则表达式使用详解
2015/08/30 Javascript
javascript数据结构之双链表插入排序实例详解
2015/11/25 Javascript
JS组件Bootstrap实现弹出框和提示框效果代码
2015/12/08 Javascript
深入理解JS addLoadEvent函数
2016/05/20 Javascript
zepto与jquery的区别及zepto的不同使用8条小结
2016/07/28 Javascript
JS封装通过className获取元素的函数示例
2016/12/20 Javascript
详解angularJs指令的3种绑定策略
2017/04/13 Javascript
微信小程序合法域名配置方法
2019/05/06 Javascript
go语言计算两个时间的时间差方法
2015/03/13 Python
Python的Django框架中URLconf相关的一些技巧整理
2015/07/18 Python
两个命令把 Vim 打造成 Python IDE的方法
2016/03/20 Python
python中实现迭代器(iterator)的方法示例
2017/01/19 Python
python多线程之事件Event的使用详解
2018/04/27 Python
Redis使用watch完成秒杀抢购功能的代码
2018/05/07 Python
python内置数据类型之列表操作
2018/11/12 Python
python组合无重复三位数的实例
2018/11/13 Python
Pytorch DataLoader 变长数据处理方式
2020/01/08 Python
flask框架蓝图和子域名配置详解
2020/01/25 Python
Python3操作YAML文件格式方法解析
2020/04/10 Python
Python CategoricalDtype自定义排序实现原理解析
2020/09/11 Python
利用python进行文件操作
2020/12/04 Python
英国领先的品牌珠宝和配件供应商:Acotis Jewellery
2018/03/07 全球购物
ETO男装官方网店:ETO Jeans
2019/02/28 全球购物
缓刑人员的思想汇报
2014/01/11 职场文书
超市仓管员岗位职责范本
2014/09/18 职场文书
投诉书格式范本
2015/07/02 职场文书