vue 空格报错_Vue SSR服务端渲染实践——将vue-cli3项目平滑转为nuxt.js项目

df34706eb581a461a77e5cfbf2b6e02e.png

目前来说,搜索引擎仍然不能很好的处理SPA页面(2019.1)。那么为了流量考虑,必须应对SEO的需求。现代的前端工具链对工作效率的提升自不必言,是不可能回到古典时代的,那很明显就只能走服务端渲染(SSR)方案了。

vue ssr名声最大的方案自然是nuxt.js,但是我本来是不想用的,因为项目结构会发生剧烈的变化,连router都没了,感受一下,这是项目根目录:

21ec5c5740e4b64e44de8e8aff0e1c68.png

因此我还是想要基于vue cli3的项目结构来搞,不做太多的变动。

我考察了几个Vue SSR方案,感觉都不是很行:官方SSR配置麻烦案例少,vue-cli3里面流行的几个,比如akryum/vue-cli-plugin-ssr、vueneue及其升级版uvue都不怎么成熟。最后没有办法,决定试一下nuxt.js,于是……

61e20b8dc3f03b17db3818a4b2017f8f.png

但是nuxt.js实在有着太多的不同,例如项目结构变化很大,router.js没了,vuex store写法有变化,router钩子没了等等。老项目毕竟也有一些体量,这么折腾我可接受不了,不过经过一番调查,我发现这些问题不是不可以解决。因此虽然迁移是要迁移的,但是要尽量保持vue-cli 3项目的风味,以最小的改动完成迁移。为此我做了很多调查,本文的目的就在于此。

重建项目

没得说,直接掏出命令行开始吧,这是官网get started文档的操作:

npx create-nuxt-app my-project

我们的原则是尽量和vue cli3项目一致。选项基本上选默认,除了eslint打开。值得注意的是有个axios module,这就是个this.$axios插件,我个人目前认为没啥用,也不影响一般的axios使用方式。

8ddfa7c91c7e824f319dd4acf33de0e0.png

然后进入目录,一开始肯定跟我一样是一脸懵逼的:

22f8136b7b7cb9eef6665bfb7ff6b1f0.png

懵逼不要紧,一步步对照着往下做就可以了。

ESLint 配置

首先把.eslintrc.js对照着修改一下:

29ebe6782e4c8a5b38ab3eca7150165f.png

我习惯的代码风格是4空格缩进的standard,_给lodash,$留给全局工具函数,我估计大家各有各的说法,自己酌情修改:

module.exports = {
  root: true,
  env: {
    browser: true,
    node: true
  },
  parserOptions: {
    parser: 'babel-eslint'
  },
  extends: [
    'plugin:vue/essential',
    '@vue/standard'
  ],
  // required to lint *.vue files
  plugins: [
    'vue'
  ],
  // add your custom rules here
  rules: {
    'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
    // indent 4 spaces
    'indent': ['error', 4, { 'SwitchCase': 1 }],
    // allow paren-less arrow functions
    'arrow-parens': 0,
    // allow async-await
    'generator-star-spacing': 0
  },
  'globals': {
      '$': true,
      '_': true
  }
}

这里别忘了把包装上

npm install --save-dev @vue/eslint-config-standard

package.json

改一下项目版本和项目说明,在scripts里加上serve,这样就能 npm run serve 了

41ff8e980a34017f899597573ee9832e.png

nuxt项目里有npm run lint,但行为和vue cli3不一样,只检测不修复。这里给lint加一个 --fix让它可以自动格式化。

  "scripts": {
    "dev": "nuxt",
    "serve": "nuxt",
    "build": "nuxt build",
    "start": "nuxt start",
    "generate": "nuxt generate",
    "lint": "eslint --ext .js,.vue --ignore-path .gitignore . --fix",
    "precommit": "npm run lint"
  },

项目依赖我觉得就没啥好说的了,vue cli3的项目依赖非常简洁,很容易分辨,基本上无脑拷贝过来就行了。

保持vue cli3风格目录结构

项目目录多了一大堆目录,我们依稀能够辨认出components、store、asssets等等,这时很容易恍然大悟,nuxt.js把之前在src目录里的内容弄出来了。

我还是比较习惯传统的风格,因此又把他们放了回去:

修改nuxt.config.js,加一行srcDir: 'src/',

b3f4824c9c26b5b7073af1d426f0b50c.png

创建一个叫src的目录,把以下目录剪切进去

be01686e70a2b5622196a2ac660b2f88.png

是不是感觉熟悉多了:

983cf2e9dd49b41bffcf0a27abfd7647.png

scss配置

npm install --save-dev node-sass sass-loader

053e232512cea7015bd66b66dcc1d90a.png
        postcss: {
            plugins: {
                autoprefixer: {}
            }
        },

Webpack插件 / 注册全局模块(如lodash)

a2756589aa35ba73724d18341f6851ec.png
        plugins: [
            new webpack.ProvidePlugin({
                '_': path.resolve(__dirname, './src/lodash.js')
            })
        ],

使用代码router,而不是nuxt自动生成的导航配置

可以注意到nuxt.js一个很大不同是router.js没了,按官方的说法是使用目录自动动态生成router.js。我觉得其实还可以,但没有必要那么精巧。老项目迁移,写都写了,直接拿过来就是。

好在可以改,nuxt.js官方提供了一个插件:nuxt-community/router-module

将其加入项目

npm install --save @nuxtjs/router

然后修改配置文件:

b8312bd460b962385988dea4739bfcc8.png
    modules: [
        '@nuxtjs/router'
    ],

router.js写在src/下,需要略微改变写法,导出为createRouter函数

export function createRouter () {
    let router = new Router({
        mode: 'history',
        base: process.env.BASE_URL,
        routes: [
            { path: '/', name: 'index', component: Index },
            { path: '/about', name: 'about', component: About }
        ]
    })

    return router
}

移除所有异步加载

有可能你像我一样碰到了这样的报错,然后一脸懵逼:

render function or template not defined in component: anonymous

3dd71ce531a8abe11f5133d067751176.png

我排查了半天,发现是router.js中异步加载的锅(当然前提是router.js按照上文移植过来了)。

const TopicNew = () => import('@/views/topic-new.vue')

改了就好了。

Vuex

nuxt.js的vuex写法有比较大的区别,建议直接看着文档改 Vuex Store - Nuxt.js

这里给个最简单的例子吧:

export const state = () => ({
    counter: 0
})

export const mutations = {
    increment (state) {
        state.counter++
    }
}

export const actions = {
    async init ({ state, commit }) {
    }
}

也有一种变动较小的方法,官方文档里讲了。但也说明了在nuxt.js 3之后就不会再支持。

说句题外话,import store from './store/index' 在ssr项目中是无法使用了(包括vue官方ssr)。

Router全局钩子

有两种办法,第一种是写nuxt plugin直接拿router对象

export default ({ app }) => {
    app.router.beforeEach(async function (to, from, next) {
    }
}

然后在外面nuxt.config.js里写一句

    plugins: [
        '@/plugins/route'
    ],

但这种办法如果在beforeEach里面做请求拿数据,就会引起DOM渲染不同步的警告(nuxt.js v2.3.4)。只要发出请求,不做任何其他事情,就会出错(其实这是非常不科学的)。

顺便提一句,这里通过app.store可以拿到store

第二种办法,也是nuxt.js官方推荐的办法,是写middleware:

我在src/middleware下写一个router-guards.js

export default async function ({ store, route, redirect, req }) {
    console.log('hello')
    // let ret = await axios.get(`http://localhost:8888/`)
}

这样就可以了。

保持用户登录身份 / 获取用户数据

注意,SSR情况下服务端会替你渲染首屏,当用户已经登录了,页面上势必会变得不同!SSR服务端去请求后端的时候,是不会管你的cookies和access_token之类东西的,因此会导致服务端和浏览器端DOM不同步。

首先可能有人想到在上面的middleware里面做这个操作,毕竟在SPA页面中beforeEach里向服务器拿当前用户的用户信息,也算是一个常规操作。

我开始的思路也是这样的,但请注意,尽管逻辑好像没有问题,这是一个错误示范

import api from '@/netapi'

export default async function ({ store, route, redirect, req }) {
    if (!store.state.misc) {
        let ret = await api.misc()
        store.commit('SET_MISC', ret.data)

        if (process.server) {
            ret = await api.userInfo2(req.headers)
        } else {
            ret = await api.userInfo()
        }

        if (ret.code === 0) {
            store.commit('SET_USERDATA', ret.data)
        }
    }
}

middleware会在服务端和浏览器端都执行,但第一次执行只是在服务端(第一次访问服务端会渲染直接整个网页给客户端,因此不需要客户端做什么),这时拿到的req就是客户端向服务端发起的那个网页请求

所以这里就很明显了,第一个api.userInfo2()是专供服务端的,并且把网页请求的headers拼装进去,以期望把cookies也带进去以用户身份拿用户信息。

第二个api.userInfo()只在浏览器端执行,这是SPA时候的正常操作。

但是请再次注意:这种办法是不行的!!虽然理论上看起来正确,但也会原因不明的报服务端浏览器端不同步……如果有谁清楚为什么,麻烦请告诉我。

正确做法:

store/index.js中写这样一个action:

export const actions = {
    async nuxtServerInit ({ commit }, { req }) {
        let ret = await api.misc()
        commit('SET_MISC', ret.data)

        ret = await api.userInfo2(req.headers)
        if (ret.code === 0) {
            commit('SET_USERDATA', ret.data)
        }
    }
}

与上面的错误示范原理是完全一样的,这个action只在服务端执行,这里的req是客户端向服务端发起的那个网页请求

有人可能会问只在服务端给state赋了值,那浏览器端又没有这么做,怎么办?

但其实不用担心,服务端渲染首屏,完成后会把当前的vuex状态和组件状态都保存起来传给浏览器,然后浏览器端读档,因此就不用再取数据了。

如果不太明白参考官方案例 Auth External API (JWT) 。

Store模式(vuex低配版)的迁移

所谓store模式,在vuex文档里讲到“什么时候应该用vuex的时候”被提了一句:Vuex 是什么? | Vuex

其实简单来说就是一个被多个组件import的js文件,在里面存放数据。

基本上可以告诉大家,忘了这回事吧。SSR的时候机制与SPA不同,无法保证全局共享同一份数据。可能你看着这里设置了,但是实际上渲染模板的时候读不出来。

所以直接改成vuex吧。

修改router-link

全局替换为nuxt-link

总结

其实我还是比较希望nuxt能够以vue cli3插件的形式被使用,这样的话事情就简单多了。不过世事无常,虽然工具链一直在进步,我们也不能干等着那一天的到来,姑且先自己动动腿往那一边走两步吧。

话又说回来,迁移到SSR这种大工程又岂止是我这二三句能够完全说清楚的,有些坑我自己还呆在里面呢!只是希望抛砖引玉,后面我也会更新一些自己遇到的问题和解决方法,也希望大家不吝赐教。

以下是一个按文中方法修改后的nuxt.js空项目:

https://github.com/fy0/nuxt-example-project-like-vue-cli3​github.com
e3be0bf7f0c564002d9d3c9ec48592db.png
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值