javascript Como lidar com inicialização e renderização de subviews em Backbone.js?




(6)

Não tenho certeza se isso responde diretamente à sua pergunta, mas acho relevante:

http://lostechies.com/derickbailey/2011/10/11/backbone-js-getting-the-model-for-a-clicked-element/

O contexto no qual eu configurei este artigo é diferente, é claro, mas acho que as duas soluções que ofereço, juntamente com os prós e contras de cada uma, devem fazer com que você se mova na direção certa.

Eu tenho três maneiras diferentes de inicializar e renderizar uma visão e suas subvisualizações, e cada uma delas tem problemas diferentes. Estou curioso para saber se existe uma maneira melhor de resolver todos os problemas:

Cenário Um:

Inicialize os filhos na função de inicialização do pai. Dessa forma, nem tudo fica preso na renderização para que haja menos bloqueio na renderização.

initialize : function () {

    //parent init stuff

    this.child = new Child();
},

render : function () {

    this.$el.html(this.template());

    this.child.render().appendTo(this.$('.container-placeholder');
}

Os problemas:

  • O maior problema é que chamar render no pai pela segunda vez removerá todas as ligações de evento childs. (Isto é devido a como o $.html() jQuery funciona.) Isso poderia ser mitigado chamando this.child.delegateEvents().render().appendTo(this.$el); em vez disso, mas depois o primeiro e, na maioria das vezes, você está fazendo mais trabalho desnecessariamente.

  • Ao anexar os filhos, você força a função de renderização a ter conhecimento da estrutura DOM do pai para que você obtenha a ordenação desejada. O que significa que alterar um modelo pode exigir a atualização da função de renderização de uma visualização.

Cenário Dois:

Inicialize os filhos no initialize() do pai ainda, mas em vez de acrescentar, use setElement().delegateEvents() para definir o filho para um elemento no modelo de pais.

initialize : function () {

    //parent init stuff

    this.child = new Child();
},

render : function () {

    this.$el.html(this.template());

    this.child.setElement(this.$('.placeholder-element')).delegateEvents().render();
}

Problemas:

  • Isso torna o delegateEvents() necessário agora, o que é um pouco negativo em relação a isso, sendo necessário apenas em chamadas subsequentes no primeiro cenário.

Cenário Três:

Inicialize os filhos no método render() do pai.

initialize : function () {

    //parent init stuff
},

render : function () {

    this.$el.html(this.template());

    this.child = new Child();

    this.child.appendTo($.('.container-placeholder').render();
}

Problemas:

  • Isso significa que a função de renderização agora precisa ser amarrada com toda a lógica de inicialização também.

  • Se eu editar o estado de um dos modos de exibição filho e, em seguida, chamar render no pai, um filho completamente novo será criado e todo o seu estado atual será perdido. Que também parece que poderia ficar perigoso por vazamentos de memória.

Realmente curioso para fazer seus caras entenderem isso. Qual cenário você usaria? ou há um quarto mágico que resolva todos esses problemas?

Você já acompanhou um estado renderizado para um View? Dizer uma bandeira renderedBefore ? Parece muito janky.


Esta é uma grande pergunta. Backbone é ótimo por causa da falta de suposições que faz, mas significa que você tem que (decidir como) implementar coisas como esta mesmo. Depois de olhar através de minhas próprias coisas, eu acho que eu uso uma mistura do cenário 1 e do cenário 2. Eu não acho que existe um quarto cenário mágico porque, simplesmente, tudo o que você faz no cenário 1 e 2 deve ser feito.

Eu acho que seria mais fácil explicar como eu gosto de lidar com isso com um exemplo. Digamos que eu tenha essa página simples dividida nas visualizações especificadas:

Digamos que o HTML seja, depois de renderizado, algo assim:

<div id="parent">
    <div id="name">Person: Kevin Peel</div>
    <div id="info">
        First name: <span class="first_name">Kevin</span><br />
        Last name: <span class="last_name">Peel</span><br />
    </div>
    <div>Phone Numbers:</div>
    <div id="phone_numbers">
        <div>#1: 123-456-7890</div>
        <div>#2: 456-789-0123</div>
    </div>
</div>

Espero que seja bastante óbvio como o HTML combina com o diagrama.

O ParentView contém duas visualizações de criança, InfoView e PhoneListView , além de algumas divs extras, uma das quais, #name , precisa ser definida em algum momento. PhoneListView mantém exibições PhoneListView , uma matriz de entradas do PhoneView .

Então, para a sua pergunta real. Eu manipulo a inicialização e renderização de maneira diferente com base no tipo de visualização. Eu divido meus pontos de vista em dois tipos: modos de Parent e ponto de vista Child .

A diferença entre eles é simples, as exibições Parent mantêm exibições filho enquanto as exibições Child não. Portanto, no meu exemplo, ParentView e PhoneListView são ParentView Parent , enquanto InfoView e as entradas de PhoneView são ParentView Child .

Como mencionei antes, a maior diferença entre essas duas categorias é quando elas podem renderizar. Em um mundo perfeito, quero que as visualizações dos Parent sejam renderizadas apenas uma vez. Cabe a suas exibições de filhos manipular qualquer nova renderização quando o (s) modelo (s) mudar (em). Visualizações de Child , por outro lado, eu permito renderizá-las novamente a qualquer momento, pois elas não têm nenhuma outra visão dependendo delas.

Em um pouco mais de detalhes, para as visualizações Parent eu gosto de minhas funções de initialize para fazer algumas coisas:

  1. Inicialize minha própria visão
  2. Renderizar minha própria visão
  3. Crie e inicialize qualquer exibição filho.
  4. Atribua a cada criança um elemento de vista dentro da minha visão (por exemplo, o InfoView seria atribuído #info ).

O passo 1 é bastante auto-explicativo.

A segunda etapa, a renderização, é feita para que quaisquer elementos que as exibições filho contenham já existam antes de eu tentar atribuí-los. Ao fazer isso, sei que todos os events filho serão configurados corretamente e posso renderizar novamente seus blocos quantas vezes quiser, sem me preocupar em re-delegar nada. Na verdade, não render nenhuma visualização de criança aqui, permito que ela faça isso em sua própria initialization .

Os passos 3 e 4 são, na verdade, tratados ao mesmo tempo que eu passo ao criar a visão infantil. Eu gosto de passar um elemento aqui como eu sinto que o pai deve determinar onde, em sua própria opinião, a criança pode colocar seu conteúdo.

Para renderização, tento mantê-lo simples para as visualizações dos Parent . Eu quero que a função de render não faça nada além de renderizar a exibição pai. Nenhuma delegação de evento, nenhuma renderização de exibições de filhos, nada. Apenas um simples render.

Às vezes isso nem sempre funciona. Por exemplo, no meu exemplo acima, o elemento #name precisará ser atualizado sempre que o nome dentro do modelo for alterado. No entanto, esse bloco faz parte do modelo ParentView e não é tratado por uma visualização dedicada de Child , então eu trabalho com isso. Eu criarei algum tipo de função subRender que apenas substitui o conteúdo do elemento #name , e não precisa #parent todo o elemento #parent . Isto pode parecer um hack, mas eu realmente achei que funciona melhor do que ter que se preocupar em renderizar novamente o DOM inteiro e reconectar elementos e tal. Se eu realmente quisesse torná-lo limpo, criaria uma nova exibição Child (semelhante ao InfoView ) que #name com o bloco #name .

Agora, para as exibições Child , a initialization é bem semelhante às exibições Parent , sem a criação de outras exibições Child . Assim:

  1. Inicialize minha opinião
  2. O programa de instalação liga para ouvir qualquer alteração no modelo que me interessa
  3. Renderizar minha visão

Renderização de visualização de Child também é muito simples, basta processar e definir o conteúdo do meu el . Mais uma vez, não mexer com delegação ou qualquer coisa assim.

Aqui está um exemplo de código do que meu ParentView pode parecer:

var ParentView = Backbone.View.extend({
    el: "#parent",
    initialize: function() {
        // Step 1, (init) I want to know anytime the name changes
        this.model.bind("change:first_name", this.subRender, this);
        this.model.bind("change:last_name", this.subRender, this);

        // Step 2, render my own view
        this.render();

        // Step 3/4, create the children and assign elements
        this.infoView = new InfoView({el: "#info", model: this.model});
        this.phoneListView = new PhoneListView({el: "#phone_numbers", model: this.model});
    },
    render: function() {
        // Render my template
        this.$el.html(this.template());

        // Render the name
        this.subRender();
    },
    subRender: function() {
        // Set our name block and only our name block
        $("#name").html("Person: " + this.model.first_name + " " + this.model.last_name);
    }
});

Você pode ver minha implementação de subRender aqui. Por ter alterações ligadas a subRender ao invés de render , eu não tenho que me preocupar em explodir e reconstruir todo o bloco.

Aqui está o código de exemplo para o bloco InfoView :

var InfoView = Backbone.View.extend({
    initialize: function() {
        // I want to re-render on changes
        this.model.bind("change", this.render, this);

        // Render
        this.render();
    },
    render: function() {
        // Just render my template
        this.$el.html(this.template());
    }
});

As ligações são a parte importante aqui. Ao ligar-me ao meu modelo, nunca preciso me preocupar em chamar manualmente o render . Se o modelo for alterado, esse bloco será renderizado novamente sem afetar outras exibições.

O PhoneListView será semelhante ao ParentView , você só precisará de um pouco mais de lógica na initialization e nas funções de render para manipular as coleções. A maneira como você lida com a coleção depende de você, mas pelo menos você precisa estar ouvindo os eventos da coleção e decidindo como você deseja renderizar (anexar / remover ou renderizar novamente todo o bloco). Eu pessoalmente gosto de acrescentar novas visualizações e remover as antigas, não re-renderizar toda a visão.

O PhoneView será quase idêntico ao InfoView , apenas ouvindo as mudanças de modelo com as quais se importa.

Espero que isso tenha ajudado um pouco, por favor, deixe-me saber se alguma coisa é confusa ou não detalhada o suficiente.


O que eu faço é dar a cada criança uma identidade (que o Backbone já fez para você: cid)

Quando o Container faz a renderização, usando o 'cid' e 'tagName' geram um marcador de posição para cada filho, portanto, no modelo, os filhos não têm idéia sobre onde ele será colocado pelo Contêiner.

<tagName id='cid'></tagName>

do que você pode usar

Container.render()
Child.render();
this.$('#'+cid).replaceWith(child.$el);
// the rapalceWith in jquery will detach the element 
// from the dom first, so we need re-delegateEvents here
child.delegateEvents();

não é necessário nenhum espaço reservado especificado, e o Container só gera o espaço reservado em vez da estrutura DOM do filho. Cotainer e Children ainda estão gerando elementos DOM próprios e apenas uma vez.


Aqui está um mixin leve para criar e renderizar subvisualizações, o que eu acho que resolve todos os problemas neste tópico:

https://github.com/rotundasoftware/backbone.subviews

A abordagem adotada por esse plug é criar e renderizar subvisualizações após a primeira vez que a exibição pai é renderizada. Em seguida, nas renderizações subseqüentes da visualização pai, $ .detach os elementos da subvisualização, renderize novamente o pai, insira os elementos da subvisualização nos locais apropriados e renderize-os novamente. Dessa forma, objetos de subvisualização são reutilizados em renderizações subseqüentes e não há necessidade de delegar novamente eventos.

Note que o caso de uma visão de coleção (onde cada modelo na coleção é representado com uma subvisão) é bem diferente e merece sua própria discussão / solução, eu acho. A melhor solução geral que conheço nesse caso é o CollectionView em Marionette .

EDIT: Para o caso de exibição de coleção, você também pode querer verificar esta implementação mais orientada a interface do usuário , se você precisar de seleção de modelos com base em cliques e / ou arrastando e soltando para reordenação.


Estou tentando evitar o acoplamento entre visualizações como essas. Existem duas maneiras que costumo fazer:

Use um roteador

Basicamente, você permite que sua função de roteador inicialize a exibição pai e filho. Portanto, a visão não tem conhecimento um do outro, mas o roteador lida com tudo.

Passando o mesmo el para ambas as vistas

this.parent = new Parent({el: $('.container-placeholder')});
this.child = new Child({el: $('.container-placeholder')});

Ambos têm conhecimento do mesmo DOM e você pode encomendá-los como quiser.


Kevin Peel dá uma ótima resposta - aqui está a minha versão dr:

initialize : function () {

    //parent init stuff

    this.render(); //ANSWER: RENDER THE PARENT BEFORE INITIALIZING THE CHILD!!

    this.child = new Child();
},




backbone.js