目录
单页面应用(优缺点)(Single Page Application)
优点:
SPA的缺点:
服务端渲染(Server Side Rendering)
SSR示例(一个ssr小引擎)
SSR优缺点分析
总结
单页面应用(优缺点)(Single Page Application)
优点:
- 体验:流畅性
- 架构:组件化
- 生态:BaaS/FaaS 调用第三方服务
- 性能:读写速度,cpu占用率,节省I/O
- 安全:容错(数据没到位,加载不出来)
SPA的缺点:
- CPU开销大(组件vs时间)(js加载页面,浏览器cpu开销大)
- 流量开销大(一次加载多张页面)
- 需要好的系统架构
- 搜索引擎不友好
服务端渲染(Server Side Rendering)
客户端dom直接就有对应事件
服务端需要重建对应事件
SSR示例(一个ssr小引擎)
- 开发一个生成html节点并且管理事件的函数
- render(meta, renderAs = dom | html(字符串) | rehydrate(事件水合))
- 实现render函数的服务端渲染和客户端渲染能力
水合(hydrateRoot)
水合是指其他物质溶于水发生化学反应
此处水合是指后端数据达到前端后,js绑定事件,才能够响应用户的操作或者DOM的更新。
工作流程(将事件比作水)
组件在服务器拉取数据,并在服务端首次渲染。
脱水,对组件进行脱水,变成HTML字符串,脱去交互事件,成为风干标本快照。
注水,发送到客户端后,重新注入数据(交互的事件),重新变成可交互组件。
此过程类似于银耳,长成后晒干,然后加入水再泡发。
// {
// name: "div",
// props : {
// onClick : () => {},
// style : {
// backgroundColor : 'blue'
// }
// },
// children : [
// {
// name : 'div',
// ...
// }
// ],
// children : "hello"
// }
function render(node, renderAs = 'dom', path = []) {
const { name, props, children } = node
if (renderAs === 'dom') { // 客户端渲染
const element = document.createElement(name)
if (props && props.onClick) {
element.addEventListener('click', props.onClick)
}
if (typeof children === 'string') {
element.innerHTML = children
} else if (Array.isArray(children)) {
children.forEach((child, i) => {
element.appendChild(
render(child, renderAs, path.concat(i)) // 第三个参数,递归函数节点路径
)
})
}
return element
} else if (renderAs === 'html') { // 服务端渲染
let childrenStr = ''
if (typeof children === 'string') {
childrenStr = children
} else {
childrenStr = children
.map((child, i) => {
return render(child, renderAs, path.concat(i))
})
.join("") // 衔接字符串
}
return `<${name} id='node-${path.join('-')}'>${childrenStr}</${name}>`
} else if (renderAs === 'rehydrate') { // 服务端渲染之后,客户端做水合
if (props && props.onClick) {
document.getElementById(`node-${path.join('-')}`) // 获取对应节点
.addEventListener('click', props.onClick) // 水合绑定事件
}
if (Array.isArray(children)) { // 数组
children.forEach((child, i) => {
render(child, renderAs, path.concat(i)) // 递归
})
}
} else {
throw "not supported renderAs type"
}
}
module.exports = render
// 主程序
const app = require('express')()
const render = require('./lib/render')
const path = require('path')
// 要渲染的html
const html = render({
name : 'div',
props : {
onClick : () => {
window.alert('123')
},
},
children : [{
name : 'ul',
children : [
{
name : 'li',
children : 'Apple'
},
{
name : 'li',
children : 'Alibaba'
},
]
}]
}, 'html') // 服务端渲染的html字符串
app.get('/page-ssr.js', (req, res) => {
res.sendFile(path.resolve(__dirname, 'lib/page-ssr.js'))
})
app.get('/page.js', (req, res) => {
res.sendFile(path.resolve(__dirname, 'lib/page.js'))
})
app.get('/', (req, res) => {
res.send(`<html>
<body>
<div id="root">
</div>
<script src='/page.js'></script>
</body>`)
})
app.get('/ssr', (req, res) => {
res.send(`<html>
<body>
<div id="root">
${html}
</div>
<script src='/page-ssr.js'></script>
</body>`)
})
app.listen(3000)
// page-ssr.js // 服务端 水合绑定事件
function render(node, renderAs = 'dom', path =[] ){
const {name, props, style, children} = node
if(renderAs === 'dom') {
const element = document.createElement(name)
if(props.onClick) {
element.addEventListener('click', props.onClick)
}
if(style) {
Object.keys(style).forEach(key => {
element.style[key] = style[key]
})
}
if(typeof children === 'string') {
element.innerHTML = children
} else if(Array.isArray(children)){
children.forEach( (child, i) => {
element.appendChild(
render(child, renderAs, path.concat(i))
)
})
} else {
throw "invalid children"
}
return element
} else if(renderAs === 'html') {
let styles = []
if(style) {
styles = Object.keys(style)
.map(key => {
const k = key.replace(/([A-Z])/, m => '-' + m.toLowerCase())
const val = style[key]
return `${k}=${val}`
})
}
const styleString = styles.join(';')
let childrenStr = ''
if(typeof children === 'string') {
childrenStr = children
} else {
childrenStr = children
.map((child, i) =>
render(child, renderAs, path.concat(i) )
)
.join("")
}
return `<${name} id='node-${path.join('-')}' style='${styleString}'>${childrenStr}</${name}>`
} else if(renderAs === 'rehydrate') {
if(props && props.onClick){
document.getElementById('node-' + path.join('-'))
.addEventListener('click', props.onClick)
}
if(Array.isArray(children)) {
children.forEach( (child, i) => {
render(child, renderAs, path.concat(i))
})
}
}
}
render({
name : 'div',
props : {
onClick : () => {
window.alert('123')
},
},
children : [{
name : 'ul',
children : [
{
name : 'li',
children : 'Apple'
},
{
name : 'li',
children : 'Alibaba'
},
]
}]
}, 'rehydrate')
// page.js render 出一个dom,客户端 ,前端渲染
function render(node, renderAs = 'dom', path =[] ){
const {name, props, style, children} = node
if(renderAs === 'dom') {
const element = document.createElement(name)
if(props && props.onClick) {
element.addEventListener('click', props.onClick)
}
if(style) {
Object.keys(style).forEach(key => {
element.style[key] = style[key]
})
}
if(typeof children === 'string') {
element.innerHTML = children
} else if(Array.isArray(children)){
children.forEach( (child, i) => {
element.appendChild(
render(child, renderAs, path.concat(i))
)
})
} else {
throw "invalid children"
}
return element
} else if(renderAs === 'html') {
let styles = []
if(style) {
styles = Object.keys(style)
.map(key => {
const k = key.replace(/([A-Z])/, m => '-' + m.toLowerCase())
const val = style[key]
return `${k}=${val}`
})
}
const styleString = styles.join(';')
let childrenStr = ''
if(typeof children === 'string') {
childrenStr = children
} else {
childrenStr = children
.map((child, i) =>
render(child, renderAs, path.concat(i) )
)
.join("")
}
return `<${name} id='node-${path.join('-')}' style='${styleString}'>${childrenStr}</${name}>`
} else if(renderAs === 'rehydrate') {
if(props && props.onClick){
document.getElementById('node-' + path.join('-'))
.addEventListener('click', props.onClick)
}
if(Array.isArray(children)) {
children.forEach( (child, i) => {
render(child, renderAs, path.concat(i))
})
}
}
}
const element = render({
name : 'div',
props : {
onClick : () => {
window.alert('123')
},
},
children : [{
name : 'ul',
children : [
{
name : 'li',
children : 'Apple'
},
{
name : 'li',
children : 'Alibaba'
},
]
}]
}, 'dom')
document.getElementById('root').appendChild(element)
SSR优缺点分析
优点
- 显著减小TTI(Time To interactive)减小浏览器执行时间;加重服务端开销,减轻客户端开销,减少js执行时间,让客户可以更快的用上
- 单页面应用SEO解决方案
- 内网数据组装
缺点
- 服务器CPU压力大
- CDN不好支持
- 开发工作量大(三套代码,水合,服务端,客户端)
- 全局对象失效
总结
- 思考:少量页面开发用单页面还是多页面?看开发人员水平,水平够了,写一个好的系统架构,就推荐用单页面应用,因为用户体验好,多个页面用异步加载,
- 思考:如果SSR目的仅仅是SEO,该如何做?用户访问量高的话,任何一点加速,都是很大的提升
服务端做分流,发现是百度的爬虫就返回SSR后的页面