GithubHelp home page GithubHelp logo

varharrie / varharrie.github.io Goto Github PK

View Code? Open in Web Editor NEW
3.6K 3.6K 541.0 1.36 MB

:blue_book: Personal blog site based on github issues.

Home Page: https://varharrie.github.io

License: MIT License

HTML 0.62% Shell 0.61% JavaScript 0.18% TypeScript 98.59%
blog issues react

varharrie.github.io's Introduction

varHarrie's Blog

Personal blog site based on github issues.

Features

  • 💪 No need backend server
  • 📱 Mobile compatible
  • 🌙 Supports dark theme
  • 🌏 Supports i18n
  • ⚾︎ Integrates code playground

Getting started

  1. Clone the repository: git clone https://github.com/varHarrie/varharrie.github.io.git
  2. Install dependencies: yarn
  3. Rename .env.example to .env.local and Modify its content
  4. Start dev server: yarn run dev

License

MIT

varharrie.github.io's People

Contributors

varharrie avatar

Stargazers

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

Watchers

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

varharrie.github.io's Issues

[Rxjs] Combination Operators

Combination Operators

目录

combineLatest

combineLatest(...obserables: Observable[], project: (...values: any[]) => any)

  • 当所有Observable都送出第一个值时,开始送出
  • 之后每个Observable送出时,继续送出
  • 将所有Observable最新值,传入project(),最终推送值为project()返回值
const source = Rx.Observalbe.interval(500).take(3)
const newest = Rx.Observable.interval(300).take(6)

const example = source.combineLatest(newest, (x, y) => x + y)
example.subscribe((value) => console.log(value))
source : ----0----1----2|
newest : --0--1--2--3--4--5|

    combineLatest(newest, (x, y) => x + y)

example: ----01--23-4--(56)--7|

zip

zip(...obserables: Observable[], project: (...values: any[]) => any)

  • 当所有Observable都送出第一个值时,开始送出
  • 之后当所有Observable都送出第i个值时,继续送出
  • 将所有Observable第i个值,传入project(),最终推送值为project()返回值
  • **注意:**当Observable之间的推送时间差距太大,中间会cache大量的值
const source = Rx.Observalbe.interval(500).take(3)
const newest = Rx.Observable.interval(300).take(6)

const example = source.zip(newest, (x, y) => x + y)
example.subscribe((value) => console.log(value))
source : ----0----1----2|
newest : --0--1--2--3--4--5|
    zip(newest, (x, y) => x + y)
example: ----0----2----4|

withLatestFrom

withLatestFrom(...obserables: Observable[], project: (...values: any[]) => any)

  • 当所有Observable都送出第一个值时,开始送出
  • 之后只有源Obsrvable送出值时,继续送出
  • 将所有Observable最新值,传入project(),最终推送值为project()返回值
const source = Rx.Observalbe.interval(500).take(3)
const newest = Rx.Observable.interval(300).take(6)

const example = source.withLatestFrom(newest, (x, y) => x + y)
example.subscribe((value) => console.log(value))
source : ----0----1----2|
newest : --0--1--2--3--4--5|
    withLatestFrom(newest, (x, y) => x + y)
example: ----0----3----6|

参考资料

RxJS Operators 详解
30 天精通 RxJS

让 Table 中的 div 占满整个单元格

<html lang="en">
<head>
  <style>
    table tr {
      height: 1px;
    }

    table tr td {
      height: inherit;
    }

    table tr td div {
      height: 100%;
      background: #ddd;
    }
  </style>
</head>
<body>
  <table>
    <tbody>
      <tr>
        <div>aaaaaaaaaaaaaaaaaaaaaaa</div>
      </tr>
      <tr>
        <div>bbbbbbbbbbbbbbbbbbbbbbb</div>
      </tr>
      <tr>
        <div>aaaaaaaaaaaaaaaaaaaaaaa</div>
      </tr>
      <tr>
        <div>aaaaaaaaaaaaaaaaaaaaaaa</div>
      </tr>
      <tr>
        <div>aaaaaaaaaaaaaaaaaaaaaaa</div>
      </tr>
      <tr>
        <div>aaaaaaaaaaaaaaaaaaaaaaa</div>
      </tr>
    </tbody>
  </table>
</body>
</html>

判断是否滚动到div底部

几个概念:

clientHeight 可见高度
offsetHeight 可见高度+边框
scrollHeight 内容高度
scrollTop 滚动高度

判断是否滚动到底部:

clientHeight + scrollTop === scrollHeight

判断是否距底部100像素以内:

scrollHeight - clientHeight - scrollTop < 100

Rust笔记:从入门到再次入门

笔记内容大多引用于:

1、Cargo常用命令

# 创建项目
cargo new <project_name>

# 创建库
cargo new --lib <library_name>

# 构建项目
cargo build

# 构建并运行项目
cargo run

# 检查代码
cargo check

# 发布构建
cargo build --release

2、变量声明

let a = 5;     // 不可变变量
a = 6;         // Error

let mut b = 5; // 可变变量
b = 6;         // OK

let spaces = "     ";
let spaces = spaces.len(); // Shadowing,隐藏先前的变量
println!("{}", spaces ); // 5

3、基本标量数据类型

(1)整形

长度 有符号 无符号
8-bit i8 u8
16-bit i16 u16
32-bit i32(默认) u32
64-bit i64 u64
128-bit i128 u128
arch(依赖计算机架构) isize usize
取值范围 ⁍ ~ ⁍ ⁍ ~ ⁍
数字字面值 例子
Decimal(十进制) 98_222
Hex(十六进制) 0xff
Octal(八进制) 0o77
Binary(二进制) 0b1111_0000
Byte(单字节字符,仅限于u8) b'A'

整形溢出:当在 debug 模式编译时,Rust 检查这类问题并使程序 *panic。*在 release 构建中,Rust 不检测溢出,相反会进行一种被称为二进制补码包装(two’s complement wrapping
)的操作。

(2)浮点型

浮点数采用 IEEE-754 标准表示。

长度 类型
32-bit f32 单精度
64-bit f64(默认) 双精度

(3)数值运算

fn main() {
    // 加法
    let add = 5 + 10;

    // 减法
    let subtract = 95.5 - 4.3;

    // 乘法
    let multiple = 4 * 30;

    // 除法
    let divide = 56.7 / 32.2;
    let divide_floored = 2 / 3; // 整数除法会向下取整

    // 取余
    let remain = 43 % 5;
}

(4)布尔型

fn main() {
    let t = true;
    let f: bool = false;
}

(5)字符型

fn main() {
    let char = 'a';
    let emoji = '😻'; // 4 bytes Unicode

    let str = "Hello World";
}

(6)元组类型

fn main() {
    let tup = (500, 6.4, "hello");
    let tup: (i32, f64, &str) = (500, 6.4, "hello");

    // 解构
    let (x, y, z) = tup;
    let (first, _, _) = tup;

    println!("x: {}, y: {}, z: {}", x, y, z);
    println!("x: {}, y: {}, z: {}", tup.0, tup.1, tup.2);
    println!("first: {}", first);
}

(7)数组类型

数组中的每个元素的类型必须相同,数组长度是固定的。

如需不固定长度,使用标准库提供的Vector 类型,一个允许增长和缩小长度的类似数组的集合类型。

fn main() {
    let a = [1, 2, 3, 4, 5];
    let a: [i32; 5] = [1, 2, 3, 4, 5];

    let first = a[0];
    let second = a[1];
}

4、函数

几个概念:

  • 字面量:5(整形字面量)、”hello”(字符串字面量)……
  • 表达式:10 5 + 6x + 1 ……(会产生一个值)
  • 语句:let i = 5; x + 1; (以;结束,没有返回值,不能作为表达式使用)……

函数由一系列语句组成,最后由一个可选的表达式结束。

fn five () -> i32 {
    5
}

fn add(x: i32, y: i32) -> i32 {
    return x + y;
}

fn foo() -> i32 {
    let x = 5;
    x + 1; // 分号结束是语句,不能作为返回值
    x + 1 // 没有分号,作为表达式
}

fn bar() -> i32 {
    // 代码块也是表达式
    {
        let i = 5;
        i + 2 // 表达式,作为代码块的返回值
    }
}

5、条件

fn main() {
    let number = 3;

    if number < 5 {
        println!("condition was true");
    } else {
        println!("condition was false");
    }

    let number = 6;

    if number % 4 == 0 {
        println!("number is divisible by 4");
    } else if number % 3 == 0 {
        println!("number is divisible by 3");
    } else if number % 2 == 0 {
        println!("number is divisible by 2");
    } else {
        println!("number is not divisible by 4, 3, or 2");
    }
}
fn main() {
    let n = 5;
    let m = 10;
    let max = if n > m { n } else { m };

    println!("{}", max);
}

6、循环

(1)loop

fn main() {
    loop {
        println!("again!");
        // break; // 退出循环
        // continue; // 进入下一次循环
    }
}
fn main() {
    'outer: loop {
        loop {
            break 'outer; // 通过循环标签
        }
    }
}
fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2; // 返回循环的值
        }
    };

    println!("The result is {}", result);
}

(2)while

fn main() {
    let mut number = 3;

    while number != 0 {
        println!("{}!", number);
        number -= 1;
    }

    println!("LIFTOFF!!!");
}

(3)for

fn main() {
    let arr = [10, 20, 30, 40, 50];

    for el in arr.iter() { // iter 迭代器每个元素 el 都是引用类型
        println!("the value is: {}", el);
    }
}
fn main() {
  for num in (1..4).rev() { // rev 反转元素顺序
      println!("the value is: {}", num);
  }
}

7、栈(Stack)与堆(Heap)

在很多语言中,你并不需要经常考虑到栈与堆。不过在像 Rust 这样的系统编程语言中,值是位于栈上还是堆上在更大程度上影响了语言的行为以及为何必须做出这样的抉择。我们会在本章的稍后部分描述所有权与栈和堆相关的内容,所以这里只是一个用来预热的简要解释。

栈和堆都是代码在运行时可供使用的内存,但是它们的结构不同。栈以放入值的顺序存储值并以相反顺序取出值。这也被称作 后进先出(last in, first out)。想象一下一叠盘子:当增加更多盘子时,把它们放在盘子堆的顶部,当需要盘子时,也从顶部拿走。不能从中间也不能从底部增加或拿走盘子!增加数据叫做 进栈(pushing onto the stack),而移出数据叫做 出栈(popping off the stack)。栈中的所有数据都必须占用已知且固定的大小。在编译时大小未知或大小可能变化的数据,要改为存储在堆上。 堆是缺乏组织的:当向堆放入数据时,你要请求一定大小的空间。内存分配器(memory allocator)在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的 指针(pointer)。这个过程称作 在堆上分配内存(allocating on the heap),有时简称为 “分配”(allocating)。将数据推入栈中并不被认为是分配。因为指针的大小是已知并且固定的,你可以将指针存储在栈上,不过当需要实际数据时,必须访问指针。想象一下去餐馆就座吃饭。当进入时,你说明有几个人,餐馆员工会找到一个够大的空桌子并领你们过去。如果有人来迟了,他们也可以通过询问来找到你们坐在哪。

入栈比在堆上分配内存要快,因为(入栈时)分配器无需为存储新数据去搜索内存空间;其位置总是在栈顶。相比之下,在堆上分配内存则需要更多的工作,这是因为分配器必须首先找到一块足够存放数据的内存空间,并接着做一些记录为下一次分配做准备。

访问堆上的数据比访问栈上的数据慢,因为必须通过指针来访问。现代处理器在内存中跳转越少就越快(缓存)。继续类比,假设有一个服务员在餐厅里处理多个桌子的点菜。在一个桌子报完所有菜后再移动到下一个桌子是最有效率的。从桌子 A 听一个菜,接着桌子 B 听一个菜,然后再桌子 A,然后再桌子 B 这样的流程会更加缓慢。出于同样原因,处理器在处理的数据彼此较近的时候(比如在栈上)比较远的时候(比如可能在堆上)能更好的工作。在堆上分配大量的空间也可能消耗时间。

当你的代码调用一个函数时,传递给函数的值(包括可能指向堆上数据的指针)和函数的局部变量被压入栈中。当函数结束时,这些值被移出栈。

跟踪哪部分代码正在使用堆上的哪些数据,最大限度的减少堆上的重复数据的数量,以及清理堆上不再使用的数据确保不会耗尽空间,这些问题正是所有权系统要处理的。一旦理解了所有权,你就不需要经常考虑栈和堆了,不过明白了所有权的主要目的就是为了管理堆数据,能够帮助解释为什么所有权要以这种方式工作。

上面提到的基础标量数据类型存放在上。其他复杂数据类型存放在上。

8、所有权

所有权是针对堆数据产生的,规则如下:

  • 每一个值都有一个被称为其**所有者(Owner)**的变量。
  • 值在任一时刻有且只有一个所有者。
  • 当所有者(变量)离开作用域,这个值将被丢弃。
fn main() {
	// Stack:基础标量类型将进行复制,即 Copy
	let x = 5;
	let y = x;
	
	println!("x: {}, y: {}", x, y); // OK
	
	// Heap:复杂数据类型将进行所有权转移,即 Move
	let s1 = String::from("hello");
	let s2 = s1;
	
	println!("{}, world!", s1); // ERROR,s1 已失效
	
	let s2 = s1.clone(); // 如必须要复制,使用 clone 函数,可能相当消耗资源
}
fn main() {
    let s = String::from("hello");  // s 进入作用域
    takes_ownership(s);             // s 的值移动到函数里
    println!("{}", s);              // ERROR,当前作用域不再有效

    let x = 5;                      // x 进入作用域
    makes_copy(x);                  // x 应该移动函数里,但 i32 是 Copy 的
    println!("{}", x);              // 所以在后面可继续使用 x
}

fn takes_ownership(some_string: String) { // some_string 进入作用域
    println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法,占用的内存被释放

fn makes_copy(some_integer: i32) { // some_integer 进入作用域
    println!("{}", some_integer);
} // 这里,some_integer 移出作用域。没有特殊处理

9、引用与借用

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{}' is {}.", s1, len); // s1 仍然有效
}

fn calculate_length(s: &String) -> usize {
    s.len()
} // 因为没有所有权,所以 s 内存不会释放
fn main() {
    let mut s = String::from("hello");

    change(&mut s); // 传入可变引用
}

fn change(some_string: &mut String) { // 声明可变引用
    some_string.push_str(", world");
}

对同一数据的多个可变引用,作用域不可重叠:

fn main(){
    let mut s = String::from("hello");

    let r1 = &mut s;  // OK
    let r2 = &mut s;  // ERROR,在同一时间只能有一个对某一特定数据的可变引用

    println!("{}, {}", r1, r2);
}
fn main (){
    let mut s = String::from("hello");

    {
        let r1 = &mut s;
    } // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用

    let r2 = &mut s;
}

可变引用与不可变引用作用域不可重叠:

fn main() {
    let mut s = String::from("hello");

    let r1 = &s;     // OK
    let r2 = &s;     // OK
    let r3 = &mut s; // ERROR,不可同时拥有可变引用和不可变引用

    println!("{}, {}, and {}", r1, r2, r3);
}
fn main(){
    let mut s = String::from("hello");

    let r1 = &s; // OK
    let r2 = &s; // OK
    println!("{} and {}", r1, r2);
    // 此位置之后 r1 和 r2 不再使用

    let r3 = &mut s; // OK
    println!("{}", r3);
}

10、Slice

fn main(){
    let s = String::from("hello world");
    let len = s.len();

    let hello = &s[0..5];
    let world = &s[6..11];

    // 等价
    let slice = &s[0..2];
    let slice = &s[..2];

    // 等价
    let slice = &s[3..len];
    let slice = &s[3..];
    
    // 等价
    let slice = &s[0..len];
    let slice = &s[..];
}
fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s); // word 为 s 的一个不可变引用
    s.clear(); // ERROR,clear 尝试获取可变引用,同时存在不可变引用

    println!("the first word is: {}", word); // word 到此依然有效
}

字符串字面值就是 slice,即&str,一个不可变引用。

fn first_word(s: &str) -> &str {
    // ...
}

fn main() {
		let my_string_literal = "hello world";

    let word = first_word(&my_string_literal[0..6]);  // OK
    let word = first_word(&my_string_literal[..]);  // OK
    let word = first_word(my_string_literal);  // OK

    let my_string = String::from("hello world");

    let word = first_word(&my_string[0..6]);  // OK
    let word = first_word(&my_string[..]);  // OK
    let word = first_word(&my_string);  // OK
}

11、结构体

(1)基本用法

// 结构体定义
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    // 实例化
    let mut user1 = User {
        email: String::from("[email protected]"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };

    // 属性赋值
    user1.email = String::from("[email protected]");

    // 从已有的实例创建新实例
    let user2 = User {
        email: String::from("[email protected]"),
        ..user1 // 此时 username、email 所有权转移到 user2 的对应属性中,user1将失效
    };
}

(2)元组结构体

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);
}

(3)类单元结构体

struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

(4)结构体方法(关联函数)

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }

    // 可实现与属性同名方法
    fn width(&self) -> bool {
        self.width > 0
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    
    // 使用.调用方法
    println!("The area of the rectangle is {} square pixels.", rect1.area());
}

(5)结构体非关联函数

impl Rectangle {
    fn square(size: u32) -> Rectangle {
        Rectangle {
            width: size,
            height: size,
        }
    }
}

fn main() {
    // 使用::调用非关联函数
    let sq = Rectangle::square(3);
}

12、枚举

(1)基本用法

enum IpAddrKind {
    V4,
    V6,
}

fn main() {
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;
}

(2)绑定值

enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

fn main() {
    let home = IpAddr::V4(127, 0, 0, 1);
    let loopback = IpAddr::V6(String::from("::1"));
}

(3)复杂的绑定值、定义枚举方法

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

impl Message {
    fn call(&self) {
        // 在这里定义方法体
    }
}

fn main() {
    let m = Message::Write(String::from("hello"));
    m.call();
}

13、Match控制流运算符

(1)基本用法

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

// 根据硬币类型,返回美分值
fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => {                 // 可以是代码块
            println!("Lucky penny!");
            1
        }
        Coin::Nickel => 5,               // 或者直接返回值
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

(2)绑定值

enum Option<T> {
    None,
    Some(T),  // 绑定泛型值
}

fn main() {
    let nan: Option<i8> = Option::None;
    let num: Option<i8> = Option::Some(1);

    match num {
        Option::None => {
            println!("num is not a number");
        }
        Option::Some(n) => {  // 访问绑定值
            println!("nam value is {}", n);
        }
    }
}

(3)通用匹配模式

fn main() {
	let roll = 9;
	match roll {
	    3 => add_fancy_hat(),
	    7 => remove_fancy_hat(),
	    other => move_player(other),  // other 通用匹配
	    // _ => reroll(),             // 忽略绑定值
	    // _ => (),                   // 空处理
	}
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}

14、if let控制流

fn main() {
  // ===== 使用 match =====
	let num = Some(5);
	match num {
	    Some(n) => println!("{}", n),
	    _ => (),
	}

  // ===== 使用 if let 简写=====
	let num = Some(5);
  if let Some(n) = num {
      println!("{}", n);
  } // 还可以支持 else 处理其他情况
}

待续。。。

NodeJS递归遍历指定目录所有文件

const fs = require('fs');
const path = require('path');

async function traverse(dir, callback) {
  const items = await fs.promises.readdir(dir, { withFileTypes: true });

  return Promise.all(
    items.map((item) => {
      const fullPath = path.join(dir, item.name);

      if (item.isDirectory()) {
        return traverse(fullPath, callback);
      }

      if (item.isFile()) {
        return callback(fullPath);
      }

      return Promise.resolve();
    }),
  );
}

await traverse(path.join(__dirname, '../src'), (filePath) => {
  console.log(filePath);
});

[造轮子教程]十分钟搭建Webpack+Vue项目

每一步的项目源码:这里

本教程涉及技术栈和相关工具为:webpack@1,vue@1,[email protected],vuex@1,eslint

对于webpack@2和vue@2,请参考官方文档作改动

如果项目本身并没有特别需求,还是推荐使用vue-cli构建项目,方便快捷

推荐vue项目目录结构:

  • config 全局变量
  • dist 编译后的项目代码
  • src 项目源码
    • apis api封装
    • components Vue组件
    • libs js工具类
    • router 路由
      • index.js 路由对象
      • routes.js 路由配置
    • store Vuex的store
      • modules vuex模块
      • types.js type管理
    • styles css样式
    • views 页面组件
    • main.js vue入口文件
  • webpack.config Webpack各种环境的配置文件
  • package.json

第一步:初始化项目

  1. 所有项目的第一步当然是:创建项目文件夹,然后使用npm init -y创建package.json

  2. 项目根目录下建立srcdist文件夹,分别用来存放项目源码webpack编译后的代码

第二步:入口文件

  1. 根目录下直接建立一个index.html,作为页面的入口文件
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Demo</title>
</head>
<body>
  <div id="app">{{message}}</div>  <!-- Vue模板入口 -->
  <script src="dist/main.js"></script>
</body>
</html>
  1. src下建立一个main.js,作为Vue的入口文件
// import...from的语法是ES6的,需要用到babel,后面再说
// require的语法是Commonjs的,webpack已经实现了,可以直接使用
const Vue = require('vue')
new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue.js!'
  }
})
  1. 安装模块

安装Vue:npm install vue@1 --save
安装Webpack: npm install webpack --save-dev

  1. 使用webpack编译打包

除非在全局安装webpack,使用本地安装需要在package.jsonscript加入运行脚本,添加之后package.json如下:

{
  "name": "step2",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "webpack src/main.js dist/main.js"  // <---添加这句
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "vue": "^1.0.28"
  },
  "devDependencies": {
    "webpack": "^1.14.0"
  }
}

运行npm run dev,再用浏览器打开index.html就能看到效果了:

Hello Vue.js!

第三步:编写webpack配置文件

上一步中直接使用webpack运行脚本webpack [入口文件] [出口文件],显然对于后期添加webpack插件和不同环境的配置是不行的。

  1. 在项目根目录下创建webpack.config文件夹专门用于存放webpack的配置文件

  2. 为了让配置文件不同的编译环境中能够复用(例如loaders的配置,不管在开发环境还是生产环境肯定都是一样的),在webpack.confg中首先创建一个base.js文件:

const path = require('path')
const root = path.resolve(__dirname, '..') // 项目的根目录绝对路径

module.exports = {
  entry: path.join(root, 'src/main.js'),  // 入口文件路径
  output: {
    path: path.join(root, 'dist'),  // 出口目录
    filename: 'main.js'  // 出口文件名
  }
}

上面这段配置就实现了webpack src/main.js dist/main.js的功能,还可以额外拓展一下,变成:

const path = require('path')
const root = path.resolve(__dirname, '..') // 项目的根目录绝对路径

module.exports = {
  entry: path.join(root, 'src/main.js'),  // 入口文件路径
  output: {
    path: path.join(root, 'dist'),  // 出口目录
    filename: 'main.js'  // 出口文件名
  },
  resolve: {
    alias: { // 配置目录别名
      // 在任意目录下require('components/example') 相当于require('项目根目录/src/components/example')
      components: path.join(root, 'src/components'),
      views: path.join(root, 'src/views'),
      styles: path.join(root, 'src/styles'),
      store: path.join(root, 'src/store')
    },
    extensions: ['', '.js', '.vue'], // 引用js和vue文件可以省略后缀名
    fallback: [path.join(root, 'node_modules')] // 找不到的模块会尝试在这个数组的目录里面再寻找
  },
  resolveLoader: {
    fallback: [path.join(root, 'node_modules')] // 找不到的loader模块会尝试在这个数组的目录里面再寻找
  },
  module: { // 配置loader
    loaders: [
      {test: /\.vue$/, loader: 'vue'}, // 所有.vue结尾的文件,使用vue-loader
      {test: /\.js$/, loader: 'babel', exclude: /node_modules/} // .js文件使用babel-loader,切记排除node_modules目录
    ]
  }
}

根目录下添加.babelrc用于配置babel

{
  "presets": ["es2015"]
}

使用了vue-loader和babel-loader需要安装包:

npm install --save-dev vue-loader@8 babel-loader babel-core babel-plugin-transform-runtime babel-preset-es2015 css-loader vue-style-loader vue-hot-reload-api@1 vue-html-loader

  1. webpack.confg创建dev.js文件:
const path = require('path')
const webpack = require('webpack')
const merge = require('webpack-merge')
const baseConfig = require('./base')
const root = path.resolve(__dirname, '..')

module.exports = merge(baseConfig, {})

上面的代码仅仅是导出了跟base.js一模一样的配置,下面我们添加更多用于dev(开发环境)的配置。

webpack-merge 用于合并两个配置文件,需要安装

npm install --save-dev webpack-merge

  1. 使用webpack dev server,开启一个小型服务器,不需要再手动打开index.html进行调试了

修改配置文件为:

module.exports = merge(baseConfig, {
  devServer: {
    historyApiFallback: true, // 404的页面会自动跳转到/页面
    inline: true, // 文件改变自动刷新页面
    progress: true, // 显示编译进度
    colors: true, // 使用颜色输出
    port: 3000, // 服务器端口
  },
  devtool: 'source-map' // 用于标记编译后的文件与编译前的文件对应位置,便于调试
})
  1. 添加热替换配置,每次改动文件不会再整个页面都刷新

安装webpack-dev-server:npm install --save-dev webpack-dev-server

module.exports = merge(baseConfig, {
  entry: [
    'webpack/hot/dev-server', // 热替换处理入口文件
    path.join(root, 'src/index.js')
  ],
  devServer: { /* 同上 */},
  plugins: [
    new webpack.HotModuleReplacementPlugin() // 添加热替换插件
  ]
}
  1. 使用HtmlWebpackPlugin,实现js入口文件自动注入
module.exports = merge(baseConfig, {
  entry: [ /* 同上 */ ],
  devServer: { /* 同上 */ },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new HtmlWebpackPlugin({
      template: path.join(root, 'index.html'), // 模板文件
      inject: 'body' // js的script注入到body底部
    })
  ]
}

最后修改后完整的dev.js请查看源码

这里的HotModuleReplacementPluginwebpack内置的插件,不需要安装

HtmlWebpackPlugin需要自行安装:

npm install --save-dev html-webpack-plugin

在文件头中引入const HtmlWebpackPlugin = require('html-webpack-plugin')

修改index.html,去掉入口文件的引入:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Demo</title>
</head>
<body>
  <div id="app">{{message}}</div>  <!-- Vue模板入口 -->
  <!-- 去掉js入口文件 -->
</body>
</html>
  1. 最后修改package.json中的webpack运行脚本为:
{
  "dev": "webpack-dev-server --config webpack.config/dev.js"
}

为了测试webpack配置是否都生效了,下面创建一个vue组件src/components/Hello.vue

<template>
  <div>{{message}}</div>
</template>

<script>
  export default {
    data: () => ({message: 'Hello Vue.js!'})
  }
</script>

修改main.js

import Vue  from 'vue'
import Hello from './components/Hello.vue'

new Vue({
  el: '#app',
  template: '<div><hello></hello></div>',
  components: {Hello}
})

运行npm run dev,浏览器打开localhost:3000查看结果:

Hello Vue.js!

第四步:配置路由

  1. 安装vue-routernpm install --save [email protected]

  2. 创建目录

src目录下创建views文件夹,用于存放页面组件
src目录下创建router文件夹,用于存放所有路由相关的配置

  1. 添加路由页面

添加页面组件src/views/Home.vue

<template>
  <div><hello></hello></div>
</template>

<script>
  import Hello from 'components/Hello'
  export default {
    components: {Hello}
  }
</script>

添加src/router/routes.js文件,用于配置项目路由:

import Home from 'views/Home'

export default {
  '/': {
    name: 'home',
    component: Home
  }
}

添加路由入口文件src/router/index.js

import Vue from 'vue'
import Router from 'vue-router'
import routes from './routes'

Vue.use(Router)

const router = new Router({
  hashbang: false,  // 关闭hash模式
  history: true,    // 开启html5history模式
  linkActiveClass: 'active' // v-link激活时添加的class,默认是`v-link-active`
})

router.map(routes)

router.beforeEach(({to, next}) => {
  console.log('---------> ' + to.name)  // 每次调整路由时打印,便于调试
  next()
})

export default router

修改main.js

import Vue  from 'vue'
import router from './router'

const App = Vue.extend({})

router.start(App, '#app')

最后别忘了编辑index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Demo</title>
</head>
<body>
  <div id="app">
    <router-view></router-view><!--路由替换位置-->
  </div>
</body>
</html>

重新执行npm run dev,浏览器打开localhost:3000查看效果

第五步:配置Vuex

vuex通常用于存放和管理不同组件中的共用状态,例如不同路由页面之间的公共数据

vuex中的几个概念:

state:状态,即数据

store:数据的集合,一个vuex引用,仅有一个store,包含n个state

mutation:state不能直接赋值,通过mutation定义最基本的操作

action:在action中调用一个或多个mutation

getter:state不能直接取值,使用getter返回需要的state

module:store和state之间的一层,便于大型项目管理,store包含多个module,module包含state、mutation和action

本教程中将以一个全局计数器作为例子

  1. 安装vuex

安装vuexnpm install --save vuex@1
添加src/store文件夹,存放vuex相关文件,添加src/store/modules用于vuex分模块管理

  1. 添加src/store/types.js,vuex的所有mutation type都放在一起,不建议分开多个文件,有效避免重名情况:
export const INCREASE = 'INCREASE' // 累加
export const RESET = 'RESET' // 清零
  1. 编写vuex模块,添加counter模块目录store/modules/counter

添加store/modules/counter/actions.js

import {INCREASE, RESET} from 'store/types'

export const increase = (({dispatch}) => {
  dispatch(INCREASE) // 调用type为INCREASE的mutation
})

export const reset = (({dispatch}) => {
  dispatch(RESET) // 调用type为RESET的mutation
})

添加store/modules/counter/index.js

import{INCREASE, RESET} from 'store/types.js'

const state = {
  count: 0
}

const mutations = {
  [INCREASE] (state) { state.count++ },
  [RESET] (state) { state.count = 0 }
}

export default {state, mutations}
  1. 添加store/index.js,作为vuex入口文件
import Vue from 'vue'
import Vuex from 'vuex'
import counter  from 'store/modules/counter'

Vue.use(Vuex) // 确保在new Vuex.Store()之前

export default new Vuex.Store({
  modules: {counter}
})
  1. 修改main.js,将store引入并添加到App中:
import Vue  from 'vue'
import router from './router'
import store from 'store'

const App = Vue.extend({store})

router.start(App, '#app')
  1. 最后改造一下src/components/Hello.vue,把action用上:
<template>
  <div>
    <p>{{message}}</p>
    <p>click count: {{count}}</p>
    <button @click="increase">increase</button><!--可以直接调用引入的action-->
    <button @click="reset">reset</button>
  </div>
</template>

<script>
  import {increase, reset} from 'store/modules/counter/actions' // 引入action
  export default {
    data: () => ({message: 'Hello Vue.js!'}),
    vuex: {
      actions: {increase, reset},
      getters: {
        count: ({counter}) => counter.count
      }
    }
  }
</script>

第六步:配置eslint

eslint不是必须的,但是强烈建议用在所有的javascript项目中

对于个人开发,可以在编程过程中发现并提示语法错误,有效过滤各种低级错误

对于团队开发,强制采用一致的编码风格,保证项目的统一性,有效避免各种任性行为

但是一定要注意,eslint定义的只是编码风格,规则是死的,人是活的,学会利用自定义规则的功能,增减规则

同时要知道,eslint检测不通过,不一定就是不能运行的,可能只是这种写法违背了编码风格,学会查看控制的查找具体错误原因

想要更好的eslint体验,请根据不同编辑器安装对应的eslint插件,主流的编辑器均已有相应插件

  1. 选择合适的编码风格

eslint提供了许多rules,可以直接在.eslintrc文件的rules中一个一个的配置

显然我们大多数情况下不需要这么做,网上已经有一些比较多人使用的风格了,本文推荐使用standard

  1. 配置.eslintrc文件

根目录下创建.eslintrc文件:

{
  "parser": "babel-eslint", // 支持babel
  "extends": "standard", // 使用eslint-config-standard的配置
  "plugins": [
    "html" // 支持.vue文件的检测
  ],
  "env": {
    "browser": true, // 不会将window上的全局变量判断为未定义的变量
    "es6": true // 支持es6的语法
  },
  "rules": { // 自定义个别规则写在这,0忽略,1警告,2报错
    "no-unused-vars": 1 // 将”未使用的变量“调整为警告级别,原为错误级别,更多规则请看官网
  }
}

结合不同编辑器的插件,打开js和vue文件中,就能看到提示了

根据使用的不同风格,安装所需的包,本文安装:

npm install --save-dev eslint babel-eslint eslint-config-standard eslint-plugin-standard eslint-plugin-html eslint-plugin-promise

第七步:webpack生产环境配置

前面已经配置过了开发环境下使用的配置文件dev.js,对于生产环境,通常需要对编译出来的文件进行压缩处理,提取公共模块等等,就需要专门提供一个配置文件

  1. 添加webpack.config/pro.js文件,把生产环境用不到的删掉,比如webpack-dev-serverwebpack-hot-replacement
const path = require('path')
const webpack = require('webpack')
const merge = require('webpack-merge')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const baseConfig = require('./base')
const root = path.resolve(__dirname, '..')

module.exports = merge(baseConfig, {
  plugins: [
    new HtmlWebpackPlugin({
      template: path.join(root, 'index.html'), // 模板文件
      inject: 'body' // js的script注入到body底部
    })
  ]
})

webpack常用插件:

extract-text-webpack-plugin 提取css到单独的文件

compression-webpack-plugin 压缩gzip

webpack.optimize.UglifyJsPlugin 压缩js文件,内置插件

webpack.DefinePlugin 定义全局变量,内置插件

webpack.optimize.CommonsChunkPlugin 提取公共依赖,内置插件

根据项目需求添加相应的插件,插件配置参数请查看官方文档,这里不进行罗列

  1. package.json中添加运行脚本:"build": "webpack --config webpack.config/pro.js"

  2. 运行npm run build,可以在dist文件夹中看到打包好的文件

“十分钟”只是噱头,多配几次,十分钟都不用。

轮子虽然都是圆的,也有轻重宽窄之分,造一辆车有时候也避免不了重造一个轮子。

DOM变动事件

  • DOMSubtreeModified

    • 在DOM结构中发生的任何变化时触发
    • 这个事件在其他任何事件触发后都会触发
    • IE9+/chrome/firefox/safari
  • DOMNodeInserted

    • 在一个节点作为子节点被插入到另一个节点中时触发
    • IE9+/chrome/firefox/safari/opera
  • DOMNodeRemoved

    • 在节点从其父节点中被移除时触发。
    • IE9+/chrome/firefox/safari/opera
  • DOMNodeInsertedIntoDocument

    • 在一个节点被直接插入文档或通过子树间接插入到文档之后触发
    • 这个事件在DOMNodeInserted之后触发
    • /chrome/safari/opera
  • DOMNodeRemovedFromDocument

    • 在一个节点被直接从文档中移除或通过子树间接从文档中移除之前触发
    • 这个事件在DOMNodeRemoved之后触发
    • /chrome/safari/opera
  • DOMAttrModified

    • 在特性被修改之后触发。
    • IE9+/firefox/opera
  • DOMCharacterDataModified

    • 在文本节点的值发生变化时触发
    • IE9+/chrome/firefox/safari/opera

相关查阅“HTML5新特性——MutationObserver”

  • Firefox(14+) Chrome(26+) Opera(15+) IE(11+) Safari(6.1+)

音频、视频播放

老哥,有没有什么办法可以支持音频、视频,好像github的markdown不支持iframe,还是说可以改markdown的解析

好厉害的博客

第一次看到这样的博客“系统”,nb 虽然不懂怎么搞的😁

HTTP缓存策略

image

一、强制缓存

1、Expires(已废弃)

服务器在HTTP响应头中返回Expires字段,指定一个过期时间(如Thu, 01 Dec 2022 15:46:39 GMT),在下次请求之前对比本地当前时间是否在过期时间之前,如符合,则从内存或硬盘中读取缓存,不去请求服务器资源。

缺陷:过度依赖本地时间,如果本地时间与服务器时间不同步,则可能导致资源无法缓存或永远缓存的问题。

2、Cache-Control

服务器在HTTP响应头中返回Cache-Control字段,包含以下属性:

  • no-store:不进行任何缓存策略
  • no-cache:跳过强制缓存策略,进行协商缓存策略,与no-store互斥
  • public:允许资源被代理服务器缓存
  • private:仅允许资源被浏览器缓存,与public互斥
  • max-age:浏览器缓存时长,单位秒
  • s-maxage:代理服务器缓存时长,单位秒,仅public时有效

举例:

Cache-control:max-age=60000,s-maxage=120000,public

通常使用Cache-Control代替Expires,因为前者时HTTP 1.1的,如果要考虑兼容性,仍可使用Expires。

二、协商缓存

1、Last-Modified / If-Modified-Sine

服务器在HTTP响应头中返回Last-Modified字段,即当前文件的修改时间,浏览器会在下一次请求时携带If-Modified-Sine字段,值为上次的Last-Modified,服务端判断时间如果时间一致,则返回304状态码,前端直接使用上次缓存的资源。

缺陷:如果文件内容没有修改,但修改时间已经更新(如只修改了文件名),这种情况仍会导致缓存失效。或者文件在几百毫秒内完成了再次修改,因为记录的最小单位是秒,依然会被认为没有修改。

2、ETag / If-None-Match

服务器在HTTP响应头中返回ETag字段,即当前文件指纹,浏览器在下次请求时携带If-None-Match字段,值为上一次的ETag,服务端判断如果一致,则返回304状态码,前端直接使用上次缓存的资源。

缺点:计算文件指纹意味着服务端需要有更多开销;ETag存在强校验、弱校验,强校验情况下生成的哈希码会深入到文件的每个字节,能够最大程度进行精确判断,但同时也意味着更大的性能消耗,弱校验整体速度比较快,但也意味着准确率较低,降低协商缓存的有效性。

三、针对不同文件采取不同缓存策略

1、文件名包含哈希码的静态文件:采用Cache-Control强制缓存。

2、index.html:采用协商缓存,具体看服务器性能、文件修改情况。

Nginx Location匹配

一、语法:

location [=|~|~*|^~] /uri/ { … }

二、修饰符:

修饰符 类型 说明 示例
= 普通匹配 精确匹配 location = /url/
^~ 普通匹配 普通字符串前缀匹配,忽略后续正则匹配 location ^~ /url/
普通匹配 普通字符串前缀匹配 location /url/
~ 正则匹配 正则匹配,区分大小写 location ~ .*.(js
~* 正则匹配 正则匹配,忽略大小写 location ~* .*.(js

三、顺序(先查找最长普通匹配,再顺序进行正则匹配):

image

1、找到精确匹配(=),不再往下查找

2、找到最长普通匹配,没有前缀(^~),往下顺序查找正则匹配

(1)找到符合的正则匹配,不再查找

(2)没有符合的正则匹配,使用最长普通匹配

3、找到最长普通匹配,有前缀(^~),不再往下查找

4、顺序查找正则匹配

四、示例

在线测试:https://nginx.viraptor.info/

1、

server {
 listen       80;
 server_name  test.com www.test.com;

 # A
 location ^~ /static/ {}

 # B
 location /static/js {}

 # C
 location ~* /(static|public)/ {}

 # D
 location = / {}
}

http://test.com/ -> D
http://test.com/static -> A
http://test.com/static/js -> C
这里B永远匹配不上,因为能匹配B的情况下,也都能匹配C

2、

server {
 listen       80;
 server_name  test.com www.test.com;

 # A
 location = / { }

 # B
 location / { }

 # C
 location /user/ { }

 # D
 location ^~ /images/ { }

 # E
 location ~* \.(gif|jpg|jpeg)$ { }
}

http://test.com/index.html -> B
http://test.com/documents/about.html -> B
http://test.com/user/index.html -> C
http://test.com/user/abc.jpg -> E
http://test.com/images/abc.jpg -> D
http://test.com/ -> A

模板字符串实现

实现

  function format (str, ...args) {
    return args.reduce((str, arg, index) => {
      // 当参数是字符串,则用参数的值替换{i}的位置
      if (typeof arg === 'string') return str.replace(new RegExp('\\{' + index + '\\}'), arg)
      // 当参数是对象,则用参数的每个属性prop的值,替换{prop}的位置
      else if (typeof arg === 'object') {
        const props = Object.keys(arg)
        return props.reduce((str, prop) => {
          return str.replace(new RegExp('\\{' + prop + '\\}'), arg[prop])
        }, str)
      }
    }, str)
  }

  // 传字符串
  const str1 = format('大家好,我叫{0},我来自{1}。', '小明', '广州')
  console.log(str1)

  // 传对象
  const str2 = format('大家好,我叫{name},我来自{city}。', {name: '小明', city: '广州'});
  console.log(str2)

  // 混合使用
  const str3 = format('{0}好,我叫{name},我来自{city}。', '大家', {name: '小明', city: '广州'});
  console.log(str3)

Vue实现虚拟列表(固定子项高度)

<template>
  <div class="list" ref="listRef" @scroll="onScroll">
   <div class="inner" :style="{ height: innerHeight }"></div>
   <div class="item" v-for="item of visibleList" :style="{ top: item.top }">{{ item.value }}</div>
  </div>
</template>

<script setup>
import { ref, reactive, computed, onMounted } from 'vue'

const itemHeight = 50;
const bufferSize = 2;
const list = Array.from({length: 10000}).map((_, i) => i);

const listRef = ref();
const listMeta = reactive({ offsetHeight: 0, scrollTop: 0 });

const onScroll = () => {
  listMeta.offsetHeight = listRef.value.offsetHeight;
  listMeta.scrollTop = listRef.value.scrollTop;
}

onMounted(() => {
  if (listRef.value) onScroll();
})

const innerHeight = computed(() => itemHeight * list.length + 'px');

const visibleList = computed(() => {
  const result = [];
  const { scrollTop, offsetHeight } = listMeta;

  const startIndex = Math.floor(scrollTop / itemHeight);
  const endIndex = Math.ceil((scrollTop + offsetHeight) / itemHeight);

  const finalStartIndex = Math.max(0, startIndex - bufferSize);
  const finalEndIndex = Math.min(list.length - 1,  endIndex + bufferSize);

  for (let i = finalStartIndex; i < finalEndIndex; i++) {
    result.push({
      top: i * itemHeight + 'px',
      value: list[i]
    })
  }

  return result;
});
</script>

<style>
.list {
  position: relative;
  height: 500px;
  width: 300px;
  border: 2px solid #000;
  overflow-y: scroll;

  .item {
    position: absolute;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    box-sizing: border-box;
    height: 50px;
    border: 1px solid #000;
  }
}
</style>

CSS禁止换行

关键css属性:

overflow: hidden; /* 超出隐藏 */
text-overflow: ellipsis; /* 文字省略号 */
word-break: keep-all; /* 禁止文字中断 */
white-space: nowrap; /* 空白不允许换行 */

格式化字节数

function clamp (value: number, min: number, max: number) {
  return Math.max(min, Math.min(max, value))
}

function formatBytes(bytes: number) {
  const units = ['Bytes', 'KB', 'MB', /** 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB' */];

  const k = 1024;
  const i = clamp(Math.floor(Math.log(bytes) / Math.log(k)), 0, units.length - 1);
  const v = parseFloat((bytes / Math.pow(k, i)).toFixed(2))

  return `${v} ${units[i]}`;
}

console.log(formatBytes(0));        // 0 Bytes
console.log(formatBytes(1024));     // 1 KB
console.log(formatBytes(20000000)); // 19.07 MB

Node模块收集

带★为个人推荐

[email protected]相关(更多

为适用于Koa2.0,以下模块安装请带上tag或版本,如npm i --save koa-logger@2

实用模块

node&npm相关

  • 包管理
    • npm
    • cnpm 不建议直接安装,推荐使用npm+--registry
    • yarn 生成yarn.lock后第二次安装包极速
    • ndm 包管理工具桌面软件,目前只有mac版
    • cost-of-modules 统计依赖信息,大小、子依赖等等
  • registry
  • node

测试、调试和管理

Web

Database

其他

Stylus实现迭代生成的Mixin

实现

  iterator(from, to, multiple, unit = 1px, prop, abbpre, abbpost = '')
    for i in (from..to)
      .{abbpre}{i * multiple}{abbpost}
        {prop}: i * multiple * unit

使用

  // 使用:
  iterator(0, 5, 5, 1px, height, ui-h)
  
  // 生成出:
  .ui-h0{height:0px}
  .ui-h5{height:5px}
  .ui-h10{height:10px}
  .ui-h15{height:15px}
  .ui-h20{height:20px}
  .ui-h25{height:25px}

VSCode拓展推荐(前端开发)

最后更新于:2021-05-31
转载请说明出处,谢谢

一、食用说明

  • 相似功能的插件,不推荐全都装上,请挑选一个使用
  • 本列表所有插件均已测试使用过,但不代表不存在问题
  • 任何插件本身的问题,请到对于代码仓库提交issue

二、拓展

名称 简述
Auto Close Tag 自动闭合HTML标签
Auto Import import提示
Auto Rename Tag 修改HTML标签时,自动修改匹配的标签
Babel JavaScript babel插件,语法高亮
Babelrc .babelrc文件高亮提示
Beautify css/sass/scss/less css/sass/less格式化
Better Align 对齐赋值符号和注释
Better Comments 编写更加人性化的注释
Bookmarks 添加行书签
Bracket Lens 在闭合的括号处提示括号头部的代码
Bracket Pair Colorizer 2 用不同颜色高亮显示匹配的括号
Can I Use HTML5、CSS3、SVG的浏览器兼容性检查
Code Outline 展示代码结构树
Code Runner 运行选中代码段(支持多数语言)
Code Spell checker 单词拼写检查
CodeBing 快速打开Bing并搜索,可配置搜索引擎
Color Highlight 颜色值在代码中高亮显示
Color Info 小窗口显示颜色值,rgb,hsl,cmyk,hex等等
Color Picker 拾色器
CSS-in-JS CSS-in-JS高亮提示和转换
Dash 集成Dash
Debugger for Chrome 调试Chrome
Document This 注释文档生成
DotENV .env文件高亮
Edit csv 编辑CSV文件
EditorConfig for VS Code EditorConfig插件
Emoji 在代码中输入emoji
endy 将输入光标跳转到当前行最后面
Error Gutters 在行号处提示错误代码
ESLint ESLint插件,高亮提示
File Peek 根据路径字符串,快速定位到文件
filesize 状态栏显示当前文件大小
Find-Jump 快速跳转到指定单词位置
Font-awesome codes for html FontAwesome提示代码段
ftp-sync 同步文件到ftp
Git Blame 在状态栏显示当前行的Git信息
Git File History 快速浏览单文件历史提交变动
Git Graph Git图形化视图,方便浏览和操作
Git History(git log) 查看git log
Git Tree Compare Git树形比对,查看不同分支的差异
gitignore .gitignore文件语法
GitLens 显示文件最近的commit和作者,显示当前行commit信息
GraphQL for VSCode graphql高亮和提示
Guides 高亮缩进基准线
Gulp Snippets Gulp代码段
Highlight Matching Tag 高亮匹配选中的标签
HTML CSS Class Completion CSS class提示
HTML CSS Support css提示(支持vue)
HTMLHint HTML格式提示
htmltagwrap 快捷包裹html标签
htmltagwrap 包裹HTML
Import Beautify import分组、排序、格式化
Import Cost 行内显示导入(import/require)的包的大小
Indenticator 缩进高亮
IntelliSense for css class names css class输入提示
JavaScript (ES6) code snippets ES6语法代码段
JavaScript Standard Style Standard风格
Jest Runner 支持执行Jest单个测试文件或单个用例
JS Refactor 代码重构工具,提取函数、变量重命名等等
JSON to TS JSON结构转化为typescript的interface
JSON Tools 格式化和压缩JSON
jumpy 快速跳转到指定单词位置
language-stylus Stylus语法高亮和提示
Less IntelliSense less变量与混合提示
Lodash Lodash代码段
Log Wrapper 生产打印选中变量的代码
markdownlint Markdown格式提示
MochaSnippets Mocha代码段
Node modules resolve 快速导航到Node模块
npm 运行npm命令
npm Intellisense 导入模块时,提示已安装模块名称
Output Colorizer 彩色输出信息
Partial Diff 对比两段代码或文件
Parameter Hints 在函数调用处指示参数名称
Path Autocomplete 路径完成提示
Path Intellisense 另一个路径完成提示
Polacode 将代码生成图片
PostCss Sorting css排序
Prettier - Code formatter prettier官方插件
Prettify JSON 格式化JSON
Project Manager 快速切换项目
Quokka.js 不需要手动运行,行内显示变量结果
Rainbow CSV CSV文件使用彩虹色渲染不同列
React Native Storybooks storybook预览插件,支持react
React Playground 为编辑器提供一个react组件运行环境,方便调试
React Standard Style code snippets react standar风格代码块
REST Client 发送REST风格的HTTP请求
Sass sass插件
Settings Sync VSCode设置同步到Gist
Sort lines 排序选中行
Sort Typescript Imports typescript的import排序
String Manipulation 字符串转换处理(驼峰、大写开头、下划线等等)
stylelint css/sass/less代码风格
SVG Viewer SVG查看器
Syncing vscode设置同步到gist
Test Spec Generator 测试用例生成(支持chai、should、jasmine)
TODO Parser Todo管理
Todo Todo Tree 收集代码中的TODO注释,支持快速搜索
Toggle Quotes 切换JS中的引号," -> ' -> `
TS/JS postfix completion ts/js后缀提示
TSLint TypeScript语法检查
Types auto installer 自动安装@types声明依赖
TypeScript Hero TypeScript辅助插件,管理import、outline等等
TypeScript Import TS自动import
TypeScript Import Sorter import整理排序
Typescript React code snippets React Typescript代码段
TypeSearch TS声明文件搜索
Version Lens package.json文件显示模块当前版本和最新版本
vetur Vue插件
Volar Vue插件,支持Vue3
View Node Package 快速打开选中模块的主页和代码仓库
Visual Studio IntelliCode 基于AI的代码提示
VS Live Share 实时多人协助
VSCode Great Icons 文件图标拓展
vscode-database 操作数据库,支持mysql和postgres
vscode-icons 文件图标,方便定位文件
vscode-random 随机字符串生成器
vscode-spotify 集成spotify,播放音乐
vscode-styled-components styled-components高亮支持
vscode-styled-jsx styled-jsx高亮支持
Vue Peek 支持跳转到Vue组件定义文件
Vue TypeScript Snippets Vue Typescript代码段
VueHelper Vue2代码段(包括Vue2 api、vue-router2、vuex2)
Wallaby.js 实时测试插件
Wrap Console Log Lite 对选中代码快速console.log

三、主题

名称 预览
Atom One Light Theme Atom One Light Theme
bluloco-dark bluloco-dark
bluloco-light bluloco-light
Enki Theme Enki Theme
eppz! (C# theme for Unity) eppz! (C# theme for Unity)
Eva Theme Eva Theme
Flat UI Flat UI
GitHub Theme GitHub Theme
Monokai Pro Monokai Pro
New Moon VSCode New Moon VSCode
One Dark Pro One Dark Pro
Plastic Plastic
spacegray-vscode spacegray-vscode
Splus Splus

四、个人首选项配置(仅供参考)

{
  "breadcrumbs.enabled": true,
  "editor.tabSize": 2,
  "editor.renderWhitespace": "boundary",
  "editor.cursorBlinking": "smooth",
  "editor.minimap.renderCharacters": false,
  "editor.fontFamily": "'Fira Code', 'Droid Sans Mono', 'Courier New', monospace, 'Droid Sans Fallback'",
  "editor.fontLigatures": true,
  "explorer.confirmDragAndDrop": false,
  "extensions.autoUpdate": false,
  "files.insertFinalNewline": true,
  "git.autofetch": true,
  "git.path": "F:\\Program Files\\Git\\cmd\\git.exe",
  "search.exclude": {
    "**/node_modules": true,
    "**/dist": true
  },
  "typescript.locale": "en",
  "window.titleBarStyle": "custom",
  "window.title": "${dirty}${activeEditorMedium}${separator}${rootName}",
  "window.zoomLevel": 1,
  "workbench.activityBar.visible": true,
  "workbench.colorTheme": "Plastic - deprioritised punctuation",
  "workbench.iconTheme": "vscode-great-icons",
  "workbench.startupEditor": "newUntitledFile",
  "eslint.autoFixOnSave": true,
  "eslint.validate": ["javascript", "javascriptreact", "vue"],
  "vsicons.projectDetection.autoReload": true,
  "vsicons.dontShowNewVersionMessage": true,
  "tslint.autoFixOnSave": true,
  "debugwrapper.wrappers": {
    "default": "console.log('$eSEL', $SEL)"
  },
  "prettier.tslintIntegration": true,
  "cSpell.userWords": [
    "Unmount"
  ],
  "jest.autoEnable": false,
}

一步步搭建React项目(二):使用webpack配置开发环境

上一个教程中,我们已经把一个react项目跑起来了,但是怎么看都觉得太过于敷衍,在这次教程中我们将使用webpack配置一个称心的开发环境

本教程webpack版本号为2+

接下来的教程,将在上一个教程项目的基础上完成

主要完成以下改进:

  1. 引入webpack,实现模块管理

  2. 实现监听文件改动,自动编译并刷新浏览器

  3. 实现热替换(HMR)

1. 初步引入webpack:实现模块管理

在根目录下创建webpack.dev.js文件:

const path = require('path')
const root = __dirname

module.exports = {
  // 入口文件
  entry: path.resolve(root, 'src/main.js'),
  // 出口文件
  output: {
    filename: 'bundle.js',
    path: path.resolve(root, 'dist')
  },
  // loaders
  module: {
    rules: [
      {test: /\.jsx?$/, use: ['babel-loader'], exclude: /node_modules/}
    ]
  }
}

这里我们通过webpack去执行babel进行编译,所以将babel的配置抽出到一个文件,根目录下创建.babelrc

{
  "presets": [
    ["es2015", {"modules": false}], // webpack 2 本身已支持es6 module
    "react"
  ]
}

将缺少的包都安装上:

$ npm install --save react react-dom
$ npm install --save-dev webpack babel-cli babel-loader babel-preset-es2015 babel-preset-react

当前的package.json模块如下:

"dependencies": {
  "react": "^15.4.2",
  "react-dom": "^15.4.2"
},
"devDependencies": {
  "babel-cli": "^6.23.0",
  "babel-loader": "^6.3.2",
  "babel-preset-es2015": "^6.22.0",
  "babel-preset-react": "^6.23.0",
  "webpack": "^2.2.1"
}

最后修改一下现有的文件:

  • 改一下index.html的script引入位置:
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>React Demo</title>
  </head>
  <body>
    <div id="app"></div>
    <script src="dist/bundle.js"></script>
  </body>
</html>
  • main.js中使用import引入模块
import React from 'react'
import ReactDOM from 'react-dom'

ReactDOM.render(
  <h1>Hello, world!</h1>,
  document.getElementById('app')
)
  • 修改package.jsonscripts
"scripts": {
  "dev": "webpack --config webpack.dev.js"
}

测试看看,编译之后打开浏览器

$ npm run dev

2. 完善webpack配置:实现监听文件改动,自动编译并刷新浏览器

实现监听文件改动然后自动编译新的bundle.js,我们需要用到webpack-dev-server去创建一个本地服务器,同时,可以结合html-webpack-plugin去生成index.html,先安装:

$ npm install webpack-dev-server html-webpack-plugin --save-dev

先说说html-webpack-plugin的使用

将我们根目录下的index.html改名为template.html,顾名思义,现在作为一个模板,通过插件会在dist中生成一个对应的index.html文件,template.html中去掉多余的东西:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>React Demo</title>
  </head>
  <body>
    <div id="app"></div>
    <!-- js文件会自动插入到这里,无需自己填写 -->
  </body>
</html>

webpack.dev.js中加入html-webpack-plugin的配置:

// 引入html-webpack-plugin
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  // ...
  // 其他配置保持不变
  // ...
  plugins: [
    new HtmlWebpackPlugin({
      title: 'React Demo',
      template: path.resolve(root, 'template.html')
    })
  ]
}

现在通过npm run dev,就能看到生成的dist/index.html

接下来引入webpack-dev.serverwebpack.dev.js配置修改如下:

module.exports = {
  entry: [
    'webpack-dev-server/client',
    path.resolve(root, 'src/main.js')
  ],
  output: {
    filename: 'bundle.js',
    path: path.resolve(root, 'dist'),
    publicPath: '/'
  },
  // ...
  // 其他配置保持不变
  // ...
  devServer: {
    contentBase: path.resolve(root, 'dist'),
    publicPath: '/',
    port: 8080,
    historyApiFallback: true
  }
}

package.json中的scripts修改如下:

"scripts": {
  "dev": "webpack-dev-server --config webpack.dev.js"
}

通过npm run dev就可以启动一个本地服务器了,只要文件有改动,就会自动刷新浏览器

3. 完善webpack配置:实现热替换(HMR)

自动刷新依然不尽兴,有时候仅仅改动了某个组件的细微地方(改动文案、样式等等),然后导致整个页面刷新了,有些调试步骤又得重新来一次

下面将讲解如何实现react的热替换

实现热替换需要用到react-hot-loader,使用npm安装:

(该教程发布时,需要添加@next才能安装3.x.x版本)

$ npm install --save-dev react-hot-loader@next

更改webpack.dev.js的配置:

// ...
const webpack = require('webpack')

module.exports = {
  entry: [
    'react-hot-loader/patch', // 激活HMR
    'webpack-dev-server/client',
    'webpack/hot/only-dev-server',
    path.resolve(root, 'src/main.js')
  ],
  // ...
  // 其他配置保持不变
  // ...
  devServer: {
    hot: true, // 激活服务器的HMR
    contentBase: path.resolve(root, 'dist'),
    publicPath: '/',
    port: 8080,
    historyApiFallback: true
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'React Demo',
      template: path.resolve(root, 'template.html')
    }),
    new webpack.HotModuleReplacementPlugin(), // 热替换插件
    new webpack.NamedModulesPlugin() // 执行热替换时打印模块名字
  ]
}

.babelrc也有相应的改动:

{
  "presets": [
    ["es2015", {"modules": false}],
    "react"
  ],
  "plugins": [
    "react-hot-loader/babel" // 添加HMR支持
  ]
}

为了测试热替换是否生效,在src目录添加一个App.js文件,作为根组件:

import React from 'react'

const App = () => (
  <h1>Hello, world!</h1>
)

export default App

main.js中引入并渲染App,同时又一些为支持HMR的改动:

import React from 'react'
import ReactDOM from 'react-dom'
import { AppContainer } from 'react-hot-loader'
import App from './App'

const render = (App) => {
  ReactDOM.render(
    <AppContainer>
      <App />
    </AppContainer>,
    document.getElementById('app')
  )
}

render(App)

if (module.hot) {
  module.hot.accept('./App', () => render(App))
}

重新运行npm run dev,在对App中的Hello, world!进行改动时,页面并不是整个刷新的,至此完成热替换的配置

asyncThrottle 异步并发节流

function asyncThrottle(tasks: Array<() => Promise<unknown>>, concurrency = 1) {
  return new Promise<void>((resolve) => {
    const entries = tasks.entries();
    let remains = tasks.length;

    const run = () => {
      const { value } = entries.next();
      if (!value) return;

      const [, task] = value;
      task().finally(() => {
        remains -= 1;
        return remains ? run() : resolve();
      });
    };


    Array.from({ length: concurrency }).forEach(run);
  });
}

// Example:

function delay() {
  const ms = Math.floor(Math.random() * 3000);
  const bool = Math.random() > 0.5 
  return new Promise((resolve, reject) => setTimeout(bool ? resolve : reject, ms));
}

const tasks = Array
  .from({ length: 10 })
  .map((_, i) => () => delay().then(() => console.log(i)));

asyncThrottle(tasks, 2);

将node开发环境从linux迁移到win10 bash on linux

开启Windows 10的Linux子系统

  • 第一步:

image

  • 第二步:

image

  • 第三步:

重启电脑,等待更新。

  • 第四步:

CMD中输入:

bash
#
lxrun /install /y

等待安装完毕,以后在CMD中输入bash,即可进入linux子系统

安装node环境(通过nvm

  • 在CMD中通过bash进入linux

  • 安装nvm:

curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.2/install.sh | bash
  • 安装node和npm:
nvm install stable
# 等待安装完毕后,激活该版本:
nvm use stable
  • 解决sudo npm 未找到命令:
sudo ln -s $(which node) /usr/bin/node
sudo ln -s $(which npm) /usr/bin/npm

至此完成linux和node的安装。

语义化时间对象为距当前时刻fromNow

  // 根据正负判断是之前还是之后
  function toText (val, before, after) {
    return Math.abs(val) + (val > 0 ? before : after)
  }
  
  function fromNow (date, format) {
    let ms = Date.now() - date.getTime()

    // 误差修正
    if (ms > 0) ms += 1000
    else ms -= 1000

    const minute = parseInt(ms / 1000 / 60)
    const hour = parseInt(minute / 60)
    const day = parseInt(hour / 24)
    const month = parseInt(day / 30)
    const year = parseInt(day / 365)

    if (year) return toText(year, '年前', '年后')
    else if (month) return toText(month, '个月前', '个月后')
    else if (day) return toText(day, '天前', '天后')
    else if (hour) return toText(hour, '小时前', '小时后')
    else if (minute) return toText(minute, '分钟前', '分钟后')
    else return ms > 0 ? '刚刚' : '不到一分钟之后'
  }

  console.log(fromNow(new Date(2020, 0)));

找不到通过symlink引入的模块中的peerDependencies依赖

最近在写一个React组件库,遇到了这么一个问题:

这个组件库my-components通过peerDependencies依赖了react

在一个Example项目(实质是storybook),我通过symlink的方式将my-components作为依赖,并且安装了react,结构如下:

Example
|--node_modules
| |--react
| |--react-router
| |--my-components (通过symlink引入,并且import * as React from 'react')
|--src
| |--index.js (import 'my-components')
|--webpack.config.js

通过webpack运行起来时,提示报错:

ERROR in ../../my-components/index.js
Module not found: Error: Can't resolve 'react' in 'xxx/my-components/index.js'

并且,在my-components下,通过npm install react手动安装react时,不会出现错误。所以,几乎可以肯定是引入包时查找的目录除了问题。

在github上找到了类似的问题和答案,通过配置webpack.config.js,可以解决这个问题:

const path = require('path')

module.exports = {
  // ...
  resolve:  {
    symlinks: false
    // 或者
    // modules: [path.resolve(__dirname, './node_modules'), 'node_modules']
  }
  // ...
}

让依赖引入的查询位置在正确的目录上进行。

建议

为什么不写在readme中,这样的资源必须进入issues才能看,fork了别人是看不到issues的

React表单Form 组件的设计与实现

为什么要造轮子

在 React 中使用表单有个明显的痛点,就是需要维护大量的valueonChange,比如一个简单的登录框:

class App extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      username: "",
      password: ""
    };
  }

  onUsernameChange = e => {
    this.setState({ username: e.target.value });
  };

  onPasswordChange = e => {
    this.setState({ password: e.target.value });
  };

  onSubmit = () => {
    const data = this.state;
    // ...
  };

  render() {
    const { username, password } = this.state;

    return (
      <form onSubmit={this.onSubmit}>
        <input value={username} onChange={this.onUsernameChange} />
        <input
          type="password"
          value={password}
          onChange={this.onPasswordChange}
        />
        <button>Submit</button>
      </form>
    );
  }
}

这已经是比较简单的登录页,一些涉及到详情编辑的页面,十多二十个组件也是常有的。一旦组件多起来就会有许多弊端:

  • 不易于维护:占据大量篇幅,阻碍视野。
  • 可能影响性能:setState的使用,会导致重新渲染,如果子组件没有相关优化,相当影响性能。
  • 表单校验:难以统一进行表单校验。
  • ...

总结起来,作为一个开发者,迫切希望能有一个表单组件能够同时拥有这样的特性:

  • 简单易用
  • 父组件可通过代码操作表单数据
  • 避免不必要的组件重绘
  • 支持自定义组件
  • 支持表单校验

表单组件社区上已经有不少方案,例如react-final-formformikant-plusnoform等,许多组件库也提供了不同方式的支持,如ant-design

但这些方案都或多或少一些重量,又或者使用方法仍然不够简便,自然造轮子才是最能复合要求的选择。

怎么造轮子

这个表单组件实现起来主要分为三部分:

  • Form:用于传递表单上下文。
  • Field: 表单域组件,用于自动传入valueonChange到表单组件。
  • FormStore: 存储表单数据,封装相关操作。

为了能减少使用ref,同时又能操作表单数据(取值、修改值、手动校验等),我将用于存储数据的FormStore,从Form组件中分离出来,通过new FormStore()创建并手动传入Form组件。

使用方式大概会长这样子:

class App extends React.Component {
  constructor(props) {
    super(props);

    this.store = new FormStore();
  }

  onSubmit = () => {
    const data = this.store.get();
    // ...
  };

  render() {
    return (
      <Form store={this.store} onSubmit={this.onSubmit}>
        <Field name="username">
          <input />
        </Field>
        <Field name="password">
          <input type="password" />
        </Field>
        <button>Submit</button>
      </Form>
    );
  }
}

FormStore

用于存放表单数据、接受表单初始值,以及封装对表单数据的操作。

class FormStore {
  constructor(defaultValues = {}, rules = {}) {
    // 表单值
    this.values = defaultValues;

    // 表单初始值,用于重置表单
    this.defaultValues = deepCopy(defaultValues);

    // 表单校验规则
    this.rules = rules;

    // 事件回调
    this.listeners = [];
  }
}

为了让表单数据变动时,能够响应到对应的表单域组件,这里使用了订阅方式,在FormStore中维护一个事件回调列表listeners,每个Field创建时,通过调用FormStore.subscribe(listener)订阅表单数据变动。

class FormStore {
  // constructor ...

  subscribe(listener) {
    this.listeners.push(listener);

    // 返回一个用于取消订阅的函数
    return () => {
      const index = this.listeners.indexOf(listener);
      if (index > -1) this.listeners.splice(index, 1);
    };
  }

  // 通知表单变动,调用所有listener
  notify(name) {
    this.listeners.forEach(listener => listener(name));
  }
}

再添加getset函数,用于获取和设置表单数据。其中,在set函数中调用notify(name),以保证所有的表单变动都会触发通知。

class FormStore {
  // constructor ...

  // subscribe ...

  // notify ...

  // 获取表单值
  get(name) {
    // 如果传入name,返回对应的表单值,否则返回整个表单的值
    return name === undefined ? this.values : this.values[name];
  }

  // 设置表单值
  set(name, value) {
    //如果指定了name
    if (typeof name === "string") {
      // 设置name对应的值
      this.values[name] = value;
      // 执行表单校验,见下
      this.validate(name);
      // 通知表单变动
      this.notify(name);
    }

    // 批量设置表单值
    else if (name) {
      const values = name;
      Object.keys(values).forEach(key => this.set(key, values[key]));
    }
  }

  // 重置表单值
  reset() {
    // 清空错误信息
    this.errors = {};
    // 重置默认值
    this.values = deepCopy(this.defaultValues);
    // 执行通知
    this.notify("*");
  }
}

对于表单校验部分,不想考虑得太复杂,只做一些规定

  1. FormStore构造函数中传入的rules是一个对象,该对象的键对应于表单域的name,值是一个校验函数
  2. 校验函数参数接受表单域的值和整个表单值,返回booleanstring类型的结果。
  • true代表校验通过。
  • falsestring代表校验失败,并且string结果代表错误信息。

然后巧妙地通过||符号判断是否校验通过,例如:

new FormStore({/* 初始值 */, {
  username: (val) => !!val.trim() || '用户名不能为空',
  password: (val) => !!(val.length > 6 && val.length < 18) || '密码长度必须大于6个字符,小于18个字符',
  passwordAgain: (val, vals) => val === vals.password || '两次输入密码不一致'
}})

FormStore实现一个validate函数:

class FormStore {
  // constructor ...

  // subscribe ...

  // notify ...

  // get ...

  // set ...

  // reset ...

  // 用于设置和获取错误信息
  error(name, value) {
    const args = arguments;
    // 如果没有传入参数,则返回错误信息中的第一条
    // const errors = store.error()
    if (args.length === 0) return this.errors;

    // 如果传入的name是number类型,返回第i条错误信息
    // const error = store.error(0)
    if (typeof name === "number") {
      name = Object.keys(this.errors)[name];
    }

    // 如果传了value,则根据value值设置或删除name对应的错误信息
    if (args.length === 2) {
      if (value === undefined) {
        delete this.errors[name];
      } else {
        this.errors[name] = value;
      }
    }

    // 返回错误信息
    return this.errors[name];
  }

  // 用于表单校验
  validate(name) {
    if (name === undefined) {
      // 遍历校验整个表单
      Object.keys(this.rules).forEach(n => this.validate(n));
      // 并通知整个表单的变动
      this.notify("*");
      // 返回一个包含第一条错误信息和表单值的数组
      return [this.error(0), this.get()];
    }

    // 根据name获取校验函数
    const validator = this.rules[name];
    // 根据name获取表单值
    const value = this.get(name);
    // 执行校验函数得到结果
    const result = validator ? validator(name, this.values) : true;
    // 获取并设置结果中的错误信息
    const message = this.error(
      name,
      result === true ? undefined : result || ""
    );

    // 返回Error对象或undefind,和表单值
    const error = message === undefined ? undefined : new Error(message);
    return [error, value];
  }
}

至此,这个表单组件的核心部分FormStore已经完成了,接下来就是这么在FormField组件中使用它。

Form

Form组件相当简单,也只是为了提供一个入口和传递上下文。

props接收一个FormStore的实例,并通过Context传递给子组件(即Field)中。

const FormStoreContext = React.createContext(undefined);

function Form(props) {
  const { store, children, onSubmit } = props;

  return (
    <FormStoreContext.Provider value={store}>
      <form onSubmit={onSubmit}>{children}</form>
    </FormStoreContext.Provider>
  );
}

Field

Field组件也并不复杂,核心目标是实现valueonChange自动传入到表单组件中。

// 从onChange事件中获取表单值,这里主要应对checkbox的特殊情况
function getValueFromEvent(e) {
  return e && e.target
    ? e.target.type === "checkbox"
      ? e.target.checked
      : e.target.value
    : e;
}

function Field(props) {
  const { label, name, children } = props;

  // 拿到Form传下来的FormStore实例
  const store = React.useContext(FormStoreContext);

  // 组件内部状态,用于触发组件的重新渲染
  const [value, setValue] = React.useState(
    name && store ? store.get(name) : undefined
  );
  const [error, setError] = React.useState(
    name && store ? store.error(name) : undefined
  );

  // 表单组件onChange事件,用于从事件中取得表单值
  const onChange = React.useCallback(
    (...args) => name && store && store.set(name, valueGetter(...args)),
    [name, store]
  );

  // 订阅表单数据变动
  React.useEffect(() => {
    if (!name || !store) return;

    return store.subscribe(n => {
      // 当前name的数据发生了变动,获取数据并重新渲染
      if (n === name || n === "*") {
        setValue(store.get(name));
        setError(store.error(name));
      }
    });
  }, [name, store]);

  let child = children;

  // 如果children是一个合法的组件,传入value和onChange
  if (name && store && React.isValidElement(child)) {
    const childProps = { value, onChange };
    child = React.cloneElement(child, childProps);
  }

  // 表单结构,具体的样式就不贴出来了
  return (
    <div className="form">
      <label className="form__label">{label}</label>
      <div className="form__content">
        <div className="form__control">{child}</div>
        <div className="form__message">{error}</div>
      </div>
    </div>
  );
}

于是,这个表单组件就完成了,愉快地使用它吧:

class App extends React.Component {
  constructor(props) {
    super(props);

    this.store = new FormStore();
  }

  onSubmit = () => {
    const data = this.store.get();
    // ...
  };

  render() {
    return (
      <Form store={this.store} onSubmit={this.onSubmit}>
        <Field name="username">
          <input />
        </Field>
        <Field name="password">
          <input type="password" />
        </Field>
        <button>Submit</button>
      </Form>
    );
  }
}

结语

这里只是把最核心的代码整理了出来,功能上当然比不上那些成百上千 star 的组件,但是用法上足够简单,并且已经能应对项目中的大多数情况。

我已在此基础上完善了一些细节,并发布了一个 npm 包——@react-hero/form,你可以通过npm安装,或者在github上找到源码。如果你有任何已经或建议,欢迎在评论或 issue 中讨论。

[Rxjs] Subject、BehaviorSubject、ReplaySubject、AsyncSubject

Subject、BehaviorSubject、ReplaySubject、AsyncSubject

Subject

SubjectObservable(可观察对象),每一个Subject都有可以被subscribe(订阅),但不会创建新的执行环境,只会把新的Observer注册到内部维护着的Observer清单

SubjectObserver(观察者),是一个由next()error()complete()方法组成的对象,可以subscribe(订阅)一个Observable,并从其中接受到推送的值。

每次Subject接收到值时,都遍历Observer清单,并推送该值。

  • Subject可以被订阅:

    const subject = new Rx.Subject()
    
    subject.subscribe((value) => console.log('A value: ', value))
    subject.subscribe((value) => console.log('B value: ', value))
    
    subject.next(1)
    // A value: 1
    // B value: 1
    subject.next(2)
    // A value: 2
    // B value: 2
  • Subject可以订阅Observable,接受并转发值:

    const observable = Rx.Observable.from([1, 2])
    
    const subject = new Rx.Subject()
    
    subject.subscribe((value) => console.log('A value: ', value))
    subject.subscribe((value) => console.log('B value: ', value))
    
    observable.subscribe(subject)
    // A value: 1
    // B value: 1
    // A value: 2
    // B value: 2

BehaviorSubject

BehaviorSubjectSubject的子类,拥有初始值,并且总是保存着一个最新值,一旦被订阅立即向订阅者发送最新值

const subject = new Rx.BehaviorSubject(0) // 初始值为0

subject.subscribe((value) => console.log('A value: ', value))

subject.next(1)
// A value: 0
// A value: 1
subject.next(2)
// A value: 2
// B value: 2

subject.subscribe((value) => console.log('B value: ', value))

subject.next(3)
// A value: 3
// B value: 3

ReplaySubject

ReplaySubject也是Subject的子类,被订阅时立即向订阅者推送最新指定数量的值

const subject = new Rx.ReplaySubject(2) // 回放2个

subject.subscribe((value) => console.log('A value: ', value))

subject.next(1)
// A value: 1
subject.next(2)
// A value: 2
subject.next(3)
// A value: 3

subject.subscribe((value) => console.log('B value: ', value))
// B value: 2
// B value: 3

subject.next(4)
// A value: 4
// B value: 4

AsyncSubject

AsyncSubject也是Subject的子类,仅会在complete()之后,向订阅者推送最后一个值

const subject = new Rx.AsyncSubject()

subject.subscribe((value) => console.log('A value: ', value))

subject.next(1)
subject.next(2)

subject.subscribe((value) => console.log('B value: ', value))

subject.next(3)
subject.complete()
// A value: 3
// B value: 3

multicast、refCount、publish、share

在开头我们提到Subject就是一个Obserable,但新的订阅者会共用同一个执行环境:

const source = Rx.Observable.interval(1000)
								.take(3)
const subject = new Rx.Subject()

subject.subscribe((value) => console.log('A value: ', value))

source.subscribe(subject)

setTimeot(() => {
  subject.subscribe((value) => console.log('B value: ', value))
}, 1000)

// A value: 0
// A value: 1
// B value: 1
// A value: 2
// B value: 2

B在1秒钟之后订阅,所以不会接受到第一个值0

  • multicast

    仔细看上面的例子,虽然定义的变量不多,但是这段代码的订阅关系还是略显复杂,这时候我们可以用multicast操作符来简化:

    const source = Rx.Observable.interval(1000)
                    .take(3)
                    .multicast(new Rx.Subject())
    
    source.subscribe((value) => console.log('A value: ', value))
    source.connect()
    
    setTimeout(() => {
      source.subscribe((value) => console.log('B value: ', value))
    }, 1000)

    这样一来,所有的订阅者都可以直接订阅source,其中的multicast用于挂载一个Subject,并返回一个ConnectableObservable ,拥有connect()方法。注意的是,必须等到connect()调用之后,Subject才真正订阅source并开始推送。

    connect()方法会返回一个subscription,可调用其unsubscribe进行退订。

  • refCount

    refCount必须搭配multicast使用,用于创建一个只要有第一个订阅就会自动connect()Observable,并且当订阅数变为0后会自动终止推送。

    const source = Rx.Observable.interval(1000) // 没有加take(3)
    								.multicast(new Rx.Subject())
    								.refCount()
    
    let subscriptionA
    let subscriptionB
    
    subscriptionA = source.subscribe((value) => console.log('A value: ', value))
    // 订阅数 0 => 1,自动connect,开始推送
    
    setTimeout(() => {
      subscriptionB = source.subscribe((value) => console.log('B value: ', value))
      // 订阅数 1 => 2
    }, 1000)
    
    setTimeout(() => {
      subscriptionA.unsubscribe()
      // 订阅数 2 => 1
      subscriptionB.unsubscribe()
      // 订阅数 1 => 0,终止推送
    }, 5000)
  • publish

    publishmulticast的简化写法:

    // Subject => publish
    const source = Rx.Observable.interval(1000).multicast(new Rx.Subject()).refCount()
    const source = Rx.Observable.interval(1000).publish().refCount()
    
    // BehaviorSubject => publishBehavior
    const source = Rx.Observable.interval(1000).multicast(new Rx.BehaviorSubject(0)).refCount()
    const source = Rx.Observable.interval(1000).publishBehavior(0).refCount()
    
    // ReplaySubject => publishReplay
    const source = Rx.Observable.interval(1000).multicast(new Rx.ReplaySubject(3)).refCount()
    const source = Rx.Observable.interval(1000).publishReplay(3).refCount()
    
    // AsyncSubject => publishLast
    const source = Rx.Observable.interval(1000).multicast(new Rx.AsyncSubject()).refCount()
    const source = Rx.Observable.interval(1000).publishLast(3).refCount()
  • share

    sharepublish+refCount的简化写法:

    // publish + refCount => share
    const source = Rx.Observable.interval(1000).publish().refCount()
    const source = Rx.Observable.interval(1000).share()
    
    // publishReplay + refCount => shareReplay
    const source = Rx.Observable.interval(1000).publishReplay(3).refCount()
    const source = Rx.Observable.interval(1000).shareReplay(3)
    
    // 没有shareBehavior和shareAsync、shareLast

参考资料

30 天精通 RxJS

Vue.js用directive判断是否点击当前DOM以外的地方

Vue 1.x

Vue.directive('clickOutside', {
  bind () {
    this.onClick = (event) => {
      if (this.el.contains(event.target)) return false
      if (this.expression) this.vm[this.expression]()
    }

    setTimeout(() => {
      document.addEventListener('click', this.onClick)
    })
  },
  unbind () {
    document.removeEventListener('click', this.onClick)
  }
})

Vue 2.x(来自评论)

Vue.directive('clickOutside', {
  bind (el, binding, vnode) {
    el.event = function (event) {
      if (!(el === event.target || el.contains(event.target))) {
        vnode.context[binding.expression](event)
      }
    }
    document.body.addEventListener('click', el.event)
  },
  unbind (el) {
    document.body.removeEventListener('click', el.event)
  }
})

Vue 3.x

<script>
const vClickOutside = {
  mounted(el, binding) {
    el.clickOutside = (e) => {
      if (el.contains(event.target)) return false
      if (typeof binding.value === 'function') binding.value(e)
    }

    document.addEventListener('click', el.clickOutside)
  },
  beforeUnmount(el) {
    document.removeEventListener('click', el.clickOutside)
  }
}

export default {
  directives: {
    clickOutside: vClickOutside,
  },
  methods: {
    log() {
      console.log('click')
    }
  }
}
</script>

<template>
  <div class="rect" v-click-outside="log">click outside</div>
</template>

<style>
  .rect {
    margin: 50px;
    padding: 12px;
		display: inline-block;
    border: 1px solid black;
    user-select: none;
  }
</style>

Babel的那些库

自从接触ES2015开始,babel就一直在用,每个项目都在用。每次初始化项目后,.babelrc@babel/core@babel/cli@babel/preset-env@babel/transform-runtime等等信手拈来,但是每次想要区别core-js@babel/poly-fill@babel/runtime@babel/plugin-transform-runtime@babel/preset-env,却又含糊不清,索性来一次整理。

我们都知道,ES的每次标准公布,都会有许多新的特性,而这些特性无非分为两部分,一部分是新语法,如箭头函数解构赋值扩展运算符for...infor...ofclassasync/await等,另一部分是API扩展,如PromiseMapSetArray.fromString.prototype.includes等。

在低版本的浏览器中要使用高版本的ES特性,就需要对其代码进行转换。对于新语法,需要通过语法转换,比如将箭头函数转化为普通函数,将赋值结构转换成单独取值,将扩展运算符转换成Object.assign等实现,而这就是babel的核心工作。对于新API,我们是可以通过JavaScript原有的语法去实现的,实现类似功能的库叫做polyfill

当然,也不是所有API都可以通过polyfill实现,比如Proxy

core-js

以模块化方式实现了ES6+的标准API,包括PromiseSymbolMapSet等,还有各种原型链上的扩展,如String.prototype.incluesArray.prototype.find等。说白了就是polyfill,对于箭头函数async/await语法,自然是无能为力的。

我们可以通过引入直接使用:

// import 'core-js' // 引入所有
import 'core-js/features/array/find';
import 'core-js/features/promise';

[1, 2, 3].find((i) => i === 1);
Promise.resolve(64).then((x) => console.log(64));

当然,这么做会直接污染全局环境,例如Promise会挂在到window上,find会挂在到Array.prototype上,也可以通过命名空间方式引入:

import find from 'core-js-pure/features/array/from';
import Promise from 'core-js-pure/features/promise';

@babel/polyfill

源码中可以看到,@babel/polyfill就是简单地直接引用core-jsregenerator-runtime。因此,它会将扩展的API直接挂载到全局(window)和对应的原型(prototype)上,但是这样会导致一些问题:

  • 所有的API都会一并引入,极大增加了打包后的体积。假如只用到了Promise特性,但是整个包的特性都打包进去了。
  • 污染了全局环境,如果在一个工具库中引入,那么所有依赖这个库的项目都会引入这个polyfill。

使用方式很简单,写在入口文件开头:

import '@babel/polyfill';

regenerator-runtime,一个提供用于Generator和异步函数的运行时。

@babel/plugin-transform-runtime

可以在一定程度上替换@babel/polyfill来使用,主要解决了两个问题:

  1. 避免产生多次的helper函数。

    babel在对新语法转换过程当中,会借助一些helper函数来实现,比如class语法:

    // 输入:
    class Circle {}
    
    // 输出:
    function _classCallCheck(instance, Constructor) {
      //...
    }
    
    var Circle = function Circle() {
      _classCallCheck(this, Circle);
    };

    这里的_classCallCheck每次转换class语法都会产生,数量多起来极大影响最终的代码体积。为了解决这个问题,有个@babel/runtime专门封装了这些helper函数。通过@babel/plugin-transform-runtime将这些helper函数的引用替换为从@babel/runtime中引入。

  2. 解决@babel/polyfill中污染全局环境和体积过大的问题。

    @babel/plugin-transform-runtime中维护了一份API和core-js特性模块间的映射,可以根据具体使用情况,按需自动引入这些core-js中的特性模块。

    // 输入:
    const p = Promise.resolve()
    
    // 输出:
    var _promise = require("@babel/runtime-corejs2/core-js/promise");
    
    var _promise2 = _interopRequireDefault(_promise);
    
    var p = _promise2.default.resolve();

但是,为了防止污染现有的运行环境,对于实例函数(如Array.prototype.find)并不支持。

babel-runtime6.x版本中,还集成了[email protected]/library(相当于core-js-pure),在7.x版本这部分被移除了,如果有需要,可以使用@babel/runtime-corejs2代替。

@babel/preset-env

在了解@babel/preset-ev先说说babel的编译过程以及pluginpreset的概念。

babel-core作为的编译过程主要分为三个步骤:

  1. 解析:通过@babbel/parser将代码转换为AST
  2. 转换:babel本身不具备代码编译转换功能,而是依赖于一个个plugin来进行处理,转换过后的代码仍然是AST
  3. 生成:通过@babel/generatorAST转换为JS代码。

所以说,plugin中实现了具体的转换功能。比如要将箭头函数转化为普通函数,就需要@babel/plugin-transform-arrow-functions,要将class语法转化为函数实现,就需要@babel/plugin-transform-classes

preset则代表一组plugin。比如需要转换jsx的语法,只需要@babel/preset-react就行了,它已经包含了@babel/plugin-transform-react-jsx@babel/plugin-transform-react-display-name@babel/plugin-transform-react-jsx-source@babel/plugin-transform-react-jsx-self

同理@babel/preset-env也代表了一组plugin。通过默认的配置,我们就能够使用ES最新规范中的语法,当然也可以通过配置来达到你需要的效果。

总结

最后简单总结一下:

  • @babel/polyfill为运行环境提供能完整的ES API,但是体积较大,新的语法如环境不支持还是不支持。
  • @babel/plugin-transform-runtime能够按需自动引入ES API,能够有效控制体积,但是不支持函数实例方法,不对语法进行转换。
  • @babel/preset-env集成若干plugin,能将ES5+语法转换成ES5语法,不集成ES API的polyfill。

Linters

Linters

JavaScript

$ npm i -D eslint-config-standard eslint-plugin-standard eslint-plugin-promise eslint-plugin-import eslint-plugin-node
// .eslintrc
{
  "extends": ["standard"]
}

TypeScript

$ npm i -D tslint tslint tslint-config-standard
// tslint.json
{
  "extends": [
    "tslint:latest",
    "tslint-config-standard"
  ],
  "rules": {
    "indent": [true, "spaces", 2],
    "interface-name": [true, "never-prefix"],
    "max-classes-per-file": true,
    "max-line-length": [true, 120],
    "member-ordering": [true, {"order": ["static-field", "static-method", "instance-field", "constructor", "instance-method"]}],
    "no-any": false,
    "no-empty-interface": false,
    "no-floating-promises": false,
    "no-shadowed-variable": true,
    "no-string-literal": true,
    "no-submodule-imports": false,
    "object-literal-sort-keys": false,
    "ordered-imports": false,
    "switch-default": true
  }
}

TypeScript + React

$ npm i -D tslint tslint tslint-config-standard tslint-react
// tslint.json
{
  "extends": [
    "tslint:latest",
    "tslint-config-standard",
    "tslint-react"
  ],
  "rules": {
    "indent": [true, "spaces", 2],
    "interface-name": [true, "never-prefix"],
    "jsx-boolean-value": [true, "never"],
    "jsx-no-multiline-js": false,
    "max-classes-per-file": true,
    "max-line-length": [true, 120],
    "member-ordering": [true, {"order": ["static-field", "static-method", "instance-field", "constructor", "instance-method"]}],
    "no-any": false,
    "no-empty-interface": false,
    "no-floating-promises": false,
    "no-shadowed-variable": true,
    "no-string-literal": true,
    "no-submodule-imports": false,
    "object-literal-sort-keys": false,
    "ordered-imports": false,
    "switch-default": true
  }
}

使用parcel配置typescript + react/preact项目

一、安装依赖

# 初始化项目
npm init -y
# 或者
yarn init -y

# 安装开发依赖
npm i --save-dev parcel-bundler typescript tslint

# 安装less(可选)
npm i --save-dev less

# 使用react(二选一)
npm i --save react react-dom react-router react-router-dom history @types/react @types/react-dom @types/react-router @types/react-router-dom @types/history

# 使用preact(二选一)
npm i --save preact preact-router history @types/history

# 创建tsconfig.json(需要npm i -g typescript)
tsc --init

# 打开tsconfig.json并设置:
# "lib": ["es6", "dom"],
# "jsx": "react"

# 创建tslint.json(需要npm i -g tslint)
tslint --init

二、配置script

package.jsonscript字段中添加:

"scripts": {
  "dev": "parcel index.html -p 80",
  "build": "parcel build index.html --public-url ./"
}

三、创建入口文件

创建index.html文件:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Hello world</title>
</head>
<body>
  <div id="root"></div>
  <script src="./src/index.tsx"></script>
</body>
</html>

创建src文件夹,在src文件夹中创建index.tsx

import * as React from 'react'
import * as ReactDOM from 'react-dom'

ReactDOM.render(
  <div id='root'>
    hello world
  </div>
, document.getElementById('root'))

// preact使用:
// import * as React from 'preact'
//
// React.render(
//   <div id='root'>
//     hello world
//   </div>
// , document.getElementById('root'))

四、运行项目

npm run start

五、配置tslint(可选)

# 安装tslint-react、tslint-eslint-rules
npm i --save-dev tslint-react tslint-eslint-rules

完整配置文件,rules请根据自己习惯或项目需求自行修改:

{
    "defaultSeverity": "error",
    "extends": [
        "tslint-react",
        "tslint-eslint-rules"
    ],
    "jsRules": {},
    "rules": {
        "align": [true, "parameters", "statements"],
        "ban": false,
        "class-name": true,
        "comment-format": [true, "check-space"],
        "curly": [true, "ignore-same-line"],
        "eofline": false,
        "forin": false,
        "indent": [ true, "spaces" ],
        "interface-name": [true, "always-prefix"],
        "jsdoc-format": true,
        "jsx-alignment": false,
        "jsx-boolean-value": false,
        "jsx-no-lambda": false,
        "jsx-no-multiline-js": false,
        "label-position": true,
        "max-classes-per-file": [false],
        "max-line-length": [ true, 120 ],
        "member-ordering": [true, "public-before-private", "static-before-instance"],
        "no-any": false,
        "no-arg": true,
        "no-bitwise": false,
        "no-console": [false],
        "no-consecutive-blank-lines": [true],
        "no-construct": true,
        "no-debugger": true,
        "no-duplicate-variable": true,
        "no-empty": true,
        "no-empty-interface": false,
        "no-eval": true,
        "no-shadowed-variable": true,
        "no-string-literal": true,
        "no-switch-case-fall-through": true,
        "no-trailing-whitespace": false,
        "no-unused-expression": true,
        "no-unused-variable": true,
        "no-use-before-declare": true,
        "object-curly-spacing": [true, "never"],
        "one-line": [true, "check-catch", "check-else", "check-open-brace", "check-whitespace"],
        "only-arrow-functions": [false],
        "ordered-imports": [false],
        "quotemark": [true, "single"],
        "radix": false,
        "semicolon": [true, "never"],
        "space-before-function-paren": [true],
        "switch-default": true,
        "trailing-comma": [false],
        "triple-equals": [ true, "allow-null-check" ],
        "typedef": [true, "parameter", "property-declaration"],
        "typedef-whitespace": [true, {"call-signature": "nospace", "index-signature": "nospace", "parameter": "nospace", "property-declaration": "nospace", "variable-declaration": "nospace"}],
        "variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore", "allow-pascal-case"],
        "whitespace": [true, "check-branch", "check-decl", "check-operator", "check-separator", "check-type", "check-typecast"]
    },
    "rulesDirectory": []
}

六、热替换

修改入口文件index.tsx

import * as React from 'react'
import * as ReactDOM from 'react-dom'

import App from './App'

declare const module: any

const render = (Component: React.ComponentClass) => {
  ReactDOM.render(
    <Component/>,
    document.getElementById('root')
  )
}

render(App)

if (module.hot) {
  module.hot.accept(() => {
    const NextApp = require('./App').default
    render(NextApp)
  })
}

根据目录使用不同的git config

通过git(>2.13)的include配置,可以合并外部的配置到~/.gitconfig,而includeIf可以添加条件,具体示例如下:

# .gitconfig
[user]
    name = varHarrie
    email = [email protected]
[includeIf "gitdir:~/company/"]
    path = .gitconfig-company
# .gitconfig-company
[user]
    name = My Name
    email = [email protected]

其中,includeIf支持以下条件参数:

  • gitdir:根据目录
  • gitdir/i:根据目录(忽略大小写)
  • onbranch:根据分支名称

具体配置,参考[官方文档](https://git-scm.com/docs/git-config#_includes)。

从开发体验探讨React模态窗口实现

业务场景

最近的项目出现大量模态窗口的应用场景,大多数业务流程也非常相似:

  • 用户点击某个按钮,弹出模态窗口,并传入一些数据
  • 这些数据可能直接用于窗口内部展示,也可能用于异步请求获取详情
  • 窗口内部有一些表单,详情的数据作为表单的初始值
  • 用户点击取消,直接关闭模态窗口
  • 用户点击确定,对表单数据进行校验或处理,发送请求保存数据,最后关闭模态窗口
  • 为了复用,这个窗口可以同时用于新增和编辑
  • 同一个页面类似功能的窗口可能同时存在多个

举个实际的例子,在一个用户列表的页面,用户列表上方有个新建用户按钮,用户列表的每一项后面都有个编辑用户按钮

  • 点击新建用户按钮,弹出用户模态窗口,里面表单初始值都是空的
  • 点击编辑用户按钮,弹出用户模态窗口,里面表单初始值从后端获取

实现和改进

按照以往做法,结合一些组件库,把每个业务场景的模态窗口封装成一个组件,例如用户窗口UserModal,然后在页面组件中引用和渲染,传入visibleonConfirm等等。

class UserListView extends React.Component {
  state = {
    modalVisible: boolean
    modalUserId: ''
  }

  // 打开新建窗口
  onCreateModalOpen = () => {
  	this.setState({modalVisible: true, modalUserId: ''})
  }
  
  // 打开编辑窗口
  onEditModal = (id) => {
    this.setState({modalVisible: true, modalUserId: id})
  }
  
  // 窗口确定
  onModalConfirm = (data) => {
    // 根据data有没有id,来判断调用新建还是保存的接口
    // ...
    // 根据接口返回信息,若成功,关闭窗口,若失败,弹出提示,保留窗口
    // ...
  }

  // 窗口取消
  onModalCancel = () => {
    this.setState({ modalVisible: false })
  }
  
  render () {
    const { modalVisible, modalUserId } = this.state
    const users = [/** 从后端获取 */]
    
		<div>
      <header>
        <button onClick={this.onCreateModalOpen}>新建用户</button>
      </header>
      <ul>
        {users.map((user) => (
          <li key={user.id}>
        		<span>{user.name}</span>
            <button onClick={() => this.onEditModal(user.id)}>编辑用户</button>
          </li>
        ))}
      </ul>
      <UserModal
        visible={modalVisible}
        id={modalUserId}
        onConfirm={this.onModalConfirm}
        onCancel={this.onModalCancel}
       />
    </div>
  }
}

页面组件UserListView需要维护模态窗口的显示状态visible,还要维护各种事件onConfirmonCancel。这还是仅仅只考虑模态窗口的逻辑的情况,而且该模态窗口依赖的modalUserId也放在的页面组件中,某种程度上说,这不是父组件需要的状态。

假如这时候需求变动原因,需要在编辑用户按钮后面再加一个编辑用户角色的按钮,点击之后弹出用户角色模态窗口,用于展示和勾选用户角色。这时,我们又要维护一份新的模态窗口状态、模态窗口事件,包括对应的命名也都要重新考虑。

经过一番思考,如果我们把模态窗口的状态、事件放进这个模块窗口组件中,会怎么样?

将窗口相关的事件、状态,都交给窗口组件自己去维护,只接受onConfirm事件,再暴露showhide之类的方法,供父组件调用。

class UserListView extends React.Component {
  refModal = React.createRef()

  // 打开新建或编辑窗口
  onModalOpen = (id) => {
  	this.refModal.current.show(id)
  }
  
  // 窗口确定
  onModalConfirm = (data) => {
    // 返回true,阻止窗口关闭
  }
  
  render () {
    const users = [/** 从后端获取 */]
    
		<div>
      <header>
        <button onClick={() => this.onModalOpen()}>新建用户</button>
      </header>
      <ul>
        {users.map((user) => (
          <li key={user.id}>
        		<span>{user.name}</span>
            <button onClick={() => this.onModalOpen(user.id)}>编辑用户</button>
          </li>
        ))}
      </ul>
      <UserModal
        ref={this.refModal}
        onConfirm={this.onModalConfirm}
       />
    </div>
  }
}

这个时候,页面组件UserListView对于UserModal就只需要关注打开窗口并传参(输入),窗口确定事件(输出)。

如果还有其他窗口,也只需要多维护一份refonModalOpenonModalConfirm

进一步改进

后来,在使用antd的modal函数调用时,联想到这个问题,发现后可以有进一步改进的实现方案。

从开发体验上讲,以上的实现方案,还有一些需要改进的地方:

  • 对于onModalOpenonModalConfirm的维护依然觉得繁琐。每个窗口都需要维护两个函数,从逻辑上讲,它们应该是“连续”的,先有onModalOpen触发窗口展示,用户操作完后触发onModalConfirm,获得反馈信息;但是代码上却分隔开来了,不同窗口间的事件还可以混合在一起。
  • ref创建之后,还需要通过在组件上传入ref={this.refModal},已将它们绑定在一起。在结合TypeScript使用时,this.refModal.current有可能是null的(在mounted之前使用会是null),需要在使用前加判断或者断言。

结合函数调用的方法,最终使用方法变成了:

class UserListView extends React.Component {
  modal = createModal(UserModal)

  // 打开新建或编辑窗口
  onModalOpen = (id) => {
    this.modal.show(id, (data) => {
      // 窗口确定回调
      // 返回true,阻止窗口关闭
    })
  }
  
  render () {
    const users = [/** 从后端获取 */]
    
		<div>
      <header>
        <button onClick={() => this.onModalOpen()}>新建用户</button>
      </header>
      <ul>
        {users.map((user) => (
          <li key={user.id}>
        		<span>{user.name}</span>
            <button onClick={() => this.onModalOpen(user.id)}>编辑用户</button>
          </li>
        ))}
      </ul>
      <this.modal.Component /> {/** 渲染窗口组件 */}
    </div>
  }
}

结语

React的魅力就在于它给予了足够广阔的实现空间,你总能发现更优的解决方案,甚至是颠覆性的。

以上功能createModal具体实现请查看gist,此外,通过结合React Hooks,还有一个新的方案@react-hero/modal,可以将<this.modal.Component />都省略了,具体实现就不展开了。

批量修改git commit author

有这么一个需求,我们在新建项目的时候,忘了修改nameemail,沿用了global中的设置,如果提交了一次commit,可以使用:

git commit –amend –author=‘[email protected]

修改上一次提交的author信息

但是,如果提交过不止一次,就不能使用这个方法了。下面是一个批量修改的办法:

#!/bin/sh

git filter-branch --env-filter '
OLD_EMAIL="[email protected]"
CORRECT_NAME="Your Correct Name"
CORRECT_EMAIL="[email protected]"
if [ "$GIT_COMMITTER_EMAIL" = "$OLD_EMAIL" ]
then
    export GIT_COMMITTER_NAME="$CORRECT_NAME"
    export GIT_COMMITTER_EMAIL="$CORRECT_EMAIL"
fi
if [ "$GIT_AUTHOR_EMAIL" = "$OLD_EMAIL" ]
then
    export GIT_AUTHOR_NAME="$CORRECT_NAME"
    export GIT_AUTHOR_EMAIL="$CORRECT_EMAIL"
fi
' --tag-name-filter cat -- --branches --tags

此方法出自github help

这里有关于这个问题的更多讨论

React withProps

实现withProps函数,用于提前注入组件属性。

import { ComponentProps, ComponentType, ForwardRefExoticComponent, forwardRef, memo } from 'react';

type ElementType = keyof JSX.IntrinsicElements | ComponentType;

type GetProps<C extends ElementType, E> =
  | Partial<ComponentProps<C>>
  | ((props: ComponentProps<C> & E) => Partial<ComponentProps<C>>);

export function withProps<C extends ElementType>(
  Component: C,
): <EP = {}>(getProps: GetProps<C, EP>) => ForwardRefExoticComponent<ComponentProps<C> & EP> {
  const Comp = Component as any; // Fixed "Expression produces a union type that is too complex to represent."

  return (getProps) => {
    return memo(
      forwardRef((props: any, ref) => {
        const injectedProps = typeof getProps === 'function' ? getProps(props) : getProps;
        return <Comp ref={ref} {...props} {...injectedProps} />;
      }),
    );
  };
}

示例:

import { CSSProperties } from 'react';

type InputProps = {
  type?: 'text' | 'password' | 'file';
  value?: string;
  style?: CSSProperties;
};

function Input(props: InputProps) {
  return <input {...props} />;
}

const PasswordInput = withProps(Input)({ type: 'password' });
// const PasswordInput = withProps('input')({ type: 'password' });

const CustomInput = withProps(Input)<{ block?: boolean }>((props) => ({
  style: props.block ? { display: 'block' } : undefined,
}));

const password = <PasswordInput value='123456' />

const custom = <CustomInput block value='hello' />

设置淘宝源的npm

非常感谢淘宝提供的NPM镜像,使国内下载NPM包速度有了极大的提升。

为了便于使用,官方还提供了一个cnpm命令行工具,通过npm install -g cnpm --registry=https://registry.npm.taobao.org安装,变可以用cnpm代替npm安装node模块

但是,由于cnpm安装机制跟npm上有区别,会导致了诸如babel全家桶一类的包安装错误(不知道现在还有没有这个问题)

为了提高包安装速度,也尝试过使用前段时间Facebook推出的yarn,速度上确实有明显提升,功能上也几乎与npm等同。只可惜,处于安全考虑,它不支持一些安装过程中带执行脚步的包。依然不是完美的替代品

下面提供一种,既能保持完整的npm功能,又能保证国内下载速度等方法

npm的参数registry提供了更改下载源的方法,使用npm安装任意包的时候可以添加--registry=https://registry.npm.taobao.org,以切换到淘宝源,下载速度上跟cnpm无异。

但是,每次安装都需要添加一大段的命令,既难记又不方便。

在linux和macos中,可以通过alias添加别名命令:

alias cnpm="npm --registry=https://registry.npm.taobao.org"

在windows中并没有这样的方法,这里提供一种实现方法:

  1. 在任意目录下创建一个文件夹,用来存放下面三个文件,例如我创建了一个c:\bin目录
  2. 在该目录下建立文件aliases,内容如下
ls=dir /ONE $*
cd=cd /d $*
cnpm=npm $* --registry=https://registry.npm.taobao.org

这个文件用于定义别名命令,你也可以添加其他命令
3. 在该目录建立文件cmd_autorun.cmd,内容如下

@echo off
cls
doskey /macrofile=c:\bin\aliases

这个文件用于注册这些命令,注意将macrofile的路径改成你自己的
4. 在该目录下建立set_cmd_autorun.reg,内容如下

REGEDIT4

[HKEY_CURRENT_USER\Software\Microsoft\Command Processor]
"Autorun"="c:\\bin\\cmd_autorun.cmd"

这个文件用于开机自动注册命令,注意将Autorun路径改成你自己的
5. 然后运行cmd_autorun.cmdset_cmd_autorun.reg文件
6. 之后再也不用管这些文件了,并且保证这些文件不会被删除

使用npm-link开发react组件库,遇到“have multiple copies of React loaded”的坑

最近把一些在项目中用到的组件抽离出来,封装成一个组件库,然后关联出去npm link,再在原项目中通过npm link my-components的方式关联回这个组件库。

运行时发现一个错误:

Uncaught Invariant Violation: addComponentAsRefTo(...): Only a ReactOwner can have refs. You might be adding a ref to a component that was not created inside a component's render method, or you have multiple copies of React loaded (details: fb.me/react-refs-must-have-owner).

检查过后并没用发现在render函数以外的地方用到ref,于是怀疑是不是have multiple copies of React loaded

这里找到两种解决方案:

  1. 修改当前项目的webpack配置:
resolve: {
  alias: {
    react: path.resolve('./node_modules/react'),
  },
}
  1. 利用npm-link的方式:
# 连接组件库
cd my-app
npm link ../my-components

# 将项目中的react连接到组件库
cd ../my-components
npm link ../my-app/node_modules/react

这两种方法都达到使react都指向同一份引用,后者更适用于库的开发。

一步步搭建React项目(一):把React跑起来

本教程侧重点在于项目结构的演变过程,为初学者提供渐进式的React搭建方法,React相关基础知识请查阅官方文档。

本教程为本人在学习过程中总结而记录下来,难免存在错误,或您有任何建议,欢迎提出

1. 创建项目

$ mkdir react-demo
$ cd react-demo
$ npm init

2. 添加入口文件

首先创建一个html入口文件index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>React Demo</title>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>

通过emmet,! + Tab键可以生成HTML模板代码。值得注意的是,body中有一个id为app的div,后面用到

创建一个js入口文件src/main.js(为了便于后续工作,放在src目录内):

ReactDOM.render(
  <h1>Hello, world!</h1>,
  document.getElementById('app')
)

src/main.js引入到index.html中:

<!-- ... -->
<body>
  <div id="app"></div>
  <script src="src/main.js"></script>
</body>
<!-- ... -->

实现的效果将会是,把内容为Hello, world!h1标签渲染到id为appdiv

毫无疑问,使用浏览器打开index.html是跑不起来的。一是因为我们没有引入reactreact-dom包,二是因为这里用到了jsx语法,需要通过babel进行转义

3. 安装相关依赖

为了方便起见,我们将reactreact-dom通过cdn形式引入,添加到index.htmlbody中

<!-- ... -->
<body>
  <div id="app"></div>
  <script src="https://unpkg.com/react@15/dist/react.js"></script>
  <script src="https://unpkg.com/react-dom@15/dist/react-dom.js"></script>
  <script src="src/main.js"></script>
</body>
<!-- ... -->

后续教程中,我们会使用webpack进行模块管理

安装babel-clibabel-preset-react

$ npm install --save-dev babel-cli babel-preset-react

我们将babel-cli安装到了本地依赖,运行babel需要借助npm script,将运行脚本添加到package.jsonscripts中:

// ...
"scripts": {
  "build": "babel --presets react src --out-dir build --watch"
},
// ...

你也可以将babel-cli安装到全局,那么不需要借助npm script来运行

紧接着运行npm run build,我们可以看到新产生的build目录,以及该目录里面的main.js文件

别忘了修改index.htmlscript,引用的位置:

<!-- ... -->
<body>
  <div id="app"></div>
  <script src="https://unpkg.com/react@15/dist/react.js"></script>
  <script src="https://unpkg.com/react-dom@15/dist/react-dom.js"></script>
  <script src="build/main.js"></script><!-- 修改位置 -->
</body>
<!-- ... -->

刷新浏览器,我们可以看到Hello, world!在页面中显示,恭喜成功入门。

4. 总结和改进

回顾整个项目,不难发现有许多可以改进的地方:

  • 没有使用模块管理,模块单纯使用script标签引入,甚至通过CDN引入到全局
  • 没有热加载,每次编辑都要刷新页面
  • 没有使用ES2015+的语法,严重影响开发效率
  • 没有引入环境变量,项目缺少适用性
  • ...

后续教程中,我们将一一解决这些问题

JavaScript原型链的简单总结

JavaScript中万物皆是对象,且对象细分为普通对象函数对象

凡是通过new Function()创建的均为函数对象,如ObjectFunctionNumberBooleanStringArrayRegExp等等,其余都是普通对象

  1. 函数对象__proto__均指向Function.prototype,且为空函数function () {}
  2. 普通对象__proto__均指向该对象构造函数prototype
  3. Function.prototype.__proto__ === Object.prototype
  4. Object.prototype.__proto__ = null

对于普通对象:

function Person () {}
Person.prototype.say = function () {}

var p = new Person()
p.say()

Person相当于是构造函数,pPerson的实例,并且继承p的原型p.__proto__,即Person.prototype的所有属性。

当然,Person本身也是函数对象,这里我们考虑的是Person的实例p

p.constructor === Person // function Person () {}
p.constructor === Person.prototype.constructor // function Person () {}
p.__proto__ === Person.prototype // {say: function () {}}
p.__proto__ === p.constructor.prototype // {say: function () {}}
p.__proto__.__proto__ === Person.prototype.__proto__ === Object.prototype

对于函数对象:

var f = new Function()

f.__proto__ === Function.prototype // function () {}
f.__proto__.__proto__ === Function.prototype.__proto__ === Object.prototype

注意:

当我们重写原型对象,而不是修改原型对象时:

function Ghost () {}
Ghost.prototype = {} // 直接赋值一个对象

var g = new Ghost()

g.__proto__ === Ghost.prototype
g.__proto__ !== g.constructor.prototype
g.__proto__ === Object.prototype

这时,g.constructor指向的是Object,可以通过指定constructor避免这种情况:

function Ghost () {}
Ghost.prototype = {
  constructor: Ghost
}

var g = new Ghost()

g.__proto__ === Ghost.prototype === g.constructor.prototype

本文参考最详尽的 JS 原型与原型链终极详解,没有「可能是」进行总结,感谢原作者。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.