Coder Social home page Coder Social logo

fantasticit / coding Goto Github PK

View Code? Open in Web Editor NEW
151.0 151.0 21.0 10.87 MB

编程技术学习笔记 https://coding.fantasticit.vercel.app

Home Page: https://fantasticit.gitee.io/coding

TypeScript 46.49% JavaScript 28.40% Shell 5.66% Python 12.45% HTML 7.01%

coding's Introduction

coding's People

Contributors

fantasticit avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

coding's Issues

next.js + nest.js 构建页面可视化编辑器 -- Ramiko

前言

最近看了不少关于 h5 页面制作工具。端午闲来无事,决定尝试下一个页面搭建工具。效果如下:



gif 录制效果不佳,可以访问以下链接进行体验。

技术栈

  • next.js:前端模块化开发
  • sass: 配合使用 css modules
  • nest.js:服务端语言
  • MySQL:数据存储

整体架构

前端开发组件库,完善组件类型,编辑器读取组件完成页面搭建,将页面数据发送至服务端保存。
访问页面,从服务端拉取页面数据,前端渲染页面即可。

编辑器设计

编辑器整体结构图

.
|____index.tsx
|____plugins               ## 组件库管理
|____Editor.tsx            ## 编辑器
|____type.ts               ## 类型定义
|____components
| |____Pannel              ## 左侧组件面板
| |____Preview             ## 中间预览面板
| |____Editor              ## 组件编辑器实现
| | |____index.tsx
| | |____PropsEditor
| | | |____index.tsx
| | | |____components
| | | | |____SwitchEditor
| | | | |____RadioEditor
| | | | |____ImgEditor
| | | | |____ColorEditor
| | | | |____TextEditor
| | | | |____TextareaEditor
| | | | |____NumberEditor
| | | |____renderEditorItem.tsx
| | | |____UnionEditor.tsx
| | |____FunctionEditor
| | |____SettingEditor
|____renderPage.tsx

数据结构

定义页面数据结构

既然是可视化页面搭建,那么页面必须可以以某种数据结构进行描述。比如:

{
  setting: {
  } // 页面设置
  components: []; // 页面使用到的组件
}

定义组件数据结构

页面核心是由组件搭建而成的,那么就要定义组件的数据结构。

import React from 'react';

export const Title = ({ title, style }) => {
  return <h1>{title}</h1>
};

Title.defaultProps = {
  title: '标题',
};

Title.schema = {
  title: {
    title: '标题',
    type: 'text'
  },
};

核心可以抽象为:

{
  name: 'Title'; // 对应组件名
  props: {
  }
  schema: {
  }
}

name

不可能把组件源代码保存到服务端,所以这里只保存组件的名称,前端渲染时,根据该名称找到对应组件渲染即可(类似 Vue.js 的动态组件)

props

React 组件的 props,这里使用 defaultProps 赋值默认值

schema

对应 props 各个属性的描述,用于编辑器针对进行渲染。进行组件编辑,实际上编辑的是组件的 propsprops 改变组件的渲染结果自然改变。为了对 props 进行编辑,需要定义 props 的描述语言,通过 props 描述来进行属性编辑。这里使用如下的 schema

{
  title: '标题';
  type: 'text';
}

对应组件 props.title,通过 type 可以决定如何渲染编辑器组件。

无渲染组件

可能光能渲染组件是不够的,也许需要更多的功能包装,比如埋点。这一类函数本质上也是组件。可以通过 schema 定义进行 props 编辑。举个例子:

import React from 'react';

export const Tracking = ({ op, pageSn, pageElSn, children }) => {
  const onClick = () => {
    alert('埋点测试:' + op + '_' + pageSn + '_' + pageElSn);
  };

  return <div onClick={onClick}>{children}</div>;
};

Tracking.defaultProps = {
  op: 'click',
  pageSn: null,
  pageElSn: null
};

Tracking.schema = {
  op: {
    title: '曝光方式',
    type: 'radio',
    options: ['click', 'pv']
  },
  pageSn: {
    title: '页面编号',
    type: 'number'
  },
  pageElSn: {
    title: '元素编号',
    type: 'number'
  }
};

丰富完善

  1. 丰富组件库
  2. 优化编辑器:比如添加组件拖拽功能。

项目启动

Github:传送门

clientserver 分别执行 yarn dev 即可。

从酷炫的果冻菜单谈起 CSS3 filter 属性

从酷炫的果冻菜单谈起 CSS3 filter 属性

今天中午刷掘金沸点时,看到一个 Jerry Menu,看着效果不错,就像学(抄)习(袭)一下。效果图见下:

jerrymenu效果图

这里我要学(抄)习(袭)的就是这个菜单效果,这个 dom 结构还是很简单的。

div.blobs
    div.circle.main
    div.circle.sub.first
    div.circle.sub.second
    div.circle.sub.last

用CSS美化一下:

.blobs {
	display: flex;
	justify-content: center;
	align-items: center;
	position: absolute;
	top: 0;
	left: 0;
	bottom: 0;
	right: 0;
}

.circle {
	position: absolute;
	width: 90px;
	height: 90px;
	transform: translate(0, -48px);
	background: hsl(337, 70%, 58%);
	clip-path: circle(42px at center);
}

.circle.main {
    z-index: 2;
}

为了更直接到达目标效果,先不做动画,先把各个菜单的位置写好:

.first {
	transform: translate(-100px, -120px);
	background: hsl(307, 70%, 58%);
}

.second {
    transform: translate(0px, -150px);
	background: hsl(277, 70%, 58%);
}

.last {
    transform: translate(100px, -120px);
	background: hsl(247, 70%, 58%);
}

这时候效果就出来了,大致长这样:

image

最开始的效果是有交互的,那我们就用JS加一点交互:

const button = document.querySelector(".circle.main");
const circles = document.querySelectorAll(".circle.sub");

button.addEventListener("click", function() {
  circles.forEach(element => {
    element.classList.toggle("show");
  });
});

相应地,CSS也要作出变更:

.first {
	transition: transform 0.5s 100ms ease-out;
	background: hsl(307, 70%, 58%);
}

.second {
	transition: transform 0.5s 300ms ease-out;
	background: hsl(277, 70%, 58%);
}

.last {
	transition: transform 0.5s 500ms ease-out;
	background: hsl(247, 70%, 58%);
}

.first.show {
	transform: translate(-100px, -120px);
}

.second.show {
	transform: translate(0px, -150px);
}

.last.show {
	transform: translate(100px, -120px);
}

这时候效果就差不多了:

jerrymenu_0

但是总感觉差了点什么,粘连效果没了,看一下原作者的效果:

jerrymenu效果图

赶紧回头看下了作者的源代码,发现作者加了 .blobs { filter: url(#goo); } 这样的滤镜效果,翻看文档看了下:

CSS滤镜属性,可以在元素呈现之前,为元素的渲染提供一些效果,如模糊、颜色转移之类的。滤镜常用于调整图像、背景、边框的渲染。SVG滤镜资源(SVG Filter Sources)是指以xml文件格式定义的svg滤镜效果集,可以通过URL引入并且通过锚点(#element-id)指定具体的一个滤镜元素。

再设置 filter 滤镜并加上相应的 svg 代码之后,整个 Jerry Menu 的效果就学(抄)习(袭)完了,效果如下:

jerrymenu_1

这里附上 MDN上关于 filter 的文档

2

extends

  1. 接口继承
interface T1 {
  name: string;
}

interface T2 {
  age: number;
}

interface T3 extends T1, T2 {
  sex: string;
}
  1. 条件判断
interface Animal {
  eat(): void;
}

interface Dog extends Animal {
  bite(): void;
}

// 如果前面的类型可以赋值给后面的类型,则为 true
type A = Dog extends Animal ? string : number; // string
  1. 泛型用法
type A1 = 'x' extends 'x' ? string : number; // string
type A2 = 'x' | 'y' extnds 'x' ? string : number; // number

type P<T> = T extends 'x' ? string : number;
type A3 = P<'x' | 'y'>; // string | number; 满足分配律,被拆开赋值,type A3 = 'x' extends 'x' | 'y' extends 'x'

type A4 = never extends 'x' ? string : number; // string
type P1<T> = T extends 'x' ? string : number;
type A5 = P1<never>; // never,在条件分配类型中 never 被认为是空的联合类型

type P2<T> = [T] extends ['x'] ? string : number; // 通过 [] 阻止条件分配类型
type A6 = P2<"x" | 'y'>; // number
type A7 = P2<never>; // string,never 被认为是所有类型的子类型


type Exclude<T, U> = T extends U ? never : T;
type B = Exclude<'key1' | 'key2', "key2"> // 相当于 type B = Exclude<key1, key2> | Exclude<key2, key2>

type Extract<T, U> = T extends U ? T : never;
type C = Extract<'key1' | 'key2', 'key2'> // 相当于 type C = Extract<key1, key2> | Extract<key2, key2>

type Pick<T, K extends keyof T> = {
  [P in K]: T[P]
}

type vs interface

  1. 都可以用于描述对象或方法签名,语法不同
interface Point {
  x: number;
  y: number;
}

interface SetPoint {
  (x: number, y: number): void;
}

type Point = {
  x: number;
  y: number;
};

type SetPoint = (x: number, y: number) => void;
  1. type 可以用作其他 type 的类型别名,interface 不可以
type Name = string;

type X = { x: number };
type Y = { y: number };
type Z = X | Y;

type D = [X, Y];
  1. 都可以进行扩展,且可以互相扩展
interface X {
  x: number;
}

interface Point extends X {
  y: number;
}

-----

type X = { x: number };
type Point = X & { y: number };

----

interface X {
  x: number;
}
type Point = X & { y: number };

----

type X = { x: number };
interface Point extends X {
  y: number;
}
  1. interface 声明会被合并
interface Point {
  x: number;
}

interface Point {
  y: number;
}

const p: Point = { x: 0, y: 0 };

浏览器缓存机制

缓存是性能优化中简单高效的一种方式。优先的缓存策略可以缩短网页请求资源的距离,减少延迟,并且可以缓存文件进行复用,还可以减少带宽,降低网络负荷。

对于一个数据请求来说,可以分为发起请求、后端处理、浏览器响应 3 个阶段,缓存可以在 1、2 两步优化性能。

缓存位置

1. service worker

service worker 是运行在浏览器背后的独立线程。使用 service worker 必须使用 https 传输协议。service worker 可以让用户自由控制缓存哪些文件,如何匹配缓存、如何读取缓存,同时缓存还是持久性的。

2. Memory Cache

memory cache 是内存中的缓存,主要包含当前页面中已经抓取到的资源。

3. Disk Cache

disk cache 是硬盘中的缓存。

4. Push Cache

Push Cache 是推送缓存,是 HTTP/2 中的内容,当以上 3 种缓存都没有命中时,才会被使用。该缓存在 会话(session)中存在,会话结束就会被释放。

小结

通常浏览器缓存分为 2 种:强缓存 和 协商缓存,并且缓存策略都是通过设置 HTTP Header 实现的。

缓存过程

  • 浏览器每次发起请求,都会现在浏览器缓存中查找请求的结果和标识
  • 浏览器每次拿到返回的请求都会将该结果和缓存标识存入缓存中

强缓存

强缓存:不会向服务器发生请求,直接从缓存中读取资源。在 chrome 控制台可以看到该请求返回 200 的状态码,并且 size 显示 from disk cache 或 from memory cache。
强缓存可以通过设置 2 中 HTTP Header 实现:expires 和 Cache-Control。

  • expires

    缓存过期时间,用来指定资源到期时间,是服务器的具体的时间点。expires=max-age + 请求时间,需要和 last-modified 结合使用。expires 是 HTTP/1 的产物,受限于本地时间,修改本地时间,可能会导致缓存失效。
  • cache-control

    cache-control 可以在请求头或响应头中设置,可以组合使用多种指令。
    • public:所有内容都会被缓存
    • private: 所有内容只有客户端可以缓存
    • no-cache: 不是说浏览器不使用缓存,而是先确认下数据和服务器是否一致。也就是使用 etag 或者 last-modified 控制缓存。
    • no-store:所有内容都不缓存,不使用强缓存也不使用协商缓存
    • max-age: 表示缓存内容在多久后失效
    • s-maxage: 同 max-age,但是只在代理服务器生效(比如 cdn)
    • max-stale: 能容忍的最大过期时间
    • max-fresh: 能容忍的最小新鲜度

expires 是 HTTP 1.0 的产物,cache-control 是 HTTP 1.1 的产物,两者同时存在时,cache-control 优先级高些。

协商缓存

强缓存判断缓存是否超出某个时间或者范围,但是不关心服务器是否已经更新内容。为了获取服务器更新,就需要使用协商缓存。

协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器决定是否使用缓存。主要有 2 中情况:

  • 协商缓存生效,返回 304 和 no modified
  • 协商缓存失效,返回 200 和 请求结果

协商缓存通过 2 中 HTTP Heeader 设置:last-modified 和 etag。

last-modified 和 if-modified-since

浏览器在第一次访问资源时,服务器返回资源的同时,在 响应头添加 last-modified,值是这个资源在服务器上的最后修改时间。

浏览器下一次请求,服务器检测到 last-modified 的值,再添加个 if-modified-since 值是 last-modified 的值。服务器再次收到请求,会根据 last-modified-since 的值于服务器中这个资源的最后修改时间对比。相同 304,如果 if-modified-since 的值比服务器上资源最后修改时间小,返回新的资源和 200。

last-modified 存在的弊端:

  • 如果在本地打开缓存文件,即使没有改内容,last-modified 也会被修改,服务器就不能命中缓存
  • last-modified 只能以秒计时,如果在不可感知的时间内修改,服务器还是会认为命中缓存了

因为根据文件修改时间决定缓存命中有欠缺,所以根据文件内容是否修改, HTTP 1.1 出现了 etag 和 if-none-match。

etag 和 if-none-match

etag 是服务器响应请求时,返回当前资源文件的一个唯一标识(有服务器生成),只要资源有变化,etag 就会重新生成。

浏览器下一次请求时,只需要把上一次 etag 的值写到 if-none-match 中,服务器再比较 if-none-match 和当前资源的 etag 是否一致。

对比

精度上,etag 优于 last-modified。

Last-Modified的时间单位是秒,如果某个文件在1秒内改变了多次,那么他们的Last-Modified其实并没有体现出来修改,但是Etag每次都会改变确保了精度;如果是负载均衡的服务器,各个服务器生成的Last-Modified也有可能不一致。

  • 在性能上,Etag要逊于Last-Modified,毕竟Last-Modified只需要记录时间,而Etag需要服务器通过算法来计算出一个hash值。
  • 在优先级上,服务器校验优先考虑Etag

小结

强缓存优先于协商缓存,若强缓存生效则直接使用缓存,若不生效使用协商缓存(last-modified/if-modified-since 和 etag/if-none-match)。协商缓存由服务器决定是否使用缓存。使用缓存返回 304,不使用 返回 200 和 新的资源。

应用

频繁变动的资源

cache-control: no-cache

通过设置 no-cache 使浏览器每次都请求服务器,然后配合使用 etag 或者 last-modified 协商缓存。

不常变化的缓存

cache-control: max-age=315360000

通过设置一个很大的过期时间,浏览器就会使用强缓存。而为了解决更新的问题,就需要在文件名(或者路径)中添加 hash, 版本号等动态字符,之后更改动态字符,从而达到更改引用 URL 的目的,让之前的强制缓存失效 (其实并未立即失效,只是不再使用了而已)。

用户行为对浏览器缓存的影响

  • 打开网页,输入URL:查找 disk-cache,有就用,没有发请求
  • F5 刷新:优先查找 memory-cache ,然后 disk-cache
  • ctrl+ f5 (强制刷新):浏览器不使用缓存,所有请求头加上 cache-control: no-cache

使用 JS 朗读文章内容

今天,从 MDN 上看到了 SpeechSynthesis 这个API。看了下它的介绍。
网页语音 API 的 SpeechSynthesis 接口是语音服务的控制接口;它可以用于获取设备上关于可用的合成声音的信息,开始、暂停语音,或除此之外的其他命令。
卧槽,这一看不得了啊,这个接口可以调用设备接口读出文字,那这样岂不是就可以在web页面上实现朗读功能。来,试一下:

function speak() {
  let synth = window.speechSynthesis;
  let voices = synth.getVoices().filter(voice => voice.lang === 'zh-CN');
  if (voices.length == 0) return;
  let utterThis = new SpeechSynthesisUtterance(document.querySelector('#issue-589242640 > div > div.edit-comment-hide > task-lists > table > tbody > tr > td').textContent);
  utterThis.voice = voices[0];
  synth.speak(utterThis);
}

可在本博客页面打开控制台,并执行代码,体验一下“朗读功能”。(如果没有朗读,可以再调一次 speak()❤️😄😁)

在控制台打印一下 voices

应该是不同的硬件设备,不同的操作系统,可用的 voices 不一样,比如 mac 和 windows。

最后,看一下兼容性还不错。😹💓

Redux源码学习:createStore

本文将学习 createStore,并实现一个简单的 createStore。

createStore

接下来就按照 createStore 的内部流程来解读。

1 引入工具函数

createStore 引入的函数有:

  • symbol-observable (Symbol.observable 的 pollyfill)
  • ActionTypes
  • isPlainObject: 通过 Object.getPrototypeof 获取对象原型,比较以判断是否为 plain object

2 参数声明

createStore接受三个参数:

  • reducer:根据当前的 state tree 和 要触发的 action 来生成并返回 新的(next)state tree
  • preloadedState: 可选的初始state
  • enhancer: 可选的用以增强 store 的 第三方能力

createStore返回一个 redux store 用以读取 state、触发 action 和 订阅 changes

3 内部流程

首先,判断 preloadedState 和 enhancer:

if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
	enhancer = preloadedState
	preloadedState = undefined
}

if (typeof enhancer !== 'undefined') {
	if (typeof enhancer !== 'function') {
		throw new Error('Expected the enhancer to be a function.')
	}

	return enhancer(createStore)(reducer, preloadedState)
}

所以 enhancer 应是一个 柯里化函数,如果给定了 enhancer 则 直接调用 reducerpreloadedState 返回 enhancedstore(当然也是 createstore生成 )。
其次,判断 reducer 是否为函数,不是则报错:

if (typeof reducer !== 'function') {
  throw new Error('Expected the reducer to be a function.')
}

紧接着,声明了以下变量:

let currentReducer = reducer
let currentState = preloadedState
let currentListeners = []
let nextListeners = currentListeners
let isDispatching = false

然后 dispatch 初始化 action(dispatch({ type: ActionTypes.INIT })) (这样每个 reducer都会返回自己的初始状态,这样就有效地填充了 初始的 state tree)
最后返回一个对象:

return {
  dispatch,
  subscribe,
  getState
}

4 dispatch (action)

dispatch 是唯一的触发 状态变化(state change)的途径,它只接受一个 isPlainObject 的 action,具体流程如下:

  1. 判断 action 是否 isPlainObject,否则报错
  2. 判断是否声明了 action.type ,没有则报错
  3. 判断是否 isDispatching,如果是报错(目前无法 dispatch action
  4. 尝试调用 currentReducer (传入的 reducer)
try {
    isDispatching = true
    currentState = currentReducer(currentState, action)
} finally {
    isDispatching = false
}

// 依次调用 listeners
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
   const listener = listeners[i]
   listener()
}

5 subscribe

subscribe 接收一个 listener 函数,它首先 ensureCanMutateNextListeners(通过 nextListeners = currentListeners.slice() 保存当前的监听函数 ),当初次 ensures 时,nextListeners为 [],然后将传入的 listener 加入到 nextListeners,这样 nextListeners中便有了 listeners,这样在dispacth 中便有了 listeners便可以循环调用 listeners,同时subscribe返回一个包含取消监听函数的对象。

6 模仿实现

目前可以简单总结下 createStore 返回一个主要包含(我说的主要) getStatesubscribedispatch。通过dispatch ation 调用 reducer(currentState, action)以完成 change state,那么接下来按照这个思路实现一个简单的 createStore:

function createStore(reducer, preloadedState) {
  let currentState = preloadedState
  let listeners = []

  function getState() {
    return currentState
  }

  function subscribe(listener) {
    listeners.push(listener)

    return {
      unsubscribe() {
        let index = listeners.indexOf(listener)
        listeners.splice(index, 1)
      }
    }
  }

  function dispatch(action) {
    try {
      currentState = reducer(currentState, action)
    } finally {}
    
    // 遍历监听函数
    for (let i = 0; i < listeners.length; i++) {
      listeners[i]()
    }

    return action
  }
  
  dispatch({ type: 'INIT' })

  return {
    getState,
    dispatch,
    subscribe
  }
}

测试以下:

function reducer (state, action) {
  switch (action.type) {
    case 'ADD':
      let count = state.count
      count++
      return { ...state, count }

    default:
      return state
  }
}

const store = createStore(reducer, { count: 0 })

let listener = store.subscribe(() => console.log('订阅store.count: ', store.getState().count))

store.dispatch({ type: 'ADD' })
store.dispatch({ type: 'ADD' })

listener.unsubscribe()

store.dispatch({ type: 'ADD' })

输出如下:

订阅store.count:  1
订阅store.count:  2

总结

通过阅读 createStore 的源码可以发现这部分的核心主要就是在 reducerdispatch 以及 state,理清楚这些便明白了 createStore 的简单与巧妙。

koa2 接口开发入门

依赖安装

  1. 安装koa2

  2. 安装koa-router
    koa-router 提供了 .get、.post、.put 和 .del 接口来处理各种请求

代码分层

这里按照MVC的**来组织代码结构:

server
├── app.js
├── controller
├── middleware
├── package.json
├── package-lock.json
└── router

  • app.js: 程序入口
  • middleware: koa2 相关中间件
  • controller: 控制器
  • router: koa-router 路由表

编码

  1. 编写控制器
    当然还是经典的hello world。在 controller 文件下新建index.js,写入以下代码:
module.exports = {
  hello: async (ctx, next) => {
    ctx.response.body = 'Hello World'
  }
}
  1. 编写中间件
    增加一个中间件来记录响应时间,在 middleware 文件下新建index.js,写入以下代码:
const logger = () =>  {
  return async (ctx, next) => {
    const start = Date.now()

    await next()

    const responseTime = (Date.now() - start)
    console.log(`响应时间为: ${responseTime / 1000}s`)
  }
}

module.exports = (app) => {
  app.use(logger())
}

注意,中间件只能是函数

  1. 编写路由表
    增加一个路由来试试,在 router 文件下新建index.js,写入以下代码:
const router = require('koa-router')()

module.exports = app => {
  router.get('/', Controller.hello) // 注意是在controller编写的hello函数

  app.use(router.routes()).use(router.allowedMethods())
}

  1. 编写 app.js
const koa = require('koa')
const app = new koa()
const middleWare = require('./middleware')
const router = require('./router')

middleWare(app)
router(app)

app.listen(port, () => {
  console.log('server is running at http://localhost:3000')
})
  1. 运行程序
    node app.js然后打开浏览器,访问http://localhost:3000就可以看到Hello World了。

总结

至此,使用koa2编写接口的基本思路就说完了,一般都是在controller对数据库进行CRUD,然后配置相关路由,就完成了一个接口服务的开发。

Docker 简单使用笔记

Docker

安装 Docker

Docker 网站下载安装 Docker 。
安装完成后,打开命令行输入 docker version,如果有相关输出即安装成功。

运行 container

docker 中与 container 相关的命令主要有:

  • docker container run ...:运行容器
  • docker container ls: 列出当前运行容器
  • docker container start [container_id | container_name]: 开始运行指定 id(或名称) 的容器
  • docker container stop [container_id | container_name]: 停止运行指定 id(或名称) 的容器
  • docker container rm [container_id| container_name]: 删除指定 id(或名称) 的容器

其中,也可以使用 docker ps 查看正在运行的容器。

运行 nginx

打开命令行,键入:

docker container run -it -p 8080:81 nginx

如果报错无法拉取,执行 docker login 输入账号密码登录 docker hub 账号后再执行。
在浏览器访问网站:http://localhost:8080,如果可以访问到 nginx 相关即表示运行成功。

-p 8080:81 指定镜像在容器内运行的端口为:81,同时将端口映射到本地机器的 8080 端口,所以需要在浏览器访问 http://localhost:8080 才可。

运行 mysql

不同与 nginx ,运行 mysql 时可能需要设置一些变量,比如数据库密码之类的,这就需要用到 --env 参数。

docker container run -d -p 3006:3036 --env MYSQL_ROOT_PASSWORD=123456 mysql

进入 container

使用 docker container -it exec [container_id | container_name] 可以运行指定的容器内的程序

首先,运行一个名为 mynginx 的容器:

docker container run -d -p 8080:80 --name mynginx nginx

然后,调用 mynginxbash 程序:

docker container exec -it mynginx bash

如此,便可以在 container 内部操作环境执行命令,就像是进入了这个 container 操作环境内一样。

绑定挂载点

在上一步中,可以知道 container mynginx 的文件路径为:/usr/share/nginx/html。如果想要修改这些文件,就需要进入 container 内部进行修改,这样便会很麻烦。docker 提供了绑定挂载点的功能,这样便可以把本地机器的文件映射到 container 内部。

docker container run -d -p 8080:81 -v ~/test:/usr/share/nginx/html --nmae mynginx nginx

在本地机器的 test 文件夹,新建一个 index.html 文件,然后浏览器访问 http://localhost:8080,便可以看到刚刚保存的内容。

制作镜像

docker container run 的过程是首先拉取镜像,然后运行这个镜像。如此便可以制作自己的镜像。例如:

首先新建一个文件夹,然后新建一个 index.html 文件并写入内容,然后再编写 Dockerfile 文件。

FROM nginx:latest

WORKDIR /usr/share/nginx/html

COPY . .

然后运行 docker image build . -t mynginx-web. 是指当前路径),便完成了镜像制作。

运行这个镜像只需要:docker container run -it -p 8080:80 mynginx-web

高德地图海量数据展示优化

在线 Demo

示例

假设开发中,遇到这样一个需求:“接口返回一片地区内所有的小区的电子围栏,将小区绘制到高德地图上”。很容易写出下面这样的代码:

const map = new Amap.Map();

for (const item of data) {
  const polygon = new AMap.Polygon(item);
  map.add(polygon);
}

示例

效果大致可能就是这样,在实际运行中,很有可能会非常卡顿,因为绘制耗了大量时间,如果在地图上还有事件交互,也可能会非常卡顿。实际业务根本无法使用,这时候就要找办法性能优化,翻阅高德地图的文档示例,可能会发现有“集群”、“海量点”渲染优化等示例,但是实际上在项目中可能还是没法使用(比如这个需求是绘制小区)。

从数据出发

从接口层面来看,很有可能是后端吐出大规模地理信息数据,前端拿到数据后根据产品需求进行渲染,本质上都是在消费数据。最直接的方式是“单次消费全部数据进行全部渲染”,基本上会带来卡顿问题。让我们回到地图本身,当我们在地图上进行交互(比如移动地图、滚动缩放)时,地图看起来好像才会绘制当前视口能看到的地方,或者说就是这一片的瓦片。

所以,从地图本身的瓦片式渲染来看,我们对数据的消费也可以是这种形式,展示“当前视口内可以渲染的数据,当前缩放等级可以看到的数据”,进而大幅减少单次需要渲染的数据,性能自然就上去了。总结一下:

通过地图当前的视口、缩放登记,获取当前可以渲染的数据、被聚合的数据

代码实现

站在巨人的肩膀上,通过 kd-brushsupercluster 对数据进行消费。

function sortKD(ids, coords, nodeSize, left, right, depth) {
  if (right - left <= nodeSize) {
    return;
  }

  const m = (left + right) >> 1;

  select(ids, coords, m, left, right, depth % 2);

  sortKD(ids, coords, nodeSize, left, m - 1, depth + 1);
  sortKD(ids, coords, nodeSize, m + 1, right, depth + 1);
}

function select(ids, coords, k, left, right, inc) {
  while (right > left) {
    if (right - left > 600) {
      const n = right - left + 1;
      const m = k - left + 1;
      const z = Math.log(n);
      const s = 0.5 * Math.exp((2 * z) / 3);
      const sd = 0.5 * Math.sqrt((z * s * (n - s)) / n) * (m - n / 2 < 0 ? -1 : 1);
      const newLeft = Math.max(left, Math.floor(k - (m * s) / n + sd));
      const newRight = Math.min(right, Math.floor(k + ((n - m) * s) / n + sd));
      select(ids, coords, k, newLeft, newRight, inc);
    }

    const t = coords[2 * k + inc];
    let i = left;
    let j = right;

    swapItem(ids, coords, left, k);
    if (coords[2 * right + inc] > t) {
      swapItem(ids, coords, left, right);
    }

    while (i < j) {
      swapItem(ids, coords, i, j);
      i++;
      j--;
      while (coords[2 * i + inc] < t) {
        i++;
      }
      while (coords[2 * j + inc] > t) {
        j--;
      }
    }

    if (coords[2 * left + inc] === t) {
      swapItem(ids, coords, left, j);
    } else {
      j++;
      swapItem(ids, coords, j, right);
    }

    if (j <= k) {
      left = j + 1;
    }
    if (k <= j) {
      right = j - 1;
    }
  }
}

function swapItem(ids, coords, i, j) {
  swap(ids, i, j);
  swap(coords, 2 * i, 2 * j);
  swap(coords, 2 * i + 1, 2 * j + 1);
}

function swap(arr, i, j) {
  const tmp = arr[i];
  arr[i] = arr[j];
  arr[j] = tmp;
}

function range(ids, coords, minX, minY, maxX, maxY, nodeSize) {
  const stack = [0, ids.length - 1, 0];
  const result = [];
  let x;
  let y;

  while (stack.length) {
    const axis = stack.pop();
    const right = stack.pop();
    const left = stack.pop();

    if (right - left <= nodeSize) {
      for (let i = left; i <= right; i++) {
        x = coords[2 * i];
        y = coords[2 * i + 1];
        if (x >= minX && x <= maxX && y >= minY && y <= maxY) {
          result.push(ids[i]);
        }
      }
      continue;
    }

    const m = Math.floor((left + right) / 2);

    x = coords[2 * m];
    y = coords[2 * m + 1];

    if (x >= minX && x <= maxX && y >= minY && y <= maxY) {
      result.push(ids[m]);
    }

    const nextAxis = (axis + 1) % 2;

    if (axis === 0 ? minX <= x : minY <= y) {
      stack.push(left);
      stack.push(m - 1);
      stack.push(nextAxis);
    }
    if (axis === 0 ? maxX >= x : maxY >= y) {
      stack.push(m + 1);
      stack.push(right);
      stack.push(nextAxis);
    }
  }

  return result;
}

function within(ids, coords, qx, qy, r, nodeSize) {
  const stack = [0, ids.length - 1, 0];
  const result = [];
  const r2 = r * r;

  while (stack.length) {
    const axis = stack.pop();
    const right = stack.pop();
    const left = stack.pop();

    if (right - left <= nodeSize) {
      for (let i = left; i <= right; i++) {
        if (sqDist(coords[2 * i], coords[2 * i + 1], qx, qy) <= r2) {
          result.push(ids[i]);
        }
      }
      continue;
    }

    const m = Math.floor((left + right) / 2);

    const x = coords[2 * m];
    const y = coords[2 * m + 1];

    if (sqDist(x, y, qx, qy) <= r2) {
      result.push(ids[m]);
    }

    const nextAxis = (axis + 1) % 2;

    if (axis === 0 ? qx - r <= x : qy - r <= y) {
      stack.push(left);
      stack.push(m - 1);
      stack.push(nextAxis);
    }
    if (axis === 0 ? qx + r >= x : qy + r >= y) {
      stack.push(m + 1);
      stack.push(right);
      stack.push(nextAxis);
    }
  }

  return result;
}

function sqDist(ax, ay, bx, by) {
  const dx = ax - bx;
  const dy = ay - by;
  return dx * dx + dy * dy;
}

const defaultGetX = function (p) {
  return p[0];
};
const defaultGetY = function (p) {
  return p[1];
};

const KDBush = function KDBush(points, getX, getY, nodeSize, ArrayType) {
  if (getX === void 0) getX = defaultGetX;
  if (getY === void 0) getY = defaultGetY;
  if (nodeSize === void 0) nodeSize = 64;
  if (ArrayType === void 0) ArrayType = Float64Array;

  this.nodeSize = nodeSize;
  this.points = points;

  const IndexArrayType = points.length < 65536 ? Uint16Array : Uint32Array;

  const ids = (this.ids = new IndexArrayType(points.length));
  const coords = (this.coords = new ArrayType(points.length * 2));

  for (let i = 0; i < points.length; i++) {
    ids[i] = i;
    coords[2 * i] = getX(points[i]);
    coords[2 * i + 1] = getY(points[i]);
  }

  sortKD(ids, coords, nodeSize, 0, ids.length - 1, 0);
};

KDBush.prototype.range = function range$1(minX, minY, maxX, maxY) {
  return range(this.ids, this.coords, minX, minY, maxX, maxY, this.nodeSize);
};

KDBush.prototype.within = function within$1(x, y, r) {
  return within(this.ids, this.coords, x, y, r, this.nodeSize);
};

const defaultOptions = {
  minZoom: 3, // min zoom to generate clusters on
  maxZoom: 18, // max zoom level to cluster the points on
  minPoints: 4, // minimum points to form a cluster
  radius: 80, // cluster radius in pixels
  extent: 512, // tile extent (radius is calculated relative to it)
  nodeSize: 64, // size of the KD-tree leaf node, affects performance
  log: false, // whether to log timing info
  // whether to generate numeric ids for input features (in vector tiles)
  generateId: false,
  // a reduce function for calculating custom cluster properties
  reduce: null, // (accumulated, props) => { accumulated.sum += props.sum; }
  // properties to use for individual points when running the reducer
  map: (props) => props, // props => ({sum: props.my_value})
};

const fround =
  Math.fround ||
  ((tmp) => (x) => {
    tmp[0] = +x;
    return tmp[0];
  })(new Float32Array(1));

class Supercluster {
  constructor(options) {
    this.options = extend(Object.create(defaultOptions), options);
    this.trees = new Array(this.options.maxZoom + 1);
  }

  load(points) {
    const { log, minZoom, maxZoom, nodeSize } = this.options;

    if (log) console.time('total time');

    const timerId = `prepare ${points.length} points`;
    if (log) console.time(timerId);

    this.points = points;

    // generate a cluster object for each point and index input points into a KD-tree
    let clusters = [];
    for (let i = 0; i < points.length; i++) {
      // if (!points[i].geometry) continue;
      clusters.push(createPointCluster(points[i], i));
    }
    this.trees[maxZoom + 1] = new KDBush(clusters, getX, getY, nodeSize, Float32Array);

    if (log) console.timeEnd(timerId);

    // cluster points on max zoom, then cluster the results on previous zoom, etc.;
    // results in a cluster hierarchy across zoom levels
    for (let z = maxZoom; z >= minZoom; z--) {
      const now = +Date.now();

      // create a new set of clusters for the zoom and index them with a KD-tree
      clusters = this._cluster(clusters, z);
      this.trees[z] = new KDBush(clusters, getX, getY, nodeSize, Float32Array);

      if (log) console.log('z%d: %d clusters in %dms', z, clusters.length, +Date.now() - now);
    }

    if (log) console.timeEnd('total time');

    return this;
  }

  getClusters(bbox, zoom) {
    let minLng = ((((bbox[0] + 180) % 360) + 360) % 360) - 180;
    const minLat = Math.max(-90, Math.min(90, bbox[1]));
    let maxLng = bbox[2] === 180 ? 180 : ((((bbox[2] + 180) % 360) + 360) % 360) - 180;
    const maxLat = Math.max(-90, Math.min(90, bbox[3]));

    if (bbox[2] - bbox[0] >= 360) {
      minLng = -180;
      maxLng = 180;
    } else if (minLng > maxLng) {
      const easternHem = this.getClusters([minLng, minLat, 180, maxLat], zoom);
      const westernHem = this.getClusters([-180, minLat, maxLng, maxLat], zoom);
      return easternHem.concat(westernHem);
    }

    const tree = this.trees[this._limitZoom(zoom)];
    const ids = tree.range(lngX(minLng), latY(maxLat), lngX(maxLng), latY(minLat));
    const clusters = [];
    for (const id of ids) {
      const c = tree.points[id];
      clusters.push(c.numPoints ? getClusterJSON(c) : this.points[c.index]);
    }
    return clusters;
  }

  getChildren(clusterId) {
    const originId = this._getOriginId(clusterId);
    const originZoom = this._getOriginZoom(clusterId);
    const errorMsg = 'No cluster with the specified id.';

    const index = this.trees[originZoom];
    if (!index) {
      console.error(errorMsg);
      return [];
    }

    const origin = index.points[originId];
    if (!origin) {
      console.error(errorMsg);
      return [];
    }

    const r = this.options.radius / (this.options.extent * Math.pow(2, originZoom - 1));
    const ids = index.within(origin.x, origin.y, r);
    const children = [];
    for (const id of ids) {
      const c = index.points[id];
      if (c.parentId === clusterId) {
        children.push(c.numPoints ? getClusterJSON(c) : this.points[c.index]);
      }
    }

    return children;
  }

  getLeaves(clusterId, limit, offset) {
    limit = limit || 10;
    offset = offset || 0;

    const leaves = [];
    this._appendLeaves(leaves, clusterId, limit, offset, 0);

    return leaves;
  }

  getTile(z, x, y) {
    const tree = this.trees[this._limitZoom(z)];
    const z2 = Math.pow(2, z);
    const { extent, radius } = this.options;
    const p = radius / extent;
    const top = (y - p) / z2;
    const bottom = (y + 1 + p) / z2;

    const tile = {
      features: [],
    };

    this._addTileFeatures(
      tree.range((x - p) / z2, top, (x + 1 + p) / z2, bottom),
      tree.points,
      x,
      y,
      z2,
      tile
    );

    if (x === 0) {
      this._addTileFeatures(tree.range(1 - p / z2, top, 1, bottom), tree.points, z2, y, z2, tile);
    }
    if (x === z2 - 1) {
      this._addTileFeatures(tree.range(0, top, p / z2, bottom), tree.points, -1, y, z2, tile);
    }

    return tile.features.length ? tile : null;
  }

  getClusterExpansionZoom(clusterId) {
    let expansionZoom = this._getOriginZoom(clusterId) - 1;
    while (expansionZoom <= this.options.maxZoom) {
      const children = this.getChildren(clusterId);
      expansionZoom++;
      if (children.length !== 1) break;
      clusterId = children[0].properties.cluster_id;
    }
    return expansionZoom;
  }

  _appendLeaves(result, clusterId, limit, offset, skipped) {
    const children = this.getChildren(clusterId);

    for (const child of children) {
      const props = child.properties;

      if (props && props.cluster) {
        if (skipped + props.point_count <= offset) {
          // skip the whole cluster
          skipped += props.point_count;
        } else {
          // enter the cluster
          skipped = this._appendLeaves(result, props.cluster_id, limit, offset, skipped);
          // exit the cluster
        }
      } else if (skipped < offset) {
        // skip a single point
        skipped++;
      } else {
        // add a single point
        result.push(child);
      }
      if (result.length === limit) break;
    }

    return skipped;
  }

  _addTileFeatures(ids, points, x, y, z2, tile) {
    for (const i of ids) {
      const c = points[i];
      const isCluster = c.numPoints;
      const f = {
        type: 1,
        geometry: [
          [
            Math.round(this.options.extent * (c.x * z2 - x)),
            Math.round(this.options.extent * (c.y * z2 - y)),
          ],
        ],
        tags: isCluster ? getClusterProperties(c) : this.points[c.index].properties,
      };

      // assign id
      let id;
      if (isCluster) {
        id = c.id;
      } else if (this.options.generateId) {
        // optionally generate id
        id = c.index;
      } else if (this.points[c.index].id) {
        // keep id if already assigned
        id = this.points[c.index].id;
      }

      if (id !== undefined) f.id = id;

      tile.features.push(f);
    }
  }

  _limitZoom(z) {
    return Math.max(this.options.minZoom, Math.floor(Math.min(+z, this.options.maxZoom + 1)));
  }

  _cluster(points, zoom) {
    const clusters = [];
    const { radius, extent, reduce, minPoints } = this.options;
    const r = radius / (extent * Math.pow(2, zoom));

    // loop through each point
    for (let i = 0; i < points.length; i++) {
      const p = points[i];
      // if we've already visited the point at this zoom level, skip it
      if (p.zoom <= zoom) continue;
      p.zoom = zoom;

      // find all nearby points
      const tree = this.trees[zoom + 1];
      const neighborIds = tree.within(p.x, p.y, r);

      const numPointsOrigin = p.numPoints || 1;
      let numPoints = numPointsOrigin;

      // count the number of points in a potential cluster
      for (const neighborId of neighborIds) {
        const b = tree.points[neighborId];
        // filter out neighbors that are already processed
        if (b.zoom > zoom) numPoints += b.numPoints || 1;
      }

      if (numPoints >= minPoints) {
        // enough points to form a cluster
        let wx = p.x * numPointsOrigin;
        let wy = p.y * numPointsOrigin;

        let clusterProperties = reduce && numPointsOrigin > 1 ? this._map(p, true) : null;

        // encode both zoom and point index on which the cluster originated -- offset by total length of features
        const id = (i << 5) + (zoom + 1) + this.points.length;

        for (const neighborId of neighborIds) {
          const b = tree.points[neighborId];

          if (b.zoom <= zoom) continue;
          b.zoom = zoom; // save the zoom (so it doesn't get processed twice)

          const numPoints2 = b.numPoints || 1;
          wx += b.x * numPoints2; // accumulate coordinates for calculating weighted center
          wy += b.y * numPoints2;

          b.parentId = id;

          if (reduce) {
            if (!clusterProperties) clusterProperties = this._map(p, true);
            reduce(clusterProperties, this._map(b));
          }
        }

        p.parentId = id;
        clusters.push(
          createCluster(wx / numPoints, wy / numPoints, id, numPoints, clusterProperties)
        );
      } else {
        // left points as unclustered
        clusters.push(p);

        if (numPoints > 1) {
          for (const neighborId of neighborIds) {
            const b = tree.points[neighborId];
            if (b.zoom <= zoom) continue;
            b.zoom = zoom;
            clusters.push(b);
          }
        }
      }
    }

    return clusters;
  }

  // get index of the point from which the cluster originated
  _getOriginId(clusterId) {
    return (clusterId - this.points.length) >> 5;
  }

  // get zoom of the point from which the cluster originated
  _getOriginZoom(clusterId) {
    return (clusterId - this.points.length) % 32;
  }

  _map(point, clone) {
    if (point.numPoints) {
      return clone ? extend({}, point.properties) : point.properties;
    }
    const original = this.points[point.index].properties;
    const result = this.options.map(original);
    return clone && result === original ? extend({}, result) : result;
  }
}

function createCluster(x, y, id, numPoints, properties) {
  return {
    x: fround(x), // weighted cluster center; round for consistency with Float32Array index
    y: fround(y),
    zoom: Infinity, // the last zoom the cluster was processed at
    id, // encodes index of the first child of the cluster and its zoom level
    parentId: -1, // parent cluster id
    numPoints,
    properties,
  };
}

function createPointCluster(p, id) {
  const x = 'x' in p ? p.x : p.position && p.position[0];
  const y = 'y' in p ? p.y : p.position && p.position[1];

  return {
    x: fround(lngX(x)), // projected point coordinates
    y: fround(latY(y)),
    zoom: Infinity, // the last zoom the point was processed at
    index: id, // index of the source feature in the original input array,
    parentId: -1, // parent cluster id
  };
}

function getClusterJSON(cluster) {
  return {
    isClutser: true,
    id: cluster.id,
    ...getClusterProperties(cluster),
    x: xLng(cluster.x),
    y: yLat(cluster.y),
    position: [xLng(cluster.x), yLat(cluster.y)],
  };
}

function getClusterProperties(cluster) {
  const count = cluster.numPoints;
  const abbrev =
    count >= 10000
      ? `${Math.round(count / 1000)}k`
      : count >= 1000
      ? `${Math.round(count / 100) / 10}k`
      : count;
  return extend(extend({}, cluster.properties), {
    cluster: true,
    cluster_id: cluster.id,
    count,
    abbreviatedCount: abbrev,
  });
}

// longitude/latitude to spherical mercator in [0..1] range
function lngX(lng) {
  return lng / 360 + 0.5;
}
function latY(lat) {
  const sin = Math.sin((lat * Math.PI) / 180);
  const y = 0.5 - (0.25 * Math.log((1 + sin) / (1 - sin))) / Math.PI;
  return y < 0 ? 0 : y > 1 ? 1 : y;
}

// spherical mercator to longitude/latitude
function xLng(x) {
  return (x - 0.5) * 360;
}
function yLat(y) {
  const y2 = ((180 - y * 360) * Math.PI) / 180;
  return (360 * Math.atan(Math.exp(y2))) / Math.PI - 90;
}

function extend(dest, src) {
  for (const id in src) dest[id] = src[id];
  return dest;
}

function getX(p) {
  return p.x;
}
function getY(p) {
  return p.y;
}

使用

function debounce(func, wait, immediate) {
  let timeout;

  const debounced = function () {
    const context = this;
    const args = arguments;
    const later = function () {
      timeout = null;
      if (!immediate) {
        func.apply(context, args);
      }
    };
    const callNow = immediate && !timeout;
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
    if (callNow) {
      func.apply(context, args);
    }
  };

  debounced.cancel = () => {
    clearTimeout(timeout);
  };

  return debounced;
}

const data = areas.map((area, index) => {
  return {
    ...area,
    path: area.lnglat,
    name: `模拟社区${index}`,
    index,
    x: area.lnglat[0][0],
    y: area.lnglat[0][1],
  };
});

const map = new AMap.Map(document.querySelector('#app'), {
  mapStyle: 'amap://styles/grey',
  zoom: 14,
  center: [116.467987, 39.992613],
});

const index = new Supercluster();
index.load(data);

let markers = [];
let polygons = [];

const render = () => {
  let bounds = map.getBounds();
  if (bounds.toBounds) {
    bounds = bounds.toBounds();
  }
  const bbox = [
    bounds.southWest.lng,
    bounds.southWest.lat,
    bounds.northEast.lng,
    bounds.northEast.lat,
  ];
  const views = index.getClusters(bbox, map.getZoom());
  const clusters = views.filter((view) => view.isClutser);
  const data = views.filter((view) => !view.isClutser);

  map.remove(markers);
  markers = clusters.map((cluster) => {
    const marker = new AMap.Marker({
      ...cluster,
      content: `<div style="width: 32px; height: 32px; line-height: 32px; border-radius: 50%; background-color: green; color: #fff; text-align: center;">${cluster.count}</div>`,
    });
    return marker;
  });
  map.add(markers);

  map.remove(polygons);
  polygons = data.map((item) => {
    const marker = new AMap.Polygon({
      ...item,
      fillColor: 'rgba(256, 0, 0, 0.2)', // 多边形填充颜色
      borderWeight: 2, // 线条宽度,默认为 1
      strokeColor: 'rgba(256, 0, 0, 1)', // 线条颜色
    });
    return marker;
  });
  map.add(polygons);
};

render();

const listener = debounce(render, 200);
map.on('zoom', listener);
map.on('moveend', listener);

示例

源码

源码

利用 Storage Event 实现页面间通信

利用 Storage Event 实现页面间通信

最近总是看到这种需求,故记录下作为笔记。

我们可能经常会在浏览器中使用多个 Tab 打开同一个网页。例如,在线聊天网页每个 Tab 页面的消息同步。其实原理真的很简单,监听 storage 事件即可。

window.addEventListener("storage", evt => handler(evt));

就这么短短一行代码即可。

storage 事件的触发条件为:

  • 使用 localStorage.setItem(key, value) 保存值
  • 使用 localStorage.setItem(key, newValue) 更新值

PS: sessionStorage 也可以。

需要注意的是当前页面监听不到这个事件。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
  </head>
  <body>
    <p></p>
    <button>发送随机消息</button>
    <script>
      const oP = document.querySelector("p");
      const oBtn = document.querySelector("button");

      window.addEventListener("storage", evt => {
        console.log(evt);
        oP.innerHTML = JSON.stringify(evt, null, 2);
      });

      oBtn.onclick = () => {
        window.localStorage.setItem("hello", window.location + new Date());
      };
    </script>
  </body>
</html>

storage-event

1

"Fira Code", SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace, Operator Mono Ssm, Consolas, 'Courier New', monospace

JavaScript 数据结构:二叉搜索树

二叉搜索树

二叉树中的节点最多只能有 2 个子节点:左侧子节点 和 右侧子节点。 二叉搜索树是二叉树的一种,但是它只允许在左侧子节点存储比父节点小的值,在右侧子节点存储大于等于父节点的值。

class Node {
  constructor(value) {
    this.left = null;
    this.value = value;
    this.right = null;
  }
}

class BinarySearchTree {
  constructor() {
    this.root = null;
  }

  /**
   * 插入节点
   * @param {} value
   */
  insert(value) {
    const newNode = new Node(value);

    if (!this.root) {
      this.root = newNode;
    } else {
      let root = this.root;
      insertNode(root, newNode);

      function insertNode(node, newNode) {
        if (node.value < newNode.value) {
          if (node.right === null) {
            node.right = newNode;
          } else {
            insertNode(node.right, newNode);
          }
        } else {
          if (node.left === null) {
            node.left = newNode;
          } else {
            insertNode(node.left, newNode);
          }
        }
      }
    }

    return true;
  }

  /**
   * 按值查找节点
   * @param {*} value
   */
  search(value) {
    const _search = (node, value) => {
      if (node === null) {
        return false;
      }

      if (node.value > value) {
        return _search(node.left, value);
      } else if (node.value < value) {
        return _search(node.right, value);
      } else {
        return node;
      }
    };

    return _search(this.root, value);
  }
}

先序遍历

先序遍历的顺序是:根 -> 左 -> 右

递归版

function preTraverse(bsTree) {
  const traverse = (node, queue) => {
    if (!node) {
      return queue;
    } else {
      queue.push(node);
      traverse(node.left, queue);
      traverse(node.right, queue);
    }
  };
  const queue = [];
  traverse(bsTree.root, queue);
  return queue;
}

循环版

function preTraverse(bsTree) {
  let stack = [];
  let queue = [];
  let node = bsTree.root;

  stack.push(node);

  while (stack.length > 0) {
    let currentNode = stack.pop();
    queue.push(currentNode);

    if (currentNode.right) {
      stack.push(currentNode.right);
    }

    if (currentNode.left) {
      stack.push(currentNode.left);
    }

  }

  return queue;
}

中序遍历

中序遍历的顺序是:左 -> 根 -> 右

递归版

function inorderTraverse(bsTree) {
  const traverse = (node, queue) => {
    if (!node) {
      return;
    }
    traverse(node.left, queue);
    queue.push(node);
    traverse(node.right, queue);
  };
  const queue = [];
  traverse(bsTree.root, queue);
  return queue;
}

循环版

function inorderTraverse(bsTree) {
  const stack = [];
  const queue = [];
  let currentNode = bsTree.root;

  while (currentNode || stack.length > 0) {
    if (currentNode != null) {
      stack.push(currentNode);
      currentNode = currentNode.left;
    } else {
      currentNode = stack.pop();
      queue.push(currentNode);
      currentNode = currentNode.right;
    }
  }

  return queue;
}

循环版的思路:

  1. 首先将所有左节点压入栈中
  2. 将栈弹出到队列中
  3. 当前节点回归到跟节点,依次入列右节点
function inorderTraverse(bsTree) {
  const stack = [];
  const queue = [];
  let currentNode = bsTree.root;

  while (currentNode) {
    currentNode = currentNode.left;
    stack.push(currentNode);
  }

  while (stack.length) {
    queue.push(stack.pop());
  }

  currentNode = bsTree.root;

  while (currentNode) {
    queue.push(currentNode);
    currentNode = currentNode.right;
  }

  return queue;
}

后序遍历

后序遍历的顺序是:左 -> 右 -> 根

递归版

function postorderTraverse(bsTree) {
  const traverse = (node, queue) => {
    if (!node) {
      return;
    }

    traverse(node.left, queue);
    traverse(node.right, queue);
    queue.push(node);
  };
  const queue = [];
  traverse(bsTree.root, queue);
  return queue;
}

循环版

function postorderTraverse(bsTree) {
  let stack = []
  let queue = []
  let node = bsTree.root
  stack.push(node)
  
  while (stack.length) {
    node = stack.pop()
    if (node.left) {
      stack.push(node.left)
    }
    if (node.right) {
      stack.push(node.right)
    }
    queue.unshift(node)
  }
  
  return queue
}

题目

1. 层级遍历

问题描述

给定一个二叉树,返回其按层次遍历的节点值。 (即逐层地,从左到右访问所有节点)。

例如:

          11
       /      \
      7       15
     / \     /  \
    3   6   12  16

层级遍历结果:

[
  [11],
  [7, 15],
  [3, 6, 12, 16]
]

解题思路

  • 建立一个queue
  • 先把根节点放进去,这时候找根节点的左右两个子节点
  • 去掉根节点,此时queue里的元素就是下一层的所有节点
  • 用for循环遍历,将结果存到一个一维向量里
  • 遍历完之后再把这个一维向量存到二维向量里
  • 以此类推,可以完成层序遍历
function levelTraverse(bsTree) {
  let queue = []
  let ret = []
  let node = bsTree.root
  queue.push(node)
  
  while (queue.length) {
    const count = queue.length
    const subRet = []
    
    while (count > 0) {
      node = queue.pop()
      subRet.push(node)
      if (node.left) queue.push(node.left)
      if (node.right) queue.push(node.right)
    }
    
    ret.push(subRet)
  }
  
  return subRet
}

vdom 原理解析与简单实现

vdom 原理解析与简单实现

0. 什么是 vnode

相信大部分前端同学之前早已无数次听过或了解过 vnode(虚拟节点),那么什么是 vnode? vnode 应该是什么样的?
如果不使用前端框架,我们可能会写出这样的页面:

<html>
  <head>
    <title></title>
  </head>
  <body>
    <div></div>
    <script></script>
  </body>
</html>

不难发现,整个文档树的根节点只有一个 html,然后嵌套各种子标签,如果使用某种数据结构来表示这棵树,那么它可能是这样。

{
  tagName: 'html',
  children: [
    {
      tagName: 'head',
      children: [
        {
          tagName: 'title'
        }
      ]
    },

    {
      tagName: 'body',
      children: [
        {
          tagName: 'div'
        },

        {
          tagName: 'script'
        }
      ]
    }
  ]
}

但是实际开发中,整个文档树中headscript 标签基本不会有太大的改动。频繁交互可能改动的应当是 body 里面的除 script 的部分,所以构建 虚拟节点树 应当是整个 HTML 文档树的一个子树,而这个子树应当保持和 HTML 文档树一致的数据结构。它可能是这样。

<html>
  <head>
    <title></title>
  </head>
  <body>
    <div id="root">
      <div class="header"></div>
      <div class="main"></div>
      <div class="footer"></div>
    </div>
    <script></script>
  </body>
</html>

这里应当构建的 虚拟节点树 应当是 div#root 这棵子树:

{
  tagName: 'div',
  children: [
    {
      tagName: 'div',
    },
    {
      tagName: 'div',
    },
    {
      tagName: 'div',
    },
  ]
}

到这里,vnode 的概念应当很清晰了,vnode 是用来表示实际 dom 节点的一种数据结构,其结构大概长这样。

{
  tagName: 'div',
  attrs: {
    class: 'header'
  },
  children: []
}

一般,我们可能会这样定义 vnode

// vnode.js
export const vnode = function vnode() {}

1. 从 JSX 到 vnode

使用 React 会经常写 JSX,那么如何将 JSX 表示成 vnode?这里可以借助 @babel/plugin-transform-react-jsx 这个插件来自定义转换函数,
只需要在 .babelrc 中配置:

{
  "plugins": [
    [
      "@babel/plugin-transform-react-jsx",
      {
        "pragma": "window.h"
      }
    ]
  ]
}

然后在 window 对象上挂载一个 h 函数:

// h.js
const flattern = arr => [].concat.apply([], arr)

window.h = function h(tagName, attrs, ...children) {
  const node = new vnode()
  node.tagName = tagName
  node.attrs = attrs || {}
  node.children = flattern(children)

  return node
}

测试一下:

jsx->vnode

2. 渲染 vnode

现在我们已经知道了如何构建 vnode,接下来就是将其渲染成真正的 dom 节点并挂载。

// 将 vnode 创建为真正的 dom 节点
export function createElement(vnode) {
  if (typeof vnode !== 'object') {
    // 文本节点
    return document.createTextNode(vnode)
  }

  const el = document.createElement(vnode.tagName)
  setAttributes(el, vnode.attrs)
  vnode.children.map(createElement).forEach(el.appendChild.bind(el))
  return el
}

// render.js
export default function render(vnode, parent) {
  parent = typeof parent === 'string' ? document.querySelector(parent) : parent
  return parent.appendChild(createElement(vnode))
}

这里的逻辑主要为:

  1. 根据 vnode.tagName 创建元素
  2. 根据 vnode.attrs 设置元素的 attributes
  3. 遍历 vnode.children 并将其创建为真正的元素,然后将真实子元素节点 append 到第 1 步创建的元素

3. diff vnode

第 2 步已经实现了 vnodedom 节点的转换与挂载,那么接下来某一个时刻 dom 节点发生了变化,如何更新 dom树?显然不能无脑卸载整棵树,然后挂载新的树,最好的办法还是找出两棵树之间的差异,然后应用这些差异。

diff-2-vnode

在写 diff 之前,首先要定义好,要 diff 什么,明确 diff 的返回值。比较上图两个 vnode,可以得出:

  1. 更换第 1、2、3li 的内容
  2. ul 下创建两个 li,这两个 li 为 第 4 个第 5 个子节点

那么可能得返回值为:

{
  "type": "UPDATE",
  "children": [
    {
      "type": "UPDATE",
      "children": [
        {
          "type": "REPLACE",
          "newVNode": 0
        }
      ],
      "attrs": []
    },
    {
      "type": "UPDATE",
      "children": [
        {
          "type": "REPLACE",
          "newVNode": 1
        }
      ],
      "attrs": []
    },
    {
      "type": "UPDATE",
      "children": [
        {
          "type": "REPLACE",
          "newVNode": 2
        }
      ],
      "attrs": []
    },
    {
      "type": "CREATE",
      "newVNode": {
        "tagName": "li",
        "attrs": {},
        "children": [
          3
        ]
      }
    },
    {
      "type": "CREATE",
      "newVNode": {
        "tagName": "li",
        "attrs": {},
        "children": [
          4
        ]
      }
    }
  ],
  "attrs": []
}

diff 的过程中,要保证节点的父节点正确,并要保证该节点在父节点 的子节点中的索引正确(保证节点内容正确,位置正确)。diff 的核心流程:

  • case CREATE: 旧节点不存在,则应当新建新节点
  • case REMOVE: 新节点不存在,则移出旧节点
  • case REPLACE: 只比较新旧节点,不比较其子元素,新旧节点标签名或文本内容不一致,则应当替换旧节点
  • case UPDATE: 到这里,新旧节点可能只剩下 attrs 和 子节点未进行 diff,所以直接循环 diffAttrs 和 diffChildren 即可
/**
 * diff 新旧节点差异
 * @param {*} oldVNode
 * @param {*} newVNode
 */
export default function diff(oldVNode, newVNode) {
  if (isNull(oldVNode)) {
    return { type: CREATE, newVNode }
  }

  if (isNull(newVNode)) {
    return { type: REMOVE }
  }

  if (isDiffrentVNode(oldVNode, newVNode)) {
    return { type: REPLACE, newVNode }
  }

  if (newVNode.tagName) {
    return {
      type: UPDATE,
      children: diffVNodeChildren(oldVNode, newVNode),
      attrs: diffVNodeAttrs(oldVNode, newVNode)
    }
  }
}

4. patch 应用更新

知道了两棵树之前的差异,接下来如何应用这些更新?在文章开头部分我们提到 dom 节点树应当只有一个根节点,同时 diff 算法是保证了虚拟节点的位置和父节点是与 dom 树保持一致的,那么 patch 的入口也就很简单了,从 虚拟节点的挂载点开始递归应用更新即可。

/**
 * 根据 diff 结果更新 dom 树
 * 这里为什么从 index = 0 开始?
 * 因为我们是使用树去表示整个 dom 树的,传入的 parent 即为 dom 挂载点
 * 从根节点的第一个节点开始应用更新,这是与整个dom树的结构保持一致的
 * @param {*} parent
 * @param {*} patches
 * @param {*} index
 */
export default function patch(parent, patches, index = 0) {
  if (!patches) {
    return
  }

  parent = typeof parent === 'string' ? document.querySelector(parent) : parent
  const el = parent.childNodes[index]

  /* eslint-disable indent */
  switch (patches.type) {
    case CREATE: {
      const { newVNode } = patches
      const newEl = createElement(newVNode)
      parent.appendChild(newEl)
      break
    }

    case REPLACE: {
      const { newVNode } = patches
      const newEl = createElement(newVNode)
      parent.replaceChild(newEl, el)
      break
    }

    case REMOVE: {
      parent.removeChild(el)
      break
    }

    case UPDATE: {
      const { attrs, children } = patches

      patchAttrs(el, attrs)

      for (let i = 0, len = children.length; i < len; i++) {
        patch(el, children[i], i)
      }

      break
    }
  }
}

总结

至此,vdom 的核心 diffpatch 都已基本实现。在测试 demo 中,不难发现 diff 其实已经很快了,但是 patch 速度会比较慢,所以这里留下了一个待优化的点就是 patch

本文完整代码均在这个仓库

JavaScript 数据结构:栈和队列

栈遵循 后进先出 的原则。

/**
 * 栈
 * 后进先出
 */
class Stack {
  constructor() {
    this.items = [];
  }

  // 添加元素
  push(value) {
    this.items.push(value);
  }

  // 返回栈顶的元素
  peek() {
    return this.items[this.items.length - 1];
  }

  // 移出栈顶的元素
  pop() {
    return this.items.pop();
  }
  
  /**
   * 判断栈是否为空
   */
  isEmpty() {
    return this.items.length <= 0;
  }
}

队列

队列遵循 先进先出 的原则。

/**
 * 队列
 * 先进先出
 */
class Queue {
  constructor() {
    this.items = [];
  }

  /**
   * 添加元素
   */
  enqueue(value) {
    this.items.push(value);
  }

  /**
   * 移出队列的第一个元素
   */
  dequeue() {
    return this.items.shift();
  }
}

常见题目

1. 有效的括号

问题描述

给定一个只包括 '(',')','{','}','[',']'的字符串,判断字符串是否有效。

解题思路

使用 来解决。

  • 遍历字符串,如果是左括号便压入栈中。
  • 如果遇到右括号,分类谈论:
    1. 如果当前栈为空,返回 false
    2. 取出栈顶元素,如果与之匹配继续循环,否则返回 false
/**
 * 给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。
 * @param {*} str
 */
function isValidStr(str) {
  let arr = str.split("");
  let stack = new Stack();

  function isMatchStr(a, b) {
    return (
      (a === "(" && b === ")") ||
      (a === "[" && b === "]") ||
      (a === "{" && b === "}")
    );
  }

  while (arr.length) {
    let char = arr.shift();

    if (char === "(" || char === "[" || char === "{") {
      stack.push(char);
    } else {
      if (stack.isEmpty()) {
        return false;
      } else if (isMatchStr(stack.pop(), char)) {
        continue;
      } else {
        return false;
      }
    }
  }

  return true;
}

2. 用两个栈实现队列

问题描述

用两个栈来实现一个队列,完成队列的 enqueuedequeue 操作。

解题思路

in 栈用于处理 入列 操作,out 栈用于处理 出列 操作。

  • 入列的值直接压入 in
  • 出列时,将 in 栈的元素出栈压入到 out 栈,然后返回 out 栈栈顶的元素(这样便保持了队列 先进先出 的原则)。
class TwoStackQueue {
  constructor() {
    this.inStack = new Stack();
    this.outStack = new Stack();
  }

  enqueue(value) {
    this.inStack.push(value);
  }

  dequeue() {
    while (!this.inStack.isEmpty()) {
      this.outStack.push(this.inStack.pop());
    }

    return this.outStack.pop();
  }
}

3. 栈的压入、弹出序列

问题描述

输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列 1,2,3,4,5 是某栈的压入顺序,序列 4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。(注意:这两个序列的长度是相等的)

解题思路

初始化一个辅助栈,遍历压入顺序,执行入栈操作,入栈的同时,如果辅助栈非空,同时弹出序列的当前值与辅助栈栈顶元素相同,则辅助栈出栈。最后判断辅助是否为空,为空这是正确的顺序,否则不是。例如:

入栈:1, 2, 3, 4, 5

出栈:4, 5, 3, 2, 1

首先,1 入栈,此时栈顶元素 1 != 4,继续入栈;

2 != 4,入栈;

3 != 4 入栈;

4 == 4,入栈后出栈(这时候栈内元素为 1, 2, 3);

3 != 5,入栈(这时候栈内元素为 1, 2, 3, 5),5 == 5 出栈,3 == 3 出栈...

最后栈为空,顺序匹配。

function isPopOrder(pushOrder, popOrder) {
  let stack = new Stack();
  let popIndex = 0;

  for (let pushIndex = 0; pushIndex < pushOrder.length; pushIndex++) {
    stack.push(pushOrder[pushIndex]);

    while (!stack.isEmpty() && stack.peek() === popOrder[popIndex]) {
      stack.pop();
      popIndex++;
    }
  }

  return stack.isEmpty();
}

mac 配置开发环境

php

mac 系统自带 php,命令行输入 php -v 即可查看版本信息。

php-composer 的安装:

php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php -r "if (hash_file('sha384', 'composer-setup.php') === '48e3236262b34d30969dca3c37281b3b4bbe3221bda826ac6a9a62d6444cdb0dcd0615698a5cbe587c3f0fe57a54d8f5') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
php composer-setup.php
php -r "unlink('composer-setup.php');"

mv composer.phar /usr/local/bin/composer

完成后,键入 composer ,如果有相关信息输出即安装成功。

使用 AST 解析 React TypeScript Component 接口定义

使用 AST 解析 React TypeScript Component 接口定义

背景

团队使用 TypeScript 进行 React 组件开发。开发组件的同时,需要为组件撰写文档(使用 Markdown 编写文档)。文档中需要对组件的 props 定义进行说明。

Antd 组件的 API 说明

在开发组件的时候,是编写组件 props 的接口定义。这时候就希望能够偷懒了,直接抽取组件源代码中的接口定义和注释来生成这部分文档。

分析

基于上面背景进行分析,可以发现核心需求是抽取组件接口定义和注释形成特定格式的文档。

import React from "react";

export interface IProps {
  // 文字
  text: string;
  // 点击事件
  onClick: () => void;
}

const Button = ({ text }) => {
  return <button>{text}</button>;
};

export default Button;

其实就是抽取 IProps 定义然后转换成其他格式文档。

思路

在人人都说 AST 的今天,我们肯定是要使用 AST 来完成。

  1. 将源代码转换成 AST
  2. 遍历 AST 抽取 interface 定义
  3. 继续遍历 interface 抽取各个字段定义 definition
code -> ast -> interface -> definitions

源代码 -> AST

站在巨人的肩膀上,使用 babel 解析源代码即可。

const parser = require("@babel/parser");

function transformCode2Ast(code) {
  return parser.parse(code, {
    sourceType: "module",
    plugins: [
      "jsx",
      "typescript",
      "asyncGenerators",
      "bigInt",
      "classProperties",
      "classPrivateProperties",
      "classPrivateMethods",
      ["decorators", { decoratorsBeforeExport: false }],
      "doExpressions",
      "dynamicImport",
      "exportDefaultFrom",
      "exportNamespaceFrom",
      "functionBind",
      "functionSent",
      "importMeta",
      "logicalAssignment",
      "nullishCoalescingOperator",
      "numericSeparator",
      "objectRestSpread",
      "optionalCatchBinding",
      "optionalChaining",
      ["pipelineOperator", { proposal: "minimal" }],
      "throwExpressions",
      "topLevelAwait",
      "estree",
    ],
  });
}

转换上面的 Button 组件,AST 大概长这样:

Node {
  type: 'File',
  start: 0,
  end: 207,
  loc:
   SourceLocation {
     start: Position { line: 1, column: 0 },
     end: Position { line: 16, column: 0 } },
  errors: [],
  program:
   Node {
     type: 'Program',
     start: 0,
     end: 207,
     loc: SourceLocation { start: [Position], end: [Position] },
     sourceType: 'module',
     interpreter: null,
     body: [ [Node], [Node], [Node], [Node] ] },
  comments:
   [ { type: 'CommentLine',
       value: ' 文字',
       start: 57,
       end: 62,
       loc: [SourceLocation] },
     { type: 'CommentLine',
       value: ' 点击事件',
       start: 81,
       end: 88,
       loc: [SourceLocation] } ] }

遍历 AST

同样站在巨人的肩膀上,使用 ast-typesast 进行遍历。

const { visit } = require("ast-types");

function findInterface(ast) {
  let ret = Object.create(null);
  let currentInterface = null;

  visit(ast, {
    visitTSInterfaceDeclaration(nodePath) {
      currentInterface = nodePath.value.id.name;
      this.traverse(nodePath);
    },
    visitTSPropertySignature(nodePath) {
      ret[currentInterface] = ret[currentInterface] || [];
      ret[currentInterface].push(nodePath.value);
      return false;
    },
  });
  return ret;
}

AST 遍历抽取 Interface 后的结果大概长这样。

Node {
  type: 'TSPropertySignature',
  start: 65,
  end: 78,
  loc: [SourceLocation],
  key: [Node],
  computed: false,
  typeAnnotation: [Node],
  leadingComments: [Array],
  trailingComments: [Array] },
Node {
  type: 'TSPropertySignature',
  start: 91,
  end: 111,
  loc: [SourceLocation],
  key: [Node],
  computed: false,
  typeAnnotation: [Node],
  leadingComments: [Array] } ] }

会发现各个定义在 typeAnnotation 中,这时候对它进行解析即可。

解析 TypeAnnotation

通过分析 typeAnnotation 很容易写出。

const get = require("lodash/get");

function parseTSTypeReference(typeName) {
  const type = get(typeName, "type");
  switch (type) {
    case "TSQualifiedName":
      return `${get(typeName, "left.name")}.${get(typeName, "right.name")}`;
    default:
      return `Unknown ReferenceType`;
  }
}

function parseTSFunctionType(parameters, typeAnnotation) {
  const parseTSFunctionParameters = (parameters) => {
    if (!parameters || !parameters.length) {
      return `()`;
    }
    let args = parameters.map((parameter) => {
      return `${get(parameter, "name")}: ${parseTypeAnnotation(
        get(parameter, "typeAnnotation.typeAnnotation")
      )}`;
    });
    return "( " + args.join(", ") + ")";
  };
  const parseTSFunctionReturn = (typeAnnotation) => {
    const type = get(typeAnnotation, "type");
    switch (type) {
      case "TSVoidKeyword":
        return "void";
      case "TSTypeReference":
        return parseTSTypeReference(get(typeAnnotation, "typeName"));
      default:
        return `Unknown FunctionType`;
    }
  };
  return `${parseTSFunctionParameters(parameters)} => ${parseTSFunctionReturn(
    typeAnnotation
  )}`;
}

function parseTSTypeLiteral(members) {
  const ret = parseInterfaceDefinitions(members);
  let args = ret.map((t) => `${t.name}: ${t.type}`);
  return "{ " + args.join(", ") + " }";
}

function parseTypeAnnotation(typeAnnotation) {
  const type = get(typeAnnotation, "type");
  switch (type) {
    case "TSNumberKeyword":
    case "TSStringKeyword":
    case "TSBoleanKeyword":
    case "TSNullKeyword":
    case "TSUndefinedKeyword":
    case "TSSymbolKeyword":
    case "TSAnyKeyword":
      return type.match(/TS(\w+)Keyword/)[1].toLowerCase();
    case "TSUnionType":
      return get(typeAnnotation, "types", [])
        .map((type) => get(type, "literal.value"))
        .join(" | ");
    case "TSFunctionType":
      return parseTSFunctionType(
        get(typeAnnotation, "parameters"),
        get(typeAnnotation, "typeAnnotation.typeAnnotation")
      );
    case "TSTypeReference":
      return parseTSTypeReference(get(typeAnnotation, "typeName"));
    case "TSTypeLiteral":
      return parseTSTypeLiteral(get(typeAnnotation, "members"));
    default:
      return "UnKnowType";
  }
}

function parseInterfaceDefinitions(nodePaths) {
  const parseInterfaceDefinitionsNode = (nodePath) => {
    const name = get(nodePath, "key.name");
    const comments = get(nodePath, "leadingComments.0.value", "")
      .trim()
      .split(/[\r\n]/)
      .map((str) => str.trim().replace(/^\*/g, "").trim())
      .filter(Boolean);
    const typeAnnotation = get(nodePath, "typeAnnotation.typeAnnotation");
    const type = parseTypeAnnotation(typeAnnotation);
    return { name, type, comments };
  };
  return nodePaths.map(parseInterfaceDefinitionsNode);
}

至此,可以得到 Button 的接口定义。

[
  [
    {
      name: "text",
      type: "string",
      comments: ["文字"],
    },
    {
      name: "onClick",
      type: "() => void",
      comments: ["点击事件"],
    },
  ],
];

接下来只要将解析后的结果转成想要的格式即可。

测试

function parseTypeScriptComponentInterface(code) {
  let ast = transformCode2Ast(code);
  let interfaces = findInterfaces(ast);
  let definitions = Object.keys(interfaces).reduce((a, c) => {
    a[c] = a[c] || [];
    a[c].push(parseInterfaceDefinitions(interfaces[c]));
    return a;
  }, Object.create(null));
  return definitions;
}

const code = `
import React from 'react';

export interface IProps {
  /**
   * button 
   * 显示文字
   */
  text: string;
  // 点击事件
  onClick: () => void;
  // 属性 3
  props3: (arg: any) => void;
  // 属性 4
  props4: (arg: { name: string, age: number }) => React.Node
}

const Button = ({ text }) => {
  return <button>{text}</button>;
};

export default Button;
`;

let ret = parseTypeScriptComponentInterface(code);

输出:

{
  "IProps": [
    [
      {
        "name": "text",
        "type": "string",
        "comments": [
          "button",
          "显示文字"
        ]
      },
      {
        "name": "onClick",
        "type": "() => void",
        "comments": [
          "点击事件"
        ]
      },
      {
        "name": "props3",
        "type": "( arg: any) => void",
        "comments": [
          "属性 3"
        ]
      },
      {
        "name": "props4",
        "type": "( arg: { name: string, age: number }) => React.Node",
        "comments": [
          "属性 4"
        ]
      }
    ]
  ]
}

源代码地址

JavaScript 实现常见排序算法

冒泡排序

冒泡排序重复遍历数组,依次比较 2 个元素,如这 2 个元素顺序错误,就交换位置。

  • 比较相邻的元素,若前者大于后者就交换
  • 对每一对相邻的元素执行上一部动作
  • 经过前 2 步,最大的元素已经到数组最后一项,对剩下的前 n - 1 项重复前 2 步即可
function bubbleSort(arr) {
  let len = arr.length;

  for (let i = 0; i < len - 1; i++) {
    for (let j = 0; j < len - 1 - i; j++) {
      if (arr[j] > arr[j + 1]) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
      }
    }
  }

  return arr;
}

选择排序

选择排序时间复杂度 O(n²)

  • 在未排序数列中,找到最小元素,存放到起始位置
  • 从剩余未排序数列中,找到最小元素,放到已排序数列末尾
  • 重复第 2 步知道完成
function selectSort(arr) {
  let len = arr.length
  let minIndex = 0
  
  for (let i = 0; i < len; i++) {
    for (let j = i + 1; j < len; j++) {
      if (arr[j] < arr[minIndex]) {
        minIndex = j
      }
    }
    
    [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]]
  }
  
  return arr
}

插入排序

插入排序首先构建有序数列,然后在有序数列中从后向前插入未排序元素。

  • 将第 1 个元素看作已排序数列,将其后元素看作未排序数列
  • 遍历未排序数列,将元素插入到已排序数列中的合适位置
function insertSort(arr) {
  let len = arr.length
  
  for (let i = 1; i < len; i++) {
    let j = i - 1;
    let tmp = arr[i]
    
    while (j >= 0 && arr[j] > tmp) {
      arr[j + 1] = arr[j]
      j--
    }
    
    arr[j + 1] = tmp
  }
  
  return arr
}

希尔排序

希尔排序是插入排序的更高效改进版。建立于插入排序的 2 点特性:

  • 插入排序对几乎已经排列好的数列排序效率高
  • 插入排序低效是因为 1 次只能移动 1 位

希尔排序的步骤。

  • 选择一个增量序列,t1, t2, ..., tk,其中 ti > tj, tk = 1
  • 按增量序列个数 k,对序列进行 k 趟排序
  • 当增量因子为 1 时,整个序列进行 1 次排序
function shellSort(arr) {
  let len = arr.length;
  let gap = 1;

  while (gap < len / 3) {
    gap = gap * 3 + 1;
  }

  for (; gap > 0; gap = Math.floor(gap / 3)) {
    for (let i = gap; i < len; i++) {
      let tmp = arr[i];
      let j = i - gap;

      while (j >= 0 && arr[j] > tmp) {
        arr[j + gap] = arr[j];
        j -= gap;
      }

      arr[j + gap] = tmp;
    }
  }

  return arr;
}

归并排序

归并排序是采用分治法的典型例子。

  • 拆分数组为 2 部分
  • 比较 2 部分数列
  • 重复前 2 步
function mergeSort(arr) {
  let len = arr.length
  
  if (len <= 1) {
    return arr
  }
  
  let middle = len >> 1
  let left = arr.slice(0, middle)
  let right = arr.slice(middle)
  
  const merge = (left, right) => {
    let ret = []
    
    while (left.length && right.length) {
      if (left[0] < right[0]) {
        ret.push(left.shift())
      } else {
        ret.push(right.shift())
      }
    }
    
    while (left.length) {
      ret.push(left.shift())
    }
    
    while (right.length) {
      ret.push(right.shift())
    }
    
    return ret
  }
  
  return merge(mergeSort(left), mergeSort(right))
}

快速排序

快速排序采用分治法将 1 个数列分成 2 个子数列进行处理。

  • 从数列中挑出一个基准值
  • 将把基准值小的值,放在前,大的放在后
  • 递归地重新排列划分后的 2 个子数列
function quickSort(arr, left, right) {
  if (arr.length <= 1) {
    return arr;
  }

  let pivotIndex = Math.floor(arr.length / 2);
  let pivot = arr.splice(pivotIndex, 1)[0];
  let left = [];
  let right = [];

  for (let i = 0; i < arr.length; i++) {
    if (arr[i] < pivot) {
      left.push(arr[i]);
    } else {
      right.push(arr[i]);
    }
  }

  return quickSort(left).concat([pivot], quickSort(right));
}

git 如何恢复被误删的本地分支

假设场景:

在某个项目下,我新建了一个本地分支 test,并在该分支做出了一些改动,但是我在没有 push 该分支代码到远程仓库之前手滑删除了该分支,那么如何在本地恢复这个分支?该场景的相关操作如下:

git branch test
git checkout test

# 做了一些改动

git status
git add .
git commit -m "some info"

git chekout - # 切到上一个分支
git branch -D test # 手滑删了 test

经过以上操作,本地更改的代码是没有了的。但是 git 中有日志(因为 git commit 到本地了),通过 git log -g 查看 commit 的记录 hash 值,然后执行 git branch test hash 即可恢复。

总结一下:

git log -g # 查找本地提交记录 hash
git branch new_branch_name hash # 新建分支

Vue 组件开发总结

前言

本文接下来会一步一步模仿造一个低配版的 Element 的对话框和弹框组件

正文

Vue 单文件组件开发

当使用vue-cli初始化一个项目的时候,会发现 src/components 文件夹下有一个 HelloWorld.vue 文件,这便是单文件组件的基本开发模式。

// 注册
Vue.component("my-component", {
  template: "<div>A custom component!</div>"
});

// 创建根实例
new Vue({
  el: "#example"
});

接下来,开始写一个 dialog 组件。

Dialog

目标对话框组件的基本样式如图:

dialog基本样式

根据目标样式,可以总结出:

  1. dialog 组件需要一个 title props 来标示弹窗标题
  2. dialog 组件需要在按下 确定 按钮时 发射确定 事件(即告诉父组件确定了)
  3. 同理,dialog 组件需要 发射取消 事件
  4. dialog 组件需要提供一个插槽,便于自定义内容

那么,编码如下:

<template>
  <div class="ta-dialog__wrapper">
    <div class="ta-dialog">
      <div class="ta-dialog__header">
        <span>{{ title }}</span>
        <i class="ios-close-empty" @click="handleCancel()"></i>
      </div>
      <div class="ta-dialog__body">
        <slot></slot>
      </div>
      <div class="ta-dialog__footer">
        <button @click="handleCancel()">取消</button>
        <button @click="handleOk()">确定</button>
      </div>
    </div>
  </div>
</template>

<script>
  export default {
    name: "Dialog",

    props: {
      title: {
        type: String,
        default: "标题"
      }
    },

    methods: {
      handleCancel() {
        this.$emit("cancel");
      },

      handleOk() {
        this.$emit("ok");
      }
    }
  };
</script>

这样便完成了dialog组件的开发,使用方法如下:

<ta-dialog title="弹窗标题" @ok="handleOk" @cancel="handleCancel">
  <p>我是内容</p>
</ta-dialog>

这时候发现一个问题,通过使用 v-if 或者 v-show 来控制弹窗的展现时,没有动画!!!,看上去很生硬。教练,我想加动画 ,这时候就该 transition 组件上场了。使用 transition 组件结合 css能做出很多效果不错的动画。接下来增强 dialog 组件动画,代码如下:

<template>
  <transition name="slide-down">
    <div class="ta-dialog__wrapper" v-if="isShow">
      <!-- 省略 -->
    </div>
  </transition>
</template>

<script>
  export default {
    data() {
      return {
        isShow: true
      };
    },

    methods: {
      handleCancel() {
        this.isShow = false;
        this.$emit("cancel");
      },

      handleOk() {
        this.isShow = true;
        this.$emit("ok");
      }
    }
  };
</script>

可以看到 transition 组件接收了一个 name props,那么怎么编写 css 完成动画呢?很简单的方式,写出两个
关键 class (css 的 className)样式即可:

.slide-down-enter-active {
  animation: dialog-enter ease 0.3s;
}

.slide-down-leave-active {
  animation: dialog-leave ease 0.5s;
}

@keyframes dialog-enter {
  from {
    opacity: 0;
    transform: translateY(-20px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes dialog-leave {
  from {
    opacity: 1;
    transform: translateY(0);
  }

  to {
    opacity: 0;
    transform: translateY(-20px);
  }
}

就是这么简单就开发出了效果还不错的动效,注意 transition 组件的 nameslide-down ,而编写的动画的关键 classNameslide-down-enter-activeslide-down-leave-active

封装DialogMessageBox

Element 的MessageBox的使用方法如下:

this.$confirm("此操作将永久删除该文件, 是否继续?", "提示", {
  confirmButtonText: "确定",
  cancelButtonText: "取消",
  type: "warning"
})
  .then(() => {
    this.$message({
      type: "success",
      message: "删除成功!"
    });
  })
  .catch(() => {
    this.$message({
      type: "info",
      message: "已取消删除"
    });
  });

看到这段代码,我的感觉就是好神奇好神奇好神奇(惊叹三连)。仔细看看,这个组件其实就是一个封装好的 dialog ,

Element MessageBox效果

接下来,我也要封装一个这样的组件。首先,整理下思路:

  1. Element 的使用方法是 this.$confirm ,这不就是挂到 Vueprototype 上就行了
  2. Element 的 then 是确定,catch 是取消,promise 就可以啦

整理好思路,我就开始编码了:

import Vue from "vue";
import MessgaeBox from "./src/index";

const Ctur = Vue.extend(MessgaeBox);
let instance = null;

const callback = action => {
  if (action === "confirm") {
    if (instance.showInput) {
      instance.resolve({ value: instance.inputValue, action });
    } else {
      instance.resolve(action);
    }
  } else {
    instance.reject(action);
  }

  instance = null;
};

const showMessageBox = (tip, title, opts) =>
  new Promise((resolve, reject) => {
    const propsData = { tip, title, ...opts };

    instance = new Ctur({ propsData }).$mount();
    instance.reject = reject;
    instance.resolve = resolve;
    instance.callback = callback;

    document.body.appendChild(instance.$el);
  });

const confirm = (tip, title, opts) => showMessageBox(tip, title, opts);

Vue.prototype.$confirm = confirm;

至此,可能会疑惑怎么 callback 呢,其实我编写了一个封装好的 dialog 并将其命名为 MessageBox ,
它的代码中,有这样两个方法:

onCancel() {
  this.visible = false
  this.callback && (this.callback.call(this, 'cancel'))
},

onConfirm() {
  this.visible = false
  this.callback && (this.callback.call(this, 'confirm'))
},

没错,就是确定取消时进行callback。我还想说一说Vue.extend,代码中引入了MessageBox,
我不是直接new MessageBox而是借助new Ctur,因为这样可以定义数据(不仅仅是props),例如:

instance = new Ctur({ propsData }).$mount();

这时候,页面上其实是还没有MessageBox的,我们需要执行:

document.body.appendChild(instance.$el);

如果你直接这样,你可能会发现 MessageBox 打开的时候没有动画,而关闭的时候有动画。解决方法也很简单,
appendChild的时候让其仍是不可见,然后使用类这样的代码:

Vue.nextTick(() => (instance.visible = true));

这样就有动画了。

总结

  1. 通过 transitioncss 实现不错的动画。其中,transition 组件的 name 决定了编写 css 的两个关键
    类名为 [name]-enter-active[name]-leave-active
  2. 通过 Vue.extend 继承一个组件的构造函数(不知道怎么说合适,就先这样说),然后通过这个构造函数,便可以
    实现组件相关属性的自定义(使用场景:js 调用组件)
  3. js 调用组件时,为了维持组件的动画效果可以先 document.body.appendChild 然后 Vue.nextTick(() => instance.visible = true)

实现 asyncSum

字节跳动面试题目:利用已知函数 add 实现 asyncSum

function add (a, b, callback) {
  callback(a + b)
}

async function asyncSum(...args) {
  // 具体实现
}

// await asyncSum(1, 2, 3, 4, 5) 的结果应当为 1 + 2 + 3 + 4+ 5 = 15

解题思路:

  1. 实现任意参数的求和函数 sum
  2. 求和函数依赖了异步的 add 函数,add 函数一次只能计算两数之和,所以任意参数要分组,每组两个参数调用 add 函数
  3. 重复第 2 步,直到 可分组参数数量为 1
[1, 2, 3, 4, 5] // 传参
[[1, 2], [3, 4], [5, 0]] // 分组
[3, 7, 5] // 各组求和
[[3, 7], [5, 0]] // 继续分组
[10, 5] // 继续各组求和
[[10, 5]] // 继续分组
[15] // 求和

首先,实现分组函数,将参数分成 2 个一组。

function chunk(arr) {
  let ret = []

  for (let i = 0; i < arr.length; i += 2) {
    ret.push([arr[i], arr[i + 1] ? arr[i + 1] : 0])
  }

  return ret
}

chunk([1, 2, 3, 4, 5]) // [[1, 2], [3, 4], [5, 0]]

然后,实现 sum 函数对分组的数字依次求和。由于需要强依赖 add 函数,首先将 add 函数 promisify

function add (a, b, callback) {
  callback(a + b)
}

function asyncAdd(a, b) {
  return new Promise(resolve => add(a, b, sum => resolve(sum)))
}

实现 sum 函数。

function sum(nums) {
  return Promise.all(chunk(nums).map(([a, b]) => asyncAdd(a, b)))
}

// await sum([1, 2, 3, 4, 5]) 结果 [3, 7, 5]

至此,可以看出数字长度在缩减,所以只需要继续拆分数组调用 sum 函数直到数字长度为 1 即为最终求和。

async function asyncSum(...args) {
  let ret = await sum(args)

  while (ret.length > 1) {
    ret = await sum(ret)
  }

  return ret[0]
}

完整实现。

function chunk(arr) {
  let ret = []

  for (let i = 0; i < arr.length; i += 2) {
    ret.push([arr[i], arr[i + 1] ? arr[i + 1] : 0])
  }

  return ret
}

function add (a, b, callback) {
  callback(a + b)
}

function asyncAdd(a, b) {
  return new Promise(resolve => add(a, b, sum => resolve(sum)))
}

function sum(nums) {
  return Promise.all(chunk(nums).map(([a, b]) => asyncAdd(a, b)))
}

async function asyncSum(...args) {
  let ret = await sum(args)

  while (ret.length > 1) {
    ret = await sum(ret)
  }

  return ret[0]
}

JavaScript 实现常用设计模式

单例模式

单例模式需确保只有一个实例且可以全局访问。

实现单例模式

let __instance = (function() {
  let instance;
  return newInstance => {
    if (newInstance) instance = newInstance;
    return instance;
  };
})();

class Singleton {
  constructor(name) {
    this.name = name;

    if (!__instance()) {
      __instance(this);
    }

    return __instance();
  }
}

测试:

const s1 = new Singleton("s1");
const s2 = new Singleton("s2");

assert.strictEqual(s1, s2);
assert.strictEqual(s1.name, s2.name);

实践

单例模式需要满足只有一个实例且可全局访问即可,可以使用 JavaScript 的闭包来实现。接下来以弹窗为例:

function createPopup(content) {
  const div = document.createElement("div");
  div.innerHTML = content;
  return div;
}

将单例模式和创建弹窗代码解耦:

function createSingleton (fn) {
  let result
  return function () {
    return result || result = fn.apply(this, arguments)
  }
}

const createSingletonPopup = createSingleton(createPopup)

发布订阅模式

事件发布订阅模式可以帮助完成更松的解耦。

EventEmiiter 的简单实现

class EventEmitter {
  constructor() {
    this.listener = new Map();
  }

  on(type, fn) {
    const subs = this.listener.get(type);

    if (!subs) {
      this.listener.set(type, [fn]);
    } else {
      this.listener.set(type, [].concat(subs, fn));
    }
  }

  emit(...args) {
    const type = args[0];

    for (let listener of this.listener.get(type)) {
      listener(...args.slice(1));
    }
  }
}

测试代码:

const em = new EventEmitter();
em.on("click", counter => {
  assert.equal(counter, 1);
});
em.emit("click", 1);

代理模式

代理对象和本体对象具有一致的接口,对使用者友好。代理模式的种类有很多种,在 JavaScript 中常见的是:虚拟代理和缓存代理。

虚拟代理实现图片预加载

const myImg = (() => {
  const img = document.appendChild("img");
  document.body.appendChild(img);

  return {
    setSrc: src => (img.src = src)
  };
})();

const proxyImage = (() => {
  const img = new Image();
  img.onload = function() {
    myImg.setSrc(this.src);
  };

  return {
    setSrc: src => {
      myImg.setSrc("loading.png");
      img.src = src;
    }
  };
})();

proxyImage.setSrc("loaded.jpg");

缓存代理实现乘积计算

const multiply = function(...args) {
  return args.reduce((accu, curr) => (accu *= curr), 1);
};

const proxyMultiply = (() => {
  const cache = {};
  return (...args) => {
    let tag = args.join(",");

    if (cache[tag]) {
      return cache[tag];
    }

    return (cache[tag] = multiply.apply(null, args));
  };
})();

策略模式

顾名思义,根据不同的参数(或配置)有不同的策略(函数)。

表单验证

以表单验证为例,不同的字段应有不同的验证方法,即不同的策略。

class Checker {
  constructor(check, message) {
    this.check = check;
    this.message = message;
  }
}

class Validator {
  constructor(config) {
    this.config = config;
    this.messages = [];
  }

  validate(data) {
    for (let [k, v] of Object.entries(data)) {
      const type = this.config.get(k);
      const checker = Validator[type];
      const result = checker.check(v);

      if (!result) {
        this.messages.push(checker.message(v));
      }
    }

    return this;
  }

  isError() {
    return this.messages.length > 0;
  }
}

测试代码:

const data = {
  name: "startegy",
  age: 0
};

const config = new Map([["name", "isNotEmpty"], ["age", "isGreaterThan"]]);

Validator.isNotEmpty = new Strategy.Checker(
  val => val.length > 0,
  val => `The ${val} is empty`
);

Validator.isGreaterThan = new Strategy.Checker(
  number => number > 20,
  number => `The number ${number} is less than 20`
);

assert.equal(new Validator(config).validate(data).isError(), true);

迭代器模式

能访问到聚合对象的顺序和元素。

ES6 的 iterator 接口

const data = {
  data: [1, 2, 3, 4, 5, 6],
  [Symbol.iterator]() {
    const len = this.data.length;
    let index = 0;

    return {
      next: () => {
        return index < len
          ? { value: this.data[index++], done: false }
          : { value: undefined, done: true };
      },
      rewind: () => (index = 0)
    };
  }
};

Vue配置svg-sprite-loader以使用svg图标

svg-sprite-loader在vue中的使用

何为svg sprite

类似于CSS中的雪碧图。将svg整合在一起,呈现的时候根据symbolId来显示特定的图标。

svg spritesymbol元素

可以这样简单理解,symbol是一个个svg图标,而svg sprite则是symbol的集合,我们可以通过use来指定使用哪一个svg

vue中使用

  1. 安装svg-sprite-loader
    执行npm install --save-dev svg-sprite-loader

  2. 修改webpack.base.conf.js
    在rules下添加并修改以下配置:

{
  test: /\.svg$/,
  loader: 'svg-sprite-loader',
  include: [resolve('src/icons')],
  options: {
    symbolId: '[name]'
  }
},
{
  test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
  loader: 'url-loader',
  exclude: [resolve('src/icons')],
  options: {
    limit: 10000,
    name: utils.assetsPath('img/[name].[hash:7].[ext]')
  }
}

配置说明:

  • svg-sprite-loader:
    这里用include: [resolve('src/icons')]来假设项目中所用到svg图标文件在src/icons文件目录下,svg-sprite-loader将只处理这里的文件

  • url-loader:
    这里用xclude: [resolve('src/icons')]来告诉url-loader不要处理src/icons下的图片文件(因为这里已经交给svg-sprite-loader了)

  1. 添加icon组件
    src/components文件夹下新建文件夹icon,并新建index.vue文件,写入内容如下:
<template>
  <svg :width="width" :height="height">
    <use :xlink:href="iconName"/>
  </svg>
</template>

<script>
export default {
  name: 'Icon',

  props: {
    type: {
      default: 'sad'
    },

    width: {
      default: 50
    },

    height: {
      default: 50
    }
  },

  computed: {
    iconName() {
      return '#' + this.$props.type
    }
  }
}
</script>

<style scoped>
svg {
  fill: currentColor;
  overflow: hidden;
}
</style>
  1. 新建src/icons文件夹
    假如所有的svg文件都放在src/icons/svg文件夹下,那么新建src/icons/index.js文件,写入:
import Vue from 'vue';
import Icon from '@/components/icon';

Vue.component('icon', Icon);

// 导入所有的svg(参照webpack文档: http://webpack.github.io/docs/context.html#dynamic-requires )
~function (requireContext) {
  return requireContext.keys().map(requireContext)
}(require.context('./svg', false, /\.svg$/))

至此,如有不理解,可参照我的src目录结构示意:

├── App.vue
├── assets
│   └── logo.png
├── components
│   └── icon
│       └── index.vue
├── icons
│   ├── index.js
│   └── svg
│       ├── more.svg
│       ├── navicon.svg
│       ├── pause.svg
│       ├── play.svg
│       ├── sad.svg
│       └── wifi.svg
└── main.js
  1. main.js引入src/icons/index.js
import Vue from 'vue'
import App from './App'

import './icons/index'

Vue.config.productionTip = false

new Vue({
  el: '#app',
  template: '<App/>',
  components: { App }
})
  1. 如何使用
    以我的App.vue文件举个例子:
<template>
  <div id="app">
    <icon type="play"></icon>
    <icon type="sad"></icon>
    <icon type="more"></icon>
    <icon type="pause"></icon>
    <icon type="wifi"></icon>
  </div>
</template>

<script>
export default {
  name: 'app'
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

效果图

JavaScript 数据结构:链表

链表存储有序的元素集合,但不同于数组,链表中的元素在内存中不是连续放置的。每个元素由一个存储元素本身的节点和指向下一个元素的 引用 组成。

------------------------------------------------------------------------
|                                                                      |
|                node                      node               null     |
|           ---------------          ---------------        --------   |
|  head ->  + item | next +    ->    + item | next +   ->   + null +   |
|           ---------------          ---------------        --------   |
|                                                                      |
------------------------------------------------------------------------
class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
  }
}

class LinkList {
  constructor() {
    this.head = null;
    this.length = 0;
  }

  /**
   * 添加节点
   * @param {*} value
   */
  append(value) {
    return this.insert(value, this.length);
  }

  /**
   * 添加节点到指定位置
   * @param {*} value
   * @param {*} position
   */
  insert(value, position) {
    if (position < 0 || position > this.length) {
      return false;
    }

    let node = new Node(value);

    if (position === 0) {
      node.next = this.head;
      this.head = node;
    } else {
      let prev = null;
      let current = this.head;
      let i = 0;

      while (i++ < position) {
        prev = current;
        current = current.next;
      }

      prev.next = node;
      node.next = current;
    }

    this.length++;
    return true;
  }

  /**
   * 移出指定位置的节点
   * @param {*} position
   */
  removeAt(position) {
    if (position < 0 || position > this.length) {
      return false;
    }

    if (position === 0) {
      this.head = this.head.next;
    } else {
      let prev = null;
      let current = this.head;
      let i = 0;

      while (i++ < position) {
        prev = current;
        current = current.next;
      }

      prev.next = current.next;
    }

    this.length--;
    return true;
  }

  /**
   * 查找给定值所在索引
   * @param {*} value
   */
  indexOf(value) {
    let current = this.head;
    let index = -1;

    while (current) {
      indeex++;
      if (current.value === value) {
        return index;
      } else {
        current = current.next;
      }
    }

    return -1;
  }

  reverse() {
    let prev = null;
    let current = this.head;
    let next = null;

    while (current) {
      next = current.next;
      current.next = prev;
      prev = current;
      current = next;
    }

    this.head = prev;
  }

  traverseFromTailToHead() {
    let stack = [];
    let current = this.head;

    while (current) {
      stack.push(current);
      current = current.next;
    }

    while (stack.length) {
      stack.pop();
    }
  }

  getKthNodeFromTailToHead(k) {
    let p1 = this.head;
    let p2 = this.head;
    let i = 0;

    while (i++ < k) {
      p1 = p1.next;
    }

    while (p1) {
      p1 = p1.next;
      p2 = p2.next;
    }

    return p2;
  }
}

题目

1. 输出单链表倒数第 k 个节点

问题描述

题目:输入一个单链表,输出此链表中的倒数第 K 个节点。(去除头结点,节点计数从 1 开始)。

解题思路

倒数第 k 个节点即为整数第 n - k 个节点(n 为链表长度)

  • 定义 2 个指针同时执行链表头节点
  • 第 1 个指针前进 k 个 节点
  • 两个指针同时前进,当第 1 个指针到达链表尾时,第 2 个指针与第一个指针相距 k 个节点,第二个指指向节点即为所求
function findKthTail(linkList, k) {
  let p1 = linkList.head;
  let p2 = linkList.head;
  let i = 0;

  while (i++ < k) {
    p1 = p1.next;
  }

  while (p1) {
    p1 = p1.next;
    p2 = p2.next;
  }

  return p2; 
}

2. 判断链表是否有环

问题描述

单链表中的环是指链表末尾的节点的 next 指针不为 NULL ,而是指向了链表中的某个节点,导致链表中出现了环形结构。

0 -> 1 -> 2 -> 3 -> 4
               |    |
               6 <- 5

链表中尾节点 6 指向了 节点 3 而非 null,导致出现了环形结构。

解题思路

  • 定义 2 个快慢指针,初始均指向头节点
  • 第 1 个指针前进 1 步,第 2 个指针前进 2 步
  • 若是无环,2 个指针最后均指向 null,但若是有环,2 指针必定在一个节点处相遇,该节点不为 null
/**
 * 判断链表是否存在环
 * @param {*} linkList
 */
function isExistLoop(linkList) {
  let p1 = linkList.head;
  let p2 = linkList.head;

  while (p1 && p2.next) {
    p1 = p1.next;
    p2 = p2.next.next;

    if (p1 === p2) {
      return true;
    }
  }

  return false;
}

问题描述

定位环的起点。

解题思路

定义2个快慢指针,第一次先找到 2 个指针在环中的相遇点,然后令 p1 指向 相遇点,p2 指向头节点,同时出发(每次走过的节点相同),当 2 指针指向的节点相同时,p1 即为环的起点。

function getMeetingNode(linkList) {
  let p1 = linkList.head;
  let p2 = linkList.head;

  while (p1 && p2.next) {
    p1 = p1.next;
    p2 = p2.next.next;

    if (p1 == p2) {
      return p1;
    }
  }

  return null;
}

function getEntryOfLoop(linkList) {
  let meetingNode = linkList.getMeetingNode();

  if (!meetingNode) {
    return null;
  }

  let p1 = meetingNode;
  let p2 = linkList.head;

  while (p1 != p2) {
    p1 = p1.next;
    p2 = p2.next;
  }

  return p1;
}

Vue 技巧:无渲染组件

Vue 技巧:无渲染组件

最近,使用 Vue 开发组件时遇到了这样一个问题:开发的组件所能够自定义的 props 比较多,导致使用该组件时需要传入太多属性,数据、样式控制什么的属性都在一起了,看起来很不美观,像下面这样:

<some-component title="我是标题" :pos="[0, 0]" :size="[500, 400]" :radius="[0, 0.5]" :borderWidth="2" :borderColor="'#fff'" :data="testData" />

虽然看起来可能还好,但是实际使用时可以自定义的属性是不止这些的,这样使用起来就很不美观了。于是就有了这样一个想法:定义一个类似于 React 中常写的 theme 组件,将一些非数据相关的 props 定义到 theme 组件上,theme 组件再自动将 props 透传给其他组件使用即可。theme 组件使用起来像这样:

<theme :pos="[0, 0]" :size="[500, 400]" :radius="[0, 0.5]" :borderWidth="2" :borderColor="'#fff'" >
  <some-component title="我是标题":data="testData" />
</thmem>

开发 theme 组件

React 中,开发这样一个高阶组件 theme 是很简单的。但是在 vue 中如何开发 theme 组件以达到上面设想的使用效果?通过翻阅 Vue 的文档,发现借助 $slotsrender 函数可以做到。

export default {
  name: 'theme',

  render(h) {
    const theme = this.$attrs // 通过 $attrs 可以拿到使用该组件时定义的 props,而无需声明有哪些 props

    const merge = vNode => {
      if (!vNode.tag) {
        return
      }

      if (vNode.componentOptions) {
        let props = vNode.componentOptions.propsData
        props = Object.assign({}, theme, props)
        vNode.componentOptions.propsData = props
      } else {
        if (!vNode.data) {
          return
        }

        let attrs = vNode.data.attrs || {}
        attrs = Object.assign({}, theme, attrs)
        vNode.data.attrs = attrs
      }
    }

    this.$slots.default.map(vNode => merge(vNode))

    Object.keys(this.$attrs).forEach(key => {
      this.$attrs[key] = null
    })

    return this.$slots.default[0] // 直接返回,无需额外渲染
  }
}

如此便达到了这样的使用效果:

<theme :pos="[0, 0]" :size="[500, 400]" :radius="[0, 0.5]" :borderWidth="2" :borderColor="'#fff'" >
  <some-component title="我是标题":data="testData" />
</thmem>

很显然 theme 组件是没有渲染的,它所做的也只不过是透传 props 给其它组件而已,称之为 无渲染组件

slot-scope

Vue文档 中提到了 slot-scope 可以使用作用域插槽变得更干净。那么结合 theme 组件的经验,可以写出这样一款 axios 组件。

Vue.component('s-axios', {
  props: ['url'],

  data() {
    return {
      loading: true,
      response: null
    }
  },

  created() {
    axios.get(this.url)
      .then(response => {
        this.loading = false
        this.response = response
      })
  },

  render() {
    return this.$scopedSlots.default({
      loading: this.loading,
      response: this.response
    })
  }
})

使用起来也很方便:

<div id="app">
  <s-axios url="https://api.github.com/orgs/reactjs/repos">
    <div slot-scope="{ loading, response }">
      <div v-if="loading">loading</div>
      <div v-else>响应数据为:${{ response.data }}</div>
    </div>
  </s-axios>
</div>

可以点击查看在线Demo

总结

通过 $slots$scopedSlots结合 render可以创造很多好玩的组件,比如本篇文章中说到的 无渲染组件 ,关键就在于使用者怎么想。

异步过程中 loading 指示器的优化

在网络请求、耗时操作等异步场景下,一般都会给用户一个指示(如 loading 加载中指示器)。但是有时候异步操作可能并没有想象的那么耗时,比如一个 ajax 请求可能会在 200ms 内完成,也可能会超过 200ms 才能完成,如果不管三七二十一直接上 loading,反而对用户体验不好。

总结一下:

  1. 在异步操作的开始和结束过程中,展示指示器
  2. 异步操作耗时很短的话,不展示指示器

针对 React ,可以封装 useAsyncLoading hook,来完成这种操作。

useAsyncLoading 使用方式

const [wrappedPromiseAction, loading] = useAsyncLoading(promiseAction, 200)

解释:针对 promiseAction 如果 200ms 内完成 loadingfalse ,反之为 true

useAsyncLoading 代码实现

import React, { useState, useCallback, useEffect, useRef } from "react";

type PromiseAction = (...args: any[]) => Promise<any>;

function useAsyncLoading<A extends PromiseAction>(
  action: A,
  wait: number
): [A, boolean] {
  const timerRef = useRef(null);
  const [pending, setPending] = useState(false);
  const [loading, setLoading] = useState(false);

  const actionWithPending = useCallback(
    (...args: Parameters<A>) => {
      setPending(true);
      const promise = action(...args);
      promise.then(
        () => setPending(false),
        () => setPending(false)
      );
      return promise;
    },
    [action]
  );

  useEffect(() => {
    clearTimeout(timerRef.current);

    timerRef.current = setTimeout(() => {
      setLoading(pending);
    }, wait);

    return () => {
      clearTimeout(timerRef.current);
    };
  }, [wait, pending]);

  return [actionWithPending as A, loading];
}

useAsyncLoading demo 示例

import React, { useState, useCallback } from "react";

const mockApi = (): Promise<string> => {
  const time = Math.random() * 400;
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("此次请求耗时: " + time + "ms");
    }, time);
  });
};

export default function App() {
  const [mock, loading] = useAsyncLoading(mockApi, 200);
  const [data, setData] = useState("");

  const getData = useCallback(() => {
    mock().then((res) => {
      setData(res);
    });
  }, [mock]);

  return (
    <div className="App">
      <button onClick={getData}>发起请求</button>
      {loading && <h1>loading</h1>}
      {!loading && <p>{data}</p>}
    </div>
  );
}

codesanbox 链接

数据可视化简介

数据可视化

数据可视化的主要任务是将数据转换为易于感知的图形。

一、可视化流程

很多人认为数据可视化无非就是数据几组数据,生成各自图表(或图形)等等。其实数据可视化大致可分为:

  1. 信息可视化
  2. 科学可视化
  3. 可视化分析

之前所提到的简单图表只是信息可视化中最常见的几种。面对不同的数据体积以及不同的可视化目标,可视化系统的复杂度很可能就会超出想象。

通用的可视化流程

可视化整体可分为三步:分析 -》 处理 -》 生成。

1. 分析

分析分为三部分:任务、数据和领域。

首先,要分析该次可视化的出发点和目标是什么。遇到什么问题、展示什么信息、要得出什么结论、验证什么假说等等。数据承载的信息是多种多样的,不同的展示方式的侧重点也是不一样的(说白了,想清楚要干什么,才能确定要过滤什么数据、怎样处理数据最后怎样展示数据)。

其次,分析数据(见数据模型)。

最后要针对不同的领域,进行响应的分析。可视化的侧重点要跟随领域做出相应变化。

2. 处理

处理可分为两部分:对数据的处理、对视觉编码的处理。

在可视化之前,要对数据进行数据清洗、数据规范、数据分析等数据处理。
所谓视觉编码即指如何使用位置、尺寸、灰度值、纹理、色彩、方向、形状等视觉通道,以映射要展示的数据维度。

3. 生成

将之前的分析和设计实现。

二、数据模型

数据说白了就是可定性或可量化的一组数据。为了更准确更形象地表达数据,先了解一些数据相关的概念。

1. 数据模型与概念模型

数据为什么可以代表世界?带着这个问题,来了解数据和概念两个模型。

数据模型 是一组数字或符号的组合,其包含着数据的定义、类型等,可以进行各类数学操作。
概念模型 描述的是事物的语义或状态行为等。

现实世界 =》 概念模型 =》 数据模型

现实世界可以用概念模型描述,而概念模型又可以用数据模型来描述。经过两层抽象,数据便可以描述现实世界。

2. 数据类型

一个东西属于哪一类,取决于用什么标准划分,数据亦然。

按数据在计算机中的存储,数据可分为浮点数、整数、字符等;从关系模型的角度来说,数据可以分为实体和关系两类;从数据结构来说,数据可以分为一维、二维、三维、多维、时间序列、空间序列、树型、图型等等。接下来说一说和数据可视化有关的分类方法。

按照测量标度来分,数据一般分为四类:类别型、有序型、区间型和比值型。

  • 类别型:用于区分事物。如:人可分为男女。
  • 有序型:用于表示对象间的顺序关系。如:人的身高可以从矮到高。
  • 区间型:用于对象间的定量比较。如:身高 160cm 和身高 158cm。
  • 比值型:用于数值间的比例关系。如:6 是 3 的 2 倍。

在数据可视化中,通常不区分区间型和比值型,通一称为 数值型

3. 举个例子

id 类型 款式 尺码 销量 年增长
1 男款 上衣 L 50 10%
2 女款 上衣 S 35 5%
3 女款 裤子 M 40 20%
4 男款 上衣 XL 30 15%

如表所示,不难看出:

  • id尺码 属于 有序型 数据。
  • 类型款式 数据 类别型 数据。
  • 销量年增长 属于 数值型 数据。

三、视觉编码

1. 什么是视觉编码?

视觉编码描述的是将数据映射到最终可视化结果上的过程。

编码二字,编可以说是指设计、映射的过程,码是指一些图形符号。图形符号和信息间的映射关系可以使人迅速获取信息。可以说图形符号中携带了信息(称之为编码了一些信息)。而人从这些符号中读取信息时,可以称作时解码了一些信息。
人解码信息靠的是眼睛,人的视觉系统。如果说图形符号是编码信息的工具或通道,那么人的视觉系统便是解码信息的通道。通常把这种 图形符号 《--》 信息 《--》 视觉系统 的对应过程称为 视觉通道

2. 常用的视觉通道

1967 年,Jacques Bertin 初版的《Semiology of Graphics》一书提出了图形符号与信息的对应关系,奠定了可视化编码的理论基础。该书中把图形符号分为两种:

  • 位置变量:一般指二维坐标
  • 视网膜变量:尺寸、数值、纹理、颜色、方向和形状

后来又补充了 长度面积体积透明度模糊/聚焦动画 等视觉通道。

3. 视觉编码设计原则

首先说一下视觉通道的性质:

  • 定性(又称分类)性质 :适用于类别型数据。如颜色或形状。
  • 定量(或定序)性质:适用于有序型和类别型数据。如长度、大小适合于编码数值/量的大小。
  • 分组性质:具有相同视觉通道的数据可以分为一组,便于识别。

最后说一下视觉编码设计的两大原则:

  • 表达性、一致性:可视化的结果应充分表达了数据要表达的信息,且无多余。
  • 有效性、理解性:可视化之后应当比其他数据表达方案更加有效,更加容易让人理解。

数据可视化编码除了视觉通道还需考虑:

  • 色彩搭配
  • 交互
  • 美学因素
  • 信息密度
  • 直观映射、隐喻

等等。

参考文献

使用js实现依次执行异步任务

最近接到了奇舞团的电话面试,总共进行了两轮电话面试,其中有几个问题印象比较深刻,其中一个便是:“如何实现依次执行异步任务”(最近脑子不太好使了,愣是想不起来两轮面试问了哪些问题,故就不记录此次奇舞团面试笔记)。

问题描述

现有 n 个异步任务,这 n 个异步任务是依次执行且下一个异步任务依赖上一个异步任务的结果作参数,问如何实现。

解法1:for 循环 + await

简单的 for 循环是依次进行循环的,利用这一特点加 async / await 很容易写出下面这样的代码:

(async () => {
  const sleep = delay => {
    return new Promise((resolve, reject) => {
      setTimeout(_ => resolve(), delay)
    })
  }
  
  const task = (i) => {
    return new Promise(async (resolve, reject) => {
      await sleep(500)
      console.log(`now is ${i}`)
      ++i
      resolve(i)
    })
  }
  
  let param = 0
  for (let i = 0; i < 4; i++) {
    param = await task(param)
  }  
})()

输出:

now is 0
now is 1
now is 2
now is 3

效果虽然做到了,但是看到 param 这个局部变量就很不爽,请看解法2。

解法2:Array.prototype.reduce

关于 Array.prototype.reduce 方法相信大部分小伙伴初见时都是用来数组求和。不了解的小伙伴可以点击链接了解下 reduce
reduce初始值积累值,以及 当前值 的概念。其中 “积累值”可以看作是 前一个值,通过 返回 积累值 又可以看作是 下一个值(可能说的比较绕,可以参照 Redux 的 中间件执行顺序 的源码,也是用的 reduce)。使用 reduce 来解决问题的代码为:

const sleep = delay => {
  return new Promise((resolve, reject) => {
    setTimeout(_ => resolve(), delay)
  })
}

const task = (i) => {
  return new Promise(async (resolve, reject) => {
    await sleep(500)
    console.log(`now is ${i}`)
    ++i
    resolve(i)
  })
}

[task, task, task, task].reduce(async (prev, task) => {
  const res = await prev
  return task(res)
}, 0)

输出:

now is 0
now is 1
now is 2
now is 3

可以这样理解 prevtask

  • prev:前一个 异步任务(promise)
  • task:当前的异步任务

当前的异步任务需要上一个异步任务的结果作参数,故很显然要 await prev

总结

要学好ES6,要不断尝试写出优雅的代码。

Vue.js 单元测试

前言

使用vue-cli可以直接生成一个包含unit & e2e测试的开发环境。这里我们主要针对unit文件进行单元测试

命令行效果预览

命令行

test/unit 文件结构及分析

├── coverage
├── jest.conf.js
├── setup.js
└── specs
    ├── api-test.spec.js
    ├── click-test.spec.js
    ├── data-test.spec.js
    ├── dom-test.spec.js
    ├── input-test.spec.js
    ├── mock-test.spec.js
    └── props-test.spec.js
  • coverage: 单元测试覆盖率的报表生成文件(当执行npm run unit后,可在该文件夹下打开index.html文件查看覆盖率)。
  • jest.conf.js: jest的配置文件。Jest是Facebook开发的一个对javascript进行单元测试的工具,之前仅在其内部使用,后开源,并且是在Jasmine测试框架上演变开发而来,使用了我们熟知的expect(value).toBe(other)这种断言格式。
  • setup.js: 默认生成的vue配置文件(不用更改,默认开发环境)。
  • specs: 编写单元测试的文件夹(所有的测试文件都将放在该文件夹下)。

data方法测试

假设我们编写了这样一个组件:

<!-- data-test.vue -->
<template>
  <span>{{ msg }}</span>
</template>

<script>
export default {
  name: 'TestData',

  data() {
    return {
      msg: 'hello'
    }
  },
  
  created() {
    this.msg = 'bye'
  }
}
</script>

那么,我们可以这样编写断言

// data.spec.js
// 导入vue.js和组件,进行测试
import Vue from 'vue'
import TestDataComponent from '@/components/data-test.vue'

describe('TestDataComponent', () => {
  // 检查原始组件选项
  it('has a created hook', () => {
    expect(typeof TestDataComponent.created).toEqual('function')
  })

  // 评估原始组件选项中的函数的结果
  it('should set the correct default data', () => {
    expect(typeof TestDataComponent.data).toEqual('function')

    const data = TestDataComponent.data()
    expect(data.msg).toEqual('hello')
  })

  // 检查mount中的组件实例
  it('should correctly set the msg when created', () => {
    const vm = new Vue(TestDataComponent).$mount()
    expect(vm.msg).toEqual('bye')
  })

  // 创建一个实例并检查渲染输出
  it('should render correct msg', () => {
    const Ctor = Vue.extend(TestDataComponent)
    const vm = new Ctor().$mount()
    expect(vm.$el.textContent).toEqual('bye')
  })
})

props测试

很多组件的渲染输出由它的 props 决定。事实上,如果一个组件的渲染输出完全取决于它的 props,那么它会让测试变得简单,就好像断言不同参数的纯函数的返回值。例子:

<template>
  <!-- props-test.vue  -->
  <p>{{ msg }}</p>
</template>

<script>
export default {
  props: ['msg']
}
</script>

那么我们就可以在不同的 props 中,通过 propsData 选项断言它的渲染输出:

// props.spec.js
import Vue from 'vue'
import TestPropsComponent from '@/components/props-test.vue'

const Ctor = Vue.extend(TestPropsComponent);

// 针对props,可以通过propsData选项断言它的渲染输出
const getRenderText = (propsData) => {
  const vm = new Ctor({propsData}).$mount()
  return vm.$el.textContent
}

describe('TestPropsComponent', () => {
  it('should render correctly with different props', () => {
    expect(getRenderText({msg: 'Hello'})).toEqual('Hello')
    expect(getRenderText({msg: 'Bye'})).toEqual('Bye')
  })
})

断言异步更新

由于 Vue 进行 异步更新 DOM 的情况,一些依赖 DOM 更新结果的断言必须在 Vue.nextTick 回调中进行:

<template>
  <!-- dom-test.vue -->
  <span>{{ msg }}</span>
</template>

<script>
export default {
  data() {
    return {
      msg: 'Hello'
    }
  }
}
</script>

断言的编写:

// dom.spec.js
import Vue from 'vue'
import DomUpdateComponent from '@/components/dom-test.vue'

describe('DomUpdateComponent', () => {
  it('the msg should be Hello', () => {
    const data = DomUpdateComponent.data()
    expect(data.msg).toEqual('Hello')
  })

  // 在状态更新后检查生成的HTML
  it('the msg should change to Bye', () =>  {
    const vm = new Vue(DomUpdateComponent).$mount()

    vm.msg = 'Bye'

    // 在状态改变后和断言 DOM 更新前等待一刻
    Vue.nextTick(() => {
      expect(vm.$el.textContent).toEqual('Bye')
    })
  })
})

组件事件的测试

假设我们编写了这样一个组件,其中有一个按钮,按钮有一个点击事件

<!-- click-test.vue -->
<template>
  <div>
    <p>现在是{{ num }}</p>
    <button @click="increase">change</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      num: 0
    }
  },

  methods: {
    increase() {
      this.num++
    }
  }
}
</script>

那么,我们可以这样在断言中触发事件:

// click.spec.js
import Vue from 'vue'
import ClickTestComponent from '@/components/click-test.vue'

describe('ClickTestComponent', () => {
  const Ctor = Vue.extend(ClickTestComponent)

  const vm = new Ctor().$mount()
  // 获取按钮
  const oBtn = vm.$el.querySelector('button')
  // 创建点击事件
  const clickEvent = new window.Event('click')

  it("the num should be 1", () => {
    // 触发按钮点击事件
    oBtn.dispatchEvent(clickEvent)
    // 需要手动监听更新
    vm._watcher.run()

    expect(vm.$el.textContent).toContain('1')
  })

  it("the num should be 2", () => {
    // 触发按钮点击事件
    oBtn.dispatchEvent(clickEvent)
    // 需要手动监听更新
    vm._watcher.run()
    
    expect(vm.$el.textContent).toContain('2')
  })
})

用户交互组件的测试

假设我们有一个输入框,并且绑定了一些事件:

<template>
  <!-- input-test.vue -->
  <div>
    <p>{{ msg }}</p>
    <input type="text" v-model="input">
    <button @click="handle"></button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      msg: '',
      input: 'hi'
    }
  },

  methods: {
    handle() {
      this.msg = this.input
    }
  }
}
</script>

很显然,我们是要利用v-model模拟input进行断言:

// input.spec.js
import Vue from 'vue'
import InputTestComponent from '@/components/input-test.vue'

describe('InputTestComponent', () => {
  const Ctor = Vue.extend(InputTestComponent)
  const vm = new Ctor().$mount()

  const oBtn = vm.$el.querySelector('button')
  const clickEvent = new window.Event('click')

  it('the msg should be equal with input', () => {
    vm.input = 'let me change msg to what I say'

    oBtn.dispatchEvent(clickEvent)

    // 手动监听更新
    vm._watcher.run()

    expect(vm.$el.textContent).toContain('let me change msg to what I say')
  })
});

API的测试

  1. 利用mock.js模拟后端接口
    src文件夹下新建mock文件夹,编写index.js:
// mock/index.js
import Mock from 'mockjs'

export default Mock.mock('http://api.rs.com/v1/test', {
  'greeting': 'Hi, Vue'
});
  1. 利用axios进行ajax
    src文件夹下新建provider文件夹,编写index.js:
// provider/index.js
import axios from 'axios'
import '../mock/index'

const getGreeting = async (url = 'http://api.rs.com/v1/test') => {
  try {
    const res = await axios.get(url)

    return res.data
  } catch (err) {
    throw new Error('获取问候语失败')
  }
}

export default getGreeting
  1. 编写组件
<template>
  <!-- mock-test.vue -->
  <p>{{ msg }}</p>
</template>

<script>
import getGreeting from '@/provider/index'

export default {
  data() {
    return {
      msg: 'waiting'
    }
  },

  created() {
    this.getMsg()
  },

  methods: {
    async getMsg(url) {
      try {
        const data = await getGreeting(url)

        this.msg = data.greeting
      } catch (err) {
        console.log(err.message)
      }
    }
  }
}
</script>
  1. 针对provider/index.js进行单元测试
// api.spec.js
import getGreeting from '@/provider/index'

describe('test greeting', async () => {
  it('res should be "Hi, Vue"', async () => {
    const res = await getGreeting();
    expect(res.greeting).toEqual('Hi, Vue')
  })

  it('err should be catched', async () => {
    try {
      const res = await getGreeting('wrongUrl')
    } catch (err) {
      expect(err.message).toEqual('获取问候语失败')
    }
  })
})
  1. 针对编写的组件进行测试
// mock-test.spec.js
import Vue from 'vue'
import MockTestComponet from '@/components/mock-test.vue'

describe('MockTestComponet', () => {
  it('should have the created hook', () => {
    expect(typeof MockTestComponet.created).toEqual('function')
  })

  it('the msg should be waiting', () => {
    const data = MockTestComponet.data()

    expect(data.msg).toEqual('waiting')
  })

  it('the msg should change when created', async () => {
    // const vm = new Vue(MockTestComponet).$mount()
    const Ctor = Vue.extend(MockTestComponet)
    const vm = new Ctor().$mount()

    setTimeout(() => {
      expect(vm.msg).toEqual('Hi, Vue')
    }, 0)
  })

  // 返回正确的结果
  it('test methods getMsg by correct url', async () => {
    const vm = new Vue(MockTestComponet)
    await vm.getMsg();

    expect(vm.msg).toEqual('Hi, Vue')
  })

  // 抛出异常
  it('test methods getMsg by wrong url', async () => {
    try {
      const vm = new Vue(MockTestComponet)
      await vm.getMsg('fuck');
    } catch (err) {
      expect(err.message).toEqual('获取问候语失败')
    }
  })
})

使用 Typescript 开发 React 组件

React Component in Typescript

最近学习了 react-redux-typescript-guide,在此记录一下。

安装类型定义

npm i -D @types/react @types/react-dom

React 中的类型

函数式组件

React.FC<props> | React.FunctionComponent<Props>

例如:

const MyComponent: React.FC<Props> = ...

类组件

React.Component<Props, State>

例如:

class MyComponent extends React.Component<Props, State> { ... }

高阶组件

React.ComponentType<Props>

例如:

const withState = <P extends WrappedComponentProps>(
  WrappedComponent: React.ComponentType<P>,
) => { ... }

组件 props

React.ComponentProps<typeof XXX>

例如:

type MyComponentProps = React.ComponentProps<typeof MyComponent>;

元素

React.ReactElement | JSX.Element;

例如:

const elementOnly: React.ReactElement = <div /> || <MyComponent />;

节点

React.ReactNode;

例如:

const elementOrPrimitive: React.ReactNode = 'string' || 0 || false || null || undefined || <div /> || <MyComponent />
const Component = ({ children: React.ReactNode }) => { ... }

样式对象

React.CSSProperties;

例如:

const style: React.CSSProperties = { color: "red" };
const element: React.ReactElement = <div style={style} />;

HTML 属性

React.HTMLProps;

例如:

const Input: React.FC<Props & React.HTMLProps<HTMLInputElement>> = props => { ... }

<Input accept={...} alt={...} />

事件处理句柄

React.ReactEventHandler<HTMLXXXElement>

例如:

const handleChange: React.ReactEventHandler<HTMLInputElement> = (evt) => { ... }
<input onChange={handleChange} />

事件对象

React.XXXEvent<HTMLXXXElement>

例如:

const handleChange = (ev: React.MouseEvent<HTMLDivElement>) => { ... }
<div onMouseMove={handleChange} />

组件实战

函数式组件

import * as React from "react";

type Props = {
  label: string,
  count: number,
  onIncrement: () => void
};

export const FCCounter: React.FC<Props> = props => {
  const { label, count, onIncrement } = props;

  return (
    <div>
      <span>
        {label}: {count}
      </span>
      <button onClick={onIncrement}>Increment</button>
    </div>
  );
};

使用:

<FCCounter
  label={"FCCounter"}
  count={this.state.count}
  onIncrement={() => this.setState({ count: this.state.count + 1 })}
/>

类组件

import * as React from "react";

type Props = {
  label: string;
  initialCount: number;
};

type State = {
  count: number;
};

export class StatefulCounter extends React.Component<Props, State> {
  static defaultProps = {
    initialCount: 0
  };

  readonly state: State = {
    count: this.props.initialCount
  };

  componentWillReceiveProps({ initialCount }: Props) {
    this.setState({ count: initialCount });
  }

  onIncrement = () => {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    const { label } = this.props;
    const { count } = this.state;

    return (
      <div>
        <span>
          {label}: {count}
        </span>
        <button onClick={this.onIncrement}>Increment</button>
      </div>
    );
  }
}

使用:

<StatefulCounter label={"StatefulCounter"} initialCount={10} />

泛型组件

import * as React from "react";

export interface IGenericListProps<T> {
  items: T[];
  itemRenderer: (item: T) => React.ReactElement;
}

export class GenericList<T> extends React.Component<IGenericListProps<T>, {}> {
  render() {
    const { items, itemRenderer } = this.props;

    return <div>{items.map(itemRenderer)}</div>;
  }
}

使用:

interface IUser {
  name: string;
}
const users: IUser[] = [{ name: "Mike" }, { name: "Tom" }, { name: "David" }];
class UserList extends GenericList<IUser> {}

<UserList
  items={users}
  itemRenderer={({ name }) => <p key={name}>{name}</p>}
/>;

渲染回调组件

import * as React from "react";

export interface IMouseProviderProps {
  render: (state: IMouseProviderState) => React.ReactNode;
}

interface IMouseProviderState {
  readonly x: number;
  readonly y: number;
}

export class MouseProvider extends React.Component<
  IMouseProviderProps,
  IMouseProviderState
> {
  readonly state: IMouseProviderState = {
    x: 0,
    y: 0
  };

  handleMousemove = (evt: React.MouseEvent<HTMLDivElement>) => {
    this.setState({
      x: evt.clientX,
      y: evt.clientY
    });
  };

  render() {
    return (
      <div style={{ height: "100%" }} onMouseMove={this.handleMousemove}>
        {this.props.render(this.state)}
      </div>
    );
  }
}

使用:

<div style={{ height: 80, border: "1px solid #000" }}>
  <MouseProvider render={({ x, y }) => `x: ${x}, y: ${y}`} />
</div>

高阶组件

import * as React from "react";
import { Subtract } from "utility-types";

interface IInjectedProps {
  count: number;
  onIncrement: () => void;
}

export const withState = <BaseProps extends IInjectedProps>(
  _BaseComponent: React.ComponentType<BaseProps>
) => {
  const BaseComponent = (_BaseComponent as unknown) as React.ComponentType<
    IInjectedProps
  >;

  type HOCProps = Subtract<BaseProps, IInjectedProps> & {
    initialCount?: number;
  };
  type HOCState = { readonly count: number };

  return class HOC extends React.Component<HOCProps, HOCState> {
    readonly state: HOCState = {
      count: Number(this.props.initialCount) || 0
    };

    onIncrement = () => {
      this.setState({ count: this.state.count + 1 });
    };

    render() {
      return (
        <BaseComponent
          count={this.state.count}
          onIncrement={this.onIncrement}
          {...this.props}
        />
      );
    }
  };
};

使用:

const FCCounterWithState = withState(FCCounter);
<FCCounterWithState initialCount={20} label={"FCCounter withState"} />;

Vue 组件开发姿势总结

前言

本文接下来会一步一步模仿造一个低配版的 Element 的对话框和弹框组件

正文

Vue 单文件组件开发

当使用vue-cli初始化一个项目的时候,会发现 src/components 文件夹下有一个 HelloWorld.vue 文件,这便是单文件组件的基本开发模式。

// 注册
Vue.component("my-component", {
  template: "<div>A custom component!</div>"
});

// 创建根实例
new Vue({
  el: "#example"
});

接下来,开始写一个 dialog 组件。

Dialog

目标对话框组件的基本样式如图:

dialog基本样式

根据目标样式,可以总结出:

  1. dialog 组件需要一个 title props 来标示弹窗标题
  2. dialog 组件需要在按下 确定 按钮时 发射确定 事件(即告诉父组件确定了)
  3. 同理,dialog 组件需要 发射取消 事件
  4. dialog 组件需要提供一个插槽,便于自定义内容

那么,编码如下:

<template>
  <div class="ta-dialog__wrapper">
    <div class="ta-dialog">
      <div class="ta-dialog__header">
        <span>{{ title }}</span>
        <i class="ios-close-empty" @click="handleCancel()"></i>
      </div>
      <div class="ta-dialog__body">
        <slot></slot>
      </div>
      <div class="ta-dialog__footer">
        <button @click="handleCancel()">取消</button>
        <button @click="handleOk()">确定</button>
      </div>
    </div>
  </div>
</template>

<script>
  export default {
    name: "Dialog",

    props: {
      title: {
        type: String,
        default: "标题"
      }
    },

    methods: {
      handleCancel() {
        this.$emit("cancel");
      },

      handleOk() {
        this.$emit("ok");
      }
    }
  };
</script>

这样便完成了dialog组件的开发,使用方法如下:

<ta-dialog title="弹窗标题" @ok="handleOk" @cancel="handleCancel">
  <p>我是内容</p>
</ta-dialog>

这时候发现一个问题,通过使用 v-if 或者 v-show 来控制弹窗的展现时,没有动画!!!,看上去很生硬。教练,我想加动画 ,这时候就该 transition 组件上场了。使用 transition 组件结合 css能做出很多效果不错的动画。接下来增强 dialog 组件动画,代码如下:

<template>
  <transition name="slide-down">
    <div class="ta-dialog__wrapper" v-if="isShow">
      <!-- 省略 -->
    </div>
  </transition>
</template>

<script>
  export default {
    data() {
      return {
        isShow: true
      };
    },

    methods: {
      handleCancel() {
        this.isShow = false;
        this.$emit("cancel");
      },

      handleOk() {
        this.isShow = true;
        this.$emit("ok");
      }
    }
  };
</script>

可以看到 transition 组件接收了一个 name props,那么怎么编写 css 完成动画呢?很简单的方式,写出两个
关键 class (css 的 className)样式即可:

.slide-down-enter-active {
  animation: dialog-enter ease 0.3s;
}

.slide-down-leave-active {
  animation: dialog-leave ease 0.5s;
}

@keyframes dialog-enter {
  from {
    opacity: 0;
    transform: translateY(-20px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes dialog-leave {
  from {
    opacity: 1;
    transform: translateY(0);
  }

  to {
    opacity: 0;
    transform: translateY(-20px);
  }
}

就是这么简单就开发出了效果还不错的动效,注意 transition 组件的 nameslide-down ,而编写的动画的关键 classNameslide-down-enter-activeslide-down-leave-active

封装DialogMessageBox

Element 的MessageBox的使用方法如下:

this.$confirm("此操作将永久删除该文件, 是否继续?", "提示", {
  confirmButtonText: "确定",
  cancelButtonText: "取消",
  type: "warning"
})
  .then(() => {
    this.$message({
      type: "success",
      message: "删除成功!"
    });
  })
  .catch(() => {
    this.$message({
      type: "info",
      message: "已取消删除"
    });
  });

看到这段代码,我的感觉就是好神奇好神奇好神奇(惊叹三连)。仔细看看,这个组件其实就是一个封装好的 dialog ,

Element MessageBox效果

接下来,我也要封装一个这样的组件。首先,整理下思路:

  1. Element 的使用方法是 this.$confirm ,这不就是挂到 Vueprototype 上就行了
  2. Element 的 then 是确定,catch 是取消,promise 就可以啦

整理好思路,我就开始编码了:

import Vue from "vue";
import MessgaeBox from "./src/index";

const Ctur = Vue.extend(MessgaeBox);
let instance = null;

const callback = action => {
  if (action === "confirm") {
    if (instance.showInput) {
      instance.resolve({ value: instance.inputValue, action });
    } else {
      instance.resolve(action);
    }
  } else {
    instance.reject(action);
  }

  instance = null;
};

const showMessageBox = (tip, title, opts) =>
  new Promise((resolve, reject) => {
    const propsData = { tip, title, ...opts };

    instance = new Ctur({ propsData }).$mount();
    instance.reject = reject;
    instance.resolve = resolve;
    instance.callback = callback;

    document.body.appendChild(instance.$el);
  });

const confirm = (tip, title, opts) => showMessageBox(tip, title, opts);

Vue.prototype.$confirm = confirm;

至此,可能会疑惑怎么 callback 呢,其实我编写了一个封装好的 dialog 并将其命名为 MessageBox ,
它的代码中,有这样两个方法:

onCancel() {
  this.visible = false
  this.callback && (this.callback.call(this, 'cancel'))
},

onConfirm() {
  this.visible = false
  this.callback && (this.callback.call(this, 'confirm'))
},

没错,就是确定取消时进行callback。我还想说一说Vue.extend,代码中引入了MessageBox,
我不是直接new MessageBox而是借助new Ctur,因为这样可以定义数据(不仅仅是props),例如:

instance = new Ctur({ propsData }).$mount();

这时候,页面上其实是还没有MessageBox的,我们需要执行:

document.body.appendChild(instance.$el);

如果你直接这样,你可能会发现 MessageBox 打开的时候没有动画,而关闭的时候有动画。解决方法也很简单,
appendChild的时候让其仍是不可见,然后使用类这样的代码:

Vue.nextTick(() => (instance.visible = true));

这样就有动画了。

总结

  1. 通过 transitioncss 实现不错的动画。其中,transition 组件的 name 决定了编写 css 的两个关键
    类名为 [name]-enter-active[name]-leave-active
  2. 通过 Vue.extend 继承一个组件的构造函数(不知道怎么说合适,就先这样说),然后通过这个构造函数,便可以
    实现组件相关属性的自定义(使用场景:js 调用组件)
  3. js 调用组件时,为了维持组件的动画效果可以先 document.body.appendChild 然后 Vue.nextTick(() => instance.visible = true)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.