0%

2020前端展望

跨端开发,混合应用,小程序等

工程化

FCC article:前端工程化

  • 编码规范
  • repository规范
  • ci/cd
  • 测试 单元测试 e2e
  • 性能
  • 数据上报 requestIdleCallback, restful api
  • 用户行为收集 metadata, page/input/click… —> kafka Queue —> ElasticSearch

Tips
刷新/关闭页面前发请求
Navigator.sendBeacon(url, data) 在页面unload时也可以调用

microservice和serverless

Interview T-stack

后端一次性给你10w条数据

网络延迟

抓包工具 Fiddler4 配置移动设备抓取 Https 请求


移动端‘像素点’密度往往高于桌面显示器,而横纵比例较小,使用移动设备打开页面,可能因为宽度不足产生挤压变形,也可能因为自动像素缩放导致layout变形

视口(viewport)

在document形成的整个视图中,从一个虚拟的窗口观察,视口较小时,需要借助横纵滚动条才能浏览页面

1
<meta name=”viewport” content=”width=device-width, initial-scale=1, maximum-scale=1″>

视口宽度为设备宽度,初始缩放比例为1,最大缩放比例为1
水平滚动条是极差的用户体验,将视口宽度设置为设备宽度,将页面样式调整到方便垂直滚动浏览

  • 请勿使用较大的固定宽度元素
  • 不要让内容依赖于特定值的视口宽度
  • 使用媒体查询为小屏幕和大屏幕应用不同样式

    Grid

    如Bootstrap Grid水平分割若干份,计算并罗列每份宽度,使用百分比,block之间用float排列,行末清除浮动
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    .col-1 {width: 8.33%;}
    .col-2 {width: 16.66%;}
    .col-3 {width: 25%;}
    .col-4 {width: 33.33%;}
    .col-5 {width: 41.66%;}
    .col-6 {width: 50%;}
    .col-7 {width: 58.33%;}
    .col-8 {width: 66.66%;}
    .col-9 {width: 75%;}
    .col-10 {width: 83.33%;}
    .col-11 {width: 91.66%;}
    .col-12 {width: 100%;}
    [class*="col-"] {
    float: left;
    padding: 15px;
    border: 1px solid red;
    }
    .row::after {
    content: "";
    clear: both;
    display: table;
    }

    Breakpoint

    设置一个边界,使用媒体查询,实现当宽度越过边界值时,触发从桌面屏幕layout到移动设备layout的跃变

    Images

    tip: 图片的拉伸是有限的,专业的做法是为不同设备提供不同的资源
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /* For width smaller than 400px: */
    body {
    background-image: url('img_smallflower.jpg');
    }

    /* For width 400px and larger: */
    @media only screen and (min-width: 400px) {
    body {
    background-image: url('img_flowers.jpg');
    }
    }
    HTML5 新元素支持指定多个资源以及媒体条件
    1
    2
    3
    4
    5
    <picture>
    <source srcset="img_smallflower.jpg" media="(max-width: 400px)">
    <source srcset="img_flowers.jpg">
    <img src="img_flowers.jpg" alt="Flowers">
    </picture>

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
npx eslint --init

安装后自动运行cli提示,选择所需的运行环境(node.js vs browser),模块化风格(es import/export vs CommonJS),是否使用typescript等

配置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:环境参数

工作区配置文件就是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对应模拟对象

用例