Title: View Plugin Development

In most cases, we need to read the data, render the template and then present it to the user. The framework does not force the use of a template engine, allowing developers to select the template themselves. For details, see Template Rendering.

This article describes the framework’s specification constraints on the View plugin, and we can use this to encapsulate the corresponding template engine plugin. The following takes egg-view-ejs as an example

Plugin directory structure

  1. egg-view-ejs
  2. ├── config
  3. ├── config.default.js
  4. └── config.local.js
  5. ├── lib
  6. └── view.js
  7. ├── app.js
  8. ├── test
  9. ├── History.md
  10. ├── README.md
  11. └── package.json

Plugin naming convention

  • Follow the plugin development specification
  • According to the convention, the names of plugins start with egg-view-
  • package.json is configured as follows. Plugins are named after the template engine, such as ejs
  1. {
  2. "name": "egg-view-ejs",
  3. "eggPlugin": {
  4. "name": "ejs"
  5. },
  6. "keywords": ["egg", "egg-plugin", "egg-view", "ejs"]
  7. }
  • The configuration item is also named after the template engine
  1. // config/config.default.js
  2. module.exports = {
  3. ejs: {}
  4. };

View base class

The next step is to provide a View base class that will be instantiated on each request.

The base class of the View needs to provide render and renderString methods and supports generator and async functions (it can also be a function that returns a Promise). The render method is used to render files, and the renderString method is used to render template strings.

The following is a simplified code that can be directly view source

  1. const ejs = require('ejs');
  2. Mmdule.exports = class EjsView {
  3. render(filename, locals) {
  4. return new Promise((resolve, reject) => {
  5. // Asynchronous API call
  6. ejs.renderFile(filename, locals, (err, result) => {
  7. if (err) {
  8. reject(err);
  9. } else {
  10. resolve(result);
  11. }
  12. });
  13. });
  14. }
  15. renderString(tpl, locals) {
  16. try {
  17. // Synchronous API call
  18. return Promise.resolve(ejs.render(tpl, locals));
  19. } catch (err) {
  20. return Promise.reject(err);
  21. }
  22. }
  23. };

Parameters

The three parameters of the render method are

  • filename: is the path to the complete file. The framework determines if the file exists when it looks for the file. It does not need to be processed here.
  • locals: The data needed for rendering. The data comes from app.locals, ctx.locals and calls render methods. The framework also has built in ctx, request, ctx.helper objects.
  • viewOptions: The incoming configuration of the user, which can override the default configuration of the template engine. This can be considered based on the characteristics of the template engine. For example, the cache is enabled by default but a page does not need to be cached.

The three parameters of the renderString method

  • tpl: template string, not file path
  • locals: same with render
  • viewOptions: same with render

Plugin configuration

According to the naming conventions mentioned above, the configuration name is generally the name of the template engine, such as ejs

The configuration of the plugin mainly comes from the configuration of the template engine, and the configuration items can be defined according to the specific conditions, such as the configuration of ejs

  1. // config/config.default.js
  2. module.exports = {
  3. ejs: {
  4. cache: true
  5. }
  6. };

Helper

The framework provides ctx.helper for developer use, but in some cases we want to override the helper method and only take effect when the template is rendered.

In template rendering, we often need to output a user-supplied html fragment, in which case, we often use the helper.shtml provided by the egg-security plugin.

  1. <div>{{ helper.shtml(data.content) | safe }}</div>

However, as shown in the above code, we need to use | safe to tell the template engine that the html is safe and it doesn’t need to run escape again.

This is more cumbersome to use and easy to forget, so we can package it:

First provide a helper subclass:

  1. // {plugin_root}/lib/helper.js
  2. module.exports = app => {
  3. return class ViewHelper extends app.Helper {
  4. // safe is injected by [egg-view-nunjucks] and will not be escaped during rendering.
  5. // Otherwise, the template call shtml will be escaped
  6. shtml(str) {
  7. return this.safe(super.shtml(str));
  8. }
  9. };
  10. };

Use a custom helper when rendering

  1. // {plugin_root}/lib/view.js
  2. const ViewHelper = require('./helper');
  3. module.exports = class MyCustomView {
  4. render(filename, locals) {
  5. locals.helper = new ViewHelper(this.ctx); // call Nunjucks render
  6. }
  7. };

You can view the specific code here

Templates and security are related and egg-security also provides some methods for the template. The template engine can be used according to requirements.

First declare a dependency on egg-security

  1. {
  2. "name": "egg-view-nunjucks",
  3. "eggPlugin": {
  4. "name": "nunjucks",
  5. "dep": ["security"]
  6. }
  7. }

The framework provides app.injectCsrf and app.injectNonce, for more information on security section.

Unit tests

As a high-quality plugin, perfect unit testing is indispensable, and we also provide a lot of auxiliary tools to make it painless for plugin developers to write tests, see unit testing and plugin docs.