Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
jiayisheji opened this issue May 9, 2019 · 5 comments
Open

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

jiayisheji opened this issue May 9, 2019 · 5 comments
Labels
Angular angular相关实践

Comments

@jiayisheji
Copy link
Owner

jiayisheji commented May 9, 2019

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

关于依赖注入问题

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

image

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

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

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

  1. 直接在服务里面申明作用域
@Injectable({
  providedIn: 'root',
})
export class NameService {
}
  1. 根模块注册到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
Copy link
Owner Author

jiayisheji commented May 13, 2019

Cannot assign to a reference or variable!

image

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

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

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

  • 模板引用变量通常用来引用 模板中的某个DOM元素
  • 模板引用变量还可以用来引用Angular组件、指令 或 Web 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
Copy link
Owner Author

jiayisheji commented May 16, 2019

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
Copy link
Owner Author

关于循环依赖问题

什么叫循环依赖,比如我们创建一个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 jiayisheji added the Angular angular相关实践 label Jun 1, 2019
@jiayisheji
Copy link
Owner Author

打包时候出现错误

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
Copy link
Owner Author

关于*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里面去处理,会消耗一定的性能,特别大量数据循环的时候。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Angular angular相关实践
Projects
None yet
Development

No branches or pull requests

1 participant