同构or服务端渲染

本文介绍了同构的概念及其在大前端中的应用,详细阐述了服务端渲染(SSR)的优缺点。同构使得JavaScript应用能够在客户端和服务器端运行,提升了SEO和页面加载速度,但同时也带来了更复杂的开发和服务器负载。文章提到了相关框架如Next.js和Nuxt.js,分析了Nuxt.js的优缺点,并通过vue-hackernews-2.0的SSR Demo解析了Vue的SSR运行流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

同构

同构(isomorph)这个概念应该来源于数学。同构是在数学对象之间定义的一类映射,若两个数学结构之间存在同构映射,那么这两个结构叫做“是同构的”。如果两个结构是同构的,那么其上的对象会有相似的属性和操作,对某个结构成立的命题在另一个结构上也就成立。

在大前端,同构JavaScript应用指的是,用JavaScript编写的应用能够同时运行于客户端和服务器。因此,只需编写一次代码,在服务器上执行它来渲染静态页面,同时执行于客户端以允许快速的交互。

大前端演变的过程:

纯后端渲染 -> 单页面应用(前端渲染和交互) -> 同构(后端渲染和前端交互)

现在,同构和服务端渲染(server side render, SSR)基本是同义词。

SSR的优缺点

优点:

  • 更好的SEO
  • 更快的页面呈现速度

缺点:

  • 更复杂的开发,开发的代码需要兼容前后端的runtime
  • 更复杂的构建和部署
  • 加重服务器负载

同构会不会使前后端的代码耦合在一起?答案是不会。同构只是同构SPA代码,提供API的后台代码完全可以独立成一个项目,可以和同构代码一起部署,也可以分开部署。分开部署的缺点是会多一次http请求,一个客户端请求会先请求同构的后端代码,而同构的后端代码又会请求提供API的应用来获取渲染需要的数据。如果同构的后端代码和供API的代码部署在同一台服务器,多出来的这一次请求基本不会影响性能。也可以一起部署,比如提供API的应用是用node编写的,只用把同构的后端代码的render部分插入到提供API的应用的listen部分就好了。

相关框架

基于React的SSR框架Next.js现在有19k的star,发展的比较好。而基于Vue的SSR框架Nuxt.js只有200多个star,比较惨淡。

看了一下Nuxt.js,写了简单的demo。Nuxt.js其实做了很多工作,主要是在vue.js的内核外又包了一层SSR相关的东西。个人觉得主要的问题有两个。第一是把现有的使用vue的项目迁移到Nuxt.js会很麻烦;第二是Nuxt.js把很多配置(比如webpack的配置)都封装起来了,极大地简化了项目的配置,但是带来的问题是,当项目需要非大众化的配置时,根本就无从下手,因为找不到对应的配置文件。

除了Nuxt.js,vue有一个插件vue-server-renderer来实现SSR。

vue-server-renderer

一个简单的项目文件结构:

├──src
│   ├── components
│   │   ├── Foo.vue
│   │   ├── Bar.vue
│   │   └── Baz.vue
│   ├── App.vue
│   ├── app.js # universal entry
│   ├── entry-client.js # runs in browser only
│   ├── entry-server.js # runs on server only
│   └── index.template.html
├──server.js

app.js是前后端共同的入口,一般返回vue实例的工厂函数。不能像SPA那样直接创建vue实例,因为直接创建的vue实例在服务端会被所有请求复用,从而造成状态污染。

import Vue from 'vue'
import App from './App.vue'

export function createApp () {
  const app = new Vue({
    render: h => h(App)
  })
  return { app }
}

entry-client.js是浏览器端的入口,创建vue实例,并挂载到DOM上。

import { createApp } from './app'

const { app } = createApp()

app.$mount('#app')

entry-server.js主要export一个供服务端的renderer使用的函数。

import { createApp } from './app'

export default context => {
  const { app } = createApp()
  return app
}

注意上面的import并不能在服务端运行,所以要用webpack进行打包。

index.template.html是首页渲染的模板文件,服务端渲染app得到的页面会替换掉<!–vue-ssr-outlet–>。注意这里没有id等于app的元素,而浏览器hydrate的挂载点是’#app’元素,这个元素将会由服务端渲染出的页面给出。

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>{{ title }}</title>
    <link rel="manifest" href="/manifest.json">
    <style>
      #skip a {  position:absolute;  left:-10000px;  top:auto;  width:1px;  height:1px;  overflow:hidden;  }
      #skip a:focus {  position:static;  width:auto;  height:auto;  }
    </style>
  </head>
  <body>
  <p>lance</p>
  <div id="skip"><a href="#app">skip to content</a></div>
  <!--vue-ssr-outlet-->
  </body>
</html>

server.js获取构建好的相关文件,然后处理请求:

// server.js
const createApp = require('/path/to/built-server-bundle.js')

server.get('*', (req, res) => {
  const context = { url: req.url }

  createApp(context).then(app => {
    renderer.renderToString(app, (err, html) => {
      if (err) {
        if (err.code === 404) {
          res.status(404).end('Page not found')
        } else {
          res.status(500).end('Internal Server Error')
        }
      } else {
        res.end(html)
      }
    })
  })
})

