mirror of
https://github.com/kevin-DL/sapper.git
synced 2026-01-12 03:05:12 +00:00
150 lines
4.9 KiB
Markdown
150 lines
4.9 KiB
Markdown
# sapper
|
|
|
|
Combat-ready apps, engineered by Svelte.
|
|
|
|
## This is not a thing yet
|
|
|
|
If you visit this README in a few weeks, hopefully it will have blossomed into the app development framework we deserve. Right now, it's just a set of ideas.
|
|
|
|
---
|
|
|
|
[Next.js](https://github.com/zeit/next.js/) introduced a beautiful idea — that you should be able to build your app as universal React components in a special `pages` directory, and the framework should take care of routing and rendering on both client and server. What if we did the same thing for Svelte?
|
|
|
|
High-level goals:
|
|
|
|
* Extreme ease of development
|
|
* Code-splitting and HMR out of the box (probably via webpack)
|
|
* Best-in-class performance
|
|
* As little magic as possible. Anyone should be able to understand how everything fits together, and e.g. make changes to the webpack config
|
|
* Links are just `<a>` tags, no special `<Link>` components
|
|
|
|
|
|
## Design
|
|
|
|
A Sapper app is just an Express app (conventionally, `server.js`) that uses the `sapper` middleware:
|
|
|
|
```js
|
|
const app = require('express')();
|
|
const sapper = require('sapper');
|
|
|
|
app.use(sapper());
|
|
|
|
const { PORT = 3000 } = process.env;
|
|
app.listen(PORT, () => {
|
|
console.log(`listening on port ${PORT}`);
|
|
});
|
|
```
|
|
|
|
The middleware serves pages that match files in the `routes` directory, and assets generated by webpack. In development mode, the middleware once activated watches `routes` to keep the app up-to-date.
|
|
|
|
|
|
## Routing
|
|
|
|
Like Next, routes are defined by the project directory structure, but with some crucial differences:
|
|
|
|
* Files with an `.html` extension are treated as Svelte components. The `routes/about.html` (or `routes/about/index.html`) would create the `/about` route.
|
|
* Files with a `.js` or `.mjs` extension are more generic route handlers. These files should export functions corresponding to the HTTP methods they support (example below).
|
|
* Instead of route masking, we embed parameters in the filename. For example `post/%id%.html` maps to `/post/:id`, and the component will be rendered with the appropriate parameter.
|
|
* Nested routes (read [this article](https://joshduff.com/2015-06-why-you-need-a-state-router.md)) can be handled by creating a file that matches the subroute — for example, `routes/app/settings/%submenu%.html` would match `/app/settings/profile` *and* `app/settings`, but in the latter case the `submenu` parameter would be `null`.
|
|
|
|
An example of a generic route:
|
|
|
|
```js
|
|
// routes/api/post/%id%.js
|
|
export async function get(req, res) {
|
|
try {
|
|
const data = await getPostFromDatabase(req.params.id);
|
|
const json = JSON.stringify(data);
|
|
|
|
res.set({
|
|
'Content-Type': 'application/json',
|
|
'Content-Length': json.length
|
|
});
|
|
|
|
res.send(json);
|
|
} catch (err) {
|
|
res.status(500).send(err.message);
|
|
}
|
|
}
|
|
```
|
|
|
|
Or, if you omit the `res` argument, it can use the return value:
|
|
|
|
```js
|
|
// routes/api/post/%id%.js
|
|
export async function get(req) {
|
|
return await getPostFromDatabase(req.params.id);
|
|
}
|
|
```
|
|
|
|
|
|
## Client-side app
|
|
|
|
Sapper will create (and in development mode, update) a barebones `main.js` file that dynamically imports individual routes and renders them — something like this:
|
|
|
|
```js
|
|
window.addEventListener('click', event => {
|
|
let a = event.target;
|
|
while (a && a.nodeName !== 'A') a = a.parentNode;
|
|
if (!a) return;
|
|
|
|
if (navigate(new URL(a.href))) event.preventDefault();
|
|
});
|
|
|
|
const target = document.querySelector('#sapper');
|
|
let component;
|
|
|
|
function navigate(url) {
|
|
if (url.origin !== window.location.origin) return;
|
|
|
|
let match;
|
|
let params = {};
|
|
const query = {};
|
|
|
|
function render(mod) {
|
|
if (component) {
|
|
component.destroy();
|
|
} else {
|
|
target.innerHTML = '';
|
|
}
|
|
|
|
component = new mod.default({
|
|
target,
|
|
data: { query, params },
|
|
hydrate: !!component
|
|
});
|
|
}
|
|
|
|
if (url.pathname === '/about') {
|
|
import('/about/index.html').then(render);
|
|
} else if (url.pathname === '/') {
|
|
import('/index.js').then(render);
|
|
} else if (match = /^\/post\/([^\/]+)$/.exec(url.pathname)) {
|
|
params.id = match[1];
|
|
import('/post/%id%.html').then(render);
|
|
} else if (match = /^\/([^\/]+)$/.exec(url.pathname)) {
|
|
params.wildcard = match[1];
|
|
import('/%wildcard%.html').then(render);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
navigate(window.location);
|
|
```
|
|
|
|
We're glossing over a lot of important stuff here — e.g. handling `popstate` — but you get the idea. Knowledge of all the possible routes means we can generate optimal code, much in the same way that statically analysing Svelte templates allows the compiler to generate optimal code.
|
|
|
|
|
|
## Things to figure out
|
|
|
|
* How to customise the overall page template
|
|
* An equivalent of `getInitialProps`
|
|
* Critical CSS
|
|
* `store` integration
|
|
* Route transitions
|
|
* Equivalent of `next export`
|
|
* A good story for realtime/GraphQL stuff
|
|
* Service worker
|
|
* Using `Link...rel=preload` headers to push main.js/[route].js plus styles
|
|
* ...and lots of other things that haven't occurred to me yet. |