從零開始構建react應用(六)同構之樣式直出

前言

上文講到經過同構服務端渲染,能夠直出html結構,雖然講解了樣式,圖片等靜態資源在服務端引入問題的解決方案,可是並無實際進行相關操做,這篇文章就講解一下如何讓樣式像html同樣直出。css

PS: 直出,個人理解就是輸入url發起get請求訪問服務端,直接獲得完整響應結果,而不是同過ajax異步去獲取。html

加入樣式文件

目前咱們的項目中還不存在任何樣式文件,因此須要先寫一個,就給組件App寫一個樣式文件吧。react

安裝依賴

下面這些依賴都是後續會用到的,先安裝一下,下面會詳細講解每一個依賴的做用。webpack

npm install postcss-loader postcss-import postcss-cssnext postcss-nested postcss-functions css-loader style-loader isomorphic-style-loader --save-dev

建立.pcss文件

css文件的後綴是.css,less文件的後綴是.less,這裏我選擇使用PostCSS配合其插件來寫樣式,因此我就本身定義一個後綴.pcss好了。git

// ./src/client/component/app/style.pcss

.root {
  color: red;
}

設定一個root類,樣式就是簡單的設置顏色爲紅色。而後在App組件裏引用它。es6

// ./src/client/component/app/index.tsx

...
import * as styles from './style.pcss';
...
  public render() {
    return (
      <div className={styles.root}>hello world</div>
    );
  }
...

這個時候你會發現編輯器裏是這樣的:
圖片描述
出現這個問題是由於ts不知道這種模塊的類型定義,因此咱們須要手動加入自定義模塊類型定義。在項目根目錄下新建@types文件夾,在此目錄下創建index.d.ts文件:github

// ./@types/index.d.ts

declare module '*.pcss' {
  const content: any;
  export = content;
}

保存以後就不會看到編輯器報錯了,可是terminal裏webpack打包會提示出錯,由於咱們尚未加對應的loader。web

配置.pcss文件的解析規則

js都組件化了,css模塊化也是頗有必要的,不用再爲避免取重複類名而煩惱。咱們在base配置裏新導出一個方法用以獲取postcss的規則。ajax

// ./src/webpack/base.ts

...
export const getPostCssRule = (styleLoader) => ({
  test: /\.pcss$/,
  use: [
    styleLoader,
    {
      loader: 'css-loader',
      options: {
        camelCase: true,
        importLoaders: 1,
        localIdentName: '[path][name]---[local]---[hash:base64:5]',
        modules: true,
      },
    },
    {
      loader: 'postcss-loader',
      options: {
        plugins: () => [
          require('postcss-import')({
            path: path.join(baseDir, './src/client/style'),
          }),
          require('postcss-cssnext'),
          require('postcss-nested'),
          require('postcss-functions')({
            functions: {
              x2(v, u) {
                return v * 2 + (u ? u : 'px');
              },
            },
          }),
        ],
      },
    },
  ],
});
...

咱們能夠從上面這個方法看到,要處理.pcss文件須要用到三個loader,按處理順序從下往上分別是postcss-loader, css-loader, 還有一個變量styleLoader,至於這個變量是什麼,咱們能夠看使用到該方法的地方:npm

// ./src/webpack/client.ts

...
(clientDevConfig.module as webpack.NewModule).rules.push(
  ...
  getPostCssRule({
    loader: 'style-loader',
  }),
  ...
);
...
// ./src/webpack/server.ts

...
(clientDevConfig.module as webpack.NewModule).rules.push(
  ...
  getPostCssRule({
    loader: 'isomorphic-style-loader',
  }),
  ...
);
...

客戶端和服務端處理樣式文件須要使用到不一樣的styleLoader。

PostCSS簡介

