vue服务端渲染

您所在的位置:网站首页 vue项目优化seo vue服务端渲染

vue服务端渲染

#vue服务端渲染| 来源: 网络整理| 查看: 265

几个月前,公司要求(服务端渲染)——用vue-ssr做了个服务端渲染,从起搭建、开发、部署、浏览器渲染到优化,希望对有需要的小伙伴有帮助,若是有不足之处,望指出,一起讨论学习。——几个月过去了,公司又提出,不希望用vue,或是react或angular的ssr,希望用express + 引擎模板 做纯的html的服务端渲染,这个我下次分享,有兴趣的小伙伴可以一起交流学习。

一.前提(为什么使用vue-ssr)

更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。更快的内容到达时间,特别是对于缓存的网络情况或运行缓慢的设备

二.使用服务端渲染的权衡 (应注意的情况)

开发条件所限。一些浏览器的特定代码,只能在某些生命周期钩子函数中使用(node环境只能使用vue的beforeCreate 与 created 生命周期),例如document的操作可以在mounted生命周期中操作;涉及构建设置与部署的更多要求。与可以部署在任何静态文件服务器上的完全静态单页面应用程序(SPA)不同,服务端渲染应用程序,需要处于 node.js server 运行环境更多的服务端负载。 在Node.js中渲染完整的应用程序,显然会比仅仅提供静态资源的 server 更加大量占用 CPU 资源,因此如果在高流量环境下使用,要准备相应的服务器负载,并采取缓存策略

三.使用vue-cli 2.0 修改配置 搭建 ssr 框架以及开发

1.首先用vue-cli2.0 搭建出你想要的命名项目 例如

vue init webpack vue-ssr-test

· webpack 配置

2.修改vue-loader.conf.js

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

- extract: isProduction + extract: false

3.修改webpack.base.conf.js

只需修改entry入门配置即可

- app: './src/main.js' + app: './src/entry-client.js'

4.修改webpack.prod.conf.js

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

... + const VueSSRClientPlugin = require('vue-server-renderer/client-plugin') ...new webpack.DefinePlugin({'process.env': env, + 'process.env.VUE_ENV': '"client"'}), ... - 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' - }), ... + new VueSSRClientPlugin() ...

5、新增 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()] })

·入口配置

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

6.修改main.js

import Vue from 'vue' import Vuex from 'vuex' - import '@/assets/style.css' //这里删除全局样式,然后直接写在app.vue组件内 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: '' + render: h => h(App)}) + return { app, router, store } +}

7.新增entry-client.js

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

import Vue from 'vue' import createApp from './main'Vue.mixin({beforeRouteUpdate (to, from, next) {const { asyncData } = this.$optionsif (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.gettersconst { 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 = falseconst 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('#app') })

8.新增entry-sever.js

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.stateresolve(app)}).catch(reject)}, reject) })

·修改组件

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

9.修改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 }},...]}) +}

10.修改状态管理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,...}}) +}

11.使用asyncData方法来获取异步数据

要特别注意的是,由于asyncData只能通过路由发生作用,使用是非路由组件的异步数据获取最好移动到路由组件中  如果要通过asyncData获取多个数据,可以使用Promise.all()方法

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

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

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

// TheHeader.vuecomputed: {... - ...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 }) - }, ...

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

// main.js - import '@/assets/style.css' // App.vue ... ...

13.由于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))) : ''

·服务器配置

14、在根目录下,新建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: 'SSR-VUE-CLI',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(8989, () => {console.log(`vue ssr started at localhost: 8989`) })

15.修改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",

16.修改index.html文件

小火柴的蓝色理想 //主要是这里

17.取消代理

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

error:connect ECONNREFUSED 127.0.0.1:80

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

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

·测试

18.安装依赖包

npm install --save-dev vue-server-renderer express

19.构建

npm run build

20.运行

node server.js、

点击右键,查看网页源代码。结果如下,说明网站已经实现了服务器端渲染 

四.部署生产环境

1.使用 pm2 对node程序进行守护

全局安装pm2 (其依赖node 和 npm, 可以自行研究 pm2 的使用)

npm install pm2 -g

由于该网站需要守护nodejs程序,使用pm2部署较为合适

在项目根目录下,新建一个ecosystem.json文件,内容如下

