Vue服务端渲染

一、简介

Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。
服务器渲染的 Vue.js 应用程序也可以被认为是"同构"或"通用",因为应用程序的大部分代码都可以在服务器和客户端上运行

二、 Ssr优势

与传统 SPA(Single-Page Application - 单页应用程序)相比,服务器端渲染(SSR)的优势主要在于:

  1. 更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。
    请注意,截至目前,Google 和 Bing 可以很好对同步 JavaScript 应用程序进行索引。在这里,同步是关键。如果你的应用程序初始展示 loading 菊花图,然后通过 Ajax 获取内容,抓取工具并不会等待异步完成后再行抓取页面内容。也就是说,如果 SEO 对你的站点至关重要,而你的页面又是异步获取内容,则你可能需要服务器端渲染(SSR)解决此问题。
  2. 更快的内容到达时间(time-to-content)
    特别是对于缓慢的网络情况或运行缓慢的设备。无需等待所有的 JavaScript 都完成下载并执行,才显示服务器渲染的标记,所以你的用户将会更快速地看到完整渲染的页面。通常可以产生更好的用户体验,并且对于那些「内容到达时间(time-to-content)与转化率直接相关」的应用程序而言,服务器端渲染(SSR)至关重要。
    使用服务器端渲染(SSR)时还需要有一些权衡之处:
    1. 开发条件所限。浏览器特定的代码,只能在某些生命周期钩子函数(lifecycle hook)中使用;一些外部扩展库(external library)可能需要特殊处理,才能在服务器渲染应用程序中运行。
    2. 涉及构建设置和部署的更多要求。与可以部署在任何静态文件服务器上的完全静态单页面应用程序(SPA)不同,服务器渲染应用程序,需要处于 Node.js server 运行环境。
    3. 更多的服务器端负载。在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用 CPU 资源(CPU-intensive - CPU 密集),因此如果你预料在高流量环境(high traffic)下使用,请准备相应的服务器负载,并明智地采用缓存策略。
      在对你的应用程序使用服务器端渲染(SSR)之前,你应该问的第一个问题是,是否真的需要它。这主要取决于内容到达时间(time-to-content)对应用程序的重要程度。例如,如果你正在构建一个内部仪表盘,初始加载时的额外几百毫秒并不重要,这种情况下去使用服务器端渲染(SSR)将是一个小题大作之举。然而,内容到达时间(time-to-content)要求是绝对关键的指标,在这种情况下,服务器端渲染(SSR)可以帮助你实现最佳的初始加载性能

三、 构建图

构建的应用层,和之前在使用前端渲染项目架构中一样,此时使用服务端渲染,需要针对服务端和客户端进行两次不同的打包,所以webpack打包时,需要相应的两个文件,一个是server-entry,一个是client-entry,同时也生成两个文件,一个是vue-ssr-client-manifest.json文件,一个是vue-ssr-server-bundle.json,这两个文件便是vue-server-renderer针对客户端和服务端的两个配置文件;

Vue服务端渲染_第1张图片
四、 特点:

  1. 在纯客户端应用程序(client-only app)中,每个用户会在他们各自的浏览器中使用新的应用程序实例。对于服务器端渲染,我们也希望如此:每个请求应该都是全新的、独立的应用程序实例,所以使用工厂模式进行实例的构建;
  2. 必须在得到数据后进行渲染,所以在渲染和响应html之前,我就已经拿到数据,所以服务端的数据响应式是多余的;
  3. 只有 beforeCreate 和 created 会在服务器端渲染(SSR)过程中被调用,所以应该避免在 beforeCreate 和 created 生命周期时产生全局副作用的代码,例如在其中使用 setInterval 设置 timer
  4. 大多数自定义指令直接操作 DOM,因此会在服务器端渲染(SSR)过程中导致错误

五、 项目目录结构

├── 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的抛出:

  1. Router/index.js(对于服务端创建,每一次打包必须创建一个新的router实例)

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)
}

  1. App.js

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

  1. Entry-client.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’)
})

  1. Entry-server.js

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客户端渲染时,正常配置,不做过多叙述

  1. Webpack.client.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 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)

  1. Server.js(本地开发或者线上部署判断)

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))
})

}

  1. App.js
    执行server.js中抛出的函数,并且注入静态文件夹:
    app.use(’/dist’, express.static(path.join(__dirname, ‘dist’)))

代码:
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

你可能感兴趣的:(前端技术框架)