Code Monkey home page Code Monkey logo

blog's People

Contributors

keep-run avatar

Stargazers

 avatar  avatar

blog's Issues

箭头函数

这里是这里更加详细的解读

箭头函数和普通函数的区别:

没有this

普通函数的this是在执行时绑定的,但是箭头函数本身没有this.所以需要通过查找作用域链来确定this的值。如果箭头函数被普通函数包含,那么箭头函数中的this就是最近一层非箭头函数的this;

没有arguments对象

获取参数的时候,可以用参数名来获取,或者用ES6中的 rest参数获取;

不能用new关键字调用

JS函数有两个内部方法[[Call]]和[[Construct]]

  • 通过new关键字调用时,会指行[[Construct]],创建一个实例对象,然后再执行函数体,将this绑定到该实例对象上。(作为构造函数使用)
  • 直接调用时,执行[[call]]方法,直接执行函数体;

箭头函数没有[[Construct]]方法,因此不能作为构造函数使用,如果用new来调用,会报错;

没有原型

由于不能使用new调用箭头函数,因此没有构建原型的需求,所以箭头函数没有prototype

没有super

箭头函数没有原型,所以也不能通过super来访问原型属性。--实际上箭头函数没有spuer;跟 this、arguments、new.target 一样,这些值由外围最近一层非箭头函数决定。

从零开始实现promise(三)

概要

前边两篇文章基本实现了一个简单的promise。但是除此之外,promise还有一些常用的基础方法,比如 catch, finially 以及 resolve,reject,race, any,all等静态方法。,这里来谈谈其实现原理。

resolve & reject

resolvereject这两个方法是promise的静态方法,都返回一个新的promise,对应的状态分别为 fulfilledrejected。实现起来相对比较简单:

class MyPromise {
  ...
  static resolve (data) {
    return new MyPromise(resolve => {
      resolve(data)
    })
  }
  static reject (data) {
    return new MyPromise((resolve,reject) => {
      reject(data)
    })
  }
}

catch & finally

大家都知道一句话,catch本质上是then的语法糖,这是个什么意思呢,个人理解就是catch是then的某一种使用方法而已,其实finally也一样。都可能通过包装then来实现,代码如下:

class MyPromise {
  ...
  // 捕获前边未处理的reject。
  catch (onRejected) {
    return this.then(null,onRejected)
  }

  // 无论前边promise是何种状态,都会执行的方法
  finally (fn) {
    return this.then(fn,fn)
  }
}

all

all也是一个静态方法,将多个promise实例包装成一个新的promise,使用方法如下:

const p = Promise.all([p1,p2,p3])

使用说明如下:

  • p1,p2,p3都是promise实例,如果不是会自动使用Promise.all包装;
  • p1,p2,p3都变成fulfilled状态时,p才会变成fulfilled状态, p1,p2,p3返回值组成一个数组传递给p的回调函数。
  • p1,p2,p3中只要有一个变为rejected状态,p就变成rejected状态,第一个被reject的实例返回值传给p的回掉函数。

代码实现

class MyPromise {
  ...
  static all(promiseArr) {
    if (!Array.isArray(promiseArr)) {
      throw new Error('参数必须是数组')
    }
    return new MyPromise((resolve, reject) => {
      const result = []
      const Arrlength = promiseArr.length
      let tempItem
      let resolveNums = 0  //记录fulfilled实例的个数
      for (let i = 0; i < Arrlength; i++) {
        tempItem = promiseArr[i]
        if (!(tempItem instanceof MyPromise)) {
          tempItem = MyPromise.resolve(tempItem)
        }
        tempItem.then((data) => {
          resolveNums++
          result[i] = data  // 保证前后位置对应
          // 全部都变为fulfilled,返回
          if (resolveNums === Arrlength) {
            resolve(result)
          }
        }, (data) => {
          reject(data)
        })
      }
    })
  }
}

测试demo

const p1 = new MyPromise(resolve => {
  setTimeout(resolve, 1000, "p1")
})
const p2 = 'p2'
const p3 = MyPromise.resolve('p3')

MyPromise.all([p1, p2, p3]).then(data => {
  console.log(data)
}).catch(data => {
  console.log(data)
})

一秒后输出 "p1","p2","p3"

any

anyall接受的参数以及规则是一样的,但是功能正好相反,比如以下代码

const p = Promise.all([p1,p2,p3])
  • p1,p2,p3都变成rejected状态时, p才会变成rejected
  • p1,p2,p3有一个变为fulfilled状态时,p就会变成fulfilled

代码实现

class MyPromise {
  ...
  static any(promiseArr) {
    if (!Array.isArray(promiseArr)) {
      throw new Error('参数必须是数组')
    }
    return new MyPromise((resolve, reject) => {
      const result = []
      const Arrlength = promiseArr.length
      let tempItem
      let rejectedNums = 0  //记录rejected实例的个数
      for (let i = 0; i < Arrlength; i++) {
        tempItem = promiseArr[i]
        if (!(tempItem instanceof MyPromise)) {
          tempItem = MyPromise.resolve(tempItem)
        }
        tempItem.then(data => {
          resolve(data)
        }, data => {
          result[i] = data
          if (++rejectedNums === Arrlength) {
            reject(result)
          }
        })
      }
    })
  }
}

测试demo

const p1 = new MyPromise((resolve, reject) => {
  setTimeout(reject, 1000, "p1")
})
const p2 = MyPromise.reject('p2')
const p3 = MyPromise.reject('p3')

MyPromise.any([p1, p2, p3]).then(data => {
  console.log("then", data)
}).catch(data => {
  console.log("reject", data)
})

1s后输出

reject [ 'p1', 'p2', 'p3' ]

race

race接受的参数和参数的处理规则和any以及all是一样的。实现的功能是:参数中的promise,有任意一个状态改变,新的promise状态就会改变。率先改变的promise的返回值将会传递给回调函数;

代码实现

class MyPromise {
  ...
  static race(promiseArr) {
    if (!Array.isArray(promiseArr)) {
      throw new Error('参数必须是数组')
    }
    return new MyPromise((resolve, reject) => {
      const Arrlength = promiseArr.length
      let tempItem
      for (let i = 0; i < Arrlength; i++) {
        tempItem = promiseArr[i]
        if (!(tempItem instanceof MyPromise)) {
          tempItem = MyPromise.resolve(tempItem)
        }
        tempItem.then((data) => {
          resolve(data)
        }, (data) => {
          reject(data)
        })
      }
    })
  }
}

测试demo

const p1 = new MyPromise((resolve, reject) => {
  setTimeout(reject, 1000, "p1")
})
const p2 = new MyPromise((resolve, reject) => {
  setTimeout(reject, 2000, "p2")
})
const p3 = new MyPromise((resolve, reject) => {
  setTimeout(reject, 3000, "p3")
})


MyPromise.race([p1, p2, p3]).then(data => {
  console.log("then", data)
}).catch(data => {
  console.log("reject", data)
})

1s后输出

reject p1

附录(源码)

// 先定义三个状态变量
const PENDING = 'pending'
const REJECTED = 'rejected'
const FULFILLED = 'fulfilled'

class MyPromise {

  state = PENDING
  value = ''      // 向后传的value值
  callbacks = []  // 回调队列

  constructor(fn) {
    if (typeof fn !== "function") {
      throw new Error('参数必须是函数')
    }
    fn(this._resolve.bind(this), this._reject.bind(this))
  }

  static resolve(data) {
    return new MyPromise(resolve => {
      resolve(data)
    })
  }
  static reject(data) {
    return new MyPromise((resolve, reject) => {
      reject(data)
    })
  }

  static all(promiseArr) {
    if (!Array.isArray(promiseArr)) {
      throw new Error('参数必须是数组')
    }
    return new MyPromise((resolve, reject) => {
      const result = []
      const Arrlength = promiseArr.length
      let tempItem
      let resolveNums = 0  //记录fulfilled实例的个数
      for (let i = 0; i < Arrlength; i++) {
        tempItem = promiseArr[i]
        if (!(tempItem instanceof MyPromise)) {
          tempItem = MyPromise.resolve(tempItem)
        }
        tempItem.then((data) => {
          resolveNums++
          result[i] = data
          // 全部都变为fulfilled,返回
          if (resolveNums === Arrlength) {
            resolve(result)
          }
        }, (data) => {
          reject(data)
        })
      }
    })
  }


  static race(promiseArr) {
    if (!Array.isArray(promiseArr)) {
      throw new Error('参数必须是数组')
    }
    return new MyPromise((resolve, reject) => {
      const Arrlength = promiseArr.length
      let tempItem
      for (let i = 0; i < Arrlength; i++) {
        tempItem = promiseArr[i]
        if (!(tempItem instanceof MyPromise)) {
          tempItem = MyPromise.resolve(tempItem)
        }
        tempItem.then((data) => {
          resolve(data)
        }, (data) => {
          reject(data)
        })
      }
    })
  }

  static any(promiseArr) {
    if (!Array.isArray(promiseArr)) {
      throw new Error('参数必须是数组')
    }
    return new MyPromise((resolve, reject) => {
      const result = []
      const Arrlength = promiseArr.length
      let tempItem
      let rejectedNums = 0  //记录rejected实例的个数
      for (let i = 0; i < Arrlength; i++) {
        tempItem = promiseArr[i]
        if (!(tempItem instanceof MyPromise)) {
          tempItem = MyPromise.resolve(tempItem)
        }
        tempItem.then(data => {
          resolve(data)
        }, data => {
          result[i] = data
          if (++rejectedNums === Arrlength) {
            reject(result)
          }
        })
      }
    })
  }
  // 捕获前边未处理的reject。
  catch(onRejected) {
    return this.then(null, onRejected)
  }

  // 无论前边promise是何种状态,都会执行的方法
  finally(fn) {
    return this.then(fn, fn)
  }

  then(onFulfilled, onRejected) {
    const _this = this;   //_this指向前一个promise对象
    return new MyPromise((resolve, reject) => {
      _this._handle({
        onFulfilled,
        onRejected,
        resolve,
        reject
      })
    })
  }

  _resolve(value) {
    if (this.state !== PENDING) {
      return
    }
    this.state = FULFILLED  //修改状态
    this.value = value      // 赋值,用于向后传递
    this.callbacks.forEach(cb => this._handle(cb))  //回调then中的注册函数
  }

  _reject(error) {
    if (this.state !== PENDING) {
      return
    }
    this.state = REJECTED  //修改状态
    this.value = error      // 赋值,用于向后传递
    this.callbacks.forEach(cb => this._handle(cb))  //回调then中的注册函数
  }
  _handle(callback) {
    // prnding状态时,注册函数入栈
    if (this.state === PENDING) {
      this.callbacks.push(callback)
      return
    }
    // 按需获取then中注册的回调函数
    const cb = this.state === FULFILLED ? callback.onFulfilled : callback.onRejected

    // 改变then返回的promise的状态,持续回调后续的then
    const cb_changeState = this.state === FULFILLED ? callback.resolve : callback.reject

    let ret

    if (cb) {
      ret = cb(this.value) //计算继续向后传递的值
      // 返回一个promise
      if (ret instanceof MyPromise) {
        ret.then((data) => {
          callback.resolve(data)
        }, (err) => {
          callback.reject(err)
        })
      } else {
        callback.resolve(ret)
      }
    } else {
      cb_changeState(this.value) //then里没有回调函数,把当前value继续向下传递
    }
  }
}

nginx常见配置

一般公司的开发机自带nginx;

  • 安装:yum install nginx;
  • 启动:service nginx start;
  • 配置:先进入'/etc/nginx' ,配置文件为该目录下的nginx.conf

配置跨域:

  location / {  
    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
    add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';

    if ($request_method = 'OPTIONS') {
        return 204;
    }
}
详情:https://segmentfault.com/a/1190000012550346?utm_source=tag-newest

ts重要知识点

索引类型( keyof & T[K ])

使用索引类型,编译器可以检查使用动态属性名的代码。

  • 索引类型查询操作符:keyof。对任何类型Tkeyof T为已知类型T的属性名的联合,比如
