前端大讲堂

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

2019-07-10  本文已影响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 构建。

上一篇 下一篇

猜你喜欢

热点阅读