2020-03 技术汇总

2020/03/30 周一

#JSON数据转Blob后,怎么还原

在axios请求下载文件接口时,一般设置responseType: 'blob',文件返回正常就没问题,但后台如果处理文件或鉴权有问题,接口返回了包含错误信息的json格式数据,那样json数据也会转为Blob对象,而前端有必要将错误信息展示的,那怎么将Blob数据转JSON呢?下面来看看

let fileType = res.headers['content-type']
if (fileType.startsWith('application/json')) {
  let reader = new FileReader();
  reader.addEventListener("loadend", function() {
    let data = JSON.parse(reader.result)
    console.log(data);
  });
  reader.readAsText(res.data, "UTF-8") // 加UTF-8防止中文乱码
  return
}

#2020/03/28 周六

#网页dark mode(深色模式)适配

微信最近推出了深色模式,我试了下,手机切换时页面效果样式是实时刷新的。于是就想着web怎么能够监听深色模式,并设置样式。查了资料后,在Stack Overflow上找到了答案

通过css里的媒体查询就能适配深色模式,先来看看怎么用js获取当前是否是深色模式

// 获取当前是否是深色模式
// window.matchMedia('(prefers-color-scheme: dark)').matches
window.matchMedia && console.log('Is dark mode: ', window.matchMedia('(prefers-color-scheme: dark)').matches)

// 用js监听深色模式的切换事件
window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (event) => {
  console.log('dark mode change,已' + (event.matches ? '进入': '退出') + 'dark mode')
})

window.matchMedia到底是用来做什么的?我查了下mdn,发现了这样一个示例

let mql = window.matchMedia('(max-width: 600px)');

document.querySelector(".mq-value").innerText = mql.matches;

从这个例子看,大概就知道怎么用css来支持dark模式了吧,就是加一个类似小屏适配的一个媒体查询样式,来看个例子

/* dark mode support */
@media (prefers-color-scheme: dark) {
  body {
    background-color: black;
    color: #aaa;
  }

  body .content article, header, aside > div, footer  {
    border-color: #333;
    color: #aaa;
    background-color: black;
    box-shadow: 0 0 10px #333;
  }
}

深色模式下,一般将背景调暗,字体设置为偏白色即可。zuo11.com 已用上面的方法适配了深色模式,可以体验下。网站是开源的,zuo11.com深色模式支持代码 - github(opens new window)

参考:

  • How do I detect dark mode using JavaScript? - Stack Overflow(opens new window)
  • window.matchMedia | MDN(opens new window)
  • Supporting Dark Mode in Your Interface | Apple Developer Documentation(opens new window)

#2020/03/27 周五

#canvas绘制模糊的问题

今天发现同样的代码在两台电脑上绘制的一个清晰,一个模糊,后来查资料发现确实有这个问题

因为canvas不是矢量图,高dpi屏幕每平方英寸有更多的像素,也就是两倍屏,浏览器会以两个像素点的宽度来渲染一个像素,所以在Retina屏上会导致图片、文字都会模糊,怎么解决呢?

获取设备像素比:window.devicePixelRatio || 1

如果绘制的实际区域大小为 750 * 40,假设设备像素比为2,那么,canvas的width、height需要设置为 1500 * 80,然后用内联样式设置width为750,height为40,相当于canvas绘制2倍的大小,然后再缩放,这样就清晰了。

综上,在canvas绘制时,各种长度一定要考虑乘以devicePixelRatio,不然可能显示的不清晰

参考:解决 canvas 在高清屏中绘制模糊的问题(opens new window)

#2020/03/26 周四

#canvas不支持文本换行怎么处理

今天在stackoveflow里面搜索ctx.fill的问题时,查到了很多关于canvas ctx.fillText()绘制文本时不支持换行的问题,找到了一个比较好的答案

I'm afraid it is a limitation of Canvas' fillText. There is no multi-line support. Whats worse, there's no built-in way to measure line height, only width, making doing it yourself even harder!

一般解决思路是,根据 ctx.measureText('Hello').width 来看需要显示的文字是否需要换行,写一个for循环来处理

参考:

  • canvas绘制文本内容自动换行(opens new window)
  • javascript - HTML5 canvas ctx.fillText won't do line breaks? - Stack Overflow(opens new window)

