0%

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import { Component, ContentChild, Input, TemplateRef } from "@angular/core";

@Component({
selector:'app-partner-carousel',
template:`
<div class="carousel">
<div class="btn-pre" (click)="carouselMove('LEFT')" [hidden]="carouselIndex===0"></div>
<div class="slick-window">
<div class="slick-container" *ngIf="data.length>0" [ngStyle]="carouselAnimotion">
<span *ngFor="let dataItem of data">
<ng-container
*ngIf="dataItemTemplateRef"
[ngTemplateOutlet]="dataItemTemplateRef"
[ngTemplateOutletContext]="{$implicit:dataItem}"></ng-container>
</span>
</div>
</div>
<div class="btn-next" (click)="carouselMove('RIGHT')" [hidden]="carouselIndex>=data.length-3"></div>
</div>
`,
styles:[`.carousel{display:flex}`,
`.slick-window {
position: relative;
width: 690px;
overflow: hidden;
transform: translateZ(0);
}`,
`.slick-container{
transform: translate3d(0px, 0px, 0px);
transition: 0.6s ease-in-out;
// width: 1500px;
position: relative;
line-height: inherit;
margin-right: 45px;
overflow-y: hidden;
}`,
`.slick-container>span{display:inline-block}`,
`.btn-pre, .btn-next {
// width: 15px;
cursor: pointer;
}`,
`.btn-pre::before, .btn-next::before {
content: '';
width: 0;
height: 0;
position: absolute;
top: 16px;
border-top: 22px solid transparent;
border-bottom: 22px solid transparent;
}`,
`.btn-pre::before {
border-right: 15px solid #ccc;
border-left: 0px solid transparent;
left: -15px;
}`,
`.btn-next::before {
border-right: 0px solid transparent;
border-left: 15px solid #ccc;
}`]
})
export class PartnerCarouselComponent{
readonly slickWidth = 230;
carouselAnimotion:any = {};
private _data:Array<any> = [];
@Input()
set data(value:Array<any>){
this.carouselAnimotion.width = `${this.slickWidth * value.length}px`;
this._data = value;
}
get data(){
return this._data;
}
@ContentChild('dataItem', {static:false}) dataItemTemplateRef:TemplateRef<any>;
carouselIndex:number = 0;
constructor(){}

carouselMove(orient: string) {
let distance = 0;
if (orient === 'LEFT') {
this.carouselIndex = this.carouselIndex > 3 ? this.carouselIndex -= 3 : 0
}
if (orient === 'RIGHT') {
this.carouselIndex = this.carouselIndex < this._data.length - 5 ? this.carouselIndex += 3 : this._data.length - 3;
}
distance = this.slickWidth * this.carouselIndex;
this.carouselAnimotion.transform = `translate3d(-${distance}px, 0px, 0px)`;
}
}

滑动原理

position: relative和transform: translateZ(0)将‘窗口’定位,其子元素是宽度超过屏幕宽度的所有滑块的集合,这个超长的‘胶片’用translate3d控制移动,使其某一格可以暴露在窗口中
移动的动画效果为 transition: 0.6s ease-in-out

循环体套用父元素模板

子组件(Carousel)中,为每个item套用其宿主定义的template,这里用到了NgContainer和TemplateRef,NgContainer使组件可以内嵌视图模板,内嵌的内容(ng-template)用ViewChild读取,此处声明为dataItemTemplateRef,使用NgTemplateOutlet指令将dataItemTemplateRef插入到ng-content位置,同时可以用ngTemplateOutletContext指令为内嵌视图附加一个上下文对象,显然就是为传递循环体的item打造的。
使用#id指定要插入到ng-container的template
官方示例:

1
2
3
4
5
6
7
8
9
10
<ng-container *ngTemplateOutlet="greet"></ng-container>
<hr>
<ng-container *ngTemplateOutlet="eng; context: myContext"></ng-container>
<hr>
<ng-container *ngTemplateOutlet="svk; context: myContext"></ng-container>
<hr>

<ng-template #greet><span>Hello</span></ng-template>
<ng-template #eng let-name><span>Hello {{name}}!</span></ng-template>
<ng-template #svk let-person="localSk"><span>Ahoj {{person}}!</span></ng-template>

其中 myContext = {$implicit: ‘World’, localSk: ‘Svet’};模板中用let-绑定上下文, $implicit(implicit 隐式的)指默认值,也就是例如let-keyXX=”parameterXX”的语法在缺省=”parameterXX”的表达下的值

响应式解决方案

npm install @angular/sdk
注意该package大版本跟随Angular版本
Angular CDK Layout

