Vue同构(一): 快速上手

前言

  首先欢迎你们关注个人Github博客,也算是对个人一点鼓励,毕竟写东西无法得到变现,能坚持下去也是靠的是本身的热情和你们的鼓励。   javascript

同构(服务器渲染)

  Vue同构也就是咱们常说的服务器渲染(Server Side Render),服务器渲染放在今天已经算不上是一个新鲜的东西了,从React到Vue都有各自的服务器渲染方案,不少小伙伴可能都有所接触,首先咱们要了解一下为何须要服务器渲染呢?Vue和React这类框架有一个特色,都属于浏览器渲染,好比一个最简单的例子:   css

<div id="app">
  {{ message }}
</div>
复制代码
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})
复制代码

  咱们能够看到,咱们收到服务器的模板中其实并无咱们所期待界面对应的html结构,而仅有一个用于挂载应用的根元素,在客户端浏览器执行加载的JavaScript代码时,才会建立对应的DOM结构。然而浏览器渲染其实存在两个明显的缺点:html

  • 对搜索引擎优化(SEO:Search Engine Optimization)不友好,各个搜索引擎实际上都是对网页的html结构和同步Javascript代码进行索引,于是客户端渲染可能会形成你的网页没法被搜索引擎正确索引。
  • TTC(内容到达时间:Time-To-Conten)过长,试想若是设备的网络较差或者设备的代码执行速度较慢,用户须要等待较长的时间才能看到页面的内容,等待期间看到的都是网页的白屏或者其余的加载状态,这绝对是糟糕的用户体验。

  幸运的是,Node的到来为这一切带来了曙光,JavaScript不只仅能够在浏览器中执行,并且也可能在后端环境中执行。所以咱们能够将用户的界面在服务器中渲染成HTML 字符串,而后再传给浏览器,这样用户得到的就是可预览的界面,最后将静态标记"混合"为客户端上彻底交互的应用程序,整个渲染的过程就结束了。前端

最简单的例子

  Vue服务器渲染使用官方提供的库vue-server-renderer,因为Express比较直观,咱们采用Express做为后端服务器,咱们首先给出一个最简单的例子:vue

// server.js
const Vue = require('vue')
const server = require('express')()
// 建立一个 renderer
const renderer = require('vue-server-renderer').createRenderer()

server.get('*', (req, res) => {
  // 建立一个 Vue 实例
  const app = new Vue({
    data: {
      url: req.url
    },
    template: `<div>访问的 URL 是: {{ url }}</div>`
  })

  renderer.renderToString(app, (err, html) => {
    if (err) {
      res.status(500).end('Internal Server Error')
      return
    }
    // html就是Vue实例app渲染的html
    res.end(` <!DOCTYPE html> <html lang="en"> <head><title>Hello</title></head> <body>${html}</body> </html> `)
  })
})

server.listen(8080)
复制代码

  而后启动node server.js,而且浏览器中访问好比http://localhost:8080/app,浏览器界面中则会显示出:java

访问的 URL 是:/appnode

  这时候观察该请求的返回值是:webpack

  咱们发现返回的html中已经渲染好DOM元素。所以咱们无需等待当即能够看见页面的内容。而上面的代码逻辑也很是简单,http服务器接收到get请求的时候,都会建立一个Vue实例,vue-server-renderer中的createRenderer用来建立一个Renderer实例,Renderer中的renderToString用来将Vue实例转化对应的HTML字符串,须要注意的是,咱们须要将建立好的字符串包裹在html一并返回。固然你能够采用页面模板的形式,将二者相分离:git

<!DOCTYPE html>
<html lang="en">
  <head><title>Hello</title></head>
  <body>
    <!--vue-ssr-outlet-->
    <!--这里将是应用程序 HTML 标记注入的地方> </body> </html> 复制代码
// renderer中包含了模板
const renderer = createRenderer({
  template: require('fs').readFileSync('./index.template.html', 'utf-8')
})

