前言
服务端渲染(SSR)可以说是大型网站的标配,也是每一个前端开发人员想要进阶的必会技能,掌握SSR原理,能让你的技术体系更完整,更是你前端晋升之路的必要一环。 在企业中使用原生Node开发SSR应用的方式并不多,更多采用同类技术栈的SSR框架,比如Angular Universal、Next.js、Remix.js,本文将介绍对标Next.js、采用Vue3技术栈的框架Nuxt3。
SSR
首先扩展下页面渲染的几种模式,几种?
广义上划分的确就是我们常说的2种:客户端渲染与服务端渲染。
在这两种模式下又可以继续划分很多种:
- SSR (Server Side Rendering)
- SSG (Static Site Generation)
- SSR With Hydration
- CSR with Pre-rendering
- CSR (Client Side Rendering)
- Trisomorphic Rendering
今天我们所讲的SSR为上述的第一种:Server Side Rendering
原理
对于 SSR 来说,数据请求的操作放在了服务端做。当请求到数据后,转化为组件所需的状态,然后把 状态 + 模版转化为 HTML 返回给浏览器端。浏览器拿到 HTML 后会先渲染出 DOM 结构,然后请求并执行 js 文件,此时我们组件的代码也会在客户端被执行一次,我们会再次去初始化路由、状态。在服务端我们已经请求过数据并处理一次组件状态了,如果不做任何操作,当客户端初始化状态时就拿不到服务端处理过的状态,客户端就会把初始化的状态覆盖给组件用,导致页面会再渲染成丢失了状态的组件。为了避免这一情况,就出现了数据注水和脱水的方案。
同构
同构这个概念起源于mvvm模型的前端框架中,本质上是客户端渲染和服务器端渲染的一个整合。我们把页面的展示与交互代码写在一起,让代码在服务器端与客户端各执行一次,服务器端执行实现组件渲染,客户端执行实现页面交互,所以前端同构应用一般也是指SSR。
喝水-render
该过程是在服务端完成的,服务端在请求完数据后对模板进行了数据填充,根据外部数据构建出初始组件树,过程中仅执行render及之前的几个生命周期,这一render过程即为喝水,以确保能够成功进入脱水阶段。
脱水-dehydrate
该过程仍然是在服务端完成,render之后内存里的组件树被序列化成了静态的 HTML 片段,但是失去了交互功能了,这种便携的HTML形态更适合网络传输,以保证高效的送达客户端。
注水-hydrate
注水是在客户端完成的,当已被填充了数据的HTML到了客户端后重新被唤起,即让原本脱水了的state、prop等数据恢复到原来的生机,并且重新render组件。对此可以最简单的理解为把原本只有样子的 HTML 恢复其功能。
优点
支持SEO
可能不少人认为 title 标签和 meta description 标签在 SEO 中起着关键作用,其实并不是。现在搜索引擎爬虫大多是读整体 HTML 的内容去分析的,分析内容涵盖了一个网站主要 3 个部分的内容:文本、多媒体(主要是图片)和外部链接,通过这些来判断网站的类型和主题。 那么 title 和 meta description 的真正作用是什么呢? 其实是用户的转化率,比如,同样高排名的网站,title 和 meta description 做得更好的那个,在搜索页的内容就更吸引人,那自然点进网页浏览的用户就更多。
内容呈现快
弊端
服务器压力变大
一是服务端多了渲染这项工作,另外就是多人访问时服务端有多任务并行的情况。
兼容范围变小
服务端是没有window与document对象的,所以与使用了这两个对象的库是没法兼容的。
Nuxt3
Nuxt3是SSR的方案,其自动导入功能结合TypeScript的支持极大程度提高了开发的便捷性,高度集成Vite、Vue Router等优秀库.
项目架构
Nuxt3 是一个约定优先的 web 开发框架,部分功能是基于目录结构的,框架在启动时会自动扫描api、routes、pages等目录文件并自动进行系统注册,包括自动导入 Vue 组件、Composables、Layouts等目录,默认目录的约定俗称相信很多接触过Nuxt的同学都深有体会。
引入UI
Nuxt3项目同样支持UI框架,比如ant-design-vue,以及按需引入。
// plugins/antd.client.ts
import 'ant-design-vue/dist/antd.css'
import { Button } from 'ant-design-vue'
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.use(ConfigProvider).use(Button);
})
export default defineNuxtConfig({
css: ['ant-design-vue/dist/antd.css'],
plugins: ['@/plugins/antd'],
// ……
});
状态管理
$ npm install --save pinia @pinia/nuxt pinia-plugin-persist
// 错误1:如果你的 npm 版本大于 6 则需要 添加 --legacy-peer-deps
$ npm install --save pinia @pinia/nuxt pinia-plugin-persist --legacy-peer-deps
export default defineNuxtConfig({
modules: [
'@pinia/nuxt'
],
});
import { defineNuxtPlugin } from "#app";
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.$pinia.use(piniaPluginPersistedstate);
});
国际化
这里我们采用vue-i18n实现
import { useI18n } from 'vue-i18n';
export const availableLocales: ILocales = {
en: {
name: 'English',
iso: 'en',
flag: '🇺🇸',
},
['zh-CN']: {
name: '中文',
iso: 'zh-CN',
flag: 'cn',
},
}
export function LanguageManager() {
const { locale } = useI18n()
const localeUserSetting = useCookie('locale')
const getSystemLocale = (): string => {
try {
const foundLang = window ? window.navigator.language : 'en'
return availableLocales[foundLang] ? foundLang : 'en'
} catch (error) {
return 'en'
}
}
const getUserLocale = (): string => localeUserSetting.value || getSystemLocale();
const localeSetting = useState<string>('locale.setting', getUserLocale);
watch(localeSetting, (localeSetting: string) => {
localeUserSetting.value = localeSetting
locale.value = localeSetting
})
const init = () => localeSetting.value = getUserLocale();
locale.value = localeSetting.value;
onBeforeMount(() => init());
return { localeSetting, init }
}
<h1>{{ $t('dev.page.message') }}</h1>
// zh:你好 Nuxt3
// en:Hello Nuxt3
响应式图
推荐插件@awesome-image/image
import '@awesome-image/image/dist/style.css'
import { AsImage } from '@awesome-image/image'
import { imageUrlGeneratorFP } from '@awesome-image/services'
import type { NuxtApp } from '#app'
import { defineNuxtPlugin } from '#app'
export default defineNuxtPlugin((nuxt: NuxtApp) => {
nuxt.vueApp.use(AsImage, {
imageUrlGenerator: imageUrlGeneratorFP,
})
})
<AsImage
class="cusimg"
:src="'/imgs/test.png'"
:lazy="true"
:progressive="false"
:responsive="false">
<template #loading>
<div class="placeholder"></div>
</template>
</AsImage>
支持Markdown文件
主要使用 nuxt 团队打造的 @nuxt/content,它可以便捷的搭建一个内容管理系统。
// 安装
$ yarn add -D @nuxt/content
// 注册
export default defineNuxtConfig({
modules: ['@nuxt/content'],
})
---
title: 'index title'
description: 'index description'
date: '2022-08-04'
---
# Hello Content
<template>
<div>
<ContentDoc>
<template #default="{ doc }">
<h1>{{ doc.title }}</h1>
<ContentRenderer :value="doc" />
</template>
<template #empty>
<h1>Post in empty</h1>
</template>
<template #not-found>
<h1>404</h1>
</template>
</ContentDoc>
</div>
</template>
主题切换
方式:使用css的var属性 主题根元素定义颜色变量
:root {
--main_color: #03a9f4;
}
:root[theme=dark] {
--main_color: #111111;
}
html[theme=dark] {
body{
background-color: var(--main_color);;
}
}
<template>
<a-config-provider>
<Html :theme="theme ? 'light' : 'dark'">
<Body>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</Body>
</Html>
</a-config-provider>
</template>
<script lang="ts" setup>
const theme = useState<boolean>('theme.setting');
</script>
实际应用中我们通常都会将不同主题定义到不同的文件中:theme.dark.scss、theme.light.scss、theme.gray.cscc等,这样每个主题问题可以交由不同的设计师负责,在项目运行中动态加载对应主题文件。
Svg图片支持
涉及插件:vite-plugin-svg-icons
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
export default defineNuxtConfig({
vite: {
logLevel: 'info',
plugins: [
createSvgIconsPlugin({
iconDirs: [path.resolve(process.cwd(), 'src/assets/svgs/')],
symbolId: 'icon-[dir]-[name]',
}),
]
}
});
通常我们封装到一个公共组件里SvgIcon.vue
<template>
<svg aria-hidden="true">
<use :xlink:href="symbolId" :fill="color" />
</svg>
</template>
<script>
import { computed, defineComponent } from 'vue'
export default defineComponent({
name: 'SvgIcon',
props: {
prefix: {
type: String,
default: 'icon',
},
name: {
type: String,
required: true,
},
color: {
type: String,
default: '#333',
},
},
setup(props) {
const symbolId = computed(() => `#${props.prefix}-${props.name}`)
return { symbolId }
},
})
</script>
使用时直接在页面传入不同name即可: // 展示assets/svgs/apollo.svg文件
打包与部署
关于Nuxt3打包
| 标题 | 内容 | 描述 |
|---|---|---|
| npm run build | 使用“混合渲染模式”创建.output目录 可通过node、pm2部署 打包成node应用程序 | 生成 .output |
| npm run gererate | 使用SPA方式预渲染模式创建.dist目录 可部署在任何静态服务器上 打包成SPA预渲染客户端程序 | 生成 .dist |
官方文档上给出了四种不同的部署方式:Azure、PM2、Netlify、Vercel。 PM2部署方式:nginx 反向代理到3000端口,用pm2管理前端项目
node版本:v14.7.0
1、安装nvm:
https://github.com/coreybutler/nvm-windows/releases
2、安装需要的node版本
nvm install v14.7.0
3、查看已安装版本号
nvm list
4、全局安装pm2进程管理工具
npm install -g pm2
5、进入项目根目录
安装依赖:npm install
6. 启动
pm2 start npm -- run serve