Front-end service framework survey: Next.js, Nuxt.js, Nest.js, Fastify

智联大前端
中文

Overview

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

Finally, the following representative frameworks were selected:

  • Next.js and Nuxt.js: They are front-end application development frameworks that are bound to specific front-end technologies React and Vue respectively. They have certain similarities and can be put together for research and comparison.
  • Nest.js: is "Angular's server-side implementation", based on decorators. You can use any compatible http provider, such as Express, Fastify, to replace the underlying kernel. It can be used for http, rpc, graphql services, and has a certain reference value for providing more diverse service capabilities.
  • Fastify: A relatively pure low-level web framework that uses plug-in mode to organize code and supports and improves operating efficiency based on schema.

Next.js、Nuxt.js

The focus of these two frameworks is on the Web part, which provides perfect support for the organization of the code of the UI rendering part and the server-side rendering function.

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

Function

The first is the routing part:

  • Page routing:

    • The same is that both follow the file-as-routing design. By default, the pages folder is used as the entry point to generate the corresponding routing structure. All files in the folder will be regarded as routing entry files, supporting multiple levels, and routing addresses will be generated according to the levels. At the same time, if the file name is index, it will be omitted, that is, the access addresses corresponding to the /pages/users and /pages/users/index files are users.
    • The difference is that the generated routing configuration and implementation are different according to the front-end framework that it depends on:

      • Next.js: Since React does not have an official routing implementation, Next.js has made its own routing implementation.
      • Nuxt.js: Based on vue-router, the routing configuration of the vue-router structure will be generated at compile time, and it also supports sub-routes. The files in the folder with the same name of the route file will become sub-routes, such as article.js, article/ a.js, article/b.js, a and b are the sub-routes of article, which can be used with the <nuxt-child /> component for sub-route rendering.
  • api routing:

    • Next.js: After the 9.x version, the support for this function is added. Files under the pages/api/ folder (why there is a historical burden on the design under the pages folder) will take effect as an api and will not enter React front-end routing. The naming rules are the same, pages/api/article/[id].js -> /api/article/123. The file export module is different from the page routing export, but it is not the point.
    • Nuxt.js: No official support is provided, but there are other ways to implement it, such as using the serverMiddleware capability of the framework.
  • Dynamic routing: Both support dynamic routing access, but the naming rules are different:

    • Next.js: Use square brackets to name, /pages/article/[id].js -> /pages/article/123.
    • Nuxt.js: Use an underscore to name, /pages/article/_id.js -> /pages/article/123.
  • Route loading: Both of them provide built-in link type components ( Link and NuxtLink ). When this component is used to replace the <a></a> label for route jump, the component will detect whether the link hits the route. If it hits, the component will appear after the viewport. Trigger the loading of the js and other resources of the corresponding route, and use the route jump when the jump is clicked, the page will not be reloaded, and there is no need to wait for the resource files such as js for rendering.
  • Error pocket bottom: Both provide the bottom jump of the error code response. As long as the page route named by the http error code is provided under the pages folder, when other routes respond incorrectly, it will jump to the error code routing page.

After generating the routing configuration according to the file structure, let's look at the difference in code organization:

  • Routing component: There is no difference between the two. Both use the default export component to determine the route rendering content. React exports the React component, and Vue exports the Vue component:

    • Next.js: An ordinary React component:

      export default function About() {
          return <div>About us</div>
      }
    • Nuxt.js: An ordinary Vue component:

      <template>
          <div>About us</div>
      </template>
      <script>
      export default {}
      <script>
  • Routing component shell: In addition to each page routing component, there can also be some predefined shells to carry the rendering of the routing component. In Next.js and Nuxt.js, there are two shells that can be customized:

    • Container: Some container components that can be shared by page routing components, and page routing components are rendered internally:

      • Next.js: Need to rewrite _app.js under the root path of pages, which will take effect for the entire Next.js application and is unique. Among them, <Component /> is the page routing component, and pageProps is the prefetched data, which will be mentioned later

        import '../styles/global.css'
        export default function App({ Component, pageProps }) {
            return <Component {...pageProps} />
        }
      • Nuxt.js: called Layout, you can create components under the layouts folder, such as layouts/blog.vue, and specify the layout in the routing component, that is, there can be multiple sets of containers in <Nuxt /> , of which 06196f016f357d is the page Routing components:

        <template>
            <div>
                <div>My blog navigation bar here</div>
                <Nuxt /> // 页面路由组件
            </div>
        </template>
        // 页面路由组件
        <template>
        </template>
        <script>
        export default {
            layout: 'blog',
            // 其他 Vue options
        }
        </script>
    • Document: html template. Both html templates are unique and will take effect for the entire application:

      • Next.js: Rewrite the only _document.js under the root path of pages, which will take effect on all page routing, and use components to render resources and attributes:

        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
      • Nuxt.js: Rewriting the only App.html in the root directory will take effect on all page routing. Use placeholders to render resources and attributes:

        <!DOCTYPE html>
        <html {{ HTML_ATTRS }}>
        <head {{ HEAD_ATTRS }}>
            {{ HEAD }}
        </head>
        <body {{ BODY_ATTRS }}>
            {{ APP }}
        </body>
        </html>
  • The head part: In addition to the way of directly writing the head content in the html template, how to make different pages render different heads. We know that the head is outside the component, so how do both of them solve this problem?

    • Next.js: You can use the built-in Head component in the page routing component, write title, meta, etc. inside, which will be rendered in 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
    • Nuxt.js: It can also be configured in the page routing component, and it also supports application-level configuration. Common script and link resources can be written in the application configuration:

      • In the page routing component configuration: use the head function to return the head configuration, in the function you 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>
      • 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' }]
            }
        }

In addition to the basic CSR (client-side rendering), SSR (server-side rendering) is also necessary. Let's see how both provide this capability, and what rendering capabilities are provided besides this?

  • Server-side rendering: It is well known that server-side rendering requires data prefetching. What is the difference between the prefetching usage of the two?

    • Next.js:

      • You can export the getServerSideProps method in the page routing file. Next.js will use the value returned by this function to render the page, and the return value will be passed to the page routing component as props:

        export async function getServerSideProps(context) {
            // 发送一些请求
            return {
                props: {}
            }
        }
      • The container component mentioned above also has its own method, which will not be introduced.
      • At the end of the rendering process, page data and page construction information will be generated. These content will be written in <script id="__NEXT_DATA__"/> , rendered to the client, and read by the client.
    • Nuxt.js: There are two data prefetching methods, namely asyncData and fetch:

      • asyncData: The component can export the asyncData method, and the return value will be 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, using Vue SSR's serverPrefetch, which is available in every component and will be called at the same time on the server side and the client side.
      • At the end of the rendering process, the page data and page information are written in window.__NUXT__, which will also be read on the client.
  • Static page generation SSG: During the construction phase, static HTML files will be generated, which is very helpful for improving access speed and optimizing CDN:

    • Next.js: Automatic SSG will be triggered under two conditions:

      1. When the page routing file component does not have a getServerSideProps method;
      2. When exporting the getStaticProps method in the page routing file, you can define this method when you need 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 }
         }
        }
    • Nuxt.js: Provides the command generate command, which will generate complete html for the entire site.
  • Regardless of the rendering method, when the client is rendered, the page resources will be loaded in advance in the head with rel="preload" to load the resources in advance and improve the rendering speed.

In other nodes of the process other than page rendering, both also provide the ability to intervene:

  • Next.js: You can create a _middleware.js file in all levels of directories under the pages folder, and export middleware functions. This function will take effect on all routes and subordinate routes in the same directory.
  • Nuxt.js: There are two ways to organize middleware code:

    1. Written in the middleware folder, the file name will become the name of the middleware, which can then be configured at the application level or declared and used in the Layout component and the page routing component.
    2. Write middleware functions directly in Layout component and page routing component.
    3. Application level: Create middleware files with the same name in middleware, these middleware will be executed before routing rendering, and then can be configured in nuxt.config.js:

      // middleware/status.js 文件
      export default function ({ req, redirect }) {
          // If the user is not authenticated
          // if (!req.cookies.authenticated) {
          //    return redirect('/login')
          // }
      }
      // nuxt.config.js
      export default {
          router: {
              middleware: 'stats'
          }
      }
    4. Component level: You can declare the use of those middleware in layout or page components:

      export default {
          middleware: ['auth', 'stats']
      }

      You can also directly write a new middleware:

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

    In terms of compilation and construction, both are based on the compilation process built by webpack, and the webpack configuration objects are exposed in the configuration file through function parameters, and there are no restrictions. Another point worth noting is that Next.js in the v12.xx version of the code compression code and the original babel translation into swc , this is a faster compiler tool developed using Rust, in terms of front-end construction, but also There are other tools that are not implemented based on JavaScript, such as ESbuild.

In terms of extension framework capabilities, Next.js directly provides richer service capabilities, while Nuxt.js designs modules and plug-in systems for extension.

Nest.js

Nest.js is "Angular's server-side implementation", based on decorators. The design idea of Nest.js is completely different from other front-end service frameworks or libraries. We experience the design of Nest.js by viewing the usage of several nodes in the request life cycle.

Let's take a look at the complete life cycle of Nest.js:

  1. Received request
  2. Middleware

    1. Globally bound middleware
    2. Middleware bound to the Module specified in the path
  3. guard

    1. Global guard
    2. Controller guard
    3. Route guard
  4. Interceptor (before Controller)

    1. Global
    2. Controller interceptor
    3. Route interceptor
  5. pipeline

    1. Global pipeline
    2. Controller pipeline
    3. Route pipeline
    4. Route parameter pipeline
  6. Controller (Method Processor)
  7. service
  8. Interceptor (after Controller)

    1. Router interceptor
    2. Controller interceptor
    3. Global interceptor
  9. Exception filter

    1. routing
    2. Controller
    3. Global
  10. Server response

It can be seen that the split is more detailed according to the functional characteristics, where the interceptor is located before and after the Controller, which is similar to the Koa onion ring model.

feature design

First look at the routing part, which is the most central Controller:

  • Path: Use decorators to decorate Controller classes such as @Controller and @GET to define routing resolution rules. like:

    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'
        }
    }

    The processing functions for /cats post request and /cats/sub get request are defined.

  • Response: The status code, response header, etc. can all be set through the decorator. Of course, you can also write directly. like:

    @HttpCode(204)
    @Header('Cache-Control', 'none')
    create(response: Response) {
        // 或 response.setHeader('Cache-Control', 'none')
        return 'This action adds a new cat'
    }
  • Parameter analysis:

    @Post()
    async create(@Body() createCatDto: CreateCatDto) {
        return 'This action adds a new cat'
    }
  • Other capabilities of request processing are similar.

Let's take a look at several other processing capabilities in the life cycle:

  • Middleware: declarative registration method:

    @Module({})
    export class AppModule implements NestModule {
        configure(consumer: MiddlewareConsumer) {
            consumer
            // 应用 cors、LoggerMiddleware 于 cats 路由 GET 方法
            .apply(LoggerMiddleware)
            .forRoutes({ path: 'cats', method: RequestMethod.GET })
        }
    }
  • Exception filters (catch specific exceptions in a specific scope and handle them), which can act on a single route, the entire controller or the global:

    // 程序需要抛出特定的类型错误
    throw new HttpException('Forbidden', HttpStatus.FORBIDDEN)
    // 定义
    @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,
                })
        }
    }
    // 使用,此时 ForbiddenException 错误就会被 HttpExceptionFilter 捕获进入 HttpExceptionFilter 处理流程
    @Post()
    @UseFilters(new HttpExceptionFilter())
    async create() {
        throw new ForbiddenException()
    }
  • Guard: return a boolean value, and determine whether to continue the subsequent declaration cycle according to the return value:

    // 声明时需要使用 @Injectable 装饰且实现 CanActivate 并返回 boolean 值
    @Injectable()
    export class AuthGuard implements CanActivate {
        canActivate(context: ExecutionContext): boolean {
            return validateRequest(context);
        }
    }
    // 使用时装饰 controller、handler 或全局注册
    @UseGuards(new AuthGuard())
    async create() {
        return 'This action adds a new cat'
    }
  • Pipeline (more emphasis on the processing of parameters, can be understood as part of the controller logic, more declarative):

    1. Verification: Parameter type verification, which is used to verify the parameter type at runtime in a program developed using TypeScript.
    2. Conversion: Conversion of parameter types, or obtaining secondary parameters from original parameters for use by controllers:

      @Get(':id')
      findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) {
       // 使用 id param 通过 UserByIdPipe 读取到 UserEntity
       return userEntity
      }

Let's briefly look at how Nest.js adapts to different application types and different http services:

  • Different application types: Nest.js supports Http, GraphQL, Websocket applications. In most cases, the life cycle functions in these types of applications are the same, so Nest.js provides context classes ArgumentsHost , ExecutionContext , such as host.switchToRpc() host.switchToHttp() to deal with this difference, to ensure that the input parameters of the life cycle function are consistent.
  • Different http services use different adapters. The default core of Nest.js is Express, but the official FastifyAdapter adapter is provided to switch to Fastify.

Fastify

There is a framework that does different things depending on data structure and type, and that is Fastify. The characteristic of its official description is "fast", and the realization of its speed improvement is the focus of our attention.

Let's take a look at the development example first:

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}`)
})
class Tokens {
  constructor () {}
  get (name) {
    return '123'
  }
}

function tokens (fastify) {
  fastify.decorate('tokens', new Tokens())
}

module.exports = tokens
// 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

Two points that can be noticed are:

  1. In the routing definition, a request schema is passed in. The official document also says that the response schema definition can increase Fastify's throughput by 10%-20%.
  2. Fastify uses decorate to enhance Fastify's capabilities. You can also extract the decorate part to other files and use register to create a new context for packaging.

What is not reflected is that the support method for Fastify to request intervention is to use life cycle hooks. Since this is a common practice for the front-end (Vue, React, Webpack), it will not be introduced.

Let's focus on the speed-up principle of Fastify again.

How to speed up

There are three more critical packages, ranked according to 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',
    })
    • Same function as JSON.stringify, faster when the load is small.
    • The principle is that in the execution phase, a function that generates a string assembly that takes the field value is generated in advance according to the field type definition, such as:

      function stringify (obj) {
        return `{"firstName":"${obj.firstName}","lastName":"${obj.lastName}"}`
      }

      It is equivalent to omitting the judgment of the type of the field value, and omitting some traversal and type judgment every time it is executed. Of course, the real function content is much more complicated than this. So by extension, as long as we can know the structure and type of the data, we can copy this set of optimization logic.

  • find-my-way: Generate a compressed prefix tree structure for the registered routes. According to the benchmark test , the data shows that it is the fastest routing library with the most complete functions.
  • Reusify: This library is used in the middleware mechanism dependency library officially provided by Fastify, which can reuse objects and functions to avoid creation and recovery overhead. This library has some requirements for users based on the optimization of the v8 engine. It is mainly used for the reuse of context objects in Fastify.

Summarize

  • In the design of routing structure, both Next.js and Nuxt.js adopt the design method of file structure, namely routing. Ada is also a conventional way of using file structure.
  • In terms of rendering, neither Next.js nor Nuxt.js directly reflects the rendering of structures other than the root component in the routing process, hiding the implementation details, but it can be determined by the root component in a more configurable way. Rendering of external structures (head content). At the same time, the request for rendering data is not separated into another file due to the close connection with the routing component, whether it is the Next.js routing file that exports various data acquisition functions at the same time or Nuxt.js directly adds Vue options to the component. Configuration or function can be seen as an enhancement to the component. The Ada method is different. The components are not directly exported under the routing folder, but different processing functions and modules need to be exported according to the operating environment. For example, the server-side corresponding index.server.js file needs to export the HTTP request method with the same name. For GET and POST functions, developers can do some data prefetching operations, page template rendering, etc. in the functions; the index.js file corresponding to the client needs to export the component mounting code.
  • In terms of rendering performance improvement, both Next.js and Nuxt.js have adopted the same strategy: static generation, preloading the resource files of the matched route, preload, etc., you can refer to optimization.
  • On request intervention (ie middleware):

    • Next.js and Nuxt.js do not divide the functions of the middleware. They adopt a method similar to Express or Koa using the next() function to control the process, while Nest.js divides it into several standardized implementations more directly based on functional characteristics. .
    • Regardless of the usage of the overall configuration at the application level, Nuxt.js uses routing to define which middleware is required. Nest.js is also more like Nuxt.js, which is determined by routing. It uses decorators to configure on routing handlers and Controllers. Next .js middleware will affect the same level and lower-level routing. The middleware determines 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 Nest.js, abstracting the execution process into several life cycles, and making the middleware into task functions with different functional types in different life cycles. For developers, the functions of custom life cycle are not exposed, but based on the code reuse level, it also provides server-side extensions, web module extensions and other capabilities. Because Ada can collectively refer to page routing, API routing, server-side extensions, web modules, etc. For the files of the artifacts to be launched independently, in order to consider the stability and clarification of the scope of influence, it is also the way to actively call the routing to determine which expansion capabilities need to be enabled.
  • Nest.js officially provides documentation capabilities based on decorators. It is a common practice to use type declarations (such as parsing TypeScript grammar, GraphQL structure definitions) to generate interface documents. However, although Nest.js supports TypeScript very well and does not directly solve the problem of type verification at runtime, it can be achieved through pipelines and middleware.
  • Fastify set out to improve the operating efficiency of the low-level details, and it can be said to have achieved the ultimate. At the same time, the more the implementation based on the bottom layer, the more it can be used in more scenarios. The optimization method of route matching and context reuse can be further investigated in the future.
  • In addition, tools such as swc and ESBuild to improve the development experience and online speed are also a direction that needs to be investigated.

interested in Ada architecture design can read the articles published in the past: 16196f01700c3a "Behind the implementation of GraphQL: the trade-off between the pros and cons" , "Decrypt the big front-end architecture of Recruitment Ada" 16196f01700c40, "The micro-front-end implementation practice of Zhaolian Recruitment—— Widget" , "Reconstruction experience of Koa middleware system" , "Zhilian Recruitment Web Module Expansion Plan" etc.

阅读 2.9k

智联大前端
作为智联招聘的前端架构团队,我们开创了细粒度的前端研发和发布模式,统一了移动端和桌面端的技术栈,...

您好, 我们是【智联大前端​】。

1.2k 声望
5.1k 粉丝
0 条评论

您好, 我们是【智联大前端​】。

1.2k 声望
5.1k 粉丝
文章目录
宣传栏