前端汇总 --007

162 阅读28分钟

一、H5与App的通讯方式

1.1 优缺点

Hybrid App在开发过程中存在的优缺点。

优点

  • H5页面交由前端进行开发,页面模块之间分开开发和维护,有效减少App的开发周期
  • H5页面不受限于应用商店繁琐的审核流程和冗长的等待时间,新增页面和功能、修复缺陷都可随时部署到线上
  • H5页面在有需要时才加载,减小App打包后的大小,缩短App在应用商店下载的时间和减少本地占用手机的空间
  • H5页面接入App Webview中,不再受限于浏览器,可通过与App交互调用设备更多底层的API来完善更多原本浏览器无法完成的操作

缺点

  • 协定好H5和App之间的通讯协议,定义好全局属性和全局方法在两者之间如何调用
  • H5页面接入App Webview中,可能会出现很多兼容问题,需要前端和客户端多加注意
  • 开发前需按照需求和交互进行页面划分,哪些页面归前端开发,哪些页面归客户端开发
  • 页面出现Bug有时候很难发现是在哪个环节出错,需要前端和客户端共同调试找出问题所在(可能各抒己见,打架都有份)

1.2 通讯方式

以下代码全部基于前端(React)进行演示。通讯方式有如下两种,都是使用JS代码来完成,兼容性还是挺不错的。

  • 前端通知客户端:拦截
  • 客户端通知前端:注入

前端通知客户端

在H5页面里触发链接跳转,App Webview检测到链接跳转再进行拦截,因此可以通过URL上携带参数来告知App下一步应该做些什么。

import React, { Component } from "react";

export default class App extends Component {
    componentDidMount() {
        location.href = "lsbox://toast?msg=页面加载完毕"; // 通知App
    }
    render() {
        return (
            <div className="app">
                <button type="button" onClick={this.openMask.bind(this)}>点它</button>
            </app>
        );
    }
    openMask() {
        location.href = "lsbox://mask?toggle=1"; // 通知App
    }
}

以上执行了location.href = "lsbox://mask?toggle=1"来通知App打开遮罩层

  • lsbox:前端和客户端统一定义链接跳转的协议(喜欢怎样定义协议都行)
  • mask:App需要执行的动作(喜欢怎样定义动作都行)
  • toggle=1:动作执行参数(自定义参数,用于告知App怎样做)
如果同步触发两个或以上的location.href(下一个location.href接着上一个location.href),App可能只会接收到一个location.href发出的通知,所以需要对下一个location.href使用setTimeout设置通知时间间隔(可使用Async/Await封装优化)
location.href = "lsbox://toast?msg=one";
setTimeout(() => {
    location.href = "lsbox://toast?msg=two";
    setTimeout(() => {
        location.href = "lsbox://toast?msg=three";
    }, 100);
}, 100);

客户端通知前端

注入一些全局方法,App Webview直接操作全局方法来控制H5页面,使用window.handleFunc = function() {}这样的形式来定义注入的方法。

import React, { Component } from "react";

export default class App extends Component {
    constructor(props) {
        super(props);
        this.state = {
            list: [0, 1, 2, 3, 4]
        };
    }
    componentDidMount() {
        window.addNum = this.addNum.bind(this); // 暴露方法给App
    }
    render() {
        return (
            <div className="app">
                <ul>{this.state.list.map(v => <li key={v}>{v}</li>)}</ul>
            </div>;
        );
    }
    addNum(num) {
        this.setState(prevState => ({
            list: prevState.list.concat(num);
        }));
    }
}

以上在组件加载完成后通过window.addNum = this.addNum.bind(this)将指定方法全局暴露到window上,App Webview可直接操作这些方法来控制H5页面。

1.3 调试神器 vConsole

1.31 vConsole介绍

vConsole是一款由微信公众平台前端团队打造的前端调试面板,专治手机端看log难题。

在线体验地址:

请在手机上打开链接:wechatfe.github.io/vconsole/de…

目前vConsole自带有5个面板,默认为“日志”面板,负责展示log。默认情况下,vConsole的面板是隐藏起来的。我们可以点击右下角的“面板”悬浮按钮来显示vConsole面板

1.32 vConsole使用

官方教程地址: github.com/Tencent/vCo…

1.4 H5移动端开发常用

1.41. 弹出数字键盘

<!-- 有"#" "*"符号输入 -->
<input type="tel">

<!-- 纯数字 -->
<input pattern="\d*">

1.42 调用系统的某些功能

<!-- 拨号 -->
<a href="tel:10086">打电话给: 10086</a>

<!-- 发送短信 -->
<a href="sms:10086">发短信给: 10086</a>

<!-- 发送邮件 -->
<a href="mailto:839626987@qq.com">发邮件给:839626987@qq.com</a>

<!-- 选择照片或者拍摄照片 -->
<input type="file" accept="image/*">

<!-- 选择视频或者拍摄视频 -->
<input type="file" accept="video/*">

<!-- 多选 -->
<input type="file" multiple>

1.43 打开原生应用

<a href="weixin://">打开微信</a>
<a href="alipays://">打开支付宝</a>
<a href="alipays://platformapi/startapp?saId=10000007">打开支付宝的扫一扫功能</a>
<a href="alipays://platformapi/startapp?appId=60000002">打开支付宝的蚂蚁森林</a>

这种方式叫做URL Scheme,是一种协议,一般用来访问APP或者APP中的某个功能/页面(如唤醒APP后打开指定页面或者使用某些功能)

URL Scheme的基本格式如下:

     行为(应用的某个功能/页面)    
            |
scheme://[path][?query]
   |               |
应用标识       功能需要的参数

一般是由APP开发者自己定义,比如规定一些参数或者路径让其他开发者来访问.

注意事项:

  • 唤醒APP的条件是你的手机已经安装了该APP
  • 某些浏览器会禁用此协议,比如微信内部浏览器(除非开了白名单)

1.44 解决active伪类失效

<body ontouchstart></body>

body注册一个空事件即可

1.45 忽略自动识别

<!-- 忽略浏览器自动识别数字为电话号码 -->
<meta name="format-detection" content="telephone=no">

<!-- 忽略浏览器自动识别邮箱账号 -->
<meta name="format-detection" content="email=no">

1.46 解决input失焦后页面没有回弹

一般出现在IOS设备中的微信内部浏览器,出现的条件为:

  • 页面高度过小
  • 聚焦时,页面需要往上移动的时候

所以一般input在页面上方或者顶部都不会出现无法回弹

解决办法为,在聚焦时,获取当前滚动条高度,然后失焦时,赋值之前获取的高度:

<template>
  <input type="text" @focus="focus" @blur="blur">
</template>

<script>
  export default {
    data() {
      return {
        scrollTop: 0
      }
    },
    
    methods: {
      focus() {
        this.scrollTop = document.scrollingElement.scrollTop;
      },
      
      blur() {
        document.scrollingElement.scrollTo(0, this.scrollTop);
      }
    }
  }
</script>

1.47 禁止长按

长按图片保存长按选择文字长按链接/手机号/邮箱时呼出菜单

想要禁止这些浏览器的默认行为,可以使用以下CSS

// 禁止长按图片保存
img {
  -webkit-touch-callout: none;
  pointer-events: none; // 像微信浏览器还是无法禁止,加上这行样式即可
}

// 禁止长按选择文字
div {
  -webkit-user-select: none;
}

// 禁止长按呼出菜单
div {
  -webkit-touch-callout: none;
}

1.48 滑动不顺畅,粘手

一般出现在IOS设备中,自定义盒子使用了overflow: auto || scroll后出现的情况。

优化代码:

div {
  -webkit-overflow-scrolling: touch;
}

1.49 屏幕旋转为横屏时,字体大小会变

具体出现的情况不明😒,有时候有有时候没有,欢迎指出。

优化代码:

* {
  -webkit-text-size-adjust: 100%;
}

二、 NPM镜像那些险象环生的坑

2.1 管理镜像

发项目时使用淘宝镜像,但是发布NPM第三方模块时就必须使用原镜像了。NPM镜像管理工具

  • 原镜像https://registry.npmjs.org/
  • 淘宝镜像https://registry.npm.taobao.org/

主角就是nrm,它是一个可随时随地自由切换NPM镜像的管理工具。

安装

npm i -g nrm

查看镜像

nrm ls

增加镜像

nrm add <name> <url>

移除镜像

nrm del <name>

测试镜像

nrm test <name>

使用镜像

nrm use <name>

查看当前镜像

nrm current

2.2 遇坑填坑

在安装过程中会隐式安装node-gypnode-gyp可编译这些依赖C++模块的模块。

npm config提供了一个参数disturl,它可设置Node镜像地址,当然还是将其指向国内的淘宝镜像。这样又能爽歪歪安装这些依赖C++模块的模块了。

npm config set disturl https://npm.taobao.org/mirrors/node/

使用node-sass作为项目开发依赖,但是node-sass的安装一直都是一个令人头疼的问题。

安装node-sass时,在install阶段会从Github上下载一个叫binding.node的文件,而GitHub Releases里的文件都托管在s3.amazonaws.com上,这个网址被Q了,所以又安装不了。

然而办法总比困难多,从node-sass的官方文档中可找到一个叫sass_binary_site的参数,它可设置Sass镜像地址,毫无疑问还是将其指向国内的淘宝镜像。这样又能爽歪歪安装node-sass了。

npm config set sass_binary_site https://npm.taobao.org/mirrors/node-sass/

其实还有好几个类似的模块,为了方便,笔者还是把它们源码里的镜像参数和淘宝镜像里对应的镜像地址扒出来,统一设置方便安装。以下是笔者常用的几个模块镜像地址配置,请收下!

分别是:SassSharpElectronPuppeteerPhantomSentrySqlitePython

镜像地址配置

npm config set <name> <url>,赶紧一键复制,永久使用。特别注意,别漏了最后面的/

npm config set sass_binary_site https://npm.taobao.org/mirrors/node-sass/
npm config set sharp_dist_base_url https://npm.taobao.org/mirrors/sharp-libvips/
npm config set electron_mirror https://npm.taobao.org/mirrors/electron/
npm config set puppeteer_download_host https://npm.taobao.org/mirrors/
npm config set phantomjs_cdnurl https://npm.taobao.org/mirrors/phantomjs/
npm config set sentrycli_cdnurl https://npm.taobao.org/mirrors/sentry-cli/
npm config set sqlite3_binary_site https://npm.taobao.org/mirrors/sqlite3/
npm config set python_mirror https://npm.taobao.org/mirrors/python/

Node版本与node-sass版本不兼容

node-sass版本兼容性好差,必须与Node版本对应使用才行,详情请参考node-sass-version-association,复用官方文档的版本对照表,如下。

NodeJSMinimum node-sass versionNode Module
Node 144.14+83
Node 134.13+79
Node 124.12+72
Node 114.10+67
Node 104.9+64
Node 84.5.3+57

执行npm i安装依赖前请确保当前的Node版本和node-sass版本已兼容。

全局缓存中的binding.node版本与Node版本不兼容

假如本地使用nvmn进行Node版本管理,并且已切换了Node版本,在安装过程中可能会出现Windows/OS X/Linux 64-bit with Node.js 12.x这样的提示,(电脑里安装了多个Node版本并且经常来回切换😂)。

这是因为node-sass版本和Node版本是关联的(看上面的表格),修改Node版本后在全局缓存中匹配不到对应的binding.node文件而导致安装失败。根据错误提示,清理NPM缓存且重新安装即可,解决办法如下。

npm cache clean -f

npm rebuild node-sass

所以没什么事就别来回切换Node版本了。

安装失败后重新安装

有可能无权限删除已安装的内容,导致重新安装时可能会产生某些问题,建议将node_modules全部删除并重新安装。

在Mac系统和Linux系统上删除node_modules比较快,但是在Windows系统上删除node_modules就比较慢了,推荐大家使用rimraf删除node_modules,一个Node版的rm -rf工具。

npm i -g rimraf

在项目的package.json中加入npm scriptsrimraf常驻。三大操作系统通用,非常推荐使用。

{
  "scripts": {
    "reinstall": "rimraf node_modules && npm i"
  }
}

一有什么安装失败重新安装之类的操作,先执行npm run remove删除node_modulesnpm i

npm run reinstall

三、js代码魔法

3.1 一致的代码格式:

使用一致且被广泛接受的代码风格指南,比如 ESLint 提供的指南,并配置你的编辑器或 IDE 以自动格式化代码。 示例:

// 错误的格式化
function calculateSum(a,b){return a+b; }

// 正确的格式化
function calculateSum(a, b) {
  return a + b;
}

3.2  有意义的变量和函数命名:

为变量、函数和类使用有意义且描述性的名称。避免使用单个字母或容易引起他人困惑的缩写。这种做法提高了代码的可读性,并减少了对注释的需求。

3.3 模块化和单一职责原则:

遵循单一职责原则,为函数和类设定单一、明确的职责。 示例:

// 错误的做法
function calculateSumAndAverage(numbers) {
  let sum = 0;
  for (let i = 0; i < numbers.length; i++) {
    sum += numbers[i];
  }
  const average = sum / numbers.length;
  return [sum, average];
}

// 正确的做法
function calculateSum(numbers) {
  let sum = 0;
  for (let i = 0; i < numbers.length; i++) {
    sum += numbers[i];
  }
  return sum;
}

function calculateAverage(numbers) {
  const sum = calculateSum(numbers);
  const average = sum / numbers.length;
  return average;
}

3.4 避免全局变量:

尽量减少使用全局变量,因为它们可能导致命名冲突,并使代码更难以理解。相反,封装你的代码到函数或模块中,并尽可能使用局部变量。 示例:

// 错误的做法
let count = 0;

function incrementCount() {
  count++;
}

// 正确的做法
function createCounter() {
  let count = 0;

  function incrementCount() {


    count++;
  }

  return {
    incrementCount,
    getCount() {
      return count;
    }
  };
}

const counter = createCounter();
counter.incrementCount();

3.5 错误处理和鲁棒性:

优雅地处理错误,并提供有意义的错误信息或适当地记录它们。验证输入,处理边界情况,并使用正确的异常处理技术,如 try-catch 块。 示例:

// 错误的做法
function divide(a, b) {
  return a / b;
}

// 正确的做法
function divide(a, b) {
  if (b === 0) {
    throw new Error('Cannot divide by zero');
  }
  return a / b;
}

try {
  const result = divide(10, 0);
  console.log(result);
} catch (error) {
  console.error(error.message);
}

3.6 避免重复代码:

代码重复不仅会导致冗余代码,还会增加维护和修复错误的难度。将可重用的代码封装到函数或类中,并努力遵循 DRY(Don't Repeat Yourself)原则。

3.7 明智地使用注释:

谨慎使用注释,并使其简洁而有意义。注重解释为什么而不是“如何”。 示例:

// 错误的做法
function calculateTotalPrice(products) {
  // 遍历产品
  let totalPrice = 0;
  for (let i = 0; i < products.length; i++) {
    totalPrice += products[i].price;
  }
  return totalPrice;
}

// 正确的做法
function calculateTotalPrice(products) {
  let totalPrice = 0;
  for (let i = 0; i < products.length; i++) {
    totalPrice += products[i].price;
  }
  return totalPrice;
  // 总价格通过将数组中所有产品的价格相加来计算。
}

3.8 优化性能:

使用适当的数据结构和算法来优化性能。使用类似 Chrome DevTools 的工具对代码进行性能分析和测量,以识别并相应地解决性能问题。

示例:

// 错误的做法
function findItemIndex(array, target) {
  for (let i = 0; i < array.length; i++) {
    if (array[i] === target) {
      return i;
    }
  }
  return -1;
}

// 正确的做法(二分算法)
function findItemIndex(array, target) {
  let left = 0;
  let right = array.length - 1;

  while (left <= right) {
    const mid = Math.floor((left + right) / 2);

    if (array[mid] === target) {
      return mid;
    }

    if (array[mid] < target) {
      left = mid + 1;
    } else {
      right = mid - 1;
    }
  }

  return -1;
}

3.9 使用函数式编程概念:

函数式编程概念,如不可变性和纯函数,可以使代码更可预测且更易于理解。拥抱不可变数据结构,并尽量避免对对象或数组进行突变。编写无副作用且对于相同的输入产生相同输出的纯函数。 示例:

// 错误的做法
let total = 0;

function addToTotal(value) {
  total += value;
}

// 正确的做法
function addToTotal(total, value) {
  return total + value;
}

3.91 头等函数

当一门编程语言的函数可以被当作变量一样用时,则称这门语言拥有头等函数。例如,在这门语言中,函数可以被当作参数、返回值、变量。

为什么 JavaScript 是头等函数 ?

在 JavaScript 中函数就是一个普通的对象(可以通过 new Function( ) ),我们可以把函数储存在变量/数组中,它还可以作为另一个函数的参数和返回值,甚至我们可以在程序运行的过程中通过 new Function( ) 来构造一个新的函数。

3.92# 高阶函数

当一个函数接收另一个函数作为参数,这种函数就称之为高阶函数。

高阶函数的特点:

  • 可以把函数作为参数返回给另一个函数
  • 可以把函数作为另一个函数的返回结果

函数作为参数:

// forEach
// 定义一个遍历数组的并对每一项做处理的函数,第一个函数是一个数组,第二个参数是一个函数。
function forEach (array, fn) {
    for (let i = 0; i < array.length; i++) {
        fn(array[i]) 
    } 
}

// test
let arr = [1, 2, 3]
forEach(arr, item => {
    item = item * 2
    console.log(item) // 2 4 6
})

// filter
// 遍历数组,并把满足条件的元素存储成数组,再进行返回
function filter(array, fn) {
    let results = []
    for (let i = 0; i < array.length; i++) { 
        //如果满足条件  
        if (fn(array[i])) { 
            results.push(array[i]) 
        }    
    }
    return results
}

// test
let arr = [1, 3, 4, 7, 8]
let result = filter(arr, item => item % 2 === 0)
console.log(result) // [4, 8]

函数作为返回值:

// 一个函数返回另一个函数
function makeFn () {
    let msg = 'Hello function' 
    return function () { 
        console.log(msg) 
    } 
}

// test
// 第一种调用方式
const fn = makeFn() 
fn() //Hello function

// 第二种调用方式
makeFn()()///Hello function
// once
// 让函数只执行一次

function once(fn) {
    let done = false
    return function() {
        // 判断值有没有被执行,如果是false表示没有执行,如果是true表示已经执行过了,不必再执行
        if(!done) {
            done = true
            // 调用fn,当前this直接传递过来,第二个参数是把fn的参数传递给return的函数
            return fn.apply(this, arguments)
        }
    }
}

// test
let pay = once(function (money) {
    console.log(`支付:${money} RMB`)
})

pay(5) //支付:5 RMB
pay(5)
pay(5)

3.93 闭包函数

函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包 通俗的讲:可以在另一个作用域中调用一个函数的内部函数并访问到该函数作用域中的成员

当函数执行完毕后会从执行栈上移除,但是堆上的作用域成员因为被外部引用而不能释放,因此内部函数依然可以访问外部函数的成员。

局部变量会常驻在内存中,可以避免使用全局变量,防止全局变量污染。如果被长期占用,而不被释放,会造成内存泄漏。

// 计算一个数平方和立方的运算
ath.pow(4, 2)
Math.pow(5, 2)
// 后面的二次方三次方很多次重复,下面要写一个二次方三次方的函数
function makePower (power) {
  return function (number) {
    return Math.pow(number, power)
  }
}

// 求平方
let power2 = makePower(2)
let power3 = makePower(3)

console.log(power2(4)) // 16
console.log(power2(5)) // 25
console.log(power3(4)) // 64
// 计算不同级别的员工工资
// 假设计算员工工资的函数第一个函数传基本工资,第二个参数传绩效工资
// getSalary(12000, 2000)
// getSalary(15000, 3000)
// getSalary(15000, 4000)

// 不同级别的员工基本工资是一样的,所以我们将基本工资提取出来,之后只需要加上绩效工资
function makeSalary (base) { 
    return function (performance) { 
        return base + performance 
    }
}
let salaryLevel1 = makeSalary(12000)
let salaryLevel2 = makeSalary(15000)

console.log(salaryLevel1(2000)) //14000
console.log(salaryLevel2(3000)) //18000
console.log(salaryLevel2(4000)) //19000

3.94 函数的柯里化

  • 使用柯里化Curry解决硬编码问题
  • 函数多个参数时进行改造,调用时只传递部分参数,并且让函数返回新的函数,新的函数接收剩余参数,并返回相应的结果
  • lodash _curry(func)使用:
  • 接收一个或多个func的参数,如果func所需的参数都被提供则执行func并返回执行结果,否则继续返回该函数等待- 接收剩余参数
  • 参数:需要柯里化的函数,返回值:柯里化后的函数 类似于对于参数的’缓存’,将多参数函数分割成颗粒度更小的纯函数,便于重用,同时组合产生强大的功能
//lodash _.curry()使用
const _ = require("lodash")

//三元函数
function getSum(a,b,c) {
  return a+b+c
}
const curried = _.curry(getSum)
console.log(curried(1,2,3))
console.log(curried(1)(2,3))
console.log(curried(1,2)(3))
console.log(curried(1)(2)(3));

函数珂里化的原理实现

//柯里化原理模拟

//通过判断返回的函数参数是否传全产生两种情况,
//传全那么直接调用传入的fn方法,为传全返回一个新函数,未传全则返回一个新函数,新函数的参数为上次参数和当前的arguments的和
/**
* 解释01:此时的arguments为继续调用的参数的伪数组 例如:fn(1)(2,3) arguments指(2,3)
*        伪数组转化通过Array.from(),
*        args是个闭包内的变量被保存下来,指的是上一次调用curriedFn时的args
* */
function curry(fn:Function) {
return function curriedFn(...args) {
  if(args.length < fn.length) //传入参数的个数 or 获取fn函数形参个数
  {
    return function () {
      return curriedFn(...args.concat(Array.from(arguments))) //解释01
    }
  }
  return fn.apply(null,args) //fn(...args)
}
}

function addSum(a,b,c) {
return a+b+c
}

const _addSum = curry(addSum)
console.log(_addSum(1,2,3));
console.log(_addSum(1)(2,3));

函数组合

  • 纯函数和柯里化很容易写出洋葱代码 h(g(f()))  .toUpper( .first(_.reverse(array)))
  • 函数组合实现以上相同功能,但是通过组合的方式
//函数组合演示
function compose(f,g) {  
    return function (value) {
      return f(g(value)) //封装个洋葱函数
    }
}
//案例:取数组最后一个值
const _reverse = function (value) { //辅助函数
    return value.reverse()
}
const _first = function (value) { //辅助函数
    return value[0]
}

const _compose = compose(_first,_reverse)
console.log(_compose([1,2,3]))

3.10 使用代码检查工具和格式化工具:

使用 ESLint 和 Prettier 等工具来强制执行一致的代码风格,并在问题出现之前捕获潜在问题。

// .eslintrc.json
{
  "extends": ["eslint:recommended", "prettier"],
  "

plugins": ["prettier"],
  "rules": {
    "prettier/prettier": "error"
  }
}

// .prettierrc.json
{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "all"
}

四、 装饰器

4.1 什么是装饰器

装饰器是一个函数,它接收函数或类作为参数,并返回修改后的函数或类。装饰器可以通过@符号应用于函数或类。

下面是一个简单的装饰器示例:

function myDecorator(target) {
  // 对类或函数进行修改
  return target;
}

@myDecorator
class MyClass {
  // 类的定义
}

@myDecorator
function myFunction() {
  // 函数的定义
}

4.2 装饰器的作用

装饰器可以用于许多场景,例如:

  • 类属性保护
  • 函数参数类型检查
  • 函数返回值类型检查
  • 函数调用计时

4.21 装饰函数参数

4.211 参数类型检查

装饰器可以用于检查函数参数的类型。下面是一个使用装饰器对函数参数进行类型检查的示例:

function myParameterDecorator(target, name, descriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function(...args) {
    args.forEach((arg, index) => {
      const argType = typeof arg;
      if (argType !== 'number' && argType !== 'string') {
        throw new Error(`Argument ${index} is of type ${argType}, but should be a string or a number`);
      }
    });
    return originalMethod.apply(this, args);
  }
  return descriptor;
}

class MyClass {
  @myParameterDecorator
  myMethod(name, age) {
    console.log(`Hello, my name is ${name} and I am ${age} years old`);
  }
}

const myObject = new MyClass();
myObject.myMethod("Alice", 30); // 输出:Hello, my name is Alice and I am 30 years old
myObject.myMethod("Bob", ["thirty"]); // 抛出错误:Argument 1 is of type string, but should be a string or a number

在上面的示例中,@myParameterDecorator 装饰器用于在调用 myMethod 函数之前检查传递给函数的每个参数是否为字符串或数字类型。如果参数不是这两种类型之一,则抛出错误。

4.212 参数默认值

装饰器还可以用于设置函数参数的默认值。下面是一个使用装饰器对函数参数设置默认值的示例:

function myParameterDecorator(target, name, descriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function(name = "World") {
    return originalMethod.call(this, name);
  }
  return descriptor;
}

class MyClass {
  @myParameterDecorator
  myMethod(name) {
    console.log(`Hello, ${name}`);
  }
}

const myObject = new MyClass();
myObject.myMethod(); // 输出:Hello, World
myObject.myMethod("Alice"); // 输出:Hello, Alice

在上面的示例中,@myParameterDecorator 装饰器用于在调用 myMethod 函数时检查参数 name 是否存在。如果参数 name 不存在,则使用默认值 "World"。

4.22 装饰类属性

4.21 类属性保护

装饰器可以用于保护类属性,防止它们被修改或直接访问。下面是一个使用装饰器保护类属性的示例:

function myPropertyDecorator(target, name) {
  let value = target[name];
  const getter = function() {
    return value;
  };
  const setter = function(newValue) {
    if (typeof newValue === 'string') {
      value = newValue;
    } else {
      throw new Error(`Property ${name} must be a string`);
    }
  };
  delete target[name];
  Object.defineProperty(target, name, {
    get: getter,
    set: setter,
    enumerable: true,
    configurable: true,
  });
}

class MyClass {
  @myPropertyDecorator
  myProperty = "initial value";
}

const myObject = new MyClass();

console.log(myObject.myProperty); // 输出:initial value
myObject.myProperty = "new value";
console.log(myObject.myProperty); // 输出:new value
myObject.myProperty = 123;
// 抛出错误:Property myProperty must be a string

在上面的示例中,@myPropertyDecorator 装饰器用于保护 MyClass 类的一个属性 myProperty。如果尝试将属性设置为非字符串值,则会抛出错误。

4.22 类属性初始值

装饰器还可以用于设置类属性的初始值。下面是一个使用装饰器设置类属性初始值的示例:

function myDefaultDecorator(defaultValue) {
  return function(target, name) {
    target[name] = defaultValue;
  }
}

class MyClass {
  @myDefaultDecorator('initial value')
  myProperty;
}

const myObject = new MyClass();
console.log(myObject.myProperty); // 输出:initial value

在上面的示例中,@myDefaultDecorator 装饰器用于设置 MyClass 类的一个属性 myProperty 的初始值为 "initial value"。

4.23 装饰函数返回值

4.231 返回类型检查

装饰器可以用于检查函数返回值的类型。下面是一个使用装饰器对函数返回值进行类型检查的示例:

function myReturnDecorator(target, name, descriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function(...args) {
    const result = originalMethod.apply(this, args);
    if (typeof result !== 'number') {
      throw new Error(`Method ${name} must return a number`);
    }
    return result;
  }
  return descriptor;
}

class MyClass {
  @myReturnDecorator
  myMethod() {
    return 42;
  }
}

const myObject = new MyClass();
console.log(myObject.myMethod()); // 输出:42
myObject.myMethod = function() {
  return "not a number";
};
// 抛出错误:Method myMethod must return a number

在上面的示例中,@myReturnDecorator 装饰器用于在调用 myMethod 函数之后检查函数返回值是否为数字类型。如果返回值不是数字类型,则抛出错误。

4.232 返回值格式化

装饰器还可以用于格式化函数返回值。下面是一个使用装饰器格式化函数返回值的示例:

function myFormatDecorator(target, name, descriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function(...args) {
    const result = originalMethod.apply(this, args);
    if (typeof result === 'string') {
      return result.toUpperCase();
    } else {
      return result;
    }
  }
  return descriptor;
}

class MyClass {
  @myFormatDecorator
  myMethod() {
    return "hello, world";
  }
}

const myObject = new MyClass();
console.log(myObject.myMethod()); // 输出:HELLO, WORLD

在上面的示例中,@myFormatDecorator 装饰器用于将 myMethod 函数返回值转换为大写字母

4.24 装饰函数调用

4.241 前置操作

装饰器可以用于在函数调用之前执行某些操作。下面是一个使用装饰器在函数调用之前执行某些操作的示例:

function myBeforeDecorator(before) {
  return function(target, name, descriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function(...args) {
      before();
      return originalMethod.apply(this, args);
    }
    return descriptor;
  }
}

class MyClass {
  @myBeforeDecorator(() => console.log("before"))
  myMethod() {
    console.log("myMethod");
  }
}

const myObject = new MyClass();
myObject.myMethod();
// 输出:
// before
// myMethod

在上面的示例中,@myBeforeDecorator 装饰器用于在调用 myMethod 函数之前执行一个回调函数。

4.242 后置操作

装饰器还可以用于在函数调用之后执行某些操作。下面是一个使用装饰器在函数调用之后执行某些操作的示例:

function myAfterDecorator(after) {
  return function(target, name, descriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function(...args) {
      const result = originalMethod.apply(this, args);
      after(result);
      return result;
    }
    return descriptor;
  }
}

class MyClass {
  @myAfterDecorator(result => console.log("after: " + result))
  myMethod() {
    console.log("myMethod");
    return "result";
  }
}

const myObject = new MyClass();
console.log(myObject.myMethod());
// 输出:
// myMethod
// after: result
// result

在上面的示例中,@myAfterDecorator 装饰器用于在调用 myMethod 函数之后执行一个回调函数,将函数返回值作为参数传递给回调函数。

五、 JavaScript快乐写法

概述 :内容的索引表,供翻阅以快速定位,如下:

应用场景标题描述补充1补充2
数组去重通过内置数据解构特性进行去重[] => set => []通过遍历并判断是否存在进行去重[many items].forEach(item => (item <不存在于> uniqueArr) && uniqueArr.push(item))
数组的最后一个元素获取数组中位置最后的一个元素使用at(-1)
数组对象的相关转换对象到数组:Object.entries()数组到对象:Obecjt.fromEntries()
短路操作通过短路操作避免后续表达式的执行a或b:a真b不执行a且b:a假b不执行
基于默认值的对象赋值通过对象解构合并进行带有默认值的对象赋值操作{...defaultData, ...data}
多重条件判断优化单个值与多个值进行对比判断时,使用includes进行优化[404,400,403].includes
交换两个值通过对象解构操作进行简洁的双值交换[a, b] = [b, a]
位运算通过位运算提高性能和简洁程度
replace()的回调通过传入回调进行更加细粒度的操作
sort()的回调通过传入回调进行更加细粒度的操作根据字母顺序排序根据真假值进行排序

5.1 数组去重

比较常见的基本有如下两类方法:

1)通过内置数据结构自身特性进行去重

主要就是利用JavaScript内置的一些数据结构带有不包含重复值的特性,然后通过两次数据结构转换的消耗[] => set => []从而达到去重的效果,如下演示:

const arr = ['justin1go', 'justin2go', 'justin2go', 'justin3go', 'justin3go', 'justin3go'];
const uniqueArr = Array.from(new Set(arr));
// const uniqueArr = [...new Set(arr)];

2)通过遍历并判断是否存在进行去重

白话描述就是:通过遍历每一项元素加入新数组,新数组存在相同的元素则放弃加入,伪代码:[many items].forEach(item => (item <不存在于> uniqueArr) && uniqueArr.push(item))

至于上述的<不存在于>操作,可以是各种各样的方法,比如再开一个for循环判断新数组是否有相等的,或者说利用一些数组方法判断,如indexOfincludesfilterreduce等等

const arr = ['justin1go', 'justin2go', 'justin2go', 'justin3go', 'justin3go', 'justin3go'];
const uniqueArr = [];
arr.forEach(item => {
	// 或者!uniqueArr.includes(item)
	if(uniqueArr.indexOf(item) === -1){
		uniqueArr.push(item)
	}
})

结合filter(),判断正在遍历的项的index,是否是原始数组的第一个索引:

const arr = ['justin1go', 'justin2go', 'justin2go', 'justin3go', 'justin3go', 'justin3go'];
const uniqueArr = arr.filter((item, index) => {
	return arr.indexOf(item, 0) === index;
})

结合reduce(),prev初始设为[],然后依次判断cur是否存在于prev数组,如果存在则加入,不存在则不动:

const arr = ['justin1go', 'justin2go', 'justin2go', 'justin3go', 'justin3go', 'justin3go'];
const uniqueArr = arr.reduce((prev,cur) => prev.includes(cur) ? prev : [...prev,cur],[]);

5.2 数组的最后一个元素

对于获取数组的最后一个元素,可能平常见得多的就是arr[arr.length - 1],我们其实可以使用at()方法进行获取

const arr = ['justin1go', 'justin2go', 'justin3go'];
console.log(arr.at(-1)) // 倒数第一个值
console.log(arr.at(-2)) // 倒数第二个值
console.log(arr.at(0)) // 正数第一个  
console.log(arr.at(1)) // 正数第二个

注:node14应该是不支持的,目前笔者并不建议使用该方法,但获取数组最后一个元素是很常用的,就应该像上述语法一样简单...

5.2 数组对象的相互转换

const entryified = [
    ["key1", "justin1go"],
    ["key2", "justin2go"],
    ["key3", "justin3go"]
];

const originalObject = Object.fromEntries(entryified);
console.log(originalObject);

5.4 短路操作

被合理运用的短路操作不仅非常的优雅,还能减少不必要的计算操作

1)基本介绍

主要就是||或操作、&&且操作当第一个条件(左边那个)已经能完全决定整个表达式的值的时候,编译器就会跳过该表达式后续的计算

  • 或操作a || b:该操作只要有一个条件为真值时,整个表达式就为真;即a为真时,b不执行;
  • 且操作a && b:该操作只要有一个条件为假值时,整个表达式就为假;即a为假时,b不执行;

2)实例

网络传输一直是前端的性能瓶颈,所以我们在做一些判断的时候,可以通过短路操作减少请求次数:

const nextStep = isSkip || await getSecendCondition();
if(nextStep) {
	openModal();
}

还有一个经典的代码片段:

function fn(callback) {
	// some logic
	callback && callback()
}

5.5 基于默认值的对象赋值

  • 很多时候,我们在封装一些函数或者类时,会有一些配置参数。
  • 但这些配置参数通常来说会给出一个默认值,而这些配置参数用户是可以自定义的
  • 除此之外,还有许许多多的场景会用到的这个功能:基于默认值的对象赋值。
function fn(setupData) {
	const defaultSetup = {
		email: "justin3go@qq.com",
		userId: "justin3go",
		skill: "code",
		work: "student"
	}
	return { ...defaultSetup, ...setupData }
}

const testSetData = { skill: "sing" }
console.log(fn(testSetData))

如上{ ...defaultSetup, ...setupData }就是后续的值会覆盖前面key值相同的值。

5.6 多重条件判断优化

if(condtion === "justin1go" || condition === "justin2go" || condition === "justin3go"){
	// some logic
}

如上,当我们对同一个值需要对比不同值的时候,我们完全可以使用如下的编码方式简化写法并降低耦合性:

const someConditions = ["justin1go", "justin2go", "justin3go"];
if(someConditions.includes(condition)) {
	// some logic
}

5.7 交换两个值

一般来说,我们可以增加一个临时变量来达到交换值的操作,在JS中,也可以通过解构操作交换值;

let a = 1;
let b = 2;
[a, b] = [b, a]
  • 这里相当于使用了一个数组对象同时存储了a和b,该数组对象作为了临时变量
  • 之后再将该数组对象通过解构操作赋值给a和b变量即可

同时,还有种比较常见的操作就是交换数组中两个位置的值:

const arr = ["justin1go", "justin2go", "justin3go"];
[arr[0], arr[2]] = [arr[2], arr[0]]

5.8 位运算

一些常见的位运算操作,参考链接

1 ) 使用&运算符判断一个数的奇偶

// 偶数 & 1 = 0
// 奇数 & 1 = 1
console.log(2 & 1)    // 0
console.log(3 & 1)    // 1

2 ) 使用~, >>, <<, >>>, |来取整

console.log(~~ 6.83)    // 6
console.log(6.83 >> 0)  // 6
console.log(6.83 << 0)  // 6
console.log(6.83 | 0)   // 6
// >>>不可对负数取整
console.log(6.83 >>> 0)   // 6

3 ) 使用^来完成值交换

var a = 5
var b = 8
a ^= b
b ^= a
a ^= b
console.log(a)   // 8
console.log(b)   // 5

4 ) 使用&, >>, |来完成rgb值和16进制颜色值之间的转换

/**
 * 16进制颜色值转RGB
 * @param  {String} hex 16进制颜色字符串
 * @return {String}     RGB颜色字符串
 */
  function hexToRGB(hex) {
    var hexx = hex.replace('#', '0x')
    var r = hexx >> 16
    var g = hexx >> 8 & 0xff
    var b = hexx & 0xff
    return `rgb(${r}, ${g}, ${b})`
}

/**
 * RGB颜色转16进制颜色
 * @param  {String} rgb RGB进制颜色字符串
 * @return {String}     16进制颜色字符串
 */
function RGBToHex(rgb) {
    var rgbArr = rgb.split(/[^\d]+/)
    var color = rgbArr[1]<<16 | rgbArr[2]<<8 | rgbArr[3]
    return '#'+ color.toString(16)
}
// -------------------------------------------------
hexToRGB('#ffffff')               // 'rgb(255,255,255)'
RGBToHex('rgb(255,255,255)')      // '#ffffff'

5.9 replace()的回调函数

replace()的回调函数

5.10 sort()的回调函数

sort()通过回调函数返回的正负情况来定义排序规则,由此,对于一些不同类型的数组,我们可以自定义一些排序规则以达到我们的目的:

  • 数字升序:arr.sort((a,b)=>a-b)
  • 按字母顺序对字符串数组进行排序:arr.sort((a, b) => a.localeCompare(b))
  • 根据真假值进行排序:
const users = [
  { "name": "john", "subscribed": false },
  { "name": "jane", "subscribed": true },
  { "name": "jean", "subscribed": false },
  { "name": "george", "subscribed": true },
  { "name": "jelly", "subscribed": true },
  { "name": "john", "subscribed": false }
];

const subscribedUsersFirst = users.sort((a, b) => Number(b.subscribed) - Number(a.subscribed))

六、 正则表达式与 glob 匹配

6.1 正则表达式

6.11简介

正则表达式由 字符特殊符号 组成,用于定义文本模式,以下是一些常见的正则表达式特殊符号的含义:

  • .:匹配任意单个字符
  • *:匹配前一个字符的零个或多个出现
  • +:匹配前一个字符的一个或多个出现
  • ?:匹配前一个字符的零个或一个出现
  • ^:匹配文本的开头
  • $:匹配文本的结尾
  • []:匹配括号内的任意一个字符
  • ():定义一个捕获组,可以提取匹配的部分

6.12 示例

一些正则使用的常见场景:

  1. 匹配邮箱地址

    • 正则表达式:^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$
    • 示例:
    const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$/;
    const email = 'example@example.com';
    if (emailRegex.test(email)) {
      console.log('邮箱地址有效');
    } else {
      console.log('邮箱地址无效');
    }
    
  2. 匹配手机号码

    • 正则表达式:/^1[3456789]\d{9}$/
    • 示例:
    const phoneRegex = /^1[3456789]\d{9}$/;
    const phoneNumber = '13812345678';
    if (phoneRegex.test(phoneNumber)) {
      console.log('手机号码有效');
    } else {
      console.log('手机号码无效');
    }
    
  3. 提取 URL 中的域名

    • 正则表达式:/https?://([^/]+)/.*/
    • 示例:
    const urlRegex = /https?://([^/]+)/.*/;
    const url = 'https://www.example.com/path/to/page';
    const domain = url.match(urlRegex)[1];
    console.log('域名:', domain);
    
  4. 替换字符串中的所有数字

    • 正则表达式:/\d/g
    • 示例:
    const string = 'Hello 123 World 456';
    const result = string.replace(/\d/g, '');
    console.log('替换后的字符串:', result);
    
  5. 验证日期格式(YYYY-MM-DD)

    • 正则表达式:/^\d{4}-\d{2}-\d{2}$/
    • 示例:
    const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
    const date = '2022-12-31';
    if (dateRegex.test(date)) {
      console.log('日期格式正确');
    } else {
      console.log('日期格式错误');
    }
    
  6. 匹配 HTML 标签

    • 正则表达式:/<[^>]+>/g
    • 示例:
    const html = '<div>Hello World</div>';
    const tags = html.match(/<[^>]+>/g);
    console.log('匹配到的标签:', tags);
    
  7. 验证密码强度(包含至少一个大写字母、一个小写字母和一个数字):

    • 正则表达式:/(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}/
    • 示例:
    const passwordRegex = /(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}/;
    const password = 'Abcdefg123';
    if (passwordRegex.test(password)) {
      console.log('密码强度符合要求');
    } else {
      console.log('密码强度不符合要求');
    }
    
  8. 匹配邮政编码(中国邮政编码为 6 位数字):

    • 正则表达式:/^\d{6}$/
    • 示例:
    const postalCodeRegex = /^\d{6}$/;
    const postalCode = '123456';
    if (postalCodeRegex.test(postalCode)) {
      console.log('邮政编码有效');
    } else {
      console.log('邮政编码无效');
    }
    
  9. 验证身份证号码(中国身份证号码为 18 位数字):

    • 正则表达式:/^\d{17}[\dXx]$/
    • 示例:
    const idRegex = /^\d{17}[\dXx]$/;
    const idNumber = '320123198012345678';
    if (idRegex.test(idNumber)) {
      console.log('身份证号码有效');
    } else {
      console.log('身份证号码无效');
    }
    
  10. 验证 URL 格式

    • 正则表达式:/^(https?://)?([\w.-]+).([a-zA-Z]{2,})(/\S*)?$/
    • 示例:
    const urlRegex = /^(https?://)?([\w.-]+).([a-zA-Z]{2,})(/\S*)?$/;
    const url = 'https://www.example.com/path/to/page';
    if (urlRegex.test(url)) {
      console.log('URL 格式正确');
    } else {
      console.log('URL 格式错误');
    }
    

6.2 glob匹配

6.21 简介

当我们需要在文件系统中进行 文件路径 匹配时,可以使用 glob 模式来快速匹配符合特定模式的文件路径,它是一种简单且常用的模式匹配语法,广泛应用于文件操作和构建工具中

其语法规则如下:

  • ***** : 匹配0个或者多个字符,比如d*,可以匹配ddlddl
  • ? : 匹配单个字符,比如d?,只匹配dd,不能匹配d
  • [] : 包含在[]中的字符,只会被匹配一个,并且[]不可以为空,比如 [abc] 匹配a,b,c三个中的一个字符
  • -: 两个字符中间用'-'连接表示range,比如[0-9]等同于[0123456789],需要注意的是如果'-'出现在开头或者结尾,并不表示range,比如[-a]或者[a-]匹配 '-'或'a' 字符中的一个
  • ! : 取反,[!abc] 表示匹配a,b,c之外的一个字符
  • ****** : 双星号代表可以匹配后代所有子目录
  • 任何以 . 开头命名的文件,都必须在glob中显示指定才能匹配,比如有一个文件.abc,那么rm *匹配不到.abc,只能使用rm .*

6.22 示例

以下是一些常见的 glob 匹配模式示例:

  • *.js:匹配当前目录下所有以 .js 结尾的文件
  • src/**/*.js:匹配 src 目录及其所有子目录下的所有以 .js 结尾的文件
  • app.{js,css}:匹配当前目录下的 app.jsapp.css 文件
  • !dist/*.js:排除匹配 dist 目录下的所有 .js 文件
  • [abc].js:匹配当前目录下的 a.jsb.jsc.js 文件
  • ?(pattern|pattern|pattern).js:匹配当前目录下的零个或一个括号内指定的模式文件,如 pattern.jspattern2.js
  • +(pattern|pattern|pattern).js:匹配当前目录下至少一个括号内指定的模式文件,如 pattern.jspattern2.js
  • *(pattern|pattern|pattern).js:匹配当前目录下任意数量的括号内指定的模式文件,如 pattern.jspattern2.js
  • @(pattern|pat*|pat?erN).js:匹配当前目录下与括号内模式之一匹配的文件,如 pattern.jspatN.js

6.3 正则表达式与 glob 匹配的区别

正则表达式和 glob 匹配都是用于 模式匹配,但它们在语法和用途上存在一些区别:

  • 语法差异:正则表达式使用 特殊符号 来表示模式,具有更高的灵活性和表达能力,而 glob 使用通配符(如 *?)来匹配文件路径模式,更加简洁易懂
  • 匹配范围:正则表达式可以匹配更复杂的 文本模式,而 glob 主要用于匹配 文件路径模式
  • 匹配方式:正则表达式是通过模式的 匹配规则 来匹配字符串的,可以进行更精确的匹配和提取,而 glob 是根据 通配符 来匹配文件路径,只能进行简单的文件名匹配
  • 使用场景:正则表达式适用于需要对字符串进行 复杂模式匹配和替换 的场景,如验证表单数据、提取特定信息,glob 主要用于 文件操作,如文件查找、筛选

七、 CSS新魔法color()已经被所有主流引擎支持

image.png

7.1 color()介绍

color() MDN 链接

color()是 CSS 中一个相对新的颜色函数,它提供了一种统一的方式来指定任何 RGB 颜色空间中的颜色值。与rgb()hsl()等函数相比,color()函数的优点是:

  1. 统一的语法可以访问不同颜色空间,更简洁。
  2. 未来如果有新的标准色域,只需要在函数中加入新值,就可以兼容,无需新增函数。
  3. 有助于实现色彩管理,由浏览器根据设备色域进行颜色转换。

7.2 color()语法

color()函数的语法如下:

color(display-p3 1 0.5 0);
color(display-p3 1 0.5 0 / .5);

color()的值有下面几个

  • colorspace 命名空间

    比如:srgb, srgb-linear, display-p3, a98-rgb, prophoto-rgb, rec2020, xyz, xyz-d50, and xyz-d65.

  • p1, p2, p3

    数字或者百分比 供颜色空间所使用的参数值

  • A 可选 alpha 值

其中color-space可以是:

  • srgb:标准 RGB 色域

  • display-p3:广色域,用于电影和电视

  • a98-rgb:Adobe RGB 色域

  • prophoto-rgb:ProPhoto RGB 色域

  • rec2020:UHDTV 和其他广色域格式,例如:

    color(srgb 0 0 1) // sRGB 色值为 0, 0, 1

    color(display-p3 0 0 1) // Display P3 色域中的蓝色

7.3 color()使用例子

color()函数可以用于 CSS 中的任何需要颜色值的地方。这里给出一些例子: 文本颜色:

.text {
  color: color(display-p3 0 0 1);
}

背景颜色:

.bg {
  background-color: color(prophoto-rgb 0 1 0);
}

渐变色:

.gradient {
  background: linear-gradient(
    to right,
    color(display-p3 0 0 1),
    color(rec2020 0 1 0)
  );
}

填充 SVG 图形:

.icon {
  fill: color(a98-rgb 1 0 0);
}

7.4 什么网站或应用需要用到color()

color()函数主要适用于以下类型的网站和应用:

  1. 需要广色域和高清颜色的网站:视频网站、产品展示网站、高清显示网站等。

  2. 需要色彩管理的网站:色彩管理网站和需要跨设备色彩一致的网站。

  3. 创意和艺术类网站:设计师可以发挥更大创意,与其他 CSS 技术结合可以创作很美的视觉效果。

  4. 未来的 HDR 显示网站:color()为网站采用更宽色域和 HDR 做好了准备。 所以,总体来说,color()为色彩敏感和未来潮流的网站带来许多好处,是 CSS 中一个强大的颜色功能

八、 一个神奇的小工具,让URL地址都变成了"ooooooooo"

8.1 前置知识点

8.11 将字符转为utf8数组,转换后的每个字符都有一个特定的唯一数值,比如 http 转换后的 utf8 格式数组即是 [104, 116, 116, 112]

    toUTF8Array(str) {
        var utf8 = [];
        for (var i = 0; i < str.length; i++) {
            var charcode = str.charCodeAt(i);
            if (charcode < 0x80) utf8.push(charcode);
            else if (charcode < 0x800) {
                utf8.push(0xc0 | (charcode >> 6),
                    0x80 | (charcode & 0x3f));
            }
            else if (charcode < 0xd800 || charcode >= 0xe000) {
                utf8.push(0xe0 | (charcode >> 12),
                    0x80 | ((charcode >> 6) & 0x3f),
                    0x80 | (charcode & 0x3f));
            }
            else {
                i++;
                charcode = ((charcode & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff)
                utf8.push(0xf0 | (charcode >> 18),
                    0x80 | ((charcode >> 12) & 0x3f),
                    0x80 | ((charcode >> 6) & 0x3f),
                    0x80 | (charcode & 0x3f));
            }
        }
        console.log(utf8, 'utf8');
        return utf8;
    }

8.12 上面是编码,对应下面的则是解码,将utf8数组转换为字符串,比如 [99, 111, 109] 转换后的 utf8 格式数组即是 com

    Utf8ArrayToStr(array) {
        var out, i, len, c;
        var char2, char3;

        out = "";
        len = array.length;
        i = 0;
        while (i < len) {
            c = array[i++];
            switch (c >> 4) {
                case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
                    // 0xxxxxxx
                    out += String.fromCharCode(c);
                    break;
                case 12: case 13:
                    // 110x xxxx   10xx xxxx
                    char2 = array[i++];
                    out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F));
                    break;
                case 14:
                    // 1110 xxxx  10xx xxxx  10xx xxxx
                    char2 = array[i++];
                    char3 = array[i++];
                    out += String.fromCharCode(((c & 0x0F) << 12) |
                        ((char2 & 0x3F) << 6) |
                        ((char3 & 0x3F) << 0));
                    break;
            }
        }

        return out;
    }

8.13 将 Number 对象以 4 进制的形式表示为字符串,toString 用的比较多,但是里面传入参数的场景比较少,这个参数 radix 是一个可选的参数,用于指定转换的进制数,范围为 2 ~ 36,如果未传入该参数,则默认使用 10 进制。

n.toString(4)

8.14 在字符串左侧填充指定字符,直到字符串达到指定长度。基本语法为 str.padStart(targetLength [, padString])

  • targetLength:必需,指定期望字符串的最小长度,如果当前字符串小于这个长度,则会在左侧使用 padString 进行填充,直到字符串达到指定长度。
  • padString:可选,指定用于填充字符串的字符,默认为 " "(空格)。
str.padStart(4, '0')

8.2 URL 编码/解码

下面正式开始URL编码的逻辑,核心的逻辑如下:

  • 转换为utf8数组
  • 转换为4进制并左侧补0到4位数
  • 分割转换为字符串数组
  • 映射到o的不同形式
  • 再次拼接为字符串,即转换完成后的URL
// 获取utf8数组
let unversioned = this.toUTF8Array(url)
    // 转换为base 4字符串
    // padstart非常重要!否则会丢失前导0
    .map(n => n.toString(4).padStart(4, "0"))
    // 转换为字符数组
    .join("").split("")
    // 映射到o的不同形式
    .map(x => this.enc[parseInt(x)])
    // 连接成单个字符串
    .join("")

上面有两个关键点解释一下,首先映射到o的不同形式这个是什么意思呢?其实转换后的o并不是一种“o”,而是4种,只不过我们肉眼看到的效果很像,通过 encodeURI 转换后的字符可以看出来。

encodeURI('o-ο-о-ᴏ')
// o-%CE%BF-%D0%BE-%E1%B4%8F

这里其实也解释了为什么上面为什么是转换为4进制和左侧补0到四位数。因为上面代码定义的 this.enc 如下,因为总共只有四种“o”,4进制只会产生0,1,2,3,这样就可以将转换后的utf8字符一一对应上这几种特殊的“o”。

enc = ["o", "ο", "о", "ᴏ"] 

最后的效果举例转换 http 这个字符:

  • 转换为utf8数组:[ 104, 116, 116, 112 ]
  • 转换为4进制并左侧补0到4位数:['1220', '1310', '1310', '1300']
  • 分割转换为字符串数组:['1', '2', '2', '0', '1', '3', '1', '0', '1', '3', '1', '0', '1', '3', '0', '0']
  • 映射到o的不同形式:[ 'ο', 'о', 'о', 'o', 'ο', 'ᴏ', 'ο', 'o', 'ο', 'ᴏ', 'ο', 'o', 'ο', 'ᴏ', 'o', 'o' ]
  • 再次拼接为字符串,即转换完成后的URL:οооoοᴏοoοᴏοoοᴏoo

到此整个转换编码的过程就结束了,看完后是不是觉得设计的很不错,编码完后就是解码,解码就是将上面的过程倒序来一遍,恢复到最原始的URL地址。这里要注意一点的是每次解析4个字符且parseInt以4进制的方式进行解析。

// 获取url的base 4字符串表示
let b4str = ooo.split("").map(x => this.dec[x]).join("")

let utf8arr = []
// 每次解析4个字符
// 记住添加前导0的填充
for (let i = 0; i < b4str.length; i += 4)
    utf8arr.push(parseInt(b4str.substring(i, i + 4), 4))
// 返回解码后的字符串
return this.Utf8ArrayToStr(utf8arr) 

九、 nodejs对项目压力测试

9.1 工具

autocannon 中文译为 自动炮, 是一款基于nodejs的压力测试工具,支持命令行写代码来进行测试

npm 地址 www.npmjs.com/package/aut…

因为是前端所有选择了这个。  其他诸如 ab、webbench均可。

9.2 压力测试

9.21 安装

全局安装autocannon

npm i autocannon -g

9.22 命令行使用

9.221 执行

autocannon -c 100 -d 5 -p 1 https://appvxnsma4r5053.pc.xiaoe-tech.com/

9.222 命令解释

上述命令解释: 用100个连接、持续5秒去访问 https://appvxnsma4r5053.pc.xiaoe-tech.com/

9.223 执行示例截图

image.png

9.224 参数释义

  • -c/--connections NUM 并发连接的数量,默认10
  • -p/--pipelining NUM 每个连接的流水线请求请求数。默认1
  • -d/--duration SEC 执行的时间,单位秒
  • -m/--method METHOD 请求类型 默认GET
  • -b/--body BODY 请求报文体
  • -H/--header 请求头

9.23 写代码压测

执行复杂一点的压力测试,建议写脚本.

例如

9.231 对后端接口进行压力测试

举例一个场景, 比如想对某新增接口的短时间内大量 新增测试。

9.2311 创建一个js文件 例如
touch app.js
9.2312 从调试工具network复制fetch

image.png

9.2313 书写如下

将上一步复制出来的信息,悉数填在下面。

const autocannon = require('autocannon')
autocannon({
    url: '你的接口',
    "headers": {
        "accept": "application/json, text/plain, */*",
        "accept-language": "zh-CN,zh;q=0.9",
        "authorization": "",
        "cookie": "acw_tc=0bca324216820466206848044ebf9191e5a0e4b89a4e9bc8b18e333d13f537",
        "Referrer-Policy": "strict-origin-when-cross-origin"
    },
    "body": "你的参数",
    "method": "POST", // 你接口的methods get / post
    connections: 10, // 连接数
    pipelining: 1, // 流水线数量
    duration: 10 // 持续时间
}, console.log)
9.2314 执行测试
node app.js

执行后发现压力测试执行了三千多次,创建了三千多个任务, 后端接口直接响应变成了30秒。 如此就找到了一个问题。

9.3 如何定时执行?

假如你想每天指定时间 来对自己的项目进行压力测试.

那么你可以把上述逻辑写成一个方法 参照这篇juejin.cn/post/716360… 定时去执行它, 这样就可以愉快的定时压测自己项目了。

十、 calc()计算函数

10.1 cale()是什么

calc是英文单词calculate的缩写,其实主要用于对项目中一些单位的计算,这里的计算是在calc()函数里面填充表达式去计算,它会返回具体的值

10.2 基本使用

项目中遇到最多的就是一些普通计算了比如:

<body>
    <div class="content"></div>
</body>
// style
 .content{
   height: calc(100% - 32px);
 }

这里计算出来的就是页面减去32像素单位的高度,如果父盒子有高度,这个100%就是继承父盒子的高度

<div style="height: 200px">
    <div class="content"></div>
</div>
// style
.content {
  background-color: #6b3434;
  height: calc(100% - 100px);
}

除了加减法calc()还支持乘除,乘除法要求必须要有一个参数是数字,而且除法的右边参数必须是数字;加、减、乘、除('+'、'-'、'*'、'/'、) ,乘除法的运算规则也是一样的会先运算乘除再算加减,所以我们不需要刻意的使用括号去包裹,但是如果你是需要先运算加减则要先用括号进行包裹

10.3 灵活使用

calc()除了这些用法还有一些比较灵活用法:

// 计算属性里面
newWidth:{
// data里设置一个menuWidth变量来操控width的大小
    return width: `calc(100% - ${menuWidth}px)`
}

还有比如我这篇旋转方块里面的每一个点的旋转用到的就是css中的自定义属性来运算,运用style中的--i属性来控制变量,从而减少我们大量的css去计算的代码

transform: rotate(calc(30deg * var(--i)));

而且calc()还支持混合单位运算,在参数单位不同时,会做预处理比如:

// turn 代表一个圆的圈,1turn就是一圈
// deg 代表角度45deg就是45度
transform: rotate(calc(1turn + 45deg))

而且calc()支持很多单位:px,%,em,rem,vw,vh,cm,pt,pc,vmin,vmax,vh

10.4 注意

calc()表达式的参数一定要用空格隔开,并且也是支持负数计算的,而且calc()不支持媒体查询哦!!

// 本身写法
@media (max-width: 600px) {
}
// 不支持的calc写法
@media (min-width: calc(400px + 200px)) {

}

10.5 兼容性

image.png

10.6 结尾

项目中使用的less预处理器,在使用calc()时会和less的一些语法有冲突,所以官方推荐使用 '~' 来进行转译这是官方文档

参考文献

h5页面在浏览器上好好的,到手机上熄火了又看不到报错信息怎么办? 前端人必须掌握的抓包技能 移动端调试痛点?——送你五款前端开发利器