Vue-Server-Renderer


文章來自
帶你五步學會Vue-SSR
Vue服務器端渲染
構建一個SSR應用程序
SSR熱更新
github項目

Vue-SSR優缺點

  • 請求到的首屏頁面是服務器渲染好的了,SEO很好
  • 但是對服務器的壓力很大

image.png

image.png

安裝插件

# 安裝 vue-server-renderer
# 安裝 lodash.merge
# 安裝 webpack-node-externals
# 安裝 cross-env
npm install vue-server-renderer lodash.merge webpack-node-externals cross-env --save-dev

# 安裝 koa
# 安裝 koa-static
npm install koa koa-static --save

改造vuex

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export function createStore() {
  return new Vuex.Store({
    state: {
       test: ''
    },
    mutations: {
      SET_TEST(state, data) {
        state.test = data;
      }
    },
    actions: {
      test({ commit },opt) { 
         //  異步查詢
         commit('SET_TEST', data); 
      }
    }
  });
}

改造vue-route

import Vue from 'vue';
import Router from 'vue-router';

Vue.use(Router);
export function createRouter() {
  return new Router({
    mode: 'history', // 注意這里要使用history模式,因為hash不會發送到服務端
    routes: []
  });
}

改造main.js

import Vue from 'vue';
import App from './App.vue';
import { createRouter } from './router';
import { createStore } from './store';

import './assets/css/style.scss';
import './assets/iconfont/iconfont.css';

Vue.config.productionTip = false;

export function createApp() {
  const router = createRouter();
  const store = createStore();
  const app = new Vue({
    router,
    store,
    render: h => h(App)
  });
  return { app, router, store };
}

改造所有的vue文件

<template>
  <div>{{ test }}</div>
</template>

<script>
export default {
  // 這個方法需要主動調用,等於是自定義生命周期函數,而且必須是這個名字
  asyncData ({ store, route }) {
    return store.dispatch('test', route.params.id)
  },
  computed: {
    // 當asyncData被執行,vuex數據改變,導致computed發生改變
    test() {
      return this.$store.state.test
    }
  }
}
</script>

創建entry-client.js

import { createApp } from './main';

// 客戶端特定引導邏輯……
const { app, router, store } = createApp();

// 如果有__INITIAL_STATE__變量,則將store的狀態用它替換
if (window.__INITIAL_STATE__) {
  store.replaceState(window.__INITIAL_STATE__);
}

router.onReady(() => {
  // 添加路由鈎子函數,用於處理 asyncData方法
  router.beforeResolve((to, from, next) => {
    const matched = router.getMatchedComponents(to);
    const prevMatched = router.getMatchedComponents(from);

    // 我們只關心非預渲染的組件
    // 所以我們對比它們,找出兩個匹配列表的差異組件
    let diffed = false;
    const activated = matched.filter((c, i) => {
      return diffed || (diffed = prevMatched[i] !== c);
    });

    if (!activated.length) {
      return next();
    }

    Promise.all( activated.map(c => {
        // 把所有的vue里的asyncData方法一起執行了
        if (c.asyncData) {
          return c.asyncData({ store, route: to });
        }
      })
    ).then(() => {
        next();
    }).catch(next);
  });

  // 將Vue實例掛載到dom中,完成瀏覽器端應用啟動
  app.$mount('#app');
});

創建entry-server.js

import { createApp } from './main';

export default context => {
  // 因為有可能會是異步路由鈎子函數或組件,所以我們將返回一個 Promise
  // 以便服務器能夠等待所有的內容在渲染前,就已經准備就緒。
  return new Promise((resolve, reject) => {
    const { app, store, router } = createApp();
    // 設置服務器端 router 的位置
    router.push(context.url);

    // 等到 router 將可能的異步組件和鈎子函數解析完
    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents();
      // 匹配不到的路由,執行 reject 函數,並返回 404
      if (!matchedComponents.length) {
        return reject({ code: 404 });
      }
      Promise.all( matchedComponents.map(c => {
          if (c.asyncData) {
            return c.asyncData({ store, route: router.currentRoute});
          }
        })
      ).then(() => {
          // 當使用 template 時,context.state 將作為 window.__INITIAL_STATE__ 狀態,自動嵌入到最終的 HTML 中
          context.state = store.state;
          // 返回根組件
          resolve(app);
        })
        .catch(reject);
    }, reject);
  });
};

修改vue.config.js
有些教程是把這個分成三個配置文件,效果也一樣

const path = require('path');
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin');
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin');
const nodeExternals = require('webpack-node-externals'); // 忽略node_modules文件夾中的所有模塊
// const merge = require('lodash.merge');
// const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

const TARGET_NODE = process.env.WEBPACK_TARGET === 'node';
const target = TARGET_NODE ? 'server' : 'client'; //根據環境變量來指向入口


