木马啊

木马啊 查看完整档案

其它编辑中山大学  |  软件工程 编辑  |  填写所在公司/组织 wintc.top 编辑
编辑

想把代码写成诗的未成名作家

欢迎交流技术问题。
微信:win_tc

个人网站: https://wintc.top

个人动态

木马啊 回答了问题 · 11月27日

解决是否应该使用uni-app来开发项目呢?

如果你熟悉vue 想快速开发小程序可以用。
但uni-app这个玩意,在继承小程序本身的坑的基础上,自己又扩展了一堆坑。

关注 3 回答 5

木马啊 回答了问题 · 11月27日

父元素为relative,子组件是absolute,父组件允许滚动的时候,子组件如何固定到父元素顶部?

如果你想简单点,那直接父元素加一个啥也不干的transform属性:

transform: translateX(0)

然后absolute改为fixed,因为transform元素可以关住fixed元素让fixed相对自身定位而不是根元素。

或者让absolute升一级,成为当前父元素的兄弟元素,而它们共同的父元素使用relative。

关注 2 回答 2

木马啊 回答了问题 · 11月27日

解决js 奇奇怪怪的问题

因为0.2不能被JS精确表示。
搜索0.1+0.2!==0.3了解详情。

关注 3 回答 3

木马啊 发布了文章 · 11月26日

优雅解决按钮”重复点击“问题

一、对思否的一点吐槽

思否上的许多按钮都没有做重复点击检测的问题,往往导致点击没反应,多次点击后突然发表多条相同内容,比如和一位同学的私信:
image.png

又如一个问题下的评论:
image.png

如果你在这篇文章下发表评论,可以不小心多点几下"提交评论"按钮,会发现也存在相同的问题~~~


这个问题怎么解决呢?
简单点,使用一个lock标记,在请求发出时上锁,上锁后就不可以再发请求,可以在请求结束后解锁:

let clickButton = (function () {
  let lock = false
  return function (postParams) {
    if (lock) return
    lock = true
    // 假设使用axios发送请求
    axios.post('urlxxx', postParams).then(
      // 表单提交成功
    ).catch(error => {
      // 表单提交出错
      console.log(error)
    }).finally(() => {
      // 不管成功失败 都解锁
      lock = false
    })
  }
})()

button.addEventListener('click', clickButton)

当然对于button按钮,可以使用setAttribute('disabled', xxx)和removeAttribute('disabled')来代替lock标记。

这个方案问题在于,对于每一次按钮点击,我们都要写个lock标记,相当于重复的逻辑会出现在代码的各个地方——是不是可以封装一下呢?

二、封装按钮锁定、解锁逻辑

写一个装饰器将逻辑封装起来:

function ignoreMultiClick(func, manual = false) {
  let lock = false
  return function (...args) {
    if (lock) return
    lock = true
    let done = () => (lock = false)
    if (manual) return func.call(this, ...args, done)
    let promise = func.call(this, ...args)
    Promise.resolve(promise).finally(done)
    return promise
  }
}

将想监听点击回调函数func作为传递给ignoreMultiClick进行装饰,会返回一个新的函数,使用该函数作为点击的回调事件即可。
这里同样用了一个标记lock来上锁,有两种方法解锁:

  1. 手动解锁:可以给ignoreMultiClick传递一个参数manual,意思是主动调用解锁。若该参数为truthy,则点击事件触发时会给原始的点击回调func传递一个参数done,done是一个函数,调用它可以解锁。
  2. 自动解锁:可以使原监听函数func返回一个promise,在该promise决议后自动执行解锁操作。因为Promise管理回调函数非常方便,并且像axios这样非常常用的请求库返回值本身也是一个promise,所以默认情况使用这种方式。当然返回promise并不是必须的,有时候我们在发请求前会进行一些验证,验证没通过则直接return,此时装饰器函数也能正常处理,因为使用Promise.resolve包裹了一下promise: Promise.resolve(promise).finally(done)

