# 2020年04月技术日常

# 2020/04/29 周三

# UI设计视觉差问题,总感觉没有居中对齐时怎么办

放大n倍,截图画框,看是否在一条水平线上。

关于UI设计视觉差相关知识点

测量面积和视觉面积

  • 视觉体量是人眼如何察觉物体的大小和感觉,不需要等同于物体的实际像素值
  • 圆、菱形、三角形和其他非方形形状为了与方形形状保持一样的视觉大小,应适当放大
  • 图标区域应为视觉平衡保留一定的空间,这一点对保持同系列图标视觉平衡很重要。(这也是iconfont上为什么有的图标周围有留白,而有的图标没有留白的原因)

不同形状之间的对齐

  • 具有锐利边缘的形状应该更大,以便于其相邻的矩形对象保持视觉平衡
  • 大写字母对齐是一种有效且被广泛运用的方法,用于文本和按钮背景对齐
  • 将三角型图标正确放置按钮中的一种有效办法是,将其圈住并使此圈与背景对齐。

视觉圆角

  • 几何圆角看起来假是因为人眼可以清楚地看到直线突然变成曲线的点
  • 视觉正确的圆角需要一些特殊的算法或手动调整形状

参考:设计师必须知道,什么是视觉差对比? (opens new window)

# 关于商品价格有小数点时精度异常的问题

在价格显示时,如果有小数位,由于js浮点数的缺陷,累加的价格可能会有经度差,我们直接 toFixed(2) 即可。一般不会超过3位小数点,且精度异常一般都是小数点很多位以后,注意js小数点运算时一定要注意这个问题,确定要保留几位小数点

37998.91 + 0.01
// 37998.920000000006

0.1+0.2
0.30000000000000004

# 0-9数字中,每个数字的宽度是多少

什么数字最宽,以字体PingFangSC-Regular,大小12px为例

10个0 72
10个1 48.13
10个2 72
10个3 72
10个4 72
10个5 72
10个6 72
10个7 65.64
10个8 72
10个9 72

总结,一般宽度 1 < 7 < 0,2,3,4,5,6,8,9 但不排除其他字体没有差异,同理我们可以使用这种方法看26个字母里每个字母的占用宽度为多少

# 使用ssh方式提交或拉取代码的步骤

# 使用git平台邮箱账号生成公钥和私钥,全部默认、回车
ssh-keygen -t rsa -C "xxx@qq.com"

# 执行成功过后会在 ~/.ssh 目录下生成 id_rsa.pub 和 id_rsa 两个文件,一个公钥、一个私钥

# 查看公钥,并配置到对应的git平台里
cat ~/.ssh/id_rsa.pub

# 测试配置是否成功
ssh -T git@xxx.com 

# 拉取 
git clone git@xxx.com/xxxproject

参考:GitHub如何配置SSH Key (opens new window)

# 2020/04/27 周一

# git删除远程分支与本地分支

删除远程仓库的分支,可以先查看当前所有分支

git branch -a # 查看当前分
# file_backup
# master
# remotes/origin/HEAD -> origin/master
# remotes/origin/file_backup

删除远程的file_backup分支

git push origin --delete file_backup

删除本地分支

git branch -d file_backup

参考:git删除远程分支和本地分支 (opens new window)

# 2020/04/26 周日

# Object.observe()与Proxy

Object.observe()是js实现观察者设计模式的一个API,现在已废弃,由Proxy取代,但Proxy不支持IE。

它用于异步地监视一个对象的修改。当对象属性被修改时,方法的回调函数会提供一个有序的修改流。

在vue2.0的双向绑定的实现里,会遍历data对象,通过建立对应的getter/setter访问器属性来追踪属性变化。Vue 3.0里已使用Proxy来追踪属性变化

// 截取至vue 3.0相关代码
// https://github.com/vuejs/vue-next/blob/40bdd51bf5ec24b8e3faab3e1cb4d91b076e456a/packages/reactivity/src/reactive.ts#L99
function createReactiveObject(
  target: unknown,
  toProxy: WeakMap<any, any>,
  toRaw: WeakMap<any, any>,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // target already has corresponding Proxy
  let observed = toProxy.get(target)
  if (observed !== void 0) {
    return observed
  }
  // target is already a Proxy
  if (toRaw.has(target)) {
    return target
  }
  // only a whitelist of value types can be observed.
  if (!canObserve(target)) {
    return target
  }
  const handlers = collectionTypes.has(target.constructor)
    ? collectionHandlers
    : baseHandlers
  observed = new Proxy(target, handlers)
  toProxy.set(target, observed)
  toRaw.set(observed, target)
  return observed
}

# vscode ESLint插件新版本保存自动fix配置变更

最近换了个办公电脑,重装了vscode,发现eslint的配置和以前不一样了,新版本废弃了原先的一些配置,配置更简洁了,先来看看老的配置

{
  "edit.formatOnSave": false,   // 取消自带fix,使用eslint自动保存fix
  "eslint.autoFixOnSave": true, // 每次保存的时候将代码按eslint格式进行修复
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    {
      "language": "vue",
      "autoFix": true
    },
    "html"
  ]
} 

最新配置vscode自动fix

{
  "editor.codeActionsOnSave": {
    "source.fixAll": true
  }
}

The old eslint.autoFixOnSave setting is now deprecated and can safely be removed

ESLint插件检测、自动修复机制,依赖当前目录下的package.json对应的eslint配置,vue项目、node项目、react项目、ts支持都对应不同的npm依赖包,比较难以理解,我打算研究一下这一块的内容,题目已取好:"彻底理解ESLint在vscode中的检测机制/自动fix机制",待有时间好好研究下

当我们从远程仓库拉取vue项目代码后,在没有npm install的情况下,vscode的eslint插件不会起任何作用,需要npm install 安装必要的插件后,重启vscode,才会生效

# 2020/04/24 周五

# v-model的理解

在前面讨论过,使用v-model的场景,来看一个实例,假如我们需要封装一个弹窗组件需要引入el-dialog组件

首先,我们来看看我们写好这个组件后应该怎么调用,通过show这个参数来控制dialog显示或隐藏

<template>
  <my-dialog v-if="showDialog" :show="showDialog">
</template>
<script>
export default {
  components: {
    MyDialog: () => import('@/components/my-dialog')
  },
  data() {
    return {
      showDialog: flase
    }
  }
}
</script>

来看看实现,这里看element官方demo里在visible属性上用了.sync修饰符,有什么用呢?

<!-- https://element.eleme.cn/#/zh-CN/component/dialog -->
<template>
  <div>
    <el-dialog title="提示" :visible.sync="dialogVisible">
      内容
      <span slot="footer" class="dialog-footer">
        <el-button @click="dialogVisible = false">取 消</el-button>
        <el-button type="primary" @click="dialogVisible = false">确 定</el-button>
      </span>
    </el-dialog>
  </div>
</template>

看来看源码

<!-- https://github.com/ElemeFE/element/blob/dev/packages/dialog/src/component.vue -->

<template>
  <transition name="dialog-fade" @after-enter="afterEnter" @after-leave="afterLeave">
    <div
      v-show="visible"
      class="el-dialog__wrapper"
      @click.self="handleWrapperClick">
      <!-- .... -->
    </div>
  </transition>
</template>

<script>
// ...
hide(cancel) {
  if (cancel !== false) {
    this.$emit('update:visible', false);
    this.$emit('close');
    this.closed = true;
  }
}
//... 
</script>

可以看到关闭弹窗时,使用了

this.$emit('update:visible', false);

修改了父组件传入的visible的值,没有使用v-model, 所以在el-dialog传入visible属性的时候加了.sync修饰符

<el-dialog title="提示" :visible.sync="dialogVisible">

vue里面有介绍这种方法可以使子组件可以修改父组件的值,而不用多加一个事件参数,参见文档 .sync修饰符 (opens new window)

但感觉这个接口设计的有点鸡肋,v-model不香吗?

子组件封装dialog的特殊之处在于,通过showDialog显示子组件的dialog后,子组件点击关闭后会自己关闭弹窗,这时 父组件的showDialog还是true,因此并不能通过show参数来控制显示,我们需要在子组件点击dialog关闭时,触发一个事件给父组件,从而修改对应的值

<my-dialog v-if="showDialog" :show="showDialog" @close="showDialog = false">

这里当子组件关闭dialog时$emit一个close事件来手动将showDialog设置为false,以到达父组件可以通过showDialog来打开或关闭dialog的目的,我们可以来看看子组件的实现

<template>
  <div>
    <el-dialog title="提示" :visible.sync="dialogVisible">
      内容
      <span slot="footer" class="dialog-footer">
        <el-button @click="dialogVisible = false">取 消</el-button>
        <el-button type="primary" @click="dialogVisible = false">确 定</el-button>
      </span>
    </el-dialog>
  </div>
</template>
<script>
// 注意如果我们把show直接设置给el-dialog的visible属性,
// el-dialog内部在关闭dialog时会触发一次 this.show = false操作
// 而show是父组件通过props单向传递的值,改动会提示错误,我们需要使用computed属性来中转
export default {
  props: {
    show: { 
      type: Boolean,
      required: true
    }
  }
  computed: {
    dialogVisible: {
      get() {
        return this.show // 父组件向下传递show的值时通过计算属性赋值到dialogVisible
      }
      set() {
        // 当dialog关闭时,会触发this.dialogVisible = false,从而来到这个方法,我们在这里将关闭事件同步给父组件
        this.$emit('close', false)
      }
    }
  },
  data() {

  }
}
</script>

上面的例子已经实现了功能,但缺点是,要使用两个参数,一个show属性,一个close事件。v-model就是一种简化参数的语法糖

<my-dialog v-if="showDialog" :show="showDialog" @close="showDialog = false">
<!-- 等价于 -->
<my-dialog v-if="showDialog" v-model="showDialog">
<script>
export default {
  model: { // 新增了model属性,用来指定v-model赋值的属性名,以及改动父组件对应值需要$emit的事件名称
    prop: 'show',
    event: 'close'
  },
  // 其他后面的逻辑不变
  props
    show: { 
      type: Boolean,
      required: true
    }
  }
  // ....
}
</script>

关于v-model的文档,参考 v-model | Vue API (opens new window)

综上,v-model比.async更优雅,而在vue的refs里,也有建议使用v-model代替.async修饰符,且已经merge到了vue 3.0版本,相关文档 Replace v-bind's .sync with a v-model argument (opens new window)

# 腾讯云慧眼架构相关设计

昨晚看了一个关于腾讯云慧眼架构相关的分享,由于之前参与过该项目的一些开发,对业务比较了解。结合业务场景再来看构架设计,会有一种豁然开朗的感觉,下面对一些构架方面比较好的点做一个总结

  • 高可用,两地三中心、异常监控上报、弹性伸缩
  • 资源有限时服务降级来提高QPS:例如,常规情况会抽取视频的10个帧图片,QPS的瓶颈在这,如果资源有限,通过服务降级,减少抽取的帧,每次抽取3-4帧,极大提高QPS
  • 单个客户流量暴增时,分配独立资源池,不影响其他客户服务
    • 客户提前告知,提前分配独立资源池
    • 未提前告知,就需要服务具备自动告警功能,怎么检测流量异常,什么时候提前通知,需要根据情况使用特定的策略。客户流量激增时,告警邮件、短信通知相关负责人,找客户确定是正常流量还是异常流量,如果异常流量,限制QPS,拒绝大部分服务。如果是正常流程,分配资源、快速扩容
  • 高扩展,可配置化服务,满足各种差异化需求
  • 低耦合,服务拆分。新增功能特性时,减少各个服务需要做的改动;发布某个模块的功能时,对其他模块不会有干扰
  • 低成本方式,服务兜底,便宜的证照库可能缺少一些用户身份信息,当出现这种情况会兜底去查询价格高一点的证照库,节约成本的情况下,保证了体验
  • 提高成功率,多引擎融合策略,多个验证引擎识别成功率不一样,取多个引擎综合结果成功率会上升4-5个点
  • 引擎服务接口hang,怎么保证服务ok,监测引擎异常次数,切换处理引擎

新引擎功能接入线上前怎么测试?

  • 灰度策略,为某个客户灰度5%的流量,看识别结果
  • 旁路策略,比灰度更好,直接copy一份某个用户的流量请求新引擎版本的服务,得出结果,就是用户请求发送到后台后,后台除了正常响应当前稳定版本验证的结果外,再把请求转发一份给测试引擎,这个结果是不会给到用户的。仅用来比对成功率。

引擎自动化评测策略

  • 正、负样本(手工标注),多纬度评测
  • 引擎在不同地域样本里的表现

# 2020/04/22 周三

# 箭头函数和普通的函数指向问题

这里复习下基础知识

  1. 普通函数 在执行函数的时候 绑定this
  2. 箭头函数 在函数定义的时候 绑定this,this继承自父执行上下文

# element 最多只能选择一个月范围

点击某个时间后会触发 pickerOptions里的 onPick函数,获取当前点击的时间,然后计算时间disabled前后一个月的时间

// <el-date-picker v-model="date" type="daterange" :picker-options="pickerOptions">
// </el-date-picker>
export default {
  data() {
    const pickerOptions = {
      // 选中时间时触发 element第一次选中后会赋值给minDate
      onPick: ({ maxDate, minDate }) => {
        this._curClickDate = minDate.getTime()
        // 第二次选中后,按两次点击的时间顺序依次赋值给 minDate、maxDate
        // 且面板会关闭,这时要清空_curClickDate,供下一次使用
        maxDate && (this._curClickDate = '')
      },
      disabledDate: time => {
        const { _curClickDate } = this
        const gap = 31 * 24 * 3600 * 1000 // 一个月,按31天算
        const t = time.getTime()
        let start = _curClickDate - gap
        let end = _curClickDate + gap
        return t > Date.now() || (_curClickDate && (t > end || t < start))
      }
    }
    return {
      date: '',
      pickerOptions,
    }
  }
}

具体demo:在线体验 (opens new window) github demo源码地址 (opens new window)

# 2020/04/20 周一

# nginx配置二级域名解析目录

最近看百度统计,有100多个河南的新ip访问,有些异常,而且都是访问的api.zuo11.com,最近两周明细里看到的ip段很有规律

111.7.100.16 - 111.7.100.27
36.99.136.131 - 36.99.136.143

百度统计有个缺点,就是仅提供两周内的访问明细,最多5000条,后面有必要做一个自己的用户行为记录系统

之前我将 https://api.zuo11.com 解析到了一个node服务用于https接口,但发现 http://api.zuo11.com 走的80端口,会直接访问zuo11.com,我仿照zuo11.com 301重定向到 www.zuo11.com 的方式写了下面的配置,但发现不生效,当时也没管。

if ($host = 'api.zuo11.com') {
  rewrite ^/(.*)$ https://api.zuo11.com/$1 permanent;
}

现在暴露出问题来了,就必须解决,让 http://api.zuo11.com 指向其他目录,发了好长时间才配置ok,以下是相关具体配置,省略了https配置

server {
  listen       80;
  server_name  zuo11.com www.zuo11.com;
  charset utf-8;

  if ($host = 'zuo11.com') {
    rewrite ^/(.*)$ http://www.zuo11.com/$1 permanent;
  }

  location / {
      root   C:\Users\Administrator\Desktop\dist;
      index  index.html index.htm;
  }
}

server {
  listen   80;
  server_name  demo.zuo11.com;
  charset  utf-8;
  location / {
        root  C:\Users\Administrator\Desktop\demo_dist;
        index  index.html index.htm;
    }
}

server {
  listen   80;
  server_name  api.zuo11.com;
  charset  utf-8;
  location / {
        root  C:\Users\Administrator\Desktop\api_dist;
        index  index.html index.htm;
    }
}

其实就是多写一个server,把二级域名指向其他目录。中间有个坑的地方就是windows使用nginx时,关闭nginx运行的terminal后,进程不一定关闭,需要打开任务管理器,找nginx相关进程删掉,这样才彻底。

# 2020/04/19 周日

# 向百度站长主动推送站点url

之前提交链接都是手动将url粘贴到输入框提交。这次试了下curl方式提交还是很方便的

# 先将要提交的url存放到 urls.txt 里
vi urls.txt

# 查看
cat urls.txt 
http://www.zuo11.com/blog/2019/11/v-if_filters.html
http://www.zuo11.com/blog/2019/12/web_storage.html
http://www.zuo11.com/blog/2019/12/phantomjs-prebuilt.html
http://www.zuo11.com/blog/2019/12/node_sleep_module.html
http://www.zuo11.com/blog/2019/12/git_clone_timeout.html
http://www.zuo11.com/blog/2019/11/git_push_branch.html
http://www.zuo11.com/blog/2019/12/npm_resource.html

# 确定内容没问题后提交,对应的链接在百度站长提交链接哪里会自动生成
curl -H 'Content-Type:text/plain' --data-binary @urls.txt "http://data.zz.baidu.com/urls?site=www.zuo11.com&token=xxxxxxx"

# 执行后返回推送接口,实时推送成功
{"remain":99993,"success":7}

# uni-app跨域问题接口代理配置

在uni-app中有一个manifest.json配置文件,里面的h5配置下有默认的devServer选项,和vue.config.js里的代理配置一致,都是使用的webpack的代理功能,默认配置为

"h5": {
  "devServer" : {
      "https" : false,
      "port" : 80
  }
}

根据具体情况,具体配置,下面是一个示范配置

"h5": {
  "devServer": {
    "port": 8086,
    "disableHostCheck": true,
    "proxy": {
      "/": {
          "target": "http://xxxx:8086/",
          "changeOrigin": true,
          "secure": false
      }
    }
  }
}

这里要注意的是,要在manifest.json文件配置,而不是pages.json里,每次修改配置后记得点击重新运行到浏览器,如果不生效关闭再开启HBuildeX,多试试

# uni.request封装为类似axios的请求对象

在uni-app中为了抹平各平台的差异,官方提供了uni.request方法,和微信小程序的请求方法类似,一般这类请求是比较通用的,如果直接使用会有大量的重复代码,于是做了简单的封装,来看看代码

axios.js

/**
 * @description 将uni.request封装成简单的aixos 
 * @author guoqzuo
 * @example
 * axios.create(config) 根据配置,创建axios实例
 * axios.get(url[, config]) get请求
 * axios.post(url[, data[, config]]) post请求
 * config支持配置项
 * baseURL: "https://some-domain.com/api/", // baseURL
 * timeout: 1000, // 超时时间 
 * headers: { "X-Custom-Header": "foobar" }, // 请求头
 * method: '' // 请求方法
 * url: '', // 请求url
 * data: '' // post 请求的data
 * toastErrorMsg 是否用toast显示错误信息,默认为是
 * showLoading  是否显示loading,默认为是
 */
