mcpField
C
client-scripts

client-scripts

github.com/happy-technologies-llc开发4143 次安装4 天前更新
代码仓库

关于 client-scripts

client-scripts 是面向 ServiceNow 开发者的技能,用于实现浏览器端表单行为、验证和动态交互。内容涵盖脚本类型、表单 API、GlideAjax、性能、移动端注意事项和调试。

主要功能

  1. 01创建 onLoad、onChange、onSubmit 和 onCellEdit 脚本
  2. 02使用 g_form 管理表单字段
  3. 03通过 g_user 和 g_scratchpad 获取会话上下文
  4. 04使用 GlideAjax 异步调用服务器逻辑
  5. 05优化脚本和服务器调用
  6. 06在浏览器工具中调试脚本

适用场景

  1. 01提交前验证表单数据
  2. 02构建字段依赖和级联行为
  3. 03避免同步调用以加载服务器数据
  4. 04让表单脚本适配移动端和 Service Portal

MCP 相关

该技能展示使用 SN-Create-Record 和 SN-Query-Table 等 MCP 工具创建并检查 ServiceNow 客户端脚本和 UI 脚本记录。

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/happy-technologies-llc/happy-platform-skills --skill client-scripts

安装后在 Claude Code 中输入/client-scripts确认已加载。

详细介绍

客户端脚本

概述

此技能涵盖 ServiceNow 中全面的客户端脚本开发:

  • 客户端脚本类型:onLoad、onChange、onSubmit、onCellEdit
  • 用于操作表单的 g_form API
  • 用于会话上下文的 g_user 和 g_scratchpad 对象
  • 用于异步服务器调用的 GlideAjax
  • 性能优化和最佳实践
  • 移动端/Service Portal 注意事项
  • 调试技术和常见模式

适用场景: 当你需要在浏览器中执行 JavaScript,以操作表单行为、验证数据或提供动态用户交互时。

适用人群: 构建自定义表单行为、验证和用户体验增强功能的开发者。

前置条件

  • 角色: client_script_admin 或 admin
  • 访问权限: sys_script_client、sys_ui_script 表
  • 知识要求: JavaScript 基础知识、ServiceNow 表单架构
  • 相关技能: 对于更简单的显示/隐藏/必填逻辑,请参阅 catalog/ui-policies

了解客户端脚本

脚本类型对比
类型触发条件使用场景性能影响
onLoad表单加载时初始化字段、设置默认值中
onChange字段值变化时字段依赖、级联逻辑低-中
onSubmit提交表单时验证、确认低
onCellEdit编辑列表单元格时列表编辑验证低
客户端脚本与 UI Policy
┌─────────────────────────────────────────────────────────────┐
│                    Decision Matrix                           │
├─────────────────────────────────────────────────────────────┤
│  Need to show/hide/mandatory fields?                         │
│      YES → Use UI Policy (no code, easier maintenance)      │
│                                                              │
│  Need to set field values or complex logic?                  │
│      YES → Use Client Script                                 │
│                                                              │
│  Need server-side data?                                      │
│      YES → Use Client Script with GlideAjax                 │
│                                                              │
│  Need to prevent form submission?                            │
│      YES → Use Client Script (onSubmit)                     │
│                                                              │
│  Simple field validation?                                    │
│      Dictionary validation → UI Policy → Client Script      │
└─────────────────────────────────────────────────────────────┘
执行顺序
Form Load Sequence:
1. UI Policies (on load = true) evaluate
2. onLoad Client Scripts execute (by order)
3. Default values applied
4. Field-level ACLs applied

Field Change Sequence:
1. onChange Client Script for field executes
2. UI Policies with that field in condition re-evaluate
3. Related onchange handlers fire

Form Submit Sequence:
1. onSubmit Client Scripts execute (by order)
2. If all return true, form submits
3. Server-side business rules fire

操作步骤

阶段 1:创建客户端脚本
步骤 1.1:创建 onLoad 脚本

基本 onLoad 结构:

Tool: SN-Create-Record
Parameters:
  table_name: sys_script_client
  data:
    name: "Initialize Request Form"
    table: incident
    type: onLoad
    script: |
      function onLoad() {
        // Set default values
        g_form.setValue('contact_type', 'email');

        // Hide fields for new records
        if (g_form.isNewRecord()) {
          g_form.setDisplay('resolution_notes', false);
          g_form.setDisplay('resolved_by', false);
        }

        // Show informational message
        g_form.addInfoMessage('Please provide detailed information for faster resolution.');
      }
    active: true
    order: 100
    ui_type: 0

ui_type 值:

值含义
0桌面端
1移动端/Service Portal
10桌面端和移动端
步骤 1.2:创建 onChange 脚本

包含字段依赖的 onChange:

Tool: SN-Create-Record
Parameters:
  table_name: sys_script_client
  data:
    name: "Category Sets Subcategory Options"
    table: incident
    type: onChange
    field_name: category
    script: |
      function onChange(control, oldValue, newValue, isLoading, isTemplate) {
        // Skip if loading form or using template
        if (isLoading || isTemplate) {
          return;
        }

        // Clear dependent field when parent changes
        g_form.clearValue('subcategory');

        // Set subcategory based on category
        if (newValue == 'hardware') {
          g_form.setValue('assignment_group', 'Hardware Support');
        } else if (newValue == 'software') {
          g_form.setValue('assignment_group', 'Software Support');
        }
      }
    active: true
    order: 100
    ui_type: 0

onChange 参数说明:

参数描述
control表单元素(很少使用)
oldValue字段先前的值
newValue字段的新值(当前值)
isLoading如果表单正在加载,则为 true
isTemplate如果正在使用模板,则为 true
步骤 1.3:创建 onSubmit 脚本

onSubmit 验证:

Tool: SN-Create-Record
Parameters:
  table_name: sys_script_client
  data:
    name: "Validate Required Fields Before Submit"
    table: incident
    type: onSubmit
    script: |
      function onSubmit() {
        // Get field values
        var shortDesc = g_form.getValue('short_description');
        var category = g_form.getValue('category');
        var priority = g_form.getValue('priority');

        // Validate short description length
        if (shortDesc.length < 10) {
          g_form.addErrorMessage('Short description must be at least 10 characters.');
          g_form.flash('short_description', '#FF0000', 0);
          return false;  // Prevent submission
        }

        // Validate P1 requires assignment group
        if (priority == '1' && !g_form.getValue('assignment_group')) {
          g_form.addErrorMessage('P1 incidents require an assignment group.');
          return false;
        }

        // Confirm high priority submission
        if (priority == '1' || priority == '2') {
          var confirmed = confirm('You are submitting a high priority incident. Continue?');
          if (!confirmed) {
            return false;
          }
        }

        return true;  // Allow submission
      }
    active: true
    order: 100
    ui_type: 0
步骤 1.4:创建 onCellEdit 脚本

列表编辑验证:

Tool: SN-Create-Record
Parameters:
  table_name: sys_script_client
  data:
    name: "Validate Priority Change in List"
    table: incident
    type: onCellEdit
    script: |
      function onCellEdit(sysIDs, table, oldValues, newValue, callback) {
        // sysIDs: array of selected record sys_ids
        // oldValues: array of previous values
        // newValue: the new value being set
        // callback: must be called to complete the edit

        var priority = g_form.getValue('priority');

        // Prevent bulk P1 changes
        if (newValue == '1' && sysIDs.length > 1) {
          g_form.addErrorMessage('Cannot bulk change to P1. Edit records individually.');
          callback(false);  // Cancel edit
          return;
        }

        // Confirm P1 assignment
        if (newValue == '1') {
          var confirmed = confirm('Setting priority to Critical. This will escalate the incident. Continue?');
          callback(confirmed);
          return;
        }

        callback(true);  // Allow edit
      }
    active: true
    order: 100
阶段 2:g_form API
步骤 2.1:获取和设置值

常用 g_form 方法:

// Get field values
var value = g_form.getValue('field_name');           // Internal value
var display = g_form.getDisplayValue('field_name');   // Display value
var reference = g_form.getReference('assigned_to');   // Reference object (deprecated - use GlideAjax)

// Set field values
g_form.setValue('field_name', 'value');               // Set value
g_form.setValue('assigned_to', sysId, displayValue);  // Set reference with display
g_form.clearValue('field_name');                      // Clear to empty

// Check field states
var isEmpty = g_form.getValue('field_name') == '';    // Check empty
var isNewRecord = g_form.isNewRecord();               // New vs existing

引用字段处理:

// DEPRECATED - Makes synchronous server call (performance issue!)
var user = g_form.getReference('assigned_to');
var email = user.email;

// BETTER - Use callback (still not ideal)
g_form.getReference('assigned_to', function(ref) {
  var email = ref.email;
  // Continue processing
});

// BEST - Use GlideAjax (see Phase 4)
步骤 2.2:可见性和状态控制
// Visibility
g_form.setDisplay('field_name', true);    // Show field (affects row)
g_form.setVisible('field_name', true);    // Show field (preserves space)
g_form.hideFieldMsg('field_name');        // Hide field message

// State control
g_form.setMandatory('field_name', true);  // Make required
g_form.setReadOnly('field_name', true);   // Make read-only
g_form.setDisabled('field_name', true);   // Disable (grayed out)

// Labels
g_form.setLabelOf('field_name', 'New Label');  // Change label text

// Options (choice fields)
g_form.clearOptions('priority');                           // Remove all options
g_form.addOption('priority', '1', 'Critical', 0);         // Add option (value, label, index)
g_form.removeOption('priority', '5');                      // Remove specific option
步骤 2.3:消息和高亮显示
// Form-level messages
g_form.addInfoMessage('Information message');
g_form.addWarningMessage('Warning message');
g_form.addErrorMessage('Error message');
g_form.clearMessages();                        // Clear all messages

// Field-level messages
g_form.showFieldMsg('field_name', 'Message text', 'info');    // info, warning, error
g_form.hideFieldMsg('field_name');                             // Clear field message
g_form.hideAllFieldMsgs();                                     // Clear all field messages

// Visual highlighting
g_form.flash('field_name', '#FF0000', 0);      // Flash red (color, count; 0=once)
步骤 2.4:节和相关列表控制
// Sections (tabs)
g_form.setSectionDisplay('section_name', true);    // Show/hide section
g_form.isSectionVisible('section_name');           // Check visibility
g_form.activateTab('section_name');                // Switch to tab

// Related lists (limited support)
// Use UI Actions or GlideAjax for related list operations
阶段 3:g_user 和 g_scratchpad 对象
步骤 3.1:g_user 对象

g_user 对象提供当前登录用户的信息:

// User identification
var userSysId = g_user.userID;           // User sys_id
var userName = g_user.userName;           // Username (login name)
var firstName = g_user.firstName;         // First name
var lastName = g_user.lastName;           // Last name
var fullName = g_user.getFullName();      // Full display name

// Role checks
var isAdmin = g_user.hasRole('admin');              // Check single role
var isItil = g_user.hasRoleExactly('itil');         // Exact role match
var hasAnyRole = g_user.hasRoles();                 // Has any role

// Client data (set in business rules)
var customData = g_user.getClientData('custom_key');

// Preferences
var pref = g_user.getPreference('preference_name');

从服务器设置客户端数据(业务规则):

// Server-side (business rule, before query/display)
gs.getSession().putClientData('manager_email', current.caller_id.manager.email);

// Client-side (client script)
var managerEmail = g_user.getClientData('manager_email');
步骤 3.2:g_scratchpad 对象

g_scratchpad 在表单加载期间将数据从服务器传递到客户端:

服务器端(显示业务规则):

// Type: display, When: before
// Set scratchpad values for client access
g_scratchpad.isVip = current.caller_id.vip == true;
g_scratchpad.callerCompany = current.caller_id.company.name.toString();
g_scratchpad.maxPriority = gs.getProperty('incident.max_priority', '3');

客户端(onLoad 脚本):

function onLoad() {
  // Access scratchpad data (no server call needed!)
  if (g_scratchpad.isVip) {
    g_form.addInfoMessage('VIP Caller - Handle with priority');
    g_form.setValue('priority', '2');
  }

  // Use server-side property value
  var maxPriority = g_scratchpad.maxPriority;

  // Remove low priority options for VIP
  if (g_scratchpad.isVip) {
    g_form.removeOption('priority', '5');
    g_form.removeOption('priority', '4');
  }
}
阶段 4:使用 GlideAjax 调用服务器
步骤 4.1:创建 Script Include

首先,创建一个可由客户端调用的 Script Include:

Tool: SN-Create-Record
Parameters:
  table_name: sys_script_include
  data:
    name: "IncidentAjaxUtils"
    api_name: IncidentAjaxUtils
    client_callable: true
    script: |
      var IncidentAjaxUtils = Class.create();
      IncidentAjaxUtils.prototype = Object.extendsObject(AbstractAjaxProcessor, {

        // Get user details by sys_id
        getUserDetails: function() {
          var userId = this.getParameter('sysparm_user_id');
          var result = {};

          var user = new GlideRecord('sys_user');
          if (user.get(userId)) {
            result.name = user.name.toString();
            result.email = user.email.toString();
            result.phone = user.phone.toString();
            result.department = user.department.getDisplayValue();
            result.manager = user.manager.getDisplayValue();
            result.vip = user.vip == true;
          }

          return JSON.stringify(result);
        },

        // Validate assignment group can handle priority
        validateAssignment: function() {
          var groupId = this.getParameter('sysparm_group_id');
          var priority = this.getParameter('sysparm_priority');
          var result = { valid: true, message: '' };

          var group = new GlideRecord('sys_user_group');
          if (group.get(groupId)) {
            // Check if group handles this priority
            var canHandleP1 = group.u_handles_critical == true;
            if (priority == '1' && !canHandleP1) {
              result.valid = false;
              result.message = group.name + ' does not handle Critical incidents. Please select a Critical-capable group.';
            }
          }

          return JSON.stringify(result);
        },

        // Get related incidents count
        getRelatedIncidentCount: function() {
          var ciId = this.getParameter('sysparm_ci_id');
          var count = 0;

          if (ciId) {
            var ga = new GlideAggregate('incident');
            ga.addQuery('cmdb_ci', ciId);
            ga.addQuery('active', true);
            ga.addAggregate('COUNT');
            ga.query();
            if (ga.next()) {
              count = ga.getAggregate('COUNT');
            }
          }

          return count.toString();
        },

        type: 'IncidentAjaxUtils'
      });
    access: public
    active: true
步骤 4.2:从客户端脚本调用 GlideAjax

基本 GlideAjax 模式:

function onChange(control, oldValue, newValue, isLoading, isTemplate) {
  if (isLoading || isTemplate || !newValue) {
    return;
  }

  // Create GlideAjax call
  var ga = new GlideAjax('IncidentAjaxUtils');
  ga.addParam('sysparm_name', 'getUserDetails');  // Method name
  ga.addParam('sysparm_user_id', newValue);       // Custom parameter

  // Make asynchronous call
  ga.getXMLAnswer(function(response) {
    // Parse JSON response
    var user = JSON.parse(response);

    if (user.name) {
      // Update form with retrieved data
      g_form.setValue('u_caller_email', user.email);
      g_form.setValue('u_caller_phone', user.phone);

      // VIP handling
      if (user.vip) {
        g_form.addInfoMessage('VIP Caller: ' + user.name);
        g_form.setValue('priority', '2');
      }
    }
  });
}

使用 GlideAjax 进行验证:

function onChange(control, oldValue, newValue, isLoading, isTemplate) {
  if (isLoading || isTemplate || !newValue) {
    return;
  }

  var priority = g_form.getValue('priority');

  var ga = new GlideAjax('IncidentAjaxUtils');
  ga.addParam('sysparm_name', 'validateAssignment');
  ga.addParam('sysparm_group_id', newValue);
  ga.addParam('sysparm_priority', priority);

  ga.getXMLAnswer(function(response) {
    var result = JSON.parse(response);

    if (!result.valid) {
      g_form.showFieldMsg('assignment_group', result.message, 'error');
      g_form.setValue('assignment_group', '');
    } else {
      g_form.hideFieldMsg('assignment_group');
    }
  });
}
步骤 4.3:使用 getXML 的 GlideAjax(完整响应)

如需更精细地控制响应:

function onLoad() {
  var ciId = g_form.getValue('cmdb_ci');
  if (!ciId) return;

  var ga = new GlideAjax('IncidentAjaxUtils');
  ga.addParam('sysparm_name', 'getRelatedIncidentCount');
  ga.addParam('sysparm_ci_id', ciId);

  ga.getXML(function(response) {
    // Get the answer element
    var answer = response.responseXML.documentElement.getAttribute('answer');
    var count = parseInt(answer) || 0;

    if (count > 5) {
      g_form.addWarningMessage('This CI has ' + count + ' active incidents. Consider checking for related problems.');
    }
  });
}
阶段 5:性能最佳实践
步骤 5.1:尽量减少服务器调用
// BAD - Multiple synchronous calls
function onLoad() {
  var caller = g_form.getReference('caller_id');      // Server call 1
  var assignee = g_form.getReference('assigned_to');  // Server call 2
  var group = g_form.getReference('assignment_group'); // Server call 3
  // Form takes 3+ seconds to load!
}

// BETTER - Use g_scratchpad (set in Display Business Rule)
function onLoad() {
  // Data already available - no server calls!
  var callerVip = g_scratchpad.callerVip;
  var assigneeEmail = g_scratchpad.assigneeEmail;
}

// BEST - Single GlideAjax call returning all needed data
function onLoad() {
  var ga = new GlideAjax('FormDataUtils');
  ga.addParam('sysparm_name', 'getFormContext');
  ga.addParam('sysparm_incident_id', g_form.getUniqueValue());

  ga.getXMLAnswer(function(response) {
    var data = JSON.parse(response);
    // All data in one call
    processFormData(data);
  });
}
步骤 5.2:合理使用条件
// BAD - Script runs for every incident
function onLoad() {
  if (g_form.getValue('priority') == '1') {
    // Logic only needed for P1
  }
}

// BETTER - Use script condition in client script record
// Condition: priority=1
function onLoad() {
  // Only runs when priority is 1
  // No condition check needed in script
}

在客户端脚本记录中设置条件:

Tool: SN-Create-Record
Parameters:
  table_name: sys_script_client
  data:
    name: "P1 Special Handling"
    table: incident
    type: onLoad
    condition: "priority=1"
    script: |
      function onLoad() {
        // Only executes when condition matches
        g_form.addInfoMessage('Critical incident - expedited handling required');
      }
    active: true
步骤 5.3:对频繁变化进行防抖
// BAD - Ajax call on every keystroke
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
  if (isLoading) return;

  // Fires on every character typed!
  var ga = new GlideAjax('SearchUtils');
  ga.addParam('sysparm_name', 'search');
  ga.addParam('sysparm_query', newValue);
  ga.getXMLAnswer(handleResults);
}

// BETTER - Debounce rapid changes
var searchTimeout;

function onChange(control, oldValue, newValue, isLoading, isTemplate) {
  if (isLoading) return;

  // Clear previous timeout
  if (searchTimeout) {
    clearTimeout(searchTimeout);
  }

  // Wait 500ms after last change before searching
  searchTimeout = setTimeout(function() {
    var ga = new GlideAjax('SearchUtils');
    ga.addParam('sysparm_name', 'search');
    ga.addParam('sysparm_query', newValue);
    ga.getXMLAnswer(handleResults);
  }, 500);
}
步骤 5.4:缓存引用数据
// Store lookup results to avoid repeated calls
var categoryAssignments = {};

function onChange(control, oldValue, newValue, isLoading, isTemplate) {
  if (isLoading || !newValue) return;

  // Check cache first
  if (categoryAssignments[newValue]) {
    g_form.setValue('assignment_group', categoryAssignments[newValue]);
    return;
  }

  // Fetch and cache
  var ga = new GlideAjax('CategoryUtils');
  ga.addParam('sysparm_name', 'getAssignmentGroup');
  ga.addParam('sysparm_category', newValue);

  ga.getXMLAnswer(function(response) {
    var groupId = response;
    categoryAssignments[newValue] = groupId;  // Cache for next time
    g_form.setValue('assignment_group', groupId);
  });
}
阶段 6:移动端和 Service Portal 注意事项
步骤 6.1:兼容移动端的脚本
Tool: SN-Create-Record
Parameters:
  table_name: sys_script_client
  data:
    name: "Mobile-Compatible Validation"
    table: incident
    type: onSubmit
    ui_type: 10
    script: |
      function onSubmit() {
        // These g_form methods work on mobile
        var shortDesc = g_form.getValue('short_description');

        if (!shortDesc || shortDesc.length < 5) {
          // Use g_form messages (works on mobile)
          g_form.addErrorMessage('Please provide a description');
          return false;
        }

        // AVOID: alert(), confirm(), prompt() - don't work on mobile
        // AVOID: document.getElementById() - DOM may differ
        // AVOID: jQuery selectors - not available

        return true;
      }
    active: true

兼容移动端的 g_form 方法:

方法桌面端移动端Service Portal
getValue()是是是
setValue()是是是
setMandatory()是是是
setDisplay()是是是
setReadOnly()是是是
addErrorMessage()是是是
showFieldMsg()是是是
getReference()是否有限支持
flash()是否否
activateTab()是否否
步骤 6.2:Service Portal UI 脚本

对于 Service Portal 专用的客户端逻辑,请使用 UI 脚本:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_script
  data:
    name: "SP Incident Form Handler"
    script: |
      // Service Portal UI Script
      // Runs in the Service Portal context

      function spIncidentFormInit() {
        // Use $scope for Angular integration
        // Access widget data
        console.log('SP Incident form initialized');
      }

      // Self-executing for immediate availability
      if (typeof angular !== 'undefined') {
        angular.element(document).ready(function() {
          spIncidentFormInit();
        });
      }
    active: true
    global: false
    ui_type: 10
阶段 7:调试客户端脚本
步骤 7.1:控制台日志
function onLoad() {
  // Development logging (remove in production)
  console.log('=== Client Script: Initialize Form ===');
  console.log('Record sys_id:', g_form.getUniqueValue());
  console.log('Is new record:', g_form.isNewRecord());
  console.log('Current user:', g_user.userName);

  // Log scratchpad contents
  console.log('Scratchpad:', JSON.stringify(g_scratchpad));

  // Performance timing
  console.time('formSetup');

  // ... form setup logic ...

  console.timeEnd('formSetup');
}
步骤 7.2:JavaScript 调试器
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
  // Add breakpoint for debugging
  debugger;  // Browser will pause here when DevTools open

  if (isLoading) return;

  // Step through logic in browser DevTools
  var priority = g_form.getValue('priority');
  var category = g_form.getValue('category');

  // Inspect variables in DevTools console
}
步骤 7.3:错误处理
function onLoad() {
  try {
    // Main logic
    initializeForm();
    setupFieldDependencies();
    loadReferenceData();
  } catch (error) {
    // Log error details
    console.error('Client Script Error:', error.message);
    console.error('Stack:', error.stack);

    // Notify user gracefully
    g_form.addErrorMessage('Form initialization error. Please refresh the page.');
  }
}

function initializeForm() {
  // Potentially error-prone code
  var data = JSON.parse(g_scratchpad.formData);
  // ...
}
步骤 7.4:查询客户端脚本以进行调试

查找某个表的所有客户端脚本:

Tool: SN-Query-Table
Parameters:
  table_name: sys_script_client
  query: table=incident^active=true
  fields: sys_id,name,type,field_name,order,condition,ui_type
  limit: 50

检查脚本执行顺序:

Tool: SN-Query-Table
Parameters:
  table_name: sys_script_client
  query: table=incident^active=true^type=onLoad
  fields: name,order,condition
  order_by: order
  limit: 50
阶段 8:常见模式
模式 1:级联字段依赖
// Category -> Subcategory -> Assignment Group cascade
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
  if (isLoading || isTemplate) return;

  // Clear downstream fields
  g_form.clearValue('subcategory');
  g_form.clearValue('assignment_group');

  if (!newValue) return;

  // Fetch valid subcategories for this category
  var ga = new GlideAjax('CategoryUtils');
  ga.addParam('sysparm_name', 'getSubcategories');
  ga.addParam('sysparm_category', newValue);

  ga.getXMLAnswer(function(response) {
    var subcategories = JSON.parse(response);

    // Clear and repopulate options
    g_form.clearOptions('subcategory');
    g_form.addOption('subcategory', '', '-- Select --');

    subcategories.forEach(function(sub) {
      g_form.addOption('subcategory', sub.value, sub.label);
    });
  });
}
模式 2:条件必填字段
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
  if (isLoading) return;

  // State = Resolved (6) requires resolution notes
  var isResolved = (newValue == '6');

  g_form.setMandatory('resolution_code', isResolved);
  g_form.setMandatory('resolution_notes', isResolved);
  g_form.setDisplay('resolution_code', isResolved);
  g_form.setDisplay('resolution_notes', isResolved);

  if (isResolved) {
    g_form.addInfoMessage('Please provide resolution details.');
  }
}
模式 3:多项检查的表单验证
function onSubmit() {
  var errors = [];

  // Check 1: Short description length
  var shortDesc = g_form.getValue('short_description');
  if (shortDesc.length < 10) {
    errors.push('Short description must be at least 10 characters');
    g_form.showFieldMsg('short_description', 'Too short', 'error');
  }

  // Check 2: High priority requires assignment
  var priority = g_form.getValue('priority');
  var assignee = g_form.getValue('assigned_to');
  var group = g_form.getValue('assignment_group');

  if ((priority == '1' || priority == '2') && !assignee && !group) {
    errors.push('High priority incidents must be assigned');
  }

  // Check 3: Configuration item recommended
  var ci = g_form.getValue('cmdb_ci');
  if (!ci) {
    // Warning only, don't block
    var proceed = confirm('No Configuration Item specified. Continue anyway?');
    if (!proceed) {
      return false;
    }
  }

  // Display all errors
  if (errors.length > 0) {
    g_form.addErrorMessage('Please fix the following issues:\n' + errors.join('\n'));
    return false;
  }

  return true;
}
模式 4:从引用字段自动填充
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
  if (isLoading || isTemplate || !newValue) {
    return;
  }

  // Get caller details via GlideAjax
  var ga = new GlideAjax('UserUtils');
  ga.addParam('sysparm_name', 'getUserInfo');
  ga.addParam('sysparm_user_id', newValue);

  ga.getXMLAnswer(function(response) {
    var user = JSON.parse(response);

    // Auto-populate fields
    g_form.setValue('location', user.location);
    g_form.setValue('u_department', user.department);
    g_form.setValue('u_phone', user.phone);

    // VIP handling
    if (user.vip) {
      g_form.setValue('priority', '2');
      g_form.addInfoMessage('VIP Caller - Priority elevated');
    }
  });
}
阶段 9:应避免的反模式
反模式 1:同步服务器调用
// BAD - Blocks the UI
function onLoad() {
  var user = g_form.getReference('caller_id');  // Synchronous!
  var manager = g_form.getReference('assigned_to');  // More blocking!
  // User experiences frozen form
}

// GOOD - Asynchronous with callback
function onLoad() {
  var callerId = g_form.getValue('caller_id');
  if (!callerId) return;

  var ga = new GlideAjax('UserUtils');
  ga.addParam('sysparm_name', 'getUserInfo');
  ga.addParam('sysparm_user_id', callerId);
  ga.getXMLAnswer(processUser);  // Non-blocking
}
反模式 2:DOM 操作
// BAD - Direct DOM access
function onLoad() {
  document.getElementById('incident.short_description').style.backgroundColor = 'yellow';
  jQuery('#incident\\.priority').hide();
}

// GOOD - Use g_form API
function onLoad() {
  g_form.flash('short_description', '#FFFF00', 3);
  g_form.setDisplay('priority', false);
}
反模式 3:硬编码 sys_ids
// BAD - Hardcoded values
function onChange(control, oldValue, newValue, isLoading) {
  if (newValue == 'hardware') {
    g_form.setValue('assignment_group', 'a715cd759f2002002920bde8132e7018');  // Magic string!
  }
}

// GOOD - Use GlideAjax lookup or sys_properties
function onChange(control, oldValue, newValue, isLoading) {
  if (isLoading || !newValue) return;

  var ga = new GlideAjax('AssignmentUtils');
  ga.addParam('sysparm_name', 'getGroupByCategory');
  ga.addParam('sysparm_category', newValue);
  ga.getXMLAnswer(function(groupId) {
    g_form.setValue('assignment_group', groupId);
  });
}
反模式 4:繁重的 onLoad 处理
// BAD - Too much in onLoad
function onLoad() {
  // 5 GlideAjax calls
  loadCallerInfo();
  loadCIDetails();
  loadRelatedIncidents();
  loadSLAStatus();
  loadApprovalHistory();
  // Form takes forever to load!
}

// GOOD - Use g_scratchpad for essential data, lazy load the rest
function onLoad() {
  // Essential data from g_scratchpad (set in display business rule)
  if (g_scratchpad.callerVip) {
    g_form.addInfoMessage('VIP Caller');
  }

  // Load additional data only when needed
  // e.g., when user clicks a tab or expands a section
}

工具使用摘要

操作MCP 工具表
创建客户端脚本SN-Create-Recordsys_script_client
更新客户端脚本SN-Update-Recordsys_script_client
查询客户端脚本SN-Query-Tablesys_script_client
创建脚本包含项SN-Create-Recordsys_script_include
创建 UI 脚本SN-Create-Recordsys_ui_script
获取架构SN-Get-Table-Schema任意表
本地编辑周期SN-Get-Record + SN-Update-Record拉取、检查新鲜度、编辑、推送

最佳实践

  • 优先使用 UI 策略: 对于简单的显示/隐藏/必填需求,避免编写代码
  • 尽量减少服务器调用: 使用 g_scratchpad、批量调用 GlideAjax、缓存结果
  • 处理 isLoading: 始终在 onChange 中进行检查,以避免初始化问题
  • 返回值: onSubmit 必须显式返回 true/false
  • 使用脚本条件: 在平台层面筛选执行内容
  • 避免访问 DOM: 使用 g_form API 以确保跨平台兼容性
  • 错误处理: 使用 try-catch 块包裹有风险的代码
  • 控制台日志: 用于开发,在生产环境中移除
  • 移动端测试: 在实际设备上使用 ui_type=10 测试脚本
  • 版本控制: 使用 SN-Get-Record 拉取 script、sys_updated_on 和 sys_mod_count;在本地编辑;再次检查新鲜度;然后使用 SN-Update-Record 推送经过审核的字段

故障排除

脚本未执行

症状: 没有控制台输出,表单未更改 原因:

  1. 脚本未激活
  2. 条件不匹配
  3. ui_type 不匹配(桌面端与移动端)
  4. JavaScript 语法错误

解决方案:

Tool: SN-Query-Table
Parameters:
  table_name: sys_script_client
  query: table=incident^name=*my script*
  fields: name,active,condition,ui_type

检查 active=true,验证条件,并确认 ui_type 与你的环境匹配。

onChange 未触发

症状: 更改字段值不会触发脚本 原因:

  1. field_name 与实际字段不匹配
  2. 未处理 isLoading/isTemplate
  3. 脚本在到达逻辑部分前出错

解决方案:

function onChange(control, oldValue, newValue, isLoading, isTemplate) {
  console.log('onChange fired:', {
    field: 'field_name',
    old: oldValue,
    new: newValue,
    loading: isLoading
  });

  if (isLoading || isTemplate) {
    console.log('Skipping - form loading');
    return;
  }
  // ...
}
GlideAjax 返回空值

症状: 响应为空或未定义 原因:

  1. 脚本包含项未设为 client_callable
  2. 方法名称拼写错误
  3. 脚本包含项中存在服务器端错误

解决方案:

Tool: SN-Query-Table
Parameters:
  table_name: sys_script_include
  query: name=MyAjaxUtil
  fields: name,client_callable,active

确认 client_callable=true。检查系统日志中的服务器错误。

表单加载缓慢

**症状:**表单需要很长时间才能交互 原因:

  1. 同步调用 getReference
  2. 在 onLoad 中多次调用 GlideAjax
  3. 执行大量 DOM 操作

解决方案:

  1. 将 getReference 替换为 GlideAjax
  2. 将多个 Ajax 调用合并为一个
  3. 使用 g_scratchpad 获取服务器端数据
  4. 使用浏览器 DevTools 的 Performance 选项卡进行分析

相关技能

  • catalog/ui-policies - 无代码显示/隐藏/设为必填
  • admin/script-execution - 服务器端脚本模式
  • development/business-rules - 服务器端表单处理
  • development/script-includes - 可复用的服务器函数
  • admin/script-sync - 本地开发工作流

参考资料

评论

支持 Markdown