分类
教程

给 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 共享首页,有两位同学主动领命,分开前后端就开做。结果我发现他们对文档的处理非常幼稚……所以便有了这样一个分享。

我的观点:

  1. 文档当然要写,但不要用自然语言这样写一大篇
  2. 文档如果不好好维护,将来表现和实际代码不一致,会造成更大的问题
  3. 所以开发者应该用更强的方式约束文档和代码,而不是大家主观协作
  4. 所以我们——尤其在前后端协作的时候——应该写格式化数据结构描述,然后通过编译的方式,用数据结构描述输出文档、测试、Mock Data

下面是视频:

分类
技术 教程

尬聊会:第一期实录

这个视频斗鱼说有问题,色情暴力之类的,给我拒了。我百思不得其解。如果你看完发现问题所在,请告诉我,谢谢。

时间锚点:

  1. [01:40] webpack该怎么学习呢?
  2. [05:20] 我为什么想搞尬聊会
  3. [07:10] 面试小经验
  4. [16:20] node怎么进行学习
  5. [21:00] 经常被问 ……源代码? 怎么读别人的源代码呢?
  6. [29:35] 初学者学习es6有什么好的方法?
  7. [34:35] 请加下,现在做跨平台,ionic2和RN选哪个好一点呢?
  8. [38:45] Phonegap
  9. [41:45 进度条这里出问题了,不知道线上怎么样] 现在的很多公司是否都不愿意带新人,兼谈学习
    1. 非常主动
    2. 胆大(有问题就问)心细(不要逮着一个人问)脸皮厚
    3. 问题必须经过自己的思考,尽量问一句话能答完的问题
  10. [44:20] 框架选择问题。Vue 作者尤雨溪的访谈
  11. [51:07] 半路出家的 JSer 学写样式
  12. [57:00] 会写页面但是不会 JS 的同学怎么学?
分类
教程

678次直播总结,兼半年总结

时间过的可真快,计划表上的大部分内容几乎还在原地踏步,半年时间就过去了。

今年的主要目标是做视频培训课程,不过很不赶巧,春节从日本回来我们全家就陷入感冒漩涡,你方咳罢我登场,轮着来,好半个月坏半个月,一直到过完五一才渐渐摆脱感冒阴影。于是系列视频教程就被我一拖再拖,第一波联系的 51CTO 学院已经把我放弃了,我也不好意思再找她们;第二波联系的慕课网要求甚严,现在都还没完成第一次课……

另一方面,SF 增设讲堂后我就立刻加入开始直播,如今进行了8期——好吧,其实早就完成了8期,只不过后几期越做越惨,身心俱伤,所以我休养了一个月……

分类
教程

知加 zhijia.io 即将关闭

今天突然从知加运营那里得到消息,知加将于7月24日关闭。其实,得知 Easy 离职回老家当独立开发者,我就猜想这个产品命不久矣。

还是挺遗憾的,因为从产品层面来看,知加有很多过人之处,目前看来市场上还真没有同类竞品:

分类
技术 教程

第一场 GitChat 总结

开始之前,先做广告吧。

GitChat 分享 《JavaScript 异步开发全攻略》

为解决异步函数的回调陷阱,开发社区不断摸索,终于折腾出 Promise/A+。它不增加新的语法,可以适配几乎所有浏览器;以队列的形式组织代码,易读好改;捕获异常方案也基本可用。这套方案在迭代中逐步完善,最终被吸收进 ES2015。不仅如此,ES2017 中还增加了 Await/Async,可以用顺序的方式书写异步代码,甚至可以正常抛出捕获错误,维护同一个栈。可以说彻底解决了异步回调的问题。 现在大部分浏览器和 Node.js 都已原生支持 Promise,很多类库也开始返回 Promise 对象,更有各种降级适配策略。Node.js 7+ 则实装了 Await/Async。如果您现在还不会使用,那么我建议您尽快学习一下。

下次直播分享 前端面试攻略:JavaScript 排序与搜索

从事前端开发的同学很多从页面仔入门,比如说我,自学比例很大,有些时候会无意中忽视一些基础,比如算法、数据结构。这些欠缺在某些时候就会显得很致命,比如说面试,或者处理大量数据的场景。所以希望这样的一场分享能够帮助大家夯实原本不太扎实的基础,将来的开发之路更加顺畅。

目前早鸟票发送中,7月13日前门票5折,19日前75折,开播当日恢复全价。

分类
教程

【修正】Promise N种用法-异步回调的问题-findLargest 解析

做慕课视频的时候,仔细琢磨了一下,发现之前讲的还是有问题,所以重新录了一遍。

分类
教程

第四,第五次直播总结

之前完成了第四次直播《写 CSS 也要开脑洞:万能的 :checked + label(后面简称 “CSS 脑洞”)和第五次直播《实战组件开发——手机日历 – 1. 项目启动》(后面简称“实战手机日历1”)。

CSS 脑洞卖的还可以,基本上跟 Promise 差不多,比较符合我的预期,慢慢成长呗。不过实战手机日历1就卖的很差,无论实售还是在线人数,几乎都创下历史新低……

事后我也在群里调查了一下,截至到目前,有两位表示大周末的不想学习,有两位表示时间太久消费太高(其中一个是学生,倒也可以理解),有两位表示觉得内容不感兴趣。

我个人觉得,单纯从干货角度来看,CSS 脑洞当然是最丰富的,6个实例,几乎都是拿去就能用的。而且相当开拓视野,能卖的好我觉得正常。但是实战1其实做的也不太差,至少在斗鱼试播的时候,围观群众中有两位当时就下单了。

至于周末嘛,也有大大在周末搞,好几百人来听的,所以,也不能完全赖周末。

换跑道果然还是很困难呀……总之,继续努力,先把这个系列做完!

分类
技术 教程

系统性学习与碎片化学习

4-27在小密圈接到第一次付费提问,喜获8块。庆祝一下。

这个话题也是我在小密圈里和那位同学的交流时产生的。他说他“学习的知识也不系统化”,“学习的知识也比较混乱”。“不系统”暂时没有好办法,但比较混乱一定是个问题,但是几句话说不清楚,所以构思了半天,准备写一篇文章来回应。


TL;DR: 中心思想:

我们以前熟悉的,以学校培训为主要形式,所谓“系统性学习法”,很难适应新时期互联网开发的需求。我们必须掌握碎片化学习法,即在快速建立起基础知识体系后,利用碎片时间,有目的有针对性的吸取专业知识,将其拼接到知识体系之上。使自己能够快速成长,在需要的时候还能及时切换学习方向。

分类
技术 教程

《写 CSS 也要开脑洞:万能的 `:checked + label`》视频补缺

4月27日直播《写 CSS 也要开脑洞:万能的 :checked + label》时,OBS 推流连接断了一下,当时不知道情况,今天看了下发现录像中间少了6分钟多的内容,虽然不是最关键的部分,不过还是补一下吧。

错误截图

不过这段视频 5:50 的位置,介绍纯 CSS 组件的优势那里有问题。当时有点忘词,这里我想说的第一点是:

纯 CSS 组件顾名思义,只改变外观,不改变行为。所以它的功能不会因为浏览器变化而变化,即使浏览器支持不完善,即使因为加载速度或者网络关系,导致 CSS、JS 加载失败,它最多样式回归到原始样式,功能是完全一致的。在非标准浏览器环境下,如读屏器,也是如此。