Javascript

??手撸JS系列:手写一个REACT SSR框架(未完结)

本文主要是介绍??手撸JS系列:手写一个REACT SSR框架(未完结),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

一、何为SSR

1.CSR介绍

在介绍SSR之前,我们可以先了解CSR。那什么是CSR呢?CSRClient Side Render在我们传统的REACT/VUE项目中采取的是单页(SPA)方式,即只有一个html,结构大致如下:

<html>
<head>
<title>传统spa应用</title>
</head> 
<body>
<script src="bundle.js"></script>
</body>
</html>
复制代码

所有的页面元素都是由打包后的bundle.js渲染出来的。

如上图 client(客户端)向server(服务端)发送ajax请求,服务端向客户端返回json数据,很明显,这样的前后端分离使得前后端职责更加清晰,后端只负责给前端提供数据,前端渲染界面。所谓CSR为客户端渲染,即页面是由js渲染出来的,不是由服务端直接返回的。

2.SSR介绍

SSRServer Slide Render,服务端渲染。即后端直接返回一个HTML页面。我们写一个服务端渲染的简单内容。新建文件夹react-ssr-frame->文件目录下执行npm init->在入口文件src/index.js编写如下代码:

var http=require("http");

const server=http.createServer(function(req,res){
    res.writeHead(200,{'Content-Type':"text/html"})
    res.end(`
        <!doctype html>
        <html>
            <head>
                <title>REACT SSR</title>
            </head>
            <body>
                <p>Hello World</p>
            </body>
        </html>
    `)
})

server.listen(3000,'127.0.0.1');
复制代码

执行node src/index命令并打开localhost:3000

可以看到后端返回了一个HTML页面并直接渲染到页面上。为了更确定我们页面的内容是服务端直接返回的。谷歌浏览器有个关闭js开关的选项,我们将其关闭。可以看到页面依旧显示正常(正常情况下CSR框架下页面会渲染失败)。

3.SSR VS CSR

借某网课的图,我们分析一下CSR和SSR流程。

3.1 CSR

  1. 浏览器下载HTML页面
  2. 解析HTML时解析到script标签,下载JS文件
  3. 浏览器接受到JS文件,执行运行React代码
  4. 页面开始渲染

3.2 SSR

1.浏览器下载HTML页面,页面开始渲染HTML页面。

3.3 优劣势对比

由上面的流程我们不难发现,SSR只需要发送1次http请求,CSR需要发送2次http请求(实际上不止2次)因为还需要发送http请求图标等。由于CSR的步骤繁杂,这就造成了一个CSR劣势,也就是我们经常头疼的单页应用的通病-首屏加载速度很慢。由于客户端只有一个HTML页面,在搜索引擎SEO爬虫爬行时只认识HTML中的内容,并不认识JS内的内容,所以不利于我们在搜索引擎上的排名。由此我们总结出SSR相比CSR的优势。

1.首屏加载速度较快。

2.SEO搜索引擎排名较好。

虽然SSR优势很多,但是对于SSR非常的消耗服务器端的性能。我们在项目首屏速度已经很快的时候并且不需要引擎排名的时候尽量还是使用CSR技术。

3.4 现有React流行SSR框架

现有比较主流的React SSR流行框架为next.js,next.js封装的很好,但是由于封装的太好了,对于我们扩展起来十分的不方便。有看Next.js文档的时间我们不如自己实现一个SSR服务端渲染框架,岂不乐哉?接下来我们就一步步为大家实现一个可以在实际项目中使用的SSR框架。

二、服务端编写React代码

我们接着使用上一章搭建的服务端项目。 既然是React项目,我们使用npm install 安装react,还记得在传统React/Vue项目中,浏览器端由于不认识我们的React等语法,我们需要使用Webpack打包,那么在服务器端呢?在服务器端node环境下也是不认识我们的React语法的,那么意味着我们也需要在服务器端渲染中使用Webpack进行打包。

const path=require("path");
const nodeExternals=require("webpack-node-externals");
module.exports={
    //由于在相同的require引用下,浏览器和服务器端效果是不一样的,所以需要指定目标平台
    target:"node",
    entry:"./src/index.js",
    mode:"development",
    output:{
        filename:"bundle.js",
        path:path.resolve(__dirname,'build')
    },
    externals:[nodeExternals()],
    //配置以后 require引用的node_modules不会被打包,还会保存之前的引用形式https://www.cnblogs.com/fanqshun/p/10073493.html
    module:{
        rules:[
            {
                test:/\.js?$/,
                loader:"babel-loader",
                exclude:/node_modules/
            }
        ]
    }
}
复制代码

react-dom中给我们提供了服务端渲染的方法renderToString,这个方法可以将react节点渲染成字符串。改造后的index.js页面如下:

var http=require("http");
import React from 'react';
import Hello from './pages/Hello';
import { renderToString } from 'react-dom/server';

const content=renderToString(<Hello />)

const server=http.createServer(function(req,res){
    res.writeHead(200,{'Content-Type':"text/html; charset=utf-8"})
    res.end(`
        <!doctype html>
        <html>
            <head>
                <title>REACT SSR</title>
            </head>
            <body>
                ${content}
            </body>
        </html>
    `)
})

server.listen(3000,'127.0.0.1');
复制代码

package.json中的script标签

"build":"webpack --config webpack.server.config.js",
"start":"node build/bundle.js"
复制代码

在项目根目录下新建.babelrc配置文件

{
    "presets":[
        ["@babel/preset-env", {
            "modules": false,
            "targets": {
              "browsers": ["> 1%", "last 2 versions", "ie >= 10"]
            },
            "useBuiltIns": "usage"
        }],
        "@babel/preset-react"
    ]
}
复制代码

现在让我们来回顾下项目目录

首先我们要在命令行安装安装项目所需要的依赖包

yarn add react-dom webpack webpack-cli webpack-node-externals babel-loader @babel/core @babel/node @babel/preset-react @babel/preset-env --save-dev 
复制代码
  • 开启一个窗口执行npm run build打包项目
  • 开启一个窗口执行npm run start运行项目

执行完打包后,我们会发现根目录下生成一个build目录,目录下的bundle.js就是本次打包后生成的源码目录。然后执行运行命令,启动打包后的bundle.js。然后我们打开浏览器看到:

有时候我们在运行项目中会出先莫名奇妙的错误,原因是node包有错误,这时候我们需要删除node_modules目录进行重新安装,但是删除node_modules本身就会出各种各样的异常,在这里我们借助rimraf来删除我们的node_modules

  1. 安装 rimraf :npm install rimraf --save-dev
  2. package.json中的script定义delete字段 "delete": "rimraf node_modules"
  3. 执行命令npm run delete

亲测有效,速度很快。我们看到

在服务器端编写React算是基本成功了。但是有没有发现这样的一个问题?在我们每次修改完代码。我们首先需要打包再运行,但是正常情况下这样肯定是不行的。我们借助--watch来监听文件的变化,借助nodemon来监听打包文件的变化并执行node命令运行项目。 首先更改build命令

"build": "webpack --config webpack.server.config.js --watch"
复制代码

--watch可以监听文件的变化并生成打包文件 npm install nodemon --save

"start": "nodemon --watch build --exec node build/bundle.js"
复制代码

分别执行命令,发现他们都好像在就绪状态,仿佛一直在监控着代码,如果你看不出来,说明你没有想象力(开个玩笑😂),我们更改下代码试下。发现这2个控制台分别又打包并执行了一次。这下子我们的目标已经达成了。但是这样依旧很烦,因为每次需要开2个命令行。那么有没有方法可以只开一个命令行呢?npm-all-run可以帮助我们完成这个任务,首先安装。

npm install npm-run-all --save
"scripts": {
    "delete": "rimraf node_modules",
    //--parallel表示并行执行后面的那个文件
    "dev": "npm-run-all --parallel dev:**",
    "dev:build": "webpack --config webpack.server.config.js --watch",
    "dev:start": "nodemon --watch build --exec node build/bundle.js"
},
复制代码

执行npm run dev,如下图所示即大功告成

三、事件响应

上一章,同学们都写出来了代码,我猜很多同学都以为服务端渲染仅仅如此?但是事实并非如此。我们在页面添加一个点击事件。

<button onClick={()=>alert("2")}>点击我!!</button>
复制代码

当我们点击这个按钮时,发现并没有任何反应,正常情况下是会有个弹框出来。控制台没报错,终端也没有报错。这时我们查看network

发现button按钮上没有click事件,小朋友,你是否有很多问号❓在我们反复确定我们是有写click事件后。我们将目光移到了enderToString方法。原来是因为renderToString方法仅仅是将React组件变成了字符串组件,但是并没有将其的事件绑定。那这就完了。服务端渲染怎么可以连最常见的点击事件都没有呢?这就要用到我们的同构。

1.什么是同构

简而言之,一套代码在服务端跑一遍,同时在客户端再跑一遍。这样的应用则为同构应用。

2.创造同构应用

服务端的代码我们已经有了,那么我们接下来编写客户端的代码。我们将src中的index.js文件移至新建的server文件夹下,该文件夹下编写的是服务端的代码。再新建客户端的文件夹取名为client,其中也放置一个index.js文件来写客户端渲染的代码。

//这是我们典型的客户端渲染挂载节点的方式,不同的是这里我们需要使用hydate,而不是render
import React from 'react';
import ReactDom from 'react-dom';
import Hello from './pages/Hello';

ReactDom.hydrate(<Hello />,document.getElementById("root"))
复制代码

然后我们需要创造一个id为root的节点方便挂载。

<!doctype html>
<html>
    <head>
        <title>REACT SSR</title>
    </head>
    <body>
        <div id="root">
            ${content}
        </div>
        <script src="./public/index.js"></script>
    </body>
</html>
复制代码

body里面script标签里面放置的是客户端渲染的代码。那么我们依旧需要将客户端代码打包。新建客户端打包文件webpack.client.config.js

const Path=require("path")

module.exports={
    entry:"./src/client/index.js",
    mode:"development",
    output:{
        filename:"index.js",
        path:Path.resolve(__dirname,'public')
    },
    module:{
        rules:[{
            test:/\.js?$/,
            loader:"babel-loader",
            exclude:/node_modules/
        }]
    }
}
复制代码

script标签内声明一个命令

"dev:build:client":"webpack --config webpack.client.config.js --watch",
复制代码

执行命令npm run dev可见public目录下生成了public目录以及目录下的index.js文件。

我们需要将node服务器文件改一下

const server = http.createServer(function (req, res) {

    let path = req.url;
    if (path === "/") {
        res.writeHead(200, { 'Content-Type': "text/html; charset=utf-8" });

        res.end(`
        <!DOCTYPE html>
        <html>
            <head>
                <title>REACT SSR</title>
            </head>
            <body>
                <div id="root">
                    ${content}
                </div>
                <script src="/index.js"></script>
            </body>
        </html>
    `)
    }else if(path==="/index.js"){
        let filePath=Path.join("public",req.url);
        fs.readFile(filePath,function(err,data){
            if(err){
                throw err;
            }
            // 设置请求头,访问文件类型为js文件
            res.setHeader("Content-Type", "text/js");
            res.end(data);
        })
    } 

})
复制代码

当访问路径为index.js时,将public目录下的index.js文件返回。 接下来我们点击按钮就可以弹出框了。

与此同时我们看见控制台报了这样一个错,这个表示服务端渲染不允许出现文本。
我们将其靠的紧凑一点,发现控制台错误消失不见了。很多同学很好奇这其中的原理。

3.同构原理分析

首先我们分析一下:

  1. 当用户输入localhost:3000,浏览器请求路径为/。

  2. node服务器返回一个html

  3. 浏览器接受到html渲染html

  4. 浏览器加载到JS文件。

  5. 浏览器中执行JS中的React代码

  6. JS中的React代码接管页面的操作

由此我们总结出:服务端渲染只发生在第一次进入页面的时候,当页面渲染完以后,路由跳转由JS进行控制,React中的JS文件将接管页面。服务端渲染并不是每一个页面都进行服务端渲染,二是第一个访问的页面进行服务端渲染。

由此我们大概知道了SSR主要作用就是使白屏事件缩短SEOSSR离不开CSR的配合。

四、路由处理

解决了事件处理,接下来便是重头戏了--路由。大家都知道在一个项目中肯定会有多个页面,每个页面都有相应的路由配置。路由的配置向来是react的一个重难点。在服务端渲染路由配置是和客户端渲染配置不一样的。

1.路由搭建

首先我们安装react-router-dom,新建router文件夹并在其下新建index.js文件,存放服务端渲染和客户端渲染的公共页面路由文件。

import Hello from './../pages/Hello';
import { Route } from 'react-router-dom';
import React from 'react';

export default(
    <div>
        <Route path="/" exact component={Hello}/>
    </div>
)
复制代码

客户端渲染文件中

import React from 'react';
import ReactDom from 'react-dom';
import Hello from './../pages/Hello';
import Routes from './../router/index';
import { BrowserRouter } from 'react-router-dom';

const App=()=>{
    return(
        <BrowserRouter>
            {Routes}
        </BrowserRouter>
    )
}

ReactDom.hydrate(<App />,document.getElementById("root"))
复制代码

服务端渲染则和客户端渲染路由不同,BrowserRouter可以智能感知你页面路径的变化并匹配相应的路由文件,但是服务端渲染并不能直接感应你路由的变化,所以我们将服务端路由写在请求方法里面,并将路径传给服务端渲染组件。此时路由文件:

var http = require("http");
var fs =require("fs");
var Path=require("path");
import Routes from './../router/index';
import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom';

const server = http.createServer(function (req, res) {
    const content = renderToString((
        <StaticRouter location={req.path} context={{}}>
            {Routes}
        </StaticRouter>
    ))

    let path = req.url;
    if(path.split(".").length===1){//判断是否是资源文件
        res.writeHead(200, { 'Content-Type': "text/html; charset=utf-8" });
    
        res.end(`
            <!DOCTYPE html>
            <html>
                <head>
                    <title>REACT SSR</title>
                </head>
                <body>
                    <div id="root">${content}</div>
                    <script src="/index.js"></script>
                </body>
            </html>
        `)
        
    }else if(path==="/index.js"){
        let filePath=Path.join("public",req.url);
        fs.readFile(filePath,function(err,data){
            if(err){
                throw err;
            }
            // 设置请求头,访问文件类型为js文件
            res.setHeader("Content-Type", "text/js");
            res.end(data);
        })
    } 

})

server.listen(3000, '127.0.0.1');
复制代码

我们新建一个LinkDemo.js页面查看效果是否生效

import React from 'react';
import { Link } from 'react-router-dom';

export default class LinkDemo extends React.Component{
    render(){
        return (
            <div>
                LINK
            </div>
        )
    }
}
复制代码

2.使用Link标签实现路由间的跳转

import React from 'react';
import { Link } from 'react-router-dom';

export default class LinkDemo extends React.Component{
    render(){
        return (
            <div>
                LINK
                <Link to="hello">hello</Link>
            </div>
        )
    }
}
复制代码

点击hello会跳转到Hello页面。

3.改造路由

我们需要将路由进行改造

//根路由文件
import Hello from './../pages/Hello';
import Link from './../pages/Link';

export default [
    {
        path:"/",
        exact:true,
        component:Hello,
        key:"/"
    },
    {
        path:"/link",
        exact:true,
        component:Link,
        key:"/link"
    }
]
//客户端渲染
<BrowserRouter>
            {
                Routes.map(props=>(
                        <Route {...props} />
                ))
            }
</BrowserRouter>
//服务端渲染
<StaticRouter location={req.path} context={{}}>
            {
                Routes.map(props=>(
                        <Route {...props} />
                ))
            }
</StaticRouter>
复制代码

4.多级路由配置

首先安装react-router-config

nm install react-router-config
复制代码

五、样式相关处理

1.图标处理

纵观大大小小的网站,图标都是一个很常见的一部分,那么如何实现这样的功能呢?其实浏览器在我们输入地址的时候,会默默的发送一个获取网站图标的请求,用于获取网站的图标。

首先我们需要一个类型为x-icon,即后缀名为.ico的图标文件,我一般是在在线生成ico这个网站生成自己喜欢的图标,然后我们放进public文件夹下,public文件夹存放网站的静态资源文件。然后node代码需要修改为:

if(path.split(".").length===1){
        res.writeHead(200, { 'Content-Type': "text/html; charset=utf-8" });
    
        res.end(`
            <!DOCTYPE html>
            <html>
                <head>
                    <title>REACT SSR</title>
                </head>
                <body>
                    <div id="root">${content}</div>
                    <script src="/index.js"></script>
                </body>
            </html>
        `)
        
    }else if(path.split(".").length===2&&path.split(".")[1]==="js"){//判断请求js文件
        let filePath=Path.join("public",req.url);
        fs.readFile(filePath,function(err,data){
            if(err){
                throw err;
            }
            // 设置请求头,访问文件类型为js文件
            res.setHeader("Content-Type", "text/js");
            res.end(data);
        })
    }else if(path.split(".").length===2&&path.split(".")[1]==="ico"){//判断请求ico文件
        let filePath=Path.join("public",req.url);
        fs.readFile(filePath,function(err,data){
            if(err){
                throw err;
            }
            // 设置请求头,访问文件类型为icon文件
            res.setHeader("Content-Type", "image/x-icon");
            res.end(data);
        })
    } 

复制代码

重新启动npm run dev,在tab标签的左上角你会发现

2.css样式处理

这部分内容主要与webpack相关,小伙伴可以看我的另一篇文章webpack4搭建开发环境

我们新建css文件并在Hello.js文件引入css样式

import React from 'react';
import { Link } from 'react-router-dom';
//引入样式
import "./index.css";

export default class Hello extends React.Component{
    render(){
        return (
            <div className="hello">
                hello world changes 啊啊啊啊我会
                <button onClick={()=>alert("2")}>点击我!!</button>
                <Link to="/link">Link</Link>
            </div>
        )
    }
}
//样式
.hello{
    background-color: aqua;
}
复制代码

1.首先我们需要安装处理css需要的包

npm install style-loader css-loader --save
复制代码

2.我们在服务端/客户端打包文件下写上loader的处理

{
                test:/\.css?$/,
                use:[
                    'style-loader','css-loader'
                ],
                exclude:/node_modules/
}
复制代码

重新运行代码,我们会发现终端报了这样一个错误

document is not defined?为什么会产生这样的错误呢,原因是style-loader需要将css挂在到document对象上,但是在服务器端没有document对象,所以需要借助另一个和style-loader相同的库来处理,这里我们安装

npm install isomorphic-style-loader --save
复制代码

然后我们在服务器端打包文件中将style-loader改为isomorphic-style-loader,这个库可以在服务端渲染css。

{
                test:/\.css?$/,
                use:[
                    'isomorphic-style-loader','css-loader'
                ],
                exclude:/node_modules/
}
复制代码

3.重新启动项目

重启项目,样式已经成功!

这篇关于??手撸JS系列:手写一个REACT SSR框架(未完结)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!