項(xiàng)目簡(jiǎn)介
vue+node+koa2
安裝ssr依賴
npm install vue-server-renderer webpack-node-externals cross-env --save-dev
npm install koa koa-static vuex-router-sync --save
目錄結(jié)構(gòu)
其中[entry-client.js] [entry-server.js] [index.template.html] [server.js]為新增文件

目錄結(jié)構(gòu)
文件內(nèi)容
【entry-client.js】
import createApp from "./main";
const { app, router, store } = createApp(window);
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__);
}
router.onReady(() => {
// 添加路由鉤子函數(shù),用于處理 asyncData.
// 在初始路由 resolve 后執(zhí)行,
// 以便我們不會(huì)二次預(yù)取(double-fetch)已有的數(shù)據(jù)。
// 使用 `router.beforeResolve()`,以便確保所有異步組件都 resolve。
router.beforeResolve((to, from, next) => {
const matched = router.getMatchedComponents(to);
const prevMatched = router.getMatchedComponents(from);
// 我們只關(guān)心非預(yù)渲染的組件
// 所以我們對(duì)比它們,找出兩個(gè)匹配列表的差異組件
let diffed = false;
const activated = matched.filter((c, i) => {
return diffed || (diffed = prevMatched[i] !== c);
});
if (!activated.length) {
return next();
}
// 這里如果有加載指示器 (loading indicator),就觸發(fā)
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 "./main";
export default context => {
return new Promise((resolve, reject) => {
const { app, router, store } = createApp();
// 注入用戶信息
if(context.userInfo) {
store.state.userInfo = JSON.parse(context.userInfo)
store.state.token = store.state.userInfo.token
}
router.push(context.url);
router.onReady(() => {
const matchedComponents = router.getMatchedComponents();
if (!matchedComponents.length) {
return reject({ code: 404 });
}
// 對(duì)所有匹配的路由組件調(diào)用 `asyncData()`
Promise.all(
matchedComponents.map((Component) => {
if (Component.asyncData) {
return Component.asyncData({
store,
route: router.currentRoute
});
}
})
).then(() => {
// 在所有預(yù)取鉤子(preFetch hook) resolve 后,
// 我們的 store 現(xiàn)在已經(jīng)填充入渲染應(yīng)用程序所需的狀態(tài)。
// 當(dāng)我們將狀態(tài)附加到上下文,
// 并且 `template` 選項(xiàng)用于 renderer 時(shí),
// 狀態(tài)將自動(dòng)序列化為 `window.__INITIAL_STATE__`,并注入 HTML。
// 動(dòng)態(tài)TDK
context.title = store.state.title + ' - ' + store.state.globalConfig.public.seotitle;
context.keywords = store.state.globalConfig.public.keyword
context.description = store.state.globalConfig.public.description
context.state = store.state
resolve(app);
})
.catch(reject);
}, reject);
});
};
【index.template.html】
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="keywords" content="{{keywords}}">
<meta name="description" content="{{description}}">
<title>{{title}}</title>
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
【server.js】
const fs = require("fs");
const Koa = require("koa");
const path = require("path");
const koaStatic = require("koa-static");
const app = new Koa();
const resolve = (file) => path.resolve(__dirname, file);
// 開放dist目錄
app.use(koaStatic(resolve("./dist/client")));
// 第 2 步:獲得一個(gè)createBundleRenderer
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 renderer = createBundleRenderer(serverBundle, {
runInNewContext: false,
template: fs.readFileSync(
path.resolve(__dirname, "./src/index.template.html"),
"utf-8"
),
clientManifest,
});
function renderToString(context) {
return new Promise((resolve, reject) => {
renderer.renderToString(context, (err, html) => {
err ? reject(err) : resolve(html);
});
});
}
function getCookie(cookie) {
let cookieObj = {}
let cookies = cookie ? cookie.split(';') : []
if (cookies.length > 0) {
cookies.forEach(item => {
if (item) {
let cookieArray = item.split('=')
if (cookieArray && cookieArray.length > 0) {
let key = cookieArray[0].trim()
let value = cookieArray[1] ? cookieArray[1].trim() : undefined
cookieObj[key] = value
}
}
})
}
return cookieObj
}
// 第 3 步:添加一個(gè)中間件來處理所有請(qǐng)求
app.use(async (ctx, next) => {
const context = {
title: "默認(rèn)title",
url: ctx.url,
};
// cgi請(qǐng)求,前端資源請(qǐng)求不能轉(zhuǎn)到這里來。這里可以通過nginx做
if (/\.\w+$/.test(context.url)) {
return next
}
const cookieObj = getCookie(ctx.header.cookie)
if(cookieObj.userInfo) {
context.userInfo = decodeURIComponent(cookieObj.userInfo)
}
// 將 context 數(shù)據(jù)渲染為 HTML
const html = await renderToString(context);
ctx.body = html;
});
/*服務(wù)啟動(dòng)*/
const port = 3000;
app.listen(port, function() {
console.log(`server started at localhost:${port}`);
})
其中[store.js] [router.js] [main.js] [action.js] [vue.config.js] [package.json]需要進(jìn)行修改
文件修改內(nèi)容
【store.js】
import Vue from 'vue'
import Vuex from 'vuex'
import mutations from "./vuex/mutations"
import actions from './vuex/actions'
Vue.use(Vuex)
export default function createStore() {
return new Vuex.Store({
state: {
token: null,
userInfo: {},
homeData: {},
title: ''
},
mutations,
actions
})
}
【router.js】
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
const routes = [
{
path: '/',
component: () => import('./views/index'),
children: [
{
path: '/',
name: 'index',
component: () => import('./views/home')
}
{
path: 'login',
name: 'login',
component: () => import('./views/login')
},
{
path: 'user',
name: 'user',
component: () => import('./views/user')
}
]
}
]
export default function createRouter() {
return new Router({
mode: 'history',
base: process.env.BASE_URL,
routes,
scrollBehavior (to, from, savedPosition) {
return { x: 0, y: 0 }
}
})
}
【main.js】
import Vue from 'vue'
import App from './App.vue'
import './assets/less/base.less'
import createRouter from "./router";
import createStore from "./store";
import { sync } from "vuex-router-sync"
Vue.config.productionTip = false
Vue.prototype.$routerOpen = (page) => {
const router = createRouter()
let routeUrl = router.resolve(page)
// window.open(routeUrl.href, '_blank')
window.location.href = routeUrl.href
}
export default function createApp(window) {
// 創(chuàng)建 router 和 store 實(shí)例
const router = createRouter();
const store = createStore(window);
// 同步路由狀態(tài)(route state)到 store
sync(store, router);
const app = new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
return { app, router, store };
}
【action.js】
import { homeCase, homeHotProjects, homeHotState, newslist } from '@/api/request'
const actions = {
getHomeData({ commit }) {
return Promise.all([homeCase(), homeHotProjects(), homeHotState(), newslist()]).then(res => {
commit('getHomeData', res)
})
}
}
export default actions
【vue.config.js】
/*
* @Author: your name
* @Date: 2020-07-01 17:17:36
* @LastEditTime: 2020-12-24 11:09:24
* @LastEditors: Please set LastEditors
* @Description: In User Settings Edit
* @FilePath: \immigrant\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";
module.exports = {
publicPath: './',
lintOnSave: false, //是否開啟eslint
devServer: {
disableHostCheck: true,
proxy: {
'/localhost': {
target: 'http://xxx.com', //API服務(wù)器的地址
changeOrigin: true, // 虛擬的站點(diǎn)需要更管origin
pathRewrite:{
'^/localhost':''
}
}
},
},
outputDir: `dist/${env.RUN_ENV}`,
configureWebpack: {
// 將 entry 指向應(yīng)用程序的 server / client 文件
entry: `./src/entry-${env.RUN_ENV}.js`,
devtool: "eval",
// 這允許 webpack 以 Node 適用方式(Node-appropriate fashion)處理動(dòng)態(tài)導(dǎo)入(dynamic import),
// 并且還會(huì)在編譯 Vue 組件時(shí),
// 告知 `vue-loader` 輸送面向服務(wù)器代碼(server-oriented code)。
target: isServer ? "node" : "web",
// 此處告知 server bundle 使用 Node 風(fēng)格導(dǎo)出模塊(Node-style exports)
output: {
libraryTarget: isServer ? "commonjs2" : undefined,
},
// https://webpack.js.org/configuration/externals/#function
// https://github.com/liady/webpack-node-externals
// 外置化應(yīng)用程序依賴模塊。可以使服務(wù)器構(gòu)建速度更快,
// 并生成較小的 bundle 文件。
externals: isServer
? nodeExternals({
// 不要外置化 webpack 需要處理的依賴模塊。
// 你可以在這里添加更多的文件類型。例如,未處理 *.vue 原始文件,
// 你還應(yīng)該將修改 `global`(例如 polyfill)的依賴模塊列入白名單
allowlist: /\.css$/,
})
: undefined,
optimization: { splitChunks: isServer ? false : undefined },
// 這是將服務(wù)器的整個(gè)輸出
// 構(gòu)建為單個(gè) JSON 文件的插件。
// 服務(wù)端默認(rèn)文件名為 `vue-ssr-server-bundle.json`
// 客戶端默認(rèn)文件名為 `vue-ssr-client-manifest.json`
plugins: [isServer ? new VueSSRServerPlugin() : new VueSSRClientPlugin()],
}
}
【package.json】
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"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 --mode server",
"service": "node server.js"
}
組件中獲取數(shù)據(jù)方式 (homeData可直接用于頁面數(shù)據(jù)渲染)
export default {
data() {
return {}
},
asyncData({store, route}) {
return Promise.all([
store.dispatch("getHomeData")
])
},
computed: {
homeData() {
return this.$store.state.homeData
}
}
}
運(yùn)行
npm run start(編譯加運(yùn)行起服務(wù))
npm run service(單獨(dú)運(yùn)行起服務(wù))
注意
本文章本用于作者筆記,所以非常之簡(jiǎn)陋,如遇問題或有疑問可一起討論研究。
運(yùn)行問題
(1)編譯失敗提示依賴庫(kù)版本不一致,根據(jù)報(bào)錯(cuò)提示重新安裝依賴保證版本一致即可