Saltar al contenido principal

Desarrollo del Frontend

Esta página describe cómo realizar cambios en la interfaz de usuario de Flarum. Cómo añadir botones, marquesinas y texto parpadeante. 🤩

Recuerda, el frontend de Flarum es una aplicación JavaScript de una sola página. No hay Twig, Blade, o cualquier otro tipo de plantilla PHP para hablar. Las pocas plantillas que están presentes en el backend sólo se utilizan para renderizar el contenido optimizado para el motor de búsqueda. Todos los cambios en la interfaz de usuario deben hacerse a través de JavaScript.

Flarum tiene dos aplicaciones frontales separadas:

  • forum, el lado público de su foro donde los usuarios crean discusiones y mensajes.
  • admin, el lado privado de tu foro donde, como administrador de tu foro, configuras tu instalación de Flarum.

Comparten el mismo código fundacional, así que una vez que sabes cómo extender uno, sabes cómo extender ambos.

Typings!

Along with new TypeScript support, we have a tsconfig package available, which you should install as a dev dependency to gain access to our typings. Make sure you follow the instructions in the package's README to configure typings support.

Transpilación y estructura de archivos

Esta parte de la guía explicará la configuración de archivos necesaria para las extensiones. Una vez más, recomendamos encarecidamente utilizar el generador de extensiones FoF no oficial para configurar la estructura de los archivos por usted. Dicho esto, usted debe leer esto para entender lo que está pasando bajo la superficie.

Antes de que podamos escribir cualquier JavaScript, necesitamos configurar un transpilador. Esto nos permite usar TypeScript y su magia en el núcleo y las extensiones de Flarum.

Para hacer esta transpilación, tienes que trabajar en un entorno capaz. No, no se trata de un entorno doméstico o de oficina, ¡puedes trabajar en el baño por lo que a mí respecta! Me refiero a las herramientas instaladas en tu sistema. Necesitarás:

  • Node.js y npm (Descarga)
  • Webpack (npm install -g webpack)

Esto puede ser complicado porque cada sistema es diferente. Desde el sistema operativo que usas, hasta las versiones de los programas que tienes instalados, pasando por los permisos de acceso de los usuarios... ¡me dan escalofríos sólo de pensarlo! Si tienes problemas, dale recuerdos utiliza Google para ver si alguien se ha encontrado con el mismo error que tú y ha encontrado una solución. Si no, pide ayuda en la Comunidad Flarum o en el chat de Discord.

Es hora de configurar nuestro pequeño proyecto de transpilación de JavaScript. Crea una nueva carpeta en tu extensión llamada js, y luego introduce un par de archivos nuevos. Una extensión típica tendrá la siguiente estructura de frontend:

js
├── dist (compiled js is placed here)
├── src
│ ├── admin
│ └── forum
├── admin.js
├── forum.js
├── package.json
└── webpack.config.json

package.json

{
"private": true,
"name": "@acme/flarum-hello-world",
"dependencies": {
"flarum-webpack-config": "0.1.0-beta.10",
"webpack": "^4.0.0",
"webpack-cli": "^3.0.7"
},
"scripts": {
"dev": "webpack --mode development --watch",
"build": "webpack --mode production"
}
}

Este es un archivo de descripción de paquetes JS estándar, utilizado por npm y Yarn (gestores de paquetes Javascript). Puedes usarlo para añadir comandos, dependencias js y metadatos del paquete. En realidad no estamos publicando un paquete npm: esto simplemente se utiliza para recoger las dependencias.

Por favor, ten en cuenta que no necesitamos incluir flarum/core o cualquier extensión de flarum como dependencias: se empaquetarán automáticamente cuando Flarum compile los frontales de todas las extensiones.

webpack.config.js

const config = require('flarum-webpack-config');

module.exports = config();

Webpack es el sistema que realmente compila y agrupa todo el javascript (y sus dependencias) para nuestra extensión. Para que funcione correctamente, nuestras extensiones deben utilizar el official flarum webpack config (mostrado en el ejemplo anterior).

tsconfig.json

