Rich Harris e723f781a8 -> v0.0.2
2017-12-11 13:08:35 -05:00
2017-12-11 08:55:21 -05:00
2017-12-11 08:55:21 -05:00
2017-12-11 13:08:35 -05:00

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 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:

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) 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:

// 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:

// 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:

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.
Description
No description provided
Readme MIT 3.4 MiB
Languages
TypeScript 58.1%
JavaScript 32%
Svelte 5.8%
HTML 3.3%
CSS 0.8%