1. <dd id="erndk"></dd>
                1. 服務端渲染(nuxt.js) 入坑到出坑

                  肉很多 2020/7/10 11:08:50

                  概述:由于公司網站需要改版,想前后端分離,但又不想影響seo,后面就決定使用nuxt.js做服務端渲染。對于nuxt.js之前是一點沒有了解過,經過幾天文檔查看。后面直接上手開始干了,在此記錄下自己踩的坑,以及一些優化?,F在網站已經上線FreeBuf 。 關于nuxt.js文檔上描述…

                  概述:

                  由于公司網站需要改版,想前后端分離,但又不想影響seo,后面就決定使用nuxt.js做服務端渲染。對于nuxt.js之前是一點沒有了解過,經過幾天文檔查看。后面直接上手開始干了,在此記錄下自己踩的坑,以及一些優化?,F在網站已經上線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. 代碼

                  1. ui組件按需引入自定義主題
                  2. 圖片懶加載
                  3. 全局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只能使用單核cpu的   -i max 是服務器分配了幾核就運行幾個,這樣的話服務器如果是8核的,運行8個的話,就會有8個線程。這樣會大大減小單核的壓力了。

                  node還有個問題就是內存溢出問題,通過 --max-memory-restart 100M 來設置當內存超過多少時就重新啟動。避免內存溢出問題。

                  下面是測試服服務器只有兩核現在可以看到有兩個進程了。

                  后面項目基本上穩定下來了。。。



                  隨時隨地學軟件編程-關注百度小程序和微信小程序
                  關于找一找教程網

                  本站文章僅代表作者觀點,不代表本站立場,所有文章非營利性免費分享。
                  本站提供了軟件編程、網站開發技術、服務器運維、人工智能等等IT技術文章,希望廣大程序員努力學習,讓我們用科技改變世界。
                  [服務端渲染(nuxt.js) 入坑到出坑]http://www.yachtsalesaustralia.com/tech/detail-143116.html

                  贊(0)
                  關注微信小程序
                  程序員編程王-隨時隨地學編程

                  掃描二維碼或查找【程序員編程王】

                  可以隨時隨地學編程啦!

                  技術文章導航 更多>
                  国产在线拍揄自揄视频菠萝

                        1. <dd id="erndk"></dd>