12-框架-SSR

1,023 阅读6分钟

前言

      服务端渲染(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

原理

小册Flows-SSR.jpg
      对于 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