From 30da0d5d466a0d6617fc3312698d9b58899dfa16 Mon Sep 17 00:00:00 2001 From: "qingwei.li" Date: Sat, 18 Feb 2017 11:41:33 +0800 Subject: [PATCH] refactor(core): add router --- src/core/config.js | 4 +- src/core/event/index.js | 4 +- src/core/event/sidebar.js | 4 +- src/core/fetch/ajax.js | 31 +++++++++++---- src/core/fetch/index.js | 36 +++++++++++++++-- src/core/global-api.js | 13 +++++++ src/core/index.js | 8 +--- src/core/render/compiler.js | 50 ++++++++++++++++++++++++ src/core/render/index.js | 20 ++++++++-- src/core/render/progressbar.js | 21 +++++----- src/core/route/hash.js | 62 +++++++++++++++++++++++++----- src/core/route/index.js | 43 ++++++++++++++++----- src/core/route/util.js | 46 +++++++++++++++++++--- src/core/util/core.js | 9 ++--- src/core/util/dom.js | 34 ++++++++++------ src/core/util/index.js | 1 - src/core/util/polyfill/css-vars.js | 18 ++++----- 17 files changed, 316 insertions(+), 88 deletions(-) create mode 100644 src/core/global-api.js create mode 100644 src/core/render/compiler.js diff --git a/src/core/config.js b/src/core/config.js index d11c3d1..a1bf9bb 100644 --- a/src/core/config.js +++ b/src/core/config.js @@ -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 diff --git a/src/core/event/index.js b/src/core/event/index.js index a31188b..4c44100 100644 --- a/src/core/event/index.js +++ b/src/core/event/index.js @@ -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') } } diff --git a/src/core/event/sidebar.js b/src/core/event/sidebar.js index aeaeeed..a02ef7b 100644 --- a/src/core/event/sidebar.js +++ b/src/core/event/sidebar.js @@ -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) diff --git a/src/core/fetch/ajax.js b/src/core/fetch/ajax.js index ba79b76..09d4390 100644 --- a/src/core/fetch/ajax.js +++ b/src/core/fetch/ajax.js @@ -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() } } diff --git a/src/core/fetch/index.js b/src/core/fetch/index.js index 61acc81..2fb7aa0 100644 --- a/src/core/fetch/index.js +++ b/src/core/fetch/index.js @@ -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') }) diff --git a/src/core/global-api.js b/src/core/global-api.js new file mode 100644 index 0000000..83a3288 --- /dev/null +++ b/src/core/global-api.js @@ -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 +} diff --git a/src/core/index.js b/src/core/index.js index 96cab23..e2c94a4 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -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 diff --git a/src/core/render/compiler.js b/src/core/render/compiler.js new file mode 100644 index 0000000..daed38c --- /dev/null +++ b/src/core/render/compiler.js @@ -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 `${text}` +} +// highlight code +renderer.code = function (code, lang = '') { + const hl = Prism.highlight(code, Prism.languages[lang] || Prism.languages.markup) + + return `
${hl}
` +} +renderer.link = function (href, title, text) { + if (!/:|(\/{2})/.test(href)) { + href = `#/${href}`.replace(/\/+/g, '/') + } + return `${text}` +} +renderer.paragraph = function (text) { + if (/^!>/.test(text)) { + return tpl.helper('tip', text) + } else if (/^\?>/.test(text)) { + return tpl.helper('warn', text) + } + return `

${text}

` +} +renderer.image = function (href, title, text) { + const url = /:|(\/{2})/.test(href) ? href : ($docsify.basePath + href).replace(/\/+/g, '/') + const titleHTML = title ? ` title="${title}"` : '' + + return `${text}` +} diff --git a/src/core/render/index.js b/src/core/render/index.js index 12f50c5..2f9610f 100644 --- a/src/core/render/index.js +++ b/src/core/render/index.js @@ -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) } diff --git a/src/core/render/progressbar.js b/src/core/render/progressbar.js index 8eff66b..c8347e6 100644 --- a/src/core/render/progressbar.js +++ b/src/core/render/progressbar.js @@ -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) } } diff --git a/src/core/route/hash.js b/src/core/route/hash.js index 1f544d4..fd0607e 100644 --- a/src/core/route/hash.js +++ b/src/core/route/hash.js @@ -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) { + +} diff --git a/src/core/route/index.js b/src/core/route/index.js index 011db74..0d4a3c6 100644 --- a/src/core/route/index.js +++ b/src/core/route/index.js @@ -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() }) } diff --git a/src/core/route/util.js b/src/core/route/util.js index 4cbf898..300253a 100644 --- a/src/core/route/util.js +++ b/src/core/route/util.js @@ -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] } diff --git a/src/core/util/core.js b/src/core/util/core.js index 9688983..5a153b8 100644 --- a/src/core/util/core.js +++ b/src/core/util/core.js @@ -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()) }) /** diff --git a/src/core/util/dom.js b/src/core/util/dom.js index 2232219..a2bd22c 100644 --- a/src/core/util/dom.js +++ b/src/core/util/dom.js @@ -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) { diff --git a/src/core/util/index.js b/src/core/util/index.js index 38687ba..bfcc8b2 100644 --- a/src/core/util/index.js +++ b/src/core/util/index.js @@ -1,3 +1,2 @@ export * from './core' export * from './env' -export * from './dom' diff --git a/src/core/util/polyfill/css-vars.js b/src/core/util/polyfill/css-vars.js index 686ad72..bb8e498 100644 --- a/src/core/util/polyfill/css-vars.js +++ b/src/core/util/polyfill/css-vars.js @@ -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) }) } })