前言
最近,我收到了一个要求重组公司活动页面项目的请求。 实现:
SEO MPA启动速度好,构建速度快后端工程浏览器至少兼容IE11
基于这个需求,我选择了vite+react+vite-plugin-ssr
文章后面是ssr的介绍,老手请跳过,直接看最后
SSR 入门 SSR 术语是什么
ssr,全称server side render,服务端渲染
csr,全称client side render,客户端渲染
spa,全称单页应用程序,单页应用程序
mpa,全称多页面应用程序,多页面应用程序
苏维埃社会主义共和国的历史
我的学习习惯是,无论学什么,先了解它的历史背景。存在就是合理的。 知道一项技术为什么形成会让我更容易理解这项技术
最初的网页渲染,前端三剑客:html+css+js,放在服务器上,静态部署可供用户访问。
后来随着网页复杂度的增加,出现了jsp/ejs等一系列模板句型。 服务器获取到数据后,将数据渲染到模板中,最后生成html返回给客户端。 这就是最原始的ssr。
随着后端框架(ng/react/vue)的诞生,越来越多的朋友开始使用框架来开发Web。 利用后端工程等新技术,强化后端工程。 而这些完全前馈的方式也带来了一些问题,比如对SEO非常不友好
企业社会责任的缺点
我们打开一个SPA网页(使用脚手架默认形式构建),右键查看网页源代码
第一个问题:SEO极其不友好。 页面上根本没有任何内容。 爬虫最喜欢这些网页,看一眼就离开。
SPA的工作方式是使用js动态渲染html,所有压力都交给客户端(浏览器)。 正因为如此,第二个问题也出现了:首屏加载速度慢
为什么又出现了ssr的需求
为了更好的SEO,为了更快的加载速度(服务器生成首页静态页面,客户端可以直接显示,然后用JS动态渲染)
前端开发使用react/vue,能熟练开发网页。 cra/vue-cli脚手架创建的默认模板是SPA。
那么我们应该如何实现“需要,而且”(我想要所有前端框架/seo)
如何实现基本的ssr
根据里面的问题,我们希望达到:
由于需要服务端渲染,而服务端是用来执行vue/react js框架的,所以第一反应就是使用nodejs来进行服务端渲染,因为nodejs自然是执行js代码
对于客户端,使用vue(react也可以,不过最近熟悉vue3),对于vue3来说,体积比react小,toC网站更好。 React18 发布了 ssr 的新 API。 开发者可以使用React.lazy和suspense来实现延迟加载,这也提供了良好的用户体验:github.com/reactwg/rea...
以下是一个基本的ssr示例
请注意以下情况:客户端使用esm规范,服务端使用cjs
如果想统一使用esm,可以使用tsx执行node脚本或者更改package.json => type: "module"
创建服务器
const express = require('express')
const app = express()
app.get('*', (req, res) => {
res.send('Hello World')
})
app.listen(4000, () => {
console.log('Server running at http://localhost:4000');
})
启动服务后,打开浏览器http:localhost:4000,可以看到内容
渲染vue
服务器有,但是是返回的字符串。 我们想使用vue来开发,尝试返回一个vue组件
Vue3提供了在服务器端渲染组件的方式,在vue/server-renderer下
const express = require('express')
const { renderToString } = require('vue/server-renderer')
const { createSSRApp } = require('vue')
const app = express()
app.get('*', (req, res) => {
const vue = createSSRApp({
data: () => ({ count: 1 }),
template: ``,
})
renderToString(vue).then((html) => {
res.send(`
Document
${html}
`)
})
})
app.listen(4000, () => {
console.log('Server running at http://localhost:4000')
})
此时打开页面,可以看到按钮,但是此时页面是静态的,因为页面已经在服务端渲染好了,但是客户端没有注入vue
右键查看网页源代码,可以看到按钮元素
客户端渲染
我们希望按钮的交互可以移动,此时客户端需要做渲染
const { createSSRApp } = require('vue')
const vue = createSSRApp({
data: () => ({ count: 1 }),
template: ``,
})
vue.mount('#app')
这段代码是不是很眼熟? 其实和服务端渲染返回的内容基本是一样的。 所以ssr的本质就是服务端渲染静态html+客户端渲染js
此外,为了在浏览器中加载客户端文件模板网站搭建缺点,我们还需要:
在server.js中添加server.use(express.static('.'))来托管客户端文件。 这里注意,在HTML shell中会添加js执行顺序来加载客户端入口文件,通过在HTML shell中添加Import Map来支持在浏览器中使用import * from 'vue'
const express = require('express')
const { renderToString } = require('vue/server-renderer')
const { createSSRApp } = require('vue')
const app = express()
app.get('/', (req, res) => {
const vue = createSSRApp({
data: () => ({ count: 1 }),
template: ``,
})
renderToString(vue).then((html) => {
res.send(`
Document
{
"imports": {
"vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
}
}
${html}
`)
})
})
app.use(express.static('.'))
app.listen(4000, () => {
console.log('Server running at http://localhost:4000')
})
此时打开本地地址,可以看到点击按钮的数字发生了变化
以上是最简单的ssr,在vue官网上可以找到。
我们甚至没有考虑后端路由、状态管理等,一个完整的ssr还需要一系列的设置。
网络路由
ssr web路由有两种形式
服务器路由 客户端路由 服务器路由
服务端路由是利用Web框架的路由能力,当匹配到某个路由时,返回相应的html代码,并加载相应的客户端代码,例如:
import express from 'express'
const router = express.Router()
router.get('/some-page', (req, res) => {
// 返回 some-page 的html
res.send(`
Document
要渲染的html字符串
`)
})
服务器端路由跳转可以直接使用a标签
客户端路由
对于客户端路由,需要使用后端框架对应的路由库,vue-router/react-router等。
可以参考官方的反例
比较两种形式
服务器端路由适合页面碎片化的项目,比如活动页面,每次路由重定向都会刷新整个页面
客户端路由适合页面间交互较强的项目,例如产品页面,跳转路由不会刷新页面
使用vite作为ssr
Vue官方推荐了几个ssr的反例,包括Nuxt/Quasar等重度框架。为了对项目进行细粒度的控制,我使用了vite+ vite-plugin-ssr方案来做
vite-插件-ssr
类似于 Next.js / Nuxt,但作为“做一件事”的 Vite 插件。
一个类似于 Next/Nuxt 但只做一件事并且做得很好的 vite 插件
这个插件的文档非常详尽,github上也有很多反例。
插件的具体功能我就不详细说了。 你可以阅读官方文档。 这里我就讲一下这个插件(v0.3x)常规路由的工作原理。 下面的vite-plugin-ssr简称为vps
vps 的传统路由
VPS建议使用文件夹名称作为路由,这也是最方便的。 活动页面中页面之间没有交互,所以我选择了默认形式。
VPS指定一系列文件命名作为开发/构建遍历的条件。 以下4个名称将被vps收集,每个文件都有其独特的功能。我们不要只用page来命名文件。***
// Vite resolves globs with micromatch: https://github.com/micromatch/micromatch
// Pattern `*([a-zA-Z0-9])` is an Extglob: https://github.com/micromatch/micromatch#extglobs
export const pageFiles = {
//@ts-ignore
'.page': import.meta.glob('/**/*.page.*([a-zA-Z0-9])'),
//@ts-ignore
'.page.client': import.meta.glob('/**/*.page.client.*([a-zA-Z0-9])'),
//@ts-ignore
'.page.server': import.meta.glob('/**/*.page.server.*([a-zA-Z0-9])'),
//@ts-ignore
'.page.route': import.meta.glob('/**/*.page.route.*([a-zA-Z0-9])'),
}
dev阶段和build阶段的项目较大后,打包速度慢怎么办?
对于活动页面,每个页面之间没有连接。 其实我希望打包是增量打包,但是如果公共文件发生变化,很难防止全打包。 所以如果能够缓存打包的文件,就可以提高打包速度。
理想是美好的模板网站搭建缺点,但现实往往是相反的。 rollup2 不支持内容哈希,但好消息是 rollup3 支持,并将于近期发布
目前我们只能通过hack的形式实现内容hash,比如使用node的crypto模块做md5hash
import { createHash } from 'crypto'
import type { PreRenderedChunk } from 'rollup'
export function getContentHash(chunk: string | Uint8Array) {
return createHash('md5').update(chunk).digest('hex').substring(0, 6)
}
export function getHash(chunkInfo: PreRenderedChunk) {
return getContentHash(
Object.values(chunkInfo.modules)
.map((m) => m.code)
.join(),
)
}
然后在汇总输出中设置文件的名称
rollupOptions: {
treeshake: 'smallest',
output: {
format: 'es',
assetFileNames: (assetInfo) => {
let extType = path.extname(assetInfo.name || '').split('.')[1]
if (/png|jpe?g|svg|gif|tiff|bmp|ico/i.test(extType!)) {
extType = 'img'
}
const hash = getContentHash(assetInfo.source)
return `assets/${extType}/[name].${hash}.[ext]`
},
chunkFileNames: (chunkInfo) => {
const server = chunkInfo.name.endsWith('server') ? 'server-' : ''
const name = chunkInfo.facadeModuleId?.match(/src/pages/(.*?)//)?.[1] || chunkInfo.name
if (chunkInfo.isDynamicEntry || chunkInfo.name === 'vendor') {
const hash = getHash(chunkInfo)
return `assets/js/${name}-${server}${hash}.chunk.js`
} else {
return `assets/js/${name}-${server}[hash].chunk.js`
}
},
entryFileNames: (chunkInfo) => {
if (chunkInfo.name === 'pageFiles') {
return '[name].js'
}
const hash = getHash(chunkInfo)
return `assets/js/entry-${hash}.js`
},
},
},
做了content-hash之后,打包率会大大提高,因为虽然有rollup,但是缓存的文件不会进行transform,而恰好transform是一个非常长期的步骤。
我尝试打包1000个文件,花了40+s,在我可以接受的范围内
快速创建页面模板
活动页面会有更多的相似之处,所以直接根据模板创建页面代码,开发效率更高(又可以钓鱼了)代码地址
做得不好的地方
记录两个ssr探索过程,我想实现,但最终没有实现
部署
对于部署,我打算使用docker来进行,下一篇文章会讲到
源地址
反应+服务端响应
vue3+ssr