# 2020年09月技术日常

# 2020/09/30 周三

# AMD、UMD、CommonJS、ES Modules的理解

在ES6之前,JS本身没有模块功能,社区出现了 CommonJS 和 AMD 等模块方案,ES6后, JS开始原生支持模块,一般称之为 ES Modules

  • CommonJS: 主要用于服务端,是 node 中使用的 require、module.exports。从 Node.js v13.2 版本开始,Node.js 已经默认打开了 ES6 模块支持。但需要采用 .mjs 后缀文件名。如果不希望将后缀名改成.mjs,可以在项目的package.json文件中,指定type字段为module。
  • AMD: Asynchronous Module Definition,异步模块定义,主要用于客户端(浏览器),以 RequireJS 为代表 define、require
  • UMD: Universal Module Definition,通用模块定义模式,主要用来解决 CommonJS 模式和 AMD 模式代码不能通用的问题,并同时还支持老式的全局变量规范。
  • ES Modules 是 ES 标准模块系统,import、export ,可以和 CommonJS混合使用
/*
 * bundle.js  UMD webpack打包后的js
 * 判断define为函数,并且是否存在define.amd,来判断是否为AMD规范,
 * 判断module是否为一个对象,并且是否存在module.exports来判断是否为CommonJS规范
 * 如果以上两种都没有,设定为原始的代码规范。
 */
(function (global, factory) {
    typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
    typeof define === 'function' && define.amd ? define(factory) :
    (global = global || self, global.myBundle = factory());
}(this, (function () { 'use strict';

    var main = () => {
        return 'hello world';
    };

    return main;

})));
// index.html
<script src="bundle.js"></script>
<script>
  console.log(myBundle());
</script>

区别:

  1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。CommonJS一旦输出一个值,模块内部的变化就影响不到这个值。ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
  2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
  3. CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。
// CommonJS模块
let { stat, exists, readfile } = require('fs');

// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

// ES6模块
import { stat, exists, readFile } from 'fs';

参考

# Script error产生原因与解决方法、调试技巧

一般为了安全起见,如果当前页面引入了跨域的 js 文件,当这个 js 文件出现错误,监听错误会无法获取对应的行号、message。仅会将 message 设置为 Script error.

本地来做一个试验,重现该问题,用 koa 写两个服务,分别监听 5001/5002端口,同时开启服务

/**
 * index_a.js
 * index_b.js 代码和下面的基本一致,只是将 5001 改为 5002
 */
const Koa = require('koa')
const app = new Koa()

app.use(require('koa-static')(__dirname + '/public'))

app.listen('5001', () => {
  console.log('server listen on 5001 port')
})

public/index.html 测试页面中,我们引入 5001端口的 a.js 或 5002 端口的 a.js,看具体 error 信息

<body>
  <button onclick="btnClick()">点击</button>
  <script src="http://127.0.0.1:5001/a.js"></script>
  <!-- <script src="http://127.0.0.1:5002/a.js"></script> -->
  <script>
    window.addEventListener('error', e => {
      console.log(e, e.message)
    })
  </script>
</body>
<!-- 
// a.js代码
function btnClick() {
  console.logaaa('aaa') // 故意制造一个错误
}
-- >

访问 http://127.0.0.1:5001/index.html,然后测试不同端口下执行情况,具体如下图

cross_origin_script_error.png

知道问题后,我们可以通过使用同域的方式引入 js。如果一定要使用跨域的,需要设置下面两点

  1. script引入时加入 crossorigin="anonymous",anonymous 即不发送 Cookie 和 HTTP 认证信息
  2. 服务器对应的js,增加允许跨域
// html里
// <script src="http://127.0.0.1:5002/a.js" crossorigin="anonymous"></script>
// index_b.js 5002端口增加允许跨域

const Koa = require('koa')
const app = new Koa()
// 允许跨域
app.use(async (ctx, next) => {
  ctx.set({
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Headers': '*',
    'Access-Control-Allow-Methods': '*',
    'Access-Control-Allow-Credentials': 'true', // 允许携带cookie
    'Access-Control-Max-Age': 3600 // 对于相同的请求,仅在第一次发送options预检请求,之后1小时内不需要预检请求
  })
  await next()
})
app.use(require('koa-static')(__dirname + '/public'))
app.listen('5002', () => {
  console.log('server listen on 5002 port')
})

设置好后,我们再测试就会发现可以正常显示跨域的信息了。如下图,完整demo参见 跨域Script_error测试demo | github (opens new window)

cross_origin_script_error_fix.png

参考:Script error.全面解析 | Fundebug (opens new window)

# 2020/09/26 周六

# 为什么会有回流/重排(Reflow)和重绘(Repaint)?怎么避免?

1. 什么是回流或重绘

  • 浏览器使用流式布局模型 (Flow Based Layout)
  • 浏览器把 HTML 解析成 DOM,把 CSS 解析为 CSDOM,DOM 和 CSDOM 合并就产生了 Render Tree(渲染树)
  • 根据Render Tree,计算各个元素在页面中的大小和位置,绘制到页面上。

回流/重排(Reflow):当 Render Tree 中部分或全部元素的尺寸、结构或位置发生改变时,浏览器会重新渲染部分或整个文档的过程就称为Reflow

重绘(RePaint): 当页面中元素样式的改变并不影响它在文档中的位置时,浏览器会重绘该区域,这个过程称为重绘

2. 回流和重绘的比较

DOM、CSS style的改变会照成回流或者重绘,回流比重绘更消耗性能

  • 回流:重新布局,会引起元素位置变化的就会reflow,比如修改 DOM 的宽高、字体大小、窗口大小改变、元素位置改变等
  • 重绘:重新绘制区域,不改变元素位置,比如修改背景、颜色,visibility等

3. 如何避免回流或重绘

CSS:

  • 尽可能在DOM树的最末端改变class
  • 避免设置多层内联样式,将动画效果应用到 position 属性为 absolute 或 fixed 的元素上
  • 避免使用CSS表达式(例如:calc())

JavaScript

  • 避免频繁操作样式、DOM
  • 在 documentFragment 或 display 为 none 的元素上进行 dom 操作不会引起回流或重绘
  • 对复杂动画,使用绝对定位脱离文档流,避免整体回流

参考:

# 图片占位、懒加载、预览

使用占位图片有什么好处?

  1. 用户体验更好,标识此处有图片,但正在加载中的这一个状态,避免弱网或图片较大时,完全不显示的问题
  2. 不阻塞其他资源加载,更快的整体文档加载速度,不阻塞其他请求。占位图片一般会和图片懒加载一起使用,当滚动到该区域时再加载图片,减少不必要的并发网络请求
  3. 可以避免回流,让图片加载完成后不改变页面的整体布局,避免回流(重新布局),只需要重绘,注意: 需要占位图片与实际图片宽高不变

怎么实现图片懒加载

这里核心问题是怎么判断图片是否已滚动到当前视口。一般可以通过监听页面滚动事件,实时比较元素的 offsetTop 与页面的 scrollTop。另外还可以通过元素的 getBoundingClientRect() 获取当前元素距离视窗的距离,如果小于 window.innerHeight 就加载,下面来看一个简单的实现

<body>
  <div>
    <div style="height:50px;">我是占位文本</div>
    <img class="lazy-img" data-src="http://zuo11.com/images/blog/c/c_vim.png">
    <div style="height:500px;">我是占位文本</div>
    <img class="lazy-img" data-src="http://zuo11.com/images/blog/c/c_saolei_3.png">
  </div>
  <script>
    let imgEls = document.querySelectorAll('.lazy-img')
    let imgs = []
    imgEls.forEach(imgEl => {
      imgEl.style.height = '300px'
      imgEl.style.width = '300px'
      imgEl.src = "placeholder.png"
      // 存储信息用于监听滚动后比对
      imgs.push({
        offsetTop: imgEl.offsetTop,
        el: imgEl,
        src: imgEl.dataset.src,
        isLoad: false
      })
    })
    console.log(imgs)
    // 判断滚动位置,显示图片
    function showImg() {
      console.log('>>>>>>')
      imgs.forEach(item => {
        // 如果图片未加载
        if (!item.isLoad) {
          console.log(window.innerHeight, item.src, item.el.getBoundingClientRect())
          let elPos = item.el.getBoundingClientRect()
          if (elPos.top < window.innerHeight) {
            // 如果元素相对顶部的距离 < 视窗高度,加载图片
            item.el.src = item.src
            item.isLoad = true
          }
        }
      })
    }
    showImg()
    window.onscroll = () => {
      showImg()
    }
  </script>
</body>

图片预览

可以参考Element UI el-image组件实现,Element UI 大图预览 | el-image (opens new window)

参考资料与扩展

# Chrome DevTools Network Waterfall各阶段详解

在Chrom调试工具Network tab下,每个请求都有一个Waterfal diagrams(瀑布图),点击具体接口,Timming(时序)位置会有具体描述。它记录了网络请求各个阶段的耗时,可以用于分析页面加载性能。

waterfall-hover.png

Timing breakdown phases explained(时序分解阶段明细)

Here's more information about each of the phases you may see in the Timing tab:(下面是在Timing标签页中能看到的各个阶段的更多信息)

1. Resource Scheduling(资源调度)

  • Queueing(排队). The browser queues requests when:(浏览器在以下情况会排队请求)
    • There are higher priority requests.(有更高优先级的请求)
    • There are already six TCP connections open for this origin, which is the limit. Applies to HTTP/1.0 and HTTP/1.1 only. (当前请求的 origin/domain,已经有 6 个 TCP 连接打开,这是限制。仅应用于 HTTP/1.0 and HTTP/1.1)
    • The browser is briefly allocating space in the disk cache(浏览器正在磁盘缓存中短暂分配空间)

2. Connecttion Start(连接开始)

  • Stalled(停滞/暂缓). The request could be stalled for any of the reasons described in Queueing. (处于排队中描述的任何原因,请求都有可能停滞/暂缓)
  • DNS Lookup(DNS查询). The browser is resolving the request's IP address. (域名解析成IP。在浏览器和服务器进行通信之前, 必须经过DNS查询, 将域名转换成IP地址. 在这个阶段, 你可以处理的东西很少. 但幸运的是, 并非所有的请求都需要经过这一阶段.)
  • Initial connection(初始化连接). The browser is establishing a connection, including TCP handshakes/retries and negotiating an SSL.(浏览器正在创建一个连接,包括握手/重试)和 协商SSL。在浏览器发送请求之前, 必须建立TCP连接. 这个过程仅仅发生在瀑布图中的开头几行, 否则这就是性能问题.
  • SSL(SSL协商) SSL/TLS Negotiation 如果你的页面是通过SSL/TLS这类安全协议加载资源, 这段时间就是浏览器建立安全连接的过程.
  • Proxy negotiation(代理协商). The browser is negotiating the request with a proxy server. (浏览器正在与代理服务器协商请求)

3. Request/Response(请求/响应)

  • Request sent(请求发送). The request is being sent. 请求开始发送

  • ServiceWorker Preparation(ServiceWorker准备). The browser is starting up the service worker. (浏览器正在启动 Service Worker)

  • Request to ServiceWorker(请求到ServiceWorker). The request is being sent to the service worker.(请求被发送到 service worker)

  • Waiting (TTFB) 等待. The browser is waiting for the first byte of a response. TTFB stands for Time To First Byte. This timing includes 1 round trip of latency and the time the server took to prepare the response.(浏览器正在等待响应的第一个字节。 TTFB代表到第一个字节的时间。 此时间包括一次往返延迟和服务器准备响应所花费的时间) TTFB 是浏览器请求发送到服务器的时间+服务器处理请求时间+响应报文的第一字节到达浏览器的时间. 我们用这个指标来判断你的web服务器是否性能不够, 或者说你是否需要使用CDN.

  • Content Download. The browser is receiving the response. (浏览器正在接收响应),这是浏览器用来下载资源所用的时间. 这段时间越长, 说明资源越大. 理想情况下, 你可以通过控制资源的大小来控制这段时间的长度.

  • Receiving Push. The browser is receiving data for this response via HTTP/2 Server Push.(浏览器正在通过HTTP / 2服务器推送接收此响应的数据)

  • Reading Push. The browser is reading the local data previously received.(浏览器正在读取先前接收的本地数据。)

参考资料及扩展:

# append和apendChild的区别

在JS高程3中,并没有讲到 append,基本都是用的 appendChild,所以有知识盲点,这里对比下 append 和 apendChiild的区别

方法 所属对象 功能 返回值 兼容性
appendChild (opens new window) Node (opens new window) 在元素内容里追加一个 Node 节点 append的节点 基本都兼容
append (opens new window) ParentNode (opens new window) 在元素内容里追加一个或多个 Node 节点或 String undefined 实验性功能 不支持 IE (opens new window)

注意,如果 append 或 appendChild 的元素是已经存在的,会移动该元素,如果需要保持原来的不移动,需要先 cloneNode, 下面来看一个例子

<body>
  <div id="c1"></div>
  <div id="existDiv">我是一段已经存在的dom</div>
  <div id="c2"></div>
  <div id="c3"></div>
  <script>
    // 1.测试基本功能:创建节点,并 appendChild 到 c1元素
    let p = document.createElement('p')
    let text = document.createTextNode('我是一段信息')
    p.appendChild(text)
    console.log(p, p.__proto__) // HTMLParagraphElement
    document.querySelector('#c1').appendChild(p)
    // document.querySelector('#c1').appendChild('1212') // Error, Node

    // 2.测试 appendChild 一个已存在的节点,移动节点,而不是copy
    let existDiv = document.querySelector('#existDiv')
    console.log(existDiv, existDiv.__proto__)  // HTMLDivElement
    console.log('测试appendChild 返回', document.querySelector('#c2').appendChild(existDiv))
    // console.log('测试append 返回', document.querySelector('#c2').append(existDiv))

    // 4.测试append
    let span = document.createElement('span')
    // 不要写成链式调用,它返回的是成功appendChild的节点
    span.appendChild(document.createTextNode('测试append'))
    document.querySelector('#c3').append('<span>abc</span>')
    document.querySelector('#c3').append(span, 'a', 'b', '<div>c</div>')
  </script>
</body>

执行效果如下图

append_vs_appendChild.png

关于Node节点Element元素对象的一些值

<div id="myDiv" class="bg test" title="body text" lang="en" dir="rtl">abcdefg</div>
// 3.测试节点类型、对象信息 HTMLDivElement => HTMLElement => Element => Node
// 参考:DOM https://www.yuque.com/guoqzuo/js_es6/hoglme#6bbee863
var div = document.getElementById("myDiv"); // HTMLDivElement
console.log(div.nodeType) // 1 Node.ELEMENT_NODE
console.log(div.nodeName) // DIV
console.log(div.id); // myDiv
console.log(div.className); // bg test
console.log(div.title); // body text
console.log(div.lang); // en
console.log(div.dir); // rtl   

# Google Analytics、Google AdSense、Google Search Console分别是干什么的?

在站点管理这一块,Google的工具有好几个,我现在知道的有三个,他们分别对应不同的功能,下面来看看他们的介绍、区别

平台 功能
Google Search Console (opens new window) 搜索控制台 站长工具、sitemap提交,展现量/点击量简单查看,死链提交等
Google Analytics (opens new window) 分析 偏数据分析,统计实时用户/累计用户/会话时长/跳出率等,受众群体、流量来源、转化率、页面价值等
Google AdSense (opens new window) 广告联盟 用于在自己站点投放 Google 广告,赚取收益

google站长工具对比

百度统计类似 Google Search Console 和 Google Analytics 的结合体,但 Google Analytics 的细分功能更加强大

关于 Google AdSense,有一本书可以参考下:《Google AdSense 实战宝典》

# Google AdSense被拒理由网站已下线或无法访问怎么解决

在自己网站的 head 部分加入 AdSense 的校验代码后,会有一段审核的时间。在这个过程中,网站可能会有来自福建龙岩的 203.208.60.* IP段蜘蛛来爬取你的网页。蜘蛛会爬取JS渲染后的效果,可以触发百度统计等js代码。不用屏蔽,这属于 Google 蜘蛛正常爬取,Google Analytics 在计算访客时会忽略这一部分访客信息。

蜘蛛爬完的第二天,就收到了邮件,说审核不通过,看了下原因:"网站已下线或无法访问怎么解决" ,如下图所示。但我的网站是确定可以正常访问的。

网站已下线或无法访问怎么解决

网上查了下,这种情况,需要在域名解析里增加两条解析记录,解析路线设置为境外。然后重新提交审核,等待即可。但我加了解析后,等了一段时间,又是同样的理由被拒绝,于是找到了官方的回复。首页不能重定向,比如 xx.com 不能重定向到 xx.com/home,或者 www.xx.com,再次修改后,重新提交审核,大概5、6天后审核通过。参考 为什么一直提示找不到adsense代码和网站已下线或无法访问? - AdSense Community (opens new window)

主机记录 记录类型 解析路线 记录值 TTL
@ A 境外 服务器IP地址 10分钟(默认)
www A 境外 服务器IP地址 10分钟(默认)

域名解析.png

www 和 @ 这样的主机记录是什么意思?

  • www:解析后的域名为 www.xx.com
  • @:直接解析主域名 xx.com。
  • *:泛解析,匹配其他所有域名 *.xx.com。

关闭 zuo11.com 到 www.zuo11.com 的重定向,大概是09月27,再次提交后,10月02号收到审核通过邮件。如下图

google_ad.jpg

# 2020/09/25 周五

# Final Cut Pro 使用网易见外工作台自动生成字幕(免费)

1.将视频的音频单独导出

在 Final Cut Pro 里导入视频素材,并拖入到时间线中。点击右上角分享按钮,导出 母版文件,一般默认是导出视频和音频。在设置里设置为仅音频,WAV格式。然后下一步,保存 .wav 格式文件

final_cut_export_wav.png

2.在网易见外工作台将音频生成.srt字幕文件

进入 网易见外工作台 (opens new window),如果没注册先注册。登录成功后,新建项目 => 选择语音转写 => 填写信息、上传之前导出的 wav 音频文件,提交后,一般等几分钟就好了(具体等待时间看音频时长)

final_cut_wav2caption.png

生成好后,点击进入详情页,可对生成的字幕进行修改,修改完成后点击导出,会生成 .srt 字幕文件

final_cut_edit_caption.png

3.将字幕文件导入到Final Cut Pro项目中

点击 Final Cut Pro 顶部 文件 => 导入 => 字幕,选择上一步生成的 .srt 文件即可。注意:如果出现下图中 "字幕文件包含不支持的数据或可能已损坏" 的问题,那么就还需要使用 Aegisub 将字幕文件转一下格式

final_cut_import_fail.png

Aegisub 官网下载很慢,我找的是PC6.com下载的,Aegisub 下载地址 | PC6.com (opens new window),下载完成后,打开该软件,点击顶部菜单 文件 => 打开字幕,打开后再点击 文件 => 导出,默认文字编码为 UTF-8 => 导出 => fileType 文件类型选择 .srt => 存储

这样转换过的 .srt 文件,再重新导入到 Final Cut Pro,这样就大功告成了。

如果字幕导入进去了,还是看不到字幕,要确保视频显示比例,是否是合适,可能放大了,字幕在最底部,显示区域正好看不到字幕。

4.每次导出时,一定要记得在角色-字幕那里勾选字幕

final_cut_add_zimu.png

# Final Cut Pro 将视频拖到时间线后不是全屏的问题

注意在资源库里点击导入的视频素材,预览时,显示的是视频本身的分辨率。而拖到流水线后,点击流水线上的视频,显示的是当前项目创建时预设的分辨率,如果你视频本身是 2800 * 1600 但最开始新建项目,选择的分辨率不是 2800 * 1600 就会出现非全屏的情况。

final_cut_not_fullscreen.png

# Final Cut Pro 导出7分钟视频就要40多G,视频太大怎么处理

一般用 QuickTimer 录制屏幕后,生成的文件默认视频编解码器为 Apple ProRes 422,导出的视频预计有40多G,我的视频才7分钟,有点恐怖。

一般这种情况,在导出母版文件时,将视频编解码器改为 H.264 即可。预计是4G多,实际导出700多M左右。清晰度给我的感觉是看不出差别。

final_cut_too_large.png

# Final Cut Pro 怎么加音效,自带的声音效果变没了怎么处理

在 Final Cut Pro 的左上侧区域是浏览器区域,分别有三个模块:资源库、照片和音频、字幕和发生器

  • 资源库用于导入和管理资源。
  • 照片和音频用于管理音视频,声音效果里有很多背景音乐,注意:如果这里的声音效果是空的列表,则需要点击左上角的 apple 标志,打开系统偏好设置 => 软件更新 => 升级需要升级的包 => 升级完成后再打开 Final Cut Pro 就能看到音效了

final_cut_audio.png

  • 字幕用于手动添加各种类别的字幕,发生器用于添加剪短的背景、动画视频片段

# 使用git ssh方式解决RPC failed; HTTP 413 curl 22 The requested URL returned error: 413

由于改了博客生成系统的生成方式,每个html都改了,导致文件相对较多,也不算大,才 4.57 M, 但 git push 一直push不了,提示如下

error: RPC failed; HTTP 413 curl 22 The requested URL returned error: 413
fatal: the remote end hung up unexpectedly

git_push_hang_up.png

网上查了下,可以改用 ssh 的方式 push 代码,结合之前的经验,大文件 git clone 不下来,可以换 ssh 方式 git clone。这条路应该可行。找了下之前的笔记 使用ssh方式拉取代码的步骤 (opens new window),下面来梳理下整个过程。

1.使用git平台(这里是github)邮箱账号生成公钥和私钥

# 先查看 github 对应的邮箱,user.email 就是git邮箱账号
git config -l
# credential.helper=osxkeychain
# user.name=guoqzuo
# user.email=guoqzuo@gmail.com
# core.quotepath=false

# 开始生成秘钥
ssh-keygen -t rsa -C 'guoqzuo@gmail.com'
# 全部默认、回车,也可以输入密码,我这里输入了密码,后面每次pull, push都需要输入密码。
# Generating public/private rsa key pair.
# Enter file in which to save the key (/Users/guoqzuo/.ssh/id_rsa): 
# Enter passphrase (empty for no passphrase): 
# Enter same passphrase again: 
# Your identification has been saved in /Users/guoqzuo/.ssh/id_rsa.
# Your public key has been saved in /Users/guoqzuo/.ssh/id_rsa.pub.

# 查看公钥内容,copy后到github里设置ssh key
cat ~/.ssh/id_rsa.pub 

2.copy公钥,并配置到对应的git平台里(这里是github)

登录 github,在个人设置里找到 ssh key,添加 ssh key,内容为 cat ~/.ssh/id_rsa.pub 后显示的内容

githu_ssh_key.png

3.测试是否配置成功

使用下面的命令,测试 github ssh key 是否配置成功,如果之前生成 key 时有输入 passphrase,这里就要输入

ssh -T git@github.com
# Enter passphrase for key '/Users/guoqzuo/.ssh/id_rsa': 
# Hi zuoxiaobai! You've successfully authenticated, but GitHub does not provide shell access.

4.将仓库http的origin地址,替换为ssh的origin地址

# 查看 remote 地址
git remote -v
# origin  https://github.com.cnpmjs.org/zuoxiaobai/zuo11.com (fetch)
# origin  https://github.com.cnpmjs.org/zuoxiaobai/zuo11.com (push)

# 设置 remote 地址为对应仓库的 ssh 地址
git remote set-url origin git@github.com:zuoxiaobai/zuo11.com.git

# 查看设置是否生效
git remote -v                                                    
# origin  git@github.com:zuoxiaobai/zuo11.com.git (fetch)
# origin  git@github.com:zuoxiaobai/zuo11.com.git (push)

配置好后,再进行git pull, git push 就可以 push 成功了,虽然也有点慢,但至少不会报错。可以push成功

guoqzuo@zuomac zuo11.com % git pull
Enter passphrase for key '/Users/guoqzuo/.ssh/id_rsa': 
Already up to date.

guoqzuo@zuomac zuo11.com % git push
Enter passphrase for key '/Users/guoqzuo/.ssh/id_rsa': 
Enumerating objects: 685, done.
Counting objects: 100% (685/685), done.
Delta compression using up to 8 threads
Compressing objects: 100% (358/358), done.
Writing objects: 100% (360/360), 4.57 MiB | 150.00 KiB/s, done.
Total 360 (delta 306), reused 0 (delta 0)
remote: Resolving deltas: 100% (306/306), completed with 296 local objects.
To github.com:zuoxiaobai/zuo11.com.git
   85b3d21..bf8eab3  master -> master
guoqzuo@zuomac zuo11.com % 

# ssh生成rsa key时Enter passphrase加和不加有什么区别

在使用ssh前,需要先生成公共/私有rsa密钥对。(Generating public/private rsa key pair.),一般使用 ssh-keygen -t rsa -C '邮箱@xx.com' 命令,这个时候,会有下面的提示

Enter passphrase (empty for no passphrase):

输入通行码(密码、口令短语),如果不输入,那么就是不使用密码。如果输入了密码,这个有什么用呢?

个人理解这个主要是安全方面的考虑,如果你私钥泄露了,还有一个保护机制。在你每次使用 ssh 做敏感操作时,就会提示你输入密码

比如我们使用 ssh 方式拉取 git 仓库代码,远程 origin 源是 ssh 地址,那么提交代码时,每次 git pull, git push 时都需要输入对应的秘钥。相对会比较麻烦一点。

# 2020/09/24 周四

# xx.github.io打不开的问题

xx.github.io 偶尔会出现打不开的问题,网上查原因是电信运营商 DNS 污染(域名解析不到正确的 IP 地址)

只要本地手动设置 host 即可正常访问,假设我要访问 https://zuoxiaobai.github.io/fedemo/ 那手动指定一下这个域名的解析ip即可

# 修改 /etc/hosts
sudo vi /etc/hosts
# 添加如下host记录
185.199.108.153 zuoxiaobai.github.io
# 查看host文件
cat /etc/hosts    
##
# Host Database
#
# localhost is used to configure the loopback interface
# when the system is booting.  Do not change this entry.
##
127.0.0.1	localhost
185.199.108.153 zuoxiaobai.github.io
255.255.255.255	broadcasthost
::1             localhost

保存后,就可以打开 https://zuoxiaobai.github.io/fedemo/

参考:

# js生成二维码怎么实现

QRCode.js是一个生成二维码的js库,使用 HTML5 Canvas将二维码绘制到dom上,不依赖任何库。支持svg绘制方式

QRCode.js is javascript library for making QRCode. QRCode.js supports Cross-browser with HTML5 Canvas and table tag in DOM. QRCode.js has no dependencies.

QRCode.js使用很简单,下面来写一个自动生成二维码的工具,在线示例 在线生成二维码工具 (opens new window)

qrcode_js.png

<body>
  <div style="margin-bottom:20px;">
    <input id="input" type="text">
    <button id="btn">生成二维码</button>
  </div>
  <div id="qrcode"></div>
  <!-- 在 https://github.com/davidshimjs/qrcodejs 下载的文件 --->
  <script src="./qrcode.min.js"></script>
  <script type="text/javascript">
    let inputEl = document.getElementById('input')
    let btnEl = document.getElementById('btn')
    btnEl.onclick = () => {
      document.getElementById("qrcode").innerHTML = ''
      // new QRCode(document.getElementById("qrcode"), inputEl.value);
      let qrcode = new QRCode(document.getElementById("qrcode"), {
        text: inputEl.value,
        width: 128,
        height: 128,
        colorDark: "#000000",
        colorLight: "#ffffff",
        correctLevel: QRCode.CorrectLevel.H
      });
      // 上面使用清空dom,再重绘,也可以使用内置的方法来重绘
      // qrcode.clear(); // clear the code.
      // qrcode.makeCode(inputEl.value); // make another code.
    }
  </script>
</body>

注意:如果输入的是纯文本,那扫描后打开的就是纯文本,如果是 http:// 开头的网址,会直接跳转到对应的 URL

完整demo,参见: 前端生成二维码 | github (opens new window)

参考

# 什么是e2e test,除了单元测试外还有什么测试?

e2e 是 end to end 的缩写,翻译为端到端测试。单元测试只是整个测试金字塔中的一部分。其他类型的测试还包括 e2e(端到端)测试,快照比对测试等。

  1. 单元测试:最小巧、最简单、最有用的测试。它们通过隔离单个组件的每一个部分,来在最小工作单元上进行断言。能帮助开发者思考如何设计一个组件或重构一个现有组。通常每次代码发生变化的时候它们都会被运行。

  2. 快照比对测试:会保存你的 Vue 组件的标记,然后比较每次新生成的测试运行结果。如果有些东西改变了,开发者就会得到通知,并决定这个改变是刻意为之 (组件更新时) 还是意外发生的 (组件行为不正确)。

  3. e2e(端到端)测试致力于确保组件的一系列交互是正确的。它们是更高级别的测试,例如可能会测试用户是否注册、登录以及更新他们的用户名。这种测试运行起来会比单元测试和快照比对测试慢一些。运行起来会更慢很多。这些测试通常只在部署前运行,来确保系统的每个部分都能够正常的协同工作。

更多测试 Vue 组件细节,可以参考 Edd Yerburgh 的书《Testing Vue.js Applications》,中文翻译版为《Vue.js应用测试》

参考:额外的上下文 - Vue 组件的单元测试 (opens new window)

# vue单元测试

在 Vue.js 官网 学习 => CookBook 下有一个 Vue 组件的单元测试 文档。

为什么要测试?组件的单元测试的好处:

  • 提供描述组件行为的文档
  • 节省手动测试的时间
  • 减少研发新特性时产生的 bug
  • 改进设计
  • 促进重构
  • 自动化测试使得大团队中的开发者可以维护复杂的基础代码

Vue Test Utils 是 Vue 组件单元测试的官方库。推荐使用 Jest 或 mocha-webpack

相关参考文档

# 涉及到状态code相关判断逻辑,建议使用常量

对于一些状态较多的场景,当我们需要进行一些逻辑时,如果我们用状态的code来写判断逻辑。当后端提供的这个状态文案、code变更或调整,改动可能会很麻烦,特别是功能较多时。

对于状态相关的判断逻辑,尽量把状态定义为容易识别的常量。这样状态名或状态code变更后,就不需要做大量修改了。

来看一个实例,假设项目的状态有几种:待审核 0 、待跟进 1、已立项 2、落地成功 3、落地失败 4、 关闭 5 ,有些组件只有待审核可以看到,有些功能只有已立项才能看到

const PROJ_STATUS = {
  PENDING_AUDIT: 0,
  PENDING_FOLLOW: 1,
  ALREADY_SET_UP: 2,
  LAND_SUCCESS: 3,
  LAND_FAILURE: 4,
  CLOSE: 5
}
const PROJ_LABEL_MAP = {
  [PROJ_STATUS.PENDING_AUDIT]: '待审核',
  [PROJ_STATUS.PENDING_FOLLOW]: '待跟进',
  [PROJ_STATUS.ALREADY_SET_UP]: '已立项',
  [PROJ_STATUS.LAND_SUCCESS]: '落地成功',
  [PROJ_STATUS.LAND_FAILURE]: '落地失败',
  [PROJ_STATUS.CLOSE]: '关闭'
}

if (status === PROJ_STATUS.PENDING_AUDIT) {
  // xxx
} else if ([PROJ_STATUS.LAND_SUCCESS,PROJ_STATUS.LAND_FAILURE].includes(status)) {
  // xxx
}

这样虽然看起来麻烦一点,但不管你状态文案、code怎么变,前端的改动都会很小。项目越大,变动后维护的成本相对会越低

# 2020/09/21 周一

# mac下怎么截取屏幕gif图:Gifox

最开始做gif图是使用QuickTime录制屏幕后,再搜索在线视频转gif图的网站进行转换,比较麻烦。

Gifox_logo.png

最近发现一个比较好的软件 Gifox (opens new window),我使用的是免费版的,有水印,最大长度限制为10s。升级付费的版本可以去水印(watermark),且没有10s限制。它功能强大,录制gif很方便

Gifox_opt.png

  1. shift + command + 6 即可录制选中区域进行录制。
  2. 录制完成后,可以像剪辑视频一样剪辑gif图,操作简单
  3. 导出时,可以调整参数,压缩文件大小
  4. 可以直接把QuickTime录制的视频转为gif,并可以剪辑

Gifox_edit.png

# 不全局安装npm包,怎么在项目中执行对应的命令

一般在vue开发中,vue命令可以执行,是因为全局安装了 @vue/cli npm包,那假设你无法使用 npm install -g 全局安装,那怎么在当前项目所在的目录运行命令呢?这里我们使用 zuo-util 这么npm包来介绍3种在项目种运行npm包命令的方法

# 随便创建一个文件夹,并使用terminal进入到该目录,执行 npm init 创建 package.json
npm init
# 输入项目名,英文,这里用 npm_cmd_test,其他全部默认回车
# 运行 zuo 这个命令
zuo
# 找不到这个命令,说明全局没有安装 zsh: command not found: zuo

我们再局部安装下

# 项目里局部安装 zuo-util,非全局安装
npm install zuo-util --save
# 再运行 zuo,还是不行
zuo

npm_zuo_1.png

我们可以使用下面三种方法来运行局部安装的npm包对应的命令

# npx 命令

可以使用npx 命令名,执行项目中的,非全局的命令

npx zuo

# ./node_modules/.bin/命令

一般局部安装的命令都会放到 ./node_modules/.bin/ 目录下,可以通过 . 语法直接运行

./node_modules/.bin/zuo

npm_zuo_2.png

# npm run xxx

在 package.json 里的 scripts 里面,我们可以设置运行的命令

// package.json
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "zuo": "zuo"
  },

按照上面的代码设置后,npm run zuo 即可执行当前目录下的zuo命令

npm_zuo_3.png

# 2020/09/13 周日

# keep-alive只在跳转到B组件页面时缓存当前路由数据,跳到其他页面不缓存路由数据

一般情况下,我们可以使用路由meta参数里传keepAlive为true或false来对某个路由页面做缓存

<keep-alive>
  <router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"></router-view>

假设有A、B、C三个页面组件。B页面只有跳到C页面才缓存数据,跳到其他页面不缓存页面数据

这里 router-view 组件会切换三个组件的显示类似与component is切换组件。我们需要动态的去判断是否缓存B页面组件,可以使用keep-alive的include参数来指定需要缓存的页面组件数据

<!-- 逗号分隔字符串 -->
<keep-alive include="a,b">
  <component :is="view"></component>
</keep-alive>

假设B页面组件的name为 'PageB',那么默认情况下include为'PageB',就表示缓存该页面,如果设置include为''就是不缓存页面数据,核心问题是怎么动态的改变这个include的值。

我们可以在B页面组件路由离开之前的钩子函数里,来修改这个值,注意,需要单独弄一个变量,使用$route.meta.include参数来修改这个值是没有用的,因为$route.meta这个值并不像data里面的数据是双向绑定的,他在进入页面时就固定了。中间修这个值不会触发template里面的模板重新渲染

来看看具体实例

<!-- index.vue 入口页面 http://localhost:8081/keepAlive/ -->
<template>
  <div>
    我是主页面
    <ul>
      <li><router-link to="/keepAlive/a">A组件页面</router-link></li>
      <li><router-link to="/keepAlive/b">B组件页面</router-link></li>
      <li><router-link to="/keepAlive/c">C组件页面</router-link></li>
    </ul>
    include{{ include }}
    <keep-alive :include="include">
      <router-view v-if="$route.meta.keepAlive"></router-view>
    </keep-alive>
    <router-view v-if="!$route.meta.keepAlive"></router-view>
    <!-- 测试 $route.meta 的值是否是双向绑定的 -->
    $route.meta.include {{$route.meta.include}}
  </div>
</template>

<script>
export default {
  data() {
    return {
      include: "PageB"
    };
  },
  created() {
    // 这里修改值后数据,页面的$route.meta.include值并没有刷新,因此不能为了节省变量使用$route.meta来做include的控制
    setTimeout(() => {
      this.$route.meta.include = "test"
      console.log('this.$route.meta.include', 'test')
    }, 3000)
  }
};
</script>

/keepAlive/a、/keepAlive/c只是单独的页面,没有任何逻辑,只是用来测试跳转。主要逻辑是在/keepAlive/b页面

<template>
  <div>
    我是B组件页面
    <el-input v-model="input" placeholder="请输入B组件内容"></el-input>
    <el-radio-group v-model="radio">
      <el-radio :label="3">备选项</el-radio>
      <el-radio :label="6">备选项</el-radio>
      <el-radio :label="9">备选项</el-radio>
    </el-radio-group>
  </div>
</template>

<script>
export default {
  name: "PageB",
  data() {
    return {
      input: "",
      radio: ""
    };
  },
  beforeRouteLeave(to, from, next) {
    if (to.name === "keepAliveC") {
      // 如果跳转的页面C组件,缓存页面数据
      this.$parent.include = "PageB";
    } else {
      // 如果跳转的页面不是C组件,不缓存数据
      this.$parent.include = "";
    }
    next();
  }
};
</script>

以上就可以实现我们的目的了,完整demo参见 keepalive测试demo| github (opens new window)

参考:

# 子组件名称与父组件name相同时死循环的问题

在vue项目中,假设父组件name与子组件名称一致,会造成死循环,出现 Error in nextTick: "RangeError: Maximum call stack size exceeded"的错误,template在编译时,不会去找components引入的组件,而是直接引入自己,形成一个递归组件。要特别注意

<template>
  <div>
    <p>我是index组件</p>
    <test-a></test-a>
  </div>
</template>

<script>
export default {
  components: {
    TestA: () => import("./TestA")
  },
  // name: "testA",
  name: "TestA",
  created() {
    console.log("index comp created");
  }
};
</script>

subcomp_samename.png

完成demo地址,参考 subcomp_samename demo| github (opens new window)

# .eslintignore在不同vscode版本中的差异

.eslintignore文件的作用是,当不想对项目的某个目录进行eslint检查时,可以在这个文件中声明,类似.gitignore文件的效果。

// .eslintignore
mock

上面表示忽略mock文件夹下的代码检查。这样eslint插件就不会显示warning、不会保存后自动fix该文件。在这个文件中去掉mock后,mock下的文件就又可以保存后fix了。

但有一个问题,不同vscode版本的行为可能有差异,有可能把目录加入到了.eslintignore后,保存时还是会fix这个目录下的eslint错误。建议升级到最新版本。

# 父组件加载子组件时,父子组件钩子顺序

<!-- index.vue -->
<template>
  <div>
    <comp-a></comp-a>
  </div>
</template>

在 index.vue 以及 compA.vue 子组件里面都加了对应的钩子函数console,具体顺序如下,父组件mounted好后,才会create子组件

index beforeCreate
index.vue index created
index.vue index beforeMount
index.vue index mounted
index.vue index beforeUpdate
A.vue A beforeCreate
A.vue A created
A.vue A beforeMount
A.vue A mounted
index.vue index updated

如下图,完整demo参见 父子组件钩子函数demo | github (opens new window)

sub_comp_hooks.png

# vue组件的封装性与可操作性:ref拿不到子组件实例以及vuex增加代码复杂度问题

一般在封装vue组件时,怎么把组件封装的更好,更容易维护理解是值得思考的问题

来看一个实际场景:一个组件里有高级查询和列表,他们东西都比较多,需要拆分成两个子组件

<!-- index.vue -->
<project-search></project-search>
<project-list></project-list>

查询的条件参数比较多,假设表单数据变量为 searchForm。

按照封装的完整性原则,自己的组件的数据放到自己的组件中,那么 searchForm 就需要放到 project-search 组件里。

但在index.vue中,我们需要查询列表,依赖searchForm的值,涉及到父组件取子组件的值的问题,一般有四种方法可以选择

  • searchForm变量放到父组件,通过props传递到子组件(这样有点违背组件的封装性)
  • vuex 个人认为,能不使用vuex的场景,尽量不要使用,除非真的需要使用,他会影响代码的简单性。
  • bus 使用 bus 来通信,但和vuex类似,尽量少使用
  • 使用ref直接取。但由于副组件加载子组件时,在mounted钩子里 ref 拿对应的子组件可能会拿不到,需要加 setTiemout,也是一种比较怪的操作

对比这几种情况。我一般使用props,把值直接放到index.vue里,再传递给子组件。这样算是比较好理解的一种方法,虽然封装性不强。但涉及到数据需要交互的场景,必须要做一些妥协。

# 2020/09/12 周六

# npm install -g或者npm link需要sudo的问题

在mac下,安装node后,会出现两个命令 npm 和 node,再使用 npm install -g 时,默认需要管理员权限,可以再前面加 sudo 以管理源方式运行即可。

如果在开发 npm 包的时候。如果开发对应的命令程序,测试时需要在当前目录执行 npm link,这里也需要加 sudo。

# git status中文乱码怎么解决

git status时,如果中问乱码,可以对git进行一个配置即可 git config --global core.quotepath false

具体效果如下图

git_chinese_show.png

# mac安装brew下载慢,安装异常,切换成ruby-china源

mac下使用brew可以很方便的安装svn、nginx等常用的工具。

什么是brew [bruː],它的全称是 Homebrew (opens new window),The Missing Package Manager for macOS (or Linux)。它可以安装macOS或linux下没有的包(Package)。

一般使用ruby来安装

ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

mac自带ruby,但如果直接安装,他会比较慢,直接卡死,安装不上。

ruby_brew_1.png

这就需要使用国内的 ruby 源了。类似与npm与cnpm的概念,这里使用ruby-china的源,具体参考:RubyGems - Ruby China (opens new window)

# 设置ruby源
gem sources --add https://gems.ruby-china.com/ --remove https://rubygems.org/
# 查看ruby源
gem sources -l

ruby_brew_2.png

这样,再重新运行安装brew的命令就可以安装成功了,虽然也慢,但至少可以安装成功。安装包就快了。

参考:

# mac安装svn,svn: error: The subversion command line tools are no longer provided by Xcode.

在新的macos系统中,xcode不再集成svn,需要自己安装。可以使用 brew install svn 进行安装

# mac安装nginx在官网下载与使用brew下载的区别

nginx官网直接下载的包,mac下可以直接进入该目录使用,但没有加到环境变量,不能在Terminal的任何目录下直接使用。

nginx_down.png

如果使用 brew install nginx 安装,他会直接都安装好,并修改环境变量。可以使用which nginx来看具体的地址

# 2020/09/02 周三

# @guoqzuo/vue-chart实用echarts组件封装

vue中使用echarts可以自己封装一个简单的组件。实现每次更新数据只需要修改options的值即可,并自带loading、暂时数据slot。先来看看使用demo

z-echart.gif

// main.js 全局引入
import VueChart from "@guoqzuo/vue-chart";
Vue.use(VueChart);

然后在任意组件里可以直接使用 z-echart

<!-- 固定宽柱状图,不自动resize -->
<div style="width:300px; height: 300px">
  <z-chart :options="bar" :loading="loading" :autoResize="false"> </z-chart>
</div>
<!-- 宽高自适应,resize后自动渲染,当hasData为false时显示暂无数据的slot内容-->
<div style="width: 50%; height: 50vh">
  <z-chart :options="pie" :hasData="hasData">
    暂无数据
  </z-chart>
</div>

来看看z-chart组件的实现细节,这里把echart单独用一个组件处理,在z-echart组件里引入,并加入loading、无数据的slot显示处理

<template>
  <!-- z-chart -->
  <div class="z-chart-container">
    <base-chart
      v-if="!loading && hasData"
      :options="options"
      v-on="$listeners"
      v-bind="$attrs"
    ></base-chart>
    <base-loading v-if="loading"></base-loading>

    <!-- no data slot -->
    <slot v-if="!loading && !hasData"></slot>
  </div>
</template>

base-chart代码如下,它主要做了三个操作

  • 使用options初始化图表
  • 如果是百分比大小的图,resize时,自动重绘
  • 使用watch监听options改动,如果有改动为了防止渲染异常,直接dispose销毁echart实例,重新加载
<template>
  <!-- base chart -->
  <div class="z-chart" ref="z-echart"></div>
</template>

<script>
import echarts from "echarts";
import { addListener, removeListener } from "resize-detector";
import { debounce } from "lodash-es";

export default {
  name: "BaseChart",

  props: {
    options: {
      type: Object,
      required: false
    },
    autoResize: {
      type: Boolean,
      required: false,
      default: true
    }
  },

  data() {
    return {};
  },

  created() {
    // 监听 options 改动,改动后重绘数据
    this.$watch(
      "options",
      () => {
        this.refresh();
      },
      { deep: true }
    );
  },

  mounted() {
    this.init();
  },

  beforeDestroy() {
    this.destroy();
  },

  methods: {
    init() {
      let chart = echarts.init(this.$refs["z-echart"]);
      chart.setOption(this.options || {});
      this.chart = chart;

      // 当元素宽高改变时resize执行重绘
      if (this.autoResize) {
        this.__resizeHanlder = debounce(
          () => {
            chart.resize();
          },
          100,
          { leading: true }
        );
        addListener(this.$el, this.__resizeHanlder);
      }
    },

    destroy() {
      // 销毁实例,防止 柱状图 动态切换到 饼图 时,柱状图部分属性依旧留存的问题
      this.chart.dispose();
      this.chart = null;
      this.autoResize && removeListener(this.$el, this.__resizeHanlder);
    },

    // 销毁后重绘,用于options变更后刷新图表
    refresh() {
      this.destroy();
      this.init();
    }
  }
};
</script>

<style lang="less" scoped>
.z-chart {
  width: 100%;
  height: 100%;
}
</style>

# 2020/09/01 周二

# normalize.css浏览器样式差异抹平

normalize.css (opens new window) A modern alternative to CSS resets 一个用于CSS重置的现代替代方法

主要用于解决不同浏览器默认样式之间的差异

What does it do?

  • Preserves useful defaults, unlike many CSS resets. 保留有用的默认样式,重置许多不一样的CSS
  • Normalizes styles for a wide range of elements. 为许多各种各样的元素标准化样式
  • Corrects bugs and common browser inconsistencies. 修正bug和常见的浏览器不一致样式
  • Improves usability with subtle modifications. 通细微的修改提高可用性
  • Explains what code does using detailed comments. 使用详细的注释来解释代码的作用

下面来看看 Chrome、Safari、Firefox 三个浏览器之间的差异

before_normalize.png

使用 normalize.css 后的效果

after_normalize.png

现有的UI框架也抹平了这些差异

上次更新: 2020/10/30 23:32:56