前端森林

前端森林 查看完整档案

上海编辑  |  填写毕业院校  |  填写所在公司/组织 github.com/Cosen95 编辑
编辑

个人动态

前端森林 发布了文章 · 1月21日

优雅的在vue中使用TypeScript

引言

近几年前端对 TypeScript 的呼声越来越高,Typescript 也成为了前端必备的技能。TypeScript 是 JS 类型的超集,并支持了泛型、类型、命名空间、枚举等特性,弥补了 JS 在大型应用开发中的不足。

在单独学习 TypeScript 时,你会感觉很多概念还是比较好理解的,但是和一些框架结合使用的话坑还是比较多的,例如使用 React、Vue 这些框架的时候与 TypeScript 的结合会成为一大障碍,需要去查看框架提供的.d.ts 的声明文件中一些复杂类型的定义、组件的书写方式等都要做出不小的调整。

本篇文章主要是结合我的经验和大家聊一下如何在Vue中平滑的从js过渡到ts,阅读本文建议对 TypeScript 有一定了解,因为文中对于一些 TypeScript 的基础的知识不会有太过于详细的讲解。(具体可以参考官方文档https://www.w3cschool.cn/typescript/typescript-tutorial.html,官方文档就是最好的入门手册)

构建

通过官方脚手架构建安装

# 1. 如果没有安装 Vue CLI 就先安装
npm install --global @vue/cli

最新的Vue CLI工具允许开发者 使用 TypeScript 集成环境 创建新项目。

只需运行vue create my-app

然后,命令行会要求选择预设。使用箭头键选择 Manually select features。

接下来,只需确保选择了 TypeScript 和 Babel 选项,如下图:

然后配置其余设置,如下图:


设置完成 vue cli 就会开始安装依赖并设置项目。

目录解析

安装完成打开项目,你会发现集成 ts 后的项目目录结构是这样子的:

|-- ts-vue
    |-- .browserslistrc     # browserslistrc 配置文件 (用于支持 Autoprefixer)
    |-- .eslintrc.js        # eslint 配置
    |-- .gitignore
    |-- babel.config.js     # babel-loader 配置
    |-- package-lock.json
    |-- package.json        # package.json 依赖
    |-- postcss.config.js   # postcss 配置
    |-- README.md
    |-- tsconfig.json       # typescript 配置
    |-- vue.config.js       # vue-cli 配置
    |-- public              # 静态资源 (会被直接复制)
    |   |-- favicon.ico     # favicon图标
    |   |-- index.html      # html模板
    |-- src
    |   |-- App.vue         # 入口页面
    |   |-- main.ts         # 入口文件 加载组件 初始化等
    |   |-- shims-tsx.d.ts
    |   |-- shims-vue.d.ts
    |   |-- assets          # 主题 字体等静态资源 (由 webpack 处理加载)
    |   |-- components      # 全局组件
    |   |-- router          # 路由
    |   |-- store           # 全局 vuex store
    |   |-- styles          # 全局样式
    |   |-- views           # 所有页面
    |-- tests               # 测试

其实大致看下来,与之前用js构建的项目目录没有什么太大的不同,区别主要是之前 js 后缀的现在改为了 ts 后缀,还多了tsconfig.jsonshims-tsx.d.tsshims-vue.d.ts这几个文件,那这几个文件是干嘛的呢:

  • tsconfig.json: typescript 配置文件,主要用于指定待编译的文件和定义编译选项
  • shims-tsx.d.ts: 允许.tsx 结尾的文件,在 Vue 项目中编写 jsx 代码
  • shims-vue.d.ts: 主要用于 TypeScript 识别.vue 文件,Ts 默认并不支持导入 vue 文件

使用

开始前我们先来了解一下在 vue 中使用 typescript 非常好用的几个库

  • vue-class-component: vue-class-component是一个 Class Decorator,也就是类的装饰器
  • vue-property-decorator: vue-property-decorator是基于 vue 组织里 vue-class-component 所做的拓展

    import { Vue, Component, Inject, Provide, Prop, Model, Watch, Emit, Mixins } from 'vue-property-decorator'
  • vuex-module-decorators: 用 typescript 写 vuex 很好用的一个库

    import { Module, VuexModule, Mutation, Action, MutationAction, getModule } from 'vuex-module-decorators'

组件声明

创建组件的方式变成如下

import { Component, Prop, Vue, Watch } from "vue-property-decorator";

@Component
export default class Test extends Vue {}

data 对象

import { Component, Prop, Vue, Watch } from 'vue-property-decorator';

@Component
export default class Test extends Vue {
  private name: string;
}

Prop 声明

@Prop({ default: false }) private isCollapse!: boolean;
@Prop({ default: true }) private isFirstLevel!: boolean;
@Prop({ default: "" }) private basePath!: string;
  • !: 表示一定存在,?: 表示可能不存在。这两种在语法上叫赋值断言
  • @Prop(options: (PropOptions | Constructor[] | Constructor) = {})

    • PropOptions,可以使用以下选项:type,default,required,validator
    • Constructor[],指定 prop 的可选类型
    • Constructor,例如 String,Number,Boolean 等,指定 prop 的类型

method

js 下是需要在 method 对象中声明方法,现变成如下

public clickFunc(): void {
  console.log(this.name)
  console.log(this.msg)
}

Watch 监听属性

@Watch("$route", { immediate: true })
private onRouteChange(route: Route) {
  const query = route.query as Dictionary<string>;
  if (query) {
  this.redirect = query.redirect;
  this.otherQuery = this.getOtherQuery(query);
  }
}
  • @Watch(path: string, options: WatchOptions = {})

    • options 包含两个属性 immediate?:boolean 侦听开始之后是否立即调用该回调函数 / deep?:boolean 被侦听的对象的属性被改变时,是否调用该回调函数
  • @Watch('arr', { immediate: true, deep: true })
    onArrChanged(newValue: number[], oldValue: number[]) {}

computed 计算属性

public get allname() {
  return 'computed ' + this.name;
}

allname 是计算后的值,name 是被监听的值

生命周期函数

public created(): void {
  console.log('created');
}

public mounted():void{
  console.log('mounted')
}

emit 事件

import { Vue, Component, Emit } from "vue-property-decorator";
@Component
export default class MyComponent extends Vue {
  count = 0;
  @Emit()
  addToCount(n: number) {
    this.count += n;
  }
  @Emit("reset")
  resetCount() {
    this.count = 0;
  }
  @Emit()
  returnValue() {
    return 10;
  }
  @Emit()
  onInputChange(e) {
    return e.target.value;
  }
  @Emit()
  promise() {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(20);
      }, 0);
    });
  }
}

使用 js 写法

export default {
  data() {
    return {
      count: 0,
    };
  },
  methods: {
    addToCount(n) {
      this.count += n;
      this.$emit("add-to-count", n);
    },
    resetCount() {
      this.count = 0;
      this.$emit("reset");
    },
    returnValue() {
      this.$emit("return-value", 10);
    },
    onInputChange(e) {
      this.$emit("on-input-change", e.target.value, e);
    },
    promise() {
      const promise = new Promise((resolve) => {
        setTimeout(() => {
          resolve(20);
        }, 0);
      });
      promise.then((value) => {
        this.$emit("promise", value);
      });
    },
  },
};
  • @Emit(event?: string)
  • @Emit 装饰器接收一个可选参数,该参数是&dollar;Emit 的第一个参数,充当事件名。如果没有提供这个参数,&dollar;Emit 会将回调函数名的 camelCase 转为 kebab-case,并将其作为事件名
  • @Emit 会将回调函数的返回值作为第二个参数,如果返回值是一个 Promise 对象,&dollar;emit 会在 Promise 对象被标记为 resolved 之后触发
  • @Emit 的回调函数的参数,会放在其返回值之后,一起被&dollar;emit 当做参数使用

vuex

在使用 store 装饰器之前,先过一下传统的 store 用法吧

export default {
  namespaced: true,
  state: {
    foo: "",
  },
  getters: {
    getFoo(state) {
      return state.foo;
    },
  },
  mutations: {
    setFooSync(state, payload) {
      state.foo = payload;
    },
  },
  actions: {
    setFoo({ commit }, payload) {
      commot("getFoo", payload);
    },
  },
};

然后开始使用vuex-module-decorators

import {
  VuexModule,
  Mutation,
  Action,
  getModule,
  Module,
} from "vuex-module-decorators";
  • VuexModule 用于基本属性

    export default class TestModule extends VuexModule {}

    VuexModule 提供了一些基本属性,包括 namespaced,state,getters,modules,mutations,actions,context

  • @Module 标记当前为 module

    @Module({ dynamic: true, store, name: "settings" })
    class Settings extends VuexModule implements ISettingsState {}

    module 本身有几种可以配置的属性:

    • namespaced:boolean 启/停用 分模块
    • stateFactory:boolean 状态工厂
    • dynamic:boolean 在 store 创建之后,再添加到 store 中。开启 dynamic 之后必须提供下面的属性
    • name:string 指定模块名称
    • store:Vuex.Store 实体 提供初始的 store
  • @Mutation 标注为 mutation

    @Mutation
    private SET_NAME(name: string) {
    // 设置用户名
    this.name = name;
    }
  • @Action 标注为 action

    @Action
    public async Login(userInfo: { username: string; password: string }) {
      // 登录接口,拿到token
      let { username, password } = userInfo;
      username = username.trim();
      const { data } = await login({ username, password });
      setToken(data.accessToken);
      this.SET_TOKEN(data.accessToken);
    }
  • getModule 得到一个类型安全的 store,module 必须提供 name 属性

    export const UserModule = getModule(User);

示例

我之前基于 ts+vue+element 构建了一个简单的中后台通用模板。


涵盖的功能如下:

- 登录 / 注销

- 权限验证
  - 页面权限
  - 权限配置

- 多环境发布
  - Dev / Stage / Prod

- 全局功能
  - 动态换肤
  - 动态侧边栏(支持多级路由嵌套)
  - Svg 图标
  - 全屏
  - 设置
  - Mock 数据 / Mock 服务器

- 组件
  - ECharts 图表

- 表格
  - 复杂表格

- 控制台
- 引导页
- 错误页面
  - 404

里面对于在 vue 中使用 typescript 的各种场景都有很好的实践,大家感兴趣的可以参考一下,https://github.com/easy-wheel/ts-vue,当然不要吝惜你的 star!!!

最后

你可以关注我的同名公众号【前端森林】,这里我会定期发一些大前端相关的前沿文章和日常开发过程中的实战总结。当然,我也是开源社区的积极贡献者,github 地址https://github.com/Cosen95,欢迎 star!!!

image

查看原文

赞 11 收藏 9 评论 0

前端森林 发布了文章 · 1月21日

useTypescript-React Hooks和TypeScript完全指南

引言

React v16.8 引入了 Hooks,它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。这些功能可以在应用程序中的各个组件之间使用,从而易于共享逻辑。Hook 令人兴奋并迅速被采用,React 团队甚至想象它们最终将替换类组件。

以前在 React 中,共享逻辑的方法是通过高阶组件和 props 渲染。Hooks 提供了一种更简单方便的方法来重用代码并使组件可塑形更强。

本文将展示 TypeScript 与 React 集成后的一些变化,以及如何将类型添加到 Hooks 以及你的自定义 Hooks 上。

引入 Typescript 后的变化

有状态组件(ClassComponent)

API 对应为:

React.Component<P, S>

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

以下是官网的一个例子,创建 Props 和 State 接口,Props 接口接受 name 和 enthusiasmLevel 参数,State 接口接受 currentEnthusiasm 参数:

import * as React from "react";

export interface Props {
  name: string;
  enthusiasmLevel?: number;
}

interface State {
  currentEnthusiasm: number;
}

class Hello extends React.Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { currentEnthusiasm: props.enthusiasmLevel || 1 };
  }

  onIncrement = () => this.updateEnthusiasm(this.state.currentEnthusiasm + 1);
  onDecrement = () => this.updateEnthusiasm(this.state.currentEnthusiasm - 1);

  render() {
    const { name } = this.props;

    if (this.state.currentEnthusiasm <= 0) {
      throw new Error('You could be a little more enthusiastic. :D');
    }

    return (
      <div className="hello">
        <div className="greeting">
          Hello {name + getExclamationMarks(this.state.currentEnthusiasm)}
        </div>
        <button onClick={this.onDecrement}>-</button>
        <button onClick={this.onIncrement}>+</button>
      </div>
    );
  }

  updateEnthusiasm(currentEnthusiasm: number) {
    this.setState({ currentEnthusiasm });
  }
}

export default Hello;

function getExclamationMarks(numChars: number) {
  return Array(numChars + 1).join('!');
}

TypeScript 可以对 JSX 进行解析,充分利用其本身的静态检查功能,使用泛型进行 Props、 State 的类型定义。定义后在使用 this.state 和 this.props 时可以在编辑器中获得更好的智能提示,并且会对类型进行检查。

react 规定不能通过 this.props.xxx 和 this.state.xxx 直接进行修改,所以可以通过 readonly 将 State 和 Props 标记为不可变数据:

interface Props {
  readonly number: number;
}

interface State {
  readonly color: string;
}

export class Hello extends React.Component<Props, State> {
  someMethod() {
    this.props.number = 123; // Error: props 是不可变的
    this.state.color = 'red'; // Error: 你应该使用 this.setState()
  }
}

无状态组件(StatelessComponent)

API 对应为:

// SFC: stateless function components
const List: React.SFC<IProps> = props => null
// v16.8起,由于hooks的加入,函数式组件也可以使用state,所以这个命名不准确。新的react声明文件里,也定义了React.FC类型^_^
React.FunctionComponent<P> or React.FC<P>。

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

无状态组件也称为傻瓜组件,如果一个组件内部没有自身的 state,那么组件就可以称为无状态组件。在@types/react已经定义了一个类型type SFC<P = {}> = StatelessComponent

先看一下之前无状态组件的写法:

import React from 'react'

const Button = ({ onClick: handleClick, children }) => (
  <button onClick={handleClick}>{children}</button>
)

如果采用 ts 来编写出来的无状态组件是这样的:

import React, { MouseEvent, SFC } from 'react';

type Props = { onClick(e: MouseEvent<HTMLElement>): void };

const Button: SFC<Props> = ({ onClick: handleClick, children }) => (
  <button onClick={handleClick}>{children}</button>
);

事件处理

我们在进行事件注册时经常会在事件处理函数中使用 event 事件对象,例如当使用鼠标事件时我们会通过 clientX、clientY 去获取指针的坐标。

大家可以想到直接把 event 设置为 any 类型,但是这样就失去了我们对代码进行静态检查的意义。

function handleMouseChange (event: any) {
  console.log(event.clientY)
}

试想下当我们注册一个 Touch 事件,然后错误的通过事件处理函数中的 event 对象去获取其 clientY 属性的值,在这里我们已经将 event 设置为 any 类型,导致 TypeScript 在编译时并不会提示我们错误, 当我们通过 event.clientY 访问时就有问题了,因为 Touch 事件的 event 对象并没有 clientY 这个属性。

通过 interface 对 event 对象进行类型声明编写的话又十分浪费时间,幸运的是 React 的声明文件提供了 Event 对象的类型声明。

  • 通用的 React Event Handler

API 对应为:

React.ReactEventHandler<HTMLElement>

简单的示例:

const handleChange: React.ReactEventHandler<HTMLInputElement> = (ev) => { ... }

<input onChange={handleChange} ... />
  • 特殊的 React Event Handler

常用 Event 事件对象类型:

ClipboardEvent<T = Element> 剪贴板事件对象


DragEvent<T = Element> 拖拽事件对象


ChangeEvent<T = Element>  Change 事件对象


KeyboardEvent<T = Element> 键盘事件对象


MouseEvent<T = Element> 鼠标事件对象


TouchEvent<T = Element>  触摸事件对象


WheelEvent<T = Element> 滚轮事件对象


AnimationEvent<T = Element> 动画事件对象


TransitionEvent<T = Element> 过渡事件对象

简单的示例:

const handleChange = (ev: React.MouseEvent<HTMLDivElement>) => { ... }

<div onMouseMove={handleChange} ... />

React 元素

API 对应为:

React.ReactElement<P> or JSX.Element

简单的示例:

// 表示React元素概念的类型: DOM元素组件或用户定义的复合组件
const elementOnly: React.ReactElement = <div /> || <MyComponent />;

React Node

API 对应为:

React.ReactNode

表示任何类型的 React 节点(基本上是 ReactElement + 原始 JS 类型的合集)

简单的示例:

const elementOrComponent: React.ReactNode = 'string' || 0 || false || null || undefined || <div /> || <MyComponent />;

React CSS 属性

API 对应为:

React.CSSProperties

