jiayisheji / blog

没事写写文章,喜欢的话请点star,想订阅点watch,千万别fork!
https://jiayisheji.github.io/blog/
505 stars 49 forks source link

Angular常见错误及解决方案 #24

Open jiayisheji opened 5 years ago

jiayisheji commented 5 years ago

Angular开发中,有时候有些错误让人一脸懵逼,不知道该如何下手,接下来我就介绍一下我在我使用angular中遇到的问题和解决方案(欢迎你留下你的问题和解决方案,让我们angular开发更轻松容易):

关于依赖注入问题

经常看有人在群里问下面这张图是什么问题,

image

上面问题解答是AppComponent依赖NameService服务,NameService却没有申明。

解决方案去申明注册:(注意:服务注册位置决定服务作用域)

全局申明:(一般用于全局数据共享使用,如果是注册到全局,推荐第一种方式,因为它对打包会有优化)

  1. 直接在服务里面申明作用域

    @Injectable({
    providedIn: 'root',
    })
    export class NameService {
    }
  2. 根模块注册到providers里

@NgModule({
  declarations: [AppComponent],
  imports: [
  ],
  providers: [NameService],
  bootstrap: [AppComponent]
})
export class AppModule { }

模块申明:(一般用于该模块下数据共享使用,你也可以导出给其他模块使用)

@NgModule({
  imports: [
  ],
  providers: [NameService],
  exports: [NameService]
})
export class coreModule { }

组件申明1:(一般用于该组件下数据共享使用,它会携带一个OnDestroy生命周期)

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  providers: [NameService]
})
export class AppComponent {
  title = 'data-analysis';
  constructor(private nameService: NameService) {
  }
}

组件申明2:(一般用于该组件下数据共享使用,它会携带一个OnDestroy生命周期)

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  viewProviders: [NameService]
})
export class AppComponent {
  title = 'data-analysis';
  constructor(private nameService: NameService) {
  }
}

注意: 在父组件用viewProviders注册的provider,对contentChildren是不可见的。而使用providers注册的provider,对viewChildrencontentChildren都可见! 补充说明:组件会逐级向上寻找provider,直到找到为止,否则就会抛出错误。

什么是contentChildren,就是<ng-content></ng-content>的内容。

为什么依赖注入需要写private

基本很多栗子都是这样的来写依赖注入:

export class NameComponent {
  constructor(private nameService: NameService) { }
}

有人就奇怪为什么我一定要写一个private,可以不写么,可以,但是会报错,如果不写ts只是当 他是类参数,其实constructor(private nameService: NameService) { }是一个语法糖。

如果不写private

export class NameComponent {
  constructor(nameService: NameService) { }
}

编辑器会提示:类型“AppComponent”上不存在属性“nameService”

试想一下ES6的class怎么写的:

class NameComponent {
  constructor(nameService) {
     this.nameService = nameService;
 }
}

constructor里的参数,ts看来它是构造参数,不是一个类的属性,如果要实现属性功能,你需要这样来写:

private nameService: NameService;
constructor(nameService: NameService) {
    this.nameService = nameService;
}

private 关键字表示这个是私有,你还可以写public公开,protected对继承的子类公开。

最后总结:你在ts写方法和属性时候公开可以不用写public关键字,但是在constructor里写依赖注入时如果需要写成公开时候一定要写public关键词。

为什么创建angular组件模块都需要带上CommonModule

FP{ $7J7(RZEKF3KSA`RG{J

angular使用中一定要注意,组件,指令,管道,服务都是封装的在模块里,如果想要给其他模块里面组件使用,一定要导出。如果当前模块想要使用别人模块一定要导入。

CommonModule里面携带angular自带组件,指令,管道等,如果你带上它不能使用*ngIf*ngFor等。

最后总结,记住两点,你可以轻松玩转angular模块:

  1. 想要暴露出去给其他模块使用就要exports
  2. 想要使用别人模块的功能先要imports

'app-xxx' is not a known element

image

这是一个什么沙雕错误,翻译成中文app-xxx不是一个已知的元素,再说简单点就不是一个标准的HTML标签,算一个自定义标签,angular不认识它。

问题找到根源了,出现这个错误有个原因:

技巧:如果你用了vs code 推荐安装 Angular Language Service

  1. 你在当前模块写了组件没有去申明。

image

这里是vs code 提示错误,翻译里面3句话比较重要:

组件是Angular应用最基本的UI构建块。一个Angular应用包含一个Angular组件树。
Angular组件是指令的子集,总是与模板相关联。与其他指令不同,模板中的每个元素只能实例化一个组件。
一个组件必须属于一个NgModule,以便它对另一个组件或应用程序可用。要使它成为NgModule的成员,请在`@NgModule`元数据的`declarations`中申明它。

一个组件只能在一个NgModule申明,不能重复申明,如果A和B模块都申明一个c组件,那么就会报错,提醒你写一个D模块去申明c组件,A和B模块去引用D模块。这就是传说的共享模块思路来源。

注意:组件、指令和管道都需要在declarations中申明它。

  1. 你使用其他模块的组件、指令和管道
<div *ngIf="true"></div>

这是内置的angular指令,如果你当前模块没有导入CommonModule,就会报错Can't bind to 'ngIf' since it isn't a known property of 'div'.

解决方案只需要导入CommonModule,使用其他模块组件,指令,管道也是一样的道理。

Angular Language Service提示:

image

image

这是angular开发神器,还有更多开发功能,期待你去发现吧。。。

Can't bind to 'appCode' since it isn't a known property of 'div'

image

这又是一个什么沙雕错误,翻译成中文appCode不是一个div上面一个属性,再说简单点就不是一个标准的HTML标签标准属性,算一个自定义属性,angular不认识它。

需要先去了解一下HTML attribute 与 DOM property 的对比

重点:模板绑定是通过 property 和事件来工作的,而不是 attribute。

这里只能推测你有2个意图:

  1. 想绑定一个自定义属性:

那你需要这样去操作:使用attr.xxxx

<div [attr.appCode]="123"></div>

技巧:我们常用的html5的自定义data,需要这样来绑定[attr.data-xxx]="xxx"

  1. 你写了一个指令,给它绑定一些@Input属性。

这种情况也分2种,一直是你没有申明,或者导入。

这个参照'app-xxx' is not a known element解决。

意思是你没有在组件或指令使用@Input()装饰器申明它,或者你属性名写错了。

解决方案请正确书写和申明。

jiayisheji commented 5 years ago

Cannot assign to a reference or variable!

image

这是一个什么错误,翻译成为无法分配给引用或变量(小细节:有些错误,你不知道什么意思时候,你可以把英文翻译成中文,你英文很差情况下,谷歌翻译很不错)

引用或变量,我不知道是什么鬼,但是我知道angular有个东西叫模板引用变量。 为什么它叫:模板引用变量,顾名思义就是引用模板。既然是引用变量,那么他应用了谁?这些变量提供了从模块中直接访问元素的能力,在标识符前加上井号 (#) 就能声明一个模板引用变量。

优点:这个模板完全是完全自包含的。它没有绑定到组件,组件也没做任何事情。这里的自包含的意思是:它不用与Component进行交互。

举个栗子:

<my-select #select></select>

比如我写了一个my-select,默认情况下,是点击它就打开option列表面板,但是产品需求点击一个按钮把它。

<my-select #select></select>
// 新需求
<button (click)="select.open()">打开</button>

就是这么容易。如果你还不知道赶紧用它把,它还可以ts获取:

// 获取一个
@ViewChild('select', { read: SelectComponent }) select: SelectComponent;
// 获取一组
@ViewChildren('selects', { read: SelectComponent }) select: QueryList<SelectComponent>;

ViewChildViewChildren请去看文档,这里不深入套路,它们不是重点。

回到错误,这才是关键。

我们看到模板引用变量调用是a.b,那么问题来了,如果我在ts里面定一个了一个变量a,它的调用也是a,这个时候就有冲突了。angular也一脸懵逼,你到底要闹哪样。

那么很好理解的错误,无法分配给引用或变量,这里引用是指模板引用变量,变量指我们在ts里面定义class的属性。

解决方案很简单,别让他们重名就好了。

注意:指令的模板引用变量默认指向HTMLElement,如果想要指向这个指令怎么办?

指令需要写导出

@Directive({
  selector: '[appRefresh]',
  exportAs: 'refresh'
})
export class RefreshDirective implements OnDestroy {
     load() {}
}
<div appRefresh #refreshRef="refresh">...code</div>
<button (click)="refreshRef.load()">加载</button>

最后说一点:写指令的时候一点要记得写exportAs.

jiayisheji commented 5 years ago

Unexpected value 'undefined' imported by the module 'XxxxxModule'

image

翻译中文:模块“XxxxxModule”导入的意外值“undefined”。

我们先看下angular模块怎么定义的:

NgModule({
  imports: [
    SharedModule,
    RouterModule.forChild(routes)
  ],
  declarations: [XxxxComponent]
})
export class XxxxxModule { }

这是一个基本的业务模块,没有exports,没有providers

现在错误是提示根据字面意思是来自imports,那么我们根据imports来找问题。

如果你找不到问题的时候,可以通过排除法。因为我们已经锁定范围了,剩下就一个一个找问题。

我们先把SharedModule,注释掉,看是否能运行,如果ok,那么目标就锁定了,它里面有问题,我们需要去排查这个模块经历什么。

因为这个模块看命名知道是共享模块,大概就是导出当前模块需要的一些其他模块,组件什么的。

可以先将SharedModule里面申明,导入,导出,全部清空掉,看有没有报错,如果有报错,找其他原因。

我先说下我这个报错原因:

我开始是这么写的

import { SharedModule } from '../shared';

因为在shared文件夹里面写了index.ts导出shared.module;

按我正常理解是不需要在这样写:../shared/shared.module

我改成新的写法:

import { SharedModule } from '../shared/shared.module';

改成这样就不报错了,哈哈,那么就说明我这个index.ts导出的有问题。

export * from './shared.module';

我写的这样的,并没有错,不知道原因它不找不到(可能cli抽风)。

这个问题原因就是导入模块时候,没有拿到导出模块,那个导入模块就变成了undefined,这也解释了,模块“XxxxxModule”导入的意外值“undefined”这句话的意思。

jiayisheji commented 5 years ago

关于循环依赖问题

什么叫循环依赖,比如我们创建一个select,它里面有两个特有子组件optionoptgroup

optgroup是分组,包含option组件。

那么我们组件写法就是

<my-select>
   <my-option>项</my-option>
</my-select>

<my-select>
  <my-optgroup label="分组">
      <my-option>项</my-option>
  </my-optgroup>  
</my-select>

如果我在optgroup设置了disabled,那么它下面option就默认自动disabled

在angular写法就比较简单,只需要这样既可:

export class OptionComponent {
 constructor(
    @Optional() readonly group: OptgroupComponent
  ) {}
}

@Optional(): 当组件或服务声明某个依赖项时,该类的构造函数会以参数的形式接收那个依赖项。 通过给这个参数加上 @Optional() 装饰器,可以告诉 Angular,该依赖是可选的。注意:当使用 @Optional() 时,你的代码必须能正确处理 null 值。

这样没啥问题,没什么问题。

我现在有个需求想知道optgroup里面的option有没有,如果没有就加一个option-empty的class,给使用者去做些其他事情。

先看下我们的optgrouphtml:

<label class="optgroup-label">{{ label }}</label>
<ng-content select="my-option, ng-container"></ng-content>

按我们正常理解使用:

   /** 所有定义的选择选项。 */
  @ContentChildren(OptionComponent, { descendants: true }) options: QueryList<OptionComponent>;

这样就可以获取到optgroup下所有的option

  ngAfterContentInit() {
    this.options.changes.subscribe((option) => console.log('ngAfterContentInit', option));
  }

这样就好获取到option变化了。注意:要在AfterContentInit生命期钩子里面,不然就会报错。

image

翻译错误:无法为“SimOptgroupComponent”的属性“options”构造查询,因为没有定义查询选择器。

简单理解就是找不到OptionComponentangular为我们提供forwardRef来解决这个问题。这里有篇博客专门介绍它,传送门

改成下面写法就行啦,需要加上tslint报错问题。

   /** 所有定义的选择选项。 */
  // tslint:disable-next-line:no-use-before-declare
  @ContentChildren(forwardRef(() => OptionComponent), { descendants: true }) options: QueryList<OptionComponent>;

如果你的optgroupoption组件不在一个文件里,angular-cli编译也会出现警告WARNING in Circular dependency detected:说我们有循环依赖问题,解决方案放在一个文件里即可。

jiayisheji commented 5 years ago

打包时候出现错误

ERROR in : Cannot determine the module for class BasisConfigComponent in E:/gitlab/angular/projects/my-app/src/app/feature/link/basis-config/basis-config.component.ts! Add BasisConfigComponent to the NgModule to fix it.

这是一个什么错误,看着一脸懵逼,翻译错误 无法确定BasisConfigComponent类的模块

虽然翻译有点直白,大概意思就是,这个组件没有在模块里面申明,又存在这个组件,打包时候就找不到组件和模块依赖关系。

引起这个原因有2个:

  1. 你不是用cli命令创建的组件,ng g c 组件名

  2. 你之前在模块里面申明这个组件,后面以为业务需要这个组件被废弃了,你取消了模块申明,但是没有删除它。

那么解决问题方案就是:如果需要使用去模块申明它,如果无用的废弃组件及时删除它。

jiayisheji commented 5 years ago

关于*ngFor使用报错问题

ERROR
Error: Cannot find a differ supporting object '[object Object]' of type 'object'. NgFor only supports binding to Iterables such as Arrays.

错误翻译:无法找到类型为“object”的不同支持对象“[object object]”。NgFor只支持绑定到数组等迭代器。直接翻译很蛋疼,简单翻译NgFor只支持绑定到数组等迭代器,不支持[object object]对象。

[object object]对象,这是一个什么样的对象。在js里面数据类型分为原始类型和对象类型,对象又分很多种,我们可以借助Object.prototype.toString.call(value)来区分它们是什么。

jq源码有2个判断:

isPlainObject: function( obj ) {
        var proto, Ctor;

        // Detect obvious negatives
        // Use toString instead of jQuery.type to catch host objects
        if ( !obj || toString.call( obj ) !== "[object Object]" ) {
            return false;
        }

        proto = getProto( obj );

        // Objects with no prototype (e.g., `Object.create( null )`) are plain
        if ( !proto ) {
            return true;
        }

        // Objects with prototype are plain iff they were constructed by a global Object function
        Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor;
        return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString;
    },
var class2type = {};

var toString = class2type.toString;

var hasOwn = class2type.hasOwnProperty;

var fnToString = hasOwn.toString;

function toType( obj ) {
    if ( obj == null ) {
        return obj + "";
    }

    // Support: Android <=2.3 only (functionish RegExp)
    return typeof obj === "object" || typeof obj === "function" ?
        class2type[ toString.call( obj ) ] || "object" :
        typeof obj;
}

// Populate the class2type map
jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ),
function( i, name ) {
    class2type[ "[object " + name + "]" ] = name.toLowerCase();
} );

[object object]对象就是一个普通对象。

它们是谁:

// 1. 对象字面量,又有很多其他叫法,json对象,字典对象
Object.prototype.toString.call({})
// "[object Object]"
// 2. 标准对象
Object.prototype.toString.call(new Object);
// "[object Object]"
// 2. 构造函数对象
const fun = function() {};
Object.prototype.toString.call(new fun);
// "[object Object]"

数组等迭代器是什么?

遍历Array可以采用下标循环,遍历MapSet就无法使用下标。为了统一集合类型,ES6标准引入了新的iterable类型,ArrayMapSet都属于iterable类型。具有iterable类型的集合可以通过新的for ... of循环来遍历。

那感情好,我们可以使用ArrayMapSet啦。然并卵

使用Map一样会报错:

ERROR
Error: Cannot find a differ supporting object '[object Map]' of type 'object'. NgFor only supports binding to Iterables such as Arrays.

意思默认只能使用ArraySet

*ngFor是angular里面一个比较重要的结构型指令,还有2个比较常用的结构型指令:*ngSwitch*ngIf

这三个指令对应大多数编程语言的:forswitchif/else。那他们作用就不用再介绍了。

我先根据angular文档*ngFor写法很多。

常见2种写法:

第一种写法

<li *ngFor="let item of items; index as i; trackBy: trackByFn">...</li>

第二种写法

<ng-template ngFor let-item [ngForOf]="items" let-i="index" [ngForTrackBy]="trackByFn">
  <li>...</li>
</ng-template>

其实第一种写法是第二种写法的语法糖,简写。

那么先我需要遍历[object Object]或者'[object Map]'怎么办,angular团队也想到这个问题,就在6.1.0 (2018-07-25)推出一个管道keyValue,来弥补这个缺陷。

<div *ngFor="let item of object | keyvalue">
      {{item.key}}:{{item.value}}
</div>
<div *ngFor="let item of map | keyvalue">
      {{item.key}}:{{item.value}}
</div>

那如果是字符串我也想遍历怎么办了:

<li *ngFor="let item of '123456'.split('')">{{item}}</li>

vue里面如果是一个数字可以直接遍历,这个又该如何做到了:

<li *ngFor="let item of 10 | times">{{item}}</li>
import { Pipe, PipeTransform } from '@angular/core';
import { toNumber } from '@angular/cdk/coercion';

const MAX_SAFE_INTEGER = 9007199254740991;
const MAX_ARRAY_LENGTH = 4294967295;

@Pipe({
  name: 'times'
})
export class TimesPipe implements PipeTransform {
  transform(value: number): number[] {
    // 先把value转成数字 如果转换失败会返回0
    value = toNumber(value);
    // 如果小于1或者大于限制就直接返回空数组
    if (value < 1 || value > MAX_SAFE_INTEGER) {
      return [];
    }
    // 获取长度
    const length = Math.min(value, MAX_ARRAY_LENGTH);
    // 循环创建数组
    let index = -1;
    const result = [];
    while (++index < length) {
      result[index] = index;
    }
    return result;
  }
}

angular指令是个好东西,借助它可以增强很多特性。

最后说一点比较重要的事情,能在ts把数据处理好,优先处理好,在html里面去处理,会消耗一定的性能,特别大量数据循环的时候。