Error 对象

JavaScript 解析或运行时,一旦发生错误,引擎就会抛出一个错误对象。JavaScript 原生提供 Error 构造函数,所有抛出的错误都是这个构造函数的实例。

属性

ECMA-262 规定了 Error 对象包括两个属性:message 和 name。message 保存错误的信息,而 name 保存错误类型。浏览器厂商还对 error 对象属性做了扩展,添加了其他相关信息。其中实现最广泛的是 stack 属性,表示错误的栈信息。

可以使用 Error 构造函数来创建错误对象,如果指定 message 参数,则该 Error 对象将把它用作 message 属性;若不指定,它将使用空字符串作为 message 属性。

当不使用 new 操作符,直接使用 Error 构造函数像一个普通函数一样调用时,它的行为与使用 new 操作符时一致。

方法

Error 对象还有一个 toString 方法,该方法覆盖了对象原型上的 toString 方法,返回代表对象类型与信息的字符串。

Read more »

问题背景

使用 webpack 开发一个库后,想在未发布的条件下,对库进行测试,通常会使用npm link命令,该命令会将当前目录链接至用户文件夹下的 npm 包公用目录,在要引入包进行测试的项目中再使用npm link 你已经链接至公用目录的包名,就可以将库从公用目录再链接到测试的项目中,这就等同于使用 npm 直接安装包了。但是使用中发现还是有问题,使用npm link引入的包,在项目中 import 后会报错,但是同样的文件发布到 npm 后再 import 就没有问题。

解决方案

在测试项目中将 webpack 的符号链接配置为 false 即可,即可正确 import 要测试的库了

// webpack配置
module.exports = {
  // ...
  resolve: {
    symlinks: false,
  },
};
Read more »

圆形布局-应用场景

在使用 echarts 关系图 graph 时,若要使用圆形或引力自动布局坐标系必须为 null;想要使关系图连线有动态效果,又需要使用动态的路线图 lines,lines 必需使用 cartesian2d 或 geo 的坐标系;因此为了实现动效,只能放弃自动圆形布局,手动计算出一个圆形布局的坐标。

圆形布局-代码示例

/**
 * 根据中心位置坐标、布局半径、布局数量及布局角度范围,按圆平均计算布局位置
 * @param {array} centerPosition 中心位置数组
 * @param {number} r 布局半径
 * @param {number} number 布局数量
 * @param {number} startAngle 布局起始角度,默认为0
 * @param {number} endAngle 布局结束角度,默认为360
 * @returns 生成的布局位置数组
 */
export function calcLayoutCircular(centerPosition, r, number, startAngle = 0, endAngle = 360) {
  /**
   * 将传入的任意角度转换为0-360度的角度
   * @param {number}} angle 传入角度
   * @returns 转换后的角度
   */
  function normalizeAngle(angle) {
    // 任意角度相对360度取余数
    const normalizedAngle = angle % 360;
    // 若为负角度则加上360度,转换为正角度
    return normalizedAngle < 0 ? normalizedAngle + 360 : normalizedAngle;
  }
  startAngle = normalizeAngle(startAngle);
  endAngle = normalizeAngle(endAngle);
  let intervalAngle;
  // 计算间隔角度
  // 若角度范围是完整圆周,起始角度与结束角度只占用一个布局数量,按布局数量平均分配角度即可;
  // 若角度范围不是完整圆周,则起始角度与结束角度会占用两个布局数量,需要以 number - 1 计算角度间隔,若number为1时不减1
  if (endAngle - startAngle === 0) {
    intervalAngle = 360 / number;
  } else {
    intervalAngle = normalizeAngle(endAngle - startAngle) / (number - 1 || 1);
  }
  // 根据布局数量与间隔角度计算所有布局位置
  const positionArr = [];
  for (let i = 0; i < number; i++) {
    let angle = normalizeAngle(startAngle + intervalAngle * i);
    const x = centerPosition[0] + r * Math.sin((angle * Math.PI) / 180);
    const y = centerPosition[1] + r * Math.cos((angle * Math.PI) / 180);
    positionArr.push({
      angle,
      position: [x, y],
    });
  }
  return positionArr;
}

标签位置-应用场景

使用圆形布局后,当元素标签不放在元素上居中显示,而要根据相对圆心的位置,显示在上、右、下、左等不同方位时,使用此方法计算。

标签位置-代码示例

/**
 * 根据传入的角度(当前元素相对圆心的角度)计算出标签位置,简单模式只计算左右,非简单模式计算上下左右
 * @param {number} angle 传入的角度
 * @param {boolean} simple 是否简单模式
 * @returns 位置字符串
 */
export function calcLabelPositionByAngle(angle, simple = true) {
  if (simple) {
    if (angle >= 0 && angle < 180) {
      return "right";
    } else {
      return "left";
    }
  } else {
    if (angle > 275 || angle <= 45) {
      return "top";
    } else if (angle > 45 && angle <= 135) {
      return "right";
    } else if (angle > 135 && angle <= 215) {
      return "bottom";
    } else if (angle > 215 && angle <= 275) {
      return "left";
    } else {
      return "top";
    }
  }
}

less 实现主题切换的语法

// 设置主题模板函数
.common-theme(@backgroundColor, @color) {
  .class__A {
    background-color: @backgroundColor;
    color: @color;
  }
}

// 实现主题-亮色
.theme-light {
  .common-theme(#fff, #d9d9d9);
}
// 实现主题-暗色
.theme-dark {
  .common-theme(#000, #fff);
}

scss 实现主题切换的语法

一、遍历配置

// 主题配置
$theme-config: (
  light: (
    backgroundColor: #fff; color: #d9d9d9;,
  ),
  dark: (
    backgroundColor: #000; color: #fff;,
  ),
);
@each $theme, $config in $theme-config {
  // 属性名使用变量时需要使用#{}包裹
  .theme-#{$theme} {
    .class__A {
      background-color: map-get($config, "backgroundColor");
      color: map-get($config, "color");
    }
  }
}

二、mixin

@mixin common-theme($backgroundColor, $color) {
  .class__A {
    background-color: $backgroundColor;
    color: $color;
  }
}
// 亮色
.theme-light {
  @include common-theme(#fff, #d9d9d9);
}
// 暗色
.theme-dark {
  @include common-theme(#000, #fff);
}

参考资料-less、scss 主题切换

应用场景

后端提供了下载单个文件的接口,返回数据类型为 blob,现在要在没有额外后端接口的条件下,前端批量下载多个文件并打包生成 zip,供客户下载。

需要安装的依赖与需要前置了解的知识

  • 需要用到 jszip 库,安装npm install jszip

  • 在 for 循环中使用异步,使所有请求并发,并在最终等所有请求结束后遍历(了解 Promise.all、Promise.allSettled、for await of)

示例

// 引入jszip库
import jszip from "jszip";

const downloadAndZipAll = async () => {
  // 创建a标签,用于最终的zip下载
  const ele = document.createElement("a");
  // 新建zip实例
  const zip = new jszip();
  // 根据所有文件id请求所有文件blob
  // selectedDocument为选中的多个要下载的文件对象
  const allPromise = selectedDocuments.map((document) => {
    // downloadDocument为根据文件id请求blob数据的Promise函数
    return downloadDocument(document.document_id)
      .then((res) => {
        const blob: Blob = new Blob([res], {
          // 定义blob的类型,这里为docx类型
          type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
        });
        // 返回包含文档信息与blob的新对象,文档信息用于后面为文档命名
        return { blob, ...document };
        // 若请求报错则返回null,用于将文件载入zip时,防止某一请求报错而导致无法生成zip
      })
      .catch(() => null);
  });
  // ES8的for await语法,需要在async函数中使用,用于待所有请求有结果后,遍历放入zip文件中
  for await (const item of allPromise) {
    // 若请求未报错,则返回值不为null,可将文档放入zip包
    if (item)
      // 将文件放入zip包,第一个参数为文件名,第二个参数为文档的blob数据
      zip.file(
        `${dayjs(item.make_time, "YYYY-MM-DD HH:mm").format("YYYY-MM-DD")}_${item.product_name}.docx`,
        item.blob
      );
  }
  // 除了使用for await语法外还可以使用Promise.all或Promise.allSettled
  // Promise.all若有一个请求报错,则Promise.all被中断,进入catch中,但若是在每一个单一请求中已做过错误处理,则不会进入catch
  // Promise.allSettled不论请求是否报错,只要响应了,就会返回
  for (const item of await Promise.all(allPromise)) {
    // 若请求未报错,则返回值不为null,可将文档放入zip包
    if (item)
      // 将文件放入zip包,第一个参数为文件名,第二个参数为文档的blob数据
      zip.file(
        `${dayjs(item.make_time, "YYYY-MM-DD HH:mm").format("YYYY-MM-DD")}_${item.product_name}.docx`,
        item.blob
      );
  }
  // await所有文件放入zip包后,生成zip的blob,供用户下载
  zip.generateAsync({ type: "blob" }).then((content) => {
    // 使用blob创建访问链接
    ele.href = URL.createObjectURL(content);
    // 模拟鼠标点击
    ele.click();
    // 释放访问链接
    URL.revokeObjectURL(ele.href);
  });
};

参考资料

Promise MDN

Promise.all 错误处理

纯前端 JSzip 打包文件并下载-张鑫旭

JSzip 官方文档 API

Using async/await with a forEach loop

for await of VS Promise.all

遇到 npm 执行命令npm install时报错,ENOENT:No Such Files Or Directories…。删除 node_modules 目录也无法修复。

ENOENT

ENOENT 是 Error NO ENTry(或 Error NO ENTity)的缩写,之所以这样缩写是因为 C 的编译器最初不支持超过 8 个字符的符号。

解决方案

可以同时删除 package-lock.json 与 node_modules 目录,再执行npm install

参考资料 1

参考资料 2

应用场景

在做可复用组件时,我们常常需要给函数组件传递 ref 属性,以访问或操作组件内部的 DOM 或向外暴露方法等。

import { useRef } from "react";

// 组件
function Component() {
  return <input />;
}

// App
function App() {
  const textInputRef = useRef();
  return <Component ref={textInputRef} />; // 这是无效的
}

在 jsx 下使用

默认情况下,我们不能在函数组件上使用 ref 属性,因为它们没有实例。

如果要在函数组件中使用 ref,可以使用 forwardRef 包裹组件函数使用(可与 useImperativeHandle 结合使用)。

被 forwardRef 包裹的组件函数除 props,还要多传入第二个参数:ref,即从外部传入的 ref。

useImperativeHandle 接收三个参数,第一个是 forwardRef 传入的 ref;第二个参数是 handle 函数,返回要向外暴露的值,通常是对象;第三个参数是依赖数组,根据依赖变化执行更新,非必需参数。

import { useRef, forwardRef, useImperativeHandle, useEffect } from "react";

// 组件1
const Component1 = forwardRef((props, ref) => <input ref={ref} />);
// 组件2
const Component2 = forwardRef((props, ref) => {
  // ref 为第一个参数,返回带有 sayHello 方法对象的函数为第二个参数
  useImperativeHandle(ref, () => {
    return {
      sayHello: () => console.log("hello"),
    };
  });
  return <div>Say hello in console.log</div>;
});

// App
function App() {
  const textInputRef = useRef();
  const helloRef = useRef();
  useEffect(() => {
    // 聚焦 textInputRef
    textInputRef.current.focus();
    // 调用 helloRef 的 sayHello 方法
    helloRef.current.sayHello();
  }, []);
  return (
    <>
      <Component1 ref={textInputRef} />
      <Component2 ref={helloRef} />
    </>
  );
}

在 tsx 下使用

以上是在 React 中使用 forwardRef 的情况,那么结合了 Typescript,又会发生很多的类型问题,如何在 TS 中使用 forwardRef 呢?

import { useRef, forwardRef, Ref, useImperativeHandle, ElementRef, useEffect } from "react";

// 组件,有两种定义类型的方式

// 组件1,一种是使用 forwardRef 的泛型
// forwardRef 泛型第一个参数是 Ref 类型,
// 第二个参数是 props 类型,不传时默认类型为{},
// 注意,forwardRef 泛型与内部包裹函数的参数顺序恰恰相反,易造成歧义
const Component1 = forwardRef<HTMLInputElement, {}>((props, ref) => <input ref={ref} />);

// 组件2,另一种是是在函数参数上直接定义类型
// 注意,在函数参数上定义类型时,ref 参数类型需要使用Ref泛型包裹,而 forwardRef 泛型则不需要
const Component2 = forwardRef(
  // ref 类型使用了 Ref 泛型包裹
  (props: {}, ref: Ref<{ sayHello: () => void }>) => {
    // ref 为第一个参数,返回带有 sayHello 方法对象的函数为第二个参数
    useImperativeHandle(ref, () => {
      return {
        sayHello: () => console.log("hello"),
      };
    });
    return <div>Say hello in console.log</div>;
  }
);

// App
export default function App() {
  // 在父组件中使用时需要使用 ElementRef 泛型,并使用 typeof 获取组件的 ref 类型
  const textInputRef = useRef<ElementRef<typeof Component1>>(null);
  const helloRef = useRef<ElementRef<typeof Component2>>(null);
  useEffect(() => {
    // 聚焦textInputRef
    if (textInputRef.current) textInputRef.current.focus();
    // 调用helloRef的sayHello方法
    if (helloRef.current) helloRef.current.sayHello();
  }, []);
  return (
    <>
      <Component1 ref={textInputRef} />
      <Component2 ref={helloRef} />
    </>
  );
}

可以看到,有两种方式定义 forwardRef 组件的类型。

  • 一种是在 forwardRef 泛型中定义 ref 类型与 props 类型,需要注意的是泛型中是 ref 类型在前,props 类型在后,而函数组件中是 props 在前,ref 在后。

  • 另一种则是直接在函数组件的参数中定义类型,需要注意的是这种方式定义 ref 类型需要使用 Ref 泛型。

另外在父组件中,使用 useRef 时,则需要通过 ElementRef 泛型与 typeof 结合获取 ref 类型。

forwardRef 泛型中,若组件没有用到 props,则 props 类型可不传,默认为{}。示例如下:

const Component = forwardRef<HTMLInputElement>((props, ref) => <input ref={ref}>);

参考资料

declare type with React.useImperativeHandle()

@types/react - github

Forwarding React Refs with TypeScript

react 白屏错误,报 failed to execute “insertBefore” on “node”

最近遇到个别客户在 Apple Chrome 环境下使用页面时,页面加载就白屏,报错 failed to execute “insertBefore” on “node”。

应急的解决办法:对报错页面关闭谷歌翻译功能

Read more »

在 React 框架中使用 Typescript 时,在遇到 DOM 事件时,总会遇到类型问题,最常见的就是 e.target.value 不存在。这时候就需要在回调函数中显式地声明事件类型。

<Input
  onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value);
  }}
/>

参考资料

0%