Commit e82e89e8 authored by wangxuxiao's avatar wangxuxiao
parents b06306b7 9ec353cc
---
title: Map和Object
date: 2022/4/26 10:00:00
tags:
- map
- object
author: 张芳利
---
# Map和Object
在 JavaScript 中,普通对象和 ES6 的新对象 Map 都可以存储键值对,平时普通对象用的较多,现在着重了解一下Map
<a name="AZnLW"></a>
## 描述
Map 对象存有键值对,其中的键可以是任何数据类型。<br />Map 对象记得键的原始插入顺序。<br />Map 对象具有表示映射大小的属性。
<a name="g2duN"></a>
<!-- more -->
## 创建
构造函数创建
```javascript
new Map([iterable])
```
Iterable 可以是一个数组或者其他 iterable 对象,其元素为键值对(两个元素的数组,例如: [[ 1, 'one' ],[ 2, 'two' ]])。 每个键值对都会添加到新的 Map。null 会被当做 undefined。
<a name="d1eEZ"></a>
## 方法和属性
```javascript
let map = new Map()
```
打印map可得<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/22328960/1650869031897-18b102f5-2bf1-40b1-ae59-50055cfeae95.png#clientId=u6819a0e1-7327-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=343&id=ubcd9b3cb&margin=%5Bobject%20Object%5D&name=image.png&originHeight=309&originWidth=481&originalType=binary&ratio=1&rotation=0&showTitle=false&size=21436&status=done&style=shadow&taskId=ubf631d28-9c30-4623-a110-07aaeb60424&title=&width=534.4444586023876)
```javascript
Map(0) {size: 0}
[[Entries]]
No properties
size: 0
[[Prototype]]: Map
clear: ƒ clear()
constructor: ƒ Map()
delete: ƒ delete()
entries: ƒ entries()
forEach: ƒ forEach()
get: ƒ ()
has: ƒ has()
keys: ƒ keys()
set: ƒ ()
size: 0
values: ƒ values()
Symbol(Symbol.iterator): ƒ entries()
Symbol(Symbol.toStringTag): "Map"
get size: ƒ size()
[[Prototype]]: Object
```
<a name="HHGjW"></a>
### size
访问属性,用于返回 一个[Map](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Map) 对象的成员数量。
```javascript
let map = new Map([
['name','wawa'],
['age',18]
])
console.log(map.size);//2
```
<a name="gqCWw"></a>
### has()
返回一个bool值,用来表明map 中是否存在指定元素,返回布尔值
```javascript
let map = new Map([
['name','wawa'],
['age',18]
])
console.log(map.has('name'));// true
console.log(map.has('hahahah') //false
```
<a name="Jqcus"></a>
### set()
为 Map 对象添加或更新一个指定了键(key)和值(value)的(新)键值对
```javascript
let map = new Map([
['name','wawa'],
['age',18]
])
map.set('bar', 'foo');
console.log(map); //Map(3) {'name' => 'wawa', 'age' => 18, 'bar' => 'foo'}
```
<a name="LXC1g"></a>
### get()
返回某个 Map 对象中的一个指定元素。
```javascript
let map = new Map([
['name','wawa'],
['age',18]
])
map.set('bar', 'foo');
console.log(map.get('name'));//wawa
```
<a name="EmioU"></a>
### keys()
返回一个引用的 **Iterator** 对象。它包含按照顺序插入 Map 对象中每个元素的key值
```javascript
let map = new Map([
['name','wawa'],
['age',18]
])
map.set('bar', 'foo');
let keys = map.keys()
console.log(keys); //MapIterator {'name', 'age', 'bar'}
for(let k of keys){
console.log(k); // name age bar
}
```
<a name="iIwd6"></a>
### values()
返回一个新的Iterator对象。它包含按顺序插入Map对象中每个元素的value值。
```javascript
let map = new Map([
['name','wawa'],
['age',18]
])
map.set('bar', 'foo');
let values = map.values()
console.log(values); //MapIterator {'wawa', 18, 'foo'}
for(let k of values){
console.log(k);
}
```
<a name="DkShq"></a>
### entries()
返回一个新的包含 [key, value] 对的 **Iterator** 对象,返回的迭代器的迭代顺序与 Map 对象的插入顺序相同。
```javascript
let map = new Map([
['name','wawa'],
['age',18]
])
map.set('bar', 'foo');
let entries = map.entries()
console.log(entries); //MapIterator {'name' => 'wawa', 'age' => 18, 'bar' => 'foo'}
console.log(entries.next().value); //(2) ['name', 'wawa']
console.log(entries.next().value); //(2)(2) ['age', 18]
console.log(entries.next().value); //(2) (2) ['bar', 'foo']
```
- next() 是**Iterator** 对象的方法
<a name="C2l2a"></a>
### clear()
移除Map对象中的所有元素
```javascript
let map = new Map([
['name','wawa'],
['age',18]
])
map.set('bar', 'foo');
map.clear()
console.log(map.size);//0
```
<a name="bkyEw"></a>
### delete()
移除 Map 对象中指定的元素,返回布尔值
```javascript
let map = new Map([
['name','wawa'],
['age',18]
])
map.set('bar', 'foo');
console.log(map.delete('name'));//true
console.log(map.delete('namess'));//false
```
<a name="mTQEz"></a>
### forEach()
按照插入顺序依次对 Map 中每个键/值对执行一次给定的函数
```javascript
let map = new Map([
['name','wawa'],
['age',18]
])
map.set('bar', 'foo');
map.forEach((value,key,map)=>{
console.log(`value:${value},key:${key}`);
})
//value:wawa,key:name value:18,key:age value:foo,key:bar
```
<a name="rynQ5"></a>
### for...of...
Map可以使用for..of循环来实现迭代
```javascript
let map = new Map([
['name','wawa'],
['age',18]
])
map.set('bar', 'foo');
for(let m of map){
console.log(m);
}
//(2) ['name', 'wawa'] ['age', 18] ['bar', 'foo']
```
<a name="Nfw1C"></a>
### 利用散布运算符...遍历集合
```javascript
let map = new Map([
['name','wawa'],
['age',18]
])
map.set('bar', 'foo');
let keys = map.keys()
console.log(keys); //MapIterator {'name', 'age', 'bar'}
console.log(...keys); // 'name', 'age', 'bar'
console.log(...map) // (2) ['name', 'wawa']['age', 18] ['bar', 'foo']
```
<a name="KrSzR"></a>
## 和普通对象的区别
Objects 和 Maps 类似的是,它们都允许你按键存取一个值、删除键、检测一个键是否绑定了值。<br />不同的是:
<a name="McD5X"></a>
### 1. 默认值
1. Map 默认情况不包含任何键。只包含显式插入的键。
1. 一个 Object 有一个原型, 原型链上的键名有可能和你自己在对象上的设置的键名产生冲突。
<a name="PK2H7"></a>
### 2. 键类型
1. 一个Object 的键必须是一个 [String](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String) 或是[Symbol](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Symbol)
1. 一个 Map的键可以是任意值,包括函数、对象或任意基本类型。
<a name="UavZb"></a>
### 3. 键顺序
1. 一个 Object 的键是无序的
1. Map 中的 key 是有序的。因此,当迭代的时候,一个 Map 对象以插入的顺序返回键值。
<a name="ei7Uf"></a>
### 4. Size
1. Object 的键值对个数只能手动计算
1. Map 的键值对个数可以轻易地通过[size](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Map/size) 属性获取
<a name="qowlQ"></a>
### 5. 迭代
1. 迭代一个Object需要以某种方式获取它的键然后才能迭代
1. 使用 for...of 语句或 Map.prototype.forEach 直接迭代 Map 的属性
<a name="lmBoK"></a>
### 序列化和解析
1. 普通对象支持 JSON 序列化
1. Map 默认无法获取正确数据
<a name="ANmN0"></a>
### 性能
1. 在频繁增删键值对的场景下表现更好
1. 在频繁添加和删除键值对的场景下未作出优化
<a name="j3rp8"></a>
## 迭代协议:
迭代协议具体分为两个协议:可迭代协议和 迭代器协议。<br />**可迭代协议**:允许 JavaScript 对象定义或定制它们的迭代行为,例如,在一个 for..of 结构中,哪些值可以被遍历到。一些内置类型同时是内置可迭代对象,并且有默认的迭代行为,比如 Array 或者 Map,而其他内置类型则不是(比如 Object))<br />要成为**可迭代**对象, 一个对象必须实现 **@@iterator** 方法。这意味着对象(或者它[原型链](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain)上的某个对象)必须有一个键为 @@iterator 的属性,可通过常量 [Symbol.iterator](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Symbol/iterator) 访问该属性:
| 属性 | 值 |
| --- | --- |
| [Symbol.iterator] | 一个无参数的函数,其返回值为一个符合[迭代器协议](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Iteration_protocols#%E8%BF%AD%E4%BB%A3%E5%99%A8%E5%8D%8F%E8%AE%AE)的对象。 |
**迭代器协议:**定义了产生一系列值(无论是有限个还是无限个)的标准方式。当值为有限个时,所有的值都被迭代完毕后,则会返回一个默认返回值<br />只有实现了一个拥有以下语义(semantic)的 **next()** 方法,一个对象才能成为迭代器:
| 属性 | 值 |
| --- | --- |
| **next** | 一个无参数或者一个参数的函数,返回一个应当拥有以下两个属性的对象:<br />**done(boolean)**<br />如果迭代器可以产生序列中的下一个值,则为 false。<br />如果迭代器已将序列迭代完毕,则为 true。<br />这种情况下,value 是可选的,如果它依然存在,即为迭代结束之后的默认返回值。<br />**next()** 方法必须返回一个对象,<br />该对象应当有两个属性: done 和 value,如果返回了一个非对象值(比如 false 或 undefined),<br />则会抛出一个 [TypeError](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/TypeError) 异常("iterator.next() returned a non-object value")。 |
Iterator 迭代器是一种接口,为不同的数据结构提供统一的访问机制,这个访问机制主要是遍历,我们知道,在数组、在对象、在类数组、在map、在set里面,都可以用for of或者扩展运算符来得到一个数组或者是遍历当前的数据结构,为什么能够遍历的原因就是因为存在这个Iterator 迭代器这个东西,所以我们可以用for of来提供一个统一的遍历,因此这个底层或者是Itrator它最初是为了for of而设计的。
为了给所有的[数据结构](https://so.csdn.net/so/search?q=%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84&spm=1001.2101.3001.7020)有一个统一的遍历接口,统一的访问机制,因此就产生了迭代器。<br />迭代器的特点:
- 按某种次序排列数据结构
- 为for of提供一个遍历支持
- 为不同的数据结构提供统一的访问机制(目的)
目前所有的内置可迭代对象如下:[String](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String)[Array](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array)[TypedArray](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/TypedArray)[Map](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Map)[Set](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Set)等,它们的原型对象都实现了 @@iterator 方法。对象(Object)之所以没有默认部署Iterator接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。<br />例如:由上面的学习,我们知道map的values() 返回的就是一个Iterator对象
```javascript
let map = new Map([
['name','zz'],
['age',15]
])
console.log(map.values().next()); // {value: 'zz', done: false}
```
展开语法... :<br />其内部实现也使用了同样的迭代协议
```javascript
console.log(...'string') // s t r i n g
```
例如:[String](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String) 是一个内置的可迭代对象
```javascript
let str = 'string'
// 利用Symbol.iterator 属性获取一个迭代器对象
let it = str[Symbol.iterator]()
console.log(it); // StringIterator {}
console.log(it.next()); // {value: 's', done: false}
```
<a name="Wg0mh"></a>
### 问题:obj is not iterable
```javascript
let obj = {
name:'张小张',
age:18
}
console.log([...obj]); // Uncaught TypeError: obj is not iterable
```
怎么做好利用展开值且不报错??<br />根据[mdn](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Iteration_protocols#%E8%BF%AD%E4%BB%A3%E5%99%A8%E7%A4%BA%E4%BE%8B)迭代器的介绍:<br />给obj加上一个属性[Symbol.iterator],且返回next()
```javascript
obj[Symbol.iterator] = function() {
let nextIndex = 0;
return {
next: function () {
let array = Object.values(obj)
return nextIndex < array.length ? {
value: array[nextIndex++],
done: false
} : {
done: true
};
}
};
}
console.log([...obj]) // ['张小张', 18]
```
---
title: React中Hooks源码简单解析
date: 2022/4/30 09:00:00
updated: 2022/5/6 15:42:24
tags:
- 前端
- JavaScript
author: 方玉新
---
## 使用Hooks时的疑惑
Hooks的出现让我们对`Function Component`逐步拥有了对标 `Class Component`的特性,比如私有状态以及生命周期函数等。`useState``useReducer`这两个`Hooks`让我们可以在`Function Component`里使用到私有状态。而`useState`其实相当于简化版的`useReducer`,下面就将这两个放到一起简单介绍下。
<!-- more -->
#### 1、useState使用示例
```javascript
function PersionInfo ({initialAge,initialName}) {
const [age, setAge] = useState(initialAge);
const [name, setName] = useState(initialName);
return (
<>
Age: {age}, Name: {name}
<button onClick={() => setAge(age + 1)}>Growing up</button>
</>
);
}
```
> `useState`可以初始化一个私有状态,它会返回这个状态的最新值和一个用来更新状态的方法。而`useReducer`则是针对更复杂的状态管理场景:
#### 2、useReducer使用示例
```javascript
const initialState = {age: 0, name: 'Dan'};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {...state, age: state.age + action.age};
case 'decrement':
return {...state, age: state.age - action.age};
default:
throw new Error();
}
}
function PersionInfo() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Age: {state.age}, Name: {state.name}
<button onClick={() => dispatch({type: 'decrement', age: 1})}>-</button>
<button onClick={() => dispatch({type: 'increment', age: 1})}>+</button>
</>
);
}
```
> `useReducer`同样也是返回当前最新的状态,并返回一个用来更新数据的方法。
在使用这两个方法的时候也许我们会想过这样的问题:
```javascript
const [age, setAge] = useState(initialAge);
const [name, setName] = useState(initialName);
```
#### 问题1、React内部如何区分这两个状态
`Function Component`不像`Class Component`那样可以将私有状态挂载到类实例中并通过对应的`key`来指向对应的状态,而且每次的页面刷新或者组件的重新渲染都会使得`Function`重新执行一遍。所以在`React`中必定有一种机制来区分这些`Hooks`
```javascript
const [age, setAge] = useState(initialAge);
// 或
const [state, dispatch] = useReducer(reducer, initialState);
```
#### 问题2、React是如何在每次重新渲染之后都能返回最新的状态
`Class Component`因为自身的特点可以将私有状态持久化的挂载到类实例上,每时每刻保存的都是最新的值。而 `Function Component` 由于本质就是一个函数,并且每次渲染都会重新执行。所以`React`必定拥有某种机制去记住每一次的更新操作,并最终得出最新的值返回。当然还会有其他的一些问题,比如这些状态究竟存放在哪?为什么只能在函数顶层使用Hooks而不能在条件语句等里面使用Hooks?
### 源码中找出上面问题的答案
先来了解`useState`以及`useReducer`的源码实现,并从中解答我们在使用Hooks时的种种疑惑。首先我们从源头开始:
```javascript
import React, { useState } from 'react';
```
> 在项目中我们通常会以这种方式来引入`useState`方法,被我们引入的这个`useState`方法是什么样子的呢?其实这个方法就在源码 `packages/react/src/ReactHook.js` 中。
```javascript
// packages/react/src/ReactHook.js
import ReactCurrentDispatcher from './ReactCurrentDispatcher';
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
// ...
return dispatcher;
}
// 我们代码中引入的useState方法
export function useState(initialState) {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState)
}
```
从源码中可以看到,我们调用的其实是 `ReactCurrentDispatcher.js` 中的`dispatcher.useState()`,那么我们继续前往`ReactCurrentDispatcher.js`文件:
```javascript
import type { Dispacther } from 'react-reconciler/src/ReactFiberHooks';
const ReactCurrentDispatcher = {
current: (null: null | Dispatcher),
};
export default ReactCurrentDispatcher;
```
好吧,它继续将我们带向 `react-reconciler/src/ReactFiberHooks.js`这个文件。那么我们继续前往这个文件。
```javascript
// react-reconciler/src/ReactFiberHooks.js
export type Dispatcher = {
useState<S>(initialState: (() => S) | S): [S, Dispatch<BasicStateAction<S>>],
useReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: (I) => S,
): [S, Dispatch<A>],
useEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void,
// 其他hooks类型定义
}
```
兜兜转转我们终于清楚了`React Hooks` 的源码就放 `react-reconciler/src/ReactFiberHooks.js` 目录下面。在这里如上图所示我们可以看到有每个`Hooks`的类型定义。同时我们也可以看到`Hooks`的具体实现,大家可以多看看这个文件。首先我们注意到,我们大部分的`Hooks`都有两个定义:
```javascript
// react-reconciler/src/ReactFiberHooks.js
// Mount 阶段Hooks的定义
const HooksDispatcherOnMount: Dispatcher = {
useEffect: mountEffect,
useReducer: mountReducer,
useState: mountState,
// 其他Hooks
};
// Update阶段Hooks的定义
const HooksDispatcherOnUpdate: Dispatcher = {
useEffect: updateEffect,
useReducer: updateReducer,
useState: updateState,
// 其他Hooks
};
```
从这里可以看出,我们的`Hooks``Mount`阶段和`Update`阶段的逻辑是不一样的。在`Mount`阶段和`Update`阶段他们是两个不同的定义。我们先来看`Mount`阶段的逻辑。在看之前我们先思考一些问题。`React Hooks`需要在`Mount`阶段做什么呢?就拿我们的`useState``useReducer`来说:
* 我们需要初始化状态,并返回修改状态的方法,这是最基本的。
* 我们要区分管理每个Hooks。
* 提供一个数据结构去存放更新逻辑,以便后续每次更新可以拿到最新的值。
先看下mountState的实现。
```javascript
// react-reconciler/src/ReactFiberHooks.js
function mountState (initialState) {
// 获取当前的Hook节点,同时将当前Hook添加到Hook链表中
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
// 声明一个链表来存放更新
const queue = (hook.queue = {
last: null,
dispatch: null,
lastRenderedReducer,
lastRenderedState,
});
// 返回一个dispatch方法用来修改状态,并将此次更新添加update链表中
const dispatch = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
)));
// 返回当前状态和修改状态的方法
return [hook.memoizedState, dispatch];
}
```
#### 区分管理Hooks
关于初始化状态并返回状态和更新状态的方法。这个没有问题,源码也很清晰利用`initialState`来初始化状态,并且返回了状态和对应更新方法 `return [hook.memoizedState, dispatch]`。那么我们来看看`React`是如何区分不同的`Hooks`的,这里我们可以从 `mountState` 里的 `mountWorkInProgressHook`方法和`Hook`的类型定义中找到答案。
```javascript
// react-reconciler/src/ReactFiberHooks.js
export type Hook = {
memoizedState: any,
baseState: any,
baseUpdate: Update<any, any> | null,
queue: UpdateQueue<any, any> | null,
next: Hook | null, // 指向下一个Hook
};
```
首先从`Hook`的类型定义中就可以看到,`React``Hooks`的定义是链表。也就是说我们组件里使用到的`Hooks`是通过链表来联系的,上一个`Hooks``next`指向下一个`Hooks`。这些`Hooks`节点是怎么利用链表数据结构串联在一起的呢?相关逻辑就在每个具体`mount` 阶段 `Hooks`函数调用的 `mountWorkInProgressHook`方法里:
```javascript
// react-reconciler/src/ReactFiberHooks.js
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
queue: null,
baseUpdate: null,
next: null,
};
if (workInProgressHook === null) {
// 当前workInProgressHook链表为空的话,
// 将当前Hook作为第一个Hook
firstWorkInProgressHook = workInProgressHook = hook;
} else {
// 否则将当前Hook添加到Hook链表的末尾
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
```
`mount`阶段,每当我们调用`Hooks`方法,比如`useState``mountState`就会调用`mountWorkInProgressHook` 来创建一个`Hook`节点,并把它添加到`Hooks`链表上。比如我们的这个例子:
```javascript
const [age, setAge] = useState(initialAge);
const [name, setName] = useState(initialName);
useEffect(() => {})
```
那么在mount阶段,就会生产如下图这样的单链表:
![链式图](/yifu-study-front-share/images/hooks1.png)
#### 返回最新的值
`useState``useReducer`都是使用了一个`queue`链表来存放每一次的更新。以便后面的`update`阶段可以返回最新的状态。每次我们调用`dispatchAction`方法的时候,就会形成一个新的`updata`对象,添加到`queue`链表上,而且这个是一个循环链表。可以看一下`dispatchAction`方法的实现:
```javascript
// react-reconciler/src/ReactFiberHooks.js
// 去除特殊情况和与fiber相关的逻辑
function dispatchAction(fiber,queue,action) {
const update = {
action,
next: null,
};
// 将update对象添加到循环链表中
const last = queue.last;
if (last === null) {
// 链表为空,将当前更新作为第一个,并保持循环
update.next = update;
} else {
const first = last.next;
if (first !== null) {
// 在最新的update对象后面插入新的update对象
update.next = first;
}
last.next = update;
}
// 将表头保持在最新的update对象上
queue.last = update;
// 进行调度工作
scheduleWork();
}
```
也就是每次执行`dispatchAction`方法,比如`setAge``setName`。就会创建一个保存着此次更新信息的`update`对象,添加到更新链表`queue`上。然后每个`Hooks`节点就会有自己的一个`queque`。比如假设我们执行了下面几个语句:
```javascript
setAge(19);
setAge(20);
setAge(21);
```
那么Hooks链表就会变成这样:
![链式图](/yifu-study-front-share/images/hooks2.png)
`Hooks`节点上面,会如上图那样,通过链表来存放所有的历史更新操作。以便在`update`阶段可以通过这些更新获取到最新的值返回。这就是在第一次调用`useState``useReducer`之后,每次更新都能返回最新值的原因。再来看看`mountReducer`,你会发现和`mountState`几乎一摸一样,只是状态的初始化逻辑有那么一点区别。毕竟`useState`其实就是阉割版的`useReducer`。这里不详细介绍`mountReducer`了。
```javascript
// react-reconciler/src/ReactFiberHooks.js
function mountReducer(reducer, initialArg, init,) {
// 获取当前的Hook节点,同时将当前Hook添加到Hook链表中
const hook = mountWorkInProgressHook();
let initialState;
// 初始化
if (init !== undefined) {
initialState = init(initialArg);
} else {
initialState = initialArg ;
}
hook.memoizedState = hook.baseState = initialState;
// 存放更新对象的链表
const queue = (hook.queue = {
last: null,
dispatch: null,
lastRenderedReducer: reducer,
lastRenderedState: (initialState: any),
});
// 返回一个dispatch方法用来修改状态,并将此次更新添加update链表中
const dispatch = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
)));
// 返回状态和修改状态的方法
return [hook.memoizedState, dispatch];
}
```
然后我们来看看`update`阶段,也就是看一下我们的`useState``useReducer`是如何利用现有的信息,去返回最新的最正确的值的。先来看一下`useState``update`阶段的代码也就是`updateState`
```javascript
// react-reconciler/src/ReactFiberHooks.js
function updateState(initialState) {
return updateReducer(basicStateReducer, initialState);
}
```
可以看到,`updateState`底层调用的其实就调用`updateReducer`,因为我们调用`useState`的时候,并不会传入`reducer`,所以这里会默认传递一个`basicStateReducer`进去。我们先看看这个`basicStateReducer`
```javascript
// react-reconciler/src/ReactFiberHooks.js
function basicStateReducer(state, action){
return typeof action === 'function' ? action(state) : action;
}
```
在使用`useState(action)`的时候,`action`通常会是一个值,而不是一个方法。所以b`aseStateReducer`要做的其实就是将这个`action`返回。来继续看一下`updateReducer`的逻辑:
```javascript
// react-reconciler/src/ReactFiberHooks.js
// 去掉与fiber有关的逻辑
function updateReducer(reducer,initialArg,init) {
const hook = updateWorkInProgressHook();
const queue = hook.queue;
// 拿到更新列表的表头
const last = queue.last;
// 获取最早的那个update对象
first = last !== null ? last.next : null;
if (first !== null) {
let newState;
let update = first;
do {
// 执行每一次更新,去更新状态
const action = update.action;
newState = reducer(newState, action);
update = update.next;
} while (update !== null && update !== first);
hook.memoizedState = newState;
}
const dispatch = queue.dispatch;
// 返回最新的状态和修改状态的方法
return [hook.memoizedState, dispatch];
}
```
`update`阶段,也就是我们组件第二次第三次。。执行到`useState``useReducer`的时候,会遍历`update`对象循环链表,执行每一次更新去计算出最新的状态来返回,以保证我们每次刷新组件都能拿到当前最新的状态。`useState``reducer``baseStateReducer`,因为传入的`update.action`为值,所以会直接返回`update.action`,而`useReducer``reducer`是用户定义的`reducer`,所以会根据传入的`action`和每次循环得到的`newState`逐步计算出最新的状态。
![链式图](/yifu-study-front-share/images/hooks3.png)
### useState/useReducer 小总结
* React 如何管理区分Hooks?
React通过单链表来管理Hooks
按Hooks的执行顺序依次将Hook节点添加到链表中
* useState和useReducer如何在每次渲染时,返回最新的值?
每个Hook节点通过循环链表记住所有的更新操作
在update阶段会依次执行update循环链表中的所有更新操作,最终拿到最新的state返回
* 为什么不能在条件语句等中使用Hooks?
链表!
![链式图](/yifu-study-front-share/images/hooks4.png)
如图所示,我们在`mount`阶段调用了`useState('A')`, `useState('B')`, `useState('C')`,如果我们将`useState('B')` 放在条件语句内执行,并且在`update`阶段中因为不满足条件而没有执行的话,那么没法正确的重回`Hooks`链表中获取信息。React也会报错。
#### Hooks链表位置
现在已经了解了`React` 通过链表来管理 `Hooks`,同时也是通过一个循环链表来存放每一次的更新操作,得以在每次组件更新的时候可以计算出最新的状态返回给我们。那么我们这个`Hooks`链表又存放在那里呢?理所当然的我们需要将它存放到一个跟当前组件相对于的地方。那么很明显这个与组件一一对应的地方就是我们的`FiberNode`
![链式图](/yifu-study-front-share/images/hooks5.png)
如图所示,组件构建的`Hooks`链表会挂载到`FiberNode`节点的`memoizedState`上面去。
![链式图](/yifu-study-front-share/images/hooks6.png)
#### useEffect
先看下`useEffect`是如何工作的
```javascript
function PersionInfo () {
const [age, setAge] = useState(18);
useEffect(() =>{
console.log(age)
}, [age])
const [name, setName] = useState('Dan');
useEffect(() =>{
console.log(name)
}, [name])
return (
<>
...
</>
);
}
```
`PersionInfo`组件第一次渲染的时候会在控制台输出`age``name`,在后面组件的每次`update`中,如果`useEffect`中的`deps`依赖的值发生了变化的话,也会在控制台中输出对应的状态,同时在`unmount`的时候就会执行清除函数(如果有)。`React`中是怎么实现的呢?其实很简单,在`FiberNode`中通过一个`updateQueue`来存放所有的`effect`,然后在每次渲染之后依次执行所有需要执行的`effect``useEffect` 也分为`mountEffect``updateEffect`
#### mountEffect
```javascript
// react-reconciler/src/ReactFiberHooks.js
// 简化去掉特殊逻辑
function mountEffect( create,deps,) {
return mountEffectImpl(
create,
deps,
);
}
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {
// 获取当前Hook,并把当前Hook添加到Hook链表
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 将当前effect保存到Hook节点的memoizedState属性上,
// 以及添加到fiberNode的updateQueue上
hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
}
function pushEffect(tag, create, destroy, deps) {
const effect: Effect = {
tag,
create,
destroy,
deps,
next: (null: any),
};
// componentUpdateQueue 会被挂载到fiberNode的updateQueue上
if (componentUpdateQueue === null) {
// 如果当前Queue为空,将当前effect作为第一个节点
componentUpdateQueue = createFunctionComponentUpdateQueue();
// 保持循环
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
// 否则,添加到当前的Queue链表中
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}
```
可以看到在`mount`阶段,`useEffect`做的事情就是将自己的`effect`添加到了`componentUpdateQueue`上。这个`componentUpdateQueue`会在`renderWithHooks`方法中赋值到`fiberNode``updateQueue`上。
```javascript
// react-reconciler/src/ReactFiberHooks.js
// 简化去掉特殊逻辑
export function renderWithHooks() {
const renderedWork = currentlyRenderingFiber;
renderedWork.updateQueue = componentUpdateQueue;
}
```
也就是在`mount`阶段我们所有的`effect`都以链表的形式被挂载到了`fiberNode`上。然后在组件渲染完毕之后,`React`就会执行`updateQueue`中的所有方法。
![链式图](/yifu-study-front-share/images/hooks7.png)
#### updateEffect
```javascript
// react-reconciler/src/ReactFiberHooks.js
// 简化去掉特殊逻辑
function updateEffect(create,deps){
return updateEffectImpl(
create,
deps,
);
}
function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps){
// 获取当前Hook节点,并把它添加到Hook链表
const hook = updateWorkInProgressHook();
// 依赖
const nextDeps = deps === undefined ? null : deps;
// 清除函数
let destroy = undefined;
if (currentHook !== null) {
// 拿到前一次渲染该Hook节点的effect
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
// 对比deps依赖
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 如果依赖没有变化,就会打上NoHookEffect tag,在commit阶段会跳过此
// effect的执行
pushEffect(NoHookEffect, create, destroy, nextDeps);
return;
}
}
}
hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps);
}
```
`update`阶段和`mount`阶段类似,只不过这次会考虑`effect` 的依赖`deps`,如果此次更新`effect`的依赖没有变化的话,就会被打上`NoHookEffect`标签,最后会在`commit`阶段跳过改`effect`的执行。
```javascript
function commitHookEffectList(unmountTag,mountTag,finishedWork) {
const updateQueue = finishedWork.updateQueue;
let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & unmountTag) !== NoHookEffect) {
// Unmount 阶段执行tag !== NoHookEffect的effect的清除函数 (如果有的话)
const destroy = effect.destroy;
effect.destroy = undefined;
if (destroy !== undefined) {
destroy();
}
}
if ((effect.tag & mountTag) !== NoHookEffect) {
// Mount 阶段执行所有tag !== NoHookEffect的effect.create,
// 我们的清除函数(如果有)会被返回给destroy属性,一遍unmount执行
const create = effect.create;
effect.destroy = create();
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
```
### useEffect 小总结
![链式图](/yifu-study-front-share/images/hooks8.png)
useEffect做了什么?
`FiberNdoe`节点中又一个`updateQueue`链表来存放所有的本次渲染需要执行的`effect`
`mountEffect`阶段和`updateEffect`阶段会把`effect` 挂载到`updateQueue`上。
`updateEffect`阶段,`deps`没有改变的`effect`会被打上`NoHookEffect tag``commit`阶段会跳过该`Effect`
到此为止,`useState/useReducer/useEffect`源码也阅读完毕了,相信有了这些基础,剩下的Hooks的源码阅读不会成问题,最后放上完整图示:
![链式图](/yifu-study-front-share/images/hooks9.png)
---
title: Docker 基础使用介绍
date: 2022/05/30 9:25:23
updated: 2022/05/30 9:25:23
tags:
- docker
- 容器
- 虚拟化
author: 蔡海飞
---
> Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口(类似 iPhone 的 app),更重要的是容器性能开销极低。我们可以通过将开发代码打包到docker 镜像中,上传到docker hub中,待发布时通过拉取docker hub镜像启动docker 容器,实现代码运行环境的高度一致,并提高部署效率。
![docker运行系统图](/yifu-study-front-share/images/docker-system.png)
本篇文章就我们较为常用的docker 命令使用通过场景使用的不同来简单介绍。
<!-- more -->
### Docker 安装与镜像加速
#### Docker安装
docker 提供了各种不同系统的安装方式,因为我们常用的正式部署环境使用centos的缘故,我这里只介绍centos里如何安装docker以及如何配置docker国内镜像加速。
1. 使用官方提供的脚本自动安装
```
$ curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun
```
2. 手动命令行安装
```
# 先卸载旧版本的docker安装包,后安装新版
$ sudo yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine
$ sudo yum install -y yum-utils \
device-mapper-persistent-data \
lvm2
```
#### 镜像加速
1. 阿里云
```
$ sudo yum-config-manager \
--add-repo \
http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
```
2. 清华大学源
```
$ sudo yum-config-manager \
--add-repo \
https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/centos/docker-ce.repo
```
3. 腾讯源
```
$ sudo yum-config-manager \
--add-repo \
https://mirror.ccs.tencentyun.com/docker-ce/linux/centos/docker-ce.repo
```
#### 启动docker 服务
```
$ sudo systemctl restart docker
```
### Docker 核心概念
![docker-system.jpeg](/yifu-study-front-share/images/docker-system.jpeg)
#### 镜像
Docker镜像是由文件系统叠加而成。一个镜像可以多个中间层组成,多个镜像可以共享同一中间层,我们也可以通过在镜像添加多一层来生成一个新的镜像。镜像在构建完成之后,便不可以再修改,而上面我们所说的添加一层构建新的镜像,这中间实际是通过创建一个临时的容器,在容器上增加或删除文件,从而形成新的镜像,因为容器是可以动态改变的。整理可以得出镜像的两个特点:
- 镜像是分层的
- 镜像是只读的
![docker-image.png](/yifu-study-front-share/images/docker-image.png)
1. 常用命令
```shell
$ docker image ls # 枚举本地主机中的docker镜像
$ docker image inspect nginx:latest # 获取nginx:latest镜像的层数据和元数据
$ docker rmi nginx:latest # 删除本地的nginx:latest,需要先删除应用该镜像的容器
$ docker tag nginx:latest nginx:test # 给本地nginx:latest镜像添加标签nginx:test
$ docker commit -a a404c6c174a2 mymysql:v1 # 基于当前容器a404c6c174a2创建新的镜像mymysql:v1
$ docker build -f Dockerfile -t myimage:0.0.1 . # 基于Dockerfile文件制作镜像myimage:0.0.1
```
#### 容器
容器是镜像的运行时实例。正如从虚拟机模板上启动 VM 一样,用户也同样可以从单个镜像上启动一个或多个容器。虚拟机和容器最大的区别是容器更快并且更轻量级——与虚拟机运行在完整的操作系统之上相比,容器会共享其所在主机的操作系统/内核。
Docker 容器是一个开源的应用容器引擎,让开发者可以以统一的方式打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何安装了docker引擎的服务器上(包括流行的Linux机器、windows机器),也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口(类似 iPhone 的 app)。几乎没有性能开销,可以很容易地在机器和数据中心中运行。最重要的是,他们不依赖于任何语言、框架包括系统。
1. 常用命令
```shell
$ docker ps -a # 列出当前主机中所有的docker容器
$ docker run -it nginx /bin/bash # 基于nginx镜像启动一个docker容器
$ docker pause containername # 暂停docker 容器
$ docker start containername # 启动docker容器
$ docker stop containername # 停止docker容器
$ docker restart containername # 重启docker容器
$ docker attach -it containername /bin/bash # 进入docker 容器,此种方式进入退出后会使得docker容器停止
$ docker exec -it containername /bin/bash # 进入docker 容器,此种方式进入退出后不会使得docker容器停止
$ docker export containername > containername.tar # 导出docker容器快照
$ docker rm containername # 删除docker 容器
$ docker logs -f containername # 持续输出docker容器的日志信息
$ docker cp containerID:container_path host_path # 复制主机文件到对应docker容器的地址
```
#### 仓库
Docker 仓库是用来包含镜像的位置。Docker提供一个注册服务器(Register)来保存多个仓库,每个仓库又可以包含多个具备不同tag的镜像。Docker运行中使用的默认仓库是 Docker Hub 公共仓库。
![docker-hub.png](/yifu-study-front-share/images/docker-hub.png)
1. 常用命令
```shell
$ docker pull nginx:latest # 从镜像仓库拉取nginx:latest镜像
$ docker search nginx # 搜索镜像库的nginx相关镜像
$ docker push nginx:0.0.1 # 提交本地镜像nginx:0.0.1到当前登录的镜像仓库
```
### 一个简单的docker 使用示例
1. 制作Dockerfile文件
```shell
# Dockerfile 文件
FROM nginx:alpine
ADD deploy/qas/default.conf /etc/nginx/conf.d
ADD dist /home/html/app/
# deploy/qas/default.conf
server{
listen 8080;
location ^~ /proxy-path/ {
#自定义头信息传递
proxy_pass http://****/;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_http_version 1.1;
}
location / {
#配置代理参数
proxy_redirect off;
proxy_set_header X-Forwarded-Host $host:$server_port;
proxy_set_header X-Forwarded-Server $host:$server_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host:$server_port;#这里是重点,这样配置才不会丢失端口
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
root /home/html/app/;
index index.html;
try_files $uri /index.html;
}
}
# dist为打包文件
```
2. 制作Docker镜像
```shell
$ docker build -f ./deploy/qas/Dockerfile -t website:0.0.1 .
```
3. 上传Docker镜像
```shell
$ docker tag website:0.0.1 website/test
$ docker images
$ docker push website/test
```
4. 启动docker 容器
```shell
$ docker pull website/test
$ docker run -it --name website-test -p 80:80 -d website/tes
```
5. 查看docker 容器运行状态
```shell
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
90cdb9ad1ae1 website/tes "nginx -g 'daemon of…" a minute ago Up a minute 0.0.0.0:80->80/tcp website-test
```
\ No newline at end of file
---
title: 性能优化---Preload&Prefetch
date: 2022/5/7 14:06:20
tags:
- 前端
- 盒子模型
- CSS
author: 刘磊
---
# 性能优化---Preload&Prefetch
在preload和prefetch之前,我们一般根据编码需求通过link或者script标签引入页面渲染和交互所依赖的js和css等;然后这些资源由浏览器决定优先级进行加载、解析、渲染等。
然而,有些情况我们需要某些依赖在浏览器进入渲染的主进程之前就希望被加载,但是实际资源加载的状况并不受我们控制,而是根据浏览器内部决定资源的优先级等状况进行加载的。即使我们将需要的资源按照希望的那样放在期望的位置,但是由于资源的加载和解析,尤其是js的加载、解析和编译是阻塞式的,因此往往还是达不到预期。或者,有些资源是我们后边需要的,我们希望在需要它的时候在进行解析和执行,并不想让它的加载影响其他资源的加载及首屏渲染时间。
preload和prefetch的出现为我们提供了可以更加细粒度地控制浏览器加载资源的方法。
```
<link ref='preload'>
<link ref='prefetch'>
```
<!-- more -->
#### preload特点
- preload加载的资源是在浏览器渲染机制之前进行处理的,并且不会阻塞onload事件;
- preload可以支持加载多种类型的资源,并且可以加载跨域资源;
- preload加载的js脚本其加载和执行的过程是分离的。即preload会预加载相应的脚本代码,待到需要时自行调用;
#### prefetch特点
- prefetch加载的资源可以获取非当前页面所需要的资源,并且将其放入缓存至少5分钟(无论资源是否可以缓存);并且,当页面跳转时,未完成的prefetch请求不会被中断;
#### preload和prefetch对比
- preload主要用于预加载当前页面需要的资源;而prefetch主要用于加载将来页面可能需要的资源;
- 不论资源是否可以缓存,prefecth会存储在net-stack cache中至少5分钟;
- preload需要使用as属性指定特定的资源类型以便浏览器为其分配一定的优先级,并能够正确加载资源;
## webpack中的perload和perfetch
问题:异步加载模块是Webpack官方所希望开发者实现的,而且异步加载也能提升首页加载速度,但是又会导致异步加载的那部分代码逻辑迟迟不能执行,可能导致用户的交互长时间没有响应。
所谓预取,就是提前获取将来可能会用到的资源。prefetch chunk会在父chunk加载完成之后进行加载。
那么该怎么做呢?我们要通过一种称作Magic Comment(魔法注释)的方式,让Webpack输出"resource hint(资源提示)",来告知浏览器。
```
document.getElementById('btn').onclick = function () {
import( /* webpackChunkName: 'base',webpackPrefetch:true*/ './base').then(({
mul
}) => {
console.log(mul(40, 3));
}).catch((err) => {
console.log(`base.js加载失败:${err}`);
});
}
```
---
title: TypeScript泛型
date: 2022/5/10 16:00:00
tags:
- 前端
- typescript
author: 杨靖
---
## 泛型
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
我们可以用一个函数createArray,它可以创建一个指定长度的数组,同时将每一项都填充一个默认值:
```typescript
function createArray(length: number, value: any): Array<any> {
let result = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
createArray(3, 'x'); // ['x', 'x', 'x']
```
上例中,我们使用了之前提到过的数组泛型来定义返回值的类型。
这段代码编译不会报错,但是一个显而易见的缺陷是,它并没有准确的定义返回值的类型:
Array<any> 允许数组的每一项都为任意类型。但是我们预期的是,数组中每一项都应该是输入的 value 的类型。
这时候,泛型就派上用场了:
<!-- more -->
```typescript
function createArray<T>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
createArray<string>(3, 'x'); // ['x', 'x', 'x']
```
上例中,我们在函数名后添加了 <T>,其中 T 用来指代任意输入的类型,在后面的输入 value: T 和输出 Array<T> 中即可使用了。
接着在调用的时候,可以指定它具体的类型为 string。当然,也可以不手动指定,而让类型推论自动推算出来:
```typescript
function createArray<T>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
createArray(3, 'x'); // ['x', 'x', 'x']
```
*定义泛型的时候,可以一次定义多个类型参数:*
```typescript
function swap<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]];
}
swap([7, 'seven']); // ['seven', 7]
```
上例中,我们定义了一个 swap 函数,用来交换输入的元组。
**泛型约束**
在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法:
```typescript
function loggingIdentity<T>(arg: T): T {
console.log(arg.length);
return arg;
}
// index.ts(2,19): error TS2339: Property 'length' does not exist on type 'T'.
```
上例中,因为泛型 T 不一定包含属性 length,所以编译的时候报错了,是一个错误的例子。
这时,我们可以对泛型进行约束,只允许这个函数传入那些包含 length 属性的变量。这就是泛型约束:
```typescript
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
```
上例中,我们使用了 extends 约束了泛型 T 必须符合接口 Lengthwise 的形状,也就是必须包含 length 属性。
此时如果调用 loggingIdentity 的时候,传入的 arg 不包含 length,那么在编译阶段就会报错了:
```typescript
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
loggingIdentity(7);
// index.ts(10,17): error TS2345: Argument of type '7' is not assignable to parameter of type 'Lengthwise'.
```
多个类型参数之间也可以互相约束:
```typescript
function copyFields<T extends U, U>(target: T, source: U): T {
for (let id in source) {
target[id] = (<T>source)[id];
}
return target;
}
let x = { a: 1, b: 2, c: 3, d: 4 };
copyFields(x, { b: 10, d: 20 });
```
上例中,我们使用了两个类型参数,其中要求 T 继承 U,这样就保证了 U 上不会出现 T 中不存在的字段。
**泛型接口**
使用接口的方式来定义一个函数需要符合的形状:
```typescript
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
return source.search(subString) !== -1;
}
```
当然也可以使用含有泛型的接口来定义函数的形状:
```typescript
interface CreateArrayFunc {
<T>(length: number, value: T): Array<T>;
}
let createArray: CreateArrayFunc;
createArray = function<T>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
createArray(3, 'x'); // ['x', 'x', 'x']
```
```typescript
interface CreateArrayFunc<T> {
(length: number, value: T): Array<T>;
}
let createArray: CreateArrayFunc<any>;
createArray = function<T>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
createArray(3, 'x'); // ['x', 'x', 'x']
```
*使用泛型接口的时候,需要定义泛型的类型。*
**泛型类**
与泛型接口类似,泛型也可以用于类的类型定义中:
```typescript
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
```
**泛型参数的默认类型**
在 TypeScript 2.3 以后,我们可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用。
```typescript
function createArray<T = string>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
```
...@@ -4,7 +4,7 @@ date: 2022/4/22 17:30:00 ...@@ -4,7 +4,7 @@ date: 2022/4/22 17:30:00
tags: tags:
- 前端 - 前端
- typescript - typescript
author: 杨靖 author: 杨靖
--- ---
## typescript的高级类型详解 ## typescript的高级类型详解
......
---
title: 性能优化--通过优化首屏加载速度实现
date: 2022/5/24 14:30
tags:
- 前端
- 路由懒加载
- 性能优化
author: 刘磊
---
#### 什么是路由懒加载
路由懒加载也叫延迟加载,即在需要的时候进行加载,随用随载。在单页面应用中浏览器默认会将所有的资源全部下载下来,这可能会导致首屏加载的时间过长。路由懒加载可以做到点击对应的路由再加载对应的文件资源
#### 路由懒加载的三种写法 (推荐用法:es6的import)
1. component: resolve => require(['../pages/home], resolve)
1. component: import(/* webpackChunkName: 'ImportFuncDemo' */ '../pages/home')
1. component: resolve=>{require.ensure(,()=>{resolve(require('../pages/home'))})}
#### 路由懒加载使用中的疑惑
项目中已经使用了路由懒加载为什么首屏渲染仍然请求了所有路由的chunk;
解答:vue-cli webpack中默认配置了prefetch这个功能可以在浏览器闲置时加载网站未来可能会用到的资源;但
经过验证这个prefetch预请求阻塞了首屏的加载
#### 解决prefetch影响首页加载问题
- 在webpack配置中删除prefetch插件
```
configplugins.delete(prefetch)
```
<!-- more -->
- 在引用时配置
```
import(<!--webpackChunkName:'[...]',webpackPrefetch:false-->)
```
#### 思考:既然可以通过import异步加载路由组件,非路由组件是否可以通过异步加载的方式,从而优化首屏的加载速度?很多情况中复杂的弹窗等组件比页面组件的资源更大。
- 经过查阅文档发现VUE官方也提到了异步组件,并有相关的用法
- 通过异步引入的弹窗组件从主包分离出来,但是仍然在首页加载是就将弹窗组件加载出来。并未实现点击组件时
再去加载文件:
```
const wechatEnter = () => ({
// 需要加载的组件 (应该是一个 `Promise` 对象)
component: import(
/*webpackChunkName:'wechatEnter',webpackPrefetch:true */ '@/views/index/components/wechatEnter.vue'
),
// 展示加载时组件的延时时间。默认值是 200 (毫秒)
delay: 200,
// 异步组件加载时使用的组件
loading: LoadingComponent,
// 加载失败时使用的组件
error: ErrorComponent,
// 则使用加载失败时使用的组件。默认值是:`Infinity`
timeout: 3000,
})
```
#### 利用webpack——SplitChunks优化
SplitChunks 插件来控制 Webpack 打包生成的 js 文件的内容的精髓在于:防止模块被重复打包,拆分过大的js
文件,合并零散的js文件;
##### 每个浏览器并发请求数量不同,分包需要考虑并发
由于并发数量的限制,对于splitchunks的分包并不是越多越好。需要具体情况具体分析,比如并发请求数量设置为4,目前有三个包一个包体积较大,那么这时候对大包拆成两个包加载速度肯定会有提升;如果本来有4个包,并发数量也是4,这时候拆个包出来如果包的体积也不大,第5个包请求的时间加上队列之间多出来的时间,综合下来谁快谁慢还不好说;最好的办法还是对静态资源做多域名请求;
#### 通过CDN引入第三方库,但是这个有风险,就是第三方库挂掉或者什么的,可以选择付费的cdn加速。或者下载到本地上传到自己的服务器,总共三套方案,可以同时使用,保证安全性。
#### 静态资源压缩,代码压缩,图片压缩;gzip压缩一定要开,并且服务端配合开一下,真的很重要。开启gzip后你会发现加载速度有质的提升,尤其是你的vendor.js和app.js体积过大的时候。
#### 代码层面的优化
---
title: 浅谈Vite
date: 2022/5/2515:00:00
tags:
- vite
- package.json
author: 张芳利
---
<a name="Sxg5O"></a>
# 1. Vite 简介
<a name="GUv9u"></a>
## 1.1 Vite是什么?
Vite是一个由原生 ES Module 驱动的 Web 开发前端构建工具,它能显著改善前端开发体验;<br />要由两部分组成:
- 一个开发服务器,它基于 [原生 ES 模块](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) 提供了 [丰富的内建功能](https://vitejs.cn/guide/features.html),如速度快到惊人的 [模块热更新(HMR)](https://vitejs.cn/guide/features.html#hot-module-replacement)
- 一套构建指令,生产环境,它使用 [Rollup](https://rollupjs.org/) 打包你的代码,并且它是预配置的,可输出用于生产环境的高度优化过的静态资源。
<a name="V0wyB"></a>
<!-- more -->
## 1.2 Vite的特性
- 💡 **极速的服务启动:** 使用原生 ESM 文件,无需打包!
- ⚡️ **轻量快速的热重载:** 无论应用程序大小如何,都始终极快的模块热重载(HMR)
- 🛠️ **丰富的功能:** 对 TypeScript、JSX、CSS 等支持开箱即用。
- 📦 **优化的构建**:可选 “多页应用” 或 “库” 模式的预配置 Rollup 构建
- 🔩 **通用的插件**: 在开发和构建之间共享 Rollup-superset 插件接口。
- 🔑 **完全类型化的API**:灵活的 API 和完整 TypeScript 类型
<a name="Vv04S"></a>
# 2. Vite概念及设计思想
**打包:**使用工具抓取、处理并将我们的源码模块串联成可以在浏览器中运行的文件。
<a name="vmIab"></a>
## 2.1 和webpack 的区别
1. **打包和速度:**
webpack: 分析依赖=> 编译打包=> 交给本地服务器进行渲染。首先分析各个模块之间的依赖,然后进行打包,在启动webpack-dev-server,请求服务器时,直接显示打包结果。webpack打包之后存在的问题:随着模块的增多,会造成打出的 bundle 体积过大,进而会造成热更新速度明显拖慢。<br />
![webpack](/yifu-study-front-share/images/viteImg/webpack.png)
vite: 启动服务器=> 请求模块时按需动态编译显示。是先启动开发服务器,请求某个模块时再对该模块进行实时编译,因为现代游览器本身支持ES-Module,所以会自动向依赖的Module发出请求。所以vite就将开发环境下的模块文件作为浏览器的执行文件,而不是像webpack进行打包后交给本地服务器。<br />
![vite](/yifu-study-front-share/images/viteImg/vite1.png)
2. **工具定位:**
webpack:是偏底层的东西,进行配置化,灵活度极高的工具,具有丰富的生态圈<br />vite: 是开箱即用,使用更简单的工具,但是目前是属于起步阶段
结论:
1. Vite主打特点就是轻快冷服务启动。冷服务的意思是,在开发预览中,它是不进行打包的。
1. 开发中可以实现热更新,也就是说在你开发的时候,只要一保存,结果就会更新。
1. 按需进行更新编译,不会刷新全部DOM节点。这功能会加快我们的开发流程度。
<a name="rEfXe"></a>
## 2.2 Esm 加载对比
1. esm 文件中,创建index.js , 引入main.js
```jsx
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESM Brower</title>
<!-- type module -->
<script src="main.js" type="module"></script>
</head>
<body>
</body>
</html>
```
2. 安装 lodash的包
`yarn add lodash-es`
3. 引入loadsh,并利用她的fiter 功能
```jsx
console.log('测试esm');
import {filter} from 'loadsh'
const arr = [{name:'张小张',age:'22'},{name:"王小王",age:'18'}]
const newArr = filter(arr,item=>item.age>20)
console.log(newArr);
```
报错:<br />Uncaught TypeError: Failed to resolve module specifier "loadsh". Relative references must start with either "/", "./", or "../".<br />浏览器原生解析路径的问题,所以换一下引入方式<br />node_modules中找到lodash-es,找到她的package,json,查看找到 "main": "lodash.js"
```jsx
import { filter } from './node_modules/lodash-es/lodash.js'
```
4. 利用**Live Server** 插件,项目启动
![live server启动](/yifu-study-front-share/images/viteImg/vite2.png)
5. 我们能看到打印出来的东西:
![打印1](/yifu-study-front-share/images/viteImg/vite3.png)<br />![打印2](/yifu-study-front-share/images/viteImg/vite4.png)<br />![打印2](/yifu-study-front-share/images/viteImg/vite5.png)
<br />从上面看到只是用到了一个过滤但是其他的模块等也都进行了请求,浪费了时间
**vite是怎么做的呢?**
1. 创建一个vite-esm 文件
`yarn create vite`
2. 同样 安装lodash-es
2. main.js 中同样操作
```jsx
console.log('测试esm');
import { filter } from 'lodash-es'
const arr = [{name:'张小张',age:'22'},{name:"王小王",age:'18'}]
const newArr = filter(arr,item=>item.age>20)
console.log(newArr);
```
4. yarn dev 后,页面中看到
![打印3](/yifu-study-front-share/images/viteImg/vite6.png)<br />![打印4](/yifu-study-front-share/images/viteImg/vite7.png)<br />对比之下可以看到,页面清爽了很多,很多没有必要的请求也没有了。
<a name="vXvPP"></a>
# 3. vite 的使用
> Vite 需要 [Node.js](https://nodejs.org/en/) 版本 >= 12.0.0。
- **npm **
```jsx
npm init vite-app <project-name>
cd <project-name>
npm install
npm run dev
```
或者
```jsx
npm init @vitejs/app 项目名
npm init vite@latest
```
这个命令安装就是一步步自己选择一下是react还是vue,以及ts 等。如果选择vue,都是vue3.0的。
- **yarn**
```jsx
yarn create vite-app <project-name>
cd <project-name>
yarn
yarn dev
```
或者
```jsx
yarn create @vitejs/app 项目名
```
使用yarn有个问题 :文件名、目录名或卷标语法不正确<br />解决方法:[https://juejin.cn/post/6844904111570190349](https://juejin.cn/post/6844904111570190349)
vite中还会有一些模板可以选择,指定的所有相关项目模板都可以在[ https://github.com/vitejs/awesome-vite#templates]() 仓库中找到
> 一般都是默认创建的是vue3的项目,所以如果创建vue2,需要做一些修改之类的或者在模板库中查找使用
<a name="z548o"></a>
## 3.1 遇到的问题
控制台会显示 user --host to expose(使用 --host 暴露网络) <br />![host问题](/yifu-study-front-share/images/viteImg/vite8.png)<br />解决方法:
1. 在根目录下的 vite.config.js 文件中添加以下内容
```jsx
import vue from '@vitejs/plugin-vue'
/**
* https://vitejs.dev/config/
* @type {import('vite').UserConfig}
*/
export default {
plugins: [vue()],
server: { // ← ← ← ← ← ←
host: '0.0.0.0' // ← 新增内容 ←
} // ← ← ← ← ← ←
}
```
2. 通过 Vite CLI 配置
执行 npx vite --host 0.0.0.0 命令后,就可以通过 http://10.56.116.128:3000/ 访问了。
3. 修改npm 脚本
修改 package.json 文件中 scripts 节点下的脚本,如下
```jsx
"scripts": {
"dev": "vite --host 0.0.0.0",
"build": "vite build",
"serve": "vite preview --host 0.0.0.0"
}
```
<a name="R0yEc"></a>
## 3.2 项目目录
```jsx
|-node_modules -- 项目依赖包的目录
|-public -- 项目公用文件
|--favicon.ico -- 网站地址栏前面的小图标
|-src -- 源文件目录,程序员主要工作的地方
|-assets -- 静态文件目录,图片图标,比如网站logo
|-components -- Vue3.x的自定义组件目录
|--App.vue -- 项目的根组件,单页应用都需要的
|--index.css -- 一般项目的通用CSS样式写在这里,main.js引入
|--main.js -- 项目入口文件,SPA单页应用都需要入口文件
|--.gitignore -- git的管理配置文件,设置那些目录或文件不管理
|-- index.html -- 项目的默认首页,Vue的组件需要挂载到这个文件上
|-- package-lock.json --项目包的锁定文件,用于防止包版本不一样导致的错误
|-- package.json -- 项目配置文件,包管理、项目名称、版本和命令
|--vite.config.js -- 配置文件
```
<a name="mLpTv"></a>
## 3.3 vite插件
Vite 旨在为常见的 web 开发工作提供开箱即用的支持。你可以使用插件进行扩展功能。<br />地址:[https://github.com/vitejs/awesome-vite#plugins](https://github.com/vitejs/awesome-vite#plugins)<br />例如:为传统浏览器提供支持
1. 下载
```jsx
npm i -D @vitejs/plugin-legacy
```
2. 需要将它添加到项目的 devDependencies 并在 vite.config.js 配置文件中的 plugins 数组中引入它
```jsx
// vite.config.js
import legacy from '@vitejs/plugin-legacy'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
legacy({
targets: ['defaults', 'not IE 11']
})
]
})
```
<a name="YkAyf"></a>
## 3.4 vite.config.js
1. 是否自动在浏览器打开
`open: true`
2. 在生产中服务时的基本公共路径
`base: './',`
3. 与“根”相关的目录,构建输出将放在其中。如果目录存在,它将在构建之前被删除
`outDir: 'dist'`
4. 更改默认端口
`port:8080,`
5. 开启热更新
`hmr:true`
6. 配置别名路径
```jsx
resolve:{
alias:{
"@":resolve(__dirname,"src"),
"@c":resolve(__dirname,"src/components"),
}
},
```
7. 设置便捷图片路径引用
```jsx
resolve:{
alias:{
"/img":"src/assets/images/"
//这里不能通过path模块解析路径的写法
}
},
```
8. 把vite打包以后的js,css和img资源分别分门别类在js/css/img文件夹中
```jsx
build:{
assetsDir:"static",
rollupOptions:{ //自定义底层的 Rollup 打包配置,从 Rollup 配置文件导出的选项相同
input:{
index:resolve(__dirname,"index.html"),
project:resolve(__dirname,"project.html")
},
output:{
chunkFileNames:'static/js/[name]-[hash].js',
entryFileNames:"static/js/[name]-[hash].js",
assetFileNames:"static/[ext]/name-[hash].[ext]"
}
},
},
```
8. 跨域
```jsx
server: {
port: '3000',
open: false, //自动打开
base: "./ ", //生产环境路径
proxy: { // 本地开发环境通过代理实现跨域,生产环境使用 nginx 转发
// 正则表达式写法
'^/api': {
target: 'http://xxx.xxx.xxx.xxx:9999', // 后端服务实际地址
changeOrigin: true, //开启代理
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
```
<a name="agldq"></a>
## 3.5 给项目配置多个环境
1. 在项目根目录下分别新建.env.development,.env.test,.env.production文件
```jsx
//.env.devopment文件内容
NODE_ENV="development"
VITE_APP_BASEAPI="https://www.dev.com"
//.env.test文件内容
NODE_ENV="test"
VITE_APP_BASEAPI="https://www.test.com"
//.env.production文件内容
NODE_ENV="production"
VITE_APP_BASEAPI="https://www.production.com"
```
2. package.json
```jsx
"scripts": {
"dev": "vite --mode development",
"build": "vite build --mode production",
"test": "vite build --mode test",
"preview": "vite preview"
},
```
3. 项目中通过Import.meta.env.VITE_APP_BASEAPI来获取对应环境的值
```jsx
<template>
<div>
<Item></Item>
</div>
</template>
<script setup>
import { defineComponent, onMounted, ref } from 'vue'
import Item from "@c/item.vue"
console.log("env", import.meta.env.VITE_APP_BASEAPI)
console.log("可选链", obj?.gender || "male")
})
</script>
```
<a name="pgCX0"></a>
# 4. npm init vite@latest做了什么?
<a name="HsCxP"></a>
## 4.1 npm init
npm init :初始化一个简单的package.json文件,执行该命令后终端会依次询问 name, version, description 等字段<br />查询[文档](https://docs.npmjs.com/cli/v7/commands/npm-init)得知:<br />**npm init <initializer>:**can be used to set up a new or existing npm package.(可以用来建立一个新的或现有的NPM包)
<a name="oLGN9"></a>
## 4.2 npm init <initializer>
initializer in this case is an npm package named create-<initializer>, which will be installed by [npm-exec](https://docs.npmjs.com/cli/v7/commands/npm-exec), and then have its main bin executed -- presumably creating or updating package.json and running any other initialization-related operations.<br />(initializer 在这种情况下是一个名为 create-<initializer> 的 npm 包,这个包将由 [npm-exec](https://link.juejin.cn?target=https%3A%2F%2Fdocs.npmjs.com%2Fcli%2Fv7%2Fcommands%2Fnpm-exec)(也就是 **npx**) 来安装,然后执行 package.json 中 bin 属性对应的脚本,运行任何其他与初始化相关的操作。
init命令转化为对应的npm exec操作如下:
- npm init foo->npm exec create-foo
- npm init @usr/foo->npm exec @usr/create-foo
- npm init @usr->npm exec @usr/create
由上面得知:<br />npm init vite@latest => npx create-vite
<a name="ExHkk"></a>
## 4.3 查询create-vite
通过 [npmjs.com](https://link.juejin.cn/?target=http%3A%2F%2Fnpmjs.com) 查找 create-vite 或在 Vite 源代码中的 [packages](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fvitejs%2Fvite%2Ftree%2Fmain%2Fpackages) 文件夹(如下图)中可以看到有 [create-vite](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fvitejs%2Fvite%2Ftree%2Fmain%2Fpackages%2Fcreate-vite)
<br />![create-vite1](/yifu-study-front-share/images/viteImg/vite9.png)
<br />![create-vite2](/yifu-study-front-share/images/viteImg/vite10.png)
<br />查看在 create-vite 文件夹下的 package.json 中发现 [bin 入口](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fvitejs%2Fvite%2Fblob%2Fmain%2Fpackages%2Fcreate-vite%2Fpackage.json%23L7)
<br />![create-vite3](/yifu-study-front-share/images/viteImg/vite11.png)
<a name="OVWEq"></a>
#
...@@ -202,7 +202,7 @@ XLSX.utils.decode_range("B1:C9"); //{s:{c: 1, r: 0},e: {c: 2, r: 8}} ...@@ -202,7 +202,7 @@ XLSX.utils.decode_range("B1:C9"); //{s:{c: 1, r: 0},e: {c: 2, r: 8}}
- readingOrder:文字方向,值为数字0(根据内容决定)、1(从左到有) 或 2(从右到左) - readingOrder:文字方向,值为数字0(根据内容决定)、1(从左到有) 或 2(从右到左)
- textRotation:文本旋转角度,值为数字,0至180或 255(垂直排列) ,默认为0 - textRotation:文本旋转角度,值为数字,0至180或 255(垂直排列) ,默认为0
`alignment`: 对其属性,其子属下如下: `border`: 边框属性,其子属下如下:
- top:上边框样式,值为对象,如:{style: 'thin', color:{rgb: "FFFFAA00"}},其中,style的值集包括:thin、medium、thick、dotted、hair、dashed、mediumDashed、dashDot、mediumDashDot、dashDotDot、mediumDashDotDot、slantDashDot、double - top:上边框样式,值为对象,如:{style: 'thin', color:{rgb: "FFFFAA00"}},其中,style的值集包括:thin、medium、thick、dotted、hair、dashed、mediumDashed、dashDot、mediumDashDot、dashDotDot、mediumDashDotDot、slantDashDot、double
- bottom: 下边框样式,值同top - bottom: 下边框样式,值同top
......
---
title: 用xlsx-style玩转Excel导出——拿走即用的Excel导出方法封装
date: 2022/5/24 09:15:20
tags:
- 前端
- xlsx-style
- JavaScript
author: 李燕
---
在《用xlsx-style玩转Excel导出——像手动操作Excel表格一样用JS操作Excel》一文中,我们详细介绍了xlsx-style插件相关的概念、属性和方法,今天,我们将利用xlsx-style插件封装出一个通用的、可自定义的Excel导出方法,以期能够满足日常Web项目的导出需求。
<!-- more -->
**一、常见自定义需求**
在正式开始封装之前,我们先梳理一下,我们Excel导出一般都有那些自定义需求。
1. 导出的Excel文件名自定义
2. 支持单sheet、多sheet导出
3. sheet名自定义
4. 表头内容自定义
5. 表格内容自定义
6. 表格样式自定义
7. 支持合并单元格
8. 支持冻结单元格
**二、抽象导出方法名及参数**
根据上述自定义需求,我们可以初步将Excel导出方法及参数定义如下:
```js
/**
* 自定义导出excel
* @param { 导出的数据 } exportData
* @param { 工作簿配置 } workBookConfig
* @param { 导出文件名 } fileName
*/
function exportExcel(exportData, workBookConfig = defaultWorkBook, fileName = '未命名') {
}
```
**三、参数介绍及处理**
(一)exportData
参数exportData是一个对象数组,可以满足单sheet、多sheet的导出,同时可以配置每个sheet的表头、表格内容、样式等,格式如下:
```js
[
{
header: [] // 表头
dataSource: [], // 数据源
workSheetConfig: {}, // 工作表配置
cellConfig: {} // 单元格配置
sheetName: '' // sheet名
}
]
```
+ header
header是一个对象数组,用来设置表头,格式如下:
```js
[
{ title: '分站', dataIndex: 'subName', width: 140 },
{ title: '企业总数', dataIndex: 'enterpriseCount', width: 140 },
]
```
接收到header后,需将header内容处理成表格语言,具体如下:
```js
const _header = header.map((item, i) =>
Object.assign({}, {
key: item.dataIndex,
title: item.title,
// 定位单元格
position: getCharCol(i) + 1,
// 设置表头样式
s: data.cellConfig && data.cellConfig.headerStyle ? data.cellConfig.headerStyle : defaultCellStyle.headerStyle,
})
).reduce((prev, next) =>
Object.assign({}, prev, {
[next.position]: { v: next.title, key: next.key,s: next.s },
}), {},
)
```
header已经被处理成worksheet Object,而其中很重要一步,就是定位单元格,而我们Excel是用字母+数字的形式定位单元格的,所以我们可以通过如下代码定位单元格。
```js
position: getCharCol(i) + 1
```
其中,getCharCol()方法是用来生成单元格对应的字母的,代码如下:
```js
/**
* 生成ASCll值 从A开始
* @param {*} n
*/
function getCharCol(n) {
if( n > 25) {
let s = ''
let m = 0
while (n > 0) {
m = n % 26 + 1
s = String.fromCharCode(m + 64) + s
n = (n - m) / 26
}
return s
}
return String.fromCharCode(65 + n)
}
```
+ cellConfig
cellConfig是一个对象,用来配置单元格的样式,格式如下:
```js
{
headerStyle: { // 表头区域样式配置
border: {}
font: {},
alignment: {},
fill: {},
},
dataStyle: { // 内容样式配置
border: {}
font: {},
alignment: {},
fill: {},
}
}
```
+ dataSource
dataSource是一个对象数组,用来表示表格内容,格式如下:
```js
[
{ subName'合肥' },
{ enterpriseCount: '100' },
]
```
同样,接收到dataSource后,需将dataSource内容处理成表格语言,具体如下:
```js
let _data = {};
// 处理表格内容及样式
dataSource.forEach((item, i) => {
header.forEach((obj, index) => {
const key = getCharCol(index) + (i + 2);
const key_t = obj.dataIndex;
_data[key] = {
v: item[key_t],
s: cellConfig.dataStyle? cellConfig.dataStyle : defaultCellStyle.dataStyle
}
})
});
```
+ workSheetConfig
workSheetConfig是一个对象,用来配置工作表的合并单元格、冻结单元格等功能,格式如下:
```js
{
merges: [ // 合并单元格
{ s: { c: 1, r: 1 },e: { c: 3, r: 3 } }
],
freeze{ // 冻结单元格
xSplit: '1',
ySplit: '1',
topLeftCell: 'B2',
state: 'frozen',
}
}
```
(二)workBookConfig
workBookConfig是一个对象,用来配置工作簿的类型等,格式如下:
```js
{
bookType: 'xlsx', // 工作簿的类型
bookSST: false,
type: 'binary', // 输出数据类型
}
```
**四、生成workbook object**
```js
const wb = {SheetNames: [], Sheets: {}}
exportData.forEach( (data, index) => {
const output = Object.assign({}, _headers, _data);
const outputPos = Object.keys(output);
// 设置单元格宽度
const colWidth = data.header.map( item => { wpx: item.width || 60 })
const merges = data.workSheetConfig && data.workSheetConfig.merges
const freeze = data.workSheetConfig && data.workSheetConfig.freeze
// 处理sheet名
wb.SheetNames[index] = data.sheetName ? data.sheetName : 'Sheet' + (index + 1)
// 处理sheet数据
wb.Sheets[wb.SheetNames[index]] = Object.assign({},
output, //导出的内容
{
'!ref': `${outputPos[0]}:${outputPos[outputPos.length - 1]}`,
'!cols': [...colWidth],
'!merges': merges ? [...merges] : undefined,
'!freeze': freeze ? [...freeze] : undefined,
}
)
})
```
**五、new Blob转换成二进制类型的对象**
```js
const tmpDown = new Blob(
[ s2ab( XLSX.write(wb, workBookConfig))],
{ type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'}
)
// 字符串转字符流---转化为二进制的数据流
function s2ab(s) {
if (typeof ArrayBuffer !== 'undefined') {
const buf = new ArrayBuffer(s.length);
const view = new Uint8Array(buf);
for (let i = 0; i !== s.length; ++i) view[i] = s.charCodeAt(i) & 0xff;
return buf;
} else {
const buf = new Array(s.length);
for (let i = 0; i !== s.length; ++i) buf[i] = s.charCodeAt(i) & 0xff;
return buf;
}
}
```
**六、下载Excel**
```js
downExcel(tmpDown, `${fileName + '.'}${workBookConfig.bookType == 'biff2' ? 'xls' : workBookConfig.bookType}`)
function downExcel(obj, fileName) {
const a_node = document.createElement('a');
a_node.download = fileName;
// 兼容ie
if ('msSaveOrOpenBlob' in navigator) {
window.navigator.msSaveOrOpenBlob(obj, fileName);
} else {
// 新的对象URL指向执行的File对象或者是Blob对象.
a_node.href = URL.createObjectURL(obj);
}
a_node.click();
setTimeout(() => {
URL.revokeObjectURL(obj);
}, 100);
}
```
**七、默认配置**
```js
// 默认工作簿配置
const defaultWorkBook = {
bookType: 'xlsx',
bookSST: false,
type: 'binary',
}
// 默认样式配置
const borderAll = { //单元格外侧框线
top: {
style: 'thin',
},
bottom: {
style: 'thin'
},
left: {
style: 'thin'
},
right: {
style: 'thin'
}
};
const defaultCellStyle = {
headerStyle: { // 表头区域样式配置
border: borderAll
font: { name: '宋体', sz: 11, italic: false, underline: false, bold: true },
alignment: { vertical: 'center', horizontal: 'center'},
fill: { fgColor: { rgb: 'FFFFFF' } },
},
dataStyle: { // 内容样式配置
border: borderAll
font: { name: '宋体', sz: 11, italic: false, underline: false },
alignment: { vertical: 'center', horizontal: 'left', wrapText: true},
fill: { fgColor: { rgb: 'FFFFFF' } },
}
}
```
**八、完整代码**
```js
import XLSX from 'xlsx-style'
// 默认工作簿配置
const defaultWorkBook = {
bookType: 'xlsx',
bookSST: false,
type: 'binary',
}
// 默认样式配置
const borderAll = {
top: {
style: 'thin',
},
bottom: {
style: 'thin',
},
left: {
style: 'thin',
},
right: {
style: 'thin',
},
}
const defaultCellStyle = {
// 表头区域样式配置
headerStyle: {
border: borderAll,
font: { name: '宋体', sz: 11, italic: false, underline: false, bold: true },
alignment: { vertical: 'center', horizontal: 'center' },
fill: { fgColor: { rgb: 'FFFFFF' } },
},
// 内容区域样式配置
dataStyle: {
border: borderAll,
font: { name: '宋体', sz: 11, italic: false, underline: false },
alignment: { vertical: 'center', horizontal: 'left', wrapText: true },
fill: { fgColor: { rgb: 'FFFFFF' } },
},
}
function exportExcel(exportData, workBookConfig = defaultWorkBook, fileName = '未命名') {
if (!(exportData && exportData.length)) {
return
}
// 定义工作簿对象
const wb = { SheetNames: [], Sheets: {} }
exportData.forEach((data, index) => {
// 处理sheet表头
const _header = data.header.map((item, i) =>
Object.assign({}, {
key: item.dataIndex,
title: item.title,
// 定位单元格
position: getCharCol(i) + 1,
// 设置表头样式
s: data.cellConfig && data.cellConfig.headerStyle ? data.cellConfig.headerStyle : defaultCellStyle.headerStyle,
})
).reduce((prev, next) =>
Object.assign({}, prev, {
[next.position]: { v: next.title, key: next.key, s: next.s },
}), {}
)
// 处理sheet内容
const _data = {}
data.dataSource.forEach((item, i) => {
data.header.forEach((obj, index) => {
const key = getCharCol(index) + (i + 2)
const key_t = obj.dataIndex
_data[key] = {
v: item[key_t],
s: data.cellConfig && data.cellConfig.dataStyle ? data.cellConfig.dataStyle : defaultCellStyle.dataStyle,
}
})
})
const output = Object.assign({}, _header, _data)
const outputPos = Object.keys(output)
// 设置单元格宽度
const colWidth = data.header.map(item => { return { wpx: item.width || 80 } })
const merges = data.workSheetConfig && data.workSheetConfig.merges
const freeze = data.workSheetConfig && data.workSheetConfig.freeze
// 处理sheet名
wb.SheetNames[index] = data.sheetName ? data.sheetName : 'Sheet' + (index + 1)
// 处理sheet数据
wb.Sheets[wb.SheetNames[index]] = Object.assign({},
output, // 导出的内容
{
'!ref': `${outputPos[0]}:${outputPos[outputPos.length - 1]}`,
'!cols': [...colWidth],
'!merges': merges ? [...merges] : undefined,
'!freeze': freeze ? [...freeze] : undefined,
}
)
})
// 转成二进制对象
const tmpDown = new Blob(
[s2ab(XLSX.write(wb, workBookConfig))],
{ type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }
)
// 下载表格
downExcel(tmpDown, `${fileName + '.'}${workBookConfig.bookType === 'biff2' ? 'xls' : workBookConfig.bookType}`)
}
/**
* 生成ASCll值 从A开始
* @param {*} n
*/
function getCharCol(n) {
if (n > 25) {
let s = ''
let m = 0
while (n > 0) {
m = n % 26 + 1
s = String.fromCharCode(m + 64) + s
n = (n - m) / 26
}
return s
}
return String.fromCharCode(65 + n)
}
// 字符串转字符流---转化为二进制的数据流
function s2ab(s) {
if (typeof ArrayBuffer !== 'undefined') {
const buf = new ArrayBuffer(s.length)
const view = new Uint8Array(buf)
for (let i = 0; i !== s.length; ++i) { view[i] = s.charCodeAt(i) & 0xff }
return buf
} else {
const buf = new Array(s.length)
for (let i = 0; i !== s.length; ++i) { buf[i] = s.charCodeAt(i) & 0xff }
return buf
}
}
function downExcel(obj, fileName) {
const a_node = document.createElement('a')
a_node.download = fileName
// 兼容ie
if ('msSaveOrOpenBlob' in navigator) {
window.navigator.msSaveOrOpenBlob(obj, fileName)
} else {
// 新的对象URL指向执行的File对象或者是Blob对象.
a_node.href = URL.createObjectURL(obj)
}
a_node.click()
setTimeout(() => {
URL.revokeObjectURL(obj)
}, 100)
}
export {
exportExcel,
}
```
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment