vue單頁面項目SEO優化問題


之前用vue做了一個動態官網項目,后期客戶要求seo,百度上之前搜索不到官網地址,后來在項目的入口文件index.html頁面加上了,固定的meta標簽,加上name名為keywords、description的meta標簽。

例子:

    <meta charset="utf-8">
    //下面這個meta標簽 是ie8的專用標記,指定ie8瀏覽器器模擬特定版本的ie瀏覽器渲染方式,下面指定的是edge
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    //在ie、360等瀏覽器打開時,默認使用極速模式,然后是兼容模式,然后是標准模式
    <meta name="renderer" content="webkit|ie-comp|ie-stand">
    //seo相關,百度收錄時,的描述文字
    <meta name="description" content="第十七屆xxxxx醫學大會">
    //seo相關:百度搜索時的 關鍵字,就是用那些關鍵字可以搜索到此網站
    <meta name="keywords" content="中國國際xx醫學大會,第十七屆,xx,醫學,國際">

以上做了個簡單的seo優化,這個項目有幾個官網,但是其中只有一個官網要求seo,也就是在百度能夠搜索到,當時為了應急,就寫死了,但是,其它的網站也就會受到干擾了,也就是對於一個項目對應幾個官網,寫死的meta標簽做seo是不科學的。

於是下面又來尋找更科學的seo優化方案,下面是一些相關連接,很感謝作者:

https://blog.csdn.net/weixin_41049850/article/details/81945201

文中提到了關於vue單頁項目的一些seo優化方案:

首先想vue、react、angular三大前端框架都有spa頁面,做起來seo就很麻煩,當然,不是必須要求用這三個框架就必須采用spa的開發模式,但是spa前后端分離的形式真的是太方便了,如果不采用spa方式進行開發,用古老的混合開發自然不會存在seo的問題,但是我們現在大多采用的是spa的形式開發的,

 

 方案1:服務端渲染

上面這個截圖是vue作者,為解決seo優化所提出的一個方案,服務端渲染(ssr):官網鏈接:https://cn.vuejs.org/v2/guide/ssr.html

如果項目剛開始就考慮到seo,采用服務端渲染,那么就用服務端渲染就得了。

但是一般來講,項目做到后期才會考慮到seo的問題,這時再去搞服務端渲染,相當於重頭寫項目,非常耗費人力物力。

那么,不采用服務端渲染該如何最大程度解決seo問題呢?

方案2:預渲染

原文鏈接:https://www.cnblogs.com/kdcg/p/9606302.html

 這比服務端渲染要簡單很多,而且可以配合 vue-meta-info 來生成title和meta標簽,基本可以滿足SEO的需求 

TIPS: 使用預渲染vue-router必須使用history模式

// 安裝
npm install prerender-spa-plugin --save-dev

然后在webpack.prod.conf.js里面添加:cli3項目在vue.config.js文件中配置

// 頭部引入
const PrerenderSPAPlugin = require('prerender-spa-plugin')
const Renderer = PrerenderSPAPlugin.PuppeteerRenderer

在plugins里面添加:

config.js

// vue.config.js
const webpack = require('webpack');
const path = require('path');
const CompressionPlugin = require('compression-webpack-plugin');//引入gzip壓縮插件
// 預渲染
const PrerenderSPAPlugin = require('prerender-spa-plugin') const Renderer = PrerenderSPAPlugin.PuppeteerRenderer function resolve(dir) {
  return path.join(__dirname, dir)
}


