威尼斯wns.9778官网活动_vnsc威尼斯城官网

热门关键词: 威尼斯wns.9778官网活动,vnsc威尼斯城官网
当前位置:威尼斯wns.9778官网活动 > 计算机教程 > 基于vue-ssr服务端渲染入门详解威尼斯wns.9778官网

基于vue-ssr服务端渲染入门详解威尼斯wns.9778官网

文章作者:计算机教程 上传时间:2019-05-10

前面的话

  不论是官网教程,还是官方DEMO,都是从0开始的服务端渲染配置。对于现有项目的服务器端渲染SSR改造,特别是基于vue cli生成的项目,没有特别提及。本文就小火柴的前端小站这个前台项目进行SSR改造

 

第一部分 基本介绍

效果

  下面是经过SSR改造后的前端小站xiaohuochai.cc的网站效果,github源码地址

 

1、前言

服务端渲染实现原理机制:在服务端拿数据进行解析渲染,直接生成html片段返回给前端。然后前端可以通过解析后端返回的html片段到前端页面,大致有以下两种形式:

1、服务器通过模版引擎直接渲染整个页面,例如java后端的vm模版引擎,php后端的smarty模版引擎。
2、服务渲染生成html代码块, 前端通过AJAX获取然后使用js动态添加。

概述

【定义】

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

【优点】

  与传统SPA相比,服务器端渲染(SSR)的优势主要在于:

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

  截至目前,Google 和 Bing 可以很好对同步 JavaScript 应用程序进行索引。但如果应用程序初始展示 loading 菊花图,然后通过 Ajax 获取内容,抓取工具并不会等待异步完成后再行抓取页面内容

  2、更快的内容到达时间,特别是对于缓慢的网络情况或运行缓慢的设备

  无需等待所有的 JavaScript 都完成下载并执行,才显示服务器渲染的标记,所以用户将会更快速地看到完整渲染的页面,通常可以产生更好的用户体验

 

2、服务端渲染的优劣

服务端渲染能够解决两大问题:

1、seo问题,有利于搜索引擎蜘蛛抓取网站内容,利于网站的收录和排名。
2、首屏加载过慢问题,例如现在成熟的SPA项目中,打开首页需要加载很多资源,通过服务端渲染可以加速首屏渲染。
同样服务端渲染也会有弊端,主要是根据自己的业务场景来选择适合方式,由于服务端渲染前端页面,必将会给服务器增加压力。

思路

  下面以官方的SSR服务器端渲染流程图为例,进行概要说明

威尼斯wns.9778官网活动 1

  1、universal Application Code是服务器端和浏览器端通用的代码

  2、app.js是应用程序的入口entry,对应vue cli生成的项目的main.js文件

  3、entry-client.js是客户端入口,仅运行于浏览器,entry-server.js是服务器端入口,仅运行于服务器

  4、entry-client和entry-server这两个文件都需要通过webpack构建,其中entry-client需要通过webpack.server.config.js文件打包,entry-server需要通过webpack.server.config.js文件打包

  5、entry-client构建后的client Bundle打包文件是vue-ssr-client-manifest.json,entry-server构建后的server Bundle打包文件是vue-ssr-server-bundle.json

  6、server.js文件将客户端打包文件vue-ssr-client-manifest.json、服务器端打包文件vue-ssr-server-bundle.json和HTML模板混合,渲染成HTML

 

3、SSR的实现原理

客户端请求服务器,服务器根据请求地址获得匹配的组件,在调用匹配到的组件返回 Promise (官方是preFetch方法)来将需要的数据拿到。最后再通过

<script>window.__initial_state=data</script>

将其写入网页,最后将服务端渲染好的网页返回回去。

接下来客户端会将vuex将写入的 initial_state 替换为当前的全局状态树,再用这个状态树去检查服务端渲染好的数据有没有问题。遇到没被服务端渲染的组件,再去发异步请求拿数据。说白了就是一个类似React的 shouldComponentUpdate 的Diff操作。

Vue2使用的是单向数据流,用了它,就可以通过 SSR 返回唯一一个全局状态, 并确认某个组件是否已经SSR过了。

webpack配置

  基于vue-cli生成的项目的build目录结构如下

build
    - build.js
    - check-versions.js
    - utils.js
    - vue-loader.conf.js
    - webpack.base.conf.js
    - webpack.dev.conf.js
    - webpack.prod.conf.js

  前面3个文件无需修改,只需修改*.*.conf.js文件

  1、修改vue-loader.conf.js,将extract的值设置为false,因为服务器端渲染会自动将CSS内置。如果使用该extract,则会引入link标签载入CSS,从而导致相同的CSS资源重复加载

-    extract: isProduction
     extract: false

  2、修改webpack.base.conf.js

  只需修改entry入门配置即可

...
module.exports = {
  context: path.resolve(__dirname, '../'),
  entry: {
    - app: './src/main.js'
      app: './src/entry-client.js'
  },
...

  3、修改webpack.prod.conf.js

  包括应用vue-server-renderer、去除HtmlWebpackPlugin、增加client环境变量

'use strict'
...
  const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
const webpackConfig = merge(baseWebpackConfig, {
  ...
  plugins: [
    // http://vuejs.github.io/vue-loader/en/workflow/production.html
    new webpack.DefinePlugin({
      'process.env': env,
      'process.env.VUE_ENV': '"client"'
    }),
    ...// generate dist index.html with correct asset hash for caching.
    // you can customize output by editing /index.html
    // see https://github.com/ampedandwired/html-webpack-plugin
-    new HtmlWebpackPlugin({
-      filename: config.build.index,
-      template: 'index.html',
-      inject: true,
-      minify: {
-        removeComments: true,
-        collapseWhitespace: true,
-        removeAttributeQuotes: true
-        // more options:
-        // https://github.com/kangax/html-minifier#options-quick-reference
-      },
-      // necessary to consistently work with multiple chunks via CommonsChunkPlugin
-      chunksSortMode: 'dependency'
-    }),
   ...// copy custom static assets
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, '../static'),
        to: config.build.assetsSubDirectory,
        ignore: ['.*']
      }
    ]),
     new VueSSRClientPlugin()
  ]
})
...
module.exports = webpackConfig

  4、新增webpack.server.conf.js

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

module.exports = merge(baseConfig, {
  entry: './src/entry-server.js',
  target: 'node',
  devtool: 'source-map',
  output: {
    libraryTarget: 'commonjs2'
  },
  externals: nodeExternals({
    whitelist: /.css$/
  }),
  plugins: [
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
      'process.env.VUE_ENV': '"server"'
    }),
    new VueSSRServerPlugin()
  ]
})

 

4、vue后端渲染主要插件:vue-server-renderer

由于virtual dom的引入,使得vue的服务端渲染成为了可能,下面是官方 vue-server-renderer提供的渲染流程图:

威尼斯wns.9778官网活动 2

可以看出vue的后端渲染分三个部分组成:页面的源码(source),node层的渲染部分和浏览器端的渲染部分。

source分为两种entry point,一个是前端页面的入口client entry,主要是实例化Vue对象,将其挂载到页面中;另外一个是后端渲染服务入口server entry,主要是控服务端渲染模块回调,返回一个Promise对象,最终返回一个Vue对象(经过测试,直接返回Vue对象也是可以的);

前面的source部分就是业务开发的代码,开发完成之后通过 webpack 进行构建,生成对应的bundle,这里不再赘述client bundle,就是一个可在浏览器端执行的打包文件;这里说下server bundle, vue2提供 vue-server-renderer模块,模块可以提供两种render: rendererer/bundleRenderer ,下面分别介绍下这两种render。

renderer接收一个vue对象 ,然后进行渲染,这种对于简单的vue对象,可以这么去做,但是对于复杂的项目,如果使用这种直接require一个vue对象,这个对于服务端代码的结构和逻辑都不太友好,首先模块的状态会一直延续在每个请求渲染请求,我们需要去管理和避免这次渲染请求的状态影响到后面的请求,因此vue-server-renderer提供了另外一种渲染模式,通过一个 bundleRenderer去做渲染。

bundleRenderer是较为复杂项目进行服务端渲染官方推荐的方式,通过webpack以server entry按照一定的要求打包生成一个 server-bundle,它相当于一个可以给服务端用的app的打包压缩文件,每一次调用都会重新初始化 vue对象,保证了每次请求都是独立的,对于开发者来说,只需要专注于当前业务就可以,不用为服务端渲染开发更多的逻辑代码。 renderer生成完成之后,都存在两个接口,分别是renderToString和renderToStream,一个是一次性将页面渲染成字符串文件,另外一个是流式渲染,适用于支持流的web服务器,可以是请求服务的速度更快。

入口配置

  在浏览器端渲染中,入口文件是main.js,而到了服务器端渲染,除了基础的main.js,还需要配置entry-client.js和entry-server.js

  1、修改main.js

import Vue from 'vue'
import Vuex from 'vuex'
-  import '@/assets/style.css'
import App from './App'
-  import router from './router'
  import createRouter from './router'
-  import store from './store'
  import createStore from './store'
import async from './utils/async'
Vue.use(async)
- new Vue({
  export default function createApp() {
   const router = createRouter()
   const store = createStore()
   const app = new Vue({
-   el: '#app',
    router,
    store,
-   components: { App },
-   template: '<App/>'
    render: h => h(App)
  })
  return { app, router, store }
 }

  2、新增entry-client.js

  后面会介绍到asyncData方法,但是asyncData方法只能用于路由绑定的组件,如果是初始数据则可以直接在entry-client.js中获取

/* eslint-disable */
import Vue from 'vue'
import createApp from './main'

Vue.mixin({
  beforeRouteUpdate (to, from, next) {
    const { asyncData } = this.$options
    if (asyncData) {
      asyncData({
        store: this.$store,
        route: to
      }).then(next).catch(next)
    } else {
      next()
    }
  }
})

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

/* 获得初始数据 */
import { LOAD_CATEGORIES_ASYNC } from '@/components/Category/module'
import { LOAD_POSTS_ASYNC } from '@/components/Post/module'
import { LOAD_LIKES_ASYNC } from '@/components/Like/module'
import { LOAD_COMMENTS_ASYNC } from '@/components/Comment/module'
import { LOAD_USERS_ASYNC } from '@/components/User/module'
(function getInitialData() {
  const { postCount, categoryCount, userCount, likeCount, commentCount } = store.getters
  const { dispatch } = store
  // 获取类别信息
  !categoryCount && dispatch(LOAD_CATEGORIES_ASYNC),
  // 获取文章信息
  !postCount && dispatch(LOAD_POSTS_ASYNC),
  // 获取点赞信息
  !likeCount && dispatch(LOAD_LIKES_ASYNC),
  // 获取评论信息
  !commentCount && dispatch(LOAD_COMMENTS_ASYNC),
  // 获取用户信息
  !userCount && dispatch(LOAD_USERS_ASYNC)
})()

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

router.onReady(() => {
  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()
    }
    Promise.all(activated.map(c => {
      if (c.asyncData) {
        return c.asyncData({ store, route: to })
      }
    })).then(() => {
      next()
    }).catch(next)
  })
  app.$mount('#root')
})

  3、新增entry-sever.js

/* eslint-disable */
import createApp from './main'

export default context => 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 })
    }
    Promise.all(matchedComponents.map(Component => {
      if (Component.asyncData) {
        return Component.asyncData({
          store,
          route: router.currentRoute
        })
      }
    })).then(() => {
      context.state = store.state
      resolve(app)
    }).catch(reject)
  }, reject)
})

 

第二部分 从零开始搭建

组件修改

  由于代码需要在服务器端和浏览器端共用,所以需要修改组件,使之在服务器端运行时不会报错

  1、修改router路由文件,给每个请求一个新的路由router实例

import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)
  export default function createRouter() {
- export default new Router({
    return new Router({
     mode: 'history',
     routes: [
       {
         path: '/',
         component: () => import(/* webpackChunkName:'home' */ '@/components/Home/Home'),
         name: 'home',
         meta: { index: 0 }
       },
    ...
     ]
  })
 }

  2、修改状态管理vuex文件,给每个请求一个新的vuex实例

import Vue from 'vue'
import Vuex from 'vuex'
import auth from '@/components/User/module'
...

Vue.use(Vuex)
  export default function createStore() {
- export default new Vuex.Store({
    return new Vuex.Store({
    modules: {
      auth,
     ...
    }
  })
 }

  3、使用asyncData方法来获取异步数据

  要特别注意的是,由于asyncData只能通过路由发生作用,使用是非路由组件的异步数据获取最好移动到路由组件中

  如果要通过asyncData获取多个数据,可以使用Promise.all()方法

asyncData({ store }) {
    const { dispatch } = store
    return Promise.all([
      dispatch(LOAD_CATEGORIES_ASYNC),
      dispatch(LOAD_POSTS_ASYNC)
    ])
}

  如果该异步数据是全局通用的,可以在entry-client.js方法中直接获取

  将TheHeader.vue通用头部组件获取异步数据的代码移动到entry-client.js方法中进行获取

// TheHeader.vue
  computed: {
    ...
-    ...mapGetters([
-      'postCount',
-      'categoryCount',
-      'likeCount',
-      'commentCount',
-      'userCount'
-    ])
  },
-  mounted() {
    // 获取异步信息
-    this.loadAsync()
  ...
-  },
...
  methods: {
-    loadAsync() {
-      const { postCount, categoryCount, userCount, likeCount, commentCount } = this
-      const { dispatch } = this.$store
-      // 获取类别信息
-      !categoryCount && dispatch(LOAD_CATEGORIES_ASYNC)
-      // 获取文章信息
-      !postCount && dispatch(LOAD_POSTS_ASYNC)
-      // 获取点赞信息
-      !likeCount && dispatch(LOAD_LIKES_ASYNC)
-      // 获取评论信息
-      !commentCount && dispatch(LOAD_COMMENTS_ASYNC)
-     // 获取用户信息
-      !userCount && dispatch(LOAD_USERS_ASYNC)
-    },

  将Post.vue中的异步数据通过asyncData进行获取

// post.vue
...
export default {
   asyncData({ store, route }) {
     return store.dispatch(LOAD_POST_ASYNC, { id: route.params.postid })
   },
...
-  mounted() {
-    this.$store.dispatch(LOAD_POST_ASYNC, { id: this.postId })
-  },
...

  4、将全局css从main.js移动到App.vue中的内联style样式中,因为main.js中未设置css文件解析

// main.js
- import '@/assets/style.css'
// App.vue
...
<style module lang="postcss">
...
</style>

  5、由于post组件的模块module.js中需要对数据通过window.atob()方法进行base64解析,而nodeJS环境下无window对象,会报错。于是,代码修改如下

// components/Post/module
- text: decodeURIComponent(escape(window.atob(doc.content))) 
  text: typeof window === 'object' ? decodeURIComponent(escape(window.atob(doc.content))) : ''

 

1、前言

上一节我们大致讲了为什么需要使用vue后端渲染,以及vue后端渲染的基本原理,这节内容我们将从零开始搭建属于自己的vue后端渲染脚手架,当然不能不参考官方页面响应的实例vue-hackernews-2.0,从零开始搭建项目,源码在将在下节与大家共享。

服务器配置

  1、在根目录下,新建server.js文件

  由于在webpack中去掉了HTMLWebpackPlugin插件,而是通过nodejs来处理模板,同时也就缺少了该插件设置的HTML文件压缩功能

  需要在server.js文件中安装html-minifier来实现HTML文件压缩

const express = require('express')
const fs = require('fs')
const path = require('path')
const { createBundleRenderer } = require('vue-server-renderer')
const { minify } = require('html-minifier')
const app = express()
const resolve = file => path.resolve(__dirname, file)

const renderer = createBundleRenderer(require('./dist/vue-ssr-server-bundle.json'), {
  runInNewContext: false,
  template: fs.readFileSync(resolve('./index.html'), 'utf-8'),
  clientManifest: require('./dist/vue-ssr-client-manifest.json'),
  basedir: resolve('./dist')
})
app.use(express.static(path.join(__dirname, 'dist')))
app.get('*', (req, res) => {
  res.setHeader('Content-Type', 'text/html')
  const handleError = err => {
    if (err.url) {
      res.redirect(err.url)
    } else if (err.code === 404) {
      res.status(404).send('404 | Page Not Found')
    } else {
      res.status(500).send('500 | Internal Server Error')
      console.error(`error during render : ${req.url}`)
      console.error(err.stack)
    }
  }

  const context = {
    title: '小火柴的前端小站',
    url: req.url
  }
  renderer.renderToString(context, (err, html) => {
    console.log(err)
    if (err) {
      return handleError(err)
    }
    res.send(minify(html, { collapseWhitespace: true, minifyCSS: true}))
  })
})

app.on('error', err => console.log(err))
app.listen(8080, () => {
  console.log(`vue ssr started at localhost: 8080`)
})

  2、修改package.json文件

-     "build": "node build/build.js",
     "build:client": "node build/build.js",
     "build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.conf.js --progress --hide-modules",
     "build": "rimraf dist && npm run build:client && npm run build:server",

  3、修改index.html文件

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no">
    <link rel="shortcut icon" href="/static/favicon.ico">
    <title>小火柴的蓝色理想</title>
  </head>
  <body>
     <!--vue-ssr-outlet-->
  </body>
</html>

  4、取消代理

  如果继续使用代理如/api代理到后端接口,则可能会报如下错误

error:connect ECONNREFUSED 127.0.0.1:80

  直接写带有http的后端接口地址即可

const API_HOSTNAME = 'http://192.168.1.103:4000'

 

2、前期准备

基本环境要求:node版本6.10.1以上,npm版本3.10.10以上,本机环境是这样的,建议升级到官方最新版本。

使用的技术栈:

1、vue 2.4.2
2、vuex 2.3.1
3、vue-router 2.7.0
4、vue-server-renderer 2.4.2
5、express 4.15.4
6、axios 0.16.2
7、qs 6.5.0
8、q https://github.com/kriskowal/q.git
9、webpack 3.5.0
10、mockjs 1.0.1-beta3
11、babel 相关插件

以上是主要是用的技术栈,在构建过程中会是用相应的插件依赖包来配合进行压缩打包,以下是npm init后package.json文件所要添加的依赖包。

"dependencies": {
 "axios": "^0.16.2",
 "es6-promise": "^4.1.1",
 "express": "^4.15.4",
 "lodash": "^4.17.4",
 "q": "git https://github.com/kriskowal/q.git",
 "qs": "^6.5.0",
 "vue": "^2.4.2",
 "vue-router": "^2.7.0",
 "vue-server-renderer": "^2.4.2",
 "vuex": "^2.3.1"
 },
 "devDependencies": {
 "autoprefixer": "^7.1.2",
 "babel-core": "^6.25.0",
 "babel-loader": "^7.1.1",
 "babel-plugin-syntax-dynamic-import": "^6.18.0",
 "babel-plugin-transform-runtime": "^6.22.0",
 "babel-preset-env": "^1.6.0",
 "babel-preset-stage-2": "^6.22.0",
 "compression": "^1.7.1",
 "cross-env": "^5.0.5",
 "css-loader": "^0.28.4",
 "extract-text-webpack-plugin": "^3.0.0",
 "file-loader": "^0.11.2",
 "friendly-errors-webpack-plugin": "^1.6.1",
 "glob": "^7.1.2",
 "less": "^2.7.2",
 "less-loader": "^2.2.3",
 "lru-cache": "^4.1.1",
 "mockjs": "^1.0.1-beta3",
 "style-loader": "^0.19.0",
 "sw-precache-webpack-plugin": "^0.11.4",
 "url-loader": "^0.5.9",
 "vue-loader": "^13.0.4",
 "vue-style-loader": "^3.0.3",
 "vue-template-compiler": "^2.4.2",
 "vuex-router-sync": "^4.2.0",
 "webpack": "^3.5.0",
 "webpack-dev-middleware": "^1.12.0",
 "webpack-hot-middleware": "^2.18.2",
 "webpack-merge": "^4.1.0",
 "webpack-node-externals": "^1.6.0"
 }

本文由威尼斯wns.9778官网活动发布于计算机教程,转载请注明出处:基于vue-ssr服务端渲染入门详解威尼斯wns.9778官网

关键词: