Vue 项目中服务端渲染的几种方式

服务端渲染在很早的时候就有了,可以追溯到 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 构建。

kbc C87 机械键盘 有线键盘 游戏键盘 87键 原厂cherry轴

关注微信公众号

码中人 微信公众号