Index
概要
react,tailwind,typescript 是现在前端开发中最常见的框架和技术。kintone ui component也是kintone前端自定义开发开发时构建统一ui的利器。所以我们在进行kintone定制的前端工程化的项目中会时常用到这些技术。这里就拿一个范例来说明如何在kintone插件的开发中使用这些技术。
插件目录结构分析
首先我们来了解下kintone插件开发的目录结构,然后对他的目录结构进行一些分析。
src为主目录。
image目录是插件的图标,无需改动。
js目录则是我们编写的jsx(tsx)代码最后编译输出的地址。
css部分可以直接通过styled-components或者tailwind框架来写在js中,或者不做修改,直接引用。
html部分作为插件设置画面,我们可以直接作为我们组件挂载的根节点。设置一个空的div即可。
比如
config.html
<div id='plugin-config-root'></div>
最后通过ReactDOM.createRoot() 进行挂载。
ReactDOM.createRoot( document.getElementById("plugin-config-root") );
项目实战
接下来,我们来看下在具体项目中应用。
需求分析:
我们在做插件开发时,常常会开发这样一个插件。需要对接外部系统。而外部系统往往提供了rest api。而且规范的外部api,往往提供了api token等方式来进行授权访问。
此时我们开发插件需要将api token等敏感信息放在插件中。然后在前台使用kintone.proxy.api ()来发起代理请求,以此达到隐藏token的目的,从而提高安全性。详情参见:https://cybozudev.kf5.com/hc/kb/article/1314677/
所以这次插件的需求是插件设置界面可以设置请求地址,以及token。
前台假设设计了一个同步按钮,模拟发起点击后会调用外部服务器,获取外部数据。
这里我们建立一个kintone应用用来模拟外部服务。同时开启api令牌,并使用rest api 来获取它的数据。
插件后台设置画面:
应用前台画面
准备应用
首先建立两个应用
应用名:appA (作为kintone的应用)
配置
字段名 | 字段类型 | 字段代码 |
---|---|---|
标题 | 单行文本框 | title |
详情 | 多行文本框 | detail |
应用名:outer app (模拟外部服务器,可以接受数据同步)
字段名 | 字段类型 | 字段代码 |
---|---|---|
标题 | 单行文本框 | title |
详情 | 多行文本框 | detail |
id | 单行文本框 | id |
然后在outer app中设置API令牌
插件开发
初始化安装
首先我们来初始化项目,并安装依赖
# 初始化 npm init # 安装webpack,typescript,ts-loader npm i -D webpack webpack-cli ts-loader typescript # 安装react,react-dom以及对应的类型 npm i react react-dom npm i -D @types/react @types/react-dom # 安装webpack-plugin-kintone-plugin插件,实现自动打包已经上传 npm i -D @kintone/webpack-plugin-kintone-plugin # 安装styled-components,以实现css-in-js的写法 npm i -D styled-components # 安装kintone-ui-component npm i -D @kintone/kintone-ui-component
manifest.json配置
manifest.json
{ "manifest_version": 1, "version": 1, "type": "APP", "desktop": { "js": ["js/desktop.js"], "css": ["css/51-modern-default.css", "css/desktop.css"] }, "icon": "image/icon.png", "config": { "html": "html/config.html", "js": ["js/config.js"], "css": ["css/51-modern-default.css", "css/config.css"] }, "name": { "en": "外部同步demo", "ja": "外部同步demo", "zh": "外部同步demo" }, "description": { "en": "外部同步demo", "ja": "外部同步demo", "zh": "外部同步demo" } }
webpack的设置
其中插件后台和应用前台的逻辑并不相干,所以我们将他们做为多入口打包配置。
在entry中定义应用前台(desktop)和插件配置(config)的入口地址
webpack.config.js
entry: { desktop: "./src/desktop/index.tsx", config: "./src/config/index.tsx", }, output: { path: path.resolve(__dirname, "plugin", "js"), filename: "[name].js", },
接下来,我们只需要在src下的desktop和config目录下编写代码即可。我们能愉快的使用模块化方式进行开发了。
kintone ui component 转 react组件
此时我们还有一个问题,就是他是以web component的组件方式来编写的。所以我们没法直接将他当作react组件使用。我们需要用转化工具@lit/react来将kuc的组件转化成react组件。
安装
npm install @lite/react
使用
然后我们来建立一个目录。将kuc的组件转化成react组件后导出
示例代码:
kucComponent/Button.tsx
import React from "react"; import { createComponent } from "@lit/react"; import { Button } from "kintone-ui-component"; import { kucVersion } from "./common"; export const ButtonReact = createComponent({ tagName: `kuc-button-${kucVersion}`, elementClass: Button, react: React, });
最后将这些组件做一个统一的导出
kucComponent/common/index.tsx
export { NotificationReact } from "./Notification"; export { TextReact } from "./Text"; export { ButtonReact } from "./Button";
插件配置画面的开发
目录结构:
index.tsx //入口文件
config.tsx //插件配置组件
pluginContext.ts //通过react context,管理公用的状态。比如说plugin id
config/index.tsx
将我们的插件配置组件挂载到config.html上的id为"plugin-config-root"的节点。
同时通过context provider共享plugin id这个状态。
import React from "react"; import ReactDOM from "react-dom/client"; import Config from "./config"; import { PluginIdContext } from "./pluginIdContext"; const root = ReactDOM.createRoot( document.getElementById("plugin-config-root") as HTMLElement ); const PLUGIN_ID = kintone.$PLUGIN_ID; root.render( <React.StrictMode> <PluginIdContext.Provider value={PLUGIN_ID}> <Config /> </PluginIdContext.Provider> </React.StrictMode> );
config/pluginContext.ts
import { createContext } from "react"; export const PluginIdContext = createContext("");
config/config.tsx
对应之前的插件设置画面的图,我们需要在插件画面设置以下信息。
token信息(token): 外部系统的token信息
接口的地址(url):外部系统的接口预设地址
同步应用id(syncId):用来模拟外部系统的kintone 应用id
导入转化为react组件的kuc组件,同时使用了react-form-hooks来包装了表单。后期大家可以实现表单校验等。
import { useForm, Controller, SubmitHandler } from "react-hook-form"; import { FC, useContext } from "react"; import { ButtonReact, TextReact } from "../kucComponent"; import { PluginIdContext } from "./pluginIdContext"; import _ from "lodash"; type Inputs = { url: string; syncId: string; token: string; }; const Config: FC = () => { const pluginId = useContext(PluginIdContext); //kintone.plugin.app.getConfig 是js api,而且是同步的,并非异步,保证这是一个纯函数。 const settingInfo = kintone.plugin.app.getConfig(pluginId); let defalutInput: Inputs = { url: "", syncId: "", token: "", }; let headerInfo: any = null; let defaultToken: string; if (!_.isEmpty(settingInfo)) { headerInfo = kintone.plugin.app.getProxyConfig(settingInfo!.url, "GET"); defaultToken = headerInfo.headers.Authorization; defalutInput = { ...settingInfo }; } const { register, handleSubmit, control, // formState: { errors }, } = useForm<Inputs>({ defaultValues: defalutInput }); const onSubmit: SubmitHandler<Inputs> = (data) => { const apiHeader = { "X-Cybozu-API-Token": `${data.token}`, }; kintone.plugin.app.setConfig(data, () => { kintone.plugin.app.setProxyConfig(data.url, "POST", apiHeader, {}, () => { kintone.plugin.app.setProxyConfig( data.url, "GET", apiHeader, {}, () => { kintone.plugin.app.setProxyConfig( data.url, "PUT", apiHeader, {}, () => { kintone.plugin.app.setProxyConfig( data.url, "DELETE", apiHeader, {} ); } ); } ); }); }); }; const handleCancel = () => { window.location.href = `../../${kintone.app.getId()}/plugin/`; }; return ( <> <div> <h2 className="m-2 p-2 text-3xl font-bold">信息参数配置</h2> <form onSubmit={handleSubmit(onSubmit)}> <p className="kintoneplugin-row"> <Controller name="token" control={control} render={({ field }) => ( <TextReact defaultValue={defaultToken} label="token信息:" {...register("token")} {...field} ></TextReact> )} /> </p> <p className="kintoneplugin-row"> <Controller name="url" control={control} render={({ field }) => ( <TextReact label="接口的地址:" {...register("url")} {...field} ></TextReact> )} /> </p> <p className="kintoneplugin-row"> <Controller name="syncId" control={control} render={({ field }) => ( <TextReact label="同步应用id:" {...register("syncId")} {...field} ></TextReact> )} /> </p> <p className="kintoneplugin-row"> <ButtonReact className="my-button mr-4" onClick={handleCancel} text="取消" /> <ButtonReact className="my-button" onClick={handleSubmit(onSubmit)} text="保存" type="submit" /> </p> </form> </div> </> ); }; export default Config;
应用前台的开发
我们来模拟一个简单需求。
我们在kintone的应用中。添加了一个同步按钮。利用这个同步按钮,可以将kintone数据同步到外部服务器。(这边用另一个kintone应用来模拟外部服务器)。初次进入应用列表页,会显示外部服务,已经同步到的数据总数。然后点击同步后,会将kintone数据传输到这个外部服务器。
desktop 画面的入口
desktop/index.tsx
import React from "react"; import ReactDOM from "react-dom/client"; import SyncData from "./components/syncData"; interface KintoneEvent { record: kintone.types.appASavedFields; } kintone.events.on("app.record.index.show", async (event: KintoneEvent) => { ReactDOM.createRoot(kintone.app.getHeaderMenuSpaceElement()!).render( <React.StrictMode> <SyncData /> </React.StrictMode> ); return event; });
同步按钮的组件
desktop/components/syncData.tsx
import { FC } from "react"; import { ButtonReact } from "../../kucComponent"; import useSyncToOut from "../hooks/useSyncToOut"; import styled from "styled-components"; const Wrapper = styled.div` display: flex; `; const SubButton = styled(ButtonReact)` --kuc-button-width: 100px; margin-right: 30px; `; const SyncData: FC = () => { const { loading, syncStatus, handleSync } = useSyncToOut(); return ( <Wrapper> <SubButton onClick={handleSync} text="同步" disabled={loading} /> <div>syncStatus: {syncStatus}</div> </Wrapper> ); }; export default SyncData;
自定义同步到外部系统的hooks
desktop/hooks/useSyncToOut.ts
import { useEffect, useState } from "react"; import { appId, syncId, url, PLUGIN_ID } from "../constants"; type appAResult = { status: boolean; result: kintone.types.appASavedFields[]; }; type outerAppResult = { status: boolean; result: kintone.types.outerAppSavedFields[]; }; export default function useSyncToOut() { const [syncStatus, setSyncStatus] = useState<string>(""); const [loading, setLoading] = useState<boolean>(false); // 获取外部系统的记录数,简单假设获取最后一条记录的id作为同步的最终结果 // 使用kintone.proxy 模拟这是一个外部系统。 //获取外部系统 const getProxyRecords = async () => { const apiUrl = `${url}/k/v1/records.json?app=${syncId}`; let resp: outerAppResult = { status: false, result: [] }; try { const [result] = await kintone.plugin.app.proxy( PLUGIN_ID, apiUrl, "GET", {}, {} ); const records = JSON.parse(result).records; if (records.length > 0) { resp.status = true; resp.result = records; } } catch (e) { console.log(e); } return resp; }; //同步外部系统 const updateProxyRecords = async ( records: kintone.types.appASavedFields[] ) => { const updateRecords = records.map((record) => { return { id: record.$id, title: record.title, detail: record.detail, }; }); const params = { app: syncId, records: updateRecords, }; const apiUrl = `${url}/k/v1/records.json`; try { await kintone.plugin.app.proxy( PLUGIN_ID, apiUrl, "POST", { "Content-Type": "application/json" }, params ); } catch (e) { console.log(e); } }; //获取当前系统 const getRecords = async () => { const params = { app: appId, }; const result = await kintone.api( kintone.api.url("/k/v1/records", true), "GET", params ); let resp: appAResult = { status: false, result: [] }; if (result.records.length > 0) { resp.status = true; resp.result = result.records; } return resp; }; //同步:取出当前应用的数据,同步到外部系统。更新同步状态 const handleSync = async () => { setLoading(true); const { result } = await getRecords(); await updateProxyRecords(result); const message = `已同步${result.length}条记录`; setSyncStatus(message); setLoading(false); }; useEffect(() => { const getSyncStatus = async () => { const { result } = await getProxyRecords(); if (result.length > 0) { const message = `外部系统共有${result.length}条记录`; setSyncStatus(message); } else { setSyncStatus("暂无同步记录"); } }; getSyncStatus(); }, []); return { loading, syncStatus, handleSync }; }
使用
首先outer app中添加api token。然后在appA中添加插件,并且设置token以及app id。
然后在appA的应用列表页,就能显示这个同步按钮,已经同步的状态啦。
注意
当然这里只是简单的演示,并没有考虑数据的更新,删除,以及超过1次请求总数的数据分页等情况。只是通过这个简单的例子做了一个利用kintone.plugin.app.proxy()来从客户端发起代理请求到外部服务器的示例。是的,通过它,你的请求会从后端服务器进行转发,并且自动带上在插件设置中配置的头部token信息。
kintone插件的监控,自动打包,自动上传
最后通过webpack插件,来实现插件开发中的文件监控,自动打包插件,自动上传kintone等。
1 安装webpack-plugin-kintone-plugin , plugin-packer ,plugin-uploader 等插件
npm install -D @kintone/plugin-packer @kintone/plugin-uploader @kintone/webpack-plugin-kintone-plugin
2 添加一个npm脚本
scripts/npm-start.js
"use strict"; const runAll = require("npm-run-all"); runAll(["develop", "upload"], { parallel: true, stdout: process.stdout, stdin: process.stdin, }).catch(({ results }) => { results .filter(({ code }) => code) .forEach(({ name }) => { console.log(`"npm run ${name}" was failed`); }); });
配置webpack插件
const KintonePlugin = require("@kintone/webpack-plugin-kintone-plugin"); plugins: [ new KintonePlugin({ manifestJSONPath: "./plugin/manifest.json", privateKeyPath: "./private.ppk", pluginZipPath: "./dist/plugin.zip", }), ]
编辑package.json,修改scripts
"scripts": { "start": "node scripts/npm-start.js", "upload": "kintone-plugin-uploader dist/plugin.zip --watch --waiting-dialog-ms 3000", "develop": "webpack --mode development --watch", "build": "webpack --mode production" }
开发模式下,修改src下的文件,会实现自动编译,自动打包,自动上传。
注意事项
本示例代码不保证其运行。
我们不为本示例代码提供技术支持。