React服務端渲染之路07——添加CSS樣式

全部源代碼、文檔和圖片都在 github 的倉庫裏,點擊進入倉庫javascript

相關閱讀

1. CSS 樣式的添加

  • 咱們以前配置的 webpack,僅僅是配置了 js,對於 css 及 css 預處理器都沒有配置,因此咱們須要配置一下 css,咱們統一採用 sass 預處理器

1.1 webpack.client.js 的配置

  • 這裏就要問了,爲何不把 css 配置到 webpack.base.js 裏呢,由於服務端不識別 css 代碼,因此咱們不能簡單的把 css 配置信息寫在 webpack.base.js 裏
  • 下載依賴 npm i node-sass sass-loader -D
  • 修改 webpack.client.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader',
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1,
              modules: true,
              localIdentName: '[name]_[local]_[hash:base64:5]'
            }
          }
        ]
      }
    ]
  }
}

1.2 webpack.server.js 的配置

  • 服務器端不能直接識別 css 資源,可是咱們仍是須要服務器端可以識別 css 資源,因此咱們使用一個庫,專門用來爲服務端識別 css,這個庫是 isomorphic-style-loader
module.exports = {
  module: {
    rules: [
      {
        test: /\.css?$/,
        use: ['isomorphic-style-loader', {
          loader: "css-loader",
          options: {
            importLoaders: 1,
            modules: true,
            localIdentName: '[name]_[local]_[hash:base64:5]'
          }
        }]
      }
    ]
  }
}

1.3 組件使用 css 樣式

  • 組件使用 css 樣式的時候,能夠像之前同樣,直接引入 css 文件,把樣式做用在對應的 DOM 標籤上
  • /src/containers/Home/index.css
/**
 * /src/containers/Home/index.css
 */
.wrapper {
  background: orange;
}

.title {
  color: red;
  font-size: 26px;
}
  • /src/containers/Home/index.js
// /src/containers/Home/index.js
import styles from './index.css';

class Home extends Component {

  render() {
    return (
      <div className={styles.wrapper}>
        <h2 className={styles.title}>HELLO, HOME PAGE</h2>
      </div>
    );
  }
}
  • 直接這樣使用,咱們就能夠在頁面上看到對應的 css 樣式
  • 可是這樣有兩個問題css

    • 第一個問題是,瀏覽器必需要開啓 js,若是不開啓 js,那麼樣式是不生效的
    • 第二個問題是,當咱們的頁面刷新頻率過快,而且不使用緩存,那麼頁面有很是明顯的抖動
  • 這兩個問題對用戶來講,體驗很是很差,因此咱們進一步改進

1.4 把樣式注入到服務端的 HTML 模板中

  • 實際上,上面咱們用的方式是把 css 寫在了 js 裏邊,若是咱們查看頁面的源代碼,咱們只能在頁面上找到 DOM 元素的類名,可是咱們找不到任何的 css 代碼,由於所有都在 /client.js 裏,因此咱們要把 css 從 js 裏拿出來,寫在 HTML 頁面上
  • 當咱們引入一個 css 文件的時候,引入的模塊就自帶一些屬性,這些屬性是 webpack 所提供的,咱們能夠看一下
import styles from './index.css';

console.log(styles);

{ wrapper: 'index_wrapper_2wP7c',
  title: 'index_title_39dQ8',
  _getContent: [Function],
  _getCss: [Function],
  _insertCss: [Function]
}

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

console.log(styles._getContent());

[
  [ './node_modules/_css-loader@2.1.1@css-loader/dist/cjs.js?!./src/containers/Home/index.css',
    '.index_wrapper_2wP7c {\r\n  background: orange;\r\n}\r\n\r\n.index_title_39dQ8 {\r\n  color: red;\
\n  font-size: 26px;\r\n}\r\n', '' ],
  toString: [Function: toString],
  i: [Function],
  locals: { wrapper: 'index_wrapper_2wP7c',
    title: 'index_title_39dQ8',
    _getContent: [Function],
    _getCss: [Function],
    _insertCss: [Function] } ]

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

console.log(styles._getCss());

.index_wrapper_2wP7c {
  background: orange;
}

.index_title_39dQ8 {
  color: red;
  font-size: 26px;
}
  • 咱們依次在控制檯輸出 styles 的一些屬性,咱們能夠查看到, 咱們定義的類名,已經被進行了轉換,並且咱們定義的樣式,所有都在 styles._getCss() 裏
  • 因此,咱們能夠把類名賦值須要使用的 DOM 元素,css 樣式的內容,傳遞給服務端,讓服務端直接把樣式載入到 HTML 模板中
  • 可是該怎麼操做呢?前邊咱們說到了 StaticRouter 靜態路由有一個 context 的屬性,這個屬性是用來先後端進行傳遞數據的,因此咱們能夠把數據經過 context 傳遞
  • 咱們直接在 Home 組件裏輸出一下 this.props,咱們會發現有一個很是有意思的現象,就是在瀏覽器的控制檯,輸出的 props.staticContext 的值是 undefined,可是在服務端的控制檯,輸出的是一個對象,裏邊的 csses 的屬性值是咱們以前定義的 css 內容
  • 這是由於,staticContext 雖然可以傳值,可是傳值僅僅存在與服務端和組件之間,並不在客戶端和組件之間,咱們咱們在服務端就能夠拿到 css 的樣式
  • 拿到 css 樣式後,直接把 css 內容做爲字符串,添加到 HTML 模板的 style 標籤裏,就能夠了
  • 注意: context.csses 必須爲數組類型,把每個組件的樣式做爲一個元素 push 到數組中,這樣每個組件的 css 樣式均可以生效,可是,若是咱們直接把 css 的樣式賦值給 context.csses ,那麼樣式將會被覆蓋,這個覆蓋不是樣式的覆蓋,而是 js 值的覆蓋,最早渲染的組件的 css 的樣式被後來渲染的組件的 css 樣式所覆蓋,這樣是不正確的,因此必定要使用數組,而不是直接賦值
  • /src/containers/Home/index.js
componentWillMount() {
  let staticContext = this.props.staticContext;
  if (staticContext) {
    if (staticContext) {
      staticContext.csses.push(styles._getCss());
    }
  }
}
  • /src/server/render.js
export default (req, res) => {

  let context = {
    csses: []
  };

  Promise.all(promises).then(() => {
    let domContent = renderToString(
      <Provider store={store}>
        <StaticRouter context={context} location={req.path}>
          {
            renderRoutes(routes)
          }
        </StaticRouter>
      </Provider>
    );

    let cssStr = context.csses.length ? context.csses.join('\n') : '';

    let html = `
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
  <link href="https://cdn.bootcss.com/twitter-bootstrap/3.3.7/css/bootstrap.css" rel="stylesheet">
  <title>react-ssr</title>
  <style>${cssStr}</style>
</head>
<body>
<div id="root">${domContent}</div>
<script>
  window.context = {
    state: ${JSON.stringify(store.getState())}
  }
</script>
<script src="/client.js"></script>
</body>
</html>
`;

    res.send(html);
  });
};
  • 這樣,快速刷新瀏覽器,頁面也不會抖動,禁用掉 js ,頁面樣式依然存在
  • 咱們能夠查看一下頁面源代碼,咱們能夠發現,css 的源代碼,就在 style 標籤裏

2. 封裝樣式組件

  • 若是咱們有多個頁面,每個頁面都有本身的 css 樣式,那麼咱們就要在每個組件裏都要寫 componmentWillMount 鉤子函數,在這個函數裏把 css 樣式傳遞到 staticContext 裏,這樣明顯不是一個好的辦法,因此咱們能夠封裝一個高階組件
  • 咱們封裝一個 WithStyle 的高階組價,把原組件和樣式做爲參數傳遞給高階組件
import React, { Component } from 'react';

export default (DecoratedComponent, styles) => {
  return class NewComponent extends Component {
    componentWillMount() {
      if (this.props.staticContext) {
        this.props.staticContext.csses.push(styles._getCss());
      }
    }

    render() {
      return (<DecoratedComponent {...this.props} />);
    }

  };
};
  • 在 Home 組件裏使用這個高階組件
import WithStyle from '../../withStyle';

export default connect(mapStateToProps, mapDispatchToProps)(WithStyle(Home, styles));
  • 這樣,咱們就能夠把樣式相關的功能做爲高階組件封裝起來,提升代碼的複用率

3. 優化組件

  • 咱們在 Home 組件裏定義了一個靜態方法 loadData,這個方法是在 Home 組件下的,可是咱們使用了 WithStyle 高階組件對 Home 組件進行了包裝,那麼咱們在導出的組件,就再也不是 Home 組件了,這樣會有一些潛在的問題,就是導出的組件沒有 loadData 方法,那麼咱們在使用的時候就會報錯,因此咱們能夠作一些改進
  • 咱們從新定義個 ExportHome 的變量,這個變量是各個高階組件包裝後的返回值,在 ExportHome 組件上定義 loadData 方法,這樣就能夠保證導出的組件必定有 loadData 方法
  • 之因此咱們以前使用 connect 包裝以後沒有報錯,是由於 connect 自動幫咱們作了轉換,已經把 loadData 方法掛載到導出的對象上了,因此沒有報錯
const ExportHome = connect(mapStateToProps, mapDispatchToProps)(WithStyle(Home, styles));

ExportHome.loadData = store => store.dispatch(UserActions.getSchoolList());

export default ExportHome;
  • 因此,這是一個須要注意的點

相關閱讀

相關文章
相關標籤/搜索