vue ssr 项目改造经历

由于工作项目需求,需要将原有的项目改造,vue ssr 没有用到nuxt,因为vue ssr更利于seo,没办法,一个小白的改造经历,

首先说明一下,小白可以借鉴,高手也可以点评一下,因为,我写的不一定准确,只是针对我的项目。

下面先说一下大致:

原有项目有用到element,在改造ssr过程中,是很坑的。如果可以的话,还是强烈建议你重新改写成nuxt项目。由于我是小白,所以开始时候备份了一下项目,然后开始网上查找相关文章。

1.首先是这位大神的文章https://segmentfault.com/a/1190000012440041,笔名  右三。

2.然后是https://www.cnblogs.com/xiaohuochai/p/9158675.html,一个小火柴项目的改造过程。

3.https://segmentfault.com/a/1190000016637877  ,五步学会基础。

我列举他们三个,是因为,刚开始以为项目直接改改代码就可以,于是按照他们所说改写,发现,处处是坑,总之,他们的说法并不适合我的项目,于是苦思冥想,去看官网,再结合他们文章,开始大刀阔斧改造。

请您备份好:

先上一张原有项目图纸,就是普通的cli2构造出来的,其中theme是element主题,可不用理会。

接下来开始改造:

是不是不可思议,不敢整的,可以看文末怎么解决的一些坑。我接着分析

删除完以后,在build里添加四个文件:

1.    setup-dev-server.js

  1. const path = require('path')
  2. const webpack = require('webpack')
  3. const MFS = require('memory-fs')
  4. const clientConfig = require('./webpack.client.config')
  5. const serverConfig = require('./webpack.server.config')
  6.  
  7. module.exports = function setupDevServer (app, cb) {
  8. let bundle
  9. let template
  10.  
  11. // 修改客户端配置添加 热更新中间件
  12. clientConfig.entry.app = ['webpack-hot-middleware/client', clientConfig.entry.app]
  13. clientConfig.output.filename = '[name].js'
  14. clientConfig.plugins.push(
  15. new webpack.HotModuleReplacementPlugin(),
  16. new webpack.NoEmitOnErrorsPlugin()
  17. )
  18.  
  19. const clientCompiler = webpack(clientConfig) // 执行webpack
  20. const devMiddleware = require('webpack-dev-middleware')(clientCompiler, {
  21. publicPath: clientConfig.output.publicPath,
  22. stats: {
  23. colors: true,
  24. chunks: false
  25. }
  26. })
  27. app.use(devMiddleware)
  28.  
  29. clientCompiler.plugin('done', () => {
  30. const fs = devMiddleware.fileSystem
  31. // 模板为打包后的html文件
  32. const filePath = path.join(clientConfig.output.path, 'index.html')
  33. if (fs.existsSync(filePath)) {
  34. template = fs.readFileSync(filePath, 'utf-8')
  35. console.log("执行4")
  36. if (bundle) {
  37. console.log("执行1")
  38. cb(bundle, template)
  39. }
  40. }
  41. })
  42.  
  43. app.use(require('webpack-hot-middleware')(clientCompiler))
  44. // 监听 server renderer
  45. const serverCompiler = webpack(serverConfig)
  46. const mfs = new MFS() // 内存文件系统,在JavaScript对象中保存数据。
  47. serverCompiler.outputFileSystem = mfs
  48. serverCompiler.watch({}, (err, stats) => {
  49. if (err) throw err
  50. stats = stats.toJson()
  51. stats.errors.forEach(err => console.error(err))
  52. stats.warnings.forEach(err => console.warn(err))
  53. // 读取使用vue-ssr-webpack-plugin生成的bundle(vue-ssr-bundle.json)
  54. const bundlePath = path.join(serverConfig.output.path, 'vue-ssr-bundle.json')
  55. bundle = JSON.parse(mfs.readFileSync(bundlePath, 'utf-8'))
  56. console.log("执行3")
  57. if (template) {
  58. console.log("执行2")
  59. cb(bundle, template)
  60. }
  61. })
  62. }

