Kintoneは、プログラミング経験が少ないユーザーでも業務アプリを簡単に作成できるプラットフォームとして、多くの企業で利用されています。しかし、より高度で柔軟なカスタマイズが求められる場面では、JavaScriptやTypeScriptを使った開発が必要になります。この記事では、WebpackとTypeScriptを使ってKintoneのカスタマイズを行う方法を解説します。
なぜWebpackとTypeScriptを使うのか?
まずは、なぜWebpackとTypeScriptを使用するのかを説明します。
1. TypeScriptの利点
JavaScriptは柔軟で強力な言語ですが、コードが複雑になるとバグが発生しやすくなります。TypeScriptを使うと、型定義によりコードの保守性と信頼性が向上します。型チェックによって開発時に多くのエラーを未然に防ぐことができるため、特に大規模なプロジェクトではTypeScriptの導入が推奨されます。
2. Webpackの役割
Webpackは、複数のJavaScriptやTypeScriptファイルを1つにまとめるモジュールバンドラです。これにより、ファイルを効率的に管理・読み込むことができ、Kintoneのようなウェブアプリケーションのパフォーマンスが向上します。また、WebpackはBabelやTypeScriptコンパイラと連携して、最新のコードを古いブラウザでも動作するようにトランスパイルできます。
環境設定
まず、Node.jsがインストールされていることを確認します。その後、プロジェクトのセットアップを行います。
npm init -y
npm install --save-dev webpack webpack-cli typescript ts-loader これで、Webpack、TypeScript、そしてTypeScriptファイルをWebpackで処理するためのts-loaderがインストールされます。
TypeScriptの設定
次に、TypeScriptの設定ファイルであるtsconfig.jsonをプロジェクトのルートに作成します。以下の設定を使用します。
{
"compilerOptions": {
"target": "ES6",
"module": "ESNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"typeRoots": ["./node_modules/@types", "./src/types"]
},
"include": ["src/**/*"]
} この設定により、生成されるJavaScriptはES6(ECMAScript 2015)に準拠し、TypeScriptの強力な型チェック機能を活用できます。※./src/types はあとで作成
Webpackの設定
次に、Webpackの設定ファイルwebpack.config.jsを作成します。基本的な設定は以下の通りです。
const path = require('path');
module.exports = {
entry: './src/index.ts',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
resolve: {
extensions: ['.ts', '.js']
},
module: {
rules: [
{
test: /\.ts$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
}
}; ここでは、entryにアプリケーションのエントリーポイントであるTypeScriptファイルを指定し、outputにはバンドルされたファイルの出力先を指定しています。
Kintone用のTypeScriptコードを書く
srcフォルダ内にindex.tsファイルを作成し、Kintoneカスタマイズ用のコードを記述します。例えば、レコードが追加された際にアラートを表示するカスタマイズコードは以下の通りです。
const events = ['app.record.create.submit'];
kintone.events.on(events, (event) => {
alert('レコードが追加されました');
return event;
});
このままだとkintoneで型エラーが起きるのでkintone.d.ts を作成します。
srcの配下にtypesというディレクトリを作成しkintone.d.ts を作成してください。
declare namespace kintone {
namespace events {
function on(event: string | string[], handler: (event: any) => any): void;
function off(
event: string | string[],
handler: (event: any) => any
): boolean;
function off(event: string | string[]): boolean;
function off(): boolean;
}
namespace api {
function url(path: string, detectGuestSpace?: boolean): string;
function urlForGet(
path: string,
params: any,
detectGuestSpace?: boolean
): string;
function getConcurrencyLimit(): Promise<{
limit: number;
running: number;
}>;
}
function api(pathOrUrl: string, method: string, params: any): Promise<any>;
function api(
pathOrUrl: string,
method: string,
params: any,
callback: (resp: any) => void,
errback: (err: any) => void
): void;
function getRequestToken(): string;
function proxy(
url: string,
method: string,
headers: any,
data: any
): Promise<any>;
function proxy(
url: string,
method: string,
headers: any,
data: any,
callback: (resp: any) => void,
errback: (err: any) => void
): void;
class Promise<T> {
constructor(
callback: (
resolve: (resolved: T) => any,
reject: (rejected: any) => any
) => void
);
then(callback: (resolved: T) => any): Promise<any>;
catch(callback: (rejected: any) => any): Promise<any>;
static resolve(resolved: any): Promise<any>;
static reject(rejected: any): Promise<any>;
static all(listOfPromise: Array<Promise<any>>): Promise<any>;
}
namespace proxy {
function upload(
url: string,
method: string,
headers: any,
data: any,
callback: (resp: any) => void,
errback: (err: any) => void
): void;
function upload(
url: string,
method: string,
headers: any,
data: any
): Promise<any>;
}
namespace app {
function getFieldElements(fieldCode: string): HTMLElement[] | null;
function getHeaderMenuSpaceElement(): HTMLElement | null;
function getHeaderSpaceElement(): HTMLElement | null;
function getId(): number | null;
function getLookupTargetAppId(fieldCode: string): string | null;
function getQuery(): string | null;
function getQueryCondition(): string | null;
function getRelatedRecordsTargetAppId(fieldCode: string): string | null;
namespace record {
function getId(): number | null;
function get(): any | null;
function getHeaderMenuSpaceElement(): HTMLElement | null;
function getFieldElement(fieldCode: string): HTMLElement | null;
function set(record: any): void;
function getSpaceElement(id: string): HTMLElement | null;
function setFieldShown(fieldCode: string, isShown: boolean): void;
function setGroupFieldOpen(fieldCode: string, isOpen: boolean): void;
}
}
namespace mobile {
namespace app {
function getFieldElements(fieldCode: string): HTMLElement[] | null;
function getHeaderSpaceElement(): HTMLElement | null;
function getId(): number | null;
function getLookupTargetAppId(fieldCode: string): string | null;
function getQuery(): string | null;
function getQueryCondition(): string | null;
function getRelatedRecordsTargetAppId(fieldCode: string): string | null;
namespace record {
function getId(): number | null;
function get(): any | null;
function getFieldElement(fieldCode: string): HTMLElement | null;
function set(record: any): void;
function getSpaceElement(id: string): HTMLElement | null;
function setFieldShown(fieldCode: string, isShown: boolean): void;
function setGroupFieldOpen(fieldCode: string, isOpen: boolean): void;
}
}
namespace portal {
function getContentSpaceElement(): HTMLElement | null;
}
namespace space {
namespace portal {
function getContentSpaceElement(): HTMLElement | null;
}
}
}
namespace plugin {
namespace app {
function getConfig(pluginId: string): any;
function setConfig(config: any, callback?: () => void): void;
function proxy(
pluginId: string,
url: string,
method: string,
headers: any,
data: any
): Promise<any>;
function proxy(
pluginId: string,
url: string,
method: string,
headers: any,
data: any,
callback: (resp: any) => void,
error: (err: any) => void
): void;
function setProxyConfig(
url: string,
method: string,
headers: any,
data: any,
callback?: () => void
): void;
function getProxyConfig(url: string, method: string): any;
namespace proxy {
function upload(
pluginId: any,
url: string,
method: string,
headers: any,
data: any
): Promise<any>;
function upload(
pluginId: any,
url: string,
method: string,
headers: any,
data: any,
callback: (resp: any) => void,
error: (err: any) => void
): void;
}
}
}
namespace portal {
function getContentSpaceElement(): HTMLElement | null;
}
namespace space {
namespace portal {
function getContentSpaceElement(): HTMLElement | null;
}
}
interface LoginUser {
id: string;
code: string;
name: string;
email: string;
url: string;
employeeNumber: string;
phone: string;
mobilePhone: string;
extensionNumber: string;
timezone: string;
isGuest: boolean;
language: string;
}
function getLoginUser(): LoginUser;
function getUiVersion(): 1 | 2;
const $PLUGIN_ID: string;
namespace fieldTypes {
interface SingleLineText {
type?: "SINGLE_LINE_TEXT";
value: string;
disabled?: boolean;
error?: string;
}
interface RichText {
type?: "RICH_TEXT";
value: string;
disabled?: boolean;
error?: string;
}
interface MultiLineText {
type?: "MULTI_LINE_TEXT";
value: string;
disabled?: boolean;
error?: string;
}
interface Number {
type?: "NUMBER";
value: string;
disabled?: boolean;
error?: string;
}
interface Calc {
type: "CALC";
value: string;
disabled?: boolean;
}
interface RadioButton {
type?: "RADIO_BUTTON";
value: string;
disabled?: boolean;
error?: string;
}
interface DropDown {
type?: "DROP_DOWN";
value: string;
disabled?: boolean;
error?: string;
}
interface Date {
type?: "DATE";
value: string;
disabled?: boolean;
error?: string;
}
interface Time {
type?: "TIME";
value: string;
disabled?: boolean;
error?: string;
}
interface DateTime {
type?: "DATETIME";
value: string;
disabled?: boolean;
error?: string;
}
interface Link {
type?: "LINK";
value: string;
disabled?: boolean;
error?: string;
}
interface CheckBox {
type?: "CHECK_BOX";
value: string[];
disabled?: boolean;
error?: string;
}
interface MultiSelect {
type?: "MULTI_SELECT";
value: string[];
disabled?: boolean;
error?: string;
}
interface UserSelect {
type?: "USER_SELECT";
value: Array<{ code: string; name: string }>;
disabled?: boolean;
error?: string;
}
interface OrganizationSelect {
type?: "ORGANIZATION_SELECT";
value: Array<{ code: string; name: string }>;
disabled?: boolean;
error?: string;
}
interface GroupSelect {
type?: "GROUP_SELECT";
value: Array<{ code: string; name: string }>;
disabled?: boolean;
error?: string;
}
interface File {
type: "FILE";
value: Array<{
contentType: string;
fileKey: string;
name: string;
size: string;
}>;
disabled?: boolean;
error?: string;
}
interface Id {
type: "__ID__";
value: string;
}
interface Revision {
type: "__REVISION__";
value: string;
}
/**
* field type of UserField is MODIFIER.
* So error property not exists.
*/
interface Modifier {
type: "MODIFIER";
value: { code: string; name: string };
}
/**
* field type of UserField is CREATOR.
* So error property not exists.
*/
interface Creator {
type: "CREATOR";
value: { code: string; name: string };
}
interface RecordNumber {
type: "RECORD_NUMBER";
value: string;
error?: string;
}
interface UpdatedTime {
type: "UPDATED_TIME";
value: string;
error?: string;
}
interface CreatedTime {
type: "CREATED_TIME";
value: string;
error?: string;
}
}
}
カスタマイズファイルのアップロード
次に、Kintoneにカスタマイズファイルをアップロードするためのkintone-customize-uploaderをプロジェクトにインストールします。
npm install --save-dev @kintone/customize-uploader カスタマイズファイルの設定
customize-manifest.jsonというファイルをプロジェクトのルートディレクトリに作成し、アップロードするファイルや適用するアプリの設定を行います。
{
"app": アプリID,
"scope": "ALL",
"desktop": {
"js": [
"dist/bundle.js"
],
"css": []
},
"mobile": {
"js": [],
"css": []
}
} ここで、appにカスタマイズを適用するKintoneアプリのIDを指定します。
環境変数の設定
次に、dotenvパッケージを使用して環境変数を設定します。まず、dotenvパッケージをインストールします。
npm install --save-dev dotenv その後、プロジェクトのルートディレクトリに.envファイルを作成し、Kintoneの環境変数を設定します。
KINTONE_BASE_URL=https://yourdomain.kintone.com
KINTONE_USERNAME=your_username
KINTONE_PASSWORD=your_password upload.js の作成
プロジェクトルートディレクトリにupload.js を作成します。
require("dotenv").config();
const { spawn } = require("child_process");
const mode = process.env.NODE_ENV || "development";
const webpackProcess = spawn("npx", ["webpack", "--watch", "--mode", mode], { shell: true });
webpackProcess.stdout.on("data", (data) => {
console.log(`Webpack: ${data}`);
if (data.toString().includes("built")) {
console.log("Webpack build completed. Starting upload...");
const uploadProcess = spawn(
"npx",
[
"kintone-customize-uploader",
"customize-manifest.json",
"--base-url",
process.env.KINTONE_BASE_URL,
"--username",
process.env.KINTONE_USERNAME,
"--password",
process.env.KINTONE_PASSWORD,
],
{ shell: true }
);
uploadProcess.stdout.on("data", (uploadData) => {
const message = uploadData.toString();
if (message.includes("Wait for deploying completed...")) {
console.log("カスタマイズの反映を待機中...");
} else if (message.includes("Customize setting has been updated!")) {
console.log("カスタマイズ設定が更新されました!");
} else if (message.includes("Setting has been deployed!")) {
console.log("カスタマイズが正常にデプロイされました!");
} else {
console.log(`Uploader: ${message}`);
}
});
uploadProcess.stderr.on("data", (uploadError) => {
console.error(`Uploader Error: ${uploadError}`);
});
uploadProcess.on("close", (code) => {
console.log(`Uploader process exited with code ${code}`);
});
}
});
webpackProcess.stderr.on("data", (data) => {
console.error(`Webpack Error: ${data}`);
});
webpackProcess.on("close", (code) => {
console.log(`Webpack process exited with code ${code}`);
});
スクリプトの実行
package.jsonに以下のようなスクリプトを追加し、upload.jsを実行してカスタマイズをアップロードします。
"scripts": {
"start": "node upload.js"
} スクリプトを実行するには、以下のコマンドを使います。
npm run start これにより、.envファイルに設定された環境変数を使ってKintoneにカスタマイズがアップロードされます。
まとめ
WebpackとTypeScriptを使用したKintoneカスタマイズは、カスタマイズのソースをGitなどでバージョン管理もできるので、保守性が高く、効率的な開発を可能にします。
さらに、kintone-customize-uploaderを利用することで、簡単にカスタマイズをKintoneにアップロードできます。これにより、Kintoneカスタマイズの開発が一層強力で効率的なものになるので、ぜひ、試してみてください。

