加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 百科 > 正文

如何规模化React应用

发布时间:2020-12-15 07:20:03 所属栏目:百科 来源:网络整理
导读:编译自How To Scale React Applications 我们最近发布了React Boilerplate3.0。在发布这一版本前,我们与数百位开发者进行了沟通,讨论了他们构建和规模化(scale) web 应用的方式。下面我将分享一些我们从中学到的东西。 React Boilerplate 不是“又一个模板

编译自How To Scale React Applications

我们最近发布了React Boilerplate3.0。在发布这一版本前,我们与数百位开发者进行了沟通,讨论了他们构建和规模化(scale) web 应用的方式。下面我将分享一些我们从中学到的东西。

React Boilerplate 不是“又一个模板项目”,而是希望能为开发者们提供创业或打造产品的最佳基石。

在过去,规模化大多和服务器系统有关。随着使用的用户越来越多,你必须确保能在服务器集群中添加更多机器,数据库能分拆到多台服务器上。

但现在,由于富 web 应用的崛起,规模化也成了前端的重要话题!复杂应用的前端也需要能应付得了大量用户、开发者和组件。开发者们在一开始就需要考虑这三个方面(用户、开发者和组件)的规模化,否则后面会碰到问题。

容器和组件

了解有状态(“容器”)和无状态(“组件”)组件的区别,能极大地提升对大型应用的清晰了解。容器管理数据或者与状态相连,通常不会有样式。组件则有样式,且不用负责任何数据或状态管理。基本上,容器负责让组件正常工作,组件负责外观。

根据这一点,我们清楚地区分了可复用组件和数据管理中间层。现在你去编辑组件时,就不用担心会搞混数据结构;编辑容器,也不用担心弄乱样式了。

代码组织结构

开发者们一般会按照类型来组织代码,比如使用 actions/,components/,containers/ 等文件夹。假设有一个 NavBar 容器,容器会管理与之相关的状态,还有一个 toggleNav 动作来打开关闭它。下面就是按类型组织的代码结构图:

这种组织方式用来做样例还可以,但如果你有成百上千个组件,用通过这种方式组织的代码开发就会很困难。每添加一项功能,你就必须在包含众多文件的多个文件夹中找到正确的文件。这很无聊,也很容易让人感到疲劳。

通过长期追踪我们的 Github 问题列表和多次尝试后,我们找到了一种好得多的代码结构组织解决方案:

那就是按照功能来组织代码!即把和某个功能相关的所有文件放到同一个文件夹里。下面是重新组织的 NavBar 代码:

这种方式非常有利于重命名、查找文件和替换,多人协作也不会导致任何冲突。需要注意的是,这种组织方式并不意味着 redux actions 和 reducers 只能用在这个组件里,它们可以也应该用于其他组件中!

样式管理

除了结构性的考虑之外,由于 CSS 本身的两个特点,在基于组件的架构中使用 CSS 很难:全局命名和继承。

独一无二的类名

假设在某个大型应用中存在这段 CSS

.header { /* … */ }
.title {
    background-color: yellow;
}

很快你就会意识到问题,title 太常用了。其他开发者也可能会写出下列代码:

.footer { /* … */ }
.title {
    border-color: blue;
}

这会产生命名冲突,让你不得不在成百上千个文件中找到这段把一切事情弄糟的声明。

幸运的是,我们有CSS Modules。CSS Modules 的关键之处在于:把组件和组件的样式文件放在同一个文件夹里。

这样我们就不用担心命名规范了,可以使用非常常见的名字:

.button {
    /* … */
}

然后我们在组件中 import 或 require CSS 文件即可,并在 className 中使用

/* Button.jsx */
var styles = require('./styles.css');

<div className={styles.button}></div>

如果你现在去查看对应的 DOM 结构,你会看到<div class="MyApp__button__1co1k"></div>!CSS Modules 帮你保证了类名的“独一无二”。

为每个组件重置属性

在 CSS 中,特定的属性会在 DOM 节点上下继承。比如,如果父节点有 line-height,而子节点没有,子节点就会自动继承父节点的 line-height。

在基于组件的架构中,这可不是我们想要的。比如下面的代码:

.header {
    line-height: 1.5em;
    /* … */
}

.footer {
    line-height: 1;
    /* … */
}

假设我们在这两个组件中渲染一个 Button,这个 Button 在这两个组件里会有不同的外观!这一点不仅适用于 line-height,也适用于其他能继承的属性。

在以前,我们可以用 Reset CSS、Normalize.css 和 sanitize.css 来重置样式表。但如果我们想将这一理念落实到每一个组件上呢?

这就是PostCSS插件PostCSS Auto Reset的用途!它会对每个组件进行样式重置,将所有能继承的 CSS 属性设置成默认值,从而覆盖继承。

数据拉取

基于组件的架构面临的第二大问题就是数据拉取。对于大部分 action 来说,将 action 和组件放在一起很合理,但数据拉取是全局 action,不和某个组件相连!

目前大多数开发者使用 Redux Thunk 来处理 Redux 中的数据拉取。标准的 thunked action 如下:

/* actions.js */

function fetchData() {
    return function thunk(dispatch) {
        // 异步加载
        fetch('https://someurl.com/somendpoint',function callback(data) {
            // 将数据加入 store
            dispatch(dataLoaded(data));
        });
    }
}

这种在 action 中拉取数据的方法很棒,但存在两个问题:很难测试这些函数,以及在 action 中包含数据拉取看起来不太对。

Redux 的一大好处就是 pure action creator 很容易测试。而在 action 中加入了 thunk 的数据拉取操作之后,你必须两次调用 action,模拟 dispatch 函数等。

最近,redux-saga在 React 世界引起了广泛关注。redux-saga 利用了 ES6 的 Generator 函数,让异步代码看起来就像同步代码一样,而且让异步流非常容易测试。redux-saga 给人的感觉是一个单独处理所有异步事务的独立线程,不会干扰应用的其他方面!

下面是 redux-saga 的一个例子:

/* sagas.js */

import { call,take,put } from 'redux-saga/effects';

// 函数后面的星号表示这是 generator 函数
function* fetchData() {
    // yield:在这个异步函数完成前一直等待
    yield take(FETCH_DATA);
    // 然后从服务器拉取数据,重新通过 yield 等待
    var data = yield call(fetch,'https://someurl.com/someendpoint');
    // 当数据完成加载后,dispatch dataLoaded action.
    put(dataLoaded(data));
}

上面的代码读起来就像小说一样,避免了回调地狱,而且非常容易测试。为什么?这是因为 redux-saga 导出的这些“作用”(effect)无需完成即可进行测试。

我们在文件顶部 import 的这些作用都是 handler,可以让我们轻松与 redux 交互:

  • put() dispatches 一个 action
  • take() 暂停 saga 直到 action 发生
  • select() 获取部分 redux 状态(有点像 mapStateToProps)
  • call() 调用传入的第一个位置的函数,并将其他参数作为被调用函数的参数

为什么这些作用有用?让我们看看下面的测试:

/* sagas.test.js */

var sagaGenerator = fetchData();

describe('fetchData saga',function() {
	// 测试当 action 被 dispatch 后,saga 启动
	// 即便无需真正模拟  dispatch 发生了
	it('should wait for the FETCH_DATA action',function() {
	    expect(sagaGenerator.next()).to.equal(take(FETCH_DATA));
	});

	// 测试 saga 用某个 URL 做参数调用了 fetch 函数
	// 即便无需真正模拟 fetch 函数或使用 API 乃至联网!
	it('should fetch the data from the server',function() {
	    expect(sagaGenerator.next()).to.equal(call(fetch,'https://someurl.com/someendpoint'));
	});

	// 测试 saga dispatch 了一个 action
	// 而无需主应用运行!
	it('should dispatch the dataLoaded action when the data has loaded',function() {
	    expect(sagaGenerator.next()).to.equal(put(dataLoaded()));
	});
});

只有 generator.next() 被调用时,generator 函数才会继续,直到遇到下一个 yield 关键字!另外,我们也把测试文件和组件放在了一起。

用 redux-saga 来做胶水中间件

我们的组件现在真正解耦了。它们不关心其他组件的样式或逻辑;它们只关心自己的事情(绝大多数情况下如此)。

假设有一个 Clock 和一个 Timer 组件。当 Clock 中的按被按下时,我们想要启动 Timer;当 Timer 中的停止按钮被按下时,我们想要在 Clock 中显示时间。

你也许会这么做:

/* Clock.jsx */

import { startTimer } from '../Timer/actions';

class Clock extends React.Component {
    render() {
	return (
	    /* … */
	    <button onClick={this.props.dispatch(startTimer())} />
	    /* … */
	);
    }
}
/* Timer.jsx */

import { showTime } from '../Clock/actions';

class Timer extends React.Component {
    render() {
	return (
	    /* … */
	    <button onClick={this.props.dispatch(showTime(currentTime))} />
	    /* … */
	);
    }
}

但这么做的话,你无法单独使用这两个组件,复用它们几乎不可能!

不过,我们可以用 redux-saga 来做这两个解耦组件的“胶水中间件”。取决于应用类型,我们可以通过听取特定 action,来以不同方式作出反应,从而让组件真正变得可复用。

先修改两个组件:

/* Clock.jsx */

import { startButtonClicked } from '../Clock/actions';

class Clock extends React.Component {
    /* … */
    <button onClick={this.props.dispatch(startButtonClicked())} />
    /* … */
}
/* Timer.jsx */

import { stopButtonClicked } from '../Timer/actions';

class Timer extends React.Component {
    /* … */
    <button onClick={this.props.dispatch(stopButtonClicked(currentTime))} />
    /* … */
}

注意两个组件现在只关心自己,只 import 了自己的 action !

下面用一个 saga 来把这两个解耦的组件连接到一起:

/* sagas.js */

import { call,put,select } from 'redux-saga/effects';

import { showTime } from '../Clock/actions';
import { START_BUTTON_CLICKED } from '../Clock/constants';
import { startTimer } from '../Timer/actions';
import { STOP_BUTTON_CLICKED } from '../Timer/constants';

function* clockAndTimer() {
    // 等待 Clock 的 startButtonClicked action 被 dispatch
    yield take(START_BUTTON_CLICKED);
    // dispatch 后,启动 timer.
    put(startTimer());
    // 等待 Timer 的 stopButtonClick action 被 dispatch
    yield take(STOP_BUTTON_CLICKED);
    // 从全局状态获取 Timer 的当前时间
    var currentTime = select(function (state) { return state.timer.currentTime });
    // 在 Clock 中显示时间
    put(showTime(currentTime));
}

太美了!

总结

  • 容器和组件的区别
  • 按功能组织代码文件
  • 使用 CSS modules 和 PostCSS Auto Reset
  • 使用 redux-saga 来:
  1. 获得可读且可测试的异步流
  2. 将解耦组件连接到一起

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读