编写 Flarum 扩展:构建自定义字段
Flarum 是一个非常快速、可扩展、免费和开源的论坛软件。 它自 2014 年以来一直在开发中,接近测试阶段的尾声。
在本教程中,我们将完成向用户帐户添加自定义字段的过程。 此自定义字段只能由用户从用户的配置文件页面设置,但也可由管理员手动编辑。 此扩展的完整和最终源代码位于 GitHub 上。
🙏非常感谢@askvortsov 的审查和帮助,以正确的方式™。
我们正在添加什么
我们将允许用户将他们的 Web3 地址添加到他们的个人资料中。 Web3 地址是用户在 Web3 生态系统中的加密身份——公私密钥对(如 SSH)的公共部分,代表一个人的区块链兼容帐户。
注 ℹ:Web3 生态系统是一个新的去中心化托管、自有数据和抗审查通信的互联网。 有关 Web3 的入门知识,请参阅 FOSDEM 上的 15 分钟演讲。
即使您对 Web3 不感兴趣,本教程也会很有用。 本教程的第一部分将向您展示如何为用户构建自定义字段,第二部分将以加密安全的方式添加实际的 Web3 地址。
先决条件
我们假设您已经安装了 NodeJS 并且是足够新的版本(12.16+ 可以),并且 Composer 在全球可用。 为了您的理智,我们还建议使用 Yarn 而不是 npm。 假设 Flarum 的 PHP、MySQL 和其他要求存在并正常运行。
在下面的示例中,我们将本地 Flarum 副本托管在 ubikforum.test
,一些屏幕截图可能反映了这一点。
还请确保您的论坛在 debug
通过在设置适当的值模式 config.php
:
<?php return array(
'debug' => true,
'database' => // ...
新扩展
我们通过在新创建的文件中运行 Friends of Flarum 样板向导来启动一个新的扩展 packages
我们本地 Flarum 安装根文件夹中的文件夹:
# cd into your flarum folder
mkdir packages & cd packages
npx @friendsofflarum/create-flarum-extension web3address
重要 ⚠:请记住遵循最佳部署实践并忽略 packages
文件夹,如果您将此 Flarum 文件夹推送到要从中部署实时版本的存储库。
填写向导提供的输入:
✔ Admin CSS & JS … no
✔ Forum CSS & JS … yes
✔ Locale … yes
✔ Javascript … yes
✔ CSS … yes
注意ℹ:你需要设置 Admin CSS & JS
到 yes
如果您计划使用设置和/或权限,例如只允许某些人修改他们的 web3address
属性或类似的。 在这种情况下,我们不需要它。
请记住,由于一个错误,生成器不支持包名称或命名空间中的数字。 因此,最好在生成完成后重命名这些值。 (例如,您不能使用 web3address
正如名字,但是 blockchain
很好。)
我们还需要编译 JavaScript。 最好让它以监视模式运行,这样它会在文件更改时自动重新编译,并且您可以在开发时快速检查更改:
cd packages/web3address
cd js
yarn && yarn dev
注意 ℹ:您需要让它在终端选项卡中运行,并在另一个选项卡中执行其余命令。 这 dev
命令激活一个永远在线的任务,它将占用当前的终端会话。
然后我们安装我们新创建的扩展:
composer config repositories.0 path "packages/*"
composer require swader/blockchain @dev
第一行会告诉 Composer 它应该寻找我们安装在 packages
子文件夹,如果找不到它们,则默认为 Packagist.org。
第二行安装我们新创建的扩展。 一旦它进入,我们就可以加载我们论坛的管理界面,激活扩展,并检查论坛前端的控制台是否有“Hello world”消息。 如果它在那里,则新的扩展有效。
延伸
在构建扩展时,您总是在扩展下面的原始 Flarum。 这些扩展在您的扩展中定义 extend.php
带有各种扩展器的文件是您可以挂钩的可能扩展点的“类别”。 我们稍后会修改这个文件。
请记住,论坛本身有一个 extend.php
文件也在其根文件夹中。 该文件对于您的用户可以在您的 Flarum 实例上执行的次要根级扩展很有用,而无需围绕该功能编写完整的扩展。 如果您想与他人分享您构建的内容,或将其分发到 Flarum 的替代副本,扩展是您的不二之选。
这 extend.php
文件目前看起来像这样:
<?php
namespace SwaderWeb3Address;
use FlarumExtend;
return [
(new ExtendFrontend('forum'))
->js(__DIR__ . '/js/dist/forum.js')
->css(__DIR__ . '/resources/less/forum.less'),
new ExtendLocales(__DIR__ . '/resources/locale')
];
如果您要扩展 admin
用户界面,还有另一个 Frontend
块引用 admin
代替 forum
. 就目前而言,我们只是向论坛的前端添加新的 JS 和样式,并且可以选择本地化我们扩展的 UI 元素,所以这些是得到扩展的部分。
我们将在这个文件中定义替代路由和一些侦听器,稍后您将看到。
JavaScript
首先,让我们添加 UI 占位符。 我们将编辑文件 js/src/forum/index.js
.
一开始,我们的 index.js
文件只包含这个:
app.initializers.add("swader/web3address", () => {
console.log("[swader/web3address] Hello, forum!");
});
这 initializers.add
调用使应用程序将此处指定的 JavaScript 附加到应用程序中的其余 JavaScript。 执行流程如下:
- 所有 PHP 代码加载 主要 JS 代码加载 扩展 JS 代码加载按管理 UI 中的激活顺序
如果某个扩展依赖于另一个,Flarum 会自动对它们的依赖进行排序,只要它们在它们的相关中被指定为彼此的依赖 composer.json
文件。
让我们将文件的内容更改为:
import { extend } from "flarum/extend";
import UserCard from "flarum/components/UserCard";
import Model from "flarum/Model";
import User from "flarum/models/User";
app.initializers.add("swader/web3address", () => {
User.prototype.web3address = Model.attribute("web3address");
extend(UserCard.prototype, "infoItems", function (items) {
items.add("web3address", <p>{this.attrs.user.web3address()}</p>);
if (app.session.user === this.attrs.user) {
items.add("web3paragraph", <p>Hello extension</p>);
}
});
});
flarum/extend
是一组实用程序,用于扩展或覆盖 Flarum 前端代码中的某些 UI 元素和 JS 组件。 我们用 extend
在这里代替 override
因为我们想扩展 UserCard
带有新项目的元素。 override
而是用我们的实现完全取代它。 有关差异的更多信息,请参见此处。UserCard
是个人资料上的用户信息卡。 该组件有其 infoitems
,这是一个实例 itemlist
. 这种类型的方法记录在这里。Model
是与后端共享的实体,表示数据库模型,并且 User
是那个的具体实例 Model
.
在上面的代码中,我们告诉 JS 扩展 User
具有新字段的原型: web3address
,我们将它设置为一个名为 web3address
通过调用 attribute
的方法 Model
. 然后我们想 extend
通过添加 UserCard 的项目列表 web3address
值作为输出,如果个人资料查看者也是个人资料所有者,则通过添加 web3paragraph
那只是一段里面有“Hello extension”的段落。
重要⚠:如果输出是可变的(例如,对象或数组,而不是数字/字符串),extend 只能改变输出。 使用 override 完全修改输出而不管类型。 更多信息在这里。
在论坛中重新加载您的用户个人资料将显示添加到用户名片中项目的“Hello extension”段落。
让我们把它变成一个自定义组件。 创造 src/forum/components/Web3Field.js
(您需要创建 components
文件夹)。
给它以下代码:
import Component from "flarum/Component";
export default class Web3Field extends Component {
view() {
return (
<input
className="FormControl"
onblur={this.saveValue.bind(this)}
placeholder="Your Web3 address"
/>
);
}
saveValue(e) {
console.log("Save");
}
}
这 Component
import 是 Flarum 的基础组件,我们希望对其进行扩展以构建我们自己的组件。 它是一个包装的 Mithril 组件,为了便于使用,还加入了一些 jQuery。 我们 export
它是因为我们想在我们的 index.js
文件,所以我们需要将它导入那里。 然后我们定义一个 view
方法告诉 Flarum 将什么显示为组件的内容。 在我们的例子中,它只是一个 input
调用函数的字段 saveValue
当它失去焦点时(也就是说,您离开它)。 刷新论坛应该会显示这已经有效。
前端模型默认带有 save
方法。 我们可以获得当前用户模型,它是 User
, 通过 app.session.user
. 然后我们可以改变 saveValue
我们组件上的方法:
saveValue(e) {
const user = app.session.user;
user
.save({
web3address: "Some value that's different",
})
.then(() => console.log("Saved"));
}
呼唤 save
在一个 user
对象将向 UpdateUserController
在 PHP 方面:
注ℹ:可以查看全局有哪些对象 app
对象,比如 session
对象,通过 console.log
在论坛开放时访问它。
移民
我们想存储每个用户的 web3address
在数据库中,所以我们需要添加一列到 users
桌子。 我们可以通过创建迁移来做到这一点。 新建一个文件夹 migrations
在扩展程序的根文件夹中及其内部 2020_11_30_000000_add_web3address_to_user.php
和:
<?php
use IlluminateDatabaseSchemaBlueprint;
use IlluminateDatabaseSchemaBuilder;
return [
'up' => function (Builder $schema) {
if (!$schema->hasColumn('users', 'web3address')) {
$schema->table('users', function (Blueprint $table) use ($schema) {
$table->string('web3address', 100)->index();
});
}
},
'down' => function (Builder $schema) {
$schema->table('users', function (Blueprint $table) use ($schema) {
$table->dropColumn('web3address');
});
}
];
这是通过迁移添加字段的标准方法。 更多信息在这里。
注意ℹ:文件名是约定俗成的: YYYY_MM_DD_HHMMSS_name_of_what_youre_doing.php
这有助于顺序执行迁移。 使用这种名称格式,它们很容易排序,这对于可能相互依赖的迁移很重要。 理论上,甚至像 000000001_web3address.php
会工作,但会违反惯例。 在 Flarum 中,迁移文件的名称必须包含下划线。
然后,在论坛安装的根文件夹中,运行 php flarum migrate
运行此迁移。
听众
Flarum 通过监听器工作:他们监听一些事件,然后通过调用某些 PHP 类来对它们做出反应。
序列化
每当通过以下方式更新用户模型时 app.session.user.save
,模型在PHP端保存后被序列化回传给前端。 在这种序列化的形式中,它很容易被解析并变成一个可用的 JS 对象,供 UI 显示和交互。 PHP 对象的序列化——特别是在它被保存之后——是我们可以监听的此类事件之一。
我们将编写一个监听器,它对序列化做出反应并添加新的 web3address
字段到飞行中的模型,以便前端知道该字段并可以在 UI 中显示它。
创造 /src/Listener/AddUserWeb3AddressAttribute.php
(如果不存在则创建目录):
<?php
namespace SwaderWeb3AddressListener;
use FlarumApiEventSerializing;
use FlarumApiSerializerUserSerializer;
class AddUserWeb3AddressAttribute
{
public function handle(Serializing $event)
{
if ($event->isSerializer(UserSerializer::class)) {
$event->attributes += [
'web3address' => $event->model->web3address,
];
}
}
}
我们进口 Serializing
事件,以便我们可以从中读取信息,以及 UserSerializer
检查事件的类型(一直有很多序列化发生,所以我们需要具体)。 然后,如果发生的序列化确实是用户序列化,我们向事件添加一个新属性并赋予它 web3address
附加到当前正在序列化的模型的数据库中的字段。
现在,为什么我们要向 $event
而不是某个用户实例? 因为 $event
对象的…