Como forçar BundleCollection a liberar pacotes de script em cache no MVC4

85

... ou como aprendi a parar de me preocupar e apenas escrever código em APIs totalmente não documentadas da Microsoft . Existe alguma documentação real do System.Web.Optimizationlançamento oficial ? Porque eu com certeza não consigo encontrar nenhum, não há documentos XML e todas as postagens do blog se referem à RC API, que é substancialmente diferente. Anyhoo ..

Estou escrevendo um código para resolver automaticamente as dependências de javascript e estou criando pacotes dinâmicos a partir dessas dependências. Tudo funciona muito bem, exceto se você editar scripts ou fizer alterações que afetariam um pacote sem reiniciar o aplicativo, as alterações não serão refletidas. Portanto, adicionei uma opção para desativar o armazenamento em cache das dependências para uso no desenvolvimento.

No entanto, aparentemente BundleTablesarmazena em cache a URL, mesmo que a coleção do pacote tenha mudado . Por exemplo, em meu próprio código, quando quero recriar um pacote, faço algo assim:

// remove an existing bundle
BundleTable.Bundles.Remove(BundleTable.Bundles.GetBundleFor(bundleAlias));

// recreate it.
var bundle = new ScriptBundle(bundleAlias);

// dependencies is a collection of objects representing scripts, 
// this creates a new bundle from that list. 

foreach (var item in dependencies)
{
    bundle.Include(item.Path);
}

// add the new bundle to the collection

BundleTable.Bundles.Add(bundle);

// bundleAlias is the same alias used previously to create the bundle,
// like "~/mybundle1" 

var bundleUrl = BundleTable.Bundles.ResolveBundleUrl(bundleAlias);

// returns something like "/mybundle1?v=hzBkDmqVAC8R_Nme4OYZ5qoq5fLBIhAGguKa28lYLfQ1"

Sempre que removo e recrio um pacote com o mesmo alias , absolutamente nada acontece: o bundleUrlretornado de ResolveBundleUrlé o mesmo de antes de remover e recriar o pacote. Por "o mesmo", quero dizer que o hash do conteúdo não é alterado para refletir o novo conteúdo do pacote.

editar ... na verdade, é muito pior do que isso. O pacote em si é armazenado em cache de alguma forma fora da Bundlescoleção. Se eu apenas gerar meu próprio hash aleatório para evitar que o navegador armazene o script em cache, o ASP.NET retorna o script antigo . Portanto, aparentemente, remover um pacote de BundleTable.Bundlesnão faz nada.

Posso simplesmente mudar o alias para contornar este problema, e isso é bom para o desenvolvimento, mas não gosto dessa ideia, pois significa que tenho que descontinuar os aliases após cada carregamento de página ou tenho um BundleCollection que aumenta de tamanho em cada carregamento de página. Se você deixar isso ativado em um ambiente de produção, será um desastre.

Portanto, parece que quando um script é servido, ele é armazenado em cache independentemente do BundleTables.Bundlesobjeto real . Portanto, se você reutilizar um URL, mesmo que tenha removido o pacote ao qual ele se refere antes de reutilizá-lo, ele responde com o que quer que esteja em seu cache e alterar o Bundlesobjeto não limpa o cache - portanto, apenas novos itens (ou em vez disso, novos itens com um nome diferente) seriam usados.

O comportamento parece estranho ... remover algo da coleção deve removê-lo do cache. Mas isso não acontece. Deve haver uma maneira de liberar esse cache e fazer com que ele use o conteúdo atual doBundleCollection vez do que ele armazenou em cache quando o pacote foi acessado pela primeira vez.

Alguma ideia de como eu faria isso?

Existe esse ResetAllmétodo que tem um propósito desconhecido, mas ele simplesmente quebra as coisas de qualquer maneira, então não é isso.

Jamie Treworgy
fonte
Mesmo problema aqui. Acho que consegui resolver o meu. Experimente e veja se funciona para você. Concordo plenamente. Documentação para System.Web.Optimization é lixo e todas as amostras que você pode encontrar na Internet estão desatualizadas.
LeftyX
2
+1 para grande referência no topo combinado com comentário mordaz sobre a expectativa de confiança de MS. E também por fazer a pergunta para a qual desejo uma resposta.
Raif

Respostas:

33

Nós ouvimos sua preocupação com a documentação, infelizmente esse recurso ainda está mudando muito rápido e a geração da documentação tem algum atraso e pode estar desatualizada quase imediatamente. A postagem do blog de Rick está atualizada, e eu tentei responder a perguntas aqui também para divulgar informações atuais nesse meio tempo. No momento, estamos em processo de criação de nosso site codeplex oficial, que sempre terá a documentação atualizada.

Agora, com relação à sua questão específica de como liberar pacotes do cache.

  1. Armazenamos a resposta agrupada dentro do cache ASP.NET usando uma chave gerada a partir do url do pacote solicitado, ou seja Context.Cache["System.Web.Optimization.Bundle:~/bundles/jquery"], também configuramos dependências de cache em relação a todos os arquivos e diretórios que foram usados ​​para gerar este pacote. Portanto, se algum dos arquivos ou diretórios subjacentes mudar, a entrada do cache será liberada.

  2. Na verdade, não oferecemos suporte à atualização em tempo real do BundleTable / BundleCollection por solicitação. O cenário com suporte total é que os pacotes são configurados durante o início do aplicativo (isso é para que tudo funcione corretamente no cenário de farm da web, caso contrário, algumas solicitações de pacote acabariam sendo 404 se enviadas para o servidor errado). Olhando seu exemplo de código, meu palpite é que você está tentando modificar a coleção do pacote dinamicamente em uma solicitação específica? Qualquer tipo de administração / reconfiguração de bundle deve ser acompanhada por uma redefinição de appdomain para garantir que tudo foi configurado corretamente.

Portanto, evite modificar as definições do pacote sem reciclar o domínio do aplicativo. Você é livre para modificar os arquivos reais dentro de seus pacotes, que devem ser detectados automaticamente e gerar novos códigos de hash para os urls de seus pacotes.

Hao Kung
fonte
2
obrigado por trazer seu conhecimento direto para suportar aqui! Sim - estou tentando modificar a coleção de pacotes dinamicamente. Os pacotes são construídos com base em um conjunto de dependências descritas em outro script (ou seja, ele mesmo, não necessariamente parte do pacote) - é por isso que estou tendo esse problema. Como alterar um script que está em um pacote forçará um flush, isso pode ser feito - há a possibilidade de adicionar um método de flush manual? Isso não é crucial - isso é para conveniência durante o desenvolvimento - mas eu odeio criar código que poderia causar problemas se usado acidentalmente no prod.
Jamie Treworgy
Você também pode elaborar sobre a questão do web farm? Adicionar um novo pacote após o início do aplicativo resultaria em ele ficar disponível apenas no servidor em que foi criado - ou apenas tentar alterar um existente? Isso seria um pouco desajeitado para o que estou tentando fazer, já que é necessário fazer a resolução de dependências em tempo de execução.
Jamie Treworgy
Claro, poderíamos adicionar um método equivalente de limpeza de cache explícito, ele já está lá internamente. Com relação ao problema do web farm, basicamente imagine que você tem dois servidores web A e B, sua solicitação vai para A que adiciona o pacote e envia a resposta, seu cliente agora vai buscar o conteúdo do pacote, mas opa, a solicitação vai para servidor B que não registrou o pacote, e aí está o seu 404.
Hao Kung
1
A atualização do cache é lenta; na primeira vez que o pacote é usado (normalmente por meio da renderização de uma referência ao pacote), ele é adicionado ao cache. Se você tiver um gancho de início de aplicativo equivalente, onde você configura seus pacotes em todos os servidores da web antes de começar a lidar com as solicitações, não há problema.
Hao Kung
2
Pelo que eu posso dizer, isso não funciona. Ou seja, se eu alterar o (s) arquivo (s) constituinte (s), o cache do servidor não será limpo conforme declarado aqui. Você tem que reciclar a coisa para obter quaisquer mudanças. Alguém sabe onde está essa documentação oficial?
philw
21

Eu tenho um problema semelhante.
Na minha aula, BundleConfigeu estava tentando ver qual era o efeito do uso BundleTable.EnableOptimizations = true.

public class BundleConfig
{
    public static void RegisterBundles(BundleCollection bundles)
    {
        BundleTable.EnableOptimizations = true;

        bundles.Add(...);
    }
}

Tudo estava funcionando bem.
Em algum ponto, eu estava fazendo uma depuração e defini a propriedade como false.
Lutei para entender o que estava acontecendo porque parecia que o pacote do jquery (o primeiro) não seria resolvido e carregado ( /bundles/jquery?v=).

Depois de alguns xingamentos, acho (?!) consegui resolver as coisas. Tente adicionar bundles.Clear()e bundles.ResetAll()no início do registro e as coisas devem começar a funcionar novamente.

public class BundleConfig
{
    public static void RegisterBundles(BundleCollection bundles)
    {
        bundles.Clear();
        bundles.ResetAll();

        BundleTable.EnableOptimizations = false;

        bundles.Add(...);
    }
}

Percebi que preciso executar esses dois métodos apenas quando alterar a EnableOptimizationspropriedade.

ATUALIZAR:

Indo mais fundo, descobri isso BundleTable.Bundles.ResolveBundleUrle @Scripts.Urlpareço ter problemas para resolver o caminho do pacote.

Para simplificar, adicionei algumas imagens:

imagem 1

Desativei a otimização e agrupei alguns scripts.

imagem 2

O mesmo pacote está incluído no corpo.

imagem 3

@Scripts.Urlme dá o caminho "otimizado" do pacote enquanto @Scripts.Rendergera o apropriado.
A mesma coisa acontece com BundleTable.Bundles.ResolveBundleUrl.

Estou usando o Visual Studio 2010 + MVC 4 + Framework .Net 4.0.

LeftyX
fonte
Hmm ... a coisa é que eu realmente não quero limpar a tabela de bundle, porque ela conterá muitas outras de páginas diferentes (criadas a partir de diferentes conjuntos de dependências). Mas como isso é apenas para trabalhar em um ambiente de desenvolvimento, acho que poderia copiar o conteúdo dele, limpar e adicioná-lo novamente, se isso liberasse o cache. Terrivelmente ineficiente, mas se funcionar, é bom o suficiente para dev.
Jamie Treworgy
Concordo, mas essa é a única opção que tenho. Passei a tarde inteira tentando entender qual era o problema.
LeftyX
2
Eu apenas tentei, AINDA não liberando o cache !! Limpei ResetAll, e tentei definir EnableOptimizationscomo falso na inicialização e em linha quando preciso redefinir o cache, mas nada aconteceu. Argh.
Jamie Treworgy
Com certeza seria bom se o desenvolvedor pudesse enviar uma postagem rápida no blog com pelo menos uma linha sobre os métodos nesses objetos :)
Jamie Treworgy
6
Portanto, apenas para explicar o que esses métodos fazem: Scripts.Url é apenas um alias para BundleTable.Bundles.ResolveBundleUrl, ele também resolverá urls não agrupadas, portanto, é um resolvedor de urls genérico que sabe sobre feixes. Scripts.Render usa o sinalizador EnableOptimizations para determinar se renderizará uma referência aos pacotes ou aos componentes que compõem o pacote.
Hao Kung
8

Tendo em mente as recomendações de Hao Kung para não fazer isso por causa de cenários de web farm, acho que há muitos cenários em que você pode querer fazer isso. Aqui está uma solução:

BundleTable.Bundles.ResetAll(); //or something more specific if neccesary
var bundle = new Bundle("~/bundles/your-bundle-virtual-path");
//add your includes here or load them in from a config file

//this is where the magic happens
var context = new BundleContext(new HttpContextWrapper(HttpContext.Current), BundleTable.Bundles, bundle.Path);
bundle.UpdateCache(context, bundle.GenerateBundleResponse(context));

BundleTable.Bundles.Add(bundle);

Você pode ligar para o código acima a qualquer momento e seus pacotes serão atualizados. Isso funciona tanto quando EnableOptimizations é verdadeiro ou falso - em outras palavras, isso descartará a marcação correta em cenários de depuração ou ao vivo, com:

