Coder Social home page Coder Social logo

blog's Introduction

  • 👋 Hi, I’m @ASkyBig
  • 👀 I’m interested in basketball and dota1.
  • 🌱 I’m currently learning low code.
  • 📫 How to reach me empzd2s4MTg=

blog's People

Contributors

askybig avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

blog's Issues

js的new操作符

js中是没有类的概念的,ES6中的class其实也是函数实现的,是一个语法糖。js之父布兰登・艾克只花了10天就完成了js这门语言,可能就是想要一个简单的语法。
传统面向对象的语言
我们以 Java 为例:

class Person {
  String name;
  public Person (String name) {
    this.name = name;
  }
   
  public void sayHi() {
    System.out.println("hi");
  }
}

可以看到,Person 类里面有一个 name 属性,还有一个 Person 构造函数(这个构造函数不是必须的,如果不写有一个默认的构造函数),还有一个 sayHi 方法。这样我们新建一个对象的时候,就会自动调用构造函数,然后就有 sayHi 方法了,还有 name 属性。
JavaScript
由于js没有类的概念,所以我们只能通过函数实现。js 里面的函数是可以当做构造函数使用的,前提是在函数前面加上 new 操作符。

function Person(name) {
  this.name = name;
}
const p1 = new Person('jack');

我们就有p1这个实例了,我们看看:
image

可以看到,p1 已经有了 name 这个属性了。(眼尖的同学可能已经发现,p1 实例还有一个 proto 属性,这个我们稍后再说)
现在我们已经有了带属性的实例了,还有一个 sayHi 方法,这个该怎么办呢?不慌,js 已经给我们想好了,给 Person函数添加一个 prototype 属性(函数也是对象,当然可以添加属性),我们把 sayHi 方法挂载在 Person.prototype 对象上。当我们调用 p1.sayHi() 的时候,js 发现 p1 上没有sayHi这个方法,就会去它的__proto__属性上去找,知识点来了:实例的__proto__属性指向的就是构造函数的prototype属性。所以就可以调用sayHi方法了:
image

当然,如果我们调用sayHello方法的话是没有用的,因为prototype上并没有定义这个方法。
当我们调用 p1.sayHello() 时,首先会去看p1这个对象有没有sayHello这个方法,没有。那么就去它的构造函数的prototype上找,也没有。那么继续去它的构造函数的 prototype 的构造函数的prototype上找,即Person.prototype的构造函数的prototype上找。参考上面提到的知识点,所以就是去Person.prototype.proto(即 Object.prototype)上找。如果Object.prototype上也没有,再往上就是null了(即Object.prototype.proto === null)。此时,p1.sayHello就是undefined,调用的话就会报sayHello is not a function 错误了。
image

我们可以试验一下,给Object.prototype加上sayHello这个方法,看看p1能不能调用:
image
可以看到,hello 已经成功调用了!如果上面的你看懂了,那你就理解什么是原型链了。

new 的作用

至此,我们已经知道 new 大概的作用了:

首先,它会新建一个对象
然后,对新对象执行 prototype 连接。(这一步是为了调用构造函数的方法。)
然后,将新对象绑定到函数调用的 this。(关于this,篇幅原因就不介绍了。)
最后,如果函数没有返回其他对象,new 表达式中的函数调用会自动返回这个新对象。

上面的步骤是从 《you don't know js》中摘的。我们来看下最后一句,这句有点意思:如果没有返回其他对象,就自动返回新对象。
那么问题来了,我们偏要返回其它对象呢?我们试试看:

  • 对象分为很多种,我们先返回一个常见的对象。
    image
    看到没,当我们返回一个普通对象时,这个实例就和Person就没啥关系了,扎心了,辛辛苦苦定义的构造函数和 const p1 = {a: 1} 没啥差别了。
  • 数组 [] 这个也是对象,我们试试
    image
    还是一样,浪费了Person函数 : (
  • 包装类型也是对象,例如 String('abc')
    image
    一样,浪费了
  • 再来一个 null 吧(不会吧不会吧,你不知道 null 是对象吗?它可是000开头的哦)
    image
    可以看到,虽然null是对象,但是它没有叛变,还是跟着Person构造函数混的。
  • 如果我们单纯返回一个基本类型呢
    image
    小黄书诚不欺我,如果没有返回其它对象,就返回新对象!毕竟基本类型它不是对象:)

把上面的例子总结下:

1、如果构造函数不返回对象(这句有两层意思哈,不返回对象和不返回对象),则返回默认新建的对象
2、如果构造函数返回了对象(null不算血统纯正的对象),那构造函数啥也不是,没用

手写 new

知道了 new 的原理,手写下扎实基础(还不是为了面试:D)

function _new (fn, ...args) {
  // 新建一个对象
  const obj = {};
  // 执行构造函数
  const res = fn.call(obj, ...args);
  // 将__proto__指向构造函数的prototype
  obj.__proto__ = fn.prototype;
  // 看看返回值是不是叛徒(null长得像叛徒,但不是叛徒)
  const isObject = Object.prototype.toString.call(res) === "[object Object]" && res !== null;
  const isFunction = Object.prototype.toString.call(res) === "[object Function]";
  // 是叛徒
  if (isObject || isFunction) return res;
  // 不是叛徒
  return obj;
}

让我们验证下:
image
再来看看返回对象的情况:
image
和预期一样,也是个浓眉大眼的,背叛了革命。
(完)

hooks 的知识点

hooks 诞生的原因

类组件不不足

1 、状态逻辑复用困难

  • 缺少复用机制
  • 渲染属性和高阶组件导致的层级冗余

2、 复杂难以维护

  • 生命周期函数混合不相干逻辑
  • 相干逻辑分散在不同生命周期

3、this 指向麻烦

  • 内联函数过度创建新句柄
  • 类成员函数不能保证this

hooks 优势

  • 函数组件无 this 问题
  • 自定义 hook 逻辑复用方便
  • 副作用关注点分离

与class的对比

对于一个按钮点击count加一,分别用 classfunction 形式写一遍:

class 形式 codesandbox

import React from "react";

export default class App extends React.Component {
  state = {
    count: 0
  };
  render() {
    const { count } = this.state;
    return (
      <button onClick={() => this.setState({ count: count + 1 })}>
        Count{count}
      </button>
    );
  }
}

function 形式codesandbox

import React, { useState } from "react";

export default function App() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>Count{count}</button>;
}

可以看到,function 形式的组件比 class 形式的组件简洁很多。

useState

  • 如果 count 为0, setCount(0) 不会触发重新渲染 codesandbox
import React, { useState } from "react";

export default function App() {
  const [count, setCount] = useState(0);

  return <button onClick={() => setCount(0)}>Click({count})</button>;
}
  • 如果从 props 里面取值复杂,将 useState 参数作为函数返回值,只会执行一次codesandbox
import React, { useState } from "react";

export default function App(props) {
  const [count, setCount] = useState(() => {
    console.log("get props");
    return props.defaultCount || 0;
  });

  return <button onClick={() => setCount(count + 1)}>Count{count}</button>;
}

useEffect

import React from "react";
class Count extends React.Component {
  state = {
    count: 0,
    size: {
      width: document.documentElement.clientWidth,
      height: document.documentElement.clientHeight
    }
  };
  onResize = () => {
    this.setState({
      size: {
        width: document.documentElement.clientWidth,
        height: document.documentElement.clientHeight
      }
    });
  };
  // resize 的监听和解绑分散在不同生命周期
  componentDidMount() {
    // title 和 resize 耦合在一起
    document.title = this.state.count;
    window.addEventListener("resize", this.onResize, false);
  }
  componentWillUnmount() {
    console.log("remove listener");
    window.removeEventListener("resize", this.onResize, false);
  }
  componentDidUpdate() {
    document.title = this.state.count;
  }
  render() {
    console.log("render");
    const { count, size } = this.state;
    return (
      <button onClick={() => this.setState({ count: count + 1 })}>
        Count({count}) Size: {size.width}*{size.height}
      </button>
    );
  }
}
export default class App extends React.Component {
  state = {
    id: 1
  };

  render() {
    const { id } = this.state;
    return (
      <div>
        {id & 1 ? <button>111</button> : <Count />}
        <button onClick={() => this.setState({ id: id + 1 })}>appClick</button>
      </div>
    );
  }
}

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

export default function App() {
  const [id, setId] = useState(0);
  return (
    <div>
      {id & 1 ? <button>111</button> : <Count />}
      <button onClick={() => setId(id + 1)}>app click</button>
    </div>
  );
}

function Count() {
  console.log("render");
  const [count, setCount] = useState(0);
  const [size, setSize] = useState({
    width: document.documentElement.clientWidth,
    height: document.documentElement.clientHeight
  });

  const onResize = () => {
    setSize({
      width: document.documentElement.clientWidth,
      height: document.documentElement.clientHeight
    });
  };
// 关注点分离,title 和 resize 各自处理各自逻辑
  useEffect(() => {
    console.log("title");
    document.title = count;
  });

  useEffect(() => {
    console.log("resize");
    window.addEventListener("resize", onResize, false);
    return () => {
      console.log("remove listener");
      window.removeEventListener("resize", onResize, false);
    };
  }, []);

  return (
    <button onClick={() => setCount(count + 1)}>
      Count({count}) Size({size.width}* {size.height})
    </button>
  );
}

useEffect 的一个错误的示例 codesandbox,本意是想让 count 累加到10,然后自动停止。

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

export default function App() {
  const [count, setCount] = useState(0);
  let timer;

  useEffect(() => {
    timer = setInterval(() => {
      console.log("setInterval", count);
      setCount(count + 1);
    }, 1000);
  }, []);

  useEffect(() => {
    if (count > 10) {
      clearInterval(timer);
    }
  });
  return <button onClick={() => setCount(count + 1)}>Count({count})</button>;
}

需要像下面这样:codesandbox

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

export default function App() {
  const [count, setCount] = useState(0);
  let timer;

  useEffect(() => {
    timer = setInterval(() => {
      console.log("setInterval", count);
      setCount(count => count + 1);
    }, 1000);
  }, []);

  useEffect(() => {
    if (count > 10) {
      clearInterval(timer);
    }
  });
  return <button onClick={() => setCount(count + 1)}>Count({count})</button>;
}

还有一个问题,如果上面代码中

 useEffect(() => {
    timer = setInterval(() => {
      console.log("setInterval", count);
      setCount(count => count + 1);
    }, 1000);
  }, []);

变成下面这样,会导致什么问题呢?

 useEffect(() => {
    timer = setInterval(() => {
      console.log("setInterval", count);
      setCount( count + 1);
    }, 1000);
  }, [count]);

useMemo

目前理解不是很深刻

有记忆功能,点击 age 的时候不会改变 count,所以就不会输出 calc countcodesandbox

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

const Counter = function(props) {
  console.log("Counter");
  return <div>{props.count}</div>;
};
export default function App() {
  const [count, setCount] = useState(0);
  const [age, setAge] = useState(20);

  const dobule = useMemo(() => {
    console.log("calc count");
    if (count > 3) {
      return count * 2;
    }
  }, [count]);

  return (
    <div>
      <Counter count={dobule} />
      <button onClick={() => setAge(age + 1)}>age: {age}</button>
      <button onClick={() => setCount(count + 1)}>
        Count({count}) double({dobule})
      </button>
    </div>
  );
}