interface Person {
    name: string;
    age: number;
}
let personProps:keyof Person    //personProps:"name" | "age"
  • 索引访问操作符:T[K]
interface Person {
  name: string;
  age: number;
}
function demo<T extends object, K extends keyof T>(obj: T, names: K[]): T[K][] {
  return names.map(item => obj[item])
}
let people: Person = {
  name: "test",
  age: 21
}
let res1 = demo(people, ["name"])  //string[] 类型
let res2 = demo(people, ["name",'age'])   //(string|number)[] 类型

T[K] 表明people['name']的类型为Person['name']。

接口(interface) 和 类型别名(type)的区别

类型别名和接口用法相似,但是还是有细微区别

  • 接口创建了新的名字,鼠标放上去,展示的就是接口名,别名没有创建新名称,鼠标悬停展示的是字面量。
  • 类型别名不能被 extends和 implements;

类型区分&类型保护

用户自定义的类型保护

interface Bird {
    fly();
    layEggs();
}

interface Fish {
    swim();
    layEggs();
}
// 有一个变量是Bird或者Fish中的一个,如何区分?
function isFish(pet:Fish|Bird):pet is Fish{
    return (<Fish>pet).swim!==undefined
}

command.js备注

  • 写完你的指令之后,一定要执行program.parse(process.argv);
  • -h 只会列出所有的指令,但是这些指令的 options会折叠起来,不会展示, 比如:create [options],想要看options的详细信息,可以执行xx create --help

class和hooks的ref处理

介绍

在react的日常开发过程中,ref虽然使用不多,但是却是一个非常重要的属性,可以解决很多实际问题。比如:

  • class组件中,父组件可以利用ref获取子组件的实例,从而访问子组件的状态以及方法(虽然不建议这么用,但确实有一定的使用场景);
  • 函数组件中,配合useRefuseImperativeHandle使用,可以暴露出供父组件访问的属性和方法(注意函数组件没有实例,不能通过获取实例的方式访问子组件);

下边就具体看看函数组件和class组件在使用中的区别;

class组件中的ref

import React, { Component, createRef } from 'react'
import { Button } from 'antd'

class ChildComp extends Component {
  state = {
    bgColor: 'red'
  }
  setBgColor = (bgColor) => {
    this.setState({ bgColor })
  }
  render() {
    return (<div style={{ width: '200px', height: "50px", backgroundColor: this.state.bgColor }} />)
  }
}

export default class Demo extends Component {
  constructor(props) {
    super(props)
    this.childRef = createRef()
  }
  /** 父组件中调用子组件的方法,从而改变子组件的状态 */
  setChildBgColor = (color) => {
    this.childRef.current.setBgColor(color)
  }
  render() {
    return (
      <>
        <Button onClick={() => { this.setChildBgColor('red') }}>red</Button>
        <Button onClick={() => { this.setChildBgColor('black') }}>black</Button>
        <Button onClick={() => { this.setChildBgColor('green') }}>green</Button>
        <ChildComp ref={this.childRef} />
      </>
    )
  }
}

函数组件中的ref

import React, { useRef, forwardRef, useState, useImperativeHandle } from 'react'
import { Button } from 'antd'

function Child(props, ref) {
  const [bgcolor, setBgColor] = useState('red')
  useImperativeHandle(ref, () => {
    return { setBgColor: setBgColor }   //区别在这里,手动暴露供父组件访问的属性和方法
  })
  return <div style={{ width: '200px', height: "50px", backgroundColor: bgcolor }} />
}

const ChildComp = forwardRef(Child) // 注意ref需要用forwardRef承接,不能直接从props中取

export default () => {
  const childRef = useRef(null)
  const setChildBgColor = (color) => {
    childRef.current.setBgColor(color)
  }
  return (
    <>
      <Button onClick={() => { setChildBgColor('red') }}>red</Button>
      <Button onClick={() => { setChildBgColor('black') }}>black</Button>
      <Button onClick={() => { setChildBgColor('green') }}>green</Button>
      <ChildComp ref={childRef} />
    </>
  )
}

总结

实现的功能相似,但是注意实现上的差异,见函数组件的备注。

长列表优化

介绍

长列表优化是一个常见的问题,简单来说就是服务一下给你返回一万条数据,前端如果真的全部渲染出来,性能将是一个无法想象的问题;怎么做到只渲染可视区以及附近那几条数据的渲染,其他区域不渲染是一个前端问题,也就是这里所说的长列表优化。一般来讲这种问题有两种解决办法:

  • 做分页处理(服务分页0r前端伪分页均可);
  • 虚拟列表;

分页处理是一个常见的处理方式,技术上没啥难度;虚拟列表处理在社区有很多讨论,也有npm包可以直接使用,这里分析一下其原理和简易实现;

虚拟列表原理

虚拟列表优化的前提条件是:列表每一项的高度一样活着近似,否则会计算不准确;

虚拟列表的核心有以下几点:

  • 可视区域用真实的数据填充;
  • 非可视区的面积通过padding来填充(这样滚动条以及滚动数据接近展示情况);
  • 监听父容器的scroll事件,通过scrollTop的值计算当前可视区应该展示的数据的范围;

demo实现

import React, { useState, useEffect, useRef, } from 'react'
import './index.less'

const height = 40  //列表的每个item的高度为40
const bufferSize = 5  //缓冲区域,实现无缝滚动的效果

export default (props) => {
  const [startOffset, setStartOffset] = useState<number>(0)
  const [endOffset, setEndOffset] = useState<number>(0)
  const [visibleData, setVisibleData] = useState<Array<any>>([])
  const container = useRef(null)
  const actionData: any = useRef({
    startIndex: 0,
    endIndex: 0,
    visibleCount: 10 + bufferSize,
    data: (new Array(1000)).fill('test')   //初始化1000条数据做测试
  })


  //计算 startIndex 和  endIndex 并设置上下边距
  const updateBoundary = (scrollTop) => {
    const { data, visibleCount } = actionData.current
    const dataLength = data.length
    const startIndex = Math.min(Math.floor(scrollTop / height), dataLength - visibleCount) 
    const endIndex = startIndex + visibleCount
    actionData.current.startIndex = startIndex
    actionData.current.endIndex = endIndex
    setStartOffset(startIndex * height) 
    setEndOffset((dataLength - endIndex) * height)
  }


  // 通过 startIndex, endIndex 截取真实的数据做渲染
  const updateVisibleData = () => {
    const { startIndex, endIndex, data } = actionData.current
    setVisibleData(data.slice(startIndex, endIndex))
  }

  const handleScroll = () => {
    const scrollTop = container.current.scrollTop
    updateBoundary(scrollTop)
    updateVisibleData()
  }

  useEffect(() => {
    const { visibleCount, startIndex } = actionData.current
    actionData.current.endIndex = visibleCount + startIndex
    updateVisibleData()
  }, [])

  return (<div className='virtual__list-container' onScroll={handleScroll} ref={container}>
    <div className='virtual__list-wrap' style={{ paddingTop: `${startOffset}px`, paddingBottom: `${endOffset}px` }} >
      {visibleData.map((item, index) => (<div className="virtual__list-item">{item}----{index + actionData.current.startIndex}</div>))}
    </div>
  </div>
  )
}
// index.less
.virtual__list-container{
  width: 100%;
  height:400px;
  overflow: scroll;
  background-color: cadetblue;
}

.virtual__list-wrap{
  width: 100%;
}

.virtual__list-item{
  height: 40px;
  line-height: 40px;
  border-bottom: 1px solid black;
}

最终实现效果如下:
demo

shell基础

参数获取

  • bash中执行: sh online.sh test1 test2,则在online.sh文件中可以分别得到三个变量:
$0    //值为 online.sh
$1    //值为 test1
$2    //值为 test2

JS中的同步等待

背景:访问一个接口,如果返回的值不符合要求,则过几秒去轮询。这个过程需要阻塞后续相关代码的执行。

代码:

//  模拟接口
function req() {
    return new Promise((reslove, reject) => {
        if (Math.random() > 0.9) {
            reslove(1)
        } else {
            reslove(0)
        }
    })
}

function sendReq(callback) {
    req().then(res => {
        console.log('res',res)
        if (res) {
            callback()
        } else {
            setTimeout(() => sendReq(callback), 1000)
        }
    })
}

function waitRes() {
    console.log('start')
    return new Promise(resolve => {
        sendReq(resolve)
    })
}

function end() {
    console.log('end')
}
async function run() {
    await waitRes()
    end()
}

run()

作用域

ES5的作用域

在ES5中没有块级作用域的概念。而有变量提升的概念。导致初学者会比较困惑。

变量提升

先看一个例子。

console.log(a);
var a=2

习惯了Java等语法的人肯定觉得以上代码会报错,因为变量升明在调用之后,但实际上却输出了undefined

这是为什么呢,原因就在于js引擎处理js代码时,会先进行编译,再去执行,而在编译阶段就会找到对应的变量声明,并和对应的作用域关联起来(变量提升)。

上述代码会被编译成如下形式:

var a;
console.log(a);
a=2

执行的时候按顺序执行,输出undefined也就理所应当了;
注意: 只有声明本身会提升,赋值或者运行等其他逻辑会留在原地;

函数提升

在js中函数本质上也是一个变量。因此遵守以上变量提升的规则。但是函数会形成一个自己的作用域(函数作用域),所以函数内部定义的变量会在函数作用域中完成变量提升,而不会提升到全局。看一个例子:

foo();
function foo(){
    console.log(a);
    var a=2
}

以上代码也会输出undefined;

在编译阶段,变量提升之后,代码转换为如下形式:

function foo(){
    var a
    console.log(a);
    a=2
}
foo();

可以看到,函数本身作为一个变量提升到最外层,而函数内部变量又在函数作用域中完成提升;

函数表达式不会被提升

这是一个容易犯的错误。比如如下代码:

foo()
var foo=function(){
    ..........
}

以上代码会报错,TypeError; 原因在于 以上代码会被编译成如下形式:

var foo;
foo();
foo=function(){
   .......
}

在执行foo()时,foo的值为undefined。对undefined执行函数操作,会报TypeError错误;

函数优先

当变量和函数名称相同时,函数提升会优先。但是相同名称的函数,后边是可以覆盖前边的。比如:

foo();
function foo(){
    console.log(1)
}
var foo=function(){
    console.log(2)
}
function foo(){
    console.log(3)
}

以上代码会输出3。因为函数首先提升,后边的会覆盖前边。而var foo出现的时候,前边已经有函数foo的声明,这里是个重复声明,因此被忽略。 引擎编译后的代码如下:

function foo(){
    console.log(3)
}
foo();
foo=function(){
    console.log(2)
}

ES6的作用域

ES6中引入了块级组作用域的概念。块级作用域主要存在以下两个地方

  • 函数
  • {}代码块中
    那么,对于js引擎来说,怎么确定这个代码块是不是一个块级作用域呢?比如以下代码:
{
    var a=1
}
{
    let b=2
}
console.log(a)   //1
console.log(b)  //Uncaught ReferenceError: b is not defined

JS引擎是怎么知道识别第二个代码块是一个块级作用域,而第一个不是呢?这就涉及到一个块级声明的概念。

块级声明(let 和 const)

块级声明的典型例子就是 let 和 const声明,都知道用这两个关键字声明的变量具有以下特征:

  • 变量不会提升;
  • 重复声明会报错;
  • 不会绑定到全局作用域(通过window对象无法访问);
  • const声明的变量不能再赋值;
    实际上JS引擎在编译代码时,遇到let或者const,会放到一个临时死区,在死区之外访问死区内的变量就会报错。只有执行过该死区的代码,变量才会从死区释放出来。所以以下代码会报错:
console.log(a);
let a=1

编辑阶段,遇到let声明的a变量,先放入死区,在执行console.log(a)时,a还在死区没释放出来。因此会报错;

在看下边一个例子:

{
    var a=1;
    let b=2;
}
console.log(a)   // 1
console.log(b)  // Uncaught ReferenceError: b is not defined