1
2
3
4
5
6
7
8
9
10
11
12
constructor(private breakpointObserver: BreakpointObserver) {
this.breakpointObserver.observe([Breakpoints.XSmall, Breakpoints.Small]).subscribe(result => {
if (result.matches) {
// max-width: 959px
this.windowWidth = { width: `${this.slickWidth}px` };
this.interval = 1;
} else {
this.windowWidth = { width: `${this.slickWidth * 3}px` };
this.interval = 3;
}
});
}

Azure SQL包括

  • Azure SQL DB
  • Azure MI (Managed Instance 与SQL Server功能一致的服务,用于云迁移方案)
  • SQL Server in Azure VM
    Azure SQL DB 和 Azure MI属于PaaS, SQL Server in Azure VM属于IaaS

    可用性功能

    可用性区域和可用性组。较低服务层级中的数据库使用“不同但等效的机制”通过存储提供冗余。 内置逻辑可帮助防范单个计算机发生故障。 使用活动异地复制功能可以在灾难损毁整个区域时提供保护。
    业务连续性和全局可伸缩性。

安装并初始化既有项目

1
npm eslint --init

安装后自动运行cli提示,选择所需的运行环境(node.js vs browser),模块化风格(es import/export vs CommonJS),是否使用typescript等
之后会向package.json添加并安装@typescript-eslint/eslint-plugin eslint-plugin-react @typescript-eslint/parser eslint等

配置cli(commandline interface)
package.json

1
"lint": "eslint --ext .js src/"

Issue: ‘global’ is not defined
对于使用webpack打包的web app其编译环境是node.js的,因此可以配置

1
2
3
4
"env": {
"browser": true,
"node": true
},

ESLint:环境参数

Issues:

Arrow function expected no return value. (consistent-return)

141:7 error React Hook useEffect has a missing dependency: ‘render’. Either include it or remove the dependency array react-hooks/exhaustive-deps

render 方法中包含state属性 应改为useCallback 加入依赖state属性, 然后把render加入报错的副作用的依赖中

Expected ‘this’ to be used by class method ‘getValBetweenFms’. (class-methods-use-this)

改为static方法

Expected to return a value at the end of method ‘getValBetweenFms’. (consistent-return)

函数在循环体中某条件达成时返回 运行时必定返回 但静态类型检查不通过 应在函数末尾return undefined

工作区配置文件就是angular.json

关于当前Project