不过有一个问题, Counter 组件会一直渲染,其实该组件只依赖于 count,不应该点击 age 时重新渲染它。可以用 memo 包裹一下就好了,这样你点击 age 就不会重渲染 Countercodesandbox

import React, { useState, useMemo, memo } from "react";

const Counter = memo(props => {
  console.log("Counter");
  return <div>Counter({props.count})</div>;
});

export default function App() {
  const [count, setCount] = useState(0);
  const [age, setAge] = useState(20);

  const dobule = useMemo(() => {
    console.log("calc count");
    if (count > 3) {
      return count * 2;
    }
  }, [count]);

  return (
    <div>
      <Counter count={dobule} />
      <button onClick={() => setAge(age + 1)}>age: {age}</button>
      <button onClick={() => setCount(count + 1)}>
        Count({count}) double({dobule})
      </button>
    </div>
  );
}

useRef

  • 获取dom元素
  • 不同生命周期需要共享的数据

自定义hook

把上面的例子用自定义hook重写:codesandbox

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

function useCount1(count) {
  return <h1>{count}</h1>
}

function useCount(defaultCount) {
  const [count, setCount] = useState(defaultCount);
  let timer = useRef();

  useEffect(() => {
    timer.current = setInterval(() => {
      console.log("setInterval", count);
      setCount(count => count + 1);
    }, 1000);
  }, []);

  useEffect(() => {
    if (count >= 10) {
      clearInterval(timer.current);
    }
  });
  return [count, setCount];
}
export default function App() {
  const [count, setCount] = useCount(0);
  const Count1 = useCount1(count)
  return (
    <div>
      {Count1}
      <button onClick={() => setCount(count + 1)}>Count({count})</button>;
    </div>
  )
}

优化网络请求

最近做的中台项目里面网络请求混乱,有页面内的 fetchaxios,还有service文件夹下专门处理请求,需要统一

例如,项目目前的 axios 的请求方式:

  setLoading(true)
  axios
    .get(url, {
      params: {
        ...
      }
    })
    .then(res => {
      if (res?.code === 0) {
        const {data} = res
        setDataSource(data)
      } else {
        message.error(res.message || '请求失败')
      }
    })
    .catch(err => {
      message.error(err.message || '请求失败')
    })
    .then(() => {
      setLoading(false)
    })

代码中值得称道的地方就是在 catch 下面的 then 里面统一移除 loading 效果,这样避免了分别在 successfail 下隐藏 loading。很多人不知道 catch 下面还可以继续 then

但是问题是,我们的请求很多,每次都这样写,需要每一次都判断 code,每一次都添加 catch。并且,有些场景是需要嵌套请求,例如某个页面需要初始化数据,然后根据初始化数据再进行请求,那就需要在 then 里面再复制一遍请求。
代码嵌套变深,不利于维护,所以需要统一封装请求。

可以这样封装:

  const $fetch = async (url, type, params) => {
    try {
      const res = await axiosFn(url, type, params)
      return [null, res]
    } catch (e) {
      return [e, null]
    }
  }

但是这样的不足是我们还是需要判断 code 是否为0的情况:

setLoading(true)
const [err, res] = await $fetch(url, type, params)

if (res) {
  if (res.code === 0) {
    const {data} = res
    setDataSource(data)
  } else {
     message.error(res.message)
  }
} else {
  message.error(err.message)
} 
setLoading(false)

所以我们继续优化:

  const $fetch = async (url, options) => {
    let res = {}
    let err = null
    try {
      res = await axios(url, options)
    } catch (error) {
      err = error
    }
    if (res.code !== 0) err = res;
    if (err !== null) message.error(err.errmsg || err.message || '请求失败')
    return [res, err] // 因为这里只有两个返回值,所以数组即可,否则需要返回对象,方便调用方接收处理
  }

这样的好处是,更加扁平化,并且我们不需要单独处理 code 不为0的情况,也不需要在失败时 toast

  setLoading(true)
  const [res, err] = $fetch(url, params)
  if (err !== null) return
  // 这里可以这样写的原因是 data 不会为 null,否则需要 const data = res.data || {}
  const {data = {}} = res 
  setDataList(data)
  setLoading(false)

新功能可以采取这样的方式,但是旧功能的重构,就像维修飞驰的火车,有点难度,需要阅读下《重构》

【智能中台】历史代码问题汇总

最近在做中台项目,优化一下历史代码

中台开发体会

接口评审需要注意的事项

  • 如果是 Select 数据源,希望返回 {label: xx, value: xx} 的格式

不足

  • 中台的响应速度要求很高,如果不能及时响应,会导致业务方block。但是中台业务很多,这个优先级就很难处理。
  • 多加一层,其实会多一点出错的概率。可能前端 mock 正常,服务自测也正常,但是中台服务挂了,这种就会浪费时间找问题。

代码问题

1、只有两种条件的时候,第二个返回不需要 else

修改前:

if (item.cbid) {
  return (
    <Link to={`/tagrulebook/booktag/${item.cbid}`} className='c404aff' target='_blank'>
      {text}
    </Link>
  );
} else {
  return text;
}

修改后:

if (item.cbid) {
  return (
    <Link to={`/tagrulebook/booktag/${item.cbid}`} className='c404aff' target='_blank'>
      {text}
    </Link>
  );
}
return text;

2、arr.length === 0

  • 判断一个数组是否为空,不需要加上 === 0
  • 利用可选链,不需要多判断一次是否存在

修改前:

if (value && value.length === 0) {
  // do something
} 

修改后:

if (value?.length) {
  // do something
}

3、重置数组对象不需要 [].concat(arr)

直接利用扩展运算符就好了

修改前:

dataRem[dataRem.indexOf(item)].status = 2
setDataRem([].concat(dataRem))

修改后:

dataRem[dataRem.indexOf(item)].status = 2
setDataRem([...dataRem])

4、利用空值合并操作符 ?? 代替 || 和 三目运算符

代替 ||:

  • 比如有些时候,我们就需要一个空,但是用 || 就有问题了,'' || 1 的结果是 1
  • 或者数值的时候,如果我们输入了 00 || '暂无金额' 的结果是 暂无金额

修改前:

<div>标签描述:{item?.description || '暂无描述'}</div>

修改后:

<div>标签描述:{item?.description ?? '暂无描述'}</div>

代替 ?...::
修改前:

const val = value ? value : ''

修改后:

const val = value ?? ''

5、利用三目运算符 ? :取代 if...else

可以让代码更加简洁

修改前:

if (item.value === '0') {
  const name = platform[0][0]['name']
  const value = platform[0][0]['value']
  onFilter({[name]: value})
} else {
  const name = platform[1][0]['name']
  const value = platform[1][0]['value']
  onFilter({[name]: value})
}

修改后:

const [name, value] = item.value === '0'
  ? [platform[0][0]['name'], platform[0][0]['value']]
  : [platform[1][0]['name'], platform[1][0]['value']]
onFilter({[name]: value})

6、将通用事件放到 catchthen 后面做

可能有些人不知道 catch 后面返回的还是一个 promise

修改前:

setLoading(true)

axios
  .get(url)
  .then(res => {
    setLoading(false)
     // do something
  })
  .catch(err => {
    setLoading(false)
    // do something
  })

修改后:

setLoading(true)

axios
  .get(url)
  .then(res => {
     // do something
  })
  .catch(err => {
    // do something
  })
  .then(() => {
     setLoading(false)
  })

7、减少无意义的代码

有时候,我们会写出一些冗余代码(当然,有时候适当冗余是好的)

修改前:

function foo () {
  if (val === null || val === undefined) return
  if (val === '') return
  // do something
}

修改后:

function foo () {
  if (!!val) return
  // do something
}

修改前:

const search = value => {
  if (!value) {
    initBookInfo(1, 10)
    return
  }
  initBookInfo(1, 10)
}

修改后:

const search = value => {
  initBookInfo(1, 10)
}

8、提取通用逻辑

下面的代码是分别点击黑白名单的时候展示对应的数据,代码基本上都是通用的:
修改前:

if (bookRadioBox.includes('1')) {
  const [res, err] = await $fetch(`/recSubject/subset_contents?subset_id=${sublibId}&page_size=10&page_num=1`, {
  method: 'GET'
  })
  if (err !== null) return
  setBookInfoLoading(false)
  setBookInfo(res.data)
} else if (bookRadioBox.includes('2')) {
  const [res, err] = await $fetch(`/recSubject/whiteBooks?subset_bw_id=${formData.whitelist}&page_size=10&page_num=1`, {
  method: 'GET'
  })
  if (err !== null) return
  setBookInfoLoading(false)
  setBookInfo(res.data)
} else if (bookRadioBox.includes('3')) {
  const [res, err] = await $fetch(`/recSubject/blackBooks?subset_bw_id=${formData.whitelist}&page_size=10&page_num=1`, {
  method: 'GET'
  })
  if (err !== null) return
  setBookInfoLoading(false)
  setBookInfo(res.data)
}

修改后:

let url
if (bookRadioBox.includes('1')) {
  url = `/recSubject/subset_contents?subset_id=${sublibId}&page_size=10&page_num=1`
} else if (bookRadioBox.includes('2')) {
  url = `/recSubject/whiteBooks?subset_bw_id=${formData.whitelist}&page_size=10&page_num=1`
} else if (bookRadioBox.includes('3')) {
  url = `/recSubject/blackBooks?subset_bw_id=${formData.whitelist}&page_size=10&page_num=1`
}

const [res, err] = await $fetch(url, { method: 'GET' })
if (err !== null) return
setBookInfoLoading(false)
setBookInfo(res.data)

9、表单字段不能空,需要区分类型

不能单纯利用 str.length > 0 判断字段,因为可能是 number。可以统一成 String(str).length > 0

利用高阶函数缓存数据并优化性能

今天在开发智能的需求的时候,偶然发现了一个问题,就是两种类型的数据切换的时候有点慢,调试了一下,发现了问题,就进行了一下优化。

问题

今天开发的一个功能和这个类似,查看这个功能的时候,发现了一个问题:切换 CheckBox 的时候,有一点点卡顿的感觉。
image
image

测试环境还好,但是在本地调试的时候,延迟已经挺严重了。

分析

通过代码审查,发现了产生这个现象的原因:每次切换都会重新计算数据:
代码:
index.js
image

IpCard.js
image

我们请求的数据结构是下面这样的,一共有好几十个类别,每个类别下又有几十上百个标签,测试环境的数据就有好多万个,真实情况下数据量更大。
每次点击都对这么多数据进行处理,所以会造成卡顿。
image

解决方案

这种情况下,我们应该有下面两种解决方案:

  • 后端排序
  • 前端缓存

可能是由于历史原因,以前的接口都是前端排序的。由于现代浏览器性能已经比较优秀了,那我们就在不浪费后端人力的情况下,前端处理。
我们利用高阶函数,对纯函数进行缓存,利用 key 是否存在进行判断:

image

此时,我们将 IpCard.js 中的计算移到父组件,并对数据进行缓存:

image

这样我们每次切换就好多了:)
原理
主要是利用了闭包:

image
类似单例模式,存在则直接取值,我们简化一下代码:

image

(完)

了解 HTTP 的 Cookie

今天下雨,打不了球了: (
看了会 HTTP 的 Cookie 知识,记录一下。

Cookie 的起源

因为 HTTP 是无状态的,服务器不知道将多个商品添加到购物车的是不是同一个用户。为了解决这个问题,网景的工程师 Lou Montulli 发明了 Cookie。

工作原理

  1. 浏览器发送用户凭证
  2. 服务器验证通过后创建一个 Session,并返回一个 SessionID 交由浏览器存到本地 Cookie 里
  3. 第二次请求时,浏览器会检查本地是否包含请求域名的对应 Cookie,如果有自动带着发给服务器
  4. 服务器检查浏览器发过来的 SessionID,进行判断

常见字段

我们启动一个 node 服务来了解一下 Cookie 里面的各个字段:

// 导入 http 内置模块
const http = require('http')
// 这个核心模块,能够帮我们解析 URL地址,从而拿到  pathname  query
const url = require('url')
// 创建一个 http 服务器
const server = http.createServer()
// 监听 http 服务器的 request 请求
server.on('request', function (req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('cookieLearn')
})
// 指定端口号并启动服务器监听
server.listen(3000, function () {
    console.log('server listen at http://127.0.0.1:3000')
})

毕竟也是持久化的一种,所以 Cookie 是存在于 Application 下的 Storage 里面,红框里就是常见的几个字段。
image
我们分别看下:

  • name、value
    这个就是键值对:
    image
    这里我看 MDN有这么一句话:
    image

但是我在 Chrome 里面的确可以用括号的,浏览器支持。

  • domain
    这个字段是指定 cookie 可以送达的主机名。域名之前的点号会被忽略。假如指定了域名,那么相当于各个子域名也包含在内了。我查看了 GitHub.com 的 Cookie,它的 domain 有的是 .github.com 有的是github.com,其实只需要设置 github.com 就行了。而且它输入 www.github.com 也会重定向到 github.com。
    还有一点是这里提到的是域名,并不包含端口,所以不同端口也可以共享这个 Cookie。我们可以验证下,本地再启动一个 4000端口:
// 导入 http 内置模块
const http = require('http')
// 这个核心模块,能够帮我们解析 URL地址,从而拿到  pathname  query
const url = require('url')
// 创建一个 http 服务器
const server = http.createServer()
// 监听 http 服务器的 request 请求
server.on('request', function (req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('cookieLearn:4000')
})
// 指定端口号并启动服务器监听
server.listen(4000, function () {
    console.log('server listen at http://127.0.0.1:4000')
})

访问一下,可以看到同样有这个 Cookie:
image

  • path
    这个字段指定一个 URL 路径,这个路径必须出现在要请求的资源的路径中才可以发送 Cookie 首部。一般我们都默认设置为 /
    下面我们设置一个路径测试下:
// 导入 http 内置模块
const http = require('http')
// 这个核心模块,能够帮我们解析 URL地址,从而拿到  pathname  query
const url = require('url')
// 创建一个 http 服务器
const server = http.createServer()
// 监听 http 服务器的 request 请求
server.on('request', function (req, res) {
    const parseUrl = url.parse(req.url, true)
    res.writeHead(200, {'Content-Type': 'text/plain', 'Set-Cookie': 'age=aaa; path=/doc'})  
    res.end('cookieLearn:4000')
})
// 指定端口号并启动服务器监听
server.listen(4000, function () {
    console.log('server listen at http://127.0.0.1:4000')
})

如下图所示,当我们访问 /doc 这个路径时,Cookie 是有的。但是访问 /doc1 这个路径,Cookie 就没有了。
image
image

  • Expires、MaxAge
    Expires 和 MaxAge 是两个作用相同的字段,所以 Chrome 把它们两个放一起了。它们的格式不一样, Expires 的格式是 Date 格式,而 Max-Age 是数字形式的多少秒。
    Expires 是 HTTP 1.0 的首部字段,因为客户端和服务端时间误差过大导致问题,所以才有 HTTP 的 Max-Age 字段。
    默认值是 Session,意味着只是在当前会话保存该 Cookie:
    image

这里注意一个问题,这里设置的不是本地时间,所有可能有 Cookie 没有过期的情况,因为这个时间并没有过服务器的时间呢:

res.writeHead(200, {'Content-Type': 'text/plain', 'Set-Cookie': 'age=aaa; path=/; Expires=Mon Dec 02 2019 20:00:00 GMT'})  

image
image

  • Size
    这个没什么好说的,就是 name 和 value 的大小之和:
    image

  • HttpOnly
    这个是防止遭受了 XSS 攻击之后不让别人获取 Cookie。
    注意,这个可不能防止 XSS 攻击,只是降低损失,不让他做一些操作而已。

res.writeHead(200, {'Content-Type': 'text/plain', 'Set-Cookie': 'agee=aaa; Httponly'}) 

image

  • Secure
    见字识意,就是在 SSL 和 Https 的时候才能传输 Cookie。的确设置了之后前端也是没有这个 Cookie 的。
  • SameSite
    这个主要是为了避免 CSRF 攻击,这个比较麻烦,就不举例了,有机会专门总结一下 CSRF。

进阶

这部分是别人写的,应该是后端的范畴了,顺便了解一下。

如何考虑分布式 Session 问题?

在互联网公司为了可以支撑更大的流量,后端往往需要多台服务器共同来支撑前端用户请求,那如果用户在 A 服务器登录了,第二次请求跑到服务 B 就会出现登录失效问题。
分布式 Session 一般会有以下几种解决方案:

Nginx ip_hash 策略,服务端使用 Nginx 代理,每个请求按访问 IP 的 hash 分配,这样来自同一 IP 固定访问一个后台服务器,避免了在服务器 A 创建 Session,第二次分发到服务器 B 的现象。

Session 复制,任何一个服务器上的 Session 发生改变(增删改),该节点会把这个 Session 的所有内容序列化,然后广播给所有其它节点。

共享 Session,服务端无状态话,将用户的 Session 等信息使用缓存中间件来统一管理,保障分发到每一个服务器的响应结果都一致。
(完)

参考资料

话萌 pc 页开发总结

背景:由于作家反映在手机上创作,输入大段内容比较麻烦,需要一个 PC 页来创作。我们花了一周时间上线了一个简易版的 pc 页。团队规模变大之后,从 0 到 1 搭建一个项目的机会是很难得的,作为主要开发者之一,在这个项目中收获了很多。
项目地址

一、如何搭建一个新项目

1、前期准备

  • 技术栈
    开始开发之前,我们要确定使用哪种语言,原生 jsjQueryReactVue 等, 任何技术都有它的使用场景,挑一个合适的语言。
    该项目相关的技术栈是:React(全部使用了 hooks) + styled-component(css解决方案) + axios(网络请求) + yapi(mock接口)
  • 相关依赖
    我们要考虑到所有的依赖方,比如登录 sdk 要找谁 、运维上线要找谁、让产品提供一些协议要找谁等等。当然这个需要通过经验积累才能考虑完善。对于新手,如果没有把握可以让 leader 来把关。
  • 评估工时
    确定好外部依赖,我们在这个基础上评估项目的复杂度,确定开发的时间。比如这个 pc 页,拆分为框架搭建1天、登录相关联调2天、业务开发3天(界面及 UI 动画1天、内容输入逻辑1天、其它逻辑1天),联调提测1天。你估计的越详细,准确性就越高。

2、开发阶段

  • 文件组织
    我们需要对文件进行一个合理的组织。比如我们需要有 assetsimgcssutilslib 等等。
  • 数据接口
    数据:开发过程中肯定需要数据,后端没有完成之前,我们可以通过代理使用 mock 数据。由于 create-react-app 封装了 webpack,我们不想 eject 出来,于是使用了 http-proxy-middleware 实现代理。在 src 目录下新建 setupProxy.js:
const { createProxyMiddleware } = require('http-proxy-middleware');

module.exports = function (app) {
  app.use(createProxyMiddleware('/api', {
    target: your mock url,
    pathRewrite: {
      "^/api": "/api"
    },
    changeOrigin: true,     // target是域名的话,需要这个参数,
  }));
};
  • 业务逻辑
    这个和具体业务有关。这个项目是在左侧展示作者已创建书籍,通过设置的规则让作者进行内容的输入,检查格式,如果正确成功创建书籍新的一章。
    组件关系:
    image

界面效果:
image

二、一些知识点

js 相关

  • emoji 是占两个字符的,对它进行一些处理的时候需要注意: encodeURI('😂'.charAt(0)) // Uncaught URIError: URI malformed

  • 正则里面的\s 是匹配任何空白字符,包括空格、制表符、换页符等等,等价于 [\f\n\r\t\v]。用户输入的文字多个空格要替换成一个,但是不能将换行替换掉,所以需要用str.replace(/[ ]+/g, ' ')

  • 生产模式下移除console.log。上面说了,我们没有 eject,所以采用下面的方式:

function noop() {}
  if (process.env.NODE_ENV !== 'development') {
    console.log = noop;
    console.warn = noop;
    console.error = noop;
  }
  • 光标。在 reactsetState 后光标是不会 focus 在原来的输入框的,所以我们需要记住它的位置,然后根据自己的需要重新插到输入框中。
  setSelectionObj({start: e.target.selectionStart, end: e.target.selectionEnd})
  • 连续 ajax 只能应用最后一个数据
    点击左侧书籍会重新拉书籍列表(因为可能在客户端修改了书籍信息),所以会有不停切换请求的情况,由于我们没有办法控制请求到达的时间,可能会有左侧书籍和右侧相关信息不一致的情况。为了避免这个场景,我们需要只应用最后一次点击的请求。这是利用了闭包的原理。
  useEffect(() => {
    const currentCount = countRef.current
    Axios.$get(`url?time=${Date.now()}`)
        .then(res => {
            if (currentCount !== countRef.current) {
                return
             }
            // do something
          }
        })

    return () => {
      countRef.current += 1
    }
  }, [bookId])

css 相关

  • 对于动态增长的数字,不要给容器设置宽度,设置padding就行

  • transformtranslate 连用要小心。比如一个用了 translate(-50%, -50%) 居中的元素,让它旋转需要这样写:
const ImgRotate = keyframes`
  0% {
    transform: translate(-50%, -50%) rotate(0deg);
  }
  100% {
    transform: translate(-50%, -50%) rotate(360deg);
  }
`

const ImageBox = styled.img`
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%)
  width: 200px;
  height: 200px;
  animation: 2s ${ImgRotate} infinite linear;
`
  • 字体会有兼容性问题。例如下面,在 IE 下面每行后面都有一个问号,是因为 lato 字体不支持,就把这个字体删了。

  • width 100% padding 宽度溢出
    box-sizing 的默认值是 content-box,设置成 border-box

  • 类选择器间的空格
    .a .b 代表 class 包含 a 下面的 class 包含 b 的元素
    .a.b 代表 class 同时包含 ab 的元素,如<span class="a b"></span>

http 相关

我们在登录这块遇到了一些问题。

  • safari 不支持第三方 iframe setCookiewindow.close
  • chrome 80版本:samesite 是灰度的,涉及第三方需要判断浏览器版本并 setCookie: none

三、总结

其实收获的不止是一些特定的 api,还有很多我感觉更重要的东西。比如:

  • 组件的可扩展性:
    现在的 <BookItem /> 长这样:
    image

那如果产品后面需要一个竖型展示的 <BookItem /> 组件,那命名是叫 <BookItem2 /> 还是<BookItemColumn /> 呢。

  • 适当的冗余
    还是这张图,我们一开始可能只需要传递书名、章节数、字数、书封给子组件就行了:
    image
    <BookInfo key={item.bookId} name={item.bookName} chapterCount={item.chapterCount} avatar={item.avatar} onClick={itemClick} />
    但是后面万一产品需要加个作者,加个是否已完结,等等等等。。。我们可以一开始就直接把这个信息带过去:
    <BookInfo key={item.bookId} bookInfo={item} onClick={itemClick} />

  • 思考问题的方式
    比如找 bug 的时候,我们可以通过二分法查找;
    比如极限的**,一般出问题的都不是连续的中间,而是边界或者两种情况之间;
    比如反向思考,看到大想到小,看到明想到暗;
    比如类比思考,看到长想到宽,看到横想到竖;
    比如否定自己,我们很容易就找各种理由否定别人的想法,试着否定自己的想法;

在 react 使用 render props 复用状态和行为

发现自己没有仔细看过 react 官网的内容,需要好好过一遍

引言

我们知道组件是可以复用的,但是如何复用通用状态和行为呢?
例如,有一个组件,可以展示鼠标当前在屏幕的坐标:

import React, { useState } from "react";

const MouseTrack = () => {
  const [mouseObj, setMouseObj] = useState({ x: 0, y: 0 });
  const mouseMove = (event) => {
    setMouseObj({
      x: event.clientX,
      y: event.clientY
    });
  };

  return (
    <div style={{ height: "100vh" }} onMouseMove={mouseMove}>
      鼠标位置:({mouseObj.x}, {mouseObj.y})
    </div>
  );
};

export default MouseTrack;

image

图片: https://uploader.shimo.im/f/QGbLyvn4rVzPtPE4.png

问题

那么问题来了,我们有另一个组件,比如一个猫咪图片,它的位置可以跟着鼠标位置变化。希望复用上面组件的能力,怎么办呢?
有一种办法,我们看看代码:

// App.js
import React from "react";
import MouseTrack from "./mouseTrack";

export default function App() {
  return (
    <div className="App">
      <h2>移动鼠标</h2>
      <MouseTrack />
    </div>
  );
}

// Cat.js
import React from "react";

export const Cat = ({ x = 0, y = 0 }) => {
  return (
    <div>
      <img
        alt="cat" src="https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=1124621307,1373233430&fm=26&gp=0.jpg"
        style={{
          width: "100px",
          height: "100px",
          position: "absolute",
          left: x,
          top: y
        }}
      />
    </div>
  );
};

// MouseTrack.js,这个组件准确地说叫做 MouseTrackWithCat,不够纯洁
import React, { useState } from "react";
import { Cat } from "./Cat";

const MouseTrack = () => {
  const [mouseObj, setMouseObj] = useState({ x: 0, y: 0 });
  const mouseMove = (event) => {
    setMouseObj({
      x: event.clientX,
      y: event.clientY
    });
  };

  return (
    <div style={{ height: "100vh" }} onMouseMove={mouseMove}>
      鼠标位置:({mouseObj.x}, {mouseObj.y})
      <Cat x={mouseObj.x} y={mouseObj.y} />
    </div>
  );
};

export default MouseTrack;

让我们看一下效果:
image

图片: https://uploader.shimo.im/f/CL8Nsq2Hrr5e3j88.png
乍一看好像是复用了。但是,考虑一下扩展性,如果现在有另一个组件,同样需要复用这个能力,就叫小狗组件吧:

  • 第一种方式
    我们需要一个 MouseTrackWithDog 组件,把上面的 MouseTrack 改一下:
// App.js
import React from "react";
import MouseTrack from "./mouseTrack";

export default function App() {
  return (
    <div className="App">
      <h2>移动鼠标</h2>
      <MouseTrack />
      <MouseTrackWithDog />
    </div>
  );
}

// Cat.js
import React from "react";

