vue服务端渲染——项目搭建、开发、生产环境的部署、浏览渲染、SEO优化
几个月前,公司要求(服务端渲染)——用vue-ssr做了个服务端渲染,从起搭建、开发、部署、浏览器渲染到优化,希望对有需要的小伙伴有帮助,若是有不足之处,望指出,一起讨论学习。——几个月过去了,公司又提出,不希望用vue,或是react或angular的ssr,希望用express + 引擎模板 做纯的html的服务端渲染,这个我下次分享,有兴趣的小伙伴可以一起交流学习。一.前提(为什么使用v..
几个月前,公司要求(服务端渲染)——用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: '<App/>'
+ 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.$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('#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.state
resolve(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 } = 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 })
- },
...
12.将全局css从main.js移动到App.vue中的内联style样式中,因为main.js中未设置css文件解析
// main.js
- import '@/assets/style.css'
// App.vue
...
<style module lang="postcss">
...
</style>
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文件
<!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>
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" : "git@github.com: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
<body>
<div id="app"></div>
</body>
// index.template.html
<body>
<!--vue-ssr-outlet-->
</body>
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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
+ <meta name='keywords' content='{{keywords}}'>
+ <meta name='description' content='{{description}}'>
<link rel="shortcut icon" href="./static/logo-icon.png">
<script type="text/javascript" src="http://api.map.baidu.com/getscript?v=2.0&ak=ttvedgC1rj7yoVihmTA8LbVLMOg8WSwv"></script>
+ <title>{{title}}</title>
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
4、在main.js 或 app.js 里面全局引入
// app.js
+ import headMixin from './utils/head';
+ Vue.mixin(headMixin);
5、在组件汇总使用
// home.vue
export default {
name: 'home',
data () {
return {
}
},
+ head () {
+ return {
+ 'title': '首页',
+ 'keywords': '关键词',
+ 'description': '描述'
+ }
+ },
...
}
6、动态设置tdk (即: 页面的内容是来势与后台,或根据id得到的)
// detail.vue 'detail/:id'
(1) 在utils下建tdk.js
// tdk.js
const tdk = (url) => {
if (url === 'goods_0000') {
return fill('深迪科技企业级BOM管理', 'bom,主数据,配置,变更管理,生命周期,EBOM,MBOM', '面向未来智能制造以及大规模个性化定制业务,构建一个全面支持正向研发体系、致力提升内部核心能力和灵活应对外部环境变化的完整的BOM管理体系。')
} else if (url === 'goods_0001') {
return fill('深迪科技MES', 'MES,制造执行系统,追溯,看板,安灯,工单,上料防错', '深迪MES的优点是精确的JIT式拉动生产。适用于大批量小批次的离散加工。')
} else if (url === 'case_000000') {
return fill('深迪科技BOM管理系统案例', 'BOM,主数据,工艺,配置,变更管理,生命周期,EBOM,MBOM,整车物料管理', 'XBOM系统解决JAC工厂工艺与MBOM结合的问题,支撑工艺断点管理需求,可适应以后其他代工工厂同类问题;作为整车物料号管理的源头,完成了整车号生成规则管理、整车号管理以及与下游系统的集成。')
} else if (url === 'case_000100') {
return fill('深迪科技MES系统案例', 'MES,JIT,价值流,工单,车间计划,车间拉料,看板', '深迪与美的携手打造,它是深迪MES以价值链维度展开的应用,倍受好评与青睐。')
} else if (url === 'case_000101') {
return fill('深迪科技MES系统案例', 'MES,ERP集成,打通ERP,andon,系统集成,工业4.0,生产订单', '深迪与百年德企快可美携手打造,它是深迪MES在德系工业4.0中的实际应用。')
}
}
function fill(title, keywords, description) {
return {title: `${title}`, keywords: `${keywords}`, description: `${description}`}
}
export default tdk
(2) 在 detail.vue
//detail.vue
<script>
...
+ import tdk from '@/utils/tdk'
...
export default {
name: 'detail',
data () {
return {
...
+ tdk: tdk(this.$route.params.id)
}
},
+ head () {
+ return {
+ 'title': this.tdk.title,
+ 'keywords': this.tdk.keywords,
+ 'description': this.tdk.description
+ }
+ },
watch: {
'$route'(to, from) {
+ this.tdk = tdk(this.$route.params.id)
...
}
},
...
methods: {
...
}
}
</script>
更多推荐
所有评论(0)