标签: 教程

  • 【视频】Node.js 开发 RAR 解压缩命令行工具

    【视频】Node.js 开发 RAR 解压缩命令行工具

    拖来拖去,终于把 使用 node.js 开发命令行工具 workshop 的视频剪出来了,前几天上传到 B 站,访问量很一般,所以在自己的博客再捞一下。

    这次视频主要面向新手,主要呈现从 0 到 1 实现命令行工具的做法,希望观众无论基础如何,都能在看完视频之后,掌握封装仓库、实现命令行工具的做法。内容大约是:

    1. 不同系统下安装 node.js
    2. 创建命令行工具项目
    3. package.json 结构介绍
    4. 介绍 unrar-promise
    5. 介绍 yargs 实现命令行接口
    6. 开发功能
    7. 发布到 npm

    完成的项目放在 GitHub:meathill/unrar: a simple script to unarchive rar files (github.com) 非常简单,大家可以参考。

    有任何问题、建议均欢迎留言讨论。新的一年,我会努力多做视频、多做好视频,希望大家支持我。

  • 超全面全栈入门项目:fastest

    超全面全栈入门项目:fastest

    TL;DR.

    前些天做了个覆盖面很广的项目:FastTest。涉及到静态网站、node.js 后端 API、响应式、多语言、Vue3 全家桶、前端工具链、nginx 配置等一系列技术,都是很基础的应用,相信对大家入门很有帮助。

    GitHub:https://github.com/meathill/fasttest

    欢迎阅读学习,有任何问题均可提问或提 issue。


    前些日子有个朋友找到我,想请我帮他做个项目。我看了一眼需求,不太想接:这个项目太小,要多了不合适;要少了感觉又不值当——有那时间还不如砍两把胖虎。

    后来我仔细一想,感觉这个项目很适合拿来做教程,而且是非常全面的全栈入门教程。

    先来看需求。他的需求很简单:开发一个测速应用。用户打开页面,点击按钮,然后下载一些东西,下载完成之后,告诉他他的网速是多少。与之前的测速产品不同,这个应用不下载大文件,而是从各种 CDN 下载 JS 库和 SDK,因为大部分 web 应用都依赖这些资源,所以可能更接近真实体验。

    关于需求我们不深入讨论(我觉得挺有道理,但也未必有道理到哪儿去……),只看实现。为了满足这个需求,我们需要开发这些东西:

    1. 静态页,让用户能够测速。这就需要:
      1. HTML+CSS+JS,静态资源
      2. 响应式,支持桌面和移动端
      3. 多语言支持
    2. 后台,能调整待测资源、修改翻译、控制广告、查看数据。这就需要:
      1. 友好的 Admin panel:vue 全家桶
      2. 后端 API:express.js + node.js server
      3. 自动发布静态页(webpack 前端工具链)
    3. 部署到服务器:
      1. 静态服务供给用户端页面和资源
      2. SPA 服务,提供后台 Admin Panel 给管理员
      3. 反向代理 express.js API
      4. 配置 CDN
    4. 其它,比如版本管理、前端预处理工具,等等

    这些东西可以说是非常全栈,除了没有移动 App、没有数据库操作,其它 Web 开发的东西都用到了。而且涉及到技术也都很基础,没有特别深入的东西。再加上,这个项目会上线,会迭代,是个真实产品。所以,很适合拿来做教程,大部分观众,不管是什么背景,都可以拿来入门。

    于是我就答应了,然后断断续续在直播的时间把它做了出来。当然还有一些问题,不过大部分功能都就绪了。

    项目放在 GitHub 上:https://github.com/meathill/fasttest,有需要的同学请自由取用。对其中任何技术点有问题都可以提问或者开 issue。

    网站地址在 https://afasttest.com

    视频还需要一些时间来整理,将来慢慢放出来吧。着急且有时间的同学可以自行从百度网盘下载:https://pan.baidu.com/s/1KPuCM-9gPd0hQsr5Df_PnA 提取码: w8f2。

  • 给 Bootstrap Modal 增加缩放功能

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

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

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

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

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

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

  • 准备搞一场专题直播

    准备搞一场专题直播

    准备在 SegmentFault 搞一场专题直播,标题是《jQuery, Backbone, Vue》,计划通过对比老中青三代框架开发的差异,带领大家理解前端发展的趋势,接触更好的未来。

    直播地址:https://segmentfault.com/l/1500000008694676
    幻灯片地址:https://meathill-lecture.github.io/jquery-backbone-vue/
    范例代码仓库:https://github.com/meathill-lecture/jquery-backbone-vue

    求关注,求购买。

  • [教程]纯CSS实现多选组件

    [教程]纯CSS实现多选组件

    产品篇

    在我们的后台中,需要设置广告精准投放的区域,也就是要在全国31个省、自治区、直辖市中选择。那么,出现下面这幅景象也就理所应当了:
    1

    这样做有几个问题:

    1. 选项很多,没有规律,找起来很累
    2. 如果是一个已经选择了部分选项的广告,修改时仍然需要用肉眼寻找,无法一眼看出来投放到哪些省份
    3. 选完一个,再选下一个,还要从头找,甚至会被已经选过的影响

    于是我想,首先应该把所有选项分为“已选中”和“未选中”两批,解决第2个问题,减轻第3个问题;其次复选框本身的价值不大,可以被替换为其它样式;唯一可能引入的问题,就是点选时,用户的预期是看到复选框里出现一个小对勾,表示选中,如果我把它移开放到“已选中”组里,用户可能会迷惑,需要一些时间学习。

    于是我跟某产品经理朋友聊了聊这个想法,他表示确实可能造成用户迷惑,不过如果能加入动画效果,那么基本没问题。嗯,开始动手。

    技术实现篇

    近日flexbox规范定案,浏览器相继支持display:flex;,同时传来一条好消息,新实现比老实现display:box;快很多。这次我打算用flexbox来解决问题,因为里面有一个很重要的属性:order(之前叫box-ordinal-group),它可以改变布局中元素的排列顺序,配合CSS3新增的选择器,应该可以满足需要。

    第一步 分拆选中/未选中

    (关于flexbox的知识,可以通过Google了解,虽然搜到的多是上一个版本,不过和最终版差别不大,只是叫法不同。本文不再过多讲解,我就当大家都会了)

    <input type="checkbox">本身的样式不能修改,所以我们必须借助<label>的帮助;实现选中/未选中区分,那自然就要用到伪类:checked;选择器一定是从外到内、从前到后的,没法选择父级元素,所以不能用<label>去包<input>,那么最终布局就只能是:

    <div>
        <input type="checkbox" name="q[]" id="q1" />
        <label for="q1">小宝3225</label>
        <input type="checkbox" name="q[]" id="q2" />
        <label for="q2">王老白白白</label>
        <input type="checkbox" name="q[]" id="q3" />
        <label for="q3">空夫31</label>
        <input type="checkbox" name="q[]" id="q4" />
        <label for="q4">谷大白话</label>
        <input type="checkbox" name="q[]" id="q5" />
        <label for="q5">Meathill</label>
        <input type="checkbox" name="q[]" id="q6" />
        <label for="q6">一毛不拔大师</label>
    </div>

    很简单哈,不解释了。CSS3新增了“下一节点”选择器 +,用来选择某节点的下一个节点,结合:checked伪类就可以将选中的<input>和它临近的<label>通过改变order属性移到前面去:

    #container {
      display:flex;
      flex-direction:row;
      flex-wrap:wrap;
    }
    #container input,
    #container label {
      order: 2; //所有选项、label顺序为2
    }
    input[type=checkbox]:checked,
    input[type=checkbox]:checked + label {
      order: 0; // 越小越靠前
    }

    不过这样只是把选中的内容提前,视觉上没有真正的分割。所以我决定再加入一根分割线,上面是选中的,下面是未选的。这个时候我们需要用到 ~ 这个选择器,选择某节点后面的节点:

    hr {
      display:none; // 默认情况下,没选任何选项,分割线隐藏
      order: 1; // 分割线顺序为1
      width:100%; // 保证独霸一行
    }
    input[type=checkbox]:checked ~ hr {
      display:block; // 有选项被选中后才会显示分割线
    }

    Demo如下:

    这样基础功能实现了。不过视觉上,排版仍然不整齐,选中的选项和未选中的选项区分不算太明显,所以下一步我准备美化下checkbox。

    第二步,美化checkbox

    做法与前面类似,也要用到CSS3新增的选择器。前面为了实现<label>提前,没有用它包裹<input>,所以在选项很多很长导致换行的时候,可能出现复选框和标签脱离的尴尬状况。好在复选框的价值可以用别的样式取代,所以先把小方框隐藏起来,转而将<label>作为操作目标,再来点边框底色圆角(参考自Bootstrap 3),就可以了:

    input[type=checkbox] {
      display: none;
    }
    label {
      min-width: 120px;
      border: 1px solid #CCC;
      padding: 2px 8px;
      text-align: center;
      margin: 0 5px 5px 0;
      background: #FFF;
      color: #333;
      border-radius: 3px;
      box-sizing: border-box;
    }
    label:hover {
      border-color: #ADADAD;
      background: #EBEBEB;
      cursor: pointer;
    }
    input[type=checkbox]:checked + label {
      order: 0;
      background-color: #5cb85c;
      border-color: #4cae4c;
      color: #FFF;
    }
    input[type=checkbox]:checked + label:hover {
      background-color: #47a447;
      border-color: #398439;
    }

    这样看起来还有上升空间,如果加上几个图标响应用户操作,那么学习成本会更低,对操作后的预期也会更准确。于是引用CDN上的font-awesome,使用:before伪类加上小图标,就得到了最终效果:

    我无意中发现,这样批量添加删除时,鼠标可以常点不动,应该也是个意外的收获吧。

    第三步,加入动画教育用户(失败)

    至此功能基本做好了,不过由于修改了行为,可能导致用户迷惑,所以准备加个动画帮助用户理解这个交互。

    可惜作为一个新功能,浏览器的支持尚不完善,虽然规范中规定“animatable: yes”,但是实测在Chrome v.30也无法工作:http://jsfiddle.net/meathill/Ka66W/1/

    看来只有等新版浏览器发布后再去完善了。

    兼容性

    使用纯CSS做组件,几乎不用担心兼容性问题,因为浏览器本身就做了很好的向下兼容,代码最多不生效,一般不会错。

    具体到这个组件,因为只针对视觉效果,没有增删改任何浏览器行为,所以兼容性也没有任何问题。不过最终效果呢,只有支持flexbox和CSS3选择符的浏览器才能正常渲染。

    我的环境是Window 8 + Chrome v.30,以及小米2 + Chrome v.30,测试通过。

    后记

    如今CSS很强,纯CSS可以实现很多功能,希望今后能做出更多有价值的东西。分享这个组件的实现,希望对大家有用。