zxf / res
共享静态资源(字体、css、js、图片)包,支持绝对路径、相对路径、浏览器URL以及在Laravel 11+、ThinkPHP 8+和原生PHP 8.2+中的外部资源注册。
v1.0.0
2026-10-29 01:39 UTC
Requires
- php: >=8.2
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-29 01:48:11 UTC
README
随 Composer 包分发的静态资源库,支持在绝对路径、相对路径、浏览器 URL 三种维度下加载字体、CSS、JS、图片,并支持注册外部资源包目录。
- 资源始终位于
vendor/zxf/res/assets/,不发布、不复制、不软链; - 全程只读,兼容
--no-dev、只读挂载、Windows 无管理员权限; - 支持分组内任意深度子目录:
xf_res_url('css', 'demo/test/base.css'); - 支持开发者注册外部资源目录为虚拟分组;
- 兼容 Laravel 11+ / ThinkPHP 8+ / 原生 PHP 8.2+;
- 路由前缀统一为
/zxf/res;助手函数统一使用xf_res_前缀。
环境要求
- PHP >= 8.2
- Laravel >= 11 或 ThinkPHP >= 8 或原生 PHP 8.2+
安装
composer require zxf/res
安装后资源位于 vendor/zxf/res/assets/,无需任何发布或复制命令。
目录约定
vendor/zxf/res/assets/
├── fonts/regular.woff2
├── css/
│ ├── base.css
│ └── demo/test/base.css
├── js/
│ ├── core.js
│ └── lib/lodash.min.js
└── images/logo.png
assets/下一级子目录名即分组名;- 分组内支持任意深度子目录;
- 扩展名决定类型:
woff/woff2/ttf/otf/eot/sfnt(字体)、css、js/mjs/cjs、png/jpg/jpeg/gif/webp/avif/svg/ico/bmp/tif/tiff(图片)。
三种加载方式
| 方式 | 助手函数 | 返回值示例 | 适用场景 |
|---|---|---|---|
| 绝对路径 | xf_res_path('fonts', 'regular.woff2') |
/path/to/vendor/zxf/res/assets/fonts/regular.woff2 |
PDF 引擎、图片处理 |
| 相对路径 | xf_res_relative('css', 'demo/test/base.css') |
css/demo/test/base.css |
日志、调试 |
| 浏览器 URL | xf_res_url('css', 'demo/test/base.css') |
https://host/zxf/res/css/demo/test/base.css |
<link>、<script>、<img> |
外部资源注册
支持把其他 Composer 包或任意目录中的资源注册为本包的虚拟分组, 之后可像访问内置资源一样访问它们(路径、URL、HTTP 代理完全一致)。
注册单个外部分组
xf_res_register_group('myicons', '/path/to/another/package/assets/icons'); // 之后即可: xf_res_path('myicons', 'star.svg'); // 绝对路径 xf_res_relative('myicons', 'star.svg'); // "myicons/star.svg" xf_res_url('myicons', 'star.svg'); // https://host/zxf/res/myicons/star.svg
批量注册
xf_res_register_groups([ 'myicons' => '/path/to/icons', 'myfonts' => '/path/to/fonts', ]);
注册整个外部 assets 根目录
自动把一级子目录注册为分组:
xf_res_register_root('/path/to/another/package/assets'); // 该目录下的 icons、fonts、js 等一级目录会自动注册为分组
若担心与内置分组冲突,可加前缀:
xf_res_register_root('/path/to/another/package/assets', 'ext'); // 自动注册为 ext_icons、ext_fonts、ext_js ...
注销与清空
xf_res_unregister_group('myicons'); // 注销单个 xf_res_clear_groups(); // 清空全部外部注册(内置不受影响) xf_res_has_group('myicons'); // 判断分组是否存在 xf_res_groups(); // 获取全部分组名(含外部)
注册约束
- 分组名只允许字母、数字、下划线、连字符,且以字母或下划线开头;
- 外部目录必须是真实存在的可读目录;
- 不允许覆盖内置分组(
fonts、css、js、images等),冲突时抛InvalidArgumentException; - 外部资源同样经过
realpath+ 前缀校验,无路径逃逸风险。
Laravel 11+ 接入
1. 自动注册路由
composer.json 已声明 extra.laravel.providers,安装即生效,无需手动注册服务提供者。
使用 route:cache 时执行一次:
php artisan route:cache
2. Blade 中使用
{{-- 内置资源 --}} <link rel="stylesheet" href="{{ xf_res_url('css', 'base.css') }}"> <script src="{{ xf_res_url('js', 'lib/lodash.min.js') }}"></script> <img src="{{ xf_res_url('images', 'logo.png') }}" alt="logo"> {{-- 外部注册资源 --}} <link rel="stylesheet" href="{{ xf_res_url('myicons', 'icon.css') }}"> <style> @font-face { font-family: 'Regular'; src: url('{{ xf_res_url('fonts', 'regular.woff2') }}') format('woff2'); } </style>
3. 在 Laravel 中注册外部资源
在 app/Providers/AppServiceProvider.php 的 boot() 中注册一次:
use zxf\res\AssetLocator; public function boot(): void { // 方式一:注册单个外部目录 AssetLocator::registerGroup('myicons', base_path('node_modules/my-icons/dist')); // 方式二:批量 AssetLocator::registerGroups([ 'mycss' => '/path/to/another/package/assets/css', 'myjs' => '/path/to/another/package/assets/js', ]); // 方式三:注册整个外部 assets 根目录(自动识别一级子目录) AssetLocator::registerRoot('/path/to/another/package/assets'); }
4. 在 Laravel 中使用绝对路径
$fontPath = xf_res_path('fonts', 'regular.woff2'); // 交给 PDF 引擎(Dompdf、TCPDF 等) $pdf->registerFont($fontPath);
ThinkPHP 8+ 接入
1. 注册路由
在 route/app.php 中注册:
use think\facade\Route; Route::get('zxf/res/<group>/<path>', function (string $group, string $path) { xf_res_serve($group, $path); exit; // 阻止 TP8 继续输出 })->pattern(['path' => '.+'])->name('zxf-res.asset');
2. 注册外部资源
在 app/event.php 的 AppInit 事件监听中,或 app/common.php 中:
use zxf\res\AssetLocator; AssetLocator::registerGroup('myicons', root_path('node_modules/my-icons/dist'));
3. 模板中使用
<link rel="stylesheet" href="{:xf_res_url('css', 'demo/test/base.css')}">
<script src="{:xf_res_url('js', 'lib/lodash.min.js')}"></script>
<img src="{:xf_res_url('images', 'logo.png')}">
原生 PHP 8.2+ 接入
1. 入口文件
<?php require __DIR__ . '/vendor/autoload.php'; // 可选:注册外部资源 xf_res_register_group('myicons', __DIR__ . '/node_modules/my-icons/dist'); // 拦截资源请求 $prefix = xf_res_route_prefix(); // "/zxf/res" $uri = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/'; if (str_starts_with($uri, $prefix . '/')) { $rest = rawurldecode(substr($uri, strlen($prefix) + 1)); [$group, $path] = array_pad(explode('/', $rest, 2), 2, ''); xf_res_serve($group, $path); exit; } // 其他路由 echo 'Hello';
2. HTML 中使用
<link rel="stylesheet" href="<?= xf_res_url('css', 'demo/test/base.css') ?>"> <script src="<?= xf_res_url('js', 'lib/lodash.min.js') ?>"></script> <img src="<?= xf_res_url('images', 'logo.png') ?>">
3. Nginx 可选优化(跳过 PHP)
location ^~ /zxf/res/ { rewrite ^/zxf/res/(.*)$ /$1 break; alias /path/to/project/vendor/zxf/res/assets/; }
此配置让 Web 服务器直接响应,性能最优;但注意外部注册的分组不在此规则内(它们在别的目录下),仍走 PHP 代理。
API 一览
助手函数(无命名空间,xf_res_ 前缀)
| 函数 | 返回值 | 说明 |
|---|---|---|
xf_res_path($g, $r) |
string | 绝对路径 |
xf_res_relative($g, $r) |
string | 相对分组目录的规范化路径 |
xf_res_exists($g, $r) |
bool | 存在性判断,不抛异常 |
xf_res_read($g, $r) |
string | 读取全文(小文件) |
xf_res_route_prefix() |
string | 固定返回 /zxf/res |
xf_res_browser_path($g, $r) |
string | 浏览器路径(无域名) |
xf_res_url($g, $r, $base) |
string | 浏览器完整 URL |
xf_res_prepare($g, $r) |
array | 资源元数据 |
xf_res_etag_matches($h, $etag) |
bool | ETag 匹配判断 |
xf_res_serve($g, $r) |
void | 直接发送 HTTP 响应 |
xf_res_register_group($g, $p) |
void | 注册单个外部组 |
xf_res_register_groups($map) |
void | 批量注册外部组 |
xf_res_register_root($p, $prefix) |
void | 注册外部 assets 根目录 |
xf_res_unregister_group($g) |
void | 注销外部组 |
xf_res_clear_groups() |
void | 清空外部注册 |
xf_res_has_group($g) |
bool | 分组是否存在 |
xf_res_groups() |
array | 全部分组名 |
xf_res_all($type) |
array | 按类型收集资源 |
xf_res_fonts() / xf_res_css() / xf_res_js() / xf_res_images() |
array | 分类收集 |
类 API(命名空间 zxf\res)
AssetLocator::path()/relative()/exists()/read()/stream()/iterate()/iterateAll()/groups()/hasGroup()/root()/registerGroup()/registerGroups()/registerRoot()/unregisterGroup()/clearRegisteredGroups()AssetServer::prepare()/isNotModified()/notModifiedHeaders()/send()AssetType::fromExtension()/fromPath()/mimeFor()
安全说明
- 路径逃逸:所有入口经
realpath+ 规范化前缀校验,../与符号链接逃逸被拒绝; - 外部注册冲突:不允许覆盖内置分组,冲突时抛
InvalidArgumentException; - 代理滥用:仅
AssetType声明的扩展名可被代理输出,.php、.env等敏感文件无法通过代理读取; - MIME 混淆:响应带
X-Content-Type-Options: nosniff; - 大文件 OOM:所有输出使用
readfile流式; - Host 头:
xf_res_url()使用$_SERVER['HTTP_HOST'],最终由应用层校验 Host。
性能说明
xf_res_url()首次调用一次realpath,之后走进程内缓存,零 IO;AssetServer::prepare()一次stat()同时拿到 size 与 mtime;- ETag 由
mtime+size组成,不读文件内容; - 强缓存
max-age=31536000, immutable+ ETag 协商,浏览器二次访问不达服务器; - 大文件流式输出,内存占用 O(1);
- 注册外部分组后自动失效相关缓存,不影响已缓存的内置分组。
License
MIT