前端开发工具插件实战
大家在进行前端开发过程中,可能都会养成一个习惯,当我们面对某一个特定问题,寻求解 决方案的时候,都会在社区中找好用的插件。如果能够找到匹配的插件,原本两天的工作量, 可能半个小时就搞定了; 不管是以前撑起前端大半江山的JQuery,还是现在提升前端开发体验的构建工具例如 Webpack,都采用了插件架构;、
插件架构 插件架构宏观上来讲就是一种框架能够在确定的点上执行外部的代码,而不需要提前知 道这部分代码的细节;它既可以很简单,也可以很复杂。我们可以编写 webpack 插件,也可以开发 vs code 的 插件,其基本架构是相似的。开发插件需要遵循一些约定,就像网络传输需要协议。它们必须能够被主进程以某种方 式获取并使用。通常最初的开发者会发布一些接口或开发套件,允许其他的开发者对原 系统开发插件,提供新的能力。插件架构是开放封闭原则(OCP)的一种开发原则的体现,表明系统对拓展开放,对修 改封闭。插件架构解决了不需要修改核心系统代码而可以对系统增加一些额外的功能特 性,只需要一些额外的代码。插件可以单独开发,单独测试。
一. Axios是什么?
一个基于 Promise 来管理 http 请求的简洁、易用且高效的代码封装库。通俗一点来讲,它是一个前端替代Ajax的一个东西,可以使用它发起http请求接口功能,它是基于Promise的,相比于Ajax的回调函数能够更好的管理异步操作
Axios源码分析
1、教大家怎么看NPM包的源码,第一步先看package.json,主要关注package.json中的main字段,它的值(一个相对路径)代表这个包的入口文件。这里可以看出axios的包的入口文件是index.js文件,再看一下包的scripts执行脚本,然后可以在本地执行脚本进行调试。
[Axios/axios.js]进入入口文件,可以看出axios的内部逻辑均在lib文件夹下
[Axios/lib/axios.js]进入axios文件里,先查看export了什么内容,然后再根据导出的内容往前看。
module.exports = axios;
[Axios/lib/axios.js]最终导出了axios,来看看axios是什么?
function createInstance(defaultConfig) {
var context = new Axios(defaultConfig);
...
return instance;
}
var axios = createInstance(defaults);
axios是createInstance函数传入了defaultConfig参数返回的值
[Axios/lib/defaults.js]先看看这个参数defaults是什么。
// 通过nodejs中的process和浏览器的XMLHttpRequest来区别当前在前端还是nodejs中
function getDefaultAdapter() {
var adapter;
if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
adapter = require('./adapters/http');
} else if (typeof XMLHttpRequest !== 'undefined') {
adapter = require('./adapters/xhr');
}
return adapter;
}
var defaults = {
adapter: getDefaultAdapter(),
...
timeout: 0,
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
maxContentLength: -1,
validateStatus: function validateStatus(status) {
return status >= 200 && status < 300;
}
};
defaults.headers = {
common: {
'Accept': 'application/json, text/plain, */*'
}
};
从上面便可以看出来,axios能够即在客户端使用又能在浏览器使用的奥秘,它是通过Nodejs和浏览器中各自的全局变量来区别当前在哪个环境下,然后底层各自实现,再暴露出一套统一的API出来给我们使用。同时它还默认了想 超时时间,Headers信息,alidateStatus等一些默认值进去,当我们在使用的时候不传递覆盖这些值时,即走默认的配置。
6、[Axios/adapters/http.js]先来看看Nodejs中的Axios的实现
// 进入./adapters/http,
...
var http = require('http');
var https = require('https');
...
module.exports = function httpAdapter(config) {
return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) {
...
var req = transport.request(options, function handleResponse(res) {
...
// 处理response后返回
})
// req的error处理
req.on('error', function handleRequestError(err) {
...
});
req.end(data);
})
}
从上可以看出在Nodejs中,Axios的实现其实是基于nodejs的http或者http模块来发起请求的。
[Axios/adapters/xhr.js]再来看看在浏览器中的Axios的实现
//进入./adapters/xhr文件下
...
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
...
var request = new XMLHttpRequest();
...
request.open(config.method.toUpperCase(), buildURL(config.url, config.params, config.paramsSerializer), true);
...
request.onreadystatechange = function handleLoad() {
...
};
...
request.onabort = function handleAbort() {...}
...
request.onerror = function handleError() {...}
...
request.ontimeout = function handleTimeout() {...}
...
request.send(requestData);
...
})
}
一个完整的Ajax库封装流程,只不过axios暴露了一个Promise出去,所以axios在浏览器端和Ajax底层的原理是一样的,都是通过浏览器的XMLhttpRequest这个底层接口进行的一次封装。
[Axios/lib/core/Axios.js]上面已经看了在入口进去的axios文件中,createInstance函数传递的参数,接下来再看看createInstance内部的Axios构造函数做了什么。
// axios中
...
var context = new Axios(defaultConfig);
...
function Axios(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
Axios这个构造函数将刚才传入的defaultConfig参数挂到自己的this上,然后新增了一个interceptors拦截器对象,这个对象有request和response两个属性,接下来看一下这两个属性中的InterceptorManager 这个构造函数又是什么。
9、[Axios/lib/core/InterceptorManager.js]拦截器里的构造函数
// 进入core/InterceptorManager.js
InterceptorManager.prototype.use = function() {...}
InterceptorManager.prototype.eject = function() {...}
InterceptorManager.prototype.forEach = function forEach(fn) {...}
拦截器暴露了三个方法use,eject,forEach三个方法,相信大家很多人在写自己的拦截器的时候都是用过use这个属性。后面两个比较少用,但是可以通过它的代码看出来,eject是删除use过的内容,forEach则是循环执行传入fn,整个拦截器部分就看完了。
10、[Axios/lib/core/Axios.js]再回到Axios.js文件里查看Axios 构造函数。
...
Axios.prototype.request = function request(config) {...})
Axios.prototype.getUri = function getUri(config) {...})
...
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
/*eslint func-names:0*/
Axios.prototype[method] = function(url, config) {
return this.request(utils.merge(config || {}, {
method: method,
url: url
}));
};
});
...
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
/*eslint func-names:0*/
Axios.prototype[method] = function(url, data, config) {
return this.request(utils.merge(config || {}, {
method: method,
url: url,
data: data
}));
};
});
接下来又在Axios构造函数下,挂载到原型上了9个方法,包括我们常用的下述方法。
axios.request(config)
axios.getUri(config)
axios.get(url[, config])
axios.delete(url[, config])
axios.head(url[, config])
axios.options(url[, config])
axios.post(url[, data[, config]])
axios.put(url[, data[, config]])
axios.patch(url[, data[, config]])
构造函数Axios看完了,再回到我们的入口文件axios下。
11、[Axios/lib/axios.js]回到入口文件
function createInstance(defaultConfig) {
var context = new Axios(defaultConfig);
var instance = bind(Axios.prototype.request, context);
utils.extend(instance, Axios.prototype, context);
utils.extend(instance, context);
return instance;
}
首先看到一个bind方法将Axios.prototype.request和context作为参数传了进入,Axios.prototype.request是一个函数,context是一个构造出的对象,上面这一步操作就是我们可以使用Axios.get这一类静态方法的原因。
12、[Axios/lib/helper/bind.js]看看bind函数又做了什么。
module.exports = function bind(fn, thisArg) {
return function wrap() {
var args = new Array(arguments.length);
for (var i = 0; i < args.length; i++) {
args[i] = arguments[i];
}
return fn.apply(thisArg, args);
};
};
bind函数在执行后返回了一个新函数,然后做了一次参数的深拷贝,接下来利用函数的apply,将传入的第二个对象参数作为一个函数参数的指定this,进行执行,即入口文件中在执行后为
Axios.prototype.request.bind(new Axios(defaultConfig);)
13、[Axios/lib/axios.js]接下来再看入口文件中剩下的两行。
...
utils.extend(instance, Axios.prototype, context);
utils.extend(instance, context);
...
从前面已经得出instance是一个函数,Axios.prototype和context均为两个对象。
14、[Axios/lib/utils.js]看一下utils.extend的实现。
function extend(a, b, thisArg) {
forEach(b, function assignValue(val, key) {
if (thisArg && typeof val === 'function') {
a[key] = bind(val, thisArg);
} else {
a[key] = val;
}
});
return a;
}
extend的简单大方,最后返回了第一个参数,即intance函数,并且给这个函数挂上一些静态属性。挂载的过程是先遍历了传入的Axios.prototype对象,如果该对象的值是函数就将函数绑定this后挂载在instance函数上,如果不是函数则直接挂载在构造函数上,我们使用的get,post等所有的方法均是通过这种方式进行挂载。
this指向
-
执行函数前有 '.'
点操作符的话,函数体中的this
就指向前面的对象,没有就指向window
,严格模式下指向undefined
。这句话特别的重要,请记住 -
函数没有直接调用者 this
指向全局对象(浏览器中是window,node中是 global)。如匿名函数等 -
构造函数的 this
指向实例本身。 -
箭头函数本身没有 this
的,箭头函数的this
指向最近的非箭头函数this
,找不到就指向window
,严格模式下指向undefined
var name = '林一一'
function fn(){
var name = '林二二'
return this.name
}
fn() // 林一一
复制代码
执行函数 fn()
,前面没有 '.'
点操作符吧,那么这里的 this
就指向 window
。输出的就是全局下的 name = '林一一'
。
再来看一下这句话:执行函数前有 '.'
点操作符的话,函数体中的 this
就指向前面的对象,没有就指向 window
var name = '林二二'
var obj = {
name: '林一一',
fn: function () {
return this.name
}
}
console.log(obj.fn()) // '林一一'
var fo = obj.fn
console.log(fo()) // '林二二' fo() ==> window.fo()
复制代码
obj.fn()
中函数 fn()
前面有 '.'
点操作符吧,那么这里的 this
就指向 obj
这个对象。再看执行函数 fo()
,前面没有 '.'
点操作符吧,那么这里的 this
就指向 window
。其实上面的函数 fo() ==> window.fo()
,所以执行函数 fo()
前面也是可以看作是有 '.'
操作符的。
var name = '林二二'
var obj = {
name: '林一一',
fn: function () {
var name = '小三'
return function(){
return this.name
}
}
}
console.log(obj.fn()()) // 林二二
var fo = obj.fn()
console.log(fo()) // 林二二
obj.fn()()
中 obj.fn()
执行完后有一个函数(这里称为函数 A
)返回,最后相当于执行函数 A()
, A()
前面没有 '.'
点操作符吧,那么这里的 this
就指向 window
,输出就是 林二二
了。上面的 fo()
函数同理
函数没有直接调用者(更新)
函数没有直接调用者 this
指向全局对象(浏览器中是window,node中是 global),如匿名函数等;严格模式下指向 undefined
var name = '林一一';
!(function(){
console.log(this.name) // 林一一
})()
自执行函数没有直接的调用者输出的 name = '林一一'
var name = '林一一'
var obj = {
name : '二二',
callback: function(){
console.log(this.name)
}
}
setTimeout(obj.callback,1000)
/* 输出
* 林一一
var name = '林一一'
var obj = {
name : '二二',
callback: function(){
console.log(this.name)
}
}
setTimeout(obj.callback,1000)
/* 输出
* 林一一
构造函数中的 this
来读一下这句话:构造函数的 this
指向实例本身
function Fn(){
var n = 0
this.name = '林一一'
this.age = 18
this.getName = function(){
return this.name
}
}
Fn.prototype.getAge = function(){
return this.age
}
Fn.x = '林二二'
var f = new Fn()
console.log(f.name) // 林一一
console.log(f.getName()) // 林一一
console.log(f.getAge()) // 18
console.log(f.n) // undefined
console.log(f.x) // undefined
复制代码
function Fn(){
var n = 0
this.name = '林一一'
this.age = 18
this.getName = function(){
return this.name
}
}
Fn.prototype.getAge = function(){
return this.age
}
Fn.x = '林二二'
var f = new Fn()
console.log(f.name) // 林一一
console.log(f.getName()) // 林一一
console.log(f.getAge()) // 18
console.log(f.n) // undefined
console.log(f.x) // undefined
复制代码
上面的 Fn
经过 new
后就是一个构造函数,this
就指向实例 f
。所以上面的1,2输出都是林一一
。f.getAge()
是实例 f
调用了getAge
输出就是 18,问:实例 f
中并没有属性 getAge
是怎么输出 18的,f.x
输出又为什么是 undefined
?答:这是原型链的查找机制,属性 x
不是在原型 prototype
上的就不是实例的属性,可以读一下这篇文章 面试 | 你不得不懂得 JS 原型和原型链;问:为什么f.n
输出的是 undefined
。因为变量 n
是构造函数的私有变量和 new
创建的实例没有关系。
箭头函数
-
箭头函数本身没有 this
,箭头函数的this
继承上下文的,里面的this
会指向当前最近的非箭头函数的this
,找不到就是window
(严格模式是undefined) -
箭头函数的this,是定义时指定的不是执行时指定的
var name = '林一一'
var obj = {
name: '二二',
a: () => {
console.log(this.name)
}
}
obj.a()
/* 输出
* '林一一'
*/
箭头函数的 this
,找不到非箭头函数的 this
就直接指向 window
。
var name = '林一一'
var obj = {
name: '二二',
fn: function() {
return () => {
console.log(this.name)
}
}
}
obj.fn()()
/* 输出
* '二二'
*/
很明显箭头函数的 this
来自函数 fn
,对象 obj
调用了函数 fn
,所以 fn
的 this
指向 obj
,输出结果就是 二二
。
call,apply,bind 改变 this 的指向
提示:所有的函数都是基于 Function
这个基类来创建的,同样拥有 Function
原型上面的方法
-
call
,接受this
的对象,和一组列表。apply
和call
一样,唯一不同的是apply
接受的是一个包含多个参数的数组。bind
同样也是改变函数的this
指向,只不过bind
执行后会返回一个新的函数,新函数中参数来源于剩余的参数
热身题
var name = '林一一'
var age = 18
function fn(){
return this.name
}
function p(){
return {
age: this.age,
arg: arguments
}
}
let obj = {
name: '二二',
age: 18
}
let o = {
name: '三三'
}
fn() // '林一一'
fn.call(obj) // '二二'
fn.call(o) // '三三'
p.call(obj, 12, 23, 45, 67) // {age: 18, arg: Arguments(4)}
fn.apply(obj) // "二二"
p.apply(obj, [1, 2, 3, 4, 5]) // {age: 18, arg: Arguments(5)}
fn.bind(obj)() // "二二"
p.bind(obj, 12, 23, 34)() // {age: 18, arg: Arguments(3)}
复制代码
笔试题 this 指向问题
var name = '林一一'
var obj = {
name: '林二二',
show: function (){
console.log(this.name)
},
wait: function () {
var fn = this.show
fn()
}
}
obj.wait() // 林一一
复制代码
obj.wait() 中,执行函数 wait() 前面有 '.' 点操作符吧,那么这里的 this 就指向obj 这个对象,所以this.show ==> obj.show。再看执行函数 fn() 前面没有 '.' 点操作符吧,那么这里的 this 就指向 window,输出就是 林一一。
阿里 this 指向和原型面试题(新增)
new 创建函数的过程中发生了什么可以看这篇 面试 | 你不得不懂得 JS 原型和原型链
function Foo() {
Foo.a = function() {
console.log(1)
}
this.a = function() {
console.log(2)
}
}
Foo.prototype.a = function() {
console.log(3)
}
Foo.a = function() {
console.log(4)
}
Foo.a();
let obj = new Foo();
obj.a();
Foo.a();
/*
* 4
* 2
* 1
*/
复制代码
Foo.a(); 中直接调用函数的私有方法a 输出结果就是 4。
new Foo(); 过程中函数的私有 a 被重新赋值,
同时原型prototype 上的属性a也被重新赋值。
所以obj.a(); 输出结果就是 2,Foo.a();输出结果就是 1。
function Foo() {
Foo.a = function() {
console.log(1)
}
this.a = function() {
console.log(2)
}
}
Foo.prototype.a = function() {
console.log(3)
}
Foo.a = function() {
console.log(4)
}
Foo.a();
let obj = new Foo();
obj.a();
Foo.a();
/*
* 4
* 2
* 1
*/
Foo.a();
中直接调用函数的私有方法a
输出结果就是 4。new Foo();
过程中函数的私有 a
被重新赋值,同时原型prototype
上的属性a
也被重新赋值。所以obj.a();
输出结果就是 2,Foo.a();
输出结果就是 1。