一、简介
Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。
服务器渲染的 Vue.js 应用程序也可以被认为是"同构"或"通用",因为应用程序的大部分代码都可以在服务器和客户端上运行
二、 Ssr优势
与传统 SPA(Single-Page Application - 单页应用程序)相比,服务器端渲染(SSR)的优势主要在于:
三、 构建图
构建的应用层,和之前在使用前端渲染项目架构中一样,此时使用服务端渲染,需要针对服务端和客户端进行两次不同的打包,所以webpack打包时,需要相应的两个文件,一个是server-entry,一个是client-entry,同时也生成两个文件,一个是vue-ssr-client-manifest.json文件,一个是vue-ssr-server-bundle.json,这两个文件便是vue-server-renderer针对客户端和服务端的两个配置文件;
五、 项目目录结构
├── Readme.md // help
├── client //客户端应用代码
│
│ │── config // 配置
│ ├── config.js // 唯一可以配置的地方
│ ├── device.js // pc和mobile设备的选择
│ ├── webpack.base.js // webpack基本配置
│ ├── webpack.build.js // webpack线上环境
│ ├── webpack.dev.js // webpack本地环境
│ ├── webpack.style.js // 关于css样式的处理
│ ├── src
│ ├── components // 组件
│ ├── directives // 指令
│ ├── router // 路由
│ ├── static // 静态文件
│ ├── store // vuex
│ ├── client //存储的store信息为客户相应的
│ ├── server //在服务端进行store操作的配置
│ ├── index.html // 单页面入口html
│ ├── app.js // 主入口文件
│ ├── entry-client.js // webpack客户端打包的入口文件
│ ├── entry-server.js // webpack服务端打包的入口文件
│
├── server //服务端代码以及线上部署目录
│
│ ├── api //接口文件
│ ├── data.js //mock的数据文件
│ ├── index.js //mock的接口文件
│ ├── bin //项目启动目录
│ ├── www //node的启动文件
│ ├── index.js //mock的接口文件
│ ├── config //项目配置
│ ├── config.js //项目配置文件
│ ├── db_connect.js //数据库连接,以及操作封装
│ ├── db //数据库sql文件
│ ├── public //服务端静态文件夹
│ ├── routes //服务端页面路由
│ ├── views //服务端路由对应的静态模板
│ ├── app.js //服务端express的server配置
│ ├── server.js //ssr本地环境和线上环境的区分
├── .editorconfig
├── .gitignore
├── package.json
六、 应用代码(client):
注:如果现在已有的vue的客户端渲染应用代码(src文件夹),则只需要进行如下文件的配置就好,对于应用代码不再做过多的叙述;
(一) 针对app,router,store的抛出:
import Vue from ‘vue’;
import VueRouter from ‘vue-router’;
Vue.use(VueRouter);
export function createRouter() {
return new VueRouter({
mode: ‘history’,
fallback: false,
scrollBehavior: () => ({ y: 0 }),
routes: [{
path: ‘/’,
component: () =>
import (’…/components/main’)
},
{
path: ‘/one’,
component: () =>
import (’…/components/one’)
}
]
})
}
2. Store
在此文件夹中,划分两个store,一个是客户端应用store,一个是服务端应用store;
Client:所有的action操作由客户端触发;
Server:在服务端页面相应之前,必须执行组件内部相应的actions进行数据的请求,请求完毕与模板进行拼接渲染后,在响应到客户端,同时,将服务端store中state的数据同步给客户端store中state
import Vuex from ‘vuex’;
import Vue from ‘vue’;
Vue.use(Vuex);
import { serverActions, serverMutations, serverState } from “./server”
export * from ‘./server’;
import { clientMutations, clientActions, clientState } from “./client”
export * from ‘./client’;
const storeConfig = {
state: {…clientState, …serverState },
actions: {…clientActions, …serverActions },
mutations: {…clientMutations, …serverMutations }
};
export function createStore() {
return new Vuex.Store(storeConfig)
}
import Vue from ‘vue’
import App from ‘./components/app.vue’
import { createStore } from ‘./store’
import { createRouter } from ‘./router’
import VueMeta from “vue-meta”
import { sync } from ‘vuex-router-sync’ //将router挂载到store上
Vue.use(VueMeta)
//暴露一个可以重复执行的工厂函数,为每个请求创建新的应用程序实例
export function createApp() {
// create store and router instances
const store = createStore()
const router = createRouter()
// sync the router with the vuex store.
// this registers `store.state.route`
sync(store, router)
// create the app instance.
// here we inject the router, store and ssr context to all child components,
// making them available everywhere as `this.$router` and `this.$store`.
const app = new Vue({
router,
store,
render: h => h(App)
})
// expose the app, the router and the store.
// note we are not mounting the app here, since bootstrapping will be
// different depending on whether we are in a browser or on the server.
return { app, router, store }
}
(二) 创建webpack的入口文件entry-client.js和entry-server.js
import Vue from ‘vue’
import { createApp } from ‘./app’
const { app, router, store } = createApp()
if (window.INITIAL_STATE) {
store.replaceState(window.INITIAL_STATE)
}
router.onReady(() => {
// actually mount to DOM
app.$mount(’#app’)
})
import { createApp } from ‘./app’
const isDev = process.env.NODE_ENV !== ‘production’
export default context => {
return new Promise((resolve, reject) => {
const s = isDev && Date.now()
const { app, router, store } = createApp()
const { url } = context
const { fullPath } = router.resolve(url).route
if (fullPath !== url) {
return reject({ url: fullPath })
}
// const meta = app.$meta()
// context.meta = meta
router.push(url)
router.onReady(() => {
const matchedComponents = router.getMatchedComponents()
// 如果没有组件,说明该路由不存在,报错404
if (!matchedComponents.length) {
return reject({ code: 404 })
}
// 遍历路由下所以的组件,如果有需要服务端渲染的请求,则进行请求
Promise.all(matchedComponents.map(({ asyncData }) => {
return asyncData && asyncData({
store,
route: router.currentRoute
})
})).then(() => {
isDev && console.log(`data pre-fetch: ${Date.now() - s}ms`)
context.state = store.state
resolve(app)
}).catch(reject)
}, reject)
})
}
(三) Index.html模板文件处理
Document
(四) 创建webpack.client.js和webpack.server.js
针对webpack.base.js按照之前vue客户端渲染时,正常配置,不做过多叙述
var path = require(“path”)
var webpack = require(“webpack”)
var config = require("./config")
const merge = require(‘webpack-merge’)
const baseWebpack = require("./webpack.base.js")
//生成客户端插件
const VueSSRClientPlugin = require(‘vue-server-renderer/client-plugin’)
module.exports = merge(baseWebpack, {
entry: config.client.entry,
devtool: config.client.devtool,
plugins: [
// strip dev-only code in Vue source
new webpack.DefinePlugin({
‘process.env.NODE_ENV’: JSON.stringify(process.env.NODE_ENV || ‘development’),
‘process.env.VUE_ENV’: ‘“client”’
}),
new VueSSRClientPlugin()
],
})
2. Webpack.server.js
var path = require(“path”)
var webpack = require(“webpack”)
var config = require("./config")
const merge = require(‘webpack-merge’)
const baseWebpack = require("./webpack.base.js")
//生成服务端文件插件
const VueSSRServerPlugin = require(‘vue-server-renderer/server-plugin’)
module.exports = merge(baseWebpack, {
target: ‘node’,
devtool: ‘#source-map’,
entry: config.server.entry,
output: {
filename: ‘server-bundle.js’,
libraryTarget: ‘commonjs2’
},
plugins: [
new webpack.DefinePlugin({
‘process.env.NODE_ENV’: JSON.stringify(process.env.NODE_ENV || ‘development’),
‘process.env.VUE_ENV’: ‘“server”’
}),
new VueSSRServerPlugin()
],
})
(五) 配置dev.server.js本地服务:借助(webpack-hot-middleware和webpack-dev-middleware)
const fs = require(‘fs’)
const path = require(‘path’)
const MFS = require(‘memory-fs’) //js内存中进行数据存储
const webpack = require(‘webpack’)
const chokidar = require(‘chokidar’) //文件监控
const clientConfig = require(’./webpack.client.js’)
const serverConfig = require(’./webpack.server.js’)
//文件读取
const readFile = (fs, file) => {
try {
return fs.readFileSync(path.join(clientConfig.output.path, file), ‘utf-8’)
} catch (e) {}
}
//设置本地服务
module.exports = function setupDevServer(app, templatePath, cb) {
//webpack打包的文件
let bundle
let template
let clientManifest
let ready //将pormise的resolve方法赋值给ready
const readyPromise = new Promise(r => { ready = r })
//当html模板和webpack进行新的打包后,进行重载
const update = () => {
if (bundle && clientManifest) {
ready()
cb(bundle, {
template,
clientManifest
})
}
}
// html模板读取
template = fs.readFileSync(templatePath, 'utf-8')
//监听html模板的改变
chokidar.watch(templatePath).on('change', () => {
template = fs.readFileSync(templatePath, 'utf-8')
console.log('index.html template updated.')
update()
})
//webpack的热启动的入口文件特殊处理
clientConfig.entry = ['webpack-hot-middleware/client', clientConfig.entry]
clientConfig.plugins.push(
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin() //错误不中断进程
)
// 根据webpack配置生成webpack配置
const clientCompiler = webpack(clientConfig)
// dev-middleware配置
const devMiddleware = require('webpack-dev-middleware')(clientCompiler, {
publicPath: clientConfig.output.publicPath,
noInfo: true //控制台不显示信息(只有警告和错误)默认:false
// quiet:‘控制台什么多不显示 默认:false’,
// lazy:‘转换到惰性模式 默认:false’,
// filename:‘在大多数情况下,这个等于output.filename webpack配置选项。’,
// watchOptions.aggregateTimeout:‘第一个变化后推迟重建,以ms为单位的时 间值,默认:300’,
// watchOptions.poll:‘true:使用轮询;number:用于轮询的间隔;默认值: undefined’,
// publicPath:‘建立中间件服务路径;在大多数情况下,这个等于output.publicPath webpack配置选项。’
// headers:‘添加自定义headers{"x-Custom-Header":"yes"}’,
// stats:‘Output options for the stats. Seenode.js API.’,
// middleware.invalidate():‘手动编译无效。有用的编译器已经改变了。’,
// middleware.fileSystem:‘一个可读的(内存)可以访问编译数据的文件系统’
})
app.use(devMiddleware)
//client端对于文件修改进行重新编译后执行的事件
clientCompiler.plugin('done', stats => {
stats = stats.toJson()
stats.errors.forEach(err => console.error(err))
stats.warnings.forEach(err => console.warn(err))
if (stats.errors.length) return
//将vue-ssr-client-mainfest中的数据重新读取
clientManifest = JSON.parse(readFile(
devMiddleware.fileSystem, //一个可读的(内存)可以访问编译数据的文件系统
'vue-ssr-client-manifest.json'
))
update()
})
// hot middleware
app.use(require('webpack-hot-middleware')(clientCompiler, { heartbeat: 5000 }))
// watch and update server renderer
const serverCompiler = webpack(serverConfig)
const mfs = new MFS() //开辟一块内存
serverCompiler.outputFileSystem = mfs
serverCompiler.watch({}, (err, stats) => {
if (err) throw err
stats = stats.toJson()
if (stats.errors.length) return
// 读取 bundle从vue-ssr-webpack-plugin
bundle = JSON.parse(readFile(mfs, 'vue-ssr-server-bundle.json'))
update()
})
return readyPromise
}
七、 服务代码(server)
const fs = require(‘fs’)
const path = require(‘path’)
const LRU = require(‘lru-cache’) //内存管理
const express = require(‘express’)
const resolveClient = file => path.resolve(__dirname, ‘…/client/’ + file)
const resolve = file => path.resolve(__dirname, file)
const { createBundleRenderer } = require(‘vue-server-renderer’)
const isProd = process.env.NODE_ENV.trim() == ‘production’
const serverInfo =
express/${require('express/package.json').version}
+
vue-server-renderer/${require('vue-server-renderer/package.json').version}
//根据生成配置创建渲染对象
function createRenderer(bundle, options) {
return createBundleRenderer(bundle, Object.assign(options, {
cache: LRU({
max: 1000,
maxAge: 1000 * 60 * 15
}),
basedir: resolve(’./dist’),
runInNewContext: false
}))
}
let renderer //渲染机制
let readyPromise
let templatePath = resolve(’./dist/index.html’)
// 本地服务和线上服务
function differentiate(app) {
if (isProd) {
const template = fs.readFileSync(templatePath, 'utf-8')
const bundle = require('./dist/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')
const options = {
template,
clientManifest
}
renderer = createRenderer(bundle, options)
} else {
templatePath = resolveClient('src/index.html')
//异步server处理,经过webpack中间处理后得到bundle,clientManifest等文件
readyPromise = require(resolveClient('config/dev.server.js'))(
app,
templatePath,
(bundle, options) => {
renderer = createRenderer(bundle, options)
}
)
}
}
function render(req, res) {
const s = Date.now()
//设置请求头
res.setHeader(“Content-Type”, “text/html”)
res.setHeader(“Server”, serverInfo)
//错误处理
const handleError = err => {
if (err.url) {
res.redirect(err.url)
} else if (err.code === 404) {
res.status(404).send(‘404 | Page Not Found’)
} else {
// Render Error Page or Redirect
res.status(500).send(‘500 | Internal Server Error’)
console.error(error during render : ${req.url}
)
console.error(err.stack)
}
}
const context = {
title: 'Vue HN 2.0', // default title
url: req.url
}
renderer.renderToString(context, (err, html) => {
// //通过vue-meta注入进来的数据
if (err) {
return handleError(err)
}
res.send(html)
if (!isProd) {
console.log(`whole request: ${Date.now() - s}ms`)
}
})
}
module.exports = function(app) {
differentiate(app)
app.get('*', isProd ? render : (req, res) => {
readyPromise.then(() => render(req, res))
})
}
代码:
var createError = require(‘http-errors’);
var express = require(‘express’);
var path = require(‘path’);
var cookieParser = require(‘cookie-parser’);
var logger = require(‘morgan’);
const fs = require(“fs”)
const serverHandle = require("./server")
var app = express();
// view engine setup
app.set(‘views’, path.join(__dirname, ‘views’));
app.set(‘view engine’, ‘jade’);
app.use(logger(‘dev’));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
const serve = (path, cache) => express.static(resolve(path), {
maxAge: cache && isProd ? 1000 * 60 * 60 * 24 * 30 : 0
})
app.use(’/dist’, express.static(path.join(__dirname, ‘dist’)))
// app.use(express.static(path.join(__dirname, ‘dist’)));
app.use(express.static(path.join(__dirname, ‘public’)));
// var indexRouter = require(’./routes/index’);
// app.use(’/users’, usersRouter);
//接口
var api = require(’./api’);
app.use(’/api’, api);
//服务端渲染
serverHandle(app)
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get(‘env’) === ‘development’ ? err : {};
// render the error page
res.status(err.status || 500);
res.render(‘error’);
});
module.exports = app;
nuxt服务端渲染基本使用
参照文档:
https://zh.nuxtjs.org/guide/installation