IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    基于作用域插槽的 Renderless 组件 · 看不见我的美 · 是你瞎了眼

    馬腊咯稽发表于 2020-04-29 00:00:00
    love 0
    基于作用域插槽的 Renderless 组件

    回来一查,原来是作用域插槽的高阶(算是吧)用法;插槽就插槽呗,你给我装什么大尾巴狼。

    普通插槽与作用域插槽

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    <!-- NavigationLink 组件 -->
    <a :href="url" class="nav-link">
     <slot name="icon" />
     <slot>默认链接</slot>
    </a>
    <!-- 使用 NavigationLink -->
    <navigation-link url="/profile">
     <template #icon>
     <font-awesome-icon name="user" />
     </template>
     <span>用户信息</span>
    </navigation-link>
    

    作用域插槽就是给普通插槽添加了传递参数的能力;作用域插槽会被编译为一个返回 VNode 的函数,通过函数调用,可以将子组件的变量传递到父组件;很像 React 的 RenderProps 模式:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    
    <!-- ContactList 组件 -->
    <template>
     <div class="card">
     <div
     class="flex items-center spaced-y-6"
     v-for="contact in contacts"
     :key="contact.id"
     >
     <div>
     <div class="font-bold">
     <!-- render-props -->
     {{ pseudoSlot({ contact }) }}
     </div>
     <div class="font-bold">
     <!-- scoped-slots -->
     <slot :item="contact" />
     </div>
     </div>
     </div>
     </div>
    </template>
    <script>
     export default {
     name: 'ContactList',
     props: ['pseudo-slot'],
     data() {
     return {
     contacts: []
     };
     },
     created() {
     fetch('/contacts.json')
     .then(res => res.json())
     .then(data => {
     this.contacts = data;
     })
     .catch(console.error);
     }
     };
    </script>
    <!-- 使用 ContactList -->
    <contact-list :pseudo-slot="({ contact }) => contact.name.first">
     <template #default="{ item }">
     <a :href="`/contacts/${item.id}`">{{ item.name.last }}</a>
     </template>
    </contact-list>
    

    实现 Renderless 组件

    Renderless 组件不会渲染具体内容,它仅仅管理一些状态和行为;将这些状态和行为通过作用域插槽暴露给父组件,由父组件对渲染内容进行控制。举个别人的 例子 :

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    
    <!-- Renderless 组件 -->
    <script>
     export default {
     props: ['value'],
     data() {
     return {
     tag: ''
     };
     },
     methods: {
     add() {
     if (!this.tag || this.tag.trim().length === 0) {
     return;
     }
     this.$emit('input', [...this.value, this.tag.trim()]);
     this.tag = '';
     },
     remove(tag) {
     this.$emit(
     'input',
     this.value.filter(v => v !== tag)
     );
     }
     },
     render() {
     return this.$scopedSlots.default({
     list: this.value,
     add: this.add, // 传了不一定会用,只是为了适应多种业务场景
     remove: this.remove,
     inputAttrs: {
     value: this.tag
     },
     inputEvents: {
     input: e => {
     this.tag = e.target.value;
     },
     keydown: e => {
     if (e.keyCode === 13) {
     e.preventDefault();
     this.add();
     }
     }
     }
     });
     }
     };
    </script>
    <!-- 使用 Renderless -->
    <template>
     <renderless-component v-model="tags">
     <template #default="{list, remove, inputAttrs, inputEvents}">
     <div class="tags-input">
     <span class="tags-input-tag">
     <span v-for="item in list" :key="item">{{ item }}</span>
     <button class="tags-input-remove" @click="remove">&times;</button>
     </span>
     <input
     class="tags-input-text"
     placeholder="add tag"
     v-bind="inputAttrs"
     v-on="inputEvents"
     />
     </div>
     </template>
     </renderless-component>
    </template>
    <script>
     export default {
     data() {
     return {
     tags: ['Testing', 'Design']
     };
     }
     };
    </script>
    

    当项目中有很多逻辑层相同展示层不同的组件时,Renderless 组件可以很好地实现逻辑复用。

    使用 Renderless 请求数据(Vue)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    
    var CancelToken = axios.CancelToken;
    var source = CancelToken.source();
    Vue.component('fetch-data', {
     props: {
     url: {
     type: String,
     required: true
     },
     method: {
     type: String,
     default: 'GET'
     },
     params: {
     type: Object,
     default: {}
     },
     body: {
     type: Object,
     default: {}
     }
     },
     data() {
     return {
     loading: false,
     response: null,
     error: null
     };
     },
     watch: {
     url() {
     this.handleRequest();
     },
     params: {
     handler: this.handleRequest,
     deep: true
     },
     body: {
     handler: this.handleRequest,
     deep: true
     }
     },
     methods: {
     handleRequest() {
     source.cancel();
     this.loading = true;
     axios({
     url: this.url,
     method: this.method.toUpperCase(),
     params: this.params,
     data: this.body,
     cancelToken: source.token
     })
     .then(({ data }) => {
     this.response = data;
     })
     .catch(error => {
     this.error = error;
     })
     .finally(() => {
     this.loading = false;
     });
     }
     },
     created() {
     this.handleRequest();
     },
     render() {
     if (this.loading) {
     return this.$scopedSlots.default({
     loading: true,
     response: null,
     error: null
     });
     }
     return this.$scopedSlots.default({
     loading: false,
     response: this.response,
     error: this.error
     });
     }
    });
    

    使用 RederProps 请求数据(React)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    
    class FetchData extends React.PureComponent {
     ct = axios.CancelToken.source();
     state = {
     loading: true,
     data: null,
     error: null
     };
     handleRequest = () => {
     this.setState({ loading: true });
     const cancelToken = this.ct.token;
     const { conf } = this.props;
     axios(...conf, cancelToken)
     .then(({ data }) =>
     this.setState({
     data
     })
     )
     .catch(error =>
     this.setState({
     error
     })
     )
     .finally(() => {
     this.setState({
     loading: false
     });
     });
     };
     componentDidMount() {
     this.handleRequest();
     }
     componentDidUpdate() {
     this.setState({
     loading: true,
     data: null,
     error: null
     });
     this.handleRequest();
     }
     componentWillUnmount() {
     if (this.props.conf.methods.toLowerCase() === 'get') {
     this.ct.cancel();
     }
     }
     render() {
     const { children } = this.props;
     // 需要判断是否为函数
     return typeof children === 'function' ? children(this.state) : children;
     }
    }
    const RenderWithFetchData = props => (
     <FetchData {...props}>
     {({ loading, data, error }) => {
     if (loading) {
     return <Loading />;
     }
     if (data) {
     return <UserProfile {...data} />;
     }
     if (error) {
     return <Error {...error} />;
     }
     }}
     </FetchData>
    );
    

    参考

    • Renderless Components In Vue.js


沪ICP备19023445号-2号
友情链接