在 JavaScript 中編寫程式碼的最佳作法
組織並設定程式碼格式
• 在頂端編寫程式碼邏輯,在結尾編寫 helper 函數。
• 在頂端宣告全域變數。
• 如果您的服務超過 150 行,請將程式碼分割為函數或多個服務。
• 適當命名變數與函數以減少註解數,並提高程式碼的可讀性。
• 使用 ThingWorx Composer 中內建的 JavaScript 自動設定格式功能。
編寫程式碼
• 在條件陳述式中使用真與假的假設條件。
// *** 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.
• 請避免直接操作物件內容,因為這樣可能會導致發生持續的同步處理問題,進而導致效能降低。
// *** 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.
• 欲隱抑下一行的 linting,請新增留言。這會略過只能透過修正 linting 程式庫解決的 linting 錯誤,但仍可在其他任何位置取得 linting。
物件
◦ 使用 {} 而非 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))。
變數
◦ 請使用 let 而非 var 來宣告變數。請使用 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";
|
這可能不是設定預設值的理想方法,因為它使用的是假陳述式。例如,如果您的值為 0,則 || 運算子可能會設定其他值,而不是 0。
|
◦ 使用點標記法來存取變數。請僅在您的金鑰中有特殊字元時,才使用中括號標記法,例如:object.row["field name with space"]。
◦ 建立變數來儲存物件,並在迴圈中使用這個變數。
// *** 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.
服務
◦ 請使用 Pascal 命名法來為服務命名。
◦ 請使用內嵌參數來呼叫服務。但是,如果輸入參數很大,或者如果您想要重新使用參數,請將輸入參數儲存為變數,然後再將變數傳遞至服務呼叫。
// *** 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()
◦ 使用點標記法存取資料負載第一列的資料:
// 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 作為最佳作法。
|
◦ 在建立物件時,請始終用
try catch 陳述式將它括起來,並在
catch 陳述式失敗時將其刪除。如此可避免建立映像實體。如果您的系統中有映像實體,請參閱
映像實體服務來刪除映像實體。
當您擁有建立實體的服務時,請務必處理使用映像實體的情況。
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 */
});
}
◦ 如果您有要在「監視」指令集記錄檔中顯示的任何錯誤記錄,請包括諸如物件名稱、服務名稱、行號以及原始錯誤訊息這類資訊。請記住,您可使用 {} 作為預留位置來在記錄中設定字串格式。請注意,已設定格式的訊息會顯示可能會導致安全性問題的不必要資訊。如果您要處理將在混搭或外部系統中使用的服務,請避免顯示過多細節。
在例外物件中,可以使用下列內容:
▪ 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.
◦ 請根據您的資料粒度需求,使用適當的記錄器層級。
請僅在必要時才寫入記錄器。記錄訊息太多會導致無法偵錯。
層級 (Level)
|
描述
|
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); }
|