最近在开发一个服务端渲染工具,通过一篇小文大致介绍下服务端渲染,和服务端渲染的方式方法。在此文后面有两中服务端渲染方式的构思,根据你对服务端渲染的利弊权衡,你会选择哪一种服务端渲染方式呢?

什么是服务器端渲染

使用 React 构建客户端应用程序,默认情况下,可以在浏览器中输出 React 组件,进行生成 DOM 和操作 DOM。React 也可以在服务端通过 Node.js 转换成 HTML,直接在浏览器端“呈现”处理好的 HTML 字符串,这个过程可以被认为 “同构”,因为应用程序的大部分代码都可以在服务器和客户端上运行。

为什么使用服务器端渲染

与传统 SPA(Single Page Application - 单页应用程序)相比,服务器端渲染(SSR)的优势主要在于:

更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。

更好的用户体验,对于缓慢的网络情况或运行缓慢的设备,加载完资源浏览器直接呈现,无需等待所有的 JavaScript 都完成下载并执行,才显示服务器渲染的HTML。

服务端渲染的弊端

由于服务端与浏览器客户端环境区别,选择一些开源库需要注意,部分库是无法在服务端执行,比如你有 document、window 等对象获取操作,都会在服务端就会报错,所以在选择的开源库要做甄别。

使用服务端渲染,比如要起一个专门在服务端渲染的服务,与之前,只管客户端所需静态资源不同,你还需要 Node.js 服务端的和运维部署的知识,对你所需要掌握的知识点要求更多

服务器需要更多的负载,在 Node.js 中完成渲染,由于 Node.js 的原因大量的CPU资源会被占用。

下文介绍一种服务端渲染的“操作”,这个新的操作拥有新的问题,比如API请求两次,各种服务端问题,你就无能为力了,因为这个新的工具用Golang写的,你的团队或者是你,需要了解一下Golang,你说气不气人又要多学东西。

服务端渲染两种方式

根据上文介绍对服务端渲染利弊有所了解,我们可以根据利弊权衡取舍,最近在做服务端渲染的项目,找到多种服务端渲染解决方案,大致分为两类。

第一种方式

传统方式服务端渲染,解决用户体验和更好的 SEO,有诸多工具使用这种方式如React的(Next.js)、Vue的(Nuxt.js)等。

有些工具将 webpack 运行在服务端生产环境,实时编译,将编译结果缓存起来,这都还是传统的方式,只不过将 webpack 运行在服务端实时编译,还是开发环境编译预编译好的问题。

我选择了将 webpack 放在开发环境,只做开发打包的功能,打包 客户端 bundle ,

服务端 bundle,资源映射文件 assets.json,CSS 等资源进行部署。

服务器 bundle 用于服务器端渲染(SSR)

客户端 bundle 给浏览器加载,浏览器通过 bundle 加载更多其它模块(chunk)js

资源映射文件 assets.json 则是,服务器 bundle 在准备所需 HTML,需要预插入那些模块(chunk)js,和CSS,这只是提高用户体验。

具体使用方法,可以看我最近造的个轮子 kkt-ssr,这个轮子将工具的部分封装起来,你只需要写业务代码,和少量的服务端渲染代码即可,还附赠十几个示例,加上一个相对比较完善的示例react-router+rematch,类似于 next.js,但是有相当大的区别。

第二种方式

这是一种创新的方法,前端单页面应用,以前怎么玩儿,现在还怎么玩儿,多的一步是,你得先访问一个Rendora的服务,在前面拦截是否需要服务端渲染。下图为官方图:

这种方式原本只是个想法,想法是前端不用管服务端渲染的事儿了,不就是解决SEO?,这些爬虫过来的时候,可以通过头信息判断,写个服务,然后将需要的内容给爬虫就可以了,昨天恰巧在GitHub的趋势榜上,恰巧看到 Rendora 个工具,也就那么巧,刚好思路一致,这个工具主要为网络爬虫提供零配置服务器端渲染,以便毫不费力地改进在现代Javascript框架(如React.js,Vue.js,Angular.js等)中开发的网站的SEO问题。