module.exports = {
  // 選項...
    //基本路徑
    publicPath: process.env.NODE_ENV === 'production'? '/': '/',//部署服務器的路徑 默認在根路徑上(影響靜態資源的引用路徑)
    outputDir: 'customizationWeb',
    assetsDir: 'static',
    productionSourceMap:false,//打包時不要map文件
    // filenameHashing: true,
  devServer: {
      port: 9522,
        proxy:{
          '/qiantai':{
            target:'http://139.xxx.xxx.xx:xxxx',
            changeOrigin: true,
            pathRewrite: {
                "^/qiantai": ''
            }
          }
          
        }
    },
    configureWebpack:()=> {
      if (process.env.NODE_ENV === 'development'){
         return {
          externals: {//如果不想影響開發環境,這里也要配置externals  沒用它的就不用在開發環境也配置一份了
            'vue': 'Vue',
            'vuex': 'Vuex',
            // 'vue-router': 'VueRouter',
            'Axios':'axios'
          },
         }
      }else{
        return {
          externals: {
            'vue': 'Vue',
            'vuex': 'Vuex',
            // 'vue-router': 'VueRouter',
            'Axios':'axios'
          },
          plugins: [ new CompressionPlugin({//gzip壓縮配置
              test:/\.js$|\.html$|\.css/,//匹配文件名
              threshold:10240,//對超過10kb的數據進行壓縮
              deleteOriginalAssets:false,//是否刪除原文件
            }),
            new PrerenderSPAPlugin({ // 生成文件的路徑,也可以與webpakc打包的一致。
              // 下面這句話非常重要!!!
              // 這個目錄只能有一級,如果目錄層次大於一級,在生成的時候不會有任何錯誤提示,在預渲染的時候只會卡着不動。
              staticDir: path.join(__dirname, 'customizationWeb'), // 對應自己的路由文件,比如a有參數,就需要寫成 /a/param1。
              routes: ['/'], // 這個很重要,如果沒有配置這段,也不會進行預編譯
              renderer: new Renderer({ inject: { foo: 'bar' }, headless: false, // 在 main.js 中 document.dispatchEvent(new Event('render-event')),兩者的事件名稱要對應上。
                renderAfterDocumentEvent: 'render-event' }) })
          ]
        }
      }
      
    },
    chainWebpack(config) {
      // set svg-sprite-loader
      config.module
        .rule('svg')
        .exclude.add(resolve('src/icons'))
        .end()
      config.module
        .rule('icons')
        .test(/\.svg$/)
        .include.add(resolve('src/icons'))
        .end()
        .use('svg-sprite-loader')
        .loader('svg-sprite-loader')
        .options({
          symbolId: 'icon-[name]'
        })
        .end() 
        // 打包依賴分析
      // config
      //   .plugin('webpack-bundle-analyzer')
      //   .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin)
    }
    
}

 

綠色背景的代碼是 預渲染相關都二代碼

在main.js加上這點代碼:

new Vue({
  router,
  store,
  render: h => h(App),
  mounted: () => document.dispatchEvent(new Event("x-app-rendered")),
  mounted() { document.dispatchEvent(new Event('render-event')) },
}).$mount("#app");

 

這種操作不需要你去添加任何一段代碼,直接npm run build,dist文件中就有你寫的幾個html靜態文件。

然后運行npm run build 

 發現打包好的index.html中多了一大堆的代碼,部署到nginx上試試看看什么效果

 

 

 

 

下面的報錯是之前用另一種方式 vue add 什么什么的全自動做預渲染的,網上可以查查,打包后會出現這些坑,第二次直接按照以上的步驟就沒有下面的問題了

和其它人一樣,一直打包不成功,有錯誤:

 

 然后和大佬們的一樣安裝

npm i puppeteer    太慢
直接用cnpm i puppeteer   就行分分鍾下載好

之后再執行npm run build

還是上面的報錯:

 

 查一下這個報錯:

使用cnpm或者

npm config set puppeteer_download_host=https://npm.taobao.org/mirrors
npm i puppeteer

然后下載成功了,也不慢,

之后打包npm run build

打包成功,index.html中多了好多代碼,但是本地運行項目出問題了,后續接着看

搞了半天,感覺對於純動態的頁面,預渲染好像並無卵用。因為動態獲取的內容,預渲染是預渲染不出來的,還的看服務端渲染,。。。

這是一位大佬的文章,付費的哦:https://gitbook.cn/books/5d7f8b5b84257d2371a8babb/index.html

想了想,我掏錢了,我的把大佬的文章偷來,給大家免費看,嘻嘻:復制粘貼走起:

-------------------------------------------------------------------------------------------------------------------------------------------------------

前言

如果開發需要復雜交互的 Web 應用,我們多半會選擇 SPA;如果要做提供內容資訊的網站,更有利於 SEO、加載速度更快的服務器端渲染(Server-Side Rendering,SSR)自然是大家的首選;那么,如果是一個 CMS 生成純靜態網頁呢?

前陣子公司官網升級,我嘗試用 Webpack 多頁配置,成功的升級了工具鏈,收獲了比較理想的效果。我還寫了一篇 Chat 分享這期間的收獲:《升級工具鏈吧!使用 Webpack 開發企業官網》。實際運行一陣子之后,我發現一些新問題:這套技術鏈對於非前端開發者來說,還真算不上簡單,開發環境搭建、不同文件的功用,對后端同事來說仍然顯得很復雜。於是,有時候雖然只是小小的文案錯誤,也要找我來改;或者“加入我們”頁面里的崗位信息,也需要熟悉代碼的前端負責增刪。

於是我就想,能否把各頁面以 Vue 組件的形式搭建起來,每個組件可以有“編輯/顯示”兩個狀態,在本地啟動開發環境,在瀏覽器里“編輯”內容,用戶可以獲得所見即所得的體驗。最后以“顯示”狀態渲染成靜態頁,繼續使用純靜態的方式部署。這就相當於,基於原先的項目開發一個所見即所得 CMS,把原先的靜態 HTML(或 Pug)頁面改造成 Vue 頁面,然后利用 Vue 的 SSR 機制發布成純靜態 HTML。感覺上是個不錯的想法。

我以前只是聽說過 SSR,一直沒有實操經驗,有了這個想法之后,就想研究下這個過程,搞個最小用例,將來官網升級 3.0 的時候可以考慮。我本以為只是舉手之勞,最多 1,2 個小時就搞定了,沒想到最后折騰了大半天。所以我覺得,很有必要再寫一篇 Chat,分享這個過程中的收獲。

本次分享大綱如下:

  1. 網頁形態的歷史
  2. Vue CMS 的產品形態
  3. 了解 Nuxt.js
  4. 生成靜態頁的相關配置
  5. 添加 SEO 關鍵信息
  6. 注入專有 JS

面向讀者

  1. 初中級開發者,熟悉 Vue
  2. 希望了解前端工具鏈
  3. 希望了解靜態化和 Nuxt.js

名詞及約定

我假定所有讀者都是有一定經驗的開發者,大家至少都具備:

  1. 能讀懂 JavaScript
  2. 了解 Vue,使用過 Vue 開發項目
  3. 知道 Webpack,了解前端工具鏈中各工具的角色和基礎用法

其它約定:

  1. 為節省時間,范例代碼中的 HTML 會以 pug 書寫,這種語言很容易閱讀,文中也用不到高級語法,應該問題不大。另外,如果你還在寫原生 HTML 或 CSS,我建議你盡快切換到新語言。
  2. 范例代碼以 ES6+ 為基礎,如果你對這些“新”語法不熟悉,附錄里有一些資源方便你學習。

名詞:

  1. SEO:搜索引擎優化。指改進網頁,讓搜索引擎更容易理解它的內容,提高頁面的排名。
  2. CMS:發布系統。沒有特指的話,特指本文中發布靜態頁的工具。

文中代碼的目標環境:

  1. Vue >= 2.6
  2. Vue-router >= 3.1
  3. Nuxt.js >= 2.8

作者介紹

大家好,我叫翟路佳,花名“肉山”,這個名字跟 Dota 沒關系,從高中起伴隨我到現在。

我熱愛編程,喜歡學習,喜歡分享,從業十余年,投入的比較多,學習積累到的也比較多,對前端方方面面都有所了解,希望能與大家分享。

我興趣愛好比較廣泛,尤其喜歡旅游,歡迎大家相互交流。

我目前就職於 OpenResty Inc.,定居廣州。

你可以在這里找到我:

或者通過 郵件 聯系我。


限於個人能力、知識視野、文字技術不足,文中難免有疏漏差錯,如果你有任何疑問,歡迎在任何時間通過任何形式向我提出,我一定盡快答復修改。

再次感謝大家。


網頁形態的發展

在正式開始之前,先介紹一下網頁形態發展的歷史,方便大家理解。

遠古時期:純靜態

發明 HTML 的目的是方便大家看論文文獻,所以早期的 HTML 都是靜態的,放在服務器上,直接映射本地文件目錄結構。

當然那個時候也沒多少網頁,很多服務並沒有網頁版本,比如論壇,是以 Telnet 形式提供服務的;文件共享,大多使用 FTP。

古典時期:動態網站

所謂“動態網站”,就是根據用戶請求,返回合適的內容。或者換用技術的說法,接受請求之后,從數據庫中讀取數據,生成頁面,返回給用戶。其實現在沒什么網站不是動態網站了,感覺這是個上個世紀的詞,比如去京東上搜“動態網站”,能搜到一大堆書,大多有着深刻的時代印記,比如《ASP+Dreamweaver動態網站開發》,簡直辣眼睛。