vue-hackernews-2.0

vue-hackernews-2.0是官方给出的一个SSR Demo。然而这个拥有6k多个star的demo跑不起来。调试了好久定位到了问题。问题出在demo获取数据的api是托管在firebase上的。

Firebase是一家实时后端数据库创业公司,它能帮助开发者很快的写出Web端和移动端的应用。Firebase在2014年10月被Google收购后,就和Google的云服务结合比较紧密。

由于在国内不翻墙是没办法访问google的网站的,所以获取数据的api访问不了。demo就卡在下面这段代码这里了:

// src/api/create-api-server.js
import Firebase from 'firebase'

export function createAPI ({ config, version }) {
  let api
  if (process.__API__) {
    api = process.__API__
  } else {
    Firebase.initializeApp(config)
    api = process.__API__ = Firebase.database().ref(version)

    //卡在下面
    ;['top', 'new', 'show', 'ask', 'job'].forEach(type => {
      api.child(`${type}stories`).on('value', snapshot => {
        api.cachedIds[type] = snapshot.val()
      })
    })
  }
  return api
}

当api访问不了时,在服务器端和浏览器端都没有输出任何错误提示,这应该算一个bug。由于对Firebase的用法不熟,所以暂时没修复这个bug。

如果要使demo跑起来,就要改写或干掉所有asyncData函数(asyncData是一个ssr获取异步数据的钩子函数,所有获取异步数据的逻辑都写在里面)。

在干掉asyncData函数后,来看看demo的运行结果。为了更好地分析build文件,不要使用npm run dev,因为里面加了很多调试功能相关的代码。直接运行 npm run build。会调用webpack –config build/webpack.client.config.js和webpack –config build/webpack.server.config.js分别对客户端和服务端的代码进行打包。客户端代码打包的文件为:

  • app.403a9442d21cef255283.js
  • manifest.9ceee00d01801404ed42.js
  • vue-ssr-client-manifest.json

服务端打包的文件如下:

  • vue-ssr-server-bundle.json

可以看到,服务端打包的文件只有一个json文件,并没有重复打包应用的js文件。因为应用的js文件是同构的,所以后端也能用,不用重复打包。vue-ssr-server-bundle.json用来创建服务端的renderer。renderer会为每个请求渲染出一个静态页面。

返回的静态页面示例:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Vue HN 2.0</title>
    <meta charset="utf-8">
    <meta name="mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="default">
    <link rel="apple-touch-icon" sizes="120x120" href="/public/logo-120.png">
    <meta name="viewport" content="width=device-width, initial-scale=1, minimal-ui">
    <link rel="shortcut icon" sizes="48x48" href="/public/logo-48.png">
    <meta name="theme-color" content="#f60">
    <link rel="manifest" href="/manifest.json">
    <style>
      #skip a {  position:absolute;  left:-10000px;  top:auto;  width:1px;  height:1px;  overflow:hidden;  }
      #skip a:focus {  position:static;  width:auto;  height:auto;  }
    </style>
  </head>
  <body>
  <p>lance</p>
  <div id="skip"><a href="#app">skip to content</a></div>
  <div id="app" data-server-rendered="true">
    <header class="header">
        <nav class="inner">
            <a href="https://github.com/vuejs/vue-hackernews-2.0" target="_blank" rel="noopener" class="github">Built with Vue.js</a>
        </nav>
    </header>
    <p>hello world !</p>
   </div>
   <script>window.__INITIAL_STATE__={"activeType":null,"itemsPerPage":20,"items":{},"users":{},"lists":{"top":[],"new":[],"show":[],"ask":[],"job":[]},"route":{"path":"\u002Ftop","hash":"","query":{},"params":{},"fullPath":"\u002Ftop","meta":{},"from":{"name":null,"path":"\u002F","hash":"","query":{},"params":{},"fullPath":"\u002F","meta":{}}}};(function(){var s;(s=document.currentScript||document.scripts[document.scripts.length-1]).parentNode.removeChild(s);}());
   </script>
  </body>
</html>

注意上面的data-server-rendered=”true”属性将告诉客户端以hydrate模式渲染页面。window.INITIAL_STATE是后端渲染时注入的状态,避免前端重复获取。

使用SSR的Vue的运行流程

代码在部署后端,当有一个请求过来时,服务器会新建一个vue实例,渲染(render)出需要显示的页面的html,把得到的页面以字符串的形式返回给客户端。同时把相关的js文件也返回(首次请求时返回vue的runtime、webpack的runtime和app.js等文件,非首次请求返回按需加载的js文件),返回的js文件和单页面应用(SPA)返回的差不多。浏览器接收到这些文件后,通过js文件把静态页面的字符串hydrate成可以交互的应用。个人理解,这里的hydrate的工作包括处理事件、运行生命周期钩子函数、实现视图和数据的双向绑定等。

和SPA相比,SSR返回的数据就是多了个静态页面(字符串形式)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值