胖蔡叨叨叨
你听我说

前端图片压缩

xuao阅读(170)

使用VUE或者React框架加载HTML大图时极容易导致前端页面卡死报out of memory错误,报出这个错误还好,如果没有报错浏览器可能就无法操作了。

前端压缩

为保证前端页面正常渲染html大图,需要在html加载前压缩图片。前端解决这个问题有两种方案,一个是上传前压缩,一个是上传后压缩。上传前可以保证当前图片无论在哪里显示大小已经被压缩过,无需再去管理。上传后压缩可以保证服务器端拥有高清原图,显示前需要做一次压缩。
无论那种压缩,压缩方式基本一致

1.获取图片尺寸
2.计算压缩比率
3.通过canvas.toDataUrl 参数转化压缩图片
4.将base64字符转转成文件或者直接显示

上传前压缩

获取到本地文件后,通过FileReader将本地文件写入到内存中

// 获取文件图片的宽高
function getImageWHFromFile(file, fn, err) {
  try {
    // 读取上传之后的文件对象
    const imageReader = new FileReader()
    // 将图片的内存url地址添加到FileReader中
    imageReader.readAsDataURL(file)
    // 当图片完全加载到FileReader对象中后,触发下面的事件
    imageReader.addEventListener('loadend', function(e) {
    // 获取上传到本机内存中的图片的url地址
      const imageSrc = e.target.result
      // 调用计算图片大小的方法
      calculateImageSize(imageSrc).then(function({ image, width, height }) {
      // 得到图片的宽和高
        fn(image, width, height)
      }).catch(e => {
        err()
      })
    })
    // 可以做一些其他状态监听
  } catch (e) {
    err(file)
  }
}

计算内存中图片的宽高

// 根据图片地址获取图片的宽和高
function calculateImageSize(src) {
  return new Promise(function(resolve, reject) {
    const image = new Image()
    image.addEventListener('load', function(e) {
      resolve({
        image: image,
        width: e.target.width,
        height: e.target.height,
      })
    })
    image.addEventListener('error', function(e) {
      reject(Error('获取尺寸失败'))
    })
    // 将图片的url地址添加到图片地址中
    image.setAttribute('crossOrigin', 'Anonymous')
    image.src = src
  })
}

得到图片宽高后就可以计算压缩比率,通过图片真水的宽高和期望的宽高计算压缩率,基本上可以得到合适的图片尺寸

// 计算压缩比率
function getRatio(oriW, oriH, width, height) {
  const proW = (oriW / width).toFixed(5)
  const proH = (oriH / height).toFixed(5)
  return proW < proH ? proW : proH
}

得到压缩比率就可以执行压缩,压缩使用canvas实现

// 缩放img对象中的图片
function compressImageToBase64(image, quality) {
  const canvas = document.createElement('canvas')
  const context = canvas.getContext('2d')
  const imageWidth = image.width * quality
  const imageHeight = image.height * quality
  canvas.width = imageWidth
  canvas.height = imageHeight
  context.drawImage(image, 0, 0, imageWidth, imageHeight)
  return canvas.toDataURL('image/jpeg', quality)
}

压缩完得到图片的base64(DataURL)数据,可以用这个数据直接输出到img标签显示,或者继续加工生成图片文件
将base64转成blob对象

// DataURL转Blob对象
function dataURLToBlob(dataurl) {
  const arr = dataurl.split(',')
  const mime = arr[0].match(/:(.*?);/)[1]
  const bstr = atob(arr[1])
  let n = bstr.length
  const u8arr = new Uint8Array(n)
  while (n--) {
    u8arr[n] = bstr.charCodeAt(n)
  }
  return new Blob([u8arr], { type: mime })
}

将Blob转成File,调用接口用FormData的形式上传文件

// 对图片进行压缩并返回文件格式
function compressToFile(image, quality) {
  const blob = dataURLToBlob(compressImageToBase64(image, quality))
  return new File([blob], new Date().getTime() + '.jpg')
}

到此,就得到了一个压缩后的文件对象

上传后压缩

上传后压缩的意思是,服务器返回一个图片url后,如果这个url指向的图片尺寸很大,比如:10M,这时候在浏览器中显示这个图片会导致页面卡死。我的解决方案是压缩后再显示,压缩方式大概与上传前压缩类似,从calculateImageSize方法开始计算图片尺寸,然后进行压缩,得到base64数据后回调显示这个图片。这里存在一个问题:压缩开始后chrome的控制台无法打开。原因则是image加载图片后会在控制台的Network中输出base64格式的日志,这个日志占用了太大的内存。

服务器压缩

服务器压缩是最好的且能满足产品对大图要求的解决方案。前端上传原图,服务器压缩后生成两个地址,一个缩略图一个原图,缩略图供前端显示,原图供下载。+
附全部代码供参考

/**
 * @param {待压缩图片文件} file
 * @param {异步回调} fn
 * @param {压缩期望尺寸} size [width ,height]
 */
const fileCompressToFile = function(file, fn, size) {
  const _size = getComperssSize(size)
  if (_size === null) {
    fn(file)
    return
  }
  // 开启后执行压缩
  getImageWHFromFile(file, (image, width, height) => {
    const pro = getRatio(_size[0], _size[1], width, height)
    if (pro >= 1) {
      fn(file)
      return
    }
    // 开始压缩
    fn(compressToFile(image, pro))
  }, () => {
    fn(file)
  })
}

// 通过url压缩图片,压缩成功返回base64
const urlCompressToBase64 = function(url, fn, size) {
  const _size = getComperssSize(size)
  if (_size === null) {
    fn(url)
    return
  }
  calculateImageSize(url).then(function({ image, width, height }) {
    const pro = getRatio(_size[0], _size[1], width, height)
    if (pro >= 1) {
      fn(url)
      return
    }
    // 开始压缩
    fn(compressImageToBase64(image, pro))
  }).catch(e => {
    console.error(e)
    fn(url)
  })
}

// 格式化压缩尺寸
function getComperssSize(size) {
  if (!size) {
    return null
  }
  const defaultSize = 500
  if (Array.isArray(size)) {
    return [size[0] || defaultSize, size[1] || defaultSize]
  }
  const pf = parseFloat(size)
  return [pf || defaultSize, pf || defaultSize]
}

// 计算压缩比率
function getRatio(oriW, oriH, width, height) {
  const proW = (oriW / width).toFixed(5)
  const proH = (oriH / height).toFixed(5)
  return proW < proH ? proW : proH
}

// 缩放img对象中的图片
function compressImageToBase64(image, quality) {
  const canvas = document.createElement('canvas')
  const context = canvas.getContext('2d')
  const imageWidth = image.width * quality
  const imageHeight = image.height * quality
  canvas.width = imageWidth
  canvas.height = imageHeight
  context.drawImage(image, 0, 0, imageWidth, imageHeight)
  return canvas.toDataURL('image/jpeg', quality)
}

// 对图片进行压缩并返回文件格式
function compressToFile(image, quality) {
  const blob = dataURLToBlob(compressImageToBase64(image, quality))
  return new File([blob], new Date().getTime() + '.jpg')
}

// DataURL转Blob对象
function dataURLToBlob(dataurl) {
  const arr = dataurl.split(',')
  const mime = arr[0].match(/:(.*?);/)[1]
  const bstr = atob(arr[1])
  let n = bstr.length
  const u8arr = new Uint8Array(n)
  while (n--) {
    u8arr[n] = bstr.charCodeAt(n)
  }
  return new Blob([u8arr], { type: mime })
}

// 获取文件图片的宽高
function getImageWHFromFile(file, fn, err) {
  try {
    // 读取上传之后的文件对象
    const imageReader = new FileReader()
    // 将图片的内存url地址添加到FileReader中
    imageReader.readAsDataURL(file)
    // 当图片完全加载到FileReader对象中后,触发下面的事件
    imageReader.addEventListener('loadend', function(e) {
    // 获取上传到本机内存中的图片的url地址
      const imageSrc = e.target.result
      // 调用计算图片大小的方法
      calculateImageSize(imageSrc).then(function({ image, width, height }) {
      // 得到图片的宽和高
        fn(image, width, height)
      }).catch(e => {
        err()
      })
    })
    // 可以做一些其他状态监听
  } catch (e) {
    err(file)
  }
}

// 根据图片地址获取图片的宽和高
function calculateImageSize(src) {
  return new Promise(function(resolve, reject) {
    const image = new Image()
    image.addEventListener('load', function(e) {
      resolve({
        image: image,
        width: e.target.width,
        height: e.target.height,
      })
    })
    image.addEventListener('error', function(e) {
      reject(Error('获取尺寸失败'))
    })
    // 将图片的url地址添加到图片地址中
    image.setAttribute('crossOrigin', 'Anonymous')
    image.src = src
  })
}

export {
fileCompressToFile,
  urlCompressToBase64,
}

简单了解面向对象编程(oop)和常见的几种设计模式

胖蔡阅读(204)

背景:

1、软件设计开发过程中疑难问题:

  • 软件复杂庞大
  • 难以维护阶
  • 版本迭代需求变更

软件设计开发中存在很多其他的问题,上面只是从程序开发和设计的角度看到的部分问题。需求解决上面软件开发中的问题,就要求我们编写(设计)的软件具有很好的可读性、可维护性和可扩展性。我们需要保证代码具有高内聚低耦合,这里我们使用了OOP编程思想。

oop四大基本特性:

  • 抽象:提取现实世界中某事物的关键特性,为该事物构建模型的过程。对同一事物在不同的需求下,需要提取的特性可能不一样。得到的抽象模型中一般包含:属性(数据)和操作(行为)。这个抽象模型我们称之为类。对类进行实例化得到对象。
  • 封装:封装可以使类具有独立性和隔离性;保证类的高内聚。只暴露给类外部或者子类必须的属性和操作。类封装的实现依赖类的修饰符(public、protected和private等)
  • 继承:对现有类的一种复用机制。一个类如果继承现有的类,则这个类将拥有被继承类的所有非私有特性(属性和操作)。这里指的继承包含:类的继承和接口的实现。
  • 多态:多态是在继承的基础上实现的。多态的三个要素:继承、重写和父类引用指向子类对象。父类引用指向不同的子类对象时,调用相同的方法,呈现出不同的行为;就是类多态特性。多态可以分成编译时多态和运行时多态。

24种设计模式: 273dd8113f18a13 常见的几种设计模式:

1、构造函数模式

/**
 * 构造一个动物的函数 
 */
function Animal(name, color){
    this.name = name;
    this.color = color;
    this.getName = function(){
        return this.name;
    }
}
// 实例一个对象
var cat = new Animal('', '白色');
console.log( cat.getName() );

2、工厂模式

/**
 * 工厂模式
 */
function Animal(opts){
    var obj = new Object();
    obj.name = opts.name;
    obj.color = opts.color;
    obj.getInfo = function(){
        return '名称:'+obj.name +', 颜色:'+ obj.color;
    }
    return obj;
}
var cat = Animal({name: '波斯猫', color: '白色'});
cat.getInfo();

3、模块模式

/**
 * 模块模式 = 封装大部分代码,只暴露必需接口
 */
var Car = (function(){
    var name = '法拉利';
    function sayName(){
        console.log( name );
    }
    function getColor(name){
        console.log( name );
    }
    return {
        name: sayName,
        color: getColor
    }
})();
Car.name();
Car.color('红色');

4、混合模式

/**
 * 混合模式 = 原型模式 + 构造函数模式
 */
function Animal(name, color){
    this.name = name;
    this.color = color;
    console.log( this.name  +  this.color)
}

Animal.prototype.getInfo = function(){
    console.log('名称:'+ this.name);
}

function largeCat(name, color){
    Animal.call(null, name, color);
    this.color = color;
}

largeCat.prototype = create(Animal.prototype);
function create (parentObj){
    function F(){}
    F.prototype = parentObj;
    return new F();
};

largeCat.prototype.getColor = function(){
    return this.color;
}
var cat = new largeCat("Persian", "白色");
console.log( cat )

5、单例模式

/**
 * 在执行当前 Single 只获得唯一一个对象
 */
var Single = (function(){
    var instance;
    function init() {
        //define private methods and properties
        //do something
        return {
            //define public methods and properties
        };
    }

    return {
        // 获取实例
        getInstance:function(){
            if(!instance){
                instance = init();
            }
            return instance;
        }
    }
})();

var obj1 = Single.getInstance();
var obj2 = Single.getInstance();

console.log(obj1 === obj2);

6、发布订阅模式

/**
 * 发布订阅模式
 */
var EventCenter = (function(){
    var events = {};
    /*
    {
      my_event: [{handler: function(data){xxx}}, {handler: function(data){yyy}}]
    }
    */
    // 绑定事件 添加回调
    function on(evt, handler){
        events[evt] = events[evt] || [];
        events[evt].push({
            handler:handler
        })
    }
    function fire(evt, arg){
        if(!events[evt]){
            return 
        }
        for(var i=0; i < events[evt].length; i++){
            events[evt][i].handler(arg);
        }
    }
    function off(evt){
        delete events[evt];
    }
    return {
        on:on,
        fire:fire,
        off:off
    }
}());

var number = 1;
EventCenter.on('click', function(data){
    console.log('click 事件' + data + number++ +'');
});
EventCenter.off('click');   //  只绑定一次
EventCenter.on('click', function(data){
    console.log('click 事件' + data + number++ +'');
});

EventCenter.fire('click', '绑定');

React中Hooks源码简单解析

胖蔡阅读(196)

使用Hooks时的疑惑

Hooks的出现让我们对Function Component逐步拥有了对标 Class Component的特性,比如私有状态以及生命周期函数等。useStateuseReducer这两个Hooks让我们可以在Function Component里使用到私有状态。而useState其实相当于简化版的useReducer,下面就将这两个放到一起简单介绍下。

1、useState使用示例

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使用示例

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同样也是返回当前最新的状态,并返回一个用来更新数据的方法。

在使用这两个方法的时候也许我们会想过这样的问题:

const [age, setAge] = useState(initialAge);
const [name, setName] = useState(initialName);

问题1、React内部如何区分这两个状态

Function Component不像Class Component那样可以将私有状态挂载到类实例中并通过对应的key来指向对应的状态,而且每次的页面刷新或者组件的重新渲染都会使得Function重新执行一遍。所以在React中必定有一种机制来区分这些Hooks

const [age, setAge] = useState(initialAge);
// 或
const [state, dispatch] = useReducer(reducer, initialState);

问题2、React是如何在每次重新渲染之后都能返回最新的状态

Class Component因为自身的特点可以将私有状态持久化的挂载到类实例上,每时每刻保存的都是最新的值。而 Function Component 由于本质就是一个函数,并且每次渲染都会重新执行。所以React必定拥有某种机制去记住每一次的更新操作,并最终得出最新的值返回。当然还会有其他的一些问题,比如这些状态究竟存放在哪?为什么只能在函数顶层使用Hooks而不能在条件语句等里面使用Hooks?

源码中找出上面问题的答案

先来了解useState以及useReducer的源码实现,并从中解答我们在使用Hooks时的种种疑惑。首先我们从源头开始:

import React, { useState } from 'react';

在项目中我们通常会以这种方式来引入useState方法,被我们引入的这个useState方法是什么样子的呢?其实这个方法就在源码 packages/react/src/ReactHook.js 中。

// 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文件:

import type { Dispacther } from 'react-reconciler/src/ReactFiberHooks';
const ReactCurrentDispatcher = {
  current: (null: null | Dispatcher),
};
export default ReactCurrentDispatcher;

好吧,它继续将我们带向 react-reconciler/src/ReactFiberHooks.js这个文件。那么我们继续前往这个文件。

// 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都有两个定义:

// 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
};

从这里可以看出,我们的HooksMount阶段和Update阶段的逻辑是不一样的。在Mount阶段和Update阶段他们是两个不同的定义。我们先来看Mount阶段的逻辑。在看之前我们先思考一些问题。React Hooks需要在Mount阶段做什么呢?就拿我们的useStateuseReducer来说:

  • 我们需要初始化状态,并返回修改状态的方法,这是最基本的。
  • 我们要区分管理每个Hooks。
  • 提供一个数据结构去存放更新逻辑,以便后续每次更新可以拿到最新的值。

先看下mountState的实现。

// 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的类型定义中找到答案。

// 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是通过链表来联系的,上一个Hooksnext指向下一个Hooks。这些Hooks节点是怎么利用链表数据结构串联在一起的呢?相关逻辑就在每个具体mount 阶段 Hooks函数调用的 mountWorkInProgressHook方法里:

// 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方法,比如useStatemountState就会调用mountWorkInProgressHook 来创建一个Hook节点,并把它添加到Hooks链表上。比如我们的这个例子:

const [age, setAge] = useState(initialAge);
const [name, setName] = useState(initialName);
useEffect(() => {})

那么在mount阶段,就会生产如下图这样的单链表:
8761a76089a8f73

返回最新的值

useStateuseReducer都是使用了一个queue链表来存放每一次的更新。以便后面的update阶段可以返回最新的状态。每次我们调用dispatchAction方法的时候,就会形成一个新的updata对象,添加到queue链表上,而且这个是一个循环链表。可以看一下dispatchAction方法的实现:

// 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方法,比如setAgesetName。就会创建一个保存着此次更新信息的update对象,添加到更新链表queue上。然后每个Hooks节点就会有自己的一个queque。比如假设我们执行了下面几个语句:

1
2
3
setAge(19);
setAge(20);
setAge(21);

那么Hooks链表就会变成这样:
19cb9bee9b880ac
Hooks节点上面,会如上图那样,通过链表来存放所有的历史更新操作。以便在update阶段可以通过这些更新获取到最新的值返回。这就是在第一次调用useStateuseReducer之后,每次更新都能返回最新值的原因。再来看看mountReducer,你会发现和mountState几乎一摸一样,只是状态的初始化逻辑有那么一点区别。毕竟useState其实就是阉割版的useReducer。这里不详细介绍mountReducer了。

// 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阶段,也就是看一下我们的useStateuseReducer是如何利用现有的信息,去返回最新的最正确的值的。先来看一下useStateupdate阶段的代码也就是updateState

// react-reconciler/src/ReactFiberHooks.js
function updateState(initialState) {
  return updateReducer(basicStateReducer, initialState);
}

可以看到,updateState底层调用的其实就调用updateReducer,因为我们调用useState的时候,并不会传入reducer,所以这里会默认传递一个basicStateReducer进去。我们先看看这个basicStateReducer

// react-reconciler/src/ReactFiberHooks.js
function basicStateReducer(state, action){
  return typeof action === 'function' ? action(state) : action;
} 

在使用useState(action)的时候,action通常会是一个值,而不是一个方法。所以baseStateReducer要做的其实就是将这个action返回。来继续看一下updateReducer的逻辑:

// 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阶段,也就是我们组件第二次第三次。。执行到useStateuseReducer的时候,会遍历update对象循环链表,执行每一次更新去计算出最新的状态来返回,以保证我们每次刷新组件都能拿到当前最新的状态。useStatereducerbaseStateReducer,因为传入的update.action为值,所以会直接返回update.action,而useReducer 的reducer是用户定义的reducer,所以会根据传入的action和每次循环得到的newState逐步计算出最新的状态。
df6d292b56657de

useState/useReducer 小总结

  • React 如何管理区分Hooks?
    React通过单链表来管理Hooks
    按Hooks的执行顺序依次将Hook节点添加到链表中
  • useState和useReducer如何在每次渲染时,返回最新的值?
    每个Hook节点通过循环链表记住所有的更新操作
    在update阶段会依次执行update循环链表中的所有更新操作,最终拿到最新的state返回
  • 为什么不能在条件语句等中使用Hooks?
    链表!
    12eaca0f5533f76
    如图所示,我们在mount阶段调用了useState('A')useState('B')useState('C'),如果我们将useState('B') 放在条件语句内执行,并且在update阶段中因为不满足条件而没有执行的话,那么没法正确的重回Hooks链表中获取信息。React也会报错。

Hooks链表位置

现在已经了解了React 通过链表来管理 Hooks,同时也是通过一个循环链表来存放每一次的更新操作,得以在每次组件更新的时候可以计算出最新的状态返回给我们。那么我们这个Hooks链表又存放在那里呢?理所当然的我们需要将它存放到一个跟当前组件相对于的地方。那么很明显这个与组件一一对应的地方就是我们的FiberNode
fac2386306e69b9
如图所示,组件构建的Hooks链表会挂载到FiberNode节点的memoizedState上面去。
1b669f187f080be

useEffect

先看下useEffect是如何工作的

function PersionInfo () {
  const [age, setAge] = useState(18);
  useEffect(() =>{
      console.log(age)
  }, [age])
 const [name, setName] = useState('Dan');
 useEffect(() =>{
      console.log(name)
  }, [name])
  return (
    <>
      ...
    </>
  );
}

PersionInfo组件第一次渲染的时候会在控制台输出agename,在后面组件的每次update中,如果useEffect中的deps依赖的值发生了变化的话,也会在控制台中输出对应的状态,同时在unmount的时候就会执行清除函数(如果有)。React中是怎么实现的呢?其实很简单,在FiberNode中通过一个updateQueue来存放所有的effect,然后在每次渲染之后依次执行所有需要执行的effectuseEffect 也分为mountEffectupdateEffect

mountEffect

// 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方法中赋值到fiberNodeupdateQueue上。

// react-reconciler/src/ReactFiberHooks.js
// 简化去掉特殊逻辑
export function renderWithHooks() {
   const renderedWork = currentlyRenderingFiber;
   renderedWork.updateQueue = componentUpdateQueue;
}

也就是在mount阶段我们所有的effect都以链表的形式被挂载到了fiberNode上。然后在组件渲染完毕之后,React就会执行updateQueue中的所有方法。
35268517ff3a83f

updateEffect

// 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的执行。

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 小总结

21fa0795018c8a8

useEffect做了什么?
FiberNdoe节点中又一个updateQueue链表来存放所有的本次渲染需要执行的effect
mountEffect阶段和updateEffect阶段会把effect 挂载到updateQueue上。
updateEffect阶段,deps没有改变的effect会被打上NoHookEffect tagcommit阶段会跳过该Effect
到此为止,useState/useReducer/useEffect源码也阅读完毕了,相信有了这些基础,剩下的Hooks的源码阅读不会成问题,最后放上完整图示:
cf02979f50098ea

性能优化—Preload&Prefetch

胖蔡阅读(203)

在preload和prefetch之前,我们一般根据编码需求通过link或者script标签引入页面渲染和交互所依赖的js和css等;然后这些资源由浏览器决定优先级进行加载、解析、渲染等。

然而,有些情况我们需要某些依赖在浏览器进入渲染的主进程之前就希望被加载,但是实际资源加载的状况并不受我们控制,而是根据浏览器内部决定资源的优先级等状况进行加载的。即使我们将需要的资源按照希望的那样放在期望的位置,但是由于资源的加载和解析,尤其是js的加载、解析和编译是阻塞式的,因此往往还是达不到预期。或者,有些资源是我们后边需要的,我们希望在需要它的时候在进行解析和执行,并不想让它的加载影响其他资源的加载及首屏渲染时间。

preload和prefetch的出现为我们提供了可以更加细粒度地控制浏览器加载资源的方法。

<link ref='preload'>
<link ref='prefetch'>

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}`);
    });
}

jQuery从$开始

胖蔡阅读(257)

jQuery requires a window with a document.jQuery版本3.6.0

使用jQuery的第一行代码总是从$(document).ready(function(){});或者$(function(){});开始,我从jQuery执行过程去了解$是怎么被识别并被使用的。

jQuery 入口函数

jQuery根据不同的js运行时环境将jQuery实例挂载到不同属性上。如果是浏览器引用,则挂载到window的$上,如果是require方式在例如node.js环境引用,则需要判断当前环境的文档流在哪一层(写此博客时还没有搭建node环境,这里没有做验证,只是想法猜测),jQuery需要运行在有文档流的环境中。

javascript
(function(global, factory){
	if ( typeof module === "object" && typeof module.exports === "object" ) {
		module.exports = global.document ?
			factory( global, true ) :
			function( w ) {
				if ( !w.document ) {
					throw new Error( "jQuery requires a window with a document" );
				}
				return factory( w );
			};
	} else {
		factory( global );
	}
	
})(typeof window !== "undefined" ? window : this, function( window, noGlobal ) {

	var jQuery = function( selector, context ) {
		return ...;
	};
	
	var _jQuery = window.jQuery, _$ = window.$;

    jQuery.noConflict = function( deep ) {
    	if ( window.$ === jQuery ) {
	    	window.$ = _$;
	    }

    	if ( deep && window.jQuery === jQuery ) {
	    	window.jQuery = _jQuery;
    	}

	return jQuery;
    };

	if ( typeof noGlobal === "undefined" ) {
		window.jQuery = window.$ = jQuery;
	};
	
	return jQuery;
});

jQuery提供了noConflict方法,解决$可能与其他框架同名产生冲突的问题,同时如果jQuery运行在没有window属性的环境中,也不能使用$。可以通过noConflict方法重新定义一个属性。

jQuery初始化

预告为了防止文档在没有完全加载之前运行jQuery代码,即在Dom加载完成后才可以对Dom进行操作,jQuery所有函数都位于document ready函数中。

Js 过滤富文本html标签提取纯文本内容

胖蔡阅读(557)

需要将富文本中的标签和特殊字符过滤提取纯文本内容,就上网找了下,但是发现都不是我想要的。所以找了下之前自己写的过滤方法,在这里记录下,方便之后使用查找。功能比较简单,就直接上代码。

 

f445b9f60a137c1-1

 

/**
 * 将富文本转换为纯文本内容
 * @function
 * @param {*} html  富文本内容
 * @returns {string} 返回纯文本内容、
 */

export function getSimpleText(html) {
  const re1 = new RegExp("<.+?>", "g"); // 匹配html标签的正则表达式,"g"是搜索匹配多个符合的内容
  const arrEntities = { 'lt': '<', 'gt': '>', 'nbsp': ' ', 'amp': '&', 'quot': '"', 'ldquo': '“', 'mdash': '—', "rdquo": '”' };
  return html.replace(re1, '').replace(/&(lt|gt|nbsp|amp|quot|ldquo|mdash|rdquo);/ig, function (all, t) {
    return arrEntities[t];
  });
}

 

 

过滤替换单个标签可以通过如下的方法实现,我这里是用于替换 img 标签,大家可以根据需要将img标签改为需要替换查找的标签。

 

/**
 * 替换富文本中的image标签,使用占位符显示,hover弹框显示
 * @param {*} html
 */

export function replaceHtmlImages(html) {
  return html.replace(/<img [^>]*src=['"]([^'"]+)[^>]*>/gi, function (match, capture) {
    return `<img class="hoverImg" onClick="openImg('${capture}')"  style="cursor:pointer;height:18px !important;width:18px !important;"  src="${picIcon}"  alt="图片" />`;
  });
}

 

大家自取自用,多多益善~

使用slatejs实现自定义元素

胖蔡阅读(274)

 

背景

项目需要,需要使用可实现自定义的富文本编辑器。所以就找了市面上的一些富文本编辑器进行对比选型。由于我们采用的是react实现的前端开发,本次需要对于富文本编辑器需要有个较为深度的自定义。所以,我对市面上常用的几款富文本编辑器做了一个选型对比表格。

 

框架
功能
实操
基础文本编辑
表格操作
表格语义化
上手难度
风险项
优势
是否付费
Draft.js
支持
支持
支持,自定义组件【契合度高】
中度
1、操作栏需要自己绘制,适配,基础功能非开箱即用
2、不支持osx按键绑定
3、UI绘制、适配可能比较耗时
4、不兼容html互转
5、复杂结构【table】编辑器会卡顿
1、facebook自研,支持react,组件化较高
2、插件支持
3、数据视图分离
Slate.js
支持
支持
支持
中度
1、对外复制粘贴功能不完善
2、slate-react实现基于hook,依赖hook
1、核心包与页面渲染包分离,支持前端框架的自定义
2、插件支持
3、支持react
Quill.js
支持
支持,需要三方插件
不支持
一般
1、富文本复制存在丢失问题
2、功能定制较为有限
3、不支持插件
1、有预设样式
TinyMCE
支持
支持
支持
中度
1、不支持react组件化,需要兼容实现,与vue更友好
2、2020年开始基于slate进行富文本的开发
3、高级功能需要收费,其中包括表格
1、比较成熟
2、预设样式较为美观
3、插件支持
有免费版和商用版
KendoReact
支持
支持
支持
/
需要付费,首次使用可以申请一个免费期限
1、高性能
2、高度可定制
3、插件支持
4、react支持

 

综合各方面元素,最终决定了选择slatejs作为富文本开发框架来帮助实现功能。

 

使用

既然选定了slatejs来作为富文本编辑器。slatejs怎么怎么优美,又是怎么怎么强大,咱先暂时抛开,看看如何去使用才是王道。

安装

常规第一步,不论啥框架,先把依赖安装了再说

$ npm i slate slate-react --save-dev // 或者
$ yarn add slate slate-react -D

基于react去应用了,react和react-dom安装就不说了,忘记装的,自己装下。

 

 

使用

这里,我就不分步骤一个个放代码了,给个终结的,再简单介绍几句就完事了。你好我好大家都好。

import React, { useState, useCallback } from 'react'
import { createEditor, Editor, Transforms } from 'slate';
import { Slate, Editable, withReact } from 'slate-react';

const initialValue = [   // 设置操作栏初始配置
  {
    type: 'paragraph',
    children: [{
      text: 'A line of text in a paragraph.'
    }],

  },

]

const CodeElement = (props) => {   // 自定义格式化代码组件
  return (
    <pre {...props.attributes}>
      <code>{props.children}</code>
    </pre>);
};

const DefaultElement = (props) => { return <p {...props.attributes}>{props.children}</p>; };


const SlateEditor: React.FC = (props) => {
  const editor = useMemo(() => withReact(createEditor()), []);
  const renderElement  useCallback((props) => {
    switch (props.element.type) {
      case 'code':
        return <CodeElement {...props} />;
      default:
        return <DefaultElement {...props} />;
    }
  }, []);

  return (
    <Slate editor={editor} value={initialValue}>
      <Editable
        onKeyDown={(event) => {
          if (event.key === '`' && event.ctrlKey) {
            event.preventDefault();
            Transforms.setNodes(
              editor,
              { type: 'code' },
              { match: (n) => Editor.isBlock(editor, n) },
            );
          }
        }
        }
      />
    </Slate>
  );
};
如上通过Slate 配置 onKeyDown来创建富文本的句柄监听按键实践,通过Tranforms添加自定义组件CodeElement。

你需要知道的JavaScript数据类型

胖蔡阅读(257)

JavaScript 是一种解释型语言,JavaScript指令以纯文本形式传递给浏览器,然后依次执行。因为不需要执行编译成计算机的二进制文件执行,使得JavaScript语言更加便于阅读、编写、测试。本篇文章主要是对JavaScript中的数据类型进行一个整理分享。

 

JavaScript属于”宽松类型”的编程语言,这也意味着JavaScript中的变量在不同的场合可以被解释为不同的类型。在JavaScript的世界里,不必如Java等其他语言那样需要事先申明变量类型,JavaScript的解释器会自己根据场景做出自己的判断。一般意义上,我们将JavaScript的类型分为如下这六类:

  • Number类型
  • Boolean 类型
  • String类型
  • Null
  • Undefined
  • Object

为了方便确认变量的数据类型,JS中提供了typeof操作符来检测当前变量的数据类型:

typeof 300
< 'number'

 

String 类型

string类型是一串字符组成,字符串被引号包裹,可以选择使用单引号或双引号包裹:

var testString = 'i'm a string~'
var testString = "haha~"

字符串还有些特殊的文本通过转义符\来转换显示,常见的转义符号如下:

符号 说明
\0 空字符
\' 单引号
\" 双引号
\\ 反斜杠
\n 换行
\r 回车
\v 垂直制表符
\t 水平制表符
\b 退格
\f 换页
\uXXXX unicode 码
\u{X} … \u{XXXXXX} unicode codepoint
\xXX Latin-1 字符(x小写)

string也提供了很多很便捷的操作、方法等

  • length:获取当前字符串长度属性
  • replace:字符串替换方法方法
  • search:检索字符串指定子串方法
  • lastIndexOf、indexOf:字符串索引查找方法
  • slice、substring 、substr:字符串截取方法
  • split:字符串拆分方法

 

Number 类型

数字类型是比较宽泛的,其实在JS中我们可以把Number类型分为如下几种类型:

  • 整型
  • 浮点型
  • 进制【二进制、八进制、十进制、十六进制】
var a1 = 10   // 整型
var a2 =  1.1 // 浮点型
var a3 = 1001  // 十进制
var a4 = 0o77 // 八进制
var a5 = 0xAE // 十六进制
var a6 = 0b1101  //二进制

常用方法

  • toString:将数字作为字符串返回
  • toFixed:返回一个指定小数位的字符串
  • toExponential:数字进行舍入并且用指数表示法来表示,传递给改方法的参数制定了转换之后小数点后面的字符数字

 

Boolean 类型

bool类型的数据只有两个值:true(真)和false(假)。一般用于程序中控制逻辑判断。

var x = true;
var y = false;

需要注意的是,JS中把非0值作为true来处理,把0作为false来处理,除了0之外,如下这些值也会被JS当做false处理:

  • false
  • undefined(非字符串)
  • null
  • NaN
  • 0
  • “”(空字符串)

Null和Undefined 类型

JavaScript中有两个比较直接的类型:Null和undefined。它们就如它们字面展示的意思一样:空和未定义。通常情况下,我们会将null和undefined称为假值,意思是“并非完全是假的,但它们可以解释为假”。

 

Object类型

js中对象是一组属性与方法的集合。所以一般情况下,我们可以将Array、Object(相对于Array)、Function都可以理解为Object类型。

var  user = {
     age:29,
     sex:'male',
     name:'胖蔡'
 };
var colors = ['red','green','yellow','blue'];

var fun = function getColor(colors,index) {
    return colors[index];
};


 

使用docxjs实现前端的word生成

胖蔡阅读(570)

Docx 可以帮助我们实现ts/js代码生成*.docx文件的功能,它可以同时被运用于Node端和浏览器端。使用docx基本可以实现大多数Microsoft Word文档英文版的绝大数API操作,但由于Microsoft Word文档中英文版本的差异,对于中文版本的功能、属性控制还是得需要通过自定义xml的方式来实现。本篇文章简单介绍下如何使用docxjs实现word文档的生成。

2c26b65871ed768

可访问官网查看官方文档:https://docx.js.org/   。我始终坚持认为,官方文档是最能帮助开发者的。

安装

$ npm install -s docx

基本用法

import * as fs from "fs"; 
import { Document, Packer, Paragraph, TextRun } from "docx";

 // 文档类Document中包含章节数组sections,可向其中添加多个章节 
const doc = new Document({ 
      sections: [{ 
          properties: {}, 
          children: [ new Paragraph({ 
             children: [ new TextRun("Hello World"), 
                  new TextRun({ text: "Foo Bar", bold: true, }),
                  new TextRun({ text: "\tGithub is the best", bold: true, }), ], }), ], }], }); 

// 通过fs实现将docx从buffer流转化成*.docx文件

 Packer.toBuffer(doc).then((buffer) => { fs.writeFileSync("My Document.docx", buffer); }); // 完成下载


通过上方的示例,可以发现docx将整个文档拆分成多个区域块,然后通过组合、包含区域块的方式实现文档的输出。下面,简单介绍下docx中常用的几个模块类。

Document

Document可以说是docx梦开始的地方。其实,我们也可以将其理解为docx文档本生。一个docx文档有且仅有一个document生成。可以通过如下方式生成一个基本的document对象。
const doc = new docx.Document();
document很重要,包含的属性有如下:
  • creator
  • description
  • title
  • subject
  • keywords
  • lastModifiedBy
  • revision
  • externalStyles
  • styles
  • numbering
  • footnotes
  • hyperlinks
  • background
  • sections
这里重点介绍externalStyles、styles。

externalStyles

这个属性对于我们的中文用户来说尤为重要,这或许也是docx作者考虑可能属性覆盖不全,所以特意留下的一个自定义属性样式的窗口。功能可以说是及其省事。我们完全可以通过解压docx文档,将其中的xml属性配置文件拿出来做些微的修改来实现我们的功能。大致就像下面这样。
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:styles xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
    xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
    xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml"
    xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" mc:Ignorable="w14 w15">
    <w:docDefaults>
        <w:rPrDefault>
            <w:rPr>
                <w:rFonts w:asciiTheme="minorHAnsi" w:eastAsiaTheme="minorHAnsi" w:hAnsiTheme="minorHAnsi" w:cstheme="minorBidi"/>
                <w:sz w:val="22"/>
                <w:szCs w:val="22"/>
                <w:lang w:val="en-GB" w:eastAsia="en-US" w:bidi="ar-SA"/>
            </w:rPr>
        </w:rPrDefault>
        <w:pPrDefault>
            <w:pPr>
                <w:spacing w:after="160" w:line="259" w:lineRule="auto"/>
            </w:pPr>
        </w:pPrDefault>
    </w:docDefaults>

 

    <w:style w:type="paragraph" w:default="1" w:styleId="Normal">
        <w:name w:val="Normal"/>
        <w:qFormat/>
        <w:rsid w:val="000D2D99"/>
        <w:rPr>
            <w:rFonts w:ascii="MV Boli" w:hAnsi="MV Boli"/>
            <w:color w:val="C45911" w:themeColor="accent2" w:themeShade="BF"/>
        </w:rPr>
    </w:style>

    <w:style w:type="paragraph" w:styleId="Heading1">
        <w:name w:val="heading 1"/>
        <w:basedOn w:val="Normal"/>
        <w:next w:val="Normal"/>
        <w:link w:val="Heading1Char"/>
        <w:uiPriority w:val="9"/>
        <w:qFormat/>
        <w:rsid w:val="000D2D99"/>
        <w:pPr>
            <w:keepNext/>
            <w:keepLines/>
            <w:spacing w:before="240" w:after="0"/>
            <w:outlineLvl w:val="0"/>
        </w:pPr>
        <w:rPr>
            <w:rFonts w:ascii="Impact" w:eastAsiaTheme="majorEastAsia" w:hAnsi="Impact" w:cstheme="majorBidi"/>
            <w:color w:val="538135" w:themeColor="accent6" w:themeShade="BF"/>
            <w:sz w:val="32"/>
            <w:szCs w:val="32"/>
        </w:rPr>
    </w:style>

    <w:style w:type="character" w:default="1" w:styleId="DefaultParagraphFont">
        <w:name w:val="Default Paragraph Font"/>
        <w:uiPriority w:val="1"/>
        <w:semiHidden/>
        <w:unhideWhenUsed/>
    </w:style>

    <w:style w:type="table" w:default="1" w:styleId="TableNormal">
        <w:name w:val="Normal Table"/>
        <w:uiPriority w:val="99"/>
        <w:semiHidden/>
        <w:unhideWhenUsed/>
        <w:tblPr>
            <w:tblInd w:w="0" w:type="dxa"/>
            <w:tblCellMar>
                <w:top w:w="0" w:type="dxa"/>
                <w:left w:w="108" w:type="dxa"/>
                <w:bottom w:w="0" w:type="dxa"/>
                <w:right w:w="108" w:type="dxa"/>
            </w:tblCellMar>
        </w:tblPr>
    </w:style>

    <w:style w:type="table" w:customStyle="1" w:styleId="MyCustomTableStyle">
        <w:name w:val="My Custom TableStyle"/>
        <w:uiPriority w:val="99"/>
        <w:rsid w:val="00C36BA2"/>
        <w:pPr>
            <w:spacing w:after="0" w:line="240" w:lineRule="auto"/>
        </w:pPr>
        <w:rPr>
            <w:rFonts w:ascii="Comic Sans MS" w:hAnsi="Comic Sans MS"/>
            <w:szCs w:val="20"/>
            <w:lang w:val="en-US"/>
        </w:rPr>

        <w:tcPr>
            <w:shd w:val="clear" w:color="auto" w:fill="auto"/>
        </w:tcPr>

        <w:tblStylePr w:type="firstRow">

            <w:rPr>
                <w:b/>
                <w:bCs/>
                <w:color w:val="FFFFFF" w:themeColor="background1"/>
            </w:rPr>

            <w:tblPr/>

            <w:tcPr>
                <w:tcBorders>
                    <w:tl2br w:val="none" w:sz="0" w:space="0" w:color="auto"/>
                    <w:tr2bl w:val="none" w:sz="0" w:space="0" w:color="auto"/>
                </w:tcBorders>
                <w:shd w:val="clear" w:color="auto" w:fill="4472C4" w:themeFill="accent1"/>
            </w:tcPr>

        </w:tblStylePr>

    </w:style>

    <w:style w:type="numbering" w:default="1" w:styleId="NoList">
        <w:name w:val="No List"/>
        <w:uiPriority w:val="99"/>
        <w:semiHidden/>
        <w:unhideWhenUsed/>
    </w:style>

    <w:style w:type="paragraph" w:customStyle="1" w:styleId="CustomStyle">
        <w:name w:val="Custom Style"/>
        <w:basedOn w:val="Normal"/>
        <w:link w:val="CustomStyleChar"/>
        <w:rsid w:val="00B557A5"/>
        <w:rPr>
            <w:sz w:val="72"/>
        </w:rPr>
    </w:style>

    <w:style w:type="character" w:customStyle="1" w:styleId="CustomStyleChar">
        <w:name w:val="Custom Style Char"/>
        <w:basedOn w:val="DefaultParagraphFont"/>
        <w:link w:val="CustomStyle"/>
        <w:rsid w:val="00B557A5"/>
        <w:rPr>
            <w:rFonts w:ascii="MV Boli" w:hAnsi="MV Boli"/>
            <w:color w:val="C45911" w:themeColor="accent2" w:themeShade="BF"/>
            <w:sz w:val="72"/>
        </w:rPr>
    </w:style>

    <w:style w:type="character" w:customStyle="1" w:styleId="Heading1Char">
        <w:name w:val="Heading 1 Char"/>
        <w:basedOn w:val="DefaultParagraphFont"/>
        <w:link w:val="Heading1"/>
        <w:uiPriority w:val="9"/>
        <w:rsid w:val="000D2D99"/>
        <w:rPr>
            <w:rFonts w:ascii="Impact" w:eastAsiaTheme="majorEastAsia" w:hAnsi="Impact" w:cstheme="majorBidi"/>
            <w:color w:val="538135" w:themeColor="accent6" w:themeShade="BF"/>
            <w:sz w:val="32"/>
            <w:szCs w:val="32"/>
        </w:rPr>
    </w:style>

    <w:style w:type="paragraph" w:customStyle="1" w:styleId="MyFancyStyle">
        <w:name w:val="MyFancyStyle"/>
        <w:basedOn w:val="Normal"/>
        <w:link w:val="MyFancyStyleChar"/>
        <w:qFormat/>
        <w:rsid w:val="008802A5"/>
        <w:rPr>
            <w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman"/>
            <w:color w:val="FF0000"/>
            <w:sz w:val="144"/>
        </w:rPr>

    </w:style>

    <w:style w:type="paragraph" w:customStyle="1" w:styleId="subject">
        <w:name w:val="subject"/>
        <w:basedOn w:val="Normal"/>
        <w:link w:val="subjectChar"/>
        <w:qFormat/>
        <w:rsid w:val="008802A5"/>
        <w:textAlignment w:val="center"/>
        <w:rPr>
            <w:rFonts w:hint="eastAsia" w:ascii="仿宋_GB2312" w:hAnsi="仿宋_GB2312" w:eastAsia="仿宋_GB2312" w:cs="仿宋_GB2312"/>
            <w:sz w:val="24"/>
            <w:szCs w:val="24"/>
            <w:lang w:val="en-US" w:eastAsia="zh-CN"/>
             <w:color w:val="FF0000"/>
        </w:rPr>

    </w:style>

     <w:style w:type="character" w:customStyle="1" w:styleId="subjectChar">
        <w:name w:val="subject Char"/>
        <w:basedOn w:val="DefaultParagraphFont"/>
        <w:link w:val="subject"/>
        <w:rsid w:val="008802A5"/>

        <w:rPr>
           <w:rFonts w:hint="eastAsia" w:ascii="仿宋_GB2312" w:hAnsi="仿宋_GB2312" w:eastAsia="仿宋_GB2312" w:cs="仿宋_GB2312"/>
            <w:sz w:val="24"/>
            <w:szCs w:val="24"/>
            <w:lang w:val="en-US" w:eastAsia="zh-CN"/>
             <w:color w:val="FF0000"/>

        </w:rPr>

    </w:style>

    <w:style w:type="character" w:customStyle="1" w:styleId="MyFancyStyleChar">
        <w:name w:val="MyFancyStyle Char"/>
        <w:basedOn w:val="DefaultParagraphFont"/>
        <w:link w:val="MyFancyStyle"/>
        <w:rsid w:val="008802A5"/>

        <w:rPr>
            <w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman"/>
            <w:color w:val="FF0000"/>
            <w:sz w:val="144"/>
        </w:rPr>
    </w:style>

</w:styles>
import * as fs from "fs"; 
import { Document, Packer, Paragraph, TextRun } from "docx";
import  { xmlStyles } from './xmlStyles';

 // 文档类Document中包含章节数组sections,可向其中添加多个章节 
const doc = new Document({ 
      externalStyles:xmlStyles,
      sections: [{ 
          properties: {}, 
          children: [ new Paragraph({ 
             children: [ new TextRun("Hello World"), 
                  new TextRun({ text: "Foo Bar", bold: true, }),
                  new TextRun({ text: "\tGithub is the best", bold: true, }), ], }), ], }], }); 

// 通过fs实现将docx从buffer流转化成*.docx文件

 Packer.toBuffer(doc).then((buffer) => { fs.writeFileSync("My Document.docx", buffer); }); // 完成下载


styles

styles也属于样式自定义的一种方式,但这里的方式定义方式是局限于当前docx提供的属性,我们可以在docx的section、Paragraph、Text、Header等中使用。
import * as fs from "fs"; 
import { Document, Packer, Paragraph, TextRun } from "docx";


/**  * 获取自定义段落格式  */ 

function getParagraphStyles() {   
  return [ {        
     id: 'tips', // 温馨提示      
     name: 'tips',           
     basedOn: "Normal",          
     next: "Normal",            
     quickFormat: true,            
     paragraph: {
                 border: {
                     top: borderStyle,
                     left: borderStyle,
                     right: borderStyle, 
                    bottom: borderStyle
                 }
             }
         }, 
 {
             id: 'section', // 题型段落
             name: 'section',
             basedOn: "Normal",
             next: "Normal", 
            quickFormat: true,
             paragraph: { 
                indent: indent,
                spacing: {
                    before: '2pc'
                 }, 
            }
         },
     ]
 }

 // 文档类Document中包含章节数组sections,可向其中添加多个章节 
const doc = new Document({ 
      sections: [{ 
          properties: {}, 
          children: [ new Paragraph({ 
             children: [ new TextRun("Hello World"), 
                  new TextRun({ text: "Foo Bar", bold: true, }),
                  new TextRun({ text: "\tGithub is the best", bold: true, }), ], }), ], }], }); 

// 通过fs实现将docx从buffer流转化成*.docx文件

 Packer.toBuffer(doc).then((buffer) => { fs.writeFileSync("My Document.docx", buffer); }); // 完成下载


React使用Ant Design Pro框架导致首屏加载缓慢问题

胖蔡阅读(313)

存在问题

React项目中使用Ant Design pro框架,发现编译后前端应用的首屏加载过于缓慢,针对该问题,对改应用的生成包进行分析并优化,使其加载速度提升,用户体验更好。整个优化的思路过程如下。

1、网页登录后,首次刷新【存在路由】,加载时长过大,用户体验不佳

0
0

存在的相关关键指标:

  • DOMCOntentLoaded:7.32s
  • Load:32.21s
  • Finish:32.90s
  • layout.async.js:23.29s
  • misc.async.js:14.53s

解决思路

根据上面出现的问题分析可以通过如下几个方面对包的大小和加载的必要性进行拆分优化:
  1. 拆出页面包
  2. 裁剪vendors,将@ant-design、antd、docx包单独拆出一个文件,降低load的响应时间,
  3. 替换@ant-design/chats 为 @antv/g2plot,减少可视化组件包的大小
const webpackPlugin = config => {   // optimize chunks   config.optimization // share the same chunks across different modules     .runtimeChunk(false)     .splitChunks({       // chunks: 'async',       name: 'vendors',       chunks: 'async',       minSize: 20000,       minChunks: 1,       maxAsyncRequests: 30,       maxInitialRequests: 30,       cacheGroups: {         vendors: {           test: module => {             const packageName = getModulePackageName(module) || '';             // console.log('get splitChunks vendors:',packageName)             if (packageName) {               return [                 '@antv',                 '@ant-design',                 'antd',                 'docx',                 'l7',                 'gg-editor-core',                 'bizcharts-plugin-slider',               ].includes(packageName);             }             return false;           },           name(module) {             const packageName = getModulePackageName(module);             if (packageName) {               // console.log('get splitChunks name:', packageName);               if (['@ant-design', 'antd'].indexOf(packageName) >= 0) {                 return 'ant-design'; // visualization package               } else if (['docx'].indexOf(packageName) >= 0) {                 // 单独拆开                 return 'docx';               }             }             return 'misc';           },         },       },     }); };

结果

1、gzip降低

22165adbf146e2b
原始的
51433a6450391ca
总包大小以及layout.async.js、vendors.async.js大小均有显著缩小

2、请求数据

cd2907c13172429

存在的相关关键指标:

  • DOMCOntentLoaded:7.72s
  • Load:13.96s
  • Finish:14.68s
  • misc.async.js:3.57s
  • umi.js:7.30s