如何为 Vue 应用配置 SSR 或 SSG,以优化 SEO 和首屏渲染性能?

各位观众老爷们,晚上好!今天咱们不聊八卦,专攻Vue的SSR和SSG,保证各位听完之后,腰不酸了,腿不疼了,一口气能优化十个Vue项目!

开场白:为何SSR/SSG如此重要?

想象一下,你的Vue应用就像一个害羞的小姑娘,第一次见未来婆婆(搜索引擎爬虫)。如果她躲在房间里(客户端渲染),等精心打扮完才出来(JS执行完才渲染),婆婆可能等不及就走了,留下的印象分肯定不高。

而SSR/SSG就像是提前把小姑娘打扮好,直接端到婆婆面前,第一印象直接拉满!搜索引擎一看,哇,内容丰富,速度飞快,立马给个好评!

第一部分:SSR(服务端渲染) – 动态的魅力

SSR,Server-Side Rendering,就是把Vue组件在服务器上预先渲染成HTML,再发送给浏览器。浏览器拿到的是可以直接显示的HTML,而不是一堆需要JS解析的代码。

  • 优点:

    • SEO优化: 搜索引擎更容易抓取到完整的内容,提高排名。
    • 首屏渲染加速: 用户更快看到页面内容,提升用户体验。
  • 缺点:

    • 服务器压力增大: 每次请求都需要服务器渲染,对服务器性能有要求。
    • 开发复杂度增加: 需要考虑服务器环境和客户端环境的差异。

1.1 使用Nuxt.js简化SSR

Nuxt.js是一个基于Vue.js的框架,专门用于简化SSR应用的开发。它封装了很多底层细节,让我们能够更专注于业务逻辑。

  • 安装Nuxt.js:

    npx create-nuxt-app my-nuxt-app
    # 或者使用 yarn
    yarn create nuxt-app my-nuxt-app

    按照提示选择配置,比如项目名称、UI框架、模块等等。

  • 目录结构:

    Nuxt.js有自己的一套目录结构,其中几个重要的目录包括:

    目录 说明
    pages/ 存放Vue组件,自动生成路由。
    layouts/ 存放布局组件,定义页面结构。
    store/ 存放Vuex状态管理相关文件。
    components/ 存放可复用的Vue组件。
  • 页面路由:

    Nuxt.js会根据pages/目录下的文件自动生成路由。例如,pages/index.vue对应根路由/pages/about.vue对应路由/about

    // pages/index.vue
    <template>
      <div>
        <h1>欢迎来到我的 Nuxt.js 应用!</h1>
        <nuxt-link to="/about">关于我</nuxt-link>
      </div>
    </template>
    
    <script>
    export default {
      name: 'IndexPage'
    }
    </script>
  • 异步数据获取:asyncDatafetch

    在SSR中,通常需要在服务器端获取数据。Nuxt.js提供了asyncDatafetch两个方法来实现这个功能。

    • asyncData 在组件渲染之前调用,可以将数据返回给组件的data

      // pages/posts/_id.vue
      <template>
        <div>
          <h1>{{ post.title }}</h1>
          <p>{{ post.content }}</p>
        </div>
      </template>
      
      <script>
      export default {
        async asyncData({ params, $axios }) { // 使用 $axios 插件
          const { data } = await $axios.$get(`https://api.example.com/posts/${params.id}`)
          return { post: data }
        }
      }
      </script>
    • fetch 类似于asyncData,但不能直接修改组件的data,通常用于更新Vuex store。

      // pages/index.vue
      <template>
        <div>
          <h1>最新文章</h1>
          <ul>
            <li v-for="post in posts" :key="post.id">{{ post.title }}</li>
          </ul>
        </div>
      </template>
      
      <script>
      import { mapState } from 'vuex'
      
      export default {
        computed: {
          ...mapState(['posts'])
        },
        async fetch({ store, $axios }) {
          const { data } = await $axios.$get('https://api.example.com/posts')
          store.commit('setPosts', data)
        }
      }
      </script>
  • SEO优化:nuxt.config.js

    Nuxt.js提供了nuxt.config.js文件来配置应用的各种选项,包括SEO相关的元数据。

    // nuxt.config.js
    export default {
      head: {
        title: '我的Nuxt应用',
        meta: [
          { charset: 'utf-8' },
          { name: 'viewport', content: 'width=device-width, initial-scale=1' },
          { hid: 'description', name: 'description', content: '我的Nuxt应用的描述' }
        ],
        link: [
          { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
        ]
      }
    }
  • 部署:

    使用npm run buildyarn build构建应用,然后使用npm run startyarn start启动服务器。也可以部署到云服务器,比如阿里云、腾讯云等。

1.2 手动配置Vue SSR(难度提升!)

如果你想更深入地了解SSR的原理,可以尝试手动配置Vue SSR。这需要更多的配置和代码,但也能让你更灵活地控制整个流程。

  • 环境准备:

    • Node.js环境
    • Vue CLI
  • 安装依赖:

    npm install vue vue-server-renderer express --save
  • 创建服务器端入口:server.js

    // server.js
    const express = require('express')
    const Vue = require('vue')
    const { createRenderer } = require('vue-server-renderer')
    
    const app = express()
    const renderer = createRenderer()
    
    app.get('*', (req, res) => {
      const app = new Vue({
        data: {
          url: req.url
        },
        template: `<div>访问的 URL 是: {{ url }}</div>`
      })
    
      renderer.renderToString(app, (err, html) => {
        if (err) {
          res.status(500).end('Internal Server Error')
          return
        }
        res.end(`
          <!DOCTYPE html>
          <html lang="en">
            <head><title>Hello</title></head>
            <body>${html}</body>
          </html>
        `)
      })
    })
    
    app.listen(8080, () => {
      console.log('服务器已启动: http://localhost:8080')
    })
  • 运行服务器:

    node server.js

    访问http://localhost:8080,就能看到服务端渲染的页面了。

  • 更复杂的配置:bundleRenderer

    上面的例子只是一个简单的演示。在实际项目中,我们需要使用bundleRenderer来处理更复杂的组件和依赖关系。

    • 构建客户端和服务器端代码:

      使用Vue CLI创建项目,并配置vue.config.js文件,分别构建客户端和服务器端的代码。

      // vue.config.js
      const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
      const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
      const nodeExternals = require('webpack-node-externals')
      
      const TARGET_NODE = process.env.WEBPACK_TARGET === 'node'
      const plugins = [
        TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()
      ]
      
      module.exports = {
        configureWebpack: () => ({
          entry: `./src/entry-${TARGET_NODE ? 'server' : 'client'}.js`,
          devtool: 'source-map',
          target: TARGET_NODE ? 'node' : 'web',
          node: TARGET_NODE ? undefined : false,
          output: {
            libraryTarget: TARGET_NODE ? 'commonjs2' : undefined
          },
          externals: TARGET_NODE ? nodeExternals({
            allowlist: [/.css$/]
          }) : undefined,
          optimization: {
            splitChunks: false
          },
          plugins: plugins
        })
      }
    • 创建客户端入口:src/entry-client.js

      // src/entry-client.js
      import { createApp } from './main'
      
      const { app, router } = createApp()
      
      router.onReady(() => {
        app.$mount('#app')
      })
    • 创建服务器端入口:src/entry-server.js

      // src/entry-server.js
      import { createApp } from './main'
      
      export default context => {
        return new Promise((resolve, reject) => {
          const { app, router } = createApp()
      
          router.push(context.url)
      
          router.onReady(() => {
            const matchedComponents = router.getMatchedComponents()
            if (!matchedComponents.length) {
              return reject({ code: 404 })
            }
      
            resolve(app)
          }, reject)
        })
      }
    • 更新server.js

      // server.js
      const express = require('express')
      const { createBundleRenderer } = require('vue-server-renderer')
      const fs = require('fs')
      
      const app = express()
      
      const template = fs.readFileSync('./public/index.template.html', 'utf-8')
      const serverBundle = require('./dist/vue-ssr-server-bundle.json')
      const clientManifest = require('./dist/vue-ssr-client-manifest.json')
      
      const renderer = createBundleRenderer(serverBundle, {
        runInNewContext: false,
        template,
        clientManifest
      })
      
      app.use(express.static('./dist'))
      app.use(express.static('./public'))
      
      app.get('*', (req, res) => {
        const context = {
          url: req.url,
          title: 'Hello SSR' // 可以动态设置 title
        }
      
        renderer.renderToString(context, (err, html) => {
          if (err) {
            console.error(err)
            if (err.code === 404) {
              res.status(404).end('Page not found')
            } else {
              res.status(500).end('Internal Server Error')
            }
          } else {
            res.end(html)
          }
        })
      })
      
      app.listen(8080, () => {
        console.log('服务器已启动: http://localhost:8080')
      })
    • 创建public/index.template.html

      <!DOCTYPE html>
      <html lang="en">
        <head>
          <meta charset="UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <title>{{ title }}</title> <!-- 使用 context.title -->
          {{{ renderStyles() }}}
        </head>
        <body>
          <!--vue-ssr-outlet-->
          {{{ renderScripts() }}}
        </body>
      </html>
    • 运行构建命令:

      npm run build:ssr
    • 运行服务器:

      node server.js

    这个过程比较复杂,需要仔细阅读Vue SSR的官方文档。

第二部分:SSG(静态站点生成) – 静态的极致

SSG,Static Site Generation,就是在构建时将Vue组件预先渲染成HTML文件,然后直接部署到服务器。每次请求都直接返回静态HTML文件,无需服务器动态渲染。

  • 优点:

    • 速度极快: 直接返回静态文件,无需服务器计算。
    • 安全性高: 没有服务器端代码,减少安全风险。
    • 易于部署: 可以部署到CDN等静态资源服务器。
  • 缺点:

    • 不适合动态内容: 每次更新都需要重新构建。
    • 构建时间较长: 大型项目构建时间可能很长。

2.1 使用Gridsome构建SSG应用

Gridsome是一个基于Vue.js的静态站点生成器。它使用GraphQL来管理数据,并提供了丰富的插件来扩展功能。

  • 安装Gridsome:

    npm install --global @gridsome/cli
    
    gridsome create my-gridsome-site
    cd my-gridsome-site
    gridsome develop
  • 目录结构:

    Gridsome的目录结构也比较规范:

    目录 说明
    src/pages/ 存放Vue组件,自动生成路由。
    src/layouts/ 存放布局组件,定义页面结构。
    src/components/ 存放可复用的Vue组件。
    src/templates/ 存放模板组件,用于动态生成页面(例如博客文章)。
    gridsome.config.js 存放Gridsome的配置信息。
    gridsome.server.js 存放服务器端的配置信息。
  • GraphQL数据源:

    Gridsome使用GraphQL来查询数据。你需要配置数据源,例如本地文件、API接口等。

    • 本地文件:

      // gridsome.config.js
      module.exports = {
        plugins: [
          {
            use: '@gridsome/source-filesystem',
            options: {
              path: 'content/**/*.md', // Markdown文件的路径
              typeName: 'Post', // GraphQL类型名称
              remark: {
                // Plugins options for remark-prismjs
                plugins: [
                  '@gridsome/remark-prismjs'
                ]
              }
            }
          }
        ]
      }
    • API接口:

      // gridsome.config.js
      module.exports = {
        plugins: [
          {
            use: '@gridsome/source-graphql',
            options: {
              url: 'https://api.example.com/graphql',
              typeName: 'MyAPI',
              fieldName: 'api'
            }
          }
        ]
      }
  • 页面查询:

    在Vue组件中使用GraphQL查询数据。

    // src/pages/Index.vue
    <template>
      <div>
        <h1>最新文章</h1>
        <ul>
          <li v-for="post in $page.posts.edges" :key="post.node.id">
            <g-link :to="post.node.path">{{ post.node.title }}</g-link>
          </li>
        </ul>
      </div>
    </template>
    
    <page-query>
    query {
      posts: allPost {
        edges {
          node {
            id
            title
            path
          }
        }
      }
    }
    </page-query>
  • 模板组件:

    使用模板组件动态生成页面。例如,为每篇博客文章生成一个页面。

    // src/templates/Post.vue
    <template>
      <div>
        <h1>{{ $page.post.title }}</h1>
        <div v-html="$page.post.content"></div>
      </div>
    </template>
    
    <page-query>
    query Post ($path: String!) {
      post: post (path: $path) {
        title
        content
      }
    }
    </page-query>
  • 构建和部署:

    使用gridsome build构建应用,然后将dist/目录下的文件部署到服务器。

2.2 使用VuePress构建文档站点

VuePress是一个由Vue驱动的静态网站生成器,专门用于构建文档站点。它使用Markdown编写内容,并提供了丰富的插件来扩展功能。

  • 安装VuePress:

    npm install -g vuepress
  • 创建项目:

    mkdir my-vuepress-site
    cd my-vuepress-site
    npm init -y
    mkdir .vuepress
    echo '# Hello VuePress' > README.md
  • 配置package.json

    // package.json
    {
      "scripts": {
        "docs:dev": "vuepress dev .",
        "docs:build": "vuepress build ."
      }
    }
  • 编写文档:

    使用Markdown编写文档,存放在项目根目录下。

    # Hello VuePress
    
    This is my first VuePress site.
  • 运行开发服务器:

    npm run docs:dev
  • 构建和部署:

    使用npm run docs:build构建应用,然后将.vuepress/dist目录下的文件部署到服务器。

第三部分:SSR vs SSG – 如何选择?

SSR和SSG各有优缺点,如何选择取决于你的项目需求。

特性 SSR SSG
内容更新频率 适合频繁更新的内容,例如新闻、电商 适合不经常更新的内容,例如博客、文档
数据来源 动态数据,例如API接口 静态数据,例如Markdown文件
性能 首屏渲染速度快,但服务器压力大 速度极快,无需服务器计算
SEO优化 搜索引擎容易抓取完整内容 搜索引擎容易抓取完整内容
开发复杂度 较高 较低
部署复杂度 较高,需要服务器环境 较低,可以部署到CDN等静态资源服务器

总结:

  • 如果你的应用需要频繁更新内容,并且对SEO有较高要求,可以选择SSR。Nuxt.js可以大大简化SSR的开发过程。
  • 如果你的应用内容不经常更新,并且对性能有极致要求,可以选择SSG。Gridsome和VuePress都是不错的选择。

最后的忠告:

不要盲目追求SSR或SSG,要根据实际情况选择最适合你的方案。记住,没有银弹,只有最适合你的工具!希望大家都能成为SSR/SSG大师,让你的Vue应用飞起来!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注