關(guān)于VueSSR的一些理解和詳細(xì)配置
如果是靜態(tài)頁面,例如官網(wǎng)的SSR處理可直接使用prerender-spa-plugin插件實(shí)現(xiàn)預(yù)渲染,參考我之前的博客:vue單頁面通過prerender-spa-plugin插件進(jìn)行SEO優(yōu)化
以下是基于vue-cli@2.x生成的工程相關(guān)的結(jié)構(gòu)改造。文章最后有異步請(qǐng)求的例子可供參考。
概念
流程圖
這是具體的流程圖,如何實(shí)現(xiàn),后續(xù)配置會(huì)詳解

編譯圖解
結(jié)合上面的流程圖來理解編譯的過程圖,因?yàn)榉?wù)端渲染只是一個(gè)可以等待異步數(shù)據(jù)的預(yù)渲染,最終用戶交互還是需要Client entry生成的js來控制,這就是為什么需要兩個(gè)entry文件,但是擁有同樣的入口(app.js)的原因。
串聯(lián)server和client的樞紐就是store,server將預(yù)渲染頁面的sotre數(shù)據(jù)放入window全局中,client再進(jìn)行數(shù)據(jù)同步,完成異步數(shù)據(jù)預(yù)渲染

相關(guān)配置
知道大致步驟和流程,再去理解VueSSR的配置就不會(huì)那么突兀了。
注意:路由如果用懶加載會(huì)出現(xiàn)vue模板里面的樣式?jīng)]辦法抽離到css中,而是用js渲染,瀏覽器會(huì)出現(xiàn)一個(gè)沒有樣式到最終頁面的空白期。由于SSR渲染做了沒有SPA首屏渲染問題,所以不用懶加載也沒事。
目錄結(jié)構(gòu)

index.html 的改動(dòng)
需要加入<!–vue-ssr-outlet–>占位符
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="renderer" content="webkit">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" href="" type="image/x-icon" />
<title>{{title}}</title>
</head>
<body>
<div id="app">
<!--vue-ssr-outlet-->
</div>
</body>
</html>需要安裝的依賴
多了一些相關(guān)的依賴需要添加一下,這里做一個(gè)匯總:
npm install memory-fs chokidar vue-server-renderer@2.5.17 lru-cache serve-favicon compression route-cache vuex-router-sync --save
注意: vue-server-renderer要和工程的vue版本一致。
server.js
開發(fā)模式會(huì)調(diào)用setup-dev-server中的熱更新插件,實(shí)時(shí)編譯
const fs = require('fs'); //讀取文件
const path = require('path');
const express = require('express');
const app= express();
const LRU = require('lru-cache'); //封裝緩存的get set方法
/*
* 處理favicon.ico文件:作用:
* 1. 去除這些多余無用的日志
* 2. 將icon緩存在內(nèi)存中,防止從因盤中重復(fù)讀取
* 3. 提供了基于icon 的 ETag 屬性,而不是通過文件信息來更新緩存
* 4. 使用最兼容的Content-Type處理請(qǐng)求
*/
const favicon = require('serve-favicon');
const compression = require('compression'); //壓縮
const microcache = require('route-cache'); //請(qǐng)求緩存
const resolve = (file) => path.resolve(__dirname, file); //返回絕對(duì)路徑
const {createBundleRenderer} = require('vue-server-renderer');
const isProd = process.env.NODE_ENV === 'production';
const useMicroCache = process.env.MICRO_CACHE !== 'false';
const serverInfo =
`express/${require('express/package').version}` +
`vue-server-renderer/${require('vue-server-renderer/package').version}`;
let renderer;
let readyPromise;
//生成renderer函數(shù)
function createRenderer(bundle, options) {
return createBundleRenderer(bundle, Object.assign(options, {
cache: new LRU({
max: 1000,
maxAge: 1000 * 60 * 15
}),
basedir: resolve('./dist'),
runInNewContext: false
}));
}
function render(req, res) {
const s = Date.now();
res.setHeader('Content-type', 'text/html');
res.setHeader('Server', serverInfo);
const handleError = err => {
if (err.url) {
res.redirect(err.url)
} else if(err.code === 404) {
res.status(404).send('404 | Page Not Found')
} else {
// Render Error Page or Redirect
res.status(500).send('500 | Internal Server Error')
console.error(`error during render : ${req.url}`)
console.error(err.stack)
}
};
const context = {
title: 'ssr標(biāo)題',
url: req.url
};
renderer.renderToString(context, (err, html) => {
console.log(err);
if (err) {
return handleError(err);
}
res.send(html);
if (!isProd) {
console.log(`whole request: ${Date.now() - s}ms`);
}
})
}
const templatePath = resolve('./index.html');
if (isProd) {
const template = fs.readFileSync(templatePath, 'utf-8');
const bundle = require('./dist/vue-ssr-server-bundle.json');
const clientManifest = require('./dist/vue-ssr-client-manifest.json');
renderer = createRenderer(bundle, {
template,
clientManifest
})
} else {
readyPromise = require('./build/setup-dev-server')(
app,
templatePath,
(bundle, options) => {
renderer = createRenderer(bundle, options)
}
)
}
const serve = (path, cache) => express.static(resolve(path), {
maxAge: cache && isProd ? 1000 * 60 * 60 * 24 * 30 : 0
});
//靜態(tài)文件壓縮,支持gzip和deflate方式(原來這步是nginx做的),threshold: 0, 0kb以上的都?jí)嚎s,即所有文件都?jí)嚎s,
//可通過filter過濾
//TODO
app.use(compression({threshold: 0}));
app.use(favicon('./favicon.ico'));
app.use('/dist', serve('./dist', true));
app.use('/static', serve('./static', true));
app.use('/service-worker.js', serve('./dist/service-worker.js', true));
//TODO
app.use(microcache.cacheSeconds(1, req => useMicroCache && req.originalUrl));
app.get('*', isProd ? render : (req, res) => {
readyPromise.then(() => render(req, res));
});
// 監(jiān)聽
app.listen(8082, function () {
console.log('success listen...8082');
});
entry-server.js
在路由resolve之前,做數(shù)據(jù)預(yù)渲染
import { createApp } from './app'
const isDev = process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'sit';
// This exported function will be called by `bundleRenderer`.
// This is where we perform data-prefetching to determine the
// state of our application before actually rendering it.
// Since data fetching is async, this function is expected to
// return a Promise that resolves to the app instance.
export default context => {
return new Promise((resolve, reject) => {
const s = isDev && Date.now();
const { app, router, store } = createApp();
const { url } = context;
const { fullPath } = router.resolve(url).route;
if (fullPath !== url) {
return reject({ url: fullPath })
}
// set router's location
router.push(url);
// wait until router has resolved possible async hooks
router.onReady(() => {
const matchedComponents = router.getMatchedComponents()
// no matched routes
if (!matchedComponents.length) {
return reject({ code: 404 })
}
// Call fetchData hooks on components matched by the route.
// A preFetch hook dispatches a store action and returns a Promise,
// which is resolved when the action is complete and store state has been
// updated.
Promise.all(matchedComponents.map(({ asyncData }) => asyncData && asyncData({
store,
route: router.currentRoute
}))).then(() => {
isDev && console.log(`data pre-fetch: ${Date.now() - s}ms`)
// After all preFetch hooks are resolved, our store is now
// filled with the state needed to render the app.
// Expose the state on the render context, and let the request handler
// inline the state in the HTML response. This allows the client-side
// store to pick-up the server-side state without having to duplicate
// the initial data fetching on the client.
context.state = store.state;
resolve(app)
}).catch(reject)
}, reject)
})
}
entery-client.js
window.INITIAL_STATE 就是服務(wù)端存在html中的store數(shù)據(jù),客戶端做一次同步,router.onReady在第一次不會(huì)觸發(fā),只有router接管頁面之后才會(huì)觸發(fā),在beforeResolved中手動(dòng)進(jìn)行數(shù)據(jù)請(qǐng)求(否則asyncData中的請(qǐng)求不會(huì)觸發(fā))
import {createApp} from './app';
const {app, router, store} = createApp();
// prime the store with server-initialized state.
// the state is determined during SSR and inlined in the page markup.
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__)
}
router.onReady(() => {
// Add router hook for handling asyncData.
// Doing it after initial route is resolved so that we don't double-fetch
// the data that we already have. Using router.beforeResolve() so that all
// async components are resolved.
router.beforeResolve((to, from, next) => {
const matched = router.getMatchedComponents(to);
const prevMatched = router.getMatchedComponents(from);
let diffed = false;
//只需匹配和上一個(gè)路由不同的路由,共用的路由因?yàn)橐呀?jīng)渲染過了,不需要再處理
const activated = matched.filter((c, i) => {
return diffed || (diffed = (prevMatched[i] !== c))
});
const asyncDataHooks = activated.map(c => c.asyncData).filter(_ => _);
if (!asyncDataHooks.length) {
console.log('there no client async');
return next()
}
// bar.start()
console.log('client async begin');
Promise.all(asyncDataHooks.map(hook => hook({ store, route: to }))).then(() => {
// bar.finish()
console.log('client async finish');
next()
}).catch(next)
});
//以激活模式掛載,不會(huì)改變?yōu)g覽器已經(jīng)渲染的內(nèi)容
app.$mount('#app');
});
// service worker
if ('https:' === location.protocol && navigator.serviceWorker) {
navigator.serviceWorker.register('/service-worker.js')
}
app.js
import Vue from 'vue';
import App from './App.vue';
import {createStore} from './store';
import {createRouter} from './router';
import {sync} from 'vuex-router-sync';
export function createApp() {
const store = createStore();
const router = createRouter();
// sync the router with the vuex store.
// this registers `store.state.route`
sync(store, router);
// create the app instance.
// here we inject the router, store and ssr context to all child components,
// making them available everywhere as `this.$router` and `this.$store`.
const app = new Vue({
router,
store,
render: h => h(App)
});
// expose the app, the router and the store.
// note we are not mounting the app here, since bootstrapping will be
// different depending on whether we are in a browser or on the server.
return {app, router, store}
}router.js 和 store.js
防止數(shù)據(jù)污染,每次都要?jiǎng)?chuàng)造新的實(shí)例
注意:路由如果用懶加載會(huì)出現(xiàn)vue模板里面的樣式?jīng)]辦法抽離到css中,而是用js渲染,瀏覽器會(huì)出現(xiàn)一個(gè)沒有樣式到最終頁面的空白期。由于SSR渲染做了沒有SPA首屏渲染問題,所以不用懶加載也沒事。
// store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export function createStore () {
return new Vuex.Store({
state: {
token,
},
mutations: {},
actions: {}
})
}
//router.js
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
import Table from '@/views/Table';
import page1 from '@/views/page1';
export const constantRouterMap = [
{ path: '/', component: Table, hidden: true },
{ path: '/page1', component: page1, hidden: true }
];
export function createRouter() {
return new Router({
mode: 'history',
fallback: false, // 設(shè)置瀏覽器不支持history.pushState時(shí),不回退
linkActiveClass: 'open active',
scrollBehavior: () => ({ y: 0 }),
routes: constantRouterMap
})
}
setup-dev-server.js
const fs = require('fs');
const path = require('path');
//文件處理工具
const MFS = require('memory-fs');
const webpack = require('webpack');
//對(duì)fs.watch的包裝,優(yōu)化fs,watch原來的功能
const chokidar = require('chokidar');
const clientConfig = require('./webpack.client.config');
const serverConfig = require('./webpack.server.config');
const readFile = (fs, file) => {
try {
return fs.readFileSync(path.join(clientConfig.output.path, file), 'utf-8');
} catch (e) {
}
};
module.exports = function setupDevServer(app, templatePath, cb) {
let bundle;
let template;
let clientManifest;
let ready;
const readyPromise = new Promise(r => ready = r);
//1. 生成新的renderer函數(shù); 2. renderer.renderToString();
const update = () => {
if (bundle && clientManifest) {
//執(zhí)行server.js中的render函數(shù),但是是異步的
ready();
cb(bundle, {
template,
clientManifest
})
}
};
template = fs.readFileSync(templatePath, 'utf-8');
//模板改了之后刷新 TODO
chokidar.watch(templatePath).on('change', () => {
template = fs.readFileSync(templatePath, 'utf-8');
console.log('index.html template updated');
update();
});
clientConfig.entry.app = ['webpack-hot-middleware/client', clientConfig.entry.app];
clientConfig.plugins.push(
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin()
);
const clientComplier = webpack(clientConfig);
const devMiddleware = require('webpack-dev-middleware')(clientComplier, {
publicPath: clientConfig.output.publicPath,
noInfo: true
});
app.use(devMiddleware);
clientComplier.plugin('done', stats => {
stats = stats.toJson();
stats.errors.forEach(err => console.log(err));
stats.warnings.forEach(err => console.log(err));
if (stats.errors.length) return;
clientManifest = JSON.parse(readFile(
devMiddleware.fileSystem,
'vue-ssr-client-manifest.json'
));
update();
});
app.use(require('webpack-hot-middleware')(clientComplier, {heartbeat: 5000}));
const serverCompiler = webpack(serverConfig);
const mfs = new MFS();
serverCompiler.outputFileSystem = mfs;
//監(jiān)聽server文件修改 TODO
serverCompiler.watch({}, (err, stats) => {
if (err) throw err;
stats = stats.toJson();
if (stats.errors.length) return;
// read bundle generated by vue-ssr-webpack-plugin
bundle = JSON.parse(readFile(mfs, 'vue-ssr-server-bundle.json'));
update();
});
return readyPromise;
};
webpack.base.config.js
vue-loader.conf就是vue腳手架自動(dòng)生成的文件,就不再貼出了
const path = require('path');
var utils = require('./utils');
var config = require('../config');
var vueLoaderConfig = require('./vue-loader.conf');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin');
var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin');
var CopyWebpackPlugin = require('copy-webpack-plugin')
function resolve(dir) {
return path.join(__dirname, '..', dir)
}
const isProd = process.env.NODE_ENV === 'production';
module.exports = {
devtool: isProd
? false
: '#cheap-module-source-map',
output: {
path: path.resolve(__dirname, '../dist'),
publicPath: '/dist/',
filename: isProd ? utils.assetsPath('js/[name].[chunkhash].js') : utils.assetsPath('[name].js'),
chunkFilename: isProd ? utils.assetsPath('js/[id].[chunkhash].js') : utils.assetsPath('[id].js')
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('client'),
'src': path.resolve(__dirname, '../client'),
'assets': path.resolve(__dirname, '../client/assets'),
'components': path.resolve(__dirname, '../client/components'),
'views': path.resolve(__dirname, '../client/views'),
'api': path.resolve(__dirname, '../client/api'),
'utils': path.resolve(__dirname, '../client/utils'),
'router': path.resolve(__dirname, '../client/router'),
'vendor': path.resolve(__dirname, '../client/vendor'),
'static': path.resolve(__dirname, '../static'),
}
},
externals: {
jquery: 'jQuery'
},
module: {
rules: [
// {
// test: /\.(js|vue)$/,
// loader: 'eslint-loader',
// enforce: "pre",
// include: [resolve('src'), resolve('test')],
// options: {
// formatter: require('eslint-friendly-formatter')
// }
// },
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
{
test: /\.js$/,
loader: 'babel-loader?cacheDirectory',
include: [resolve('client'), resolve('test')]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
query: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
query: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[ext]')
}
},
...utils.styleLoaders({sourceMap: config.dev.cssSourceMap})
]
},
plugins: isProd
? [
// new webpack.optimize.ModuleConcatenationPlugin(),
new ExtractTextPlugin({
filename: 'common.[chunkhash].css'
}),
// Compress extracted CSS. We are using this plugin so that possible
// duplicated CSS from different components can be deduped.
new OptimizeCSSPlugin(),
// copy custom static assets
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.build.assetsSubDirectory,
ignore: ['.*']
}
]),
]
: [
new FriendlyErrorsPlugin(),
// copy custom static assets
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.build.assetsSubDirectory,
ignore: ['.*']
}
]),
]
};
webpack.server.config.js
const webpack = require('webpack');
const merge = require('webpack-merge');
const config = require('../config');
const baseConfig = require('./webpack.base.config');
// const nodeExternals = require('webpack-node-externals')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin');
let env, NODE_ENV = process.env.NODE_ENV;
if (NODE_ENV === 'development') {
env = config.dev.env;
} else if (NODE_ENV === 'production') {
env = config.build.prodEnv;
} else {
env = config.build.sitEnv;
}
module.exports = merge(baseConfig, {
target: 'node',
devtool: '#source-map',
entry: './client/entry-server.js',
output: {
filename: 'server-bundle.js',
libraryTarget: 'commonjs2'
},
plugins: [
new webpack.DefinePlugin({
'process.env': env,
'process.env.VUE_ENV': '"server"',
}),
new VueSSRServerPlugin()
]
});
webpack.client.config.js
const webpack = require('webpack');
const merge = require('webpack-merge');
const config = require('../config');
const utils = require('./utils');
const base = require('./webpack.base.config');
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin');
// TODO
// const SWPrecachePlugin = require('sw-precache-webpack-plugin');
let env, NODE_ENV = process.env.NODE_ENV;
if (NODE_ENV === 'development') {
env = config.dev.env;
} else if (NODE_ENV === 'production') {
env = config.build.prodEnv;
} else {
env = config.build.sitEnv;
}
module.exports = merge(base, {
entry: {
app: './client/entry-client.js'
},
plugins:
NODE_ENV !== 'development' ? [
new webpack.DefinePlugin({
'process.env': env,
'process.env.VUE_ENV': '"client"'
}),
new webpack.optimize.UglifyJsPlugin({
compress: {warnings: false}
}),
// extract vendor chunks for better caching
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function (module) {
// a module is extracted into the vendor chunk if...
return (
// it's inside node_modules
/node_modules/.test(module.context) &&
// and not a CSS file (due to extract-text-webpack-plugin limitation)
!/\.css$/.test(module.request)
)
}
}),
// extract webpack runtime & manifest to avoid vendor chunk hash changing
// on every build.
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest'
}),
new VueSSRClientPlugin(),
] : [
new webpack.DefinePlugin({
'process.env': env,
'process.env.VUE_ENV': '"client"'
}),
new VueSSRClientPlugin(),
],
});
啟動(dòng)命令
- 開發(fā)模式:npm run dev
- 生產(chǎn)模式:npm run build & npm run start
"scripts": {
"dev": "cross-env NODE_ENV=development supervisor server/app.js",
"start": "cross-env NODE_ENV=production node server/app.js",
"build": "npm run build:client && npm run build:server",
"build:client": "cross-env NODE_ENV=production webpack --config build/webpack.client.config.js",
"build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.config.js"
},異步處理例子
store.js
//store.js
import {TableRequest} from '@/api/Table';
export default {
state: {
userName: ''
},
mutations: {
GETUSERNAME(state, data) {
state.userName = data;
}
},
actions: {
GetUserName({commit}) {
return TableRequest().then(res => {
commit('GETUSERNAME', res.data.content);
})
}
}
}
// api.js 請(qǐng)求可以用node做一個(gè)
export function GetUserName (user, password) {
const data = {
user,
password
}
return fetch({
url: '/apis/getUserName',
data
})
}
vue頁面
<template>
? ? <div>
? ? ? ? {{$store.state.userName}}
? ? </div>
</template><script>
? ? export default {
? ? ? ? name: 'APP',
? ? ? ? asyncData({store, route}) {
? ? ? ? ? ? return ?store.dispatch('fetchItem', route.params.id);
? ? ? ? },
? ? }
</script>server.js添加請(qǐng)求處理
const fs = require('fs'); //讀取文件
const path = require('path');
const express = require('express');
const app= express();
const LRU = require('lru-cache'); //封裝緩存的get set方法
/*
* 處理favicon.ico文件:作用:
* 1. 去除這些多余無用的日志
* 2. 將icon緩存在內(nèi)存中,防止從因盤中重復(fù)讀取
* 3. 提供了基于icon 的 ETag 屬性,而不是通過文件信息來更新緩存
* 4. 使用最兼容的Content-Type處理請(qǐng)求
*/
const favicon = require('serve-favicon');
const compression = require('compression'); //壓縮
const microcache = require('route-cache'); //請(qǐng)求緩存
const resolve = (file) => path.resolve(__dirname, file); //返回絕對(duì)路徑
const {createBundleRenderer} = require('vue-server-renderer');
const isProd = process.env.NODE_ENV === 'production';
const useMicroCache = process.env.MICRO_CACHE !== 'false';
const serverInfo =
`express/${require('express/package').version}` +
`vue-server-renderer/${require('vue-server-renderer/package').version}`;
let renderer;
let readyPromise;
//生成renderer函數(shù)
function createRenderer(bundle, options) {
return createBundleRenderer(bundle, Object.assign(options, {
cache: new LRU({
max: 1000,
maxAge: 1000 * 60 * 15
}),
basedir: resolve('./dist'),
runInNewContext: false
}));
}
function render(req, res) {
const s = Date.now();
res.setHeader('Content-type', 'text/html');
res.setHeader('Server', serverInfo);
const handleError = err => {
if (err.url) {
res.redirect(err.url)
} else if(err.code === 404) {
res.status(404).send('404 | Page Not Found')
} else {
// Render Error Page or Redirect
res.status(500).send('500 | Internal Server Error')
console.error(`error during render : ${req.url}`)
console.error(err.stack)
}
};
const context = {
title: 'ssr標(biāo)題',
url: req.url
};
renderer.renderToString(context, (err, html) => {
console.log(err);
if (err) {
return handleError(err);
}
res.send(html);
if (!isProd) {
console.log(`whole request: ${Date.now() - s}ms`);
}
})
}
const templatePath = resolve('./index.html');
if (isProd) {
const template = fs.readFileSync(templatePath, 'utf-8');
const bundle = require('./dist/vue-ssr-server-bundle.json');
const clientManifest = require('./dist/vue-ssr-client-manifest.json');
renderer = createRenderer(bundle, {
template,
clientManifest
})
} else {
readyPromise = require('./build/setup-dev-server')(
app,
templatePath,
(bundle, options) => {
renderer = createRenderer(bundle, options)
}
)
}
const serve = (path, cache) => express.static(resolve(path), {
maxAge: cache && isProd ? 1000 * 60 * 60 * 24 * 30 : 0
});
/**
* 開始
* 測(cè)試數(shù)據(jù)請(qǐng)求的配置,可刪
**/
app.get('/getName', function (req, res, next) {
res.json({
code: 200,
content: '我是userName',
msg: '請(qǐng)求成功'
})
});
/**
* 結(jié)束
* 測(cè)試數(shù)據(jù)請(qǐng)求的配置,可刪
**/
//靜態(tài)文件壓縮,支持gzip和deflate方式(原來這步是nginx做的),threshold: 0, 0kb以上的都?jí)嚎s,即所有文件都?jí)嚎s,
//可通過filter過濾
//TODO
app.use(compression({threshold: 0}));
app.use(favicon('./favicon.ico'));
app.use('/dist', serve('./dist', true));
app.use('/static', serve('./static', true));
app.use('/service-worker.js', serve('./dist/service-worker.js', true));
//TODO
app.use(microcache.cacheSeconds(1, req => useMicroCache && req.originalUrl));
app.get('*', isProd ? render : (req, res) => {
readyPromise.then(() => render(req, res));
});
// 監(jiān)聽
app.listen(8082, function () {
console.log('success listen...8082');
});
結(jié)尾
以上就是結(jié)合vue-cli改造的一個(gè)ssr框架,流程和原理理解了之后可自行改造相關(guān)配置。希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
- Egg Vue SSR 服務(wù)端渲染數(shù)據(jù)請(qǐng)求與asyncData
- 15分鐘學(xué)會(huì)vue項(xiàng)目改造成SSR(小白教程)
- vue ssr+koa2構(gòu)建服務(wù)端渲染的示例代碼
- Vue SSR 即時(shí)編譯技術(shù)的實(shí)現(xiàn)
- Vue使用預(yù)渲染代替SSR的方法
- 如何構(gòu)建 vue-ssr 項(xiàng)目的方法步驟
- vuecli項(xiàng)目構(gòu)建SSR服務(wù)端渲染的實(shí)現(xiàn)
- vue的ssr服務(wù)端渲染示例詳解
- vue中vue-router的使用說明(包括在ssr中的使用)
- Vue.js?狀態(tài)管理及?SSR解析
相關(guān)文章
vue.js實(shí)現(xiàn)只能輸入數(shù)字的輸入框
這篇文章主要為大家詳細(xì)介紹了vue.js實(shí)現(xiàn)只能輸入數(shù)字的輸入框,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-10-10
vue+element table表格實(shí)現(xiàn)動(dòng)態(tài)列篩選的示例代碼
這篇文章主要介紹了vue+element table表格實(shí)現(xiàn)動(dòng)態(tài)列篩選的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01
vue自適應(yīng)布局postcss-px2rem詳解
這篇文章主要介紹了vue自適應(yīng)布局(postcss-px2rem)的相關(guān)知識(shí),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2022-05-05

