第13章 SSR高级数据预取策略

13.1 分页数据预取实现

13.1.1 路由参数处理

// 服务端入口文件:entry-server.js
export default context => {
  return new Promise((resolve, reject) => {
    const { app, router, store } = createApp()

    // 将客户端请求的URL推送到路由实例
    router.push(context.url)

    router.onReady(() => {
      // 获取匹配的组件列表(数组形式)
      const matchedComponents = router.getMatchedComponents()

      // 404处理:没有匹配的组件
      if (!matchedComponents.length) {
        return reject({ code: 404 })
      }

      // 遍历所有匹配组件,收集需要预取的数据Promise
      Promise.all(matchedComponents.map(Component => {
        // 检查组件是否定义了asyncData方法
        if (Component.asyncData) {
          // 调用asyncData并传入store和当前路由信息
          // 注意:需要处理可能的reject情况
          return Component.asyncData({
            store,
            route: router.currentRoute // 包含查询参数等路由信息
          })
        }
      })).then(() => {
        // 所有数据预取完成后,将状态注入context
        context.state = store.state
        resolve(app)
      }).catch(reject)
    }, reject)
  })
}

13.1.2 分页组件实现

// components/ArticleList.vue
export default {
  name: 'ArticleList',
  asyncData({ store, route }) {
    // 从路由查询参数中获取页码,默认为1
    const page = Number(route.query.page) || 1
    
    // 调用Vuex action获取数据,返回Promise
    return store.dispatch('fetchArticles', {
      page,
      perPage: 10 // 每页数量
    })
  },
  computed: {
    articles() {
      return this.$store.state.articles.list
    },
    totalPages() {
      return this.$store.state.articles.totalPages
    }
  },
  watch: {
    // 监听路由变化,客户端切换页面时获取数据
    '$route'() {
      // 重新执行数据获取(客户端混合后)
      this.loadData()
    }
  },
  methods: {
    loadData() {
      const page = Number(this.$route.query.page) || 1
      this.$store.dispatch('fetchArticles', { page })
    }
  }
}

13.2 认证状态处理

13.2.1 用户认证中间件

// middleware/auth.js
export default function ({ store, redirect }) {
  // 服务端检查用户认证状态
  if (!store.state.user.isAuthenticated) {
    // 未认证用户重定向到登录页
    return redirect('/login')
  }
}

// 在组件中使用
export default {
  middleware: 'auth',
  // ...其他组件选项
}

13.2.2 认证数据预取

// store/user.js
export const actions = {
  async checkAuth({ commit }, { req }) {
    // 服务端从请求头获取cookie
    if (process.server && req.headers.cookie) {
      // 解析cookie获取token
      const parsed = parseCookies(req)
      if (parsed.token) {
        // 验证token有效性
        const user = await verifyToken(parsed.token)
        commit('SET_USER', user)
      }
    }
    // 客户端从localStorage获取token
    if (process.client) {
      const token = localStorage.getItem('token')
      if (token) {
        const user = await verifyToken(token)
        commit('SET_USER', user)
      }
    }
  }
}

// 在全局导航守卫中调用
router.beforeEach(async (to, from, next) => {
  await store.dispatch('checkAuth', { req: context.req })
  next()
})

13.3 缓存策略优化

13.3.1 页面级别缓存

// 服务端缓存配置
const LRU = require('lru-cache')
const microCache = new LRU({
  max: 100,       // 最大缓存项数
  maxAge: 1000 * 60 // 缓存有效期1分钟
})

// 请求处理中间件
server.get('*', (req, res) => {
  const cacheKey = req.url
  const hit = microCache.get(cacheKey)
  
  // 命中缓存直接返回
  if (hit) {
    console.log(`Cache hit: ${cacheKey}`)
    return res.end(hit)
  }

  renderer.renderToString(context, (err, html) => {
    if (err) {
      // 错误处理...
    }
    
    // 缓存HTML结果
    microCache.set(cacheKey, html)
    res.end(html)
  })
})

13.3.2 组件级别缓存

// 创建带缓存的renderer
const renderer = createBundleRenderer(serverBundle, {
  cache: {
    get: key => componentCache.get(key),
    set: (key, val) => componentCache.set(key, val)
  }
})

// 组件定义缓存策略
export default {
  name: 'HeavyComponent',
  props: ['id'],
  serverCacheKey: props => props.id, // 根据props生成唯一缓存key
  
  // 客户端复用缓存策略
  mounted() {
    // 如果已有缓存数据,直接使用
    if (this.$ssrContext && this.$ssrContext.cachedData) {
      this.data = this.$ssrContext.cachedData
    }
  }
}

13.4 数据预取异常处理

13.4.1 全局错误处理

// 服务端错误处理中间件
const handleError = (err, req, res) => {
  console.error(`Error during SSR: ${err.stack}`)
  
  // 区分404和其他错误
  if (err.code === 404) {
    res.status(404).send('404 | Page Not Found')
  } else {
    // 生产环境显示友好错误页
    if (process.env.NODE_ENV === 'production') {
      res.status(500).send('500 | Internal Server Error')
    } else {
      // 开发环境显示错误堆栈
      res.status(500).send(err.stack)
    }
  }
}

// 在渲染流程中捕获错误
renderer.renderToString(context, (err, html) => {
  if (err) {
    return handleError(err, req, res)
  }
  // ...正常处理
})

13.4.2 组件级错误边界

// components/ErrorBoundary.vue
export default {
  data: () => ({ error: null }),
  errorCaptured(err, vm, info) {
    this.error = err
    // 服务端记录错误日志
    if (process.server) {
      serverLogError(err)
    }
    return false // 阻止错误继续传播
  },
  render(h) {
    return this.error 
      ? h('div', ['Error: ', this.error.message])
      : this.$slots.default[0]
  }
}

// 使用示例
<template>
  <error-boundary>
    <unstable-component/>
  </error-boundary>
</template>

本章重点总结:

  1. 分页策略:结合路由参数与Vuex实现动态数据加载
  2. 认证集成:服务端与客户端统一的认证状态管理
  3. 缓存优化:多级缓存策略提升服务端性能
  4. 错误处理:全局与组件级的错误防御机制

最佳实践提示

  1. 使用process.serverprocess.client区分环境
  2. 为缓存设置合理的TTL和容量限制
  3. 敏感数据避免直接存储在Vuex state中
  4. 使用<client-only>包裹浏览器端特有功能
  5. 定期清理无效缓存项
// 客户端专用组件示例
<template>
  <div>
    <client-only>
      <!-- 依赖浏览器API的功能 -->
      <calendar-widget/>
    </client-only>
  </div>
</template>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

道不尽世间的沧桑

作者想喝瓶哇哈哈,谢谢大佬

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值