一、 配一个eslint 官网学习

目前快速配一个的指令示范

npm install eslint -D
./node_modules/.bin/eslint --init

  • image.png

③ 最新的eslint初始化时此步已被合入上面,无需再执行。采用airbnb-base标准npx install-peerdeps --dev eslint-config-airbnb-base

  • image.png

④ 增加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渲染函数中的实现

image.png

五、去掉 input标签 type=file 的 “未选择任何文件”标志

<label for="upload-file">通过label标签点击这里上传文件,修改一下样式</label>
<input type="file" id="upload-file" title=" " style="display: none">

六、.native将原生事件绑定到组件

image.png

七、 vm.$listeners 的诉求

.native进阶,解决写高阶组件时加工过的属性过多,希望一次性向下传递解决
  • vm.$listeners
    在文档中解释:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件——在创建更高层次的组件时非常有用。
    假设有父组件Parent和子组件Child,那么你在Parent父组件中使用Child时,传入的所有v-on事件都可以在$listeners对象中找到,子组件Child中只需要有v-on="$listeners"
    image.png
  • 但是我们有时候还需要对特殊组件内的一些标签实现功能。
    image.png

八、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
  • image.png
  • image.png

十二、github gh-pages分支展示自己的项目

  • 先用npm安装 gh-pages: npm install gh-pages --save-dev
  • image.png
  • 自动打包并上传分支gh-pages: npm run deploy
  • image.png
注:多个html文件的项目,如官网,用下面方法
1 git symbolic-ref HEAD refs/heads/gh-pages
2 git add -A
3 git commit -m "描叙"
4 git push origin gh-pages

Macrohoo
28 声望2 粉丝

half is wisdom!🤔