这种方式非常好,之前写好的项目一句不用改,只需新起 Rendora 服务。对于来自前端服务器或外部的每个请求(百度谷歌爬虫),Rendora会根据配置文件,根据头,路径来检测或过滤,以确定 Rendora 是否应该只传递从后端服务器返回的初始HTML或使用Chrome提供的无头服务器端呈现的HTML。更具体地说,对于每个请求,有2条路径:

请求被列入白名单作为SSR的候选者(即过滤后的Get请求),Rendora 会指示无头Chrome实例请求相应的页面,呈现它,并返回包含最终服务器端的响应呈现出HTML。通常只需要将百度、谷歌、必应爬虫等网络抓取工具列入白名单即可。

未列入白名单(即请求不是GET请求或未通过任何过滤器),Rendora将只是充当反向HTTP代理,只是按原样传送请求和响应。

Rendora可以看作是位于后端服务器(例如Node.js / Express.js,Python / Django等等)之间的反向HTTP代理服务器,也可能是你的前端代理服务器(例如nginx,traefik,apache等),

Rendora 是我见过的接近于完美的动态渲染器,提供零配置服务器端渲染

我们到底选择哪一种服务端渲染呢?

Rendora,新的方式非常厉害,有很多优势:

方便迁移老的项目,前端和后端代码不需要更改。

可能更快的性能,资源(CPU)消耗可能更少,Golang编写的二进制文件

多种缓存策略

已经拥有 docker 容器方案

此工具,服务端渲染的页面需要缓存,缓存引发的小问题就是

通过缓存解决,性能问题和调用API两次的问题,服务端渲染,客户端展示渲染,平常调用一次API,现在调用了两次。

被缓存的页面,不能及时清理,比如网站发现用户发了不良信息,需要清理,就需要清理缓存页面了。如果想提高用户体验,浏览器端一些页面需要服务端渲染,这个时候服务端需要请求API,就会有权限问题,或者直接从缓存里面读取的HTML,到浏览器客户端,可能会有服务端和浏览器端渲染不一致的错误。

如果上面两种方式不在你的考虑范畴之内,那Rendora将是你完美的服务端渲染解决方案

总结

感觉我的轮子kkt-ssr好像白写了一样,经过分析发现目前还有一点作用吧,至少解决了不多调用一次API,和API调用权限问题导致渲染不一致的问题。但是我更推荐Rendora的方式,这将是未来。

补充:

同构方案

这里我们采用React技术体系做同构,由于React本身的设计特点,它是以Virtual DOM的形式保存在内存中,这是服务端渲染的前提。

对于客户端,通过调用ReactDOM.render方法把Virtual DOM转换成真实DOM最后渲染到界面。

import { render } from 'react-dom'

import App from './App'

render(, document.getElementById('root'))

对于服务端,通过调用ReactDOMServer.renderToString方法把Virtual DOM转换成HTML字符串返回给客户端,从而达到服务端渲染的目的。

import { renderToString } from 'react-dom/server'

import App from './App'

async function(ctx) {

await ctx.render('index', {

root: renderToString()

})

}

状态管理方案

我们选择Redux来管理React组件的非私有组件状态,并配合社区中强大的中间件Devtools、Thunk、Promise等等来扩充应用。当进行服务端渲染时,创建store实例后,还必须把初始状态回传给客户端,客户端拿到初始状态后把它作为预加载状态来创建store实例,否则,客户端上生成的markup与服务端生成的markup不匹配,客户端将不得不再次加载数据,造成没必要的性能消耗。

服务端

import { renderToString } from 'react-dom/server'

import { Provider } from 'react-redux'

import { createStore } from 'redux'

import App from './App'

import rootReducer from './reducers'

const store = createStore(rootReducer)

async function(ctx) {

await ctx.render('index', {

root: renderToString(

),

state: store.getState()

})

}

HTML

window.REDUX_STATE =

客户端

