zhangl

zhangl 查看完整档案

上海编辑  |  填写毕业院校  |  填写所在公司/组织 blog.csdn.net/zhanglong_web 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

zhangl 赞了文章 · 2020-07-03

互联网大厂的面经大全 (阿里、头条、腾讯、美团等)

本文永久地址: 大厂面经大全将会持续更新和补充

各大厂

阿里

头条

腾讯

高赞文章收藏

关注我

欢迎关注公众号【互联网大厂招聘】,作者将在公众号里持续推送各个大厂的招聘职位及要求,并与大厂面试官以及招聘负责人直通,感兴趣的可以直接与负责人交流。

另外,作者也将持续推送优质的大厂面试经验,各种大厂独家面试题以及优秀文章分享,不限于前端,后端,运维和系统设计。

更多大厂招聘,面试面经,技能要求,请关注公众号【互联网大厂招聘】

扫码添加我的微信,一定要备注面试,进入面试群,或者与我交流。

加我微信拉你进入面试交流群

查看原文

赞 160 收藏 125 评论 0

zhangl 关注了专栏 · 2020-07-03

程序员的自我成长

对一些学习的总结

关注 41

zhangl 赞了文章 · 2020-07-03

面试官在“逗”你系列:数组去重你会几种呀?

前言

数组去重是一个老生常谈的话题,也是前端童鞋在面试时的一道高频题。本文将深入的探索数组去重的原理及实现,为各位小伙伴提供多种可以反手“调戏”面试官的解决方案。

话不多说,上去就来一梭子...

数组去重核心原理

价值100W的核心原理上来就给你了...,记得留言点赞鸭!
  1. 一般我们都会创建临时变量tmp,存储不重复的元素(以数组元素存储或对象的键来存储);
  2. 遍历待去重数组arr,依次判断tmp中是否包含该元素;
  3. 若tmp中不存在该元素,则放入;否则跳过不处理。
基本上无论什么样的实现,其核心皆是如此(判断是否已存在)。不行你就留言,咱们可以battle一下

经典去重方案一:

设置tmp为对象,对象的键存储数组元素的值,最终返回对象的所有键。
function array_unique (arr) {
  if (arr.length === 0) {
    return arr
  }
  let tmp = {}
  let len = arr.length
  for (let i = 0; i < len; i++) {
    if (tmp[arr[i]] === undefined) {
      tmp[arr[i]] = i
    }
  }
  return Object.keys(tmp)
}

// 调用数组去重
let arr = [1, 2, 3, 1, 2]
let newArr = array_unique(arr)
console.log(newArr) // ['1', '2', '3']

如果你采用这种方式来回答面试官的话,你就陷入了他在内心中早早设下的陷阱:

  1. 你这种方式能区分数字和字符串吗?能区分undefined'undefined'吗?
  2. 你现在返回的数据类型还和原有的数据类型一致吗?

带着面试官的疑问,我们来看另外一种经典去重方式。

经典去重方式二:

设置tmp为数组,数组中存储唯一的元素,最终返回tmp
function array_unique (arr) {
  let len = arr.length
  if (!len) {
    return []
  }
  let tmp = []
  for (let i = 0; i < len; i++) {
    // 判断数组arr的元素是否在数组tmp中
    if (tmp.indexOf(arr[i]) === -1) {
      tmp.push(arr[i])
    }
  }
  return tmp
}
let arr = [1, 2, 3, '1', 2, undefined, undefined, 'undefined']
let newArr = array_unique(arr)
console.log(newArr) // [1, 2, 3, '1', undefined, 'undefined']

此刻,内心是否窃喜!

But, 如果你这么考虑,又陷入了面试官的另一个陷阱:

  1. 你这方式能筛选NaN吗?

好吧,面试官最大,再考虑!

数组去重方式三:

原理还是同去重方式二,只不过我们使用ES6的includes替换indexOf方法,

includes() 方法,判断数组中是否包含某个元素,如果包含返回true,否则返回false

就是这么so easy!

function array_unique (arr) {
  let len = arr.length
  if (!len) {
    return []
  }
  let tmp = []
  for (let i = 0; i < len; i++) {
    // 判断数组arr的元素是否在数组tmp中
    if (!tmp.includes(arr[i]) {
      tmp.push(arr[i])
    }
  }
  return tmp
}
let arr = [1, 2, 3, '1', 2, undefined, undefined,  'undefined', NaN, NaN]
let newArr = array_unique(arr)
console.log(newArr) // [1, 2, 3, '1', undefined, 'undefined', NaN]

此刻,你以为就结束吗?不,不可能!

面试官的坑已经在前面等你很久了:

  1. 你的这个筛选方式能区分对象吗?如{}、{a: 1}

有没有想把自己的四十米大砍刀拿出来,neng屎面试官!(图就不配了,自己脑补吧...)
然而,什么都做不了,继续想吧...

数组去重方式四:

原理同上,我们要继续换一个判断数组是否包含某元素的方法:`findIndex

findIndex查询数组是否包含某元素,如果存在返回元素的索引,否则返回-1。它比indexOf更加先进的地方在于能传入callback,按约定方式查询。

function array_unique (arr) {
  let len = arr.length
  if (!len) {
    return []
  }
  let tmp = []
  for (let i = 0; i < len; i++) {
    // 判断数组arr的元素是否在数组tmp中
    if (tmp.findIndex((v) => JSON.stringify(v) === JSON.stringify(arr[i])) === -1) {
      tmp.push(arr[i])
    }
  }
  return tmp
}
let arr = [1, 2, 3, '1', 2, undefined, undefined,  'undefined', NaN, NaN, {}, {}, {a: 1}, {a: 1}]
let newArr = array_unique(arr)
console.log(newArr) // [1, 2, 3, '1', undefined, 'undefined', NaN, {}, {a: 1}]
终于成功啦!来来来,可以潇洒的问面试官,“您还有问题没有?”

当然,主动挑衅面试官,是要承担风险呦,有可能会因为你眨眼的时候,先眨了右眼被挂掉了...

判断数组是否包含某元素的几种方式:

给大家列个表格,好区分几个方法的作用,

方法是否可检测nullundefinedNaN{}备注
indexOf
includes
findIndex需传入特定的callback

小结

数组去重这道面试题,考察的知识点还是非常多的。首先是对数组的常用方法要比较熟悉,还有其他的如NaN与NaN不相等,{}与{}不相等等知识点,以及灵活多变的思维逻辑。

当然,数组去重还有其他的多种实现方式,欢迎各位小伙伴留言交流!

后记

以上就是胡哥今天给大家分享的内容,喜欢的小伙伴记得点赞收藏呦,关注胡哥有话说,学习前端不迷路,欢迎多多留言交流...

胡哥有话说,一个有技术,有情怀的胡哥!现任京东前端攻城狮一枚。

胡哥有话说,专注于大前端技术领域,分享前端系统架构,框架实现原理,最新最高效的技术实践!

查看原文

赞 30 收藏 21 评论 7

zhangl 收藏了文章 · 2020-03-06

127个常用的JS代码片段,每段代码花30秒就能看懂(四)

大家好,今天我继续给大家分享本系列文章的第四部分,希望对你的日常工作有所帮助。

64、getColonTimeFromDate

用于判断程序运行环境是否在浏览器,这有助于避免在node环境运行前端模块时出错。

const isBrowser = () => ![typeof window, typeof document].includes('undefined');

isBrowser(); // true (browser)
isBrowser(); // false (Node)

65、isBrowserTabFocused

用于判断当前页面是否处于活动状态(显示状态)。

const isBrowserTabFocused = () => !document.hidden;
isBrowserTabFocused(); // true

66、isLowerCase

用于判断当前字符串是否都为小写。

const isLowerCase = str => str === str.toLowerCase();

isLowerCase('abc'); // true
isLowerCase('a3@$'); // true
isLowerCase('Ab4'); // false

67、isNil

用于判断当前变量的值是否为 null 或 undefined 类型。

const isNil = val => val === undefined || val === null;

isNil(null); // true
isNil(undefined); // true

68、isNull

用于判断当前变量的值是否为 null 类型。

const isNull = val => val === null;

isNull(null); // true

69、isNumber

用于检查当前的值是否为数字类型。

function isNumber(n) {

return !isNaN(parseFloat(n)) && isFinite(n);  

}

isNumber('1'); // false
isNumber(1); // true

70、isObject

用于判断参数的值是否是对象,这里运用了Object 构造函数创建一个对象包装器,如果是对象类型,将会原值返回。

const isObject = obj => obj === Object(obj);

isObject([1, 2, 3, 4]); // true
isObject([]); // true
isObject(['Hello!']); // true
isObject({ a: 1 }); // true
isObject({}); // true
isObject(true); // false

71、isObjectLike

用于检查参数的值是否为null以及类型是否为对象。

const isObjectLike = val => val !== null && typeof val === 'object';

isObjectLike({}); // true
isObjectLike([1, 2, 3]); // true
isObjectLike(x => x); // false
isObjectLike(null); // false

72、isPlainObject

此代码段检查参数的值是否是由Object构造函数创建的对象。

const isPlainObject = val => !!val && typeof val === 'object' && val.constructor === Object;

isPlainObject({ a: 1 }); // true
isPlainObject(new Map()); // false

73、isPromiseLike

用于检查当前的对象是否类似Promise函数。

const isPromiseLike = obj =>
obj !== null &&
(typeof obj === 'object' || typeof obj === 'function') &&
typeof obj.then === 'function';

isPromiseLike({
then: function() {

return '';  

}
}); // true
isPromiseLike(null); // false
isPromiseLike({}); // false

74、isSameDate

用于判断给定的两个日期是否是同一天。

const isSameDate = (dateA, dateB) => dateA.toISOString() === dateB.toISOString();

isSameDate(new Date(2010, 10, 20), new Date(2010, 10, 20)); // true

75、isString

用于检查当前的值是否为字符串类型。

const isString = val => typeof val === 'string';

isString('10'); // true

76、isSymbol

用于判断参数的值是否是 Symbol 类型。

const isSymbol = val => typeof val === 'symbol';

isSymbol(Symbol('x')); // true

77、isUndefined

用于判断参数的类型是否是 Undefined 类型。

const isUndefined = val => val === undefined;

isUndefined(undefined); // true

78、isUpperCase

用于判断当前字符串的字母是否都为大写。

const isUpperCase = str => str === str.toUpperCase();

isUpperCase('ABC'); // true
isLowerCase('A3@$'); // true
isLowerCase('aB4'); // false

79、isValidJSON

用于判断给定的字符串是否是 JSON 字符串。

const isValidJSON = str => {
try {

JSON.parse(str);  
return true;  

} catch (e) {

return false;  

}
};

isValidJSON('{"name":"Adam","age":20}'); // true
isValidJSON('{"name":"Adam",age:"20"}'); // false
isValidJSON(null); // true

80、last

此函数功能返回数组的最后一个元素。

const last = arr => arr[arr.length - 1];

last([1, 2, 3]); // 3

81、matches

此函数功能用于比较两个对象,以确定第一个对象是否包含与第二个对象相同的属性与值。

onst matches = (obj, source) =>
Object.keys(source).every(key => obj.hasOwnProperty(key) && obj[key] === source[key]);

matches({ age: 25, hair: 'long', beard: true }, { hair: 'long', beard: true }); // true
matches({ hair: 'long', beard: true }, { age: 25, hair: 'long', beard: true }); // false

82、maxDate

此代码段查找日期数组中最大的日期进行输出。

const maxDate = (...dates) => new Date(Math.max.apply(null, ...dates));

const array = [
new Date(2017, 4, 13),
new Date(2018, 2, 12),
new Date(2016, 0, 10),
new Date(2016, 0, 9)
];
maxDate(array); // 2018-03-11T22:00:00.000Z

83、maxN

此段代码输出数组中前 n 位最大的数。

const maxN = (arr, n = 1) => [...arr].sort((a, b) => b - a).slice(0, n);

maxN([1, 2, 3]); // [3]
maxN([1, 2, 3], 2); // [3,2]

84、minDate

此代码段查找日期数组中最早的日期进行输出。

const minDate = (...dates) => new Date(Math.min.apply(null, ...dates));

const array = [
new Date(2017, 4, 13),
new Date(2018, 2, 12),
new Date(2016, 0, 10),
new Date(2016, 0, 9)
];
minDate(array); // 2016-01-08T22:00:00.000Z

小节

今天的内容就和大家分享到这里,感谢你的阅读,如果你喜欢我的分享,麻烦给个关注、点赞加转发哦,你的支持,就是我分享的动力,后续会持续分享剩余的代码片段,欢迎持续关注。

本文原作者:Fatos Morina 来源网站:medium 注:并非直译

往期

127个常用的JS代码片段,每段代码花30秒就能看懂(三)

在P站做web前端,是种怎样的体验?

[
](http://mp.weixin.qq.com/s?__b...

查看原文

zhangl 赞了文章 · 2020-03-01

深入浅出js中的策略模式

什么是策略模式,官方定义是:定义一些列算法,把他们封装起来,并且可以相互替换。说人话(⊙ˍ⊙):就是把看似毫无联系的代码提取封装、复用,使之更容易被理解和拓展。常见的用于一次if判断、switch枚举、数据字典等流程判断语句中。

使用策略模式计算等级

在游戏中,我们每玩完一局游戏都有对用户进行等级评价,比如S级4倍经验,A级3倍经验,B级2倍经验,其他1倍经验,用函数来表达如下:

    function getExperience(level, experience){
      if(level == 'S'){
        return 4*experience
      }
      if(level == 'A'){
        return 3*experience
      }
      if(level == 'B'){
        return 2*experience
      }
      return experience
    }

可知getExperience函数各种if条件判断,复用性差,我们根据策略模式封装复用的思想,进行改写。

    // 改为策略模式 分成两个函数来写
    const strategy = {
      'S' : function(experience){
        return 4*experience
      },
      'A' : function(experience){
        return 3*experience
      },
      'B' : function(experience){
        return 2*experience
      }
    }
    // getExperience可以复用
    function getExperience(strategy, level, experience){
      return (level in strategy) ? strategy[level](experience) : experience
    }
    var s = getExperience(strategy, 'S', 100)
    var a = getExperience(strategy, 'A', 100)
    console.log(s, a) // 400 300

分为两个函数之后,strategy对象解耦,拓展性强。在vue数据驱动视图更新的更新器updater使用,就使用了策略模式。想要进一步了解vue底层原理,可以参考可以参考github上的一篇文章 ☛ MVVM实现

// 指令处理集合
var compileUtil = {
    // v-text更新视图原理
    text: function(node, vm, exp) {
        this.bind(node, vm, exp, 'text');
    },
    // v-html更新视图原理
    html: function(node, vm, exp) {
        this.bind(node, vm, exp, 'html');
    },
    // v-class绑定原理
    class: function(node, vm, exp) {
        this.bind(node, vm, exp, 'class');
    },
    bind: function(node, vm, exp, dir) {
        // 不同指令触发视图更新
        var updaterFn = updater[dir + 'Updater'];
        updaterFn && updaterFn(node, this._getVMVal(vm, exp));
        new Watcher(vm, exp, function(value, oldValue) {
            updaterFn && updaterFn(node, value, oldValue);
        });
    }
    ......
}

使用策略模式验证表单

常见表单验证用if、else流程语句判断用户输入数据是否符合验证规则,而在Elementui中,基于async-validator库,只需要通过rule属性传入约定的验证规则,即可校验。方便快捷,可复用。现在我们根据策略模式仿写一个校验方式。

    // 我们写一个form表单
    <form action="/" class="form">
      <input type="text" name="username">
      <input type="password" name="password"> 
      <button>submit</button>
    </form>
    <div id="tip"></div>
  • 首先定义校验规则
    const strategies = {
      // 非空
      noEmpty: function(value, errMsg){
        if(value === ''){
          return errMsg
        }
      },
      // 最小长度
      minLength: function(value, length, errMsg){
        if(!value || value.length < length){
          return errMsg
        }
      },
      // 最大长度
      maxLength: function(value, length, errMsg){
        if(value.length > length){
          return errMsg
        }
      }
    }
  • 接着设置验证器
    // 创建验证器
    var Validator = function(strategies){
      this.strategies = strategies
      this.cache = [] // 存储校验规则
    }
    // 添加校验规则
    Validator.prototype.add = function(dom, rules){
      rules.forEach(item => {
        this.cache.push(() => {
          let value = dom.value
          let arr = item.rule.split(':')
          let name = arr.shift()
          let params = [value, ...arr, item.errMsg]
          // apply保证上下文一致
          return this.strategies[name].apply(dom, params)
        })
      })
    }
    // 校验结果
    Validator.prototype.validate = function(dom, rules, errMsg){
      // 遍历cache里面的校验函数
      for(let i = 0, validateFun; validateFun = this.cache[i++];){
        const message = validateFun()
        // 返回报错信息,终止验证并抛出异常
        if(message) return message
      }
    }
  • 最后进行校验
    var form = document.querySelector("form")
    // 提交表单
    form.onsubmit = function(event){
      event.preventDefault() 
      // 判断验证结果
      const message = validate()
      const tip = document.getElementById('tip')
      if(message){
        tip.innerHTML = message
        tip.style.color = 'red'
      }else{
        tip.innerHTML = '验证通过!'
        tip.style.color = 'green'
      }
    }
    // 校验函数
    function validate(){
      // 实例验证器
      const validator = new Validator(strategies)
      // 添加验证规则
      validator.add(form.username, [
        {
          rule: 'noEmpty',
          errMsg: '用户名不能为空!'
        },
        {
          rule: 'minLength:3',
          errMsg: '用户名长度大于3!'
        }
      ])
      validator.add(form.password, [
        {
          rule: 'minLength:6',
          errMsg: '密码长度大于6!'
        },
        {
          rule: 'maxLength:10',
          errMsg: '密码最大长度为10!'
        }
      ])
      // 进行校验,并返回结果
      return validator.validate()
    }

如上所示,我们只要添加strategies对象的属性,就能自定义自己的验证规则,并且可以复用,大大方便了日常开发!

查看原文

赞 22 收藏 12 评论 2

zhangl 收藏了文章 · 2020-03-01

前端程序员经常忽视的一个JavaScript面试题

题目

阅读往期更多优质文章可移步我的 GitHub 查看哦?
function Foo() {
    getName = function () { alert (1); };
    return this;
}
Foo.getName = function () { alert (2);};
Foo.prototype.getName = function () { alert (3);};
var getName = function () { alert (4);};
function getName() { alert (5);}
 
//请写出以下输出结果:
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();

这几天面试上几次碰上这道经典的题目,特地从头到尾来分析一次答案,这道题的经典之处在于它综合考察了面试者的JavaScript的综合能力,包含了变量定义提升、this指针指向、运算符优先级、原型、继承、全局变量污染、对象属性及原型属性优先级等知识,此题在网上也有部分相关的解释,当然我觉得有部分解释还欠妥,不够清晰,特地重头到尾来分析一次,当然我们会把最终答案放在后面,并把此题再改高一点点难度,改进版也放在最后,方便面试官在出题的时候有个参考。

image

第一问

先看此题的上半部分做了什么,首先定义了一个叫Foo的函数,之后为Foo创建了一个叫getName的静态属性存储了一个匿名函数,之后为Foo的原型对象新创建了一个叫getName的匿名函数。之后又通过函数变量表达式创建了一个getName的函数,最后再声明一个叫getName函数。

第一问的Foo.getName自然是访问Foo函数上存储的静态属性,答案自然是2,这里就不需要解释太多的,一般来说第一问对于稍微懂JS基础的同学来说应该是没问题的,当然我们可以用下面的代码来回顾一下基础,先加深一下了解

function User(name) {
    var name = name; //私有属性
    this.name = name; //公有属性
    function getName() { //私有方法
        return name;
    }
}
User.prototype.getName = function() { //公有方法
    return this.name;
}
User.name = 'Wscats'; //静态属性
User.getName = function() { //静态方法
    return this.name;
}
var Wscat = new User('Wscats'); //实例化

注意下面这几点:

  • 调用公有方法,公有属性,我们必需先实例化对象,也就是用new操作符实化对象,就可构造函数实例化对象的方法和属性,并且公有方法是不能调用私有方法和静态方法的
  • 静态方法和静态属性就是我们无需实例化就可以调用
  • 而对象的私有方法和属性,外部是不可以访问的

第二问

第二问,直接调用getName函数。既然是直接调用那么就是访问当前上文作用域内的叫getName的函数,所以这里应该直接把关注点放在4和5上,跟1 2 3都没什么关系。当然后来我问了我的几个同事他们大多数回答了5。此处其实有两个坑,一是变量声明提升,二是函数表达式和函数声明的区别。
我们来看看为什么,可参考(1)关于Javascript的函数声明和函数表达式(2)关于JavaScript的变量提升
在Javascript中,定义函数有两种类型

函数声明

// 函数声明
function wscat(type) {
    return type === "wscat";
}

函数表达式

// 函数表达式
var oaoafly = function(type) {
    return type === "oaoafly";
}

先看下面这个经典问题,在一个程序里面同时用函数声明和函数表达式定义一个名为getName的函数

getName() //oaoafly
var getName = function() {
    console.log('wscat')
}
getName() //wscat
function getName() {
    console.log('oaoafly')
}
getName() //wscat

上面的代码看起来很类似,感觉也没什么太大差别。但实际上,Javascript函数上的一个“陷阱”就体现在Javascript两种类型的函数定义上。

  • JavaScript 解释器中存在一种变量声明被提升的机制,也就是说函数声明会被提升到作用域的最前面,即使写代码的时候是写在最后面,也还是会被提升至最前面。
  • 而用函数表达式创建的函数是在运行时进行赋值,且要等到表达式赋值完成后才能调用
var getName //变量被提升,此时为undefined

getName() //oaoafly 函数被提升 这里受函数声明的影响,虽然函数声明在最后可以被提升到最前面了
var getName = function() {
    console.log('wscat')
} //函数表达式此时才开始覆盖函数声明的定义
getName() //wscat
function getName() {
    console.log('oaoafly')
}
getName() //wscat 这里就执行了函数表达式的值

所以可以分解为这两个简单的问题来看清楚区别的本质

var getName;
console.log(getName) //undefined
getName() //Uncaught TypeError: getName is not a function
var getName = function() {
    console.log('wscat')
}            
var getName;
console.log(getName) //function getName() {console.log('oaoafly')}
getName() //oaoafly
function getName() {
    console.log('oaoafly')
}

这个区别看似微不足道,但在某些情况下确实是一个难以察觉并且“致命“的陷阱。出现这个陷阱的本质原因体现在这两种类型在函数提升和运行时机(解析时/运行时)上的差异。
当然我们给一个总结:Javascript中函数声明函数表达式是存在区别的,函数声明在JS解析时进行函数提升,因此在同一个作用域内,不管函数声明在哪里定义,该函数都可以进行调用。而函数表达式的值是在JS运行时确定,并且在表达式赋值完成后,该函数才能调用。
所以第二问的答案就是4,5的函数声明被4的函数表达式覆盖了

第三问

Foo().getName(); 先执行了Foo函数,然后调用Foo函数的返回值对象的getName属性函数。
Foo函数的第一句getName = function () { alert (1); };是一句函数赋值语句,注意它没有var声明,所以先向当前Foo函数作用域内寻找getName变量,没有。再向当前函数作用域上层,即外层作用域内寻找是否含有getName变量,找到了,也就是第二问中的alert(4)函数,将此变量的值赋值为function(){alert(1)}
此处实际上是将外层作用域内的getName函数修改了。

注意:此处若依然没有找到会一直向上查找到window对象,若window对象中也没有getName属性,就在window对象中创建一个getName变量。

之后Foo函数的返回值是this,而JS的this问题已经有非常多的文章介绍,这里不再多说。
简单的讲,this的指向是由所在函数的调用方式决定的。而此处的直接调用方式,this指向window对象。
遂Foo函数返回的是window对象,相当于执行window.getName(),而window中的getName已经被修改为alert(1),所以最终会输出1
此处考察了两个知识点,一个是变量作用域问题,一个是this指向问题
我们可以利用下面代码来回顾下这两个知识点

var name = "Wscats"; //全局变量
window.name = "Wscats"; //全局变量
function getName() {
    name = "Oaoafly"; //去掉var变成了全局变量
    var privateName = "Stacsw";
    return function() {
        console.log(this); //window
        return privateName
    }
}
var getPrivate = getName("Hello"); //当然传参是局部变量,但函数里面我没有接受这个参数
console.log(name) //Oaoafly
console.log(getPrivate()) //Stacsw

因为JS没有块级作用域,但是函数是能产生一个作用域的,函数内部不同定义值的方法会直接或者间接影响到全局或者局部变量,函数内部的私有变量可以用闭包获取,函数还真的是第一公民呀~
而关于this,this的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定this到底指向谁,实际上this的最终指向的是那个调用它的对象
所以第三问中实际上就是window在调用Foo()函数,所以this的指向是window

window.Foo().getName();
//->window.getName();

第四问

直接调用getName函数,相当于window.getName(),因为这个变量已经被Foo函数执行时修改了,遂结果与第三问相同,为1,也就是说Foo执行后把全局的getName函数给重写了一次,所以结果就是Foo()执行重写的那个getName函数

第五问

第五问new Foo.getName();此处考察的是JS的运算符优先级问题,我觉得这是这题灵魂的所在,也是难度比较大的一题
下面是JS运算符的优先级表格,从高到低排列。可参考MDN运算符优先级

优先级运算类型关联性运算符
19圆括号n/a( … )
18成员访问从左到右… . …
需计算的成员访问从左到右… [ … ]
new (带参数列表)n/a new… ( … )
17函数调用从左到右… ( … )
new (无参数列表)从右到左new …
16后置递增(运算符在后)n/a… ++
后置递减(运算符在后)n/a… --
15逻辑非从右到左! …
按位非从右到左~ …
一元加法从右到左+ …
一元减法从右到左- …
前置递增从右到左++ …
前置递减从右到左-- …
typeof从右到左typeof …
void从右到左void …
delete从右到左delete …
14乘法从左到右… * …
除法从左到右… / …
取模从左到右… % …
13加法从左到右… + …
减法从左到右… - …
12按位左移从左到右… << …
按位右移从左到右… >> …
无符号右移从左到右… >>> …
11小于从左到右… < …
小于等于从左到右… <= …
大于从左到右… > …
大于等于从左到右… >= …
in从左到右… in …
instanceof从左到右… instanceof …
10等号从左到右… == …
非等号从左到右… != …
全等号从左到右… === …
非全等号从左到右… !== …
9按位与从左到右… & …
8按位异或从左到右… ^ …
7按位或从左到右… 按位或 …
6逻辑与从左到右… && …
5逻辑或从左到右… 逻辑或 …
4条件运算符从右到左… ? … : …
3赋值从右到左… = …
… += …
… -= …
… *= …
… /= …
… %= …
… <<= …
… >>= …
… >>>= …
… &= …
… ^= …
… 或= …
2yield从右到左yield …
yield*从右到左yield* …
1展开运算符n/a... …
0逗号从左到右… , …

这题首先看优先级的第18和第17都出现关于new的优先级,new (带参数列表)比new (无参数列表)高比函数调用高,跟成员访问同级

new Foo.getName();的优先级是这样的

相当于是:

new (Foo.getName)();
  • 点的优先级(18)比new无参数列表(17)优先级高
  • 当点运算完后又因为有个括号(),此时就是变成new有参数列表(18),所以直接执行new,当然也可能有朋友会有疑问为什么遇到()不函数调用再new呢,那是因为函数调用(17)比new有参数列表(18)优先级低
.成员访问(18)->new有参数列表(18)

所以这里实际上将getName函数作为了构造函数来执行,遂弹出2。

第六问

这一题比上一题的唯一区别就是在Foo那里多出了一个括号,这个有括号跟没括号我们在第五问的时候也看出来优先级是有区别的

(new Foo()).getName()

那这里又是怎么判断的呢?首先new有参数列表(18)跟点的优先级(18)是同级,同级的话按照从左向右的执行顺序,所以先执行new有参数列表(18)再执行点的优先级(18),最后再函数调用(17)

new有参数列表(18)->.成员访问(18)->()函数调用(17)

这里还有一个小知识点,Foo作为构造函数有返回值,所以这里需要说明下JS中的构造函数返回值问题。

构造函数的返回值

在传统语言中,构造函数不应该有返回值,实际执行的返回值就是此构造函数的实例化对象。
而在JS中构造函数可以有返回值也可以没有。

  1. 没有返回值则按照其他语言一样返回实例化对象。
function Foo(name) {
    this.name = name
}
console.log(new Foo('wscats'))

image

  1. 若有返回值则检查其返回值是否为引用类型。如果是非引用类型,如基本类型(String,Number,Boolean,Null,Undefined)则与无返回值相同,实际返回其实例化对象。
function Foo(name) {
    this.name = name
    return 520
}
console.log(new Foo('wscats'))

image

  1. 若返回值是引用类型,则实际返回值为这个引用类型。
function Foo(name) {
    this.name = name
    return {
        age: 16
    }
}
console.log(new Foo('wscats'))

image
原题中,由于返回的是this,而this在构造函数中本来就代表当前实例化对象,最终Foo函数返回实例化对象。
之后调用实例化对象的getName函数,因为在Foo构造函数中没有为实例化对象添加任何属性,当前对象的原型对象(prototype)中寻找getName函数。
当然这里再拓展个题外话,如果构造函数和原型链都有相同的方法,如下面的代码,那么默认会拿构造函数的公有方法而不是原型链,这个知识点在原题中没有表现出来,后面改进版我已经加上。

function Foo(name) {
    this.name = name
    this.getName = function() {
        return this.name
    }
}
Foo.prototype.name = 'Oaoafly';
Foo.prototype.getName = function() {
    return 'Oaoafly'
}
console.log((new Foo('Wscats')).name) //Wscats
console.log((new Foo('Wscats')).getName()) //Wscats

第七问

new new Foo().getName();同样是运算符优先级问题。做到这一题其实我已经觉得答案没那么重要了,关键只是考察面试者是否真的知道面试官在考察我们什么。
最终实际执行为:

new ((new Foo()).getName)();
new有参数列表(18)->new有参数列表(18)

先初始化Foo的实例化对象,然后将其原型上的getName函数作为构造函数再次new,所以最终结果为3

答案

function Foo() {
    getName = function () { alert (1); };
    return this;
}
Foo.getName = function () { alert (2);};
Foo.prototype.getName = function () { alert (3);};
var getName = function () { alert (4);};
function getName() { alert (5);}

//答案:
Foo.getName();//2
getName();//4
Foo().getName();//1
getName();//1
new Foo.getName();//2
new Foo().getName();//3
new new Foo().getName();//3

后续

后续我把这题的难度再稍微加大一点点(附上答案),在Foo函数里面加多一个公有方法getName,对于下面这题如果用在面试题上那通过率可能就更低了,因为难度又大了一点,又多了两个坑,但是明白了这题的原理就等同于明白了上面所有的知识点了

function Foo() {
    this.getName = function() {
        console.log(3);
        return {
            getName: getName //这个就是第六问中涉及的构造函数的返回值问题
        }
    }; //这个就是第六问中涉及到的,JS构造函数公有方法和原型链方法的优先级
    getName = function() {
        console.log(1);
    };
    return this
}
Foo.getName = function() {
    console.log(2);
};
Foo.prototype.getName = function() {
    console.log(6);
};
var getName = function() {
    console.log(4);
};

function getName() {
    console.log(5);
} //答案:
Foo.getName(); //2
getName(); //4
console.log(Foo())
Foo().getName(); //1
getName(); //1
new Foo.getName(); //2
new Foo().getName(); //3
//多了一问
new Foo().getName().getName(); //3 1
new new Foo().getName(); //3             

最后,其实我是不建议把这些题作为考察面试者的唯一评判,但是作为一名合格的前端工程师我们不应该因为浮躁忽略了我们的一些最基本的基础知识,当然我也祝愿所有面试者找到一份理想的工作,祝愿所有面试官找到心中那匹千里马~

交流

如果文章和笔记能带您一丝帮助或者启发,请不要吝啬你的赞和收藏,文章同步持续更新,可以微信搜索「 前端遨游 」关注公众号方便你往后阅读,往期文章也收录在 https://github.com/Wscats/art...
欢迎您的关注和交流,你的肯定是我前进的最大动力?

image

查看原文

zhangl 收藏了文章 · 2020-03-01

深入浅出js中的策略模式

什么是策略模式,官方定义是:定义一些列算法,把他们封装起来,并且可以相互替换。说人话(⊙ˍ⊙):就是把看似毫无联系的代码提取封装、复用,使之更容易被理解和拓展。常见的用于一次if判断、switch枚举、数据字典等流程判断语句中。

使用策略模式计算等级

在游戏中,我们每玩完一局游戏都有对用户进行等级评价,比如S级4倍经验,A级3倍经验,B级2倍经验,其他1倍经验,用函数来表达如下:

    function getExperience(level, experience){
      if(level == 'S'){
        return 4*experience
      }
      if(level == 'A'){
        return 3*experience
      }
      if(level == 'B'){
        return 2*experience
      }
      return experience
    }

可知getExperience函数各种if条件判断,复用性差,我们根据策略模式封装复用的思想,进行改写。

    // 改为策略模式 分成两个函数来写
    const strategy = {
      'S' : function(experience){
        return 4*experience
      },
      'A' : function(experience){
        return 3*experience
      },
      'B' : function(experience){
        return 2*experience
      }
    }
    // getExperience可以复用
    function getExperience(strategy, level, experience){
      return (level in strategy) ? strategy[level](experience) : experience
    }
    var s = getExperience(strategy, 'S', 100)
    var a = getExperience(strategy, 'A', 100)
    console.log(s, a) // 400 300

分为两个函数之后,strategy对象解耦,拓展性强。在vue数据驱动视图更新的更新器updater使用,就使用了策略模式。想要进一步了解vue底层原理,可以参考可以参考github上的一篇文章 ☛ MVVM实现

// 指令处理集合
var compileUtil = {
    // v-text更新视图原理
    text: function(node, vm, exp) {
        this.bind(node, vm, exp, 'text');
    },
    // v-html更新视图原理
    html: function(node, vm, exp) {
        this.bind(node, vm, exp, 'html');
    },
    // v-class绑定原理
    class: function(node, vm, exp) {
        this.bind(node, vm, exp, 'class');
    },
    bind: function(node, vm, exp, dir) {
        // 不同指令触发视图更新
        var updaterFn = updater[dir + 'Updater'];
        updaterFn && updaterFn(node, this._getVMVal(vm, exp));
        new Watcher(vm, exp, function(value, oldValue) {
            updaterFn && updaterFn(node, value, oldValue);
        });
    }
    ......
}

使用策略模式验证表单

常见表单验证用if、else流程语句判断用户输入数据是否符合验证规则,而在Elementui中,基于async-validator库,只需要通过rule属性传入约定的验证规则,即可校验。方便快捷,可复用。现在我们根据策略模式仿写一个校验方式。

    // 我们写一个form表单
    <form action="/" class="form">
      <input type="text" name="username">
      <input type="password" name="password"> 
      <button>submit</button>
    </form>
    <div id="tip"></div>
  • 首先定义校验规则
    const strategies = {
      // 非空
      noEmpty: function(value, errMsg){
        if(value === ''){
          return errMsg
        }
      },
      // 最小长度
      minLength: function(value, length, errMsg){
        if(!value || value.length < length){
          return errMsg
        }
      },
      // 最大长度
      maxLength: function(value, length, errMsg){
        if(value.length > length){
          return errMsg
        }
      }
    }
  • 接着设置验证器
    // 创建验证器
    var Validator = function(strategies){
      this.strategies = strategies
      this.cache = [] // 存储校验规则
    }
    // 添加校验规则
    Validator.prototype.add = function(dom, rules){
      rules.forEach(item => {
        this.cache.push(() => {
          let value = dom.value
          let arr = item.rule.split(':')
          let name = arr.shift()
          let params = [value, ...arr, item.errMsg]
          // apply保证上下文一致
          return this.strategies[name].apply(dom, params)
        })
      })
    }
    // 校验结果
    Validator.prototype.validate = function(dom, rules, errMsg){
      // 遍历cache里面的校验函数
      for(let i = 0, validateFun; validateFun = this.cache[i++];){
        const message = validateFun()
        // 返回报错信息,终止验证并抛出异常
        if(message) return message
      }
    }
  • 最后进行校验
    var form = document.querySelector("form")
    // 提交表单
    form.onsubmit = function(event){
      event.preventDefault() 
      // 判断验证结果
      const message = validate()
      const tip = document.getElementById('tip')
      if(message){
        tip.innerHTML = message
        tip.style.color = 'red'
      }else{
        tip.innerHTML = '验证通过!'
        tip.style.color = 'green'
      }
    }
    // 校验函数
    function validate(){
      // 实例验证器
      const validator = new Validator(strategies)
      // 添加验证规则
      validator.add(form.username, [
        {
          rule: 'noEmpty',
          errMsg: '用户名不能为空!'
        },
        {
          rule: 'minLength:3',
          errMsg: '用户名长度大于3!'
        }
      ])
      validator.add(form.password, [
        {
          rule: 'minLength:6',
          errMsg: '密码长度大于6!'
        },
        {
          rule: 'maxLength:10',
          errMsg: '密码最大长度为10!'
        }
      ])
      // 进行校验,并返回结果
      return validator.validate()
    }

如上所示,我们只要添加strategies对象的属性,就能自定义自己的验证规则,并且可以复用,大大方便了日常开发!

查看原文

zhangl 收藏了文章 · 2019-02-20

理解 JavaScript 的 async/await

2020-06-04 更新

JavaScript 中的 async/await 是 AsyncFunction 特性 中的关键字。目前为止,除了 IE 之外,常用浏览器和 Node (v7.6+) 都已经支持该特性。具体支持情况可以在 这里 查看。


我第一次看到 async/await 这组关键字并不是在 JavaScript 语言里,而是在 C# 5.0 的语法中。C# 的 async/await 需要在 .NET Framework 4.5 以上的版本中使用,因此我还很悲伤了一阵——为了要兼容 XP 系统,我们开发的软件不能使用高于 4.0 版本的 .NET Framework。

我之前在《闲谈异步调用“扁平”化》 中就谈到了这个问题。无论是在 C# 还是 JavaScript 中,async/await 都是非常棒的特性,它们也都是非常甜的语法糖。C# 的 async/await 实现离不开 Task 或 Task\<Result\> 类,而 JavaScript 的 async/await 实现,也离不开 Promise

现在抛开 C# 和 .NET Framework,专心研究下 JavaScript 的 async/await。

1. async 和 await 在干什么

任意一个名称都是有意义的,先从字面意思来理解。async 是“异步”的简写,而 await 可以认为是 async wait 的简写。所以应该很好理解 async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。

另外还有一个很有意思的语法规定,await 只能出现在 async 函数中。然后细心的朋友会产生一个疑问,如果 await 只能出现在 async 函数中,那这个 async 函数应该怎么调用?

如果需要通过 await 来调用一个 async 函数,那这个调用的外面必须得再包一个 async 函数,然后……进入死循环,永无出头之日……

如果 async 函数不需要 await 来调用,那 async 到底起个啥作用?

1.1. async 起什么作用

这个问题的关键在于,async 函数是怎么处理它的返回值的!

我们当然希望它能直接通过 return 语句返回我们想要的值,但是如果真是这样,似乎就没 await 什么事了。所以,写段代码来试试,看它到底会返回什么:

async function testAsync() {
    return "hello async";
}

const result = testAsync();
console.log(result);

看到输出就恍然大悟了——输出的是一个 Promise 对象。

c:\var\test> node --harmony_async_await .
Promise { 'hello async' }

所以,async 函数返回的是一个 Promise 对象。从文档中也可以得到这个信息。async 函数(包含函数语句、函数表达式、Lambda表达式)会返回一个 Promise 对象,如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。

补充知识点 [2020-06-04]

Promise.resolve(x) 可以看作是 new Promise(resolve => resolve(x)) 的简写,可以用于快速封装字面量对象或其他对象,将其封装成 Promise 实例。

async 函数返回的是一个 Promise 对象,所以在最外层不能用 await 获取其返回值的情况下,我们当然应该用原来的方式:then() 链来处理这个 Promise 对象,就像这样

testAsync().then(v => {
    console.log(v);    // 输出 hello async
});

现在回过头来想下,如果 async 函数没有返回值,又该如何?很容易想到,它会返回 Promise.resolve(undefined)

联想一下 Promise 的特点——无等待,所以在没有 await 的情况下执行 async 函数,它会立即执行,返回一个 Promise 对象,并且,绝不会阻塞后面的语句。这和普通返回 Promise 对象的函数并无二致。

那么下一个关键点就在于 await 关键字了。

1.2. await 到底在等啥

一般来说,都认为 await 是在等待一个 async 函数完成。不过按语法说明,await 等待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值(换句话说,就是没有特殊限定)。

因为 async 函数返回一个 Promise 对象,所以 await 可以用于等待一个 async 函数的返回值——这也可以说是 await 在等 async 函数,但要清楚,它等的实际是一个返回值。注意到 await 不仅仅用于等 Promise 对象,它可以等任意表达式的结果,所以,await 后面实际是可以接普通函数调用或者直接量的。所以下面这个示例完全可以正确运行

function getSomething() {
    return "something";
}

async function testAsync() {
    return Promise.resolve("hello async");
}

async function test() {
    const v1 = await getSomething();
    const v2 = await testAsync();
    console.log(v1, v2);
}

test();

1.3. await 等到了要等的,然后呢

await 等到了它要等的东西,一个 Promise 对象,或者其它值,然后呢?我不得不先说,await 是个运算符,用于组成表达式,await 表达式的运算结果取决于它等的东西。

如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。

如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。

看到上面的阻塞一词,心慌了吧……放心,这就是 await 必须用在 async 函数中的原因。async 函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行。

2. async/await 帮我们干了啥

2.1. 作个简单的比较

上面已经说明了 async 会将其后的函数(函数表达式或 Lambda)的返回值封装成一个 Promise 对象,而 await 会等待这个 Promise 完成,并将其 resolve 的结果返回出来。

现在举例,用 setTimeout 模拟耗时的异步操作,先来看看不用 async/await 会怎么写

function takeLongTime() {
    return new Promise(resolve => {
        setTimeout(() => resolve("long_time_value"), 1000);
    });
}

takeLongTime().then(v => {
    console.log("got", v);
});

如果改用 async/await 呢,会是这样

function takeLongTime() {
    return new Promise(resolve => {
        setTimeout(() => resolve("long_time_value"), 1000);
    });
}

async function test() {
    const v = await takeLongTime();
    console.log(v);
}

test();

眼尖的同学已经发现 takeLongTime() 没有申明为 async。实际上,takeLongTime() 本身就是返回的 Promise 对象,加不加 async 结果都一样,如果没明白,请回过头再去看看上面的“async 起什么作用”。

又一个疑问产生了,这两段代码,两种方式对异步调用的处理(实际就是对 Promise 对象的处理)差别并不明显,甚至使用 async/await 还需要多写一些代码,那它的优势到底在哪?

2.2. async/await 的优势在于处理 then 链

单一的 Promise 链并不能发现 async/await 的优势,但是,如果需要处理由多个 Promise 组成的 then 链的时候,优势就能体现出来了(很有意思,Promise 通过 then 链来解决多层回调的问题,现在又用 async/await 来进一步优化它)。

假设一个业务,分多个步骤完成,每个步骤都是异步的,而且依赖于上一个步骤的结果。我们仍然用 setTimeout 来模拟异步操作:

/**
 * 传入参数 n,表示这个函数执行的时间(毫秒)
 * 执行的结果是 n + 200,这个值将用于下一步骤
 */
function takeLongTime(n) {
    return new Promise(resolve => {
        setTimeout(() => resolve(n + 200), n);
    });
}

function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);
}

function step2(n) {
    console.log(`step2 with ${n}`);
    return takeLongTime(n);
}

function step3(n) {
    console.log(`step3 with ${n}`);
    return takeLongTime(n);
}

现在用 Promise 方式来实现这三个步骤的处理

function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => step2(time2))
        .then(time3 => step3(time3))
        .then(result => {
            console.log(`result is ${result}`);
            console.timeEnd("doIt");
        });
}

doIt();

// c:\var\test>node --harmony_async_await .
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
// doIt: 1507.251ms

输出结果 resultstep3() 的参数 700 + 200 = 900doIt() 顺序执行了三个步骤,一共用了 300 + 500 + 700 = 1500 毫秒,和 console.time()/console.timeEnd() 计算的结果一致。

如果用 async/await 来实现呢,会是这样

async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time2);
    const result = await step3(time3);
    console.log(`result is ${result}`);
    console.timeEnd("doIt");
}

doIt();

结果和之前的 Promise 实现是一样的,但是这个代码看起来是不是清晰得多,几乎跟同步代码一样

2.3. 还有更酷的

现在把业务要求改一下,仍然是三个步骤,但每一个步骤都需要之前每个步骤的结果。

function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);
}

function step2(m, n) {
    console.log(`step2 with ${m} and ${n}`);
    return takeLongTime(m + n);
}

function step3(k, m, n) {
    console.log(`step3 with ${k}, ${m} and ${n}`);
    return takeLongTime(k + m + n);
}

这回先用 async/await 来写:

async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time1, time2);
    const result = await step3(time1, time2, time3);
    console.log(`result is ${result}`);
    console.timeEnd("doIt");
}

doIt();

// c:\var\test>node --harmony_async_await .
// step1 with 300
// step2 with 800 = 300 + 500
// step3 with 1800 = 300 + 500 + 1000
// result is 2000
// doIt: 2907.387ms

除了觉得执行时间变长了之外,似乎和之前的示例没啥区别啊!别急,认真想想如果把它写成 Promise 方式实现会是什么样子?

function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => {
            return step2(time1, time2)
                .then(time3 => [time1, time2, time3]);
        })
        .then(times => {
            const [time1, time2, time3] = times;
            return step3(time1, time2, time3);
        })
        .then(result => {
            console.log(`result is ${result}`);
            console.timeEnd("doIt");
        });
}

doIt();

有没有感觉有点复杂的样子?那一堆参数处理,就是 Promise 方案的死穴—— 参数传递太麻烦了,看着就晕!

3. 洗洗睡吧

就目前来说,已经理解 async/await 了吧?但其实还有一些事情没提及——Promise 有可能 reject 啊,怎么处理呢?如果需要并行处理3个步骤,再等待所有结果,又该怎么处理呢?

阮一峰老师已经说过了,我就懒得说了。

4. 推荐相关文章

5. 来跟边城(作者)学 更新@2020-11-14

TypeScript从入门到实践 【2020 版】

TypeScript从入门到实践 【2020 版】

6. 关于转载 补充@2020-03-05

常有读者问是否可以转载。

笔者表示欢迎各位转载,但转载时一定注明作者和出处,谢谢!


公众号-边城客栈
请关注公众号 边城客栈

看完了先别走,点个赞啊 ⇓,赞赏 ⇘ 也行!

查看原文

zhangl 收藏了文章 · 2018-12-28

从零搭建React全家桶框架教程

从零搭建React全家桶框架教程

源码地址:https://github.com/brickspert/react-family 欢迎star
提问反馈:blog

原文地址:https://github.com/brickspert/blog/issues/1(github这里我会不断更新教程的)

此处不更新,github上会一直更新

写在前面

当我第一次跟着项目做react项目的时候,由于半截加入的,对框架了解甚少,只能跟着别人的样板写。对整个框架没有一点了解。

做项目,总是要解决各种问题的,所以每个地方都需要去了解,但是对整个框架没有一个整体的了解,实在是不行。

期间,我也跟着别人的搭建框架的教程一步一步的走,但是经常因为自己太菜,走不下去。在经过各种蹂躏之后,对整个框架也有一个大概的了解,
我就想把他写下来,让后来的菜鸟能跟着我的教程对react全家桶有一个全面的认识。

我的这个教程,从新建根文件夹开始,到成型的框架,每个文件为什么要建立?建立了干什么?每个依赖都是干什么的?一步一步写下来,供大家学习。

当然,这个框架我以后会一直维护的,也希望大家能一起来完善这个框架,如果您有任何建议,欢迎留言,欢迎fork

在完善本框架的同时,我准备再新建一个兼容ie8的框架react-family-ie8,当然是基于该框架改造的。

说明

  1. 每个命令行块都是以根目录为基础的。例如下面命令行块,都是基于根目录的。
cd src/pages
mkdir Home
  1. 技术栈均是目前最新的。
  • react 15.6.1
  • react-router-dom 4.2.2
  • redux 3.7.2
  • webpack 3.5.5
  1. 目录说明
│  .babelrc                          #babel配置文件
│  package-lock.json
│  package.json
│  README.MD
│  webpack.config.js                 #webpack生产配置文件
│  webpack.dev.config.js             #webpack开发配置文件
│  
├─dist
├─public                             #公共资源文件
└─src                                #项目源码
    │  index.html                    #index.html模板
    │  index.js                      #入口文件
    │  
    ├─component                      #组建库
    │  └─Hello
    │          Hello.js
    │          
    ├─pages                          #页面目录
    │  ├─Counter
    │  │      Counter.js
    │  │      
    │  ├─Home
    │  │      Home.js
    │  │      
    │  ├─Page1
    │  │  │  Page1.css                #页面样式
    │  │  │  Page1.js
    │  │  │  
    │  │  └─images                    #页面图片
    │  │          brickpsert.jpg
    │  │          
    │  └─UserInfo
    │          UserInfo.js
    │          
    ├─redux
    │  │  reducers.js
    │  │  store.js
    │  │  
    │  ├─actions
    │  │      counter.js
    │  │      userInfo.js
    │  │      
    │  ├─middleware
    │  │      promiseMiddleware.js
    │  │      
    │  └─reducers
    │          counter.js
    │          userInfo.js
    │          
    └─router                        #路由文件
            Bundle.js
            router.js
            

init项目

  1. 创建文件夹并进入

    `mkdir react-family && cd react-family`
    
  2. init npm

    `npm init` 按照提示填写项目基本信息
    

webpack

  1. 安装 webpack

    npm install --save-dev webpack

    Q: 什么时候用--save-dev,什么时候用--save

    A: --save-dev 是你开发时候依赖的东西,--save 是你发布之后还依赖的东西。看这里

  2. 根据webpack文档编写最基础的配置文件

    新建webpack开发配置文件 touch webpack.dev.config.js

    webpack.dev.config.js

    const path = require('path');
    
    module.exports = {
     
        /*入口*/
        entry: path.join(__dirname, 'src/index.js'),
        
        /*输出到dist文件夹,输出文件名字为bundle.js*/
        output: {
            path: path.join(__dirname, './dist'),
            filename: 'bundle.js'
        }
    };
  3. 学会使用webpack编译文件

    新建入口文件

    mkdir src && touch ./src/index.js

    src/index.js 添加内容

    document.getElementById('app').innerHTML = "Webpack works"

    现在我们执行命令 webpack --config webpack.dev.config.js

    我们可以看到生成了dist文件夹和bundle.js

  4. 现在我们测试下~

    dist文件夹下面新建一个index.html

    touch ./dist/index.html

    dist/index.html填写内容

    <!doctype html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Document</title>
    </head>
    <body>
    <div id="app"></div>
    <script type="text/javascript" data-original="./bundle.js" charset="utf-8"></script>
    </body>
    </html>

    用浏览器打开index.html,可以看到Webpack works!

    webpack

    现在回头看下,我们做了什么或者说webpack做了什么。

    把入口文件 index.js 经过处理之后,生成 bundle.js。就这么简单。

babel

Babel 把用最新标准编写的 JavaScript 代码向下编译成可以在今天随处可用的版本。 这一过程叫做“源码到源码”编译, 也被称为转换编译。

通俗的说,就是我们可以用ES6, ES7等来编写代码,Babel会把他们统统转为ES5。

npm install --save-dev babel-core babel-loader babel-preset-es2015 babel-preset-react babel-preset-stage-0

新建babel配置文件.babelrc

touch .babelrc

.babelrc

 {
   "presets": [
     "es2015",
     "react",
     "stage-0"
   ],
   "plugins": []
 }

修改webpack.dev.config.js,增加babel-loader

 /*src文件夹下面的以.js结尾的文件,要使用babel解析*/
 /*cacheDirectory是用来缓存编译结果,下次编译加速*/
 module: {
     rules: [{
         test: /\.js$/,
         use: ['babel-loader?cacheDirectory=true'],
         include: path.join(__dirname, 'src')
     }]
 }

现在我们简单测试下,是否能正确转义ES6~

修改 src/index.js

 /*使用es6的箭头函数*/
 var func = str => {
     document.getElementById('app').innerHTML = str;
 };
 func('我现在在使用Babel!');

执行打包命令webpack --config webpack.dev.config.js

浏览器打开index.html,我们看到正确输出了我现在在使用Babel!

babel

然后我们打开打包后的bundle.js,翻页到最下面,可以看到箭头函数被转换成普通函数了!

babel-bundle.png

Q: babel-preset-state-0,babel-preset-state-1,babel-preset-state-2,babel-preset-state-3有什么区别?

A: 每一级包含上一级的功能,比如 state-0包含state-1的功能,以此类推。state-0功能最全。具体可以看这篇文章:babel配置-各阶段的stage的区别

参考地址:

  1. https://segmentfault.com/a/11...
  2. http://www.ruanyifeng.com/blo...

react

npm install --save react react-dom

修改 src/index.js使用react

import React from 'react';
import ReactDom from 'react-dom';

ReactDom.render(
    <div>Hello React!</div>, document.getElementById('app'));

执行打包命令webpack --config webpack.dev.config.js

打开index.html 看效果。

我们简单做下改进,把Hello React放到组件里面。体现组件化~

cd src
mkdir component
cd component
mkdir Hello
cd Hello
touch Hello.js

按照React语法,写一个Hello组件

import React, {Component} from 'react';

export default class Hello extends Component {
    render() {
        return (
            <div>
                Hello,React!
            </div>
        )
    }
}

然后让我们修改src/index.js,引用Hello组件!

src/index.js

import React from 'react';
import ReactDom from 'react-dom';
import Hello from './component/Hello/Hello';

ReactDom.render(
    <Hello/>, document.getElementById('app'));

根目录执行打包命令

webpack --config webpack.dev.config.js

打开index.html看效果咯~

命令优化

Q:每次打包都得在根目录执行这么一长串命令webpack --config webpack.dev.config.js,能不打这么长吗?

A:修改package.json里面的script,增加dev-build

package.json

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev-build": "webpack --config webpack.dev.config.js"
  }

现在我们打包只需要执行npm start-build就可以啦!

参考地址:

http://www.ruanyifeng.com/blo...

react-router

npm install --save react-router-dom

新建router文件夹和组件

cd src
mkdir router && touch router/router.js

按照react-router文档编辑一个最基本的router.js。包含两个页面homepage1

src/router/router.js

import React from 'react';

import {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom';

import Home from '../pages/Home/Home';
import Page1 from '../pages/Page1/Page1';


const getRouter = () => (
    <Router>
        <div>
            <ul>
                <li><Link to="/">首页</Link></li>
                <li><Link to="/page1">Page1</Link></li>
            </ul>
            <Switch>
                <Route exact path="/" component={Home}/>
                <Route path="/page1" component={Page1}/>
            </Switch>
        </div>
    </Router>
);

export default getRouter;

新建页面文件夹

cd src
mkdir pages

新建两个页面 Home,Page1

cd src/pages
mkdir Home && touch Home/Home.js
mkdir Page1 && touch Page1/Page1.js

填充内容:

src/pages/Home/Home.js

import React, {Component} from 'react';

export default class Home extends Component {
    render() {
        return (
            <div>
                this is home~
            </div>
        )
    }
}

Page1.js

import React, {Component} from 'react';

export default class Page1 extends Component {
    render() {
        return (
            <div>
                this is Page1~
            </div>
        )
    }
}

现在路由和页面建好了,我们在入口文件src/index.js引用Router。

修改src/index.js

import React from 'react';
import ReactDom from 'react-dom';

import getRouter from './router/router';

ReactDom.render(
    getRouter(), document.getElementById('app'));

现在执行打包命令npm start-build。打开index.html查看效果啦!

那么问题来了~我们发现点击‘首页’和‘Page1’没有反应。不要惊慌,这是正常的。

我们之前一直用这个路径访问index.html,类似这样:file:///F:/react/react-family/dist/index.html
这种路径了,不是我们想象中的路由那样的路径http://localhost:3000~我们需要配置一个简单的WEB服务器,指向
index.html~有下面两种方法来实现

  1. Nginx, Apache, IIS等配置启动一个简单的的WEB服务器。
  2. 使用webpack-dev-server来配置启动WEB服务器。

下一节,我们来使用第二种方法启动服务器。这一节的DEMO,先放这里。

参考地址

  1. http://www.jianshu.com/p/e3ad...
  2. http://reacttraining.cn/web/g...

webpack-dev-server

简单来说,webpack-dev-server就是一个小型的静态文件服务器。使用它,可以为webpack打包生成的资源文件提供Web服务。

npm install webpack-dev-server --save-dev

修改webpack.dev.config.js,增加webpack-dev-server的配置。

webpack.dev.config.js

    devServer: {
        contentBase: path.join(__dirname, './dist')
    }

现在执行

webpack-dev-server --config webpack.dev.config.js

浏览器打开http://localhost:8080,OK,现在我们可以点击首页,Page1了,
看URL地址变化啦!我们看到react-router已经成功了哦。

Q: --content-base是什么?

A:URL的根目录。如果不设定的话,默认指向项目根目录。

**重要提示:webpack-dev-server编译后的文件,都存储在内存中,我们并不能看见的。你可以删除之前遗留的文件dist/bundle.js
仍然能正常打开网站!**

每次执行webpack-dev-server --config webpack.dev.config.js,要打很长的命令,我们修改package.json,增加script->start:

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev-build": "webpack --config webpack.dev.config.js",
    "start": "webpack-dev-server --config webpack.dev.config.js"
  }

下次执行npm start就可以了。

既然用到了webpack-dev-server,我们就看看它的其他的配置项
看了之后,发现有几个我们可以用的。

  • color(CLI only) console中打印彩色日志
  • historyApiFallback 任意的404响应都被替代为index.html。有什么用呢?你现在运行
    npm start,然后打开浏览器,访问http://localhost:8080,然后点击Page1到链接http://localhost:8080/page1

然后刷新页面试试。是不是发现刷新后404了。为什么?dist文件夹里面并没有page1.html,当然会404了,所以我们需要配置
historyApiFallback,让所有的404定位到index.html

  • host 指定一个host,默认是localhost。如果你希望服务器外部可以访问,指定如下:host: "0.0.0.0"。比如你用手机通过IP访问。
  • hot 启用Webpack的模块热替换特性。关于热模块替换,我下一小节专门讲解一下。
  • port 配置要监听的端口。默认就是我们现在使用的8080端口。
  • proxy 代理。比如在 localhost:3000 上有后端服务的话,你可以这样启用代理:
    proxy: {
      "/api": "http://localhost:3000"
    }
  • progress(CLI only) 将编译进度输出到控制台。

根据这几个配置,修改下我们的webpack-dev-server的配置~

webpack.dev.config.js

    devServer: {
        contentBase: path.join(__dirname, './dist'),
        historyApiFallback: true,
        host: '0.0.0.0'
    }

CLI ONLY的需要在命令行中配置

package.json

"dev": "webpack-dev-server --config webpack.dev.config.js --color --progress"

现在我们执行npm start 看看效果。是不是看到打包的时候有百分比进度?在http://localhost:8080/page1页面刷新是不是没问题了?
用手机通过局域网IP是否可以访问到网站?

参考地址:

  1. https://segmentfault.com/a/11...
  2. https://webpack.js.org/guides...

模块热替换(Hot Module Replacement)

到目前,当我们修改代码的时候,浏览器会自动刷新,不信你可以去试试。(如果你的不会刷新,看看这个调整文本编辑器

我相信看这个教程的人,应该用过别人的框架。我们在修改代码的时候,浏览器不会刷新,只会更新自己修改的那一块。我们也要实现这个效果。

我们看下webpack模块热替换教程。

我们接下来要这么修改

package.json 增加 --hot

"dev": "webpack-dev-server --config webpack.dev.config.js --color --progress --hot"

src/index.js 增加module.hot.accept(),如下。当模块更新的时候,通知index.js

src/index.js

import React from 'react';
import ReactDom from 'react-dom';

import getRouter from './router/router';

if (module.hot) {
    module.hot.accept();
}

ReactDom.render(
    getRouter(), document.getElementById('app'));

现在我们执行npm start,打开浏览器,修改Home.js,看是不是不刷新页面的情况下,内容更新了?惊不惊喜?意不意外?

做模块热替换,我们只改了几行代码,非常简单的。纸老虎一个~

现在我需要说明下我们命令行使用的--hot,可以通过配置webpack.dev.config.js来替换,
向文档上那样,修改下面三处。但我们还是用--hot吧。下面的方式我们知道一下就行,我们不用。同样的效果。

const webpack = require('webpack');

devServer: {
    hot: true
}

plugins:[
     new webpack.HotModuleReplacementPlugin()
]

HRM配置其实有两种方式,一种CLI方式,一种Node.js API方式。我们用到的就是CLI方式,比较简单。
Node.js API方式,就是建一个server.js等等,网上大部分教程都是这种方式,这里不做讲解了。

你以为模块热替换到这里就结束了?no~no~no~

上面的配置对react模块的支持不是很好哦。

例如下面的demo,当模块热替换的时候,state会重置,这不是我们想要的。

修改Home.js,增加计数state

src/pages/Home/Home.js

import React, {Component} from 'react';

export default class Home extends Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        }
    }

    _handleClick() {
        this.setState({
            count: ++this.state.count
        });
    }

    render() {
        return (
            <div>
                this is home~<br/>
                当前计数:{this.state.count}<br/>
                <button onClick={() => this._handleClick()}>自增</button>
            </div>
        )
    }
}

你可以测试一下,当我们修改代码的时候,webpack在更新页面的时候,也把count初始为0了。

为了在react模块更新的同时,能保留state等页面中其他状态,我们需要引入react-hot-loader~

Q: 请问webpack-dev-serverreact-hot-loader两者的热替换有什么区别?

A: 区别在于webpack-dev-server自己的--hot模式只能即时刷新页面,但状态保存不住。因为React有一些自己语法(JSX)是HotModuleReplacementPlugin搞不定的。
react-hot-loader--hot基础上做了额外的处理,来保证状态可以存下来。(来自segmentfault

下面我们来加入react-hot-loader v3,

安装依赖

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

根据文档
我们要做如下几个修改~

  1. .babelrc 增加 react-hot-loader/babel

.babelrc

{
  "presets": [
    "es2015",
    "react",
    "stage-0"
  ],
  "plugins": [
    "react-hot-loader/babel"
  ]
}
  1. webpack.dev.config.js入口增加react-hot-loader/patch

webpack.dev.config.js

    entry: [
        'react-hot-loader/patch',
        path.join(__dirname, 'src/index.js')
    ]
  1. src/index.js修改如下

src/index.js

import React from 'react';
import ReactDom from 'react-dom';
import {AppContainer} from 'react-hot-loader';

import getRouter from './router/router';

/*初始化*/
renderWithHotReload(getRouter());

/*热更新*/
if (module.hot) {
    module.hot.accept('./router/router', () => {
        const getRouter = require('./router/router').default;
        renderWithHotReload(getRouter());
    });
}

function renderWithHotReload(RootElement) {
    ReactDom.render(
        <AppContainer>
            {RootElement}
        </AppContainer>,
        document.getElementById('app')
    )
}

现在,执行npm start,试试。是不是修改页面的时候,state不更新了?

参考文章:

  1. https://github.com/gaearon/re...

文件路径优化

做到这里,我们简单休息下。做下优化~

在之前写的代码中,我们引用组件,或者页面时候,写的是相对路径~

比如src/router/router.js里面,引用Home.js的时候就用的相对路径

import Home from '../pages/Home/Home';

webpack提供了一个别名配置,就是我们无论在哪个路径下,引用都可以这样

import Home from 'pages/Home/Home';

下面我们来配置下,修改webpack.dev.config.js,增加别名~

webpack.config.js

    resolve: {
        alias: {
            pages: path.join(__dirname, 'src/pages'),
            component: path.join(__dirname, 'src/component'),
            router: path.join(__dirname, 'src/router')
        }
    }

然后我们把之前使用的绝对路径统统改掉。

src/router/router.js


import Home from 'pages/Home/Home';
import Page1 from 'pages/Page1/Page1';

src/index.js

import getRouter from 'router/router';

我们这里约定,下面,我们会默认配置需要的别名路径,不再做重复的讲述哦。

redux

接下来,我们就要就要就要集成redux了。

要对redux有一个大概的认识,可以阅读阮一峰前辈的Redux 入门教程(一):基本用法

如果要对redux有一个非常详细的认识,我推荐阅读中文文档,写的非常好。读了这个教程,有一个非常深刻的感觉,redux并没有任何魔法。

不要被各种关于 reducers, middleware, store 的演讲所蒙蔽 ---- Redux 实际是非常简单的。

当然,我这篇文章是写给新手的,如果看不懂上面的文章,或者不想看,没关系。先会用,多用用就知道原理了。

开始整代码!我们就做一个最简单的计数器。自增,自减,重置。

先安装reduxnpm install --save redux

初始化目录结构

cd src
mkdir redux
cd redux
mkdir actions
mkdir reducers
touch reducers.js
touch store.js
touch actions/counter.js
touch reducers/counter.js

先来写action创建函数。通过action创建函数,可以创建action~
src/redux/actions/counter.js

/*action*/

export const INCREMENT = "counter/INCREMENT";
export const DECREMENT = "counter/DECREMENT";
export const RESET = "counter/RESET";

export function increment() {
    return {type: INCREMENT}
}

export function decrement() {
    return {type: DECREMENT}
}

export function reset() {
    return {type: RESET}
}

再来写reducer,reducer是一个纯函数,接收action和旧的state,生成新的state.

src/redux/reducers/counter.js

import {INCREMENT, DECREMENT, RESET} from '../actions/counter';

/*
* 初始化state
 */

const initState = {
    count: 0
};
/*
* reducer
 */
export default function reducer(state = initState, action) {
    switch (action.type) {
        case INCREMENT:
            return {
                count: state.count + 1
            };
        case DECREMENT:
            return {
                count: state.count - 1
            };
        case RESET:
            return {count: 0};
        default:
            return state
    }
}

一个项目有很多的reducers,我们要把他们整合到一起

src/redux/reducers.js

import counter from './reducers/counter';

export default function combineReducers(state = {}, action) {
    return {
        counter: counter(state.counter, action)
    }
}

到这里,我们必须再理解下一句话。

reducer就是纯函数,接收stateaction,然后返回一个新的 state

看看上面的代码,无论是combineReducers函数也好,还是reducer函数也好,都是接收stateaction
返回更新后的state。区别就是combineReducers函数是处理整棵树,reducer函数是处理树的某一点。

接下来,我们要创建一个store

前面我们可以使用 action 来描述“发生了什么”,使用action创建函数来返回action

还可以使用 reducers 来根据 action 更新 state

那我们如何提交action?提交的时候,怎么才能触发reducers呢?

store 就是把它们联系到一起的对象。store 有以下职责:

  • 维持应用的 state
  • 提供 getState() 方法获取 state
  • 提供 dispatch(action) 触发reducers方法更新 state
  • 通过 subscribe(listener) 注册监听器;
  • 通过 subscribe(listener) 返回的函数注销监听器。

src/redux/store.js

import {createStore} from 'redux';
import combineReducers from './reducers.js';

let store = createStore(combineReducers);

export default store;

到现在为止,我们已经可以使用redux了~

下面我们就简单的测试下

cd src
cd redux
touch testRedux.js

src/redux/testRedux.js

import {increment, decrement, reset} from './actions/counter';

import store from './store';

// 打印初始状态
console.log(store.getState());

// 每次 state 更新时,打印日志
// 注意 subscribe() 返回一个函数用来注销监听器
let unsubscribe = store.subscribe(() =>
    console.log(store.getState())
);

// 发起一系列 action
store.dispatch(increment());
store.dispatch(decrement());
store.dispatch(reset());

// 停止监听 state 更新
unsubscribe();

当前文件夹执行命令

webpack testRedux.js build.js

node build.js

是不是看到输出了state变化?

{ counter: { count: 0 } }
{ counter: { count: 1 } }
{ counter: { count: 0 } }
{ counter: { count: 0 } }

做这个测试,就是为了告诉大家,reduxreact没关系,虽说他俩能合作。

到这里,我建议你再理下redux的数据流,看看这里

  1. 调用store.dispatch(action)提交action
  2. redux store调用传入的reducer函数。把当前的stateaction传进去。
  3. reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。
  4. Redux store 保存了根 reducer 返回的完整 state 树。

就是酱紫~~

这会webpack.dev.config.js路径别名增加一下,后面好写了。

webpack.config.js

        alias: {
            ...
            actions: path.join(__dirname, 'src/redux/actions'),
            reducers: path.join(__dirname, 'src/redux/reducers'),
            redux: path.join(__dirname, 'src/redux')
        }

把前面的相对路径都改改。

下面我们开始搭配react使用。

写一个Counter页面

cd src/pages
mkdir Counter
touch Counter/Counter.js

src/pages/Counter/Counter.js

import React, {Component} from 'react';

export default class Counter extends Component {
    render() {
        return (
            <div>
                <div>当前计数为(显示redux计数)</div>
                <button onClick={() => {
                    console.log('调用自增函数');
                }}>自增
                </button>
                <button onClick={() => {
                    console.log('调用自减函数');
                }}>自减
                </button>
                <button onClick={() => {
                    console.log('调用重置函数');
                }}>重置
                </button>
            </div>
        )
    }
}

修改路由,增加Counter

src/router/router.js

import React from 'react';

import {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom';

import Home from 'pages/Home/Home';
import Page1 from 'pages/Page1/Page1';
import Counter from 'pages/Counter/Counter';

const getRouter = () => (
    <Router>
        <div>
            <ul>
                <li><Link to="/">首页</Link></li>
                <li><Link to="/page1">Page1</Link></li>
                <li><Link to="/counter">Counter</Link></li>
            </ul>
            <Switch>
                <Route exact path="/" component={Home}/>
                <Route path="/page1" component={Page1}/>
                <Route path="/counter" component={Counter}/>
            </Switch>
        </div>
    </Router>
);

export default getRouter;

npm start看看效果。

下一步,我们让Counter组件和Redux联合起来。使Counter能获得到Reduxstate,并且能发射action

当然我们可以使用刚才测试testRedux的方法,手动监听~手动引入store~但是这肯定很麻烦哦。

react-redux提供了一个方法connect

容器组件就是使用 store.subscribe() 从 Redux state 树中读取部分数据,并通过 props 来把这些数据提供给要渲染的组件。你可以手工来开发容器组件,但建议使用 React Redux 库的 connect() 方法来生成,这个方法做了性能优化来避免很多不必要的重复渲染。

connect接收两个参数,一个mapStateToProps,就是把reduxstate,转为组件的Props,还有一个参数是mapDispatchToprops,
就是把发射actions的方法,转为Props属性函数。

先来安装react-redux

npm install --save react-redux

src/pages/Counter/Counter.js

import React, {Component} from 'react';
import {increment, decrement, reset} from 'actions/counter';

import {connect} from 'react-redux';

class Counter extends Component {
    render() {
        return (
            <div>
                <div>当前计数为{this.props.counter.count}</div>
                <button onClick={() => this.props.increment()}>自增
                </button>
                <button onClick={() => this.props.decrement()}>自减
                </button>
                <button onClick={() => this.props.reset()}>重置
                </button>
            </div>
        )
    }
}

const mapStateToProps = (state) => {
    return {
        counter: state.counter
    }
};

const mapDispatchToProps = (dispatch) => {
    return {
        increment: () => {
            dispatch(increment())
        },
        decrement: () => {
            dispatch(decrement())
        },
        reset: () => {
            dispatch(reset())
        }
    }
};

export default connect(mapStateToProps, mapDispatchToProps)(Counter);

下面我们要传入store

所有容器组件都可以访问 Redux store,所以可以手动监听它。一种方式是把它以 props 的形式传入到所有容器组件中。但这太麻烦了,因为必须要用 store 把展示组件包裹一层,仅仅是因为恰好在组件树中渲染了一个容器组件。

建议的方式是使用指定的 React Redux 组件 <Provider> 来 魔法般的 让所有容器组件都可以访问 store,而不必显示地传递它。只需要在渲染根组件时使用即可。

src/index.js

import React from 'react';
import ReactDom from 'react-dom';
import {AppContainer} from 'react-hot-loader';
import {Provider} from 'react-redux';
import store from './redux/store';

import getRouter from 'router/router';

/*初始化*/
renderWithHotReload(getRouter());

/*热更新*/
if (module.hot) {
    module.hot.accept('./router/router', () => {
        const getRouter = require('router/router').default;
        renderWithHotReload(getRouter());
    });
}

function renderWithHotReload(RootElement) {
    ReactDom.render(
        <AppContainer>
            <Provider store={store}>
                {RootElement}
            </Provider>
        </AppContainer>,
        document.getElementById('app')
    )
}

到这里我们就可以执行npm start,打开localhost:8080/counter看效果了。

但是你发现npm start一直报错

ERROR in ./node_modules/react-redux/es/connect/mapDispatchToProps.js
Module not found: Error: Can't resolve 'redux' in 'F:\Project\react\react-family\node_modules\react-redux\es\connect'

ERROR in ./src/redux/store.js
Module not found: Error: Can't resolve 'redux' in 'F:\Project\react\react-family\src\redux'

WTF?这个错误困扰了半天。我说下为什么造成这个错误。我们引用redux的时候这样用的

import {createStore} from 'redux'

然而,我们在webapck.dev.config.js里面这样配置了

    resolve: {
        alias: {
            ...
            redux: path.join(__dirname, 'src/redux')
        }
    }

然后webapck编译的时候碰到redux都去src/redux去找了。但是找不到啊。所以我们把webpack.dev.config.js里面redux这一行删除了,就好了。
并且把使用我们自己使用redux文件夹的地方改成相对路径哦。

现在你可以npm start去看效果了。

这里我们再缕下(可以读React 实践心得:react-redux 之 connect 方法详解

  1. Provider组件是让所有的组件可以访问到store。不用手动去传。也不用手动去监听。
  2. connect函数作用是从 Redux state 树中读取部分数据,并通过 props 来把这些数据提供给要渲染的组件。也传递dispatch(action)函数到props

接下来,我们要说异步action

参考地址: http://cn.redux.js.org/docs/a...

想象一下我们调用一个异步get请求去后台请求数据:

  1. 请求开始的时候,界面转圈提示正在加载。isLoading置为true
  2. 请求成功,显示数据。isLoading置为false,data填充数据。
  3. 请求失败,显示失败。isLoading置为false,显示错误信息。

下面,我们以向后台请求用户基本信息为例。

  1. 我们先创建一个user.json,等会请求用,相当于后台的API接口。
cd dist
mkdir api
cd api
touch user.json

dist/api/user.json

{
  "name": "brickspert",
  "intro": "please give me a star"
}
  1. 创建必须的action创建函数。
cd src/redux/actions
touch userInfo.js

src/redux/actions/getUserInfo.js

export const GET_USER_INFO_REQUEST = "userInfo/GET_USER_INFO_REQUEST";
export const GET_USER_INFO_SUCCESS = "userInfo/GET_USER_INFO_SUCCESS";
export const GET_USER_INFO_FAIL = "userInfo/GET_USER_INFO_FAIL";

function getUserInfoRequest() {
    return {
        type: GET_USER_INFO_REQUEST
    }
}

function getUserInfoSuccess(userInfo) {
    return {
        type: GET_USER_INFO_SUCCESS,
        userInfo: userInfo
    }
}

function getUserInfoFail() {
    return {
        type: GET_USER_INFO_FAIL
    }
}

我们创建了请求中,请求成功,请求失败三个action创建函数。

  1. 创建reducer

再强调下,reducer是根据stateaction生成新state纯函数

cd src/redux/reducers
touch userInfo.js

src/redux/reducers/userInfo.js

import {GET_USER_INFO_REQUEST, GET_USER_INFO_SUCCESS, GET_USER_INFO_FAIL} from 'actions/userInfo';


const initState = {
    isLoading: false,
    userInfo: {},
    errorMsg: ''
};

export default function reducer(state = initState, action) {
    switch (action.type) {
        case GET_USER_INFO_REQUEST:
            return {
                ...state,
                isLoading: true,
                userInfo: {},
                errorMsg: ''
            };
        case GET_USER_INFO_SUCCESS:
            return {
                ...state,
                isLoading: false,
                userInfo: action.userInfo,
                errorMsg: ''
            };
        case GET_USER_INFO_FAIL:
            return {
                ...state,
                isLoading: false,
                userInfo: {},
                errorMsg: '请求错误'
            };
        default:
            return state;
    }
}

这里的...state语法,是和别人的Object.assign()起同一个作用,合并新旧state。我们这里是没效果的,但是我建议都写上这个哦

组合reducer

src/redux/reducers.js

import counter from 'reducers/counter';
import userInfo from 'reducers/userInfo';

export default function combineReducers(state = {}, action) {
    return {
        counter: counter(state.counter, action),
        userInfo: userInfo(state.userInfo, action)
    }
}
  1. 现在有了action,有了reducer,我们就需要调用把action里面的三个action函数和网络请求结合起来。

    • 请求中 dispatch getUserInfoRequest
    • 请求成功 dispatch getUserInfoSuccess
    • 请求失败 dispatch getUserInfoFail

src/redux/actions/userInfo.js增加

export function getUserInfo() {
    return function (dispatch) {
        dispatch(getUserInfoRequest());

        return fetch('http://localhost:8080/api/user.json')
            .then((response => {
                return response.json()
            }))
            .then((json) => {
                    dispatch(getUserInfoSuccess(json))
                }
            ).catch(
                () => {
                    dispatch(getUserInfoFail());
                }
            )
    }
}

我们这里发现,别的action创建函数都是返回action对象:

{type: xxxx}

但是我们现在的这个action创建函数 getUserInfo则是返回函数了。

为了让action创建函数除了返回action对象外,还可以返回函数,我们需要引用redux-thunk

npm install --save redux-thunk

这里涉及到redux中间件middleware,我后面会讲到的。你也可以读这里Middleware

简单的说,中间件就是action在到达reducer,先经过中间件处理。我们之前知道reducer能处理的action只有这样的{type:xxx},所以我们使用中间件来处理
函数形式的action,把他们转为标准的actionreducer。这是redux-thunk的作用。
使用redux-thunk中间件

我们来引入redux-thunk中间件

src/redux/store.js

import {createStore, applyMiddleware} from 'redux';
import thunkMiddleware from 'redux-thunk';
import combineReducers from './reducers.js';

let store = createStore(combineReducers, applyMiddleware(thunkMiddleware));

export default store;

到这里,redux这边OK了,我们来写个组件验证下。

cd src/pages
mkdir UserInfo
cd UserInfo
touch UserInfo.js

src/pages/UserInfo/UserInfo.js

import React, {Component} from 'react';
import {connect} from 'react-redux';
import {getUserInfo} from "actions/userInfo";

class UserInfo extends Component {

    render() {
        const {userInfo, isLoading, errorMsg} = this.props.userInfo;
        return (
            <div>
                {
                    isLoading ? '请求信息中......' :
                        (
                            errorMsg ? errorMsg :
                                <div>
                                    <p>用户信息:</p>
                                    <p>用户名:{userInfo.name}</p>
                                    <p>介绍:{userInfo.intro}</p>
                                </div>
                        )
                }
                <button onClick={() => this.props.getUserInfo()}>请求用户信息</button>
            </div>
        )
    }
}

export default connect((state) => ({userInfo: state.userInfo}), {getUserInfo})(UserInfo);

这里你可能发现connect参数写法不一样了,mapStateToProps函数用了es6简写,mapDispatchToProps用了react-redux提供的简单写法。

增加路由
src/router/router.js

import React from 'react';

import {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom';

import Home from 'pages/Home/Home';
import Page1 from 'pages/Page1/Page1';
import Counter from 'pages/Counter/Counter';
import UserInfo from 'pages/UserInfo/UserInfo';

const getRouter = () => (
    <Router>
        <div>
            <ul>
                <li><Link to="/">首页</Link></li>
                <li><Link to="/page1">Page1</Link></li>
                <li><Link to="/counter">Counter</Link></li>
                <li><Link to="/userinfo">UserInfo</Link></li>
            </ul>
            <Switch>
                <Route exact path="/" component={Home}/>
                <Route path="/page1" component={Page1}/>
                <Route path="/counter" component={Counter}/>
                <Route path="/userinfo" component={UserInfo}/>
            </Switch>
        </div>
    </Router>
);

export default getRouter;

现在你可以执行npm start去看效果啦!

redux

到这里redux集成基本告一段落了,后面我们还会有一些优化。

combinReducers优化

redux提供了一个combineReducers函数来合并reducer,不用我们自己合并哦。写起来简单,但是意思和我们
自己写的combinReducers也是一样的。

src/redux/reducers.js

import {combineReducers} from "redux";

import counter from 'reducers/counter';
import userInfo from 'reducers/userInfo';


export default combineReducers({
    counter,
    userInfo
});

devtool优化

现在我们发现一个问题,代码哪里写错了,浏览器报错只报在build.js第几行。

错误图片

这让我们分析错误无从下手。看这里

我们增加webpack配置devtool

src/webpack.dev.config.js增加

devtool: 'inline-source-map'

这次看错误信息是不是提示的很详细了?

错误图片

同时,我们在srouce里面能看到我们写的代码,也能打断点调试哦~

错误图片

编译css

先说这里为什么不用scss,因为Windows使用node-sass,需要先安装 Microsoft Windows SDK for Windows 7 and .NET Framework 4
我怕有些人copy这份代码后,没注意,运行不起来。所以这里不用scss了,如果需要,自行编译哦。

npm install css-loader style-loader --save-dev

css-loader使你能够使用类似@importurl(...)的方法实现 require()的功能;

style-loader将所有的计算后的样式加入页面中; 二者组合在一起使你能够把样式表嵌入webpack打包后的JS文件中。

webpack.dev.config.jsrules增加

{
   test: /\.css$/,
   use: ['style-loader', 'css-loader']
}

我们用Page1页面来测试下

cd src/pages/Page1
touch Page1.css

src/pages/Page1/Page1.css

.page-box {
    border: 1px solid red;
}

src/pages/Page1/Page1.js

import React, {Component} from 'react';

import './Page1.css';

export default class Page1 extends Component {
    render() {
        return (
            <div className="page-box">
                this is page1~
            </div>
        )
    }
}

好了,现在npm start去看效果吧。

编译图片

npm install --save-dev url-loader file-loader

webpack.dev.config.jsrules增加

{
    test: /\.(png|jpg|gif)$/,
    use: [{
        loader: 'url-loader',
        options: {
            limit: 8192
        }
    }]
}

options limit 8192意思是,小于等于8K的图片会被转成base64编码,直接插入HTML中,减少HTTP请求。

我们来用Page1 测试下

cd src/pages/Page1
mkdir images

images文件夹放一个图片。

修改代码,引用图片

src/pages/Page1/Page1.js

import React, {Component} from 'react';

import './Page1.css';

import image from './images/brickpsert.jpg';

export default class Page1 extends Component {
    render() {
        return (
            <div className="page-box">
                this is page1~
                <img data-original={image}/>
            </div>
        )
    }
}

可以去看看效果啦。

按需加载

为什么要实现按需加载?

我们现在看到,打包完后,所有页面只生成了一个build.js,当我们首屏加载的时候,就会很慢。因为他也下载了别的页面的js了哦。

如果每个页面都打包了自己单独的JS,在进入自己页面的时候才加载对应的js,那首屏加载就会快很多哦。

react-router 2.0时代, 按需加载需要用到的最关键的一个函数,就是require.ensure(),它是按需加载能够实现的核心。

在4.0版本,官方放弃了这种处理按需加载的方式,选择了一个更加简洁的处理方式。

传送门

根据官方示例,我们开搞

  1. npm install bundle-loader --save-dev
  2. 新建bundle.js
cd src/router
touch Bundle.js

src/router/Bundle.js

import React, {Component} from 'react'

class Bundle extends Component {
    state = {
        // short for "module" but that's a keyword in js, so "mod"
        mod: null
    };

    componentWillMount() {
        this.load(this.props)
    }

    componentWillReceiveProps(nextProps) {
        if (nextProps.load !== this.props.load) {
            this.load(nextProps)
        }
    }

    load(props) {
        this.setState({
            mod: null
        });
        props.load((mod) => {
            this.setState({
                // handle both es imports and cjs
                mod: mod.default ? mod.default : mod
            })
        })
    }

    render() {
        return this.props.children(this.state.mod)
    }
}

export default Bundle;
  1. 改造路由器

src/router/router.js

import React from 'react';

import {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom';

import Bundle from './Bundle';

import Home from 'bundle-loader?lazy&name=home!pages/Home/Home';
import Page1 from 'bundle-loader?lazy&name=page1!pages/Page1/Page1';
import Counter from 'bundle-loader?lazy&name=counter!pages/Counter/Counter';
import UserInfo from 'bundle-loader?lazy&name=userInfo!pages/UserInfo/UserInfo';

const Loading = function () {
    return <div>Loading...</div>
};

const createComponent = (component) => () => (
    <Bundle load={component}>
        {
            (Component) => Component ? <Component/> : <Loading/>
        }
    </Bundle>
);

const getRouter = () => (
    <Router>
        <div>
            <ul>
                <li><Link to="/">首页</Link></li>
                <li><Link to="/page1">Page1</Link></li>
                <li><Link to="/counter">Counter</Link></li>
                <li><Link to="/userinfo">UserInfo</Link></li>
            </ul>
            <Switch>
                <Route exact path="/" component={createComponent(Home)}/>
                <Route path="/page1" component={createComponent(Page1)}/>
                <Route path="/counter" component={createComponent(Counter)}/>
                <Route path="/userinfo" component={createComponent(UserInfo)}/>
            </Switch>
        </div>
    </Router>
);

export default getRouter;

现在你可以npm start,打开浏览器,看是不是进入新的页面,都会加载自己的JS的~

但是你可能发现,名字都是0.bundle.js这样子的,这分不清楚是哪个页面的js呀!

我们修改下webpack.dev.config.js,加个chunkFilenamechunkFilename是除了entry定义的入口js之外的js~

    output: {
        path: path.join(__dirname, './dist'),
        filename: 'bundle.js',
        chunkFilename: '[name].js'
    }

现在你运行发现名字变成home.js,这样的了。棒棒哒!

那么问题来了home是在哪里设置的?webpack怎么知道他叫home

其实在这里我们定义了,router.js里面

import Home from 'bundle-loader?lazy&name=home!pages/Home/Home';

看到没。这里有个name=home。嘿嘿。

参考地址:

  1. http://www.jianshu.com/p/8dd9...
  2. https://github.com/ReactTrain...
  3. https://segmentfault.com/a/11...
  4. http://react-china.org/t/webp...
  5. https://juejin.im/post/58f971...

缓存

想象一下这个场景~

我们网站上线了,用户第一次访问首页,下载了home.js,第二次访问又下载了home.js~

这肯定不行呀,所以我们一般都会做一个缓存,用户下载一次home.js后,第二次就不下载了。

有一天,我们更新了home.js,但是用户不知道呀,用户还是使用本地旧的home.js。出问题了~

怎么解决?每次代码更新后,打包生成的名字不一样。比如第一次叫home.a.js,第二次叫home.b.js

文档看这里

我们照着文档来

webpack.dev.config.js

    output: {
        path: path.join(__dirname, './dist'),
        filename: '[name].[hash].js',
        chunkFilename: '[name].[chunkhash].js'
    }

每次打包都用增加hash~

现在我们试试,是不是修改了文件,打包后相应的文件名字就变啦?

package

但是你可能发现了,网页打开报错了~因为你dist/index.html里面引用js名字还是bundle.js老名字啊,改成新的名字就可以啦。

啊~那岂不是我每次编译打包,都得去改一下js名字?欲知后事如何,且看下节分享。

HtmlWebpackPlugin

这个插件,每次会自动把js插入到你的模板index.html里面去。

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

新建模板index.html

cd src
touch index.html

src/index.html

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

修改webpack.dev.config.js,增加plugin

var HtmlWebpackPlugin = require('html-webpack-plugin');

    plugins: [new HtmlWebpackPlugin({
        filename: 'index.html',
        template: path.join(__dirname, 'src/index.html')
    })],

npm start运行项目,看看是不是能正常访问啦。~

说明一下:npm start打包后的文件存在内存中,你看不到的。~ 你可以把遗留dist/index.html删除掉了。

提取公共代码

想象一下,我们的主文件,原来的bundle.js里面是不是包含了react,redux,react-router等等
这些代码??这些代码基本上不会改变的。但是,他们合并在bundle.js里面,每次项目发布,重新请求bundle.js的时候,相当于重新请求了
react等这些公共库。浪费了~

我们把react这些不会改变的公共库提取出来,用户缓存下来。从此以后,用户再也不用下载这些库了,无论是否发布项目。嘻嘻。

webpack文档给了教程,看这里

webpack.dev.config.js

    var webpack = require('webpack');

    entry: {
        app: [
            'react-hot-loader/patch',
            path.join(__dirname, 'src/index.js')
        ],
        vendor: ['react', 'react-router-dom', 'redux', 'react-dom', 'react-redux']
    }
    
        /*plugins*/
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor'
        })

react等库生成打包到vendor.hash.js里面去。

但是你现在可能发现编译生成的文件app.[hash].jsvendor.[hash].js生成的hash一样的,这里是个问题,因为呀,你每次修改代码,都会导致vendor.[hash].js名字改变,那我们提取出来的意义也就没了。其实文档上写的很清楚,

   output: {
        path: path.join(__dirname, './dist'),
        filename: '[name].[hash].js', //这里应该用chunkhash替换hash
        chunkFilename: '[name].[chunkhash].js'
    }

但是无奈,如果用chunkhash,会报错。和webpack-dev-server --hot不兼容,具体看这里

现在我们在配置开发版配置文件,就向webpack-dev-server妥协,因为我们要用他。问题先放这里,等会我们配置正式版webpack.config.js的时候要解决这个问题。

生产坏境构建

开发环境(development)和生产环境(production)的构建目标差异很大。在开发环境中,我们需要具有强大的、具有实时重新加载(live reloading)或热模块替换(hot module replacement)能力的 source map 和 localhost server。而在生产环境中,我们的目标则转向于关注更小的 bundle,更轻量的 source map,以及更优化的资源,以改善加载时间。由于要遵循逻辑分离,我们通常建议为每个环境编写彼此独立的 webpack 配置。

文档看这里

我们要开始做了~

touch webpack.config.js

webpack.dev.config.js的基础上先做以下几个修改~

  1. 先删除webpack-dev-server相关的东西~
  2. devtool的值改成cheap-module-source-map
  3. 刚才说的hash改成chunkhash

webpack.config.js

const path = require('path');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var webpack = require('webpack');

module.exports = {
    devtool: 'cheap-module-source-map',
    entry: {
        app: [
            path.join(__dirname, 'src/index.js')
        ],
        vendor: ['react', 'react-router-dom', 'redux', 'react-dom', 'react-redux']
    },
    output: {
        path: path.join(__dirname, './dist'),
        filename: '[name].[chunkhash].js',
        chunkFilename: '[name].[chunkhash].js'
    },
    module: {
        rules: [{
            test: /\.js$/,
            use: ['babel-loader'],
            include: path.join(__dirname, 'src')
        }, {
            test: /\.css$/,
            use: ['style-loader', 'css-loader']
        }, {
            test: /\.(png|jpg|gif)$/,
            use: [{
                loader: 'url-loader',
                options: {
                    limit: 8192
                }
            }]
        }]
    },
    plugins: [
        new HtmlWebpackPlugin({
            filename: 'index.html',
            template: path.join(__dirname, 'src/index.html')
        }),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor'
        })
    ],

    resolve: {
        alias: {
            pages: path.join(__dirname, 'src/pages'),
            component: path.join(__dirname, 'src/component'),
            router: path.join(__dirname, 'src/router'),
            actions: path.join(__dirname, 'src/redux/actions'),
            reducers: path.join(__dirname, 'src/redux/reducers')
        }
    }
};

package.json增加打包脚本

"build":"webpack --config webpack.config.js"

然后执行npm run build~看看dist文件夹是不是生成了我们发布要用的所有文件哦?

接下来我们还是要优化正式版配置文件~

文件压缩

webpack使用UglifyJSPlugin来压缩生成的文件。

npm i --save-dev uglifyjs-webpack-plugin

webpack.config.js

const UglifyJSPlugin = require('uglifyjs-webpack-plugin')

module.exports = {
  plugins: [
    new UglifyJSPlugin()
  ]
}

npm run build发现打包文件大小减小了好多。

uglify

指定环境

许多 library 将通过与 process.env.NODE_ENV 环境变量关联,以决定 library 中应该引用哪些内容。例如,当不处于生产环境中时,某些 library 为了使调试变得容易,可能会添加额外的日志记录(log)和测试(test)。其实,当使用 process.env.NODE_ENV === 'production' 时,一些 library 可能针对具体用户的环境进行代码优化,从而删除或添加一些重要代码。我们可以使用 webpack 内置的 DefinePlugin 为所有的依赖定义这个变量:

webpack.config.js

module.exports = {
  plugins: [
       new webpack.DefinePlugin({
          'process.env': {
              'NODE_ENV': JSON.stringify('production')
           }
       })
  ]
}

npm run build后发现vendor.[hash].js又变小了。

uglify

优化缓存

刚才我们把[name].[hash].js变成[name].[chunkhash].js后,npm run build后,
发现app.xxx.jsvendor.xxx.js不一样了哦。

但是现在又有一个问题了。

你随便修改代码一处,例如Home.js,随便改变个字,你发现home.xxx.js名字变化的同时,
vendor.xxx.js名字也变了。这不行啊。这和没拆分不是一样一样了吗?我们本意是vendor.xxx.js
名字永久不变,一直缓存在用户本地的。~

官方文档推荐了一个插件HashedModuleIdsPlugin

    plugins: [
        new webpack.HashedModuleIdsPlugin()
    ]

现在你打包,修改代码再试试,是不是名字不变啦?错了,现在打包,我发现名字还是变了,经过比对文档,我发现还要加一个runtime代码抽取,

new webpack.optimize.CommonsChunkPlugin({
    name: 'runtime'
})

加上这句话就好了~为什么呢?看下解释

注意,引入顺序在这里很重要。CommonsChunkPlugin 的 'vendor' 实例,必须在 'runtime' 实例之前引入。

public path

想象一个场景,我们的静态文件放在了单独的静态服务器上去了,那我们打包的时候,如何让静态文件的链接定位到静态服务器呢?

看文档Public Path

webpack.config.jsoutput 中增加一个publicPath,我们当前用/,相对于当前路径,如果你要改成别的url,就改这里就好了。

    output: {
        publicPath : '/'
    }

打包优化

你现在打开dist,是不是发现好多好多文件,每次打包后的文件在这里混合了?我们希望每次打包前自动清理下dist文件。

npm install clean-webpack-plugin --save-dev

webpack.config.js

const CleanWebpackPlugin = require('clean-webpack-plugin');


plugins: [
    new CleanWebpackPlugin(['dist'])
]

现在npm run bundle试试,是不是之前的都清空了。当然我们之前的api文件夹也被清空了,不过没关系哦~本来就是测试用的。

抽取css

目前我们的css是直接打包进js里面的,我们希望能单独生成css文件。

我们使用extract-text-webpack-plugin来实现。

npm install --save-dev extract-text-webpack-plugin

webpack.config.js

const ExtractTextPlugin = require("extract-text-webpack-plugin");

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ExtractTextPlugin.extract({
          fallback: "style-loader",
          use: "css-loader"
        })
      }
    ]
  },
  plugins: [
     new ExtractTextPlugin({
         filename: '[name].[contenthash:5].css',
         allChunks: true
     })
  ]
}

npm run build后发现单独生成了css文件哦

使用axiosmiddleware优化API请求

先安装下axios

npm install --save axios

我们之前项目的一次API请求是这样写的哦~

action创建函数是这样的。比我们现在写的fetch简单多了。

export function getUserInfo() {
    return {
        types: [GET_USER_INFO_REQUEST, GET_USER_INFO_SUCCESS, GET_USER_INFO_FAIL],
        promise: client => client.get(`http://localhost:8080/api/user.json`)
        afterSuccess:(dispatch,getState,response)=>{
            /*请求成功后执行的函数*/
        },
        otherData:otherData
    }
}

然后在dispatch(getUserInfo())后,通过redux中间件来处理请求逻辑。

中间件的教程看这里

我们想想中间件的逻辑

  1. 请求前dispatchREQUEST请求。
  2. 成功后dispatchSUCCESS请求,如果定义了afterSuccess()函数,调用它。
  3. 失败后dispatchFAIL请求。

来写一个

cd src/redux
mkdir middleware
cd middleware
touch promiseMiddleware.js

src/redux/middleware/promiseMiddleware.js

import axios from 'axios';

export default  store => next => action => {
    const {dispatch, getState} = store;
    /*如果dispatch来的是一个function,此处不做处理,直接进入下一级*/
    if (typeof action === 'function') {
        action(dispatch, getState);
    }
    /*解析action*/
    const {
        promise,
        types,
        afterSuccess,
        ...rest
    } = action;

    /*没有promise,证明不是想要发送ajax请求的,就直接进入下一步啦!*/
    if (!action.promise) {
        return next(action);
    }

    /*解析types*/
    const [REQUEST,
        SUCCESS,
        FAILURE] = types;

    /*开始请求的时候,发一个action*/
    next({
        ...rest,
        type: REQUEST
    });
    /*定义请求成功时的方法*/
    const onFulfilled = result => {
        next({
            ...rest,
            result,
            type: SUCCESS
        });
        if (afterSuccess) {
            afterSuccess(dispatch, getState, result);
        }
    };
    /*定义请求失败时的方法*/
    const onRejected = error => {
        next({
            ...rest,
            error,
            type: FAILURE
        });
    };

    return promise(axios).then(onFulfilled, onRejected).catch(error => {
        console.error('MIDDLEWARE ERROR:', error);
        onRejected(error)
    })
}

修改src/redux/store.js来应用这个中间件

import {createStore, applyMiddleware} from 'redux';
import combineReducers from './reducers.js';

import promiseMiddleware from './middleware/promiseMiddleware'

let store = createStore(combineReducers, applyMiddleware(promiseMiddleware));

export default store;

修改src/redux/actions/userInfo.js

export const GET_USER_INFO_REQUEST = "userInfo/GET_USER_INFO_REQUEST";
export const GET_USER_INFO_SUCCESS = "userInfo/GET_USER_INFO_SUCCESS";
export const GET_USER_INFO_FAIL = "userInfo/GET_USER_INFO_FAIL";

export function getUserInfo() {
    return {
        types: [GET_USER_INFO_REQUEST, GET_USER_INFO_SUCCESS, GET_USER_INFO_FAIL],
        promise: client => client.get(`http://localhost:8080/api/user.json`)
    }
}

是不是简单清新很多啦?

修改src/redux/reducers/userInfo.js

        case GET_USER_INFO_SUCCESS:
            return {
                ...state,
                isLoading: false,
                userInfo: action.result.data,
                errorMsg: ''
            };

action.userInfo修改成了action.result.data。你看中间件,请求成功,会给action增加一个result字段来存储响应结果哦~不用手动传了。

npm start看看我们的网络请求是不是正常哦。

调整文本编辑器

使用自动编译代码时,可能会在保存文件时遇到一些问题。某些编辑器具有“安全写入”功能,可能会影响重新编译。

要在一些常见的编辑器中禁用此功能,请查看以下列表:

  • Sublime Text 3 - 在用户首选项(user preferences)中添加 atomic_save: "false"。
  • IntelliJ - 在首选项(preferences)中使用搜索,查找到 "safe write" 并且禁用它。
  • Vim - 在设置(settings)中增加 :set backupcopy=yes。
  • WebStorm - 在 Preferences > Appearance & Behavior > System Settings 中取消选中 Use "safe write"。
查看原文

zhangl 赞了问题 · 2018-12-27

react-router4按需加载显示loading

react-router4配置了按需加载,现在网络不好情况下分割的js还没请求回来页面是空白的,怎么配置分割的js还没有请求回来之前页面显示一个loading动画,等到对应的分割的js请求回来了之后隐藏掉loading显示页面?

关注 5 回答 3

认证与成就

  • 获得 40 次点赞
  • 获得 16 枚徽章 获得 0 枚金徽章, 获得 5 枚银徽章, 获得 11 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-11-30
个人主页被 691 人浏览