服务端渲染(nuxt.js) 入坑到出坑
肉很多 2020/7/10 11:08:50
概述:
由于公司网站需要改版,想前后端分离,但又不想影响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) 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 来设置当内存超过多少时就重新启动。避免内存溢出问题。
下面是测试服服务器只有两核现在可以看到有两个进程了。
后面项目基本上稳定下来了。。。

关于找一找教程网
本站文章仅代表作者观点,不代表本站立场,所有文章非营利性免费分享。
本站提供了软件编程、网站开发技术、服务器运维、人工智能等等IT技术文章,希望广大程序员努力学习,让我们用科技改变世界。
[服务端渲染(nuxt.js) 入坑到出坑]http://www.zyiz.net/tech/detail-143116.html
- 2023-03-22接口思维:如何使用 Context API 构建灵活、可维护的 React 组件-icode9专业技术文章分享
- 2023-03-22掌握 ReactJS Hooks:现代 Web 开发综合指南-icode9专业技术文章分享
- 2023-03-12热点面试题:Vue2、3 生命周期及作用?
- 2023-03-06【备战春招】第19天 新版 Node.js+Express+Koa2 开发Web Server博客 10-13
- 2023-02-14【备战春招】第6天 6-14 Vue3如何实现响应式
- 2023-02-14【备战春招】第六天+vue复习
- 2023-02-14【备战春招】第6天 新版 Node.js+Express+Koa2 开发Web Server博客 8-8~8-9
- 2023-02-13【老卫拆书】009期:Vue+Node肩挑全栈!《Node.js+Express+MongoDB+Vue.js全栈开发实战》开箱
- 2023-02-13【备战春招】第5天 React零基础入门到实战,完成企业级项目简书网站开发——React基础篇
- 2023-02-13【备战春招】第5天 新版 Node.js+Express+Koa2 开发Web Server博客 8-6