import { render } from 'react-dom'

import { Provider } from 'react-redux'

import { createStore } from 'redux'

import App from './App'

import rootReducer from './reducers'

const store = createStore(rootReducer, window.REDUX_STATE)

render(

,

document.getElementById('root')

)

路由方案

客户端路由的好处就不必多说了,客户端可以不依赖服务端,根据hash方式或者调用history API,不同的URL渲染不同的视图,实现无缝的页面切换,用户体验极佳。但服务端渲染不同的地方在于,在渲染之前,必须根据URL正确找到相匹配的组件返回给客户端。

React Router为服务端渲染提供了两个API:

- match 在渲染之前根据URL匹配路由组件

- RoutingContext 以同步的方式渲染路由组件

服务端

import { renderToString } from 'react-dom/server'

import { Provider } from 'react-redux'

import { createStore } from 'redux'

import { match, RouterContext } from 'react-router'

import rootReducer from './reducers'

import routes from './routes'

const store = createStore(rootReducer)

async function clientRoute(ctx, next) {

let _renderProps

match({routes, location: ctx.url}, (error, redirectLocation, renderProps) => {

_renderProps = renderProps

})

if (_renderProps) {

await ctx.render('index', {

root: renderToString(

),

state: store.getState()

})

} else {

await next()

}

}

客户端

import { Route, IndexRoute } from 'react-router'

import Common from './Common'

import Home from './Home'

import Explore from './Explore'

import About from './About'

const routes = (

)

export default routes

静态资源处理方案

在客户端中,我们使用了大量的ES6/7语法,jsx语法,css资源,图片资源,最终通过webpack配合各种loader打包成一个文件最后运行在浏览器环境中。但是在服务端,不支持import、jsx这种语法,并且无法识别对css、image资源后缀的模块引用,那么要怎么处理这些静态资源呢?我们需要借助相关的工具、插件来使得Node.js解析器能够加载并执行这类代码,下面分别为开发环境和产品环境配置两套不同的解决方案。

开发环境

首先引入babel-polyfill这个库来提供regenerator运行时和core-js来模拟全功能ES6环境。

引入babel-register,这是一个require钩子,会自动对require命令所加载的js文件进行实时转码,需要注意的是,这个库只适用于开发环境。

引入css-modules-require-hook,同样是钩子,只针对样式文件,由于我们采用的是CSS Modules方案,并且使用SASS来书写代码,所以需要node-sass这个前置编译器来识别扩展名为.scss的文件,当然你也可以采用LESS的方式,通过这个钩子,自动提取className哈希字符注入到服务端的React组件中。

引入asset-require-hook,来识别图片资源,对小于8K的图片转换成base64字符串,大于8k的图片转换成路径引用。

// Provide custom regenerator runtime and core-js

require('babel-polyfill')

// Javascript required hook

require('babel-register')({presets: ['es2015', 'react', 'stage-0']})

// Css required hook

require('css-modules-require-hook')({

extensions: ['.scss'],

preprocessCss: (data, filename) =>

require('node-sass').renderSync({

data,

file: filename

}).css,

camelCase: true,

generateScopedName: '[name]__[local]__[hash:base64:8]'

})

// Image required hook

require('asset-require-hook')({

extensions: ['jpg', 'png', 'gif', 'webp'],

limit: 8000

})

产品环境

对于产品环境,我们的做法是使用webpack分别对客户端和服务端代码进行打包。客户端代码打包这里不多说,对于服务端代码,需要指定运行环境为node,并且提供polyfill,设置__filename和__dirname为true,由于是采用CSS Modules,服务端只需获取className,而无需加载样式代码,所以要使用css-loader/locals替代css-loader加载样式文件

// webpack.config.js

{

target: 'node',

node: {

__filename: true,

__dirname: true

},

module: {

loaders: [{

test: /\.js$/,

exclude: /node_modules/,

loader: 'babel',

query: {presets: ['es2015', 'react', 'stage-0']}

}, {

test: /\.scss$/,

loaders: [

'css/locals?modules&camelCase&importLoaders=1&localIdentName=[hash:base64:8]',

'sass'

]

}, {

test: /\.(jpg|png|gif|webp)$/,

loader: 'url?limit=8000'

}]

}

}

