likes
comments
collection
share

Vue-SSR系列(一)vue2.0+node+express+webpack实现vue-ssr服务端渲染的单页应用小demo

作者站长头像
站长
· 阅读数 14

本文写于 2018.09.03 22:41,部分安装包可能落后、流程依然可以借鉴

一、什么是服务器端渲染(SSR)?

大致就是在服务端拼接好用户请求的静态页面,直接返回给客户端,客户端激活这些静态页面,让他们变成动态的,并且能够响应后续的数据变化。

二、为什么使用服务器端渲染(SSR)?

1、更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。

2、产生更好的用户体验,更快的内容到达时间(time-to-content),特别是对于缓慢的网络情况或运行缓慢的设备。无需等待所有的 JavaScript 都完成下载并执行,才显示服务器渲染的标记,所以你的用户将会更快速地看到完整渲染的页面。

三、基本用法

首先vue-ssr 需要一个及其重要的插件,所以我们需要安装一下 npm install vue vue-server-renderer --save

3.1 渲染一个简单的实例(官网)

// 第 1 步:创建一个 Vue 实例
const Vue = require('vue')
const app = new Vue({
  template: `<div>Hello World</div>`
})

// 第 2 步:创建一个 renderer
const renderer = require('vue-server-renderer').createRenderer()

// 第 3 步:将 Vue 实例渲染为 HTML
renderer.renderToString(app, (err, html) => {
  if (err) throw err
  console.log(html)
  // => <div data-server-rendered="true">Hello World</div>
})

// 在 2.5.0+,如果没有传入回调函数,则会返回 Promise:
renderer.renderToString(app).then(html => {
  console.log(html)
}).catch(err => {
  console.error(err)
})

3.2 与服务器集成的案例(官网) 先安装npm install express --save

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

server.get('*', (req, res) => {
  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
    }
    res.end(`
      <!DOCTYPE html>
      <html lang="en">
        <head><title>Hello</title></head>
        <body>${html}</body>
      </html>
    `)
  })
})

server.listen(8080)

3.3 使用一个页面模板

// index.html
<!DOCTYPE html>
<html lang="en">
  <head><title>Hello</title></head>
  <body>
    <!--vue-ssr-outlet-->
  </body>
</html>

注意 注释 -- 这里将是应用程序 HTML 标记注入的地方。

const renderer = createRenderer({
  template: require('fs').readFileSync('./index.template.html', 'utf-8')
})

renderer.renderToString(app, (err, html) => {
  console.log(html) // html 将是注入应用程序内容的完整页面
})

当然,以上内容只是基础中的基础,最终我们使用,都是需要集成vue-cli,并结合node,express,webpack等的配置,来生成一个更加灵活的vue-ssr应用,花了将近4天时间,一遍看api,一遍查资料,终于搭建出了一个静态的单页应用vue-ssr。赶紧趁热乎分享一下,毕竟年纪大了,记性不好。


四、vue2.0+node+express+webpack实现vue-ssr单页应用

首先,我们需要安装vue-cli,具体安装,可以参考笔者的vue系列 第一篇文章,如果之前已经全局安装过,那么只需要init一下即可。

Vue-SSR系列(一)vue2.0+node+express+webpack实现vue-ssr服务端渲染的单页应用小demo

刚开始看不懂官网上的图,后来搭建出来以后,发现官方的就是官方的,就跟北京地铁图一样清晰,借鉴一下别人的话就是,ssr 有两个入口文件,client.js 和 server.js, 都包含了应用代码,webpack 通过两个入口文件分别打包成给服务端用的 server bundle 和给客户端用的 client bundle. 当服务器接收到了来自客户端的请求之后,会创建一个渲染器 bundleRenderer,这个 bundleRenderer 会读取上面生成的 server bundle 文件,并且执行它的代码, 然后发送一个生成好的 html 到浏览器,等到客户端加载了 client bundle 之后,会和服务端生成的DOM 进行 Hydration(判断这个DOM 和自己即将生成的DOM 是否相同,如果相同就将客户端的vue实例挂载到这个DOM上, 否则会提示警告)。

