JSX是XML样语法扩展到JavaScript。它主要与前端开发有关,因为它用于客户端库/框架(例如React和Solid),但实际上它的潜力超出了模板渲染。 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>
);
}
技术
概述
为了实现目标,应将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。