2.     webpack.base.config.js

  1. const path = require('path')
  2. const ExtractTextPlugin = require('extract-text-webpack-plugin')
  3. const extractCSS = new ExtractTextPlugin('stylesheets/[name]-one.css');
  4.  
  5. // 这样我们在开发过程中仍然可以热重载,CSS 提取应该只用于生产环境
  6. const isProduction = process.env.NODE_ENV === 'production'
  7.  
  8. module.exports = {
  9. devtool: '#source-map',
  10. entry: {
  11. app: './src/entry-client.js',
  12. //app: ["babel-polyfill", "./src/entry-client.js"],///解决ie关键
  13. vendor: [
  14. 'vue',
  15. 'vue-router',
  16. 'vuex'
  17. ]
  18. },
  19. output: {
  20. path: path.resolve(__dirname, '../dist'),
  21. publicPath: '/dist/',
  22. filename: '[name].[chunkhash].js'
  23. },
  24. resolve: {
  25. alias: {
  26. 'static': path.resolve(__dirname, '../static'),
  27. // '@': path.resolve('src'),
  28. }
  29. },
  30. module: {
  31. noParse: /es6-promise\.js$/, // avoid webpack shimming process
  32. rules: [
  33. {
  34. test: /\.vue$/,
  35. loader: 'vue-loader',
  36. options: {
  37. extractCSS: isProduction,
  38. preserveWhitespace: false,
  39. postcss: [
  40. require('autoprefixer')({
  41. browsers: ['last 3 versions']
  42. })
  43. ]
  44. }
  45. },
  46.  
  47. {
  48. test: /\.js$/,
  49. loader: 'buble-loader',
  50. exclude: /node_modules/,
  51. options: {
  52. objectAssign: 'Object.assign'
  53. }
  54. },
  55. {
  56. test: /\.(png|jpg|gif|svg)$/,
  57. loader: 'url-loader',
  58. options: {
  59. limit: 10000,
  60. name: '[name].[ext]?[hash]'
  61. }
  62. },
  63. {
  64. test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
  65. loader: 'url-loader',
  66. query: {
  67. limit: 10000,
  68. name: 'fonts/[name].[hash:7].[ext]'
  69. }
  70. },
  71. {
  72. test: /\.css$/,
  73. use: isProduction
  74. ? ExtractTextPlugin.extract({
  75. use: 'css-loader',
  76. fallback: 'vue-style-loader'
  77. })
  78. : ['vue-style-loader', 'css-loader']
  79. }
  80. ]
  81. },
  82. plugins: isProduction
  83. // 确保添加了此插件!
  84. ? [new ExtractTextPlugin({ filename: 'common.[chunkhash].css' })]
  85. : [],
  86. performance: {
  87. hints: process.env.NODE_ENV === 'production' ? 'warning' : false,
  88. maxAssetSize: 30000000, // 整数类型(以字节为单位)
  89. maxEntrypointSize: 50000000, // 整数类型(以字节为单位)
  90. assetFilter: function(assetFilename) {
  91. // 提供资源文件名的断言函数
  92. return assetFilename.endsWith('.css') || assetFilename.endsWith('.js');
  93. }
  94.  
  95. }
  96. }

3.     webpack.client.config.js

  1. const webpack = require('webpack')
  2. const merge = require('webpack-merge')
  3. const base = require('./webpack.base.config')
  4. const HTMLPlugin = require('html-webpack-plugin')
  5. const SWPrecachePlugin = require('sw-precache-webpack-plugin')
  6.  
  7. const config = merge(base, {
  8. plugins: [
  9. // 全局变量
  10. new webpack.DefinePlugin({
  11. 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
  12. 'process.env.VUE_ENV': '"client"'
  13. }),
  14. // 将依赖模块提取到 vendor chunk 以获得更好的缓存,是很常见的做法。
  15. new webpack.optimize.CommonsChunkPlugin({
  16. name: 'vendor',
  17. minChunks: function (module) {
  18. return (
  19. // 如果它在 node_modules 中
  20. /node_modules/.test(module.context) &&
  21. // 如果 request 是一个 CSS 文件,则无需外置化提取
  22. !/\.css$/.test(module.request)
  23. )
  24. }
  25. }),
  26. // 提取 webpack 运行时和 manifest
  27. new webpack.optimize.CommonsChunkPlugin({
  28. name: 'manifest'
  29. }),
  30. // html模板
  31. new HTMLPlugin({
  32. template: 'index.html'
  33. })
  34. ]
  35. })
  36.  
  37. if (process.env.NODE_ENV === 'production') {
  38. config.plugins.push(
  39. // 生产环境下 - 压缩js
  40. new webpack.optimize.UglifyJsPlugin({
  41. compress: {
  42. warnings: false
  43. }
  44. }),
  45. // 用于使用service worker来缓存外部项目依赖项。
  46. new SWPrecachePlugin({
  47. cacheId: 'vue-hn',
  48. filename: 'service-worker.js',
  49. dontCacheBustUrlsMatching: /./,
  50. staticFileGlobsIgnorePatterns: [/index\.html$/, /\.map$/]
  51. })
  52. )
  53. }
  54.  
  55. module.exports = config