根据以上内容,我们先来搭建一个简单的暂时没有数据请求的vue-ssr。

如何搭建?

1、创建一个vue实例 2、配置路由,以及相应的视图组件 3、创建客户端入口文件 4、创建服务端入口文件 5、配置 webpack,分服务端打包配置和客户端打包配置 6、创建服务器端的渲染器,将vue实例渲染成html

**创建vue实例:**为每个请求创建一个新的根 Vue 实例。这与每个用户在自己的浏览器中使用新应用程序的实例类似。如果我们在多个请求之间使用一个共享的实例,很容易导致交叉请求状态污染,因此,我们应该暴露一个可以重复执行的工厂函数,为每个请求创建新的应用程序实例

//  app.js
import Vue from 'vue'
import App from './App.vue'
import createRouter  from './router'

Vue.config.productionTip = false

export function createApp () {
  const router = createRouter()
  const app = new Vue({
    // el: '#app',
    router,
    render: h => h(App)
  })
  return { app, router }
}

配置路由,以及相应的视图组件: 注意,类似于 createApp,我们也需要给每个请求一个新的 router 实例,所以文件导出一个 createRouter 函数.

// router/index.js

import Vue from 'vue'
import Router from 'vue-router'
import home from '@/components/home'
import about from '@/components/about'

Vue.use(Router)

export default function createRouter() {
  return new Router({
    mode:'history',
    routes: [
      {
        path: '/',
        name: 'home',
        component: home
      },
      {
        path: '/about',
        name: 'about',
        component: about
      }
    ]
  })
}

创建客户端入口文件:entry-client.js 客户端 entry 只需创建应用程序,并且将其挂载到 DOM 中

import { createApp } from './app'

// 客户端特定引导逻辑……

const { app, router } = createApp()

// 这里假定 App.vue 模板中根元素具有 `id="app"`
router.onReady(() => {
  app.$mount('#app')
})

创建服务端入口文件:entry-server.js 中实现服务器端路由逻辑

import { createApp } from './app'

export default (context) => {
  // 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,
  // 以便服务器能够等待所有的内容在渲染前,
  // 就已经准备就绪。
  return new Promise((resolve, reject) => {
    const { app, router } = createApp(context)

    const { url } = context
    const { fullPath } = router.resolve(url).route
    if (fullPath !== url) {
      return reject({ url: fullPath })
    }
    // 设置服务器端 router 的位置
    router.push(context.url)
    // 等到 router 将可能的异步组件和钩子函数解析完
    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents()
      if (!matchedComponents.length) {
        return reject({ code: 404 })
      }
      // Promise 应该 resolve 应用程序实例,以便它可以渲染
      resolve(app)

    }, reject)
  })
}

创建好以后,我们的项目还不能启动,我们需要配置webpack,来生成服务端用的 server bundle 和给客户端用的 client bundle。

客户端的client bundle比较好创建,我们在vue-cli下的build文件夹下的webpack.dev.conf.js中,引入const vueSSRClient = require('vue-server-renderer/client-plugin'),然后,在下方配置plugins的地方new vueSSRClient() 创建一个 vueSSRClient()就好了,此时我们客户端的bundle就已经悄然生成了。

服务端的server bundle,就需要我们在build文件夹下新建一个webpack.server.conf.js,配置如下

const webpack = require('webpack')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
const nodeExternals = require('webpack-node-externals')

module.exports = merge(baseWebpackConfig,{
    entry: './src/entry-server.js',
    devtool:'source-map',
    target:'node',
    output:{
        filename:'server-bundle.js',
        libraryTarget:'commonjs2'
    },
    externals: [nodeExternals({
        // do not externalize CSS files in case we need to import it from a dep
        whitelist: /\.css$/
    })],
    plugins:[
        new webpack.DefinePlugin({
            'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
            'process.env.VUE_ENV': '"server"'
          }),

        new VueSSRServerPlugin()
    ]
})

现在我们已经能通过webpack,生成了服务端用的 server bundle 和给客户端用的 client bundle,但是我们需要创建一个渲染器 bundleRenderer,这个 bundleRenderer 会读取上面生成的 server bundle 文件,并且执行它的代码, 然后发送一个生成好的 html 到浏览器,所以我们在build文件夹下 ,新建一个dev-server.js 文件,内容如下(注意看注释)

// dev-server.js

const webpack = require('webpack')
const baseWebpackConfig = require('./webpack.server.conf')
const clientConfig = require('./webpack.base.conf')
const fs = require('fs')
const path = require('path')
// 读取内存的文件
const Mfs = require('memory-fs')
const axios = require('axios')

module.exports = (cb) => {
    // 用来读取内存文件
    var mfs = new Mfs()
    const webpackComplier = webpack(baseWebpackConfig)
    webpackComplier.outputFileSystem = mfs
    const readFile = (fs, file) => {
        try {
          return fs.readFileSync(path.join(clientConfig.output.path, file), 'utf-8')
        } catch (e) {}
    }


    webpackComplier.watch({},async (err,stats) => {
        if(err) {
            return console.log(err)
        }
        stats = stats.toJson();
        stats.errors.forEach(err => {console.log(err)});
        stats.warnings.forEach(err => {console.log(err)});
        // 获取vue-server-renderer/server-plugin生成的服务端bundle的json文件 默认名字为vue-ssr-server-bundle.json
        let serverBundle = JSON.parse(readFile(mfs, 'vue-ssr-server-bundle.json'))
        // 获取vue-server-renderer/client-plugin生成的客户端bundle的json文件,默认名字vue-ssr-client-manifest.json
        let clientBundle =  await axios.get('http://localhost:8080/vue-ssr-client-manifest.json')
        // 获取模板文件 注意模板文件里面加上<!--vue-ssr-outlet-->
        let template = fs.readFileSync(path.join(__dirname,'..','index.html'), 'utf-8')
        cb(serverBundle, clientBundle, template)
    })
}

现在就差临门一脚了,就是调用我们的dev-server,用bundleRenderer生成我们的html,并返回到页面上、那我们在根目录下新建一个server.js ,整合所有资源,使其成为我们客户端的入口

// server.js
const devServer = require('./build/dev-server.js')
const server = require('express')()
const { createBundleRenderer } = require('vue-server-renderer')
// 在服务器处理函数中……
server.get('*', (req, res) => {
  const context = { url: req.url }
    res.status(200)
    devServer((serverBundle, clientBundle, template) => {
        let renderer = createBundleRenderer(serverBundle, {
            runInNewContext: false, // 推荐
            template, // (可选)页面模板
            clientManifest:clientBundle.data // (可选)客户端构建 manifest
        })

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

    })

})

const port = process.env.PORT || 5001;
server.listen(port, () => {
    console.log(`server started at localhost:${port}`)
    console.log('启动成功')
})

最终我们的项目目录如下: Vue-SSR系列(一)vue2.0+node+express+webpack实现vue-ssr服务端渲染的单页应用小demo

为了能在服务端启动我们的项目,在package.json的scripts中,加一行代码"server": "node server.js" 现在,我们运行npm run dev启动的就是客户端渲染,运行npm run server启动的就是服务端渲染,前端页面呈现是一样的,但是翻看页面源码的时候就会发现区别 npm run dev 默认启动8080端口

Vue-SSR系列(一)vue2.0+node+express+webpack实现vue-ssr服务端渲染的单页应用小demo

查看客户端源码

Vue-SSR系列(一)vue2.0+node+express+webpack实现vue-ssr服务端渲染的单页应用小demo

npm run server 默认启动5001端口 Vue-SSR系列(一)vue2.0+node+express+webpack实现vue-ssr服务端渲染的单页应用小demo

查看服务端源码,会有一个明显的 data-server-rendered="true"

Vue-SSR系列(一)vue2.0+node+express+webpack实现vue-ssr服务端渲染的单页应用小demo

现在,我们的简单的案例就完成了,那么最终我们还要集成vuex来动态显示数据,就放下集吧。