创建两份webpack配置文件,一份为客户端运行文件,一份服务端运行文件 同时我们也需要创建两个入口文件,一个给客户端,一个给服务端。
webpack.server.conf.js
打包服务端代码
js target:'node',
output:{
libraryTarget:'commonjs2'
}
不使用自己生成的server.bundle.js,使用client.bundle.js文件。客户端激活server.bundle.js会在开启node服务时被用到
jsnew HtmlWebpackPlugin({
template: path.resolve(__dirname,'../index.ssr.html'),
filename:'index.ssr.html',
files:{
js:'client.bundle.js'
},
excludeChunks:['server']
})
以函数形式导出,每次服务端调用时执行函数,生成一份全新的app
jsimport Vue from 'vue'
import App from './App.vue'
export default ()=> {
const app=new Vue({
el:'#app',
render:h=>h(App)
})
return {
app
}
}
<!--vue-ssr-outlet-->
的作用是作为一个占位符,后续通过vue-server-renderer插件,将服务器解析出的组件html字符串插入到这里。
html<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>服务端渲染</title>
</head>
<body>
<!--vue-ssr-outlet-->
<script type="text/javascript" src="<%= htmlWebpackPlugin.options.files.js %>"></script>
</body>
</html>
jscreateBundleRenderer(serverBundle, { /* 选项 */ })
// serverBundle为webpack编译过后的代码
let renderer=createBundleRenderer(bundle,{ // 创建renderer,此时renderer会有两个值,renderToString和renderToStream
template:fs.readFileSync(path.resolve(__dirname,'../dist/index.ssr.html'),'utf-8')
})
renderer.renderToString((err,html)=>{ // 返回编译过后的代码
console.log(html)
ctx.status =200
ctx.body=html
})
完整代码:
jsconst Koa = require('koa');
const Router = require('koa-router');
const serve = require('koa-static');
const path = require('path');
const fs = require('fs');
const { createBundleRenderer } = require('vue-server-renderer')
const backendApp = new Koa();
const backendRouter = new Router();
let bundle=fs.readFileSync(path.resolve(__dirname,'../dist/server.bundle.js'),'utf-8')
let renderer=createBundleRenderer(bundle,{
template:fs.readFileSync(path.resolve(__dirname,'../dist/index.ssr.html'),'utf-8')
})
backendRouter.get('*',(ctx,next)=>{
renderer.renderToString((err,html)=>{
if (err) {
console.error(err);
ctx.status = 500;
ctx.body = '服务器内部错误';
} else {
ctx.body = html;
}
})
})
backendApp .use(serve(path.resolve(__dirname,'../dist')))
backendApp .use(backendRouter.routes()).use(backendRouter.allowedMethods())
backendApp.listen(3000,()=>{
console.log('服务器端渲染地址: http://localhost:3000');
})
思路:
在渲染前,要预先获取所有需要的异步数据,然后存到Vuex的store中。
在后端渲染时,通过Vuex将获取到的数据注入到相应组件中。
把store中的数据设置到window.__INITIAL_STATE__属性中。
在浏览器环境中,通过Vuex将window.__INITIAL_STATE__里面的数据注入到相应组件中。
js if (typeof window !== 'undefined' && window.__INITIAL_STATE__) {
console.log('window.__INITIAL_STATE__', window.__INITIAL_STATE__);
store.replaceState(window.__INITIAL_STATE__);
}
jsfetchInitialData 执行后,最终会返回Promise
const fetchInitialData = ({ store }) => {
return store.dispatch('fetchBar');
};
所有的asyncData执行完毕以后,才会resolve,context.state 会赋值给window.INITIAL_STATE
jsexport default (context)=>{
return new Promise((resolve,reject)=>{
const {App,store,app}=createApp()
let components=App.components
let asyncDataPromiseFns = []
Object.values(components).forEach(component=>{
if(component.asyncData) asyncDataPromiseFns.push(component.asyncData({store}))
})
Promise.all(asyncDataPromiseFns).then(result=>{
context.state = store.state;
console.log(222);
console.log(store.state);
console.log(context.state);
console.log(context);
resolve(app);
})
})
}
因为现在入口文件返回的是Promise,且在异步返回以后,才会return。 所以我们服务端需要等待入口文件的asyncData执行完以后,才能往下走,否则会有问题
jstry {
let html=await new Promise((resolve,reject)=>{
renderer.renderToString((err,html)=>{
if (err) {
reject(err)
} else {
resolve(html)
}
})
})
ctx.status=200
ctx.body=html
}
catch (err) {
console.error(err);
ctx.status = 500;
ctx.body = '服务器内部错误';
}
jsrequire('vue-server-renderer/client-plugin')
和
require('vue-server-renderer/server-plugin')
webpack.server.conf.js
jsconst nodeExternals = require('webpack-node-externals')
devtool: '#source-map', // 对bundle renderer 提供 source map 支持
externals: [nodeExternals()], // 打包时忽略node_modules里面的包
jsconst serverBundle = require(path.resolve(__dirname, '../dist/vue-ssr-server-bundle.json'));
const clientManifest = require(path.resolve(__dirname, '../dist/vue-ssr-client-manifest.json'));
const template = fs.readFileSync(path.resolve(__dirname, '../dist/index.ssr.html'), 'utf-8');
let renderer=createBundleRenderer(serverBundle,{
runInNewContext: false,
template,
clientManifest
})
jsrouter.push(context.url) // 路由跳转
router.onReady(() => {
const matchedComponents = router.getMatchedComponents(); //获取到当前路由的components
if (!matchedComponents.length) {
return reject({ code: 404 });
}
})
jslet context = {
url: ctx.url
};
const ssrStream = renderer.renderToStream(context); // 生成的是Node流文件,不是html文本
ctx.status = 200;
ctx.type = 'html';
ctx.body = ssrStream;
本文作者:BARM
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!