Deservicify core (#2458)

* De-servicify router (mostly)

Still uses the redraw service, but it no longer has an intermediate
service of its own.

Also, did a *lot* of test deduplication in this. About 30-40% of the
router service tests were already tested on the main router API instance
itself.

Bundle size decreased from 9560 to 9548 bytes min+gzip.

* Merge `m.mount` + `m.redraw`, update router

Simplifies the router and redraw mechanism, and makes it much easier to
keep predictable.

Bundle size down to 9433 bytes min+gzip, docs updated accordingly.

* Make `mithril/render` just return the `m.render` function directly.

* Deservicify `m.render`, revise `m.route`

- You now have to use `mithril/render/render` directly if you want an
  implicit redraw function. (This will likely be going away in v3.)
- Revise `m.route` to only `key` components

* Add `redraw` to `m.render`, deservicify requests

* Test error logging

* Update docs + changelog [skip ci]
This commit is contained in:
Isiah Meadows 2019-07-07 18:28:43 -04:00 committed by GitHub
parent db277217f8
commit 1f4b2cf49a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
60 changed files with 1212 additions and 1393 deletions

View file

@ -2,55 +2,153 @@
var Vnode = require("../render/vnode")
var Promise = require("../promise/promise")
var coreRouter = require("../router/router")
var buildPathname = require("../pathname/build")
var parsePathname = require("../pathname/parse")
var compileTemplate = require("../pathname/compileTemplate")
var assign = require("../pathname/assign")
var sentinel = {}
module.exports = function($window, redrawService) {
var routeService = coreRouter($window)
module.exports = function($window, mountRedraw) {
var callAsync = typeof setImmediate === "function" ? setImmediate : setTimeout
var supportsPushState = typeof $window.history.pushState === "function"
var routePrefix = "#!"
var fireAsync
function setPath(path, data, options) {
path = buildPathname(path, data)
if (fireAsync != null) {
fireAsync()
var state = options ? options.state : null
var title = options ? options.title : null
if (options && options.replace) $window.history.replaceState(state, title, routePrefix + path)
else $window.history.pushState(state, title, routePrefix + path)
}
else {
$window.location.href = routePrefix + path
}
}
var currentResolver = sentinel, component, attrs, currentPath, lastUpdate
var route = function(root, defaultRoute, routes) {
if (root == null) throw new Error("Ensure the DOM element that was passed to `m.route` is not undefined")
var init = false
var bail = function(path) {
if (path !== defaultRoute) routeService.setPath(defaultRoute, null, {replace: true})
else throw new Error("Could not resolve default route " + defaultRoute)
// 0 = start
// 1 = init
// 2 = ready
var state = 0
var compiled = Object.keys(routes).map(function(route) {
if (route[0] !== "/") throw new SyntaxError("Routes must start with a `/`")
if ((/:([^\/\.-]+)(\.{3})?:/).test(route)) {
throw new SyntaxError("Route parameter names must be separated with either `/`, `.`, or `-`")
}
return {
route: route,
component: routes[route],
check: compileTemplate(route),
}
})
var onremove, asyncId
fireAsync = null
if (defaultRoute != null) {
var defaultData = parsePathname(defaultRoute)
if (!compiled.some(function (i) { return i.check(defaultData) })) {
throw new ReferenceError("Default route doesn't match any known routes")
}
}
function run() {
init = true
if (sentinel !== currentResolver) {
var vnode = Vnode(component, attrs.key, attrs)
if (currentResolver) vnode = currentResolver.render(vnode)
function resolveRoute() {
// Consider the pathname holistically. The prefix might even be invalid,
// but that's not our problem.
var prefix = $window.location.hash
if (routePrefix[0] !== "#") {
prefix = $window.location.search + prefix
if (routePrefix[0] !== "?") {
prefix = $window.location.pathname + prefix
if (prefix[0] !== "/") prefix = "/" + prefix
}
}
// This seemingly useless `.concat()` speeds up the tests quite a bit,
// since the representation is consistently a relatively poorly
// optimized cons string.
var path = prefix.concat()
.replace(/(?:%[a-f89][a-f0-9])+/gim, decodeURIComponent)
.slice(routePrefix.length)
var data = parsePathname(path)
assign(data.params, $window.history.state)
for (var i = 0; i < compiled.length; i++) {
if (compiled[i].check(data)) {
var payload = compiled[i].component
var route = compiled[i].route
var update = lastUpdate = function(routeResolver, comp) {
if (update !== lastUpdate) return
component = comp != null && (typeof comp.view === "function" || typeof comp === "function")? comp : "div"
attrs = data.params, currentPath = path, lastUpdate = null
currentResolver = routeResolver.render ? routeResolver : null
if (state === 2) mountRedraw.redraw()
else {
state = 2
mountRedraw.redraw.sync()
}
}
if (payload.view || typeof payload === "function") update({}, payload)
else {
if (payload.onmatch) {
Promise.resolve(payload.onmatch(data.params, path, route)).then(function(resolved) {
update(payload, resolved)
}, function () {
if (path === defaultRoute) throw new Error("Could not resolve default route " + defaultRoute)
setPath(defaultRoute, null, {replace: true})
})
}
else update(payload, "div")
}
return
}
}
if (path === defaultRoute) throw new Error("Could not resolve default route " + defaultRoute)
setPath(defaultRoute, null, {replace: true})
}
if (supportsPushState) {
onremove = function() {
$window.removeEventListener("popstate", fireAsync, false)
}
$window.addEventListener("popstate", fireAsync = function() {
if (asyncId) return
asyncId = callAsync(function() {
asyncId = null
resolveRoute()
})
}, false)
} else if (routePrefix[0] === "#") {
onremove = function() {
$window.removeEventListener("hashchange", resolveRoute, false)
}
$window.addEventListener("hashchange", resolveRoute, false)
}
return mountRedraw.mount(root, {
onbeforeupdate: function() {
state = state ? 2 : 1
return !(!state || sentinel === currentResolver)
},
oncreate: resolveRoute,
onremove: onremove,
view: function() {
if (!state || sentinel === currentResolver) return
// Wrap in a fragment to preserve existing key semantics
var vnode = [Vnode(component, attrs.key, attrs)]
if (currentResolver) vnode = currentResolver.render(vnode[0])
return vnode
}
}
routeService.defineRoutes(routes, function(payload, params, path, route) {
var update = lastUpdate = function(routeResolver, comp) {
if (update !== lastUpdate) return
component = comp != null && (typeof comp.view === "function" || typeof comp === "function")? comp : "div"
attrs = params, currentPath = path, lastUpdate = null
currentResolver = routeResolver.render ? routeResolver : null
if (init) redrawService.redraw()
else {
init = true
redrawService.redraw.sync()
}
}
if (payload.view || typeof payload === "function") update({}, payload)
else {
if (payload.onmatch) {
Promise.resolve(payload.onmatch(params, path, route)).then(function(resolved) {
update(payload, resolved)
}, function () { bail(path) })
}
else update(payload, "div")
}
}, bail, defaultRoute, function (unsubscribe) {
redrawService.subscribe(root, function(sub) {
sub.c = run
return sub
}, unsubscribe)
},
})
}
route.set = function(path, data, options) {
@ -59,18 +157,18 @@ module.exports = function($window, redrawService) {
options.replace = true
}
lastUpdate = null
routeService.setPath(path, data, options)
setPath(path, data, options)
}
route.get = function() {return currentPath}
route.prefix = function(prefix) {routeService.prefix = prefix}
route.prefix = function(prefix) {routePrefix = prefix}
var link = function(options, vnode) {
vnode.dom.setAttribute("href", routeService.prefix + vnode.attrs.href)
vnode.dom.setAttribute("href", routePrefix + vnode.attrs.href)
vnode.dom.onclick = function(e) {
if (e.ctrlKey || e.metaKey || e.shiftKey || e.which === 2) return
e.preventDefault()
e.redraw = false
var href = this.getAttribute("href")
if (href.indexOf(routeService.prefix) === 0) href = href.slice(routeService.prefix.length)
if (href.indexOf(routePrefix) === 0) href = href.slice(routePrefix.length)
route.set(href, undefined, options)
}
}