{
// Use Flarum's tsconfig as a starting point
"extends": "flarum-tsconfig",
// This will match all .ts, .tsx, .d.ts, .js, .jsx files in your `src` folder
// and also tells your Typescript server to read core's global typings for
// access to `dayjs` and `$` in the global namespace.
"include": ["src/**/*", "../vendor/flarum/core/js/dist-typings/@types/**/*"],
"compilerOptions": {
// This will output typings to `dist-typings`
"declarationDir": "./dist-typings",
"baseUrl": ".",
"paths": {
"flarum/*": ["../vendor/flarum/core/js/dist-typings/*"]
}
}
}

This is a standard configuration file to enable support for Typescript with the options that Flarum needs.

Always ensure you're using the latest version of this file: https://github.com/flarum/flarum-tsconfig#readme.

A continuación repasaremos las herramientas disponibles para las extensiones.

To get the typings working, you'll need to run composer update in your extension's folder to download the latest copy of Flarum's core into a new vendor folder. Remember not to commit this folder if you're using a version control system such as Git.

You may also need to restart your IDE's TypeScript server. In Visual Studio Code, you can press F1, then type "Restart TypeScript Server" and hit ENTER. This might take a minute to complete.

admin.js and forum.js

Estos archivos contienen la raíz de nuestro frontend JS real. Podrías poner toda tu extensión aquí, pero eso no estaría bien organizado. Por esta razón, recomendamos poner el código en src, y que estos archivos sólo exporten el contenido de src. Por ejemplo:

// admin.js
export * from './src/admin';

// forum.js
export * from './src/forum';

src

Si seguimos las recomendaciones para admin.js y forum.js, querremos tener 2 subcarpetas aquí: una para el código del frontend admin, y otra para el código del frontend forum. Si tienes componentes, modelos, utilidades u otro código que se comparte en ambos frontends, puedes crear una subcarpeta common y colocarla allí.

La estructura para admin y forum es idéntica, así que sólo la mostraremos para forum aquí:

src/forum/
├── components/
|-- models/
├── utils/
└── index.js

components, models, y utils son directorios que contienen archivos donde puedes definir componentes, modelos, y funciones de ayuda reutilizables. Tenga en cuenta que todo esto es simplemente una recomendación: no hay nada que le obligue a utilizar esta estructura de archivos en particular (o cualquier otra estructura de archivos).

El archivo más importante aquí es index.js: todo lo demás es simplemente extraer clases y funciones en sus propios archivos. Repasemos una estructura típica de archivos index.js:

import {extend, override} from 'flarum/extend';

// Proporcionamos nuestro código de extensión en forma de un "inicializador".
// Este es un callback que se ejecutará después de que el núcleo haya arrancado.
app.initializers.add('our-extension', function(app) {
// Su código de extensión aquí
console.log("EXTENSION NAME is working!");
});

We'll go over tools available for extensions below.

Transpilación

Bibliotecas externas

Casi todas las extensiones de Flarum necesitarán importar algo de Flarum Core. Como la mayoría de las extensiones, el código fuente JS del núcleo está dividido en las carpetas admin, common y forum. Sin embargo, todo se exporta bajo flarum. Para elaborar:

En algunos casos, una extensión puede querer extender el código de otra extensión de flarum. Esto sólo es posible para las extensiones que exportan explícitamente su contenido.

  • flarum/tags y flarum/flags son actualmente las únicas extensiones empaquetadas que permiten extender su JS. Puedes importar sus contenidos desde flarum/{EXT_NAME}/PATH (por ejemplo, flarum/tags/components/TagHero).
  • The process for extending each community extension is different; you should consult documentation for each individual extension.

Transpilation

Bien, es hora de encender el transpilador. Ejecuta los siguientes comandos en el directorio js:

npm install
npm run dev

Esto compilará su código JavaScript listo para el navegador en el archivo js/dist/forum.js, y se mantendrá atento a los cambios en los archivos fuente. ¡Genial!