#canvas绘制不规则形状填充渐变色

在JS高程3中,有一章专门将使用canvas绘图,今天终于用上了,效果还不错,来看效果,原生js,70行不到

cavas_unnormal_shape.png
A draw of something.


参考之前的笔记:使用canvas绘图 - JS高程3笔记(opens new window)

完整demo: canvas画不规则形状填充渐变背景 - github(opens new window)

#canvas 多次fill会覆盖前面的fill

在使用canvas进行绘图时,封装了一个绘制函数,每次都会填充颜色 ctx.fill(),如果多次执行,只会在最后一次时,整体fill一次?

刚开始以为是后面的fill覆盖了前面的fill,后来网上查了下,第一次fill后,再次fill需要再次调用ctx.beginPath(),不然只会在最后一次fill。

参考: HTML5的canvas标签为什么会覆盖之前画的东西的颜色 (opens new window)

#2020/03/25 周三

#根据文件名后缀判断文件类型不准确

比如我有个1.png文件,我修改下后缀名 1.txt,那前端如果仅凭文件名的后缀来显然是不行的,我们需要根据文件类型的二进制数据标记来判断对应的文件类型,这样才会更加准确,安全性更高

判断文件类型.png

#怎么判断两个文件一模一样

一般文件的md5可能会有重复的,怎么减少这种概率呢?校验分三个部分

  1. 比较整个文件的md5
  2. 选择文件固定位置的几个片段分别计算md5进行比对
  3. 比较文件名是否一样

#md5加密是可逆的吗?

理论上md5加密后,在不知道原始消息的前提下,是无法凭借16个字节的消息摘要(Message Digest),还原出原始的消息的.

但为什么有些网站可以破解md5加密后的密码呢?主要是使用的碰撞检测。它会提前算出一些常用弱密码的md5值,一个个比较。才会让人产生md5可逆的错觉。一般除了md5加密外,我们可以再多进行一些处理(加盐),来进行干扰,提高破解难度

参考:为什么说 MD5 是不可逆的?(opens new window)

#两个不同文件md5可能一样吗?

什么是md5?md5 是 messge digest [daɪˈdʒest] 5 的缩写,意思是信息摘要算法

linux下,在terminal中执行man md5,可以查看对应的文档

md5 -- calculate a message-digest fingerprint (checksum) for a file

md5 -- 为一个文件计算信息摘要指纹('校验和'或'校验码')

md5 -s '123456'
# MD5 ("123456") = e10adc3949ba59abbe56e057f20f883e

md5 1.txt
# MD5 (1.txt) = 6f74626e0749e5353cc7e11767418d43

从上面的例子中我们可以看到,将文件或字符串进行 md5校验 会生成一个 32位 的校验码。问题来了,网上看到 md5加密后一般是128位,而这里只有32位为什么呢?我们要分清16进制与2进制,标准说法是,md5加密后的字符为 128bit(16字节),而一个我们看到的32位是16进制,每一位都可以转为4bit,也就是4个二进制位。1 - f 分别对应 0000 - 1111,所以128bit

一个二进制位(bit)只能表示0或1两种情况,128bit可以表示 Math.pow(2, 128) 2的128次方种情况,定死了,最多只能表示这么多种情况,不同内容的文件实在是太多了,理论上绝对是会超过2的128次方的。

综上:两个不同文件md5是有可能相同的,因为md5最多只能表示2的128次方种情况,而不同的文件绝对大于这个数

虽然两个文件的md5可能一致,但给定一个文件的md5值,想伪造另一个文件的md5值与该值一样,相对还是比较困难的,因此可用于判断文件完整性

参考: 有没有两个完全不一样的文件,但是他们的md5值是一样的?(opens new window)

#2020/03/22 周日

#koa-multer与@koa/multer逻辑差异

之前有了解过以@开头的作用域包,这次在使用koa-multer这个模块时,发现@koa/multer与koa-multer的逻辑居然不一样。源码有些差异,下面来具体看看

// 在使用 koa-multer 时
const multer = require('koa-multer')
router.post('/test', multer().none(), ctx => { 
  let isFormData = ctx.headers['content-type'].startsWith('multipart/form-data')
  // ctx.req node的request对象, ctx.request koa的request对象
  ctx.body = isFormData ? ctx.req.body : ctx.request.body
})

