Vue: Discussão: Melhor maneira de criar um HOC

Criado em 25 jul. 2017  ·  40Comentários  ·  Fonte: vuejs/vue

Versão

2.4.2

Link de reprodução

https://jsfiddle.net/o7yvL2jd/

Passos para reproduzir

Estou procurando o caminho certo para implementar HoC com vue.js. Mas não foi possível encontrar nenhum exemplo adequado.
O link abaixo é conhecido implementações HoC. Mas não funcionou esperado.
https://jsfiddle.net/o7yvL2jd/

Como posso implementar HoC com vue.js?
Eu só quero saber como implementar o HoC no modo react.js.

O que é esperado?

Eles são HoCs que simplesmente renderizam componentes passados ​​como parâmetros.
HoCs contendo slots e eventos serão renderizados normalmente.

O que está realmente acontecendo?

O elemento a ser renderizado está ausente ou a ordem renderizada difere do baseComponent.
Algumas implementações de HoC não funcionam com manipuladores de eventos.

discussion

Comentários muito úteis

Ok, então eu brinquei com uma maneira de tornar isso mais fácil.

Dê uma olhada aqui: https://jsfiddle.net/Linusborg/j3wyz4d6/

Não estou feliz com a API, pois é um esboço de ideia muito áspero, mas é capaz de fazer qualquer coisa que deveria ser capaz de fazer.

import { createHOC, createRenderFn, normalizeSlots } from 'vue-hoc'
const Component = {
  props: ['message']
  template: `<p>{{message}}</p>`
}
const HOC = createHOC(Component)

createHOC cuidará de:

  • copiando os adereços do componente
  • trocando $createElement com o do pai para resolver os slots adequados.
  • adicione um nome
  • se nenhum segundo argumento for fornecido (como no exemplo acima), ele renderizará o componente e passará:

    • adereços

    • atr

    • ouvintes

    • slots normais

    • slots com escopo

Isso por si só não é muito útil, é claro. Então a diversão acontece no segundo argumento, que é um objeto Component simples.

Se você quiser escrever a função de renderização por conta própria, é claro que pode. Se você quiser apenas estender props, attrs ou listeners, você pode usar o helper createRenderFn . ele criará uma função de renderização como a padrão descrita acima, mas mesclará qualquer attrs , props ou listeners que você passar para ela com os do pai.

const HOC = createHOC(Component, {
  mounted() { 
    console.log(`lifecycle hooks work, it's simply component options`) 
  },
  render: createRenderFn(Component, {
    props: {
      message: 'Hello from your HOC!'
    }
  }
})

Se você quiser escrever sua própria função de renderização, você pode usar o auxiliar normalizeSlots para transformar o objeto de this.$slots em um array apropriado para passar adiante:

const HOC = createHOC(Component, {
  render(h) {
    return h(Component, {
      props: { message: 'hi from HOC'}, // nothing from parent will be passed on
    }, normalizeSlots(this.$slots))
  }
})

Comentários desejados :)

Todos 40 comentários

Olá @eu81273

Obrigado pelo seu interesse neste projeto.

No entanto, seu problema é uma questão de uso/suporte, e o rastreador de problemas é reservado exclusivamente para relatórios de bugs e solicitações de recursos (conforme descrito em nosso Guia de contribuição ).

Nós encorajamos você a perguntar no fórum , Stack Overflow ou em nosso bate- papo do discord e estamos felizes em ajudá-lo.

FWIW, dei uma olhada por interesse pessoal - isso deve funcionar 100% dos casos.

const HOC = WrappedComponent => ({
  props: typeof WrappedComponent === 'function' // accept both a construtor and an options object
    ? WrappedComponent.options.props 
    : WrappedComponent.props,
  render (h) {
    // reduce all slots to a single array again.
    const slots = Object.keys(this.$slots).reduce((arr, key) => arr.concat(this.$slots[key]), []);
    return h(WrappedComponent, {
      attrs: this.$attrs,
      props: this.$props,
      on: this.$listeners,
    }, slots);
 }
});

Editei o HOC04 no seu exemplo, pois era o mais próximo da solução:

https://jsfiddle.net/Linusborg/o7yvL2jd/22/

Edit: ainda um problema com slots, investigando ...

Eu poderia ter resolvido: https://jsfiddle.net/BogdanL/ucpz8ph4/. Os slots são apenas codificados agora, mas isso é trivial de resolver.

Parece que a solução está no método de @lbogdan, mas createElement deve ter uma maneira de pegar slots, assim como pode levar scopedSlots.

No entanto, é _ainda_ muito esforço para criar um HoC. Há muito o que lembrar de passar, enquanto com react você apenas renderiza o WrappedComponent com props.

Acabei de pensar em uma solução muito simples ... deixe-me saber se estiver faltando algo aqui:

const HOC06 = WrappedComponent => Vue.extend({
   mounted () {
      console.log('mounted 6')
   },
   ...WrappedComponent
})

Com base nos exemplos dados por @LinusBorg e @lbogdan , a implementação HoC mais mínima que pode lidar com componentes com slots é:

const HoC = WrappedComponent => ({
    props: typeof WrappedComponent === 'function' 
        ? WrappedComponent.options.props 
        : WrappedComponent.props,
    render (h) {
        const slots = this.$slots;
        const scopedSlots = {};
        Object.keys(slots).map(key => (scopedSlots[key] = () => slots[key]));

        return h(WrappedComponent, {
            attrs: this.$attrs,
            props: this.$props,
            on: this.$listeners,
            scopedSlots,
        });
     }
});

Como o @blocka mencionou, ainda dá muito trabalho criar um HoC com vue.js.

@eu81273

const HOC06 = WrappedComponent => Vue.extend({
   mounted () {
      console.log('mounted 6')
   },
   ...WrappedComponent
})

Isso parece passar no seu teste, não? Claro, você teria que ajustá-lo dependendo se WrappedComponent é um construtor ou objeto, mas não há necessidade de passar slots, eventos ou props.

ainda dá muito trabalho criar um HoC com vue.js.

Além do problema com slots, isso se deve apenas ao fato de o Vue ter uma API mais complexa que o React, o que neste cenário é uma desvantagem. Eu admiro a API mínima do Reacts nesses tipos de casos de uso - o Vue foi projetado apenas com objetivos de design ligeiramente diferentes, então os HOCs não vêm tão facilmente quanto no React.

Mas deve ser bastante trivial criar uma função auxiliar createHOC() que envolva essa configuração inicial para você, não deveria?

Bem, isso realmente depende de qual é o objetivo final. Pelo que entendi, o objetivo do HoC é de alguma forma alterar (decorar) o componente original (WrappedComponent) para adicionar (ou injetar) props, métodos, ouvintes de eventos etc. (muito parecido com um mixin, realmente :smile: ). A variante HOC06 altera apenas a definição do componente, não altera a maneira como é instanciado.

@blocka O objetivo dos HOCs geralmente é obter o estado (por exemplo, de redux / vuex) e injetá-lo nas props do componente encapsulado - isso não funcionaria com sua abordagem.

@LinusBorg certo. Eu sabia que era bom demais para ser verdade e que estava esquecendo algo óbvio.

Eu acho que este é um bom exemplo de implementação de um caso de uso real HoC no Vue: https://github.com/ktsn/vuex-connect.

Vue Hocs seria uma vantagem incrível (já que quase sempre é mencionado em qualquer debate vue vs react). Talvez um repositório oficial possa ser criado para desenvolver um pacote vue-hoc-creator? Dessa forma, poderíamos trabalhar em uma implementação robusta e com suporte

Aliás, tem uma maneira melhor: use $createElement do componente pai em vez do próprio HOC - isso faz com que o filho resolva os slots corretamente:

https://jsfiddle.net/o7yvL2jd/23/

Bonito, mas mais uma razão para haver alguma ferramenta oficial, então nós
nem todos continuem reinventando esse código.

No domingo, 30 de julho de 2017 às 16h33, Thorsten Lünborg [email protected]
escreveu:

Btw, tem uma maneira melhor: use $createElement do componente pai
em vez do próprio HOC - isso faz com que o filho resolva os slots corretamente:

https://jsfiddle.net/o7yvL2jd/23/


Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/vuejs/vue/issues/6201#issuecomment-318927628 ou silenciar
o segmento
https://github.com/notifications/unsubscribe-auth/AACoury4Fix2jsX_lyTsS6CYOiHJaOuVks5sTOiugaJpZM4Oh0Ij
.

Sinto muito por não chegar a uma solução oficial ainda. Ainda bem que você acha bonito.

Minha solução também não é perfeita, há outros problemas para resolver - ou seja, slots com escopo não funcionarão com meu último truque.

edit: oh, não importa, eles funcionam

Uma solução oficial provavelmente será feita, pelo menos eu esperaria que sim - mas tem que ser minuciosamente pensado e testado.

Ok, então eu brinquei com uma maneira de tornar isso mais fácil.

Dê uma olhada aqui: https://jsfiddle.net/Linusborg/j3wyz4d6/

Não estou feliz com a API, pois é um esboço de ideia muito áspero, mas é capaz de fazer qualquer coisa que deveria ser capaz de fazer.

import { createHOC, createRenderFn, normalizeSlots } from 'vue-hoc'
const Component = {
  props: ['message']
  template: `<p>{{message}}</p>`
}
const HOC = createHOC(Component)

createHOC cuidará de:

  • copiando os adereços do componente
  • trocando $createElement com o do pai para resolver os slots adequados.
  • adicione um nome
  • se nenhum segundo argumento for fornecido (como no exemplo acima), ele renderizará o componente e passará:

    • adereços

    • atr

    • ouvintes

    • slots normais

    • slots com escopo

Isso por si só não é muito útil, é claro. Então a diversão acontece no segundo argumento, que é um objeto Component simples.

Se você quiser escrever a função de renderização por conta própria, é claro que pode. Se você quiser apenas estender props, attrs ou listeners, você pode usar o helper createRenderFn . ele criará uma função de renderização como a padrão descrita acima, mas mesclará qualquer attrs , props ou listeners que você passar para ela com os do pai.

const HOC = createHOC(Component, {
  mounted() { 
    console.log(`lifecycle hooks work, it's simply component options`) 
  },
  render: createRenderFn(Component, {
    props: {
      message: 'Hello from your HOC!'
    }
  }
})

Se você quiser escrever sua própria função de renderização, você pode usar o auxiliar normalizeSlots para transformar o objeto de this.$slots em um array apropriado para passar adiante:

const HOC = createHOC(Component, {
  render(h) {
    return h(Component, {
      props: { message: 'hi from HOC'}, // nothing from parent will be passed on
    }, normalizeSlots(this.$slots))
  }
})

Comentários desejados :)

@LinusBorg Muito bom!

O que acho que ajudaria é criar casos de uso de HoC da vida real e resolvê-los usando esses primitivos.

Absolutamente.

Estou mencionando este problema (EDIT (mybad): https://github.com/vuejs/vuejs.org/issues/658).
Como você está usando a API $createElement não documentada, valeria a pena documentá-la para desenvolvedores de plugins.

Seu link está errado (a menos que você realmente queira vincular a um problema de 2014)

Mas sim, tecnicamente a API $ceateElement ainda está faltando na página da API.

@AlexandreBonaventure Esse problema é do vue 0.x dias. :sorriso:
Além disso, createElement está documentado aqui: https://vuejs.org/v2/guide/render-function.html#createElement -Arguments .

A função está documentada como um argumento da função de renderização, mas não está disponível via this.$createElement . Há um problema em aberto para isso em vuejs/vuejs.org/issues/658

@LinusBorg Mas é basicamente a mesma função que é enviada para a função render() , certo?

exatamente o mesmo. Apenas não está documentado claramente que também está disponível fora da função de renderização por meio desse método de instância.

Estou apenas brincando com o exemplo acima e há alguns problemas ao usá-lo em um cenário mais complexo, daí a necessidade de um repositório oficial. Algumas notas:

  • createRenderFn deve verificar se attrs/props/listeners são funções e avaliá-los, isso permitiria que você defina props etc. dinamicamente com base em props existentes.
  • para composição, o componente deve ser o segundo parâmetro e, se todo o método createHOC fosse curry, poderíamos encadear facilmente vários criadores hoc.
  • porque o hoc é adicionado como um mixin, se você tentar encadear 2 hocs juntos (ou seja withDefaultProps(withProps(component, {}), {}) o segundo hoc não tem acesso aos adereços do pai exemplo

Ei pessoal, eu estive olhando para escrever uma implementação disso.
https://github.com/jackmellis/vue-hoc/tree/develop
Deixe-me saber o que você pensa e eu vou olhar para publicá-lo em breve. Também estou pensando em escrever um pacote no estilo recompose.

Um pacote de estilo de recomposição seria ótimo. Realmente poderia ter usado algo
como withState recentemente.

No sábado, 19 de agosto de 2017 às 5h50, Jack [email protected] escreveu:

Ei pessoal, eu estive olhando para escrever uma implementação disso.
https://github.com/jackmellis/vue-hoc/tree/develop
Deixe-me saber o que você pensa e eu vou olhar para publicá-lo em breve. Eu tambem
pensando em escrever um pacote no estilo recompose.


Você está recebendo isso porque foi mencionado.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/vuejs/vue/issues/6201#issuecomment-323513287 ou silenciar
o segmento
https://github.com/notifications/unsubscribe-auth/AACoumQWQMgpVeIvzJ3Ti8A4kLBD04Hhks5sZq_zgaJpZM4Oh0Ij
.

@jackmellis Que bom que você lidera isso :)

Algumas considerações sobre seu feedback que você deu anteriormente:

  • Eu acho que a versão com curry é um ótimo ponto e deve ser o "padrão" de certa forma, pois é assim que os HOCs geralmente são feitos, não são?
  • Bom ponto sobre o problema com mixins. Você já tem uma ideia de como mitigar isso? Eu não tenho um no momento, mas meu pressentimento é que isso deve ser mitigável em uma combinação das variantes de curry que você criou e usando Vue.config.optionsMergeStrategies

Também pensei no nome. Eu não gosto de createRenderFn , algo como renderComponentWith seria mais significativo e faria mais sentido em um cenário em que o incorporamos em alguns outros nós:

render(h) {
  return h('DIV', {staticClass: 'some-styling' }, renderComponentWith({ props: {... } }))
}

  • No final, optei por ambos. Portanto createHOC é o componente primeiro sem curry e, em seguida, há uma variante com curry createHOCc . O ecossistema React é muito focado na funcionalidade, ao contrário do Vue, que atua mais como um framework OOP. Então eu acho que é melhor manter a consistência com o resto do Vue, mas ainda oferecer uma alternativa funcional.
  • Acabei de adicionar algum código para lidar com isso. Em vez de ter o hoc inteiro como um mixin, eu mesclo manualmente o hoc e as opções usando o optionsMergeStrategies. O único problema com isso é que optionsMergeStrategies espera um vm como o último parâmetro, mas estou fazendo a mesclagem antes da instanciação, então não há vm.
  • Estou muito feliz com createRenderFn já que é exatamente isso que está fazendo. Mas quanto mais eu coloco isso junto, menos eu acho que as pessoas vão usá-lo diretamente. O uso geral será algo como:
const hoc = createHOC(
  Component,
  {
    created(){
      /* ... */
    }
  },
  {
    props: (props) => ({
      ...props,
      someProp: 'foo'
    })
  }
);

Talvez eu não entenda muito bem o seu exemplo?

Talvez eu não entenda muito bem o seu exemplo?

Não, acho que você está no caminho certo, meus pensamentos estavam indo na mesma direção quando pensei um pouco mais sobre isso depois da minha última resposta.

No meu POC inicial, incluí-o para que as pessoas pudessem adicionar marcação adicional em torno do componente renderizado, mas isso significaria que não é mais um HOC, pois também traria a interface do usuário ...

Sim, acho que você está tentando renderizar conteúdo adicional, você saiu do território do HOC e pode criar um SFC para lidar com isso.

Acabei de publicar o vue-hoc no npm!

Eu também tenho trabalhado no vue-compose, que é uma sessão de documentação rápida, longe de estar pronto também. Embora seja semelhante ao recompose, o Vue lida com muitas coisas inteligentes (como cálculos de cache e incentiva o uso de this ) então na verdade não precisa de uma sintaxe tão complexa (ou de estilo funcional).

extends funciona de forma diferente, mas pode ser usado para obter resultados semelhantes, isso é verdade.

Vou fechar isso porque acho que o projeto @jackmellis fornece uma base sólida. Não pretendemos incluir uma forma de criar HOC's no core, principalmente porque não vemos um grande benefício sobre mixins/Extended.

principalmente porque não vemos um grande benefício sobre mixins / Extends.

@LinusBorg React já abandonou mixin , HOC traz muitos benefícios.

Este artigo analisou os benefícios:

  • O contrato entre um componente e seus mixins é implícito.
  • Conforme você usa mais mixins em um único componente, eles começam a entrar em conflito.
  • Mixins tendem a adicionar mais estado ao seu componente, enquanto você deve se esforçar por menos.
  • ....

Eu acho que a equipe do Vue deveria considerar apoiar o HoC com mais cuidado... embora não seja fácil (parece que o Vue não foi projetado dessa maneira).

Não estou convencido de que os HoCs sejam um conceito tão superior. Os conflitos potenciais do "contrato implícito" também podem acontecer com HoCs, por exemplo.

Veja esta palestra do mantenedor do React-router sobre isso: https://www.youtube.com/watch?v=BcVAq3YFiuc

Dito isto, também não acho que sejam ruins , são úteis em muitas situações. Eles simplesmente não são a bala mágica que são elogiados como no mundo React, porém, eles têm suas próprias desvantagens.

Como ficou evidente na discussão acima, implementar HoCs no Vue não é tão trivial quanto no React porque a API e a funcionalidade do Vue são mais amplas e mais casos extremos precisam ser atendidos.

Certamente podemos falar sobre como melhorar isso, desde que não exija quebrar nada no Vue - HoCs não valem uma mudança inovadora na minha opinião.

666

a implementação de HoC mais mínima que pode lidar com componentes com slots é:

function hoc(WrappedComponent) {
  return {
    render(h) {
      return h(WrappedComponent, {
        on: this.$listeners,
        attrs:this.$attrs,
        scopedSlots: this.$scopedSlots,
      });
    },
  };
}

comparando com as respostas acima,

  • props config não é necessário, o que pode ser passado de this.$attrs para child
  • slots extras não são necessários, this.$scopedSlots contém slots desde a v2.6.0+

eu escrevi um exemplo para verificar

Esta página foi útil?
0 / 5 - 0 avaliações