renderer.renderToString(app, (err, html) => {
  res.end(html)
})
复制代码

  固然这只是最简单的一个例子,浏览器收到的仅仅是对应Vue实例的html代码,并无将其激活,所以是不可交互的。github

浏览器渲染的流程

  对于浏览器渲染中,咱们首选Webpack对代码进行打包,总体流程能够经过下面图来释义:

  对于一个Vue应用,源码层面其实主要包括三个方面: 组件、路由、状态管理。这部分代码咱们认为是通用代码,能够同时在服务器端和浏览器端执行,Webpack有两个入口: server entryclient entry,分别用来打包在服务器端执行的代码与在浏览器端执行的代码。Server Bundle做为打包在服务器端执行的代码,负责的生成对应的HTML,而Clinet Bundle做为执行在浏览器端的代码,主要负责的就是激活应用。

下面咱们给出对应的webpack配置,为了方便上手咱们就仅仅只列出最简单的配置,让咱们能将代码跑起来,配置包括三个部分: baseclientserver,其中base是两者间能通用的部分,client则是对应浏览器的打包配置,server是服务器端的打包配置,经过webpack-merge(能够简单理解成 Object.assign)将其链接:

// webpack.base.config.js
const path = require('path')
const { VueLoaderPlugin } = require('vue-loader')

module.exports = {
    output: {
        path: path.resolve(__dirname, '../dist'),
        publicPath: '/dist/',
        filename: '[name].[chunkhash].js'
    },
    module: {
        rules: [
            {
                test: /\.vue$/,
                loader: 'vue-loader'
            },
            {
                test: /\.js$/,
                loader: 'babel-loader',
                exclude: /node_modules/
            }
        ]
    },
    plugins: [
        new VueLoaderPlugin()
    ]
}
复制代码

  上面是一个最简单的webpack中通用的配置,规定了三部分:

  • output: 打包文件怎样存储输出结果以及存储到哪里
  • module: 咱们对js文件和vue文件执行相应的loader
  • plugins: VueLoaderPlugin插件是必须的,做用是将你定义过的其它规则复制并应用到 .vue 文件里相应语言的块。好比vue文件中script标签对应的JavaScript代码和stype标签对应的css代码。
// webpack.server.config.js
const merge = require('webpack-merge')
const base = require('./webpack.base.config')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')

module.exports = merge(base, {
    target: 'node',
    entry: './src/entry-server.js',
    output: {
        libraryTarget: 'commonjs2'
    },
    plugins: [
        new VueSSRServerPlugin()
    ]
})
复制代码

  上面的配置用来打包服务器架bundle:

  • target: 用来指示构建目标,node表示webpack会编译为用于类 Node.js环境
  • entry: 服务器打包入口文件
  • libraryTarget: 由于是用于Node环境,所以咱们选择commonjs2
  • VueSSRServerPlugin: 用来打包生成的服务器端的bundle,最终能够将全部文件打包成一个json文件,最终传给服务器renderer使用。
// webpack.client.config.js
const webpack = require('webpack')
const merge = require('webpack-merge')
const base = require('./webpack.base.config')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')

module.exports = merge(base, {
    entry: {
        app: './src/entry-client.js'
    },
    plugins: [
        // extract vendor chunks for better caching
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor',
            minChunks: function (module) {
                // a module is extracted into the vendor chunk if...
                return (
                    // it's inside node_modules
                    /node_modules/.test(module.context)
                )
            }
        }),
        // extract webpack runtime & manifest to avoid vendor chunk hash changing
        // on every build.
        new webpack.optimize.CommonsChunkPlugin({
            name: 'manifest'
        }),
        new VueSSRClientPlugin()
    ]
})
复制代码
  • entry: 浏览器打包入口文件。
  • VueSSRClientPlugin:相似于VueSSRServerPlugin插件,主要的做用就是将前端的代码打包成bundle.json,而后传值给renderer,能够自动推断和注入preload/prefetch指令和script标签到渲染的HTML中。

  关于CommonsChunkPlugin插件,其实对于一个最简单的应用而言是能够没有的,可是由于其有助于性能提高仍是加了进来。在最开始学习Webpack的时候,每次打包的时候都会将全部的代码打包到同一个文件,好比app.[hash].js中,其实在app.[hash].js中包含两部分代码,一部分是每次都在变化的业务逻辑代码,另外一部分是几乎不会变化的类库代码(例如Vue的源码)。如今这种状况其实很不利于浏览器的缓存,由于每次业务代码改变后,app.[hash].js必定会发生改变,所以浏览器不得不从新请求,而app.[hash].js的代码量可能都是数以兆计的。所以咱们能够将业务代码和类库代码相分离,在上面的例子中:

new webpack.optimize.CommonsChunkPlugin({
    name: 'vendor',
    minChunks: function (module) {
    // a module is extracted into the vendor chunk if...
        return (
        // it's inside node_modules
        /node_modules/.test(module.context)
        )
    }
}),
复制代码

  咱们将引用的node_modules中的代码打包成vendor.[hash].js,其中就包含了引用的类库,这是代码中相对不变的部分。可是若是仅仅只有上面的部分的话,你会发现每次逻辑代码改变后,vendor.[hash].jshash值也会发生改变,这是为何呢?由于Webpack每次打包运行的时候,仍然是会产生一些和Webpack当前运行相关的代码,会影响到运行的打包值,所以vendor.[hash].js每次打包仍然是会发生改变,这时候其实浏览器并不能正确的缓存。所以咱们使用:

new webpack.optimize.CommonsChunkPlugin({
    name: 'manifest'
})
复制代码

咱们须要将运行环境提取到一个单独的manifest文件中,这样vendorhash就不会变了,浏览器就能够将vendor正确缓存,mainfesthash虽然每次在变,但很小,比起vendor变化带来的影响能够忽略不计。

咱们以前讲过,Vue的应用其实能够划分红三个部分: 组件、路由、状态管理,做为SSR系列的第一篇上手文章,咱们仅介绍如何在服务端渲染一个简单组件并在客户端激活该组件,使得其可交互。路由和状态管理等其余部分会在后序部分介绍。

组件

  首先咱们用Vue写一个最简单的可计数的组件,点击"+"能够增长计数,点击"-"能够减小计数。

// App.vue
<template>
    <div id="app">
        <span>times: {{times}}</span>
        <button @click="add">+</button>
        <button @click="sub">-</button>
    </div>
</template>

<script>
    export default {
        name: "app",
        data: function () {
            return {
                times: 0
            }
        },
        methods: {
            add: function () {
                this.times = this.times + 1;
            },
            sub: function () {
                this.times = this.times - 1;
            }
        }
    }
</script>
<style scoped>
</style>
复制代码

  上面的部分是一个很是简单的Vue组件,也是服务端和客户端渲染的通用代码。在单纯的客户端渲染的程序中,会存在一个app.js用来建立一个Vue实例并将其挂载到对应的dom上,例如:

// 客户端渲染 app.js
import App from './App.vue'

new Vue({
  el: '#app',
  components: { App },
  template: '<App/>',
})
复制代码

  在服务器渲染中,app.js仅会对外暴露一个工厂函数,用来每次都调用的都会返回一个新的组件实例用于渲染。具体的其余逻辑都被各自转移到客户端和浏览器端的入口文件中。

import Vue from 'vue'
import App from './components/App.vue'

export function createApp() {
    return new Vue({
        render: h => h(App)
    })
}
复制代码

  不一样于客户端渲染,值得注意的是咱们须要为每一次请求都建立一个新的Vue实例,而不能共享同一个实例,由于若是咱们在多个请求之间使用一个共享的实例,可能会在各自的请求中形成状态的污染,因此咱们为每一次请求都建立独立的的组件实例。

  接下来看浏览器端打包入口文件:

// entry-server.js
import { createApp } from './app'