4.    webpack.server.config.js

  1. const webpack = require('webpack')
  2. const merge = require('webpack-merge')
  3. const base = require('./webpack.base.config')
  4. const VueSSRPlugin = require('vue-ssr-webpack-plugin')
  5. const nodeExternals = require('webpack-node-externals')
  6.  
  7. module.exports = merge(base, {
  8. target: 'node',
  9. entry: './src/entry-server.js',
  10. devtool: 'source-map',
  11. output: {
  12. filename: 'server-bundle.js',
  13. libraryTarget: 'commonjs2'
  14. },
  15.  
  16. externals: nodeExternals({
  17. // do not externalize CSS files in case we need to import it from a dep
  18. whitelist: /\.css$/,
  19. "jquery": "$",
  20. 'Vue': true,
  21. 'VueLazyload': true,
  22. '$': true,
  23. 'vue-router': 'VueRouter',
  24. 'vuex':'Vuex',
  25. 'axios': 'axios',
  26. // 'element-ui':'ELEMENT',
  27. }),
  28. plugins: [
  29. new webpack.DefinePlugin({
  30. 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
  31. 'process.env.VUE_ENV': '"server"'
  32. }),
  33. /*
  34. 使用webpack按需代码分割的特性的时候(require.ensure或动态import)结果就是服务端bundle会包含很多分开的文件。
  35. 'vue-ssr-webpack-plugin'作用是将其打包为一个单独的JSON文件,这个文件可以传入到bundleRenderer中(server.js),可以极大地简化了工作流。
  36. 默认文件名为 `vue-ssr-server-bundle.json`,也可以参数形式传入其他名称
  37. */
  38. new VueSSRPlugin()
  39. ]
  40. })

加完这些,大概是这样子

显然,加如这些不够。

我们还需要在根目录添加

server.js

  1. const fs = require('fs')
  2. const path = require('path')
  3. const express = require('express')
  4. const compression = require('compression') // 开启gzip压缩
  5. const resolve = file => path.resolve(__dirname, file)
  6.  
  7. const isProd = process.env.NODE_ENV === 'production'
  8. const serverInfo = `express/${require('express/package.json').version} ` +
  9. `vue-server-renderer/${require('vue-server-renderer/package.json').version}`
  10. const app = express()
  11. function createRenderer (bundle, template) {
  12. return require('vue-server-renderer').createBundleRenderer(bundle, {
  13. template, // 缓存
  14. cache: require('lru-cache')({
  15. max: 1000,
  16. maxAge: 1000 * 60 * 15
  17. })
  18. })
  19. }
  20. let renderer
  21. if (isProd) {
  22. const bundle = require('./dist/vue-ssr-bundle.json')
  23. const template = fs.readFileSync(resolve('./dist/index.html'), 'utf-8')
  24. renderer = createRenderer(bundle, template)
  25. } else {
  26. require('./build/setup-dev-server')(app, (bundle, template) => {
  27. renderer = createRenderer(bundle, template)
  28. })
  29. }
  30. const serve = (path, cache) => express.static(resolve(path), {
  31. maxAge: cache && isProd ? 60 * 60 * 24 * 30 : 0 // 静态资源设置缓存
  32. })
  33.  
  34. app.use(compression({ threshold: 0 })) // gzip压缩
  35. app.use('/dist', serve('./dist', true)) // 静态资源
  36. app.use('/static', serve('./static', true)) // 静态资源 (如:http://localhost:8080/public/logo-120.png)
  37. app.use('/manifest.json', serve('./manifest.json', true))
  38. app.use('/service-worker.js', serve('./dist/service-worker.js'))
  39.  
  40. app.get('*', (req, res) => {
  41. if (!renderer) {
  42. return res.end('未渲染成功||wei cheng gong')
  43. }
  44. const s = Date.now()
  45. res.setHeader("Content-Type", "text/html")
  46. res.setHeader("Server", serverInfo)
  47. const errorHandler = err => {
  48. if (err && err.code === 404) {
  49. console.log(404)
  50. res.status(404).end('404 | Page Not Found')
  51. } else {
  52. res.status(500).end('500 | Internal Server Error')
  53. console.error(`error during render : ${req.url}`)
  54. console.error(err)
  55. }
  56. }
  57.  
  58. var title = '测试-首页' // 自定义变量(此处用于title)
  59. var author ='Anne' // 默认author
  60. var keywords ='我是keywords' // 默认keywords
  61. var description ='我是description' //默认description
  62. renderer.renderToStream({title,author,keywords,description, url: req.url})
  63.  
  64. .on('error', errorHandler)
  65. .on('end', () => console.log(`整体请求: ${Date.now() - s}ms`))
  66. .pipe(res)
  67. })
  68.  
  69. const port = process.env.PORT || 3026
  70.  
  71. app.listen(port, () => {
  72. console.log(`localhost:${port}`)
  73. })

  

在src目录下还有俩:

这里注意一下我的入口文件为main.js。

app.js和utils.js文件夹,可不用理会,是项目单分出来的接口文件用axio。我们获取数据接口将在vuex里面写。也就是store文件夹。

entry-client.js:

  1. // entry-client.js 客户端渲染入口文件
  2. import Vue from 'vue'
  3. import { app, store, router } from './main'
  4.  
  5. /*Vue-SSR 根据访问的路由会调用当前路由组件中的asyncData方法由服务端调用相关接口,根据数据
  6. 生成首屏对应的html,并在返回的html中写入 window.__INITIAL_STATE__ = {服务端请求到的数据}
  7. 不需要服务端渲染的数据则在 mounted 中请求接口。*/
  8.  
  9. /*路由切换时组件的asyncData方法并不会被调用,若该组件存在服务端渲染方法asyncData,可通过下面
  10. 三种方式客户端调用,并进行客户端渲染*/
  11.  
  12. //(1)
  13. // 全局mixin,beforeRouteEnter,切换路由时,调用asyncData方法拉取数据进行客户端渲染
  14. // 注意beforeRouteEnter无法直接获取到当前组件this,需使用next((vm)=>{ vm即为this }) 获取
  15.  
  16. /*Vue.mixin({
  17. beforeRouteEnter (to, from, next) {
  18. console.log('beforeRouteEnter1')
  19. next((vm)=>{
  20. const {asyncData} = vm.$options
  21. console.log('beforeRouteEnter1'+ asyncData)
  22. if (asyncData) {
  23. asyncData(vm.$store, vm.$route).then(next).catch(next)
  24. } else {
  25. next()
  26. }
  27. })
  28.  
  29. }
  30. })*/
  31.  
  32. //(2)
  33. // 全局mixin,beforeRouteUpdate,切换路由时,调用asyncData方法拉取数据进行客户端渲染
  34. // beforeRouteUpdate可直接获取到this对象(2.2版本以上)
  35. /*Vue.mixin({
  36. beforeRouteUpdate (to, from, next) {
  37. console.log('beforeRouteUpdate2')
  38. const { asyncData } = this.$options
  39. if (asyncData) {
  40. // 传入store与route
  41. asyncData(this.$store, this.$route).then(next).catch(next)
  42. } else {
  43. next()
  44. }
  45. }
  46. })*/
  47.  
  48. // (3)
  49. // 注册全局mixin,所有组件beforeMount时,如果根组件_isMounted为真(即根实例已mounte,该钩子函数是由路由跳转触发的)
  50. // 调用asyncData方法拉取数据进行客户端渲染
  51. Vue.mixin({
  52. data(){ //全局mixin一个loading
  53. return {
  54. //loading:false
  55. }
  56. },
  57. beforeMount () {
  58. const { asyncData } = this.$options;
  59. let data=null; //把数据在computed的名称固定为data,防止重复渲染
  60. try{
  61. data=this.data; //通过try/catch包裹取值,防止data为空报错
  62. }catch(e){}
  63. if(asyncData&&!data){ //如果拥有asyncData和data为空的时候,进行数据加载
  64. //触发loading加载为true,显示加载器不显示实际内容
  65. //this.loading=true;
  66. //为当前组件的dataPromise赋值为这个返回的promise,通过判断这个的运行情况来改变loading状态或者进行数据的处理 (在组件内通过this.dataPromise.then保证数据存在)
  67. this.dataPromise=asyncData({store,route:router.currentRoute})
  68. // this.dataPromise.then(()=>{
  69. // //this.loading=false;
  70. // }).catch(e=>{
  71. // // this.loading=false;
  72. // })
  73. }else if(asyncData){
  74. //如果存在asyncData但是已经有数据了,也就是首屏情况的话返回一个成功函数,防止组件内因为判断then来做的操作因为没有promise报错
  75. this.dataPromise=Promise.resolve();
  76. }
  77. }
  78. })
  79. // 使用 window.__INITIAL_STATE__ 中的数据替换store中的数据
  80. if (window.__INITIAL_STATE__) {
  81. store.replaceState(window.__INITIAL_STATE__)
  82. }
  83.  
  84. router.onReady(() => {
  85. app.$mount('#app')
  86. })

entry-server.js:

  1. // entry-server.js
  2. import { app, router, store } from './main'
  3.  
  4. const isDev = process.env.NODE_ENV !== 'production' // 开发模式 || 生产模式
  5. export default context => {
  6. const s = isDev && Date.now()
  7.  
  8. // 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,
  9. // 以便服务器能够等待所有的内容在渲染前,
  10. // 就已经准备就绪。
  11. return new Promise((resolve, reject) => {
  12. console.log(context)
  13. // push对应访问路径
  14. router.push(context.url)
  15.  
  16. // 等到 router 将可能的异步组件和钩子函数解析完
  17. router.onReady(() => {
  18. const matchedComponents = router.getMatchedComponents() // 返回当前路径匹配到的组件
  19.  
  20. // 匹配不到的路由,reject(),返回 404
  21. if (!matchedComponents.length) {
  22. reject({ code: 404 })
  23. }
  24. // Promise.all 组件的 asyncData 方法 拿数据 全部数据返回后 为window.__INITIAL_STATE__赋值并 resolve(app)
  25. Promise.all(matchedComponents.map(component => {
  26. if (component.asyncData) {
  27. return component.asyncData({store, route: router.currentRoute})
  28. }
  29. }))
  30. .then(() => {
  31. isDev && console.log(`数据预取: ${Date.now() - s}ms`)
  32. context.state = store.state
  33. resolve(app)
  34. }).catch(reject)
  35. })
  36.  
  37. })
  38. }

