jorgebucaran / hyperapp

1kB-ish JavaScript framework for building hypertext applications
MIT License
19.08k stars 780 forks source link

Middleware/Plugin API #120

Closed FlorianWendelborn closed 7 years ago

FlorianWendelborn commented 7 years ago

The Idea

A middleware API could be used to easily add a logger, time-travelling-debugger or a router to any hyperapp project. It would provide a single point where a plugin can attach to everything it needs to.

app({
    plugins: [logger, router, debug]
    // ...
})

Plugins themselves are just a function that will be called as soon as hyperapp starts (basically like a subscription). They will then be able to add reducers, effects, hooks and subscriptions.


What should be exposed to a plugin?

  1. hooks, useful for stuff like a logger
  2. effects, already used by the router for actions.setLocation
  3. reducers, since they allow model-access
  4. subscriptions, can be used to register events. E.g. window.addEventListener('popstate')

To prevent this from becoming an unpredictable mess I'm suggesting a convention that all plugins only use their own namespace. With nested actions that's already possible. E.g.:

actions.router.setLocation('...')
model.router = {
    matched: '/some/route/:id',
    // ...
}

Further changes

Router needs a way to interact with the view, so a onRender hook would be a good start. Hooks need to be an array, since multiple plugins may want to listen to them.

Router rewrite

I'm also proposing that the router shares state with the rest of the application and uses the normal hyperapp lifecycle. (subscribes to subs, adds some effects, reducers, hooks, etc.)

model.router = {
    matched: '/some/route/:id',
    url: '/some/route/34593458',
    params: {
        id: 34593458
    }
}

Moving this information to the model has some really good side-effects:

  1. A time-travelling debugger can easily roll back to an old state by just adjusting the model. No need to change the URL.
  2. You may decide to directly use model.router.params.id in your effects and reducers, which greatly simplifies the actions bound to the view
  3. It's compatible with the hyperapp lifecycle and embraces how hyperapp works, rather than keeping state somewhere else and hacking together logic that is similar to what hyperapp already does
  4. Plugins will feel familiar to everyone who ever worked with hyperapp, which makes it trivial to write one yourself

Meta

As usual I added :+1: and :-1: so it's easy to vote if you don't want to leave a comment. Ultimately the vote won't decide what we do though. It's just an additional thing to consider.

jorgebucaran commented 7 years ago

@dodekeract This would open the gates to hell, can we trust people will not create entangled plugins that depend on other plugins?

I like this proposal though. Currently hyperapp expects a clunky router property if you want to use it. Using something like app({ ..., plugins }) would generalize the concept, making things feel more "right". The router is just another plugin.

app({
   view,
   reducers,
   effects,
   plugins: [router, devtools, debugger, ...]
})

If we move forward with this, however, I'd find it more useful if HyperApp still provided the router out of the box.

The same is going to be true for toString, whenever that lands.

tunnckoCore commented 7 years ago

About everything, no. As about hooks to be array - good idea. About the onRender - pretty good. Actually exactly that hook is kinda very specific and very important point for the whole application. And in my case with mich/chika it is main thing.

FlorianWendelborn commented 7 years ago

This would open the gates to hell, can we trust people will not create entangled plugins that depend on other plugins? — @jbucaran

People can and will do that. We're providing everything that's important out-of-the-box though, so we don't suffer from the same problems react has with react + redux + react-router + react-redux + react-redux-router.

HyperApp still provided the router out of the box. — @jbucaran

I'm not suggesting to change that. It'd just work a bit different internally.

About everything, no. — @tunnckoCore

Can you provide any reasons/concerns why you don't like this? I think it makes the whole router thing less magical and embraces the data-flow that hyperapp already has.

tunnckoCore commented 7 years ago

Actually, no so. But okey, how they will be called? more specifically when and where? Don't know, not sure enough. But yea, router seems like a plugin in any way.

FlorianWendelborn commented 7 years ago

@tunnckoCore Once on the beginning of the application start. Plugins may then decide to register subscriptions, add effects & reducers and subscribe to hooks. That allows them to do everything they need to do while embracing the state-management of hyperapp.

jorgebucaran commented 7 years ago

Right now, we really have no way to open up app(options) to userland other than what options has: reducers, effects, subscriptions, etc. In order to make the router work with app we added a special router prop, but @dodekeract's proposal raises this to a new level.

If we move forward with this, router would be just a plugin, which we happen to ship out of the box with this repo, but a plugin nevertheless.

I wonder what other stuff we can make with plugins? 🤔 Perhaps someone out there might come up with a plugin that helps to create domain specific kind of apps. Games, socket-based apps, etc.

FlorianWendelborn commented 7 years ago

@jbucaran a socket.io integration would be trivial to do with this. Basically everything that's possible to do with the normal hyperapp workflow should be possible as a plugin.

jorgebucaran commented 7 years ago

Interesting.

tunnckoCore commented 7 years ago

Exactly. Why not just then use/compose multiple apps()? If we expose the whole options object to plugins, then why not just use multiple apps :D Don't know.

The very big and main thing for me to externalize the state management. One good Elm-like state management - and actually when i think a bit did it. There's only barracks currently. But we can do it smaller of course. Also that nested actions is kinda cool feature.

jorgebucaran commented 7 years ago

I was wondering, does it have to be app({ plugins })?

Just a wild and random thought: what do you think about this: app(options, ...plugins)?

So:

app({
   view,
   reducers,
   effects
}, router, devtools, debugger)

For comparison:

app({
   view,
   reducers,
   effects,
   plugins: [router, devtools, debugger, ...]
})
FlorianWendelborn commented 7 years ago

@tunnckoCore that's actually what I did at dodekeract/hyperapp-router. It looks odd though.

FlorianWendelborn commented 7 years ago

@jbucaran That syntax would imply that hyperapp is fine with multiple apps too. Not sure if that makes sense to do. :)