美文网首页前端大讲堂
Vue 项目中服务端渲染的几种方式

Vue 项目中服务端渲染的几种方式

作者: 前端大课堂 | 来源:发表于2019-07-10 11:04 被阅读0次

    服务端渲染在很早的时候就有了,可以追溯到 ASP、JSP 的时代,就是在后端返回一个静态页面给浏览器,由浏览器直接显示。 

    但是在 React 以及 nodejs 普及之后,开始出现同构渲染,简单来说就是在服务端渲染前端组件然后返回给浏览器显示。 

    00 背景

    同构渲染简称 SSR(Server-Side Render),也叫页面直出。具体的优势可以看这篇文章《手把手教你 ReactJS 和 VueJS 的服务端渲染》

    SSR 是由 React 的虚拟 dom 可以直接在 nodejs 中渲染出 dom string, 就是类似于

    <div> xxxx </div> 

    Vue.js 的服务端渲染的方式和 React 还有点不一样。 

    下面介绍下 Vue.js 整个直出的过程。 

    01 开始 

    Vue SSR 官方文档《Vue.js 服务器端渲染指南》. 直接上例子

    const Vue = require('vue')

    const server = require('express')()

    const renderer = require('vue-server-renderer').createRenderer({

      template: require('fs').readFileSync('./index.template.html', 'utf-8')

    })

    Vue.component('button-counter', {

      data: function () {

        return {

          count: 0

        }

      },

      template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'

    })

    server.get('*', (req, res) => {

      const app = new Vue({

        data: {

          url: req.url

        },

        template: `

          <div id="components-demo">

            <button-counter></button-counter>

          </div>

        `

      })

      const context = {

        title: 'hello',

        meta: `

          <meta ...>

          <meta ...>

        `

      }

      renderer.renderToString(app, context, (err, html) => {

        if (err) {

          res.status(500).end('Internal Server Error')

          return

        }

        console.log(html)

        res.end(html)

      })

    })

    server.listen(8080)

    同构渲染的关键就是 renderToString, 无论是 vue 还是 react 都是通过这个方法输出 dom string. 

    上面 console.log(html) 输出结果

    <html>

    <head>

      <!-- 使用双花括号(double-mustache)进行 HTML 转义插值(HTML-escaped interpolation) -->

      <title>hello</title>

      <!-- 使用三花括号(triple-mustache)进行 HTML 不转义插值(non-HTML-escaped interpolation) -->

          <meta ...>

          <meta ...>

    </head>

    <body>

      <div id="components-demo" data-server-rendered="true"><button>You clicked me 0 times.</button></div>

    </body>

    </html>

    <html>

    <head>

      <!-- 使用双花括号(double-mustache)进行 HTML 转义插值(HTML-escaped interpolation) -->

      <title>hello</title>

      <!-- 使用三花括号(triple-mustache)进行 HTML 不转义插值(non-HTML-escaped interpolation) -->

          <meta ...>

          <meta ...>

    </head>

    <body>

      <div id="components-demo" data-server-rendered="true"><button>You clicked me 0 times.</button></div>

    </body>

    </html>

    但是真正用 vue 构建的复杂的应用应该是由很多 *.vue 文件组成的,但是 commonjs 规范根本识别不了 *.vue 文件,所以需要对 vue 文件做服务端构建。 

    02 复杂应用下服务端构建 

    由于前端也需要构建,所以抽出一个公用的 webpack.base.config.js, 

    const path = require('path')

    const utils = require('./utils')

    const vueLoaderConfig = require('./vue-loader.conf')

    const webpack = require("webpack")

    function resolve(dir) {

      return path.join(__dirname, '..', dir)

    }

    module.exports = {

      context: path.resolve(__dirname, '../'),

      output: {

        path: path.resolve(__dirname, '../dist'),

        filename: '[name].[chunkhash:8].js',

        publicPath: './'

      },

      resolve: {

        extensions: ['.js', '.vue', '.json'],

        alias: {

          'vue$': 'vue/dist/vue.esm.js',

          '@': resolve('src'),

        }

      },

      module: {

        rules: [{

          test: /\.vue$/,

          loader: 'vue-loader',

          options: vueLoaderConfig

        },

        {

          test: /\.js$/,

          loader: 'babel-loader',

          include: [resolve('src'), resolve('test'), resolve('/node_modules/element-ui/src'), resolve('/node_modules/element-ui/packages'), resolve('node_modules/webpack-dev-server/client')]

        },

        {

          test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,

          loader: 'url-loader',

          options: {

            limit: 1000,

            name: utils.assetsPath('img/[name].[hash:7].[ext]')

          }

        },

        {

          test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,

          loader: 'url-loader',

          options: {

            limit: 1000,

            name: utils.assetsPath('media/[name].[hash:7].[ext]')

          }

        },

        {

          test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,

          loader: 'url-loader',

          options: {

            limit: 1000,

            name: utils.assetsPath('fonts/[name].[hash:7].[ext]')

          }

        },

        {

          test: /\.less$/,

          loader: "style-loader!css-loader!postcss-loader!less-loader",

        },

        ]

      },

      node: {

        // prevent webpack from injecting useless setImmediate polyfill because Vue

        // source contains it (although only uses it if it's native).

        setImmediate: false,

        // prevent webpack from injecting mocks to Node native modules

        // that does not make sense for the client

        dgram: 'empty',

        fs: 'empty',

        net: 'empty',

        tls: 'empty',

        child_process: 'empty'

      },

      plugins: [

        new webpack.ProvidePlugin({

          $: "jquery",

          jQuery: "jquery",

          $moment: "moment",

          $numeral: "numeral",

          echarts: "echarts"

        })

      ]

    }

    前端的构建配置 webpack.client.config.js 和 node 的 webpack.server.config.js 

    // webpack.client.config.js

    const path = require('path')

    const webpack = require('webpack')

    const merge = require('webpack-merge')

    const baseConfig = require('./webpack.base.config.js')

    const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')

    module.exports = merge(baseConfig, {

      entry: {

        client: path.resolve(__dirname, '../src/entry-client.js'),

      },

      plugins: [

        // 重要信息:这将 webpack 运行时分离到一个引导 chunk 中,

        // 以便可以在之后正确注入异步 chunk。

        // 这也为你的 应用程序/vendor 代码提供了更好的缓存。

        new webpack.optimize.CommonsChunkPlugin({

          name: "manifest",

          minChunks: Infinity

        }),

        // 此插件在输出目录中

        // 生成 `vue-ssr-client-manifest.json`。

        new VueSSRClientPlugin()

      ]

    })

    // webpack.server.config.js

    const path = require('path')

    const merge = require('webpack-merge')

    const nodeExternals = require('webpack-node-externals')

    const baseConfig = require('./webpack.base.config.js')

    const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')

    module.exports = merge(baseConfig, {

      // 将 entry 指向应用程序的 server entry 文件

      entry: {

        client: path.resolve(__dirname, '../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',

        path: path.resolve(__dirname, '../dist'),

        filename: '[name].[chunkhash:8].js',

        publicPath: './'

      },

      // https://webpack.js.org/configuration/externals/#function

      // https://github.com/liady/webpack-node-externals

      // 外置化应用程序依赖模块。可以使服务器构建速度更快,

      // 并生成较小的 bundle 文件。

      externals: nodeExternals({

        // 不要外置化 webpack 需要处理的依赖模块。

        // 你可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,

        // 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单

        whitelist: /\.css$/

      }),

      // 这是将服务器的整个输出

      // 构建为单个 JSON 文件的插件。

      // 默认文件名为 `vue-ssr-server-bundle.json`

      plugins: [

        new VueSSRServerPlugin()

      ]

    })

    服务端的构建会再 dist 目录下生成 vue-ssr-server-bundle.json 的文件,

    const path = require('path')

    const Koa = require('koa')

    const app = new Koa()

    const koaStatic = require('koa-static')

    const cors = require('@koa/cors')

    const router = require('koa-router')()

    const {

      createBundleRenderer

    } = require('vue-server-renderer')

    app.use(cors())

    let jsonPath = path.resolve(__dirname, './dist/vue-ssr-server-bundle.json')

    const renderer = createBundleRenderer(jsonPath, {

      template: require('fs').readFileSync('./index.template.html', 'utf-8')

    })

    app.use(koaStatic('dist/', {

      maxage: 1000 * 3600 * 24 * 30, // a month

    }))

    // app.use(koaStatic('examples/', {

    //  maxage: 1000 * 3600 * 24 * 30, // a month

    // }))

    router.get("*", async ctx => {

      const context = {

        title: 'hello',

        meta: `

          <meta ...>

          <meta ...>

        `

      }

      renderer.renderToString(context, (err, html) => {

        if (err) {

          console.log(err.stack)

          ctx.status = 500

          ctx.body = "Internal Server Error"

          return

        }

        console.log(html)

        ctx.body = html

      })

    })

    app

      .use(router.routes())

      .use(router.allowedMethods({

        throw: true

      }))

    app.listen(7000)

    console.log('localhost:7000')

    总结: 

        这就是 vue.js 同构渲染的两种方式。一种是直接通过 Vue.components 注册全局组件,这种在后端也是可以直接通过 renderToString 渲染。 

    另一种的方式是写 *.vue 组件,但是要通过服务端的 webpack 构建。

    相关文章

      网友评论

        本文标题:Vue 项目中服务端渲染的几种方式

        本文链接:https://www.haomeiwen.com/subject/saetkctx.html