服务端渲染的基本模型
const http = require('http')
let str = "hello, SSR"
http.createServer(function(req, res){
req.write('<h1>' + str + '</h1>');
req.end();
}).listen(8090)
所谓服务端渲染, 其实一直都有, java, python, php 都有渲染模板来做服务端渲染, 简单来讲就是前发起请求, 服务器去数据库请求数据, 服务器拿到数据以后, 并不是直接返回, 而是根据相应的设计, 将数据拼接成一个渲染模板, 返回给前端, 前端直接渲染或经过简单处理之后渲染.
Vue 服务端渲染基本模板
<!--template.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport">
<title>Title</title>
</head>
<body>
<p>title</p>
<!--vue-ssr-outlet-->
</body>
</html>
里面的那一行注释就是告诉 serverRenderer 将模板插入到这里
// server.js
const http = require("http")
const Vue = require('vue')
const serverRender = require('vue-server-renderer')
const app = new Vue({
template: `<div>{{ title }}</div>`,
data: {
title: 'server side render'
}
})
const render = serverRender.createRenderer({
template: require('fs').readFileSync('./template.html', 'utf8')
})
let server = http.createServer(function (req, res) {
render.renderToString(app, {
// src 可以使用三括号插值语法将其放入渲染模板中 {{{ src }}}
// 如果使用双括号, 将会被作为文本直接放在 document 中
src: '<script>console.log("src")</script>',
init: ''
},(err, html) => {
if(!err) {
res.end(html)
}
})
})
server.listen(8090, function () {
console.log('server is runing at 8090');
})
更为具体的实例肯定不会如此简陋,因为这也做不了什么事.
下面会有一个 express + vue + webpack 按照 vue 文档上的 ssr 写的实例.
先放个大招:
按照这个流程
我们看看目录结构:
首先肯定需要使用到webpack
, 编写webpack
配置文件如下:
const path = require('path') // 方便使用路径
const root = path.resolve(__dirname, '..'); // root 就是根路径 ssr 了
const VueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
mode: 'development',
entry: path.join(root, 'entry/entry.server.js'), // ssr/entry/entry-server.js
output: {
libraryTarget: "commonjs2", // 将导出的文件放到 module.exports 上
path: path.join(root, 'dist'), // 输出到 ssr/dist
filename: 'server.bundle.js' // 定义输出文件名
},
module: {
rules: [
{
test: /\.vue$/,
// use: ['vue-loader']
loader: 'vue-loader'
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/ // 忽略 node_modules 中的 js
}
]
},
plugins: [new VueLoaderPlugin()]
}
配置好webpack, 就开始写代码了
肯定是需要一个开发路径src
的, 在里面写vue
的组件等等, 另外,main.js
也在这里定义.
// main.js
import Vue from 'vue'
import App from './App.vue'
export default function () {
return new Vue({
template: `<App />`,
components: {
App
}
})
}
它的主要作用就是生成一个最外层的vue
实例, 作为服务端渲染vue
的入口.
然后看看 App.vue
// App.vue
<template>
<div id="app">
<h1>Server Side Render</h1>
<test />
</div>
</template>
<script>
import test from './components/test.vue'
export default {
name: "App",
components: {
test
}
}
</script>
<style scoped>
</style>
在App.vue
里, 引入了一个组件test
.这意味着和在客户端使用vue
没什么区别.
但是其实是有区别的. 不如它会失去vue
应有的特性. 这个后面会说.
// test.vue 注意, 这里在浏览器渲染之后, 理想中的双向数据绑定不会生效
<template>
<div class="test">
<input type="text" v-model="val">
<p>{{ val }}</p>
</div>
</template>
<script>
export default {
name: "test",
data () {
return {
val: 'hello, ssr'
}
}
}
</script>
<style scoped>
</style>
后面就应该是服务端的入口登场了, entry.server.js
是服务端的入口
// entry.server.js
import createApp from '../src/main.js'
export default function () {
return createApp()
}
最后就是在server.js
里面, 使用它, 为服务端渲染创建一个vue
实例, 再使用服务端的渲染插件vue-server-renderer
, 将我们创建的vue
实例生成字符串, 返回给浏览器, 由浏览器去将其渲染出来.
// server.js
const express = require('express')
const serverRender = require('vue-server-renderer')
const createApp = require('./dist/server.bundle.js')['default'] // default 才能拿到生成实例的函数
console.log(createApp); // [Function]
const render = serverRender.createRenderer({
template: require('fs').readFileSync('./template.html', 'utf8') // fs 读取 html 模板
})
const server = express()
server.get('*', (req, res) => {
let app = createApp()
render.renderToString(app, (err, html) => {
res.end(html) // 渲染成字符串返回给浏览器
})
})
server.listen(8090, function () {
console.log('server is runing at 8090');
})
使用 webpack
打包, 使用node
执行server.js
, 在浏览器打开localhost:8090
, 看到如下画面:
到目前为止, 大招图的上半部分已经通了:
此时, vue的特性依然没有. 只是一个死的不能再死的页面.
接下来要做的事情, 就是要让
vue
活过来.那么这就是客户端的事情了在
entry
目录中, 添加一个entry.client.js
import createApp from '../src/main.js'
let app = createApp()
// 页面构建完成, 将 app 挂载
window.onload = function () {
app.$mount('#app')
}
当然, 也要写一个webpack
客户端的配置文件, 基本跟服务端的一样, 只是名字不同.
// webpack.client.js
const path = require('path') // 方便使用路径
const root = path.resolve(__dirname, '..'); // root 就是根路径 ssr 了
const VueLoaderPlugin = require('vue-loader/lib/plugin')
entry = path.join(root, 'dist');
console.log(entry);
module.exports = {
mode: 'development',
entry: path.join(root, 'entry/entry.client.js'), // ssr/entry/entry-server.js
output: {
path: path.join(root, 'dist'), // 输出到 ssr/dist
filename: 'client.bundle.js' // 定义输出文件名
},
module: {
rules: [
{
test: /\.vue$/,
// use: ['vue-loader']
loader: 'vue-loader'
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/ // 忽略 node_modules 中的 js
}
]
},
plugins: [new VueLoaderPlugin()]
}
到了这里, 我们来想一下, 为什么浏览器页面的vue
是死的? 以为没有vue.js
文件, 2333....
这个时候, 三括号插值语法就派上用场了.
先在template.html
里面, 添加一行: {{{ load }}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport">
<title>Title</title>
</head>
<body>
<p>title</p>
<!--vue-ssr-outlet-->
{{{ load }}}
</body>
</html>
意思是去加载一个js
文件, 注意, 双花括号不会去加载, 直接被当成字符串了.
然后 server.js
里面
const express = require('express')
const path = require('path')
const serverRender = require('vue-server-renderer')
const createApp = require('./dist/server.bundle.js')['default']
console.log(createApp);
const render = serverRender.createRenderer({
template: require('fs').readFileSync('./template.html', 'utf8')
})
const server = express()
console.log(path.resolve('/client.bundle.js'));
// 客户端请求这个文件, 将这个文件返回, 路径不一样, 可以偷偷的给它改了嘛
// 请求文件写绝对路径, 返回文件随意
server.get('/client.bundle.js', (req, res) => {
res.sendFile(path.resolve('./dist/client.bundle.js'))
})
server.get('*', (req, res) => {
let app = createApp()
render.renderToString(app, {
// 绝对路径
load: '<script src="/client.bundle.js"></script>'
}, (err, html) => {
res.end(html)
})
})
server.listen(8090, function () {
console.log('server is runing at 8090');
})
然后打包运行, 就会看到:
为啥?
这是因为, 在导入的时候 import Vue from 'vue'
使用的其实是vue/dist/vue.common.js
, 所以会少一些东西, 将其改为vue/dist/vue.js
就好了.这怎么改呢.
在main.js
中, 修改.
然后:
可算是活过来了.
但是这么修改, 意味着在每一个地方都需要这么修改.
在
webpack
里面修改一下, 两个文件中分别添加一个对象.
resolve: {
alias: {
'vue': 'vue/dist/vue.js'
}
}
这样基本的服务端渲染就算是完成了.
使用 vue-router
像正常使用vue-router
一样, 写好 router
配置文件, 注入App
组件的配置对象. 然后router-link
, router-view
一气呵成.
使用 vuex
也和往常使用一样, 使用store
.