文藝復興:偽靜態

相比於純靜態網站而言,動態網站通常需要更長的響應時間,而且 URL 也經常難以閱讀。比如:/post.php?id=157,沒人知道它是什么意思。這個時期的搜索引擎也比較弱,面對類似的網頁,會降低權重。所以網站運營方要想辦法改進 SEO,就用服務器 rewrite 的方式,把形似靜態頁的路徑指向動態路徑,提升搜索排名。

偽靜態常常和真靜態共同工作,這個階段 CDN 還不夠普及,所以生成靜態頁面並部署到終端服務器的操作也很常見。

近代:SPA

隨着各項技術的發展,瀏覽器在整個互聯網產品中的地位越來越重要,承擔的職責也越來越多,最終單頁應用(Single Page Application,簡稱 SPA)脫穎而出,成為最流行的產品形態。關於它的好處我就不一一敘說了,相信大家都了解。

而接下來,MVVM 框架橫空出世,一統江湖,更是大大提升了 SPA 的開發體驗,同時大大降低了入門門檻。然后,類似的產品如雨后春筍搬涌現。

現代:SPA + SSR

SEO 的問題在 SPA 這里更明顯:對於一些不思進取的搜索引擎爬蟲來說,SPA 應用里什么內容都沒有。而不思進取,是很多統治級公司、統治級產品的常態。所以沒辦法,它們不適配我們,我們就得去適配它們。然后,服務器端渲染(Server-Side Rendering,簡稱 SSR)就顯得非常必要。

SPA 的 SSR 和以前的偽靜態不同。偽靜態時期,業務邏輯都是通過后端完成的,前端主要用來收集數據;SPA 時期,大部分業務邏輯已經移到前端,后端只負責數據校驗和必要的存儲。此時 SSR 的目的是讓用戶盡快看到第一波需要的數據,接下來的操作仍然由 SPA 負責。所以我們就面臨兩種選擇:

  1. 前后端共用一套模板,渲染后的頁面擁有全部功能,可以繼續與用戶交互
  2. 部分頁面生成純靜態內容,可以部署在服務器上;其它重交互的部分保持 SPA,獨立工作

現代分支:在本地寫作的純靜態網站

隨着雲服務發展,現在很多網站都提供基礎的靜態網站托管服務,比如 GitHub Pages,我們可以使用一些工具在本地完成靜態頁面的批量創建工作,然后上傳到服務器,擁有自己的網站。

本文的工作實際應該算到這個分支。

Vue CMS 的產品形態

回顧完歷史,我們來看看業務需求。

看過我上一次 Chat 《升級工具鏈吧!使用 Webpack 開發企業官網》的同學應該知道,一切都源自我廠的官網要改版。

我廠官網 v1.0 采用的是前文中“遠古時期”的模式,由設計師設計、制作完網頁之后,直接把純靜態資源上傳到服務器,然后提供服務。這樣的好處是:

  1. 簡單好操作,對人員要求低
  2. 訪問速度快,對機器配置要求低
  3. 對搜索引擎友好

但是也有很多不足:

  1. 純靜態,不利於調整內容
  2. 不利於 i18n,沒有多語言版

這些問題,我在 v2.0 時進行了修正。首先,我引入 Gulp 做批處理,增加了“發布”環節,雖然最終還是部署純靜態內容,但是內容可以簡單調整,也通過 DOM 查找,實現了 i18n,同步提供中英兩個語言。

新版本上線一年半,效果不錯,但也有很多不足:

  1. 需要開發者手動管理所有資源,很累
  2. 發布腳本很多很復雜,不便於理解;Gulp 采用 stream 模式,沒搞過的新人很難接手
  3. 沒有好的開發環境

於是,當整個頁面內容都要更新時,我決定升級到 v3.0。這次使用 Webpack 工具鏈,采用多頁面模式,配合 Pug 的可編程特性,更好的實現了最終效果,還可以配合 webpack-dev-server 方便的在本地開發。新版本大大改善了開發效率。新同事打開項目看了看 Webpack 的配置文件,很快就搞明白它是怎么工作的,如果要做工作,應該從哪里入手。

