JavaScript でのコーディングの最良事例
コードの構成とフォーマット
• コードロジックを先頭に記述し、ヘルパー関数を末尾に記述します。
• グローバル変数を先頭で宣言します。
• サービスに 150 以上の行がある場合、コードを関数または複数のサービスに分割します。
• 変数と関数に適切な名前を付けることでコメントの数を減らし、コードを読みやすくします。
• ThingWorx Composer の組み込みの JavaScript 自動フォーマット機能を使用します。
コードの記述
• 条件文では truthy/falsy ケースを使用します。
// *** will act like a false statement ***
if (false)
if (null)
if (undefined)
if (0)
if (-0)
if (0n)
if (NaN)
if ("")
// *** will act like a true statement ***
if (true)
if ({})
if ([])
if (1)
if ("0")
if ("false")
if (new Date())
if (-1)
if (12n)
if (3.14)
if (-3.14)
if (Infinity)
if (-Infinity)
// *** you can force it into a boolean value by using the !! operator ***
// i.e.:
var myNullValue = null;
var myBooleanValue = !!myNullValue; // it will store as false
• データベースとのやり取りを最小限にするため、バッチ CRUD 処理を実行します。ただし、データベースから大量の情報が返される場合、データベースに対して単一の呼び出しを複数回行うこともできます。
// *** bad practice ***
for (let i = 0; i < Things["thingName"].GetPropertyDefinitions().length; i++) {
var newEntry = new Object();
newEntry.name = Things["thingName"].GetPropertyDefinitions().rows[i].name;
newEntry.description = Things["thingName"].GetPropertyDefinitions().rows[i].description;
result.AddRow(newEntry);
}
// GetPropertyDefinitions() is called multiple times because of the looping
// *** preferred practice ***
let propertyDefinitions = Things["thingName"].GetPropertyDefinitions();
propertyDefinitions.rows.toArray().forEach((propertyDefinition) => {
result.AddRow({
name : propertyDefinition.name,
description : propertyDefinition.description
});
});
// By storing the content of GetPropertyDefinitions() ahead of time, you will prevent many unnecessary REST calls.
• 永続的な同期化の問題が発生してパフォーマンスが低下する可能性があるので、Thing プロパティを直接操作することは避けてください。
// *** common practice ***
var referencedInfoTable = Things["thingName"].myInfoTable;
for (let i = 0; i < 10; i++) {
var row = new Object();
row.field1 = i;
row.field2 = i * 2;
row.field3 = i * 3;
referencedInfoTable.AddRow(row);
}
Things["thingName"].myInfoTable = referencedInfoTable;
// the .AddRow() will implicitely write to cache, thus causing some performance issues
// *** preferred practice ***
var deepCloneInfoTable = Things["thingName"].myInfoTable.clone();
for (let i = 0; i < 10; i++) {
deepCloneInfoTable.AddRow({
field1 : i,
field2 : i * 2,
field3 : i * 3
});
}
Things["thingName"].myInfoTable = deepCloneInfoTable;
// the .AddRow() of the cloned infotable does not have a reference to the original infotable, therefore does not write to cache.
// *** for your information ***
var referencedInfoTable = Things["thingName"].myInfoTable;
var tableLength = referencedInfoTable.rows.length;
for (let i = 0; i < tableLength; i++) {
referencedInfoTable.rows[i] = {
field1 : i,
field2 : i * 2,
field3 : i * 3
};
}
Things["thingName"].myInfoTable = referencedInfoTable;
// because you are not using .AddRow() it doesn't require access to the referenced object's .AddRow() method, therefore it doesn't need to access the cache.
• 次の行の lint を抑制するには、コメントを追加します。これにより、lint ライブラリを修正することによってのみ対処可能であった lint エラーは回避され、その他の場所では引き続き lint 処理が行われます。
オブジェクト
◦ オブジェクトは new Object(); ではなく {} を使用して作成します。読みやすく、速やかに記述できるように、オブジェクトの値をインラインで作成するようにします。
// *** old practice ***
var myObject = new Object();
myObject.item1 = "value1";
myObject.item2 = "value2";
myObject.item3 = "value3";
// *** preferred practice ***
var myObject = {
item1:"value1",
item2:"value2",
item3:"value3"
};
◦ オブジェクトのディープクローンを作成するには、JSON.parse(JSON.stringify(object)) を使用します。
変数
◦ 変数は var ではなく let を使用して宣言します。定数変数を宣言するには、const を使用します。
let を使用することで、変数を再宣言して上書きすることが防止されます。これにより、同じ名前の関数が作成されることも防止されます。
// *** common mistake ***
var value = 1;
if (true) {
var value = 2;
}
// value = 2;because it was overwritten by `value` inside if statement
// *** how to avoid mistake ***
let value = 1;
if (true) {
let value = 2;
// value = 2; inside the if statement
}
// value = 1;because the `value` inside if statement does not affect the first `value`
// *** you can still access the variable in the outter scope ***
let value = 1;
if (true) {
value = 2;
}
// value = 2;because the content of `value` was replaced
◦ 変数に名前を付ける際にはキャメルケースを使用します。
◦ 値が undefined または null である変数にデフォルト値を設定するには、|| (OR) 演算子を使用します。
// *** traditional way ***
var myNullValue; //undefined
if (myNullValue == null || myNullValue == undefined || myNullValue == "") {
myNullValue = "something";
}
var output = myNullValue;
// *** alternate way with ternary operator ***
var myNullValue; //undefined
var output = (myNullValue == null || myNullValue == undefined || myNullValue == "") ? "something" : myNullValue;
// *** preferred practice ***
var myNullValue; //undefined
var output = myNullValue || "something";
|
これは falsy 文を使用するので、デフォルト値を設定する方法として理想的ではない場合があります。たとえば、値が 0 の場合、|| 演算子によって 0 以外の値が設定されることがあります。
|
◦ 変数にアクセスするには、ドットで区切った表記法を使用します。キーに特殊文字が含まれている場合にのみ、object.row["field name with space"] などの括弧表記を使用します。
◦ ループでは、Thing を格納する変数を作成してこの変数を使用します。
// *** common mistake ***
for (let i = 0; i < 100; i++ ){
Things["thingName"].myProperty;
}
// *** best practice practice ***
let myThing = Things["thingName"];
for (let i = 0; i < 100; i++){
myThing.myProperty;
}
関数
◦ 関数に名前を付ける際にはキャメルケースを使用します。
◦ 関数は、変数としてではなく、関数宣言として宣言します。
関数定義式やアロー関数定義式をコードの一番下に移動することはできません。
// *** function declaration (Preferred practice) ***
function doSomething1 (param) {
return param;
}
// *** function expression (Avoid) ***
const doSomething2 = function (param) {
return param;
}
// *** function arrow expression (Can be used inside .map(), .redude(), .filter()) ***
const doSomething3 = (param) => {
return param;
}
◦ コードロジックの流れを中断するには、関数内で return を使用します。これにより、CPU による一部の演算が不要になり、読みやすくなります。
// given that we have multiple validations, you can save some operations by leveraging a function return
// given dice1 = undefined, dice2 = null
// common way
let outputMessage;
if (dice1 > 0 && dice1 < 7) {
if (dice2 > 0 && dice2 < 7) {
if (dice1 + dice2 == 7) {
outputMessage = SUCCESS;
} else {
outputMessage = ERROR_NOT_SEVEN;
}
} else {
outputMessage = ERROR_BAD_DICE2;
}
} else {
outputMessage = ERROR_BAD_DICE1;
}
result = outputMessage;
// preferred way
result = rollSeven(dice1, dice2);
function rollSeven (dice1, dice2) {
if (!(dice1 > 0 && dice1 < 7)) {
return ERROR_BAD_DICE1;
}
if (!(dice2 > 0 && dice2 < 7)) {
return ERROR_BAD_DICE2;
}
if (dice1 + dice2 != 7) {
return ERROR_NOT_SEVEN;
}
return SUCCESS;
}
// the preferred way promotes readability. By using a return statement and only testing for failing cases it reduces the need for nested if statements.
サービス
◦ サービスに名前を付ける際にはパスカルケースを使用します。
◦ サービスの呼び出しにはインラインパラメータを使用します。ただし、入力パラメータが大きい場合や、パラメータを再使用する場合、入力パラメータを変数として格納してから、これらの変数をサービス呼び出しに渡します。
// *** old practice ***
var params = {
"param1" : "paramValue1",
"param2" : "paramValue2"
}
Thing["thingName"].CallServiceName(params);
// *** preferred practice ***
Thing["thingName"].CallServiceName({
"param1" : "paramValue1",
"param2" : "paramValue2"
});
◦ ループ内では、データベース呼び出しを行うサービスを使用しないようにします。
インフォテーブル
◦ インフォテーブルを作成するには、以下のメソッドを使用します。
// common way
var table = Resources["InfoTableFunctions"].CreateInfoTableFromDataShape({
infoTableName : "InfoTable",
dataShapeName : "GenericStringList"
});
// easier way
var table = DataShapes["GenericStringList"].CreateValues();
◦ インフォテーブルの長さ (行) を取得するには、以下のメソッドを使用します。
// fetching a row
myInfoTable[i] = myInfoTable.rows[i] = myInfoTable.getRow(i)
// getting the length
myInfoTable.length = myInfoTable.rows.length = myInfoTable.getRowCount()
◦ インフォテーブルの 1 行目のデータにアクセスするには、ドットで区切った表記法を使用します。
// if you have an infotable with rows
// classical way to access the first row field
myInfoTable.row[0].field
// trick to access the first row field
myInfoTable.field
◦ インフォテーブルを操作したり、複数のインフォテーブル行に変更を加えたりするには、.map()、.reduce()、.filter()、.some()、.every() などの配列メソッドを使用します。infotable.rows.toArray().[map() | reduce() | filter()] を使用している場合、インフォテーブルに対してこれらのメソッドを使用できます。
.map() を使用:
// *** What you have ***
var officers = [
{ id: 20, name: 'Captain' },
{ id: 24, name: 'General' },
{ id: 56, name: 'Admiral' },
{ id: 88, name: 'Commander' }
];
// *** What you need ***
//[39, 44, 57, 95]
var officersIds = officers.map((officer) => officer.id);
result = officersIds;
複数のインフォテーブル行に変更を適用:
var table = Resources["InfoTableFunctions"].CreateInfoTableFromDataShape({
infoTableName : "InfoTable",
dataShapeName : "GenericStringList"
});
table.AddRow({item:1});
table.AddRow({item:2});
table.AddRow({item:3});
table.AddRow({item:4});
Array.from(table.rows).map(row => {row.item = row.item * 2 + "hello";});
result = table;
.reduce() を使用:
var table = Resources["InfoTableFunctions"].CreateInfoTableFromDataShape({
infoTableName : "InfoTable",
dataShapeName : "GenericStringList"
});
table.AddRow({item:1});
table.AddRow({item:2});
table.AddRow({item:3});
table.AddRow({item:4});
// total sum should be 1 + 2 + 3 + 4 = 10
result = Array.from(table.rows).reduce((total, row) => total + parseInt(row.item), 0);
◦ インフォテーブル内のデータを反復してループするには、.forEach() ループを使用します。それによってパフォーマンスが向上します。
table.rows.toArray().forEach(row => {
data += row.item;
});
◦ インフォテーブルのディープクローンを作成するには、var deepClonedInfoTable = myInfoTable.clone() を使用します。
ロガーメッセージ
◦ try … catch 文で、例外オブジェクトに err という名前を付けます。
try {
Things["thingName"].CallServiceName();
} catch (err) {
logger.error(err);
} finally { // finally is optional
// will go here whether an exception occured or not
}
|
エラー (この場合は err) のログを JSON フォーマットで作成することはお勧めしません。ベストプラクティスとしては、代わりに err.message を使用する必要があります。
|
◦ Thing を作成する場合には必ず、これを
try catch 文で囲み、エラーが発生した場合には
catch 文で Thing を削除します。これにより、ゴーストエンティティが作成されなくなります。システムにゴーストエンティティがある場合、
ゴーストエンティティサービスを参照してゴーストエンティティを削除します。
エンティティを作成するサービスでは、必ずゴーストエンティティのユースケースを処理するようにしてください。
try {
// Successfully creates the GhostThing entity.
Resources["EntityServices"].CreateThing({
name: "GhostThing" /* STRING */,
description: "Ghost" /* STRING */,
thingTemplateName: "GenericThing" /* THINGTEMPLATENAME */
});
} catch (err) {
// If an exception is caught, we need to delete the entity
// that was created to prevent it from becoming a Ghost Entity
Resources["EntityServices"].DeleteThing({
name: "GhostThing" /* THINGNAME */
});
}
◦ スクリプト監視ログにエラーログを表示する場合、Thing 名、サービス名、行番号、エラーメッセージ本文などの情報を追加します。ログの文字列フォーマットのプレースホルダーとして {} を使用できることに注意してください。フォーマットされたメッセージから不必要な情報が明らかになり、セキュリティの問題が生じる可能性があることに注意してください。マッシュアップや外部システムで使用されるサービスを扱う場合には、あまり詳しい情報を表示しないようにしてください。
例外オブジェクトでは以下のプロパティを使用できます。
▪ err.lineNumber - 例外が発生した行番号。
▪ err.name - 例外のタイプ (JavaException など)。
▪ err.fileName - 例外が発生したサービスの名前。
▪ err.message - 例外メッセージ。
try {
// error is fired here
} catch (err) {
let errMsg = "Thing [{}] Service [{}] error at line [{}] : {}";
logger.error(errMsg, me.name, err.fileName, err.lineNumber, err);
throw err; // throw original error message
}
// log output => Thing [PTC.SCA.SCO.MyThing] Service [CallMyService] error at line [2]: TypeError: Cannot find function CallMyService in object com.thingworx.things.MyThing@251c8397.
◦ 必要な粒度に応じて、適切なロガーレベルを使用します。
必要な場合にのみロガーを記述します。ログメッセージが多すぎるとデバッグが不可能になります。
レベル
|
説明
|
DEBUG
|
アプリケーションのデバッグに最も役立つ詳細な情報イベント。
// for example, show the length of time a function takes to run. let startTime = new Date().getTime(); // do something let endTime = new Date().getTime(); let time = endTime - startTime; logger.debug("How long did it take to run something (ms) : {}", time);
|
INFO
|
アプリケーションの進行状況を概要レベルで示す情報メッセージ。
// for example, you wish to track the progress of your service logger.info("Step 1 - creating variable"); let step1 = "do something"; logger.info("Step 2 - run a function"); myFunctionDoSomething(step1);
|
WARN
|
有害な影響を及ぼす可能性がある状況。
// for example, you see a potential issue here let resultFromService = Things["thingName"].CallServiceName(); if (resultFromService == undefined) { logger.warn("My variable is undefined... strange"); } myFunctionDoSomething(resultFromService);
|
ERROR
|
アプリケーションの実行が引き続き可能なエラーイベント。
// for example, a service was not suppose to throw an error try { var resultFromService = Things["thingName"].CallServiceName(); } catch (err) { logger.error("Something really bad happened: {}", err); }
|