An overview of the

This survey of Node.js service framework will focus on the function of each framework, organization and intervention of request process, in order to provide reference for front-end Node.js service design and improvement of Zhaopin Ada architecture, but pay more attention to concrete implementation.

Finally, the following representative frameworks are selected:

  • Next. Js and Nuxt.js: They are front-end application development frameworks bound to specific front-end technologies React and Vue respectively. They have certain similarities and can be used for investigation and comparison.
  • Nest.js is an “Angular server-side implementation” based on decorators. You can replace the underlying kernel with any compatible HTTP provider, such as Express or Fastify. It can be used for HTTP, RPC and GraphQL services, and has certain reference value for providing more diversified service capabilities.
  • Fastify: A pure-play, low-level Web framework that organizes code using plug-in patterns and supports and improves performance based on schemas.

Next, js, Nuxt js

Both frameworks focus on the Web and provide complete support for the organization of UI rendering code, server-side rendering capabilities, and so on.

  • Next. Js: React Web application framework, research version 12.0.x
  • Nuxt.js: Vue Web application framework, research version 2.15.x.

function

First, the routing section:

  • Page routing:
    • The same is that both follow the file-as-route design. By default, the pages folder is used as the entry point to generate the corresponding routing structure. All files in the folder are regarded as routing entry files. Routing addresses are generated according to the hierarchy. In addition, if the file name index is omitted, the /pages/users and /pages/users/index files correspond to users.
    • The difference is that the generated route configuration and implementation varies depending on the dependent front-end framework:
      • Next.js: Since React has no official routing implementation, next.js does its own routing implementation.
      • Nuxt. Js: Based on vue-Router, vue-Router structure routing configuration is generated during compilation. At the same time, it also supports child routing, routing files in the folder of the same name will become child routes, such as article. Js, article/a.js, article/ B.js. A and B are child routes of article and can be used together <nuxt-child />Component for child routing rendering.
  • API routing:
    • Next.js: with support for this feature added after 9.x, files in the Pages/API/folder will function as apis and will not enter React front-end routes. Pages/API /article/[id].js -> / API /article/123 Its file export module is different from page route export, but that’s not the point.
    • Nuxt.js: Not officially supported, but there are other ways to implement it, such as using the framework’s serverMiddleware capability.
  • Dynamic routing: Both support dynamic routing, but the naming rules are different:
    • Next.js: name it with brackets, /pages/article/[id].js -> /pages/article/123.
    • Nuxt.js: use the underscore name, /pages/article/ id.js -> /pages/article/123.
  • Route loading: Both provide a link type component built in (LinkNuxtLink) when using this component instead<a></a>When the label redirects the route, the component will detect whether the link matches the route. If the link matches the route, the js and other resources of the corresponding route will be loaded after the component appears in the viewport. In addition, when the redirect is clicked, the route redirects the page, and there is no need to wait for obtaining the JS and other resource files required for rendering.
  • Error routing: Both routes provide the error code response to the bottom of the loop. As long as the Pages folder provides a page named HTTP error code route, when other routes have an error response, they will be redirected to the error code route page.

After generating the routing configuration from the file structure, let’s look at the differences in the way the code is organized:

  • React exports React components, Vue exports Vue components:
    • Next. Js: a generic React component:
      export default function About() {
          return <div>About us</div>
      }
      Copy the code
    • Nuxt.js: a generic Vue component:
      <template>
          <div>About us</div>
      </template>
      <script>
      export default {}
      <script>
      Copy the code
  • Routing component shells: In addition to each page routing component, there can be predefined shells to host the rendering of routing components. There are two shells in next.js and nuxt.js that can be customized:
    • 3. Container components that are common to page-routing components and that render page-routing components internally:
      • Next. Js: Rewriting _app.js in the pages root path is unique to the entire Next. Among them<Component />For the page routing component,pagePropsIs prefetched data, which will be discussed later
        import '.. /styles/global.css'
        export default function App({ Component, pageProps }) {
            return <Component {. pageProps} / >
        }
        Copy the code
      • Nuxt.js: Called Layout, you can create components in a layouts folder, such as layouts/blog.vue, and specify layouts in routing components<Nuxt />Routing components for pages:
        <template>
            <div>
                <div>My blog navigation bar here</div>
                <Nuxt />// Page routing component</div>
        </template>
        Copy the code
        // Page routing component
        <template>
        </template>
        <script>
        export default {
            layout: 'blog'.// Other Vue options
        }
        </script>
        Copy the code
    • Document: i.e. HTML template. Both HTML templates are unique and apply to the entire application:
      • Next.js: Overwrite the unique _document.js file in the pages root path, which will apply to all page routes, and render resources and attributes as components:
        import Document, { Html, Head, Main, NextScript } from 'next/document'
        class MyDocument extends Document {
            render() {
                return (
                    <Html>
                        <Head />
                        <body>
                            <Main />
                            <NextScript />
                        </body>
                    </Html>)}}export default MyDocument
        Copy the code
      • Nuxt.js: Overwrite the unique app.html file in the root directory. This will apply to all page routes and render resources and attributes using placeholders:
        <! DOCTYPEhtml>
        <html {{ HTML_ATTRS}} >
        <head {{ HEAD_ATTRS}} >
            {{ HEAD }}
        </head>
        <body {{ BODY_ATTRS}} >
            {{ APP }}
        </body>
        </html>
        Copy the code
  • The head section: In addition to writing the head content directly in the HTML template, how can different pages render a different head? We know that the head is outside the component, so how do both solve this problem?
    • Next. Js: We can use the built-in Head component in the page routing component to write the title, meta, etc., and render the Head part of the HTML when rendering:
      import Head from 'next/head'
      
      function IndexPage() {
          return (
              <div>
              <Head>
                  <title>My page title</title>
                  <meta property="og:title" content="My page title" key="title" />
              </Head>
              <Head>
                  <meta property="og:title" content="My new title" key="title" />
              </Head>
              <p>Hello world!</p>
              </div>)}export default IndexPage
      Copy the code
    • Nuxt.js: can also be configured in the page routing component, and also supports application level configuration. Common script and link resources can be written in the application configuration:
      • In the page routing component configuration: return the head configuration using the head function, where we can use this to get an instance:
        <template>
            <h1>{{ title }}</h1>
        </template>
        <script>
            export default {
                data() {
                    return {
                        title: 'Home page'}},head() {
                    return {
                        title: this.title,
                        meta: [{name: 'description'.content: 'Home page description'}]}}}</script>
        Copy the code
      • Nuxt.config. js for application configuration:
        export default {
            head: {
                title: 'my website title'.meta: [{charset: 'utf-8' },
                    { name: 'viewport'.content: 'width=device-width, initial-scale=1' },
                    { hid: 'description'.name: 'description'.content: 'my website description'}].link: [{ rel: 'icon'.type: 'image/x-icon'.href: '/favicon.ico'}}}]Copy the code

In addition to basic CSR (client-side rendering), SSR (server-side rendering) is also required. Let’s take a look at how both provide this capability, and what other rendering capabilities do they provide?

  • Server-side rendering: it is well known that server-side rendering requires data prefetch. What is the difference between the prefetch and server-side rendering?
    • Next. Js:
      • You can export the getServerSideProps method in the page routing file. Next. Js will render the page using the value returned by this function, which will be passed to the page routing component as props:
        export async function getServerSideProps(context) {
            // Send some requests
            return {
                props: {}}}Copy the code
      • The container components mentioned above also have their own methods, which are not covered.
      • At the end of the rendering process, page data and page build information are generated, which are written in<script id="__NEXT_DATA__"/>Render to the client and be read in the client.
    • Nuxt.js: There are two data prefetch methods, namely asyncData and FETCH:
      • AsyncData: The component can export asyncData methods, and the returned value is merged with the data of the page routing component for subsequent rendering, which is only available in the page routing component.
      • Fetch: Added in 2.12.x, serverPrefetch utilizes Vue SSR, is available in every component, and is called both server-side and client-side.
      • At the end of the rendering process, page data and page information are written in window.nuxt, which is also read by the client.
  • Static page generation SSG: Static HTML files will be generated during the construction phase, which is very helpful for improving access speed and CDN optimization:
    • Next.js: trigger automatic SSG under both conditions:
      1. Page routing file component without the getServerSideProps method;
      2. When exporting the getStaticProps method from the page routing file, you can define this method when you want to use data rendering:
      export async function getStaticProps(context) {
          const res = await fetch(`https://... /data`)
          const data = await res.json()
      
          if(! data) {return {
                  notFound: true,}}return {
              props: { data }
          }
      }
      Copy the code
    • Nuxt.js: provides the command generate command, which generates full HTML for the entire site.
  • In either rendering mode, page resources are pre-loaded in the header with rel=”preload” at client rendering time to pre-load resources and speed up rendering.