用于标识 jsx 文件中的 style 对象(通常用于 css-in-js

简单的示例:

const styles: React.CSSProperties = { display: 'flex', ...
const element = <div style={styles} ...

Hooks 登场

首先,什么是 Hooks 呢?

React 一直都提倡使用函数组件,但是有时候需要使用 state 或者其他一些功能时,只能使用类组件,因为函数组件没有实例,没有生命周期函数,只有类组件才有。

Hooks 是 React 16.8 新增的特性,它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

默认情况下,React 包含 10 个钩子。其中 3 个挂钩被视为是最常使用的“基本”或核心挂钩。还有 7 个额外的“高级”挂钩,这些挂钩最常用于边缘情况。10 个钩子如下:

  • 基础

    • useState
    • useEffect
    • useContext
  • 高级

    • useReducer
    • useCallback
    • useMemo
    • useRef
    • useImperativeHandle
    • useLayoutEffect
    • useDebugValue

useState with TypeScript

API 对应为:

// 传入唯一的参数: initialState,可以是数字,字符串等,也可以是对象或者数组。
// 返回的是包含两个元素的数组:第一个元素,state 变量,setState 修改 state值的方法。
const [state, setState] = useState(initialState);

useState是一个允许我们替换类组件中的 this.state 的挂钩。我们执行该挂钩,该挂钩返回一个包含当前状态值和一个用于更新状态的函数的数组。状态更新时,它会导致组件的重新 render。下面的代码显示了一个简单的 useState 钩子:

import * as React from 'react';

const MyComponent: React.FC = () => {
  const [count, setCount] = React.useState(0);
  return (
    <div onClick={() => setCount(count + 1)}>
      {count}
    </div>
  );
};

useEffect with TypeScript

API 对应为:

// 两个参数
// 第一个是一个函数,是在第一次渲染(componentDidMount)以及之后更新渲染之后会进行的副作用。这个函数可能会有返回值,倘若有返回值,返回值也必须是一个函数,会在组件被销毁(componentWillUnmount)时执行。
// 第二个参数是可选的,是一个数组,数组中存放的是第一个函数中使用的某些副作用属性。用来优化 useEffect
useEffect(() => { // 需要在componentDidMount执行的内容 return function cleanup() { // 需要在componentWillUnmount执行的内容 } }, [])

useEffect是用于我们管理副作用(例如 API 调用)并在组件中使用 React 生命周期的。useEffect 将回调函数作为其参数,并且回调函数可以返回一个清除函数(cleanup)。回调将在第一次渲染(componentDidMount) 和组件更新时(componentDidUpate)内执行,清理函数将组件被销毁(componentWillUnmount)内执行。

useEffect(() => {
  // 给 window 绑定点击事件
  window.addEventListener('click', handleClick);

  return () => {
      // 给 window 移除点击事件
      window.addEventListener('click', handleClick);
  }
});

默认情况下,useEffect 将在每个渲染时被调用,但是你还可以传递一个可选的第二个参数,该参数仅允许您在 useEffect 依赖的值更改时或仅在初始渲染时执行。第二个可选参数是一个数组,仅当其中一个值更改时才会 reRender(重新渲染)。如果数组为空,useEffect 将仅在 initial render(初始渲染)时调用。

useEffect(() => {
  // 使用浏览器API更新文档标题
  document.title = `You clicked ${count} times`;
}, [count]);    // 只有当数组中 count 值发生变化时,才会执行这个useEffect。

useContext with TypeScript

useContext允许您利用React context这样一种管理应用程序状态的全局方法,可以在任何组件内部进行访问而无需将值传递为 props。

useContext 函数接受一个 Context 对象并返回当前上下文值。当提供程序更新时,此挂钩将触发使用最新上下文值的重新渲染。

import { createContext, useContext } from 'react';

props ITheme {
  backgroundColor: string;
  color: string;
}

const ThemeContext = createContext<ITheme>({
  backgroundColor: 'black',
  color: 'white',
})

const themeContext = useContext<ITheme>(ThemeContext);

useReducer with TypeScript

对于更复杂的状态,您可以选择将该 useReducer 函数用作的替代 useState。

const [state,dispatch] =  useReducer(reducer,initialState,init);

如果您以前使用过Redux,则应该很熟悉。useReducer接受 3 个参数(reducer,initialState,init)并返回当前的 state 以及与其配套的 dispatch 方法。reducer 是如下形式的函数(state, action) => newState;initialState 是一个 JavaScript 对象;而 init 参数是一个惰性初始化函数,可以让你延迟加载初始状态。

这听起来可能有点抽象,让我们看一个实际的例子:

const initialState = 0;
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {number: state.number + 1};
    case 'decrement':
      return {number: state.number - 1};
    default:
      throw new Error();
  }
}
function init(initialState){
    return {number:initialState};
}
function Counter(){
    const [state, dispatch] = useReducer(reducer, initialState,init);
    return (
        <>
          Count: {state.number}
          <button onClick={() => dispatch({type: 'increment'})}>+</button>
          <button onClick={() => dispatch({type: 'decrement'})}>-</button>
        </>
    )
}

看完例子再结合上面 useReducer 的 api 是不是立马就明白了呢?

useCallback with TypeScript

useCallback 钩子返回一个 memoized 回调。这个钩子函数有两个参数:第一个参数是一个内联回调函数,第二个参数是一个数组。数组将在回调函数中引用,并按它们在数组中的存在顺序进行访问。

const memoizedCallback =  useCallback(()=> {
    doSomething(a,b);
  },[ a,b ],);

useCallback 将返回一个记忆化的回调版本,它仅会在某个依赖项改变时才重新计算 memoized 值。当您将回调函数传递给子组件时,将使用此钩子。这将防止不必要的渲染,因为仅在值更改时才执行回调,从而可以优化组件。可以将这个挂钩视为与shouldComponentUpdate生命周期方法类似的概念。

useMemo with TypeScript

useMemo返回一个 memoized 值。 传递“创建”函数和依赖项数组。useMemo 只会在其中一个依赖项发生更改时重新计算 memoized 值。此优化有助于避免在每个渲染上进行昂贵的计算。

const memoizedValue =  useMemo(() =>  computeExpensiveValue( a, b),[ a, b ]);
useMemo 在渲染过程中传递的函数会运行。不要做那些在渲染时通常不会做的事情。例如,副作用属于 useEffect,而不是 useMemo。

看到这,你可能会觉得,useMemouseCallback的作用有点像啊,那它们之间有什么区别呢?

  • useCallback 和 useMemo 都可缓存函数的引用或值。
  • 从更细的使用角度来说 useCallback 缓存函数的引用,useMemo 缓存计算数据的值。

useRef with TypeScript

useRef挂钩允许你创建一个 ref 并且允许你访问基础 DOM 节点的属性。当你需要从元素中提取值或获取与 DOM 相关的元素信息(例如其滚动位置)时,可以使用此方法。

const refContainer  =  useRef(initialValue);

useRef 返回一个可变的 ref 对象,其.current属性被初始化为传递的参数(initialValue)。返回的对象将存留在整个组件的生命周期中。

function TextInputWithFocusButton() {
  const inputEl = useRef<HTMLInputElement>(null);
  const onButtonClick = () => {
    inputEl.current.focus();
  };

  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

useImperativeHandle with TypeScript

useImperativeHandle可以让你在使用 ref 时,自定义暴露给父组件的实例值。

useImperativeHandle(ref, createHandle, [inputs])

useImperativeHandle 钩子函数接受 3 个参数: 一个 React ref、一个 createHandle 函数和一个用于暴露给父组件参数的可选数组。

function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
FancyInput = React.forwardRef(FancyInput);

const fancyInputRef = React.createRef();
<FancyInput ref={fancyInputRef}>Click me!</FancyInput>;

useLayoutEffect with TypeScript

与 useEffect Hooks 类似,都是执行副作用操作。但是它是在所有 DOM 更新完成后触发。可以用来执行一些与布局相关的副作用,比如获取 DOM 元素宽高,窗口滚动距离等等。

useLayoutEffect(() => { doSomething });
进行副作用操作时尽量优先选择 useEffect,以免阻止视图更新。与 DOM 无关的副作用操作请使用 useEffect。
import React, { useRef, useState, useLayoutEffect } from 'react';

export default () => {

    const divRef = useRef(null);

    const [height, setHeight] = useState(50);

    useLayoutEffect(() => {
        // DOM 更新完成后打印出 div 的高度
        console.log('useLayoutEffect: ', divRef.current.clientHeight);
    })

    return <>
        <div ref={ divRef } style={{ background: 'red', height: height }}>Hello</div>
        <button onClick={ () => setHeight(height + 50) }>改变 div 高度</button>
    </>

}

useDebugValue with TypeScript

useDebugValue是用于调试自定义挂钩(自定义挂钩请参考https://reactjs.org/docs/hooks-custom.html)的工具。它允许您在 React Dev Tools 中显示自定义钩子函数的标签。

示例

我之前基于 umi+react+typescript+ant-design 构建了一个简单的中后台通用模板。

涵盖的功能如下:

- 组件
  - 基础表格
  - ECharts 图表
  - 表单
    - 基础表单
    - 分步表单
  - 编辑器

- 控制台
- 错误页面
  - 404

里面对于在 react 中结合Hooks使用 typescript 的各种场景都有很好的实践,大家感兴趣的可以参考一下,https://github.com/FSFED/Umi-hooks/tree/feature_hook,当然不要吝惜你的 star!!!

最后

你可以关注我的同名公众号【前端森林】,这里我会定期发一些大前端相关的前沿文章和日常开发过程中的实战总结。当然,我也是开源社区的积极贡献者,github地址https://github.com/Cosen95,欢迎star!!!

image

查看原文

赞 4 收藏 3 评论 0

前端森林 发布了文章 · 1月21日

深入理解浏览器的缓存机制

引言

浏览器缓存,一个经久不衰的话题。

先来看一下百度百科对它的定义:

浏览器缓存(Browser Caching)是为了节约网络的资源加速浏览,浏览器在用户磁盘上对最近请求过的文档进行存储,当访问者再次请求这个页面时,浏览器就可以从本地磁盘显示文档,这样就可以加速页面的阅览。

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

本篇文章会从缓存位置、缓存过程分析、缓存类型、缓存机制、缓存策略以及用户行为对浏览器缓存的影响几方面带你一步步深入了解浏览器缓存。

缓存位置

从缓存位置上来说分为四种,并且各自有优先级,当依次查找缓存且都没有命中的时候,才会去请求网络。

  • Service Worker
  • Memory Cache
  • Disk Cache
  • Push Cache

Service Worker

Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker 的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。

Memory Cache

Memory Cache 也就是内存中的缓存,主要包含的是当前页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放(一旦我们关闭 Tab 页面,内存中的缓存也就被释放了)。

内存缓存中有一块重要的缓存资源是 preloader 相关指令(例如<link rel="prefetch">)众所周知 preloader 的相关指令已经是页面优化的常见手段之一,它可以一边解析 js/css 文件,一边网络请求下一个资源。

Disk Cache

Disk Cache 也就是存储在硬盘中的缓存,读取速度虽然慢点,但是什么都能存储到磁盘中,与 Memory Cache 相比,优势是容量和存储时效性。

在所有浏览器缓存中,Disk Cache 覆盖面基本上是最大的。它会根据 HTTP Header 中的字段判断哪些资源缓存(不用慌,关于 HTTP 的协议头中的缓存字段,会在下面详细介绍的),哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。绝大部分的缓存都来自 Disk Cache。

浏览器会把哪些文件丢进内存中?哪些丢进硬盘中?

关于这点,网上说法不一,不过以下两点比较靠得住:

  • 对于大文件来说,大概率是不存储在内存中的
  • 当前系统内存使用率高的话,文件优先存进硬盘

Push Cache

Push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂。

如果以上四种缓存都没有命中的话,那么只能发起请求来获取资源了。

为了性能上的考虑,大部分的接口都应该选择好缓存策略,通常浏览器缓存策略分为两种:强缓存和协商缓存,并且缓存策略都是通过设置 HTTP Header 来实现的。

缓存过程分析

浏览器与服务器通信的方式为应答模式,即: 浏览器发起 HTTP 请求 >> 服务器响应该请求,那么浏览器怎么确定一个资源该不该缓存,如何去缓存呢?浏览器第一次向服务器发起该请求后拿到请求结果后,将请求结果和缓存标识存入浏览器缓存,浏览器对于缓存的处理是根据第一次请求资源时返回的响应头来确定的。具体过程如下图:

由上图我们可以知道:

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

以上两点是浏览器缓存机制的关键,它确保了每个请求的缓存存入与读取。下面说一下浏览器缓存的使用规则。根据是否需要向服务器重新发起 HTTP 请求将缓存过程分为两个部分,分别是强缓存和协商缓存。

强缓存

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

1、 Expires

缓存过期时间,用来指定资源到期的时间,是服务端的具体时间点。也就是说,Expires=max-age + 请求时间,需要和 Last-modified 结合使用。Expires 是 Web 服务器响应消息头字段,在响应 http 请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。

Expires 是 HTTP/1 的产物,受限于本地时间,如果修改了本地时间,可能会造成缓存失效。

2、 Cache-Control

在 HTTP/1.1 中,Cache-Control 是最重要的规则,主要用于控制网页缓存。

Cache-Control 可以在请求头或者响应头中设置,并且可以组合使用多种指令:

  • public: 表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存,即使是通常不可缓存的内容(例如,该响应没有max-age指令或Expires消息头)。
  • private: 表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它)。私有缓存可以缓存响应内容。
  • no-cache: 在发布缓存副本之前,强制要求缓存把请求提交给原始服务器进行验证。
  • no-store: 缓存不应存储有关客户端请求或服务器响应的任何内容。
  • max-age: 设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)。与Expires相反,时间是相对于请求的时间。
  • s-maxage: 覆盖max-age或者Expires头,但是仅适用于共享缓存(比如各个代理),私有缓存会忽略它。
  • max-stale: 表明客户端愿意接收一个已经过期的资源。可以设置一个可选的秒数,表示响应不能已经过时超过该给定的时间。
  • min-fresh: 表示客户端希望获取一个能在指定的秒数内保持其最新状态的响应。

3、 Expires 和 Cache-Control 两者对比

其实这两者差别不大,区别就在于 Expires 是 http1.0 的产物,Cache-Control 是 http1.1 的产物,两者同时存在的话,Cache-Control 优先级高于 Expires;在某些不支持 HTTP1.1 的环境下,Expires 就会发挥用处。所以 Expires 其实是过时的产物,现阶段它的存在只是一种兼容性的写法。

强缓存判断是否缓存的依据来自于是否超出某个时间或者某个时间段,而不关心服务器端文件是否已经更新,这可能会导致加载文件不是服务器端最新的内容,那我们如何获知服务器端内容是否已经发生了更新呢?此时我们需要用到协商缓存策略。

协商缓存

协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程,主要有以下两种情况:

  • 协商缓存生效,返回 304 和 Not Modified

  • 协商缓存成功,返回 200 和请求结果

协商缓存可以通过设置两种 HTTP Header 实现: Last-Modified 和 ETag

缓存机制

强制缓存优先于协商缓存进行,若强制缓存 (Expires 和 Cache-Control) 生效则直接使用缓存,若不生效则进行协商缓存 (Last-Modified / If-Modified-Since 和 Etag / If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,返回 200,重新返回资源和缓存标识,再存入浏览器缓存中;生效则返回 304,继续使用缓存。具体流程图如下:

实际场景应用缓存策略

  • 频繁变动的资源

对于频繁变动的资源,首先需要使用 Cache-Control: no-cache 使浏览器每次都请求服务器,然后配合 ETag 或者 Last-Modified 来验证资源是否有效。这样的做法虽然不能节省请求数量,但是能显著减少响应数据大小。

  • 不常变化的资源

通常在处理这类资源时,给它们的 Cache-Control 配置一个很大的 max-age=31536000 (一年),这样浏览器之后请求相同的 URL 会命中强制缓存。而为了解决更新的问题,就需要在文件名 (或者路径) 中添加 hash, 版本号等动态字符,之后更改动态字符,从而达到更改引用 URL 的目的,让之前的强制缓存失效 (其实并未立即失效,只是不再使用了而已)。

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

所谓用户行为对浏览器缓存的影响,指的就是用户在浏览器如何操作时,会触发怎样的缓存策略。主要有 3 种:

  • 打开网页,地址栏输入地址: 查找 disk cache 中是否有匹配。如有则使用;如没有则发送网络请求;
  • 普通刷新 (F5):因为 TAB 并没有关闭,因此 memory cache 是可用的,会被优先使用 (如果匹配的话)。其次才是 disk cache;
  • 强制刷新 (Ctrl + F5):浏览器不使用缓存,因此发送的请求头部均带有 Cache-control: no-cache(为了兼容,还带了 Pragma: no-cache), 服务器直接返回 200 和最新内容。

最后

你可以关注我的同名公众号【前端森林】,这里我会定期发一些大前端相关的前沿文章和日常开发过程中的实战总结。当然,我也是开源社区的积极贡献者,github地址https://github.com/Jack-cool,欢迎star!!!

前端森林公众号二维码.png

查看原文

赞 3 收藏 1 评论 0

前端森林 发布了文章 · 1月21日

webpack5快发布了,你还没用过4吗?

webpack.jpeg

引言

webpack5 预计会在 2020 年年初发布,之前从 alpha 版本就有关注,本次重点更新在长期缓存,tree shakking 和 es6 打包这块。具体变更可以参考https://github.com/webpack/ch...

webpack 是现代前端开发中最火的模块打包工具,只需要通过简单的配置,便可以完成模块的加载和打包。那它是怎么做到通过对一些插件的配置,便可以轻松实现对代码的构建呢?

本篇文章不会去探讨 webpack5 中所要更新的内容,我相信大多数前端同学对于 webpack 只是会简单的配置,而且现在像 vue-cli、umi 等对于 webpack 都有很好的封装,但其实这样对于我们自己是不太好的。尤其是想针对业务场景去做一些个性化的定制时。只有对 webpack 中的细节足够了解,我们才能游刃有余,本文将从 webpack 现有的大版本 webpack4,带你一步步打造极致的前端开发环境。

安装 webpack 的几种方式

  • global(全局):通过 webpack index.js 运行
  • local(项目维度安装):通过 npx webpack index.js 运行

避免全局安装 webpack(针对多个项目采用不同的 webpack 版本进行打包的场景),可采用npx

entry(入口)

单一入口

// webpack.config.js

const config = {
  entry: {
    main: "./src/index.js"
  }
};

多入口

// webpack.config.js

const config = {
  entry: {
    main: "./src/index.js",
    sub: "./src/sub.js"
  }
};

output(输出)

默认配置

// webpack.config.js
const path = require('path');
...

const config = {
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
};

module.exports = config;

多个入口起点

如果配置创建了多个单独的 "chunk"(例如,使用多个入口起点或使用像 CommonsChunkPlugin 这样的插件),则应该使用占位符(substitutions)来确保每个文件具有唯一的名称。
// webpack.config.js
const path = require('path');
{
  entry: {
    main: './src/index.js',
    sub: './src/sub.js'
  },
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist')
  }
}

// 写入到硬盘:./dist/main.js, ./dist/sub.js

高级进阶

使用 cdn
// webpack.config.js
const path = require('path');
{
  entry: {
    main: './src/index.js',
    sub: './src/sub.js'
  },
  output: {
    publicPath: 'http://cdn.example.com'
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist')
  }
}

// 写入到http://cdn.example.com/main.js, http://cdn.example.com/sub.js

loaders

webpack 可以使用 loader 来预处理文件。这允许你打包除 JavaScript 之外的任何静态资源。

file-loader

  • file-loader 可以解析项目中的 url 引入(不仅限于 css),根据我们的配置,将图片拷贝到相应的路径,再根据我们的配置,修改打包后文件引用路径,使之指向正确的文件。
  • 默认情况下,生成的文件的文件名就是文件内容的 MD5 哈希值并会保留所引用资源的原始扩展名。
rules: [
  {
    test: /\.(jpg|png|gif)$/,
    use: {
      loader: "file-loader",
      options: {
        name: "[name]_[hash].[ext]",
        outputPath: "images/"
      }
    }
  }
];

url-loader

  • url-loader 功能类似于 file-loader,但是在文件大小(单位 byte)低于指定的限制时,可以返回一个 DataURL。
  • url-loader 把资源文件转换为 URL,file-loader 也是一样的功能。不同之处在于 url-loader 更加灵活,它可以把小文件转换为 base64 格式的 URL,从而减少网络请求次数。url-loader 依赖 file-loader。
rules: [
  {
    test: /\.(jpg|png|gif)$/,
    use: {
      loader: "url-loader",
      options: {
        name: "[name]_[hash].[ext]",
        outputPath: "images/",
        limit: 204800
      }
    }
  }
];

css-loader

  • 只负责加载 css 模块,不会将加载的 css 样式应用到 html
  • importLoaders 用于指定在 css-loader 前应用的 loader 的数量
  • 查询参数 modules 会启用 CSS 模块规范
module: {
  rules: [
    {
      test: /\.css$/,
      use: ["style-loader", "css-loader"]
    }
  ];
}

style-loader

  • 负责将 css-loader 加载到的 css 样式动态的添加到 html-head-style 标签中
  • 一般建议将 style-loader 与 css-loader 结合使用

sass-loader

安装

yarn add sass-loader node-sass webpack --dev

  • node-sass 和 webpack 是 sass-loader 的 peerDependency,因此能够精确控制它们的版本。
  • loader 执行顺序:从下至上,从右至左
  • 通过将 style-loader 和 css-loader 与 sass-loader 链式调用,可以立刻将样式作用在 DOM 元素。
// webpack.config.js
module.exports = {
...
module: {
  rules: [{
    test: /\.scss$/,
    use: [{
        loader: "style-loader" // 将 JS 字符串生成为 style 节点
    }, {
        loader: "css-loader" // 将 CSS 转化成 CommonJS 模块
    }, {
        loader: "sass-loader" // 将 Sass 编译成 CSS
    }]
  }]
}
};

postcss-loader

  • webpack4 中使用 postcss-loader 代替 autoprefixer,给 css3 样式加浏览器前缀。具体可参考https://blog.csdn.net/u014628388/article/details/82593185
// webpack.config.js
 {
  test: /\.scss$/,
  use: [
    'style-loader',
      'css-loader',
      'sass-loader',
      'postcss-loader'
    ],
}

//postcss.config.js
module.exports = {
    plugins: [
        require('autoprefixer')({ browsers: ['last 2 versions'] }),
    ],
};

plugins

plugin 可以在 webpack 运行到某个时刻的时候,帮你做一些事情

HtmlWebpackPlugin

  • HtmlWebpackPlugin 会在打包结束后,自动生成一个 html 文件,并把打包生成的 js 自动引入到这个 html 文件中
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
...
plugins: [
    new HtmlWebpackPlugin({
      template: 'src/index.html'
    }),
  ],
};

clean-webpack-plugin

  • clean-webpack-plugin 插件用来清除残留打包文件,特别是文件末尾添加了 hash 之后,会导致改变文件内容后重新打包时,文件名不同而内容越来越多。
  • 新版本中的 clean-webpack-plugin 仅接受一个对象,默认不需要传任何参数。具体可参考https://blog.csdn.net/qq_23521659/article/details/88353708
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
...
plugins: [
    new CleanWebpackPlugin()
  ],
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  }

SplitChunksPlugin

  • 具体概念可参考https://juejin.im/post/5af15e895188256715479a9a
splitChunks: {
    chunks: "async",
    minSize: 30000,
    minChunks: 1,
    maxAsyncRequests: 5,
    maxInitialRequests: 3,
    automaticNameDelimiter: '~',
    name: true,
    cacheGroups: {
        vendors: {
            test: /[\\/]node_modules[\\/]/,
            priority: -10
        },
    default: {
            minChunks: 2,
            priority: -20,
            reuseExistingChunk: true
        }
    }
}

MiniCssExtractPlugin

将 CSS 提取为独立的文件的插件,对每个包含 css 的 js 文件都会创建一个 CSS 文件,支持按需加载 css 和 sourceMap
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
  plugins: [
    new MiniCssExtractPlugin({
      // Options similar to the same options in webpackOptions.output
      // both options are optional
      filename: "[name].css",
      chunkFilename: "[id].css"
    })
  ],
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader
          },
          {
            loader: "css-loader",
            options: {
              importLoaders: 2 // 用于指定在 css-loader 前应用的 loader 的数量
              // modules: true   // 查询参数 modules 会启用 CSS 模块规范
            }
          },
          "sass-loader",
          "postcss-loader"
        ]
      },
      {
        test: /\.css$/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader
          },
          "css-loader",
          "postcss-loader"
        ]
      }
    ]
  }
};

OptimizeCSSAssetsPlugin

webpack5 可能会内置 CSS 压缩器,webpack4 需要自己使用压缩器,可以使用 optimize-css-assets-webpack-plugin 插件。 设置 optimization.minimizer 覆盖 webpack 默认提供的,确保也指定一个 JS 压缩器
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");

module.exports = {
  optimization: {
    minimizer: [
      new UglifyJsPlugin({
        cache: true,
        parallel: true,
        sourcMap: true
      }),
      new OptimizeCSSAssetsPlugin({})
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "[name].css",
      chunkFilename: "[id].css"
    })
  ],
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, "css-loader"]
      }
    ]
  }
};

devtool

source map

source map 就是对打包生成的代码与源代码的一种映射,主要是为了方便定位问题和排查问题。devtool 关键有 eval、cheap、module、inline 和 source-map 这几块,具体可参考文档:https://www.webpackjs.com/configuration/devtool/
  • development 环境参考配置: 'cheap-module-eval-source-map'
  • production 环境参考配置: 'cheap-module-source-map'

webpack-dev-server

webpack-dev-server 提供了一个简单的 web 服务器,并且能够实时重新加载(live reloading)。具体可参考https://www.webpackjs.com/guides/development/#%E4%BD%BF%E7%94%A8-webpack-dev-server

接口代理(请求转发)

如果你有单独的后端开发服务器 API,并且希望在同域名下发送 API 请求 ,那么代理某些 URL 会很有用。dev-server 使用了非常强大的 http-proxy-middleware 包。常用于接口请求转发。具体参考https://www.webpackjs.com/configuration/dev-server/#devserver-proxy
devServer: {
    contentBase: "./dist",
    open: true,
    hot: true,
    hotOnly: true,
    proxy: {
      "/api": {
        target: "https://other-server.example.com",
        pathRewrite: {"^/api" : ""},
        secure: false,
        bypass: function(req, res, proxyOptions) {
          if (req.headers.accept.indexOf("html") !== -1) {
            console.log("Skipping proxy for browser request.");
            return "/index.html";
          }
        }
      }
    }
  },

解决单页面路由问题

当使用 HTML5 History API 时,任意的 404 响应都可能需要被替代为 index.html
通过传入以下启用:
historyApiFallback: true;

通过传入一个对象,比如使用 rewrites 这个选项,此行为可进一步地控制:

historyApiFallback: {
  rewrites: [
    { from: /^\/$/, to: "/views/landing.html" },
    { from: /^\/subpage/, to: "/views/subpage.html" },
    { from: /./, to: "/views/404.html" }
  ];
}

webpack-dev-middleware

webpack-dev-middleware 是一个容器(wrapper),它可以把 webpack 处理后的文件传递给一个服务器(server)。 webpack-dev-server 在内部使用了它,同时,它也可以作为一个单独的包来使用,以便进行更多自定义设置来实现更多的需求
// server.js
// 使用webpack-dev-middleware
// https://www.webpackjs.com/guides/development/#%E4%BD%BF%E7%94%A8-webpack-dev-middleware
const express = require("express");
const webpack = require("webpack");
const webpackDevMiddleware = require("webpack-dev-middleware");
const config = require("./webpack.config.js");
const complier = webpack(config);

const app = express();

app.use(
  webpackDevMiddleware(complier, {
    publicPath: config.output.publicPath
  })
);

app.listen(3000, () => {
  console.log("server is running");
});

Hot Module Replacement

模块热替换(Hot Module Replacement 或 HMR)是 webpack 提供的最有用的功能之一。它允许在运行时更新各种模块,而无需进行完全刷新。
// webpack.config.js
...
const webpack = require('webpack');
...
devServer: {
  contentBase: './dist',
  open: true,
  hot: true,
  hotOnly: true
},
plugins: [
  ...
  new webpack.HotModuleReplacementPlugin()
],

如果已经通过 HotModuleReplacementPlugin 启用了模块热替换(Hot Module Replacement),则它的接口将被暴露在 module.hot 属性下面。通常,用户先要检查这个接口是否可访问,然后再开始使用它。

// index.js
if (module.hot) {
  module.hot.accept("./library.js", function() {
    // 使用更新过的 library 模块执行某些操作...
  });
}

bundle 分析

借助一些官方推荐的可视化分析工具,可对打包后的模块进行分析以及优化
  • webpack-chart: webpack 数据交互饼图
  • webpack-visualizer: 可视化并分析你的 bundle,检查哪些模块占用空间,哪些可能是重复使用的
  • webpack-bundle-analyzer: 一款分析 bundle 内容的插件及 CLI 工具,以便捷的、交互式、可缩放的树状图形式展现给用户

Preloading、Prefetching

prefetch:会等待核心代码加载完成后,页面带宽空闲后再去加载 prefectch 对应的文件;preload:和主文件一起去加载
  • 可以使用谷歌浏览器 Coverage 工具查看代码覆盖率(ctrl+shift+p > show coverage)
  • 使用异步引入 js 的方式可以提高 js 的使用率,所以 webpack 建议我们多使用异步引入的方式,这也是 splitChunks.chunks 的默认值是"async"的原因
  • 使用魔法注释 /_ webpackPrefetch: true _/ ,这样在主要 js 加载完,带宽有空闲时,会自动下载需要引入的 js
  • 使用魔法注释 /_ webpackPreload: true _/,区别是 webpackPrefetch 会等到主业务文件加载完,带宽有空闲时再去下载 js,而 preload 是和主业务文件一起加载的

babel

babel 编译 es6、jsx 等

  • @babel/core babel 核心模块
  • @babel-preset-env 编译 es6 等
  • @babel/preset-react 转换 jsx
  • @babel/plugin-transform-runtime 避免 polyfill 污染全局变量,减少打包体积
  • @babel/polyfill es6 内置方法和函数转化垫片
  • @babel/runtime
module: {
  rules: [
    {
      test: /\.js$/,
      exclude: /node_modules/,
      use: {
        loader: "babel-loader"
      }
    }
  ];
}

新建.babelrc 文件

{
  "presets": ["@babel/preset-env", "@babel/preset-react"],
  "plugins": ["@babel/plugin-transform-runtime"]
}

按需引入 polyfill

在 src 下的 index.js 中全局引入@babel/polyfill 并写入 es6 语法,但是这样有一个缺点:
全局引入@babel/polyfill 的这种方式可能会导入代码中不需要的 polyfill,从而使打包体积更大,修改.babelrc 配置


`yarn add core-js@2 @babel/runtime-corejs2 --dev`

{
  "presets": [
    [
      "@babel/preset-env", {
      "useBuiltIns": "usage"
      }
    ],
    "@babel/preset-react"
  ],
  "plugins": ["@babel/plugin-transform-runtime"]
}

这就配置好了按需引入。配置了按需引入 polyfill 后,用到 es6 以上的函数,babel 会自动导入相关的 polyfill,这样能大大减少打包编译后的体积。

babel-runtime 和 babel-polyfill 的区别

参考https://www.jianshu.com/p/73ba084795ce
  • babel-polyfill 会”加载整个 polyfill 库”,针对编译的代码中新的 API 进行处理,并且在代码中插入一些帮助函数
  • babel-polyfill 解决了 Babel 不转换新 API 的问题,但是直接在代码中插入帮助函数,会导致污染了全局环境,并且不同的代码文件中包含重复的代码,导致编译后的代码体积变大。 Babel 为了解决这个问题,提供了单独的包 babel-runtime 用以提供编译模块的工具函数, 启用插件 babel-plugin-transform-runtime 后,Babel 就会使用 babel-runtime 下的工具函数
  • babel-runtime 适合在组件,类库项目中使用,而 babel-polyfill 适合在业务项目中使用。

高级概念

tree shaking(js)

tree shaking 可清除代码中无用的 js 代码,只支持 import 方式引入,不支持 commonjs 的方式引入
mode 是 production 的无需配置,下面的配置是针对 development 的
// webpack.config.js
optimization: {
  usedExports: true
}


// package.json
"sideEffects": false,

Code Spliting

代码分割,和 webpack 无关
  • 同步代码(需在 webpack.config.js 中配置 optimization)
// index.js
import _ from 'lodash';

console.log(_.join(['a','b','c'], '****'))

// 在webpack.base.js里做相关配置
optimization: {
    splitChunks: {
      chunks: 'all'
    }
  },
  • 异步代码(无需任何配置,但需安装@babel/plugin-syntax-dynamic-import包)
// index.js
function getComponent() {
  return import("lodash").then(({ default: _ }) => {
    const element = document.createElement("div");
    element.innerHTML = _.join(["Jack", "Cool"], "-");
    return element;
  });
}

getComponent().then(el => {
  document.body.appendChild(el);
});

Caching(缓存)

通过使用 output.filename 进行文件名替换,可以确保浏览器获取到修改后的文件。[hash] 替换可以用于在文件名中包含一个构建相关(build-specific)的 hash,但是更好的方式是使用 [contenthash] 替换,当文件内容发生变化时,[contenthash]也会发生变化
output: {
  filename: "[name].[contenthash].js",
  chunkFilename: '[name].[contenthash].chunk.js'
}

Shimming

webpack 编译器(compiler)能够识别遵循 ES2015 模块语法、CommonJS 或 AMD 规范编写的模块。然而,一些第三方的库(library)可能会引用一些全局依赖(例如 jQuery 中的 &dollar;)。这些库也可能创建一些需要被导出的全局变量。这些“不符合规范的模块”就是 shimming 发挥作用的地方
  • shimming 全局变量(第三方库)(ProvidePlugin 相当于一个垫片)
 const path = require('path');
+ const webpack = require('webpack');

  module.exports = {
    entry: './src/index.js',
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist')
-   }
+   },
+   plugins: [
+     new webpack.ProvidePlugin({
+       _: 'lodash'
+     })
+   ]
  };
  • 细粒度 shimming(this 指向 window)(需要安装 imports-loader 依赖)
 const path = require('path');
  const webpack = require('webpack');

  module.exports = {
    entry: './src/index.js',
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist')
    },
+   module: {
+     rules: [
+       {
+         test: require.resolve('index.js'),
+         use: 'imports-loader?this=>window'
+       }
+     ]
+   },
    plugins: [
      new webpack.ProvidePlugin({
        join: ['lodash', 'join']
      })
    ]
  };

环境变量

webpack 命令行环境选项 --env 允许您传入任意数量的环境变量。您的环境变量将可访问 webpack.config.js。例如,--env.production 或--env.NODE_ENV=local
webpack --env.NODE_ENV=local --env.production --progress

使用环境变量必须对 webpack 配置进行一项更改。通常,module.exports 指向配置对象。要使用该 env 变量,必须转换 module.exports 为函数:

// webpack.config.js
const path = require("path");

module.exports = env => {
  // Use env.<YOUR VARIABLE> here:
  console.log("NODE_ENV: ", env.NODE_ENV); // 'local'
  console.log("Production: ", env.production); // true

  return {
    entry: "./src/index.js",
    output: {
      filename: "bundle.js",
      path: path.resolve(__dirname, "dist")
    }
  };
};

library 打包配置

除了打包应用程序代码,webpack 还可以用于打包 JavaScript library
用户应该能够通过以下方式访问 library:
  • ES2015 模块。例如 import library from 'library'
  • CommonJS 模块。例如 require('library')
  • 全局变量,当通过 script 脚本引入时

我们打包的 library 中可能会用到一些第三方库,诸如 lodash。现在,如果执行 webpack,你会发现创建了一个非常巨大的文件。如果你查看这个文件,会看到 lodash 也被打包到代码中。在这种场景中,我们更倾向于把 lodash 当作 peerDependency。也就是说,用户应该已经将 lodash 安装好。因此,你可以放弃对外部 library 的控制,而是将控制权让给使用 library 的用户。这可以使用 externals 配置来完成:

  // webpack.config.js
  var path = require('path');

  module.exports = {
    entry: './src/index.js',
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: 'webpack-numbers.js'
-   }
+   },
+   externals: {
+     lodash: {
+       commonjs: 'lodash',
+       commonjs2: 'lodash',
+       amd: 'lodash',
+       root: '_'
+     }
+   }
  };

对于用途广泛的 library,我们希望它能够兼容不同的环境,例如 CommonJS,AMD,Node.js 或者作为一个全局变量。为了让你的 library 能够在各种用户环境(consumption)中可用,需要在 output 中添加 library 属性:

  // webpack.config.js
  var path = require('path');

  module.exports = {
    entry: './src/index.js',
    output: {
      path: path.resolve(__dirname, 'dist'),
-     filename: 'library.js'
+     filename: 'library.js',
+     library: 'library'
    },
    externals: {
      lodash: {
        commonjs: 'lodash',
        commonjs2: 'lodash',
        amd: 'lodash',
        root: '_'
      }
    }
  };

当你在 import 引入模块时,这可以将你的 library bundle 暴露为名为 webpackNumbers 的全局变量。为了让 library 和其他环境兼容,还需要在配置文件中添加 libraryTarget 属性。这是可以控制 library 如何以不同方式暴露的选项。

  var path = require('path');

  module.exports = {
    entry: './src/index.js',
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: 'library.js',
+     library: 'library',
+     libraryTarget: 'umd'
    },
    externals: {
      lodash: {
        commonjs: 'lodash',
        commonjs2: 'lodash',
        amd: 'lodash',
        root: '_'
      }
    }
  };

我们还需要通过设置 package.json 中的 main 字段,添加生成 bundle 的文件路径。

// package.json
{
  ...
  "main": "dist/library.js",
  ...
}

PWA 打包配置

渐进式网络应用程序(Progressive Web Application - PWA),是一种可以提供类似于原生应用程序(native app)体验的网络应用程序(web app)。PWA 可以用来做很多事。其中最重要的是,在离线(offline)时应用程序能够继续运行功能。这是通过使用名为 Service Workers 的网络技术来实现的
添加 workbox-webpack-plugin 插件,并调整 webpack.config.js 文件:
npm install workbox-webpack-plugin --save-dev

webpack.config.js

 const path = require('path');
  const HtmlWebpackPlugin = require('html-webpack-plugin');
  const CleanWebpackPlugin = require('clean-webpack-plugin');
+ const WorkboxPlugin = require('workbox-webpack-plugin');

  module.exports = {
    entry: {
      app: './src/index.js',
      print: './src/print.js'
    },
  plugins: [
    new CleanWebpackPlugin(['dist']),
    new HtmlWebpackPlugin({
-     title: 'Output Management'
+     title: 'Progressive Web Application'
-   })
+   }),
+   new WorkboxPlugin.GenerateSW({
+     // 这些选项帮助 ServiceWorkers 快速启用
+     // 不允许遗留任何“旧的” ServiceWorkers
+     clientsClaim: true,
+     skipWaiting: true
+   })
  ],
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist')
    }
  };

注册 Service Worker

  import _ from 'lodash';
  import printMe from './print.js';

+ if ('serviceWorker' in navigator) {
+   window.addEventListener('load', () => {
+     navigator.serviceWorker.register('/sw.js').then(registration => {
+       console.log('SW registered: ', registration);
+     }).catch(registrationError => {
+       console.log('SW registration failed: ', registrationError);
+     });
+   });
+ }

现在来进行测试。停止服务器并刷新页面。如果浏览器能够支持 Service Worker,你应该可以看到你的应用程序还在正常运行。然而,服务器已经停止了服务,此刻是 Service Worker 在提供服务。

TypeScript 打包配置

可参考https://www.webpackjs.com/guides/typescript/https://webpack.js.org/guides/typescript/
  • 安装 ts 依赖npm install --save-dev typescript ts-loader
  • 增加 tsconfig.json 配置文件
{
  "compilerOptions": {
    "outDir": "./dist/",
    "noImplicitAny": true,
    "module": "es6",
    "target": "es5",
    "jsx": "react",
    "allowJs": true
  }
}
  • webpack.config.js 添加对 ts/tsx 语法支持(ts-loader)
const path = require("path");

module.exports = {
  entry: "./src/index.ts",
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: "ts-loader",
        exclude: /node_modules/
      }
    ]
  },
  resolve: {
    extensions: [".tsx", ".ts", ".js"]
  },
  output: {
    filename: "bundle.js",
    path: path.resolve(__dirname, "dist")
  }
};
  • 当从 npm 安装第三方库时,一定要牢记同时安装这个库的类型声明文件。可以从 TypeSearch 中找到并安装这些第三方库的类型声明文件。如npm install --save-dev @types/lodash

webpack 性能优化

  • 及时更新 node、yarn、webpack 等的版本
  • 在尽可能少的模块上应用 loader
  • plugin 尽可能精简并确保可靠(选用社区已验证的插件)
  • resolve 参数合理配置(具体参考https://www.webpackjs.com/configuration/resolve/)
  • 使用 DllPlugin 提高打包速度
  • 控制包文件大小(tree shaking / splitChunksPlugin)
  • thread-loader,parallel-webpack,happypack 多进程打包
  • 合理利用 sourceMap
  • 结合stats.json分析打包结果(bundle analyze)
  • 开发环境内存编译
  • 开发环境无用插件剔除

最后

你可以关注我的同名公众号【前端森林】,这里我会定期发一些大前端相关的前沿文章和日常开发过程中的实战总结。当然,我也是开源社区的积极贡献者,github地址https://github.com/Jack-cool,欢迎star!!!

前端森林公众号二维码.png

查看原文

赞 7 收藏 4 评论 0

前端森林 收藏了文章 · 2019-05-31

Vue3.0数据双向绑定Proxy探究

前言

2018年11月16日,关注vue的人都知道这个时间点发生了什么事儿吧。vue3.0更新内容

研究数据双向绑定的大佬们都在开始猜测这个新机制了,用原生Proxy替换Object.defineProperty

1. 为什么要替换Object.defineProperty

替换不是因为不好,是因为有更好的方法使用效率更高

Object.defineProperty的缺点:

  1. 在Vue中,Object.defineProperty无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应。为了解决这个问题,经过vue内部处理后可以使用以下几种方法来监听数组。有关于这个说明,可以看看这个文章 vue为什么不能检测数组变动

    push()
    pop()
    shift()
    unshift()
    splice()
    sort()
    reverse()

    目前只针对以上方法做了hack处理,所以恰数组属性是检测不到的,有局限性。

  2. Object.defineProperty只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue里,是通过递归以及遍历data对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象,不管是对操作性还是性能都会有一个很大的提升。
    而要取代它的Proxy有以下两个优点:

    1. 可以劫持整个对象,并返回一个新对象
    2. 有13种劫持操作
    

2. 什么是Proxy

Proxy是 ES6 中新增的一个特性,翻译过来意思是"代理",用在这里表示由它来“代理”某些操作。 Proxy 让我们能够以简洁易懂的方式控制外部对对象的访问。其功能非常类似于设计模式中的代理模式。

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

使用 Proxy 的核心优点是可以交由它来处理一些非核心逻辑(如:读取或设置对象的某些属性前记录日志;设置对象的某些属性值前,需要验证;某些属性的访问控制等)。 从而可以让对象只需关注于核心逻辑,达到关注点分离,降低对象复杂度等目的。

基本用法:

let p = new Proxy(target, handler);

参数:

target: 是用Proxy包装的被代理对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
handler: 是一个对象,其声明了代理target 的一些操作,其属性是当执行一个操作时定义代理的行为的函数。

p是Proxy对象,当其他操作对p进行更改的时候,会执行handler对象的方法。Proxy有13种数据劫持的操作,常用的handler处理方法:

get: 读取值,
set: 设置值,
has: 判断对象是否拥有该属性,
construct: 构造函数

给个例子:

let obj = {};
 let handler = {
   get(target, property) {
    console.log(`${property} 被读取`);
    return property in target ? target[property] : 3;
   },
   set(target, property, value) {
    console.log(`${property} 被设置为 ${value}`);
    target[property] = value;
   }
 }

 let p = new Proxy(obj, handler);
 p.name = 'tom' //name 被设置为 tom
 p.age; //age 被读取 3

更多的Proxy属性方法参考MDN Proxy

3. Proxy实现数据劫持

observe(data) {
  const that = this;
  let handler = {
   get(target, property) {
      return target[property];
    },
    set(target, key, value) {
      let res = Reflect.set(target, key, value);
      that.subscribe[key].map(item => {
        item.update();
      });
      return res;
    }
  }
  this.$data = new Proxy(data, handler);
}

这段代码里把代理器返回的对象代理到this.$data,即this.$data是代理后的对象,外部每次对this.$data进行操作时,实际上执行的是这段代码里handler对象上的方法。
注:这儿用到了reflect属性,这也是ES6里面的,不知道的去这儿看看吧。reflect属性

4. 对于怎么拼接到watcher和compile

上面说到了怎么使用Proxy做数据劫持,怎么结合订阅发布,请结合 vue2.0数据双向绑定探究 对照着Object.defineProperty
数据劫持的部分去替换看一下。其他的设计思想估计跟之前的八九不离十。

查看原文

前端森林 收藏了文章 · 2019-05-05

git代码统计

命令行

查看git上的个人代码量:

git log --author="username" --pretty=tformat: --numstat | awk '{ add += $1; subs += $2; loc += $1 - $2 } END { printf "added lines: %s, removed lines: %s, total lines: %s\n", add, subs, loc }' -

结果示例:(记得修改 username)

added lines: 120745, removed lines: 71738, total lines: 49007

统计每个人增删行数

git log --format='%aN' | sort -u | while read name; do echo -en "$name\t"; git log --author="$name" --pretty=tformat: --numstat | awk '{ add += $1; subs += $2; loc += $1 - $2 } END { printf "added lines: %s, removed lines: %s, total lines: %s\n", add, subs, loc }' -; done

结果示例

Max-laptop    added lines: 1192, removed lines: 748, total lines: 444
chengshuai    added lines: 120745, removed lines: 71738, total lines: 49007
cisen    added lines: 3248, removed lines: 1719, total lines: 1529
max-h    added lines: 1002, removed lines: 473, total lines: 529
max-l    added lines: 2440, removed lines: 617, total lines: 1823
mw    added lines: 148721, removed lines: 6709, total lines: 142012
spider    added lines: 2799, removed lines: 1053, total lines: 1746
thy    added lines: 34616, removed lines: 13368, total lines: 21248
wmao    added lines: 12, removed lines: 8, total lines: 4
xrl    added lines: 10292, removed lines: 6024, total lines: 4268
yunfei.huang    added lines: 427, removed lines: 10, total lines: 417
³Ÿö    added lines: 5, removed lines: 3, total lines: 2

查看仓库提交者排名前 5

git log --pretty='%aN' | sort | uniq -c | sort -k1 -n -r | head -n 5

贡献值统计

git log --pretty='%aN' | sort -u | wc -l

提交数统计

git log --oneline | wc -l

添加或修改的代码行数:

git log --stat|perl -ne 'END { print $c } $c += $1 if /(\d+) insertions/'

使用gitstats

GitStats项目,用Python开发的一个工具,通过封装Git命令来实现统计出来代码情况并且生成可浏览的网页。官方文档可以参考这里。

使用方法

git clone git://github.com/hoxu/gitstats.git
cd gitstats
./gitstats 你的项目的位置 生成统计的文件夹位置

可能会提示没有安装gnuplot画图程序,那么需要安装再执行:

//mac osx
brew install gnuplot
//centos linux
yum install gnuplot

生成的统计文件为HTML:
2014-8-16-git.jpg

使用cloc

npm install -g cloc

image

参考文章

git代码行统计命令集
统计本地Git仓库中不同贡献者的代码行数的一些方法
使用Git工具统计代码

查看原文

前端森林 收藏了文章 · 2019-04-11

Vue面试中,经常会被问到的面试题/Vue知识点整理

看看面试题,只是为了查漏补缺,看看自己那些方面还不懂。切记不要以为背了面试题,就万事大吉了,最好是理解背后的原理,这样面试的时候才能侃侃而谈。不然,稍微有水平的面试官一看就能看出,是否有真才实学还是刚好背中了这道面试题。
(都是一些基础的vue面试题,大神不用浪费时间往下看)

一、对于MVVM的理解?

MVVM 是 Model-View-ViewModel 的缩写。
Model代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑。
View 代表UI 组件,它负责将数据模型转化成UI 展现出来。
ViewModel 监听模型数据的改变和控制视图行为、处理用户交互,简单理解就是一个同步View 和 Model的对象,连接Model和View。
在MVVM架构下,View 和 Model 之间并没有直接的联系,而是通过ViewModel进行交互,Model 和 ViewModel 之间的交互是双向的, 因此View 数据的变化会同步到Model中,而Model 数据的变化也会立即反应到View 上。
ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,而View 和 Model 之间的同步工作完全是自动的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作DOM, 不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。
bg2015020110.png

二、Vue的生命周期

beforeCreate(创建前) 在数据观测和初始化事件还未开始
created(创建后) 完成数据观测,属性和方法的运算,初始化事件,$el属性还没有显示出来
beforeMount(载入前) 在挂载开始之前被调用,相关的render函数首次被调用。实例已完成以下的配置:编译模板,把data里面的数据和模板生成html。注意此时还没有挂载html到页面上。
mounted(载入后) 在el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的html内容替换el属性指向的DOM对象。完成模板中的html渲染到html页面中。此过程中进行ajax交互。
beforeUpdate(更新前) 在数据更新之前调用,发生在虚拟DOM重新渲染和打补丁之前。可以在该钩子中进一步地更改状态,不会触发附加的重渲染过程。
updated(更新后) 在由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用。调用时,组件DOM已经更新,所以可以执行依赖于DOM的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。
beforeDestroy(销毁前) 在实例销毁之前调用。实例仍然完全可用。
destroyed(销毁后) 在实例销毁之后调用。调用后,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务器端渲染期间不被调用。
1.什么是vue生命周期?
答: Vue 实例从创建到销毁的过程,就是生命周期。从开始创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、销毁等一系列过程,称之为 Vue 的生命周期。

2.vue生命周期的作用是什么?
答:它的生命周期中有多个事件钩子,让我们在控制整个Vue实例的过程时更容易形成好的逻辑。

3.vue生命周期总共有几个阶段?
答:它可以总共分为8个阶段:创建前/后, 载入前/后,更新前/后,销毁前/销毁后。

4.第一次页面加载会触发哪几个钩子?
答:会触发 下面这几个beforeCreate, created, beforeMount, mounted 。

5.DOM 渲染在 哪个周期中就已经完成?
答:DOM 渲染在 mounted 中就已经完成了。

三、 Vue实现数据双向绑定的原理:Object.defineProperty()

vue实现数据双向绑定主要是:采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应监听回调。当把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项时,Vue 将遍历它的属性,用 Object.defineProperty 将它们转为 getter/setter。用户看不到 getter/setter,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。

vue的数据双向绑定 将MVVM作为数据绑定的入口,整合Observer,Compile和Watcher三者,通过Observer来监听自己的model的数据变化,通过Compile来解析编译模板指令(vue中是用来解析 {{}}),最终利用watcher搭起observer和Compile之间的通信桥梁,达到数据变化 —>视图更新;视图交互变化(input)—>数据model变更双向绑定效果。

js实现简单的双向绑定

<body>
    <div id="app">
    <input type="text" id="txt">
    <p id="show"></p>
</div>
</body>
<script type="text/javascript">
    var obj = {}
    Object.defineProperty(obj, 'txt', {
        get: function () {
            return obj
        },
        set: function (newValue) {
            document.getElementById('txt').value = newValue
            document.getElementById('show').innerHTML = newValue
        }
    })
    document.addEventListener('keyup', function (e) {
        obj.txt = e.target.value
    })
</script>

四、Vue组件间的参数传递

1.父组件与子组件传值
父组件传给子组件:子组件通过props方法接受数据;
子组件传给父组件:$emit方法传递参数
2.非父子组件间的数据传递,兄弟组件传值
eventBus,就是创建一个事件中心,相当于中转站,可以用它来传递事件和接收事件。项目比较小时,用这个比较合适。(虽然也有不少人推荐直接用VUEX,具体来说看需求咯。技术只是手段,目的达到才是王道。)

五、Vue的路由实现:hash模式 和 history模式

hash模式:在浏览器中符号“#”,#以及#后面的字符称之为hash,用window.location.hash读取;
特点:hash虽然在URL中,但不被包括在HTTP请求中;用来指导浏览器动作,对服务端安全无用,hash不会重加载页面。
hash 模式下,仅 hash 符号之前的内容会被包含在请求中,如 http://www.xxx.com,因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回 404 错误。

history模式:history采用HTML5的新特性;且提供了两个新方法:pushState(),replaceState()可以对浏览器历史记录栈进行修改,以及popState事件的监听到状态变更。
history 模式下,前端的 URL 必须和实际向后端发起请求的 URL 一致,如 http://www.xxx.com/items/id。后端如果缺少对 /items/id 的路由处理,将返回 404 错误。Vue-Router 官网里如此描述:“不过这种模式要玩好,还需要后台配置支持……所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。”

六、Vue与Angular以及React的区别?

(版本在不断更新,以下的区别有可能不是很正确。我工作中只用到vue,对angular和react不怎么熟)
1.与AngularJS的区别
相同点:
都支持指令:内置指令和自定义指令;都支持过滤器:内置过滤器和自定义过滤器;都支持双向数据绑定;都不支持低端浏览器。

不同点:
AngularJS的学习成本高,比如增加了Dependency Injection特性,而Vue.js本身提供的API都比较简单、直观;在性能上,AngularJS依赖对数据做脏检查,所以Watcher越多越慢;Vue.js使用基于依赖追踪的观察并且使用异步队列更新,所有的数据都是独立触发的。

2.与React的区别
相同点:
React采用特殊的JSX语法,Vue.js在组件开发中也推崇编写.vue特殊文件格式,对文件内容都有一些约定,两者都需要编译后使用;中心思想相同:一切都是组件,组件实例之间可以嵌套;都提供合理的钩子函数,可以让开发者定制化地去处理需求;都不内置列数AJAX,Route等功能到核心包,而是以插件的方式加载;在组件开发中都支持mixins的特性。
不同点:
React采用的Virtual DOM会对渲染出来的结果做脏检查;Vue.js在模板中提供了指令,过滤器等,可以非常方便,快捷地操作Virtual DOM。

七、vue路由的钩子函数

首页可以控制导航跳转,beforeEach,afterEach等,一般用于页面title的修改。一些需要登录才能调整页面的重定向功能。

beforeEach主要有3个参数to,from,next:

to:route即将进入的目标路由对象,

from:route当前导航正要离开的路由

next:function一定要调用该方法resolve这个钩子。执行效果依赖next方法的调用参数。可以控制网页的跳转。

八、vuex是什么?怎么使用?哪种功能场景使用它?

只用来读取的状态集中放在store中; 改变状态的方式是提交mutations,这是个同步的事物; 异步逻辑应该封装在action中。
在main.js引入store,注入。新建了一个目录store,….. export 。
场景有:单页应用中,组件之间的状态、音乐播放、登录状态、加入购物车
图片描述

state
Vuex 使用单一状态树,即每个应用将仅仅包含一个store 实例,但单一状态树和模块化并不冲突。存放的数据状态,不可以直接修改里面的数据。
mutations
mutations定义的方法动态修改Vuex 的 store 中的状态或数据。
getters
类似vue的计算属性,主要用来过滤一些数据。
action
actions可以理解为通过将mutations里面处里数据的方法变成可异步的处理数据的方法,简单的说就是异步操作数据。view 层通过 store.dispath 来分发 action。

const store = new Vuex.Store({ //store实例
      state: {
         count: 0
             },
      mutations: {                
         increment (state) {
          state.count++
         }
          },
      actions: { 
         increment (context) {
          context.commit('increment')
   }
 }
})

modules
项目特别复杂的时候,可以让每一个模块拥有自己的state、mutation、action、getters,使得结构非常清晰,方便管理。

const moduleA = {
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
 }
const moduleB = {
  state: { ... },
  mutations: { ... },
  actions: { ... }
 }

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
})

九、vue-cli如何新增自定义指令?

1.创建局部指令

var app = new Vue({
    el: '#app',
    data: {    
    },
    // 创建指令(可以多个)
    directives: {
        // 指令名称
        dir1: {
            inserted(el) {
                // 指令中第一个参数是当前使用指令的DOM
                console.log(el);
                console.log(arguments);
                // 对DOM进行操作
                el.style.width = '200px';
                el.style.height = '200px';
                el.style.background = '#000';
            }
        }
    }
})

2.全局指令

Vue.directive('dir2', {
    inserted(el) {
        console.log(el);
    }
})

3.指令的使用

<div id="app">
    <div v-dir1></div>
    <div v-dir2></div>
</div>

十、vue如何自定义一个过滤器?

html代码:

<div id="app">
     <input type="text" v-model="msg" />
     {{msg| capitalize }}
</div>

JS代码:

var vm=new Vue({
    el:"#app",
    data:{
        msg:''
    },
    filters: {
      capitalize: function (value) {
        if (!value) return ''
        value = value.toString()
        return value.charAt(0).toUpperCase() + value.slice(1)
      }
    }
})

全局定义过滤器

Vue.filter('capitalize', function (value) {
  if (!value) return ''
  value = value.toString()
  return value.charAt(0).toUpperCase() + value.slice(1)
})

过滤器接收表达式的值 (msg) 作为第一个参数。capitalize 过滤器将会收到 msg的值作为第一个参数。

十一、对keep-alive 的了解?

keep-alive是 Vue 内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染。
在vue 2.1.0 版本之后,keep-alive新加入了两个属性: include(包含的组件缓存) 与 exclude(排除的组件不缓存,优先级大于include) 。

使用方法

<keep-alive include='include_components' exclude='exclude_components'>
  <component>
    <!-- 该组件是否缓存取决于include和exclude属性 -->
  </component>
</keep-alive>

参数解释
include - 字符串或正则表达式,只有名称匹配的组件会被缓存
exclude - 字符串或正则表达式,任何名称匹配的组件都不会被缓存
include 和 exclude 的属性允许组件有条件地缓存。二者都可以用“,”分隔字符串、正则表达式、数组。当使用正则或者是数组时,要记得使用v-bind 。

使用示例

<!-- 逗号分隔字符串,只有组件a与b被缓存。 -->
<keep-alive include="a,b">
  <component></component>
</keep-alive>

<!-- 正则表达式 (需要使用 v-bind,符合匹配规则的都会被缓存) -->
<keep-alive :include="/a|b/">
  <component></component>
</keep-alive>

<!-- Array (需要使用 v-bind,被包含的都会被缓存) -->
<keep-alive :include="['a', 'b']">
  <component></component>
</keep-alive>

十二、一句话就能回答的面试题

1.css只在当前组件起作用
答:在style标签中写入scoped即可 例如:<style scoped></style>

2.v-if 和 v-show 区别
答:v-if按照条件是否渲染,v-show是display的block或none;

3.$route$router的区别
答:$route是“路由信息对象”,包括path,params,hash,query,fullPath,matched,name等路由信息参数。而$router是“路由实例”对象包括了路由的跳转方法,钩子函数等。

4.vue.js的两个核心是什么?
答:数据驱动、组件系统

5.vue几种常用的指令
答:v-for 、 v-if 、v-bind、v-on、v-show、v-else

6.vue常用的修饰符?
答:.prevent: 提交事件不再重载页面;.stop: 阻止单击事件冒泡;.self: 当事件发生在该元素本身而不是子元素的时候会触发;.capture: 事件侦听,事件发生的时候会调用

7.v-on 可以绑定多个方法吗?
答:可以

8.vue中 key 值的作用?
答:当 Vue.js 用 v-for 正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。key的作用主要是为了高效的更新虚拟DOM。

9.什么是vue的计算属性?
答:在模板中放入太多的逻辑会让模板过重且难以维护,在需要对数据进行复杂处理,且可能多次使用的情况下,尽量采取计算属性的方式。好处:①使得数据处理结构清晰;②依赖于数据,数据更新,处理结果自动更新;③计算属性内部this指向vm实例;④在template调用时,直接写计算属性名即可;⑤常用的是getter方法,获取数据,也可以使用set方法改变数据;⑥相较于methods,不管依赖的数据变不变,methods都会重新计算,但是依赖数据不变的时候computed从缓存中获取,不会重新计算。

10.vue等单页面应用及其优缺点
答:优点:Vue 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件,核心是一个响应的数据绑定系统。MVVM、数据驱动、组件化、轻量、简洁、高效、快速、模块友好。
缺点:不支持低版本的浏览器,最低只支持到IE9;不利于SEO的优化(如果要支持SEO,建议通过服务端来进行渲染组件);第一次加载首页耗时相对长一些;不可以使用浏览器的导航按钮需要自行实现前进、后退。

11.怎么定义 vue-router 的动态路由? 怎么获取传过来的值
答:在 router 目录下的 index.js 文件中,对 path 属性加上 /:id,使用 router 对象的 params.id 获取。

Vue面试中,经常会被问到的面试题/Vue知识点整理
532道前端真实大厂面试题
学习ES6笔记──工作中常用到的ES6语法

查看原文

前端森林 收藏了文章 · 2019-04-09

React 的未来:Time Slicing 和 Suspense

本文转自 FEPulse 公众号(微信搜索 FEPulse,每日精选一条国内外最新前端资讯,为你把握前端脉搏)。

JSConf Iceland 大会于 3.1 - 3.2 在冰岛举行,在会中,React 核心团队的 Dan Abramov 发表了名为 “Beyond React 16” 的演讲,描述了对 React 未来的展望,本文根据 Dan 的演讲整理。

React 作为一个通用的框架,需要考虑各种网络状况(网速有快有慢)以及各种设备类型(CPU 性能有好有坏),而框架开发者的一个重要使命就是帮助开发者们开发在各种情况下用户体验都很好的应用程序。

影响用户体验的因素主要可以归为两大类:计算能力(Computing Power)和网络速度(Network Speed),他们对应计算设备的 CPU 和 IO 能力。在 React 中,CPU 主要影响 DOM 元素创建和更新的效率,而 IO 则影响获取数据和懒加载的代码。下面 Dan 用了两个 Demo 来展示 React 在这两个方面的尝试。

Demo 1

图片描述

第一个例子,由一个输入框和下面的三个图表组成,当输入的内容越复杂,下面的图表也越来越复杂。

为了更直观地看到页面刷新的效率,Dan 还写了一个时钟,绿色表示页面刷新时帧与帧之间间隔的时间很短,而颜色越深则表示间隔时间越长,页面卡顿感越强,用户体验自然也越差。
图片描述

这种场景中,提升用户体验的一个经典的做法是 Debounce(Throttle 类似),即等用户暂停输入后再刷新页面,对应的 Demo 如下:
图片描述

但这么做用户体验上也是有缺陷的,如果用户的 CPU 很强劲,那也不得不等暂停输入后才看到结果。那有没有更好的解决方案呢?有的,Dan 给了一种异步刷新的方案,先看图:
图片描述

引用 Dan PPT 中的一句话,用来体现这种异步方案的精髓。

We've built a generic way to ensure that high-priority updates like user input don't get blocked by rendering low-priority updates.

Dan 称这种特性为 “Time Slicing”,主要包括以下几点:
图片描述

Demo 2
图片描述

第二个 Demo 是一个电影信息展示的应用,选择一个电影,点击进入可查看电影的详情和评论。在后面的演示中,Dan 通过一步步修改这个 Demo 的代码来让我们理解 React 在网络方面如何提升用户体验的。

首先,Dan 将数据从硬编码改成了从网络获取。代码如下:
图片描述

在上面这段代码中,当 React 渲染 MovieDetails 组件时,会先看对应电影 ID 的详情有没有被缓存,因为是第一次,电影详情信息还没被缓存,所以需要去网络拉取,下面高能!React 会阻止渲染过程,直到数据被拉取回来!而我们需要做的,仅仅告诉 React,从电影列表页到电影详情页的过程是个异步过程即可。代码如下:
图片描述

这样修改过后,下面的动图展示了这种异步过程:点击一个电影,React 去网络拉取对应电影的详情数据,等数据拉取回来后,React 再将页面渲染出来。
图片描述

同时,页面还保持良好的可交互性,点击一个电影之后紧接着用户也可以点击其他的电影。

上面的修改仅仅模拟了 1s 的网络延迟,如果延迟更长咋办,用户点击了一个电影,发现页面像卡死了一般,用户体验肯定很差。Dan 紧接着又对代码做了修改:使用一个叫 “Placeholder” 的组件包裹即将要渲染的异步组件,当组件在加载的过程中,Placehodler 会显示一个“安慰剂”。
图片描述

同样的,这个过程界面仍然保持着良好的交互性,当页面在转圈时,用户可以选择返回。
图片描述

上面的修改中,只有一个电影详情数据需要从网络获取,如果有多个数据需要从网络获取,那么 React 可以选择等所有数据都返回后显示最终的页面,也可以选择优先展示先获取到的数据。
图片描述

除了在详情页中展示安慰剂,也可以在电影列表处展示,这需要通过一个叫 Loading 的组件来实现,Loading 与 Placehodler 一样,是一个未来的特性。
图片描述

除此以外,还可以使用 Code Splitting 来优化用户体验。但点击一个电影时,再去拉取跟电影详情页相关的代码。这里还是使用未来提供的 createFetcher 接口来实现 Code Splitting。
图片描述

运行代码,点击一个电影,会发现 Network 中新拉下来一个叫 “1.chunk.js” 的文件。
图片描述

最后,Dan 还做了一个用户体验方面的优化,目前为止的 Demo 中,打开页面时,电影海报可能没加载出来(从上面一张动图中也可以看出来),所以 Dan 做了优化,需要等图片加载完成后,页面才能显示。这里的细节不再详述,当然这也是用 createFetcher 实现的。

到这里,Demo 2 就结束了。Dan 用一句话概括了这个新特性:

We've built a generic way to suspend rendering while they load asynchronous data.

Dan 称这个新特性为 “Suspense”,主要包括以下几点:
图片描述

这两项新的特性据说将在今年发布,是不是很期待呢?你对 Timing Slicing 和 Suspense 又有什么看法呢?欢迎留言。

最后,再次邀请大家关注我们的公众号 FEPulse,第一时间获取我们精心整理的最新的前端资讯。

查看原文

前端森林 收藏了文章 · 2019-04-08

Apache设置反向代理解决js跨域问题

这是一个很简单的方案,通过启用Apache反向代理解决js跨域问题。

其实有一个更简单的方法,如果你使用Chrome浏览器,你可以装一个叫Allow-Control-Allow-Origin: *的拓展程序,跨域问题就直接解决啦!但是,这个方法有一个问题,前端跨域POST请求的时候,因为CORS(cross origin resource share)规范的存在,浏览器会首先发送一次OPTIONS嗅探,同时header带上origin,判断是否有跨域请求权限,服务器响应access control allow origin的值,供浏览器与origin匹配,如果匹配则正式发送POST请求。也就是说,如果之前开发的api没有考虑OPTIONS方法的话,OPTIONS嗅探就会失败,那么跨域POST请求也会跟着失败!如果你刚好遇上了这种情况,可以使用Apache反向代理的方法完美解决js跨域问题。

为什么要这么做?

在现在的开发过程中大家会遇到这样一个问题:后端代码写好之后,前端需要将后端代码部署到本地才能正常使用api。若直接使用远程服务器上的api(例如测试服务器上的api)就会出现js跨域问题,导致远程服务器上的api无法使用。但是,将后端代码部署到前端的本地会出现以下几个问题:

  • 前端下载后端代码到本地并配置,花时间
  • 后端代码有更新之后,前端也需要更新本地的后端代码,花时间
  • 前端本地安装的服务器环境可能略有差异,从而导致后端代码在本机上不能正常运行,需要后端协助调试,花时间

为了简单高效的开发,建议前端启用Apache反向代理解决js跨域问题。在解决js跨域问题之后,前端可直接使用测试服务器上的api接口,不需要再在本地部署后端代码。这样前端和后端都只需要在关心自己的代码就可以了。

正向代理和反向代理

这里有一篇非常好的文章做了详细解释:正向代理与反向代理有什么区别。我在这里还是简单概述一下:

  • 正向代理:正向代理隐藏了真实的请求客户端,服务端不知道真实的客户端是谁,客户端请求的服务都被代理服务器代替来请求。用浏览器访问 www.google.com 时,被残忍的拒绝了,于是你可以在国外搭建一台代理服务器,让代理帮我去请求 google.com,代理把请求返回的相应结构再返回给我。
  • 反向代理:反向代理隐藏了真实的服务端,当我们请求 www.baidu.com 的时候,背后可能有成千上万台服务器为我们服务,但具体是哪一台,你不知道,也不需要知道,你只需要知道反向代理服务器是谁就好了,www.baidu.com 就是我们的反向代理服务器,反向代理服务器会帮我们把请求转发到真实的服务器那里去。

准备

在本地安装好apache服务器!

怎么做?

  1. 启用proxy模块

    • Windows环境一般是安装了xampp或者wamp。在xampp或者wamp安装目录下,修改httpd.conf配置文件,去掉以下两行前面 # 号,从而启用Apache proxy module。

      LoadModule proxy_module modules/mod_proxy.so
      LoadModule proxy_http_module modules/mod_proxy_http.so
    • Ubuntu:

      sudo apt-get install libapache2-mod-proxy-html
      sudo apt-get install libxml2-dev
      sudo a2enmod proxy proxy_http
  2. 在httpd-vhosts.conf里配置virtualHost实现反向代理:

    <VirtualHost *:80>
        ProxyRequests Off
    
        <Proxy *>
          Order deny,allow
          Allow from all
        </Proxy>
    
        ProxyPass /project http://ip_address/project
    </VirtualHost>
    • ProxyRequests Off 指令是指采用反向(reverse)代理
    • ProxyPass 指令允许将一个远端服务器映射到本地服务器的URL空间中
    • 配置完成之后,访问 http://localhost/project 实际就是访问 http://ip_address/project 上的资源
  3. 重启Apache
查看原文

前端森林 收藏了文章 · 2019-04-03

不要再问我跨域的问题了

写下这篇文章后我想,要不以后就把这种基础的常见知识都归到这个“不要再问我XX的问题”,形成一系列内容,希望大家看完之后再有人问你这些问题,你心里会窃喜:“嘿嘿,是时候展现真正的技术了!”
一、不要再问我this的指向问题了

跨域这两个字就像一块狗皮膏药一样黏在每一个前端开发者身上,无论你在工作上或者面试中无可避免会遇到这个问题。为了应付面试,我每次都随便背几个方案,也不知道为什么要这样干,反正面完就可以扔了,我想工作上也不会用到那么多乱七八糟的方案。到了真正工作,开发环境有webpack-dev-server搞定,上线了服务端的大佬们也会配好,配了什么我不管,反正不会跨域就是了。日子也就这么混过去了,终于有一天,我觉得不能再继续这样混下去了,我一定要彻底搞懂这个东西!于是就有了这篇文章。

要掌握跨域,首先要知道为什么会有跨域这个问题出现

确实,我们这种搬砖工人就是为了混口饭吃嘛,好好的调个接口告诉我跨域了,这种阻碍我们轻松搬砖的事情真恶心!为什么会跨域?是谁在搞事情?为了找到这个问题的始作俑者,请点击浏览器的同源策略
这么官方的东西真难懂,没关系,至少你知道了,因为浏览器的同源策略导致了跨域,就是浏览器在搞事情。
所以,浏览器为什么要搞事情?就是不想给好日子我们过?对于这样的质问,浏览器甩锅道:“同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。”
这么官方的话术真难懂,没关系,至少你知道了,似乎这是个安全机制。
所以,究竟为什么需要这样的安全机制?这样的安全机制解决了什么问题?别急,让我们继续研究下去。

没有同源策略限制的两大危险场景

据我了解,浏览器是从两个方面去做这个同源策略的,一是针对接口的请求,二是针对Dom的查询。试想一下没有这样的限制上述两种动作有什么危险。

没有同源策略限制的接口请求

有一个小小的东西叫cookie大家应该知道,一般用来处理登录等场景,目的是让服务端知道谁发出的这次请求。如果你请求了接口进行登录,服务端验证通过后会在响应头加入Set-Cookie字段,然后下次再发请求的时候,浏览器会自动将cookie附加在HTTP请求的头字段Cookie中,服务端就能知道这个用户已经登录过了。知道这个之后,我们来看场景:
1.你准备去清空你的购物车,于是打开了买买买网站www.maimaimai.com,然后登录成功,一看,购物车东西这么少,不行,还得买多点。
2.你在看有什么东西买的过程中,你的好基友发给你一个链接www.nidongde.com,一脸yin笑地跟你说:“你懂的”,你毫不犹豫打开了。
3.你饶有兴致地浏览着www.nidongde.com,谁知这个网站暗地里做了些不可描述的事情!由于没有同源策略的限制,它向www.maimaimai.com发起了请求!聪明的你一定想到上面的话“服务端验证通过后会在响应头加入Set-Cookie字段,然后下次再发请求的时候,浏览器会自动将cookie附加在HTTP请求的头字段Cookie中”,这样一来,这个不法网站就相当于登录了你的账号,可以为所欲为了!如果这不是一个买买买账号,而是你的银行账号,那……
这就是传说中的CSRF攻击浅谈CSRF攻击方式
看了这波CSRF攻击我在想,即使有了同源策略限制,但cookie是明文的,还不是一样能拿下来。于是我看了一些cookie相关的文章聊一聊 cookieCookie/Session的机制与安全,知道了服务端可以设置httpOnly,使得前端无法操作cookie,如果没有这样的设置,像XSS攻击就可以去获取到cookieWeb安全测试之XSS;设置secure,则保证在https的加密通信中传输以防截获。

没有同源策略限制的Dom查询

1.有一天你刚睡醒,收到一封邮件,说是你的银行账号有风险,赶紧点进www.yinghang.com改密码。你吓尿了,赶紧点进去,还是熟悉的银行登录界面,你果断输入你的账号密码,登录进去看看钱有没有少了。
2.睡眼朦胧的你没看清楚,平时访问的银行网站是www.yinhang.com,而现在访问的是www.yinghang.com,这个钓鱼网站做了什么呢?

// HTML
<iframe name="yinhang" data-original="www.yinhang.com"></iframe>
// JS
// 由于没有同源策略的限制,钓鱼网站可以直接拿到别的网站的Dom
const iframe = window.frames['yinhang']
const node = iframe.document.getElementById('你输入账号密码的Input')
console.log(`拿到了这个${node},我还拿不到你刚刚输入的账号密码吗`)

由此我们知道,同源策略确实能规避一些危险,不是说有了同源策略就安全,只是说同源策略是一种浏览器最基本的安全机制,毕竟能提高一点攻击的成本。其实没有刺不穿的盾,只是攻击的成本和攻击成功后获得的利益成不成正比。

跨域正确的打开方式

经过对同源策略的了解,我们应该要消除对浏览器的误解,同源策略是浏览器做的一件好事,是用来防御来自邪门歪道的攻击,但总不能为了不让坏人进门而把全部人都拒之门外吧。没错,我们这种正人君子只要打开方式正确,就应该可以跨域。
下面将一个个演示正确打开方式,但在此之前,有些准备工作要做。为了本地演示跨域,我们需要:
1.随便跑起一份前端代码(以下前端是随便跑起来的vue),地址是http://localhost:9099。
2.随便跑起一份后端代码(以下后端是随便跑起来的node koa2),地址是http://localhost:9971。

同源策略限制下接口请求的正确打开方式

1.JSONP
在HTML标签里,一些标签比如script、img这样的获取资源的标签是没有跨域限制的,利用这一点,我们可以这样干:

后端写个小接口

// 处理成功失败返回格式的工具
const {successBody} = require('../utli')
class CrossDomain {
  static async jsonp (ctx) {
    // 前端传过来的参数
    const query = ctx.request.query
    // 设置一个cookies
    ctx.cookies.set('tokenId', '1')
    // query.cb是前后端约定的方法名字,其实就是后端返回一个直接执行的方法给前端,由于前端是用script标签发起的请求,所以返回了这个方法后相当于立马执行,并且把要返回的数据放在方法的参数里。
    ctx.body = `${query.cb}(${JSON.stringify(successBody({msg: query.msg}, 'success'))})`
  }
}
module.exports = CrossDomain

简单版前端

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
  </head>
  <body>
    <script type='text/javascript'>
      // 后端返回直接执行的方法,相当于执行这个方法,由于后端把返回的数据放在方法的参数里,所以这里能拿到res。
      window.jsonpCb = function (res) {
        console.log(res)
      }
    </script>
    <script data-original='http://localhost:9871/api/jsonp?msg=helloJsonp&cb=jsonpCb' type='text/javascript'></script>
  </body>
</html>

简单封装一下前端这个套路

/**
 * JSONP请求工具
 * @param url 请求的地址
 * @param data 请求的参数
 * @returns {Promise<any>}
 */
const request = ({url, data}) => {
  return new Promise((resolve, reject) => {
    // 处理传参成xx=yy&aa=bb的形式
    const handleData = (data) => {
      const keys = Object.keys(data)
      const keysLen = keys.length
      return keys.reduce((pre, cur, index) => {
        const value = data[cur]
        const flag = index !== keysLen - 1 ? '&' : ''
        return `${pre}${cur}=${value}${flag}`
      }, '')
    }
    // 动态创建script标签
    const script = document.createElement('script')
    // 接口返回的数据获取
    window.jsonpCb = (res) => {
      document.body.removeChild(script)
      delete window.jsonpCb
      resolve(res)
    }
    script.src = `${url}?${handleData(data)}&cb=jsonpCb`
    document.body.appendChild(script)
  })
}
// 使用方式
request({
  url: 'http://localhost:9871/api/jsonp',
  data: {
    // 传参
    msg: 'helloJsonp'
  }
}).then(res => {
  console.log(res)
})

2.空iframe加form
细心的朋友可能发现,JSONP只能发GET请求,因为本质上script加载资源就是GET,那么如果要发POST请求怎么办呢?

后端写个小接口

// 处理成功失败返回格式的工具
const {successBody} = require('../utli')
class CrossDomain {
  static async iframePost (ctx) {
    let postData = ctx.request.body
    console.log(postData)
    ctx.body = successBody({postData: postData}, 'success')
  }
}
module.exports = CrossDomain

前端

const requestPost = ({url, data}) => {
  // 首先创建一个用来发送数据的iframe.
  const iframe = document.createElement('iframe')
  iframe.name = 'iframePost'
  iframe.style.display = 'none'
  document.body.appendChild(iframe)
  const form = document.createElement('form')
  const node = document.createElement('input')
  // 注册iframe的load事件处理程序,如果你需要在响应返回时执行一些操作的话.
  iframe.addEventListener('load', function () {
    console.log('post success')
  })

  form.action = url
  // 在指定的iframe中执行form
  form.target = iframe.name
  form.method = 'post'
  for (let name in data) {
    node.name = name
    node.value = data[name].toString()
    form.appendChild(node.cloneNode())
  }
  // 表单元素需要添加到主文档中.
  form.style.display = 'none'
  document.body.appendChild(form)
  form.submit()

  // 表单提交后,就可以删除这个表单,不影响下次的数据发送.
  document.body.removeChild(form)
}
// 使用方式
requestPost({
  url: 'http://localhost:9871/api/iframePost',
  data: {
    msg: 'helloIframePost'
  }
})

3.CORS

CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)跨域资源共享 CORS 详解。看名字就知道这是处理跨域问题的标准做法。CORS有两种请求,简单请求和非简单请求。

这里引用上面链接阮一峰老师的文章说明一下简单请求和非简单请求。
浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

只要同时满足以下两大条件,就属于简单请求。
(1) 请求方法是以下三种方法之一:

  • HEAD
  • GET
  • POST

(2)HTTP的头信息不超出以下几种字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

1.简单请求
后端

// 处理成功失败返回格式的工具
const {successBody} = require('../utli')
class CrossDomain {
  static async cors (ctx) {
    const query = ctx.request.query
    // *时cookie不会在http请求中带上
    ctx.set('Access-Control-Allow-Origin', '*')
    ctx.cookies.set('tokenId', '2')
    ctx.body = successBody({msg: query.msg}, 'success')
  }
}
module.exports = CrossDomain

前端什么也不用干,就是正常发请求就可以,如果需要带cookie的话,前后端都要设置一下,下面那个非简单请求例子会看到。

fetch(`http://localhost:9871/api/cors?msg=helloCors`).then(res => {
  console.log(res)
})

