DataWorks提供了丰富的OpenAPI,您可以根据需要使用DataWorks的OpenAPI等开放能力实现各种业务场景。本文以元数据表管理为例,为您介绍如何串连元数据的OpenAPI来达成查询列表、查询表详情、血缘图展示与创建表等操作。

背景信息

在进行本实践之前,建议您先参考以下链接,了解DataWorks的OpenAPI的基本能力和概念: 下文为您提供了表管理的多个细分场景的实践:

实践1:查询表列表

以下实践将介绍如何使用OpenAPI来实现查询表列表,获得MaxCompute项目下的所有表的列表,并做分页查询,实践的主要流程如下。

  1. 在MetaServiceProxy中编写一个GetMetaDBTableList方法,处理前端参数并发送请求到OpenAPI GetMetaDBTableList中,以获取元数据表的列表信息。
    package com.aliyun.dataworks.services;
    
    import com.aliyun.dataworks.dto.*;
    import com.aliyuncs.IAcsClient;
    import com.aliyuncs.dataworks_public.model.v20200518.*;
    import com.aliyuncs.exceptions.ClientException;
    import com.aliyuncs.exceptions.ServerException;
    import com.aliyuncs.utils.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.util.CollectionUtils;
    
    /**
     * @author dataworks demo
     */
    @Service
    public class MetaServiceProxy {
      @Autowired
      private DataWorksOpenApiClient dataWorksOpenApiClient;
      /**
       * DataWorks OpenAPI : GetMetaDBTableList
       * Link : https://help.aliyun.com/document_detail/173916.html
       *
       * @param listTablesDto
       */
      public GetMetaDBTableListResponse.Data getMetaDBTableList(ListTablesDto listTablesDto) {
        try {
          IAcsClient client = dataWorksOpenApiClient.createClient();
          GetMetaDBTableListRequest getMetaDBTableListRequest = new GetMetaDBTableListRequest();
          if (StringUtils.isEmpty(listTablesDto.getDataSourceType())) {
            // 数据类型,目前仅支持odps和emr。
            getMetaDBTableListRequest.setDataSourceType("odps");
          }
          // 项目的唯一标识,格式为odps.{projectName}。仅当数据类型为odps时,需要配置该参数。
          getMetaDBTableListRequest.setAppGuid(listTablesDto.getAppGuid());
          // 请求的数据页数,用于翻页。
          getMetaDBTableListRequest.setPageNumber(listTablesDto.getPageNumber());
          // 每页显示的条数,默认为10条,最大为100条。
          getMetaDBTableListRequest.setPageSize(listTablesDto.getPageSize());
          // 数据库的名称。
          getMetaDBTableListRequest.setDatabaseName(listTablesDto.getDatabaseName());
          GetMetaDBTableListResponse acsResponse = client.getAcsResponse(getMetaDBTableListRequest);
          // 计算引擎的总数
          System.out.println(acsResponse.getData().getTotalCount());
          for (GetMetaDBTableListResponse.Data.TableEntityListItem tableEntityListItem : acsResponse.getData()
            .getTableEntityList()) {
            // 表的唯一标识
            System.out.println(tableEntityListItem.getTableGuid());
            // 表的名称
            System.out.println(tableEntityListItem.getTableName());
            // 数据库的名称
            System.out.println(tableEntityListItem.getDatabaseName());
          }
          return acsResponse.getData();
        } catch (ServerException e) {
          e.printStackTrace();
        } catch (ClientException e) {
          e.printStackTrace();
          // 请求ID
          System.out.println(e.getRequestId());
          // 错误码
          System.out.println(e.getErrCode());
          // 错误信息
          System.out.println(e.getErrMsg());
        }
        return null;
      }
    }
  2. 在MetaRestController中添加一个listMetaDBTable方法作为入口供前端访问。
    package com.aliyun.dataworks.demo;
    
    import com.aliyun.dataworks.dto.*;
    import com.aliyun.dataworks.services.MetaServiceProxy;
    import com.aliyuncs.dataworks_public.model.v20200518.*;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.bind.annotation.CrossOrigin;
    
    
    /**
     * 演示如何通过DataWorks OpenAPI构建自定义元数据平台
     *
     * @author dataworks demo
     */
    @RestController
    @RequestMapping("/meta")
    public class MetaRestController {
      @Autowired
      private MetaServiceProxy metaService;
      /**
       * 分页查询表数据
       *
       * @param listTablesDto
       * @return {@link GetMetaDBTableListResponse.Data}
       */
      @GetMapping("/listTables")
      public GetMetaDBTableListResponse.Data listMetaDBTable(ListTablesDto listTablesDto) {
          return metaService.getMetaDBTableList(listTablesDto);
      }
    }
  3. 在前端实现一个简易的可交互介面。
    说明 下方代码简化了部分非表查询流程,只展示表查询的最小实现。
    import React from 'react';
    import { Table, Form, Field, Input, Button, Pagination, Dialog, Card } from '@alifd/next';
    
    function getTableList(params: TableListInput): Promise<TableListOutput> {
      const url = helpers.createFetchUrl(METHOD_URL.GET_TABLE_LIST, params);
      const response = await fetch(url);
      const result = await response.json();
      return result;
    }
    
    const { Column } = Table;
    const { Item } = Form;
    const { Header, Content } = Card;
    const App: React.FunctionComponent<Props> = () => {
      const field = Field.useField();
      const [datasource, setDatasource] = React.useState<TableItem[]>([]);
      const [loading, setLoading] = React.useState<boolean>(false);
      const [total, setTotal] = React.useState<number>(0);
      React.useEffect(() => field.setValue('dataSourceType', 'odps'), []);
      const onSubmit = React.useCallback((pageNumber: number = 1) => {
        field.validate(async (errors, values) => {
          if (errors) return;
          setLoading(true);
          try {
            const response = await getTableList({ pageNumber, ...values } as TableListInput);
            setDatasource(response.tableEntityList);
            setTotal(response.totalCount);
          } catch (e) {
            throw e;
          } finally {
            setLoading(false);
          }
        });
      }, [field]);
      return (
        <div>
          <Card free hasBorder={false}>
            <Header title="元数据表管理场景 Demo" />
            <Content style={{ marginTop: 24 }}>
              <Form field={field} colon fullWidth>
                <Item label="MaxCompute项目ID" name="appGuid" required>
                  <Input />
                </Item>
                <Item label="数据库名称" name="databaseName">
                  <Input />
                </Item>
                <div>
                  <Button type="primary" onClick={() => onSubmit()}>查询</Button>
                  <Button type="primary">创建表</Button>
                </div>
              </Form>
              <div>
                <Table dataSource={datasource} loading={loading}>
                  <Column title="数据库名称" dataIndex="databaseName" />
                  <Column title="表GUID" dataIndex="tableGuid" />
                  <Column title="表名称" dataIndex="tableName" />
                  <Column title="操作" width={150} cell={(value, index, record) => (
                    <div>
                      <Button type="primary" text>查看详情</Button>
                      <Button type="primary" text>查看表血缘</Button>
                    </div>
                  )} />
                </Table>
                <Pagination
                  total={total}
                  onChange={onSubmit}
                  showJump={false}
                />
              </div>
            </Content>
          </Card>
        </div>
      );
    };
    
    export default App;
                            
  4. 完成上述代码开发后,您可在本地部署并运行工程代码。部署并运行的操作请参见通用操作:本地部署运行
完成上述实践步骤后,您可以输入MaxCompute项目空间ID与数据库名称后进行查询,获得项目空间下的所有表的列表,也可以做分页查询,如下图所示。查询表列表

实践2:查询表详情

以下实践将结合元数据中GetMetaTableBasicInfoGetMetaTableColumnGetMetaTablePartition三个OpenAPI来实现查询表详情,实践操作流程如下。

  1. 在MetaServiceProxy中构建getMetaTableBasicInfo、getMetaTableColumn与getMetaTablePartition方法来调用OpenAPI服务获取表的基础信息、表的字段信息与表的分区信息。
    package com.aliyun.dataworks.services;
    
    import com.aliyun.dataworks.dto.*;
    import com.aliyuncs.IAcsClient;
    import com.aliyuncs.dataworks_public.model.v20200518.*;
    import com.aliyuncs.exceptions.ClientException;
    import com.aliyuncs.exceptions.ServerException;
    import com.aliyuncs.utils.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.util.CollectionUtils;
    
    /**
     * @author dataworks demo
     */
    @Service
    public class MetaServiceProxy {
      @Autowired
      private DataWorksOpenApiClient dataWorksOpenApiClient;
      /**
       * DataWorks OpenAPI : GetMetaTableBasicInfo
       * Link : https://help.aliyun.com/document_detail/173920.html
       *
       * @param getMetaTableBasicInfoDto
       */
      public GetMetaTableBasicInfoResponse.Data getMetaTableBasicInfo(GetMetaTableBasicInfoDto getMetaTableBasicInfoDto) {
        try {
          IAcsClient client = dataWorksOpenApiClient.createClient();
          GetMetaTableBasicInfoRequest getMetaTableBasicInfoRequest = new GetMetaTableBasicInfoRequest();
          // MaxCompute表的唯一标识。格式为odps.projectName.tableName。
          getMetaTableBasicInfoRequest.setTableGuid(getMetaTableBasicInfoDto.getTableGuid());
          // 数据库名称
          getMetaTableBasicInfoRequest.setDatabaseName(getMetaTableBasicInfoDto.getDatabaseName());
          // EMR的表名称
          getMetaTableBasicInfoRequest.setTableName(getMetaTableBasicInfoDto.getTableName());
          // 数据类型,包括odps和emr。
          getMetaTableBasicInfoRequest.setDataSourceType("odps");
          // 是否包含扩展字段。扩展字段包含读取次数、收藏次数、浏览次数等。仅当数据类型为ODPS时,该参数生效。
          getMetaTableBasicInfoRequest.setExtension(true);
          GetMetaTableBasicInfoResponse acsResponse = client.getAcsResponse(getMetaTableBasicInfoRequest);
          // 表的名称。
          System.out.println(acsResponse.getData().getTableName());
          // 表的收藏次数。仅当Extension参数取值为true时才会返回该参数,并且该参数仅对odps数据类型生效。
          System.out.println(acsResponse.getData().getFavoriteCount());
          // 表的描述。
          System.out.println(acsResponse.getData().getComment());
          // 字段的个数。
          System.out.println(acsResponse.getData().getColumnCount());
          // 创建表的时间。
          System.out.println(acsResponse.getData().getCreateTime());
          // 工作空间的ID。
          System.out.println(acsResponse.getData().getProjectId());
          // 表所有者的ID。
          System.out.println(acsResponse.getData().getOwnerId());
          // 环境类型,取值如下:
          // 0表示开发表。
          // 1表示生产表。
          System.out.println(acsResponse.getData().getEnvType());
          // 数据库的名称。
          System.out.println(acsResponse.getData().getDatabaseName());
          // 表的可见性:0表示目标表对工作空间成员可见。
          // 1表示目标表对租户内成员可见。
          // 2表示目标表对租户间成员均可见。
          // 3表示目标表仅对责任人可见。
          System.out.println(acsResponse.getData().getIsVisible());
          // 表的唯一标识。
          System.out.println(acsResponse.getData().getTableGuid());
          // 表的读取次数。仅当Extension参数取值为true时才会返回该参数,并且该参数仅对odps数据类型生效。
          System.out.println(acsResponse.getData().getReadCount());
          // EMR集群的ID。
          System.out.println(acsResponse.getData().getClusterId());
          // 是否为分区表,取值如下:
          // true:是分区表。
          // false:不是分区表。
          System.out.println(acsResponse.getData().getIsPartitionTable());
          // 是否为视图,取值如下:
          // true:是视图。
          // false:不是视图。
          System.out.println(acsResponse.getData().getIsView());
          // 表的生命周期。单位为天。
          System.out.println(acsResponse.getData().getLifeCycle());
          // 工作空间的名称。
          System.out.println(acsResponse.getData().getProjectName());
          // 表的浏览次数。仅当Extension参数取值为true时才会返回该参数,并且该参数仅对odps数据类型生效。
          System.out.println(acsResponse.getData().getViewCount());
          // 最近一次访问表的时间。
          System.out.println(acsResponse.getData().getLastAccessTime());
          // 表占用的存储空间。单位为Byte。
          System.out.println(acsResponse.getData().getDataSize());
          // 最近一次更新表的时间。
          System.out.println(acsResponse.getData().getLastModifyTime());
          // 最近一次变更表结构的时间。
          System.out.println(acsResponse.getData().getLastDdlTime());
          // Hive分区。
          System.out.println(acsResponse.getData().getPartitionKeys());
          // Hive数据库的存储地址。
          System.out.println(acsResponse.getData().getLocation());
          // 表的中文名称。
          System.out.println(acsResponse.getData().getCaption());
          // 租户ID。
          System.out.println(acsResponse.getData().getTenantId());
          return acsResponse.getData();
        } catch (ServerException e) {
          e.printStackTrace();
        } catch (ClientException e) {
          e.printStackTrace();
          // 请求ID
          System.out.println(e.getRequestId());
          // 错误码
          System.out.println(e.getErrCode());
          // 错误信息
          System.out.println(e.getErrMsg());
        }
        return null;
      }
      /**
       * DataWorks OpenAPI : GetMetaTableColumn
       * Link : https://help.aliyun.com/document_detail/173921.html
       *
       * @param getMetaTableColumnDto
       */
      public GetMetaTableColumnResponse.Data getMetaTableColumn(GetMetaTableColumnDto getMetaTableColumnDto) {
        try {
          IAcsClient client = dataWorksOpenApiClient.createClient();
          GetMetaTableColumnRequest getMetaTableColumnRequest = new GetMetaTableColumnRequest();
          // 表的唯一标识。
          getMetaTableColumnRequest.setTableGuid(getMetaTableColumnDto.getTableGuid());
          // 请求获取的数据页码数,用于翻页。
          getMetaTableColumnRequest.setPageNum(getMetaTableColumnDto.getPageNum());
          // 每页显示的条数,默认为10条,最大100条。
          getMetaTableColumnRequest.setPageSize(getMetaTableColumnDto.getPageSize());
          // EMR集群的ID,您可以登录EMR管理控制台,获取集群ID。
          getMetaTableColumnRequest.setClusterId(getMetaTableColumnDto.getClusterId());
          // EMR的数据库名称
          getMetaTableColumnRequest.setDatabaseName(getMetaTableColumnDto.getDatabaseName());
          // EMR的数据库名称
          getMetaTableColumnRequest.setTableName(getMetaTableColumnDto.getTableName());
          // 数据类型,当前仅支持取值为emr。
          getMetaTableColumnRequest.setDataSourceType(getMetaTableColumnDto.getDataSourceType());
          GetMetaTableColumnResponse acsResponse = client.getAcsResponse(getMetaTableColumnRequest);
          // 字段的总数。
          System.out.println(acsResponse.getData().getTotalCount());
          for (GetMetaTableColumnResponse.Data.ColumnListItem columnListItem : acsResponse.getData()
            .getColumnList()) {
            // 字段的唯一标识。
            System.out.println(columnListItem.getColumnGuid());
            // 字段的名称。
            System.out.println(columnListItem.getColumnName());
            // 字段是否为分区字段,取值如下:
            // true,是分区字段。
            // false,不是分区字段。
            System.out.println(columnListItem.getIsPartitionColumn());
            // 字段的备注。
            System.out.println(columnListItem.getComment());
            // 字段的类型。
            System.out.println(columnListItem.getColumnType());
            // 字段是否为主键,取值如下:
            // true,是主键。
            // false,不是主键。
            System.out.println(columnListItem.getIsPrimaryKey());
            // 字段的排序。
            System.out.println(columnListItem.getPosition());
            // 字段的描述。
            System.out.println(columnListItem.getCaption());
            // 字段是否为外键,取值如下:
            // true,是外键。
            // false,不是外键。
            System.out.println(columnListItem.getIsForeignKey());
            // 字段热度。
            System.out.println(columnListItem.getRelationCount());
          }
          return acsResponse.getData();
        } catch (ServerException e) {
          e.printStackTrace();
        } catch (ClientException e) {
          e.printStackTrace();
          // 请求ID
          System.out.println(e.getRequestId());
          // 错误码
          System.out.println(e.getErrCode());
          // 错误信息
          System.out.println(e.getErrMsg());
        }
        return null;
      }
        /**
       * DataWorks OpenAPI : GetMetaTablePartition
       * Link : https://help.aliyun.com/document_detail/173923.html
       *
       * @param getMetaTablePartitionDto
       */
      public GetMetaTablePartitionResponse.Data getMetaTablePartition(GetMetaTablePartitionDto getMetaTablePartitionDto) {
        try {
          IAcsClient client = dataWorksOpenApiClient.createClient();
          GetMetaTablePartitionRequest getMetaTablePartitionRequest = new GetMetaTablePartitionRequest();
          // 请求的数据页数,用于翻页。
          getMetaTablePartitionRequest.setPageNumber(getMetaTablePartitionDto.getPageNumber());
          // 每页显示的条数,默认为10条,最大100条。
          getMetaTablePartitionRequest.setPageSize(getMetaTablePartitionDto.getPageSize());
          // 表的唯一标识。
          getMetaTablePartitionRequest.setTableGuid(getMetaTablePartitionDto.getTableGuid());
          // EMR集群的ID,仅当数据类型为EMR时,需要配置该参数。
          getMetaTablePartitionRequest.setClusterId(getMetaTablePartitionDto.getClusterId());
          // 数据库的名称。仅当数据类型为EMR时,需要配置该参数。
          getMetaTablePartitionRequest.setDatabaseName(getMetaTablePartitionDto.getDatabaseName());
          // 数据库的名称。仅当数据类型为EMR时,需要配置该参数。
          getMetaTablePartitionRequest.setTableName(getMetaTablePartitionDto.getTableName());
          // 数据类型,支持ODPS或者EMR。
          getMetaTablePartitionRequest.setDataSourceType(getMetaTablePartitionDto.getDataSourceType());
          GetMetaTablePartitionResponse acsResponse = client.getAcsResponse(getMetaTablePartitionRequest);
          for (GetMetaTablePartitionResponse.Data.DataEntityListItem dataEntityListItem : acsResponse.getData()
            .getDataEntityList()) {
            // 分区的目录。
            System.out.println(dataEntityListItem.getPartitionPath());
            // 分区的大小,单位为Byte。
            System.out.println(dataEntityListItem.getDataSize());
            // 分区的名称。
            System.out.println(dataEntityListItem.getPartitionName());
            // 备注信息。
            System.out.println(dataEntityListItem.getComment());
            // 修改分区的时间。
            System.out.println(dataEntityListItem.getModifiedTime());
            // 创建分区的时间。
            System.out.println(dataEntityListItem.getCreateTime());
            // 分区的数据量。
            System.out.println(dataEntityListItem.getRecordCount());
            // 分区的类型。
            System.out.println(dataEntityListItem.getPartitionType());
            // 分区的唯一标识。
            System.out.println(dataEntityListItem.getPartitionGuid());
            // Hive分区的地址。
            System.out.println(dataEntityListItem.getPartitionLocation());
            // 表的唯一标识。
            System.out.println(dataEntityListItem.getTableGuid());
          }
          return acsResponse.getData();
        } catch (ServerException e) {
          e.printStackTrace();
        } catch (ClientException e) {
          e.printStackTrace();
          // 请求ID
          System.out.println(e.getRequestId());
          // 错误码
          System.out.println(e.getErrCode());
          // 错误信息
          System.out.println(e.getErrMsg());
        }
        return null;
      }
    }
  2. 在MetaRestController中提供三个方法分别为getMetaTableBasicInfo、getMetaTableColumn与getMetaTablePartition供前端进行调用。
    package com.aliyun.dataworks.demo;
    
    import com.aliyun.dataworks.dto.*;
    import com.aliyun.dataworks.services.MetaServiceProxy;
    import com.aliyuncs.dataworks_public.model.v20200518.*;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.bind.annotation.CrossOrigin;
    
    
    /**
     * 演示如何通过DataWorks OpenAPI构建自定义元数据平台
     *
     * @author dataworks demo
     */
    @RestController
    @RequestMapping("/meta")
    public class MetaRestController {
      @Autowired
      private MetaServiceProxy metaService;
      /**
       * 获取表的基本属性
       *
       * @param getMetaTableBasicInfoDto
       * @return {@link GetMetaTableBasicInfoResponse.Data}
       */
      @GetMapping("/getTable")
      public GetMetaTableBasicInfoResponse.Data getMetaTableBasicInfo(GetMetaTableBasicInfoDto getMetaTableBasicInfoDto) {
          return metaService.getMetaTableBasicInfo(getMetaTableBasicInfoDto);
      }
      /**
       * 查询表的列信息
       *
       * @param getMetaTableColumnDto
       * @return {@link GetMetaTableColumnResponse.Data}
       */
      @GetMapping("/getTableColumn")
      public GetMetaTableColumnResponse.Data getMetaTableColumn(GetMetaTableColumnDto getMetaTableColumnDto) {
          return metaService.getMetaTableColumn(getMetaTableColumnDto);
      }
      /**
       * 查询表分区
       *
       * @param getMetaTablePartitionDto
       * @return
       */
      @GetMapping("/getTablePartition")
      public GetMetaTablePartitionResponse.Data getMetaTablePartition(GetMetaTablePartitionDto getMetaTablePartitionDto) {
          return metaService.getMetaTablePartition(getMetaTablePartitionDto);
      }
    }
  3. 在前端实现一个简易的详情页来调用表基本信息、表列信息与表分区的接口,并展示出来。
    import React from 'react';
    import moment from 'moment';
    import { Form, Grid, Table, Pagination } from '@alifd/next';
    import cn from 'classnames';
    import * as services from '../services';
    import type { TableItem, TableDetailOutput, TableColumn, TableEntity } from '../services/meta';
    
    export interface Props {
      item: TableItem;
    }
    
    const formItemLayout = {
      labelCol: {
        fixedSpan: 6
      },
      wrapperCol: {
        span: 18
      },
      labelTextAlign: 'left' as const,
      colon: true,
    };
    const { Row, Col } = Grid;
    const { Item } = Form;
    const { Column } = Table;
    const DetailContent: React.FunctionComponent<Props> = (props) => {
      const [detail, setDetail] = React.useState<Partial<TableDetailOutput>>({});
      const [columns, setColumns] = React.useState<Partial<TableColumn[]>>([]);
      const [partitions, setPartitions] = React.useState<Partial<TableEntity[]>>([]);
      const [columnsTotal, setColumnsTotal] = React.useState<number>(0);
      const [partitionTotal, setPartitionTotal] = React.useState<number>(0);
      // 实现一个调用获取表基本信息接口的方法
      const getTableDetail = React.useCallback(async () => {
        const response = await services.meta.getTableDetail({ tableGuid: props.item.tableGuid });
        setDetail(response);
      }, [props.item.tableGuid]);
      // 实现一个调用获取表字段信息接口的方法
      const getTableColumns = React.useCallback(async (pageNum: number = 1) => {
        const response = await services.meta.getMetaTableColumns({
          pageNum,
          tableGuid: props.item.tableGuid,
        });
        setColumns(response.columnList);
        setColumnsTotal(response.totalCount);
      }, [props.item.tableGuid]);
      // 实现一个调用获取表分区信息接口的方法
      const getTablePartition = React.useCallback(async (pageNumber: number = 1) => {
        const response = await services.meta.getTablePartition({
          pageNumber,
          tableGuid: props.item.tableGuid,
        });
        setPartitions(response.dataEntityList);
        setPartitionTotal(response.totalCount);
      }, []);
      // 表的权限映射
      const isVisible = React.useMemo(() => {
        switch (detail.isVisible) {
          case 0:
            return '对工作空间成员可见';
          case 1:
            return '对租户内成员可见';
          case 2:
            return '对租户间成员均可见';
          case 3:
            return '仅对责任人可见';
          default:
            return '';
        }
      }, [detail.isVisible]);
      React.useEffect(() => {
        if (props.item.tableGuid) {
          getTableDetail();
          getTableColumns();
          getTablePartition();
        }
      }, [props.item.tableGuid]);
      // 渲染前端页面
      return (
        <div>
          <Form labelTextAlign="left">
            <Row>
              <Col>
                <Item {...formItemLayout} label="表GUID">
                  <span>{detail.tableGuid}</span>
                </Item>
              </Col>
              <Col>
                <Item {...formItemLayout} label="表名称">
                  <span>{detail.tableName}</span>
                </Item>
              </Col>
            </Row>
            <Row>
              <Col>
                <Item {...formItemLayout} label="所属工作空间">
                  <span className={cn(classes.formContentWrapper)}>{detail.projectName}</span>
                </Item>
              </Col>
              <Col>
                <Item {...formItemLayout} label="表所有者ID">
                  <span className={cn(classes.formContentWrapper)}>{detail.ownerId}</span>
                </Item>
              </Col>
            </Row>
            <Row>
              <Col>
                <Item {...formItemLayout} label="表生命周期">
                  <span className={cn(classes.formContentWrapper)}>{detail.lifeCycle ? `${detail.lifeCycle} 天` : '永久'}</span>
                </Item>
              </Col>
              <Col>
                <Item {...formItemLayout} label="备注信息">
                  <span className={cn(classes.formContentWrapper)}>{detail.comment}</span>
                </Item>
              </Col>
            </Row>
            <Row>
              <Col>
                <Item {...formItemLayout} label="存储空间">
                  <span className={cn(classes.formContentWrapper)}>{`${detail.dataSize} Bytes`}</span>
                </Item>
              </Col>
              <Col>
                <Item {...formItemLayout} label="是否为分区表">
                  <span className={cn(classes.formContentWrapper)}>{detail.isView ? '是' : '否'}</span>
                </Item>
              </Col>
            </Row>
            <Row>
              <Col>
                <Item {...formItemLayout} label="表的可见性">
                  <span className={cn(classes.formContentWrapper)}>{isVisible}</span>
                </Item>
              </Col>
              <Col>
                <Item {...formItemLayout} label="表的读取次数">
                  <span className={cn(classes.formContentWrapper)}>{detail.readCount}</span>
                </Item>
              </Col>
            </Row>
            <Row>
              <Col>
                <Item {...formItemLayout} label="创建时间">
                  <span className={cn(classes.formContentWrapper)}>{moment(detail.createTime).format('YYYY-MM-DD HH:mm:ss')}</span>
                </Item>
              </Col>
              <Col>
                <Item {...formItemLayout} label="最后修改时间">
                  <span className={cn(classes.formContentWrapper)}>{moment(detail.lastModifyTime).format('YYYY-MM-DD HH:mm:ss')}</span>
                </Item>
              </Col>
            </Row>
            <Item label="字段详情" colon>
              <Table dataSource={columns}>
                <Column title="名称" dataIndex="columnName" />
                <Column title="类型" dataIndex="columnType" />
                <Column title="是否为主键" dataIndex="isPrimaryKey" />
                <Column title="是否为外键" dataIndex="isForeignKey" />
                <Column title="是否为分区" dataIndex="isPartitionColumn" />
                <Column title="说明" dataIndex="comment" />
              </Table>
              <Pagination
                total={columnsTotal}
                onChange={getTableColumns}
                showJump={false}
                className={cn(classes.tablePaginationWrapper)}
              />
            </Item>
            <Item label="分区详情" style={{ marginTop: 32, marginBottom: 32 }} colon>
             <Table
              dataSource={partitions}
              emptyContent={<span>非分区表</span>}
            >
                <Column title="名称" dataIndex="partitionName" />
                <Column title="类型" dataIndex="partitionType" />
                <Column title="分区大小" dataIndex="dataSize" cell={value => `${value} bytes`} />
                <Column title="备注" dataIndex="comment" />
              </Table>
              <Pagination
                total={partitionTotal}
                onChange={getTablePartition}
                showJump={false}
                className={cn(classes.tablePaginationWrapper)}
              />
            </Item>
          </Form>
        </div>
      );
    }
    
    export default DetailContent;
                            
  4. 完成上述代码开发后,您可在本地部署并运行工程代码。部署并运行的操作请参见通用操作:本地部署运行
完成上述实践步骤后,您可以实现表详情展示,如下图所示。查看表详情

实践3:查找表的血缘信息并上下钻

以下实践将使用元数据中GetMetaTableLineage来查找表的血缘信息以及上下钻的能力,实践流程如下。

  1. 在MetaServiceProxy中构建getMetaTableLineage方法来调用OpenAPI getMetaTableLineage方法,并对入参进行处理。
    package com.aliyun.dataworks.services;
    
    import com.aliyun.dataworks.dto.*;
    import com.aliyuncs.IAcsClient;
    import com.aliyuncs.dataworks_public.model.v20200518.*;
    import com.aliyuncs.exceptions.ClientException;
    import com.aliyuncs.exceptions.ServerException;
    import com.aliyuncs.utils.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.util.CollectionUtils;
    
    /**
     * @author dataworks demo
     */
    @Service
    public class MetaServiceProxy {
      @Autowired
      private DataWorksOpenApiClient dataWorksOpenApiClient;
      /**
       * DataWorks OpenAPI : GetMetaTableLineage
       * Link : https://help.aliyun.com/document_detail/173927.html
       *
       * @param getMetaTableLineageDto
       */
      public GetMetaTableLineageResponse.Data getMetaTableLineage(GetMetaTableLineageDto getMetaTableLineageDto) {
        try {
          IAcsClient client = dataWorksOpenApiClient.createClient();
          GetMetaTableLineageRequest getMetaTableLineageRequest = new GetMetaTableLineageRequest();
          // 表的唯一标识。
          getMetaTableLineageRequest.setTableGuid(getMetaTableLineageDto.getTableGuid());
          // 字段的上下游方向:up表示上游,down表示下游。
          getMetaTableLineageRequest.setDirection(getMetaTableLineageDto.getDirection());
          // 分页数据。
          getMetaTableLineageRequest.setNextPrimaryKey(getMetaTableLineageDto.getTableGuid());
          // 每页显示的条数,默认为10条,最大100条。
          getMetaTableLineageRequest.setPageSize(getMetaTableLineageDto.getPageSize());
          // EMR集群的ID,针对EMR情况。
          getMetaTableLineageRequest.setClusterId(getMetaTableLineageDto.getClusterId());
          // 数据库的名称。
          getMetaTableLineageRequest.setDatabaseName(getMetaTableLineageDto.getDatabaseName());
          // 表名
          getMetaTableLineageRequest.setTableName(getMetaTableLineageDto.getTableName());
          // 数据类型,包括odps或emr。
          getMetaTableLineageRequest.setDataSourceType(getMetaTableLineageDto.getDataSourceType());
          GetMetaTableLineageResponse acsResponse = client.getAcsResponse(getMetaTableLineageRequest);
          for (GetMetaTableLineageResponse.Data.DataEntityListItem dataEntityListItem : acsResponse.getData()
            .getDataEntityList()) {
            // 表的名称。
            System.out.println(dataEntityListItem.getTableName());
            // 表的唯一标识。
            System.out.println(dataEntityListItem.getTableGuid());
            // 创建时间。
            System.out.println(dataEntityListItem.getCreateTimestamp());
          }
          return acsResponse.getData();
        } catch (ServerException e) {
          e.printStackTrace();
        } catch (ClientException e) {
          e.printStackTrace();
          // 请求ID
          System.out.println(e.getRequestId());
          // 错误码
          System.out.println(e.getErrCode());
          // 错误信息
          System.out.println(e.getErrMsg());
        }
        return null;
      }
    }
  2. 在入口处定义一个getMetaTableLineage方法供前端调用。
    package com.aliyun.dataworks.demo;
    
    import com.aliyun.dataworks.dto.*;
    import com.aliyun.dataworks.services.MetaServiceProxy;
    import com.aliyuncs.dataworks_public.model.v20200518.*;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.bind.annotation.CrossOrigin;
    
    
    /**
     * 演示如何通过DataWorks OpenAPI构建自定义元数据平台
     *
     * @author dataworks demo
     */
    @RestController
    @RequestMapping("/meta")
    public class MetaRestController {
      @Autowired
      private MetaServiceProxy metaService;
      /**
       * 查询表的血缘关系
       *
       * @param getMetaTableLineageDto
       * @return
       */
      @GetMapping("/getTableLineage")
      public GetMetaTableLineageResponse.Data getMetaTableLineage(GetMetaTableLineageDto getMetaTableLineageDto) {
          return metaService.getMetaTableLineage(getMetaTableLineageDto);
      }
    }
  3. 在前端实现一个调用血缘图接口的能力并渲染出来。
    import React from 'react';
    import Graphin, { Behaviors } from '@antv/graphin';
    import type { IUserNode, IUserEdge, GraphinData, GraphEvent } from '@antv/graphin';
    import * as services from '../services';
    import type { TableItem, GetTableLineageOutput, LineageEntity } from '../services/meta';
    import '@antv/graphin/dist/index.css';
    
    export interface Props {
      item: TableItem;
    }
    
    function transNode(entity: LineageEntity | TableItem, direction?: string): IUserNode {
      return {
        id: entity.tableGuid,
        label: entity.tableName,
        data: {
          ...entity,
          direction,
        },
        style: {
          label: {
            value: entity.tableName,
          }
        },
      }
    }
    function transEdge(source: LineageEntity | TableItem, target: LineageEntity | TableItem): IUserEdge {
      return {
        source: source.tableGuid,
        target: target.tableGuid,
      };
    }
    function parse(
      source: LineageEntity | TableItem,
      data: GetTableLineageOutput,
      direction: string,
    ): [IUserNode[], IUserEdge[]] {
      const nodes: IUserNode[] = [];
      const edges: IUserEdge[] = [];
      data.dataEntityList.forEach((entity) => {
        nodes.push(transNode(entity, direction));
        if (direction === 'down') {
          edges.push(transEdge(source, entity));
        } else {
          edges.push(transEdge(entity, source));
        }
      });
      return [nodes, edges];
    }
    function mergeNodes(prev: IUserNode[], next: IUserNode[]) {
      const result: IUserNode[] = prev.slice();
      next.forEach((item) => {
        const hasValue = prev.findIndex(i => i.id === item.id) >= 0;
        !hasValue && result.push(item);
      });
      return result;
    }
    function mergeEdges(source: IUserEdge[], target: IUserEdge[]) {
      const result: IUserEdge[] = source.slice();
      target.forEach((item) => {
        const hasValue = source.findIndex(i => i.target === item.target && i.source === item.source) >= 0;
        !hasValue && result.push(item);
      });
      return result;
    }
    
    const { ActivateRelations, DragNode, ZoomCanvas } = Behaviors;
    const LineageContent: React.FunctionComponent<Props> = (props) => {
      const ref = React.useRef<Graphin>();
      const [data, setData] = React.useState<GraphinData>({ nodes: [], edges: [] });
      // 调用后端接口获取血缘信息
      const getTableLineage = async (
        collection: GraphinData,
        target: TableItem | LineageEntity,
        direction: string,
      ) => {
        if (!direction) {
          return collection;
        }
        const response = await services.meta.getTableLineage({
          direction,
          tableGuid: target.tableGuid,
        });
        const [nodes, edges] = parse(target, response, direction);
        collection.nodes = mergeNodes(collection.nodes!, nodes);
        collection.edges = mergeEdges(collection.edges!, edges);
        return collection;
      };
      // 初始化时获取数据并渲染页面
      const init = async () => {
        let nextData = Object.assign({}, data, { nodes: [transNode(props.item)] });
        nextData = await getTableLineage(nextData, props.item, 'up');
        nextData = await getTableLineage(nextData, props.item, 'down');
        setData(nextData);
        ref.current!.graph.fitCenter();
      };
      React.useEffect(() => {
        ref.current?.graph && init();
      }, [
        ref.current?.graph,
      ]);
      // 处理表节点被点击时的事件,发起请求获取数据
      React.useEffect(() => {
        const graph = ref.current?.graph;
        const event = async (event: GraphEvent) => {
          const source = event.item?.get('model').data;
          let nextData = Object.assign({}, data);
          nextData = await getTableLineage(nextData, source, source.direction);
          setData(nextData);
        };
        graph?.on('node:click', event);
        return () => {
          graph?.off('node:click', event);
        };
      }, [
        data,
        ref.current?.graph,
      ]);
      // 渲染血缘图
      return (
        <div>
          <Graphin
            data={data}
            ref={ref as React.LegacyRef<Graphin>}
            layout={{
              type: 'dagre',
              rankdir: 'LR',
              align: 'DL',
              nodesep: 10,
              ranksep: 40,
            }}
          >
            <ActivateRelations />
            <DragNode disabled />
            <ZoomCanvas enableOptimize />
          </Graphin>
        </div>
      );
    };
    
    export default LineageContent;
                            
  4. 完成上述代码开发后,您可在本地部署并运行工程代码。部署并运行的操作请参见通用操作:本地部署运行
完成上述实践步骤后,您可以实现表血缘关系展示以及上下钻能力,如下图所示。查询血缘

实践4:创建表

以下实践使用了CreateTable来实现创建表,实践流程如下。

  1. 在MetaServiceProxy中编写一个createTable方法来调用OpenAPI并处理入参与出参。
    package com.aliyun.dataworks.services;
    
    import com.aliyun.dataworks.dto.*;
    import com.aliyuncs.IAcsClient;
    import com.aliyuncs.dataworks_public.model.v20200518.*;
    import com.aliyuncs.exceptions.ClientException;
    import com.aliyuncs.exceptions.ServerException;
    import com.aliyuncs.utils.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.util.CollectionUtils;
    
    /**
     * @author dataworks demo
     */
    @Service
    public class MetaServiceProxy {
      @Autowired
      private DataWorksOpenApiClient dataWorksOpenApiClient;
      /**
       * DataWorks OpenAPI : CreateTable
       * Link : https://help.aliyun.com/document_detail/185656.html
       *
       * @param createTableDto
       */
      public boolean createTable(CreateTableDto createTableDto) {
        try {
          IAcsClient client = dataWorksOpenApiClient.createClient();
          CreateTableRequest createTableRequest = new CreateTableRequest();
          // 指定创建视图或创建表:
          // 0为创建表。
          // 1为创建视图。
          createTableRequest.setIsView(createTableDto.getIsView());
          // 表或工作空间是否可见:
          // 0为表和工作空间均不可见。
          // 1为表和工作空间均可见。
          // 2为仅工作空间可见。
          createTableRequest.setVisibility(createTableDto.getVisibility());
          // 表的生命周期。默认取值为空,表示永久存储。
          createTableRequest.setLifeCycle(createTableDto.getLifeCycle());
          // 关联类目的ID。
          createTableRequest.setCategoryId(createTableDto.getCategoryId());
          // 逻辑层级ID。
          createTableRequest.setLogicalLevelId(createTableDto.getLogicalLevelId());
          // 物理层级ID。
          createTableRequest.setPhysicsLevelId(createTableDto.getPhysicsLevelId());
          // 外部表的存储类型。取值如下:
          // 0表示OSS。
          // 1表示TableStore。
          // 2表示Volume。
          // 3表示MySQL。
          createTableRequest.setExternalTableType(createTableDto.getExternalTableType());
          // 外部表的存储地址。
          createTableRequest.setLocation(createTableDto.getLocation());
          // DataWorks工作空间的ID。
          createTableRequest.setProjectId(createTableDto.getProjectId());
          // 表的名称。
          createTableRequest.setTableName(createTableDto.getTableName());
          // MaxCompute的Endpoint。
          createTableRequest.setEndpoint(createTableDto.getEndpoint());
          // DataWorks工作空间的环境。取值如下:
          // 0表示开发环境。
          // 1表示生产环境。
          createTableRequest.setEnvType(createTableDto.getEnvType());
          // MaxCompute项目的ID,格式为odps.{projectName}。
          createTableRequest.setAppGuid(createTableDto.getAppGuid());
          // 备注信息。
          createTableRequest.setComment(createTableDto.getComment());
          // 创建的MaxCompute表是否为分区表,包括1(是)和0(否)。该字段已废弃,请勿使用。
          createTableRequest.setHasPart(createTableDto.getHasPart());
          // 保留字段。
          createTableRequest.setClientToken(createTableDto.getClientToken());
          if (!CollectionUtils.isEmpty(createTableDto.getColumns())) {
            int i = 0;
            for (CreateTableRequest.Columns columns : createTableDto.getColumns()) {
              columns.setSeqNumber(++i);
            }
            // 字段属性
            createTableRequest.setColumnss(createTableDto.getColumns());
          }
    
          // 主题属性
          createTableRequest.setThemess(createTableDto.getThemes());
          CreateTableResponse acsResponse = client.getAcsResponse(createTableRequest);
          // 当前执行的子任务的状态信息。取值如下:
          // operating表示子任务正在执行中。
          // success表示子任务执行成功。
          // failure表示子任务执行失败。详细的报错信息请参见Content参数。
          System.out.println(acsResponse.getTaskInfo().getStatus());
          // 即将执行的子任务ID。如果该字段为空,则表示所有子任务均已结束。
          System.out.println(acsResponse.getTaskInfo().getNextTaskId());
          // 当前执行的子任务ID。
          System.out.println(acsResponse.getTaskInfo().getTaskId());
          // 当前子任务的执行状态详细信息。具体如下:
          // 执行成功,则显示success。
          // 执行失败则显示对应的报错详情。
          System.out.println(acsResponse.getTaskInfo().getContent());
          String taskId = acsResponse.getTaskInfo().getNextTaskId();
          // 保证文件异步处理成功
          if (taskId != null) {
            Boolean isTrue = true;
            while (isTrue) {
              GetDDLJobStatusRequest getDDLJobStatusRequest = new GetDDLJobStatusRequest();
              // 任务的ID。
              getDDLJobStatusRequest.setTaskId(taskId);
              GetDDLJobStatusResponse jobAcsResponse = client.getAcsResponse(getDDLJobStatusRequest);
              // 正在进行的TaskID。如果为空,说明全部任务已经结束。
              System.out.println(jobAcsResponse.getData().getNextTaskId());
              if (jobAcsResponse.getData().getNextTaskId() != null) {
                taskId = jobAcsResponse.getData().getNextTaskId();
              } else {
                isTrue = false;
              }
            }
          }
          return true;
        } catch (ServerException e) {
          e.printStackTrace();
        } catch (ClientException e) {
          e.printStackTrace();
          // 请求ID
          System.out.println(e.getRequestId());
          // 错误码
          System.out.println(e.getErrCode());
          // 错误信息
          System.out.println(e.getErrMsg());
        }
        return false;
      }
    }
  2. 在MetaRestController中提供一个createTable入口供前端调用。
    package com.aliyun.dataworks.demo;
    
    import com.aliyun.dataworks.dto.*;
    import com.aliyun.dataworks.services.MetaServiceProxy;
    import com.aliyuncs.dataworks_public.model.v20200518.*;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.bind.annotation.CrossOrigin;
    
    
    /**
     * 演示如何通过DataWorks OpenAPI构建自定义元数据平台
     *
     * @author dataworks demo
     */
    @RestController
    @RequestMapping("/meta")
    public class MetaRestController {
      @Autowired
      private MetaServiceProxy metaService;
      /**
       * 创建表
       *
       * @param createTableDto
       * @return
       */
      @CrossOrigin(origins = "http://localhost:8080")
      @PostMapping("/createTable")
      public boolean createTable(CreateTableDto createTableDto) {
          return metaService.createTable(createTableDto);
      }
    }
  3. 在前端实现一个简易的表单来填写创建表的相关信息。
    import React from 'react';
    import { Form, Input, Field, NumberPicker, Radio, Dialog, Transfer, Message } from '@alifd/next';
    import type { DialogProps } from '@alifd/next/types/dialog';
    import type { CreateTableInput } from '../services/meta';
    import * as services from '../services';
    
    export interface Props extends DialogProps {
      appGuid: string;
    }
    
    const formItemLayout = {
      labelCol: {
        fixedSpan: 6
      },
      wrapperCol: {
        span: 18
      },
    };
    // 表的字段选单
    const columnsDataSource = [
      { label: '性别', value: 'sex', type: 'string', comment: '性别' },
      { label: '年龄', value: 'age', type: 'bigint', comment: '年龄' },
      { label: '星座', value: 'constellation', type: 'string', comment: '星座' },
      { label: '职业', value: 'job', type: 'string', comment: '职业' },
      { label: '是否为学生', value: 'isStudent', type: 'boolean', comment: '是否为学生' },
    ];
    const { TextArea } = Input;
    const { Item } = Form;
    const { Group } = Radio;
    const CreateTableDialog: React.FunctionComponent<Props> = (props) => {
      const [loading, setLoading] = React.useState<boolean>(false);
      const field = Field.useField();
      // 点击提交时触发
      const createTable = (event: React.MouseEvent<Element, MouseEvent>) => {
        field.validate(async (errors, values) => {
          if (errors) {
            return;
          }
          try {
            setLoading(true);
            const columns = (values as any).columns.map((column: any) => {
              const result = columnsDataSource.find(i => i.value === column);
              return {
                columnNameCn: result!.label,
                columnName: result!.value,
                comment: result!.comment,
                columnType: result!.type,
              };
            });
            const nextValues = Object.assign({}, values, { columns });
            // 调用后端接口
            const response = await services.meta.createTable(nextValues as CreateTableInput);
            response ? props.onOk?.(event) : Message.error('创建失败');
          } catch (e) {
            throw e;
          } finally {
            setLoading(false);
          }
        });
      };
      React.useEffect(() => {
        field.setValue('appGuid', props.appGuid);
      }, [
        props.appGuid,
      ]);
      // 渲染表单
      return (
        <Dialog
          title="创建表"
          visible={props.visible}
          onOk={createTable}
          onCancel={props.onCancel}
          onClose={props.onClose}
          okProps={{
            loading,
            disabled: loading,
          }}
          cancelProps={{
            disabled: loading,
          }}
          width={614}
          v2
        >
          <Form {...formItemLayout} field={field} labelTextAlign="left" colon>
            <Item name="tableName" label="表名称" required>
              <Input disabled={loading} />
            </Item>
            <Item name="lifeCycle" label="生命周期">
              <NumberPicker style={{ width: '100%' }} disabled={loading} />
            </Item>
            <Item name="envType" label="环境">
              <Group disabled={loading} defaultValue={1}>
                <Radio label="开发" value={0} />
                <Radio label="生产" value={1} />
              </Group>
            </Item>
            <Item name="comment" label="备注">
              <TextArea disabled={loading} />
            </Item>
            <Item name="columns" label="表字段">
              <Transfer
                mode="simple"
                dataSource={columnsDataSource}
              />
            </Item>
          </Form>
        </Dialog>
      );
    };
    
    export default CreateTableDialog;
                            
  4. 完成上述代码开发后,您可在本地部署并运行工程代码。部署并运行的操作请参见通用操作:本地部署运行
完成上述实践步骤后,您可以实现创建表,如下图所示。创建表

通用操作:本地部署运行

  1. 准备依赖环境。
    您需准备好以下依赖环境:java8及以上、maven构建工具、node环境、pnpm工具。您可以执行以下命令来确定是否具备上述环境:
    npm -v // 如果已安装成功,执行命令将展示版本号,否则会报没有命令错误
    java -version // 如果已安装成功,执行命令将展示版本号,否则会报没有命令错误
    pnpm -v // 如果已安装成功,执行命令将展示版本号,否则会报没有命令错误
  2. 下载工程代码并执行以下命令。
    工程代码下载链接:meta-api-demo.zip
    pnpm i
  3. 在范例工程中的backend/src/main/resources路径下找到application.properties文件,修改文件中的核心参数。
    • api.access-key-id与api.access-key-secret需修改为您阿里云账号的AccessKey ID 和 AccessKey Secret。
      说明 您可以在AccessKey 管理页面获取阿里云账号的相关信息。
    • api.region-id、api.endpoint需修改为待调用OpenAPI的地域信息。
    其他参数可根据实际情况修改,修改后的填写示例如下。本地部署示例
  4. 在工程根目录下执行以下命令,运行示例实践代码。
    npm run dev