Archive for the ‘javascript’ tag
JS Lazyload for Windows Mobile 6.5 widgets
It took me the better part of the afternoon, but I tamed it.
I am creating a widget that has to run on different devices. I implemented a lazy loader to loadmost of the javascript files and it worked on all my targets, except Windows Mobile 6.5. I wanted to be notified when all the files were loaded to proceed to my main app init code, but it was not working on the WM6.5 widget context. On the browser it worked, but when packaged as a widget it didn’t.
I thought I had hit a brick wall and had to resort to something (more) hackish, but in the end the perserverance paid off, with a little luck.
First, for some reason, the attachEvent method does not exist on a script element when running as a widget, although it exists on Mobile IE, which was why it worked.
Poking at a newly created script object, I noticed a onreadystatechange property. Experimenting with it on a live widget I managed to make it call my callback function once, but when I tried to put it to use on my code it didn’t.
After much debugging, in which Bluetooth File Transfer.app didn’t help (on 10.6.2 it crashes 80% of the time when transferring a file to a WM device), I figured it out.
I was creating the new script object, setting all the properties and then adding it to the DOM. This seemed to be a sensible solution, but it didn’t work in this case. Then I tried to add the script element to the DOM, immediately after its creation. Bingo! It worked. I only set the “src” property in the end, to prevent loading the script before I have the chance to set the onreadystatechange callback and everything else. I don’t know if this is really needed, but after wasting lots of time on this today I’ll wait until tomorrow to find out.
I don’t know if this code is still working on the other platforms, but I don’t see any reason not to. Then again, I better check.
Here is the relevant code. I’m posting it here in case some poor soul stumps on the same problem. “cb” is the callback function.
var script = document.createElement("script");
document.getElementsByTagName("head")[0].appendChild(script);
if (script.addEventListener) {
script.addEventListener("load", cb, false);
} else if (script.attachEvent) {
script.attachEvent("onreadystatechange", function() {
if (script.readyState == "loaded") {
cb();
}
});
} else {
script.onreadystatechange = function () {
cb();
};
}
script.type = "text/javascript";
script.src = name;
Mental note, I need some wordpress plugin for posting code.
Codebits Challenge 4
Já acabou o quiz 4 do Codebits, e poucos quizzes me deram tanto gozo fazer como este, pelo que acho que seria interesante fazer um post sobre o assunto. Esta foi a minha participação, mas incorpora várias sugestões de colegas do trabalho que estão perto de mim e que felizmente percebem mais disto que eu. Kudos!
A ideia era simples: implementar uma função em Javascript para listar os programas que dão num canal com um nome que contém uma determinada substring, menores que um determinado número de minutos. Isto no menor número de caracteres possível. Não vou documentar o processo todo, que o quiz foi há duas semanas e já não me lembro de tudo, mas deve dar para perceber o que se tem que fazer neste casos.
Vendo a página de serviços do SAPO, vemos que temos que aceder a dois serviços. Um para determinar a lista de canais para saber a sigla dos que tenham no nome a substring desejada, e outro para pedir a programação destes canais para o dia desejado. A filtragem pela duração será feita localmente.
Aqui temos o primeiro problema: aceder aos serviços do SAPO a partir do HTML do Codebits. Como estão em dois domínios diferentes, não podemos usar as técnicas de Ajax normais, já que se viola a regra de que um site apenas pode aceder ao mesmo domínio (e subdomínios). É uma restrição que faz sentido, mas quse pode contornar facimente com o script tag hack, em escrevemos para o header da página uma nova tag de SCRIPT em que o src é o URL do pedido que pretendemos fazer. Em Firefox e Safari, podemos despoletar um evento que ocorre quando termina o carregamento dos dados. Uma função deste tipo pode ser a seguinte:
function loadJSON(url, callback) {
var tag = document.createElement("script");
tag.src = url;
tag.type="text/javascript";
tag.onload = callback;
document.getElementsByTagName("head")[0].appendChild(tag);
}
Duas notas sobre os pedidos ao serviço: vamos pedir os dados em JSON, que é mais fácil de processar que XML, e vamos pedir que os dados sejam atribuídos a uma variável em javascript (com o parâmetro jsontag), de modo a ser mais fácil o acesso.
Nesta função, o parâmetro callback é invocado quando termina o carregamento do JSON, tal como o evento onload normal. Podemos passar uma função anónima neste parâmetro.
Para o primeiro request, e usando a função anterior, podemos fazer algo assim:
loadJSON("http://services.sapo.pt/EPG/GetChannelListjson?jsontag=Canais", function() {
// init com array vazio
var chn = [];
// caminho para os canais (Canais é o nome da varável que o JSON devolvido no request nos criou)
var chns = Canais.GetChannelListResponse.GetChannelListResult.Channel;
// iterar sobre os canais, acrescentando ao array os que tenham "TV" no nome.
for (i in chns) {
// usei uma RegExp, mas podiamos usar simplesmente um indexOf
var r = new RegExp("TV", "i");
if (r.test(chns[i].Name))
chn.push(escape(chns[i].Sigla));
}
// terminado! aqui já temos na variável "chn" um array com as siglas dos canais que tenham "p1" no nome.
// parte 2 aqui!!!
});
Para o segundo request, as coisas são parecidas. Fazer request (passando as siglas dos canais obtidos no passo anterior) e para cada canal ver todos os programas que tenham menos que X minutos. Ordenar pela hora de início, e mostrar or resultado.
// ...
loadJSON("http://services.sapo.pt/EPG/GetChannelListByDateIntervaljson?channelSiglas="+chn.join(",")+"&startDate="+p2+"+00%3A00%3A00&endDate=01-01-2001+23%3A59%3A59&jsontag=Programacao", function() {
// inicializar a vazio o array com os programas
var progs = [];
// caminho para os canais
var chns = Programacao.GetChannelListByDateIntervalResponse.GetChannelListByDateIntervalResult.Channel;
// para cada canal...
for (j in chns) {
// ...e para cada programa deste canal
var prog = chns[j].Programs.Program;
for (i in prog) {
p = prog[i];
// se duracao menor que 31 minutos (a duração obtida vem em segundos)
if (p.Duration < 31 * 60) {
progs.push(p);
}
}
}
// ordenar os vários programas por hora de início (sort() recebe um parâmetro que é o comparador para determinar a ordenação entre 2 elementos)
progs.sort(function(a, b) {
return a.StartTime < b.StartTime ? -1 : 1;
});
// mostrar os programas, invocado a função o() fornecida.
for (i in progs) {
p = progs[i];
o(p.ChannelName + " at " + p.StartTime + ": " + p.Title + " (" + (p.Duration/60) + ")");
}
});
// ...
Como podemos ver, não é complicado. É moroso, talvez, especialmente para quem tenha pouca experiência em Javascript, mas é tudo uma questão de tentar.
Este quiz foi para mim como uma montanha russa. Até aqui estivemos a subir, a partir daqui é que vem a parte gira.
Este código que aqui meti foi uma versão inicial. Já com algumas optimizações que não consegui evitar fazer, mas ainda há muito a encurtar. Um conselho: nem todas as optimizações originam código menor. É preciso medir bem a alteração que se faz. Coisas como eficiência, validação de código, legibilidade, boas práticas: esqueçam tudo. Um piloto da Red Bull Air Race não se chateia de não ter refeições a bordo do avião.
Há muitas micro-optimizações que podemos fazer, mas posso enumerar o princípio:
- Esqueçam packers/minifiers de Javascript: aqui funcionam mal, porque a escala é muito menor do habitual, e perceberão melhor o código se fizerem tudo à mão.
- Omitir tudo o que não for mesmo necessário. Quebras de linha, pontos e vírgula, “var” na declaração de uma variável, espaços a mais nas atribuições, comparações, etc.
- Inicialização de variáveis, só mesmo se imprescindível (como no caso dos arrays)
- Nomes longos de variáveis e funções, como o “loadJSON”? Um luxo impensável: aqui pode ser ficar apenas como a função “J”. Têm 26*2 variáveis/funções possíveis, são suficientes
- Usar defaults. A tag de script tem por omissão o type “text/javacsript”. Apagar a linha em que se faz “src.type =”.
- Façam a composição de nomes longos com fragmentos.
Para este último ponto é melhor explicar em detalhe. Por exemplo, os dois serviços começam por http://services.sapo.pt/.
var u = "http://services.sapo.pt/"; var a = u + "EPG/GetChannelListjson..."; var b = u + "EPG/GetChannelListByDateIntervaljson...";
Podemos ir mais além, uma vez que vemos que continam a ter partes em comum.
var u = "http://services.sapo.pt/EPG/GetChannelList"; var a = u + "json..."; var b = u + "ByDateIntervaljson...";
Nada mau. Outra coisa que temos ainda em tamanho XXL são os caminhos para as propriedades do resultado do pedido JSON.
var chns = Programacao.GetChannelListByDateIntervalResponse.GetChannelListByDateIntervalResult.Channel;
Isto pode ser muito reduzido. Primeiro, em vez de fazer jsontag=Programacao, fazemos jsontag=B. Depois, em Javascript é igual usar a sintaxe de array ([]) para aceder aos membros de um objecto, pelo que esta linha é equivalente:
var chns = B["GetChannelListByDateIntervalResponse"]["GetChannelListByDateIntervalResult"]["Channel"];
Isto é útil porque agora temos strings nos índices. O que que significa que tal como para o URL dos serviçøs, podemos fazer algo assim:
var g = "GetChannelListByDateInterval"; var chns = B[g + "Response"][g + "Result"]["Channel"];
Muito melhor. Mas o Response/Result é usado noutro local no código, ainda podemos fazer melhor.
var g = "GetChannelListByDateInterval"; var u = "Response"; var v = "Result" var chns = B[g + u][g + v]["Channel"];
Mas o “Channel” também é usado noutros sítios…
var c = "Channel"; var g = "Get" + c + "ListByDateInterval"; var u = "Response"; var v = "Result" var chns = B[g + u][g + v][c];
Esta última optimização já não é muito útil. Que com a nova variáveil e a concatenação acabamos quase por ter o mesmo número de caracteres, mas aqui é só um exemplo e sabemos que o “Channel” é usado noutros sítios, pelo que temos lucro. Estes exemplos também são escritos com vista a serem claros enquanto explico. No meu código, seria algo como isto:
c="Channel",g="Get"+c+"ListByDateInterval",u="Response",v=" Result",chns=B[g+u][g+v][c];
Muito mais denso. Tendo todo o código podemos ver o que podemos reutilizar e sempre se poupam uns caracteres. Chega a uma altura em que é muito difícil espremer mais, mas tal como uma bisnaga de uma pasta de dentes, há sempre um pouquinho mais.
O serviço de EPG devolve os programas que comecem até 20 minutos antes da hora especificada, pelo que isto causava problemas de ter programas do dia anterior. Em vez de meter no código uma condição para lidar com a situação, faço o segundo pedido para programas que comecem às 00:20 do dia. Problema resolvido no mesmo número de caracteres.
Algures no tempo tive a participação mais curta, mas depois perdi o comboio e não consegui apanhá-lo mais. Tentei a técnica dos caracteres “chineses” que acabou por ganhar, mas uns bugs no meu encode/decoder não me deixaram avançar. A ideia em si é simples: o form do quiz contabilizava caracteres, e não bytes. Se um caracter UTF-8 é multi-byte, podemos empacotar vários (acho que no máximo 4) caracteres ASCII (8 bits) no mesmo caracter UTF-8. Há sempre o overhead de um decoder, mas como se viu é irrisório comparado com o que se poupa no empactamento.
Sem mais delongas, esta foi uma das minhas últimas versões. Esta tem 677 caracteres, mas a final acho que tinha menos, mas já desapareceu da intranet do Codebits. Oops.. É o que dá confiar na cloud.
function epg(a,b,d){T="StartTime",E="Channel",D="Duration",L="Get"+E+"List",G=L+"ByDateInterval",O=document,R="Response",U="Result",Z="json?jsontag="
function J(u,k){s=O.createElement("script");s.src="/services.sapo.pt/EPG/"+u;s.onload=k;O.body.appendChild(s)}J(L+Z+"A",function(){c="";z=A[L+R][L+U][E]
for(i in z)with(z[i])if(Name.match(a))c+=Sigla+","
J(G+Z+"B&channelSiglas="+c+"&startDate="+b+"+00:20:00&endDate="+b+"+23:59:59",function(){m=[];c=B[G+R][G+U][E]
for(j in c){g=c[j].Programs.Program;for(i in g)if(g[i][D]<d*60)m.push(g[i])}for(i in m.sort(function(a,b){return a[T]<b[T]?-1:1})){p=m[i];o(p[E+"Name"]+" at "+p[T]+": "+p.Title+" ("+(p[D]/60)+" minutes)\n")}})})}
Só garanto que funcione esta última versão. Como as outras foram quase refeitas enquanto escrevia o post, pode-me ter escapado algo, mas não interessa muito, desde que percebam o funcionamento.
Enjoy!
Objective-J & Cappucinno
O Objective-J e Cappucinno, a linguagem e framework por debaixo do 280 Slides, foram lançados hoje.
O futuro das web apps?
Via DF.
HTML 5 media
Estive para aqui a brincar com a tag <audio> uma das novas funcionalidades do HTML5. Penso que apenas o Safari a suporta para já.
Estive a tentar fazer uma theremin, controlada com o rato: um dos eixos controlava a frequência, outro eixo o vibrato e a scroll-wheel controlava o volume. Tentei gerar amostras curtas, da ordem de 1/4 de segundo, em função das variáveis de controlo, mas ainda tenho alguns problemas a resolver.
O elemento AUDIO fornece um evento “ended” que é invocado quando termina o playback, mas parece-me ter um atraso de meio segundo entre o fim e a invocação do evento. Para contornar esta questão, larguei o “ended” e coloquei um evento agarrado ao timer a gerar e a fazer playback de amostras em sequência, mas tenho problemas de sincronização entre as várias amostras.
Para gerar as amostras dinamicamente estou a construir em memória uma estrutura de um ficheiro WAV, com que alimento o elemento AUDIO com o método “data:”. Neste caso, o atributo “src” do Audio é algo como “data:audio/wav;base64,” + base64_encode(wav).
Sei que estou a abusar com geração de som em tempo real em Javascript, ainda para mais com codificação base64 pelo meio, mas se funcionar bem fica um bom hack…
Sempre pode dar para fazer um dialer DTMF ou um gerador de blips daqui a uns dias.
Pelo menos pode dar para colocar um site a apitar como as caixas do Pingo Doce de cada vez que se passa com o rato num link, tal como faz o anúncio ao site da SIC que passa na SIC Notícias a toda a hora…
Placeholder em jQuery
Uma das funcionalidades não-standard que o Safari implementa é o atributo “placeholder”, utilizado em inputs de um formulário, para indicar um exemplo ou uma sugestão para o valor desse campo.
Ainda que não standard, é útil, e muitas vezes acabamos por ter de implementar algo do género em Javascript para inputs específicos, como caixas de pesquisa ou de login.
Procurei algo do género nos plugins do jQuery e encontrei alguns, mas queria uma solução automática, que não fosse necessário invocar mais Javascript que o necessário, além de especificar o texto no atributo placeholder. Assim em Safari o código Javascript nem sequer é necessário.
Aqui vai um exemplo. Depois ainda faço um plugin a sério para o jQuery.
Script
Porque o Javascript é muito mais do que abrir popups e bloquear o botão direito do rato sobre imagens, ficam aqui alguns recursos que ando há uns dias a pensar como integrar de forma a fazer algo giro.
- Hinclude, templating no browser
- Hinclude, URL templating
- Webkit (ie: Safari/Konq) HTML5 local database storage