编译阶段 遇到var声明,会将a提升到顶级作用域,再遇到let声明,此时{}会形成一个块级作用域。因此再外边可以访问a却不能访问b;

规划结构,获取参数

源码地址:https://github.com/keep-run/simple-cli

有了前边的准备,下边我们首先规划一下项目的结构,我们预计要写这样三个指令:

simple-cli  init    //初始化一个项目;
simple-cli  start  //启动一个项目;
simple-cli  build  //打包项目;

首先在cli目录下建立如下目录:

cli
├── index.js
└── src
    ├── clis
    │   ├── build.js
    │   ├── init.js
    │   └── start.js
    └── index.js

其中cli/index.js为脚手架的根入口,src/index.js用来获取指令,选择执行clis下边的对应脚本。

为了测试,我们先在build.js、init.js和start.js下分别export一个函数,函数体分别为:console.log('build')console.log('init')console.log('strat')

为了后边可以用一些es6的语法,我们还需要在项目入口处配置babel。简单贴一下cli/index.js的代码。

#!/usr/bin/env node
require('@babel/polyfill')
require('@babel/core')

require('@babel/register')({
    presets: [require.resolve('@babel/preset-env')],
    ignore: [],
    cache: true,
  })
require('./src/index')

接下来就要通过输入的指令执行执行相应的脚本了。这里推荐一个npm包optimist, src/index.js的关键代码如下:

import optimist from 'optimist'
import fs from 'fs'
import path from 'path'

const { argv } = optimist
const commands = argv._
const clis = fs.readdirSync(path.resolve(__dirname, 'clis')).map(item => item.replace('.js', ''))
const cmd = clis.indexOf(commands[0]) > -1 ? commands[0] : ''
if (cmd) {
    const command = require(`./clis/${cmd}`).default
    argv.cwd = process.cwd()
    command(argv)
} else {
    console.log('not found')
}

到这里就可以在命令行执行simple-cli start,simple-cli init,simple-cli build指令。会输出clis目录下相应的脚本的console.log内容了。

下一步就是开发各脚本的具体内容了。

数据结构与算法--单链表

关于单链表的常见题目主要有以下几个方面:

判断链表是否有环

如下图所示,即为一个环形链表。写程序判断链表是否有环?
环形链表
这个题大概有以下两种解法:

暴力搜索法

从头开始遍历,用一个数组存储每个节点的hash值,每遍历一项,就去数组查找对应hash值,如果找到,则说明有环,否则没有环;而这种方法,需要额外的存储空间,还有大量的数组搜索,实际使用当中不值得推荐;

快慢指针法

设置两个指针:

  • 快指针:每次向后遍历两个节点;
  • 慢指针:每次向后遍历一个节点;

两个指针从头同时开始向后遍历,如果链表有环,那么两个指针总会有相遇的时候。JavaScript实现如下:

  function hasCycle (head) {
    let fast = head, slow = head;
    while (fast && fast.next) {
        fast = fast.next.next;
        slow = slow.next
        if (fast === slow) {
            return true
        }
    }
    return false
};

参考练习题目:leetcode 141题

查找一个链表的环的起点位置

这个问题是对前一个问题的提升,不光要判断链表是否有环,如果有环,还要找到环的入口位置。我们还是用前边的快慢指针法,接下来我们分析两个指针的相遇点和环的入口点有什么关系。如下图所示:
环链表1

假设链表的起点为A,环的入口点为B,快慢指针的相遇点在C处。(慢指针在走完第一圈之前一定会被快指针追上,据说这个是一个小学奥数题,网上很多相关分析证明,这里直接拿结论来用了)。相遇的时候:

  • 快指针走的距离:s_fast=LAB+LBC+LCB+LBC
  • 慢指针走的距离:s_slow=LAB+LBC

由于快指针的速度是慢指针的两倍,所以有:s_fast=2*s_slow,由此可以得到:LAB=LCB

上边这个结论就太重要了。这表明相遇点离环的起点距离和链表的起点与环的起点距离相等。那么,当快慢指针相遇的时候,让快指针变为慢指针,且从链表的头开始遍历,原始慢指针继续向后遍历。 那么两者一定会在环的入口位置相遇。

有了以上分析,具体实现也就不难了,代码如下:

var detectCycle = function (head) {
    let slow = head, fast = head;
    while (true) {
        if (!fast || !fast.next) { return null }
        fast = fast.next.next;
        slow = slow.next
        if (fast === slow) { break }
    }
    fast = head
    while (fast !== slow) {
        fast = fast.next;
        slow = slow.next
    }
    return fast
};

参考练习题目:leetcode 142题

判断两个链表是否有交叉?如果交叉,找出交叉点

交叉链表

分析以上交叉链表的特征,可以得出以下结论:

  • 如果交叉,那么两个链表的最后一个节点一定相同;
  • 假设长链表的长度为m,短链表的长度为n。那么长链表从第m-n个位置向后遍历,同时短链表从头开始向后遍历。那么他们第一次相遇的地方就是交叉点;

有了以上结论作支撑,JavaScript代码实现如下:

var getIntersectionNode = function (headA, headB) {
    let lenA = 0;
    let lenB = 0;
    let difLen = 0;
    let first = headA;
    let second = headB;

    while (first) {
        first = first.next;
        lenA++;
    }
    while (second) {
        second = second.next;
        lenB++;
    }
    if (lenA > lenB) {
        difLen = lenA - lenB;
        first = headA;
        second = headB;
    } else {
        difLen = lenB - lenA;
        first = headB;
        second = headA;
    }
    while (difLen--) {
        first = first.next
    }
    while (first && second) {
        if (first === second) {
            return first
        }
        first = first.next;
        second = second.next;
    }
    return null
};

上边代码虽然易懂,但是不够优雅,来自leetcode的高手写法如下:

var getIntersectionNode = function (headA, headB) {
    let pA = headA;
    let pB = headB;
    while (pA !== pB) {
        pA = pA == null ? headB : pA.next;
        pB = pB == null ? headA : pB.next;
    }
    return pA;
};

参考习题:LeetCode 160题

从零开始一个前端脚手架(三)

本节我们将陆续实现startinit指令;

start指令

start指令,用于启动一个项目,其原理是基于webpackDevServer在本地启一个服务。

webpack配置

首先配置入口文件src/index.js

import start from './commanders/start.js'
...
program
    .command('start')
    .description('start a program')
    .action(() => {
        start(config)
    })
...

src/commanders/start.js

import Webpack from "webpack";
import WebpackDevServer from "webpack-dev-server";
import path from 'path'
import getDevConfig from '../webpack/config.dev.js'
export default (config) => {
  const { port = 8000, cwd } = config
  const host = '0.0.0.0'
  const webpackConfig = getDevConfig(config)
  const devServer = {
    port,
    host,
    open: true,
    compress: true,

    static: {
      directory: path.join(cwd, 'dist'),
    }
  }

  const complier = Webpack(webpackConfig)
  const server = new WebpackDevServer(complier, devServer)

  server.listen(port, host, () => {
    console.log(`start server on http://localhost:${port}`)
  })
}

src/webpack/config.dev.js

import commonConfig from './config.com.js'

export default (pkg) => {
  return {
    mode: 'development',          //设置开发模式
    ...commonConfig(pkg)          //各个模式下的公共配置
  }
}

配置模板

对于一个前端项目,在浏览器打开是需要有一个html模板来承接的。webpack提供了一个插件HtmlWebpackPlugin,可以实现自动生成一个模板,并在模板内引入打包生成的所有的js文件。

配置src/babel/plugin.js

import HtmlWebpackPlugin from 'html-webpack-plugin'
...
export default () => {
  return [
    ...
    new HtmlWebpackPlugin(),    // 自动生成html模板,并引入打包生成的所有js文件

  ]
}

此时已经可以在本地启项目了。

测试

修改demo/index.js文件

 document.body.innerHTML = '<div>demo</div>'

demo目录下执行simple-cli start。会自动打开一个网页,显示demo文件。

init指令

作为一个完整的脚手架工具,除了start,build之外,init指令一样的重要,该指令的主要作用是:复制内置的模板到用户目录。

新建模板

首先建立一个react模板目录cli/templates/react:该目录下默认有两个文件index.jspackage.json
index.js:

import React from 'react'
import { render } from 'react-dom'

render(
  <div className="container">
    <div>this is a react app</div>
  </div>,
  document.getElementById('root'),
)

package.json:

{
  "name": "demo",
  "version": "1.0.0",
  "description": "a simple-cli init demo",
  "scripts": {
    "start": "simple-cli start",
    "build": "simple-cli build"
  },
  "creator": "",
  "license": "ISC",
  "simple-cli": {
    "port": 9000,
    "entry": "./index"
  },
  "dependencies": {
    "react": "^16.9.0",
    "react-dom": "^16.9.0"
  }
}

目前为止,我们的项目还不能解析jsx语法,我们更新src/babel/jsloader.js

...
{
  "test": /\.(m?js|jsx)$/,
  "exclude": /(node_modules|bower_components)/,
  "use": {
      loader: require.resolve('babel-loader'),     // require.resolve很重要,在当前项目的node_modules中寻找对应的模块
      options: {
        presets: [
          require.resolve('@babel/preset-env'),
          require.resolve('@babel/preset-react')     // 支持react
        ]
    }
  }
}

用同样的方法,可以配置其他对应的loader以支持less,stylus,css以及文件,图片等内容,这里不再累述,可以查看文末的源码。

此时任意新建一个目录,该目录下依次执行以下指令:

simple-init
npm install
simple-start

就会启动一个项目,并在打开一个默认的页面;

结语

至此,我们的脚手架的基本功能都已经实现了。其中包括init,start,build指令。可以满足日常所需,在生产环境,可以结合公司的技术栈,做相应的扩展。

源码地址

判断元素是否在可视区

判断元素是否在可视区,一般会用到以下函数的一种或者几种:

getBoundingClientRect

getBoundingClientRect这个函数会返回一个对象,这个对象中包含以下几个重要属性:

width     // 当前元素的宽度
height    // 当前元素的高度
left      // 当前元素左边离浏览器窗口左侧的距离,如果由于滚动,
          //  当前元素左侧超出浏览器左侧窗口,则该值为负值;

right      // 当前元素右侧离浏览器窗口左侧的距离
           // 如果由于滚动,当前元素从浏览器左侧完全滑出,则该值为负值,
          // 如果从浏览器右侧滑出,该值不受影响;仍为`left+width`

top        // 当前元素上边缘离浏览器上侧的距离,
           // 如果元素从浏览器浏览器上侧滑出,则该值为负值;

bottom     // 当前元素下边缘离浏览器上侧的距离,
           //  如果元素从浏览器浏览器上侧完全滑出,则该值为负值;
           // 如果从下侧完全滑出,该值不受影响;

具体情况如图所示:
boundingClientRect

通过以上分析,可以做如下判断:

  • top>0时,当前元素一定没有超出浏览器上侧;
  • left>0时,当前元素一定没有超出浏览器左侧;

但是 只通过这个元素无法判断元素有没有超出浏览器的下侧和右侧,要判断这两种情况就必须借助视窗的宽高信息;

scrollTop、offsetTop

  • scrollTop:MDN上解释为一个滚动元素的顶部到视口顶部的值。该值为正数。如果没有滚动,则该值为0; 这篇文章讲的跟清楚

  • offsetTop:一个只读属性,返回当前元素相对于其offsetParent元素的顶部距离,offsetParent元素为包裹该元素的最近的一个定位元素,如果没有定位元素,则为最近的table,table cell或者根元素。

流文件的读写操作

日常开发过程中,通常都是application/json格式的数据交互,但是偶尔也会遇到关于流文件的处理(比如上传下载文件)。

发送流(上传文件)

import FormData from 'form-data'
import axios from 'axios'
const form = new FormData();
    form.append(key, value);
    form.append('file', fs.readFileSync(filePath), filename)
    return request({
        method: 'post',
        url: url
        data: form,
        headers: form.getHeaders()    //FormData会自动设置合适的头
    })

接受流(接受文件,并写到指定位置)

const getFiles = async (data, targetDist) => {

  const res = await axios({
    method: 'post',
    baseURL: host,
    url: '/api/block/download',
    data:data,
    responseType: "stream"
  })

  return new Promise((reslove, reject) => {
    const writeStream = tar.extract({
      strip: 1,
      cwd: targetDist
    })

    res.data.pipe(writeStream)          //res.data就是一个文件流
      .on('end', () => {
        reslove()
        console.log('添加成功')
      })
      .on('error', (err) => {
        console.log('err', err)
        process.exit(1)
      })
  })
}

从零开始一个前端脚手架(四)

我们的cli工具现在虽然可以使用了,但是还是很简陋,这一节主要从以下两个方面去丰富其功能:

  • 打包css
  • 代理

css打包

css打包有两种形式,第一是借助style-loader.配置如下:

  {
    test: /\.css$/,
    exclude: /node_modules/,
    use: [require.resolve('style-loader'), require.resolve('css-loader')],
  },

这样会把css打包进js文件,最后以style标签的形式把css样式渲染在html中。这种方式在生产环境会产生一些问题:

  • 单个文件太大,影响加载;
  • 大量的css代码存在于html中,不利于排查问题以及样式覆盖;

第二种形式是借助插件MiniCssExtractPlugin。 把样式单独抽离出来打包成一个文件。在html中通过一个link标签引入即可,这也是当下主流的使用方式,配置如下:
配置loader:

  import MiniCssExtractPlugin from 'mini-css-extract-plugin'
  ...
  {
    test: /\.css$/,
    exclude: /node_modules/,
    use: [MiniCssExtractPlugin.loader, require.resolve('css-loader')],
  },

配置plugin(src/babel/plugins)

export default (config) => {
  return [
    ...
    new MiniCssExtractPlugin({
      filename: config.mode === 'development' ? 'bundle.css' : 'bundle.[contenthash].css'     //输出css文件名称,  开发环境,文件名不要带hash,带hash会做持久缓存,影响开发热更。  生产环境需要带hash,用于做缓存
    }),
  ]
}

这里有一个坑啊。 开发环境的css文件名不要带hash值。webpack官网有一个一句话的解释:

使用 filename: "[contenthash].css" 启动长期缓存。根据需要添加 [name]。

这导致一个什么问题呢,如果开发环境文件名称带了hash值,当我们修改css文件时,页面不会热更,所以这里文件名称会加入当前模式的判断。

代理

对于一个成熟的脚手架工具,开发过程中mock数据是一项基本且重要的操作。当然实现mock的方法有很多,比如常用的charles等。我们这里提供一个方法,代理到本地目录。省去各种配置。

基本原理是:devServer中拦截ajax请求,去本地读取某个文件作为接口返回。
commanders/strat.js中增加配置:

...
const { port, cwd, mock } = config  
...

{
  ...
    onBeforeSetupMiddleware: function (devServer) {
      if (devServer) {
        devServer.app.use((req, res, next) => {
          // 对于ajax请求,可以设置mock
          if (mock && req.headers['x-requested-with']) {  //请求头中增加 x-requested-with  早期ajax请求会自动加, 新的fetch没有了,需要手动加
            const filePath = path.join(cwd, `/mock/${req._parsedUrl.path}.json`)
            let mockData = {}
            try {
              mockData = require(filePath)
            } catch (err) {
              mockData = { message: `${filePath} is not a file` }
            }
            res.set('Content-Type', 'application/json')
            res.send(mockData)
          }
          next()
        })
      }
    }
}

几点解释:

  • mock字段作为配置,允许用户自定义。和其他配置字段一样在package.jsonsimple_cli字段下。
  • 发送请求的请求头中必须有x-requested-with字段,以此作为识别ajax请求的标识。
  • mock地址。在本地mock文件夹下。 比如请求路径为/test。则会读取本地目录:mock/test.json的值作为返回。

更多内容持续丰富中。

相关文章:

端口占用(listen EADDRINUSE)

端口占用&暴力杀死端口

在利用npm start或者npm run dev启动项目时经常遇到以下错误:

events.js:85
      throw er; // Unhandled 'error' event
            ^
Error: listen EADDRINUSE

很显然,这是端口占用了,怎么解决呢?

  • 第一步:执行lsof -i tcp : your-port,会返回占用该端口的进程,记住相关进程的pid;
  • 第二步:执行kill -9 your-pid。这一句会杀死相关进程;

到此,重新启动项目即可;

端口为什么会占用?

有的时候,项目的端口确实会被本地某个应用的端口占用,那么只能有以下两种解决方案:

  • 换一个项目端口,重启项目;
  • 利用前边的方法强制杀死该端口;重启项目

有时候会有另一种情况,没有应用占用端口,正常启动项目,一顿操作之后关闭项目,然后再启动项目,出现端口占用。这是为什么呢?

直观上看是项目停了,但是node服务没停,小白我尝试升级node版本,检查项目,最终发觉是我停止项目的方式有问题。正确方法是使用ctrl+c停止项目,而我误使用ctrl+z,两者区别:

  • ctrl+c:终止程序执行,结束进程;
  • ctrl+z:将前台执行的程序放到后台,并处于暂停状态;还有以下后续操作:
    • bg:启动当前后台暂停的进程; 该进程仍会后台运行;
    • fg:后台进程转向前台并启动; 如果有多个后台进程,可以先执行jobs,拿到各进程的jobsNumber,然后执行fg % jobsNumber,将指定的后台进程转向前台;

关于ts的一些坑

绑定键盘事件

  • 为一个元素绑定键盘事件,如下:
const handleSearch = (e:KeyboardEvent) => {  ....  }
...
<input
        type="search"
        onChange={(e) => { setKeyWord(e.target.value) }}
        onKeyPress={e => handleSearch(e)}
      />

此时ts报错:

类型“KeyboardEvent<HTMLInputElement>”的参数不能赋给类型“KeyboardEvent”的参数.
解决办法:

const handleSearch = (e:React.KeyboardEvent<HTMLInputElement>) => {...}

动态获取state

  • 动态获取state
let key=""
...
this.state[key]     //会报 类型any--->string的错误
...

解决办法

const state:pageState = this.state
...
state[key] 
...

接口任意属性

对象可能存在某些不确定的的属性。为其定义类型

interface  demo{
    width:string,
    [unsurekey:string]:any   //除了width属性以为,其他可能存在的属性值类型都为any
}

从零开始一个前端脚手架(一)

背景

脚手架在前端工程化领域的重要性不言而喻,每个公司(或团队)都会根据自己的业务特性维护适合自己的脚手架。具体来说其重要性体现在以下几个方面:

  • 统一开发流程(项目初始化,启动,打包,部署都可以收拢在脚手架内部完成,业务人员只用关注业务即可);
  • 与其他基建能力更好的配合,比如代码检测,mock等。都可以在脚手架层面对接;

既然如此,那作为一个前端人员,得理解脚手架的工作原理以及其基本实现吧。接下来我们一步一步从零开始完成一个简单的脚手架。

项目搭建

初始化项目

mkdir simple-cli
cd simple-cli
mkdir cli    // 存放cli源码的目录
mkdir demo   // 存放demo的目录

进入到cli目录(后续操作默认都在cli目录下)

平平无奇,直接执行npm init。一顿回车就能得到一个项目。但是只有一个package.json文件。

同一层级新建index.js

#!/usr/bin/env node
// 上边这一行代码很重要,指定开发环境为 node
console.log('---index.js-----')

全局链接配置

package.json文件增加配置:

{
  "bin": {
    "simple-cli": "index.js"
  }
}

bin的配置很重要,用来指明对应可执行文件的位置。 未来用户通过npm全局安装我们的脚手架之后,就可以执行simple-cli指令了。
更多关于package.json配置的解读在这里

但是这个包还在开发阶段,当然不可能安装了。此时可以借助另一个指令npm link。控制台进入当前目录下执行:

npm link

此时npm会将bin下的指令链接到全局(未来可以通过npm unlink断开)。

此时在控制台执行:

simple-cli    // 输出 ---index.js-----

babel配置

脚手架的执行环境是node。而node是遵循CommonJs的。 为了能执行ES6代码,则需要对我们的代码进行转义。继续在package.json中增加如下配置:

  "scripts": {
    "dev": "npx babel src --watch --out-dir lib"
  }

这个配置的含义是:babel转义src目录下的文件,并输出到lib目录下, --watch的含义是监听src目录,当该目录下有代码改动时,就会重新编译一次。

配置 .babelrc, 新建文件cli/.babelrc:

{
  "presets": [
    "@babel/preset-env"
  ]
}

控制台执行:

npm install @babel/cli --save-dev
npm install @babel/core --save-dev
npm install @babel/preset-env --save-dev
npm run dev

新建文件src/index.js,写入代码:

console.log('src/index.js')

此时会自动新建一个文件lib/index.js,其内容就是src/index.js转义后的代码。

修改cli/index.js文件代码为:

#!/usr/bin/env node
// 上边这一行代码很重要,指定开发环境为 node
require('./lib/index')

此时控制台输入

simple-cli    // 输出'src/index.js'

到这里得到如下目录:

.
├── cli
│   ├── index.js
│   ├── lib
│   │   └── index.js
│   ├── package.json
│   ├── .babelrc
│   └── src
│       └── index.js
└── demo

我们的项目基础配置已经完成了。后续在src目录下开发具体功能。

定义基础指令

对于一个脚手架工具,应该具有以下基础指令:

simple-cli start  
simple-cli build
simple-cli publish

这里借助commander来实现其定义。 首先npm install commander --save-dev。修改src/index.js文件:

import { program } from 'commander'
const pkg = require('../package.json')

// 定义 -v/ --version 指令 
program.version(`当前版本: ${pkg.version}`, '-v, --version', 'get current version')

// 自定义指令 start
program
    .command('start')
    .description('start a program')
    .action(() => {
        // todo
        console.log('command start ')
    })

// 自定义指令 build
program
    .command('build')
    .description('build program')
    .action(() => {
        // todo
        console.log('command build')
    })

// 自定义指令 publish
program
    .command('publish')
    .description('publish program')
    .action(() => {
        // todo
        console.log('command publish')
    })

program.parse(process.argv)

此时控制台执行simple-cli help将得到如下输出:

Options:
  -v, --version   get current version
  -h, --help      display help for command

Commands:
  start           start a program
  build           build program
  publish         publish program
  help [command]  display help for command

这里只做了其定义,每个指令的具体实现将在后面陆续实现。

未完待续

源码地址

参考文章

promise应用---红绿灯问题

基于promise和递归可以实现红绿灯问题。其中红灯亮3秒,黄灯2秒,绿灯1秒。代码如下:

function red() {
  console.log('red')
}
function green() {
  console.log('green')
}
function yellow() {
  console.log('yellow')
}

function light(callback, timer) {
  return new Promise((reslove) => {
    setTimeout(function () {
      callback();
      reslove();
    }, timer)
  })
}
function step() {
  Promise.resolve().then(() => {
    return light(red, 3000)     //这里一定要return
  }).then(() => {
    return light(green, 1000)
  }).then(() => {
    return light(yellow, 2000)
  }).then(() => {
    step()
  })
}
step()     //该怎么设置停止这个递归呢?

从零开始实现promise(二)

问题分析

上一篇文章实现了一个简单的promise,文章末尾提到一个问题,当前then的回调怎么只受前一个promise状态控制;其实问题主要出在这几行代码中

// 按需获取then中注册的回调函数
const cb = this.state === FULFILLED ? callback.onFulfilled : callback.onRejected

// 改变then返回的promise的状态,持续回调后续的then
const cb_changeState = this.state === FULFILLED ? callback.resolve : callback.reject

cb取决于当前状态无可厚非,但是cb_changeState就不应该了。cb_changeState应该取决于cb的执行结果,如果结果是一个promise对象,则依赖该promise的状态;

优化代码

