根据 官网例子一步步实现vue ssr
标题对应官网标题
新建一个文件夹 ssr
cd ssr
npm init -y
npm install vue vue-server-renderer express --save
或者yarn add vue vue-server-renderer express --save
新建文件server.js
server.js
// 第 1 步:创建一个 Vue 实例
const Vue = require('vue')
const app = new Vue({
template: `Hello World`
})
// 第 2 步:创建一个 renderer
const renderer = require('vue-server-renderer').createRenderer()
// 第 3 步:将 Vue 实例渲染为 HTML
renderer.renderToString(app, (err, html) => {
if (err) throw err
console.log(html)
})
// 在 2.5.0+,如果没有传入回调函数,则会返回 Promise:
renderer.renderToString(app).then(html => {
console.log(html)
}).catch(err => {
console.error(err)
})
运行命令node server.js
,控制台会输出两行
const Vue = require('vue')
const server = require('express')()
const renderer = require('vue-server-renderer').createRenderer()
server.get('*', (req, res) => {
const app = new Vue({
data: {
url: req.url
},
template: `访问的 URL 是: {{ url }}`
})
renderer.renderToString(app, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
}
res.end(`
vue ssr
${html}
`)
})
})
server.listen(8080,()=>{
console.log('已监听 localhost:8080')
})
运行命令node server.js
,控制台会输出已监听 localhost:8080
浏览器打开 localhost:8080
<html lang="en">
<head>
<meta charset="utf-8">
<title>vue ssrtitle>
head>
<body>
body>
html>
修改 index.template.html 和 server.js ,结果不变
<html lang="en">
<head>
{{{ meta }}}
<title>{{ title }}title>
head>
<body>
body>
html>
const context = {
title: 'vue ssr',
meta: `
`
}
renderer.renderToString(app,context, (err, html) => {
...
res.end(html)
})
将server.js 拆分为app.js 和 server.js
app.js
const Vue = require('vue')
module.exports = function createApp (context) {
return new Vue({
data: {
url: context.url
},
template: `访问的 URL 是: {{ url }}`
})
}
server.js
运行node server.js
,结果不变.
简单来说就是 server.js 将vue代码注入到模版html中 ,并返回
这里是基本用法,源码在官网Vue SSR 指南中都有注解,先将前面弄懂在看下面。
另起一个项目,使用vue-cli3脚手架搭建,根据自己需求选择
项目结构( +
是新增文件,*
是修改文件 )
.
├── README.md
├── babel.config.js
├── node_modules
│ └──...
├── public
│ ├── favicon.ico
│ └── index.html
├── src
│ ├── App.vue
│ ├── assets
│ ├── components
│ ├── entry-client.js +
│ ├── entry-server.js +
│ ├── main.js *
│ ├── router.js
│ ├── store.js
│ └── views
├── package.json *
├── vue.config.js +
└── yarn.lock
这是一个vue-cli3搭建完成后的前端项目
暂时先不考虑路由的问题
import Vue from 'vue'
import App from './App.vue'
// 导出一个工厂函数,用于创建新的
// 应用程序、router 和 store 实例
export function createApp () {
const app = new Vue({
// 根实例简单的渲染应用程序组件。
render: h => h(App)
})
return { app }
}
import { createApp } from './main'
// 客户端特定引导逻辑……
const { app } = createApp()
// 这里假定 App.vue 模板中根元素具有 `id="app"`
app.$mount('#app')
import { createApp } from './main'
export default context => {
const { app } = createApp()
return app
}
过程简述
entry-server.js构建后成为 vue-ssr-server-bundle.json文件(vue代码)
server.js将 vue-ssr-server-bundle.json (vue代码)注入到模版文件中,并输出
暂时未用到 entry-client.js
安装 cross-env
安装 webpack-node-externals
安装 vue-server-renderer
yarn add cross-env webpack-node-externals vue-server-renderer --save-dev
vue.config.js(文件配置参考官网 Vue SSR 指南 构建配置,官网里面有注解,这里就不加注解了)
调整 webpack 配置最简单的方式就是在 vue.config.js 中的 configureWebpack 选项提供一个对象,
该对象将会被 webpack-merge 合并入最终的 webpack 配置。
const nodeExternals = require('webpack-node-externals')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
module.exports = {
configureWebpack: () => {
if (process.env.WEBPACK_TARGET === 'node') {
return {
entry: './src/entry-server.js',
target: 'node',
devtool: 'source-map',
output: {
libraryTarget: 'commonjs2'
},
externals: nodeExternals({
whitelist: /\.css$/
}),
plugins: [
new VueSSRServerPlugin()
]
}
} else {
return {
entry: './src/entry-client.js',
plugins: [
new VueSSRClientPlugin()
]
}
}
},
}
修改package.json
"scripts": {
...
"build:client": "vue-cli-service build",
"build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build --mode server",
"build:win": "npm run build:server && move dist\\vue-ssr-server-bundle.json bundle && npm run build:client && move bundle dist\\vue-ssr-server-bundle.json",
"build:mac": "npm run build:server && mv dist/vue-ssr-server-bundle.json bundle && npm run build:client && mv bundle dist/vue-ssr-server-bundle.json"
},
根据自己系统运行 yarn run build:win
或者yarn run build:mac
,会在根目录下生成dist文件夹
.
├── css
│ └── ...
├── favicon.ico
├── index.html
├── js
│ └── ....
├── vue-ssr-client-manifest.json
└── vue-ssr-server-bundle.json
在当前项目内 或者 新建一个文件夹 ,放置文件内容如下
.
├── ...
├── dist
├── index.template.html
└── server.js
安装express
yarn add express --save-dev
index.template.html 代码在上面代码构建
server.js(根据上面的js略作修改,使用了createBundleRenderer,详情看Bundle Renderer 指引)
const serverBundle = require('./dist/vue-ssr-server-bundle.json')
const server = require('express')()
const renderer = require('vue-server-renderer').createBundleRenderer(serverBundle,{
template: require('fs').readFileSync('./index.template.html', 'utf-8')
})
server.get('*', (req, res) => {
const context = {
title: 'ssr demo',
meta: `
`
}
renderer.renderToString(context, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
}
res.end(html)
})
})
server.listen(8080,()=>{
console.log('已监听 localhost:8080')
})
运行node server.js
,并打开浏览器(因为还未用到router,所以home|about不可跳转路由)
路由和代码分割
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export function createRouter() {
return new Router({
mode: 'history',
routes: [
{ path: '/', component: () => import('./views/Home.vue') },
{ path: '/about', component: () => import('./views/About.vue') }
]
})
}
import { createApp } from './main'
export default context => {
// 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,
// 以便服务器能够等待所有的内容在渲染前,
// 就已经准备就绪。
return new Promise((resolve, reject) => {
const { app, router } = createApp()
// 设置服务器端 router 的位置
router.push(context.url)
// 等到 router 将可能的异步组件和钩子函数解析完
router.onReady(() => {
const matchedComponents = router.getMatchedComponents()
// 匹配不到的路由,执行 reject 函数,并返回 404
if (!matchedComponents.length) {
return reject({ code: 404 })
}
// Promise 应该 resolve 应用程序实例,以便它可以渲染
resolve(app)
}, reject)
})
}
<html>
<head>
<meta charset="utf-8">
<title>{{ title }}title>
head>
<body>
body>
html>
url 会传给 entry-server.js
此时点击home|about,页面均是从服务端发送过来的
app.use(express.static(path.resolve(__dirname, './dist')))
(全部引入,不可取)const express = require('express')
const app = express()
const { createBundleRenderer } = require('vue-server-renderer')
const path = require("path");
const template = require('fs').readFileSync('./index.template.html', 'utf-8')
const serverBundle = require('./dist/vue-ssr-server-bundle.json')
const renderer = createBundleRenderer(serverBundle, {
template,
})
//引入静态文件 否则运行报错
app.use(express.static(path.resolve(__dirname, './dist')))
app.get('*', (req, res) => {
const context = {
title: 'ssr demo====',
url: req.url
}
renderer.renderToString(context, (err, html) => {
if (err) {
if (err.code === 404) {
res.status(404).end('Page not found')
} else {
res.status(500).end('Internal Server Error')
}
} else {
res.end(html)
}
})
})
app.listen(8080, () => {
console.log('已监听 localhost:8080')
})
运行node server.js
这里你会发现,你在服务端设置模版了(index.template.html),并设置了title为ssr demo====
,但是他的显示不是这样,他所显示的是public文件夹下的index.html(若是删掉他,它会通过webpack配置自动生成一个index.html ,title为Vue App
)
点击about标签,你会发现并没有发送请求.点击浏览器的刷新按钮,他才发送请求.
这里需要注意的是 静态文件的引入 app.use(express.static(path.resolve(__dirname, './dist')))
所以连dist中的index.html也一并引入了,此index.html使用了main.js ,所以点击about走的是路由而不是服务端渲染.
测试
在entry-client.js 加入测试router.onReady(() => { console.log('entry-client.js'); app.$mount('#app') })
重新打包,并运行,浏览器控制台会显示
entry-client.js
app.use(express.static(path.resolve(__dirname, './dist')))
替换成app.use('/js', express.static(path.resolve(__dirname, './dist/js')))
app.use('/img', express.static(path.resolve(__dirname, './dist/img')))
app.use('/css', express.static(path.resolve(__dirname, './dist/css')))
也就是说不引入 dist/index.html
这回浏览器的标题变成了ssr demo====
,点击about,也是从服务请求的.
之前的是所有的请求都是服务端渲染app.get('*' (req, res)
改成只/
服务端渲染
app.get('/' (req, res) => {
...
})
这时,除/
其他路由页面 会显示Cannot GET /about
,
要想要about也展示,须使用entry-client.js
打包生成的vue-ssr-client-manifest.json
const clientManifest = require("./dist/vue-ssr-client-manifest.json");
const renderer = createBundleRenderer(serverBundle, {
runInNewContext: false, // 推荐
template,
clientManifest
})
这时,页面还是有问题,Internal Server Error
须修改vue.config.js
module.exports = {
...
css: {
extract: false
},
}
打包后,再次运行
现在已经达到目的.
也可以写成数据形式.
app.get(['/','/about'], (req, res) => {
...
})
注:有的地方没有粘贴代码,而是使用的图片,主要是突出变化
未完待续…