说起来已经使用了大概一年半的angular,大概已经渐渐习惯了这个前端框架吧,但是具体的原理和底层的代码都没有仔细的看过。

前阵子以前做的一个项目有了新的需求,而且比较麻烦,讲讲做的过程中遇到的一些问题

首先是勾选状态的保持问题
image.png
在模板中思考了很久,用了各种方法,修改传入值,页码改变以后勾选状态就是会有奇怪的问题,比如在代码里明明把选中的给去掉了,但是页面上还是勾选状态,最后折腾来折腾去,发觉操作的对象不对:页面上的是模板里面的list,就是listpagemodel.list,而我之前一直在操作传入的list,后面操作对象弄对了,只需要操作listpagemodel.list的状态就可以控制页面上的数据。

所以上面给我的经验就是一定要明白页面上展示的数据具体是由哪个对象控制的,对象找对了,控制起来就简单,对象没找对,忙活再久也是白干。

第二个就是自己创建的组件问题

image.png

sl-upload是我自己封装的一个组件

image.png

这是组件内部的代码,很简单,就是包含了nz的一个上传组件,还有一些数据的处理

image.png

比如上传之前的判断大小和类型,还有上传成功的提示之类的。

之所以自己写这部分,是因为nzzorro的组件自己虽然有提供大小控制和上传文件的类型控制,但是限制了以后选中保持以后不会有任何提示。所以就自己封装一下,加上了提示。

其中遇到的一个不解的问题就是双向绑定的问题

一开始我单纯的以为只要给[fileList]加上小括号就行
[(fileList)]这样就变成了双向绑定,实际上并没有,小括号加上去以后毫无作用,组件内部只是接收到了这个fileList

image.png

后续的操作,父组件(严格来讲其实包了三层,是祖孙组件了)里面是拿不到子组件对fileList的修改的,因为只是单纯的传入了这个值。现在要拿子组件对fileList修改后的值,就需要加上输出,也就是@Output
image.png

下图的那一行代码

this.fileListChange.emit(this.fileList)

image.png
就是输出用的,只有改变值的时候加上了这一行代码,才算是完成了输出
才让父组件中[(fileList)]中的小括号有了意义,不然加不加小括号都没影响。

这样才可以在父组件中拿到经过子组件修改后的fileList的值了。

    • -

2021-1-12日更新

这次是对以前的一个老项目的升级,需求是在登录以后,调用一个接口。
调用接口的组件是父组件,登录组件是子组件

这次使用的是通过服务的方式,注意一点,通过服务只能在组件家族内部实现双向通讯,组件树之外的组件无法访问该服务

首先就是创建服务了

import { Injectable } from '@angular/core';
import { Subject, Observable, BehaviorSubject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class TestPassDataService {

testSubject$ = new Subject<any>()

public testOb: Observable<any> //创建一个可订阅对象

constructor(){
    this.testOb = this.testSubject$.asObservable();
}

testEvent($event){
    this.testSubject$.next(item)
}


}

然后登录组件中的登录完成以后触发的事件中的代码

//在构造函数里面引入上面的服务文件
constructor(
    private testPassDataService:TestPassDataService
){}

loginSuccess(){//登录成功事件
    this.testPassDataService.testEvent('成功了');
}

调用登录成功的父组件中的代码

//设立一个全局变量接收
testSubscribe:Subscription;

constructor(//构造函数里引入服务文件
    private testPassDataService:TestPassDataService
){}

ngOnInit(){
    this.testSubscribe = this.testPassDataService.testOb.subscribe($event=>{
       console.log($event)//在登录触发事件以后就会打印出'成功了'
    })
}

ngOnDestroy(){
    this.testSubscribe.unsubscribe()//在组件销毁以后解除绑定.
}
    • -

2021-1-14
package.json中使用自己的ip地址

"scripts": {
    "ng": "ng",
    "start": "ng serve --host ip地址 --port 4203 --proxy-config proxy.config.json",
    "build": "ng build  --prod --aot --progress",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e",
    "package": "npm run build && cd .. && gradle clean build"
  },
    • -

2021-1-18

更新globalState

正如上面说的组件内的数据传递,angular项目可以看做都是从app.component组件延伸出来的,所以在根组件内创建一个用于数据传递的服务就可以在全组件内通用了,不过这样很耗性能,所以不必要的情况下一般不使用。

创建服务

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';

@Injectable({providedIn:'root'})
export class GlobalState {
  private _data = new Subject<Object>();
  private _dataStream$ = this._data.asObservable();

  private _subscriptions: Map<string, Array<Function>> = new Map<string, Array<Function>>();

  constructor() {
    this._dataStream$.subscribe((data) => this._onEvent(data));
  }

  /**
   *
   * @param event
   * @param value
   */
  notifyDataChanged(event, value) {
    let current = this._data[event];
    if (current !== value) {
      this._data[event] = value;

      this._data.next({
        event: event,
        data: this._data[event]
      });
    }
  }

  subscribe(event: string, callback: Function) {
    let subscribers = this._subscriptions.get(event) || [];
    subscribers.push(callback);

    this._subscriptions.set(event, subscribers);
  }

  _onEvent(data: any) {
    let subscribers = this._subscriptions.get(data['event']) || [];

    subscribers.forEach((callback) => {
      callback.call(null, data['data']);
    });
  }
}

然后在根组件里引入

import { GlobalState } from './global.state';

image.png

在需要变更检测的组件里使用

import { GlobalState } from 'webapp/app/global.state'; //引入

constructor(
    private gl$:GlobalState,
  ) { }

  this.gl$.notifyDataChanged('return-map',{showMap:true,id:this.stationId})

在触发变更时间的组件里使用

import { GlobalState } from 'webapp/app/global.state'; //引入

constructor(
    private gl$:GlobalState,
  ) { }

 this.gl$.subscribe('return-map',(item)=>{
    console.log('item',item)
 })
    • -

2021-2-24 17:11更新

这次是遇到一个小问题,在使用递归函数的时候,遇到了使用return,break都无法结束多余循环的情况,代码会直接一路走完全程。

这种情况原因其实很简单,就是在递归的时候,例如

testFunc(a,b){
    if(a>b){
       return a-1
    }else{
        this.testFunc(a,b)
    }
}

注意,else里面直接引用了方法本身,这样是无法结束循环的,要在调用自己之前加上return
return this.testFunc(a,b)
这样,就不会遇到循环一路走到底,无法中断的问题。

    • -
    • -

2021-3-3 11:53 更新

上面更新的递归结束循环是错的 (:3」∠❀)

举个例子

findFather(baseData: Array<any>, item) {//找到父元素
    let parentItem;
    for (let ele of baseData) {
      if (ele.id === item.parentId) {
        parentItem = ele;
        break;
      }
      if (ele.children && ele.children.length) {
        parentItem = this.findFather(ele.children, item);
      }
    }
    return parentItem;
  }

这样的递归循环才能够正常结束。

回到正题,这次以前的一个项目提了一个新的需求,就是对一个四级的树进行操作

image.png

如上图所示,这是一个四级深度的树

用代码表示就是

data:{
    level:1,
    children:[
        {
            level:2
            children:[
                {
                    level:3,
                    children:[
                        {
                            level:4,
                            children:[]
                        }
                    ]
                }
            ]
        }
    ]
}

现在的需求就是在同一级之间可以任意改变其位置,上移下移都行(但是处于最后一位和第一位的只能上移和下移)。

咋一看需求很简单,但是由于整个树是可以折叠的,所以页面上展示的数据结构其实是这样的

    showData:[
        {
         level:1,
         children:[...]
        },
        {
         level:2,
         children:[...]
        },
        {
         level:3,
         children:[...]
        },
        {
         level:4,
         children:[]
        }
    ]

level1children[...]中包含整个树的所有数据,后续点击图中的展开与折叠都是对level1这个原始数据进行splice操作,在正确的位置插入和裁减数据,从而使页面上的数据看起来像是折叠和展开了一样。

所以难点就在于要认识到真正的操作对象是level1与leve11的children,操作level1之后的数据也就是showData只会让页面上展示的数据变动,但是一旦折叠再展开,数据又会变成操作之前的样子。

接下来就是思考如何完成这个工作了

首先点击右侧的上移下移按钮的时候,可以拿到点击的这个数据。

然后用点击得到的数据,可以在level1中遍历,找到其父元素,然后找到这个父元素有多少个子元素以及子元素的子元素等,从而找到点击的数据在level1中的index,和其要上移下移的数据的具体信息 ,得到这些以后,就得到了完成上下移动的前置数据了。

首先要明确的是,点击一次上移下移要修改两个地方,第一个地方是level1中的children[...],第二个地方是showData中的数据,因为level1中的数据是真正的数据,而showData中的数据是页面上展示的数据,如果只操作level1,那些数据只有被折叠和展开以后才会变更为操作后的样子,而只操作showData页面上的数据会立刻变动,但是在折叠展开操作以后,数据又会变成未操作之前的样子。

从点击操作以后开始

clickItem//点击的那个数据
showData:Array<any>//包含所有数据的数组 也就是上面的showData
//首先需要找到点击项目的父级元素

findFather(showData:Array<any>,clickItem){//找到点击元素的父元素
    let parentItem;//用来装找到的父元素
    for(let ele of showData){
        if(ele.id === clickItem.parentId ){
            parentItem = ele;
            break;
        }
        if(ele.children && ele.children.length){
            parentItem = this.findFather(ele.children,clickItem)
        }
    }
    return parentItem
}

接下来找到点击的那个数据有多少个子元素(包括该数据下所有的层级的子元素)

findLength(showData,num){
    for(let ele of showData){
        if(ele.children && ele.children.length){
            if(ele.expand&&ele.isParent){//有后代,且为展开状态的时候
                num = num + ele.children.length
            }
            num = this.findLength(ele.children,num)
        }
    }
    return num
}

得到该要的数据以后就开始进行splice操作了

let index:number;//这是点击元素上移下移的时候传进来元素在showData中的位置也就是index
let father = this.findFather(this.showData,clickItme);//得到clickItem的父级元素
for(let x = 0;x < father.children.length;x++){
    let item = fahter.children[x];
    if(item.id == clickItem.id){
        father.children.splice(x,1)//这是相当于是level1中的children,直接把找到的这个元素剪掉
        let moveRange = this.findLength([clickItem],0)//点击的元素有多少子元素
        let spliceItem = this.showData.splice(index,moveRange+1)//把页面展示中的数据从点击的那项开始把它自己包括所有的下级元素都剪掉
        if(moveIndex == 1){//为1的时候是上移
            let exchangeRange = this.findLength([father.children[x]],0)//与点击元素交换位置的元素有多少展开的下级元素
            father.children.splice(x+1,0,clickItem)//由于未知原因,father.children拿到的时候排序是倒序的,也就是页面上显示的第一个元素其实是最后一个元素,所以是x+1,如果是正序的话就会是x-1了。
            this.showData.splice(index - (exchangeRange+1),0,...spliceItem)//在展示数据中,在点击的位置(因为已经把点击元素和其子元素剪掉了,所以是index-1)减去交换元素及其所有子元素的长度以后,插入点击元素及其所有子元素
            //注意,其中 ... 为es6的语法为扩展运算符 spliceItem为一个array对象,插入以后会报错,所以要用...将其遍历取出数组中的元素。
        }else{其他情况是下移
            let exchangeRange = this.findLength([father.children[x-1]],0)//下移时候点击元素位置下移,所以要找到与其交换位置的元素在父元素中的位置就是x-1
            father.children.splice(x - 1, 0, item)
            this.showData.splice(index + (exchangeRange + 1), 0, ...spliceItem)//同上移
        }
        
    }
}
对象中的扩展运算符(...)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中

这样就能够正常的上下移了,其他的比如判断元素是能上移下移(处在边界处)如何判断就不说了。


2021-3-11 15:57

简单的一个防抖的操作

debounce
clickTest(item){
    clearInterval(this.debounce)
    this.debounce = setTimeout(()=>{
        this.service.testfunction(item)
    },500)
}

这个操作就是在重复触发clickTest这个事件的时候,首先清除定时器,然后再设立一个500毫秒延迟的定时器,这样只要点击间隔小于500毫秒的时候,不论点击多少次,都只会实现一次这个方法。不过这种操作有一个问题就是点击的操作有500毫秒的延迟。


2021-3-23 18:15更新

上次的防抖操作以后,又想到了另外一个合并请求操作
简而言之就是在短时间内触发比较大的请求的时候,把请求合并,这样能够极大的缓解服务器的压力,坏处就是每一次操作都会有延迟,就和上面的防抖操作类似,话不多说,上代码

postlist = []
timeout
delaypost(item){//这个为短时间内重复触发的事件
    let delaytime = 1000//延迟时间为1000毫秒
    this.postlist.push(item)//将触发的事件塞到数组中
    let that = this//保存this作用域,后面setTimeout里要用上
    if(this.timeout) return //如果timeout存在,直接中断操作,防止重复触发计时器 很重要
    this.timeout = setTimeout(function(){
        console.log(second+'毫秒后请求了哪些东西?'+" "+that.postlist.join(','))
        that.postlist = []//清空数组
        clearTimeout(that.timeout)//清除计时器
        that.timeout = null//将timeout设为不存在
    },delaytime)
}

image.png


2021-4-14更新

angular动态添加class的时候有多种方式
比如[ngClass]="{'active':item.active}"[ngClass]="[item.class]" 但是如果想要混合使用是不能的,例如[ngClass]="[item.class,{'active':item.active}]"这种是会报错的
但是如果遇到这种情况又想使用该怎么办?
解决办法如下
[class]="item.class" [ngClass]="{'active':item.active}"


2021-4-27 20:31
再次遇到了组件双向绑定的问题,如果想要自定义双向绑定,得完成如下操作,在子组件中绑定输入输出

Input()abc:any;
Output()abcChange:EventEmitter<any> = new EventEmitter();

这样在父组件中才可以用到[()],不然不会生效。
同时一旦使用了双向绑定,那么在ngOnChanges()生命中的操作很容易引发 Expression has changed after it was checked 错误,这时候其实只要绑定object对象就可以避免绝大多数这种错误,不过有得有失.


image.png

保留代码但是保持换行的操作

image.png


munergs
30 声望8 粉丝

现在即是最好。