Skip to content

Commit

Permalink
feat(router): provide server context awareness to routing and HttpCli…
Browse files Browse the repository at this point in the history
…ent requests (#1223)
  • Loading branch information
brandonroberts authored Jul 26, 2024
1 parent f5c7c18 commit b1cdd08
Show file tree
Hide file tree
Showing 26 changed files with 441 additions and 96 deletions.
1 change: 0 additions & 1 deletion apps/analog-app/.env

This file was deleted.

8 changes: 6 additions & 2 deletions apps/analog-app/src/app/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@ import {
} from '@angular/common/http';
import { ApplicationConfig } from '@angular/core';
import { provideClientHydration } from '@angular/platform-browser';
import { provideFileRouter } from '@analogjs/router';
import { provideFileRouter, requestContextInterceptor } from '@analogjs/router';
import { withNavigationErrorHandler } from '@angular/router';

import { cookieInterceptor } from './interceptors/cookies.interceptor';

export const appConfig: ApplicationConfig = {
providers: [
provideFileRouter(withNavigationErrorHandler(console.error)),
provideHttpClient(withFetch(), withInterceptors([cookieInterceptor])),
provideHttpClient(
withFetch(),
withInterceptors([cookieInterceptor, requestContextInterceptor])
),
provideClientHydration(),
],
};
3 changes: 1 addition & 2 deletions apps/analog-app/src/app/cart.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { Product } from './products';
})
export class CartService {
items: Product[] = [];
private readonly apiURL = import.meta.env.VITE_ANALOG_PUBLIC_BASE_URL;

constructor(private http: HttpClient) {}

Expand All @@ -26,7 +25,7 @@ export class CartService {

getShippingPrices() {
return this.http.get<{ type: string; price: number }[]>(
`${this.apiURL}/assets/shipping.json`
`/assets/shipping.json`
);
}
}
19 changes: 6 additions & 13 deletions apps/analog-app/src/main-cf.server.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import 'zone.js/node';
import { InjectionToken, enableProdMode } from '@angular/core';
import { enableProdMode } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { renderApplication } from '@angular/platform-server';
import { APP_BASE_HREF } from '@angular/common';
import { provideServerContext } from '@analogjs/router/server';
import { ServerContext } from '@analogjs/router/tokens';

import { config } from './app/app.config.server';
import { AppComponent } from './app/app.component';
Expand All @@ -11,27 +12,19 @@ if (import.meta.env.PROD) {
enableProdMode();
}

const REQUEST = new InjectionToken<Request>('REQUEST');
const RESPONSE = new InjectionToken<Response>('RESPONSE');
const baseHref = process.env['CF_PAGES_URL'] ?? `http://localhost:8888`;

export function bootstrap() {
return bootstrapApplication(AppComponent, config);
}

export default async function render(
url: string,
document: string,
{ req, res }: { req: Request; res: Response }
serverContext: ServerContext
) {
const html = await renderApplication(bootstrap, {
document,
url: `${baseHref}${url}`,
platformProviders: [
{ provide: REQUEST, useValue: req },
{ provide: RESPONSE, useValue: res },
{ provide: APP_BASE_HREF, useValue: baseHref },
],
url,
platformProviders: [provideServerContext(serverContext)],
});

return html;
Expand Down
14 changes: 5 additions & 9 deletions apps/analog-app/src/main.server.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import 'zone.js/node';
import { InjectionToken, enableProdMode } from '@angular/core';
import { enableProdMode } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { renderApplication } from '@angular/platform-server';
import { provideServerContext } from '@analogjs/router/server';
import { ServerContext } from '@analogjs/router/tokens';

import { config } from './app/app.config.server';
import { AppComponent } from './app/app.component';
Expand All @@ -10,25 +12,19 @@ if (import.meta.env.PROD) {
enableProdMode();
}

const REQUEST = new InjectionToken<Request>('REQUEST');
const RESPONSE = new InjectionToken<Response>('RESPONSE');

export function bootstrap() {
return bootstrapApplication(AppComponent, config);
}

export default async function render(
url: string,
document: string,
{ req, res }: { req: Request; res: Response }
serverContext: ServerContext
) {
const html = await renderApplication(bootstrap, {
document,
url,
platformProviders: [
{ provide: REQUEST, useValue: req },
{ provide: RESPONSE, useValue: res },
],
platformProviders: [provideServerContext(serverContext)],
});

return html;
Expand Down
8 changes: 2 additions & 6 deletions apps/analog-app/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,11 @@ import { defineConfig, Plugin, splitVendorChunkPlugin } from 'vite';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';

// Only run in Netlify CI
let base = process.env['URL'] || 'http://localhost:3000';
if (process.env['NETLIFY'] === 'true') {
let base = process.env['URL'];

if (process.env['CONTEXT'] === 'deploy-preview') {
base = `${process.env['DEPLOY_PRIME_URL']}/`;
}

// set process.env.VITE_ANALOG_PUBLIC_BASE_URL = base URL
process.env['VITE_ANALOG_PUBLIC_BASE_URL'] = base;
}

// https://vitejs.dev/config/
Expand All @@ -38,7 +34,7 @@ export default defineConfig(({ mode }) => {
prerender: {
routes: ['/', '/cart'],
sitemap: {
host: process.env['VITE_ANALOG_PUBLIC_BASE_URL'],
host: base,
},
},
vite: {
Expand Down
142 changes: 142 additions & 0 deletions apps/docs-app/docs/features/data-fetching/overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Overview

Data fetching in Analog builds on top of concepts in Angular, such as using `HttpClient` for making API requests.

## Using HttpClient

Using `HttpClient` is the recommended way to make API requests for internal and external endpoints. The context for the request is provided by the `provideServerContext` function for any request that uses `HttpClient` and begins with a `/`.

## Server Request Context

On the server, use the `provideServerContext` function from the Analog router in the `main.server.ts`.

```ts
import 'zone.js/node';
import { enableProdMode } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { renderApplication } from '@angular/platform-server';

// Analog server context
import { provideServerContext } from '@analogjs/router/server';
import { ServerContext } from '@analogjs/router/tokens';

import { config } from './app/app.config.server';
import { AppComponent } from './app/app.component';

if (import.meta.env.PROD) {
enableProdMode();
}

export function bootstrap() {
return bootstrapApplication(AppComponent, config);
}

export default async function render(
url: string,
document: string,
serverContext: ServerContext
) {
const html = await renderApplication(bootstrap, {
document,
url,
platformProviders: [provideServerContext(serverContext)],
});

return html;
}
```

This provides the `Request` and `Response`, and `Base URL` from the server and registers them as providers that can be injected and used.

## Injection Functions

```ts
import { inject } from '@angular/core';
import {
injectRequest,
injectResponse,
injectBaseURL,
} from '@analogjs/router/tokens';

class MyService {
request = injectRequest(); // <- Server Request Object
response = injectResponse(); // <- Server Response Object
baseUrl = injectBaseURL(); // <-- Server Base URL
}
```

## Request Context Interceptor

Analog also provides `requestContextInterceptor` for the HttpClient that handles transforming any request to URL beginning with a `/` to a full URL request on the server, client, and during prerendering.

Use it with the `withInterceptors` function from the `@angular/common/http` packages.

```ts
import {
provideHttpClient,
withFetch,
withInterceptors,
} from '@angular/common/http';
import { ApplicationConfig } from '@angular/core';
import { provideClientHydration } from '@angular/platform-browser';
import { provideFileRouter, requestContextInterceptor } from '@analogjs/router';
import { withNavigationErrorHandler } from '@angular/router';

export const appConfig: ApplicationConfig = {
providers: [
provideFileRouter(withNavigationErrorHandler(console.error)),
provideHttpClient(
withFetch(),
withInterceptors([requestContextInterceptor])
),
provideClientHydration(),
],
};
```

> Make sure the `requestContextInterceptor` is **last** in the array of interceptors.
## Making Requests

In your component/service, use `HttpClient` along with [/docs/features/api/overview](API routes) with providing a full URL.

An example API route that fetches todos.

```ts
// src/server/routes/v1/todos.ts -> /api/v1/todos
import { eventHandler } from 'h3';

export default eventHandler(async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/todos');
const todos = await response.json();

return todos;
});
```

An example service that fetches todos from the API endpoint.

```ts
// todos.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

import { Todo } from './todos';

@Injectable({
providedIn: 'root',
})
export class TodosService {
http = inject(HttpClient);

getAll() {
return this.http.get<Todo[]>('/api/v1/todos');
}

getData() {
return this.http.get<Todo[]>('/assets/data.json');
}
}
```

Data requests also use Angular's `TransferState` to store any requests made during Server-Side Rendering, and are transferred to prevent an additional request during the initial client-side hydration.
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,6 @@

Analog supports fetching data from the server before loading a page. This can be achieved by defining an async `load` function in `.server.ts` file of the page.

## Setting the Public Base URL

Analog requires the public base URL to be set when using the server-side data fetching. Set an environment variable, using a `.env` file to define the public base URL.

```
// .env
VITE_ANALOG_PUBLIC_BASE_URL="http://localhost:5173"
```

The environment variable must also be set when building for deployment.

## Fetching the Data

To fetch the data from the server, create a `.server.ts` file that contains the async `load` function alongside the `.page.ts` file.
Expand Down Expand Up @@ -123,3 +112,14 @@ export const routeMeta: RouteMeta = {
},
};
```

## Overriding the Public Base URL

Analog automatically infers the public base URL to be set when using the server-side data fetching through its [Server Request Context](/docs/features/data-fetching/overview#server-request-context) and [Request Context Interceptor](/docs/features/data-fetching/overview#request-context-interceptor). To explcitly set the base URL, set an environment variable, using a `.env` file to define the public base URL.

```
// .env
VITE_ANALOG_PUBLIC_BASE_URL="http://localhost:5173"
```

The environment variable must also be set when building for deployment.
24 changes: 0 additions & 24 deletions apps/docs-app/docs/features/deployment/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,30 +118,6 @@ export default defineConfig(({ mode }) => ({

Analog supports deploying to [Cloudflare](https://cloudflare.com/) Pages with minimal configuration.

### Updating the Server Entry Point

The `main.server.ts` file should be updated to provide the full URL and the `APP_BASE_HREF` token on the server for Cloudflare support.

```ts
import { renderApplication } from '@angular/platform-server';
import { APP_BASE_HREF } from '@angular/common';
/// imports and bootstrap code ...

// set the base href
const baseHref = process.env['CF_PAGES_URL'] ?? `http://localhost:8888`;

export default async function render(url: string, document: string) {
// Use the full URL and provide the APP_BASE_HREF
const html = await renderApplication(bootstrap, {
document,
url: `${baseHref}${url}`,
platformProviders: [{ provide: APP_BASE_HREF, useValue: baseHref }],
});

return html;
}
```

### Deploying to Cloudflare

To connect your repository and deploy automatically to Cloudflare:
Expand Down
5 changes: 5 additions & 0 deletions apps/docs-app/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ const sidebars = {
type: 'category',
label: 'Data Fetching',
items: [
{
type: 'doc',
id: 'features/data-fetching/overview',
label: 'Overview',
},
{
type: 'doc',
id: 'features/data-fetching/server-side-data-fetching',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import {
provideServerRendering,
ɵSERVER_CONTEXT as SERVER_CONTEXT,
} from '@angular/platform-server';
import { provideServerRendering } from '@angular/platform-server';
import { appConfig } from './app.config';

const serverConfig: ApplicationConfig = {
providers: [
provideServerRendering(),
{ provide: SERVER_CONTEXT, useValue: 'ssr-analog' },
],
providers: [provideServerRendering()],
};

export const config = mergeApplicationConfig(appConfig, serverConfig);
Loading

0 comments on commit b1cdd08

Please sign in to comment.