2.非简单请求
非简单请求会发出一次预检测请求,返回码是204,预检测通过才会真正发出请求,这才返回200。这里通过前端发请求的时候增加一个额外的headers来触发非简单请求。
clipboard.png

后端

// 处理成功失败返回格式的工具
const {successBody} = require('../utli')
class CrossDomain {
  static async cors (ctx) {
    const query = ctx.request.query
    // 如果需要http请求中带上cookie,需要前后端都设置credentials,且后端设置指定的origin
    ctx.set('Access-Control-Allow-Origin', 'http://localhost:9099')
    ctx.set('Access-Control-Allow-Credentials', true)
    // 非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)
    // 这种情况下除了设置origin,还需要设置Access-Control-Request-Method以及Access-Control-Request-Headers
    ctx.set('Access-Control-Request-Method', 'PUT,POST,GET,DELETE,OPTIONS')
    ctx.set('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, t')
    ctx.cookies.set('tokenId', '2')

    ctx.body = successBody({msg: query.msg}, 'success')
  }
}
module.exports = CrossDomain

一个接口就要写这么多代码,如果想所有接口都统一处理,有什么更优雅的方式呢?见下面的koa2-cors。

const path = require('path')
const Koa = require('koa')
const koaStatic = require('koa-static')
const bodyParser = require('koa-bodyparser')
const router = require('./router')
const cors = require('koa2-cors')
const app = new Koa()
const port = 9871
app.use(bodyParser())
// 处理静态资源 这里是前端build好之后的目录
app.use(koaStatic(
  path.resolve(__dirname, '../dist')
))
// 处理cors
app.use(cors({
  origin: function (ctx) {
    return 'http://localhost:9099'
  },
  credentials: true,
  allowMethods: ['GET', 'POST', 'DELETE'],
  allowHeaders: ['t', 'Content-Type']
}))
// 路由
app.use(router.routes()).use(router.allowedMethods())
// 监听端口
app.listen(9871)
console.log(`[demo] start-quick is starting at port ${port}`)

前端

fetch(`http://localhost:9871/api/cors?msg=helloCors`, {
  // 需要带上cookie
  credentials: 'include',
  // 这里添加额外的headers来触发非简单请求
  headers: {
    't': 'extra headers'
  }
}).then(res => {
  console.log(res)
})

4.代理
想一下,如果我们请求的时候还是用前端的域名,然后有个东西帮我们把这个请求转发到真正的后端域名上,不就避免跨域了吗?这时候,Nginx出场了。
Nginx配置

server{
    # 监听9099端口
    listen 9099;
    # 域名是localhost
    server_name localhost;
    #凡是localhost:9099/api这个样子的,都转发到真正的服务端地址http://localhost:9871 
    location ^~ /api {
        proxy_pass http://localhost:9871;
    }    
}

前端就不用干什么事情了,除了写接口,也没后端什么事情了

// 请求的时候直接用回前端这边的域名http://localhost:9099,这就不会跨域,然后Nginx监听到凡是localhost:9099/api这个样子的,都转发到真正的服务端地址http://localhost:9871 
fetch('http://localhost:9099/api/iframePost', {
  method: 'POST',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    msg: 'helloIframePost'
  })
})

Nginx转发的方式似乎很方便!但这种使用也是看场景的,如果后端接口是一个公共的API,比如一些公共服务获取天气什么的,前端调用的时候总不能让运维去配置一下Nginx,如果兼容性没问题(IE 10或者以上),CROS才是更通用的做法吧。

同源策略限制下Dom查询的正确打开方式

1.postMessage
window.postMessage() 是HTML5的一个接口,专注实现不同窗口不同页面的跨域通讯。
为了演示方便,我们将hosts改一下:127.0.0.1 crossDomain.com,现在访问域名crossDomain.com就等于访问127.0.0.1。

这里是http://localhost:9099/#/crossDomain,发消息方

<template>
  <div>
    <button @click="postMessage">给http://crossDomain.com:9099发消息</button>
    <iframe name="crossDomainIframe" data-original="http://crossdomain.com:9099"></iframe>
  </div>
</template>

<script>
export default {
  mounted () {
    window.addEventListener('message', (e) => {
      // 这里一定要对来源做校验
      if (e.origin === 'http://crossdomain.com:9099') {
        // 来自http://crossdomain.com:9099的结果回复
        console.log(e.data)
      }
    })
  },
  methods: {
    // 向http://crossdomain.com:9099发消息
    postMessage () {
      const iframe = window.frames['crossDomainIframe']
      iframe.postMessage('我是[http://localhost:9099], 麻烦你查一下你那边有没有id为app的Dom', 'http://crossdomain.com:9099')
    }
  }
}
</script>

这里是http://crossdomain.com:9099,接收消息方

<template>
  <div>
    我是http://crossdomain.com:9099
  </div>
</template>

<script>
export default {
  mounted () {
    window.addEventListener('message', (e) => {
      // 这里一定要对来源做校验
      if (e.origin === 'http://localhost:9099') {
        // http://localhost:9099发来的信息
        console.log(e.data)
        // e.source可以是回信的对象,其实就是http://localhost:9099窗口对象(window)的引用
        // e.origin可以作为targetOrigin
        e.source.postMessage(`我是[http://crossdomain.com:9099],我知道了兄弟,这就是你想知道的结果:${document.getElementById('app') ? '有id为app的Dom' : '没有id为app的Dom'}`, e.origin);
      }
    })
  }
}
</script>

结果可以看到:

clipboard.png

2.document.domain
这种方式只适合主域名相同,但子域名不同的iframe跨域。
比如主域名是http://crossdomain.com:9099,子域名是http://child.crossdomain.com:9099,这种情况下给两个页面指定一下document.domain即document.domain = crossdomain.com就可以访问各自的window对象了。

3.canvas操作图片的跨域问题
这个应该是一个比较冷门的跨域问题,张大神已经写过了我就不再班门弄斧了解决canvas图片getImageData,toDataURL跨域问题

最后

希望看完这篇文章之后,再有人问跨域的问题,你可以嘴角微微上扬,冷笑一声:“不要再问我跨域的问题了。”
扬长而去。

查看原文

前端森林 收藏了文章 · 2019-04-03

一篇文章教会你Event loop——浏览器和Node

最近对Event loop比较感兴趣,所以了解了一下。但是发现整个Event loop尽管有很多篇文章,但是没有一篇可以看完就对它所有内容都了解的文章。大部分的文章都只阐述了浏览器或者Node二者之一,没有对比的去看的话,认识总是浅一点。所以才有了这篇整理了百家之长的文章。

1. 定义

Event loop:为了协调事件(event),用户交互(user interaction),脚本(script),渲染(rendering),网络(networking)等,用户代理(user agent)必须使用事件循环(event loops)。(3月29修订)

那什么是事件?

事件:事件就是由于某种外在或内在的信息状态发生的变化,从而导致出现了对应的反应。比如说用户点击了一个按钮,就是一个事件;HTML页面完成加载,也是一个事件。一个事件中会包含多个任务。

我们在之前的文章中提到过,JavaScript引擎又称为JavaScript解释器,是JavaScript解释为机器码的工具,分别运行在浏览器和Node中。而根据上下文的不同,Event loop也有不同的实现:其中Node使用了libuv库来实现Event loop; 而在浏览器中,html规范定义了Event loop,具体的实现则交给不同的厂商去完成。

所以,浏览器的Event loop和Node的Event loop是两个概念,下面分别来看一下。

2. 意义

在实际工作中,了解Event loop的意义能帮助你分析一些异步次序的问题(当然,随着es7 async和await的流行,这样的机会越来越少了)。除此以外,它还对你了解浏览器和Node的内部机制有积极的作用;对于参加面试,被问到一堆异步操作的执行顺序时,也不至于两眼抓瞎。

3. 浏览器上的实现

在JavaScript中,任务被分为Task(又称为MacroTask,宏任务)和MicroTask(微任务)两种。它们分别包含以下内容:

MacroTask: script(整体代码), setTimeout, setInterval, setImmediate(node独有), I/O, UI rendering
MicroTask: process.nextTick(node独有), Promises, Object.observe(废弃), MutationObserver

需要注意的一点是:在同一个上下文中,总的执行顺序为同步代码—>microTask—>macroTask[6]。这一块我们在下文中会讲。

浏览器中,一个事件循环里有很多个来自不同任务源的任务队列(task queues),每一个任务队列里的任务是严格按照先进先出的顺序执行的。但是,因为浏览器自己调度的关系,不同任务队列的任务的执行顺序是不确定的。

具体来说,浏览器会不断从task队列中按顺序取task执行,每执行完一个task都会检查microtask队列是否为空(执行完一个task的具体标志是函数执行栈为空),如果不为空则会一次性执行完所有microtask。然后再进入下一个循环去task队列中取下一个task执行,以此类推。

图片描述

注意:图中橙色的MacroTask任务队列也应该是在不断被切换着的。

本段大批量引用了《什么是浏览器的事件循环(Event Loop)》的相关内容,想看更加详细的描述可以自行取用。

4. Node上的实现

nodejs的event loop分为6个阶段,它们会按照顺序反复运行,分别如下:

  1. timers:执行setTimeout() 和 setInterval()中到期的callback。
  2. I/O callbacks:上一轮循环中有少数的I/Ocallback会被延迟到这一轮的这一阶段执行
  3. idle, prepare:队列的移动,仅内部使用
  4. poll:最为重要的阶段,执行I/O callback,在适当的条件下会阻塞在这个阶段
  5. check:执行setImmediate的callback
  6. close callbacks:执行close事件的callback,例如socket.on("close",func)

不同于浏览器的是,在每个阶段完成后,而不是MacroTask任务完成后,microTask队列就会被执行。这就导致了同样的代码在不同的上下文环境下会出现不同的结果。我们在下文中会探讨。

另外需要注意的是,如果在timers阶段执行时创建了setImmediate则会在此轮循环的check阶段执行,如果在timers阶段创建了setTimeout,由于timers已取出完毕,则会进入下轮循环,check阶段创建timers任务同理。

图片描述

5. 示例

5.1 浏览器与Node执行顺序的区别

setTimeout(()=>{
    console.log('timer1')

    Promise.resolve().then(function() {
        console.log('promise1')
    })
}, 0)

setTimeout(()=>{
    console.log('timer2')

    Promise.resolve().then(function() {
        console.log('promise2')
    })
}, 0)



浏览器输出:
time1
promise1
time2
promise2

Node输出:
time1
time2
promise1
promise2

在这个例子中,Node的逻辑如下:

最初timer1和timer2就在timers阶段中。开始时首先进入timers阶段,执行timer1的回调函数,打印timer1,并将promise1.then回调放入microtask队列,同样的步骤执行timer2,打印timer2;
至此,timer阶段执行结束,event loop进入下一个阶段之前,执行microtask队列的所有任务,依次打印promise1、promise2。

而浏览器则因为两个setTimeout作为两个MacroTask, 所以先输出timer1, promise1,再输出timer2,promise2。

更加详细的信息可以查阅《深入理解js事件循环机制(Node.js篇)

为了证明我们的理论,把代码改成下面的样子:

setImmediate(() => {
  console.log('timer1')

  Promise.resolve().then(function () {
    console.log('promise1')
  })
})

setTimeout(() => {
  console.log('timer2')

  Promise.resolve().then(function () {
    console.log('promise2')
  })
}, 0)

Node输出:
timer1               timer2
promise1    或者     promise2
timer2               timer1
promise2             promise1

按理说setTimeout(fn,0)应该比setImmediate(fn)快,应该只有第二种结果,为什么会出现两种结果呢?
这是因为Node 做不到0毫秒,最少也需要1毫秒。实际执行的时候,进入事件循环以后,有可能到了1毫秒,也可能还没到1毫秒,取决于系统当时的状况。如果没到1毫秒,那么 timers 阶段就会跳过,进入 check 阶段,先执行setImmediate的回调函数。

另外,如果已经过了Timer阶段,那么setImmediate会比setTimeout更快,例如:

const fs = require('fs');

fs.readFile('test.js', () => {
  setTimeout(() => console.log(1));
  setImmediate(() => console.log(2));
});

上面代码会先进入 I/O callbacks 阶段,然后是 check 阶段,最后才是 timers 阶段。因此,setImmediate才会早于setTimeout执行。

具体可以看《Node 定时器详解》。

5.2 不同异步任务执行的快慢

setTimeout(() => console.log(1));
setImmediate(() => console.log(2));

Promise.resolve().then(() => console.log(3));
process.nextTick(() => console.log(4));


输出结果:4 3 1 2或者4 3 2 1

因为我们上文说过microTask会优于macroTask运行,所以先输出下面两个,而在Node中process.nextTick比Promise更加优先[3],所以4在3前。而根据我们之前所说的Node没有绝对意义上的0ms,所以1,2的顺序不固定。

5.3 MicroTask队列与MacroTask队列

   setTimeout(function () {
       console.log(1);
   },0);
   console.log(2);
   process.nextTick(() => {
       console.log(3);
   });
   new Promise(function (resolve, rejected) {
       console.log(4);
       resolve()
   }).then(res=>{
       console.log(5);
   })
   setImmediate(function () {
       console.log(6)
   })
   console.log('end');

Node输出:
2 4 end 3 5 1 6

这个例子来源于《JavaScript中的执行机制》。Promise的代码是同步代码,then和catch才是异步的,所以4要同步输出,然后Promise的then位于microTask中,优于其他位于macroTask队列中的任务,所以5会优于1,6输出,而Timer优于Check阶段,所以1,6。

6. 总结

综上,关于最关键的顺序,我们要依据以下几条规则:

  1. 同一个上下文下,MicroTask会比MacroTask先运行
  2. 然后浏览器按照一个MacroTask任务,所有MicroTask的顺序运行,Node按照六个阶段的顺序运行,并在每个阶段后面都会运行MicroTask队列
  3. 同个MicroTask队列下process.tick()会优于Promise

Event loop还是比较深奥的,深入进去会有很多有意思的东西,有任何问题还望不吝指出。

参考文档:

  1. 什么是浏览器的事件循环(Event Loop)
  2. 不要混淆nodejs和浏览器中的event loop
  3. Node 定时器详解
  4. 浏览器和Node不同的事件循环(Event Loop)
  5. 深入理解js事件循环机制(Node.js篇)
  6. JavaScript中的执行机制
查看原文

前端森林 收藏了文章 · 2019-04-03

一篇文章教会你Event loop——浏览器和Node

最近对Event loop比较感兴趣,所以了解了一下。但是发现整个Event loop尽管有很多篇文章,但是没有一篇可以看完就对它所有内容都了解的文章。大部分的文章都只阐述了浏览器或者Node二者之一,没有对比的去看的话,认识总是浅一点。所以才有了这篇整理了百家之长的文章。

1. 定义

Event loop:为了协调事件(event),用户交互(user interaction),脚本(script),渲染(rendering),网络(networking)等,用户代理(user agent)必须使用事件循环(event loops)。(3月29修订)

那什么是事件?

事件:事件就是由于某种外在或内在的信息状态发生的变化,从而导致出现了对应的反应。比如说用户点击了一个按钮,就是一个事件;HTML页面完成加载,也是一个事件。一个事件中会包含多个任务。

我们在之前的文章中提到过,JavaScript引擎又称为JavaScript解释器,是JavaScript解释为机器码的工具,分别运行在浏览器和Node中。而根据上下文的不同,Event loop也有不同的实现:其中Node使用了libuv库来实现Event loop; 而在浏览器中,html规范定义了Event loop,具体的实现则交给不同的厂商去完成。

所以,浏览器的Event loop和Node的Event loop是两个概念,下面分别来看一下。

2. 意义

在实际工作中,了解Event loop的意义能帮助你分析一些异步次序的问题(当然,随着es7 async和await的流行,这样的机会越来越少了)。除此以外,它还对你了解浏览器和Node的内部机制有积极的作用;对于参加面试,被问到一堆异步操作的执行顺序时,也不至于两眼抓瞎。

3. 浏览器上的实现

在JavaScript中,任务被分为Task(又称为MacroTask,宏任务)和MicroTask(微任务)两种。它们分别包含以下内容:

MacroTask: script(整体代码), setTimeout, setInterval, setImmediate(node独有), I/O, UI rendering
MicroTask: process.nextTick(node独有), Promises, Object.observe(废弃), MutationObserver

需要注意的一点是:在同一个上下文中,总的执行顺序为同步代码—>microTask—>macroTask[6]。这一块我们在下文中会讲。

浏览器中,一个事件循环里有很多个来自不同任务源的任务队列(task queues),每一个任务队列里的任务是严格按照先进先出的顺序执行的。但是,因为浏览器自己调度的关系,不同任务队列的任务的执行顺序是不确定的。

具体来说,浏览器会不断从task队列中按顺序取task执行,每执行完一个task都会检查microtask队列是否为空(执行完一个task的具体标志是函数执行栈为空),如果不为空则会一次性执行完所有microtask。然后再进入下一个循环去task队列中取下一个task执行,以此类推。

图片描述

注意:图中橙色的MacroTask任务队列也应该是在不断被切换着的。

本段大批量引用了《什么是浏览器的事件循环(Event Loop)》的相关内容,想看更加详细的描述可以自行取用。

4. Node上的实现

nodejs的event loop分为6个阶段,它们会按照顺序反复运行,分别如下:

  1. timers:执行setTimeout() 和 setInterval()中到期的callback。
  2. I/O callbacks:上一轮循环中有少数的I/Ocallback会被延迟到这一轮的这一阶段执行
  3. idle, prepare:队列的移动,仅内部使用
  4. poll:最为重要的阶段,执行I/O callback,在适当的条件下会阻塞在这个阶段
  5. check:执行setImmediate的callback
  6. close callbacks:执行close事件的callback,例如socket.on("close",func)

不同于浏览器的是,在每个阶段完成后,而不是MacroTask任务完成后,microTask队列就会被执行。这就导致了同样的代码在不同的上下文环境下会出现不同的结果。我们在下文中会探讨。

另外需要注意的是,如果在timers阶段执行时创建了setImmediate则会在此轮循环的check阶段执行,如果在timers阶段创建了setTimeout,由于timers已取出完毕,则会进入下轮循环,check阶段创建timers任务同理。

图片描述

5. 示例

5.1 浏览器与Node执行顺序的区别

setTimeout(()=>{
    console.log('timer1')

    Promise.resolve().then(function() {
        console.log('promise1')
    })
}, 0)

setTimeout(()=>{
    console.log('timer2')

    Promise.resolve().then(function() {
        console.log('promise2')
    })
}, 0)



浏览器输出:
time1
promise1
time2
promise2

Node输出:
time1
time2
promise1
promise2

在这个例子中,Node的逻辑如下:

最初timer1和timer2就在timers阶段中。开始时首先进入timers阶段,执行timer1的回调函数,打印timer1,并将promise1.then回调放入microtask队列,同样的步骤执行timer2,打印timer2;
至此,timer阶段执行结束,event loop进入下一个阶段之前,执行microtask队列的所有任务,依次打印promise1、promise2。

而浏览器则因为两个setTimeout作为两个MacroTask, 所以先输出timer1, promise1,再输出timer2,promise2。

更加详细的信息可以查阅《深入理解js事件循环机制(Node.js篇)

为了证明我们的理论,把代码改成下面的样子:

setImmediate(() => {
  console.log('timer1')

  Promise.resolve().then(function () {
    console.log('promise1')
  })
})

setTimeout(() => {
  console.log('timer2')

  Promise.resolve().then(function () {
    console.log('promise2')
  })
}, 0)

Node输出:
timer1               timer2
promise1    或者     promise2
timer2               timer1
promise2             promise1

按理说setTimeout(fn,0)应该比setImmediate(fn)快,应该只有第二种结果,为什么会出现两种结果呢?
这是因为Node 做不到0毫秒,最少也需要1毫秒。实际执行的时候,进入事件循环以后,有可能到了1毫秒,也可能还没到1毫秒,取决于系统当时的状况。如果没到1毫秒,那么 timers 阶段就会跳过,进入 check 阶段,先执行setImmediate的回调函数。

另外,如果已经过了Timer阶段,那么setImmediate会比setTimeout更快,例如:

const fs = require('fs');

fs.readFile('test.js', () => {
  setTimeout(() => console.log(1));
  setImmediate(() => console.log(2));
});

上面代码会先进入 I/O callbacks 阶段,然后是 check 阶段,最后才是 timers 阶段。因此,setImmediate才会早于setTimeout执行。

具体可以看《Node 定时器详解》。

5.2 不同异步任务执行的快慢

setTimeout(() => console.log(1));
setImmediate(() => console.log(2));

Promise.resolve().then(() => console.log(3));
process.nextTick(() => console.log(4));


输出结果:4 3 1 2或者4 3 2 1

因为我们上文说过microTask会优于macroTask运行,所以先输出下面两个,而在Node中process.nextTick比Promise更加优先[3],所以4在3前。而根据我们之前所说的Node没有绝对意义上的0ms,所以1,2的顺序不固定。

5.3 MicroTask队列与MacroTask队列

   setTimeout(function () {
       console.log(1);
   },0);
   console.log(2);
   process.nextTick(() => {
       console.log(3);
   });
   new Promise(function (resolve, rejected) {
       console.log(4);
       resolve()
   }).then(res=>{
       console.log(5);
   })
   setImmediate(function () {
       console.log(6)
   })
   console.log('end');

Node输出:
2 4 end 3 5 1 6

这个例子来源于《JavaScript中的执行机制》。Promise的代码是同步代码,then和catch才是异步的,所以4要同步输出,然后Promise的then位于microTask中,优于其他位于macroTask队列中的任务,所以5会优于1,6输出,而Timer优于Check阶段,所以1,6。

6. 总结

综上,关于最关键的顺序,我们要依据以下几条规则:

  1. 同一个上下文下,MicroTask会比MacroTask先运行
  2. 然后浏览器按照一个MacroTask任务,所有MicroTask的顺序运行,Node按照六个阶段的顺序运行,并在每个阶段后面都会运行MicroTask队列
  3. 同个MicroTask队列下process.tick()会优于Promise

Event loop还是比较深奥的,深入进去会有很多有意思的东西,有任何问题还望不吝指出。

参考文档:

  1. 什么是浏览器的事件循环(Event Loop)
  2. 不要混淆nodejs和浏览器中的event loop
  3. Node 定时器详解
  4. 浏览器和Node不同的事件循环(Event Loop)
  5. 深入理解js事件循环机制(Node.js篇)
  6. JavaScript中的执行机制
查看原文

前端森林 收藏了文章 · 2019-03-31

抽象语法树(Abstract Syntax Tree)

一般来说,程序中的一段源代码在执行之前会经历下面三个步骤
1 分词/词法分析
这个过程会将由字符组成的字符串分解成有意义的代码快,这些代码块被称为词法单元。例如 var a = 4;会被分解成 var、a、=、4、;

2 解析/语法分析
这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树,这个树就叫“抽象语法树”(AST)。全称 Abstract Syntax Tree。

3 代码生成
将AST转换成可执行代码的过程称为代码生成。抛开具体细节不讲,简单来说就是有某种方法可以将var a= 4; 的AST转化为一组机器指令,用来创建一个叫做a的变量,并将一个值储存在a中。

可能以上的这些听起来有些云里雾里。因为在平时写代码的时候,不关注这些也能写代码。但是多了解一些,就多一扇看到未知世界的窗口。你肯定使用过前端的很多工具插件,webpack,eslint啥的。你知道这些工具的核心都是通过抽象语法树这个概念来实现对代码的检查,分析,转换的吗?

抽象语法树的定义

In computer science, an abstract syntax tree (AST), or just syntax tree, is a tree representation of the abstract syntactic structure of source code written in a programming language.

翻译一下就是:
在计算机科学中,一个抽象语法树,或者词法树,是一个树,这个树表示或者说抽象出了编程语言的源代码的结构。

这个工具能在线可视化解析出一段代码的抽象语法树,可能能帮助你理解

这个可以进一步看更详尽的信息

下面举一个上边工具中的demo,看看js在执行之前的三步中的前两步都是具体的干了啥。

            var a = 42;
            var b = 5;
            function addA(d) {
            return a + d;
            }
            var c = addA(2) + b;

第一步,词法分析,以上代码词法分析之后长成如下图所示
image
第二步,语法分析,生产抽象语法树,生成的抽象语法树如下图所示
image

JavaScript Parser

