欢迎光临
我们一直在努力

基于vue现有项目的服务器端渲染SSR改造

前面的话

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

 

效果

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

 

概述

【定义】

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

【优点】

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

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

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

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

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

 

思路

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

基于vue现有项目的服务器端渲染SSR改造

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

 

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() ] })

 

入口配置

在浏览器端渲染中,入口文件是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、在根目录下,新建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.180

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

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

 

测试

1、安装依赖包

cnpm install --save-dev vue-server-renderer

2、构建

npm run build

3、运行

node server.js

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

基于vue现有项目的服务器端渲染SSR改造

 

部署

【pm2】

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

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

{ "apps" : [{ "name" : "blog-www", "script" : "./index.js", "env": { "COMMON_VARIABLE": "true" }, "env_production" : { "NODE_ENV": "production" } }], "deploy" : { "production" : { "user" : "xxx", "host" : ["1.2.3.4"], "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" } } } }

【CDN】

由于项目实际上既有静态资源,也有nodeJS程序。因此,最好把静态资源上传到七牛CDN上

自行选择服务器的一个目录,新建upload.js文件

var fs = require('fs'); var qiniu = require('qiniu'); var accessKey = 'xxx'; var secretKey = 'xxx'; var mac = new qiniu.auth.digest.Mac(accessKey, secretKey); var staticPath = '/home/www/blog/client/source/'; var prefix = 'client/static'; var bucket = 'static'; var config = new qiniu.conf.Config(); config.zone = qiniu.zone.Zone_z1; var formUploader = new qiniu.form_up.FormUploader(config); var putExtra = new qiniu.form_up.PutExtra(); putExtra = null; // 一定要将putExtra设置为null,否则会出现所有文件类别都被识别为第一个文件的类型的情况 // 文件上传方法 function uploadFile (localFile) { // 配置上传到七牛云的完整路径 const key = localFile.replace(staticPath, prefix) const options = { scope: bucket + ":" + key, } const putPolicy = new qiniu.rs.PutPolicy(options) // 生成上传凭证 const uploadToken = putPolicy.uploadToken(mac) // 上传文件  formUploader.putFile(uploadToken, key, localFile, putExtra, function(respErr, respBody, respInfo) { if (respErr) throw respErr if (respInfo.statusCode == 200) { console.log(respBody); } else { console.log(respInfo.statusCode); console.log(respBody); } }) } // 目录上传方法 function uploadDirectory (dirPath) { fs.readdir(dirPath, function (err, files) { if (err) throw err // 遍历目录下的内容 files.forEach(item => { let path = `${dirPath}/${item}` fs.stat(path, function (err, stats) { if (err) throw err // 是目录就接着遍历 否则上传 if (stats.isDirectory()) uploadDirectory(path) else uploadFile(path, item) }) }) }) } fs.exists(staticPath, function (exists) { if (!exists) { console.log('目录不存在!') } else { console.log('开始上传...') uploadDirectory(staticPath) } })

【post-deploy】

然后,修改ecosystem.json文件中的post-deploy项

"source ~/.nvm/nvm.sh && cnpm install && npm run build && node /home/xiaohuochai/blog/client/upload.js&& pm2 startOrRestart ecosystem.json --env production",

但是,经过实际测试,在服务器端进行构建build,极其容易造成服务器死机。于是,还是在本地构建完成后,上传dist文件到服务器再进行相关操作

"source ~/.nvm/nvm.sh && cnpm install && node /home/xiaohuochai/blog/client/upload.js&& pm2 startOrRestart ecosystem.json --env production"

修改项目的静态资源地址为CDN地址,API地址为服务器API地址

// config/index.js assetsPublicPath: 'https://static.xiaohuochai.site/client/' // src/constants/API.js const API_HOSTNAME = 'https://api.xiaohuochai.cc'

【nginx】

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

upstream client { server 127.0.0.1:3002; } server{ listen 80; server_name www.xiaohuochai.cc xiaohuochai.cc; return 301 https://www.xiaohuochai.cc$request_uri; } server{ listen 443 http2; server_name www.xiaohuochai.cc xiaohuochai.cc; ssl on; ssl_certificate /home/blog/client/crt/www.xiaohuochai.cc.crt; ssl_certificate_key /home/blog/client/crt/www.xiaohuochai.cc.key; ssl_session_timeout 5m; ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; if ($host = 'xiaohuochai.cc'){ rewrite ^/(.*)$ http://www.xiaohuochai.cc/$1 permanent;  } location / { expires 7d; add_header Content-Security-Policy "default-src 'self' https://static.xiaohuochai.site; connect-src https://api.xiaohuochai.cc; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://static.xiaohuochai.site ; img-src 'self' data: https://pic.xiaohuochai.site https://static.xiaohuochai.site; style-src 'self' 'unsafe-inline' https://static.xiaohuochai.site; frame-src https://demo.xiaohuochai.site https://xiaohuochai.site https://www.xiaohuochai.site;"; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forward-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_set_header X-Nginx-Proxy true; proxy_pass http://client;  proxy_redirect off; } } 

 

浏览器渲染

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

能否应用当前的webpack.dev.conf.js来进行开发呢?完全可以,开发环境中使用浏览器端渲染,生产环境中使用服务器端渲染

需要做出如下三点更改:

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

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

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

// index.html <body> <div id="root"></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') })

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

 

  • 海报
海报图正在生成中...
赞(0) 打赏
声明:
1、本博客不从事任何主机及服务器租赁业务,不参与任何交易,也绝非中介。博客内容仅记录博主个人感兴趣的服务器测评结果及一些服务器相关的优惠活动,信息均摘自网络或来自服务商主动提供;所以对本博客提及的内容不作直接、间接、法定、约定的保证,博客内容也不具备任何参考价值及引导作用,访问者需自行甄别。
2、访问本博客请务必遵守有关互联网的相关法律、规定与规则;不能利用本博客所提及的内容从事任何违法、违规操作;否则造成的一切后果由访问者自行承担。
3、未成年人及不能独立承担法律责任的个人及群体请勿访问本博客。
4、一旦您访问本博客,即表示您已经知晓并接受了以上声明通告。
文章名称:《基于vue现有项目的服务器端渲染SSR改造》
文章链接:https://www.456zj.com/5625.html
本站资源仅供个人学习交流,请于下载后24小时内删除,不允许用于商业用途,否则法律问题自行承担。

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址