天天看点

vue ssr搭建服务端渲染项目

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

Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。

服务器渲染的 Vue.js 应用程序也可以被认为是"同构"或"通用",因为应用程序的大部分代码都可以在服务器和客户端上运行。

为什么使用服务器端渲染

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

2.更快的内容到达时间 (time-to-content),特别是对于缓慢的网络情况或运行缓慢的设备

3.在对你的应用程序使用服务器端渲染 (SSR) 之前,你应该问的第一个问题是,是否真的需要它。这主要取决于内容到达时间 (time-to-content) 对应用程序的重要程度。例如,如果你正在构建一个内部仪表盘,初始加载时的额外几百毫秒并不重要,这种情况下去使用服务器端渲染 (SSR) 将是一个小题大作之举。然而,内容到达时间 (time-to-content) 要求是绝对关键的指标,在这种情况下,服务器端渲染 (SSR) 可以帮助你实现最佳的初始加载性能。

准备工作

1.搭建一个vue项目

2.安装的插件

cnpm install vue vue-server-renderer --save
cnpm install express --save
cnpm install cross-env -D
           

参考来源 https://ssr.vuejs.org/zh/guide/

参考文章链接https://blog.csdn.net/weixin_30732487/article/details/98167965

首先是main.js

我们为每个请求创建一个新的根 Vue 实例。这与每个用户在自己的浏览器中使用新应用程序的实例类似。如果我们在多个请求之间使用一个共享的实例,很容易导致交叉请求状态污染 (cross-request state pollution)

因此,我们不应该直接创建一个应用程序实例,而是应该暴露一个可以重复执行的工厂函数,为每个请求创建新的应用程序实例:

// main.js
import Vue from 'vue'
import App from './App.vue'
import {createRouter} from './router'
import {createStore} from './store'
import { sync } from 'vuex-router-sync'

Vue.config.productionTip = false

export default function createApp () {
  // 创建 router 和 store 实例
  const router = createRouter()
  const store = createStore()

  // 同步路由状态(route state)到 store
  sync(store, router)
  
  const app = new Vue({
    router,
    store,
    render: h => h(App)
  })
  return {app, router, store}
}

           

entry-client.js客户端配置

客户端 entry 只需创建应用程序,并且将其挂载到 DOM 中:

// entry-client.js
import createApp from './main'

const {app, router, store} = createApp()

if (window.__INITIAL_STATE__) {
  store.replaceState(window.__INITIAL_STATE__)
}

router.onReady(() => {
  // 添加路由钩子函数,用于处理 asyncData.
  // 在初始路由 resolve 后执行,
  // 以便我们不会二次预取(double-fetch)已有的数据。
  // 使用 `router.beforeResolve()`,以便确保所有异步组件都 resolve。
  router.beforeResolve((to, from, next) => {
    const matched = router.getMatchedComponents(to)
    const prevMatched = router.getMatchedComponents(from)

    // 我们只关心非预渲染的组件
    // 所以我们对比它们,找出两个匹配列表的差异组件
    let diffed = false
    const activated = matched.filter((c, i) => {
      return diffed || (diffed = (prevMatched[i] !== c))
    })

    if (!activated.length) {
      return next()
    }

    // 这里如果有加载指示器 (loading indicator),就触发

    Promise.all(activated.map(c => {
      if (c.asyncData) {
        return c.asyncData({ store, route: to })
      }
    })).then(() => {

      // 停止加载指示器(loading indicator)

      next()
    }).catch(next)
  })
  
  app.$mount('#app')
})

           

entry-server.js服务端配置

在 entry-server.js 中,我们可以通过路由获得与 router.getMatchedComponents() 相匹配的组件,如果组件暴露出 asyncData,我们就调用这个方法。然后我们需要将解析完成的状态,附加到渲染上下文(render context)中。

// entry-server.js
import createApp from './main'

export default context => {
  return new Promise((resolve, reject) => {
    const { app, router, store } = createApp()

    router.push(context.url)

    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents()
      if (!matchedComponents.length) {
        return reject({ code: 404 })
      }

      // 对所有匹配的路由组件调用 `asyncData()`
      Promise.all(matchedComponents.map(Component => {
        if (Component.asyncData) {
          return Component.asyncData({
            store,
            route: router.currentRoute
          })
        }
      })).then(() => {
        // 在所有预取钩子(preFetch hook) resolve 后,
        // 我们的 store 现在已经填充入渲染应用程序所需的状态。
        // 当我们将状态附加到上下文,
        // 并且 `template` 选项用于 renderer 时,
        // 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。
        context.state = store.state

        resolve(app)
      }).catch(err => {
        throw err
      })
    }, reject)
  })
}

           

router.js路由配置

你可能已经注意到,我们的服务器代码使用了一个 * 处理程序,它接受任意 URL。这允许我们将访问的 URL 传递到我们的 Vue 应用程序中,然后对客户端和服务器复用相同的路由配置!

为此,建议使用官方提供的 vue-router。我们首先创建一个文件,在其中创建 router。注意,类似于 createApp,我们也需要给每个请求一个新的 router 实例,所以文件导出一个 createRouter 函数:

// router.js
import Vue from 'vue';
import Router from 'vue-router';

Vue.use(Router);

export function createRouter() {
  return new Router({
    mode: 'history',
    routes: [
      {
        path: '/',
        name: 'home',
        component: () => import('../views/Home.vue')
      },
      {
        path: '/about',
        name: 'about',
        component: () => import('../views/About.vue')
      }
    ],
  });
}

           

store.js

// store.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

// 假定我们有一个可以返回 Promise 的
// 通用 API(请忽略此 API 具体实现细节)
import { fetchItem } from '../api'

export function createStore () {
  return new Vuex.Store({
    state: {
      items: {}
    },
    actions: {
      fetchItem ({ commit }, id) {
        // `store.dispatch()` 会返回 Promise,
        // 以便我们能够知道数据在何时更新
        return fetchItem(id).then(item => {
          console.log(item)
          commit('setItem', { id, item })
        })
      }
    },
    mutations: {
      setItem (state, { id, item }) {
        Vue.set(state.items, id, item)
      }
    }
  })
}

           

vue.config

// vue.config.js
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
const nodeExternals = require('webpack-node-externals')
const env = process.env
const isServer = env.RUN_ENV === 'server'

console.log(isServer, env.RUN_ENV)

module.exports = {
  publicPath: './',
  outputDir: `dist/${env.RUN_ENV}`,
  configureWebpack: {
    // 将 entry 指向应用程序的 server / client 文件
    entry: `./src/entry-${env.RUN_ENV}.js`,
    devtool: 'eval',
    // 这允许 webpack 以 Node 适用方式(Node-appropriate fashion)处理动态导入(dynamic import),
    // 并且还会在编译 Vue 组件时,
    // 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。
    target: isServer ? 'node' : 'web',
    // 此处告知 server bundle 使用 Node 风格导出模块(Node-style exports)
    output: {
      libraryTarget: isServer ? 'commonjs2' : undefined
    },
    optimization: {
      splitChunks: isServer ? false : undefined
    },
    // https://webpack.js.org/configuration/externals/#function
    // https://github.com/liady/webpack-node-externals
    // 外置化应用程序依赖模块。可以使服务器构建速度更快,
    // 并生成较小的 bundle 文件。
    externals: isServer ? nodeExternals({
      // 不要外置化 webpack 需要处理的依赖模块。
      // 你可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
      // 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单
      allowlist: /\.css$/
    }) : undefined,
    // 这是将服务器的整个输出
    // 构建为单个 JSON 文件的插件。
    // 服务端默认文件名为 `vue-ssr-server-bundle.json`
    // 客户端默认文件名为 `vue-ssr-client-manifest.json`
    plugins: [
      isServer ? new VueSSRServerPlugin() : new VueSSRClientPlugin(),
    ]
  }
}

           

最后配置一个本地开发使用的server.js

// server.js
const http = require('http')
const path = require('path')
const fs = require('fs')
const express = require('express')
const {createBundleRenderer} = require('vue-server-renderer')
const serverBundle = require('./dist/server/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/client/vue-ssr-client-manifest.json')

const app = express()
app.set('port', 8001);
// 静态文件目录指向dist文件夹
app.use(express.static(path.join(__dirname, './dist/client')))

const renderer = createBundleRenderer(serverBundle, {
  runInNewContext: false,
  template: fs.readFileSync(path.resolve(__dirname, './index.html'), 'utf-8'),
  clientManifest
})

app.get('*', (req, res) => {
  const context = {
    title: 'Hello SSR',
    url: req.url
  }

  renderer.renderToString(context, (err, html) => {
    if (err) {
      if (err.code === 404) {
        res.status(404).end('404 not found')
      } else {
        res.status(500).end(err.message)
      }
    } else {
      res.end(html)
    }
  })
})

/*服务启动*/
http.createServer(app).listen(app.get('port'), function () {
  console.log('service start at ' + app.get('port'));
});

           

package.json添加下面命令

"start": "npm run build:server && npm run build:client && npm run service",
"build:client": "cross-env RUN_ENV=client vue-cli-service build",
"build:server": "cross-env RUN_ENV=server vue-cli-service build",
"service": "node server.js"
           

一个坑打包server时报错Server-side bundle should have one single entry file. Avoid using CommonsChu

在config.js里面添加下面几行代码

optimization: {
  splitChunks: isServer ? false : undefined
}
           

服务器端包应该有一个条目文件。避免在服务器配置中使用CommonsChunkPlugin。

猜测可能是因为vue-cli3配置了webpack4 里面的分包功能,所以,再弄得环境下给他禁止掉就好了

自己记录也可以给有兴趣的同学参考,不喜勿喷

继续阅读