roger-hiro / blogfn Goto Github PK
View Code? Open in Web Editor NEW前端劝退指南,公众号[前端劝退师]的作者,微信:huab119,欢迎来内推
前端劝退指南,公众号[前端劝退师]的作者,微信:huab119,欢迎来内推
其实Vue
官方从2.6.X
版本开始就部分使用Ts
重写了。
我个人对更严格类型限制没有积极的看法,毕竟各类转类型的*写法写习惯了。
然鹅最近的一个项目中,是TypeScript
+ Vue
,毛计喇,学之...…真香!
注意此篇标题的“前”,本文旨在讲Ts
混入框架的使用,不讲Class API
npm install -g @vue/cli
# OR
yarn global add @vue/cli
新的Vue CLI
工具允许开发者 使用 TypeScript
集成环境 创建新项目。
只需运行vue create my-app
。
然后,命令行会要求选择预设。使用箭头键选择Manually select features
。
接下来,只需确保选择了TypeScript
和Babel
选项,如下图:
完成此操作后,它会询问你是否要使用class-style component syntax
。
然后配置其余设置,使其看起来如下图所示。
Vue CLI工具现在将安装所有依赖项并设置项目。
总之,先跑起来再说。
通过tree
指令查看目录结构后可发现其结构和正常构建的大有不同。
这里主要关注shims-tsx.d.ts
和 shims-vue.d.ts
两个文件
两句话概括:
shims-tsx.d.ts
,允许你以.tsx
结尾的文件,在Vue
项目中编写jsx
代码shims-vue.d.ts
主要用于 TypeScript
识别.vue
文件,Ts
默认并不支持导入 vue
文件,这个文件告诉 ts
导入.vue
文件都按VueConstructor<Vue>
处理。此时我们打开亲切的src/components/HelloWorld.vue
,将会发现写法已大有不同
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<!-- 省略 -->
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
@Component
export default class HelloWorld extends Vue {
@Prop() private msg!: string;
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped></style>
至此,准备开启新的篇章 TypeScript
极速入门 和 vue-property-decorator
TypeScript
极速入门Typescript
与Javascript
共享相同的基本类型,但有一些额外的类型。
Tuple
enum
Any
与Void
// 数字,二、八、十六进制都支持
let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
// 字符串,单双引都行
let name: string = "bob";
let sentence: string = `Hello, my name is ${ name }.
// 数组,第二种方式是使用数组泛型,Array<元素类型>:
let list: number[] = [1, 2, 3];
let list: Array<number> = [1, 2, 3];
let u: undefined = undefined;
let n: null = null;
#####1. 元组 Tuple
想象 元组 作为有组织的数组,你需要以正确的顺序预定义数据类型。
const messyArray = [' something', 2, true, undefined, null];
const tuple: [number, string, string] = [24, "Indrek" , "Lasn"]
如果不遵循 为元组 预设排序的索引规则,那么Typescript
会警告。
(tuple
第一项应为number
类型)
enum
*enum
类型是对JavaScript标准数据类型的一个补充。 像C#等其它语言一样,使用枚举类型可以为一组数值赋予友好的名字。
// 默认情况从0开始为元素编号,也可手动为1开始
enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green;
let colorName: string = Color[2];
console.log(colorName); // 输出'Green'因为上面代码里它的值是2
另一个很好的例子是使用枚举来存储应用程序状态。
Void
在Typescript
中,你必须在函数中定义返回类型。像这样:
若没有返回值,则会报错:
我们可以将其返回值定义为void
:
此时将无法 return
Any
Emmm...就是什么类型都行,当你无法确认在处理什么类型时可以用这个。
但要慎重使用,用多了就失去使用Ts的意义。
let person: any = "前端劝退师"
person = 25
person = true
主要应用场景有:
Never
用很粗浅的话来描述就是:"Never
是你永远得不到的爸爸。"
具体的行为是:
throw new Error(message)
return error("Something failed")
while (true) {} // 存在无法达到的终点
简略的定义是:可以用来手动指定一个值的类型。
有两种写法,尖括号和as
:
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
let strLength: number = (someValue as string).length;
使用例子有:
当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法:
function getLength(something: string | number): number {
return something.length;
}
// index.ts(2,22): error TS2339: Property 'length' does not exist on type 'string | number'.
// Property 'length' does not exist on type 'number'.
如果你访问长度将会报错,而有时候,我们确实需要在还不确定类型的时候就访问其中一个类型的属性或方法,此时需要断言才不会报错:
function getLength(something: string | number): number {
if ((<string>something).length) {
return (<string>something).length;
} else {
return something.toString().length;
}
}
Generics
软件工程的一个主要部分就是构建组件,构建的组件不仅需要具有明确的定义和统一的接口,同时也需要组件可复用。支持现有的数据类型和将来添加的数据类型的组件为大型软件系统的开发过程提供很好的灵活性。
在C#
和Java
中,可以使用"泛型"来创建可复用的组件,并且组件可支持多种数据类型。这样便可以让用户根据自己的数据类型来使用组件。
在TypeScript里,声明泛型方法有以下两种方式:
function gen_func1<T>(arg: T): T {
return arg;
}
// 或者
let gen_func2: <T>(arg: T) => T = function (arg) {
return arg;
}
调用方式也有两种:
gen_func1<string>('Hello world');
gen_func2('Hello world');
// 第二种调用方式可省略类型参数,因为编译器会根据传入参数来自动识别对应的类型。
Any
Ts
的特殊类型 Any
在具体使用时,可以代替任意类型,咋一看两者好像没啥区别,其实不然:
// 方法一:带有any参数的方法
function any_func(arg: any): any {
console.log(arg.length);
return arg;
}
// 方法二:Array泛型方法
function array_func<T>(arg: Array<T>): Array<T> {
console.log(arg.length);
return arg;
}
arg
参数的length
属性。因为any
可以代替任意类型,所以该方法在传入参数不是数组或者带有length
属性对象时,会抛出异常。Array
的泛型类型,肯定会有length
属性,所以不会抛出异常。泛型接口:
interface Generics_interface<T> {
(arg: T): T;
}
function func_demo<T>(arg: T): T {
return arg;
}
let func1: Generics_interface<number> = func_demo;
func1(123); // 正确类型的实际参数
func1('123'); // 错误类型的实际参数
Interface
vs Type alias
Interface
,国内翻译成接口。
Type alias
,类型别名。
以下内容来自:
都可以用来描述一个对象或函数:
interface User {
name: string
age: number
}
type User = {
name: string
age: number
};
interface SetUser {
(name: string, age: number): void;
}
type SetUser = (name: string, age: number): void;
都允许拓展(extends):
interface
和 type
都可以拓展,并且两者并不是相互独立的,也就是说interface
可以 extends type
, type
也可以 extends interface
。 虽然效果差不多,但是两者语法不同。
interface extends interface
interface Name {
name: string;
}
interface User extends Name {
age: number;
}
type extends type
type Name = {
name: string;
}
type User = Name & { age: number };
interface extends type
type Name = {
name: string;
}
interface User extends Name {
age: number;
}
type extends interface
interface Name {
name: string;
}
type User = Name & {
age: number;
}
type
可以而 interface
不行
type
可以声明基本类型别名,联合类型,元组等类型// 基本类型别名
type Name = string
// 联合类型
interface Dog {
wong();
}
interface Cat {
miao();
}
type Pet = Dog | Cat
// 具体定义数组每个位置的类型
type PetList = [Dog, Pet]
type
语句中还可以使用 typeof
获取实例的 类型进行赋值// 当你想获取一个变量的类型时,使用 typeof
let div = document.createElement('div');
type B = typeof div
type StringOrNumber = string | number;
type Text = string | { text: string };
type NameLookup = Dictionary<string, Person>;
type Callback<T> = (data: T) => void;
type Pair<T> = [T, T];
type Coordinates = Pair<number>;
type Tree<T> = T | { left: Tree<T>, right: Tree<T> };
interface
可以而 type
不行
interface
能够声明合并
interface User {
name: string
age: number
}
interface User {
sex: string
}
/*
User 接口为 {
name: string
age: number
sex: string
}
*/
interface
有可选属性和只读属性
可选属性
接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在。 例如给函数传入的参数对象中只有部分属性赋值了。带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个?
符号。如下所示
interface Person {
name: string;
age?: number;
gender?: number;
}
只读属性
顾名思义就是这个属性是不可写的,对象属性只能在对象刚刚创建的时候修改其值。 你可以在属性名前用 readonly
来指定只读属性,如下所示:
interface User {
readonly loginName: string;
password: string;
}
上面的例子说明,当完成User对象的初始化后loginName就不可以修改了。
implements
vsextends
extends
很明显就是ES6里面的类继承,那么implement
又是做什么的呢?它和extends
有什么不同?
implement
,实现。与C#或Java里接口的基本作用一样,TypeScript
也能够用它来明确的强制一个类去符合某种契约
implement基本用法:
interface IDeveloper {
name: string;
age?: number;
}
// OK
class dev implements IDeveloper {
name = 'Alex';
age = 20;
}
// OK
class dev2 implements IDeveloper {
name = 'Alex';
}
// Error
class dev3 implements IDeveloper {
name = 'Alex';
age = '9';
}
而extends
是继承父类,两者其实可以混着用:
class A extends B implements C,D,E
搭配 interface
和type
的用法有:
declare
和 namespace
前面我们讲到Vue项目中的shims-tsx.d.ts
和shims-vue.d.ts
,其初始内容是这样的:
// shims-tsx.d.ts
import Vue, { VNode } from 'vue';
declare global {
namespace JSX {
// tslint:disable no-empty-interface
interface Element extends VNode {}
// tslint:disable no-empty-interface
interface ElementClass extends Vue {}
interface IntrinsicElements {
[elem: string]: any;
}
}
}
// shims-vue.d.ts
declare module '*.vue' {
import Vue from 'vue';
export default Vue;
}
declare
:当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。
这里列举出几个常用的:
declare var 声明全局变量
declare function 声明全局方法
declare class 声明全局类
declare enum 声明全局枚举类型
declare global 扩展全局变量
declare module 扩展模块
namespace
:“内部模块”现在称做“命名空间”
module X {
相当于现在推荐的写法 namespace X {
)
类似模块,同样也可以通过为其他 JS 库使用了命名空间的库创建 .d.ts
文件的声明文件,如为 D3
JS 库,可以创建这样的声明文件:
declare namespace D3{
export interface Selectors { ... }
}
declare var d3: D3.Base;
所以上述两个文件:
shims-tsx.d.ts
, 在全局变量 global
中批量命名了数个内部模块。shims-vue.d.ts
,意思是告诉 TypeScript
*.vue
后缀的文件可以交给 vue
模块来处理。private
、public
、protected
其实很好理解:
默认为public
当成员被标记为private
时,它就不能在声明它的类的外部访问,比如:
class Animal {
private name: string;
constructor(theName: string) {
this.name = theName;
}
}
let a = new Animal('Cat').name; //错误,‘name’是私有的
protected
和private
类似,但是,protected
成员在派生类中可以访问
class Animal {
protected name: string;
constructor(theName: string) {
this.name = theName;
}
}
class Rhino extends Animal {
constructor() {
super('Rhino');
}
getName() {
console.log(this.name) //此处的name就是Animal类中的name
}
}
可选参数
function buildName(firstName: string, lastName?: string) {
return firstName + ' ' + lastName
}
// 错误演示
buildName("firstName", "lastName", "lastName")
// 正确演示
buildName("firstName")
// 正确演示
buildName("firstName", "lastName")
非空断言操作符:
能确定变量值一定不为空时使用。
与可选参数 不同的是,非空断言操作符不会防止出现 null 或 undefined。
let s = e!.name; // 断言e是非空并访问name属性
Vue
组件的Ts
写法从 vue2.5 之后,vue 对 ts 有更好的支持。根据官方文档,vue 结合 typescript ,有两种书写方式:
**Vue.extend **
import Vue from 'vue'
const Component = Vue.extend({
// type inference enabled
})
import { Component, Vue, Prop } from 'vue-property-decorator'
@Component
export default class Test extends Vue {
@Prop({ type: Object })
private test: { value: string }
}
理想情况下,Vue.extend
的书写方式,是学习成本最低的。在现有写法的基础上,几乎 0 成本的迁移。
但是Vue.extend
模式,需要与mixins
结合使用。在 mixin 中定义的方法,不会被 typescript 识别到
,这就意味着会出现丢失代码提示、类型检查、编译报错等问题。
菜鸟才做选择,大佬都挑最好的。直接讲第二种吧:
vue-class-component
我们回到src/components/HelloWorld.vue
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<!-- 省略 -->
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
@Component
export default class HelloWorld extends Vue {
@Prop() private msg!: string;
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped></style>
有写过python
的同学应该会发现似曾相识:
vue-property-decorator
这个官方支持的库里,提供了函数 **装饰器(修饰符)**语法@
“@”,与其说是修饰函数倒不如说是引用、调用它修饰的函数。
或者用句大白话描述:@
: "下面的被我包围了。"
举个栗子,下面的一段代码,里面两个函数,没有被调用,也会有输出结果:
test(f){
console.log("before ...");
f()
console.log("after ...");
}
@test
func(){
console.log("func was called");
}
直接运行,输出结果:
before ...
func was called
after ...
上面代码可以看出来:
test
和func
,没有调用它们。但是,解释器读到函数修饰符“@”的时候,后面步骤会是这样:
去调用 test
函数,test
函数的入口参数就是那个叫“func
”的函数;
test
函数被执行,入口参数的(也就是func
函数)会被调用(执行);
换言之,修饰符带的那个函数的入口参数,就是下面的那个整个的函数。有点儿类似JavaScrip
t里面的
function a (function () { ... });
vue-property-decorator
和vuex-class
提供的装饰器vue-property-decorator
的装饰器:
@Prop
@PropSync
@Provide
@Model
@Watch
@Inject
@Provide
@Emit
@Component
(provided by vue-class-component)Mixins
(the helper function named mixins
provided by vue-class-component)vuex-class
的装饰器:
我们拿原始Vue组件模版来看:
import {componentA,componentB} from '@/components';
export default {
components: { componentA, componentB},
props: {
propA: { type: Number },
propB: { default: 'default value' },
propC: { type: [String, Boolean] },
}
// 组件数据
data () {
return {
message: 'Hello'
}
},
// 计算属性
computed: {
reversedMessage () {
return this.message.split('').reverse().join('')
}
// Vuex数据
step() {
return this.$store.state.count
}
},
methods: {
changeMessage () {
this.message = "Good bye"
},
getName() {
let name = this.$store.getters['person/name']
return name
}
},
// 生命周期
created () { },
mounted () { },
updated () { },
destroyed () { }
}
以上模版替换成修饰符写法则是:
import { Component, Vue, Prop } from 'vue-property-decorator';
import { State, Getter } from 'vuex-class';
import { count, name } from '@/person'
import { componentA, componentB } from '@/components';
@Component({
components:{ componentA, componentB},
})
export default class HelloWorld extends Vue{
@Prop(Number) readonly propA!: number | undefined
@Prop({ default: 'default value' }) readonly propB!: string
@Prop([String, Boolean]) readonly propC!: string | boolean | undefined
// 原data
message = 'Hello'
// 计算属性
private get reversedMessage (): string[] {
return this.message.split('').reverse().join('')
}
// Vuex 数据
@State((state: IRootState) => state . booking. currentStep) step!: number
@Getter( 'person/name') name!: name
// method
public changeMessage (): void {
this.message = 'Good bye'
},
public getName(): string {
let storeName = name
return storeName
}
// 生命周期
private created ():void { },
private mounted ():void { },
private updated ():void { },
private destroyed ():void { }
}
正如你所看到的,我们在生命周期 列表那都添加private XXXX
方法,因为这不应该公开给其他组件。
而不对method
做私有约束的原因是,可能会用到@Emit
来向父组件传递信息。
引入全局模块,需要改main.ts
:
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
Vue.config.productionTip = false;
new Vue({
router,
store,
render: (h) => h(App),
}).$mount('#app');
npm i VueI18n
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
// 新模块
import i18n from './i18n';
Vue.config.productionTip = false;
new Vue({
router,
store,
i18n, // 新模块
render: (h) => h(App),
}).$mount('#app');
但仅仅这样,还不够。你需要动src/vue-shim.d.ts
:
// 声明全局方法
declare module 'vue/types/vue' {
interface Vue {
readonly $i18n: VueI18Next;
$t: TranslationFunction;
}
}
之后使用this.$i18n()
的话就不会报错了。
Axios
的封装千人千面
如果只是想简单在Ts里体验使用Axios
,可以安装vue-axios
简单使用Axios
$ npm i axios vue-axios
main.ts
添加:
import Vue from 'vue'
import axios from 'axios'
import VueAxios from 'vue-axios'
Vue.use(VueAxios, axios)
然后在组件内使用:
Vue.axios.get(api).then((response) => {
console.log(response.data)
})
this.axios.get(api).then((response) => {
console.log(response.data)
})
this.$http.get(api).then((response) => {
console.log(response.data)
})
request.ts
文件目录:
-api
- main.ts // 实际调用
-utils
- request.ts // 接口封装
request.ts
文件解析import * as axios from 'axios';
import store from '@/store';
// 这里可根据具体使用的UI组件库进行替换
import { Toast } from 'vant';
import { AxiosResponse, AxiosRequestConfig } from 'axios';
/* baseURL 按实际项目来定义 */
const baseURL = process.env.VUE_APP_URL;
/* 创建axios实例 */
const service = axios.default.create({
baseURL,
timeout: 0, // 请求超时时间
maxContentLength: 4000,
});
service.interceptors.request.use((config: AxiosRequestConfig) => {
return config;
}, (error: any) => {
Promise.reject(error);
});
service.interceptors.response.use(
(response: AxiosResponse) => {
if (response.status !== 200) {
Toast.fail('请求错误!');
} else {
return response.data;
}
},
(error: any) => {
return Promise.reject(error);
});
export default service;
为了方便,我们还需要定义一套固定的 axios 返回的格式,新建ajax.ts
:
export interface AjaxResponse {
code: number;
data: any;
message: string;
}
main.ts
接口调用:// api/main.ts
import request from '../utils/request';
// get
export function getSomeThings(params:any) {
return request({
url: '/api/getSomethings',
});
}
// post
export function postSomeThings(params:any) {
return request({
url: '/api/postSomethings',
methods: 'post',
data: params
});
}
为了减少时间,我们来替换掉src/components/HelloWorld.vue
,做一个博客帖子组件:
<template>
<div class="blogpost">
<h2>{{ post.title }}</h2>
<p>{{ post.body }}</p>
<p class="meta">Written by {{ post.author }} on {{ date }}</p>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
// 在这里对数据进行类型约束
export interface Post {
title: string;
body: string;
author: string;
datePosted: Date;
}
@Component
export default class HelloWorld extends Vue {
@Prop() private post!: Post;
get date() {
return `${this.post.datePosted.getDate()}/${this.post.datePosted.getMonth()}/${this.post.datePosted.getFullYear()}`;
}
}
</script>
<style scoped>
h2 {
text-decoration: underline;
}
p.meta {
font-style: italic;
}
</style>
然后在Home.vue
中使用:
<template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png">
<HelloWorld v-for="blogPost in blogPosts" :post="blogPost" :key="blogPost.title" />
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import HelloWorld, { Post } from '@/components/HelloWorld.vue'; // @ is an alias to /src
@Component({
components: {
HelloWorld,
},
})
export default class Home extends Vue {
private blogPosts: Post[] = [
{
title: 'My first blogpost ever!',
body: 'Lorem ipsum dolor sit amet.',
author: 'Elke',
datePosted: new Date(2019, 1, 18),
},
{
title: 'Look I am blogging!',
body: 'Hurray for me, this is my second post!',
author: 'Elke',
datePosted: new Date(2019, 1, 19),
},
{
title: 'Another one?!',
body: 'Another one!',
author: 'Elke',
datePosted: new Date(2019, 1, 20),
},
];
}
</script>
这时候运行项目:
这就是简单的父子组件
TypeScript — JavaScript with superpowers — Part II
Typescript 中的 interface 和 type到底有什么区别
而关于Class API
撤销,其实还是挺舒服的。
用class
来编写 Vue
组件确实太奇怪了。
(所以我这篇Ts
入门压根没写Class API
)
需要转载到公众号的喊我加下白名单就行了。
在本篇文章你将会学到:
IntersectionObserver API
的用法,以及如何兼容。React Hook
中实现无限滚动。当你使用滚动作为发现数据的主要方法时,它可能使你的用户在网页上停留更长时间并提升用户参与度。随着社交媒体的流行,大量的数据被用户消费。无线滚动提供了一个高效的方法让用户浏览海量信息,而不必等待页面的预加载。
如何构建一个体验良好的无限滚动,是每个前端无论是项目或面试都会碰到的一个课题。
关于无限滚动,早期的解决方案基本都是依赖监听scroll事件:
function fetchData() {
fetch(path).then(res => doSomeThing(res.data));
}
window.addEventListener('scroll', fetchData);
然后计算各种.scrollTop()
、.offset().top
等等。
手写一个也是非常枯燥。而且:
scroll
事件会频繁触发,因此我们还需要手动节流。DOM
,容易造成卡顿。
后来出现交叉观察者IntersectionObserver API
,在与Vue
、React
这类数据驱动视图的框架后,无限滚动的通用方案就出来了。
IntersectionObserver
const box = document.querySelector('.box');
const intersectionObserver = new IntersectionObserver((entries) => {
entries.forEach((item) => {
if (item.isIntersecting) {
console.log('进入可视区域');
}
})
});
intersectionObserver.observe(box);
敲重点:
IntersectionObserver API是异步的,不随着目标元素的滚动同步触发,性能消耗极低。
IntersectionObserverEntry
对象IntersectionObserverEntry
对象
callback
函数被调用时,会传给它一个数组,这个数组里的每个对象就是当前进入可视区域或者离开可视区域的对象(IntersectionObserverEntry
对象)
这个对象有很多属性,其中最常用的属性是:
target
: 被观察的目标元素,是一个 DOM 节点对象isIntersecting
: 是否进入可视区域intersectionRatio
: 相交区域和目标元素的比例值,进入可视区域,值大于0,否则等于0options
调用IntersectionObserver
时,除了传一个回调函数,还可以传入一个option
对象,配置如下属性:
threshold
: 决定了什么时候触发回调函数。它是一个数组,每个成员都是一个门槛值,默认为[0],即交叉比例(intersectionRatio)达到0时触发回调函数。用户可以自定义这个数组。比如,[0, 0.25, 0.5, 0.75, 1]就表示当目标元素 0%、25%、50%、75%、100% 可见时,会触发回调函数。root
: 用于观察的根元素,默认是浏览器的视口,也可以指定具体元素,指定元素的时候用于观察的元素必须是指定元素的子元素rootMargin
: 用来扩大或者缩小视窗的的大小,使用css的定义方法,10px 10px 30px 20px表示top、right、bottom 和 left的值const io = new IntersectionObserver((entries) => {
console.log(entries);
}, {
threshold: [0, 0.5],
root: document.querySelector('.container'),
rootMargin: "10px 10px 30px 20px",
});
observer
observer.observer(nodeone); //仅观察nodeOne
observer.observer(nodeTwo); //观察nodeOne和nodeTwo
observer.unobserve(nodeOne); //停止观察nodeOne
observer.disconnect(); //没有观察任何节点
React Hook
中使用IntersectionObserver
在看Hooks
版之前,来看正常组件版的:
class SlidingWindowScroll extends React.Component {
this.$bottomElement = React.createRef();
...
componentDidMount() {
this.intiateScrollObserver();
}
intiateScrollObserver = () => {
const options = {
root: null,
rootMargin: '0px',
threshold: 0.1
};
this.observer = new IntersectionObserver(this.callback, options);
this.observer.observe(this.$bottomElement.current);
}
render() {
return (
<li className='img' ref={this.$bottomElement}>
)
}
众所周知,React 16.x
后推出了useRef
来替代原有的createRef
,用于追踪DOM节点。那让我们开始吧:
实现一个组件,可以显示具有15个元素的固定窗口大小的n个项目的列表:
即在任何时候,无限滚动n元素上也仅存在15个DOM
节点。
relative/absolute
定位来确定滚动位置ref
: top/bottom
来决定向上/向下滚动的渲染与否useState
声明状态变量我们开始编写组件SlidingWindowScrollHook
:
const THRESHOLD = 15;
const SlidingWindowScrollHook = (props) => {
const [start, setStart] = useState(0);
const [end, setEnd] = useState(THRESHOLD);
const [observer, setObserver] = useState(null);
// 其它代码...
}
useState的简单理解:
const [属性, 操作属性的方法] = useState(默认值);
start
:当前渲染的列表第一个数据,默认为0end
: 当前渲染的列表最后一个数据,默认为15observer
: 当前观察的视图ref
元素useRef
定义追踪的DOM
元素const $bottomElement = useRef();
const $topElement = useRef();
正常的无限向下滚动只需关注一个dom元素,但由于我们是固定15个dom
元素渲染,需要判断向上或向下滚动。
useEffect
请配合注释食用:
useEffect(() => {
// 定义观察
intiateScrollObserver();
return () => {
// 放弃观察
resetObservation()
}
},[end])
// 定义观察
const intiateScrollObserver = () => {
const options = {
root: null,
rootMargin: '0px',
threshold: 0.1
};
const Observer = new IntersectionObserver(callback, options)
// 分别观察开头和结尾的元素
if ($topElement.current) {
Observer.observe($topElement.current);
}
if ($bottomElement.current) {
Observer.observe($bottomElement.current);
}
// 设初始值
setObserver(Observer)
}
// 交叉观察的具体回调,观察每个节点,并对实时头尾元素索引处理
const callback = (entries, observer) => {
entries.forEach((entry, index) => {
const listLength = props.list.length;
// 向下滚动,刷新数据
if (entry.isIntersecting && entry.target.id === "bottom") {
const maxStartIndex = listLength - 1 - THRESHOLD; // 当前头部的索引
const maxEndIndex = listLength - 1; // 当前尾部的索引
const newEnd = (end + 10) <= maxEndIndex ? end + 10 : maxEndIndex; // 下一轮增加尾部
const newStart = (end - 5) <= maxStartIndex ? end - 5 : maxStartIndex; // 在上一轮的基础上计算头部
setStart(newStart)
setEnd(newEnd)
}
// 向上滚动,刷新数据
if (entry.isIntersecting && entry.target.id === "top") {
const newEnd = end === THRESHOLD ? THRESHOLD : (end - 10 > THRESHOLD ? end - 10 : THRESHOLD); // 向上滚动尾部元素索引不得小于15
let newStart = start === 0 ? 0 : (start - 10 > 0 ? start - 10 : 0); // 头部元素索引最小值为0
setStart(newStart)
setEnd(newEnd)
}
});
}
// 停止滚动时放弃观察
const resetObservation = () => {
observer && observer.unobserve($bottomElement.current);
observer && observer.unobserve($topElement.current);
}
// 渲染时,头尾ref处理
const getReference = (index, isLastIndex) => {
if (index === 0)
return $topElement;
if (isLastIndex)
return $bottomElement;
return null;
}
const {list, height} = props; // 数据,节点高度
const updatedList = list.slice(start, end); // 数据切割
const lastIndex = updatedList.length - 1;
return (
<ul style={{position: 'relative'}}>
{updatedList.map((item, index) => {
const top = (height * (index + start)) + 'px'; // 基于相对 & 绝对定位 计算
const refVal = getReference(index, index === lastIndex); // map循环中赋予头尾ref
const id = index === 0 ? 'top' : (index === lastIndex ? 'bottom' : ''); // 绑ID
return (<li className="li-card" key={item.key} style={{top}} ref={refVal} id={id}>{item.value}</li>);
})}
</ul>
);
App.js
:
import React from 'react';
import './App.css';
import { SlidingWindowScrollHook } from "./SlidingWindowScrollHook";
import MY_ENDLESS_LIST from './Constants';
function App() {
return (
<div className="App">
<h1>15个元素实现无限滚动</h1>
<SlidingWindowScrollHook list={MY_ENDLESS_LIST} height={195}/>
</div>
);
}
export default App;
定义一下数据 Constants.js
:
const MY_ENDLESS_LIST = [
{
key: 1,
value: 'A'
},
{
key: 2,
value: 'B'
},
{
key: 3,
value: 'C'
},
// 中间就不贴了...
{
key: 45,
value: 'AS'
}
]
SlidingWindowScrollHook.js
:
import React, { useState, useEffect, useRef } from "react";
const THRESHOLD = 15;
const SlidingWindowScrollHook = (props) => {
const [start, setStart] = useState(0);
const [end, setEnd] = useState(THRESHOLD);
const [observer, setObserver] = useState(null);
const $bottomElement = useRef();
const $topElement = useRef();
useEffect(() => {
intiateScrollObserver();
return () => {
resetObservation()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
},[start, end])
const intiateScrollObserver = () => {
const options = {
root: null,
rootMargin: '0px',
threshold: 0.1
};
const Observer = new IntersectionObserver(callback, options)
if ($topElement.current) {
Observer.observe($topElement.current);
}
if ($bottomElement.current) {
Observer.observe($bottomElement.current);
}
setObserver(Observer)
}
const callback = (entries, observer) => {
entries.forEach((entry, index) => {
const listLength = props.list.length;
// Scroll Down
if (entry.isIntersecting && entry.target.id === "bottom") {
const maxStartIndex = listLength - 1 - THRESHOLD; // Maximum index value `start` can take
const maxEndIndex = listLength - 1; // Maximum index value `end` can take
const newEnd = (end + 10) <= maxEndIndex ? end + 10 : maxEndIndex;
const newStart = (end - 5) <= maxStartIndex ? end - 5 : maxStartIndex;
setStart(newStart)
setEnd(newEnd)
}
// Scroll up
if (entry.isIntersecting && entry.target.id === "top") {
const newEnd = end === THRESHOLD ? THRESHOLD : (end - 10 > THRESHOLD ? end - 10 : THRESHOLD);
let newStart = start === 0 ? 0 : (start - 10 > 0 ? start - 10 : 0);
setStart(newStart)
setEnd(newEnd)
}
});
}
const resetObservation = () => {
observer && observer.unobserve($bottomElement.current);
observer && observer.unobserve($topElement.current);
}
const getReference = (index, isLastIndex) => {
if (index === 0)
return $topElement;
if (isLastIndex)
return $bottomElement;
return null;
}
const {list, height} = props;
const updatedList = list.slice(start, end);
const lastIndex = updatedList.length - 1;
return (
<ul style={{position: 'relative'}}>
{updatedList.map((item, index) => {
const top = (height * (index + start)) + 'px';
const refVal = getReference(index, index === lastIndex);
const id = index === 0 ? 'top' : (index === lastIndex ? 'bottom' : '');
return (<li className="li-card" key={item.key} style={{top}} ref={refVal} id={id}>{item.value}</li>);
})}
</ul>
);
}
export { SlidingWindowScrollHook };
以及少许样式:
.li-card {
list-style: none;
box-shadow: 2px 2px 9px 0px #bbb;
padding: 70px 10px;
margin-bottom: 20px;
border-radius: 10px;
position: absolute;
left: 30px;
width: 80%;
}
然后你就可以慢慢耍了。。。
IntersectionObserver
不兼容Safari
?
莫慌,我们有polyfill
版
如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:
一线大厂笔试题灵感来源
目录:
筛选自以下两篇文章:
原本只想筛选下上面的那篇文章,在精简掉了部分多余且无用的代码片段后,感觉不够。于是顺藤摸瓜,找到了原地址:
然后将所有代码段都看了遍,筛选了以下一百多段代码片段,并加入了部分自己的理解。
另外,本文工具函数的命名非常值得借鉴。
all
:布尔全等判断const all = (arr, fn = Boolean) => arr.every(fn);
all([4, 2, 3], x => x > 1); // true
all([1, 2, 3]); // true
allEqual
:检查数组各项相等const allEqual = arr => arr.every(val => val === arr[0]);
allEqual([1, 2, 3, 4, 5, 6]); // false
allEqual([1, 1, 1, 1]); // true
approximatelyEqual
:约等于const approximatelyEqual = (v1, v2, epsilon = 0.001) => Math.abs(v1 - v2) < epsilon;
approximatelyEqual(Math.PI / 2.0, 1.5708); // true
arrayToCSV
:数组转CSV
格式(带空格的字符串)
const arrayToCSV = (arr, delimiter = ',') =>
arr.map(v => v.map(x => `"${x}"`).join(delimiter)).join('\n');
arrayToCSV([['a', 'b'], ['c', 'd']]); // '"a","b"\n"c","d"'
arrayToCSV([['a', 'b'], ['c', 'd']], ';'); // '"a";"b"\n"c";"d"'
arrayToHtmlList
:数组转li
列表此代码段将数组的元素转换为<li>
标签,并将其附加到给定ID的列表中。
const arrayToHtmlList = (arr, listID) =>
(el => (
(el = document.querySelector('#' + listID)),
(el.innerHTML += arr.map(item => `<li>${item}</li>`).join(''))
))();
arrayToHtmlList(['item 1', 'item 2'], 'myListID');
average
:平均数const average = (...nums) => nums.reduce((acc, val) => acc + val, 0) / nums.length;
average(...[1, 2, 3]); // 2
average(1, 2, 3); // 2
averageBy
:数组对象属性平均数此代码段将获取数组对象属性的平均值
const averageBy = (arr, fn) =>
arr.map(typeof fn === 'function' ? fn : val => val[fn]).reduce((acc, val) => acc + val, 0) /
arr.length;
averageBy([{ n: 4 }, { n: 2 }, { n: 8 }, { n: 6 }], o => o.n); // 5
averageBy([{ n: 4 }, { n: 2 }, { n: 8 }, { n: 6 }], 'n'); // 5
bifurcate
:拆分断言后的数组可以根据每个元素返回的值,使用reduce()
和push()
将元素添加到第二次参数fn
中 。
const bifurcate = (arr, filter) =>
arr.reduce((acc, val, i) => (acc[filter[i] ? 0 : 1].push(val), acc), [[], []]);
bifurcate(['beep', 'boop', 'foo', 'bar'], [true, true, false, true]);
// [ ['beep', 'boop', 'bar'], ['foo'] ]
castArray
:其它类型转数组const castArray = val => (Array.isArray(val) ? val : [val]);
castArray('foo'); // ['foo']
castArray([1]); // [1]
castArray(1); // [1]
compact
:去除数组中的无效/无用值const compact = arr => arr.filter(Boolean);
compact([0, 1, false, 2, '', 3, 'a', 'e' * 23, NaN, 's', 34]);
// [ 1, 2, 3, 'a', 's', 34 ]
countOccurrences
:检测数值出现次数const countOccurrences = (arr, val) => arr.reduce((a, v) => (v === val ? a + 1 : a), 0);
countOccurrences([1, 1, 2, 1, 2, 3], 1); // 3
deepFlatten
:递归扁平化数组const deepFlatten = arr => [].concat(...arr.map(v => (Array.isArray(v) ? deepFlatten(v) : v)));
deepFlatten([1, [2], [[3], 4], 5]); // [1,2,3,4,5]
difference
:寻找差异此代码段查找两个数组之间的差异。
const difference = (a, b) => {
const s = new Set(b);
return a.filter(x => !s.has(x));
};
difference([1, 2, 3], [1, 2, 4]); // [3]
differenceBy
:先执行再寻找差异在将给定函数应用于两个列表的每个元素之后,此方法返回两个数组之间的差异。
const differenceBy = (a, b, fn) => {
const s = new Set(b.map(fn));
return a.filter(x => !s.has(fn(x)));
};
differenceBy([2.1, 1.2], [2.3, 3.4], Math.floor); // [1.2]
differenceBy([{ x: 2 }, { x: 1 }], [{ x: 1 }], v => v.x); // [ { x: 2 } ]
dropWhile
:删除不符合条件的值此代码段从数组顶部开始删除元素,直到传递的函数返回为true
。
const dropWhile = (arr, func) => {
while (arr.length > 0 && !func(arr[0])) arr = arr.slice(1);
return arr;
};
dropWhile([1, 2, 3, 4], n => n >= 3); // [3,4]
flatten
:指定深度扁平化数组此代码段第二参数可指定深度。
const flatten = (arr, depth = 1) =>
arr.reduce((a, v) => a.concat(depth > 1 && Array.isArray(v) ? flatten(v, depth - 1) : v), []);
flatten([1, [2], 3, 4]); // [1, 2, 3, 4]
flatten([1, [2, [3, [4, 5], 6], 7], 8], 2); // [1, 2, 3, [4, 5], 6, 7, 8]
indexOfAll
:返回数组中某值的所有索引此代码段可用于获取数组中某个值的所有索引,如果此值中未包含该值,则返回一个空数组。
const indexOfAll = (arr, val) => arr.reduce((acc, el, i) => (el === val ? [...acc, i] : acc), []);
indexOfAll([1, 2, 3, 1, 2, 3], 1); // [0,3]
indexOfAll([1, 2, 3], 4); // []
intersection
:两数组的交集
const intersection = (a, b) => {
const s = new Set(b);
return a.filter(x => s.has(x));
};
intersection([1, 2, 3], [4, 3, 2]); // [2, 3]
intersectionWith
:两数组都符合条件的交集此片段可用于在对两个数组的每个元素执行了函数之后,返回两个数组中存在的元素列表。
const intersectionBy = (a, b, fn) => {
const s = new Set(b.map(fn));
return a.filter(x => s.has(fn(x)));
};
intersectionBy([2.1, 1.2], [2.3, 3.4], Math.floor); // [2.1]
intersectionWith
:先比较后返回交集const intersectionWith = (a, b, comp) => a.filter(x => b.findIndex(y => comp(x, y)) !== -1);
intersectionWith([1, 1.2, 1.5, 3, 0], [1.9, 3, 0, 3.9], (a, b) => Math.round(a) === Math.round(b)); // [1.5, 3, 0]
minN
:返回指定长度的升序数组const minN = (arr, n = 1) => [...arr].sort((a, b) => a - b).slice(0, n);
minN([1, 2, 3]); // [1]
minN([1, 2, 3], 2); // [1,2]
negate
:根据条件反向筛选
const negate = func => (...args) => !func(...args);
[1, 2, 3, 4, 5, 6].filter(negate(n => n % 2 === 0)); // [ 1, 3, 5 ]
randomIntArrayInRange
:生成两数之间指定长度的随机数组const randomIntArrayInRange = (min, max, n = 1) =>
Array.from({ length: n }, () => Math.floor(Math.random() * (max - min + 1)) + min);
randomIntArrayInRange(12, 35, 10); // [ 34, 14, 27, 17, 30, 27, 20, 26, 21, 14 ]
sample
:在指定数组中获取随机数const sample = arr => arr[Math.floor(Math.random() * arr.length)];
sample([3, 7, 9, 11]); // 9
sampleSize
:在指定数组中获取指定长度的随机数此代码段可用于从数组中获取指定长度的随机数,直至穷尽数组。
使用Fisher-Yates
算法对数组中的元素进行随机选择。
const sampleSize = ([...arr], n = 1) => {
let m = arr.length;
while (m) {
const i = Math.floor(Math.random() * m--);
[arr[m], arr[i]] = [arr[i], arr[m]];
}
return arr.slice(0, n);
};
sampleSize([1, 2, 3], 2); // [3,1]
sampleSize([1, 2, 3], 4); // [2,3,1]
shuffle
:“洗牌” 数组此代码段使用Fisher-Yates
算法随机排序数组的元素。
const shuffle = ([...arr]) => {
let m = arr.length;
while (m) {
const i = Math.floor(Math.random() * m--);
[arr[m], arr[i]] = [arr[i], arr[m]];
}
return arr;
};
const foo = [1, 2, 3];
shuffle(foo); // [2, 3, 1], foo = [1, 2, 3]
nest
:根据parent_id
生成树结构(阿里一面真题)根据每项的parent_id
,生成具体树形结构的对象。
const nest = (items, id = null, link = 'parent_id') =>
items
.filter(item => item[link] === id)
.map(item => ({ ...item, children: nest(items, item.id) }));
用法:
const comments = [
{ id: 1, parent_id: null },
{ id: 2, parent_id: 1 },
{ id: 3, parent_id: 1 },
{ id: 4, parent_id: 2 },
{ id: 5, parent_id: 4 }
];
const nestedComments = nest(comments); // [{ id: 1, parent_id: null, children: [...] }]
强烈建议去理解这个的实现,因为这是我亲身遇到的阿里一面真题:
attempt
:捕获函数运行异常该代码段执行一个函数,返回结果或捕获的错误对象。
onst attempt = (fn, ...args) => {
try {
return fn(...args);
} catch (e) {
return e instanceof Error ? e : new Error(e);
}
};
var elements = attempt(function(selector) {
return document.querySelectorAll(selector);
}, '>_>');
if (elements instanceof Error) elements = []; // elements = []
defer
:推迟执行此代码段延迟了函数的执行,直到清除了当前调用堆栈。
const defer = (fn, ...args) => setTimeout(fn, 1, ...args);
defer(console.log, 'a'), console.log('b'); // logs 'b' then 'a'
runPromisesInSeries
:运行多个Promises
const runPromisesInSeries = ps => ps.reduce((p, next) => p.then(next), Promise.resolve());
const delay = d => new Promise(r => setTimeout(r, d));
runPromisesInSeries([() => delay(1000), () => delay(2000)]);
//依次执行每个Promises ,总共需要3秒钟才能完成
timeTaken
:计算函数执行时间
const timeTaken = callback => {
console.time('timeTaken');
const r = callback();
console.timeEnd('timeTaken');
return r;
};
timeTaken(() => Math.pow(2, 10)); // 1024, (logged): timeTaken: 0.02099609375ms
createEventHub
:简单的发布/订阅模式创建一个发布/订阅(发布-订阅)事件集线,有emit
,on
和off
方法。
Object.create(null)
创建一个空的hub
对象。emit
,根据event
参数解析处理程序数组,然后.forEach()
通过传入数据作为参数来运行每个处理程序。on
,为事件创建一个数组(若不存在则为空数组),然后.push()
将处理程序添加到该数组。off
,用.findIndex()
在事件数组中查找处理程序的索引,并使用.splice()
删除。const createEventHub = () => ({
hub: Object.create(null),
emit(event, data) {
(this.hub[event] || []).forEach(handler => handler(data));
},
on(event, handler) {
if (!this.hub[event]) this.hub[event] = [];
this.hub[event].push(handler);
},
off(event, handler) {
const i = (this.hub[event] || []).findIndex(h => h === handler);
if (i > -1) this.hub[event].splice(i, 1);
if (this.hub[event].length === 0) delete this.hub[event];
}
});
用法:
const handler = data => console.log(data);
const hub = createEventHub();
let increment = 0;
// 订阅,监听不同事件
hub.on('message', handler);
hub.on('message', () => console.log('Message event fired'));
hub.on('increment', () => increment++);
// 发布:发出事件以调用所有订阅给它们的处理程序,并将数据作为参数传递给它们
hub.emit('message', 'hello world'); // 打印 'hello world' 和 'Message event fired'
hub.emit('message', { hello: 'world' }); // 打印 对象 和 'Message event fired'
hub.emit('increment'); // increment = 1
// 停止订阅
hub.off('message', handler);
memoize
:缓存函数通过实例化一个Map
对象来创建一个空的缓存。
通过检查输入值的函数输出是否已缓存,返回存储一个参数的函数,该参数将被提供给已记忆的函数;如果没有,则存储并返回它。
const memoize = fn => {
const cache = new Map();
const cached = function(val) {
return cache.has(val) ? cache.get(val) : cache.set(val, fn.call(this, val)) && cache.get(val);
};
cached.cache = cache;
return cached;
};
Ps: 这个版本可能不是很清晰,还有Vue源码版的:
/**
* Create a cached version of a pure function.
*/
export function cached<F: Function> (fn: F): F {
const cache = Object.create(null)
return (function cachedFn (str: string) {
const hit = cache[str]
return hit || (cache[str] = fn(str))
}: any)
}
once
:只调用一次的函数const once = fn => {
let called = false
return function () {
if (!called) {
called = true
fn.apply(this, arguments)
}
}
};
flattenObject
:以键的路径扁平化对象使用递归。
Object.keys(obj)
联合Array.prototype.reduce()
,以每片叶子节点转换为扁平的路径节点。prefix
以创建路径Object.assign()
。prefix
除非你希望每个键都有一个前缀,否则应始终省略第二个参数。const flattenObject = (obj, prefix = '') =>
Object.keys(obj).reduce((acc, k) => {
const pre = prefix.length ? prefix + '.' : '';
if (typeof obj[k] === 'object') Object.assign(acc, flattenObject(obj[k], pre + k));
else acc[pre + k] = obj[k];
return acc;
}, {});
flattenObject({ a: { b: { c: 1 } }, d: 1 }); // { 'a.b.c': 1, d: 1 }
unflattenObject
:以键的路径展开对象与上面的相反,展开对象。
const unflattenObject = obj =>
Object.keys(obj).reduce((acc, k) => {
if (k.indexOf('.') !== -1) {
const keys = k.split('.');
Object.assign(
acc,
JSON.parse(
'{' +
keys.map((v, i) => (i !== keys.length - 1 ? `"${v}":{` : `"${v}":`)).join('') +
obj[k] +
'}'.repeat(keys.length)
)
);
} else acc[k] = obj[k];
return acc;
}, {});
unflattenObject({ 'a.b.c': 1, d: 1 }); // { a: { b: { c: 1 } }, d: 1 }
这个的用途,在做Tree
组件或复杂表单时取值非常舒服。
byteSize
:返回字符串的字节长度const byteSize = str => new Blob([str]).size;
byteSize('😀'); // 4
byteSize('Hello World'); // 11
capitalize
:首字母大写const capitalize = ([first, ...rest]) =>
first.toUpperCase() + rest.join('');
capitalize('fooBar'); // 'FooBar'
capitalize('fooBar', true); // 'Foobar'
capitalizeEveryWord
:每个单词首字母大写const capitalizeEveryWord = str => str.replace(/\b[a-z]/g, char => char.toUpperCase());
capitalizeEveryWord('hello world!'); // 'Hello World!'
decapitalize
:首字母小写const decapitalize = ([first, ...rest]) =>
first.toLowerCase() + rest.join('')
decapitalize('FooBar'); // 'fooBar'
decapitalize('FooBar'); // 'fooBar'
luhnCheck
:银行卡号码校验(luhn
算法)Luhn
算法的实现,用于验证各种标识号,例如信用卡号,IMEI号,国家提供商标识号等。
与String.prototype.split('')
结合使用,以获取数字数组。获得最后一个数字。实施luhn
算法。如果被整除,则返回,否则返回。
const luhnCheck = num => {
let arr = (num + '')
.split('')
.reverse()
.map(x => parseInt(x));
let lastDigit = arr.splice(0, 1)[0];
let sum = arr.reduce((acc, val, i) => (i % 2 !== 0 ? acc + val : acc + ((val * 2) % 9) || 9), 0);
sum += lastDigit;
return sum % 10 === 0;
};
用例:
luhnCheck('4485275742308327'); // true
luhnCheck(6011329933655299); // false
luhnCheck(123456789); // false
关于luhn
算法,可以参考以下文章:
银行卡号码的校验采用Luhn
算法,校验过程大致如下:
从右到左给卡号字符串编号,最右边第一位是1,最右边第二位是2,最右边第三位是3….
从右向左遍历,对每一位字符t执行第三个步骤,并将每一位的计算结果相加得到一个数s。
对每一位的计算规则:如果这一位是奇数位,则返回t本身,如果是偶数位,则先将t乘以2得到一个数n,如果n是一位数(小于10),直接返回n,否则将n的个位数和十位数相加返回。
如果s能够整除10,则此号码有效,否则号码无效。
因为最终的结果会对10取余来判断是否能够整除10,所以又叫做模10算法。
当然,还是库比较香: bankcardinfo
splitLines
:将多行字符串拆分为行数组。使用String.prototype.split()
和正则表达式匹配换行符并创建一个数组。
const splitLines = str => str.split(/\r?\n/);
splitLines('This\nis a\nmultiline\nstring.\n'); // ['This', 'is a', 'multiline', 'string.' , '']
stripHTMLTags
:删除字符串中的HTMl
标签从字符串中删除HTML / XML
标签。
使用正则表达式从字符串中删除HTML / XML
标记。
const stripHTMLTags = str => str.replace(/<[^>]*>/g, '');
stripHTMLTags('<p><em>lorem</em> <strong>ipsum</strong></p>'); // 'lorem ipsum'
dayOfYear
:当前日期天数const dayOfYear = date =>
Math.floor((date - new Date(date.getFullYear(), 0, 0)) / 1000 / 60 / 60 / 24);
dayOfYear(new Date()); // 285
forOwn
:迭代属性并执行回调const forOwn = (obj, fn) => Object.keys(obj).forEach(key => fn(obj[key], key, obj));
forOwn({ foo: 'bar', a: 1 }, v => console.log(v)); // 'bar', 1
Get Time From Date
:返回当前24小时制时间的字符串const getColonTimeFromDate = date => date.toTimeString().slice(0, 8);
getColonTimeFromDate(new Date()); // "08:38:00"
Get Days Between Dates
:返回日期间的天数const getDaysDiffBetweenDates = (dateInitial, dateFinal) =>
(dateFinal - dateInitial) / (1000 * 3600 * 24);
getDaysDiffBetweenDates(new Date('2019-01-01'), new Date('2019-10-14')); // 286
is
:检查值是否为特定类型。const is = (type, val) => ![, null].includes(val) && val.constructor === type;
is(Array, [1]); // true
is(ArrayBuffer, new ArrayBuffer()); // true
is(Map, new Map()); // true
is(RegExp, /./g); // true
is(Set, new Set()); // true
is(WeakMap, new WeakMap()); // true
is(WeakSet, new WeakSet()); // true
is(String, ''); // true
is(String, new String('')); // true
is(Number, 1); // true
is(Number, new Number(1)); // true
is(Boolean, true); // true
is(Boolean, new Boolean(true)); // true
isAfterDate
:检查是否在某日期后const isAfterDate = (dateA, dateB) => dateA > dateB;
isAfterDate(new Date(2010, 10, 21), new Date(2010, 10, 20)); // true
isBeforeDate
:检查是否在某日期前const isBeforeDate = (dateA, dateB) => dateA < dateB;
isBeforeDate(new Date(2010, 10, 20), new Date(2010, 10, 21)); // true
tomorrow
:获取明天的字符串格式时间
const tomorrow = () => {
let t = new Date();
t.setDate(t.getDate() + 1);
return t.toISOString().split('T')[0];
};
tomorrow(); // 2019-10-15 (如果明天是2019-10-15)
equals
:全等判断在两个变量之间进行深度比较以确定它们是否全等。
此代码段精简的核心在于Array.prototype.every()
的使用。
const equals = (a, b) => {
if (a === b) return true;
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
if (!a || !b || (typeof a !== 'object' && typeof b !== 'object')) return a === b;
if (a.prototype !== b.prototype) return false;
let keys = Object.keys(a);
if (keys.length !== Object.keys(b).length) return false;
return keys.every(k => equals(a[k], b[k]));
};
用法:
equals({ a: [2, { e: 3 }], b: [4], c: 'foo' }, { a: [2, { e: 3 }], b: [4], c: 'foo' }); // true
randomIntegerInRange
:生成指定范围的随机整数const randomIntegerInRange = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
randomIntegerInRange(0, 5); // 3
randomNumberInRange
:生成指定范围的随机小数const randomNumberInRange = (min, max) => Math.random() * (max - min) + min;
randomNumberInRange(2, 10); // 6.0211363285087005
round
:四舍五入到指定位数const round = (n, decimals = 0) => Number(`${Math.round(`${n}e${decimals}`)}e-${decimals}`);
round(1.005, 2); // 1.01
sum
:计算数组或多个数字的总和
const sum = (...arr) => [...arr].reduce((acc, val) => acc + val, 0);
sum(1, 2, 3, 4); // 10
sum(...[1, 2, 3, 4]); // 10
toCurrency
:简单的货币单位转换const toCurrency = (n, curr, LanguageFormat = undefined) =>
Intl.NumberFormat(LanguageFormat, { style: 'currency', currency: curr }).format(n);
toCurrency(123456.789, 'EUR'); // €123,456.79
toCurrency(123456.789, 'USD', 'en-us'); // $123,456.79
toCurrency(123456.789, 'USD', 'fa'); // ۱۲۳٬۴۵۶٫۷۹
toCurrency(322342436423.2435, 'JPY'); // ¥322,342,436,423
bottomVisible
:检查页面底部是否可见const bottomVisible = () =>
document.documentElement.clientHeight + window.scrollY >=
(document.documentElement.scrollHeight || document.documentElement.clientHeight);
bottomVisible(); // true
Create Directory
:检查创建目录此代码段调用fs
模块的existsSync()
检查目录是否存在,如果不存在,则mkdirSync()
创建该目录。
const fs = require('fs');
const createDirIfNotExists = dir => (!fs.existsSync(dir) ? fs.mkdirSync(dir) : undefined);
createDirIfNotExists('test');
currentURL
:返回当前链接url
const currentURL = () => window.location.href;
currentURL(); // 'https://juejin.im'
distance
:返回两点间的距离该代码段通过计算欧几里得距离来返回两点之间的距离。
const distance = (x0, y0, x1, y1) => Math.hypot(x1 - x0, y1 - y0);
distance(1, 1, 2, 3); // 2.23606797749979
elementContains
:检查是否包含子元素此代码段检查父元素是否包含子元素。
const elementContains = (parent, child) => parent !== child && parent.contains(child);
elementContains(document.querySelector('head'), document.querySelector('title')); // true
elementContains(document.querySelector('body'), document.querySelector('body')); // false
getStyle
:返回指定元素的生效样式const getStyle = (el, ruleName) => getComputedStyle(el)[ruleName];
getStyle(document.querySelector('p'), 'font-size'); // '16px'
getType
:返回值或变量的类型名const getType = v =>
v === undefined ? 'undefined' : v === null ? 'null' : v.constructor.name.toLowerCase();
getType(new Set([1, 2, 3])); // 'set'
getType([1, 2, 3]); // 'array'
hasClass
:校验指定元素的类名const hasClass = (el, className) => el.classList.contains(className);
hasClass(document.querySelector('p.special'), 'special'); // true
hide
:隐藏所有的指定标签const hide = (...el) => [...el].forEach(e => (e.style.display = 'none'));
hide(document.querySelectorAll('img')); // 隐藏所有<img>标签
httpsRedirect
:HTTP
跳转 HTTPS
const httpsRedirect = () => {
if (location.protocol !== 'https:') location.replace('https://' + location.href.split('//')[1]);
};
httpsRedirect(); // 若在`http://www.baidu.com`, 则跳转到`https://www.baidu.com`
insertAfter
:在指定元素之后插入新元素const insertAfter = (el, htmlString) => el.insertAdjacentHTML('afterend', htmlString);
// <div id="myId">...</div> <p>after</p>
insertAfter(document.getElementById('myId'), '<p>after</p>');
insertBefore
:在指定元素之前插入新元素const insertBefore = (el, htmlString) => el.insertAdjacentHTML('beforebegin', htmlString);
insertBefore(document.getElementById('myId'), '<p>before</p>'); // <p>before</p> <div id="myId">...</div>
isBrowser
:检查是否为浏览器环境此代码段可用于确定当前运行时环境是否为浏览器。这有助于避免在服务器(节点)上运行前端模块时出错。
const isBrowser = () => ![typeof window, typeof document].includes('undefined');
isBrowser(); // true (browser)
isBrowser(); // false (Node)
isBrowserTab
:检查当前标签页是否活动const isBrowserTabFocused = () => !document.hidden;
isBrowserTabFocused(); // true
nodeListToArray
:转换nodeList
为数组const nodeListToArray = nodeList => [...nodeList];
nodeListToArray(document.childNodes); // [ <!DOCTYPE html>, html ]
Random Hexadecimal Color Code
:随机十六进制颜色
const randomHexColorCode = () => {
let n = (Math.random() * 0xfffff * 1000000).toString(16);
return '#' + n.slice(0, 6);
};
randomHexColorCode(); // "#e34155"
scrollToTop
:平滑滚动至顶部该代码段可用于平滑滚动到当前页面的顶部。
const scrollToTop = () => {
const c = document.documentElement.scrollTop || document.body.scrollTop;
if (c > 0) {
window.requestAnimationFrame(scrollToTop);
window.scrollTo(0, c - c / 8);
}
};
scrollToTop();
smoothScroll
:滚动到指定元素区域该代码段可将指定元素平滑滚动到浏览器窗口的可见区域。
const smoothScroll = element =>
document.querySelector(element).scrollIntoView({
behavior: 'smooth'
});
smoothScroll('#fooBar');
smoothScroll('.fooBar');
detectDeviceType
:检测移动/PC设备const detectDeviceType = () =>
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
? 'Mobile'
: 'Desktop';
getScrollPosition
:返回当前的滚动位置默认参数为window
,pageXOffset(pageYOffset)
为第一选择,没有则用scrollLeft(scrollTop)
const getScrollPosition = (el = window) => ({
x: el.pageXOffset !== undefined ? el.pageXOffset : el.scrollLeft,
y: el.pageYOffset !== undefined ? el.pageYOffset : el.scrollTop
});
getScrollPosition(); // {x: 0, y: 200}
size
:获取不同类型变量的长度这个的实现非常巧妙,利用Blob
类文件对象的特性,获取对象的长度。
另外,多重三元运算符,是真香。
const size = val =>
Array.isArray(val)
? val.length
: val && typeof val === 'object'
? val.size || val.length || Object.keys(val).length
: typeof val === 'string'
? new Blob([val]).size
: 0;
size([1, 2, 3, 4, 5]); // 5
size('size'); // 4
size({ one: 1, two: 2, three: 3 }); // 3
escapeHTML
:转义HTML
当然是用来防XSS
攻击啦。
const escapeHTML = str =>
str.replace(
/[&<>'"]/g,
tag =>
({
'&': '&',
'<': '<',
'>': '>',
"'": ''',
'"': '"'
}[tag] || tag)
);
escapeHTML('<a href="#">Me & you</a>'); // '<a href="#">Me & you</a>'
如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:
目录:
筛选自以下两篇文章:
原本只想筛选下上面的那篇文章,在精简掉了部分多余且无用的代码片段后,感觉不够。于是顺藤摸瓜,找到了原地址:
然后将所有代码段都看了遍,筛选了以下一百多段代码片段,并加入了部分自己的理解。
另外,本文工具函数的命名非常值得借鉴。
all
:布尔全等判断const all = (arr, fn = Boolean) => arr.every(fn);
all([4, 2, 3], x => x > 1); // true
all([1, 2, 3]); // true
allEqual
:检查数组各项相等const allEqual = arr => arr.every(val => val === arr[0]);
allEqual([1, 2, 3, 4, 5, 6]); // false
allEqual([1, 1, 1, 1]); // true
approximatelyEqual
:约等于const approximatelyEqual = (v1, v2, epsilon = 0.001) => Math.abs(v1 - v2) < epsilon;
approximatelyEqual(Math.PI / 2.0, 1.5708); // true
arrayToCSV
:数组转CSV
格式(带空格的字符串)
const arrayToCSV = (arr, delimiter = ',') =>
arr.map(v => v.map(x => `"${x}"`).join(delimiter)).join('\n');
arrayToCSV([['a', 'b'], ['c', 'd']]); // '"a","b"\n"c","d"'
arrayToCSV([['a', 'b'], ['c', 'd']], ';'); // '"a";"b"\n"c";"d"'
arrayToHtmlList
:数组转li
列表此代码段将数组的元素转换为<li>
标签,并将其附加到给定ID的列表中。
const arrayToHtmlList = (arr, listID) =>
(el => (
(el = document.querySelector('#' + listID)),
(el.innerHTML += arr.map(item => `<li>${item}</li>`).join(''))
))();
arrayToHtmlList(['item 1', 'item 2'], 'myListID');
average
:平均数const average = (...nums) => nums.reduce((acc, val) => acc + val, 0) / nums.length;
average(...[1, 2, 3]); // 2
average(1, 2, 3); // 2
averageBy
:数组对象属性平均数此代码段将获取数组对象属性的平均值
const averageBy = (arr, fn) =>
arr.map(typeof fn === 'function' ? fn : val => val[fn]).reduce((acc, val) => acc + val, 0) /
arr.length;
averageBy([{ n: 4 }, { n: 2 }, { n: 8 }, { n: 6 }], o => o.n); // 5
averageBy([{ n: 4 }, { n: 2 }, { n: 8 }, { n: 6 }], 'n'); // 5
bifurcate
:拆分断言后的数组可以根据每个元素返回的值,使用reduce()
和push()
将元素添加到第二次参数fn
中 。
const bifurcate = (arr, filter) =>
arr.reduce((acc, val, i) => (acc[filter[i] ? 0 : 1].push(val), acc), [[], []]);
bifurcate(['beep', 'boop', 'foo', 'bar'], [true, true, false, true]);
// [ ['beep', 'boop', 'bar'], ['foo'] ]
castArray
:其它类型转数组const castArray = val => (Array.isArray(val) ? val : [val]);
castArray('foo'); // ['foo']
castArray([1]); // [1]
castArray(1); // [1]
compact
:去除数组中的无效/无用值const compact = arr => arr.filter(Boolean);
compact([0, 1, false, 2, '', 3, 'a', 'e' * 23, NaN, 's', 34]);
// [ 1, 2, 3, 'a', 's', 34 ]
countOccurrences
:检测数值出现次数const countOccurrences = (arr, val) => arr.reduce((a, v) => (v === val ? a + 1 : a), 0);
countOccurrences([1, 1, 2, 1, 2, 3], 1); // 3
deepFlatten
:递归扁平化数组const deepFlatten = arr => [].concat(...arr.map(v => (Array.isArray(v) ? deepFlatten(v) : v)));
deepFlatten([1, [2], [[3], 4], 5]); // [1,2,3,4,5]
difference
:寻找差异此代码段查找两个数组之间的差异。
const difference = (a, b) => {
const s = new Set(b);
return a.filter(x => !s.has(x));
};
difference([1, 2, 3], [1, 2, 4]); // [3]
differenceBy
:先执行再寻找差异在将给定函数应用于两个列表的每个元素之后,此方法返回两个数组之间的差异。
const differenceBy = (a, b, fn) => {
const s = new Set(b.map(fn));
return a.filter(x => !s.has(fn(x)));
};
differenceBy([2.1, 1.2], [2.3, 3.4], Math.floor); // [1.2]
differenceBy([{ x: 2 }, { x: 1 }], [{ x: 1 }], v => v.x); // [ { x: 2 } ]
dropWhile
:删除不符合条件的值此代码段从数组顶部开始删除元素,直到传递的函数返回为true
。
const dropWhile = (arr, func) => {
while (arr.length > 0 && !func(arr[0])) arr = arr.slice(1);
return arr;
};
dropWhile([1, 2, 3, 4], n => n >= 3); // [3,4]
flatten
:指定深度扁平化数组此代码段第二参数可指定深度。
const flatten = (arr, depth = 1) =>
arr.reduce((a, v) => a.concat(depth > 1 && Array.isArray(v) ? flatten(v, depth - 1) : v), []);
flatten([1, [2], 3, 4]); // [1, 2, 3, 4]
flatten([1, [2, [3, [4, 5], 6], 7], 8], 2); // [1, 2, 3, [4, 5], 6, 7, 8]
indexOfAll
:返回数组中某值的所有索引此代码段可用于获取数组中某个值的所有索引,如果此值中未包含该值,则返回一个空数组。
const indexOfAll = (arr, val) => arr.reduce((acc, el, i) => (el === val ? [...acc, i] : acc), []);
indexOfAll([1, 2, 3, 1, 2, 3], 1); // [0,3]
indexOfAll([1, 2, 3], 4); // []
intersection
:两数组的交集
const intersection = (a, b) => {
const s = new Set(b);
return a.filter(x => s.has(x));
};
intersection([1, 2, 3], [4, 3, 2]); // [2, 3]
intersectionWith
:两数组都符合条件的交集此片段可用于在对两个数组的每个元素执行了函数之后,返回两个数组中存在的元素列表。
const intersectionBy = (a, b, fn) => {
const s = new Set(b.map(fn));
return a.filter(x => s.has(fn(x)));
};
intersectionBy([2.1, 1.2], [2.3, 3.4], Math.floor); // [2.1]
intersectionWith
:先比较后返回交集const intersectionWith = (a, b, comp) => a.filter(x => b.findIndex(y => comp(x, y)) !== -1);
intersectionWith([1, 1.2, 1.5, 3, 0], [1.9, 3, 0, 3.9], (a, b) => Math.round(a) === Math.round(b)); // [1.5, 3, 0]
minN
:返回指定长度的升序数组const minN = (arr, n = 1) => [...arr].sort((a, b) => a - b).slice(0, n);
minN([1, 2, 3]); // [1]
minN([1, 2, 3], 2); // [1,2]
negate
:根据条件反向筛选
const negate = func => (...args) => !func(...args);
[1, 2, 3, 4, 5, 6].filter(negate(n => n % 2 === 0)); // [ 1, 3, 5 ]
randomIntArrayInRange
:生成两数之间指定长度的随机数组const randomIntArrayInRange = (min, max, n = 1) =>
Array.from({ length: n }, () => Math.floor(Math.random() * (max - min + 1)) + min);
randomIntArrayInRange(12, 35, 10); // [ 34, 14, 27, 17, 30, 27, 20, 26, 21, 14 ]
sample
:在指定数组中获取随机数const sample = arr => arr[Math.floor(Math.random() * arr.length)];
sample([3, 7, 9, 11]); // 9
sampleSize
:在指定数组中获取指定长度的随机数此代码段可用于从数组中获取指定长度的随机数,直至穷尽数组。
使用Fisher-Yates
算法对数组中的元素进行随机选择。
const sampleSize = ([...arr], n = 1) => {
let m = arr.length;
while (m) {
const i = Math.floor(Math.random() * m--);
[arr[m], arr[i]] = [arr[i], arr[m]];
}
return arr.slice(0, n);
};
sampleSize([1, 2, 3], 2); // [3,1]
sampleSize([1, 2, 3], 4); // [2,3,1]
shuffle
:“洗牌” 数组此代码段使用Fisher-Yates
算法随机排序数组的元素。
const shuffle = ([...arr]) => {
let m = arr.length;
while (m) {
const i = Math.floor(Math.random() * m--);
[arr[m], arr[i]] = [arr[i], arr[m]];
}
return arr;
};
const foo = [1, 2, 3];
shuffle(foo); // [2, 3, 1], foo = [1, 2, 3]
nest
:根据parent_id
生成树结构(阿里一面真题)根据每项的parent_id
,生成具体树形结构的对象。
const nest = (items, id = null, link = 'parent_id') =>
items
.filter(item => item[link] === id)
.map(item => ({ ...item, children: nest(items, item.id) }));
用法:
const comments = [
{ id: 1, parent_id: null },
{ id: 2, parent_id: 1 },
{ id: 3, parent_id: 1 },
{ id: 4, parent_id: 2 },
{ id: 5, parent_id: 4 }
];
const nestedComments = nest(comments); // [{ id: 1, parent_id: null, children: [...] }]
强烈建议去理解这个的实现,因为这是我亲身遇到的阿里一面真题:
attempt
:捕获函数运行异常该代码段执行一个函数,返回结果或捕获的错误对象。
onst attempt = (fn, ...args) => {
try {
return fn(...args);
} catch (e) {
return e instanceof Error ? e : new Error(e);
}
};
var elements = attempt(function(selector) {
return document.querySelectorAll(selector);
}, '>_>');
if (elements instanceof Error) elements = []; // elements = []
defer
:推迟执行此代码段延迟了函数的执行,直到清除了当前调用堆栈。
const defer = (fn, ...args) => setTimeout(fn, 1, ...args);
defer(console.log, 'a'), console.log('b'); // logs 'b' then 'a'
runPromisesInSeries
:运行多个Promises
const runPromisesInSeries = ps => ps.reduce((p, next) => p.then(next), Promise.resolve());
const delay = d => new Promise(r => setTimeout(r, d));
runPromisesInSeries([() => delay(1000), () => delay(2000)]);
//依次执行每个Promises ,总共需要3秒钟才能完成
timeTaken
:计算函数执行时间
const timeTaken = callback => {
console.time('timeTaken');
const r = callback();
console.timeEnd('timeTaken');
return r;
};
timeTaken(() => Math.pow(2, 10)); // 1024, (logged): timeTaken: 0.02099609375ms
createEventHub
:简单的发布/订阅模式创建一个发布/订阅(发布-订阅)事件集线,有emit
,on
和off
方法。
Object.create(null)
创建一个空的hub
对象。emit
,根据event
参数解析处理程序数组,然后.forEach()
通过传入数据作为参数来运行每个处理程序。on
,为事件创建一个数组(若不存在则为空数组),然后.push()
将处理程序添加到该数组。off
,用.findIndex()
在事件数组中查找处理程序的索引,并使用.splice()
删除。const createEventHub = () => ({
hub: Object.create(null),
emit(event, data) {
(this.hub[event] || []).forEach(handler => handler(data));
},
on(event, handler) {
if (!this.hub[event]) this.hub[event] = [];
this.hub[event].push(handler);
},
off(event, handler) {
const i = (this.hub[event] || []).findIndex(h => h === handler);
if (i > -1) this.hub[event].splice(i, 1);
if (this.hub[event].length === 0) delete this.hub[event];
}
});
用法:
const handler = data => console.log(data);
const hub = createEventHub();
let increment = 0;
// 订阅,监听不同事件
hub.on('message', handler);
hub.on('message', () => console.log('Message event fired'));
hub.on('increment', () => increment++);
// 发布:发出事件以调用所有订阅给它们的处理程序,并将数据作为参数传递给它们
hub.emit('message', 'hello world'); // 打印 'hello world' 和 'Message event fired'
hub.emit('message', { hello: 'world' }); // 打印 对象 和 'Message event fired'
hub.emit('increment'); // increment = 1
// 停止订阅
hub.off('message', handler);
memoize
:缓存函数通过实例化一个Map
对象来创建一个空的缓存。
通过检查输入值的函数输出是否已缓存,返回存储一个参数的函数,该参数将被提供给已记忆的函数;如果没有,则存储并返回它。
const memoize = fn => {
const cache = new Map();
const cached = function(val) {
return cache.has(val) ? cache.get(val) : cache.set(val, fn.call(this, val)) && cache.get(val);
};
cached.cache = cache;
return cached;
};
Ps: 这个版本可能不是很清晰,还有Vue源码版的:
/**
* Create a cached version of a pure function.
*/
export function cached<F: Function> (fn: F): F {
const cache = Object.create(null)
return (function cachedFn (str: string) {
const hit = cache[str]
return hit || (cache[str] = fn(str))
}: any)
}
once
:只调用一次的函数const once = fn => {
let called = false
return function () {
if (!called) {
called = true
fn.apply(this, arguments)
}
}
};
flattenObject
:以键的路径扁平化对象使用递归。
Object.keys(obj)
联合Array.prototype.reduce()
,以每片叶子节点转换为扁平的路径节点。prefix
以创建路径Object.assign()
。prefix
除非你希望每个键都有一个前缀,否则应始终省略第二个参数。const flattenObject = (obj, prefix = '') =>
Object.keys(obj).reduce((acc, k) => {
const pre = prefix.length ? prefix + '.' : '';
if (typeof obj[k] === 'object') Object.assign(acc, flattenObject(obj[k], pre + k));
else acc[pre + k] = obj[k];
return acc;
}, {});
flattenObject({ a: { b: { c: 1 } }, d: 1 }); // { 'a.b.c': 1, d: 1 }
unflattenObject
:以键的路径展开对象与上面的相反,展开对象。
const unflattenObject = obj =>
Object.keys(obj).reduce((acc, k) => {
if (k.indexOf('.') !== -1) {
const keys = k.split('.');
Object.assign(
acc,
JSON.parse(
'{' +
keys.map((v, i) => (i !== keys.length - 1 ? `"${v}":{` : `"${v}":`)).join('') +
obj[k] +
'}'.repeat(keys.length)
)
);
} else acc[k] = obj[k];
return acc;
}, {});
unflattenObject({ 'a.b.c': 1, d: 1 }); // { a: { b: { c: 1 } }, d: 1 }
这个的用途,在做Tree
组件或复杂表单时取值非常舒服。
byteSize
:返回字符串的字节长度const byteSize = str => new Blob([str]).size;
byteSize('😀'); // 4
byteSize('Hello World'); // 11
capitalize
:首字母大写const capitalize = ([first, ...rest]) =>
first.toUpperCase() + rest.join('');
capitalize('fooBar'); // 'FooBar'
capitalize('fooBar', true); // 'Foobar'
capitalizeEveryWord
:每个单词首字母大写const capitalizeEveryWord = str => str.replace(/\b[a-z]/g, char => char.toUpperCase());
capitalizeEveryWord('hello world!'); // 'Hello World!'
decapitalize
:首字母小写const decapitalize = ([first, ...rest]) =>
first.toLowerCase() + rest.join('')
decapitalize('FooBar'); // 'fooBar'
decapitalize('FooBar'); // 'fooBar'
luhnCheck
:银行卡号码校验(luhn
算法)Luhn
算法的实现,用于验证各种标识号,例如信用卡号,IMEI号,国家提供商标识号等。
与String.prototype.split('')
结合使用,以获取数字数组。获得最后一个数字。实施luhn
算法。如果被整除,则返回,否则返回。
const luhnCheck = num => {
let arr = (num + '')
.split('')
.reverse()
.map(x => parseInt(x));
let lastDigit = arr.splice(0, 1)[0];
let sum = arr.reduce((acc, val, i) => (i % 2 !== 0 ? acc + val : acc + ((val * 2) % 9) || 9), 0);
sum += lastDigit;
return sum % 10 === 0;
};
用例:
luhnCheck('4485275742308327'); // true
luhnCheck(6011329933655299); // false
luhnCheck(123456789); // false
关于luhn
算法,可以参考以下文章:
银行卡号码的校验采用Luhn
算法,校验过程大致如下:
从右到左给卡号字符串编号,最右边第一位是1,最右边第二位是2,最右边第三位是3….
从右向左遍历,对每一位字符t执行第三个步骤,并将每一位的计算结果相加得到一个数s。
对每一位的计算规则:如果这一位是奇数位,则返回t本身,如果是偶数位,则先将t乘以2得到一个数n,如果n是一位数(小于10),直接返回n,否则将n的个位数和十位数相加返回。
如果s能够整除10,则此号码有效,否则号码无效。
因为最终的结果会对10取余来判断是否能够整除10,所以又叫做模10算法。
当然,还是库比较香: bankcardinfo
splitLines
:将多行字符串拆分为行数组。使用String.prototype.split()
和正则表达式匹配换行符并创建一个数组。
const splitLines = str => str.split(/\r?\n/);
splitLines('This\nis a\nmultiline\nstring.\n'); // ['This', 'is a', 'multiline', 'string.' , '']
stripHTMLTags
:删除字符串中的HTMl
标签从字符串中删除HTML / XML
标签。
使用正则表达式从字符串中删除HTML / XML
标记。
const stripHTMLTags = str => str.replace(/<[^>]*>/g, '');
stripHTMLTags('<p><em>lorem</em> <strong>ipsum</strong></p>'); // 'lorem ipsum'
dayOfYear
:当前日期天数const dayOfYear = date =>
Math.floor((date - new Date(date.getFullYear(), 0, 0)) / 1000 / 60 / 60 / 24);
dayOfYear(new Date()); // 285
forOwn
:迭代属性并执行回调const forOwn = (obj, fn) => Object.keys(obj).forEach(key => fn(obj[key], key, obj));
forOwn({ foo: 'bar', a: 1 }, v => console.log(v)); // 'bar', 1
Get Time From Date
:返回当前24小时制时间的字符串const getColonTimeFromDate = date => date.toTimeString().slice(0, 8);
getColonTimeFromDate(new Date()); // "08:38:00"
Get Days Between Dates
:返回日期间的天数const getDaysDiffBetweenDates = (dateInitial, dateFinal) =>
(dateFinal - dateInitial) / (1000 * 3600 * 24);
getDaysDiffBetweenDates(new Date('2019-01-01'), new Date('2019-10-14')); // 286
is
:检查值是否为特定类型。const is = (type, val) => ![, null].includes(val) && val.constructor === type;
is(Array, [1]); // true
is(ArrayBuffer, new ArrayBuffer()); // true
is(Map, new Map()); // true
is(RegExp, /./g); // true
is(Set, new Set()); // true
is(WeakMap, new WeakMap()); // true
is(WeakSet, new WeakSet()); // true
is(String, ''); // true
is(String, new String('')); // true
is(Number, 1); // true
is(Number, new Number(1)); // true
is(Boolean, true); // true
is(Boolean, new Boolean(true)); // true
isAfterDate
:检查是否在某日期后const isAfterDate = (dateA, dateB) => dateA > dateB;
isAfterDate(new Date(2010, 10, 21), new Date(2010, 10, 20)); // true
isBeforeDate
:检查是否在某日期前const isBeforeDate = (dateA, dateB) => dateA < dateB;
isBeforeDate(new Date(2010, 10, 20), new Date(2010, 10, 21)); // true
tomorrow
:获取明天的字符串格式时间
const tomorrow = () => {
let t = new Date();
t.setDate(t.getDate() + 1);
return t.toISOString().split('T')[0];
};
tomorrow(); // 2019-10-15 (如果明天是2019-10-15)
equals
:全等判断在两个变量之间进行深度比较以确定它们是否全等。
此代码段精简的核心在于Array.prototype.every()
的使用。
const equals = (a, b) => {
if (a === b) return true;
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
if (!a || !b || (typeof a !== 'object' && typeof b !== 'object')) return a === b;
if (a.prototype !== b.prototype) return false;
let keys = Object.keys(a);
if (keys.length !== Object.keys(b).length) return false;
return keys.every(k => equals(a[k], b[k]));
};
用法:
equals({ a: [2, { e: 3 }], b: [4], c: 'foo' }, { a: [2, { e: 3 }], b: [4], c: 'foo' }); // true
randomIntegerInRange
:生成指定范围的随机整数const randomIntegerInRange = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
randomIntegerInRange(0, 5); // 3
randomNumberInRange
:生成指定范围的随机小数const randomNumberInRange = (min, max) => Math.random() * (max - min) + min;
randomNumberInRange(2, 10); // 6.0211363285087005
round
:四舍五入到指定位数const round = (n, decimals = 0) => Number(`${Math.round(`${n}e${decimals}`)}e-${decimals}`);
round(1.005, 2); // 1.01
sum
:计算数组或多个数字的总和
const sum = (...arr) => [...arr].reduce((acc, val) => acc + val, 0);
sum(1, 2, 3, 4); // 10
sum(...[1, 2, 3, 4]); // 10
toCurrency
:简单的货币单位转换const toCurrency = (n, curr, LanguageFormat = undefined) =>
Intl.NumberFormat(LanguageFormat, { style: 'currency', currency: curr }).format(n);
toCurrency(123456.789, 'EUR'); // €123,456.79
toCurrency(123456.789, 'USD', 'en-us'); // $123,456.79
toCurrency(123456.789, 'USD', 'fa'); // ۱۲۳٬۴۵۶٫۷۹
toCurrency(322342436423.2435, 'JPY'); // ¥322,342,436,423
bottomVisible
:检查页面底部是否可见const bottomVisible = () =>
document.documentElement.clientHeight + window.scrollY >=
(document.documentElement.scrollHeight || document.documentElement.clientHeight);
bottomVisible(); // true
Create Directory
:检查创建目录此代码段调用fs
模块的existsSync()
检查目录是否存在,如果不存在,则mkdirSync()
创建该目录。
const fs = require('fs');
const createDirIfNotExists = dir => (!fs.existsSync(dir) ? fs.mkdirSync(dir) : undefined);
createDirIfNotExists('test');
currentURL
:返回当前链接url
const currentURL = () => window.location.href;
currentURL(); // 'https://juejin.im'
distance
:返回两点间的距离该代码段通过计算欧几里得距离来返回两点之间的距离。
const distance = (x0, y0, x1, y1) => Math.hypot(x1 - x0, y1 - y0);
distance(1, 1, 2, 3); // 2.23606797749979
elementContains
:检查是否包含子元素此代码段检查父元素是否包含子元素。
const elementContains = (parent, child) => parent !== child && parent.contains(child);
elementContains(document.querySelector('head'), document.querySelector('title')); // true
elementContains(document.querySelector('body'), document.querySelector('body')); // false
getStyle
:返回指定元素的生效样式const getStyle = (el, ruleName) => getComputedStyle(el)[ruleName];
getStyle(document.querySelector('p'), 'font-size'); // '16px'
getType
:返回值或变量的类型名const getType = v =>
v === undefined ? 'undefined' : v === null ? 'null' : v.constructor.name.toLowerCase();
getType(new Set([1, 2, 3])); // 'set'
getType([1, 2, 3]); // 'array'
hasClass
:校验指定元素的类名const hasClass = (el, className) => el.classList.contains(className);
hasClass(document.querySelector('p.special'), 'special'); // true
hide
:隐藏所有的指定标签const hide = (...el) => [...el].forEach(e => (e.style.display = 'none'));
hide(document.querySelectorAll('img')); // 隐藏所有<img>标签
httpsRedirect
:HTTP
跳转 HTTPS
const httpsRedirect = () => {
if (location.protocol !== 'https:') location.replace('https://' + location.href.split('//')[1]);
};
httpsRedirect(); // 若在`http://www.baidu.com`, 则跳转到`https://www.baidu.com`
insertAfter
:在指定元素之后插入新元素const insertAfter = (el, htmlString) => el.insertAdjacentHTML('afterend', htmlString);
// <div id="myId">...</div> <p>after</p>
insertAfter(document.getElementById('myId'), '<p>after</p>');
insertBefore
:在指定元素之前插入新元素const insertBefore = (el, htmlString) => el.insertAdjacentHTML('beforebegin', htmlString);
insertBefore(document.getElementById('myId'), '<p>before</p>'); // <p>before</p> <div id="myId">...</div>
isBrowser
:检查是否为浏览器环境此代码段可用于确定当前运行时环境是否为浏览器。这有助于避免在服务器(节点)上运行前端模块时出错。
const isBrowser = () => ![typeof window, typeof document].includes('undefined');
isBrowser(); // true (browser)
isBrowser(); // false (Node)
isBrowserTab
:检查当前标签页是否活动const isBrowserTabFocused = () => !document.hidden;
isBrowserTabFocused(); // true
nodeListToArray
:转换nodeList
为数组const nodeListToArray = nodeList => [...nodeList];
nodeListToArray(document.childNodes); // [ <!DOCTYPE html>, html ]
Random Hexadecimal Color Code
:随机十六进制颜色
const randomHexColorCode = () => {
let n = (Math.random() * 0xfffff * 1000000).toString(16);
return '#' + n.slice(0, 6);
};
randomHexColorCode(); // "#e34155"
scrollToTop
:平滑滚动至顶部该代码段可用于平滑滚动到当前页面的顶部。
const scrollToTop = () => {
const c = document.documentElement.scrollTop || document.body.scrollTop;
if (c > 0) {
window.requestAnimationFrame(scrollToTop);
window.scrollTo(0, c - c / 8);
}
};
scrollToTop();
smoothScroll
:滚动到指定元素区域该代码段可将指定元素平滑滚动到浏览器窗口的可见区域。
const smoothScroll = element =>
document.querySelector(element).scrollIntoView({
behavior: 'smooth'
});
smoothScroll('#fooBar');
smoothScroll('.fooBar');
detectDeviceType
:检测移动/PC设备const detectDeviceType = () =>
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
? 'Mobile'
: 'Desktop';
getScrollPosition
:返回当前的滚动位置默认参数为window
,pageXOffset(pageYOffset)
为第一选择,没有则用scrollLeft(scrollTop)
const getScrollPosition = (el = window) => ({
x: el.pageXOffset !== undefined ? el.pageXOffset : el.scrollLeft,
y: el.pageYOffset !== undefined ? el.pageYOffset : el.scrollTop
});
getScrollPosition(); // {x: 0, y: 200}
size
:获取不同类型变量的长度这个的实现非常巧妙,利用Blob
类文件对象的特性,获取对象的长度。
另外,多重三元运算符,是真香。
const size = val =>
Array.isArray(val)
? val.length
: val && typeof val === 'object'
? val.size || val.length || Object.keys(val).length
: typeof val === 'string'
? new Blob([val]).size
: 0;
size([1, 2, 3, 4, 5]); // 5
size('size'); // 4
size({ one: 1, two: 2, three: 3 }); // 3
escapeHTML
:转义HTML
当然是用来防XSS
攻击啦。
const escapeHTML = str =>
str.replace(
/[&<>'"]/g,
tag =>
({
'&': '&',
'<': '<',
'>': '>',
"'": ''',
'"': '"'
}[tag] || tag)
);
escapeHTML('<a href="#">Me & you</a>'); // '<a href="#">Me & you</a>'
如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:
前段时间在研究前端异常监控/埋点平台的实现。
在思考方案时,想到了浏览器自带的观察者以及页面生命周期API 。
于是在翻查资料时意外发现,原来现代浏览器支持多达四种不同类型的观察者:
Intersection Observer
,交叉观察者。Mutation Observer
,变动观察者。Resize Observer
,视图观察者。Performance Observer
,性能观察者IntersectionObserver | MutationObserver | ResizeObserver | PerformanceObserver | |
---|---|---|---|---|
用途 | 观察一个元素是否在视窗可见 | 观察DOM中的变化 | 观察视口大小的变化 | 监测性能度量事件 |
方法 | observe() disconnect() takeRecords() |
observe() disconnect() takeRecords() unobserve() |
observe() disconnect() unobserve() |
observe() disconnect() takeRecords() |
取代 | Dom Mutation events | getBoundingClientRect() 返回元素的大小及其相对于可视窗口的位置 Scroll 和 Resize 事件 |
Resize 事件 | Performance 接口 |
用途 | 1. 无限滚动 2. 图片懒加载 3. 兴趣埋点 4. 控制动画/视频执行(性能优化) |
1. 更高性能的数据绑定及响应 2. 实现视觉差滚动 3. 图片预加载 4. 实现富文本编辑器 |
1. 更智能的响应式布局(取代@media) 2. 响应式组件 |
1. 更细颗粒的性能监控 2. 分析性能对业务的影响(交互快/慢是否会影响销量) |
IntersectionObserver
:交叉观察者
IntersectionObserver
接口,提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport
)交叉状态的方法,祖先元素与视窗(viewport)
被称为根(root
)
想要计算Web页面的元素的位置,非常依赖于DOM
状态的显式查询。但这些查询是同步的,会导致昂贵的样式计算开销(重绘和回流),且不停轮询会导致大量的性能浪费。
scroll
等事件或通过插件的形式,计算真实元素可见性。而它们都有几项共同特点:
web
平台支持匮乏,各有各家的处理。需要开发人员消耗大量精力兼容。IntersectionObserver
的优势Intersection Observer API
通过为开发人员提供一种新方法来异步查询元素相对于其他元素或全局视口的位置,从而解决了上述问题:
DOM
和样式查询,连续轮询以及使用自定义插件的需求。CPU
,GPU
和资源成本。IntersectionObserver
基本使用使用IntersectionObserver API
主要需要三个步骤:
const options = {
root: document.querySelector('.scrollContainer'),
rootMargin: '0px',
threshold: [0.3, 0.5, 0.8, 1] }
const observer = new IntersectionObserver(handler, options)
这几个参数用大白话解释就是:
root
:指定一个根元素rootMargin
:使用类似于设置CSS边距的语法来指定根边距(根元素的观察影响范围)threshold
:阈值,可以为数组。[0.3]
意味着,当目标元素在根元素指定的元素内可见30%时,调用处理函数。当目标元素与根元素通过阈值相交时,就会触发回调函数。
function handler (entries, observer) {
entries.forEach(entry => {
// 每个成员都是一个IntersectionObserverEntry对象。
// 举例来说,如果同时有两个被观察的对象的可见性发生变化,entries数组就会有两个成员。
// entry.boundingClientRect
// entry.intersectionRatio
// entry.intersectionRect
// entry.isIntersecting
// entry.rootBounds
// entry.target
// entry.time
});
}
任何目标元素都可以通过调用.observer(target)
方法来观察。
const target = document.querySelector(“.targetBox”);
observer.observe(target);
此外,还有两个方法:
停止对某目标的监听
observer.unobserve(target)
终止对所有目标的监听
observer.disconnect()
HTML
:
<img src="placeholder.png" data-src="img-1.jpg">
<img src="placeholder.png" data-src="img-2.jpg">
<img src="placeholder.png" data-src="img-3.jpg">
<!-- more images -->
脚本:
let observer = new IntersectionObserver(
(entries, observer) => {
entries.forEach(entry => {
/* 替换属性 */
entry.target.src = entry.target.dataset.src;
observer.unobserve(entry.target);
});
},
{rootMargin: "0px 0px -200px 0px"});
document.querySelectorAll('img').forEach(img => { observer.observe(img) });
上述例子表示 仅在到达视口距离底部200px视加载图片。
关于兴趣埋点,一个比较通用的方案是:
const boxList = [...document.querySelectorAll('.box')]
var io = new IntersectionObserver((entries) =>{
entries.forEach(item => {
// intersectionRatio === 1说明该元素完全暴露出来,符合业务需求
if (item.intersectionRatio === 1) {
// 。。。 埋点曝光代码
io.unobserve(item.target)
}
})
}, {
root: null,
threshold: 1, // 阀值设为1,当只有比例达到1时才触发回调函数
})
// observe遍历监听所有box节点
boxList.forEach(box => io.observe(box))
至于怎样评断用户是否感兴趣,记录方式就见仁见智了:
PC
端记录鼠标点击次数/悬停时间,移动端记录touch
事件这里就不展开写了(我懒)。
这里提供控制视频的版本
HTML
:
<video src="OSRO-animation.mp4" controls=""></video>
js
:
let video = document.querySelector('video');
let isPaused = false; /* Flag for auto-paused video */
let observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if(entry.intersectionRatio!=1 && !video.paused){
video.pause(); isPaused = true;
}
else if(isPaused) {video.play(); isPaused=false}
});
}, {threshold: 1});
observer.observe(video);
效果:
Mutation Observer
:变动观察者接口提供了监视对
DOM
树所做更改的能力。它被设计为旧的MutationEvents
功能的替代品,该功能是DOM3 Events
规范的一部分。
MDN
中也写到了,是被DOM Event
承认在API上有缺陷,反对使用。DOM
添加 mutation
监听器极度降低进一步修改DOM
文档的性能(慢1.5 - 7倍),此外, 移除监听器不会逆转的损害。MutationEvents
的原理:通过绑定事件监听DOM
乍一看到感觉很正常,那列一下相关监听的事件:
DOMAttributeNameChanged
DOMCharacterDataModified
DOMElementNameChanged
DOMNodeInserted
DOMNodeInsertedIntoDocument
DOMNodeRemoved
DOMNodeRemovedFromDocument
DOMSubtreeModified
甭记,这么多事件,各内核各版本浏览器想兼容怕是要天荒地老。
MutationObserver
的优势而Mutation Observer
的优势在于:
MutationEvents
事件是同步触发,也就是说,DOM
的变动立刻会触发相应的事件;Mutation Observer
则是异步触发,DOM
的变动并不会马上触发,而是要等到当前所有 DOM
操作都结束才触发。DOM
下子元素的变更记录简单讲:异步**
!
MutationObserver
基本使用使用MutationObserver API
主要需要三个步骤:
let observer = new MutationObserver(callback);
上面代码中的回调函数,会在每次 DOM 变动后调用。该回调函数接受两个参数,第一个是变动数组,第二个是观察器实例,下面是一个例子:
function callback (mutations, observer) {
mutations.forEach(function(mutation) {
console.log(mutation);
});
});
其中每个mutation
都对应一个MutationRecord
对象,记录着DOM
每次发生变化的变动记录
MutationRecord
对象包含了DOM的相关信息,有如下属性:
属性 | 意义 |
---|---|
type |
观察的变动类型(attribute 、characterData 或者childList ) |
target |
发生变动的DOM 节点 |
addedNodes |
新增的DOM 节点 |
removedNodes |
删除的DOM 节点 |
previousSibling |
前一个同级节点,如果没有则返回null |
nextSibling |
下一个同级节点,如果没有则返回null |
attributeName |
发生变动的属性。如果设置了attributeFilter ,则只返回预先指定的属性 |
oldValue |
变动前的值。这个属性只对attribute 和characterData 变动有效,如果发生childList 变动,则返回null |
MutationObserver.observe(dom, options)
启动监听,接收两个参数。
DOM
节点。options
。mutationObserver.observe(content, {
attributes: true, // Boolean - 观察目标属性的改变
characterData: true, // Boolean - 观察目标数据的改变(改变前的数据/值)
childList: true, // Boolean - 观察目标子节点的变化,比如添加或者删除目标子节点,不包括修改子节点以及子节点后代的变化
subtree: true, // Boolean - 目标以及目标的后代改变都会观察
attributeOldValue: true, // Boolean - 表示需要记录改变前的目标属性值
characterDataOldValue: true, // Boolean - 设置了characterDataOldValue可以省略characterData设置
// attributeFilter: ['src', 'class'] // Array - 观察指定属性
});
优先级 :
attributeFilter/attributeOldValue
> attributes
characterDataOldValue
> characterData
attributes/characterData/childList
(或更高级特定项)至少有一项为true;true
此外,还有两个方法:
停止观察。调用后不再触发观察器,解除订阅
MutationObserver.disconnect()
清除变动记录。即不再处理未处理的变动。该方法返回变动记录的数组,注意,该方法立即生效。
MutationObserver.takeRecords()
MutationObserver
监听文本变化基本使用是:
const target = document.getElementById('target-id')
const observer = new MutationObserver(records => {
// 输入变更记录
})
// 开始观察
observer.observe(target, {
characterData: true
})
这里可以有几种处理。
#
”号时,启动搜索框预检文本或高亮话题。有个Vue
的小型插件就是这么实现的:
这个实现也是秀得飞起:
游戏的逻辑很简单,当中间的色块颜色改变时,在时间限制内于底下的选项选择跟它颜色一样的选项就得分。难的点在于越后面的关卡选项越多,而且选项颜色也越相近,例如:
其实原理非常简单,就是观察色块的backgroundColor
(属性变化attributes
),然后触发点击事件e.click()
。
var targetNode = document.querySelector('#kolor-kolor');
var config = { attributes: true };
var callback = function(mutationsList, observer) {
if (mutationsList[0].type == 'attributes') {
console.log('attribute change!');
let ans = document.querySelector('#kolor-kolor').style.backgroundColor;
document.querySelectorAll('#kolor-options a').forEach( (e) => {
if (e.style.backgroundColor == ans) {
e.text = 'Ans!';
e.click()
}
})
}
};
var observer = new MutationObserver(callback);
observer.observe(targetNode, config);
ResizeObserver
,视图观察者ResizeObserver API
是一个新的JavaScript API
,与IntersectionObserver API
非常相似,它们都允许我们去监听某个元素的变化。
开发过程当中经常遇到的一个问题就是如何监听一个 div
的尺寸变化。
但众所周知,为了监听 div
的尺寸变化,都将侦听器附加到 window
中的 resize
事件。
但这很容易导致性能问题,因为大量的触发事件。
换句话说,使用
window.resize
通常是浪费的,因为它告诉我们每个视窗大小的变化,而不仅仅是当一个元素的大小发生变化。
而且resize
事件会在一秒内触发将近60次,很容易在改变窗口大小时导致性能问题
比如说,你要调整一个元素的大小,那就需要在 resize
的回调函数 callback()
中调用 getBoundingClientRect
或 getComputerStyle
。不过你要是不小心处理所有的读和写操作,就会导致布局混乱。比如下面这个小示例:
ResizeObserver
的优势ResizeObserver API
的核心优势有两点:
DOM
元素观察,而不是window
ResizeObserver
基本使用使用ResizeObserver API
同样也是三个步骤:
let observer = new ResizeObserver(callback);
const callback = entries => {
entries.forEach(entry => {
})
}
每一个entry
都是一个对象,包含两个属性contentRect
和target
contentRect
都是一些位置信息:
属性 | 作用 |
---|---|
bottom |
top + height 的值 |
height |
元素本身的高度,不包含padding ,border 值 |
left |
padding-left 的值 |
right |
left + width 的值 |
top |
padidng-top 的值 |
width |
元素本身的宽度,不包含padding ,border 值 |
x |
大小与top 相同 |
y |
大小与left 相同 |
observer.observe(document.body)
unobserve
方法:取消单节点观察
observer.unobserve(document.body)
disconnect
方法:取消所有节点观察
observer.disconnect(document.body)
html
:
<div class="box">
<h3 class="info"></h3>
</div>
<div class="box small">
<h3 class="info"></h3>
</div>
添加点样式:
body {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
padding: 2vw;
box-sizing: border-box;
}
.box {
text-align: center;
height: 20vh;
border-radius: 8px;
box-shadow: 0 0 4px rgba(0,0,0,.25);
display: flex;
justify-content: center;
align-items: center;
padding: 1vw
}
.box h3 {
color: #fff;
margin: 0;
font-size: 5vmin;
text-shadow: 0 0 10px rgba(0,0,0,0.4);
}
.box.small {
max-width: 550px;
margin: 1rem auto;
}
JavaScript
代码:
const boxes = document.querySelectorAll('.box');
let callbackFired = 0;
const myObserver = new ResizeObserver(entries => {
for (let entry of entries) {
callbackFired++
const infoEl = entry.target.querySelector('.info');
const width = Math.floor(entry.contentRect.width);
const height = Math.floor(entry.contentRect.height);
const angle = Math.floor(width / 360 * 100);
const gradient = `linear-gradient(${ angle }deg, rgba(0,143,104,1) 50%, rgba(250,224,66,1) 50%)`;
entry.target.style.background = gradient;
infoEl.innerText = `
I'm ${ width }px and ${ height }px tall
Callback fired: ${callbackFired}
`;
}
});
boxes.forEach(box => {
myObserver.observe(box);
});
Vue
组件简单的@media
就可以实现:
@media only screen and (max-width: 576px) {
.post__item {
flex-direction: column;
}
.post__image {
flex: 0 auto;
height: auto;
}
}
@media
查询的最大问题是:
以下是指令版实现:
效果:
这是vue-responsive-components
库的具体实现代码,还有组件形式的实现,感兴趣的可以去看看。
PerformanceObserver
:性能观察者这是一个浏览器和Node.js
里都存在的API,采用相同W3C
的Performance Timeline
规范
window.performance
和 window.PerformanceObserver
。Node.js
程序中需要perf_hooks
取得性能对象,如下:
const { PerformanceObserver, performance } = require('perf_hooks');
首先来看Performance
接口:
可以获取到当前页面中与性能相关的信息。它是 High Resolution Time API
的一部分,同时也融合了 Performance Timeline API
、Navigation Timing AP
、 User Timing API
和 Resource Timing API
。
Performance API
是大家熟悉的一个接口,他记录着几种性能指数的庞大对象集合。
performance.getEntries
或者performance.getEntriesByName
来获得。performance.now
来计算。为了解决上述的问题,在Performance Timeline Level 2
中,除了扩展了Performance
的基本定义以外,还增加了PerformanceObserver
接口。
PerformanceObserver
的优势PerformanceObserver
是浏览器内部对Performance
实现的观察者模式,也是现代浏览器支持的几个 Observer
之一。
它解决了以下3点问题:
timeline
获取记录。W3C
官网文档鼓励开发人员尽可能使用PerformanceObserver
,而不是通过Performance
获取性能参数及指标。
PerformanceObserver
的使用使用PerformanceObserver API
主要需要三个步骤:
let observer = new PerformanceObserver(callback);
const callback = (list, observer) => {
const entries = list.getEntries();
entries.forEach((entry) => {
console.log(“Name: “ + entry.name + “, Type: “ + entry.entryType + “, Start: “ + entry.startTime + “, Duration: “ + entry.duration + “\n”); });
}
其中每一个list
都是一个完整的PerformanceObserverEntryList
对象:
包含三个方法getEntries
、getEntriesByType
、getEntriesByName
:
方法 | 作用 |
---|---|
getEntries() | 返回一个列表,该列表包含一些用于承载各种性能数据的对象,不做任何过滤 |
getEntriesByType() | 返回一个列表,该列表包含一些用于承载各种性能数据的对象,按类型过滤 |
getEntriesByName() | 返回一个列表,,该列表包含一些用于承载各种性能数据的对象,按名称过滤 |
observer.observe({entryTypes: ["entryTypes"]});
observer.observe(...)
方法接受可以观察到的有效的入口类型。这些输入类型可能属于各种性能API,比如User tming
或Navigation Timing API
。有效的entryType
值:
属性 | 别名 | 类型 | 描述 |
---|---|---|---|
frame, navigation | PerformanceFrameTiming, PerformanceNavigationTiming | URL | 文件的地址。 |
resource | PerformanceResourceTiming | URL | 所请求资源的解析URL。 |
mark | PerformanceMark | DOMString | 通过调用创建标记时使用的名称performance.mark()。 |
measure | PerformanceMeasure | DOMString | 通过调用创建度量时使用的名称performance.measure()。 |
paint | PerformancePaintTiming | DOMString | 无论是'first-paint'或'first-contentful-paint'。 |
longtask | PerformanceLongTaskTiming | DOMString | 报告长任务的实例 |
function filterTime(a, b) {
return (a > 0 && b > 0 && (a - b) >= 0) ? (a - b) : undefined;
}
let resolvePerformanceTiming = (timing) => {
let o = {
initiatorType: timing.initiatorType,
name: timing.name,
duration: parseInt(timing.duration),
redirect: filterTime(timing.redirectEnd, timing.redirectStart), // 重定向
dns: filterTime(timing.domainLookupEnd, timing.domainLookupStart), // DNS解析
connect: filterTime(timing.connectEnd, timing.connectStart), // TCP建连
network: filterTime(timing.connectEnd, timing.startTime), // 网络总耗时
send: filterTime(timing.responseStart, timing.requestStart), // 发送开始到接受第一个返回
receive: filterTime(timing.responseEnd, timing.responseStart), // 接收总时间
request: filterTime(timing.responseEnd, timing.requestStart), // 总时间
ttfb: filterTime(timing.responseStart, timing.requestStart), // 首字节时间
};
return o;
};
let resolveEntries = (entries) => entries.map(item => resolvePerformanceTiming(item));
let resources = {
init: (cb) => {
let performance = window.performance || window.mozPerformance || window.msPerformance || window.webkitPerformance;
if (!performance || !performance.getEntries) {
return void 0;
}
if (window.PerformanceObserver) {
let observer = new window.PerformanceObserver((list) => {
try {
let entries = list.getEntries();
cb(resolveEntries(entries));
} catch (e) {
console.error(e);
}
});
observer.observe({
entryTypes: ['resource']
})
} else {
window.addEventListener('load', () => {
let entries = performance.getEntriesByType('resource');
cb(resolveEntries(entries));
});
}
},
};
参考文章有点多:
- 资源监控
- Media Queries Based on Element Width with MutationObserver
- 以用户为中心的性能指标
- A Few Functional Uses for Intersection Observer to Know When an Element is in View
- Getting To Know The MutationObserver API
- Different Types Of Observers Supported By Modern Browsers
- THE RESIZE OBSERVER EXPLAINED
- A Look at the Resize Observer JavaScript API
这四个观察者,都非常适合集成到监控系统。
且都有对应的Polyfills
版实现。
网上的总结和文档都深浅不一,如果哪里有错误,欢迎指正。
如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:
也可以来我的GitHub
博客里拿所有文章的源文件:
roughViz.js
基于D3(v5)
, roughjs
, 和handy
。
有三种衡量方式:
简答CDN
:
<script src="https://unpkg.com/[email protected]"></script>
npm
:
npm install rough-viz
react/vue
:
npm install react-roughviz
npm install vue-roughviz
甚至在Python
中也可以:
pip install roughviz
具体用法请参照官方文档:https://github.com/jwilber/roughViz
react-three-fiber
Web
和react-native
都可用的高性能Threejs for react
库。
可以在React
外部驱动渲染循环,而不会产生额外开销。
最新版本采用了Hooks
的写法,不像以往强行兼容的Threejs
,写起来更加友好。
不止抖音字体爆炸特效,它能实现什么,源于你的技术和想象力。
以下一部分特效:
如果有人学会了...大佬带带?
抖音爆炸特效的实现:
其中用到一个库:react-spring
,这是react
最优秀的动画库,没有之一。
Mini Music Player - VueJS
国外友人写的一个Vue.js
音乐播放器,好看的不得了。
其中的交互和逻辑,也是非常精炼。
源码:https://codepen.io/JavaScriptJunkie/pen/qBWrRyg
UI
都夸好的卡片验证库:interactive-paycard
这个11月Vue
新库一发布,就狂揽3k+
star,过于优秀。
完整库名vue-interactive-paycard
React
版的作者表示也即将发布了。
源码:https://github.com/muhammederdem/vue-interactive-paycard/issues
SandDance
微软出品,必属精品
SandDance
是使用Vega
进行图表布局,使用Deck.gl
进行WebGL
渲染。
能在如此密集的数据量上保持动画流畅和美观的,也就微软爸爸能做到了。
我先跪了,你们随意。
此外,该库还有多种使用方式:
本文项目基于Vue-Cli3,想知道如何正确搭建请看我之前的文章:
axios
二次封装这里封装的依据是后台传的JWT
,已封装好的请跳过。
import axios from 'axios'
import router from '../router'
import {MessageBox, Message} from 'element-ui'
let loginUrl = '/login'
// 根据环境切换接口地址
axios.defaults.baseURL = process.env.VUE_APP_API
axios.defaults.headers = {'X-Requested-With': 'XMLHttpRequest'}
axios.defaults.timeout = 60000
// 请求拦截器
axios.interceptors.request.use(
config => {
if (router.history.current.path !== loginUrl) {
let token = window.sessionStorage.getItem('token')
if (token == null) {
router.replace({path: loginUrl, query: {redirect: router.currentRoute.fullPath}})
return false
} else {
config.headers['Authorization'] = 'JWT ' + token
}
}
return config
}, error => {
Message.warning(error)
return Promise.reject(error)
})
紧接着的是响应拦截器(即异常处理)
axios.interceptors.response.use(
response => {
return response.data
}, error => {
if (error.response !== undefined) {
switch (error.response.status) {
case 400:
MessageBox.alert(error.response.data)
break
case 401:
if (window.sessionStorage.getItem('out') === null) {
window.sessionStorage.setItem('out', 1)
MessageBox.confirm('会话已失效! 请重新登录', '提示', {confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning'}).then(() => {
router.replace({path: loginUrl, query: {redirect: router.currentRoute.fullPath}})
}).catch(action => {
window.sessionStorage.clear()
window.localStorage.clear()
})
}
break
case 402:
MessageBox.confirm('登陆超时 !', '提示', {confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning'}).then(() => {
router.replace({path: loginUrl, query: {redirect: router.currentRoute.fullPath}})
})
break
case 403:
MessageBox.alert('没有权限!')
break
// ...忽略
default:
MessageBox.alert(`连接错误${error.response.status}`)
}
return Promise.resolve(error.response)
}
return Promise.resolve(error)
})
这里做的处理分别是会话已失效和登陆超时,具体的需要根据业务来作变更。
最后是导出基础请求类型封装。
export default {
get (url, param) {
if (param !== undefined) {
Object.assign(param, {_t: (new Date()).getTime()})
} else {
param = {_t: (new Date()).getTime()}
}
return axios({method: 'get', url, params: param})
},
// 不常更新的数据用这个
getData (url, param) {
return axios({method: 'get', url, params: param})
},
post (url, param, config) {
return axios.post(url, param, config)
},
put: axios.put,
_delete: axios.delete
}
其中给get
请求加上时间戳参数,避免从缓存中拿数据。
除了基础请求类型,还有很多类似下载、上传这种,需要特殊的的请求头,此时可以根据自身需求进行封装。
浏览器缓存是基于url进行缓存的,如果页面允许缓存,则在一定时间内(缓存时效时间前)再次访问相同的URL,浏览器就不会再次发送请求到服务器端,而是直接从缓存中获取指定资源。
import http from '@/utils/request'
export default {
A (param) { return http.get('/api/', param) },
B (param) { return http.post('/api/', param) }
C (param) { return http.put('/api/', param) },
D (param) { return http._delete('/api/', {data: param}) },
}
utils/api/index.js
:
import http from '@/utils/request'
import account from './account'
// 忽略...
const api = Object.assign({}, http, account, \*...其它模块*\)
export default api
在global.js
中引入:
import Vue from 'vue'
import api from './api/index'
// 略...
const errorHandler = (error, vm) => {
console.error(vm)
console.error(error)
}
Vue.config.errorHandler = errorHandler
export default {
install (Vue) {
// 添加组件
// 添加过滤器
})
// 全局报错处理
Vue.prototype.$throw = (error) => errorHandler(error, this)
Vue.prototype.$http = api
// 其它配置
}
}
写接口的时候就可以简化为:
async getData () {
const params = {/*...key : value...*/}
let res = await this.$http.A(params)
res.code === 4000 ? (this.aSata = res.data) : this.$message.warning(res.msg)
}
我们写组件的时候通常需要引入另外的组件:
<template>
<BaseInput v-model="searchText" @keydown.enter="search"/>
<BaseButton @click="search">
<BaseIcon name="search"/>
</BaseButton>
</template>
<script>
import BaseButton from './baseButton'
import BaseIcon from './baseIcon'
import BaseInput from './baseInput'
export default {
components: { BaseButton, BaseIcon, BaseInput }
}
</script>
写小项目这么引入还好,但等项目一臃肿起来...啧啧。
这里是借助webpack
,使用 require.context()
方法来创建自己的模块上下文,从而实现自动动态require
组件。
这个方法需要3个参数:
在你放基础组件的文件夹根目录下新建componentRegister.js
:
import Vue from 'vue'
/**
* 首字母大写
* @param str 字符串
* @example heheHaha
* @return {string} HeheHaha
*/
function capitalizeFirstLetter (str) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
/**
* 对符合'xx/xx.vue'组件格式的组件取组件名
* @param str fileName
* @example abc/bcd/def/basicTable.vue
* @return {string} BasicTable
*/
function validateFileName (str) {
return /^\S+\.vue$/.test(str) &&
str.replace(/^\S+\/(\w+)\.vue$/, (rs, $1) => capitalizeFirstLetter($1))
}
const requireComponent = require.context('./', true, /\.vue$/)
// 找到组件文件夹下以.vue命名的文件,如果文件名为index,那么取组件中的name作为注册的组件名
requireComponent.keys().forEach(filePath => {
const componentConfig = requireComponent(filePath)
const fileName = validateFileName(filePath)
const componentName = fileName.toLowerCase() === 'index'
? capitalizeFirstLetter(componentConfig.default.name)
: fileName
Vue.component(componentName, componentConfig.default || componentConfig)
})
最后我们在main.js
中
import 'components/componentRegister.js'
我们就可以随时随地使用这些基础组件,无需手动引入了。
我们写单页面应用,想看页面修改后性能变更其实挺繁琐的。有时想知道是「正优化」还是「负优化」只能靠手动刷新查看network
。而Hiper
很好解决了这一痛点(其实Hiper
是后台静默运行Chromium
来实现无感调试)。
我们开发完一个项目或者给一个项目做完性能优化以后,如何来衡量这个项目的性能是否达标?
我们的常见方式是在Dev Tool
中的performance
和network
中看数据,记录下几个关键的性能指标,然后刷新几次再看这些性能指标。
有时候我们发现,由于样本太少,受当前「网络」、「CPU」、「内存」的繁忙程度的影响很重,有时优化后的项目反而比优化前更慢。
如果有一个工具,一次性地请求N次网页,然后把各个性能指标取出来求平均值,我们就能非常准确地知道这个优化是「正优化」还是「负优化」。
并且,也可以做对比,拿到「具体优化了多少」的准确数据。这个工具就是为了解决这个痛点的。
sudo npm install hiper -g
# 或者使用 yarn:
# sudo yarn global add hiper
Key | Value |
---|---|
DNS查询耗时 | domainLookupEnd - domainLookupStart |
TCP连接耗时 | connectEnd - connectStart |
第一个Byte到达浏览器的用时 | responseStart - requestStart |
页面下载耗时 | responseEnd - responseStart |
DOM Ready之后又继续下载资源的耗时 | domComplete - domInteractive |
白屏时间 | domInteractive - navigationStart |
DOM Ready 耗时 | domContentLoadedEventEnd - navigationStart |
页面加载总耗时 | loadEventEnd - navigationStart |
https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceTiming
# 当我们省略协议头时,默认会在url前添加`https://`
# 最简单的用法
hiper baidu.com
# 如何url中含有任何参数,请使用双引号括起来
hiper "baidu.com?a=1&b=2"
# 加载指定页面100次
hiper -n 100 "baidu.com?a=1&b=2"
# 禁用缓存加载指定页面100次
hiper -n 100 "baidu.com?a=1&b=2" --no-cache
# 禁JavaScript加载指定页面100次
hiper -n 100 "baidu.com?a=1&b=2" --no-javascript
# 使用GUI形式加载指定页面100次
hiper -n 100 "baidu.com?a=1&b=2" -H false
# 使用指定useragent加载网页100次
hiper -n 100 "baidu.com?a=1&b=2" -u "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36"
此外,还可以配置Cookie
访问
module.exports = {
....
cookies: [{
name: 'token',
value: process.env.authtoken,
domain: 'example.com',
path: '/',
httpOnly: true
}],
....
}
# 载入上述配置文件(假设配置文件在/home/下)
hiper -c /home/config.json
# 或者你也可以使用js文件作为配置文件
hiper -c /home/config.js
我们常用的<transition>
和<keep-alive>
就是一个高阶(抽象)组件。
export default {
name: 'keep-alive',
abstract: true,
...
}
所有的高阶(抽象)组件是通过定义abstract
选项来声明的。高阶(抽象)组件不渲染真实DOM
。
一个常规的抽象组件是这么写的:
import { xxx } from 'xxx'
const A = () => {
.....
}
export default {
name: 'xxx',
abstract: true,
props: ['...', '...'],
// 生命周期钩子函数
created () {
....
},
....
destroyed () {
....
},
render() {
const vnode = this.$slots.default
....
return vnode
},
})
关于防抖和节流是啥就不赘述了。这里贴出组件代码:
改编自:Vue实现函数防抖组件
const throttle = function(fn, wait=50, isDebounce, ctx) {
let timer
let lastCall = 0
return function (...params) {
if (isDebounce) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(ctx, params)
}, wait)
} else {
const now = new Date().getTime()
if (now - lastCall < wait) return
lastCall = now
fn.apply(ctx, params)
}
}
}
export default {
name: 'Throttle',
abstract: true,
props: {
time: Number,
events: String,
isDebounce: {
type: Boolean,
default: false
},
},
created () {
this.eventKeys = this.events.split(',')
this.originMap = {}
this.throttledMap = {}
},
render() {
const vnode = this.$slots.default[0]
this.eventKeys.forEach((key) => {
const target = vnode.data.on[key]
if (target === this.originMap[key] && this.throttledMap[key]) {
vnode.data.on[key] = this.throttledMap[key]
} else if (target) {
this.originMap[key] = target
this.throttledMap[key] = throttle(target, this.time, this.isDebounce, vnode)
vnode.data.on[key] = this.throttledMap[key]
}
})
return vnode
},
})
通过第三个参数isDebounce
来控制切换防抖节流。
最后在main.js
里引用:
import Throttle from '../Throttle'
....
Vue.component('Throttle', Throttle)
<div id="app">
<Throttle :time="1000" events="click">
<button @click="onClick($event, 1)">click+1 {{val}}</button>
</Throttle>
<Throttle :time="1000" events="click" :isDebounce="true">
<button @click="onAdd">click+3 {{val}}</button>
</Throttle>
<Throttle :time="3300" events="mouseleave" :isDebounce="true">
<button @mouseleave.prevent="onAdd">click+3 {{val}}</button>
</Throttle>
</div>
const app = new Vue({
el: '#app',
data () {
return {
val: 0
}
},
methods: {
onClick ($ev, val) {
this.val += val
},
onAdd () {
this.val += 3
}
}
})
抽象组件是一个接替Mixin实现抽象组件公共功能的好方法,不会因为组件的使用而污染DOM(添加并不想要的div标签等)、可以包裹任意的单一子元素等等
至于用不用抽象组件,就见仁见智了。
**事件总线eventBus的实质就是创建一个vue实例,通过一个空的vue实例作为桥梁实现vue组件间的通信。它是实现非父子组件通信的一种解决方案。
而eventBus
实现也非常简单
import Vue from 'Vue'
export default new Vue
我们在使用中经常最容易忽视,又必然不能忘记的东西,那就是:清除事件总线eventBus
。
不手动清除,它是一直会存在,这样当前执行时,会反复进入到接受数据的组件内操作获取数据,原本只执行一次的获取的操作将会有多次操作。本来只会触发并只执行一次,变成了多次,这个问题就非常严重。
当不断进行操作几分钟后,页面就会卡顿,并占用大量内存。
所以一般在vue生命周期beforeDestroy
或者destroyed
中,需要用vue实例的$off
方法清除eventBus
beforeDestroy(){
bus.$off('click')
}
可当你有多个eventBus
时,就需要重复性劳动$off
销毁这件事儿。
这时候封装一个 eventBus
就是更优的解决方案。
eventBus
我们从Vue源码Vue.init
中可以得知:
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid vm实例唯一标识
vm._uid = uid++
// ....
}
每个Vue实例有自己的_uid
作为唯一标识,因此我们让EventBus
和_uid
关联起来,并将其改造:
class EventBus {
constructor (vue) {
if (!this.handles) {
Object.defineProperty(this, 'handles', {
value: {},
enumerable: false
})
}
this.Vue = vue
// _uid和EventName的映射
this.eventMapUid = {}
}
setEventMapUid (uid, eventName) {
if (!this.eventMapUid[uid]) this.eventMapUid[uid] = []
this.eventMapUid[uid].push(eventName) // 把每个_uid订阅的事件名字push到各自uid所属的数组里
}
$on (eventName, callback, vm) {
// vm是在组件内部使用时组件当前的this用于取_uid
if (!this.handles[eventName]) this.handles[eventName] = []
this.handles[eventName].push(callback)
if (vm instanceof this.Vue) this.setEventMapUid(vm._uid, eventName)
}
$emit () {
let args = [...arguments]
let eventName = args[0]
let params = args.slice(1)
if (this.handles[eventName]) {
let len = this.handles[eventName].length
for (let i = 0; i < len; i++) {
this.handles[eventName][i](...params)
}
}
}
$offVmEvent (uid) {
let currentEvents = this.eventMapUid[uid] || []
currentEvents.forEach(event => {
this.$off(event)
})
}
$off (eventName) {
delete this.handles[eventName]
}
}
// 写成Vue插件形式,直接引入然后Vue.use($EventBus)进行使用
let $EventBus = {}
$EventBus.install = (Vue, option) => {
Vue.prototype.$eventBus = new EventBus(Vue)
Vue.mixin({
beforeDestroy () {
// 拦截beforeDestroy钩子自动销毁自身所有订阅的事件
this.$eventBus.$offVmEvent(this._uid)
}
})
}
export default $EventBus
使用:
// main.js中
...
import EventBus from './eventBus.js'
Vue.use(EnemtBus)
...
组件中使用:
created () {
let text = Array(1000000).fill('xxx').join(',')
this.$eventBus.$on('home-on', (...args) => {
console.log('home $on====>>>', ...args)
this.text = text
}, this) // 注意第三个参数需要传当前组件的this,如果不传则需要手动销毁
},
mounted () {
setTimeout(() => {
this.$eventBus.$emit('home-on', '这是home $emit参数', 'ee')
}, 1000)
},
beforeDestroy () {
// 这里就不需要手动的off销毁eventBus订阅的事件了
}
uglifyjs
的Terser Plugin
在二月初项目升级Vue-cli3时遇到了一个问题:uglifyjs
不再支持webpack4.0。找了一圈,在Google
搜索里查到Terser Plugin
这个插件。
我主要用到了其中这几个功能:
cache
,启用文件缓存。parallel
,使用多进程并行来提高构建速度。sourceMap
,将错误消息位置映射到模块(储存着位置信息)。drop_console
,打包时剔除所有的console
语句drop_debugger
,打包时剔除所有的debugger
语句作为一个管小组前端的懒B,很多时候写页面会遗留console.log
,影响性能。设置个drop_console
就非常香。以下配置亲测有效。
const TerserPlugin = require('terser-webpack-plugin')
....
new TerserPlugin({
cache: true,
parallel: true,
sourceMap: true, // Must be set to true if using source-maps in production
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
})
更多的配置请看Terser Plugin
开启gzip压缩的好处是什么?
可以减小文件体积,传输速度更快。gzip是节省带宽和加快站点速度的有效方法。
Webpack
开启gzip
这里使用的插件为:CompressionWebpackPlugin
const CompressionWebpackPlugin = require('compression-webpack-plugin')
module.exports = {
“plugins”:[new CompressionWebpackPlugin]
}
具体配置:
const CompressionWebpackPlugin = require('compression-webpack-plugin');
webpackConfig.plugins.push(
new CompressionWebpackPlugin({
asset: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp('\\.(js|css)$'),
// 只处理大于xx字节 的文件,默认:0
threshold: 10240,
// 示例:一个1024b大小的文件,压缩后大小为768b,minRatio : 0.75
minRatio: 0.8 // 默认: 0.8
// 是否删除源文件,默认: false
deleteOriginalAssets: false
})
)
Nginx
的gzip
设置打开/etc/nginx/conf.d
编写以下配置。
server {
gzip on;
gzip_static on;
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
gzip_proxied any;
gzip_vary on;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
...
}
Nginx
尝试查找并发送文件/path/to/bundle.js.gz
。如果该文件不存在,或者客户端不支持 gzip
,Nginx则会发送该文件的未压缩版本。
保存配置后,重新启动Nginx
:
$ sudo service nginx restart
gzip
?通过使用curl
测试每个资源的请求响应,并检查Content-Encoding
:
显示 Content-Encoding: gzip
,即为配置成功。
不同之处在于:
Webpack
压缩会在构建运行期间一次压缩文件,然后将这些压缩版本保存到磁盘。
nginx
在请求时压缩文件时,某些包可能内置了缓存,因此性能损失只发生一次(或不经常),但通常不同之处在于,这将在响应 HTTP请求时发生。
对于实时压缩,让上游代理(例如 Nginx)处理 gzip和缓存通常更高效,因为它们是专门为此而构建的,并且不会遭受服务端程序运行时的开销(许多都是用C语言编写的) 。
使用 Webpack的
好处是, Nginx
每次请求服务端都要压缩很久才回返回信息回来,不仅服务器开销会增大很多,请求方也会等的不耐烦。我们在 Webpack
打包时就直接生成高压缩等级的文件,作为静态资源放在服务器上,这时将 Nginx
作为二重保障就会高效很多(请求其它目录资源时)。
注:具体是在请求时实时压缩,或在构建时去生成压缩文件,就要看项目业务情况。
本来还想谢谢动态配置表单相关,但篇幅太长也太难写了。
好了,又水完一篇,入正题:
目前本人在(又)准备跳槽,希望各位大佬和HR小姐姐可以内推一份靠谱的深圳前端岗位!996.ICU
就算了。
huab119
[email protected]
roughViz.js
基于D3(v5)
, roughjs
, 和handy
。
有三种衡量方式:
简答CDN
:
<script src="https://unpkg.com/[email protected]"></script>
npm
:
npm install rough-viz
react/vue
:
npm install react-roughviz
npm install vue-roughviz
甚至在Python
中也可以:
pip install roughviz
首先定义两个div
<div id="vis0"></div>
<div id="vis1"></div>
之后new
两个实例:
new roughViz.BarH(
{
element: '#vis0',
title: "Vehicles I've Had",
titleFontSize: '1.5rem',
legend: false,
margin: {top: 50, bottom: 100, left: 160, right: 0},
data: {
labels: ['1992 Ford Aerostar Van', '2013 Kia Rio', '1980 Honda CB 125s', '1992 Toyota Tercel'],
values: [8, 4, 6, 2]
},
xLabel: 'Time Owned (Years)',
strokeWidth: 2,
fillStyle: 'zigzag-line',
highlight: 'gold',
}
);
new roughViz.BarH(
{
element: '#vis1',
titleFontSize: '1.5rem',
data: 'https://raw.githubusercontent.com/jwilber/random_data/master/owTanks.csv'
color: 'tan',
labels: 'name',
values: 'health',
title: "Overwatch Tank Health",
roughness: 4,
}
);
整个的配置非常简洁,其中:
线上体验demo
: https://blockbuilder.org/jwilber/419fa6d878fe6c0f79a28f9fc72d7ec6
具体用法请参照官方文档:https://github.com/jwilber/roughViz
react-three-fiber
Web
和react-native
都可用的高性能Threejs for react
库。
可以在React
外部驱动渲染循环,而不会产生额外开销。
最新版本采用了Hooks
的写法,不像以往强行兼容的Threejs
,写起来更加友好。
不止抖音字体爆炸特效,它能实现什么,源于你的技术和想象力。
以下一部分特效:
如果有人学会了...大佬带带?
其中用到一个库:react-spring
,这是react
最优秀的动画库,没有之一。
官方文档:https://github.com/react-spring/react-three-fiber
字体爆炸:https://codesandbox.io/s/y3j31r13zz
Mini Music Player - VueJS
国外友人写的一个Vue.js
音乐播放器,好看的不得了。
其中的交互和逻辑,也是非常精炼。
源码:https://codepen.io/JavaScriptJunkie/pen/qBWrRyg
UI
都夸好的卡片验证库:interactive-paycard
这个11月Vue
新库一发布,就狂揽3k+
star,过于优秀。
完整库名vue-interactive-paycard
React
版的作者表示也即将发布了。
源码:https://github.com/muhammederdem/vue-interactive-paycard/issues
SandDance
微软出品,必属精品
SandDance
是使用Vega
进行图表布局,使用Deck.gl
进行WebGL
渲染。
能在如此密集的数据量上保持动画流畅和美观的,也就微软爸爸能做到了。
我先跪了,你们随意。
此外,该库还有多种使用方式:
体验:https://sanddance.js.org/app/
AR
: AR.js
+Three.js
+Autodesk 3D
这是个很有意思的实现,大致流程是:
AR.js
程序开始ARToolKit
识别到图片标记A-Frame.js
开始调用Three.js
渲染3D模型首先我们先到 https://sketchfab.com
下载自己喜欢的3D模型
下载glTF
格式(A
框架提供glTF
,OBJ
两个格式官网建议使用glTF
)
index.html
并把这些代码都贴上<script src="https://aframe.io/releases/0.9.0/aframe.min.js"></script>
<script src="https://rawgit.com/jeromeetienne/ar.js/master/aframe/build/aframe-ar.js"></script>
<script>THREEx.ArToolkitContext.baseURL = 'https://rawgit.com/jeromeetienne/ar.js/master/three.js/'</script>
<body style='margin : 0px; overflow: hidden;'>
<a-scene embedded arjs='sourceType: webcam; debugUIEnabled: false;'>
<a-marker type='pattern' url='res/pattern-marker.patt'>
<a-entity position='-3 2 0' text="width: 5; value:I am Psyduck. We are pokemon. We love you"></a-entity>
<a-entity position='0 0 0' gltf-model="url(res/scene.gltf)"></a-entity>
</a-marker>
</a-scene>
</body>
A-frame
的html
标签添加一个a-scene
摄像头并把AR.js
崁入Marker
如果标记的Marker
出现在摄像头里就会执行下方的事情3D
模型原文:AR用AR.js做一個讓另對方 喔喔喔喔! 的小卡片吧!
还有超赞的《这个杀手不太冷》女孩模型
原本想凑齐十个再发的,但找了好久,都没什么开源库能入我法眼。
恳请大家,推荐几款*得不行的开源库,我来补充补充,谢谢喇。
如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:
也可以来我的GitHub
博客里拿所有文章的源文件:
密码学是各种安全应用程序所必需的,现代密码学旨在创建通过应用数学原理和计算机科学来保护信息的机制。但相比之下,密码分析旨在解密此类机制,以便获得对信息的非法访问。
密码学具有三个关键属性:
例如个人医疗数据:
在本文中,我们将从加密,哈希,编码和混淆四种密码学基础技术来入门。
加密定义:以保证机密性的方式转换数据的过程。
为此,加密需要使用一个保密工具,就密码学而言,我们称其为“密钥”。
加密密钥和任何其他加密密钥应具有一些属性:
加密分为两类:对称和非对称
对称加密:
用途:文件系统加密,Wi-Fi保护访问(WPA),数据库加密(例如信用卡详细信息)
非对称加密:
其主要区别是:所需的密钥数量:
能被密码界承认的加密算法都是公开的:
哈希算法定义:·一种只能加密,不能解密的密码学算法,可以将任意长度的信息转换成一段固定长度的字符串。
加密算法是可逆的(使用密钥),并且可以提供机密性(某些较新的加密算法也可以提供真实性),而哈希算法是不可逆的,并且可以提供完整性,以证明未修改特定数据。
哈希算法的前提很简单:给定任意长度的输入,输出特定长度的字节。在大多数情况下,此字节序列对于该输入将是唯一的,并且不会给出输入是什么的指示。换一种说法:
为了说明这一点,请想象一个强大的哈希算法通过将每个唯一输入放在其自己的存储桶中而起作用。当我们要检查两个输入是否相同时,我们可以简单地检查它们是否在同一存储桶中。
散列文件的存储单位称为桶(Bucket)
提供文件下载的网站通常会返回每个文件的哈希值,以便用户可以验证其下载副本的完整性。
例如,在Debian
的图像下载服务中,您会找到其他文件,例如SHA256SUMS
,其中包含可供下载的每个文件的哈希输出(在本例中为SHA-256
算法)。
在终端中,可以用openssl
来对文件进行哈希处理:
$ openssl sha256 /Users/hiro/Downloads/非对称.png
SHA256(/Users/hiro/Downloads/非对称.png)= 7c264efc9ea7d0431e7281286949ec4c558205f690c0df601ff98d59fc3f4f64
同一个文件采用相同的hash
算法时,就可以用来校验是否同源。
在强大的哈希算法中,如果有两个不同的输入,则几乎不可能获得相同的输出。
而相反的,如果计算后的结果范围有限,就会存在不同的数据经过计算后得到的值相同,这就是哈希冲突。(两个不同的数据计算后的结果一样)
这种称为:哈希碰撞(哈希冲突)。
如果两个不同的输入最终出现在同一个存储桶中,则会发生冲突。如MD5
和SHA-1
,就会出现这种情况。这是有问题的,因为我们无法区分哪个碰撞的值匹配输入。
强大的哈希算法几乎会为每个唯一输入创建一个新存储桶。
在web
开发中,哈希算法使用最频繁的是在网站登陆应用上:
绝大多数的网站,在将登陆数据存入时,都会将密码哈希后存储。
哈希算法的一个有趣的方面是:无论输入数据的长度如何,散列的输出始终是相同的长度。
从理论上讲,碰撞冲突将始终在可能性的范围之内,尽管可能性很小。
与之相反的是编码。
编码定义:将数据从一种形式转换为另一种形式的过程,与加密无关。
它不保证机密性,完整性和真实性这三种加密属性,因为:
URL
编码又叫百分号编码,是统一资源定位(URL
)编码方式。URL
地址(常说网址)规定了:
/,:@
等)%xx
编码处理。现在已经成为一种规范了,基本所有程序语言都有这种编码,如:
编码方法很简单,在该字节ascii
码的16进制字符前面加%
. 如 空格字符,ascii
码是32,对应16进制是'20',那么urlencode
编码结果是:%20
。
# 源文本:
The quick brown fox jumps over the lazy dog
# 编码后:
#!shell
%54%68%65%20%71%75%69%63%6b%20%62%72%6f%77%6e%20%66%6f%78%20%6a%75%6d%70%73%20%6f%76%65%72%20%74%68%65%20%6c%61%7a%79%20%64%6f%67
HTML实体编码
在HTML
中,需要对数据进行HTML
编码以遵守所需的HTML
字符格式。转义避免XSS攻击也是如此。
Base64/32/16
编码base64
、base32
、base16
可以分别编码转化8位字节为6位、5位、4位。
16,32,64分别表示用多少个字符来编码,
Base64
常用于在通常处理文本数据的场合,表示、传输、存储一些二进制数据。包括MIME
的email,email via MIME
,在XML
中存储复杂数据。
编码原理:
Base64
编码要求把3个8位字节转化为4个6位的字节A-Z,a-z,0-9,+,/
这64个编码字符,=
号不属于编码字符,而是填充字符Base64
映射表,如下:
上面的示例旨在指出,编码的用例仅是数据处理,而不为编码的数据提供保护。
混淆定义:将人类可读的字符串转换为难以理解的字符串
。
尽管不能保证机密性,但混淆仍有其它应用:
但是,如此存在许多有助于消除应用程序代码混淆的工具。那就是其它话题了。。。
JavaScript
混淆JavaScript
源代码:
function hello(name) {
console.log('Hello, ' + name);
}
hello('New user');
混淆后:
var _0xa1cc=["\x48\x65\x6C\x6C\x6F\x2C\x20","\x6C\x6F\x67","\x4E\x65\x77\x20\x75\x73\x65\x72"];
function hello(_0x2cc8x2){console[_0xa1cc[1]](_0xa1cc[0]+ _0x2cc8x2)}hello(_0xa1cc[2])
从机密性,完整性,真实性分析四种密码技术:
加密 | 哈希 | 编码 | 混淆 | |
---|---|---|---|---|
机密性 | ✅ | ❌ | ❌ | ❌ |
完整性 | ❓ | ✅ | ❌ | ❌ |
真实性 | ❓ | ❌ | ❌ | ❌ |
HMAC
)和某些传输层安全性(TLS
)方法。因为写博客加了许多同行粉丝,有些人会试探性地询问我哪高就,以及是否科班。
多数情况下我基本不回应。并非我高傲冷漠,而是真没可谈的:
以下是稍微正式点的自我介绍:
我叫“笑妄”,16年地理信息系统专业专科毕业,自学的前端。
目前三年半的经验,前后工作过几家中小公司,做过Python爬虫,也曾在运维开发部混过。前两年的工作,都在为生计挣扎,做码农仅因自身一无所长,看这行工资高,就挤进来了。
微博时代起,我的网名“笑妄”就一直没变过,而今年则多了一个身份:“前端劝退师”。
从前端废材,到掘金/公众号 总阅读破百万的前端劝退师,我耗时10个月达成。
而我也从中获益良多,在不停变法子劝退自己的同时,写了一些对掘友还算有点帮助的文章。
19年初,当我在某速运公司的运维开发部待了近大半年后,开始有了跳槽的念头:
于是我开始审视更新后的简历,简直惨不忍睹,堪称“三无”前端:无学历,无能力,无亮点。
心想完蛋,拿什么与人争。当时也没什么好的想法,就把面试相关基础补了一遍。而某一天在水群时,看了浪浪@浪里行舟 的一篇《写技术博客那点事》:
“从某种意义上说,博客是我最好的学习笔记和个人名片。在IT行业内,技术博客是了解一个开发者最好的方式之一,特别是当你没有一张足够分量的文凭或者一段出彩的工作经历时,你就应该沉下心来好好打磨自己技术,打造自己的博客。往者不可谏,来者犹可追。”
喔吼,那就写吧。就在我着手博客事宜时,我迎来了自己的第一只猫:多多。
于是,我便开启了左手撸猫,右手敲文的快哉生活。
自3月起,我开始利用攒下来的加班调休时间,陆续的请假去面试。而且当时部门处于空闲期,作为一个不起眼的小虾米,上班逛技术社区也没人理会。
于是我多进程操作:
medium
,掘金
等技术社区,寻求答案。总结起来就是,前两天面试,中间一天整理,剩下三四天总结成文。
说来容易,过程却是很煎熬:既不想写堆叠资源list文,也不愿复刻一遍他人的知识。
那咋办?先仿写,再总结。 加上该有的引用,一篇热辣辣的文章就出炉了。
三月面试时,面试吹嘘过自己的博客,当时说自己掘金阅读3W+
,而到了今日,已经突破50W
关卡,并晋级为掘金共建者。
30篇博客,每篇都是力求有趣,不乏味。涉猎的范围,也不至于前端:
Vue
相关JavaScript
相关这些多数篇章都在3000
字以上,早期的更是8000
字开外。碍于知识有限,文章多有勘误。在此也感谢各位掘友的指正与支持。
在写完这些文章后,我多多也长成下面模样:
2019年应该是我从业以来技术进步最快的一年:
React/Vue
涉猎源码和性能优化。JavaScript
及各类库而我已收获了不少:
“有天若有人问,这一行烟尘滚滚。成败不论,我给自己打几分。我会说我很努力,
也祈祷好运气......一路上,要不是你们这些人,我不可能。” --- 李宗盛《你们》
劝退师的公众号头像是李宗盛,熟悉我的盆友也知道,我将李宗盛当作偶像。
除了他写的歌深入人心,还因为他的人生经历:瓦斯行老板的儿子,中专挂科读多了两年,后靠自己的天赋加努力成为一代音乐教父。
他前二十几岁的经历,像极了我们这些普普通通的转行前端er:
年少成绩不佳,待到出来社会,家中也无IT行业的带路人,只得独自摸索,磕磕碰碰的从事了编程这行。
Carson
(前IBM技术大拿),若非您从茫茫简历中捞起我来,我可能已经自卑转行了。前东家部门是做旅游的,在这次疫情打击下,基本玩完。
于是我半休半远程三个月后,在4月底领了裁员便当。至今,差不多找了两个月的工作。
本篇不是标准的面经,想从中获取大厂跳槽经验的可以歇一歇。
更多的是想讲一下绝大多数如你如我,学历渣技术差,没大厂经验的前端如何走。
Offer
情况个人比较懒,一周可能就面2~3家,只约下午。部分星期没有面试邀约。
囿于学历+公司,两招聘软件都被我用成“Boss
直拒”和“拉钩上吊”
粗略算了下,面了约12家大中小型公司,仅4家Offer
,情况分别为:
作为一个社交孤儿,在本次跳槽历程中也是发现自己不少的问题,且听我慢慢道来。
本篇虽然有点丧,但你们可以从中找到对应的问题(我几乎犯了所有面试的低级错误)
部分的公司有:360
奇舞团,某上市游戏公司,风变编程,金山系某司,阿里。
面过的公司有点多,一并说了吧。
见于:某游戏公司、小鹅通、阿里一面、另外三家小公司
这问题的答案,我结合了yck
《前端面试之道》和 浏览器原理专栏:
整个过程可以分为几步:
用户输入
当用户输入关键字并键入回车之后,这意味着当前页面即将要被替换成新的页面,不过在这个流程继续之前,浏览器还给了当前页面一次执行 beforeunload
事件的机会,beforeunload
事件允许页面在退出之前执行一些数据清理操作,还可以询问用户是否要离开当前页面。
URL
请求过程
首先,网络进程会查找本地缓存是否缓存了该资源。
如果有缓存资源,那么直接返回资源给浏览器进程;如果在缓存中没有查找到资源,那么直接进入网络请求流程。这请求前的第一步是要进行 DNS
解析,以获取请求域名的服务器 IP
地址。如果请求协议是 HTTPS
,那么还需要建立 TLS
连接。
DNS
也有几步缓存:浏览器缓存,hosts
文件,TCP
三次握手,HTTP
。TLS
握手,HTTPS
。接下来就是利用 IP
地址和服务器建立 TCP
连接。连接建立之后,浏览器端会构建请求行、请求头等信息,并把和该域名相关的 Cookie
等数据附加到请求头中,然后向服务器发送构建的请求信息。
数据在进入服务端之前,可能还会先经过负责负载均衡的服务器,它的作用就是将请求合理的分发到多台服务器上,这时假设服务端会响应一个 HTML
文件。
首先浏览器会判断状态码是什么,如果是 200
那就继续解析,如果 400
或 500
的话就会报错,如果 300
的话会进行重定向,这里会有个重定向计数器,避免过多次的重定向,超过次数也会报错。
浏览器开始解析文件,如果是 gzip
格式的话会先解压一下,然后通过文件的编码格式知道该如何去解码文件。
准备渲染进程
默认情况下,Chrome
会为每个页面分配一个渲染进程,也就是说,每打开一个新页面就会配套创建一个新的渲染进程。
渲染阶段
文件解码成功后会正式开始渲染流程,先会根据 HTML
构建 DOM
树,有CSS
的话会去构建 CSSOM
树。如果遇到 script
标签的话,会判断是否存在 async
或者 defer
,前者会并行进行下载并执行 JS,后者会先下载文件,然后等待 HTML
解析完成后顺序执行。
如果以上都没有,就会阻塞住渲染流程直到 JS
执行完毕。
CSSOM
树和 DOM
树构建完成后会开始生成 Render
树,这一步就是确定页面元素的布局、样式等等诸多方面的东西
在生成 Render
树的过程中,浏览器就开始调用GPU
绘制,合成图层,将内容显示在屏幕上了。
eventloop
机制,promise
的实现和静态方法、async
实现。
这题聊起来可就大了,进程,线程,协程。部分还会配以那道最经典的eventloop
题目。
见于:阿里一面、小鹅通、头条一面、360一面、风变编程、以及其它四家公司,必考。
Event Loop
是什么?
JavaScript
的事件分两种,宏任务(macro-task
)和微任务(micro-task
)
script,setTimeout,setInterval
Promise.then(非new Promise)
,process.nextTick(node中)
Event Table
并注册函数,异步事件完成后,会将回调函数放入Event Queue
中(宏任务和微任务是不同的Event Queue
),同步任务执行完成后,会从Event Queue
中读取事件放入主线程执行,回调函数中可能还会包含不同的任务,因此会循环执行上述操作。Promise
的含义
Promise
是一个异步编程的解决方案,简单来讲,Promise
类似一个盒子,里面保存着在未来某个时间点才会结束的事件。
三种状态:
pending
:进行中fulfilled
:已经成功rejected
:已经失败pending
变成 fulfilled
或者 rejected
,状态不可逆。async
实现和常用方法
async
函数的实现原理,就是将 Generator
函数和自动执行器,包装在一个函数里。
Generator
函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。
协程是一种用户态的轻量级线程,
协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
Vue
和 React
之间的区别摘自yck《前端面试之道》
Vue 的表单可以使用 v-model
支持双向绑定,相比于 React 来说开发上更加方便,当然了 v-model
其实就是个语法糖,本质上和 React 写表单的方式没什么区别。
改变数据方式不同,Vue 修改状态相比来说要简单许多,React 需要使用 setState
来改变状态,并且使用这个 API 也有一些坑点。并且 Vue 的底层使用了依赖追踪,页面更新渲染已经是最优的了,但是 React 还是需要用户手动去优化这方面的问题。
React 16以后,有些钩子函数会执行多次,这是因为引入 Fiber 的原因,这在后续的章节中会讲到。
React 需要使用 JSX,有一定的上手成本,并且需要一整套的工具链支持,但是完全可以通过 JS 来控制页面,更加的灵活。Vue 使用了模板语法,相比于 JSX 来说没有那么灵活,但是完全可以脱离工具链,通过直接编写 render
函数就能在浏览器中运行。
在生态上来说,两者其实没多大的差距,当然 React 的用户是远远高于 Vue 的。
在上手成本上来说,Vue 一开始的定位就是尽可能的降低前端开发的门槛,然而 React 更多的是去改变用户去接受它的概念和**,相较于 Vue 来说上手成本略高。
Vue 3.0
面试题见于:360一面、风变编程,预计下半年必考。
Vue3.0
都有哪些重要新特性?
React Hooks
和webpack
的Tree-shaking
Vue3.0
对比Vue2.0
的优势在哪?
Vue3.0
和React 16.X
都有哪些区别和相似处?
- 可以更新下你的横比答案了,重点突出两者开始相互借鉴,互有优点。记得夸夸
Vue3.0
抄过来,却做得更好的部分。
Vue3.0
是如何实现代码逻辑复用的?
- 可以先对比
Composition API
和mixin
的差异,并凸显出Vue2.0
那种代码上下反复横跳的缺点。
以上答案基本可以在下面两篇博客里找到:
React
高频面试题
React 16.X
的Fiber
原理
setState
原理,什么时候是同步的?
React Hooks
相对高阶组件和Class
组件有什么优势/缺点?
React 16.X
的生命周期,以及为何要替换掉以前的?
React
跨平台的实现原理。
- 说一说
redux
,以及比flux
先进的原因。
平心而论,如果面试前未要求技术栈,建议往Vue
方向引。React
的面试题要高一两个难度....
HTTP
高频面试题见于:阿里一面、头条一面、360一面、风变编程、以及其它四家公司,必考。
- 讲一讲强缓存和协议缓存?
HTTP/2.0
都有哪些特性?头部压缩的原理?
TCP
三次握手和四次挥手?以其存在意义。
- 状态码。
302.304.301.401.403
的区别?
- 状态码。
204
和304
分别有什么作用?
HTTP
和HTTPS
握手差异?
CSRF
跨站请求伪造和XSS
跨站脚本攻击是什么?
- 你是如何解决跨域的?都有几种?
nginx
了解吗?你都用来做什么?
- 有了【
Last-Modified,If-Modified-Since
】为何还要有【ETag、If-None-Match
】
我总结过一张xmind
图,欢迎到我公众号里自取。
JS/CSS
高频基础问题见于:阿里一面、头条一面、风变编程、以及其它多家公司,非常高频。
- 弹性盒子中
flex: 0 1 auto
表示什么意思?
- 箭头函数可以用
new
实例化吗?聊聊this
的指向问题。
- 聊一聊原型链。
- 垃圾回收中的堆和栈的区别。
0.1 + 0.2 != 0.3
背后的原理?
TypeScript
用过吗?聊聊你对TypeScript
的理解?
- 图片懒加载的原理。
call、apply
和bind
方法的用法以及区别
Webpack
原理,以及常用插件
- 项目中遇到的难点,以及解决思路。
- 你是如何做
Web
性能优化的?首屏渲染如何处理?
这个问题很大,我有个简略版,回答思路引自专栏《
浏览器工作原理与实践》:
主要围绕着两个阶段:加载阶段 和 交互阶段
加载阶段:
Webpack
拆/合包,懒加载等)RTT
的时间(Gzip
压缩,边缘节点CDN
)交互阶段:
JS
代码不可占用主线程太久,与首屏无关的脚本加上延后处理(aysnc/defer
)属性,与DOM
无关的交给Web Worker
。CSS
用对选择器(尽可能绑定Class
或Id
),否则会遍历多次。will-change
属性,浏览器会开辟新的层处理(触发合成机制)
- 埋点数据上报的方案做过吗?
- 前端架构思考,你是如何考量部门的技术栈的?
- 前端重构思考,老项目在新业务紧急与重构技术债务间如何衡量轻重?
DevOps
认知由于我第二家公司部门是做DevOps
平台,有些与前端无关的面试题。
- 单元测试做过吗?你是怎么做的?
docker
准备流程?
DevOps
平台关键功能点?
- 自动化测试,
CI/CD
的关键核心都有哪些?
- 如何保障
DevOps
推动?
- 接口如何做优化?
Mock
平台搭建方案?
今年,太难太难了。
@木易杨 大佬的这段话很对:
先来说下大环境,感觉非常不好,就一二线互联网来说招人的没几家公司,裁员的、内部调整的、锁 HC 的确是一大堆,所以大家在换工作的时候一定不要裸辞,风险太大。
今年面试和往年感受有些不同,对于项目的重难点、亮点、个人在团队中做的贡献、对项目的 Owner 意识等比较关注,还有就是编程能力的考察会更多一些。
简单讲,就是学历是第一竞争力,你靠APP
投简历几乎没反馈,内推最靠谱。
其次,如果你仅有大专,能力非高得不可替代,不建议在今年频繁投大厂,我就经历过各种各样简历面挂和HR面挂,部分原因:
平心而论,过往跳槽都不是很顺利,但今年是真感受到了天花板:
你往后的日子里,都在为高中不努力买单。
严格说,本面渣不配给一些面试技巧,但有些是我没做好却很不错的东西。建议你们看一看:
如果你一没学历,二没大厂简历,简历写得稀烂。
相信我,投递之前,付个几十块找业内大佬帮你改改简历,你一定会有更多的投递反馈。
面试官您好!
我叫***
,很开心今天来应聘**
岗位,我有*
年***
岗位工作经验,工作内容包括**、*** **
等,曾参与***
项目/工作,完成**
业绩,这些经验锻炼了我***
能力。除了日常业务开发外,我还在***
方面.....
面试之前,我了解到咱们公司主要从事***
业务、***
类产
品,属于行业排名***
的企业,我对这个行业非常看好,也想在这行
长期发展!这个岗位要求的** ***
能力和经验,与我的工作经历很
匹配,相信我能够胜任这个岗位,谢谢!
在我面试一个月左右,发现经常挂在二/三面后,开始审视自己的问题:
说实话,挺难述说的,特别是我这种没啥大项目经验,前三年都在做中后台系统居多,只能从一些细节入手,以下是我的草稿(虽然很烂),供你们参考一下。:
Vue后台细颗粒权限控制与防多人操作:
一般简单的权限控制会以角色区别,但开发/运维们想自己设成员,需求1
给他们做了一个权限管理的模块,但因为DevOps的功能模块太多,记不住,开发/运维们想要直观的修改权限,需求2。
考虑到这个痛点,我设计了一个“授权模式”,高权限的拥有切换功能。
切换后,该模块下所有的按钮都会先拦截左键点击,并拦截默认右键,多了一个自定义属性。以及授权/冻结 菜单。授权功能,直接拥有选择成员的树控件弹窗。
使用后会上传 成员数组 + 自定义属性。下一次访问后台便返回 该自定义属性 对象 ,以确定新的权限。
权限问题解决后,开发/运维们又发现,一个部署/发布 点击模块存在同时操作的人很多。会冲突。需求4。
在多人频繁触发模块,加入了WebSocket
管理,实现类似在线文档的显示功能,显示当前操作人,并设屏蔽(同时操作会有显示姓名,并锁住)。
简单版本:当前按钮 点击后,后台设个时间阀值,该段时间内其他人点击了就弹出提醒当前有人操作。
复杂版本:多人WebSocket
维护一个值。
对应场景:DevOps
平台
去年写过一篇年度总结:
我叫“笑妄”,16年地理信息系统专业专科毕业,自学的前端。 目前三年半的经验,前后工作过几家中小公司,做过Python爬虫,也曾在运维开发部混过。前两年的工作,都在为生计挣扎,做码农仅因自身一无所长,看这行工资高,就挤进来了。--- 《前端废材的自我劝退之路 》
y1s1
,我底子很差。1年时才搞懂布局,2年不会函数return
,3年才熟悉React
。
入这行以来,一路虽有伯乐,但于前端领域仍单打独斗居多。
却得益于这行,让我知“井外方觉天地大”。认识优秀的人越多,越想努力,精进。
所以这段时间也没闲着,看了一些还不错的书/专栏:
DevOps
实战笔记》,有趣,但需有一定的基础。HTTP
协议》,还不错,值二刷。Google V8
》,相对第一本《浏览器原理》,水了不少,部分值二刷。Nginx
核心知识100讲》,目前正在看。如何提高自己的技术竞争力(或收入)?
DevOps
,帮助我搞掂了不少2/3面技术面。这一路上,我的能力,学历,背景样样不如人,也曾懒惰曾迷茫,但一直都在和自己赛跑,我不服输:
虽然在你离开学校的时候
所有的人都认为你不会有出息
你却没有因此怨天尤人自暴自弃
....
在那时侯我们身边都有一卡车的难题
不知道成功的意义就在超越自己
我们都是和自己赛跑的人
为了更好的未来拼命努力
争取一种意义非凡的胜利
为了更好的明天拼命努力
前方没有终点
我们永不停息
如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:
闲暇之余也建了一个“前端人才库”群
不少人也是如愿获得了满意的Offer
:360、腾讯、大搜车、金山等等。想进来讨论面试/蹭蹭最新面试题的可以加我微信。
劝退师个人微信:huab119
也可以来我的GitHub
博客里拿所有文章的源文件:
前端劝退指南:https://github.com/roger-hiro/BlogFN
一起玩耍呀。~
本文暂未涉及Performance
面板的内容。
后续会单独出一篇,以下是目录:
Blackbox Script
WebView
bug
跟踪、断点调试等API
数据是否正确、审查页面元素样式及布局、页面加载性能分析、自动化测试AdBlock、Gliffy、Axure
等Elements
- 页面dom元素Console
- 控制台Sources
- 页面静态资源Network
- 网络Performance
- 设备加载性能分析Application
- 应用信息,PWA/Storage/Cache/Frames
Security
- 安全分析Audits
- 审计,自动化测试工具cmd + shift + p
在Devtools
打开的情况下,键入cmd + shift + p
将其激活,然后开始在栏中键入要查找的命令或输入"?"
号以查看所有可用命令。
...
: 打开文件:
: 前往文件@
:前往标识符(函数,类名等)!
: 运行脚本文件>
: 打开某菜单功能> performance monitor
将显示性能监视器以及相关信息,例如CPU,JS堆大小和DOM节点。
> FPS
选择第一项> screen
选择Capture node screenhot
DOM
断点调试当你要调试特定元素的DOM中的更改时,可以使用此选项。这些是DOM更改断点的类型:
Subtree modifications
: 子节点删除或添加时Attributes modifications
: 属性修改时Node Removal
: 节点删除时如上图:监听form
标签,在input
框获得焦点时,触发断点调试
Blackbox Script
剔除多余脚本断点。
例如第三方(Javascript
框架和库,广告等的堆栈跟踪)。
为避免这种情况并集中精力处理核心代码,在Sources
或网络选项卡中打开文件,右键单击并选择Blackbox Script
。
Event Listener Breakpoints
Sources
面板Event Listener Breakpoints
local overrides
使用我们自己的本地资源覆盖网页所使用的资源。
类似的,使用DevTools
的工作区设置持久化,将本地的文件夹映射到网络,在chrome
开发者功能里面对css 样式的修改,都会直接改动本地文件,页面重新加载,使用的资源也是本地资源,达到持久化的效果。
Sources > Overrides > Enable local Overrides
,选择本地文件夹Elements
,编辑样式,自动生成本地文件。Sources
,检查文件,编辑更改。local overrides
模拟Mock
数据对于返回json 数据的接口,可以利用该功能,简单模拟返回数据。
比如:
api
为: http://www.xxx.com/api/v1/list
在根目录下,新建文件 www.xxx.com/api/v1/list
,list
文件中的内容,与正常接口返回格式相同。
对象或者数组类型,从而覆盖掉原接口请求。
可以执行常见任务的功能,例如选择DOM
元素,触发事件,监视事件,在DOM
中添加和删除元素等。
这像是Chrome
自身实现的jquery
加强版。
$(selector, [startNode])
:单选择器document.querySelector
的简写
语法:
$('a').href;
$('[test-id="logo-img"]').src;
$('#movie_player').click();
控制台还会预先查询对应的标签,十分贴心。
还可以触发事件,如暂停播放:
此函数还支持第二个参数startNode,该参数指定从中搜索元素的“元素”或Node。此参数的默认值为document
$$(选择器,[startNode])
:全选择器document.querySelectorAll
的简写,返回一个数组标签元素
语法:
$$('.button')
此函数还支持第二个参数startNode,该参数指定从中搜索元素的“元素”或Node。此参数的默认值为document
用法:
var images = $$('img', document.querySelector('.devsite-header-background'));
for (each in images) {
console.log(images[each].src);
}
$x(path, [startNode])
:xpath
选择器$x(path)
返回与给定xpath
表达式匹配的DOM元素数组。
例如,以下代码返回<p>
页面上的所有元素:
$x("//p")
$x("//p[a]")
xpath
多用于爬虫抓取,前端的同学可能不熟悉。
getEventListeners(object)
:获取指定对象的绑定事件getEventListeners(object)
返回在指定对象上注册的事件侦听器。返回值是一个对象,其中包含每个已注册事件类型(例如,click
或keydown
)的数组。每个数组的成员是描述为每种类型注册的侦听器的对象。
用法:
getEventListeners(document);
console
除了不同等级的warn
和error
打印外
还有其它非常实用的打印。
%s
、%o
、%d
、和%c
const text = "文本1"
console.log(`打印${text}`)
除了标准的ES6
语法,实际上还支持四种字符串输出。
分别是:
console.log("打印 %s", text)
%s
:字符串%o
:对象%d
:数字或小数还有比较特殊的%c
,可用于改写输出样式。
console.log('%c 文本1', 'font-size:50px; background: ; text-shadow: 10px 10px 10px blue')
const text = "文本1"
console.log('%c %s', 'font-size:50px; background: ; text-shadow: 10px 10px 10px blue', text)
你还可以这么玩:
console.log('%c Auth ',
'color: white; background-color: #2274A5',
'Login page rendered');
console.log('%c GraphQL ',
'color: white; background-color: #95B46A',
'Get user details');
console.log('%c Error ',
'color: white; background-color: #D33F49',
'Error getting user details');
当我们需要打印多个对象时,经常一个个输出。且看不到对象名称,不利于阅读:
以前我的做法是这么打印:
console.log('hello', hello);
console.log('world', world);
这显然有点笨拙繁琐。其实,输出也支持对象解构:
console.log({hello, world});
console.assert()
当你需要在特定条件判断时打印日志,这将非常有用。
语法
console.assert(assertion,obj)
用法
const value = 1001
console.assert(value===1000,"value is not 1000")
console
编组:console.group()
当你需要将详细信息分组或嵌套在一起以便能够轻松阅读日志时,可以使用此功能。
console.group('用户列表');
console.log('name: 张三');
console.log('job: 🐶前端');
// 内层
console.group('地址');
console.log('Street: 123 街');
console.log('City: 北京');
console.log('State: 在职');
console.groupEnd(); // 结束内层
console.groupEnd(); // 结束外层
console.time()
没有Performance API
精准,但胜在使用简便。
let i = 0;
console.time("While loop");
while (i < 1000000) {
i++;
}
console.timeEnd("While loop");
console.time("For loop");
for (i = 0; i < 1000000; i++) {
// For Loop
}
console.timeEnd("For loop");
console.table()
这个适用于打印数组对象。。。
let languages = [
{ name: "JavaScript", fileExtension: ".js" },
{ name: "TypeScript", fileExtension: ".ts" },
{ name: "CoffeeScript", fileExtension: ".coffee" }
];
console.table(languages);
DOM
对象节点:console.dir()
打印出该对象的所有属性和属性值.
console.dir()
和console.log()
的作用区别并不明显。若用于打印字符串,则输出一摸一样。
console.log("Why,hello!");
console.dir("Why,hello!");
在输出对象时也仅是显示不同(log
识别为字符串输出,dir
直接打印对象。)。
唯一显著区别在于打印dom
对象上:
console.log(document)
console.dir(document)
WebView
使用Chrome
开发者工具在原生Android
应用中调试WebView
。
配置WebViews
进行调试。
在 WebView
类上调用静态方法setWebContentsDebuggingEnabled
。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
WebView.setWebContentsDebuggingEnabled(true);
}
手机打开usb
调试,插上电脑。
在Chrome
地址栏输入:Chrome://inspect
正常的话在App
中打开WebView
时,chrome
中会监听到并显示你的页面。
4. 点击页面下的inspect
,就可以实时看到手机上WebView
页面的显示状态了。(第一次使用可能会白屏,这是因为需要去https://chrome-devtools-frontend.appspot.com
那边下载文件)
除了inspect
标签,还有 Focus tab
:
Android
设备上的相同操作其他应用里的WebView
也可以,例如这是某个应用里的游戏,用的也是网页:
如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:
"use strict"
指令在 JavaScript 1.8.5 (ECMAScript5)
中新增。
至今,前端er们基本都默认开启严格模式敲代码。
那么,你知道Typescript
其实也有属于自己的严格模式吗?
Typescript
严格模式规则当Typescript
严格模式设置为on
时,它将使用 strict
族下的严格类型规则对项目中的所有文件进行代码验证。规则是:
规则名称 | 解释 |
---|---|
noImplicitAny |
不允许变量或函数参数具有隐式any 类型。 |
noImplicitThis |
不允许this 上下文隐式定义。 |
strictNullChecks |
不允许出现null 或undefined 的可能性。 |
strictPropertyInitialization |
验证构造函数内部初始化前后已定义的属性。 |
strictBindCallApply |
对 bind, call, apply 更严格的类型检测。 |
strictFunctionTypes |
对函数参数进行严格逆变比较。 |
noImplicitAny
此规则不允许变量或函数参数具有隐式any
类型。请看以下示例:
// Javascript/Typescript 非严格模式
function extractIds (list) {
return list.map(member => member.id)
}
上述例子没有对list
进行类型限制,map
循环了item
的形参member
。
而在Typescript
严格模式下,会出现以下报错:
// Typescript 严格模式
function extractIds (list) {
// ❌ ^^^^
// Parameter 'list' implicitly
// has an 'any' type. ts(7006)
return list.map(member => member.id)
// ❌ ^^^^^^
// Parameter 'member' implicitly
// has an 'any' type. ts(7006)
}
正确写法应是:
// Typescript 严格模式
interface Member {
id: number
name: string
}
function extractIds (list: Member[]) {
return list.map(member => member.id)
}
浏览器自带事件,比如e.preventDefault()
,是阻止浏览器默认行为的关键代码。
这在Typescript
严格模式下是会报错的:
// Typescript 严格模式
function onChangeCheckbox (e) {
// ❌ ^
// Parameter 'e' implicitly
// has an 'any' type. ts(7006)
e.preventDefault()
const value = e.target.checked
validateCheckbox(value)
}
若需要正常使用这类Web API
,就需要在全局定义扩展。比如:
// Typescript 严格模式
interface ChangeCheckboxEvent extends MouseEvent {
target: HTMLInputElement
}
function onChangeCheckbox (e: ChangeCheckboxEvent) {
e.preventDefault()
const value = e.target.checked
validateCheckbox(value)
}
请注意,如果导入了非Typescript
库,这也会引发错误,因为导入的库的类型是any
。
// Typescript 严格模式
import { Vector } from 'sylvester'
// ❌ ^^^^^^^^^^^
// Could not find a declaration file
// for module 'sylvester'.
// 'sylvester' implicitly has an 'any' type.
// Try `npm install @types/sylvester`
// if it exists or add a new declaration (.d.ts)
// file containing `declare module 'sylvester';`
// ts(7016)
这可能是项目重构Typescript
版的一大麻烦,需要专门定义第三方库接口类型
noImplicitThis
此规则不允许this
上下文隐式定义。请看以下示例:
// Javascript/Typescript 非严格模式
function uppercaseLabel () {
return this.label.toUpperCase()
}
const config = {
label: 'foo-config',
uppercaseLabel
}
config.uppercaseLabel()
// FOO-CONFIG
在非严格模式下,this
指向config
对象。this.label
只需检索config.label
。
但是,this
在函数上进行引用可能是不明确的:
// Typescript严格模式
function uppercaseLabel () {
return this.label.toUpperCase()
// ❌ ^^^^
// 'this' implicitly has type 'any'
// because it does not have a type annotation. ts(2683)
}
如果单独执行this.label.toUpperCase()
,则会因为this
上下文config
不再存在而报错,因为label
未定义。
解决该问题的一种方法是避免this
在没有上下文的情况下使用函数:
// Typescript strict mode
const config = {
label: 'foo-config',
uppercaseLabel () {
return this.label.toUpperCase()
}
}
更好的方法是编写接口,定义所有类型,而不是Typescript
来推断:
// Typescript strict mode
interface MyConfig {
label: string
uppercaseLabel: (params: void) => string
}
const config: MyConfig = {
label: 'foo-config',
uppercaseLabel () {
return this.label.toUpperCase()
}
}
strictNullChecks
此规则不允许出现null
或undefined
的可能性。请看以下示例:
// Typescript 非严格模式
function getArticleById (articles: Article[], id: string) {
const article = articles.find(article => article.id === id)
return article.meta
}
Typescript
非严格模式下,这样写不会有任何问题。但严格模式会非给你搞出点幺蛾子:
“你这样不行,万一find
没有匹配到任何值呢?”:
// Typescript strict mode
function getArticleById (articles: Article[], id: string) {
const article = articles.find(article => article.id === id)
return article.meta
// ❌ ^^^^^^^
// Object is possibly 'undefined'. ts(2532)
}
“我星星你个星星!”
于是你会将改成以下模样:
// Typescript strict mode
function getArticleById (articles: Article[], id: string) {
const article = articles.find(article => article.id === id)
if (typeof article === 'undefined') {
throw new Error(`Could not find an article with id: ${id}.`)
}
return article.meta
}
“真香!”
strictPropertyInitialization
此规则将验证构造函数内部初始化前后已定义的属性。
必须要确保每个实例的属性都有初始值,可以在构造函数里或者属性定义时赋值。
(strictPropertyInitialization
,这臭长的命名像极了React
源码里的众多任性属性)
请看以下示例:
// Typescript non-strict mode
class User {
username: string;
}
const user = new User();
const username = user.username.toLowerCase();
如果启用严格模式,类型检查器将进一步报错:
class User {
username: string;
// ❌ ^^^^^^
// Property 'username' has no initializer
// and is not definitely assigned in the constructor
}
const user = new User();
/
const username = user.username.toLowerCase();
// ❌ ^^^^^^^^^^^^
// TypeError: Cannot read property 'toLowerCase' of undefined
解决方案有四种。
undefined
为username
属性定义提供一个undefined
类型:
class User {
username: string | undefined;
}
const user = new User();
username
属性可以为string | undefined
类型,但这样写,需要在使用时确保值为string
类型:
const username = typeof user.username === "string"
? user.username.toLowerCase()
: "n/a";
这也太不Typescript
了。
这个方法有点笨,却挺有效:
class User {
username = "n/a";
}
const user = new User();
// OK
const username = user.username.toLowerCase();
最有用的解决方案是向username
构造函数添加参数,然后将其分配给username
属性。
这样,无论何时new User()
,都必须提供默认值作为参数:
class User {
username: string;
constructor(username: string) {
this.username = username;
}
}
const user = new User("mariusschulz");
// OK
const username = user.username.toLowerCase();
还可以通过public
修饰符进一步简化:
class User {
constructor(public username: string) {}
}
const user = new User("mariusschulz");
// OK
const username = user.username.toLowerCase();
在某些场景下,属性会被间接地初始化(使用辅助方法或依赖注入库)。
这种情况下,你可以在属性上使用显式赋值断言来帮助类型系统识别类型。
class User {
username!: string;
constructor(username: string) {
this.initialize(username);
}
private initialize(username: string) {
this.username = username;
}
}
const user = new User("mariusschulz");
// OK
const username = user.username.toLowerCase();
通过向该username
属性添加一个明确的赋值断言,我们告诉类型检查器:
username
,即使它自己无法检测到该属性,也可以期望该属性被初始化。
strictBindCallApply
此规则将对 bind, call, apply
更严格地检测类型。
啥意思?请看以下示例:
// JavaScript
function sum (num1: number, num2: number) {
return num1 + num2
}
sum.apply(null, [1, 2])
// 3
在你不记得参数类型时,非严格模式下不会校验参数类型和数量,运行代码时,Typescript
和环境(可能是浏览器)都不会引发错误:
// Typescript non-strict mode
function sum (num1: number, num2: number) {
return num1 + num2
}
sum.apply(null, [1, 2, 3])
// 还是...3?
而Typescript
严格模式下,这是不被允许的:
// Typescript strict mode
function sum (num1: number, num2: number) {
return num1 + num2
}
sum.apply(null, [1, 2, 3])
// ❌ ^^^^^^^^^
// Argument of type '[number, number, number]' is not
// assignable to parameter of type '[number, number]'.
// Types of property 'length' are incompatible.
// Type '3' is not assignable to type '2'. ts(2345)
那怎么办? “...”
扩展运算符和reduce
老友来相救:
// Typescript strict mode
function sum (...args: number[]) {
return args.reduce<number>((total, num) => total + num, 0)
}
sum.apply(null, [1, 2, 3])
// 6
该规则将检查并限制函数类型参数是抗变(contravariantly
)而非双变(bivariantly
,即协变或抗变)的。
初看,内心OS: “这什么玩意儿?”,这里有篇介绍:
协变和逆变维基上写的很复杂,但是总结起来原理其实就一个。
说个最容易理解的例子,int
和float
两个类型的关系可以写成下面这样。
int
≦ float
:也就是说int
是float
的子类型。
这一更严格的检查应用于除方法或构造函数声明以外的所有函数类型。方法被专门排除在外是为了确保带泛型的类和接口(如 Array )总体上仍然保持协变。
请看下面这个 Animal
是 Dog
和 Cat
的父类型的例子:
declare let f1: (x: Animal) => void;
declare let f2: (x: Dog) => void;
declare let f3: (x: Cat) => void;
f1 = f2; // 启用 --strictFunctionTypes 时错误
f2 = f1; // 正确
f2 = f3; // 错误
用另一种方式来描述这个例子则是,默认类型检查模式中 T
在类型 (x: T) => void
是 双变的,但在严格函数类型模式中 T
是 抗变的:
interface Comparer<T> {
compare: (a: T, b: T) => number;
}
declare let animalComparer: Comparer<Animal>;
declare let dogComparer: Comparer<Dog>;
animalComparer = dogComparer; // 错误
dogComparer = animalComparer; // 正确
写到此处,逼死了一个菜鸡前端。
参考文章:
在面试的过程中,常被问到为什么Typescript
比JavaScript
好用?
从这些严格模式规则,你就可以一窥当中的奥秘,今日开严格,他日Bug秒甩锅,噢耶。
某天在逛社区时看到一帖子:
react-dynamic-charts — A React Library for Visualizing Dynamic Data
这是一个国外大佬在其公司峰会的代码竞赛中写的一个库:react-dynamic-charts
,用于根据动态数据创建动态图表可视化。
它的设计非常灵活,允许你控制内部的每个元素和事件。使用方法也非常简单,其源码也是非常精炼,值得学习。
但因其提供了不少API
,不利于理解源码。所以以下实现有所精简:
getRandomColor
:随机颜色const getRandomColor = () => {
const letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)]
}
return color;
};
translateY
:填充Y轴偏移量const translateY = (value) => {
return `translateY(${value}px)`;
}
useState Hook
声明状态变量我们开始编写组件DynamicBarChart
const DynamicBarChart = (props) => {
const [dataQueue, setDataQueue] = useState([]);
const [activeItemIdx, setActiveItemIdx] = useState(0);
const [highestValue, setHighestValue] = useState(0);
const [currentValues, setCurrentValues] = useState({});
const [firstRun, setFirstRun] = useState(false);
// 其它代码...
}
useState
的简单理解:const [属性, 操作属性的方法] = useState(默认值);
dataQueue
:当前操作的原始数据数组activeItemIdx
: 第几“帧”highestValue
: “榜首”的数据值currentValues
: 经过处理后用于渲染的数据数组firstRun
: 第一次动态渲染时间useEffect
请配合注释食用
// 动态跑起来~
function start () {
if (activeItemIdx > 1) {
return;
}
nextStep(true);
}
// 对下一帧数据进行处理
function setNextValues () {
// 没有帧数时(即已结束),停止渲染
if (!dataQueue[activeItemIdx]) {
iterationTimeoutHolder = null;
return;
}
// 每一帧的数据数组
const roundData = dataQueue[activeItemIdx].values;
const nextValues = {};
let highestValue = 0;
// 处理数据,用作最后渲染(各种样式,颜色)
roundData.map((c) => {
nextValues[c.id] = {
...c,
color: c.color || (currentValues[c.id] || {}).color || getRandomColor()
};
if (Math.abs(c.value) > highestValue) {
highestValue = Math.abs(c.value);
}
return c;
});
// 属性的操作,触发useEffect
setCurrentValues(nextValues);
setHighestValue(highestValue);
setActiveItemIdx(activeItemIdx + 1);
}
// 触发下一步,循环
function nextStep (firstRun = false) {
setFirstRun(firstRun);
setNextValues();
}
对应useEffect
:
// 取原始数据
useEffect(() => {
setDataQueue(props.data);
}, []);
// 触发动态
useEffect(() => {
start();
}, [dataQueue]);
// 设触发动态间隔
useEffect(() => {
iterationTimeoutHolder = window.setTimeout(nextStep, 1000);
return () => {
if (iterationTimeoutHolder) {
window.clearTimeout(iterationTimeoutHolder);
}
};
}, [activeItemIdx]);
useEffect
示例:
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
为什么要在 effect
中返回一个函数?
这是 effect
可选的清除机制。每个 effect
都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。
Dom
的数据const keys = Object.keys(currentValues);
const { barGapSize, barHeight, showTitle } = props;
const maxValue = highestValue / 0.85;
const sortedCurrentValues = keys.sort((a, b) => currentValues[b].value - currentValues[a].value);
const currentItem = dataQueue[activeItemIdx - 1] || {};
keys
: 每组数据的索引maxValue
: 图表最大宽度sortedCurrentValues
: 对每组数据进行排序,该项影响动态渲染。currentItem
: 每组的原始数据大致的逻辑就是:
Props
,循环排列后的数据:sortedCurrentValues
label
、bar
、value
transform
。<div className="live-chart">
{
<React.Fragment>
{
showTitle &&
<h1>{currentItem.name}</h1>
}
<section className="chart">
<div className="chart-bars" style={{ height: (barHeight + barGapSize) * keys.length }}>
{
sortedCurrentValues.map((key, idx) => {
const currentValueData = currentValues[key];
const value = currentValueData.value
let width = Math.abs((value / maxValue * 100));
let widthStr;
if (isNaN(width) || !width) {
widthStr = '1px';
} else {
widthStr = `${width}%`;
}
return (
<div className={`bar-wrapper`} style={{ transform: translateY((barHeight + barGapSize) * idx), transitionDuration: 200 / 1000 }} key={`bar_${key}`}>
<label>
{
!currentValueData.label
? key
: currentValueData.label
}
</label>
<div className="bar" style={{ height: barHeight, width: widthStr, background: typeof currentValueData.color === 'string' ? currentValueData.color : `linear-gradient(to right, ${currentValueData.color.join(',')})` }} />
<span className="value" style={{ color: typeof currentValueData.color === 'string' ? currentValueData.color : currentValueData.color[0] }}>{currentValueData.value}</span>
</div>
);
})
}
</div>
</section>
</React.Fragment>
}
</div>
propTypes
和defaultProps
:DynamicBarChart.propTypes = {
showTitle: PropTypes.bool,
iterationTimeout: PropTypes.number,
data: PropTypes.array,
startRunningTimeout: PropTypes.number,
barHeight: PropTypes.number,
barGapSize: PropTypes.number,
baseline: PropTypes.number,
};
DynamicBarChart.defaultProps = {
showTitle: true,
iterationTimeout: 200,
data: [],
startRunningTimeout: 0,
barHeight: 50,
barGapSize: 20,
baseline: null,
};
export {
DynamicBarChart
};
import React, { Component } from "react";
import { DynamicBarChart } from "./DynamicBarChart";
import helpers from "./helpers";
import mocks from "./mocks";
import "react-dynamic-charts/dist/index.css";
export default class App extends Component {
render() {
return (
<DynamicBarChart
barGapSize={10}
data={helpers.generateData(100, mocks.defaultChart, {
prefix: "Iteration"
})}
iterationTimeout={100}
showTitle={true}
startRunningTimeout={2500}
/>
)
}
}
Mock
数据function getRandomNumber(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
};
function generateData(iterations = 100, defaultValues = [], namePrefix = {}, maxJump = 100) {
const arr = [];
for (let i = 0; i <= iterations; i++) {
const values = defaultValues.map((v, idx) => {
if (i === 0 && typeof v.value === 'number') {
return v;
}
return {
...v,
value: i === 0 ? this.getRandomNumber(1, 1000) : arr[i - 1].values[idx].value + this.getRandomNumber(0, maxJump)
}
});
arr.push({
name: `${namePrefix.prefix || ''} ${(namePrefix.initialValue || 0) + i}`,
values
});
}
return arr;
};
export default {
getRandomNumber,
generateData
}
mocks.js
:
import helpers from './helpers';
const defaultChart = [
{
id: 1,
label: 'Google',
value: helpers.getRandomNumber(0, 50)
},
{
id: 2,
label: 'Facebook',
value: helpers.getRandomNumber(0, 50)
},
{
id: 3,
label: 'Outbrain',
value: helpers.getRandomNumber(0, 50)
},
{
id: 4,
label: 'Apple',
value: helpers.getRandomNumber(0, 50)
},
{
id: 5,
label: 'Amazon',
value: helpers.getRandomNumber(0, 50)
},
];
export default {
defaultChart,
}
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import './styles.scss';
const getRandomColor = () => {
const letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)]
}
return color;
};
const translateY = (value) => {
return `translateY(${value}px)`;
}
const DynamicBarChart = (props) => {
const [dataQueue, setDataQueue] = useState([]);
const [activeItemIdx, setActiveItemIdx] = useState(0);
const [highestValue, setHighestValue] = useState(0);
const [currentValues, setCurrentValues] = useState({});
const [firstRun, setFirstRun] = useState(false);
let iterationTimeoutHolder = null;
function start () {
if (activeItemIdx > 1) {
return;
}
nextStep(true);
}
function setNextValues () {
if (!dataQueue[activeItemIdx]) {
iterationTimeoutHolder = null;
return;
}
const roundData = dataQueue[activeItemIdx].values;
const nextValues = {};
let highestValue = 0;
roundData.map((c) => {
nextValues[c.id] = {
...c,
color: c.color || (currentValues[c.id] || {}).color || getRandomColor()
};
if (Math.abs(c.value) > highestValue) {
highestValue = Math.abs(c.value);
}
return c;
});
console.table(highestValue);
setCurrentValues(nextValues);
setHighestValue(highestValue);
setActiveItemIdx(activeItemIdx + 1);
}
function nextStep (firstRun = false) {
setFirstRun(firstRun);
setNextValues();
}
useEffect(() => {
setDataQueue(props.data);
}, []);
useEffect(() => {
start();
}, [dataQueue]);
useEffect(() => {
iterationTimeoutHolder = window.setTimeout(nextStep, 1000);
return () => {
if (iterationTimeoutHolder) {
window.clearTimeout(iterationTimeoutHolder);
}
};
}, [activeItemIdx]);
const keys = Object.keys(currentValues);
const { barGapSize, barHeight, showTitle, data } = props;
console.table('data', data);
const maxValue = highestValue / 0.85;
const sortedCurrentValues = keys.sort((a, b) => currentValues[b].value - currentValues[a].value);
const currentItem = dataQueue[activeItemIdx - 1] || {};
return (
<div className="live-chart">
{
<React.Fragment>
{
showTitle &&
<h1>{currentItem.name}</h1>
}
<section className="chart">
<div className="chart-bars" style={{ height: (barHeight + barGapSize) * keys.length }}>
{
sortedCurrentValues.map((key, idx) => {
const currentValueData = currentValues[key];
const value = currentValueData.value
let width = Math.abs((value / maxValue * 100));
let widthStr;
if (isNaN(width) || !width) {
widthStr = '1px';
} else {
widthStr = `${width}%`;
}
return (
<div className={`bar-wrapper`} style={{ transform: translateY((barHeight + barGapSize) * idx), transitionDuration: 200 / 1000 }} key={`bar_${key}`}>
<label>
{
!currentValueData.label
? key
: currentValueData.label
}
</label>
<div className="bar" style={{ height: barHeight, width: widthStr, background: typeof currentValueData.color === 'string' ? currentValueData.color : `linear-gradient(to right, ${currentValueData.color.join(',')})` }} />
<span className="value" style={{ color: typeof currentValueData.color === 'string' ? currentValueData.color : currentValueData.color[0] }}>{currentValueData.value}</span>
</div>
);
})
}
</div>
</section>
</React.Fragment>
}
</div>
);
};
DynamicBarChart.propTypes = {
showTitle: PropTypes.bool,
iterationTimeout: PropTypes.number,
data: PropTypes.array,
startRunningTimeout: PropTypes.number,
barHeight: PropTypes.number,
barGapSize: PropTypes.number,
baseline: PropTypes.number,
};
DynamicBarChart.defaultProps = {
showTitle: true,
iterationTimeout: 200,
data: [],
startRunningTimeout: 0,
barHeight: 50,
barGapSize: 20,
baseline: null,
};
export {
DynamicBarChart
};
styles.scss
:
.live-chart {
width: 100%;
padding: 20px;
box-sizing: border-box;
position: relative;
text-align: center;
h1 {
font-weight: 700;
font-size: 60px;
text-transform: uppercase;
text-align: center;
padding: 20px 10px;
margin: 0;
}
.chart {
position: relative;
margin: 20px auto;
}
.chart-bars {
position: relative;
width: 100%;
}
.bar-wrapper {
display: flex;
flex-wrap: wrap;
align-items: center;
position: absolute;
top: 0;
left: 0;
transform: translateY(0);
transition: transform 0.5s linear;
padding-left: 200px;
box-sizing: border-box;
width: 100%;
justify-content: flex-start;
label {
position: absolute;
height: 100%;
width: 200px;
left: 0;
padding: 0 10px;
box-sizing: border-box;
text-align: right;
top: 50%;
transform: translateY(-50%);
font-size: 16px;
font-weight: 700;
display: flex;
justify-content: flex-end;
align-items: center;
}
.value {
font-size: 16px;
font-weight: 700;
margin-left: 10px;
}
.bar {
width: 0%;
transition: width 0.5s linear;
}
}
}
原项目地址:react-dynamic-charts
一直对实现动态排行榜可视化感兴趣,无奈多数都是基于D3
或echarts
实现。
而这个库,不仅脱离图形库,还使用了React 16
的新特性。也让我彻底理解了React Hook
的妙用。
如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:
你将在该篇学到:
React Hooks
函数组件useState
、useEffect
、useRef
是如何替代原生命周期和Ref
的。dragover
、dragenter
、drop
、dragleave
React Hooks
编写自己的UI组件库。文章讲了React
拖拽上传的精简实现,但直接翻译照搬显然不是我的风格。
于是我又用React Hooks
重写了一版,除CSS
的代码总数 120
行。
效果如下:
app.js
import React from 'react';
import PropTypes from 'prop-types';
import { FilesDragAndDrop } from '../components/Common/FilesDragAndDropHook';
export default class App extends React.Component {
static propTypes = {};
onUpload = (files) => {
console.log(files);
};
render() {
return (
<div>
<FilesDragAndDrop
onUpload={this.onUpload}
/>
</div>
);
}
}
FilesDragAndDrop.js(非Hooks):
import React from 'react';
import PropTypes from 'prop-types';
import '../../scss/components/Common/FilesDragAndDrop.scss';
export default class FilesDragAndDrop extends React.Component {
static propTypes = {
onUpload: PropTypes.func.isRequired,
};
render() {
return (
<div className='FilesDragAndDrop__area'>
传下文件试试?
<span
role='img'
aria-label='emoji'
className='area__icon'
>
😎
</span>
</div>
);
}
}
Hooks
组件?Hooks
版组件属于函数组件,将以上改造:
import React, { useEffect, useState, useRef } from "react";
import PropTypes from 'prop-types';
import classNames from 'classnames';
import classList from '../../scss/components/Common/FilesDragAndDrop.scss';
const FilesDragAndDrop = (props) => {
return (
<div className='FilesDragAndDrop__area'>
传下文件试试?
<span
role='img'
aria-label='emoji'
className='area__icon'
>
😎
</span>
</div>
);
}
FilesDragAndDrop.propTypes = {
onUpload: PropTypes.func.isRequired,
children: PropTypes.node.isRequired,
count: PropTypes.number,
formats: PropTypes.arrayOf(PropTypes.string)
}
export { FilesDragAndDrop };
FilesDragAndDrop.scss
.FilesDragAndDrop {
.FilesDragAndDrop__area {
width: 300px;
height: 200px;
padding: 50px;
display: flex;
align-items: center;
justify-content: center;
flex-flow: column nowrap;
font-size: 24px;
color: #555555;
border: 2px #c3c3c3 dashed;
border-radius: 12px;
.area__icon {
font-size: 64px;
margin-top: 20px;
}
}
}
从操作DOM、组件复用、事件触发、阻止默认行为、以及Hooks
应用方面分析。
useRef
由于需要拖拽文件上传以及操作组件实例,需要用到ref
属性。
React Hooks
中 新增了useRef API
语法
const refContainer = useRef(initialValue);
useRef
返回一个可变的 ref
对象,。initialValue
)...
const drop = useRef();
return (
<div
ref={drop}
className='FilesDragAndDrop'
/>
...
)
dragleave
, 离开范围dragenter
,用来确定放置目标是否接受放置。dragover
,用来确定给用户显示怎样的反馈信息drop
,允许放置对象。这四个事件并存,才能阻止 Web 浏览器默认行为和形成反馈。
代码很简单:
e.preventDefault() //阻止事件的默认行为(如在浏览器打开文件)
e.stopPropagation() // 阻止事件冒泡
每个事件阶段都需要阻止,为啥呢?举个🌰栗子:
const handleDragOver = (e) => {
// e.preventDefault();
// e.stopPropagation();
};
不阻止的话,就会触发打开文件的行为,这显然不是我们想看到的。
useState
拖拽上传组件,除了基础的拖拽状态控制,还应有成功上传文件或未通过验证时的消息提醒。
状态组成应为:
state = {
dragging: false,
message: {
show: false,
text: null,
type: null,
},
};
写成对应useState
前先回归下写法:
const [属性, 操作属性的方法] = useState(默认值);
于是便成了:
const [dragging, setDragging] = useState(false);
const [message, setMessage] = useState({ show: false, text: null, type: null });
除了drop
事件,另外三个事件都是动态变化的,而在拖动元素时,每隔 350
毫秒会触发 dragover
事件。
此时就需要第二ref
来统一控制。
所以全部的`ref``为:
const drop = useRef(); // 落下层
const drag = useRef(); // 拖拽活动层
我们在应用组件时,prop
需要传入类型和数量来控制
<FilesDragAndDrop
onUpload={this.onUpload}
count={1}
formats={['jpg', 'png']}
>
<div className={classList['FilesDragAndDrop__area']}>
传下文件试试?
<span
role='img'
aria-label='emoji'
className={classList['area__icon']}
>
😎
</span>
</div>
</FilesDragAndDrop>
onUpload
:拖拽完成处理事件count
: 数量控制formats
: 文件类型。对应的组件Drop
内部事件:handleDrop
:
const handleDrop = (e) => {
e.preventDefault();
e.stopPropagation();
setDragging(false)
const { count, formats } = props;
const files = [...e.dataTransfer.files];
if (count && count < files.length) {
showMessage(`抱歉,每次最多只能上传${count} 文件。`, 'error', 2000);
return;
}
if (formats && files.some((file) => !formats.some((format) => file.name.toLowerCase().endsWith(format.toLowerCase())))) {
showMessage(`只允许上传 ${formats.join(', ')}格式的文件`, 'error', 2000);
return;
}
if (files && files.length) {
showMessage('成功上传!', 'success', 1000);
props.onUpload(files);
}
};
.endsWith
是判断字符串结尾,如:"abcd".endsWith("cd"); // true
showMessage
则是控制显示文本:
const showMessage = (text, type, timeout) => {
setMessage({ show: true, text, type, })
setTimeout(() =>
setMessage({ show: false, text: null, type: null, },), timeout);
};
需要触发定时器来回到初始状态
原本EventListener
的事件需要在componentDidMount
添加,在componentWillUnmount
中销毁:
componentDidMount () {
this.drop.addEventListener('dragover', this.handleDragOver);
}
componentWillUnmount () {
this.drop.removeEventListener('dragover', this.handleDragOver);
}
但Hooks
中有内部操作方法和对应useEffect
来取代上述两个生命周期
useEffect
示例:
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
而 每个effect
都可以返回一个清除函数。如此可以将添加(componentDidMount
)和移除(componentWillUnmount
) 订阅的逻辑放在一起。
于是上述就可以写成:
useEffect(() => {
drop.current.addEventListener('dragover', handleDragOver);
return () => {
drop.current.removeEventListener('dragover', handleDragOver);
}
})
FilesDragAndDropHook.js
:
import React, { useEffect, useState, useRef } from "react";
import PropTypes from 'prop-types';
import classNames from 'classnames';
import classList from '../../scss/components/Common/FilesDragAndDrop.scss';
const FilesDragAndDrop = (props) => {
const [dragging, setDragging] = useState(false);
const [message, setMessage] = useState({ show: false, text: null, type: null });
const drop = useRef();
const drag = useRef();
useEffect(() => {
// useRef 的 drop.current 取代了 ref 的 this.drop
drop.current.addEventListener('dragover', handleDragOver);
drop.current.addEventListener('drop', handleDrop);
drop.current.addEventListener('dragenter', handleDragEnter);
drop.current.addEventListener('dragleave', handleDragLeave);
return () => {
drop.current.removeEventListener('dragover', handleDragOver);
drop.current.removeEventListener('drop', handleDrop);
drop.current.removeEventListener('dragenter', handleDragEnter);
drop.current.removeEventListener('dragleave', handleDragLeave);
}
})
const handleDragOver = (e) => {
e.preventDefault();
e.stopPropagation();
};
const handleDrop = (e) => {
e.preventDefault();
e.stopPropagation();
setDragging(false)
const { count, formats } = props;
const files = [...e.dataTransfer.files];
if (count && count < files.length) {
showMessage(`抱歉,每次最多只能上传${count} 文件。`, 'error', 2000);
return;
}
if (formats && files.some((file) => !formats.some((format) => file.name.toLowerCase().endsWith(format.toLowerCase())))) {
showMessage(`只允许上传 ${formats.join(', ')}格式的文件`, 'error', 2000);
return;
}
if (files && files.length) {
showMessage('成功上传!', 'success', 1000);
props.onUpload(files);
}
};
const handleDragEnter = (e) => {
e.preventDefault();
e.stopPropagation();
e.target !== drag.current && setDragging(true)
};
const handleDragLeave = (e) => {
e.preventDefault();
e.stopPropagation();
e.target === drag.current && setDragging(false)
};
const showMessage = (text, type, timeout) => {
setMessage({ show: true, text, type, })
setTimeout(() =>
setMessage({ show: false, text: null, type: null, },), timeout);
};
return (
<div
ref={drop}
className={classList['FilesDragAndDrop']}
>
{message.show && (
<div
className={classNames(
classList['FilesDragAndDrop__placeholder'],
classList[`FilesDragAndDrop__placeholder--${message.type}`],
)}
>
{message.text}
<span
role='img'
aria-label='emoji'
className={classList['area__icon']}
>
{message.type === 'error' ? <>😢</> : <>😘</>}
</span>
</div>
)}
{dragging && (
<div
ref={drag}
className={classList['FilesDragAndDrop__placeholder']}
>
请放手
<span
role='img'
aria-label='emoji'
className={classList['area__icon']}
>
😝
</span>
</div>
)}
{props.children}
</div>
);
}
FilesDragAndDrop.propTypes = {
onUpload: PropTypes.func.isRequired,
children: PropTypes.node.isRequired,
count: PropTypes.number,
formats: PropTypes.arrayOf(PropTypes.string)
}
export { FilesDragAndDrop };
App.js
:
import React, { Component } from 'react';
import { FilesDragAndDrop } from '../components/Common/FilesDragAndDropHook';
import classList from '../scss/components/Common/FilesDragAndDrop.scss';
export default class App extends Component {
onUpload = (files) => {
console.log(files);
};
render () {
return (
<FilesDragAndDrop
onUpload={this.onUpload}
count={1}
formats={['jpg', 'png', 'gif']}
>
<div className={classList['FilesDragAndDrop__area']}>
传下文件试试?
<span
role='img'
aria-label='emoji'
className={classList['area__icon']}
>
😎
</span>
</div>
</FilesDragAndDrop>
)
}
}
FilesDragAndDrop.scss
:
.FilesDragAndDrop {
position: relative;
.FilesDragAndDrop__placeholder {
position: absolute;
top: 0;
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.