关于SSR框架调研

背景

调研一下remix这个SSR框架,顺便把市面上的vue和react的SSR框架都评估一下。

SSR解决什么问题

  • 更好的SEO
    因为SPA页面的内容是通过Ajax获取,而搜索引擎爬取工具并不会等待Ajax异步完成后再抓取页面内容,所以在SPA中是抓取不到页面通过Ajax获取到的内容的;而SSR是直接由服务端返回已经渲染好的页面(数据已经包含在页面中),所以搜索引擎爬取工具可以抓取渲染好的页面;
  • 更利于首屏渲染
    首屏的渲染是node发送过来的html字符串,并不依赖于js文件了,这就会使用户更快的看到页面的内容。尤其是针对大型单页应用,打包后文件体积比较大,普通客户端渲染加载所有所需文件时间较长,首页就会有一个很长的白屏等待时间。

概念

  • FCP: FCP (First Contentful Paint) 首次内容绘制 标记浏览器渲染来自 DOM 第一位内容的时间点,该内容可能是文本、图像、SVG 甚至 元素.
  • TTI: TTI (Time to Interactive) 可交互时间: 指标用于标记应用已进行视觉渲染并能可靠响应用户输入的时间点.

CSR客户端渲染

VP7Vx0

SSR服务端渲染

xFBvDs

服务端渲染效果

客户端渲染效果

从上面几张图片,我们可以看到:

  1. 首屏渲染CSR比SSR要慢很多
  2. SEO提供给搜索引擎的内容SSR比CSR要丰富得多
  3. 数据的获取CSR在前端通过接口可查看,而SSR在服务端不可查看

SSR框架

Vue:

  • Nuxt.js

React:

  • Next.js
  • Remix.js

Nuxt.js 对标 Next.js
2016 年 10 月 25 日,zeit.co背后的团队对外发布了Next.js,一个 React 的服务端渲染应用框架。几小时后,与 Next.js 异曲同工,一个基于Vue.js的服务端渲染应用框架应运而生,我们称之为:Nuxt.js。

我的关注点对比

Next.js(react) Nuxt.js(vue) Remix.js(react)
静态站点生成 ☑️内置 next export ☑️内置 nuxt generate 🚫不支持
请求接口 ☑️fetch ☑️axios ☑️Fetch API Request 和 Response 接口
数据库访问 ☑️支持,更倾向api接口获取 ☑️支持,更倾向api接口获取 ☑️支持
访问路由 Routing 基于文件系统的路由 基于文件系统的路由,可根据文件目录自动生成路由配置 基于文件系统的路由
api路由 API Routes pages/api目录下 自定义路由 自定义路由
数据加载 Data Fetching ☑️内置 通过 getServerSideProps ☑️内置 通过 asyncData ☑️内置 通过 loader

路由

Remix.js

路由地址 组件
/ App.js > routes/index.js
/invoices App.js > routes/invoices.js > routes/invoices/index.js
/invoices/late App.js > routes/invoices.js > routes/invoices/late.js
/invoices/123 App.js > routes/invoices.js > routes/invoices/$id.js
/invoices/123/edit App.js > routes/invoices.js > routes/invoices/$id.edit.js
/invoices/no/match App.js > routes/404.js
/invoices/new App.js > routes/invoices.new.js
/contact App.js > routes/contact.js

nuxt.js

Nuxt.js 依据 pages 目录结构自动生成 vue-router 模块的路由配置

目录

1
2
3
4
5
pages/
--| user/
-----| index.vue
-----| one.vue
--| index.vue

自动生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
router: {
routes: [
{
name: 'index',
path: '/',
component: 'pages/index.vue'
},
{
name: 'user',
path: '/user',
component: 'pages/user/index.vue'
},
{
name: 'user-one',
path: '/user/one',
component: 'pages/user/one.vue'
}
]
}

next.js

1
2
pages/index.js → /
pages/blog/index.js → /blog

数据加载对比

Remix.js

每个路由模块都可以导出一个组件和一个loader. useLoaderData将加载器的数据提供给您的组件

useLoaderData这个钩子从你的路由的loader函数返回JSON解析数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { useLoaderData } from "remix";
import type { LoaderFunction } from "remix";

export let loader: LoaderFunction = () => {
return fetch('https://.../products') // -> 从接口获取
// return Db.Product.findAll() -> 从数据库获取
// return [{ name: "Pants" }, { name: "Jacket" }]; -> 从静态数据获取
};

export default function Products() {
let products = useLoaderData();
return (
<div>
<h1>Products</h1>
{products.map(product => (
<div>{product.name}</div>
))}
</div>
);
}

nuxt.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div>
<h1>{{ title }}</h1>
<NLink to="/product">
About Product
</NLink>
</div>
</template>

<script>
export default {
data() {
return { project: 'default' }
},
async asyncData({ params }) {
const { data } = await axios.get(`https://my-api/products/${params.id}`)
// return Db.Product.findAll()
// return [{ name: "Pants" }, { name: "Jacket" }];
return { title: data.title }

}
}
</script>

next.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function Product({ products }) {
return (
<ul>
{products.map((product) => (
<li>{product.title}</li>
))}
</ul>
)
}

export async function getServerSideProps() {
const res = await fetch('https://.../products')
// return Db.Product.findAll()
// return [{ name: "Pants" }, { name: "Jacket" }];
const products = await res.json()

return {
props: {
products,
},
}
}

export default Product

相关链接

Remix vs. Next: Which React Meta-Framework Should You Use?

Next.js
Nuxt.js
Remix.js