1
2
3
4
5
6
7
8
"MyApp": {
"root": "",
"sourceRoot": "src",
"projectType": "application",
"prefix": "app",
"schematics": {
"@schematics/angular:component": {}
},

编译/测试等工具

关于build target

architect/build 节会为 ng build 命令的选项配置默认值。它具有下列顶层属性:builder,option,configurations.
另外对应ng serve,ng test,ng lint命令,有architect/serve|test|lint命令

  • builder就是个编译器名字,默认是@angular-devkit/build-angular:browser,(ng test的是karma,ng lint的是tslint)
  • options提供构建时的选项及默认值,私以为这些option可以认为是builder工具链所需参数,因而在test和lint中也各有不同
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    "options": {
    "aot": true,
    "progress": false,
    "extractCss": true,
    "outputPath": "dist",
    "index": "src/index.html",
    "main": "src/main.ts",
    "polyfills": "src/polyfills.ts",
    "tsConfig": "src/tsconfig.app.json",
    "assets": [
    "src/favicon.ico",
    "src/assets",
    {
    "glob": "**/*",
    "input": "./node_modules/@ant-design/icons-angular/src/inline-svg/",
    "output": "/assets/"
    }
    ],
    "styles": [
    "src/theme.less",
    "src/styles.scss"
    ],
    "scripts": []
    }
  • configurations 脚手架生成项目会添加一个production的配置在这里,对编译进行部分优化以及打包限制,ng build 带—prod参数(注意—xxx是命令参数)使用该production配置,可以仿照production写其他(如 stage)配置,使用时形如 ng build —configuration stage
    另配置可以加载复数个,后者的项会覆盖前者:ng build —configuraion staging,fr
    “configurations”: {
    “production”: {
      "fileReplacements": [
      {
          "replace": "src/environments/environment.ts",
          "with": "src/environments/environment.prod.ts"
      }
      ],
      "optimization": true,
      "outputHashing": "all",
      "sourceMap": false,
      "extractCss": true,
      "namedChunks": false,
      "aot": true,
      "extractLicenses": true,
      "vendorChunk": false,
      "buildOptimizer": true,
      "budgets": [
      {
          "type": "initial",
          "maximumWarning": "2mb",
          "maximumError": "5mb"
      },
      {
          "type": "anyComponentStyle",
          "maximumWarning": "6kb",
          "maximumError": "10kb"
      }
      ]
    
    }
    }
    ```

    样式预处理选项

    Angular Doc:Styles and scripts configuration

ngFor loop with async pipe

参数订阅可观察对象的值,从而保持最新,当数据更新时,循环渲染的视图会随之更新,见StackOverflow:*ngFor loop with async pipe?
完整栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component({
selector: 'users-list',
template: `
<ul>
<li *ngFor="let user of users$ | async">
{{ user.username }}
</li>
</ul>
`
})
export class UsersListComponent {
users$;

constructor(private http: Http) { }

ngOnInit() {
this.users$ = this.http
.get('/api/users')
.map(res => res.json());
}
}

如果需要基于返回值的其他参数,有
1
*ngFor="let user of users$ | async as users; index as i"

这里的users就是Array类型了
类似的ngIf也可以用异步管道,形如*ngIf=”user$ | async as user”,显然这里的user$应为Observable\

ngIf else

1
2
3
4
5
6
<div *ngIf="isLoggedIn(); else notLoggedIn">
Hi, {%raw%}{{ user.name }}!{%endraw%}
</div>
<ng-template #notLoggedIn>
You're not logged in.
</ng-template>

事件‘委托’

需求是文件上传,往往隐藏input type=”file”而放一个好看的入口。
事件可以直接在HTML的native element上触发,在jQuery中

1
$("#fileInput").click();

原生js
1
document.getElementById("fileInput").click();

Angular可以使用viewChild获取元素
1
2
3
4
5
6
7
8
@ViewChild('fileInput') fileInput:ElementRef;
constructor(private renderer:Renderer) {}

showImageBrowseDlg() {
let event = new MouseEvent('click', {bubbles: true});
this.renderer.invokeElementMethod(
this.fileInput.nativeElement, 'dispatchEvent', [event]);
}

MDN:dispatchEvent
通过dom结构定位元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div style="width: 128px; height: 128px; background-color: #fafafa;
border: 1px dashed #d9d9d9;
border-radius: 2px;cursor: pointer;display: flex;justify-content: center;align-items: center;"
(click)="showImageBrowseDlg($event)"> +
<input type="file" style="display: none;" (change)="handleUpload($event)">
</div>

showImageBrowseDlg(event){
if(event.target.children[0]){
const fileinput:HTMLElement = event.target.children[0] as HTMLElement;
fileinput.click();
event.stopPropagation();
}
}

测试驱动开发(Test Driven Development, TDD)和行为驱动开发(Behavior Driven Development, BDD)
Jasmine 通过用自然语言书写非程序员可读的测试用例扩展了测试驱动开发方法, 行为驱动开发人员使用混合了领域中统一的语言的母语语言来描述他们的代码的目的

被测系统(System under test, SUT)

单测不负责检查跨类或者跨系统的交互逻辑,那都是集成测试的范围

单测不能受到外界环境的影响, 依赖需要用本地实现注入,或者提供一个mock(桩对象)

单测需要能快速执行,有必要在每次修改代码时运行单测

单测应随编码进行,补单测是没有意义的

1
2
3
4
5
6
7
8
9
10
11
12
13
├───lib
│ └───jasmine-3.4.0
│ boot.js
│ jasmine-html.js
│ jasmine.css
│ jasmine.js
│ jasmine_favicon.png
├───spec
│ PlayerSpec.js
│ SpecHelper.js
└───src
Player.js
Song.js

spec + src文件夹是栗子

笔记目的导向:

  • spec.ts写的啥(Jasmine 单元测试的书写语法)
  • 组件声明周期中应该存在哪些用例(如实例化,输入/输出,方法,释放)
  • 依赖项的处理
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    describe("Player", function() {
    var player;
    var song;

    beforeEach(function() {
    player = new Player();
    song = new Song();
    });

    it("should be able to play a Song", function() {
    player.play(song);
    expect(player.currentlyPlayingSong).toEqual(song);

    //demonstrates use of custom matcher
    expect(player).toBePlaying(song);
    });

    describe("when song has been paused", function() {
    beforeEach(function() {
    player.play(song);
    player.pause();
    });

    it("should indicate that the song is currently paused", function() {
    expect(player.isPlaying).toBeFalsy();

    // demonstrates use of 'not' with a custom matcher
    expect(player).not.toBePlaying(song);
    });

    it("should be possible to resume", function() {
    player.resume();
    expect(player.isPlaying).toBeTruthy();
    expect(player.currentlyPlayingSong).toEqual(song);
    });
    });

    // demonstrates use of spies to intercept and test method calls
    it("tells the current song if the user has made it a favorite", function() {
    spyOn(song, 'persistFavoriteStatus');

    player.play(song);
    player.makeFavorite();

    expect(song.persistFavoriteStatus).toHaveBeenCalledWith(true);
    });

    //demonstrates use of expected exceptions
    describe("#resume", function() {
    it("should throw an exception if song is already playing", function() {
    player.play(song);

    expect(function() {
    player.resume();
    }).toThrowError("song is already playing");
    });
    });
    });
    init with Node.js
    1
    2
    npm install --save-dev jasmine
    npx jasmine init

    describe

    以describe分组specs,它代表一组相似的测试用例,通常有 2 个参数:字符串和方法。字符串作为特定 Suite 的名字和标题。方法是包含实现的代码。
    1
    2
    3
    4
    5
    6
    describe ('HeroesService (with spies)', () => {
    ...
    });
    describe('HeroesService (with mocks)', () => {
    ...
    });

    waitForAsync TestBed

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing';
    describe('nz-table', () => {
    let injector: Injector;
    beforeEach(
    waitForAsync(() => { //
    injector = TestBed.configureTestingModule({
    imports: [BidiModule, NzTableModule],
    declarations: [NzTestTableBasicComponent, NzTestTableScrollComponent, NzTableSpecCrashComponent, NzTestTableRtlComponent]
    });
    TestBed.compileComponents();
    })
    );
    ...
    })

    it 和 specs

    specs即specification(规则),它们是一个个断言,可以是 true 或者 false。当每个 Spec 中的所有 expectations 都是 true,则通过测试。以it函数定义,与describe类似的,有 2 个参数:标题和方法。

    expect tobe

    1
    2
    expect(true).toBe(true);
    expect(false).not.toBe(true);

    断言

  • toBe 和 toEqual 前者相当于比较运算符=== 后者比较字面量的值(对于对象进行属性的比较)
  • toMatch
  • toBeDefined 和 toBeNull
  • toContain
  • toBeGreaterThan 和 toBeLessThan
  • toBeCloseTo
  • toThrow

    beforeEach和afterEach

    分别在每个it断言测试前/后调用

    spy

    存根(stub)和跟踪(track)任意函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    spyOn(foo, 'setBar');
    foo.setBar(123);
    foo.setBar(456, 'another param');
    });
    it("tracks that the spy was called", function() {
    expect(foo.setBar).toHaveBeenCalled();
    });
    it("tracks that the spy was called x times", function() {
    expect(foo.setBar).toHaveBeenCalledTimes(2);
    });
    it("tracks all the arguments of its calls", function() {
    expect(foo.setBar).toHaveBeenCalledWith(123);
    expect(foo.setBar).toHaveBeenCalledWith(456, 'another param');
    });
    spy一个foo对象上的setBar方法,分别断言该方法被调用、被调用若干次、被以某某参数调用
    很多时候用spy对应模拟对象

用例

Azure DevOps Engineer Expert

DevOps 是人员、流程和产品的集合体现,它可让我们向最终用户持续交付价值。 ———— Donovan Brown

Azure DevOps

Azure DevOps 是 Microsoft 提供的一种软件即服务 (SaaS) 平台,它能提供用于开发和部署软件的端到端 DevOps 工具链。
组成

  • Azure Repos 源代码管理
  • Azure Pipelines CI/CD服务
  • Azure Boards 类似TP的kanban工具以及Agile tools等
  • Azure Test Plans 测试工具,包括manual/exploratory testing 和 continuous testing
  • Azure Artifacts 大致上就是构建自己的库(allows teams to share packages such as Maven, npm, NuGet and more from public and private sources and integrate package sharing into your CI/CD pipelines)

生产DevOps的内容

  • 操作系统
  • 脚本
  • 容器
  • 其他

对于branch

对于敏捷开发的团队,选择避免使用长期分支(long-lived branch),以致力于短期功能和 Bug 修复分支,任何工作付出的目标都是以生成pull request将工作合并回master

A long-lived branch is a Git branch that is never deleted. Some teams prefer to avoid them altogether in favor of short-lived feature and bug fix branches. For those teams, the goal of any effort is to produce a pull request that merges their work back into master.
对于web应用,往往不会支持或回退到起初的版本,适用于上述工作方式,但也有其他场景需要长期保留分支,如用于同时支持市场上的多个版本,release V1, release V2将持续维护

来源微软Docs:GitHub基于Release的工作流

master是稳定版分支,与线上版本保持绝对一致,release是预发布分支,从develop创建出来进行测试。

关于release

语义化版本标签
release notes(存目)