当前位置: 首页 > news >正文

谈谈检测浏览器类型

前几天被问到如何检测浏览器类型,我突然发现我对此并不了解,之前的项目中也没有使用到过,只隐约记得通过一个自带的方法即可获取。所以今天特意来仔细补习一下。

核心:navigator.userAgent

1.正则表达式

2.引用外部库

3.判断浏览器的其他特性

一.首先讲一下navigator.userAgent

navigator.userAgent 是一个包含用户代理字符串(User Agent String)的属性,这个字符串提供了关于用户浏览器操作系统设备的信息。用户代理字符串的生成和传递涉及浏览器和服务器之间的通信过程。

用户代理字符串包含主要内容:

-用户浏览器

-操作系统

-设备信息

接下来从构成、生成原理、传递解析和局限性几个方面来讲解一下用户代理字符串:

1.1用户代理字符串的构成

  1. 浏览器名称和版本:标识浏览器的名称和版本号。【这个内容导致经常用于判断浏览器类型场景上!】
  2. 操作系统:标识用户使用的操作系统及其版本。
  3. 渲染引擎:标识浏览器使用的渲染引擎。
  4. 设备信息:标识设备类型(比如手机、平板、桌面)。
举个例子
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36

我们来分析一下这个例子用户代理字符串中包含了哪些信息呢

  • Mozilla/5.0:兼容性标识符,表示兼容 Mozilla 的浏览器。
  • (Windows NT 10.0; Win64; x64):操作系统信息,表示 Windows 10 64 位操作系统。
  • AppleWebKit/537.36:渲染引擎,表示使用 WebKit 内核。
  • (KHTML, like Gecko):表示兼容 Gecko 内核。
  • Chrome/92.0.4515.107:浏览器名称和版本号,表示 Chrome 92。
  • Safari/537.36:表示兼容 Safari 537.36。【这里注意,正则判断时,由于兼容性标识,也有可能chrome浏览器识别到safari字段】

1.2用户代理字符串的生成

  1. 浏览器内部生成
    • 浏览器在初始化时,根据其内置的信息和用户的操作系统,生成用户代理字符串。
    • 这些信息通常在浏览器的配置文件中定义,浏览器开发者在开发时设定。
  1. 用户定制
    • 某些浏览器允许用户通过设置或扩展自定义用户代理字符串。用户可以修改用户代理字符串以伪装成不同的浏览器或设备。【这一点导致了他的不可靠性】

1.3用户代理字符串的传递(浏览器和服务端之间)

  1. HTTP 请求
    • 当用户访问网页时,浏览器会发送 HTTP 请求到服务器。在请求头中包含一个名为User-Agent的字段,携带用户代理字符串。
    • 示例 HTTP 请求头:
GET / HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36
  1. 服务器处理
    • 服务器接收到请求后,可以读取User-Agent字段,解析用户代理字符串以了解客户端的浏览器、操作系统和设备信息。
    • 基于这些信息,服务器可以返回相应的内容或进行特定的处理,如返回适配移动设备的网页。

1.4用户代理字符串的解析

知道了用户代理字符串的内容之后,我们需要把他运用到实处,那么很重要的一点就是如何对其进行解析!

  1. 前端解析
    • 在前端 JavaScript 代码中,可以通过navigator.userAgent获取用户代理字符串,并使用正则表达式字符串匹配解析其中的信息。【这也是下一节要重点讲的内容之一】
    • 示例代码:
const userAgent = navigator.userAgent;
console.log(userAgent); // 输出用户代理字符串
  1. 后端解析
    • 在服务器端,可以使用编程语言(如 Python、Java、Node.js)解析用户代理字符串,进行用户设备和浏览器的识别。
    • 示例代码(Node.js):
const http = require('http');http.createServer((req, res) => {const userAgent = req.headers['user-agent'];console.log(userAgent); // 输出用户代理字符串res.writeHead(200, {'Content-Type': 'text/plain'});res.end('Hello World\n');
}).listen(8080);

1.5用户代理字符串的局限性

  1. 不可靠性
    • 用户代理字符串可以被用户修改或伪装,因此基于其进行的判断不总是可靠的。
    • 某些浏览器或扩展允许用户自定义用户代理字符串,导致误判。
  1. 复杂性和变化
    • 用户代理字符串格式复杂且随时间变化,不同浏览器和版本的格式可能不同。
    • 解析用户代理字符串需要持续更新解析规则,以应对新浏览器和版本的变化。
  1. 特性检测优先
    • 现代 Web 开发推荐使用特性检测,而不是依赖用户代理字符串。特性检测直接检测浏览器是否支持特定功能,更加可靠。
    • 示例代码(特性检测):
if ('geolocation' in navigator) {console.log('Geolocation is supported');
} else {console.log('Geolocation is not supported');
}

二.学习完用户代理字符串,回到正题,如何判断浏览器类型

  • 正则表达式
  • match结合正则表达式
  • 第三方库(最简单便捷)
  • 条件注释(老方法了,了解即可)
  • 判断特性(最推荐最可靠的!)

2.1直接使用正则表达式匹配 navigator.userAgent

function getBrowserType() {const userAgent = navigator.userAgent;if (userAgent.indexOf("Firefox") > -1) {return "Firefox";} else if (userAgent.indexOf("SamsungBrowser") > -1) {return "Samsung Internet";} else if (userAgent.indexOf("Opera") > -1 || userAgent.indexOf("OPR") > -1) {return "Opera";} else if (userAgent.indexOf("Trident") > -1) {return "Internet Explorer";} else if (userAgent.indexOf("Edge") > -1) {return "Microsoft Edge";} else if (userAgent.indexOf("Chrome") > -1) {return "Chrome";} else if (userAgent.indexOf("Safari") > -1) {return "Safari";} else {return "Unknown";}
}console.log(getBrowserType());
  • navigator.userAgent获取了用户代理字符串。
  • indexOf方法用于检查字符串中是否包含特定的子字符串。如果包含,返回子字符串在字符串中的第一个索引,否则返回 -1
  • 判断不同浏览器标志来返回相应的浏览器类型。

这个方法简单直观,容易理解,是学习了用户代理字符串后最容易想到的方法之一,但是却不可靠,首先利用indexOf会存在大小写敏感问题,其次这么多的ifelse也使得维护工作做起来很困难。

2.2 match() 结合正则表达式

function getBrowserType() {const userAgent = navigator.userAgent;if (userAgent.match(/Firefox/i)) {return "Firefox";} else if (userAgent.match(/SamsungBrowser/i)) {return "Samsung Internet";} else if (userAgent.match(/Opera|OPR/i)) {return "Opera";} else if (userAgent.match(/Trident/i)) {return "Internet Explorer";} else if (userAgent.match(/Edge/i)) {return "Microsoft Edge";} else if (userAgent.match(/Chrome/i)) {return "Chrome";} else if (userAgent.match(/Safari/i)) {return "Safari";} else {return "Unknown";}
}console.log(getBrowserType());
  • match()方法在字符串中执行一个搜索匹配,并返回匹配结果的数组,如果没有找到匹配,则返回null。
  • 正则表达式中的i标志表示不区分大小写。(规避了直接使用indexOf大小写敏感问题)

这个方法其实和第一种差不多,而且正则表达式提供了更强的匹配能力,处理起来更灵活,但是大部分人对于正则表达式还是做不到说写就写的,所以对于不熟悉正则表达式的开发者可能有一定的学习成本。

