再說 Vue SSR 的 Cookies 問題

一個網站一旦涉及到多用戶, 就很難從 Cookies 中逃脫, Vue SSR 的 cookies 也真算是遇到的一個不小的問題, 從開始玩 SSR 開始到如今, 一共想出了3種方案, 從最先的把 Cookies 注入到 state 中, 到把 Cookies 注入到 global, 到如今的將 Cookies 注入到組件的 asyncData 方法.javascript

隨着 Vue 的升級, 第一種方案已經再也不適用, 第二種也有很多的限制, 因而想到第三種方案, 下來就說說具體實現的方法:html

第一種方案

第一種方案已經再也不適用, 這裏再也不細說vue

第二種方案

思路: 將 cookies 注入到 ssr 的 context裏, 而後在請求 api 時讀取, 再追加到 axios 的header 裏java

1, 首先在 server.js 裏將 cookies 加到 context裏ios

const context = {
    title: 'M.M.F 小屋',
    description: 'M.M.F 小屋',
    url: req.url,
    cookies: req.cookies
}
renderer.renderToString(context, (err, html) => {
    if (err) {
        return errorHandler(err)
    }
    res.end(html)
})

以後, Vue 會把 context 加到 global.__VUE_SSR_CONTEXT__git

2, 在 api.js 裏讀取 cookiesgithub

import axios from 'axios'
import qs from 'qs'
import md5 from 'md5'
import config from './config-server'

const SSR = global.__VUE_SSR_CONTEXT__
const cookies = SSR.cookies || {}
const parseCookie = cookies => {
    let cookie = ''
    Object.keys(cookies).forEach(item => {
        cookie+= item + '=' + cookies[item] + '; '
    })
    return cookie
}

export default {
    async post(url, data) {
        const cookie = parseCookie(cookies)
        const res = await axios({
            method: 'post',
            url: config.api + url,
            data: qs.stringify(data),
            timeout: config.timeout,
            headers: {
                'X-Requested-With': 'XMLHttpRequest',
                'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
                cookie
            }
        })
        return res
    },
}

爲何能夠這麼作?
默認狀況下,Vue 對於每次渲染,bundle renderer 將建立一個新的 V8 上下文並從新執行整個 bundle。應用程序代碼與服務器進程隔離, 因此每一個訪問的用戶上下文都是獨立的, 不會互相影響.vuex

可是從Vue@2.3.0開始, 在createBundleRenderer方法的選項中, 添加了runInNewContext選項, 使用 runInNewContext: false,bundle 代碼將與服務器進程在同一個 global 上下文中運行,因此咱們不能再將 cookies 放在 global, 由於這會讓全部用戶共用同一個 cookies.axios

爲何如今不這麼作?
那咱們繼續將runInNewContext設置成true, 不就行了嗎? 固然也是能夠的, 可是從新建立上下文並執行整個 bundle 仍是至關昂貴的,特別是當應用很大的時候.api

以我本身的博客爲例, 以前只有渲染 5 個路由組件, loadtest 的 rps, 有 50 左右, 可是後來把後臺的 12 個路由組件也加到 SSR 後, rps 直接降到了個位數...

因此出現瞭如今的第三種方案

第三種方案

思路: 將 Cookies 做爲參數注入到組件的asyncData方法, 而後用傳參數的方法把 cookies 傳給 api, 不得不說這種方法很麻煩, 可是這是我的能想到的比較好的方法

步驟1:

仍是在 server.js 裏, 把 cookies 注入到 context 中

const context = {
    title: 'M.M.F 小屋',
    url: req.url,
    cookies: req.cookies,
}
renderer.renderToString(context, (err, html) => {
    if (err) {
        return handleError(err)
    }
    res.end(html)
})

步驟2:

entry-server.js裏, 將cookies做爲參數傳給 asyncData 方法

Promise.all(matchedComponents.map(({asyncData}) => asyncData && asyncData({
    store,
    route: router.currentRoute,
    cookies: context.cookies,
    isServer: true,
    isClient: false
}))).then(() => {
    context.state = store.state
    context.isProd = process.env.NODE_ENV === 'production'
    resolve(app)
}).catch(reject)

步驟3:

在組件裏, 把 cookies 作爲參數給 Vuex 的 actions

export default {
    name: 'frontend-index',
    async asyncData({store, route, cookies}, config = { page: 1}) {
        config.cookies = cookies
        await store.dispatch('frontend/article/getArticleList', config)
    }
    // .....
}

步驟4:

在 Vuex 裏將 cookies 作爲參數給 api

import api from '~api'

const state = () => ({
    lists: {
        data: [],
        hasNext: 0,
        page: 1,
        path: ''
    },
})

const actions = {
    async ['getArticleList']({commit, state}, config) {
        // vuex 做爲臨時緩存
        if (state.lists.data.length > 0 && config.path === state.lists.path && config.page === 1) {
            return
        }
        let cookies
        if (config.cookies) {
            cookies = config.cookies
            delete config.cookies
        }
        const { data: { data, code} } = await api.get('frontend/article/list', {...config, cache: true}, cookies)
        if (data && code === 200) {
            commit('receiveArticleList', {
                ...config,
                ...data,
            })
        }
    },
}

const mutations = {
    ['receiveArticleList'](state, {list, hasNext, hasPrev, page, path}) {
        if (page === 1) {
            list = [].concat(list)
        } else {
            list = state.lists.data.concat(list)
        }
        state.lists = {
            data: list, hasNext, hasPrev, page, path
        }
    },
}

const getters = {

}

export default {
    namespaced: true,
    state,
    actions,
    mutations,
    getters
}

這裏必定要注意, state 必定要用函數返回值來初始化 state, 否則會致使全部用戶共用 state

步驟5:

在 api 裏接收 cookies, 並加到 axios 的 headers 裏

import axios from 'axios'
import qs from 'qs'
import config from './config-server'

const parseCookie = cookies => {
    let cookie = ''
    Object.keys(cookies).forEach(item => {
        cookie+= item + '=' + cookies[item] + '; '
    })
    return cookie
}

export default {
    get(url, data, cookies = {}) {
        const cookie = parseCookie(cookies)
        return axios({
            method: 'get',
            url: config.api + url,
            data: qs.stringify(data),
            timeout: config.timeout,
            headers: {
                'X-Requested-With': 'XMLHttpRequest',
                'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
                cookie
            }
        })
    },
}

若是你沒有將 axios 從新封裝, 那麼也能夠把第五步省略, 直接在第四部把 cookies 給 axios 便可

方案 2 具體實例: https://github.com/lincenying...

方案 3 具體實例: https://github.com/lincenying...


綜上, 若是你項目不大, 仍是直接用方案 2 吧, 項目有不少頁面, 而且大部分頁面是每一個用戶都同樣的, 能夠考慮方案 3, 或者你有什麼更好的方法, 歡迎討論

Vue SSR 對須要 SEO, 而且每一個用戶看到的內容都是一致的, 配合緩存, 將是一個很是好的體驗...

相關文章
相關標籤/搜索