Usar serviços JavaScript para criar aplicativos de página única no ASP.NET Core
Por Fiyaz Hasan
Advertência
Os recursos descritos neste artigo estão obsoletos a partir do ASP.NET Core 3.0. Um mecanismo de integração de estruturas SPA mais simples está disponível no pacote Microsoft.AspNetCore.SpaServices.Extensions NuGet. Para obter mais informações, consulte [Anúncio] Tornando obsoletos Microsoft.AspNetCore.SpaServices e Microsoft.AspNetCore.NodeServices.
Um aplicativo de página única (SPA) é um tipo popular de aplicativo da Web devido à sua rica experiência de usuário inerente. A integração de estruturas ou bibliotecas SPA do lado do cliente, como Angular ou React, com estruturas do lado do servidor, como ASP.NET Core, pode ser difícil. Os Serviços JavaScript foram desenvolvidos para reduzir o atrito no processo de integração. Ele permite operação sem interrupções entre as diferentes pilhas de tecnologia de cliente e servidor.
O que são os Serviços JavaScript
Os Serviços JavaScript são uma coleção de tecnologias do lado do cliente para o ASP.NET Core. Seu objetivo é posicionar ASP.NET Core como a plataforma de servidor preferida dos desenvolvedores para a criação de SPAs.
Os Serviços JavaScript consistem em dois pacotes NuGet distintos:
Esses pacotes são úteis nos seguintes cenários:
- Executar JavaScript no servidor
- Usar uma estrutura ou biblioteca SPA
- Crie ativos do lado do cliente com o Webpack
Grande parte do foco neste artigo é colocado no uso do pacote SpaServices.
O que é SpaServices
SpaServices foi criado para posicionar ASP.NET Core como a plataforma de servidor preferida dos desenvolvedores para a criação de SPAs. SpaServices não é necessário para desenvolver SPAs com ASP.NET Core, e não prende os desenvolvedores em uma estrutura de cliente específica.
SpaServices fornece infraestrutura útil, tais como:
- Pré-renderização do lado do servidor
- Webpack Dev Middleware
- Substituição a Quente de Módulos
- Auxiliares de roteamento
Coletivamente, esses componentes de infraestrutura aprimoram o fluxo de trabalho de desenvolvimento e a experiência de tempo de execução. Os componentes podem ser adotados individualmente.
Pré-requisitos para usar SpaServices
Para trabalhar com SpaServices, instale o seguinte:
Node.js (versão 6 ou posterior) com npm
Para verificar se esses componentes estão instalados e podem ser encontrados, execute o seguinte na linha de comando:
node -v && npm -v
Ao implantar num site do Azure, nenhuma ação será necessária —Node.js está instalado e disponível nos ambientes de servidores.
.NET Core SDK 2.0 ou posterior
- No Windows usando o Visual Studio 2017, o SDK é instalado selecionando o o desenvolvimento entre plataformas do .NET Core a carga de trabalho.
Pré-renderização do lado do servidor
Um aplicativo universal (também conhecido como isomórfico) é um aplicativo JavaScript capaz de ser executado tanto no servidor quanto no cliente. Angular, React e outras estruturas populares fornecem uma plataforma universal para esse estilo de desenvolvimento de aplicativos. A ideia é primeiro renderizar os componentes da estrutura no servidor via Node.jse, em seguida, delegar mais execução ao cliente.
ASP.NET Core Tag Helpers fornecidos pela SpaServices simplificam a implementação da pré-renderização do lado do servidor invocando as funções JavaScript no servidor.
Pré-requisitos para pré-renderização no lado do servidor
Instale o pacote npm aspnet-prerendering.
npm i -S aspnet-prerendering
Configuração de pré-renderização do lado do servidor
Os Ajudantes de Tag são tornados detetáveis por meio do registro de namespace no arquivo _ViewImports.cshtml
do projeto:
@using SpaServicesSampleApp
@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"
@addTagHelper "*, Microsoft.AspNetCore.SpaServices"
Esses Ajudantes de Tag abstraem as complexidades da comunicação direta com APIs de baixo nível, aproveitando uma sintaxe semelhante a HTML dentro da visualização Razor:
<app asp-prerender-module="ClientApp/dist/main-server">Loading...</app>
Módulo de Pré-renderização ASP Tag Helper
O asp-prerender-module
Tag Helper, usado no exemplo de código anterior, executa ClientApp/dist/main-server.js
no servidor via Node.js. Por uma questão de clareza, main-server.js
arquivo é um artefato da tarefa de transpilação TypeScript-to-JavaScript no Webpack processo de compilação. Webpack define como alias de ponto de entrada o main-server
; e a análise do gráfico de dependência deste alias começa no arquivo ClientApp/boot-server.ts
.
entry: { 'main-server': './ClientApp/boot-server.ts' },
No exemplo Angular a seguir, o arquivo ClientApp/boot-server.ts
utiliza a função createServerRenderer
e RenderResult
tipo do pacote npm aspnet-prerendering
para configurar a renderização do servidor via Node.js. A marcação HTML destinada à renderização do lado do servidor é passada para uma chamada de função de resolução, que está encapsulada em um objeto JavaScript Promise
fortemente tipado. A importância do objeto Promise
é que ele fornece a marcação HTML de forma assíncrona à página, para injeção no elemento de espaço reservado do DOM.
import { createServerRenderer, RenderResult } from 'aspnet-prerendering';
export default createServerRenderer(params => {
const providers = [
{ provide: INITIAL_CONFIG, useValue: { document: '<app></app>', url: params.url } },
{ provide: 'ORIGIN_URL', useValue: params.origin }
];
return platformDynamicServer(providers).bootstrapModule(AppModule).then(moduleRef => {
const appRef = moduleRef.injector.get(ApplicationRef);
const state = moduleRef.injector.get(PlatformState);
const zone = moduleRef.injector.get(NgZone);
return new Promise<RenderResult>((resolve, reject) => {
zone.onError.subscribe(errorInfo => reject(errorInfo));
appRef.isStable.first(isStable => isStable).subscribe(() => {
// Because 'onStable' fires before 'onError', we have to delay slightly before
// completing the request in case there's an error to report
setImmediate(() => {
resolve({
html: state.renderToString()
});
moduleRef.destroy();
});
});
});
});
});
asp-prerender-data Assistente de Etiqueta
Quando acoplado ao asp-prerender-module
Tag Helper, o asp-prerender-data
Tag Helper pode ser usado para passar informações contextuais da visualização Razor para o JavaScript do lado do servidor. Por exemplo, a marcação a seguir passa dados do usuário para o módulo main-server
:
<app asp-prerender-module="ClientApp/dist/main-server"
asp-prerender-data='new {
UserName = "John Doe"
}'>Loading...</app>
O argumento UserName
recebido é serializado usando o serializador JSON interno e é armazenado no objeto params.data
. No exemplo Angular a seguir, os dados são usados para construir uma saudação personalizada dentro de um elemento h1
:
import { createServerRenderer, RenderResult } from 'aspnet-prerendering';
export default createServerRenderer(params => {
const providers = [
{ provide: INITIAL_CONFIG, useValue: { document: '<app></app>', url: params.url } },
{ provide: 'ORIGIN_URL', useValue: params.origin }
];
return platformDynamicServer(providers).bootstrapModule(AppModule).then(moduleRef => {
const appRef = moduleRef.injector.get(ApplicationRef);
const state = moduleRef.injector.get(PlatformState);
const zone = moduleRef.injector.get(NgZone);
return new Promise<RenderResult>((resolve, reject) => {
const result = `<h1>Hello, ${params.data.userName}</h1>`;
zone.onError.subscribe(errorInfo => reject(errorInfo));
appRef.isStable.first(isStable => isStable).subscribe(() => {
// Because 'onStable' fires before 'onError', we have to delay slightly before
// completing the request in case there's an error to report
setImmediate(() => {
resolve({
html: result
});
moduleRef.destroy();
});
});
});
});
});
Os nomes de propriedade passados nos Ajudantes de Tags são representados com notação PascalCase. Compare isso com JavaScript, onde os mesmos nomes de propriedade são representados com camelCase. A configuração de serialização JSON padrão é responsável por essa diferença.
Para expandir o exemplo de código anterior, os dados podem ser passados do servidor para a exibição hidratando a propriedade globals
fornecida à função resolve
:
import { createServerRenderer, RenderResult } from 'aspnet-prerendering';
export default createServerRenderer(params => {
const providers = [
{ provide: INITIAL_CONFIG, useValue: { document: '<app></app>', url: params.url } },
{ provide: 'ORIGIN_URL', useValue: params.origin }
];
return platformDynamicServer(providers).bootstrapModule(AppModule).then(moduleRef => {
const appRef = moduleRef.injector.get(ApplicationRef);
const state = moduleRef.injector.get(PlatformState);
const zone = moduleRef.injector.get(NgZone);
return new Promise<RenderResult>((resolve, reject) => {
const result = `<h1>Hello, ${params.data.userName}</h1>`;
zone.onError.subscribe(errorInfo => reject(errorInfo));
appRef.isStable.first(isStable => isStable).subscribe(() => {
// Because 'onStable' fires before 'onError', we have to delay slightly before
// completing the request in case there's an error to report
setImmediate(() => {
resolve({
html: result,
globals: {
postList: [
'Introduction to ASP.NET Core',
'Making apps with Angular and ASP.NET Core'
]
}
});
moduleRef.destroy();
});
});
});
});
});
A matriz postList
definida dentro do objeto globals
é anexada ao objeto window
global do navegador. A elevação dessa variável para o escopo global elimina a duplicação de esforços, particularmente no que toca ao carregamento dos mesmos dados, uma vez no servidor e novamente no cliente.
Webpack Middleware de Desenvolvimento
Webpack Dev Middleware introduz um fluxo de trabalho de desenvolvimento simplificado em que o Webpack cria recursos sob demanda. O middleware compila e serve automaticamente recursos do lado do cliente quando uma página é recarregada no navegador. A abordagem alternativa é invocar manualmente o Webpack por meio do script de construção npm do projeto quando uma dependência de terceiros ou o código personalizado for alterado. Um script de construção npm no arquivo package.json
é mostrado no exemplo a seguir:
"build": "npm run build:vendor && npm run build:custom",
Pré-requisitos do Webpack Dev Middleware
Instale o pacote npm aspnet-webpack.
npm i -D aspnet-webpack
Configuração do Webpack Dev Middleware
O Webpack Dev Middleware é registrado no pipeline de solicitação HTTP por meio do seguinte código no método Configure
do arquivo Startup.cs
:
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseWebpackDevMiddleware();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
// Call UseWebpackDevMiddleware before UseStaticFiles
app.UseStaticFiles();
O método de extensão UseWebpackDevMiddleware
deve ser chamado antes registrar o arquivo estático hospedando por meio do método de extensão UseStaticFiles
. Por motivos de segurança, registre o middleware somente quando o aplicativo for executado no modo de desenvolvimento.
A propriedade output.publicPath
do ficheiro webpack.config.js
instrui o middleware a observar a pasta dist
para alterações:
module.exports = (env) => {
output: {
filename: '[name].js',
publicPath: '/dist/' // Webpack dev middleware, if enabled, handles requests for this URL prefix
},
Substituição de Módulo Quente
Pense no recurso Hot Module Replacement (HMR) do Webpack como uma evolução do Webpack Dev Middleware. O HMR introduz todos os mesmos benefícios, mas simplifica ainda mais o fluxo de trabalho de desenvolvimento, atualizando automaticamente o conteúdo da página após a compilação das alterações. Não confunda isso com uma atualização do navegador, o que interferiria no estado atual na memória e na sessão de depuração do SPA. Há um link ao vivo entre o serviço Webpack Dev Middleware e o navegador, o que significa que as alterações são enviadas por push para o navegador.
Pré-requisitos de substituição de módulo quente
Instale o pacote webpack-hot-middleware npm:
npm i -D webpack-hot-middleware
Configuração de substituição de módulo quente
O componente HMR deve ser registrado no pipeline de solicitação HTTP do MVC no método Configure
:
app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions {
HotModuleReplacement = true
});
Como foi verdade com Webpack Dev Middleware, o método de extensão UseWebpackDevMiddleware
deve ser chamado antes do método de extensão UseStaticFiles
. Por motivos de segurança, registre o middleware somente quando o aplicativo for executado no modo de desenvolvimento.
O arquivo webpack.config.js
deve definir uma matriz plugins
, mesmo que ela seja deixada vazia:
module.exports = (env) => {
plugins: [new CheckerPlugin()]
Depois de carregar o aplicativo no navegador, a guia Console das ferramentas de desenvolvedor fornece a confirmação da ativação do HMR:
Auxiliares de roteamento
Na maioria dos SPAs baseados em ASP.NET Core, o roteamento do lado do cliente geralmente é desejado, além do roteamento do lado do servidor. Os sistemas de roteamento SPA e MVC podem funcionar de forma independente, sem interferências. Há, contudo, um caso limite que apresenta desafios: identificar respostas HTTP 404.
Considere o cenário em que uma rota /some/page
sem extensão é usada. Suponha que a solicitação não corresponda a uma rota do lado do servidor, mas seu padrão corresponde a uma rota do lado do cliente. Agora considere uma solicitação de entrada para /images/user-512.png
, que geralmente espera encontrar um arquivo de imagem no servidor. Se esse caminho de recurso solicitado não corresponder a nenhuma rota do lado do servidor ou arquivo estático, é improvável que o aplicativo do lado do cliente o manipule — geralmente é desejado retornar um código de status HTTP 404.
Pré-requisitos dos ajudantes de roteamento
Instale o pacote npm de roteamento do lado do cliente. Usando o Angular como exemplo:
npm i -S @angular/router
Configuração de auxiliares de roteamento
Um método de extensão chamado MapSpaFallbackRoute
é usado no método Configure
:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
routes.MapSpaFallbackRoute(
name: "spa-fallback",
defaults: new { controller = "Home", action = "Index" });
});
As rotas são avaliadas na ordem em que são configuradas. Consequentemente, a rota default
no exemplo de código anterior é usada primeiro para correspondência de padrões.
Criar um novo projeto
Os Serviços JavaScript fornecem modelos de aplicativos pré-configurados. SpaServices é usado nesses modelos em conjunto com diferentes estruturas e bibliotecas, como Angular, React e Redux.
Esses modelos podem ser instalados por meio da CLI do .NET executando o seguinte comando:
dotnet new --install Microsoft.AspNetCore.SpaTemplates::*
Uma lista de modelos de SPA disponíveis é exibida:
Modelos | Nome curto | Idioma | Etiquetas |
---|---|---|---|
MVC ASP.NET Core com Angular | Angular | [C#] | Web/MVC/SPA |
MVC ASP.NET Core com React.js | reagir | [C#] | Web/MVC/SPA |
MVC ASP.NET Core com React.js e Redux | Reactredux | [C#] | Web/MVC/SPA |
Para criar um novo projeto usando um dos modelos SPA, inclua o Short Name do modelo no comando dotnet new. O comando a seguir cria um aplicativo Angular com ASP.NET Core MVC configurado para o lado do servidor:
dotnet new angular
Definir o modo de configuração de tempo de execução
Existem dois modos principais de configuração de tempo de execução:
-
Desenvolvimento:
- Inclui mapas de origem para facilitar a depuração.
- Não otimiza o código do lado do cliente para desempenho.
-
Produção:
- Exclui mapas de origem.
- Otimiza o código do lado do cliente através da agregação e minificação.
ASP.NET Core usa uma variável de ambiente chamada ASPNETCORE_ENVIRONMENT
para armazenar o modo de configuração. Para obter mais informações, consulte Definir o ambiente.
Executar com a CLI do .NET
Restaure os pacotes NuGet e npm necessários executando o seguinte comando na raiz do projeto:
dotnet restore && npm i
Compile e execute o aplicativo:
dotnet run
O aplicativo é iniciado no localhost de acordo com o modo de configuração de tempo de execução . Navegar até http://localhost:5000
no navegador exibe a página de destino.
Executar com o Visual Studio 2017
Abra o arquivo .csproj
gerado pelo comando dotnet new. Os pacotes NuGet e npm necessários são restaurados automaticamente após a abertura do projeto. Esse processo de restauração pode levar até alguns minutos e o aplicativo estará pronto para ser executado quando for concluído. Clique no botão verde Executar ou pressione Ctrl + F5
, e o navegador será aberto na página de destino do aplicativo. O aplicativo é executado em localhost de acordo com o modo de configuração de tempo de execução .
Testar a aplicação
Os modelos SpaServices são pré-configurados para executar testes do lado do cliente usando Karma e Jasmine. Jasmine é um framework popular de testes unitários para JavaScript, enquanto o Karma é um executor de testes para esses testes. O Karma está configurado para funcionar com o Webpack Dev Middleware de modo que o desenvolvedor não seja obrigado a parar e executar o teste toda vez que forem feitas alterações. Quer seja o código a ser executado no caso de teste ou o próprio caso de teste, o teste é realizado automaticamente.
Usando o aplicativo Angular como exemplo, dois casos de teste Jasmine já são fornecidos para o CounterComponent
no arquivo counter.component.spec.ts
:
it('should display a title', async(() => {
const titleText = fixture.nativeElement.querySelector('h1').textContent;
expect(titleText).toEqual('Counter');
}));
it('should start with count 0, then increments by 1 when clicked', async(() => {
const countElement = fixture.nativeElement.querySelector('strong');
expect(countElement.textContent).toEqual('0');
const incrementButton = fixture.nativeElement.querySelector('button');
incrementButton.click();
fixture.detectChanges();
expect(countElement.textContent).toEqual('1');
}));
Abra o prompt de comando no diretório
npm test
O script inicia o executor de teste Karma, que lê as configurações definidas no arquivo karma.conf.js
. Entre outras configurações, o karma.conf.js
identifica os arquivos de teste a serem executados por meio de sua matriz files
:
module.exports = function (config) {
config.set({
files: [
'../../wwwroot/dist/vendor.js',
'./boot-tests.ts'
],
Publicar o aplicativo
Consulte esta questão no GitHub para obter mais informações sobre como publicar no Azure.
Combinar os ativos gerados do lado do cliente e os artefatos ASP.NET Core publicados em um pacote pronto para implantação pode ser complicado. Felizmente, SpaServices orquestra todo esse processo de publicação com um destino MSBuild personalizado chamado RunWebpack
:
<Target Name="RunWebpack" AfterTargets="ComputeFilesToPublish">
<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
<Exec Command="npm install" />
<Exec Command="node node_modules/webpack/bin/webpack.js --config webpack.config.vendor.js --env.prod" />
<Exec Command="node node_modules/webpack/bin/webpack.js --env.prod" />
<!-- Include the newly-built files in the publish output -->
<ItemGroup>
<DistFiles Include="wwwroot\dist\**; ClientApp\dist\**" />
<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
<RelativePath>%(DistFiles.Identity)</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</ResolvedFileToPublish>
</ItemGroup>
</Target>
O alvo MSBuild tem as seguintes responsabilidades:
- Restaure os pacotes npm.
- Crie uma compilação de nível de produção dos ativos de terceiros do lado do cliente.
- Crie uma compilação de nível de produção dos ativos personalizados do lado do cliente.
- Copie os ativos gerados pelo Webpack para a pasta de publicação.
O destino MSBuild é invocado ao executar:
dotnet publish -c Release