php .net
传统web
客户端发送一次请求 服务端返回html模板 su友好
SPA:单页应用 vue react CSR:客户端渲染
客户端发送第一次请求 服务端返回html结构
再发送请求要数据 su不友好
SSR:服务端渲染
后端渲染出完整的首屏dom结构 前端拿到内容包括首屏及完整spa结构 应用激活后依然按照spa方式运行
新建工程
(https://github.com/iosKey/VueSSR)
vue create ssr
安装依赖
npm install vue-server-renderer express -D
这里是静态替换 只是一个例子
创建server/index.js
创建一个渲染器 把一个vue的实例转成html返回
const express = require('express')
const Vue = require('vue')
const app = express();
const page = new Vue({
data: {name:'开课吧'},
template: '<div>{{name}}</div>'
})
// 1.渲染器
const renderer = require('vue-server-renderer').createRenderer();
app.get('/', async function(req, res){
// 2.执行渲染
const html = await renderer.renderToString(page)
res.send(html);
})
//监听端口
app.listen(3000, () => {
console.log('渲染服务器就绪');
})
启动服务器
cd server
node .\index.js
浏览器打开localhost:3000
这里开始是动态替换
接下来用Vue router来管理页面
安装Vue router:
npm i vue-router -S
创建src/router/index.js
import Vue from "vue";
import Router from "vue-router";
// 分别创建Index.vue和Detail.vue
import Index from "@/views/Index";
import Detail from "@/views/Detail";
Vue.use(Router);
//导出工厂函数
export function createRouter() {
return new Router({
mode: 'history',
routes: [
{ path: "/", component: Index },
{ path: "/detail", component: Detail }
]
});
}
创建src/views/Index.vue
<template>
<div>
index page
<h2>num:{{$store.state.count}}</h2>
<button @click="$store.commit('add')">add</button>
</div>
</template>
<script>
//数据预取
export default {
asyncData({ store, route }) {
// 约定预取逻辑编写在预取钩子asyncData中
// 触发 action 后,返回 Promise 以便确定请求结果
return store.dispatch("getCount");
}
};
</script>
<style lang="scss" scoped>
</style>
创建src/views/Detail.vue
<template>
<div>
detail page
</div>
</template>
<script>
export default {
}
</script>
<style lang="scss" scoped>
</style>
src/App.vue
<template>
<div id="app">
<nav>
<router-link to="/">首页</router-link>
<router-link to="/detail">详情页</router-link>
</nav>
<router-view></router-view>
</div>
</template>
<script>
import HelloWorld from '@/components/HelloWorld.vue'
export default {
components: {
HelloWorld
},
}
</script>
<style>
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
服务端渲染的构建过程
通过webpack打包 生成server bundle和client bundle
server bundle:处理首屏渲染,生成html,包括spa的构成部分,用于服务器端渲染
client bundle:描述spa构成结构和激活,发动给浏览器
整合Vuex
安装
npm install -S vuex
创建src/store/index.js
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export function createStore() {
return new Vuex.Store({
state: {
count: 0,
},
mutations: {
add(state) {
state.count += 1;
},
init(state, count) {
state.count = count;
},
},
actions: {
// 加一个异步请求count的action
getCount({ commit }) {
return new Promise(resolve => {
setTimeout(() => {
commit("init", Math.random() * 100);
resolve();
}, 1000);
});
},
},
});
}
src创建app.js (定义创建vue实例方法)传入路由、store实例
// 创建Vue实例
import Vue from 'vue'
import App from './App.vue'
import {createRouter} from './router'
import { createStore } from './store'
// 客户端挂载之前,检查组件是否存在异步数据获取
Vue.mixin({
beforeMount() {
const { asyncData } = this.$options;
if (asyncData) {
// 将获取数据操作分配给 promise
// 以便在组件中,我们可以在数据准备就绪后
// 通过运行 `this.dataPromise.then(...)` 来执行其他任务
this.dataPromise = asyncData({
store: this.$store,
route: this.$route,
});
}
},
});
export function createApp(context) {
// 1.获取路由实例
const router = createRouter();
// 2.获取store实例
const store = createStore()
// 2.创建vue实例
const app = new Vue({
router,
store,
context,
render: h => h(App)
})
return {app, router, store}
}
根目录创建entry-server.js(创建vue实例 路由跳转 返回vue实例)
// 创建vue实例并且做首屏渲染
import {createApp} from './app'
export default context => {
return new Promise((resolve, reject) => {
const {app,router,store} = createApp(context)
// 跳转首屏
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(() => {
// 所有预取钩子 resolve 后,
// store 已经填充入渲染应用所需状态
// 将状态附加到上下文,且 `template` 选项用于 renderer 时,
// 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。
context.state = store.state;
resolve(app);
})
.catch(reject);
}, reject);
})
}
根目录创建entry-client.js(挂载到#app上)
// 客户端激活
import { createApp } from "./app";
const { app, router, store } = createApp();
// 当使用 template 时,context.state 将作为 window.__INITIAL_STATE__ 状态自动嵌入到最终的 HTML // 在客户端挂载到应用程序之前,store 就应该获取到状态:
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__);
}
router.onReady(() => {
// 激活
app.$mount("#app");
});
做webpack配置
根目录创建vue.config.js
const VueSSRServerPlugin = require("vue-server-renderer/server-plugin");
const VueSSRClientPlugin = require("vue-server-renderer/client-plugin");
const TARGET_NODE = process.env.WEBPACK_TARGET === "node";
const target = TARGET_NODE ? "server" : "client";
module.exports = {
css: {
extract: false
},
outputDir: './dist/'+target,
configureWebpack: () => ({
// 将 entry 指向应用程序的 server / client 文件
entry: `./src/entry-${target}.js`,
// 对 bundle renderer 提供 source map 支持
devtool: 'source-map',
// 这允许 webpack 以 Node 适用方式处理动态导入(dynamic import),
// 并且还会在编译 Vue 组件时告知 `vue-loader` 输送面向服务器代码(server-oriented code)。
target: TARGET_NODE ? "node" : "web",
node: TARGET_NODE ? undefined : false,
output: {
// 此处使用 Node 风格导出模块
libraryTarget: TARGET_NODE ? "commonjs2" : undefined
},
// 这是将服务器的整个输出构建为单个 JSON 文件的插件。
// 服务端默认文件名为 `vue-ssr-server-bundle.json`
plugins: [TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()]
})
};
安装依赖
npm i cross-env
在package.json配置指令
"scripts": {
"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",
},
执行 在dist打出client和server两个包
npm run build
根目录创建宿主文件 public/index.tmpl.html
是约定好的 将来直接替换这个
<!DOCTYPE html>
<html lang="en">
<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>Document</title>
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
根目录创建server/index2.js(创建渲染器 配置dist里面client和server包的路径和宿主文件 执行渲染)首屏渲染才会走服务器
const express = require('express')
const fs = require('fs')
const app = express();
// 1.渲染器
const {createBundleRenderer} = require('vue-server-renderer');
const bundle = require('../dist/server/vue-ssr-server-bundle.json')
//解决报错和页面刷新 指定为静态目录
app.use(express.static('../dist/client', {index: false}))
// bundle是服务端包
const renderer = createBundleRenderer(bundle, {
runInNewContext: false,
template: fs.readFileSync('../public/index.tmpl.html', "utf-8"),
clientManifest: require('../dist/client/vue-ssr-client-manifest.json')
})
app.get('*', async function(req, res) {
console.log(req.url);
const context = {
title: 'SSR Test',
url: req.url
}
// 2.执行渲染
const html = await renderer.renderToString(context)
res.send(html);
})
app.listen(3000, () => {
console.log('渲染服务器就绪');
})
渲染服务器 cd进server目录
node .\index2.js
修改代码需要重新npm run build
然后重启服务器 node .\index2.js