服务端渲染在很早的时候就有了,可以追溯到 ASP、JSP 的时代,就是在后端返回一个静态页面给浏览器,由浏览器直接显示。
但是在 React 以及 nodejs 普及之后,开始出现同构渲染,简单来说就是在服务端渲染前端组件然后返回给浏览器显示。
00 背景
同构渲染简称 SSR(Server-Side Render),也叫页面直出。具体的优势可以看这篇文章《手把手教你 ReactJS 和 VueJS 的服务端渲染》。
SSR 是由 React 的虚拟 dom 可以直接在 nodejs 中渲染出 dom string, 就是类似于
<div> xxxx </div>
Vue.js 的服务端渲染的方式和 React 还有点不一样。
下面介绍下 Vue.js 整个直出的过程。
01 开始
Vue SSR 官方文档《Vue.js 服务器端渲染指南》. 直接上例子
const Vue = require('vue')
const server = require('express')()
const renderer = require('vue-server-renderer').createRenderer({
template: require('fs').readFileSync('./index.template.html', 'utf-8')
})
Vue.component('button-counter', {
data: function () {
return {
count: 0
}
},
template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})
server.get('*', (req, res) => {
const app = new Vue({
data: {
url: req.url
},
template: `
<div id="components-demo">
<button-counter></button-counter>
</div>
`
})
const context = {
title: 'hello',
meta: `
<meta ...>
<meta ...>
`
}
renderer.renderToString(app, context, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
}
console.log(html)
res.end(html)
})
})
server.listen(8080)
同构渲染的关键就是 renderToString, 无论是 vue 还是 react 都是通过这个方法输出 dom string.
上面 console.log(html) 输出结果
<html>
<head>
<!-- 使用双花括号(double-mustache)进行 HTML 转义插值(HTML-escaped interpolation) -->
<title>hello</title>
<!-- 使用三花括号(triple-mustache)进行 HTML 不转义插值(non-HTML-escaped interpolation) -->
<meta ...>
<meta ...>
</head>
<body>
<div id="components-demo" data-server-rendered="true"><button>You clicked me 0 times.</button></div>
</body>
</html>
<html>
<head>
<!-- 使用双花括号(double-mustache)进行 HTML 转义插值(HTML-escaped interpolation) -->
<title>hello</title>
<!-- 使用三花括号(triple-mustache)进行 HTML 不转义插值(non-HTML-escaped interpolation) -->
<meta ...>
<meta ...>
</head>
<body>
<div id="components-demo" data-server-rendered="true"><button>You clicked me 0 times.</button></div>
</body>
</html>
但是真正用 vue 构建的复杂的应用应该是由很多 *.vue 文件组成的,但是 commonjs 规范根本识别不了 *.vue 文件,所以需要对 vue 文件做服务端构建。
02 复杂应用下服务端构建
由于前端也需要构建,所以抽出一个公用的 webpack.base.config.js,
const path = require('path')
const utils = require('./utils')
const vueLoaderConfig = require('./vue-loader.conf')
const webpack = require("webpack")
function resolve(dir) {
return path.join(__dirname, '..', dir)
}
module.exports = {
context: path.resolve(__dirname, '../'),
output: {
path: path.resolve(__dirname, '../dist'),
filename: '[name].[chunkhash:8].js',
publicPath: './'
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
}
},
module: {
rules: [{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test'), resolve('/node_modules/element-ui/src'), resolve('/node_modules/element-ui/packages'), resolve('node_modules/webpack-dev-server/client')]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 1000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 1000,
name: utils.assetsPath('media/[name].[hash:7].[ext]')
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 1000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
},
{
test: /\.less$/,
loader: "style-loader!css-loader!postcss-loader!less-loader",
},
]
},
node: {
// prevent webpack from injecting useless setImmediate polyfill because Vue
// source contains it (although only uses it if it's native).
setImmediate: false,
// prevent webpack from injecting mocks to Node native modules
// that does not make sense for the client
dgram: 'empty',
fs: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty'
},
plugins: [
new webpack.ProvidePlugin({
$: "jquery",
jQuery: "jquery",
$moment: "moment",
$numeral: "numeral",
echarts: "echarts"
})
]
}
前端的构建配置 webpack.client.config.js 和 node 的 webpack.server.config.js
// webpack.client.config.js
const path = require('path')
const webpack = require('webpack')
const merge = require('webpack-merge')
const baseConfig = require('./webpack.base.config.js')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
module.exports = merge(baseConfig, {
entry: {
client: path.resolve(__dirname, '../src/entry-client.js'),
},
plugins: [
// 重要信息:这将 webpack 运行时分离到一个引导 chunk 中,
// 以便可以在之后正确注入异步 chunk。
// 这也为你的 应用程序/vendor 代码提供了更好的缓存。
new webpack.optimize.CommonsChunkPlugin({
name: "manifest",
minChunks: Infinity
}),
// 此插件在输出目录中
// 生成 `vue-ssr-client-manifest.json`。
new VueSSRClientPlugin()
]
})
// webpack.server.config.js
const path = require('path')
const merge = require('webpack-merge')
const nodeExternals = require('webpack-node-externals')
const baseConfig = require('./webpack.base.config.js')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
module.exports = merge(baseConfig, {
// 将 entry 指向应用程序的 server entry 文件
entry: {
client: path.resolve(__dirname, '../src/entry-server.js'),
},
// 这允许 webpack 以 Node 适用方式(Node-appropriate fashion)处理动态导入(dynamic import),
// 并且还会在编译 Vue 组件时,
// 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。
target: 'node',
// 对 bundle renderer 提供 source map 支持
devtool: 'source-map',
// 此处告知 server bundle 使用 Node 风格导出模块(Node-style exports)
output: {
libraryTarget: 'commonjs2',
path: path.resolve(__dirname, '../dist'),
filename: '[name].[chunkhash:8].js',
publicPath: './'
},
// https://webpack.js.org/configuration/externals/#function
// https://github.com/liady/webpack-node-externals
// 外置化应用程序依赖模块。可以使服务器构建速度更快,
// 并生成较小的 bundle 文件。
externals: nodeExternals({
// 不要外置化 webpack 需要处理的依赖模块。
// 你可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
// 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单
whitelist: /\.css$/
}),
// 这是将服务器的整个输出
// 构建为单个 JSON 文件的插件。
// 默认文件名为 `vue-ssr-server-bundle.json`
plugins: [
new VueSSRServerPlugin()
]
})
服务端的构建会再 dist 目录下生成 vue-ssr-server-bundle.json 的文件,
const path = require('path')
const Koa = require('koa')
const app = new Koa()
const koaStatic = require('koa-static')
const cors = require('@koa/cors')
const router = require('koa-router')()
const {
createBundleRenderer
} = require('vue-server-renderer')
app.use(cors())
let jsonPath = path.resolve(__dirname, './dist/vue-ssr-server-bundle.json')
const renderer = createBundleRenderer(jsonPath, {
template: require('fs').readFileSync('./index.template.html', 'utf-8')
})
app.use(koaStatic('dist/', {
maxage: 1000 * 3600 * 24 * 30, // a month
}))
// app.use(koaStatic('examples/', {
// maxage: 1000 * 3600 * 24 * 30, // a month
// }))
router.get("*", async ctx => {
const context = {
title: 'hello',
meta: `
<meta ...>
<meta ...>
`
}
renderer.renderToString(context, (err, html) => {
if (err) {
console.log(err.stack)
ctx.status = 500
ctx.body = "Internal Server Error"
return
}
console.log(html)
ctx.body = html
})
})
app
.use(router.routes())
.use(router.allowedMethods({
throw: true
}))
app.listen(7000)
console.log('localhost:7000')
总结:
这就是 vue.js 同构渲染的两种方式。一种是直接通过 Vue.components 注册全局组件,这种在后端也是可以直接通过 renderToString 渲染。
另一种的方式是写 *.vue 组件,但是要通过服务端的 webpack 构建。
网友评论