React SSR重構踩坑記錄(持續更新)

Cryptolalia發表於2019-03-01

最近將以前的一個畢業設計的網站的文章詳情頁做了服務端渲染的重構,看SSR的實現文件看似很簡單,但是實現起來確實坑不少。

無法使用import引入

  1. 錯誤資訊: unexpected token import
  2. 場景:第一次在node中直接使用import Story from `../js/containers/story`;就會報這個錯誤。
  3. 錯誤說明:node本身使用的是commonjs的語法,支援的模組引入和匯出方式為require以及module.export,然而es6定義的js模組方式為importexport[default],因此node雖然支援了大部分的es6語法,但是由於es6的模組與node本身的cjs的模組產生了衝突,因此node不會支援esm的模組,因此造成了無法識別import的情況。
  4. 解決辦法:以前整個專案中node和react使用同一個.babelrc檔案,為了解決這個問題,同時也由於reactnode的差異越來越大,最後決定拆分.babelrc檔案,在/node(後端)目錄以及/public(前端)目錄下分別建立.babelrc檔案,作為前端和後端各自的babel配置。 其中node的.babelrc檔案配置中的:
// .babelrc
"presets": [
    [
        "env",
	{
	    "targets": {
		"node": "current"
	    }
	}
    ],
"react",
"es2015",
"stage-0"
]
複製程式碼

可以讓node識別es6的語法。 然後在根目錄重新建立nodemon.json檔案用來處理import問題。 上網查資料,babel-node外掛可以解決不識別import的問題。

$npm i babel-cli --save

然後改寫nodemon.json檔案:

// nodemon.json
{
  "verbose": false,
  "env": {
    "NODE_ENV": "development",
    "BABEL_ENV": "node"
  },
  "watch": ["node", "config"],
  "ignore": ["public"],
  "execMap":{
    "js": "babel-node"
  }
}
複製程式碼

然後再次啟動node伺服器就可以發現node正常識別import了。

無法訪問window物件

  1. 錯誤資訊:window is not defined
  2. 場景:js檔案中一開始使用了很多window.xxx的屬性,import到node環境中之後就會報這個錯誤。
  3. 說明:服務端缺乏BOM和DOM環境,服務端下無法訪問window,navigator等物件。
  4. 解決辦法:針對此種錯誤,有三種解決辦法:
    1. 通過fake window等物件(如window等庫)的使用,給node環境建立全域性window物件。
    2. 前端元件中延遲這些物件的呼叫,在didMount中才進行呼叫。
    3. 將元件中的所有用window的屬性,都通過props的方式獲取,然後將所有應該傳入元件的props屬性在node中傳進元件。
    // storyController.js
const props = {
  userInfo: ctx.session,
  articleInfo: {
    author: author[0].nickname,
    avatar: author[0].avatar,
    author_fans_count: fans_count[0].count,
    ...info[0]
  },
  isSelf: info[0].uid === ctx.session.uid
};
const html = renderToString(<Story {...props} />);
ctx.render(`story`, {
  __PROPS__: JSON.stringify(props),
  title: info[0].title,
  html
});
複製程式碼
// pages/story.js
render(
  <Story {...window.__PROPS__} />,
  document.getElementById(`root`)
);
複製程式碼

這樣在元件中就可以通過props的方式獲取資料,從而解決這個問題。

無法訪問alias路徑

  1. 錯誤資訊: cannot find module `components/xxx`
  2. 場景:在元件中使用了webpack配置的alias路徑,做ssr時node就會報這個錯誤。
  3. 錯誤說明:當在webpack中配置alias時,我們可以在元件中簡寫路徑,但是在node中無法識別webpack的alias,所以這種路徑node會從node_modules中尋找這個元件,找不到就會報錯。
  4. 解決辦法:解決辦法自然是node也找一個alias的庫:module-resolver庫可以完美解決這個問題。 $ npm install --save-dev babel-plugin-module-resolver 安裝完成之後,改造一下node下面的.babelrc檔案即可:
// .babelrc
"plugins": [
	["module-resolver", {
		"cwd": "babelrc",
		"root": ["../public/js"],
		"alias": {
			"scss": "../public/scss",
			"components": "../public/js/components",
			"containers": "../public/js/containers",
			"constants": "../public/js/constants",
			"lib": "../public/js/lib",
			"router": "../public/js/router",
			"stirngs": "../public/js/string.js",
			"store": "../public/js/store"
		}
	}]
]
複製程式碼

其中的alias和webpack中的alias一樣。

無法引入靜態資源

  1. 錯誤資訊: /Users/xxx/xxx/node_modules/antd/lib/style/index.css:6
  2. 場景:元件中使用了antd元件,或者引入了我們自己的scss檔案時,會報這個錯誤。
  3. 錯誤說明:客戶端通常使用webpack進行編譯,資源的載入通過各種loader進行處理,但這寫loader只是針對於客戶端環境的,編譯生成的程式碼,無法應用於服務端,因此node無法解析scssless等檔案。
  4. 解決辦法:我們只要讓node不解析這些樣式檔案即可。 在node入口檔案app.js中最上方加入以下程式碼:
// app.js
require.extensions[`.scss`] = function() {
  return null;
};
require.extensions[`.css`] = function() {
  return null;
};
require.extensions[`.less`] = function() {
  return null;
};
require.extensions[`.png`] = function(module, file) {
  return module._compile(`module.exports = ""`, file);
};
require.extensions[`.svg`] = function() {
  return null;
};
複製程式碼

這樣node就可以正常執行了,但是同時又暴露出了一個問題,當node進行首屏渲染的時候,是沒有樣式的,這就導致當客戶端開始載入樣式之後,會造成頁面樣式抖動的問題。

為此我們通過編寫webpack外掛,將ExtractTextPlugin生成的css檔案,內聯插入頁面的pug模板中,這樣服務端首屏渲染就可以支援樣式了。

require方式引入元件報錯

  1. 錯誤資訊: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object
  2. 場景:node中使用require引入元件,然後傳入renderToString會報錯。
  3. 錯誤說明:這個錯誤涉及到esm和cjs互動的問題,我們通過require引入的東西和我們通過import引入的並不一樣,具體原因可以參考另一篇文章深入解析ES Module
  4. 解決辦法:
    1. 我們在元件中通過export default class xxx extends Component的方式匯出元件,在node中必須要通過const Component = require(`....`).default的方式才能夠正確獲取到元件,大家可以自己console.log一下,直接require進來的是一個object,裡面的default屬性才是我們的元件。
    2. 安裝babel-plugin-add-module-exports外掛。 $ npm install babel-plugin-add-module-exports@next --save-dev 然後改寫react中的.babelrc檔案:
// .babelrc
"plugins": [
    ...
    "babel-plugin-add-module-exports"
]
複製程式碼

這是個比較hack的方法,強行將esm和cjs的表現置為相同,但是可能會出現問題,所以儘量不要將esm和cjs混用,在node中直接使用import引入元件最好,不要用require引入。

(待續)

相關文章