上篇文章(《服务器端渲染与Nuxt.js》)介绍了服务器端渲染和一些Nuxt.js的概念,如今咱们就Vue SSR方面,从基础开始,分低、中、高三个层面,来手写实现下传说中的服务端渲染。css
在正式搭建项目以前,咱们仍是要回顾下vue服务器端渲染的一些特性。
服务器端渲染的 Vue.js 应用程序,是使vue应用既能够在客户端(浏览器)执行,也能够在服务器端执行,咱们称之为“同构”或“通用”。html
Vue.js is a framework for building client-side applications. By default, Vue components produce and manipulate DOM in the browser as output. However, it is also possible to render the same components into HTML strings on the server, send them directly to the browser, and finally "hydrate" the static markup into a fully interactive app on the client.vue
beforeCreate
和created
两个钩子函数,其它的只会在客户端执行。那么之前spa应用中,在created
中建立一个setInterval
,而后在destroyed
中将其销毁的相似操做就不能出现了,服务器渲染期间不会调用销毁钩子函数,因此这个定时器会永远保留下来,服务器很容易就崩了。window
和document
,在node.js(好比created
钩子函数)中执行就会报错。而且,咱们使用的第三方API中,须要确保能在node和浏览器都能正常运行,好比axios,它向服务器和客户端都暴露相同的 API(浏览器的源生XHR就不行)。咱们先不考虑同构、不考虑各类配置,先实现一个基础的服务器端渲染demo。node
npm install vue vue-server-renderer express --save
复制代码
vue-server-renderer
是vue
服务器端渲染的核心模块,它须要匹配你的vue版本。安装express
是由于咱们等会会使用它来起个服务看到咱们的页面效果。webpack
// 第 1 步:建立一个 Vue 实例
const Vue = require('vue')
const app = new Vue({
template: `<div>Hello Vue SSR</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 Vue SSR</div>
})
复制代码
上面只是生产了一个html代码片断,通常来讲,须要将html片断插入一个模板文件里。OK,那咱们就来写一个模板文件index.html
。ios
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head><title>Hello</title></head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
复制代码
在渲染的时候,html片断会被插入到<!--vue-ssr-outlet-->
这个注释标记这里。git
const Vue = require('vue')
const app = new Vue({
template: `<div>Hello Vue SSR</div>`
})
const renderer = require('vue-server-renderer').createRenderer({
template: require('fs').readFileSync('./index.html', 'utf-8')
})
renderer.renderToString(app, (err, html) => {
if (err) throw err
console.log(html) // html 将是注入应用程序内容的完整页面
})
复制代码
咱们用fs
模块将文件读取进来丢入render的template
中,再重复上述步骤将html片断插入到咱们的标记位。github
如今,咱们将使用express来启动一个node服务,验证一下页面效果。web
const Vue = require('vue')
// 第一步: 建立一个 express 应用
const server = require('express')()
// 第二步: 建立一个 Vue 实例
const app = new Vue({
data: {
msg: 'Hello Vue SSR'
},
template: `<div>{{msg}}</div>`
})
// 第三步: 建立一个 renderer
const renderer = require('vue-server-renderer').createRenderer({
template: require('fs').readFileSync('./index.html', 'utf-8')
})
// 第四步: 设置路由,"*" 表示任意路由均可以访问它
server.get('*', (req, res) => {
renderer.renderToString(app, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
}
res.end(html)
})
})
// 第五步: 启动服务并监遵从8080端口进入的全部链接请求
server.listen(8080)
复制代码
这样,咱们的一个简单的页面渲染就完成了,看下页面效果和Response数据。vue-router
知道了怎么在服务器端渲染出一个页面,下一步就是实现同构啦。为了跳过各类项目配置,咱们就从熟悉的vue-cli模板下手。
官方提供了vue-cli的项目快速构建工具,能够用它也进行SPA项目的快速搭建,咱们如今就把这个模板,改形成一个可以集成SSR的模板。
第二部份内容参考让vue-cli初始化后的项目集成支持SSR,侵删。
安装vue-cli (至少v2.x版本)后,使用基础模板搭建个项目
vue init webpack spa_ssr
cd spa_ssr
复制代码
跑一下确保项目可以正常运行,而后记得安装vue-server-renderer
模块
npm install vue-server-renderer --save-dev
复制代码
安装完成,咱们就开始进入下一步。
src
下的文件咱们须要在src
目录下建立两个js。
src
├── router
│ └── index.js
├── components
│ └── HelloSsr.vue
├── App.vue
├── main.js
├── entry-client.js # 仅运行于浏览器
└── entry-server.js # 仅运行于服务器
复制代码
这两个entry
以后会进行配置,先来改造main.js
。
在改造main.js
以前,须要说明一下,因单线程的机制,在服务器端渲染时,过程当中有相似于单例的操做,那么全部的请求都会共享这个单例的操做,因此应该使用工厂函数来确保每一个请求之间的独立性。好比在main.js
中,咱们原先直接建立一个Vue实例,并直接挂载到DOM。如今的main.js
做为通用entry文件,它应该改形成一个能够重复执行的工厂函数,为每一个请求建立新的应用程序实例。挂载的工做,是由以后的客户端entry来完成。
import Vue from 'vue'
import App from './App'
import { CreateRouter } from './router'
export function createApp () {
const router = new CreateRouter()
const app = new Vue({
router,
render: h => h(App)
})
return { app, router }
}
复制代码
在/router/index.js
中,咱们一样须要使用工厂函数来建立路由实例。而后将路由配置改成history
模式(由于哈希不支持)
import Vue from 'vue'
import Router from 'vue-router'
import HelloSsr from '@/components/HelloSsr'
Vue.use(Router)
export function CreateRouter () {
return new Router({
mode: 'history',
routes: [{
path: '/ssr',
name: 'HelloSsr',
component: HelloSsr
}]
})
}
复制代码
接下来咱们来写客户端的entry和服务器端的entry。客户端的entry要作的很简单,就是将vue实例挂载到DOM上,只不过,考虑到可能存在异步组件,须要等到路由将异步组件加载完毕,才进行此操做。
// entry-client.js
import { createApp } from './main'
const { app, router } = createApp()
router.onReady(() => {
app.$mount('#app')
})
复制代码
服务器entry要作的有两步:1.解析服务器端路由;2.返回一个vue实例用于渲染。
// entry-server.js
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) {
// eslint-disable-next-line
return reject({ code: 404 })
}
// Promise 应该 resolve 应用程序实例,以便它能够渲染
resolve(app)
}, reject)
})
}
复制代码
webpack
配置vue相关代码已处理完毕,接下来就须要对webpack
打包配置进行修改了。 官方推荐了下面配置:
build
├── webpack.base.conf.js # 基础通用配置
├── webpack.client.conf.js # 客户端打包配置
└── webpack.server.conf.js # 服务器端打包配置
复制代码
咱们的项目中的配置文件是base
,dev
,prod
,如今咱们仍然保留这三个配置文件,只须要增长webpack.server.conf.js
便可。
webpack.base.conf.js
修改咱们首先修改webpack.base.conf.js
的entry
入口配置为:./src/entry-client.js
,来生成客户端的构建清单client manifest
。服务器端的配置因为引用base配置,entry
会经过merge
覆盖,来指向server-entry.js
。
// webpack.base.conf.js
module.exports = {
entry: {
// app: './src/main.js'
app: './src/entry-client.js' // <-修改入口文件改成
},
// ...
}
复制代码
webpack.prod.conf.js
修改在客户端的配置prod
中,咱们须要引入一个服务器端渲染的插件client-plugin
,用来生成vue-ssr-client-manifest.json
(用做静态资源注入),同时,咱们须要把HtmlWebpackPlugin
给去掉,在SPA应用中,咱们用它来生成index.html
文件,可是这里咱们有vue-ssr-client-manifest.json
以后,服务器端会帮咱们作好这个工做。
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
// ...
plugins: [
new webpack.DefinePlugin({
'process.env': env,
'process.env.VUE_ENV': '"client"' // 增长process.env.VUE_ENV
}),
// ...
// 如下内容注释(或去除)
// new HtmlWebpackPlugin({
// filename: config.build.index,
// template: 'index.html',
// inject: true,
// minify: {
// removeComments: true,
// collapseWhitespace: true,
// removeAttributeQuotes: true
// // more options:
// // https://github.com/kangax/html-minifier#options-quick-reference
// },
// // necessary to consistently work with multiple chunks via CommonsChunkPlugin
// chunksSortMode: 'dependency'
// }),
// ...
// 此插件在输出目录中生成 `vue-ssr-client-manifest.json`。
new VueSSRClientPlugin()
]
// ...
复制代码
webpack.server.conf.js
配置server
配置基本参考官方的配置,这里仍是说明下:
webpack-node-externals
来加快构建速度和减少打包体积,因此咱们要先安装一下它:npm install webpack-node-externals --save-dev
。prod
配置同样,这里须要引入并使用server-plugin
插件来生成vue-ssr-server-bundle.json
。这东西是用来等会作服务器端渲染的。const webpack = require('webpack')
const merge = require('webpack-merge')
const nodeExternals = require('webpack-node-externals')
const baseConfig = require('./webpack.base.conf.js')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
// 去除打包css的配置
baseConfig.module.rules[1].options = ''
module.exports = merge(baseConfig, {
// 将 entry 指向应用程序的 server entry 文件
entry: './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'
},
// https://webpack.js.org/configuration/externals/#function
// https://github.com/liady/webpack-node-externals
// 外置化应用程序依赖模块。可使服务器构建速度更快,
// 并生成较小的 bundle 文件。
externals: nodeExternals({
// 不要外置化 webpack 须要处理的依赖模块。
// 你能够在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
// 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单
whitelist: /\.css$/
}),
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
'process.env.VUE_ENV': '"server"'
}),
// 这是将服务器的整个输出
// 构建为单个 JSON 文件的插件。
// 默认文件名为 `vue-ssr-server-bundle.json`
new VueSSRServerPlugin()
]
})
复制代码
package.json
打包命令修改"scripts": {
//...
"build:client": "node build/build.js",
"build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.conf.js --progress --hide-modules",
"build": "rimraf dist && npm run build:client && npm run build:server"
}
复制代码
这里须要先安装cross-env
。(cross-env
用来防止使用NODE_ENV =production 来设置环境变量时,Windows命令提示会报错)
npm install --save-dev cross-env
复制代码
index.html
如第一层说的,咱们须要在这个index.html
外层模板文件中,插入一个<!--vue-ssr-outlet-->
注释标记,用来标识服务器渲染的html代码片断插入的地方,同时删掉原先的<div id="app">
。
服务器端会在这个标记的位置自动生成一个<div id="app" data-server-rendered="true">
,客户端会经过app.$mount('#app')
挂载到服务端生成的元素上,并变为响应式的。
npm run build
复制代码
在dist目录下会生成两个json文件:vue-ssr-server-bundle.json
和vue-ssr-client-manifest.json
,用于服务端端渲染和静态资源注入。
这里仍是采用express
来做为服务器端,先进行安装:
npm install express --save
复制代码
以后在根目录下建立server.js
,代码主要分为3步:
createBundleRenderer
来建立renderer
,咱们引入以前生成好的json文件,并读取index.html
做为外层模板;const express = require('express')
const app = express()
const fs = require('fs')
const path = require('path')
const { createBundleRenderer } = require('vue-server-renderer')
const resolve = file => path.resolve(__dirname, file)
// 生成服务端渲染函数
const renderer = createBundleRenderer(require('./dist/vue-ssr-server-bundle.json'), {
// 模板html文件
template: fs.readFileSync(resolve('./index.html'), 'utf-8'),
// client manifest
clientManifest: require('./dist/vue-ssr-client-manifest.json')
})
function renderToString (context) {
return new Promise((resolve, reject) => {
renderer.renderToString(context, (err, html) => {
err ? reject(err) : resolve(html)
})
})
}
app.use(express.static('./dist'))
app.use(async(req, res, next) => {
try {
const context = {
title: '服务端渲染测试', // {{title}}
url: req.url
}
// 设置请求头
res.set('Content-Type', 'text/html')
const render = await renderToString(context)
// 将服务器端渲染好的html返回给客户端
res.end(render)
} catch (e) {
console.log(e)
// 若是没找到,放过请求,继续运行后面的中间件
next()
}
})
app.listen(3000)
复制代码
完过后启动服务命令:
node server.js
复制代码
访问localhost:3000/ssr,就能获取咱们以前定义好的页面。
Nuxt.js
源码初探Nuxt.js
是什么Nuxt.js
是Vue官方推荐的一个项目,它是一个基于 Vue.js 的通用应用框架。预设了服务器端渲染所需的各类配置,如异步数据,中间件,路由,只要遵循其中的规则就能轻松实现SSR。开箱即用,体验友好。经过对客户端/服务端基础架构的抽象组织,Nuxt.js 主要关注的是应用的 UI渲染。
Nuxt.js
的一小小小部分源码解读Nuxt.js
源码涉及的内容比较多,咱们不一一细说(好吧,是我功力不够,吃不透 = =||)。咱们就来看看,Nuxt.js
做为中间件的时候,整个流程都干了些什么。
Nuxt.js
官方提供的examples里有一个custom-server
,Nuxt
会做为中间件传入express中,咱们来看下代码:
import express from 'express'
import { Nuxt, Builder } from 'nuxt'
const app = express()
const host = process.env.HOST || '127.0.0.1'
const port = process.env.PORT || 3000
// Import and set Nuxt.js options
let config = require('./nuxt.config.js')
config.dev = !(process.env.NODE_ENV === 'production')
const nuxt = new Nuxt(config)
// Start build process in dev mode
if (config.dev) {
const builder = new Builder(nuxt)
builder.build()
}
// Give nuxt middleware to express
app.use(nuxt.render)
// Start express server
app.listen(port, host)
复制代码
这段代码有部分地方和咱们以前写的类似,都用express起了一个服务,这里涉及Nuxt.js
的代码有2处:
Nuxt是根据pages文件夹下的目录结构来生成对应的路由的,这是Nuxt的特色之一,这部份内容咱们这里不展开细说,之后再详细讨论。咱们主要来讲说new Nuxt()
里的事。
上面咱们看的,咱们是将Nuxt
的实例化对象的render属性值传做为中间件传给了express,咱们在源码中全局搜索找到Nuxt
构造函数:
Renderer
构造函数。代码太长我就不全贴了。这么多内容感受仍是无从下手,不慌,既然以前说了,
vue-server-renderer
是SSR服务器端渲染的核心模块,那咱们尝试在这里搜索
vue-server-renderer
,果真,搜到以下内容:
bundle renderer
,这不就是咱们以前本身实现的服务器渲染函数么,它的第一个参数
this.resources.serverBundle
,咱们在文件中能够搜到:
Nuxt.js
一样是使用预编译的应用程序包
createBundleRenderer
来建立了渲染器,其中所需的
server-bundle.json
和
client-mainfest.json
会由nuxt在运行的时候自动构建生成。
renderRoute
方法中找到了它:
Nuxt.js
的核心服务器端渲染原理和咱们以前写的demo基本相同。
服务器端渲染的优劣都很是明显,若是仅仅是为了优化网页的SEO,咱们还能够尝试Vue官方给咱们推荐的预渲染(Prerendering),这里就很少赘述。 以上Demo仅仅是做为服务器端渲染的一种实现demo,若是须要正式用到项目中,还须要更加复杂的配置。若是只是使用服务器端渲染来开发简单的项目,咱们能够直接用Nuxt.js
便可。 以上有些知识点在概念上有误差,欢迎指正。