JavaScript
每个 Lightning Web Component 必须有一个 JavaScript 文件,且必须是 ECMAScript 6 (ES6) 模块。本章覆盖代码共享、第三方库、API 调用、动态组件和 TypeScript 等完整的 JavaScript 参考。
LWC 开发必备 JavaScript 特性
高效的 LWC 开发需要熟悉以下 ES6 特性:
- 数组方法:
Array.prototype.map、filter、reduce—— 数据转换的核心工具 - 箭头函数:简洁语法 + 词法 this 绑定,模板和回调中特别有用
- 类(Classes):ES6 class 语法,每个组件继承
LightningElement - 模块(Modules):
import/export用于代码组织和共享 - 对象方法:
Object.keys、Object.values操作数据结构 - Promise:异步操作,现代更推荐
async/await - 变量声明:
const(不可变)、let(可变),避免 var
async/await 优先:lwc-recipes 和本指南中的示例已广泛采用 async/await 替代原始 Promise。async/await 更直观、像同步代码、与 try/catch 天然兼容。
共享 JavaScript 代码 —— 概述
通过 ES6 模块共享代码,每个 JS 文件有 1 MB 大小限制。LWC 支持两种共享模式:
模式 1 —— 同文件夹(内部组织):在同组件文件夹中创建 .js 文件,用相对路径导入("./utils")。只有该组件能导入——用于组件内部代码组织,不能跨组件共享。
模式 2 —— API 模块组件(跨组件共享库):创建不含 HTML 文件的组件文件夹作为库。其他组件用 c/componentName 语法导入。只能从主 JS 文件导入(与文件夹同名的文件)。要从补充文件暴露代码:先导出到补充文件 → 再从主文件重新导出。
// 同文件夹模式
import { getAmount, calculateCost } from "./utils";
import myFunction from "./myFunction";
// API 模块组件模式
import { getTermOptions, calculateMonthlyPayment } from "c/mortgageUtils";
// ✅ c/mortgageUtils(文件夹名)
// ✗ c/mortgageUtils.js(不能加扩展名)
// ✗ c/mortgageUtils/other(不能访问其他文件)
API 模块组件 —— mortgageUtils 完整示例
API 模块组件是跨组件共享代码的推荐方式。核心要求:文件夹名和主 JS 文件名必须一致。
lwc/
├── mortgageUtils/ ← API 模块组件
│ ├── mortgageUtils.js ← 主文件(与文件夹同名)
│ └── mortgageUtils.js-meta.xml
└── myComponent/
├── myComponent.html
├── myComponent.js ← 在此导入
└── myComponent.js-meta.xml
// myComponent.js
import { getTermOptions, calculateMonthlyPayment } from "c/mortgageUtils";
关键规则:import 中指定文件夹名,不能加文件扩展名。不能从不同名的补充文件或嵌套文件夹中导入。需要暴露补充文件中的代码时,先重新导出到主 JS 文件。
导出默认函数和变量
ES6 模块可以导出单个默认函数或变量。默认导出适用于模块只有一个主要用途的场景。
// myFunction.js
export default function () { /* 函数体 */ }
// 或:export { myFunction as default, ... };
// myComponent.js —— 导入时可以起任意名字
import myFunction from "./myFunction"; // 名字随意,不一定匹配原名
导入者自己选择导入名称——不一定要匹配原函数名或文件名(虽然通常这样约定)。
导出命名函数和变量
ES6 模块可以导出多个命名函数或变量。导入时名字必须精确匹配。
// mortgage.js
const getTermOptions = () => {
return [
{ label: "20 years", value: 20 },
{ label: "25 years", value: 25 },
];
};
const calculateMonthlyPayment = (principal, years, rate) => { /* 计算逻辑 */ };
export { getTermOptions, calculateMonthlyPayment };
// myComponent.js —— 名字必须精确匹配
import { getTermOptions, calculateMonthlyPayment } from "c/mortgage";
// 可以只导入子集,可以用 as 重命名:
import { getTermOptions as options } from "c/mortgage";
LWC 编译器如何解析入口点
LWC 模块 c/moduleName 只暴露一个文件作为入口点:
- moduleName.js 存在 → 它是入口点(JS 优先)
- 否则 moduleName.css 是入口点(纯 CSS 模块)
- 两者都不存在 → 编译失败
// ✅ 有效——从模块名导入
import { getSomething } from "c/utils"; // 解析为 utils.js
// ✗ 全部无效
import { getSomething } from "c/utils/utils.js"; // 不能加扩展名
import { getSomething } from "c/utils/other.js"; // 不能指定其他文件
import { getSomething } from "c/utils/utils"; // 不能显式指定文件名
访问补充 JavaScript 文件中的导出
补充 JS 文件的文件名与模块名不同。通过 export-from 语法在主文件中重新导出:
// moreUtils.js(补充文件)
const someFunction = () => { /* 逻辑 */ };
export { someFunction, someOtherFunction };
// utils.js(主文件——重新导出)
export { someFunction, someOtherFunction } from "./moreUtils";
// 或用通配符:export * from "./moreUtils";
// someComponent.js——像在主文件中一样导入
import { someFunction } from "c/utils";
注意:重新导出只是让外部消费者能用——函数不会自动在 utils.js 内部可用,除非另外加 import。
避免循环导入
LWC 不支持模块间的循环导入。循环导入发生时,一个模块在其依赖完成评估前加载,导入的名字解析为 undefined。
常见症状:TypeError: X is not a function、Cannot read properties of undefined、组件单独渲染正常但组合后失败、平台升级后突然出现错误。
// ❌ 直接循环:alpha → beta → alpha
// c/alpha/alpha.js
import { useBeta } from "c/beta";
// c/beta/beta.js
import { useAlpha } from "c/alpha";
识别与解决循环导入
如何识别:从失败的模块开始,追踪其 import → 继续追踪每个被导入模块的 import → 直到回到起点(确认循环)或穷尽所有路径。
四种解决方案:
- 提取共享代码到第三个模块 —— 最常用、最干净的方案
- 合并模块 —— 紧密耦合的模块合并为一个
- 反转依赖 —— 以函数参数或组件属性传递依赖,而非 import
- 移除不必要的 import —— 重构后残留的过时 import
使用第三方 JavaScript 库 —— 概述
前提:先检查 AppExchange 和基础组件是否有现成方案。
完整步骤
- 下载第三方库
- 上传为静态资源(Content Security Policy 要求)
- 在组件中导入:
import myLib from "@salesforce/resourceUrl/myLib" - 导入加载方法:
import { loadStyle, loadScript } from "lightning/platformResourceLoader" - 异步加载:
loadScript(this, myLib + "/myLib.js").then(() => { /* 使用库 */ })
合规要求:LWS 未启用时库必须满足 Lightning Locker 要求;LWS 启用时大多数库无需修改即可工作,但显式设置 "use strict" 的库可能需要调整。
DOM 操作 —— lwc:dom="manual"
不推荐用 JavaScript 直接操作 DOM——LWC 引擎效率更高。但 D3 等可视化库需要直接 DOM 控制。默认情况下 appendChild() 操作的元素不会应用样式。
解决方案:在需要手动操作的空原生 HTML 元素上加 lwc:dom="manual":
<template>
<div lwc:dom="manual"></div>
</template>
引擎看到此指令后保留该容器内的 CSS 封装,手动添加的元素会正确应用样式。注意:Salesforce 不提供对第三方库的官方支持。
D3 示例 —— 模板与设置
<!-- libsD3.html -->
<template>
<div class="slds-m-around_medium">
<svg class="d3" width={svgWidth} height={svgHeight}
lwc:dom="manual"></svg>
</div>
</template>
空 <svg> 作为 D3 力导向图容器。lwc:dom="manual" 是关键——告诉 LWC 此元素 DOM 由 D3 手动管理。参考实现:lwc-recipes libsD3。
D3 示例 —— JavaScript(一)加载库
/* global d3 */
import D3 from "@salesforce/resourceUrl/d3";
import { loadScript, loadStyle } from "lightning/platformResourceLoader";
d3Initialized = false; // 防止重渲染时重复初始化
async renderedCallback() {
if (this.d3Initialized) return;
this.d3Initialized = true;
try {
await Promise.all([
loadScript(this, D3 + "/d3.v5.min.js"),
loadStyle(this, D3 + "/style.css"),
]);
this.initializeD3();
} catch (error) {
this.dispatchEvent(new ShowToastEvent({
title: "Error loading D3", message: error.message, variant: "error",
}));
}
}
关键模式:① /* global d3 */ 告诉 ESLint d3 是全局变量;② d3Initialized 标志防止重复初始化;③ Promise.all 并行加载脚本+样式;④ try/catch 优雅处理加载失败。
D3 示例 —— JavaScript(二)初始化可视化
initializeD3() {
const svg = d3.select(this.template.querySelector("svg.d3"));
// 使用 this.template.querySelector()——不能用 document.querySelector()!
const simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(d => d.id))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
// ... 创建 links (lines) 和 nodes (circles),添加 drag 行为
}
LWC 关键模式:在 LWC 中不能使用 document 查询 DOM——必须用 this.template.querySelector() 将查询限制在组件自己的 Shadow DOM 内。
加载库的实用模式
// 模式 1 —— 仅加载 JS(无 CSS)
loadScript(this, RESOURCE_NAME + "/lib.js").then(() => { /* callback */ });
// 模式 2 —— 并行加载多个 JS 文件
Promise.all([
loadScript(this, RESOURCE_NAME + "/lib1.js"),
loadScript(this, RESOURCE_NAME + "/lib2.js"),
]).then(() => { /* callback */ });
// 模式 3 —— 同时加载 JS + CSS(最常用)
Promise.all([
loadScript(this, RESOURCE_NAME + "/lib.js"),
loadStyle(this, RESOURCE_NAME + "/lib.css"),
]).then(() => { /* callback */ });
最佳实践:始终用 .catch() 或 try/catch 处理错误;用标志防止重复加载;在 renderedCallback() 中加载(确保 DOM 就绪);在 disconnectedCallback() 中清理;始终使用 this.template.querySelector()。
Locker 兼容的 JavaScript 库
适用场景:组织使用 Lightning Locker(非 LWS)。LWS 启用后大多数库无需修改。
Locker 三大要求:① 避免跨命名空间直接 DOM 操作;② 支持 JS ES5 严格模式;③ 避免 Locker 阻止的 API(查阅 Locker API Viewer)。
测试步骤:创建小型示例应用 → 验证加载和基本功能 → 在 Locker Console 中测试合规性。
Locker 合规 —— 常见违规与修复
| 违规 | 问题 | 修复 |
|---|---|---|
| 意外全局变量 | 严格模式禁止未声明变量的全局泄露 | window.myLib = (function() { ... })(); |
| CSP 违规 | eval()、new Function()、<script> | 移除或用 CSP 合规替代 |
| DOM 访问违规 | 库扫描整个 DOM 而非仅操作传入的元素 | 使用 lwc:dom="manual" |
| 不支持的 DOM API | 使用 Locker 阻止或未实现的 API | 查阅 Locker API Viewer 替换 |
不合规时的选项:联系库维护者更新、开源项目自行贡献修复、fork 仓库自行维护。
LWS 兼容的 JavaScript 库
LWS 使用虚拟沙箱方式,比 Locker 更宽松——大多数现代第三方库无需修改即能运行。DOM API 表面基本等同于标准浏览器 API。但显式设置 "use strict" 的库可能需要调整。如果组织仍在用 Locker,迁移到 LWS 应优先考虑——它极大简化了第三方库的兼容性。
从 JavaScript 调用 API —— 概述
Lightning 组件框架使用 Content Security Policy (CSP) 控制可加载内容的来源。默认 CSP 不允许从 JavaScript 发起 API 调用和 WebSocket 连接。需要在 Setup 中将第三方 URL 添加为受信任 URL(Trusted URL)来修改 CSP 策略。
两类 API:Salesforce API(通过 Lightning Data Service 或 Apex)、第三方 API(通过 Fetch API + 受信任 URL,认证头信息必须用 Apex HttpRequest 代理)。
Salesforce API —— Lightning Data Service 与 Apex
LDS 优先:Lightning Data Service 基于 User Interface API,自动处理缓存、共享规则和字段级安全。大多数 CRUD 操作通过 @wire 或命令式调用即可满足需求。
当 LDS 不够时:写 Apex 类,用 @AuraEnabled 注解方法。通过 @salesforce/apex 导入,用 @wire(响应式)或直接调用(命令式)。Apex 在服务器端执行——安全、支持事务、复杂业务逻辑、完全访问所有 Salesforce API。
第三方 API —— Fetch API 使用
// 基本 Fetch
async getItems() {
const response = await fetch("http://example.com/items.json");
const items = await response.json();
}
// 正确处理 HTTP 错误
async getItems() {
try {
const response = await fetch("http://example.com/items.json");
if (!response.ok) throw Error(response); // ⚠️ .catch() 不捕获 4XX/5XX!
const myItems = await response.json();
} catch (error) {
console.error("Fetch failed:", error); // 网络失败才到这里
} finally { /* 总是执行 */ }
}
关键陷阱:.catch()只在网络失败(离线、超时)时触发——不捕获 HTTP 4XX/5XX 错误!必须检查response.ok或response.status。认证头信息绝不放在客户端 JS 中——用 Apex HttpRequest 代理。参考 lwc-recipes miscRestApiCall。
动态实例化组件 —— 概述与设置
动态组件延迟加载代码到需要时再获取——减少初始包大小。但每次动态导入增加网络往返开销。
环境设置
- 启用 LWS:动态导入的硬性要求
- 关闭持久缓存(仅开发环境):Setup → Session Settings → 取消勾选 "Enable secure and persistent browser caching"(生产环境重新启用)
- 配置 .js-meta.xml:添加
lightning__dynamicComponentcapability,apiVersion 需 ≥ 55.0
支持托管包,不支持非托管包。
动态组件语法 —— lwc:component 与 lwc:is
<template>
<div class="container">
<lwc:component lwc:is={componentConstructor}></lwc:component>
</div>
</template>
// JS
componentConstructor;
connectedCallback() {
import("c/concreteComponent")
.then(({ default: ctor }) => (this.componentConstructor = ctor))
.catch((err) => console.log("Error importing component"));
}
<lwc:component> 是 DOM 中的占位符,lwc:is 绑定构造函数。构造函数为 falsy → 不渲染;定义但非 LightningElement → 抛出错误。import() 返回 Promise,解析为 { default: constructor }。组件名用 c/componentName 格式。
动态组件 —— async/await 模式
connectedCallback() {
this.loadComponent(); // 调用 async helper,保持自身同步
}
async loadComponent() {
try {
const { default: ctor } = await import("c/myComponent");
this.componentConstructor = ctor;
} catch (err) {
console.error("Error importing component", err);
}
}
为什么 connectedCallback 不标记 async?生命周期钩子应保持同步——标记 async 会导致时序问题。改为调用 async helper 方法。标准模式:sync hook → async helper。
选择与识别动态组件
<lwc:component lwc:is={componentConstructor} lwc:ref="myCmp"></lwc:component>
renderedCallback() {
if (this.refs.myCmp) { // 构造函数设置后的下一个渲染周期才可用
console.log(this.refs.myCmp);
}
}
两种判断就绪的方法:① 动态组件自身的 connectedCallback;② 父组件的 renderedCallback 配合 this.refs 守卫检查。Jest 测试注意:动态组件的标签名是内部默认值——用 data-* 属性代替标签名选择器。
动态组件 —— 属性、子元素和属性传递
支持所有标准 HTML 全局属性、data-* 属性和事件监听器。子元素在动态组件之后渲染——构造函数变化时整个子树(组件+子元素)一起替换。
// 直接传属性
<lwc:component lwc:is={componentConstructor} text="I love dynamic components!">
// lwc:spread 传属性(运行时动态决定)
childProps = { city: "San Francisco", state: "CA" };
<lwc:component lwc:is={componentConstructor} lwc:spread={childProps}>
动态组件 —— 事件监听与组件切换
组合使用四大指令的完整示例——lwc:is、lwc:spread、lwc:on、onclick:
get childProps() {
return this.dynamicCtor === ChildA
? { name: "Child A Name", age: 30 }
: { name: "Child B Name", age: 5 };
}
get eventHandlers() {
return this.dynamicCtor === ChildA
? { customEventA: this.handleCustomEventA }
: { customEventB: this.handleCustomEventB };
}
switchComponent() {
this.dynamicCtor = this.dynamicCtor === ChildA ? ChildB : ChildA;
}
关键洞察:getter 根据当前加载的组件返回不同的属性和事件处理器。
动态组件 —— 传递 Record ID 与事件监听器
<!-- 传 record-id -->
<lwc:component record-id={recordId} lwc:is={componentConstructor}></lwc:component>
<!-- lwc:on 附加事件监听 -->
<lwc:component lwc:is={childComponent} lwc:on={eventHandlers}></lwc:component>
eventHandlers = { customEvent: this.handleCustomEvent };
handleCustomEvent(event) { console.log("Received:", event.detail); }
动态组件 —— 性能考量
静态导入:框架将组件+所有依赖打包到单个 JS 文件——一次网络请求全部加载。动态导入:框架不预取——每次需要额外的网络往返(除非已缓存)。
建议 1:让动态导入可静态分析——用字符串字面量import("c/componentName"),而非import("c/" + variable)。未来框架优化可预打包可静态分析的动态导入。
建议 2:不要过度使用动态导入——从静态导入开始,只在包大小成为可测量的性能问题时切换。
动态组件 —— 最佳实践与反模式
反模式 1 —— 不可分析动态导入:避免 import(\`c/\${val}Chart\`)。优先用静态映射(小包)或可分析动态导入(大包)。
反模式 2 —— 组件名作为属性:避免接受组件名字符串后再动态导入。更好的设计:让父组件处理导入,直接传构造函数作为属性。
字符串插值:只有在组件名来自 Custom Metadata 或 Apex 时才用 import(\`c/\${this.metaDataValue}\`)——无法在构建时评估,增加运行时开销。
动态组件 —— 包与调试
包支持:仅限托管包。从托管包导入时用包的命名空间(不是 c/)。
调试清单:① LWS 启用?② .js-meta.xml 含 lightning__dynamicComponent?③ apiVersion ≥ 55.0?④ 关闭持久缓存?⑤ import 路径正确?⑥ 构造函数有效?⑦ 浏览器控制台错误?⑧ 网络标签页确认动态导入请求。最可靠确认方式:检查 DOM,标签应物理替换。
TypeScript for LWC —— 概述(Developer Preview)
⚠️ Developer Preview:非 GA、可能随时变更或废弃、不要用于生产功能。
Winter '25 起可用 TypeScript v5.4.5+ 开发 LWC。收益:构建时类型检查、IDE 自动补全、静态类型分析、更大类型安全、内联文档。前提:VS Code + Salesforce Extension Pack(推荐),或手动安装 @salesforce/lightning-types + lwc npm 包。
启用 TypeScript 支持
- 设置 feature flag:
settings.json→"salesforcedx-vscode-lwc.preview.typeScriptSupport": true - 配置 tsconfig.json:关键设置
"experimentalDecorators": false(必须为 false!) - 安装 TypeScript:
npm install typescript --save-dev(最低 v5.4.5)
TypeScript 版本自行管理——平台不提供。保存 feature flag 设置后自动生成 tsconfig.json 到 lwc 文件夹;未出现则重启 VS Code。
编译 TypeScript 为 JavaScript 并部署
LWC 编译器不编译 TypeScript——你必须在部署前手动转换:
npx tsc --project ./path/to/lwc
每个 .ts 文件旁生成对应的 .js 文件。Salesforce 平台不存储 TypeScript 源码——只接收编译后的 JS。你必须通过 Git 等管理 .ts 源文件。
转换现有组件:重命名 .js → .ts → 运行编译器 → 解决类型错误。
TypeScript 组件的 Jest 测试
sfdx-lwc-jest 开箱即支持 TypeScript 编译——无需修改 jest.config.js。
// 安装 Jest 类型定义
npm install --save-dev @types/jest
// tsconfig.json: "compilerOptions": { "types": ["jest"] }
// 测试文件:.test.ts,结构同 JS 测试
npm run test:unit // JS 和 TS 测试通用命令
非空断言注意:this.template! 仅在 shadow DOM 模式下安全——light DOM 下始终为 null,断言会导致运行时错误。
Salesforce Lightning 类型与 Ambient Modules
VS Code Extension Pack 自动提供 lwc、@salesforce/apex、@salesforce/schema、lightning/messageService 的类型定义(临时——将迁移到 @salesforce/lightning-types)。
手动安装:安装 @salesforce/lightning-types → 创建 types/salesforce.d.ts → 在 tsconfig.json 中配置 paths(每个模块都要映射)。基础组件类型:import LightningButton from "lightning/button"。
Ambient Modules:为无类型定义的组件声明类型形状——定义类、方法签名和可选属性。
TypeScript —— 注意事项与限制
- 自行管理 TypeScript 版本 —— 平台不提供
- 必须使用源码控制 —— .ts 文件不部署到平台,只部署 .js
- 小心非空断言 ——
this.template!仅在 shadow DOM 安全 - 装饰器错误抑制:
experimentalDecorators: false导致装饰器报错——每行前加// @ts-ignore(GA 后应移除) - 当前不支持:Salesforce CLI 集成、TypeScript 源码映射调试、非 Salesforce 类型、自定义 Salesforce 对象/字段的类型定义
感谢阅读本指南。如需继续学习,请参阅下一章:操作 DOM。