动态加载方案

对于大型Web应用程序来说,将所有代码打包成一个文件不是一种优雅的做法,特别是对于单页面应用,用户有时候并不想得到其余路由模块的内容,加载全部模块内容,不仅增加用户等待时间,而且会增加服务器负荷。Webpack提供一个功能可以拆分模块,每一个模块称为chunk,这个功能叫做Code Splitting。你可以在你的代码库中定义分割点,调用require.ensure,实现按需加载,而对于服务端渲染,require.ensure是不存在的,因此需要判断运行环境,提供钩子函数。

重构后的路由模块为

// Hook for server

if (typeof require.ensure !== 'function') {

require.ensure = function(dependencies, callback) {

callback(require)

}

}

const routes = {

childRoutes: [{

path: '/',

component: require('./common/containers/Root').default,

indexRoute: {

getComponent(nextState, callback) {

require.ensure([], require => {

callback(null, require('./home/containers/App').default)

}, 'home')

}

},

childRoutes: [{

path: 'explore',

getComponent(nextState, callback) {

require.ensure([], require => {

callback(null, require('./explore/containers/App').default)

}, 'explore')

}

}, {

path: 'about',

getComponent(nextState, callback) {

require.ensure([], require => {

callback(null, require('./about/containers/App').default)

}, 'about')

}

}]

}]

}

export default routes

优化方案

vendor: ['react', 'react-dom', 'redux', 'react-redux']

所有js模块以chunkhash方式命名

output: {

filename: '[name].[chunkhash:8].js',

chunkFilename: 'chunk.[name].[chunkhash:8].js',

}

提取公共模块,manifest文件起过渡作用

new webpack.optimize.CommonsChunkPlugin({

names: ['vendor', 'manifest'],

filename: '[name].[chunkhash:8].js'

})

提取css文件,以contenthash方式命名

new ExtractTextPlugin('[name].[contenthash:8].css')

模块排序、去重、压缩

new webpack.optimize.OccurrenceOrderPlugin(), // webpack2 已移除

new webpack.optimize.DedupePlugin(), // webpack2 已移除

new webpack.optimize.UglifyJsPlugin({

compress: {warnings: false},

comments: false

})

使用babel-plugin-transform-runtime取代babel-polyfill,可节省大量文件体积

需要注意的是,你不能使用最新的内置实例方法,例如数组的includes方法

{

presets: ['es2015', 'react', 'stage-0'],

plugins: ['transform-runtime']

}

最终打包结果

部署方案

pm2 start ./server.js -i 0

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