{"apps" : [{"name" : "vue-ssr-test", //项目名称"script" : "./server.js", //启动服务的js脚本"env": { //配置的环境"COMMON_VARIABLE": "true"},"env_production" : { //生产环境"NODE_ENV": "production"}}],"deploy" : { //配置自动化的指令"production" : {"user" : "xxx", // 购买的服务器的用户名"host" : ["1.2.3.4"], // 服务器的ip地址"port" : "22", // 服务器的端口号"ref" : "origin/master", //代码管理的远程分支"repo" : "[email protected]:littlematch0123/blog-client.git", //代码管理的远程仓库地址"path" : "/home/xxx/www/mall", //在服务器中静态资源的存放路径"post-deploy" : "source ~/.nvm/nvm.sh && cnpm install && pm2 startOrRestart ecosystem.json --env production", //自定义的启动自动化的指令"ssh_options": "StrictHostKeyChecking=no","env" : {"NODE_ENV": "production" //该自动指令的环境}}} }

2.cdn、也就是说我们需要将我们的项目放到服务器里面去 (例如我这里)

首先在本地打包dist 【执行npm run build】

然后将所需要的文件或文件夹上传到服务器 【我这里用的是 FileZilla 软件】

dist 文件夹 server.js package.json index.template.html ecosystem.json

 

3、nginx 代理

如果要使用域名对项目进行访问,还需要进行nginx配置 

通过以上步骤就可以将我们的项目部署到线上了

五: 浏览器渲染

官网的代码中,如果使用开发环境development,则需要进行相当复杂的配置

能否应用当前的webpack.dev.conf.js来进行开发呢?完全可以,开发环境中使用浏览器端渲染,生产环境中使用服务器端渲染需要做出如下三点更改:

1、更改API地址,开发环境使用webpack代理,生产环境使用上线地址

// src/constants/API let API_HOSTNAME if (process.env.NODE_ENV === 'production') {API_HOSTNAME = 'https://pc.zhumanyao.com' } else {API_HOSTNAME = '/api' }

2.在index.html同级目录下,新建一个index.template.html文件,index.html是开发环境的模板文件,index.template.html是生产环境的模板文件

// index.html// index.template.html

3、更改服务器端入口文件server.js的模板文件为index.template.html

// server.js const renderer = createBundleRenderer(require('./dist/vue-ssr-server-bundle.json'), {runInNewContext: false,template: fs.readFileSync(resolve('./index.template.html'), 'utf-8'),clientManifest: require('./dist/vue-ssr-client-manifest.json'),basedir: resolve('./dist') })

经过简单的更改,即可实现开发环境使用浏览器端渲染,生产环境使用服务器端渲染的效果

六、SEO优化方案

1、图片格式

对于图片:(1)如果可以用精灵图的用精灵图——减少http请求;

(2)也可以将其转换成base64格式——这样他可以可js一起加载,同样减少http请求;

(3)再大点的图片,就可以利用浏览做缓存

2、服务端渲染的数据

服务端渲染的数据,我们要放在created 生命周期函数里面——保证源码可以看到

3、js的需求

对于页面对js的请求;在head部分中都是 preload 或 prefetch;他们的作用分别是高数浏览器 哪些是当前路由页面必须要用的js; 哪些是接下来页面要用,可以预加载(在浏览器空闲时加载)

4、路由的传参方式

这里最好是利用 /url/:id 方式,这样对seo友好;

5、页面首页的链接数

在首页最好是有外链,即:当首页最好有连接可以跳转到其他页面。(这里的连接是要直接嵌入在页面内,最好不用编程式。有利于SEO);但是当外链是跳出当前网站,在需要在连接上加el="nofollow" 属性;阻止爬虫去爬取,因为爬虫一旦爬出去,就不会再回来。 (为了seo优化,其他页面有连接也直接嵌入式的写入写页面)

6、多用语义化标签

这里我们多使用语义化标签 header、footer、section、h1~h6、article等,这样可以让爬虫直接抓取内容,对seo友好

7、设置TDK

每个页面都需要设置自己的tdk(title、description、keywords),这对seo优化起关键作用

8、符合XML标准

所有的单标签,都需要在尾部添加 '/';

等等,这里还有很多其他优化方案————(也用于其他部分)

七、动态设置TDK

在vue的服务端渲染中,如何动态设置tdk?

1、在src目下的建一个 /utils/head.js

// head.js function getHead (vm) {const { head } = vm.$options;if (head) {return typeof head === 'function' ?head.call(vm) :head;} } const serverHeadMixin = {created () {const head = getHead(this);if (head) {if (head.title) this.$ssrContext.title = `${head.title}`;if (head.keywords) this.$ssrContext.keywords = head.keywords;if (head.description) this.$ssrContext.description = head.description;}} };const clientHeadMixin = {mounted () {const head = getHead(this);if (head) {if (head.title) document.title = `${head.title}`;if (head.keywords) document.querySelector('meta[name="keywords"]').setAttribute('content', head.keywords);if (head.description) document.querySelector('meta[name="description"]').setAttribute('content', head.description);}} };export default process.env.VUE_ENV === 'server' ?serverHeadMixin :clientHeadMixin;

2、在server.js 里新增 //server.js

const context = { + title: '',url: req.url, + keywords: '', + description: '',}

3、在 index.template.html 里

//index.template.html +


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3