客户端 & 服务器端
Nuxt.js
前后端同构
beforeCreate()
和created()
生命周期同时存在于客户端和服务器端
有些代码可能只能运行在客户端,就需要加判断
if (process.browser) {
// 客户端代码
}
asyncData & fetch
asyncData
和fetch
只有在pages
里面有;layout
和components
里面都没有
过滤器
- 在
plugins/
目录下新建文件filter.js
import Vue from 'vue'
import moment from 'moment'
const filters = {
formatNum: (val) => {
if (val > 1000) {
return (val / 1000).toFixed(1) + 'k'
} else if (val > 1000000) {
return (val / 1000000).toFixed(1) + 'b'
} else {
return val
}
},
formatTime: (val) => {
if (!val) {
return '';
}
const d = new Date(val);
const now = new Date();
const Y1 = d.getFullYear();
const M1 = d.getMonth() + 1;
const D1 = d.getDate();
const yesterday = new Date(now.getTime() - 60 * 60 * 24 * 1000);
if (Y1 === now.getFullYear() && M1 === (now.getMonth() + 1) && D1 === now.getDate()) {
return moment(d).format('hh:mm');
} else if (Y1 === yesterday.getFullYear() && M1 === (yesterday.getMonth() + 1) && D1 === yesterday.getDate()) {
return `yesterday ${moment(d).format('hh:mm')}`;
} else {
return moment(d).format('yyyy-MM-dd hh:mm');
}
},
formatSize: (val) => {
let size = parseInt(val)
size = (size / 1024).toFixed(2)
if (size < 1024) {
return size + 'KB'
}
size = (size / 1024).toFixed(2)
if (size < 1024) {
return size + 'MB'
}
size = (size / 1024).toFixed(2)
return size + 'GB'
}
}
Object.keys(filters).forEach(key => {
Vue.filter(key, filters[key])
})
export default filters
- 修改
nuxt.config.js
文件,添加pluginis
配置
// ...
module.exports = {
// ...
plugins: [
// ...
{ src: '@/plugins/filters', ssr: true },
// ...
]
// ...
}
- 组件中直接使用即可
Vuex持久化
可以使用插件vuex-persistedstate
原理是将vuex
里面的数据存储到localStorage
里面
localStorage
只存在客户端,所以只能作用在客户端
详情参考:Vue常用插件:vuex-persistedstate
- 在
plugins/
目录下添加文件vuex-persistedstate.js
import createPersistedState from 'vuex-persistedstate'
export default (context) => {
createPersistedState()(context.store)
}
- 修改
nuxt.config.js
文件,添加pluginis
配置
// ...
module.exports = {
// ...
plugins: [
// ...
{ src: '@/plugins/vuex-persistedstate', ssr: false },
// ...
]
// ...
}
- 如果只要部分数据自动存储。修改
plugins/vuex-persistedstate.js
文件
import createPersistedState from 'vuex-persistedstate'
export default (context) => {
createPersistedState({
reducer(obj) {
// 其中 mobile, sidebar, user, deviceId, sessionId 为需要自动存储的 state
const { mobile, sidebar, user, deviceId, sessionId } = obj
return { mobile, sidebar, user, deviceId, sessionId }
}
})(context.store)
}
初始化Vuex
vuex-persistedstate
可以将数据持久化,但是只作用于客户端
页面刷新,asyncData
和fetch
请求数据的时候,可能需要vuex
里面的数据(比如:token
),这个时候vuex
的初始值不是存储在localStorage
中的值
需要将localStorage
里面的值获取到
思路:
- 页面请求时,在请求头中会携带
cookie
信息,我们将需要的数据放在cookie
里面nuxtServerInit
可以让我们在asyncData
和fetch
之前设置vuex
的值
步骤:
- 修改
store/
目录下的各模块文件,比如:index.js
import Cookies from 'js-cookie'
export const state = () => ({
// ...
user: {},
token: '',
// ...
})
export const mutations = {
// ...
setUserAvatar(state, avatar) {
state.user.Avatar = avatar
// avatar里面可能有 = 等特殊字符,先格式化一下再存
Cookies.set('userAvatar', encodeURIComponent(avatar))
},
setToken(state, token) {
state.token = token
Cookies.set('token', token)
},
// ...
}
// ...
- 修改
store/index.js
文件,添加nuxtServerInit
// ...
import { cookieParse } from '../utils/utils'
// ...
export const actions = {
nuxtServerInit({ commit }, { req }) {
const cookie = req.headers.cookie
if (cookie) {
// 将cookie转成json对象(自己实现该方法)
const cookieObj = cookieParse(cookie)
const { userAvatar, token } = cookieObj
// avatar里面可能有 = 等特殊字符,取出来后先转一下
userAvatar && commit('setUserAvatar', decodeURIComponent(userAvatar))
token && commit('setToken', token)
}
},
// ...
}
- 修改
utils/utils.js
文件,添加cookieParse
方法
// ...
export function cookieParse(cookie) {
if (!cookie) {
return {}
}
const arr = cookie.split(';')
const obj = {}
arr.forEach(item => {
const temp = item.split('=')
const key = trim(temp[0])
let value = trim(temp[1])
value = unescape(value)
try {
value = JSON.parse(value)
} catch {}
obj[key] = value
})
return obj
}
// ...
-
页面刷新的时候,可能不会触发
mutations
,我们主动调用一下,比如在layout
-
新建
mixins/
目录 -
在
mixins/
目录下新建文件layout.js
// ...
export default {
// ...
created() {
if (process.browser) {
this.user.Avatar && this.$store.commit('setUserAvatar', this.user.Avatar)
this.token && this.$store.commit('setToken', this.token)
}
},
// ...
}
- 修改
layout/
目录下的文件,给每个layout
添加mixins
// ...
import layoutMixin from '@/mixins/layout'
export default {
mixins: [layoutMixin],
// ...
}
配置多环境
思路:
- 执行命令时带上全局变量
env_config
- 根据全局变量调用不同的配置文件
- 修改
package.json
文件的scripts
部分
{
"scripts": {
"dev": "cross-env NODE_ENV=development env_config=dev nodemon server/index.js --watch server",
"build:sit": "cross-env NODE_ENV=production env_config=sit nuxt build",
"start:sit": "cross-env NODE_ENV=production env_config=sit node server/index.js",
"generate:sit": "cross-env NODE_ENV=production env_config=sit nuxt generate",
"build:prod": "cross-env NODE_ENV=production env_config=prod nuxt build",
"start:prod": "cross-env NODE_ENV=production env_config=prod node server/index.js",
"generate:prod": "cross-env NODE_ENV=production env_config=prod nuxt generate",
"lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
}
}
- 修改
nuxt.config.js
文件
module.exports = {
// ...
env: {
NODE_ENV: process.env.NODE_ENV,
env_config: process.env.env_config
},
// ...
}
-
在根目录下新建
config/
目录 -
在
config/
目录下新建文件dev.env.js
export default {
NODE_ENV: 'development',
ENV_CONFIG: 'dev',
urlApi: 'http://xxx', // 接口地址
urlCDN: 'http://xxx',
}
- 在
config/
目录下新建文件sit.env.js
export default {
NODE_ENV: 'production',
ENV_CONFIG: 'sit',
urlApi: 'http://xxx', // 接口地址
urlCDN: 'http://xxx',
}
- 在
config/
目录下新建文件prod.env.js
export default {
NODE_ENV: 'production',
ENV_CONFIG: 'prod',
urlApi: 'http://xxx', // 接口地址
urlCDN: 'http://xxx',
}
- 在
config/
目录下新建文件index.env.js
(使用require
的话,不用这么麻烦,直接const config = require('../config/' + process.env.env_config + '.env')
)
import dev from './dev.env'
import sit from './sit.env'
import prod from './prod.env'
let config = {}
switch (process.env.env_config) {
case 'dev':
config = dev
break
case 'sit':
config = sit
break
case 'prod':
config = prod
break
}
export default config
- 其他
Vue
或者js
文件中使用
import config from '@/config/'
console.log(config.urlApi)
全局样式分离
默认全局样式代码是直接写到页面中的
修改成打包成单独的css文件引入
- 修改
nuxt.config.js
文件
module.exports = {
// ...
build: {
// ...
extractCSS: {
// allChunks: true,
filename: '[name].css',
chunkFilename: '[id].css',
ignoreOrder: true
},
// ...
},
// ...
}
备注:开发环境页面会发生闪烁。不用管,生成环境是正常的
element-ui按需加载
-
安装插件
babel-plugin-component
:npm i babel-plugin-component --save-dev
-
修改
plugins/element-ui.js
文件
import Vue from 'vue'
// import Element from 'element-ui'
// import locale from 'element-ui/lib/locale/lang/en'
// Vue.use(Element, { locale })
// 按需引入
import {
Message,
Tooltip,
Dialog,
Menu,
Submenu,
MenuItem,
MenuItemGroup,
Tabs,
TabPane,
Row,
Col,
Button,
Form,
FormItem,
Input,
InputNumber,
Radio,
RadioGroup,
RadioButton,
Checkbox,
CheckboxButton,
CheckboxGroup,
Switch,
Select,
Option,
Popover,
Carousel,
CarouselItem,
Upload,
DatePicker,
Scrollbar,
Loading
} from 'element-ui'
import lang from 'element-ui/lib/locale/lang/en'
import locale from 'element-ui/lib/locale'
// 设置语言
locale.use(lang)
// 引入组件
Vue.component(Message)
Vue.use(Tooltip)
Vue.use(Dialog)
Vue.use(Menu)
Vue.use(Submenu)
Vue.use(MenuItem)
Vue.use(MenuItemGroup)
Vue.use(Tabs)
Vue.use(TabPane)
Vue.use(Row)
Vue.use(Col)
Vue.use(Button)
Vue.use(Form)
Vue.use(FormItem)
Vue.use(Input)
Vue.use(InputNumber)
Vue.use(Radio)
Vue.use(RadioGroup)
Vue.use(RadioButton)
Vue.use(Checkbox)
Vue.use(CheckboxButton)
Vue.use(CheckboxGroup)
Vue.use(Switch)
Vue.use(Select)
Vue.use(Option)
Vue.use(Popover)
Vue.use(Carousel)
Vue.use(CarouselItem)
Vue.use(Upload)
Vue.use(DatePicker)
Vue.use(Scrollbar)
Vue.use(Loading.directive)
Vue.prototype.$loading = Loading.service
Vue.prototype.$message = Message
- 修改
nuxt.config.js
配置
module.exports = {
// ...
css: [
// 'element-ui/lib/theme-chalk/index.css',
// ...
],
// ...
build: {
// ...
babel: {
plugins: [
[
'component',
{
'libraryName': 'element-ui',
// 'style': false // 不引入对应的css文件
'styleLibraryName': 'theme-chalk'
}
]
]
},
// ...
},
// ...
}
页面缓存
页面缓存可以使用keep-alive
添加 keep-alive
- 修改
layouts/
目录下的vue
文件,比如default.vue
<nuxt :key="activeDate" keep-alive :keep-alive-props="{ exclude: ['Publish'] }" />
修改页面
-
页面打开和离开,需要使用新的生命周期
activated
和deactivated
-
如果有监听的
$route
,需要添加路由判断
export default {
// ...
watch: {
$route(to, from) {
if (to.name === 'xxx') {
// ...
}
},
// ...
},
// ...
}
处理 asyncData
打开或者刷新页面,asyncData
在服务器端触发;然后页面由客户端接管,点击切换页面asyncData
在客户端触发
加上keep-alive
后asyncData
依然会在路由切换的时候触发
处理思路:
- 在
vuex
中维护一个数组(比如arrFirst: []
),该数组初始值为一个空数组- 某页面触发
asyncData
,先判断arrFirst
数组中有没有这个页面路由的name
- 如果有,表示已经执行过该页面的
asyncData
,直接return
- 如果没有,执行接口请求操作,将该页面路由的
name
给push
到arrFirst
数组中- 在
nuxtServerInit
中初始化arrFirst
,刷新页面清空arrFirst
- 备注:
arrFirst
不要做持久化存储,否则会造成客户端和服务器端不一致
- 修改
store/index.js
文件
export const state = () => ({
// ...
arrFirst: []
})
export const mutations = {
// ...
setArrFirst(state, str) {
if (state.arrFirst.indexOf(str) === -1) {
state.arrFirst.push(str)
}
},
clearArrFirst(state) {
state.arrFirst = []
}
}
export const actions = {
nuxtServerInit({ commit }, { req }) {
// 清空各个页面的first标识
commit('clearArrFirst')
// ...
},
// ...
}
- 修改
plugins/vuex-persistedstate.js
文件
import createPersistedState from 'vuex-persistedstate'
export default (context) => {
createPersistedState({
reducer(obj) {
const { xx, xxx } = obj
return { xx, xxx } // 这里不要加 arrFirst
}
})(context.store)
}
- 修改需要做缓存的页面,比如:
pages/index.vue
// ...
export default {
asyncData({ app, store, route }) {
if (store.state.arrFirst.indexOf(route.name) !== -1) {
return
}
return axios
.all([
// ...
])
.then(
axios.spread((xxx, xxx, xxx) => {
store.commit('setArrFirst', route.name)
// ...
return res
})
)
},
// ...
}
发表评论