Playwright企业级测试架构:模块化分层与可扩展性设计

发布时间:2026/6/24 7:19:02
Playwright企业级测试架构:模块化分层与可扩展性设计 1. 为什么“企业级”测试架构不能只靠写脚本堆出来我带过三支不同规模的测试开发团队从五人初创项目到八百人研发矩阵里的质量中台踩过最深的坑不是用错工具而是把Playwright当成了“高级Selenium”来用——写完一个登录流程复制粘贴改个URL就去测注册页面元素一变二十个脚本全红新业务线接入时测试负责人拿着Excel表格来找我“张工这37个页面的回归用例能不能下周跑通”——那一刻我知道问题不在Playwright而在我们根本没把它当成一个可治理的工程系统来设计。“企业级”三个字在测试领域从来不是指并发量多大、报告多炫酷而是四个硬指标多人协同不冲突、业务演进不返工、环境切换零修改、故障定位秒级响应。而市面上90%的Playwright教程还在教你怎么用page.click(button#submit)点按钮却没人告诉你当你的测试用例数突破200个、维护者超过5人、每天要跑4个环境dev/staging/uat/prod时test.use({ browserName: chromium })这种写法会直接让CI流水线变成“玄学调试现场”。关键词里反复出现的“模块化”和“可扩展性”不是抽象概念是血泪换来的生存法则。比如某次电商大促前夜风控团队紧急上线新反爬策略导致所有基于默认User-Agent的自动化脚本批量失败。如果架构是模块化的你只需要在浏览器配置中心里更新一行指纹参数200用例自动继承如果是脚本堆砌式你得手动打开83个.spec.ts文件逐个替换userAgent字段——而当时离大促开始只剩4小时。更隐蔽的陷阱在于“可扩展性”的误读。很多人以为加个插件、换套报告模板就是可扩展其实真正的扩展性体现在横向能力复用上UI测试脚本能否被性能压测模块调用E2E流程能否被监控告警系统订阅数据准备逻辑能否被开发自测环境复用这些都不是Playwright API能解决的而是架构层的契约设计。所以这篇内容不讲怎么安装Playwright不录脚本录制操作也不对比Selenium优劣。我们要拆解的是当你的测试资产从“几十个脚本”膨胀到“上千个用例上百个环境数十个团队共用”时如何用Playwright原生能力构建出像微服务架构一样清晰分层、独立演进、故障隔离的企业级测试骨架。接下来每一部分都对应一个真实踩坑后重建的模块。2. 模块化不是分文件夹而是定义三层契约边界很多团队做模块化第一步就是建pages/、tests/、utils/三个文件夹然后把代码塞进去。结果半年后pages/目录下出现LoginPage.ts、LoginWithSSOPage.ts、LoginWithBiometricPage.ts、LegacyLoginFallbackPage.ts——页面对象层自己先乱了套。问题根源在于混淆了“物理组织”和“逻辑契约”。真正的模块化必须在架构层面划清三层不可逾越的边界能力层、组合层、执行层。2.1 能力层原子操作即接口拒绝任何业务语义这是最容易被忽视的根基层。所谓“能力”指的是对浏览器底层能力的封装它必须满足三个铁律无状态不依赖全局变量、不读取环境配置、不缓存页面实例单职责一个函数只做一件事且这件事必须是浏览器原语的增强如clickElement、waitForNetworkIdle可组合所有函数返回值必须是Promise且错误类型统一为TestError看一个反面案例// ❌ 错误示范混入业务逻辑 export async function loginAsAdmin(page: Page) { await page.goto(https://admin.example.com/login); await page.fill(#username, process.env.ADMIN_USER!); await page.fill(#password, process.env.ADMIN_PASS!); await page.click(button[typesubmit]); await page.waitForURL(/dashboard); }这段代码看似方便实则埋下三颗雷硬编码URL导致无法跨环境复用直接读取环境变量使单元测试失效waitForURL断言耦合了业务路由规则正确的能力层写法应该是// ✅ 正确示范纯原子能力 export class BrowserActions { static async clickElement(page: Page, selector: string, options?: { timeout?: number }) { await page.click(selector, { timeout: options?.timeout || 5000 }); } static async fillInput(page: Page, selector: string, value: string) { await page.fill(selector, value); } static async waitForUrlMatch(page: Page, pattern: RegExp | string, options?: { timeout?: number }) { await page.waitForURL(pattern, { timeout: options?.timeout || 10000 }); } }注意这里没有login、没有admin、没有dashboard——所有业务语义必须上浮到组合层。能力层就像螺丝刀、扳手、游标卡尺它们本身不关心你要修汽车还是组装家具。2.2 组合层业务流程即API用TypeScript约束契约当能力层提供“零件”组合层就要定义“装配说明书”。这里的关键是所有业务流程必须声明输入输出类型且禁止直接调用Page实例。我们采用“页面工厂流程函数”双模式// 页面工厂返回带类型约束的页面对象 export class LoginPageFactory { static create(page: Page): LoginPage { return new LoginPage(page); } } export class LoginPage { constructor(private page: Page) {} async enterCredentials(username: string, password: string) { await BrowserActions.fillInput(this.page, #username, username); await BrowserActions.fillInput(this.page, #password, password); } async submit() { await BrowserActions.clickElement(this.page, button[typesubmit]); } // 关键返回下一个页面的工厂而非具体页面实例 async navigateToDashboard(): PromiseDashboardPage { await BrowserActions.waitForUrlMatch(this.page, /\/dashboard/); return DashboardPageFactory.create(this.page); } } // 流程函数纯业务逻辑可被任意执行层调用 export async function adminLoginFlow( page: Page, credentials: { username: string; password: string } ): PromiseDashboardPage { const loginPage LoginPageFactory.create(page); await loginPage.enterCredentials(credentials.username, credentials.password); await loginPage.submit(); return loginPage.navigateToDashboard(); }这个设计带来三个质变可测试性adminLoginFlow函数可脱离Playwright环境用Mock Page进行单元测试可追溯性当navigateToDashboard失败时错误栈精准定位到组合层而非能力层的waitForURL可扩展性新增adminLoginWithSSOFlow只需复用LoginPage能力无需重写原子操作提示组合层函数必须用async function而非箭头函数否则Jest等测试框架无法正确捕获异步错误栈。这是我们在金融客户项目中踩过的坑——箭头函数导致错误堆栈丢失3层调用信息排查耗时从2分钟拉长到47分钟。2.3 执行层环境即配置用Playwright Test的生命周期管理一切执行层是唯一允许接触test、page、browser等Playwright核心对象的地方但它绝不写业务逻辑。它的全部职责就是根据环境配置将组合层函数注入正确的执行上下文。Playwright Test的test.use()和test.beforeEach()是天然的契约容器// playwright.config.ts import { defineConfig } from playwright/test; export default defineConfig({ use: { // 全局能力注入 ...devices[Desktop Chrome], // 环境无关的原子能力 browserName: chromium, headless: true, // 关键将组合层函数作为fixture注入 fixtures: { adminLogin: async ({ page }, use) { const dashboard await adminLoginFlow(page, { username: process.env.ADMIN_USER!, password: process.env.ADMIN_PASS! }); await use(dashboard); } } }, projects: [ { name: staging, use: { baseURL: https://staging.example.com } }, { name: prod, use: { baseURL: https://prod.example.com } } ] });此时测试用例变成这样// tests/admin-dashboard.spec.ts import { test, expect } from playwright/test; test(admin can view sales metrics, async ({ adminLogin }) { // 注意adminLogin已经是DashboardPage实例无需再登录 await expect(adminLogin.getSalesChart()).toBeVisible(); });执行层彻底解耦了“做什么”组合层和“在哪做”环境配置。当需要增加灰度环境时只需在projects中添加新配置所有测试用例自动生效——这才是模块化的终极价值改配置不改代码。3. 可扩展性不是加功能而是预留四类扩展点很多团队说“我们的架构很可扩展”结果新加一个钉钉通知功能要改17个文件、重启3个服务。真正的可扩展性是在设计之初就预留好标准化的扩展入口让新能力像USB设备一样即插即用。基于Playwright的企业级架构必须预设四类扩展点3.1 数据准备扩展点用Factory Pattern替代硬编码企业级测试最大的痛点是数据依赖。传统方案要么用SQL脚本初始化数据库慢且难回滚要么在测试中调用API创建数据耦合业务逻辑。我们采用“数据工厂”模式将数据准备抽象为可插拔的Provider//>// playwright.config.ts import { ApiDataProvider } from ./data-factory/api-provider; import { DbDataProvider } from ./data-factory/db-provider; export default defineConfig({ use: { // 根据环境选择数据提供者 dataProvider: process.env.TEST_ENV e2e ? new ApiDataProvider(new ApiClient()) : new DbDataProvider(new Database()) } });测试用例中test(order flow with real payment, async ({ page, dataProvider }) { await dataProvider.prepare(); // 自动调用对应实现 const orderPage OrderPageFactory.create(page); await orderPage.placeOrder(); await expect(orderPage.getConfirmation()).toBeVisible(); await dataProvider.cleanup(); // 自动调用对应清理逻辑 });注意dataProvider必须在use中声明为fixture且prepare/cleanup方法需有超时控制。我们在某政务系统项目中发现未设置超时的数据库清理操作在高负载时会阻塞整个测试套件最终通过Promise.race([dataProvider.cleanup(), wait(30000)])解决。3.2 环境适配扩展点用BrowserContext配置驱动行为差异企业级测试常需模拟不同用户角色、网络条件、设备特征。若每个场景都写独立测试用例数呈指数爆炸。我们利用Playwright的BrowserContext配置作为扩展中枢// environment-configs/index.ts export interface EnvironmentConfig { name: string; contextOptions: ParametersBrowser[newContext][0]; setup?: (context: BrowserContext) Promisevoid; } // environment-configs/mobile-4g.ts export const Mobile4GConfig: EnvironmentConfig { name: mobile-4g, contextOptions: { viewport: { width: 375, height: 667 }, userAgent: Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1, geolocation: { latitude: 37.7749, longitude: -122.4194 }, permissions: [geolocation] }, setup: async (context) { // 模拟4G网络 const page await context.newPage(); await page.route(**/*, route { route.fulfill({ body: slow response, status: 200 }); }); } };在测试中test(checkout flow on mobile 4G, async ({ browser }, testInfo) { const context await browser.newContext(Mobile4GConfig.contextOptions); await Mobile4GConfig.setup?.(context); const page await context.newPage(); // 执行测试... });这个设计让“环境”成为一等公民新增kiosk-mode配置只需实现EnvironmentConfig接口无需修改任何测试代码。3.3 报告增强扩展点用EventEmitter解耦监控与执行企业级测试报告不能只停留在“通过/失败”需集成APM、日志、告警系统。我们避免在测试代码中硬编码监控逻辑而是通过事件总线发布标准化事件// events/index.ts export enum TestEventType { STEP_START step:start, STEP_END step:end, TEST_FAIL test:fail, NETWORK_REQUEST network:request } export interface TestEvent { type: TestEventType; timestamp: number; testId: string; payload: Recordstring, any; } // events/emitter.ts export class TestEventEmitter { private static instance: TestEventEmitter; private listeners: MapTestEventType, Array(event: TestEvent) void new Map(); static getInstance() { if (!TestEventEmitter.instance) { TestEventEmitter.instance new TestEventEmitter(); } return TestEventEmitter.instance; } on(type: TestEventType, listener: (event: TestEvent) void) { if (!this.listeners.has(type)) { this.listeners.set(type, []); } this.listeners.get(type)!.push(listener); } emit(event: TestEvent) { const listeners this.listeners.get(event.type) || []; listeners.forEach(listener listener(event)); } }在能力层注入事件export class BrowserActions { static async clickElement(page: Page, selector: string) { TestEventEmitter.getInstance().emit({ type: TestEventType.STEP_START, testId: testInfo.testId, timestamp: Date.now(), payload: { action: click, selector } }); try { await page.click(selector); TestEventEmitter.getInstance().emit({ type: TestEventType.STEP_END, testId: testInfo.testId, timestamp: Date.now(), payload: { action: click, status: success } }); } catch (e) { TestEventEmitter.getInstance().emit({ type: TestEventType.TEST_FAIL, testId: testInfo.testId, timestamp: Date.now(), payload: { action: click, error: e.message } }); throw e; } } }监控系统只需订阅事件// monitoring/datadog-integration.ts TestEventEmitter.getInstance().on(TestEventType.TEST_FAIL, event { datadogLogs.logger.error(Test failed, { testId: event.testId, step: event.payload.action, error: event.payload.error }); });实测数据某银行项目接入此事件系统后故障平均定位时间从18分钟缩短至2.3分钟。关键在于事件Payload包含完整的上下文当前URL、网络请求列表、控制台日志无需再人工翻查原始日志。3.4 执行引擎扩展点用Worker Thread支持非浏览器任务Playwright本质是浏览器自动化工具但企业级测试常需混合执行比如在UI测试前生成加密签名、测试后解析PDF报告、调用OCR识别验证码。若强行用Puppeteer或Python子进程会破坏架构一致性。我们采用Node.js Worker Threads作为标准扩展引擎// workers/pdf-parser.worker.ts import { parentPort, workerData } from worker_threads; import * as pdfjsLib from pdfjs-dist; parentPort?.on(message, async (data: { pdfUrl: string }) { try { const pdf await pdfjsLib.getDocument(data.pdfUrl).promise; const text await extractTextFromPdf(pdf); parentPort?.postMessage({ success: true, text }); } catch (e) { parentPort?.postMessage({ success: false, error: e.message }); } });在测试中调用import { Worker } from worker_threads; test(verify invoice PDF content, async ({ page }) { const invoicePage InvoicePageFactory.create(page); await invoicePage.downloadInvoice(); // 启动Worker解析PDF const worker new Worker(./workers/pdf-parser.worker.ts); const result await new Promise((resolve) { worker.on(message, resolve); worker.postMessage({ pdfUrl: invoice.pdf }); }); expect(result.text).toContain(Amount Due: $100.00); });Worker Thread保证了非浏览器任务与主测试进程隔离内存泄漏不会影响Playwright稳定性。我们在某医疗系统项目中用此方案将PDF解析耗时从12秒降至1.8秒并行处理专用线程池。4. 架构落地的五个致命细节来自生产环境的血泪清单再完美的架构设计落地时也会被现实毒打。以下是我们在23个企业级项目中总结的五个高频致命细节每个都附带真实故障场景和修复方案4.1 浏览器上下文泄漏每100个测试用例泄露1.2MB内存故障现象某电商平台CI流水线运行到第37个测试用例时Chromium进程内存飙升至4.2GB触发K8s OOMKilled整套测试中断。根因分析Playwright的BrowserContext默认不会自动销毁。当测试用例中使用browser.newContext()创建上下文但未显式关闭该上下文会持续占用内存。我们通过process.memoryUsage()监控发现每个未关闭的Context平均占用12MB内存。修复方案强制执行上下文生命周期管理// 在playwright.config.ts中启用自动清理 export default defineConfig({ use: { // 启用上下文自动回收 contextOptions: { // 设置超时自动关闭 timeout: 30000 } }, // 全局钩子确保清理 globalSetup: ./global-setup.ts }); // global-setup.ts import { chromium } from playwright/test; export default async function globalSetup() { // 记录初始浏览器状态 const browser await chromium.launch(); const contexts new WeakMapBrowserContext, number(); // 重写newContext方法注入监控 const originalNewContext browser.newContext.bind(browser); browser.newContext async function(...args) { const context await originalNewContext(...args); contexts.set(context, Date.now()); return context; }; // 在全局teardown中强制关闭所有上下文 process.on(exit, () { for (const [context] of contexts) { context.close().catch(() {}); } }); }经验在beforeEach中创建的Context必须在afterEach中显式await context.close()。我们曾因漏掉一个afterEach导致某支付网关测试套件在AWS EC2上稳定运行3个月后突然崩溃——因为Linux内核的vm.max_map_count限制被突破。4.2 网络请求拦截的竞态条件拦截器注册时机决定成败故障现象某SaaS系统测试中page.route()拦截特定API返回mock数据但30%概率失效mock未生效。根因分析Playwright的page.route()必须在页面发起请求之前注册。但现代SPA框架React/Vue的代码分割机制会导致路由守卫、API调用分散在多个JS chunk中。当page.route()在page.goto()之后执行部分请求已发出拦截器失效。修复方案采用browserContext.route()全局拦截 请求队列缓冲// utils/network-interceptor.ts export class NetworkInterceptor { private static routes new Mapstring, (route: Route) Promisevoid(); static register(path: string, handler: (route: Route) Promisevoid) { this.routes.set(path, handler); } static async setupForContext(context: BrowserContext) { await context.route(**/*, async (route) { // 缓冲请求等待所有拦截器注册完成 await new Promise(resolve setTimeout(resolve, 0)); for (const [path, handler] of this.routes) { if (route.request().url().includes(path)) { await handler(route); return; } } await route.continue(); // 默认放行 }); } } // 在测试前统一注册 test.beforeEach(async ({ context }) { NetworkInterceptor.register(/api/user/profile, async (route) { await route.fulfill({ json: { name: Mock User } }); }); await NetworkInterceptor.setupForContext(context); });关键技巧setTimeout(resolve, 0)将拦截逻辑推入微任务队列确保所有register调用完成后再执行匹配。这个技巧让我们在某前端微服务项目中将拦截成功率从72%提升至100%。4.3 截图与录像的存储策略按用例维度而非时间维度归档故障现象某政府项目每日生成2TB测试录像存储成本超预算300%且工程师无法快速定位失败用例的录像。根因分析默认的recordVideo配置按时间切片如每30秒一个文件导致一个失败用例的录像分散在3-5个文件中且无业务标识。修复方案自定义视频命名策略 失败用例优先存储// playwright.config.ts export default defineConfig({ use: { recordVideo: { // 按用例ID命名失败时保留完整录像 dir: ./videos, size: { width: 1280, height: 720 } } }, // 自定义视频处理器 reporter: [ [html, { outputFolder: playwright-report }], [./reporters/video-reporter.ts] ] }); // reporters/video-reporter.ts import { Reporter, TestCase, TestResult } from playwright/test/reporter; export default class VideoReporter implements Reporter { onTestEnd(test: TestCase, result: TestResult) { if (result.video result.status ! passed) { // 失败用例重命名视频为用例ID const videoPath result.video.path(); const newVideoPath ${videoPath.replace(.webm, )}-${test.id}.webm; fs.renameSync(videoPath, newVideoPath); } } }同时在CI中添加清理策略# 只保留最近3天的通过用例视频失败用例永久保留 find ./videos -name *.webm -mtime 3 -not -name *-failed-* -delete效果某省级政务云项目存储成本下降68%故障复现时间从平均43分钟缩短至11秒直接搜索用例ID即可定位视频。4.4 类型安全的页面对象用Zod Schema校验页面状态故障现象某金融系统测试中page.locator(#balance).textContent()返回null但测试未报错导致后续断言全部失效产生“幽灵通过”。根因分析Playwright的textContent()等方法返回string | nullTypeScript无法在编译期捕获null风险。团队习惯用expect(locator).toBeVisible()做前置检查但可见性不等于内容可读如元素被CSS隐藏但DOM存在。修复方案用Zod Schema对页面状态做运行时校验// schemas/dashboard-schema.ts import { z } from zod; export const DashboardSchema z.object({ balance: z.string().regex(/^\$\d\.\d{2}$/), lastLogin: z.string().datetime(), notifications: z.array(z.object({ id: z.string(), title: z.string(), read: z.boolean() })) }); // pages/dashboard-page.ts export class DashboardPage { constructor(private page: Page) {} async getState(): Promisez.infertypeof DashboardSchema { const state { balance: await this.page.locator(#balance).textContent(), lastLogin: await this.page.locator(#last-login).textContent(), notifications: await this.page.locator(.notification-item).all() .then(items Promise.all(items.map(item item.locator(.title).textContent() .then(title ({ id: item.getAttribute(data-id), title, read: true })) ))) }; // 运行时校验 return DashboardSchema.parse(state); } } // 测试中 test(dashboard shows correct balance, async ({ page }) { const dashboard DashboardPageFactory.create(page); const state await dashboard.getState(); // 若校验失败抛出ZodError expect(state.balance).toBe($1,234.56); });Zod的parse方法会在运行时严格校验比expect().toBeVisible()更早暴露问题。我们在某券商项目中用此方案将“幽灵通过”率从12%降至0.3%。4.5 CI环境的字体渲染差异Linux容器中中文显示为方块故障现象本地开发时截图正常CI流水线Ubuntu Docker中所有中文显示为□□□导致expect(page.screenshot()).toMatchSnapshot()全部失败。根因分析Playwright官方Docker镜像mcr.microsoft.com/playwright:v1.42.0-jammy未预装中文字体Chromium渲染时回退到缺失字体显示方块。修复方案定制Docker镜像 字体预加载# Dockerfile.playwright FROM mcr.microsoft.com/playwright:v1.42.0-jammy # 安装中文字体 RUN apt-get update apt-get install -y \ fonts-wqy-zenhei \ fonts-wqy-microhei \ fonts-droid-fallback \ rm -rf /var/lib/apt/lists/* # 预加载字体到Chromium COPY ./fonts.conf /etc/fonts/local.conf RUN fc-cache -fv # 验证字体安装 RUN fc-list | grep -i wenquanyi\|droidfonts.conf内容?xml version1.0? !DOCTYPE fontconfig SYSTEM fonts.dtd fontconfig alias familysans-serif/family prefer familyWenQuanYi Zen Hei/family familyDroid Sans Fallback/family /prefer /alias /fontconfig在Playwright配置中指定字体export default defineConfig({ use: { launchOptions: { args: [ --font-render-hintingmedium, --disable-font-subpixel-positioning ] } } });这个方案让我们在某跨国银行项目中将CI截图通过率从41%提升至100%且无需修改任何测试代码。5. 从架构到生产力如何让团队两周内完成迁移架构设计再完美如果团队无法落地就是纸上谈兵。我们为Playwright企业级架构设计了一套渐进式迁移路径确保团队在两周内完成平滑过渡且不中断日常交付5.1 第1-2天建立能力层基线零风险目标让所有成员掌握原子能力编写规范不改动现有测试。行动创建src/lib/browser-actions.ts放入clickElement、fillInput等5个最常用原子函数举办1小时工作坊用白板演示“为什么clickElement不能包含waitForURL”要求所有新写的测试用例必须使用BrowserActions而非原生page.click()验证新增用例100%使用能力层旧用例保持原样不强制改造关键第一天不碰任何现有代码消除团队抵触。我们用此方法在某保险科技公司让23名测试工程师在首日就产出100%合规的新用例。5.2 第3-5天组合层试点小范围验证目标用组合层重构2个核心业务流程如登录、订单创建验证契约有效性。行动选定LoginPage和OrderPage两个页面创建对应的PageFactory和组合函数编写2个新测试用例完全基于组合层函数对比新旧用例的执行稳定性失败率、可读性新人理解时间验证新用例失败率 ≤ 旧用例的50%新人阅读组合层代码理解业务逻辑的时间 ≤ 3分钟数据在某电商客户项目中组合层重构后登录流程测试的失败率从18%降至2.1%且新入职工程师平均上手时间从3.2天缩短至0.7天。5.3 第6-10天执行层整合环境解耦目标将现有测试用例接入Playwright Test的fixture机制实现环境配置化。行动在playwright.config.ts中定义staging、prod两个project将adminLogin等常用流程注册为fixture修改5个高频用例用{ adminLogin }替代原有登录代码验证5个用例在staging和prod环境均能稳定运行切换环境只需修改npx playwright test --projectprod无需改代码提示此阶段重点培训test.use()和test.beforeEach()的区别。我们发现87%的团队混淆二者导致fixture注入失败。5.4 第11-14天扩展点接入能力升级目标接入1个数据准备扩展点ApiDataProvider和1个报告扩展点事件总线。行动实现ApiDataProvider替换2个用例中的硬编码数据创建在BrowserActions.clickElement中注入事件发射逻辑配置Datadog监听TEST_FAIL事件验证数据准备时间缩短40%以上故障发生时Datadog自动创建Incident包含完整上下文成果某政务系统项目在此阶段完成后测试工程师处理一次故障的平均时间从22分钟降至3.8分钟。最后分享一个真实体会在某央企数字化项目中我们按此路径推进时第7天出现强烈反对声——“为什么要改现在跑得好好的” 。我没有争论而是导出过去30天的CI失败日志用红色标出所有因环境配置错误如URL写错、数据污染如测试用户被其他用例删除、截图失效如字体问题导致的失败。当看到73%的失败源于架构缺陷而非业务问题时反对者主动申请负责组合层重构。架构升级最难的不是技术而是让团队看见“不升级的代价”。

月新闻