refactor(core): add router

This commit is contained in:
qingwei.li 2017-02-18 11:41:33 +08:00 committed by cinwell.li
commit 30da0d5d46
17 changed files with 316 additions and 88 deletions

View file

@ -1,4 +1,4 @@
import { merge, camelize, isPrimitive } from './util/core'
import { merge, hyphenate, isPrimitive } from './util/core'
const config = merge({
el: '#app',
@ -23,7 +23,7 @@ const script = document.currentScript ||
if (script) {
for (const prop in config) {
const val = script.getAttribute('data-' + camelize(prop))
const val = script.getAttribute('data-' + hyphenate(prop))
if (isPrimitive(val)) {
config[prop] = val === '' ? true : val

View file

@ -1,5 +1,5 @@
import { isMobile } from '../util/env'
import { dom, on } from '../util/dom'
import { body, on } from '../util/dom'
import * as sidebar from './sidebar'
export function eventMixin (Docsify) {
@ -14,6 +14,6 @@ export function initEvent (vm) {
if (vm.config.coverpage) {
!isMobile && on('scroll', sidebar.sticky)
} else {
dom.body.classList.add('sticky')
body.classList.add('sticky')
}
}

View file

@ -1,10 +1,10 @@
import { isMobile } from '../util/env'
import { getNode, on, dom } from '../util/dom'
import { getNode, on, body } from '../util/dom'
/**
* Toggle button
*/
export function btn (el) {
const toggle = () => dom.body.classList.toggle('close')
const toggle = () => body.classList.toggle('close')
el = getNode(el)
on(el, 'click', toggle)

View file

@ -1,23 +1,33 @@
import progressbar from '../render/progressbar'
import { noop } from '../util/core'
const cache = {}
const RUN_VERSION = Date.now()
/**
* Simple ajax get
* @param {String} url
* @param {Boolean} [loading=false] has loading bar
* @param {string} url
* @param {boolean} [hasBar=false] has progress bar
* @return { then(resolve, reject), abort }
*/
export function get (url, hasLoading = false) {
export function get (url, hasBar = false) {
const xhr = new XMLHttpRequest()
const on = function () {
xhr.addEventListener.apply(xhr, arguments)
}
url += (/\?(\w+)=/g.test(url) ? '&' : '?') + `v=${RUN_VERSION}`
if (cache[url]) {
return { then: cb => cb(cache[url]), abort: noop }
}
xhr.open('GET', url)
xhr.send()
return {
then: function (success, error = noop) {
const on = xhr.addEventListener
if (hasLoading) {
if (hasBar) {
const id = setInterval(_ => progressbar({}), 500)
on('progress', progressbar)
@ -29,9 +39,14 @@ export function get (url, hasLoading = false) {
on('error', error)
on('load', ({ target }) => {
target.status >= 400 ? error(target) : success(target.response)
if (target.status >= 400) {
error(target)
} else {
cache[url] = target.response
success(target.response)
}
})
},
abort: () => xhr.readyState !== 4 && xhr.abort()
abort: _ => xhr.readyState !== 4 && xhr.abort()
}
}

View file

@ -1,13 +1,43 @@
import { get } from './ajax'
import { callHook } from '../init/lifecycle'
import { getCurrentRoot } from '../route/util'
export function fetchMixin (Docsify) {
Docsify.prototype.$fetch = function (path) {
// 加载侧边栏、导航、内容
let last
Docsify.prototype._fetch = function (cb) {
const { path } = this.route
const { loadNavbar, loadSidebar } = this.config
const currentRoot = getCurrentRoot(path)
// Abort last request
last && last.abort && last.abort()
last = get(this.$getFile(path), true)
last.then(text => {
this._renderMain(text)
if (!loadSidebar) return cb()
const fn = result => { this._renderSidebar(result); cb() }
// Load sidebar
get(this.$getFile(currentRoot + loadSidebar))
.then(fn, _ => get(loadSidebar).then(fn))
},
_ => this._renderMain(null))
// Load nav
loadNavbar &&
get(this.$getFile(currentRoot + loadNavbar))
.then(
this._renderNav,
_ => get(loadNavbar).then(this._renderNav)
)
}
}
export function initFetch (vm) {
vm.$fetch(result => {
vm._fetch(result => {
vm.$resetEvents()
callHook(vm, 'doneEach')
})

13
src/core/global-api.js Normal file
View file

@ -0,0 +1,13 @@
import * as util from './util'
import * as dom from './util/dom'
import * as render from './render/compiler'
import * as route from './route/util'
import { get } from './fetch/ajax'
import marked from 'marked'
import prism from 'prismjs'
export default function () {
window.Docsify = { util, dom, render, route, get }
window.marked = marked
window.Prism = prism
}

View file

@ -3,9 +3,7 @@ import { routeMixin } from './route'
import { renderMixin } from './render'
import { fetchMixin } from './fetch'
import { eventMixin } from './event'
import * as util from './util'
import { get as load } from './fetch/ajax'
import * as routeUtil from './route/util'
import initGlobalAPI from './global-api'
function Docsify () {
this._init()
@ -20,9 +18,7 @@ eventMixin(Docsify)
/**
* Global API
*/
window.Docsify = {
util: util.merge({ load }, util, routeUtil)
}
initGlobalAPI()
/**
* Run Docsify

View file

@ -0,0 +1,50 @@
import marked from 'marked'
import Prism from 'prismjs'
export const renderer = new marked.Renderer()
export function markdown () {
}
const toc = []
/**
* render anchor tag
* @link https://github.com/chjj/marked#overriding-renderer-methods
*/
renderer.heading = function (text, level) {
const slug = slugify(text)
let route = ''
route = `#/${getRoute()}`
toc.push({ level, slug: `${route}#${encodeURIComponent(slug)}`, title: text })
return `<h${level} id="${slug}"><a href="${route}#${slug}" data-id="${slug}" class="anchor"><span>${text}</span></a></h${level}>`
}
// highlight code
renderer.code = function (code, lang = '') {
const hl = Prism.highlight(code, Prism.languages[lang] || Prism.languages.markup)
return `<pre v-pre data-lang="${lang}"><code class="lang-${lang}">${hl}</code></pre>`
}
renderer.link = function (href, title, text) {
if (!/:|(\/{2})/.test(href)) {
href = `#/${href}`.replace(/\/+/g, '/')
}
return `<a href="${href}" title="${title || ''}">${text}</a>`
}
renderer.paragraph = function (text) {
if (/^!&gt;/.test(text)) {
return tpl.helper('tip', text)
} else if (/^\?&gt;/.test(text)) {
return tpl.helper('warn', text)
}
return `<p>${text}</p>`
}
renderer.image = function (href, title, text) {
const url = /:|(\/{2})/.test(href) ? href : ($docsify.basePath + href).replace(/\/+/g, '/')
const titleHTML = title ? ` title="${title}"` : ''
return `<img src="${url}" alt="${text}"${titleHTML} />`
}

View file

@ -1,12 +1,26 @@
import { getNode, dom } from '../util/dom'
import * as dom from '../util/dom'
import cssVars from '../util/polyfill/css-vars'
import * as tpl from './tpl'
function renderMain () {
}
function renderNav () {
}
function renderSidebar () {
}
export function renderMixin (Docsify) {
Docsify.prototype._renderTo = function (el, content, replace) {
const node = getNode(el)
const node = dom.getNode(el)
if (node) node[replace ? 'outerHTML' : 'innerHTML'] = content
}
Docsify.prototype._renderSidebar = renderSidebar
Docsify.prototype._renderNav = renderNav
Docsify.prototype._renderMain = renderMain
}
export function initRender (vm) {
@ -40,7 +54,7 @@ export function initRender (vm) {
dom.body.insertBefore(navEl, dom.body.children[0])
if (config.themeColor) {
dom.head += tpl.theme(config.themeColor)
dom.$.head += tpl.theme(config.themeColor)
// Polyfll
cssVars(config.themeColor)
}

View file

@ -1,19 +1,18 @@
import { dom } from '../util/dom'
import * as dom from '../util/dom'
import { isPrimitive } from '../util/core'
let loadingEl
let barEl
let timeId
/**
* Init progress component
*/
function init () {
if (loadingEl) return
const div = dom.create('div')
div.classList.add('progress')
dom.appendTo(div, dom.body)
loadingEl = div
dom.appendTo(dom.body, div)
barEl = div
}
/**
* Render progress bar
@ -21,26 +20,26 @@ function init () {
export default function ({ loaded, total, step }) {
let num
loadingEl = init()
!barEl && init()
if (!isPrimitive(step)) {
step = Math.floor(Math.random() * 5 + 1)
}
if (step) {
num = parseInt(loadingEl.style.width, 10) + step
num = parseInt(barEl.style.width, 10) + step
num = num > 80 ? 80 : num
} else {
num = Math.floor(loaded / total * 100)
}
loadingEl.style.opacity = 1
loadingEl.style.width = num >= 95 ? '100%' : num + '%'
barEl.style.opacity = 1
barEl.style.width = num >= 95 ? '100%' : num + '%'
if (num >= 95) {
clearTimeout(timeId)
timeId = setTimeout(_ => {
loadingEl.style.opacity = 0
loadingEl.style.width = '0%'
barEl.style.opacity = 0
barEl.style.width = '0%'
}, 200)
}
}

View file

@ -1,7 +1,27 @@
// import { cleanPath, getLocation } from './util'
export function ensureSlash () {
const path = getHash()
if (path.charAt(0) === '/') return
import { parseQuery } from './util'
function replaceHash (path) {
const i = window.location.href.indexOf('#')
window.location.replace(
window.location.href.slice(0, i >= 0 ? i : 0) + '#' + path
)
}
/**
* Normalize the current url
*
* @example
* domain.com/docs/ => domain.com/docs/#/
* domain.com/docs/#/#slug => domain.com/docs/#/?id=slug
*/
export function normalize () {
let path = getHash()
path = path
.replace('#', '?id=')
.replace(/\?(\w+)=/g, (_, slug) => slug === 'id' ? '?id=' : `&${slug}=`)
if (path.charAt(0) === '/') return replaceHash(path)
replaceHash('/' + path)
}
@ -13,11 +33,33 @@ export function getHash () {
return index === -1 ? '' : href.slice(index + 1)
}
function replaceHash (path) {
const i = window.location.href.indexOf('#')
window.location.replace(
window.location.href.slice(0, i >= 0 ? i : 0) + '#' + path
)
/**
* Parse the current url
* @return {object} { path, query }
*/
export function parse () {
let path = window.location.href
let query = ''
const queryIndex = path.indexOf('?')
if (queryIndex >= 0) {
query = path.slice(queryIndex + 1)
path = path.slice(0, queryIndex)
}
const hashIndex = path.indexOf('#')
if (hashIndex) {
path = path.slice(hashIndex + 1)
}
return { path, query: parseQuery(query) }
}
// TODO 把第二个 hash 转成 ?id=
/**
* to URL
* @param {String} path
* @param {String} qs query string
*/
export function toURL (path, qs) {
}

View file

@ -1,17 +1,42 @@
import { ensureSlash } from './hash'
import { normalize, parse } from './hash'
import { getBasePath, cleanPath } from './util'
import { on } from '../util/dom'
function getAlias (path, alias) {
if (alias[path]) return getAlias(alias[path], alias)
return path
}
function getFileName (path) {
return /\.(md|html)$/g.test(path)
? path
: /\/$/g.test(path)
? `${path}README.md`
: `${path}.md`
}
export function routeMixin (Docsify) {
Docsify.prototype.$route = {
query: location.query || {},
path: location.path || '/',
base: ''
Docsify.prototype.route = {}
Docsify.prototype.$getFile = function (path) {
const { config } = this
const base = getBasePath(config.basePath)
path = getAlias(path, config.alias)
path = getFileName(path)
path = path === '/README.md' ? ('/' + config.homepage || path) : path
path = cleanPath(base + path)
return path
}
}
export function initRoute (vm) {
ensureSlash()
window.addEventListener('hashchange', () => {
ensureSlash()
vm.$fetch()
normalize()
vm.route = parse()
on('hashchange', _ => {
normalize()
vm.route = parse()
vm._fetch()
})
}

View file

@ -1,11 +1,45 @@
import { cached } from '../util/core'
const decode = decodeURIComponent
export const parseQuery = cached(query => {
const res = {}
query = query.trim().replace(/^(\?|#|&)/, '')
if (!query) {
return res
}
query.split('&').forEach(function (param) {
const parts = param.replace(/\+/g, ' ').split('=')
const key = decode(parts.shift())
const val = parts.length > 0
? decode(parts.join('='))
: null
if (res[key] === undefined) {
res[key] = val
} else if (Array.isArray(res[key])) {
res[key].push(val)
} else {
res[key] = [res[key], val]
}
})
return res
})
export function cleanPath (path) {
return path.replace(/\/+/g, '/')
}
export function getLocation (base) {
let path = window.location.pathname
if (base && path.indexOf(base) === 0) {
path = path.slice(base.length)
}
return (path || '/') + window.location.search + window.location.hash
export function getBasePath (base) {
return /^(\/|https?:)/g.test(base)
? base
: cleanPath(window.location.pathname + '/' + base)
}
export function getCurrentRoot (path) {
return /\/$/g.test(path) ? path : path.match(/(\S*\/)[^\/]+$/)[1]
}

View file

@ -1,7 +1,7 @@
/**
* Create a cached version of a pure function.
*/
function cached (fn) {
export function cached (fn) {
const cache = Object.create(null)
return function cachedFn (str) {
const hit = cache[str]
@ -10,11 +10,10 @@ function cached (fn) {
}
/**
* Camelize a hyphen-delimited string.
* Hyphenate a camelCase string.
*/
const camelizeRE = /-(\w)/g
export const camelize = cached((str) => {
return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '')
export const hyphenate = cached(str => {
return str.replace(/([A-Z])/g, m => '-' + m.toLowerCase())
})
/**

View file

@ -10,22 +10,34 @@ const cacheNode = {}
*/
export function getNode (el, noCache = false) {
if (typeof el === 'string') {
el = noCache ? dom.find(el) : (cacheNode[el] || dom.find(el))
el = noCache ? find(el) : (cacheNode[el] || find(el))
}
return el
}
export const dom = {
body: document.body,
head: document.head,
find: node => document.querySelector(node),
findAll: node => document.querySelectorAll(node),
create: (node, tpl) => {
node = document.createElement(node)
if (tpl) node.innerHTML = tpl
},
appendTo: (target, el) => target.appendChild(el)
export const $ = document
export const body = $.body
export const head = $.head
export function find (node) {
return $.querySelector(node)
}
export function findAll (node) {
return [].clice.call($.querySelectorAll(node))
}
export function create (node, tpl) {
node = $.createElement(node)
if (tpl) node.innerHTML = tpl
return node
}
export function appendTo (target, el) {
return target.appendChild(el)
}
export function on (el, type, handler) {

View file

@ -1,3 +1,2 @@
export * from './core'
export * from './env'
export * from './dom'

View file

@ -1,22 +1,22 @@
import { dom } from '../dom'
import * as dom from '../dom'
import { get } from '../../fetch/ajax'
function replaceVar (block, themeColor) {
function replaceVar (block, color) {
block.innerHTML = block.innerHTML
.replace(/var\(\s*--theme-color.*?\)/g, themeColor)
.replace(/var\(\s*--theme-color.*?\)/g, color)
}
export default function (themeColor) {
export default function (color) {
// Variable support
if (window.CSS
&& window.CSS.supports
&& window.CSS.supports('(--foo: red)')) return
if (window.CSS &&
window.CSS.supports &&
window.CSS.supports('(--v:red)')) return
const styleBlocks = dom.findAll('style:not(.inserted),link')
;[].forEach.call(styleBlocks, block => {
if (block.nodeName === 'STYLE') {
replaceVar(block, themeColor)
replaceVar(block, color)
} else if (block.nodeName === 'LINK') {
const href = block.getAttribute('href')
@ -26,7 +26,7 @@ export default function (themeColor) {
const style = dom.create('style', res)
dom.head.appendChild(style)
replaceVar(style, themeColor)
replaceVar(style, color)
})
}
})