Both also provide the ability to intervene at other nodes of the process outside of page rendering:

  • Next. Js: You can create a _middleware.js file in each level of directory in the Pages folder and export middleware functions that take effect layer by layer for all routes and subordinate routes in each level of directory.
  • Nuxt.js: Middleware code is organized in two ways:
    1. Written in the Middleware folder, the file name becomes the middleware name and can then be declared for use in application-level configuration or Layout components, page routing components.
    2. Write middleware functions directly in the Layout component, page routing component.
    • Application level: Create middleware files of the same name in middleware, which will be executed before routing rendering and can then be configured in nuxt.config.js:
      / / middleware/status. Js file
      export default function ({ req, redirect }) {
          // If the user is not authenticated
          // if (! req.cookies.authenticated) {
          // return redirect('/login')
          // }
      }
      Copy the code
      // nuxt.config.js
      export default {
          router: {
              middleware: 'stats'}}Copy the code
    • Component level: Which middleware can be declared in a layout or page component:
      export default {
          middleware: ['auth'.'stats']}Copy the code

      You can also write a brand new Middleware:

      <script>
      export default {
          middleware({ store, redirect }) {
              // If the user is not authenticated
              if(! store.state.authenticated) {return redirect('/login')
              }
          }
      }
      </script>
      Copy the code

In terms of compilation and construction, both are webPack-based compilation processes, and the webPack configuration object is exposed in the configuration file in the way of function parameters, without any restrictions. Another point of note is that iN the V12.x. x release, next.js has changed the code compression and translation from Babel to SWC, which is a faster compilation tool developed using Rust, as well as other tools that are not implemented based on JavaScript for front-end builds. Such as ESbuild.

In terms of the ability to extend the framework, Next. Js directly provides rich service capabilities, while Nuxt.js designs modules and plug-in systems to extend.

Nest.js

Nest.js is an “Angular server-side implementation” based on decorators. Nest.js is designed in a completely different way from other front-end service frameworks or libraries. Let’s get a taste of how Nest.js is designed by looking at the usage of several nodes in the request life cycle.

Take a look at the full life cycle of Nest.js:

  1. Receipt of a request
  2. The middleware
    1. Globally bound middleware
    2. The middleware bound to the Module specified in the path
  3. The guards
    1. Global guard
    2. The Controller guarding
    3. The Route guard
  4. Interceptor (before Controller)
    1. global
    2. The Controller interceptor
    3. The Route interceptor
  5. The pipe
    1. Global pipeline
    2. The Controller pipeline
    3. The Route the pipe
    4. Route parameter pipeline
  6. Controller (Method handler)
  7. service
  8. Interceptor (after Controller)
    1. The Router interceptor
    2. The Controller interceptor
    3. Global interceptor
  9. Abnormal filter
    1. routing
    2. The controller
    3. global
  10. Server response

You can see the breakdown in terms of features, where the interceptors are placed before and after the Controller, similar to the Koa onion ring model.

Functional design

Let’s take a look at the routing section first, the central Controller:

  • Paths: Use decorators to decorate Controller classes like @Controller and @get to define routing resolution rules. Such as:
    import { Controller, Get, Post } from '@nestjs/common'
    
    @Controller('cats')
    export class CatsController {
        @Post()
        create(): string {
            return 'This action adds a new cat'
        }
    
        @Get('sub')
        findAll(): string {
            return 'This action returns all cats'}}Copy the code

    Defines handlers for /cats POST requests and /cats/sub GET requests.

  • Response: The status code, response, and so on can all be set through the decorator. Or you could just write it. Such as:
    @HttpCode(204)
    @Header('Cache-Control'.'none')
    create(response: Response) {
        / / or response. SetHeader (' cache-control ', 'none')
        return 'This action adds a new cat'
    }
    Copy the code
  • Parameter analysis:
    @Post(a)async create(@Body() createCatDto: CreateCatDto) {
        return 'This action adds a new cat'
    }
    Copy the code
  • Other capabilities for request processing are similar.

Let’s look at some of the other processing capabilities in the lifecycle:

  • Middleware: declarative registration method:
    @Module({})
    export class AppModule implements NestModule {
        configure(consumer: MiddlewareConsumer) {
            consumer
            // Apply CORS and LoggerMiddleware to cats route GET
            .apply(LoggerMiddleware)
            .forRoutes({ path: 'cats'.method: RequestMethod.GET })
        }
    }
    Copy the code
  • Exception filters (to catch and handle specific exceptions in a specific scope) that can be applied to a single route, the entire controller, or globally:
    // The program needs to throw specific type errors
    throw new HttpException('Forbidden', HttpStatus.FORBIDDEN)
    Copy the code
    / / define
    @Catch(HttpException)
    export class HttpExceptionFilter implements ExceptionFilter {
        catch(exception: HttpException, host: ArgumentsHost) {
            const ctx = host.switchToHttp()
            const response = ctx.getResponse<Response>()
            const request = ctx.getRequest<Request>()
            const status = exception.getStatus()
    
            response
                .status(status)
                .json({
                    statusCode: status,
                    timestamp: new Date().toISOString(),
                    path: request.url,
                })
        }
    }
    // in this case, the ForbiddenException error will be caught by HttpExceptionFilter and enter the HttpExceptionFilter process
    @Post(a)@UseFilters(new HttpExceptionFilter())
    async create() {
        throw new ForbiddenException()
    }
    Copy the code
  • Guard: Returns a Boolean that determines whether to continue with subsequent declaration cycles:
    // Declare it with the @Injectable decorator and implement CanActivate and return Boolean
    @Injectable(a)export class AuthGuard implements CanActivate {
        canActivate(context: ExecutionContext): boolean {
            returnvalidateRequest(context); }}Copy the code
    // Decorate the controller, handler, or global registry when used
    @UseGuards(new AuthGuard())
    async create() {
        return 'This action adds a new cat'
    }
    Copy the code
  • Pipes (more focused on handling parameters, which can be understood as part of controller logic, and more declarative) :
    1. Validation: Parameter type validation, performed at run time in applications developed using TypeScript.
    2. 2. The conversion of a parameter type or secondary parameter from the original parameter for use by controllers:
    @Get(':id')
    findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) {
        // Read the UserEntity through UserByIdPipe with id param
        return userEntity
    }
    Copy the code

Let’s take a quick look at how Nest.js works for different application types and different HTTP services:

  • Different application types: Nest.js supports Http, GraphQL, and Websocket applications, and for the most part, the lifecycle functions are the same in these types of applications, so Nest.js provides context classesArgumentsHost,ExecutionContext, such as usinghost.switchToRpc(),host.switchToHttp()To handle this discrepancy and ensure that the input parameters of the lifecycle function are consistent.
  • Different HTTP providers use different adapters. The default kernel of Nest.js is Express, but the FastifyAdapter adapter is officially provided for switching to Fastify.

Fastify

There’s a framework that does something different with data structures and types, and that’s Fastify. The feature of its official description is “fast”, and its implementation of speed enhancement is the focus of our attention.

Let’s start with a development example:

const routes = require('./routes')
const fastify = require('fastify') ({logger: true
})

fastify.register(tokens)

fastify.register(routes)

fastify.listen(3000.function (err, address) {
  if (err) {
    fastify.log.error(err)
    process.exit(1)
  }
  fastify.log.info(`server listening on ${address}`)})Copy the code
class Tokens {
  constructor () {}
  get (name) {
    return '123'}}function tokens (fastify) {
  fastify.decorate('tokens'.new Tokens())
}

module.exports = tokens
Copy the code
// routes.js
class Tokens {
  constructor(){}get(name) {
    return '123'}}const options = {
  schema: {
    querystring: {
      name: { type: 'string'}},response: {
      200: {
        type: 'object'.properties: {
          name: { type: 'string' },
          token: { type: 'string' }
        }
      }
    }
  }
}

function routes(fastify, opts, done) {
  fastify.decorate('tokens'.new Tokens())

  fastify.get('/', options, async (request, reply) => {
    reply.send({
      name: request.query.name,
      token: fastify.tokens.get(request.query.name)
    })
  })
  done()
}
module.exports = routes
Copy the code

Two points to note are:

  1. When routing is defined, a request’s schema is passed in, and the official documentation says that defining the response’s schema can increase Fastify’s throughput by 10-20%.
  2. Fastify enhances the capabilities of Fastify by using The Decorate method, or encapsulating it with a register that creates a whole new context by extracting the decorate part into another file.

What’s missing is that the way Fastify requests are supported for intervention is by using lifecycle hooks, which we won’t cover since this is a common practice for the front end (Vue, React, Webpack).

Let’s focus again on Fastify’s acceleration principle.

How to speed up

There are three key packages, in order of importance:

  1. fast-json-stringify
  2. find-my-way
  3. reusify
  • Fast – json – stringify:
    const fastJson = require('fast-json-stringify')
    const stringify = fastJson({
      title: 'Example Schema'.type: 'object'.properties: {
        firstName: {
          type: 'string'
        },
        lastName: {
          type: 'string'}}})const result = stringify({
      firstName: 'Matteo'.lastName: 'Collina',})Copy the code
    • Same functionality as json.stringify, faster under low load.
    • Its principle is to generate a string assembly function that takes field values in advance according to the field type definition at the execution stage, such as:
      function stringify (obj) {
        return `{"firstName":"${obj.firstName}","lastName":"${obj.lastName}"} `
      }
      Copy the code

      Equivalent to omitting the judgment of the type of the field value, omitting some traversal and type judgment that must be carried out every time the execution, of course, the real function content is much more complex than this. By extension, we can copy this optimization logic as long as we know the structure and type of data.

  • Find-my-way: generates a compressed prefix tree structure of registered routes, which according to benchmark data is the fastest and most fully featured routing library.
  • Reusify: Used in the official Fastify middleware mechanism dependencies library, reusable objects and functions to avoid creation and reclamation overhead, this library has some REQUIREMENTS for users based on v8 engine optimization. In Fastify, it is primarily used for reuse of context objects.

conclusion

  • In the design of routing structure, Next. Js, nuxt. js adopt file structure that is routing design. Ada is also a way of using file structure reductives.
  • In terms of rendering, both Next. Js and Nuxt.js do not directly reflect the rendering of structures outside the root component in the routing process, hiding the implementation details, but the root component can determine the rendering of structures outside the component in a more configuration way (head content). At the same time, the request for rendering data is not separated from another file because it is closely related to the routing component. Whether the routing file of Next. Js exports various data acquisition functions at the same time or nuxt.js directly adds configuration or functions other than Vue options on the component, it can be regarded as an enhancement of the component. The route folder does not export components directly. Instead, you need to export different processing functions and modules based on the operating environment. For example, you need to export the GET and POST functions with the same name in the index.server.js file corresponding to the server. Developers can do some data prefetching operations, page template rendering, etc. The index.js file corresponding to the client needs to export the component mount code.
  • In terms of rendering performance improvement, both Next. Js and Nuxt.js adopt the same strategy: static generation, pre-loading of resource files matching routes, preload, etc. Please refer to optimization.
  • On request intervention (middleware) :
    • Next. Js and Nuxt.js do not differentiate middleware functionality, adopting Express or Koa usagenext()The way functions control flow, while Nest.js divides more directly into several standardized implementations based on functional characteristics.
    • Leaving aside the use of application-level overall configuration, nuxt.js uses routing to define which middleware is required, and Nest.js is more like Nuxt.js using decorators configured on routing handlers and controllers in a routing manner. The middleware of Next. Js will affect the peer and subordinate routes, and the middleware will decide the scope of influence, which are two completely opposite control ideas.
    • The Ada architecture is based on the Koa kernel, but the internal middleware implementation is similar to Nex.js, which abstracts the execution process into several life cycles and makes the middleware into task functions with different function types in different life cycles. For developers, the function of custom life cycle is not exposed, but based on the level of code reuse, it also provides server-side extension, Web module extension and other capabilities. Because Ada can independently online the files collectively referred to as artifacts, such as page routing, API routing, server-side extension and Web module, For the sake of stability and clear scope of influence, it is also the way the route is actively invoked that determines which extension capabilities you need to enable.
  • Nex.js officially provides the ability to document based on decorators, and it is common practice to generate interface documentation using type declarations such as parsing TypeScript syntax and GraphQL structure definitions. However, while Nex.js has good TypeScript support and does not directly address runtime type validation issues, it can be done through pipelines and middleware.
  • Fastify works at the bottom to improve performance, and it does it to the hilt. And the more low-level the implementation is, the more scenarios it can be used in. The optimization methods of routing matching and context reuse can be further investigated.
  • In addition, SWC, ESBuild and other tools to improve development experience and online speed are also a direction to be investigated.

If you are interested in Ada architecture design, you can read the previous published articles: “Decoding the Big Front-end Architecture Ada of Zhaopin”, “Implementation Practice of Zhaopin’s Micro Front-end — Widget”, “Reconstruction Experience of Koa Middleware System”, “Implementation Scheme of Zhaopin’s Web Module Expansion”, etc.