class Axios {
  constructor() {
    this.config = {}
  }

  // 全局配置,返回一个axios实例
  create(config = {}) {
    Object.assign(this.config, config);
    return this
  }

  get(url = '', config = {}) {
    // 暂不支持params
    Object.assign(this.config, config, { url, method: 'GET' })
    return this._request()
  }

  post(url = '', data = {}, config = {}) {
    Object.assign(this.config, config, { url, method: 'POST', data })
    return this._request()
  }

  // uni.request 封装
  _request() {
    return new Promise(async (resolve, reject) => {
      let { baseURL, timeout, headers, method, url, data, toastErrorMsg, showLoading, successCode = 0 } = this.config
      showLoading && uni.showLoading({
        mask: true
      })

      let [error, res] = await uni.request({
        data,
        method,
        header: headers,
        timeout,
        url: baseURL + url
      })

      // 请求完成后做 complete 该执行的内容
      showLoading && uni.hideLoading();

      // 判断请求是否成功,这里很奇怪,官方把error直接返回了,类似于node的callback
      if (error) {
        this._showToast(toastErrorMsg, error.errMsg)
        reject(error.errMsg);
        return
      }

      // statusCode === 200
      let { statusCode, header, data: resData } = res;
      if (!resData || typeof resData !== 'object') {
        let msg = "接口异常,data数据出错"
        this._showToast(toastErrorMsg, msg)
        reject(msg);
        return;
      }


      let { code = '', msg = '' } = resData;
      // 请求成功,且状态码ok
      if (Number.parseInt(code) === successCode) {
        resolve(resData.data);
      } else {
        this._showToast(toastErrorMsg, msg)
        reject(msg);
      }
    });
  }

  // 根据toastErrorMsg判断是否需要显示错误信息
  _showToast(toastErrorMsg, msg) {
    toastErrorMsg && uni.showToast({
      title: msg,
      icon: 'none',
      image: '',
      duration: 1500,
      mask: false,
    });
  }
}

const axios = new Axios();
export default axios;

配置axios service.js

// service.js
import axios from './utils/axios'

const axiosInstance = axios.create({
  baseURL: "/index.php",
  successCode: 0, // 后端自己定义的成功或的错误码
  toastErrorMsg: true, // 是否用toast显示错误信息,默认为是
  showLoading: true  // 是否显示loading
});

export default axiosInstance

模块 modules/user.js

import createServiceFromConfig from '../utils/createServiceFromConfig'

// 用户模块service
export default createServiceFromConfig([
  ['login', '/user/login', 'post'] // 登录
])

根据配置生成接口服务 createServiceFromConfig.js

import service from '../service'

function createServiceFromConfig(configList) {
  let serviceObj = {}
  configList.forEach(item => {
    let [apiName, url, method = 'get'] = item
    serviceObj[apiName] = (payload = {}, config = {}) => {
      let url = item[1] // url 是第二位
      let params = method === 'get' ? [config] : [payload, config]
      return service[method](url, ...params); // 等价于 return axios.get(..)
    }
  })
  return serviceObj
}

export default createServiceFromConfig

调用接口

import userService from "@/service/modules/user";
async login() {
  try {
    let data = await userService.login({
      mobile: "xxx",
      password: "xxx"
    });
    console.log(data);
  } catch (e) {
    console.error(e);
  }
}

# 2020/04/17 周五

# text-align: justify 两端对齐不生效的问题

一般直接设置text-align: justify是不会生效的。但你改为text又会立即居中对齐。我们需要注意的是我们需要将子元素设置一个after的属性或者后面放置一个空的占位标签,设置 display: inline-block; width: 100%,当出现高度占位间隙时,设置占位元素的height没用,需要设置元素的height。css 确实有点毫无逻辑的感觉....

text_align_justify.png

<head>
  <style>
    .sec,
    .sec2 {
      width: 150px;
      text-align: justify;
    }

    /* fix 占位元素高度问题 */
    .sec>div,
    .sec2>div {
      height: 25px;
      line-height: 25px;
    }

    .zw {
      display: inline-block;
      width: 100%
    }

    .sec2>div::after {
      content: '';
      display: inline-block;
      width: 100%;
      overflow: hidden;
      height: 0;
    }
  </style>
</head>

<body>
  <div class="sec">
    <div>
      我是较长<p class="zw"></p>
    </div>
    <div>
      我是较长的<p class="zw"></p>
    </div>
    <div>
      我是较长的文字<p class="zw"></p>
    </div>
  </div>
  <hr>
  <div class="sec2">
    <div>我是较长</div>
    <div>我是较长的</div>
    <div>我是较长的文字</div>
  </div>
</body>

参考:使用text-align:justify,让内容两端对齐,兼容IE及主流浏览器的方法 (opens new window)

# 2020/04/16 周四

# vue-router 跳转的问题

编程方式跳转路由

router.push(location, onComplete?, onAbort?)
router.push(location).then(onComplete).catch(onAbort)
router.replace(location, onComplete?, onAbort?)
router.replace(location).then(onComplete).catch(onAbort)
router.go(n)
router.back()
router.forward()

push跳转页面

// 知道路由名称,跳转
this.$router.push({
  name: '路由name',
  params: { userId: '123' } // 参数,url上面不可见
  query: { plan: 'private' } // 查询参数 url上可见
})

// 知道路由path,跳转 /register?plan=private
this.$router.push({ path: 'register', query: { plan: 'private' }})
// 注意:如果提供了 path,params 会被忽略,只能使用query参数

replce跳转页面,push会在history添加一条记录,而replce不会,他只会改变当前页面

// router.replace(location, onComplete?, onAbort?)
this.$router.replace({
  name: '路由name',
  params: { userId: '123' } // 参数,url上面不可见
  query: { plan: 'private' } // 查询参数 url上可见
})

在新的tab页打开,可以用 this.$router.resolve() 来处理路由信息,也可以直接使用相对路径,注意hash、history跳转方式的区别

const resolved: {
  location: Location;
  route: Route;
  href: string;
} = router.resolve(location, current?, append?)
window.open('#/user/info') 
// ‘_blank’为默认,在新的窗口打开,'_self' 为在当前页打开

# element 时间选择器限制选中日期

一般使用 picker-options 这个属性来disable某些时间段,注意 如果设置了default-time 00:00:00 - 23:59:59 会影响对应的日期判断,必要时可以去掉,逻辑可以由后端处理

<el-date-picker
  v-model="value1"
  type="date"
  :picker-options="pickerOptions"
  placeholder="选择日期">
</el-date-picker>
<script>
export default {
  data() {
    return {
      // 时间不能选择今天之后的日期
      pickerOptions: {
        disabledDate(time) {
          return time.getTime() > Date.now()
        }
      }
    }
  }
}
</script>

# 2020/04/14 周二

# css 利用 perspective 画梯形

今天看小伙伴的代码,发现有一个梯形的实现居然是css写的,我以为需要UI提供icon。这里用到了 perspective 远景这个参数

/*  <div class="tx"></div> */
.tx {
  width: 100px;
  height: 40px;
  margin: 100px;
  border: 1px solid #ccc;
  transform: perspective(2em) rotateX(-10deg);
}

CSS 属性 perspective指定了观察者与 z=0 平面的距离,使具有三维位置变换的元素产生透视效果。 z>0 的三维元素比正常大,而 z<0 时则比正常小,大小程度由该属性的值决定。

参考:

# git无法检测到文件名大小写的更改

今天把开发分支合并到月底分支后,发现之前修改过文件名没有提交上去,手动改文件名后,git status 提示没有任何改动。

于是百度了下,发现git默认配置为忽略大小写,因此无法正确检测大小写的更改。

解决办法在当前项目:git config core.ignorecase false,关闭git忽略大小写配置即可

参考: Git无法检测到文件名大小写的更改 (opens new window)

# 2020/04/12 周日

# vue页面中监听路由改变的几种方法

// 1.单页面组件中,使用 beforeRouteEnter
created() {
},
beforeRouteEnter(to, from, next) {
	next(vm => {
		// vm就是this了
	})
},

// 2.使用watch监听$route
watch: {
	$route(to, from) {
		// xxx
	}
}

更多详情参数之前的笔记:vue-router 笔记 (opens new window)

# 全局引入组件Vue.use()与Vue.component()

之前的笔记里有将Vue.use()全局引入组件的方法,其实用 Vue.compoennt()也可以全局引入组件。

alert
├── src
│   └── main.vue  # 组件实现
└── index.js # install方法,供全局引入
index.js 提供install方法,供Vue.use()使用
import Alert from './src/main';

/* istanbul ignore next */
Alert.install = function(Vue) {
  Vue.component(Alert.name, Alert);
};

export default Alert;

main.js全局引入组件的两种方式

// 1. Vue.use()
import Alert from '@/components/alert/index.js'
Vue.use(Alert)

// 2. Vue.component()
import Alert from "@/components/alert/src/main.vue"
Vue.component('alert', Alert)

Vue.use 和Vue.component 全局引入组件之间的区别:

  1. Vue.component 只是单纯的引入组件、不需要额外写支持的js文件
  2. Vuew.use 除组件外,需要写额外的js实现install方法,但它不仅可以注入组件,还可以注入很多其他东西,比如全局实例属性等。

# 2020/04/11 周六

# iconfont通过设置class来显示图标内部做了那些操作

注意我们在用class使用iconfont图标时,为什么可以使用,主要是iconfont.css里面做了三步操作:

  1. 定义iconfont的 font-familay
  2. 为.iconfont设置默认样式,指定为font-family字体
  3. 为每个图标的class设置before的content

来看具体的demo,示例

@font-face {
  font-family: "iconfont";
  src: url('iconfont.eot?t=1586579952536'); /* IE9 */
  src: url('iconfont.eot?t=1586579952536#iefix') format('embedded-opentype'), /* IE6-IE8 */
  url('data:application/x-font-woff2;charset=utf-8;base64,省略...') format('woff2'),
  url('iconfont.woff?t=1586579952536') format('woff'),
  url('iconfont.ttf?t=1586579952536') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
  url('iconfont.svg?t=1586579952536#iconfont') format('svg'); /* iOS 4.1- */
}

.iconfont {
  font-family: "iconfont" !important;
  font-size: 16px;
  font-style: normal;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.iconok:before {
  content: "\e63c";
}

.iconerror1:before {
  content: "\e651";
}

# 2020/04/10 周五

# JSON.stringify的非常规用法以及内部执行顺序

JSON.stringify我们一般用于将JSON对象转为字符串,但他不仅仅只有一个参数,而是三个,除最常用的用法外,还可以用来做三种实用功能

  1. 利用第三参数在console里更好的展示对象
// 1. 对于像这种多层级的数据,console到控制台时,会不好查看需要一层层点击,很麻烦
let obj = { list: [ {a: 1}, {a: 2}], total: 100 }
console.log(obj)

// 更好的展现,第三参数可以在转JSON字符串时,在json对象的缩进位置填充字符并加上换行符
// 缩进位置填充的内容,根据第三参数的类型决定:
// 如果是整数,填充对应的空格数(最大为10),如果是字符串,填充充对应的字符串
JSON.stringify(obj, null, 2) 

json_stringify.png

  1. 有选择性的过滤字段
let obj = { list: [ {a: 1}, {a: 2} ], total: 100 }
// 如果我们想深拷贝obj,但只深拷贝其total字段,其他的字段不需要,就可以用第二个参数
let newObj = JSON.parse(JSON.stringify(obj, ['total']))
// newObj 值为 { total: 100 }

// 实例:在vue项目中,用js删除当前url query参数中的id参数
let query = this.$route.query
this.$router.replace({ 
  path: this.$route.path, 
  query: JSON.parse(JSON.stringify(query, Object.keys(query).filter(item => item !== 'id'))) 
})
  1. 详细处理没有字段序列化的值
let obj = { list: [ {a: 1}, {a: 2} ], total: 100 }
let new = JSON.stringify(obj, (key, value) => {
  return key === 'list' ? '改写list序列化的值' : value
}) 
// new 的值为 {"list":"改写list序列化的值","total":100}

JSON.stringify(obj)时,其实内部调用的是 obj 的 toJSON()方法,如果我们重写该方法,就可以改变序列化后返回的值

JSON.stringify()执行顺序

  1. 如果对象中存在toJSON方法,且能通过它获取有效的值,则调用该方法,返回对应的值用于下一步,否则返回对象本身。
  2. 如果提供了第二个参数,根据对应的参数过滤第(1)步得到的值
  3. 对第(2)步返回的每个值进行进行相应的序列化
  4. 如果提供了第三个参数,执行相应的格式化

详情参考之前的笔记: JSON.stringify() - JS高程3笔记 (opens new window)

# 2020/04/09 周四

# Array.prototype.fill()填充引用类型值时的坑

在mock表格list数据时,我一般为了简洁会先创建一个对象info,然后new Array(10).fill(info) 来生成10条数据的数组

但这次发现一个问题,由于表格有一个字段是状态值0 - 5,我想随机设置下值,发现修改后的值都一样,来看看例子

let a = {a: 1}
let b = Object.assign({}, a) // {a: 1}
let c = Object.assign({}, a) // {a: 1}
b.b = 2 // 尝试修改值,看看a和c是否修改,发现只有b修改了,a,c没变说明地址不一样

// 来看看fill
let list = new Array(5).fill(Object.assign({}, a))
// 乍一看,貌似每个填充的地址都不一样,我们修改数组中的一个元素试试
list[0].b = 2

再打印list,发现list所有数组对象的值都变了,因此fill填充的对象都指向同一个地址

用 Array.prototype.fill() mdn查权威文档,发现里面确实有这一块的描述

// Objects by reference.
var arr = Array(3).fill({}) // [{}, {}, {}];
// 需要注意如果fill的参数为引用类型,会导致都执行都一个引用类型
// 如 arr[0] === arr[1] 为true
arr[0].hi = "hi"; // [{ hi: "hi" }, { hi: "hi" }, { hi: "hi" }]

那么遇到这种情况不能用fill要怎么处理呢

let a = {a: 1}
// 这里还是用了fill 如果不fill内容到数组,map遍历时会忽略所有空的元素
let list = new Array(5).fill({}).map(item => {
  item = Object.assign({}, a)
  item.xxx = Math.round(Matn.random() * 5)
  return item
})

参考: Array.prototype.fill() - MDN (opens new window)

# 2020/04/08 周三

# Failed to resolve directive: infinite-scroll

在element InfiniteScroll无限滚动功能里,使用了v-infinite-scroll指令,但直接使用会发现提示 Failed to resolve directive: infinite-scroll,后面查了下,发现要使用该指令需要安装一个vue-infinite-scroll npm包,并且在main.js里引入才行

// 安装vue-infinite-scroll
// npm install vue-infinite-scroll --save

// 在main.js里引入
import infiniteScroll from "vue-infinite-scroll";
Vue.use(infiniteScroll);

这样就能正常使用了,注意加载数据 v-infinite-scroll="load" 指定的load函数里,获取数据后,每次push到列表list即可。首次进入会自动加载一次,无需在created钩子里手动请求一次数据,注意当在load里要加个判断,如果获取数据长度为0,或超出数据页数时,就不再继续load了

参考:

# 2020/04/07 周二

# 怎么将Date数据转为TZ格式的字符串

后台要求的数据格式 "2020-04-10T04:01:00.000Z" 为TZ格式的字符串。Date对象toString为 "Fri Apr 10 2020 12:00:00 GMT+0800 (中国标准时间)" 这种格式,那怎么转TZ字符串格式呢?用 Date.prototype.toJSON() 方法即可

a = new Date('2020/04/10 12:00:00') 
Fri Apr 10 2020 12:00:00 GMT+0800 (中国标准时间)

a.toJSON() //"2020-04-10T04:01:00.000Z"

参考:Date.prototype.toJSON() - MDN (opens new window)

# Date.prototype.toLocaleString()的坑

注意在把Date转为字符串时,需要额外注意时间为 '00:00:00' 的情况,会被转为 上午12:00:00

a = new Date('2020/04/10 00:00:00') 
// Fri Apr 10 2020 00:00:00 GMT+0800 (中国标准时间)

a.toLocaleString() // "2020/4/10 上午12:00:00"
a.getHours() // 0
a.getMinutes() // 0
a.getSeconds() // 0

那我们来试试 '12:00:00' 的情况

a = new Date('2020/04/10 12:00:00') 
// Fri Apr 10 2020 12:00:00 GMT+0800 (中国标准时间)
a.toLocaleString() // "2020/4/10 下午12:00:00"
a.getHours() // 12

???怎么变下午12点了,综上虽然Date对象的toLocaleString()比较好用,但还是仅用日期方面的toLocaleDateString()吧,时间方面的还是尽量不要使用,12点和00点傻傻分不清楚

参考:Date.prototype.toLocaleString() - MDN (opens new window)

# 2020/04/06 周一

# xx.com重定向到www.xx.com方式

今天帮一个同学测试网络,使用curl来请求百度,发现baidu.com到www.baidu.com的重定向使用的是 html 的meta元素

curl -v baidu.com
# <html>
# <meta http-equiv="refresh" content="0;url=http://www.baidu.com/">
# </html>

当访问 baidu.com时,0秒后刷新页面到 http://www.baidu.com,以后如果在不依赖nginx配置的情况下,可以使用这种方式。meta元素使用方法可以参考之前的笔记:http-equiv改写http标头字段 (opens new window)

之前我有处理过 zuo11.com 跳转到 www.zuo11.com 的情况,配置了nginx,具体方法参见 nginx 访问不带www的域名,自动切到www (opens new window)

curl -v zuo11.com nginx响应内容如下:

curl -v zuo11.com
* Rebuilt URL to: zuo11.com/
*   Trying 47.107.190.93...
* TCP_NODELAY set
* Connected to zuo11.com (47.107.190.93) port 80 (#0)
> GET / HTTP/1.1
> Host: zuo11.com
> User-Agent: curl/7.54.0
> Accept: */*
> 
< HTTP/1.1 301 Moved Permanently
< Server: nginx/1.16.1
< Date: Mon, 06 Apr 2020 12:56:22 GMT
< Content-Type: text/html
< Content-Length: 169
< Connection: keep-alive
< Location: http://www.zuo11.com/
< 
<html>
<head><title>301 Moved Permanently</title></head>
<body>
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx/1.16.1</center>
</body>
</html>

也就是当访问zuo11.com时,nginx会用301重定向到www.zuo11.com。我们可以做一个测试,当接收到一个请求后,修改状态码为301,然后设置Location响应头为 http://www.zuo11.com 看是否可以重定向页面,来写个koa例子

const Koa = require('koa')
const app = new Koa()

app.use((ctx) => {
  console.log(ctx.url)
  if (ctx.url === '/test') {
    // 当访问 /test 时 301重定向到 http://www.zuo11.com
    ctx.status = 301
    ctx.set({
      'Location': 'http://www.zuo11.com'
    })
    return
  }
  // 非 /test 时,页面显示 welcome
  ctx.body = 'welcome'
})

app.listen('9000', () => { console.log('服务已开启,9000端口') })

通过测试上面的例子,发现只要响应的状态码设置为301,且响应头Location设置为要重定向的网页,就可以301跳转到对应的页面,新技能get

# 什么是BFC

虽然做前端好几年了,但只是听说个这个名词,一直不清楚具体是什么意思,今天来研究下

BFC是 Block Formatting Context 的缩写,字面意思是 块格式化上下文

字面意思很难理解,我们先来看几个例子

1. margin塌陷问题

下面的例子中A、B两个元素的margin都为10px,理论上A、B上下间的距离为 20px,但实际却是 10px

<div>
  <div class="elementA" style="margin: 10px">123</div>
  <div class="elementB" style="margin: 10px">456</div>
</div> 

利用BFC解决塌陷的问题,用父元素包裹并设置overflow为hidden,这样间隔就是20px了

<div>
  <div class="elementA" style="margin: 10px">123</div>
  <div style="overflow:hidden">
    <div class="elementB" style="margin: 10px;">456</div>
  </div>
</div> 

2. float父级元素高度为0的问题

wrapper的子元素使用了float,其高度为100px,但他的父级元素wrapper高度为0

<div class="wrapper">
  <div style="float:left;height:100px">123</div>
</div>

解决方案如下,给wrapper添加一个BFC属性,这时wrapper的高度就是子元素的高度

<div class="wrapper" style="overflow:hidden">
  <div style="float:left;height:100px">123</div>
</div>

3. float高度超出父元素容器区域的问题

box元素为父元素,float元素高度为150,此时,float会超出父元素范围

<div class="box" style="background: blue;border:1px solid red;">
  <div class="float" style="float: left;width: 200px;height: 150px;border: 1px solid #ccc;background: white;">
    I am a floated box!
  </div>
  <p>I am content inside the container.</p>
</div>

利用BFC,使浮动内容和周围的内容等高,给box元素加一个overflow:hidden即可

<div class="box" style="background: blue;border:1px solid red;overflow:hidden">
  <div class="float" style="float: left;width: 200px;height: 150px;border: 1px solid #ccc;background: white;">
    I am a floated box!
  </div>
  <p>I am content inside the container.</p>
</div>

通过上面两个例子,我们会很好奇,为什么加个overlfow:hidden就能解决问题,BFC 块级格式化上下文到底是什么?

BFC 块级格式化上下文主要和float、clear、margin塌陷问题有关联

一般情况下BFC只存在于根级元素(html),但设置某些CSS属性时也会让产生BFC。但是前提是必须是块级元素。

以下属性声明会产生BFC:

  • 浮动元素(元素的 float 不是 none)
  • 绝对定位元素(元素的 position 为 absolute 或 fixed)
  • 行内块元素(元素的 display 为 inline-block)
  • 表格单元格(元素的 display为 table-cell,HTML表格单元格默认为该值)
  • 表格标题(元素的 display 为 table-caption,HTML表格标题默认为该值)
  • 匿名表格单元格元素(元素的 display为 table、table-row、 table-row-group、table-header-group、table-footer-group(分别是HTML table、row、tbody、thead、tfoot的默认属性)或 inline-table)
  • overflow 值不为 visible 的块元素
  • display 值为 flow-root 的元素(该属性safari不支持)
  • contain 值为 layout、content或 paint 的元素
  • 弹性元素(display为 flex 或 inline-flex元素的直接子元素)
  • 网格元素(display为 grid 或 inline-grid 元素的直接子元素)
  • 多列容器(元素的 column-count 或 column-width 不为 auto,包括 column-count 为 1)
  • column-span 为 all 的元素始终会创建一个新的BFC,即使该元素没有包裹在一个多列容器中(标准变更,Chrome bug)。

BFC布局规则

  • 垂直方向的间隔由margin决定,同一个BFC里同级别的两个元素之间的margin会产生坍塌(问题1)
  • BFC计算高度时,float元素的高度也参与计算 (问题 2/3)
  • 每个BFC区域是隔离的,它里面元素不会影响外层,外层的各种变化也不会影响BFC区域。

Block formatting contexts are important for the positioning (see float) and clearing (see clear) of floats. The rules for positioning and clearing of floats apply only to things within the same block formatting context. Floats don't affect the layout of the content inside other block formatting contexts, and clear only clears past floats in the same block formatting context. Margin collapsing also occurs only between blocks that belong to the same block formatting context.

块格式化上下文对浮动定位(参见 float)与清除浮动(参见 clear)都很重要。浮动定位和清除浮动时只会应用于同一个BFC内的元素。浮动不会影响其它BFC中元素的布局,而清除浮动只能清除同一BFC中在它前面的元素的浮动。外边距折叠(Margin collapsing)也只会发生在属于同一BFC的块级元素之间。

参考:

# 怎么用一行css代码将整个站点变灰

前天4月4号全国哀悼日,各大网公司网站的风格都变灰了,是怎么实现的呢?其实很简单,一行代码就搞定。

/* 将html下的所有内容置灰 grayscale为灰度 */
html {
  filter: grayscale(100%);
}

css filter是什么属性?用css filter mdn关键字查了下,这里filter翻译为 滤镜他可以将模糊或色相等图形效果应用于元素,来看一些例子

/* 模糊,类似于马赛克效果 */
filter: blur(5px); 
/* 对比度 */
filter: contrast(100%);
/* 色相 */
filter: hue-rotate(90deg);
/* 阴影 */
filter: drop-shadow(16px 16px 20px red) invert(75%);

兼容性除了IE4-9,其他基本都兼容。Internet Explorer 4 to 9 implemented a non-standard filter property. The syntax was completely different from this one.

css_filter.png

更多filter文档及用法,参考:css filter - MDN (opens new window)

# 2020/04/05 周日

# uni-app开发体验怎么样?

如果你习惯开发vue,再来写uni,你会发现有很多问题

  1. vue-router非常不方便、使用vant组件用不了
  2. img标签到app上显示不了,需要使用image标签
  3. 路由页面要在pages.json里设置,js跳转需要使用uni.navigator等内置API

感觉开发体验很差。但如果你开发过小程序,你会发现

  1. template里UI组件部分基本类似于小程序开发
  2. script、style方面基本和vue开发体验保持一致,uni提供了许多类似于小程序的内置api,比如跳转路由、通用请求等

综上:uni开发就是小程序开发和vue开发的结合,碰巧我在小程序开发和vue开发方面都有一定的开发经验,所以感觉还算得心应手,但对于没有开发过小程序的开发人员来讲,刚开始会觉得不怎么习惯。另外我个人不喜欢这种开发方式,有点乱,能少碰就尽量少碰,感觉整体代码很乱,强依赖uni框架与HBuilderX开发工具。

# 2020/04/04 周六

# uni 设置tabbar后没显示

在pages.json里设置了tabbar配置,但设置成功后,在chrome运行却没效果,需要注意两点

  1. pages.json配置中tabBar参数设置的页面同时也需要在pages参数里设置
  2. pages数组中的第一项,必须是tabBar配置里的页面

# uni-app开发需要了解的事情

uni-app 读法为 u ni ai po,官网:uni-app 官方文档 (opens new window)

  1. uni-app开发需要下载HBuilderX开发工具
  2. 需要注册ucloud账号,打包安卓、iOS时需要有证书。安卓的证书免费,iOS证书需要花钱
  3. uni开发和通常vue-cli搭的脚手架开发方式还是有一定区别的,更像是vue + 小程序开发的结合
  4. 默认不支持 vue-router
  5. 默认hello word打包安卓的apk包为 16M 左右

# 常用的.gitignore 配置

.DS_Store
node_modules
/dist

# local env files
.env.local
.env.*.local

# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*

package-lock.json

# 2020/04/03 周五

# vue-cli项目使用@开头的路径

我们在vue-cli创建的vue项目,可以使用@ 开头的路径,表示从src目录开始,比如

() => import('../../../components/xxx/xx')  // bad 
() => import('@/components/xxx/xx')  // good 对应 src/components/xxx/xx

require('../../assets/img/xxx.png') // bad
require('@/assets/img/xxx.png') // good 对应 src/assets/img/xxx.png

这样如果某个文件夹下为一个功能模块的代码,那我们将这个目录移动到src下任意层级的目录都不会有影响,更健壮

# mac pro retina屏两倍图

之前在处理canvas绘制模糊的问题时了解到,retina是两倍像素屏,50 x 50像素的图会绘制在 100 x 100 像素区域,会导致绘制模糊。同理,如果需要在retina屏上显示50 x 50的图,需要提供两倍图 100 * 100,然后设置样式宽高为50,这样才会清晰。最好的方式还是使用矢量图、iconfont等不会失真的图。这样就不会模糊。

# 2020/04/01 周三

# grid布局问题

grid适用于网格化布局,对于各个模块按百分比来的情况比较好,如果多个模块有的固定宽高有的非固定宽高就不怎么好分了, IE下支持需要加前缀,详情参见之前的笔记: grid网格布局 (opens new window)

2020/04/17 更新,grid的某些属性不兼容IE,最开始我使用了grid发现IE怎么都兼容不了,有几个属性加了-ms-都没用,最后为了不必要的麻烦和风险,还是换成flex布局了,改成flex后的代码居然比grid还简单...

参考: CSS网格布局即使带有前缀也不能在IE11中工作 (opens new window)

# canvas怎么绘制环形进度条

有4个重点:

  1. 怎么画圆弧, ctx.arc函数里开始角度、结束角度以PI为基准,取值范围时:0 ~ 2PI,PI就是π值约等于3.14,圆心正上方位置为1.5PI,圆心右侧为 0 或 2*PI,圆心正下方为0.5PI,选定义额开始位置和结束位置就可以绘制任意一个弧形,详情参见: 之前的canvas笔记 - 绘制路径 (opens new window)

  2. 画圆环使用的是ctx.stroke,一般默认画圆大概是1px的宽度,线的宽度可以使用 ctx.lineWidth 调整,这样就成圆环了

  3. 用ctx.arc画一个完整的圆,再画一个进度圆弧,重叠在一起,颜色设置不一样,就是一个标准的圆环进度条了。

  4. 进度圆环怎么设置圆角,可以使用 ctx.lineCap = "round";

canvas_progress.png

<canvas id="drawing" width="180" height="180" >A draw of something.</canvas>
<script>
  let drawing = document.getElementById('drawing');
  let ctx = drawing.getContext('2d');
  let percent = 80 // 进度百分比
  let circleRadios = 80 // 圆环半径
  let lineWidth = 10
  let PI = 3.1415926
  let long  = (percent / 100) * PI * 2 // 百分比进度条长度
  let start = 1.5 * PI // 圆心正上方位置是 1.5PI
  ctx.lineWidth = lineWidth

  // 背景圆环
  let x = circleRadios + lineWidth
  let y = x
  ctx.beginPath()
  ctx.strokeStyle = 'rgb(241,247,255)'
  ctx.arc(x, y, circleRadios, start + long, start)
  ctx.stroke()

  // 进度圆环
  ctx.beginPath()
  let gradient = ctx.createLinearGradient(circleRadios * 2 + lineWidth * 2, lineWidth + circleRadios, 0 , circleRadios + lineWidth); // 从(130,130)到(160,160)渐变
  gradient.addColorStop(0, '#64E1FA'); // 渐变的起点色
  gradient.addColorStop(1, '#215BF7'); // 渐变的结束色
  ctx.strokeStyle = gradient
  ctx.arc(x, y, circleRadios, start, start + long)
  ctx.lineCap = "round";
  ctx.stroke()
</script>

参考:

上次更新: 2020/10/29 22:59:19