概述:
由于公司网站需要改版,想前后端分离,但又不想影响seo,后面就决定使用nuxt.js做服务端渲染。对于nuxt.js之前是一点没有了解过,经过几天文档查看。后面直接上手开始干了,在此记录下自己踩的坑,以及一些优化。现在网站已经上线FreeBuf 。 关于nuxt.js文档上描述比较清晰,没有了解过的,自行查看文档。后面我直接跳过项目搭建,以及一些文档中提及到的问题。
问题:
1. 关于路由
了解nuxt.js的都知道,路由是通过page目录下面的文件生成的。但我们做的是改版,老版的各种奇怪的路由以及各种不同路由指定同一个页面,这样的话每一个都创建一个目录文件是不现实的。以下是我的处理方法:
创建一个utils目录以及router.js文件如下:
const { resolve } = require('path')
const router = [
{
name: 'vuls',
path: '/vuls',
component: resolve('./', 'pages/articles/web/index.vue')
},
{
name: 'columnId',
path: '/column/:id(\\d+).html',
component: resolve('./', 'pages/articles/web/_id.vue')
},
{
name: 'webid',
path: '/web/:id',
component: resolve('./', 'pages/articles/web/_id.vue')
},
....
]
module.exports = router
在nuxt.config.js引入:
const zrouter = require('./utils/router')
...
module.exports = {
mode: 'universal',
...
router: {
extendRoutes (router) {
//我这样写的目的是把自定义的路由拼接在前面(由于一些路由的问题所以需要优先匹配自己定义的路由)
const routerList = zrouter.concat(router)
return routerList
}
}
...
}
在这里我要提到的时我遇到一个特坑的问题:由于我在page目录下创建了
**/column/:id **路由指向到一个页面。但上线后告诉我 **/column/6666.html **这种路由指向的是文章详情和 **/column/:id **完全是两个页面。。。处理方法见上。这也是我为啥把自定义路由拼接在前面优先匹配的问题了。
2. 环境变量
在开发中环境变量肯定是需要的,我的配置方法如下:
在根目录创建env.js
const env = {
production: {
base_url: 'xxxxxxx',
host_name: 'xxxxxxx'
},
development: {
base_url: 'xxxxxx',
host_name: 'xxxxxx'
}
}
module.exports = env
在nuxt.config.js中
const env = require('./env')
module.exports = {
mode: 'universal',
// 环境变量配置
env: {
base_url: env[process.env.NODE_ENV].base_url,
host_name: env[process.env.NODE_ENV].host_name,
},
}
在package.json中
{
...
"scripts": {
"dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server --exec babel-node",
"build": "nuxt build",
"start": "cross-env NODE_ENV=production pm2 start server/index.js --max-memory-restart 100M -i max",
},
...
}
这样就可以在页面中通过process.env.xxxx来使用了。
3. 自定义SVG全局使用
首先在assets目录创建icons目录用来存放svg
在components目录下创建svgIcon/index目录组件
<template>
<svg :class="svgClass" aria-hidden="true">
<use :xlink:href="iconName" />
</svg>
</template>
<script>
export default {
name: 'SvgIcon',
props: {
iconClass: {
type: String,
required: true
},
className: {
type: String,
default: ''
}
},
computed: {
iconName() {
return `#${this.iconClass}`;
},
svgClass() {
if (this.className) {
return 'svg-icon ' + this.className;
} else {
return 'svg-icon';
}
}
}
};
</script>
<style scoped lang="less">
.svg-icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
</style>
在plugins目录下面创建svg-icon.js
import Vue from 'vue'
import SvgIcon from '@/components/svgIcon'
// 注册组件
Vue.component('svg-icon', SvgIcon)
// 预请求svg组件(通过之前的svg-sprite-loader加载)
const req = require.context('@/assets/icons', false, /\.svg$/)
const requireAll = requireContext => requireContext.keys().map(requireContext)
requireAll(req)
在nuxt.config.js中
module.exports = {
mode: 'universal',
plugins: [
'@/plugins/svg-icon',
],
build: {
extend (config, ctx) {
const svgRule = config.module.rules.find(rule => rule.test.test('.svg'))
svgRule.exclude = [resolve(__dirname, 'assets/icons')]
config.module.rules.push({
test: /\.svg$/,
include: [resolve(__dirname, 'assets/icons')],
loader: 'svg-sprite-loader',
options: {
symbolId: '[name]'
}
})
}
}
}
在组件中使用
<svg-icon icon-class="xxx" class-name="xxx" />
4. 文件版本号避免缓存
module.exports = {
...
build:{
filenames: {
app: ({ isDev }) => isDev ? '[name].js' : process.env.npm_package_version + '.[contenthash].js',
chunk: ({ isDev }) => isDev ? '[name].js' : process.env.npm_package_version + '.[contenthash].js',
css: ({ isDev }) => isDev ? '[name].css' : process.env.npm_package_version + '.[contenthash].css'
},
analyze: false,//特别要注意这个要设置false不然js是无法添加版本号的
...
}
}
5. 修改文件引入路径及cdn
module.exports = {
...
build:{
publicPath: '/freebuf/',
publicPath: 'https://www.xxx',// 打包后将.nuxt/dist/client目录的内容上传到您的CDN即可!
}
}
6. 关于用户token的问题
之前是想在服务端通过cookie里面的字段来获取token,之前是在zhong'jian但是会出现用户会串的问题,最后放弃在服务端做获取token的操作,放在客户端来做,在页面中与用户相关的内容都放在客户端来请求,这样也减轻服务器的压力
7. 路由鉴权
在网站中需要一些登录过的用户才能访问的页面,我的处理方法如下
在plugins目录下面创建router.js
import { getCookie } from '../utils/tool'
const authorityRouter = ['write']
export default ({ app, store }) => {
app.router.beforeEach((to, from, next) => {
if (authorityRouter.includes(to.name) && !getCookie('token')) {
return window.location.href = `${process.env.host_name}/oauth`
}
next()
})
}
在nuxt.config.js中
module.exports = {
mode: 'universal',
...
plugins: [
{ src: '@/plugins/router', ssr: false },
],
...
}
记住这只在服务端运行的。
8. 关于server error
由于用到nuxt来做服务端渲染,必须要用到asyncData,在asyncData中进行ajax请求时错误不做捕获处理的必然会出现server error。
我的处理如下:
首先在layouts中创建error页面
<template>
<div class="container">
<div class="container-top" style="background-image:url('/images/404.png')" />
<div class="container-bottom">
<p>404</p>
<nuxt-link to="/">
<a-button type="primary"> 返回首页</a-button>
</nuxt-link>
</div>
</div>
</template>
<script>
export default {
props: ['error'],
layout: 'blog', // 你可以为错误页面指定自定义的布局
head () {
return {
title: '404'
}
}
}
</script>
在asyncData中
async asyncData ({ route, error }) {
const [listData1, listData2, listData3, listData4] = await Promise.all([
userCategory().catch((e) => {
return error({ statusCode: 404 })
}),
categoryKeyword().catch((e) => {
return error({ statusCode: 404 })
}),
categorylist().catch((e) => {
return error({ statusCode: 404 })
}),
columnHot().catch((e) => {
return error({ statusCode: 404 })
})
])
return {
userDataList: listData1.data,
seoData: listData2.data,
dataLists: listData3.data,
homeColumnData: listData4.data
}
}
// 或者
async asyncData ({ route, error }) {
const [listData1, listData2, listData3, listData4] = await Promise.all([
try{
userCategory()
categoryKeyword()
categorylist()
columnHot()
} catch{
return error({ statusCode: 404 })
}
])
return {
userDataList: listData1.data,
seoData: listData2.data,
dataLists: listData3.data,
homeColumnData: listData4.data
}
}
这样捕获错误的话就会指向自定义的error页面,也可以通过不同的statusCode值定义不同的页面内容
9. vuex
关于状态管理的话,nuxt.js里面以及集成了。
在store目录下面创建userInfo.js
export const state = () => ({
userInfo: {},
})
export const mutations = {
setUserInfo (state, text) {
state.userInfo = text
},
deleteUserInfo (state) {
state.userInfo = ''
},
}
在页面中使用:
// 读
computed: {
userInfoData () {
return this.$store.state.userInfo.userInfo
}
}
//写
const userData = {}
this.$store.commit('userInfo/setUserInfo', userData)
//删
this.$store.commit('userInfo/deleteUserInfo')
由于nuxt.js 的集成,在服务端的一些也是可以操作vuex的
优化
1. 代码
- ui组件按需引入自定义主题
- 图片懒加载
- 全局css
2. 文件
const CompressionPlugin = require('compression-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin')module.exports = {
...
build:{
plugins: [
//代码压缩为gz 这个需要后端配合 Content-Encoding:gzip
new CompressionPlugin({
test: /\.js$|\.html$|\.css/, // 匹配文件名
threshold: 10240, // 对超过10kb的数据进行压缩
deleteOriginalAssets: false // 是否删除原文件
}),
// 屏蔽一些警告及console debugger
new TerserPlugin({
terserOptions: {
compress: {
warnings: true,
drop_console: true,
drop_debugger: true
}
}
})
],
// 大文件切割
optimization: {
splitChunks: {
minSize: 10000,
maxSize: 250000
}
}
}
}
3. 缓存
缓存我使用的是lru-cache具体查看文档,在这里我说一下出现的坑。
// 判定是否是需要缓存的接口,是否有缓存
instance.interceptors.request.use( (config) => {
if (config.cache) {
const { params = {}, data = {} } = config
key = md5(config.url + JSON.stringify(params) + JSON.stringify(data))
// 记住 config.url === CACHED.get(key).config.url 这个判断必须要加上不然会出现取缓存内容错乱问题
// 这也是我不明白的问题key已经是唯一的了,但还是会出现数据错乱问题
if (CACHED.has(key) && config.url === CACHED.get(key).config.url) {
return Promise.reject(CACHED.get(key))
} else {
return config
}
} else {
return config
}
}
//接口封装页面 可以设置不同接口缓存不同时间
import instance from '@/plugins/service.js'
export const getHotList = (params) => {
return instance.get('/index/index', {
params, cache: true, time: 5000
}
)}
// 存缓存
instance.interceptors.response.use(
// 请求成功
(res) => {
if (res.status === 200) {
// code根据实际情况
if (res.data.code !== 200) {
return Promise.reject(res.data)
} else {
if (res.config.cache) {
// 返回结果前先设置缓存
CACHED.set(key, res, res.config.time)
}
return Promise.resolve(res.data)
}
} else {
return Promise.reject(res)
}
},
// 请求失败
(error) => {
if (error) {
if (error.status === 200) {
return Promise.resolve(error.data)
} else {
return Promise.reject(error)
}
}
}
首先我解释下上面的写法:
为啥我在axios拦截器request中使用return Promise.reject(CACHED.get(key))
原因是我在拦截器中无法直接运行 return Promise.resolve()
更不可能终结此次请求,后来直接return Promise.reject(CACHED.get(key))
把缓存数据带过来,在通过接口响应拦截器进行判断是否是从缓存中的数据,直接返回这个数据就行,对封装的接口是没有影响的。
很奇怪的一点是数据会串的问题,取值是通过key来取值的但不知道为啥返回的数据会是另外一个接口的数据,感觉很诡异。后面判断是否有缓存时多加了一个条件,就是当前的接口和缓存中的接口是否一致,勉强解决了此问题。
页面缓存:
在根目录创建pageCache.js
import instance from './plugins/service.js'
const cachePage = require('./globalCache')
const cacheUrl = require('./utils/urlCache')
export default async function (req, res, next) {
let isUpdata = false
const pathname = req.originalUrl
const urlData = cacheUrl(pathname)
if (pathname === '/') {
console.log(parseInt(new Date().getTime() / 1000))
await instance.get('xxxxxxxx', { time: parseInt(new Date().getTime() / 1000) }).then((res) => {
if (res.data.home) {
console.log('首页有更新-需要缓存-设置缓存内容', req.originalUrl)
isUpdata = true
// cachePage.set('/', null)
}
console.log(res)
}).catch((err) => {
console.log(err)
})
}
const existsHtml = cachePage.get(pathname)
if (existsHtml && !isUpdata) {
console.log('取缓存内容', req.originalUrl)
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
return res.end(existsHtml, 'utf-8')
} else {
res.original_end = res.end
// 重写res.end
if (!cacheUrl(pathname)) {
console.log('没有缓存-不需要缓存', req.originalUrl)
return next()
} else {
res.end = function (data) {
if (res.statusCode === 200) {
// 设置缓存
console.log('需要缓存-设置缓存内容', req.originalUrl)
cachePage.set(pathname, data, urlData.time)
}
res.original_end(data, 'utf-8')
}
}
}
next()
}
在utils中创建urlCache.js
const cacheUrl = function (url) {
const list = new RegExp('/[articles | abc]+(/[a-z]*)?')
const nuber = new RegExp('[0-9]')
if (url === '/') {
return { name: '/', time: 1000 * 60 }
} else if (nuber.test(url)) {
return false
} else if (list.test(url)) {
return { name: url, time: 1000 * 60 * 5 }
} else {
return false
}
}
module.exports = cacheUrl
这个文件的意义就是控制所需要缓存的页面已经不同缓存时间
在根目录中创建globalCache.js
const LRU = require('lru-cache')
const cachePage = new LRU({
max: 10 //设置最大缓存数量
})
module.exports = cachePage
最后在nuxt.config.js中引入
module.exports = {
...
serverMiddleware: [
'./pageCache'
],
...
}
在这里我要提及一个问题就是检测是否有更新这个问题
上面我注释掉的**cachePage.set('/', null)**这个之前是检测到有更新,就把缓存内容设为null,在高并发的时候会引发一些问题。因为缓存内容是共用的,这边设置为null的时候,可能出现用户取缓存时,取到的结果是null。所以后面运用变量的方法来控制,避免用户取到null而引发错误
4. 部署运行
由于运行时是通过node来运行的,在此项目部署后出现了一些问题,测试服和线上预发布环境都是ok的,但最后上线时刻首页频繁出现server error为了排查这个问题发了很长时间,最后想到是不是服务器内存和cpu的原因。
最后通过查看服务器:...cpu竟然高达100%以上。而且还只是一个线程在运行。突然明白了这个不挂才怪呢 。
后面就让运维安装了PM2,我这边改了下配置:
{
...
"scripts": {
"dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server --exec babel-node",
"build": "nuxt build",
"start": "cross-env NODE_ENV=production pm2 start server/index.js --max-memory-restart 100M -i max",
},
...
}
node还有个问题就是内存溢出问题,通过 --max-memory-restart 100M来设置当内存超过多少时就重新启动。避免内存溢出问题。
下面是测试服服务器只有两核现在可以看到有两个进程了。
后面项目基本上稳定下来了。。。