一、什么是服务器端渲染(SSR)?
大致就是在服务端拼接好用户请求的静态页面,直接返回给客户端,客户端激活这些静态页面,让他们变成动态的,并且能够响应后续的数据变化。
二、为什么使用服务器端渲染(SSR)?
1、更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。
2、产生更好的用户体验,更快的内容到达时间(time-to-content),特别是对于缓慢的网络情况或运行缓慢的设备。无需等待所有的 JavaScript 都完成下载并执行,才显示服务器渲染的标记,所以你的用户将会更快速地看到完整渲染的页面。
三、基本用法
首先vue-ssr 需要一个及其重要的插件,所以我们需要安装一下
npm install vue vue-server-renderer --save
3.1 渲染一个简单的实例(官网)
// 第 1 步:创建一个 Vue 实例
const Vue = require('vue')
const app = new Vue({
template: `<div>Hello World</div>`
})
// 第 2 步:创建一个 renderer
const renderer = require('vue-server-renderer').createRenderer()
// 第 3 步:将 Vue 实例渲染为 HTML
renderer.renderToString(app, (err, html) => {
if (err) throw err
console.log(html)
// => <div data-server-rendered="true">Hello World</div>
})
// 在 2.5.0+,如果没有传入回调函数,则会返回 Promise:
renderer.renderToString(app).then(html => {
console.log(html)
}).catch(err => {
console.error(err)
})
3.2 与服务器集成的案例(官网)
先安装npm install express --save
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: `<div>访问的 URL 是: {{ url }}</div>`
})
renderer.renderToString(app, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
}
res.end(`
<!DOCTYPE html>
<html lang="en">
<head><title>Hello</title></head>
<body>${html}</body>
</html>
`)
})
})
server.listen(8080)
3.3 使用一个页面模板
// index.html
<!DOCTYPE html>
<html lang="en">
<head><title>Hello</title></head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
注意 注释 -- 这里将是应用程序 HTML 标记注入的地方。
const renderer = createRenderer({
template: require('fs').readFileSync('./index.template.html', 'utf-8')
})
renderer.renderToString(app, (err, html) => {
console.log(html) // html 将是注入应用程序内容的完整页面
})
当然,以上内容只是基础中的基础,最终我们使用,都是需要集成vue-cli,并结合node,express,webpack等的配置,来生成一个更加灵活的vue-ssr应用,花了将近4天时间,一遍看api,一遍查资料,终于搭建出了一个静态的单页应用vue-ssr。赶紧趁热乎分享一下,毕竟年纪大了,记性不好。
四、vue2.0+node+express+webpack实现vue-ssr单页应用
首先,我们需要安装vue-cli,具体安装,可以参考笔者的vue系列 第一篇文章,如果之前已经全局安装过,那么只需要init一下即可。
刚开始看不懂官网上的图,后来搭建出来以后,发现官方的就是官方的,就跟北京地铁图一样清晰,借鉴一下别人的话就是,ssr 有两个入口文件,client.js 和 server.js, 都包含了应用代码,webpack 通过两个入口文件分别打包成给服务端用的 server bundle 和给客户端用的 client bundle. 当服务器接收到了来自客户端的请求之后,会创建一个渲染器 bundleRenderer,这个 bundleRenderer 会读取上面生成的 server bundle 文件,并且执行它的代码, 然后发送一个生成好的 html 到浏览器,等到客户端加载了 client bundle 之后,会和服务端生成的DOM 进行 Hydration(判断这个DOM 和自己即将生成的DOM 是否相同,如果相同就将客户端的vue实例挂载到这个DOM上, 否则会提示警告)。
根据以上内容,我们先来搭建一个简单的暂时没有数据请求的vue-ssr。
如何搭建?
1、创建一个vue实例
2、配置路由,以及相应的视图组件
3、创建客户端入口文件
4、创建服务端入口文件
5、配置 webpack,分服务端打包配置和客户端打包配置
6、创建服务器端的渲染器,将vue实例渲染成html
创建vue实例:为每个请求创建一个新的根 Vue 实例。这与每个用户在自己的浏览器中使用新应用程序的实例类似。如果我们在多个请求之间使用一个共享的实例,很容易导致交叉请求状态污染,因此,我们应该暴露一个可以重复执行的工厂函数,为每个请求创建新的应用程序实例
// app.js
import Vue from 'vue'
import App from './App.vue'
import createRouter from './router'
Vue.config.productionTip = false
export function createApp () {
const router = createRouter()
const app = new Vue({
// el: '#app',
router,
render: h => h(App)
})
return { app, router }
}
配置路由,以及相应的视图组件: 注意,类似于 createApp,我们也需要给每个请求一个新的 router 实例,所以文件导出一个 createRouter 函数.
// router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import home from '@/components/home'
import about from '@/components/about'
Vue.use(Router)
export default function createRouter() {
return new Router({
mode:'history',
routes: [
{
path: '/',
name: 'home',
component: home
},
{
path: '/about',
name: 'about',
component: about
}
]
})
}
创建客户端入口文件:entry-client.js 客户端 entry 只需创建应用程序,并且将其挂载到 DOM 中
import { createApp } from './app'
// 客户端特定引导逻辑……
const { app, router } = createApp()
// 这里假定 App.vue 模板中根元素具有 `id="app"`
router.onReady(() => {
app.$mount('#app')
})
创建服务端入口文件:entry-server.js 中实现服务器端路由逻辑
import { createApp } from './app'
export default (context) => {
// 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,
// 以便服务器能够等待所有的内容在渲染前,
// 就已经准备就绪。
return new Promise((resolve, reject) => {
const { app, router } = createApp(context)
const { url } = context
const { fullPath } = router.resolve(url).route
if (fullPath !== url) {
return reject({ url: fullPath })
}
// 设置服务器端 router 的位置
router.push(context.url)
// 等到 router 将可能的异步组件和钩子函数解析完
router.onReady(() => {
const matchedComponents = router.getMatchedComponents()
if (!matchedComponents.length) {
return reject({ code: 404 })
}
// Promise 应该 resolve 应用程序实例,以便它可以渲染
resolve(app)
}, reject)
})
}
创建好以后,我们的项目还不能启动,我们需要配置webpack,来生成服务端用的 server bundle 和给客户端用的 client bundle。
客户端的client bundle比较好创建,我们在vue-cli下的build文件夹下的webpack.dev.conf.js中,引入const vueSSRClient = require('vue-server-renderer/client-plugin')
,然后,在下方配置plugins的地方new vueSSRClient()
创建一个 vueSSRClient()就好了,此时我们客户端的bundle就已经悄然生成了。
服务端的server bundle,就需要我们在build文件夹下新建一个webpack.server.conf.js,配置如下
const webpack = require('webpack')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
const nodeExternals = require('webpack-node-externals')
module.exports = merge(baseWebpackConfig,{
entry: './src/entry-server.js',
devtool:'source-map',
target:'node',
output:{
filename:'server-bundle.js',
libraryTarget:'commonjs2'
},
externals: [nodeExternals({
// do not externalize CSS files in case we need to import it from a dep
whitelist: /\.css$/
})],
plugins:[
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
'process.env.VUE_ENV': '"server"'
}),
new VueSSRServerPlugin()
]
})
现在我们已经能通过webpack,生成了服务端用的 server bundle 和给客户端用的 client bundle,但是我们需要创建一个渲染器 bundleRenderer,这个 bundleRenderer 会读取上面生成的 server bundle 文件,并且执行它的代码, 然后发送一个生成好的 html 到浏览器,所以我们在build文件夹下 ,新建一个dev-server.js 文件,内容如下(注意看注释)
// dev-server.js
const webpack = require('webpack')
const baseWebpackConfig = require('./webpack.server.conf')
const clientConfig = require('./webpack.base.conf')
const fs = require('fs')
const path = require('path')
// 读取内存的文件
const Mfs = require('memory-fs')
const axios = require('axios')
module.exports = (cb) => {
// 用来读取内存文件
var mfs = new Mfs()
const webpackComplier = webpack(baseWebpackConfig)
webpackComplier.outputFileSystem = mfs
const readFile = (fs, file) => {
try {
return fs.readFileSync(path.join(clientConfig.output.path, file), 'utf-8')
} catch (e) {}
}
webpackComplier.watch({},async (err,stats) => {
if(err) {
return console.log(err)
}
stats = stats.toJson();
stats.errors.forEach(err => {console.log(err)});
stats.warnings.forEach(err => {console.log(err)});
// 获取vue-server-renderer/server-plugin生成的服务端bundle的json文件 默认名字为vue-ssr-server-bundle.json
let serverBundle = JSON.parse(readFile(mfs, 'vue-ssr-server-bundle.json'))
// 获取vue-server-renderer/client-plugin生成的客户端bundle的json文件,默认名字vue-ssr-client-manifest.json
let clientBundle = await axios.get('http://localhost:8080/vue-ssr-client-manifest.json')
// 获取模板文件 注意模板文件里面加上<!--vue-ssr-outlet-->
let template = fs.readFileSync(path.join(__dirname,'..','index.html'), 'utf-8')
cb(serverBundle, clientBundle, template)
})
}
现在就差临门一脚了,就是调用我们的dev-server,用bundleRenderer生成我们的html,并返回到页面上、那我们在根目录下新建一个server.js ,整合所有资源,使其成为我们客户端的入口
// server.js
const devServer = require('./build/dev-server.js')
const server = require('express')()
const { createBundleRenderer } = require('vue-server-renderer')
// 在服务器处理函数中……
server.get('*', (req, res) => {
const context = { url: req.url }
res.status(200)
devServer((serverBundle, clientBundle, template) => {
let renderer = createBundleRenderer(serverBundle, {
runInNewContext: false, // 推荐
template, // (可选)页面模板
clientManifest:clientBundle.data // (可选)客户端构建 manifest
})
renderer.renderToString(context,(err,html) => {
res.send(html)
})
})
})
const port = process.env.PORT || 5001;
server.listen(port, () => {
console.log(`server started at localhost:${port}`)
console.log('启动成功')
})
最终我们的项目目录如下:
为了能在服务端启动我们的项目,在package.json的scripts中,加一行代码"server": "node server.js"
现在,我们运行npm run dev
启动的就是客户端渲染,运行npm run server
启动的就是服务端渲染,前端页面呈现是一样的,但是翻看页面源码的时候就会发现区别
npm run dev
默认启动8080端口
查看客户端源码
npm run server
默认启动5001端口
查看服务端源码,会有一个明显的 data-server-rendered="true"
现在,我们的简单的案例就完成了,那么最终我们还要集成vuex来动态显示数据,就放下集吧。