react 个人学习过程


安装

npx create-react-app your-app

格式化

Prettier - Code formatter

或者右下角语言选JavascriptReact或者TypescriptReact

vscode开发相关配置

https://blog.csdn.net/weixin_40461281/article/details/79964659

jsx语法

{  }
布尔类型、Null 以及 Undefined 将会忽略

值得注意的是有一些 “falsy” 值,如数字 0,仍然会被 React 渲染。例如,以下代码并不会像你预期那样工作,因为当 props.messages 是空数组时,0 仍然会被渲染:

<div>
  {props.messages.length &&
    <MessageList messages={props.messages} />
  }
</div>

要解决这个问题,确保 && 之前的表达式总是布尔值:


<div>
  {props.messages.length > 0 &&
    <MessageList messages={props.messages} />
  }
</div>

State & 生命周期

State 的更新可能是异步的

出于性能考虑,React 可能会把多个 setState() 调用合并成一个调用。

因为 this.props 和 this.state 可能会异步更新,所以你不要依赖他们的值来更新下一个状态。

可以让 setState() 接收一个函数而不是一个对象。这个函数用上一个 state 作为第一个参数,将此次更新被应用时的 props 做为第二个参数

// Wrong
this.setState({
  counter: this.state.counter + this.props.increment,
});


// Correct
this.setState((state, props) => ({
  counter: state.counter + props.increment
}));

列表 & Key

在 map() 方法中的元素需要设置 key 属性。

表单

受控组件

class EssayForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: '请撰写一篇关于你喜欢的 DOM 元素的文章.'
    };

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }

  handleSubmit(event) {
    alert('提交的文章: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          文章:
          <textarea value={this.state.value} onChange={this.handleChange} />
        </label>
        <input type="submit" value="提交" />
      </form>
    );
  }
}

非受控组件

希望 React 能赋予组件一个初始值,但是不去控制后续的更新。 在这种情况下, 你可以指定一个 defaultValue 属性,而不是 value。

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.input = React.createRef();
  }

  handleSubmit(event) {
    alert('A name was submitted: ' + this.input.current.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input defaultValue="Bob" type="text" ref={this.input} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

状态提升 (props)

在 React 应用中,任何可变数据应当只有一个相对应的唯一“数据源”。通常,state 都是首先添加到需要渲染数据的组件中去。然后,如果其他组件也需要这个 state,那么你可以将它提升至这些组件的最近共同父组件中。你应当依靠自上而下的数据流,而不是尝试在不同组件间同步 state。

懒加载组件 React.lazy 需要配合Suspense使用

const OtherComponent = React.lazy(() => import('./OtherComponent'));

然后应在 Suspense 组件中渲染 lazy 组件,如此使得我们可以使用在等待加载 lazy 组件时做优雅降级(如 loading 指示器等)。

import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import React, { Suspense, lazy } from 'react';

const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));

const App = () => (
  <Router>
    <Suspense fallback={<div>Loading...</div>}>
      <Switch>
        <Route exact path="/" component={Home}/>
        <Route path="/about" component={About}/>
      </Switch>
    </Suspense>
  </Router>
);

错误边界(Error Boundaries)

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染能够显示降级后的 UI
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // 你同样可以将错误日志上报给服务器
    logErrorToMyService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // 你可以自定义降级后的 UI 并渲染
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children; 
  }
}

然后你可以将它作为一个常规组件去使用

<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>

Context

Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。顶层创建context

theme-context.js

export const themes = {
  light: {
    foreground: '#000000',
    background: '#eeeeee',
  },
  dark: {
    foreground: '#ffffff',
    background: '#222222',
  },
};

export const ThemeContext = React.createContext(
  themes.dark // 默认值
);

themed-button.js

import {ThemeContext} from './theme-context';

class ThemedButton extends React.Component {
  render() {
    let props = this.props;
    let theme = this.context;
    return (
      <button
        {...props}
        style={{backgroundColor: theme.background}}
      />
    );
  }
}
ThemedButton.contextType = ThemeContext;

export default ThemedButton;

app.js

import {ThemeContext, themes} from './theme-context';
import ThemedButton from './themed-button';

// 一个使用 ThemedButton 的中间组件
function Toolbar(props) {
  return (
    <ThemedButton onClick={props.changeTheme}>
      Change Theme
    </ThemedButton>
  );
}

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      theme: themes.light,
    };

    this.toggleTheme = () => {
      this.setState(state => ({
        theme:
          state.theme === themes.dark
            ? themes.light
            : themes.dark,
      }));
    };
  }

  render() {
    // 在 ThemeProvider 内部的 ThemedButton 按钮组件使用 state 中的 theme 值,
    // 而外部的组件使用默认的 theme 值
    return (
      <Page>
        <ThemeContext.Provider value={this.state.theme}>
          <Toolbar changeTheme={this.toggleTheme} />
        </ThemeContext.Provider>
        <Section>
          <ThemedButton />
        </Section>
      </Page>
    );
  }
}

ReactDOM.render(<App />, document.root);

在 ThemeProvider 内部的 ThemedButton 按钮组件使用 state 中的 theme 值,
而外部的组件使用默认的 theme 值

更新 Context

  1. 动态 Context ---> 修改 传入的value(state)

顶部组件


import React, { Component } from 'react'
import Welcome from './components/welcome';
import TodoList from './components/TodoList';
import './App.css';
import {themes, ThemeContext} from './config/context'

export default class App extends Component {
  constructor(props) {
    super(props);
    // 改变state的方法 传递给context
    this.toggleTheme = (change) => {
      this.setState((state)=> (change))
    }
    this.state = {
      date: 123,
      mark: false,
      theme: themes.light,
      toggleTheme: this.toggleTheme
    }
  }


  render() {
    return (
      <ThemeContext.Provider value={this.state}>
        <div className="App">
          {this.state.date}
          <header className="App-header">
            <Welcome />
            <TodoList />
          </header> 
        </div>
      </ThemeContext.Provider>
    )
  }
}

内部组件

import React, { Component } from 'react'
import {themes, ThemeContext} from '../config/context'
class Model extends Component {
  static contextType = ThemeContext
  constructor(props) {
    super(props)
    this.state = {}
  }
  toggle = () => {
  // 修改 顶部state
    this.context.toggleTheme({
      theme: themes.dark,
      date: 111111111111
    })
  }
  render() {
    return (
      <div>
        {this.context.theme.background}
        <button onClick={this.toggle}>toggle</button>
      </div>
    )
  }
}

export default Model
  1. 在嵌套组件中更新 Context

你可以通过 context 传递一个函数,使得 consumers 组件更新 context:

theme-context.js

// 确保传递给 createContext 的默认值数据结构是调用的组件(consumers)所能匹配的!
export const ThemeContext = React.createContext({
  theme: themes.dark,
  toggleTheme: () => {},
});

theme-toggler-button.js

import {ThemeContext} from './theme-context';

function ThemeTogglerButton() {
  // Theme Toggler 按钮不仅仅只获取 theme 值,它也从 context 中获取到一个 toggleTheme 函数
  return (
    <ThemeContext.Consumer>
      {({theme, toggleTheme}) => (
        <button
          onClick={toggleTheme}
          style={{backgroundColor: theme.background}}>

          Toggle Theme
        </button>
      )}
    </ThemeContext.Consumer>
  );
}

export default ThemeTogglerButton;

app.js

import {ThemeContext, themes} from './theme-context';
import ThemeTogglerButton from './theme-toggler-button';

class App extends React.Component {
  constructor(props) {
    super(props);

    this.toggleTheme = () => {
      this.setState(state => ({
        theme:
          state.theme === themes.dark
            ? themes.light
            : themes.dark,
      }));
    };

    // State 也包含了更新函数,因此它会被传递进 context provider。
    this.state = {
      theme: themes.light,
      toggleTheme: this.toggleTheme,
    };
  }

  render() {
    // 整个 state 都被传递进 provider
    return (
      <ThemeContext.Provider value={this.state}>
        <Content />
      </ThemeContext.Provider>
    );
  }
}

function Content() {
  return (
    <div>
      <ThemeTogglerButton />
    </div>
  );
}

ReactDOM.render(<App />, document.root);

视图层框架 react

setState( , cb) 第二个参数 回调函数中Dom已更新

ref

ref 获取 Dom节点

回调函数里参数获取是Dom节点

  1. 回调函数
<input type="number" name="num" id=""  value={this.state.num} onChange={this.setVal}  ref={(input) => { this.input = input }} />
  1. 用React.createRef()接收
this.file = React.createRef()

<input type="file" ref={this.file}/>

在 JSX 类型中使用点语法

当你在一个模块中导出许多 React 组件时,这会非常方便。例如,如果 MyComponents.DatePicker 是一个组件,你可以在 JSX 中直接使用:


import React from 'react';

const MyComponents = {
  DatePicker: function DatePicker(props) {
    return <div>Imagine a {props.color} datepicker here.</div>;
  }
}

function BlueDatePicker() {
  return <MyComponents.DatePicker color="blue" />;
}

性能优化 - shouldComponentUpdate

  1. shouldComponentUpdate
 shouldComponentUpdate(nextProps, nextState) {
    if (this.props.color !== nextProps.color) {
      return true;
    }
    if (this.state.count !== nextState.count) {
      return true;
    }
    return false;
  }
  1. 继承React.PureComponent state是immutable对象优先
class CounterButton extends React.PureComponent {

    
}

react-transition-group

CSSTransition

appear 入场动画

unmountOnExit 动画结束关闭 none

钩子函数 onEnter onEntering onEntered (onExit)

HOC (使用 HOC 解决横切关注点问题 之前是mixins )

// 此函数接收一个组件...
function withSubscription(WrappedComponent, selectData) {
  // ...并返回另一个组件...
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.state = {
        data: selectData(DataSource, props)
      };
    }

    componentDidMount() {
      // ...负责订阅相关的操作...
      DataSource.addChangeListener(this.handleChange);
    }

    componentWillUnmount() {
      DataSource.removeChangeListener(this.handleChange);
    }

    handleChange() {
      this.setState({
        data: selectData(DataSource, this.props)
      });
    }

    render() {
      // ... 并使用新数据渲染被包装的组件!
      // 请注意,我们可能还会传递其他属性
      return <WrappedComponent data={this.state.data} {...this.props} />;
    }
  };
}
注意事项
  • 不要在 render 方法中使用 HOC
  • 约定:将不相关的 props 传递给被包裹的组件
  • 约定:包装显示名称以便轻松调试
function withSubscription(WrappedComponent) {
  class WithSubscription extends React.Component {/* ... */}
  WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
  return WithSubscription;
}

function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}

Render Props

术语 “render prop” 是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术

class Cat extends React.Component {
  render() {
    const mouse = this.props.mouse;
    return (
      <img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
    );
  }
}

class MouseWithCat extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>

        {/*
          我们可以在这里换掉 <p> 的 <Cat>   ......
          但是接着我们需要创建一个单独的 <MouseWithSomethingElse>
          每次我们需要使用它时,<MouseWithCat> 是不是真的可以重复使用.
        */}
        <Cat mouse={this.state} />
      </div>
    );
  }
}

class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>移动鼠标!</h1>
        <MouseWithCat />
      </div>
    );
  }
}

注意事项: 将 Render Props 与 React.PureComponent 一起使用时要小心

如果你在 render 方法里创建函数,那么使用 render prop 会抵消使用 React.PureComponent 带来的优势。因为浅比较 props 的时候总会得到 false,并且在这种情况下每一个 render 对于 render prop 将会生成一个新的值。

class Mouse extends React.PureComponent {
  // 与上面相同的代码......
}

class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>Move the mouse around!</h1>

        {/*
          这是不好的!
          每个渲染的 `render` prop的值将会是不同的。 每次返回props.mouse是新函数
        */}
        <Mouse render={mouse => (
          <Cat mouse={mouse} />
        )}/>
      </div>
    );
  }
}

为了绕过这一问题,有时你可以定义一个 prop 作为实例方法,类似这样:

class MouseTracker extends React.Component {
  // 定义为实例方法,`this.renderTheCat`始终
  // 当我们在渲染中使用它时,它指的是相同的函数
  renderTheCat(mouse) {
    return <Cat mouse={mouse} />;
  }

  render() {
    return (
      <div>
        <h1>Move the mouse around!</h1>
        <Mouse render={this.renderTheCat} />
      </div>
    );
  }
}

PropTypes

import PropTypes from 'prop-types';

MyComponent.propTypes = {
  // 你可以将属性声明为 JS 原生类型,默认情况下
  // 这些属性都是可选的。
  optionalArray: PropTypes.array,
  optionalBool: PropTypes.bool,
  optionalFunc: PropTypes.func,
  optionalNumber: PropTypes.number,
  optionalObject: PropTypes.object,
  optionalString: PropTypes.string,
  optionalSymbol: PropTypes.symbol,

  // 任何可被渲染的元素(包括数字、字符串、元素或数组)
  // (或 Fragment) 也包含这些类型。
  optionalNode: PropTypes.node,

  // 一个 React 元素。
  optionalElement: PropTypes.element,

  // 一个 React 元素类型(即,MyComponent)。
  optionalElementType: PropTypes.elementType,

  // 你也可以声明 prop 为类的实例,这里使用
  // JS 的 instanceof 操作符。
  optionalMessage: PropTypes.instanceOf(Message),

  // 你可以让你的 prop 只能是特定的值,指定它为
  // 枚举类型。
  optionalEnum: PropTypes.oneOf(['News', 'Photos']),

  // 一个对象可以是几种类型中的任意一个类型
  optionalUnion: PropTypes.oneOfType([
    PropTypes.string,
    PropTypes.number,
    PropTypes.instanceOf(Message)
  ]),

  // 可以指定一个数组由某一类型的元素组成
  optionalArrayOf: PropTypes.arrayOf(PropTypes.number),

  // 可以指定一个对象由某一类型的值组成
  optionalObjectOf: PropTypes.objectOf(PropTypes.number),

  // 可以指定一个对象由特定的类型值组成
  optionalObjectWithShape: PropTypes.shape({
    color: PropTypes.string,
    fontSize: PropTypes.number
  }),
  
  // An object with warnings on extra properties
  optionalObjectWithStrictShape: PropTypes.exact({
    name: PropTypes.string,
    quantity: PropTypes.number
  }),   

  // 你可以在任何 PropTypes 属性后面加上 `isRequired` ,确保
  // 这个 prop 没有被提供时,会打印警告信息。
  requiredFunc: PropTypes.func.isRequired,

  // 任意类型的数据
  requiredAny: PropTypes.any.isRequired,

  // 你可以指定一个自定义验证器。它在验证失败时应返回一个 Error 对象。
  // 请不要使用 `console.warn` 或抛出异常,因为这在 `onOfType` 中不会起作用。
  customProp: function(props, propName, componentName) {
    if (!/matchme/.test(props[propName])) {
      return new Error(
        'Invalid prop `' + propName + '` supplied to' +
        ' `' + componentName + '`. Validation failed.'
      );
    }
  },

  // 你也可以提供一个自定义的 `arrayOf` 或 `objectOf` 验证器。
  // 它应该在验证失败时返回一个 Error 对象。
  // 验证器将验证数组或对象中的每个值。验证器的前两个参数
  // 第一个是数组或对象本身
  // 第二个是他们当前的键。
  customArrayProp: PropTypes.arrayOf(function(propValue, key, componentName, location, propFullName) {
    if (!/matchme/.test(propValue[key])) {
      return new Error(
        'Invalid prop `' + propFullName + '` supplied to' +
        ' `' + componentName + '`. Validation failed.'
      );
    }
  })
};

React.memo

React.memo 为高阶组件。它与 React.PureComponent 非常相似,但它适用于函数组件,但不适用于 class 组件。

const MyComponent = React.memo(function MyComponent(props) {
  /* 使用 props 渲染 */
});

默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。

function MyComponent(props) {
  /* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
  /*
  如果把 nextProps 传入 render 方法的返回结果与
  将 prevProps 传入 render 方法的返回结果一致则返回 true,
  否则返回 false
  */
}
export default React.memo(MyComponent, areEqual);

Redux

安装

yarn add redux

调试工具 redux-devtools

https://github.com/zalmoxisus/redux-devtools-extension#installation

开启redux-devtools

 const store = createStore(
   reducer, /* preloadedState, */
+  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
 );

要点

import { createStore } from 'redux'

/**
 * 这是一个 reducer,形式为 (state, action) => state 的纯函数。
 * 描述了 action 如何把 state 转变成下一个 state。
 *
 * state 的形式取决于你,可以是基本类型、数组、对象、
 * 甚至是 Immutable.js 生成的数据结构。惟一的要点是
 * 当 state 变化时需要返回全新的对象,而不是修改传入的参数。
 *
 * 下面例子使用 `switch` 语句和字符串来做判断,但你可以写帮助类(helper)
 * 根据不同的约定(如方法映射)来判断,只要适用你的项目即可。
 */
function counter(state = 0, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    default:
      return state
  }
}

// 创建 Redux store 来存放应用的状态。
// API 是 { subscribe, dispatch, getState }。
let store = createStore(counter)

// 可以手动订阅更新,也可以事件绑定到视图层。
store.subscribe(() => console.log(store.getState()))

// 改变内部 state 惟一方法是 dispatch 一个 action。
// action 可以被序列化,用日记记录和储存下来,后期还可以以回放的方式执行
store.dispatch({ type: 'INCREMENT' })
// 1
subscribe, dispatch, getState的使用
import React, { PureComponent } from 'react'

import { List, Typography, Button } from 'antd';
import store from '../store/index'



export default class Antd extends PureComponent {
  constructor(props){
    super(props)
    this.state = {
      data: store.getState().list
    }
    // store变化监听回调
    store.subscribe(() => this.handChangeState())
  }
  add = (li) => {
    console.log(li)
    store.dispatch({type: 'ADD_LIST', payload: li})
  }
  // 更新视图
  handChangeState = () => {
    this.setState({
      data: store.getState().list
    })
  }
  render() {
    return (
      <div>
        <Button onClick={this.add.bind(this, 'Los Angeles battles huge wildfires.')}>add</Button>
        <h3 style={{ marginBottom: 16 }}>Default Size</h3>
        <List
          header={<div>Header</div>}
          footer={<div>Footer</div>}
          bordered
          dataSource={this.state.data}
          renderItem={item => (
            <List.Item>
              <Typography.Text mark>[ITEM]</Typography.Text> {item}
            </List.Item>
          )}
        />
       
      </div>
    )
  }
}


reducer 只是一个接收 state 和 action,并返回新的 state 的函数

对于大的应用来说,不大可能仅仅只写一个这样的函数,所以我们编写很多小函数来分别管理 state 的一部分:

action

把action.type 抽离出来 actionTypes.js

书写错误时会有提示.

// 报错提示
const ADD_LIST = 'ADD_LIST'
actionCreator

生成 action creator 的函数:减少多余的样板代码


function makeActionCreator(type, ...argNames) {
  return function(...args) {
    const action = { type }
    argNames.forEach((arg, index) => {
      action[argNames[index]] = args[index]
    })
    return action
  }
}

const ADD_TODO = 'ADD_TODO'
const EDIT_TODO = 'EDIT_TODO'
const REMOVE_TODO = 'REMOVE_TODO'

export const addTodo = makeActionCreator(ADD_TODO, 'text')
export const editTodo = makeActionCreator(EDIT_TODO, 'id', 'text')
export const removeTodo = makeActionCreator(REMOVE_TODO, 'id')

reducer

combineReducers
import { combineReducers } from 'redux'

export default combineReducers({
  visibilityFilter,
  todos
})

上面的写法和下面完全等价:

export default function todoApp(state = {}, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action),
    todos: todos(state.todos, action)
  }
}

store

Store 有以下职责:

  • 维持应用的 state;
  • 提供 getState() 方法获取 state;
  • 提供 dispatch(action) 方法更新 state;
  • 通过 subscribe(listener) 注册监听器;
  • 通过 subscribe(listener) 返回的函数注销监听器。
createStore

createStore() 的第二个参数是可选的, 用于设置 state 初始状态 使用服务器state时

react-redux

使用 connect() 前,需要先定义 mapStateToProps 这个函数来指定如何把当前 Redux store state 映射到展示组件的 props 中。

const mapStateToProps = state => ({
  data: state.list
})

除了读取 state,容器组件还能分发 action。类似的方式,可以定义 mapDispatchToProps() 方法接收 dispatch() 方法并返回期望注入到展示组件的 props 中的回调方法

const mapDispatchToProps = (dispatch) => {
  return {
    addList: (li) => {
      dispatch(addList(li))
    }
  }
}

connect()

import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import { List, Typography, Button } from 'antd';
import { addList } from '../store/actionCreators';



class Antd extends PureComponent {

  add = (li) => {
    console.log(li)
    // 修改store
    this.props.addList(li)
  }
  render() {
    const {data} = this.props
    return (
      <div>
        <Button onClick={this.add.bind(this, 'Los Angeles battles huge wildfires.')}>add</Button>
        <h3 style={{ marginBottom: 16 }}>Default Size</h3>
        <List
          header={<div>Header</div>}
          footer={<div>Footer</div>}
          bordered
          dataSource={data}
          renderItem={item => (
            <List.Item>
              <Typography.Text mark>[ITEM]</Typography.Text> {item}
            </List.Item>
          )}
        />
       
      </div>
    )
  }
}


const mapStateToProps = state => ({
  data: state.list
})
const mapDispatchToProps = (dispatch) => {
  return {
    addList: (li) => {
      dispatch(addList(li))
    }
  }
}
export default connect( 
  mapStateToProps,
  mapDispatchToProps
)(Antd)
传入 Store Provider包裹
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import {Provider} from 'react-redux'
import store from './store'
// jsx
ReactDOM.render(
  <Provider store={store}>
   < App / > 
  </Provider>,
  document.getElementById('root')
);

分容器组件 和 展示组件(函数组件) 其它组件(组件的视图和逻辑混合)

    <容器组件>
        <展示组件/>
    <容器组件/>
    <其它组件/>

异步请求处理的中间件 redux-thunk / redux-saga

redux-thunk

store.js

import { compose, createStore, applyMiddleware } from 'redux'
// redux中间件
import thunk from 'redux-thunk'
import reducer from './reducer'
const middleware = [thunk]
// 创建 Redux store 来存放应用的状态。
// API 是 { subscribe, dispatch, getState }。

// redux-devtools-extension
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
  reducer, 
  /* preloadedState, */ 
  composeEnhancers(
    applyMiddleware(...middleware),
  )
)

// 注意 subscribe() 返回一个函数用来注销监听器
store.subscribe(() => console.log(store.getState()))

// 改变内部 state 惟一方法是 dispatch 一个 action。
// action 可以被序列化,用日记记录和储存下来,后期还可以以回放的方式执行
store.dispatch({type: 'INCREMENT'})

// 停止监听 state 更新
// unsubscribe()
export default store

组件中

import React, { PureComponent } from 'react'
import { connect } from 'react-redux'
import { List, Typography, Button } from 'antd';
import { addList, initList} from '../store/tolist/actions';


class Antd extends PureComponent {
  componentDidMount() {
    this.props.initList()
  }
  

  add = (li) => {
    // 修改store
    this.props.addList(li)
  }
  render() {
    const {data} = this.props
    return (
      <div>
        <Button onClick={this.add.bind(this, 'Los Angeles battles huge wildfires.')}>add</Button>
        <h3 style={{ marginBottom: 16 }}>Default Size</h3>
        <List
          header={<div>Header</div>}
          footer={<div>Footer</div>}
          bordered
          dataSource={data}
          renderItem={item => (
            <List.Item>
              <Typography.Text mark>[ITEM]</Typography.Text> {item}
            </List.Item>
          )}
        />
       
      </div>
    )
  }
}


const mapStateToProps = state => ({
  data: state.list
})
// const mapDispatchToProps = (dispatch) => {
//   return {
//     addList: (li) => {
//       dispatch(addList(li))
//     },
//     initList: (list) => {
//       dispatch(initList(list))
//     }
//   }
// }
export default connect( 
  mapStateToProps,
  { addList, initList }
)(Antd)

actions.js

import {ADD_LIST, INIT_LIST} from './actionTypes'
import axios from 'axios'

// 帮助生成 action creator
function makeActionCreator(type, ...argNames) {
  return function(...args) {
    const action = { type }
    argNames.forEach((arg, index) => {
      action[argNames[index]] = args[index]
    })
    return action
  }
}
// 统一管理 action

export const addList = makeActionCreator(ADD_LIST, 'payload')

// Action Creator(动作生成器),返回一个函数 redux-thunk中间件,改造store.dispatch,使得后者可以接受函数作为参数。
export const initList = () => (dispatch) => {
  axios.get('http://localhost.charlesproxy.com:3000/api/list').then((res) => {
      console.log(res.data)
      dispatch({
        type: INIT_LIST,
        list: res.data
      })
    }).catch((res) => {
      console.log(res)
    })
}

redux-saga

saga.js

import { call, put, takeEvery, takeLatest } from 'redux-saga/effects'
import Api from '...'

// worker Saga : 将在 USER_FETCH_REQUESTED action 被 dispatch 时调用
function* fetchUser(action) {
   try {
      // yield call([obj, obj.method], arg1, arg2, ...) // 如同 obj.method(arg1, arg2 ...) 
      const user = yield call(Api.fetchUser, action.payload.userId);
      yield put({type: "USER_FETCH_SUCCEEDED", user: user});
   } catch (e) {
      yield put({type: "USER_FETCH_FAILED", message: e.message});
   }
}

/*
  在每个 `USER_FETCH_REQUESTED` action 被 dispatch 时调用 fetchUser
  允许并发(译注:即同时处理多个相同的 action)
*/
function* mySaga() {
  yield takeEvery("USER_FETCH_REQUESTED", fetchUser);
}

/*
  也可以使用 takeLatest

  不允许并发,dispatch 一个 `USER_FETCH_REQUESTED` action 时,
  如果在这之前已经有一个 `USER_FETCH_REQUESTED` action 在处理中,
  那么处理中的 action 会被取消,只会执行当前的
*/
function* mySaga() {
  yield takeLatest("USER_FETCH_REQUESTED", fetchUser);
  // 非异步或而外操作不需要 直接actions 
  // yield takeEvery(DELETE_LIST_SAGA, deleteList)
}

export default mySaga;

store.js

import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'

import reducer from './reducers'
import mySaga from './sagas'

// create the saga middleware
const sagaMiddleware = createSagaMiddleware()
// mount it on the Store
const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
)

// then run the saga
sagaMiddleware.run(mySaga)

// render the application
执行多个任务

当我们需要 yield 一个包含 effects 的数组, generator 会被阻塞直到所有的 effects 都执行完毕,或者当一个 effect 被拒绝 (就像 Promise.all 的行为)。

const [users, repos] = yield [
  call(fetch, '/users'),
  call(fetch, '/repos')
]

immutable 性能优化

immutable https://immutable-js.github.io/immutable-js/

Immutable 详解及 React 中实践: https://github.com/camsong/blog/issues/3

Immutable Data 就是一旦创建,就不能再被更改的数据。对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象

注意点:

immutable对象 正常使用相关遍历方法(map)

immutable对象 不能直接通过下标访问. 可以通过转化为原始对象后访问

const newList = list.toJS();

immutable对象 获取长度 是==size==

reducer.js

import * as contants from './actionTypes'
import { fromJS } from 'immutable'

// immutable对象
const initialState = fromJS({
  cn: 'yewq'
})

export default (state = initialState, action) => {
  switch (action.type) {
    case contants.DEFAULT:
      // 嵌套的话 setIn(['cn', '..']) 修改多个值merge({...})  根据原有值更新 updateIn
      return state.set('cn', action.payload)
    default:
      return state
  }
}

组件中取值 store中{}都是immutable创建对象 api获取的数据(对象类型)也用fromJS()包裹后存入store

import React from 'react'
import { connect } from 'react-redux'
import { setName, fetchName } from '../../store/name/actions'
import './App.styl'

function App({ name, setName, fetchName }) {
  return (
    <div className="App">
      <header className="App-header">
        <h1 onClick={() => setName('test')}>{name}</h1>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  )
}

const mapStateToProps = (state, ownProps) => {
  return {
    //immutable取值 get('cn') 嵌套数据 getIn
    name: state.getIn(['user', 'cn'])
  }
}

export default connect(
  mapStateToProps,
  { setName, fetchName }
)(App)

reducers的合并 借助redux-immutable

import { combineReducers } from 'redux-immutable'
import nameReducer from './name/reducer'

const reducers = combineReducers({
  user: nameReducer
})

export default reducers
redux-immutable 统一格式
import { combineReducers } from 'redux-immutable'
import name from './name/reducer'

export default combineReducers({
  name
})

antd

安装

yarn add antd

按需加载配置

yarn add react-app-rewired customize-cra babel-plugin-import

/* package.json */
"scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test"
},

根目录新建 config-overrides.js

const { override, fixBabelImports } = require('customize-cra');


 module.exports = override(
   fixBabelImports('import', {
     libraryName: 'antd',
     libraryDirectory: 'es',
     style: 'css',
   }),
 );

使用

 import { Button } from 'antd';

FAQ:

yarn eject 之后 需要 yarn install一下

更换scripts后 若丢失react-scripts 重新安装一下即可


react-router

安装

yarn add react-router-dom

导航式

import React from "react";
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";

function Home() {
  return <h2>Home</h2>;
}

function About() {
  return <h2>About</h2>;
}

function Users() {
  return <h2>Users</h2>;
}

export default function App() {
  return (
    <Router>
      <div>
        <nav>
          <ul>
            <li>
              <Link to="/">Home</Link>
            </li>
            <li>
              <Link to="/about">About</Link>
            </li>
            <li>
              <Link to="/users">Users</Link>
            </li>
          </ul>
        </nav>

        {/* A <Switch> looks through its children <Route>s and
            renders the first one that matches the current URL. */}
        <Switch>
          <Route path="/about">
            <About />
          </Route>
          <Route path="/users">
            <Users />
          </Route>
          <Route path="/">
            <Home />
          </Route>
        </Switch>
      </div>
    </Router>
  );
}

跳转标签

<link to='/'></link>

显示标签


<Route path='/'></Route>

嵌套路由

嵌套路由放前面, 第二才放不匹配的

<Route path="/contact/:id">
  <Contact />
</Route>
<Route path="/contact">
  <AllContacts />
</Route>

钩子 useRouteMatch useParams return之前使用

useParams -> '/:id'

useLocation -> '?id=1'

app.js

<Route path='/nestedRouting' component={NestedRouting}></Route>

NestedRouting.js

import React, { Fragment } from 'react'
import { Switch, Route, useRouteMatch, useParams, Link } from 'react-router-dom'

function Topic() {
  let { topicId } = useParams()
  return <h3>Requested topic ID: {topicId}</h3>
}
// 嵌套路由
export default function NestedRouting() {
  let match = useRouteMatch()
  console.log(match)
  return (
    <Fragment>
      <div>
        <div>NestedRouting</div>
      </div>
      <Switch>
        <Route path={`${match.path}/:topicId`}>
          <Topic />
        </Route>
        <Route path={match.path}>
          <p>
            <Link to={`${match.url}/li1`}>1123</Link>
          </p>
          <p>
            <Link to={`${match.url}/li2`}>222222</Link>
          </p>
        </Route>
      </Switch>
    </Fragment>
  )
}

Redirect

function Topic() {
  let { topicId } = useParams()
  let match = useRouteMatch()
  console.log(match)
  return topicId === 'back' ? (
    <Redirect to={`/antd`}></Redirect>
  ) : (
    <h3>Requested topic ID: {topicId}</h3>
  )
}

钩子 useHistory useLocation

import { useHistory } from "react-router-dom";

function HomeButton() {
  let history = useHistory();

  function handleClick() {
    history.push("/home");
  }

  return (
    <button type="button" onClick={handleClick}>
      Go home
    </button>
  );
}

exact 路径完全匹配时才会显示

<Route path='/' exact component={Home}></Route>
<Route path='/login' exact component={Login}></Route>

react-loadable

常见建议是将您的应用划分为单独的路由,并异步加载每个路由。对于许多应用程序来说,这似乎已经足够好了-作为用户,单击链接并等待页面加载是网络上的一种熟悉体验。

react-loadable可以做得更好。

api 介绍

const LoadableComponent = Loadable({
  loader: () => import('./Bar'),
  loading: LoadingComponent,
  delay: 200,
  timeout: 10000,
  <!--render(loaded, props) {-->
  <!--  let Bar = loaded.Bar.default;-->
  <!--  let i18n = loaded.i18n;-->
  <!--  return <Bar {...props} i18n={i18n}/>;-->
  <!--},-->
});

function LoadingComponent(props) {
  if (props.error) {
    // When the loader has errored
    return <div>Error! <button onClick={ props.retry }>Retry</button></div>;
  } else if (props.timedOut) {
    // When the loader has taken longer than the timeout
    return <div>Taking a long time... <button onClick={ props.retry }>Retry</button></div>;
  } else if (props.pastDelay) {
    // When the loader has taken longer than the delay
    return <div>Loading...</div>;
  } else {
    // When the loader has just started
    return null;
  }
}

使用

import React from 'react'
import Loadable from 'react-loadable'

const LoadableComponent = Loadable({
  loader: () => import('./组件'),
  loading() {
    return <div>正在加载</div>
  }
})

export default () => <LoadableComponent />

如果路由传参的话 需要withRouter包裹一下组件(使组件能访问路由) 或者直接使用useParams

import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { DetailWrapper, Header, Content } from './style';
import { actionCreators } from './store';

class Detail extends PureComponent {
    render() {
        return (
            <DetailWrapper>
                <Header>{this.props.title}</Header>
                <Content 
                    dangerouslySetInnerHTML={{__html: this.props.content}}
                />
            </DetailWrapper>
        )
    }

    componentDidMount() {
        this.props.getDetail(this.props.match.params.id);
    }
}

const mapState = (state) => ({
    title: state.getIn(['detail', 'title']),
    content: state.getIn(['detail', 'content'])
});

const mapDispatch = (dispatch) => ({
    getDetail(id) {
        dispatch(actionCreators.getDetail(id));
    }
});

export default connect(mapState, mapDispatch)(withRouter(Detail));

app.js

import Detail from './pages/detail/loadable.js';

<Route path='/detail/:id' exact component={Detail}></Route>

react 引入 stylus

yarn eject

webpack.config.js下 module->rules->oneOf 添

{
  test: /\.styl$/,
  use: [
    require.resolve('style-loader'),
    require.resolve('css-loader'),
    require.resolve('stylus-loader')
  ]
},

Hook (独立的)

Hook 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。

State Hook
import React, { useState } from 'react';

function Example() {
  // 声明一个叫 “count” 的 state 变量。
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

通过在函数组件里调用它来给组件添加一些内部 state。React 会在重复渲染时保留这个 state。useState 会返回一对值:当前状态和一个让你更新它的函数,你可以在事件处理函数中或其他一些地方调用这个函数。

Effect Hook 可以用多个 Effect 实现关注点分离

useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 具有相同的用途,只不过被合并成了一个 API。(我们会在使用 Effect Hook 里展示对比 useEffect 和这些方法的例子。)

==在 React 组件中有两种常见副作用操作:需要清除的和不需要清除的。==

不需要清除的

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });
}

需要清除的 effect
之前,我们研究了如何使用不需要清除的副作用,还有一些副作用是需要清除的。例如订阅外部数据源。这种情况下,清除工作是非常重要的,可以防止引起内存泄露!现在让我们来比较一下如何用 Class 和 Hook 来实现。

为什么要在 effect 中返回一个函数? 这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect 的一部分。

 useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

==通过跳过 Effect 进行性能优化==

如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可:

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。这并不属于特殊情况 —— 它依然遵循输入数组的工作方式。
Hook 使用规则
  • 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
  • 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。(还有一个地方可以调用 Hook —— 就是自定义的 Hook 中,我们稍后会学习到。)

添加eslint检测

npm install eslint-plugin-react-hooks --save-dev
// 你的 ESLint 配置
{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error", // 检查 Hook 的规则
    "react-hooks/exhaustive-deps": "warn" // 检查 effect 的依赖
  }
}
自定义 Hook

有时候我们会想要在组件之间重用一些状态逻辑。目前为止,有两种主流方案来解决这个问题:高阶组件和 render props。自定义 Hook
可以让你在不增加组件的情况下达到同样的目的。

一个叫 FriendStatus 的组件,它通过调用 useState 和 useEffect 的 Hook 来订阅一个好友的在线状态。假设我们想在另一个组件里重用这个订阅逻辑。

import React, { useState, useEffect } from 'react';

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

现在我们可以在两个组件中使用它:

function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}
其他 Hook

除此之外,还有一些使用频率较低的但是很有用的 Hook。比如,useContext 让你不使用组件嵌套就可以订阅 React 的 Context。

function Example() {
  const locale = useContext(LocaleContext);
  const theme = useContext(ThemeContext);
  // ...
}
useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>。
useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context。
useReducer 可以让你通过 reducer 来管理组件本地的复杂 state。

initialArg: 初始 state | init: 惰性初始化-> 初始 state 将被设置为 init(initialArg)

const [state, dispatch] = useReducer(reducer, initialArg, init);

在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数 。

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

惰性初始化

你可以选择惰性地创建初始 state。为此,需要将 init 函数作为 useReducer 的第三个参数传入,这样初始 state 将被设置为 init(initialArg)。

这么做可以将用于计算 state 的逻辑提取到 reducer 外部,这也为将来对重置 state 的 action 做处理提供了便利:

function init(initialCount) {
  return {count: initialCount};
}

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    case 'reset':
      return init(action.payload);
    default:
      throw new Error();
  }
}

function Counter({initialCount}) {
  const [state, dispatch] = useReducer(reducer, initialCount, init);
  return (
    <>
      Count: {state.count}
      <button
        onClick={() => dispatch({type: 'reset', payload: initialCount})}>

        Reset
      </button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}
useCallback

把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);
usePrevious
function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);
  return <h1>Now: {count}, before: {prevCount}</h1>;
}

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

CSS Module

style.类名

import style from './index.module.css';



return (
      <div className={style.face_container}>
        <div className={style.login_bt}>
          <p>
            友情提示: 因为facebook api一段时间内访问有流量限制,
            小伙伴工作时间尽量错开哈~ 使用时可以在群通报一声.
          </p>
          <Button
            type="primary"
            loading={this.state.loginLoading}
            onClick={this.resetLogin}
          >
            如果访问令牌失效了,才点击重新登录
          </Button>
          <Button
            type="primary"
            loading={this.state.adaccountLoading}
            onClick={this.getNewAdaccounts}
          >
            如果广告账户有新添,才点击重新获取广告账户
          </Button>
        </div>
        <Ads options={this.state.options} />
      </div>
    );
  }

typescript

创建

npx create-react-app my-app --typescript

或者添加 TypeScript到现有项目中

yarn add --dev typescript

在配置编译器之前,让我们将 tsc 添加到 package.json 中的 “scripts” 部分:

{
  // ...
  "scripts": {
    "build": "tsc",
    // ...
  },
  // ...
}

函数组件

hello.tsx

import React from 'react'

interface HelloProps {
  name: string
  age?: number
}

const Hello: React.FC<HelloProps> = ({ name }) => {
  return <>hello, {name}</>
}

export default Hello

类组件

import * as React from 'react';

export interface MouseProviderProps {
  render: (state: MouseProviderState) => React.ReactNode;
}

interface MouseProviderState {
  readonly x: number;
  readonly y: number;
}

export class MouseProvider extends React.Component<MouseProviderProps, MouseProviderState> {
  readonly state: MouseProviderState = { x: 0, y: 0 };

  handleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
    this.setState({
      x: event.clientX,
      y: event.clientY,
    });
  };

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
        {/*
          Instead of providing a static representation of what <Mouse> renders,
          use the `render` prop to dynamically determine what to render.
        */}
        {this.props.render(this.state)}
      </div>
    );
  }
}

添加泛型T 使用

import * as React from 'react';

export interface GenericListProps<T> {
  items: T[];
  itemRenderer: (item: T) => JSX.Element;
}

export class GenericList<T> extends React.Component<GenericListProps<T>, {}> {
  render() {
    const { items, itemRenderer } = this.props;

    return (
      <div>
        {items.map(itemRenderer)}
      </div>
    );
  }
}

使用style

const scrollStyle = (): React.CSSProperties => ({
    position: 'fixed',
    top: contentTop + 'px'
  })

<div
  className={style.scroll_container}
  style={contentTop > 0 ? scrollStyle() : undefined}
></div>

个人项目模板 react-basic

branch
  • master
  • immutable 引入 immutable + react-loadable

目录结构

├─src
    ├─assets
    ├─components
    │  └─App
    ├─pages
    └─store
        └─name
优质内容筛选与推荐>>
1、Yii AR(Active Record) 详解
2、存储NAS和SAN
3、逆序数
4、Projections.groupProperty
5、Flume配置


长按二维码向我转账

受苹果公司新规定影响,微信 iOS 版的赞赏功能被关闭,可通过二维码转账支持公众号。

    阅读
    好看
    已推荐到看一看
    你的朋友可以在“发现”-“看一看”看到你认为好看的文章。
    已取消,“好看”想法已同步删除
    已推荐到看一看 和朋友分享想法
    最多200字,当前共 发送

    已发送

    朋友将在看一看看到

    确定
    分享你的想法...
    取消

    分享想法到看一看

    确定
    最多200字,当前共

    发送中

    网络异常,请稍后重试

    微信扫一扫
    关注该公众号