class MyPromise {
  ...
 _handle(callback) {
    // prnding状态时,注册函数入栈
    if (this.state === PENDING) {
      this.callbacks.push(callback)
      return
    }
    // 按需获取then中注册的回调函数
    const cb = this.state === FULFILLED ? callback.onFulfilled : callback.onRejected

    // 改变then返回的promise的状态,持续回调后续的then
    const cb_changeState = this.state === FULFILLED ? callback.resolve : callback.reject

    let ret

    if (cb) {
      ret = cb(this.value) //计算继续向后传递的值
      // 返回一个promise
      if (ret instanceof MyPromise) {
        ret.then((data) => {
          callback.resolve(data)
        }, (err) => {
          callback.reject(err)
        })
      } else {
        callback.resolve(ret)
      }
    } else {
      cb_changeState(this.value) // then里没有回调函数,把当前value继续向下传递
    }
  }

}

demo

new MyPromise((resolve, reject) => {
  resolve('test')
}).then((value) => {
  console.log('then1', value)
  return new MyPromise((resolve, reject) => {
    reject(value)
  })
}, (err) => {
  console.log('reject1', err)
}).then((value) => {
  console.log('then2', value)
}, (err) => {
  console.log('reject2', err)
})

如下输出

then1 test
reject2 test

可以看到中途实现了状态的变更;

附录(完整代码)

// 先定义三个状态变量
const PENDING = 'pending'
const REJECTED = 'rejected'
const FULFILLED = 'fulfilled'

class MyPromise {

  state = PENDING
  value = ''      // 向后传的value值
  callbacks = []  // 回调队列

  constructor(fn) {
    if (typeof fn !== "function") {
      throw new Error('参数必须是函数')
    }
    fn(this._resolve.bind(this), this._reject.bind(this))
  }

  then(onFulfilled, onRejected) {
    const _this = this;   //_this指向前一个promise对象
    return new MyPromise((resolve, reject) => {
      _this._handle({
        onFulfilled,
        onRejected,
        resolve,
        reject
      })
    })
  }

  _resolve(value) {
    if (this.state !== PENDING) {
      return
    }
    this.state = FULFILLED  //修改状态
    this.value = value      // 赋值,用于向后传递
    this.callbacks.forEach(cb => this._handle(cb))  //回调then中的注册函数
  }

  _reject(error) {
    if (this.state !== PENDING) {
      return
    }
    this.state = REJECTED  //修改状态
    this.value = error      // 赋值,用于向后传递
    this.callbacks.forEach(cb => this._handle(cb))  //回调then中的注册函数
  }
  _handle(callback) {
    // prnding状态时,注册函数入栈
    if (this.state === PENDING) {
      this.callbacks.push(callback)
      return
    }
    // 按需获取then中注册的回调函数
    const cb = this.state === FULFILLED ? callback.onFulfilled : callback.onRejected

    // 改变then返回的promise的状态,持续回调后续的then
    const cb_changeState = this.state === FULFILLED ? callback.resolve : callback.reject

    let ret

    if (cb) {
      ret = cb(this.value) //计算继续向后传递的值
      // 返回一个promise
      if (ret instanceof MyPromise) {
        ret.then((data) => {
          callback.resolve(data)
        }, (err) => {
          callback.reject(err)
        })
      } else {
        callback.resolve(ret)
      }
    } else {
      cb_changeState(this.value) //then里没有回调函数,把当前value继续向下传递
    }
  }
}

new MyPromise((resolve, reject) => {
  resolve('test')
}).then((value) => {
  console.log('then1', value)
  return new MyPromise((resolve, reject) => {
    reject(value)
  })
}, (err) => {
  console.log('reject1', err)
}).then((value) => {
  console.log('then2', value)
}, (err) => {
  console.log('reject2', err)
})

输出结果如下

then1 test
reject2 test

可见第二个then的回调依赖第一个then的状态改变;

new关键字解读以及模拟实现

new关键字干了什么?

通常使用new关键字是为了创建一个自定义对象类型的实例。使用new的时候,会有以下几个过程:

1、创建一个空的简单JavaScript对象(即{});
2、链接该对象(即设置该对象的构造函数)到另一个对象 ;
3、将步骤1新创建的对象作为this的上下文 ;
4、如果该函数(2中设置的构造函数)没有返回对象,则返回this。
原文(中文):https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/new
英文(英文):https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new

以上仅仅是new的过程,如果要创建一个用户自定类型的对象,需要以下两部:

1、写一个函数定义对象类型;
2、通过new关键字来创建对象的实例;

有两点值得注意:
1、如果构造函数没有显示的返回一个对象,则返回this(创建的实例对象)。 如果显示返回一个对象,则返回该对象,且该对象不是构造函数的实例。 如果返回 null string number等,会直接忽略,继续返回this对象。测试一下:

  • 返回 null string number等被忽略的情况
function Demo1(a){
  this.a=a
  return 'test'
}
Demo1.prototype.getA=function(){
  return 'a is :'+this.a
}
const inst1=new Demo1(5)
inst1.getA()
  • 显示返回一个对象:
function Demo2(a){
  this.a=a
  return {
    b:3
  }
}
Demo2.prototype.getB=function(){
  return 'a is :'+this.b
}

const inst2=new Demo2(5)
inst2.a   //undefined
inst2.b   //2
inst2.getB()  // inst2.getB is not a function  显示返回的对象不是Demo2的实例,不能访问原型上的对象。

怎么模拟一个new ?

先实现一个简单的版本。

function _new(constructFunc,...rest){
  let obj={}
  constructFunc.apply(obj,rest)
  obj.__proto__=constructFunc.prototype
  return obj
}
function Test(a){
  this.a=a
}
Test.prototype.getA=function(){
  return this.a
}
const testInst=_new(Test,999)
console.log(testInst.getA())  // 999

注意:定义新对象的时候,以下两种方法是等价的:

let a={}
let a=Object.create({})

如果使用Object.create(null)创建对象,则会丢失原型,只能通过Object.setPrototypeOf()重新设置对象的原型。o.__proto__设置是无效的。

处理异常返回

这里要做一次判断,如果constructFunc函数返回一个对象,则直接返回该对象,否则就返回构造的实例对象。

function _new(constructFunc, ...rest) {
  let obj = {}
  obj.__proto__ = constructFunc.prototype
  let res=constructFunc.apply(obj, rest)
  return (res && typeof res === 'object') ? res : obj
}
function Test(a) {
  this.a = a
  return {b:a}
}
Test.prototype.getA = function () {
  return this.a
}
const testInst = _new(Test, 999)
testInst.getA()   // testInst.getA is not a function  不是实例

参考文章:mqyqingfeng/Blog#13

new.target

new.target是es6新增的指令, 返回new调用的函数的函数名。基于该指令可以辅助实现限制部分函数必须用new调用。比如定义了一个构造函数,必须先实例化才能继续使用。可以写以下函数来容错。

function Person(name){
  if(new.target!==Person){
    return  new Person(name)
  }
  this.name=name
}

let p1=new Person('A')   //Person {name: "A"}
let P2=Person('B')         // Person {name: "B"}

以上写法,不管有没有使用new来调用函数,都可以返回一个Person的实例。增加程序健壮性;

观察者模式&发布订阅模式

很多文章认为:观察者模式===发布订阅模式。 这个结论有待商榷。至少代码层面是不一样的。

观察者模式

每个实例都有发布任务(publish)和订阅任务(subscribe)的能力。且都各自维护自己的任务列表,列表中存储订阅自己的个体信息。

//假设有个学生群体,有A,B,C,D四个学生,A,B,C同时订阅D。D发布信息时,A,B,C给予响应。
class Student {
  constructor(name) {
    this.name = name;
    this.list = []
  }
  subscribe(target, callback) {
    target.list.push(callback)
  }

  publish(params) {
    this.list.forEach(func => func(params))
  }
}
const stuA = new Student("A")
const stuB = new Student("B")
const stuC = new Student("C")
const stuD = new Student("D")

//stuA,stuB,stuC 分别观察stuD。并传入相应的回调函数

stuA.subscribe(stuD, function (params) {
  console.log("A receive D:" + params)
})
stuB.subscribe(stuD, function (params) {
  console.log("B receive D:" + params)
})
stuC.subscribe(stuD, function (params) {
  console.log("C receive D:" + params)
})

stuD.publish('send a message')

发布订阅模式

有一个事件管理中心,订阅者把自己想订阅的事件注册在事件管理中心,发布者发布事件时,由管理中心统一触发订阅者注册的事件。

//事件管理中心
const union = {
  eventObj:{},
  subscribe: function (type, callback) {
    if (!this.eventObj[type]) {
      this.eventObj[type] = []
    }
    this.eventObj[type].push(callback)
  },
  publish: function (type, ...items) {
    const callbacks = this.eventObj[type]
    if (callbacks) {
      callbacks.forEach(callback => {
        callback.apply(null, items)
      });
    }
  }
}

class Student {
  constructor(name) {
    this.name = name;
  }
  subscribe(type, callback) {
    union.subscribe(type,callback)
  }

  publish(type,params) {
    union.publish(type,params)
  }
}

const stuA = new Student("A")
const stuB = new Student("B")
const stuC = new Student("C")
const stuD = new Student("D")

//stuA,stuB,stuC 分别订阅事件‘demo'。并传入相应的回调函数  stuD触发该事件

stuA.subscribe('demo', function (params) {
  console.log("A receive D:" + params)
})
stuB.subscribe('demo', function (params) {
  console.log("B receive D:" + params)
})
stuC.subscribe('demo', function (params) {
  console.log("C receive D:" + params)
})

stuD.publish('demo','simple demo')

参考文献

lazyMan

题目大概是实现一个如下功能的程序:

LazyMan('tom')
输出:
"this is tom"
LazyMan('tom').sleep(10).eat('apple')
输出:
"this is tom"
等待10秒...
"eat apple"
LazyMan('tom').eat('apple').eat('banana')
输出:
"this is tom"
"eat apple"
"eat banana"
LazyMan('tom').eat('banana').sleepFirst(5)
输出:
等待 5 秒...
"this is tom"
"eat banana"

分析

从输出结果看有几个特点:

  • 链式调用,可以一直调用下去。一般会有两种方式实现:队列&promise。
  • 任务有两种,一种输出顺序和调用位置一致,另一种是sleepFirst。后调用却先输出。所以这里更适合采用队列来实现,根据任务类型来判断是添加在队尾还是队首。
  • 基于以上分析可知,每次调用函数只是向队列添加任务而已。执行任务在下一个时机。 可以考虑使用setTimeout来实现。

代码实现

class _LazyMan {
  constructor(name) {
    this.name = name;
    this.tasks = [this.sayName];  //任务队列 默认添加sayName在队列中
    setTimeout(() => this.next(), 0)  // 任务添加完毕后,下一个宏任务中执行队列
  }

  /**
   * 添加任务
   * @param {function} task  任务
   * @param {boolean} addToTail 添加在队尾
   */
  addTask(task, addToTail = true) {
    if (addToTail) {
      this.tasks.push(task)
    } else {
      this.tasks.unshift(task)
    }
  }

  //每次去任务队列的第一个任务执行
  next = () => {
    const task = this.tasks.shift()
    task && task()
  }

  sayName = () => {
    console.log(`this is ${this.name}`)
    this.next()
  }
  eat = (name) => {
    this.addTask(() => {
      console.log(`eat ${name}`)
      this.next()
    })
    return this     // 必须return this 方便链式调用,继续向队列添加任务
  }

  sleep = (time) => {
    this.addTask(this.sleepTask(time))
    return this
  }

  sleepFirst = (time) => {
    this.addTask(this.sleepTask(time), false)
    return this
  }

  sleepTask = (time) => {
    return () => {
      console.log(`等待${time}秒 ...`)
      setTimeout((() => {
        this.next()
      }), time * 1000)
    }
  }
}

function LazyMan(name) {
  return new _LazyMan(name)
}

参考文档:
lazyMan

addRemote

题目内容

掘金看到一个比较有意思的题目(https://juejin.cn/post/6987529814324281380#heading-11) 。大致的意思就是,需要调用接口计算两个数相加。实现一个函数实现任意个数的数字和。

实现思路

  • 遍历,老老实实的计算。实现方法:
    • for遍历;
    • 递归;
    • reduce遍历;
  • 基于promise.all,串行遍历变为并行;
  • 做本地缓存,避免重复请求(连续执行时,后续可以秒返回)

源代码

// 第一种,普通遍历,串行执行,效率比较低
async function addFn1(args) {
  let res = 0
  for (const item of args) {
    res = await addRemote(res, item)
  }
  return res
}

// 递归实现, 串行执行, 和addFn1差距不大
async function addFn2(args = []) {
  if (args.length < 2) {
    return args[0] || 0
  }
  args.push(await addRemote(args.shift(), args.shift()))
  return addFn2(args)
}

// 基于reduce来实现
async function addFn3(args = []) {
  return args.reduce((calculator, current) => {
    return calculator.then(cal => addRemote(cal, current))
  }, Promise.resolve(0))
}

// 利用promiseAll 两两一组执行
async function addFn4(args = []) {
  let promiseChain = []
  if (args.length % 2) {
    promiseChain.push(Promise.resolve(args.shift()))
  }
  for (let i = 0; i < args.length / 2; i++) {
    promiseChain.push(addRemote(args[2 * i], args[(2 * i) + 1]))
  }
  return Promise.all(promiseChain).then(res => {
    if (res.length === 1) {
      return res[0]
    }
    return addFn4(res)
  })
}

let cache = {}
async function addFn5(args = []) {
  async function addWithCache(params) {
    let promiseChain = []
    if (params.length % 2) {
      promiseChain.push(Promise.resolve(params.shift()))
    }
    for (let i = 0; i < params.length / 2; i++) {
      let key1 = params[2 * i];
      let key2 = params[(2 * i) + 1];
      let cacheRes = cache[`${key1}${key2}`] || cache[`${key2}${key1}`]

      if (cacheRes) {
        promiseChain.push(cacheRes)
      } else {
        promiseChain.push(addRemote(key1, key2).then(res => {
          cache[`${key1}${key2}`] = res;
          return res
        }))
      }
    }
    return Promise.all(promiseChain).then(res => {
      if (res.length === 1) {
        return res[0]
      }
      return addWithCache(res)
    })
  }
  return addWithCache(args)
}

测试

const addRemote = async (a, b) => new Promise(resolve => {
  setTimeout(() => resolve(a + b), 500)
});
async function add(...inputs) {
  console.log('---------------------分割线---Fn1------------------------------')
  console.time('addFn1')
  console.log(await addFn1([...inputs]))
  console.timeEnd('addFn1')
  console.log('---------------------分割线---Fn2------------------------------')
  console.time('addFn2')
  console.log(await addFn2([...inputs]))
  console.timeEnd('addFn2')
  console.log('---------------------分割线---Fn3------------------------------')
  console.time('addFn3')
  console.log(await addFn3([...inputs]))
  console.timeEnd('addFn3')
  console.log('---------------------分割线---Fn4------------------------------')
  console.time('addFn4')
  console.log(await addFn4([...inputs]))
  console.timeEnd('addFn4')
  console.log('---------------------分割线---Fn5------------------------------')
  console.time('addFn5')
  console.log(await addFn5([...inputs]))
  console.timeEnd('addFn5')
  console.log('---------------------分割线---Fn5withcache---------------------')
  console.time('addFn5--again')
  console.log(await addFn5([...inputs]))
  console.timeEnd('addFn5--again')
}

add(1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 10, 10, 10, 10)

时间统计结果
image

从零开始实现promise(一)

概要

promise的使用现在已经非常普及了。 虽然方便了开发者,但是也带了一些八股文考题,常见的就是如何实现一个promise. 下边从零开始,一步步实现一个完整的promise

简易版分析

简易版的promise应该具有以下基础功能:

  • 新建promise;
  • promise状态正常流转;
  • then方法;

简单实现

状态管理

首先,一个promise具有三个状态pendingfulfilledrejected。且只能按照如下方式流转

  • pending ---> fulfilled
  • pending ---> rejected

状态一旦改变,就不再变了。

其次,创建Promise的时候,接受一个函数,且会立即执行,该函数接受两个函数参数,resolve,reject,用来改变状态;

基于此可以实现如下代码

// 先定义三个状态变量
const PENDING = 'pending'
const REJECTED = 'rejected'
const FULFILLED = 'fulfilled'

class MyPromise {
  state = PENDING
  value = ''   // 向后传的value值

  constructor(fn) {
    if (typeof fn !== "function") {
      throw new Error('参数必须是函数')
    }
    fn(this._resolve.bind(this), this._reject.bind(this))
  }


  _resolve(value) {
    this.state = FULFILLED  //修改状态
    this.value = value      // 赋值,用于向后传递
  }

  _reject(error) {
    this.state = REJECTED  //修改状态
    this.value = error      // 赋值,用于向后传递
  }
}

then

thenpromise的一个基本且强大方法,具有以下特点

  • 接受两个函数参数onFulfilled, onRejected;分别承接 resolvereject的回调;
  • then方法会返回一个新的promise,因此可以实现链式调用,一直then下去;

实现then的一个难点就是,每次then方法会注册两个函数参数onFulfilled, onRejected,但是具体执行哪一个,依赖前一个promise的状态,fulfilled状态时调用onFulfilledrejected状态时调用onRejected。 因此onFulfilled, onRejected必须注册在前一个promise的回调队列中,以便状态变更时,正确回调;

基于以上分析,完善代码如下

// 先定义三个状态变量
const PENDING = 'pending'
const REJECTED = 'rejected'
const FULFILLED = 'fulfilled'

class MyPromise {

  state = PENDING
  value = ''      // 向后传的value值
  callbacks = []  // 回调队列

  constructor(fn) {
    if (typeof fn !== "function") {
      throw new Error('参数必须是函数')
    }
    fn(this._resolve.bind(this), this._reject.bind(this))
  }

  then(onFulfilled, onRejected) {
    const _this = this;   // _this指向前一个promise对象,这个很关键
    return new MyPromise((resolve, reject) => {
      _this._handle({
        onFulfilled,
        onRejected,
        resolve,
        reject
      })
    })
  }

  _resolve(value) {
    if (this.state !== PENDING) {
      return
    }
    this.state = FULFILLED  // 修改状态
    this.value = value      // 赋值,用于向后传递
    this.callbacks.forEach(cb => this._handle(cb))  //回调then中的注册函数
  }

  _reject(error) {
    if (this.state !== PENDING) {
      return
    }
    this.state = REJECTED   // 修改状态
    this.value = error      // 赋值,用于向后传递
    this.callbacks.forEach(cb => this._handle(cb))  //回调then中的注册函数
  }
  _handle(callback) {
    // pending状态时,注册函数入栈
    if (this.state === PENDING) {
      this.callbacks.push(callback)
      return
    }
    // 按需获取then中注册的回调函数
    const cb = this.state === FULFILLED ? callback.onFulfilled : callback.onRejected

    // 改变then返回的promise的状态,持续回调后续的then
    const cb_changeState = this.state === FULFILLED ? callback.resolve : callback.reject

    let ret

    if (cb) {
      ret = cb(this.value)  // 计算继续向后传递的值
    }
    cb_changeState(ret)
  }
}

demo

new MyPromise((resolve, reject) => {
  resolve('test')
}).then((value) => {
  console.log('then1', value)
}, (err) => {
  console.log('reject1', err)
}).then((value) => {
  console.log('then2', value)
}, (err) => {
  console.log('reject2', err)
})

最终输出如下:

then1,test
then2,undefined

遗留问题

上述代码虽然实现了状态变化以及链式调用等基本能力,但是会存在一个很大的问题,第一个promise的状态变为fulfilled,那么会依次调用后续的onFulfilled函数。这是不符合预期的。

预期是当前then的回调函数,只取决于前一个promise的状态,即整个回调链路中可能同时存在onFulfilledonRejected

在下一篇文章中将专门解决这个问题。

参考文献

ts的一些题目

  1. 写一个函数,接受两个参数,一个为object,另一个为object中的一个key。函数返回类型指定为obj[key]的类型。
interface Person{
	name:string,
	age:number
}

// 函数不显示设置返回值的类型,利用动态推导得出
function demo<T extends object, K entends keyof T>(obj:T, key:K){
	return obj[k]
}

//测试
let obj:Person={
 	 name:"tea",
 	 age:23
}
let age = demo(obj, "age")   // number类型
let name = demo(obj, "name")   // string类型

interSectionObserver实践

简介

interSection Observer 提供了一种异步检测目标元素与指定祖先元素相交情况变化的方法;基于这个API,最常见的两个使用场景是:

  • 移动端无限滚动;
  • 图片懒加载;
    下边具体看看这两个场景的具体例子;

图片懒加载

传统的做法都是绑定scroller事件,滚动过程通过Element.getBoundingClientRect()计算是否出现在可视区,然后加载图片,然而这些都是在主线程频繁触发,会影响性能;

import React, { useState, useEffect, useRef } from 'react';
import { imgs } from './imgs.ts'  //这个文件会返回一个很长的图片列表

export default () => {

  useEffect(() => {
    const interSectionObserver = new IntersectionObserver(entries => {
      // 当目标元素与视窗(默认的祖先元素)交叉超过阈值(默认为0)时的回调,entries是所有观测的目标元素
      entries.forEach(item => {
        if (item.isIntersecting) {  // true表示超过交叉阈值,取dataset.src复制给src,实现图片加载
          const imgDom = item.target
          const imgSrc = imgDom.dataset.src
          imgDom.src = imgSrc
          interSectionObserver.unobserve(imgDom) // 图片加载完毕之后,取消观测该元素,否则来回滚动会继续触发;
        }
      })
    }, {
      rootMargin: "350px 0px"  // 这个会使得根元素的矩形大小上下各增加350px; 实现图片没有出现在屏幕可视区,但是开始加载;
    })

    Array.from(document.querySelectorAll('img')).forEach(dom => {
      interSectionObserver.observe(dom)  // 找到所有的img元素,并观测;
    })

  }, [])

  return (<div style={{ height: "600px", overflow: "scroll" }}>
    {
      imgs.map((item, index) => {
        return <div style={{ 'justifyContent': "center" }} key={item}>
          {/** 先把图片资源存储在data-src中 */}
          <img data-src={item} style={{ width: "300px", height: "165px", margin: "20px" }} />
          <span>{index}</span>
        </div>
      })
    }
  </div>)
}

无限滚动

这个场景在移动端会比较常见,滚定到底部实现自动分页,一般的做法也是绑定scoller事件,判断距离底部一定的值时发接口。问题和图片懒加载是一样的,利用IntersectionObserver就可以改善很多,在最底部放一个元素,观测该元素的交叉状况即可实现分页;

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

const bgColors = ['#4e61d4', '#ccc', '#999', 'pink', 'yellow']
const generateBlock = (counts: Number) => {
  return new Array(counts).fill(0).map(() => {
    const backgroundColor = bgColors[Math.floor(Math.random() * bgColors.length)]
    return <div style={{ height: "50px", margin: "20px 0", backgroundColor }} />
  })
}

export default function () {
  const [blocks, setBlocks] = useState(generateBlock(10));
  const footerRef = useRef('')

  useEffect(() => {
    const interSectionObserver = new IntersectionObserver((enteries) => {
      // 可见
      if (enteries[0].isIntersecting) {
        addblocks(10)  //增加10个背景色随机的块(模拟分页)
      }
    },{
      rootMargin:"50px 0px"
    });
    if (footerRef.current) {
      interSectionObserver.observe(footerRef.current)
    }
  }, [])

  const addblocks = (counts: Number) => {
    setBlocks((prevBlocks) => ([...prevBlocks, ...generateBlock(counts)]))
  }

  return (
    <>
      <div>无线向下滚动</div>
      {[...blocks]}
      <div ref={footerRef} style={{ height: "1px" }}>footer</div>
    </>
  );

}

shell相关

参数获取

  • bash中执行: sh online.sh test1 test2,则在online.sh文件中可以分别得到三个变量:
$0    //值为 online.sh
$1    //值为 test1
$2    //值为 test2

脚手架开发中遇到的坑

1、webpack5devServer其中后,打开的页面,控制台显示 socket链接失败, 大概率是端口问题,换一个端口就好了;
2、 利用mini-css-extract-plugin插件提取css后,修改js文件,页面更新,但是修改css文件,页面不更新。检查该插件的配置:

  new MiniCssExtractPlugin({
    filename: 'bundle.[contenthash].css'     //输出css文件名称
  })

开发环境下,不要带hash或者contenthash,就可以解决刷新问题。生产环境需要带,可以用来解决缓存问题。 webpack官网有这样一个一笔带过的解释:

使用 filename: "[contenthash].css" 启动长期缓存。根据需要添加 [name]。

操作开发机

  • 登录开发机:ssh username@ip

  • 查看用户:who am i

  • 创建用户:adduser

  • 删除用户:userdel name 只删除用户,不删除用户家目录, 如果要把家目录一起删了,需要执行:deluser -remove-home name

  • 切换用户:su username

  • 修改主机名称: sudo hostnamectl set-hostname newhostname

从零开始一个前端脚手架(二)

在上一节中,已经把基础环境搭建好了,这里就陆续实现各指令。首先我们从打包开始,毕竟能够打包编译js代码,这应该是一个脚手架工具最基础的功能。

webpack配置

webpack基础配置

新建文件src/webpack/config.com.js,该文件存放公用的webpack配置:

import jsLoader from '../babel/jsLoader'
import plugin from '../babel/plugin'
const path = require('path')
export default (props) => {
  const {
    entry = './index.js'
  } = props
  return {
    entry,
    output: {
      path: path.join(process.cwd(), 'dist'),  //输出路径
      filename: 'bundle.js'
    },
    resolve: {
      extensions: ['.js', '.jsx', '.ts', '.tsx', '.json', '.vue'],
    },
    module: {
      rules: [...jsLoader()]   // 解析文件的各种loader
    },
    plugins: [...plugin()]     // 配置一些常用的插件
  }
}

新建文件src/webpack/config.prod.js,存放打包时的配置信息

import commonConfig from './config.com.js'
export default (pkg) => {
  return {
    mode: 'production',            //设置开发模式
    ...commonConfig(pkg)          //各个模式下的公共配置
  }
}

配置loader

先配置babel-loader用以解析js代码
src/babel/jsLoader.js

export default () => {
  return [
    // babel-loader转义js  https://webpack.docschina.org/loaders/babel-loader/#root
    {
      test: /\.m?js$/,
      exclude: /node_modules/,     //不用编译 node_modules
      use: {
        loader: require.resolve('babel-loader'),
        options: {
          presets: [require.resolve('@babel/preset-env')]  
        }
      }
    }
  ]
}

配置plugin

plugin是一些辅助插件, 暂时以配置ProgressPlugin为例,其作用是实时输出打包信息:
src/babel/plugin.js

import ProgressPlugin from "progress-bar-webpack-plugin"
export default () => {
  return [new ProgressPlugin()]    // 输出打包进度信息
}

打包配置

src/commander/build.js

import Webpack from 'webpack'
import webpackConfig from '../webpack/config.prod.js'
export default pkg => {
  const compiler = Webpack(webpackConfig(pkg))
  compiler.run((err, status) => {
    if (err) {
      console.log('build err', err)
      process.exit(1)
    }
  })
}

入口文件配置

src/index.js

import build from './commanders/build'
...
// 自定义指令 build
program
    .command('build')
    .description('build program')
    .action(() => {
        build(config)
    })
...

新建文件:demo/index.js,在此目录下执行

simple-cli build

就可以看到输出打包进度信息,并输出结果demo/dist/bundle.js。此时我们最基础的打包指令已经做好了。

优化

交互优化

现有的webpack关键配置,比如entry都是在工具内部写死的,但是一个合理的交互需要提给供用户自定义的能力。我们约定这些配置在用户项目的package.json文件的simple-cli字段下。

cli/src/index.js

...
const cwd = process.cwd()
// 定义默认值
let config = {
    port: 8000,
    entry: './index.js',
    cwd: process.cwd()
}

const userPkgPath = `${cwd}/package.json`   //用户目录下package.json文件路径

// 判断是否存在 package.json
if (fs.existsSync(userPkgPath)) {
    let userConfig = require(userPkgPath).simple_cli || {}
    // 优先使用用户自定义的配置
    config = Object.assign(config, userConfig)
}
...
program
    .command('build')
    .description('build program')
    .action(() => {
        build(config)
    })

打包优化

在生产环境下,我们都是遵循增量部署的原则。这就意味着,每次打包输出的js文件名称都是唯一的,webpack支持带哈希值的输出文件来满足这一要求。

更新src/webpack/config.com.js

...
  output: {
    path: path.join(process.cwd(), 'dist'),   // 输出路径
    filename: 'bundle.[hash].js'
  }
...

此时你会发现每次打包都会生成一个名称类似bundle.1ff2020b12189c7388ec.js的文件。

但是,这样又引入了另一个问题,你的dist目录下存储的文件越来越多,对于本地开发这显然没必要,保留最新的一份就够了。那么有没有一个办法在写入文件之前,先清空dist目录呢。

webpack告诉你,‘有’ 。 借助插件clean-webpack-plugin即可。

更新src/babel/plugin.js

import ProgressPlugin from "progress-bar-webpack-plugin"
import { CleanWebpackPlugin } from 'clean-webpack-plugin'
export default () => {
  return [
    new CleanWebpackPlugin(),   // 清空output目录
    new ProgressPlugin()        // 输出打包进度信息
  ]
}

到这里一个简单的打包js指令已经完成了。其他指令的具体实现,努力更新中。

未完待续

源码地址

promise.all 实现

简介

promise的地位不言而喻,凡是需要异步处理的现在基本都在使用promise; promise的出现解决了异步处理中回调地狱的问题,使得开发者可以使用同步的方式去写异步逻辑。开发体验得到极大的提升;同时Promise还提供了很多有用的工具函数。 这里着重说一下Promise.all方法;

功能

Promise.all实现将多个Promise实例包装成一个的功能。

  • 接受一个数组作为参数,每一项为一个Promise实例,如果不是,内部会用Promise.resolve转化。
  • 参数中的每个Promisereslove时,Promise.allreslove。且返回的顺序和参数顺序一致(可以保证多个异步任务返回的顺序),其中一个失败时,整体失败,返回失败的那个实例数据。

简单实现

function PromiseAll(promises = []) {
  return new Promise((resolve, reject) => {
    if (!Array.isArray(promises)) {
      reject('PromiseAll 参数必须为数组')
    }
    let resloveNum = 0 //记录reslove的个数
    const promisesLength = promises.length
    const result = []
    promises.forEach((item, index) => {
      let actionItem = item
      // 如果不是promise实例,使用Promise.resolve转换成 Promise 实例
      if (!(item instanceof Promise)) {
        actionItem = Promise.resolve(item)
      }
      actionItem.then(res => {
        resloveNum++;
        result[index] = res  //确保返回顺序和参数顺序一致
        if (resloveNum === promisesLength) {
          resolve(result)
        }
      }).catch(res => {
        reject(res)
      })
    })
  })
}

测试

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(1)
  }, 1000)
})
const p2 = 2
const p3 = Promise.resolve(3)
const p4 = Promise.reject(4)
PromiseAll([p1, p2, p3]).then((res) => {
  console.log('resolve',res)   
}).catch(res => {
  console.log('catch', res)
})
// 1秒后输出 resolve [1,2,3]

