SOURCE

console 命令行工具 X clear

                    
>
console
class MarkdownParser {
    compile(nodes) {
        let matchArr
        let html = ''
        for (let i = 0, len = nodes.length; i < len; i++) {
            let text = ''
            let index = 0
            const node = nodes[i]
            if (node.nodeType === Node.ELEMENT_NODE) {
                if (node.innerHTML === '<br>') {
                    // 多个空行只算一个
                    html += `<div></div>`
                    while (nodes[i + 1].nodeType === Node.ELEMENT_NODE && nodes[i + 1].innerHTML === '<br>') {
                        i++
                    }

                    continue
                }

                index = node.dataset.index
                text = node.textContent.trim()
            } else {
                text = node.nodeValue.trim()
            }

            matchArr = text.match(/^#\s/)
                        || text.match(/^##\s/)
                        || text.match(/^###\s/)
                        || text.match(/^####\s/)
                        || text.match(/^#####\s/)
                        || text.match(/^######\s/)
                        || text.match(/^\*{3,}/)
                        || text.match(/^>/)
                        || text.match(/^\*\s/)
                        || text.match(/^\d*\.\s/)
                        || text.match(/^```/)
                        || text.match(/^\|.*\|/)

            if (matchArr) {
                let temp = ''
                const re1 = /^>/
                const re2 = /^\*\s/
                const re3 = /^\d*\.\s/
                const re4 = /^```/
                const re5 = /^\|.*\|/
                switch(matchArr[0]) {
                    case '# ':
                        html += `<h1 data-index="${index}">` + this.format(text.substring(2)) + '</h1>'
                        break
                    case '## ':
                        html += `<h2 data-index="${index}">` + this.format(text.substring(3)) + '</h2>'
                        break
                    case '### ':
                        html += `<h3 data-index="${index}">` + this.format(text.substring(4)) + '</h3>'
                        break
                    case '#### ':
                        html += `<h4 data-index="${index}">` + this.format(text.substring(5)) + '</h4>'
                        break
                    case '##### ':
                        html += `<h5 data-index="${index}">` + this.format(text.substring(6)) + '</h5>'
                        break
                    case '###### ':
                        html += `<h6 data-index="${index}">` + this.format(text.substring(7)) + '</h6>'
                        break
                    case text.match(/^\*{3,}/) && text.match(/^\*{3,}/)[0]:
                        html += text.replace(/^\*{3,}/g, '<hr>')
                        break
                    case '>':
                        while (i < len && nodes[i].textContent.match(re1)) {
                            const str = nodes[i].textContent
                            temp += '<div>' + this.format(str.slice(1)) + '</div>'
                            i++
                        }

                        i--
                        html += `<blockquote data-index="${index}">` + temp + '</blockquote>'
                        break
                    case '* ':
                        while (i < len && nodes[i].textContent?.match(re2)) {
                            const str = nodes[i].textContent
                            temp += `<li data-index="${nodes[i]?.dataset?.index}">` + this.format(str.slice(2)) + '</li>'
                            i++
                        }

                        i--
                        html += `<ul data-index="${index}">` + temp + '</ul>'
                        break
                    case text.match(/^\d*\.\s/) && text.match(/^\d*\.\s/)[0]:
                        while (i < len && nodes[i].textContent?.match(re3)) {
                            const str = nodes[i].textContent
                            temp += `<li data-index="${nodes[i]?.dataset?.index}">` + this.format(str.replace(/^\d*\.\s/, '')) + '</li>'
                            i++
                        } 

                        i--
                        html += `<ol data-index="${index}">` + temp + '</ol>'
                        break
                    case '```':
                        i++
                        while (i < len && !re4.test(nodes[i].textContent)) {
                            temp += `<div data-index="${nodes[i]?.dataset?.index}">` + escapeHTML(nodes[i].textContent) + '</div>'
                            i++
                        }
                        
                        html += `<pre data-index="${index}"><code>` + temp + '</code></pre>'
                        break
                    case text.match(/^\|.*\|/) && text.match(/^\|.*\|/)[0]:
                        let thRe = /^\[th\]/
                        let arr, j, jlen
                        while (i < len && re5.test(nodes[i].textContent)) {
                            arr = nodes[i].textContent.split('|')
                            temp += `<tr data-index="${nodes[i]?.dataset?.index}">`
                            for (j = 1, jlen = arr.length - 1; j < jlen; j++) {
                                if (thRe.test(arr[1])) {
                                    temp += '<th>' + arr[j] + '</th>'
                                } else {
                                    temp += '<td>' + arr[j] + '</td>'
                                }
                            }
                            temp += '</tr>'
                            temp = temp.replace('[th]', '')
                            i++
                        }

                        html += '<table>' + temp + '</table>'
                        break
                }
            } else if (text) {
                html += `<div data-index="${index}">` + this.format(text) + '</div>'
            }

        }

        return html
    }

    format(str) { 
        str = str.replace(/\s/g, '&nbsp') 

        const bold = str.match(/\*{2}[^*].*?\*{2}/g) // 惰性匹配
        if (bold) {
            for (let i = 0, len = bold.length; i < len; i++) {
                str = str.replace(bold[i], '<b>' + bold[i].substring(2, bold[i].length - 2) + '</b>')
            }
        }

        const italic = str.match(/\*[^*].*?\*/g)  
        if (italic) {
            for (let i = 0, len = italic.length; i < len; i++) {
                str = str.replace(italic[i], '<i>' + italic[i].substring(1, italic[i].length - 1) + '</i>')
            }
        }

        const code = str.match(/`[^`]*`/g)
        if (code) {
            for (let i = 0, len = code.length; i < len; i++) {
                str = str.replace(code[i], '<code>' + code[i].substring(1, code[i].length - 1) + '</code>')
            }
        }

        const img = str.match(/!\[.*\]\(.*\)/g)
        const re1 = /\(.*\)/
        const re2 = /\[.*\]/
        if (img) {
            for (let i = 0, len = img.length; i < len; i++) {
                const url = img[i].match(re1)[0]
                const title = img[i].match(re2)[0]
                str = str.replace(img[i], '<img src=' + url.substring(1, url.length - 1) + ' alt=' + title.substring(1, title.length -1) + '>')
            }
        }

        const a = str.match(/\[.*?\]\(.*?\)/g)
        if (a) {
            for (let i = 0, len = a.length; i < len; i++) {
                const url = a[i].match(re1)[0]
                const title = a[i].match(re2)[0]
                str = str.replace(a[i], '<a href=' + url.substring(1, url.length - 1)  + '>' + title.substring(1, title.length -1) + '</a>')
            }
        }

        return escapeHTML2(str)
    }
}

function escapeHTML(html) {
    return html.replace(/</g, '&lt;').replace(/>/g, '&gt;')
}

function escapeHTML2(html) {
    return html.replace(/<(\/)?script>/g, '&lt;$1script&gt;')
}

function $(selector) {
    return document.querySelector(selector)
}

// 修改光标位置
function changeCursorPosition(node) {
    const selection = window.getSelection()
    // 清除所有选区 如果是 Caret 类型,清除选区后变为 Range,如果不是 Range 类型,后面的 addRange() 就不起作用
    selection.removeAllRanges()
    const range = document.createRange()
    // 选中节点的内容
    range.selectNode(node)
    selection.addRange(range)
    // 取消选中并将光标移至选区最后
    selection.collapseToEnd()
}

// 清除复制后的内容样式
function clearTextStyle(e) {
    e.preventDefault()

    let text
    const clp = (e.originalEvent || e).clipboardData
    if (clp === undefined || clp === null) {
        text = window.clipboardData.getData('text') || ''
        if (text !== '') {
            if (window.getSelection) {
                var newNode = document.createElement('span')
                newNode.innerHTML = text
                window.getSelection().getRangeAt(0).insertNode(newNode)
            } else {
                document.selection.createRange().pasteHTML(text)
            }
        }
    } else {
        text = clp.getData('text/plain') || ''
        if (text !== '') {
            document.execCommand('insertText', false, text)
        }
    }
}

function throttle(delay) {
    let waitForCallFunc
    let canCall = true
    return function helper(callback, ...args) {
        if (!canCall) {
            if (callback) waitForCallFunc = callback
            return
        }

        callback(...args)
        canCall = false
        setTimeout(() => {
            canCall = true
            if (waitForCallFunc) {
                helper(waitForCallFunc, ...args)
                waitForCallFunc = null
            }
        }, delay)
    }
}

function debounce(delay) {
    let timer
    return function(callback, ...args) {
        clearTimeout(timer)
        timer = setTimeout(() => callback.call(null, ...args), delay)
    }
}

/**
 * 计算 dom 到容器顶部的距离
 * @param {HTMLElement} dom 需要计算的容器
 * @param {HTMLElement} topContainer 终止条件
 * @returns 
 */
 function getHeightToTop(dom) {
    let height = dom.offsetTop
    let parent = dom.offsetParent

    while (parent) {
        height += parent.offsetTop
        parent = parent.offsetParent
    }

    return height
}

// dom 是否在屏幕内
function isInScreen(dom) {
    const { top, bottom } = dom.getBoundingClientRect()
    return bottom >= 0 && top < window.innerHeight
}

// dom 在当前屏幕展示内容的百分比
function percentOfdomInScreen(dom) {
    const { height, bottom } = dom.getBoundingClientRect()
    if (bottom <= 0) return 0
    if (bottom >= height) return 1
    return bottom / height
}

function canNodeCalculate(node) {
    return (
        node.innerHTML 
        && node.innerHTML !== '<br>' 
        && !node.textContent.startsWith('```') 
        && isInScreen(node) 
        && percentOfdomInScreen(node) >= 0
    )
}

const editor = $('#editor')
const showDom = $('#show-content')
const markdown = new MarkdownParser()
showDom.innerHTML = markdown.compile(editor.children)

function onInput() {
    // 为每个元素加上索引,通过索引找到 markdown 渲染后的元素
    let index = 0
    const data = Array.from(editor.children)
    data.forEach(item => {
        delete item.dataset.index
        // 忽略 br 换行符和空文本字节
        if (item.tagName !== 'BR' && item.innerText.trim() !== '') {
            if (!item.children.length || (item.children.length === 1 && item.children[0].tagName === 'BR')) {
                item.dataset.index = index++
                return
            }

            // 这里主要是针对复制过来的有嵌套节点的内容
            const frag = document.createDocumentFragment()
            Array.from(item.childNodes).forEach(e => {
                if (e.nodeType === Node.TEXT_NODE) {
                    const div = document.createElement('div')
                    div.textContent = e.nodeValue
                    item.replaceChild(div, e)
                    div.dataset.index = index++
                    frag.appendChild(div)
                } else if (item.tagName !== 'BR') {
                    e.dataset?.index && delete e.dataset.index
                    e.dataset.index = index++
                    frag.appendChild(e)
                }
            })
            
            editor.replaceChild(frag, item)
            
            // 需要修改光标位置,不然光标会在复制内容的前面,修改后会在复制内容的后面
            changeCursorPosition(editor.querySelector(`[data-index="${index - 1}"]`))
        }
    })

    showDom.innerHTML = markdown.compile(editor.childNodes)
}

const debounceFn = debounce(100) // 防抖
editor.oninput = () => {
    debounceFn(onInput)
}

editor.onpaste = (e) => {
    clearTextStyle(e)
}

// 是否允许滚动
const canScroll = {
    editor: true,
    showDom: true,
}

const debounceFn2 = debounce(100) // 防抖
const throttleFn = throttle(50) // 节流
editor.onscroll = () => {
    if (!canScroll.editor) return

    canScroll.showDom = false
    throttleFn(onScroll, editor, showDom)
    debounceFn2(resumeScroll)
}

showDom.onscroll = () => {
    if (!canScroll.showDom) return

    canScroll.editor = false
    throttleFn(onScroll, showDom, editor)
    debounceFn(resumeScroll)
}

// 恢复滚动
function resumeScroll() {
    canScroll.editor = true
    canScroll.showDom = true
}

/**
 * 
 * @param {HTMLElement} scrollContainer 正在滚动的容器
 * @param {HTMLElement} ShowContainer 需要同步滚动的容器
 * @returns 
 */
function onScroll(scrollContainer, ShowContainer) {
    const scrollHeight = ShowContainer.scrollHeight
    // 滚动到底部
    if (scrollContainer.offsetHeight + scrollContainer.scrollTop >= scrollContainer.scrollHeight) {
        ShowContainer.scrollTo({ top: scrollHeight - ShowContainer.clientHeight })
        return
    }

    // 滚动到顶部
    if (scrollContainer.scrollTop === 0) {
        ShowContainer.scrollTo({ top: 0 })
        return
    }

    const nodes = Array.from(scrollContainer.children)
    for (const node of nodes) {
        // 从上往下遍历,找到第一个在屏幕内的元素
        if (canNodeCalculate(node)) {
            // 如果当前滚动的元素是 <pre> <table>
            if (node.tagName === 'PRE' || node.tagName === 'TABLE') {
                // 如果 pre 里面的子元素同步滚动了,则直接返回
                if (hasPreElementInScrollContainerScroll(node, ShowContainer)) return
                // 否则直接从下一个元素开始计算
                continue
            }

            const index = node.dataset.index
            const dom = ShowContainer.querySelector(`[data-index="${index}"]`)
            if (!dom) continue

            const percent = percentOfdomInScreen(node)
            const heightToTop = getHeightToTop(dom)
            const domNeedHideHeight = dom.offsetHeight * (1 - percent)
            ShowContainer.scrollTo({ top: heightToTop + domNeedHideHeight })
            break
        }
    }
}

function hasPreElementInScrollContainerScroll(preElement, ShowContainer) {
    for (const node of preElement.children[0].children) {
        // 从上往下遍历,找到第一个在屏幕内的元素
        if (isInScreen(node) && percentOfdomInScreen(node) >= 0) {
            const index = node.dataset.index
            const dom = ShowContainer.querySelector(`[data-index="${index}"]`)
            if (!dom) continue

            const percent = percentOfdomInScreen(node)
            const heightToTop = getHeightToTop(dom)
            const domNeedHideHeight = dom.offsetHeight * (1 - percent)
            ShowContainer.scrollTo({ top: heightToTop + domNeedHideHeight })
            return true
        }
    }

    return false
}
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>markdown 编辑器双屏同步滚动 demo6</title>
    <link rel="stylesheet" href="index.css">
</head>
<body>
    <div id="editor" contenteditable class="text-container">
        <div data-index="0">本文是可视化拖拽系列的第四篇,比起之前的三篇文章,这篇功能点要稍微少一点,总共有五点:</div><div data-index="1">1. SVG 组件</div><div data-index="2">2. 动态属性面板</div><div data-index="3">3. 数据来源(接口请求)</div><div data-index="4">4. 组件联动</div><div data-index="5">5. 组件按需加载</div><div><br></div><div data-index="6">如果你对我之前的系列文章不是很了解,建议先把这三篇文章看一遍,再来阅读本文(否则没有上下文,不太好理解):</div><div data-index="7">* [可视化拖拽组件库一些技术要点原理分析](https://github.com/woai3c/Front-end-articles/issues/19)</div><div data-index="8">* [可视化拖拽组件库一些技术要点原理分析(二)](https://github.com/woai3c/Front-end-articles/issues/20)</div><div data-index="9">* [可视化拖拽组件库一些技术要点原理分析(三)](https://github.com/woai3c/Front-end-articles/issues/21)</div><div><br></div><div data-index="10">另附上项目、在线 DEMO 地址:</div><div data-index="11">* [一个低代码(可视化拖拽)教学项目](https://github.com/woai3c/visual-drag-demo)</div><div data-index="12">* [在线 DEMO](https://woai3c.github.io/visual-drag-demo/)</div><div><br></div><div data-index="13">## SVG 组件</div><div data-index="14">目前项目里提供的自定义组件都是支持自由放大缩小的,不过他们有一个共同点——都是规则形状。也就是说对它们放大缩小,直接改变宽高就可以实现了,无需做其他处理。但是不规则形状就不一样了,譬如一个五角星,你得考虑放大缩小时,如何成比例的改变尺寸。最终,我采用了 svg 的方案来实现(还考虑过用 iconfont 来实现,不过有缺陷,放弃了),下面让我们来看看具体的实现细节。</div><div><br></div><div data-index="15">### 用 SVG 画一个五角星</div><div data-index="16">假设我们需要画一个 100 * 100 的五角星,它的代码是这样的:</div><div data-index="17">```html</div><div data-index="18">&lt;svg&nbsp;</div><div data-index="19">&nbsp; &nbsp; version="1.1"&nbsp;</div><div data-index="20">&nbsp; &nbsp; baseProfile="full"&nbsp;</div><div data-index="21">&nbsp; &nbsp; xmlns="http://www.w3.org/2000/svg"</div><div data-index="22">&gt;</div><div data-index="23">&nbsp; &nbsp; &lt;polygon&nbsp;</div><div data-index="24">&nbsp; &nbsp; &nbsp; &nbsp; points="50 0,62.5 37.5,100 37.5,75 62.5,87.5 100,50 75,12.5 100,25 62.5,0 37.5,37.5 37.5"&nbsp;</div><div data-index="25">&nbsp; &nbsp; &nbsp; &nbsp; stroke="#000"&nbsp;</div><div data-index="26">&nbsp; &nbsp; &nbsp; &nbsp; fill="rgba(255, 255, 255, 1)"&nbsp;</div><div data-index="27">&nbsp; &nbsp; &nbsp; &nbsp; stroke-width="1"</div><div data-index="28">&nbsp; &nbsp; &gt;&lt;/polygon&gt;</div><div data-index="29">&lt;/svg&gt;</div><div data-index="30">```</div><div data-index="31">svg 上的版本、命名空间之类的属性不是很重要,可以先忽略。重点是 polygon 这个元素,它在 svg 中定义了一个由`一组首尾相连的直线线段构成的闭合多边形形状`,最后一点连接到第一点。也就是说这个多边形由一系列坐标点组成,相连的点之间会自动连上。polygon 的 points 属性用来表示多边形的一系列坐标点,每个坐标点由 x y 坐标组成,每个坐标点之间用 `,`逗号分隔。</div><div><br></div><div data-index="32">![在这里插入图片描述](https://img-blog.csdnimg.cn/001d5384ef4841e9af16718d769da90b.png)</div><div><br></div><div data-index="33">上图就是一个用 svg 画的五角星,它由十个坐标点组成 `50 0,62.5 37.5,100 37.5,75 62.5,87.5 100,50 75,12.5 100,25 62.5,0 37.5,37.5 37.5`。由于这是一个 100*100 的五角星,所以我们能够很容易的根据每个坐标点的数值算出它们在五角星(坐标系)中所占的比例。譬如第一个点是 p1(`50,0`),那么它的 x y 坐标比例是 `50%, 0`;第二个点 p2(`62.5,37.5`),对应的比例是 `62.5%, 37.5%`...</div><div data-index="34">```js</div><div data-index="35">// 五角星十个坐标点的比例集合</div><div data-index="36">const points = [</div><div data-index="37">&nbsp; &nbsp; [0.5, 0],</div><div data-index="38">&nbsp; &nbsp; [0.625, 0.375],</div><div data-index="39">&nbsp; &nbsp; [1, 0.375],</div><div data-index="40">&nbsp; &nbsp; [0.75, 0.625],</div><div data-index="41">&nbsp; &nbsp; [0.875, 1],</div><div data-index="42">&nbsp; &nbsp; [0.5, 0.75],</div><div data-index="43">&nbsp; &nbsp; [0.125, 1],</div><div data-index="44">&nbsp; &nbsp; [0.25, 0.625],</div><div data-index="45">&nbsp; &nbsp; [0, 0.375],</div><div data-index="46">&nbsp; &nbsp; [0.375, 0.375],</div><div data-index="47">]</div><div data-index="48">```</div><div data-index="49">既然知道了五角星的比例,那么要画出其他尺寸的五角星也就易如反掌了。我们只需要在每次对五角星进行放大缩小,改变它的尺寸时,等比例的给出每个坐标点的具体数值即要。</div><div data-index="50">```html</div><div data-index="51">&lt;div class="svg-star-container"&gt;</div><div data-index="52">&nbsp; &nbsp; &lt;svg</div><div data-index="53">&nbsp; &nbsp; &nbsp; &nbsp; version="1.1"</div><div data-index="54">&nbsp; &nbsp; &nbsp; &nbsp; baseProfile="full"</div><div data-index="55">&nbsp; &nbsp; &nbsp; &nbsp; xmlns="http://www.w3.org/2000/svg"</div><div data-index="56">&nbsp; &nbsp; &gt;</div><div data-index="57">&nbsp; &nbsp; &nbsp; &nbsp; &lt;polygon</div><div data-index="58">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ref="star"</div><div data-index="59">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; :points="points"</div><div data-index="60">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; :stroke="element.style.borderColor"</div><div data-index="61">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; :fill="element.style.backgroundColor"</div><div data-index="62">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; stroke-width="1"</div><div data-index="63">&nbsp; &nbsp; &nbsp; &nbsp; /&gt;</div><div data-index="64">&nbsp; &nbsp; &lt;/svg&gt;</div><div data-index="65">&nbsp; &nbsp; &lt;v-text :prop-value="element.propValue" :element="element" /&gt;</div><div data-index="66">&lt;/div&gt;</div><div><br></div><div data-index="67">&lt;script&gt;</div><div data-index="68">function drawPolygon(width, height) {</div><div data-index="69">&nbsp; &nbsp; // 五角星十个坐标点的比例集合</div><div data-index="70">&nbsp; &nbsp; const points = [</div><div data-index="71">&nbsp; &nbsp; &nbsp; &nbsp; [0.5, 0],</div><div data-index="72">&nbsp; &nbsp; &nbsp; &nbsp; [0.625, 0.375],</div><div data-index="73">&nbsp; &nbsp; &nbsp; &nbsp; [1, 0.375],</div><div data-index="74">&nbsp; &nbsp; &nbsp; &nbsp; [0.75, 0.625],</div><div data-index="75">&nbsp; &nbsp; &nbsp; &nbsp; [0.875, 1],</div><div data-index="76">&nbsp; &nbsp; &nbsp; &nbsp; [0.5, 0.75],</div><div data-index="77">&nbsp; &nbsp; &nbsp; &nbsp; [0.125, 1],</div><div data-index="78">&nbsp; &nbsp; &nbsp; &nbsp; [0.25, 0.625],</div><div data-index="79">&nbsp; &nbsp; &nbsp; &nbsp; [0, 0.375],</div><div data-index="80">&nbsp; &nbsp; &nbsp; &nbsp; [0.375, 0.375],</div><div data-index="81">&nbsp; &nbsp; ]</div><div><br></div><div data-index="82">&nbsp; &nbsp; const coordinatePoints = points.map(point =&gt; width * point[0] + ' ' + height * point[1])</div><div data-index="83">&nbsp; &nbsp; this.points = coordinatePoints.toString() // 得出五角星的 points 属性数据</div><div data-index="84">}</div><div data-index="85">&lt;/script&gt;</div><div data-index="86">```</div><div><br></div><div data-index="87">![在这里插入图片描述](https://img-blog.csdnimg.cn/c9c766ad70624321905014a4e8e610a0.gif#pic_center)</div><div><br></div><div data-index="88">### 其他 SVG 组件</div><div data-index="89">同理,要画其他类型的 svg 组件,我们只要知道它们坐标点所占的比例就可以了。如果你不知道一个 svg 怎么画,可以网上搜一下,先找一个能用的 svg 代码(这个五角星的 svg 代码,就是在网上找的)。然后再计算它们每个坐标点所占的比例,转成小数点的形式,最后把这些数据代入上面提供的 `drawPolygon()` 函数即可。譬如画一个三角形的代码是这样的:</div><div data-index="90">```js</div><div data-index="91">function drawTriangle(width, height) {</div><div data-index="92">&nbsp; &nbsp; const points = [</div><div data-index="93">&nbsp; &nbsp; &nbsp; &nbsp; [0.5, 0.05],</div><div data-index="94">&nbsp; &nbsp; &nbsp; &nbsp; [1, 0.95],</div><div data-index="95">&nbsp; &nbsp; &nbsp; &nbsp; [0, 0.95],</div><div data-index="96">&nbsp; &nbsp; ]</div><div><br></div><div data-index="97">&nbsp; &nbsp; const coordinatePoints = points.map(point =&gt; width * point[0] + ' ' + height * point[1])</div><div data-index="98">&nbsp; &nbsp; this.points = coordinatePoints.toString() // 得出三角形的 points 属性数据</div><div data-index="99">}</div><div data-index="100">```</div><div><br></div><div data-index="101">![在这里插入图片描述](https://img-blog.csdnimg.cn/71f5181fbf874cd7a7ee4d5d3b53a0b7.png)</div><div><br></div><div data-index="102">## 动态属性面板</div><div data-index="103">目前所有自定义组件的属性面板都共用同一个 AttrList 组件。因此弊端很明显,需要在这里写很多 if 语句,因为不同的组件有不同的属性。例如矩形组件有 content 属性,但是图片没有,一个不同的属性就得写一个 if 语句。</div><div data-index="104">```html</div><div data-index="105">&lt;el-form-item v-if="name === 'rectShape'" label="内容"&gt;</div><div data-index="106">&nbsp; &nbsp;&lt;el-input /&gt;</div><div data-index="107">&lt;/el-form-item&gt;</div><div data-index="108">&lt;!-- 其他属性... --&gt;</div><div data-index="109">```</div><div data-index="110">幸好,这个问题的解决方案也不难。在本系列的第一篇文章中,有讲解过如何动态渲染自定义组件:</div><div data-index="111">```html</div><div data-index="112">&lt;component :is="item.component"&gt;&lt;/component&gt; &lt;!-- 动态渲染组件 --&gt;</div><div data-index="113">```</div><div data-index="114">在每个自定义组件的数据结构中都有一个 `component` 属性,这是该组件在 Vue 中注册的名称。因此,每个自定义组件的属性面板可以和组件本身一样(利用 `component` 属性),做成动态的:</div><div data-index="115">```html</div><div data-index="116">&lt;!-- 右侧属性列表 --&gt;</div><div data-index="117">&lt;section class="right"&gt;</div><div data-index="118">&nbsp; &nbsp; &lt;el-tabs v-if="curComponent" v-model="activeName"&gt;</div><div data-index="119">&nbsp; &nbsp; &nbsp; &nbsp; &lt;el-tab-pane label="属性" name="attr"&gt;</div><div data-index="120">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &lt;component :is="curComponent.component + 'Attr'" /&gt; &lt;!-- 动态渲染属性面板 --&gt;</div><div data-index="121">&nbsp; &nbsp; &nbsp; &nbsp; &lt;/el-tab-pane&gt;</div><div data-index="122">&nbsp; &nbsp; &nbsp; &nbsp; &lt;el-tab-pane label="动画" name="animation" style="padding-top: 20px;"&gt;</div><div data-index="123">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &lt;AnimationList /&gt;</div><div data-index="124">&nbsp; &nbsp; &nbsp; &nbsp; &lt;/el-tab-pane&gt;</div><div data-index="125">&nbsp; &nbsp; &nbsp; &nbsp; &lt;el-tab-pane label="事件" name="events" style="padding-top: 20px;"&gt;</div><div data-index="126">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &lt;EventList /&gt;</div><div data-index="127">&nbsp; &nbsp; &nbsp; &nbsp; &lt;/el-tab-pane&gt;</div><div data-index="128">&nbsp; &nbsp; &lt;/el-tabs&gt;</div><div data-index="129">&nbsp; &nbsp; &lt;CanvasAttr v-else&gt;&lt;/CanvasAttr&gt;</div><div data-index="130">&lt;/section&gt;</div><div data-index="131">```</div><div data-index="132">同时,自定义组件的目录结构也需要做下调整,原来的目录结构为:</div><div data-index="133">```</div><div data-index="134">- VText.vue</div><div data-index="135">- Picture.vue</div><div data-index="136">...</div><div data-index="137">```</div><div data-index="138">调整后变为:</div><div data-index="139">```html</div><div data-index="140">- VText</div><div data-index="141">&nbsp;- Attr.vue &lt;!-- 组件的属性面板 --&gt;</div><div data-index="142">&nbsp;- Component.vue &lt;!-- 组件本身 --&gt;</div><div data-index="143">- Picture</div><div data-index="144">&nbsp;- Attr.vue</div><div data-index="145">&nbsp;- Component.vue</div><div data-index="146">```</div><div data-index="147">现在每一个组件都包含了组件本身和它的属性面板。经过改造后,图片属性面板代码也更加精简了:</div><div data-index="148">```html</div><div data-index="149">&lt;template&gt;</div><div data-index="150">&nbsp; &nbsp; &lt;div class="attr-list"&gt;</div><div data-index="151">&nbsp; &nbsp; &nbsp; &nbsp; &lt;CommonAttr&gt;&lt;/CommonAttr&gt; &lt;!-- 通用属性 --&gt;</div><div data-index="152">&nbsp; &nbsp; &nbsp; &nbsp; &lt;el-form&gt;</div><div data-index="153">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &lt;el-form-item label="镜像翻转"&gt;</div><div data-index="154">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &lt;div style="clear: both;"&gt;</div><div data-index="155">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &lt;el-checkbox v-model="curComponent.propValue.flip.horizontal" label="horizontal"&gt;水平翻转&lt;/el-checkbox&gt;</div><div data-index="156">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &lt;el-checkbox v-model="curComponent.propValue.flip.vertical" label="vertical"&gt;垂直翻转&lt;/el-checkbox&gt;</div><div data-index="157">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &lt;/div&gt;</div><div data-index="158">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &lt;/el-form-item&gt;</div><div data-index="159">&nbsp; &nbsp; &nbsp; &nbsp; &lt;/el-form&gt;</div><div data-index="160">&nbsp; &nbsp; &lt;/div&gt;</div><div data-index="161">&lt;/template&gt;</div><div data-index="162">```</div><div data-index="163">这样一来,组件和对应的属性面板都变成动态的了。以后需要单独给某个自定义组件添加属性就非常方便了。</div><div><br></div><div data-index="164">![在这里插入图片描述](https://img-blog.csdnimg.cn/9c51c5aecd3a40598c656d84665d856d.gif#pic_center)</div><div><br></div><div data-index="165">## 数据来源(接口请求)</div><div data-index="166">有些组件会有动态加载数据的需求,所以特地加了一个 `Request` 公共属性组件,用于请求数据。当一个自定义组件拥有 `request` 属性时,就会在属性面板上渲染接口请求的相关内容。至此,属性面板的公共组件已经有两个了:</div><div data-index="167">```html</div><div data-index="168">-common</div><div data-index="169">&nbsp;- Request.vue &lt;!-- 接口请求 --&gt;</div><div data-index="170">&nbsp;- CommonAttr.vue &lt;!-- 通用样式 --&gt;</div><div data-index="171">```</div><div data-index="172">```js</div><div data-index="173">// VText 自定义组件的数据结构</div><div data-index="174">{</div><div data-index="175">&nbsp; &nbsp; component: 'VText',</div><div data-index="176">&nbsp; &nbsp; label: '文字',</div><div data-index="177">&nbsp; &nbsp; propValue: '双击编辑文字',</div><div data-index="178">&nbsp; &nbsp; icon: 'wenben',</div><div data-index="179">&nbsp; &nbsp; request: { // 接口请求</div><div data-index="180">&nbsp; &nbsp; &nbsp; &nbsp; method: 'GET',</div><div data-index="181">&nbsp; &nbsp; &nbsp; &nbsp; data: [],</div><div data-index="182">&nbsp; &nbsp; &nbsp; &nbsp; url: '',</div><div data-index="183">&nbsp; &nbsp; &nbsp; &nbsp; series: false, // 是否定时发送请求</div><div data-index="184">&nbsp; &nbsp; &nbsp; &nbsp; time: 1000, // 定时更新时间</div><div data-index="185">&nbsp; &nbsp; &nbsp; &nbsp; paramType: '', // string object array</div><div data-index="186">&nbsp; &nbsp; &nbsp; &nbsp; requestCount: 0, // 请求次数限制,0 为无限</div><div data-index="187">&nbsp; &nbsp; },</div><div data-index="188">&nbsp; &nbsp; style: { // 通用样式</div><div data-index="189">&nbsp; &nbsp; &nbsp; &nbsp; width: 200,</div><div data-index="190">&nbsp; &nbsp; &nbsp; &nbsp; height: 28,</div><div data-index="191">&nbsp; &nbsp; &nbsp; &nbsp; fontSize: '',</div><div data-index="192">&nbsp; &nbsp; &nbsp; &nbsp; fontWeight: 400,</div><div data-index="193">&nbsp; &nbsp; &nbsp; &nbsp; lineHeight: '',</div><div data-index="194">&nbsp; &nbsp; &nbsp; &nbsp; letterSpacing: 0,</div><div data-index="195">&nbsp; &nbsp; &nbsp; &nbsp; textAlign: '',</div><div data-index="196">&nbsp; &nbsp; &nbsp; &nbsp; color: '',</div><div data-index="197">&nbsp; &nbsp; },</div><div data-index="198">}</div><div data-index="199">```</div><div><br></div><div data-index="200">![在这里插入图片描述](https://img-blog.csdnimg.cn/0c43c6caa3f4450f84660825e3340a15.gif#pic_center)</div><div data-index="201">从上面的动图可以看出,api 请求的方法参数等都是可以手动修改的。但是怎么控制返回来的数据赋值给组件的某个属性呢?这可以在发出请求的时候把组件的整个数据对象 `obj` 以及要修改属性的 `key` 当成参数一起传进去,当数据返回来时,就可以直接使用 `obj[key] = data` 来修改数据了。</div><div data-index="202">```js</div><div data-index="203">// 第二个参数是要修改数据的父对象,第三个参数是修改数据的 key,第四个数据修改数据的类型</div><div data-index="204">this.cancelRequest = request(this.request, this.element, 'propValue', 'string')</div><div data-index="205">```</div><div data-index="206">## 组件联动</div><div data-index="207">组件联动:当一个组件触发事件时,另一个组件会收到通知,并且做出相应的操作。</div><div><br></div><div data-index="208">![在这里插入图片描述](https://img-blog.csdnimg.cn/fae5aa63455b41e5aeec714a9ec1e9d1.gif#pic_center)</div><div data-index="209">上面这个动图的矩形,它分别监听了下面两个按钮的悬浮事件,第一个按钮触发悬浮并广播事件,矩形执行回调向右旋转移动;第二个按钮则相反,向左旋转移动。</div><div><br></div><div data-index="210">要实现这个功能,首先要给自定义组件加一个新属性 `linkage`,用来记录所有要联动的组件:</div><div data-index="211">```js</div><div data-index="212">{</div><div data-index="213">&nbsp;// 组件的其他属性...</div><div data-index="214">&nbsp;linkage: {</div><div data-index="215">&nbsp; &nbsp; &nbsp; duration: 0, // 过渡持续时间</div><div data-index="216">&nbsp; &nbsp; &nbsp; data: [ // 组件联动</div><div data-index="217">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {</div><div data-index="218">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; id: '', // 联动的组件 id</div><div data-index="219">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; label: '', // 联动的组件名称</div><div data-index="220">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; event: '', // 监听事件</div><div data-index="221">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; style: [{ key: '', value: '' }], // 监听的事件触发时,需要改变的属性</div><div data-index="222">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; },</div><div data-index="223">&nbsp; &nbsp; &nbsp; ],</div><div data-index="224">&nbsp; }</div><div data-index="225">}</div><div data-index="226">```</div><div data-index="227">对应的属性面板为:</div><div><br></div><div data-index="228">![在这里插入图片描述](https://img-blog.csdnimg.cn/796fbf38d04041f4b21763d28094a0d1.png)</div><div data-index="229">组件联动本质上就是订阅/发布模式的运用,每个组件在渲染时都会遍历它监听的所有组件。</div><div data-index="230">### 事件监听</div><div data-index="231">```html</div><div data-index="232">&lt;script&gt;</div><div data-index="233">import eventBus from '@/utils/eventBus'</div><div><br></div><div data-index="234">export default {</div><div data-index="235">&nbsp; &nbsp; props: {</div><div data-index="236">&nbsp; &nbsp; &nbsp; &nbsp; linkage: {</div><div data-index="237">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; type: Object,</div><div data-index="238">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; default: () =&gt; {},</div><div data-index="239">&nbsp; &nbsp; &nbsp; &nbsp; },</div><div data-index="240">&nbsp; &nbsp; &nbsp; &nbsp; element: {</div><div data-index="241">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; type: Object,</div><div data-index="242">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; default: () =&gt; {},</div><div data-index="243">&nbsp; &nbsp; &nbsp; &nbsp; },</div><div data-index="244">&nbsp; &nbsp; },</div><div data-index="245">&nbsp; &nbsp; created() {</div><div data-index="246">&nbsp; &nbsp; &nbsp; &nbsp; if (this.linkage?.data?.length) {</div><div data-index="247">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; eventBus.$on('v-click', this.onClick)</div><div data-index="248">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; eventBus.$on('v-hover', this.onHover)</div><div data-index="249">&nbsp; &nbsp; &nbsp; &nbsp; }</div><div data-index="250">&nbsp; &nbsp; },</div><div data-index="251">&nbsp; &nbsp; mounted() {</div><div data-index="252">&nbsp; &nbsp; &nbsp; &nbsp; const { data, duration } = this.linkage || {}</div><div data-index="253">&nbsp; &nbsp; &nbsp; &nbsp; if (data?.length) {</div><div data-index="254">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.$el.style.transition = `all ${duration}s`</div><div data-index="255">&nbsp; &nbsp; &nbsp; &nbsp; }</div><div data-index="256">&nbsp; &nbsp; },</div><div data-index="257">&nbsp; &nbsp; beforeDestroy() {</div><div data-index="258">&nbsp; &nbsp; &nbsp; &nbsp; if (this.linkage?.data?.length) {</div><div data-index="259">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; eventBus.$off('v-click', this.onClick)</div><div data-index="260">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; eventBus.$off('v-hover', this.onHover)</div><div data-index="261">&nbsp; &nbsp; &nbsp; &nbsp; }</div><div data-index="262">&nbsp; &nbsp; },</div><div data-index="263">&nbsp; &nbsp; methods: {</div><div data-index="264">&nbsp; &nbsp; &nbsp; &nbsp; changeStyle(data = []) {</div><div data-index="265">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; data.forEach(item =&gt; {</div><div data-index="266">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; item.style.forEach(e =&gt; {</div><div data-index="267">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (e.key) {</div><div data-index="268">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.element.style[e.key] = e.value</div><div data-index="269">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }</div><div data-index="270">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; })</div><div data-index="271">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; })</div><div data-index="272">&nbsp; &nbsp; &nbsp; &nbsp; },</div><div><br></div><div data-index="273">&nbsp; &nbsp; &nbsp; &nbsp; onClick(componentId) {</div><div data-index="274">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const data = this.linkage.data.filter(item =&gt; item.id === componentId &amp;&amp; item.event === 'v-click')</div><div data-index="275">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.changeStyle(data)</div><div data-index="276">&nbsp; &nbsp; &nbsp; &nbsp; },</div><div><br></div><div data-index="277">&nbsp; &nbsp; &nbsp; &nbsp; onHover(componentId) {</div><div data-index="278">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const data = this.linkage.data.filter(item =&gt; item.id === componentId &amp;&amp; item.event === 'v-hover')</div><div data-index="279">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this.changeStyle(data)</div><div data-index="280">&nbsp; &nbsp; &nbsp; &nbsp; },</div><div data-index="281">&nbsp; &nbsp; },</div><div data-index="282">}</div><div data-index="283">&lt;/script&gt;</div><div data-index="284">```</div><div data-index="285">从上述代码可以看出:</div><div data-index="286">1. 每一个自定义组件初始化时,都会监听 `v-click` `v-hover` 两个事件(目前只有点击、悬浮两个事件)</div><div data-index="287">2. 事件回调函数触发时会收到一个参数——发出事件的组件 id(譬如多个组件都触发了点击事件,需要根据 id 来判断是否是自己监听的组件)</div><div data-index="288">3. 最后再修改对应的属性</div><div><br></div><div data-index="289">### 事件触发</div><div data-index="290">```html</div><div data-index="291">&lt;template&gt;</div><div data-index="292">&nbsp; &nbsp; &lt;div @click="onClick" @mouseenter="onMouseEnter"&gt;</div><div data-index="293">&nbsp; &nbsp; &nbsp; &nbsp; &lt;component</div><div data-index="294">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; :is="config.component"</div><div data-index="295">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ref="component"</div><div data-index="296">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; class="component"</div><div data-index="297">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; :style="getStyle(config.style)"</div><div data-index="298">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; :prop-value="config.propValue"</div><div data-index="299">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; :element="config"</div><div data-index="300">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; :request="config.request"</div><div data-index="301">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; :linkage="config.linkage"</div><div data-index="302">&nbsp; &nbsp; &nbsp; &nbsp; /&gt;</div><div data-index="303">&nbsp; &nbsp; &lt;/div&gt;</div><div data-index="304">&lt;/template&gt;</div><div><br></div><div data-index="305">&lt;script&gt;</div><div data-index="306">import eventBus from '@/utils/eventBus'</div><div><br></div><div data-index="307">export default {</div><div data-index="308">&nbsp; &nbsp; methods: {</div><div data-index="309">&nbsp; &nbsp; &nbsp; &nbsp; onClick() {</div><div data-index="310">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; const events = this.config.events</div><div data-index="311">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Object.keys(events).forEach(event =&gt; {</div><div data-index="312">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; this[event](events[event])</div><div data-index="313">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; })</div><div><br></div><div data-index="314">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; eventBus.$emit('v-click', this.config.id)</div><div data-index="315">&nbsp; &nbsp; &nbsp; &nbsp; },</div><div><br></div><div data-index="316">&nbsp; &nbsp; &nbsp; &nbsp; onMouseEnter() {</div><div data-index="317">&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; eventBus.$emit('v-hover', this.config.id)</div><div data-index="318">&nbsp; &nbsp; &nbsp; &nbsp; },</div><div data-index="319">&nbsp; &nbsp; },</div><div data-index="320">}</div><div data-index="321">&lt;/script&gt;</div><div data-index="322">```</div><div data-index="323">从上述代码可以看出,在渲染组件时,每一个组件的最外层都监听了 `click` `mouseenter` 事件,当这些事件触发时,eventBus 就会触发对应的事件( v-click 或 v-hover ),并且把当前的组件 id 作为参数传过去。</div><div><br></div><div data-index="324">最后再捊一遍整体逻辑:</div><div data-index="325">1. a 组件监听原生事件 click mouseenter</div><div data-index="326">2. 用户点击或移动鼠标到组件上触发原生事件 click 或 mouseenter</div><div data-index="327">3. 事件回调函数再用 eventBus 触发 v-click 或 v-hover 事件</div><div data-index="328">4. 监听了这两个事件的 b 组件收到通知后再修改 b 组件的相关属性(例如上面矩形的 x  坐标和旋转角度)</div><div><br></div><div data-index="329">## 组件按需加载</div><div data-index="330">目前这个项目本身是没有做按需加载的,但是我把实现方案用文字的形式写出来其实也差不多。</div><div data-index="331">#### 第一步,抽离</div><div data-index="332">第一步需要把所有的自定义组件出离出来,单独存放。建议使用 monorepo 的方式来存放,所有的组件放在一个仓库里。每一个 package 就是一个组件,可以单独打包。</div><div data-index="333">```bash</div><div data-index="334">- node_modules</div><div data-index="335">- packages</div><div data-index="336">&nbsp;- v-text # 一个组件就是一个包&nbsp;</div><div data-index="337">&nbsp;- v-button</div><div data-index="338">&nbsp;- v-table</div><div data-index="339">- package.json</div><div data-index="340">- lerna.json</div><div data-index="341">```</div><div data-index="342">#### 第二步,打包</div><div data-index="343">建议每个组件都打包成一个 js 文件 ,例如叫 bundle.js。打包好直接调用上传接口放到服务器存起来(发布到 npm 也可以),每个组件都有一个唯一 id。前端每次渲染组件的时,通过这个组件 id 向服务器请求组件资源的 URL。</div><div data-index="344">#### 第三步,动态加载组件</div><div data-index="345">动态加载组件有两种方式:</div><div data-index="346">1. `import()`</div><div data-index="347">2. `&lt;script&gt;` 标签</div><div><br></div><div data-index="348">第一种方式实现起来比较方便:</div><div data-index="349">```js</div><div data-index="350">const name = 'v-text' // 组件名称</div><div data-index="351">const component = await import('https://xxx.xxx/bundile.js')</div><div data-index="352">Vue.component(name, component)</div><div data-index="353">```</div><div data-index="354">但是兼容性上有点小问题,如果要支持一些旧的浏览器(例如 IE),可以使用 `&lt;script&gt;` 标签的形式来加载:</div><div data-index="355">```js</div><div data-index="356">function loadjs(url) {</div><div data-index="357">&nbsp; &nbsp; return new Promise((resolve, reject) =&gt; {</div><div data-index="358">&nbsp; &nbsp; &nbsp; &nbsp; const script = document.createElement('script')</div><div data-index="359">&nbsp; &nbsp; &nbsp; &nbsp; script.src = url</div><div data-index="360">&nbsp; &nbsp; &nbsp; &nbsp; script.onload = resolve</div><div data-index="361">&nbsp; &nbsp; &nbsp; &nbsp; script.onerror = reject</div><div data-index="362">&nbsp; &nbsp; })</div><div data-index="363">}</div><div><br></div><div data-index="364">const name = 'v-text' // 组件名称</div><div data-index="365">await loadjs('https://xxx.xxx/bundile.js')</div><div data-index="366">// 这种方式加载组件,会直接将组件挂载在全局变量 window 下,所以 window[name] 取值后就是组件</div><div data-index="367">Vue.component(name, window[name])</div><div data-index="368">```</div><div data-index="369">为了同时支持这两种加载方式,在加载组件时需要判断一下浏览器是否支持 ES6。如果支持就用第一种方式,如果不支持就用第二种方式:</div><div data-index="370">```js</div><div data-index="371">function isSupportES6() {</div><div data-index="372">&nbsp; &nbsp; try {</div><div data-index="373">&nbsp; &nbsp; &nbsp; &nbsp; new Function('const fn = () =&gt; {};')</div><div data-index="374">&nbsp; &nbsp; } catch (error) {</div><div data-index="375">&nbsp; &nbsp; &nbsp; &nbsp; return false</div><div data-index="376">&nbsp; &nbsp; }</div><div><br></div><div data-index="377">&nbsp; &nbsp; return true</div><div data-index="378">}</div><div data-index="379">```</div><div data-index="380">最后一点,打包也要同时兼容这两种加载方式:</div><div data-index="381">```js</div><div data-index="382">import VText from './VText.vue'</div><div><br></div><div data-index="383">if (typeof window !== 'undefined') {</div><div data-index="384">&nbsp; &nbsp; window['VText'] = VText</div><div data-index="385">}</div><div><br></div><div data-index="386">export default VText</div><div data-index="387">```</div><div data-index="388">同时导出组件和把组件挂在 window 下。</div><div data-index="389">## 其他小优化</div><div data-index="390">### 图片镜像翻转</div><div data-index="391">![在这里插入图片描述](https://img-blog.csdnimg.cn/d85b1582d4694af892e98b1ee8362e79.gif#pic_center)</div><div data-index="392">图片镜像翻转需要使用 canvas 来实现,主要使用的是 canvas 的 `translate()` `scale()` 两个方法。假设我们要对一个 100*100 的图片进行水平镜像翻转,它的代码是这样的:</div><div data-index="393">```html</div><div data-index="394">&lt;canvas width="100" height="100"&gt;&lt;/canvas&gt;</div><div><br></div><div data-index="395">&lt;script&gt;</div><div data-index="396">&nbsp; &nbsp; const canvas = document.querySelector('canvas')</div><div data-index="397">&nbsp; &nbsp; const ctx = canvas.getContext('2d')</div><div data-index="398">&nbsp; &nbsp; const img = document.createElement('img')</div><div data-index="399">&nbsp; &nbsp; const width = 100</div><div data-index="400">&nbsp; &nbsp; const height = 100</div><div data-index="401">&nbsp; &nbsp; img.src = 'https://avatars.githubusercontent.com/u/22117876?v=4'</div><div data-index="402">&nbsp; &nbsp; img.onload = () =&gt; ctx.drawImage(img, 0, 0, width, height)</div><div><br></div><div data-index="403">&nbsp; &nbsp; // 水平翻转</div><div data-index="404">&nbsp; &nbsp; setTimeout(() =&gt; {</div><div data-index="405">&nbsp; &nbsp; &nbsp; &nbsp; // 清除图片</div><div data-index="406">&nbsp; &nbsp; &nbsp; &nbsp; ctx.clearRect(0, 0, width, height)</div><div data-index="407">&nbsp; &nbsp; &nbsp; &nbsp; // 平移图片</div><div data-index="408">&nbsp; &nbsp; &nbsp; &nbsp; ctx.translate(width, 0)</div><div data-index="409">&nbsp; &nbsp; &nbsp; &nbsp; // 对称镜像</div><div data-index="410">&nbsp; &nbsp; &nbsp; &nbsp; ctx.scale(-1, 1)</div><div data-index="411">&nbsp; &nbsp; &nbsp; &nbsp; ctx.drawImage(img, 0, 0, width, height)</div><div data-index="412">&nbsp; &nbsp; &nbsp; &nbsp; // 还原坐标点</div><div data-index="413">&nbsp; &nbsp; &nbsp; &nbsp; ctx.setTransform(1, 0, 0, 1, 0, 0)</div><div data-index="414">&nbsp; &nbsp; }, 2000)</div><div data-index="415">&lt;/script&gt;</div><div data-index="416">```</div><div data-index="417">`ctx.translate(width, 0)` 这行代码的意思是把图片的 x 坐标往前移动 width 个像素,所以平移后,图片就刚好在画布外面。然后这时使用 `ctx.scale(-1, 1)` 对图片进行水平翻转,就能得到一个水平翻转后的图片了。</div><div><br></div><div data-index="418">![在这里插入图片描述](https://img-blog.csdnimg.cn/a9da66492e8141cdb80c4fb32327e6e1.gif#pic_center)</div><div><br></div><div><br></div><div data-index="419">垂直翻转也是一样的原理,只不过参数不一样:</div><div data-index="420">```js</div><div data-index="421">// 原来水平翻转是 ctx.translate(width, 0)</div><div data-index="422">ctx.translate(0, height)&nbsp;</div><div data-index="423">// 原来水平翻转是 ctx.scale(-1, 1)</div><div data-index="424">ctx.scale(1, -1)</div><div data-index="425">```</div><div data-index="426">### 实时组件列表</div><div data-index="427">画布中的每一个组件都是有层级的,但是每个组件的具体层级并不会实时显现出来。因此,就有了这个实时组件列表的功能。</div><div><br></div><div data-index="428">这个功能实现起来并不难,它的原理和画布渲染组件是一样的,只不过这个列表只需要渲染图标和名称。</div><div data-index="429">```html</div><div data-index="430">&lt;div class="real-time-component-list"&gt;</div><div data-index="431">&nbsp; &nbsp; &lt;div</div><div data-index="432">&nbsp; &nbsp; &nbsp; &nbsp; v-for="(item, index) in componentData"</div><div data-index="433">&nbsp; &nbsp; &nbsp; &nbsp; :key="index"</div><div data-index="434">&nbsp; &nbsp; &nbsp; &nbsp; class="list"</div><div data-index="435">&nbsp; &nbsp; &nbsp; &nbsp; :class="{ actived: index === curComponentIndex }"</div><div data-index="436">&nbsp; &nbsp; &nbsp; &nbsp; @click="onClick(index)"</div><div data-index="437">&nbsp; &nbsp; &gt;</div><div data-index="438">&nbsp; &nbsp; &nbsp; &nbsp; &lt;span class="iconfont" :class="'icon-' + getComponent(index).icon"&gt;&lt;/span&gt;</div><div data-index="439">&nbsp; &nbsp; &nbsp; &nbsp; &lt;span&gt;{{ getComponent(index).label }}&lt;/span&gt;</div><div data-index="440">&nbsp; &nbsp; &lt;/div&gt;</div><div data-index="441">&lt;/div&gt;</div><div data-index="442">```</div><div data-index="443">但是有一点要注意,在组件数据的数组里,越靠后的组件层级越高。所以不对数组的数据索引做处理的话,用户看到的场景是这样的(**假设添加组件的顺序为文本、按钮、图片**):</div><div><br></div><div data-index="444">![在这里插入图片描述](https://img-blog.csdnimg.cn/6482b0f8f3b74434944e552412bed9c6.png)</div><div data-index="445">从用户的角度来看,层级最高的图片,在实时列表里排在最后。这跟我们平时的认知不太一样。所以,我们需要对组件数据做个 `reverse()` 翻转一下。譬如文字组件的索引为 0,层级最低,它应该显示在底部。那么每次在实时列表展示时,我们可以通过下面的代码转换一下,得到翻转后的索引,然后再渲染,这样的排序看起来就比较舒服了。</div><div data-index="446">```html</div><div data-index="447">&lt;div class="real-time-component-list"&gt;</div><div data-index="448">&nbsp; &nbsp; &lt;div</div><div data-index="449">&nbsp; &nbsp; &nbsp; &nbsp; v-for="(item, index) in componentData"</div><div data-index="450">&nbsp; &nbsp; &nbsp; &nbsp; :key="index"</div><div data-index="451">&nbsp; &nbsp; &nbsp; &nbsp; class="list"</div><div data-index="452">&nbsp; &nbsp; &nbsp; &nbsp; :class="{ actived: transformIndex(index) === curComponentIndex }"</div><div data-index="453">&nbsp; &nbsp; &nbsp; &nbsp; @click="onClick(transformIndex(index))"</div><div data-index="454">&nbsp; &nbsp; &gt;</div><div data-index="455">&nbsp; &nbsp; &nbsp; &nbsp; &lt;span class="iconfont" :class="'icon-' + getComponent(index).icon"&gt;&lt;/span&gt;</div><div data-index="456">&nbsp; &nbsp; &nbsp; &nbsp; &lt;span&gt;{{ getComponent(index).label }}&lt;/span&gt;</div><div data-index="457">&nbsp; &nbsp; &lt;/div&gt;</div><div data-index="458">&lt;/div&gt;</div><div><br></div><div data-index="459">&lt;script&gt;</div><div data-index="460">function getComponent(index) {</div><div data-index="461">&nbsp; &nbsp; return componentData[componentData.length - 1 - index]</div><div data-index="462">}</div><div><br></div><div data-index="463">function transformIndex(index) {</div><div data-index="464">&nbsp; &nbsp; return componentData.length - 1 - index</div><div data-index="465">}</div><div data-index="466">&lt;/script&gt;</div><div data-index="467">```</div><div data-index="468">![在这里插入图片描述](https://img-blog.csdnimg.cn/c6fa69e18ec64f54a95f403d3c476067.png)</div><div data-index="469">经过转换后,层级最高的图片在实时列表里排在最上面,完美!</div><div data-index="470">## 总结</div><div data-index="471">至此,可视化拖拽系列的第四篇文章已经结束了,距离上一篇系列文章的发布时间(2021年02月15日)已经有一年多了。没想到这个项目这么受欢迎,在短短一年的时间里获得了很多网友的认可。所以希望本系列的第四篇文章还是能像之前一样,对大家有帮助,再次感谢!</div><div><br></div><div data-index="472">**最后**,毛遂自荐一下自己,本人五年+前端,有基础架构和带团队的经验。有没有大佬有北京、天津的前端岗位推荐。如果有,请在评论区留言,或者私信帮忙内推一下,感谢!</div><div><br></div><div data-index="473">本人的社交主页:</div><div data-index="474">* [Github](https://github.com/woai3c)</div><div data-index="475">* [知乎](https://www.zhihu.com/people/tan-guang-zhi-19)</div><div data-index="476">* [掘金](https://juejin.cn/user/1433418893103645)</div>
    </div>
    <div id="show-content" class="text-container markdown"></div>
</body>
</html>
html,
body {
    height: 100%;
    font-size: 0;
}

* {
    margin: 0;
    padding: 0;
    font-family: 'Microsoft yahei';
}

body {
    display: flex;
}

table {
    border-collapse: collapse;
}

.text-container {
    box-sizing: border-box;
    width: 50%;
    border: 1px solid #e6e6e6;
    height: 100%;
    font-size: 14px;
    background: #f5f5f5;
    padding: 10px;
    line-height: 20px;
    outline: none;
    overflow: auto;
}
.text-container > div {
    min-height: 20px;
}

/* 定义滚动条高宽及背景
 高宽分别对应横竖滚动条的尺寸 */
 ::-webkit-scrollbar {
    width: 5px;
    height: 5px;
}

/* 定义滚动条轨道
  内阴影+圆角 */
::-webkit-scrollbar-track {
    background-color: transparent;
}

/* 定义滑块
  内阴影+圆角 */
::-webkit-scrollbar-thumb {
    border-radius: 4px;
    background-color: #ddd;
}

/* markdown style */
.markdown {
    font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;
    font-size: 16px;
    line-height: 1.5;
    word-wrap: break-word;
    background-color: #fff;
    color: #24292f;
}
.markdown img {
    max-width: 100%;
    margin: 10px 0;
}
.markdown>:first-child {
    margin-top: 0!important;
}
.markdown h1,
.markdown h2 {
    border-bottom: 1px solid #eaecef;
    padding-bottom: .3em;
}
.markdown h1,
.markdown h2, 
.markdown h3, 
.markdown h4, 
.markdown h5, 
.markdown h6 {
    font-weight: 600;
    line-height: 1.25;
    margin-bottom: 16px;
    margin-top: 24px;
}
.markdown div {
    min-height: 20px;
}
code, 
pre, 
tt {
    font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace;
    font-size: 14px;
}
.markdown pre {
    background-color: #f6f8fa;
    border-radius: 3px;
    font-size: 85%;
    line-height: 1.45;
    overflow: auto;
    padding: 16px;
    color: #24292e;
    font-size: 14px;
    font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace;
    margin: 0;
    margin-bottom: 1em;
}
.markdown code {
    background-color: #f6f8fa;
    border: 0;
    display: inline;
    line-height: inherit;
    margin: 0;
    max-width: auto;
    overflow: visible;
    padding: 0;
    word-wrap: normal;
    word-break: break-all;
}
.markdown blockquote {
    background-color: #f6f8fa;
    padding: 10px;
}
.markdown table {
    display: block;
    overflow: auto;
    width: 100%;
    margin-bottom: 15px;
}
.markdown table tr {
    background-color: #fff;
}
.markdown table td,
.markdown table th {
    border: 1px solid #dfe2e5;
    padding: 6px 13px;
}
.markdown p {
    margin-bottom: 1em;
}
.markdown li {
    word-wrap: break-all;
}
.markdown ul li {
    list-style: disc;
}
.markdown ol,
.markdown ul {
    padding-left: 2em;
}
.markdown blockquote, 
.markdown details, 
.markdown dl, 
.markdown ol, 
.markdown p, 
.markdown pre, 
.markdown table, 
.markdown ul {
    margin-bottom: 16px;
    margin-top: 0;
}
.markdown a {
    color: #0366d6;
    text-decoration: none;
}