天天看點

vue ssr搭建服務端渲染項目

什麼是伺服器端渲染 (SSR)

Vue.js 是建構用戶端應用程式的架構。預設情況下,可以在浏覽器中輸出 Vue 元件,進行生成 DOM 和操作 DOM。然而,也可以将同一個元件渲染為伺服器端的 HTML 字元串,将它們直接發送到浏覽器,最後将這些靜态标記"激活"為用戶端上完全可互動的應用程式。

伺服器渲染的 Vue.js 應用程式也可以被認為是"同構"或"通用",因為應用程式的大部分代碼都可以在伺服器和用戶端上運作。

為什麼使用伺服器端渲染

1.更好的 SEO,由于搜尋引擎爬蟲抓取工具可以直接檢視完全渲染的頁面

2.更快的内容到達時間 (time-to-content),特别是對于緩慢的網絡情況或運作緩慢的裝置

3.在對你的應用程式使用伺服器端渲染 (SSR) 之前,你應該問的第一個問題是,是否真的需要它。這主要取決于内容到達時間 (time-to-content) 對應用程式的重要程度。例如,如果你正在建構一個内部儀表盤,初始加載時的額外幾百毫秒并不重要,這種情況下去使用伺服器端渲染 (SSR) 将是一個小題大作之舉。然而,内容到達時間 (time-to-content) 要求是絕對關鍵的名額,在這種情況下,伺服器端渲染 (SSR) 可以幫助你實作最佳的初始加載性能。

準備工作

1.搭建一個vue項目

2.安裝的插件

cnpm install vue vue-server-renderer --save
cnpm install express --save
cnpm install cross-env -D
           

參考來源 https://ssr.vuejs.org/zh/guide/

參考文章連結https://blog.csdn.net/weixin_30732487/article/details/98167965

首先是main.js

我們為每個請求建立一個新的根 Vue 執行個體。這與每個使用者在自己的浏覽器中使用新應用程式的執行個體類似。如果我們在多個請求之間使用一個共享的執行個體,很容易導緻交叉請求狀态污染 (cross-request state pollution)

是以,我們不應該直接建立一個應用程式執行個體,而是應該暴露一個可以重複執行的工廠函數,為每個請求建立新的應用程式執行個體:

// main.js
import Vue from 'vue'
import App from './App.vue'
import {createRouter} from './router'
import {createStore} from './store'
import { sync } from 'vuex-router-sync'

Vue.config.productionTip = false

export default function createApp () {
  // 建立 router 和 store 執行個體
  const router = createRouter()
  const store = createStore()

  // 同步路由狀态(route state)到 store
  sync(store, router)
  
  const app = new Vue({
    router,
    store,
    render: h => h(App)
  })
  return {app, router, store}
}

           

entry-client.js用戶端配置

用戶端 entry 隻需建立應用程式,并且将其挂載到 DOM 中:

// entry-client.js
import createApp from './main'

const {app, router, store} = createApp()

if (window.__INITIAL_STATE__) {
  store.replaceState(window.__INITIAL_STATE__)
}

router.onReady(() => {
  // 添加路由鈎子函數,用于處理 asyncData.
  // 在初始路由 resolve 後執行,
  // 以便我們不會二次預取(double-fetch)已有的資料。
  // 使用 `router.beforeResolve()`,以便確定所有異步元件都 resolve。
  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()
    }

    // 這裡如果有加載訓示器 (loading indicator),就觸發

    Promise.all(activated.map(c => {
      if (c.asyncData) {
        return c.asyncData({ store, route: to })
      }
    })).then(() => {

      // 停止加載訓示器(loading indicator)

      next()
    }).catch(next)
  })
  
  app.$mount('#app')
})

           

entry-server.js服務端配置

在 entry-server.js 中,我們可以通過路由獲得與 router.getMatchedComponents() 相比對的元件,如果元件暴露出 asyncData,我們就調用這個方法。然後我們需要将解析完成的狀态,附加到渲染上下文(render context)中。

// entry-server.js
import createApp from './main'

export default context => {
  return new Promise((resolve, reject) => {
    const { app, router, store } = createApp()

    router.push(context.url)

    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents()
      if (!matchedComponents.length) {
        return reject({ code: 404 })
      }

      // 對所有比對的路由元件調用 `asyncData()`
      Promise.all(matchedComponents.map(Component => {
        if (Component.asyncData) {
          return Component.asyncData({
            store,
            route: router.currentRoute
          })
        }
      })).then(() => {
        // 在所有預取鈎子(preFetch hook) resolve 後,
        // 我們的 store 現在已經填充入渲染應用程式所需的狀态。
        // 當我們将狀态附加到上下文,
        // 并且 `template` 選項用于 renderer 時,
        // 狀态将自動序列化為 `window.__INITIAL_STATE__`,并注入 HTML。
        context.state = store.state

        resolve(app)
      }).catch(err => {
        throw err
      })
    }, reject)
  })
}

           

router.js路由配置

你可能已經注意到,我們的伺服器代碼使用了一個 * 處理程式,它接受任意 URL。這允許我們将通路的 URL 傳遞到我們的 Vue 應用程式中,然後對用戶端和伺服器複用相同的路由配置!