export const Cat = ({ x = 0, y = 0 }) => {
  return (
    <div>
      <img
        alt="cat" src="https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=1124621307,1373233430&fm=26&gp=0.jpg"
        style={{
          width: "100px",
          height: "100px",
          position: "absolute",
          left: x,
          top: y
        }}
      />
    </div>
  );

// Dog.js
import React from "react";

export const Dog = ({ x = 0, y = 0 }) => {
  return (
    <div>
      <img
        alt="cat"      src="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fpic2.zhimg.com%2F50%2Fv2-de3e5710753e5760c9d1a3aee075164f_hd.jpg&refer=http%3A%2F%2Fpic2.zhimg.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1613464244&t=c32fe626cbd5a0182108215399a599f8"
        style={{
          width: "100px",
          height: "100px",
          position: "absolute",
          left: x + 100,
          top: y + 100
        }}
      />
    </div>
  );
};

// MouseTrack.js,这个组件准确地说叫做 MouseTrackWithCat,不够纯洁
import React, { useState } from "react";
import { Cat } from "./Cat";

const MouseTrack = () => {
  const [mouseObj, setMouseObj] = useState({ x: 0, y: 0 });
  const mouseMove = (event) => {
    setMouseObj({
      x: event.clientX,
      y: event.clientY
    });
  };

  return (
    <div style={{ height: "100vh" }} onMouseMove={mouseMove}>
      鼠标位置:({mouseObj.x}, {mouseObj.y})
      <Cat x={mouseObj.x} y={mouseObj.y} />
    </div>
  );
};

export default MouseTrack;

// MouseTrackWithDog.js
import React, { useState } from "react";
import { Dog } from "./Dog";

const MouseTrackWithDog = () => {
  const [mouseObj, setMouseObj] = useState({ x: 0, y: 0 });
  const mouseMove = (event) => {
    setMouseObj({
      x: event.clientX,
      y: event.clientY
    });
  };

  return (
    <div style={{ height: "100vh" }} onMouseMove={mouseMove}>
      鼠标位置:({mouseObj.x}, {mouseObj.y})
      <Dog x={mouseObj.x} y={mouseObj.y} />
    </div>
  );
};

export default MouseTrackWithDog;

我们看下效果:
image

图片: https://uploader.shimo.im/f/VSAl2nghXwghLzJj.png
注:这里其实有个问题,我们会有两个div : (

  • 第二种方式
// App.js
import React from "react";
import MouseTrack from "./mouseTrack";

export default function App() {
  return (
    <div className="App">
      <h2>移动鼠标</h2>
      <MouseTrack />
    </div>
  );
}

// Cat.js
import React from "react";

export const Cat = ({ x = 0, y = 0 }) => {
  return (
    <div>
      <img
        alt="cat" src="https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=1124621307,1373233430&fm=26&gp=0.jpg"
        style={{
          width: "100px",
          height: "100px",
          position: "absolute",
          left: x,
          top: y
        }}
      />
    </div>
  );

// Dog.js
import React from "react";

export const Dog = ({ x = 0, y = 0 }) => {
  return (
    <div>
      <img
        alt="cat"      src="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fpic2.zhimg.com%2F50%2Fv2-de3e5710753e5760c9d1a3aee075164f_hd.jpg&refer=http%3A%2F%2Fpic2.zhimg.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1613464244&t=c32fe626cbd5a0182108215399a599f8"
        style={{
          width: "100px",
          height: "100px",
          position: "absolute",
          left: x + 100,
          top: y + 100
        }}
      />
    </div>
  );
};

// MouseTrack.js,这个组件准确地说叫做 MouseTrackWithCat,不够纯洁
import React, { useState } from "react";
import { Cat } from "./Cat";
import { Dog } from "./Dog";

const MouseTrack = () => {
  const [mouseObj, setMouseObj] = useState({ x: 0, y: 0 });
  const mouseMove = (event) => {
    setMouseObj({
      x: event.clientX,
      y: event.clientY
    });
  };

  return (
    <div style={{ height: "100vh" }} onMouseMove={mouseMove}>
      鼠标位置:({mouseObj.x}, {mouseObj.y})
      <Cat x={mouseObj.x} y={mouseObj.y} />
      <Dog x={mouseObj.x} y={mouseObj.y} />
    </div>
  );
};

export default MouseTrack;

看下效果:
image

图片: https://uploader.shimo.im/f/WKFD5bfjTMC2o5JT.png
这下的确可以了,但是有个问题,为了复用这个状态,我们把组件都写在了这个状态里面,是不是感觉很奇怪。我要用一个东西,还得给别人当儿子?
解决方案
这里,就进入主题了。让我们看看如何利用 render props 来重写这个功能:

// app.js 
import React from "react";
import MouseTrack from "./mouseTrack";

export default function App() {
  return (
    <div className="App">
      <h2>移动鼠标</h2>
      <MouseTrack />
    </div>
  );
}

// mouseTrack.js
import React from "react";
import { Cat } from "./Cat";
import { Dog } from "./Dog";
import Mouse from "./mouse";

// mouseTrack.js
const MouseTrack = () => {
  return (
    <div>
      <Mouse
        render={(mouse) => (
          <div>
            <Dog x={mouse.x} y={mouse.y} />
            <Cat x={mouse.x} y={mouse.y} />
          </div>
        )}
      />
    </div>
  );
};

export default MouseTrack;

// mouse.js
import React, { useState } from "react";

const Mouse = (props) => {
  const [mouseObj, setMouseObj] = useState({ x: 0, y: 0 });
  const mouseMove = (event) => {
    setMouseObj({
      x: event.clientX,
      y: event.clientY
    });
  };

  return (
    <div style={{ height: "100vh" }} onMouseMove={mouseMove}>
      {/* 咱老实做人,只做本分的事 */}
      {props.render(mouseObj)}
    </div>
  );
};

export default Mouse;

看看效果:
image

图片: https://uploader.shimo.im/f/WVvrjbyz4jbsEUQf.png
完美,就解决了状态复用的问题。
这里的关键在于,我们给 组件传递了一个函数来动态渲染组件,而不是特定数据!怎么渲染时爸爸决定的,儿子只需要执行就好。

优化

这里我们注意到,渲染一个函数,是不是会造成重复渲染?

// mouseTrack.js
import React, { useEffect, useCallback } from "react";
import { Cat } from "./Cat";
import { Dog } from "./Dog";
import Mouse from "./mouse";

const MouseTrack = () => {
  useEffect(() => {
    console.log("render mouseTrack");
  });

  const renderMouse = useCallback(
    (mouse) => (
      <div>
        鼠标位置:({mouse.x}, {mouse.y})
        <Dog x={mouse.x} y={mouse.y} />
        <Cat x={mouse.x} y={mouse.y} />
      </div>
    ),
    []
  );
  return (
    <div>
      <Mouse children={renderMouse} />
    </div>
  );
};

export default MouseTrack;

// mouse.js
import React, { useState, useEffect, memo } from "react";

const Mouse = memo((props) => {
  useEffect(() => {
    console.log("render mouse", props);
  });

  const [mouseObj, setMouseObj] = useState({ x: 0, y: 0 });
  const mouseMove = (event) => {
    setMouseObj({
      x: event.clientX,
      y: event.clientY
    });
  };

  return (
    <div style={{ height: "100vh" }} onMouseMove={mouseMove}>
      {props.children(mouseObj)}
    </div>
  );
});

export default Mouse;

(完)

利用 Nginx 反向代理解决跨域问题

上篇 JSONP 的文章里提到过利用 Nginx 也可以解决跨域问题。趁着自己以前没有接触过 Nginx,熟悉了一下,顺带写了一个非常非常简单的 demo 实验下。

正向代理和反向代理

提到代理,肯定要说一下这两个的区别。
举个正向代理的例子:我打球累了走不动了,找看球的小朋友帮我去旁边的商店买瓶水。商店老板是不知道到底是谁需要喝水的,隐藏了客户端。当然,小朋友可以告诉老板就是那个打球像蔡徐坤的人要喝水。还有,VPN 就是正向代理。
反向代理的例子:我打球累了,找看球的小朋友要瓶水喝(当然我肯定会给钱的:D)。我不需要知道小朋友的水是从旁边的商店还是两公里外的超市买的。隐藏了服务端。还有,我们连好了 VPN 访问谷歌的时候,浏览的那些页面,我们是不会知道具体是哪台服务器的资源。

具体步骤

服务接口
既然请求,肯定需要先写一个服务接口,我们用 node 起一个服务:

  // index.js
const http = require('http');
const fs = require('fs');
const url = require('url');

const server = http.createServer(function (req, res) {
  if (req.url === '/favicon.ico') {
    return;
  }

  const parseUrl = url.parse(req.url, true);
  console.log('parseUrl', parseUrl.pathname)

  if (parseUrl.pathname === '/api/getList') {

    const list = {'a': 1, 'b': 2}
    res.writeHead(200, {'content-Type':'text/html;charset=UTF-8'})  
    res.end(JSON.stringify(list))
  }else {
    res.write(`
    port: 666
  `)
    res.end()
  }
});

server.listen(666, function () {
  console.log('server is starting on port 666');
});

我们来访问一下,可以拿到数据了。
image

测试页面
然后,我们写一个简单的 ajax 请求页面。你可以本地用http-server启动访问下,可以发现请求跨域了:

<html>
<head>
  <title></title>
</head>
<body>
  <button onclick="sendAjax()">sendAjax</button>
<script type="text/javascript">
  var sendAjax = () => {
      var xhr = new XMLHttpRequest();
      xhr.open('GET', 'http://localhost:666/api/getList', true);
      xhr.send();
      xhr.onreadystatechange = function (e) {
        if (xhr.readyState == 4 && xhr.status == 200) {
          console.log(xhr.responseText);
        }
      };
  }
</script>
</body>
</html>

image

安装 Nginx
这个时候,你可以通过设置响应头来允许跨域。或者用 Nginx 来解决这个问题了。首先肯定需要安装 Nginx。这个按照对应的平台安装就行了。

brew update
brew install nginx
nginx
nginx -s reload // 重启

配置
然后我们配置一下代理,这个意思就是我们请求中有 api 这样的就会代理到 http://127.0.0.1:666,所以我们只要访问 http://localhost:9999/api/getList 这个不跨域的接口,然后就会由服务器反向代理到 http://localhost:666/api/getList。

        listen       9999;
        server_name  localhost;
        #charset koi8-r;
        #access_log  logs/host.access.log  main;
        location / {
            root   html;
            index  index.html index.htm;
        }

        location /api/ {
            proxy_pass http://127.0.0.1:666;
        }

配置好之后我们需要重启一下 Nginx 服务。注意一点,重启时可能会报这么一个错误:

nginx: [error] open() "/usr/local/var/run/nginx.pid" failed (2: No such file or directory)

这是 sudo nginx -s stop 这个命令会删除 pid 文件,可以执行 sudo nginx 重新添加这个文件。
测试结果
这个时候,我们不用绝对地址了,我们把ajax请求里面的接口换成相对地址:

// xhr.open('GET', 'http://localhost:666/api/getList', true);
xhr.open('GET', '/api/getList', true);

image
美滋滋,这就不跨域了呢。
当然,还可以更加真实一点,我们随便用一个域名测试一下。Nginx 重新配置下:

        listen       80;
        server_name  yumingtest;
        #charset koi8-r;
        #access_log  logs/host.access.log  main;
        location / {
            root   html;
            index  index.html index.htm;
        }
        location /api/ {
            proxy_pass http://127.0.0.1:666;
        }

然后在 hosts 文件里面添加这条:127.0.0.1 yumingtest.com,重启下 Nginx。
image
这下是不是更加真实了。关于hosts 文件的作用,就是我们输入域名不是要需要经过DNS解析IP嘛,这个里面就存了一些。首先自动从 Hosts 文件中寻找对应的 IP 地址,一旦找到,系统会立即打开对应网页,如果没有找到, 则系统再会将网址提交 DNS 域名解析服务器进行 IP 地址的解析。
还要一个问题,关于 HTTP 502 状态码,我是把接口服务停了,于是就报 502了。
image
也不一定是网上说的什么”连接超时 我们向服务器发送请求 由于服务器当前链接太多,导致服务器方面无法给于正常的响应,产生此类报错“那样。
(完)

开发过程中遇到的问题

记录下开发中遇到的问题(包括 error和 warning)

1、Expected to return a value in arrow function:箭头函数需要返回值

发生问题的原因:
是遍历数组的时候用了 map,其实是直接操作了数组,但是没有 return 每个元素。
解决方法:
改成 forEach,或者 return 每个元素

let arr = [
  {label: 'a', value: 1},
  {label: 'b', value: 2}, 
  {label: 'c', value: 3}
]
arr.forEach(item => item.value *= 2)

2、重置查询的时候,初始化时间设置为 undefined 后,new Date(undefined) 就会报错:

new Date(0) // Thu Jan 01 1970 08:00:00 GMT+0800 (**标准时间)
new Date(null) // Thu Jan 01 1970 08:00:00 GMT+0800 (**标准时间)
new Date(undefined) //  Invalid Date

3、arr[Symbol.iterator] is not a function

原因:
异步请求忘了加 await,直接赋值给一个数组了。请求返回的是一个 Promise,不可迭代:

// 错误代码
const [res, err] = $fetch(`/recSubject/subset_contents?subset_id=${sublibId}&page_size=10&page_num=1`, {
  method: 'GET'
})
if (err !== null) return
setBookInfoLoading(false)
setBookInfo(res.data)

4、can't read property 'join' of null

原因:

  • 参数默认值在数据为 null 时是无效的
  • 接口参数的值为 null
    image
    image
    image

5、调试时要小心,promise会把错误吃掉,不会暴露出来

6、Unexpected token < in JSON at position 0

当用 fetch 请求时:

    fetch(url, { method: 'POST', body: JSON.stringify(data)})
      .then(response => {
        return response.json() // 这里会报错 Unexpected token < in JSON at position 0 
      })
      .then(res => {})
      .catch(err => {})

因为可能请求错误时返回的不是一个 json 格式,要对错误兼容一下:

    fetch(url, { method: 'POST', body: JSON.stringify(data)})
      .then(response => {
        if (response.ok) {
          return response.json()
        } 
        return Promise.reject(response)
      })
      .then(res => {})
      .catch(err => {})

开发的流程

今天后端突然把接口很多字段变了,我修改了半天,很僵。本来想加一层对字段进行 map,不影响我已经使用的,但是发送的时候还要转一遍,而且如果结构变了,还是要改很多,就直接改了。真是一个惨痛的教训。

不能做老好人

我以为只有几个字段,原来是全部的,本来应该是后端和我对接完,再和服务对接,抹平接口差异。后端好像没有时间,只做了一层转发,导致这个结果。既然答应了,就得硬着头皮做完。所以,不能随口答应别人,得考虑成本。早知道这个结果,我肯定不能同意。

开发的流程

我觉得正常的开发流程应该这样:

  • 1、产品提出需求
  • 2、拉对应的接口人进行评审
  • 3、收集意见,整理需求,贴出tapd,确认相关人员
  • 4、前后端各自进行排期
  • 5、后端给出接口文档
    这时候不应该再修改已经存在的字段,除非业务用不到了。
    接口文档的一些注意点:
    枚举类型:要把所有枚举列举出来;
  • 6、前端查看,提出意见
  • 7、前后端分别开发,对接口进行补充或者修改
  • 8、联调
  • 9、提测,周知相关人员(待补充)
  • 10、测试(若有)或者自测
  • 11、bugfix
  • 12、上线

正则里面的 lastIndex

同事遇到了这样一个场景,用正则进行循环判断,简化的代码大概是下面这样:

var reg = /ab/g

console.log(reg.test('ab')) // true
console.log(reg.test('ab')) // false

这里是不是有人会觉得奇怪,为什么第二个是 false 呢。

test()

其实如果我们仔细看过MDN上关于test的描述,应该就知道了:

关于这个lastIndex,很多小伙伴可能没有听过也没有用过,让我们看看下面的代码:

var reg = /ab/g
console.log(Object.getOwnPropertyDescriptors(reg))
//{lastIndex: {value: 0, writable: true, enumerable: false, configurable: false}}

可以看到,reg 含有一个不可枚举的 lastIndex 属性。当然,如果你直接用 Object.keys(reg) 是看不到的:

var reg = /ab/g
console.log(Object.keys(reg)) // []

(知识点:getOwnPropertyDescriptors() 和 keys() 的区别是什么呢。keys() 方法会返回一个由一个给定对象的自身可枚举属性组成的数组。)
这个就是造成上面第二个是false的原因。因为每执行一次,其实我们的lastIndex都会变化:

var reg = /[ab]/g

console.log('lastIndex', reg.lastIndex) // lastIndex 0

reg.test('ab')
console.log('lastIndex', reg.lastIndex) // lastIndex 1

reg.test('ab')
console.log('lastIndex', reg.lastIndex) // lastIndex 2

reg.test('ab')
console.log('lastIndex', reg.lastIndex) // lastIndex 0

根据上面的代码可以看到,这个lastIndex是会循环的。

所以第二次就出了问题。如果我们想让一开始的代码正常工作,该怎么办呢?

var reg = /ab/g

console.log(reg.test('ab')) // true
reg.lastIndex = 0
console.log(reg.test('ab')) // true

我们需要在每次执行后手动设置lastIndex。

test() 应用场景

我们举一个应用场景:后端传回来了一个图片 url 的数组,有 jpg、png 各种格式的,我们需要拿到以 jpg 结尾的图片:
首先,我们看看下面的代码:

const reg = /\.jpg/g;
const arr = [
    'test0.png',
    'test1.jpg',
    'test2.jpg',
    'test3.jpg',
    'test4.jpg',
    'test5.jpg',
];
const res = arr.filter(item => reg.test(item));

(关于判断图片格式的这个正则大家不要纠结,其实是不完善的,比如url的名字是 ajpg.jpg 这种也可以匹配到,这个不重要,不是本文重点)
乍一看,觉得没毛病。我们看看结果:

WTF???
你肯定反应过来了,因为lastIndex变了,所以和预期不一致了,我们输出一下看看:

可以看到,true和false是依次输出的,所以我们没有拿到全部的jpg格式的url。
还有,就是如果当前匹配失败了,lastIndex也会重置为0,这一点也很重要,不然后面的就匹配不到了。

所以解决方法之一就是手动重置lastIndex:

其实,最好的方法就是直接去掉g,因为这种场景其实是不需要加上g的。这样lastIndex就一直是0,不会变了。结果也是符合预期的:

exec

此外,lastIndex 还和另一个正则方法 exec() 也是好兄弟,关系很近。用法和test()原理一样。
很多人分不清 execmatch 的关系,或者可能没有用过 exec
matchexec的关系:
首先,两个方法的来源是不一样的:

String.prototype.match()
RegExp.prototype.exec()

然后,不加g的时候是一样的:

然后,加了g的话:
match就没有捕获组了:

这个时候我们就需要用exec()方法了。

捕获组

关于捕获组,这里简单介绍一下。
捕获组分为四类:
捕获 ()
不捕获 (?:)
正向捕获 (?=)
反向捕获 (?!)

捕获组的功能很强大,这里就举两个例子:
例1:对日期进行任意格式的转换

var reg = /(\d{2})\/(\d{2})\/(\d{4})/
var data = '10/24/2017'
data = data.replace(reg, '$3/$1.$2')
console.log(data) //2017/10.24

例2: 千分位数字

'12345678'.replace(/(?=(\d{3})+$)/g, ',') // 12,345,678

因此,exec结合捕获组就可以实现很多复杂的逻辑。

exec() 应用场景

举个例子,我们要把一个字符串 a1b2c3d4e5里面的偶数的数字变成0。(方法肯定很多,这里只考虑用正则的方式)
肯定有同学说直接用 replace 就行了:

 'a1b2c3d4e5'.replace(/[2468]/g,0) // "a1b0c3d0e5"

(因为这里的转换条件比较简单,如果是很复杂的转换,比如我要把偶数的数字先转成ASCII码,然后反转呢?主要就是意思一下哈。)

我个人觉得exec比match强大的地方在于它可以深入到内部,暂停执行,类似 generate 函数,而不像match一样是个黑盒。

exec() 和 lastIndex

关于这个,其实业务中感觉用的不多,就举个例子吧:

var regex = /[a-z]_[a-z]/ig;
var str = 'a_b_c';
var match;

while ((match = regex.exec(str)) !== null) {
    console.log('match', match)
    regex.lastIndex--
}
// a_b
// b_c

代码大家肯定看得懂,我就不说了。match 是无法实现这样的功能的,因为匹配完就丢弃了。
使用exec() 要注意的点就是不要写出死循环(毕竟有while)

redux、react-redux、观察者模式

redux

redux 的使用:redux

初始化数据

// 创建仓库
const store = createStore(reducer)
// reduce: 初始化数据的参数、根据不同的action处理返回数据
const reducer = function(state={value: 0}, action) {
  switch (action.type) {
    case 'plus':
      state.value++;
      break;
    case 'minus':
      state.value--;
      break;
    default:
      break;
  }
  return {...state}
}

获取数据

let store = store.getState()

修改数据(by action)

store.dispatch({type: 'plus', content:{id: 1}})

修改视图(监听数据变化,rerender)

// 监听的是一个函数
store.subscribe(() => (ReactDOM.render(<App />, document.getElementById('root'))))

react-redux

react-redux 的使用:react-redux

  • Provider组件: 自动将 store 里的state 关联到组件
  • mapStateToProps: 将store的state映射到组件的props里
  • mapDispatchToProps: 将 store 中的 dispatch 映射到组件的props里
  • Connect:将组件和数据方法进行连接

初始化数据

// 创建仓库
const store = createStore(reducer)
// reduce: 初始化数据的参数、根据不同的action处理返回数据
const reducer = function(state={value: 0}, action) {
  switch (action.type) {
    case 'plus':
      state.value++;
      break;
    case 'minus':
      state.value--;
      break;
    default:
      break;
  }
  return {...state}
}

获取数据,修改数据

将state、修改数据的方法映射到组件props里

// 将state映射到props
function mapStateToProps(state) {
	return {
		value: state.value
	}
}

// 将修改 state 的方法映射到 props
function mapDispatchToProps(dispatch) {
	return {
		plus: () => {dispatch(plusAction)},
		minus: () => {dispatch(minusAction)}
	}
}

// 关联(顺序不能变)
const App = connect(mapStateToProps, mapDispatchToProps)(Counter)

观察者模式

  • 首先我们要知道 reducer就是根据不同的 action 处理 state
  • subscribe 的函数都要执行
function reducer(state = {value: 0}, action) {
	if (action.type === 'plus') {
		state.value++
	} else if (action.type === 'minus') {
		state.value--
	}
	return {...state}
}

function createStore(reducer) {
    let state
    let listeners = []
    function getState () {
	return reducer(state, {type: "@@redux/INITq.f.4.i.v"})
    }
    function dispatch(action) {
        state = reducer(state, action)
        listeners.forEach(listen => {
            listen()  
        })
    }
    function subscribe(fn) {
        listeners.push(fn)
    }
    return { getState, dispatch, subscribe }
}

const store = createStore(reducer)
store.getState() // {value: 0}
store.subscribe(() => console.log(store.getState()))
store.subscribe(() => console.log('1'))
store.subscribe(() => console.log('2'))
store.dispatch({type: 'plus'}) // {value: 1}, 1, 2

二分查找

如果从1到100,我随便写一个数,让你猜,你咋样才能快速猜出来。

function search(arr, target) {
	let start = 0;
	let end = arr.length - 1;
	while (start <= end) { // = 号不能丢,你想要是只有一个元素,刚好 target 就是
		let mid = Math.floor((start + end) / 2);
		if (arr[mid] < target) {
			start = mid + 1;
		} else if (arr[mid] > target) {
			end = mid - 1;
		} else {
			return mid;
		}
	}
	return -1;
}

封装axios

post传给后端可以用payload、formdata、query
【注意】简单请求的Content-Type:只限于三个值 application/x-www-form-urlencodedmultipart/form-datatext/plain。如果是 application/json 则会多发一个option请求。

import axios from 'axios'
import qs from 'qs'
 
const CSRF_KEY = '_csrfToken'

const instance = axios.create()

export function getCsrfToken () {
  const cookie = document.cookie
  const csrfToken = parseCookie(cookie || '')[CSRF_KEY]
  return {
    [CSRF_KEY]: csrfToken
  }
}

function parseCookie (cookie) {
  const cookieObj = {}
  const cArr = cookie.split(';')
  for (const str of cArr) {
    const iArr = str.trim().split('=')
    cookieObj[iArr[0]] = iArr[1]
  }
  return cookieObj
}

instance.interceptors.request.use(config => {
  const csrfToken = getCsrfToken()
  const devParams =
    process.env.NODE_ENV === 'development' ? { nocsrf: 1, nologin: 1 } : {}
  config.params = Object.assign(devParams, csrfToken, config.params || {})
  return config
})

instance.interceptors.response.use(
  (response) => {
    return response
  },
  (error) => {
    return Promise.reject(error.response)
  }
)

instance.$get = function (url, data, config = {}) {
  !config.params && (config.params = {})
  data && (Object.assign(config.params, data))
  return this.get.call(this, url, config).then((response) => {
    if (response) {
      return response
    }
    throw new Error('网络不畅,请稍候再试')
   }, (err) => {
    return Promise.reject(err.data)
}

instance.$postForm = function (url, data = {}, config = {}) {
  !config.params && (config.params = {})
  !config.headers && (config.hearders = {
    'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
  })

  const csrfToken = getCsrfToken()
  return this.post.call(this, url, qs.stringify(Object.assign(csrfToken, data)), config).then((response) => { 
    if (response) {
      return response
    }
    throw new Error('网络不畅,请稍候再试')
   }, (err) => {
    return Promise.reject(err.data)
}

instance.$postFormData = function (url, data = {}, config = {}) {
  !config.params && (config.params = {})
  !config.headers && (config.hearders = {
    'Content-Type': 'multipart/form-data; charset=UTF-8'
  })

  const csrfToken = getCsrfToken()
  const formData = new FormData()
  csrfToken[CSRF_KEY] && formData.set(CSRF_KEY, csrfToken[CSRF_KEY])
  for (const key of Object.keys(data)) {
    formData.set(key, data[key])
  }

  return this.post.call(this, url, formData, config).then((response) => {
    if (response) {
      return response
    }
    throw new Error('网络不畅,请稍候再试')
  }, () => {
    throw new Error('网络不畅,请稍候再试')
  })
}

instance.$post = function (url, data = {}, config = {}) {
  !config.params && (config.params = {})

  const csrfToken = getCsrfToken()
  return this.post.call(this, url, Object.assign(csrfToken, data), config).then((response) => {
    if (response) {
      return response
    }
    throw new Error('网络不畅,请稍候再试')
  }, () => {
    throw new Error('网络不畅,请稍候再试')
  })
}

instance.$postQuery = function (url, data = {}, config = {}) {
  !config.params && (config.params = data)

  const csrfToken = getCsrfToken()
  return this.post.call(this, url, qs.stringify(csrfToken), config).then((response) => {
    if (response) {
      return response
    }
    throw new Error('网络不畅,请稍候再试')
  }, () => {
    throw new Error('网络不畅,请稍候再试')
  })
}

export default instance

文件大小遍历

哥们今天的面试题,对文件夹和文件遍历以大小排序
文件遍历得用 node 做,简单起见,直接拿结果操作:

遇到的问题:

  • 如果以T或者B作为单位,可能出现科学计数法,不好比较大小了。所以采用中间单位KB来统一转换
  • 字符串的大小比较是利用ASCII码的,所以会出现 '230' < '4' 的情况,最好直接相减,会隐式转成成Number类型

1、利用 Array.prototype.sort

var obj = {a: '2M', b: '3G', c: '100KB', d: '3T', e: '0', f: '1B', g: '100T', h: '1023M'}

function sortBySize (obj) {
    let temp = []
    for (key in obj) {
        switch (true) {
            case /T/i.test(obj[key]):
                temp.push([key, parseInt(obj[key]) * 1024 * 1024 * 1023 + 'KB'])
                break
            case /G/i.test(obj[key]):
                temp.push([key, parseInt(obj[key]) * 1024 * 1023 + 'KB'])
                break
            case /M/i.test(obj[key]):
                temp.push([key, parseInt(obj[key]) * 1024 + 'KB'])
                break
            case /KB/i.test(obj[key]):
                temp.push([key, parseInt(obj[key]) / 1 + 'KB'])
                break
            default:
                 temp.push([key, parseInt(obj[key]) / 1024 + 'KB'])
        }
    }
     temp.sort((a, b) => {
        return a[1].slice(0, a[1].length - 2) - b[1].slice(0, b[1].length - 2)
    })

    temp.forEach(item => console.log(item[0], obj[item[0]]))
}

sortBySize(obj)
// e 0
// f 1B
// c 100KB
// a 2M
// h 1023M
// b 3G
// d 3T
// g 100T

2、不用 sort

var obj = {a: '2M', b: '3G', c: '100KB', d: '3T', e: '0', f: '1B', g: '100T', h: '1023M'}

function sortBySize (obj) {
    let temp = []
    for (key in obj) {
        switch (true) {
            case /T/i.test(obj[key]):
                temp.push([key, parseInt(obj[key]) * 1024 * 1024 * 1023 + 'KB'])
                break
            case /G/i.test(obj[key]):
                temp.push([key, parseInt(obj[key]) * 1024 * 1023 + 'KB'])
                break
            case /M/i.test(obj[key]):
                temp.push([key, parseInt(obj[key]) * 1024 + 'KB'])
                break
            case /KB/i.test(obj[key]):
                temp.push([key, parseInt(obj[key]) / 1 + 'KB'])
                break
            default:
                 temp.push([key, parseInt(obj[key]) / 1024 + 'KB'])
        }
    }
    // 此处应该用 quicksort
    for (let i = 0; i < temp.length; i++) {
        for (let j = i + 1; j < temp.length; j++) {
            if (!/\./.test(temp[i][1]) && !/\./.test(temp[j][1])) {
                  if (parseInt(temp[i][1].slice(0, temp[i][1].length - 2)) > parseInt(temp[j][1].slice(0, temp[j][1].length - 2))) {
                        [temp[i], temp[j]] = [temp[j], temp[i]]
                    }
            } else {
                if (temp[i][1].slice(0, temp[i][1].length - 2) > temp[j][1].slice(0, temp[j][1].length - 2)) {
                        [temp[i], temp[j]] = [temp[j], temp[i]]
                    }
            }
          
        }
    }

    temp.forEach(item => console.log(item[0], obj[item[0]]))
}

sortBySize(obj)
// e 0
// f 1B
// c 100KB
// a 2M
// h 1023M
// b 3G
// d 3T
// g 100T

手写 JSONP

 最近要在组内做一个xss 的分享,遇到跨域的时候就想到了 JSONP,就想着很久以前都是用 jQuery 直接调用,这次有时间自己实现了一下。

首先,看到 JSONP 这个词,很多人就会联想到它和 JSON 肯定有千丝万缕的关系。我不太清楚这个历史,不过个人感觉关系也不是很大(雷锋和雷锋塔?)。

言归正传,开始实现。关于 JSONP 的原理网上一大堆,我就不啰嗦了。主要是记录一下个人实现时的心路历程。

肯定有一个 .html 文件,用来接收服务端的数据,代码很简单:

<!-- jsonDemo.html -->
<html>
<head>
  <title>jsonp_demo</title>
</head>
<body>
  <script>
    function handleJsonp (data) {
      console.log('data', data)
    }
  </script>
  <script src="http://localhost:3002?callback=handleJsonp"></script>
</body>
</html>

然后,肯定还有一个服务器。我用 node 跑的,代码同样也很简单:

// index.js
const http = require('http');
const fs = require('fs');
const url = require('url');

const server = http.createServer(function (req, res) {
  if (req.url === '/favicon.ico') {
    return;
  }
  const parseUrl = url.parse(req.url, true);
  console.log('parseUrl', parseUrl)
  res.writeHead(200, {'content-Type':'text/plain;charset=UTF-8'})
  const data = "age: 27"

  const str = parseUrl.query.callback + '("' + data + '")'
  res.end(str)
});

server.listen(3002, function () {
  console.log('server is starting on port 3002');
});

再然后,我们跑一下 node index.js ,刷新一下浏览器,可以看到控制台的确把后端数据吐给前端了。
image

这里遇到的问题是 http.createServer 执行了两次,node 我不是很熟,查了一下,别人是这么解释的:

因为浏览器默认一次会请求 favicon.ico,添加下面代码就行:

if (req.url === '/favicon.ico') {
    return;
}

试了一下,可以的。对了,还有一点,就是返回数据的时候,可以这样:

  const data = "{age: 27}"
  const str = parseUrl.query.callback + '(' + JSON.stringify(data) + ')'

那么前端接收的就是一个 JSON 对象。(所以这就是 JSONP 的由来?那接受字符串也可以叫 StringP

以前我没有理解为什么后端这么返回给前端然后就能拿到数据了,后来理解了:

因为浏览器加载完脚本是自动执行的,比如我们加载一个脚本,里面只有一行alert(1),那浏览器就会自动弹出1。所以我们请求的结果其实是一个函数 callback(data),然后会自动执行,刚好上面定义了这个函数,于是就输出了结果。

补充

JSONP 的缺点

只能支持 GET 请求,说加载脚本却拿数据,欺骗浏览器的感情。

Access-Control-Allow-Origin 设置多域名

因为现在的浏览器都支持 CORS(跨域资源共享),不一定需要 JSONP 这种方式。实验了一下这个东西,然后发现响应头 Access-Control-Allow-Origin 只支持单域名。你要是不想设置 * 号的话咋办呢?这样写:

 if (req.headers.origin === 'http://localhost:2999' || req.headers.origin === 'http://localhost:3000') {
      res.writeHead(200, {'content-Type':'text/html;charset=UTF-8', 'Access-Control-Allow-Origin': '*'})  
    }

把你需要添加的白名单都判断一下。
(完)

[译]通过例子介绍一下 react 里的 useCallback 和 useMemo

原文地址:React’s useCallback and useMemo Hooks By Example

介绍

最近我正在学习 React hooks 的 API,已经被它的表现惊呆了。Hooks 让我可以通过极少的行数来重写数十行的样板代码。不幸的是,这种便利性需要付出一些代价,我发现一些更高级的 hooks 例如 useCallbackuseMemo 难以学习,在一开始有点反直觉。
在本文中,我将通过一些简单的例子来说明为什么我们需要这些 hooks,在什么时候需要使用它们以及怎么使用。这不是一篇关于 hooks 的介绍,你需要熟悉 useState 以了解下面的内容,

问题

在我们开始之前,让我们引入一个帮助按钮组件。我们将使用 React.memo 把它放入一个记忆组件中。这将强制让 React 不再重新渲染它,除非它的某些属性值改变了。我们还需要添加一个随机颜色作为背景以便我们能够在组件重新渲染时跟踪它。

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

// 当被调用时随机生成颜色
const randomColour = () => '#'+(Math.random()*0xFFFFFF<<0).toString(16);

// props 的类型
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement>;

// 一个有着随机背景色的记忆按钮

const Button = React.memo((props: ButtonProps) => 
  <button onClick={props.onClick} style={{background: randomColour()}}> 
    {props.children}
  </button>
)

现在让我们看一看下面的简单的 app 例子,它展示了两个数字 - cdelta。一个按钮允许用户对 delta 加一。另一个按钮允许用户通过添加 delta 以增加 Counter。我们将创建 incrementincrementDelta 两个方法,并把它们绑定到按钮的点击事件。让我们看看当用户点击按钮时,这样的函数会被创建多少次:

import React, { useState } from 'react';

// 跟踪 app 存在时所有创建的函数
const functions: Set<any> = new Set();

const App = () => {
  const [delta, setDelta] = useState(1);
  const [c, setC] = useState(0);

  const incrementDelta = () => setDelta(delta => delta + 1);
  const increment = () => setC(c => c + delta);

  // 注册函数以便计算它们
  functions.add(incrementDelta);
  functions.add(increment);

  return (<div>
    <div> Delta is {delta} </div>
    <div> Counter is {c} </div>
    <br/>
    <div>
      <Button onClick={incrementDelta}>Increment Delta</Button>
      <Button onClick={increment}>Increment Counter</Button>
    </div>
    <br/>
    <div> Newly Created Functions: {functions.size - 2} </div>
  </div>)
}

当我们运行 app 并且开始点击按钮,我们观察到一些有趣的事情。每次点击按钮都会创建两个新的函数!并且,每次改变两个按钮都会重新渲染!
without-use-callback
组件的每次重新渲染,都会创建两个新的函数。每次改变都会重新渲染两个按钮
换句话说,在每次重新渲染我们都创建了两个新的函数。如果我们增加 c,我们为什么需要重新创建 incrementDelta 函数呢?这不仅是记忆 - 它导致了子组件不必要地重新渲染。这将迅速成为一个性能问题。

一个可能的解决方案是将这两个函数移到功能组件 App 的外面。不幸的是,这样并不会奏效,因为它们使用了 App 作用域外的状态变量。

天真的解决方案 - 为什么依赖很关键

这是为什么需要引入 useCallback 的原因。它以一个函数作为参数,并返回一个缓存的\记忆的版本。它还需要第二个参数,稍后再做介绍。让我们用 useCallBack 重写:

const App = () => {
  const [delta, setDelta] = useState(1);
  const [c, setC] = useState(0);

  // No dependencies (i.e. []) for now
  const incrementDelta = useCallback(() => setDelta(delta => delta + 1), []);
  const increment = useCallback(() => setC(c => c + delta), []);

  // Register the functions so we can count them
  functions.add(incrementDelta);
  functions.add(increment);

  return (<div>
    <div> Delta is {delta} </div>
    <div> Counter is {c} </div>
    <br/>
    <div>
      <Button onClick={incrementDelta}>Increment Delta</Button>
      <Button onClick={increment}>Increment Counter</Button>
    </div>
    <br/>
    <div> Newly Created Functions: {functions.size - 2} </div>
  </div>)
}

这将阻止新函数的实例化以及不必要的重新渲染。然而,当我们重新运行 app,我们注意到我们以及引入了一个 bug。如果我们把 detla 增加到2,然后试着增加计数器,它的值增加了1而不是2:
without-dependencies

无论 delta 的状态有没有改变,不会有新的函数被创建。在初始化的渲染中, useCallback 创建了一个单独、缓存的 “increment” 版本,封装了 detla 的状态值,在后面的每次重新渲染时都重复使用。

这是因为在 increment 函数的初始化时,delta 的值是1,该值会被函数的作用域捕获。由于我们缓存了 increment 实例,它不会重新创建并会使用初始的作用域的值 detla = 1。.

useCallback 创建了一个单独、缓存的 increment 版本,封装了 delta 的初始值。当 App使用不同的 detla 值重新渲染时, useCallback 返回一个先前的 increment 函数的版本,该函数保留第一次渲染时的delta旧值。

我们需要告诉 useCallback 在每次 delta 改变时创建新的、缓存的 increment 版本。

依赖

这是 useCallback 第二个参数出现的地方。它是一系列值的数组,代表了缓存的依赖。如果依赖项的值相等,则在随后的任何两个重渲染中,useCallback将返回相同的缓存函数实例。

我们可以使用依赖以解决前面的 bug

const incrementDelta = useCallback(() => setDelta(delta => delta + 1), []);

  // Recreate increment on every change of delta!
  const increment = useCallback(() => setC(c => c + delta), [delta]);

现在我们可以看到,只有 delta 变化时,新的 increment 函数才会创建。所以,counter 按钮只会在 delta 改变时才会重新渲染,因为新的 onClick 属性实例被添加。换句话说,我们只会创建一个新的回调,如果它使用的闭包部分(即依赖)自上一次渲染时改变了。
with-dependencies

每次 delta 的改变都会创建一个新的 increment 函数。仅重新创建依赖改变的函数
useCallback 的一个很有用的特性是,如果依赖没有改变,它会返回了相同的函数实例。因此我们可以在其它的 hooks 的依赖列表中使用它。例如,让我们创建一个缓存/记忆函数来增加全部数字:

const incrementDelta = useCallback(() => setDelta(delta => delta + 1), []);
const increment = useCallback(() => setC(c => c + delta), [delta]);

// Can depend on [delta] instead, but it would be brittle
const incrementBoth = useCallback(() => {
    incrementDelta();
    increment();
}, [increment, incrementDelta]); 

新的 incrementBoth 函数依赖 delta,我们可以使用 useCallback(... ,[delta]) 。然而,这是一个十分脆弱的方法!如果我们改变 increment 或者 incrementDelta 的依赖,我们不得不记住 incrementBoth 的依赖的改变。

由于 increment 或者 incrementDelta 的引用不会改变,除非它们的依赖改变了,我们才能使用它们。依赖可以被忽略!这是一个简单的规则:
在功能组件范围内声明的每个函数都必须使用 useCallback 进行存储/缓存。如果它从组件作用域引用函数或其他变量,则应在其依赖列表中列出它们。
可以由linter强制执行此规则,以检查useCallback缓存相关性是否一致。

两个相似的 hooks - useCallback 和 useMemo
React 引入了另一个相似的 hook,叫做 useMemo。它有相同的签名,但是工作方式不同。不像 useCallback 缓存提供的函数实例,useMemo 调用提供的函数并缓存其结果:

const [c, setC] = useState(0);
// This value will not be recomputed between re-renders
// unless the value of c changes
const sinOfC: number = useMemo(() => Math.sin(c) , [c])

与使用 useCallback 一样,useMemo 返回的值可以用作其它 hooks 的依赖项。
有趣的是,useMemo 也可以缓存函数值。换句话说,它是 useCallback 的通用版本,在以下示例可以替换

// Some function ...
const f = () => { ... }

// The following are functionally equivalent
const callbackF = useCallback(f, [])
const callbackF = useMemo(() => f, [])

JS 条件语句

在编写 js 的过程中,我们肯定会用到条件语句。

常用的 if...else

大部分的条件语句都是用 if...else,适用于“非黑即白”的情况:

场景一:是否登录

根据不同的登录态做不同的行为:

if (isLogin) {
  // do something
} else {
  this.login()
}

场景二:性别差异

根据不同的性别做不同的行为:

if (sex === 'male') {
  // do something
} else {
  // do something
}

判断条件变多时

也有一些时候条件的情况可能超过两种,这个时候可以用 if...else if...else:

场景一:成绩等级差异

根据不同的等级做不同的行为:

if (grade === 'A') {
  // do something
} else if (grade === 'B') {
  // do something
} else {
  // do something
}

当然,还会有某些情况下,条件可能会更多,这个时候就有很多种选择方案了:

场景二:一周的日期

根据不同的日期吃不同的食物:

function eat(food) {
	console.log('eat food:', food)
}

function getFood(day) {
    if (day === 'day0') {
    	eat('apple')
    } else if (day === 'day1') {
		eat('banana')
	} else if (day === 'day2') {
		eat('orange')
	} else if (day === 'day3') {
		eat('noodle')
	} else if (day === 'day4') {
		eat('milk')
	} else if (day === 'day5') {
		eat('rice')
	} else if (day === 'day6') {
		eat('water')
	}
}

(插一句,我们可以尽可能把出现概率高的放在前面,这样可以提高效率。)

这种情况下有七个判断条件,可以看到可能有一点乱,这个时候,就可以考虑用 switch 语句了,这是针对较多条件下的判断方式:

function eat(food) {
	console.log('eat food:', food)
}

function getFood(day) {
	switch (day) {
		case 'day0':
			eat('apple')
			break;
		case 'day1':
			eat('banana')
			break;
		case 'day2':
			eat('orange')
			break;
		case 'day3':
			eat('noodle')
			break;
		case 'day4':
			eat('milk')
			break;
		case 'day5':
			eat('rice')
			break;
		default:
			eat('water')
	}
}

可以看到,虽然代码行数多了一点,但是更加简洁易懂。

另:关于 switch,有几个注意点。

  • switch 里的条件和 case 的条件要 ===,不然不会匹配
  • 每个 case 都要记得加上 break
  • 可以利用 switch (true) 来解决一些条件判断:
switch (true) {
  case age > 0 && age < 20:
    // do something
    break;
  // ...
}

一般讲到这里基本就结束了,不过...
比如我们可以把上面的“一周的日期”这个场景重写一下:

function eat(food) {
	console.log('eat food:', food)
}

const foodObj = {
	'day0': 'apple',
	'day1': 'banana',
	'day2': 'orange',
	'day3': 'noodle',
	'day4': 'milk',
	'day5': 'rice',
	'day6': 'water'
}

function eatFood(day) {
  eat(foodObj[day])
}

可以发现,代码量是不是少了很多。将条件变成对象键值对。
当然,可以用对象就可以用 map 结构:

function eat(food) {
	console.log('eat food:', food)
}

const foodMap = new Map([
	['day0', 'apple'],
	['day1', 'banana'],
	['day2', 'orange'],
	['day3', 'noodle'],
	['day4', 'milk'],
	['day5', 'rice'],
	['day6', 'water']
])
	
function eatFood(day) {
  eat(foodMap.get(day))
}

对象和 map 的差别主要是顺序不同,然后 map 的数量可以通过 map.size 拿到,并且 map 的键不止是字符串。
这种方式的好处在多条件下更加明显,回到上面的一个例子:

function eat(food) {
	console.log('eat food:', food)
}

function play(item) {
   console.log('play', item)
}

function getFood(sex, day) {
    if (sex === 'boy') {
        if (day === 'day0') {
	    	eat('apple')
	    	play('computer')
	    } else if (day === 'day1') {
			eat('banana')
			play('basketball')
		} else if (day === 'day2') {
			eat('orange')
			play('baseball')
		} else if (day === 'day3') {
			eat('noodle')
			play('football')
		} else if (day === 'day4') {
			eat('milk')
			play('pingpong')
		} else if (day === 'day5') {
			eat('rice')
			play('golf')
		} else if (day === 'day6') {
			eat('water')
			play('volleyball')
		}
    } else {
      	if (day === 'day0') {
	    	eat('apple1')
	    } else if (day === 'day1') {
			eat('banana1')
		} else if (day === 'day2') {
			eat('orange1')
		} else if (day === 'day3') {
			eat('noodle1')
		} else if (day === 'day4') {
			eat('milk1')
		} else if (day === 'day5') {
			eat('rice1')
		} else if (day === 'day6') {
			eat('water1')
		}
		play('makeUp') // 女生肯定天天化妆
    }
}

在多个条件的情况下,代码量会成倍增加,很是难看。但是如果我们用 map 来做的话:

function eat(food) {
	console.log('eat food:', food)
}

function play(item) {
   console.log('play', item)
}

const map1 = new Map([
    ['day0_boy', ['apple', 'computer']],
	['day1_boy', ['banana', 'basketball']],
	['day2_boy', ['orange', 'baseball']],
	['day3_boy', ['noodle', 'football']],
	['day4_boy', ['milk', 'pingpong']],
	['day5_boy', ['rice', 'golf']],
	['day6_boy', ['water', 'volleyball']],
        ['day0_girl', ['apple1', 'makeUp']],
	['day1_girl', ['banana1', 'makeUp']],
	['day2_girl', ['orange1', 'makeUp']],
	['day3_girl', ['noodle1', 'makeUp']],
	['day4_girl', ['milk1', 'makeUp']],
	['day5_girl', ['rice1', 'makeUp']],
	['day6_girl', ['water1', 'makeUp']]  
])

function foodAndPlay(day, sex) {
  eat(map1.get(`${day}_${sex}`)[0])
  play(map1.get(`${day}_${sex}`)[1])
}

这样写简洁易懂,上面的那种用对象也可以实现,下面这种就只能通过 map 了,因为对象的 key 只能是字符串:

function eat(food) {
	console.log('eat food:', food)
}

function play(item) {
   console.log('play', item)
}

const map2 = new Map([
	[{day: 'day0', sex: 'boy'}, ['apple', 'computer']],
	[{day: 'day1', sex: 'boy'}, ['banana', 'basketball']],
	[{day: 'day2', sex: 'boy'}, ['orange', 'baseball']],
	[{day: 'day3', sex: 'boy'}, ['noodle', 'football']],
	[{day: 'day4', sex: 'boy'}, ['milk', 'pingpong']],
	[{day: 'day5', sex: 'boy'}, ['rice', 'golf']],
	[{day: 'day6', sex: 'boy'}, ['water', 'volleyball']],
        [{day: 'day0', sex: 'girl'}, ['apple1', 'makeUp']],
	[{day: 'day1', sex: 'girl'}, ['banana1', 'makeUp']],
	[{day: 'day2', sex: 'girl'}, ['orange1', 'makeUp']],
	[{day: 'day3', sex: 'girl'}, ['noodle1', 'makeUp']],
	[{day: 'day4', sex: 'girl'}, ['milk1', 'makeUp']],
	[{day: 'day5', sex: 'girl'}, ['rice1', 'makeUp']],
	[{day: 'day6', sex: 'girl'}, ['water1', 'makeUp']]
])

function foodAndPlay(day, sex) {
   const result = [...map2].filter(([key, value]) => (key.day === day && key.sex === sex))
   result.forEach(([key, value] => {
     eat(value[0])
     play(value[1])
   }))   
}

上面的result.forEach方法可以直接换成下面这个,不过可能有点语义上的不友好:

   eat(result[0][1][0])
   play(result[0][1][1])

现在需求改了,小姐姐们为了减肥,决定每天都只喝水,那么 map 就会变成下面这样:

const map3 = new Map([
	[{day: 'day0', sex: 'boy'}, ['apple', 'computer']],
	[{day: 'day1', sex: 'boy'}, ['banana', 'basketball']],
	[{day: 'day2', sex: 'boy'}, ['orange', 'baseball']],
	[{day: 'day3', sex: 'boy'}, ['noodle', 'football']],
	[{day: 'day4', sex: 'boy'}, ['milk', 'pingpong']],
	[{day: 'day5', sex: 'boy'}, ['rice', 'golf']],
	[{day: 'day6', sex: 'boy'}, ['water', 'volleyball']],
        [{day: 'day0', sex: 'girl'}, ['water1', 'makeUp']],
	[{day: 'day1', sex: 'girl'}, ['water1', 'makeUp']],
	[{day: 'day2', sex: 'girl'}, ['water1', 'makeUp']],
	[{day: 'day3', sex: 'girl'}, ['water1', 'makeUp']],
	[{day: 'day4', sex: 'girl'}, ['water1', 'makeUp']],
	[{day: 'day5', sex: 'girl'}, ['water1', 'makeUp']],
	[{day: 'day6', sex: 'girl'}, ['water1', 'makeUp']]
}

可以看到,小姐姐们每一天都是重复的,那这样写其实很浪费,怎么办呢:

function eat(food) {
	console.log('eat food:', food)
}

function play(item) {
   console.log('play', item)
}

const map3 = new Map([
	[/^boy_day0$/, ['apple', 'computer']],
	[/^boy_day1$/, ['banana', 'basketball']],
	[/^boy_day2$/, ['orange', 'baseball']],
	[/^boy_day3$/, ['noodle', 'football']],
	[/^boy_day4$/, ['milk', 'pingpong']],
	[/^boy_day5$/, ['rice', 'golf']],
	[/^boy_day6$/, ['water', 'volleyball']],
        [/^girl_day[0-6]$/, ['water1', 'makeUp']]
])

function foodAndPlay(day, sex) {
   const result = [...map3].filter(([key, value]) => (key.test(`${sex}_${day}`)))
   result.forEach(([key, value]) => {
     eat(value[0])
     play(value[1])
   })
}

利用正则来实现条件,是不是很赞!

其实我以前就写过,比如对代码转义来减少 XSS 攻击:
####常规写法:

function transformHtml(str) {
    return str.replace(/[&<>'"]/g, (item) => {
        switch (item) {
            case '&':
               return '&amp;';
            case '<':
               return '&lt;';
            case '>':
               return '&gt;';
            case '’':
               return '&#39;';
            case '"':
                return '&quot'
        }
    })
}
escapeHTML('<script>1</script>') // "&lt;script&gt;1&lt;/script&gt;"

对象写法:

const escapeHTML = str =>
  str.replace(
    /[&<>'"]/g,
    tag =>({'&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;' }[tag] || tag)
  );
escapeHTML('<script>1</script>') // "&lt;script&gt;1&lt;/script&gt;"

还有一点,关于函数返回值:

// 记住一句,有花括号就要 return
let fn1 = function (a) {
    a
}
let fn2 = function (a) {
    return a
}
let fn3 = (a) => (a) 
let fn4 = (a) => { a }
let fn5 = (a) => {return a}

fn1(1) // undefined
fn1(2) // 1
fn1(3) // 1
fn1(4) // undefined
fn1(5) // 1

完。

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.