不過正如《矛盾論》所說,當主要矛盾被解決,次要矛盾就會變成新的主要矛盾。此刻,新的問題出現:專業的前端開發能很快摸清項目結構,但對於后端同事來說,未知的新概念還是很多。教會他們理解所有框架、類庫、工具沒什么意義,如果能夠給他們一個簡單可操作的 UI,那才能真正提高效率。比如 npm run edit,然后瀏覽器就打開一個頁面。他們按需編輯后,保存,提交倉庫,發布。這樣的流程才是真正要追求的流程。

所以,最合適的做法就是用 Vue 實現一些編輯器組件,它們有兩種形態:編輯,靜態。編輯態就不用解釋了;我們只要把靜態的 HTML 保存下來,生成靜態頁,即可。

好的,現在需求已經出來了:

  1. 已有一個 Vue 項目
  2. 這個項目大部分功能由 SPA 提供,不需要靜態化,不需要預渲染
  3. 這個項目部分路由需要生成靜態頁
  4. 部署的時候,只需要部署靜態頁和其引用的資源

看到這個需求,我想大多數關注前端的同學可能跟我一樣,第一反應就是:應該找一個 SSR 框架,添加到現有項目中。那么,就選最出名的 Nuxt.js 吧,這個框架的作者還跟 Vue 的作者一起看 NBA 呢。

了解 Nuxt.js

Nuxt.js 的官方網站在此:https://nuxtjs.org,大家可以先看一下。有中文版。不過需要注意,我看的時候,中文翻譯並不全,而且不是一半中文一半英文的那種不全,是少了一大部分內容。后來通過 Google 搜索找到了需要的內容,我才發現中文翻譯有缺失,浪費我不少時間。所以我建議,大家盡量看英文,或者,先看一遍英文,再看中文。

言歸正傳,接下來,我們來了解下 Nuxt.js。

定位

相信大家都用 Vue 開發過不止一個項目,各種組件庫、工具庫也都用過不少。那么 Nuxt.js 在整個 Vue 生態里,大約是個什么定位呢?

讀罷官方文檔的介紹,我們明白,Nuxt.js,定位於在服務器端提供 UI 渲染的通用框架。它的應用層級高於 Vue,並不打算對 Vue 的功能進行補強,而是利用 Vue 的數據雙向綁定,提供更強的 B/S 架構中服務器(Server)一端的開發環境。換句話說,它就是 Node.js 版的 Laravel。而靜態頁發布,也就是 nuxt generate 功能,只是服務器端渲染衍生出的一個子功能。

坦率的說,這並不是一個好消息。看過前面產品設計的同學應該明白,我想要的,實際上是類似 Vue-Router 或者 Webpack 插件一樣的東西,我只要引用它,然后啟動一個開關,然后 npm run build,就能生成我需要到的靜態頁,還有靜態需要的各種靜態資源。

但是在服務器端實現 Vue 模板渲染太過復雜,為了這樣簡單的目的,開發 Nuxt.js 這種規模的工具,實在太不划算。所以無論是 Next.js 也好,Nuxt.js 也好,他們的團隊都渴望更有挑戰性更有成長空間的項目,比如挑戰 Laravel。(偷偷說一句,Nuxt.js 的網站上廣告真多……)

作為一般用戶,我只能去適應他們。

結構

Nuxt.js 里面集成了 Vue 全家桶和 Webpack 全家桶。好消息是這些組件我都常用,應該不會遭遇什么困難。Nuxt.js 團隊還提供了腳手架創建工具,方便初始化項目。不過我的項目是現成的,所以作用不大,這個部分跳過不看。

直接在項目根目錄安裝 Nuxt.js:npm i nuxt -D,完成之后可以使用 nuxt -h 查看 nuxt 命令的各項參數。nuxt 命令和 vue-cli-service 一樣,提供啟動開發環境、build 等功能。nuxt 使用 nuxt.config.js 作為配置文件,它的地位和 vue.config.js 一樣,包含了 Webpack 的默認配置,和一大堆私有配置。

所以,使用 Vue CLI 創建項目時,最好選擇把所有工具的配置分散在各自的配置文件里,比如 babel 就是 .babelrc,這樣復用起來更加方便。

Nuxt.js 有一套默認目錄結構,使用這套目錄結構可以最大限度的利用 Nuxt.js 的工作機制,不過對於我們來說,暫時排不上用場,也不需要關注。

nuxt generate

這就是最終我們要使用的命令,其實只能算作 Nuxt.js 的衍生功能。大家可以先看一下它的文檔:靜態應用部署以及generate 屬性配置,后者需要寫在 nuxt.config.js 里。

 

使用這個命令,會在指定的文件夾里生成預渲染文件。它的過程大概是這樣:

  1. nuxt 啟動一個本地服務
  2. 根據配置中的預渲染路徑,生成靜態頁面
  3. 把頁面保存成 .html 文件,其中引用的靜態資源也都保存下來
  4. 預渲染的頁面當中,包含完整的 JS 功能代碼

然后我們就可以把這些靜態資源整個部署到服務器上。

接下來,我們先來重構下老項目。

重構現有項目

理解了 Nuxt.js 的定位,就不難判斷,如果我們想集成 Nuxt.js 到現有項目,必須進行一些重構。

生命周期鈎子

服務器端渲染沒有掛載(amount)DOM 的過程,所以自然也不支持 beforeMount 和 mounted 鈎子。如果你跟我一樣,習慣把初始化代碼寫在 beforeMount 里,那么可能有不少地方要修改。所以,以后如果有對時機要求不嚴的操作,最好放在 created 甚至 beforeCreated 里。

如果頁面需要異步加載數據,就需要用 asyncData 函數。這個函數的用法和鈎子函數類似,它需要返回 Promise 實例,真正的模板渲染會在這個 Promise 完成之后才開始。

不過需要注意,正常來說,靜態頁面里會包含完整的 JS,即包含完整的頁面邏輯,所以鈎子函數在瀏覽器里會照常執行。所以要避免同樣的代碼在 asyncData 和鈎子函數里重復運行。

入口

一般來說,如果使用 Vue CLI 創建項目,入口文件是 src/main.js,這個文件會 importsrc/App.vue,並且 mount 到 #app 元素上。

我們當然可以繼續使用這個入口,不過考慮到靜態頁面的依賴跟 CMS 頁面的需求不同,我認為重新定義一個入口比較好。在本項目中,我把兩者都需要的依賴放在 src/App.vue 里,比如 Bootstrap 的樣式;只有 CMS 需要的依賴放在 src/main.js 里,比如 CMS 路由和 Vuex。然后在 nuxt.config.js 里重新配置路由,以 src/App.vue 作為入口。

網頁的頭信息,包括 <title>,關鍵詞(keywords)、描述(description)對 SEO 非常重要。即使不考慮 SEO,只是方便用戶使用瀏覽器前進后退,顯示正確的 <title> 也是應該的。在 Vue 項目中,我們使用 vue-meta 滿足這個需求(vue-meta 也是 Nuxt.js 團隊開發的)。

但是在 Nuxt.js 生成的靜態頁面里,我們需要使用 head 屬性。它的用法和 vue-meta 基本一致,等下我會在具體配置里詳細介紹。

生成靜態頁的相關配置

接下來講解配置項。其實配置本身可說的部分不多,之所以鋪墊這么久,正是因為我踩了很多坑。原本打算一兩個小時搞定的事情,最后花費5、6個小時才摸索出來。后來我想,如果這些知識點我以前就知道,那該多好。所以才有了前面的內容。

好吧,言歸正傳,最終的配置如下:

const path = require('path');
const {
  promises: {
    copyFile,
  },
} = require('fs');
const {DefinePlugin} = require('webpack');
const pkg = require('./package');
const {POST_TABLE} = require('./src/model/Post');

module.exports = {
  // 用來輸出 `<head>` 里面的信息
  head: {
    titleTemplate: '%s - Meathill',
    meta: [
      {charset: 'utf-8'},
      {name: 'viewport', content: 'width=device-width, initial-scale=1, user-scalable=no'},
    ],
  },

  // build 配置,其實就是封裝起來的 webpack 配置
  build: {
    extend(config) {
      config.resolve.alias['@'] = path.resolve(__dirname, 'src');
    },
    extractCSS: true,
    plugins: [
      new DefinePlugin({
        VERSION: JSON.stringify(pkg.version),
      }),
    ],
  },

  // 重新構建路由。因為靜態頁對 URL 的需求和 CMS 完全不同,所以這里我只針對靜態頁添加了簡單的路由設定。
  // 前文說過,這里我用 `src/App.vue` 作為入口
  router: {
    extendRoutes(routes, resolve) {
      routes.push({
        name: 'home',
        path: '/',
        component: resolve(__dirname, 'src/App.vue'),
        children: [
          {
            name: 'page.view',
            path: ':path',
            component: resolve(__dirname, 'src/modules/page/page.vue'),
          },
        ],
      });
    },
  },

  // 渲染配置,因為是純靜態頁,所以我選擇不注入業務邏輯(即 CMS 里的 JS)
  // 那么也就不需要 prefetch
  render: {
    injectScripts: false,
    resourceHints: false,
  },

  // `nuxt generate` 的配置,只有只作用於生成的配置才寫在這里,所以其實是遠遠不夠的
  generate: {
    dir: 'static',
    fallback: false,
    async routes() {
      const posts = await getAllPages();
      return result.map(item => `/${item.get('permanentLink')}`);
    },
  },

  // 鈎子函數,復制文件,后面會解釋
  hooks: {
    generate: {
      async done() {
        const from = path.resolve(__dirname, 'src/footer.js');
        const to = path.resolve(__dirname, 'static/footer.js');
        await copyFile(from, to);
        console.log('[CMS : Nuxt] Static files copied.');
      },
    },
  },
};

