最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 解析React服务端渲染原理

    正文概述 掘金(隐冬)   2021-03-03   491

    关于服务端渲染也就是我们说的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服务端渲染原理

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元