分类
技术

配置 nginx 支持目录别名

我有一个小项目,我们假定它叫 up,是用 PHP 写的私人图床,部署在服务器上,域名是 up.meathill.com。因为是 PHP,不需要编译,直接 `git clone` 仓库然后配置 nginx 指过去就好。

后来想做一个在线预览的功能,是个单页应用,也放在这个仓库里,目录是 `up/fe`,项目用 Vue + Webpack 来做,源码放在 `up/fe/src`,生成的文件放在 `up/fe/dist`。

这样一来,我就需要在原本的 nginx 里配置一个虚拟目录 `/admin`,指向生成的文件。预览的文件路径就是 `/admin/${file}`,所以我还要把未命中的文件重定向到 `index.html

经过反复摸索,最终的配置如下:

{
        location / {
                # 未直接命中的请求都交给根目录里的 index.php 处理
                try_files $uri $uri/ /index.php$args;
        }

        # 默认的 nginx 1.14 + PHP 7.1 + php-fpm 配置
        location ~ \.php$ {
                include snippets/fastcgi-php.conf;
                fastcgi_pass unix:/var/run/php/php7.1-fpm.sock;
        }

        location /admin {
                alias /mnt/www/uploader/fe/dist;
        }

        location ~ "/admin/(?<page>\d{8}\-\d{6})$" {
                #auth_basic "Only for OpenResty Inc.";
                #auth_basic_user_file /mnt/www/uploader/.htpasswd;
                alias /mnt/www/uploader/fe/dist/index.html;
                default_type text/html;
        }

        location /fe {
                return 404;
        }
}
分类
教程

给 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-widthmodal-content 的最大宽度就无法超过它,达不到预期效果。但是也不能改成 width,这样的话,弹窗会失去弹性,分辨率低的时候表现不好。

所以还是要在 max-width 上做文章。如果直接去掉它,modal-dialog 的宽度就会是 100%,失去弹窗效果,所以也不能这样做。最终,我的方案是:

  1. 窗口完全展开后,获取宽高,赋给 modal-content
  2. 去掉 modal-dialogmax-width
  3. 用 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 打开并完成布局后,高度和宽度被内容撑开导致记录不准,或者内容被异常遮盖。请读者自己想办法处理,就当练习题吧。

总结

本次组件开发非常符合我理想的组件模式:

  1. 充分利用浏览器原生机制
  2. 配合尽量少的 JS
  3. 需要什么功能就加什么功能,不需要大而全

在 MVVM 框架的配合下,这样的方案很容易实现。另一方面,每个项目都有独特的使用场景,通过长期在特定场景下工作,我们可以逐步整理出适用于这个场景的组件库,不断改进该项目的开发效率。我认为这才是组件化的正道。

分类
chrome

Chrome Devtool Protocol 开发笔记

周末动笔写教程,越写越长暂时收不了笔;周日发现上一台 iMac 回收被坑了,显示器从烧屏变闪屏了,心情大坏,简直写不动了,哎……

Chrome Devtool Protocol(下面简称 CDP)是一个非常强大工具,简单来说,它可以揭开束缚 Chrome 的各种封印,从浏览器角度深入页面(及其它领域,包括 worker),完成一些平日里难以完成的操作。

我目前研究它主要是想优化我厂官网的首屏 CSS,顺便为将来 QA 插件的深入开发做准备。

不过 CDP 的文档、资料各种不全,Google 也没什么结果,所以我会把日常踩到的坑记到这里,以备回顾。

分类
技术

正则笔记

常看常新的正则教程

正则表达式30分钟入门教程

匹配中文及中文标点

const reg = /[\u4E00-\u9FCC\u3002\uff1b\uff0c\uff1a\u201c\u201d\uff08\uff09\u3001\uff1f\u300a\u300b]+/g

ES2018~ES2019 中的正则

分类
技术

导出 CSV 时有数字的处理

导出数据为 CSV 的时候,如果是纯数字,可能会被 Excel 启动科学记数法,丢掉最后几位。

以前比较流行在数字前加 `,强制把数字转换成文本,这样做能解决数字被取整的问题,坏处是字段变文本,影响观感,且无法应用公式。

