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

一、什么是服务器端渲染(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一下即可。

屏幕快照 2018-08-28 下午9.55.30.png

刚开始看不懂官网上的图,后来搭建出来以后,发现官方的就是官方的,就跟北京地铁图一样清晰,借鉴一下别人的话就是,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('启动成功')
})

最终我们的项目目录如下:


项目目录

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

8080.gif

查看客户端源码

屏幕快照 2018-08-28 下午10.39.02.png

npm run server 默认启动5001端口

5001.gif

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

屏幕快照 2018-08-28 下午10.39.16.png

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容