From 400769989ba1604d922336663e47b2bf35a81113 Mon Sep 17 00:00:00 2001 From: Matt Weber <1062734+mweberxyz@users.noreply.github.com> Date: Fri, 22 Mar 2024 11:39:30 -0400 Subject: [PATCH 1/3] feat: viewAsync reply decorator Closes fastify/point-of-view#394 Closes fastify/point-of-view#412 --- README.md | 267 ++++++++++++++++++++++++--------- benchmark/fastify-viewAsync.js | 6 + index.js | 49 +++--- test/test-ejs-async.js | 68 +++++++++ test/test-ejs.js | 267 +++++++++++++++++++++++++++++++++ test/test-handlebars.js | 38 +++++ types/index.d.ts | 3 + 7 files changed, 610 insertions(+), 88 deletions(-) create mode 100644 benchmark/fastify-viewAsync.js diff --git a/README.md b/README.md index fb01e97..30f73b0 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Templates rendering plugin support for Fastify. -`@fastify/view` decorates the reply interface with the `view` method for managing view engines, which can be used to render templates responses. +`@fastify/view` decorates the reply interface with the `view` and `viewAsync` methods for managing view engines, which can be used to render templates responses. Currently supports the following templates engines: @@ -27,6 +27,10 @@ _Note: For **Fastify v3 support**, please use point-of-view `5.x` (npm i point-o _Note that at least Fastify `v2.0.0` is needed._ +## Recent Changes + +_Note: `reply.viewAsync` added as a replacement for `reply.view` and `fastify.view`. See [Migrating from view to viewAsync](#migrating-from-view-to-viewAsync)._ + _Note: [`ejs-mate`](https://github.com/JacksonTian/ejs-mate) support [has been dropped](https://github.com/fastify/point-of-view/pull/157)._ _Note: [`marko`](https://markojs.com/) support has been dropped. Please use [`@marko/fastify`](https://github.com/marko-js/fastify) instead._ @@ -54,56 +58,66 @@ npm i @fastify/view - the template to be rendered - the data that should be available to the template during rendering -This example will render the template and provide a variable `text` to be used inside the template: +This example will render the template using the EJS engine and provide a variable `name` to be used inside the template: + +```html + + + + + +

Hello, <%= name %>!

+ + +``` ```js -const fastify = require("fastify")(); +// index.js: +const fastify = require("fastify")() +const fastifyView = require("@fastify/view") -fastify.register(require("@fastify/view"), { +fastify.register(fastifyView, { engine: { - ejs: require("ejs"), - }, -}); + ejs: require("ejs") + } +}) +// synchronous handler: fastify.get("/", (req, reply) => { - reply.view("/templates/index.ejs", { text: "text" }); -}); + reply.view("index.ejs", { name: "User" }); +}) + +// asynchronous handler: +fastify.get("/", async (req, reply) => { + return reply.viewAsync("index.ejs", { name: "User" }); +}) fastify.listen({ port: 3000 }, (err) => { if (err) throw err; console.log(`server listening on ${fastify.server.address().port}`); -}); -``` - -If your handler function is asynchronous, make sure to return the result - otherwise this will result in an `FST_ERR_PROMISE_NOT_FULFILLED` error: - -```js -// This is an async function -fastify.get("/", async (req, reply) => { - // We are awaiting a function result - const t = await something(); - - // Note the return statement - return reply.view("/templates/index.ejs", { text: "text" }); -}); +}) ``` ## Configuration -`fastify.register(, )` accepts an options object. - ### Options -- `engine`: The template engine object - pass in the return value of `require('')`. This option is mandatory. -- `layout`: @fastify/view supports layouts for **EJS**, **Handlebars**, **Eta** and **doT**. This option lets you specify a global layout file to be used when rendering your templates. Settings like `root` or `viewExt` apply as for any other template file. Example: `./templates/layouts/main.hbs` -- `propertyName`: The property that should be used to decorate `reply` and `fastify` - E.g. `reply.view()` and `fastify.view()` where `"view"` is the property name. Default: `"view"`. -- `root`: The root path of your templates folder. The template name or path passed to the render function will be resolved relative to this path. Default: `"./"`. -- `includeViewExtension`: Setting this to `true` will automatically append the default extension for the used template engine **if omitted from the template name** . So instead of `template.hbs`, just `template` can be used. Default: `false`. -- `viewExt`: Let's you override the default extension for a given template engine. This has precedence over `includeViewExtension` and will lead to the same behavior, just with a custom extension. Default `""`. Example: `"handlebars"`. -- `defaultContext`: The template variables defined here will be available to all views. Variables provided on render have precedence and will **override** this if they have the same name. Default: `{}`. Example: `{ siteName: "MyAwesomeSite" }`. -- `maxCache`: In `production` mode, maximum number of templates file and functions caches. Default: `100`. Example: `{ maxCache: 100 }`. +| Option | Description | Default | +| ---------------------- | ----------- | ------- | +| `engine` | **Required**. The template engine object - pass in the return value of `require('')` | | +| `production` | Enables caching of template files and render functions | `NODE_ENV === "production"` | +| `maxCache` | In `production` mode, maximum number of cached template files and render functions | `100` | +| `defaultContext` | Template variables available to all views. Variables provided on render have precedence and will **override** this if they have the same name.

Example: `{ siteName: "MyAwesomeSite" }` | `{}` | +| `propertyName` | The property that should be used to decorate `reply` and `fastify`

E.g. `reply.view()` and `fastify.view()` where `"view"` is the property name | `"view"` | +| `asyncPropertyName` | The property that should be used to decorate `reply` for async handler

Defaults to `${propertyName}Async` if `propertyName` is defined | `"viewAsync"` | +| `root` | The root path of your templates folder. The template name or path passed to the render function will be resolved relative to this path | `"./"` | +| `charset` | Default charset used when setting `Content-Type` header | `"utf-8"` | +| `includeViewExtension` | Automatically append the default extension for the used template engine **if omitted from the template name** . So instead of `template.hbs`, just `template` can be used | `false` | +| `viewExt` | Override the default extension for a given template engine. This has precedence over `includeViewExtension` and will lead to the same behavior, just with a custom extension.

Example: `"handlebars"` | `""` | +| `layout` | See [Layouts](#layouts)

This option lets you specify a global layout file to be used when rendering your templates. Settings like `root` or `viewExt` apply as for any other template file.

Example: `./templates/layouts/main.hbs` | | +| `options` | See [Engine-specific settings](#engine-specific-settings) | `{}` | -Example: +### Example ```js fastify.register(require("@fastify/view"), { @@ -121,9 +135,78 @@ fastify.register(require("@fastify/view"), { }); ``` -## Rendering the template into a variable +## Layouts + +@fastify/view supports layouts for **EJS**, **Handlebars**, **Eta** and **doT**. When a layout is specified, the request template is first rendered, then the layout template is rendered with the request-rendered html set on `body`. + +### Example + +```html + + + + + + + <%- body %> +
+ + +``` + +```html + +

<%= text %>

+``` -The `fastify` object is decorated the same way as `reply` and allows you to just render a view into a variable instead of sending the result back to the browser: +```js +// index.js: +fastify.register(fastifyView, { + engine: { ejs }, + layout: "layout.ejs" +}) + +fastify.get('/', (req, reply) => { + const data = { text: "Hello!"} + reply.view('template.ejs', data) +}) +``` + +### Providing a layout on render +**Please note:** Global layouts and providing layouts on render are mutually exclusive. They can not be mixed. + +```js +fastify.get('/', (req, reply) => { + const data = { text: "Hello!"} + reply.view('template.ejs', data, { layout: 'layout.ejs' }) +}) +``` + +## Setting request-global variables +Sometimes, several templates should have access to the same request-specific variables. E.g. when setting the current username. + +If you want to provide data, which will be depended on by a request and available in all views, you have to add property `locals` to `reply` object, like in the example below: + +```js +fastify.addHook("preHandler", function (request, reply, done) { + reply.locals = { + text: getTextFromRequest(request), // it will be available in all views + }; + + done(); +}); +``` + +Properties from `reply.locals` will override those from `defaultContext`, but not from `data` parameter provided to `reply.view(template, data)` function. + +## Rendering the template into a variable +The `fastify` object is decorated the same way as `reply` and allows you to just render a view into a variable (without request-global variables) instead of sending the result back to the browser: ```js // Promise based, using async/await @@ -136,6 +219,9 @@ fastify.view("/templates/index.ejs", { text: "text" }, (err, html) => { }); ``` +If called within a request hook and you need request-global variables, see [Migrating from view to viewAsync](#migrating-from-view-to-viewAsync). + + ## Registering multiple engines Registering multiple engines with different configurations is supported. They are distinguished via their `propertyName`: @@ -164,36 +250,6 @@ fastify.get("/desktop", (req, reply) => { }); ``` -## Providing a layout on render - -@fastify/view supports layouts for **EJS**, **Handlebars**, **Eta** and **doT**. -These engines also support providing a layout on render. - -**Please note:** Global layouts and providing layouts on render are mutually exclusive. They can not be mixed. - -```js -fastify.get('/', (req, reply) => { - reply.view('index-for-layout.ejs', data, { layout: 'layout.html' }) -}) -``` - -## Setting request-global variables -Sometimes, several templates should have access to the same request-specific variables. E.g. when setting the current username. - -If you want to provide data, which will be depended on by a request and available in all views, you have to add property `locals` to `reply` object, like in the example below: - -```js -fastify.addHook("preHandler", function (request, reply, done) { - reply.locals = { - text: getTextFromRequest(request), // it will be available in all views - }; - - done(); -}); -``` - -Properties from `reply.locals` will override those from `defaultContext`, but not from `data` parameter provided to `reply.view(template, data)` function. - ## Minifying HTML on render To utilize [`html-minifier-terser`](https://www.npmjs.com/package/html-minifier-terser) in the rendering process, you can add the option `useHtmlMinifier` with a reference to `html-minifier-terser`, @@ -729,7 +785,6 @@ fastify.get('/', (req, reply) => { }) }) ``` -