這里需要介紹一下 generate 屬性,它的詳細文檔在這里:Nuxt.js > Configuration > The generate Property。它里面的信息是專門為 nuxt generate 准備的,但是只配置這個屬性是不夠的。

它的 routes 屬性很重要,里面是所有要渲染的靜態頁。如我的例子所示,這里也可以使用異步函數,動態獲取要渲染的頁面列表,然后逐個渲染。Nuxt 還提供了一個優化方案,可以批量渲染頁面,而不需要每次都訪問數據源,不過我暫時沒有用到。

添加 SEO 關鍵信息

SEO,中文全稱“搜索引擎優化”,英文全稱 Search Engeine Optimization,簡寫即 SEO。SEO 雖然名為“搜索引擎優化”,但其實優化的並不是搜索引擎,而是我們自己的頁面。目的是提升我們的網頁在搜索引擎中的排名。

要知道,流量是很貴的,最便宜的也要好幾塊/人,那些消費能力強、消費欲望高的用戶,一個可能要賣300+。所以對於老板來說,如果能夠通過 SEO,讓我們的網頁排到更靠前的位置,引來更多的用戶,那么就等於省去了很大一筆費用。所以顯而易見,SEO 對他們來說吸引力很大。

對於我們前端來說,SEO 的需求不可避免,那么如何做呢?這需要我們對搜索引擎的原理有一些理解。我簡單介紹一下:

  1. 搜索引擎會抓取所有頁面
  2. 然后搜索引擎會分析每個頁面,對內容進行分詞,對每個詞進行打分
  3. 最終得到“所有網頁”里“所有內容”的評分
  4. 當用戶輸入搜索關鍵詞的時候,尋找評分最高的頁面倒序顯示

如果想了解更詳細的搜索引擎知識,我推薦大家閱讀吳軍博士的《數學之美》,雖然我覺得他的其它作品水平不佳,但這本書科學內容比較多,還是蠻值得看的。

換言之,SEO,就是要讓我們的網頁針對某個關鍵詞,得分更高。一般來說,有以下工作:

  1. <title> 里應該包含關鍵詞
  2. <meta name="keywords"> 里應該包含關鍵詞
  3. 頁面內的標題,即 <h1><h2> 等,應該包含關鍵詞
  4. 圖片等元素的 alt 屬性,應該包含關鍵詞
  5. 其它正文中,應保持一些關鍵詞密度,比如每一段,每一百個字等,都要出現至少一次關鍵詞

具體到網站生成工作中,(3)(4)(5)通常都由運營/編輯/內容團隊負責,我們真正能做的,就是(1)(2),也就是接下來的組件使用。

組件

首先,在頁面組件里,添加 head 屬性,用來返回頭信息。在本次項目中,它必須是個函數,根據頁面數據動態返回值:

export default {
  head() {
    if (!this.meta) {
      return;
    }
    return {
      title: this.meta.title,
      meta: [
        {
          vmid: 'keywords',
          name: 'keywords',
          content: this.meta.keywords,
        },
        {
          vmid: 'description',
          name: 'description',
          content: this.meta.description,
        },
      ],
      script: [
        {
          body: true,
          defer: true,
          src: 'https://unpkg.com/swiper@4.5.0/dist/js/swiper.min.js',
        },
        {
          body: true,
          defer: true,
          scr: '/footer.js',
        },
      ],
    };
  },
}

有些時候,我們會在 nuxt.config.js 里配置默認的 meta 信息,為了避免頁面的 meta 信息和默認 meta 信息重復出現,所以要用到 vmid(在組件里) 和 hid(在配置里)。這樣同樣 id 的頭信息就只出現一個,權重當然是頁面更高。

接下來 script 的部分,可以通過 body 屬性控制 <script> 插入的位置,默認為 false,插入 <head>。這里當然應該放在 </body> 之前。靜態網頁不需要 Vue 那些很復雜的交互,所以在上一章中,我通過 render 屬性把它們去掉了。但是有一些其它交互要添加進來,比如頭圖切換用 swiper,還有統計代碼。所以要插入一個 footer.js 進去。

這里需要注意,Nuxt.js 並不會調用 webpack 去處理這里的 JS,所以我們需要人工控制它們的路徑。下一章你會看到,我是直接復制文件到 static 文件夾的,所以它的路徑也就寫成固定的 /footer.js。如果你有 publicPath 之類的需求,還要自己處理一下哦。

配置文件

配置文件里的內容上一章展示過:

module.exports = {
  head: {
    titleTemplate: '%s - Meathill',
    meta: [
      {charset: 'utf-8'},
      {name: 'viewport', content: 'width=device-width, initial-scale=1, user-scalable=no'},
    ],
  },
}

好像沒什么可說的……我暫時只用到標題模板,比如一個頁面標題是“今天晚上吃什么?”,就會渲染成:“今天晚上吃什么? - Meathill”。其它選項大家可以參考 Vue Meta > API > metaInfo properties

渲染靜態頁的時候,vue-meta 似乎不是必須的;換言之,我一開始用了 vue-meta,沒有配 head,也沒有輸出需要的 meta 信息。

利用鈎子注入 JS

Nuxt.js 提供了很多鈎子,方便我們在特定的環節進行定制化操作。這些鈎子跟不同的操作綁定,接受不同的參數,返回不同的結果,具體鈎子列表請參考 Nuxt.js > API > Hooks > List of hooks

在本項目中,我的需求是生成靜態頁面需要的 JS。這個 JS 包含兩個功能:

  1. 初始化 swiper
  2. 啟動統計代碼

因為功能非常簡單,我暫時不打算用 webpack 打包,只想復制到目標文件夾里。啟動復制操作的時機並不敏感,因為和主要的生成靜態頁工作不存在相互依賴的關系。不過 Nuxt.js 默認會清理生成目錄,所以我覺得晚一些復制會比較好,最后選擇 generate:done 這個鈎子(基本是最后才會執行)。代碼如下:

const {
  promises: {
    copyFile,
  },
} = require('fs');

module.exports = {
  hooks: {
    generate: {
      async done() {
        const from = path.resolve(__dirname, 'src/footer.js');
        const to = path.resolve(__dirname, 'static/footer.js');
        await copyFile(from, to);
        console.log('[CMS : Nuxt] Static files copied.');
      },
    },
  },
}

這段代碼應該很容易看懂吧,就是一個簡單的復制。不過需要注意,copyFile 函數是 v10 之后添加到 Node.js 里的,而預 promise 化的 fs.promises 是 v12 之后添加的。

后記 & 附錄

先回顧一下本文的主要觀點:

  1. Nuxt.js 的目標是覆蓋完整的網站開發場景,這個場景更有前(錢)途
  2. 要達成這個目標,支持 Vue 模板的服務器渲染是必經之路
  3. 所以生成靜態頁只是 Nuxt.js 的一個衍生功能
  4. 所以在現有項目中集成 Nuxt.js ,渲染靜態頁的成本比較高,很多文章也提供類似的觀點
  5. 但是如果有必要,這仍然是最好操作的方案

接下來,關於技術選型:

  1. 如果是新項目,必須 SSR,那么建議從開始就用 Nuxt.js 創建項目
  2. 如果是老項目,部分頁面需要靜態化,請參考本文

最后,如果要在現有項目中解成 Nuxt.js,我們應該:

  1. 重構現有項目,一般要重構入口和路由
  2. 新建 nuxt.config.js,添加基礎配置
  3. 配置 generate 屬性,生成所有要靜態化的路徑
  4. 如果不需要復雜的交互,可以用 render 屬性移除老的 JS,然后手動添加其它的。

希望可以幫大家節省學習嘗試踩坑的時間。

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM