高级
Registering assets
介绍
Filament 生态系统中的所有包都共享同一个资源管理系统。它允许官方插件及第三方插件注册用于 Blade 视图的 CSS 和 JavaScript 文件。
FilamentAsset Facade
FilamentAsset Facade 用于将文件注册到资源系统。这些文件可以来自于文件系统的任何地方,之后当运行 php artisan filament:assets 命令时,它们将被复制到应用的 public 目录中。通过将它们复制到 public 目录,我们可以在 Blade 视图中可预测地加载它们,同时确保第三方包也能够加载它们的资源,而不必操心资源所在的位置。
资源始终有一个由你确定的唯一 ID,当资源被复制到 /public 目录时,该 ID 将被用作文件名。此 ID 还用于在 Blade 视图中引用资源。虽然 ID 是唯一的,但如果你正在为插件注册资源,则不必担心 ID 会与其他插件冲突,因为资源将被复制到以你的插件命名的目录中。
FilamentAsset Facade 应该在服务提供者的 boot() 方法中使用。它可用于应用服务提供者(如 AppServiceProvider)之中,也可用于插件的服务提供者内。
FilamentAsset Facade 有一个主方法:register(),它接受要注册的资源数组:
use Filament\Support\Facades\FilamentAsset;
public function boot(): void
{
// ...
FilamentAsset::register([
// ...
]);
// ...
}
为插件注册静态资源
为插件注册资源时,你应该传入 Composer 包名作为 register() 方法的第二个参数:
use Filament\Support\Facades\FilamentAsset;
FilamentAsset::register([
// ...
], package: 'danharrin/filament-blog');
现在,该插件的所有资源都会被复制到 public 下他们自己的目录之内,以避免与其他插件出现同名冲突。
注册 CSS 文件
要在资源系统中注册 CSS 文件,请在服务提供者的 boot 方法中使用 FilamentAsset::register() 方法。然后传递一个 Css,数组中的每个值对应于要注册到系统的 CSS 文件。
每个 Css 文件都有唯一的 ID 以及指向 CSS 文件的路径:
use Filament\Support\Assets\Css;
use Filament\Support\Facades\FilamentAsset;
FilamentAsset::register([
Css::make('custom-stylesheet', __DIR__ . '/../../resources/css/custom.css'),
]);
本例中,我们使用 __DIR__ 来生成一个相对于当前文件的资源相对路径。比如,如果你将此代码添加到 /app/Providers/AppServiceProvider.php,那么该 CSS 文件则位于 /resources/css/custom.css 中。
然后,当运行 php artisan filament:assets 命令时,该 CSS 文件将被复制到 /public 目录。此外,它现在已加载到所有使用 Filament 的 Blade 视图中。如果你只想在页面上的元素需要时加载 CSS,请查看懒加载 CSS部分。
在插件中使用 Tailwind CSS
通常,注册 CSS 文件用在应用的自定义样式表。如果你想使用 Tailwind CSS 处理这些文件,你需要考虑其影响,特别是如果你是一名插件开发者
Tailwind 编译对于每个应用而言都是唯一的 —— 它们包含一套最小化的实用类,只有你在应用中实际使用到的那些类。这意味着,如果你是一名插件开发者,你可能不应该将 Tailwind CSS 文件构建到你的插件中。相反,你应该提供原始 CSS 文件,并指示用户自己构建 Tailwind CSS 文件。为此,他们可能需要使用 @source 指令将你的 vendor 目录添加到他们自定义主题的 CSS 文件中。在自定义主题 CSS 文件(e.g., resources/css/filament/admin/theme.css)中,他们应该添加:
@import "tailwindcss";
@source '../../../../app/Filament/**/*';
@source '../../../../resources/views/filament/**/*';
@source '../../../../vendor/danharrin/filament-blog/resources/views/**/*'; /* Your plugin's vendor directory */
也就是说当他们编译他们自己的 Tailwind CSS 文件时,它将包含所有你插件视图中用到的实用(utility)类,以及他自己应用中和 Filament 核心中用到的实用类。
不过,使用这一技术,对于使用你插件的面板构建器用户来说,可能会有额外的复杂性。如果他们有自己的自定义主题,则没什么影响,因为他们无论如何都在使用 Tailwind CSS 编译自己的 CSS 文件。但是,如果他们使用的是面板生成器附带的默认样式表,你可能需要小心留意在插件视图中使用的实用类。比如,如果你使用了默认样式表中未曾使用过的实用类,而用户又没有自己编译,那么这些类就没有包含在最终的 CSS 文件中。也就是说你的插件视图外观可能不如预期那般展示。这是推荐在插件中编译和注册 Tailwind css 编译过的样式表的少数几种情况之一。
懒加载 CSS
默认情况下,使用资源系统注册的所有 CSS 文件都会在每一个 Filament 页面的 <head> 中加载。这是加载 CSS 文件的最简单方式,不过有时候这种方式可能偏于繁重,而且不是每个页面都需要它。这种情况下,你可以利用 Filament 中捆绑的 Alpine.js 懒加载资源包。它允许你使用 Alpine.js 按需加载 CSS 文件。引导很简单,你可以在元素上使用 x-load-css 指令,当该元素加载到页面上时,指定的 css 文件将加载到页面的 <head> 中。这对于需要 CSS 文件的小 UI 元素和整个页面来说都是完美的:
<div
x-data="{}"
x-load-css="[@js(\Filament\Support\Facades\FilamentAsset::getStyleHref('custom-stylesheet'))]"
>
<!-- ... -->
</div>
要防止 CSS 文件自动加载,你可以使用 loadedOnRequest() 方法:
use Filament\Support\Assets\Css;
use Filament\Support\Facades\FilamentAsset;
FilamentAsset::register([
Css::make('custom-stylesheet', __DIR__ . '/../../resources/css/custom.css')->loadedOnRequest(),
]);
如果你的 CSS 文件是注册到插件的,你应该将其作为第二个参数传入到 FilamentAsset::getStyleHref() 方法:
<div
x-data="{}"
x-load-css="[@js(\Filament\Support\Facades\FilamentAsset::getStyleHref('custom-stylesheet', package: 'danharrin/filament-blog'))]"
>
<!-- ... -->
</div>
使用 URL 注册 CSS 文件
如果你想注册一个基于 URL 的 CSS 文件,也是可行的。这些资源会像平常一样在每页中加载,而不会在运行 php artisan filament:assets 时复制到 /public 目录。这对于注册来自于 CDN 的外部样式表,或者已经直接编译到 /public 目录的样式表尤其有用。
use Filament\Support\Assets\Css;
use Filament\Support\Facades\FilamentAsset;
FilamentAsset::register([
Css::make('example-external-stylesheet', 'https://example.com/external.css'),
Css::make('example-local-stylesheet', asset('css/local.css')),
]);
注册 CSS 变量
有时,你可能希望在 CSS 文件中使用来自于后端的动态数据。为此,你可以在服务提供者的 boot() 中调用 FilamentAsset::registerCssVariables() 方法:
use Filament\Support\Facades\FilamentAsset;
FilamentAsset::registerCssVariables([
'background-image' => asset('images/background.jpg'),
]);
现在,你可以在 CSS 文件中访问这些变量:
background-image: var(--background-image);
注册 JavaScript 文件
想要使用资源系统注册 JavaScript 文件,你可以在服务提供者的 boot() 方法中使用 FilamentAsset::register() 方法。然后传入一个 JS 对象数组,数组中的每个对象对应于你要在资源系统中注册的 JavaScript 文件。
每个 Js 文件都有唯一的 ID 以及指向 JavaScript 文件的路径:
use Filament\Support\Assets\Js;
FilamentAsset::register([
Js::make('custom-script', __DIR__ . '/../../resources/js/custom.js'),
]);
本例中,我们使用 __DIR__ 来生成一个相对于当前文件的资源相对路径。比如,如果你将此代码添加到 /app/Providers/AppServiceProvider.php,那么该 JavaScript 文件则位于 /resources/js/custom.js 中。
然后,当运行 php artisan filament:assets 命令时,该 JavaScript 文件将被复制到 /public 目录。此外,它现在已加载到所有使用 Filament 的 Blade 视图中。如果你只想在页面上的元素需要时加载 JavaScript,请查看懒加载 JavaScript文档。
懒加载 JavaScript
默认情况下,使用资源系统注册的所有 JavaScript 文件都会在每一个 Filament 页面的底部中加载。这是加载 JavaScript 文件的最简单方式,不过有时候这种方式可能偏于繁重,而且不是每个页面都需要它。这种情况下,你可以利用 Filament 中捆绑的 Alpine.js 懒加载资源包。它允许你使用 Alpine.js 按需加载 JavaScript 文件。引导很简单,你可以在元素上使用 x-load-js 指令,当该元素加载到页面上时,指定的 JavaScript 文件将加载到页面底部。这对于需要 JavaScript 文件的小 UI 元素和整个页面来说都是完美的:
<div
x-data="{}"
x-load-js="[@js(\Filament\Support\Facades\FilamentAsset::getScriptSrc('custom-script'))]"
>
<!-- ... -->
</div>
要防止 JavaScript 文件自动加载,你可以使用 loadedOnRequest() 方法:
use Filament\Support\Assets\Js;
use Filament\Support\Facades\FilamentAsset;
FilamentAsset::register([
Js::make('custom-script', __DIR__ . '/../../resources/js/custom.js')->loadedOnRequest(),
]);
如果你的 JavaScript 文件是注册到插件的,你应该将其作为第二个参数传入到 FilamentAsset::getScriptSrc() 方法:
<div
x-data="{}"
x-load-js="[@js(\Filament\Support\Facades\FilamentAsset::getScriptSrc('custom-script', package: 'danharrin/filament-blog'))]"
>
<!-- ... -->
</div>
异步 Alpine.js 组件
有时,你可能希望为基于 Alpine.js 的组件加载外部 JavaScript 库。最好的方法是将编译后的 JavaScript 和 Alpine 组件存储在一个单独的文件中,并让我们在渲染组件时加载它。
首先,你应该通过 npm 安装 esbuild,我们将使用它创建一个包含外部库和 Alpine 组件的 JavaScript 文件:
npm install esbuild --save-dev
然后,你必须创建一个脚本来编译 JavaScript 和 Alpine 组件。你可以把它放在任何地方,例如 bin/build.js :
import * as esbuild from 'esbuild'
const isDev = process.argv.includes('--dev')
async function compile(options) {
const context = await esbuild.context(options)
if (isDev) {
await context.watch()
} else {
await context.rebuild()
await context.dispose()
}
}
const defaultOptions = {
define: {
'process.env.NODE_ENV': isDev ? `'development'` : `'production'`,
},
bundle: true,
mainFields: ['module', 'main'],
platform: 'neutral',
sourcemap: isDev ? 'inline' : false,
sourcesContent: isDev,
treeShaking: true,
target: ['es2020'],
minify: !isDev,
plugins: [{
name: 'watchPlugin',
setup(build) {
build.onStart(() => {
console.log(`Build started at ${new Date(Date.now()).toLocaleTimeString()}: ${build.initialOptions.outfile}`)
})
build.onEnd((result) => {
if (result.errors.length > 0) {
console.log(`Build failed at ${new Date(Date.now()).toLocaleTimeString()}: ${build.initialOptions.outfile}`, result.errors)
} else {
console.log(`Build finished at ${new Date(Date.now()).toLocaleTimeString()}: ${build.initialOptions.outfile}`)
}
})
}
}],
}
compile({
...defaultOptions,
entryPoints: ['./resources/js/components/test-component.js'],
outfile: './resources/js/dist/components/test-component.js',
})
就如上述脚本底部所示,我们将 resources/js/components/test-component.js 文件编译到 resources/js/dist/components/test-component.js 中。你可以根据需要修改文件路径。你也可以根据实际需要编译尽可能多的组件。
现在,创建一个新文件 resources/js/components/test-component.js,如下:
// 从 NPM 导入外部 JavaScript 库
export default function testComponent({
state,
}) {
return {
state,
// 你可以在此处定义其他 Alpine.js 属性
init() {
// 根据需要在此处初始化 Alpine 组件
},
// 你可以在此处定义其他 Alpine.js 函数
}
}
然后,运行如下命令,将该文件编译到 resources/js/dist/components/test-component.js:
node bin/build.js
如果你想监控文件的变化,而非马上进行编译,可以使用如下命令:
node bin/build.js --dev
现在,你需要告诉 Filament 将这个编译好的 JavaScript 文件发布到 Laravel 应用的 /public 目录中,这样浏览器就可以访问它了。为此,你可以在服务提供者的 boot() 方法中使用 FilamentAsset::register() 方法,传入一个 AlpineComponent 对象:
use Filament\Support\Assets\AlpineComponent;
use Filament\Support\Facades\FilamentAsset;
FilamentAsset::register([
AlpineComponent::make('test-component', __DIR__ . '/../../resources/js/dist/components/test-component.js'),
]);
当你运行 php artisan filament::assets 命令时,编译文件会复制到 /public 目录中。
最后,你可以在你的视图中使用 x-load 属性和 FilamentAsset::getAlpineComponentSrc() 方法异步加载这个 Alpine 组件:
<div
x-load
x-load-src="{{ \Filament\Support\Facades\FilamentAsset::getAlpineComponentSrc('test-component') }}"
x-data="testComponent({
state: $wire.{{ $applyStateBindingModifiers("\$entangle('{$statePath}')") }},
})"
>
<input x-model="state" />
</div>
该示例为自定义表单字段,它将 state 作为参数传入到 testComponent() 函数,该函数与 Livewire 组件属性绑定(entangle)。你可以传入任何你想要的参数,并在 testComponent() 函数中访问。如果你未使用自定义表单字段,你可以在本示例中忽视 state 参数。
x-load 属性来自于异步 Alpine包,该包的所有特性都可以在此处使用。
注册脚本数据
有时,你可能希望让后端数据可用于 JavaScript 文件中。为此,你可以在服务提供者的 boot() 中调用 FilamentAsset::registerScriptData() 方法:
use Filament\Support\Facades\FilamentAsset;
FilamentAsset::registerScriptData([
'user' => [
'name' => auth()->user()?->name,
],
]);
现在,你可以使用 window.filamentData 对象,在运行时从 JavaScript 文件中访问这些数据:
window.filamentData.user.name // 'Dan Harrin'
使用 URL 注册 JavaScript 文件
如果你想注册一个基于 URL 的 JavaScript 文件,也是可行的。这些资源会像平常一样在每页中加载,而不会在运行 php artisan filament:assets 时复制到 /public 目录。这对于注册来自于 CDN 的外部脚本,或者已经直接编译到 /public 目录的脚本尤其有用:
use Filament\Support\Facades\FilamentAsset;
use Filament\Support\Assets\Js;
FilamentAsset::register([
Js::make('example-external-script', 'https://example.com/external.js'),
Js::make('example-local-script', asset('js/local.js')),
]);
Using Vite-compiled JavaScript files
The php artisan filament:assets command copies files as-is into the /public directory without bundling or resolving dependencies. This means that if your JavaScript file uses import statements to pull in npm packages, the browser will not be able to resolve them. To use JavaScript files that require bundling, you should compile them with Vite first, and then register the compiled output as a URL-based asset.
First, add your JavaScript file as an entry point in your vite.config.js:
import { defineConfig } from 'vite'
import laravel from 'laravel-vite-plugin'
export default defineConfig({
plugins: [
laravel({
input: [
'resources/css/app.css',
'resources/js/app.js',
'resources/js/timezone.js', // Your custom script
],
}),
],
})
Then, compile the assets using Vite:
npm run build
Finally, register the compiled asset using Vite::asset() to resolve the versioned URL:
use Filament\Support\Assets\Js;
use Filament\Support\Facades\FilamentAsset;
use Illuminate\Support\Facades\Vite;
FilamentAsset::register([
Js::make('timezone', Vite::asset('resources/js/timezone.js')),
]);
This approach also works for TypeScript files or any other JavaScript that needs a build step. Since Vite::asset() returns a URL, the asset will not be copied by php artisan filament:assets — it is served directly from Vite’s build output.
NOTE
If you need to bundle JavaScript for an asynchronous Alpine.js component, consider using esbuild instead, as documented in that section.
Ignoring published assets in version control
The files that the php artisan filament:assets command copies into the /public directory for Filament’s own packages are generated, so there is no need to commit them to version control. When you run php artisan filament:install, Filament adds the following rules to your app’s .gitignore file, if they are not already there:
/public/css/filament
/public/fonts/filament
/public/js/filament
If you have customized the assets_path in the config/filament.php file, the rules use that path instead, for example /public/filament/css/filament.
Assets registered by your app or third-party plugins may be published outside these filament directories. You should add their generated paths to .gitignore separately if needed.
The /public/css/filament rule also ignores any custom themes that you compile directly into that directory instead of through Vite. You should compile those themes as part of your deployment process, since the filament:assets command does not build them.
Since Filament’s published package assets are not committed, you should run php artisan filament:assets when you deploy your app. The php artisan filament:install command adds @php artisan filament:upgrade to the post-autoload-dump scripts in your app’s composer.json file, which runs the filament:assets command for you each time Composer dumps the autoloader.
Still need help? Join our Discord community or open a GitHub discussion