JavaScript Parser 把js源码转化为抽象语法树的解析器。前边我们也说了。浏览器在执行js之前会把js源码通过解析器转化为抽象语法树,再进一步转化为字节码甚至是机器码。

常用的JavaScript Parser有:

使用Esprima生成并使用抽象语法树。

  1. 通过esprima将一个空函数的源码生成一棵AST树
  2. 通过estraverse遍历并更新AST树
  3. 通过escodegen将AST重新生成源码

抽象语法树的用途

其实从以上的三个工具,也可大致猜测到抽象语法树的一般用途了。大致分为几类吧

  1. IDE插件,如代码语法检查,代码风格检查,代码的格式化,代码高亮,代码错误等等之类的
  2. 代码的混淆压缩,如UglifyJS2等
  3. 转换代码的工具。如webpack,rollup,各种代码规范之间的转换,ts,jsx等转换为原生js
查看原文

前端森林 收藏了文章 · 2019-03-24

1.前端自动化测试 之 视觉测试

前端自动化测试 之 视觉测试

image

前端测试分类

5ad6b227e0634.png

前端测试主要分五大方向测试,而这五大方向也分很多小方向测试,首先简单的介绍每个方向的概念

  1. 界面样式测试

    • 固定界面样式测试:主要针对文字内容不变的区域,例如页面的页头,页脚这类结构、内容不变的区域,而测试一般通过截图对比解决。
    • 结构不变界面样式测试:主要针对结构不变的区域,例如新闻区域这类结构不变,内容变化的区域,这类测试一般通过DOM元素对比解决。
    • 计算样式测试:主要针对计算样式不变的区域,这类测试一般通过比较计算样式解决,但是这种测试不推荐,因为测试成本比较大。
  2. 功能测试

    • 服务器数据预期测试:主要针对用户在前端界面进行某种操作后,提交数据给后台后,测试后台能否返回预期的数据
    • 界面功能测试:主要针对用户在前端界面进行某种交互性操作后,测试能否获取预期的功能、界面交互
  3. 多浏览器测试

    • 多浏览器测试:基于界面样式测试、功能测试的基础上来进行不同浏览器的的测试,俗称兼容性测试。
  4. 性能测试

    • 白屏时间:用户浏览器输入网址后至浏览器出现至少1px画面为止。
    • 首屏时间:用户浏览器首屏内所有的元素呈现所花费时间。
    • 页面回归时间:用户浏览器非第一次加载所有的元素呈现所花费时间。
    • 用户可操作时间(dom ready) :网站某些功能可以使用的时间。
    • 页面总下载时间(onload):网站中所有资源加载完成并且可用时间。
  5. 质量测试

什么样的项目适合自动化测试呢?

5ae7f3fc745e7.png

如上图所示,真正工作中无法全部满足以上条件,所以需要作出权衡,一般来说,只需要满足以下几点,就可以对项目开展自动化测试(1.需求稳定
不会频繁变更。2.多平台运行,组合遍历型,大量的重复任务。3.软件维护周期长,有生命力。4.被测系统开发较为规范,可测试性强。):

  1. 需求稳定,不会频繁变更

    自动化测试最大的挑战就是需求的变化,而自动化脚本本身就需要修改、扩展、debug,去适应新的功能,如果投入产出比太低,那么自动化测试也失去了其价值和意义;

    折中的做法是选择相对稳定的模块和功能进行自动化测试,变动较大、需求变更较频繁的部分用手工测试;

  2. 多平台运行,组合遍历型、大量的重复任务

    测试数据、测试用例、自动化脚本的重用性和移植性较强,降低成本,提高效率和价值;

  3. 软件维护周期长,有生命力

    自动化测试的需求稳定性要求、自动化框架的设计、脚本开发与调试均需要时间,这其实也是一个软件开发过程,如果项目周期较短,没有足够的时间去支持这一过程,那自动化测试也就不需要了;

  4. 被测系统开发较为规范,可测试性强

    主要出于这几点考虑:被测试系统的架构差异、测试技术和工具的适应性、测试人员的能力能否设计开发出适应差异的自动化测试框架;

什么是UI测试

对于界面布局,传统的测试都是由人工对比设计图和产品界面。当界面有修改之后,再由人通过肉眼去检查修改(包括正确的和错误的修改),这样即费时而且测试结果又不稳定,因为人工对比测试存在两个巨坑:1.效率低;2.人的不确定性。对于拥有大量复杂界面的Web应用,界面布局的测试的数量巨大,再加上这两个问题,导致这类应用的界面布局测试/回归测试时间很长,成本很高,所以很多基于Agile(敏捷开发)项目基本不可能在迭代周期内高质量的完成其视觉测试。对于每天做一次,那更是不可能完成的任务。

视觉感知测试/视觉回归测试

为了解决上面提到的各种问题,视觉感知测试孕育而生。它使用传统的对图片进行二进制比较的办法,结合敏捷迭代开发的理念,产生的一种针对界面布局的自动化测试方法。

视觉感知测试

视觉感知测试就是对第一个版本的所有界面进行第一次测试。

视觉感知测试包含以下几个主要的测试步骤:

5ae86eda2a1cc.jpg

需要注意的是!

  1. 配对URL(忽略hostname)

    通过配对URL,对所有的截图按照相同的URL进行分组。当然有时候会出现新的界面,有时候老的界面会被删除。对于新的界面就需要人工进行首次验证测试 。

  2. 像素级别的图形比较

    对于分组之后的截图进行像素级别的比较并生产差别图。有时候为了降噪,可以只对局部关心的组件进行比较。

  3. 人工查看所有不同

    最后通过人工审查差别图报告完成测试。

视觉感知测试结果:

预期(expected)实际(actual)比较结果(diff)
1diff

视觉回归测试

我们认为如果一个界面通过第一次的人工验证并发布之后,它就是一个正确的标准界面,并且是包含了人工测试价值的资产。当下一次测试的时候,这部分价值就应该被保留并重用起来,用于减少新的一次测试的时间,从而实现界面的快速回归测试。

视觉回归测试包含以下几个主要的测试步骤:
5aebce31440f0.png

回归和感知测试流程差不多只是差异值要更小一点,并且只有效果图需要替换内容。

视觉自动测试怎么做?

要进行视觉自动测试,有三种方式。

  • 第一种是截屏比对(局部、整页)。
  • 第二种通过JavaScript调用window.getComputedStyle()获取计算后的样式并进行断言。
  • 第三种dom结构对比加css样式对比。

这三种各有明显的优势和不足。 第二种方式强绑定了实现,从而变得可能比较脆弱。 第一种方式离设计太近了,当页面中有可变内容时就会有问题。
第三种方式,无法进行视觉感知测试结果只能进行视觉回归测试和上一版的dom继续比较差异。

我更倾向与第一种截图对比;它的测试基于用户所见而不是用户所见的抽象。当然第三种也是非常好的 page-monitor 有兴趣的朋友可以自行了解。为什么第三种那么好为什么不使用呢?因为上面这个库是基于phantomjs并且它的实现方式过于复杂不适合新手玩玩。

像素对比工具,有哪些?

名称地址
PhantomCSShttps://github.com/HuddleEng/...
GhostStoryhttps://github.com/thingsinja...
Cactushttps://github.com/winston/ca...
Needlehttps://github.com/python-nee...
CSSCritichttps://github.com/cburgmer/c...
sikulihttp://www.sikuli.org/
Mogohttp://mogotest.com/
pixelmatchhttps://github.com/mapbox/pix...
pixel-diffhttps://github.com/koola/pixe...

好了介绍了那么多,怎么选一个合适的Headless Browser呢?

Headless Browser???我是视觉测试要无界面浏览器干嘛?

因为有了像素对比工具我们还需要一个浏览器进行截图和设计图进行像素比较。

比较常见出名的几个Headless Browser,有哪些?

名称内核地址
PuppeteerWebkithttps://github.com/GoogleChro...
PhantomJSWebkithttp://phantomjs.org/
SlimerJSGeckohttps://github.com/laurentj/s...
TrifleJSIEhttps://github.com/sdesalas/t...
PhantomJS 基于 Webkit 内核,不支持 Flash 的播放;SlimerJS 基于火狐的 Gecko 内核,支持 Flash播放,并且执行过程会有页面展示。

我们这里呢就只讲Webkit内核的,其他的我就不讲了。

PhantomJS简介:

PhantomJS 是一个基于webkit的JavaScript API。它使用QtWebKit作为它核心浏览器的功能,使用webkit来编译解释执行JavaScript代码。任何你可以在基于webkit浏览器做的事情,它都能做到。它不仅是个隐形的浏览器,提供了诸如CSS选择器、支持Web标准、DOM操作、JSON、HTML5、Canvas、SVG等,同时也提供了处理文件I/O的操作,从而使你可以向操作系统读写文件等。PhantomJS的用处可谓非常广泛,诸如网络监测、网页截屏、无需浏览器的 Web 测试、页面访问自动化等。

但是 PhantomJS 因为毕竟不是真实的用户浏览器环境,使用起来还是有不少的诟病。之前一直在使用 PhantomJS ,功能虽然够用,不过和在真实的浏览器里面访问的界面来对比差别还是比较大的。

Puppeteer简介:

Puppeteer是Chrome团队开发的一个Node库。它提供了一个高级API来控制无头或完整的Chrome。它通过使用Chrome无界面模式 (Headless Chrome )和DevTools协议的组合来实现这一点。它使用一个更上层的API来封装其功能,让用户界面测试自动化变得轻而易举。
人们基于Chrome DevTools协议开发了一系列Google Chrome工具。你在浏览器中点击更多工具 ->开发工具,打开的就是DevTools。DevTools协议是DevTools的动力基础,我们现在可以使用Chrome中的DevTools来做更多的事情。

好了简介讲完了,我们来对比一下这两个Headless Browser的区别。

截图比较
代码:

PhantomJS:

var page = require('webpage').create();

page.viewportSize = { width: 400, height: 400 };
page.open("http://localhost:8899/VS", function(status) {
    if (status === "success") {
        page.render("a.jpg");

    } else {
        console.log("Page failed to load.");
    }
    phantom.exit(0);
});

Puppeteer:

const puppeteer = require('puppeteer');

(async() => {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    await page.goto('http://localhost:8899/VS');

    await page.setViewport({ width: 400, height: 400 })

    //保存图片
    const images = await page.screenshot({ path: 'VS.jpg', fullPage: true, omitBackground: true });

    //关闭浏览器
    await browser.close();

})();
PuppeteerPhantomJSChrome浏览器
5ab35f3ecea48.jpg5ab23225073a2.jpg5ab361e464d76.png

浏览器效果(大图):

5ab232251f25b.png

好了看到这里已经可以分别出来胜负了。

使用puppeteer进行视觉感知测试

我们来做一个简单的dome

我们这里拿掘金来做一个视觉感知测试的例子。

1.首先创建项目名字就叫“PerceptionTest”把。

2.安装依赖

安装Puppeteer

npm install puppeteer --save

像素对比工具我就选我最常用的blink-dif了

npm install blink-diff --save

3.依赖安完了我们来创建一个js文件“app.js”

4.加载依赖

const puppeteer = require('puppeteer'),//无头浏览器
    BlinkDiff = require('blink-diff'),//像素对比
    imgUrl = __dirname + "/blink-diff_img/";//图片目录

5.使用puppeteer进行截图

(async () => {
    //创建puppeteer
    const browser = await puppeteer.launch({ headless: true });
    //new 一个新的tab页面
    const page = await browser.newPage();
    //设置浏览器的尺寸
    await page.setViewport({ width: 1920, height: 945 });
    //打开url
    await page.goto('https://juejin.im/');
    //保存截图
   await page.screenshot({ path: imgUrl + 'Screenshots.png', fullPage: true });
  
    //关闭浏览器
    await browser.close();
})();

6.分析页面找到要替换的内容

为什么要替换内容呢,因为我们UI测试指的是测试界面样式而不是去匹配里面的内容,如果不替换里面的内容那像素对比工具比较出来的相似度肯定很低。所以我们要替换掉内容,要让内容完整统一,我们才好更加精确的去比较差异。

好了我们来分析一下要替换的内容。

5aeb1fa0de449.png

要替换的内容如下:

  1. 标题
  2. 标签
  3. 作者
  4. 发布时间
  5. 阅读数
  6. 列表图片

7.替换内容

puppeteer提供了非常丰富的api,其中有个api叫page.evaluate可以向页面插入一段js。

 await page.evaluate(async () => {

        //列表
        var Lists = document.querySelectorAll("div.feed.welcome__feed > ul > li > div > a > div");
        Lists.forEach(function (element, index, array) {

            element.querySelector("a.title").innerHTML = "测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试";

            //替换标签
            element.querySelector("ul > li.item.category > span").innerHTML = "测试";

            //替换作者
            element.querySelector("ul > li.item.username.clickable > div > a").innerHTML = "测试";

            //替换发布时间
            element.querySelector("div.info-row.meta-row > ul > li:nth-child(3)").innerHTML = "9999天前";

            //替换发布时间
            element.querySelector("div.info-row.meta-row > ul > li:nth-child(4)").innerHTML = "99999999999 次阅读";

            //列表图片
            if (element.querySelectorAll("div.lazy.thumb.thumb.loaded").length==1) {
                element.querySelector("div.lazy.thumb.thumb.loaded").style.background = "#fdedc9";
            } else {
                var loaded=document.createElement("div");
                loaded.className=" lazy thumb thumb loaded";
                loaded.style.background = "#fdedc9";
                loaded.setAttribute("data-v-b2db8566","");
                loaded.setAttribute("data-v-009ea7bb","");
                loaded.setAttribute("data-v-f2ca14b0","");
                element.appendChild(loaded);
            }
        });

    });

8.使用Blink-Diff进行像素对比较

const diff = new BlinkDiff({
        imageAPath: imgUrl + 'example.png', // 设计图
        imageBPath: imgUrl + 'Screenshots.png',//页面截图
        //低于其中差异的像素数/ p(默认值:500) - 百分比阈值:1 = 100%,0.2 = 20%
        threshold: 0.02, // 1% threshold
        imageOutputPath: imgUrl + 'Diff.png'//Diff路径
    });

    diff.run(function (error, result) {
        if (error) {
            throw error;
        } else {
            console.log(diff.hasPassed(result.code) ? '通过' : '失败');
            console.log('总像素:' + result.dimension);
            console.log('发现:' + result.differences + ' 差异.');
        }
    });

完整代码:

const puppeteer = require('puppeteer'),
    BlinkDiff = require('blink-diff'),
    imgUrl = __dirname + "/blink-diff_img/";

(async () => {
    const browser = await puppeteer.launch({ headless: true });
    const page = await browser.newPage();
    await page.setViewport({ width: 1920, height: 945 });
    await page.goto('https://juejin.im/');



    await page.evaluate(async () => {

        //列表
        var Lists = document.querySelectorAll("div.feed.welcome__feed > ul > li > div > a > div");
        Lists.forEach(function (element, index, array) {

            element.querySelector("a.title").innerHTML = "测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试";

            //替换标签
            element.querySelector("ul > li.item.category > span").innerHTML = "测试";

            //替换作者
            element.querySelector("ul > li.item.username.clickable > div > a").innerHTML = "测试";

            //替换发布时间
            element.querySelector("div.info-row.meta-row > ul > li:nth-child(3)").innerHTML = "9999天前";

            //替换发布时间
            element.querySelector("div.info-row.meta-row > ul > li:nth-child(4)").innerHTML = "99999999999 次阅读";

            //列表图片
            if (element.querySelectorAll("div.lazy.thumb.thumb.loaded").length==1) {
                element.querySelector("div.lazy.thumb.thumb.loaded").style.background = "#fdedc9";
            } else {
                var loaded=document.createElement("div");
                loaded.className=" lazy thumb thumb loaded";
                loaded.style.background = "#fdedc9";
                loaded.setAttribute("data-v-b2db8566","");
                loaded.setAttribute("data-v-009ea7bb","");
                loaded.setAttribute("data-v-f2ca14b0","");
                element.appendChild(loaded);
            }
        });

    });

    await page.screenshot({ path: imgUrl + 'Screenshots.png', fullPage: true });

    const diff = new BlinkDiff({
        imageAPath: imgUrl + 'example.png', // 设计图
        imageBPath: imgUrl + 'Screenshots.png',//页面截图
        threshold: 0.02, // 1% threshold
        imageOutputPath: imgUrl + 'Diff.png'//Diff路径
    });

    diff.run(function (error, result) {
        if (error) {
            throw error;
        } else {
            console.log(diff.hasPassed(result.code) ? '通过' : '失败');
            console.log('总像素:' + result.dimension);
            console.log('发现:' + result.differences + ' 差异.');
        }
    });


    //关闭puppeteer
    await browser.close();
})();

差异图

有差异无差异
5aeb373a1fa34.png5aeb3773ec472.png

git完整代码

git完整代码:https://github.com/my07ke/Per...

好了,欲知后事如何,请听下回分解。

5aebd8bbb1762.gif

点击链接加入群聊【前端|WEB|CSS|Javascript|HTML】:https://jq.qq.com/?_wv=1027&k...

参考:

浅谈UI自动化测试

视觉感知测试

查看原文

前端森林 收藏了文章 · 2019-03-03

React Native面试知识点

本文原创首发于公众号:ReactNative开发圈,转载需注明出处。

本文会不定期不断更新,想查看最新版本请移步至https://github.com/forrest23/...

1.React Native相对于原生的ios和Android有哪些优势?

1.性能媲美原生APP
2.使用JavaScript编码,只要学习这一种语言
3.绝大部分代码安卓和IOS都能共用
4.组件式开发,代码重用性很高
5.跟编写网页一般,修改代码后即可自动刷新,不需要慢慢编译,节省很多编译等待时间
6.支持APP热更新,更新无需重新安装APP

缺点:
内存占用相对较高
版本还不稳定,一直在更新,现在还没有推出稳定的1.0版本

2.React Native组件的生命周期

生命周期 调用次数 能否使用 setSate()
getDefaultProps 1(全局调用一次) 否
getInitialState 1 否
componentWillMount 1 是
render >=1 否
componentDidMount 1 是
componentWillReceiveProps >=0 是
shouldComponentUpdate >=0 否
componentWillUpdate >=0 否
componentDidUpdate >=0 否
componentWillUnmount 1 否

3.当你调用setState的时候,发生了什么事?

当调用 setState 时,React会做的第一件事情是将传递给 setState 的对象合并到组件的当前状态。
这将启动一个称为和解(reconciliation)的过程。
和解(reconciliation)的最终目标是以最有效的方式,根据这个新的状态来更新UI。
为此,React将构建一个新的 React 元素树(您可以将其视为 UI 的对象表示)。
一旦有了这个树,为了弄清 UI 如何响应新的状态而改变,React 会将这个新树与上一个元素树相比较( diff )。
通过这样做, React 将会知道发生的确切变化,并且通过了解发生什么变化,只需在绝对必要的情况下进行更新即可最小化 UI 的占用空间。

4.props和state相同点和不同点

1.不管是props还是state的改变,都会引发render的重新渲染。
2.都能由自身组件的相应初始化函数设定初始值。

不同点
1.初始值来源:state的初始值来自于自身的getInitalState(constructor)函数;props来自于父组件或者自身getDefaultProps(若key相同前者可覆盖后者)。

2.修改方式:state只能在自身组件中setState,不能由父组件修改;props只能由父组件修改,不能在自身组件修改。

3.对子组件:props是一个父组件传递给子组件的数据流,这个数据流可以一直传递到子孙组件;state代表的是一个组件内部自身的状态,只能在自身组件中存在。

5.shouldComponentUpdate 应该做什么

其实这个问题也是跟reconciliation有关系。
“和解( reconciliation )的最终目标是以最有效的方式,根据新的状态更新用户界面”。
如果我们知道我们的用户界面(UI)的某一部分不会改变,
那么没有理由让 React 很麻烦地试图去弄清楚它是否应该渲染。
通过从 shouldComponentUpdate 返回 false,
React 将假定当前组件及其所有子组件将保持与当前组件相同

6.reactJS的props.children.map函数来遍历会收到异常提示,为什么?应该如何遍历?

this.props.children 的值有三种可能:

1.当前组件没有子节点,它就是 undefined;
2.有一个子节点,数据类型是 object ;
3.有多个子节点,数据类型就是 array 。

系统提供React.Children.map()方法安全的遍历子节点对象

7.redux状态管理的流程


action是用户触发或程序触发的一个普通对象。
reducer是根据action操作来做出不同的数据响应,返回一个新的state。
store的最终值就是由reducer的值来确定的。(一个store是一个对象, reducer会改变store中的某些值)
action -> reducer -> 新store -> 反馈到UI上有所改变。

8.加载bundle的机制

要实现RN的脚本热更新,我们要搞明白RN是如何去加载脚本的。 在编写业务逻辑的时候,我们会有许多个js文件,打包的时候RN会将这些个js文件打包成一个叫index.android.bundle(ios的是index.ios.bundle)的文件,所有的js代码(包括rn源代码、第三方库、业务逻辑的代码)都在这一个文件里,启动App时会第一时间加载bundle文件,所以脚本热更新要做的事情就是替换掉这个bundle文件。

9.Flex布局

采用Flex布局的元素,称为Flex容器(flex Container),简称"容器"。它的所有子元素自动成为容器成员,称为Flex项目(flex item),简称"项目"。

容器默认存在两根轴:水平的主轴(main axis)和垂直的交叉轴(cross axis)。主轴的开始位置(与边框的交叉点)叫做main start,结束位置叫做main end;交叉轴的开始位置叫做cross start,结束位置叫做cross end。

项目默认沿主轴排列。单个项目占据的主轴空间叫做main size,占据的交叉轴空间叫做cross size。

容器的属性
以下6个属性设置在容器上。
flex-direction 属性决定主轴的方向(即项目的排列方向)。
flex-wrap 属性定义,如果一条轴线排不下,如何换行。
flex-flow flex-flow属性是flex-direction属性和flex-wrap属性的简写形式。
justify-content 定义了项目在主轴上的对齐方式。
align-items 属性定义项目在交叉轴上如何对齐。
align-content align-content属性定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用。

10.请简述 code push 的原理

code push 调用 react native 的打包命令,将当前环境的非 native 代码全量打包成一个 bundle 文件,然后上传到微软云服务器(Windows Azure)。 在 app 中启动页(或 splash 页)编写请求更新的代码(请求包含了本地版本,hashCode、appToken 等信息),微软服务端对比本地 js bundle 版本和微软服务器的版本,如果本地版本低,就下载新的 js bundle 下来后实现更新(code push 框架实现)。

11.Redux中同步 action 与异步 action 最大的区别是什么

同步只返回一个普通 action 对象。而异步操作中途会返回一个 promise 函数。当然在 promise 函数处理完毕后也会返回一个普通 action 对象。thunk 中间件就是判断如果返回的是函数,则不传导给 reducer,直到检测到是普通 action 对象,才交由 reducer 处理。

本文会不定期不断更新,想查看最新版本请移步至https://github.com/forrest23/...

本文题目节选自互联网,如有侵权请联系我!

欢迎关注我的微信公众号:ReactNative开发圈

查看原文

前端森林 收藏了文章 · 2019-02-14

[NodeJs系列][译]理解NodeJs中的Event Loop、Timers以及process.nextTick()

译者注:

  1. 为什么要翻译?其实在翻译这篇文章前,笔者有Google了一下中文翻译,看的不是很明白,所以才有自己翻译的打算,当然能力有限,文中或有错漏,欢迎指正。
  2. 文末会有几个小问题,大家不妨一起思考一下
  3. 如果你对NodeJs系列感兴趣,欢迎关注微信公众号:前端神盾局或 github NodeJs系列文章

什么是Event Loop?

尽管JavaScript是单线程的,通过Event Loop使得NodeJs能够尽可能的通过卸载I/O操作到系统内核,来实现非阻塞I/O的功能。

由于大部分现代系统内核都是多线程的,因此他们可以在后台执行多个操作。当这些操作中的某一个完成后,内核便会通知NodeJs,这样(这个操作)指定的回调就会添加到poll队列以便最终执行。关于这个我们会在随后的章节中进一步说明。

Event Loop解析

当NodeJs启动时,event loop 随即会被初始化,而后会执行对应的输入脚本(直接把脚本放入REPL执行不在本文讨论范围内),这个过程中(脚本的执行)可能会存在对异步API的调用,产生定时器或者调用process.nextTick(),接着开始event loop。

译者注:这段话的意思是NodeJs优先执行同步代码,在同步代码的执行过程中可能会调用到异步API,当同步代码和process.nextTick()回调执行完成后,就会开始event loop