三、使用实例

自动解锁使用例子:

let clickButton = ignoreMultiClick(function (postParams) {
  if (!checkForm()) return // 假设有一些检测表单的操作,检查不通过则直接返回
  // 返回promise
  return axios.post('urlxxx', postParams).then(
    // 表单提交成功
  ).catch(error => {
    // 表单提交出错
    console.log(error)
  })
})
button.addEventListener('click', clickButton)

手动解锁:

let clickButton = ignoreMultiClick(function (postParams, done) {
  if (!checkForm()) return done() // 表单验证不通过解锁
  axios.post('urlxxx', postParams).then(
    // 表单提交成功
  ).catch(error => {
    // 表单提交出错
    console.log(error)
  }).finally(() => done()) // 请求结束解锁
})
button.addEventListener('click', clickButton)

普通场景下还是自动解锁比较简单,因为可能有多个条件分支,手动解锁需要在每一个返回的地方都调用done。

查看原文

赞 24 收藏 12 评论 33

木马啊 赞了回答 · 11月25日

Promise then catch 的 使用问题

二二八嗯阿大战

关注 4 回答 5

木马啊 回答了问题 · 11月25日

Promise then catch 的 使用问题

首先理解catch只不过是then的一个简写形式。
对于你的需求:setDate你应该返回fetch().then()这个表达式的结果,而不是fetch().then().catch()的结果。

function setDate () {
  const promise = fetch().then()
  promise.catch(err => console.log(err)) // 处理promise的异常
  return promise
}

关注 4 回答 5

木马啊 回答了问题 · 11月25日

如何用css实现左侧label宽度一致且为最长标题的宽度,宽度不固定自适应

如果右侧每项高度不定的话,可用table去边框。

关注 1 回答 1

木马啊 回答了问题 · 11月24日

用js刷leetcode第1题,forEach和for循环执行,返回结果为什么不一样,如下图代码

看return语句所在。
forEach的参数是个函数,return语句相当于结束这个函数的运行,并不是外层函数。外层函数相当于没有return语句,默认返回undefined

关注 2 回答 2

木马啊 发布了文章 · 11月24日

关键词高亮:HTML字符串中匹配跨标签关键词

本文发布于个人网站:https://wintc.top/article/59,转载请注明

很久之前写过一个Vue组件,可以匹配文本内容中的关键词高亮,类似浏览器ctrl+f搜索结果。实现方案是,将文本字符串中的关键字搜索出来,然后使用特殊的标签(比如font标签)包裹关键词替换匹配内容,最后得到一个HTML字符串,渲染该字符串并在font标签上使用CSS样式即可实现高亮的效果。

当时的实现过于简单,没有支持接收HTML字符串作为内容进行关键词匹配。这两天有同学问到,就又思考了这个问题,发现并不是那么麻烦,写了几行代码解决一下。

一、匹配关键字:HTML字符串与文本字符串对比

1. 纯文本字符串的处理

对于纯文本字符串,如:“江畔何人初见月?江月何年初照人? ”,假如我们想匹配“江月”这个关键字,则匹配结果可处理为:

江畔何人初见月?<font style="background: #ff9632">江月</font>何年初照人?

这样“江月”两个字被font标签包裹,在font标签上应用特殊的背景样式以达到关键字高亮的效果。

2. 对HTML字符串的处理

对于上述例子,如果内容字符串是一个HTML文本:

江畔何人初见<b>月</b>?江<b>月</b>何年初照人?

对于同样的关键词“江月”,怎样处理它呢?因为关键词中的字在不同的标签内,所以只能分别用font标签进行替换:

江畔何人初见<b>月</b>?<font style="background: #ff9632">江</font><b><font style="background: #ff9632">月</font></b>何年初照人?

这是比较简单的情况,实际情况下关键字则可能跨多级、多层标签。

二、跨标签匹配关键词

跨标签解析关键词,其实就是对于匹配到的关键词,提取出各标签中对应的子片段,然后用font之类的标签包裹,再将高亮样式用于font标签即可。

对于整个HTML内容而言,渲染出来的文本由各类标签内的文本节点组成。因为关键词匹配的内容会跨标签,所以需要将各文本节点有序取出,并将节点内容拼接起来进行匹配。拼接时记下节点文本在拼接串中的起止位置,以便关键词匹配到拼接串的某位置时截取文本片段并使用font标签包裹。

1. 深度优先遍历DOM树取出文本节点

深度优先可以采用循环或者递归的方式遍历,这里采用循环实现,按取出某个元素下所有文本节点(利用nodeType判断文本节点):

function getTextNodeList (dom) {
  const nodeList = [...dom.childNodes]
  const textNodes = []
  while (nodeList.length) {
    const node = nodeList.shift()
    if (node.nodeType === node.TEXT_NODE) {
      textNodes.push(node)
    } else {
      nodeList.unshift(...node.childNodes)
    }
  }
  return textNodes
}

2. 取出所有文本内容进行拼接

获取到了文本节点列表,可以取出所有文本内容并记录每个文本片段在拼接结果中的开始、结束索引:

function getTextInfoList (textNodes) {
  let length = 0
  const textList = textNodes.map(node => {
    let startIdx = length, endIdx = length + node.wholeText.length
    length = endIdx
    return {
      text: node.wholeText,
      startIdx,
      endIdx
    }
  })
  return textList
},

拼接文本:

const content = textList.map(({ text }) => text).join('')

3. 匹配关键词

获得了拼接文本,可以利用拼接文本获取所有的拼接结果了。这里偷个懒直接用正则匹配吧,得把正则用到的一些特殊符号进行转义一下:

function getMatchList (content, keyword) {
  const characters = [...'[]()?.+*^${}:'].reduce((r, c) => (r[c] = true, r), {})
  keyword = keyword.split('').map(s => characters[s] ? `\\${s}` : s).join('[\\s\\n]*')
  const reg = new RegExp(keyword, 'gmi')
  return [...content.matchAll(reg)] // matchAll结果是个迭代器,用扩展符展开得到数组
}

关键词字符转义处理后,字符与字符之间中间插入了正则中的空白符和换行符(\s\n),以在匹配时忽略一些看不见的字符。上述代码使用了matchAll函数,匹配结果展开后得到的结果是一个数组,数组中的每一项都包含了匹配文本、匹配索引等。matchAll的一个简单例子:

4. 关键词使用font标签替换

根据关键词匹配结果索引,以及每个文本节点的起止索引,可以计算出每个关键词匹配了哪几个文本节点,其中对于开始和结束的文本节点,可能只是部分匹配到,而中间的文本节点的所有内容都是匹配到的。

比如对于HTML文本:

<span>江畔何人初见<b>月</b>?江月何年初照人?</span>

其DOM树对应的的文本节点有3个:

假如关键字是“何人初见月?”,那此时,对于第一个文本节点匹配了后半部分,第二个文本节点完全匹配,第三个文本节点匹配了第一个字符。三个节点中匹配的部分需要分别用font标签替换:

<span>江畔<font>何人初见</font><b><font>月</font></b><font>?</font>江月何年初照人?</span>

默认情况下,连续的文字会在同一个文本节点中,而对于匹配了部分内容的文本节点,就需要将它一分为二,可以利用Text.splitText()")API来分割文本节点,API接收一个索引值,从索引位置将文本节点后半部分切割并返回包含后半部分内容的新文本节点。上述例子中匹配的是3个节点,拆分后就会得到5个文本节点:

中间三个文本节点即是需要被替换的节点,使用replaceChild就可以直接将文本节点替换为font标签。