PostCSS是一個使用js來轉換css的工具,這個是官方介紹。其配合webpack使用的loader就是postcss-loader,可是隻有單個postcss-loader其實沒有什麼用,須要配合其插件來實現強大的功能。

  1. postcss-import
    這個插件我這裏使用的緣由是爲了在樣式文件中@import時避免複雜的路徑編寫,我設定好path值,那麼我在其它任何層級下的樣式文件中要引入path對應文件夾裏的公共變量樣式文件(假設叫"variables.pcss")時就很是方便,只須要寫import 'variables.pcss';就能夠了,固然若是找不到對應的文件,它會忽略path使用默認相對路徑來查找。
  2. postcss-cssnext
    這個插件可使用下一代css語法。
  3. postcss-nested
    這個插件能夠嵌套編寫樣式。
  4. postcss-functions
    這個插件能夠自定義函數,並在樣式文件中調用。

講這麼多,寫代碼舉個栗子吧~
咱們在client目錄下新增style文件夾,用於存放一些樣式reset,變量文件之類的東西。而後建立兩個pcss文件:

// ./src/client/style/variables.pcss

:root {
  --fontSizeValue: 16;
}
// ./src/client/style/index.pcss

@import 'variables.pcss';

body {
  margin: 0;
  font-size: x2(var(--fontSizeValue));
}

引入咱們剛寫的index.pcss

// ./src/client/index.tsx
...
import './style/index.pcss';
...

CSS Modules簡介

簡單來講就是css模塊化,不用再擔憂全局類名的問題。咱們根據上述css-loader的options來看:

  1. camelCase爲true運行使用駝峯寫法來寫類名
  2. importLoaders的值爲N是由於在css-loader以前有N個loader已經處理過文件了,這裏的N值是1,由於以前有一個postcss-loader,這個值必定要設置對,不然會影響@import語句,個人這個表述可能不是太正確,詳細可參見Clarify importLoaders documentation?這個地方詳細講解了,我翻譯一下大概意思是,這個屬性的值N表明的是對於@import的文件要通過css-loader後面的N個loader的處理,英文不太好,你們能夠自行理解。
  3. localIdentName這個就是指生成的類名啦,具體看後續結果截圖就一目瞭然了。
  4. modules爲true即啓用模塊化

isomorphic-style-loader

在客戶端,使用style-loader,它會動態的往dom裏插入style元素,而服務端因爲缺乏客戶端的相關對象及API,因此須要isomorphic-style-loader,目前用到它只是爲了不報錯哈哈,後續還有大做用,樣式直出全靠它。

打包運行

注意:打包運行以前不要忘了給tsconfig.client.json和tsconfig.server.json引入咱們的自定義模塊定義文件index.d.ts,否則webpack編譯就會報找不到pcss這種模塊啦。

// ./src/webpack/tsconfig.client(server).json
...
"include": [
    ...
    "../../@types/**/*",
    ...
]
...

運行結果以下:
圖片描述
雖然style元素已經存在,可是這個是由style-loader生成的,並非服務端直出的,看page source就知道了。
圖片描述
並且在刷新頁面的時候能很明顯的看到樣式變化閃爍的效果。

直出樣式

咱們利用isomorphic-style-loader來實現服務端直出樣式,原理的話根據官方介紹就是利用了react的context api來實現,在服務端渲染的過程當中,利用注入的insertCss方法和高階組件(hoc high-order component)來獲取樣式代碼。

安裝依賴

npm install prop-types --save-dev

改寫App組件

根據其官方介紹,咱們在不使用其整合完畢的isomorphic router的狀況下,須要寫一個Provider給App組件:

// ./src/client/component/app/provider.tsx

import * as React from 'react';

import * as PropTypes from 'prop-types';

class AppProvider extends React.PureComponent<any, any> {
  public static propTypes = {
    context: PropTypes.object,
  };

  public static defaultProps = {
    context: {
      insertCss: () => '',
    },
  };

  public static childContextTypes = {
    insertCss: PropTypes.func.isRequired,
  };

  public getChildContext() {
    return this.props.context;
  }

  public render() {
    return this.props.children || null;
  }
}

export default AppProvider;

將原App組件裏的具體內容遷移到AppContent組件裏去:

// ./src/client/component/app/content.tsx

import * as React from 'react';

import * as styles from './style.pcss';

/* tslint:disable-next-line no-submodule-imports */
import withStyles from 'isomorphic-style-loader/lib/withStyles';

@withStyles(styles)
class AppContent extends React.PureComponent {
  public render() {
    return (
      <div className={styles.root}>hello world</div>
    );
  }
}