下图简要的概述了event loop的操作顺序:

   ┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<─────┤  connections, │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘
注:每一个框代表event loop中的一个阶段

每个阶段都有一个FIFO(先进先出)的回调队列等待执行。虽然每个阶段都有其独特之处,但总体而言,当event loop进入到指定阶段后,它会执行该阶段的任何操作,并执行对应的回调直到队列中没有可执行回调或者达到回调执行上限,而后event loop会进入下一阶段。

由于任何这些阶段的操作可能产生更多操作,内核也会将新的事件推入到poll阶段的队列中,所以新的poll事件被允许在处理poll事件时继续加入队,这也意味着长时间运行的回调可以允许poll阶段运行的时间比计时器的阈值要长

注意:Windows和Unix/Linux在实现上有些差别,但这对本文并不重要。事实上存在7到8个步骤,但以上列举的是Node.js中实际使用的。

阶段概览

  • timers:执行的是setTimeout()setInterval()的回调
  • I/O callbacks:执行除了 close callbacks、定时器回调和setImmediate()设定的回调之外的几乎所有回调
  • idle, prepare:仅内部使用
  • poll:接收新的I/O事件,适当时node会阻塞在这里(==什么情况下是适当的?==)
  • checksetImmediate回调在这里触发
  • close callbacks:比如socket.on('close', ...)

在每次执行完event loop后,Node.js都会检查是否还有需要等待的I/O或者定时器没有处理,如果没有那么进程退出。

阶段细节

timers

一个定时器会指定阀值,并在达到阀值之后执行给定的回调,但通常来说这个阀值会超过我们预期的时间。定时器回调会尽可能早的执行,不过操作系统的调度和其他回调的执行时间会造成一定的延时。

注:严格意义上说,定时器什么时候执行取决于poll阶段

举个例子,假定一个定时器给定的阀值是100ms,异步读取文件需要95ms的时间

const fs = require('fs');

function someAsyncOperation(callback) {
  // 假定这里花费了95ms
  fs.readFile('/path/to/file', callback);
}

const timeoutScheduled = Date.now();

setTimeout(function() {

  const delay = Date.now() - timeoutScheduled;

  console.log(delay + 'ms have passed since I was scheduled');
}, 100);


// 95ms后异步操作才完成
someAsyncOperation(function() {

  const startCallback = Date.now();

  // 这里花费了10ms
  while (Date.now() - startCallback < 10) {
    // do nothing
  }
});

就本例而言,当event loop到达poll阶段,它的队列是空的(fs.readFile()还未完成),因此它会停留在这里直到达到最早的定时器阀值。fs.readFile()
花费了95ms读取文件,之后它的回调被推入poll队列并执行(执行花了10ms)。回调执行完毕后,队列中已经没有其他回调需要执行了,那么event loop就会去检查是否有定时器的回调可以执行,如果有就跳回到timer阶段执行相应回调。在本例中,你可以看到从定时器被调用到其回调被执行一共耗时105ms。

注:为了防止event loop一直阻塞在poll阶段,libuv(http://libuv.org/ 这是用c语言实现了Node.js event loop以及各个平台的异步行为的库)会指定一个硬性的最大值以阻止更多的事件被推入poll。

I/O callbacks阶段

这个阶段用于执行一些系统操作的回调,比如TCP错误。举个例子,当一个TCP socket 在尝试连接时接收到ECONNREFUSED的错误,一些*nix系统会想要得到这些错误的报告,而这都会被推到 I/O callbacks中执行。

poll阶段

poll阶段有两个功能:

  1. 执行已经达到阀值的定时器脚本
  2. 处理在poll队列中的事件

当event loop进入到poll阶段且此代码中为设定定时器,将会发生下面情况:

  1. 如果poll队列非空,event loop会遍历执行队列中的回调函数直到队列为空或达到系统上限
  2. 如果poll队列是空的,将会发生下面情况:

    • 如果脚本中存在对setImmediate()的调用,event loop将会结束poll阶段进入check阶段并执行这些已被调度的代码
    • 如果脚本中不存在对setImmediate()的调用,那么event loop将阻塞在这里直到有回调被添加进来,新加的回调将会被立即执行

一旦poll队列为空,event loop就会检查是否有定时器达到阀值,如果有1个或多个定时器符合要求,event loop将将会回到timers阶段并执行改阶段的回调.

check阶段

一旦poll阶段完成,本阶段的回调将被立即执行。如果poll阶段处于空闲状态并且脚本中有执行了setImmediate(),那么event loop会跳过poll阶段的等待进入本阶段。

实际上setImmediate()是一个特殊的定时器,它在事件循环的一个单独阶段运行,它使用libuv API来调度执行回调。

通常而言,随着代码的执行,event loop最终会进入poll阶段并在这里等待新事件的到来(例如新的连接和请求等等)。但是,如果存在setImmediate()的回调并且poll阶段是空闲的,那么event loop就会停止在poll阶段漫无目的的等等直接进入check阶段。

close callbacks阶段

如果一个socket或者handle突然关闭(比如:socket.destory()),close事件就会被提交到这个阶段。否则它将会通过process.nextTick()触发

setImmediate() 和 setTimeout()

setImmediatesetTimeout()看起来是比较相似,但它们有不同的行为,这取决于它们什么时候被调用。

  • setImmediate() 被设计成一旦完成poll阶段就会被立即调用
  • setTimeout() 则是在达到最小阀值是才会被触发执行

其二者的调用顺序取决于它们的执行上下文。如果两者都在主模块被调用,那么其回调被执行的时间点就取决于处理过程的性能(这可能被运行在同一台机器上的其他应用影响)

比如说,如果下列脚本不是在I/O循环中运行,这两种定时器运行的顺序是不一定的(==这是为什么?==),这取决于处理过程的性能:

// timeout_vs_immediate.js
setTimeout(function timeout() {
  console.log('timeout');
}, 0);

setImmediate(function immediate() {
  console.log('immediate');
});
$ node timeout_vs_immediate.js
timeout
immediate

$ node timeout_vs_immediate.js
immediate
timeout

但是如果你把上面的代码置于I/O循环中,setImmediate回调会被优先执行:

// timeout_vs_immediate.js
const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});
$ node timeout_vs_immediate.js
immediate
timeout

$ node timeout_vs_immediate.js
immediate
timeout

使用setImmediate()而不是setTimeout()的主要好处是:如果代码是在I/O循环中调用,那么setImmediate()总是优先于其他定时器(无论有多少定时器存在)

process.nextTick()

理解process.nextTick()

你可能已经注意到process.nextTick()不在上面的图表中,即使它也是异步api。这是因为严格意义上来说process.nextTick()不属于event loop中的一部分,它会忽略event loop当前正在执行的阶段,而直接处理nextTickQueue中的内容。

回过头看一下图表,你在任何给定阶段调用process.nextTick(),在继续event loop之前,所有传入process.nextTick()的回调都会被执行。这可能会导致一些不好的情况,因为它允许你递归调用process.nextTick()从而使得event loop无法进入poll阶段,导致无法接收到新的 I/O事件

为什么这会被允许?

那为什么像这样的东西会被囊括在Node.js?部分由于Node.js的设计理念:API应该始终是异步的即使有些地方是没必要的。举个例子:

function apiCall(arg, callback) {
  if (typeof arg !== 'string')
    return process.nextTick(callback,
                            new TypeError('argument should be string'));
}

这是一段用于参数校验的代码,如果参数不正确就会把错误信息传递到回调。最近process.nextTick()有进行一些更新,使得我们可以传递多个参数到回调中而不用嵌套多个函数。

我们(在这个例子)所做的是在保证了其余(同步)代码的执行完成后把错误传递给用户。通过使用process.nextTick()我们可以确保apiCall()的回调总是在其他(同步)代码运行完成后event loop开始前调用的。为了实现这一点,JS调用栈被展开(==什么是栈展开?==)然后立即执行提供的回调,那我们就可以对process.nextTick进行递归(==怎么做到的?==)调用而不会触发RangeError: Maximum call stack size exceeded from v8的错误。

这种理念可能会导致一些潜在的问题。比如:

let bar;

// this has an asynchronous signature, but calls callback synchronously
function someAsyncApiCall(callback) { callback(); }

// the callback is called before `someAsyncApiCall` completes.
someAsyncApiCall(() => {

  // since someAsyncApiCall has completed, bar hasn't been assigned any value
  console.log('bar', bar); // undefined

});

bar = 1;

用户定义了一个异步签名的函数someAsyncApiCall()(函数名可以看出),但实际上操作是同步的。当它被调用时,其回调也在event loop中的同一阶段被调用了,因为someAsyncApiCall()实际上并没有任何异步动作。结果,在(同步)代码还没有全部执行的时候,回调就尝试去访问变量bar

通过把回调置于process.nextTick(),脚本就能完整运行(同步代码全部执行完毕),这就使得变量、函数等可以先于回调执行。同时它也有阻止event loop继续执行的好处。有时候我们可能希望在event loop继续执行前抛出一个错误,这种情况下process.nextTick()变的很有用。下面是对上一个例子的process.nextTick()改造:

let bar;

function someAsyncApiCall(callback) {
  process.nextTick(callback);
}

someAsyncApiCall(() => {
  console.log('bar', bar); // 1
});

bar = 1;

这是一个实际的例子:

const server = net.createServer(() => {}).listen(8080);

server.on('listening', () => {});

当只有一个端口作为参数传入,端口会被立即绑定。所以监听回调可能被立即调用。问题是:on('listening') 回调在那时还没被注册。

为了解决这个问题,把listening事件加入到nextTick() 队列中以允许脚本先执行完(同步代码)。这允许用户(在同步代码中)设置任何他们需要的事件处理函数。

process.nextTick() 和 setImmediate()

对于用户而言,这两种叫法是很相似的但它们的名字又让人琢磨不透。

  • process.nextTick() 会在同一个阶段执行
  • setImmediate() 会在随后的迭代中执行

本质上,这两个的名字应该互换一下,process.nextTick()setImmediate()更接近于立即,但是由于历史原因这不太可能去改变。名字互换可能影响大部分的npm包,每天都有大量的包在提交,这意味这越到后面,互换造成的破坏越大。所以即使它们的名字让人困惑也不可能被改变。

我们建议开发者在所有情况中使用setImmediate(),因为这可以让你的代码兼容更多的环境比如浏览器。

为什么要使用process.nextTick()?

这里又两个主要的原因:

  1. 让开发者处理错误、清除无用的资源或者在event loop继续之前再次尝试重新请求资源
  2. 有时需要允许回调在调用栈展开之后但在事件循环继续之前运行

下面这个例子会满足我们的期望:

const server = net.createServer();
server.on('connection', function(conn) { });

server.listen(8080);
server.on('listening', function() { });

假设listen()是在event loop开始前运行,但是监听回调是包裹在setImmediate中,除非指定hostname参数否则端口将被立即绑定(listening回调被触发),event loop必须要执行到poll阶段才会去处理,这意味着存在一种可能:在listening事件的回调执行前就收到了一个连接,也就是相当于先于listening 触发了connection事件。

另一个例子是运行一个继承至EventEmitter的构造函数,而这个构造函数中会发布一个事件。

const EventEmitter = require('events');
const util = require('util');

function MyEmitter() {
  EventEmitter.call(this);
  this.emit('event');
}
util.inherits(MyEmitter, EventEmitter);

const myEmitter = new MyEmitter();
myEmitter.on('event', function() {
  console.log('an event occurred!');
});

你无法立即从构造函数中真正触发事件,因为脚本还没有运行到用户为该事件分配回调的位置。因此,在构造函数中,您可以使用 process.nextTick() 来设置回调以在构造函数完成后发出事件,从而提供预期的结果

const EventEmitter = require('events');
const util = require('util');

function MyEmitter() {
  EventEmitter.call(this);

  // use nextTick to emit the event once a handler is assigned
  process.nextTick(function() {
    this.emit('event');
  }.bind(this));
}
util.inherits(MyEmitter, EventEmitter);

const myEmitter = new MyEmitter();
myEmitter.on('event', function() {
  console.log('an event occurred!');
});

译者注(Q&A)

翻译完本文,笔者给自己提了几个问题?

  1. poll阶段什么时候会被阻塞?
  2. 为什么在非I/O循环中,setTimeoutsetImmediate的执行顺序是不一定的?
  3. JS调用栈展开是什么意思?
  4. 为什么process.nextTick()可以被递归调用?

笔者将在之后的文章《Q&A之理解NodeJs中的Event Loop、Timers以及process.nextTick()》探讨这些问题,有兴趣的同学可以关注笔者的公众号: 前端情报局-NodeJs系列获取最新情报

原文地址: https://github.com/nodejs/nod...

image

查看原文

前端森林 赞了文章 · 2019-02-14

[NodeJs系列][译]理解NodeJs中的Event Loop、Timers以及process.nextTick()

译者注:

  1. 为什么要翻译?其实在翻译这篇文章前,笔者有Google了一下中文翻译,看的不是很明白,所以才有自己翻译的打算,当然能力有限,文中或有错漏,欢迎指正。
  2. 文末会有几个小问题,大家不妨一起思考一下
  3. 如果你对NodeJs系列感兴趣,欢迎关注微信公众号:前端神盾局或 github NodeJs系列文章

什么是Event Loop?

尽管JavaScript是单线程的,通过Event Loop使得NodeJs能够尽可能的通过卸载I/O操作到系统内核,来实现非阻塞I/O的功能。

由于大部分现代系统内核都是多线程的,因此他们可以在后台执行多个操作。当这些操作中的某一个完成后,内核便会通知NodeJs,这样(这个操作)指定的回调就会添加到poll队列以便最终执行。关于这个我们会在随后的章节中进一步说明。

Event Loop解析

当NodeJs启动时,event loop 随即会被初始化,而后会执行对应的输入脚本(直接把脚本放入REPL执行不在本文讨论范围内),这个过程中(脚本的执行)可能会存在对异步API的调用,产生定时器或者调用process.nextTick(),接着开始event loop。

译者注:这段话的意思是NodeJs优先执行同步代码,在同步代码的执行过程中可能会调用到异步API,当同步代码和process.nextTick()回调执行完成后,就会开始event loop

下图简要的概述了event loop的操作顺序:

   ┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<─────┤  connections, │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘
注:每一个框代表event loop中的一个阶段

每个阶段都有一个FIFO(先进先出)的回调队列等待执行。虽然每个阶段都有其独特之处,但总体而言,当event loop进入到指定阶段后,它会执行该阶段的任何操作,并执行对应的回调直到队列中没有可执行回调或者达到回调执行上限,而后event loop会进入下一阶段。

由于任何这些阶段的操作可能产生更多操作,内核也会将新的事件推入到poll阶段的队列中,所以新的poll事件被允许在处理poll事件时继续加入队,这也意味着长时间运行的回调可以允许poll阶段运行的时间比计时器的阈值要长

注意:Windows和Unix/Linux在实现上有些差别,但这对本文并不重要。事实上存在7到8个步骤,但以上列举的是Node.js中实际使用的。

阶段概览

  • timers:执行的是setTimeout()setInterval()的回调
  • I/O callbacks:执行除了 close callbacks、定时器回调和setImmediate()设定的回调之外的几乎所有回调
  • idle, prepare:仅内部使用
  • poll:接收新的I/O事件,适当时node会阻塞在这里(==什么情况下是适当的?==)
  • checksetImmediate回调在这里触发
  • close callbacks:比如socket.on('close', ...)

在每次执行完event loop后,Node.js都会检查是否还有需要等待的I/O或者定时器没有处理,如果没有那么进程退出。

阶段细节

timers

一个定时器会指定阀值,并在达到阀值之后执行给定的回调,但通常来说这个阀值会超过我们预期的时间。定时器回调会尽可能早的执行,不过操作系统的调度和其他回调的执行时间会造成一定的延时。

注:严格意义上说,定时器什么时候执行取决于poll阶段

举个例子,假定一个定时器给定的阀值是100ms,异步读取文件需要95ms的时间

const fs = require('fs');

function someAsyncOperation(callback) {
  // 假定这里花费了95ms
  fs.readFile('/path/to/file', callback);
}

const timeoutScheduled = Date.now();

setTimeout(function() {

  const delay = Date.now() - timeoutScheduled;

  console.log(delay + 'ms have passed since I was scheduled');
}, 100);


// 95ms后异步操作才完成
someAsyncOperation(function() {

  const startCallback = Date.now();

  // 这里花费了10ms
  while (Date.now() - startCallback < 10) {
    // do nothing
  }
});

就本例而言,当event loop到达poll阶段,它的队列是空的(fs.readFile()还未完成),因此它会停留在这里直到达到最早的定时器阀值。fs.readFile()
花费了95ms读取文件,之后它的回调被推入poll队列并执行(执行花了10ms)。回调执行完毕后,队列中已经没有其他回调需要执行了,那么event loop就会去检查是否有定时器的回调可以执行,如果有就跳回到timer阶段执行相应回调。在本例中,你可以看到从定时器被调用到其回调被执行一共耗时105ms。

注:为了防止event loop一直阻塞在poll阶段,libuv(http://libuv.org/ 这是用c语言实现了Node.js event loop以及各个平台的异步行为的库)会指定一个硬性的最大值以阻止更多的事件被推入poll。

I/O callbacks阶段

这个阶段用于执行一些系统操作的回调,比如TCP错误。举个例子,当一个TCP socket 在尝试连接时接收到ECONNREFUSED的错误,一些*nix系统会想要得到这些错误的报告,而这都会被推到 I/O callbacks中执行。

poll阶段

poll阶段有两个功能:

  1. 执行已经达到阀值的定时器脚本
  2. 处理在poll队列中的事件

当event loop进入到poll阶段且此代码中为设定定时器,将会发生下面情况:

  1. 如果poll队列非空,event loop会遍历执行队列中的回调函数直到队列为空或达到系统上限
  2. 如果poll队列是空的,将会发生下面情况:

    • 如果脚本中存在对setImmediate()的调用,event loop将会结束poll阶段进入check阶段并执行这些已被调度的代码
    • 如果脚本中不存在对setImmediate()的调用,那么event loop将阻塞在这里直到有回调被添加进来,新加的回调将会被立即执行

一旦poll队列为空,event loop就会检查是否有定时器达到阀值,如果有1个或多个定时器符合要求,event loop将将会回到timers阶段并执行改阶段的回调.

check阶段

一旦poll阶段完成,本阶段的回调将被立即执行。如果poll阶段处于空闲状态并且脚本中有执行了setImmediate(),那么event loop会跳过poll阶段的等待进入本阶段。

实际上setImmediate()是一个特殊的定时器,它在事件循环的一个单独阶段运行,它使用libuv API来调度执行回调。

通常而言,随着代码的执行,event loop最终会进入poll阶段并在这里等待新事件的到来(例如新的连接和请求等等)。但是,如果存在setImmediate()的回调并且poll阶段是空闲的,那么event loop就会停止在poll阶段漫无目的的等等直接进入check阶段。

close callbacks阶段

如果一个socket或者handle突然关闭(比如:socket.destory()),close事件就会被提交到这个阶段。否则它将会通过process.nextTick()触发

setImmediate() 和 setTimeout()

setImmediatesetTimeout()看起来是比较相似,但它们有不同的行为,这取决于它们什么时候被调用。

  • setImmediate() 被设计成一旦完成poll阶段就会被立即调用
  • setTimeout() 则是在达到最小阀值是才会被触发执行

其二者的调用顺序取决于它们的执行上下文。如果两者都在主模块被调用,那么其回调被执行的时间点就取决于处理过程的性能(这可能被运行在同一台机器上的其他应用影响)

比如说,如果下列脚本不是在I/O循环中运行,这两种定时器运行的顺序是不一定的(==这是为什么?==),这取决于处理过程的性能:

// timeout_vs_immediate.js
setTimeout(function timeout() {
  console.log('timeout');
}, 0);

setImmediate(function immediate() {
  console.log('immediate');
});
$ node timeout_vs_immediate.js
timeout
immediate

$ node timeout_vs_immediate.js
immediate
timeout

但是如果你把上面的代码置于I/O循环中,setImmediate回调会被优先执行:

// timeout_vs_immediate.js
const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});
$ node timeout_vs_immediate.js
immediate
timeout

$ node timeout_vs_immediate.js
immediate
timeout

使用setImmediate()而不是setTimeout()的主要好处是:如果代码是在I/O循环中调用,那么setImmediate()总是优先于其他定时器(无论有多少定时器存在)

process.nextTick()

理解process.nextTick()

你可能已经注意到process.nextTick()不在上面的图表中,即使它也是异步api。这是因为严格意义上来说process.nextTick()不属于event loop中的一部分,它会忽略event loop当前正在执行的阶段,而直接处理nextTickQueue中的内容。

回过头看一下图表,你在任何给定阶段调用process.nextTick(),在继续event loop之前,所有传入process.nextTick()的回调都会被执行。这可能会导致一些不好的情况,因为它允许你递归调用process.nextTick()从而使得event loop无法进入poll阶段,导致无法接收到新的 I/O事件

为什么这会被允许?

那为什么像这样的东西会被囊括在Node.js?部分由于Node.js的设计理念:API应该始终是异步的即使有些地方是没必要的。举个例子:

function apiCall(arg, callback) {
  if (typeof arg !== 'string')
    return process.nextTick(callback,
                            new TypeError('argument should be string'));
}

这是一段用于参数校验的代码,如果参数不正确就会把错误信息传递到回调。最近process.nextTick()有进行一些更新,使得我们可以传递多个参数到回调中而不用嵌套多个函数。

我们(在这个例子)所做的是在保证了其余(同步)代码的执行完成后把错误传递给用户。通过使用process.nextTick()我们可以确保apiCall()的回调总是在其他(同步)代码运行完成后event loop开始前调用的。为了实现这一点,JS调用栈被展开(==什么是栈展开?==)然后立即执行提供的回调,那我们就可以对process.nextTick进行递归(==怎么做到的?==)调用而不会触发RangeError: Maximum call stack size exceeded from v8的错误。

这种理念可能会导致一些潜在的问题。比如:

let bar;

// this has an asynchronous signature, but calls callback synchronously
function someAsyncApiCall(callback) { callback(); }

// the callback is called before `someAsyncApiCall` completes.
someAsyncApiCall(() => {

  // since someAsyncApiCall has completed, bar hasn't been assigned any value
  console.log('bar', bar); // undefined

});

bar = 1;

用户定义了一个异步签名的函数someAsyncApiCall()(函数名可以看出),但实际上操作是同步的。当它被调用时,其回调也在event loop中的同一阶段被调用了,因为someAsyncApiCall()实际上并没有任何异步动作。结果,在(同步)代码还没有全部执行的时候,回调就尝试去访问变量bar

通过把回调置于process.nextTick(),脚本就能完整运行(同步代码全部执行完毕),这就使得变量、函数等可以先于回调执行。同时它也有阻止event loop继续执行的好处。有时候我们可能希望在event loop继续执行前抛出一个错误,这种情况下process.nextTick()变的很有用。下面是对上一个例子的process.nextTick()改造:

let bar;

function someAsyncApiCall(callback) {
  process.nextTick(callback);
}

someAsyncApiCall(() => {
  console.log('bar', bar); // 1
});

bar = 1;

这是一个实际的例子:

const server = net.createServer(() => {}).listen(8080);

server.on('listening', () => {});

当只有一个端口作为参数传入,端口会被立即绑定。所以监听回调可能被立即调用。问题是:on('listening') 回调在那时还没被注册。

为了解决这个问题,把listening事件加入到nextTick() 队列中以允许脚本先执行完(同步代码)。这允许用户(在同步代码中)设置任何他们需要的事件处理函数。

process.nextTick() 和 setImmediate()

对于用户而言,这两种叫法是很相似的但它们的名字又让人琢磨不透。

  • process.nextTick() 会在同一个阶段执行
  • setImmediate() 会在随后的迭代中执行

本质上,这两个的名字应该互换一下,process.nextTick()setImmediate()更接近于立即,但是由于历史原因这不太可能去改变。名字互换可能影响大部分的npm包,每天都有大量的包在提交,这意味这越到后面,互换造成的破坏越大。所以即使它们的名字让人困惑也不可能被改变。

我们建议开发者在所有情况中使用setImmediate(),因为这可以让你的代码兼容更多的环境比如浏览器。

为什么要使用process.nextTick()?

这里又两个主要的原因:

  1. 让开发者处理错误、清除无用的资源或者在event loop继续之前再次尝试重新请求资源
  2. 有时需要允许回调在调用栈展开之后但在事件循环继续之前运行

下面这个例子会满足我们的期望:

const server = net.createServer();
server.on('connection', function(conn) { });

server.listen(8080);
server.on('listening', function() { });

假设listen()是在event loop开始前运行,但是监听回调是包裹在setImmediate中,除非指定hostname参数否则端口将被立即绑定(listening回调被触发),event loop必须要执行到poll阶段才会去处理,这意味着存在一种可能:在listening事件的回调执行前就收到了一个连接,也就是相当于先于listening 触发了connection事件。

另一个例子是运行一个继承至EventEmitter的构造函数,而这个构造函数中会发布一个事件。

const EventEmitter = require('events');
const util = require('util');

function MyEmitter() {
  EventEmitter.call(this);
  this.emit('event');
}
util.inherits(MyEmitter, EventEmitter);

const myEmitter = new MyEmitter();
myEmitter.on('event', function() {
  console.log('an event occurred!');
});

你无法立即从构造函数中真正触发事件,因为脚本还没有运行到用户为该事件分配回调的位置。因此,在构造函数中,您可以使用 process.nextTick() 来设置回调以在构造函数完成后发出事件,从而提供预期的结果

const EventEmitter = require('events');
const util = require('util');

function MyEmitter() {
  EventEmitter.call(this);

  // use nextTick to emit the event once a handler is assigned
  process.nextTick(function() {
    this.emit('event');
  }.bind(this));
}
util.inherits(MyEmitter, EventEmitter);

const myEmitter = new MyEmitter();
myEmitter.on('event', function() {
  console.log('an event occurred!');
});

译者注(Q&A)

翻译完本文,笔者给自己提了几个问题?

  1. poll阶段什么时候会被阻塞?
  2. 为什么在非I/O循环中,setTimeoutsetImmediate的执行顺序是不一定的?
  3. JS调用栈展开是什么意思?
  4. 为什么process.nextTick()可以被递归调用?

笔者将在之后的文章《Q&A之理解NodeJs中的Event Loop、Timers以及process.nextTick()》探讨这些问题,有兴趣的同学可以关注笔者的公众号: 前端情报局-NodeJs系列获取最新情报

原文地址: https://github.com/nodejs/nod...

image

查看原文

赞 10 收藏 7 评论 0

前端森林 收藏了文章 · 2019-01-09

Node.js使用Nodemailer发送邮件

原文链接:Node.js使用Nodemailer发送邮件

电子邮件是—种用电子手段提供信息交换的通信方式,是互联网应用最广的服务。通过网络的电子邮件系统,用户可以以非常低廉的价格(不管发送到哪里,都只需负担网费)、非常快速的方式(几秒钟之内可以发送到世界上任何指定的目的地),与世界上任何一个角落的网络用户联系。

在很多项目中,我们都会遇到邮件注册,邮件反馈等需求。在node中收发电子邮件也非常简单,因为强大的社区有各种各样的包可以供我么直接使用。Nodemailer包就可以帮助我们快速实现发送邮件的功能。

Github源码:https://github.com/ogilhinn/node-abc/tree/master/lesson10

Nodemailer简介

Nodemailer是一个简单易用的Node.js邮件发送组件

官网地址:https://nodemailer.com

GitHub地址:https://github.com/nodemailer/nodemailer

Nodemailer的主要特点包括:

  • 支持Unicode编码
  • 支持Window系统环境
  • 支持HTML内容和普通文本内容
  • 支持附件(传送大附件)
  • 支持HTML内容中嵌入图片
  • 支持SSL/STARTTLS安全的邮件发送
  • 支持内置的transport方法和其他插件实现的transport方法
  • 支持自定义插件处理消息
  • 支持XOAUTH2登录验证

安装使用

首先,我们肯定是要下载安装 注意:Node.js v6+

npm install nodemailer --save

打开官网可以看见一个小例子

'use strict';
const nodemailer = require('nodemailer');

// Generate test SMTP service account from ethereal.email
// Only needed if you don't have a real mail account for testing
nodemailer.createTestAccount((err, account) => {

    // create reusable transporter object using the default SMTP transport
    let transporter = nodemailer.createTransport({
        host: 'smtp.ethereal.email',
        port: 587,
        secure: false, // true for 465, false for other ports
        auth: {
            user: account.user, // generated ethereal user
            pass: account.pass  // generated ethereal password
        }
    });

    // setup email data with unicode symbols
    let mailOptions = {
        from: '"Fred Foo ?" <foo@blurdybloop.com>', // sender address
        to: 'bar@blurdybloop.com, baz@blurdybloop.com', // list of receivers
        subject: 'Hello ✔', // Subject line
        text: 'Hello world?', // plain text body
        html: '<b>Hello world?</b>' // html body
    };

    // send mail with defined transport object
    transporter.sendMail(mailOptions, (error, info) => {
        if (error) {
            return console.log(error);
        }
        console.log('Message sent: %s', info.messageId);
        // Preview only available when sending through an Ethereal account
        console.log('Preview URL: %s', nodemailer.getTestMessageUrl(info));

        // Message sent: <b658f8ca-6296-ccf4-8306-87d57a0b4321@blurdybloop.com>
        // Preview URL: https://ethereal.email/message/WaQKMgKddxQDoou...
    });
});

这个小例子是生成了Ethereal的账户进行邮件发送演示的。但是这多没意思,我们来使用自己的邮箱来发送邮件

发出个真实的邮件

这里我使用了我的qq邮箱给163邮箱发送email。

'use strict';

const nodemailer = require('nodemailer');

let transporter = nodemailer.createTransport({
  // host: 'smtp.ethereal.email',
  service: 'qq', // 使用了内置传输发送邮件 查看支持列表:https://nodemailer.com/smtp/well-known/
  port: 465, // SMTP 端口
  secureConnection: true, // 使用了 SSL
  auth: {
    user: 'xxxxxx@qq.com',
    // 这里密码不是qq密码,是你设置的smtp授权码
    pass: 'xxxxxx',
  }
});

let mailOptions = {
  from: '"JavaScript之禅" <xxxxx@qq.com>', // sender address
  to: 'xxxxxxxx@163.com', // list of receivers
  subject: 'Hello', // Subject line
  // 发送text或者html格式
  // text: 'Hello world?', // plain text body
  html: '<b>Hello world?</b>' // html body
};

// send mail with defined transport object
transporter.sendMail(mailOptions, (error, info) => {
  if (error) {
    return console.log(error);
  }
  console.log('Message sent: %s', info.messageId);
  // Message sent: <04ec7731-cc68-1ef6-303c-61b0f796b78f@qq.com>
});

运行程序,成功将返回messageId。这是便可以去收件箱查看这个新邮件啦

email

这里我们需要注意,auth.pass 不是邮箱账户的密码而是stmp的授权码。

到此我们就掌握了发邮件的基本操作。

更多配置

  • CC: Carbon Copy(抄送),用于通知相关的人,收件人可以看到都邮件都抄送给谁了。一般回报工作或跨部门沟通时,都会CC给收件人的领导一份
  • BCC:Blind Carbon Copy(暗抄送),也是用于通知相关的人,但是收件人是看不到邮件被密送给谁了。
  • attachments: 附件

更多配置项:https://nodemailer.com/message/

这里我们就不演示CC、BCC了,请自行尝试。我们来试试发送附件

...
// 只需添加attachments配置项即可
attachments: [
    {   // utf-8 string as an attachment
      filename: 'text.txt',
      content: 'hello world!'
    },
    {
      filename: 'ZenQcode.png',
      path: path.resolve(__dirname, 'ZenQcode.png'),
    }
  ]
...

发送email,就可以收到一个内容为hello world的text.txt文件,以及一个我公众号的二维码。

好看的HTML邮件

HTML Email 编写指南: http://www.ruanyifeng.com/blog/2013/06/html_email.html

这儿,我们使用Foundation for Emails: https://foundation.zurb.com/emails.html的模板

'use strict';

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

let transporter = nodemailer.createTransport({
  // host: 'smtp.ethereal.email',
  service: 'qq', // 使用内置传输发送邮件 查看支持列表:https://nodemailer.com/smtp/well-known/
  port: 465, // SMTP 端口
  secureConnection: true, // 使用 SSL
  auth: {
    user: 'xxxxxx@qq.com',
    // 这里密码不是qq密码,是你设置的smtp授权码
    pass: 'xxxxxx',
  }
});

let mailOptions = {
  from: '"JavaScript之禅" <xxxxx@qq.com>', // sender address
  to: 'xxxxxxxx@163.com', // list of receivers
  subject: 'Hello', // Subject line
  // 发送text或者html格式
  // text: 'Hello world?', // plain text body
  html: fs.createReadStream(path.resolve(__dirname, 'email.html')) // 流
};

// send mail with defined transport object
transporter.sendMail(mailOptions, (error, info) => {
  if (error) {
    return console.log(error);
  }
  console.log('Message sent: %s', info.messageId);
  // Message sent: <04ec7731-cc68-1ef6-303c-61b0f796b78f@qq.com>
});

运行程序,你将如愿以偿收到如下Email。样式可能会有细微偏差

屏幕快照 2017-12-01 16.32.41

上面email中我们用了外链的图片,我们也可以使用附件的方式,将图片嵌入进去。给附件加个cid属性即可。

...
let mailOptions = {
  ...
  html: '<img data-original="cid:01">', // html body
  attachments: [
    {
      filename: 'ZenQcode.png',
      path: path.resolve(__dirname, 'ZenQcode.png'),
      cid: '01',
    }
  ]
};
...

使用模板引擎

邮件信息一般都不是固定的,我们可以引入模板引擎对HTML内容进行渲染。

这里我们使用Ejs:https://github.com/mde/ejs/来做演示

$ npm install ejs --save

ejs具体语法请参看官方文档

先建立一个email.ejs文件

<h1>hello <%= title %></h1>
<p><%= desc %></p>

修改我们的js文件

...
const template = ejs.compile(fs.readFileSync(path.resolve(__dirname, 'email.ejs'), 'utf8'));

const html = template({
  title: 'Ejs',
  desc: '使用Ejs渲染模板',
});

let mailOptions = {
  from: '"JavaScript之禅" <xxxxx@qq.com>', // sender address
  to: 'xxxxx@163.com', // list of receivers
  subject: 'Hello', // Subject line
  html: html,// html body
};
...

到此,你的邮箱将收到一个动态渲染的hello Ejs。

本文到此告一段落,在此基础上你可以实现更多有用的功能

HTML email 框架推荐

左手代码,右手砖,抛砖引玉

如果你知道更多好用HTML email资源,留言交流让更多人知道。

最后福利干货来了

36个国内外精致电子邮件HTML模版

产品周报类预览

投票

知乎周报

关注公众号【JavaScript之禅】回复【 666 】,免费获取

JavaScript之禅

查看原文

前端森林 赞了文章 · 2019-01-09

koa2 使用passport权限认证中间件

做后端系统避免不了要做权限认证,比如本地用户登录,第三方登录。
权限认证的思路也极其简单,不外乎就是登录,登出,路由守护三部分。

那么有没有现成的轮子可用呢?答案是肯定的,node发展了这么迅速,各种npm包层出不穷,总有那么几款厉害的。
今天要讲的权限认证中间件那就是:passport

passport目前有很多已经写好的登录策略,比如github登录,微信登录,Facebook登录,google等等。

官网 http://passportjs.org/docs/

官网是英文的,英文差的话不建议看了,去找个demo撸起来才是正确的学习思路。

通过一阵摸索,本文决定记录下koa2具体的使用步骤。

安装包

koa2中使用的是 koa-passport 这个包。
本地验证用的是 passport-local这个策略

npm install -S koa-passport

代码

先来看代码,稍后再做解释。
这里使用 passport-local 策略(本地权限认证)为例子。
因为passport使用之前要定义策略及序列化与反序列化操作,所以把 passport 的配置及策略写到一个文件passport.js

定义策略

// passport.js
const passport = require('koa-passport')
var LocalStrategy = require('passport-local').Strategy


// 序列化ctx.login()触发
passport.serializeUser(function(user, done) {
  console.log('serializeUser: ', user)
  done(null, user.id)
})
// 反序列化(请求时,session中存在"passport":{"user":"1"}触发)
passport.deserializeUser(async function(id, done) {
  console.log('deserializeUser: ', id)
  var user = {id: 1, username: 'admin', password: '123456'}
  done(null, user)
})
// 提交数据(策略)
passport.use(new LocalStrategy({
  // usernameField: 'email',
  // passwordField: 'passwd'
}, function(username, password, done) {
  console.log('LocalStrategy', username, password)
  var user = {id: 1, username: username, password: password}
  done(null, user, {msg: 'this is a test'})
  // done(err, user, info)
}))


module.exports = passport

记得文件末 module.exports = passport 导出 passport

入口载入

然后在 koa 入口 app.js 中载入 passport.js 文件

const passport = require('./passport')

并在适当位置(看下边 app.js)使用passport中间件

app.use(passport.initialize())
app.use(passport.session())

passport 中间件需要用到 session ()所以,你的app.js入口文件类似这样

// app.js
const Koa = require('koa')
const bodyParser = require('koa-bodyparser')
const static = require('koa-static')
const session = require('koa-session')
const RedisStore = require('koa-redis')
const app = new Koa()


const passport = require('./libs/passport')
const baseConf = require('./config/base')
const redisConf = require('./config/redis')

// 基础中间件
app.use(async (ctx, next) => {
  const start = Date.now()
  await next()
  const ms = Date.now() - start
  console.log(`${ctx.method} ${ctx.status} ${ctx.url} - ${ms} ms`)
})


app.keys = ['123456']
app.use(bodyParser())
app.use(session({
  cookie: {secure: false, maxAge:86400000},
  store: RedisStore(redisConf.session)
}, app))


app.use(passport.initialize())
app.use(passport.session())

var router = require('./routes')

router.all('404', '*', ctx => {
  ctx.status = 404
  ctx.body = '404'
})

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

var log = require('./libs/log')
app.on('error', (err, ctx) => {
  log.error(`${ctx.method} ${ctx.url}`, 'Error: ')
  log.error(err)
  console.log(err)
})

app.listen(baseConf.port)
console.log('listening on ' + baseConf.port)
module.exports = app

编写路由

编写路由及守护中间件。

  • 登录
POST /login
router.post('/login', ctx => {
  // 会调用策略
  return passport.authenticate('local',
    function(err, user, info, status) {
      ctx.body = {user, err, info, status}
      return ctx.login({id: 1, username: 'admin', password: '123456'})
    })(ctx)
})
  • 登出
GET /logout
router.get('/logout', ctx => {
  ctx.logout()
  ctx.body = {auth: ctx.isAuthenticated(), user: ctx.state.user}
})
  • 路由守护中间件

比如你/api/*的路由需要用户认证才能访问

router.use('/api/*', (ctx, next) => {
   if(ctx.isAuthenticated()) {
     next()
   } else {
    ctx.status = 401
    ctx.body = {
      msg: 'auth fail'
    }
  }
})

到这里,本地权限认证基本完成了,post请求 /login 并且提交表单username,和 password即可登录一个用户。

/logout 退出当前登录。

解释

使用 passport 这个中间件,必须了解其运行步骤和知识点。

passport 以策略来扩展验证,什么是策略呢?

比如:本地策略,github登录策略,微信登录策略

passport 中间件使用前,需要注册策略,及实习序列化与反序列化操作。

序列化

通过 passport.serializeUser 函数定义序列化操作。

// 序列化
passport.serializeUser(function(user, done) {
  done(null, user.id)
})

在调用 ctx.login() 时会触发序列化操作。

反序列化

通过 passport.deserializeUser 函数定义反序列化操作。

// 反序列化
passport.deserializeUser(async function(id, done) {
  console.log('deserializeUser: ', id)
  var user = {id: 1, username: 'admin', password: '123456'}
  done(null, user)
})

在请求时,session中如果存在 "passport":{"user":"xxx"}时会触发定义的反序列化操作。

注册策略

// 策略
passport.use(new LocalStrategy({
  // usernameField: 'email',
  // passwordField: 'passwd'
}, function(username, password, done) {
  var user = {id: 1, username: username, password: password}
  done(null, user)
}))

在使用 passport.authenticate('策略', ...) 的时候,会执行策略

其他

app.use(passport.initialize()) 会在请求周期ctx对象挂载以下方法与属性

  • ctx.state.user 认证用户
  • ctx.login(user) 登录用户(序列化用户)
  • ctx.isAuthenticated() 判断是否认证

github

另外附上github的认证代码

安装包

npm install -S passport-github

passport.js载入

var GitHubStrategy = require('passport-github').Strategy

passport.js 增加代码

passport.use(new GitHubStrategy({
    clientID: githubConf.clientId,
    clientSecret: githubConf.secret,
    callbackURL: githubConf.callback
  },
  function(accessToken, refreshToken, profile, done) {
    // console.log(accessToken, refreshToken, profile)
    return done(null, {accessToken, refreshToken, profile})
  }
))

添加两个路由

// 调用授权页面
router.get('/auth/github', ctx => {
  return passport.authenticate('github', {scope: ['user:email']})(ctx)
})
// 授权回调得到code
router.get('/auth/github/callback', async ctx => {
  return passport.authenticate('github', (err, user, info, status) => {
    ctx.body = {err, user, info, status}
    return ctx.login(user)
  })(ctx)
})

以上例子只是模拟,并没有涉及数据库的操作,具体的实现还需要自己按照业务需求实现。

passport使用session来维护会话。对于token验证的来说,并不能用,所以要实现token验证的话还需要另外编写策略才行。

更多详细用法,请自行到官网查看文档。

查看原文

赞 33 收藏 32 评论 4

前端森林 收藏了文章 · 2019-01-09

koa2 使用passport权限认证中间件

做后端系统避免不了要做权限认证,比如本地用户登录,第三方登录。
权限认证的思路也极其简单,不外乎就是登录,登出,路由守护三部分。

那么有没有现成的轮子可用呢?答案是肯定的,node发展了这么迅速,各种npm包层出不穷,总有那么几款厉害的。
今天要讲的权限认证中间件那就是:passport

passport目前有很多已经写好的登录策略,比如github登录,微信登录,Facebook登录,google等等。

官网 http://passportjs.org/docs/

官网是英文的,英文差的话不建议看了,去找个demo撸起来才是正确的学习思路。

通过一阵摸索,本文决定记录下koa2具体的使用步骤。

安装包

koa2中使用的是 koa-passport 这个包。
本地验证用的是 passport-local这个策略

npm install -S koa-passport

代码

先来看代码,稍后再做解释。
这里使用 passport-local 策略(本地权限认证)为例子。
因为passport使用之前要定义策略及序列化与反序列化操作,所以把 passport 的配置及策略写到一个文件passport.js

定义策略

// passport.js
const passport = require('koa-passport')
var LocalStrategy = require('passport-local').Strategy


// 序列化ctx.login()触发
passport.serializeUser(function(user, done) {
  console.log('serializeUser: ', user)
  done(null, user.id)
})
// 反序列化(请求时,session中存在"passport":{"user":"1"}触发)
passport.deserializeUser(async function(id, done) {
  console.log('deserializeUser: ', id)
  var user = {id: 1, username: 'admin', password: '123456'}
  done(null, user)
})
// 提交数据(策略)
passport.use(new LocalStrategy({
  // usernameField: 'email',
  // passwordField: 'passwd'
}, function(username, password, done) {
  console.log('LocalStrategy', username, password)
  var user = {id: 1, username: username, password: password}
  done(null, user, {msg: 'this is a test'})
  // done(err, user, info)
}))


module.exports = passport

记得文件末 module.exports = passport 导出 passport

入口载入

然后在 koa 入口 app.js 中载入 passport.js 文件

const passport = require('./passport')

并在适当位置(看下边 app.js)使用passport中间件

app.use(passport.initialize())
app.use(passport.session())

passport 中间件需要用到 session ()所以,你的app.js入口文件类似这样

// app.js
const Koa = require('koa')
const bodyParser = require('koa-bodyparser')
const static = require('koa-static')
const session = require('koa-session')
const RedisStore = require('koa-redis')
const app = new Koa()


const passport = require('./libs/passport')
const baseConf = require('./config/base')
const redisConf = require('./config/redis')

// 基础中间件
app.use(async (ctx, next) => {
  const start = Date.now()
  await next()
  const ms = Date.now() - start
  console.log(`${ctx.method} ${ctx.status} ${ctx.url} - ${ms} ms`)
})


app.keys = ['123456']
app.use(bodyParser())
app.use(session({
  cookie: {secure: false, maxAge:86400000},
  store: RedisStore(redisConf.session)
}, app))


app.use(passport.initialize())
app.use(passport.session())

var router = require('./routes')

router.all('404', '*', ctx => {
  ctx.status = 404
  ctx.body = '404'
})

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

var log = require('./libs/log')
app.on('error', (err, ctx) => {
  log.error(`${ctx.method} ${ctx.url}`, 'Error: ')
  log.error(err)
  console.log(err)
})

app.listen(baseConf.port)
console.log('listening on ' + baseConf.port)
module.exports = app

编写路由

编写路由及守护中间件。

  • 登录
POST /login
router.post('/login', ctx => {
  // 会调用策略
  return passport.authenticate('local',
    function(err, user, info, status) {
      ctx.body = {user, err, info, status}
      return ctx.login({id: 1, username: 'admin', password: '123456'})
    })(ctx)
})
  • 登出
GET /logout
router.get('/logout', ctx => {
  ctx.logout()
  ctx.body = {auth: ctx.isAuthenticated(), user: ctx.state.user}
})
  • 路由守护中间件

比如你/api/*的路由需要用户认证才能访问

router.use('/api/*', (ctx, next) => {
   if(ctx.isAuthenticated()) {
     next()
   } else {
    ctx.status = 401
    ctx.body = {
      msg: 'auth fail'
    }
  }
})

到这里,本地权限认证基本完成了,post请求 /login 并且提交表单username,和 password即可登录一个用户。

/logout 退出当前登录。

解释

使用 passport 这个中间件,必须了解其运行步骤和知识点。

passport 以策略来扩展验证,什么是策略呢?

比如:本地策略,github登录策略,微信登录策略

passport 中间件使用前,需要注册策略,及实习序列化与反序列化操作。

序列化

通过 passport.serializeUser 函数定义序列化操作。

// 序列化
passport.serializeUser(function(user, done) {
  done(null, user.id)
})

在调用 ctx.login() 时会触发序列化操作。

反序列化

通过 passport.deserializeUser 函数定义反序列化操作。

// 反序列化
passport.deserializeUser(async function(id, done) {
  console.log('deserializeUser: ', id)
  var user = {id: 1, username: 'admin', password: '123456'}
  done(null, user)
})

在请求时,session中如果存在 "passport":{"user":"xxx"}时会触发定义的反序列化操作。

注册策略

// 策略
passport.use(new LocalStrategy({
  // usernameField: 'email',
  // passwordField: 'passwd'
}, function(username, password, done) {
  var user = {id: 1, username: username, password: password}
  done(null, user)
}))

在使用 passport.authenticate('策略', ...) 的时候,会执行策略

其他

app.use(passport.initialize()) 会在请求周期ctx对象挂载以下方法与属性

  • ctx.state.user 认证用户
  • ctx.login(user) 登录用户(序列化用户)
  • ctx.isAuthenticated() 判断是否认证

github

另外附上github的认证代码

安装包

npm install -S passport-github

passport.js载入

var GitHubStrategy = require('passport-github').Strategy

passport.js 增加代码

passport.use(new GitHubStrategy({
    clientID: githubConf.clientId,
    clientSecret: githubConf.secret,
    callbackURL: githubConf.callback
  },
  function(accessToken, refreshToken, profile, done) {
    // console.log(accessToken, refreshToken, profile)
    return done(null, {accessToken, refreshToken, profile})
  }
))

添加两个路由

// 调用授权页面
router.get('/auth/github', ctx => {
  return passport.authenticate('github', {scope: ['user:email']})(ctx)
})
// 授权回调得到code
router.get('/auth/github/callback', async ctx => {
  return passport.authenticate('github', (err, user, info, status) => {
    ctx.body = {err, user, info, status}
    return ctx.login(user)
  })(ctx)
})

以上例子只是模拟,并没有涉及数据库的操作,具体的实现还需要自己按照业务需求实现。

passport使用session来维护会话。对于token验证的来说,并不能用,所以要实现token验证的话还需要另外编写策略才行。

更多详细用法,请自行到官网查看文档。

查看原文