openapi-ts-request

openapi-ts-request

介绍

- 根据openapi生成ts的前端api接口定义代码
- 由于openapi2ts不支持对请求路径的过滤,所以使用了 openapi-ts-request库

官网

- https://github.com/openapi-ui/openapi-ts-request?tab=readme-ov-file#%E5%8F%82%E6%95%B0

安装

- pnpm i openapi-ts-request -D
- 在项目根目录新建 openapi-ts-request.config.ts
export default defineConfig(
//可以配置多组
[{ 
    //地址www为group名字,需要在springboot项目application.yaml中配置
    schemaPath: 'http://localhost:8123/v3/api-docs/www',
    //存储目录
    serversPath: './apis',
    //生成哪些路径的方法
    includePaths: [
        '/www/**'
        ],
    //网络访问库的名字
    requestLibPath: '~/request',
    //定制方法的名字
    hook: {
        customFunctionName: function(data: APIDataType): string{
        return data.operationId || ''
        }
    }
}])

在package.json 中配置脚本

scripts {
  "openapi": "openapi-ts"
}

springboot项目application.yaml中配置openapi分组

springdoc:
  group-configs:
    - group: 'sso'
      packages-to-scan:
        - cn.com.rstone.api.controller.sso
    - group: 'system'
      packages-to-scan:
        - cn.com.rstone.api.controller.system
    - group: 'web'
      packages-to-scan:
        - cn.com.rstone.api.controller.web
    - group: 'www'
      packages-to-scan:
        - cn.com.rstone.api.controller.www
    - group: 'wxmp'
      packages-to-scan:
        - cn.com.rstone.api.controller.wxmp

⚠️解决方法名称准确性

  • 在openapi-ts-request.config.ts 配置文件中做如下定制

export default defineConfig([
{
   hook: {
        customFunctionName: function(data: any): string{
          return data.operationId || ''
        },
}

⚠️解决在springboot controller方法接收form提交的文件和对象同时存在的问题

  • 此问题解决起来比较麻烦,可以通过修改模板来解决,但由于代码量比较大这里通过新建一个文件夹存放文件,然后引入的方法使用

  • 安装模板解析库

pnpm install -D nunjucks 
  • 新建目录结构及文件

#目录及文件结构
- openapi-ts-custome
  - serviceController.ts
  - serviceController.njk
//serviceController.ts
import nunjucks from 'nunjucks';
import { existsSync, readFileSync } from 'fs';
import { join } from 'path';

//@ts-ignore
export const serviceController =  (apis: any, context: any): any => {
 
    // 设置输出不转义
    const env = nunjucks.configure({
        autoescape: false,
    });

    const template =   readFileSync( join(__dirname,  './serviceController.njk'),'utf8');

    const r =  nunjucks.renderString(template, { 
            disableTypeCheck: false,
            api: apis,
            genType: 'ts',
            namespace: 'API',
            requestOptionsType: '{ [key: string]: unknown }' 
        });
    return r;
}
//serviceController.njk


  {% if api.customTemplate %}
    {{ api.data }}
  {% else %}
    /** {{ api.desc if api.desc else '此处后端没有提供注释' }} {{ api.method | upper }} {{ api.pathInComment | safe }}{{ ' ' if api.apifoxRunLink else '' }}{{ api.apifoxRunLink }} */
    export function {{ api.functionName }}({
      {%- if api.params and api.hasParams %}
        params
        {%- if api.hasParams -%}
          {{ "," if api.body or api.file }}
        {%- endif -%}
      {%- endif -%}
      {%- if api.body -%}
        body
        {{ "," if api.file }}
      {%- endif %}
      {%- if api.file -%}
        {%- for file in api.file -%}
          {{ file.title | safe }}
          {{ "," if not loop.last }}
        {%- endfor -%}
      {%- endif -%}
      {{ "," if api.body or api.hasParams or api.file }}
      options
    }
    {%- if genType === "ts" -%}
      : {
        {%- if api.params and api.hasParams %}
          // 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
          params: {{ namespace }}.{{ api.typeName }}
          {# header 入参 -#}
          {% if api.params.header -%}
            & { // header
            {% for param in api.params.header -%}
              {% if param.description -%}
                /** {{ param.description }} */
              {% endif -%}
              '{{ param.name }}'
              {{- "?" if not param.required }}
              {{- (": " + param.type + ";") | safe }}
            {% endfor -%}
            }
          {%- endif -%}
          {%- if api.hasParams -%}
            {{ ";" if api.body or api.file }}
          {%- endif -%}
        {%- endif -%}

        {%- if api.body -%}
          body: {{ api.body.type }}
          {{ ";" if api.file }}
        {%- endif %}

        {%- if api.file -%}
          {%- for file in api.file -%}
            {{ file.title | safe }}
            {{- "?" if not api.file.required -}}
            : globalThis.File {{ "[]" if file.multiple }}
            {{ ";" if not loop.last }}
          {%- endfor -%}
        {%- endif -%}
        {{ ";" if api.body or api.hasParams or api.file }}
        options?: {{ requestOptionsType }}
      }
    {%- endif -%}
    ) 
    {
      {% if api.params and api.params.path %}
        const { {% for param in api.params.path %}'{{ param.name }}': {{ param.alias }}, {% endfor %}
        {% if api.params.path -%}
          ...queryParams
        {% endif -%}
        } = params;
      {% endif %}
      {%- if api.hasFormData -%}
        const formData = new FormData();

        {% if api.file -%}
          {% for file in api.file %}
            if({{ file.title | safe }}) {
            {% if file.multiple %}
              {{ file.title | safe }}.forEach(f => formData.append('{{ file.title | safe }}', f || ''));
            {% else %}
              formData.append('{{ file.title | safe }}', {{ file.title | safe }})
            {% endif %}
            }
          {% endfor %}
        {%- endif -%}

        {% if api.body %}
          Object.keys(body).forEach(ele => {
          {% if genType === "ts" %}
            const item = (body as { [key: string]: any })[ele];
          {% else %}
            const item = body[ele];
          {% endif %}
            if (item !== undefined && item !== null) {
              {% if genType === "ts" %}
                if (typeof item === 'object' && !(item instanceof globalThis.File)) {
                  if (item instanceof Array) {
                    item.forEach((f) => formData.append(ele, f || ''));
                  } else {
                    {# 修正开始: 在 mediaType: "multipart/form-data" 时没有做 new Bolod 的问题 -#}
                    {% if api.hasHeader and api.body.mediaType and api.body.mediaType == 'multipart/form-data' %}
                      formData.append(ele, new Blob([JSON.stringify(item)], {type: 'application/json'})) ;
                    {% else %}
                      formData.append(ele, JSON.stringify(item));
                    {% endif %}
                    {# 修正结束 -#}
                  }
                } else {
                  formData.append(ele, item);
                }
              {% else %}
                formData.append(ele, typeof item === 'object' ? JSON.stringify(item) : item);
              {% endif %}
            }
          });
        {% endif %}
      {% endif %}

      {% if api.hasPathVariables or api.hasApiPrefix -%}
        return request{{ ("<" + api.response.type + ">") | safe if genType === "ts" }}(`{{ api.path | safe }}`, {
      {% else -%}
        return request{{ ("<" + api.response.type + ">") | safe if genType === "ts" }}('{{ api.path }}', {
      {% endif -%}
        method: '{{ api.method | upper }}',
        {%- if api.response.responseType %}
        responseType: '{{ api.response.responseType }}',
        {%- endif %}
        {%- if api.hasHeader and api.body.mediaType %}
          headers: {
            {%- if api.body.mediaType %}
            'Content-Type': '{{ api.body.mediaType | safe }}',
            {%- endif %}
          },
        {%- endif %}
        {%- if api.params and api.hasParams %}
          params: {
            {%- for query in api.params.query %}
              {% if query.schema.default -%}
                // {{ query.name | safe }} has a default value: {{ query.schema.default | safe }}
                '{{ query.name | safe }}': '{{query.schema.default | safe}}',
              {%- endif -%}
            {%- endfor -%}
            ...{{ 'queryParams' if api.params and api.params.path else 'params' }},
            {%- for query in api.params.query %}
              {%- if query.isComplexType %}
                '{{ query.name | safe }}': undefined,
                ...{{ 'queryParams' if api.params and api.params.path else 'params' }}['{{ query.name | safe }}'],
              {%- endif %}
            {%- endfor -%}
          },
        {%- endif %}
        {%- if api.hasFormData %}
          data: formData,
        {%- elseif api.body %}
          data: body,
        {%- endif %}
        ...(options || {{ api.options | dump }}),
      });
    }
  {% endif %}
  • 在openapi-ts-request.config.ts 配置文件中做如下定制「⚠️如果有多个组,则每个组都要配置」

//引入方法
import {serviceController} from './openapi-ts-custome/serviceController'
export default defineConfig([
{
   hook: {
        customTemplates: {
          serviceController
        }
}])

# openapi-ts-request

## 介绍

```
- 根据openapi生成ts的前端api接口定义代码
- 由于openapi2ts不支持对请求路径的过滤,所以使用了 openapi-ts-request库
```

## 官网

```
- https://github.com/openapi-ui/openapi-ts-request?tab=readme-ov-file#%E5%8F%82%E6%95%B0
```

## 安装

```
- pnpm i openapi-ts-request -D
- 在项目根目录新建 openapi-ts-request.config.ts
export default defineConfig(
//可以配置多组
[{ 
    //地址www为group名字,需要在springboot项目application.yaml中配置
    schemaPath: 'http://localhost:8123/v3/api-docs/www',
    //存储目录
    serversPath: './apis',
    //生成哪些路径的方法
    includePaths: [
        '/www/**'
        ],
    //网络访问库的名字
    requestLibPath: '~/request',
    //定制方法的名字
    hook: {
        customFunctionName: function(data: APIDataType): string{
        return data.operationId || ''
        }
    }
}])
```

## 在package.json 中配置脚本

```
scripts {
  "openapi": "openapi-ts"
}
```

## springboot项目application.yaml中配置openapi分组

```yml
springdoc:
  group-configs:
    - group: 'sso'
      packages-to-scan:
        - cn.com.rstone.api.controller.sso
    - group: 'system'
      packages-to-scan:
        - cn.com.rstone.api.controller.system
    - group: 'web'
      packages-to-scan:
        - cn.com.rstone.api.controller.web
    - group: 'www'
      packages-to-scan:
        - cn.com.rstone.api.controller.www
    - group: 'wxmp'
      packages-to-scan:
        - cn.com.rstone.api.controller.wxmp
```

## ⚠️解决方法名称准确性

- 在openapi-ts-request.config.ts 配置文件中做如下定制

```ts
export default defineConfig([
{
   hook: {
        customFunctionName: function(data: any): string{
          return data.operationId || ''
        },
}
```

## ⚠️解决在springboot controller方法接收form提交的文件和对象同时存在的问题

- 此问题解决起来比较麻烦,可以通过修改模板来解决,但由于代码量比较大这里通过新建一个文件夹存放文件,然后引入的方法使用
- 安装模板解析库

```bash
pnpm install -D nunjucks 
```

- 新建目录结构及文件

```
#目录及文件结构
- openapi-ts-custome
  - serviceController.ts
  - serviceController.njk
```

```ts
//serviceController.ts
import nunjucks from 'nunjucks';
import { existsSync, readFileSync } from 'fs';
import { join } from 'path';

//@ts-ignore
export const serviceController =  (apis: any, context: any): any => {
 
    // 设置输出不转义
    const env = nunjucks.configure({
        autoescape: false,
    });

    const template =   readFileSync( join(__dirname,  './serviceController.njk'),'utf8');

    const r =  nunjucks.renderString(template, { 
            disableTypeCheck: false,
            api: apis,
            genType: 'ts',
            namespace: 'API',
            requestOptionsType: '{ [key: string]: unknown }' 
        });
    return r;
}
```

```
//serviceController.njk


  {% if api.customTemplate %}
    {{ api.data }}
  {% else %}
    /** {{ api.desc if api.desc else '此处后端没有提供注释' }} {{ api.method | upper }} {{ api.pathInComment | safe }}{{ ' ' if api.apifoxRunLink else '' }}{{ api.apifoxRunLink }} */
    export function {{ api.functionName }}({
      {%- if api.params and api.hasParams %}
        params
        {%- if api.hasParams -%}
          {{ "," if api.body or api.file }}
        {%- endif -%}
      {%- endif -%}
      {%- if api.body -%}
        body
        {{ "," if api.file }}
      {%- endif %}
      {%- if api.file -%}
        {%- for file in api.file -%}
          {{ file.title | safe }}
          {{ "," if not loop.last }}
        {%- endfor -%}
      {%- endif -%}
      {{ "," if api.body or api.hasParams or api.file }}
      options
    }
    {%- if genType === "ts" -%}
      : {
        {%- if api.params and api.hasParams %}
          // 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
          params: {{ namespace }}.{{ api.typeName }}
          {# header 入参 -#}
          {% if api.params.header -%}
            & { // header
            {% for param in api.params.header -%}
              {% if param.description -%}
                /** {{ param.description }} */
              {% endif -%}
              '{{ param.name }}'
              {{- "?" if not param.required }}
              {{- (": " + param.type + ";") | safe }}
            {% endfor -%}
            }
          {%- endif -%}
          {%- if api.hasParams -%}
            {{ ";" if api.body or api.file }}
          {%- endif -%}
        {%- endif -%}

        {%- if api.body -%}
          body: {{ api.body.type }}
          {{ ";" if api.file }}
        {%- endif %}

        {%- if api.file -%}
          {%- for file in api.file -%}
            {{ file.title | safe }}
            {{- "?" if not api.file.required -}}
            : globalThis.File {{ "[]" if file.multiple }}
            {{ ";" if not loop.last }}
          {%- endfor -%}
        {%- endif -%}
        {{ ";" if api.body or api.hasParams or api.file }}
        options?: {{ requestOptionsType }}
      }
    {%- endif -%}
    ) 
    {
      {% if api.params and api.params.path %}
        const { {% for param in api.params.path %}'{{ param.name }}': {{ param.alias }}, {% endfor %}
        {% if api.params.path -%}
          ...queryParams
        {% endif -%}
        } = params;
      {% endif %}
      {%- if api.hasFormData -%}
        const formData = new FormData();

        {% if api.file -%}
          {% for file in api.file %}
            if({{ file.title | safe }}) {
            {% if file.multiple %}
              {{ file.title | safe }}.forEach(f => formData.append('{{ file.title | safe }}', f || ''));
            {% else %}
              formData.append('{{ file.title | safe }}', {{ file.title | safe }})
            {% endif %}
            }
          {% endfor %}
        {%- endif -%}

        {% if api.body %}
          Object.keys(body).forEach(ele => {
          {% if genType === "ts" %}
            const item = (body as { [key: string]: any })[ele];
          {% else %}
            const item = body[ele];
          {% endif %}
            if (item !== undefined && item !== null) {
              {% if genType === "ts" %}
                if (typeof item === 'object' && !(item instanceof globalThis.File)) {
                  if (item instanceof Array) {
                    item.forEach((f) => formData.append(ele, f || ''));
                  } else {
                    {# 修正开始: 在 mediaType: "multipart/form-data" 时没有做 new Bolod 的问题 -#}
                    {% if api.hasHeader and api.body.mediaType and api.body.mediaType == 'multipart/form-data' %}
                      formData.append(ele, new Blob([JSON.stringify(item)], {type: 'application/json'})) ;
                    {% else %}
                      formData.append(ele, JSON.stringify(item));
                    {% endif %}
                    {# 修正结束 -#}
                  }
                } else {
                  formData.append(ele, item);
                }
              {% else %}
                formData.append(ele, typeof item === 'object' ? JSON.stringify(item) : item);
              {% endif %}
            }
          });
        {% endif %}
      {% endif %}

      {% if api.hasPathVariables or api.hasApiPrefix -%}
        return request{{ ("<" + api.response.type + ">") | safe if genType === "ts" }}(`{{ api.path | safe }}`, {
      {% else -%}
        return request{{ ("<" + api.response.type + ">") | safe if genType === "ts" }}('{{ api.path }}', {
      {% endif -%}
        method: '{{ api.method | upper }}',
        {%- if api.response.responseType %}
        responseType: '{{ api.response.responseType }}',
        {%- endif %}
        {%- if api.hasHeader and api.body.mediaType %}
          headers: {
            {%- if api.body.mediaType %}
            'Content-Type': '{{ api.body.mediaType | safe }}',
            {%- endif %}
          },
        {%- endif %}
        {%- if api.params and api.hasParams %}
          params: {
            {%- for query in api.params.query %}
              {% if query.schema.default -%}
                // {{ query.name | safe }} has a default value: {{ query.schema.default | safe }}
                '{{ query.name | safe }}': '{{query.schema.default | safe}}',
              {%- endif -%}
            {%- endfor -%}
            ...{{ 'queryParams' if api.params and api.params.path else 'params' }},
            {%- for query in api.params.query %}
              {%- if query.isComplexType %}
                '{{ query.name | safe }}': undefined,
                ...{{ 'queryParams' if api.params and api.params.path else 'params' }}['{{ query.name | safe }}'],
              {%- endif %}
            {%- endfor -%}
          },
        {%- endif %}
        {%- if api.hasFormData %}
          data: formData,
        {%- elseif api.body %}
          data: body,
        {%- endif %}
        ...(options || {{ api.options | dump }}),
      });
    }
  {% endif %}

```

- 在openapi-ts-request.config.ts 配置文件中做如下定制「⚠️如果有多个组,则每个组都要配置」

```ts
//引入方法
import {serviceController} from './openapi-ts-custome/serviceController'
export default defineConfig([
{
   hook: {
        customTemplates: {
          serviceController
        }
}])
```