使用 <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 再翻一遍了。

Chat idea:记一次 Firefox 下 Vue 带来的性能危机的解决

前两天遇到一个问题:我厂的一个产品在 Firefox 下,可能发生因为 CPU 占用过高而卡死的情况。这个问题在测试环境不复现,在 Chrome下基本上也不会复现。

因为我厂老板是这个产品的主要用户,这个 bug 让我倍感压力,但一直没有解决它的好办法。终于有一天,在某台生产机上调试另外一个 bug 的时候,我终于发现了稳定复现这个 bug 的方式。

接下来就是几个小时的 debug,然后发现问题所在,然后解决问题,然后发现解决方案不理想,于是寻求新的解决方案,然后找到新的 API,最终彻底的解决这个问题。

接下来我就分享这个过程,读完整篇文章当中你将学会:

  1. 使用开发者工具查找性能问题
  2. 不断切分,缩小问题范围
  3. 理解 Vue 响应式原理分析问题根源
  4. 修复问题并验证
  5. 新的解决方案 Intersection Observer
  6. 解决问题并上线

目标读者:

  1. 中级开发者
  2. 熟悉原生 JS

大家觉得这个 idea 如何?请留言告诉我。不出意外的话这将是我下个月的 gitchat 内容。

Stylus 实现 `content: “5”`

一般来说 Stylus 对属性是直接替换的,所以正常来说下面的 stylus

$a = 10

.foo::before
  content $a

会编译成:

.foo::before {
  content: 10;
}

这样是非法的,10 不会渲染出来。如果想让它渲染出来,必须用字符串。如果只有一个值,那就好办了,直接 $a = '10' 就好。但如果在循环里,就比较麻烦,此时,可以用 '' + 10 转换成字符串,或者使用 s(template, value),以下两种方式是等效的。

// 注意运算顺序哦
.foo
  for n in 1..5
    &:nth-child({n})::before
      $str = '' + (5 - n)
      content $str

  for n in 1..5
    &:nth-child({n})::after
      $str = 5 - n
      content s('"%s"', $str)

最近折腾 @babel/preset-env 的一些小心得

近来厂里的项目越来越多,代码共享必不可少。我现在采取的方案是:

  1. 把公共组件拿出来,开一个新仓库
  2. 使用 webpack 进行打包编译,libraryTarget: 'umd'
  3. 将打包编译的代码一起提交到仓库
  4. 使用 npm i <owner>/<repo> -S 安装依赖,因为我厂的仓库均为私有,所以不能发布到 NPM

这套方案简单好用,实操效果良好。接下来我希望优化打包结果,于是研究了打包配置项,下面是我的一点心得。

继续阅读“最近折腾 @babel/preset-env 的一些小心得”

如何加入一家全职远程的公司

自从我厂开启招聘之后,陆续收到一些简历。除去一些明显不靠谱的,也有不少同学的简历看起来很好,但明显只是为“远程”而来,满脑子都惦记着远程之后坐在海边一边打望比基尼美女,一边啜饮鲜榨果汁,一边写代码的生活。却没有想过自己能给公司带来什么。

没错,我也曾这么幻想着,加入了我厂。加入之后的工作生活虽然称不上幻灭,但绝对和预期不同。所以今天,我决定写点什么,让大家了解全职远程的真实状态,同时理解远程的需求,以便更好的抉择今后的职业道路。

以下的内容不仅包含远程,还有“小型创业公司”。我还没见过中型或更大的“全员远程”公司,所以姑且认为这种组织形式和大多数老板的管理水平,暂时无法负担更大规模的协作吧。

继续阅读“如何加入一家全职远程的公司”

使用 Proxy 添加魔术属性/方法

使用 Proxy 实现魔术属性/方法。

最近在开发我厂的 QA 工具时,遇到一个问题。我需要模拟 Puppeteer 的所有方法,以便兼容原先的 JS 文件。Puppeteer 提供一个 .asElement() 方法,可以把函数执行结果转换成一个伪 DOM Element(如果函数返回的就是 DOM Element 的话),然后我们就可以在 Node.js 里调用原本属于 DOM 的方法,比如 .focus()。Pupputeer 会替我们完成映射和函数调用,并且返回结果。

对于大部分对象来说,我只要模拟对应的属性、方法,然后用自己的函数实现功能即可。但是 DOM Element 有上百个属性和方法,手工实现一遍实在太低效了。必须寻找其它途径。

好在我之前看过 Proxy 的介绍,赶紧翻出文档和书又复习两遍,就大概知道怎么做了。

Proxy 类如其名,可以“代理”对某个对象的访问。你可以把他理解成明星的经纪人。明星成名之前都是自己处理一切事务,有了经纪人之后,大部分事务就由经纪人负责,但仍然有一些事情需要明星自己处理。

Proxy 的用法很简单,实例化时,把要代理的对象传进去,定义一下代理方法就好。

const obj = { name: 'meathill' };
new Proxy(obj, {
  get(target, property) {
    // 如果对象中有要求的属性或方法,则返回
    if (property in target) {
      return target[property];
    }
    // 没有的话,进行其它处理
    return 'hello';
  }
});

obj.name // 'meathill'
obj.age // 'hello'
obj.sex // 'hello'

接下来,比如我们访问 obj.foo,那么代理就会生效,它会先检查 obj,如果这个对象上本来就有 foo 属性,就会返回;如果没有,则会调用我们定义的方法来处理。

如此一来,我们可以定义一个 VElement 类,这个类可以实现一些特殊方法,比如 .type(str) 输入,.click() 点击等;然后用 Proxy 代理其它方法和属性,让对象进入插件 Context 执行。


Proxy 还有其它方法也很有用,尤其是 get 对应的 set ,以后再介绍。大家可以自己抽空研究下。

参考

  • 阮一峰的 ES6
  • 《深入理解 ES6》

近期用 webpack-dev-server 作代理的一些经验

使用 webpack-dev-server 代理远程服务器;修改 http header 以便本地登录;代理 WebSocket。

如今前端开发使用 webpack-dev-server 作为本地服务器已经是基本配置,加上 proxy 功能可以很好的应对 SSL、跨域、线上环境切换等需求。Vue CLI 3 里也做了相应的集成,用起来很方便。

继续阅读“近期用 webpack-dev-server 作代理的一些经验”

2018 WeGeek 小程序 Hackthon 记

参加了 SF 组织的小程序 Hackthon 活动,收获很多,希望将来还有机会。

某天,行政找到我:“你今年的年假还剩7天,只有5天能保留到明年,建议你找时间把那两天休掉。”我正在盘算怎么用,突然就在 SegmentFault 上看到12月15-16日要在北京举办小程序 Hackthon 的消息。作为一名程序员,我其实早就想参加类似的活动了,所以,干脆就来吧。

因为最近跟蛋东剑剑一起搞的东西比较多,而且他们俩单身,比较好约,所以就拉了他们组队。

初选很顺利的通过了,然后我就定了日程、机票和酒店。

这次 Hackthon 的题目提前一周公布。我们简单商量了一下,既然没有更好的想法,不如就把我之前计划的“姆伊读书”,又叫“以后再听”做出来,感觉很能呼应小程序的主题。而且这个需求来自于我的真实日常,即使不得奖,也能收获一个有价值的产品,何乐而不为呢?本来我想抢跑来着,结果赶上老婆孩子一起生病,公司的正事儿都干不完,只好到了现场才开始写。好在我对项目比较有把握,对自己的能力也很有把握,所以最后提交的 MVP 完成度还不错。

做的时候他们表示对姆伊没有感情,不想跟狗绑定在一起,所以改名作“换听”。

结果仍然没能得奖。不得不说,这种提前公布题目的做法确实值得商榷,很多团队一看就知道是先弄了五六成,现场只搞拼装、联调,产品复杂度超过我们很多。另外评委的指导原则也有些迷,一等奖还算符合小程序的场景没啥可说,二三等奖其实都不适合用小程序来实现,独立应用才有价值。实在是为小程序而小程序。

我觉得,要是张小龙在现场的话,我们赢的概率要高很多 XD。

不过抱怨归抱怨,我倒也乐意接受这个结果。规则是人家定的,过程也公平公正公开,还不收钱,还提供盒饭,真心很感激。尤其是,在这次活动的激励下,我终于把早早就翻来覆去想了很久的产品给做了出来。而且,确实好用,我现在已经用得停不下来了。

另外最后的展示和点评也收获很多,你能发现很多人,很多不同的视角,你可以试着站在别人的角度看问题,学习别人解决问题的思路。收获很大。我觉得程序员都应该参与类似的活动,因为有机会把自己的 side project 从 idea 转化为实物。我觉得未毕业的有志于从事 IT 研发事业的毕业生也应该参与类似的活动,可以学到很多从立项、到产品规划、到实现的知识。

频率嘛,我觉得每年参加一次吧,哈哈。

继续阅读“2018 WeGeek 小程序 Hackthon 记”

BaaS 碎碎念

暂时没时间写完整,零散记一些吧。

BaaS 的核心其实在于 ACL。

因为 BaaS 把获取数据的机制下发到客户端,所以 BaaS 和传统应用最显著的差距就是在哪里处理数据的可视性问题。传统应用里,哪个用户看到哪个数据是后端处理的。而 BaaS 里,则是前端、后端、ACL 一起确定的,而且通常情况下,ACL 设计的好,前端后端的验证步骤都可以省去。

小程序云不支持后台,无法使用。