NutUI React 如何向类型文件添加注释

NutUI 是一款京东风格的移动端组件库。NutUI 目前支持 Vue 和 React技术栈,支持Taro多端适配。

期待您早日成为我们共建大军中的一员!

微信群:hanyuxinting(暗号:NutUI-React)

官网GitHub: 点击进入

欢迎共建、使用!

背景

在 NutUI-React 2.x 版本的构建产物中,增加了更完善的类型,不过组件 Props 相关的类型字段并未增加注释,开发者没有办法借助 IDE 的提示功能,在不脱离 IDE 的环境下查看每个 Props 的功能描述。不过在 NutUI 的文档中已经有详细的 Props 描述,是否可以将文档中的 Props 描述插入到产物中去?

基于上面的问题,我们需要分析已有文档中的 Props 描述,将描述注入到 Props 的类型文件中。下面是 ActionSheet 组件 Props 处理的前后对照。

未加注释版本:

ts 复制代码
export interface ActionSheetProps extends BasicComponent {
    visible: boolean
    title: ReactNode
    description: ReactNode
    options: ItemType<string | boolean>[]
    optionKey: ItemType<string>
    cancelText: ReactNode
    onCancel: () => void
    onSelect: (item: ItemType<string | boolean>, index: number) => void
}

注入注释版本:

ts 复制代码
export interface ActionSheetProps extends BasicComponent {
    /**
     * 遮罩层可见
     * @default false
     */
    visible: boolean
    /**
     * 设置列表面板标题
     * @default -
     */
    title: ReactNode
    /**
     * 设置列表面板副标题/描述
     * @default -
     */
    description: ReactNode
    /**
     * 列表项
     * @default []
     */
    options: ItemType<string | boolean>[]
    /**
     * 列表项的自定义设置
     * @default -
     */
    optionKey: ItemType<string>
    /**
     * 取消文案
     * @default 取消
     */
    cancelText: ReactNode
    /**
     * 点击取消文案时触发
     * @default -
     */
    onCancel: () => void
    /**
     * 选择之后触发
     * @default -
     */
    onSelect: (item: ItemType<string | boolean>, index: number) => void
}

实现思路

NutUI-React 仓库中每个组件有各自的 doc.md 文件,在 doc.md 文件中,Props 相关的内容都是按照如下规范编写的:

markdown 复制代码
## 组件名称

### Props

| 属性 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |

因为组件文档遵循此规范,所以可以很方便的通过脚本进行处理。首先我们要使用 markdown-itdoc.md 文档进行解析。由此得到 markdown 的 token。通过对 token 进行迭代处理,提取出 Props 相关的表格,并将 table 相关的 token 转换为二维数组。

在获取到每个组件的 Props 数据后,需要对 NutUI-React 构建产物中的 typings 进行代码转换。代码转换可以通过 jscodeshift 来处理。

上面这一过程主要描述了单个组件的处理情况,然而 NutUI-React 有 70 多个组件都需要进行相关的处理。所以我们要获取到所有的组件,应用上述的处理过程。

实践细节

markdown-it

markdown-it 是一个流行的基于 JavaScript 的 Markdown 解析器。它具有灵活的插件系统,可扩展性和高性能。它支持标准化的 Markdown 格式,同时还支持 GFM(GitHub Flavored Markdown)和 CommonMark。它的输入和输出都是纯文本字符串,可以轻松地集成到任何 Web 应用程序中。markdown-it 还支持同步和异步模式的解析,可以用于服务器端和客户端的应用程序。

首先在项目中安装 markdown-it

shell 复制代码
npm install markdown-it --save-dev

之后在脚本中引入它,const markdown = require('markdown-it')(),并解析 doc.md 的内容 const tokens = markdown.parse(documentData) 得到 tokens。 相关 API 可参考Markdown 文档 为了更方便的查看 tokens,我们可以通过 markdown-it.github.io/ 中提供的 debug 模式,观察 tokens。

例如 ActionSheet Props 的片段,

markdown 复制代码
## 组件名称

### Props

| 属性 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| visible | 遮罩层可见 | `boolean` | `false` |

解析后的 tokens 如下:

json 复制代码
[
  {
    "type": "heading_open",
    "tag": "h2",
    "attrs": null,
    "map": [
      0,
      1
    ],
    "nesting": 1,
    "level": 0,
    "children": null,
    "content": "",
    "markup": "##",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "inline",
    "tag": "",
    "attrs": null,
    "map": [
      0,
      1
    ],
    "nesting": 0,
    "level": 1,
    "children": [
      {
        "type": "text",
        "tag": "",
        "attrs": null,
        "map": null,
        "nesting": 0,
        "level": 0,
        "children": null,
        "content": "组件名称",
        "markup": "",
        "info": "",
        "meta": null,
        "block": false,
        "hidden": false
      }
    ],
    "content": "组件名称",
    "markup": "",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "heading_close",
    "tag": "h2",
    "attrs": null,
    "map": null,
    "nesting": -1,
    "level": 0,
    "children": null,
    "content": "",
    "markup": "##",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "heading_open",
    "tag": "h3",
    "attrs": null,
    "map": [
      1,
      2
    ],
    "nesting": 1,
    "level": 0,
    "children": null,
    "content": "",
    "markup": "###",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "inline",
    "tag": "",
    "attrs": null,
    "map": [
      1,
      2
    ],
    "nesting": 0,
    "level": 1,
    "children": [
      {
        "type": "text",
        "tag": "",
        "attrs": null,
        "map": null,
        "nesting": 0,
        "level": 0,
        "children": null,
        "content": "Props",
        "markup": "",
        "info": "",
        "meta": null,
        "block": false,
        "hidden": false
      }
    ],
    "content": "Props",
    "markup": "",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "heading_close",
    "tag": "h3",
    "attrs": null,
    "map": null,
    "nesting": -1,
    "level": 0,
    "children": null,
    "content": "",
    "markup": "###",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "table_open",
    "tag": "table",
    "attrs": null,
    "map": [
      2,
      5
    ],
    "nesting": 1,
    "level": 0,
    "children": null,
    "content": "",
    "markup": "",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "thead_open",
    "tag": "thead",
    "attrs": null,
    "map": [
      2,
      3
    ],
    "nesting": 1,
    "level": 1,
    "children": null,
    "content": "",
    "markup": "",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "tr_open",
    "tag": "tr",
    "attrs": null,
    "map": [
      2,
      3
    ],
    "nesting": 1,
    "level": 2,
    "children": null,
    "content": "",
    "markup": "",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "th_open",
    "tag": "th",
    "attrs": null,
    "map": null,
    "nesting": 1,
    "level": 3,
    "children": null,
    "content": "",
    "markup": "",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "inline",
    "tag": "",
    "attrs": null,
    "map": null,
    "nesting": 0,
    "level": 4,
    "children": [
      {
        "type": "text",
        "tag": "",
        "attrs": null,
        "map": null,
        "nesting": 0,
        "level": 0,
        "children": null,
        "content": "属性",
        "markup": "",
        "info": "",
        "meta": null,
        "block": false,
        "hidden": false
      }
    ],
    "content": "属性",
    "markup": "",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "th_close",
    "tag": "th",
    "attrs": null,
    "map": null,
    "nesting": -1,
    "level": 3,
    "children": null,
    "content": "",
    "markup": "",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "th_open",
    "tag": "th",
    "attrs": null,
    "map": null,
    "nesting": 1,
    "level": 3,
    "children": null,
    "content": "",
    "markup": "",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "inline",
    "tag": "",
    "attrs": null,
    "map": null,
    "nesting": 0,
    "level": 4,
    "children": [
      {
        "type": "text",
        "tag": "",
        "attrs": null,
        "map": null,
        "nesting": 0,
        "level": 0,
        "children": null,
        "content": "说明",
        "markup": "",
        "info": "",
        "meta": null,
        "block": false,
        "hidden": false
      }
    ],
    "content": "说明",
    "markup": "",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "th_close",
    "tag": "th",
    "attrs": null,
    "map": null,
    "nesting": -1,
    "level": 3,
    "children": null,
    "content": "",
    "markup": "",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "th_open",
    "tag": "th",
    "attrs": null,
    "map": null,
    "nesting": 1,
    "level": 3,
    "children": null,
    "content": "",
    "markup": "",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "inline",
    "tag": "",
    "attrs": null,
    "map": null,
    "nesting": 0,
    "level": 4,
    "children": [
      {
        "type": "text",
        "tag": "",
        "attrs": null,
        "map": null,
        "nesting": 0,
        "level": 0,
        "children": null,
        "content": "类型",
        "markup": "",
        "info": "",
        "meta": null,
        "block": false,
        "hidden": false
      }
    ],
    "content": "类型",
    "markup": "",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "th_close",
    "tag": "th",
    "attrs": null,
    "map": null,
    "nesting": -1,
    "level": 3,
    "children": null,
    "content": "",
    "markup": "",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "th_open",
    "tag": "th",
    "attrs": null,
    "map": null,
    "nesting": 1,
    "level": 3,
    "children": null,
    "content": "",
    "markup": "",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "inline",
    "tag": "",
    "attrs": null,
    "map": null,
    "nesting": 0,
    "level": 4,
    "children": [
      {
        "type": "text",
        "tag": "",
        "attrs": null,
        "map": null,
        "nesting": 0,
        "level": 0,
        "children": null,
        "content": "默认值",
        "markup": "",
        "info": "",
        "meta": null,
        "block": false,
        "hidden": false
      }
    ],
    "content": "默认值",
    "markup": "",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "th_close",
    "tag": "th",
    "attrs": null,
    "map": null,
    "nesting": -1,
    "level": 3,
    "children": null,
    "content": "",
    "markup": "",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "tr_close",
    "tag": "tr",
    "attrs": null,
    "map": null,
    "nesting": -1,
    "level": 2,
    "children": null,
    "content": "",
    "markup": "",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "thead_close",
    "tag": "thead",
    "attrs": null,
    "map": null,
    "nesting": -1,
    "level": 1,
    "children": null,
    "content": "",
    "markup": "",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "tbody_open",
    "tag": "tbody",
    "attrs": null,
    "map": [
      4,
      5
    ],
    "nesting": 1,
    "level": 1,
    "children": null,
    "content": "",
    "markup": "",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "tr_open",
    "tag": "tr",
    "attrs": null,
    "map": [
      4,
      5
    ],
    "nesting": 1,
    "level": 2,
    "children": null,
    "content": "",
    "markup": "",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "td_open",
    "tag": "td",
    "attrs": null,
    "map": null,
    "nesting": 1,
    "level": 3,
    "children": null,
    "content": "",
    "markup": "",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "inline",
    "tag": "",
    "attrs": null,
    "map": null,
    "nesting": 0,
    "level": 4,
    "children": [
      {
        "type": "text",
        "tag": "",
        "attrs": null,
        "map": null,
        "nesting": 0,
        "level": 0,
        "children": null,
        "content": "visible",
        "markup": "",
        "info": "",
        "meta": null,
        "block": false,
        "hidden": false
      }
    ],
    "content": "visible",
    "markup": "",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "td_close",
    "tag": "td",
    "attrs": null,
    "map": null,
    "nesting": -1,
    "level": 3,
    "children": null,
    "content": "",
    "markup": "",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "td_open",
    "tag": "td",
    "attrs": null,
    "map": null,
    "nesting": 1,
    "level": 3,
    "children": null,
    "content": "",
    "markup": "",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "inline",
    "tag": "",
    "attrs": null,
    "map": null,
    "nesting": 0,
    "level": 4,
    "children": [
      {
        "type": "text",
        "tag": "",
        "attrs": null,
        "map": null,
        "nesting": 0,
        "level": 0,
        "children": null,
        "content": "遮罩层可见",
        "markup": "",
        "info": "",
        "meta": null,
        "block": false,
        "hidden": false
      }
    ],
    "content": "遮罩层可见",
    "markup": "",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "td_close",
    "tag": "td",
    "attrs": null,
    "map": null,
    "nesting": -1,
    "level": 3,
    "children": null,
    "content": "",
    "markup": "",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "td_open",
    "tag": "td",
    "attrs": null,
    "map": null,
    "nesting": 1,
    "level": 3,
    "children": null,
    "content": "",
    "markup": "",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "inline",
    "tag": "",
    "attrs": null,
    "map": null,
    "nesting": 0,
    "level": 4,
    "children": [
      {
        "type": "code_inline",
        "tag": "code",
        "attrs": null,
        "map": null,
        "nesting": 0,
        "level": 0,
        "children": null,
        "content": "boolean",
        "markup": "`",
        "info": "",
        "meta": null,
        "block": false,
        "hidden": false
      }
    ],
    "content": "`boolean`",
    "markup": "",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "td_close",
    "tag": "td",
    "attrs": null,
    "map": null,
    "nesting": -1,
    "level": 3,
    "children": null,
    "content": "",
    "markup": "",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "td_open",
    "tag": "td",
    "attrs": null,
    "map": null,
    "nesting": 1,
    "level": 3,
    "children": null,
    "content": "",
    "markup": "",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "inline",
    "tag": "",
    "attrs": null,
    "map": null,
    "nesting": 0,
    "level": 4,
    "children": [
      {
        "type": "code_inline",
        "tag": "code",
        "attrs": null,
        "map": null,
        "nesting": 0,
        "level": 0,
        "children": null,
        "content": "false",
        "markup": "`",
        "info": "",
        "meta": null,
        "block": false,
        "hidden": false
      }
    ],
    "content": "`false`",
    "markup": "",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "td_close",
    "tag": "td",
    "attrs": null,
    "map": null,
    "nesting": -1,
    "level": 3,
    "children": null,
    "content": "",
    "markup": "",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "tr_close",
    "tag": "tr",
    "attrs": null,
    "map": null,
    "nesting": -1,
    "level": 2,
    "children": null,
    "content": "",
    "markup": "",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "tbody_close",
    "tag": "tbody",
    "attrs": null,
    "map": null,
    "nesting": -1,
    "level": 1,
    "children": null,
    "content": "",
    "markup": "",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  },
  {
    "type": "table_close",
    "tag": "table",
    "attrs": null,
    "map": null,
    "nesting": -1,
    "level": 0,
    "children": null,
    "content": "",
    "markup": "",
    "info": "",
    "meta": null,
    "block": true,
    "hidden": false
  }
]

可以看出 markfown 文档解析后的 tokens 是一个数组。通过迭代处理数组,可以很容易的生成 HTML 标签。针对 tokens 这样的特征,可以采用识别 token 的 type 进行处理(markdown 每个 token 都带有 type、tag、content 属性)。

我们可以根据 NutUI-React 文档的规范,对 token 进行解析。解析的方式主要是识别特定的 token,向前读取 1 个 token,进行匹配,之后开启 table 的数据收集。主要代码逻辑如下:

js 复制代码
function extractPropsTable(doc) {
    const MarkdownIt = require('markdown-it')()
    let sources = MarkdownIt.parse(doc, {})
    const tables = []
    sources.forEach((token, index) => {
        // if 语句中的代码匹配的 markdown 结构如下:
        // ## 组件名称
        // ### Props 
        if (
            token.type == 'heading_open' &&
            token.tag == 'h3' &&
            // 查看下一个 token 的类型,是不是能匹配到 Props
            sources[index + 1].type == 'inline' &&
            sources[index + 1].content === 'Props'
        ) {
            // index 位置的 token 向后加 3 个,正好是 table_open,然后循环读到 table_close
            let startIndex = index + 3
            while (startIndex < sources.length) {
                tables.push(sources[startIndex])
                if (sources[startIndex].type == 'table_close') {
                    startIndex = null
                    break
                }
                startIndex = startIndex + 1
            }
        }
    })
    return tables
}

通过上面的 extractPropsTable 方法,可以提取出 table 相关的 token。然后在将这些 token 转换为 JSON 格式。

例如 ActionSheet 组件 Props 表格对应的 JSON 数据:

json 复制代码
[
  ["visible", "遮罩层可见", "`boolean`" , "`false`"]
]

接下来要分析 typescript 生成的 .d.ts 文件了。在这里仍然通过 ActionSheet 组件举例。ActionSheet 组件的 .d.ts 文件路径:dist/types/packages/actionsheet/actionsheet.d.ts。文件待处理部分内容如下:

ts 复制代码
import React, { FunctionComponent, ReactNode } from 'react';
import { BasicComponent } from '../../utils/typings';
export type ItemType<T> = {
    [key: string]: T;
};
export interface ActionSheetProps extends BasicComponent {
    visible: boolean
    title: ReactNode
    description: ReactNode
    options: ItemType<string | boolean>[]
    optionKey: ItemType<string>
    cancelText: ReactNode
    onCancel: () => void
    onSelect: (item: ItemType<string | boolean>, index: number) => void
}
export declare const ActionSheet: FunctionComponent<Partial<ActionSheetProps> & Omit<React.HTMLAttributes<HTMLDivElement>, 'title' | 'onSelect'>>;

面对上面这种代码,通过正则匹配替换出错的机率比较大,而且正则写起来也不容易。通过 AST 的方式进行处理更合理,更加稳定。

jscodeshift

jscodeshift 是一个用于编写 JavaScript 代码转换的工具,它基于 AST。通过使用 jscodeshift,可以是代码重构更容易和安全,它支持自定义转换规则,可以将重复的代码转换为可重用的组件,可以自动化执行代码重构,还可以帮助升级代码库。例如:react-codemod 是基于 jscodeshift 开发的帮助开发者更新 React API 的工具。

首先在项目中安装 jscodeshift

shell 复制代码
npm install jscodeshift --dev-save

在脚本中通过 const j = require('jscodeshift') 引入 jscodeshift。然后将读取的代码文本传给 jscodeshift。

js 复制代码
const j = require('jscodeshift')
const coverted = transform({sorce}, {jscodeshift: j})

在上面的代码中,出现了 transform 调用。从 jscodeshift 的 README.md 中,也可以看到 Transform module 的介绍:" transform 是具有特定格式的一个导出函数"。

接下来我们需要告诉 jscodeshift 采用哪个解析器,它支持:"babel", "babylon", "flow", "ts", or "tsx"。由于我们的 .d.ts 文件是纯粹的 ts,所以我们通过设置 parser 为 ts 来告诉 jscodeshift 如何解释代码。

js 复制代码
const transform = (file, api) => {
    const j = api.jscodeshift.withParser('ts')
}

由于 jscodeshift 是基于 AST 的代码转换工具,所以要具备一定的 AST 相关的知识。首先如果能方便的查看 AST 的结构,可以方便我们在头脑中形成如何处理 AST 节点的观念。AST 查看可以通过 astexplorer.net/。针对本文中待处理的代码,我们需要在 astexplorer.net 中将编译设置为 @typescript-eslint/parser。因为同样的代码不同的编译器有不同的 AST 表达,@typescript-eslint/parser 构造出来的 AST 与 jscodeshift 设置 ts 解析后的形式一致。

因为我们要处理 ActionSheetProps 中的属性,而 ActionSheetProps 在 AST 中对应 TSInterfaceDeclaration 类型的节点。所以只需要遍历 AST,就可以查询到 TSInterfaceDeclaration 类型且标识符是 ActionSheetProps 的节点。继续遍历这些节点,直接更新节点的值,就可以更改 AST。关键代码逻辑如下:

js 复制代码
const transform = (file, api) => {
  const j = api.jscodeshift.withParser('ts')
  return j(file.source)
    .find(j.TSInterfaceDeclaration, {
      id: {
        name: componentName + 'Props',
        type: 'Identifier',
      },
    })
    .forEach((path) => {
      path.value?.body?.body?.forEach((item) => {
        if (!item.key) return
        const info = findInTable(item.key.name)
        if (!info) return
        item['comments'] = [
          j.commentBlock(`*\n* ${info[1]}\n* @default ${info[3]}\n`),
        ]
      })
    })
    .toSource()
}

到这里就完成了通过 jscodeshift 向类型文件增加注释的功能。代码细节可以参考NutUI-React 仓库中的 add-comments-to-dts.js 文件。

加入我们

再次期待您早日成为我们共建大军中的一员!

一起共建,一起使用!

做站内最优秀的开源组件库!

更多文章

NutUI-React 适配 Taro 的实现

基于 Leo+NutUI 的移动端项目模板实践

NutUI-React Input输入框的使用指南

NUTUI-React 数字滚动组件的设计与实现

NutUI-React 组件库的动态主题探索

NutUI 4.0 正式发布!

2022 倒带-NutUI

京东 React 组件库支持小程序开发了

NutUI 京东小程序发布了!

相关推荐
学地理的小胖砸1 小时前
【GEE的Python API】
大数据·开发语言·前端·python·遥感·地图学·地理信息科学
垦利不2 小时前
css总结
前端·css·html
八月的雨季 最後的冰吻2 小时前
C--字符串函数处理总结
c语言·前端·算法
6230_3 小时前
关于HTTP通讯流程知识点补充—常见状态码及常见请求方式
前端·javascript·网络·网络协议·学习·http·html
pan_junbiao4 小时前
Vue组件:使用$emit()方法监听子组件事件
前端·javascript·vue.js
正在绘制中4 小时前
如何部署Vue+Springboot项目
前端·vue.js·spring boot
Keep striving5 小时前
SpringMVC基于注解使用:国际化
java·前端·spring·servlet·tomcat·maven
Loong_DQX5 小时前
【前端】vue+html+js 实现table表格展示,以及分页按钮添加
前端·javascript·vue.js
Boyi美业5 小时前
连锁美业门店开设不同的课程有什么用?美业系统源码分享
java·前端·团队开发·创业创新·源代码管理
AI创客岛5 小时前
15个提高转化率的着陆页最佳实践
大数据·前端·人工智能