export default AppContent;

新的App組件:

// ./src/client/component/app/index.tsx

import * as React from 'react';

import AppProvider from './provider';

import AppContent from './content';

class App extends React.PureComponent {
  public render() {
    return (
      <AppProvider>
        <AppContent />
      </AppProvider>
    );
  }
}

export default App;

疑問一:AppProvider組件是作什麼的?

答:Provider的意思是供應者,提供者。顧名思義,AppProvider爲其後代組件提供了一些東西,這個東西就是context,它有一個insertCss方法。根據其定義,該方法擁有默認值,返回空字符串的函數,即默認沒什麼做用,可是能夠經過props傳入context來達到自定義的目的。經過設定childContextTypes和getChildContext,該組件後代凡是設定了contextTypes的組件都會擁有this.context對象,而這個對象正是getChildContext的返回值。

疑問二:AppContent爲什麼要獨立出去?

答:接上一疑問,AppProvider組件render其子組件,而要使得context這個api生效,其子組件必須是定義了contextTypes的,可是咱們並無看見AppContent有這個定義,這個是由於這個定義在高階組件withStyles裏面(參見其源碼)。

疑問三:@withStyles是什麼語法?

答:這個是裝飾器,屬於es7,具體概念內容可參見Decorators in ES7。使用該語法,須要配置tsconfig:

// ./tsconfig.json
// ./src/webpack/tsconfig.client(server).json

{
  ...
  "compilerOptions": {
    ...
    "experimentalDecorators": true,
    ...
  },
  ...
}

改寫服務端bundle文件

因爲App組件的改寫,服務端不能再複用該組件,可是AppProvider和AppContent目前仍是能夠複用的。

// ./src/server/bundle.tsx

import * as React from 'react';

/* tslint:disable-next-line no-submodule-imports */
import { renderToString } from 'react-dom/server';

import AppProvider from '../client/component/app/provider';

import AppContent from '../client/component/app/content';

export default {
  render() {
    const css = [];
    const context = { insertCss: (...styles) => styles.forEach((s) => css.push(s._getCss())) };
    const html = renderToString(
      <AppProvider context={context}>
        <AppContent />
      </AppProvider>,
    );
    const style = css.join('');
    return {
      html,
      style,
    };
  },
};

這裏咱們傳入了自定義的context對象,經過css這個變量來存儲style信息。咱們原先render函數直接返回renderToString的html字符串,而如今多了一個style,因此咱們返回擁有html和style屬性的對象。

疑問四:官方示例css是一個Set類型實例,這裏怎麼是一個數組類型實例?

答:Set是es6中新的數據結構,相似數組,但能夠保證無重複值,只有tsconfig的編譯選項中的target爲es6時,且加入es2017的lib時纔不會報錯,因爲咱們的target是es5,因此是數組,且使用數組並無太大問題。

處理服務端入口文件

因爲bundle的render值變動,因此咱們也要處理一下。

// ./src/server/index.tsx

...
router.get('/*', (ctx: Koa.Context, next) => { // 配置一個簡單的get通配路由
  const renderResult = bundle ? bundle.render() : {}; // 得到渲染出的結果對象
  const { html = '', style = '' } = renderResult;
  ...
  ctx.body = `
    ...
    <head>
      ...
      ${style ? `<style>${style}</style>` : ''}
      ...
    </head>
    ...
  `;
  ...
});
...

直出結果

樣式直出後的page source:
圖片描述

找回丟失的公共樣式文件

從上面的直出結果來看,缺乏./src/style/index.pcss這個樣式代碼,緣由顯而易見,它不屬於任何一個組件,它是公共的,咱們在客戶端入口文件裏引入了它。對於公共樣式文件,服務端要直出這部份內容,能夠這麼作:

./src/server/bundle.tsx

...
import * as commonStyles from '../client/style/index.pcss';
...
const css = [commonStyles._getCss()];
...

咱們利用isomorphic-style-loader提供的api能夠獲得這部分樣式代碼字符串。這樣就能夠獲得完整的直出樣式了。

Thanks

By devlee

相關文章
相關標籤/搜索