一点关于服务端渲染的分享__前端__Vue.js
发布于 3 年前 作者 banyungong 1199 次浏览 来自 分享
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利

theme: juejin

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动。

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。


首先带大家认识什么是服务端渲染以及常见的前端服务端渲染框架,然后再围绕服务端渲染如何与 vue 搭配使用,大家一起来看看吧。

什么是服务端渲染

简单理解是将组件或页面通过服务器生成html字符串,再发送到浏览器,最后将静态标记"混合"为客户端上完全交互的应用程序。

  • 传统PHP、JSP、ASP的模版渲染
  • 前端Express、Koa、Egg等基于Node.jsMVC框架
  • React、Vue的同构框架

关于Express & Koa & Egg

快速上手Koa2

koa2koa1的最大区别是koa2实现异步是通过async/awaitkoa1实现异步是通过generator/yield,而express实现异步是通过回调函数的方式。

学习目标:
  • 快速搭建服务
  • 理解洋葱模型 image
  • koa+pug服务端渲染的简单实现
app.use(views(resolve(__dirname,'../views'), {
    extension: 'pug'
}));

我们的服务端渲染实践

  1. Express + Jade框架开始前后端分离。
  2. 从服务端渲染转到浏览器渲染(Vue)框架。
ExpressVue的几点原因:
  • Express比较吃服务器
  • Express项目前端页面与服务端耦合度高,服务挂掉会影响全局
  • Express开发调试不方便,数据问题排除依赖日志
  • Vue组件化效果更好,数据驱动

服务端渲染完败???

客户端渲染(CSR)的不足

image

1.SEO能力不足

虽然Google开始通过爬虫技术直接从js里面抓取关键字,但是实际效果依然与使用SSR页面有着明显的差距。

国外一个技术博客Spectrum的开发者之一Max Stoiber,也是 styled-components 的作者,在分享网站搭建历程时说,后悔刚开始没有采用Next.js。他们在九月引入SSR后,网站收录有了质的影响。

image

2.首次加载白屏,加载慢

目前在我们项目中也有类似的问题,打包的主js比较大,纯浏览器渲染需要等待js加载完之后,才开始页面渲染。

哔哩哔哩(B站)的前端之路 - Crazy-吴俊毅的文章 - 知乎地址

如何优化
// Basic Usage
const path = require('path')
const PrerenderSPAPlugin = require('prerender-spa-plugin')

module.exports = {
  plugins: [
    ...
    new PrerenderSPAPlugin({
      // Required - The path to the webpack-outputted app to prerender.
      staticDir: path.join(__dirname, 'dist'),
      // Required - Routes to render.
      routes: [ '/', '/about', '/some/deep/nested/route' ],
    })
  ]
}
  • 同构渲染

同构渲染之Nuxt

来自Vue官网大评价: 从头搭建一个服务端渲染的应用是相当复杂的。幸运的是,我们有一个优秀的社区项目 Nuxt.js 让这一切变得非常简单。Nuxt 是一个基于 Vue 生态的更高层的框架,为开发服务端渲染的 Vue 应用提供了极其便利的开发体验。更酷的是,你甚至可以用它来做为静态站生成器。推荐尝试。

附:nuxt中文文档

实战项目分析:Nuxt + ZanUI

如何新增插件
  • 使用第三方模块
npm install --save axios
<template>
  <h1>{{ title }}</h1>
</template>

<script>
  import axios from 'axios'

  export default {
    async asyncData({ params }) {
      let { data } = await axios.get(`https://my-api/posts/${params.id}`)
      return { title: data.title }
    }
  }
</script>
  • 直接使用链接
head: {
    link: [
      {
        rel: 'stylesheet',
        href: '//at.alicdn.com/t/font_1985456_2ushr2u1y69.css',
      },
    ],
    script: [
      {src: 'https://webapi.amap.com/maps', async: true},
    ],
  },
  • 使用 Vue 插件
~/plugins/ui.js

import Vue from 'vue'
import Element from 'element-ui'
import locale from 'element-ui/lib/locale/lang/zh-CN'

Vue.use(Element, { locale })
~/nuxt.config.js

plugins: [
    { src: '~/plugins/ui.js', ssr: true },
  ],
  • 注入 Vue 实例
~/plugins/vue-inject.js

import Vue from 'vue'

Vue.prototype.$myInjectedFunction = string =>
  console.log('This is an example', string)
~/nuxt.config.js

export default {
  plugins: ['~/plugins/vue-inject.js']
}
  • 注入 context
~/plugins/ctx-inject.js

export default ({ app }, inject) => {
  // Set the function directly on the context.app object
  app.myInjectedFunction = string =>
    console.log('Okay, another function', string)
}
~/nuxt.config.js
export default {
  plugins: ['~/plugins/ctx-inject.js']
}
  • 同时注入
// 引入@nuxtjs/axios 添加到 module
...
// 注册服务
const extendAxiosInstance = axios => {
  const axiosModules = <%= serialize(options.axios.modules) %>

  if (axiosModules) {
    const keys = Object.keys(axiosModules);
    for (let module of keys) {
      // Request helpers ($get, $post, ...)
      axios['$' + module] = {}
      for (let method of ['request', 'delete', 'get', 'head', 'options', 'post', 'put', 'patch']) {
        axios['$' + module][method] = function() {
          if (arguments.length > 0) {
            ...
          }
          return axios[method].apply(axios, arguments)
        }
      }
    }
  }
}


function AxiosFunction(content) {
  const indexAxios = content.app.$axios;
  extendAxiosInstance(indexAxios)
  ...
  // 请求拦截
  indexAxios.interceptors.request.use(function(config) {
    ...
    return config;
  });


  // 响应拦截器
  indexAxios.interceptors.response.use(response => new Promise((resolve, reject) => {
    ....
    resolve(data.data);
  }), error => {
    //关闭加载
    close(error.config);
    const resultData = {}
    ...
    return Promise.reject(resultData)
  });

  // 自定义拦截器
  const interceptors = <%= serialize(options.axios.interceptors) %>
  if (interceptors) {
    interceptors.forEach(config => {
      ...
    })
  }

  return indexAxios;
}

// 同时注入
export default (content, inject) => {
  inject('http', AxiosFunction(content))
}

服务端获取数据
asyncData ({isDev, route, store, env, params, query, req, res, redirect, error}) {
    const detailDate = await $http.get(parkProductDetail + params.id);
    const picLab = detailDate.wapPictureLib;
    if (detailDate.flashUrl) {
      picLab.unshift({
        type: 'video',
        url: detailDate.flashUrl,
        cover: ' ',
      });
    }
    return { parkInfo: detailDate, parkId: params.id, picLab };
 fetch({ store, params }) {
    const detailDate = await $http.get(parkProductDetail + params.id);
    store.commit('setDetail', detailDate)
 })    
Store的使用
~/store/mdse.js

import vm from 'vue';

export const state = () => ({
  orderInfo: {}, // 缓存下单数据
});

export const getters = {
  getOrderInfo(_state) {
    return _state.orderInfo;
  },
};

export const mutations = {
  // 更新下单信息
  updateOrderInfo(_state, info) {
    vm.set(_state, 'orderInfo', info);
  },
};

node中间件
~/middleware/auth.js

/**
 * 拦截登录
 */

export default async ({ store, route: { path, fullPath }, redirect }) => {
  // 商户端
  if (path.startsWith('/merchant')) {
    const ignoreMerchantCatchPath = [
      '/merchant',
      ....
    ];

    if (
      !ignoreMerchantCatchPath.includes(path) &&
      !store.getters['user/isMerchantLogin']
    ) {
      redirect(`/merchant/login/shop?redirect=${encodeURIComponent(fullPath)}`);
      return;
    }
    return;
  } else {
    await store.dispatch('getProjectInfo');
    // 核销二维码地址
    if (path.indexOf('/t/') >= 0) {
      return;
    }

    // 忽略校验登录地址
    const ignoreCatchPath = [
      '/',
      '/login',
      '/login/registry',
      ....
    ];

    if (
      !(
        ignoreCatchPath.includes(path) ||
        path.indexOf('/detail/') !== -1 ||
        path.indexOf('/group/') !== -1
      ) &&
      !store.getters['user/isLogin']
    ) {
      redirect(`/login?redirect=${encodeURIComponent(fullPath)}`);
      return;
    }
  }
};

打包与部署
npm install
npm run build

pm2 start ./ecosystem.config.js
~/ecosystem.config.js

module.exports = {
  apps: [
    {
      name: 'cqyjy-mall-mobile-ui',
      script: 'npm start',
      log_date_format: 'YYYY-MM-DD HH:mm:ss',
      error_file: './logs/app-err.log',
      out_file: './logs/app-out.log',
    },
  ],
};

版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: ITEM 原文链接:https://juejin.im/post/7025164660751990798

回到顶部