-
Notifications
You must be signed in to change notification settings - Fork 87
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
A lot on the state platform, but a lot of links are still not filled in yet.
- Loading branch information
1 parent
7b9d2df
commit 6c5f6a7
Showing
13 changed files
with
352 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
# Plugins | ||
|
||
One of the most powerful features of Perseus is its extensibility, which comes in a number of forms. The first is the openness of the API that runs Perseus, primarily through the [`Turbine`](=turbine/struct.Turbine@perseus) type, which can be used to manually generate state, prerender pages, and even build your app. This can be used to create custom engines by ignoring the `#[perseus::main]` macro, and to control your entire app from start to finish. You can even replace Perseus' default instantiation code on the client-side. | ||
|
||
However, 99.99% of the time, you won't need to do any of this, because your needs will be met far more effectively by either a [custom server], or a plugin. | ||
|
||
Plugins in Perseus are library crates, usually published on crates.io, that can be used as dependencies in your app that have access to various *plugin opportunities*, whcih are basically points in your app where Perseus allows third-party code to do certain things. | ||
|
||
Plugins fall into two types: *functional* and *control*. Functional plugins are very simple: they're given some data at a certain time, they do some stuff, and then they return some data. For a single plugin opportunity, there can be as many functional plugins registered as you like. For example, a plugin can define extra static aliases, and, of course, the results of many plugins doing this can all be collated together. Control plugins work differently: for a single control plugin opportunity, only one plugins can act, for example redefining the index view (since you can't necessarily combine two completely different index views from two completely different plugins). | ||
|
||
Currently, there aren't a huge number of plugin actions in Perseus, but this will change in future, and the number of plugin opportunities can be expected to grow over the coming releases. Writing plugins is somewhat complex, and is best explained through the [plugin API documentation](=plugins@perseus), and the [plugin example] in the Perseus repository. If you need any further help writing your plugin, feel free to [open a GitHub discussion] or [ask on our Discord], and our community will be happy to help! | ||
|
||
## Tinker plugins | ||
|
||
One type of plugin that is particualrly special is the *tinker plugin*. These plugins have free reign to do whatever they want when a special command, `perseus tinker`, is run. An example of registering a tinker plugin can be found [here]. These may be used to run special build processes, or to even modify user code in arbitrary ways (for example to set a custom allocator), since they run as a separate stage to the build process. These can be considered the closest equivalent to normal Rust build scripts in a Perseus app. (Although you can use normal build scripts if you like, those will work too.) Since the removal of the legacy `.perseus/` directory, tinker plugins have far less utility today than they once did. | ||
|
||
## The plugins registry | ||
|
||
On this website, a registry of all known plugins is maintained [here](plugins), which currently has a very small number of plugins, because the ecosystem for all this is still very young (plugins were only introduced in v0.3.x). Plugins that are endorsed by the Perseus developers (which implies a code audit, but by no means a guarantee of security, and Perseus takes no responsibility for rogue plugins whatsoever, as they are third-party code) will appear with a tick next to them. You can add your plugin to the regsitry by following the instructions in [our issue-reporting system], which will guide you through the process. | ||
|
||
## Security | ||
|
||
Plugins are third-party code, and, because they can do basically anything they want, they can pose a serious risk to the security of your system. For example, a plugin running at build-time could say it's parsing some [Less], but actually be downloading ransomware onto your computer. Any reports of rogue plugins should be reported confidentially [to the Perseus maintainer], and they will be dealt with expeditiously from there. Of course, we cannot take down rogue third-party plugins, but we can report them to code hosts like GitHub, and have them removed from our own plugin registry. | ||
|
||
Once again, the Perseus project cannot be held responsible in any way for rogue plugins, including those we list on our registry, as no code audits take place before listing. Plugins that have a tick next to them have undergone an audit *in the past*, and may have since been taken over. Always make sure you trust the plugins you install! (You should do this for any library you install on your system.) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
# Using state | ||
|
||
Most of this section has been devoted to methods of generating state, but what about actually *using* it in views? This is actually a complex subject, mainly because of how Perseus handles the difference between reactive and unreactive state. | ||
|
||
## The flow of state | ||
|
||
Once your state has been generated on the engine-side, at build-time, request-time, whatever, it will be used to render the HTML of your page in advance (as early as possible). This will produce an HTML fragment, which will be interpolated into the index view of your app, with a few variables set (including a JSON representation of your state, which can be used in hydration). This is then sent to the browser, where [hydration](:fundamentals/hydration) occurs and your state is deserialized into your state type. | ||
|
||
From here, things get a bit more complicated, because of the reactive state system. The simplest thing possible would be for your deserialized state to go straight to your page, but Perseus intervenes here. All state types in Perseus must implement four traits: `Serialize` and `Deserialize` (from Serde, to allow turning them into JSON and back again), `Clone` (for some internal mechanics, but this is used sparingly), and `MakeRx`. Now, this fourth one definitely qualifies as implementation details, and you don't need to know how this works to use it, but a lot of Rust developers like to know what's going on behind the scenes, so here you go! | ||
|
||
When you derive `ReactiveState`, what that macro does is create an implementation of `MakeRx` that takes each field of your state and wraps it in an [`RcSignal`], from Sycamore, which makes it *reactive*, meaning you can run `.get()` and `.set()` on it. This reactive version is named according to the `#[rx(alias = "..")]` derive macro helper that you provide. Then, this reactive version has `MakeUnrx` implemented on it, which allows it to be turned back into its unreactive version. There are also some more special traits involved with [state freezing](:state/freezing), but that will be dealt with later. | ||
|
||
<details> | ||
|
||
<summary>How does unreactive state work?</summary> | ||
|
||
The `MakeRx` implementation just creates a wrapper that isn't really reactive, and the `MakeUnrx` implementation just removes that wrapper. Yeah, it's that simple. | ||
|
||
</details> | ||
|
||
Once Perseus has made your state reactive, it will store it in the *state store*, which is pretty much a giant repository of all the states your app has. As a user visits a new page, its state will be added to this cache, allowing that page to be re-rendered later without any network requests. This can be thought of as the caching equivalent of SPA routing (if you're familiar with that), and it allows Perseus to ensure a seamless experience for your users. The number of pages that can be in the state store at any one time is 25 by default (but this may change in a future release), and you can it manually with the `.pss_max_size()` method on your `PerseusApp`. | ||
|
||
Because Perseus makes your state reactive, *and then* stores it in the state store (abbreviated PSS for Perseus state store, since the alternative is quite unsavoury), any updates your pages make to their state will be reflected in this cache, meaning that, when users come back to, say, a pahge whose state included some form inputs, those inputs will be as they left them, without needing to rely on the browser to provide this. We strongly believe this behavior should be the default for the web, and it's built into Perseus. (If you'd like to avoid it though, you can always use unreactive state, or use `Signal`s manually that aren't checked into Perseus.) | ||
|
||
When the user goes to a page they've already visited in the past, Perseus will try to find the cached state in the PSS, and it will use that if it can. Otherwise, it will request the state only (no HTML) from the server, and then cache it. | ||
|
||
## Using reactive state | ||
|
||
When you're writing views that don't take state, the function signatures are very simple: just accept a Sycamore scope, and return a `View<G>`. But, when there's state involved, things get *way* more complicated. Most of the time, you'll write something like this: | ||
|
||
``` | ||
#[auto_scope] | ||
fn my_view<G: Html>(cx: Scope, state: &MyStateRx) -> View<G> | ||
``` | ||
|
||
This is made possible by the `#[auto_scope]` macro, which rewrites this function signature into something much more complicated with lifetimes everywhere: | ||
|
||
``` | ||
fn my_view<'page, G: Html>(cx: BoundedScope<'_, 'page>, state: &'page MyStateRx) -> View<G> | ||
``` | ||
|
||
So let's break this down. We've gone from `Scope` to `BoundedScope`, which is an important difference. Basically, a `BoundedScope` is the fundamental primitive in Sycamore: it takes the lifetime of some root-level scope, and then the lifetime of itself. The reason for this is that, in Sycamore, you can have *child scopes*: so, in Perseus, the first lifetime is `'app`, and the second is `'page`, where the app will outlive the page. `Scope` is actually just an alias for a special type of `BoundedScope` where the lifetimes are the same, but it's much easier to write, so `#[auto_scope]` lets you do that. Notice that the `'app` lifetime can be elided, and Rust will figure this out itself. | ||
|
||
The next thing is that the state is borrowed for the lifetime of the page, which might not make sense at first: don't you want it to live as long as the app if it's in some cache? Well, this gets to the idea of Perseus being a *framework*, not a *library*. Perseus is in charge of your state, so the cache actually comes first. The cache is what has the owned copy of the state, and you get a reference. Since the reactive version of your state is all `RcSignal`s anyway, there's no cost to `Clone`ing it, but, if we use a reference with the same lifetime as the page, Sycamore's `view!` macro can understand that it's safe to interpolate the state anywhere we want: it is *guaranteed* through Rust's type system to live as long as the page. This avoids all sorts of nasty lifetime errors, as anyone who used Sycamore before v0.8 can attest to! | ||
|
||
Note that it's perfectly fine for you to write out the full lifetime bounds if you want to, the `#[auto_scope]` macro just exists for convenience. If you don't like the magic of it, you don't have to use it at all. (In fact, you don't have to use *any* of Perseus' macros if you don't want, and you can even disable them altogether, they're gated by the `macros` feature, which is enabled by default.) | ||
|
||
## Unreactive state | ||
|
||
When you're using unreactive state, none of this is necessary, because Perseus just gives you an owned copy of your state to do with as you please, and you don't need `#[auto_scope]` or any special lifetimes. (You can even use a normal `Scope`, which is a white lie to Rust's type system, but it's totally immaterial to the output, so it's a useful elision.) |
Oops, something went wrong.