Skip to content
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

Lazy routes support #62

Open
YannickDot opened this issue Nov 15, 2016 · 2 comments
Open

Lazy routes support #62

YannickDot opened this issue Nov 15, 2016 · 2 comments

Comments

@YannickDot
Copy link

YannickDot commented Nov 15, 2016

I have been playing around with choo last night and it made me dive into the internals of choo, sheet-router, and wayfarer!

Your modules have a pretty nice api and are small enough to be easy to be grokked, good job man 😊

I have seen that the version used in choo currently is 3.1.0 whereas the latest version is 4.1.1.

I'm a avid Webpack user and as I was playing around, I managed to add support for lazy-loading of views on the router.
I ended up modifying wayfarer and sheet-router for that purpose.

Based on version 3.1.0 (used in choo), I've added a parameter called createLazyRoute to the sheet-router constructor:

// from sheet-router

// fn(str, any[..], fn?, fn?) -> fn(str, any[..])
function sheetRouter (dft, createTree, createRoute, createLazyRoute) {
  createRoute = (createRoute ? createRoute(_createRoute) : _createRoute)
  createLazyRoute = (createLazyRoute ? createLazyRoute(_createLazyRoute) : _createLazyRoute)
  /* ... unchanged code ... */
}

/* ... unchanged code ... */

// register lazy route
function _createLazyRoute (route, inline, child) {
  const lazy = true
  if (!child) {
    child = inline
    inline = null
  }
  assert.equal(typeof route, 'string', 'route must be a string')
  assert.ok(child, 'child exists')

  route = route.replace(/^\//, '')
  return [ route, inline, child, lazy ] // <-- lazy flag to tell wayfarer to tell that he'll receive a Promise<view()> from the cb
}

I did a slight modification on wayfarer to handle Promise<view()> :

// from wayfarer
function on (route, cb, type) {
  /* ... unchanged code ... */

  if (cb && cb._wayfarer && cb._trie) {
    _trie.mount(route, cb._trie.trie)
  } else {
    const node = _trie.create(route)
    node.cb = cb
    if(type === "lazy") node.lazy = cb // <-- here is the lazy fn
  }

  return emit
}

function emit (route) {
   /* ... unchanged code ... */

  if (node && node.lazy) {
    args[0] = node.params
    return node.lazy().then(view => view.apply(null, args)) // <-- node.lazy() returns a Promise<view()>
  }

  if (node && node.cb) {
    args[0] = node.params
    return node.cb.apply(null, args)
  }

  /* ... unchanged code ... */
}

And in the end on the choo part, I can register lazy routes as is :

// from choo
function createLazyRoute (routeFn) {
  return function (route, inline, child) {
    const wrap_curry = (_route) => (_child) => wrap(_child, _route)
    if (typeof inline === 'function') {
      var newInline = () => inline(wrap_curry(route))
    }
    return routeFn(route, newInline, child)
  }

  function wrap (child, route) {
    const send = createSend('view: ' + route, true)
    return function chooWrap (params, state, resolveData) {
      const nwPrev = prev
      const nwState = prev = xtend(state, { params: params, resolveData: resolveData })
      if (opts.freeze !== false) Object.freeze(nwState)
      return child(nwState, nwPrev, send)
    }
  }
}

Now on the user side, registering lazy routes is done like that :

// Do not forget to wrap the view fn with chooWrap :-)
const lazyLoadedView = (wrap) => System.import('./dependency.js').then(module => wrap(module)) 

app.router((route, lazyRoute) => [ //<-- I can have a new parameter to register lazy routes
  route('/', view),
  lazyRoute('/lazy', lazyLoadedView)  //<-- Here is a lazy route
])

// That part is a necessary tradeoff if we want to have lazy routes ... :( It can be better I think.
app.start()
.then(domTree => document.querySelector("#choo").appendChild(domTree))

Here is the dependency.js file :

const html = require('choo/html')

const view = (state, prev, send) => {
  return html`
    <div>
      <h1>Hi I'm lazy-loaded !</h1>
    </div>`
}

module.exports = view

All of this was possible because of the route() arg needed to register routes on the sheetRouter tree.
I could create a lazyRoute()arg that has the same behavior as route() but for views wrapped in a Promise.

I've noticed that in v4.1.1, there is no more route() arg since this commit.

So here I am today :

  • I'd like to discuss with you guys if code-splitting support can be a valuable addition to sheet-router.
  • Is there a way to do code-splitting with browserify ? How ? (maybe with substack/factor-bundle ?)
  • How would be the best way to implement it with the new lispy-syntax ?

Cheers 😊

EDIT : I've added an example project if you want to have a more concrete look on what I did

https://github.com/YannickDot/lazy-routes-choo-example

@yoshuawuyts
Copy link
Owner

This is heaps cool; personally I'm not a fan of promises but having some way of doing this would be neat I reckon - perhaps doing it through a wrapRoutes hook would be the way to go?

I've never attempted code splitting before as it's a very late-stage optimization, but I believe factor-bundle is indeed the way to go. But you might be a better judge of if it's a good fit or not (:

@YannickDot
Copy link
Author

YannickDot commented Nov 16, 2016

Yeah, the same, I wish I had a way to do it without wrapping everything in a Promise, it makes me sad 🤕 I'll try to find an alternative.

What to you mean by a wrapRoutes hook ? You mean in the createRoute function ?

I've not that much experience with browserify but it is a nice occasion to try myself at it.


Oh by the way, I've managed to simplify the previous code.
Now I only have to patch wayfarer and the createRoute fn from choo.

// from choo
function createRoute (routeFn) {
  return function (route, inline, child) {
    var wrapped = inline
    if (typeof inline === 'function' && inline.lazy) {
      const wrap_lazy = (_route) => (_child) => wrap(_child, _route)
      wrapped = () => inline(wrap_lazy(route))
      wrapped.lazy = true
    }
    else if (typeof inline === 'function') {
      wrapped = wrap(inline, route)
    }
    return routeFn(route, wrapped, child)
  }

  function wrap (child, route) {
   // ... no changes
  }
}
// from wayfarer
function emit() {
  // ... unchanged code ..

  if (node && node.cb && node.cb.lazy) { 
    args[0] = node.params
    return node.cb().then(view => view.apply(null, args))
  }

  // ... unchanged code ...
}

Usage

const lazyLoadedView = (wrap) => System.import('./dependency.js').then(module => wrap(module.view))


app.router((route) => [
  route('/', view),
  route('/lazy', markAslazy(lazyLoadedView)) // <-- mark view cb as lazy view
])

app.start()
.then(domTree => document.querySelector("#choo").appendChild(domTree))

The markAslazy function is basically :

function lazy (fn) {
  fn.lazy = true
  return fn
}

I hope this would make a transition to v4 easier.

I keep updating this repo as I keep experimenting with it and when we have something satisfactory, I'll make a pull request to each package impacted by these changes 😉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants