Summary:

Because the company’s website needs to be revised, we want to separate the front and back ends, but we don’t want to affect SEO, so we decided to use Nuxt.js for server-side rendering. I didn’t know anything about nuxt.js before. After a few days of document review. Behind the direct hand began to dry, in this record of their own pit, as well as some optimization. Now the website is FreeBuf. The nuxt.js documentation is quite clear. If you don’t know about it, check the documentation yourself. I’ll skip ahead to project scaffolding and some of the issues covered in the documentation.

Question:

1. About routes

Those of you who know nuxt.js know that routes are generated from files in the page directory. But what we’re doing is changing the old version, with all the weird routes and all the different routes specifying the same page, so it’s not practical to create a directory file for each one. Here’s how I handled it:

Create an utils directory and router.js file as follows:

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 = routerCopy the code

Introduced in nuxt.config.js:

const zrouter = require('./utils/router') ... module.exports = { mode: 'universal', ... router: {extendRoutes (router) {const routerList = zrouter.concat(router) {const routerList = zrouter.concat(router) {const routerList = zRouter.concat (router) return routerList } } ... }Copy the code

I’m going to mention here that I ran into a bug problem because I created it in the page directory

**/column/:id ** Route points to a page. **/column/:id **/column/:id ** See above for treatment. This is why I concatenated custom routes in front of the matching priority.

2. Environment variables

Environment variables are definitely needed in development. My configuration method is as follows:

Create env.js in the root directory

const env = {  
    production: {    
        base_url: 'xxxxxxx',    
        host_name: 'xxxxxxx'  
    },  
    development: {    
        base_url: 'xxxxxx',    
        host_name: 'xxxxxx'  
    }
}
module.exports = env
Copy the code

In nuxt. Config. Js

// env = require('./env') module. Exports = {mode: 'universal', env[process.env.NODE_ENV].base_url, host_name: env[process.env.NODE_ENV].host_name, }, }Copy the code

In the package. In 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", }, ... }Copy the code

This can then be used in the page via process.env.xxxx.

3. Customize SVG global usage

First create the ICONS directory in assets to house the SVG

Create the svgIcon/index directory component under the Components directory

<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.15 em. fill: currentColor; overflow: hidden; } </style>Copy the code

Create svG-icon.js under the plugins directory

Import Vue from 'Vue' import SvgIcon from '@/components/ SvgIcon 'Vue.com (' svG-icon ', SvgIcon) // Prerequest SVG component (loaded by previous SVG-sprite-loader) const req = require.context('@/assets/ ICONS ', false, /\.svg$/) const requireAll = requireContext => requireContext.keys().map(requireContext) requireAll(req)Copy the code

In 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]'        
                }      
            })    
        }
    }
}
Copy the code

Used in components

<svg-icon icon-class="xxx" class-name="xxx" />
Copy the code

4. File version to avoid cache

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,// Set this to false, otherwise js cannot add version number... }}Copy the code

5. Modify the file import path and CDN

module.exports = { ... Build :{publicPath: '/freebuf/', publicPath: 'https://www.xxx',// upload the contents of. Nuxt /dist/client directory to your CDN. }}Copy the code

6. Questions about user tokens

In the past, we wanted to obtain the token through the field in cookie on the server side. In the past, it was in Zhong ‘jian, but there was a problem that users could string. Finally, we gave up the operation of obtaining token on the server side and put it on the client side

7. Route authentication

In the website need some login users can access the page, I deal with the following method

Create router.js under the plugins directory

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() }) }Copy the code

In nuxt. Config. Js

module.exports = {  
    mode: 'universal',
    ...
    plugins: [
        { src: '@/plugins/router', ssr: false },
    ],    
    ...
}
Copy the code

Remember this only runs on the server side.

8. About server Error

Since nuXT is used for server rendering, asyncData must be used. Server error will inevitably occur if errors are not captured during Ajax request in asyncData.

My treatment is as follows:

Start by creating an error page in your layouts

<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"> </div> </template> <script> export default { props: ['error'], layout: Head () {return {title: '404'}}} </script>Copy the code

In 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 } }Copy the code

This will point you to a custom error page if you catch an error, or you can define different page contents with different statusCode values

9. vuex

For state management, nuxt.js is integrated as well.

Create userinfo.js under the store directory

export const state = () => ({
    userInfo: {},
})
export const mutations = {
    setUserInfo (state, text) {
        state.userInfo = text
    },
    deleteUserInfo (state) {
        state.userInfo = ''
    },
}
Copy the code

Use:

/ / read the computed: {userInfoData () {return this. $store. State. The userInfo. The userInfo}} / / write const userData = {} Use this. Codestore.com ('userInfo/setUserInfo', userData)Copy the code

Because of the nuxt.js integration, vuex can also be manipulated on the server side

To optimize the

Code 1.

  1. UI components introduce custom themes on demand
  2. Lazy loading of images
  3. Global CSS

2. The file

const CompressionPlugin = require('compression-webpack-plugin') const TerserPlugin = require('terser-webpack-plugin')module.exports = { ... Content-encoding :gzip new CompressionPlugin({test: / \. Js $| \. HTML $| \. CSS /, / / matching filename threshold: 10240, / / to compression of the data of more than 10 KB deleteOriginalAssets: False // Whether to delete the original file}), // Block some warnings and console debugger new TerserPlugin({terserOptions: {compress: {warnings: }}})], // large file fragmentation optimization: {splitChunks: {minSize: 10000, maxSize: 250000 } } } }Copy the code

3. The cache

The cache I use is lru-cache to view the document specifically, and here I will mention the pit that appears.

// Check whether the interface needs to be cached. Is there a cache instance. Interceptors. Request. Use ((config) = > {if (config. The cache) {const {params = {}, Data = {}} = config key = md5(config.url + json.stringify (params) + json.stringify (data)) // Remember config.url === Cached.get (key).config.url; // The key is unique. 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})} / / save the cache instance. The interceptors. Response. Use (/ / request success (res) = > {the if (res) status = = = 200) {/ / code if according to actual situation (res.data.code ! Reject (res.data)} else {if (res.config.cache) {// Set cached. set(key, res, res.config.time) } return Promise.resolve(res.data) } } else { return Promise.reject(res) } }, // Request failed (error) => {if (error) {if (error.status === 200) {return promise.resolve (error.data)} else {return Promise.reject(error) } } }Copy the code

First of all, let me explain how it’s written:

Why AM I using return promise.reject (cached.get (key)) in axios interceptor Request?

The reason is that I can’t run return promise.resolve () directly in the interceptor

Return promise.reject (cached.get (key))

The interface response interceptor checks whether the cached data is from the cache and returns the data directly, without affecting the encapsulated interface.

The odd thing is that the data string problem, the value is evaluated by key but somehow the returned data is the data of another interface, it feels weird. Later, a condition is added to determine whether there is a cache, that is, whether the current interface is consistent with the interface in the cache, which barely solves this problem.

Page caching:

Create pagecache.js in the root directory

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(' home update - need cache - set cache content ', 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(' fetch cached Content ', req.originalURL) res.writehead (200, {' content-type ': 'text/ HTML; Charset =utf-8'}) return res.end(existsHtml, 'utf-8')} else {res.original_end = res.end // Override res.end if (! CacheUrl (pathName)) {console.log(' No cache - no cache required ', Req.originalurl) return next()} else {res.end = function (data) {if (res.statuscode === 200) {// Set cache Console. log(' Cache required - set cache contents ', req.originalURL) cachePage.set(pathname, data, urldata.time)} res.original_end(data, 'utf-8') } } } next() }Copy the code

Create urlcache.js in utils

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 = cacheUrlCopy the code

The purpose of this file is to control how many pages need to be cached for different times

Create globalCache.js in the root directory

Module. Exports = cachePage const LRU = require('lru-cache') const cachePage = new LRU({Max: 10 // set maximum cache number}) module.exports = cachePageCopy the code

Finally introduced in nuxt.config.js

module.exports = {  
    ...
    serverMiddleware: [    
        './pageCache'  
    ],    
    ...
}
Copy the code

One issue I want to mention here is the issue of checking for updates

**cachePage.set(‘/’, null)** The cachepage.set (‘/’, null) I commented out above. Because the cache contents are shared, if the value is set to NULL, the result may be null when the user accesses the cache. Therefore, the method of variable is used to control the following, so as to avoid the error caused by the user taking null

4. The deployment is running

Since the runtime was run through Node, some problems occurred after the deployment of this project. Both the test server and the online pre-release environment were OK. However, server error frequently appeared on the home page when the project was finally launched.

Finally by looking at the server:… The CPU is actually more than 100%. And that’s just one thread running. I’d be surprised if I suddenly realized this.

Then I asked the operation and maintenance to install PM2, and I changed the configuration:

{... "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", }, ... }Copy the code
Although Node can only use single-core CPU ** -i Max **, the server can run as many cores as allocated, so that the server can run eight cores and have eight threads. This will greatly reduce the pressure of the core.

–max-memory-restart 100M sets node to restart when memory exceeds 100 MB. Avoid memory overflow problems.

Below is the test server with only two cores. Now you can see that there are two processes.

The rest of the project is basically stable…

5. RSS page display Settings: