Bigpipe é uma tecnologia desenvolvida pelo Facebook para otimizar a velocidade de carregamento da página da web. Quase não há artigos implementados com o Node.js na Internet. De fato, não é apenas o Node.js. As implementações do BIGPIPE em outros idiomas são raras na Internet. Muito tempo depois que essa tecnologia apareceu, pensei que depois que toda a estrutura da página da web foi enviada primeiro, usei outra ou várias solicitações de Ajax para solicitar os módulos na página. Não foi há pouco tempo que aprendi que o conceito central de Bigpipe é usar apenas uma solicitação HTTP, mas os elementos da página são enviados em ordem.
Será fácil entender esse conceito central. Graças ao recurso assíncrono do Node.js, é fácil implementar o BigPipe com o Node.js. Este artigo usará exemplos passo a passo para ilustrar as causas da tecnologia Bigpipe e uma implementação simples baseada no Node.js.
Vou usar o Express para demonstrar. Por simplicidade, escolhemos o Jade como o mecanismo de modelo e não usamos o recurso Sub-Template (parcial) do mecanismo, mas, em vez disso, usamos o modelo filho para renderizar HTML como dados do modelo pai.
Primeiro, crie uma pasta Nodejs-Bigpipe e escreva um arquivo package.json da seguinte forma:
A cópia do código é a seguinte:
{
"Nome": "Experimento Bigpipe"
, "versão": "0.1.0"
, "privado": verdadeiro
, "Dependências": {
"Express": "3.xx"
, "consolidar": "mais recente"
, "Jade": "mais recente"
}
}
Execute o NPM Instale para instalar essas três bibliotecas. O consolidação é usado para facilitar a chamada de Jade.
Vamos fazer a tentativa mais simples primeiro, dois arquivos:
App.js:
A cópia do código é a seguinte:
var express = requer ('expresso')
, CONS = requer ('consolidar')
, jade = requer ('jade')
, caminho = requer ('caminho')
var app = express ()
App.Engine ('Jade', Contras.jade)
App.set ('Views', Path.Join (__ Dirname, 'Views'))))
App.Set ('View Engine', 'Jade')
App.use (function (req, res) {
res.render ('layout', {
S1: "Olá, sou a primeira seção."
, S2: "Olá, eu sou a segunda seção."
})
})
App.Listen (3000)
visualizações/layout.jade
A cópia do código é a seguinte:
Doctype html
cabeça
Título Olá, mundo!
estilo
seção {
margem: 20px automático;
borda: 1px cinza pontilhado;
largura: 80%;
Altura: 150px;
}
Seção#S1! = S1
Seção#S2! = S2
Os efeitos são os seguintes:
Em seguida, colocamos dois modelos de seção em dois arquivos de modelo diferentes:
visualizações/s1.jade:
A cópia do código é a seguinte:
H1 parcial 1
.Content! = Conteúdo
visualizações/s2.jade:
A cópia do código é a seguinte:
H1 parcial 2
.Content! = Conteúdo
Adicione alguns estilos ao layout.Jade Style
A cópia do código é a seguinte:
Seção H1 {
tamanho de fonte: 1.5;
preenchimento: 10px 20px;
margem: 0;
fundo da borda: 1px cinza pontilhado;
}
seção div {
margem: 10px;
}
Altere o app.use () parte do app.js para:
A cópia do código é a seguinte:
var temp = {
S1: jade.compile (fs.readfilesync (path.join (__ dirname, 'views', 's1.jade')))))
, s2: jade.compile (fs.readfilesync (path.join (__ dirname, 'views', 's2.jade'))))))
}
App.use (function (req, res) {
res.render ('layout', {
S1: temp.s1 ({content: "Olá, eu sou a primeira seção."})
, s2: temp.s2 ({content: "Olá, eu sou a segunda seção."})
})
})
Antes, disse: "O HTML após a renderização é concluído com o modelo filho como os dados do modelo pai", o que significa que os dois métodos temp.s1 e temp.s2 gerarão o código HTML para os dois arquivos s1.jade e s2.jade, e depois usará essas duas peças de código como os valores das duas variáveis S2 e S2 e s2. e depois usarão esses valores como os valores dos dois variáveis.
Agora a página se parece com o seguinte:
De um modo geral, os dados das duas seções são obtidos separadamente - seja na consulta do banco de dados ou da solicitação RESTful, usamos duas funções para simular essas operações assíncronas.
A cópia do código é a seguinte:
var getData = {
d1: function (fn) {
setTimeout (fn, 3000, null, {content: "Olá, eu sou a primeira seção."})
}
, d2: function (fn) {
setTimeout (fn, 5000, null, {content: "Olá, eu sou a segunda seção."})
}
}
Dessa forma, a lógica no app.use () será mais complicada, e a maneira mais simples de lidar com isso é:
A cópia do código é a seguinte:
App.use (function (req, res) {
getData.d1 (função (err, s1data) {
getData.d2 (função (err, s2data) {
res.render ('layout', {
S1: temp.s1 (s1data)
, s2: temp.s2 (s2data)
})
})
})
})
Isso também obterá os resultados que queremos, mas, neste caso, levará 8 segundos para retornar.
De fato, a lógica de implementação mostra que o getData.d2 começa a ligar após o resultado do getData.d1 ser retornado e eles não têm essa dependência. Podemos usar bibliotecas como o Async, que lida com chamadas assíncronas de JavaScript para resolver esse problema, mas vamos simplesmente escrever aqui:
A cópia do código é a seguinte:
App.use (function (req, res) {
var n = 2
, resultado = {}
getData.d1 (função (err, s1data) {
Result.S1Data = S1Data
--n || WriterSult ()
})
getData.d2 (função (err, s2data) {
Result.S2Data = S2Data
--n || WriterSult ()
})
function writesult () {
res.render ('layout', {
S1: temp.s1 (resultado.s1Data)
, s2: temp.s2 (resultado.s2data)
})
}
})
Isso leva apenas 5 segundos.
Antes da próxima otimização, adicionamos a biblioteca jQuery e colocamos o estilo CSS em arquivos externos. A propósito, adicionaremos o arquivo Runtime.js necessário para usar o modelo Jade que usaremos mais adiante e o executaremos no diretório que contém app.js:
A cópia do código é a seguinte:
Mkdir estático
CD estático
Curl http://code.jquery.com/jquery-1.8.3.min.js -o jQuery.js
ln -s ../node_modules/jade/runtime.min.js jade.js
E retire o código na etiqueta de estilo em layout.jade e coloque -o em estático/style.css, e altere a etiqueta da cabeça para:
A cópia do código é a seguinte:
cabeça
Título Olá, mundo!
link (href = "/static/style.css", rel = "STILELEET")
script (src = "/static/jquery.js")
script (src = "/static/jade.js")
No App.js, simulamos as duas velocidades de download para dois segundos e adicionamos antes do app.use (function (req, res) {:
A cópia do código é a seguinte:
var static = express.static (path.join (__ dirname, 'static')))
App.use ('/static', function (req, res, próximo) {
Settimeout (Static, 2000, req, res, próximo)
})
Devido a arquivos estáticos externos, nossa página agora tem um tempo de carregamento de cerca de 7 segundos.
Se retornarmos a parte da cabeça assim que recebermos uma solicitação HTTP e duas seções esperam até que a operação assíncrona seja concluída antes de retornar, isso usa o mecanismo de codificação de transmissão bloqueada do HTTP. No Node.js, desde que você use o método res.Write (), o cabeçalho de codificação de transferência: o cabeçalho será adicionado automaticamente. Dessa forma, enquanto o navegador carrega o arquivo estático, o servidor Node está aguardando o resultado da chamada assíncrona. Vamos primeiro excluir as duas linhas da seção em layout.jade:
A cópia do código é a seguinte:
Seção#S1! = S1
Seção#S2! = S2
Portanto, não precisamos dar a esse objeto em res.Render () {S1:…, S2:…}, e porque res.Render () ligará para res.END () por padrão, precisamos definir manualmente a função de retorno de chamada após a conclusão da renderização e usar o método res.write () nele. O conteúdo do layout.jade não precisa estar na função de retorno de chamada Writesult (). Podemos devolvê -lo quando recebermos essa solicitação. Observe que adicionamos manualmente o cabeçalho do tipo conteúdo:
A cópia do código é a seguinte:
App.use (function (req, res) {
res.render ('layout', function (err, str) {
se (err) return res.req.next (err)
Res.Setheader ('Content-Type', 'Text/html; charset = utf-8')
Res.Write (STR)
})
var n = 2
getData.d1 (função (err, s1data) {
res.Write ('<seção id = "s1">' + temp.s1 (s1data) + '</section>')
--n || res.nd ()
})
getData.d2 (função (err, s2data) {
res.Write ('<seção id = "s2">' + temp.s2 (s2data) + '</section>')
--n || res.nd ()
})
})
Agora a velocidade final de carregamento está de volta a cerca de 5 segundos. Na operação real, o navegador primeiro recebe o código da peça da cabeça e carrega três arquivos estáticos. Isso leva dois segundos. Então, no terceiro segundo, parcial 1 aparece, o parcial 2 aparece no quinto segundo e o carregamento da página da web termina. Não vou dar uma captura de tela, o efeito da captura de tela é o mesmo que as capturas de tela nos 5 segundos anteriores.
No entanto, é importante observar que esse efeito pode ser alcançado porque getData.d1 é mais rápido que getData.d2. Ou seja, qual bloco na página da web é retornado primeiro depende de quem retorna o resultado da chamada assíncrona da interface por trás dela. Se alterarmos o getData.d1 para retornar em 8 segundos, retornaremos primeiro parcial 2. A ordem de S1 e S2 é revertida e o resultado final da página da web é inconsistente com nossas expectativas.
Esse problema nos leva ao BigPipe, que é uma tecnologia que pode dissociar a ordem de exibição de cada parte da página da web da ordem de transmissão dos dados.
A idéia básica é primeiro transmitir a estrutura geral de toda a página da web, e as peças que precisam ser transmitidas posteriormente são representadas por divs vazios (ou outras tags):
A cópia do código é a seguinte:
res.render ('layout', function (err, str) {
se (err) return res.req.next (err)
Res.Setheader ('Content-Type', 'Text/html; charset = utf-8')
Res.Write (STR)
res.Write ('<seção id = "s1"> </section> <seção id = "s2"> </section>')
})
Em seguida, escreva os dados retornados no JavaScript
A cópia do código é a seguinte:
getData.d1 (função (err, s1data) {
Res.Write ('<Script> $ ("#S1"). html ("' + temp.s1 (s1data) .Replace (/"/g, '// "') + '") </sCript>')
--n || res.nd ()
})
O processamento de S2 é semelhante a isso. Neste momento, você verá que, no segundo segundo solicitando a página da web, duas caixas pontilhadas em branco aparecem, no quinto segundo, o parcial 2 aparece e, no oitavo segundo, parcial 1 aparece e a solicitação da página da web é concluída.
Neste ponto, concluímos a página da web mais simples implementada pela Bigpipe Technology.
Deve -se notar que, se o fragmento da página da web a ser escrito tiver tags de script, como alterar S1.Jade para:
A cópia do código é a seguinte:
H1 parcial 1
.Content! = Conteúdo
script
Alerta ("Alerta de S1.jade")
Em seguida, atualize a página da web e você descobrirá que a frase alerta não foi executada e a página da web terá erros. Verifique o código -fonte e saiba que é um erro causado pela string no <Script>. Basta substituí -lo por <// script>
A cópia do código é a seguinte:
res.Write ('<SCRIPT> $ ("#S1"). HTML ("' + temp.s1 (s1data) .replace (/"/g, '//"'.replace(/</Script>/g,' <// script>) + '") </script>')
Acima, explicamos os princípios do bigpipe e o método básico de implementar o bigpipe com o node.js. E como deve ser usado na realidade? Aqui está um método simples para jogar tijolos e jade, o código é o seguinte:
A cópia do código é a seguinte:
var resproto = requer ('express/lib/resposta')
resproto.pipe = function (seletor, html, substituir) {
this.Write ('<Script>' + '$ ("' + Selector + '").' +
(substitua === true? 'Substituawith': 'html') +
'("' + html.replace (/"/g, '//"').replace(/<//script>/g,' <// script> ') +
'") </script>')
}
Função PipeName (res, nome) {
res.pipeCount = res.pipeCount || 0
res.pipeMap = res.pipeMap || {}
if (res.pipeMap [nome]) retornar
Res.pipeCount ++
res.pipeMap [nome] = this.id = ['Pipe', Math.random (). ToString (). Substring (2), (new Date ()). ValueOf ().
this.res = res
this.name = nome
}
resproto.pipeName = function (nome) {
Retorne o novo nome de tubulação (este, nome)
}
resproto.piplayout = function (exibir, opções) {
var res = este
Object.Keys (Opções) .ForEach (function (key) {
if (options [key] instanceOf PipeName) opções [key] = '<span id = "' + opções [chave] .id + '"> </span>' '
})
res.render (veja, opções, função (err, str) {
se (err) return res.req.next (err)
Res.Setheader ('Content-Type', 'Text/html; charset = utf-8')
Res.Write (STR)
if (! res.pipeCount) res.nd ()
})
}
resproto.pipepartial = function (nome, visualização, opções) {
var res = este
res.render (veja, opções, função (err, str) {
se (err) return res.req.next (err)
res.pipe ('#'+res.pipeMap [nome], str, true)
-Res.pipeCount || res.nd ()
})
}
App.get ('/', function (req, res) {
Res.piplayout ('layout', {
S1: Res.pipename ('S1Name')
, S2: Res.pipename ('S2Name')
})
getData.d1 (função (err, s1data) {
Res.Pipepartial ('S1Name', 'S1', S1Data)
})
getData.d2 (função (err, s2data) {
Res.Pipepartial ('S2Name', 'S2', S2Data)
})
})
Adicione também duas seções no layout.jade:
A cópia do código é a seguinte:
Seção#S1! = S1
Seção#S2! = S2
A idéia aqui é que o conteúdo do Pipe precisa ser colocado com uma tag de extensão primeiro, obtenha os dados de forma assíncrona e renderize o código HTML correspondente antes de emitê -lo para o navegador e substituir o elemento de span de espaço reservado pelo método Replicação do JQuery.
O código deste artigo está em https://github.com/undozen/bigpipe-on-node. Eu fiz cada etapa em um compromisso. Espero que você possa executá -lo localmente e invadi -lo. Como as próximas etapas envolvem a ordem de carregamento, você realmente precisa abrir o navegador para experimentá -lo e não pode vê -lo na captura de tela (na verdade, ele deve ser implementado com a animação GIF, mas eu estava com preguiça de fazê -lo).
Ainda há muito espaço para otimização sobre a prática Bigpipe. Por exemplo, é melhor definir um valor de tempo acionado para o conteúdo do Pipe. Se os dados chamados retornarem de forma assíncrona rapidamente, você não precisará usar o BigPipe. Você pode gerar diretamente uma página da web e enviá -la. Você pode esperar até que a solicitação de dados tenha excedido um certo período de tempo antes de usar o BigPipe. Comparado com o AJAX, o uso do BigPipe não apenas salva o número de solicitações do navegador no servidor Node.js, mas também salva o número de solicitações do servidor Node.js na fonte de dados. No entanto, vamos compartilhar os métodos específicos de otimização e prática depois que a rede de bola de neve usa BigPipe.