為此,建議使用官方提供的 vue-router。我們首先建立一個檔案,在其中建立 router。注意,類似于 createApp,我們也需要給每個請求一個新的 router 執行個體,是以檔案導出一個 createRouter 函數:

// router.js
import Vue from 'vue';
import Router from 'vue-router';

Vue.use(Router);

export function createRouter() {
  return new Router({
    mode: 'history',
    routes: [
      {
        path: '/',
        name: 'home',
        component: () => import('../views/Home.vue')
      },
      {
        path: '/about',
        name: 'about',
        component: () => import('../views/About.vue')
      }
    ],
  });
}

           

store.js

// store.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

// 假定我們有一個可以傳回 Promise 的
// 通用 API(請忽略此 API 具體實作細節)
import { fetchItem } from '../api'

export function createStore () {
  return new Vuex.Store({
    state: {
      items: {}
    },
    actions: {
      fetchItem ({ commit }, id) {
        // `store.dispatch()` 會傳回 Promise,
        // 以便我們能夠知道資料在何時更新
        return fetchItem(id).then(item => {
          console.log(item)
          commit('setItem', { id, item })
        })
      }
    },
    mutations: {
      setItem (state, { id, item }) {
        Vue.set(state.items, id, item)
      }
    }
  })
}

           

vue.config

// 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 env = process.env
const isServer = env.RUN_ENV === 'server'

console.log(isServer, env.RUN_ENV)

module.exports = {
  publicPath: './',
  outputDir: `dist/${env.RUN_ENV}`,
  configureWebpack: {
    // 将 entry 指向應用程式的 server / client 檔案
    entry: `./src/entry-${env.RUN_ENV}.js`,
    devtool: 'eval',
    // 這允許 webpack 以 Node 适用方式(Node-appropriate fashion)處理動态導入(dynamic import),
    // 并且還會在編譯 Vue 元件時,
    // 告知 `vue-loader` 輸送面向伺服器代碼(server-oriented code)。
    target: isServer ? 'node' : 'web',
    // 此處告知 server bundle 使用 Node 風格導出子產品(Node-style exports)
    output: {
      libraryTarget: isServer ? 'commonjs2' : undefined
    },
    optimization: {
      splitChunks: isServer ? false : undefined
    },
    // https://webpack.js.org/configuration/externals/#function
    // https://github.com/liady/webpack-node-externals
    // 外置化應用程式依賴子產品。可以使伺服器建構速度更快,
    // 并生成較小的 bundle 檔案。
    externals: isServer ? nodeExternals({
      // 不要外置化 webpack 需要處理的依賴子產品。
      // 你可以在這裡添加更多的檔案類型。例如,未處理 *.vue 原始檔案,
      // 你還應該将修改 `global`(例如 polyfill)的依賴子產品列入白名單
      allowlist: /\.css$/
    }) : undefined,
    // 這是将伺服器的整個輸出
    // 建構為單個 JSON 檔案的插件。
    // 服務端預設檔案名為 `vue-ssr-server-bundle.json`
    // 用戶端預設檔案名為 `vue-ssr-client-manifest.json`
    plugins: [
      isServer ? new VueSSRServerPlugin() : new VueSSRClientPlugin(),
    ]
  }
}

           

最後配置一個本地開發使用的server.js

// server.js
const http = require('http')
const path = require('path')
const fs = require('fs')
const express = require('express')
const {createBundleRenderer} = require('vue-server-renderer')
const serverBundle = require('./dist/server/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/client/vue-ssr-client-manifest.json')

const app = express()
app.set('port', 8001);
// 靜态檔案目錄指向dist檔案夾
app.use(express.static(path.join(__dirname, './dist/client')))

const renderer = createBundleRenderer(serverBundle, {
  runInNewContext: false,
  template: fs.readFileSync(path.resolve(__dirname, './index.html'), 'utf-8'),
  clientManifest
})

app.get('*', (req, res) => {
  const context = {
    title: 'Hello SSR',
    url: req.url
  }

  renderer.renderToString(context, (err, html) => {
    if (err) {
      if (err.code === 404) {
        res.status(404).end('404 not found')
      } else {
        res.status(500).end(err.message)
      }
    } else {
      res.end(html)
    }
  })
})

/*服務啟動*/
http.createServer(app).listen(app.get('port'), function () {
  console.log('service start at ' + app.get('port'));
});

           

package.json添加下面指令

"start": "npm run build:server && npm run build:client && npm run service",
"build:client": "cross-env RUN_ENV=client vue-cli-service build",
"build:server": "cross-env RUN_ENV=server vue-cli-service build",
"service": "node server.js"
           

一個坑打包server時報錯Server-side bundle should have one single entry file. Avoid using CommonsChu

在config.js裡面添加下面幾行代碼

optimization: {
  splitChunks: isServer ? false : undefined
}
           

伺服器端包應該有一個條目檔案。避免在伺服器配置中使用CommonsChunkPlugin。

猜測可能是因為vue-cli3配置了webpack4 裡面的分包功能,是以,再弄得環境下給他禁止掉就好了

自己記錄也可以給有興趣的同學參考,不喜勿噴

繼續閱讀