2.3使用第三方库(如 bowser

import Bowser from "bowser";function getBrowserType() {const browser = Bowser.getParser(window.navigator.userAgent);return browser.getBrowserName();
}console.log(getBrowserType());
  • Bowser是一个 JavaScript 库,用于解析用户代理字符串,并提供 API 来获取浏览器和操作系统信息。
  • Bowser.getParser(userAgent)创建一个解析器对象。
  • parser.getBrowserName()返回浏览器名称

这种方法应该是很多人会选择使用的,直接使用第三方库,减少了自行编写和维护代码的工作量,而且库本身就提供了丰富的功能和良好的兼容性。但是呢显而易见需要引入额外的第三方库,也是增加了项目的依赖。

2.4使用条件注释(了解即可)

<script>var isIE = false;/*@cc_on@if (@_jscript_version)isIE = true;@end@*/if (isIE) {console.log("Internet Explorer");} else {console.log("Not Internet Explorer");}
</script>
  • 条件注释是 Internet Explorer 的一种特性,允许在 HTML 注释中包含条件代码。
  • @cc_on是开启条件编译的指令。
  • @_jscript_version是一个 JScript 变量,表示当前 JScript 引擎的版本。
  • 这种方法仅适用于 Internet Explorer,且现代浏览器不再支持条件注释。

2.5基于特性检测(最准确最推荐!)

function isIE() {return !!window.ActiveXObject || "ActiveXObject" in window;
}
function isEdge() {return !isIE() && !!window.StyleMedia;
}
if (isIE()) {console.log("Internet Explorer");
} else if (isEdge()) {console.log("Microsoft Edge");
} else {console.log("Not Internet Explorer or Edge");
}
  • 基于特性检测是通过检测特定浏览器的特性来判断浏览器类型,而不是依赖于userAgent
  • window.ActiveXObject"ActiveXObject" in window用于检测 Internet Explorer。
  • window.StyleMedia用于检测 Microsoft Edge。

这个方更加可靠,不依赖userAgent,避免了用户代理字符串可能被篡改的问题。(规避了不可靠性),但是缺点也很明显,需要知道每种浏览器特有的特性,增加了复杂性。

三、应用场景

对于我个人的项目经历来说,之前是几乎没有用到过浏览器类型判断的,所以,我还去了解了一下需要用到浏览器类型判断的一些应用场景。

3.1 浏览器特性检测和兼容性处理

最容易想到的第一个应用场景,就是对兼容性的处理。某些浏览器对特定的功能或 API 支持不一致。需要通过检测用户代理字符串,可以针对特定浏览器执行不同的代码,来确保功能的兼容性。

const userAgent = navigator.userAgent;if (userAgent.match(/Trident/i)) {// 针对 Internet Explorer 的特定处理
} else if (userAgent.match(/Edge/i)) {// 针对 Microsoft Edge 的特定处理
} else if (userAgent.match(/Chrome/i)) {// 针对 Chrome 的特定处理
} else if (userAgent.match(/Safari/i)) {// 针对 Safari 的特定处理
}

3.2 响应式设计和设备检测

第二个我想到的就是响应式,因为之前经常在项目中使用媒体查询去实现响应式涉及,我们知道很多页面在移动设备和桌面设备之间是不同的布局和功能。所以我们通常需要通过检测用户代理字符串,确定用户是否在使用移动设备,从而去调整页面布局和交互方式。

const userAgent = navigator.userAgent;if (/Mobi|Android/i.test(userAgent)) {// 针对移动设备的布局和交互处理
} else {// 针对桌面设备的布局和交互处理
}

3.3 分析和统计

对于一些数据埋点,有时候可能还需要考虑到触发时所在的环境,在网站分析和统计中,收集用户代理字符串信息可以帮助开发者了解用户的浏览器和设备使用情况,这个对于开发者是很有好处的。可以作为性能优化的量化依据。

// 将用户代理字符串发送到分析服务器
sendUserAgentToAnalytics(navigator.userAgent);function sendUserAgentToAnalytics(userAgent) {// 发送数据到服务器的代码
}

还有一些收集来的运用场景,自己想可能不太会想的到,不过看完之后就会觉得确实了:

3.4. 功能降级

某些情况有些特定功能可能不能再在某些浏览器使用。也可以通过检测用户代理字符串,·去选择替代方案。

再说直白点就比如很多用户使用很老的版本的设备或者未更新系统之类的,这里就可以运用到。

const userAgent = navigator.userAgent;if (userAgent.match(/MSIE|Trident/i)) {// 提供替代方案或警告用户使用现代浏览器alert("Your browser is not supported. Please upgrade to a modern browser.");
}

5. 动态加载资源

涉及到资源加载优化了,很多时候不同设备下的静态资源是截然不同的,可以根据用户的浏览器和设备类型,动态加载不同的资源(如 CSS、JavaScript 文件),以优化性能和用户体验。

<script>const userAgent = navigator.userAgent;if (/Mobi|Android/i.test(userAgent)) {// 加载移动设备的样式表document.write('<link rel="stylesheet" href="mobile.css">');} else {// 加载桌面设备的样式表document.write('<link rel="stylesheet" href="desktop.css">');}
</script>

6. 调试和日志记录

在调试和日志记录中,记录用户代理字符串可以帮助开发者了解用户的浏览器环境,从而更快地定位和解决问题。

console.log("User Agent: " + navigator.userAgent);// 将用户代理字符串记录到日志服务器
logUserAgent(navigator.userAgent);function logUserAgent(userAgent) {// 记录数据到服务器的代码
}

完。

相关文章:

谈谈检测浏览器类型

前几天被问到如何检测浏览器类型&#xff0c;我突然发现我对此并不了解&#xff0c;之前的项目中也没有使用到过&#xff0c;只隐约记得通过一个自带的方法即可获取。所以今天特意来仔细补习一下。 核心&#xff1a;navigator.userAgent 1.正则表达式 2.引用外部库 3.判断浏…...

Django 和 Django REST framework 创建对外 API

1. 环境准备 确保你已经安装了 Python 和 Django。如果尚未安装 Django REST framework&#xff0c;通过 pip 安装它&#xff1a; pip install djangorestframework 2. 创建 Django 项目 如果你还没有 Django 项目&#xff0c;可以通过以下命令创建&#xff1a; django-ad…...

数据结构之“刷链表题”

&#x1f339;个人主页&#x1f339;&#xff1a;喜欢草莓熊的bear &#x1f339;专栏&#x1f339;&#xff1a;数据结构 目录 前言 一、相交链表 题目链接 大致思路 代码实现 二、环形链表1 题目链接 大致思路 代码实现 三、环形链表2 题目链接 大致思路 代码实…...

复分析——第9章——椭圆函数导论(E.M. Stein R. Shakarchi)

第 9 章 椭圆函数导论 (An Introduction to Elliptic Functions) The form that Jacobi had given to the theory of elliptic functions was far from perfection; its flaws are obvious. At the base we find three fundamental functions sn, cn and dn. These functio…...

使用kubeadm安装k8s并部署应用

安装k8s 1. 准备机器 准备三台机器 192.168.136.104 master节点 192.168.136.105 worker节点 192.168.136.106 worker节点2. 安装前配置 1.基础环境 ######################################################################### #关闭防火墙&#xff1a; 如果是云服务器&…...

springMVC学习

概述 Spring MVC&#xff08;Model-View-Controller&#xff0c;模型-视图-控制器&#xff09;是Spring框架的一部分&#xff0c;用于构建基于Java的Web应用程序。它遵循MVC设计模式&#xff0c;分离了应用程序的不同方面&#xff08;输入逻辑、业务逻辑和UI逻辑&#xff09;&…...

深入探讨光刻技术:半导体制造的关键工艺

前言 光刻&#xff08;Photolithography&#xff09;是现代半导体制造过程中不可或缺的一环&#xff0c;它的精度和能力直接决定了芯片的性能和密度。本文将详细介绍光刻技术的基本原理、过程、关键技术及其在半导体制造中的重要性。 光刻技术的基本原理 光刻是一种利用光化…...

CesiumJS【Basic】- #042 绘制纹理线(Primitive方式)

文章目录 绘制纹理线(Primitive方式)1 目标2 代码2.1 main.ts3 资源文件绘制纹理线(Primitive方式) 1 目标 使用Primitive方式绘制纹理线 2 代码 2.1 main.ts var start = Cesium.Cartesian3.fromDegrees(-75.59777, 40.03883);var...

代码随想录第38天|动态规划

1049. 最后一块石头的重量 II 参考 备注: 当物体容量也等同于价值时, 01背包问题的含义则是利用好最大的背包容量sum/2, 使得结果尽可能的接近或者小于 sum/2 等价: 尽可能的平分成相同的两堆, 其差则为结果, 比如 (abc)-d, (ac)-(bd) , 最终的结果是一堆减去另外一堆的和, 问…...

java生成excel,uniapp微信小程序接收excel并打开

java引包&#xff0c;引的是apache.poi <dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml</artifactId><version>5.2.3</version></dependency> 写一个测试类&#xff0c;把excel输出到指定路径 public s…...

sam_out 目标检测的应用

缺点参考地址训练验证模型解析 缺点 词表太大量化才可 参考地址 https://aistudio.baidu.com/projectdetail/8103098 训练验证 import os from glob import glob import cv2 import paddle import faiss from out_yolo_model import GPT as GPT13 import pandas as pd imp…...

VLAN原理与配置

AUTHOR &#xff1a;闫小雨 DATE&#xff1a;2024-04-28 目录 VLAN的三种端口类型 VLAN原理 什么是VLAN 为什么使用VLAN VLAN的基本原理 VLAN标签 VLAN标签各字段含义如下&#xff1a; VLAN的划分方式 VLAN的划分包括如下5种方法&#xff1a; VLAN的接口链路类型 创建V…...

使用Spring Boot实现RESTful API

使用Spring Boot实现RESTful API 大家好&#xff0c;我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01;今天我们将深入探讨如何利用Spring Boot框架实现RESTful API&#xff0c;这是现…...

中英双语介绍美国常春藤联盟( Ivy League):八所高校

中文版 常春藤联盟简介 常春藤联盟&#xff08;Ivy League&#xff09;是美国东北部八所私立大学组成的高校联盟。虽然最初是因体育联盟而得名&#xff0c;但这些学校以其学术卓越、历史悠久、校友杰出而闻名于世。以下是对常春藤联盟的详细介绍&#xff0c;包括其由来、成员…...

【计算机网络】常见的网络通信协议

目录 1. TCP/IP协议 2. HTTP协议 3. FTP协议 4. SMTP协议 5. POP3协议 6. IMAP协议 7. DNS协议 8. DHCP协议 9. SSH协议 10. SSL/TLS协议 11. SNMP协议 12. NTP协议 13. VoIP协议 14. WebSocket协议 15. BGP协议 16. OSPF协议 17. RIP协议 18. ICMP协议 1…...

java实现http/https请求

在Java中&#xff0c;有多种方式可以实现HTTP或HTTPS请求。以下是使用第三方库Apache HttpClient来实现HTTP/HTTPS请求的工具类。 优势和特点 URIBuilder的优势在于它提供了一种简单而灵活的方式来构造URI&#xff0c;帮助开发人员避免手动拼接URI字符串&#xff0c;并处理参…...

NC204871 求和

链接 思路&#xff1a; 对于一个子树来说&#xff0c;子树的节点就包括在整颗树的dfs序中子树根节点出现的前后之间&#xff0c;所以我们先进行一次dfs&#xff0c;用b数组的0表示区间左端点&#xff0c;1表示区间右端点&#xff0c;同时用a数组来标记dfs序中的值。处理完dfs序…...

git克隆代码warning: could not find UI helper ‘git-credential-manager-ui‘

git克隆代码warning: could not find UI helper ‘git-credential-manager-ui’ 方案 git config --global --unset credential.helpergit-credential-manager configure...

Generator 是怎么样使用的以及各个阶段的变化如何

Generators 是 JavaScript 中一种特殊类型的函数&#xff0c;可以在执行过程中暂停&#xff0c;并且在需要时恢复执行。它们是通过 function* 关键字来定义的。Generator 函数返回的是一个迭代器对象&#xff0c;通过调用该迭代器对象的 next() 方法来控制函数的执行。在调用 n…...

一文了解Java中 Vector、ArrayList、LinkedList 之间的区别

目录 1. 数据结构 Vector 和 ArrayList LinkedList 2. 线程安全 Vector ArrayList 和 LinkedList 3. 性能 插入和删除操作 随机访问 4. 内存使用 ArrayList 和 Vector LinkedList 5. 迭代器行为 ArrayList 和 Vector LinkedList 6. 扩展策略 ArrayList Vecto…...

【数字孪生实战案例】三维场景中怎样点击飞线,唤起弹窗并加载匹配的关联数据?~山海鲸可视化

在三维数据可视化场景中&#xff0c;飞线常用于呈现跨区域业务关联与流转关系。为增强交互体验与数据可读性&#xff0c;需实现点击飞线触发弹窗&#xff0c;并精准加载匹配的关联数据&#xff0c;让用户可实时查看单条飞线对应的业务信息&#xff0c;提升三维场景的数据交互与…...

Crustocean/conch:轻量级容器化工具,简化开发者本地环境搭建

1. 项目概述&#xff1a;一个面向开发者的轻量级容器化工具最近在和一些做后端开发的朋友聊天&#xff0c;发现大家普遍有个痛点&#xff1a;本地开发环境和线上环境不一致&#xff0c;导致“在我机器上好好的”这种经典问题频繁上演。虽然Docker已经普及&#xff0c;但完整的D…...

轨道交通条形屏电源技术分析:超薄化与高可靠性的工程平衡

一、行业背景与技术挑战在智慧城轨建设中&#xff0c;地铁站内条形屏是乘客信息显示系统的核心终端设备。该应用场景对配套电源提出以下技术要求&#xff1a;技术需求具体指标工程挑战超薄化整机厚度3-8mm传统变压器/散热器高度难以压缩高可靠性MTBF≥50000小时轨道交通振动、温…...

开源AI本地化框架py-gpt:微内核插件化架构与RAG应用实战

1. 项目概述&#xff1a;一个本地化、可扩展的AI应用框架最近在折腾AI应用本地化部署的朋友&#xff0c;可能都绕不开一个核心矛盾&#xff1a;既想享受大语言模型&#xff08;LLM&#xff09;强大的对话和推理能力&#xff0c;又对数据隐私、网络依赖和持续付费心存顾虑。市面…...

从Excel到数据库:用Pandas Timestamp统一你的时间数据(pd.to_datetime实战解析)

从Excel到数据库&#xff1a;用Pandas Timestamp统一你的时间数据&#xff08;pd.to_datetime实战解析&#xff09; 在数据工程领域&#xff0c;时间数据的标准化处理往往是ETL流程中最容易被低估的痛点。当Excel表格中的"2023/1/15"遇上数据库里的"15-JAN-23&q…...

深入PEX8796:从Serdes到Virtual Switch,图解PCIe交换芯片的三种工作模式

深入解析PEX8796&#xff1a;PCIe交换芯片的架构设计与模式创新 在高速数据传输领域&#xff0c;PCIe交换芯片如同交通枢纽般连接着计算系统的各个组件。作为PLX公司&#xff08;现已被博通收购&#xff09;的经典之作&#xff0c;PEX8796凭借其灵活的架构设计和多样化的操作模…...

令牌管理实战:从JWT原理到token-ninja库的集成与应用

1. 项目概述&#xff1a;一个专为令牌处理而生的“忍者”如果你在开发中经常和令牌&#xff08;Token&#xff09;打交道&#xff0c;比如处理JWT、API密钥、会话标识&#xff0c;或者是在构建需要精细权限控制的微服务、身份认证系统&#xff0c;那你一定遇到过这些麻烦&#…...

Arm架构在中国市场的潜力与挑战:从技术选型到实践落地

1. 项目概述&#xff1a;从一次技术选型引发的深度思考最近在为一个边缘计算项目做硬件选型&#xff0c;团队里关于采用x86还是Arm架构的服务器争论了好几天。这让我想起&#xff0c;这几年在国内的云计算、数据中心、甚至个人消费电子领域&#xff0c;Arm架构的声音是越来越响…...

两种 Linux 发行版:Ubuntu 与 CentOS Shell 环境核心差异对比(查看 Linux 版本,Hadoop 是什么)

Xshell5作为远程连接工具&#xff0c;可通过命令行查看连接的Linux服务器版本。推荐使用cat /etc/os-release或lsb_release -a查看发行版信息&#xff0c;特定系统可用cat /etc/redhat-release(CentOS)或cat /etc/debian_version(Debian)。内核版本用uname -r查看。Ubuntu和Cen…...

现代Web全栈开发实战:基于React、Node.js与Prisma的足球赛事应用架构解析

1. 项目概述与核心价值最近在整理个人技术栈时&#xff0c;翻到了一个之前参与过的很有意思的Web项目——一个基于“NLW”&#xff08;Next Level Week&#xff09;活动构建的足球赛事Web应用。这个项目虽然源于一个线上编程活动&#xff0c;但其架构设计和实现思路&#xff0c…...