koa-multer这个包是从express的multer包上面加了一层封装,而koa-multer并没有把fileds字段挂载到ctx.request.body上,只维持原来express那样挂载到node的request对象上,也就是ctx.req.body,来看看koa-multer的源码部分

// https://github.com/koa-modules/multer/blob/master/index.js
multer[name] = function () {
    const middleware = fn.apply(this, arguments)

    return (ctx, next) => {
      return new Promise((resolve, reject) => {
        middleware(ctx.req, ctx.res, (err) => {
          err ? reject(err) : resolve(ctx)
        })
      }).then(next)
    }
  }

而@koa/multer则做了处理,可以与上面的例子对比下

// https://github.com/koajs/multer/blob/master/index.js
multer[name] = function() {
    const middleware = Reflect.apply(fn, this, arguments);

    return (ctx, next) => {
      return new Promise((resolve, reject) => {
        middleware(ctx.req, ctx.res, err => {
          if (err) return reject(err);
          if ('request' in ctx) {
            if (ctx.req.body) {
              ctx.request.body = ctx.req.body;
              delete ctx.req.body;
            }

            if (ctx.req.file) {
              ctx.request.file = ctx.req.file;
              ctx.file = ctx.req.file;
              delete ctx.req.file;
            }

            if (ctx.req.files) {
              ctx.request.files = ctx.req.files;
              ctx.files = ctx.req.files;
              delete ctx.req.files;
            }
          }

          resolve(ctx);
        });
      }).then(next);
    };
  };

我们再来看看使用@koa/multer的情况,就比较方便了,和其他数据一样挂载到 ctx.request.body

const multer = require('@koa/multer')
router.post('/test', multer().none(), ctx => { 
  ctx.body = ctx.request.body
}) 

综上,如果某个模块有两种包名,建议先考虑@开头的作用域包,通常这种功能会新点。后面迭代维护应该都是以这个为准

#2020/03/20 周五

#npm包前面加@是什么意思(vue-cli与@vue/cli)

Vue CLI 的包名称由 vue-cli 改成了 @vue/cli。 如果你已经全局安装了旧版本的 vue-cli (1.x 或 2.x),你需要先通过 npm uninstall vue-cli -g 或 yarn global remove vue-cli 卸载它。

今天看vue-cli文档,发现上面的这段话 vue-cli 改为了 @vue/cli,这两个npm有什么区别呢?npm包前面加@是什么意思,查了下官网

npm包前面加@,代表scopes相关的包,可以理解为作用域(范围)包,作用域使我们可以创建与其他用户或组织创建的包同名,而不会发生冲突。

A scope allows you to create a package with the same name as a package created by another user or Org without conflict.

作用域名称是介于@和斜线之间的所有内容:

The scope name is everything between the @ and the slash:

// “npm” scope:
@npm/package-name
// “npmcorp” scope:
@npmcorp/package-name

npm包一个诟病就是包名很容易被占用的问题,占用后用其他人就不能用了。而作用域包类似于创建了一个命名空间,不同的命名空间,可以使用相同的包名

作用域的命名不是谁便就能用的,只有两种可以使用:自己的用户名、自己创建的组织名

注意:必须先注册一个npm用户帐户,然后才能发布用户作用域的npm软件包。此外,要发布组织作用域的软件包,您必须创建一个npm用户帐户,然后创建一个npm Org(组织)。

在 vue-cli 中可以用@vue/cli说明使用了vue这个npm账号或者组织发布了该包。

参考:

  • npm学习(十)之如何使用创建、发布、使用作用域包(opens new window)
  • About scopes - Packages and modules | npm(opens new window)
  • Creating and publishing scoped public packages | npm(opens new window)

#2020/03/18 周三

#md怎么加引用注释,脚注

这里有一个注脚[^1],这段话的还有其他意思[^2]在里面

[^1]:这里是注脚内容
[^2]:这里是其他意思的注脚

注脚放到中间也可以

这里有一个注脚^1,这段话的还有其他意思^2在里面

注脚放到中间也可以

#md中链接的另一种写法

我是一段文字,[baidu][1]、[qq][2]里面有链接

[1]: http://baidu.com "baidu"
[2]: http://qq.com "qq"

我是一段文字,baidu (opens new window)、qq (opens new window)里面有链接

#2020/03/17 周二

#Node.js的核心用处及应用场景

  1. 打包构建、工程化,主要依赖基础的fs模块,文件读写,如xxx-cli(脚手架)、webpack、parcel、hexo,node在打包构建、前端工程化这块基本影响了整个前端的开发过程,各框架基本都有基于node的cli,快速生成脚手架,使开发更加高效、规范。

  2. 写后台接口,主要依赖基础的http模块,处理请求和响应,如 express.js、koa.js,一般主要用于模拟假数据接口, 调UI、交互效果以及做一些请求响应方面的自测

  3. 综合应用:获取数据+渲染页面(高并发、高性能),koa.js对于开发商业化应用来说还是比较单薄,egg.js基于koa做了一些增强,让node也可以做企业级应用。阿里的使用场景就是一个很好的例子,基础设施大部分采用 Java 实现,变化较少,有事务要求的 Business Services 通常使用 Java。而Node主要用于替代过去php、jsp使用场景, 用在需要快速迭代,需求变化非常快的用户侧。node已经经受了阿里双11的考验,技术上是可行的。

题外话:个人认为综合应用这块,自己玩玩还可以,小团队或node不是非常强的技术团队尽量不要尝试,阿里能做好这块是因为国内顶尖的node方面人才基本都在阿里,经过多年实践踩坑,拥有相对完善的node基建和生态。目前市面上前端里node强的比较少,饿了么为了招node服务端开发,还专门写了个node相关的面试教程。可想而知这方面人才有多少。

node支持高并发的原因:

  • node.js基于异步I/O,接收到请求后,直接开一个I/O线程去执行,然后就不管了,立即继续执行主线程。等I/O线程执行完成后,直接执行对应的回调函数即可。省去了许多等待请求的时间
  • 事务驱动,主线程通过event loop事件循环触发的方式来运行程序,这一条暂时还不是很理解,先写上~

参考:

  • 如何评价阿里开源的企业级 Node.js 框架 EggJS?(opens new window)
  • Node.js:浅析高并发与分布式集群(opens new window)
  • 天猫双11前端分享系列(四):大规模 Node.js 应用(opens new window)
  • egg.js(opens new window)
  • node-interview | ElemeFE(opens new window)

#webpack与parcel区别

webpack与parcel都是打包工具,webpack功能强大,但比较重,配置项比较多,有点繁琐。而parcel就是为了解决配置项太多这个问题的,它默认集成了通用的常规功能,零配置,如果自定义较多,还是推荐webpack

If you don't want to worry about configuring everything and your needs are common needs, you should go directly with parcel. Parcel provides defaults (for babel-preset-env, post-css, html, etc) that fits most scenarios and works for everybody. You don't have to worry about configuring anything.

From the other hand, if you need a more customization, you should go with webpack. Keep in mind that you will have to setup everything that you need, explicitly set those things.

参考:Webpack vs Parcel - Stack Overflow(opens new window)

#mac使用触控板拖动复制、移动窗口

今天才意识到,每次要复制一段文字或移动某个应用窗口,我都是点击触控板再拖动。而且Mac Air的触控板按的声音比较响,于是找了下是否有手势可以支持。

发现可以设置使用 三指拖移 复制文字和拖动窗口,三个手指放上去拖动就可以了。

设置方法:打开系统统偏好设置 => 点 “辅助功能” => 点 “鼠标与触控板” => 点 “触控板选项” => 先勾上启用拖移,然后选择“三指拖移”,点击 “好”

图文详情参考: MacBook触控板选中/复制(opens new window)

#2020/03/12 周四

#关于vue和react思考

我个人理解 vue组件设计的对新手超级友好,使用门槛很低。虽然我没用过react,但我可以猜到React应该也是牺牲了一定的用户使用门槛,来实现更加灵活的js控制。而vue牺牲了用js控制的灵活度来降低了用户使用门槛。侧重点不一样,但底层实现基本都差不多,比如虚拟dom、hooks,只是开放给用户的调用方式不一样。

前端框架再怎么变动,就算弄出一朵花来,也只是对dom的修改,最终还是会回归到 js高程3里面讲到的dom章节部分内容。

#git commit 提交信息有误怎么修改

如果不小心提交了,但没有push,可以使用下面的命令来修改上一次的commit信息

git commit --amend -m 'xxx'

#他人提交了package-lock.json的更新导致拉取时和本地冲突

一般在npm install 时会修改package-lock.json文件,我一般不会提交这个更新,但今天发现有人提交。我拉取时,提示这个文件冲突,导致拉取不下来,我又不想提交更新,所以尝试用下面的命令,将工作区该文件的修改丢弃,再拉取

git checkout -- package-lock.json

拉取成功后,npm run serve 基本没什么影响

#2020/03/11 周三

#vue封装组件方式的思考

在封装组件时,一般我们使用的方法是

把组件单独放到一个xx.vue,然后需要引入时在components使用懒加载引入再使用

我就在想,每次引入组件都需要三步

  1. 把组件通过 components 引入
  2. 在template中写对应的代码
  3. 在data中写对应的数据,methods里写绑定的事件

会不会太麻烦了,我希望像element的组件那样,通过 this.$message.error(e.message) 这样直接调用一个组件

于是我尝试使用js的方式来调用单文件组件(.vue),在之前02/20号写过方法,除了直接挂载到body外,也可以挂载到任何地方,只要你能拿到对应的dom,可以使用ref属性,再通过this.$refs['xx']来获取其DOM 元素和组件实例。

// 引入该组件
import ShowInfo from 'showInfo.vue'

// 通过js调用
clickShow() {
  // 创建一个vue组件
  const Component = Vue.extend(ShowInfo)

  // 在文档之外渲染并且随后挂载,返回对应的Vue实例(vm)
  let showInfoVue = new Component().$mount() 

  // 将组件实例的dom,append到当前页面body下
  this.$el.appendChild(showInfoVue.$el) 
}

其实你发现没,用js直接调用vue组件可以是可以,但也要比正常情况下写更多代码,比如

  1. js调用vue的方法需要封装为一个class
  2. 以上面的示例为例子,通过js调用组件,我们需要一个成功的回调,以及传参到组件,在获取到ShowInfo时,我们需要知道我们引入的只是一个'js对象'。我们可以在对象的methods里面注入方法,用来获取传入的值,或挂载成功后的回调。这样相当于mixin,但.vue组件实现里使用这些注入的事件时会不好理解,有种默认其妙多出来全局函数的疑惑
  3. 以js方法写的组件,不能兼容默认的引用方法,如果要支持那就要写一些额外的代码
  4. 你会发现逻辑会变的不好理解,不够简单,对新手不友好,如果需要其他人维护时,可能不好理解为什么这么做

综上所述:默认的封装调用组件的方式就很好,简单、明了,你想在调用的时候轻松,那么在封装组件时,就会增加对应的工作量,整体工作量差不多。

我们再来看element组件,对于内容比较少的,比如通知类,element都提供的是js调用方法,而没有普通的组件调用方法。且一般挂载到body上。为什么dialog组件没有封装成js呢?我的理解是dialog里面的内容可扩展性很强,如果改为js调用,可能会出现把大量代码写在js的情况,或者需要写VNode的render方法。就显得不够优雅了。

总结:在封装组件时,什么时候用封装为js调用方式、什么时候采用普通的封装呢?我的理解是可以通过下面几个方面来进行评估

  1. 被封装的组件需求是否稳定,有没有可能会在后面经常变更或者进行渐进式增强,如果不稳定,不建议封装为js调用方式,对于可扩展性强的,还是建议使用普通组件封装方式,更利于维护
  2. 是否是挂载到body下,还是需要放到任意的div内? 一般普通组件更好放置。如果挂载到body下,可以考虑封装为js调用方式
  3. 是否功能相对单一简单,如果组件功能单一简单建议封装为js调用方式
  4. 该组件是否在页面里大量被调用 大量的被调用,意味着大量重复的代码,可以考虑封装为js调用方式,增加组件实现时的复杂度,来降低调用时的复杂度

总之,不管怎么样封装,当有人问你为什么这么封装时,你要能够说出你自己的理由。

#2020/03/10 周二

#vue自定义组件v-model属性实现dialog组件的二次封装

当某个组件是element的dialog组件时,我们需要对dialog进行隐藏显示,当子组件里的dialog关闭时,需要修改父组件传入的值,尽管不是表单组件也可以使用v-model来解决,先来看看怎么调用