@Scripts.Render("~/bundles/your-bundle-virtual-path")
Zac
fonte
Leia mais aqui que fala um pouco sobre o cache eGenerateBundleResponse
Zac
4

Também tive problemas com a atualização de pacotes sem reconstruir. Aqui estão as coisas importantes para entender:

  • O pacote NÃO é atualizado se os caminhos dos arquivos mudarem.
  • O pacote é atualizado se o caminho virtual do pacote mudar.
  • O pacote é atualizado se os arquivos no disco mudarem.

Portanto, sabendo que, se você estiver fazendo empacotamento dinâmico, você pode escrever algum código para fazer com que o caminho virtual do pacote seja baseado nos caminhos de arquivo. Eu recomendo fazer o hash dos caminhos dos arquivos e anexar esse hash ao final do caminho virtual do pacote. Dessa forma, quando os caminhos dos arquivos mudam, o caminho virtual também muda e o pacote é atualizado.

Este é o código que acabei com e que resolveu o problema para mim:

    public static IHtmlString RenderStyleBundle(string bundlePath, string[] filePaths)
    {
        // Add a hash of the files onto the path to ensure that the filepaths have not changed.
        bundlePath = string.Format("{0}{1}", bundlePath, GetBundleHashForFiles(filePaths));

        var bundleIsRegistered = BundleTable
            .Bundles
            .GetRegisteredBundles()
            .Where(bundle => bundle.Path == bundlePath)
            .Any();

        if(!bundleIsRegistered)
        {
            var bundle = new StyleBundle(bundlePath);
            bundle.Include(filePaths);
            BundleTable.Bundles.Add(bundle);
        }

        return Styles.Render(bundlePath);
    }

    static string GetBundleHashForFiles(IEnumerable<string> filePaths)
    {
        // Create a unique hash for this set of files
        var aggregatedPaths = filePaths.Aggregate((pathString, next) => pathString + next);
        var Md5 = MD5.Create();
        var encodedPaths = Encoding.UTF8.GetBytes(aggregatedPaths);
        var hash = Md5.ComputeHash(encodedPaths);
        var bundlePath = hash.Aggregate(string.Empty, (hashString, next) => string.Format("{0}{1:x2}", hashString, next));
        return bundlePath;
    }
FriendScottN
fonte
Eu recomendo geralmente evitar Aggregatepara concatenação de strings, devido ao risco de alguém não pensar no algoritmo de Schlemiel, o Pintor inerente ao usar repetidamente +. Em vez disso, apenas faça string.Join("", filePaths). Isso não terá esse problema, mesmo para entradas muito grandes.
ErikE
3

Você tentou derivar de ( StyleBundle ou ScriptBundle ), sem adicionar inclusões em seu construtor e, em seguida, sobrescrever

public override IEnumerable<System.IO.FileInfo> EnumerateFiles(BundleContext context)

Eu faço isso para folhas de estilo dinâmicas e EnumerateFiles é chamado em cada solicitação. Provavelmente não é a melhor solução, mas funciona.

tulde23
fonte
0

Peço desculpas por reviver um thread morto, no entanto, encontrei um problema semelhante com o cache de Bundle em um site de Umbraco, onde queria que as folhas de estilo / scripts diminuíssem automaticamente quando o usuário alterasse a bela versão no backend.

O código que eu já tinha era (no método onSaved para a folha de estilo):

 BundleTable.Bundles.Add(new StyleBundle("~/bundles/styles.min.css").Include(
                           "~/css/main.css"
                        ));

e (onApplicationStarted):

BundleTable.EnableOptimizations = true;

Não importa o que eu tentei, o arquivo "~ / bundles / styles.min.css" não mudou. No cabeçalho da minha página, estava originalmente carregando na folha de estilo assim:

<link rel="stylesheet" href="~/bundles/styles.min.css" />

No entanto, comecei a trabalhar mudando isso para:

@Styles.Render("~/bundles/styles.min.css")

O método Styles.Render puxa uma string de consulta no final do nome do arquivo que eu imagino ser a chave de cache descrita por Hao acima.

Para mim, era simples assim. Espero que isso ajude alguém como eu, que estava pesquisando no Google por horas e só conseguiu encontrar postagens de vários anos!

SY6Dave
fonte