Tenadolanter

Tenadolanter 查看完整档案

上海编辑  |  填写毕业院校  |  填写所在公司/组织 www.kelede.win 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

Tenadolanter 赞了文章 · 4月16日

前端高亮功能实现复盘

前端高亮功能实现复盘

“高亮”功能,个人觉得没必要再解释什么了。作为一名程序猿,天天都会接触高亮:写代码时的语法高亮;使用搜索引擎时的搜索结果高亮。作为一名前端,如果你做过与搜索相关的功能,那么你很有可能就实现过高亮,本文也主要从前端的角度复盘一下“高亮”功能实现的关键知识点。

高亮

高亮实现思路

高亮原理

对用户的输入进行分词得到关键词,根据关键词搜索得到搜索结果。再次使用关键词从搜索结果中找到匹配,对匹配加上高亮样式,即完成高亮。细分这个过程,会有以下细节点:

  • 对用户输入分词得到关键词

  • 根据关键词得到搜索结果

  • 关键词匹配

  • 对匹配使用高亮样式

前两步一般在后台完成,后两步才是我们前端的工作,下面通过具体例子来实际演练一下。

普通文本高亮

比如我们有这样的文本:“我是中国人,我爱中华人民共和国,中华人民共和国万岁!”,这时我们的关键词是“我”,即要高亮文本中所有的我。代码实现完整如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>普通文本高亮</title>
  <style>
    .keyword-match {
      color: red;
    }
  </style>
</head>
<body>
<div>
  原文本:我是中国人,我爱中华人民共和国,中华人民共和国万岁!<br />
  关键词: 我 <br />
  结果如下:<br /><br />
</div>
<div id="content"></div>

<script>
  const content = document.getElementById('content');
  const text = '我是中国人,我爱中华人民共和国,中华人民共和国万岁!';
  const keyword = '我';

  // 根据关键词,对匹配加上高亮样式
  let hightlightText = text.replace(new RegExp(`(${keyword})`, 'g'), `<span class="keyword-match">$1</span>`);

  // 通过innerHTML写入匹配后的内容
  content.innerHTML = hightlightText;
</script>
</body>
</html>

上面的demo运行效果如下

普通文本高亮

上面的例子虽然很简单,但已经完整实现了高亮功能,说明了高亮的实现原理。在实际应用中,我们的关键词多半不会是一个简简单单的“我”,而是一个List,下面我们将关键词改成:我,中华。只需稍加修改,就可以实现同时对“我”,“中华”两个关键词高亮

<body>
<div>
  原文本:我是中国人,我爱中华人民共和国,中华人民共和国万岁!<br />
  关键词: 我,中华 <br />
  结果如下:<br /><br />
</div>
<div id="content"></div>

<script>
  const content = document.getElementById('content');
  const text = '我是中国人,我爱中华人民共和国,中华人民共和国万岁!';
  const keyword = ['我', '中华']; // 关键词是一个数组

  // new RegExp(`(${keyword})` 改成 new RegExp(`(${keyword.join('|')})`
  let hightlightText = text.replace(new RegExp(`(${keyword.join('|')})`, 'g'), `<span class="keyword-match">$1</span>`);

  // 通过innerHTML写入匹配后的内容
  content.innerHTML = hightlightText;
</script>
</body>

运行效果:

普通文本高亮

上面的代码似乎已经完美了。但如果我们将关键词改成:我,中华,中华人民共和国。再次运行看结果,会发现“中华人民共和国”这个关键词没有被高亮,而“中华”高亮了,但这并不是我们想要的结果。

注意:如果一个关键词包含另一个关键词,要优先高亮长度较长的词才对。要修复这个Bug也很简单,只需要将字数多的关键词放到关键词数组的最前面即可

<body>
<div>
  原文本:我是中国人,我爱中华人民共和国,中华人民共和国万岁!<br />
  关键词: 我,中华,中华人民共和国 <br />
  结果如下:<br /><br />
</div>
<div id="content"></div>
<div id="content2"></div>

<script>
  const content = document.getElementById('content');
  const text = '我是中国人,我爱中华人民共和国,中华人民共和国万岁!';
  const keyword = ['我', '中华', '中华人民共和国']; // 关键词是一个数组

  let hightlightText = text.replace(new RegExp(`(${keyword.join('|')})`, 'g'), `<span class="keyword-match">$1</span>`);

  // 通过innerHTML写入匹配后的内容
  content.innerHTML = hightlightText;
</script>

<script>
  const content2 = document.getElementById('content2');
  const text2 = '我是中国人,我爱中华人民共和国,中华人民共和国万岁!';
  const keyword2 = ['中华人民共和国', '中华', '我']; // 将字数多的关键词放到前面,保证字数多的关键词优先匹配

  let hightlightText2 = text.replace(new RegExp(`(${keyword2.join('|')})`, 'g'), `<span class="keyword-match">$1</span>`);

  content2.innerHTML = hightlightText2;
</script>
</body>

效果如下:

匹配优先级问题

富文本高亮

说完了普通文本的高亮,我们来说说富文本的高亮。所谓富文本,即待高亮的字符串不再是单独的文本,而是html代码。如果你需要富文本编辑器,可以考虑百度的UEditor,富文本编辑器生成的代码就是可以直接插入到页面的html串。

// 普通文本
我是中国人,我爱中华人民共和国,中华人民共和国万岁!

// 富文本,即html串
<div>我是<span style="font-size: 20px; font-weight: bold; background-color: cyan;">中国人</span>,我爱中华人民共和国,中华人民共和国万岁!</div>
<body>
富文本高亮
<br />
<br />

<h3>未高亮</h3>
<div id="text">
  <div style="font-style: italic;">我是<span style="font-size: 20px; font-weight: bold; background-color: cyan;">中国人</span>,我爱中华人民共和国,中华人民共和国万岁!</div>
</div>

<br />
<h3>高亮效果</h3>
<div id="content"></div>

<script>
  const content = document.getElementById('content');
  const text = document.getElementById('text').innerHTML; // 富文本串
  const keyword = ['中华人民共和国', '中华', '我'];

  let hightlightText = text.replace(new RegExp(`(${keyword.join('|')})`, 'g'), `<span class="keyword-match">$1</span>`);

  content.innerHTML = hightlightText;
</script>

</body>

富文本高亮

这里的高亮实现直接使用了前面普通文本高亮,似乎也能正常工作。但前提是这个富文本串太简单了。我们现在考虑这样一种情况,如果富文本串中的元素属性具有完全匹配关键词的内容,会发生什么呢?

// 待高亮富文本串
<div style="font-style: italic;" data-attr="中华人民共和国">我是<span style="font-size: 20px; font-weight: bold; background-color: cyan;">中国人</span>,我爱中华人民共和国,中华人民共和国万岁!</div>

对于上面的字符串,直接使用前面的高亮逻辑,得到的字符串是:

<div style="font-style: italic;" data-attr="<span class="keyword-match">中华人民共和国</span>"><span class="keyword-match">我</span>是<span style="font-size: 20px; font-weight: bold; background-color: cyan;">中国人</span>,<span class="keyword-match">我</span>爱<span class="keyword-match">中华人民共和国</span>,<span class="keyword-match">中华人民共和国</span>万岁!</div>

显然,第一个div属性中的“中华人民共和国”不应该被匹配到。如果直接匹配会破坏原来的富文本串,使之不再是一个有效的html串。这是富文本高亮的最大难题,那这个难题怎么解决呢?

当然,第一反应可能就是改正则。但根据个人经验,对于一个模式内包含自己时,正则几乎无能为力。比如下面我们常见的字符串结构:

// 这是常见的less语法。现在如果要求使用正则将最未的选择器名称加上“$”,大家可以试一下,能否做到
// 原串
@media (max-width: 600px) {
  .header {
    .hd {
      width: 100px;
    }

    .bd {
      background-color: #fff;
    }
  }
}

// 要求结果
@media (max-width: 600px) {
  .header {
    .hd$ { // 加上$
      width: 100px;
    }

    .bd$ {
      background-color: #fff;
    }
  }
}

对于富文本串,它也可能是模式自包含的字符串类型,比如

// 这是一个标准得不能再标准的html串了
<div data-html="<div>hello world!</div>">hello world!</div>

如果要使用正则去匹配上面字符串的hello内容,这个正则应该怎么写?先提醒一下,真正的富文本串要比这复杂太多太多,真实的用户输入也比这复杂太多太多。我当时做富文本高亮时,遇到这个问题也是一时找不到办法。某天,突然灵光一闪,不要硬碰硬啊,曲线救国嘛(这其实应该早就想到,只是走入正则的死胡同了):如果能够先把富文本串中的html标签去掉,剩下的不就是普通文本了吗?匹配普通文本简直是不要太简单了哦。匹配完成后,再把去掉的html还原,就完成高亮匹配了。不过这里有几个难题:

  • 如何去掉html标签。别笑,这真心难,不信你试下

  • 占位符一定要够特殊,不能在关键词匹配时被破坏

  • 如何还原html串

富文本高亮原理

根据上面的思路,完成了新一版的高亮逻辑,代码如下:

富文本高亮,关键词:['中华人民共和国', '中华', '我'];
<br />

<h3>未高亮</h3>
<div id="text">
  <div style="font-style: italic;" data-attr="中华人民共和国">我是<span style="font-size: 20px; font-weight: bold; background-color: cyan;">中国人</span>,我爱中华人民共和国,中华人民共和国万岁!</div>
</div>
<h3>高亮效果</h3>
<div id="content"></div>

<script>
  const content = document.getElementById('content');
  const text = document.getElementById('text').innerHTML; // 富文本串
  const keyword = ['中华人民共和国', '中华', '我'];

  let hightlightText = hightlightKeyword(text, keyword.join('|'));

  content.innerHTML = hightlightText;

  /**
   * 高亮
   * @param input - 待高亮的富文本串
   * @param keyword - 由关键词生成的匹配串,格式 'xxxx|xxx|x'
   * @returns {string}
   */
  function hightlightKeyword(input, keyword) {
    let store = {
      length: 0
    };

    try {
      return input
          .replace(/^\s+/, ' ') // 去掉多余的空白

          // 去掉Html标签,并使用特殊占位符占位,方便后面还原
          .replace(/(<\w+[^>]*?>)|(<\/\w+[^>]*?>)/g, function(match) {
            var key = '\t' + store.length++; // 注意这里使用了\t

            store[key] = match;
            return key;
          })

          // 关键词高亮
          .replace(new RegExp('(' + keyword + ')', 'gi'), '<span class="keyword-match">' + '$1' + '</span>')

          // html标签还原
          .replace(/\t\d+/g, function(match) {
            return store[match] || '';
          });
    } catch (e) {
      return input;
    }
  }

</script>

上面的hightlightKeyword函数已经能够满足大多数情况下富文本高亮,博主曾经负责的一个项目,使用这个高亮逻辑安全运行1年多,也没出现大问题。但其实,上面的高亮逻辑还是有bug,对于一些十分特殊的富文本串还是存在问题,比如下面的富文本串

<div>
    <div data-html="<div>hello world!</div>">
      hello world!
      <div data-html="<div>hello world!</div>">hello world!</div>
    </div>
</div>

经过多次尝试,碰壁,最后决定借用浏览器来将html转成DOM,通过DOM操作来完成高亮。下面是高亮终极版本

<body>
富文本高亮,关键词:['中华人民共和国', '中华', '我'];
<br />

<h3>未高亮</h3>
<div id="text">
  <div data-html="<div>hello world!</div>">hello world!</div>
  <div>
    <div data-html="<div>hello world!</div>">
      hello world!
      <div data-html="<div>hello world!</div>">hello world!</div>
    </div>
  </div>
  <div style="font-style: italic;" data-attr="中华人民共和国">
    我是
    <span style="font-size: 20px; font-weight: bold; background-color: cyan;">中国人</span>
    ,我爱中华人民共和国,中华人民共和国万岁!
  </div>
</div>

<h3>高亮效果</h3>
<div id="content"></div>

<script>
  const content = document.getElementById('content');
  const text = document.getElementById('text').innerHTML; // 富文本串
  const keyword = ['中华人民共和国', 'hello', '中华', '我'];

  let hightlightText = hightlightKeyword(text, keyword.join('|'));

  content.innerHTML = hightlightText;

  /**
   * 借助浏览器完成高亮。
   * 深度优先遍历所有的节点,对文本节点进行高亮
   *
   * @param input - 待高亮的富文本串
   * @param keyword - 由关键词生成的匹配串,格式 'xxxx|xxx|x'
   * @returns {string}
   */

  function hightlightKeyword(html, keyword) {
    // 复制一个节点去进行遍历操作
    let wrap = document.createElement('div');

    wrap.innerHTML = html;

    return DFSTraverseAndHightlight(wrap);

    function DFSTraverseAndHightlight (node) {
      const rootNodes = node.childNodes;
      const childNodes = Array.from(rootNodes);

      for(let i = 0, len = childNodes.length; i < len; i++) {
        const node = childNodes[i];

        // 文本节点,要进行高亮
        if (node.nodeType === 3) {
          let span = document.createElement('span');
          let a = span.innerHTML = node.nodeValue.replace(new RegExp(`(${keyword})`, 'g'), `<span class="keyword-match">$1</span>`);
          console.log(node.nodeValue);
          node.parentNode.insertBefore(span, node);
          node.parentNode.removeChild(node);
        }

        //文本节点不会有childNodes属性,如果有子节点,继续遍历
        if (node.childNodes.length) {
          DFSTraverseAndHightlight(node);
        }
      }

      return node.innerHTML;
    }
  }

</script>
</body>

简单的分词实现

文章最开始说了,分词逻辑一般是后台通过专门的库来完成的。但其实,前端也可以自己实现一个简单的分词,只不过会产生许多无意义的词而已,思路大家一看就明白了

// 简单,粗暴分词
function splitWord(word) {
  var len = word.length,
    splitWordList = word.split(''); // 一字分组


  // 分词
  for(var i = 2; i <= len; i++) {
    for (var j = 0; j + i <= len; j++) {
        splitWordList.push(word.slice(j, j+i));
    }
  }

  // 必须把长度最长的放到最前面,否则会造成匹配不全的情况
  return splitWordList.reverse();
}

运行效果

简单分词

小结

本文主要从前端的角度,介绍了如何实现高亮功能,包括普通文本高亮和富文本高亮。关键的知识点是:

  • 利用new RegExp((${keyword}), 'g')方式动态创建正则

  • 利用str.replace(regexp, <span class="keyword-match">$1</span>)为匹配加上高亮样式

  • 最长的关键词一定要优先匹配,否则会造成匹配不全的情况

  • 富文本匹配,用正则很难做到100%精确。但如果有浏览器环境,可以借助浏览器先将富文本串转换成DOM,通过DOM操作来实现一个更精确的富文本高亮

  • 前端也可以自己实现分词,只不过会产生大量无意义词组而已

原文地址:http://www.u3xyz.com/#/detail/22

查看原文

赞 33 收藏 84 评论 7

Tenadolanter 提出了问题 · 4月14日

怎么理解,字典是非线性的数据结构?

怎么理解,字典是非线性的数据结构?RT

关注 3 回答 2

Tenadolanter 提出了问题 · 3月22日

字符串的位运算,是怎么执行的

如代码:

10 | 5  // 15
true | '' // 1
'abc' | ''  // 0

那么位运算是怎么进行的呢?字符串的位运算是怎么处理的?

关注 4 回答 3

Tenadolanter 赞了文章 · 2020-11-08

@vue/cli 项目编译重复命中缓存问题解析

@vue/cli 项目编译重复命中缓存问题解析

文章首发于个人blog,欢迎关注~

背景

最近遇到一个更新了 package,但是本地编译打包后没有更新代码的情况,先来复现下这个 case 的流程:

  1. A 同学在 npm 上发布了0.1.0版本的 package;
  2. B 同学开发了一个新的 feature,并发布0.2.0版本;
  3. C 同学将本地的0.1.0版本升级到0.2.0版本,并执行npm run deploy,代码经过 webpack 本地编译后发布到测试环境。但是测试环境的代码并不是最新的 package 的内容。但是在 node_modules 当中的 package 确实是最新的版本。

这个问题其实在社区里面有很多同学已经遇到了:

TL;DR(流程分析较复杂,可一拉到底)

发现 & 分析问题

翻了那些 issue 后,基本知道了是由于 webpack 在编译代码过程中走到 cache-loader 然后命中了缓存,这个缓存是之前编译的老代码,既然命中了缓存,那么就不会再去编译新的代码,于是最终编译出来的代码并不是我们所期望的。所以这个时候 cd node_modules && rm -rf .cache && npm run deploy,就是进入到 node_modules 目录,将 cache-loader 缓存的代码全部清除掉,并重新执行部署的命令,这些编译出来的代码肯定是最新的。

既然知道了问题的所在,那么就开始着手去分析这个问题的来龙去脉。这里我也简单的介绍下 cache-loader 的 workflow 是怎么进行的:

  1. 在 cache-loader 上部署了 pitch 方法(有关 loader pitch function 的用法可戳我),在 pitch 方法内部会根据生成的 cacheKey(例如abc) 去寻找 node_modules/.cache 文件夹下的缓存的 json 文件(abc.json)。其中 cacheKey 的生成支持外部传入 cacheIdentifier 和 cacheDirectory 具体参见官方文档
// cache-loader 内部定义的默认的 cacheIdentifier 及 cacheDirectory
const defaults = {
  cacheContext: '',
  cacheDirectory: findCacheDir({ name: 'cache-loader' }) || os.tmpdir(),
  cacheIdentifier: `cache-loader:${pkg.version} ${env}`,
  cacheKey,
  compare,
  precision: 0,
  read,
  readOnly: false,
  write
}

function cacheKey(options, request) {
  const { cacheIdentifier, cacheDirectory } = options;
  const hash = digest(`${cacheIdentifier}\n${request}`);

  return path.join(cacheDirectory, `${hash}.json`);
}

如果缓存文件(abc.json)当中记录的所有依赖以及这个文件都没发生变化,那么就会直接读取缓存当中的内容,并返回且跳过后面的 loader 的正常执行。一旦有依赖或者这个文件发生变化,那么就正常的走接下来的 loader 上部署的 pitch 方法,以及正常的 loader 处理文本文件的流程。

cache-loader 在决定是否使用缓存内容时是通过缓存内容当中记录的所有的依赖文件的 mtime 与对应文件最新的 mtime 做对比来看是否发生了变化,如果没有发生变化,即命中缓存,读取缓存内容并跳过后面的 loader 的处理,否则走正常的 loader 处理流程。

function pitch(remainingRequest, prevRequest, dataInput) {
  ...
  // 根据 cacheKey 的标识获取对应的缓存文件内容
  readFn(data.cacheKey, (readErr, cacheData) => {
    async.each(
      cacheData.dependencies.concat(cacheData.contextDependencies), // 遍历所有依赖文件路径
      (dep, eachCallback) => {
        // Applying reverse path transformation, in case they are relatives, when
        // reading from cache
        const contextDep = {
          ...dep,
          path: pathWithCacheContext(options.cacheContext, dep.path),
        };

        // fs.stat 获取对应文件状态
        FS.stat(contextDep.path, (statErr, stats) => {
          if (statErr) {
            eachCallback(statErr);
            return;
          }

          // When we are under a readOnly config on cache-loader
          // we don't want to emit any other error than a
          // file stat error
          if (readOnly) {
            eachCallback();
            return;
          }

          const compStats = stats;
          const compDep = contextDep;
          if (precision > 1) {
            ['atime', 'mtime', 'ctime', 'birthtime'].forEach((key) => {
              const msKey = `${key}Ms`;
              const ms = roundMs(stats[msKey], precision);

              compStats[msKey] = ms;
              compStats[key] = new Date(ms);
            });

            compDep.mtime = roundMs(dep.mtime, precision);
          }
          
          // 对比当前文件最新的 mtime 和缓存当中记录的 mtime 是否一致
          // If the compare function returns false
          // we not read from cache
          if (compareFn(compStats, compDep) !== true) {
            eachCallback(true);
            return;
          }
          eachCallback();
        });
      },
      (err) => {
        if (err) {
          data.startTime = Date.now();
          callback();
          return;
        }
        ...
        callback(null, ...cacheData.result);
      }
    );
  })
}
  1. 通过 @vue/cli 初始化的项目内部会通过脚手架去完成 webpack 相关的配置,其中针对 vue SFC 文件当中的script blocktemplate block在代码编译构建的流程当中都利用了 cache-loader 进行了缓存相关的配置工作。
// @vue/cli-plugin-babel
module.export = (api, options) => {
  ...
  api.chainWebpack(webpackConfig => {
    const jsRule = webpackConfig.module
      .rule('js')
        .test(/\.m?jsx?$/)
        .use('cache-loader')
          .loader(require.resolve('cache-loader'))
          .options(api.genCacheConfig('babel-loader', {
            '@babel/core': require('@babel/core/package.json').version,
            '@vue/babel-preset-app': require('@vue/babel-preset-app/package.json').version,
            'babel-loader': require('babel-loader/package.json').version,
            modern: !!process.env.VUE_CLI_MODERN_BUILD,
            browserslist: api.service.pkg.browserslist
          }, [
            'babel.config.js',
            '.browserslistrc'
          ]))
          .end()
    jsRule
      .use('babel-loader')
        .loader(require.resolve('babel-loader'))
  })
  ...
}

// @vue/cli-serive/lib/config
module.exports = (api, options) => {
  ...
  api.chainWebpack(webpackConfig => {
    const vueLoaderCacheConfig = api.genCacheConfig('vue-loader', {
      'vue-loader': require('vue-loader/package.json').version,
      /* eslint-disable-next-line node/no-extraneous-require */
      '@vue/component-compiler-utils': require('@vue/component-compiler-utils/package.json').version,
      'vue-template-compiler': require('vue-template-compiler/package.json').version
    })

    webpackConfig.module
      .rule('vue')
        .test(/\.vue$/)
        .use('cache-loader')
          .loader(require.resolve('cache-loader'))
          .options(vueLoaderCacheConfig)
          .end()
        .use('vue-loader')
          .loader(require.resolve('vue-loader'))
          .options(Object.assign({
            compilerOptions: {
              whitespace: 'condense'
            }
          }, vueLoaderCacheConfig))
    ...
  })
}

即:

  • 对于script block来说经过babel-loader的处理后经由cache-loader,若之前没有进行缓存过,那么新建本地的缓存 json 文件,若命中了缓存,那么直接读取经过babel-loader处理后的 js 代码;
  • 对于template block来说经过vue-loader转化成 renderFunction 后经由cache-loader,若之前没有进行缓存过,那么新建本地的缓存 json 文件,若命中了缓存,那么直接读取 json 文件当中缓存的 renderFunction。

上面对于 cache-loader 和 @vue/cli 内部工作原理的简单介绍。那么在文章一开始的时候提到的那个 case 具体是因为什么原因导致的呢?

事实上在npm 5.8+版本,npm 将发布的 package 当中包含的文件的 mtime 都统一置为了1985-10-26T08:15:00.000Z(可参见 issue-20439)

A 同学(npm版本为6.4.1)发布了0.1.0的版本后,C 同学安装了0.1.0版本,本地构建后生成缓存文件记录的文件 mtime 为1985-10-26T08:15:00.000Z。B 同学(npm版本为6.2.1)发布了0.2.0,C 同学安装0.2.0版本,本地开始构建,但是经由 cache-loader 的过程当中,cache-loader 通过对比缓存文件记录的依赖的 mtime 和新安装的 package 的文件的 mtime,但是发现都是1985-10-26T08:15:00.000Z,这样也就命中了缓存,即直接获取上一次缓存文件当中所包含的内容,而不会对新安装的 package 的文件进行编译。

针对这个问题,@vue/cli 在19年4月的3.7.0版本(具体代码变更的内容请戳我)当中也做了相关的修复性的工作,主要是将:package-lock.jsonyarn.lockpnpm-lock.yaml,这些做版本控制文件也加入到了 hash 生成的策略当中:

// @vue/cli-service/lib/PluginAPI.js

class PluginAPI {
  ...
  genCacheConfig(id, partialIdentifier, configFiles = []) {
    ...
    if (!Array.isArray(configFiles)) {
      configFiles = [configFiles]
    }
    configFiles = configFiles.concat([
      'package-lock.json',
      'yarn.lock',
      'pnpm-lock.yaml'
    ])

    const readConfig = file => {
      const absolutePath = this.resolve(file)
      if (!fs.existsSync(absolutePath)) {
        return
      }

      if (absolutePath.endsWith('.js')) {
        // should evaluate config scripts to reflect environment variable changes
        try {
          return JSON.stringify(require(absolutePath))
        } catch (e) {
          return fs.readFileSync(absolutePath, 'utf-8')
        }
      } else {
        // console.log('the absolute path is:', fs.readFileSync(absolutePath, 'utf-8'))
        return fs.readFileSync(absolutePath, 'utf-8')
      }
    }

    // 获取版本控制文件的文本内容
    for (const file of configFiles) {
      const content = readConfig(file)
      if (content) {
        variables.configFiles = content.replace(/\r\n?/g, '\n')
        break
      }
    }

    // 将带有版本控制文件的内容加入到 hash 算法当中,生成新的 cacheIdentifier
    // 并传入 cache-loader(缓存文件的 cacheKey 依据这个 cacheIdentifier 来生成,👆上文有说明)
    const cacheIdentifier = hash(variables)
    return { cacheDirectory, cacheIdentifier }
  }
}

这样来做的核心思想就是:当你升级了某个 package 后,相应的版本控制文件也会对应的更新(例如 package-lock.json),那么再一次进行编译流程时,所生成的缓存文件的 cacheKey 就会是最新的,因为也就不会命中缓存,还是走正常的全流程的编译,最终打包出来的代码也就是最新的。

不过这次升级后,还是有同学在社区反馈命中缓存,代码没有更新的问题,而且出现的 case 是 package 当中需要走 babel-loader 的 js 会遇到命中缓存不更新的情况,但是 package 当中被项目代码引用的 vue 的 template 文件不会出现这种情况。后来我调试了下@vue/cli-service/lib/PluginAPI.js的代码,发现代码在读取多个配置文件的过程中,一旦获取到某个配置文件的内容后就不再读取后面的配置文件的内容了,这样也就导致就算package-lock.json发生了更新,但是因为在编译流程当中并未读取package-lock.json这个文件的最新的内容话,那么也就不会生成新的 cacheKey,仍然会出现命中缓存的问题:

// 针对需要走 babel-loader 流程的配置文件为:
['babel.config.js', '.browserslistrc', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml']
// 针对需要缓存的 vue template 的配置文件为:
['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml']

// @vue/cli-service/lib/PluginAPI.js
class PluginAPI {
  ...
  genCacheConfig(id, partialIdentifier, configFiles = []) {
    ...
    if (!Array.isArray(configFiles)) {
      configFiles = [configFiles]
    }
    configFiles = configFiles.concat([
      'package-lock.json',
      'yarn.lock',
      'pnpm-lock.yaml'
    ])

    const readConfig = file => {
      ...
    }

    // 一旦获取到某个配置文件的内容后,就直接跳出了 for ... of 的循环
    // 那么也就不会继续获取其他配置文件的内容,
    // 所以对于处理 js 文件的流程来说,因为读取了 babel.config.js 的内容,那么也就不会再去获取更新后的 packge-lock.json 文件内容
    // 但是对于处理 vue template 的流程来说,配置文件当中第一项就位 package-lock.json,这种情况下会获取最新的 package-lock.json 文件,所以对于 vue template 的不会出现升级了 package 内容,但是会因为命中缓存,导致编译代码不更新的情况。
    for (const file of configFiles) {
      const content = readConfig(file)
      if (content) {
        variables.configFiles = content.replace(/\r\n?/g, '\n')
        break
      }
    }

    const cacheIdentifier = hash(variables)
    return { cacheDirectory, cacheIdentifier }
  }
}

不过就在前几天,@vue/cli 的作者也重新看了下这个有关 vue template 正常,但是对于 js 命中缓存的原因,并针对这个问题进行了修复(具体代码内容变更请戳我),这次的代码变更就是通过 map 循环(而非 for ... of 循环读取到内容后直接 break),这样去确保所有的配置文件都被获取得到:

variables.configFiles = configFiles.map(file => {
  const content = readConfig(file)
  return content && content.replace(/\r\n?/g, '\n')
}

目前在@vue/cli-service@4.1.2版本中已经进行了修复。

以上就是通过 @vue/cli 初始化的项目,在升级 package 的过程中,cache-loader 命中缓存,新一轮代码编译生成非最新代码问题的分析。

总结 & 解决方案

cache-loader 使用缓存文件(node_modules/.cache)记录了不同依赖文件的 mtime,并通过对比缓存记录的 mtime 和最新文件的 mtime 是否发生了变化来觉得是否使用缓存。由于npm@5.8.0之后,每次新发布的 package 内部所包含的文件的 mtime 都被重置为1985-10-26T08:15:00.000Z,导致 cache-loader 这个对比 mtime 的策略失效。因为 @vue/cli-service 从3.7.0(19年4月)版本针对这个问题进行了第一次的修复,核心思想就是将package-lock.json这样的版本控制文件的内容纳入到了生成缓存文件的 cacheKey 的 hash 算法当中,每次升级 package 后,package-lock.json也会随之变化,这样会生成新的 cacheKey,进而不会命中缓存策略,这样也就解开了由于 npm 重置 mtime 而带来的重复命中缓存的问题,但是3.7.0版本的修复是有bug的,主要就是有些项目当中package-lock.json(由项目结构决定)这样的版本控制文件根本就没有被读取,导致 cache-loader 生成的 cacheKey 依然没有变化。然后在前几天(2020年1月28日),@vue/cli 的作者重新针对这个问题进行优化,确保package-lock.json版本控制文件能被读取到,从而避免 cacheKey 不变的问题,于@vue/cli-service@4.1.2版本中完全修复了重复命中缓存的问题。

这里比较有意思的一点就是这个问题的出现需要满足2个条件:

  1. 发布 package 的同学使用的 npm 的版本需要高于 5.8.0;
  2. 使用 package 的同学使用的 @vue/cli-service 的版本要低于 4.1.2 版本

比如我一直使用的 node 版本为 8.11.0,对应的 npm 版本为 5.6.0,那么经由我去修改发布的所有 package 所包含的文件的 mtime 都是被修改的那一刻,其他人升级到我发布的版本后,是不会出现重复命中缓存的问题。

不过既然问题被梳理清楚后,那么本地编译的过程避免出现这个问题的解决方式:

  1. 如果你的项目使用 @vue/cli@4.x 初始化的,那么直接升级 @vue/cli-service 到 4.1.2 版本即可;
  2. 如果你不想升级 @vue/cli-service 的版本(特别是你是使用 @vue/cli@3.x 版本初始化项目的同学,可能会出现兼容性问题,具体可自行测试),那么可以在每次本地编译开始前,删除掉node_module/.cache文件夹,例如将本地编译构建的npm script修改为rm -rf node_module/.cache && vue-cli-service build。(不过对于大型的项目来说,少了这部分的缓存内容的话,编译速度还是会受到一定的影响的。)
查看原文

赞 2 收藏 0 评论 0

Tenadolanter 赞了回答 · 2020-11-08

有办法给slot加事件吗

定义:

  <slot
            name="item"
            :item="item"
            :selectKeysObject="selectKeysObject"
            :onCheck="() => onSelect({ id: item[sourceId], item })"
          ></slot>

使用:

 <template #item="{ item, selectKeysObject, onCheck }">
          <div :class="$style.list">
            <div :class="$style.check" @click="onCheck">
              <simple-checkbox :checked="Boolean(selectKeysObject[item.sourceId])"></simple-checkbox>
            </div>
            <div :class="$style.img">
              <loading-image :url="item.sceneImg" />
            </div>
            <div>{{ item.absTimestamp }}</div>
            <div :class="$style.deviceName" :title="item.deviceName">{{ item.deviceName }}</div>
          </div>
        </template>

利用箭头函数,在使用的地方解析出来直接使用

微信图片_20200416173847.png

关注 6 回答 5

Tenadolanter 回答了问题 · 2020-07-27

解决关于Vue-cli3.0 的配置。pages.chunks的参数的意义

1、产生的效果

pages.chunks 取自webpack插件 HtmlWebpackPlugin 里面的 chunks 属性,在打包的时候会被插入到入口文件index.html里面,他大概长这样

<!DOCTYPE html>
<html lang="en">
  ...
  <body>
    <div id="app"></div>
    <!-- 注意下面这个家伙,就是它,文件名以chunk-vendors开头 -->
    <script type="text/javascript" data-original="/module_a/js/chunk-vendors.72b338bb.js"></script>
  </body>
</html>

2、从哪里看配置

如果你使用@vue/cli, 使用命令 vue inspect可以查看到配置里面有这么一段代码:

new HtmlWebpackPlugin(
  {
    title: 'test',
    templateParameters: function () { /* omitted long function */ },
    // 看他看他
    chunks: [
      'module_a',
      'chunk-vendors',
      'chunk-common'
    ],
    template: './public/index.html',
    filename: 'module_a.html',
    minify: {},
    inject: true
  }
)

3、为什么配置的是chunk-vendors和chunk-common,而不是其他?

继续使用 vue inspect, 可以看到有这样一段代码:

optimization: {
    splitChunks: {
      cacheGroups: {
        vendors: {
          name: 'chunk-vendors',
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          chunks: 'initial'
        },
        common: {
          name: 'chunk-common',
          minChunks: 2,
          priority: -20,
          chunks: 'initial',
          reuseExistingChunk: true
        }
      }
    }
}

Vue对生产环境代码的打包进行了处理,对 node_modules 和使用超过两次的文件提取到公共部分,生成了名称以 chunk-vendorschunk-common 开头的文件,所以我们需要在模块入口文件里面引入这两个模块。

关注 3 回答 3

Tenadolanter 赞了文章 · 2020-05-27

简要总结microtask和macrotask

前言

我是在做前端面试题中看到了setTimeout和Promise的比较,然后第一次看到了microtask和macrotask的概念,在阅读了一些文章之后发现没有一个比较全面易懂的文章,所以我尝试做一个梳理性的总结.

这道经典的面试题引起了我的兴趣

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');

JavaScript的事件循环机制

首先我们先弄清楚setTimeout和Promise的共同点,也就是我第一次的看到那道面试题的疑惑点.

JavaScript 主线程拥有一个 执行栈 以及一个 任务队列,主线程会依次执行代码,当遇到函数时,会先将函数 入栈,函数运行完毕后再将该函数 出栈,直到所有代码执行完毕。

上面的例子的执行栈执行顺序应该是这样的

console.log('script start');
console.log('script end');
Promise.resolve();

而任务队列的执行顺序应该是这样的

Promise.then(function() {
  console.log('promise1');
});
Promise.then(function() {
  console.log('promise2');
});
setTimeout(function() {
  console.log('setTimeout');
}, 0);

而主线程则会在 清空当前执行栈后,按照先入先出的顺序读取任务队列里面的任务。

众所周知setTimeout和Promise.then()都属于上述异步任务的一种,那到底为什么setTimeout和Promise.then()会有顺序之分,这就是我想分析总结的问题所在了.

macrotasks(tasks) 和 microtasks

tasks

tasks的作用是为了让浏览器能够从内部获取javascript / dom的内容并确保执行栈能够顺序进行。

tasks的调度是随处可见的,例如解析HTML,获得鼠标点击的事件回调等等,在这个例子中,我们所迷惑的setTimeout也是一个tasks.

microtasks

microtasks通常用于在当前正在执行的脚本之后直接发生的事情,比如对一系列的行为做出反应,或者做出一些异步的任务,而不需要新建一个全新的tasks。

只要执行栈没有其他javascript在执行,在每个tasks结束时,microtasks队列就会在回调后处理。在microtasks期间排队的任何其他microtasks将被添加到这个队列的末尾并进行处理。

microtasks包括mutation observer callbacks,就像上例中的promise callbacks一样。

所以上面的例子执行顺序的实质是

  • tasks =>start end以及resolve
  • microtasks =>promise1和promise2
  • tasks =>setTimeout

具体应用

需要注意的是,在两个tasks之间,浏览器会重新渲染。这也是我们需要了解tasks和microtasks的一个非常重要的原因.

Vue 中如何使用 MutationObserver 做批量处理? - 顾轶灵的回答 - 知乎

根据 HTML Standard,在每个 task 运行完以后,UI 都会重渲染,那么在 microtask 中就完成数据更新,当前 task 结束就可以得到最新的 UI 了。反之如果新建一个 task 来做数据更新,那么渲染就会进行两次。

浏览器兼容问题

在__Microsoft Edge__, Firefox 40__, __iOS Safari 以及 desktop Safari 8.0.8 中setTimeout会先于Promise

该例子来自Jake Archibald-->Tasks, microtasks, queues and schedules,其中有动画来展现tasks和microtasks的具体工作流程,十分推荐阅读

//html
<div class="outer">
  <div class="inner"></div>
</div>
// Let's get hold of those elements
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');

// Let's listen for attribute changes on the
// outer element
new MutationObserver(function() {
  console.log('mutate');
}).observe(outer, {
  attributes: true
});

// Here's a click listener…
function onClick() {
  console.log('click');

  setTimeout(function() {
    console.log('timeout');
  }, 0);

  Promise.resolve().then(function() {
    console.log('promise');
  });

  outer.setAttribute('data-random', Math.random());
}

// …which we'll attach to both elements
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);

在这个例子中,不同浏览器的log是不同的,如下所示

ChromeFirefoxSafariedge
clickclickclickclick
promisemutatemutateclick
mutateclickclickmutate
clickmutatemutatetimeout
promisetimeoutpromisepromise
mutatepromisepromisetimeout
timeoutpromisetimeoutpromise
timeouttimeouttimeout\

事实上Chrome是正确的,而且由此可发现microtasks并不是在tasks的结束阶段开始执行,而是在tasks中回调结束之后(只要没有正在执行的JavaScript代码)

总结

tasks会顺序执行,浏览器会在执行间隔重新渲染

microtasks会顺序执行,执行时机为

在没有JavaScript代码执行的callback之后

在每一个tasks之后


由于我是前端初学者,对于JavaScript还很不熟悉,对事件循环的进程模型不是很了解,希望这篇文章能够帮助大家.

事件循环机制建议参考文章

阮一峰-->JavaScript 运行机制详解:再谈Event Loop

HTML Living Standard — Last Updated 9 April 2018

tasks建议参考文章

Jake Archibald-->Tasks, microtasks, queues and schedules

理解 JavaScript 中的 macrotask 和 microtask

setImmediate.js --A YuzuJS production

查看原文

赞 9 收藏 6 评论 0

Tenadolanter 赞了文章 · 2020-05-25

浅析toString与valueOf

valueOf():返回最适合该对象类型的原始值;
toString(): 将该对象的原始值以字符串形式返回。

这两个方法一般是交由JS去隐式调用,以满足不同的运算情况。
在数值运算里,会优先调用valueOf(),在字符串运算里,会优先调用toString()。

let e2 = {
        n : 2,
        toString : function (){
            console.log('this is toString')
            return this.n
        },
        valueOf : function(){
            console.log('this is valueOf')
            return this.n*2
        }
    }
    alert(e2) //  2  this is toString
    alert(+e2)  // 4 this is valueOf
    alert(''+e2) // 4 this is valueOf
    alert(String(e2)) // 2 this is toString
    alert(Number(e2)) // 4 this is valueOf
    alert(e2 == '4') // true  this is valueOf
    alert(e2 === 4) //false ===操作符不进行隐式转换

第三个alert,之所以会调用valueOf是因为:在有运算操作符的情况下,valueOf的优先级高于toString

看接下来的两给例子:

let e3 = {
        n : 2,
        toString : function (){
            console.log('this is toString')
            return this.n
        }
    }
    alert(e3) //  2  this is toString
    alert(+e3)  // 2 this is toString
    alert(''+e3) // 2 this is toString
    alert(String(e3)) // 2 this is toString
    alert(Number(e3)) // 2 this is toString
    alert(e3 == '2') // true  this is toString
    alert(e3 === 2) //false  ===操作符不进行隐式转换
    
    
   Object.prototype.toString = null; 
   let e4 = {
        n : 2,
        valueOf : function(){
            console.log('this is valueOf')
            return this.n*2
        }
    }
    alert(e4) //  4 this is valueOf
    alert(+e4)  // 4 this is valueOf
    alert(''+e4) // 4 this is valueOf
    alert(String(e4)) // 4 this is valueOf
    alert(Number(e4)) // 4 this is valueOf
    alert(e4 == '4') // true  this is valueOf
    alert(e4 === 4) //false  ===操作符不进行隐式转换
    
 

修改过的toString与ValueOf调用顺序

 哪个修改先调用哪个

总结:

  1. 进行对象转换时(alert(e2)),优先调用toString方法,如没有重写toString将调用valueOf方法,如果两方法都不没有重写,但按Object的toString输出。

  2. 进行强转字符串类型时将优先调用toString方法,强转为数字时优先调用valueOf。

  3. 在有运算操作符的情况下,valueOf的优先级高于toString。

查看原文

赞 7 收藏 6 评论 0

Tenadolanter 赞了文章 · 2020-05-23

chrome 开发者工具 - local overrides

使用chrome 作为本地网络服务

chrome 65+ 新功能, 使用我们自己的本地资源覆盖网页所使用的资源,可以使用本地css文件覆盖网页的css文件,修改样式。

类似的,使用DevTools的工作区设置持久化,将本地的文件夹映射到网络,在chrome开发者功能里面对css 样式的修改,都会直接改动本地文件,页面重新加载,使用的资源也是本地资源,达到持久化的效果。

然后就是,很少使用的使用 local override 功能,来搭建一个本地的静态网页服务器
搭建过程非常简单,根据原文中的步骤(假设访问的域名 chromeserver.com):

  1. 搭建local overrider的根目录, C:/Dev/Web/chromelocal,
  2. 在根目录中新建文件夹,以 chromeserver.com 命名,进入该文件目录,新建一个 index.html
  3. 打开chrome 开发者工具, sources --> Overrides --> 勾选 Enable Local Overriders --> 点击 Select folder for overrides ,选择文件 C:/Dev/Web/chromelocal
  4. 结果图 : overrides.png

在打开了 开发者工具的tab中,访问 http://chromeserver.com/,就可以看到页面了。

扩展:

1. 设置持久化。

经过上面的设置后,尝试打开其他网页, 在Elements 面板中进行的样式,

change-class

然后,在sources 中,就会自动生成对应域名的文件(在本地磁盘上新建文件)
图片描述

新开tab 中重新打开页面,修改依然生效,已经将本地的资源文件映射到了网络,跟上面工作区设置持久化的效果类似。

在chrome 中点击对应index.css文件,可以在右侧面板中,直接修改未格式化的文件
indexcss

或者在本地磁盘中,使用IDE打开文件 C:/Dev/Web/chromelocal/static.segmentfault.com/v-5badf55c/index/css/index.css,格式化文件后,再修改里面的内容,也可以自动刷新,显示文件更改。

2. 模拟接口数据

对于返回json 数据的接口,可以利用该功能,简单模拟返回数据。

比如:
api 为 http://www.xxx.com/api/v1/list,

对应在chromelocal 目录下,新建文件 www.xxx.com/api/v1/list,list 文件中的内容,与正常接口返回格式相同,
图片描述

对象或者数组类型,从而覆盖掉原接口请求。

相关功能

workspaces ,chrome 很早就引入了 workspaces , 这个功能允许把DevTools 当做IDE使用,
只要在选择了 Add folder to workspace 后,允许资源访问,在chrome DevTools 中的改变,相应也会保存在本地版本中,跟使用文本编辑器类似的类似的功能。

在 Chrome 63 What's New In DevTools中 提及 workspace2.0 发布,增强用户体验,不过延期了,

查看原文

赞 18 收藏 11 评论 2

Tenadolanter 回答了问题 · 2020-05-20

有办法查看github上已经删除的分支吗

刚找回一个很久之前被删除的分支,也是因为幸运。

之前尝试使用git reflog命令,但是很不幸没找到这个分支对应的提交记录。

最后,是在sourceTree的远端origin里面找到了想要恢复的分支feature/recover_branch,最后在跳转到里面找到分支对应的commit id例如8888d814e

FCAD9952-845F-4EC2-8983-1591FC52751E.png

执行命令,恢复删除的分支,并推送到线上

# 根据commit id,本地新建分支
git checkout -b feature/recover_branch 8888d814e 

# 推送到远端
git push origin

关注 6 回答 2

认证与成就

  • 获得 21 次点赞
  • 获得 12 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 11 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-10-15
个人主页被 726 人浏览