React服務端渲染SSR,加快首屏渲染

前言

祝願各位過年回家的單身攻城獅相親成功,已脫單的找機會~~~~css

服務端渲染聽起來高大上,其實也就那麼回事,若是網站不是用於商業用途,也不須要被網站收錄,那就仍是乖乖用正常普通的方式寫寫就完事了,除非本身想裝逼一下,那能夠玩一下。如下就是個人裝逼時間了~~🙃html

項目地址: github.com/chenjiaobin…前端

爲何使用SSR

可參考https://www.jdon.com/50088node

上文中描述的客戶端渲染和服務端渲染,實際上對應了兩種Web構建模式:先後分離模式和直出模式react

  1. 模式一:先後分離模式(對應客戶端渲染)

2. 模式二:直出模式(對應服務端渲染)

不管是客戶端渲染,服務端渲染,它們都包含三個主體過程:webpack

  • 下載JS/CSS代碼
  • 請求數據
  • 渲染頁面

客戶端渲染:a -> b ->c (a,b,c都在客戶端進行)git

服務端渲染:b -> c ->a (b,c在服務端進行,最後的a在客戶端進行)github

服務端渲染改變了a,b,c三個過程的執行順序和執行方web

技術棧

  • react、react-dom、redux等(前端)
  • express(Node服務端)
  • webpack(先後端打包工具)

客戶端代碼

由於瀏覽器對於一些新的JS用法和react的語法糖沒法識別,所以咱們須要安裝一下webpack包來對源代碼進行打包處理,以保證代碼能在瀏覽器運行,具體包的做用就不細講,主要貼了幾個比較重要的包,細看請前往express-react-ssrexpress

// webpack主要的打包依賴
cnpm i webpack webpack-cli webpack-merge html-webpack-plugin autoprefixer -D
// 安裝babel主要用於編輯ES6和jsx語法,轉換代碼的做用
cnpm i @babel/cli @babel/core @babel/preset-env @babel/preset-react babel-loader -D
// 主要用於打包css
cnpm i css-loader style-loader postcss-loader -D
複製代碼

步驟

執行npm init建立一個帶項目信息的package.json,並新建build、client和server文件夾,build主要存放webpack打包配置,client存放客戶端文件,即前端react頁面文件,server則存放node服務端代碼,主要用於服務端渲染

  1. 新建文件webpack-client-config.js,客戶端文件打包配置,這裏我一次性給出我項目的配置,詳細說明看裏面的備註,具體一些關鍵配置後面會說明
// 用於合併webpack的配置
const merge = require('webpack-merge')
// 用戶導出html文件
const HTMLplugin = require('html-webpack-plugin')
const { resolvePath } = require('./webpack-util')
// webpack公共配置文件,主要用於服務端打包和客戶端打包的公用配置
const baseConfig = require('./webpack-base')
// 分離CSS爲單獨的問題
var ExtractTextPlugin = require("extract-text-webpack-plugin")

module.exports = merge(baseConfig, {
  // 用於調試
  devtool: 'inline-source-map',
  mode: 'development',
  entry: {
    app: resolvePath('../client/client.js')
  },
  output: {
    filename: 'js/[name].[hash].js',
    path: resolvePath('../dist'),
    // 服務端的publicPath要跟這裏的一致,做用是在最後打包出來的靜態資源路徑都是在/public下,這裏主要的做用是由於服務端渲染時,打包後的js文件也返回了html文件,因此須要經過設置一個靜態資源文件的路徑來區分
    publicPath: '/public'
  },
  devServer: {
    port: 8060,
    contentBase: '../client', //src文件夾裏面的內容改變就會從新打包
    // 路由使用history,所以有個問題就是,一步路由沒有緩存在頁面中,第一次進入頁面會找不到,
    // 所以在開發環境能夠配置historyApiFallback恢復正常
  	historyApiFallback: true,
  	hot: true,
  	inline: true
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        exclude: [/node_modules/],
        use: ExtractTextPlugin.extract({
          fallback: 'style-loader',
          use: [
            { loader: 'css-loader', 
              // https://stackoverflow.com/questions/57899750/error-while-configuring-css-modules-with-webpack
              // Syntax of css-loader options has changed in version 3.0.0. localIdentName was moved under modules //option. 意思大概是css-loader3.0.0版本的localIndentName屬性被移除了
              // 所以不能寫成 options: { modules: true, importLoaders: 1, localIdentName: '[name]___[hash:base64:5]' }
              // 只能寫成如下方式
              options: { modules: { localIdentName: '[name]___[hash:base64:5]' }, importLoaders: 1 } 
            },
            'postcss-loader'
          ]
        })
      }
    ]
  },
  plugins: [
    new HTMLplugin({
      filename: 'index.html',
      template: resolvePath("../template.html")
    }),
    // 注意:若是打包的時候報錯,那從新npm install –save-dev extract-text-webpack-plugin@next安裝一下,由於webpack版本較高,因此老版本的extract-text-webpack-plugin有問題
    new ExtractTextPlugin('./css/[name]-[hash:8].css')
  ]
})
複製代碼

基礎webpack-config-base.js就去看項目就行了哈😁沒什麼特別

注:這裏主要的坑就是ExtractTextPlugin的使用,即在配置css-loader的時候若是跟不使用它的時候同樣,那可能會出現問題,緣由是版本css-loader 3.0.0版本的時候移除了localIdentName(做用:自定義樣式打包規則)屬性

錯誤配置

options: { modules: true, importLoaders: 1, localIdentName: '[name]___[hash:base64:5]'
複製代碼

正確配置

options: { modules: { localIdentName: '[name]___[hash:base64:5]' }, importLoaders: 1 }
複製代碼
  1. 新建webpack-util.js,webpack基礎工具配置
// 獲取文件路徑
const path = require('path')
exports.resolvePath = (filePath) => path.join(__dirname, filePath)
複製代碼
  1. webpack配置完還沒完事,須要根目錄建立一個.babelrc文件
{
  "presets": ["@babel/preset-react"]
}
複製代碼
  1. 開始寫react文件,在client文件夾下面建立一個客戶端打包入口文件client.js和app.js文件
// app.js
export default class App extends Component {
  render () {
    return ( 
        <div>
          <p>服務端渲染測試</p>
        </div>    
    )
  }
}

// client.js
export class Home extends React.Component {
  render () {
    return (
          <App/>
      </Provider>
    )
  }
}
ReactDom.render(<Home/>, document.getElementById('app'))
複製代碼
  1. 目前基本就能夠正常打包webpack --config build/webpack-client-config.js,打包正常你會在根目錄生成了一個dist目錄,不正常的話本身再調試調試把,或者拉個人項目去看下,傳送門
  2. 最後服務端渲染的時候要把render換成hydrate,兩個的主要區別以下

ReactDom.render()會將後端返回的dom節點全部子節點所有清除,再從新生成子節點。而ReactDom.hydrate()則會複用dom節點的子節點,將其與virtualDom關聯

可見,第一種方式明顯是作了重複工,影響效率,所以,react16版本也放棄了用render,也可能將會在react17版本中不能用ReactDOM.render()去混合服務端渲染出來的標籤

服務端代碼

  1. 安裝Node相關依賴,這裏主要安裝了express
  2. 再server目錄建立index.js執行文件
import Express from 'express'
import path from 'path'
import { renderToString } from 'react-dom/server'
import fs from 'fs'
import React from 'react'
import { StaticRouter } from 'react-router-dom'
// const App = require('../dist/server').default
import App from '../client/app'
import { Provider } from 'react-redux'
import createStore  from '../client/redux/store'

const server = Express()
// 靜態資源路徑
server.use('/public', Express.static(path.join(__dirname, "../dist")))

// 這個函數主要用於匹配模板文件的{{}}標籤的內容,替換成咱們後端給出的數據
function templating(props) {
  const template = fs.readFileSync(path.join(__dirname, '../dist/index.html'), 'utf-8');
  return template.replace(/{{([\s\S]*?)}}/g, (_, key) => props[ key.trim() ]);
}

server.use('/', (req, res) => {
  const store = createStore({
    list: {
      list: ['關羽', '張飛', '趙雲']
    },
    home: {
      title: '我是小菜雞,請賜教'
    }
  })
  // 核心代碼
  const html = renderToString(
    <Provider store={ store }>
    //若是咱們頁面上使用到了路由那就須要這個來包含
      <StaticRouter location={req.url}>
        <App/>
      </StaticRouter>
    </Provider>
  )
  res.send(templating({html,store: JSON.stringify(store.getState())}))
})
server.listen('8888', () => {
  console.log('server is started, port is 8888')
})
複製代碼

關鍵點:

  • StaticRouter和BrowserRouter的主要區別就是,在瀏覽器上咱們可使用js獲取到location,可是在node環境卻獲取不到,因此react-router提供了StaticRouter來讓咱們本身設置location
  • 在開始講以前我仍是得先和你們說說傳統的spa頁面路由是怎麼配置的,下面就以history模式爲例

首先咱們從瀏覽器輸入url,無論你的url是匹配的哪一個路由,後端通通都給你index.html,而後加載js匹配對應的路由組件,渲染對應的路由。

那咱們的ssr路由是怎麼樣的模式呢?

首先咱們從瀏覽器輸入url,後端匹配對應的路由獲取到對應的路由組件,獲取對應的數據填充路由組件,將組件轉成html返回給瀏覽器,瀏覽器直接渲染。當這個時候若是你在頁面中點擊跳轉,咱們依舊仍是不會發送請求,由js匹配對應的路由渲染

  1. 服務端打包,再build文件夾新建webpack-server-pro.js,配置以下
const merge = require('webpack-merge')
const { resolvePath } = require('./webpack-util')
const baseConfig = require('./webpack-base')
// 打包忽略重複文件,挺重要的,以前打包沒加報了Critical dependency: the request of a dependency is an // //expression的警告,不將node_modules裏面的包打進去
const nodeExternals = require('webpack-node-externals')

// 打包server node的配置

module.exports = merge(baseConfig, {
  mode: 'production',
  // 表示是node環境,必須加
  target: 'node',
  node: {
    // 使用__filename變量獲取當前模塊文件的帶有完整絕對路徑的文件名
    __filename: true,
    // 使用__dirname變量得到當前文件所在目錄的完整目錄名
    __dirname: true
  },
  context: resolvePath('..'),
  entry: {
    app: resolvePath('../server/index.js')
  },
  output: {
    filename: '[name].js',
    path: resolvePath('../dist/server'),
    // 必須跟客戶端的路徑同樣
    publicPath: '/public' 
  },
  module: {
    rules: [
      {
        test: /\.css?$/,
        use: ['isomorphic-style-loader', {
         loader: 'css-loader',
         options: {
          importLoaders: 1,
          modules: {
            localIdentName: '[name]___[hash:base64:5]'
          },
         }
        }]
       }
    ]
  },
  
  externals: [
    nodeExternals()
  ],
})
複製代碼

注:isomorphic-style-loader主要是用於解決css在服務端打包時候的問題,由於app.js文件在服務端引進去,app.js裏面有css的文件應用,那麼這個時候打包就會出現document is not defined的錯誤,所以咱們就須要解決css文件在服務端的問題,此時咱們只須要提取css樣式名字到標籤上就好了,不須要額外打包出css文件,那麼isomorphic-style-loader這個包就派上用場了,配置如上。localIdentName自定義配置要和客戶端的一致 4. 這個時候基本就已經完成服務端打包了,執行webpack --config build/webpack-server-pro.js,會在dist下生成一個server文件夾,且文件夾裏面有個app.js文件,這個文件就是node的啓動文件,咱們經過終端進入文件夾,而後執行node app.js這樣就能夠啓動咱們的node服務了,前提是咱們按前面的客戶端打包步驟打包好前端代碼,那麼咱們就能夠經過在瀏覽器訪問localhost:8888訪問到後端渲染的頁面了

打包部署

  1. git clone github.com/chenjiaobin…
  2. npm install 安裝依賴
  3. npm run build 打包客戶端
  4. npm run server-pro-build 打包服務端
  5. 進入第四步打包好的server目錄,啓動node服務node app.js(線上仍是使用pm2吧,自行查閱)
  6. 訪問localhost:8888

✔🤣敬上,項目地址 github.com/chenjiaobin…

參考

相關文章
相關標籤/搜索