Забудьте об ajax, используйте интеграцию с интерфейсом и сервером

Node.js внешний интерфейс
Забудьте об ajax, используйте интеграцию с интерфейсом и сервером

Просматривал GitHub два дня назад и нашелmidwayjsЭтот проект делает упор на интеграцию интерфейсных и внутренних функций.После опыта я обнаружил, что это потрясающе.Можно запросить интерфейс без ajax. На следующий день я просмотрел GitHub и нашел этот байт с открытым исходным кодом.modern.jsУ меня тоже есть эта концепция, и я обнаружил, что в Интернете очень мало статей, объясняющих ее, поэтому я решил написать эту статью после исследования.

Что такое фронтенд-интеграция?

Прежде чем говорить о концепции, давайте заглянем на официальный сайт Modern.js.примерПолучите более интуитивное ощущение:

// api/hello.ts

export const get = async () => "Hello Modern.js";
// src/App.tsx

import { useState, useEffect } from "react";
import { get as hello } from "@api/hello";

export default () => {
  const [text, setText] = useState("");

  useEffect(() => {
    hello().then(setText);
  }, []);
  return <div>{text}</div>;
};

Когда мы включили сеть, было удивительно обнаружить, что онаhttp://localhost:8080/api/helloЗапрос отправлен, и данные, возвращенные нашей функцией, получены.

На самом деле, нет официального определения front-end и back-end интеграции, с точки зрения производительности ее можно просто определить как: ​

  • Интерфейсный код и nodejs в качестве внутреннего кода размещаются в одном проекте.
  • использовать то же самоеpackage.jsonУправление зависимостями
  • Две стороны взаимодействуют напрямую через вызовы функций вместо традиционных запросов ajax.

Зачем интегрировать front-end и back-end?

По моему собственному опыту, есть как минимум два преимущества:

  • Тип единство

Если вы написали код с Node + ts, то должна быть болевая точка, то есть определение типа нужно определить на стороне Node, а потом скопировать во фронтенд запрос, иначе нужно писать инструмент для синхронизации определения на стороне узла с интерфейсом.Это очень недружественный опыт разработки.

Интеграция передних и задних концов прекрасно решает эту проблему.

  • легко развиваться

Этот способ интеграции фронтенда и бекенда, без ajax, без роутинга, без GET, POST, вызывает бэкэнд-интерфейс точно так же, как вызов обычных функций, такой опыт разработки действительно классный.

Вопросы и ответы в комментариях

Вопрос 1: В чем разница между этой вещью и традиционными передними и задними концами, не разделенными, реинкарнацией небес?

  • Интеграция внешнего и внутреннего интерфейса по существу отделена от внешнего и внутреннего интерфейса, а это означает, что по-прежнему создаются две части кода (интерфейсный и внутренний), а также вызов ajax. в конечном итоге используется, что полностью отличается от традиционного шаблона замены строки, где внешний и внутренний интерфейсы не разделены. ;
  • Традиционное разделение интерфейса и сервера подходит только для нативной разработки, потому что это все еще замена строки, поэтому это может быть только .html, .tsx или .vue. Браузеры не знают (конечно, это также может быть визуализируется в режиме реального времени), но интерфейс и сервер интегрированы. Ajax не ограничивает стек интерфейсных технологий. Он подобен уровню инкапсуляции Ajax, который скрывает настоящие вызовы Ajax.

Вопрос 2: Применимые сценарии?

Судя по описанию двух проектов, они больше используются в бессерверных сценариях, потому что функция — это интерфейс, разве это не легендарная концепция фасса, но оба проекта также могут быть развернуты независимо.

Принцип и реализация

принцип

Его производительность кажется волшебной — введение функции для отправки ajax-запросов, но принцип на самом деле довольно прост, ключевым моментом являетсяapisКаталог фактически читается дважды:

  • Один раз — превратить функцию в код запроса во время сборки.
  • Однажды в функции чтения в качестве внутреннего обработчика маршрута

выполнить

Для более простого объяснения мы используемviteКак инструмент сборки (по сравнению с плагином webpack, который легче понять), он шаг за шагом реализует функцию интеграции с интерфейсом и сервером. ​

Полный код находится на GitHub:GitHub.com/dream2023/…

1. Инициализировать проект

Ссылаться наviteДокументация:

yarn create @vitejs/app my-vue-app --template vue-ts
cd my-vue-app
yarn
yarn dev

1.pngПриведенный выше интерфейс указывает на то, что запуск прошел успешно.

2. Измените и добавьте файлы

новыйsrc/apis/user.tsдокумент

export interface User {
  name: string;
  age: number;
}

interface Response<T = any> {
  code: number;
  msg: string;
  data?: T;
}

export async function getUser(): Promise<Response<User>> {
  // 假设从数据库读取
  const user: User = {
    name: "jack",
    age: 18,
  };
  return {
    code: 0,
    msg: "ok",
    data: user,
  };
}

export async function updateUser(user: User): Promise<Response<User>> {
  return {
    code: 0,
    msg: "ok",
    data: user,
  };
}

Изменить src/App.vue

<script setup lang="ts">
  import { onMounted, ref } from "vue";
  import { getUser, User, updateUser } from "./apis/user";
  const user = ref<User>();
  onMounted(() => {
    getUser().then((res) => {
      user.value = res.data;
    });
  });

  const handleUpdate = () => {
    updateUser({ name: "li", age: 10 }).then((res) => {
      alert(JSON.stringify(res.data));
    });
  };
</script>

<template>
  <div v-if="user">
    <div>username: {{ user.name }}</div>
    <div>age: {{ user.age }}</div>
    <button @click="handleUpdate">更新 user</button>
  </div>
</template>

2.pngМы открыли сеть и обнаружили, что данные были получены без отправки ajax-запроса, это неправильно, и нам нужно внести дополнительные улучшения.

3. Преобразовать функцию в запрос интерфейса

Преобразование функции в запрос интерфейса требует от нас изменения содержимого файла во время построения, для чего нам нужно использоватьвайт-плагинСпособность.

// 项目/myPlugin.ts
import { Plugin } from "vite";

export default function VitePlugin(): Plugin {
  return {
    name: "my-plugin",
    transform(src, id) {
      // src 是文件内容,id 是文件路径
    },
  };
}
// 项目/vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import myPlugin from "./myPlugin";

export default defineConfig({
  plugins: [vue(), myPlugin()],
});

Затем устанавливается съемная полка. ​

Конкретную логику реализации можно разделить на два этапа: ​

  • определитьsrc/apisСодержимое файла в
  • Если нужно переписать следующую функцию для запроса кода

3.png

// 目标转换结果

function getUser() {
  // 1. 使用 fetch 请求
  // 2. url 为 /api/ + 文件名 + 函数名,避免路由重复
  return fetch("/api/user/getUser", {
    method: "GET",
    headers: {
      "Content-Type": "application/json",
    },
  }).then((res) => res.json());
}

function updateUser(data) {
  return fetch("/api/user/updateUser", {
    method: "POST",
    body: JSON.stringify(data),
    headers: {
      "Content-Type": "application/json",
    },
  }).then((res) => res.json());
}
// 具体代码实现
import * as path from "path";
import { Plugin } from "vite";

const apisPath = path.join(__dirname, "./src/apis");

// 发起请求的模板
const requestTemp = (
  fileName: string,
  fn: string
) => `export function ${fn}(data) {
    const isGet = !data
    return fetch("/api/${fileName}/${fn}", {
        method: isGet ? "GET" : "POST",
        body: isGet ? undefined : JSON.stringify(data),
        headers: {
          'Content-Type': 'application/json'
        },
    }).then((res) => res.json());
}`;

export default function VitePlugin(): Plugin {
  return {
    name: "my-plugin",
    transform(src, id) {
      // 1.判断是否为 apis 目录下的文件
      if (id.startsWith(apisPath)) {
        // 获取文件名
        const fileName = path.basename(id, ".ts");

        // 正则获取函数名
        const fnNames = [...src.matchAll(/async function (\w+)/g)].map(
          (item) => item[1]
        );

        // 2.转换文件内容为 request
        const code = fnNames.map((fn) => requestTemp(fileName, fn)).join("\n");
        return {
          code,
          map: null,
        };
      }
    },
  };
}

4.png 5.pngКак видно из рисунка выше, содержимое файла было перезаписано и можно делать запрос, далее мы перехватим и обработаем запрос.

4. Перехватить запрос

vite имеет функцию ловушки для перехвата запросовconfigureserver, использование также очень простое:

export default function VitePlugin(): Plugin {
  return {
    name: "my-plugin",
    configureServer(server) {
      // 我们使用 server.middlewares 进行统一处理
      // https://vitejs.cn/guide/api-javascript.html#vitedevserver
      server.middlewares.use((req, res, next) => {
        // 判断是否为 /api 开头
        if (req.url.startsWith("/api")) {
          // 写一个假数据
          res.end(JSON.stringify({ data: { name: "jack", age: 19 } }));
          return;
        }
        next();
      });
    },
  };
}

6.png

Мы видим, что сеть перехватила данные и вернула фальшивые данные, которые мы написали.

5. Запустите экспресс-сервис для обработки запроса

Учитывая, что служба интерфейса должна быть отделена от сервера внутри vite, нам нужно запустить отдельную службу для обработки этих запросов, здесь мы выбираемexpressДля получения и обработки этих запросов используется специальный код:

yarn add express body-parser  # express
yarn add ts-node # 用于在 node 环境下读取 ts 文件
yarn add axios # 用于转发请求
import * as fs from "fs";

// 想要在 node 环境下直接读取文件,需要使用 ts-node 的 register
require("ts-node/register");

// 获取 apis 下的文件和函数
function getApis() {
  const files = fs
    .readdirSync(apisPath)
    .map((filePath) => path.join(apisPath, filePath));
  const apis = files
    .filter((filePath) => {
      const stat = fs.statSync(filePath);
      return stat.isFile();
    })
    .map((filePath) => {
      // 直接 require ts 文件
      const fns = require(filePath);
      const fileName = path.basename(filePath, ".ts");
      return Object.keys(fns).map((fnName) => ({
        fileName,
        fn: fns[fnName],
      }));
    });
  return apis.flat();
}
// tsconfig.json
// 如果想使用 ts-node/register 读取 ts 文件,必须将 module 改为 "commonjs",这是一个坑点
{
  "module": "commonjs",
}
import { Express } from "./node_modules/@types/express-serve-static-core/index";

// 注册路由处理函数
function registerApis(server: Express) {
  const apis = getApis();

  // 遍历 apis,注册路由及其处理函数
  apis.forEach(({ fileName, fn }) => {
    // 和前端一样的路由规则
    server.all(`/api/${fileName}/${fn.name}`, async (req, res) => {
      // 执行函数,并将结果返回
      const data = await fn(req.body);
      res.send(JSON.stringify(data));
    });
  });
}
// 启动 app
import express from "express";
const bodyParser = require("body-parser");

function appStart(): Promise<string> {
  const app = express();
  app.use(bodyParser.json());

  // 注册 apis
  registerApis(app);

  const server = http.createServer(app);

  return new Promise((resolve) => {
    // listen 的第一个参数如果为 0,则表示随机获取一个未被占用的端口
    server.listen(0, () => {
      const address = server.address();

      // 返回请求地址
      if (typeof address === "string") {
        resolve(`http://${address}`);
      } else {
        resolve(`http://127.0.0.1:${address.port}`);
      }
    });
  });
}
// 请求转发
function sendRequest(address: string, url: string, body: any, params: any) {
  return axios.post(`${address}${url}`, body, {
    params,
    headers: {
      "Content-Type": "application/json",
    },
  });
}
// 设置 middleware 拦截请求
async function middleware() {
  // 启动 app
  const address = await appStart();
  return async (req, res, next) => {
    if (req.url.startsWith("/api")) {
      // 转发请求到 app
      const response = await sendRequest(address, req.url, req.body, req.query);

      // 返回结果
      res.end(JSON.stringify(response.data));
      return;
    }
    next();
  };
}
export default function VitePlugin(): Plugin {
  return {
    // ...
    async configureServer(server) {
      // vite 内部的 server 也要注册 bodyParser
      // 用于在转发时获取 body
      server.middlewares.use(bodyParser.json());
      // 注册中间件
      server.middlewares.use(await middleware());
    },
  };
}

7.gif

Как видно из изображения выше, мы уже можем правильно отправлять запросы GET и POST.

6. Окончательный код плагина

import * as path from "path";
import * as fs from "fs";
import * as http from "http";
import express from "express";
import axios from "axios";
import { Plugin } from "vite";
import { Express } from "./node_modules/@types/express-serve-static-core/index";

// 想要在 node 环境下直接读取文件,需要使用 ts-node 的 register
require("ts-node/register");

const bodyParser = require("body-parser");
const apisPath = path.join(__dirname, "./src/apis");

// 发起请求的模板
const requestTemp = (
  fileName: string,
  fn: string
) => `export function ${fn}(data) {
    const isGet = !data
    return fetch("/api/${fileName}/${fn}", {
        method: isGet ? "GET" : "POST",
        body: isGet ? undefined : JSON.stringify(data),
        headers: {
          'Content-Type': 'application/json'
        },
    }).then((res) => res.json());
}`;

// 获取 apis 下的文件和函数
function getApis() {
  const files = fs
    .readdirSync(apisPath)
    .map((filePath) => path.join(apisPath, filePath));
  const apis = files
    .filter((filePath) => {
      const stat = fs.statSync(filePath);
      return stat.isFile();
    })
    .map((filePath) => {
      const fns = require(filePath);
      const fileName = path.basename(filePath, ".ts");
      return Object.keys(fns).map((fnName) => ({
        fileName,
        fn: fns[fnName],
      }));
    });
  return apis.flat();
}

// 注册路由处理函数
function registerApis(server: Express) {
  const apis = getApis();

  // 遍历 apis,注册路由及其处理函数
  apis.forEach(({ fileName, fn }) => {
    server.all(`/api/${fileName}/${fn.name}`, async (req, res) => {
      const data = await fn(req.body);
      res.send(JSON.stringify(data));
    });
  });
}

// 启动 app
function appStart(): Promise<string> {
  const app = express();
  app.use(bodyParser.json());
  registerApis(app);
  const server = http.createServer(app);

  return new Promise((resolve) => {
    // listen 的第一个参数如果为 0,则表示随机获取一个未被占用的端口
    server.listen(0, () => {
      const address = server.address();

      if (typeof address === "string") {
        resolve(`http://${address}`);
      } else {
        resolve(`http://127.0.0.1:${address.port}`);
      }
    });
  });
}

// 请求转发
function sendRequest(address: string, url: string, body: any, params: any) {
  return axios.post(`${address}${url}`, body, {
    params,
    headers: {
      "Content-Type": "application/json",
    },
  });
}

// 设置 middleware 拦截请求
async function middleware() {
  // 启动 app
  const address = await appStart();
  return async (req, res, next) => {
    if (req.url.startsWith("/api")) {
      // 转发请求到 app
      const response = await sendRequest(address, req.url, req.body, req.query);
      // 返回结果
      res.end(JSON.stringify(response.data));
      return;
    }
    next();
  };
}

// 将函数转为请求
function transformRequest(src: string, id: string) {
  if (id.startsWith(apisPath)) {
    const fileName = path.basename(id, ".ts");
    const fnNames = [...src.matchAll(/async function (\w+)/g)].map(
      (item) => item[1]
    );
    return {
      code: fnNames.map((fn) => requestTemp(fileName, fn)).join("\n"),
      map: null,
    };
  }
}

export default function VitePlugin(): Plugin {
  return {
    name: "my-plugin",
    transform: transformRequest,
    async configureServer(server) {
      // vite 内部的 server 也要注册 bodyParser
      // 用于在转发时获取 body
      server.middlewares.use(bodyParser.json());
      server.middlewares.use(await middleware());
    },
  };
}

Общий код см. на GitHub:GitHub.com/dream2023/…

постскриптум

В целом интеграция фронтенда и бэкенда делает обращение к интерфейсу очень простым, но при этом возникают такие проблемы, как конструирование, деплоймент и чрезмерный размер проекта. думать?