Cuando hayas terminado de desarrollar tu extensión (o antes de un nuevo lanzamiento), querrás ejecutar npm run build en lugar de npm run dev: esto construye la extensión en modo de producción, lo que hace que el código fuente sea más pequeño y rápido.

Registro de activos

JavaScript

Para que el JavaScript de tu extensión se cargue en el frontend, necesitamos decirle a Flarum dónde encontrarlo. Podemos hacer esto usando el método js del extensor Frontend. Añádelo al archivo extend.php de tu extensión:

<?php

use Flarum\Extend;

return [
(new Extend\Frontend('forum'))
->js(__DIR__.'/js/dist/forum.js')
];

Flarum hará que cualquier cosa que haga export desde forum.js esté disponible en el objeto global flarum.extensions['acme-hello-world']. Por lo tanto, puede elegir exponer su propia API pública para que otras extensiones interactúen con ella.

Sólo se permite un archivo JavaScript principal por extensión. Si necesitas incluir alguna librería JavaScript externa, instálala con NPM e import para que se compile en tu archivo JavaScript, o consulta Rutas y Contenido para saber cómo añadir etiquetas <script> adicionales al documento del frontend.

:::

CSS

También puedes añadir activos CSS y LESS al frontend utilizando el método css del extensor Frontend:

    (new Extend\Frontend('forum'))
->js(__DIR__.'/js/dist/forum.js')
->css(__DIR__.'/less/forum.less')
tip

Debes desarrollar las extensiones con el modo de depuración activado en config.php. Esto asegurará que Flarum recompile los activos de forma automática, por lo que no tendrás que limpiar manualmente la caché cada vez que hagas un cambio en el JavaScript de tu extensión.

Cambiando la UI Parte 1

La interfaz de Flarum está construida con un framework de JavaScript llamado Mithril.js. Si estás familiarizado con React, lo entenderás enseguida. Pero si no estás familiarizado con ningún framework de JavaScript, te sugerimos que pases por un tutorial para entender los fundamentos antes de continuar.

El quid de la cuestión es que Flarum genera elementos virtuales del DOM que son una representación de JavaScript del HTML. Mithril toma estos elementos virtuales del DOM y los convierte en HTML real de la manera más eficiente posible. (¡Por eso Flarum es tan rápido!)

Debido a que la interfaz está construida con JavaScript, es realmente fácil engancharse y hacer cambios. Todo lo que tienes que hacer es encontrar el extensor adecuado para la parte de la interfaz que quieres cambiar, y luego añadir tu propio DOM virtual a la mezcla.

La mayoría de las partes mutables de la interfaz son en realidad listas de elementos. Por ejemplo:

  • The controls that appear on each post (Reply, Like, Edit, Delete)
  • El proceso para extender cada extensión comunitaria es diferente; debe consultar la documentación de cada extensión individual.
  • Los elementos de la cabecera (Búsqueda, Notificaciones, Menú de usuario)

Cada elemento de estas listas recibe un nombre para que puedas añadir, eliminar y reorganizar los elementos fácilmente. Simplemente encuentre el componente apropiado para la parte de la interfaz que desea cambiar, y monkey-patch sus métodos para modificar el contenido de la lista de elementos. Por ejemplo, para añadir un enlace a Google en la cabecera:

import { extend } from 'flarum/extend';
import HeaderPrimary from 'flarum/components/HeaderPrimary';

extend(HeaderPrimary.prototype, 'items', function(items) {
items.add('google', <a href="https://google.com">Google</a>);
});

No está mal. Sin duda, nuestros usuarios harán cola para agradecernos un acceso tan rápido y cómodo a Google.

En el ejemplo anterior, utilizamos la utilidad extend (explicada más adelante) para añadir HTML a la salida de HeaderPrimary.prototype.items(). ¿Cómo funciona esto realmente? Bueno, primero tenemos que entender lo que es HeaderPrimary.

Componentes

La interfaz de Flarum se compone de muchos componentes anidados. Los componentes son un poco como los elementos de HTML, ya que encapsulan el contenido y el comportamiento. Por ejemplo, mira este árbol simplificado de los componentes que conforman una página de discusión:

DiscussionPage
├── DiscussionList (the side pane)
│ ├── DiscussionListItem
│ └── DiscussionListItem
├── DiscussionHero (the title)
├── PostStream
│ ├── Post
│ └── Post
├── SplitDropdown (the reply button)
└── PostStreamScrubber

Deberías familiarizarte con la API de componentes de Mithril y el sistema de redraw. Flarum envuelve los componentes en la clase flarum/Component, que extiende la clase componentes de Mithril. Proporciona las siguientes ventajas:

  • Los controles que aparecen en cada entrada (Responder, Me gusta, Editar, Borrar)
  • El método estático initAttrs muta this.attrs antes de establecerlos, y te permite establecer valores por defecto o modificarlos de alguna manera antes de usarlos en tu clase. Ten en cuenta que esto no afecta al vnode.attrs inicial.
  • El método $ devuelve un objeto jQuery para el elemento DOM raíz del componente. Opcionalmente se puede pasar un selector para obtener los hijos del DOM.
  • el método estático component puede ser utilizado como una alternativa a JSX y al hyperscript m. Los siguientes son equivalentes:
    • m(CustomComponentClass, attrs, children)
    • CustomComponentClass.component(attrs, children)
    • <CustomComponentClass {...attrs}>{children}</CustomComponentClass>

Sin embargo, las clases de componentes que extienden Component deben llamar a super cuando utilizan los métodos oninit, oncreate y onbeforeupdate.

Volvamos al ejemplo original de "añadir un enlace a Google en la cabecera" para demostrarlo.

Todas las demás propiedades de los componentes Mithril, incluidos los métodos del ciclo de vida (con los que debería familiarizarse), se conservan. Teniendo esto en cuenta, una clase de componente personalizada podría tener este aspecto:

import Component from 'flarum/Component';

class Counter extends Component {
oninit(vnode) {
super.oninit(vnode);

this.count = 0;
}

view() {
return (
<div>
Count: {this.count}
<button onclick={e => this.count++}>
{this.attrs.buttonLabel}
</button>
</div>
);
}

oncreate(vnode) {
super.oncreate(vnode);

// En realidad no estamos haciendo nada aquí, pero este sería
// un buen lugar para adjuntar manejadores de eventos, inicializar librerías
// como sortable, o hacer otras modificaciones en el DOM.
$element = this.$();
$button = this.$('button');
}
}

m.mount(document.body, <MyComponent buttonLabel="Increment" />);

Cambiando la UI Parte 2

Ahora que tenemos una mejor comprensión del sistema de componentes, vamos a profundizar un poco más en cómo funciona la ampliación de la interfaz de usuario.

ItemList

Como se ha indicado anteriormente, la mayoría de las partes fácilmente extensibles de la interfaz de usuario le permiten extender métodos llamados items o algo similar (por ejemplo, controlItems, accountItems, toolbarItems, etc. Los nombres exactos dependen del componente que estés extendiendo) para añadir, eliminar o reemplazar elementos. Bajo la superficie, estos métodos devuelven una instancia de utils/ItemList, que es esencialmente un objeto ordenado. La documentación detallada de sus métodos está disponible en nuestra documentación de la API. Cuando se llama al método toArray de ItemList, los elementos se devuelven en orden ascendente de prioridad (0 si no se proporciona), y luego por clave alfabéticamente cuando las prioridades son iguales.

Utilidades de Flarum

Casi todas las extensiones del frontend utilizan monkey patching para añadir, modificar o eliminar comportamientos. Por ejemplo:

// Esto añade un atributo al global `app`.
app.googleUrl = "https://google.com";

// Esto reemplaza la salida de la página de discusión con "Hello World"
import DiscussionPage from 'flarum/components/DiscussionPage';

DiscussionPage.prototype.view = function() {
return <p>Hello World</p>;
}

convertirá las páginas de discusión de Flarum en anuncios de "Hola Mundo". ¡Qué creativo!

