写给自己的React-HOC高阶组件手册

前言HOC(高阶组件)是React中的一种组织代码的手段,而不是一个API. 这种设计模式可以复用在React组件中的代码与逻辑,因为一般来讲React组件比较容易复用渲染函数, 也就是主要负责HTML的输出. 高阶组件实际上是经过一个包装函数返回的组件,这类函数接收React组件处理传入的组件,然后返回一个新的组件. 注意:前提是建立在不修改原有组件的基础上. 文字描述太模糊,借助于官方文档稍稍修改,我们可以更加轻松的理解高阶组件. 具体的实施流程如下: 找出组件中复用的逻辑创建适用于上方逻辑的函数利用这个函数来创建一个组件enjoy it找出组件中复用的逻辑在实际开发中, 这种逻辑的组件非常常见: 组件创建向服务器拉取数据利用数据渲染组件监听数据的变化数据变化或者触发修改的事件利用变化后的数据再次渲染组件销毁移除监听的数据源首先我们来创建一个生产假数据的对象来模拟数据源: const fakeDataGenerator = ()=>({ timer: undefined, getData(){ return ['hello', 'world']; }, addChangeListener(handleChangeFun){ // 监听数据产生钩子 if(this.timer){ return; } this.timer = setInterval(()=> { handleChangeFun(); },2000) }, removeChangeListener(){ // 停止数据监听 clearInterval(this.timer); }});然后来编写我们的组件A: const FakeDataForA = fakeDataGenerator();class A extends React.Component { constructor(props) {// 1 组件创建 super(props); this.state = { someData: fakeData.getData() // 1.1 向服务器拉取数据 } } handleFakeDataChange = ()=>{ this.setState({ someData:fakeData.getData() // 4. 数据变化或者触发修改的事件 }); } componentDidMount(){ // 3. 监听数据的变化 // 4. 数据变化或者触发修改的事件 fakeData.addChangeListener(this.handleFakeDataChange); } componentWillUnmount(){ fakeData.removeChangeListener(); // 6. 组件销毁移除监听的数据源 } render() { return ( {/* 2. 利用数据渲染组件 5. 利用变化后的数据再次渲染 */} this.state.someData.map(name => (<span key={name}>{name}</span>)) ) }}ReactDOM.render(<A />, document.getElementById('root'));然后我们再来创建一个组件B这个虽然渲染方式不同,但是数据获取的逻辑是一致的. 在一般的开发过程中实际上也是遵循这个请求模式的,然后创建一个组件B: ...

May 21, 2019 · 5 min · jiezi

译三分钟掌握-React-高阶组件

掌握这个有用的模式,停止在 React Components 中重复逻辑! ????原文:React Higher Order Components in 3 minutes作者:Jhey Tompkins译者:博轩 PS:今天是母亲节,先祝所有母亲大人们节日快乐啦 ???????????? 什么是高阶组件?高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。 译注:对,我又一次借鉴了官网 ????它做了什么?他们接收一个组件并返回一个新的组件! 什么时候去使用?当你的组件之间出现重复的模式 / 逻辑的时候。 栗子: 挂载,订阅数据为 UI 增加交互(也可以使用容器组件,或者 Render Props)排序,过滤输入的数据译注:第三个说法,我个人可能更加倾向于在传入组件之前做处理,而不是使用高阶组件一个愚蠢的例子我们有一个 Mouse 组件。 const Mouse = () => ( <span className="mouse" role="img">????</span>)接下来,让我们使用 GreenSock’s Draggable 模块,来让组件变的可以拖拽。 class Mouse extends Component { componentDidMount = () => new Draggable(this.ELEMENT) render = () => ( <span className="mouse" ref={e => (this.ELEMENT = e)} role="img">????</span> )} ...

May 12, 2019 · 2 min · jiezi

译函数式的React

原文:The functional side of React作者:Andrea Chiarelli译者:博轩React 是现在最流行的 JavaScript 库之一。使用 React 可以非常轻松地创建 Web 用户交互界面。 它的成功有很多因素,但也许其中一个因素是清晰有效的编程方法。 在 React 的世界中,UI 是由一个一个组件所组成的。组件可以组合在一起以创建其他组件, 应用本身就是一个包含了所有组件的一个大组件。开发者使用 React 会很容易联想到:面向对象编程 。因为定义组件的语法本身,就会给人这种感觉: class HelloReact extends Component { render() { return (<div>Hello React!</div>); }}然鹅,在面向对象的的表象之下,React 隐藏了一种函数式的特质。让我们看看这些特质都是什么? 使用 render() 渲染输出React 组件的一大特征是是包含了 render() 方法。没有包含 render() 方法的组件不是 React 组件。render() 方法总会返回一个 React 元素,这种行为就像是组件的一种特征一样。换句话说,React 会要求任何组件必须有输出。组件是根据输入来返回输出的,这样来考虑组件的话,就会让你感觉组件更像一个函数,而不是一个对象。 组件就是一个函数实际上,您不仅可以将 React 组件视为函数。 您还可以用函数来实现组件。 以下代码展示了如何使用函数实现上面定义的组件: const HelloReact = () => <div>Hello React!</div>;如您所见,它是一种实现组件的简单而紧凑的方法。 此外,您可以将参数传递给函数: const Hello = (props) => <div>Hello {props.name}!</div>;在上面的示例中,您传递了 props 参数,这里的 props 对象用于将数据从一个组件传递到另一个组件。 ...

May 7, 2019 · 2 min · jiezi

【译】TypeScript中的React高阶组件

原文链接:https://medium.com/@jrwebdev/…高阶组件(HOCs)在React中是组件复用的一个强大工具。但是,经常有开发者在结合TypeScript使用中抱怨道很难去为其设置types。这边文章将会假设你已经具备了HOCs的基本知识,并会根据由浅入深的例子来向你展示如何去为其设置types。在本文中,高阶组件将会被分为两种基本模式,我们将其命名为enhancers和injectors:enhancers:用附加的功能/props来包裹组件。injectors:向组件注入props。请注意,本文中的示例并不是最佳实践,本文主要只是展示如何在HOCs中设置types。Enhancers我们将从enhancers开始,因为它更容易去设置types。此模式的一个基本示例是一个向组件添加loading props的HOC,并且将其设置为true的时候展示loading图。下面是一个没有types的示例:const withLoading = Component => class WithLoading extends React.Component { render() { const { loading, …props } = this.props; return loading ? <LoadingSpinner /> : <Component {…props} />; } };然后是加上typesinterface WithLoadingProps { loading: boolean;}const withLoading = <P extends object>(Component: React.ComponentType<P>) => class WithLoading extends React.Component<P & WithLoadingProps> { render() { const { loading, …props } = this.props; return loading ? <LoadingSpinner /> : <Component {…props as P} />; } };这里发生了一些事情,所以我们将把它分解:interface WithLoadingProps { loading: boolean;}在这里,声明一个props的interface,将会被添加到被包裹的组件上。<P extends object>(Component: React.ComponentType<P>)这里我们使用泛型:P表示传递到HOC的组件的props。React.ComponentType<P> 是 React.FunctionComponent<P> | React.ClassComponent<P>的别名,表示传递到HOC的组件可以是类组件或者是函数组件。class WithLoading extends React.Component<P & WithLoadingProps>在这里,我们定义从HOC返回的组件,并指定该组件将包括传入组件的props(P)和HOC的props(WithLoadingProps)。它们通过 & 组合在一起。const { loading, …props } = this.props;最后,我们使用loading props有条件地显示加loading图或传递了自己props的组件:return loading ? <LoadingSpinner /> : <Component {…props as P} />;注意:由于typescript中可能存在的bug,因此从typescript v3.2开始,这里需要类型转换(props as p)。我们的withloading HOC也可以重写以返回函数组件而不是类:const withLoading = <P extends object>( Component: React.ComponentType<P>): React.FC<P & WithLoadingProps> => ({ loading, …props}: WithLoadingProps) => loading ? <LoadingSpinner /> : <Component {…props as P} />;这里,我们对对象rest/spread也有同样的问题,因此通过设置显式的返回类型React.FC<P & WithLoadingProps>来解决这个问题,但只能在无状态功能组件中使用WithLoadingProps。注意:React.FC是React.FunctionComponent的缩写。在早期版本的@types/react中,是React.SFC或React.StatelessFunctionalComponent。Injectorsinjectors是更常见的HOC形式,但更难为其设置类型。除了向组件中注入props外,在大多数情况下,当包裹好后,它们也会移除注入的props,这样它们就不能再从外部设置了。react redux的connect就是是injector HOC的一个例子,但是在本文中,我们将使用一个更简单的例子,它注入一个计数器值并回调以增加和减少该值:import { Subtract } from ‘utility-types’;export interface InjectedCounterProps { value: number; onIncrement(): void; onDecrement(): void;}interface MakeCounterState { value: number;}const makeCounter = <P extends InjectedCounterProps>( Component: React.ComponentType<P>) => class MakeCounter extends React.Component< Subtract<P, InjectedCounterProps>, MakeCounterState > { state: MakeCounterState = { value: 0, }; increment = () => { this.setState(prevState => ({ value: prevState.value + 1, })); }; decrement = () => { this.setState(prevState => ({ value: prevState.value - 1, })); }; render() { return ( <Component {…this.props as P} value={this.state.value} onIncrement={this.increment} onDecrement={this.decrement} /> ); } };这里有几个关键区别:export interface InjectedCounterProps { value: number; onIncrement(): void; onDecrement(): void;}我们给将要注入到组件的props声明一个interface,该接口将被导出,以便这些props可由被HOC包裹的组件使用:import makeCounter, { InjectedCounterProps } from ‘./makeCounter’;interface CounterProps extends InjectedCounterProps { style?: React.CSSProperties;}const Counter = (props: CounterProps) => ( <div style={props.style}> <button onClick={props.onDecrement}> - </button> {props.value} <button onClick={props.onIncrement}> + </button> </div>);export default makeCounter(Counter);<P extends InjectedCounterProps>(Component: React.ComponentType<P>)我们再次使用泛型,但是这次,你要确保传入到HOC的组件包含注入到其中的props,否则,你将收到一个编译错误。class MakeCounter extends React.Component< Subtract<P, InjectedCounterProps>, MakeCounterState >HOC返回的组件使用Piotrek Witek’s的utility-types包中的subtract,它将从传入组件的props中减去注入的props,这意味着如果它们设置在生成的包裹组件上,则会收到编译错误:Enhance + Inject结合这两种模式,我们将在计数器示例的基础上,允许将最小和最大计数器值传递给HOC,而HOC又被它截取并使用,而不将它们传递给组件:export interface InjectedCounterProps { value: number; onIncrement(): void; onDecrement(): void;}interface MakeCounterProps { minValue?: number; maxValue?: number;}interface MakeCounterState { value: number;}const makeCounter = <P extends InjectedCounterProps>( Component: React.ComponentType<P>) => class MakeCounter extends React.Component< Subtract<P, InjectedCounterProps> & MakeCounterProps, MakeCounterState > { state: MakeCounterState = { value: 0, }; increment = () => { this.setState(prevState => ({ value: prevState.value === this.props.maxValue ? prevState.value : prevState.value + 1, })); }; decrement = () => { this.setState(prevState => ({ value: prevState.value === this.props.minValue ? prevState.value : prevState.value - 1, })); }; render() { const { minValue, maxValue, …props } = this.props; return ( <Component {…props as P} value={this.state.value} onIncrement={this.increment} onDecrement={this.decrement} /> ); } };这里,Subtract与types交集相结合,将组件自身的props与HOCs自身的props相结合,减去注入组件的props:Subtract<P, InjectedCounterProps> & MakeCounterProps除此之外,与其他两种模式相比,没有真正的差异需要强调,但是这个示例确实带来了一些高阶组件的问题。这些并不是真正特定于typescript的,但值得详细说明,以便我们可以讨论如何使用typescript来解决这些问题。首先,MinValue和MaxValue被HOC拦截,而不是传递给组件。但是,你也许希望它们是这样的,这样你就可以基于这些值禁用递增/递减按钮,或者向用户显示一条消息。如果用HOC,你也可以简单地修改它来注入这些值,但是如果你没有(例如,它来自一个NPM包),这就将会是一个问题。其次,由HOC注入的prop有一个非常通用的名称;如果要将其用于其他目的,或者如果要从多个HOC注入prop,则此名称可能与其他注入的prop冲突。您可以将名称更改为不太通用的解决方案,但就解决方案而言,这不是一个很好的解决方案! ...

April 18, 2019 · 2 min · jiezi

React高阶组件(HOC)入门示例

什么是高阶组件?简称:HOC全称:High Order Component高阶组件其实不是什么高深莫测的东西,它类似于高阶函数,就是一个纯函数,它会接受一个组件作为参数,然后返回一个新的组件。什么时候使用HOC?在React中,组件是代码复用的主要单元。但是业务开发过程中难免会遇到一些个性化的需求,此时如果再去重新开发一个组件,会让后续的维护成本变高。接下来举一个简单的例子说明假设有需求如下v1.0:页面上显示10行’hello, world!‘v2.0:页面上要多加10行‘HELLO,WORLD!‘要实现上述的需求,第一个版本会写一个显示hello,world!的组件,这个没有问题。第二个版本可以选择写一个显示HELLO,WORLD!的组件,或者在第一个组件的基础上包装一下,只给第一个组件返回的数据做一个转成大写的处理。我用HOC的方式写一下示例代码,很快就能明白v1.0的组件示例代码如下:class HelloWorld extends React.Component { render() { return “hello,world!” }}ReactDOM.render(<HelloWorld />, document.querySelector("#root"))v2.0的组件示例代码如下:// HOC函数,实现v2.0版本的需求export const toUpperCaseHoc = function(WrappedComponent) { return class Hoc extends React.Component { render() { const { text } = this.props; const text2Upper = text.toUpperCase(); return <WrappedComponent text={text2Upper} />; } };};// v1.0版本实现的组件export class HelloWorld extends React.Component { render() { return this.props.text; }}// 用HOC包装后生成的新的组件,符合v2.0版本的需求,同时包含了v1.0的其它功能const HelloWorld2Upper = toUpperCaseHoc(HelloWorld);ReactDOM.render(<HelloWorld2Upper text=“hello,world!” />, document.querySelector(’#root’));总结业务开发中可能会有一些功能大部分逻辑相似,部分个性化,这个时候可以考虑一下是不是可以开发一个基础组件,在基础组件的基础上去增加一些个性化的需求。最后,一个HOC最好只做一件事。可以参考:React官方高阶组件相关文档

January 12, 2019 · 1 min · jiezi

使用Vue的HOC技术开发一个无限加载列表

前言在web开发上,我们都对数据采用分页加载的机制,一种变形就是在页面采用循环加载的机制,拉到页面最下方有个加载更多的按钮。问题在于,当不同的数据要展示时,就要写很多这种列表,但是其中的逻辑都是相似的。维护一组数据加载更多数据将数据用对应的组件显示出来处理加载状态等那有没有这么一个组件,来完成这一切相同的逻辑呢?需求需要有这么一个InfiniteList组件,它负责管理相关数据的加载和维护,然后以列表的形式显示出来,而列表项必须是由调用方决定的组件。HOC高阶组件的概念,是React里面经常提到的,类似于高阶函数。高阶函数:(fn) => otherFn高阶组件:component => otherComponent高阶组件用是代码复用的优秀工具,主要在处理逻辑方面和普适性上,有着奇效。所以我决定用HOC来实现这个需求参考文章:http://hcysun.me/2018/01/05/%…良心博客本文涉及的知识vuevue的render函数实现0我使用的是vue和iview UI库1先弄出UI框架先,我用一个vue文件来构建整个组件的基本框架。源代码地址html部分<template> <div class=“wrapper”> <div class=“content-wrapper”> <slot></slot> </div> <div class=“load-wrapper”> <Button :icon=“tipIcon” type=“text” v-bind:disabled="!hasMore" v-bind:style="{color: tipColor}" v-bind:loading=“loading” v-on:click=“handleClickLoad”> {{loadButtonText}} </Button> </div> </div></template>用一个slot来分发要循环渲染的项目js部分一些UI有关的数据(不是很重要) props: { loadTip: { type: String, default: “加载更多” } … }, computed: { loadButtonText() {}, tipIcon() {} }这部分比较重要的只有一个事件发射,将点按钮的行为转换为 请求加载数据handleClickLoad() { // 发射 请求加载数据的 事件 this.$emit(“on-load”); }css部分略2接下来就是最重要的部分,编写HOC首先要明白,Vue中的组件,到底是什么。像我们写一个Vue文件,export出的是一个对象,所以我们现在写HOC,其实也是要最后返回一个对象。所以我写了下面的函数来生成HOC/** * 使用高阶组件的办法实现了一个无限加载列表 * 可以根据数据循环渲染出特定的组件,并且管理加载状态 * @param component 具体项的组件 {props: {data}}*/function InfiniteList(listItem) { return { props:… data(){} … }}而我们如果渲染呢,当然是用Vue的render函数render(h) { return h(component, data, children);}我们使用组合的方式,最外层需要用到我们第1步写到的模板,于是导入它,并注册它import InfiniteListTemplate from “./InfiniteListTemplate”;function InfiniteList(listItem) { return { … components: { InfiniteListTemplate // 列表框架的模板,这个模板里面只有ui表现 }, … }}render函数对于熟悉React的程序员来说应该是不难的,官网也有很详细的介绍。render(h) { const self = this; // 根据 data 的 dataList循环渲染子组件 const listItems = … return h(InfiniteListTemplate, { props: { …self.$props, // 传递所有参数 hasMore: self.hasMore, // 另外的hasMore和loading是这个HOC的state loading: self.loading }, attrs: self.$attrs, on: { // 监听加载按钮事件 “on-load”: () => self.handleLoadData() } }, listItems); },这里在最外层渲染我们的模板(且称为模板组件),并将当前HOC的props,attrs传递给模板组件。这里提到了HOC的data,非常简单,就是两个状态和一个数据数组data() { return { hasMore: true, loading: false, dataList: [] } }然后呢,循环渲染在哪?别急,render中的listItems就是我们循环渲染出来的组件,这里使用了map,相信使用React的人非常熟悉这种风格const listItems = this.dataList.map(item => h(component, { props: { data: item } }) );最终返回的就是return h(InfiniteListTemplate, {options}, listItems);在哪里维护数据呢?当然是要传入一个加载数据的函数来进行管理,我们在HOC的props里面定义props: { tipColor, loadTip, loadingTip, // 上面的数据都是为了传给模板(组件) offset: { type: Number, default: 5 }, // 数据加载的函数,需要的是一个 (index, offset) => Promise<[]> loadDataFunc: { type: Function, default() { return (index, offset) => Promise.resolve(new Array(offset).map((o, i) => index + i)); } } },然后我们还记得模板函数发射了个on-load事件么?我们需要在HOC里监听它并且处理逻辑render(h) { return h(InfiniteListTemplate, { … on: { ‘on-load’: () => self.handleLoadData() } }, listItems);},methods: { /** * 监听模板点出了加载按钮时的操作 * 调用数据加载函数加载数据 * @return {Promise<void>} */ async handleLoadData() { try { this.loading = true; let res = await this.loadDataFunc(this.dataList.length, this.offset); if (res && res.length) { this.dataList = this.dataList.concat(res); this.$Message.success(成功获取到${res.length}条新数据); } else { this.$Message.info(已经获取了全部数据了); this.hasMore = false; } } catch (e) { this.$Message.error(“加载失败” + e.message); } finally { this.loading = false; } } },完整InfiniteList.js代码3接下来使用一遍<script>import MyComponent from “./components/MyComponent”;import InfiniteList from “./components/hoc/InfiniteList”;const InfiniteListComponent = InfiniteList(MyComponent);…data() { loadDataFunc: (index, offset) => Promise<[]>}</script><template> <div id=“app”> <InfiniteListComponent v-if=“loadDataFunc” v-bind:load-data-func=“loadDataFunc”> </InfiniteListComponent> </div></template>MyComponent.vue是个非常简单的组件<template> <div>Hello</div></template><script> export default { name: “MyComponent”, props: { data: { type: String } } }</script>效果图如下总结在前端开发过程中,HOC是代码利用的利器,但是对抽象的要求高。我觉得自己爱上了React…Vue实现这个HOC烦死了 ...

January 8, 2019 · 2 min · jiezi

奇技淫巧 - Vue Mixins 高级组件 与 Vue HOC 高阶组件 实践

在项目里,我们经常会使用组件库进行快速开发,然而在过程中,又难免会遇到对组件库的改造和拓展,如何优雅且简单的进行重构,下面让我们从一个简单需求来探索组件的奇技淫巧–Mixins和HOC项目中使用组件库遇到的需求需求: 实现所有页面按钮的点击事件节流控制随便选用一套组件库,在这次例子里,我选用iview进行开发iview官网的Button组件的使用方法如下<template> <Button @click=“click”>Default</Button></template><script> export default { methods: { click () { console.log(‘yes’) } } }</script>Button的源码也很简单,在这里我剔除了不相关的内容<template> <button @click=“handleClick”></button></template><script> export default { name: ‘Button’, components: { Icon }, props: { }, data () { }, computed: { }, methods: { handleClick (event) { this.$emit(‘click’, event); } }, mounted () { } };</script>从源码里可以得到以下信息iview的Button组件封装了原生的button组件对原生的button组件,进行了click事件的绑定click事件触发时,向组件emit了click事件需求怎么实现再看看我们的需求 实现所有页面按钮的点击事件节流控制,这里有几个关键点点击事件,iview的Button组件里已经对原生的button的click进行了绑定,我们需要劫持这段绑定,进行节流节流控制,节流(debounce)函数,网上有很多示例,容易实现那么需求的难点就在于,如何实现点击事件的劫持?在这里有两种方案Mixins1. Mixin是什么Mixins 在官方Vue文档中已经有了很详细的介绍,不熟悉的朋友们可以看看,用一句话来理解,即合并组件的组件官方介绍用一张图来表示2. 怎么解决需求直接上源码// 节流函数function debounce (func, delay, context, event) { clearTimeout(func.timer) func.timer = setTimeout(function () { func.call(context, event) }, delay)}// iview中click方法拷贝function _handleClick (event) { this.$emit(‘click’, event) const openInNewWindow = event.ctrlKey || event.metaKey this.handleCheckClick(event, openInNewWindow)}// 导出新组件export default { props: { }, mixins: [Vue.options.components.Button], // iview 中Button组件 data () { return {} }, mounted () { console.log(‘mixins succeed’) }, methods: { handleClick (event) { let that = this console.log(‘debounce’) debounce(_handleClickLink, 300, that, event) } }}3. 原理mixins的原理很容易理解,上列源码我们做了这些操作,来实现合并ivew Button组件,劫持click事件创建debounce节流函数复制iview Button组件中handleClick方法为_handleClick导出对象,methods里重写handleClick方法,进行节流控制使用mixins 来实现我们需求很简单,但也因此会有许多问题需要知道Button源码结构带来了隐式依赖,如果mixins嵌套,会很难理解那么有没有更好的方法?HOC1.什么是HOC?所谓高阶组件其实就是高阶函数,React 和 Vue 都证明了一件事儿:一个函数就是一个组件。所以组件是函数这个命题成立了,那高阶组件很自然的就是高阶函数,即一个返回函数的函数HOC的详细介绍和实现,这篇文章探索Vue高阶组件有详细介绍,用一句话来理解,即包裹组件的组件用一张图来表示2. 怎么解决需求直接上源码// 节流函数function debounce (func, delay, context, event) { clearTimeout(func.timer) func.timer = setTimeout(function () { func.call(context, event) }, delay)}// 导出新组件export default { props: {}, name: ‘ButtonHoc’, data () { return {} }, mounted () { console.log(‘HOC succeed’) }, methods: { handleClickLink (event) { let that = this console.log(‘debounce’) // that.$listeners.click为绑定在新组件上的click函数 debounce(that.$listeners.click, 300, that, event) } }, render (h) { const slots = Object.keys(this.$slots) .reduce((arr, key) => arr.concat(this.$slots[key]), []) .map(vnode => { vnode.context = this._self return vnode }) return h(‘Button’, { on: { click: this.handleClickLink //新组件绑定click事件 }, props: this.$props, // 透传 scopedSlots scopedSlots: this.$scopedSlots, attrs: this.$attrs }, slots) }}3. 原理HOC的特点在于它的包裹性,上列源码我们做了这些操作,来实现包裹iview的Button组件,劫持click事件创建debounce节流函数导出新的组件render渲染出iview ButtonButton 绑定debounce后的click方法HOC的包裹性同时也会带来几个问题组件之间通信会被拦截,比如子组件访问父组件的方法(this.$parent.methods)vue官方并没有推荐使用HOC :(总结Mixins 和 HOC 都能实现这个简单的需求,希望大家能理解这两种技巧,解决项目中的问题源码:https://github.com/warpcgd/mi…demo:https://warpcgd.github.io/mix… ...

January 3, 2019 · 2 min · jiezi