在React应用程序中提供服务

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

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

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

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

209486 次浏览

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

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

但在这些情况下,它总是会绑定到某个文本输入域。这就是它们应该耦合的地方。

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

这本质上与为逻辑提供服务/工厂的结果相同,但您将其直接耦合到输入。因此,现在您永远不需要告诉该函数在哪里查找它的验证输入,因为它是永久地绑定在一起的。

我和你处境相同。在你提到的情况下,我将输入验证UI组件实现为一个React组件。

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

也就是说,对于不应该耦合的逻辑,在单独的文件中使用JS模块/类,并使用require/import将组件从“服务”中解耦。

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

第一个答案没有反映当前的容器vs Presenter范例。

如果你需要做一些事情,比如验证密码,你可能会有一个函数来做这件事。你会把这个函数作为道具传递给你的可重用视图。

容器

因此,正确的方法是编写一个ValidatorContainer,它将该函数作为属性,并将表单包装在其中,将正确的道具传递给子对象。当涉及到视图时,验证器容器包装视图,视图使用容器逻辑。

验证可以在容器的属性中完成,但如果你使用第三方验证器,或任何简单的验证服务,你可以将该服务作为容器组件的属性,并在容器的方法中使用它。我已经为restful组件做过这样的操作,效果非常好。

供应商

如果需要进行更多的配置,则可以使用提供者/消费者模型。提供者是一种高级组件,它包装在顶层应用程序对象(您要挂载的对象)附近和下面的某个地方,并向上下文API提供它自己的一部分,或者在顶层配置的属性。然后,我将容器元素设置为使用上下文。

父/子上下文关系不必彼此接近,只是子上下文关系必须以某种方式继承。Redux以这种方式存储和React Router函数。我已经使用它为我的rest容器提供了一个根rest上下文(如果我没有提供自己的)。

(注意:上下文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存储区之上定义服务对象。在存储创建过程中,将服务注入中间件,中间件处理影响该服务的任何操作。

通过这种方式,我可以将rest .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)
}
}

同样的情况:在做了多个Angular项目后转向React,没有一个简单的方法通过DI提供服务似乎是一个缺失的部分(抛开服务的细节不谈)。

使用context和ES7装饰器,我们可以接近:

< a href = " https://jaysoo。ca / 2015/06/09 / react-contexts-and-dependency-injection / noreferrer“rel = > 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. 它(只)与国家管理有关吗?移动到
  3. 两者都有关系?移动到单独的文件,在组件中通过选择器和在坦克中消费。

有些人还使用特殊进行重用,但对我来说,以上几乎涵盖了所有的用例。另外,考虑使用鸭子来扩展状态管理,以保持关注点分离并以状态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,目前,一个推荐的(?)方法似乎是使用高阶组件:

高阶组件(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>
)

然后编写一个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'));

我创建了一个简单的演示

编辑:另一个演示是使用props来传递一个函数数组,这样你就可以在__abc0之间共享由多个验证函数组成的逻辑,比如:

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

Edit2: React 16.8+提供了一个新特性,这是另一种共享逻辑的好方法。

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>

这里有一个简单的例子:

或者你可以将类继承“http”注入到React组件中

通过道具对象。

  1. < p >更新:

    ReactDOM.render(<ReactApp data={app} />, document.getElementById('root'));
    
  2. Simply edit React Component ReactApp like this:

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

Service并不局限于Angular,即使在Angular2 +中也是如此,

Service只是helper函数的集合…

有很多方法可以创建它们并在应用程序中重用它们……

1)它们可以都是从js文件导出的分离函数,如下所示:

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


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

2)我们也可以使用工厂方法,比如,with function的集合…对于ES6,它可以是一个类而不是函数构造函数:

class myService {


constructor() {
this._data = null;
}


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


getMyService() {
return this._data;
}


}

在这种情况下,你需要用new key创建一个实例…

const myServiceInstance = new myService();

同样,在这种情况下,每个实例都有自己的生命周期,所以如果你想跨界共享它要小心,在这种情况下,你应该只导出你想要的实例……

3)如果你的函数和utils不会被共享,你甚至可以把它们放在React组件中,在这种情况下,就像function在React组件中一样…

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


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

4)另一种处理方法,可以使用回来的,它是你的临时存储,所以如果你的应用程序反应中有它,它可以帮助你使用许多Getter setter函数…它就像一个大的存储,可以跟踪你的状态,并可以在你的组件之间共享它,所以可以摆脱我们在服务中使用的getter setter的许多痛苦……

干燥的代码总是好的,并且不重复需要使用的东西来使代码可重用和可读,但是不要尝试在React应用中遵循Angular的方法,正如第4项所提到的,使用Redux可以减少你对服务的需求,并且你限制了对一些可重用的帮助函数的使用,如第1项……

当你意识到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文件。

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

如果你还在寻找像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世界中,我们有两种类型的逻辑。有状态和无状态。这是开始react时要掌握的主要概念。这里我们更新的状态应该是更新UI,而不是angular直接更新dom。这两种类型的逻辑是:

  1. 不依赖于状态变化,即静态逻辑不需要基于状态变化重新呈现某些内容。对于这种情况,只需创建常规的js文件,并像库或helper方法一样导入它们
  2. 如果你有一些代码依赖于状态,你需要重用它,那么两个选项- hocs和更新的钩子。钩子有点难以理解,但基本上,如果它们的内部状态发生变化,它们会强制它们的父对象重新呈现,因此任何有状态逻辑都可以在不同的组件中定义和重用,并且每个钩子实例都有自己的独立作用域。 这是一个理解状态和声明性组件的思维转变,但请随意在评论中提出后续问题

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

让我举个例子。假设我们有一个名为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 <></>
}