如何搭建能异步请求(Axios)数据的 Vue SEO 的 SS

2021-06-22  本文已影响0人  酷酷的凯先生

# 前言

咱们书接上回,之前写过一篇文章《教你如何搭建 Vue SEO 的 SSR》。这个主要针对的是纯静态的页面,可以这么做 SEO。在正常开发中,这几乎是不现实的,多多少少肯定会有异步请求数据的需求。
那该怎么实现呢,其实也很简单,就是在server入口entry-server.js那增加了asyncData()桥梁函数,使客户端和服务端能后相互传数据,接下来咱们一起见证奇迹。

# 直接上代码

第一步:安装插件 vue-server-renderer、webpack-node-externals、lodash.merge、cross-env

npm i --save-dev vue-server-renderer webpack-node-externals lodash.merge cross-env

第二步:更改路由导出

const router = new Router({
    mode: 'history',
    routes,
});

// ssr 输出
export default function createRouter() {
    return new Router({
        mode: 'history',
        routes,
    });
}

// 普通输出
// export default router;

第三步:更改Vuex导出

import Vue from "vue";
import Vuex from "vuex";
import { login, consultList } from "../apis"

Vue.use(Vuex);

const VueStore = {
    state: {
        userInfo: {},
        dataList: [],
        token: ''
    },
    mutations: {
        setdataList: (state, dataList) => {
            state.dataList = dataList;
        },
        SET_TOKEN: (state, token) => {
            state.token = token
        },
    },
    actions: {
        // 用户名登录 获得 token
        Login({ commit, state }, userInfo) {
            return new Promise((resolve, reject) => {
                login({
                    password: userInfo.pwd,
                    username: userInfo.unm
                }).then(response => {
                    console.log('response', response)
                    if (response.isSuccess) {
                        commit('SET_TOKEN', response.data.token)
                    }
                    resolve(response)
                }).catch(error => {
                    reject(error)
                })
            })
        },
        // 获得资讯列表
        consultList({ commit, state }, params) {
            return new Promise((resolve, reject) => {
                consultList(params).then(response => {
                    if (response.isSuccess) {
                        commit('setdataList', response.data.content)
                    }
                    resolve(response)
                }).catch(error => {
                    reject(error)
                })
            })
        }
    },
    modules: {},
};

// SSR 导出
export function createStore() {
    return new Vuex.Store(VueStore)
}
// 普通导出
// export default new Vuex.Store(VueStore)

第四步:在src根目录下创建 app.js

// 创建 vue 实例
import Vue from 'vue'
import App from './App.vue'
import createRouter from './router'
import { createStore } from './store'
import { sync } from 'vuex-router-sync'

export default function createApp() {
    // 创建 router 和 store 实例
    const router = createRouter();
    const store = createStore();

    // 同步路由状态(route state)到 store
    sync(store, router);

    // 创建应用程序实例,将 router 和 store 注入
    const app = new Vue({
        router,
        store,
        render: h => h(App)
    });

    // 暴露 app, router 和 store
    return { app, router, store }
}

第五步:在 src 目录下分别创建 entry-client.js、 entry-server.js

entry-client.js 文件 --------------------

// 挂载、激活app
import createApp from './app'

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 文件 --------------------
// 将来渲染首屏
import createApp from './app'

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(reject)
        }, reject)
    });
}

第六步:配置 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 merge = require("lodash.merge");
const TARGET_NODE = process.env.WEBPACK_TARGET === "node";
const target = TARGET_NODE ? "server" : "client";

module.exports = {
    css: {
        extract: false,
    },
    outputDir: "./dist/" + target,
    configureWebpack: () => {
        return ({
            entry: `/src/entry-${target}.js`,
            devtool: "source-map",
            target: TARGET_NODE ? "node" : "web",
            node: TARGET_NODE ? undefined : false,
            output: {
                libraryTarget: TARGET_NODE ? "commonjs2" : undefined
            },
            externals: TARGET_NODE ?
                nodeExternals({
                    // whitelist
                    allowlist: [/\.css$/]
                }) : undefined,
            optimization: {
                splitChunks: undefined
            },
            plugins: [TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()]
        })
    },
    chainWebpack: config => {
        config.module.rule("vue").use("vue-loader").tap(options => {
            merge(options, { optimizeSSR: false })
        })
    }
};

第七步:与 src 平级,创建 server 文件夹并创建 index.js 文件

const express = require('express');
const app = express();
const fs = require('fs')
const { createBundleRenderer } = require("vue-server-renderer");
const serverBundle = require("../dist/server/vue-ssr-server-bundle.json");
// 不写这个文件,则看不到生成的js文件,不能调用 methods 的方法
const clientManifest = require("../dist/client/vue-ssr-client-manifest.json");
const renderer = createBundleRenderer(serverBundle, {
    runInNewContext: false,
    template: fs.readFileSync("../public/index.temp.html", "utf-8"), // 宿主文件
    clientManifest
});

app.use(express.static('../dist/client', { index: false }));

app.get("*", async(req, res) => {
    try {
        const context = {
            url: req.url,
            title: 'test title'
        }

        const html = await renderer.renderToString(context);

        res.send(html)
    } catch (error) {
        console.log(error)
    }
})

app.listen(3000, () => {
    console.log('服务启动成功')
})

第八步:修改 packsge.json 文件

"scripts": {
        "serve": "cross-env WEBPACK_TARGET=dev vue-cli-service serve",
        "build:client": "vue-cli-service build",
        "build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build --mode server",
        "build": "npm run build:server && npm run build:client",
        "lint": "vue-cli-service lint"
}

第九步:在 public 文件夹下创建 index.temp.html

<!DOCTYPE html>
<html lang="">

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">

    <title>ssr</title>
</head>

<body>
    <!--vue-ssr-outlet-->
</body>

</html>

注意:body 里必须这么写

第十步:运行打包

npm run build 

注意: 这里打包会执行两次:客户端 和 服务端

打包后的结果:


image.png

目录总览:


image.png

到这里可以异步请求的 SSR 就创建完成了,在页面如何使用呢?咱们往下瞅

# 页面里请求数据

通过 更新状态 和 检测状态 来完成数据的请求与渲染

<template>
    <div class="home">
        <h1>This is an test page</h1>
        <div v-for="(item, key) in dataList" :key="key">
            <p v-for="(i,k) in item" :key="k">
                {{i}}
            </p>
        </div>
    </div>
</template>
<script>
export default {
    // 这里的参数 store 和 route 是我们在 entry-server.js 文件里暴露的
    // 可根据需要自行定义参数
    // 注意: asyncData 函数会在组件实例化之前调用,所以它无法访问 this
    asyncData ( { store, route } ) {
        return  store.dispatch( 'consultList', { pageSize: 1, pageNum: 100, level1: 1 } )
    },
    computed: {
        dataList () {
            return this.$store.state.dataList;
        }
    }
};
</script>

其他方法和之前 Vue 的写法一样,该怎么写怎么写。
问题: 只能通过状态来请求和渲染数据,能不能向普通的请求 一样,把数据放在 data 里然后进行渲染?
这里得注意一个问题: 页面一加载首先会执行 asyncData 函数,然后再去执行其他的钩子函数。
换句话说,只有在 asyncData 函数里请求的数据才会被爬虫爬到,在右击查看源码时能看到
手动触发的,比如触发搜索 或者 翻页时请求的数据则不会被爬虫爬到,右击查看源码也看不到
小伙伴们有什么好想法吗,欢迎评论。
小弟在此给你们抱拳了。

上一篇下一篇

猜你喜欢

热点阅读