-
Notifications
You must be signed in to change notification settings - Fork 781
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce plugin architecture. #124
Conversation
Codecov Report
@@ Coverage Diff @@
## master #124 +/- ##
=====================================
Coverage 100% 100%
=====================================
Files 3 4 +1
Lines 170 174 +4
=====================================
+ Hits 170 174 +4
Continue to review full report at Codecov.
|
@tunnckoCore @lukejacksonn Could you review this PR too? 🙏 |
Sure, I will take a look this afternoon! 👍 |
src/router.js
Outdated
], | ||
hooks: { | ||
onRender: function (model, view) { | ||
console.log(model.router.location) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
console.log here 🔥
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haha.
src/router.js
Outdated
} | ||
} | ||
|
||
return routes["/"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Were we going to be implementing the changes made in #113 to this PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better after this is landed 👍.
Too much set in one commit.. but i'll try. In first glance i have some notes. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me!
src/app.js
Outdated
onUpdate: Function.prototype, | ||
onError: function (err) { | ||
var onError = function (err) { | ||
if (!hook("onError", function (fn) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is very strange to me.. Consider moving the callback function before the if?
var onerror = function (fn) { fn(err) }
if (!hook("onError", onerror)) {
throw err
}
and in anyway it looks strange, but that's better
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's not going to work, because we pass onError to effects and subscriptions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wat? no, no. I not meant that. It's no matter how the var is named. The point is to not be inside the if condition
var foobar = function (fn) { fn(err) }
if (!hook("onError", foobar)) {
throw err
}
and that (as currently) are the same thing
if (!hook("onError", function (fn) { fn(err) })) { // collapsed in one line
throw err
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe I'm missing something, but I still think this is not going to work when sharing onError with multiple plugins. This is a minor thing, so please send me a PR after we land this one 👍.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the new hook system this problem has gone away!
src/router.js
Outdated
params[re.keys.shift()] = arguments[i] | ||
} | ||
|
||
match = function (model, actions) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move function out of the replace? Move it inside the outside if
. It would be faster for sure. It may sounds like a super micro optimization, but not only that won't redefined that function on each found match, but it would looks a bit better and natural.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope. It isn't (looking at the latest commit) L54-56.
Simply make match = 1
and if there is no matches in that .replace()
function match would be falsey value, otherwise will be 1.
path.replace(new RegExp(re.re, "g"), function () {
for (var i = 1; i < arguments.length - 2; i++) {
params[re.keys.shift()] = arguments[i]
}
match = 1
})
if (match) {
return function (model, actions) {
return routes[route](model, actions, params)
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hehe, done, locally. I haven't pushed anything 😄.
src/router.js
Outdated
} | ||
} | ||
match = function (model, actions) { | ||
return routes[route](model, actions, params) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't params
be in the model now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should actually also improve performance since match
doesn't need to be executed on every render and instead just on every go
. I'd not be surprised if this gives us -0.1
or more relative performance. (in addition to being less hacky and being easier to use)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure I understand what you are saying 🤔.
Shouldn't params be in the model now?
Not sure about this. location
is in the model not because it's the only way for the Router to control when the view is rendered without using a hack like passing a render
function as we were doing before.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From #120:
model.router = {
matched: '/some/route/:id',
url: '/some/route/34593458',
params: {
id: 34593458
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it makes more sense and it should also be faster, since right now we call match
every time the model is updated. That function looks quite expensive. I propose that go
also tells setLocation
the match()
results so that it can be added to params.
This also has the advantage that actions may become easier since they can access the route params.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dodekeract Ah, I think I see what you are saying now! It doesn't make sense to call match every time the view is rendered 👍.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer:
params.id
// or
(model, actions, { id }) -> id
over
model.router.params.id
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But, I was able to extract the match functionality outside onRender.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jbucaran Your syntax proposal is compatible with mine. You can just call the render function with model, actions, model.router.params
.
src/app.js
Outdated
} | ||
|
||
if (app.hooks) { | ||
hooks.push(app.hooks) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of pushing objects into an array I would iterate through the objects and construct this data-structure:
var hooks = {
onRender: [f1, f2, f3],
onError: [f4]
}
This has the advantage that access is fast and easy. Basically it should be handled the same as subscriptions
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dodekeract Can you provide some pseudocode?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
var hooks = {
onRender: []
}
function addToHooks(hook, name) {
hooks[name].push(hook)
}
function handlePluginHooks (hooks) {
for (var name in hooks) {
addToHooks(hooks[name], name)
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems more verbose. Specially if more hooks are added in the future. I'll see if there are any performance gains! 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There will be performance gains. https://jsperf.com/for-in-or-for
1,400,835,640
vs 43,479
for me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you talking about for-in used with arrays?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, yes. Hard to keep track of 10 conversations at the same time.
src/app.js
Outdated
} | ||
|
||
function hook(name, fn) { | ||
for (var key in hooks) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't use for in
for array iteration. It's slow and potentially buggy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/app.js
Outdated
|
||
domContentLoaded(function () { | ||
root = options.root || document.body.appendChild(document.createElement("div")) | ||
for (var key in plugins) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't use for ... in ...
for array iteration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On the other hand for loop is more verbose and we'd need to check if it's empty or not with .length. I'll experiment with for
and see how that affects size/performance. 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Size might be more, but for in
is basically a hack when used on arrays.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just out of curiosity how come for ... in
is being preferred over forEach
or map
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lukejacksonn for in is used to loop through props of an object. This can be used to traverse an array too, but it's a "hack". The best practice to traverse arrays is using a regular for loop or forEach.
I've changed them all to use a regular for loop, so say bye to for in! :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉
src/app.js
Outdated
subs[key](model, actions, hooks.onError) | ||
render(model, view) | ||
|
||
for (var key in subscriptions) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for ... in
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/app.js
Outdated
@@ -53,32 +52,72 @@ export default function (options) { | |||
|
|||
if (typeof action === "function") { | |||
container[key] = function (data) { | |||
hooks.onAction(name, data) | |||
hook("onAction", function (fn) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if you implement the data structure I suggested this would be
hooks.onAction.forEach(hook => hook(name, data))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, this sounds good. Let me see how I can do that.
@@ -1,5 +1,5 @@ | |||
import h from "./h" | |||
import app from "./app" | |||
import router from "./router" | |||
import Router from "./router" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this TitleCase? It's not a class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not a class.
I don't care about that convention. I think it sucks to limit myself to a convention for a construct I rarely use in JavaScript anyway. I don't use Classes/Prototypes.
Router is more distinctive. It's not a component, like in React, but they also use Router, which is familiar.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jbucaran the thing is that it implies that router
has to be used like new Router()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't use new or this; as an extension, neither does HyperApp. So, why should Router imply that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because it's TitleCase. For someone familiar with javascript (and not that much with hyperapp's source-code) that usually means it has to be instantiated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Plugins written in title case look better, just like Components.
app({
plugins: [
Router,
Logger,
TimeDebugger,
FusionGenerator,
FluxCapacitor
]
})
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Router could be a plugin but that doesn't mean it has to be. If it is then it negates the R
vs r
issue. The awkward thing about the router is that it is not needed for all cases, but in reality it is needed for a lot of cases and if it is not needed (they only have one view/route) then hyperapp already has that covered with very little redundant bytes. Good frontend routers are hard to come by. I think this..
model.router = {
matched: '/some/route/:id',
url: '/some/route/34593458',
params: {
id: 34593458
}
}
with an onrender
onRender
hook, feels like a very robust approach. I'd say for the sakes of argument.. abstract the router out, use it as an example of a really useful plugin (npm i hyperapp-router -S
is a good practice for developers to get into anyway if we are encouraging a plugin ecosystem) and let people call it what they want.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @lukejacksonn. Notice the correct name is onRender
. Hooks are application level events so we use camelCase. DOM events are lowercase so we use oncreate, onupdate, onremove for lifecycle methods. Just for the record and in case someone finds this in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For what it's worth, the convention in Hapi is to capitalize anything you 'require'. It makes it more clear which variables are from an external file, and I agree with @jbucaran that it looks nicer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good to know! Components are also capitalized in HyperApp, but they are not classes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Other than the for ... in ...
it's good.
Since we use gzip I don't think there is any meaningful file-size difference in using the proper array iteration. There is a huge performance difference though.
src/app.js
Outdated
})) { | ||
throw err | ||
var onError = function (error) { | ||
for (var hookKey in hooks.onError) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for in
😢
src/app.js
Outdated
hook("onAction", function (fn) { | ||
fn(name, data) | ||
}) | ||
for (var hookKey in hooks.onAction) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😢
src/app.js
Outdated
hook("onUpdate", function (fn) { | ||
fn(oldModel, model, data) | ||
}) | ||
for (var hookKey in hooks.onUpdate) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😢
src/app.js
Outdated
hook("onRender", function (fn) { | ||
view = fn(model, view) | ||
}) | ||
for (var i in hooks.onRender) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😢
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dodekeract Using for in here is the easiest way to avoid checking if onRender is undefined.
src/app.js
Outdated
|
||
domContentLoaded(function () { | ||
root = options.root || document.body.appendChild(document.createElement("div")) | ||
for (var key in plugins) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just out of curiosity how come for ... in
is being preferred over forEach
or map
@dodekeract Thanks for the comments. I removed several instances of for..in in favor of a for loop, updated the hooks system as suggested and revamped the router. |
[*.{diff,md}] | ||
trim_trailing_whitespace = true | ||
|
||
[*.json] | ||
indent_style = space | ||
indent_size = 2 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❤️
Introduce plugin architecture.
Plugins are used like this:
In a nutshell, if you know how to make an app with HyperApp you already know how to make a plugin.
A plugin is a function that exports an object with the same props as the object you pass into
app(options)
(except views, plugins don't export views at the moment, but you can listen to the onRender hook to alter this process, see Router).For example, a plugin that adds a new subscription to your app:
A plugin function is called by HyperApp during your app's init step (before rendering a view). The
app
argument is the same options object you pass intoapp(options)
. Plugins don't need to modify this object, instead they return an object that is merged back into your app's options.Since this is JavaScript, plugins can totally modify this object's properties, so they have both the power to bring joy and devastation to the world.
I'll be updating the docs later, once this ships.
Also on this branch:
h
.