Building wheels -- implement a data-driven router according to the principle of Vue Router

Let's create a data-driven router as follows:

new Router({
  id: 'router-view', // Container view
  mode: 'hash', // pattern
  routes: [
    {
      path: '/',
      name: 'home',
      component: '<div>Home</div>',
      beforeEnter: (next) => {
        console.log('before enter home')
        next()
      },
      afterEnter: (next) => {
        console.log('enter home')
        next()
      },
      beforeLeave: (next) => {
        console.log('start leave home')
        next()
      }
    },
    {
      path: '/bar',
      name: 'bar',
      component: '<div>Bar</div>',
      beforeEnter: (next) => {
        console.log('before enter bar')
        next()
      },
      afterEnter: (next) => {
        console.log('enter bar')
        next()
      },
      beforeLeave: (next) => {
        console.log('start leave bar')
        next()
      }
    },
    {
      path: '/foo',
      name: 'foo',
      component: '<div>Foo</div>'
    }
  ]
})

Thinking arrangement

The first is data driven, so we can express the current routing state through a route object, such as:

current = {
    path: '/', // route
    query: {}, // query
    params: {}, // params
    name: '', // Route name
    fullPath: '/', // Full path
    route: {} // Record current route properties
}

current.route stores the configuration information of the current route, so we only need to listen to current Dynamically render the page according to the change of route.

Then we need to monitor different routing changes and do corresponding processing. And implement hash and history mode.

Data driven

Here we continue to use vue data-driven model to realize a simple data hijacking and update the view. First, define our observer

class Observer {
  constructor (value) {
    this.walk(value)
  }

  walk (obj) {
    Object.keys(obj).forEach((key) => {
      // If it is an object, call walk recursively to ensure that each attribute can be defineReactive
      if (typeof obj[key] === 'object') {
        this.walk(obj[key])
      }
      defineReactive(obj, key, obj[key])
    })
  }
}

function defineReactive(obj, key, value) {
  let dep = new Dep()
  Object.defineProperty(obj, key, {
    get: () => {
      if (Dep.target) {
        // Dependency collection
        dep.add()
      }
      return value
    },
    set: (newValue) => {
      value = newValue
      // Notification update, corresponding update view
      dep.notify()
    }
  })
}

export function observer(value) {
  return new Observer(value)
}

Next, we need to define Dep and Watcher:

export class Dep {
  constructor () {
    this.deppend = []
  }
  add () {
    // Collect watcher
    this.deppend.push(Dep.target)
  }
  notify () {
    this.deppend.forEach((target) => {
      // Call the update function of the watcher
      target.update()
    })
  }
}

Dep.target = null

export function setTarget (target) {
  Dep.target = target
}

export function cleanTarget() {
  Dep.target = null
}

// Watcher
export class Watcher {
  constructor (vm, expression, callback) {
    this.vm = vm
    this.callbacks = []
    this.expression = expression
    this.callbacks.push(callback)
    this.value = this.getVal()

  }
  getVal () {
    setTarget(this)
    // Trigger the get method to complete the collection of watcher s
    let val = this.vm
    this.expression.split('.').forEach((key) => {
      val = val[key]
    })
    cleanTarget()
    return val
  }

  // Update action
  update () {
    this.callbacks.forEach((cb) => {
      cb()
    })
  }
}

So far, we have implemented a simple subscription publisher, so we need to update current Route does data hijacking. Once current Route update, we can update the current page in time:

  // Responsive data hijacking
  observer(this.current)

  // Yes, current The route object collects dependencies and updates them through render when changes occur
  new Watcher(this.current, 'route', this.render.bind(this))

okay.... So far, we seem to have completed a simple responsive data update. In fact, render is to dynamically render the corresponding content for the specified area of the page. Here is only a simplified version of render:

 render() {
    let i
    if ((i = this.history.current) && (i = i.route) && (i = i.component)) {
      document.getElementById(this.container).innerHTML = i
    }
  }

hash and history

Next is the implementation of hash and history mode. Here we can follow the idea of Vue router and establish different processing models. Take a look at the core code I implemented:

this.history = this.mode === 'history' ? 
new HTML5History(this) : 
new HashHistory(this)

When the page changes, we only need to listen to hashchange and pop state events and make routing transition to:

  /**
   * Route conversion
   * @param target Target path
   * @param cb Callback after success
   */
  transitionTo(target, cb) {
    // Obtain the matched targetRoute object by comparing the incoming routes
    const targetRoute = match(target, this.router.routes)
    this.confirmTransition(targetRoute, () => {
      // The view update is triggered here
      this.current.route = targetRoute
      this.current.name = targetRoute.name
      this.current.path = targetRoute.path
      this.current.query = targetRoute.query || getQuery()
      this.current.fullPath = getFullPath(this.current)
      cb && cb()
    })
  }

  /**
   * Confirm jump
   * @param route
   * @param cb
   */
  confirmTransition (route, cb) {
    // Hook function execution queue
    let queue = [].concat(
      this.router.beforeEach,
      this.current.route.beforeLeave,
      route.beforeEnter,
      route.afterEnter
    )
    
    // Scheduling execution through step
    let i = -1
    const step = () => {
      i ++
      if (i > queue.length) {
        cb()
      } else if (queue[i]) {
        queue[i](step)
      } else {
        step()
      }

    }
    step(i)
  }
}

In this way, on the one hand, we pass this current. Route = targetroute updates the previously hijacked data to update the view. On the other hand, through the scheduling of task queue, we realize the basic hook functions beforeEach, beforeLeave, beforeEnter and afterEnter.
In fact, it's almost here. Next, let's implement several API s:

  /**
   * Jump to add history
   * @param location 
   * @example this.push({name: 'home'})
   * @example this.push('/')
   */
  push (location) {
    const targetRoute = match(location, this.router.routes)

    this.transitionTo(targetRoute, () => {
      changeUrl(this.router.base, this.current.fullPath)
    })
  }

  /**
   * Jump to add history
   * @param location
   * @example this.replaceState({name: 'home'})
   * @example this.replaceState('/')
   */
  replaceState(location) {
    const targetRoute = match(location, this.router.routes)

    this.transitionTo(targetRoute, () => {
      changeUrl(this.router.base, this.current.fullPath, true)
    })
  }

  go (n) {
    window.history.go(n)
  }

  function changeUrl(path, replace) {
    const href = window.location.href
    const i = href.indexOf('#')
    const base = i >= 0 ? href.slice(0, i) : href
    if (replace) {
      window.history.replaceState({}, '', `${base}#/${path}`)
    } else {
      window.history.pushState({}, '', `${base}#/${path}`)
    }
  }

It's almost over here. Source address

 

 

 

 

 

 

Keywords: Vue

Added by jonker on Mon, 07 Feb 2022 23:40:12 +0200