如何加強網站的SEO - 同構篇
在上一篇了解為什麼很多開發者希望把CSR做到如SSR的效果,這篇就來介紹如何做到Isomorphic的效果。
以下將會介紹如何讓SPA程式做到類似SSR的效果。
補充:不管你是開發Vue或是React,Server端的程式都是Node.js,因此開發任何框架的開發者都可以觀看這篇參考。
SEO全系列文章
📌 如何加強網站的SEO - 基礎篇
📌 如何加強網站的SEO - 進階篇
📌 如何加強網站的SEO - SSR與CSR篇
📌 如何加強網站的SEO - 同構篇
📌 如何加強網站的SEO - 框架篇
📌 如何加強網站的SEO - 資安篇
認識Isomorphic
Isomorphic的翻譯為同構,你可以理解成,建構出相同程式的概念,有些人又稱它叫做pre-render。
一般傳統的SSR會把所有的程式產生出完整的HTML再丟到Client,但是同構的方式跟SSR不一樣。
同構所做的事情是,他只會把產生出部分的HTML給Client,剩下半殘的未產生的HTML繼續交給Client產生。
關於調整
以下的調整將會分F2E與Node(中間層或API server)的部分改寫
調整的部分其實比較麻煩的不會是程式碼的撰寫,反倒是相依套件的管理比較麻煩
尤其是使用越多套件的專案,越需要花時間盤點和修改
前端調整
React渲染
原本渲染方式為ReactDOM.render,需要改成ReactDOM.hydrate。
簡單來說,render是舊版的渲染方式,早期的同構處理會比較麻煩。
但是React 16之後,官方希望能夠在開發的時候,Client和Server能夠渲染一致,早期會自動fix,但是這應該是由開發者自行發現的,而不應該丟給程式自己去校正,反而會消耗額外的效能。
React-router-config
為了方便讓Node讀取Route,因此建議將Routes包起來以方便使用
這邊推薦使用React-router-config
使用方式其實很簡單,只需要精準地填寫path和component即可
1 | import Home from './containers/home'; |
在需要的路徑,我們需要加入getInitialData供後續同構執行的action做使用
這邊先打一下草稿,後續整合再加入
1 | import Home from './containers/home'; |
解決React-router-config需要擴充資源的問題
如果你希望讓Route加入其他參數,或是想要套用到現有的頁面,卻被State綁死了
這時候你可以直接把需要的路徑map出來擴充
此外,getInitialData也在這邊一起加入,如此可以避免掉,日後要在指定的頁面執行同構渲染的時候,需要改寫很多components的狀況
1 | class Layout extends React.Component { |
包裝一個SEO component
這邊推薦你把SEO包裝成一個component,即可方便管理指定的頁面需要什麼樣的meta tags。
這邊使用Helmet方便管理meta tags,並且保護資料不被惡意注入。
以下是範例:
1 | import React, { Component } from 'react'; |
Store準備
記得前端包裝好store,以方便後續供Node同步使用
1 | import { createStore, applyMiddleware } from 'redux'; |
Node調整
通常開發的時候,主要的難點都是在以下的部分:
- 看懂ES6+語法
- 由於Node是commonjs的語法,有些語法和JavaScript的版本有差異,因此需要透過套件讓Node看懂Client的程式。
- 需使用webpack確保能執行 import/es6/es7/jsx…
- 在Node編譯過一次前端的程式
- 由於需要跑遍所有的Routes,因此需要考慮使用React-router-config
- 確保Redux可正常運作
- 其實只需要使用前端寫好的store即可
- history
- React有history可以提供路徑的使用,要在Node模擬可以參考使用staticRouter。
安裝相依套件
以下將以React為例,但是套件只要把React的相關套件拿掉就行了。
安裝@babel
- 念法:外國人幾乎都唸:杯bo,但是台灣人喜歡唸:把貝喔
- 請注意,這裡示範的版本是7.0以上,只要旁邊有@的都是7以上
- 我這邊列出開發React有用到的babel套件,詳細請參考套件開發者的文件介紹
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19"dependencies": {
"@babel/polyfill": "^7.8.3",
"@babel/runtime": "^7.10.5",
},
"devDependencies": {
"@babel/core": "^7.0.0",
"@babel/plugin-proposal-class-properties": "^7.0.0",
"@babel/plugin-proposal-object-rest-spread": "^7.0.0",
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/plugin-transform-runtime": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@babel/preset-react": "^7.0.0",
"babel-core": "^7.0.0-bridge.0",
"babel-eslint": "^9.0.0",
"babel-jest": "^23.4.2",
"babel-loader": "^8.0.0",
"babel-plugin-import": "^1.6.7",
"babel-preset-react-optimize": "^1.0.1",
}
根目錄加入babel.config.js
舊版的babel是使用.babelrc,新版本的統一命名都是babel.config.js1
2
3
4
5
6
7
8
9
10
11
12module.exports = () => {
return {
presets: [
'@babel/preset-env',
'@babel/preset-react'
],
plugins: [
'@babel/plugin-transform-runtime',
'@babel/plugin-proposal-class-properties',
]
}
}根目錄加入.eslintrc(非必要 optional)
加入eslint可以更嚴格把關你的code1
2
3
4
5
6
7
8
9{
"extends": "airbnb",
"rules": {
"react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
},
"env" :{
"browser": true,
}
}加入webpack.config.js
請注意套件的版本!之前開發一直被套件版本搞混,這邊可能會花你最多時間研究
例如舊版的這樣就行:1
const CleanWebpackPlugin = require('clean-webpack-plugin');
但是新版的要這樣使用:
1
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
以下是範例:
1 | const path = require('path'); |
相信你看到需要裝這麼多相依套件有點頭暈了吧?
但是很多主流的工具,例如:Sass,都是必須經過轉譯才能讓Node解讀的。
如果有對套件本身不理解的部分,可以去查看官方文件。
在server的進入點加入渲染
這裡展示Node的渲染進入點,渲染的方式需要模擬啟動前端(React)所需要執行的步驟。
這裡大概展示了基本的骨架,另外也引入了前端寫好的一些元件
例如:store, routes……
1 | import nodeFetch from 'node-fetch'; |
處理路徑與Store
再來我們會在裡面添加之前在前端寫好的store
下面這個路徑會在每次切換路徑的時候執行
因此需要讓在此判斷網址的路徑為何,再跑指定的Route
這裡解析路徑的方式直接調用request的參數即可
1 | app.get('/*', (req, res) => { |
遍歷前端的Routes
這邊使用react-router-config提供的matchRoutes搭配剛剛取得的指定路徑來走訪component
補充:網路上大部分的寫法都是直接把loadData寫在每個components裡面,但是為了保持每次專案開發的整潔度
不要動到太多component以利之後管理git追蹤歷史紀錄,因此建議把loadData固定寫在Routes裡面即可
相較其他人寫的版本,這裡算是優化過的寫法:
1 | app.get('/*', (req, res) => { |
開始在Node渲染HTML
將要指定路徑要回傳的DOM包裝成promise之後,再來就可以渲染成HTML了
下面就是大家熟悉的進入點-root,將要渲染的HTML放在裡面,就可以完成伺服器產生HTML的過程了。
1 | app.get('/*', (req, res) => { |
不過這樣還不是完美的同構程式,你會發現前端在渲染的時候會有initial State,但是Node這邊卻沒有
因此需要在後面加上initial State讓Node取得初始資料。
使用Helmet
另外為了方便管理和同步meta tags,這邊會使用Helmet來inject前端所用到的meta tags。
同時也會使用Helmet來保護HTML的tags不被注入惡意程式。
1 | app.get('/*', (req, res) => { |
注入tags
這邊主要展示React如何注入初始化資料,還有其他你想加入的tags。
1 | const injectHTML = (data, { |
過濾編碼
這是我額外Bonus給讀者的,如果你發現從Node產生出來的HTML會把一些特殊符號變成其他編碼方式
擔心會影響SEO的話,我這邊展示一個方式,搭配正規運算式可以修正HTML tags。
1 | function decodeEntity(htmlTags) { |
開始整合前端與Node
Node和前端都準備好之後,再來就是一起整合了,讓我們回到前端的routes
getInitialData需要放入actions來初始化資料。
網路上有很多人的做法是,把很多actions都擠在裡面
不過這邊建議讀者可以直接另外寫一個SSR的action方便管理
因此這邊使用到的initialUserPage是另外寫的一個action
1 | import Home from './containers/home'; |
在這裡你可能會疑惑,為什麼要從Node引入store,主要是為了確保資料一開始產生的時候,就產生在store
如果產生好store,後面再呼叫action,其實是無法達到資料初始化的效果,也因此就無法同構了
所以,這邊不建議直接在這個action裡面寫dispatch,應該使用store的dispatch
1 | import { loadUser, loadArea } from './common'; |
總結
這樣大體上就完成了SPA的同構渲染了!
有沒有覺得挺繁瑣的呢?主要是因為,需要考量到套件, 前端, Node等…
太多相依功能了,所以開發起來很不容易