1. Office Web Add-ins 开发概述
Office Web Add-ins 是一种基于现代Web技术开发的Office插件解决方案,它允许开发者使用HTML、CSS和JavaScript等前端技术为Word、Excel、PowerPoint等Office应用程序创建功能扩展。与传统的VSTO插件相比,Web Add-ins具有跨平台、无需安装、自动更新等显著优势。
在实际项目中,我们经常遇到需要在Office文档中集成特定功能的场景。比如最近我们团队开发了一个Word插件,它能够在文档编辑过程中直接调用AI服务进行文本处理,避免了频繁切换应用的低效操作。这种"编辑-处理-输出"的一体化工作流,极大提升了内容创作的效率。
2. 开发环境搭建
2.1 基础工具准备
开发Office Web Add-ins需要以下基础环境:
- Node.js:推荐安装LTS版本(16.x或更高),这是运行Yeoman生成器和相关工具链的基础
- 代码编辑器:VS Code是最佳选择,轻量且对JavaScript/TypeScript有很好的支持
- Office应用程序:建议安装Microsoft 365订阅版,确保支持最新插件功能
安装Yeoman和Office插件生成器:
bash复制npm install -g yo generator-office
2.2 项目初始化
使用Yeoman快速创建项目骨架:
bash复制yo office
生成器会交互式询问以下配置项:
- 项目类型:选择"Office Add-in Task Pane project"
- 脚本类型:根据团队习惯选择JavaScript或TypeScript
- 插件名称:使用有意义的名称如"WordAIHelper"
- 支持的Office应用:选择Word(可根据需要多选)
初始化完成后,项目目录结构如下:
code复制├── manifest.xml # 插件配置文件
├── src # 源代码目录
│ ├── taskpane # 任务窗格前端代码
│ ├── commands # 命令按钮处理逻辑
│ └── ... # 其他资源文件
├── package.json # 项目依赖配置
└── webpack.config.js # 构建配置
3. 核心配置文件解析
3.1 Manifest文件结构
manifest.xml是插件的核心配置文件,采用XML格式定义插件的各种属性和行为。完整的manifest文件包含以下几个主要部分:
xml复制<?xml version="1.0" encoding="UTF-8"?>
<OfficeApp
xmlns="http://schemas.microsoft.com/office/appforoffice/1.1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0"
xsi:type="TaskPaneApp">
<!-- 基本信息 -->
<Id>xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx</Id>
<Version>1.0.0</Version>
<ProviderName>Your Company</ProviderName>
<DefaultLocale>en-US</DefaultLocale>
<!-- 显示信息 -->
<DisplayName DefaultValue="Word AI Helper"/>
<Description DefaultValue="AI-powered writing assistant for Word"/>
<!-- 图标配置 -->
<IconUrl DefaultValue="https://yourdomain.com/assets/icon-32.png"/>
<HighResolutionIconUrl DefaultValue="https://yourdomain.com/assets/icon-80.png"/>
<!-- 支持的主机应用 -->
<Hosts>
<Host Name="Document"/>
</Hosts>
<!-- 默认设置 -->
<DefaultSettings>
<SourceLocation DefaultValue="https://localhost:3000/taskpane.html"/>
</DefaultSettings>
<!-- 权限配置 -->
<Permissions>ReadWriteDocument</Permissions>
<!-- 版本覆盖配置 -->
<VersionOverrides xmlns="http://schemas.microsoft.com/office/taskpaneappversionoverrides" xsi:type="VersionOverridesV1_0">
<!-- 高级功能配置 -->
</VersionOverrides>
</OfficeApp>
3.2 关键配置详解
3.2.1 插件标识信息
- Id:必须使用GUID格式,可通过在线工具生成,确保全局唯一性
- Version:遵循语义化版本规范,每次更新插件都需要递增版本号
- ProviderName:显示在Office插件管理界面,建议使用公司/组织名称
3.2.2 主机与权限配置
- Hosts:定义插件支持哪些Office应用,如Document(Word)、Workbook(Excel)、Presentation(PowerPoint)等
- Permissions:声明插件需要的权限级别,常见的有:
- ReadDocument:仅读取文档内容
- ReadWriteDocument:读写文档内容
- ReadWriteAll:完全访问权限(慎用)
3.2.3 版本覆盖配置
VersionOverrides节点用于定义高级功能,如自定义功能区、命令按钮等:
xml复制<VersionOverrides ...>
<Hosts>
<Host xsi:type="Document">
<DesktopFormFactor>
<!-- 功能区配置 -->
<ExtensionPoint xsi:type="PrimaryCommandSurface">
<CustomTab id="AITab">
<Group id="AIGroup" label="AI Tools">
<Label resid="AIGroup.Label"/>
<Control xsi:type="Button" id="AISummarize">
<Label resid="AISummarize.Label"/>
<Supertip>
<Title resid="AISummarize.Title"/>
<Description resid="AISummarize.Desc"/>
</Supertip>
<Icon>
<bt:Image size="16" resid="Icon.16x16"/>
<bt:Image size="32" resid="Icon.32x32"/>
</Icon>
<Action xsi:type="ExecuteFunction" FunctionName="summarizeText"/>
</Control>
</Group>
</CustomTab>
</ExtensionPoint>
<!-- 功能文件配置 -->
<FunctionFile resid="Commands.Url"/>
</DesktopFormFactor>
</Host>
</Hosts>
<!-- 资源定义 -->
<Resources>
<bt:Images>
<bt:Image id="Icon.16x16" DefaultValue="https://yourdomain.com/assets/icon-16.png"/>
<bt:Image id="Icon.32x32" DefaultValue="https://yourdomain.com/assets/icon-32.png"/>
</bt:Images>
<bt:Urls>
<bt:Url id="Commands.Url" DefaultValue="https://localhost:3000/commands.html"/>
</bt:Urls>
<bt:ShortStrings>
<bt:String id="AIGroup.Label" DefaultValue="AI Tools"/>
<bt:String id="AISummarize.Label" DefaultValue="Summarize"/>
<bt:String id="AISummarize.Title" DefaultValue="Summarize Selected Text"/>
</bt:ShortStrings>
<bt:LongStrings>
<bt:String id="AISummarize.Desc" DefaultValue="Generate a concise summary of the selected text using AI"/>
</bt:LongStrings>
</Resources>
</VersionOverrides>
4. 开发与调试
4.1 开发服务器启动
使用以下命令启动开发服务器:
bash复制npm start
这会同时启动:
- Webpack开发服务器(默认端口3000)
- Office插件调试代理
4.2 插件加载与调试
4.2.1 Windows平台
- 在Word中打开"开发工具"选项卡
- 点击"加载项"按钮
- 选择"共享文件夹"并指向项目中的manifest.xml文件
4.2.2 macOS平台
- 在终端运行调试命令:
bash复制office-addin-debugging start manifest.xml --dev-tools
- 此命令会在Word的插件目录创建manifest文件的硬链接
- 启动Word即可看到加载的插件
4.2.3 浏览器调试
Office Web Add-ins实际上是在WebView中运行的,可以通过浏览器开发者工具进行调试:
- Windows:使用Edge开发者工具(F12)
- macOS:需要先启用Safari调试权限:
bash复制defaults write com.microsoft.Word OfficeWebAddinDeveloperExtras -bool true
然后通过Safari的"开发"菜单访问插件页面
4.3 调试技巧
- 上下文调试:在代码中使用
console.log(Office.context)输出当前上下文信息 - API调用追踪:在Word.run块中添加日志,跟踪API调用顺序
- 错误处理:确保所有异步操作都有错误捕获:
javascript复制Word.run(async (context) => {
// 操作代码
}).catch((error) => {
console.error("Error:", error);
showNotification("操作失败,请重试");
});
5. 核心API使用指南
5.1 文档内容操作
5.1.1 文本选择与读取
javascript复制async function getSelectedText() {
let text = "";
await Word.run(async (context) => {
const selection = context.document.getSelection();
selection.load("text");
await context.sync();
text = selection.text;
});
return text;
}
5.1.2 文本插入与替换
javascript复制async function insertTextAtSelection(content, insertMode = "replace") {
await Word.run(async (context) => {
const selection = context.document.getSelection();
const location = Word.InsertLocation[insertMode]; // replace/start/end/before/after
selection.insertText(content, location);
await context.sync();
});
}
5.1.3 格式设置
javascript复制async function formatSelection(style) {
await Word.run(async (context) => {
const selection = context.document.getSelection();
selection.font.name = style.font || "Calibri";
selection.font.size = style.size || 11;
selection.font.color = style.color || "black";
selection.style = style.style || "Normal";
await context.sync();
});
}
5.2 高级文档操作
5.2.1 表格处理
javascript复制async function insertTable(rows, cols, data) {
await Word.run(async (context) => {
const range = context.document.getSelection();
const table = range.insertTable(rows, cols, Word.InsertLocation.after);
// 填充表格数据
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
if (data[i] && data[i][j]) {
table.getCell(i, j).body.insertText(data[i][j], Word.InsertLocation.replace);
}
}
}
// 设置表格样式
table.style = "Grid Table 4 - Accent 1";
table.width = "100%";
await context.sync();
});
}
5.2.2 内容控件
javascript复制async function addRichTextControl(title, content) {
await Word.run(async (context) => {
const range = context.document.getSelection();
const control = range.insertContentControl();
control.title = title;
control.tag = "ai_content";
control.appearance = Word.ContentControlAppearance.boundingBox;
control.color = "#4472C4";
// 如果提供了内容,插入到控件中
if (content) {
control.insertText(content, Word.InsertLocation.replace);
}
await context.sync();
});
}
5.3 事件处理
5.3.1 选择变化监听
javascript复制function setupSelectionListener() {
Office.context.document.addHandlerAsync(
Office.EventType.DocumentSelectionChanged,
(eventArgs) => {
getSelectedText().then(text => {
if (text.length > 0) {
// 启用相关功能按钮
updateUIForSelection(true);
} else {
updateUIForSelection(false);
}
});
}
);
}
5.3.2 内容变化监听
javascript复制function setupContentChangeListener() {
Office.context.document.addHandlerAsync(
Office.EventType.DocumentContentChanged,
debounce((eventArgs) => {
checkDocumentStats();
}, 500)
);
}
// 防抖函数
function debounce(func, delay) {
let timeout;
return function() {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, arguments), delay);
};
}
6. 与后端服务集成
6.1 API调用封装
javascript复制class AIService {
constructor(apiKey) {
this.baseUrl = "https://api.your-ai-service.com/v1";
this.apiKey = apiKey;
}
async summarizeText(text, options = {}) {
const response = await fetch(`${this.baseUrl}/summarize`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${this.apiKey}`
},
body: JSON.stringify({
text,
length: options.length || "medium",
format: options.format || "paragraph"
})
});
if (!response.ok) {
throw new Error(`API request failed: ${response.status}`);
}
return response.json();
}
// 其他AI功能方法...
}
6.2 在插件中使用AI服务
javascript复制const aiService = new AIService("your-api-key");
async function processWithAI(action) {
try {
const text = await getSelectedText();
if (!text || text.length < 10) {
throw new Error("请选择至少10个字符的文本");
}
showLoadingIndicator(true);
let result;
switch (action) {
case "summarize":
result = await aiService.summarizeText(text);
break;
case "rewrite":
result = await aiService.rewriteText(text);
break;
// 其他AI操作...
}
await insertTextAtSelection(result.content, "after");
} catch (error) {
showErrorNotification(error.message);
} finally {
showLoadingIndicator(false);
}
}
7. 插件打包与发布
7.1 生产环境构建
bash复制npm run build
这会生成优化后的代码到dist目录,包括:
- 压缩的JavaScript和CSS文件
- 处理过的HTML文件
- 资源文件(如图片、字体)
7.2 Manifest文件更新
构建完成后,需要更新manifest.xml中的资源引用:
xml复制<DefaultSettings>
<SourceLocation DefaultValue="https://yourdomain.com/taskpane.html"/>
</DefaultSettings>
<!-- 更新版本号 -->
<Version>1.0.1</Version>
<!-- 更新资源URL -->
<bt:Urls>
<bt:Url id="Commands.Url" DefaultValue="https://yourdomain.com/commands.html"/>
</bt:Urls>
7.3 发布选项
Office Web Add-ins可以通过多种方式分发:
-
企业集中部署:
- 通过Microsoft 365管理员中心部署到整个组织
- 适合内部工具的统一管理
-
共享目录分发:
- 将manifest.xml放在网络共享位置
- 用户手动加载(适合测试和小范围使用)
-
应用商店发布:
- 提交到Office应用商店
- 需要经过微软审核
- 适合公开分发的商业插件
7.4 部署注意事项
- HTTPS要求:生产环境必须使用HTTPS协议
- CORS配置:确保服务器配置了正确的跨域策略
- CDN加速:建议使用CDN分发静态资源,提升加载速度
- 版本兼容性:测试插件在不同Office版本上的表现
8. 性能优化与最佳实践
8.1 API调用优化
- 批量加载属性:减少context.sync调用次数
javascript复制// 不推荐 - 多次同步
range.load("text");
await context.sync();
range.load("font");
await context.sync();
// 推荐 - 一次加载多个属性
range.load("text, font");
await context.sync();
-
操作批处理:在单个Word.run块中完成多个操作
-
选择性同步:只在必要时调用context.sync
8.2 内存管理
- 释放对象引用:对于大型文档操作,及时释放不再需要的对象
javascript复制const paragraphs = context.document.body.paragraphs;
paragraphs.load("items");
await context.sync();
// 处理完成后释放
paragraphs.items = null;
- 分块处理大数据:对于大型文档,分段处理内容
8.3 UI响应优化
- 异步操作反馈:长时间操作提供进度反馈
javascript复制async function longRunningOperation() {
showProgress("处理中...", 0);
try {
// 分阶段操作
await stage1();
showProgress("处理中...", 30);
await stage2();
showProgress("处理中...", 60);
await stage3();
showProgress("完成", 100);
} catch (error) {
showProgress("失败", 0);
}
}
- 禁用UI交互:长时间操作期间禁用相关按钮
8.4 错误处理策略
- 全局错误捕获:设置全局错误处理
javascript复制Office.initialize = function() {
$(window).on("error", function(message, source, lineno, colno, error) {
logErrorToServer({
message,
stack: error?.stack,
location: `${source}:${lineno}:${colno}`
});
showUserFriendlyError();
});
};
- 重试机制:对暂时性错误实现自动重试
javascript复制async function withRetry(fn, maxRetries = 3, delay = 1000) {
let attempt = 0;
while (attempt < maxRetries) {
try {
return await fn();
} catch (error) {
if (!isTransientError(error)) throw error;
attempt++;
if (attempt < maxRetries) await new Promise(r => setTimeout(r, delay));
}
}
throw new Error(`操作失败,重试${maxRetries}次后仍不成功`);
}
9. 安全注意事项
- 输入验证:所有从文档中读取的内容都应视为不可信数据
javascript复制function sanitizeInput(text) {
// 移除潜在的危险HTML/XML标签
return text.replace(/<[^>]*>?/gm, '');
}
-
认证与授权:
- 使用OAuth 2.0进行用户认证
- 实施基于角色的访问控制
-
敏感数据处理:
- 不在客户端存储API密钥
- 使用HTTPS传输所有数据
- 考虑对敏感内容进行端到端加密
-
权限最小化:只申请必要的API权限
10. 调试与问题排查
10.1 常见问题
-
插件未加载:
- 检查manifest.xml语法是否正确
- 验证SourceLocation URL可访问
- 查看浏览器控制台是否有加载错误
-
API调用失败:
- 检查是否在Word.run块中调用
- 确认已加载所需属性
- 验证是否有足够的权限
-
跨域问题:
- 确保所有资源来自同一域或配置了CORS
- 检查manifest.xml中的AppDomains配置
10.2 调试工具
- Fiddler/Charles:监控网络请求
- Office JS Debugger:专用调试工具
- 日志系统:实现客户端日志收集
javascript复制const logger = {
log(...args) {
console.log(...args);
if (window.logServer) {
window.logServer.send("log", args);
}
},
error(...args) {
console.error(...args);
if (window.logServer) {
window.logServer.send("error", args);
}
}
};
11. 实际案例:Word AI助手实现
11.1 功能概述
我们开发的Word AI助手主要提供以下功能:
- 文本摘要:自动生成选中文本的简洁摘要
- 内容改写:用不同风格重写选定内容
- 语法检查:识别并修正语法错误
- 术语解释:解释文档中的专业术语
- 智能推荐:基于上下文建议后续内容
11.2 核心实现代码
11.2.1 功能区配置
xml复制<ExtensionPoint xsi:type="PrimaryCommandSurface">
<CustomTab id="AITab">
<Group id="AIGroup" label="AI Tools">
<Label resid="AIGroup.Label"/>
<!-- 摘要按钮 -->
<Control xsi:type="Button" id="SummarizeBtn">
<Label resid="SummarizeBtn.Label"/>
<Supertip>
<Title resid="SummarizeBtn.Title"/>
<Description resid="SummarizeBtn.Desc"/>
</Supertip>
<Icon>
<bt:Image size="16" resid="Summarize.Icon16"/>
<bt:Image size="32" resid="Summarize.Icon32"/>
</Icon>
<Action xsi:type="ExecuteFunction" FunctionName="summarizeText"/>
</Control>
<!-- 改写按钮 -->
<Control xsi:type="Menu" id="RewriteMenu">
<Label resid="RewriteMenu.Label"/>
<Supertip>
<Title resid="RewriteMenu.Title"/>
<Description resid="RewriteMenu.Desc"/>
</Supertip>
<Icon>
<bt:Image size="16" resid="Rewrite.Icon16"/>
<bt:Image size="32" resid="Rewrite.Icon32"/>
</Icon>
<Items>
<Item id="RewriteFormal">
<Label resid="RewriteFormal.Label"/>
<Supertip>
<Title resid="RewriteFormal.Title"/>
</Supertip>
<Icon>
<bt:Image size="16" resid="Formal.Icon16"/>
</Icon>
<Action xsi:type="ExecuteFunction" FunctionName="rewriteFormal"/>
</Item>
<!-- 其他改写选项... -->
</Items>
</Control>
</Group>
</CustomTab>
</ExtensionPoint>
11.2.2 命令处理
javascript复制// commands.js
Office.actions.associate("summarizeText", async function(event) {
try {
const text = await getSelectedText();
if (!text) {
event.completed({ error: "请先选择要摘要的文本" });
return;
}
const result = await aiService.summarizeText(text);
await insertTextAtSelection(`\n摘要:${result.summary}\n`, "after");
event.completed();
} catch (error) {
event.completed({ error: error.message });
}
});
// 其他命令处理函数...
11.2.3 任务窗格UI
html复制<div class="ai-panel">
<div class="tool-header">
<h2>AI 写作助手</h2>
<div class="loading-indicator" id="loadingIndicator"></div>
</div>
<div class="tool-section">
<h3>选中文档内容</h3>
<div class="selection-preview" id="selectionPreview">
<p>请先在文档中选择文本...</p>
</div>
<div class="char-count" id="charCount">0 字符</div>
</div>
<div class="tool-section">
<h3>可用操作</h3>
<button class="action-btn" id="summarizeBtn" disabled>
<i class="icon-summarize"></i> 生成摘要
</button>
<div class="action-menu">
<button class="menu-btn">改写风格 <i class="icon-arrow"></i></button>
<div class="menu-content">
<button data-action="rewriteFormal">正式商务</button>
<button data-action="rewriteCasual">轻松随意</button>
<button data-action="rewriteAcademic">学术风格</button>
</div>
</div>
</div>
<div class="result-section" id="resultSection" style="display:none">
<h3>处理结果</h3>
<div class="result-content" id="resultContent"></div>
<button class="insert-btn" id="insertBtn">插入文档</button>
</div>
</div>
12. 进阶开发技巧
12.1 使用React/Vue框架
虽然可以使用纯JavaScript开发,但使用现代前端框架能显著提升开发效率:
javascript复制// React组件示例
function AIPanel() {
const [selectedText, setSelectedText] = useState('');
const [isProcessing, setIsProcessing] = useState(false);
const [result, setResult] = useState(null);
useEffect(() => {
// 监听选择变化
const handler = () => {
getSelectedText().then(text => {
setSelectedText(text);
});
};
Office.context.document.addHandlerAsync(
Office.EventType.DocumentSelectionChanged,
handler
);
return () => {
Office.context.document.removeHandlerAsync(handler);
};
}, []);
const handleSummarize = async () => {
setIsProcessing(true);
try {
const summary = await aiService.summarizeText(selectedText);
setResult(summary);
} finally {
setIsProcessing(false);
}
};
return (
<div className="ai-panel">
{/* UI渲染 */}
</div>
);
}
12.2 共享代码与状态管理
对于复杂插件,建议使用状态管理库:
javascript复制// store.js
import { createStore } from 'redux';
const initialState = {
selectedText: '',
documentStats: null,
aiResults: {},
uiState: {
isLoading: false,
activeTool: null
}
};
function reducer(state = initialState, action) {
switch (action.type) {
case 'SET_SELECTION':
return { ...state, selectedText: action.text };
case 'START_LOADING':
return { ...state, uiState: { ...state.uiState, isLoading: true }};
// 其他case...
default:
return state;
}
}
export const store = createStore(reducer);
12.3 自定义UI组件
Office JS API支持创建丰富的自定义界面:
javascript复制function showCustomDialog(url, options) {
return new Promise((resolve, reject) => {
Office.context.ui.displayDialogAsync(url, {
width: options.width || 60,
height: options.height || 60,
displayInIframe: true
}, (result) => {
if (result.status === Office.AsyncResultStatus.Failed) {
reject(result.error);
return;
}
const dialog = result.value;
dialog.addEventHandler(Office.EventType.DialogMessageReceived, (arg) => {
try {
const data = JSON.parse(arg.message);
dialog.close();
resolve(data);
} catch (error) {
reject(error);
}
});
dialog.addEventHandler(Office.EventType.DialogEventReceived, (arg) => {
if (arg.error) {
reject(arg.error);
}
});
});
});
}
13. 测试策略
13.1 单元测试
使用Jest等框架测试业务逻辑:
javascript复制// aiService.test.js
describe('AI Service', () => {
let aiService;
beforeEach(() => {
aiService = new AIService('test-key');
fetchMock.reset();
});
it('should summarize text correctly', async () => {
const mockText = "这是一段测试文本...";
const mockResponse = { summary: "摘要内容" };
fetchMock.post('https://api.your-ai-service.com/v1/summarize', {
status: 200,
body: mockResponse
});
const result = await aiService.summarizeText(mockText);
expect(result).toEqual(mockResponse);
expect(fetchMock.lastUrl()).toBe('https://api.your-ai-service.com/v1/summarize');
});
});
13.2 集成测试
测试Office API交互:
javascript复制describe('Word Integration', () => {
beforeAll(() => {
// 模拟Office环境
global.Office = {
context: {
document: {
getSelection: jest.fn(),
addHandlerAsync: jest.fn()
}
},
run: jest.fn((callback) => {
const context = {
document: {
getSelection: jest.fn(() => ({
load: jest.fn(),
text: "模拟文本",
insertText: jest.fn()
})),
sync: jest.fn(() => Promise.resolve())
}
};
return callback(context).then(() => context);
})
};
});
it('should insert text at selection', async () => {
await insertTextAtSelection("测试内容");
expect(Office.run).toHaveBeenCalled();
});
});
13.3 UI测试
使用Cypress或Selenium进行端到端测试:
javascript复制// cypress/integration/plugin.spec.js
describe('Word AI Plugin', () => {
it('should load task pane', () => {
cy.visit('http://localhost:3000/taskpane.html');
cy.contains('AI 写作助手').should('be.visible');
});
it('should show selection info', () => {
// 模拟选择文本
cy.window().then(win => {
win.postMessage({ type: 'selectionChanged', text: '测试文本' }, '*');
});
cy.get('#selectionPreview').should('contain', '测试文本');
});
});
14. 持续集成与部署
14.1 CI/CD流水线配置
yaml复制# .github/workflows/build.yml
name: Build and Deploy
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build production
run: npm run build
- name: Deploy to staging
if: github.ref == 'refs/heads/main'
run: |
npm run deploy-staging
- name: Deploy to production
if: github.event_name == 'push' && contains(github.ref, 'tags')
run: |
npm run deploy-production
14.2 自动化测试与验证
- Manifest验证:
bash复制office-addin-manifest validate manifest.xml
-
Office加载测试:使用Office-Addin-TestRunner自动化测试插件加载
-
安全扫描:集成OWASP ZAP进行安全扫描
15. 用户体验优化
15.1 响应式设计
确保插件在不同尺寸任务窗格中表现良好:
css复制/* 基础布局 */
.ai-panel {
display: flex;
flex-direction: column;
height: 100%;
padding: 12px;
box-sizing: border-box;
}
/* 响应式调整 */
@media (max-width: 400px) {
.tool-section {
padding: 8px;
}
.action-btn {
padding: 6px 8px;
font-size: 14px;
}
}
15.2 无障碍访问
遵循WCAG 2.1标准:
- 键盘导航:确保所有功能可通过键盘访问
- ARIA属性:为动态内容添加适当的ARIA角色和属性
- 颜色对比:文本与背景对比度至少4.5:1
- 屏幕阅读器支持:测试与主流屏幕阅读器的兼容性
html复制<button
id="summarizeBtn"
aria-label="生成选中文本的摘要"
aria-disabled="true"
>
<span aria-hidden="true">生成摘要</span>
</button>
15.3 多语言支持
- 资源文件分离:
json复制// locales/en.json
{
"AIGROUP_LABEL": "AI Tools",
"SUMMARIZE_BTN": "Summarize"
}
// locales/zh.json
{
"AIGROUP_LABEL": "AI工具",
"SUMMARIZE_BTN": "摘要"
}
- 动态加载:
javascript复制function loadStrings(locale) {
return import(`./locales/${locale}.json`)
.then(module => module.default)
.catch(() => import('./locales/en.json'));
}
// 初始化时
Office.context.displayLanguage.getAsync(language => {
const userLocale = language.value.split('-')[0];
loadStrings(userLocale).then(strings => {
updateUIWithLocalizedStrings(strings);
});
});
16. 性能监控与分析
16.1 客户端性能追踪
javascript复制const perf = {
marks: {},
start(name) {
this.marks[name] = {
start: performance.now(),
end: null,
duration: null
};
},
end(name) {
if (this.marks[name]) {
this.marks[name].end = performance.now();
this.marks[name].duration =
this.marks[name].end - this.marks[name].start;
// 发送到分析服务器
logPerfMetric(name, this.marks[name].duration);
}
}
};
// 使用示例
perf.start('summarizeText');
await aiService.summarizeText(text);
perf.end('summarizeText');
16.2 异常监控
集成Sentry或类似服务:
javascript复制import * as Sentry from '@sentry/browser';
if (process.env.NODE_ENV === 'production') {
Sentry.init({
dsn: 'your-dsn-here',
release: 'word-ai-plugin@' + process.env.VERSION,
environment: process.env.ENV
});
// 捕获未处理的Promise异常
window.addEventListener('unhandledrejection', event => {
Sentry.captureException(event.reason);
});
}
// 手动捕获异常
try {
// 可能出错的代码
} catch (error) {
Sentry.captureException(error);
throw error;
}
16.3 使用情况分析
收集匿名使用数据帮助改进产品:
javascript复制function trackEvent(eventName, properties = {}) {
if (userHasOptedInAnalytics()) {
const payload = {
event: eventName,
properties: {
...properties,
pluginVersion: process.env.VERSION,
officeVersion: Office.context.diagnostics.version,
timestamp: new Date().toISOString()
}
};
navigator.sendBeacon('https://analytics.yourdomain.com/events', payload);
}
}
// 使用示例
trackEvent('button_click', { button_id: 'summarize' });