场景:
现在有一个通过build指令构建的包,要在本地模拟其发布后的状态,即在本地来构建一个服务端,然后通过浏览器访问。
相关技术:
框架: react (脚手架create-react-app@5.0.1 create-next-app@14.1.4)
构建前端服务: node的http模块
搭建两个react项目
通过create-react-app和create-next-app分别搭建
create-next-app脚手架搭建的项目采用next框架,特点是约定式路由和支持SSG和SSR渲染(简单概述,不是本文重点就不展开论述了,感兴趣的朋友可以看next官方文档)
create-react-app搭建的项目就是一个常规的react项目,需要自己去实现路由。
下面简单写了一些东西,代码内容不是重点,不用关注。只是写了一个主页,一个路由页面,用于对比build后单页面路由和SSR的区别:
next
create-react-app
项目分别build
create-react-app 默认输出build文件夹
next 默认输出.next文件夹
编写node要执行的js
为了客户端在本地访问时能返回我们上面打包好的资源,在这里用node的相关模块来进行处理。
先介绍node http的用法
引入:
const http = require('http')
api调用:
http.createServer((req,res) => {}).listen(8088)
createServer方法会在本机创建一个服务端,监听端口8088,当客户端(在这个模拟场景中,本机既当客户端又当服务端)访问127.0.0.1:8088时,服务端返回的资源就是这里res.end(返回的资源xxx)方法返回的资源
这时候为了客户端访问时能返回我们上面写的首页,以及首页引用的相关资源,还需要进行一些逻辑编写,详见下方:
还要强调几点:
1,先通过url模块的url.pares方法去处理客户端请求信息req,拿到请求资源的名字
2,拿到资源名字,根据不同框架打包后的文件路径进行匹配处理生成相对路径(因为node要执行的这个js和打包资源在一个项目文件夹下,所以用相对路径就行)
3,最后通过fs模块的fs.readFile方法去读取这个相对路径的文件,在回调函数中将读取到的资源作为服务端的返回资源传递到res.end()中进行返回
4,cp.exec(start http://127.0.0.1:8088)是执行浏览器访问这个url的操作
next做法
const fs = require('fs')
const http = require('http')
const cp=require('child_process')
const url=require('url')
const routers = ['/page2']
http.createServer((req,res) => {
let pathname=url.parse(req.url).pathname //
pathname = pathname.replace('/_next', './.next')
console.log('客户端请求pathname', pathname)
let filename = ''
if (pathname.includes('./.next')) {
filename = pathname
} else {
if (pathname==='/'||pathname==='') {
filename = './.next/server/app/index.html'
} else if (routers.includes(pathname)) {
filename = `./.next/server/pages/${pathname}.html`
} else {
filename = `./.next/${pathname}`
}
}
fs.readFile(`${filename}`,(err, file) => {
if (err) {
console.log('资源读取失败', filename)
res.end('404 not found')
} else {
res.end(file)
}
})
}).listen(8088)
cp.exec("start http://127.0.0.1:8088")
create-react-app做法
const fs = require('fs')
const http = require('http')
const cp=require('child_process')
const url=require('url')
const routers = ['/', '', '/page2']
http.createServer((req,res) => {
var pathname=url.parse(req.url).pathname
console.log('客户端请求pathname', pathname)
let filename = ''
if (routers.includes(pathname)) {
filename = 'index.html'
} else {
filename = pathname
}
fs.readFile(`./build/${filename}`,(err, file) => {
if (err) {
console.log('资源读取失败')
res.end('404 not found')
} else {
res.end(file)
}
})
}).listen(8085)
cp.exec("start http://127.0.0.1:8085")
如果你有仔细看上面两段代码的话,应该发现了,当客户端请求的资源相对路径包含预设的路由时,在next框架中返回的是那个路由自己对应的html文件,而create-react-app返回的都是index.html。
因为next是支持SSR渲染的,所以根据每一个路由下的文件,都生成了对应的html。
下面分别执行这两个js文件来看看效果:
打开终端使用node执行js
node xxx/你的js的文件路径/xxx.js
运行效果
next
首页
路由页
create-react-app
首页
路由页
两个框架build后的资源都正常渲染出来了。现在根据运行效果(主要是看网络资源的返回)来展开讨论:
先看next的
首先是首页,客户端返回了首页对应的index.html。
然后在首页点击To页面2,跳转到路由页,客户端又发起了新的请求,服务端返回给了它路由/page2对应的资源page2.html(这里next打包后根据路由名生成的html,就叫page2.html)。
大伙想个问题,这时候在page2再跳回首页(无论是通过代码跳转,a标签点击跳转,还是直接修改url,都一样),客户端还会发起一次index.html的请求吗,答案是会的,因为next这里打包是支持SSR渲染的,所以它不会去对url的变化做操作(即取消url变化后,浏览器根据新的url发起资源请求的操作)。
所以从返回资源的角度去理解,所谓的SSR渲染,其实就是在服务端就生成好了对应的html资源,然后客户端访问不同路由时,服务端就会返回对应的资源。这也就解释了为什么SSR对seo友好,因为html包含的内容已经生成好了,更利于引擎抓取,以及页面已经在服务端生成渲染好了,首屏加载的速度会更快,用户更快看到页面,体验更好,在网站停留下的可能就越大(比如有些网站半天打不开,用户一生气就退了),用户在网站停留时间越久,就越有利于网站排名相关的数据,最终也会反馈到seo上,这也是为什么提升网站性能也是优化seo的原因
再看create-react-app的
首先还是首页,客户端返回了对应的index.html。但是这里仔细看,html中的内容和页面上的内容完全对不上。这是因为这里不是在服务端就完成了渲染的,而是服务端先返回了html,html里面引用了js,再在js中将比如说react组件的jsx片段渲染到页面上。
然后是首页点击跳转路由页,细心的朋友这里应该发现了,浏览器并没有发起新的资源请求,网络的文档请求里还是只有index.html的请求。为什么呢?就像刚刚说到的一样,html里的内容是js在客户端渲染上去,那么这里即使切换了路由,还是由js渲染路由页面对应的jsx片段就好了,这也是为什么这种模式叫单页面路由的原因,全程服务端都只返回了一个页面index.html,页面上的变化都是通过js来操作的。
所以单页面路由对应SSR的优势就在于,它切换路由后,并不用再去请求html资源,所以加载速度和页面性能会更好,这也是为啥SSR只强调了首屏加载更快,因为只有第一次返回html时,服务端渲染才有加载到渲染完成展示给用户的一个时间优势,在后续的页面切换中,SSR都会请求新的资源,自然就比不过了。
看到这里,对于SSR的理解应该更透彻一些了罢。
最后大伙再思考一个问题,在next中在跳转到page2时我专门强调了无论是通过代码跳转,a标签点击跳转,还是直接修改url,服务端返回的行为都一样,那么在单页面路由的场景下呢?不通过点击跳转,而是直接修改url为http://127.0.0.1:8085/page2会怎么样?
答案是 会再发起一次请求,服务端还是返回index.html,至于请求的是page2,返回的还是index.html,是因为我在服务端就是这么去定义的(即node要执行的那个js里就是这么写的,当匹配上路由page2,仍然返回资源index.html)
那么如果我没写这么一段呢?是不是就会404了。这里也就能解释很多朋友都遇到过的一个坑:在部署前端项目的时候,如果是单页面项目,在项目部署好后,访问一切正常,包括跳转路由。唯独在路由页面一刷新,就会报错404找不到资源。就是因为没有根据路由去匹配html资源,所以往往需要去nginx配置一段try_files $uri $uri/ /index.html,让路由都匹配上index.html来解决刷新页面404的问题。