在React应用程序中使用服务

342

我来自Angular世界,在那里我可以将逻辑提取到服务/工厂中,并在控制器中使用它们。

我试图了解如何在React应用程序中实现相同的功能。

假设我有一个组件,用于验证用户的密码输入(其强度)。它的逻辑非常复杂,因此我不想在组件本身中编写它。

我应该在哪里编写这个逻辑?在使用flux时,在一个存储区吗?还是有更好的选择?


12
不。只有客户端逻辑,不应直接放在组件中。密码强度检查器只是一个例子。 - Dennis Nerush
6
如果你有许多这样的函数,你可以将它们存储在一个帮助文件中,并在组件文件中使用时只需引用它。如果这是一个仅与该组件相关的单个函数,无论其复杂程度如何,它都应该放在那里。 - Jesse Kernaghan
你可以使用 Context API 来完成像服务一样的任何事情。 - raythurnevoid
13个回答

259
当你意识到Angular服务只是提供一组与上下文无关的方法的对象时,这个问题就变得非常简单了。它只是Angular DI机制使其看起来更加复杂。虽然DI对于为您创建和维护实例很有用,但您并不真正需要它。
考虑一个叫做axios的流行AJAX库(你可能已经听说过):
import axios from "axios";
axios.post(...);

它不是作为一个服务行为吗?它提供一组负责某些特定逻辑的方法,并且与主代码无关。

你的例子是关于创建一个独立的方法集来验证你的输入(例如检查密码强度)。有人建议将这些方法放在组件内,但对我来说,这显然是一个反模式。如果验证涉及到进行和处理XHR后端调用或进行复杂的计算,那么将这种逻辑与鼠标单击处理程序和其他UI特定的东西混合在一起是荒谬的。容器/HOC方法也是如此。仅仅为了添加一个检查值中是否有数字的方法而包装你的组件?开玩笑。

我只会创建一个名为“ValidationService.js”的新文件,并按以下方式组织:

const ValidationService = {
    firstValidationMethod: function(value) {
        //inspect the value
    },

    secondValidationMethod: function(value) {
        //inspect the value
    }
};

export default ValidationService;

然后在你的组件中:

import ValidationService from "./services/ValidationService.js";

...

//inside the component
yourInputChangeHandler(event) {

    if(!ValidationService.firstValidationMethod(event.target.value) {
        //show a validation warning
        return false;
    }
    //proceed
}

您可以在任何地方使用此服务。如果验证规则更改,则只需关注 ValidationService.js 文件即可。

您可能需要一个依赖于其他服务的更复杂的服务。在这种情况下,您的服务文件可能会返回一个类构造函数,而不是静态对象,因此您可以在组件中自己创建对象实例。您还可以考虑实现一个简单的单例,以确保整个应用程序中始终只有一个服务对象实例在使用。


11
我也会这样做。我很惊讶这个答案的得票数这么少,因为这似乎是最省力的方法。如果您的服务依赖于其他服务,那么您需要通过它们的模块导入这些其他服务。此外,模块在定义上是单例的,所以实际上不需要进一步的工作来“将其实现为简单的单例”——您可以免费获得这种行为 :) - Mickey Puri
37
如果您只使用提供函数的服务,那么+1 - 这是一个很好的答案。然而,Angular的服务是定义一次的类,因此提供的功能远不止提供函数。例如,您可以将对象缓存为服务类参数。 - Nino Filiu
30
但是依赖注入呢?除非你以某种方式将其注入,否则在组件中无法模拟该服务。也许有一个顶级的“容器”全局对象,其中包含每个服务作为字段,可以解决这个问题。然后在你的测试中,你可以使用模拟对象覆盖容器字段,以模拟你想要模拟的服务。 - Andrew M.
2
@Defacto 针对这个问题的一个解决方案是使用响应式扩展(observables)。订阅从服务返回的可观察流,并使用Subjects将更改“推送”到组件中。就我个人而言,我更喜欢这个答案,因为它让我将业务逻辑移出组件,使我的组件尽可能小,并且不需要手动处理数据。较简单的部分=>较少的错误/更易于维护。 - RoboBear
2
我只能部分地同意这个观点。这种方法仅适用于无状态函数逻辑。如果要在内存中存储变量,通常需要使用Angular中的服务来实现该任务。 - Ajmal Moochingal
显示剩余5条评论

91
第一个答案没有反映当前的Container vs Presenter范例。
如果您需要做某些事情,比如验证密码,您可能会有一个执行此操作的函数。您将把该函数作为属性传递给可重用视图。
容器
因此,正确的方法是编写一个ValidatorContainer,它将具有该函数作为属性,并将表单包装在其中,传递正确的道具到子级中。当涉及到您的视图时,您的验证器容器包装了您的视图,视图使用容器的逻辑。
验证可以全部在容器的属性中完成,但是如果您正在使用第三方验证器或任何简单的验证服务,则可以将该服务作为容器组件的属性并在容器的方法中使用它。我已经为restful组件做过这个,它非常有效。
提供者
如果需要更多配置,您可以使用提供者/使用者模型。 提供者是一个高级组件,包装在靠近和下面的顶级应用程序对象(您安装的那个对象)附近,并将其自身的一部分或在顶层配置的属性提供给上下文API。 然后我设置我的容器元素以使用上下文。 父/子上下文关系不必相互靠近,只需以某种方式向下传递即可。 Redux存储和React路由器按此方式运行。 我已将其用于为我的rest容器提供根restful上下文(如果我没有自己提供)。 (注意:文档中标记上下文API为实验性,但考虑到正在使用的内容,我认为它已经不再是实验性的了。)

//An example of a Provider component, takes a preconfigured restful.js
//object and makes it available anywhere in the application
export default class RestfulProvider extends React.Component {
 constructor(props){
  super(props);

  if(!("restful" in props)){
   throw Error("Restful service must be provided");
  }
 }

 getChildContext(){
  return {
   api: this.props.restful
  };
 }

 render() {
  return this.props.children;
 }
}

RestfulProvider.childContextTypes = {
 api: React.PropTypes.object
};

中间件

另一种我没有尝试过但已经看到使用的方法是与Redux一起使用中间件。您在应用程序之外定义服务对象,或者至少高于redux存储区。在创建存储区时,将服务注入中间件,中间件处理任何影响服务的操作。

通过这种方式,我可以将我的restful.js对象注入中间件,并使用独立的操作替换容器方法。我仍然需要一个容器组件将操作提供给表单视图层,但connect()和mapDispatchToProps在那里为我提供了支持。

例如,新的v4 react-router-redux使用此方法来影响历史状态。

//Example middleware from react-router-redux
//History is our service here and actions change it.

import { CALL_HISTORY_METHOD } from './actions'

/**
 * This middleware captures CALL_HISTORY_METHOD actions to redirect to the
 * provided history object. This will prevent these actions from reaching your
 * reducer or any middleware that comes after this one.
 */
export default function routerMiddleware(history) {
  return () => next => action => {
    if (action.type !== CALL_HISTORY_METHOD) {
      return next(action)
    }

    const { payload: { method, args } } = action
    history[method](...args)
  }
}


容器示例的用途是什么? - sensei
我不是在推荐这样做,但如果你想要走服务定位器的路线(类似于Angular),你可以添加某种“注入器/容器”提供程序,从中解决服务(之前注册过它们)。 - eddiewould
1
React Hooks来拯救了我们。使用Hooks,您可以编写可重用的逻辑而无需编写类。https://reactjs.org/docs/hooks-custom.html?source=post_page--------------------------- - Raja Malik
1
太棒了!根据@RajaMalik的评论,我发现自己经常使用钩子来提供服务,并最终编写了一个小型库来封装这样做:https://github.com/traviskaufman/react-service-container - Travis Kaufman
1
我不同意所谓的“正确”方式。这是我在React中经常看到被滥用的一种模式。所有东西都变成了组件,并且XML的语义结构与应用程序/业务逻辑之间的界限变得模糊。这并不是说它们没有作用(我已经实现了<Form>,<Input>等包装器组件来构建自定义表单库),我只是不明白为什么纯js函数和服务在这里会更不合适。对于您的中间件解决方案也是如此,它假定存在一个集中式存储。这些类型的问题可以与框架解耦。 - GHOST-34
什么是“第一个答案”?请您的回答独立于投票或接受,通过直接链接到其他资源(如答案)来解释。 - isherwood

59

我需要一些格式化逻辑在多个组件之间共享,并且作为 Angular 开发者,自然而然地倾向于使用服务。

我通过将这些逻辑放入一个单独的文件中来进行共享。

function format(input) {
    //convert input to output
    return output;
}

module.exports = {
    format: format
};

然后将其作为模块进行导入

import formatter from '../services/formatter.service';

//then in component

    render() {

        return formatter.format(this.props.data);
    }

11
这是一个好主意,甚至在React文档中也提到了:https://reactjs.org/docs/composition-vs-inheritance.html如果你想在组件之间重复使用非UI功能,我们建议将其提取到一个单独的JavaScript模块中。这些组件可以导入它并使用其中的函数、对象或类,而无需扩展它。 - user3426603
3
这个回答中的依赖注入在哪里? - ZenVentzi
你如何在组件测试中进行模拟? - Mohsen
这在SSR中不起作用,因为它将是一个不安全的全局变量。 - Jonathan

33

请记住,React 的目的是更好地耦合那些在逻辑上应该被耦合在一起的东西。如果您正在设计一个复杂的“验证密码”方法,那么它应该与哪里相耦合?

好吧,每当用户需要输入新密码时,您都需要使用它。这可能出现在注册屏幕、“忘记密码”屏幕、管理员“为其他用户重置密码”屏幕等等。

但在任何这些情况下,它始终会与某个文本输入字段相关联。所以它应该与此相关联。

创建一个非常小的 React 组件,仅包含一个输入字段和相关的验证逻辑。在所有可能需要密码输入的表单中输入该组件。

这实际上与使用服务/工厂获得相同的结果,但是您将其直接与输入字段耦合在一起。因此,您现在永远不需要告诉该函数在哪里查找其验证输入,因为它们被永久地绑定在一起。


20
将逻辑与用户界面耦合在一起是不良的实践。为了更改逻辑,我必须修改组件。 - Dennis Nerush
15
React根本性地挑战了你所做的那种假设。它与传统的MVC架构形成鲜明对比。 这个视频相当好地解释了为什么会这样(相关部分从视频的第2分钟开始)。 - Jake Haller-Roby
15
如果相同的验证逻辑也需要应用于文本区域元素,那么该逻辑仍然需要提取到共享文件中。我认为在 React 库中没有任何等效项。Angular Service 是可注入的,Angular 框架构建在依赖注入设计模式之上,允许 Angular 管理依赖项的实例。当服务被注入时,通常会有一个单例在提供的范围内,为了在 React 中拥有相同的服务,需要引入第三方依赖注入库到应用程序中。 - Downhillski
24
我很喜欢使用React。这不是Angular的模式,而是软件设计模式。我喜欢在从其他优秀部分借用自己喜欢的东西的同时保持头脑开放。 - Downhillski
2
@MickeyPuri ES6模块不同于依赖注入。 - Spock
显示剩余6条评论

17

相同情况:已经完成了多个Angular项目并转向React,没有简单的方法通过DI提供服务似乎是一个缺失的部分(除了服务的细节)。

使用上下文和ES7装饰器,我们可以接近实现:

https://jaysoo.ca/2015/06/09/react-contexts-and-dependency-injection/

看起来这些人已经将其推向了更高层次/不同的方向:

http://blog.wolksoftware.com/dependency-injection-in-react-powered-inversifyjs

仍然感觉像是逆水行舟。完成一个重要的React项目后,将在6个月后重新访问此答案。

编辑:6个月后回来,有了更多的React经验。考虑逻辑的性质:

  1. 它是否只与UI相关?将其移入组件中(接受的答案)。
  2. 它是否只与状态管理相关?将其移入thunk中。
  3. 两者都有关联?将其移至单独的文件中,通过selector在组件中使用并在thunks中使用。

一些人也会使用HOCs进行重复使用,但对我来说,上述几点几乎涵盖了所有用例。此外,考虑使用ducks扩展状态管理,以保持关注点分离和状态UI为中心。


我认为通过使用ES6模块系统,提供依赖注入服务有一种简单的方法。 - Mickey Puri
2
@MickeyPuri,ES6 模块的 DI 不会包括 Angular DI 中的分层性质,即父组件(在 DOM 中)实例化和覆盖提供给子组件的服务。在我看来,ES6 模块的 DI 更接近于后端 DI 系统,例如 Ninject 和 Structuremap,与 DOM 组件层次结构相比独立存在,而不是基于其之上。但我很想听听你的想法。 - corolla

14

我也来自Angular.js领域,React.js中的服务和工厂更简单。

你可以使用普通函数或类、回调样式和像我一样的Mobx事件 :)

// Here we have Service class > dont forget that in JS class is Function
class HttpService {
  constructor() {
    this.data = "Hello data from HttpService";
    this.getData = this.getData.bind(this);
  }

  getData() {
    return this.data;
  }
}


// Making Instance of class > it's object now
const http = new HttpService();


// Here is React Class extended By React
class ReactApp extends React.Component {
  state = {
    data: ""
  };

  componentDidMount() {
    const data = http.getData();

    this.setState({
      data: data
    });
  }

  render() {
    return <div>{this.state.data}</div>;
  }
}

ReactDOM.render(<ReactApp />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>
  
  <div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>

</body>
</html>

这里是一个简单的例子:


1
React.js是一个UI库,用于渲染和组织UI组件。当涉及到可以帮助我们添加额外功能的服务时,我们应该创建函数、功能对象或类的集合。我发现类非常有用,但现在我也尝试使用函数式风格来创建助手,以添加超出React.js范畴的高级功能。 - Juraj

12

我也来自Angular,正在尝试React。目前,一种推荐的方法似乎是使用高阶组件

高阶组件(HOC)是React中的一种高级技术,用于重复使用组件逻辑。 HOC不是React API的一部分,它们是从React的构成性质中出现的模式。

假设您有一个inputtextarea,想要应用相同的验证逻辑:

const Input = (props) => (
  <input type="text"
    style={props.style}
    onChange={props.onChange} />
)
const TextArea = (props) => (
  <textarea rows="3"
    style={props.style}
    onChange={props.onChange} >
  </textarea>
)

然后编写一个高阶组件,对包装的组件进行验证和样式设置:

function withValidator(WrappedComponent) {
  return class extends React.Component {
    constructor(props) {
      super(props)

      this.validateAndStyle = this.validateAndStyle.bind(this)
      this.state = {
        style: {}
      }
    }

    validateAndStyle(e) {
      const value = e.target.value
      const valid = value && value.length > 3 // shared logic here
      const style = valid ? {} : { border: '2px solid red' }
      console.log(value, valid)
      this.setState({
        style: style
      })
    }

    render() {
      return <WrappedComponent
        onChange={this.validateAndStyle}
        style={this.state.style}
        {...this.props} />
    }
  }
}

现在这些高阶组件共享相同的验证行为:

const InputWithValidator = withValidator(Input)
const TextAreaWithValidator = withValidator(TextArea)

render((
  <div>
    <InputWithValidator />
    <TextAreaWithValidator />
  </div>
), document.getElementById('root'));

我创建了一个简单的演示

编辑:另一个演示使用props传递函数数组,以便您可以跨HOC共享由多个验证函数组成的逻辑:

<InputWithValidator validators={[validator1,validator2]} />
<TextAreaWithValidator validators={[validator1,validator2]} />

编辑2:React 16.8+提供了一个新功能,Hook,这是另一种很好的共享逻辑的方式。

const Input = (props) => {
  const inputValidation = useInputValidation()

  return (
    <input type="text"
    {...inputValidation} />
  )
}

function useInputValidation() {
  const [value, setValue] = useState('')
  const [style, setStyle] = useState({})

  function handleChange(e) {
    const value = e.target.value
    setValue(value)
    const valid = value && value.length > 3 // shared logic here
    const style = valid ? {} : { border: '2px solid red' }
    console.log(value, valid)
    setStyle(style)
  }

  return {
    value,
    style,
    onChange: handleChange
  }
}

https://stackblitz.com/edit/react-shared-validation-logic-using-hook?file=index.js


谢谢,我从这个解决方案中真的学到了很多。如果我需要有多个验证器怎么办。例如除了3个字母验证器之外,如果我想要另一个验证器来确保没有数字被输入,我们能组合验证器吗? - Yousof Sharief
1
@YoussefSherif 你可以准备多个验证函数并将它们作为 HOC 的 props 传递,查看我的编辑以获取另一个演示。 - bob
那么HOC基本上是容器组件吗? - sensei
是的,来自React文档的说明:“请注意,HOC不会修改输入组件,也不会使用继承来复制其行为。相反,HOC通过将原始组件包装在容器组件中来组合原始组件。HOC是一个纯函数,没有副作用。” - bob
1
需求是注入逻辑,我不明白为什么我们需要一个高阶组件来实现这个。虽然你可以用高阶组件来完成它,但感觉有点过于复杂了。我的理解是,只有在需要添加和管理一些额外状态时才需要使用高阶组件,而不是纯粹的逻辑(这在这里是适用的情况)。 - Mickey Puri
@MickeyPuri 在我的代码中有状态是你称其为“不纯”的原因吗? - bob

3

服务不仅限于Angular,即使在Angular2+中,

服务只是一组辅助函数的集合...

有许多方法可以创建它们并在整个应用程序中重复使用...

1) 它们可以是所有分离的函数,这些函数从一个js文件中导出,类似于下面的示例:

export const firstFunction = () => {
   return "firstFunction";
}

export const secondFunction = () => {
   return "secondFunction";
}
//etc

2) 我们也可以使用工厂方法,例如,使用函数集合...在ES6中,它可以是一个类而不是函数构造器:

class myService {

  constructor() {
    this._data = null;
  }

  setMyService(data) {
    this._data = data;
  }

  getMyService() {
    return this._data;
  }

}

在这种情况下,您需要使用新的密钥创建一个实例...
const myServiceInstance = new myService();

同样的,在这种情况下,每个实例都有自己的生命周期,因此如果您想共享它,请小心,这种情况下您应该只导出您想要的实例...

3) 如果您的函数和工具不会被共享,甚至可以将它们放在React组件中,在这种情况下,就像您React组件中的函数一样...

class Greeting extends React.Component {
  getName() {
    return "Alireza Dezfoolian";
  }

  render() {
    return <h1>Hello, {this.getName()}</h1>;
  }
}

4) 另一种处理方式是使用Redux,它是一个临时存储器,如果你在React应用程序中使用它,它可以帮助你处理许多getter setter函数... 它就像一个大型存储器,可以跟踪你的状态并在组件之间共享,因此可以摆脱服务中使用的许多getter setter问题...

DRY代码是一种很好的做法,避免重复代码以使代码可重用和易读性高,但是不要试图在React应用程序中遵循Angular的方式,如第4项所述,使用Redux可以减少服务的需要,并仅限于使用类似于item 1的可重用辅助函数...


当然,你可以在我的个人网站上找到它,我的个人网站链接在我的资料页面上... - Alireza
1
不要在React中遵循Angular的方式。咳咳,Angular推广使用Redux,并使用Observables将存储流传递到表示组件和类似RxJS/Store的Redux状态管理。你是指AngularJS吗?因为那是另一回事。 - Spock
1
不要试图追随Angular的方式。不能同意,虽然在Angular 2+中状态管理更好,但应尽可能重用最佳实践。 - Ievgen

3
如果您仍在寻找类似Angular的服务,可以尝试使用react-rxbuilder库。您可以使用@Injectable来注册该服务,然后在组件中使用useServiceCountService.ins来使用该服务。
import { RxService, Injectable, useService } from "react-rxbuilder";

@Injectable()
export class CountService {
  static ins: CountService;

  count = 0;
  inc() {
    this.count++;
  }
}

export default function App() {
  const [s] = useService(CountService);
  return (
    <div className="App">
      <h1>{s.count}</h1>
      <button onClick={s.inc}>inc</button>
    </div>
  );
}

// Finally use `RxService` in your root component
render(<RxService>{() => <App />}</RxService>, document.getElementById("root"));

注意事项

  • 依赖于rxjs和typescript
  • 服务中不能使用箭头函数

2

我和你一样处于同样的情况。在你提到的情况下,我会把输入验证UI组件实现为一个React组件。

我认为验证逻辑本身的实现不应该(必须)耦合。因此,我会将其放入一个单独的JS模块中。

也就是说,对于不应该耦合的逻辑,请使用单独文件中的JS模块/类,并使用require/import来解耦组件与“服务”之间的关系。

这样可以进行依赖注入并对两者进行单元测试。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接