日志

  • 使用 Webpack + Mocha 进行单元测试(一)

    使用 Webpack + Mocha 进行单元测试(一)

    我厂产品经过两年的打磨,目前功能基本稳定了,接下来的工作重点是用测试保证质量。除了继续丰富 E2E 测试,我还计划添加一些单元测试,用来确保一些重要函数和类正常工作。

    所以就要选择框架。开始想试试近期比较热门的 Jest,找了些文章看了看,发现也没啥吸引我的特色,E2E 和 UI 我厂的 Navlang 目前独步江湖,无出其右者,考虑到以前用 Mocha 写过一些单元测试,所以决定继续用 Mocha。

    但是已经很长时间没写了,所以想先研究下工具链。如果只是单文件的话,直接 mocha test.js 就好;如果测试文件用了 ESM,那么加上 @babel/register 也就够了,mocha —require @babel/register test.js

    但是我们项目用到了 Vue,还用 Webpack 里的 resolve 定义了很多目录,所以为了能够兼容项目代码,我必须要 Mocha 结合 Webpack,用这个关键词一搜索,得到两个结论:mocha-webpack 和 mocha-loader。

    前者已经一年多没更新了,而且要求 mocha@5,所以我就不想用了。转过头研究 mocha-loader,经过反复试验,加上阅读源码,我得出结论:

    mocha-loader 的目标是在 Webpack 里集成测试,比如边开发边测试,并不是我想要的。我想要的是:打包测试内容,整合测试框架,方便用户在 CI 系统里集中进行测试。

    所以我真正需要的,其实是:

    1. 创建一个独立的 webpack 配置文件,用来打包单元测试
    2. 打包后执行测试,返回测试结果

    基本上,今天大半天的工作是白费了。mocha-loader 的作者应该把目的写在 README 里的……

  • 解决 load-scripts.php jQuery is not defined 的问题

    解决 load-scripts.php jQuery is not defined 的问题

    不知道从哪天起,我的博客后台就坏了。没错,就是这个后台。写文章变成非常困难的一件事。各种功能都不好使,页面布局也混乱不堪。于是我打开开发者工具,看到一大堆报错,基本上都是 load-scripts.php jQuery is not defined。看起来是某些 jQuery 插件启动的时候 jQuery 还没完成加载。

    这就有点蛋疼了。我应该没有手动调整过这些脚本的加载,但是也说不定,我好像有把本地的 jQuery 换成 CDN 上的版本,但也不是很确定。不过我有好几台电脑,有些电脑上保留着可用的 JS 的缓存,我就勉强用着,反正也不常写文章。我试着 Google 这个问题,但可能关键词组合没选好,没能找到答案。WordPress 历史太悠久,跟 jQuery 相关的问题不胜枚举,搜索结果里噪音太多。

    结果前两天终于所有缓存都失效了,然后我就没法写文章了,于是必须解决这个问题了。没想到这次我很快找到了解答:

    Try adding define('CONCATENATE_SCRIPTS', false); to your wp-config.php file just below the define('DB_HOST' line.

    https://wordpress.org/support/topic/failed-to-load-jquery-at-load-scripts-php/

    使用后台的时候,WordPress 会试图把所有 JS 合并到一起,以便节省 HTTP 请求。这个设计思路没问题,但看起来他们的实现比较简单粗暴,只是简单的合并,并没有很好的检查依赖顺序,以至于可能导致后台功能失败。

  • 在 Node.js 12 中使用 ESM

    在 Node.js 12 中使用 ESM

    Node.js 12 之后开始支持 ECMAScript Modules(简称ESM),不过并不是默认开启或者自动切换。坦率地说我也卡了一阵子才搞清楚怎么直接使用。简单记一下吧。

    (更多…)
  • 小程序初始化时的用户状态管理

    小程序初始化时的用户状态管理

    经常能看到有同学提问这个问题,刚好我之前也处理过,分享下我的方案。

    (更多…)
  • 解决 composer global install 失败的问题

    解决 composer global install 失败的问题

    今天突发奇想,打算写会儿 PHP,按照 Laravel 文档,准备先安装 laravel/installer,结果 composer global require laravel/install 安装失败,提示错误信息:

    Your requirements could not be resolved to an installable set of packages.

    出问题的包是 symfony/console,需要的版本和已安装版本不一致,安装新版本失败。简单搜了一下,答案比较多,有说版本问题,有说权限问题,有说 PHP 模块问题。

    我看了一眼权限,应该没问题。然后 composer help 看了看命令参数,发现有一个命令,可以查看所有已安装,现在版本有些旧了,可以升级的包:composer global outdated。执行之,发现以前安装过 laravel/installer 1.x,自然也安装了 1.x 需要的 symfony/console 作为依赖。那么,多半是这个旧版本阻止了新版本的安装,导致新版 laravel/installer 安装失败。

    于是 composer global remove laravel/installer 先删除就版本,然后安装新版本,就 OK 了。

    然后,折腾了半天,我又不想写 PHP 了……

  • Chat idea: Office + 前端碰撞出的火花

    Chat idea: Office + 前端碰撞出的火花

    在论坛里经常看到有些同学询问一些日常办公的问题,比如:

    1. 如何输出 Excel
    2. 如何打印分页 PDF,包括页眉页脚

    作为开发人员,在公司里经常被其它部门的同事拜托做一些办公相关的工作,也实属正常。这些非主营业务,做好了,也是蛮有价值的资产。

    刚好这两个方面我都有经验,以前也都写过文章分享。不过代码有点年久失修,不知道现在还能不能稳定工作,所以可以考虑利用一次 Chat 的机会把它们修整修整,写写文档,做成工具包直接让大家使用。

    (更多…)
  • 使用 `position: sticky` 的要点

    使用 `position: sticky` 的要点

    position 属性非常重要,它有五个可选值。“这五个选项是哪些?它们的作用如何?”是我非常喜欢的面试题。以我的经验,凡是这道题答得好的,后面多半没啥问题;这道题答不出或者错漏的,后面能翻盘的概率很低。

    属性及释义大家请看 MDN,本文不再赘述。

    position: sticky 比较复杂,简单来说,它包含以下特性:

    1. 当它的位置让它可以正常呈现的时候,它的定位等同于 position: static,随着正常的文档流滚动
    2. 当它的位置不足以让它正常显示,但它的父元素有足够多让它显示的空间,它的定位等同于 position: fixed
    3. 当它的父元素的空间不够让它显示,它的定位等同于 position:absolute

    言说太复杂,大家可以看下这个演示,基本上就能明白了:

    (更多…)
  • 使用 <wbr> 解决长 URL 的换行问题

    使用 <wbr> 解决长 URL 的换行问题

    问题

    我们知道,世界上文字主要有两种:一种是以中文为代表的象形文字;另一种是以英法俄等为代表的拼音语系。前者的换行很简单,每个单字都有自己的意义,所以每个字后面都可以换行。拼音语言,字母组合本身无意义,连在一起才有意义;不同单词意义差异巨大,所以只能以单词为单位换行。

    Web 开发中,屏幕宽度有限,超长文字必须换行。在 CSS 中,控制换行的属性主要有 word-breakwhite-space,其中,默认换行行为的是 word-break: normal,即以单词为单位换行。比较奇怪的是,对于 URL,我本以为类似 /.: 都是明显的单词分隔符,理应换行,但实际上,浏览器并不会在这些地方换行。如果我们使用 break-all 或者 break-word,则会使得浏览器在不合理的地方换行,如果刚好在表格里,别的列内容比较多,那么包含 URL 的单元格就会被挤压得非常窄,拉得特别高,非常难看,非常难读。

    尝试

    原生方法无法解决问题,只好摸索手动断行的做法。但是想完美解决问题非常困难:

    第一个方案是全部换行,肯定不行;

    第二个方案固定宽度换行,因为表格内容不固定,效果也很差,也不行;

    老板提出了第三个方案:使用“8.3”格式,即超长字符串只保留前8个字符,后面显示“…”,然后可以手动展开。很明显,这个方案对 URL 来说没有什么价值,https:// 加起来正好 8 个字符,有意义么……即使加长也一样,因为用户有时候看域名,有时候看 pathname,也有时候看 search,我们没有办法预测。

    然后老板又提出“Excel 方案”,即固定列宽,自动隐藏超出的文字,用户可以通过拖拽来调整列宽。这个方案理论上可以解决问题,但是实现难度太大,因为浏览器自带表格自适应宽度的算法,采用 “Excel 方案” 就必须放弃这个算法自己手动实现,成本很高,非万不得已也不想做。

    最后,动态换行,根据表格宽度计算在哪里断行。还是不行,计算难度太大。

    <wbr> 解决

    这个问题困扰了我很久,直到前两天,我突然发现原来有 <wbr> 软换行的存在。而且它的兼容性非常之好,甚至连 IE8 都支持。

    它的含义是“可换可不换”。当元素宽度不够需要换行,就从它这里换;如果宽度够,就不换行。所以,只需要在“可能”换行的地方加上这个元素,就可以达成我的目标。写成代码很简单,大约是这样:

    function wrapUrl(url) {
      if (!url) {
        return '';
      }
    
      // 先把协议取出来,我不希望在协议这里换行
      const head = url.substring(0, 10);
      const left = url.substring(10);
      // 在 `?&/` 前面插入 `<wbr>`
      // 或者16个连续英文数字也要换行,打断 hash 和 md5
      return head + left.replace(/([?&\/]|([a-zA-Z0-9]{16}))/g, str => '<wbr>' + str );
    }

    实际效果很好,大概是这样(截图时,<wbr> 放在断开位置的后面,我觉得不好看,就调整了下):

    <br> 对比,后者是固定换行,当表格内容很少,有充足的空间显示 URL 时,也会换行,就不合适了。

    总结

    需要注意,<table> 的渲染很特殊,浏览器要花很多时间计算每个列的内容、计算它的宽度,所以性能会比较差,这也是不要用 <table> 做布局的原因。本案例中,使用 <wbr> 实际上是想借用浏览器计算表格各列宽度的机制。所以是合适的。表格渲染之后,内容最好就固定住,不要有复杂的变动,比如隐藏/显示(前面说的8.3格式),因为内容的变化会导致浏览器重新计算布局重新渲染,比较消耗机器的性能。

    以及,做了十几年前端,稍一放松,竟然有完全不清楚没用过的标签,看来有必要找时间再把 HTML、CSS 再翻一遍了。

  • GitChat: 使用 webpack 开发企业官网

    GitChat: 使用 webpack 开发企业官网

    最近我厂官网改版,我尝试用 Webpack 重建了开发工具链,效果不错,配置代码少了很多,逻辑更加简单清晰。我觉得值得拿出来分享一下。

    文章已经发布,慢慢写了将近5w字,干货很多,覆盖面很广,欢迎大家前去阅读:升级工具链吧!使用 Webpack 开发企业官网。感谢大家的支持。

    交流应该会通过直播进行,暂定7月中吧,斗鱼直播间:douyu.tv/meathill。

    (更多…)
  • Intersection Observer 笔记

    Intersection Observer 笔记

    有时候我们需要根据一个元素的位置来修改它的属性,比如图片的 lazyload,比如视频离开视窗之后停止播放。

    以前的做法通常是:

    1. 侦听 window.scroll 事件
    2. scroll 触发,遍历每个要检查的 DOM Element,执行 .getBoundingClientRect() 取出它的 widthheighttopleft,然后根据 viewport 和宽高 scrollTop scrollLeft 计算对象是否应该出现
    3. 然后做处理

    这样做会产生一些问题:

    1. 如果漏掉移除侦听器,可能造成内存泄漏
    2. 不同组件之间,很难共享侦听器
    3. 每一次都计算所有 Element,成本很高

    于是现在我们有了 Intersection Observer,专门用来观察一个 Element 是否出现在 viewport 或者父容器里。它可以很好的解决这些问题:

    1. API 更清晰
    2. 逻辑原生,速度更快,消耗更少
    3. 可以自定义阈值,更加可控

    关于 Intersection Observer 的详细知识,建议大家认真阅读 MDN – Intersection Observer,我就不抄文档了。

    使用 Intersection Observer 大体上分为三步:

    1. 声明一个 Intersection Observer 实例,这一步最关键的是要确定显隐依据哪个元素,也就是 root
    2. 将需要检查的元素加入侦听队列
    3. 在回调函数里处理元素状态

    写成代码大概是这样的:

    // 声明一个实例
    // 因为我的视口即当前 viewport,所以这里不需要 `options`
    const observer = new IntersectionObserver(entries => {
      // 遍历所有实例,如果它显示出来,即 intersectionRatio 显示比例大于 0
      // 那么就让它 `dispatch('visible')`
      entries.forEach(({target, intersectionRatio}) => {
        const event = new CustomEvent('visible', {
          detail: {
            isVisible: intersectionRatio > 0,
          },
        });
        target.dispatchEvent(event);
      });
    });
    
    // 然后可以在 Vue 里侦听这个事件
    export default {
      template: '<div @visible="onVisible"></div>',
      mounted() {
        observer.observe(this.$el);
      },
      beforeDestroy() {
        observer.unobserve(this.$el);
      },
    }

    Intersection Observer API 公布一段时间之后,又进行了升级,现在支持更丰富的参数,比如我们可以定义一个函数去判断更复杂情况下的显示状态。不过大部分场景下,我们并不需要很精准的判断,所以我觉得这个只是保障选择的机会。


    参考阅读:

    Proposed Updates for Intersection Observer