function resolve(dir) {
  return path.join(__dirname, dir);
}

module.exports = {
  //基本路徑
  publicPath: process.env.NODE_ENV !== 'production' ? 'http://127.0.0.1:8080' : './',
  // 如果你不需要生產環境的 source map,可以將其設置為 false 以加速生產環境構建。
  // productionSourceMap: false,
  // 輸出文件目錄
  outputDir: 'dist',
  css: {
    extract: process.env.NODE_ENV === 'production',
    sourceMap: true
    //向 CSS 相關的 loader 傳遞選項(支持 css-loader postcss-loader sass-loader less-loader stylus-loader)
  },
  configureWebpack: () => ({
    // 將 entry 指向應用程序的 server / client 文件
    entry: `./src/entry-${target}.js`,
    // 需要開啟source-map文件映射,因為服務器端在渲染時,
    // 會通過Bundle中的map文件映射關系進行文件的查詢
    devtool: 'source-map',
    // 服務器端在Node環境中運行,需要打包為類Node.js環境可用包(使用Node.js require加載chunk)
    // 客戶端在瀏覽器中運行,需要打包為類瀏覽器環境里可用包
    target: TARGET_NODE ? 'node' : 'web',
    // 關閉對node變量、模塊的polyfill
    node: TARGET_NODE ? undefined : false,
    output: {
      // 配置模塊的暴露方式,服務器端采用module.exports的方式,客戶端采用默認的var變量方式
      libraryTarget: TARGET_NODE ? 'commonjs2' : undefined
    },
    // 外置化應用程序依賴模塊。可以使服務器構建速度更快
    externals: TARGET_NODE
      ? nodeExternals({
          // 不要外置化 webpack 需要處理的依賴模塊。
          // 你可以在這里添加更多的文件類型。例如,未處理 *.vue 原始文件,
          // 你還應該將修改 `global`(例如 polyfill)的依賴模塊列入白名單
          whitelist: [/\.css$/]
        })
      : undefined,
    optimization: {
      splitChunks: TARGET_NODE ? false : undefined
    },
    // 根據之前配置的環境變量判斷打包為客戶端/服務器端Bundle
    plugins: [TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()]
  }),
  chainWebpack: config => {
    // 關閉vue-loader中默認的服務器端渲染函數
    config.module
      .rule('vue')
      .use('vue-loader')
      .tap(options => {
        // merge(options, {
        //   optimizeSSR: false
        // });
        options.optimizeSSR = false;
        return options;
      });
    config.resolve.alias
      .set('@src', resolve('src'))
      .set('@api', resolve('src/api'))
      .set('@assets', resolve('src/assets'))
      .set('@comp', resolve('src/components'))
      .set('@views', resolve('src/views'));
  },
  devServer: {
    historyApiFallback: true,
    headers: { 'Access-Control-Allow-Origin': '*' }
    // port: 8088
    // proxy: { ... }
    // }
  },
  lintOnSave: false
};

創建index.temp.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>{{ title }}</title>
</head>
<body>
  <div id="app">
    <!--vue-ssr-outlet-->
  </div>
</body>
</html>

修改package.json

// 在原本的dev和build基礎上加上
"build:server": "cross-env NODE_ENV=production WEBPACK_TARGET=node vue-cli-service build",

運行npm run build:server會生成兩個json

創建service.js

const fs = require("fs");
const Koa = require("koa");
const path = require("path");
const koaStatic = require('koa-static')
const app = new Koa();

const resolve = file => path.resolve(__dirname, file);
// 開放dist目錄
app.use(koaStatic(resolve('./dist')))

// 第 2 步:獲得一個createBundleRenderer
const template = fs.readFileSync(resolve("./public/index.temp.html"), "utf-8");
const { createBundleRenderer } = require("vue-server-renderer");
const bundle = require("./dist/vue-ssr-server-bundle.json");
const clientManifest = require("./dist/vue-ssr-client-manifest.json");

const renderer = createBundleRenderer(bundle, {
  runInNewContext: false,
  template: template,
  clientManifest: clientManifest
});

function renderToString(context) {
  return new Promise((resolve, reject) => {
    renderer.renderToString(context, (err, html) => {
      err ? reject(err) : resolve(html);
    });
  });
}
// 第 3 步:添加一個中間件來處理所有請求
app.use(async (ctx, next) => {
  const context = {
    title: "ssr-test",
  };
  // 將 context 數據渲染為 HTML
  const html = await renderToString(context);
  ctx.body = html;
});

const port = 3000;
app.listen(port, function() {
  console.log(`server started at localhost:${port}`);
});

修改package.json

// 再加上
"dev:serve": "node server.js",

全部配置完成,先執行打包再啟動服務

其他配置

  • 服務器路由緩存,加快編譯


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM