在 React 应用程序中提供服务

Having services in React application

我来自 angular 世界,在那里我可以将逻辑提取到 service/factory 并在我的控制器中使用它们。

我正在尝试了解如何在 React 应用程序中实现相同的目标。

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

我应该在哪里写这个逻辑?如果我在商店里使用助焊剂?或者有更好的选择吗?

请记住,React 的目的是更好地耦合逻辑上应该耦合的事物。如果你正在设计一个复杂的 "validate password" 方法,应该在哪里耦合它?

好吧,每次用户需要输入新密码时,您都需要使用它。这可能在注册屏幕、"forgot password" 屏幕、管理员 "reset password for another user" 屏幕等

但在任何一种情况下,它总是会与某些文本输入字段相关联。所以这就是它应该耦合的地方。

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

它与逻辑的 service/factory 本质上是相同的结果,但是您将它直接耦合到输入。因此,您现在永远不需要告诉该函数在哪里寻找它的验证输入,因为它是永久绑定在一起的。

我和你在同一条船上。在您提到的情况下,我会将输入验证 UI 组件实现为 React 组件。

我同意验证逻辑本身的实现应该(必须)不耦合。所以我会把它放在一个单独的 JS 模块中。

也就是说,对于不应耦合的逻辑,在单独的文件中使用 JS module/class,并使用 require/import 将组件与“服务”分离。

这允许独立地对两者进行依赖注入和单元测试。

第一个答案没有反映当前的 Container vs Presenter 范式。

如果您需要执行某些操作,例如验证密码,您可能需要一个函数来执行此操作。你会将该函数作为道具传递给你的可重用视图。

容器

因此,正确的做法是编写一个 ValidatorContainer,它将具有 属性 的功能,并将表单包装在其中,将正确的道具传递给 child.当涉及到您的视图时,您的验证器容器会包装您的视图,而视图会使用容器逻辑。

验证可以全部在容器的属性中完成,但如果您使用的是第 3 方验证器或任何简单的验证服务,您可以将该服务用作容器组件的 属性 并使用它在容器的方法中。我已经为 restful 组件完成了此操作,并且效果很好。

提供商

如果需要更多配置,您可以使用 Provider/Consumer 模型。提供者是一个高级组件,它包裹在靠近顶层应用程序 object(您挂载的应用程序)和下方的某处,并提供其自身的一部分,或在顶层配置的 属性 到上下文 API。然后我设置我的容器元素来使用上下文。

parent/child 上下文关系不必彼此靠近,只是 child 必须以某种方式下降。 Redux 以这种方式存储和 React Router 功能。我用它为我的其余容器提供根 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 结合使用。您在应用程序外部定义您的服务 object,或者至少,高于 redux 存储。在商店创建期间,您将服务注入中间件,中间件处理影响服务的任何操作。

通过这种方式,我可以将我的 restful.js object 注入到中间件中,并用独立的操作替换我的容器方法。我仍然需要一个容器组件来为表单视图层提供操作,但是 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)
  }
}

同样的情况:完成了多个 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 for reuse but for me the above covers almost all use cases. Also, consider scaling state management using ducks 来保持关注的独立性并以 UI 为中心。

我需要一些格式化逻辑在多个组件之间共享,作为一个 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);
    }

我也来自 Angular 并且正在尝试 React,截至目前,一种推荐的(?)方式似乎是使用 High-Order Components:

A higher-order component (HOC) is an advanced technique in React for reusing component logic. HOCs are not part of the React API, per se. They are a pattern that emerges from React’s compositional nature.

假设您有 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>
)

然后编写一个 HOC 来验证和设置包装组件的样式:

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} />
    }
  }
}

现在这些 HOC 共享相同的验证行为:

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

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

我创建了一个简单的 demo

编辑:另一个 demo 正在使用 props 传递函数数组,以便您可以在 HOC 之间共享由多个验证函数组成的逻辑喜欢:

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

Edit2: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

我也是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>

这是一个简单的例子:

或者您可以将 class 继承 "http" 注入 React 组件

通过道具对象。

  1. 更新:

    ReactDOM.render(<ReactApp data={app} />, document.getElementById('root'));
    
  2. 只需像这样编辑 React 组件 ReactApp:

    class ReactApp extends React.Component {
    
    state = {
    
        data: ''
    
    }
    
        render(){
    
        return (
            <div>
            {this.props.data.getData()}      
            </div>
    
        )
        }
    }
    

服务不限于Angular,甚至在Angular2+

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

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

1) 都可以是js文件导出的分离函数,类似如下:

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

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

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

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 可以减少您对服务的需求,并且您限制将它们用于某些可重用的辅助函数,例如第 1 项...

当您意识到 Angular 服务只是一个提供一组与上下文无关的方法的对象时,问题就变得非常简单了。只是 Angular DI 机制让它看起来更复杂。 DI 很有用,因为它负责为您创建和维护实例,但您并不真正需要它。

考虑一个名为 axios 的流行 AJAX 库(您可能听说过):

import axios from "axios";
axios.post(...);

它不作为服务吗?它提供了一组方法负责一些特定的逻辑,并且独立于主代码。

您的示例案例是关于创建一组独立的方法来验证您的输入(例如检查密码强度)。有人建议将这些方法放在组件中,这对我来说显然是一种反模式。如果验证涉及进行和处理 XHR 后端调用或进行复杂计算怎么办?您会将此逻辑与鼠标单击处理程序和其他 UI 特定内容混合使用吗?废话。与 container/HOC 方法相同。包装你的组件只是为了添加一个方法来检查值中是否有数字?加油

我会创建一个名为 say '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 文件。

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

如果您仍在寻找类似 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
  • 不能在服务中使用箭头函数

派对可能迟到了,但这是我的两分钱: 在反应世界中,我们有两种类型的逻辑。有状态和无状态。这是开始使用 React 时要掌握的主要概念。在这里我们更新状态应该更新 UI 而不是 angular 直接更新 dom。两种类型的逻辑是:

  1. 不依赖于状态变化,即不需要根据状态变化重新渲染某些东西的静态逻辑。对于这种情况,只需创建常规 js 文件并像库或辅助方法一样导入它们
  2. 如果您有一些依赖于状态的代码并且您需要重新使用它,那么有两个选择 - hocs 和更新的钩子。钩子有点难以理解,但基本上,如果它们的内部状态发生变化,它们会强制其父对象重新渲染,因此可以在不同的组件中定义和重用任何有状态逻辑,并且每个钩子实例都有自己的隔离范围。 理解状态和声明组件有点思维转变,但请随时在评论中提出后续问题

可以使用 export 关键字来使用包含必要方法的文件中的函数。

让我举个例子。假设我们有一个名为 someService.ts:

的文件
export const foo = (formId: string) => {
    // ... the code is omitted for the brevity
}


export const bar = (): Entity[] => [
    // ... the code is omitted for the brevity
]

export default {
    foo,
    bar,
}

然后我们可以像这样在组件中使用这个服务:

import {
    foo,
    bar,
} from './someService'

const InnerOrderModal: FC = observer(() => {
    const handleFormClick = (value: unknown, item: any) => {
    foo(item.key)
    bar()
    
    return <></>
}