从零开始构建react应用(六)同构之样式直出

前言

上文讲到通过同构服务端渲染,可以直出html结构,虽然讲解了样式,图片等静态资源在服务端引入问题的解决方案,但是并没有实际进行相关操作,这篇文章就讲解一下如何让样式像html一样直出。

PS: 直出,我的理解就是输入url发起get请求访问服务端,直接得到完整响应结果,而不是同过ajax异步去获取。

加入样式文件

目前我们的项目中还不存在任何样式文件,所以需要先写一个,就给组件App写一个样式文件吧。

安装依赖

下面这些依赖都是后续会用到的,先安装一下,下面会详细讲解每个依赖的作用。

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好了。

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

.root {
  color: red;
}

设定一个root类,样式就是简单的设置颜色为红色。然后在App组件里引用它。

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

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

这个时候你会发现编辑器里是这样的:
从零开始构建react应用(六)同构之样式直出
出现这个问题是因为ts不知道这种模块的类型定义,所以我们需要手动加入自定义模块类型定义。在项目根目录下新建@types文件夹,在此目录下建立index.d.ts文件:

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

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

保存之后就不会看到编辑器报错了,但是terminal里webpack打包会提示出错,因为我们还没有加对应的loader。

配置.pcss文件的解析规则

js都组件化了,css模块化也是很有必要的,不用再为避免取重复类名而烦恼。我们在base配置里新导出一个方法用以获取postcss的规则。

// ./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,至于这个变量是什么,我们可以看使用到该方法的地方:

// ./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/**/*",
    ...
]
...

运行结果如下:
从零开始构建react应用(六)同构之样式直出
虽然style元素已经存在,但是这个是由style-loader生成的,并不是服务端直出的,看page source就知道了。
从零开始构建react应用(六)同构之样式直出
而且在刷新页面的时候能很明显的看到样式变化闪烁的效果。

直出样式

我们利用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:
从零开始构建react应用(六)同构之样式直出

找回丢失的公共样式文件

从上面的直出结果来看,缺少./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

相关推荐