這次我要上SSR

Seattle、發表於2019-04-21

專案原始碼

拉了就跑

  1. yarn
  2. npm run ssr

Gif預覽效果

SSR:服務端渲染,在服務端將首屏的html直接返回給客戶端

  1. SEO:讓各爬蟲能爬到首屏的內容,匹配到搜尋關鍵字,提高在百度谷歌的排序
  2. 加快首屏速度,不再是載入完js然後ReactDom.render('#app', Com),讓伺服器分擔部分渲染的壓力
  3. 詳細看參考文件

精簡易懂的內容來了

  1. SSR怎麼實現首屏渲染
  2. 結合Router
  3. 結合Redux
  4. 怎麼按需載入
  5. 服務端怎麼處理CSS和圖片

SSR怎麼實現首屏渲染

  1. 通過express服務來返回給瀏覽器首屏的HTML
  2. HTML是由react-dom/server提供的renderToString來生成,理論依據是node雖然不能識別HTML和JSX,但React能生成虛擬DOM,然後插入HTML直接返回給瀏覽器識別渲染
import { renderToString } from "react-dom/server";
import { StaticRouter } from "react-router-dom";
const app = express();
app.get( "/*", (req, res) => {
  const context = {}; // 上下文物件,可以儲存有關渲染的資訊
  const jsx = (
    <StaticRouter context={ context } location={ req.url }>
      <App />
    </StaticRouter>
  );
  const reactDom = renderToString( jsx ); // 在服務端將JSX渲染為HTML
  res.writeHead( 200, { "Content-Type": "text/html" } );
  res.end(`
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="UTF-8">
      <link rel="icon" href="data:;base64,=">
      <title>React SSR</title>
    </head>
    <body>
      <div id="app">${ reactDom }</div>
      <script src="/index.js"></script> // 注意這裡,服務端返回的HTMl會去載入webpack打包好的SPA應用,然後客戶端接管應用,繫結事件,處理路由跳轉和其他Ajax請求
    </body>
    </html>
  `);
});
app.listen(3000, () => {
  console.log('Running on http://localhost:3000/');
});
複製程式碼
const jsx = (
    <Provider store={store}>
      <Router>
        <Layout />
      </Router>
    </Provider>
);
const app = document.getElementById( "app" );
ReactDOM.hydrate( jsx, app ); // 客戶端接管應用後,React會將服務端渲染的標記載入,並將嘗試將事件偵聽器附加到現有標記
複製程式碼

結合Router

  1. 服務端通過StaticRouter來渲染對應的元件,根據瀏覽器請求的url來匹配路由並返回對應元件
  2. 返回給瀏覽器後,載入webpack打包好的客戶端js,然後接管頁面
  3. 具體看程式碼哈

結合Redux

  1. 首屏頁一般要請求介面渲染對應資料
  2. 將請求的資料設定到狀態樹上,並將JSON一併返回給瀏覽器
    <script>
        window.REDUX_DATA = ${ JSON.stringify( state ) }
    </script>
    複製程式碼
  3. 客戶端接管應用後將資料初始化到客戶端的狀態樹上
    const store = createClientStore( window.REDUX_DATA );
    const jsx = (
        <Provider store={store}>
          <Router>
            <Layout />
          </Router>
        </Provider>
    );
    複製程式碼

怎麼按需載入

  1. 使用react-lodable按需載入,其原理是import().then(),webpack在識別到後就能程式碼分割了
  2. 其會生成react-loadable.json來標識需要動態載入的元件
    const jsx = (
      <Provider store={store}>
        <Loadable.Capture report={moduleName => modules.push(moduleName)}> // 需要懶載入的模組,最後在返回的HTML中去遍歷
          <StaticRouter context={ context } location={ req.url }>
              <Layout />
          </StaticRouter>
        </Loadable.Capture>
      </Provider>
    );
    
    ${bundles.map(bundle => {
        return `<script src="/${bundle.file}"></script>`
        // alternatively if you are using publicPath option in webpack config
        // you can use the publicPath value from bundle, e.g:
        // return `<script src="${bundle.publicPath}"></script>`
      }).join('\n')}
    複製程式碼
  3. lodable實現原理
    Lodable({
      loader: ()=> import(/* webpackChunkName: 'Hello' */'./Hello'),
      loading,
    })
    class Lodable {
      componentWillMount() {
        this.cancelUpdate = false;
        const { loader } = this.props;
        loader.then(Com=> {
          this.Com = Com;
          if(!this.cancelUpdate) {
            thi.forceUpdate(); // 初次懶載入完後重新渲染
          }
        })
      }
      componentWillUnmount() {
        this.cancelUpdate = true;
      }
      render() {
        const { comProps } = this.props;
        return this.Com ? (
          <this.Com.default {...comProps} />
        ) : (
          <this.Com {...comProps}/>
        )
      }
    }
    複製程式碼

服務端怎麼處理CSS和圖片

  1. 服務端還是通過webpack來編譯對應的node端的程式碼,來實現對CSS和圖片資源的引用和處理
  2. 其實這裡只要理解客戶端和服務端分別打包,最後node執行服務端的js,返回HTML後載入客戶端js,實現客戶端接管應用
  3. 處理靜態資源還有webpack-isomorphic-tools,universal-webpack,前者作者已經不更新了,我後面發現其實用webpack直接處理簡單多了,可能是應用場景還用不到
    const serverConfig = {
      target: 'node', // 標記為node端
      mode: 'development',
      entry: './src/server',
      output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'build')
      },
      externals: [nodeExternals()], // 不會講node_modules打包進去
      module: {
        rules: [{
          test: /\.js?$/,
          loader: 'babel-loader',
          exclude: /node_modules/,
          options: {
            cacheDirectory: true,
            plugins: ['transform-decorators-legacy'],
            presets: ["react", ["env", {
              "targets": {
                "node": "current"
              }
            }], "stage-0"],
          }
        }, {
          test: /\.css?$/,
          use: ['isomorphic-style-loader', { // 處理CSS服務端的loader
            loader: 'css-loader',
            options: {
              importLoaders: 1,
              modules: true,    // 開啟css-module
              localIdentName: '[name]_[local]_[hash:base64:5]' //生成的class的命名規則
            }
          }]
        }, {
          test: /\.(png|jpeg|jpg|gif|svg)?$/,
          loader: 'url-loader',
          options: {
            limit: 8000,
            outputPath: '../build/',
            publicPath: '/'
          }
        }]
      },
      plugins: [
        new ReactLoadablePlugin({
          filename: './build/react-loadable.json',  // 按需載入的配置檔案
        }),
      ]
    };
    複製程式碼

參考文件

  1. 循序漸進, 基礎篇
  2. 稍微深入點
  3. SSR首屏到底讓SPA快多少

接下來準備做個

結合TS的服務端渲染全網免費聽歌SPA應用(Jay Chou的也有的那種)

  • 首頁,排行榜,我的
  • 將周杰倫的歌爬到mongoDB或者存在記憶體中
  • 結合Aplayer
  • 結合頻譜圖
  • 看情況支援搜尋其他歌手和歌曲

相關文章