react 服务端渲染

栏目: JavaScript · 发布时间: 5年前

内容简介:关于服务端渲染,这是一个老生常谈的事情了,已经很有成熟的方案在里面,最近接触了一下react的服务端渲染方式,偶有心得,为什么我要写这么一篇文章呢,因为我在研究如何开发的时候,发现网上的资料真的不够齐全,很多导致我花费更多的时间,所以我希望能够给予看到我文章的朋友一些帮助项目github链接:ssr: 服务端渲染

关于服务端渲染,这是一个老生常谈的事情了,已经很有成熟的方案在里面,最近接触了一下react的服务端渲染方式,偶有心得,为什么我要写这么一篇文章呢,因为我在研究如何开发的时候,发现网上的资料真的不够齐全,很多导致我花费更多的时间,所以我希望能够给予看到我文章的朋友一些帮助

项目github链接: github.com/HuskyToMa/h…

语言描述

ssr: 服务端渲染

前期准备

什么是ssr

ssr就是服务器渲染好前端页面然后推送到前端进行展示的一个过程。

为什么需要ssr

  1. 在单页应用中,是无法很好的支持seo的,所以很多方法是将入口页面用普通的html编写然后剩下的用单页应用的形式,这样就显得不是一个整体的项目。
  2. 服务端请求文件是直接在本地读取文件的,而不是客户端还需要发送http请求去获取文件内容,所以ssr会比正常的项目速度快一点
  3. 单页应用的首屏渲染会有一段空白时间,因为js还在加载的时候,页面是没有内容在里面的,而ssr会直接把dom元素渲染到html里面返回回来

前端如何做ssr

既然ssr是服务端渲染,那么肯定是后端进行渲染我们的数据,那不是不能用前端的各种框架了吗?

其实不然,由于nodejs的出现,打破了这么一个僵局,nodejs良好的接洽react框架,以至于我们在node中可以直接使用react开发页面,前后端共用同一套代码,通过react的rendertoString方法直接让react组件转换成dom字符串,然后输入到html模板中,在通过http response返回就直接渲染到页面(当然讲的简单,里面还是有一些东西的,后面慢慢分解)

技术支持

  1. react:主要库
  2. redux:状态管理库
  3. react-router:路由管理库
  4. webpack:代码编译
  5. babel:处理es6+的代码
  6. babel-node:处理nodejs的es6+代码
  7. nodejs:基础的nodejs知识
  8. express:nodejs框架
  9. typescript:js的超集(可以不用)

开始

ssr渲染本身是需要打包两套代码,一个用于服务器,一套用于客户端,所以我需要两套的开发环境以及生成环境

npm init 初始化我们的文件夹

创建文件目录

以下是我个人的习惯目录,可能并不是很正规,看一下即可

- webpack : 包含webpack的配置文件
    - antd.config.js:antd的less配置文件
    - babel.config.js:webpack到时候使用的配置文件
    - webpack.base.conf.js:webpack base文件
    - webpack.dev.conf.js: webpack 客户端dev文件
    - webpack.devServer.conf.js:webpack 服务端dev文件
    - webpack.pro.conf.js:webpack 客户端pro文件
    - webpack.proServer.conf.js:webpack 服务端pro文件
    
- src : 主要js目录
    - component:组件内容
    - pages :入口文件
    - redux :redux入口
    - store :生成store的方法
    - sass : sass样式目录
    - app.js : react文件统一入口
    - index.js: 渲染入口
    
- utils : 主要功能函数
    - render.js:主要执行渲染的文件
    - getTpl.js:获取模板
    
- build.js:启动构建的方法

- devServer.js:启动dev模式的文件

- server.js:打包构建的服务端内容

- .gitignore:忽略git上传内容

- .babelrc:node使用的babel配置文件

- tsconfig.json:ts的配置文件
复制代码

开发环境

需要几个必要的包:

react , react-dom , react-redux , redux , react-router , react-router-dom 
@types/react , @types/react-dom
复制代码

通过npm安装完成之后,我们就能在项目中进行引用了

如果已经按照我刚刚的目录创建好文件了,那么我们可以直接开始配置了。

很详细的配置内容大家基本都清楚,所以我这里就添加一些我自己的修改内容。

babel.conf.js

如果不用服务端渲染的话, 我们可以直接使用根目录中的babelrc文件,但是这次我们用了服务端渲染,所以我单独把wenpack打包的文件拎了出来放在一个js文件中

很多人会问,webpack中不是只能用require引用吗,你这里写export能拿到吗?
这个问题我会在稍后做出解释

export const clientBabelConfig = {
    presets:[
        "@babel/preset-env",
        "@babel/preset-react"
    ],// 没有啥特别变化,正常流程
    plugins: [
        "@babel/plugin-syntax-dynamic-import",// 支持import(),虽然我最后没有用
        "@babel/plugin-transform-runtime",
        "@babel/plugin-proposal-class-properties",
        [
            "import",
            {
                "libraryName": "antd",
                "libraryDirectory": "es",
                "style": true
            }
        ],
    ],
}
export const serverBabelConfig = {
    presets:[
        [
            "@babel/preset-env",
            {
                "modules": false,
                "targets":{
                    node: 'current',
                }
            }
        ],
        "@babel/preset-react"
    ],
    plugins:[
        [
            "import",
            {
                "libraryName": "antd",
            }
        ],
        "@babel/plugin-proposal-class-properties",
        "dynamic-import-node"   // node端的import()
    ]
} 
复制代码

貌似有一些重复的配置,我当时写完也就没有处理掉了,将就一下哈。

设置好babel的内容,就可以开始设置webpack的配置了,这一步相对简单,毕竟有过webpack配置竟然的人,很快就能够搭好了,我就只讲我这边觉得需要更改的几个内容:

webpack.dev.conf.js
  1. 需要使用 webpack-manifest-plugin 插件,生成manifest文件,用于读取生成的js目录
  2. dev环境关掉输出html的插件 html-webpack-plugin
webpack.devServer.conf.js
  1. output中的libraryTarget要设置成commonjs2
  2. 添加 target:'node'
  3. entry: './utils/render.js' // 开发环境中,直接将入口定在render就行了,我们只改变render的内容进行渲染

其余的配置与正常配置基本相近并没有特别大的区别

devServer.js

由于我们要同时启动客户端的代码和服务端的代码,所以我写了一个devServer的文件,内部使用了webpack的nodeAPI来构建环境,以及express来搭建服务端服务

在这文件里,其实分成了两个部分

import webpack from 'webpack';
import middleware from 'webpack-dev-middleware';
import devServerConfig from './webpack/webpack.devSever.conf';
import devClientConfig from './webpack/webpack.dev.conf';
import {ChunkExtractor} from '@loadable/server';
import express from 'express';
import MFS from 'memory-fs';
import path from 'path';

const app = new express();
const fs  = new MFS();
const clientCompiler = webpack(devClientConfig);
const serverCompiler = webpack(devServerConfig);
const PORT = 8080;
let renderPath = '';
let render;

// 使用webpack-dev-middleware中间件( webpack , devServer用的插件 )
app.use(middleware(clientCompiler , {
    noInfo: true,
    serverSideRender: true,
    publicPath: devClientConfig.output.publicPath,
}))
// 监听客户端内容编译完成
clientCompiler.hooks.done.tapAsync("done", stats=>{
    const info = stats.toJson();
    if( stats.hasErrors ) console.log(info.errors);
    if( stats.hasWarnings ) console.log(info.warnings);
    
})
// 修改服务端编译的输出方式( memory-fs 输入到内存 )
serverCompiler.outputFileSystem = fs;
serverCompiler.watch({
    aggregateTimeout: 300,
    poll: 1000,
    ignored: /node_modules/,
},(err , stats)=>{
    
    if(err) return console.log(err);
    console.log('compiler done');
    // 编译到内存的路径
    renderPath = path.join( devServerConfig.output.path , devServerConfig.output.filename );
    // 读取内容并转成String类型
    const content = fs.readFileSync( renderPath , 'utf-8').toString();
    // 因为读取的是js文件,所以直接执行可以获取到输出的内容 
    // new Function 找不到module 所以改用eval,由于在后端所以避免了风险
    render = eval(content).default;
})
// 设置项目的静态文件地址
app.use( express.static( devServerConfig.output.path ) );
app.get('/*',(req,res)=>{
    // console.log(  , '111111');
    const manifest = JSON.parse(clientCompiler.outputFileSystem.readFileSync(`${clientCompiler.outputPath}/manifest.json`));
    res.send( render( req.url , manifest) );
})
app.listen(PORT,function(){
    console.log('启动成功:localhost:' + PORT);
})
复制代码