正确的做法是用公式,变成 ="${number}",这样首先看不到多余的 \,其次公式也能正常生效。

分类
技术

npm 支持开发者添加募款链接

昨天随手升级个人首页的依赖,更新完之后,npm 提示:

6 packages are looking for funding
  run `npm fund` for details

如果你执行 npm fund,可以看到类似这样的信息:

your-repo@0.1.0
├─┬ core-js@3.4.2
│ ├── type: opencollective
│ └── url: https://opencollective.com/core-js
├─┬ eslint@6.7.0
│ └── url: https://opencollective.com/eslint
└─┬ swiper@5.2.1
  ├── type: patreon
  └── url: https://www.patreon.com/vladimirkharlampidi

opencollective 和 patreon 都是众筹网站,只不过众筹的内容不是实物商品,而是支持你喜欢的创作者,无论他是摄影师、画家,还是开源软件作者。据说 Vue 的作者尤雨溪每年可以从 Patreon 上获得将近 $20w 的捐赠,这笔钱可以帮助他以独立身份继续开发 Vue,还可以雇佣一名全职开发者帮助他把这份工作做得更好。

我去 NPM 官网找了一下,npm fund 的文档还没更新上去,只找到这篇 RFC: Add funding support to package.json,大意是包开发者可以在包的描述文件(package.json)里放上一段募款声明,这样包的用户在安装的时候,就会看到我开篇写的那些文字。提醒包的开发者:你们可以通过捐赠的方式,表达对依赖开发者的感谢。

我觉得这种形式挺好的。当然,有些人可能看不惯这种有“伸手嫌疑”的行为,但是,与其在无酬工作中纠结、放弃自己的项目、甚至被别有用心的人利用埋后门,任何不影响安全性、不增加使用负担的变现方式都是合理且值得鼓励的。

希望开源软件开发者越过越好,开源基建开发者越过越好,世界越来越好。

分类
css

诡异的 `height: intrinsic`

我厂最新也是最重要的产品 OpenResty XRay 即将开始邀请测试,所以官网上自然要添加对应的网页。目前该网页已经部署到生产环境,大家可以访问 https://openresty.com.cn/cn/xray/ 简单了解一下。

这个页面的最下面,“信任与合规”区块是一些标准化组织的认证,按照需求应该放几个 logo。然后我就很自然的用 display: flex 来做了。在桌面浏览器显示正常。

但是在 iPhone Safari 上,上面的两个图标会变得瘦长,看起来是高度计算有问题。我尝试修复这个问题,却除了写明高度,只有 height: intrinsic 可以让它显示正常。去 MDN 一搜,竟然没有这个属性?!只提到 max-contentmin-content 两种“intrinsic”的属性。

caniuse 上,可以看到 intrinsic 是个非标准化的属性,应该是以前浏览器自发实现过,后来被 max-contentmin-content 取代。但是为何 Safari 明明支持这几个属性,但是只有 height: intrinsic 能显示正常,我就不知道了,也没有查到。

先记一下吧,将来再看。如果有同学遇到类似的,图片在 display:flex 横排时尺寸出现问题,可以试试这个。

分类
js 技术

基于 @vue/cli 的项目配置 browserslist

前些日子虽然写了 最近折腾 @babel/preset-env 的一些小心得,但其实没有正确的理解和配置 browserslist,所以今天问题又来了。

分类
技术

我的编程职业生涯

经常爬论坛,时常看到年轻的同学对职业生涯有各种迷茫。赶上这次 SF 征文,索性聊一聊我个人的编程职业生涯,给大家一些参考吧。

分类
技术

不要用战术上的勤奋掩盖站略上的懒惰

前两天,有个同学在群里问一道面试题:

  1. 写一个函数,返回一个长度为5的数组
  2. 数组里的元素都是 2-32 不重复的整数
  3. 需要用递归的方式写,不超过15行

这道题其实蛮简单,而且我觉得出的并不好,不过最后再说,先回到群里。然后就有热心同学动手写代码了,但是他犯了两个严重错误:

第一个错误

我们都知道 Math.random() 会返回一个 [0, 1) 的随机数,我们可以乘上一个数然后取整来获取一定范围内的随机数,比如 Math.random() * 100 >> 0 就能取到 0 ~ 99。那位同学也想到了,但是这次范围不太规整,2~32正好是31个数,跟平时不太一样。于是他可能突然脑子抽筋,先试了 Math.floor,又试了 Math.ceil,都不行,然后,他就用了 Math.round

这是他第一个错误。四舍五入之后,随机数就不再均匀,此时他有很多种选择,但他偏偏选择了一定不对的方案。

第二个错误

然后我就指出“你这种做法是错的”,接着他就犯了第二个也是更严重的错误,他写了个循环,跑100w次,检查结果是否符合预期。

这里的问题在于,首先他只检查能否覆盖到 2 ~ 32,其次他完全没有考虑到概率。

结论

这就是典型的,用战术上的勤奋,掩盖战略上的懒惰。看起来似乎也很努力,响应很及时,实际上纯粹瞎搞。

明明应该先想清楚,或者先查清楚,再去做好的事情。变成了“上来就瞎JB搞”,搞出一个疑似有效的方案,就心满意足。遇到挑战的时候,则顺手抄起一套方案去验证,也不管这套方案的设计思路和覆盖面。

这样做的结果,运气好的时候,勉强不出错;运气不好,事故错误一个不会少。类似的例子,在我们周围不胜枚举,比如 996,不管产品卖不卖的出去,先按住大家把活儿干了再说。看起来为了明天拼拼拼,其实只是老板一厢情愿的豪赌(或者小赌)。

解决方案

不要只顾着埋头苦干,也要学会抬头看路。不要在错误的道路上努力耕耘,赶紧找到正确的道路。

比如找工作。有些同学海投,能投则投,所有都投,然后没有面试机会,回来抱怨说三本没人权。实际一看简历,非常烂,没有重点,没有区分,看不出他擅长什么,能做什么岗位。

准备简历并不简单。

  1. 要找几家公司作为主攻。比如拥有自己的产品、有大牛坐镇、赛道比较喜欢、技术成长空间比较大。
  2. 针对这几家公司的 JD,针对性的修改简历。比如做后台系统,那你就突出 Vue/React 等框架经验;比如做小程序,你就突出小程序开发。
  3. 海投简历也应该根据岗位调整。前端岗位有一些区分度,需要突出不同技术能力和经验。这样做可以让你显得更加适合一些岗位。比如:
    1. 数据可视化:Canvas、SVG、WebGL
    2. JS 框架、语言开发:编译原理、执行基础
    3. 后台等工具类开发:三大框架
    4. Hybrid 开发,比如 RN
    5. 小程序开发:小程序、普适类框架

随便做个简历,乱投,有面试就去面,也不考察公司,最后要么找不到工作,要么找不到好工作,都是典型的“用战术的勤奋掩盖战略的懒惰”问题。

另外,如果一件事是正确的,但是暂时没有收获到正确的结果,那么不要着急变换思路,再想想再看看再查查,排除错误,要在正确的道路上找到正解。

吐槽面试题

这道题出的不好。看得出来想考察候选人是否会使用递归(可能还要考察尾递归优化),但是题目本身不用递归就能做,而且效果更好。其实随便出个归并排序之类的就好了嘛。

总结

我们从小接受到很多歌颂勤奋的教育,所以大家喜欢“没有功劳,也有苦劳”,用勤奋来感动自己。现在一定要明白,闷头苦干是不对的,一定要看请道路,选好方向。

与诸君共勉。