export default context => {
    const app = createApp()
    return app
}
复制代码

  entry-server.js对外提供一个函数,用于建立当前的组件实例。接着看客户端打包入口文件:

// client-server.js
import { createApp } from './app'

var app = createApp();

app.$mount('#app')
复制代码

  逻辑也是很是的简单,咱们建立一个Vue实例,用将其挂载到idapp的DOM结构中。

  这时候咱们运行命令分别打包客户端和服务器端的代码,咱们发现dist,目录下分别出现如下文件:

  咱们能够看到app.[hash].js是打包的业务代码,vendor.[hash].js则是相应的库的代码(好比Vue源码),manifest.[hash].js则是CommonsChunkPlugin生成manifest文件。而vue-ssr-client-manifest.json则是VueSSRClientPlugin生成的对应客户端的bundle,而vue-ssr-server-bundle.json则是VueSSRServerPlugin插件生成的服务器端的bundle。有了上面的打包文件,咱们就能够处理请求:

//server.js
const fs = require("fs")
const express = require("express")
const { createBundleRenderer } = require('vue-server-renderer')

const template = fs.readFileSync("./src/index.template.html", "utf-8")
const bundle = require('./dist/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')


const app = express();
app.use("/dist", express.static("dist"))

const renderer = createBundleRenderer(bundle, {
    template,
    clientManifest
})


app.get('*', (req, res) => {
    renderer.renderToString({}, function (err, html) {
        res.end(html);
    });
})

app.listen(8080, function () {
    console.log("server start and listen port 8080")
})
复制代码

  此次咱们并无使用一开始介绍的vue-server-renderer中的createRenderer函数,而是使用的createBundleRenderer函数,咱们在server.js中分别引入了server-bundle.jsonclient-manifest.json与模板template.html,而后将其传给createBundleRenderer函数生成renderer,而后在每一次请求中,调用的rendererrenderToString方法,生成对应的html,而后返回客户端。renderToString的第一个参数实质是上下文context对象,一方面context用于处理模板文件,好比模板文件中存在

<title>{{title}}</title>
复制代码

  而context中存在title: 'SSR',模板中的文件则会被插值。另外一部分,客户端的入口文件server-entry.js的中函数也会收到该context,可用于传递相关的参数。

  咱们之因此会使用express.staticdist文件夹下面的文件提供静态的资源服务的缘由是客户端的代码中会注入相应的JavaScript文件(好比app.[hash].js),这样才能保证对应的资源能够被请求到。

  而后咱们运行命令:

node server.js
复制代码

  并在浏览器中访问http://localhost:8080。你就会发现一个简单的计数器的程序已经运行,而且是可运行,点击按钮会触发相应的事件。

  这是的对应接受的html结构为:

  咱们发现返回的html代码中就有咱们Vue实例对应的DOM结构,与普通的客户端结构不一样的,根元素中存在data-server-rendered属性,表示该结构是由服务端对应渲染的节点,在开发模式中,Vue将渲染的虚拟DOM与当前的DOM结构相比较,若是相等的时候,则会复用当前结构,不然会放弃已经渲染好的结构,转而从新在客户端渲染。在生产模式下,则会略过检测的步骤,直接复用,避免浪费性能。

  在服务器渲染中,一个组件仅仅会经历beforeCreatecreated两个生命周期,而其他的例如beforeMount等生命周期并不会在服务器端执行,所以应该注意的是避免在beforeCreatecreated 生命周期时产生全局反作用的代码,例如在beforeCreatecreated中使用setInterval设置timer,而在beforeDestroydestroyed生命周期时将其销毁,这会形成timer永远不会被取消。

  至此咱们介绍了一个最简单的Vue服务器渲染示例并在客户端对应将其激活,服务器渲染的其余部分好比路由、状态管理等部分咱们将在接下来的文章一一介绍,有兴趣的同窗记得在个人Github博客中点个Star,若是文章中有不正确的地方,欢迎指出,愿一同进步。

相关文章
相关标签/搜索