一、 配一个eslint 官网学习
目前快速配一个的指令示范
①
npm install eslint -D
②./node_modules/.bin/eslint --init
③ 最新的eslint初始化时此步已被合入上面,无需再执行。采用airbnb-base标准
npx install-peerdeps --dev eslint-config-airbnb-base
④ 增加package.json中script指令
"lint": "eslint --fix --ext .js,.vue src"
⑤ 修改.eslint.js中的部分规则和airbnb-base依赖,及解决一些airbnb中不合理的报错规则如:airbnb-base报import/no-unresolved
module.exports = {
env: {
es2021: true,
node: true,
},
extends: ['eslint:recommended', 'plugin:vue/essential', 'airbnb-base'],
parserOptions: {
ecmaVersion: 12,
sourceType: 'module',
},
plugins: ['vue'],
rules: {
'max-len': ['error', { code: 150 }],
'import/no-unresolved': 'off', // 取消自动解析路径,以此开启alias的别名路径设置
'arrow-parens': ['error', 'as-needed'], // 箭头函数的参数可以不使用圆括号
'comma-dangle': ['error', 'never'], // 不允许末尾逗号
'no-underscore-dangle': 'off', //允许标识符中有下划线,从而支持vue中插件的使用
'linebreak-style': 'off', // 取消换行符\n或\r\n的验证
'no-param-reassign': 'off', // 允许对函数参数进行再赋值
'consistent-return': 'off', // 关闭函数中return的检测
},
settings: {
'import/resolver': {
node: {
extensions: ['.js', '.jsx', '.vue'],
},
},
},
};
二、 Node循环加载
我们在写工程化项目的时候,还是应该避免循环加载,但是当项目复杂化的时候,我们可能不可避免。
阮一峰ES6循环加载
“循环加载”(circular dependency)
指的是,a脚本的执行依赖b脚本,而b脚本的执行又依赖a脚本。通常,“循环加载”表示存在强耦合,如果处理不好,还可能导致递归加载,使得程序无法执行,因此应该避免出现。但是实际上,这是很难避免的,尤其是依赖关系复杂的大项目,很容易出现a依赖b,b依赖c,c又依赖a这样的情况。ESlint代码规范会帮你非常简便的查出a、b两个文件的相互引用,但是ab、bc、ca这种隔代相互引用的情况是无法帮你查询出来的。
这意味着,模块加载机制必须考虑“循环加载”的情况。对于 JavaScript 语言来说,目前最常见的两种模块格式 CommonJS 和 ES6,处理“循环加载”的方法是不一样的,返回的结果也不一样。
一、 CommonJS 模块的循环加载
- CommonJS 输入的是被输出值的拷贝,不是引用。
- CommonJS 的一个模块,就是一个脚本文件。require命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。以后需要用到这个模块的时候,就会到exports属性上面取值。即使再次执行require命令,也不会再次执行该模块,而是到缓存之中取值。也就是说,CommonJS 模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。
- CommonJS 模块的重要特性是加载时执行,即脚本代码在require的时候,就会全部执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。
- 由于 CommonJS 模块遇到循环加载时,返回的是当前已经执行的部分的值,而不是代码全部执行后的值,两者可能会有差异。所以,输入变量的时候,必须非常小心。
var a = require('a'); // 安全的写法
var foo = require('a').foo; // 危险的写法
exports.good = function (arg) {
return a.foo('good', arg); // 使用的是 a.foo 的最新值
};
exports.bad = function (arg) {
return foo('bad', arg); // 使用的是一个部分加载时的值
};
- 上面代码中,如果发生循环加载,require('a').foo的值很可能后面会被改写,改用require('a')会更保险一点。
二、 ES6 模块的循环加载
- ES6 处理“循环加载”与 CommonJS 有本质的不同。ES6 模块是动态引用,如果使用import从一个模块加载变量(即import foo from 'foo'),那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值
- ES6循环引用的错误时刻
// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar);
export let foo = 'foo';
// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo);
export let bar = 'bar';
- 上面代码中,a.mjs加载b.mjs,b.mjs又加载a.mjs,构成循环加载。执行a.mjs以后会报错,foo变量未定义,这是为什么?让我们一行行来看,ES6 循环加载是怎么处理的。首先,执行a.mjs以后,引擎发现它加载了b.mjs,因此会优先执行b.mjs,然后再执行a.mjs。接着,执行b.mjs的时候,已知它从a.mjs输入了foo接口,这时不会去执行a.mjs,而是认为这个接口已经存在了,继续往下执行。执行到第三行console.log(foo)的时候,才发现这个接口根本没定义,因此报错。
- 解决这个问题的方法,就是让b.mjs运行的时候,foo已经有定义了。这可以通过将foo写成函数来解决,因为函数具有提升作用,在执行import {bar} from './b'时,函数foo就已经有定义了,所以b.mjs加载的时候不会报错。这也意味着,如果把函数foo改写成函数表达式,也会报错。因为函数表达式并不能提升。
总结:尽量在工程化项目中不要出现循环引用,即使是CommonJS或是ES6模块都会出现各自不同的问题。
- 因为CommonJS 模块遇到循环加载时,返回的是当前已经执行的部分的值,而不是代码全部执行后的值,两者可能会有差异。
- 而ES6 模块遇到循环加载时,动态引用导致执行到某处代码时,才发现引用文件中的接口根本没定义。
三、 Promise源码
//定义三个常量来存放promise的状态值
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
class MyPromise {
constructor(handle) {
//0.初始化默认状态
this.status = PENDING;
// 定义变量保存传给then方法的参数
this.value = undefined;
this.reason = undefined;
// 定义变量保存监听的函数,同一个Promise对象可以添加多个then监听,状态改变时所有的监听按照添加顺序执行,所以将多个函数放在一个数组里
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];
//1.判断是否传入了一个函数,如果没有则抛出异常
if (!this._isFunction(handle)) {
throw new Error('请传入一个函数');
}
//2.给传入的函数传递两个形参(形参为函数)
//**用bind修改_resolve函数指向(只能用bind,因为使用call和apply会立即执行,而bind会返回一个函数)
handle(this._resolve.bind(this), this._reject.bind(this));
//分析核心步骤只讲解fulfilled这种状态
//new Promise((resolve, reject) => {})中的resolve,reject形参,等于在执行new时整个(resolve, reject) => {} 函数被直接调用了。
//且这个函数内部对resolve和reject形参进行了一个bind上下文绑定改变,这两个形参不能用apply或者call。
//一般当我们写一个function return 出一个new实例化的promise时,一般我们内部还会发起的一个axios请求。
//这个axios请求中会使用then()方法,通过then订阅异步成功后要执行的任务,去判断当前状态(axios这个promise实例的this.state)是否为padding状态,直接将监听回调函数推入onResolvedCallbacks[],等状态改变时候再执行监听回调。
//然后就是最关键的一步,我们需要去看axios的源码,因为在axios源码中settle函数中会resolve(response)。
//axios这个promise被手动执行了它的resolve形参,resolve调用后执行已订阅的任务onResolvedCallbacks[]中被推入的fn,就是我们请求调用中then执行时订阅的任务fn。axios.get('/user?ID=12345').then(fn)。所以源码的逻辑和我们正常的理解逻辑是反的,我们可以看出是我们业务逻辑中请求调用axios的then()订阅了任务,反而上一步axios源码中的resolve(response)执行了订阅。这就是回调+异步的魅力。
//而且细看then()实例方法,我们发现then返回的也是一个promise,因为promise设计就是then可以一直链式调用下去。
//当然在我们自己封装的一个返回promise的函数被then时,和上面axios请求then时,是一样的,我们也会在封的这个函数中resolve(我们想要的res出来)。
}
catch(onRejected) {
return this.then(undefined, onRejected);
}
_resolve(value) {
//console.log(123);
//为了防止重复修改
if (this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
//状态发生改变时,执行保存的函数(比如定时器被触发时,函数执行),因为会将所有的then保存在一个数组里所以用foreach遍历
this.onResolvedCallbacks.forEach((fn) => fn(this.value));
}
}
_reject(reason) {
if (this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
//状态发生改变时,执行保存的函数(比如定时器被触发时,函数执行)
this.onRejectedCallbacks.forEach((fn) => fn(this.value));
}
}
then(onResolved, onRejected) {
//then方法会返回一个新的promise函数
return new MyPromise((nextResolve, nextReject) => {
//1.判断有没有传入成功回调
if (this._isFunction(onResolved)) {
//2.判断当前的状态是否是成功状态
if (this.status === FULFILLED) {
//后一个then可以捕获前一个then方法的异常
try {
//拿到上一个promise成功回调执行的结果并执行
let result = onResolved(this.value); //就是我们平时在写的then()内部的那个回调函数,去执行它一下;但是形参里的那个值,就是上一个promise成功回调的值如res,response。
//判断执行的结果是否是一个promise对象
if (result instanceof MyPromise) {
//如果上一个传递的是一个promise对象,那么传给下一个的是成功还是失败由传递的promise状态传递
result.then(nextResolve, nextReject);
} else {
//将上一个promise成功回调执行的结果传递给下一个promise成功的回调
nextResolve(result);
}
} catch (e) {
nextReject(e);
}
}
}
//2.判断当前的状态是否是失败状态
//为什么不用判断是否传入失败回调,因为当promise为失败状态时,then方法没有写第二个参数时仍需保证返回的promise对象为上一个promise的失败状态
//后一个then可以捕获前一个then方法的异常
try {
if (this.status === REJECTED) {
let result = onRejected(this.reason);
//console.log("result:",result);
if (result instanceof MyPromise) {
result.then(nextResolve, nextReject);
} else if (result !== undefined) {
nextResolve(result);
} else {
nextReject();
}
}
} catch (e) {
nextReject(e);
}
//2.判断当前状态是否是默认状态
//如果添加监听时状态还未发生改变,那么状态改变时候再执行监听回调,(比如将成功失败的函数放在一个定时器里)
if (this.status === PENDING) {
if (this._isFunction(onResolved)) {
//将成功的函数先保存在定义的变量里
this.onResolvedCallbacks.push(() => {
try {
let result = onResolved(this.value);
if (result instanceof MyPromise) {
result.then(nextResolve, nextReject);
} else {
nextResolve(result);
}
} catch (e) {
nextReject(e);
}
});
}
this.onRejectedCallbacks.push(() => {
try {
let result = onRejected(this.reason);
if (result instanceof MyPromise) {
result.then(nextResolve, nextReject);
} else if (result !== undefined) {
nextResolve(result);
} else {
nextReject();
}
} catch (e) {
nextReject(e);
}
});
}
});
}
_isFunction(fn) {
return typeof fn === 'function';
}
static all(list) {
return new MyPromise(function (resolve, reject) {
let arr = [];
let count = 0;
for (let i = 0; i < list.length; i++) {
let p = list[i];
p.then(function (value) {
arr.push(value);
count++;
//当所有的promise对象都成功返回时,判断是否是最后一个如果是则返回保存的数组
if (list.length === count) {
resolve(arr);
}
}).catch(function (e) {
reject(e);
});
}
});
}
static race(list) {
return new Mypromise((resolve, reject) => {
for (let p of list) {
p.then(function (value) {
resolve(value);
}).catch(function (e) {
reject(e);
});
}
});
}
}
四、关于v-model语法糖在render渲染函数中的实现
五、去掉 input标签 type=file 的 “未选择任何文件”标志
<label for="upload-file">通过label标签点击这里上传文件,修改一下样式</label>
<input type="file" id="upload-file" title=" " style="display: none">
六、.native将原生事件绑定到组件
七、 vm.$listeners 的诉求
.native进阶,解决写高阶组件时加工过的属性过多,希望一次性向下传递解决
- vm.$listeners
在文档中解释:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件——在创建更高层次的组件时非常有用。
假设有父组件Parent和子组件Child,那么你在Parent父组件中使用Child时,传入的所有v-on事件都可以在$listeners对象中找到,子组件Child中只需要有v-on="$listeners"
。
- 但是我们有时候还需要对特殊组件内的一些标签实现功能。
八、vue中使用v-on绑定事件中,获取$event.currentTarget
- 我们其实不需要
@click="clickEvent('hello',$event)"
这么写,直接@click="clickEvent"
然后再方法中clickEvent(e)就可以了。- e.currentTarget是一个瞬时的值,当打印event后,等到展开log信息时,冒泡事件已经结束,所以当前currentTarget是null。
- 我们可以在Vue中像小程序那样的推荐方式传值
<div @click="checkGroupAll" data-index="-1">
methods: {
checkGroupAll(e){
console.log(parseInt(e.currentTarget.dataset.index));
}
}
九、关于props数据单向流的解决方法
你可以试下不用v-model
语法糖,而用原生方法来处理
<input :value="user.name" @input="$emit('update-name', $event.target.value)">
你若不想用难看的$event.target.value
也可以多写几行代码借助计算属性
<template>
<input v-model="userName">
</template>
<script>
export default {
props: ['user'],
computed: {
userName:{
get(){
return this.user.name
},
set(name){
this.$emit('update-name', name)
}
}
},
当然父组件必须监听这个事件来触发更新
<ChildInput
:user="user"
@update-name="name => user.name = name"
>
</ChildInput>
十、让按钮变灰且不可选中的样式
button {
color: #C0C4CC;
cursor: not-allowed
}
十一、hostOnly=true 引起的bug问题
- BUG情况说明:当使用<el-upload>组件的时候使用
:with-credentials="true"
并不能带上cookie- 原因:cookie中hostOnly导致的
Sec-Fetch-Site: cross-site
- 分析:egg框架要通过session往前端设置cookie,这个cookie会带上
host-only:true
的属性,但是由于第一次的host判断是由第一次跨域校验的时候axios带过来的请求判断的,axios中会把所有127.0.0.1或者localhost都转化成localhost赋给host,这时在我客户端中的cookie的host就已经被定格了。- 当<el-uplod>中action写成127.0.0.1:端口号这种方式,就会出现
Sec-Fetch-Site: cross-site
的情况,cookie无法被带上,因为hostOnly
十二、github gh-pages分支展示自己的项目
- 先用npm安装 gh-pages:
npm install gh-pages --save-dev
- 自动打包并上传分支gh-pages:
npm run deploy
注:多个html文件的项目,如官网,用下面方法
1 git symbolic-ref HEAD refs/heads/gh-pages
2 git add -A
3 git commit -m "描叙"
4 git push origin gh-pages
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。