欢迎光临
我们一直在努力

零成本隐私AI!1套React+WebGPU端侧模型项目,吃透组件封装、Tailwind、浏览器本地大模型全流程

前言

你还在调用第三方AI接口?按token扣费、用户输入数据全上传服务器、隐私风险拉满。

最近我完整落地一套浏览器本地跑DeepSeek-R1大模型项目,全程不用后端、不用API密钥,依靠WebGPU调用本地显卡推理,断网也能用。

整个项目覆盖前端全套核心技能: React+TS工程规范、Tailwind原子CSS、可复用业务组件封装、WebGPU兼容性判断、模型下载进度条、Hooks状态管理、JSX合成事件。

在这里插入图片描述

读完你能学会:

  • 端侧LLM和云端API/Ollama本地部署的核心区别
  • React函数组件、Props/State响应式数据完整设计思路
  • 从零封装通用文件下载进度条组件(带字节格式化、空值容错)
  • Tailwind底层原理、JSX、合成事件底层知识点
  • WebGPU浏览器大模型落地完整页面实现、兼容降级方案
  • 项目开发中高频踩坑点与修复方案
  • 一、为什么选择WebGPU端侧模型?3大核心优势

    市面上AI使用方案分三类,对比差距非常明显:

    1. 云端API(OpenAI/DeepSeek官方接口)

    • 缺点:按调用计费、用户prompt全部上传服务端,隐私泄露风险高;并发高时接口限流、响应延迟不可控

    2. Ollama本地部署

    • 缺点:需要用户手动安装客户端、下载模型,门槛高,无法开箱即用

    3. WebGPU浏览器端侧模型(本项目方案)

    ✅ 零安装:打开网页自动下载量化小模型,开箱即用 ✅ 数据本地闭环:所有对话、推理仅在浏览器运行,不上传任何内容 ✅ 离线可用:模型下载完成后断网也能对话 ✅ 低成本:无接口费用,仅消耗用户本地显卡算力 ✅ 适配多端:PC浏览器、平板、高性能移动端均可运行

    配套技术栈: React18 + TypeScript + TailwindCSS + Transformers.js + ONNX Runtime Web

    二、项目基础技术前置知识点

    2.1 React + TS 大型项目优势

    AI相关前端项目优先选择React+TS,原因:

  • 大型工程代码约束更强,配合ESLint统一代码风格,多人协作无分歧
  • AI训练、推理相关开源库大多基于React生态,生态完善
  • TypeScript静态类型提前拦截报错,线上bug大幅减少
  • 函数式组件+Hooks逻辑复用能力远强于Vue选项式API
  • 2.2 TailwindCSS 运行原理(告别手写CSS)

    很多人只会复制Tailwind类名,底层原理一知半解:

  • 不属于原生CSS,是原子化CSS框架,预设海量基础样式类
  • 依靠Vite/Webpack插件扫描代码中用到的class,自动打包对应样式,无冗余代码
  • 无需手写选择器、CSS规则,开发效率提升一倍以上
  • JSX中不能使用class关键字(JS类语法冲突),统一使用className
  • 2.3 JSX与React合成事件

    JSX核心作用

    React专属语法,支持JS内直接书写XML式HTML标签,编译后转为原生DOM操作,是React核心特性。

    // JSX代码
    <div className="box">文本</div>
    // 编译后原生JS
    React.createElement("div", {className: "box"}, "文本")

    合成事件底层逻辑
  • 原生DOM绑定分为3级:
    • DOM0:行内onclick(耦合严重,不推荐)
    • DOM2:addEventListener标准事件监听
  • React中onClick并非原生事件,是合成事件:统一事件委托、抹平浏览器兼容性差异,不直接绑定到DOM节点,性能更好。
  • 2.4 React两种核心数据:Props & State

    组件内数据分为两类,规范边界不能混淆:

  • State(组件内部状态) 通过useState声明,组件自身管理、可内部修改,修改后自动驱动视图刷新。 示例:模型加载状态、输入框文本、下载进度数组。
  • Props(父传子属性) 从父组件传入子组件,子组件禁止直接修改,如需变更必须通知父组件回调更新。 示例:进度条百分比、文件名称、文件总大小。
  • 三、实战1:封装通用Progress进度条组件(完整可运行代码)

    项目需要展示模型分片下载进度,抽离独立复用组件,兼顾容错、字节格式化、空值兼容。

    3.1 工具函数:字节单位格式化

    模型文件动辄几百MB,封装方法自动转换B/kB/MB // Provarfunction formatBytes(size: number) { // size为0直接使用B单位 const i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024)); return ( +(size / Math.pow(1024, i)).toFixed(2) * 1 + [“B”, “kB”, “MB”, “GB”, “TB”][i] ); }

    ### 3.2 完整进度条组件代码
    核心亮点:
    – 使用ES12空值合并赋值`percentage ??= 0`,兼容父组件不传进度的场景
    – 动态宽度行内样式控制进度填充
    – 自动判断是否传入文件总大小,展示容量信息
    – Tailwind原子类完成全部样式,无独立css文件
    ```typescript
    // Progress.ts
    const Progress = ({ text, percentage, total }: {
    text: string;
    percentage?: number;
    total?: number;
    }) => {
    // 容错:不传百分比默认赋值0
    percentage ??= 0;
    return (
    <div className="w-full bg-gray-100 text-left rounded-lg overflow-hidden mb-0.5">
    {/* 动态宽度控制进度 */}
    <div
    style={{ width: `${percentage}%` }}
    className={`bg-blue-400 whitespace-nowrap px-1 text-sm`}
    >
    {text} {percentage.toFixed(2)}%
    {/* 无总大小则不展示容量文本 */}
    {isNaN(total) ? "" : ` of ${formatBytes(total)}`}
    </div>
    </div>
    );
    };

    export default Progress;

    3.3 组件踩坑提醒

  • 不要在子组件内部修改percentage,会破坏单向数据流,必须由父组件更新state传递
  • ??=空值合并仅拦截null/undefined,传入0、空字符串不会覆盖,适配业务默认值场景
  • 进度宽度必须写在行内style,Tailwind无法动态拼接百分比类名
  • 四、实战2:WebGPU本地大模型完整页面App.tsx

    页面实现功能:

  • WebGPU浏览器兼容性检测,不支持直接降级提示
  • 模型加载状态管理(ready/loading/error)
  • 多文件并行下载进度条列表渲染
  • 聊天输入框,回车发送对话、Shift+Enter换行
  • 加载按钮状态禁用控制,错误提示展示 完整代码:
  • // App.tsx
    import { useState, useEffect } from 'react';
    import Progress from '../components/Progress';

    function App() {
    // 输入框文本状态
    const [input, setInput] = useState('');
    // 模型状态:ready就绪 / loading下载中 / error报错
    const [status, setStatus] = useState("ready");
    // 错误信息存储
    const [error, setError] = useState<null | string>(null);
    // 下载提示文案
    const [loadingMessage, setLoadingMessage] = useState("开始加载");
    // 多模型文件下载进度数组
    const [progressItems, setProgressItems] = useState<Array<{
    text: string;
    percentage?: number;
    total?: number;
    }>>
    ([]);

    // WebGPU兼容性判断
    const IS_WEBGPU_AVALABLE = !!navigator.gpu;

    // 发送对话回调
    const onEnter = () => {
    console.log('发送提问:', input);
    // 后续可接入Transformers.js推理逻辑
    };

    // 组件挂载生命周期
    useEffect(() => {
    console.log('页面组件挂载完成');
    }, []);

    return (
    IS_WEBGPU_AVALABLE ? (
    <div className="flex flex-col h-screen mx-auto items-center justify-end text-gray-800 bg-white">
    <div className="h-full overflow-auto flex justify-center items-center flex-col relative">
    <div className="flex flex-col items-center mb-1 max-w-[400px] text-center">
    <h1 className="text-4xl font-bold mb-1">DeepSeek–R1 WebGPU</h1>
    <h2 className="font-semibold">
    浏览器本地运行推理模型,无需后端、保护隐私
    </h2>
    </div>

    <div className="flex flex-col items-center px-4">
    <p className="max-w-[510px] mb-4">
    加载 <a
    href="https://huggingface.co/onnx-community/DeepSeek-R1-Distill-Qwen-1.5B-ONNX"
    target="_blank"
    rel="noreferrer"
    className="font-medium underline"
    >DeepSeek–R1 1.5B蒸馏轻量化模型</a>,基于Transformers.js+ONNX Web运行,所有数据仅保存在本地,支持离线对话。
    </p>

    {/* 错误提示区域 */}
    {error && (
    <div className="text-red-500 text-center mb-2">
    <p className="mb-1">模型加载失败:</p>
    <p className="text-sm">{error}</p>
    </div>
    )}

    {/* 加载按钮,加载/报错状态禁用 */}
    <button
    className="border px-4 py-2 rounded-lg bg-blue-400 text-white hover:bg-blue-500 disabled:cursor-not-allowed select-none"
    disabled={status !== "ready" || error !== null}
    onClick={() => setStatus("loading")}
    >
    加载本地模型
    </button>
    </div>
    </div>

    {/* 模型下载进度列表 */}
    {status === "loading" && (
    <div className="w-full max-w-[500px] text-left mx-auto p-4 mt-auto">
    <p className="text-center mb-1">{loadingMessage}</p>
    {progressItems.map((item, i) => (
    <Progress
    key={i}
    text={item.text}
    percentage={item.percentage}
    total={item.total}
    />
    ))}
    </div>
    )}

    {/* 聊天输入框 */}
    <div className="mt-2 border border-gray-300 rounded-lg w-[600px] max-w-[80%] max-h-[200px] mx-auto relative mb-3 flex">
    <textarea
    className="w-full px-3 py-4 rounded-lg bg-transparent border-none outline-hidden disabled:text-gray-400"
    placeholder="输入你的问题…"
    rows={1}
    disabled={status !== "ready"}
    value={input}
    // 双向绑定模拟:onInput同步文本
    onInput={(e) => {
    const target = e.target as HTMLTextAreaElement;
    setInput(target.value);
    }}
    // 回车发送,Shift+Enter换行
    onKeyDown={(e) => {
    if (input.length > 0 && e.key === 'Enter' && !e.shiftKey) {
    e.preventDefault();
    onEnter();
    }
    }}
    title={status === 'ready' ? '模型就绪,可以提问' : "请等待模型加载完成"}
    />
    </div>
    </div>
    ) : (
    <div className="h-screen flex items-center justify-center text-xl text-gray-600">
    当前浏览器不支持WebGPU,无法运行本地AI模型,请升级Chrome/Edge浏览器
    </div>
    )
    );
    }

    export default App;

    页面开发踩坑清单

  • WebGPU兼容判断:必须双重取反!!navigator.gpu,将undefined转为布尔值,直接判断会出现类型报错
  • TextArea类型断言:TS无法自动识别e.target为HTMLTextAreaElement,使用as类型断言避免value不存在报错
  • 按钮禁用逻辑:加载中、报错状态均禁止重复点击,防止多次触发模型下载
  • 列表key规范:简单演示使用索引key,生产环境建议使用文件唯一名称做key,避免列表渲染错乱
  • React无双向绑定:Vue的v-model在React中需要value+onInput手动实现,完全受控组件保证数据一致性
  • 五、端侧AI项目落地核心痛点解决方案

    痛点1:用户浏览器不支持WebGPU

    解决方案:

  • 页面初始化navigator.gpu检测,不支持直接展示降级提示
  • Transformers.js内置WASM CPU降级,可自动切换CPU推理(速度较慢,适合低配置设备)
  • 痛点2:大模型文件下载缓慢、重复下载

    解决方案:

  • 浏览器IndexedDB缓存模型文件,第二次打开页面无需重复下载
  • 多文件分片并行下载,搭配进度条实时展示下载状态,提升用户感知
  • 痛点3:模型下载过程页面卡死

    解决方案: 使用Web Worker单独运行模型加载、推理逻辑,脱离主线程,UI操作不会阻塞

    痛点4:进度数据状态混乱,多文件进度互相覆盖

    解决方案: 使用数组存储每个文件独立进度对象,循环渲染独立进度条,每项数据隔离互不干扰

    六、整体项目架构总结

    1. 分层结构

    • 页面层:App.tsx 负责整体布局、状态管理、交互逻辑
    • 通用组件层:Progress.ts 可复用业务组件,纯展示接收Props
    • 工具函数层:formatBytes 通用计算工具,解耦业务逻辑

    2. 核心技术能力复盘

  • React Hooks:useState状态管理、useEffect生命周期副作用
  • TS类型约束:组件Props、事件对象类型断言,规避线上类型错误
  • Tailwind原子CSS:零手写css,快速搭建响应式页面 读完你能学会:
  • 端侧LLM和云端API/Ollama本地部署的核心区别
  • React函数组件、Props/State响应式数据完整设计思路
  • 从零封装通用文件下载进度条组件(带字节格式化、空值容错)
  • Tailwind底层原理、JSX、合成事件底层知识点
  • WebGPU浏览器大模型落地完整页面实现、兼容降级方案
  • 项目开发中高频踩坑点与修复方案
  • 一、为什么选择WebGPU端侧模型?3大核心优势

    市面上AI使用方案分三类,对比差距非常明显:

    1. 云端API(OpenAI/DeepSeek官方接口)

    • 缺点:按调用计费、用户prompt全部上传服务端,隐私泄露风险高;并发高时接口限流、响应延迟不可控

    2. Ollama本地部署

    • 缺点:需要用户手动安装客户端、下载模型,门槛高,无法开箱即用

    3. WebGPU浏览器端侧模型(本项目方案)

    ✅ 零安装:打开网页自动下载量化小模型,开箱即用 ✅ 数据本地闭环:所有对话、推理仅在浏览器运行,不上传任何内容 ✅ 离线可用:模型下载完成后断网也能对话 ✅ 低成本:无接口费用,仅消耗用户本地显卡算力 ✅ 适配多端:PC浏览器、平板、高性能移动端均可运行

    配套技术栈: React18 + TypeScript + TailwindCSS + Transformers.js + ONNX Runtime Web

    二、项目基础技术前置知识点

    2.1 React + TS 大型项目优势

    AI相关前端项目优先选择React+TS,原因:

  • 大型工程代码约束更强,配合ESLint统一代码风格,多人协作无分歧
  • AI训练、推理相关开源库大多基于React生态,生态完善
  • TypeScript静态类型提前拦截报错,线上bug大幅减少
  • 函数式组件+Hooks逻辑复用能力远强于Vue选项式API
  • 2.2 TailwindCSS 运行原理(告别手写CSS)

    很多人只会复制Tailwind类名,底层原理一知半解:

  • 不属于原生CSS,是原子化CSS框架,预设海量基础样式类
  • 依靠Vite/Webpack插件扫描代码中用到的class,自动打包对应样式,无冗余代码
  • 无需手写选择器、CSS规则,开发效率提升一倍以上
  • JSX中不能使用class关键字(JS类语法冲突),统一使用className
  • 2.3 JSX与React合成事件

    JSX核心作用

    React专属语法,支持JS内直接书写XML式HTML标签,编译后转为原生DOM操作,是React核心特性。

    // JSX代码
    <div className="box">文本</div>
    // 编译后原生JS
    React.createElement("div", {className: "box"}, "文本")

    合成事件底层逻辑
  • 原生DOM绑定分为3级:
    • DOM0:行内onclick(耦合严重,不推荐)
    • DOM2:addEventListener标准事件监听
  • React中onClick并非原生事件,是合成事件:统一事件委托、抹平浏览器兼容性差异,不直接绑定到DOM节点,性能更好。
  • 2.4 React两种核心数据:Props & State

    组件内数据分为两类,规范边界不能混淆:

  • State(组件内部状态) 通过useState声明,组件自身管理、可内部修改,修改后自动驱动视图刷新。 示例:模型加载状态、输入框文本、下载进度数组。
  • Props(父传子属性) 从父组件传入子组件,子组件禁止直接修改,如需变更必须通知父组件回调更新。 示例:进度条百分比、文件名称、文件总大小。
  • 三、实战1:封装通用Progress进度条组件(完整可运行代码)

    项目需要展示模型分片下载进度,抽离独立复用组件,兼顾容错、字节格式化、空值兼容。

    3.1 工具函数:字节单位格式化

    模型文件动辄几百MB,封装方法自动转换B/kB/MB/GB/TB:

    // Progress.ts
    function formatBytes(size: number) {
    // size为0直接使用B单位
    const i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
    return (
    +(size / Math.pow(1024, i)).toFixed(2) * 1 +
    ["B", "kB", "MB", "GB", "TB"][i]
    );
    }

    3.2 完整进度条组件代码

    核心亮点:

    • 使用ES12空值合并赋值percentage ??= 0,兼容父组件不传进度的场景
    • 动态宽度行内样式控制进度填充
    • 自动判断是否传入文件总大小,展示容量信息
    • Tailwind原子类完成全部样式,无独立css文件

    // Progress.ts
    const Progress = ({ text, percentage, total }: {
    text: string;
    percentage?: number;
    total?: number;
    }) => {
    // 容错:不传百分比默认赋值0
    percentage ??= 0;
    return (
    <div className="w-full bg-gray-100 text-left rounded-lg overflow-hidden mb-0.5">
    {/* 动态宽度控制进度 */}
    <div
    style={{ width: `${percentage}%` }}
    className={`bg-blue-400 whitespace-nowrap px-1 text-sm`}
    >
    {text} {percentage.toFixed(2)}%
    {/* 无总大小则不展示容量文本 */}
    {isNaN(total) ? "" : ` of ${formatBytes(total)}`}
    </div>
    </div>
    );
    };

    export default Progress;

    3.3 组件踩坑提醒

  • 不要在子组件内部修改percentage,会破坏单向数据流,必须由父组件更新state传递
  • ??=空值合并仅拦截null/undefined,传入0、空字符串不会覆盖,适配业务默认值场景
  • 进度宽度必须写在行内style,Tailwind无法动态拼接百分比类名
  • 四、实战2:WebGPU本地大模型完整页面App.tsx

    页面实现功能:

  • WebGPU浏览器兼容性检测,不支持直接降级提示
  • 模型加载状态管理(ready/loading/error)
  • 多文件并行下载进度条列表渲染
  • 聊天输入框,回车发送对话、Shift+Enter换行
  • 加载按钮状态禁用控制,错误提示展示 完整代码:
  • // App.tsx
    import { useState, useEffect } from 'react';
    import Progress from '../components/Progress';

    function App() {
    // 输入框文本状态
    const [input, setInput] = useState('');
    // 模型状态:ready就绪 / loading下载中 / error报错
    const [status, setStatus] = useState("ready");
    // 错误信息存储
    const [error, setError] = useState<null | string>(null);
    // 下载提示文案
    const [loadingMessage, setLoadingMessage] = useState("开始加载");
    // 多模型文件下载进度数组
    const [progressItems, setProgressItems] = useState<Array<{
    text: string;
    percentage?: number;
    total?: number;
    }>>
    ([]);

    // WebGPU兼容性判断
    const IS_WEBGPU_AVALABLE = !!navigator.gpu;

    // 发送对话回调
    const onEnter = () => {
    console.log('发送提问:', input);
    // 后续可接入Transformers.js推理逻辑
    };

    // 组件挂载生命周期
    useEffect(() => {
    console.log('页面组件挂载完成');
    }, []);

    return (
    IS_WEBGPU_AVALABLE ? (
    <div className="flex flex-col h-screen mx-auto items-center justify-end text-gray-800 bg-white">
    <div className="h-full overflow-auto flex justify-center items-center flex-col relative">
    <div className="flex flex-col items-center mb-1 max-w-[400px] text-center">
    <h1 className="text-4xl font-bold mb-1">DeepSeek–R1 WebGPU</h1>
    <h2 className="font-semibold">
    浏览器本地运行推理模型,无需后端、保护隐私
    </h2>
    </div>

    <div className="flex flex-col items-center px-4">
    <p className="max-w-[510px] mb-4">
    加载 <a
    href="https://huggingface.co/onnx-community/DeepSeek-R1-Distill-Qwen-1.5B-ONNX"
    target="_blank"
    rel="noreferrer"
    className="font-medium underline"
    >DeepSeek–R1 1.5B蒸馏轻量化模型</a>,基于Transformers.js+ONNX Web运行,所有数据仅保存在本地,支持离线对话。
    </p>

    {/* 错误提示区域 */}
    {error && (
    <div className="text-red-500 text-center mb-2">
    <p className="mb-1">模型加载失败:</p>
    <p className="text-sm">{error}</p>
    </div>
    )}

    {/* 加载按钮,加载/报错状态禁用 */}
    <button
    className="border px-4 py-2 rounded-lg bg-blue-400 text-white hover:bg-blue-500 disabled:cursor-not-allowed select-none"
    disabled={status !== "ready" || error !== null}
    onClick={() => setStatus("loading")}
    >
    加载本地模型
    </button>
    </div>
    </div>

    {/* 模型下载进度列表 */}
    {status === "loading" && (
    <div className="w-full max-w-[500px] text-left mx-auto p-4 mt-auto">
    <p className="text-center mb-1">{loadingMessage}</p>
    {progressItems.map((item, i) => (
    <Progress
    key={i}
    text={item.text}
    percentage={item.percentage}
    total={item.total}
    />
    ))}
    </div>
    )}

    {/* 聊天输入框 */}
    <div className="mt-2 border border-gray-300 rounded-lg w-[600px] max-w-[80%] max-h-[200px] mx-auto relative mb-3 flex">
    <textarea
    className="w-full px-3 py-4 rounded-lg bg-transparent border-none outline-hidden disabled:text-gray-400"
    placeholder="输入你的问题…"
    rows={1}
    disabled={status !== "ready"}
    value={input}
    // 双向绑定模拟:onInput同步文本
    onInput={(e) => {
    const target = e.target as HTMLTextAreaElement;
    setInput(target.value);
    }}
    // 回车发送,Shift+Enter换行
    onKeyDown={(e) => {
    if (input.length > 0 && e.key === 'Enter' && !e.shiftKey) {
    e.preventDefault();
    onEnter();
    }
    }}
    title={status === 'ready' ? '模型就绪,可以提问' : "请等待模型加载完成"}
    />
    </div>
    </div>
    ) : (
    <div className="h-screen flex items-center justify-center text-xl text-gray-600">
    当前浏览器不支持WebGPU,无法运行本地AI模型,请升级Chrome/Edge浏览器
    </div>
    )
    );
    }

    export default App;

    页面开发踩坑清单

  • WebGPU兼容判断:必须双重取反!!navigator.gpu,将undefined转为布尔值,直接判断会出现类型报错
  • TextArea类型断言:TS无法自动识别e.target为HTMLTextAreaElement,使用as类型断言避免value不存在报错
  • 按钮禁用逻辑:加载中、报错状态均禁止重复点击,防止多次触发模型下载
  • 列表key规范:简单演示使用索引key,生产环境建议使用文件唯一名称做key,避免列表渲染错乱
  • React无双向绑定:Vue的v-model在React中需要value+onInput手动实现,完全受控组件保证数据一致性
  • 五、端侧AI项目落地核心痛点解决方案

    痛点1:用户浏览器不支持WebGPU

    解决方案:

  • 页面初始化navigator.gpu检测,不支持直接展示降级提示
  • Transformers.js内置WASM CPU降级,可自动切换CPU推理(速度较慢,适合低配置设备)
  • 痛点2:大模型文件下载缓慢、重复下载

    解决方案:

  • 浏览器IndexedDB缓存模型文件,第二次打开页面无需重复下载
  • 多文件分片并行下载,搭配进度条实时展示下载状态,提升用户感知
  • 痛点3:模型下载过程页面卡死

    解决方案: 使用Web Worker单独运行模型加载、推理逻辑,脱离主线程,UI操作不会阻塞

    痛点4:进度数据状态混乱,多文件进度互相覆盖

    解决方案: 使用数组存储每个文件独立进度对象,循环渲染独立进度条,每项数据隔离互不干扰

    六、整体项目架构总结

    1. 分层结构

    • 页面层:App.tsx 负责整体布局、状态管理、交互逻辑
    • 通用组件层:Progress.ts 可复用业务组件,纯展示接收Props
    • 工具函数层:formatBytes 通用计算工具,解耦业务逻辑

    2. 核心技术能力复盘

  • React Hooks:useState状态管理、useEffect生命周期副作用
  • TS类型约束:组件Props、事件对象类型断言,规避线上类型错误
  • Tailwind原子CSS:零手写css,快速搭建响应式页面
  • 赞(0)
    未经允许不得转载:171主机测评 » 零成本隐私AI!1套React+WebGPU端侧模型项目,吃透组件封装、Tailwind、浏览器本地大模型全流程
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址