创建vue项目
vue create ssr-demo
安装router、vuex
vue add router
vue add vuex
基础vue项目目录如下:
重构成ssr项目
1、修改路由配置
// router/index.jsimport Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
Vue.use(VueRouter)
// 工厂函数 每次请求返回一个Router实例
export function createRouter() {
return new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
}
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
}
]
2、修改vuex配置
// store/index.jsimport Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export function createStore() {
return new Vuex.Store({
state: {
count: 99,
},
mutations: {
add(state){
state.count += 1
}
},
actions: {
},
modules: {
}
})
}
3、修改main.js 挂载router、vuex
// main.jsimport Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router'
import { createStore } from './store'
Vue.config.productionTip = false
// 需要每次请求 放回一个vue实例
export function createApp(context) {
const router = createRouter()
const store = createStore()
const app = new Vue({
router,
store, // 挂载
context, // 用于和外的renderer交互
render: h => h(App)
})
return {app, router}
}
4、创建服务端入口和客户端入口文件
// src/entry-server.js
// 和渲染器打交道
// 创建vue实例
import { createApp } from './main'
export default context => {
const { app, router } = createApp(context)
return new Promise((resolve, reject) => {
// 跳转首屏地址
router.push(context.url)
// 等待路由就绪
router.onReady(() => {
resolve(app)
}, reject)
})
}
// src/entry-client.js
// 激活
import { createApp } from './main'
const { app, router } = createApp()
router.onReady(() => {
app.$mount('#app')
})
5、webpack配置
安装依赖
npm install webpack-node-externals lodash.merge -D
npm i vue vue-server-renderer -S // 重点: 两者同时安装 确保版本匹配
配置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: () => ({
// 将 entry 指向应用程序的 server / client 文件
entry: `./src/entry-${target}.js`,
// 对 bundle renderer 提供 source map 支持
devtool: 'source-map',
// target设置为node使webpack以Node适用的方式处理动态导入,
// 并且还会在编译Vue组件时告知`vue-loader`输出面向服务器代码。
target: TARGET_NODE ? "node" : "web",
// 是否模拟node全局变量
node: TARGET_NODE ? undefined : false,
output: {
// 此处使用Node风格导出模块
libraryTarget: TARGET_NODE ? "commonjs2" : undefined
},
// https://webpack.js.org/configuration/externals/#function
// https://github.com/liady/webpack-node-externals
// 外置化应用程序依赖模块。可以使服务器构建速度更快,并生成较小的打包文件。
externals: TARGET_NODE
? nodeExternals({
// 不要外置化webpack需要处理的依赖模块。
// 可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
// 还应该将修改`global`(例如polyfill)的依赖模块列入白名单
whitelist: [/\.css$/]
})
: undefined,
optimization: {
splitChunks: undefined
},
// 这是将服务器的整个输出构建为单个 JSON 文件的插件。
// 服务端默认文件名为 `vue-ssr-server-bundle.json`
// 客户端默认文件名为 `vue-ssr-client-manifest.json`。
plugins: [TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()]
}),
chainWebpack: config => {
// cli4项目添加
if (TARGET_NODE) {
config.optimization.delete('splitChunks')
}
config.module
.rule("vue")
.use("vue-loader")
.tap(options => {
merge(options, {
optimizeSSR: false
});
});
}
};
6、脚本配置
安装依赖
npm i cross-env -D // 用于下面的命令
修改 package.json
"scripts": {
"serve": "vue-cli-service serve",
"build": "npm run build:server & npm run build:client",
"build:client": "vue-cli-service build",
"build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build"
},
7、修改./public/index.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">
<!-- 1.删掉之前动态标签 -->
<title>Vue-ssr</title>
</head>
<body>
<!-- 2.把宿主元素变成一个注释 -->
<!--vue-ssr-outlet-->
</body>
</html>
另:修改./src/App.vue
// 用于测试vuex
<h2 @click="$store.commit('add')">{{$store.state.count}}</h2>
8、打包文件
npm run build
打包生产的dist文件钟就有如下文件
9、服务器启动文件
根目录创建文件 server/ssr.js
// 创建一个express实例
const express = require('express')
const app = express()
// 获取绝对地址
const resolve = dir => require('path').resolve(__dirname, dir)
// 静态文件服务
// 开发dist/client目录,关闭默认的index页面打开功能
app.use(express.static(resolve('../dist/client'), {index: false}))
// 创建渲染器
const { createBundleRenderer } = require('vue-server-renderer')
// 参数1:服务端bundle
const bundle = resolve('../dist/server/vue-ssr-server-bundle.json')
const renderer = createBundleRenderer(bundle, {
runInNewContext: false, // https://ssr.vuejs.org/zh/api/#runinnewcontext
template: require('fs').readFileSync(resolve("../public/index.html"), "utf-8"), // 宿主文件
clientManifest: require(resolve("../dist/client/vue-ssr-client-manifest.json")) // 客户端清单
})
// 只做一个件事,渲染
app.get('*', async (req, res) => {
try {
const context = {
url: req.url
}
// 渲染: 得到html字符串
const html = await renderer.renderToString(context)
// 发送回前端
res.send(html)
} catch (error) {
res.status(500).send('服务器内部错误')
}
})
// 监听端口
app.listen(3000, () => console.log('服务器开启'))
运行
node ./server/ssr.js
浏览器打开 本地3000端口,可查看网站源码来区分是否是重构ssr成功,如下:
最后项目目录如下:
**
可能会出现的bug
**
1、vue与vue-server-renderer版本一致
2、bug:
解决方法:——修改vue.config.js(whitelist 改成 allowlist)
// ...
// whitelist 改成 allowlist
externals: TARGET_NODE
? nodeExternals({
// 不要外置化webpack需要处理的依赖模块。
// 可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
// 还应该将修改`global`(例如polyfill)的依赖模块列入白名单
allowlist: [/\.css$/]
})
: undefined,
// ...
3、每次修改需要重新打包,重启服务器