同时main.js也需要改:

  1. import Vue from 'vue'
  2.  
  3. import App from './App.vue'
  4.  
  5. import { createRouter } from './router/index'
  6. import { createStore } from './store/index'
  7. import {sync} from 'vuex-router-sync'
  8.  
  9. //时间过滤器
  10. import './utils/jsontime.js'
  11. //title
  12. //import titleMixin from './utils/title'
  13. //ie
  14. //import 'babel-polyfill'
  15. require("babel-polyfill");
  16. import axios from 'axios'
  17. import VueAxios from 'vue-axios'
  18. //import Vuex from'vuex'
  19. //import MetaInfo from 'vue-meta-info'
  20.  
  21. Vue.prototype.filterHtml = function (msg) {
  22. if (msg) {
  23. return msg.replace(/<img/g, "<img style='max-width: 800px;max-height: 500px;margin:10px 30px;'")
  24. }
  25. return ''
  26. };
  27.  
  28. if (typeof window !== 'undefined') {
  29. require('element-ui/lib/theme-chalk/index.css');
  30. const ElementUI = require('element-ui');
  31. Vue.use(ElementUI);
  32. }
  33. // if (process.browser) {
  34. // //console.log('浏览器端渲染');
  35. // Vue.use(require('element-ui'),require('element-ui/lib/theme-chalk/index.css'))
  36. // } else {
  37. // //console.log("非浏览器端渲染");
  38. // }
  39.  
  40. Vue.config.productionTip = false;
  41. //Vue.mixin(titleMixin),
  42. // Vue.use(Vuex);
  43. Vue.use(VueAxios, axios);
  44. //Vue.use(MetaInfo);
  45.  
  46. const router = createRouter();
  47. const store = createStore();
  48. sync(store, router);
  49.  
  50. const app = new Vue({
  51. router,
  52. store,
  53. render: h => h(App)
  54. });
  55.  
  56. export { app, router, store }

这里主要部分还是这几句:其他的一会简单说明,

  1. import Vue from 'vue'
  2. import App from './App.vue'
  3. import { createRouter } from './router/index'
  4. import { createStore } from './store/index'
  5. import {sync} from 'vuex-router-sync'
  6.  
  7. Vue.config.productionTip = false;
  8. const router = createRouter();
  9. const store = createStore();
  10. sync(store, router);
  11. const app = new Vue({
  12. router,
  13. store,
  14. render: h => h(App)
  15. });
  16. export { app, router, store }

还有一个文件:差点忘了那个模板index,html

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="viewport" content="width=device-width,initial-scale=1.0">
  6. <title>{{title}}</title>
  7. <meta name="keywords" content='{{keywords}}'>
  8. <meta name="description" content='{{description}}'>
  9. <link rel="shortcut icon" type="image/x-icon" href="/static/favicon.ico">
  10.  
  11. <style>
  12. .Tmain{
  13. margin: 0px;
  14. }
  15. </style>
  16. </head>
  17. <body class="Tmain">
  18. <!--vue-ssr-outlet-->
  19. </body>
  20. </html>

接下来看路由文件:router/index.js

  1. import Vue from 'vue'
  2. import Router from 'vue-router'
  3.  
  4. Vue.use(Router)
  5.  
  6. export function createRouter() {
  7. return new Router({
  8. mode: 'history',
  9. routes: [
  10. {
  11. path: '/', name: 'index',
  12. component: () =>import('../views/index.vue')
  13. },
  14. {
  15. path: '/articlex/:id', name: 'articlex',
  16. component: resolve => require(['../views/main/article/articlex.vue'], resolve)
  17. },
  18. {path: "*", redirect: "/"}
  19. ]
  20. })
  21. }

这里第一个路由是ssr的异步加载,第二个为懒加载写法,第三个为404,返回到原页面。

还有store/index.js文件,问什么把这个放到最后呢,因为这个文件可以模块化,也可以写到一个里。

在一个里面写:

  1. import Vue from 'vue'
  2. import Vuex from 'vuex'
  3. import axios from 'axios'
  4.  
  5. Vue.use(Vuex)
  6.  
  7. // 数据
  8. let state = {
  9. lists: [], // 文章列表
  10. detail: {} // 文章详情
  11. }
  12.  
  13. // 事件
  14. let actions = {
  15. // 获取文章列表
  16. fetchLists ({ commit }, data) {
  17. return axios.get('https://xxxx/api/v1/topics?page=' + data.page)
  18. .then((res) => {
  19. if (res.data.success) {
  20. commit('setLists', res.data.data)
  21. }
  22. })
  23. },
  24. // 获取文章详情
  25. fetchDetail ({ commit }, data) {
  26. return axios.get('https://xxxx/api/v1/topic/' + data.id)
  27. .then((res) => {
  28. if (res.data.success) {
  29. commit('setDetail', res.data.data)
  30. }
  31. })
  32. }
  33. }
  34.  
  35. // 改变
  36. let mutations = {
  37. setLists (state, data) {
  38. state.lists = data
  39. },
  40. setDetail (state, data) {
  41. state.detail = data
  42. }
  43. }
  44.  
  45. // 获取
  46. let getters = {
  47. getLists: state => {
  48. return state.lists
  49. },
  50. getDetail: state => {
  51. return state.detail
  52. }
  53. }
  54.  
  55. export function createStore () {
  56. return new Vuex.Store({
  57. state,
  58. actions,
  59. mutations,
  60. getters
  61. })
  62. }

