头图

Angular Universal

This article introduces Angular Universal (Unified Platform), a technology for running Angular applications on the server side, namely server-side rendering.

As shown in the following figure, the dependency @nguniversal/express-engine defined in package.json:

A standard Angular application will run in the browser, and it will render the page in the DOM in response to user operations. And Angular Universal will run on the server side to generate some static application pages, which will be launched later on the client side. This means that the rendering of the app is usually faster, allowing users to view the layout of the app before it becomes fully interactive.

The HTML source code returned by server-side rendering does not load the corresponding .js file and cannot respond to user input, but it can give users an intuitive and complete page layout.

The Node.js Express Web server will use Universal to compile HTML pages according to the client's request.

To create the server application module app.server.module.ts, run the following CLI command:

ng add @nguniversal/express-engine

This command will automatically generate the following files highlighted in green:

To use Universal to render your application on the local system, use the following command:

npm run dev:ssr

This serve-ssr is defined in Angular.json:

The server target is defined as follows:

It works fine when navigating through routerLinks because they use native link tags (\<a>).

It does not support any user events other than clicking routerLink. You have to wait for the complete client application to be up and running, or use a library like preboot to buffer these events so that you can replay these events after the client script is loaded.

Angular Universal can generate a static version of the application for you. It is easy to search, linkable, and does not require JavaScript when browsing. It also allows the site to be previewed, because each URL returns a fully rendered page.

With Angular Universal, you can generate "landing pages" for your application, and they look just like a complete application. These landing pages are pure HTML and can be displayed even if JavaScript is disabled. These pages will not handle browser events, but they can use routerLink to navigate the site.

In practice, you may want to use a static version of the landing page to keep the user's attention. At the same time, you will also load the complete Angular application behind the scenes. Users will feel that the landing page appears almost immediately, and when the complete application is loaded, they can get a complete interactive experience.

The Universal Web server uses the static HTML rendered by the Universal template engine to respond to requests for application pages. The server receives and responds to HTTP requests from the client (usually a browser), and replies to static files such as scripts, CSS, and images. It can directly respond to data requests or act as a proxy for an independent data server to respond.

Any kind of Web server technology can be used as a Universal application server, as long as it can call Universal's renderModule() function. The principles and decision points discussed here also apply to any Web server technology.

Universal applications use the platform-server package (rather than platform-browser), which provides a server-side implementation of DOM, XMLHttpRequest, and other low-level features that do not depend on the browser.

The server (Node.js Express server is used in this example) will pass the client's request to the application page to NgUniversal's ngExpressEngine. In internal implementation, it will call Universal's renderModule() function, and it also provides useful utility functions such as caching.

For specific debugging steps, refer to these articles of mine:

Use browser API

Since the Universal application does not run in the browser, some APIs and other capabilities of the browser may be missing on the server.

For example, server-side applications cannot reference global objects unique to the browser, such as window, document, navigator, or location.

Angular provides some injectable abstraction layers of these objects, such as Location or DOCUMENT, which can be used as equivalent substitutes for the APIs you call. If Angular does not provide it, you can also write your own abstraction layer. When it runs in the browser, it delegates it to the browser API. When it runs in the server, it provides a surrogate implementation that meets the requirements ( Also called shimming).

Similarly, since there are no mouse or keyboard events, Universal apps cannot rely on the user to click a button to display every component. The Universal application must only decide what to render based on the request from the client. Making the application routable is a good solution.

Universal template engine

The core logic of server.ts is shown in the following code:

const server = express();
  const distFolder = join(process.cwd(), 'dist/mystore/browser');
  const indexHtml = existsSync(join(distFolder, 'index.original.html'))
    ? 'index.original.html'
    : 'index';

  server.set('trust proxy', 'loopback');

  server.engine(
    'html',
    ngExpressEngine({
      bootstrap: AppServerModule,
    })
  );

The source of ngExpressEngine:

const ngExpressEngine = NgExpressEngineDecorator.get(engine, { timeout: 90000, concurrency: 1, 
  forcedSsrTimeout:90000,
  maxRenderTime:100000,
  cache: true, cacheSize: 10,
  renderingStrategyResolver: (req) => RenderingStrategy.DEFAULT});

ngExpressEngine() is a wrapper for Universal's renderModule() function. It converts the client request into an HTML page rendered by the server. It accepts an object with the following properties, of type NgSetupOptions:

bootstrap: The root NgModule or NgModule factory used to bootstrap the application when rendering on the server. For SAP Commerce Cloud applications, it is AppServerModule. It is the bridge between the Universal server-side renderer and the Angular application.

The ngExpressEngine() function returns a promise that will be resolved into a rendered page. Next, your engine has to decide what to do with this page. In the Promise callback function of this engine, the rendered page is returned to the Web server, and then the server forwards it to the client through an HTTP response.

Filter the requested URL

The Web server must distinguish requests for application pages from other types of requests.

This is not as simple as intercepting the request for the root path /. The browser can request any routing address in the application, such as /dashboard, /heroes, or /detail:12. In fact, if the application will only be rendered by the server, any link clicked in the application will be sent to the server, just as the address during navigation will be sent to the router.

Fortunately, application routes have some common characteristics: their URLs generally do not have a file extension. (Data requests may also lack extensions, but they are easy to identify because they always start with /api. All static resource requests will have an extension, such as main.js or /node_modules/zone.js/ dist/zone.js).

Due to the use of routing, we can easily identify these three types of requests and process them separately.

  • Data request: the requested URL starts with /api
  • Application navigation: requested URL without extension
  • Static resources: all other requests.

The Node.js Express server is a pipeline composed of a series of middleware, which filters and processes URL requests one by one. You can call app.get() to configure the pipeline of the Express server, just like the following data request:

// TODO: implement data requests securely
server.get('/api/**', (req, res) => {
  res.status(404).send('data requests are not yet supported');
});

The meaning of the above code is that the current SSR server does not support processing data requests.

The following code will filter out URLs without extensions and treat them as navigation requests.

// All regular routes use the Universal engine
server.get('*', (req, res) => {
  res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
});

Serve static files securely

A single server.use() will handle all other URLs, such as requests for static resources such as JavaScript, images, and style sheets.
To ensure that clients can only download files that they are allowed to access, you should put all client-oriented resource files in the /dist directory, and only allow clients to request files from the /dist directory.

const distFolder = join(process.cwd(), 'dist/mystore/browser');
// Serve static files from /browser
server.get('*.*', express.static(distFolder, {
  maxAge: '1y'
}));

注销
1k 声望1.6k 粉丝

invalid