如何搭建能异步请求(Axios)数据的 Vue SEO 的 SS
# 前言
咱们书接上回,之前写过一篇文章《教你如何搭建 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
函数里请求的数据才会被爬虫爬到,在右击查看源码时能看到
。
手动触发的,比如触发搜索 或者 翻页时请求的数据则不会被爬虫爬到,右击查看源码也看不到
。
小伙伴们有什么好想法吗,欢迎评论。
小弟在此给你们抱拳了。