美文网首页
如何搭建能异步请求(Axios)数据的 Vue SEO 的 SS

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

作者: 酷酷的凯先生 | 来源:发表于2021-06-22 11:56 被阅读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 函数里请求的数据才会被爬虫爬到,在右击查看源码时能看到
    手动触发的,比如触发搜索 或者 翻页时请求的数据则不会被爬虫爬到,右击查看源码也看不到
    小伙伴们有什么好想法吗,欢迎评论。
    小弟在此给你们抱拳了。

    相关文章

      网友评论

          本文标题:如何搭建能异步请求(Axios)数据的 Vue SEO 的 SS

          本文链接:https://www.haomeiwen.com/subject/xpppyltx.html