python 服务端渲染_详解React 服务端渲染方案完美的解决方案相关推荐

  1. java语音播报源代码_详解Android 语音播报实现方案(无SDK)

    本文介绍了详解Android 语音播报实现方案(无SDK),分享给大家,具体如下: 功能描述 类似支付宝收款时候的语音播报功能:当别人扫描你的收款码,你收到钱之后,就会听到"支付宝到账12. ...

  2. python变量定义大全_详解python变量与数据类型

    这篇文章我们学习 Python 变量与数据类型 变量 变量来源于数学,是计算机语言中能储存计算结果或能表示值抽象概念,变量可以通过变量名访问.在 Python 中 变量命名规定,必须是大小写英文,数字 ...

  3. python编写数据库连接工具_详解使用Python写一个向数据库填充数据的小工具(推荐)...

    一. 背景 公司又要做一个新项目,是一个合作型项目,我们公司出web展示服务,合作伙伴线下提供展示数据. 而且本次项目是数据统计展示为主要功能,并没有研发对应的数据接入接口,所有展示数据源均来自数据库 ...

  4. python提取邮件附件_详解python实现读取邮件数据并下载附件的实例

    详解python实现读取邮件数据并下载附件的实例 实现结果图: 实现代码: #!/usr/bin/python2.7 # _*_ coding: utf-8 _*_ """ ...

  5. python英语字典程序修改_详解如何修改python中字典的键和值

    我们知道python中字典是无序的,它们都是通过hash去对应的.一般的如果我们需要修改字典的值,只需要直接覆盖即可,而修改字典的键,则需要使用字典自带的pop函数,示例如下: t = {} t['a ...

  6. python停止线程池_详解python中Threadpool线程池任务终止示例代码

    需求 加入我们需要处理一串个位数(0~9),奇数时需要循环打印它:偶数则等待对应时长并完成所有任务:0则是错误,但不需要终止任务,可以自定义一些处理. 关键点 定义func函数处理需求 callbac ...

  7. python模块搜索原则_详解python模块路径查找规则及定义

    在python中创建一个模块非常简单,只需要在当前目录下创建一个 .py文件即可,系统自动将其解析成模块,文件名就是模块名.很多我们源码时代的同学都认为在查找模块时优先使用当前目录下的自定义模块.其实 ...

  8. python中index函数_详解python中的index函数用法

    1.函数的创建 def fun(): #定义 print('hellow') #函数的执行代码 retrun 1 #返回值 fun() #执行函数 2.函数的参数 普通参数 :要按照顺序输入参数 de ...

  9. python获取屏幕文字_详解:四种方法教你对Python获取屏幕截图(PyQt , pyautogui)...

    前言: 今天为大家带来的内容是详解:四种方法教你对Python获取屏幕截图(PyQt , pyautogui)本文具有不错的参考意义,希望能够帮助到大家! Python获取电脑截图有多种方式,具体如下 ...

  10. python中 的用法_详解python中@的用法

    python中@的用法 @是一个装饰器,针对函数,起调用传参的作用. 有修饰和被修饰的区别,@function作为一个装饰器,用来修饰紧跟着的函数(可以是另一个装饰器,也可以是函数定义). 代码1 结 ...

最新文章

  1. matlab生成ai图片,MATLAB绘图:导出矢量图
  2. 第四节:IO、序列化和反序列化、加密解密技术
  3. 为什么 wait/notify/notifyAll 在 Object 类定义而不是 Thread 类?
  4. ADO连接ACCESS数据库
  5. 通过一条语句的执行,深入理解innoDB的底层架构
  6. 声乐学习----关于发声的个人解读
  7. class反编译成java_Java黑科技之源:JVMTI完全解读
  8. android通用对话框,android-所有活动中的“通用”对话框
  9. APMServ5.2.6win10系统Apache、MySQL5.1启动失败解决办法
  10. 华成英模拟电子技术笔记(1)-绪
  11. 网上购物系统问题陈述、词汇表与领域类图
  12. java 读取写入excel_java读取和写入Excel文件
  13. Java面试相关问题以及解析
  14. 被“现实”打败的3D打印
  15. 交通·未来第2期:深度交通感知: 从区域流量预测到在线OD预测
  16. 雷蛇 笔记本 装linux,顺应民意:雷蛇考虑开发Linux版Blade游戏本
  17. Nginx教程(一) Nginx入门教程
  18. linux python pyenv,在Centos中安装并使用Pyenv,一个强大的Python版本管理工具
  19. 项目施工阶段的BIM主流应用,主要集中在哪些环节?
  20. Redis实现世界杯排行榜功能(实战)

热门文章

  1. (30)auth模块(django自带的用户认证模块)
  2. zabbix再爆高危SQL注入漏洞,可获系统权限
  3. 10.11 cocoapods安装
  4. 【C#】 用Route进行URL重写
  5. DevExpress学习之Gridcontrol
  6. linux双显卡配置_Kali Linux 2.0 安装 NVIDIA显卡驱动实现双显卡(联想笔记本)
  7. 709. To Lower Case
  8. 使用docker-compose 大杀器来部署服务 上(转)
  9. Android 项目规范
  10. Etag If-None-Match 专题