带有JSX路由的NodeJS服务器(FART FARTIFY)
#javascript #node #esbuild #jsx

JSX是XML样语法扩展到JavaScript。它主要与前端开发有关,因为它用于客户端库/框架(例如ReactSolid),但实际上它的潜力超出了模板渲染。 JSX被转移到纯JavaScript中,您对此的处理取决于您。

您可以处理的一件事是为后端服务器生成端点。

目标

目标是拥有一台NodeJS服务器,该服务器将使用这样的JSX组件路由请求:

/** src/routes/Users.jsx */

const GetUser = async (req) => {
  const id = req.query.id;
  return Promise.resolve({
    id,
    message: `You requested user with id ${id}`,
  });
};

const GetAllUsers = async () => {
  return Promise.resolve({
    message: 'You requested all users',
  });
};

const PostUser = async (req) => {
  const { name, surname } = req.body;
  return Promise.resolve({
    message: `You posted user with name = "${name}" and surname = "${surname}"`,
  });
};

export const UserRouter = () => (
  <Router path="/users">
    <Endpoint method="GET">
      <GetUser />
    </Endpoint>
    <Endpoint method="POST">
      <PostUser />
    </Endpoint>
    <Endpoint method="GET" path="/all">
      <GetAllUsers />
    </Endpoint>
  </Router>
);

/** src/App.jsx */
export const App = () => {
  const port = 3000;
  const options = {
    logger: false,
  };

  const onStart = () => console.log(`App started on port ${port}`);

  return (
    <FastifyApp
      port={port}
      fastifyOptions={options}
      onStart={onStart}
    >
      <UserRouter/>
    </FastifyApp>
  );
}

技术

  • esbuild:transpile JSX和捆绑服务器代码
  • fastify:node.js
  • 的快速和低架空网络框架

概述

为了实现目标,应将JSX代码转移到普通的JavaScript中,可以由node执行。您可以使用所需的任何转板器,在本教程中,我将与esbuild一起使用,因为它易于使用和设置。我们还必须提供一个自定义JSX处理器,在我们的情况下,它将是一个返回参数的简单函数,逻辑将在组件本身中实现。

实体(基本组件)

从上面的代码中,我们可以确定以下实体:

  • fastifyapp:将实例化fastify服务器并分配路由的组件
  • 路由器:将处理其子女并将端点列表返回fastifyApp
  • 的组件
  • 端点:包含端点描述(方法,路径)的组件,并将返回处理程序

设置

要运行此项目,您必须安装以下依赖项(您不必使用相同的版本):

  • esbuild(v^0.15.12
  • fastify(v^4.9.2

自定义处理器和启动功能

  • customjsxprocessor:此功能将用于替换JSX元素。它与react.createelement相同,应在所有JSX文件中导入(本教程中没有自动IMPORT)。
  • 开始:此功能将启动应用程序
/* src/CustomJsxProcessor.js */
function CustomJsxProcessor(tagName, props, ...children) {
  return {
    fn: tagName,// since we have no string tags here (all components are functions), we will rename tagName to fn
    props,
    children,
  };
}

export function start (app) {
  const proto = app();
  proto.fn({
    ...proto.props,
    children: proto.children,
  });
}

export default CustomJsxProcessor;

基本组件

请注意,基本组件(端点,路由器和FastifyApp)不会像您使用客户端侧库那样返回JSX,而是返回可以通过Fastify使用的JavaScript对象。

基本组件将在开始时间(一次)在运行时进行修改(如每次执行组件执行的情况)。

>

端点

/* src/components/Endpoint.js */

/**
 * Process endpoint
 * For simplicity, only the first child will be used as handler
 */
const getEndpoint = ({ method, path }, children) => {
  if (children.length < 1) {
    console.warn(`No handler detected for endpoint ${method}: ${path}`);
  } else {
    if (children.length > 1) {
      console.warn(`Multiple handlers detected for endpoint ${method}: ${path}. Only the first one will be used`);
    }

    return ({
      method: method,
      url: path || '',
      handler: typeof children[0] === "function" ? children[0] : children[0].fn,
    });
  }
}

export const Endpoint = ({ method = 'GET', path, children }) => getEndpoint({ method, path }, children);

路由器

/* src/components/Router.js */

import {Endpoint} from "./Endpoint";

/**
 * process Endpoint component.
 * This will take in input a JSX component and return javascript endpoint object
 * <Endpoint path="/users" method="GET">{handler}</Endpoint> -> ({
 *   path: '/users',
 *   method: 'GET',
 *   handler: (req, res) => { ... },
 * })
 */
const getEndpoint = (path, node) => {
  // path -> router path. Endpoint will use it as prefix for nesting
  // node -> a JSX element (<Endpoint ... />)

  // execute endpoint function to get endpoint info (method, url, handler)
  const endpoint = node.fn({
    ...node.props,
    children: node.children,
  });

  // prefix endpoint path with router path
  if (path) {
    endpoint.url = path + endpoint.url;
  }

  return endpoint;
};

export const Router = ({ children, path }) => {
  const endpoints = [];

  for (const child of children) {
    if (child.fn === Router) {
      // a child can be a nested router, execute it and get all of its endpoints as nested paths
      const r = child;
      endpoints.push(
        ...r.fn({
          ...r.props,
          path: r.props.path ? path + r.props.path : '',
          children: r.children,
        }),
      );
    } else if (Endpoint === child.fn) {
      // a child is endpoint, process it and add it to the endpoints list
      endpoints.push(getEndpoint(
        path,
        child,
      ));
    } else {
      console.warn(`${child.fn} is not supported under Router`);
    }
  }

  return endpoints;
}

fastifyapp

/* src/components/Router.js */

import fastify from "fastify";
import { Router } from "./Router";

/**
 * Process Router children and return list of endpoints
 */
export const getEndpoints = (nodes) => {
  const endpoints = [];

  for (const child of nodes) {
    let r;
    /**
     * A child can be Router or a component returning a Router.
     * In the latest case, we should execute the function to get the Router
     */
    if (child.fn === Router) {
      // child is a Router
      r = child;
    } else {
      // child is a Component, execute it, since it may return a Router
      r = child.fn({
        ...child.props,
        children: child.children,
      });
    }

    // if `r` is router, get endpoints
    if (r.fn === Router) {
      endpoints.push(
        ...r.fn({
          ...r.props,
          children: r.children,
        }),
      );
    }
  }

  return endpoints;
};

export const FastifyApp = ({
  children,
  onStart,
  port,
  fastifyOptions,
}) => {
  const endpoints = getEndpoints(children);
  // create fastify server
  const server = fastify(fastifyOptions);

  // assign endpoints
  for (const endpoint of endpoints) {
    console.log(`Create endpoint ${endpoint.method}: ${endpoint.url}`);
    server.route({
      method: endpoint.method,
      url: endpoint.url,
      handler: endpoint.handler,
    });
  }

  // start server
  server.listen({ port }).then(() => {
    if (onStart) {
      onStart();
    }
  });
};

应用程序

现在我们拥有所有基本组件,我们可以开始实现应用程序。

用户路由器

/* src/routes/Users.jsx */

/* ! REMEMBER TO IMPORT JSX FACTORY (CustomJsxProcessor) */
import CustomJsxProcessor from "../CustomJsxProcessor";
import {Router} from "../components/Router";
import {Endpoint} from "../components/Endpoint";

const GetUser = async (req) => {
  const id = req.query.id;
  return Promise.resolve({
    id,
    message: `You requested user with id ${id}`,
  });
};

const GetAllUsers = async () => {
  return Promise.resolve({
    message: 'You requested all users',
  });
};

const PostUser = async (req) => {
  const { name, surname } = req.body;
  return Promise.resolve({
    message: `You posted user with name = "${name}" and surname = "${surname}"`,
  });
};

const NestedGet = async () => Promise.resolve({
  message: `This endpoint is nested`,
});

export const UserRouter = () => (
  <Router path="/users">
    <Endpoint method="GET">
      <GetUser />
    </Endpoint>
    <Endpoint method="POST">
      <PostUser />
    </Endpoint>
    <Endpoint method="GET" path="/all">
      <GetAllUsers />
    </Endpoint>

    {/* Nested routing, will inherit /users */}
    {/* Current implementation does not allow nested component routers */}
    <Router path="/nested">
      <Endpoint method="GET">
        <NestedGet />
      </Endpoint>
      <Endpoint method="GET" path="/no-component-example">
        {async (req, res) => {
          // you can use handlers without components
          const date = Date.now();
          return Promise.resolve({
            message: `This handler does not have component`,
            timestamp: date,
          });
        }}
      </Endpoint>
    </Router>
  </Router>
);

快速实例化应用程序

/* src/App.jsx */

import CustomJsxProcessor from "./CustomJsxProcessor";
import {UserRouter} from "./routes/Users";
import {FastifyApp} from "./components/FastifyApp";

export const App = () => {
  const port = 3000;
  // fastify options
  const options = {
    logger: false,
  };

  const onStart = () => console.log(`App started on port ${port}`);

  return (
    <FastifyApp
      port={port}
      fastifyOptions={options}
      onStart={onStart}
    >
      <UserRouter/>
    </FastifyApp>
  );
}

启动服务器的入口点

/* src/server.js */

import {App} from "./App";
import { start } from "./CustomJsxProcessor";

start(App);

运行服务器

现在所有组件都已经到位,并且已实现应用程序,唯一剩下的就是运行它。我们将需要使用esbuild捆绑该应用并用节点执行捆绑包。

捆绑脚本

/* scripts/index.js */

#!/usr/bin/env node
const esbuild = require('esbuild');

esbuild.build({
  entryPoints: ["src/server.js"],
  bundle: true,
  outfile: "build/server.js",
  jsxFactory: 'CustomJsxProcessor',
  jsx: 'transform',
  platform: 'node',
}).catch(() => process.exit(1));

脚本命令

运行应用程序执行以下命令:

node scripts/index.js && node ./build/server.js

package.json

仅供参考,package.json看起来像这样:

{
  "name": "jsx-server-routing-with-fastify",
  "version": "0.0.0",
  "scripts": {
    "start:article": "node scripts/index.js && node ./build/server.js"
  },
  "dependencies": {
    "fastify": "^4.9.2",
    "esbuild": "^0.15.12"
  }
}

您可以找到代码here