向JavaScript自定义中级开发者的目标前进(5)〜TypeScript导入篇〜

cybozu发表于:2021年03月10日 14:42:58更新于:2021年09月06日 17:49:13

Index

前言

为了更加便捷地在kintone中使用TypeScript,这次我们将演练如何使用@kintone/dts-genkintone JavaScript Client (@kintone/rest-api-client)这两个工具来编写TypeScript。
关于如何用TypeScript来自定义kintone的基本方法,本网站的另一篇文章使用TypeScript开发kintone自定义中有所介绍。大家可以自行参考。

什么是TypeScript?TypeScript的优点

TypeScript是Microsoft开发的开源编程软件。使得在JavaScript中可以添加变量的类型。

“类型”指的是为了明确变量中所赋的值,是数值,还是字符串等种类而事先声明的。
除了数值、字符串等基本类型以外,用户还可以声明自定义类型。
因为有了“类型”信息,所以即使不去读取变量中的数据,也可以在编写代码时就知道数据类型,这样可以避免很多bug的产生。

例如:从kintone API中获取到的数值、计算字段的数字其实不是数值型而是字符串,直接进行乘法运算的话就会被预判为错误。

  • 对数值、计算字段直接进行乘法运报错的例子
    计算字段的「合计金额」  "record.合计金额.value * 0.1" 中的乘法运算会显示为error。
    0016094e79d75cd6606a4ed2c55fdbc

除此之外,类似对象中没有指定的key之类的也会被IDE判断为error。

  • 访问对象时error的例子。
    给对象 "record.字符串.value" 赋值时,由于实际上不存在“字符串”这个字段,所以显示了error。
    0016094e79d4aad1dad1fd7ec00d4e9

TypeScript准备了kintone应用中各个字段的类型信息,基于这些信息,可以避免上述kintone字段代码的错误输入。
特别是处理表格中深层复杂的结构、REST API 的request参数、response时可以发挥明显的效果。

实际操作起来会如何呢?让我们来试一下。

准备

代码:https://github.com/cybozudevnet/sample-kintone-webpack-for-intermediate

通过点击绿色按钮可获得链接地址来执行 git clone 或下载 Zip 文件。

有关后续部署,请参阅上面的自述。

上述代码的URL在向JavaScript自定义中级开发者的目标前进(1) 〜webpack篇〜到(4)中所提到的都是同一代码。
在之前的文章中已经试过的用户,为以防万一,可以在目录下再次执行 npm install。

设置的细节之后会介绍,到此为止使用TypeScript所需要的包都安装完毕了。

范例

我们使用第4篇文章向JavaScript自定义中级开发者的目标前进(4)〜kintone REST API Client篇〜中的范例,就如何用TypeScript改写来演示一下。
使用到的应用也是相同的,所以请大家按第4篇来配置好应用。

“类型”信息的获取

在实际编写代码前,需要事先定义好应用中的字段类型。使用 @kintone/dts-gen 这个库来获取应用字段的类型。
执行下列代码,获取报价单的字段类型。所生成的文件叫做类型定义文件。

在此范例中,我们已经放置了类型定义文件。 通过执行下列代码,您环境中的应用的字段类型会覆盖类型定义文件。

npx @kintone/dts-gen --host https://kintone的域名.cybozu.cn/ -u 用户名 -p 密码 --app-id 应用ID --type-name Quote --namespace KintoneTypes -o src/types/Quote.d.ts

这次的范例中,使用了报价单应用和商品应用,但只用JavaScriptAPI改写报价单应用的值、所以用 @kintone/dts-gen 只生成了报价单应用字段的类型定义文件。商品应用使用REST API来改写,所以需要另外在范例大妈中定义类型。

命令执行成功后,会生成像下面这样的文件

文件: src/types/Quote.d.ts

declare namespace KintoneTypes {
  interface Quote {
    备注: kintone.fieldTypes.MultiLineText;
    报价日期: kintone.fieldTypes.Date;
    地址: kintone.fieldTypes.SingleLineText;
    报价单号: kintone.fieldTypes.SingleLineText;
    合计金额: kintone.fieldTypes.Calc;
    报价明细: {
      type: "SUBTABLE";
      value: {
        id: string;
        value: {
          数量: kintone.fieldTypes.Number;
          型号: kintone.fieldTypes.SingleLineText;
          price: kintone.fieldTypes.Number;
          商品名: kintone.fieldTypes.SingleLineText;
          小计: kintone.fieldTypes.Calc;
        };
      }[];
    };
  }
  interface SavedQuote extends Quote {
    $id: kintone.fieldTypes.Id;
    $revision: kintone.fieldTypes.Revision;
    更新人: kintone.fieldTypes.Modifier;
    创建人: kintone.fieldTypes.Creator;
    记录编号: kintone.fieldTypes.RecordNumber;
    更新时间: kintone.fieldTypes.UpdatedTime;
    创建时间: kintone.fieldTypes.CreatedTime;
  }
}

范例代码

文件: src/apps/quote_ts/index.ts

import {KintoneRestAPIClient, KintoneRecordField} from '@kintone/rest-api-client';

// 商品应用的类型定义
type SavedProduct = {
  $id: KintoneRecordField.ID;
  $revision: KintoneRecordField.Revision;
  更新人: KintoneRecordField.Modifier;
  创建人: KintoneRecordField.Creator;
  记录编号: KintoneRecordField.RecordNumber;
  更新时间: KintoneRecordField.UpdatedTime;
  创建时间: KintoneRecordField.CreatedTime;
  service_type: KintoneRecordField.RadioButton;
  note: KintoneRecordField.MultiLineText;
  型号: KintoneRecordField.SingleLineText;
  product_name: KintoneRecordField.SingleLineText;
  price: KintoneRecordField.Number;
  在库数量: KintoneRecordField.Number;
}

// 请输入商品列表的应用ID
const productsAppId = 122;

const events = ['app.record.create.submit', 'app.record.edit.submit'];

kintone.events.on(events, async (event) => {
  const record = event.record as KintoneTypes.Quote;
  record.合计金额.value
  // 创建一个连接kintone的实例
  const client = new KintoneRestAPIClient({});

  // 这次为了简便,表中的商品不允许重复。
  // 只是简易的重复检查,不理解也没关系。
  const hasDuplicatedRow = record.报价明细.value.some((rowA, indexA, arr) => {
    return arr.find(
      (rowB, indexB) =>
        indexA !== indexB && rowA.value.型号.value === rowB.value.型号.value
    );
  });
  if (hasDuplicatedRow) {
    event.error = '不允许选择重复的商品';
    return event;
  }

  // 获取表中的商品记录
  let products;
  try {
    // 通过指定泛型,在使用products变量时,可以推断出类型
    products = await client.record.getRecords({
      app: productsAppId,
      query: `型号 in (${record.报价明细.value
        .map((row) => `"${row.value.型号.value}"`)
        .join(', ')})`,
    });
  } catch (error) {
    event.error = '获取记录失败';
    return event;
  }

  // 在商品列表的库存中减去相应数量
  const deductedProductRecords = products.records.map((productRecord) => {
    const tableRow = record.报价明细.value.find(
      (row) => productRecord.型号.value === row.value.型号.value
    );

    // 存放型号值和计算后的库存值
    return {
      型号: {
        value: productRecord.型号.value,
      },
      在库数量: {
        value:
          Number(productRecord.在库数量.value) -
          Number(tableRow?.value.数量.value),
      },
    };
  });

  // 计算后的库存值是否有小于0的情况
  const noStockRecords = deductedProductRecords.filter(
    (productRecord) => Number(productRecord.在库数量.value) < 0
  );

  // 存在1条以上记录时报错并跳过保存
  if (noStockRecords.length > 0) {
    // event.error中存放错误信息后返回
    // 列出出问题的商品型号
    event.error = `存在库存不够的商品。型号 ${noStockRecords
      .map((productRecord) => productRecord.型号.value)
      .join(', ')}`;

    return event;
  }

  // 没有问题的话更新
  try {
    await client.record.updateRecords({
      app: productsAppId,
      records: deductedProductRecords.map((productRecord) => {
        return {
          updateKey: {
            field: '型号',
            value: productRecord.型号.value,
          },
          record: {
            在库数量: {
              value: productRecord.在库数量.value,
            },
          },
        };
      }),
    });
  } catch (error) {
    event.error = `更新失败 ${error.message}`;
    return event;
  }

  return event;
});

请实际编辑一下,尝试查看27行附近的record中的内容,然后看看Visual Studio Code会发生什么吧。

就像下方的图片那样,输入「record.」后,报价应用的字段就会出现在提示框中了。

0016094e79d9877ef0b8c3860922c55

范例代码的说明

由于篇幅关系不能表尽TypeScript,在此就范例代码大致要点做一些介绍。

其实,和第四篇所介绍的代码基本没有什么不同。如下所示,只有类型信息的处理上有所不同。

  • 第26行: 申明event.record的类型信息
    0016094e79d0c5eb1930de6ebaf2e99

    const record = event.record as kintoneTypes.Quote;

    这里指的是之前讲到的用 @kintone/dts-gen 所作成的类型。这么做可以定义「event.record 是报价应用的的记录哦」这件事。
    这就是所谓的类型断言(日文)

  • 第4行〜第18行: 产品应用的类型定义(@kintone/rest-api-client)

    实际上@kintone/rest-api-client是支持TypeScript的。
    但是,@kintone/rest-api-client 的类型定义不是像 @kintone/dts-gen 一样是用命令行完成的。
    所以,必须像这样准备好产品应用自身的类型。
    @kintone/rest-api-client的类型定义的详细方法请参照这里

  • 第48行: 向 @kintone/rest-api-client 里传递类型信息
    0016094e79dc3843e196f062ce24fe9

    products = await client.record.getRecords<SavedProduct>({

    这一行,通过把第4行~第18行所定义的产品应用的类型信息(SavedProduct)传递过去,告诉我们得到getRecords()所返回的记录是SavedProduct类型。
    这样,REST API 所返回的记录就会得到提示框的支持了。

范例代码的编译

我们不能在浏览器里直接执行TypeScript,但可以通过webpack把TypeScript转换成JavaScript。
输入以下命令,转换完之后再上传。

npx webpack --mode production

详细信息请参照向JavaScript自定义中级开发者的目标前进(3) 〜自动批量上传文件篇〜还具有自动上传功能。

结束语

把 TypeScript 和 kintone 的生态环境相互酝酿结合,得到了相当不错的开发体验。
在处理 kintone 字段的过程中,不可避免地需要进行对照字段代码等繁琐又容易出错的工作。而使用 TypeScript 的特性,便可将此防范于未然,对于开发者来说是一大福音。

如果对本篇文章感兴趣的话,十分建议借此契机开始学习 TypeScript。TypeScript 如今备受瞩目,相信将来一定可以在开发中起到非常重要的作用。

该Tips在 kintone 2021年2月版,@kintone/rest-api-client@1.10.0 中进行过确认。

附件:2021-05-07 14_37_35-index.ts - sample-kintone-webpack-for-intermediate-master - Visual Studio Code.png • 31.25KB • 下载

附件:2021-05-07 14_01_37-● index.ts - sample-kintone-webpack-for-intermediate-master - Visual Studio Code.png • 26.37KB • 下载

附件:2021-05-07 14_01_03-● index.ts - sample-kintone-webpack-for-intermediate-master - Visual Studio Code.png • 34.11KB • 下载

附件:typesuggest.gif • 200.09KB • 下载

附件:2021-05-07 13_56_35-index.ts - sample-kintone-webpack-for-intermediate-master - Visual Studio Code.png • 72.15KB • 下载

附件:sample-kintone-webpack-for-intermediate-master.zip • 111.22KB • 下载