PromiseAll([p2, p3, p4]).then((res) => {
  console.log('resolve', res)   
}).catch(res => {
  console.log('catch', res)
})
// 输出 catch 4

vi、vim&zsh中的指令&快捷键

vim指令

q : 退出;
wq:修改后保存退出;
q!:强制退出,不保存修改的内容;

vim快捷键

shift+g:定位到最后

zsh快捷键

ctrl+a:回到行手;
ctrl+e:回到行尾;

前期准备

源码地址:https://github.com/keep-run/simple-cli

第一步:建立目录

首先,在你的工作目录下建立一个目录simple-cli。该目录下有两个文件夹cli,demo

simple-cli
├── cli
└── demo

未来会在cli下写我们的脚手架工具,demo中写一些测试用例。

第二步:建立软链接

在cli下建立一个index.js文件,文件中写入

#!/usr/bin/env node   // 指定执行环境是node。 
console.log('test')

执行pwd。得到该文件的绝对路径:

/Users/XXXX/Desktop/study/simple-cli/cli   //xxxx是你的用户名

现在思考几个问题:

  • 怎么才能执行这个文件?
  • 当我们本地安装了create-react-app后,为什么就能在工作目录下执行 create-react-app my-app?

这里就出现了一个叫软链接的东西。在create-react-app源码的package.json中有这么一段:

  "bin": {
    "create-react-app": "./index.js"
  },

这一段的意思就是,全局安装create-react-app后,会建立一个软链接到项目的.index.js。所以执行任何create-react-app的指令,都会走到这个index.js

本地看一下,打开你的命令行工具,输入:which create-react-app, 会返回:/usr/local/bin/create-react-app。现在我们执行cd /usr/local/bin/,接着执行ll。会看到其中一行为:

lrwxr-xr-x  1 root         wheel    45B  9 27  2018 create-react-app -> ../lib/node_modules/create-react-app/index.js

可以看到,create-react-app指向了一个js文件。

现在在该目录下执行指令建立链接:

ln -s /Users/XXXX/Desktop/study/simple-cli/cli/index.js  simple-cli

返回到你的工作目录,执行 simple-cli。如果不出意外的话会得到如下异常:

permission denied: simple-cli

说明权限不够,接下来修改index.js文件的权限,使之可以执行:

chmod 777  index.js

再次执行 simple-cli。就会输出:test

至此,前期准备工作就完成了。接下来就可以在index.js文件中开发脚手架的具体功能了。

koa2参数解析

koa是基于node.js的web开发框架,官网:https://koa.bootcss.com/ 。不做详细介绍;在node端与前端交互时,不可避免的就是接口请求。在获取请求参数的时候,koa是基于一些中间件来处理的,而且这些中间件很多,至少有koa-body,koa-bodyparser,koa-better-body,koa-multer等(这么多真让人头大)。这里以koa-body为例介绍一些常用的接口参数获取方法;

get请求

// server 端 demo.js
const Koa = require('koa')
const route = require('koa-route')
const koaBody = require('koa-body')({
    multipart: true
})
const app = new Koa()
app.use(koaBody)
const koaBodyTest = ctx => {
    const query=ctx.request.query
    const querystring=ctx.request.querystring
    ctx.response.body = {
        query, querystring
    }
}
app.use(route.get('/test/get', koaBodyTest))
app.listen(3001, () => {
    console.log('listen 3001')
})
// client端 demo.js
const axios =require('axios')
axios({
    url:'http://0.0.0.0:3001/test/get',
    params:{
        test:123
    }
}).then(res=>{
    console.log('response---->',res.data)
})

分别进入个字文件目录执行 node demo.js,可以看到client端的接口返回为:

response----> { query: { test: '123' }, querystring: 'test=123' }

post请求

koa自身没有封装关于post参数解析的方法,所以出现两种办法解析post参数。原生实现&中间件

原生方法

//server端 demo.js
const Koa = require('koa')
const route = require('koa-route')
const app = new Koa()
const handlePost = async (ctx) => {
    let data = '';
    await new Promise(reslove => {
        ctx.req.addListener('data', (chunk) => {
            data += chunk
        })
        ctx.req.addListener('end', (chunk) => {
            reslove()
        })
    })
    ctx.body = data
}
app.use(route.post('/test/post', handlePost))
app.listen(3001, () => {
    console.log('listen 3001')
})
// client端 demo.js

const axios = require('axios')
axios({
    url: 'http://0.0.0.0:3001/test/post',
    data: {
        test: 123
    },
    method: 'post'
}).then(res => {
    console.log('response---->', res.data)
})

分别执行各自的demo.js。client端的返回为:
response----> { test: 123 }

使用koa-body

// server端 demo.js
const Koa = require('koa')
const route = require('koa-route')
const koaBody = require('koa-body')({
    multipart: true
})
const app = new Koa()
app.use(koaBody)

const handlePost = async (ctx) => {
    ctx.response.body = ctx.request.body  
}
app.use(route.post('/test/post', handlePost))
app.listen(3001, () => {
    console.log('listen 3001')
})
const axios = require('axios')
axios({
    url: 'http://0.0.0.0:3001/test/post',
    data: {
        test: 123
    },
    method: 'post'
}).then(res => {
    console.log('response---->', res.data)
})

client返回代码:response----> { test: 123 }

formdata数据

// server端 demo.js
const Koa = require('koa')
const route = require('koa-route')
const koaBody = require('koa-body')({
    multipart: true
})
const app = new Koa()
app.use(koaBody)
const handlePost = async (ctx) => {
    ctx.response.body = ctx.request.body
}
app.use(route.post('/test/post', handlePost))
app.listen(3001, () => {
    console.log('listen 3001')
})
// client端 demo.js
const axios =require('axios')
const FormData = require('form-data'); 
const form = new FormData();
form.append('file', 'teset');
axios.post('http://0.0.0.0:3001/test/post', form).then(result => {
  console.log('response---->',result.data);
});

此时,client端的返回如下:response----> { '----------------------------511028953551316351151647\r\nContent-Disposition: form-data; name': '"file"\r\n\r\nteset\r\n----------------------------511028953551316351151647--\r\n' }

可以看到,koa没能解析formdata的数据,原因在于client发送请求的时候,没指定content-type。默认为:application/x-www-form-urlencoded。这个头,koa-body不能解析。而formdata的强大之处在于,可以执行form.getHeaders(),为请求添加合适的头。修改client端代码如下:

const axios =require('axios')
const FormData = require('form-data'); 
const form = new FormData();
form.append('file', 'teset');
axios.post('http://0.0.0.0:3001/test/post', form,{
  headers:form.getHeaders()
}).then(result => {
  console.log('response---->',result.data);
});

重新执行 client端代码,输出如下:response----> { file: 'teset' }

函数柯里化

'函数柯里化可以理解为分步接受函数的参数,等到接受完毕的时候,最终执行'。下边以实现累加为例,具体剖析:

简易版

function curry(fn){
  let args=[]
  return function next(...params){
     if(params.length>0){          // 还有参数,则接着返回函数
       args=[...args,...params];
       return next
     }else{        //没有参数,表明接受完所有的参数了
       return fn(...args)
     }
  }
}

let add=curry(function(...items){
  return items.reduce((prev,cur)=>{return prev+cur},0)
})

console.log(add(1)(2,3)())   //6

升级版

在简易版中,判断最终执行函数的条件是传入了空参数。有没有办法省略这一步呢。其实是有以下两个办法:

  • js获取某个值的时候,会根据语境隐式调用toString或者valueOf函数。
  • 判断函数的参数长度;

toString或者valueOf方法

function curry(fn) {
  let args = []
  function next(...params) {
    args = [...args, ...params]
    return next
  }
  next.toString = function () {
    return fn(...args)
  }
  next.valueOf=function(){
    return fn(...args)
  }
  return next
}

let add = curry(function (...items) {
  return items.reduce((prev, cur) => { return prev + cur }, 0)
})

//case1
let res = add(1)(2, 3)
console.log(res)  //{ [Function: next] toString: [Function], valueOf: [Function] } //没有求值的操作,所以打印了函数。
// case2
let res = add(1)(2, 3)+0     //求值的操作,会调用`valueOf`。
console.log(res)  //6     

判断函数的参数长度

这种方法适用于预先知道需要柯里化的函数的参数个数。

function curry(fn) {
  let allArgs = []
  let len = fn.length
  return function next(...args) {
    allArgs = [...allArgs, ...args]
    if (allArgs.length === len){
      return fn(...allArgs)
    }else{
      return next
    }
  }
}
let add = curry(function (p1, p2, p3) {
  return p1 + p2 + p3
})
console.log('res',add(1)(2,3))   //6

或者curry函数接受第二个参数,显示指定参数的个数。

function curry(fn,len) {
  let allArgs = []
  return function next(...args) {
    allArgs = [...allArgs, ...args]
    if (allArgs.length === len){
      return fn(...allArgs)
    }else{
      return next
    }
  }
}
let add = curry(function (...items) {
  return items.reduce((prev, cur) => { return prev + cur }, 0)
},3)

console.log('res',add(1)(2,3)) //6

补充,函数的length

上面的例子用到了函数的length属性,length表示函数预期输入的参数个数,那么给了参数默认值的,或者rest参数,都不在length中。且给了默认值后面的没有默认值的参数,也不再length中。比如:

(function(a,b,c){}).length                //3   正常使用
(function(a,b,...args){}).length       //2    rest参数不计入
(function(a,b,c=1){}).length           //2    默认值的参数不计入
(function(a,b=2,c){}).length          //1     默认值之后的参数也不计入

参考文献:
1、https://juejin.im/post/5b561426518825195f499772
2、https://es6.ruanyifeng.com/#docs/function#rest-%E5%8F%82%E6%95%B0

常用的git操作

git 撤销合并

  • 执行 git log ,找到合并操作的 commit值;
  • 执行git revert -m 1 <commit>;传入参数1,会保留当前分支merge前的状态,传入参数2会保留被合并的分支之前的状态;详细解释

获取git仓库的某一个文件的指令:

  • git archive --remote=xxxxx.git HEAD README.md ,读取仓库中的readme.md
  • curl --request GET --header 'PRIVATE-TOKEN:*****' 'http://git.xxxx.com/plat-fe/fe-store-finance/raw/master/package.json?ref=master'

刷新远程分支列表到本地:

cookie&session

概述

首先http协议是一个无状态的协议。那么对一个服务来说,怎么区分是A访问的还是B访问的,这很重要,与之相关的就是登陆各个网站的登陆系统; 采用的方案大多是cookie和session;

cookie-session原理

cookie

cookie由http响应时服务写入的。下一次请求就会自动携带相应域名下的cookie,作为http请求的一部分发送给服务。这样客户端就可以在cookie中携带自己的身份证明发送给服务;

session

首先session是放在服务端的,可以在内存中、数据库甚至文件中。说白了就是一个对象,客户端首次访问时,会生成一个session_id(根据服务类型,key默认名称不一样,常见的有jsessionId,connect_id等)。session_id对应一个对象,存储一些信息,比如用户信息等。同时会保存在内存以及存储设备上,然后把这个id作为cookie的一部分返回给客户端, 客户端下次访问时,服务从cookie中读取以上id。并在内存或者存储设备中找,如果找到则认为是同一用户。以此实现不同访问之间的身份识别。

举例

koakoa-session为例子来说明以上过程。koa-session中可以设置store,并提供get,set,destroy即可。

const session = require('koa-session');
const Koa = require('koa');
const app = new Koa();
const path = require('path');
const fs = require('fs');

app.keys = ['some secret hurr'];

const store = {
  get(key) {   /** 这个key就是cookie中的id, 对应CONFIG.key的值 */
    const sessionDir = path.resolve(__dirname, './session');
    const files = fs.readdirSync(sessionDir);

    /** 读取session时,在session目录下找相应的文件,每个文件都为一个session的信息 */
    for (let i = 0; i < files.length; i++) {
      if (files[i].startsWith(key)) {
        const filepath = path.resolve(sessionDir, files[i]);
        delete require.cache[require.resolve(filepath)];
        const result = require(filepath);
        return result;
      }
    }
  },
  set(key, session) {
    /** 设置session时,在session目录下建一个文件,文件名为session_id。值为对应的其他信息*/
    const filePath = path.resolve(__dirname, './session', `${key}.js`);
    const content = `module.exports = ${JSON.stringify(session)};`;
    
    fs.writeFileSync(filePath, content);
  },

  destroy(key){
    const filePath = path.resolve(__dirname, './session', `${key}.js`);
    fs.unlinkSync(filePath);
  }
}


const CONFIG = {
  key: 'koa:sess', /** cookie的key 默认 'koa:sess'*/
  maxAge: 86400000,  /** 有效时间,默认一天 */
  overwrite: true, /** (boolean) can overwrite or not (default true) */
  httpOnly: true, /** (boolean) httpOnly or not (default true) */
  signed: true, /** (boolean) signed or not (default true) */
  rolling: false, /** (boolean) Force a session identifier cookie to be set on every response. The expiration is reset to the original maxAge, resetting the expiration countdown. default is false **/
  store  /** 自定义的store */
};

app.use(session(CONFIG, app));
// or if you prefer all default config, just use => app.use(session(app));

app.use(ctx => {
  // ignore favicon
  if (ctx.path === '/favicon.ico') return;

  let n = ctx.session.views || 0;
  ctx.session.views = ++n;
  ctx.body = n + ' views';
});

app.listen(3000);
console.log('listening on port 3000');

以上程序实现的是同一个session每次请求,views加1的功能;

客户端第一次访问,session目录下会建立如下文件
ded82c73-e772-449d-acf4-c1a298eefd42.js 文件内容
module.exports = {"views":1,"_expire":1609926748437,"_maxAge":86400000};

同一个浏览器再次访问,文件内容中的views每次增加1。
同时 客户端浏览器会存储一个cookie。 key为koa:sess, value为: ded82c73-e772-449d-acf4-c1a298eefd42

清除cookie再次访问,返回1views. 服务端session目录下会新建另一个相同形式的js文件;

参考文档

数据库相关指令

  • 登录本地数据库
mysql -u [username] -p     //回车输入密码即可
  • 显示数据库
show databases;    //记得有‘;’,否则语句没有结束
  • 查看数据库端口
show global variables like 'port'     //默认3306

Linux指令

很多前端对于Linux指令都不熟,当需要操作开发机、堡垒机的时候就束手无策。这里总结一下基本指令(持续增加中。。。)

基本指令

  • 切换目录:cd
  • 查看当前路径:pwd
  • 查看当前文件下所有的文件:ls,可以增加一些参数:
    • ls -l 等价于ll:查看当前文件夹下的所有文件,并显示文件属性;
    • ls -a:查看当前文件夹下的隐藏文件;l's
  • 查看文件行数:cat filename|wc -l
  • 查看文件字符数:cat filrname|wc -c
  • 查看文件大小:du -sh filename ,-sh会自动换算到合适的单位

文件管理

  • 新建文件:touch filename
  • 删除文件:rm -rf filename
  • 编辑文件:vi filename
  • 查看文件内容:cat filename
  • 查看文件权限:ls -l dirname

目录管理

  • 创建目录(文件夹): mkdir foldername
  • 删除目录:rm -rf foldername。(删除真个文件夹)

文件搜索

  • root目录下搜索 file1: find /root file1
  • root目录下搜索 file1并删除: find /root file1 -exec rm -rf {}
  • 查找目录: which dirname

内容搜索

  • grep
    • cat filename|grep linux 查找filename中包含linux的行;
    • cat filename|grep -E “linux|php” 查找filename中包含linux或者php的行

查看进程

  • ps : Process Status的缩写, 该指令可以列出系统中当前运行的那些进程。该指令可以指定以下参数:
    • -a:显示统一终端下的所有程序
    • -H:显示树状结构;
    • -aux: 显示所有包含其他使用者的进程;
    • 还有好多参数,待列举。

以上参数可以联合使用, 比如ps -auxH

另外linux指令管道化** ps -aux | grep cust 查找进程中包含关键字"cust"的进程。

建立链接(ln)

  • 软链接: ln -s 源文件 目标文件
  • 硬链接: ln 源文件 目标文件

两者的区别:软链接会在指定位置生成一个源文件的镜像,不会占用磁盘空间,但是硬链接会在指定位置生成一个和源文件相同的文件。

用户相关

  • 查看用户:cat /etc/passwd
  • 切换到用户:su name (若要切到root,则需要 sudo su name。 否则可能出现输入密码后,返回鉴定故障)

文件权限

chmod , 详情

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.