关于服务端:前端服务框架调研NextjsNuxtjsNestjsFastify

13次阅读

共计 11751 个字符,预计需要花费 30 分钟才能阅读完成。

概述

这次 Node.js 服务框架的调研将着点于各框架性能、申请流程的组织和染指形式,以对前端 Node.js 服务设计和对智联 Ada 架构改良提供参考,不过多关注具体实现。

最终选取了以下具备代表性的框架:

  • Next.js、Nuxt.js:它们是别离与特定前端技术 React、Vue 绑定的前端利用开发框架,有肯定的相似性,能够放在一起进行调研比照。
  • Nest.js:是“Angular 的服务端实现”,基于装璜器。能够应用任何兼容的 http 提供程序,如 Express、Fastify 替换底层内核。可用于 http、rpc、graphql 服务,对提供更多样的服务能力有肯定参考价值。
  • Fastify:一个应用插件模式组织代码且反对并基于 schema 做了运行效率晋升的比拟纯正的偏底层的 web 框架。

Next.js、Nuxt.js

这两个框架的重心都在 Web 局部,对 UI 出现局部的代码的组织形式、服务器端渲染性能等提供了欠缺的反对。

  • Next.js:React Web 利用框架,调研版本为 12.0.x。
  • Nuxt.js:Vue Web 利用框架,调研版本为 2.15.x。

性能

首先是路由局部:

  • 页面路由:

    • 雷同的是两者都遵循文件即路由的设计。默认以 pages 文件夹为入口,生成对应的路由构造,文件夹内的所有文件都会被当做路由入口文件,反对多层级,会依据层级生成路由地址。同时如果文件名为 index 则会被省略,即 /pages/users 和 /pages/users/index 文件对应的拜访地址都是 users。
    • 不同的是,依据依赖的前端框架的不同,生成的路由配置和实现不同:

      • Next.js:因为 React 没有官网的路由实现,Next.js 做了本人的路由实现。
      • Nuxt.js:基于 vue-router,在编译时会生成 vue-router 构造的路由配置,同时也反对子路由,路由文件同名的文件夹下的文件会变成子路由,如 article.js,article/a.js,article/b.js,a 和 b 就是 article 的子路由,可配合 <nuxt-child /> 组件进行子路由渲染。
  • api 路由:

    • Next.js:在 9.x 版本之后增加了此性能的反对,在 pages/api/ 文件夹下(为什么放在 pages 文件夹下有设计上的历史包袱)的文件会作为 api 失效,不会进入 React 前端路由中。命名规定雷同,pages/api/article/[id].js -> /api/article/123。其文件导出模块与页面路由导出不同,但不是重点。
    • Nuxt.js:官网未提供反对,然而有其余实现路径,如应用框架的 serverMiddleware 能力。
  • 动静路由:两者都反对动静路由拜访,然而命名规定不同:

    • Next.js:应用中括号命名,/pages/article/[id].js -> /pages/article/123。
    • Nuxt.js:应用下划线命名,/pages/article/_id.js -> /pages/article/123。
  • 路由加载:两者都内建提供了 link 类型组件(LinkNuxtLink),当应用这个组件代替 <a></a> 标签进行路由跳转时,组件会检测链接是否命中路由,如果命中,则组件呈现在视口后会触发对对应路由的 js 等资源的加载,并且点击跳转时应用路由跳转,不会从新加载页面,也不须要再期待获取渲染所需 js 等资源文件。
  • 出错兜底:两者都提供了错误码响应的兜底跳转,只有 pages 文件夹下提供了 http 错误码命名的页面路由,当其余路由产生响应谬误时,就会跳转到到错误码路由页面。

在依据文件构造生成路由配置之后,咱们来看下在代码组织形式上的区别:

  • 路由组件:两者没有区别,都是应用默认导出组件的形式决定路由渲染内容,React 导出 React 组件,Vue 导出 Vue 组件:

    • Next.js:一个普普通通的 React 组件:

      export default function About() {return <div>About us</div>}
    • Nuxt.js:一个普普通通的 Vue 组件:

      <template>
          <div>About us</div>
      </template>
      <script>
      export default {}
      <script>
  • 路由组件外壳:在每个页面路由组件之外还能够有一些预约义外壳来承载路由组件的渲染,在 Next.js 和 Nuxt.js 中都别离有两层外壳能够自定义:

    • 容器:可被页面路由组件专用的一些容器组件,外部会渲染页面路由组件:

      • Next.js:须要改写 pages 根门路下的 _app.js,会对整个 Next.js 利用失效,是惟一的。其中 <Component /> 为页面路由组件,pageProps 为预取的数据,前面会提到

        import '../styles/global.css'
        export default function App({Component, pageProps}) {return <Component {...pageProps} />
        }
      • Nuxt.js:称为 Layout,能够在 layouts 文件夹下创立组件,如 layouts/blog.vue,并在路由组件中指明 layout,也就是说,Nuxt.js 中能够有多套容器,其中 <Nuxt /> 为页面路由组件:

        <template>
            <div>
                <div>My blog navigation bar here</div>
                <Nuxt /> // 页面路由组件
            </div>
        </template>
        // 页面路由组件
        <template>
        </template>
        <script>
        export default {
            layout: 'blog',
            // 其余 Vue options
        }
        </script>
    • 文档:即 html 模板,两者的 html 模板都是惟一的,会对整个利用失效:

      • Next.js:改写 pages 根门路下惟一的 _document.js,会对所有页面路由失效,应用组件的形式渲染资源和属性:

        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:改写根目录下惟一的 App.html,会对所有页面路由失效,应用占位符的形式渲染资源和属性:

        <!DOCTYPE html>
        <html {{HTML_ATTRS}}>
        <head {{HEAD_ATTRS}}>
            {{HEAD}}
        </head>
        <body {{BODY_ATTRS}}>
            {{APP}}
        </body>
        </html>
  • head 局部:除了在 html 模板中间接写 head 内容的形式,如何让不同的页面渲染不同的 head 呢,咱们晓得 head 是在组件之外的,那么两者都是如何解决这个问题的呢?

    • Next.js:能够在页面路由组件中应用内建的 Head 组件,外部写 title、meta 等,在渲染时就会渲染在 html 的 head 局部:

      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:同样能够在页面路由组件中配置,同时也反对进行利用级别配置,通用的 script、link 资源能够写在利用配置中:

      • 在页面路由组件配置:应用 head 函数的形式返回 head 配置,函数中能够应用 this 获取实例:

        <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 进行利用配置:

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

除去根本的 CSR(客户端渲染),SSR(服务器端渲染)也是必须的,咱们来看下两者都是怎么提供这种能力的,在此之外又提供了哪些渲染能力?

  • 服务器端渲染:家喻户晓的是服务器端渲染须要进行数据预取,两者的预取用法有何不同?

    • Next.js:

      • 能够在页面路由文件中导出 getServerSideProps 办法,Next.js 会应用此函数返回的值来渲染页面,返回值会作为 props 传给页面路由组件:

        export async function getServerSideProps(context) {
            // 发送一些申请
            return {props: {}
            }
        }
      • 上文提到的容器组件也有本人的办法,不再介绍。
      • 渲染过程的最初,会生成页面数据与页面构建信息,这些内容会写在 <script id="__NEXT_DATA__"/> 中渲染到客户端,并被在客户端读取。
    • Nuxt.js:数据预取办法有两个,别离是 asyncData、fetch:

      • asyncData:组件可导出 asyncData 办法,返回值会和页面路由组件的 data 合并,用于后续渲染,只在页面路由组件可用。
      • fetch:在 2.12.x 中减少,利用了 Vue SSR 的 serverPrefetch,在每个组件都可用,且会在服务器端和客户端同时被调用。
      • 渲染过程的最初,页面数据与页面信息写在 window.__NUXT__ 中,同样会在客户端被读取。
  • 动态页面生成 SSG:在构建阶段会生成动态的 HTML 文件,对于访问速度晋升和做 CDN 优化很有帮忙:

    • Next.js:在两种条件下都会触发主动的 SSG:

      1. 页面路由文件组件没有 getServerSideProps 办法时;
      2. 页面路由文件中导出 getStaticProps 办法时,当须要应用数据渲染时能够定义这个办法:

        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:提供了命令 generate 命令,会对整站生成残缺的 html。
  • 不论是那种渲染形式,在客户端出现时,页面资源都会在头部通过 rel=”preload” 的形式提前加载,以提前加载资源,晋升渲染速度。

在页面渲染之外的流程的其余节点,两者也都提供了的染指能力:

  • Next.js:能够在 pages 文件夹下的各级目录建设 _middleware.js 文件,并导出中间件函数,此函数会对同级目录下的所有路由和上级路由逐层失效。
  • Nuxt.js:中间件代码有两种组织形式:

    1. 写在 middleware 文件夹下,文件名将会成为中间件的名字,而后能够在利用级别进行配置或 Layout 组件、页面路由组件中申明应用。
    2. 间接在 Layout 组件、页面路由组件写 middleware 函数。
    3. 利用级别:在 middleware 中创立同名的中间件文件,这些中间件将会在路由渲染前执行,而后能够在 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. 组件级别:能够在 layout 或页面组件中申明应用那些 middleware:

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

      也能够间接写全新的 middleware:

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

    在编译构建方面,两者都是基于 webpack 搭建的编译流程,并在配置文件中通过函数参数的形式裸露了 webpack 配置对象,未做什么限度。其余值得注意的一点是 Next.js 在 v12.x.x 版本中将代码压缩代码和与本来的 babel 转译换为了 swc,这是一个应用 Rust 开发的更快的编译工具,在前端构建方面,还有一些其余非基于 JavaScript 实现的工具,如 ESbuild。

在扩大框架能力方面,Next.js 间接提供了较丰盛的服务能力,Nuxt.js 则设计了模块和插件零碎来进行扩大。

Nest.js

Nest.js 是“Angular 的服务端实现”,基于装璜器。Nest.js 与其余前端服务框架或库的设计思路齐全不同。咱们通过查看申请生命周期中的几个节点的用法来体验下 Nest.js 的设计形式。

先来看下 Nest.js 残缺的的生命周期:

  1. 收到申请
  2. 中间件

    1. 全局绑定的中间件
    2. 门路中指定的 Module 绑定的中间件
  3. 守卫

    1. 全局守卫
    2. Controller 守卫
    3. Route 守卫
  4. 拦截器(Controller 之前)

    1. 全局
    2. Controller 拦截器
    3. Route 拦截器
  5. 管道

    1. 全局管道
    2. Controller 管道
    3. Route 管道
    4. Route 参数管道
  6. Controller(办法处理器)
  7. 服务
  8. 拦截器(Controller 之后)

    1. Router 拦截器
    2. Controller 拦截器
    3. 全局拦截器
  9. 异样过滤器

    1. 路由
    2. 控制器
    3. 全局
  10. 服务器响应

能够看到依据性能特点拆分的比拟细,其中拦截器在 Controller 前后都有,与 Koa 洋葱圈模型相似。

功能设计

首先看下路由局部,即最核心的 Controller:

  • 门路:应用装璜器装璜 @Controller 和 @GET 等装璜 Controller 类,来定义路由解析规定。如:

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

    定义了 /cats post 申请和 /cats/sub get 申请的处理函数。

  • 响应:状态码、响应头等都能够通过装璜器设置。当然也能够间接写。如:

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

    @Post()
    async create(@Body() createCatDto: CreateCatDto) {return 'This action adds a new cat'}
  • 申请解决的其余能力形式相似。

再来看看生命周期中其中几种其余的解决能力:

  • 中间件:申明式的注册办法:

    @Module({})
    export class AppModule implements NestModule {configure(consumer: MiddlewareConsumer) {
            consumer
            // 利用 cors、LoggerMiddleware 于 cats 路由 GET 办法
            .apply(LoggerMiddleware)
            .forRoutes({path: 'cats', method: RequestMethod.GET})
        }
    }
  • 异样过滤器(在特定范畴捕捉特定异样并解决),可作用于单个路由,整个控制器或全局:

    // 程序须要抛出特定的类型谬误
    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()
    }
  • 守卫:返回 boolean 值,会依据返回值决定是否继续执行后续申明周期:

    // 申明时须要应用 @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'}
  • 管道(更偏重对参数的解决,能够了解为 controller 逻辑的一部分,更申明式 ):

    1. 校验:参数类型校验,在应用 TypeScript 开发的程序中的运行时进行参数类型校验。
    2. 转化:参数类型的转化,或者由原始参数求取二级参数,供 controllers 应用:

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

咱们再来简略的看下 Nest.js 对不同利用类型和不同 http 提供服务是怎么做适配的:

  • 不同利用类型:Nest.js 反对 Http、GraphQL、Websocket 利用,在大部分状况下,在这些类型的利用中生命周期的性能是统一的,所以 Nest.js 提供了上下文类 ArgumentsHostExecutionContext,如应用 host.switchToRpc()host.switchToHttp() 来解决这一差别,保障生命周期函数的入参统一。
  • 不同的 http 提供服务则是应用不同的适配器,Nest.js 的默认内核是 Express,然而官网提供了 FastifyAdapter 适配器用于切换到 Fastify。

Fastify

有这么一个框架依附数据结构和类型做了不同的事件,就是 Fastify。它的官网阐明的特点就是“快”,它晋升速度的实现是咱们关注的重点。

咱们先来看看开发示例:

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

能够留神到的两点是:

  1. 在路由定义时,传入了一个申请的 schema,在官网文档中也说对响应的 schema 定义能够让 Fastify 的吞吐量回升 10%-20%。
  2. Fastify 应用 decorate 的形式对 Fastify 能力进行加强,也能够将 decorate 局部提取到其余文件,应用 register 的形式创立全新的上下文的形式进行封装。

没体现到的是 Fastify 申请染指的反对形式是应用生命周期 Hook,因为这是个对前端(Vue、React、Webpack)来说很常见的做法就不再介绍。

咱们重点再来看一下 Fastify 的提速原理。

如何提速

有三个比拟要害的包,依照重要性排别离是:

  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',
    })
    • 与 JSON.stringify 性能雷同,在负载较小时,速度更快。
    • 其原理是在执行阶段先依据字段类型定义提前生成取字段值的字符串拼装的函数,如:

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

      相当于省略了对字段值的类型的判断,省略了每次执行时都要进行的一些遍历、类型判断,当然实在的函数内容比这个要简单的多。那么引申而言,只有可能晓得数据的构造和类型,咱们都能够将这套优化逻辑复制过来。

  • find-my-way:将注册的路由生成了压缩前缀树的构造,依据基准测试的数据显示是速度最快的路由库中性能最全的。
  • reusify:在 Fastify 官网提供的中间件机制依赖库中,应用了此库,可复用对象和函数,防止创立和回收开销,此库对于使用者有一些基于 v8 引擎优化的应用要求。在 Fastify 中次要用于上下文对象的复用。

总结

  • 在路由构造的设计上,Next.js、Nuxt.js 都采纳了文件构造即路由的设计形式。Ada 也是应用文件构造约定式的形式。
  • 在渲染方面 Next.js、Nuxt.js 都没有将根组件之外的构造的渲染间接体现在路由解决的流程上,暗藏了实现细节,然而能够以更偏差配置化的形式由根组件决定组件之外的构造的渲染(head 内容)。同时渲染数据的申请因为和路由组件分割严密也都没有拆散到另外的文件,不论是 Next.js 的路由文件同时导出各种数据获取函数还是 Nuxt.js 的在组件上间接减少 Vue options 之外的配置或函数,都能够看做对组件的一种加强。Ada 的形式有所不同,路由文件夹下并没有间接导出组件,而是须要依据运行环境导出不同的处理函数和模块,如服务器端对应的 index.server.js 文件中须要导出 HTTP 申请形式同名的 GET、POST 函数,开发人员能够在函数内做一些数据预取操作、页面模板渲染等;客户端对应的 index.js 文件则须要导出组件挂载代码。
  • 在渲染性能晋升方面,Next.js、Nuxt.js 也都采取了雷同的策略:动态生成、提前加载匹配到的路由的资源文件、preload 等,能够参考优化。
  • 在申请染指上(即中间件):

    • Next.js、Nuxt.js 未对中间件做性能划分,采取的都是相似 Express 或 Koa 应用 next() 函数管制流程的形式,而 Nest.js 则将更间接的依照性能特色分成了几种规范化的实现。
    • 不谈利用级别整体配置的用法,Nuxt.js 是由路由来定义须要哪个中间件,Nest.js 也更像 Nuxt.js 由路由来决定的形式应用装璜器配置在路由 handler、Controller 上,而 Next.js 的中间件会对同级及上级路由产生影响,由中间件来决定影响范畴,是两种齐全相同的管制思路。
    • Ada 架构基于 Koa 内核,然而外部中间件实现也与 Nest.js 相似,将执行流程形象成了几个生命周期,将中间件做成了不同生命周期内性能类型不同的工作函数。对于开发人员未裸露自定义生命周期的性能,然而基于代码复用层面,也提供了服务器端扩大、Web 模块扩大等能力,因为 Ada 能够对页面路由、API 路由、服务器端扩大、Web 模块等统称为工件的文件进行独立上线,为了稳定性和明确影响范畴等方面思考,也是由路由被动调用的形式决定本人须要启用哪些扩大能力。
  • Nest.js 官网基于装璜器提供了文档化的能力,利用类型申明(如解析 TypeScript 语法、GraphQL 构造定义)生成接口文档是比拟广泛的做法。不过尽管 Nest.js 对 TypeScript 反对很好,也没有间接解决运行时的类型校验问题,不过能够通过管道、中间件达成。
  • Fastify 则着手于底层细节进行运行效率晋升,且堪称做到了极致。同时越是基于底层的实现越可能应用在越多的场景中。其路由匹配和上下文复用的优化形式能够在之后进行进一步的落地调研。
  • 除此之外 swc、ESBuild 等晋升开发体验和上线速度的工具也是须要落地调研的一个方向。

对 Ada 架构设计感兴趣的能够浏览往期公布的文章:《GraphQL 落地背地:利弊取舍》、《解密智联招聘的大前端架构 Ada》、《智联招聘的微前端落地实际——Widget》、《Koa 中间件体系的重构教训》、《智联招聘的 Web 模块扩大落地计划》等。

正文完
 0