去年,我 35 岁,一不小心就达到这个尴尬的年龄。这一年,是我远程工作的第三年,也是我在 OpenResty Inc. 工作的第二年。去年,儿子开始上小学,正式受教育。
(更多…)作者: meathill
-

Webpack 4 笔记
之前的《Webpack 笔记》内容有些陈旧,不打算再改,改名为《Webpack 3 笔记》。今后关于 Webpack 4 的笔记写在这里。如果将来 Webpack API 再次发生巨大变化,类似 3->4 这样,就再开一篇。
(更多…) -

给 Bootstrap Modal 增加缩放功能
需求
Bootstrap 应该还是目前最流行的前端基础框架之一。因为架构方面的优势,它的侵入性很低,可以以各种方式集成到其它项目当中。在我厂的各种产品里,都有它的用武之地。
前两天,老板抱怨,说 Modal(弹窗)在他的屏幕上太小,浪费他的 5K 显示器。
我看了一下,按照 Bootstrap 的设计,超过 1200px 就算是 XL,此时
.modal-lg的宽度固定在 1140px。其实 Bootstrap 这么设计也有它的道理,因为人眼聚焦后宽度有限,如果弹窗太宽的话,内容一眼看不全,也不好。不过在我厂的产品里,弹窗要呈现火焰图,所以宽一些也有好处。技术方案
那么,综合来看,最合适的做法,就给 Modal 添加一个拖拽的功能:用户觉得够大了,就这么着;用户想看大一点,就自己拉大一些,然后我记录用户的选择,以便复用。
看过我《用 `resize` 和 MutationObserver 实现缩放 DOM 并记录尺寸》的同学,应该知道
resize这个 CSS 属性,使用它可以很方便的给元素添加缩放功能。参考 caniuse 上面的普及度,大部分新版本的浏览器都已经支持,可以放心使用。使用它的时候要注意两点:
首先,我们在缩放元素的同时,也会对它的子元素、父元素同时造成影响。因为在静态文档流当中,块级元素的宽度默认是父元素
content-box的 100%,而高度由子元素决定。所以,对一个块级元素的缩放,不可能宽过它的父元素(如果限制了宽度的话),也不可能矮于它的子元素。其次,拖拽手柄的显示优先级很低,会被子元素盖住,哪怕子元素没有填充任何内容。换言之,一定要有
padding的元素才适合添加resize缩放。实施方案
总而言之,把这个属性加在哪个元素上面,很有讲究。具体到本次需求,Bootstrap Modal,最合适添加
resize属性的的是modal-content,因为它有1rem的内边距。但是限制宽度的是父元素,也就是
modal-dialog,它是响应式的,会根据显示器的宽度设置一个最大宽度。如果不修改它的max-width,modal-content的最大宽度就无法超过它,达不到预期效果。但是也不能改成width,这样的话,弹窗会失去弹性,分辨率低的时候表现不好。所以还是要在
max-width上做文章。如果直接去掉它,modal-dialog的宽度就会是 100%,失去弹窗效果,所以也不能这样做。最终,我的方案是:- 窗口完全展开后,获取宽高,赋给
modal-content - 去掉
modal-dialog的max-width - 用 MutationObserver 监测
modal-content的宽高,保存 localStorage,以便接下来使用
完整代码展示
我厂的产品基于 Vue 开发,所以部分逻辑用 Vue 组件实现。
效果演示
为方便在 Codepen 里呈现,有部分修改。
代码及解释
<template lang="pug"> .modal.simple-modal( :style="{display: visibility ? 'block' : 'none'}", @click="doCloseFromBackdrop", ) .modal-dialog.modal-dialog-scrollable( ref="dialog", :class="dialogClass", ) .modal-content(ref="content", :style="contentStyle") .modal-header.p-2 slot(name="header") h4 {{title}} span.close(v-if="canClose", @click="doClose") × .modal-body slot(name="body") </template> <script> import debounce from 'lodash/debounce'; const RESIZED_SIZE = 'resized_width_key'; let sharedSize = null; export default { props: { canClose: { type: Boolean, default: true, }, size: { type: String, default: null, validator: function(value) { return ['sm', 'lg', 'xl'].indexOf(value) !== -1; }, }, resizable: { type: Boolean, default: false, }, backdrop: { type: Boolean, default: true, }, title: { type: String, default: 'Modal title', }, }, computed: { dialogClass() { const classes = []; if (this.size) { classes.push(`modal-${this.size}`); } if (this.resizable) { classes.push('modal-dialog-resizable'); } if (this.resizedSize) { classes.push('ready'); } return classes.join(' '); }, contentStyle() { if (!this.resizable || !this.resizedSize) { return null; } const {width, height} = this.resizedSize; return { width: `${width}px`, height: `${height}px`, }; }, }, data() { return { visibility: false, resizedSize: null, }; }, methods: { async doOpen() { this.visibility = true; this.$emit('open'); if (this.resizable) { // 通过 debounce 节流可以降低函数运行次数 const onResize = debounce(this.onEditorResize, 100); // 这里用 MutationObserver 监测元素尺寸 const observer = this.observer = new MutationObserver(onResize); observer.observe(this.$refs.content, { attributes: true, }); if (sharedSize) { this.resizedSize = sharedSize; } // 第一次运行的时候,记录 Modal 尺寸,避免太大 if (!this.resizedSize) { await this.$nextTick(); // 按照张鑫旭的说法,这里用 `clientWidth` 有性能问题,不过暂时还没有更好的解决方案 // https://weibo.com/1263362863/ImwIOmamC const width = this.$refs.dialog.clientWidth; this.resizedSize = {width}; // 这里产生纪录之后,上面的 computed 属性就会把 `max-width` 去掉了 } } }, doClose() { this.visibility = false; this.$emit('close'); }, doCloseFromBackdrop({target}) { if (!this.backdrop || target !== this.$el) { return; } this.doClose(); }, onEditorResize([{target}]) { const width = target.clientWidth; const height = target.clientHeight; if (width < 320 || height < 160) { return; } sharedSize = {width, height}; localStorage.setItem(RESIZED_SIZE, JSON.stringify(sharedSize)); }, }, beforeMount() { const size = localStorage.getItem(RESIZED_SIZE); if (size) { this.resizedSize = JSON.parse(size); } }, beforeDestroy() { if (this.observer) { this.observer.disconnect(); this.observer = null; } }, }; </script> <style lang="stylus"> .simple-modal background-color: rgba(0, 0, 0, 0.5) .modal-content padding 1em .close cursor pointer .modal-dialog-resizable &.ready max-width unset !important .modal-content resize both margin 0 auto </style>注意
因为浏览器的异步加载机制,有可能在 modal 打开并完成布局后,高度和宽度被内容撑开导致记录不准,或者内容被异常遮盖。请读者自己想办法处理,就当练习题吧。
总结
本次组件开发非常符合我理想的组件模式:
- 充分利用浏览器原生机制
- 配合尽量少的 JS
- 需要什么功能就加什么功能,不需要大而全
在 MVVM 框架的配合下,这样的方案很容易实现。另一方面,每个项目都有独特的使用场景,通过长期在特定场景下工作,我们可以逐步整理出适用于这个场景的组件库,不断改进该项目的开发效率。我认为这才是组件化的正道。
- 窗口完全展开后,获取宽高,赋给
-

Chrome Devtool Protocol 开发笔记
周末动笔写教程,越写越长暂时收不了笔;周日发现上一台 iMac 回收被坑了,显示器从烧屏变闪屏了,心情大坏,简直写不动了,哎……
Chrome Devtool Protocol(下面简称 CDP)是一个非常强大工具,简单来说,它可以揭开束缚 Chrome 的各种封印,从浏览器角度深入页面(及其它领域,包括 worker),完成一些平日里难以完成的操作。
我目前研究它主要是想优化我厂官网的首屏 CSS,顺便为将来 QA 插件的深入开发做准备。
不过 CDP 的文档、资料各种不全,Google 也没什么结果,所以我会把日常踩到的坑记到这里,以备回顾。
(更多…) -

正则笔记
常看常新的正则教程
匹配中文及中文标点
如今建议用
/\p{sc=Han}/gu,参见:JavaScript 中使用正则 `u` 标记匹配多语言。const reg = /[\u4E00-\u9FCC\u3002\uff1b\uff0c\uff1a\u201c\u201d\uff08\uff09\u3001\uff1f\u300a\u300b]+/gES2018~ES2019 中的正则
转大写/小写
转大写
\U或\u,转小写\L或\l。比如 camel case 转 kebab case,可以查找[A-Z],替换为\L$0。IDE 里可以,JS 不行。
判断行开头或xx
比如 css 规则中的值,我们可能会用
/[\s:](值表达式)/这样的表达式,但它没法匹配行的开头,^不能放到[]里。此时只需要(^|[\s:])即可。 -

导出 CSV 时有数字的处理
导出数据为 CSV 的时候,如果是纯数字,可能会被 Excel 启动科学记数法,丢掉最后几位。
以前比较流行在数字前加
`,强制把数字转换成文本,这样做能解决数字被取整的问题,坏处是字段变文本,影响观感,且无法应用公式。正确的做法是用公式,变成
="${number}",这样首先看不到多余的\,其次公式也能正常生效。 -

startalk 客服版安装笔记
startalk 是去哪儿团队推出的企业级 IM 软件,我厂使用它作为私有 IM。本文主要记录客服版安装笔记,这方面文档好像不太多。
组成部分
startalk 客服版主要由三部分组成:
- 服务器端:一个 ejabberd 后端,可以同时提供服务给 qtalk
- startalk_node node.js 代理:用来适配前端请求和服务器接口
- startalk_web web 前端:提供 web 服务
其中,服务器端的安装配置我暂时不太清楚,是去哪儿的同学帮忙搞的。
部署步骤
- 准备好服务器(过程不知,略)
- clone 上面说的两个项目
- 修改 startalk_web 中的配置文件,指向自己的服务器
- 编译,生成
dist目录 - 将 startalk_node 项目部署到服务器上
- 将刚才 startalk_web 生成的文件部署到 startalk_node 的
public目录下 - 使用 pm2 等进程管理工具启动服务
- 完成
配置客服
startalk 使用 pgSQL,所以直接连上数据库,修改
supplier表即可,内容见字段名,应该比较容易理解。其它要修改的表内容见 Google Drive 里的范例文档。
先总结到这里,回头给他们提 PR。
-

远程工作需要的特质:愿意负责
本来想写“愿意背锅”的,后来想想还是不要那么标题党了。
前些天跟朋友聊天,聊到我厂的日常工作。他问我:“你们怎么解决人员互相不熟悉的协作问题的?Tower还是IM?”
我厂日常主要用 IM,包括文字或语音或共享桌面,但这并不是他要的答案。这位朋友是位产品经理,曾经多次向我抱怨说他们公司的前端太不给力,这不会做那不能做,他们产品设计,到最终实现总是大打折扣,产品人员不得不仔细核对每一项细节,并不断跟前端讨价还价,才能有所保障。
所以夏虫不可以语冰,从他的角度,很难理解我厂(全职远程)日常怎么才做能保证效率。
所以我回答:“IM 和文档。还有就是不要甩锅。其实我觉得,协同问题70%来自甩锅。”
“责任分清是不是甩锅?”他又问。
其实对于创业公司而言,最可怕的情况就是,没有大公司的命,得了大公司的病。在做出足够宽足够深的护城河之前,先琢磨怎么厘清责任;大家惦记的不是怎么把事情做好,而是出了问题不是我的责任。
对于远程创业公司来说,这个问题更加致命,因为这样要浪费非常多的沟通成本。所以我如实说出我的想法:“要看具体操作,很可能是。这里的问题在于,分清责任后,是只把自己的责任摘出来做,还是连带督促另一个人;就是分清责任之后,是选择做执行人,还是PM+执行人。”
“团队是合作关系还是里面存在管理关系?”
“合作,基本不存在管理。”
“督促另一个人时,对方如果不愿意怎么办?”
“说明那个人不适合在远程团队里。如果远程团队不能形成互相督促互相配合的氛围,工作效率多半是不够的。”
这两个问题其实是一个意思:你说督促就督促了?别人不听怎么办?其实回顾我的从业经验,大部分互联网公司都没有那么明确的上下级概念,也很少有人会去挑战督促者。大部分情况是,大家都不愿意出头(特有国情),但是有人出头,只要不犯傻,大家多半还是愿意接受领导的。
“挺好。”他最后总结道。
总结
远程工作,对每个参与者的要求都更高,除了能完成日常工作,更友好的合作精神,更主动的参与精神也非常重要。
希望这段对话对大家有帮助。
-

解决新机不能 npm install 私有仓库的问题
前些天换了电脑,从 iMac 27 Retina 2014 late 升级到 iMac 27 2019。关于前因后果,我写在张大妈,感兴趣的同学可以看下:《肉山的电子产品 篇九:机不如新,人不如故——iMac 27 换新记》。
换机器要搭环境。有两个选择:
- 整盘复制,利用 Time Machine 之类的工具
- 自己慢慢搭
考虑到老电脑已经工作了 5 年,经历了小十个系统版本,各种软件删装无数,里面的垃圾很多;再加上我的环境并不复杂,所以我决定自己慢慢搭。
然后遇到一个问题:
npm install会卡住,没有明显的提示信息,只有一段比较可疑:The authenticity of host 'GitHub.com (1.1.1.1)' can't be established. ECDSA key fingerprint is SHA256:abcdefg. Are you sure you want to continue connecting (yes/no)?因为项目中用到一些私有仓库的依赖,所以我怀疑是它们的问题。于是我把私有仓库从
package.json里移除,再安装,成功。看来猜的没错。然后我手动
git clone一个项目到本地,clone 的过程中,会看到上面的提示,手动输入yes,记录目标机器的指纹,视它安全可靠。clone 完成。再把私有仓库依赖加回package.json,安装,成功。问题解决。原因:使用 npm 安装 GitHub 仓库作为依赖,实际上等同于使用
git clone将目标仓库 clone 到node_modules里。而 git 协议依托于 ssh 协议。在新机器上安装时,因为没有访问过目标机器,本地不信任目标机器,而 npm 又没有对这个异常做处理,所以就会卡住,不上不下。