render文件内容

import {Provider} from 'react-redux';
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import getTpl from './getTpl';
import configureStore from 'STORE'; // store 就是我们react设置的store内容
import isObject from 'isobject';
import routerConfig from 'ROUTER';

// 设置store内容
const store = configureStore();
export default function( url , manifest){

    if( !routerConfig[url] ){
        return null;
    }
    const js = manifestJsLoader( manifest );
    const css = manifestCssLoader( manifest );
    // 获取当前路由下面的组件
    const Component = routerConfig[url].component;

    const html = renderToStaticMarkup(  <Provider store={store}><Component/></Provider> );

    return getTpl( html , css , js )

}

const manifestJsLoader = ( manifest ) => {

    return  Object.keys(manifest)
            .filter(item=>item.endsWith('.js'))
            .map(item=>`<script src="${manifest[item]}"></script>`)
            .join('\n');

}
const manifestCssLoader = ( manifest ) => {

    return  Object.keys(manifest)
            .filter(item=>item.endsWith('.css'))
            .map(item=>`<link rel="stylesheet" href="${manifest[item]}"/>`)
            .join('\n');

}
复制代码

getTpl.js

就是用来渲染模板的

const getTpl = ( htmlContent , css , js) => {

    return `
    <!DOCTYPE html>
    <html>

    <head>
        <meta charset="utf-8">
        <title>create-react-project</title>
        <meta charset="UTF-8" />
        <meta property="qc:admins" content="1521476575645356367" />
        <!-- <meta name="google" value="notranslate" /> -->
        <meta http-equiv="pragma" content="no-cache" />
        <meta http-equiv="Cache-Control" content="no-store, no-cache, must-revalidate" />
        <meta http-equiv="Expires" content="0" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
        ${css}
    </head>

    <body>
        <div id="root">${htmlContent}</div>
    </body>
    ${js}
    </html>
    `

}

export default getTpl;
复制代码

很多import中大写的内容我用了webpack的alisa对应的文件可以去github上看下,当然,跟redux数据相对应的内容,需要服务端请求完之后,然后写入getTpl的模板里面,用script标签写一个window的内容,然后客户端进行重绘的时候,去调用window的内容存入store里面。

写好dev文件,基本上pro文件也已经出来了,配置基本相同没有啥区别,唯一的区别在于构建的文件中,webpack的入口我是用server.js 然后生成在dist目录里面。

还有重要的一点是,直接在命令行中运行node XXX.js如果内部使用了es6+的语法,那么是不可以运行的,安装一个babel-node然后用babel-node替换node来使用,但是最好在构建完之后的运行不要用babel-node,应该会增加额外的内容

希望这篇文章对大家有用处


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

超级IP

超级IP

吴声 / 中信出版集团 / 2016-7 / 49.00元

一切商业皆内容,一切内容皆IP! 从迪士尼、airbnb、YouTube、Instagram到微信、Papi酱、芈月传、鹿晗,IP浪潮席卷全球,这不仅仅是互联网领域的革命,更是未来商业的游戏新规则。 IP从泛娱乐形态快速渗透新商业生态全维度,正深化为不同行业共同的战略方法,甚至是一种全新的商业生存方式,即IP化生存。 超级IP的内核,是辨识度极高的可认同的商业符号,它意味着一种对......一起来看看 《超级IP》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

MD5 加密
MD5 加密

MD5 加密工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具