1.首先利用vue-cli初始化项目:
npm init webpack ssr-demo
然后启动项目,确保项目能够正确跑起来:
npm run dev
初始项目结构:

这里可以看到服务端返回回来界面只有一个 id=“app”的div

这一步完之后就开始我们的SSR之路了~
2.集成SSR
2.1首先安装SSR支持
npm i -D vue-server-renderer
注意:vue-server-renderer版本需要和vue版本一致,相信很多人都不会注意这个的。
2.2在src目录下创建两个js文件
src
├── entry-client.js # 仅运行于浏览器
└── entry-server.js # 仅运行于服务器
2.3修改路由配置
路由可以说是在一个网站最重要的了,即使是服务端渲染也要共用一套路由系统,为了避免单例的影响,我们为每一个请求都导出一个新的路由实例。(单例模式:单例模式是一种常用的软件设计模式,其定义是单例对象的类只能允许一个实例存在。)
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
Vue.use(Router)
export function createRouter () {
return new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'Hello',
component: HelloWorld
}
]
})
}
2.4改造main.js文件
import Vue from 'vue'
import App from './App'
import { createRouter } from './router'
Vue.config.productionTip = false
export function createApp () {
const router = new createRouter()//创建router实例
const app = new Vue({
router,
render: h => h(app)//vue2.0的写法,render函数是渲染一个视图,然后提供给el挂载,如果没有render那页面什么都不会出来
})
return { app, router}
}
2.5在entry-client里面添加以下内容
import { createApp } from './main'
const { app, router} = createApp()
// 因为可能存在异步组件,所以等待router将所有异步组件加载完毕,服务器端配置也需要此操作
router.onReady(()=> {
app.$mount('#app')
})
2.6在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)
})
}
到这里vue的相关配置就完成了,接下来就是webpack的相关配置了:
2.7webpack配置
初始项目的相关webpack配置:

初始化配置文件里面包含了base,dev,prod三个文件,我们只需要在增加一个webpack.server.conf.js即可。
2.8webpack客户端的配置
(1)修改webpack.base.conf.js的entry入口配置为: ./src/entry-client.js。这样原 dev 配置与 prod 配置都不会受到影响。

注:服务器端的配置也会引用base配置,但是会将entry通过merge覆盖为entry.server.js
(2)在webpack.prod.conf.js文件中引入一个插件,并配置到plugin中;
另外需要将 prod 的HtmlWebpackPlugin 去除,因为我们有了vue-ssr-client- manifest.json之后,服务器端会帮我们做好这个工作;
然后增加process.env.VUE_ENV:
const path = require('path')
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')//增加插件
const env = require('../config/prod.env')
const webpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({
sourceMap: config.build.productionSourceMap,
extract: true,
usePostCSS: true
})
},
devtool: config.build.productionSourceMap ? config.build.devtool : false,
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
},
plugins: [
// http://vuejs.github.io/vue-loader/en/workflow/production.html
new webpack.DefinePlugin({
'process.env': env,
'process.env.VUE_ENV': '"client"'//添加'process.env.VUE_ENV': '"client"'
}),
new UglifyJsPlugin({
uglifyOptions: {
compress: {
warnings: false
}
},
sourceMap: config.build.productionSourceMap,
parallel: true
}),
// extract css into its own file
new ExtractTextPlugin({
filename: utils.assetsPath('css/[name].[contenthash].css'),
// Setting the following option to `false` will not extract CSS from codesplit chunks.
// Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
// It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`,
// increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
allChunks: true,
}),
// Compress extracted CSS. We are using this plugin so that possible
// duplicated CSS from different components can be deduped.
new OptimizeCSSPlugin({
cssProcessorOptions: config.build.productionSourceMap
? { safe: true, map: { inline: false } }
: { safe: true }
}),
// generate dist index.html with correct asset hash for caching.
// you can customize output by editing /index.html
// see https://github.com/ampedandwired/html-webpack-plugin
// new HtmlWebpackPlugin({//注释掉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'
// }),
// keep module.id stable when vendor modules does not change
new webpack.HashedModuleIdsPlugin(),
// enable scope hoisting
new webpack.optimize.ModuleConcatenationPlugin(),
// split vendor js into its own file
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks (module) {
// any required modules inside node_modules are extracted to vendor
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
)
}
}),
// extract webpack runtime and module manifest to its own file in order to
// prevent vendor hash from being updated whenever app bundle is updated
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
minChunks: Infinity
}),
// This instance extracts shared chunks from code splitted chunks and bundles them
// in a separate chunk, similar to the vendor chunk
// see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
new webpack.optimize.CommonsChunkPlugin({
name: 'app',
async: 'vendor-async',
children: true,
minChunks: 3
}),
// copy custom static assets
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.build.assetsSubDirectory,
ignore: ['.*']
}
]),
// 此插件在输出目录中
// 生成 `vue-ssr-client-manifest.json`。
new VueSSRClientPlugin()
]
})
2.9webpack服务端的配置
server的配置有用到新插件运行安装: npm i -D webpack-node-externals
webpack.server.conf.js配置如下:
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()
]
})
2.10 配置package.json增加打包服务器端构建命令并修改原打包命令
"scripts": {
"dev": "node build/dev-server.js",
"start": "npm run dev",
"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",
"lint": "eslint --ext .js,.vue src",
"start-prod": "node server.js"
},
2.11修改index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>ssrnewdemo</title>
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
原来的<div id="app">删掉,只在 body 中保留一个标记即可:<!--vue-ssr-outlet-->。 服务器端会在这个标记的位置自动生成一个<div id="app" data-server-rendered="true">,客户端会通过app.$mount('#app')挂载到服务端生成的元素上,并变为响应式的。
2.12 运行构建命令
npm run build
然后在dist目录下可见生成的两个 json 文件: vue-ssr-server-bundle.json与vue-ssr-client-manifest.json。
这两个文件都会应用在 node 端,进行服务器端渲染与注入静态资源文件。
遇到的问题:
1.可能是我之前安装失败了,从新安装一遍:
npm install vue vue-server-renderer --save

2.'cross-env' 不是内部或外部命令,也不是可运行的程序或批处理文件,安装一下:
npm i -D cross-env

3.这个错误似乎没有影响,可以先暂时不管,生成两个json文件就好了:

生成两个json文件:

2.13 构建服务器端(官方使用的express,这里我们使用koa2)
首先安装koa2:
npm i -S koa
然后在项目根目录创建server.js文件,添加内容:
const Koa = require('koa')
const app = new Koa()
// response
app.use(ctx => {
ctx.body = 'Hello Koa'
})
app.listen(3001)
然后运行:node server.js,访问localhost:3001,确保浏览器得到了Hello Koa。

2.14编写服务端代码
const Koa = require('koa')
const app = new Koa()
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'), {
// 推荐
runInNewContext: false,
// 模板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(require('koa-static')(resolve('./dist')))
// response
app.use(async (ctx, next) => {
try {
const context = {
title: '服务端渲染测试', // default title
url: ctx.url
}
// 将服务器端渲染好的html返回给客户端
ctx.body = await renderToString(context)
// 设置请求头
ctx.set('Content-Type', 'text/html')
ctx.set('Server', 'Koa2 server side render')
} catch (e) {
// 如果没找到,放过请求,继续运行后面的中间件
next()
}
})
app.listen(3001)
.on('listening', () => console.log('服务已启动'))
.on('error', err => console.log(err))
运行启动服务命令:
node server.js
可能出现的错误:
1.Error: Cannot find module 'koa-static':
安装一下:
npm install koa-static

启动成功:

成功访问,这里可以看到服务器返回回来的已经是一个HTML页面了。
整个项目结构:

项目源码地址:Hacker233/vue-ssr-demo