En la mayoría de los casos, no queremos reemplazar completamente los métodos que estamos modificando. Por esta razón, Flarum incluye las utilidades extend y override. extend nos permite añadir código para que se ejecute después de que un método se haya completado. La función override nos permite reemplazar un método por uno nuevo, manteniendo el método anterior disponible como callback. Ambas son funciones que toman 3 argumentos:

  1. El prototipo de una clase (o algún otro objeto extensible)
  2. El nombre de cadena de un método de esa clase
  3. Un callback que realiza la modificación.
    1. En el caso de extend, la llamada de retorno recibe la salida del método original, así como cualquier argumento pasado al método original.
    2. Para override, el callback recibe un callable (que puede ser usado para llamar al método original), así como cualquier argumento pasado al método original.
Overriding multiple methods

With extend and override, you can also pass an array of multiple methods that you want to patch. This will apply the same modifications to all of the methods you provide:

extend(IndexPage.prototype, ['oncreate', 'onupdate'], () => { /* your logic */ });

Ten en cuenta que si intentas cambiar la salida de un método con override, debes devolver la nueva salida. Si estás cambiando la salida con extend, simplemente debes modificar la salida original (que se recibe como primer argumento). Ten en cuenta que extend sólo puede mutar la salida si ésta es mutable (por ejemplo, un objeto o un array, y no un número/cadena).

Let's now revisit the original "adding a link to Google to the header" example to demonstrate.

import { extend, override } from 'flarum/extend';
import HeaderPrimary from 'flarum/components/HeaderPrimary';
import ItemList from 'flarum/utils/ItemList';
import CustomComponentClass from './components/CustomComponentClass';

// Aquí, añadimos un elemento a la lista de elementos devuelta. Estamos utilizando un componente personalizado
// como se ha comentado anteriormente. También hemos especificado una prioridad como tercer argumento,
// que se utilizará para ordenar estos elementos. Ten en cuenta que no necesitamos devolver nada.
extend(HeaderPrimary.prototype, 'items', function(items) {
items.add(
'google',
<CustomComponentClass>
<a href="https://google.com">Google</a>
</CustomComponentClass>,
5
);
});

// Aquí, utilizamos condicionalmente la salida original de un método,
// o creamos nuestro propio ItemList, y luego añadimos un elemento a él.
// Ten en cuenta que DEBEMOS devolver nuestra salida personalizada.
override(HeaderPrimary.prototype, 'items', function(original) {
let items;

if (someArbitraryCondition) {
items = original();
} else {
items = new ItemList();
}

items.add('google', <a href="https://google.com">Google</a>);

return items;
});

Dado que todos los componentes y utilidades de Flarum están representados por clases, extend, override, y el típico JS significa que podemos enganchar o reemplazar cualquier método en cualquier parte de Flarum. Algunos usos potenciales "avanzados" incluyen:

  • Extender o anular view para cambiar (o redefinir completamente) la estructura html de los componentes de Flarum. Esto abre a Flarum a una tematización ilimitada
  • Engancharse a los métodos de los componentes Mithril para añadir escuchas de eventos JS, o redefinir la lógica del negocio.

Flarum Utils

Flarum define (y proporciona) bastantes funciones de ayuda y utilidades, que puede querer utilizar en sus extensiones. A few particularly useful ones:

  • flarum/common/utils/Stream provides Mithril Streams, and is useful in forms.
  • Engancharse a los métodos de los componentes Mithril para añadir escuchas de eventos JS, o redefinir la lógica del negocio.
  • flarum/common/utils/extractText extracts text as a string from Mithril component vnode instances (or translation vnodes).
  • flarum/common/utils/throttleDebounce provides the throttle-debounce library
  • flarum/common/helpers/avatar displays a user's avatar
  • flarum/common/helpers/highlight highlights text in strings: great for search results!
  • flarum/common/helpers/icon displays an icon, usually used for FontAwesome.
  • flarum/common/helpers/username shows a user's display name, or "deleted" text if the user has been deleted.

And there's a bunch more! Some are covered elsewhere in the docs, but the best way to learn about them is through the source code or our javascript API documentation.