对于整个HTML字符串,同一个关键词可能同时有多处匹配结果,因此要对所有匹配结果进行上述处理。使用前几步获取的textNodes、textList、matchList,代码实现如下:

function replaceMatchResult (textNodes, textList, matchList) {
  // 对于每一个匹配结果,可能分散在多个标签中,找出这些标签,截取匹配片段并用font标签替换出
  for (let i = matchList.length - 1; i >= 0; i--) {
    const match = matchList[i]
    const matchStart = match.index, matchEnd = matchStart + match[0].length // 匹配结果在拼接字符串中的起止索引
    // 遍历文本信息列表,查找匹配的文本节点
    for (let textIdx = 0; textIdx < textList.length; textIdx++) {
      const { text, startIdx, endIdx } = textList[textIdx] // 文本内容、文本在拼接串中开始、结束索引
      if (endIdx < matchStart) continue // 匹配的文本节点还在后面
      if (startIdx >= matchEnd) break // 匹配文本节点已经处理完了
      let textNode = textNodes[textIdx] // 这个节点中的部分或全部内容匹配到了关键词,将匹配部分截取出来进行替换
      const nodeMatchStartIdx = Math.max(0, matchStart - startIdx) // 匹配内容在文本节点内容中的开始索引
      const nodeMatchLength = Math.min(endIdx, matchEnd) - startIdx - nodeMatchStartIdx // 文本节点内容匹配关键词的长度
      if (nodeMatchStartIdx > 0) textNode = textNode.splitText(nodeMatchStartIdx) // textNode取后半部分
      if (nodeMatchLength < textNode.wholeText.length) textNode.splitText(nodeMatchLength)
      const font = document.createElement('font')
      font.innerText = text.substr(nodeMatchStartIdx, nodeMatchLength)
      textNode.parentNode.replaceChild(font, textNode)
    }
  }
}

代码里对匹配结果遍历时,采用的是倒序遍历,原因是遍历过程对textNodes存在副作用:在遍历中会对textNodes中的文本节点进行切割。假设同一个文本节点中有多处匹配,会进行多次分割,而textNodes里引用的是原文本节点即前半部分,因此从后往前遍历会确保未处理的匹配文本节点的完整。

同时代码中省去了font节点的样式设置,这个可以根据自己的逻辑来设置。

三、完整代码调用

上述步骤描述了HTML字符串跨标签匹配关键词的所有流程实现,下面是完整的代码调用示例:

function replaceKeywords (htmlString, keyword) {
  if (!keyword) return htmlString
  const div = document.createElement('div')
  div.innerHTML = htmlString
  const textNodes = getTextNodeList(div)
  const textList = getTextInfoList(textNodes)
  const content = textList.map(({ text }) => text).join('')
  const matchList = getMatchList(content, keyword)
  replaceMatchResult(textNodes, textList, matchList)
  return div.innerHTML
}

输入一个HTML字符串和关键词,将HTML串中的关键词用font标签包裹后返回。

四、总结

上述实现方案中有一些简单的细节省去了,比如设置font标签的样式、隐藏的dom匹配时忽略等。

font标签样式设置看使用场景吧,如果是长HTML字符串匹配建议是不要直接设置style属性,而是操作样式表来达到目的。可以给font标签设置特殊的属性,然后使用属性选择器来设置样式。比如可以给font设置highlight="${i}"属性,来针对匹配的关键词应用不同的样式。操作样式表可以给style标签设置innerText或者调用CSSStyleSheet.insertRule()")和CSSStyleSheet.deleteRule()")。

demo: https://wintc.top/laboratory/#/search-highlight
github查看源码:https://github.com/Lushenggang/vue-search-highlight


码代码五分钟,写博客两小时....

查看原文

赞 5 收藏 3 评论 2

认证与成就

  • 获得 817 次点赞
  • 获得 28 枚徽章 获得 1 枚金徽章, 获得 9 枚银徽章, 获得 18 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2018-01-16
个人主页被 6k 人浏览