PHP用户代理解析:jenssegers/agent库的集成与应用实践
1. 项目概述:一个为PHP应用注入“智能”的轻量级探针
在构建现代PHP应用时,我们常常面临一个挑战:如何让应用“认识”自己运行的环境?是来自桌面浏览器的访问,还是移动App的API调用?用户使用的是iPhone还是Android手机,是Windows系统还是macOS?更进一步,用户的浏览器版本、设备型号、甚至是否是机器人爬虫,这些信息对于提供差异化的用户体验、进行精准的数据统计或实施安全策略都至关重要。
这就是 jenssegers/agent 这个库要解决的核心问题。它不是一个功能庞杂的框架,而是一个极其专注且高效的“用户代理解析器”。简单来说,它的工作就是解析HTTP请求头中的 User-Agent 字符串——这个由客户端(浏览器、App、爬虫程序)发送的、包含其身份信息的“身份证”。将这个杂乱无章的长字符串,转化为结构清晰、可直接编程使用的数据对象。
我最初接触它是在一个需要根据设备类型提供不同样式表的项目中。手动解析 User-Agent 不仅繁琐,而且规则日新月异,新设备、新浏览器版本层出不穷,维护一个准确的解析器几乎是不可能的任务。 jenssegers/agent 基于著名的 mobiledetect/mobiledetectlib 构建,并进行了面向Laravel等现代PHP框架的友好封装,提供了更简洁、更符合直觉的API。它让判断设备类型、操作系统、浏览器变得像调用 $agent->isMobile() 或 $agent->browser() 一样简单直接。对于任何需要客户端环境感知的PHP开发者,无论是做响应式网页的逻辑判断、API接口的版本适配,还是反爬虫和访问分析,这个库都是一个不可或缺的“瑞士军刀”。
2. 核心功能与设计哲学解析
2.1 功能全景:从字符串到结构化数据的蜕变
jenssegers/agent 的核心价值在于将原始的、对人类不友好的 User-Agent 字符串,转化为开发者可以轻松理解和使用的布尔值、字符串和数组。它的主要功能可以概括为以下几个维度:
- 设备类型检测 :这是最常用的功能。它能准确区分访问来源是桌面电脑、平板电脑、移动电话,还是机器人(爬虫)。方法如
isMobile(),isTablet(),isDesktop(),isRobot()直观明了。 - 操作系统识别 :可以识别出客户端运行的操作系统,例如
Windows,macOS,iOS,Android,Linux等,并通过version方法获取其主版本号。 - 浏览器识别 :识别出用户使用的浏览器品牌,如
Chrome,Firefox,Safari,Edge,Internet Explorer等,同样支持版本号获取。 - 设备品牌与型号探测 :对于移动设备,它能进一步识别出制造商(如 Apple, Samsung, Huawei, Xiaomi)和具体的设备型号(如 iPhone 13 Pro, SM-G998B)。
- 语言与平台判断 :可以获取客户端接受的语言偏好,以及判断是否来自某些特定平台(如
isAndroidOS(),isiOS())。
它的设计哲学是“约定优于配置”和“专注单一职责”。它不处理HTTP请求的接收,不管理会话,也不渲染视图。它只做一件事:解析 User-Agent 。这种单一性使得它极其轻量(核心依赖只有一个)、性能出色,并且可以无缝集成到任何PHP项目中,无论是传统的过程式脚本,还是现代的Laravel、Symfony等框架。
2.2 底层依赖与工作原理解析
jenssegers/agent 并非从头造轮子,其检测能力的基石是 mobiledetect/mobiledetectlib 。这个库维护了一个庞大的、持续更新的正则表达式规则库,用于匹配 User-Agent 字符串中的特定模式。
其工作原理可以简化为以下几步:
- 注入信息 :开发者将
$_SERVER[‘HTTP_USER_AGENT’]或其他来源的UA字符串传递给Agent实例。 - 规则匹配 :Agent实例内部调用Mobile Detect库,用成千上万条正则表达式规则对UA字符串进行扫描。
- 属性填充 :匹配成功后,将结果(如设备类型、浏览器名称)填充到Agent对象的内部属性中。
- API暴露 :通过友好的Getter方法(如
isMobile(),browser())将结果返回给开发者。
jenssegers/agent 在 mobiledetect 之上做的封装,主要体现在:
- 更优雅的API :提供了流畅的、面向对象的方法调用方式。
- Laravel集成 :提供了开箱即用的Service Provider、Facade和Blade指令,在Laravel中使用体验极佳。
- 额外功能 :如设备型号解析、语言获取等补充功能。
注意 :由于依赖正则规则库,其准确性完全取决于
mobiledetect规则库的更新及时性。对于市面上最新出现的设备或小众浏览器,可能存在短暂的识别延迟或误差。这是所有基于规则匹配的UA解析器的共同特点。
3. 集成与基础使用指南
3.1 环境准备与安装
jenssegers/agent 要求PHP 7.2或更高版本。安装方式极其简单,通过Composer即可完成。
composer require jenssegers/agent
对于非Laravel的普通PHP项目,安装后即可直接使用。
3.2 在普通PHP项目中的使用
在传统的PHP脚本或框架(如Slim, Lumen)中,你需要手动实例化 Jenssegers\Agent\Agent 类,并通常将当前请求的User-Agent字符串传递给它。
<?php
require ‘vendor/autoload.php’;
use Jenssegers\Agent\Agent;
// 创建Agent实例
$agent = new Agent();
// 你可以显式设置一个UA字符串进行测试
// $agent->setUserAgent(‘Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) ...’);
// 但更常见的做法是,让它自动从当前PHP全局变量中检测
// 这通常依赖于 $_SERVER[‘HTTP_USER_AGENT’]
// 进行各种检测
if ($agent->isMobile()) {
echo “您正在使用手机访问。”;
}
$browser = $agent->browser(); // 例如: “Chrome”
$version = $agent->version($browser); // 浏览器版本,例如: “96.0.4664.110”
$platform = $agent->platform(); // 例如: “macOS”
$platformVersion = $agent->version($platform); // 操作系统版本
$device = $agent->device(); // 例如: “iPhone”
// 对于移动设备,可以尝试获取更详细的型号(此功能有限)
// $model = $agent->device(‘model’);
if ($agent->isRobot()) {
$robotName = $agent->robot(); // 例如: “Googlebot”
echo “检测到机器人:” . $robotName;
}
?>
3.3 在Laravel框架中的深度集成
在Laravel中, jenssegers/agent 的体验达到了最佳。因为它提供了完整的服务容器绑定、门面(Facade)和Blade指令。
首先,确保已经通过Composer安装。对于Laravel 5.5及以上版本,包会自动发现并注册服务提供者和门面。对于更早的版本,你需要在 config/app.php 中手动添加:
在 providers 数组中添加:
Jenssegers\Agent\AgentServiceProvider::class,
在 aliases 数组中添加:
‘Agent’ => Jenssegers\Agent\Facades\Agent::class,
完成配置后,你可以在代码的任何地方通过依赖注入、Facade或辅助函数来使用它。
1. 使用依赖注入(推荐): 在控制器、中间件或服务的构造函数或方法中,类型提示 Jenssegers\Agent\Agent 接口,Laravel会自动解析实例。
<?php
namespace App\Http\Controllers;
use Jenssegers\Agent\Agent;
use App\Http\Controllers\Controller;
class HomeController extends Controller
{
protected $agent;
public function __construct(Agent $agent)
{
$this->agent = $agent;
}
public function index()
{
if ($this->agent->isMobile()) {
// 返回移动端优化的视图或数据
return view(‘home.mobile’);
}
return view(‘home.desktop’);
}
}
2. 使用门面(Facade): 代码更简洁,适合在非构造函数的地方快速使用。
use Agent;
// 在路由、控制器方法或任何地方
Route::get(‘/’, function () {
if (Agent::isMobile()) {
return ‘Mobile view’;
}
return ‘Desktop view’;
});
3. 在Blade模板中使用指令: jenssegers/agent 为Blade提供了便捷的指令,让视图层的条件判断变得非常清晰。
<!DOCTYPE html>
<html>
<head><title>设备适配页面</title></head>
<body>
@desktop
<h1>欢迎来到桌面版网站!</h1>
<!-- 加载桌面端特定的CSS/JS -->
<link rel=“stylesheet” href=“/css/desktop.css”>
@elsemobile
<h1>欢迎来到移动版网站!</h1>
<!-- 加载移动端特定的CSS/JS -->
<link rel=“stylesheet” href=“/css/mobile.css”>
<meta name=“viewport” content=“width=device-width, initial-scale=1.0”>
@enddesktop
<p>您的浏览器是:{{ Agent::browser() }}, 版本:{{ Agent::version(Agent::browser()) }}</p>
<p>您的系统是:{{ Agent::platform() }}</p>
@if (Agent::is(‘Chrome’))
<p>您正在使用谷歌Chrome浏览器。</p>
@endif
</body>
</html>
Blade指令 @desktop , @mobile , @tablet , @phone , @robot 极大地简化了基于设备类型的视图逻辑分发。
4. 高级应用场景与实战技巧
4.1 场景一:API接口的版本控制与内容协商
在构建RESTful API时,我们可能需要为不同的客户端(如iOS App v1.0, Android App v2.0, 网页端)返回不同结构的数据。除了使用标准的 Accept 头, User-Agent 也是一个重要的判断依据。
// 在API控制器或中间件中
use Jenssegers\Agent\Agent;
public function getUserData(Request $request, Agent $agent)
{
$platform = $agent->platform(); // e.g., ‘iOS’, ‘Android’
$browser = $agent->browser(); // 对于App,可能是自定义字符串,如 ‘MyApp/1.0’
$userData = User::find(1)->toArray();
// 根据平台和客户端类型,调整响应数据
if (str_contains($browser, ‘MyApp/’)) {
// 移动App客户端,返回精简字段
$responseData = [
‘id’ => $userData[‘id’],
‘name’ => $userData[‘name’],
‘avatar’ => $userData[‘avatar_url’],
// App不需要的字段如 ‘created_at’, ‘updated_at’ 在此省略
];
} elseif ($agent->isDesktop()) {
// 网页桌面端,返回完整字段
$responseData = $userData;
$responseData[‘is_desktop’] = true; // 甚至可以附加元信息
} else {
// 移动网页端或其他
$responseData = $userData;
unset($responseData[‘some_sensitive_field’]);
}
return response()->json($responseData);
}
4.2 场景二:反爬虫与安全策略
结合 isRobot() 方法,我们可以对已知的友好爬虫(如Googlebot, Bingbot)和恶意爬虫或扫描器采取不同策略。
// 在一个全局中间件中
use Closure;
use Jenssegers\Agent\Agent;
class CheckRobotMiddleware
{
public function handle($request, Closure $next)
{
$agent = new Agent();
$userAgent = $request->header(‘User-Agent’);
// 如果没有User-Agent,视为可疑请求
if (empty($userAgent)) {
// 可以记录日志、返回错误或跳转到验证页面
Log::warning(‘Request without User-Agent’, [‘ip’ => $request->ip()]);
// return response(‘Access Denied’, 403);
// 或者,为了不影响正常用户,可以仅限速而不拒绝
}
if ($agent->isRobot()) {
$robotName = $agent->robot();
// 区分友好爬虫和可疑爬虫
$friendlyBots = [‘Googlebot’, ‘Bingbot’, ‘Slurp’, ‘DuckDuckBot’, ‘Baiduspider’];
if (in_array($robotName, $friendlyBots)) {
// 对搜索引擎爬虫,可以设置特定的限速规则,或提供缓存内容
// 例如,使用 Laravel RateLimiter
// 同时,确保robots.txt和站点地图对其开放
} else {
// 对于其他未知机器人,实施严格的访问限制或验证码挑战
Log::info(‘Non-friendly robot detected’, [‘name’ => $robotName, ‘ip’ => $request->ip()]);
// 可以应用更严格的速率限制
}
}
// 检查某些已知的恶意UA模式(作为规则库的补充)
$badPatterns = [‘sqlmap’, ‘nikto’, ‘acunetix’];
foreach ($badPatterns as $pattern) {
if (stripos($userAgent, $pattern) !== false) {
Log::critical(‘Security scanner detected’, [‘ua’ => $userAgent, ‘ip’ => $request->ip()]);
// 立即阻断请求
return response(‘Forbidden’, 403);
}
}
return $next($request);
}
}
4.3 场景三:数据统计与用户行为分析
在后端记录访问日志时,将解析后的设备信息一并存入数据库,可以极大地方便后续的数据分析,而无需在查询时再实时解析原始的UA字符串。
// 在记录访问日志的Observer、Middleware或专用Logger中
use Jenssegers\Agent\Agent;
public function logAccess(Request $request)
{
$agent = new Agent();
$agent->setUserAgent($request->userAgent());
$accessLog = new AccessLog();
$accessLog->ip_address = $request->ip();
$accessLog->user_agent_raw = $request->userAgent(); // 保存原始字符串以备不时之需
$accessLog->device_type = $agent->isMobile() ? ($agent->isTablet() ? ‘tablet’ : ‘mobile’) : ‘desktop’;
$accessLog->platform = $agent->platform();
$accessLog->platform_version = $agent->version($agent->platform());
$accessLog->browser = $agent->browser();
$accessLog->browser_version = $agent->version($agent->browser());
$accessLog->is_robot = $agent->isRobot();
$accessLog->robot_name = $agent->isRobot() ? $agent->robot() : null;
// 尝试获取设备型号(注意:此信息可能为空或不准确)
$accessLog->device_model = $agent->device();
$accessLog->url = $request->fullUrl();
$accessLog->save();
// 现在,你可以轻松地写出如下查询:
// “统计过去一个月使用iOS系统Chrome浏览器的移动端用户访问量”
// AccessLog::where(‘platform’, ‘iOS’)->where(‘browser’, ‘Chrome’)->where(‘device_type’, ‘mobile’)->whereBetween(‘created_at’, […])->count();
}
4.4 实战技巧与避坑指南
-
UA字符串的可伪造性 :必须清醒认识到,
User-Agent是客户端自发上报的,完全可以被修改或伪造。一个普通的浏览器可以伪装成Googlebot,一个爬虫也可以把自己伪装成iPhone的Safari。因此, 绝不能将UA解析结果用于核心安全认证或关键业务逻辑 。它最适合用于用户体验优化、数据统计和低安全层级的风控。 -
缓存解析结果 :
User-Agent解析,特别是涉及大量正则匹配,是有计算成本的。在一个请求生命周期内,如果多次调用Agent方法,应该复用同一个实例。对于高并发应用,可以考虑将解析结果(如设备类型)缓存一段时间(例如1小时),键名可以使用UA字符串的哈希值。Laravel中可以很方便地使用Cache门面。use Illuminate\Support\Facades\Cache; use Jenssegers\Agent\Agent; $uaString = $request->userAgent(); $cacheKey = ‘parsed_ua:’ . md5($uaString); $parsedInfo = Cache::remember($cacheKey, 3600, function () use ($uaString) { $agent = new Agent(); $agent->setUserAgent($uaString); return [ ‘is_mobile’ => $agent->isMobile(), ‘browser’ => $agent->browser(), ‘platform’ => $agent->platform(), ]; }); -
处理缺失或异常的UA :有些请求可能没有
User-Agent头(如一些古老的爬虫、curl命令),或者UA字符串极其怪异。你的代码应该对此有健壮性处理,避免因为解析失败而抛出异常。jenssegers/agent本身对此有容错,但你的业务逻辑(比如根据$agent->browser()的结果做字符串操作)需要考虑到可能返回null或空字符串的情况。 -
设备型号检测的局限性 :
$agent->device()方法返回的设备型号信息(如iPhone12,1)可能比较原始,且对于Android设备,其识别能力和准确性远不如iOS设备。不要指望它能返回一个美观的、市场化的型号名称(如 “Galaxy S21 Ultra”)。如果需要更精确的商业化设备信息,可能需要结合其他商业数据库或服务。 -
在队列任务中使用 :如果你在队列任务(Job)中也需要使用Agent,注意队列任务通常没有HTTP请求上下文。你需要将必要的UA字符串作为任务属性(property)传入,然后在任务内部实例化并设置Agent。
// 分发任务时 ProcessVisitLog::dispatch($request->userAgent(), $url); // 在任务类中 class ProcessVisitLog implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; protected $userAgent; protected $url; public function __construct($userAgent, $url) { $this->userAgent = $userAgent; $this->url = $url; } public function handle() { $agent = new Agent(); $agent->setUserAgent($this->userAgent); // … 后续处理逻辑 } }
5. 性能考量、测试与扩展
5.1 性能分析与优化建议
在绝大多数Web应用中,单个请求解析一次UA的开销是微不足道的(通常在几毫秒内)。性能瓶颈主要出现在两个方面:
- 极高并发 :每秒数千甚至数万次请求,每次请求都进行完整的正则匹配,累积的CPU开销会变得显著。
- 复杂页面 :一个页面视图(Blade模板)中如果遍布几十个
@mobile、Agent::is()等指令和调用,会导致多次重复解析。
优化策略:
- 单例/请求内复用 :确保在一个请求生命周期内只解析一次。在Laravel中,通过依赖注入或Facade获取的实例,默认就是被服务容器绑定为单例的。
- 视图Composer/共享数据 :如果整个网站布局都基于设备类型变化,可以在一个全局视图Composer或中间件中解析一次,然后将结果(如
‘is_mobile’ => true)共享给所有视图,避免在多个Blade指令中重复调用。 - 如前所述的缓存 :对于匿名用户或可容忍短暂数据延迟的场景,将解析结果缓存是效果最显著的优化。
5.2 如何编写单元测试
测试依赖于Agent的功能至关重要,因为UA字符串千变万化。我们可以通过 setUserAgent 方法模拟各种客户端。
// tests/Unit/AgentDetectionTest.php
use Jenssegers\Agent\Agent;
use PHPUnit\Framework\TestCase;
class AgentDetectionTest extends TestCase
{
public function test_mobile_detection()
{
$agent = new Agent();
// 测试 iPhone
$agent->setUserAgent(‘Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1’);
$this->assertTrue($agent->isMobile());
$this->assertFalse($agent->isTablet());
$this->assertFalse($agent->isDesktop());
$this->assertEquals(‘iPhone’, $agent->device());
$this->assertEquals(‘iOS’, $agent->platform());
$this->assertEquals(‘Safari’, $agent->browser());
// 测试 Android Chrome
$agent->setUserAgent(‘Mozilla/5.0 (Linux; Android 11; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.104 Mobile Safari/537.36’);
$this->assertTrue($agent->isMobile());
$this->assertEquals(‘Android’, $agent->platform());
$this->assertEquals(‘Chrome’, $agent->browser());
// 测试 iPad (平板)
$agent->setUserAgent(‘Mozilla/5.0 (iPad; CPU OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1’);
$this->assertTrue($agent->isTablet());
$this->assertTrue($agent->isMobile()); // 平板也被视为移动设备
$this->assertEquals(‘iPad’, $agent->device());
// 测试桌面版 Chrome
$agent->setUserAgent(‘Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36’);
$this->assertTrue($agent->isDesktop());
$this->assertFalse($agent->isMobile());
$this->assertEquals(‘Windows’, $agent->platform());
// 测试机器人
$agent->setUserAgent(‘Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)’);
$this->assertTrue($agent->isRobot());
$this->assertEquals(‘Googlebot’, $agent->robot());
}
}
5.3 扩展与自定义匹配规则
虽然 jenssegers/agent 本身没有直接提供扩展接口来添加自定义规则,但其底层的 mobiledetect 库是允许的。不过,直接修改vendor包不是好主意。更常见的需求是,在Agent解析的基础上,添加自己业务特有的逻辑判断。
例如,你的应用有一个自定义的客户端App,其UA格式为 MyAwesomeApp/2.0.1 (com.company.app; build:1024) 。你可以创建一个自定义的“探测器”类来包装Agent。
<?php
namespace App\Services;
use Jenssegers\Agent\Agent;
class CustomAgent extends Agent
{
// 判断是否来自我们自己的App
public function isMyApp(): bool
{
$ua = $this->getUserAgent();
return str_contains($ua, ‘MyAwesomeApp/’);
}
// 获取我们App的版本号
public function myAppVersion(): ?string
{
$ua = $this->getUserAgent();
if (preg_match(‘/MyAwesomeApp\/([\d\.]+)/’, $ua, $matches)) {
return $matches[1];
}
return null;
}
// 覆盖或扩展原有的 browser 方法,将我们自己的App识别为一种特殊的“浏览器”
public function browser(): string
{
if ($this->isMyApp()) {
return ‘MyAwesomeApp’;
}
// 否则调用父类的检测逻辑
return parent::browser();
}
}
然后,在Laravel的服务容器中,将自定义类绑定到原来的接口上。
// 在 AppServiceProvider 的 register 方法中
$this->app->singleton(Agent::class, function ($app) {
return new \App\Services\CustomAgent();
});
// 如果你使用了Facade,也需要重新绑定
$this->app->alias(Agent::class, \Jenssegers\Agent\Facades\Agent::class);
这样,在你的代码中,通过依赖注入或 Agent Facade 获取到的实例,就是你自定义的 CustomAgent ,可以使用 Agent::isMyApp() 这样的方法了。
6. 常见问题排查与社区资源
6.1 常见问题速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
isMobile() 对某些新手机返回 false | mobiledetect 规则库未及时更新。 | 1. 确保 jenssegers/agent 和 mobiledetect/mobiledetectlib 更新到最新版本。 2. 如果问题持续,可考虑在 mobiledetect 的GitHub仓库提交Issue,附上未能识别的UA字符串。 |
device() 返回空或奇怪的型号代码 | 设备型号检测本身就不完全可靠,尤其对于Android。 | 降低对型号信息的依赖,或将其仅用于辅助性日志记录。核心逻辑应基于 isMobile() , platform() 等更稳定的属性。 |
| 在Lumen或非Laravel框架中Facade不可用 | Lumen默认关闭了Facade支持,或未正确注册服务提供者。 | 1. 在 bootstrap/app.php 中取消注释 $app->withFacades(); 。 2. 手动注册服务提供者: $app->register(Jenssegers\Agent\AgentServiceProvider::class); 。 3. 或者直接使用 new Jenssegers\Agent\Agent() 。 |
| 在队列任务或命令行中调用Agent方法报错 | 这些环境没有 HTTP_USER_AGENT 服务器变量。 | 必须在使用前通过 setUserAgent() 方法显式设置UA字符串,不能依赖自动检测。 |
Blade指令 @desktop 等不生效 | 可能未正确加载Blade指令,或指令作用域有误。 | 1. 确保 Jenssegers\Agent\AgentServiceProvider 已注册,该提供者负责注册Blade指令。 2. 检查指令语法是否正确闭合( @enddesktop )。 3. 尝试使用 @if(Agent::isDesktop()) 替代指令,以排查是否为指令注册问题。 |
| 识别结果明显错误(如将Chrome识别为Safari) | UA字符串本身可能具有欺骗性,或者规则匹配冲突。 | 1. 首先验证原始的 $_SERVER[‘HTTP_USER_AGENT’] 字符串。 2. 查阅 mobiledetect 的已知Issue。 3. 对于边缘情况,可能需要结合多种检测方法,或接受一定误差。 |
6.2 获取帮助与资源
- 官方仓库 :所有开发、Issue和Pull Request都应指向GitHub上的 jenssegers/agent 。使用前阅读README是第一步。
- 底层库 :了解其核心依赖 mobiledetect/mobiledetectlib 的文档和规则更新情况。
- 调试技巧 :当检测结果不符合预期时,第一件事是记录或打印出原始的
User-Agent字符串。你可以使用在线的UA解析工具(如 whatismybrowser.com )进行交叉验证,看是库的问题还是UA字符串本身就很特殊。 - 版本更新 :定期通过
composer update更新这个包,以获取最新的设备识别规则。识别能力的提升主要来自于底层mobiledetect库的更新。
jenssegers/agent 以其简单、专注和高效,成为了PHP生态中处理用户代理识别的首选工具。它完美地诠释了“做好一件事”的Unix哲学。在实际项目中,将它作为你工具箱中的标准配置,可以优雅地解决设备感知这一常见需求,让代码更清晰,让用户体验更精细。
更多推荐


所有评论(0)