在模块化的话结构这样:

index.js:

  1. import Vue from 'vue'
  2. import vuex from 'vuex'
  3. import user from './modules/user'
  4. import getters from './getters'
  5.  
  6. Vue.use(vuex);
  7. export function createStore() {
  8. return new vuex.Store({
  9. modules: {
  10. user,
  11. },
  12. getters
  13. });
  14.  
  15. }

getters.js

  1. const getters = {
  2. lists: state => state.user.lists,
  3. detail: state => state.detail,
  4.  
  5. };
  6. export default getters

modules/user.js

  1. import axios from 'axios'
  2.  
  3. const user = {
  4. state: {
  5. lists: [],
  6. detail:{},
  7. },
  8.  
  9. mutations: {
  10. SET_LISTS: (state, lists) => {
  11. state.lists = lists;
  12. },
  13. SET_DETAIL: (state, detail) => {
  14. state.detail = detail;
  15. },
  16.  
  17. },
  18.  
  19. actions: {
  20.  
  21. // 获取文章列表
  22. fetchLists ({ commit }, data) {
  23. return axios.get('https://XXX/api/v1/topics?page=' + data.page)
  24. .then((res) => {
  25. if (res.data.success) {
  26. commit('SET_LISTS', res.data.data)
  27. }
  28. })
  29. },
  30. // 获取文章详情
  31. fetchDetail ({ commit }, data) {
  32. return axios.get(XXXX/api/v1/topic/' + data.id)
  33. .then((res) => {
  34. if (res.data.success) {
  35. commit('SET_DETAIL', res.data.data)
  36. }
  37. })
  38. }
  39. }
  40.  
  41. // 前端 登出
  42. FedLogOut({commit}) {
  43. return new Promise(resolve => {
  44. commit('SET_LISTS', "");
  45. commit('SET_DETAIL', "");
  46. resolve();
  47. });
  48. },
  49. }
  50. };
  51.  
  52. export default user;

写到这也算完成一半多了,继续往下看

一个页面怎么才算是开始渲染,用到了asyncData,

由于项目保密原因我只展示一页内容仅供参考:index.vue

  1. <template>
  2. <div>
  3. {{lists.id}}
  4. </div>
  5. </template>
  6. <script>
  7. import axios from 'axios'
  8. export default {
  9. /**
  10. * [SSR获取所有组件的asyncData并执行获得初始数据]
  11. * @param {[Object]} store [Vuex Store]
  12. * 此函数会在组件实例化之前调用,所以它无法访问 this。需要将 store 和路由信息作为参数传递进去:
  13. */
  14. asyncData (store, route) {
  15. return store.dispatch('fetchLists' ) // 服务端渲染触发
  16. },
  17. name: "home",
  18. // 数据
  19. data() {
  20. return {
  21.  
  22. }
  23. },
  24. // 计算属性
  25. computed: {
  26. lists () {
  27. return this.$store.getters.lists // 文章列表
  28. },
  29. },
  30.  
  31. mounted() {
  32.  
  33. },
  34. // 方法
  35. methods: {
  36.  
  37. },
  38. // 子组件
  39. components: {
  40. }
  41. }
  42. </script>
  43. <!--当前组件的样式 -->
  44. <style scoped>
  45.  
  46. </style>

最后启动了,改为

package.json

  1. "scripts": {
  2. "dev": "node server",
  3. "start": "cross-env NODE_ENV=production node server",
  4. "build": "rimraf dist && npm run build:client && npm run build:server",
  5. "build:client": "cross-env NODE_ENV=production webpack --config build/webpack.client.config.js --progress",
  6. "build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.config.js --progress"
  7. },
  1. // 安装依赖包
  2. npm install
  3. // 开发模式
  4. npm run dev
  5. // 生产模式
  6. npm run build
  7. npm run start

这样,就算是结束一个简单的渲染了,下面我将介绍我在开发中遇到的问题以及解决方法:


1.当我写完这些后发现一个大的问题,路径问题:之前写的项目有的路径用@/api/sre.js等。

由于重新写完发现@不好使了,没太深究直接../../绝对路径。


2.npm install 安装依赖插件。都安什么呢?

如果少个依赖,看报错信息《Can't find xxxx  dependence》。

我这里大致总结了写=些,项目直接码上:

npm install -g npm (更新npm)

npm i vue-server-renderer(一定要和vue版本一致,别问为啥,官方大大)

npm install babel-plugin-component --save-dev(element)

npm i axios buble buble-loader compression cross-env es6-promise express http-proxy-middleware lru-cache serve-favicon sw-precache-webpack-plugin vue-ssr-webpack-plugin vue-style-loader vuex vuex-router-sync webpack-hot-middleware webpack-merge webpack-node-externals

