mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 09:54:41 -05:00
chore: add guide for contributors (#1131)
This commit is contained in:
231
ARCHITECTURE.md
Normal file
231
ARCHITECTURE.md
Normal file
@@ -0,0 +1,231 @@
|
||||
# Architecture
|
||||
|
||||
The goal of this document is to make it easier for contributors (and anyone
|
||||
who’s interested!) to understand the architecture of the framework.
|
||||
|
||||
The whole Leptos framework is built from a series of layers. Each of these layers
|
||||
depends on the one below it, but each can be used independently from the ones
|
||||
built on top of it. While running a command like `cargo leptos new --git
|
||||
leptos-rs/start` pulls in the whole framework, it’s important to remember that
|
||||
none of this is magic: each layer of that onion can be stripped away and
|
||||
reimplemented, configured, or adapted as needed, incrementally.
|
||||
|
||||
> Everything that follows will assume you have a good working understanding
|
||||
> of the framework. There will be explanations of how some parts of it work
|
||||
> or fit together, but these are not docs. They assume you know what I’m
|
||||
> talking about.
|
||||
|
||||
## The Reactive System: `leptos_reactive`
|
||||
|
||||
The reactive system allows you to define dynamic values (signals),
|
||||
the relationships between them (derived signals and memos), and the side effects
|
||||
that run in response to them (effects).
|
||||
|
||||
These concepts are completely independent of the DOM and can be used to drive
|
||||
any kind of reactive updates. The reactive system is based on the assumption
|
||||
that data is relatively cheap, and side effects are relatively expensive. Its
|
||||
goal is to minimize those side effects (like updating the DOM or making a network
|
||||
requests) as infrequently as possible.
|
||||
|
||||
The reactive system is implemented as a single data structure that exists at
|
||||
runtime. In exchange for giving ownership over a value to the reactive system
|
||||
(by creating a signal), you receive a `Copy + 'static` identifier for its
|
||||
location in the reactive system. This enables most of the ergonomics of storing
|
||||
and sharing state, the use of callback closures without lifetime issues, etc.
|
||||
This is implemented by storing signals in a slotmap arena. The signal, memo,
|
||||
and scope types that are exposed to users simply carry around an index into that
|
||||
slotmap.
|
||||
|
||||
> Items owned by the reactive system are dropped when the corresponding reactive
|
||||
> scope is dropped, i.e., when the component or section of the UI they’re
|
||||
> created in is removed. In a sense, Leptos implements a “garbage collector”
|
||||
> in which the lifetime of data is tied to the lifetime of the UI, not Rust’s
|
||||
> lexical scopes.
|
||||
|
||||
## The DOM Renderer: `leptos_dom`
|
||||
|
||||
The reactive system can be used to drive any kinds of side effects. One very
|
||||
common side effect is calling an imperative method, for example to update the
|
||||
DOM.
|
||||
|
||||
The entire DOM renderer is built on top of the reactive system. It provides
|
||||
a builder pattern that can be used to create DOM elements dynamically.
|
||||
|
||||
The renderer assumes, as a convention, that dynamic attributes, classes,
|
||||
styles, and children are defined by being passed a `Fn() -> T`, where their
|
||||
static equivalents just receive `T`. There’s nothing about this that is
|
||||
divinely ordained, but it’s a useful convention because it allows us to use
|
||||
zero-overhead derived signals as one of several ways to indicate dynamic
|
||||
content.
|
||||
|
||||
`leptos_dom` also contains code for server-side rendering of the same
|
||||
UI views to HTML, either for out-of-order streaming (`src/ssr.rs`) or
|
||||
in-order streaming/async rendering (`src/ssr_in_order.rs`).
|
||||
|
||||
## The Macros: `leptos_macro`
|
||||
|
||||
It’s entirely possible to write Leptos code with no macros at all. The
|
||||
`view` and `component` macros, the most common, can be replaced by
|
||||
the builder syntax and simple functions (see the `counter_without_macros`
|
||||
example). But the macros enable a JSX-like syntax for describing views.
|
||||
|
||||
This package also contains the `Params` derive macro used for typed
|
||||
queries and route params in the router.
|
||||
|
||||
### Macro-based Optimizations
|
||||
|
||||
Leptos 0.0.x was built much more heavily on macros. Taking its cues
|
||||
from SolidJS, the `view` macro emitted different code for CSR, SSR, and
|
||||
hydration, optimizing each. The CSR/hydrate versions worked by compiling
|
||||
the view to an HTML template string, cloning that `<template>`, and
|
||||
traversing the DOM to set up reactivity. The SSR version worked similarly
|
||||
by compiling the static parts of the view to strings at compile time,
|
||||
reducing the amount of work that needed to be done on each request.
|
||||
|
||||
Proc macros are hard, and this system was brittle. 0.1 introduced a
|
||||
more robust renderer, including the builder syntax, and rebuilt the `view`
|
||||
macro to use that builder syntax instead. It moved the optimized-but-buggy
|
||||
CSR version of the macro to a more-limited `template` macro.
|
||||
|
||||
The `view` macro now separately optimizes SSR to use the same static-string
|
||||
optimizations, which (by our benchmarks) makes Leptos about 3-4x faster
|
||||
than similar Rust frontend frameworks in its HTML rendering.
|
||||
|
||||
> The optimization is pretty straightforward. Consider the following view:
|
||||
>
|
||||
> ```rust
|
||||
> view! { cx,
|
||||
> <main class="text-center">
|
||||
> <div class="flex-col">
|
||||
> <button>"Click me."</button>
|
||||
> <p class="italic">"Text."</p>
|
||||
> </div>
|
||||
> </main>
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> Internally, with the builder this is something like
|
||||
>
|
||||
> ```rust
|
||||
> Element {
|
||||
> tag: "main",
|
||||
> attrs: vec![("class", "text-center")],
|
||||
> children: vec![
|
||||
> Element {
|
||||
> tag: "div",
|
||||
> attrs: vec![("class", "flex-col")],
|
||||
> children: vec![
|
||||
> Element {
|
||||
> tag: "button",
|
||||
> attrs: vec![],
|
||||
> children: vec!["Click me"]
|
||||
> },
|
||||
> Element {
|
||||
> tag: "p",
|
||||
> attrs: vec![("class", "italic")],
|
||||
> children: vec!["Text"]
|
||||
> }
|
||||
> ]
|
||||
> }
|
||||
> ]
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> This is a _bunch_ of small allocations and separate strings,
|
||||
> and in early 0.1 versions we used a `SmallVec` for children and
|
||||
> attributes and actually caused some stack overflows.
|
||||
>
|
||||
> But if you look at the view itself you can see that none of this
|
||||
> will _ever_ change. So we can actually optimize it at compile
|
||||
> time to a single `&'static str`:
|
||||
>
|
||||
> ```rust
|
||||
> r#"<main class="text-center">
|
||||
> <div class="flex-col">
|
||||
> <button>"Click me."</button>
|
||||
> <p class="italic">"Text."</p>
|
||||
> </div>
|
||||
> </main>"#
|
||||
> ```
|
||||
|
||||
## Server Functions (`leptos_server`, `server_fn`, and `server_fn_macro`)
|
||||
|
||||
Server functions are a framework-agnostic shorthand for converting
|
||||
a function, whose body can only be run on the server, into an ad hoc
|
||||
REST API endpoint, and then generating code on the client to call that
|
||||
endpoint when you call the function.
|
||||
|
||||
These are inspired by Solid/Bling’s `server$` functions, and there’s
|
||||
similar work being done in a number of other JavaScript frameworks.
|
||||
|
||||
RPC is not a new idea, but these kinds of server functions may be.
|
||||
Specifically, by using web standards (defaulting to `POST`/`GET` requests
|
||||
with URL-encoded form data) they allow easy graceful degradation and the
|
||||
use of the `<form>` element.
|
||||
|
||||
This function is split across three packages so that `server_fn` and
|
||||
`server_fn_macro` can be used by other frameworks. `leptos_server`
|
||||
includes some Leptos-specific reactive functionality (like actions).
|
||||
|
||||
## `leptos`
|
||||
|
||||
This package is built on and reexports most of the layers already
|
||||
mentioned, and implements a number of control-flow components (`<Show/>`,
|
||||
`<ErrorBoundary/>`, `<For/>`, `<Suspense/>`, `<Transition/>`) that use
|
||||
public APIs of the other packages.
|
||||
|
||||
This is the main entrypoint for users, but is relatively light itself.
|
||||
|
||||
## `leptos_meta`
|
||||
|
||||
This package exists to allow you to work with tags normally found in
|
||||
the `<head>`, from within your components.
|
||||
|
||||
It is implemented as a distinct package, rather than part of
|
||||
`leptos_dom`, on the principle that “what can be implemented in userland,
|
||||
should be.” The framework can be used without it, so it’s not in core.
|
||||
|
||||
## `leptos_router`
|
||||
|
||||
The router originates as a direct port of `solid-router`, which is the
|
||||
origin of most of its terminology, architecture, and route-matching logic.
|
||||
|
||||
Subsequent developments (like animated routing, and managing route transitions
|
||||
given the lack of `useTransition` in Leptos) have caused it to diverge
|
||||
slightly from Solid’s exact code, but it is still very closely related.
|
||||
|
||||
The core principle here is “nested routing,” dividing a single page
|
||||
into independently-rendered parts. This is described in some detail in the docs.
|
||||
|
||||
Like `leptos_meta`, it is implemented as a distinct package, because it
|
||||
can be replaced with another router or with none. The framework can be used
|
||||
without it, so it’s not in core.
|
||||
|
||||
## Server Integrations
|
||||
|
||||
The server integrations are the most “frameworky” layer of the whole framework.
|
||||
These **do** assume the use of `leptos`, `leptos_router`, and `leptos_meta`.
|
||||
They specifically draw routing data from `leptos_router`, and inject the
|
||||
metadata from `leptos_meta` into the `<head>` appropriately.
|
||||
|
||||
But of course, if you one day create `leptos-helmet` and `leptos-better-router`,
|
||||
you can create new server integrations that plug them into the SSR rendering
|
||||
methods from `leptos_dom` instead. Everything involved is quite modular.
|
||||
|
||||
These packages essentially provide helpers that save the templates and user apps
|
||||
from including a huge amount of boilerplate to connect the various other packages
|
||||
correctly. Again, early versions of the framework examples are illustrative here
|
||||
for reference: they include large amounts of manual SSR route handling, etc.
|
||||
|
||||
## `cargo-leptos` helpers
|
||||
|
||||
`leptos_config` and `leptos_hot_reload` exist to support two different features
|
||||
of `cargo-leptos`, namely its configuration and its view-patching/hot-
|
||||
reloading features.
|
||||
|
||||
It’s important to say that the main feature `cargo-leptos` remains its ability
|
||||
to conveniently tie together different build tooling, compiling your app to
|
||||
WASM for the browser, building the server version, pulling in SASS and
|
||||
Tailwind, etc. It is an extremely good build tool, not a magic formula. Each
|
||||
of the examples includes instructions for how to run the examples without
|
||||
`cargo-leptos`.
|
||||
75
CONTRIBUTING.md
Normal file
75
CONTRIBUTING.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Contributing to Leptos
|
||||
|
||||
Thanks for your interesting in contributing to Leptos! This is a truly
|
||||
community-driven framework, and while we have a central maintainer (@gbj)
|
||||
large parts of the renderer, reactive system, and server integrations have
|
||||
all been written by other contributors. Contributions are always welcome.
|
||||
|
||||
Participation in this community is governed by a [Code of Conduct](./CODE_OF_CONDUCT.md).
|
||||
Some of the most active conversations around development take place on our
|
||||
[Discord server](https://discord.gg/YdRAhS7eQB).
|
||||
|
||||
This guide seeks to
|
||||
|
||||
- describe some of the framework’s values (in a technical, not an ethical, sense)
|
||||
- provide a high-level overview of how the pieces of the framework fit together
|
||||
- orient you to the organization of this repository
|
||||
|
||||
## Values
|
||||
|
||||
Leptos, as a framework, reflects certain technical values:
|
||||
|
||||
- **Expose primitives rather than imposing patterns.** Provide building blocks
|
||||
that users can combine together to build up more complex behavior, rather than
|
||||
requiring users follow certain templates, file formats, etc. e.g., components
|
||||
are defined as functions, rather than a bespoke single-file comonent format.
|
||||
The reactive system feeds into the rendering system, rather than being defined
|
||||
by it.
|
||||
- **Bottom-up over top-down.** If you envision a user’s application as a tree
|
||||
(like an HTML document), push meaning toward the leaves of the tree. e.g., If data
|
||||
needs to be loaded, load it in a granular primitive (resources) rather than a
|
||||
route- or page-level data structure.
|
||||
- **Performance by default.** When possible, users should only pay for what they
|
||||
use. e.g., we don’t make all component props reactive by default. This is
|
||||
because doing so would force the overhead of a reactive prop onto props that don’t
|
||||
need to be reactive.
|
||||
- **Full-stack performance.** Performance can’t be limited to a single metric,
|
||||
whether that’s a DOM rendering benchmark, WASM binary size, or server response
|
||||
time. Use methods like HTTP streaming and progressive enhancement to enable
|
||||
applications to load, become interactive, and respond as quickly as possible.
|
||||
- **Use safe Rust.** There’s no need for `unsafe` Rust in the framework, and
|
||||
avoiding it at all costs reduces the maintenance and testing burden significantly.
|
||||
- **Embrace Rust semantics.** Especially in things like UI templating, use Rust
|
||||
semantics or extend them in a predictable way with control-flow components
|
||||
rather than overloading the meaning of Rust terms like `if` or `for` in a
|
||||
framework-speciic way.
|
||||
- **Enhance ergonomics without obfuscating what’s happening.** This is by far
|
||||
the hardest to achieve. It’s often the case that adding additional layers to
|
||||
improve DX (like a custom build tool and starter templates) comes across as
|
||||
“too magic” to some people who haven’t had to build the same things manually.
|
||||
When possible, make it easier to see how the pieces fit together, without
|
||||
sacrificing the improved DX.
|
||||
|
||||
## Processes
|
||||
|
||||
We do not have PR templates or formal processes for approving PRs. But there
|
||||
are a few guidelines that will make it a better experience for everyone:
|
||||
|
||||
- Run `cargo fmt` before submitting your code.
|
||||
- Keep PRs limited to addressing one feature or one issue, in general. In some
|
||||
cases (e.g., “reduce allocations in the reactive system”) this may touch a number
|
||||
of different areas, but is still conceptually one thing.
|
||||
- If it’s an unsolicited PR not linked to an open issue, please include a
|
||||
specific explanation for what it’s trying to achieve. For example: “When I
|
||||
was trying to deploy my app under _circumstances X_, I found that the way
|
||||
_function Z_ was implemented caused _issue Z_. This PR should fix that by
|
||||
_solution._”
|
||||
- Our CI tests every PR against all the existing examples, sometimes requiring
|
||||
compilation for both server and client side, etc. It’s thorough but slow. If
|
||||
you want to run CI locally to reduce frustration, you can do that by installing
|
||||
`cargo-make` and using `cargo make check && cargo make test && cargo make
|
||||
check-examples`.
|
||||
|
||||
## Architecture
|
||||
|
||||
See [ARCHITECTURE.md](./ARCHITECTURE.md).
|
||||
Reference in New Issue
Block a user