浅谈react 同构之样式直出

前言

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

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

React 同构的关键要素

完善的 Compponent 属性及生命周期与客户端的 render 时机是 React 同构的关键。

DOM 的一致性

在前后端渲染相同的 Compponent,将输出一致的 Dom 结构。

不同的生命周期

在服务端上 Component 生命周期只会到 componentWillMount,客户端则是完整的。

客户端 render 时机

同构时,服务端结合数据将 Component 渲染成完整的 HTML 字符串并将数据状态返回给客户端,客户端会判断是否可以直接使用或需要重新挂载。

以上便是 React 在同构/服务端渲染的提供的基础条件。在实际项目应用中,还需要考虑其他边角问题,例如服务器端没有 window 对象,需要做不同处理等。下面将通过在手Q家校群上的具体实践,分享一些同构的 Tips 及优化成果

加入样式文件

目前我们的项目中还不存在任何样式文件,所以需要先写一个,就给组件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>
  );
 }
...

这个时候你会发现编辑器里是这样的:

出现这个问题是因为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/**/*",
  ...
]
...

运行结果如下:

虽然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。使用该语法,需要配置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可以得到这部分样式代码字符串。这样就可以得到完整的直出样式了。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 浅谈react前后端同构渲染

    前后端同构渲染:当客户端请求一个包含React组件页面的时候,服务端首先响应输出这个页面,客户端和服务端有了第一次交互.然后,如果加载组件的过程需要向服务端发出Ajax请求等,客户端和服务端又进行了一次交互,这样,耗时相对较长.前后端同构渲染可以在页面初次加载时把所有地方渲染好一次性响应给客户端 实现方式:保证包管理工具和模块依赖方式一致 包管理工具-npm管理,保证前后端都使用同一个兼容包 模块依赖方式-webpack,保证前后端都采用commonjs的依赖方式,确保代码可以互相依赖 服务端如

  • 详解react服务端渲染(同构)的方法

    学习react也有一段时间了,使用react后首页渲染的速度与seo一直不理想.打算研究一下react神奇服务端渲染. react服务端渲染只能使用nodejs做服务端语言实现前后端同构,在后台对react组件进行解析并生成html字符串后返回视图页面. 后台为什么可以解析react组件?因为Node.js是一个Javascript运行环境,nodejs与javascript语法基本是相同的,所以nodejs可以正常解析react组件. 一.准备动作 1.安装nodejs与安装express 安

  • 浅谈react 同构之样式直出

    前言 上文讲到通过同构服务端渲染,可以直出html结构,虽然讲解了样式,图片等静态资源在服务端引入问题的解决方案,但是并没有实际进行相关操作,这篇文章就讲解一下如何让样式像html一样直出. PS: 直出,我的理解就是输入url发起get请求访问服务端,直接得到完整响应结果,而不是同过ajax异步去获取. React 同构的关键要素 完善的 Compponent 属性及生命周期与客户端的 render 时机是 React 同构的关键. DOM 的一致性 在前后端渲染相同的 Compponent,

  • 浅谈React中组件逻辑复用的那些事儿

    基本每个开发者都需要考虑逻辑复用的问题,否则你的项目中将充斥着大量的重复代码.那么 React 是怎么复用组件逻辑的呢?本文将一一介绍 React 复用组件逻辑的几种方法,希望你读完之后能够有所收获.如果你对这些内容已经非常清楚,那么略过本文即可. 我已尽量对文中的代码和内容进行了校验,但是因为自身知识水平限制,难免有错误,欢迎在评论区指正. 1. Mixins Mixins 事实上是 React.createClass 的产物了.当然,如果你曾经在低版本的 react 中使用过 Mixins,

  • 浅谈react+es6+webpack的基础配置

    这是模块化开发.主流框架和最新版的ECMAScript语法规范的一个小demo 准备工作 安装 nodeJs 首先进入node官网,去下载最新版的nodeJs webpack 安装webpack npm install webpack -g 参数-g表示全局安装webpack,你在cmd命令中哪个文件夹下都可以使用webpack的命令,如果不加-g的话,是只可以在你安装webpack的目录下使用webpack这个命令 webpack 也有一个 web 服务器 npm install webpac

  • 浅谈React Native打包apk的坑

    RN的打包,大家可以根据官网一步一步来,但这里有几个地方注意,一下简单介绍: 生成一个签名密钥 在项目的目录下打开cmd命令窗口输入一下命令运行: keytool -genkey -v -keystore my-release-key.keystore -alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000 这条命令会要求你输入密钥库(keystore)和对应密钥的密码,然后设置一些发行相关的信息.最后它会生成一个叫做my-re

  • 浅谈React深度编程之受控组件与非受控组件

    受控组件与非受控组件在官网与国内网上的资料都不多,有些人觉得它可有可不有,也不在意.这恰恰显示React的威力,满足不同规模大小的工程需求.譬如你只是做ListView这样简单的数据显示,将数据拍出来,那么for循坏与 {} 就足够了,但后台系统存在大量报表,不同的表单联动,缺了受控组件真的不行. 受控组件与非受控组件是React处理表单的入口.从React的思路来讲,作者肯定让数据控制一切,或者简单的理解为,页面的生成与更新得忠实地执行JSX的指令. 但是表单元素有其特殊之处,用户可以通过键盘

  • 浅谈React Router关于history的那些事

    如果你想理解React Router,那么应该先理解history.更确切地说,是history这个为React Router提供核心功能的包.它能轻松地在客户端为项目添加基于location的导航,这种对于单页应用至关重要的功能. npm install --save history 存在三类history,分别时browser,hash,与 memory.history包提供每种history的创建方法. import { createBrowserHistory, createHashHi

  • 浅谈react useEffect闭包的坑

    问题代码 看一段因为useEffect导致的闭包问题代码 const btn = useRef(); const [v, setV] = useState(''); useEffect(() => { let clickHandle = () => { console.log('v:', v); } btn.current.addEventListener('click', clickHandle) return () => { btn.removeEventListener('clic

  • 浅谈React 中的浅比较是如何工作的

    React 中浅比较的概念无处不在,它在不同的流程中起着关键的作用,也可以在React组件的多个生命周期中找到.比如,React Hooks中的依赖数组,通过React.memo进行记忆.在React的官方文档中也多次提到“浅比较”这个概念,下面我们就来看看React中的浅比较是如何工作的! 想要理解浅比较的概念,最直接的方法就是研究React的源代码,下面就来看看React中的shallowEqual.js 文件: import is from './objectIs'; import has

  • 浅谈React Refs 使用场景及核心要点

    目录 什么是 Refs? 使用方式 Refs 核心要点 避免重复创建 ref 内容 ref.current 存储的内容修改是突变 ref 作为数据存储时内容的变化不会引起 re-render ref 的读写只能在 useEffect 或者回调函数中进行 跨组件传递ref 获取dom时需要借助 forwardRef 包裹组件 ref 绑定的dom在离屏或者未挂载时ref.current 值会被修改为null 最佳实践 dom 操作相关 用于在两次 render 之间传递数据 在使用 React 进

  • 浅谈React底层实现原理

    目录 1. props,state与render函数关系,数据和页面如何实现互相联动? 2. React中的虚拟DOM 常规思路 改良思路(仍使用DOM) React的思路 深入理解虚拟DOM 3. 虚拟DOM的diff算法 4. React中ref的使用 5. React中的生命周期函数 6. 生命周期函数的使用场景 1. props,state与render函数关系,数据和页面如何实现互相联动? 当组件的state或者props发生改变的时候,自己的render函数就会重新执行.注意:当父组

随机推荐