(lru-cache4.0.2,这个插件报错,就换这个版本)

npm install --save babel-polyfill   (ie兼容)


3。安装element-ui,这个大坑,趟浑水了。

1.    npm i element-ui -S

2.    main.js里这样引入就可以了。原因自己猜去吧

  1. if (typeof window !== 'undefined') {
  2. require('element-ui/lib/theme-chalk/index.css');
  3. const ElementUI = require('element-ui');
  4. Vue.use(ElementUI);
  5. }
  6. // if (process.browser) {
  7. // //console.log('浏览器端渲染');
  8. // Vue.use(require('element-ui'),require('element-ui/lib/theme-chalk/index.css'))
  9. // } else {
  10. // //console.log("非浏览器端渲染");
  11. // }

3.发现一个问题没有过多探究,好像首页和他的组件不能用懒加载方式。有待研究,因为我用懒加载,报错                                   error during render : / Error: stream.push() after EOF at readableAddChun


4.ie浏览器不兼容问题。

首页模板index.html 添加

  1. <meta http-equiv="X-UA-Compatible" content="IE=7,IE=9">
  2. <meta http-equiv="X-UA-Compatible" content="IE=7,9">
  3. <meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">
  4. #以上代码IE=edge告诉IE使用最新的引擎渲染网页,chrome=1则可以激活Chrome Frame.

npm install --save babel-polyfill

main.js里加

  1. require("babel-polyfill");//或者import "babel-polyfill";//注意放的位置

webpack.base.config.js里加

  1. module.exports = {
  2.  
  3.   entry: ["babel-polyfill", "./app/js"]
  4.  
  5. };

5.假如asyncData 调用多个vuex怎么写,这样就能并发

  1. asyncData ({ store , route }) {
  2. let data1= Promise.all([
  3. store.dispatch('ghnavList'),
  4. store.dispatch('gxnavList'),
  5. store.dispatch('getdonglist',{artdongid:21}),
  6. store.dispatch('getyoulist',{artyouid:22}),
  7. store.dispatch('getzhilist',{artzhiid:23})
  8. ])
  9. return data1
  10. },

6.第一次完成项目的时候,发现页面渲染了两次,就是一个页面显示两个一样的版块,很是揪心,于是我将index.html里的文件搞成这样:

  1. <body >
  2. <div id="app">
  3. <!--vue-ssr-outlet-->
  4. </div>
  5. </body>

指标不治本,下次完善。

7.head管理。

新建一个head.js

  1. function getHead (vm) {
  2. const { head } = vm.$options;
  3.  
  4. if (head) {
  5. return typeof head === 'function' ?
  6. head.call(vm) :
  7. head;
  8. }
  9. }
  10.  
  11. const serverHeadMixin = {
  12. created () {
  13. const head = getHead(this);
  14.  
  15. if (head) {
  16. if (head.title) this.$ssrContext.title = `${head.title}`;
  17. if (head.author) this.$ssrContext.author = `${head.author}`;
  18. if (head.keywords) this.$ssrContext.keywords = head.keywords;
  19. if (head.description) this.$ssrContext.description = head.description;
  20. }
  21. }
  22. };
  23.  
  24. const clientHeadMixin = {
  25. mounted () {
  26. const head = getHead(this);
  27.  
  28. if (head) {
  29. if (head.title) document.title = `${head.title}`;
  30. if (head.author) document.querySelector('meta[name="author"]').setAttribute('content', `${head.author}`);
  31. if (head.keywords) document.querySelector('meta[name="keywords"]').setAttribute('content', head.keywords);
  32. if (head.description) document.querySelector('meta[name="description"]').setAttribute('content', head.description);
  33. }
  34. }
  35. };
  36.  
  37. export default process.env.VUE_ENV === 'server' ?
  38. serverHeadMixin :
  39. clientHeadMixin;

在main.js引用

  1. import headMixin from './utils/head';
  2. Vue.mixin(headMixin);

当然index.html也做修改

  1. <head>
  2. <title>{{title}}</title>
  3. <meta name="keywords" content='{{keywords}}'>
  4. <meta name="description" content='{{description}}'>
  5.  
  6. </head>

在页面引用

  1. export default {
  2. name: 'index',
  3.  
  4. head(){
  5. return {
  6. 'title': '你好',
  7. 'author': '星涑'
  8. };
  9. },
  10.  
  11. }

在server.js里配置默认

  1. var title = '测试-首页' // 自定义变量(此处用于title)
  2. var author ='Anne' // 默认author
  3. var keywords ='我是keywords' // 默认keywords
  4. var description ='我是description' //默认description
  5. renderer.renderToStream({title,author,keywords,description, url: req.url})

现在时间24:00整,有很多东西还没有想到,来不及整理了,希望有更多的人看到我文章,对其进行评价补充,我将不胜感激。

还是墨迹那句话,希望有更多的人支持我,点个关注。我会努力发表更好的文章

QQ:1763907618------备注:博客园


vue ssr 项目改造经历的更多相关文章

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

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

  2. 改造@vue/cli项目为服务端渲染-ServerSideRender

    VUE SEO方案二 - SSR服务端渲染 在上一章中,我们分享了预渲染的方案来解决SEO问题,个人还是很中意此方案的,既简单又能解决大部分问题.但是也有着一定的缺陷,所以我们继续来看下一个方案--服 ...

  3. Vue SSR 配合Java的Javascript引擎j2v8实现服务端渲染2创建Vue2+webpack4项目

    前提 安装好nodejs并配置好环境变量,最好是 node10,https://nodejs.org/en/download/ 参考我之前的文章 debian安装nodejs Yarn &&a ...

  4. vue SSR 部署详解

    先用vue cli初始化一个项目吧. 输入命令行开始创建项目: vue create my-vue-ssr 记得不要选PWA,不知为何加了这个玩意儿就报错. 后续选router模式记得选 histor ...

  5. 一次优化web项目的经历记录(三)

    一次优化web项目的经历记录 这段时间以来的总结与反思 前言:最近很长一段时间没有更新博客了,忙于一堆子项目的开发,严重拖慢了学习与思考的进程. 开水倒满了需要提早放下杯子,晚了就会烫手,这段时间以来 ...

  6. vue SSR : 原理(一)

    前言: 由于vue 单页面对seo搜索引擎不支持,vue官网给了一个解决方案是ssr服务端渲染来解决seo这个问题,最近看了很多关于ssr的文章, 决定总结下: 参考博客:从0开始,搭建Vue2.0的 ...

  7. 转载一篇好理解的vue ssr文章

    转载:原文链接https://www.86886.wang/detail/5b8e6081f03d630ba8725892,谢谢作者的分享 前言 大多数Vue项目要支持SSR应该是为了SEO考虑,毕竟 ...

  8. Vue SSR不可不知的问题

    Vue SSR不可不知的问题 本文主要介绍Vue SSR(vue服务端渲染)的应用场景,开发中容易遇到的一些问题,提升ssr性能的方法,以及ssr的安全性问题. ssr的应用场景 1.SEO需求 SE ...

  9. 建立多页面vue.js项目

    介绍 根据需求,我们希望建立一个多页面的vue.js项目,如何改造单页面vue.js项目为多页面项目?跟着我的步伐看下去吧. 1.创建单页面vue.js项目 简单的记录一下创建步骤: --安装cnpm ...

随机推荐

  1. Sybase_ASA 字符串拼接

    列转行并拼接字符串,使用LIST函数 SELECT LIST(T.NAME,',') FROM TAB_DEMO T;

  2. spring IOC bean间关系

    1.0 继承关系 实体 package com.java.test5; import java.util.*; /** * @author nidegui * @create 2019-06-22 1 ...

  3. HTML5轻松实现全屏视频背景

    想在你的网页首页中全屏播放一段视频吗?而这段视频是作为网页的背景,不影响网页内容的正常浏览.那么我告诉你有一款Javascript库正合你意,它就是Bideo.js. 参考网址: https://ww ...

  4. 转:LINQ教程一:LINQ简介

    原文地址:https://www.cnblogs.com/dotnet261010/p/8278793.html 一.为什么要使用LINQ 要理解为什么使用LINQ,先来看下面一个例子.假设有一个整数 ...

  5. .net 学习视频

    http://www.iqiyi.com/a_19rrh9jx9p.html http://www.cnblogs.com/aarond/p/SQLDispatcher.html  --读写分离 ht ...

  6. 优化yum下载安装慢,不断换mirror

    不停地换mirror,为了解决这个问题,在网上搜了好多资料,总结出一个基于aliyun的mirror源 先检查:是否能正常上网,DNS是否正常,网关gw是否正常,若通过ping 不正常,则解决好网络, ...

  7. Django——11 状态保持 form表单 登陆注册样例

    Django 状态保持 用户状态例子 实现注册登陆实例 django forms 表单的使用 注册功能 登陆功能   状态保持cookie和session 1.http协议是无状态的:每次请求都是一次 ...

  8. python基础综合题----选自python二级考试

    <笑傲江湖>是金庸的重要武侠作品之一.这里给出一个<笑傲江湖>的网络版本, 文件名为“笑傲江湖-网络版.txt”.‪‬‪‬‪‬‪‬‪‬‮‬‫‬‫‬‪‬‪‬‪‬‪‬‪‬‮‬‪‬‭ ...

  9. CentOS 7.3降低内核版本为7.2

    查看当前内核版本: [root@nineep ~]# uname -r 2.3.10.0-514.2.2.el7.x86_64  查看当前发行版本: [root@nineep ~]# cat /etc ...

  10. Python 2 声明变量 输入输出 练习

    变量: 代指,用于将具体信息对应到一个值,便于反复使用时方便调用.例如  name = ("斯诺登")   变量声明规则:以字母开头的 字母数字下划线的组合.且不能是python代 ...