diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..441ac71b Binary files /dev/null and b/.DS_Store differ diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index eba1110b..00000000 --- a/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -# Auto detect text files and perform LF normalization -* text=auto \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..f296fe54 --- /dev/null +++ b/.gitignore @@ -0,0 +1,82 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +*.DS_Store \ No newline at end of file diff --git a/.idea/Leo-JavaScript.iml b/.idea/Leo-JavaScript.iml new file mode 100644 index 00000000..24643cc3 --- /dev/null +++ b/.idea/Leo-JavaScript.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 00000000..9c694110 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..28a804d8 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..af6a5e30 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..94a25f7f --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 00000000..e0d8afc5 --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,187 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/hero-detail/hero-detail.component.spec.ts" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/hero-detail/hero-detail.component.spec.ts" new file mode 100644 index 00000000..5e34497b --- /dev/null +++ "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/hero-detail/hero-detail.component.spec.ts" @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HeroDetailComponent } from './hero-detail.component'; + +describe('HeroDetailComponent', () => { + let component: HeroDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ HeroDetailComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(HeroDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/hero-detail/hero-detail.component.ts" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/hero-detail/hero-detail.component.ts" new file mode 100644 index 00000000..4d6b9733 --- /dev/null +++ "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/hero-detail/hero-detail.component.ts" @@ -0,0 +1,40 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { Hero } from '../hero'; +import { ActivatedRoute } from '@angular/router'; +import { Location } from '@angular/common'; +import { HeroService } from '../hero.service'; + +@Component({ + selector: 'app-hero-detail', + templateUrl: './hero-detail.component.html', + styleUrls: ['./hero-detail.component.css'] +}) +export class HeroDetailComponent implements OnInit { + @Input() hero: Hero; // 外部数据通过@Input绑定数据 + constructor( + private route: ActivatedRoute, + private heroService: HeroService, + private location: Location + ) { } + + ngOnInit(): void { + this.getHero(); + } + goBack(): void { + this.location.back(); + } + getHero(): void{ + const id = + this.route.snapshot.paramMap.get('id'); + this.heroService.getHero(id).subscribe( + hero => this.hero = hero + ); + // route.snapshot 是一个路由信息的静态快照,抓取自组件刚刚创建完毕之后。 + // paramMap 是一个从 URL 中提取的路由参数值的字典。 "id" 对应的值就是要获取的英雄的 id。 + }; + save(): void{ + this.heroService.updateHero(this.hero).subscribe( + () => this.goBack() + ) + }; + +} diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/hero-search/hero-search.component.css" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/hero-search/hero-search.component.css" new file mode 100644 index 00000000..3faebe3a --- /dev/null +++ "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/hero-search/hero-search.component.css" @@ -0,0 +1,40 @@ +/* HeroSearch private styles */ +.search-result li { + border-bottom: 1px solid gray; + border-left: 1px solid gray; + border-right: 1px solid gray; + width: 195px; + height: 16px; + padding: 5px; + background-color: white; + cursor: pointer; + list-style-type: none; + margin: 0 auto; + } + + .search-result li:hover { + background-color: #607D8B; + } + + .search-result li a { + color: #888; + display: block; + text-decoration: none; + } + + .search-result li a:hover { + color: white; + } + .search-result li a:active { + color: white; + } + #search-box { + width: 200px; + height: 20px; + } + + + ul.search-result { + margin-top: 0; + padding-left: 0; + } \ No newline at end of file diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/hero-search/hero-search.component.html" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/hero-search/hero-search.component.html" new file mode 100644 index 00000000..c111268e --- /dev/null +++ "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/hero-search/hero-search.component.html" @@ -0,0 +1,14 @@ +
+

Hero Search

+ + + +
\ No newline at end of file diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/hero-search/hero-search.component.spec.ts" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/hero-search/hero-search.component.spec.ts" new file mode 100644 index 00000000..901bb7f2 --- /dev/null +++ "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/hero-search/hero-search.component.spec.ts" @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HeroSearchComponent } from './hero-search.component'; + +describe('HeroSearchComponent', () => { + let component: HeroSearchComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ HeroSearchComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(HeroSearchComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/hero-search/hero-search.component.ts" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/hero-search/hero-search.component.ts" new file mode 100644 index 00000000..a7d8a06d --- /dev/null +++ "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/hero-search/hero-search.component.ts" @@ -0,0 +1,39 @@ +import { Component, OnInit } from '@angular/core'; +import { Observable, Subject } from 'rxjs'; +import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators'; + +import { Hero } from '../hero'; +import { HeroService } from '../hero.service'; + +@Component({ + selector: 'app-hero-search', + templateUrl: './hero-search.component.html', + styleUrls: ['./hero-search.component.css'] +}) +export class HeroSearchComponent implements OnInit { + heroes$: Observable; + private searchTerms = new Subject(); + + constructor( + private heroSerive: HeroService + ) { } + + search(term: string): void{ + // Push a search term into the observable stream. + this.searchTerms.next(term); + // 通过调用next(value) 方法往 Observable 中推送一些值, + } + + ngOnInit() : void{ + this.heroes$ = this.searchTerms.pipe( + // 在传出最终字符串之前,debounceTime(300) 将会等待,直到新增字符串的事件暂停了 300 毫秒。 你实际发起请求的间隔永远不会小于 300ms。 + debounceTime(300), + + // distinctUntilChanged() 会确保只在过滤条件变化时才发送请求。 + distinctUntilChanged(), + + // switchMap() 会为每个从 debounce 和 distinctUntilChanged 中通过的搜索词调用搜索服务。 它会取消并丢弃以前的搜索可观察对象,只保留最近的。 + switchMap((term: string) => this.heroSerive.searchHeroes(term)) + ); + } +} diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/hero.service.spec.ts" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/hero.service.spec.ts" new file mode 100644 index 00000000..082791a7 --- /dev/null +++ "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/hero.service.spec.ts" @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { HeroService } from './hero.service'; + +describe('HeroService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: HeroService = TestBed.get(HeroService); + expect(service).toBeTruthy(); + }); +}); diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/hero.service.ts" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/hero.service.ts" new file mode 100644 index 00000000..bbe21fda --- /dev/null +++ "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/hero.service.ts" @@ -0,0 +1,99 @@ +import { Injectable } from '@angular/core'; +import { Hero } from './hero'; +import { HEROES } from './mock-heroes'; +import { Observable, of } from 'rxjs'; +import { MessageService } from './message.service'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { catchError, map, tap } from 'rxjs/operators'; + +const httpOptions = { + headers: new HttpHeaders({ + 'Content-Type': 'application/json' + }) +}; + + +// @Injectable把这个类标记为依赖注入系统的参与者之一 注册服务的提供商 +@Injectable({ + providedIn: 'root'//单一的、共享的,可以把它注入到任何想要它的类上 +}) + +export class HeroService { + + constructor( + private http: HttpClient, + private messageService: MessageService// 注入 MessageService + + ) { + + } + private heroesUrl = 'api/heroes'; + private log(message: string){ + this.messageService.add(`HeroService: ${message}`); + }; + private handleError (operation = 'opration', result?:T){ + return (error: any): Observable => { + console.error(error); + this.log(`${operation} failed:${error.message}`); + return of(result as T);// 返回一个空结果 让程序继续运行 + } + }; + getHeroes(): Observable{ + // this.messageService.add('HeroService: fetched heroes!'); + // return of(HEROES); + return this.http.get(this.heroesUrl).pipe( + tap(_ => this.log('fetched heroes')), + catchError(this.handleError('getHeroes', [])) + ); + } + + getHero(id: number): Observable{ + // this.messageService.add(`HeroService: fetched heroes!id = ${id}`); + // return of(HEROES.find(hero => hero.id === id)); + const url = `${this.heroesUrl}/${id}`; + return this.http.get(url).pipe( + tap(_ => this.log(`fetched hero id=${id}`)), + catchError(this.handleError(`getHero id=${id}`)) + ); + } + + updateHero (hero: Hero): Observable{ + return this.http.put(this.heroesUrl, hero, httpOptions).pipe( + tap(_ => this.log(`updated hero id = ${hero.id}`)), + catchError(this.handleError('updateHero')) + ) + }; + + addHero (hero: Hero): Observable{ + return this.http.post(this.heroesUrl, hero, httpOptions).pipe( + tap((hero:Hero) => this.log(`added hero w/ id=${hero.id}`)), + catchError(this.handleError('addHero')) + ); + } + + deleteHero(hero: Hero): Observable{ + const id = typeof hero === 'number' ? hero : hero.id; + const url = `${this.heroesUrl}/${id}`; + return this.http.delete(url, httpOptions).pipe( + tap( _ => this.log(`deleted hero id=${id}`)), + catchError(this.handleError('deleteHero')) + ); + }; + + searchHeroes(term: string): Observable{ + //如果没有搜索词,该方法立即返回一个空数组 + if(!term.trim()){ + return of([]) + } + return this.http.get(`${this.heroesUrl}/?name=${term}`).pipe( + tap(_ => this.log(`found heroes matching "${term}"`)), + catchError(this.handleError('serchHeroes', [])) + ) + } +} + + +// tap操作符会查看 Observable 中的值,使用那些值做一些事情,并且把它们传出来。这种 tap 回调不会改变这些值本身。 + +// HttpClient.put() 方法接受三个参数 +// URL 地址, 要修改的数据(这里就是修改后的英雄),选项 \ No newline at end of file diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/hero.ts" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/hero.ts" new file mode 100644 index 00000000..08a76ef9 --- /dev/null +++ "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/hero.ts" @@ -0,0 +1,4 @@ +export class Hero { + id: number; + name: string; +} \ No newline at end of file diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/heroes/heroes.component.css" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/heroes/heroes.component.css" new file mode 100644 index 00000000..0f74618f --- /dev/null +++ "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/heroes/heroes.component.css" @@ -0,0 +1,73 @@ +/* HeroesComponent's private CSS styles */ +.heroes { + margin: 0 0 2em 0; + list-style-type: none; + padding: 0; + width: 15em; +} +.heroes li { + position: relative; + cursor: pointer; + background-color: #EEE; + margin: .5em; + padding: .3em 0; + height: 1.6em; + border-radius: 4px; +} + +.heroes li:hover { + color: #607D8B; + background-color: #DDD; + left: .1em; +} + +.heroes a { + color: #888; + text-decoration: none; + position: relative; + display: block; + width: 250px; +} + +.heroes a:hover { + color:#607D8B; +} + +.heroes .badge { + display: inline-block; + font-size: small; + color: white; + padding: 0.8em 0.7em 0 0.7em; + background-color: #607D8B; + line-height: 1em; + position: relative; + left: -1px; + top: -4px; + height: 1.8em; + min-width: 16px; + text-align: right; + margin-right: .8em; + border-radius: 4px 0 0 4px; +} + +button { + background-color: #eee; + border: none; + padding: 5px 10px; + border-radius: 4px; + cursor: pointer; + cursor: hand; + font-family: Arial; +} + +button:hover { + background-color: #cfd8dc; +} + +button.delete { + position: relative; + left: 194px; + top: -32px; + background-color: gray !important; + color: white; +} \ No newline at end of file diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/heroes/heroes.component.html" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/heroes/heroes.component.html" new file mode 100644 index 00000000..1a510250 --- /dev/null +++ "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/heroes/heroes.component.html" @@ -0,0 +1,30 @@ +

My Heroes

+
+ + +
+ + diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/heroes/heroes.component.spec.ts" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/heroes/heroes.component.spec.ts" new file mode 100644 index 00000000..66518e44 --- /dev/null +++ "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/heroes/heroes.component.spec.ts" @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HeroesComponent } from './heroes.component'; + +describe('HeroesComponent', () => { + let component: HeroesComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ HeroesComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(HeroesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/heroes/heroes.component.ts" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/heroes/heroes.component.ts" new file mode 100644 index 00000000..82bcc39f --- /dev/null +++ "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/heroes/heroes.component.ts" @@ -0,0 +1,55 @@ +import { Component, OnInit } from '@angular/core'; +import { Hero } from '../hero'; +import { HeroService } from '../hero.service'; + +@Component({ + selector: 'app-heroes', + templateUrl: './heroes.component.html', + styleUrls: ['./heroes.component.css'] + // selector — 组件的选择器(CSS 元素选择器) + // templateUrl — 组件模板文件的位置。 + // styleUrls — 组件私有 CSS 样式表文件的位置。 +}) +export class HeroesComponent implements OnInit { + hero: Hero = { + id: 1, + name:'Windstorm' + }; + heroes: Hero[]; + + // selectedHero : Hero; + // onSelect(hero : Hero): void{ + // this.selectedHero = hero; + // }; + + constructor(private heroService: HeroService) { //标记为一个 HeroService 的注入点 + + } + + getHeroes(): void{ + this.heroService.getHeroes().subscribe( // 订阅服务 + heroes => this.heroes = heroes + ); + + }; + + ngOnInit() { + this.getHeroes(); + } + + add(name: string): void{ + name = name.trim(); + if(!name)return; + this.heroService.addHero({name} as Hero).subscribe( + hero => { + this.heroes.push(hero); + } + ) + } + + delete(hero: Hero):void{ + this.heroes = this.heroes.filter(h => h !== hero); + this.heroService.deleteHero(hero).subscribe(); + } + +} diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/in-memory-data.service.spec.ts" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/in-memory-data.service.spec.ts" new file mode 100644 index 00000000..a75ef029 --- /dev/null +++ "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/in-memory-data.service.spec.ts" @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { InMemoryDataService } from './in-memory-data.service'; + +describe('InMemoryDataService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: InMemoryDataService = TestBed.get(InMemoryDataService); + expect(service).toBeTruthy(); + }); +}); diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/in-memory-data.service.ts" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/in-memory-data.service.ts" new file mode 100644 index 00000000..91250c2a --- /dev/null +++ "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/in-memory-data.service.ts" @@ -0,0 +1,29 @@ +import { Injectable } from '@angular/core'; +import { Hero } from './hero'; +import { InMemoryDbService } from 'angular-in-memory-web-api'; + +@Injectable({ + providedIn: 'root' +}) +export class InMemoryDataService implements InMemoryDbService { + createDb() { + const heroes = [ + { id: 11, name: 'Mr. Nice' }, + { id: 12, name: 'Narco' }, + { id: 13, name: 'Bombasto' }, + { id: 14, name: 'Celeritas' }, + { id: 15, name: 'Magneta' }, + { id: 16, name: 'RubberMan' }, + { id: 17, name: 'Dynama' }, + { id: 18, name: 'Dr IQ' }, + { id: 19, name: 'Magma' }, + { id: 20, name: 'Tornado' } + ] + return {heroes}; + } + genId(heroes: Hero[]): number { + return heroes.length > 0? Math.max(...heroes.map(hero => hero.id)) + 1 : 11; + } + + constructor() { } +} diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/message.service.spec.ts" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/message.service.spec.ts" new file mode 100644 index 00000000..24d2d1d3 --- /dev/null +++ "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/message.service.spec.ts" @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { MessageService } from './message.service'; + +describe('MessageService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: MessageService = TestBed.get(MessageService); + expect(service).toBeTruthy(); + }); +}); diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/message.service.ts" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/message.service.ts" new file mode 100644 index 00000000..bae6d133 --- /dev/null +++ "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/message.service.ts" @@ -0,0 +1,18 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class MessageService { + messages: string[] = []; + + add(message: string) { + this.messages.push(message); + }; + + clear(){ + this.messages = []; + }; + + constructor() { } +} diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/messages/messages.component.css" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/messages/messages.component.css" new file mode 100644 index 00000000..e69de29b diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/messages/messages.component.html" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/messages/messages.component.html" new file mode 100644 index 00000000..586527c2 --- /dev/null +++ "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/messages/messages.component.html" @@ -0,0 +1,12 @@ +
+

Message

+ +
+ {{message}} +
+
\ No newline at end of file diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/messages/messages.component.spec.ts" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/messages/messages.component.spec.ts" new file mode 100644 index 00000000..66109cc1 --- /dev/null +++ "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/messages/messages.component.spec.ts" @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MessagesComponent } from './messages.component'; + +describe('MessagesComponent', () => { + let component: MessagesComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ MessagesComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MessagesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/messages/messages.component.ts" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/messages/messages.component.ts" new file mode 100644 index 00000000..95165a52 --- /dev/null +++ "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/messages/messages.component.ts" @@ -0,0 +1,16 @@ +import { Component, OnInit } from '@angular/core'; +import { MessageService } from '../message.service'; + +@Component({ + selector: 'app-messages', + templateUrl: './messages.component.html', + styleUrls: ['./messages.component.css'] +}) +export class MessagesComponent implements OnInit { + + constructor(public messageService: MessageService) { } + + ngOnInit() { + } + +} diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/mock-heroes.ts" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/mock-heroes.ts" new file mode 100644 index 00000000..317e0224 --- /dev/null +++ "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/app/mock-heroes.ts" @@ -0,0 +1,14 @@ +import { Hero } from './hero'; + +export const HEROES: Hero[] = [ + { id: 11, name: 'Mr. Nice' }, + { id: 12, name: 'Narco' }, + { id: 13, name: 'Bombasto' }, + { id: 14, name: 'Celeritas' }, + { id: 15, name: 'Magneta' }, + { id: 16, name: 'RubberMan' }, + { id: 17, name: 'Dynama' }, + { id: 18, name: 'Dr IQ' }, + { id: 19, name: 'Magma' }, + { id: 20, name: 'Tornado' } +]; \ No newline at end of file diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/src/assets/.gitkeep" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/assets/.gitkeep" new file mode 100644 index 00000000..e69de29b diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/src/browserslist" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/browserslist" new file mode 100644 index 00000000..37371cb0 --- /dev/null +++ "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/browserslist" @@ -0,0 +1,11 @@ +# This file is currently used by autoprefixer to adjust CSS to support the below specified browsers +# For additional information regarding the format and rule options, please see: +# https://github.com/browserslist/browserslist#queries +# +# For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed + +> 0.5% +last 2 versions +Firefox ESR +not dead +not IE 9-11 \ No newline at end of file diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/src/environments/environment.prod.ts" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/environments/environment.prod.ts" new file mode 100644 index 00000000..3612073b --- /dev/null +++ "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/environments/environment.prod.ts" @@ -0,0 +1,3 @@ +export const environment = { + production: true +}; diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/src/environments/environment.ts" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/environments/environment.ts" new file mode 100644 index 00000000..7b4f817a --- /dev/null +++ "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/environments/environment.ts" @@ -0,0 +1,16 @@ +// This file can be replaced during build by using the `fileReplacements` array. +// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. +// The list of file replacements can be found in `angular.json`. + +export const environment = { + production: false +}; + +/* + * For easier debugging in development mode, you can import the following file + * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. + * + * This import should be commented out in production mode because it will have a negative impact + * on performance if an error is thrown. + */ +// import 'zone.js/dist/zone-error'; // Included with Angular CLI. diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/src/favicon.ico" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/favicon.ico" new file mode 100644 index 00000000..8081c7ce Binary files /dev/null and "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/favicon.ico" differ diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/src/index.html" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/index.html" new file mode 100644 index 00000000..565f4a57 --- /dev/null +++ "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/index.html" @@ -0,0 +1,14 @@ + + + + + AngularDemo + + + + + + + + + diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/src/karma.conf.js" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/karma.conf.js" new file mode 100644 index 00000000..b6e00421 --- /dev/null +++ "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/karma.conf.js" @@ -0,0 +1,31 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage-istanbul-reporter'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + coverageIstanbulReporter: { + dir: require('path').join(__dirname, '../coverage'), + reports: ['html', 'lcovonly'], + fixWebpackSourcePaths: true + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false + }); +}; \ No newline at end of file diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/src/main.ts" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/main.ts" new file mode 100644 index 00000000..28bfa9e1 --- /dev/null +++ "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/main.ts" @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic().bootstrapModule(AppModule) + .catch(err => console.error(err)); + diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/src/polyfills.ts" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/polyfills.ts" new file mode 100644 index 00000000..d310405a --- /dev/null +++ "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/polyfills.ts" @@ -0,0 +1,80 @@ +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), + * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. + * + * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** IE9, IE10 and IE11 requires all of the following polyfills. **/ +// import 'core-js/es6/symbol'; +// import 'core-js/es6/object'; +// import 'core-js/es6/function'; +// import 'core-js/es6/parse-int'; +// import 'core-js/es6/parse-float'; +// import 'core-js/es6/number'; +// import 'core-js/es6/math'; +// import 'core-js/es6/string'; +// import 'core-js/es6/date'; +// import 'core-js/es6/array'; +// import 'core-js/es6/regexp'; +// import 'core-js/es6/map'; +// import 'core-js/es6/weak-map'; +// import 'core-js/es6/set'; + +/** IE10 and IE11 requires the following for NgClass support on SVG elements */ +// import 'classlist.js'; // Run `npm install --save classlist.js`. + +/** IE10 and IE11 requires the following for the Reflect API. */ +// import 'core-js/es6/reflect'; + + +/** Evergreen browsers require these. **/ +// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. +import 'core-js/es7/reflect'; + + +/** + * Web Animations `@angular/platform-browser/animations` + * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. + * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). + **/ +// import 'web-animations-js'; // Run `npm install --save web-animations-js`. + +/** + * By default, zone.js will patch all possible macroTask and DomEvents + * user can disable parts of macroTask/DomEvents patch by setting following flags + */ + + // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame + // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick + // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames + + /* + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + */ +// (window as any).__Zone_enable_cross_context_check = true; + +/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ +import 'zone.js/dist/zone'; // Included with Angular CLI. + + + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/src/styles.css" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/styles.css" new file mode 100644 index 00000000..90d4ee00 --- /dev/null +++ "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/styles.css" @@ -0,0 +1 @@ +/* You can add global styles to this file, and also import other style files */ diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/src/test.ts" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/test.ts" new file mode 100644 index 00000000..16317897 --- /dev/null +++ "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/test.ts" @@ -0,0 +1,20 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/dist/zone-testing'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + +declare const require: any; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting() +); +// Then we find all the tests. +const context = require.context('./', true, /\.spec\.ts$/); +// And load the modules. +context.keys().map(context); diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/src/tsconfig.app.json" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/tsconfig.app.json" new file mode 100644 index 00000000..190fd300 --- /dev/null +++ "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/tsconfig.app.json" @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/app", + "types": [] + }, + "exclude": [ + "test.ts", + "**/*.spec.ts" + ] +} diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/src/tsconfig.spec.json" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/tsconfig.spec.json" new file mode 100644 index 00000000..de773363 --- /dev/null +++ "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/tsconfig.spec.json" @@ -0,0 +1,18 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/spec", + "types": [ + "jasmine", + "node" + ] + }, + "files": [ + "test.ts", + "polyfills.ts" + ], + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/src/tslint.json" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/tslint.json" new file mode 100644 index 00000000..52e2c1a5 --- /dev/null +++ "b/Cute-Angular/angualr\345\256\230\347\275\221demo/src/tslint.json" @@ -0,0 +1,17 @@ +{ + "extends": "../tslint.json", + "rules": { + "directive-selector": [ + true, + "attribute", + "app", + "camelCase" + ], + "component-selector": [ + true, + "element", + "app", + "kebab-case" + ] + } +} diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/tsconfig.json" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/tsconfig.json" new file mode 100644 index 00000000..916247e4 --- /dev/null +++ "b/Cute-Angular/angualr\345\256\230\347\275\221demo/tsconfig.json" @@ -0,0 +1,21 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "sourceMap": true, + "declaration": false, + "module": "es2015", + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "target": "es5", + "typeRoots": [ + "node_modules/@types" + ], + "lib": [ + "es2017", + "dom" + ] + } +} diff --git "a/Cute-Angular/angualr\345\256\230\347\275\221demo/tslint.json" "b/Cute-Angular/angualr\345\256\230\347\275\221demo/tslint.json" new file mode 100644 index 00000000..6ddb6b29 --- /dev/null +++ "b/Cute-Angular/angualr\345\256\230\347\275\221demo/tslint.json" @@ -0,0 +1,131 @@ +{ + "rulesDirectory": [ + "node_modules/codelyzer" + ], + "rules": { + "arrow-return-shorthand": true, + "callable-types": true, + "class-name": true, + "comment-format": [ + true, + "check-space" + ], + "curly": true, + "deprecation": { + "severity": "warn" + }, + "eofline": true, + "forin": true, + "import-blacklist": [ + true, + "rxjs/Rx" + ], + "import-spacing": true, + "indent": [ + true, + "spaces" + ], + "interface-over-type-literal": true, + "label-position": true, + "max-line-length": [ + true, + 140 + ], + "member-access": false, + "member-ordering": [ + true, + { + "order": [ + "static-field", + "instance-field", + "static-method", + "instance-method" + ] + } + ], + "no-arg": true, + "no-bitwise": true, + "no-console": [ + true, + "debug", + "info", + "time", + "timeEnd", + "trace" + ], + "no-construct": true, + "no-debugger": true, + "no-duplicate-super": true, + "no-empty": false, + "no-empty-interface": true, + "no-eval": true, + "no-inferrable-types": [ + true, + "ignore-params" + ], + "no-misused-new": true, + "no-non-null-assertion": true, + "no-redundant-jsdoc": true, + "no-shadowed-variable": true, + "no-string-literal": false, + "no-string-throw": true, + "no-switch-case-fall-through": true, + "no-trailing-whitespace": true, + "no-unnecessary-initializer": true, + "no-unused-expression": true, + "no-use-before-declare": true, + "no-var-keyword": true, + "object-literal-sort-keys": false, + "one-line": [ + true, + "check-open-brace", + "check-catch", + "check-else", + "check-whitespace" + ], + "prefer-const": true, + "quotemark": [ + true, + "single" + ], + "radix": true, + "semicolon": [ + true, + "always" + ], + "triple-equals": [ + true, + "allow-null-check" + ], + "typedef-whitespace": [ + true, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + } + ], + "unified-signatures": true, + "variable-name": false, + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ], + "no-output-on-prefix": true, + "use-input-property-decorator": true, + "use-output-property-decorator": true, + "use-host-property-decorator": true, + "no-input-rename": true, + "no-output-rename": true, + "use-life-cycle-interface": true, + "use-pipe-transform-interface": true, + "component-class-suffix": true, + "directive-class-suffix": true + } +} diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/README.md" "b/Cute-Angular/books\351\241\271\347\233\256demo/README.md" new file mode 100644 index 00000000..0eb45f08 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/README.md" @@ -0,0 +1,46 @@ +## 本文目录 +* 一、[项目起步](https://github.com/pingan8787/Leo-JavaScript/blob/master/Angular/books%E9%A1%B9%E7%9B%AEdemo/angular_books_1.md) +* 二、[编写路由组件](https://github.com/pingan8787/Leo-JavaScript/blob/master/Angular/books%E9%A1%B9%E7%9B%AEdemo/angular_books_1.md) +* 三、[编写页面组件](https://github.com/pingan8787/Leo-JavaScript/blob/master/Angular/books%E9%A1%B9%E7%9B%AEdemo/angular_books_2.md) + * 1.[编写单一组件](https://github.com/pingan8787/Leo-JavaScript/blob/master/Angular/books%E9%A1%B9%E7%9B%AEdemo/angular_books_2.md) + * 2.[模拟数据](https://github.com/pingan8787/Leo-JavaScript/blob/master/Angular/books%E9%A1%B9%E7%9B%AEdemo/angular_books_2.md) + * 3.[编写主从组件](https://github.com/pingan8787/Leo-JavaScript/blob/master/Angular/books%E9%A1%B9%E7%9B%AEdemo/angular_books_2.md) +* 四、[编写服务](https://github.com/pingan8787/Leo-JavaScript/blob/master/Angular/books%E9%A1%B9%E7%9B%AEdemo/angular_books_3.md) + * 1.[为什么需要服务](https://github.com/pingan8787/Leo-JavaScript/blob/master/Angular/books%E9%A1%B9%E7%9B%AEdemo/angular_books_3.md) + * 2.[编写服务](https://github.com/pingan8787/Leo-JavaScript/blob/master/Angular/books%E9%A1%B9%E7%9B%AEdemo/angular_books_3.md) +* 五、[引入RxJS](https://github.com/pingan8787/Leo-JavaScript/blob/master/Angular/books%E9%A1%B9%E7%9B%AEdemo/angular_books_3.md) + * 1.[关于RxJS](https://github.com/pingan8787/Leo-JavaScript/blob/master/Angular/books%E9%A1%B9%E7%9B%AEdemo/angular_books_3.md) + * 2.[引入RxJS](https://github.com/pingan8787/Leo-JavaScript/blob/master/Angular/books%E9%A1%B9%E7%9B%AEdemo/angular_books_3.md) + * 3.[改造数据获取方式](https://github.com/pingan8787/Leo-JavaScript/blob/master/Angular/books%E9%A1%B9%E7%9B%AEdemo/angular_books_3.md) +* 六、[改造组件](https://github.com/pingan8787/Leo-JavaScript/blob/master/Angular/books%E9%A1%B9%E7%9B%AEdemo/angular_books_4.md) + * 1.[添加历史记录组件](https://github.com/pingan8787/Leo-JavaScript/blob/master/Angular/books%E9%A1%B9%E7%9B%AEdemo/angular_books_4.md) + * 2.[添加和删除历史记录](https://github.com/pingan8787/Leo-JavaScript/blob/master/Angular/books%E9%A1%B9%E7%9B%AEdemo/angular_books_4.md) +* 七、[HTTP改造](https://github.com/pingan8787/Leo-JavaScript/blob/master/Angular/books%E9%A1%B9%E7%9B%AEdemo/angular_books_4.md) + * 1.[引入HTTP](https://github.com/pingan8787/Leo-JavaScript/blob/master/Angular/books%E9%A1%B9%E7%9B%AEdemo/angular_books_4.md) + * 2.[通过HTTP请求数据](https://github.com/pingan8787/Leo-JavaScript/blob/master/Angular/books%E9%A1%B9%E7%9B%AEdemo/angular_books_4.md) + * 3.[通过HTTP修改数据](https://github.com/pingan8787/Leo-JavaScript/blob/master/Angular/books%E9%A1%B9%E7%9B%AEdemo/angular_books_4.md) + * 4.[通过HTTP增加数据](https://github.com/pingan8787/Leo-JavaScript/blob/master/Angular/books%E9%A1%B9%E7%9B%AEdemo/angular_books_4.md) + * 5.[通过HTTP删除数据](https://github.com/pingan8787/Leo-JavaScript/blob/master/Angular/books%E9%A1%B9%E7%9B%AEdemo/angular_books_4.md) + * 6.[通过HTTP查找数据](https://github.com/pingan8787/Leo-JavaScript/blob/master/Angular/books%E9%A1%B9%E7%9B%AEdemo/angular_books_4.md) +* 八、结语 + +## 项目介绍 + +这个入门项目是我学习完[Angular 英雄指南教程](https://angular.cn/tutorial)后,自己手写的一个练习项目,一步一步来,最终的[项目源码可以这里查看](https://github.com/pingan8787/Leo-JavaScript/tree/master/Cute-Angular/books%E9%A1%B9%E7%9B%AEdemo),大佬们请指点啦。 + +推荐两个Angular学习网站: +1. [Angular 中文网](https://angular.cn/) +2. [Angular 修仙之路](http://www.semlinker.com/) + +还有呢,我没怎么关注到样式,所以样式会有点丑,主要都放在核心逻辑中了。 +**最终实现:** +* 首页书本列表数据展示 +* 各个页面静态/动态路由跳转 +* 本地模拟数据服务 +* 书本数据的增删改查 +* 父子组件通信 +* 常用指令使用和介绍 + +![图片结果](https://camo.githubusercontent.com/ded9be340188be8d3cb8ee2fd6adfe614f07e043/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031392f322f32332f313639313832386637643766663866373f773d38393526683d38343726663d706e6726733d313031303632) + +后面我将把这个系列的文章,收录到我的[【CuteJavaScript】](http://js.pingan8787.com)中,里面有整理了**ES6/7/8/9知识点**和**重温JS基础系列**文章。 \ No newline at end of file diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/angular_books_1.md" "b/Cute-Angular/books\351\241\271\347\233\256demo/angular_books_1.md" new file mode 100644 index 00000000..1e904465 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/angular_books_1.md" @@ -0,0 +1,275 @@ +## 本文目录 +* **一、项目起步** +* **二、编写路由组件** +* 三、编写页面组件 + * 1.编写单一组件 + * 2.模拟数据 + * 3.编写主从组件 +* 四、编写服务 + * 1.为什么需要服务 + * 2.编写服务 +* 五、引入RxJS + * 1.关于RxJS + * 2.引入RxJS + * 3.改造数据获取方式 +* 六、改造组件 + * 1.添加历史记录组件 + * 2.添加和删除历史记录 +* 七、HTTP改造 + * 1.引入HTTP + * 2.通过HTTP请求数据 + * 3.通过HTTP修改数据 + * 4.通过HTTP增加数据 + * 5.通过HTTP删除数据 + * 6.通过HTTP查找数据 +* 八、结语 + + + +这个入门项目是我学习完[Angular 英雄指南教程](https://angular.cn/tutorial)后,自己手写的一个练习项目,一步一步来,最终的[项目源码可以这里查看](https://github.com/pingan8787/Leo-JavaScript/tree/master/Cute-Angular/books%E9%A1%B9%E7%9B%AEdemo),大佬们请指点啦。 + +推荐两个Angular学习网站: +1. [Angular 中文网](https://angular.cn/) +2. [Angular 修仙之路](http://www.semlinker.com/) + +还有呢,我没怎么关注到样式,所以样式会有点丑,主要都放在核心逻辑中了。 +**最终实现:** +* 首页书本列表数据展示 +* 各个页面静态/动态路由跳转 +* 本地模拟数据服务 +* 书本数据的增删改查 +* 父子组件通信 +* 常用指令使用和介绍 + +![图片结果](https://camo.githubusercontent.com/ded9be340188be8d3cb8ee2fd6adfe614f07e043/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031392f322f32332f313639313832386637643766663866373f773d38393526683d38343726663d706e6726733d313031303632) + +后面我将把这个系列的文章,收录到我的[【CuteJavaScript】](http://js.pingan8787.com)中,里面有整理了**ES6/7/8/9知识点**和**重温JS基础系列**文章。 + +那么,快跟我一步步来完成这个入门项目吧。 + +## 零、Angular安装 +Angular 需要 `Node.js` 的 `8.x` 或 `10.x` 版本。 +检查你的`Node.js`版本,请在终端/控制台窗口中运行 `node -v` 命令。 +要想安装` Node.js`,请访问 nodejs.org。 + +1. 安装Angular CLI + +```sh +npm install -g @angular/cli +``` + +2. 常用命令 + +后续用到会详细介绍这些命令。 + +* 启动服务,并打开新窗口 +```sh +ng serve --open +# --open 可简写 -o +``` + +* 创建新组件 +```sh +ng generate component books +# generate 可简写 g +``` + +* 创建新服务 +```sh +ng generate service books +``` + +* 创建路由模块 +```sh +ng generate module app-routing --flat --module=app +``` + +* 其他 +另外Angular CLI还有很多的命令提供,详细可以查阅官方文档 [Angular CLI 命令](https://angular.cn/cli)。 + +最后搭建完是这样: + +![图片创建](https://camo.githubusercontent.com/9c67d00f8b707eae15fd6c5d1a907942641909ad/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031392f322f32332f313639313832386637633431386237393f773d35373926683d36333126663d706e6726733d3239313137) + +## 一、项目起步 +1. 创建项目 +```sh +ng new books +cd books +``` + +2. 创建所需的两个页面组件 +```sh +ng g component index +ng g component detail +``` +`g`是`generate`的简写。 + +然后运行项目: + +```sh +ng serve --open +``` + +## 二、编写路由组件 +这里为了项目结构先起来,所以先简单配置一下路由,后面路由会调整,如果遇到什么不懂,可以查看[Angular 路由与导航](https://angular.cn/guide/router)。 + +1. 安装**路由模块** +```sh +ng g module app-routing --flat --module=app +``` +**知识点:** +`--flat` 把这个文件放进了 `src/app` 中,而不是单独的目录中。 +`--module=app` 告诉 CLI 把它注册到 `AppModule` 的 `imports` 数组中。 + +2. 引入**路由模块** +```js +// app-routing.module.ts +import { RouterModule, Routes } from '@angular/router'; +``` +3. 导出**路由模块**的指令 + +这里需要添加一个 `@NgModule.exports` 数组,并传入`RouterModule`,导出 `RouterModule` 让路由器的相关指令可以在 `AppModule` 中的组件中使用。 +```js +// app-routing.module.ts +@NgModule({ + imports: [CommonModule], + declarations: [], + exports: [RouterModule] +}) +``` + +4. 添加定义路由 + +这里添加路由的时候,记得将所需要指向的组件也引入进来,这里我们需要引入两个页面的组件: +```js +// app-routing.module.ts +import { IndexComponent } from './index/index.component'; +import { DetailComponent } from './detail/detail.component'; +``` +然后将我们所需要的路由定义在`routes`变量中,类型是我们引入的`Routes`: +```js +// app-routing.module.ts +const routes: Routes = [ + { path: '', redirectTo:'/index', pathMatch:'full' }, // 1 + { path: 'index', component: IndexComponent}, // 2 + { path: 'detail/:id', component: DetailComponent}, // 3 +] +``` +**知识点**: +`angular`的路由接收两个参数: +* `path`:用于匹配浏览器地址栏中 `URL` 的字符串。 +* `component`:当导航到此路由时,路由器展示的组件名称。 + +**第1行代码**: +作为路由系统的默认路由,当所有路由都不匹配的话,就会重定向到这个路由,并展示对应的组件。 +**第2行代码**: +正常情况下的路由配置。 +**第3行代码**: +配置的是携带参数的路由,在路由`/`后,用 `:` 拼接参数名来实现,**获取这个参数的值的方法后面会介绍**。 + +另外,我们还可以这么传递参数,直接将数据通过路由传入,后面还会介绍: +```js +{ path: 'pathname', component: DemoComponent, data: { title: 'pingan8787' } }, +``` + +5. 添加路由监视 + +配置好路由还不能使用,需要一个监视路由变化的工具,这时候需要把`RouterModule`添加到 `@NgModule.imports` 数组中,并用 `routes` 来配置它。 +这里只需要调用` imports `数组中的 `RouterModule.forRoot()` 函数就行了,就像这样: +```js +// app-routing.module.ts +imports: [ RouterModule.forRoot(routes) ], +``` + +6. 添加路由出口 + +所谓的路由出口,就是路由所对应的组件展示的地方,接下来我们在`app.component.html`内容中,添加``: +```html + +
+

欢迎来到我的个人书屋!

+ +
+``` +这里的``就是我们路由输出的地方,也是组件展示的地方,简单理解就是,它会告诉路由器要在哪里显示路由的视图。 + +7. 添加路由链接 + +所谓的路由链接,就是出发路由跳转事件的地方,比如一个按钮,一张图片等,我们还是在`app.component.html`中,使用``添加3个按钮: +```html + +
+

欢迎来到我的个人书屋!

+ 重定向 | + 打开首页 | + 打开书本详情 + +
+``` +这边3个按钮的路由,我们将上面定义的3种路由,传入到`routerLink`参数中,现在就项目就可以实现页面跳转了。 + +另外,这里还可以传入一个可选参数`routerLinkActive="className"`,表示当这个``标签激活的时候显示的样式,值是一个字符串,为样式的类名: +```html +打开首页 | +``` + +8. 获取带参数路由的参数 + +在第7步中,我们点击 **打开书本详情** 按钮中,在路由中带了参数,这时候我们需要这么来获取这个参数: +* 先导出模块`ActivatedRoute`和`Location`: +```js +// detail.component.ts +import { ActivatedRoute } from '@angular/router'; +import { Location } from '@angular/common'; +``` +* 再注入到构造函数中,并将值作为私有变量: +```js +// detail.component.ts +export class DetailComponent implements OnInit { + constructor( + private route: ActivatedRoute, + private location: Location + ) { } + ngOnInit() {} +} +``` +**知识点:** +`ActivatedRoute` 保存该 `DetailComponent` 实例的路由信息。可以从这个组件获取URL中的路由参数和其他数据。 +`Location` 是一个 `Angular` 的服务,用来与浏览器打交道。后续会使用它来导航回上一个视图。 + +* 提取路由参数: + +这里声明`getDetail`方法,提取路由参数,并`ngOnInit`**生命周期钩子方法**在中执行。 +```js +// detail.component.ts +ngOnInit() { + this.getDetail() +} +getDetail(): void{ + const id = +this.route.snapshot.paramMap.get('id'); + console.log(`此课本的id是${id}`) +} +``` +**知识点**: +`route.snapshot` 是一个路由信息的**静态快照**,抓取自组件刚刚创建完毕之后。 +`paramMap` 是一个URL中路由所携带的参数值的对象。"id"对应的值就是要获取的书本的 id。 +**注意**: +路由参数总会是字符串。这里我们使用 (+) 操作符,将字符串转换成数字。 + +现在在浏览器上刷新下页面,再点击 **打开书本详情** 按钮,可以看到控制台输出了` 此课本的id是1 `的结果。 +到这一步,我们算是把路由配置完成了,接下来可以开始做页面的逻辑了。 + +**本部分内容到这结束** + +|Author|王平安| +|---|---| +|E-mail|pingan8787@qq.com| +|博 客|www.pingan8787.com| +|微 信|pingan8787| +|每日文章推荐|https://github.com/pingan8787/Leo_Reading/issues| +|JS小册|js.pingan8787.com| +|微信公众号|前端自习课| + + +![前端自习课](https://camo.githubusercontent.com/7d890fb10cccf99c03dcf144e0e290357195ac44/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031392f322f31362f313638663439663032333831393163613f773d3130373826683d36343726663d706e6726733d323832353135) diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/angular_books_2.md" "b/Cute-Angular/books\351\241\271\347\233\256demo/angular_books_2.md" new file mode 100644 index 00000000..1ab75c51 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/angular_books_2.md" @@ -0,0 +1,227 @@ +## 本文目录 +* 一、[项目起步](https://juejin.im/post/5c70ae586fb9a049c64476a0) +* 二、[编写路由组件](https://juejin.im/post/5c70ae586fb9a049c64476a0) +* 三、**编写页面组件** + * 1.**编写单一组件** + * 2.**模拟数据** + * 3.**编写主从组件** +* 四、编写服务 + * 1.为什么需要服务 + * 2.编写服务 +* 五、引入RxJS + * 1.关于RxJS + * 2.引入RxJS + * 3.改造数据获取方式 +* 六、改造组件 + * 1.添加历史记录组件 + * 2.添加和删除历史记录 +* 七、HTTP改造 + * 1.引入HTTP + * 2.通过HTTP请求数据 + * 3.通过HTTP修改数据 + * 4.通过HTTP增加数据 + * 5.通过HTTP删除数据 + * 6.通过HTTP查找数据 + + +## 三、编写页面组件 +接下来开始编写页面组件,这里我们挑重点来写,一些布局的样式,后面可以看源码。 + +### 1.编写单一组件 +我们首先写一个书本信息的组件,代码如下: +```html + +
+
+ +
+ + +
程姬
+
+
+
+``` +**知识点**: +`*ngFor` 是一个 Angular 的复写器(repeater)指令,就像**angular1**中的`ng-for`和**vuejs**中的`v-for`。 它会为列表中的每项数据复写它的宿主元素。 +这时候可以看到页面变成下面这个样子: +![图片3-1](http://images.pingan8787.com/angular_books_3_1.png) + + +接下来我们要把写死在HTML上面的数据,抽到JS中: + +现在先新建一个`books.ts`文件来定义一个`Book`类,并添加`id`,`url`,`title`和`author`四个属性: +```js +// src/app/books.ts +export class Book { + id: number; + url: string; + title: string; + author: string; +} +``` +然后回到`index.component.ts`文件去引入它,并定义一个`books`属性,使用导入进来的`Book`类作为类型: +```js +// index.component.ts +import { Book } from '../books'; +export class IndexComponent implements OnInit { + books: Book = { + id: 1, + url: 'https://img3.doubanio.com/view/subject/m/public/s29988481.jpg', + title: '像火焰像灰烬', + author: '程姬', + } +} +``` +然后再改造前面的组件文件`index.component.html`: +```html + +
+ {{books.id}} + +
{{books.author}}
+
+``` + +接着,我们再为每个课本添加一个点击事件,来实现点击封面图能查看大图的效果,现在`index.component.ts`中定义一个`getDetailImage`方法,并在`index.component.html`中绑定该方法: +```js +// index.component.ts +export class IndexComponent implements OnInit { + getDetailImage(books){ + alert(`正在查看id为${books.id}的大图!`); + } +} +``` +这边方法的具体实现,不写,不是本文重点。下面是增加点击事件的绑定: +```html + +{{books.id}} +``` +**知识点**: +`(click)`是Angular用来绑定事件,它会让 Angular 监听这个` ` 元素的 `click` 事件。 当用户点击 `` 时,Angular 就会执行表达式 `getDetailImage(books)`。 + +再来,我们引入前面学到的**路由链接**指令来改造HTML: +```html + +{{books.title}} +``` +这时候,我们在点击书本的标题,发现页面跳转到URL地址为`http://localhost:4200/detail/1`的页面,这就说明,我们页面的路由跳转也成功了~ + +改造完成后,可以看到,页面显示的还是一样,接下来我们先这样放着,因为我们后面会进行数据模拟,和模拟服务器请求。 + +我们就这样写好第一个单一组件,并且数据是从JS中读取的。 + +### 2.模拟数据 +这时候为了方便后面数据渲染,我们这里需要模拟一些本地数据,我们创建一个本地` mock-books.ts`文件来存放模拟的数据: +```js +// app/mock-books.ts +import { Books } from './books'; +export const BookList: Books[] = [ + { + id: 1, + url: 'https://img3.doubanio.com/view/subject/m/public/s29988481.jpg', + title: '像火焰像灰烬', + author: '程姬', + }, + // 省略其他9条 +] +``` +然后在`index.component.ts`中导入模拟的数据,并将原有的`books`值修改成导入的模拟数据`BookList`: +```js +// index.component.ts +import { BookList } from '../mock-books'; +books = BookList; +``` +并将原本的`*ngFor`中修改成这样,绑定真正的数据: +```html + +
+ {{item.id}} + +
{{item.author}}
+
+``` + +### 3.编写主从组件 +当我们写完一个单一组件后,我们会发现,如果我们把每个组件都写到同一个HTML文件中,这是很糟糕的事情,这样做有缺点: +* 代码复用性差;(导致每次相同功能要重新写) +* 代码难维护;(因为一个文件会非常长) +* 影响性能;(打开每个页面都要重复加载很多) + +为了解决这个问题,我们这里就要开始使用真正的**组件化思维**,将通用常用组件抽离出来,通过参数传递来控制组件的不同业务形态。 +这便是我们接下来要写的主从组件。 + +思考一下,我们这里现在能抽成组件作为公共代码的,就是这个单个书本的内容,因为每个书本的内容都一致,只是里面数据的差异,于是我们再新建一个组件: +```sh +ng g component books +``` +并将前面`index.component.html`中关于课本的代码剪切到`books.component.html`中来,然后删除掉`*ngFor`的内容,并将原本本地的变量`books`替换成`list`,这个变量我们等会会取到: +```html + +
+ {{list.id}} + +
{{list.author}}
+
+``` +再将这个组件,引用到它的父组件中,这里是要引用到`index.component.html`的组件中,并将前面的`*ngFor`再次传入``: +```html +
+
+ +
+
+``` + +接下来要做的就是获取到`list`变量的值,显然这个值是要从外面组件传进来的,我们需要在`books.component.ts`引入前面定义的 `Books`类 和 `@Input() 装饰器`,还要添加一个带有 `@Input() 装饰器`的 `list` 属性,另外还要记得将`getDetailImage`方法也剪切过来: +```js +// books.component.ts +import { Component, OnInit, Input } from '@angular/core'; +import { Books } from '../books'; + +export class BooksComponent implements OnInit { + @Input() list: Books; + constructor() { } + ngOnInit() {} + getDetailImage(books){ + alert(`正在查看id为${books.id}的大图!`); + } +} +``` +`@Input() 装饰器`介绍具体可以查看 [手册](https://angular.cn/guide/template-syntax#inputs-outputs) + +我们要获取的 `list` 属性必须是一个带有` @Input() `装饰器的输入属性,因为外部的 `IndexComponent` 组件将会绑定到它。就像这样: +```html + +``` + +**知识点**: +`[list]="item"` 是 `Angular` 的**属性绑定**语法。这是一种**单向数据绑定**。从 `IndexComponent` 的 `item` 属性绑定到目标元素的 `list` 属性,并映射到了 `BooksComponent` 的 `list` 属性。 + +做到这里,我们已经将`BooksComponent`作为`IndexComponent`的子组件来引用了,在实际开发过程中,这样的父子组件关系,会用的非常多。 + +写到这里,看看我们项目,还是一样正常在运行,只是现在项目中组件分工更加明确了。 + +现在的效果图: +![图片3-2](http://images.pingan8787.com/angular_books_3_2.png) + + +**本部分内容到这结束** + +|Author|王平安| +|---|---| +|E-mail|pingan8787@qq.com| +|博 客|www.pingan8787.com| +|微 信|pingan8787| +|每日文章推荐|https://github.com/pingan8787/Leo_Reading/issues| +|JS小册|js.pingan8787.com| +|微信公众号|前端自习课| + + +![前端自习课](https://camo.githubusercontent.com/7d890fb10cccf99c03dcf144e0e290357195ac44/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031392f322f31362f313638663439663032333831393163613f773d3130373826683d36343726663d706e6726733d323832353135) diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/angular_books_3.md" "b/Cute-Angular/books\351\241\271\347\233\256demo/angular_books_3.md" new file mode 100644 index 00000000..b98ff92e --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/angular_books_3.md" @@ -0,0 +1,251 @@ +## 本文目录 +* 一、[项目起步](https://juejin.im/post/5c70ae586fb9a049c64476a0) +* 二、[编写路由组件](https://juejin.im/post/5c70ae586fb9a049c64476a0) +* 三、[编写页面组件](https://juejin.im/post/5c70b48d6fb9a04a0b22cbce) + * 1.[编写单一组件](https://juejin.im/post/5c70b48d6fb9a04a0b22cbce) + * 2.[模拟数据](https://juejin.im/post/5c70b48d6fb9a04a0b22cbce) + * 3.[编写主从组件](https://juejin.im/post/5c70b48d6fb9a04a0b22cbce) +* 四、**编写服务** + * 1.**为什么需要服务** + * 2.**编写服务** +* 五、**引入RxJS** + * 1.**关于RxJS** + * 2.**引入RxJS** + * 3.**改造数据获取方式** +* 六、改造组件 + * 1.添加历史记录组件 + * 2.添加和删除历史记录 +* 七、HTTP改造 + * 1.引入HTTP + * 2.通过HTTP请求数据 + * 3.通过HTTP修改数据 + * 4.通过HTTP增加数据 + * 5.通过HTTP删除数据 + * 6.通过HTTP查找数据 + +## 四、编写服务 +截止到这部分,我们的`BooksComponent`组件获取和显示的都是本地模拟的数据。 +接下来我们要开始对这些进行重构,让聚焦于为它的视图提供支持,这也让它更容易使用模拟服务进行单元测试。 + +### 1.为什么需要服务 +我们不应该让组件来直接获取或保存数据,它们应该聚焦于展示数据,而数据访问的工作交给其他服务来做。 +这里我们需要创建一个名为`BooksService`的服务,让我们应用中所有的类都使用它来获取书本列表的数据,使用的时候,只需要将它通过Angular的**依赖注入机制**注入到需要用的组件的构造函数中。 + +**知识点:** +服务可以实现多个不同组件之间信息共享,后面我们还会将它注入到两个地方: +`BooksService`中,使用该服务发送消息。 +`IndexService`中,使用该服务来展示消息。 + +接下来我们使用命令行,创建`BooksService `: +```sh +ng g service books +``` +在生成的`books.service.ts`文件中: +```js +// books.service.ts +import { Injectable } from '@angular/core'; +@Injectable({ + providedIn: 'root' +}) +``` +新导入了`@Injectable`装饰器,是为了让`BooksService`提供一个可注入的服务,并且它还可以拥有自己的待注入的依赖,简单理解就是**如果你的服务需要依赖,那么你就需要导入它**。 +并且它接收该服务的元数据对象。 + +### 2.编写服务 +接下来我们开始编写`books.service.ts`服务。 + +* 导入服务所需组件 + +这里我们导入`Books`和`BookList`,并添加一个`getBooks`方法来返回所有书本的数据,并且还需要添加一个`getBooks`方法来返回指定id的书本信息: +```js +// index.component.ts +import { Books } from './books'; +import { BookList } from './mock-books'; +@Injectable({ + providedIn: 'root' +}) +export class BooksService { + constructor() { } + getBookList(): Books[] { + return BookList; + } + getBook(id: number): Books{ + return BookList.find(book => book.id === id) + } +} +``` +在我们使用这个服务之前,需要先注册该服务,因为我们在使用`ng g service books`命令创建服务时,CLI已经默认为我们添加了注册了,这是方法就是上面代码中的: +```js +providedIn: 'root' +``` +表示将我们的服务注册在**根注入器**上,这样我们就可以把这个服务注入到任何享用的类上了。 + +* 修改`IndexComponent` + +先删除`BookList`的引入,并修改`books`属性的定义: + +```js +// index.component.ts +import { BooksService } from '../books.service'; +export class IndexComponent implements OnInit { + books : Books[]; + ngOnInit() {} +} +``` +然后注入我们的`BooksService`服务,需要先往构造函数中添加一个私有的`booksservice`,使用注入的`BooksService`作为类型,理解成一个注入点: +```js +// index.component.ts +constructor(private booksservice: BooksService) { } +``` + +之后我们需要添加一个`getBooks`方法来获取这些书本数据,并在生命周期函数`ngOnInit`中调用: +```js +export class IndexComponent implements OnInit { + ngOnInit() { + this.getBooks(); + } + getBooks(): void{ + this.books = this.booksservice.getBookList(); + } +} +``` + +* 修改`DetailComponent` +我们先改造书本详情页的HTML结构: +```html + +
+

《{{books.title}}》介绍

+
+ +
+

书本标题: {{books.title}}

+

书本作者: {{books.author}}

+

书本id: {{books.id}}

+
+
+

暂无信息

+
+``` +**知识点**: +这里使用了`*ngIf`指令,当条件为`true`则显示其HTML内容。 + +```js +// detail.component.ts +import { Books } from '../books'; +import { BooksService } from '../books.service'; +export class DetailComponent implements OnInit { + constructor( + private route: ActivatedRoute, + private location: Location, + private booksservice: BooksService // 引入BooksService服务 + ) { } + + books: Books; // 定义books类型 + ngOnInit() { + this.getDetail() + } + getDetail(): void{ + const id = +this.route.snapshot.paramMap.get('id'); + this.getBooks(id); + } + getBooks(id: number): void { + this.books = this.booksservice.getBook(id); + } +} +``` +这段代码,主要定义了`getBooks`方法,当刚进入页面时,将书本`id`传入`getBooks`方法,去`BooksService`去获取对应id的书本信息,并复制给变量`books`,然后展示到页面。 + +改造之后,我们的页面显示依旧正常。 + +![图片3-2](http://images.pingan8787.com/angular_books_3_2.png) + +但是我们要知道,这背后的逻辑已经改变了。 + +## 五、引入RxJS改造项目 +### 1.关于RxJS +这里简单介绍关键概念,具体可以查看 [RxJS 官网](https://RxJS.dev/),也可以参考 [浅析Angular之RxJS](https://www.jianshu.com/p/36d85f8cafdd)。 + +#### 什么是RxJS + +RxJS全称`Reactive Extensions for JavaScript`,中文意思: JavaScript的响应式扩展。 +RxJS主要是提供一种更加强大和优雅的方式,来利用响应式编程的模式,实现JavaScript的异步编程。 + +#### RxJS优点 + +* 纯净性; +* 流动性; + +#### RxJS核心概念 + +RxJS 是基于观察者模式和迭代器模式以函数式编程思维来实现的。RxJS 中含有两个基本概念:`Observables` 与 `Observer`。 +`Observables` 作为被观察者,是一个值或事件的流集合;而 `Observer` 则作为观察者,根据 `Observables` 进行处理。它们之间的订阅发布关系(观察者模式) 如下: +**订阅**:`Observer` 通过 `Observable` 提供的 `subscribe()` 方法订阅 `Observable`。 +**发布**:`Observable` 通过回调 `next` 方法向 `Observer` 发布事件。 + +———— 来源[Angular修仙之路 RxJS Observable](http://www.semlinker.com/rxjs-observable/) + +另外这里列出来一些核心,具体还是看官网咯,并且下面使用到的时候会具体介绍。 +* `Observable` (可观察对象): 表示一个概念,这个概念是一个可调用的未来值或事件的集合。 +* `Observer`(观察者): 一个回调函数的集合,它知道如何去监听由 `Observable` 提供的值。 +* `Subscription` (订阅): 表示 `Observable` 的执行,主要用于取消 `Observable` 的执行。 +* `Operators` (操作符): 采用函数式编程风格的纯函数 (`pure function`),使用像 `map`、`filter`、`concat`、`flatMap` 等这样的操作符来处理集合。 +* `Subject` (主体): 相当于 `EventEmitter`,并且是将值或事件多路推送给多个 `Observer` 的唯一方式。 +* `Schedulers` (调度器): 用来控制并发并且是中央集权的调度员,允许我们在发生计算时进行协调,例如 `setTimeout` 或` requestAnimationFrame `或其他。 + +### 2.引入RxJS +在我们的真实应用中,我们必须要等到服务器响应后,我们才能获取到数据,因此这天生就需要用异步思维来操作。 + +由于Angular中已经自带RxJS,所以我们只要在需要使用的时候,引入即可使用: + +### 3.改造数据获取方式 +了解完RxJS的一些概念后,我们开始改造下这些书本的数据获取方式。 + +* 改造`BooksService` + +首先我们从 RxJS 中导入 `Observable` 和 `of` 符号: +```js +// books.service.ts +import { Observable, of } from 'rxjs'; +``` +**知识点**: +`Observable`: 观察者模式中的观察者,具体可以参考 [Angular修仙之路 RxJS Observable](http://www.semlinker.com/rxjs-observable/) +`of`: 用来获取观察者拿到的数据,通常是一个`Observable`。 + +然后修改`getBookList`方法 +```js +// books.service.ts +getBookList(): Observable { + return of(BookList); +} +``` +这里 `of(BookList)` 返回一个` Observable`,它会发出单个值,这个值就是这些模拟书本的数组。 + +* 改造`IndexComponent` + +这里也要修改`getBooks`方法,使用`subscribe`去订阅服务返回回来的值: +```js +// index.component.ts +getBooks(): void{ + this.booksservice.getBookList() + .subscribe(books => this.books = books); +} +``` +由于原本直接赋值数据,在实际场景中是不可能这样同步的,所以这里`subscribe`函数,会在`Observable`发出数据以后,再把书本列表传到里面的回调函数,再复制给`books`属性。 +使用这种异步方式,当 `BooksService` 从远端服务器获取英雄数据时,不用担心还没拿到数据就执行后面。 + +下一步,我们就要改造一下项目了。 + +**本部分内容到这结束** + +|Author|王平安| +|---|---| +|E-mail|pingan8787@qq.com| +|博 客|www.pingan8787.com| +|微 信|pingan8787| +|每日文章推荐|https://github.com/pingan8787/Leo_Reading/issues| +|JS小册|js.pingan8787.com| +|微信公众号|前端自习课| + + +![前端自习课](https://camo.githubusercontent.com/7d890fb10cccf99c03dcf144e0e290357195ac44/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031392f322f31362f313638663439663032333831393163613f773d3130373826683d36343726663d706e6726733d323832353135) diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/angular_books_4.md" "b/Cute-Angular/books\351\241\271\347\233\256demo/angular_books_4.md" new file mode 100644 index 00000000..f99493dd --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/angular_books_4.md" @@ -0,0 +1,562 @@ +## 本文目录 +* 一、[项目起步](https://juejin.im/post/5c70ae586fb9a049c64476a0) +* 二、[编写路由组件](https://juejin.im/post/5c70ae586fb9a049c64476a0) +* 三、[编写页面组件](https://juejin.im/post/5c70b48d6fb9a04a0b22cbce) + * 1.[编写单一组件](https://juejin.im/post/5c70b48d6fb9a04a0b22cbce) + * 2.[模拟数据](https://juejin.im/post/5c70b48d6fb9a04a0b22cbce) + * 3.[编写主从组件](https://juejin.im/post/5c70b48d6fb9a04a0b22cbce) +* 四、[编写服务](https://juejin.im/post/5c70b5486fb9a049d7484fdf) + * 1.[为什么需要服务](https://juejin.im/post/5c70b5486fb9a049d7484fdf) + * 2.[编写服务](https://juejin.im/post/5c70b5486fb9a049d7484fdf) +* 五、[引入RxJS](https://juejin.im/post/5c70b5486fb9a049d7484fdf) + * 1.[关于RxJS](https://juejin.im/post/5c70b5486fb9a049d7484fdf) + * 2.[引入RxJS](https://juejin.im/post/5c70b5486fb9a049d7484fdf) + * 3.[改造数据获取方式](https://juejin.im/post/5c70b5486fb9a049d7484fdf) +* 六、**改造组件** + * 1.**添加历史记录组件** + * 2.**添加和删除历史记录** +* 七、**HTTP改造** + * 1.**引入HTTP** + * 2.**通过HTTP请求数据** + * 3.**通过HTTP修改数据** + * 4.**通过HTTP增加数据** + * 5.**通过HTTP删除数据** + * 6.**通过HTTP查找数据** + + +## 六、改造组件 +从这里开始,我们要使用RxJS来改造组件和添加新功能了,让整个项目更加完善。 + +### 1.添加历史记录组件 + +* 创建`HistoryComponent`组件 +```sh +ng g component hostory +``` +然后在`app.component.html`文件夹中添加组件: +```html + + +``` + +### 2.添加增删改查功能 + +这里我们要开始做书本的增删改查功能,需要先创建一个`HistoryService`服务,方便我们实现这几个功能: + +* 创建`HistoryService`服务 +```sh +ng g service history +``` +然后在生成的ts文件中,增加`add`和`clear`方法,`add`方法用来添加历史记录到`history`数组中,`clear`方法则是清空`history`数组: +```js +// history.service.ts +export class HistoryService { + history: string[] = []; + add(history: string){ + this.history.push(history); + } + clear(){ + this.history = []; + } +} +``` + +* 使用`HistoryService`服务 + +在将这个服务,注入到`BooksService`中,并改造`getBooks`方法: +```js +// books.service.ts +import { HistoryService } from './history.service'; +constructor( + private historyservice: HistoryService +) { } +getBooks(): void{ + this.historyservice.add('请求书本数据') + this.booksservice.getBookList() + .subscribe(books => this.books = books); +} +``` +也可以用相同方法,在`IndexComponent`中添加`访问首页书本列表`的记录。 +```js +// index.component.ts +import { HistoryService } from '../history.service'; +constructor( + private booksservice: BooksService, + private historyservice: HistoryService +) { } +getBooks(): void{ + this.historyservice.add('访问首页书本列表'); + this.booksservice.getBookList() + .subscribe(books => this.books = books); +} +``` + +接下来,将我们的`HistoryService`注入到`HistoryComponent`中,然后才能将历史数据显示到页面上: +```js +// history.component.ts +import { HistoryService } from '../history.service'; +export class HistoryComponent implements OnInit { + constructor(private historyservice: HistoryService) { } + ngOnInit() {} +} +``` +```html + +
+

操作历史:

+
+ +
{{item}}
+
+
+``` +**代码解释**: +`*ngIf="historyservice.history.length"`,是为了防止还没有拿到历史数据,导致后面的报错。 +`(click)="historyservice.clear()"`, 绑定我们服务中的`clear`事件,实现清除缓存。 +`*ngFor="let item of historyservice.history"`,将我们的历史数据渲染到页面上。 + + +到了这一步,就能看到历史数据了,每次也换到首页,都会增加一条。 + +![图片5-1](http://images.pingan8787.com/angular_books_5_1.png) + +接下来,我们要在书本详情页也加上历史记录的统计,导入文件,注入服务,然后改造`getBooks`方法,实现历史记录的统计: +```js +// detail.component.ts +import { HistoryService } from '../history.service'; + +export class DetailComponent implements OnInit { + constructor( + private route: ActivatedRoute, + private location: Location, + private booksservice: BooksService, + private historyservice: HistoryService + ) { } + //... + getBooks(id: number): void { + this.books = this.booksservice.getBook(id); + this.historyservice.add(`查看书本${this.books.title},id为${this.books.id}`); + console.log(this.books) + } +} +``` +![图片5-2](http://images.pingan8787.com/angular_books_5_2.png) + +这时候就可以在历史记录中,看到这些操作的记录了,并且**清除**按钮也正常使用。 + +## 七、HTTP改造 +原本我只想写到上一章,但是想到,我们实际开发中,哪有什么本地数据,基本上数据都是要从服务端去请求,所以这边也有必要引入这一张,模拟实际的HTTP请求。 + +### 1.引入HTTP +在这一章,我们使用Angular提供的 `HttpClient` 来添加一些数据持久化特性。 +然后实现对书本数据进行**获取,增加,修改,删除和查找**功能。 + +`HttpClient`是Angular通过 HTTP 与远程服务器通讯的机制。 + +这里我们为了让`HttpClient`在整个应用全局使用,所以将`HttpClient`导入到根模块`app.module.ts`中,然后把它加入 `@NgModule.imports` 数组: +```js +import { HttpClientModule } from '@angular/common/http'; +@NgModule({ + //... + imports: [ + BrowserModule, + AppRoutingModule, + HttpClientModule + ], + //... +}) +``` + +这边我们使用 [内存 Web API(In-memory Web API) ](https://github.com/angular/in-memory-web-api)模拟出的远程数据服务器通讯。 +**注意:** 这个内存 Web API 模块与 Angular 中的 HTTP 模块无关。 + +通过下面命令来安装: +```sh +npm install angular-in-memory-web-api --save +``` +然后在`app.module.ts`中导入 `HttpClientInMemoryWebApiModule` 和 `InMemoryDataService` 类(后面创建): +```js +// app.module.ts +import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api'; +import { InMemoryDataService } from './in-memory-data.service'; +@NgModule({ + // ... + imports: [ + // ... + HttpClientInMemoryWebApiModule.forRoot( + InMemoryDataService, {dataEncapsulation:false} + ) + ], + // ... +}) +export class AppModule { } +``` +**知识点:** +`forRoot()` 配置方法接受一个 InMemoryDataService 类(初期的内存数据库)作为参数。 + +然后我们要创建`InMemoryDataService`类: +```sh +ng g service InMemoryData +``` +并将生成的`in-memory-data.service.ts`修改为: +```js +// in-memory-data.service.ts +import { Injectable } from '@angular/core'; +import { InMemoryDbService } from 'angular-in-memory-web-api'; +import { Books } from './books'; +@Injectable({ + providedIn: 'root' +}) +export class InMemoryDataService implements InMemoryDbService { + createDb(){ + const books = [ + { + id: 1, + url: 'https://img3.doubanio.com/view/subject/m/public/s29988481.jpg', + title: '像火焰像灰烬', + author: '程姬', + }, + // 省略其他9条数据 + ]; + return {books}; + } + constructor() { } +} +``` + +这里先总结`InMemoryDbService`所提供的RESTful API,后面都要用到: +例如如果`url`是`api/books`,那么 +* 查询所有成员:以**GET**方法访问`api/books` +* 查询某个成员:以**GET**方法访问`api/books/id`,比如`id`是`1`,那么访问`api/books/1` +* 更新某个成员:以**PUT**方法访问`api/books/id` +* 删除某个成员:以**DELETE**方法访问`api/books/id` +* 增加一个成员:以**POST**方法访问`api/books` + + +### 2.通过HTTP请求数据 + +现在要为接下来的网络请求做一些准备,先在`books.service.ts`中引入HTTP符号,然后注入`HttpClient`并改造: +```js +// books.service.ts +import { HttpClient, HttpHeaders} from '@angular/common/http'; +// ... +export class BooksService { + constructor( + private historyservice: HistoryService, + private http: HttpClient + ) { } + private log(histories: string){ + this.historyservice.add(`正在执行:${histories}`) + } + private booksUrl = 'api/books'; // 提供一个API供调用 + // ... +} +``` +这里我们还新增一个私有方法`log`和一个私有变量`booksUrl`。 + +接下来我们要开始发起http请求数据,开始改造`getBookList`方法: +```js +// books.service.ts +// ... +getBookList(): Observable { + this.historyservice.add('请求书本数据') + return this.http.get(this.booksUrl); +} +// ... +``` +这里我们使用 `http.get` 替换了 `of`,其它没修改,但是应用仍然在正常工作,这是因为这两个函数都返回了 `Observable`。 + +实际开发中,我们还需要考虑到**请求的错误处理**,要捕获错误,我们就要使用 RxJS 的 `catchError()` 操作符来建立对 Observable 结果的处理管道(pipe)。 + +我们引入`catchError `并改造原本`getBookList`方法: + +```js +// books.service.ts +getBookList(): Observable { + this.historyservice.add('请求书本数据') + return this.http.get(this.booksUrl).pipe( + catchError(this.handleError('getHeroes', [])) + ); +} +private handleError (operation = 'operation', result?: T) { + return (error: any): Observable => { + this.log(`${operation} 失败: ${error.message}`); // 发出错误通知 + return of(result as T); // 返回空结果避免程序出错 + }; +} +``` +**知识点**: +`.pipe()` 方法用来扩展 `Observable` 的结果。 +`catchError()` 操作符会拦截失败的 Observable。并把错误对象传给错误处理器,错误处理器会处理这个错误。 +`handleError()` 错误处理函数做了两件事,发出错误通知和返回空结果避免程序出错。 + +这里还需要使用`tap`操作符改造`getBookList`方法,来窥探`Observable`数据流,它会查看`Observable`的值,然后我们使用`log`方法,记录一条历史记录。 +`tap` 回调不会改变这些值本身。 +```js +// books.service.ts +getBookList(): Observable { + return this.http.get(this.booksUrl) + .pipe( + tap( _ => this.log('请求书本数据')), + catchError(this.handleError('getHeroes', [])) + ); +} +``` + +### 3.通过HTTP修改数据 +这里我们需要在原来`DetailComponent`上面,添加一个输入框、保存按钮和返回按钮,就像这样: +```html + + +
+

修改信息:

+ + + +
+``` +这边切记一点,一定要在`app.module.ts`中引入 `FormsModule`模块,并在`@NgModule`的`imports`中引入,不然要报错了。 +```js +// app.module.ts +// ... +import { FormsModule } from '@angular/forms'; +@NgModule({ + // ... + imports: [ + // ... + FormsModule + ], + // ... +}) +``` +`input`框绑定书本的标题`books.title`,而保存按钮绑定一个`save()`方法,这里还要实现这个方法: +```js +// detail.component.ts +save(): void { + this.historyservice.updateBooks(this.books) + .subscribe(() => this.goBack()); +} +goBack(): void { + this.location.back(); +} +``` +这里通过调用`BooksService`的`updateBooks`方法,将当前修改后的书本信息修改到源数据中,这里我们需要去`books.service.ts`中添加`updateBooks`方法: +```js +// books.service.ts +// ... +updateBooks(books: Books): Observable{ + return this.http.put(this.booksUrl, books, httpOptions).pipe( + tap(_ => this.log(`修改书本的id是${books.id}`)), + catchError(this.handleError(`getBooks请求是id为${books.id}`)) + ) +} +// ... +``` +**知识点**: +`HttpClient.put()` 方法接受三个参数:`URL 地址`、`要修改的数据`和`其他选项`。 +`httpOptions` 常量需要定义在`@Injectable`修饰器之前。 + +现在,我们点击首页,选择一本书进入详情,修改标题然后保存,会发现,首页上这本书的名称也会跟着改变呢。这算是好了。 + + +### 4.通过HTTP增加数据 +我们可以新增一个页面,并添加上路由和按钮: +```sh +ng g component add +``` +添加路由: +```js +// app-routing.module.ts +// ... +import { AddComponent } from './add/add.component'; + +const routes: Routes = [ + { path: '', redirectTo:'/index', pathMatch:'full' }, + { path: 'index', component: IndexComponent}, + { path: 'detail/:id', component: DetailComponent}, + { path: 'add', component: AddComponent}, +] +``` +添加路由入口: +```html + + +添加书本 +``` +编辑添加书本的页面: +```html + +
+

添加书本:

+ + + + +
+
+``` +初始化添加书本的数据: +```js +// add.component.ts +// ... +import { Books } from '../books'; +import { BooksService } from '../books.service'; +import { HistoryService } from '../history.service'; +import { Location } from '@angular/common'; +export class AddComponent implements OnInit { + books: Books = { + id: 0, + url: '', + title: '', + author: '' + } + constructor( + private location: Location, + private booksservice: BooksService, + private historyservice: HistoryService + ) { } + ngOnInit() {} + add(books: Books): void{ + books.title = books.title.trim(); + books.author = books.author.trim(); + this.booksservice.addBooks(books) + .subscribe( book => { + this.historyservice.add(`新增书本${books.title},id为${books.id}`); + this.location.back(); + }); + } +} +``` +然后在`books.service.ts`中添加`addBooks`方法,来添加一本书本的数据: +```js +// books.service.ts +addBooks(books: Books): Observable{ + return this.http.post(this.booksUrl, books, httpOptions).pipe( + tap((newBook: Books) => this.log(`新增书本的id为${newBook.id}`)), + catchError(this.handleError('添加新书')) + ); +} +``` + + +现在就可以正常添加书本啦。 + +![图片5-3](http://images.pingan8787.com/angular_books_5_3.png) + + +### 5.通过HTTP删除数据 +这里我们先为每个书本后面添加一个删除按钮,并绑定删除事件`delete`: +```html + + +X +``` +```js +// books.component.ts +import { BooksService } from '../books.service'; +export class BooksComponent implements OnInit { + @Input() list: Books; + constructor( + private booksservice: BooksService + ) { } + // ... + delete(books: Books): void { + this.booksservice.deleteBooks(books) + .subscribe(); + } +} +``` +然后还要再`books.service.ts`中添加`deleteBooks`方法来删除: +```js +// books.service.ts +deleteBooks(books: Books): Observable{ + const id = books.id; + const url = `${this.booksUrl}/${id}`; + return this.http.delete(url, httpOptions).pipe( + tap(_ => this.log(`删除书本${books.title},id为${books.id}`)), + catchError(this.handleError('删除书本')) + ); +} +``` +这里需要在删除书本结束后,通知`IndexComponent`将数据列表中的这条数据删除,这里还需要再了解一下[Angular 父子组件数据通信](https://blog.csdn.net/u010730126/article/details/68080139)。 +然后我们在父组件`IndexComponent`上添加`change`事件监听,并传入本地的`funChange`: +```html + + +``` +在对应的`index.component.ts`中添加`funChange`方法: +```js +// index.component.ts +funChange(books, $event){ + this.books = this.books.filter(h => h.id !== books.id); +} +``` + +再来,我们在子组件`BooksComponent`上多导入`Output`和`EventEmitter`,并添加`@Output()`修饰器和调用`emit`: +```js +import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; +export class BooksComponent implements OnInit { + // ... + @Output() + change = new EventEmitter() + // ... + delete(books: Books): void { + this.booksservice.deleteBooks(books) + .subscribe(()=>{ + this.change.emit(books); + }); + } +} +``` +这样就实现了我们父子组件之间的事件传递啦,现在我们的页面还是正常运行,并且删除一条数据后,页面数据会更新。 + + +### 6.通过HTTP查找数据 +还是在`books.service.ts`,我们添加一个方法`getBooks`,来实现通过ID来查找指定书本,因为我们是通过ID查找,所以返回的是单个数据,这里就是`Observable`类型: +```js +// books.service.ts +getBooks(id: number): Observable{ + const url = `${this.booksUrl}/${id}`; + return this.http.get(url).pipe( + tap( _ => this.log(`请求书本的id为${id}`)), + catchError(this.handleError(`getBooks请求是id为${id}`)) + ) +} +``` +注意,这里 `getBooks` 会返回 `Observable`,是一个可观察的单个对象,而不是一个可观察的对象数组。 + + +## 八、结语 +这个项目其实很简单,但是我还是一步一步的写下来,一方面让自己更熟悉Angular,另一方面也是希望能帮助到更多朋友哈~ +最终效果: + +![图片结果](http://images.pingan8787.com/angular_books_result.png) + + +**本部分内容到这结束** + +|Author|王平安| +|---|---| +|E-mail|pingan8787@qq.com| +|博 客|www.pingan8787.com| +|微 信|pingan8787| +|每日文章推荐|https://github.com/pingan8787/Leo_Reading/issues| +|JS小册|js.pingan8787.com| +|微信公众号|前端自习课| + + +![前端自习课](https://camo.githubusercontent.com/7d890fb10cccf99c03dcf144e0e290357195ac44/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031392f322f31362f313638663439663032333831393163613f773d3130373826683d36343726663d706e6726733d323832353135) diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/angular_books_all.md" "b/Cute-Angular/books\351\241\271\347\233\256demo/angular_books_all.md" new file mode 100644 index 00000000..d9de7974 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/angular_books_all.md" @@ -0,0 +1,1197 @@ +* 零、Angular安装 +* 一、项目起步 +* 二、编写路由组件 +* 三、编写页面组件 + * 1.编写单一组件 + * 2.模拟数据 + * 3.编写主从组件 +* 四、编写服务 + * 1.为什么需要服务 + * 2.编写服务 +* 五、引入RxJS改造项目 + * 1.关于RxJS + * 2.引入RxJS + * 3.改造数据获取方式 +* 六、改造组件 + * 1.添加历史记录组件 + * 2.添加增删改查功能 +* 七、HTTP改造 + * 1.引入HTTP + * 2.通过HTTP请求数据 + * 3.通过HTTP修改数据 + * 4.通过HTTP增加数据 + * 5.通过HTTP删除数据 + * 6.通过HTTP查找数据 +* 八、结语 + + +这个入门项目是我学习完[Angular 英雄指南教程](https://angular.cn/tutorial)后,自己手写的一个练习项目,一步一步来,最终的[项目源码可以这里查看](https://github.com/pingan8787/Leo-JavaScript/blob/master/Cute-Angular/books%E9%A1%B9%E7%9B%AEdemo/books_angular),大佬们请指点啦。 + +推荐两个Angular学习网站: +1. [Angular 中文网](https://angular.cn/) +2. [Angular 修仙之路](http://www.semlinker.com/) + +还有呢,我没怎么关注到样式,所以样式会有点丑,主要都放在核心逻辑中了。 +**最终实现:** +* 首页书本列表数据展示 +* 各个页面静态/动态路由跳转 +* 本地模拟数据服务 +* 书本数据的增删改查 +* 父子组件通信 +* 常用指令使用和介绍 + +![图片结果](https://camo.githubusercontent.com/ded9be340188be8d3cb8ee2fd6adfe614f07e043/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031392f322f32332f313639313832386637643766663866373f773d38393526683d38343726663d706e6726733d313031303632) + +后面我将把这个系列的文章,收录到我的[【CuteJavaScript】](http://js.pingan8787.com)中,里面有整理了**ES6/7/8/9知识点**和**重温JS基础系列**文章。 + +那么,快跟我一步步来完成这个入门项目吧。 + +## 零、Angular安装 +Angular 需要 `Node.js` 的 `8.x` 或 `10.x` 版本。 +检查你的`Node.js`版本,请在终端/控制台窗口中运行 `node -v` 命令。 +要想安装` Node.js`,请访问 nodejs.org。 + +1. 安装Angular CLI + +```sh +npm install -g @angular/cli +``` + +2. 常用命令 + +后续用到会详细介绍这些命令。 + +* 启动服务,并打开新窗口 +```sh +ng serve --open +# --open 可简写 -o +``` + +* 创建新组件 +```sh +ng generate component books +# generate 可简写 g +``` + +* 创建新服务 +```sh +ng generate service books +``` + +* 创建路由模块 +```sh +ng generate module app-routing --flat --module=app +``` + +* 其他 +另外Angular CLI还有很多的命令提供,详细可以查阅官方文档 [Angular CLI 命令](https://angular.cn/cli)。 + +最后搭建完是这样: + +![图片创建](https://camo.githubusercontent.com/9c67d00f8b707eae15fd6c5d1a907942641909ad/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031392f322f32332f313639313832386637633431386237393f773d35373926683d36333126663d706e6726733d3239313137) + +## 一、项目起步 +1. 创建项目 +```sh +ng new books +cd books +``` + +2. 创建所需的两个页面组件 +```sh +ng g component index +ng g component detail +``` +`g`是`generate`的简写。 + + +## 二、编写路由组件 +这里为了项目结构先起来,所以先简单配置一下路由,后面路由会调整,如果遇到什么不懂,可以查看[Angular 路由与导航](https://angular.cn/guide/router)。 + +1. 安装**路由模块** +```sh +ng g module app-routing --flat --module=app +``` +**知识点:** +`--flat` 把这个文件放进了 `src/app` 中,而不是单独的目录中。 +`--module=app` 告诉 CLI 把它注册到 `AppModule` 的 `imports` 数组中。 + +2. 引入**路由模块** +```js +// app-routing.module.ts +import { RouterModule, Routes } from '@angular/router'; +``` +3. 导出**路由模块**的指令 + +这里需要添加一个 `@NgModule.exports` 数组,并传入`RouterModule`,导出 `RouterModule` 让路由器的相关指令可以在 `AppModule` 中的组件中使用。 +```js +// app-routing.module.ts +@NgModule({ + imports: [CommonModule], + declarations: [], + exports: [RouterModule] +}) +``` + +4. 添加定义路由 + +这里添加路由的时候,记得将所需要指向的组件也引入进来,这里我们需要引入两个页面的组件: +```js +// app-routing.module.ts +import { IndexComponent } from './index/index.component'; +import { DetailComponent } from './detail/detail.component'; +``` +然后将我们所需要的路由定义在`routes`变量中,类型是我们引入的`Routes`: +```js +// app-routing.module.ts +const routes: Routes = [ + { path: '', redirectTo:'/index', pathMatch:'full' }, // 1 + { path: 'index', component: IndexComponent}, // 2 + { path: 'detail/:id', component: DetailComponent}, // 3 +] +``` +**知识点**: +`angular`的路由接收两个参数: +* `path`:用于匹配浏览器地址栏中 `URL` 的字符串。 +* `component`:当导航到此路由时,路由器展示的组件名称。 + +**第1行代码**: +作为路由系统的默认路由,当所有路由都不匹配的话,就会重定向到这个路由,并展示对应的组件。 +**第2行代码**: +正常情况下的路由配置。 +**第3行代码**: +配置的是携带参数的路由,在路由`/`后,用 `:` 拼接参数名来实现,**获取这个参数的值的方法后面会介绍**。 + +另外,我们还可以这么传递参数,直接将数据通过路由传入,后面还会介绍: +```js +{ path: 'pathname', component: DemoComponent, data: { title: 'pingan8787' } }, +``` + +5. 添加路由监视 + +配置好路由还不能使用,需要一个监视路由变化的工具,这时候需要把`RouterModule`添加到 `@NgModule.imports` 数组中,并用 `routes` 来配置它。 +这里只需要调用` imports `数组中的 `RouterModule.forRoot()` 函数就行了,就像这样: +```js +// app-routing.module.ts +imports: [ RouterModule.forRoot(routes) ], +``` + +6. 添加路由出口 + +所谓的路由出口,就是路由所对应的组件展示的地方,接下来我们在`app.component.html`内容中,添加``: +```html + +
+

欢迎来到我的个人书屋!

+ +
+``` +这里的``就是我们路由输出的地方,也是组件展示的地方,简单理解就是,它会告诉路由器要在哪里显示路由的视图。 + +7. 添加路由链接 + +所谓的路由链接,就是出发路由跳转事件的地方,比如一个按钮,一张图片等,我们还是在`app.component.html`中,使用``添加3个按钮: +```html + +
+

欢迎来到我的个人书屋!

+ 重定向 | + 打开首页 | + 打开书本详情 + +
+``` +这边3个按钮的路由,我们将上面定义的3种路由,传入到`routerLink`参数中,现在就项目就可以实现页面跳转了。 + +另外,这里还可以传入一个可选参数`routerLinkActive="className"`,表示当这个``标签激活的时候显示的样式,值是一个字符串,为样式的类名: +```html +打开首页 | +``` + +8. 获取带参数路由的参数 + +在第7步中,我们点击 **打开书本详情** 按钮中,在路由中带了参数,这时候我们需要这么来获取这个参数: +* 先导出模块`ActivatedRoute`和`Location`: +```js +// detail.component.ts +import { ActivatedRoute } from '@angular/router'; +import { Location } from '@angular/common'; +``` +* 再注入到构造函数中,并将值作为私有变量: +```js +// detail.component.ts +export class DetailComponent implements OnInit { + constructor( + private route: ActivatedRoute, + private location: Location + ) { } + ngOnInit() {} +} +``` +**知识点:** +`ActivatedRoute` 保存该 `DetailComponent` 实例的路由信息。可以从这个组件获取URL中的路由参数和其他数据。 +`Location` 是一个 `Angular` 的服务,用来与浏览器打交道。后续会使用它来导航回上一个视图。 + +* 提取路由参数: + +这里声明`getDetail`方法,提取路由参数,并`ngOnInit`**生命周期钩子方法**在中执行。 +```js +// detail.component.ts +ngOnInit() { + this.getDetail() +} +getDetail(): void{ + const id = +this.route.snapshot.paramMap.get('id'); + console.log(`此课本的id是${id}`) +} +``` +**知识点**: +`route.snapshot` 是一个路由信息的**静态快照**,抓取自组件刚刚创建完毕之后。 +`paramMap` 是一个URL中路由所携带的参数值的对象。"id"对应的值就是要获取的书本的 id。 +**注意**: +路由参数总会是字符串。这里我们使用 (+) 操作符,将字符串转换成数字。 + +现在在浏览器上刷新下页面,再点击 **打开书本详情** 按钮,可以看到控制台输出了` 此课本的id是1 `的结果。 +到这一步,我们算是把路由配置完成了,接下来可以开始做页面的逻辑了。 + + + +## 三、编写页面组件 +接下来开始编写页面组件,这里我们挑重点来写,一些布局的样式,后面可以看源码。 + +### 1.编写单一组件 +我们首先写一个书本信息的组件,代码如下: +```html + +
+
+ +
+ + +
程姬
+
+
+
+``` +**知识点**: +`*ngFor` 是一个 Angular 的复写器(repeater)指令,就像**angular1**中的`ng-for`和**vuejs**中的`v-for`。 它会为列表中的每项数据复写它的宿主元素。 +这时候可以看到页面变成下面这个样子: +![图片3-1](http://images.pingan8787.com/angular_books_3_1.png) + + +接下来我们要把写死在HTML上面的数据,抽到JS中: + +现在先新建一个`books.ts`文件来定义一个`Book`类,并添加`id`,`url`,`title`和`author`四个属性: +```js +// src/app/books.ts +export class Book { + id: number; + url: string; + title: string; + author: string; +} +``` +然后回到`index.component.ts`文件去引入它,并定义一个`books`属性,使用导入进来的`Book`类作为类型: +```js +// index.component.ts +import { Book } from '../books'; +export class IndexComponent implements OnInit { + books: Book = { + id: 1, + url: 'https://img3.doubanio.com/view/subject/m/public/s29988481.jpg', + title: '像火焰像灰烬', + author: '程姬', + } +} +``` +然后再改造前面的组件文件`index.component.html`: +```html + +
+ {{books.id}} + +
{{books.author}}
+
+``` + +接着,我们再为每个课本添加一个点击事件,来实现点击封面图能查看大图的效果,现在`index.component.ts`中定义一个`getDetailImage`方法,并在`index.component.html`中绑定该方法: +```js +// index.component.ts +export class IndexComponent implements OnInit { + getDetailImage(books){ + alert(`正在查看id为${books.id}的大图!`); + } +} +``` +这边方法的具体实现,不写,不是本文重点。下面是增加点击事件的绑定: +```html + +{{books.id}} +``` +**知识点**: +`(click)`是Angular用来绑定事件,它会让 Angular 监听这个` ` 元素的 `click` 事件。 当用户点击 `` 时,Angular 就会执行表达式 `getDetailImage(books)`。 + +再来,我们引入前面学到的**路由链接**指令来改造HTML: +```html + +{{books.title}} +``` +这时候,我们在点击书本的标题,发现页面跳转到URL地址为`http://localhost:4200/detail/1`的页面,这就说明,我们页面的路由跳转也成功了~ + +改造完成后,可以看到,页面显示的还是一样,接下来我们先这样放着,因为我们后面会进行数据模拟,和模拟服务器请求。 + +我们就这样写好第一个单一组件,并且数据是从JS中读取的。 + +### 2.模拟数据 +这时候为了方便后面数据渲染,我们这里需要模拟一些本地数据,我们创建一个本地` mock-books.ts`文件来存放模拟的数据: +```js +// app/mock-books.ts +import { Books } from './books'; +export const BookList: Books[] = [ + { + id: 1, + url: 'https://img3.doubanio.com/view/subject/m/public/s29988481.jpg', + title: '像火焰像灰烬', + author: '程姬', + }, + // 省略其他9条 +] +``` +然后在`index.component.ts`中导入模拟的数据,并将原有的`books`值修改成导入的模拟数据`BookList`: +```js +// index.component.ts +import { BookList } from '../mock-books'; +books = BookList; +``` +并将原本的`*ngFor`中修改成这样,绑定真正的数据: +```html + +
+ {{item.id}} + +
{{item.author}}
+
+``` + +### 3.编写主从组件 +当我们写完一个单一组件后,我们会发现,如果我们把每个组件都写到同一个HTML文件中,这是很糟糕的事情,这样做有缺点: +* 代码复用性差;(导致每次相同功能要重新写) +* 代码难维护;(因为一个文件会非常长) +* 影响性能;(打开每个页面都要重复加载很多) + +为了解决这个问题,我们这里就要开始使用真正的**组件化思维**,将通用常用组件抽离出来,通过参数传递来控制组件的不同业务形态。 +这便是我们接下来要写的主从组件。 + +思考一下,我们这里现在能抽成组件作为公共代码的,就是这个单个书本的内容,因为每个书本的内容都一致,只是里面数据的差异,于是我们再新建一个组件: +```sh +ng g component books +``` +并将前面`index.component.html`中关于课本的代码剪切到`books.component.html`中来,然后删除掉`*ngFor`的内容,并将原本本地的变量`books`替换成`list`,这个变量我们等会会取到: +```html + +
+ {{list.id}} + +
{{list.author}}
+
+``` +再将这个组件,引用到它的父组件中,这里是要引用到`index.component.html`的组件中,并将前面的`*ngFor`再次传入``: +```html +
+
+ +
+
+``` + +接下来要做的就是获取到`list`变量的值,显然这个值是要从外面组件传进来的,我们需要在`books.component.ts`引入前面定义的 `Books`类 和 `@Input() 装饰器`,还要添加一个带有 `@Input() 装饰器`的 `list` 属性,另外还要记得将`getDetailImage`方法也剪切过来: +```js +// books.component.ts +import { Component, OnInit, Input } from '@angular/core'; +import { Books } from '../books'; + +export class BooksComponent implements OnInit { + @Input() list: Books; + constructor() { } + ngOnInit() {} + getDetailImage(books){ + alert(`正在查看id为${books.id}的大图!`); + } +} +``` +`@Input() 装饰器`介绍具体可以查看 [手册](https://angular.cn/guide/template-syntax#inputs-outputs) + +我们要获取的 `list` 属性必须是一个带有` @Input() `装饰器的输入属性,因为外部的 `IndexComponent` 组件将会绑定到它。就像这样: +```html + +``` + +**知识点**: +`[list]="item"` 是 `Angular` 的**属性绑定**语法。这是一种**单向数据绑定**。从 `IndexComponent` 的 `item` 属性绑定到目标元素的 `list` 属性,并映射到了 `BooksComponent` 的 `list` 属性。 + +做到这里,我们已经将`BooksComponent`作为`IndexComponent`的子组件来引用了,在实际开发过程中,这样的父子组件关系,会用的非常多。 + +写到这里,看看我们项目,还是一样正常在运行,只是现在项目中组件分工更加明确了。 + +现在的效果图: +![图片3-2](http://images.pingan8787.com/angular_books_3_2.png) + + + +## 四、编写服务 +截止到这部分,我们的`BooksComponent`组件获取和显示的都是本地模拟的数据。 +接下来我们要开始对这些进行重构,让聚焦于为它的视图提供支持,这也让它更容易使用模拟服务进行单元测试。 + +### 1.为什么需要服务 +我们不应该让组件来直接获取或保存数据,它们应该聚焦于展示数据,而数据访问的工作交给其他服务来做。 +这里我们需要创建一个名为`BooksService`的服务,让我们应用中所有的类都使用它来获取书本列表的数据,使用的时候,只需要将它通过Angular的**依赖注入机制**注入到需要用的组件的构造函数中。 + +**知识点:** +服务可以实现多个不同组件之间信息共享,后面我们还会将它注入到两个地方: +`BooksService`中,使用该服务发送消息。 +`IndexService`中,使用该服务来展示消息。 + +接下来我们使用命令行,创建`BooksService `: +```sh +ng g service books +``` +在生成的`books.service.ts`文件中: +```js +// books.service.ts +import { Injectable } from '@angular/core'; +@Injectable({ + providedIn: 'root' +}) +``` +新导入了`@Injectable`装饰器,是为了让`BooksService`提供一个可注入的服务,并且它还可以拥有自己的待注入的依赖,简单理解就是**如果你的服务需要依赖,那么你就需要导入它**。 +并且它接收该服务的元数据对象。 + +### 2.编写服务 +接下来我们开始编写`books.service.ts`服务。 + +* 导入服务所需组件 + +这里我们导入`Books`和`BookList`,并添加一个`getBooks`方法来返回所有书本的数据,并且还需要添加一个`getBooks`方法来返回指定id的书本信息: +```js +// index.component.ts +import { Books } from './books'; +import { BookList } from './mock-books'; +@Injectable({ + providedIn: 'root' +}) +export class BooksService { + constructor() { } + getBookList(): Books[] { + return BookList; + } + getBook(id: number): Books{ + return BookList.find(book => book.id === id) + } +} +``` +在我们使用这个服务之前,需要先注册该服务,因为我们在使用`ng g service books`命令创建服务时,CLI已经默认为我们添加了注册了,这是方法就是上面代码中的: +```js +providedIn: 'root' +``` +表示将我们的服务注册在**根注入器**上,这样我们就可以把这个服务注入到任何享用的类上了。 + +* 修改`IndexComponent` + +先删除`BookList`的引入,并修改`books`属性的定义: + +```js +// index.component.ts +import { BooksService } from '../books.service'; +export class IndexComponent implements OnInit { + books : Books[]; + ngOnInit() {} +} +``` +然后注入我们的`BooksService`服务,需要先往构造函数中添加一个私有的`booksservice`,使用注入的`BooksService`作为类型,理解成一个注入点: +```js +// index.component.ts +constructor(private booksservice: BooksService) { } +``` + +之后我们需要添加一个`getBooks`方法来获取这些书本数据,并在生命周期函数`ngOnInit`中调用: +```js +export class IndexComponent implements OnInit { + ngOnInit() { + this.getBooks(); + } + getBooks(): void{ + this.books = this.booksservice.getBookList(); + } +} +``` + +* 修改`DetailComponent` +我们先改造书本详情页的HTML结构: +```html + +
+

《{{books.title}}》介绍

+
+ +
+

书本标题: {{books.title}}

+

书本作者: {{books.author}}

+

书本id: {{books.id}}

+
+
+

暂无信息

+
+``` +**知识点**: +这里使用了`*ngIf`指令,当条件为`true`则显示其HTML内容。 + +```js +// detail.component.ts +import { Books } from '../books'; +import { BooksService } from '../books.service'; +export class DetailComponent implements OnInit { + constructor( + private route: ActivatedRoute, + private location: Location, + private booksservice: BooksService // 引入BooksService服务 + ) { } + + books: Books; // 定义books类型 + ngOnInit() { + this.getDetail() + } + getDetail(): void{ + const id = +this.route.snapshot.paramMap.get('id'); + this.getBooks(id); + } + getBooks(id: number): void { + this.books = this.booksservice.getBook(id); + } +} +``` +这段代码,主要定义了`getBooks`方法,当刚进入页面时,将书本`id`传入`getBooks`方法,去`BooksService`去获取对应id的书本信息,并复制给变量`books`,然后展示到页面。 + +改造之后,我们的页面显示依旧正常。 + +![图片3-2](http://images.pingan8787.com/angular_books_3_2.png) + +但是我们要知道,这背后的逻辑已经改变了。 + +## 五、引入RxJS改造项目 +### 1.关于RxJS +这里简单介绍关键概念,具体可以查看 [RxJS 官网](https://RxJS.dev/),也可以参考 [浅析Angular之RxJS](https://www.jianshu.com/p/36d85f8cafdd)。 + +#### 什么是RxJS + +RxJS全称`Reactive Extensions for JavaScript`,中文意思: JavaScript的响应式扩展。 +RxJS主要是提供一种更加强大和优雅的方式,来利用响应式编程的模式,实现JavaScript的异步编程。 + +#### RxJS优点 + +* 纯净性; +* 流动性; + +#### RxJS核心概念 + +RxJS 是基于观察者模式和迭代器模式以函数式编程思维来实现的。RxJS 中含有两个基本概念:`Observables` 与 `Observer`。 +`Observables` 作为被观察者,是一个值或事件的流集合;而 `Observer` 则作为观察者,根据 `Observables` 进行处理。它们之间的订阅发布关系(观察者模式) 如下: +**订阅**:`Observer` 通过 `Observable` 提供的 `subscribe()` 方法订阅 `Observable`。 +**发布**:`Observable` 通过回调 `next` 方法向 `Observer` 发布事件。 + +———— 来源[Angular修仙之路 RxJS Observable](http://www.semlinker.com/rxjs-observable/) + +另外这里列出来一些核心,具体还是看官网咯,并且下面使用到的时候会具体介绍。 +* `Observable` (可观察对象): 表示一个概念,这个概念是一个可调用的未来值或事件的集合。 +* `Observer`(观察者): 一个回调函数的集合,它知道如何去监听由 `Observable` 提供的值。 +* `Subscription` (订阅): 表示 `Observable` 的执行,主要用于取消 `Observable` 的执行。 +* `Operators` (操作符): 采用函数式编程风格的纯函数 (`pure function`),使用像 `map`、`filter`、`concat`、`flatMap` 等这样的操作符来处理集合。 +* `Subject` (主体): 相当于 `EventEmitter`,并且是将值或事件多路推送给多个 `Observer` 的唯一方式。 +* `Schedulers` (调度器): 用来控制并发并且是中央集权的调度员,允许我们在发生计算时进行协调,例如 `setTimeout` 或` requestAnimationFrame `或其他。 + +### 2.引入RxJS +在我们的真实应用中,我们必须要等到服务器响应后,我们才能获取到数据,因此这天生就需要用异步思维来操作。 + +由于Angular中已经自带RxJS,所以我们只要在需要使用的时候,引入即可使用: + +### 3.改造数据获取方式 +了解完RxJS的一些概念后,我们开始改造下这些书本的数据获取方式。 + +* 改造`BooksService` + +首先我们从 RxJS 中导入 `Observable` 和 `of` 符号: +```js +// books.service.ts +import { Observable, of } from 'rxjs'; +``` +**知识点**: +`Observable`: 观察者模式中的观察者,具体可以参考 [Angular修仙之路 RxJS Observable](http://www.semlinker.com/rxjs-observable/) +`of`: 用来获取观察者拿到的数据,通常是一个`Observable`。 + +然后修改`getBookList`方法 +```js +// books.service.ts +getBookList(): Observable { + return of(BookList); +} +``` +这里 `of(BookList)` 返回一个` Observable`,它会发出单个值,这个值就是这些模拟书本的数组。 + +* 改造`IndexComponent` + +这里也要修改`getBooks`方法,使用`subscribe`去订阅服务返回回来的值: +```js +// index.component.ts +getBooks(): void{ + this.booksservice.getBookList() + .subscribe(books => this.books = books); +} +``` +由于原本直接赋值数据,在实际场景中是不可能这样同步的,所以这里`subscribe`函数,会在`Observable`发出数据以后,再把书本列表传到里面的回调函数,再复制给`books`属性。 +使用这种异步方式,当 `BooksService` 从远端服务器获取英雄数据时,不用担心还没拿到数据就执行后面。 + +下一步,我们就要改造一下项目了。 + + +## 六、改造组件 +从这里开始,我们要使用RxJS来改造组件和添加新功能了,让整个项目更加完善。 + +### 1.添加历史记录组件 + +* 创建`HistoryComponent`组件 +```sh +ng g component hostory +``` +然后在`app.component.html`文件夹中添加组件: +```html + + +``` + +### 2.添加增删改查功能 + +这里我们要开始做书本的增删改查功能,需要先创建一个`HistoryService`服务,方便我们实现这几个功能: + +* 创建`HistoryService`服务 +```sh +ng g service history +``` +然后在生成的ts文件中,增加`add`和`clear`方法,`add`方法用来添加历史记录到`history`数组中,`clear`方法则是清空`history`数组: +```js +// history.service.ts +export class HistoryService { + history: string[] = []; + add(history: string){ + this.history.push(history); + } + clear(){ + this.history = []; + } +} +``` + +* 使用`HistoryService`服务 + +在将这个服务,注入到`BooksService`中,并改造`getBooks`方法: +```js +// books.service.ts +import { HistoryService } from './history.service'; +constructor( + private historyservice: HistoryService +) { } +getBooks(): void{ + this.historyservice.add('请求书本数据') + this.booksservice.getBookList() + .subscribe(books => this.books = books); +} +``` +也可以用相同方法,在`IndexComponent`中添加`访问首页书本列表`的记录。 +```js +// index.component.ts +import { HistoryService } from '../history.service'; +constructor( + private booksservice: BooksService, + private historyservice: HistoryService +) { } +getBooks(): void{ + this.historyservice.add('访问首页书本列表'); + this.booksservice.getBookList() + .subscribe(books => this.books = books); +} +``` + +接下来,将我们的`HistoryService`注入到`HistoryComponent`中,然后才能将历史数据显示到页面上: +```js +// history.component.ts +import { HistoryService } from '../history.service'; +export class HistoryComponent implements OnInit { + constructor(private historyservice: HistoryService) { } + ngOnInit() {} +} +``` +```html + +
+

操作历史:

+
+ +
{{item}}
+
+
+``` +**代码解释**: +`*ngIf="historyservice.history.length"`,是为了防止还没有拿到历史数据,导致后面的报错。 +`(click)="historyservice.clear()"`, 绑定我们服务中的`clear`事件,实现清除缓存。 +`*ngFor="let item of historyservice.history"`,将我们的历史数据渲染到页面上。 + + +到了这一步,就能看到历史数据了,每次也换到首页,都会增加一条。 + +![图片5-1](http://images.pingan8787.com/angular_books_5_1.png) + +接下来,我们要在书本详情页也加上历史记录的统计,导入文件,注入服务,然后改造`getBooks`方法,实现历史记录的统计: +```js +// detail.component.ts +import { HistoryService } from '../history.service'; + +export class DetailComponent implements OnInit { + constructor( + private route: ActivatedRoute, + private location: Location, + private booksservice: BooksService, + private historyservice: HistoryService + ) { } + //... + getBooks(id: number): void { + this.books = this.booksservice.getBook(id); + this.historyservice.add(`查看书本${this.books.title},id为${this.books.id}`); + console.log(this.books) + } +} +``` +![图片5-2](http://images.pingan8787.com/angular_books_5_2.png) + +这时候就可以在历史记录中,看到这些操作的记录了,并且**清除**按钮也正常使用。 + +## 七、HTTP改造 +原本我只想写到上一章,但是想到,我们实际开发中,哪有什么本地数据,基本上数据都是要从服务端去请求,所以这边也有必要引入这一张,模拟实际的HTTP请求。 + +### 1.引入HTTP +在这一章,我们使用Angular提供的 `HttpClient` 来添加一些数据持久化特性。 +然后实现对书本数据进行**获取,增加,修改,删除和查找**功能。 + +`HttpClient`是Angular通过 HTTP 与远程服务器通讯的机制。 + +这里我们为了让`HttpClient`在整个应用全局使用,所以将`HttpClient`导入到根模块`app.module.ts`中,然后把它加入 `@NgModule.imports` 数组: +```js +import { HttpClientModule } from '@angular/common/http'; +@NgModule({ + //... + imports: [ + BrowserModule, + AppRoutingModule, + HttpClientModule + ], + //... +}) +``` + +这边我们使用 [内存 Web API(In-memory Web API) ](https://github.com/angular/in-memory-web-api)模拟出的远程数据服务器通讯。 +**注意:** 这个内存 Web API 模块与 Angular 中的 HTTP 模块无关。 + +通过下面命令来安装: +```sh +npm install angular-in-memory-web-api --save +``` +然后在`app.module.ts`中导入 `HttpClientInMemoryWebApiModule` 和 `InMemoryDataService` 类(后面创建): +```js +// app.module.ts +import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api'; +import { InMemoryDataService } from './in-memory-data.service'; +@NgModule({ + // ... + imports: [ + // ... + HttpClientInMemoryWebApiModule.forRoot( + InMemoryDataService, {dataEncapsulation:false} + ) + ], + // ... +}) +export class AppModule { } +``` +**知识点:** +`forRoot()` 配置方法接受一个 InMemoryDataService 类(初期的内存数据库)作为参数。 + +然后我们要创建`InMemoryDataService`类: +```sh +ng g service InMemoryData +``` +并将生成的`in-memory-data.service.ts`修改为: +```js +// in-memory-data.service.ts +import { Injectable } from '@angular/core'; +import { InMemoryDbService } from 'angular-in-memory-web-api'; +import { Books } from './books'; +@Injectable({ + providedIn: 'root' +}) +export class InMemoryDataService implements InMemoryDbService { + createDb(){ + const books = [ + { + id: 1, + url: 'https://img3.doubanio.com/view/subject/m/public/s29988481.jpg', + title: '像火焰像灰烬', + author: '程姬', + }, + // 省略其他9条数据 + ]; + return {books}; + } + constructor() { } +} +``` + +这里先总结`InMemoryDbService`所提供的RESTful API,后面都要用到: +例如如果`url`是`api/books`,那么 +* 查询所有成员:以**GET**方法访问`api/books` +* 查询某个成员:以**GET**方法访问`api/books/id`,比如`id`是`1`,那么访问`api/books/1` +* 更新某个成员:以**PUT**方法访问`api/books/id` +* 删除某个成员:以**DELETE**方法访问`api/books/id` +* 增加一个成员:以**POST**方法访问`api/books` + + +### 2.通过HTTP请求数据 + +现在要为接下来的网络请求做一些准备,先在`books.service.ts`中引入HTTP符号,然后注入`HttpClient`并改造: +```js +// books.service.ts +import { HttpClient, HttpHeaders} from '@angular/common/http'; +// ... +export class BooksService { + constructor( + private historyservice: HistoryService, + private http: HttpClient + ) { } + private log(histories: string){ + this.historyservice.add(`正在执行:${histories}`) + } + private booksUrl = 'api/books'; // 提供一个API供调用 + // ... +} +``` +这里我们还新增一个私有方法`log`和一个私有变量`booksUrl`。 + +接下来我们要开始发起http请求数据,开始改造`getBookList`方法: +```js +// books.service.ts +// ... +getBookList(): Observable { + this.historyservice.add('请求书本数据') + return this.http.get(this.booksUrl); +} +// ... +``` +这里我们使用 `http.get` 替换了 `of`,其它没修改,但是应用仍然在正常工作,这是因为这两个函数都返回了 `Observable`。 + +实际开发中,我们还需要考虑到**请求的错误处理**,要捕获错误,我们就要使用 RxJS 的 `catchError()` 操作符来建立对 Observable 结果的处理管道(pipe)。 + +我们引入`catchError `并改造原本`getBookList`方法: + +```js +// books.service.ts +getBookList(): Observable { + this.historyservice.add('请求书本数据') + return this.http.get(this.booksUrl).pipe( + catchError(this.handleError('getHeroes', [])) + ); +} +private handleError (operation = 'operation', result?: T) { + return (error: any): Observable => { + this.log(`${operation} 失败: ${error.message}`); // 发出错误通知 + return of(result as T); // 返回空结果避免程序出错 + }; +} +``` +**知识点**: +`.pipe()` 方法用来扩展 `Observable` 的结果。 +`catchError()` 操作符会拦截失败的 Observable。并把错误对象传给错误处理器,错误处理器会处理这个错误。 +`handleError()` 错误处理函数做了两件事,发出错误通知和返回空结果避免程序出错。 + +这里还需要使用`tap`操作符改造`getBookList`方法,来窥探`Observable`数据流,它会查看`Observable`的值,然后我们使用`log`方法,记录一条历史记录。 +`tap` 回调不会改变这些值本身。 +```js +// books.service.ts +getBookList(): Observable { + return this.http.get(this.booksUrl) + .pipe( + tap( _ => this.log('请求书本数据')), + catchError(this.handleError('getHeroes', [])) + ); +} +``` + +### 3.通过HTTP修改数据 +这里我们需要在原来`DetailComponent`上面,添加一个输入框、保存按钮和返回按钮,就像这样: +```html + + +
+

修改信息:

+ + + +
+``` +这边切记一点,一定要在`app.module.ts`中引入 `FormsModule`模块,并在`@NgModule`的`imports`中引入,不然要报错了。 +```js +// app.module.ts +// ... +import { FormsModule } from '@angular/forms'; +@NgModule({ + // ... + imports: [ + // ... + FormsModule + ], + // ... +}) +``` +`input`框绑定书本的标题`books.title`,而保存按钮绑定一个`save()`方法,这里还要实现这个方法: +```js +// detail.component.ts +save(): void { + this.historyservice.updateBooks(this.books) + .subscribe(() => this.goBack()); +} +goBack(): void { + this.location.back(); +} +``` +这里通过调用`BooksService`的`updateBooks`方法,将当前修改后的书本信息修改到源数据中,这里我们需要去`books.service.ts`中添加`updateBooks`方法: +```js +// books.service.ts +// ... +updateBooks(books: Books): Observable{ + return this.http.put(this.booksUrl, books, httpOptions).pipe( + tap(_ => this.log(`修改书本的id是${books.id}`)), + catchError(this.handleError(`getBooks请求是id为${books.id}`)) + ) +} +// ... +``` +**知识点**: +`HttpClient.put()` 方法接受三个参数:`URL 地址`、`要修改的数据`和`其他选项`。 +`httpOptions` 常量需要定义在`@Injectable`修饰器之前。 + +现在,我们点击首页,选择一本书进入详情,修改标题然后保存,会发现,首页上这本书的名称也会跟着改变呢。这算是好了。 + + +### 4.通过HTTP增加数据 +我们可以新增一个页面,并添加上路由和按钮: +```sh +ng g component add +``` +添加路由: +```js +// app-routing.module.ts +// ... +import { AddComponent } from './add/add.component'; + +const routes: Routes = [ + { path: '', redirectTo:'/index', pathMatch:'full' }, + { path: 'index', component: IndexComponent}, + { path: 'detail/:id', component: DetailComponent}, + { path: 'add', component: AddComponent}, +] +``` +添加路由入口: +```html + + +添加书本 +``` +编辑添加书本的页面: +```html + +
+

添加书本:

+ + + + +
+
+``` +初始化添加书本的数据: +```js +// add.component.ts +// ... +import { Books } from '../books'; +import { BooksService } from '../books.service'; +import { HistoryService } from '../history.service'; +import { Location } from '@angular/common'; +export class AddComponent implements OnInit { + books: Books = { + id: 0, + url: '', + title: '', + author: '' + } + constructor( + private location: Location, + private booksservice: BooksService, + private historyservice: HistoryService + ) { } + ngOnInit() {} + add(books: Books): void{ + books.title = books.title.trim(); + books.author = books.author.trim(); + this.booksservice.addBooks(books) + .subscribe( book => { + this.historyservice.add(`新增书本${books.title},id为${books.id}`); + this.location.back(); + }); + } +} +``` +然后在`books.service.ts`中添加`addBooks`方法,来添加一本书本的数据: +```js +// books.service.ts +addBooks(books: Books): Observable{ + return this.http.post(this.booksUrl, books, httpOptions).pipe( + tap((newBook: Books) => this.log(`新增书本的id为${newBook.id}`)), + catchError(this.handleError('添加新书')) + ); +} +``` + + +现在就可以正常添加书本啦。 + +![图片5-3](http://images.pingan8787.com/angular_books_5_3.png) + + +### 5.通过HTTP删除数据 +这里我们先为每个书本后面添加一个删除按钮,并绑定删除事件`delete`: +```html + + +X +``` +```js +// books.component.ts +import { BooksService } from '../books.service'; +export class BooksComponent implements OnInit { + @Input() list: Books; + constructor( + private booksservice: BooksService + ) { } + // ... + delete(books: Books): void { + this.booksservice.deleteBooks(books) + .subscribe(); + } +} +``` +然后还要再`books.service.ts`中添加`deleteBooks`方法来删除: +```js +// books.service.ts +deleteBooks(books: Books): Observable{ + const id = books.id; + const url = `${this.booksUrl}/${id}`; + return this.http.delete(url, httpOptions).pipe( + tap(_ => this.log(`删除书本${books.title},id为${books.id}`)), + catchError(this.handleError('删除书本')) + ); +} +``` +这里需要在删除书本结束后,通知`IndexComponent`将数据列表中的这条数据删除,这里还需要再了解一下[Angular 父子组件数据通信](https://blog.csdn.net/u010730126/article/details/68080139)。 +然后我们在父组件`IndexComponent`上添加`change`事件监听,并传入本地的`funChange`: +```html + + +``` +在对应的`index.component.ts`中添加`funChange`方法: +```js +// index.component.ts +funChange(books, $event){ + this.books = this.books.filter(h => h.id !== books.id); +} +``` + +再来,我们在子组件`BooksComponent`上多导入`Output`和`EventEmitter`,并添加`@Output()`修饰器和调用`emit`: +```js +import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; +export class BooksComponent implements OnInit { + // ... + @Output() + change = new EventEmitter() + // ... + delete(books: Books): void { + this.booksservice.deleteBooks(books) + .subscribe(()=>{ + this.change.emit(books); + }); + } +} +``` +这样就实现了我们父子组件之间的事件传递啦,现在我们的页面还是正常运行,并且删除一条数据后,页面数据会更新。 + + +### 6.通过HTTP查找数据 +还是在`books.service.ts`,我们添加一个方法`getBooks`,来实现通过ID来查找指定书本,因为我们是通过ID查找,所以返回的是单个数据,这里就是`Observable`类型: +```js +// books.service.ts +getBooks(id: number): Observable{ + const url = `${this.booksUrl}/${id}`; + return this.http.get(url).pipe( + tap( _ => this.log(`请求书本的id为${id}`)), + catchError(this.handleError(`getBooks请求是id为${id}`)) + ) +} +``` +注意,这里 `getBooks` 会返回 `Observable`,是一个可观察的单个对象,而不是一个可观察的对象数组。 + + +## 八、结语 +这个项目其实很简单,但是我还是一步一步的写下来,一方面让自己更熟悉Angular,另一方面也是希望能帮助到更多朋友哈~ +最终效果: + +![图片结果](http://images.pingan8787.com/angular_books_result.png) + + +**本部分内容到这结束** + +|Author|王平安| +|---|---| +|E-mail|pingan8787@qq.com| +|博 客|www.pingan8787.com| +|微 信|pingan8787| +|每日文章推荐|https://github.com/pingan8787/Leo_Reading/issues| +|JS小册|js.pingan8787.com| +|微信公众号|前端自习课| + + +![前端自习课](https://camo.githubusercontent.com/7d890fb10cccf99c03dcf144e0e290357195ac44/68747470733a2f2f757365722d676f6c642d63646e2e786974752e696f2f323031392f322f31362f313638663439663032333831393163613f773d3130373826683d36343726663d706e6726733d323832353135) diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/.editorconfig" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/.editorconfig" new file mode 100644 index 00000000..6e87a003 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/.editorconfig" @@ -0,0 +1,13 @@ +# Editor configuration, see http://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/.gitignore" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/.gitignore" new file mode 100644 index 00000000..ee5c9d83 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/.gitignore" @@ -0,0 +1,39 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# compiled output +/dist +/tmp +/out-tsc + +# dependencies +/node_modules + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# misc +/.sass-cache +/connect.lock +/coverage +/libpeerconnection.log +npm-debug.log +yarn-error.log +testem.log +/typings + +# System Files +.DS_Store +Thumbs.db diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/README.md" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/README.md" new file mode 100644 index 00000000..55356bf0 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/README.md" @@ -0,0 +1,27 @@ +# Books + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 6.2.4. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/angular.json" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/angular.json" new file mode 100644 index 00000000..1f14688a --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/angular.json" @@ -0,0 +1,127 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "books": { + "root": "", + "sourceRoot": "src", + "projectType": "application", + "prefix": "app", + "schematics": {}, + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "dist/books", + "index": "src/index.html", + "main": "src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tsconfig.app.json", + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "src/styles.css" + ], + "scripts": [] + }, + "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 + } + } + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "options": { + "browserTarget": "books:build" + }, + "configurations": { + "production": { + "browserTarget": "books:build:production" + } + } + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "books:build" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tsconfig.spec.json", + "karmaConfig": "src/karma.conf.js", + "styles": [ + "src/styles.css" + ], + "scripts": [], + "assets": [ + "src/favicon.ico", + "src/assets" + ] + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": [ + "src/tsconfig.app.json", + "src/tsconfig.spec.json" + ], + "exclude": [ + "**/node_modules/**" + ] + } + } + } + }, + "books-e2e": { + "root": "e2e/", + "projectType": "application", + "architect": { + "e2e": { + "builder": "@angular-devkit/build-angular:protractor", + "options": { + "protractorConfig": "e2e/protractor.conf.js", + "devServerTarget": "books:serve" + }, + "configurations": { + "production": { + "devServerTarget": "books:serve:production" + } + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": "e2e/tsconfig.e2e.json", + "exclude": [ + "**/node_modules/**" + ] + } + } + } + } + }, + "defaultProject": "books" +} \ No newline at end of file diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/e2e/protractor.conf.js" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/e2e/protractor.conf.js" new file mode 100644 index 00000000..86776a39 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/e2e/protractor.conf.js" @@ -0,0 +1,28 @@ +// Protractor configuration file, see link for more information +// https://github.com/angular/protractor/blob/master/lib/config.ts + +const { SpecReporter } = require('jasmine-spec-reporter'); + +exports.config = { + allScriptsTimeout: 11000, + specs: [ + './src/**/*.e2e-spec.ts' + ], + capabilities: { + 'browserName': 'chrome' + }, + directConnect: true, + baseUrl: 'http://localhost:4200/', + framework: 'jasmine', + jasmineNodeOpts: { + showColors: true, + defaultTimeoutInterval: 30000, + print: function() {} + }, + onPrepare() { + require('ts-node').register({ + project: require('path').join(__dirname, './tsconfig.e2e.json') + }); + jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); + } +}; \ No newline at end of file diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/e2e/src/app.e2e-spec.ts" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/e2e/src/app.e2e-spec.ts" new file mode 100644 index 00000000..21fad901 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/e2e/src/app.e2e-spec.ts" @@ -0,0 +1,14 @@ +import { AppPage } from './app.po'; + +describe('workspace-project App', () => { + let page: AppPage; + + beforeEach(() => { + page = new AppPage(); + }); + + it('should display welcome message', () => { + page.navigateTo(); + expect(page.getParagraphText()).toEqual('Welcome to books!'); + }); +}); diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/e2e/src/app.po.ts" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/e2e/src/app.po.ts" new file mode 100644 index 00000000..82ea75ba --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/e2e/src/app.po.ts" @@ -0,0 +1,11 @@ +import { browser, by, element } from 'protractor'; + +export class AppPage { + navigateTo() { + return browser.get('/'); + } + + getParagraphText() { + return element(by.css('app-root h1')).getText(); + } +} diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/e2e/tsconfig.e2e.json" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/e2e/tsconfig.e2e.json" new file mode 100644 index 00000000..a6dd6220 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/e2e/tsconfig.e2e.json" @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/app", + "module": "commonjs", + "target": "es5", + "types": [ + "jasmine", + "jasminewd2", + "node" + ] + } +} \ No newline at end of file diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/package.json" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/package.json" new file mode 100644 index 00000000..552ba76a --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/package.json" @@ -0,0 +1,49 @@ +{ + "name": "books", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "test": "ng test", + "lint": "ng lint", + "e2e": "ng e2e" + }, + "private": true, + "dependencies": { + "@angular/animations": "^6.1.0", + "@angular/common": "^6.1.0", + "@angular/compiler": "^6.1.0", + "@angular/core": "^6.1.0", + "@angular/forms": "^6.1.0", + "@angular/http": "^6.1.0", + "@angular/platform-browser": "^6.1.0", + "@angular/platform-browser-dynamic": "^6.1.0", + "@angular/router": "^6.1.0", + "angular-in-memory-web-api": "^0.8.0", + "core-js": "^2.5.4", + "rxjs": "~6.2.0", + "zone.js": "~0.8.26" + }, + "devDependencies": { + "@angular-devkit/build-angular": "~0.8.0", + "@angular/cli": "~6.2.4", + "@angular/compiler-cli": "^6.1.0", + "@angular/language-service": "^6.1.0", + "@types/jasmine": "~2.8.8", + "@types/jasminewd2": "~2.0.3", + "@types/node": "~8.9.4", + "codelyzer": "~4.3.0", + "jasmine-core": "~2.99.1", + "jasmine-spec-reporter": "~4.2.1", + "karma": "~3.0.0", + "karma-chrome-launcher": "~2.2.0", + "karma-coverage-istanbul-reporter": "~2.0.1", + "karma-jasmine": "~1.1.2", + "karma-jasmine-html-reporter": "^0.2.2", + "protractor": "~5.4.0", + "ts-node": "~7.0.0", + "tslint": "~5.11.0", + "typescript": "~2.9.2" + } +} diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/add/add.component.css" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/add/add.component.css" new file mode 100644 index 00000000..ba0564ca --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/add/add.component.css" @@ -0,0 +1,14 @@ +.add{ + width: 260px; + margin: 0 auto; +} +.add label{ + display: block; + margin: 5px 0; +} +.add label input{ + float: right; +} +.add div{ + text-align: center; +} \ No newline at end of file diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/add/add.component.html" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/add/add.component.html" new file mode 100644 index 00000000..945c2e08 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/add/add.component.html" @@ -0,0 +1,16 @@ +
+

添加书本:

+ + + + +
+
\ No newline at end of file diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/add/add.component.spec.ts" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/add/add.component.spec.ts" new file mode 100644 index 00000000..fdcddf4c --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/add/add.component.spec.ts" @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AddComponent } from './add.component'; + +describe('AddComponent', () => { + let component: AddComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ AddComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AddComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/add/add.component.ts" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/add/add.component.ts" new file mode 100644 index 00000000..5d5fb48b --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/add/add.component.ts" @@ -0,0 +1,36 @@ +import { Component, OnInit } from '@angular/core'; +import { Books } from '../books'; +import { BooksService } from '../books.service'; +import { HistoryService } from '../history.service'; +import { Location } from '@angular/common'; + +@Component({ + selector: 'app-add', + templateUrl: './add.component.html', + styleUrls: ['./add.component.css'] +}) +export class AddComponent implements OnInit { + books: Books = { + id: 0, + url: '', + title: '', + author: '' + } + constructor( + private location: Location, + private booksservice: BooksService, + private historyservice: HistoryService + ) { } + + ngOnInit() { + } + add(books: Books): void{ + books.title = books.title.trim(); + books.author = books.author.trim(); + this.booksservice.addBooks(books) + .subscribe( book => { + this.historyservice.add(`新增书本${books.title},id为${books.id}`); + this.location.back(); + }); + } +} diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/app-routing.module.spec.ts" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/app-routing.module.spec.ts" new file mode 100644 index 00000000..d68ef067 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/app-routing.module.spec.ts" @@ -0,0 +1,13 @@ +import { AppRoutingModule } from './app-routing.module'; + +describe('AppRoutingModule', () => { + let appRoutingModule: AppRoutingModule; + + beforeEach(() => { + appRoutingModule = new AppRoutingModule(); + }); + + it('should create an instance', () => { + expect(appRoutingModule).toBeTruthy(); + }); +}); diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/app-routing.module.ts" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/app-routing.module.ts" new file mode 100644 index 00000000..5521ccc5 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/app-routing.module.ts" @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { IndexComponent } from './index/index.component'; +import { DetailComponent } from './detail/detail.component'; +import { AddComponent } from './add/add.component'; + +const routes: Routes = [ + { path: '', redirectTo:'/index', pathMatch:'full' }, + { path: 'index', component: IndexComponent}, + { path: 'detail/:id', component: DetailComponent}, + { path: 'add', component: AddComponent}, +] + +@NgModule({ + imports: [ RouterModule.forRoot(routes) ], + declarations: [], + exports: [RouterModule] +}) +export class AppRoutingModule { } diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/app.component.css" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/app.component.css" new file mode 100644 index 00000000..395f433b --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/app.component.css" @@ -0,0 +1,14 @@ +.my_books{ + width: 770px; + margin: 0 auto; + border: 1px solid rebeccapurple; + border-radius: 10px; + box-sizing: border-box; +} +.my_books_title{ + text-align: center; +} + +.my_books .router{ + text-align: center; +} \ No newline at end of file diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/app.component.html" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/app.component.html" new file mode 100644 index 00000000..9154d026 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/app.component.html" @@ -0,0 +1,12 @@ +
+

欢迎来到我的个人书屋!

+ + + + +
\ No newline at end of file diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/app.component.spec.ts" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/app.component.spec.ts" new file mode 100644 index 00000000..64497cb0 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/app.component.spec.ts" @@ -0,0 +1,31 @@ +import { TestBed, async } from '@angular/core/testing'; +import { AppComponent } from './app.component'; + +describe('AppComponent', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + AppComponent + ], + }).compileComponents(); + })); + + it('should create the app', () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.debugElement.componentInstance; + expect(app).toBeTruthy(); + }); + + it(`should have as title 'books'`, () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.debugElement.componentInstance; + expect(app.title).toEqual('books'); + }); + + it('should render title in a h1 tag', () => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.debugElement.nativeElement; + expect(compiled.querySelector('h1').textContent).toContain('Welcome to books!'); + }); +}); diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/app.component.ts" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/app.component.ts" new file mode 100644 index 00000000..6d034386 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/app.component.ts" @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] +}) +export class AppComponent { + title = 'books'; +} diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/app.module.ts" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/app.module.ts" new file mode 100644 index 00000000..ba73ce1c --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/app.module.ts" @@ -0,0 +1,37 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { HttpClientModule } from '@angular/common/http'; +import { FormsModule } from '@angular/forms'; + +import { AppComponent } from './app.component'; +import { IndexComponent } from './index/index.component'; +import { DetailComponent } from './detail/detail.component'; +import { AppRoutingModule } from './app-routing.module'; +import { BooksComponent } from './books/books.component'; +import { HistoryComponent } from './history/history.component'; + +import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api'; +import { InMemoryDataService } from './in-memory-data.service'; +import { AddComponent } from './add/add.component'; +@NgModule({ + declarations: [ + AppComponent, + IndexComponent, + DetailComponent, + BooksComponent, + HistoryComponent, + AddComponent + ], + imports: [ + BrowserModule, + AppRoutingModule, + HttpClientModule, + HttpClientInMemoryWebApiModule.forRoot( + InMemoryDataService, {dataEncapsulation:false} + ), + FormsModule + ], + providers: [], + bootstrap: [AppComponent] +}) +export class AppModule { } diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/books.service.spec.ts" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/books.service.spec.ts" new file mode 100644 index 00000000..40950f51 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/books.service.spec.ts" @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { BooksService } from './books.service'; + +describe('BooksService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: BooksService = TestBed.get(BooksService); + expect(service).toBeTruthy(); + }); +}); diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/books.service.ts" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/books.service.ts" new file mode 100644 index 00000000..64366e6b --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/books.service.ts" @@ -0,0 +1,75 @@ +import { Injectable } from '@angular/core'; +import { Books } from './books'; +import { BookList } from './mock-books'; +import { Observable, of } from 'rxjs'; +import { HistoryService } from './history.service'; +import { HttpClient, HttpHeaders} from '@angular/common/http'; +import { catchError, map, tap } from 'rxjs/operators'; +import { Options } from 'selenium-webdriver/chrome'; + +const httpOptions = { + headers: new HttpHeaders({ 'Content-Type': 'application/json' }) +} +@Injectable({ + providedIn: 'root' +}) +export class BooksService { + constructor( + private historyservice: HistoryService, + private http: HttpClient + ) { } + private log(histories: string){ + this.historyservice.add(`正在执行:${histories}`) + } + private booksUrl = 'api/books'; // 提供一个API供调用 + // 获取书本列表 + getBookList(): Observable { + return this.http.get(this.booksUrl) + .pipe( + tap( _ => this.log('请求书本数据')), + catchError(this.handleError('getHeroes', [])) + ); + } + // 获取指定id的书本 + getBook(id: number): Books{ + return BookList.find(book => book.id === id) + } + // 获取指定id的书本 + getBooks(id: number): Observable{ + const url = `${this.booksUrl}/${id}`; + return this.http.get(url).pipe( + tap( _ => this.log(`请求书本的id为${id}`)), + catchError(this.handleError(`getBooks请求是id为${id}`)) + ) + } + // 更新书本数据 + updateBooks(books: Books): Observable{ + return this.http.put(this.booksUrl, books, httpOptions).pipe( + tap(_ => this.log(`修改书本的id是${books.id}`)), + catchError(this.handleError(`getBooks请求是id为${books.id}`)) + ) + } + // 添加书本 + addBooks(books: Books): Observable{ + return this.http.post(this.booksUrl, books, httpOptions).pipe( + tap((newBook: Books) => this.log(`新增书本的id为${newBook.id}`)), + catchError(this.handleError('添加新书')) + ); + } + // 删除书本 + deleteBooks(books: Books): Observable{ + const id = books.id; + const url = `${this.booksUrl}/${id}`; + return this.http.delete(url, httpOptions).pipe( + tap(_ => this.log(`删除书本${books.title},id为${books.id}`)), + catchError(this.handleError('删除书本')) + ); + } + + private handleError (operation = 'operation', result?: T) { + return (error: any): Observable => { + this.log(`${operation} 失败: ${error.message}`); // 发出错误通知 + return of(result as T); // 返回空结果避免程序出错 + }; + } +} diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/books.ts" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/books.ts" new file mode 100644 index 00000000..fe4b7954 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/books.ts" @@ -0,0 +1,6 @@ +export class Books { + id: number; + url: string; + title: string; + author: string; +} \ No newline at end of file diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/books/books.component.css" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/books/books.component.css" new file mode 100644 index 00000000..b9c4248f --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/books/books.component.css" @@ -0,0 +1,33 @@ +.books_item{ + width: 116px; + margin: 6px; + padding: 10px; + border: 1px solid green; + display: inline-block; + position: relative; +} +.books_item img{ + width: 100%; + height: 160px; +} +.books_item div{ + margin: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.books_item .title a{ + font-size: 14px; +} +.books_item .author{ + font-size: 13px; +} +.books_item .delete{ + position: absolute; + right: 0; + top: 0; + background: red; + color: #fff; + padding: 0 5px; + cursor: pointer; +} diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/books/books.component.html" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/books/books.component.html" new file mode 100644 index 00000000..2a0bcd67 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/books/books.component.html" @@ -0,0 +1,8 @@ +
+ {{list.id}} + +
{{list.author}}
+ X +
\ No newline at end of file diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/books/books.component.spec.ts" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/books/books.component.spec.ts" new file mode 100644 index 00000000..13067bab --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/books/books.component.spec.ts" @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { BooksComponent } from './books.component'; + +describe('BooksComponent', () => { + let component: BooksComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ BooksComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(BooksComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/books/books.component.ts" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/books/books.component.ts" new file mode 100644 index 00000000..6d91b66b --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/books/books.component.ts" @@ -0,0 +1,32 @@ +import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; +import { Books } from '../books'; +import { BooksService } from '../books.service'; + +@Component({ + selector: 'app-books', + templateUrl: './books.component.html', + styleUrls: ['./books.component.css'] +}) +export class BooksComponent implements OnInit { + @Input() list: Books; + + @Output() + change = new EventEmitter() + + constructor( + private booksservice: BooksService + ) { } + ngOnInit() {} + + getDetailImage(books){ + alert(`正在查看id为${books.id}的大图!`); + } + + delete(books: Books): void { + this.booksservice.deleteBooks(books) + .subscribe(()=>{ + this.change.emit(books); + }); + } + +} diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/detail/detail.component.css" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/detail/detail.component.css" new file mode 100644 index 00000000..8efcf1e1 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/detail/detail.component.css" @@ -0,0 +1,8 @@ +.detail{ + width: 360px; + margin: 0 auto; +} +.detail img{ + width:200px; + height: 300px; +} \ No newline at end of file diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/detail/detail.component.html" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/detail/detail.component.html" new file mode 100644 index 00000000..abe744cc --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/detail/detail.component.html" @@ -0,0 +1,20 @@ +
+

《{{books.title}}》介绍

+
+ +
+

书本标题: {{books.title}}

+

书本作者: {{books.author}}

+

书本id: {{books.id}}

+
+
+

暂无信息

+
+ +
+

修改信息:

+ + +
\ No newline at end of file diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/detail/detail.component.spec.ts" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/detail/detail.component.spec.ts" new file mode 100644 index 00000000..149b9be7 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/detail/detail.component.spec.ts" @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DetailComponent } from './detail.component'; + +describe('DetailComponent', () => { + let component: DetailComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ DetailComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/detail/detail.component.ts" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/detail/detail.component.ts" new file mode 100644 index 00000000..1280cd15 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/detail/detail.component.ts" @@ -0,0 +1,41 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { Location } from '@angular/common'; +import { Books } from '../books'; +import { BooksService } from '../books.service'; +import { HistoryService } from '../history.service'; + +@Component({ + selector: 'app-detail', + templateUrl: './detail.component.html', + styleUrls: ['./detail.component.css'] +}) +export class DetailComponent implements OnInit { + constructor( + private route: ActivatedRoute, + private location: Location, + private booksservice: BooksService, + private historyservice: HistoryService + ) { } + + books: Books; + ngOnInit() { + this.getDetail() + } + getDetail(): void{ + const id = +this.route.snapshot.paramMap.get('id'); + this.getBooks(id); + } + getBooks(id: number): void { + this.books = this.booksservice.getBook(id); + this.historyservice.add(`查看书本${this.books.title},id为${this.books.id}`); + console.log(this.books) + } + save(): void { + this.booksservice.updateBooks(this.books) + .subscribe(() => this.goBack()); + } + goBack(): void { + this.location.back(); + } +} diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/history.service.spec.ts" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/history.service.spec.ts" new file mode 100644 index 00000000..594fa69e --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/history.service.spec.ts" @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { HistoryService } from './history.service'; + +describe('HistoryService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: HistoryService = TestBed.get(HistoryService); + expect(service).toBeTruthy(); + }); +}); diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/history.service.ts" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/history.service.ts" new file mode 100644 index 00000000..6c05ce63 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/history.service.ts" @@ -0,0 +1,16 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class HistoryService { + history: string[] = []; + + add(histories: string){ + this.history.push(histories); + } + clear(){ + this.history = []; + } + constructor() { } +} diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/history/history.component.css" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/history/history.component.css" new file mode 100644 index 00000000..e69de29b diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/history/history.component.html" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/history/history.component.html" new file mode 100644 index 00000000..8d836c75 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/history/history.component.html" @@ -0,0 +1,9 @@ +
+

操作历史:

+
+ +
{{item}}
+
+
\ No newline at end of file diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/history/history.component.spec.ts" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/history/history.component.spec.ts" new file mode 100644 index 00000000..f68be4f2 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/history/history.component.spec.ts" @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HistoryComponent } from './history.component'; + +describe('HistoryComponent', () => { + let component: HistoryComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ HistoryComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(HistoryComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/history/history.component.ts" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/history/history.component.ts" new file mode 100644 index 00000000..50606ddf --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/history/history.component.ts" @@ -0,0 +1,16 @@ +import { Component, OnInit } from '@angular/core'; +import { HistoryService } from '../history.service'; + +@Component({ + selector: 'app-history', + templateUrl: './history.component.html', + styleUrls: ['./history.component.css'] +}) +export class HistoryComponent implements OnInit { + + constructor(private historyservice: HistoryService) { } + + ngOnInit() { + } + +} diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/in-memory-data.service.spec.ts" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/in-memory-data.service.spec.ts" new file mode 100644 index 00000000..a75ef029 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/in-memory-data.service.spec.ts" @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { InMemoryDataService } from './in-memory-data.service'; + +describe('InMemoryDataService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: InMemoryDataService = TestBed.get(InMemoryDataService); + expect(service).toBeTruthy(); + }); +}); diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/in-memory-data.service.ts" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/in-memory-data.service.ts" new file mode 100644 index 00000000..2c84ad77 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/in-memory-data.service.ts" @@ -0,0 +1,76 @@ +import { Injectable } from '@angular/core'; +import { InMemoryDbService } from 'angular-in-memory-web-api'; +import { Books } from './books'; + +@Injectable({ + providedIn: 'root' +}) +export class InMemoryDataService implements InMemoryDbService { + createDb(){ + const books = [ + { + id: 1, + url: 'https://img3.doubanio.com/view/subject/m/public/s29988481.jpg', + title: '像火焰像灰烬', + author: '程姬', + }, + { + id: 2, + url: 'https://img3.doubanio.com/view/subject/m/public/s30002856.jpg', + title: '拜占庭帝国史', + author: '[美] A.A.瓦西列夫', + }, + { + id: 3, + url: 'https://img3.doubanio.com/view/subject/m/public/s30005383.jpg', + title: '吴承恩捉妖记 上', + author: '有时右逝', + }, + { + id: 4, + url: 'https://img3.doubanio.com/view/subject/m/public/s29952612.jpg', + title: '生命是什么', + author: '[以色列]埃迪·普罗斯', + }, + { + id: 5, + url: 'https://img3.doubanio.com/view/subject/m/public/s29965934.jpg', + title: '圆屋', + author: '[美]厄德里克(Louise Erdrich)', + }, + { + id: 6, + url: 'https://img3.doubanio.com/view/subject/m/public/s29960204.jpg', + title: '通识', + author: '日本实业出版社 / [日] 茂木健一郎 主编', + }, + { + id: 7, + url: 'https://img3.doubanio.com/view/subject/m/public/s30002353.jpg', + title: '读心师', + author: '向林', + }, + { + id: 8, + url: 'https://img1.doubanio.com/view/subject/m/public/s29951649.jpg', + title: '微精通', + author: '[英] 罗伯特·特威格尔', + }, + { + id: 9, + url: 'https://img3.doubanio.com/view/subject/m/public/s29958456.jpg', + title: '人生最焦虑的就是吃些什么', + author: '刘汀', + }, + { + id: 10, + url: 'https://img3.doubanio.com/view/subject/m/public/s29906241.jpg', + title: '过剩之地', + author: '[美]莫妮卡·普拉萨德', + }, + ]; + return {books}; + } + constructor() { } + +} diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/index/index.component.css" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/index/index.component.css" new file mode 100644 index 00000000..56a3a8a5 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/index/index.component.css" @@ -0,0 +1,6 @@ +.content{ + width: 100%; + padding: 10px; +} + + diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/index/index.component.html" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/index/index.component.html" new file mode 100644 index 00000000..b83baab5 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/index/index.component.html" @@ -0,0 +1,7 @@ +
+
+ +
+
\ No newline at end of file diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/index/index.component.spec.ts" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/index/index.component.spec.ts" new file mode 100644 index 00000000..03122420 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/index/index.component.spec.ts" @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { IndexComponent } from './index.component'; + +describe('IndexComponent', () => { + let component: IndexComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ IndexComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(IndexComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/index/index.component.ts" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/index/index.component.ts" new file mode 100644 index 00000000..684eabcd --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/index/index.component.ts" @@ -0,0 +1,40 @@ +import { Component, OnInit } from '@angular/core'; +import { Books } from '../books'; +// import { BookList } from '../mock-books'; +import { BooksService } from '../books.service'; +import { HistoryService } from '../history.service'; +import { HttpClient, HttpHeaders} from '@angular/common/http'; + + +@Component({ + selector: 'app-index', + templateUrl: './index.component.html', + styleUrls: ['./index.component.css'] +}) +export class IndexComponent implements OnInit { + // books: Books = { + // id: 1, + // url: 'https://img3.doubanio.com/view/subject/m/public/s29988481.jpg', + // title: '像火焰像灰烬', + // author: '程姬', + // } + books : Books[]; + constructor( + private booksservice: BooksService, + private historyservice: HistoryService, + private http: HttpClient + ) { } + + ngOnInit() { + this.getBooks(); + } + getBooks(): void{ + this.historyservice.add('访问首页书本列表'); + this.booksservice.getBookList() + .subscribe(books => this.books = books); + } + funChange(books, $event){ + this.books = this.books.filter(h => h.id !== books.id); + console.log('ssssssss') + } +} diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/mock-books.ts" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/mock-books.ts" new file mode 100644 index 00000000..4a7863a9 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/app/mock-books.ts" @@ -0,0 +1,64 @@ +import { Books } from './books'; + +export const BookList: Books[] = [ + { + id: 1, + url: 'https://img3.doubanio.com/view/subject/m/public/s29988481.jpg', + title: '像火焰像灰烬', + author: '程姬', + }, + { + id: 2, + url: 'https://img3.doubanio.com/view/subject/m/public/s30002856.jpg', + title: '拜占庭帝国史', + author: '[美] A.A.瓦西列夫', + }, + { + id: 3, + url: 'https://img3.doubanio.com/view/subject/m/public/s30005383.jpg', + title: '吴承恩捉妖记 上', + author: '有时右逝', + }, + { + id: 4, + url: 'https://img3.doubanio.com/view/subject/m/public/s29952612.jpg', + title: '生命是什么', + author: '[以色列]埃迪·普罗斯', + }, + { + id: 5, + url: 'https://img3.doubanio.com/view/subject/m/public/s29965934.jpg', + title: '圆屋', + author: '[美]厄德里克(Louise Erdrich)', + }, + { + id: 6, + url: 'https://img3.doubanio.com/view/subject/m/public/s29960204.jpg', + title: '通识', + author: '日本实业出版社 / [日] 茂木健一郎 主编', + }, + { + id: 7, + url: 'https://img3.doubanio.com/view/subject/m/public/s30002353.jpg', + title: '读心师', + author: '向林', + }, + { + id: 8, + url: 'https://img1.doubanio.com/view/subject/m/public/s29951649.jpg', + title: '微精通', + author: '[英] 罗伯特·特威格尔', + }, + { + id: 9, + url: 'https://img3.doubanio.com/view/subject/m/public/s29958456.jpg', + title: '人生最焦虑的就是吃些什么', + author: '刘汀', + }, + { + id: 10, + url: 'https://img3.doubanio.com/view/subject/m/public/s29906241.jpg', + title: '过剩之地', + author: '[美]莫妮卡·普拉萨德', + }, +] \ No newline at end of file diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/assets/.gitkeep" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/assets/.gitkeep" new file mode 100644 index 00000000..e69de29b diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/browserslist" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/browserslist" new file mode 100644 index 00000000..37371cb0 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/browserslist" @@ -0,0 +1,11 @@ +# This file is currently used by autoprefixer to adjust CSS to support the below specified browsers +# For additional information regarding the format and rule options, please see: +# https://github.com/browserslist/browserslist#queries +# +# For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed + +> 0.5% +last 2 versions +Firefox ESR +not dead +not IE 9-11 \ No newline at end of file diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/environments/environment.prod.ts" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/environments/environment.prod.ts" new file mode 100644 index 00000000..3612073b --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/environments/environment.prod.ts" @@ -0,0 +1,3 @@ +export const environment = { + production: true +}; diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/environments/environment.ts" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/environments/environment.ts" new file mode 100644 index 00000000..7b4f817a --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/environments/environment.ts" @@ -0,0 +1,16 @@ +// This file can be replaced during build by using the `fileReplacements` array. +// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. +// The list of file replacements can be found in `angular.json`. + +export const environment = { + production: false +}; + +/* + * For easier debugging in development mode, you can import the following file + * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. + * + * This import should be commented out in production mode because it will have a negative impact + * on performance if an error is thrown. + */ +// import 'zone.js/dist/zone-error'; // Included with Angular CLI. diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/favicon.ico" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/favicon.ico" new file mode 100644 index 00000000..8081c7ce Binary files /dev/null and "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/favicon.ico" differ diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/index.html" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/index.html" new file mode 100644 index 00000000..d2020127 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/index.html" @@ -0,0 +1,14 @@ + + + + + Books + + + + + + + + + diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/karma.conf.js" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/karma.conf.js" new file mode 100644 index 00000000..b6e00421 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/karma.conf.js" @@ -0,0 +1,31 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage-istanbul-reporter'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + coverageIstanbulReporter: { + dir: require('path').join(__dirname, '../coverage'), + reports: ['html', 'lcovonly'], + fixWebpackSourcePaths: true + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false + }); +}; \ No newline at end of file diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/main.ts" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/main.ts" new file mode 100644 index 00000000..28bfa9e1 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/main.ts" @@ -0,0 +1,13 @@ +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic().bootstrapModule(AppModule) + .catch(err => console.error(err)); + diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/polyfills.ts" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/polyfills.ts" new file mode 100644 index 00000000..d310405a --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/polyfills.ts" @@ -0,0 +1,80 @@ +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), + * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. + * + * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** IE9, IE10 and IE11 requires all of the following polyfills. **/ +// import 'core-js/es6/symbol'; +// import 'core-js/es6/object'; +// import 'core-js/es6/function'; +// import 'core-js/es6/parse-int'; +// import 'core-js/es6/parse-float'; +// import 'core-js/es6/number'; +// import 'core-js/es6/math'; +// import 'core-js/es6/string'; +// import 'core-js/es6/date'; +// import 'core-js/es6/array'; +// import 'core-js/es6/regexp'; +// import 'core-js/es6/map'; +// import 'core-js/es6/weak-map'; +// import 'core-js/es6/set'; + +/** IE10 and IE11 requires the following for NgClass support on SVG elements */ +// import 'classlist.js'; // Run `npm install --save classlist.js`. + +/** IE10 and IE11 requires the following for the Reflect API. */ +// import 'core-js/es6/reflect'; + + +/** Evergreen browsers require these. **/ +// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. +import 'core-js/es7/reflect'; + + +/** + * Web Animations `@angular/platform-browser/animations` + * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. + * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). + **/ +// import 'web-animations-js'; // Run `npm install --save web-animations-js`. + +/** + * By default, zone.js will patch all possible macroTask and DomEvents + * user can disable parts of macroTask/DomEvents patch by setting following flags + */ + + // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame + // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick + // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames + + /* + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + */ +// (window as any).__Zone_enable_cross_context_check = true; + +/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ +import 'zone.js/dist/zone'; // Included with Angular CLI. + + + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/styles.css" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/styles.css" new file mode 100644 index 00000000..90d4ee00 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/styles.css" @@ -0,0 +1 @@ +/* You can add global styles to this file, and also import other style files */ diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/test.ts" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/test.ts" new file mode 100644 index 00000000..16317897 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/test.ts" @@ -0,0 +1,20 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/dist/zone-testing'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + +declare const require: any; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting() +); +// Then we find all the tests. +const context = require.context('./', true, /\.spec\.ts$/); +// And load the modules. +context.keys().map(context); diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/tsconfig.app.json" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/tsconfig.app.json" new file mode 100644 index 00000000..190fd300 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/tsconfig.app.json" @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/app", + "types": [] + }, + "exclude": [ + "test.ts", + "**/*.spec.ts" + ] +} diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/tsconfig.spec.json" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/tsconfig.spec.json" new file mode 100644 index 00000000..de773363 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/tsconfig.spec.json" @@ -0,0 +1,18 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/spec", + "types": [ + "jasmine", + "node" + ] + }, + "files": [ + "test.ts", + "polyfills.ts" + ], + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/tslint.json" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/tslint.json" new file mode 100644 index 00000000..52e2c1a5 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/src/tslint.json" @@ -0,0 +1,17 @@ +{ + "extends": "../tslint.json", + "rules": { + "directive-selector": [ + true, + "attribute", + "app", + "camelCase" + ], + "component-selector": [ + true, + "element", + "app", + "kebab-case" + ] + } +} diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/tsconfig.json" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/tsconfig.json" new file mode 100644 index 00000000..916247e4 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/tsconfig.json" @@ -0,0 +1,21 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "sourceMap": true, + "declaration": false, + "module": "es2015", + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "target": "es5", + "typeRoots": [ + "node_modules/@types" + ], + "lib": [ + "es2017", + "dom" + ] + } +} diff --git "a/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/tslint.json" "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/tslint.json" new file mode 100644 index 00000000..6ddb6b29 --- /dev/null +++ "b/Cute-Angular/books\351\241\271\347\233\256demo/books_angular/tslint.json" @@ -0,0 +1,131 @@ +{ + "rulesDirectory": [ + "node_modules/codelyzer" + ], + "rules": { + "arrow-return-shorthand": true, + "callable-types": true, + "class-name": true, + "comment-format": [ + true, + "check-space" + ], + "curly": true, + "deprecation": { + "severity": "warn" + }, + "eofline": true, + "forin": true, + "import-blacklist": [ + true, + "rxjs/Rx" + ], + "import-spacing": true, + "indent": [ + true, + "spaces" + ], + "interface-over-type-literal": true, + "label-position": true, + "max-line-length": [ + true, + 140 + ], + "member-access": false, + "member-ordering": [ + true, + { + "order": [ + "static-field", + "instance-field", + "static-method", + "instance-method" + ] + } + ], + "no-arg": true, + "no-bitwise": true, + "no-console": [ + true, + "debug", + "info", + "time", + "timeEnd", + "trace" + ], + "no-construct": true, + "no-debugger": true, + "no-duplicate-super": true, + "no-empty": false, + "no-empty-interface": true, + "no-eval": true, + "no-inferrable-types": [ + true, + "ignore-params" + ], + "no-misused-new": true, + "no-non-null-assertion": true, + "no-redundant-jsdoc": true, + "no-shadowed-variable": true, + "no-string-literal": false, + "no-string-throw": true, + "no-switch-case-fall-through": true, + "no-trailing-whitespace": true, + "no-unnecessary-initializer": true, + "no-unused-expression": true, + "no-use-before-declare": true, + "no-var-keyword": true, + "object-literal-sort-keys": false, + "one-line": [ + true, + "check-open-brace", + "check-catch", + "check-else", + "check-whitespace" + ], + "prefer-const": true, + "quotemark": [ + true, + "single" + ], + "radix": true, + "semicolon": [ + true, + "always" + ], + "triple-equals": [ + true, + "allow-null-check" + ], + "typedef-whitespace": [ + true, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + } + ], + "unified-signatures": true, + "variable-name": false, + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ], + "no-output-on-prefix": true, + "use-input-property-decorator": true, + "use-output-property-decorator": true, + "use-host-property-decorator": true, + "no-input-rename": true, + "no-output-rename": true, + "use-life-cycle-interface": true, + "use-pipe-transform-interface": true, + "component-class-suffix": true, + "directive-class-suffix": true + } +} diff --git "a/Cute-Angular/\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/README.md" "b/Cute-Angular/\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/README.md" new file mode 100644 index 00000000..02f4e27c --- /dev/null +++ "b/Cute-Angular/\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206/README.md" @@ -0,0 +1,33 @@ +Angular知识点整理,内容以Angular4实战课程中整理: + +1. 程序架构 +* 组件:Angular基本构建块,是一段含业务逻辑和数据的html +* 服务:封装可重用的业务逻辑 +* 指令:允许向html元素添加指定行为 +* 模块:将不同部分组成一个单元 + + +2. 组件相关概念 +Component 必备: +* 装饰器 @Component() 告知Angular如何处理类,它包含的值叫**元数据**,根据元数据来渲染和展示组件。 +@叫装饰器,@Component()叫组件元数据装饰器 + +* 模版 Template +* 控制器 Controller 包含绝大多数页面逻辑 + +可选的可注入对象: +* 输入属性 @Imports() 组件之间传递数据 +* 提供器 providers 依赖注入 +* 生命周期钩子 Lifecycle Hooks + +可选的输出对象: +* 输出属性 @Outputs +* 样式表 styles +* 动画 Animations +* 生命周期钩子 Lifecycle Hooks + +@NgModule: +* declatations 模块包含的内容,只能组件指令和管道 +* imports 组件依赖的模块 +* providers 模块提供的服务 +* bootstrap 模块的主组件 \ No newline at end of file diff --git a/Cute-Article/README.md b/Cute-Article/README.md new file mode 100644 index 00000000..65c41316 --- /dev/null +++ b/Cute-Article/README.md @@ -0,0 +1,116 @@ + +## 关于作者 +[![博客](http://images.pingan8787.com/icon_my1.png)](http://www.pingan8787.com) +[![语雀](http://images.pingan8787.com/assets/icon_26_yuque.png)](https://www.yuque.com/wangpingan/cute-frontend) +[![知乎](http://images.pingan8787.com/icon_zhihu1.png)](https://zhuanlan.zhihu.com/cute-javascript) +[![掘金](http://images.pingan8787.com/icon_juejin2.png)](https://juejin.im/user/586fc337a22b9d0058807d53/posts) +[![思否](http://images.pingan8787.com/icon_sf1.png)](https://segmentfault.com/blog/pingan8787) +[![CSDN](http://images.pingan8787.com/icon_csdn1.png)](https://blog.csdn.net/qq_36380426) +[![简书](http://images.pingan8787.com/icon_jianshu1.png)](https://www.jianshu.com/u/2ec5d94afd60) + + +完整知识库,请查看我的【**语雀知识库**】,阅读体验更好。[💌跳转~](https://www.yuque.com/wangpingan/cute-frontend) + +## 目录 + +### 0、前端工程化 + +* [74-《大前端工程化的时实践和思考》狼叔分享](https://github.com/pingan8787/Leo-JavaScript/blob/master/Cute-Article/article/74-%E3%80%8A%E5%A4%A7%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96%E7%9A%84%E6%97%B6%E5%AE%9E%E8%B7%B5%E5%92%8C%E6%80%9D%E8%80%83%E3%80%8B%E7%8B%BC%E5%8F%94%E5%88%86%E4%BA%AB.md) + +### 1、JS业务逻辑实现 +* [1-JavaScript实现页面防抖](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/1-JavaScript%E5%AE%9E%E7%8E%B0%E9%A1%B5%E9%9D%A2%E9%98%B2%E6%8A%96.md) +* [2-同步返回ajax请求结果方法](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/2-%E5%90%8C%E6%AD%A5%E8%BF%94%E5%9B%9Eajax%E8%AF%B7%E6%B1%82%E7%BB%93%E6%9E%9C%E6%96%B9%E6%B3%95.md) +* [★ 9-常用业务模块代码整理](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/9-%E5%B8%B8%E7%94%A8%E4%B8%9A%E5%8A%A1%E6%A8%A1%E5%9D%97%E4%BB%A3%E7%A0%81%E6%95%B4%E7%90%86.md) +* [12-javascript开发的一些简写技巧](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/12-javascript%E5%BC%80%E5%8F%91%E7%9A%84%E4%B8%80%E4%BA%9B%E7%AE%80%E5%86%99%E6%8A%80%E5%B7%A7.md) +* [15-精心收集的48个JavaScript片段,简单理解](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/15-%E7%B2%BE%E5%BF%83%E6%94%B6%E9%9B%86%E7%9A%8448%E4%B8%AAJavaScript%E7%89%87%E6%AE%B5%EF%BC%8C%E7%AE%80%E5%8D%95%E7%90%86%E8%A7%A3.md) +* [29-关于随机数的一些总结](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/29-%E5%85%B3%E4%BA%8E%E9%9A%8F%E6%9C%BA%E6%95%B0%E7%9A%84%E4%B8%80%E4%BA%9B%E6%80%BB%E7%BB%93.md) + +### 2、VueJS +* [4-基于Vue配置axios](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/4-%E5%9F%BA%E4%BA%8EVue%E9%85%8D%E7%BD%AEaxios.md) +* [★ 5-[原创]VUE中实现通用js函数库封装](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/5-%5B%E5%8E%9F%E5%88%9B%5DVUE%E4%B8%AD%E5%AE%9E%E7%8E%B0%E9%80%9A%E7%94%A8js%E5%87%BD%E6%95%B0%E5%BA%93%E5%B0%81%E8%A3%85.md) +* [★ 7-[原创]缩小Vuejs打包体积方法](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/7-%5B%E5%8E%9F%E5%88%9B%5D%E7%BC%A9%E5%B0%8FVuejs%E6%89%93%E5%8C%85%E4%BD%93%E7%A7%AF%E6%96%B9%E6%B3%95.md) +* [14-Vue的一些小注意点](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/14-Vue%E7%9A%84%E4%B8%80%E4%BA%9B%E5%B0%8F%E6%B3%A8%E6%84%8F%E7%82%B9.md) +* [★ 24-Vue折腾记-给Axios做个挺靠谱的封装(报错,鉴权,跳转,拦截,提示)](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/24-Vue%E6%8A%98%E8%85%BE%E8%AE%B0-%E7%BB%99Axios%E5%81%9A%E4%B8%AA%E6%8C%BA%E9%9D%A0%E8%B0%B1%E7%9A%84%E5%B0%81%E8%A3%85%EF%BC%88%E6%8A%A5%E9%94%99%2C%E9%89%B4%E6%9D%83%2C%E8%B7%B3%E8%BD%AC%2C%E6%8B%A6%E6%88%AA%2C%E6%8F%90%E7%A4%BA%EF%BC%89.md) +* [40-解密Vue SSR](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/40-%E8%A7%A3%E5%AF%86Vue%20SSR.md) +* [49-Vue 面试中常问知识点整理](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/49-Vue%20%E9%9D%A2%E8%AF%95%E4%B8%AD%E5%B8%B8%E9%97%AE%E7%9F%A5%E8%AF%86%E7%82%B9%E6%95%B4%E7%90%86.md) + +### 3、ES6/ES7/ES8... +* [3-Promise简单用法](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/3-Promise%E7%AE%80%E5%8D%95%E7%94%A8%E6%B3%95.md) +* [13-ES7和ES8的一点新东西](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/13-ES7%E5%92%8CES8%E7%9A%84%E4%B8%80%E7%82%B9%E6%96%B0%E4%B8%9C%E8%A5%BF.md) +* [19-ES6的7个实用技巧](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/19-ES6%E7%9A%847%E4%B8%AA%E5%AE%9E%E7%94%A8%E6%8A%80%E5%B7%A7.md) +* [★ 31-ES6这些就够了](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/31-ES6%E8%BF%99%E4%BA%9B%E5%B0%B1%E5%A4%9F%E4%BA%86.md) +* [★ 34-我眼中的async&await](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/34-%E6%88%91%E7%9C%BC%E4%B8%AD%E7%9A%84async%26await.md) +* [35-ES6中的模块导入导出整理](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/35-ES6%E4%B8%AD%E7%9A%84%E6%A8%A1%E5%9D%97%E5%AF%BC%E5%85%A5%E5%AF%BC%E5%87%BA%E6%95%B4%E7%90%86.md) +* [41-ES2018(ES9)的新特性](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/41-ES2018%EF%BC%88ES9%EF%BC%89%E7%9A%84%E6%96%B0%E7%89%B9%E6%80%A7.md) +* [63-ES6汇总.md](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/63-ES6%E6%B1%87%E6%80%BB.md) +* [64-ES7汇总.md](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/64-ES7%E6%B1%87%E6%80%BB.md) +* [65-ES8汇总.md](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/65-ES8%E6%B1%87%E6%80%BB.md) +* [66-ES9汇总.md](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/66-ES9%E6%B1%87%E6%80%BB.md) + +### 4、Webpack +* [8-vue-cli2的webpack配置分析](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/8-vue-cli2%E7%9A%84webpack%E9%85%8D%E7%BD%AE%E5%88%86%E6%9E%90.md) +* [25-Webpack入门教程整理(整理中)](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/25-Webpack%E5%85%A5%E9%97%A8%E6%95%99%E7%A8%8B%E6%95%B4%E7%90%86%EF%BC%88%E6%95%B4%E7%90%86%E4%B8%AD%EF%BC%89.md) +* [26-Webpack常用配置整理(整理中)](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/26-Webpack%E5%B8%B8%E7%94%A8%E9%85%8D%E7%BD%AE%E6%95%B4%E7%90%86%EF%BC%88%E6%95%B4%E7%90%86%E4%B8%AD%EF%BC%89.md) +* [75-Webpack怎么运行?](https://github.com/pingan8787/Leo-JavaScript/blob/master/Cute-Article/article/75-Webpack%E6%80%8E%E4%B9%88%E8%BF%90%E8%A1%8C%EF%BC%9F.md) +* [77-一看就懂之webpack高级配置与优化](https://github.com/pingan8787/Leo-JavaScript/blob/master/Cute-Article/article/77-%E4%B8%80%E7%9C%8B%E5%B0%B1%E6%87%82%E4%B9%8Bwebpack%E9%AB%98%E7%BA%A7%E9%85%8D%E7%BD%AE%E4%B8%8E%E4%BC%98%E5%8C%96.md) + +### 5、WebSocket +* [18-websocket常用demo](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/18-websocket%E5%B8%B8%E7%94%A8demo.md) +* [★ 20-WebSocket重新学习](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/20-WebSocket%E9%87%8D%E6%96%B0%E5%AD%A6%E4%B9%A0.md) + +### 6、细节知识点 +* [6-关于js的作用域和声明提前](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/6-%E5%85%B3%E4%BA%8Ejs%E7%9A%84%E4%BD%9C%E7%94%A8%E5%9F%9F%E5%92%8C%E5%A3%B0%E6%98%8E%E6%8F%90%E5%89%8D.md) +* [★ 10-知识点整理1](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/10-%E7%9F%A5%E8%AF%86%E7%82%B9%E6%95%B4%E7%90%861.md) +* [16-带你理解 JS 容易出错的坑和细节](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/16-%E5%B8%A6%E4%BD%A0%E7%90%86%E8%A7%A3%20JS%20%E5%AE%B9%E6%98%93%E5%87%BA%E9%94%99%E7%9A%84%E5%9D%91%E5%92%8C%E7%BB%86%E8%8A%82.md) +* [17-TypeScript和 JavaScript 深度对比](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/17-TypeScript%E5%92%8C%20JavaScript%20%E6%B7%B1%E5%BA%A6%E5%AF%B9%E6%AF%94.md) +* [★ 21-JavaScript异步机制详解](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/21-JavaScript%E5%BC%82%E6%AD%A5%E6%9C%BA%E5%88%B6%E8%AF%A6%E8%A7%A3.md) +* [22-JavaScript中有趣的区分同步和异步Ajax](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/22-JavaScript%E4%B8%AD%E6%9C%89%E8%B6%A3%E7%9A%84%E5%8C%BA%E5%88%86%E5%90%8C%E6%AD%A5%E5%92%8C%E5%BC%82%E6%AD%A5Ajax.md) +* [23-JavaScript八张思维导图](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/23-JavaScript%E5%85%AB%E5%BC%A0%E6%80%9D%E7%BB%B4%E5%AF%BC%E5%9B%BE.md) +* [28-JavaScript中的void运算符](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/28-JavaScript%E4%B8%AD%E7%9A%84void%E8%BF%90%E7%AE%97%E7%AC%A6.md) +* [31-聊一聊JavaScript的IIFE](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/32-%E8%81%8A%E4%B8%80%E8%81%8AJavaScript%E7%9A%84IIFE.md) +* [33-javascript的纯函数](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/33-javascript%E7%9A%84%E7%BA%AF%E5%87%BD%E6%95%B0.md) +* [36-好好学习toLocaleString方法](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/36-%E5%A5%BD%E5%A5%BD%E5%AD%A6%E4%B9%A0toLocaleString%E6%96%B9%E6%B3%95.md) +* [37-JavaScript事件委托详解](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/37-JavaScript%E4%BA%8B%E4%BB%B6%E5%A7%94%E6%89%98%E8%AF%A6%E8%A7%A3.md) +* [38-JavaScript中常见设计模式](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/38-JavaScript%E4%B8%AD%E5%B8%B8%E8%A7%81%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md) +* [42-JS高程中的垃圾回收机制与常见内存泄露的解决方法](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/42-JS%E9%AB%98%E7%A8%8B%E4%B8%AD%E7%9A%84%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6%E4%B8%8E%E5%B8%B8%E8%A7%81%E5%86%85%E5%AD%98%E6%B3%84%E9%9C%B2%E7%9A%84%E8%A7%A3%E5%86%B3%E6%96%B9%E6%B3%95.md) +* [43-手机端页面开发常见问题和解决](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/43-%E6%89%8B%E6%9C%BA%E7%AB%AF%E9%A1%B5%E9%9D%A2%E5%BC%80%E5%8F%91%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98%E5%92%8C%E8%A7%A3%E5%86%B3.md) +* [44-前端本地文件操作和上传](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/44-%E5%89%8D%E7%AB%AF%E6%9C%AC%E5%9C%B0%E6%96%87%E4%BB%B6%E6%93%8D%E4%BD%9C%E5%92%8C%E4%B8%8A%E4%BC%A0.md) +* [45-js中reduce的神奇用法](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/45-js%E4%B8%ADreduce%E7%9A%84%E7%A5%9E%E5%A5%87%E7%94%A8%E6%B3%95.md) +* [46-在JavaScript中更好的使用数组](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/46-%E5%9C%A8JavaScript%E4%B8%AD%E6%9B%B4%E5%A5%BD%E7%9A%84%E4%BD%BF%E7%94%A8%E6%95%B0%E7%BB%84.md) +* [48-js获取元素高度和浏览器各种高度方法汇总](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/48-js获取元素高度和浏览器各种高度方法汇总.md) +* [50-js中get和post的区别](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/50-js%E4%B8%ADget%E5%92%8Cpost%E7%9A%84%E5%8C%BA%E5%88%AB.md) +* [52-前端模块化(CommonJs,AMD和CMD)](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/52-%E5%89%8D%E7%AB%AF%E6%A8%A1%E5%9D%97%E5%8C%96%EF%BC%88CommonJs%2CAMD%E5%92%8CCMD%EF%BC%89.md) +* [53-js中call和apply和bind方法介绍](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/53-js%E4%B8%ADcall%E5%92%8Capply%E5%92%8Cbind%E6%96%B9%E6%B3%95%E4%BB%8B%E7%BB%8D.md) +* [54-ajax详解](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/54-ajax%E8%AF%A6%E8%A7%A3.md) +* [55-JS中attribute和property区别](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/55-JS%E4%B8%ADattribute%E5%92%8Cproperty%E5%8C%BA%E5%88%AB.md) +* [56-js中原型继承原理](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/56-js%E4%B8%AD%E5%8E%9F%E5%9E%8B%E7%BB%A7%E6%89%BF%E5%8E%9F%E7%90%86.md) +* [58-详解HTML5data-自定义属性](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/58-%E8%AF%A6%E8%A7%A3HTML5data-%E8%87%AA%E5%AE%9A%E4%B9%89%E5%B1%9E%E6%80%A7.md) +* [59-前端HTML5几种存储方式的总结](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/59-%E5%89%8D%E7%AB%AFHTML5%E5%87%A0%E7%A7%8D%E5%AD%98%E5%82%A8%E6%96%B9%E5%BC%8F%E7%9A%84%E6%80%BB%E7%BB%93.md) +* [60-懒加载和预加载](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/60-%E6%87%92%E5%8A%A0%E8%BD%BD%E5%92%8C%E9%A2%84%E5%8A%A0%E8%BD%BD.md) +* [61-JS中this的4种绑定规则](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/61-JS%E4%B8%ADthis%E7%9A%844%E7%A7%8D%E7%BB%91%E5%AE%9A%E8%A7%84%E5%88%99.md) +* [67-JS箭头函数的适用和不适用的场景.md](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/67-JS%E7%AE%AD%E5%A4%B4%E5%87%BD%E6%95%B0%E7%9A%84%E9%80%82%E7%94%A8%E5%92%8C%E4%B8%8D%E9%80%82%E7%94%A8%E7%9A%84%E5%9C%BA%E6%99%AF.md) +* [68-创建对象的七种方式](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/68-%E5%88%9B%E5%BB%BA%E5%AF%B9%E8%B1%A1%E7%9A%84%E4%B8%83%E7%A7%8D%E6%96%B9%E5%BC%8F.md) +* [69-秒懂this](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/69-秒懂this.md) +* [70-JS复杂判断的更优雅写法](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/70-JS复杂判断的更优雅写法.md) +* [71-复习instanceof运算符](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/71-%E5%A4%8D%E4%B9%A0instanceof%E8%BF%90%E7%AE%97%E7%AC%A6.md) +* [72-【重温基础】JS中的高阶函数](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/72-%E3%80%90%E9%87%8D%E6%B8%A9%E5%9F%BA%E7%A1%80%E3%80%91JS%E4%B8%AD%E7%9A%84%E9%AB%98%E9%98%B6%E5%87%BD%E6%95%B0.md) +* [78-【JavaScript】模块化方案总结](https://github.com/pingan8787/Leo-JavaScript/blob/master/Cute-Article/article/78-JavaScript%20%E6%A8%A1%E5%9D%97%E5%8C%96%E6%96%B9%E6%A1%88%E6%80%BB%E7%BB%93.md) + +### 7、HTTP +* [47-http请求头与响应头的应用](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/47-http%E8%AF%B7%E6%B1%82%E5%A4%B4%E4%B8%8E%E5%93%8D%E5%BA%94%E5%A4%B4%E7%9A%84%E5%BA%94%E7%94%A8.md) +* [51-Apache之HTTP协议](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/51-Apache%E4%B9%8BHTTP%E5%8D%8F%E8%AE%AE.md) + +### 8、正则表达式 +* [30-一次记住js的6个正则方法](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/30-%E4%B8%80%E6%AC%A1%E8%AE%B0%E4%BD%8Fjs%E7%9A%846%E4%B8%AA%E6%AD%A3%E5%88%99%E6%96%B9%E6%B3%95.md) +* [11-20个超级常用的正则表达式](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/11-20%E4%B8%AA%E8%B6%85%E7%BA%A7%E5%B8%B8%E7%94%A8%E7%9A%84%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F.md) +* [57-ES5ES6正则表达式总结](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/57-ES5ES6%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%80%BB%E7%BB%93.md) + +### 9、面试题 +* [62-2018各大公司近期面试题](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/62-2018%E5%90%84%E5%A4%A7%E5%85%AC%E5%8F%B8%E8%BF%91%E6%9C%9F%E9%9D%A2%E8%AF%95%E9%A2%98.md) + +### 10、React +* [79-在React中使用ShadowDOM](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/79-在React中使用ShadowDOM.md) + +### 11、其他 +* [★ 27-Markdowm语法整理](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/27-Markdowm%E8%AF%AD%E6%B3%95%E6%95%B4%E7%90%86.md) +* [39-作为前端需要了解的开源协议知识](https://github.com/pingan8787/Leo-JavaScript/blob/master/article/39-%E4%BD%9C%E4%B8%BA%E5%89%8D%E7%AB%AF%E9%9C%80%E8%A6%81%E4%BA%86%E8%A7%A3%E7%9A%84%E5%BC%80%E6%BA%90%E5%8D%8F%E8%AE%AE%E7%9F%A5%E8%AF%86.md) diff --git "a/1-JavaScript\345\256\236\347\216\260\351\241\265\351\235\242\351\230\262\346\212\226.md" "b/Cute-Article/article/1-JavaScript\345\256\236\347\216\260\351\241\265\351\235\242\351\230\262\346\212\226.md" similarity index 100% rename from "1-JavaScript\345\256\236\347\216\260\351\241\265\351\235\242\351\230\262\346\212\226.md" rename to "Cute-Article/article/1-JavaScript\345\256\236\347\216\260\351\241\265\351\235\242\351\230\262\346\212\226.md" diff --git "a/10-\347\237\245\350\257\206\347\202\271\346\225\264\347\220\2061.md" "b/Cute-Article/article/10-\347\237\245\350\257\206\347\202\271\346\225\264\347\220\2061.md" similarity index 96% rename from "10-\347\237\245\350\257\206\347\202\271\346\225\264\347\220\2061.md" rename to "Cute-Article/article/10-\347\237\245\350\257\206\347\202\271\346\225\264\347\220\2061.md" index fe117fcf..44ed17a6 100644 --- "a/10-\347\237\245\350\257\206\347\202\271\346\225\264\347\220\2061.md" +++ "b/Cute-Article/article/10-\347\237\245\350\257\206\347\202\271\346\225\264\347\220\2061.md" @@ -400,7 +400,27 @@ console.log(samesums[0]); // 1 > * `defer`并行加载js文件,会按照页面上`script`标签的顺序执行 > * `async`并行加载js文件,下载完成立即执行,不会按照页面上`script`标签的顺序执行 +### 13、检测属性 +用于判断对象是否存在某个属性: +```js +let a = { + x : 11, + y : 22 +} +``` +> 1.通过 in 运算符判断: +```js +'x' in a ; // true +'z' in a ; // false +``` +> 2.通过hasOwnProperty()方法判断: +```js +a.hasOwnProperty("x") ; //true : a有一个自有属性x,若是继承属性,返回false +``` +> 3.更便捷 !== undefined: +```js +a.x !== undefined ;//true: a 中有属性 x +``` - -### 最近更新 2018.04.09 +### 最近更新 2018.05.17 回到顶部 [介绍](#介绍) diff --git "a/11-20\344\270\252\350\266\205\347\272\247\345\270\270\347\224\250\347\232\204\346\255\243\345\210\231\350\241\250\350\276\276\345\274\217.md" "b/Cute-Article/article/11-20\344\270\252\350\266\205\347\272\247\345\270\270\347\224\250\347\232\204\346\255\243\345\210\231\350\241\250\350\276\276\345\274\217.md" similarity index 100% rename from "11-20\344\270\252\350\266\205\347\272\247\345\270\270\347\224\250\347\232\204\346\255\243\345\210\231\350\241\250\350\276\276\345\274\217.md" rename to "Cute-Article/article/11-20\344\270\252\350\266\205\347\272\247\345\270\270\347\224\250\347\232\204\346\255\243\345\210\231\350\241\250\350\276\276\345\274\217.md" diff --git "a/12-javascript\345\274\200\345\217\221\347\232\204\344\270\200\344\272\233\347\256\200\345\206\231\346\212\200\345\267\247.md" "b/Cute-Article/article/12-javascript\345\274\200\345\217\221\347\232\204\344\270\200\344\272\233\347\256\200\345\206\231\346\212\200\345\267\247.md" similarity index 100% rename from "12-javascript\345\274\200\345\217\221\347\232\204\344\270\200\344\272\233\347\256\200\345\206\231\346\212\200\345\267\247.md" rename to "Cute-Article/article/12-javascript\345\274\200\345\217\221\347\232\204\344\270\200\344\272\233\347\256\200\345\206\231\346\212\200\345\267\247.md" diff --git "a/13-ES7\345\222\214ES8\347\232\204\344\270\200\347\202\271\346\226\260\344\270\234\350\245\277.md" "b/Cute-Article/article/13-ES7\345\222\214ES8\347\232\204\344\270\200\347\202\271\346\226\260\344\270\234\350\245\277.md" similarity index 92% rename from "13-ES7\345\222\214ES8\347\232\204\344\270\200\347\202\271\346\226\260\344\270\234\350\245\277.md" rename to "Cute-Article/article/13-ES7\345\222\214ES8\347\232\204\344\270\200\347\202\271\346\226\260\344\270\234\350\245\277.md" index 67c3854a..e5431109 100644 --- "a/13-ES7\345\222\214ES8\347\232\204\344\270\200\347\202\271\346\226\260\344\270\234\350\245\277.md" +++ "b/Cute-Article/article/13-ES7\345\222\214ES8\347\232\204\344\270\200\347\202\271\346\226\260\344\270\234\350\245\277.md" @@ -55,8 +55,8 @@ Object.keys(obj).forEach((key) => // Object.keys() 对象=>数组 }); ``` -2-不使用ES8 -使用Object.values()遍历对象的属性值,无需使用使用属性名: +2-使用ES8 +使用Object.values()遍历对象的属性值,无需使用属性名: ```js let obj = {a: 1, b: 2, c: 3} Object.values(obj).forEach(value => @@ -77,8 +77,8 @@ Object.keys(obj).forEach((key) => }) ``` -2-不使用ES8 -使用Object.values()遍历对象的属性值,无需使用使用属性名: +2-使用ES8 +使用Object.entries()遍历对象的属性值,无需使用属性名: ```js let obj = {a: 1, b: 2, c: 3}; Object.entries(obj).forEach(([key, value]) => @@ -90,14 +90,13 @@ Object.entries(obj).forEach(([key, value]) => ### 5、ES8 - padStart() 在字符串前面填充指定的字符串。 1-不使用ES8 -使用Object.keys()遍历对象的属性名和属性值: ```js console.log('0.00') // 0.00 console.log('10,000.00') // 10,000.00 console.log('250,000.00') // 250,000.00 ``` -2-不使用ES8 +2-使用ES8 使用padStart()可以在字符串前面填充指定的字符串: ```js console.log('0.00'.padStart(20)) // 0.00 @@ -108,14 +107,13 @@ console.log('250,000.00'.padStart(20)) // 250,000.00 ### 6、ES8 - padEnd() 在字符串后面填充指定的字符串。 1-不使用ES8 -使用Object.keys()遍历对象的属性名和属性值: ```js console.log('0.00 ' + '0.00' ) // 0.00 0.00 console.log('10,000.00 ' + '10,000.00' ) // 10,000.00 10,000.00 console.log('250,000.00 ' + '250,000.00') // 250,000.00 250,000.00 ``` -2-不使用ES8 +2-使用ES8 使用padEnd()可以在字符串后面填充指定的字符串: ```js console.log('0.00'.padEnd(20) + '0.00' ) // 0.00 0.00 @@ -181,4 +179,4 @@ fetchData(query).then(data => this.props.processfetchedData(data) }) ``` -`Async/Await` 是写异步代码的新方式,以前的方法有 `回调函数` 和 `Promise` 。相比于 `Promise` ,它更加简洁,并且处理错误、条件语句、中间值都更加方便,因此有望替代 `Promise` ,成为新一代的一步代码编写方式。 \ No newline at end of file +`Async/Await` 是写异步代码的新方式,以前的方法有 `回调函数` 和 `Promise` 。相比于 `Promise` ,它更加简洁,并且处理错误、条件语句、中间值都更加方便,因此有望替代 `Promise` ,成为新一代的一步代码编写方式。 diff --git "a/14-Vue\347\232\204\344\270\200\344\272\233\345\260\217\346\263\250\346\204\217\347\202\271.md" "b/Cute-Article/article/14-Vue\347\232\204\344\270\200\344\272\233\345\260\217\346\263\250\346\204\217\347\202\271.md" similarity index 100% rename from "14-Vue\347\232\204\344\270\200\344\272\233\345\260\217\346\263\250\346\204\217\347\202\271.md" rename to "Cute-Article/article/14-Vue\347\232\204\344\270\200\344\272\233\345\260\217\346\263\250\346\204\217\347\202\271.md" diff --git "a/15-\347\262\276\345\277\203\346\224\266\351\233\206\347\232\20448\344\270\252JavaScript\347\211\207\346\256\265\357\274\214\347\256\200\345\215\225\347\220\206\350\247\243.md" "b/Cute-Article/article/15-\347\262\276\345\277\203\346\224\266\351\233\206\347\232\20448\344\270\252JavaScript\347\211\207\346\256\265\357\274\214\347\256\200\345\215\225\347\220\206\350\247\243.md" similarity index 100% rename from "15-\347\262\276\345\277\203\346\224\266\351\233\206\347\232\20448\344\270\252JavaScript\347\211\207\346\256\265\357\274\214\347\256\200\345\215\225\347\220\206\350\247\243.md" rename to "Cute-Article/article/15-\347\262\276\345\277\203\346\224\266\351\233\206\347\232\20448\344\270\252JavaScript\347\211\207\346\256\265\357\274\214\347\256\200\345\215\225\347\220\206\350\247\243.md" diff --git "a/16-\345\270\246\344\275\240\347\220\206\350\247\243 JS \345\256\271\346\230\223\345\207\272\351\224\231\347\232\204\345\235\221\345\222\214\347\273\206\350\212\202.md" "b/Cute-Article/article/16-\345\270\246\344\275\240\347\220\206\350\247\243 JS \345\256\271\346\230\223\345\207\272\351\224\231\347\232\204\345\235\221\345\222\214\347\273\206\350\212\202.md" similarity index 100% rename from "16-\345\270\246\344\275\240\347\220\206\350\247\243 JS \345\256\271\346\230\223\345\207\272\351\224\231\347\232\204\345\235\221\345\222\214\347\273\206\350\212\202.md" rename to "Cute-Article/article/16-\345\270\246\344\275\240\347\220\206\350\247\243 JS \345\256\271\346\230\223\345\207\272\351\224\231\347\232\204\345\235\221\345\222\214\347\273\206\350\212\202.md" diff --git "a/17-TypeScript\345\222\214 JavaScript \346\267\261\345\272\246\345\257\271\346\257\224.md" "b/Cute-Article/article/17-TypeScript\345\222\214 JavaScript \346\267\261\345\272\246\345\257\271\346\257\224.md" similarity index 100% rename from "17-TypeScript\345\222\214 JavaScript \346\267\261\345\272\246\345\257\271\346\257\224.md" rename to "Cute-Article/article/17-TypeScript\345\222\214 JavaScript \346\267\261\345\272\246\345\257\271\346\257\224.md" diff --git "a/18-websocket\345\270\270\347\224\250demo.md" "b/Cute-Article/article/18-websocket\345\270\270\347\224\250demo.md" similarity index 100% rename from "18-websocket\345\270\270\347\224\250demo.md" rename to "Cute-Article/article/18-websocket\345\270\270\347\224\250demo.md" diff --git "a/19-ES6\347\232\2047\344\270\252\345\256\236\347\224\250\346\212\200\345\267\247.md" "b/Cute-Article/article/19-ES6\347\232\2047\344\270\252\345\256\236\347\224\250\346\212\200\345\267\247.md" similarity index 100% rename from "19-ES6\347\232\2047\344\270\252\345\256\236\347\224\250\346\212\200\345\267\247.md" rename to "Cute-Article/article/19-ES6\347\232\2047\344\270\252\345\256\236\347\224\250\346\212\200\345\267\247.md" diff --git "a/2-\345\220\214\346\255\245\350\277\224\345\233\236ajax\350\257\267\346\261\202\347\273\223\346\236\234\346\226\271\346\263\225.md" "b/Cute-Article/article/2-\345\220\214\346\255\245\350\277\224\345\233\236ajax\350\257\267\346\261\202\347\273\223\346\236\234\346\226\271\346\263\225.md" similarity index 100% rename from "2-\345\220\214\346\255\245\350\277\224\345\233\236ajax\350\257\267\346\261\202\347\273\223\346\236\234\346\226\271\346\263\225.md" rename to "Cute-Article/article/2-\345\220\214\346\255\245\350\277\224\345\233\236ajax\350\257\267\346\261\202\347\273\223\346\236\234\346\226\271\346\263\225.md" diff --git "a/20-WebSocket\351\207\215\346\226\260\345\255\246\344\271\240.md" "b/Cute-Article/article/20-WebSocket\351\207\215\346\226\260\345\255\246\344\271\240.md" similarity index 100% rename from "20-WebSocket\351\207\215\346\226\260\345\255\246\344\271\240.md" rename to "Cute-Article/article/20-WebSocket\351\207\215\346\226\260\345\255\246\344\271\240.md" diff --git "a/2017-\345\205\250\345\271\264\346\200\273\347\273\223.md" "b/Cute-Article/article/2017-\345\205\250\345\271\264\346\200\273\347\273\223.md" similarity index 100% rename from "2017-\345\205\250\345\271\264\346\200\273\347\273\223.md" rename to "Cute-Article/article/2017-\345\205\250\345\271\264\346\200\273\347\273\223.md" diff --git "a/Cute-Article/article/2018-\345\205\250\345\271\264\346\200\273\347\273\223.md" "b/Cute-Article/article/2018-\345\205\250\345\271\264\346\200\273\347\273\223.md" new file mode 100644 index 00000000..3867c47b --- /dev/null +++ "b/Cute-Article/article/2018-\345\205\250\345\271\264\346\200\273\347\273\223.md" @@ -0,0 +1,202 @@ +**** +|Author|王平安| +|---|---| +|E-mail|pingan8787@qq.com| +|博 客|www.pingan8787.com| +|微 信|pingan8787| +|每日文章推荐|https://github.com/pingan8787/Leo_Reading/issues| +|JS小册|js.pingan8787.com| +**** + +今年一整年,有这么几个感悟: +> 我相信每个人都是幸运,而且聪明。 +> 厚积才能薄发,基础牢固才能做好。 +> 做出改变,做好调整,接受挑战,对自己有好处。 +> 自律很重要。 + +# 零、瞎聊瞎感慨 +2018年,我很高兴,体重比和一个朋友打赌说的那个数字多了15斤,哇。 +2018年,我也很难过,离开了一群很棒的小伙伴,哎。 + +这一年,我买了好几本新书,但都没有看完。 +这一年,我独自去了很多地方,锻炼自己的内心。 + +程序员第二年,我边放边学了深度学习,有点时间不够。 +程序员第二年,我可以总结文章,和同事朋友聊代码聊网络聊计算机原理算法(也是皮毛)。 + +当了第26年的儿子和弟弟,我多往母亲的卡里打了很多钱(我的能力范围内),让家人省心但也让家人操心了。 +当了第26年的儿子和弟弟,我更多的替哥哥姐姐着想,也更清楚家的重要。 + +离开第三年,思想总在围城,但总有一件事能让人彻底释放。 +离开第三年,清空以后,发现我应该我可以做更多事情。 + +这一年的其他角色,有好有坏,愿随时间长河,消逝,冲散,堆积,沉淀。 + +# 一、2018计划完成情况 +开始写这篇总结的时候,我翻了一下 [49-【总结】2017全年总结](http://pingan8787.com/2018/02/21/49-%E3%80%90%E6%80%BB%E7%BB%93%E3%80%912017%E5%85%A8%E5%B9%B4%E6%80%BB%E7%BB%93/) 也深深回忆了一下,感叹,感谢你们。 + +我忐忑的打开 [49-【总结】2018全年计划](pingan8787.com/2018/02/21/49-【总结】2018全年计划/),总觉得,这一年,这么快,也这么慢。 + +## 1.1 2018计划概述 +大概整理一些: +**学习上** +* 前端: +前端知识的基础打牢固,学习React框架,深入研究Vuejs源码,和其他NPM包。 + +* 后端: +学习Nodejs,从Express/Koa去实践,Mongodb也要研究和运用。 + +* 客户端开发: +移动端APP从RN和Weex入手,桌面端放弃掉。 + +* 博客: +改版和模块进来,Github和掘金,开始发原创和维护自己的项目。 + +* 机器学习: +深入去学习这块,可以从图像识别入手,另外可以考虑物联网方面。 + +* 计算机研究: +从一本书开始研究计算机原理,《算法导论》去学习算法,适当学习计算机通信知识。 + +**生活上** +* 日常生活: +换个新宿舍,自己学着做健康早餐,考完驾照,和女票游玩一次。 + +* 健康生活: +多跑步,研究健康饮食。 + +* 家庭生活: +替母亲分担,争取过年给更大礼物,帮家里重新装修,多和哥哥姐姐分享交流,找个女票。 + + +## 1.2 2018计划统计 +检查了下 **2018全年计划** 的完成情况,有些高兴,有些失望,也有些欣慰。 +先来一张github的contributions图: + +![github2018](http://images.pingan8787.com/github2018.png) + +**学习上** +* 前端: +前端基础知识已经复习起来了,在掘金整理了一个系列的文章,还在更新中。 +[pingan8787 掘金](https://juejin.im/user/586fc337a22b9d0058807d53/posts) 和 +[【复习资料】ES6/ES7/ES8/ES9资料整理(个人整理)](https://juejin.im/post/5c02b106f265da61764aa0c1) +这两块大的文章。 +React有学习一些,Vuejs源码也有研究一些,但是现在的工作,用的都是Angular,所以React和Vuejs对于我而言更多的意义是参考和学习。 +NPM包,今年就研究了lodash的源码。 + +* 后端: +`Nodejs`和`Mongodb`开始学习,有用了`Express`和`Mongodb`做了个自己用的小记事本,源码放在 [小小日记本](https://github.com/pingan8787/Leo_Nodejs/tree/master/express/express%2Bmongoose%20%E6%97%A5%E8%AE%B0%E6%9C%AC) 里面,功能比较简单,以后还会开发新的项目。 + +* 客户端开发: +移动端APP:RN研究过一点皮毛,但是如今主攻`flutter`这个非常棒,也要学习`dart`,桌面端放弃掉。 + +* 博客: +博客:添加了【微博】【评论】【样式】等一些东西。 +模块:添加了【ES规范小册】,后面还会有【ES基础】【ES面试】等。 +Github掘金简书思否:开始在同步自己的原创文章,目前主要两块[pingan8787 掘金](https://juejin.im/user/586fc337a22b9d0058807d53/posts) 和 +[【复习资料】ES6/ES7/ES8/ES9资料整理(个人整理)](https://juejin.im/post/5c02b106f265da61764aa0c1) ,也有一些其他的。 +另外我的[每日文章](https://github.com/pingan8787/Leo_Reading/issues)系列也坚持了2018年完整一年了。 + +* 机器学习: +深度学习这块,今年算是从入门到入门吧,后面真的是没有时间去研究,也没有机会去实战,导致`python`的也不熟练,如果接下来还继续,会考虑`JavaScript`实现。但是原理和高数这些,还是得实打实。 + +* 计算机研究: +今年通过网易云课堂学了一些大学计算机基础的知识,了解了很多原理层面的知识,不过还要再深入点。 + +**生活上** +* 日常生活: +宿舍还是没搬,在这里离公司比较近,而且房东一家都很好,觉得可以再住一年。 +健康早餐,一直在坚持煮粥或者燕麦或者核桃粉,搭配面包或包子,感觉还是很棒的。 +女票木有,驾照科三挂了就没去了,因为烦也因为不好请假那么多天事情也多。 + +* 健康生活: +好像今年,跑的真的很少,主要是骑行,30多公里是最多的。 + +* 家庭生活: +借着让母亲帮忙存钱的理由,往家里多寄了更多钱,也存好给母亲买礼物的钱。 +家里重新装修好了,家电设备也换了新的了。 +哥哥姐姐交流更多,也更多的互相理解。 + +# 二、2018总结和分析 +总结和分析,按照以前的框架,总结了下这一年: + +## 2.1 最正确的一件事 +在这一年,做正确的事情,很多,但不可否认,在我内心认为,做得最重要的事情,是来到现在的公司。 +写下这句,心里有点惭愧,但确实是如此,我追求技能提高,技术进步,价值提升,我追求事业追求理想。 +我在WLHD很开心,也很自豪,一群小伙伴,每个人都这么的友善和友好,感谢每个人也很感谢镇智的亦师亦友。 +![WLHD生日蛋糕](http://images.pingan8787.com/2018%E6%80%BB%E7%BB%93-%E8%9B%8B%E7%B3%95.png) + +之所以作为最正确的一件事情,是因为进入到EXE的三个月,我真的学习到很多,跟我自己理想的,计划的是一致的,总结有这几方面: +* 公司主项目,是我所喜欢的,是用户使用频率高,贴近生活的,有前景和价值的,是正导向的。 +* 公司福利,比较符合我的想法。 +* 项目技术栈,是`Angular`这个我爱恨交加的框架,虽然现在我负责的还是用1的版本,但是`Angular`确实是个好家伙,国外大厂都喜欢,并且项目也混合这其他技术进来,好像还有一些我不知道的,也有一些未来的尝试(具体涉及到的名称我删除掉了)。 +* 团队协作,是`git流`,这真的是超级棒,代码管理起来非常方便(虽然刚开始用的时候踩了很多坑),公司用的`JIRA`也特别棒。 +* 团队氛围,开发职责分明,有大牛,还有宝哥这个超神的人,每周二周四晚上的技术分享会,技术氛围特别好。 + +在公司每次和同事,特别是宝哥,都会给我们讲很多很棒的知识,大到前沿技术小到源码阅读等,受到宝哥的影响,我开始尝试学习和分享,坚持下来。感觉很棒。 +家里没有矿也没有油,我只能激励自己,做得更好,希望路过我的世界的你们,都能好,能更好。 + +## 2.2 最错误的一件事 +可能,没有最,因为我回忆了好久,没有想到最错误。 +但有一件事,我心里比较清楚,只是有点愧疚,不想写,愿这样做,会是做好的结果。 + +## 2.3 最疯狂的一件事 +这必须是**世界杯**,哈哈,四年一届,2014年的世界杯,我还在实习,不得不说时间过得真的快。 +这届世界杯,我差不多看了35场左右,基本都是半夜12点 2点 4点开始的球赛,有时候一个晚上2场,直接看到早上,并且第二天要上班,然而我还能精神,也是不容易。 +这届世界杯,喜怒哀乐都有,花了点小球买了球,德国的早早淘汰,中国的迟迟观望,法国克罗地亚的超常发挥,日本韩国的眼前一亮,西班牙葡萄牙的惊险刺激,还有一些熟悉的名字:莫德里奇 C罗 梅西 姆巴佩 凯恩 库尔图瓦 萨拉赫 久巴 苏亚雷斯 小豌豆 佩佩 德赫亚 伊涅斯塔 吉鲁 格里兹曼 阿圭罗 伊瓜因 等等等等,太多了太棒了。 +我也在想,下届世界杯,那时候的我,会是神马样? + +## 2.4 最开心的一件事 +还是和去年一样,一整年下来,大大小小的开心事,贼多呢,but,最开心,应该是和我哥一起做了一个小项目,一个很有意思的项目。 +这个项目我们两个人,一个出想法,一个敲代码,我感觉很棒,和哥哥齐心协力,赚钱,兄弟应该是这样的。 +虽然呢,赚不多,但是,是好的开始,兄弟应该一起加油的。 +现在也是经常跟我哥一起聊一些赚钱的项目,聊一些我们的人生观价值观等,对一些事情的看法,更加互相了解,我哥是我的榜样。 + +## 2.5 2018工作学习上 +现在是北京时间(2019.01.06 02:14),可能写完会更晚。 +工作上分为两块: +* WLHD期间: +**第一阶段:** +人工智能研究,还在为人工智能深度学习挥洒汗水,虽然工作上没有实际用到,但是更重要的意义在于,让我拓展了视野,知道更广阔的计算机世界,还有知识与实践结合,也清楚了人工智能对于未来的意义所在。 +**第二阶段:** +公司旧项目维护和新项目开展,大约有半年左右,这段时间,对于后台管理系统的完善,我有个更清晰的认识,就是对于开发后台管理系统,要多考虑数据默认值,搜索性能,路由安全权限,组件模块化。对于公司人才的招聘,我也是有些自己的感想,由于团队小,招聘进来的人,需要性格跟团队合得来,技术上需要看得到热情。 +**第三阶段:** +项目转型,转型往往会使人一下子难以适应,产生迷茫,自己该坚持的事情依然坚持,但环境却在变化,变化着,有时候转过身来才发现,原来自己再原地走。 + +* EXE期间: +**第一阶段:** +适应期,这段时间有各种不适应,也担心出错,也积极去融入团队和公司,这段时间经历了尴尬的团队成员的认识,经历了公司的羽毛球比赛,经历了合并掉其他同事的代码,经历了发版之前的代码大修改,经历了好多次的通宵,经历了一次严重的感冒,经历... +**第二阶段:** +稳定成长期,这段时间算开始稳定和熟悉了,日常工作也开始正常,也跟着公司的脚步开始学习一些新技术,特别是`flutter`,真香。 +现在也更懂得项目的团队协作方式,并且这是非常重要的,好比前面写到的。 + +学习上: +* 今年也参加了好几个前端技术分享会,确实,大公司大团队新技术,这些都是让我大开眼界,更加坚信前端的未来前景。 +* 这一年学习的新知识可以说是内容多范围广,大到人工智能机器学习,小到冒泡算法等,学的越多,越需要总结和记录。 + +## 2.6 2018感情上 +这一年的感情生活,相比去年,有进步,被困扰的感情,已经都放开了,放过彼此,我也过得更好,对于自己内心的想法,还是早点告知免得浪费对方时间。出现在我的世界的人,依然在。 +这一年,似乎对于找女朋友的看法,也在转变,有很多的应该,也有很多的至少,这算是一些条件。 +反正,对我来说,能聊的开心,这是重要的。 + +## 2.7 2018养成新习惯 +这一年新养成的一些习惯,相比去年,增加了几个,也时常告诉着自己,自律自律自律: +* 第一个,学习上,开始去按照一个系列的内容,去系统的学习和回顾知识,这包括我整理下来的[pingan8787 掘金](https://juejin.im/user/586fc337a22b9d0058807d53/posts) 和 +[【复习资料】ES6/ES7/ES8/ES9资料整理(个人整理)](https://juejin.im/post/5c02b106f265da61764aa0c1) 这两个系列,按照系列的课程系统学习,是一种循序渐进的过程,才能深入去学习。现在的系统学习,我更偏向于,学习+记录,将复习和学习完的知识点,按照自己的逻辑写一遍,补充完善,成为自己的东西,也方便以后查阅。 +* 第二个,迁移自己的开发环境,慢慢把自己开发环境往Mac迁移,因为公司给的电脑是Win的额,只能用自己的Mac系统,不过确实Mac很适合我们前端开发呢。 +* 第三个,更注重健康生活,每天必须坚持做的几个:早上不会着急马上起床,必须伸懒腰全身舒展后休息一会才起床,毕竟急急忙忙起床会影响一天的工作情绪;早起必须做早操,活动下全身,很棒的感觉;晚上睡觉前必须做几组俯卧撑,已经是习惯了。 +* 第四个,坚持公众号推送,不为给别人看,只为自己做积累。 +## 2.8 2018小小欣慰的事 +我更愿意,花时间去看一些书,虽然是后半年才开始,新加入两本书《南方高速》和《地球上最后的夜晚》,似乎境界还不足理解作者真正或深入的内涵,但看书,确实能让人静心,养性,抛开外面的纷繁世界,沉浸自我。 + + +# 完、再来几句 +原来,总结就是个回忆的过程,各种感受都会有,相信我们现在走的每一步,都是我们的成长,以后成为自己喜欢的那个人。 +这一年,也更加相信“你在成就公司,公司也在成就你”这句话了。 + +Tips:我用Markdown也算是熟练一些了,比起去年总结的排版好看多了呢。 + + + + +完:2019.01.06 12:39 \ No newline at end of file diff --git "a/2018-\345\205\250\345\271\264\350\256\241\345\210\222.md" "b/Cute-Article/article/2018-\345\205\250\345\271\264\350\256\241\345\210\222.md" similarity index 59% rename from "2018-\345\205\250\345\271\264\350\256\241\345\210\222.md" rename to "Cute-Article/article/2018-\345\205\250\345\271\264\350\256\241\345\210\222.md" index fbccd84b..07403f46 100644 --- "a/2018-\345\205\250\345\271\264\350\256\241\345\210\222.md" +++ "b/Cute-Article/article/2018-\345\205\250\345\271\264\350\256\241\345\210\222.md" @@ -22,35 +22,43 @@ # 1、工作 今年我会换一份工作,公司偏向正常化,项目正常化,也许我还是比较实在,很多现在公司在做的项目和方向我并不喜欢,有各种担心。 我想说现在的工作,公司的方向不明确项目不靠谱,画饼太多真实性差,同事积极性差,唯我难以带起所有人,我已经很努力做好我本职,包括不是本职我也努力帮忙,努力维护军心,但是依旧难以调动公司氛围,于自身能力也好,于职位有碍也好,我认为公司的Leader必须要有几点,非常重要(个人想法): -* 1.决策力,在该做决定时能有决策能力; -* 2.影响力,能让自己(积极工作等)影响到其他人; -* 3.执行力,在确定的项目上不轻易放弃一个项目,一个idea,一句title; + +1. 决策力,在该做决定时能有决策能力; +2. 影响力,能让自己(积极工作等)影响到其他人; +3. 执行力,在确定的项目上不轻易放弃一个项目,一个idea,一句title; + 其他还有一个,大概意思就是Leader需要对员工负责,关心员工自身发展。 这一年,我想继续试试心目中比较向往的那家公司,一年多的沉淀也许技术上不是非常牛逼,但是我想继续学习更多知识,学习更多项目开发的经验,项目管理等,若能成我将在此潜心学习修行很长时间,我不喜欢现在的虚虚实实现在公司的迷茫,性格问题,我想接触更多有意思的人和事。 # 2、学习 通过去年一年的各种踩坑试错,慢慢总结出自己的一套学习方法,不能说高效,但适合现在的自己,2018年继续按照这套学习方法,学习更多知识,踩更多坑,并总结起来,优化调整学习方法。 经过去年的尝试和踩坑,2018年,是时候该好好学习下面几个内容: -* 前端知识: -* 1. 前端基础:HTML/CSS/JavaScript更多基础,将原生知识基础打牢固,还有ES6/7/8/9研究; -* 2. 前端框架:React需要开始学习,Vuejs深入源码学习更多原理知识,还有很多不错的NPM包; -* 后端知识: -* 1. 服务端开发:Nodejs(包含Express/koa等框架)是时候开始研究并运用了; -* 2. 数据库开发:Mongodb(包含Mongoose)也需要开始研究和运用了; -* 客户端开发: -* 1. APP开发:React Native或者Weex,今年应该从这两个方向入手APP开发,开发环境也将开始转向Mac开发; -* 2. 桌面应用程序开发:Electorn可以用,但是我放弃桌面应用程序开发,no why 就是不喜欢; -* 博客: -* 1. 个人博客:需要做改版和调整,添加新标签进来; -* 2. github:几个仓库和issue继续维护,也许会再添加一个自己项目的仓库; -* 3. 掘金:开始尝试发表自己的原创文章; -* 机器学习: -* 1. 深度学习:找个方向深入学习,可能会是图像识别; -* 2. 物联网:我有兴趣; -* 计算机研究: -* 1. 计算机原理:这是我一直想好好研究的内容,去年碍于各种问题未能开展,今年从一本书入手研究; -* 2. 计算机算法:算法真的很有意思,《算法导论》很棒但很难,尝试静心研究下去; -* 3. 计算机通信:反正很棒,暂未想好如何研究; + +**前端知识**: +1. 前端基础:HTML/CSS/JavaScript更多基础,将原生知识基础打牢固,还有ES6/7/8/9研究; +2. 前端框架:React需要开始学习,Vuejs深入源码学习更多原理知识,还有很多不错的NPM包; + +**后端知识**: +1. 服务端开发:Nodejs(包含Express/koa等框架)是时候开始研究并运用了; +2. 数据库开发:Mongodb(包含Mongoose)也需要开始研究和运用了; + +**客户端开发**: +1. APP开发:React Native或者Weex,今年应该从这两个方向入手APP开发,开发环境也将开始转向Mac开发; +2. 桌面应用程序开发:Electorn可以用,但是我放弃桌面应用程序开发,no why 就是不喜欢; + +**博客**: +1. 个人博客:需要做改版和调整,添加新标签进来; +2. github:几个仓库和issue继续维护,也许会再添加一个自己项目的仓库; +3. 掘金:开始尝试发表自己的原创文章; + +**机器学习**: +1. 深度学习:找个方向深入学习,可能会是图像识别; +2. 物联网:我有兴趣; + +**计算机研究**: +1. 计算机原理:这是我一直想好好研究的内容,去年碍于各种问题未能开展,今年从一本书入手研究; +2. 计算机算法:算法真的很有意思,《算法导论》很棒但很难,尝试静心研究下去; +3. 计算机通信:反正很棒,暂未想好如何研究; 差不多这样,满满一整年的任务,还是很重的,毕竟今年是工作上开始转向成熟稳定的一年。 > 这里插入一件小事,一个朋友再向我讨教一个框架的问题,这个框架我花了很多时间研究,ta说我怎么这么厉害懂这么多,我说多花时间研究就懂啦,ta的回答有点出乎我的意料,说真的,我的下巴都快掉了,ta说(大概意思):“我除了上班没事做才研究,不然都没时间研究这些,下班了谁还敲代码!”,这句话当时真心吓到我了,我总结的是:“出门在外学习知识是为了提升自身价值,并非被工作所左右,想要学习知识你就会想办法主动学习,主动自觉的学习!”。说完,便不想回复ta微信,因为发现跟ta认识到现在,基本能了解这个人,抱怨太多而不脚踏实地研究学习。 @@ -59,18 +67,21 @@ # 3.生活 从前年至今,我经历了最苦逼的日子(真正是快吃不起饭的日子),经历了最迷茫混沌的日子(离职和学习的迷茫),经历了最黑暗的加班赶项目的日子(第一次每天加班到晚上十点多周末无休),经历了感情上最纠缠的日子(难以割舍的真正初恋的她),还经历了最奋斗的日子(人工智能深度学习让我真的着迷)等等,这一年的神奇竟然我是都撑了下来,自己的一次成长,看待这些事情更多了一份成熟。 2018年生活规划: -* 日常生活: -* 1. 住宿条件:换一个新的安静点的房间,为以后女朋友一起住做准备; -* 2. 住宿生活:尽量自己煮饭,外卖真心难吃且油腻不健康,早餐一定自己煮,健康且营养; -* 3. 驾驶证:6月份前考完; -* 4. 关于游玩:跟女朋友游玩一次,任性,哈哈; -* 健康生活: -* 1. 跑步健身:6月份左右或者提前,担心自己程序猿坐久了身体出问题,而且我也挺喜欢跑步,放空自己; -* 2. 饮食健康:早餐食谱研究一套,或者在现在的基础丰富,午餐公司,晚餐尽量更好点,夜宵感觉不需要,怕变胖就成中年大叔了; -* 家庭生活: -* 1. 母亲:替母亲分担重任,每个月打更多钱回家,过年买一条比今年大一倍的金项链什么的给母亲,过年红包也会更大,今年还要帮母亲把家里重新装修; -* 2. 女朋友:感情需要更加深入,也许来我这边一起工作生活,彼此磨合,更加了解对方,然后带她去看更大的世界; -* 3. 哥哥姐姐:帮他们分担更多烦恼,多跟他们交流,帮他们做更多我办得到的事情还要我的小外甥,常回家看看; + +**日常生活**: +1. 住宿条件:换一个新的安静点的房间,为以后女朋友一起住做准备; +2. 住宿生活:尽量自己煮饭,外卖真心难吃且油腻不健康,早餐一定自己煮,健康且营养; +3. 驾驶证:6月份前考完; +4. 关于游玩:跟女朋友游玩一次,任性,哈哈; + +**健康生活**: +1. 跑步健身:6月份左右或者提前,担心自己程序猿坐久了身体出问题,而且我也挺喜欢跑步,放空自己; +2. 饮食健康:早餐食谱研究一套,或者在现在的基础丰富,午餐公司,晚餐尽量更好点,夜宵感觉不需要,怕变胖就成中年大叔了; + +**家庭生活**: +1. 母亲:替母亲分担重任,每个月打更多钱回家,过年买一条比今年大一倍的金项链什么的给母亲,过年红包也会更大,今年还要帮母亲把家里重新装修; +2. 女朋友:感情需要更加深入,也许来我这边一起工作生活,彼此磨合,更加了解对方,然后带她去看更大的世界; +3. 哥哥姐姐:帮他们分担更多烦恼,多跟他们交流,帮他们做更多我办得到的事情还要我的小外甥,常回家看看; 2018全年规划基本如此,需要让自己将24小时过成26小时。 去年一年的沉淀,让我看来很多事情更有自己的考虑和考量,2018继续加油,脚踏实地,不忘初心,为自己为家庭也为公司,继续加油!Love coding , Love life . diff --git "a/Cute-Article/article/2019-\345\205\250\345\271\264\346\200\273\347\273\223.md" "b/Cute-Article/article/2019-\345\205\250\345\271\264\346\200\273\347\273\223.md" new file mode 100644 index 00000000..a21c91f4 --- /dev/null +++ "b/Cute-Article/article/2019-\345\205\250\345\271\264\346\200\273\347\273\223.md" @@ -0,0 +1,136 @@ + + + +回顾这一年,几句总结: +> 再忙也要留点学习进步的时间; +> 再小的事,都是未来的基石; + +## 零、与人为善 + + +## 一、2019 计划完成情况 + +每年总结,写到回顾去年计划完成情况,各种事情便仿佛在眼前一闪而过。翻起去年[《130-【总结】2019全年计划》](http://pingan8787.com/2019/01/12/130-%E3%80%90%E6%80%BB%E7%BB%93%E3%80%912019%E5%85%A8%E5%B9%B4%E8%AE%A1%E5%88%92/),隐约闻到时间的味道,还有太阳的味道,不知你们会不会这样觉得。 + +先放几张这一年的代码 push 记录,累加起来接近一整年啦: + +* **Github**: + + + +* **Gitlab**: + + + +* **Gitee**: + + + + +### 1. 回顾学习规划 + +回顾今年学习规划,学习的知识范围更集中了,也做到知识沉淀,将沉淀的知识转化为自己的文章,并同步到各个前端技术社区、[博客](htt://www.pingan8787.com)和[语雀](https://www.yuque.com/wangpingan/cute-frontend)上。 + +下面将几部分内容分别总结下: + +* **前端知识** + +前端基础知识,整理了包括 JavaScript / ECAMScript / TypeScript / HTTP / 数据结构与算法 / 正则表达式 / Hybrid / 设计模式 等一系列基础入门知识的文章。 + +前端框架知识,整理了包括 Webpack / Angular / GraphQL / React / MobX 几个框架的基础入门知识的文章。 + +今年也学习很多其他知识,包括 JSBridge / 前端原生端交互 / ionic-native 原理 / Auto2 / CSRF和CORS / Restful等等等等。 + +很多已经汇总在[《Cute-Frontend》](https://www.yuque.com/wangpingan/cute-frontend)中。 + +* **后端知识** + +学习 Eggjs + Mongodb + Redis ,并开发团队内部项目网盘项目,说真的也是学习了很多后端开发的思维。 +另外今年也有使用 Nodejs 写了几个简单工具,如:Fiddler 请求接口时间统计工具 / Excel 公众号数据清洗工具。 + +* **个人品牌** + +公众号【前端自习课】:今年满勤,截止今天已经连续推送 461 篇(含删除/未获得白名单的转载/发重复),粉丝数涨了约 11 倍(从2019/7/24开始),阅读量稳定 160+(自然)。 + +个人博客:将自己在其他社区发的文章都同步到博客,原创文章 57 篇,未达标。 + +其他社区:今年推送的文章分享的平台包括:[掘金](https://juejin.im/user/586fc337a22b9d0058807d53/posts)、[思否](https://segmentfault.com/blog/pingan8787)、[知乎](https://zhuanlan.zhihu.com/cute-javascript)、[CSDN](https://blog.csdn.net/qq_36380426)、[语雀](https://www.yuque.com/wangpingan/cute-frontend)、[简书](https://www.jianshu.com/u/2ec5d94afd60)、[慕课手记](https://www.imooc.com/u/3591250/articles)还有自己[博客](http://www.pingan8787.com)和 [github](https://github.com/pingan8787)。掘金粉丝 4429 未达标,点赞数 8862 也未达标。 + +[Github Reading](https://github.com/pingan8787/Leo_Reading/issues):今年满勤,截止这个月已经连续打卡 30 个月了。 + +### 2. 回顾工作规划 + +工作上,对于迭代需求任务,已经养成开发前核对需求,提测前核对UI稿和原型稿这样的习惯,几次迭代任务实现 0 BUG。 + +这一年也向团队前端学习了很多,为人处世,工作方式,工作技能等等。对于公司产品,今年积极参与各种运营活动(学习打卡、点赞活动、学习任务等),也发表一些自己对产品的看法,两个专栏号持续输出中。 + +### 3. 回顾生活规划 + +生活的规划中,2019年,熬夜少了很多,基本12点准时关电脑睡觉,除了自己喜欢的球赛啦~ + +锻炼这方面也是陆陆续续,唯一做得好的可能就是坚持自己做早饭,牛奶燕麦,白米粥,绿豆粥,燕麦粥,烤面包,煎蛋,水煮蛋,培根鸡蛋堡,今年的早餐都是这些,也比较清淡。 + +宿舍没换,不过住得舒适呀,另外独自去了很多地方,骑行 1100+ 公里: + + + +达成厦门岛换到骑行🚴的个人记录: + + + + +## 二、2019 总结和分析 + +老套路,继续按照每个类型选一件事情来说: + +### 2019 最正确的一件事 + +回顾这一整年,发生的事情真的很多,做得正确的事情也很多,从对于职业生涯和自身性格出发,我觉得2019我做得最正确的一件事是**12月份去杭州参加语雀知识大会**,我也总结一篇[《北上 寻“雀”》](https://www.yuque.com/wangpingan/cute-frontend/uf68aa)。 + +这趟三天旅行,是我独自一人制定所有行程和计划,我逛了阿里巴巴、支付宝、蚂蚁金服、湖畔花园这些互联网传奇的地方,也去了西湖、雷峰塔、钱塘江大桥、京杭大运河、浙江大学、黄龙洞等等。 + +也面基玉伯大佬,并拿到签名与合影,😄哈哈。 + + + +这次行程,很大程度拓展我的视野,一些只在互联网上存在的庞然大物,原来也是离我这么近的,并且今年有一次蚂蚁金服的面试,虽然挂了,还是自身本领不够。 + +很多时候,人的眼界格局一下子产生变换,很多失败,我都将之视为经验,未来,再来。 + +### 2019 最错误的一件事 + +这应该算是那次顶撞母亲的事情,被催相亲催到发脾气,顶撞母亲,实在惭愧惭愧,虽然已经和母亲交谈,但心里还是挺惭愧。 + +### 2019 最开心的一件事 + +当然属于公众号【前端自习课】,全年满勤,截止今天已经连续推送 461 篇(含删除/未获得白名单的转载/发重复),粉丝数涨了约 11 倍(从2019/7/24开始),阅读量稳定 160+(自然)。 + +公众号的初衷,是想**看看自己能坚持做这件事情做多久**,后面发现,做这个事情特别有意义,对我对他人都是。 + +对我而已,是自己学习的素材库,坚持的动力,也是和圈子交流的话题。 + +对他人而言,这也许是他们发现宝藏的时刻。 + +* **[【前端自习课】的2019](https://cu2019.newrank.cn/h5.html?n=5903fb7)** + +2019.07.24 开始正式向外部宣传,至今收获 2300+ 粉丝,一个 100+ 人微信群。 + +累计 3W 阅读量,693 个在看,362 天发布, 404 篇文章, 89 篇原创 ,267W 字... + +![fe-study01.png](https://blog.pingan8787.com/EveryYear/2019Summary/fe-study01.png) + +另外今年也将为【前端自习课】开发了[文章分类大全,点击链接即可体验](https://blog.pingan8787.com/weixin/index.html),另外也有小程序,只是小程序有现在无法打开公众号文章。 + +## 三、思考自省 + +21世纪20年代已经开始。 + +总结过往 10 年,人生两大事情改变了我,人的生老病死,爱情的现实。 + +既然遇到了,那我就积极、勇敢面对,去改变。 + +这 10 年,从一个娇生惯养的小孩,到一心只有这个家的年轻人,双手不再稚嫩,双肩不再轻荡。 + +努力做好自己,强大自己,去学习,去进步。 + +这 10 年,我尽力了。 \ No newline at end of file diff --git "a/Cute-Article/article/2019-\345\205\250\345\271\264\350\256\241\345\210\222.md" "b/Cute-Article/article/2019-\345\205\250\345\271\264\350\256\241\345\210\222.md" new file mode 100644 index 00000000..e4716209 --- /dev/null +++ "b/Cute-Article/article/2019-\345\205\250\345\271\264\350\256\241\345\210\222.md" @@ -0,0 +1,100 @@ +**** +|Author|王平安| +|---|---| +|E-mail|pingan8787@qq.com| +|博 客|www.pingan8787.com| +|微 信|pingan8787| +|每日文章推荐|https://github.com/pingan8787/Leo_Reading/issues| +|JS小册|js.pingan8787.com| +**** + +> 我相信每个人都是幸运,而且聪明。 +> 厚积才能薄发,基础牢固才能做好。 +> 做出改变,做好调整,接受挑战,对自己有好处。 +> 自律很重要。 + +以上是摘自我的《2018-全年计划》(这里就不放地址了) + +也许还有一句,我比较喜欢的:你在成就公司,公司也在成就你 + +2019年对我来说,应该会变得更加理性,为人处世,日常生活,今年剩下的12个月(到2020年春节前),计划依旧分三方面:`工作`、`学习`和`生活`。 + +# 1、工作 +今年我会坚持在现在公司沉淀自己,完善自己,也多为公司出力。对于现在公司,我挺满意,产品也好,技术氛围也好,学习氛围还有同事间的氛围,我觉得都很符合我的理想工作环境。而且公司给我们提供很多很棒的资源,如**技术分享会**,**书籍借阅**,**设备支持**,**实践机会**等等,所以今年要利用好这些呢。 +今年呢,工作上有这样的安排: +1. 跟着公司项目的脚步,学习和开发,另外在需求提交之前,一定要做好UI稿**核对**,原型逻辑**核对**等,减少BUG。 +2. 与小伙伴们分享自己的**知识总结**和**技术分享**,锻炼自己,思路和思维。 +3. 向公司老大前辈们学习,对于一些**问题的处理方式**,和**管理团队的方式**等。 +4. 努力尝试能为公司写一些工具,**简化工作流程**,或者**提高工作效率**等。 +5. 多**使用公司产品**,**参与产品运营**,发现问题,提出改善意见等。 +6. 研究一套项目基础架构(如公司现在这一套`flutter`的架构)。 + +这一年工作上,会有更多的挑战,已经能感受到了,还是必须要相信自己,踩坑填坑踩坑填坑...,增长经验沉淀技术咯。 + +# 2.学习 +经过这两年的程序员学习和工作生涯,也算是找到一套自己的学习方法,也是挺适合自己,相信今年的学习之路,效率和质量会高一些。 +今年的话,学习的内容会更加集中,而不会想去年那样涉及那么多方面: + +## 2.1学习内容 + +**前端知识(核心)**: +1. 前端基础:基础依然非常重要,今年需要更深入研究`JavaScript原理层面`(如引擎原理,工作原理等),还有CSS的一些不太常用的知识,另外ES规范还需复习,特别是如`Proxy`/`Set`和`Map`这类用的比较少的,但是用起来效率又特别高的这类知识。 +2. 前端框架:`Angular6`(也有可能升级到7)这个必须学会,而且能用在工作中使用,也需要针对一些知识点去研究原理(这方面可以多跟宝哥还有其他人学习),其他框架,就不需要太去研究,现在觉得学好一个框架,研究清楚原理,其他都会是一样的。 +3. 前端趋势:把握和跟进前端的发展趋势,新技术,新工具等,学着了解未来的变化。 + +**后端知识**: +1. 服务端开发:今年需要把`Nodejs`用于工作中了,开发一些工具,提高公司的开发效率,还是使用`Express`和`Koa`,重点朝`Koa`方向。 +2. 数据库开发:搭配服务端开发,使用`Mongodb`和它延伸出来的`Mongoose`进行实际应用。 + +**混合开发**: +1. APP开发:熟练mac环境开发,学习`flutter`,并且开始使用到公司的一些模块上面(看老大安排),毕竟这个技术很棒,今年自己也要自己开发一个App(demo会是现在在开始的TIMI,正式的后面再规划),`RN`和`Weex`今年不会花时间去研究,除非公司业务需要。 + +**博客**: +1. 个人博客:内容改版,新增栏目(待定),“作品”这个栏目内容要充实上去,同步自己在其他平台推送的文章。今年希望将【原创文章】突破到60篇(目前17篇)。 +2. Cute-JavaScrip:需要添加功能,访问量统计(博客也需要,使用百度流量统计),另外【JS基础系列】【面试题系列】必须完成,有空也需要改一改之前的文章。 +3. github:开源新项目,目前暂定2个(Flutter的两个),将学习过程中生产的一些笔记和代码,整理后也放到github上。另外【每日文章】继续维护,坚持第三年。 +4. 掘金/思否/CSDN/简书:各个平台同步自己的原创文章,首发掘金,另外掘金今年加油**粉丝突破5000**,**点赞突破1W**(虽然不太看重这些)。 + +5. 公众号:继续维护,每日一篇,不做宣传,不去吸粉,只想积累,也许某一天,一个幸运的粉丝发现了它,希望能让他有种如获至宝的感觉。截至今天已经发了第106期 + +**机器学习**: +1. 深度学习:`tensorflow.js`有必要尝试下,毕竟对图像识别还是挺有兴趣的呢。 + +**技术书籍**: +1. CSS:《CSS世界》(喜欢)。 +2. JS:《你不知道的JavaScript》上中下,也是加强基础和原理理解。 +3. 算法:《算法图解》和《学习JavaScript数据结构与算法》(这本之前看过,但没看完),主要还是要多刷题,好的算法能提高代码质量和效率。 +书太多,要取舍,研究好几本就好。 +4. 架构:《前端技术架构》这本需要结合自己工作再去看。 + +**非技术书籍**: +1. 《地球上最后的夜晚》和《南方高速》,尝试着去理解作者想表达的思想和道理。 + +## 2.2小结 +这学习计划,看上去真的多,但是在我看来,这些都是必须学的。 +所以,这当做是一整年的计划,我还需要拆分到具体的时间上面,做安排。 +坚持! +自律! + +# 3.生活 +生活,我向往简单,人和事。 +新公司加班的现象比较常见,但是也是给我提供很多的学习机会。 +这一年的生活,应该是健康,且有更多幸福感的。 + +**日常生活**: +1. 业余生活:少熬夜(必须少熬夜),多锻炼(早操和睡前锻炼要坚持),坚持能自己做饭尽量自己做饭(早餐不吃外面),学习做饭做菜(找老妈讨教,换宿舍以后),毕竟我有做饭神器——小米电饭煲和小米电磁炉,研究下健康食谱。 +2. 住宿条件:换个宿舍,不用多么高档,安静舒适,适合自己或者和未来女票。 +3. 旅游:出去旅游一次,国内外都行(现在护照港澳通行证都有),需要旅游一次放松也开阔下视野。 + +**家庭生活**: +1. 未来的她:还需要好好加油,一起加油,一起做一件事的感觉会非常好。 +2. 母亲:为母亲分担更多的压力,每个月多汇钱给母亲,帮忙开个店铺给母亲做,照顾好母亲。 +3. 哥哥姐姐:常回家看看,多跟他们交流,今年找个项目再和哥哥一起做,亲兄弟团结。另外哥哥今年结婚,我要给他包个很大的红包。 + +2019全年规划基本如此,不论多与少,需要分配好,偶尔迷茫的时候,会给我带来动力。和去年一样,加油让自己24小时过程26小时。 + +沉淀自己,自律生活,为人和善,为公司,为家庭,也为自己。 + + + +leo 2019.01.12 \ No newline at end of file diff --git "a/Cute-Article/article/2020-\345\205\250\345\271\264\350\256\241\345\210\222.md" "b/Cute-Article/article/2020-\345\205\250\345\271\264\350\256\241\345\210\222.md" new file mode 100644 index 00000000..e69de29b diff --git "a/Cute-Article/article/2020\345\205\250\345\271\264\346\200\273\347\273\223.md" "b/Cute-Article/article/2020\345\205\250\345\271\264\346\200\273\347\273\223.md" new file mode 100644 index 00000000..5cfc97f2 --- /dev/null +++ "b/Cute-Article/article/2020\345\205\250\345\271\264\346\200\273\347\273\223.md" @@ -0,0 +1,175 @@ +![海沧湾公园.png](https://images.pingan8787.com/2020/cover-haicang.png) +> 生活不可能像你想象的那么好,但也不会像你想象的那么糟。我觉得人的脆弱和坚强都超乎自己的想像,有时我脆弱得一句话就泪流满面,有时又发现自己咬着牙走了很长的路。 + +回看 2020,我更加喜爱这句话了,每个小句子都有了不同味道。 + +## 一、再见 2020 👋 +2020疫情复工后,我便开始进入“战斗模式”,深受公众号“全栈修仙之路”作者“[阿宝哥](https://juejin.cn/user/764915822103079)”影响🌟,开始把更多时间和精力用来修炼自身,努力成长和进阶,成为一位“靠谱的人”和一名“T 型人才”。 + +- 靠谱的人:让自己靠谱,让别人放心; +- T 型人才:深挖知识深度,拓展知识广度。 + +(以下数据统计的时间,全部以 2020-12-19 日截止) + +🤗🤗🤗 + +### 1. 走过的路 + +疫情期间为了不给国家添麻烦,咱就天天家里窝着,闲来无事就给“貔貅”拍拍照啥的😎。 + +![貔貅](https://images.pingan8787.com/2020/2020-pixiu.png) + + +复工后,骑上我的“绿豆”去了好多地方(快把厦门岛逛透透了),算是把疫情期间没去玩的地方都补上了🤠。 + +![骑行.png](https://images.pingan8787.com/2020/2020-bike.png) + +还有这杯意义不同的咖啡,和一句“心之所在即为家”😜。 + +![星巴克的味道](https://images.pingan8787.com/2020/2020-coffe.png) + +当然,除了玩,这一年也做了很多重要的事情😊。 + +2020 年,写了很多文章(包含未发布),基本都放在语雀上。数一数,将近 **150** 篇文档是在 2020 年完成的🤔。 + +![语雀](https://images.pingan8787.com/2020/2020-yuque.png) + +除了文章,我也画了很多图,慢慢形成自己的画图风格。 + +![画图](https://images.pingan8787.com/2020/2020-draw.png) + +当然,代码还是少不了的😎。 + +程序员嘛,当然要看看代码提交次数,这一年提交了这些代码,感觉 [Github](https://github.com/pingan8787) + Gitlab + [Gitee](https://gitee.com/pingan8787) 三个提交记录合并一下,都快铺满了。 + +1. Github 提交记录 + +![gitlab 提交记录.png](https://images.pingan8787.com/2020/2020-github.png) + +2. Gitlab 提交记录 + +![gitlab 提交记录.png](https://images.pingan8787.com/2020/2020-gitlab.png) + +3. Gitee(码云) 提交记录 + +![gitlab 提交记录.png](https://images.pingan8787.com/2020/2020-gitee.png) + + +另外自己的微信公众号“前端自习课”也完成“**连续推送 810+ 天**”的成绩,这一年,我也玩起短视频,视频号了,也开始自己做动画,将一些知识点通过动画和大家分享,视频可以查看[《1分钟了解 Axios 拦截器实现原理》](https://mp.weixin.qq.com/s/1s4_WXCZscB6MXsVP3lbzQ)。 + +![1分钟了解 Axios 拦截器实现原理](https://images.pingan8787.com/2020/2020-video.png) + +2020 这一年还有很多事情想和大家分享,考虑到本文主旨和内容篇幅,就不再多介绍咯~有兴趣的朋友欢迎私聊我(微信:pingan8787)💘。 + +### 2. 感谢的人 +今年最需要感谢的,是**阿宝哥**和我们“**前端突击队**”**学习小组**的每位小伙伴啦💐~ + +🌰最大感受是:原来前端还能这样玩! +🌰最开心的是:团队学习更有动力,你不是一个人在战斗! + +在阿宝哥指导下,整理了一份自己的前端技能树,才知道自己的前端技能有几斤几两重,也才有更多动力和更清晰的方向。 + +![前端技能树](https://images.pingan8787.com/2020/2020-knowledge.png) + +在我们学习小组中,采用“**专题学习 + 总结输出**”的方式一起学习,目前已经沉淀 **200+ **篇文章啦! +小组目前 7 人(不含班主任),平均下来每人将近写了 30+ 篇!为小伙伴们点赞👍~ + +![学习小组](https://images.pingan8787.com/2020/2020-study.png) + +2020 年 11 月的某一天,思考了最近学习的知识和接下来的需要做的事情,于是有了下面的这篇字数少,内容多的笔记(用手机敲的,就是有点手酸🙁): + +![学习总结](https://images.pingan8787.com/2020/2020-learn.png) + +慢慢的,越来越发现,学得越多,发现自己要学的越多。🤣 + +这里再次感谢阿宝哥,感谢“前端突击队”的小伙伴们。未来继续冲🦆! + +### 3. 遗憾的事 +这一年,比较遗憾的事,是自己与阿里插肩而过呀🥺~倒也让我发现更多不足。 + +![遗憾的事](https://images.pingan8787.com/2020/2020-sad.png) + +这里也非常感谢内推的小伙伴,还有几位面试官,人都挺不错。😃 + +我们闽南人嘛,喜欢“爱拼才会赢”,所以,趁年轻多拼多创。 + +### 4. 点赞的事 +这一年为自己坚持的几件事情点赞~ +①自己微信公众号“前端自习课”连续推送 810+ 天文章,为此我把所有文章分类做了一张词云图,如下: + +![前端自习课](https://images.pingan8787.com/2020/2020-fe-study.png) + +可以看出,我主要分享的内容包括:“JS”、“CSS”、“拓展”和“Web技术”。🔔 + +②自己坚持的每月学习文章整理,也超过 40+ 个月了,截图如下: + +详细请看 github 地址:[https://github.com/pingan8787/Leo_Reading](https://github.com/pingan8787/Leo_Reading) + +![github 学习记录](https://images.pingan8787.com/2020/2020-github-study.png) + +## 二、你好 2021 👏 +2021 年即将到来,希望新的一年,每一个“下次一定”都能实现完成承诺。 + +⚽️⚽️⚽️ + +### 1. 加油,前端工程师 +在这前端生涯的第五年伊始,回想自己踩过的坑,走过的弯路,才慢慢领悟自己的前端生涯应该如何去走。 + +曾经和多数人一样,时常迷失学习什么知识,看到什么火,就去学什么,到头来,效果并不好。 + +未来自己的前端生涯,更应该站在巨人肩膀上,看向更远的地方。定个小目标呗,早日晋升技术专家。 + +接下来的时间里,做好自己在工作中的身份,做一个优秀的前端工程师。 + +![桌面](https://images.pingan8787.com/2020/2021-zhuomian.png) + +### 2. 加油,小儿子 +作为家中最小的孩子,被催婚已经成为这一年的常事,哈哈。 + +也许性格如此,加上独自在外工作,每天只想把事情做得更好,学更多知识,提升自己的价值。 + +很幸运这一年遇到了女孩 C。 + +接下来的时间里,做好自己在家里的身份,做一个让父母放心的好儿子。 + +![五店市.png](https://images.pingan8787.com/2020/2021-wudian.png) + +### 3. 加油,骑行侠 +我这人,兴趣爱好不太多,比如:骑行🚴、足球⚽️、敲代码💻。 + +骑行让我如此着迷。 + +换上衣服,12 月的寒风,也依然无法阻挡我的脚步。 + +接下来的时间里,坚持自己的热爱,做一个勇往直前大胆创的闽南人。 + +![寒风.png](https://images.pingan8787.com/2020/2021-bike.png) + +### 4. 加油,前端自习课 + +运营公众号“前端自习课”以后,认识了许多小伙伴,看见了许多从前的自己。 + +后来也慢慢和大家分享一些自己的经验和经历。 + +深刻记得,我简历中最后一句话:“希望自己的成⻓之路能帮助更多人,也希望在这个世界留下自己的一些足迹”。 + +接下来的时间里,坚持自己的初心,做一个对这个社区、这个社会有帮助的人。 +![zhihu.png](https://images.pingan8787.com/2020/2021-zhihu.png) + +## 三、总结 +每一年的总结,都是五味杂陈,才发现这一年来,自己又进步和成长了。 + +回顾篇头的一句话:“有时又发现自己咬着牙走了很长的路”。有时候一瞬间,一个偶然,发现自己原来咬着牙前进这么久,改变这么多。 + +最后,再思考一句话,希望对大家能有不同感受: + +> 除去睡眠,人的一生有一万多天。但是人与人之间的区别就在于,你究竟是活了一万多天,还是仅仅活了一天,却重复了一万多次。 + +希望未来的我们,会感到自己的每一天都是崭新的。 +![武磊](https://images.pingan8787.com/2020/2021-wulei.JPG) +像武磊一样努力,加油! + +最后欢迎关注我呀~ +[![博客](http://images.pingan8787.com/icon_my1.png)](http://www.pingan8787.com)[![语雀](http://images.pingan8787.com/assets/icon_26_yuque.png)](https://www.yuque.com/wangpingan/cute-frontend)[![知乎](http://images.pingan8787.com/icon_zhihu1.png)](https://zhuanlan.zhihu.com/cute-javascript)[![掘金](http://images.pingan8787.com/icon_juejin2.png)](https://juejin.im/user/586fc337a22b9d0058807d53/posts)[![思否](http://images.pingan8787.com/icon_sf1.png)](https://segmentfault.com/blog/pingan8787)[![CSDN](http://images.pingan8787.com/icon_csdn1.png)](https://blog.csdn.net/qq_36380426)[![简书](http://images.pingan8787.com/icon_jianshu1.png)](https://www.jianshu.com/u/2ec5d94afd60) + + [掘金年度征文 | 2020 与我的技术之路 征文活动正在进行中......](https://juejin.cn/post/6901125532729999374)  \ No newline at end of file diff --git "a/Cute-Article/article/21 \345\274\240\345\233\276\346\200\273\347\273\223\346\210\221\347\232\204 2020 \345\271\264.md" "b/Cute-Article/article/21 \345\274\240\345\233\276\346\200\273\347\273\223\346\210\221\347\232\204 2020 \345\271\264.md" new file mode 100644 index 00000000..5cfc97f2 --- /dev/null +++ "b/Cute-Article/article/21 \345\274\240\345\233\276\346\200\273\347\273\223\346\210\221\347\232\204 2020 \345\271\264.md" @@ -0,0 +1,175 @@ +![海沧湾公园.png](https://images.pingan8787.com/2020/cover-haicang.png) +> 生活不可能像你想象的那么好,但也不会像你想象的那么糟。我觉得人的脆弱和坚强都超乎自己的想像,有时我脆弱得一句话就泪流满面,有时又发现自己咬着牙走了很长的路。 + +回看 2020,我更加喜爱这句话了,每个小句子都有了不同味道。 + +## 一、再见 2020 👋 +2020疫情复工后,我便开始进入“战斗模式”,深受公众号“全栈修仙之路”作者“[阿宝哥](https://juejin.cn/user/764915822103079)”影响🌟,开始把更多时间和精力用来修炼自身,努力成长和进阶,成为一位“靠谱的人”和一名“T 型人才”。 + +- 靠谱的人:让自己靠谱,让别人放心; +- T 型人才:深挖知识深度,拓展知识广度。 + +(以下数据统计的时间,全部以 2020-12-19 日截止) + +🤗🤗🤗 + +### 1. 走过的路 + +疫情期间为了不给国家添麻烦,咱就天天家里窝着,闲来无事就给“貔貅”拍拍照啥的😎。 + +![貔貅](https://images.pingan8787.com/2020/2020-pixiu.png) + + +复工后,骑上我的“绿豆”去了好多地方(快把厦门岛逛透透了),算是把疫情期间没去玩的地方都补上了🤠。 + +![骑行.png](https://images.pingan8787.com/2020/2020-bike.png) + +还有这杯意义不同的咖啡,和一句“心之所在即为家”😜。 + +![星巴克的味道](https://images.pingan8787.com/2020/2020-coffe.png) + +当然,除了玩,这一年也做了很多重要的事情😊。 + +2020 年,写了很多文章(包含未发布),基本都放在语雀上。数一数,将近 **150** 篇文档是在 2020 年完成的🤔。 + +![语雀](https://images.pingan8787.com/2020/2020-yuque.png) + +除了文章,我也画了很多图,慢慢形成自己的画图风格。 + +![画图](https://images.pingan8787.com/2020/2020-draw.png) + +当然,代码还是少不了的😎。 + +程序员嘛,当然要看看代码提交次数,这一年提交了这些代码,感觉 [Github](https://github.com/pingan8787) + Gitlab + [Gitee](https://gitee.com/pingan8787) 三个提交记录合并一下,都快铺满了。 + +1. Github 提交记录 + +![gitlab 提交记录.png](https://images.pingan8787.com/2020/2020-github.png) + +2. Gitlab 提交记录 + +![gitlab 提交记录.png](https://images.pingan8787.com/2020/2020-gitlab.png) + +3. Gitee(码云) 提交记录 + +![gitlab 提交记录.png](https://images.pingan8787.com/2020/2020-gitee.png) + + +另外自己的微信公众号“前端自习课”也完成“**连续推送 810+ 天**”的成绩,这一年,我也玩起短视频,视频号了,也开始自己做动画,将一些知识点通过动画和大家分享,视频可以查看[《1分钟了解 Axios 拦截器实现原理》](https://mp.weixin.qq.com/s/1s4_WXCZscB6MXsVP3lbzQ)。 + +![1分钟了解 Axios 拦截器实现原理](https://images.pingan8787.com/2020/2020-video.png) + +2020 这一年还有很多事情想和大家分享,考虑到本文主旨和内容篇幅,就不再多介绍咯~有兴趣的朋友欢迎私聊我(微信:pingan8787)💘。 + +### 2. 感谢的人 +今年最需要感谢的,是**阿宝哥**和我们“**前端突击队**”**学习小组**的每位小伙伴啦💐~ + +🌰最大感受是:原来前端还能这样玩! +🌰最开心的是:团队学习更有动力,你不是一个人在战斗! + +在阿宝哥指导下,整理了一份自己的前端技能树,才知道自己的前端技能有几斤几两重,也才有更多动力和更清晰的方向。 + +![前端技能树](https://images.pingan8787.com/2020/2020-knowledge.png) + +在我们学习小组中,采用“**专题学习 + 总结输出**”的方式一起学习,目前已经沉淀 **200+ **篇文章啦! +小组目前 7 人(不含班主任),平均下来每人将近写了 30+ 篇!为小伙伴们点赞👍~ + +![学习小组](https://images.pingan8787.com/2020/2020-study.png) + +2020 年 11 月的某一天,思考了最近学习的知识和接下来的需要做的事情,于是有了下面的这篇字数少,内容多的笔记(用手机敲的,就是有点手酸🙁): + +![学习总结](https://images.pingan8787.com/2020/2020-learn.png) + +慢慢的,越来越发现,学得越多,发现自己要学的越多。🤣 + +这里再次感谢阿宝哥,感谢“前端突击队”的小伙伴们。未来继续冲🦆! + +### 3. 遗憾的事 +这一年,比较遗憾的事,是自己与阿里插肩而过呀🥺~倒也让我发现更多不足。 + +![遗憾的事](https://images.pingan8787.com/2020/2020-sad.png) + +这里也非常感谢内推的小伙伴,还有几位面试官,人都挺不错。😃 + +我们闽南人嘛,喜欢“爱拼才会赢”,所以,趁年轻多拼多创。 + +### 4. 点赞的事 +这一年为自己坚持的几件事情点赞~ +①自己微信公众号“前端自习课”连续推送 810+ 天文章,为此我把所有文章分类做了一张词云图,如下: + +![前端自习课](https://images.pingan8787.com/2020/2020-fe-study.png) + +可以看出,我主要分享的内容包括:“JS”、“CSS”、“拓展”和“Web技术”。🔔 + +②自己坚持的每月学习文章整理,也超过 40+ 个月了,截图如下: + +详细请看 github 地址:[https://github.com/pingan8787/Leo_Reading](https://github.com/pingan8787/Leo_Reading) + +![github 学习记录](https://images.pingan8787.com/2020/2020-github-study.png) + +## 二、你好 2021 👏 +2021 年即将到来,希望新的一年,每一个“下次一定”都能实现完成承诺。 + +⚽️⚽️⚽️ + +### 1. 加油,前端工程师 +在这前端生涯的第五年伊始,回想自己踩过的坑,走过的弯路,才慢慢领悟自己的前端生涯应该如何去走。 + +曾经和多数人一样,时常迷失学习什么知识,看到什么火,就去学什么,到头来,效果并不好。 + +未来自己的前端生涯,更应该站在巨人肩膀上,看向更远的地方。定个小目标呗,早日晋升技术专家。 + +接下来的时间里,做好自己在工作中的身份,做一个优秀的前端工程师。 + +![桌面](https://images.pingan8787.com/2020/2021-zhuomian.png) + +### 2. 加油,小儿子 +作为家中最小的孩子,被催婚已经成为这一年的常事,哈哈。 + +也许性格如此,加上独自在外工作,每天只想把事情做得更好,学更多知识,提升自己的价值。 + +很幸运这一年遇到了女孩 C。 + +接下来的时间里,做好自己在家里的身份,做一个让父母放心的好儿子。 + +![五店市.png](https://images.pingan8787.com/2020/2021-wudian.png) + +### 3. 加油,骑行侠 +我这人,兴趣爱好不太多,比如:骑行🚴、足球⚽️、敲代码💻。 + +骑行让我如此着迷。 + +换上衣服,12 月的寒风,也依然无法阻挡我的脚步。 + +接下来的时间里,坚持自己的热爱,做一个勇往直前大胆创的闽南人。 + +![寒风.png](https://images.pingan8787.com/2020/2021-bike.png) + +### 4. 加油,前端自习课 + +运营公众号“前端自习课”以后,认识了许多小伙伴,看见了许多从前的自己。 + +后来也慢慢和大家分享一些自己的经验和经历。 + +深刻记得,我简历中最后一句话:“希望自己的成⻓之路能帮助更多人,也希望在这个世界留下自己的一些足迹”。 + +接下来的时间里,坚持自己的初心,做一个对这个社区、这个社会有帮助的人。 +![zhihu.png](https://images.pingan8787.com/2020/2021-zhihu.png) + +## 三、总结 +每一年的总结,都是五味杂陈,才发现这一年来,自己又进步和成长了。 + +回顾篇头的一句话:“有时又发现自己咬着牙走了很长的路”。有时候一瞬间,一个偶然,发现自己原来咬着牙前进这么久,改变这么多。 + +最后,再思考一句话,希望对大家能有不同感受: + +> 除去睡眠,人的一生有一万多天。但是人与人之间的区别就在于,你究竟是活了一万多天,还是仅仅活了一天,却重复了一万多次。 + +希望未来的我们,会感到自己的每一天都是崭新的。 +![武磊](https://images.pingan8787.com/2020/2021-wulei.JPG) +像武磊一样努力,加油! + +最后欢迎关注我呀~ +[![博客](http://images.pingan8787.com/icon_my1.png)](http://www.pingan8787.com)[![语雀](http://images.pingan8787.com/assets/icon_26_yuque.png)](https://www.yuque.com/wangpingan/cute-frontend)[![知乎](http://images.pingan8787.com/icon_zhihu1.png)](https://zhuanlan.zhihu.com/cute-javascript)[![掘金](http://images.pingan8787.com/icon_juejin2.png)](https://juejin.im/user/586fc337a22b9d0058807d53/posts)[![思否](http://images.pingan8787.com/icon_sf1.png)](https://segmentfault.com/blog/pingan8787)[![CSDN](http://images.pingan8787.com/icon_csdn1.png)](https://blog.csdn.net/qq_36380426)[![简书](http://images.pingan8787.com/icon_jianshu1.png)](https://www.jianshu.com/u/2ec5d94afd60) + + [掘金年度征文 | 2020 与我的技术之路 征文活动正在进行中......](https://juejin.cn/post/6901125532729999374)  \ No newline at end of file diff --git "a/21-JavaScript\345\274\202\346\255\245\346\234\272\345\210\266\350\257\246\350\247\243.md" "b/Cute-Article/article/21-JavaScript\345\274\202\346\255\245\346\234\272\345\210\266\350\257\246\350\247\243.md" similarity index 100% rename from "21-JavaScript\345\274\202\346\255\245\346\234\272\345\210\266\350\257\246\350\247\243.md" rename to "Cute-Article/article/21-JavaScript\345\274\202\346\255\245\346\234\272\345\210\266\350\257\246\350\247\243.md" diff --git "a/22-JavaScript\344\270\255\346\234\211\350\266\243\347\232\204\345\214\272\345\210\206\345\220\214\346\255\245\345\222\214\345\274\202\346\255\245Ajax.md" "b/Cute-Article/article/22-JavaScript\344\270\255\346\234\211\350\266\243\347\232\204\345\214\272\345\210\206\345\220\214\346\255\245\345\222\214\345\274\202\346\255\245Ajax.md" similarity index 100% rename from "22-JavaScript\344\270\255\346\234\211\350\266\243\347\232\204\345\214\272\345\210\206\345\220\214\346\255\245\345\222\214\345\274\202\346\255\245Ajax.md" rename to "Cute-Article/article/22-JavaScript\344\270\255\346\234\211\350\266\243\347\232\204\345\214\272\345\210\206\345\220\214\346\255\245\345\222\214\345\274\202\346\255\245Ajax.md" diff --git "a/23-JavaScript\345\205\253\345\274\240\346\200\235\347\273\264\345\257\274\345\233\276.md" "b/Cute-Article/article/23-JavaScript\345\205\253\345\274\240\346\200\235\347\273\264\345\257\274\345\233\276.md" similarity index 100% rename from "23-JavaScript\345\205\253\345\274\240\346\200\235\347\273\264\345\257\274\345\233\276.md" rename to "Cute-Article/article/23-JavaScript\345\205\253\345\274\240\346\200\235\347\273\264\345\257\274\345\233\276.md" diff --git "a/24-Vue\346\212\230\350\205\276\350\256\260-\347\273\231Axios\345\201\232\344\270\252\346\214\272\351\235\240\350\260\261\347\232\204\345\260\201\350\243\205\357\274\210\346\212\245\351\224\231,\351\211\264\346\235\203,\350\267\263\350\275\254,\346\213\246\346\210\252,\346\217\220\347\244\272\357\274\211.md" "b/Cute-Article/article/24-Vue\346\212\230\350\205\276\350\256\260-\347\273\231Axios\345\201\232\344\270\252\346\214\272\351\235\240\350\260\261\347\232\204\345\260\201\350\243\205\357\274\210\346\212\245\351\224\231,\351\211\264\346\235\203,\350\267\263\350\275\254,\346\213\246\346\210\252,\346\217\220\347\244\272\357\274\211.md" similarity index 100% rename from "24-Vue\346\212\230\350\205\276\350\256\260-\347\273\231Axios\345\201\232\344\270\252\346\214\272\351\235\240\350\260\261\347\232\204\345\260\201\350\243\205\357\274\210\346\212\245\351\224\231,\351\211\264\346\235\203,\350\267\263\350\275\254,\346\213\246\346\210\252,\346\217\220\347\244\272\357\274\211.md" rename to "Cute-Article/article/24-Vue\346\212\230\350\205\276\350\256\260-\347\273\231Axios\345\201\232\344\270\252\346\214\272\351\235\240\350\260\261\347\232\204\345\260\201\350\243\205\357\274\210\346\212\245\351\224\231,\351\211\264\346\235\203,\350\267\263\350\275\254,\346\213\246\346\210\252,\346\217\220\347\244\272\357\274\211.md" diff --git "a/25-Webpack\345\205\245\351\227\250\346\225\231\347\250\213\346\225\264\347\220\206\357\274\210\346\225\264\347\220\206\344\270\255\357\274\211.md" "b/Cute-Article/article/25-Webpack\345\205\245\351\227\250\346\225\231\347\250\213\346\225\264\347\220\206\357\274\210\346\225\264\347\220\206\344\270\255\357\274\211.md" similarity index 100% rename from "25-Webpack\345\205\245\351\227\250\346\225\231\347\250\213\346\225\264\347\220\206\357\274\210\346\225\264\347\220\206\344\270\255\357\274\211.md" rename to "Cute-Article/article/25-Webpack\345\205\245\351\227\250\346\225\231\347\250\213\346\225\264\347\220\206\357\274\210\346\225\264\347\220\206\344\270\255\357\274\211.md" diff --git "a/26-Webpack\345\270\270\347\224\250\351\205\215\347\275\256\346\225\264\347\220\206\357\274\210\346\225\264\347\220\206\344\270\255\357\274\211.md" "b/Cute-Article/article/26-Webpack\345\270\270\347\224\250\351\205\215\347\275\256\346\225\264\347\220\206\357\274\210\346\225\264\347\220\206\344\270\255\357\274\211.md" similarity index 100% rename from "26-Webpack\345\270\270\347\224\250\351\205\215\347\275\256\346\225\264\347\220\206\357\274\210\346\225\264\347\220\206\344\270\255\357\274\211.md" rename to "Cute-Article/article/26-Webpack\345\270\270\347\224\250\351\205\215\347\275\256\346\225\264\347\220\206\357\274\210\346\225\264\347\220\206\344\270\255\357\274\211.md" diff --git "a/27-Markdowm\350\257\255\346\263\225\346\225\264\347\220\206.md" "b/Cute-Article/article/27-Markdowm\350\257\255\346\263\225\346\225\264\347\220\206.md" similarity index 100% rename from "27-Markdowm\350\257\255\346\263\225\346\225\264\347\220\206.md" rename to "Cute-Article/article/27-Markdowm\350\257\255\346\263\225\346\225\264\347\220\206.md" diff --git "a/28-JavaScript\344\270\255\347\232\204void\350\277\220\347\256\227\347\254\246.md" "b/Cute-Article/article/28-JavaScript\344\270\255\347\232\204void\350\277\220\347\256\227\347\254\246.md" similarity index 100% rename from "28-JavaScript\344\270\255\347\232\204void\350\277\220\347\256\227\347\254\246.md" rename to "Cute-Article/article/28-JavaScript\344\270\255\347\232\204void\350\277\220\347\256\227\347\254\246.md" diff --git "a/29-\345\205\263\344\272\216\351\232\217\346\234\272\346\225\260\347\232\204\344\270\200\344\272\233\346\200\273\347\273\223.md" "b/Cute-Article/article/29-\345\205\263\344\272\216\351\232\217\346\234\272\346\225\260\347\232\204\344\270\200\344\272\233\346\200\273\347\273\223.md" similarity index 100% rename from "29-\345\205\263\344\272\216\351\232\217\346\234\272\346\225\260\347\232\204\344\270\200\344\272\233\346\200\273\347\273\223.md" rename to "Cute-Article/article/29-\345\205\263\344\272\216\351\232\217\346\234\272\346\225\260\347\232\204\344\270\200\344\272\233\346\200\273\347\273\223.md" diff --git "a/3-Promise\347\256\200\345\215\225\347\224\250\346\263\225.md" "b/Cute-Article/article/3-Promise\347\256\200\345\215\225\347\224\250\346\263\225.md" similarity index 100% rename from "3-Promise\347\256\200\345\215\225\347\224\250\346\263\225.md" rename to "Cute-Article/article/3-Promise\347\256\200\345\215\225\347\224\250\346\263\225.md" diff --git "a/30-\344\270\200\346\254\241\350\256\260\344\275\217js\347\232\2046\344\270\252\346\255\243\345\210\231\346\226\271\346\263\225.md" "b/Cute-Article/article/30-\344\270\200\346\254\241\350\256\260\344\275\217js\347\232\2046\344\270\252\346\255\243\345\210\231\346\226\271\346\263\225.md" similarity index 100% rename from "30-\344\270\200\346\254\241\350\256\260\344\275\217js\347\232\2046\344\270\252\346\255\243\345\210\231\346\226\271\346\263\225.md" rename to "Cute-Article/article/30-\344\270\200\346\254\241\350\256\260\344\275\217js\347\232\2046\344\270\252\346\255\243\345\210\231\346\226\271\346\263\225.md" diff --git "a/31-ES6\350\277\231\344\272\233\345\260\261\345\244\237\344\272\206.md" "b/Cute-Article/article/31-ES6\350\277\231\344\272\233\345\260\261\345\244\237\344\272\206.md" similarity index 100% rename from "31-ES6\350\277\231\344\272\233\345\260\261\345\244\237\344\272\206.md" rename to "Cute-Article/article/31-ES6\350\277\231\344\272\233\345\260\261\345\244\237\344\272\206.md" diff --git "a/32-\350\201\212\344\270\200\350\201\212JavaScript\347\232\204IIFE.md" "b/Cute-Article/article/32-\350\201\212\344\270\200\350\201\212JavaScript\347\232\204IIFE.md" similarity index 100% rename from "32-\350\201\212\344\270\200\350\201\212JavaScript\347\232\204IIFE.md" rename to "Cute-Article/article/32-\350\201\212\344\270\200\350\201\212JavaScript\347\232\204IIFE.md" diff --git "a/33-javascript\347\232\204\347\272\257\345\207\275\346\225\260.md" "b/Cute-Article/article/33-javascript\347\232\204\347\272\257\345\207\275\346\225\260.md" similarity index 100% rename from "33-javascript\347\232\204\347\272\257\345\207\275\346\225\260.md" rename to "Cute-Article/article/33-javascript\347\232\204\347\272\257\345\207\275\346\225\260.md" diff --git "a/34-\346\210\221\347\234\274\344\270\255\347\232\204async&await.md" "b/Cute-Article/article/34-\346\210\221\347\234\274\344\270\255\347\232\204async&await.md" similarity index 100% rename from "34-\346\210\221\347\234\274\344\270\255\347\232\204async&await.md" rename to "Cute-Article/article/34-\346\210\221\347\234\274\344\270\255\347\232\204async&await.md" diff --git "a/35-ES6\344\270\255\347\232\204\346\250\241\345\235\227\345\257\274\345\205\245\345\257\274\345\207\272\346\225\264\347\220\206.md" "b/Cute-Article/article/35-ES6\344\270\255\347\232\204\346\250\241\345\235\227\345\257\274\345\205\245\345\257\274\345\207\272\346\225\264\347\220\206.md" similarity index 97% rename from "35-ES6\344\270\255\347\232\204\346\250\241\345\235\227\345\257\274\345\205\245\345\257\274\345\207\272\346\225\264\347\220\206.md" rename to "Cute-Article/article/35-ES6\344\270\255\347\232\204\346\250\241\345\235\227\345\257\274\345\205\245\345\257\274\345\207\272\346\225\264\347\220\206.md" index 7908ac22..73ae0428 100644 --- "a/35-ES6\344\270\255\347\232\204\346\250\241\345\235\227\345\257\274\345\205\245\345\257\274\345\207\272\346\225\264\347\220\206.md" +++ "b/Cute-Article/article/35-ES6\344\270\255\347\232\204\346\250\241\345\235\227\345\257\274\345\205\245\345\257\274\345\207\272\346\225\264\347\220\206.md" @@ -8,7 +8,7 @@ **** ## 1、导出export命令 -如果需要外部获取文件内部变量,需要用`expor`t将变量输出,有三种方式: +如果需要外部获取文件内部变量,需要用`export`将变量输出,有三种方式: ```js //*方式一 单独输出 //main.js @@ -102,4 +102,4 @@ function foo() { export default foo; ``` -> 参考阮一峰[ES6 Module语法](http://es6.ruanyifeng.com/#docs/module) \ No newline at end of file +> 参考阮一峰[ES6 Module语法](http://es6.ruanyifeng.com/#docs/module) diff --git "a/36-\345\245\275\345\245\275\345\255\246\344\271\240toLocaleString\346\226\271\346\263\225.md" "b/Cute-Article/article/36-\345\245\275\345\245\275\345\255\246\344\271\240toLocaleString\346\226\271\346\263\225.md" similarity index 100% rename from "36-\345\245\275\345\245\275\345\255\246\344\271\240toLocaleString\346\226\271\346\263\225.md" rename to "Cute-Article/article/36-\345\245\275\345\245\275\345\255\246\344\271\240toLocaleString\346\226\271\346\263\225.md" diff --git "a/37-JavaScript\344\272\213\344\273\266\345\247\224\346\211\230\350\257\246\350\247\243.md" "b/Cute-Article/article/37-JavaScript\344\272\213\344\273\266\345\247\224\346\211\230\350\257\246\350\247\243.md" similarity index 100% rename from "37-JavaScript\344\272\213\344\273\266\345\247\224\346\211\230\350\257\246\350\247\243.md" rename to "Cute-Article/article/37-JavaScript\344\272\213\344\273\266\345\247\224\346\211\230\350\257\246\350\247\243.md" diff --git "a/Cute-Article/article/38-JavaScript\344\270\255\345\270\270\350\247\201\350\256\276\350\256\241\346\250\241\345\274\217.md" "b/Cute-Article/article/38-JavaScript\344\270\255\345\270\270\350\247\201\350\256\276\350\256\241\346\250\241\345\274\217.md" new file mode 100644 index 00000000..b1578d37 --- /dev/null +++ "b/Cute-Article/article/38-JavaScript\344\270\255\345\270\270\350\247\201\350\256\276\350\256\241\346\250\241\345\274\217.md" @@ -0,0 +1,1247 @@ +开发中,我们或多或少地接触了设计模式,但是很多时候不知道自己使用了哪种设计模式或者说该使用何种设计模式。本文意在梳理常见设计模式的特点,从而对它们有比较清晰的认知。 + +### JavaScript 中常见设计模式 +* 单例模式 +* 策略模式 +* 代理模式 +* 迭代器模式 +* 发布-订阅模式 +* 命令模式 +* 组合模式 +* 模板方法模式 +* 享元模式 +* 职责链模式 +* 中介者模式 +* 装饰者模式 +* 状态模式 +* 适配者模式 + +### 各设计模式关键词 +看完了上述设计模式后,把它们的关键词特点罗列出来,以后提到某种设计模式,进而联想相应的关键词和例子,从而心中有数。 + +|设计模式|特点|案例| +|-|-|-| +|单例模式|一个类只能构造出唯一实例|创建菜单对象| +|策略模式|根据不同参数可以命中不同的策略|动画库里的算法函数| +|代理模式|代理对象和本体对象具有一致的接口|图片预加载| +|迭代器模式|能获取聚合对象的顺序和元素|each([1, 2, 3], cb)| +|发布-订阅模式|PubSub|瀑布流库| +|命令模式|不同对象间约定好相应的接口|按钮和命令的分离| +|组合模式|组合模式在对象间形成一致对待的树形结构|扫描文件夹| +|模板方法模式|父类中定好执行顺序|咖啡和茶| +|享元模式|减少创建实例的个数|男女模具试装| +|职责链模式|通过请求第一个条件,会持续执行后续的条件,直到返回结果为止|if else 优化| +|中介者模式|对象和对象之间借助第三方中介者进行通信|测试结束告知结果| +|装饰者模式|动态地给函数赋能|天冷了穿衣服,热了脱衣服| +|状态模式|每个状态建立一个类,状态改变会产生不同行为|电灯换挡| +|适配者模式|一种数据结构改成另一种数据结构|枚举值接口变更| + + +## 1.单例模式 +### 两个条件 +* 确保只有一个实例 +* 可以全局访问 + +### 适用 +适用于弹框的实现,全局缓存 +### 实现单例模式 +```js +const singleton = function(name) { + this.name = name + this.instance = null +} + +singleton.prototype.getName = function() { + console.log(this.name) +} + +singleton.getInstance = function(name) { + if (!this.instance) { // 关键语句 + this.instance = new singleton(name) + } + return this.instance +} + +// test +const a = singleton.getInstance('a') // 通过 getInstance 来获取实例 +const b = singleton.getInstance('b') +console.log(a === b) +``` +### JavaScript 中的单例模式 +因为 JavaScript 是无类的语言,而且 JS 中的全局对象符合单例模式两个条件。很多时候我们把全局对象当成单例模式来使用, +```js +var obj = {} +``` +### 弹框层的实践 +实现弹框的一种做法是先创建好弹框,然后使之隐藏,这样子的话会浪费部分不必要的 DOM 开销,我们可以在需要弹框的时候再进行创建,同时结合单例模式实现只有一个实例,从而节省部分 DOM 开销。下列为登入框部分代码: +```js +const createLoginLayer = function() { + const div = document.createElement('div') + div.innerHTML = '登入浮框' + div.style.display = 'none' + document.appendChild(div) + return div +} +``` +使单例模式和创建弹框代码解耦 +```js +const getSingle = function(fn) { + const result + return function() { + return result || result = fn.apply(this, arguments) + } +} +``` +```js +const createSingleLoginLayer = getSingle(createLoginLayer) + +document.getElementById('loginBtn').onclick = function() { + createSingleLoginLayer() +} +``` + +*** + +## 2.策略模式 +> 定义:根据不同参数可以命中不同的策略 + +### JavaScript 中的策略模式 +观察如下获取年终奖的 demo,根据不同的参数(level)获得不同策略方法(规则),这是策略模式在 JS 比较经典的运用之一。 +```js +const strategy = { + 'S': function(salary) { + return salary * 4 + }, + 'A': function(salary) { + return salary * 3 + }, + 'B': function(salary) { + return salary * 2 + } +} + +const calculateBonus = function(level, salary) { + return strategy[level](salary) +} + +calculateBonus('A', 10000) // 30000 +``` +在函数是一等公民的 JS 中,策略模式的使用常常隐藏在高阶函数中,稍微变换下上述 demo 的形式如下,可以发现我们平时已经在使用它了,恭喜我们又掌握了一种设计模式。 +```js +const S = function(salary) { + return salary * 4 +} + +const A = function(salary) { + return salary * 3 +} + +const B = function(salary) { + return salary * 2 +} + +const calculateBonus = function(func, salary) { + return func(salary) +} + +calculateBonus(A, 10000) // 30000 +``` +### 优点 +* 能减少大量的 if 语句 +* 复用性好 + +*** + +## 3.代理模式 +情景:小明追女生 A +* 非代理模式:小明 =花=> 女生A +* 代理模式:小明 =花=> 让女生A的好友B帮忙 =花=> 女生A + +### 代理模式的特点 +* 代理对象和本体对象具有一致的接口,对使用者友好 + +代理模式的种类有很多,在 JS 中最常用的为虚拟代理和缓存代理。 +#### 虚拟代理实现图片预加载 +下面这段代码运用代理模式来实现图片预加载,可以看到通过代理模式巧妙地将创建图片与预加载逻辑分离,并且在未来如果不需要预加载,只要改成请求本体代替请求代理对象就行。 +```js +const myImage = (function() { + const imgNode = document.createElement('img') + document.body.appendChild(imgNode) + return { + setSrc: function(src) { + imgNode.src = src + } + } +})() + +const proxyImage = (function() { + const img = new Image() + img.onload = function() { // http 图片加载完毕后才会执行 + myImage.setSrc(this.src) + } + return { + setSrc: function(src) { + myImage.setSrc('loading.jpg') // 本地 loading 图片 + img.src = src + } + } +})() + +proxyImage.setSrc('http://loaded.jpg') +``` + +缓存代理实现乘积计算 +```js +const mult = function() { + let a = 1 + for (let i = 0, l; l = arguments[i++];) { + a = a * l + } + return a +} + +const proxyMult = (function() { + const cache = {} + return function() { + const tag = Array.prototype.join.call(arguments, ',') + if (cache[tag]) { + return cache[tag] + } + cache[tag] = mult.apply(this, arguments) + return cache[tag] + } +})() + +proxyMult(1, 2, 3, 4) // 24 +``` + +### 小 tip +在开发时候不要先去猜测是否需要使用代理模式,如果发现直接使用某个对象不方便时,再来优化不迟。 + +*** + +## 4.迭代器模式 +> 定义:能访问到聚合对象的顺序与元素 +### 实现一个内部迭代器 +```js +function each(arr, fn) { + for (let i = 0; i < arr.length; i++) { + fn(i, arr[i]) + } +} + +each([1, 2, 3], function(i, n) { + console.log(i) // 0 1 2 + console.log(n) // 1 2 3 +}) +``` +可以看出内部迭代器在调用的时候非常简单,使用者不用关心迭代器内部实现的细节,但这也是内部迭代器的缺点。比如要比较两数组是否相等,只能在其回调函数中作文章了,代码如下: +```js +const compare = function(arr1, arr2) { + each(arr1, function(i, n) { + if (arr2[i] !== n) { + console.log('两数组不等') + return + } + }) + console.log('两数组相等') +} + +const arr1 = [1, 2, 3] +const arr2 = [1, 2, 3] +compare(arr1, arr2) // 两数组相等 +``` +### 实现一个外部迭代器 + +相较于内部迭代器,外部迭代器将遍历的权利转移到外部,因此在调用的时候拥有了更多的自由性,不过缺点是调用方式较复杂。 +```js +const iterator = function(arr) { + let current = 0 + const next = function() { + current = current + 1 + } + const done = function() { + return current >= arr.length + } + const value = function() { + return arr[current] + } + return { + next, + done, + value, + } +} + +const arr1 = [1, 2 ,3] +const arr2 = [1, 2, 3] +const iterator1 = iterator(arr1) +const iterator2 = iterator(arr2) + +const compare = function(iterator1, iterator2) { + while (!iterator1.done() && !iterator2.done()) { + if (iterator1.value() !== iterator2.value()) { + console.log('两数组不等') + return + } + iterator1.next() // 外部迭代器将遍历的权利转移到外部 + iterator2.next() + } + console.log('两数组相等') +} + +compare(iterator1, iterator2) +``` + +*** + +## 5.发布订阅模式 +事件发布/订阅模式 (PubSub) 在异步编程中帮助我们完成更松的解耦,甚至在 MVC、MVVC 的架构中以及设计模式中也少不了发布-订阅模式的参与。 +优点:在异步编程中实现更深的解耦 +缺点:如果过多的使用发布订阅模式,会增加维护的难度 + +### 实现一个发布订阅模式 +```js +var Event = function() { + this.obj = {} +} + +Event.prototype.on = function(eventType, fn) { + if (!this.obj[eventType]) { + this.obj[eventType] = [] + } + this.obj[eventType].push(fn) +} + +Event.prototype.emit = function() { + var eventType = Array.prototype.shift.call(arguments) + var arr = this.obj[eventType] + for (let i = 0; i < arr.length; i++) { + arr[i].apply(arr[i], arguments) + } +} + +var ev = new Event() + +ev.on('click', function(a) { // 订阅函数 + console.log(a) // 1 +}) + +ev.emit('click', 1) // 发布函数 +``` + +### 订阅函数逻辑一定要优先于发布函数吗 +考虑以下场景: +```js +$.ajax('', () => { + // 异步订阅函数逻辑 +}) + +// 在其他地方执行发布函数,此时并不能保证执行发布函数的时候,订阅函数已经执行 +``` +我们需要实现这样的逻辑: +```js +var ev = new Event() +ev.emit('click', 1) + +ev.on('click', function(a) { + console.log(a) // 1 +}) +``` +目标明确后,来着手实现它: +```js +var Event = function() { + this.obj = {} + this.cacheList = [] +} + +Event.prototype.on = function(eventType, fn) { + if (!this.obj[eventType]) { + this.obj[eventType] = [] + } + this.obj[eventType].push(fn) + + for (let i = 0; i < this.cacheList.length; i++) { + this.cacheList[i]() + } +} + +Event.prototype.emit = function() { + const arg = arguments + const that = this + function cache() { + var eventType = Array.prototype.shift.call(arg) + var arr = that.obj[eventType] + for (let i = 0; i < arr.length; i++) { + arr[i].apply(arr[i], arg) + } + } + this.cacheList.push(cache) +} +``` +以上代码实现思路就是把原本在 `emit` 里触发的函数存到 `cacheList`,再转交到 `on` 中触发。从而实现了发布函数先于订阅函数执行。 + +*** + +## 6.命令模式 +命令模式与策略模式有些类似,在 JavaScript 中它们都是隐式的。 +重要性:较低 +### JavaScript 中的命令模式 +命令模式在 JavaScript 中也比较简单,下面代码中对按钮和命令进行了抽离,因此可以复杂项目中可以使用命令模式将界面的代码和功能的代码交付给不同的人去写。 +```js +const setCommand = function(button, command) { + button.onClick = function() { + command.excute() + } +} + +// -------------------- 上面的界面逻辑由A完成,下面的由B完成 + +const menu = { + updateMenu: function() { + console.log('更新菜单') + }, +} + +const UpdateCommand = function(receive) { + return { + excute: receive.updateMenu, + } +} + +const updateCommand = UpdateCommand(menu) // 创建命令 + +const button1 = document.getElementById('button1') +setCommand(button1, updateCommand) +``` + +*** + +## 7.组合模式 +* 组合模式在对象间形成树形结构; +* 组合模式中基本对象和组合对象被一致对待; +* 无须关心对象有多少层,调用时只需在根部进行调用; + +### demo1 —— 宏命令 + +想象我们现在手上有个万能遥控器,当我们回家,按一下开关,下列事情将被执行: +1. 煮咖啡 +2. 打开电视、打开音响 +3. 打开空调、打开电脑 + +我们把任务划分为 3 类,效果图如下: +![任务划分](https://camo.githubusercontent.com/a44e97b789bd6e25d1aa13b15b12239c0566b015/687474703a2f2f6f71687473637573302e626b742e636c6f7564646e2e636f6d2f39633837636538333535313566336439623630613836613066323838393764392e6a70672d343030) +接着看看结合了命令模式和组合模式的具体实现: +```js +const MacroCommand = function() { + return { + lists: [], + add: function(task) { + this.lists.push(task) + }, + excute: function() { // ①:组合对象调用这里的 excute, + for (let i = 0; i < this.lists.length; i++) { + this.lists[i].excute() + } + }, + } +} + +const command1 = MacroCommand() // 基本对象 + +command1.add({ + excute: () => console.log('煮咖啡') // ②:基本对象调用这里的 excute, +}) + +const command2 = MacroCommand() // 组合对象 + +command2.add({ + excute: () => console.log('打开电视') +}) + +command2.add({ + excute: () => console.log('打开音响') +}) + +const command3 = MacroCommand() + +command3.add({ + excute: () => console.log('打开空调') +}) + +command3.add({ + excute: () => console.log('打开电脑') +}) + +const macroCommand = MacroCommand() +macroCommand.add(command1) +macroCommand.add(command2) +macroCommand.add(command3) + +macroCommand.excute() + +// 煮咖啡 +// 打开电视 +// 打开音响 +// 打开空调 +// 打开电脑 +``` + +可以看出在组合模式中基本对象和组合对象被一致对待,所以要保证基本对象(叶对象)和组合对象具有一致方法。 + +### demo2 —— 扫描文件夹 +扫描文件夹时,文件夹下面可以为另一个文件夹也可以为文件,我们希望统一对待这些文件夹和文件,这种情形适合使用组合模式。 +```js +const Folder = function(folder) { + this.folder = folder + this.lists = [] +} + +Folder.prototype.add = function(resource) { + this.lists.push(resource) +} + +Folder.prototype.scan = function() { + console.log('开始扫描文件夹:', this.folder) + for (let i = 0, folder; folder = this.lists[i++];) { + folder.scan() + } +} + +const File = function(file) { + this.file = file +} + +File.prototype.add = function() { + throw Error('文件下不能添加其它文件夹或文件') +} + +File.prototype.scan = function() { + console.log('开始扫描文件:', this.file) +} + +const folder = new Folder('根文件夹') +const folder1 = new Folder('JS') +const folder2 = new Folder('life') + +const file1 = new File('深入React技术栈.pdf') +const file2 = new File('JavaScript权威指南.pdf') +const file3 = new File('小王子.pdf') + +folder1.add(file1) +folder1.add(file2) + +folder2.add(file3) + +folder.add(folder1) +folder.add(folder2) + +folder.scan() + +// 开始扫描文件夹: 根文件夹 +// 开始扫描文件夹: JS +// 开始扫描文件: 深入React技术栈.pdf +// 开始扫描文件: JavaScript权威指南.pdf +// 开始扫描文件夹: life +// 开始扫描文件: 小王子.pdf +``` + +*** + +## 8.模板方法模式 +> 定义:在继承的基础上,在父类中定义好执行的算法。 +> +### 泡茶和泡咖啡 +来对比下泡茶和泡咖啡过程中的异同 + +|步骤|泡茶|泡咖啡| +|-|-|-| +|1|烧开水|烧开水| +|2|浸泡茶叶|冲泡咖啡| +|3|倒入杯子|倒入杯子| +|4|加柠檬|加糖| + +可以清晰地看出仅仅在步骤 2 和 4 上有细微的差别,下面着手实现: +```js +const Drinks = function() {} + +Drinks.prototype.firstStep = function() { + console.log('烧开水') +} + +Drinks.prototype.secondStep = function() {} + +Drinks.prototype.thirdStep = function() { + console.log('倒入杯子') +} + +Drinks.prototype.fourthStep = function() {} + +Drinks.prototype.init = function() { // 模板方法模式核心:在父类上定义好执行算法 + this.firstStep() + this.secondStep() + this.thirdStep() + this.fourthStep() +} + +const Tea = function() {} + +Tea.prototype = new Drinks + +Tea.prototype.secondStep = function() { + console.log('浸泡茶叶') +} + +Tea.prototype.fourthStep = function() { + console.log('加柠檬') +} + +const Coffee = function() {} + +Coffee.prototype = new Drinks + +Coffee.prototype.secondStep = function() { + console.log('冲泡咖啡') +} + +Coffee.prototype.fourthStep = function() { + console.log('加糖') +} + +const tea = new Tea() +tea.init() + +// 烧开水 +// 浸泡茶叶 +// 倒入杯子 +// 加柠檬 + +const coffee = new Coffee() +coffee.init() + +// 烧开水 +// 冲泡咖啡 +// 倒入杯子 +// 加糖 +``` + +### 钩子 +假如客人不想加佐料(糖、柠檬)怎么办,这时可以引人钩子来实现之,实现逻辑如下: +```js +// ... + +Drinks.prototype.ifNeedFlavour = function() { // 加上钩子 + return true +} + +Drinks.prototype.init = function() { // 模板方法模式核心:在父类上定义好执行算法 + this.firstStep() + this.secondStep() + this.thirdStep() + if (this.ifNeedFlavour()) { // 默认是 true,也就是要加调料 + this.fourthStep() + } +} + +// ... +const Coffee = function() {} + +Coffee.prototype = new Drinks() +// ... + +Coffee.prototype.ifNeedFlavour = function() { + return window.confirm('是否需要佐料吗?') // 弹框选择是否佐料 +} +``` + +*** + +## 9.享元模式 +享元模式是一种优化程序性能的模式,本质为减少对象创建的个数。 + +以下情况可以使用享元模式: +* 有大量相似的对象,占用了大量内存 +* 对象中大部分状态可以抽离为外部状态 + +### demo +某商家有 50 种男款内衣和 50 种款女款内衣,要展示它们 + +方案一:造 50 个塑料男模和 50 个塑料女模,让他们穿上展示,代码如下: +```js +const Model = function(gender, underwear) { + this.gender = gender + this.underwear = underwear +} + +Model.prototype.takephoto = function() { + console.log(`${this.gender}穿着${this.underwear}`) +} + +for (let i = 1; i < 51; i++) { + const maleModel = new Model('male', `第${i}款衣服`) + maleModel.takephoto() +} + +for (let i = 1; i < 51; i++) { + const female = new Model('female', `第${i}款衣服`) + female.takephoto() +} +``` + +方案二:造 1 个塑料男模特 1 个塑料女模特,分别试穿 50 款内衣 +```js +const Model = function(gender) { + this.gender = gender +} + +Model.prototype.takephoto = function() { + console.log(`${this.sex}穿着${this.underwear}`) +} + +const maleModel = new Model('male') +const femaleModel = new Model('female') + +for (let i = 1; i < 51; i++) { + maleModel.underwear = `第${i}款衣服` + maleModel.takephoto() +} + +for (let i = 1; i < 51; i++) { + femaleModel.underwear = `第${i}款衣服` + femaleModel.takephoto() +} +``` +对比发现:方案一创建了 100 个对象,方案二只创建了 2 个对象,在该 demo 中,gender(性别) 是内部对象,underwear(穿着) 是外部对象。 + +当然在方案二的 demo 中,还可以进一步改善: + +* 一开始就通过构造函数显示地创建实例,可用工场模式将其升级成可控生成 +* 在实例上手动添加 underwear 不是很优雅,可以在外部单独在写个 manager 函数 +```js +const Model = function(gender) { + this.gender = gender +} + +Model.prototype.takephoto = function() { + console.log(`${this.gender}穿着${this.underwear}`) +} + +const modelFactory = (function() { // 优化第一点 + const modelGender = {} + return { + createModel: function(gender) { + if (modelGender[gender]) { + return modelGender[gender] + } + return modelGender[gender] = new Model(gender) + } + } +}()) + +const modelManager = (function() { + const modelObj = {} + return { + add: function(gender, i) { + modelObj[i] = { + underwear: `第${i}款衣服` + } + return modelFactory.createModel(gender) + }, + copy: function(model, i) { // 优化第二点 + model.underwear = modelObj[i].underwear + } + } +}()) + +for (let i = 1; i < 51; i++) { + const maleModel = modelManager.add('male', i) + modelManager.copy(maleModel, i) + maleModel.takephoto() +} + +for (let i = 1; i < 51; i++) { + const femaleModel = modelManager.add('female', i) + modelManager.copy(femaleModel, i) + femaleModel.takephoto() +} +``` + +*** + +## 10.职责链模式 +职责链模式:类似多米诺骨牌,通过请求第一个条件,会持续执行后续的条件,直到返回结果为止。 +![职责链模式](https://camo.githubusercontent.com/cb2073f5e9c165843754e0b5984652c3291e88a6/687474703a2f2f6f71687473637573302e626b742e636c6f7564646e2e636f6d2f63626338633130626261653230326263643234336636623037303464653362612e6a70672d333030) +重要性:4 星,在项目中能对 if-else 语句进行优化 +### 场景 demo +场景:某电商针对已付过定金的用户有优惠政策,在正式购买后,已经支付过 500 元定金的用户会收到 100 元的优惠券,200 元定金的用户可以收到 50 元优惠券,没有支付过定金的用户只能正常购买。 +```js +// orderType: 表示订单类型,1:500 元定金用户;2:200 元定金用户;3:普通购买用户 +// pay:表示用户是否已经支付定金,true: 已支付;false:未支付 +// stock: 表示当前用于普通购买的手机库存数量,已支付过定金的用户不受此限制 + +const order = function( orderType, pay, stock ) { + if ( orderType === 1 ) { + if ( pay === true ) { + console.log('500 元定金预购,得到 100 元优惠券') + } else { + if (stock > 0) { + console.log('普通购买,无优惠券') + } else { + console.log('库存不够,无法购买') + } + } + } else if ( orderType === 2 ) { + if ( pay === true ) { + console.log('200 元定金预购,得到 50 元优惠券') + } else { + if (stock > 0) { + console.log('普通购买,无优惠券') + } else { + console.log('库存不够,无法购买') + } + } + } else if ( orderType === 3 ) { + if (stock > 0) { + console.log('普通购买,无优惠券') + } else { + console.log('库存不够,无法购买') + } + } +} + +order( 3, true, 500 ) // 普通购买,无优惠券 +``` + +下面用职责链模式改造代码: +```js +const order500 = function(orderType, pay, stock) { + if ( orderType === 1 && pay === true ) { + console.log('500 元定金预购,得到 100 元优惠券') + } else { + order200(orderType, pay, stock) + } +} + +const order200 = function(orderType, pay, stock) { + if ( orderType === 2 && pay === true ) { + console.log('200 元定金预购,得到 50 元优惠券') + } else { + orderCommon(orderType, pay, stock) + } +} + +const orderCommon = function(orderType, pay, stock) { + if (orderType === 3 && stock > 0) { + console.log('普通购买,无优惠券') + } else { + console.log('库存不够,无法购买') + } +} + +order500( 3, true, 500 ) // 普通购买,无优惠券 +``` + +改造后可以发现代码相对清晰了,但是链路代码和业务代码依然耦合在一起,进一步优化: +```js +// 业务代码 +const order500 = function(orderType, pay, stock) { + if ( orderType === 1 && pay === true ) { + console.log('500 元定金预购,得到 100 元优惠券') + } else { + return 'nextSuccess' + } +} + +const order200 = function(orderType, pay, stock) { + if ( orderType === 2 && pay === true ) { + console.log('200 元定金预购,得到 50 元优惠券') + } else { + return 'nextSuccess' + } +} + +const orderCommon = function(orderType, pay, stock) { + if (orderType === 3 && stock > 0) { + console.log('普通购买,无优惠券') + } else { + console.log('库存不够,无法购买') + } +} + +// 链路代码 +const chain = function(fn) { + this.fn = fn + this.sucessor = null +} + +chain.prototype.setNext = function(sucessor) { + this.sucessor = sucessor +} + +chain.prototype.init = function() { + const result = this.fn.apply(this, arguments) + if (result === 'nextSuccess') { + this.sucessor.init.apply(this.sucessor, arguments) + } +} + +const order500New = new chain(order500) +const order200New = new chain(order200) +const orderCommonNew = new chain(orderCommon) + +order500New.setNext(order200New) +order200New.setNext(orderCommonNew) + +order500New.init( 3, true, 500 ) // 普通购买,无优惠券 +``` + +重构后,链路代码和业务代码彻底地分离。假如未来需要新增 order300,那只需新增与其相关的函数而不必改动原有业务代码。 +另外结合 AOP 还能简化上述链路代码: +```js +// 业务代码 +const order500 = function(orderType, pay, stock) { + if ( orderType === 1 && pay === true ) { + console.log('500 元定金预购,得到 100 元优惠券') + } else { + return 'nextSuccess' + } +} + +const order200 = function(orderType, pay, stock) { + if ( orderType === 2 && pay === true ) { + console.log('200 元定金预购,得到 50 元优惠券') + } else { + return 'nextSuccess' + } +} + +const orderCommon = function(orderType, pay, stock) { + if (orderType === 3 && stock > 0) { + console.log('普通购买,无优惠券') + } else { + console.log('库存不够,无法购买') + } +} + +// 链路代码 +Function.prototype.after = function(fn) { + const self = this + return function() { + const result = self.apply(self, arguments) + if (result === 'nextSuccess') { + return fn.apply(self, arguments) // 这里 return 别忘记了~ + } + } +} + +const order = order500.after(order200).after(orderCommon) + +order( 3, true, 500 ) // 普通购买,无优惠券 +``` +职责链模式比较重要,项目中能用到它的地方会有很多,用上它能解耦 1 个请求对象和 n 个目标对象的关系。 + +*** + +## 11.中介者模式 +中介者模式:对象和对象之间借助第三方中介者进行通信。 +![中介者模式](https://camo.githubusercontent.com/8411d6ad7b3c4e4f4fa3a14115f33428f5e4ab0f/687474703a2f2f6f71687473637573302e626b742e636c6f7564646e2e636f6d2f61653039353866383539393039373863343862336136616132636137366561312e6a70672d343030) +### 场景 demo +一场测试结束后,公布结果:告知解答出题目的人挑战成功,否则挑战失败。 +```js +const player = function(name) { + this.name = name + playerMiddle.add(name) +} + +player.prototype.win = function() { + playerMiddle.win(this.name) +} + +player.prototype.lose = function() { + playerMiddle.lose(this.name) +} + +const playerMiddle = (function() { // 将就用下这个 demo,这个函数当成中介者 + const players = [] + const winArr = [] + const loseArr = [] + return { + add: function(name) { + players.push(name) + }, + win: function(name) { + winArr.push(name) + if (winArr.length + loseArr.length === players.length) { + this.show() + } + }, + lose: function(name) { + loseArr.push(name) + if (winArr.length + loseArr.length === players.length) { + this.show() + } + }, + show: function() { + for (let winner of winArr) { + console.log(winner + '挑战成功;') + } + for (let loser of loseArr) { + console.log(loser + '挑战失败;') + } + }, + } +}()) + +const a = new player('A 选手') +const b = new player('B 选手') +const c = new player('C 选手') + +a.win() +b.win() +c.lose() + +// A 选手挑战成功; +// B 选手挑战成功; +// C 选手挑战失败; +``` +在这段代码中 A、B、C 之间没有直接发生关系,而是通过另外的 playerMiddle 对象建立链接,姑且将之当成是中介者模式了。 + +*** + +## 12.装饰者模式 +装饰器模式:动态地给函数赋能。 + +### JavaScript 的装饰者模式 +生活中的例子:天气冷了,就添加衣服来保暖;天气热了,就将外套脱下;这个例子很形象地含盖了装饰器的神韵,随着天气的冷暖变化,衣服可以动态的穿上脱下。 +```js +let wear = function() { + console.log('穿上第一件衣服') +} + +const _wear1 = wear + +wear = function() { + _wear1() + console.log('穿上第二件衣服') +} + +const _wear2 = wear + +wear = function() { + _wear2() + console.log('穿上第三件衣服') +} + +wear() + +// 穿上第一件衣服 +// 穿上第二件衣服 +// 穿上第三件衣服 +``` + +这种方式有以下缺点:1:临时变量会变得越来越多;2:this 指向有时会出错 +### AOP 装饰函数 +```js +// 前置代码 +Function.prototype.before = function(fn) { + const self = this + return function() { + fn.apply(this, arguments) + return self.apply(this, arguments) + } +} + +// 后置代码 +Function.prototype.after = function(fn) { + const self = this + return function() { + self.apply(this, arguments) + return fn.apply(this, arguments) + } +} +``` +用后置代码来实验下上面穿衣服的 demo, +```js +const wear1 = function() { + console.log('穿上第一件衣服') +} + +const wear2 = function() { + console.log('穿上第二件衣服') +} + +const wear3 = function() { + console.log('穿上第三件衣服') +} + +const wear = wear1.after(wear2).after(wear3) +wear() + +// 穿上第一件衣服 +// 穿上第二件衣服 +// 穿上第三件衣服 +``` + +但这样子有时会污染原生函数,可以做点通变 +```js +const after = function(fn, afterFn) { + return function() { + fn.apply(this, arguments) + afterFn.apply(this, arguments) + } +} + +const wear = after(after(wear1, wear2), wear3) +wear() +``` + +*** + +## 13.状态模式 +状态模式:将事物内部的每个状态分别封装成类,内部状态改变会产生不同行为。 + +优点:用对象代替字符串记录当前状态,状态易维护 +缺点:需编写大量状态类对象 + +### 场景 demo +某某牌电灯,按一下按钮打开弱光,按两下按钮打开强光,按三下按钮关闭灯光。 +```js +// 将状态封装成不同类 +const weakLight = function(light) { + this.light = light +} + +weakLight.prototype.press = function() { + console.log('打开强光') + this.light.setState(this.light.strongLight) +} + +const strongLight = function(light) { + this.light = light +} + +strongLight.prototype.press = function() { + console.log('关灯') + this.light.setState(this.light.offLight) +} + +const offLight = function(light) { + this.light = light +} + +offLight.prototype.press = function() { + console.log('打开弱光') + this.light.setState(this.light.weakLight) +} + +const Light = function() { + this.weakLight = new weakLight(this) + this.strongLight = new strongLight(this) + this.offLight = new offLight(this) + this.currentState = this.offLight // 初始状态 +} + +Light.prototype.init = function() { + const btn = document.createElement('button') + btn.innerHTML = '按钮' + document.body.append(btn) + const self = this + btn.addEventListener('click', function() { + self.currentState.press() + }) +} + +Light.prototype.setState = function(state) { // 改变当前状态 + this.currentState = state +} + +const light = new Light() +light.init() + +// 打开弱光 +// 打开强光 +// 关灯 +``` + +### 非面向对象实现的状态模式 +借助于 JavaScript 的委托机制,可以像如下实现状态模式: +```js +const obj = { + 'weakLight': { + press: function() { + console.log('打开强光') + this.currentState = obj.strongLight + } + }, + 'strongLight': { + press: function() { + console.log('关灯') + this.currentState = obj.offLight + } + }, + 'offLight': { + press: function() { + console.log('打开弱光') + this.currentState = obj.weakLight + } + }, +} + +const Light = function() { + this.currentState = obj.offLight +} + +Light.prototype.init = function() { + const btn = document.createElement('button') + btn.innerHTML = '按钮' + document.body.append(btn) + const self = this + btn.addEventListener('click', function() { + self.currentState.press.call(self) // 通过 call 完成委托 + }) +} + +const light = new Light() +light.init() +``` + +*** + +## 14.适配者模式 +适配者模式:主要用于解决两个接口之间不匹配的问题。 +### demo +```js +// 老接口 +const zhejiangCityOld = (function() { + return [ + { + name: 'hangzhou', + id: 11, + }, + { + name: 'jinhua', + id: 12 + } + ] +}()) + +console.log(getZhejiangCityOld()) + +// 新接口希望是下面形式 +{ + hangzhou: 11, + jinhua: 12, +} + +// 这时候就可采用适配者模式 +const const adaptor = (function(oldCity) { + const obj = {} + for (let city of zhejiangCityOld) { + obj[city.name] = city.id + } + return obj +}()) +``` + + +> 原文地址 [JavaScript 中常见设计模式整理](https://juejin.im/post/5afe6430518825428630bc4d) \ No newline at end of file diff --git "a/Cute-Article/article/39-\344\275\234\344\270\272\345\211\215\347\253\257\351\234\200\350\246\201\344\272\206\350\247\243\347\232\204\345\274\200\346\272\220\345\215\217\350\256\256\347\237\245\350\257\206.md" "b/Cute-Article/article/39-\344\275\234\344\270\272\345\211\215\347\253\257\351\234\200\350\246\201\344\272\206\350\247\243\347\232\204\345\274\200\346\272\220\345\215\217\350\256\256\347\237\245\350\257\206.md" new file mode 100644 index 00000000..8345ff42 --- /dev/null +++ "b/Cute-Article/article/39-\344\275\234\344\270\272\345\211\215\347\253\257\351\234\200\350\246\201\344\272\206\350\247\243\347\232\204\345\274\200\346\272\220\345\215\217\350\256\256\347\237\245\350\257\206.md" @@ -0,0 +1,112 @@ +* 作为前端工程师,开发中在所难免会用到一些开源框架,而每个框架都有自己的开源协议,每个开源协议之间有什么差别呢? 如果你要开源一个项目,又应该选择哪种开源协议呢? +* 许多开发者,对于开源协议的认知很少,本文从这些常用的前端框架入手,介绍开源常用开源协议的基础知识。 + +### 什么是开源协议? +根据 [开源协议](https://en.wikipedia.org/wiki/Open-source_license) 在维基百科的定义: +> 开源许可是一种计算机软件和其他产品的许可类型,允许使用、修改或在定义的条款和条件下使用、修改或共享的源代码、蓝图和设计。 +> 这允许终端用户和商业公司对源代码、图纸或设计进行审查和修改,以满足自己的定制、好奇心或故障排除的需要。 +> 开源许可的软件大多是免费的,尽管这并不一定是必须的。许可证只允许非商业的重新分配或修改个人使用的源代码,通常不被认为是开源许可。 +> 然而,开源许可可能会有一些限制,尤其是对软件的起源的表达,比如要求保留作者的名字和代码中的版权声明,或要求重新分配授权软件只有在相同的许可(如copyleft许可证)。 +> 一组流行的开源软件许可证是由开源计划(OSI)根据其开源定义(OSD)批准的。 + + +### 为什么要选用开源协议? +在 [GcsSloop](http://www.gcssloop.com/) 写的文章 [程序员不可不知的版权协议](http://www.gcssloop.com/tips/choose-license) 中给出了很好的概括。 +* 首先是对作者的保护,防止知识成果被恶意利用。开源协议中一般都包含免责声明(禁止代码的作者承担代码被使用后产生的风险及后果),比如你开源了一个破解智能锁的代码,如果有人利用这个去盗窃导致他人损失,你是无需承担责任的。 +* 其次是对使用者的保护,方便使用者。使用者一看就知道自己允许进行哪些操作,不允许进行哪些操作。未添加协议的代码默认是作者保留所有权利的(对此不同国家的法律可能稍微存在区别),这就像一颗定时炸弹,如果你在项目中使用了这一份没有协议的代码,原作者只要能证明你未经许可使用了他的代码,是能够起诉你的。 + +### 当前主流开源许可证(GPL、BSD、MIT、Mozilla、Apache、LGPL)和它们的异同? +![当前主流开源许可证](https://pic2.zhimg.com/80/v2-1c76c3c63f4db1727ebe07815423f3b7_hd.jpg) +相关概念解析: +> * 协议和版权信息(License and copyright notice):在代码中保留作者提供的协议和版权信息 +> * 声明变更(State Changes):在代码中声明对原来代码的重大修改及变更 +> * 公开源码(Disclose Source):代码必需公开。如果是基于LGPL协议 下,则只需使用的开源代码公开,不必将整个软件源码公开 +> * 库引用(Library usage):该库可以用于商业软件中 +> * 责任承担(Hold Liable):代码的作者承担代码使用后的风险及产生的后果 +> * 商标使用(Use Trademark):可以使用作者的姓名,作品的Logo,或商标 +> * 附加协议(Sublicensing):允许在软件分发传播过程中附加上原来没有的协议条款等 + + +### 当前前端主流框架选取的开源协议 +纵观比较常用的前端框架,用的最广泛的便是 MIT 开源协议。 +> * Vue: MIT [点击阅读](https://github.com/vuejs/vue/blob/dev/LICENSE ) +> * React:MIT [点击阅读]( https://github.com/facebook/react/blob/master/LICENSE) +> * Element: MIT [点击阅读](https://github.com/ElemeFE/element/blob/master/LICENSE ) +> * Ant Design:MIT [点击阅读](https://github.com/ant-design/ant-design/blob/master/LICENSE ) + +列举的框架开源协议都是 MIT 。那么为什么选择 MIT 呢? +MIT是一种简短而简单的许可,只需要保留版权和许可通知。许可的作品、修改和更大的作品可以在不同的条件下分发,并且没有源代码。MIT允许别人用作者的代码做任何事情,但必须保证作者的所有权,并且作者无须承担代码使用产生的风险。 +其中,要重点说一下 React 的开源协议,从 github 的提交历史来看,React 的开源协议经历了一个动荡的过程。从 LICENCE 的提交历史看,Facebook 对专利的重视程度可见一斑。 +![React的开源协议](https://pic2.zhimg.com/80/v2-3d9fea33d17605fefe04d121b0a55b4d_hd.jpg) +去年知乎上一个《如何看待百度要求内部全面停止使用 React / React Native?》的文章引起了前端界的热议,事情的起因是大家发现了 Facebook 专利许可证上的描述暗藏玄机。在技术开源的世界,对于开发者而言,许可证就是他们使用开源软件的 “用户协议”。而 Facebook 的开源方式跟其他家都不太一样,别家一般用的都是开源社区公认通用的许可证,而 Facebook 使用的是两个许可证,第一个是通用的 BSD 许可证,第二个是自己写的专利许可证 (patent grant)。 +而在 React 的开源协议中这么写到: +![ React 的开源协议](https://pic4.zhimg.com/80/v2-a04b940e9f42b1c9801b2612d04a6bee_hd.jpg) +意思就是: +当发生下列情况时,facebook 有权益吊销你的 React 使用权: +> * 与 facebook 及其附属机构发生利益冲突 +> * 同任何一个和 facebook 有关的组织发生了法律纠纷 +> * 同任何与 React 有关的组织发生利益冲突 + + +翻译成大白话就是:如果你觉得 Facebook 侵犯了你的知识产权,同时你的核心产品是基于 React 实现,如果你想起诉 Facebook,就要权衡一下了,因为根据条款它有权利吊销你的 React 使用权。或者说你用 React 做了一个产品并且在某些领域对 Facebook 构成了利益冲突,那么它就可以强制你的产品下线。 + +可以说,一旦你开始使用 React 去构建你的核心产品,你的公司就被 facebook 埋下了一颗定时炸弹,并且,炸弹的引爆按钮就握在 facebook 手中。 + +其实这种事情,从去年就在前端技术圈开吵,后来愈演愈烈,形势每况日下:开源社区在更多 Facebook 开源的热门项目中发现了相同的许可证模式和条款。开发者认为 Facebook 的这种许可证模式正在毒害社区,污染开源精神。而且 Apache 软件基金会宣布所有使用 Apache 开源协议的软件都不得使用带有 Facebook BSD + 专利许可证模式的组件。 + +不过 Facebook 最后还是意识到了这些问题,修改了开源协议。 +![Facebook开源协议](https://pic2.zhimg.com/80/v2-6809df1f7f8a7915a3f8909a5b9cff5d_hd.jpg) + +### 如何为我们的项目选择一个开源协议? +首先,我们要清楚我们选择开源的目的是什么? +作为个人,在开源的情况下,我们可以帮助他人,也可以获得他人的帮助,还是一个提升个人代码质量的好方法,同样,也是一个展示自己能力的好方法。世界上开源软件协议的种类非常之多,并且同一款协议有很多变种,协议太宽松会导致作者丧失对作品的很多权利,太严格又不便于使用者使用及作品的传播,所以开源作者要考虑自己对作品想保留哪些权利,放开哪些限制。 +作为公司,代码开源后,会提升公司的地位,树立一个良好的品牌形象,也可以帮助公司发掘潜在员工。 +那么,我们如何选择适合我们的开源许可证呢? + +由一张图直观了解如何选取所需要的开源许可证。(原著:乌克兰程序员Paul Bagwell,翻译:阮一峰) +![开源许可证](https://pic4.zhimg.com/80/v2-253a7b1819e2af555ed0a7e0f11a0b59_hd.jpg) + +举例来说: +如果我只是想专心的写代码,那么可以选择 MIT ,MIT在保证了作者的所有权的前提下允许别人使用作者的代码,且作者不需要承担使用时的风险。如果我想保护我的代码、专利,那么可以选择 Apache ,Apache 与 MIT 的区别就是提供了专利贡献者的授权,使用者需要明确这一点。 + +Github 专门发布了一个网站 [Choosing an OSS license doesn’t need to be scary](https://choosealicense.com/) 来帮助开源项目开发者。 + +> * 我想要一个简单宽松的许可证建议: MIT 许可证。这是一个宽松的、简明扼要的许可证,只要用户在项目副本中包含了版权声明和许可声明,他们就可以拿你的代码做任何想做的事情,你也无需承担任何责任。 +> 使用该许可证的项目:jQuery、Rails +> * 我比较关心专利 +> 建议: Apache许可证。这类似于 MIT 许可证,但它同时还包含了贡献者向用户提供专利授权相关的条款。 +> 使用该许可证的项目:Apache、SVN和NuGet +> * 我关心项目的共享改进 +> 建议:GPL( V2或 V3)许可证。这是一种 copyleft 许可证,要求修改项目代码的用户再次分发源码或二进制代码时,必须公布他的相关修改。V3版本与V2类>似,但其进一步约束了在某些限制软件更改的硬件上的使用范围。 +> 使用该许可证的项目:Linux、Git +> * 我的开源项目不是代码 +> 建议: Creative Commons。这是一个相对宽松的版权协议。它只保留几种了权利(some rights reserved)。使用者可以明确知道所有者的权利,不容易侵犯对>方的版权,作品可以得到有效传播。作为作者,你可以选择以下1~4种权利组合: +> 1) 署名(Attribution,简写为BY):必须提到原作者。 +> 2) 非商业用途(Noncommercial,简写为NC):不得用于盈利性目的。 +> 3) 禁止演绎(No Derivative Works,简写为ND):不得修改原作品, 不得再创作。 +> 4) 相同方式共享(Share Alike,简写为SA):允许修改原作品,但必须使用相同的许可证发布。 +> * 更多选择 +> Licenses - [http://ChooseALicense.com](https://choosealicense.com/),这里提供了Apache/ GPL/ MIT/ Artistic/ Eclipse/ BSD/ LGPL/ Mozilla/ No License/ Public Domain >Dedication 协议的适用情形、许可内容、禁止内容,及协议全文。 + +### 如何为代码添加开源协议? +#### GitHub +1. 首先需要注册一个 GitHub 账号,并登录 +2. 在 GitHub 上选择创建一个新的 repository +![GitHub](https://pic4.zhimg.com/80/v2-5ab3d4f220e6524d445135c029014fe6_hd.jpg) +3. 进入创建 repository 页面后,输入基本信息后,点击右下角的 Add a license 选择开源协议,默认是 none。对应的 license 可以直接选择,也可以输入自己想要的 license +![GitHub](https://pic4.zhimg.com/80/v2-fb3c51c8d10b9767792603877e32ec65_hd.jpg) +![GitHub](https://pic1.zhimg.com/80/v2-209460d2fa43ea984f1f19de8606be3e_hd.jpg) + +4. 点击最下方 Create repository,就创建成功了。 +5. 创建成功后,代码库中就可以看到自动生成了一个 LICENSE 文件。 + +### 参考文献 +> [选择一个开源软件协议](http://choosealicense.online/) +> [程序员不可不知的版权协议](http://www.gcssloop.com/tips/choose-license) +> [开源许可证都有什么区别,一般开源项目用什么许可证?](https://www.zhihu.com/question/28292322) +> [都在封杀 React/React Native ,那我到底还该不该继续学呢?](https://zhuanlan.zhihu.com/p/29492362) +> [React开源协议之争知多少?](https://cauu.github.io/2017/09/React-Opensource-license/) +> [how to choose a license](https://www.cnblogs.com/Wayou/p/how_to_choose_a_license.html) + + +> [原文地址](https://zhuanlan.zhihu.com/p/35876146) \ No newline at end of file diff --git "a/4-\345\237\272\344\272\216Vue\351\205\215\347\275\256axios.md" "b/Cute-Article/article/4-\345\237\272\344\272\216Vue\351\205\215\347\275\256axios.md" similarity index 100% rename from "4-\345\237\272\344\272\216Vue\351\205\215\347\275\256axios.md" rename to "Cute-Article/article/4-\345\237\272\344\272\216Vue\351\205\215\347\275\256axios.md" diff --git "a/Cute-Article/article/40-\350\247\243\345\257\206Vue SSR.md" "b/Cute-Article/article/40-\350\247\243\345\257\206Vue SSR.md" new file mode 100644 index 00000000..ab2faccf --- /dev/null +++ "b/Cute-Article/article/40-\350\247\243\345\257\206Vue SSR.md" @@ -0,0 +1,251 @@ +## 1.引言 +最近笔者和小伙伴在研究Vue SSR,但是市面上充斥了太多的从0到1的文章,对大家理解这其中的原理帮助并不是很大,因此,本文将从 *Vue SSR的构建流程、运行流程、SSR的特点和利弊* 这几方面对Vue SSR有一个较为详细的介绍。最后还将附上一个笔者实现的 *去除Vue全家桶的Demo案例* 。 + +## 2.剖析构建流程 +首先我们镇上一张官网给出的构建图: +![Vue SSR构建流程](https://pic3.zhimg.com/80/v2-8f5dc75e94e8cfe49416e460f6bd2a0e_hd.jpg) + +### app.js入口文件 +`app.js` 是我们的通用`entry`,它的作用就是构建一个Vue的实例以供服务端和客户端使用,注意一下,在纯客户端的程序中我们的`app.js`将会挂载实例到`dom`中,而在`ssr`中这一部分的功能放到了`Client entry`中去做了。 + +### 两个entry +接下里我们来看`Client entry`和`Server entry`,这两者分别是客户端的入口和服务端的入口。*Client entry的功能很简单,就是挂载我们的Vue实例到指定的dom元素上*;`Server entry`是一个使用`export`导出的函数。主要负责调用组件内定义的获取数据的方法,获取到SSR渲染所需数据,并存储到上下文环境中。*这个函数会在每一次的渲染中重复的调用*。 + +### webpack打包构建 +然后我们的服务端代码和客户端代码通过`webpack`分别打包,生成`Server Bundle`和`Client Bundle`,前者会运行在服务器上通过node生成预渲染的`HTML字符串`,发送到我们的客户端以便完成初始化渲染;而客户端bundle就自由了,初始化渲染完全不依赖它了。客户端拿到服务端返回的HTML字符串后,会去“激活”这些静态HTML,是其变成由`Vue动态管理`的DOM,以便响应后续数据的变化。 + +## 3.剖析运行流程 +到这里我们该谈谈`ssr`的程序是怎么跑起来的了。首先我们得去构建一个vue的实例,也就是我们前面构建流程中说到的`app.js`做的事情,但是这里不同于传统的客户端渲染的程序,我们*需要用一个工厂函数去封装它,以便每一个用户的请求都能够返回一个新的实例,也就是官网说到的避免交叉污染了*。 + +然后我们可以暂时移步到服务端的`entry`中了,这里要做的就是拿到当前路由匹配的组件,调用组件里定义的一个方法(官网取名叫`asyncData`)拿到初始化渲染的数据,而这个方法要做的也很简单,就是去调用我们`vuex store`中的方法去异步获取数据。 + +接下来`node服务器`如期启动了,跑的是我们刚写好的服务端`entry`里的函数。在这里还要做的就是将我们刚刚构建好的Vue实例渲染成`HTML字符串`,然后将拿到的数据混入我们的`HTML字符串`中,最后发送到我们客户端。 + +打开浏览器的network,我们看到了初始化渲染的HTML,并且是我们想要初始化的结构,且完全不依赖于客户端的js文件了。再仔细研究研究,里面有初始化的dom结构,有css,还有一个script标签。script标签里把我们在服务端`entry`拿到的数据挂载了`window`上。原来只是一个纯静态的HTML页面啊,没有任何的交互逻辑,所以啊,现在知道为啥子需要服务端跑一个`vue客户端`再跑一个`vue`了,服务端的`vue`只是混入了个数据渲染了个静态页面,客户端的`vue`才是去实现交互的! +![chrome network](https://pic1.zhimg.com/80/v2-8354c78be3249def1cfc6b40d795c3a4_hd.jpg) + +顺着前面的思路,我们该看客户端的`entry`了。在这里客户端拿到存在`window`中的数据混入我们客户端的`vuex`中,然后分析数据去执行我们熟悉的其余客户端操作了。 + +## 4.SSR独特之处 +在SSR中,创建`Vue实例`、创建`store`和创建`router`都是套了一层`工厂函数`的,目的就是`避免数据的交叉污染`。 + +在服务端只能执行生命周期中的`created`和`beforeCreate`,原因是在服务端是无法操纵dom的,所以可想而知其他的周期也就是不能执行的了。 + +服务端渲染和客户端渲染不同,需要创建两个`entry`分别跑在`服务端`和`客户端`,并且需要*webpack对其分别打包*; + +SSR服务端请求不带`cookie`,需要手动拿到浏览器的`cookie`传给服务端的请求。[实现方式戳这里](https://www.mmxiaowu.com/article/596cbb2d436eb550a5423c30)。 + +SSR要求dom结构规范,因为浏览器会自动给HTML添加一些结构比如tbody,但是客户端进行混淆服务端放回的HTML时,不会添加这些标签,导致混淆后的HTML和浏览器渲染的HTML不匹配。 + +*性能问题需要多加关注*。 +* vue.mixin、axios拦截请求使用不当,会内存泄漏。[原因戳这里](https://github.com/vuejs/vue/issues/5089) +* lru-cache向内存中缓存数据,需要合理缓存改动不频繁的资源。 + +## 5.可能是把双刃剑 +### SSR的优点 + +* 更利于SEO。 + +不同爬虫工作原理类似,只会爬取源码,不会执行网站的任何脚本(Google除外,据说Googlebot可以运行javaScript)。 +使用了`Vue`或者其它`MVVM框架`之后,页面大多数DOM元素都是在客户端根据js动态生成,可供爬虫抓取分析的内容大大减少。 +另外,浏览器爬虫不会等待我们的数据完成之后再去抓取我们的页面数据。服务端渲染返回给客户端的是已经获取了异步数据并执行JavaScript脚本的最终HTML,网络爬中就可以抓取到完整页面的信息。 + +* 更利于首屏渲染 +首屏的渲染是node发送过来的html字符串,并不依赖于js文件了,这就会使用户更快的看到页面的内容。尤其是针对大型单页应用,打包后文件体积比较大,普通客户端渲染加载所有所需文件时间较长,首页就会有一个很长的白屏等待时间。 + + +## 6.SSR的局限 + +* 服务端压力较大 +本来是通过客户端完成渲染,现在统一到服务端node服务去做。尤其是高并发访问的情况,会大量占用服务端CPU资源; + +* 开发条件受限 +在服务端渲染中,`created`和`beforeCreate`之外的生命周期钩子不可用,因此项目引用的第三方的库也不可用其它生命周期钩子,这对引用库的选择产生了很大的限制; + +* 学习成本相对较高 +除了对`webpack`、`Vue`要熟悉,还需要掌握`node`、`Express`相关技术。相对于客户端渲染,项目构建、部署过程更加复杂。 + +## 6.去除VUEX的SSR实践 +先附上demo地址,[戳这里](https://github.com/LNoe-lzy/vue-ssr-demo/tree/vue-ssr-without-vuex)! + +说在前面: + +* vue-router不是必须的,不用router其实做个vue的[preRender](https://github.com/chrisvfritz/prerender-spa-plugin)就可以了,完全没必要做ssr; +* vuex不是必须的,vuex是实现我们客户端和服务端的状态共享的关键,我们可以不使用vuex,但是我们得去实现一套数据预取的逻辑; + +官网的demo大而全,集成了`vue-router`和`vuex`,想想我们的项目如果没有使用到这两者,光引入就又需要改造成本,这并不是我们想搞的“丝滑般”过渡,接下来笔者将带领大家一步一步的做个“啥都没有的”demo。 + +在此笔者的思路是:*构造一个Vue的实例,那么我们可以用这个实例的data来存储我们的预取数据,而用methods中的方法去做数据的异步获取,这样我们只在需要预取数据的组件中去调用这个方法就可以了*。 + +首先我们需要让我们的组件“共享”这个EventBus,为此笔者简单的封装了一个plugin: +```js +export default { + install (Vue) { + const EventBus = new Vue({ + data () { + return { + list: [], + nav: [] + } + }, + methods: { + getList () { + // get list + }, + getNav () { + // get nav + } + } + }) + + Vue.prototype.$events = EventBus + Vue.$events = EventBus + } +} +``` + +然后我们需要在`main.js`中`export`出我们的`EventBus`以便两个`entry`使用。这样我们的`main.js`就像下面这样: +```js +import Vue from 'vue' +import App from './App' +import EventBus from './event' + +Vue.use(EventBus) +Vue.config.devtools = true + +export function createApp () { + const app = new Vue({ + // 注入 router 到根 Vue 实例 + router, + render: h => h(App) + }) + + return { app, router, eventBus: app.$events } +} +``` + +接下来是我们的两个`entry`了。`server`用来匹配我们的组件并调用组件的`asyncData`方法去获取数据,`client`用来将预渲染的数据存储到我们`eventBus`中的`data`中。 +```js +// server +import { createApp } from './main' + +export default context => { + return new Promise((resolve, reject) => { + const { app, eventBus, App } = createApp() + // 这里笔者的demo比较简单,仅app组件需要预取数据,复杂业务可以递归遍历哈; + const matchedComponents = [App] + + Promise.all(matchedComponents.map(({ asyncData }) => asyncData && asyncData({ + eventBus + }))).then(() => { + context.state = eventBus._data + resolve(app) + }).catch(reject) + }) +} + + +// client +import Vue from 'vue' +import { createApp } from './main' +const { app, eventBus } = createApp() + +if (window.__INITIAL_STATE__) { + eventBus._data = window.__INITIAL_STATE__ +} + +app.$mount('#app') +``` + +然后我们需要改造我们的组件了,只需要定义一个`async`方法去调用`EventBus`中的方法获取,考虑到服务端只会执行`beforeCreate`和`created`两个生命周期而`beforeCreate`不能拿到`data`,所以我们需要在`created`中去做数据的获取。 +```js +// 服务端渲染数据预取; +asyncData ({ store, eventBus }) { + return eventBus.getNav() +} +// 将服务端拿到的数据混入vue组件中; +created () { + this.nav = this.$events.nav +} +``` + +然后是`webpack`的改造了,`webpack`的配置其实和纯客户端应用类似,为了区分客户端和服务端两个环境我们将配置分为`base`、`client`和`server`三部分,`base`就是我们的通用基础配置,而`client`和`server`分别用来打包我们的客户端和服务端代码。 + +首先是`webpack.server.conf.js`,用于生成`server bundle`来传递给`createBundleRenderer函数`在node服务器上调用,入口文件是我们的`entry-server`: +```js +const webpack = require('webpack') +const merge = require('webpack-merge') +const nodeExternals = require('webpack-node-externals') +const baseConfig = require('./webpack.base.conf.js') +const VueSSRServerPlugin = require('vue-server-renderer/server-plugin') +// 去除打包css的配置 +baseConfig.module.rules[1].options = '' + +module.exports = merge(baseConfig, { + entry: './src/entry-server.js', + // 以 Node 适用方式导入 + target: 'node', + // 对 bundle renderer 提供 source map 支持 + devtool: '#source-map', + output: { + filename: 'server-bundle.js', + libraryTarget: 'commonjs2' + }, + externals: nodeExternals({ + whitelist: /\.css$/ + }), + plugins: [ + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), + 'process.env.VUE_ENV': '"server"' + }), + // 这是将服务器的整个输出 + // 构建为单个 JSON 文件的插件。 + // 默认文件名为 `vue-ssr-server-bundle.json` + new VueSSRServerPlugin() + ] +}) +``` +其次是`webpack.client.conf.js`,这里我们可以根据官方的配置生成`clientManifest`,自动推断和注入资源预加载,以及 css 链接 / script 标签到所渲染的 HTML。入口是我们的`client-server`: +```js +const webpack = require('webpack') +const merge = require('webpack-merge') +const base = require('./webpack.base.conf') +const VueSSRClientPlugin = require('vue-server-renderer/client-plugin') + +const config = merge(base, { + entry: { + app: './src/entry-client.js' + }, + plugins: [ + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), + 'process.env.VUE_ENV': '"client"' + }), + new webpack.optimize.CommonsChunkPlugin({ + name: 'vendor', + minChunks: function (module) { + return ( + /node_modules/.test(module.context) && + !/\.css$/.test(module.request) + ) + } + }), + // 这将 webpack 运行时分离到一个引导 chunk 中, + // 以便可以在之后正确注入异步 chunk。 + // 这也为你的 应用程序/vendor 代码提供了更好的缓存。 + new webpack.optimize.CommonsChunkPlugin({ + name: 'manifest' + }), + new VueSSRClientPlugin() + ] +}) +``` + +从`localhost`中我们看到`ssr`预取的数据已经成功出来了,大功告成! +![localhost](https://pic1.zhimg.com/80/v2-8354c78be3249def1cfc6b40d795c3a4_hd.jpg) + +## 7.结语 +本文介绍了Vue的SSR的构建和运行流程,也分析了SSR的特点和利弊,希望对大家了解SSR有一定的帮助。最后针对不使用vuex的SSR实现方案进行了介绍,如果感兴趣或者有疑问,欢迎大家留言交流。 + +[阅读原文](https://zhuanlan.zhihu.com/p/35871344) \ No newline at end of file diff --git "a/Cute-Article/article/41-ES2018\357\274\210ES9\357\274\211\347\232\204\346\226\260\347\211\271\346\200\247.md" "b/Cute-Article/article/41-ES2018\357\274\210ES9\357\274\211\347\232\204\346\226\260\347\211\271\346\200\247.md" new file mode 100644 index 00000000..a10e3cdc --- /dev/null +++ "b/Cute-Article/article/41-ES2018\357\274\210ES9\357\274\211\347\232\204\346\226\260\347\211\271\346\200\247.md" @@ -0,0 +1,226 @@ +在这篇文章中,我将介绍ES2018(ES9)的新特性,并介绍如何使用它们。 + +JavaScript(ECMAScript)是跨多个平台的许多厂商实施的不断发展的标准。ES6(ECMAScript 2015)花费六年的时间敲定,是一个很大的发行版。新的年度发布流程被制定,以简化流程并更快地添加功能。 ES9(ES2018)是撰写本文时的最新版本。 + +TC39由包括浏览器厂商在内的各方组成,他们开会推动JavaScript提案沿着一条严格的发展道路前进: + +* Stage 0: strawman——最初想法的提交。 +* Stage 1: proposal(提案)——由TC39至少一名成员倡导的正式提案文件,该文件包括API事例。 +* Stage 2: draft(草案)——功能规范的初始版本,该版本包含功能规范的两个实验实现。 +* Stage 3: candidate(候选)——提案规范通过审查并从厂商那里收集反馈 +* Stage 4: finished(完成)——提案准备加入ECMAScript,但是到浏览器或者Nodejs中可能需要更长的时间 + +## ES2016 +ES2016添加了两个小的特性来说明标准化过程: + +1. 数组`includes()`方法,用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回`true`,否则返回`false`。 + +2. `a ** b`指数运算符,它与 `Math.pow(a, b)`相同。 + +## ES2017 +ES2017提供了更多的新特性: + +1. `Async` 函数呈现更清晰的 `Promise` 语法 + +2. `Object.values` 方法返回一个给定对象自己的所有可枚举属性值的数组,值的顺序与使用`for...in`循环的顺序相同(区别在于`for...in`循环枚举原型链中的属性) + +3. `Object.entries()`方法返回一个给定对象自身可枚举属性的键值对数组,其排列与使用`for...in`循环遍历改对象时返回的顺序一致(区别在于`for...in`循环也枚举原型链中的属性) + +4. `Object.getOwnPropertyDescriptors()`返回一个对象的所有自身属性的描述符(`.value`,`.writable`,`.get`,`.set`,`.configurable`,`enumerable`) + +5. `padStart()`和`padEnd()`,填充字符串达到当前长度 + +6. 结尾逗号,数组定义和函数参数列表 + +7. `ShareArrayBuffer`和`Atomics`用于从共享内存位置读取和写入 + +关于ES2017的更多信息请[参阅](https://www.sitepoint.com/es2017-whats-new/) + +## ES2018 +ECMAScript 2018(或者叫ES9)现在已经可用了。以下功能已经到达 stage 4,但是在撰写本文时在各个浏览器的实现还不完整。 + +### 异步迭代 +在`async/await`的某些时刻,你可能尝试在同步循环中调用异步函数。例如: +```js +async function process(array) { + for (let i of array) { + await doSomething(i); + } +} +``` +这段代码不会正常运行,下面这段同样也不会: +```js +async function process(array) { + array.forEach(async i => { + await doSomething(i); + }); +} +``` +这段代码中,循环本身依旧保持同步,并在在内部异步函数之前全部调用完成。 +ES2018引入异步迭代器(asynchronous iterators),这就像常规迭代器,除了`next()`方法返回一个`Promise`。因此`await`可以和`for...of`循环一起使用,以串行的方式运行异步操作。例如: +```js +async function process(array) { + for await (let i of array) { + doSomething(i); + } +} +``` + +### Promise.finally() +一个`Promise`调用链要么成功到达最后一个`.then()`,要么失败触发`.catch()`。在某些情况下,你想要在无论`Promise`运行成功还是失败,运行相同的代码,例如清除,删除对话,关闭数据库连接等。 +`.finally()`允许你指定最终的逻辑: +```js +function doSomething() { + doSomething1() + .then(doSomething2) + .then(doSomething3) + .catch(err => { + console.log(err); + }) + .finally(() => { + // finish here! + }); +} +``` + +## Rest/Spread 属性 +ES2015引入了[Rest参数](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Rest_parameters)和[扩展运算符](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Spread_syntax)。三个点(...)仅用于数组。Rest参数语法允许我们将一个布丁数量的参数表示为一个数组。 +```js +restParam(1, 2, 3, 4, 5); + +function restParam(p1, p2, ...p3) { + // p1 = 1 + // p2 = 2 + // p3 = [3, 4, 5] +} +``` + +展开操作符以相反的方式工作,将数组转换成可传递给函数的单独参数。例如`Math.max()`返回给定数字中的最大值: +```js +const values = [99, 100, -1, 48, 16]; +console.log( Math.max(...values) ); // 100 +``` + +ES2018为对象解构提供了和数组一样的Rest参数()和展开操作符,一个简单的例子: +```js +const myObject = { + a: 1, + b: 2, + c: 3 +}; + +const { a, ...x } = myObject; +// a = 1 +// x = { b: 2, c: 3 } +``` + +或者你可以使用它给函数传递参数: +```js +restParam({ + a: 1, + b: 2, + c: 3 +}); + +function restParam({ a, ...x }) { + // a = 1 + // x = { b: 2, c: 3 } +} +``` +跟数组一样,Rest参数只能在声明的结尾处使用。此外,它只适用于每个对象的顶层,如果对象中嵌套对象则无法适用。 +扩展运算符可以在其他对象内使用,例如: +```js +const obj1 = { a: 1, b: 2, c: 3 }; +const obj2 = { ...obj1, z: 26 }; +// obj2 is { a: 1, b: 2, c: 3, z: 26 } +``` + +可以使用扩展运算符拷贝一个对象,像是这样`obj2 = {...obj1}`,但是 这只是一个对象的浅拷贝。另外,如果一个对象A的属性是对象B,那么在克隆后的对象cloneB中,该属性指向对象B。 + +### 正则表达式命名捕获组(Regular Expression Named Capture Groups) +JavaScript正则表达式可以返回一个匹配的对象——一个包含匹配字符串的类数组,例如:以YYYY-MM-DD的格式解析日期: +```js +const + reDate = /([0-9]{4})-([0-9]{2})-([0-9]{2})/, + match = reDate.exec('2018-04-30'), + year = match[1], // 2018 + month = match[2], // 04 + day = match[3]; // 30 +``` + +这样的代码很难读懂,并且改变正则表达式的结构有可能改变匹配对象的索引。 +ES2018允许命名捕获组使用符号`?`,在打开捕获括号(后立即命名,示例如下: +```js +const + reDate = /(?[0-9]{4})-(?[0-9]{2})-(?[0-9]{2})/, + match = reDate.exec('2018-04-30'), + year = match.groups.year, // 2018 + month = match.groups.month, // 04 + day = match.groups.day; // 30 +``` +任何匹配失败的命名组都将返回`undefined`。 +命名捕获也可以使用在`replace()`方法中。例如将日期转换为美国的 MM-DD-YYYY 格式: +```js +const + reDate = /(?[0-9]{4})-(?[0-9]{2})-(?[0-9]{2})/, + d = '2018-04-30', + usDate = d.replace(reDate, '$-$-$'); +``` + +### 正则表达式反向断言(lookbehind) +目前JavaScript在正则表达式中支持先行断言(lookahead)。这意味着匹配会发生,但不会有任何捕获,并且断言没有包含在整个匹配字段中。例如从价格中捕获货币符号: +```js +const + reLookahead = /\D(?=\d+)/, + match = reLookahead.exec('$123.89'); + +console.log( match[0] ); // $ +``` + +ES2018引入以相同方式工作但是匹配前面的反向断言(lookbehind),这样我就可以忽略货币符号,单纯的捕获价格的数字: +```js +const + reLookbehind = /(?<=\D)\d+/, + match = reLookbehind.exec('$123.89'); + +console.log( match[0] ); // 123.89 +``` + +以上是 肯定反向断言,非数字`\D`必须存在。同样的,还存在 否定反向断言,表示一个值必须不存在,例如: +```js +const + reLookbehindNeg = /(? [阅读原文 obkoro1.com](http://obkoro1.com/2018/07/08/JS%E9%AB%98%E7%A8%8B%E4%B8%AD%E7%9A%84%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6%E4%B8%8E%E5%B8%B8%E8%A7%81%E5%86%85%E5%AD%98%E6%B3%84%E9%9C%B2%E7%9A%84%E8%A7%A3%E5%86%B3%E6%96%B9%E6%B3%95/) + +### 内存的生命周期: +1. 分配你所需要的内存: + +由于字符串、对象等没有固定的大小,js程序在每次创建字符串、对象的时候,程序都会**分配内存来存储那个实体**。 + +2. 使用分配到的内存做点什么。 + +3. 不需要时将其释放回归: + +在不需要字符串、对象的时候,需要释放其所占用的内存,否则将会消耗完系统中所有可用的内存,造成系统崩溃,这就是**垃圾回收机制所存在的意义**。 + +**所谓的内存泄漏**指的是:由于疏忽或错误造成程序未能释放那些已经不再使用的内存,造成内存的浪费。 +*** +### 垃圾回收机制: +在C和C++之类的语言中,需要手动来管理内存的,这也是造成许多不必要问题的根源。幸运的是,在编写js的过程中,内存的分配以及内存的回收完全实现了自动管理,我们不用操心这种事情。 + +### 垃圾收集机制的原理: +垃圾收集器会按照固定的时间间隔,**周期性的找出不再继续使用的变量,然后释放其占用的内存**。 + +#### 什么叫不再继续使用的变量? + +不再使用的变量也就是生命周期结束的变量,是局部变量,局部变量只在函数的执行过程中存在,当函数运行结束,没有其他引用(闭包),那么该变量会被标记回收。 + +全局变量的生命周期直至浏览器卸载页面才会结束,也就是说**全局变量不会被当成垃圾回收**。 + +### 标记清除:当前采用的垃圾收集策略 +工作原理: + +当变量进入环境时(例如在函数中声明一个变量),将这个变量标记为“进入环境”,当变量离开环境时,则将其标记为“离开环境”。标记“离开环境”的就回收内存。 + +工作流程: + +1. 垃圾收集器会在运行的时候会给存储在内存中的**所有变量都加上标记**。 +2. 去掉环境中的变量以及被环境中的变量引用的变量的标记。 +3. 那些还存在标记的变量被视为准备删除的变量。 +4. 最后垃圾收集器会执行最后一步内存清除的工作,销毁那些带标记的值并回收它们所占用的内存空间。 +到2008年为止,IE、Chorme、Fireofx、Safari、Opera **都使用标记清除式的垃圾收集策略**,只不过垃圾收集的时间间隔互有不同。 + +### 引用计数略:被废弃的垃圾收集策 +循环引用:跟踪记录每个值被引用的技术 + +在老版本的浏览器中(对,又是IE),IE9以下BOM和DOM对象就是使用C++以COM对象的形式实现的。 + +COM的垃圾收集机制采用的就是引用计数策略,这种机制在出现循环引用的时候永远都释放不掉内存。 +```js +var element = document.getElementById('something'); +var myObject = new Object(); +myObject.element = element; // element属性指向dom +element.someThing = myObject; // someThing回指myObject 出现循环引用(两个对象一直互相包含 一直存在计数)。 +``` +解决方式是,当我们不使用它们的时候,手动切断链接: +```js +myObject.element = null; +element.someThing = null; +``` +#### 淘汰: + +IE9把BOM和DOM对象转为了真正的js对象,避免了使用这种垃圾收集策略,消除了IE9以下常见的内存泄漏的主要原因。 + +IE7以下有一个声明狼藉的性能问题,大家了解一下: + +1. 256个变量,4096个对象(或数组)字面或者64KB的字符串,达到任何一个临界值会触发垃圾收集器运行。 +2. 如果一个js脚本的生命周期一直保有那么多变量,垃圾收集器会一直频繁的运行,引发严重的性能问题。 +IE7已修复这个问题。 +*** + +### 哪些情况会引起内存泄漏? +虽然有垃圾回收机制,但我们在编写代码的时候,有些情况还是会造成内存泄漏,了解这些情况,并在编写程序的时候,注意避免,我们的程序会更具健壮性。 + +#### 意外的全局变量: +上文我们提到了**全局变量不会被当成垃圾回收**,我们在编码中有时会出现下面这种情况: +```js +function foo() { + this.bar2 = '默认绑定this指向全局' // 全局变量=> window.bar2 + bar = '全局变量'; // 没有声明变量 实际上是全局变量=>window.bar +} +foo(); +``` +当我们使用[默认绑定](https://juejin.im/post/5b3715def265da59af40a630#heading-3),this会指向全局,`this.something`也会创建一个全局变量,这一点可能很多人没有注意到。 + +**解决方法:在函数内使用严格模式or细心一点** +```js +function foo() { + "use strict"; + this.bar2 = "严格模式下this指向undefined"; + bar = "报错"; +} +foo(); +``` +当然我们也可以手动释放全局变量的内存: +```js +window.bar = undefined +delete window.bar2 +``` +#### 被遗忘的定时器和回调函数 +当不需要`setInterval`或者`setTimeout`时,**定时器没有被clear**,定时器的**回调函数以及内部依赖的变量都不能被回收**,造成内存泄漏。 +```js +var someResource = getData(); +setInterval(function() { + var node = document.getElementById('Node'); + if(node) { + node.innerHTML = JSON.stringify(someResource)); + // 定时器也没有清除 + } + // node、someResource 存储了大量数据 无法回收 +}, 1000); +``` +**解决方法**: 在定时器完成工作的时候,手动清除定时器。 + +### 闭包: +**闭包可以维持函数内局部变量,使其得不到释放,造成内存泄漏。** +```js +function bindEvent() { + var obj = document.createElement("XXX"); + var unused = function () { + console.log(obj,'闭包内引用obj obj不会被释放'); + }; + // obj = null; +} +``` +**解决方法**:手动解除引用,`obj = null`。 + +#### 循环引用问题 +就是IE9以下的循环引用问题,上文讲过了。 + +#### 没有清理DOM元素引用: +```js +var refA = document.getElementById('refA'); +document.body.removeChild(refA); // dom删除了 +console.log(refA, "refA"); // 但是还存在引用 能console出整个div 没有被回收 +``` +不信的话,可以看下这个[dom](https://codepen.io/OBKoro1/pen/vroKbg)。 + +**解决办法**:`refA = null`; + +#### console保存大量数据在内存中。 +过多的console,比如定时器的console会导致浏览器卡死。 + +**解决**:合理利用console,线上项目尽量少的使用console,当然如果你要发招聘,除外。 +*** +### 如何避免内存泄漏: +**记住一个原则:不用的东西,及时归还,毕竟你是’借的’嘛。** + +1. 减少不必要的全局变量,使用严格模式避免意外创建全局变量。 +2. 在你使用完数据后,及时解除引用(闭包中的变量,dom引用,定时器清除)。 +3. 组织好你的逻辑,避免死循环等造成浏览器卡顿,崩溃的问题。 + +#### 关于内存泄漏: +1. 即使是1byte的内存,也叫内存泄漏,并不一定是导致浏览器崩溃、卡顿才能叫做内存泄漏。 +2. 一般是堆区内存泄漏,栈区不会泄漏。 +基本类型的值存在内存中,被保存在栈内存中,引用类型的值是**对象,保存在堆内存中。所以对象、数组之类的,才会发生内存泄漏**。 + +3. 使用chorme监控内存泄漏,可以看一下[这篇文章](https://jinlong.github.io/2016/05/01/4-Types-of-Memory-Leaks-in-JavaScript-and-How-to-Get-Rid-Of-Them/) diff --git "a/Cute-Article/article/43-\346\211\213\346\234\272\347\253\257\351\241\265\351\235\242\345\274\200\345\217\221\345\270\270\350\247\201\351\227\256\351\242\230\345\222\214\350\247\243\345\206\263.md" "b/Cute-Article/article/43-\346\211\213\346\234\272\347\253\257\351\241\265\351\235\242\345\274\200\345\217\221\345\270\270\350\247\201\351\227\256\351\242\230\345\222\214\350\247\243\345\206\263.md" new file mode 100644 index 00000000..ffb5253f --- /dev/null +++ "b/Cute-Article/article/43-\346\211\213\346\234\272\347\253\257\351\241\265\351\235\242\345\274\200\345\217\221\345\270\270\350\247\201\351\227\256\351\242\230\345\222\214\350\247\243\345\206\263.md" @@ -0,0 +1,356 @@ +## 1.解决页面使用 overflow: scroll 在 iOS 上滑动卡顿的问题? +首先你可能会给页面的 html 和 body 增加了 height: 100%, 然后就可能造成 iOS 上页面滑动的卡顿问题。解决方案是: +(1) 看是否能把 body 和 html 的 height: 100% 去除掉。 +(2) 在滚动的容器中增加:`-webkit-overflow-scrolling: touch` 或者给 body 增加:`body {overflow-x: hidden}`。 + +## 2.iOS 页面橡皮弹回效果遮挡页面选项卡? +(1) 有时 body 和 html 的 height: 100% 去除掉问题可能就没有了。 +(2) 到达临界值的时候在阻止事件默认行为 +```js +var startY,endY; +//记录手指触摸的起点坐标 +$('body').on('touchstart',function (e) { + startY = e.touches[0].pageY; +}); +$('body').on('touchmove',function (e) { + endY = e.touches[0].pageY; //记录手指触摸的移动中的坐标 + //手指下滑,页面到达顶端不能继续下滑 + if(endY>startY&& $(window).scrollTop()<=0){ + e.preventDefault(); + } + //手指上滑,页面到达底部能继续上滑 + if(endY=$('body')[0].scrollHeight){ + e.preventDefault(); + } +}) +``` +有时也会碰见弹窗出来后两个层的橡皮筋效果出现问题,我们可以在弹出弹出时给底层页面加上一个类名,类名禁止页面滑动这样下层的橡皮筋效果就会被禁止,就不会影响弹窗层。 + +## 3.iOS 机型 margin 属性无效问题? +(1) 设置 html body 的高度为百分比时,margin-bottom 在 safari 里失效 +(2) 直接 padding 代替 margin + +## 4.iOS 绑定点击事件不执行? +(1) 添加样式 `cursor :pointer`。点击后消除背景闪一下的 css:`-webkit-tap-highlight-color:transparent`; + +## 5.iOS 键盘换行变为搜索? +首先,input 要放在 form 里面。 +这时 "换行" 已经变成 “前往”。 +如果想变成 “搜索”,input 设置 `type="search"`。 + +## 6.jQuery对 a 标签点击事件不生效? +出现这种情况的原因不明,有的朋友解释:我们平时都是点击的 A 标签中的文字了。 所以要想用 JS 模拟点击 A 标签事件,就得先往 A 标签中的文字添加能被 JS 捕获的元素,然后再用 JS 模拟点击该元素即可。但是我觉得不合理,虽然找不到原因但是解决办法还是有的。 +```js +// 方法1 +document.getElementById("abc").click(); +// 方法2 +$("#abc")[0].click(); +``` +## 7.有时因为服务器或者别的原因导致页面上的图片没有找到? +这是我们想需要用一个本地的图片代替没有找的的图片 +```html + + +``` + +## 8.transform 属性影响 position:fixed? +(1) 规范中有规定:如果元素的 transform 值不为 none,则该元素会生成包含块和层叠上下文。CSS Transforms Module Level 1 不只在手机上,电脑上也一样。除了 fixed 元素会受影响之外,z-index(层叠上下文)值也会受影响。绝对定位元素等和包含块有关的属性都会受到影响。当然如果 transform 元素的 display 值为 inline 时又会有所不同。最简单的解决方法就是 transform 元素内部不能有 absolute、fixed 元素. + +## 9.iOS 对 position: fixed 不太友好,有时我们需要加点处理? +在安卓上面,点击页面底部的输入框,软键盘弹出,页面移动上移。 而 iOS 上面,点击页面底部输入框,软键盘弹出,输入框看不到了。。。查资料说什么的都有,iscroll,jquery-moblie,absolute,fixe,static 都非常复杂,要改很多。。。 让他弹出时让滚动条在最低部 +```js +var u = navigator.userAgent, app = navigator.appVersion; +var isiOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); //iOS终端 +if (isiOS) { + $('textarea').focus(function () { + window.setTimeout('scrollBottom()', 500); + }); +} +function scrollBottom() { + window.scrollTo(0, $('body').height()); +} +``` + +## 10.jQuery validate 插件验证问题? +所有的 input 必须有 name 不然会出错 + +## 11.有时手机会出现断网的情况,我没可能会对断网的情况做一些处理? +`navigator.onLine` 可判断是否是脱机状态. + +## 12.判断对象的长度? +(1) 用 `Object.keys`,`Object.keys` 方法返回的是一个数组,数组里面装的是对象的属性。 +```js +var person = { + "name" : "zhangshan", + "sex" : "man", + "age" : "50", + "height" : "180", + "phone" : "1xxxxxxxxxx", + "email" : "xxxxxxxxx@xxx.com" +}; +var arr = Object.keys(person); +console.log(arr.length); +``` +(2)Object.getOwnPropertyNames(obj).length + +## 13.上一题我们用到了 Object.keys 与 Object.getOwnPropertyNames 他们的区别? +`Object.keys` 定义:返回一个对象可枚举属性的字符串数组; +`Object.getOwnPropertyNames` 定义:返回一个对象可枚举、不可枚举属性的名称; +属性的可枚举性、不可枚举性:定义:可枚举属性是指那些内部 “可枚举” 标志设置为 true 的属性,对于通过直接的赋值和属性初始化的属性,该标识值默认为即为 true,对于通过 `Object.defineProperty` 等定义的属性,该标识值默认为 false。 +```js +var obj = { "prop1": "v1" }; +Object.defineProperty(obj, "prop2", { value: "v2", enumerable: false }); +console.log(Object.keys(obj).length); //output:1 +console.log(Object.getOwnPropertyNames(obj).length); //output:2 +console.log(Object.keys(obj)); //output:Array[1] => [0: "prop1"] +console.log(Object.getOwnPropertyNames(obj)); //output:Array[2] => [0: "prop1", 1: "prop2"] +``` +![内置的判断,访问和迭代方法](http://p3nqtyvgo.bkt.clouddn.com/196513361-5b16021f6db85_articlex.png) +综合实例: +```js +var obj = { "prop1": "v1" }; +Object.defineProperty(obj, "prop2", { value: "v2", enumerable: false}); +console.log(obj.hasOwnProperty("prop1")); //output: true +console.log(obj.hasOwnProperty("prop2")); //output: true +console.log(obj.propertyIsEnumerable("prop1")); //output: true +console.log(obj.propertyIsEnumerable("prop2")); //output: false +console.log('prop1' in obj); //output: true +console.log('prop2' in obj); //output: true +for (var item in obj) { + console.log(item); +} +//output:prop1 +for (var item in Object.getOwnPropertyNames(obj)) { + console.log(Object.getOwnPropertyNames(obj)[item]); +} +//ouput:[prop1,prop2] +``` + +## 14.移动开发不同手机弹出数字键盘问题? +#### 1. type="tel" +iOS 和 Android 的键盘表现都差不多 + +#### 2. type="number" +**优点**:Android 下实现的一个真正的数字键盘 +**缺点一**:iOS 下不是九宫格键盘,输入不方便 +**缺点二**:旧版 Android(包括微信所用的 X5 内核)在输入框后面会有超级鸡肋的小尾巴,好在 Android 4.4.4 以后给去掉了。 不过对于缺点二,我们可以用 webkit 私有的伪元素给 fix 掉: +```js +input[type=number]::-webkit-inner-spin-button, +input[type=number]::-webkit-outer-spin-button { + -webkit-appearance: none; + appearance: none; + margin: 0; +} +``` +#### 3. pattern 属性 +pattern 用于验证表单输入的内容,通常 HTML5 的 type 属性,比如 email、tel、number、data 类、url 等,已经自带了简单的数据格式验证功能了,加上 pattern 后,前端部分的验证更加简单高效了。 +显而易见,pattern 的属性值要用正则表达式。 +实例 简单的数字验证 +数字的验证有两个: +```html + + +``` + +## 15.input[number] 类型输入非数字字符 +js 获取的值是空;比如 - 12,+123 等 + +## 16.Javascript:history.go() 和 history.back() 的用法与区别? +`go(-1)`: 返回上一页,原页面表单中的内容会丢失; +`back()`: 返回上一页,原页表表单中的内容会保留; +`history.go(-1)`: 后退 + 刷新; +`history.back()`: 后退; + +之所以注意到这个区别,是因为不同的浏览器后退行为也是有区别的,而区别就跟 `javascript:history.go()` 和 `history.back()` 的区别类似。 +Chrome 和 ff 浏览器后退页面,会刷新后退的页面,若有数据请求也会提交数据申请。类似于 `history.go(-1)`; +而 safari(包括桌面版和 ipad 版)的后退按钮则不会刷新页面,也不会提交数据申请。类似于 `javascript:history.back()`; + +## 17.Meta 基础知识: +```html + +// width 设置viewport宽度,为一个正整数,或字符串‘device-width’ +// height 设置viewport高度,一般设置了宽度,会自动解析出高度,可以不用设置 +// initial-scale 默认缩放比例,为一个数字,可以带小数 +// minimum-scale 允许用户最小缩放比例,为一个数字,可以带小数 +// maximum-scale 允许用户最大缩放比例,为一个数字,可以带小数 +// user-scalable 是否允许手动缩放 +空白页基本meta标签 + + + + + + + + +其他meta标签 + + + + + + + + + + + + + + + + + + + + + + +``` +## 18.移动端如何定义字体 font-family? +@ ------------ 中文字体的英文名称 +@ 宋体 SimSun +@ 黑体 SimHei +@ 微信雅黑 Microsoft Yahei +@ 微软正黑体 Microsoft JhengHei +@ 新宋体 NSimSun +@ 新细明体 MingLiU +@ 细明体 MingLiU +@ 标楷体 DFKai-SB +@ 仿宋 FangSong +@ 楷体 KaiTi +@ 仿宋GB2312 FangSongGB2312 +@ 楷体GB2312 KaiTiGB2312 +**说明**:中文字体多数使用宋体、雅黑,英文用 Helvetica +```css +body {font-family: Microsoft Yahei,SimSun,Helvetica;} +``` + +## 19.打电话发短信写邮件怎么实现? +```html +// 一、打电话 +打电话给:0755-10086 +// 二、发短信,winphone系统无效 +发短信给: 10086 +// 三、写邮件 +点击我发邮件 +//2.收件地址后添加?cc=开头,可添加抄送地址(Android存在兼容问题) +点击我发邮件 +//3.跟着抄送地址后,写上&bcc=,可添加密件抄送地址(Android存在兼容问题) +点击我发邮件 +//4.包含多个收件人、抄送、密件抄送人,用分号(;)隔开多个邮件人的地址 +点击我发邮件 +//5.包含主题,用?subject= +点击我发邮件 +//6.包含内容,用?body=;如内容包含文本,使用%0A给文本换行 +点击我发邮件 +//7.内容包含链接,含http(s)://等的文本自动转化为链接 +点击我发邮件 +//8.内容包含图片(PC不支持) +点击我发邮件 +//9.完整示例 +点击我发邮件 +``` + +## 20.移动端 touch 事件(区分 webkit 和 winphone)? +#### 1. 以下支持 webkit +**touchstart**——当手指触碰屏幕时候发生。不管当前有多少只手指 +**touchmove**——当手指在屏幕上滑动时连续触发。通常我们再滑屏页面,会调用 event 的 **preventDefault() 可以阻止默认情况的发生:阻止页面滚动 +**touchend**——当手指离开屏幕时触发 +**touchcancel**——系统停止跟踪触摸时候会触发。例如在触摸过程中突然页面 alert() 一个提示框,此时会触发该事件,这个事件比较少用 + +#### 2. TouchEvent 说明: +**touches**:屏幕上所有手指的信息 +**targetTouches**:手指在目标区域的手指信息 +**changedTouches**:最近一次触发该事件的手指信息 +touchend 时,touches 与 targetTouches 信息会被删除,changedTouches 保存的最后一次的信息,最好用于计算手指信息 +#### 3.参数信息 (changedTouches[0]) +**clientX**、**clientY** 在显示区的坐标 +**target**:当前元素 +#### 4.事件响应顺序 +ontouchstart > ontouchmove > ontouchend > onclick + +## 21.点击元素产生背景或边框怎么去掉 +* **iOS用户** 点击一个链接,会出现一个半透明灰色遮罩, 如果想要禁用,可设置`-webkit-tap-highlight-color`的alpha值为`0`去除灰色半透明遮罩; +* **android用户** 点击一个链接,会出现一个边框或者半透明灰色遮罩, 不同生产商定义出来额效果不一样,可设置`-webkit-tap-highlight-color`的alpha值为`0`去除部分机器自带的效果; +* **winphone系统** 点击标签产生的灰色半透明背景,能通过设置``去掉; +* 特殊说明:有些机型去除不了,如小米2。对于按钮类还有个办法,不使用a或者input标签,直接用div标签 +```css +a,button,input,textarea { + -webkit-tap-highlight-color: rgba(0,0,0,0); + -webkit-user-modify:read-write-plaintext-only; + //-webkit-user-modify 副作用 输入法不再能够输入多个字符 +} +``` +也可以 +```css +* { -webkit-tap-highlight-color: rgba(0,0,0,0); } +``` +winphone下 +```html + +``` +## 22.美化表单元素 +#### 1. 使用 appearance 改变 webkit 浏览器的默认外观 +```css +input,select {-webkit-appearance:none; appearance: none;} +``` +#### 2.winphone 下,使用伪元素改变表单元素默认外观 +* 1) 禁用 select 默认箭头,`::-ms-expand` 修改表单控件下拉箭头,设置隐藏并使用背景图片来修饰 +```css +select::-ms-expand {display:none;} +``` +* 2) 禁用 radio 和 checkbox 默认样式,`::-ms-check` 修改表单复选框或单选框默认图标,设置隐藏并使用背景图片来修饰 +```css +input[type=radio]::-ms-check, +input[type=checkbox]::-ms-check { display:none; } +``` +* 3) 禁用 pc 端表单输入框默认清除按钮,`::-ms-clear` 修改清除按钮,设置隐藏并使用背景图片来修饰 +```css +input[type=text]::-ms-clear, +input[type=tel]::-ms-clear, +input[type=number]::-ms-clear { display:none; } +``` + +## 23.移动端字体单位 font-size 选择 px 还是 rem? +如需适配多种移动设备,建议使用 rem。以下为参考值: +```css +html {font-size: 62.5%;} //10*16 = 62.5% +``` +设置 12px 字体 这里注意在 rem 前要加上对应的 px 值,解决不支持 rem 的浏览器的兼容问题,做到优雅降级 +```css +body {font-size:12px; font-size:1.2rem;} +``` + +## 24.input 标签添加上 disable 属性在 iOS 端字体颜色不兼容的问题? +```css +input[disabled],input:disabled,input.disabled{ + color: #3e3e3e; + -webkit-text-fill-color: #3e3e3e; + -webkit-opacity:1; + opacity: 1; +} +``` + +## 25.iOS 的光标大小问题 +#### IE: +不管该行有没有文字,光标高度与 font-size 一致。 +#### FF: +该行有文字时,光标高度与 font-size 一致。该行无文字时,光标高度与 input 的 height 一致。 +#### Chrome: +该行无文字时,光标高度与 line-height 一致;该行有文字时,光标高度从 input 顶部到文字底部 (这两种情况都是在有设定 line-height 的时候),如果没有 line-height,则是与 font-size 一致。 + +iOS 中情况和 Chrome 相似。 +设置字体大小和行高一致,然后通过 padding 撑开大小,只给 IE 浏览器设置 +```css +line-height:-ms-line-height:40px; +``` + +原文:https://segmentfault.com/a/1190000015178877 作者:键盘上的眼泪 \ No newline at end of file diff --git "a/Cute-Article/article/44-\345\211\215\347\253\257\346\234\254\345\234\260\346\226\207\344\273\266\346\223\215\344\275\234\345\222\214\344\270\212\344\274\240.md" "b/Cute-Article/article/44-\345\211\215\347\253\257\346\234\254\345\234\260\346\226\207\344\273\266\346\223\215\344\275\234\345\222\214\344\270\212\344\274\240.md" new file mode 100644 index 00000000..195d40fc --- /dev/null +++ "b/Cute-Article/article/44-\345\211\215\347\253\257\346\234\254\345\234\260\346\226\207\344\273\266\346\223\215\344\275\234\345\222\214\344\270\212\344\274\240.md" @@ -0,0 +1,352 @@ +[原文地址](https://juejin.im/post/5a193b4bf265da43052e528a) + +前端无法像原生APP一样直接操作本地文件,否则的话打开个网页就能把用户电脑上的文件偷光了,所以需要通过用户触发,用户可通过以下三种方式操作触发: + +1. 通过input type="file" 选择本地文件 +2. 通过拖拽的方式把文件拖过来 +3. 在编辑框里面复制粘贴 + +### 第一种 +第一种是最常用的手段,通常还会自定义一个按钮,然后盖在它上面,因为`type="file"`的input不好改变样式。如下代码写一个选择控件,并放在form里面: +```html +
+ +
+``` +然后就可以用FormData获取整个表单的内容: +```js +$("#file-input").on("change", function() { + console.log(`file name is ${this.value}`); + let formData = new FormData(this.form); + formData.append("fileName", this.value); + console.log(formData); +}); +``` +把input的value和formData打印出来是这样的: +![预览](https://user-gold-cdn.xitu.io/2017/11/25/15ff349ac0afefa0?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) + +可以看到文件的路径是一个假的路径,也就是说在浏览器无法获取到文件的真实存放位置。同时FormData打印出来是一个空的Objet,但并不是说它的内容是空的,只是它对前端开发人员是透明的,无法查看、修改、删除里面的内容,只能`append`添加字段。 + +`FormData`无法得到文件的内容,而使用`FileReader`可以读取整个文件的内容。用户选择文件之后,`input.files`就可以得到用户选中的文件,如下代码: +```js +$("#file-input").on("change", function() { + let fileReader = new FileReader(), + fileType = this.files[0].type; + fileReader.onload = function() { + if (/^image/.test(fileType)) { + // 读取结果在fileReader.result里面 + $(``).appendTo("body"); + } + } + // 打印原始File对象 + console.log(this.files[0]); + // base64方式读取 + fileReader.readAsDataURL(this.files[0]); +}); +``` +把原始的File对象打印出来是这样的: +![预览](https://user-gold-cdn.xitu.io/2017/11/25/15ff349ac079ba68?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) + + +它是一个window.File的实例,包含了文件的修改时间、文件名、文件的大小、文件的mime类型等。 +如果需要`限制上传文件的大小`就可以通过判断`size`属性有没有超,单位是字节,而要判断是否为图片文件就可以通过type类型是否以image开头。通过判断文件名的后缀可能会不准,而通过这种判断会比较准。上面的代码使用了一个正则判断,如果是一张图片的话就把它赋值给img的src,并添加到dom里面,但其实这段代码有点问题,就是web不是所有的图片都能通过img标签展示出来,通常是jpg/png/gif这三种,所以你应该需要再判断一下图片格式,如可以把判断改成: +```js +/^image\/[jpeg|png|gif]/.test(this.type) +``` +然后实例化一个`FileReader`,调它的`readAsDataURL`并把`File`对象传给它,监听它的`onload`事件,load完读取的结果就在它的`result`属性里了。它是一个`base64`格式的,可直接赋值给一个img的src。 + +使用`FileReader`除了可读取为`base64`之外,还能读取为以下格式: +```js +// 按base64的方式读取,结果是base64,任何文件都可转成base64的形式 +fileReader.readAsDataURL(this.files[0]); + +// 以二进制字符串方式读取,结果是二进制内容的utf-8形式,已被废弃了 +fileReader.readAsBinaryString(this.files[0]); + +// 以原始二进制方式读取,读取结果可直接转成整数数组 +fileReader.readAsArrayBuffer(this.files[0]); +``` +其它的主要是能读取为`ArrayBuffer`,它是一个原始二进制格式的结果。把`ArrayBuffer`打印出来是这样的: +![预览](https://user-gold-cdn.xitu.io/2017/11/25/15ff349ac05a9b42?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) +可以看到,它对前端开发人员也是透明的,不能够直接读取里面的内容,但可以通过`ArrayBuffer.length`得到长度,还能转成整型数组,就能知道文件的原始二进制内容了: +```js +let buffer = this.result; +// 依次每字节8位读取,放到一个整数数组 +let view = new Uint8Array(buffer); +console.log(view); +``` + +### 第二种 +如果是通过第二种拖拽的方式,应该怎么读取文件呢?如下html(样式略): +```html +
+ drop your image here +
+``` +这将在页面显示一个框: +![预览](https://user-gold-cdn.xitu.io/2017/11/25/15ff349ac1f2ebd5?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) + +然后监听它的拖拽事件: +```js +$(".img-container").on("dragover", function (event) { + event.preventDefault(); +}) + +.on("drop", function(event) { + event.preventDefault(); + // 数据在event的dataTransfer对象里 + let file = event.originalEvent.dataTransfer.files[0]; + + // 然后就可以使用FileReader进行操作 + fileReader.readAsDataURL(file); + + // 或者是添加到一个FormData + let formData = new FormData(); + formData.append("fileContent", file); +}) +``` +数据在`drop`事件的`event.dataTransfer.files`里面,拿到这个`File`对象之后就可以和输入框进行一样的操作了,即使用`FileReader`读取,或者是新建一个空的`formData`,然后把它`append`到`formData`里面。 + + +### 第三种 +第三种粘贴的方式,通常是在一个编辑框里操作,如把`div`的`contenteditable`设置为true: +```html +
+ hello, paste your image here +
+``` +粘贴的数据是在`event.clipboardData.files`里面: +```js +$("#editor").on("paste", function(event) { + let file = event.originalEvent.clipboardData.files[0]; +}); +``` +但是Safari的粘贴不是通过`event`传递的,它是直接在输入框里面添加一张图片,如下图所示: +![预览](https://user-gold-cdn.xitu.io/2017/11/25/15ff349ac3330972?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) + +它新建了一个`img`标签,并把`img`的`src`指向一个`blob`的本地数据。什么是`blob`呢,如何读取`blob`的内容呢? +blob是一种类文件的存储格式,它可以存储几乎任何格式的内容,如json: +```js +let data = {hello: "world"}; +let blob = new Blob([JSON.stringify(data)], + {type : 'application/json'}); +``` +为了获取本地的blob数据,我们可以用ajax发个本地的请求: +```js +$("#editor").on("paste", function(event) { + // 需要setTimeout 0等图片出来了再处理 + setTimeout(() => { + let img = $(this).find("img[src^='blob']")[0]; + console.log(img.src); + // 用一个xhr获取blob数据 + let xhr = new XMLHttpRequest(); + xhr.open("GET", img.src); + // 改变mime类型 + xhr.responseType = "blob"; + xhr.onload = function () { + // response就是一个Blob对象 + console.log(this.response); + }; + xhr.send(); + }, 0); +}); +``` +上面代码把blob打印出来是这样的: +![预览](https://user-gold-cdn.xitu.io/2017/11/25/15ff349ac408b172?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) + +能得到它的大小和类型,但是具体内容也是不可见的,它有一个`slice`的方法,可用于切割大文件。和`File`一样,可以使用`FileReader`读取它的内容: +```js +function readBlob(blobImg) { + let fileReader = new FileReader(); + fileReader.onload = function() { + console.log(this.result); + } + fileReader.onerror = function(err) { + console.log(err); + } + fileReader.readAsDataURL(blobImg); +} +readBlob(this.response); +``` +除此,还能使用`window.URL`读取,这是一个新的API,经常和`Service Worker`配套使用,因为SW里面常常要解析url。如下代码: +```js +function readBlob(blobImg) { + let urlCreator = window.URL || window.webkitURL; + // 得到base64结果 + let imageUrl = urlCreator.createObjectURL(this.response); + return imageUrl; +} + +readBlob(this.response); +``` +关于src使用的是blob链接的,除了上面提到的img之外,另外一个很常见的是video标签,如youtobe的视频就是使用的blob: +![](https://user-gold-cdn.xitu.io/2017/11/25/15ff349af40951d9?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) + +这种数据不是直接在本地的,而是通过持续请求视频数据,然后再通过`blob`这个容器媒介添加到`video`里面,它也是通过URL的API创建的: +```js +let mediaSource = new MediaSource(); +video.src = URL.createObjectURL(mediaSource); +let sourceBuffer = mediaSource.addSourceBuffer('video/mp4; codecs="avc1.42E01E, mp4a.40.2"'); +sourceBuffer.appendBuffer(buf); +``` +具体我也没实践过,不再展开讨论。 + +上面,我们使用了三种方式获取文件内容,最后得到: + +1. `FormData`格式 +2. `FileReader`读取得到的`base64`或者`ArrayBuffer`二进制格式 + +如果直接就是一个`FormData`了,那么直接用`ajax`发出去就行了,不用做任何处理: +```js +let form = document.querySelector("form"), + formData = new FormData(form), +formData.append("fileName", "photo.png"); + +let xhr = new XMLHttpRequest(); +// 假设上传文件的接口叫upload +xhr.open("POST", "/upload"); +xhr.send(formData); +``` +如果用jQuery的话,要设置两个属性为false: +```js +$.ajax({ + url: "/upload", + type: "POST", + data: formData, + processData: false, // 不处理数据 + contentType: false // 不设置内容类型 +}); +``` +因为jQuery会自动把内容做一些转义,并且根据`data`自动设置请求`mime`类型,这里告诉jQuery直接用`xhr.send`发出去就行了。 + +观察控制台发请求的数据: +![预览](https://user-gold-cdn.xitu.io/2017/11/25/15ff349b104c3494?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) + +可以看到这是一种区别于用`&`连接参数的方式,它的编码格式是`multipart/form-data`,就是上传文件`form`表单写的`enctype`: +```html +
+ +
+``` +如果`xhr.send`的是`FormData`类型话,它会自动设置`enctype`,如果你用默认表单提交上传文件的话就得在`form`上面设置这个属性,因为上传文件只能使用`POST`的这种编码。常用的`POST`编码是`application/x-www-form-urlencoded`,它和`GET`一样,发送的数据里面,参数和参数之间使用`&`连接,如: +```js +key1=value1&key2=value2 +``` +特殊字符做转义,这个数据`POST`是放在请求`body`里的,而`GET`是拼在`url`上面的,如果用jq的话,jq会帮你拼并做转义。 + +而上传文件用的这种`multipart/form-data`,参数和参数之间是且一个相同的字符串隔开的,上面的是使用: +``` +------WebKitFormBoundary72yvM25iSPYZ4a3F +``` +这个字符通常会取得比较长、比较随机,因为要保证正常的内容里面不会出现这个字符串,这样内容的特殊字符就不用做转义了。 + +请求的contentType被浏览器设置成: +``` +Content-Type: +multipart/form-data; boundary=----WebKitFormBoundary72yvM25iSPYZ4a3F +``` +后端服务通过这个就知道怎么解析这么一段数据了。(通常是使用的框架处理了,而具体的接口不需要关心应该怎么解析) + +如果读取结果是`ArrayBuffer`的话,也是可以直接用`xhr.send`发送出去的,但是一般我们不会直接把一个文件的内容发出去,而是用某个字段名等于文件内容的方式。如果你读取为`ArrayBuffer`的话再上传的话其实作用不是很大,还不如直接用`formData`添加一个`File`对象的内容,因为上面三种方式都可以拿到`File`对象。如果一开始就是一个`ArrayBuffer`了,那么可以转成`blob`然后再`append`到`FormData`里面。 + +使用比较多的应该是`base64`,因为前端经常要处理图片,读取为`base64`之后就可以把它画到一个`canvas`里面,然后就可以做一些处理,如压缩、裁剪、旋转等。最后再用`canvas`导出一个`base64`格式的图片,那怎么上传`base64`格式的呢? + +### 怎么上传`base64`格式 +第一种是拼一个表单上传的`multipart/form-data`的格式,再用`xhr.sendAsBinary`发出去,如下代码: +```js +let base64Data = base64Data.replace(/^data:image\/[^;]+;base64,/, ""); +let boundary = "----------boundaryasoifvlkasldvavoadv"; +xhr.sendAsBinary([ + // name=data + boundary, + 'Content-Disposition: form-data; name="data"; filename="' + fileName + '"', + 'Content-Type: ' + "image/" + fileType, '', + atob(base64Data), boundary, + //name=imageType + boundary, + 'Content-Disposition: form-data; name="imageType"', '', + fileType, + boundary + '--' +].join('\r\n')); +``` +上面代码使用了`window.atob`的api,它可以把`base64`还原成原始内容的字符串表示,如下图所示: +![预览](https://user-gold-cdn.xitu.io/2017/11/25/15ff349b4c18f7d8?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) + +`btoa`是把内容转化成`base64`编码,而`atob`是把`base64`还原。在调`atob`之前,需要把表示内容格式的不属于`base64`内容的字符串去掉,即上面代码第一行的`replace`处理。 + +这样就和使用`formData`类似了,但是由于`sendAsBinary`已经被`deprecated`了,所以新代码不建议再使用这种方式。那怎么办呢? + +可以把`base64`转化成`blob`,然后再`append`到一个`formData`里面,下面的函数(来自b64-to-blob)可以把`base64`转成`blob`: +```js +function b64toBlob(b64Data, contentType, sliceSize) { + contentType = contentType || ''; + sliceSize = sliceSize || 512; + + var byteCharacters = atob(b64Data); + var byteArrays = []; + + for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) { + var slice = byteCharacters.slice(offset, offset + sliceSize); + + var byteNumbers = new Array(slice.length); + for (var i = 0; i < slice.length; i++) { + byteNumbers[i] = slice.charCodeAt(i); + } + + var byteArray = new Uint8Array(byteNumbers); + + byteArrays.push(byteArray); + } + + var blob = new Blob(byteArrays, {type: contentType}); + return blob; +} +``` +然后就可以`append`到`formData`里面: +```js +let blob = b64toBlob(b64Data, "image/png"), + formData = new FormData(); +formData.append("fileContent", blob); +``` +这样就不用自己去拼一个`multipart/form-data`的格式数据了。 + +上面处理和上传文件的API可以兼容到IE10+,如果要兼容老的浏览器应该怎么办呢? + +可以借助一个`iframe`,原理是默认的`form`表单提交会刷新页面,或者跳到`target`指定的那个url,但是如果把`ifrmae`的`target`指向一个`iframe`,那么刷新的就是`iframe`,返回结果也会显示在`ifame`,然后获取这个`ifrmae`的内容就可得到上传接口返回的结果。 + +如下代码: +```js +let iframe = document.createElement("iframe"); +iframe.display = "none"; +iframe.name = "form-iframe"; +document.body.appendChild(iframe); +// 改变form的target +form.target = "form-iframe"; + +iframe.onload = function() { + //获取iframe的内容,即服务返回的数据 + let responseText = this.contentDocument.body.textContent + || this.contentWindow.document.body.textContent; +}; + +form.submit(); +``` +`form.submit`会触发表单提交,当请求完成(成功或者失败)之后就会触发iframe的onload事件,然后在onload事件获取返回的数据,如果请求失败了的话,iframe里的内容就为空,可以用这个判断请求有没有成功。 + + + +使用iframe没有办法获取上传进度,使用xhr可以获取当前上传的进度,这个是在XMLHttpRequest 2.0引入的: +```js +xhr.upload.onprogress = function (event) { + if (event.lengthComputable) { + // 当前上传进度的百分比 + duringCallback ((event.loaded / event.total)*100); + } +}; +``` +这样就可以做一个真实的loading进度条。 + + +本文讨论了3种交互方式的读取方式,通过`input`控件在`input.files`可以得到File文件对象,通过拖拽的是在`drop`事件的`event.dataTransfer.files`里面,而通过粘贴的`paste`事件在`event.clipboardData.files`里面,Safari这个怪胎是在编辑器里面插入一个src指向本地的img标签,可以通过发送一个请求加载本地的`blob`数据,然后再通过`FileReader`读取,或者直接`append`到`formData`里面。得到的File对象就可以直接添加到`FormData`里面,如果需要先读取`base64`格式做处理的,那么可以把处理后的`base64`转化为`blob`数据再`append`到`formData`里面。对于老浏览器,可以使用一个iframe解决表单提交刷新页面或者跳页的问题。 + +总之,前端处理和上传本地文件应该差不多就是这些内容了,但是应该还有好多细节没有提及到,读者可通过本文列的方向自行实践。如果有其它的上传方式还请告知。 \ No newline at end of file diff --git "a/Cute-Article/article/45-js\344\270\255reduce\347\232\204\347\245\236\345\245\207\347\224\250\346\263\225.md" "b/Cute-Article/article/45-js\344\270\255reduce\347\232\204\347\245\236\345\245\207\347\224\250\346\263\225.md" new file mode 100644 index 00000000..01b61708 --- /dev/null +++ "b/Cute-Article/article/45-js\344\270\255reduce\347\232\204\347\245\236\345\245\207\347\224\250\346\263\225.md" @@ -0,0 +1,135 @@ +最近经常在项目中经常看到别人用reduce处理数据,很是牛掰,很梦幻, 不如自己琢磨琢磨。先看w3c语法。 +## w3c语法 +```js +array.reduce(function(total, currentValue, currentIndex, arr), initialValue); +/* +total: 必需。初始值, 或者计算结束后的返回值。 +currentValue: 必需。当前元素。 +currentIndex: 可选。当前元素的索引; +arr: 可选。当前元素所属的数组对象。 +initialValue: 可选。传递给函数的初始值,相当于total的初始值。 +*/ +``` + +## 常见用法 + +### 1.数组求和 +```js +const arr = [12, 34, 23]; +const sum = arr.reduce((total, num) => total + num); +// 设定初始值求和 +const arr = [12, 34, 23]; +const sum = arr.reduce((total, num) => total + num, 10); // 以10为初始值求和 +// 对象数组求和 +var result = [ +{ subject: 'math', score: 88 }, +{ subject: 'chinese', score: 95 }, +{ subject: 'english', score: 80 } +]; +const sum = result.reduce((prev, cur) => prev + cur.score, 0); +const sum = result.reduce((prev, cur) => prev + cur.score, -10); // 总分扣除10分 +``` + +### 2.数组最大值 +```js +const a = [23,123,342,12]; +const max = a.reduce(function(pre,cur,inde,arr){return pre>cur?pre:cur;}); // 342 +``` + +## 进阶用法 + +### 1.数组对象中的用法 +```js +// 比如生成“老大、老二和老三” +const objArr = [{name: '老大'}, {name: '老二'}, {name: '老三'}]; +const res = objArr.reduce((pre, cur, index, arr) => { +if (index === 0) { +return cur.name; +} +else if (index === (arr.length - 1)) { +return pre + '和' + cur.name; +} +else { +return pre + '、' + cur.name; +} +}, ''); +``` +### 2.求字符串中字母出现的次数 +```js +const str = 'sfhjasfjgfasjuwqrqadqeiqsajsdaiwqdaklldflas-cmxzmnha'; +const res = str.split('').reduce((prev, cur) => {prev[cur] ? prev[cur]++ : prev[cur] = 1; return prev;}, {}); +``` + +### 3.数组转数组 +```js +// 按照一定的规则转成数组 +var arr1 = [2, 3, 4, 5, 6]; // 每个值的平方 +var newarr = arr1.reduce((prev, cur) => {prev.push(cur * cur); return prev;}, []); +``` + +### 4.数组转对象 +```js +// 按照id 取出stream +var streams = [{name: '技术', id: 1}, {name: '设计', id: 2}]; +var obj = streams.reduce((prev, cur) => {prev[cur.id] = cur; return prev;}, {}); +``` + +## 高级用法 + +### 1.多维的叠加执行操作 +```js +// 各科成绩占比重不一样, 求结果 +var result = [ +{ subject: 'math', score: 88 }, +{ subject: 'chinese', score: 95 }, +{ subject: 'english', score: 80 } +]; +var dis = { +math: 0.5, +chinese: 0.3, +english: 0.2 +}; +var res = result.reduce((prev, cur) => dis[cur.subject] * cur.score + prev, 0); + +// 加大难度, 商品对应不同国家汇率不同,求总价格 +var prices = [{price: 23}, {price: 45}, {price: 56}]; +var rates = { +us: '6.5', +eu: '7.5', +}; +var initialState = {usTotal:0, euTotal: 0}; +var res = prices.reduce((prev1, cur1) => Object.keys(rates).reduce((prev2, cur2) => { +console.log(prev1, cur1, prev2, cur2); +prev1[`${cur2}Total`] += cur1.price * rates[cur2]; +return prev1; +}, {}), initialState); + +var manageReducers = function() { +return function(state, item) { +return Object.keys(rates).reduce((nextState, key) => { +state[`${key}Total`] += item.price * rates[key]; +return state; +}, {}); +} +}; +var res1= prices.reduce(manageReducers(), initialState); +``` + +### 2.扁平一个多维数组 +```js +var arr = [[1, 2, 8], [3, 4, 9], [5, 6, 10]]; +var res = arr.reduce((x, y) => x.concat(y), []); +``` + +### 3.对象数组去重 +```js +const hash = {}; +chatlists = chatlists.reduce((obj, next: Object) => { +const hashId = `${next.topic}_${next.stream_id}`; +if (!hash[hashId]) { +hash[`${next.topic}_${next.stream_id}`] = true; +obj.push(next); +} +return obj; +}, []); +``` diff --git "a/Cute-Article/article/46-\345\234\250JavaScript\344\270\255\346\233\264\345\245\275\347\232\204\344\275\277\347\224\250\346\225\260\347\273\204.md" "b/Cute-Article/article/46-\345\234\250JavaScript\344\270\255\346\233\264\345\245\275\347\232\204\344\275\277\347\224\250\346\225\260\347\273\204.md" new file mode 100644 index 00000000..f6ed44b0 --- /dev/null +++ "b/Cute-Article/article/46-\345\234\250JavaScript\344\270\255\346\233\264\345\245\275\347\232\204\344\275\277\347\224\250\346\225\260\347\273\204.md" @@ -0,0 +1,142 @@ +[阅读原文](https://juejin.im/post/5b8d0a74f265da431d0e7ec0) +[MDN Array 介绍](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array) + +本文短小精悍,我保证。在过去的数个月里,我注意到在我审阅的 pull request 中有四个(关于数组使用的)错误经常出现。同时,我自己也会犯这些错误,因此有了这篇文章。让我们一起学习,以确保以后能正确地使用数组方法! + +## 1.使用 `Array.includes` 替代 `Array.indexOf` + +> "如果需要在数组中查找某个元素,请使用 `Array.indexOf`。" + +我记得在我学习 JavaScript 的课程中有类似的这么一句话。毫无疑问,这完全正确! + +在 MDN 文档中,对 `Array.indexOf` 的描述是:返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回`-1`。因此,如果在之后的代码中需要用到(给给定元素的)索引,那么` Array.indexOf` 是不二之选。 + +然而,如果我们仅需要知道数组中是否包含给定元素呢?这意味着只是是与否的区别,这是一个布尔问题(boolean question)。针对这种情况,我建议使用直接返回布尔值的 `Array.includes`。 +```js +'use strict'; + +const characters = [ + 'ironman', + 'black_widow', + 'hulk', + 'captain_america', + 'hulk', + 'thor', +]; + +console.log(characters.indexOf('hulk')); +// 2 +console.log(characters.indexOf('batman')); +// -1 + +console.log(characters.includes('hulk')); +// true +console.log(characters.includes('batman')); +// false +``` + +## 2.使用 `Array.find` 替代 `Array.filter` +`Array.filter` 是一个十分有用的方法。它通过回调函数过滤原数组,并将过滤后的项作为新数组返回。正如它的名字所示,我们将这个方法用于过滤,(一般而言)会获得一个长度更短的新数组。 + +然而,如果知道经回调函数过滤后,只会剩余唯一的一项,那么我不建议使用 `Array.filter`。比如:使用等于某个唯一 ID 为过滤条件去过滤一个数组。在这个例子中,`Array.filter` 返回一个仅有一项的新数组。然而,我们仅仅是为了获取 ID 为特定 ID 的那一项,这个新数组显得毫无用处。 + +让我们讨论一下性能。为了获取所有符合回调函数过滤条件的项,`Array.filter` 必须遍历整个数组。如果原数组中有成千上万项,回调函数需要执行的次数是相当多的。 + +为避免这些情况,我建议使用 `Array.find`。它与 `Array.filter` 一样需要一个回调函数,(但只是返回)符合条件的第一项。当找到符合回调函数过滤条件的第一个元素时,它会立即停止往下的搜寻。不再遍历整个数组。 +```js +'use strict'; + +const characters = [ + { id: 1, name: 'ironman' }, + { id: 2, name: 'black_widow' }, + { id: 3, name: 'captain_america' }, + { id: 4, name: 'captain_america' }, +]; + +function getCharacter(name) { + return character => character.name === name; +} + +console.log(characters.filter(getCharacter('captain_america'))); +// [ +// { id: 3, name: 'captain_america' }, +// { id: 4, name: 'captain_america' }, +// ] + +console.log(characters.find(getCharacter('captain_america'))); +// { id: 3, name: 'captain_america' } +``` + +## 3.使用 `Array.some` 替代 `Array.find` +我承认我经常犯这个错误。之后,一位朋友建议我去查看 [MDN 文档](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/some) 以寻找更好的方法。事实上(这错误)与上面 `Array.indexOf`/`Array.includes` 的例子十分相像。 + +在上面的例子中,我们知道 `Array.find` 需要一个回调函数作为参数,并返回(符合条件的)第一个元素。然而,当我们需要知道数组中是否存在一个元素时,`Array.find` 是最好的选择吗?不一定是,因为它返回一个元素,而不是一个布尔值。 + +在下面的例子中,我建议使用 `Array.some`,它返回你需要的布尔值。 +```js +'use strict'; + +const characters = [ + { id: 1, name: 'ironman', env: 'marvel' }, + { id: 2, name: 'black_widow', env: 'marvel' }, + { id: 3, name: 'wonder_woman', env: 'dc_comics' }, +]; + +function hasCharacterFrom(env) { + return character => character.env === env; +} + +console.log(characters.find(hasCharacterFrom('marvel'))); +// { id: 1, name: 'ironman', env: 'marvel' } + +console.log(characters.some(hasCharacterFrom('marvel'))); +// true +``` +译者注:补充一下 `Array.some` 与 `Array.includes` 使用上的区别。两者都返回一个布尔值,表示某项是否存在于数组之中,一旦找到对应的项,立即停止遍历数组。不同的是 `Array.some` 的参数是回调函数,而 `Array.includes` 的参数是一个值(均不考虑第二个可选参数)。 +假设希望知道值为 value 的项是否存在于数组中,既可以编写代码:`[].includes(value)`, 也可以给 `Array.some` 传入 `item => item === value` 作为回调函数。`Array.includes` 使用更简单,`Array.some` 可操控性更强。 + +## 4.使用 `Array.reduce` 替代 `Array.filter` 与 `Array.map` 的组合 +事实上说,`Array.reduce` 不太容易理解。然而,如果我们先使用 `Array.filter` 过滤原数组,之后(对结果)再调用 `Array.map` (以获取一个新数组)。这看起似乎有点问题,是我们忽略了什么吗? + +这样做的问题是:我们遍历了两次数组。第一次是过滤原数组以获取一个长度稍短的新数组,第二次遍历(译者注:指 `Array.map`)是对 `Array.filter` 的返回的新数组进行加工,再次创造了一个新数组!为得到最终的结果,我们结合使用了两个数组方法。每个方法都有它自己的回调函数,而且供 `Array.map` 使用的临时数组是由 `Array.filter` 提供的,(一般而言)该数组无法复用。 + +为避免如此低效场景的出现,我的建议是使用 `Array.reduce` 。一样的结果,更好的代码!`Array.reduce` 允许你将过滤后切加工过的项放进累加器中。累加器可以是需要待递增的数字、待填充的对象、 待拼接的字符串或数组等。 + +在上面的例子中,我们使用了 `Array.map`,(但更)建议使用累加器为待拼接数组的 `Array.reduce` 。在下面的例子中,根据变量 `env` 的值,我们会将它加进累加器中或保持累加器不变(即不作任何处理)。 +```js +'use strict'; + +const characters = [ + { name: 'ironman', env: 'marvel' }, + { name: 'black_widow', env: 'marvel' }, + { name: 'wonder_woman', env: 'dc_comics' }, +]; + +console.log( + characters + .filter(character => character.env === 'marvel') + .map(character => Object.assign({}, character, { alsoSeenIn: ['Avengers'] })) +); +// [ +// { name: 'ironman', env: 'marvel', alsoSeenIn: ['Avengers'] }, +// { name: 'black_widow', env: 'marvel', alsoSeenIn: ['Avengers'] } +// ] + +console.log( + characters + .reduce((acc, character) => { + return character.env === 'marvel' + ? acc.concat(Object.assign({}, character, { alsoSeenIn: ['Avengers'] })) + : acc; + }, []) +) +// [ +// { name: 'ironman', env: 'marvel', alsoSeenIn: ['Avengers'] }, +// { name: 'black_widow', env: 'marvel', alsoSeenIn: ['Avengers'] } +// ] +``` + +### 这就是本文的全部内容! +希望这对你有帮助。如果你对本文有任何意见或(关于数组方法使用的)例子需要讨论,请在评论中告诉我。如果你觉得本文不错,请给我点赞 👏 (译者注:对灯发誓,这是原文,不是译者骗赞!)并分享给更多的小伙伴。感谢你的阅读! + +注意:请在使用 `Array.find` 和 `Array.includes` 前检查浏览器是否支持相关方法,上述两个方法在 Internet Explorer 上并不支持(译者注:可以使用` Polyfill`)。 diff --git "a/Cute-Article/article/47-http\350\257\267\346\261\202\345\244\264\344\270\216\345\223\215\345\272\224\345\244\264\347\232\204\345\272\224\347\224\250.md" "b/Cute-Article/article/47-http\350\257\267\346\261\202\345\244\264\344\270\216\345\223\215\345\272\224\345\244\264\347\232\204\345\272\224\347\224\250.md" new file mode 100644 index 00000000..5d93ba9c --- /dev/null +++ "b/Cute-Article/article/47-http\350\257\267\346\261\202\345\244\264\344\270\216\345\223\215\345\272\224\345\244\264\347\232\204\345\272\224\347\224\250.md" @@ -0,0 +1,547 @@ +> [阅读原文](https://juejin.im/post/5b854ddef265da43635d9302) + +## Chap1 发现headers +当我们随便打开一个网址(比如大家经常拿来测试网络的百度)时,打开Network,会看到如下请求头,响应头: +![图1](https://user-gold-cdn.xitu.io/2018/8/28/16580c211bbcc595?imageView2/0/w/1280/h/960/ignore-error/1) +究竟这些headers都有什么用呢? 咱们挨个探个究竟。 + +## Chap2 headers用途 + +### 2.1 Content-Type +`Content-Type`表示请求头或响应头的内容类型。作为请求头时,利用它可以进行`body-parser`。 +Sooo~ What is body-parser? +body-parser是node常用的中间件,其作用是: + +> Parse incoming request bodies in a middleware before your handlers, available under the req.body property. + +即在处理数据之前用中间件对post请求体进行解析。 +[body-parser](https://www.npmjs.com/package/body-parser)的例子为: + +下面的例子展示了如何给路由添加`body parser`。通常,这是在`express`中最为推荐的使用`body-parser`的方法。 +```js +var express = require('express') +var bodyParser = require('body-parser') +var app = express() +// create application/json parser +var jsonParser = bodyParser.json() +// create application/x-www-form-urlencoded parser +var urlencodedParser = bodyParser.urlencoded({ extended: false }) +// POST /login gets urlencoded bodies +app.post('/login', urlencodedParser, function (req, res) { + if (!req.body) return res.sendStatus(400) + res.send('welcome, ' + req.body.username) +}) +// POST /api/users gets JSON bodies +app.post('/api/users', jsonParser, function (req, res) { + if (!req.body) return res.sendStatus(400) + // create user in req.body +}) +``` +`body-parser`核心源码为: +```js + // this uses a switch for static require analysis + switch (parserName) { + case 'json': + parser = require('./lib/types/json') + break + case 'raw': + parser = require('./lib/types/raw') + break + case 'text': + parser = require('./lib/types/text') + break + case 'urlencoded': + parser = require('./lib/types/urlencoded') + break + } +``` +以`json`为例: +```js +var contentType = require('content-type') +//... +/** + * Get the charset of a request. + * + * @param {object} req + * @api private + */ +function getCharset (req) { + try { + return (contentType.parse(req).parameters.charset || '').toLowerCase() + } catch (e) { + return undefined + } +} +//... +// assert charset per RFC 7159 sec 8.1 +var charset = getCharset(req) || 'utf-8' +if (charset.substr(0, 4) !== 'utf-') { + debug('invalid charset') + next(createError(415, 'unsupported charset "' + charset.toUpperCase() + '"', { + charset: charset, + type: 'charset.unsupported' + })) + return +} +``` +可以看出:其背后工作原理就是通过分析请求头中的`Content-Type`的类型,根据不同的类型进行相应数据处理,我们自己模拟一下: + +step1: 先建立`server.js`: +```js + req.on('end',function (params) { + let r = Buffer.concat(arr).toString(); + // body-parser 解析请求,根据不同的格式进行不同的解析 + if (req.headers['content-type'] === www.js){ + let querystring = require('querystring'); + r = querystring.parse(r); // a=1&b=2 + console.log(r,1); + } else if (req.headers['content-type'] === 'application/json'){ + console.log(JSON.parse(r),2); + } else{ + console.log(r,3); + } + res.end('end'); + }) +``` +step2: 客户端模拟请求: +```js +let opts = { + host:'localhost', + port:3000, + path:'/hello', + headers:{ + 'a':1, + 'Content-Type':'application/json', + "Content-Length":7 //模拟的时候需要带上长度,不然客户端会当成没有传递数据 + } +} +let http = require('http'); +let client = http.request(opts,function (res) { + res.on('data',function (data) { + console.log(data.toString()); + }) +}); +client.end("{\"a\":1}"); // 表示把请求发出去 +``` +step3: 测试。 +先启动server,再启动client,服务端收到按照`application/json`格式解析的数据: `{ a: 1 } 2`.`Content-Type`与`body-parser`之间的关系就先分析到这里了。后面我们接着看请求头。 + +### 2.2 Range:bytes +请求头通过`Range:bytes`可以请求资源的某一部分。利用这个字段可模拟部分读取。如下: +```js + http.createServer(function (req, res) { + let range = req.headers['range']; + }) +``` +server: +```js +let http = require('http'); +let fs = require('fs'); +let path = require('path'); +// 当前要下载的文件的大小 +let size = fs.statSync(path.join(__dirname, 'my.txt')).size; +let server = http.createServer(function (req, res) { + let range = req.headers['range']; // 0-3 + if (range) { + // 模拟请求 curl -v --header "Range:bytes=0-3" http://localhost:3000 + let [, start, end] = range.match(/(\d*)-(\d*)/); + start = start ? Number(start) : 0; + end = end ? Number(end) : size - 1; // 10个字节 size 10 (0-9) + res.setHeader('Content-Range', `bytes ${start}-${end}/${size - 1}`); + fs.createReadStream(path.join(__dirname, 'my.txt'), { start, end }).pipe(res); + } else { + // 会把文件的内容写给客户端 + fs.createReadStream(path.join(__dirname, 'my.txt')).pipe(res); + //可读流可以通过pipe导到可写流 + } +}); +server.listen(3000); +``` +client: +```js +let opts = { + host:'localhost', + port:3000, + headers:{} +} +let http = require('http'); +let start = 0; +let fs = require('fs'); +function download() { + opts.headers.Range = `bytes=${start}-${start+3}`; + start+=4; + console.log(`start is ${start}`) + let client = http.request(opts,function (res) { + let total = res.headers['content-range'].split('/')[1]; + // console.log(half) + res.on('data',function (data) { + fs.appendFileSync('./download1.txt',data); + }); + res.on('end',function () { + setTimeout(() => { + if ((!pause)&&(start < total)) + download(); + }, 1000); + }) + }); + client.end(); +} +download() +``` +分段读取添加暂停功能,监听用户输入 +```js +let pause = false; +process.stdin.on('data',function (data) { + if (data.toString().includes('p')){ + pause = true + }else{ + pause = false; + download() + } +}) +``` +测试结果: +![图2](https://user-gold-cdn.xitu.io/2018/9/10/165c2355ff651d7d?imageView2/0/w/1280/h/960/ignore-error/1) + +分段读取有以下好处: + +> 提高读取速度,多线程并行,分块读取 +> 断点续传 + +模拟并行下载: +```js +let halfFlag = 20 +function download() { + opts.headers.Range = `bytes=${start}-${start+3}`; + start+=4; + console.log(`start is ${start}`) + let client = http.request(opts,function (res) { + let total = res.headers['content-range'].split('/')[1]; + let halfFlag = Math.floor(total/2) + // console.log(half) + res.on('data',function (data) { + fs.appendFileSync('./download1.txt',data); + }); + res.on('end',function () { + setTimeout(() => { + if ((!pause)&&(start < halfFlag)) + download(); + }, 1000); + }) + }); + client.end(); +} +let half = halfFlag + +function downloadTwo() { + opts.headers.Range = `bytes=${half}-${half+3}`; + half+=4; + console.log(`half is ${half}`) + let client = http.request(opts,function (res) { + let total = res.headers['content-range'].split('/')[1]; + res.on('data',function (data) { + fs.appendFileSync('./download2.txt',data); + }); + res.on('end',function () { + setTimeout(() => { + if (!pause&&half < total) + downloadTwo(); + }, 1000); + }) + }); + client.end(); +} +download(); +downloadTwo(); +``` +运行结果,会把原文件分成两部分下载到download1.txt和download2.txt。 +测试: +![图3](https://user-gold-cdn.xitu.io/2018/9/10/165c23670c9165b8?imageView2/0/w/1280/h/960/ignore-error/1) + +理论上,这样的下载方式会比第一种方法节约一半的时间。但是实际中的文件下载怎样实现加速以及并行下载的,还有待考究。 + +### 2.3 Cache-Control与Expires之强制缓存 +Response Header响应头中`Cache-Control: max-age=1233`可以设置相对当前的时间的强制缓存,与它相关的`Expires`可以设置某个绝对时间点限定读取缓存的时间。 +模拟实现: +```js +let url = require('url'); // 专门用来处理url路径的核心模块 +// http://username:password@hostname:port/pathname?query +let server = http.createServer(async function (req,res) { + console.log(req.url) + let { pathname,query} = url.parse(req.url,true); + // true就是将query转化成对象 + let readPath = path.join(__dirname, 'public', pathname); + try { + let statObj = await stat(readPath); + // 根客户端说 10s 内走缓存 + res.setHeader('Cache-Control','max-age=10'); + res.setHeader('Expires',new Date(Date.now()+10*1000).toGMTString()); + // 10s之内的请求都会走cache 返回200, (from disk cache)不发生请求 + if (statObj.isDirectory()) { + let p = path.join(readPath, 'index.html'); + await stat(p); + // 如果当前目录下有html那么就返回这个文件 + fs.createReadStream(p).pipe(res); + } else { + fs.createReadStream(readPath).pipe(res); + } + }catch(e){ + res.statusCode = 404; + res.end(`Not found`); + } +}).listen(3000); +``` +测试: +![图4](https://user-gold-cdn.xitu.io/2018/9/10/165c24917e9fcc8f?imageView2/0/w/1280/h/960/ignore-error/1) +10s内刷新: +![图4](https://user-gold-cdn.xitu.io/2018/9/10/165c2498b620d636?imageView2/0/w/1280/h/960/ignore-error/1) + +### 2.4 对比缓存之Last-Modified和If-Modified-Since +对比响应头Last-Modified and 与请求头If-Modified-Since,可以通过文件修改时间看文件是否修改,从而决定是重新请求还是走缓存。 +模拟如下: +step1 不设置强制缓存 +```js +res.setHeader('Cache-Control','no-cache'); +``` +step2 应用文件修改时间比对是否修改, +```js +res.setHeader('Last-Modified', statObj.ctime.toGMTString()); +if (req.headers['if-modified-since'] === statObj.ctime.toGMTString()) { + res.statusCode = 304; + res.end(); + return; // 走缓存 +} +fs.createReadStream(readPath).pipe(res); +``` +测试: +![图6](https://user-gold-cdn.xitu.io/2018/9/10/165c25325aa08daf?imageView2/0/w/1280/h/960/ignore-error/1) + +### 2.5 对比缓存之Etag和 If-None-Match +对比响应头:Etag 与请求头:If-None-Match,Etag和If-None-Match如果相等,即返回304。 +etag如何添加? + +> 根据文件内容,生成一个md5的摘要,给实体加一个标签。 + +这种方法虽然比较耗性能,但是能够更加精确的对比出文件是否进行了修改。依靠文件修改时间进行对比并不够准确。因为有时文件有改动Last-Modified发生了变化,但是文件的内容可能根本没有变化。所以这种方案要优于2.4. + +实现方法: +```js +let rs = fs.createReadStream(p); +let md5 = crypto.createHash('md5'); // 不能写完响应体再写头 +let arr = []; +rs.on('data',function (data) { + md5.update(data); + arr.push(data); +}); +``` +设置Etag +```js +rs.on('end',function () { +let r = md5.digest('base64'); +res.setHeader('Etag', r); +if (req.headers['if-none-match'] === r ){ + res.statusCode = 304; + res.end(); + return; +} +res.end(Buffer.concat(arr)); +}) +``` +测试: +![图7](https://user-gold-cdn.xitu.io/2018/9/10/165c25456876367f?imageView2/0/w/1280/h/960/ignore-error/1) + +### 2.6 Accept-Encoding +依靠请求头: `Accept-Encoding: gzip, deflate`, br告诉服务端可接受的数据格式。服务端返回后会把数据格式通过响应格式通过Content-Encoding来标记。 +在客户端接受gzip的格式下,后端可通过文件压缩处理传递,提高性能。 +node api中提供了[zlib](http://nodejs.cn/api/zlib.html#zlib_class_zlib_gzip)模块: +> zlib模块提供通过 Gzip 和` Deflate/Inflate` 实现的压缩功能 + +下面我们来应用zlib与请求头`Accept-Encoding`来实现压缩功能。 +```js +let zlib = require('zlib'); +let fs = require('fs'); +let path = require('path'); +function gzip(filePath) { + let transform = zlib.createGzip();//转化流通过transform压缩,然后再写 + fs.createReadStream(filePath).pipe(transform).pipe(fs.createWriteStream(filePath+'.gz')); +} +gzip('2.txt') +``` +解压: +```js +function gunzip(filePath) { + let transform = zlib.createGunzip(); + fs.createReadStream(filePath).pipe(transform).pipe(fs.createWriteStream(path.basename(filePath,'.gz'))); +} +``` +`path.basename(filePath,'.gz')`用来去掉filePath文件名的后缀`.gz`。 +根据请求头接受的类型后端的具体操作 : +```js +if(req.url === '/download'){ + res.setHeader('Content-Disposition', 'attachment' ) + return fs.createReadStream(path.join(__dirname, '1.html')).pipe(res); +} +``` + +```js +let http = require('http'); +let fs = require('fs'); +let path = require('path'); +let zlib = require('zlib'); +http.createServer(function (req,res) { + if(req.url === '/download'){ + res.setHeader('Content-Disposition', 'attachment' ) + return fs.createReadStream(path.join(__dirname, '1.html')).pipe(res); + } + let rule = req.headers['accept-encoding']; + if(rule){ + if(rule.match(/\bgzip\b/)){ + res.setHeader('Content-Encoding','gzip'); + fs.createReadStream(path.join(__dirname, '1.html')) + .pipe(zlib.createGzip()) + .pipe(res); + } else if (rule.match(/\bdeflate\b/)){ + res.setHeader('Content-Encoding', 'deflate'); + fs.createReadStream(path.join(__dirname, '1.html')) + .pipe(zlib.createDeflate()) + .pipe(res); + }else{ + fs.createReadStream(path.join(__dirname, '1.html')).pipe(res); + } + }else{ + fs.createReadStream(path.join(__dirname, '1.html')).pipe(res); + } +}).listen(3000); +``` + +test deflate: +```docker +curl -v --header "Accept-Encoding:deflate" http://localhost:3000 +* Rebuilt URL to: http://localhost:3000/ +* Trying 127.0.0.1... +* TCP_NODELAY set +* Connected to localhost (127.0.0.1) port 3000 (#0) +> GET / HTTP/1.1 +> Host: localhost:3000 +> User-Agent: curl/7.54.0 +> Accept: */* +> Accept-Encoding:deflate +> +< HTTP/1.1 200 OK +< Content-Encoding: deflate +< Date: Thu, 23 Aug 2018 03:01:13 GMT +< Connection: keep-alive +< Transfer-Encoding: chunked +``` +test others: +```docker +curl -v --header "Accept-Encoding:nn" http://localhost:3000 +* Rebuilt URL to: http://localhost:3000/ +* Trying 127.0.0.1... +* TCP_NODELAY set +* Connected to localhost (127.0.0.1) port 3000 (#0) +> GET / HTTP/1.1 +> Host: localhost:3000 +> User-Agent: curl/7.54.0 +> Accept: */* +> Accept-Encoding:nn +> +< HTTP/1.1 200 OK +< Date: Thu, 23 Aug 2018 03:02:51 GMT +< Connection: keep-alive +< Transfer-Encoding: chunked +< + + + + + + + Document + + + 你好 + + +* Connection #0 to host localhost left intact +% +``` +### 2.7 referer +![图8](https://user-gold-cdn.xitu.io/2018/9/10/165c268acf50a0af?imageView2/0/w/1280/h/960/ignore-error/1) +referer表示请求文件的网址,请求时会携带。为了防止自己网站的文件被外网直接引用,可以通过比较referer,即请求的地址,与本地地址比较,设置防盗链。 +```js +let http = require('http'); +let fs = require('fs'); +let url = require('url'); +let path = require('path'); +// 这是百度的服务器 +let server = http.createServer(function (req,res) { + let { pathname } = url.parse(req.url); + let realPath = path.join(__dirname,pathname); + fs.stat(realPath,function(err,statObj) { + if(err){ + res.statusCode = 404; + res.end(); + }else{ + let referer = req.headers['referer'] || req.headers['referred']; + if(referer){ + let current = req.headers['host'] // 代表的是当前图片的地址 + referer = url.parse(referer).host // 引用图片的网址 + if (current === referer){ + fs.createReadStream(realPath).pipe(res); + }else{ + fs.createReadStream(path.join(__dirname,'images/2.jpg')).pipe(res); + } + }else{ + fs.createReadStream(realPath).pipe(res); + } + } + }) +}).listen(3000); +``` +### 2.8 Accept-Language +请求头:`Accept-Language: zh-CN,zh;q=0.9` +多个语言用 ',' 分隔,权重用 '=' 表示',没有默认权重为1 + +后端根据请求接受语言的权重一次查找,查找到就返回,找不到就用默认语言 +```js +let langs = { + en: 'hello world', + 'zh-CN':'你好世界', + zh:'你好', + ja: 'こんにちは、世界' +} +let defualtLanguage = 'en' +// 多语言之服务端方案:来做 (浏览器会发一个头) 前端来做 +// 通过url实现多语言 +let http = require('http'); +http.createServer(function (req,res) { + let lan = req.headers['accept-language']; + //[[zh,q=0.9],[zh-CN]] =>[{name:'zh-CN',q=1},{name:'zh',q:0.9}] + if(lan){ + lan = lan.split(','); + lan = lan.map(l=>{ + let [name,q] = l.split(';'); + q = q?Number(q.split('=')[1]):1 + return {name,q} + }).sort((a,b)=>b.q-a.q); // 排出 权重数组 + + for(let i = 0 ;i `标签,所以也可表示为: +```js +// 表示HTML文档所在窗口的当前高度 +let height = document.body.clientHeight; +// 表示HTML文档所在窗口的当前宽度 +let width = document.body.clientWidth; +``` +结论: +`document.body.clientWidth/Height` 的宽高偏小,高甚至默认200; +`document.documentElement.clientWidth/Height` 和 `window.innerWidth/Height` 的宽高始终相等。 +所以在不同浏览器都实用的的Javascripit方案: +```js +let height = document.documentElement.clientWidth || document.body.clientWidth; +let width = document.documentElement.clientHeight || document.body.clientHeight; +``` + +## 二、网页正文全文宽高 +`scrollWidth` 和 `scrollHeight` 获取网页内容高度和宽度: + +* 1.针对IE.Opera: +`scrollHeight`是网页内容实际高度,可以小于`clientHeight`; + +* 2.针对NS.firefox: +`scrollHeight`是网页内容高度,不过最小值是`clientHeight`;也就是说网页内容实际高度小于`clientHeight`的时候,`scrollHeight`返回`clientHeight`; + +* 3.浏览器兼容代码: + +```js +let height = document.documentElement.scrollHeight || document.body.scrollHeight; +let width = document.documentElement.scrollWidth || document.body.scrollWidth; +``` + +## 三、网页可见区域宽高,包括滚动条等边线(会随窗口的显示大小改变) +* 1.值: +offsetWidth = scrollWidth + 左右滚动条 + 左右边框; +offsetHeight = scrollHeight + 上下滚动条 + 上下边框; + +* 2.浏览器兼容代码: + +```js +let width = document.documentElement.offsetWidth || document.body.offsetWidth ; +let height = document.documentElement.offsetHeight || document.body.offsetHeight ; +``` + +## 四、网页卷去的距离与偏移量 +1.`scrollLeft`:设置或获取位于给定**对象左边界**与窗口中**目前可见内容的最左端**之间的距离; +2.`scrollTop`:设置或获取位于给定**对象最顶端**与窗口中**目前可见内容的最左端**之间的距离; +3.`offsetLeft`:设置或获取位于给定对象相对于版面或由offsetParent属性指定的父坐标的计算左侧位置; +4.`offsetTop`:设置或获取位于给定对象相对于版面或由offsetParent属性指定的父坐标的计算顶端位置; + +## 常用高度/宽度获取的整理 +* 1.获取屏幕的高度和宽度(屏幕分辨率): + +```js +window.screen.height +window.screen.width +``` + +* 2.获取屏幕工作区域的高度和宽度(去掉状态栏): + +```js +window.screen.availHeight +window.screen.availWidth +``` + +* 3.网页全文的高度和宽度: + +```js +document.body.scrollHeight +document.body.scrollWidth +``` + +* 4.滚动条卷上去的高度和向右卷的宽度: + +```js +document.body.scrollTop +document.body.scrollLeft +``` +* 5.网页可见区域的高度和宽度(不加边线): + +```js +document.body.clientHeight +document.body.clientWidth +``` +* 6.网页可见区域的高度和宽度(加边线): + +```js +document.body.offsetHeight +document.body.offsetWidth +``` \ No newline at end of file diff --git "a/Cute-Article/article/49-Vue \351\235\242\350\257\225\344\270\255\345\270\270\351\227\256\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206.md" "b/Cute-Article/article/49-Vue \351\235\242\350\257\225\344\270\255\345\270\270\351\227\256\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206.md" new file mode 100644 index 00000000..b1166a77 --- /dev/null +++ "b/Cute-Article/article/49-Vue \351\235\242\350\257\225\344\270\255\345\270\270\351\227\256\347\237\245\350\257\206\347\202\271\346\225\264\347\220\206.md" @@ -0,0 +1,208 @@ +[原文](https://mp.weixin.qq.com/s/5tiAmJCLlPTQObMDY2ZgkA) + +看看面试题,只是为了查漏补缺,看看自己那些方面还不懂。切记不要以为背了面试题,就万事大吉了,最好是理解背后的原理,这样面试的时候才能侃侃而谈。不然,稍微有水平的面试官一看就能看出,是否有真才实学还是刚好背中了这道面试题(有空再把例子中代码补上)。 + +## 一、对于MVVM的理解? +MVVM 是 Model-View-ViewModel 的缩写。 + +* **Model** 代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑。 +* **View** 代表UI 组件,它负责将数据模型转化成UI 展现出来。 +* **ViewModel** 监听模型数据的改变和控制视图行为、处理用户交互,简单理解就是一个同步View 和 Model的对象,连接Model和View。 + +在MVVM架构下,View 和 Model 之间并没有直接的联系,而是通过ViewModel进行交互,Model 和 ViewModel 之间的交互是双向的, 因此View 数据的变化会同步到Model中,而Model 数据的变化也会立即反应到View 上。 + +ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,而View 和 Model 之间的同步工作完全是自动的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作DOM,不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。 + +## 二、Vue的生命周期 + +`beforeCreate`(创建前),在数据观测和初始化事件还未开始 + +`created`(创建后),完成数据观测,属性和方法的运算,初始化事件, $el 属性还没有显示出来 + +`beforeMount`(载入前),在挂载开始之前被调用,相关的render函数首次被调用。实例已完成以下的配置:编译模板,把data里面的数据和模板生成html。注意此时还没有挂载html到页面上。 + +`mounted`(载入后),在el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的html内容替换el属性指向的DOM对象。完成模板中的html渲染到html页面中。此过程中进行ajax交互。 + +`beforeUpdate`(更新前),在数据更新之前调用,发生在虚拟DOM重新渲染和打补丁之前。可以在该钩子中进一步地更改状态,不会触发附加的重渲染过程。 + +`updated`(更新后),在由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用。调用时,组件DOM已经更新,所以可以执行依赖于DOM的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。 + +`beforeDestroy`(销毁前),在实例销毁之前调用。实例仍然完全可用。 + +`destroyed`(销毁后),在实例销毁之后调用。调用后,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务器端渲染期间不被调用。 + +### 1、什么是vue生命周期? + +答: Vue 实例从创建到销毁的过程,就是生命周期。从`开始创建`、`初始化数据`、`编译模板`、`挂载Dom→渲染`、`更新→渲染`、`销毁`等一系列过程,称之为 Vue 的**生命周期**。 + +### 2、vue生命周期的作用是什么? + +答:它的生命周期中有多个事件钩子,让我们在控制整个Vue实例的过程时更容易形成好的逻辑。 + +### 3、vue生命周期总共有几个阶段? + +答:它可以总共分为8个阶段:创建前/后、载入前/后、更新前/后、销毁前/销毁后。 + +### 4、第一次页面加载会触发哪几个钩子? + +答:会触发下面这几个`beforeCreate`、`created`、`beforeMount`、`mounted` 。 + +### 5、DOM 渲染在哪个周期中就已经完成? + +答:DOM 渲染在 `mounted` 中就已经完成了。 + +## 三、 Vue实现数据双向绑定的原理:Object.defineProperty() + +vue实现数据双向绑定主要是: +采用**数据劫持**结合**发布者-订阅者模式**的方式,通过 `Object.defineProperty()` 来劫持各个属性的`setter`,`getter`,在数据变动时发布消息给订阅者,触发相应监听回调。当把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项时,Vue 将遍历它的属性,用 `Object.defineProperty()` 将它们转为 `getter/setter`。用户看不到 `getter/sette`r,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。 + +vue的数据双向绑定: +将MVVM作为数据绑定的入口,整合`Observer`,`Compile`和`Watcher`三者,通过`Observer`来监听自己的`model`的数据变化,通过`Compile`来解析编译模板指令(vue中是用来解析` {{}}`),最终利用`watcher`搭起`observer`和`Compile`之间的通信桥梁,达到**数据变化 —>视图更新**;视图交互变化(input)—>数据model变更双向绑定效果。 + +js实现简单的双向绑定: +```html + +   
+    +   

+
+ + + +``` + + +## 四、Vue组件间的参数传递 + +### 1、父组件与子组件传值 +父组件传给子组件:子组件通过`props`方法接受数据; + +子组件传给父组件: `$emit` 方法传递参数 + +### 2、非父子组件间的数据传递,兄弟组件传值 +`eventBus`,就是创建一个事件中心,相当于中转站,可以用它来传递事件和接收事件。项目比较小时,用这个比较合适(虽然也有不少人推荐直接用VUEX,具体来说看需求咯。技术只是手段,目的达到才是王道)。 + +## 五、Vue的路由实现:hash模式 和 history模式 +**hash模式**:在浏览器中符号 `#`,#以及#后面的字符称之为`hash`,用 `window.location.hash` 读取。 +特点:`hash`虽然在URL中,但不被包括在HTTP请求中;用来指导浏览器动作,对服务端安全无用,`hash`不会重加载页面。 + +**history模式**:`history`采用HTML5的新特性;且提供了两个新方法: `pushState()`, `replaceState()`可以对浏览器历史记录栈进行修改,以及`popState`事件的监听到状态变更。 + +## 六、Vue与Angular以及React的区别? +版本在不断更新,以下的区别有可能不是很正确。我工作中只用到vue,对angular和react不怎么熟。 + +### 1、与AngularJS的区别 +#### 相同点: +* 都支持指令:内置指令和自定义指令; +* 都支持过滤器:内置过滤器和自定义过滤器; +* 都支持双向数据绑定; +* 都不支持低端浏览器。 + +#### 不同点: +* AngularJS的学习成本高,比如增加了Dependency Injection特性,而Vue.js本身提供的API都比较简单、直观; +* 在性能上,AngularJS依赖对数据做脏检查,所以Watcher越多越慢; +* Vue.js使用基于依赖追踪的观察并且使用异步队列更新,所有的数据都是独立触发的。 + +### 2、与React的区别 +#### 相同点: +* React采用特殊的`JSX`语法,Vue.js在组件开发中也推崇编写`.vue`特殊文件格式,对文件内容都有一些约定,两者都需要编译后使用; +* 中心思想相同:一切都是组件,组件实例之间可以嵌套;都提供合理的钩子函数,可以让开发者定制化地去处理需求; +* 都不内置列数AJAX,Route等功能到核心包,而是以插件的方式加载; +* 在组件开发中都支持`mixins`的特性。 + +#### 不同点: +* React采用的Virtual DOM会对渲染出来的结果做脏检查; +* Vue.js在模板中提供了指令,过滤器等,可以非常方便,快捷地操作Virtual DOM。 + +## 七、vue路由的钩子函数 +首页可以控制导航跳转,`beforeEach`,`afterEach`等,一般用于页面`title`的修改。一些需要登录才能调整页面的重定向功能。 + +**beforeEach**主要有3个参数`to`,`from`,`next`。 +**`to`**:route即将进入的目标路由对象。 +**`from`**:route当前导航正要离开的路由。 +**`next`**:function一定要调用该方法`resolve`这个钩子。执行效果依赖`next`方法的调用参数。可以控制网页的跳转。 + +## 八、vuex是什么?怎么使用?哪种功能场景使用它? +只用来读取的状态集中放在`store`中; +改变状态的方式是提交`mutations`,这是个同步的事物; +异步逻辑应该封装在`action`中。 + +在`main.js`引入`store`,注入。新建了一个目录`store`,`… export` 。 + +场景有:单页应用中,组件之间的状态、音乐播放、登录状态、加入购物车。 +![vue生命周期](http://p3nqtyvgo.bkt.clouddn.com/vue%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F.jpg) + +**state**:Vuex 使用单一状态树,即每个应用将仅仅包含一个`store` 实例,但单一状态树和模块化并不冲突。存放的数据状态,不可以直接修改里面的数据。 + +**mutations**:`mutations`定义的方法动态修改`Vuex `的 `store` 中的状态或数据。 + +**getters**:类似`vue`的计算属性,主要用来过滤一些数据。 + +**action**:`actions`可以理解为通过将`mutations`里面处里数据的方法变成可异步的处理数据的方法,简单的说就是异步操作数据。`view` 层通过` store.dispath` 来分发 `action`。 +```js +onst store = new Vuex.Store({ //store实例 +     state: { +        count: 0 +            }, +     mutations: {                 +        increment (state) { +         state.count++ +        } +         }, +     actions: { +        increment (context) { +         context.commit('increment') +   } + } +}) + +``` +**modules**:项目特别复杂的时候,可以让每一个模块拥有自己的`state`、`mutation`、`action`、`getters`,使得结构非常清晰,方便管理。 +```js +const moduleA = { + state: { ... }, + mutations: { ... }, + actions: { ... }, + getters: { ... } +} +const moduleB = { + state: { ... }, + mutations: { ... }, + actions: { ... } +} +const store = new Vuex.Store({ + modules: { +   a: moduleA, +   b: moduleB +}) + +``` + + +## 九、其它小知识点 + +### 1、css只在当前组件起作用 +答:在`style`标签中写入`scoped`即可 例如: `` + +### 2、`v-if` 和 `v-show` 区别 +答:`v-if`按照条件是否渲染,`v-show`是`display`的`block`或`none`; + +### 3、`$route`和`$router` 区别 + +答:`$route`是 `路由信息对象`,包括`path`,`params`,`hash`,`query`,`fullPath`,`matched`,`name`等路由信息参数。而`$router`是 `路由实例` 对象包括了路由的跳转方法,钩子函数等。 + +PS:缺少的案例代码,这几天再补上去。有些地方可能描述的不够清楚,如果有歧义,可能是我理解错了。 \ No newline at end of file diff --git "a/5-[\345\216\237\345\210\233]VUE\344\270\255\345\256\236\347\216\260\351\200\232\347\224\250js\345\207\275\346\225\260\345\272\223\345\260\201\350\243\205.md" "b/Cute-Article/article/5-[\345\216\237\345\210\233]VUE\344\270\255\345\256\236\347\216\260\351\200\232\347\224\250js\345\207\275\346\225\260\345\272\223\345\260\201\350\243\205.md" similarity index 100% rename from "5-[\345\216\237\345\210\233]VUE\344\270\255\345\256\236\347\216\260\351\200\232\347\224\250js\345\207\275\346\225\260\345\272\223\345\260\201\350\243\205.md" rename to "Cute-Article/article/5-[\345\216\237\345\210\233]VUE\344\270\255\345\256\236\347\216\260\351\200\232\347\224\250js\345\207\275\346\225\260\345\272\223\345\260\201\350\243\205.md" diff --git "a/Cute-Article/article/50-js\344\270\255get\345\222\214post\347\232\204\345\214\272\345\210\253.md" "b/Cute-Article/article/50-js\344\270\255get\345\222\214post\347\232\204\345\214\272\345\210\253.md" new file mode 100644 index 00000000..f6027312 --- /dev/null +++ "b/Cute-Article/article/50-js\344\270\255get\345\222\214post\347\232\204\345\214\272\345\210\253.md" @@ -0,0 +1,33 @@ +在常见的客户端传递参数的方式有`GET`和`POST`两种: +* **浏览器地址栏直接输入**:一定是`GET`请求; +* **超链接**:一定是`GET`请求; +* **表单**:可以是`GET`,也可以是`POST`,这取决与`
`的`method`属性值; + +### 两者区别: +#### 1.效率 +* `GET`的意思是 `得` ,从服务器获取数据(也可以上传数据,参数就是),效率较高; +* `POST`的意思是 `给` ,但可以向服务器发送数据和下载数据,效率不如`GET`; + +#### 2.缓存 +* `GET` 请求能够被缓存,默认的请求方式也是有缓存的; +* `POST` 请求默认不会缓存; + +> * 缓存是针对`URL`来进行缓存的,`GET`请求由于其参数是直接加在`URL`上的,一种参数组合就有一种URL的缓存,可以根据参数来进行一一对应,重复请求是幂等的(不论请求多少次,结果都一样); +> * 而POST请求的URL没有参数,每次请求的URL都相同,数据体(HTTPBody)可能不同,无法一一对应,所以缓存没有意义; + +#### 3.安全性 +* `GET` 的所有参数全部包装在`URL`中,明文显示,且服务器的访问日志会记录,非常不安全; +* `POST` 的`URL`中只有资源路径,不包含参数,参数封装在二进制的数据体中,服务器也不会记录参数,相对安全。所有涉及用户隐私的数据都要用 `POST` 传输; + +> `POST`的安全是**相对的**,对于普通用户来说他们看不到明文,数据封装对他们来说就是屏障。但是对于专业人士,它们会抓包会分析,没有加密的数据包对他们来说也是小case。所以POST仅仅是相对安全,唯有对数据进行加密才会更安全。当然加密也有被破解的可能性,**理论上所有的加密方式都可以破解**,只是时间长短的问题。而加密算法要做的就是使得破解需要的时间尽量长,越长越安全。由于我们也需要解密,加密算法太过复杂也并非好事,这就要结合使用情况进行折中或者足够实际使用即可。绕的有点远,具体的话,我将在后续的文章之中介提及,并介绍一些常用的加密算法。 + +#### 4.数据量 +`HTTP`协议中均没有对`GET`和`POST`请求的**数据大小**进行限制,但是实际应用中它们通常受限于软硬件平台的设计和性能。 +* `GET`:不同的浏览器和服务器不同,一般限制在`2~8K`之间,更加常见的是`1k`以内; +* `POST`:提交的数据较大,大小靠服务器的设定值限制,PHP默认是`2M`(具体的话大家以后看后端给的开发文档就行了); + +#### 5.数据获取方式 +* `GET` 需要使用` Request.QueryString `来取得变量的值; +* `POST` 通过 `Request.Form` 来获取变量的值; + +也就是说 `Get` 是通过**地址栏**来传值,而 `Post` 是通过**提交表单**来传值。 \ No newline at end of file diff --git "a/Cute-Article/article/51-Apache\344\271\213HTTP\345\215\217\350\256\256.md" "b/Cute-Article/article/51-Apache\344\271\213HTTP\345\215\217\350\256\256.md" new file mode 100644 index 00000000..87e79f96 --- /dev/null +++ "b/Cute-Article/article/51-Apache\344\271\213HTTP\345\215\217\350\256\256.md" @@ -0,0 +1,480 @@ +**`HTTP`(Hypertext Transfer Protocol)超文本传输协议**。是一种详细规定了客户端浏览器和万维网服务器之间相互通讯的规则,通过因特网传送万维网文档的数据传送协议。 + +## 一、HTTP的前世今生 +**超文本传输协议**的前身是Xanadu项目,超文本的概念是泰德.纳尔森在1960年提出的。而HTTP在1989年诞生在CERN(欧洲量子物理实验室)。1990年12月,超文本协议在CERN首次上线。1991年夏天,继Telnet等协议之后,超文本传输协议正式成为互联网诸多协议的一份子。 + +**HTTP诞生的原因**:为了实现从一台计算机上获取并显示存放在多台计算机里的文件、数据、图片和其他类型的文件而诞生HTTP协议。因为在当时其他诸多已经诞生的协议解决不了这个问题。例如:Telnet、SMTP、POP3、IAMP4、FTP等。所以HTTP协议应运而生。 + +## 二、HTTP协议的版本 +### HTTP 0.9 +**HTTP 0.9**作为HTTP协议的第一个成熟版本。此版本功能非常薄弱。 +* 1.请求只有一行 +* 2.没有HTTP头部信息和错误代码信息 +* 3.只能接收一种类型的数据:纯文本 +* 4.只有一种方法:GET + +### HTTP 1.0 +随着互联网的发展,**HTTP 0.9**已经不能满足互联网发展的需求。因此**HTTP 1.0**就这样诞生了。**HTTP 1.0**的最大改变是引入了`POST`方法,使得客户端通过`HTML表单`向服务器发送数据成为可能。从而实现了客户端和服务器端的数据交互。这是WEB应用程序的一个基础。另一个巨大的改变是引入了`HTTP头`,使得HTTP协议不仅能返回错误代码,并且借助于MIME技术能够传输更为丰富的文件类型,不再局限于纯文本。还可以是图片、动画等其他文件格式。 + +除此之外,还允许保持连接,即一次TCP连接,可以实现多次通讯。`HTTP 1.0`默认是**传输一次数据后就关闭连接**。 + +### HTTP 1.1 +2000年5月,`HTTP 1.1`诞生。`HTTP 1.1`并不像`HTTP 1.0`对`HTTP 0.9`那样的革命性。但是对`HTTP 1.0`做了很多功能性的增强。 +* 1.增加了`Host头` + 使得GET后面只需使用相对路径; + 使得一台主机可以使用多个域名; +* 2.引入了`Range头 + 使得客户端通过`HTTP`下载时只下载内容的一部分,使得多线程下载成为可能; + +## 三、HTTP协议的特点 +* **1.支持C/S模式** + 支持基本认证和安全认证。 + +* **2.简单快速** + 客户端向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有`GET`、`HEAD`、`POST`。每种方法规定了客户端与服务器端联系的不同类型。由于`HTTP协议`简单,使得HTTP服务器的程序规模小,因而通讯速度快。 + +* **3.灵活** + HTTP协议允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记。 + +* **4.HTTP 0.9和HTTP 1.0使用非持续连接** + 限制每次连接只处理一个请求,服务器处理完客户端的请求,并收到客户端的应答后,即断开连接。采用这种方式可以节省传输时间。 + HTTP 1.1使用持续连接:不必为每个Web对象创建一个新的连接,一个连接可以传送多个对象。 + +* **5.无状态:** + HTTP协议是无状态协议。 + 无状态是指协议对于事物处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。 + +## 四、HTTP实现原理 +**在TCP/IP协议栈中的位置**: + HTTP协议通常承载于TCP协议之上,有时也承载于TLS或SSL协议层之上,这个时候,就成了我们常说的HTTPS。如下图所示: +![HTTPS](http://s3.51cto.com/wyfs02/M02/38/15/wKioL1OybmbgvkXfAABJlqiTUPM914.jpg) + + 默认HTTP的端口号为80,HTTPS的端口号为443。 + +**HTTP的请求响应模型**: + HTTP协议永远都是客户端发起请求,服务器回送响应。如下图所示: +![发起请求](http://s3.51cto.com/wyfs02/M00/38/15/wKiom1OybujR2rlWAABZ59w_LlM241.jpg) + + + 这样就限制了使用HTTP协议,无法实现在客户端没有发起请求的时候,服务器将消息推送给客户端。 + + HTTP协议是一个无状态的协议,同一个客户端的这次请求和上次请求是没有对应关系。 + +**工作流程:** +一次HTTP操作称为一个事务,其工作过程可分为四步: +* 1.首先客户机与服务器需要**建立连接**。只要单击某个超级链接,HTTP的工作开始。 + +* 2.建立连接后,客户机发送一个请求给服务器,请求方式的格式为:**统一资源标识符(URL)、协议版本号**,后边是MIME信息包括请求修饰符、客户机信息和可能的内容。 + +* 3.服务器接到请求后,给予相应的响应信息,其格式为一个状态行,包括**信息的协议版本号**、**一个成功或错误的代码**,后边是MIME信息包括服务器信息、实体信息和可能的内容。 + +* 4.客户端接收服务器所返回的信息通过浏览器显示在用户的显示屏上,然后客户机与服务器**断开连接**。 + +如果在以上过程中的某一步出现错误,那么产生错误的信息将返回到客户端,有显示屏输出。对于用户来说,这些过程是由HTTP自己完成的,用户只要用鼠标点击,等待信息显示就可以了。 + +**简化版**: +* 1.建立连接 +* 2.接收请求 +* 3.处理请求 +* 4.获取资源 +* 5.构建响应 +* 6.回送响应 +* 7.记录日志 + +## 五、HTTP的几个重要概念 +### URI、URL、URN: +* **URI**: Uniform Resource Identifier,统一资源标识符; +* **URL**: Uniform Resource Locator,统一资源定位符; +* **URN**: Uniform Resource Name,统一资源命名符; + +其中,URL,URN是URI的**子集**。 + +Web上地址的**基本形式**是URI,它代表**通用资源标识符**。有两种形式: +* **URL**:目前URI的最普遍形式就是无处不在的URL或统一资源定位器。 +* **URN**:URL的一种更新形式,统一资源名称(URN, Uniform Resource Name)不依赖于位置,并且有可能减少失效连接的个数。但是其流行还需假以时日,因为它需要更精密软件的支持。 + +**URI格式**: +``` +scheme://[username:password@]HOST:port/path/to/source +``` + +### 连接(Connection): +一个传输层的实际环流,它是建立在**两个相互通讯的应用程序之间**。 + +在**http1.1**,`request`和`reponse`头中都有可能出现一个`connection`的头,此`header`的含义是当`client`和`serve`r通信时对于长链接如何进行处理。 + +在**http1.1**中,`client`和`server`都是默认**对方支持长链接**的, 如果`client`使用**http1.1协议**,但又不希望使用长链接,则需要在`header`中指明`connection`的值为`close`;如果`server`方也不想支持长链接,则在`response`中也需要明确说明`connection`的值为`close`。不论`request`还是`response`的`header`中包含了值为`close`的`connection`,都表明当前正在使用的tcp链接在**当天请求处理完毕后会被断掉**。以后client再进行新的请求时就必须**创建新的tcp链接**了。 + +### 消息(Message): + HTTP通讯的基本单位,包括一个结构化的八元组序列并通过连接传输。 + +### 请求(Request): + 一个从客户端到服务器的请求信息包括**应用于资源的方法**、**资源的标识符**和**协议的版本号**。 + +### 响应(Response): + 一个从服务器返回的信息包括**HTTP协议的版本号**、**请求的状态**(例如“成功”或“没找到”)和**文档的MIME类型**。 + +### 资源(Resource): + 由**URI**标识的网络数据对象或服务。 + +### 实体(Entity): + 数据资源或来自服务资源的回映的一种特殊表示方法,它可能被包围在一个请求或响应信息中。一个实体包括**实体头信息**和**实体的本身内容**。 + +### 客户机(Client): + 一个为发送请求目的而建立连接的应用程序。 + +### 用户代理(UserAgent): + 初始化一个请求的客户机。它们是浏览器、编辑器或其它用户工具。 + +### 服务器(Server): + 一个接受连接并对请求返回信息的应用程序。 + +### 源服务器(OriginServer): + 是一个给定资源可以在其上驻留或被创建的服务器。 + +### 代理(Proxy): + 一个中间程序,它可以充当一个**服务器**,也可以充当一个**客户机**,为其它客户机建立请求。请求是通过可能的翻译在内部或经过传递到其它的服务器中。一个代理在发送请求信息之前,必须解释并且如果可能重写它。 + + 代理经常作为通过防火墙的客户机端的门户,代理还可以作为一个帮助应用来通过协议处理没有被用户代理完成的请求。 + +### 网关(Gateway): + 一个作为**其它服务器中间媒介的服务器**。与代理不同的是,网关接受请求就好象对被请求的资源来说它就是源服务器;发出请求的客户机并没有意识到它在同网关打交道。 + + 网关经常作为通过防火墙的服务器端的门户,网关还可以作为一个协议翻译器以便存取那些存储在非HTTP系统中的资源。 + +### 通道(Tunnel): + 是作为两个连接中继的中介程序。一旦激活,通道便被认为不属于HTTP通讯,尽管通道可能是被一个HTTP请求初始化的。当被中继的连接两端关闭时,通道便消失。当一个门户(Portal)必须存在或中介(Intermediary)不能解释中继的通讯时通道被经常使用。 + +### 缓存(Cache): + 反应信息的局域存储。 + +## 六、HTTP协议头 +HTTP头按照其不同的作用,分为四类: + +### 1.通用头(General Header) + 通用头**即可以包含在HTTP请求中,也可以包含在HTTP响应中**。 + 通用头的作用是描叙HTTP协议本身。比如**描叙HTTP是否持续连接**的`Connection头`,**HTTP发送日期**的`Date头`,**描述HTTP所在的TCP连接时间**的`Keep-Alive头`,用于**缓存控制**的`Cache-Control头`等。 + +### 2.实体头(Entity Header) + 实体头是那些**描叙HTTP信息的头**。即可出现在HTTP POST方法的请求中,也可以出现在HTTP响应中。 + 例如`Content-Type`和`Content-length`都是描述实体的类型和大小的头都属于实体头。其它还有用于描述实体的`Content-Language`,`Content-MD5`,`Content-Encoding`以及控制实体缓存的`Expires`和`Last-Modifies头`等。 + +**常见的实体头**如下: +> * **Allow**:服务器支持哪些请求方法(如GET、POST等); +> * **Content-Encoding**:文档的编码(Encode)方法,例如:gzip,见“2.5 响应头”; +> * **Content-Language**:内容的语言类型,例如:zh-cn; +> * **Content-Length**:表示内容长度,eg:80,可参考“2.5响应头”; +> * **Content-Location**:表示客户应当到哪里去提取文档; +> * **Content-MD5**:MD5 实体的一种MD5摘要,用作校验和。发送方和接受方都计算MD5摘要,接受方将其计算的值与此头标中传递的值进行比较。Eg1:Content-MD5: 。`Eg2:dfdfdfdfdfdfdff==`; +> * **Content-Range**:随部分实体一同发送;标明被插入字节的低位与高位字节偏移,也标明此实体的总长度。Eg1:`Content-Range: 1001-2000/5000,eg2:bytes 2543-4532/7898`; +> * **Content-Type**:标明发送或者接收的实体的MIME类型。Eg:`text/html`; `charset=GB2312`主类型/子类型; +> * **Expires**:为0证明不缓存; +> * **Last-Modified**:WEB 服务器认为对象的最后修改时间,比如文件的最后修改时间,动态页面的最后产生时间等等。例如:`Last-Modified:Tue, 06 May 2008 02:42:43 GMT`; + +### 3.请求头(HTTP Request Header) + 请求头是那些由客户端发往服务器端以便帮助服务器端更好的满足客户端请求的头。 + 请求头只能出现在HTTP请求中。比如告诉服务器**只接收某种响应内容**的`Accept头`,**发送Cookies**的`Cookie头`,**显示请求主机域**的`HOST头`,**用于缓存**的`If-Match`,`If-Match-Since`,`If-None-Match头`,**用于只取HTTP响应信息中部分信息**的`Range头`,**用于附属HTML相关请求引用**的`Referee头`等。 + +**常见请求头如下:** + +> * **Accept**:浏览器可接受的MIME类型; +> * **Accept-Charse**t:浏览器可接受的字符集; +> * **Accept-Encoding**:浏览器能够进行解码的数据编码方式,比如gzip。Servlet能够向支持gzip的浏览器返回经gzip编码的HTML页面。许多情形下这可以减少5到10倍的下载时间; +> * **Accept-Language**:浏览器所希望的语言种类,当服务器能够提供一种以上的语言版本时要用到; +> * **Authorization**:授权信息,通常出现在对服务器发送的WWW-Authenticate头的应答中; +> * **Connection**:表示是否需要持久连接。如果Servlet看到这里的值为“Keep-Alive” ,或者看到请求使用的是HTTP 1.1(HTTP 1.1默认进行持久连接),它就可以利用持久连接的优点,当页面包含多个元素时(例如Applet,图片),显著地减少下载所需要的时间。要实现这一点,Servlet需要在应答中发送一个Content-Length头,最简单的实现方法是:先把内容写入ByteArrayOutputStream,然后在正式写出内容之前计算它的大小; +> * **Content-Length**:表示请求消息正文的长度; +> * **Cookie**:这是最重要的请求头信息之一; +> * **From**:请求发送者的email地址,由一些特殊的Web客户程序使用,浏览器不会用到它; +> * **Host**:初始URL中的主机和端口; +> * **If-Modified-Since**:只有当所请求的内容在指定的日期之后又经过修改才返回它,否则返回304“Not Modified”应答; +> * **Pragma**:指定“no-cache”值表示服务器必须返回一个刷新后的文档,即使它是代理服务器而且已经有了页面的本地拷贝; +> * **Referer**:包含一个URL,用户从该URL代表的页面出发访问当前请求的页面。 +> * **User-Agent**:浏览器类型,如果Servlet返回的内容与浏览器类型有关则该值非常有用; +> * **UA-Pixels,UA-Color,UA-OS,UA-CPU**:由某些版本的IE浏览器所发送的非标准的请求头,表示屏幕大小、颜色深度、操作系统和CPU类型。 + +### 4.响应头(HTTP Response Header) +HTTP响应头是那些描述HTTP响应本身的头,这里并不包含描述HTTP响应中第三部分也就是HTTP信息的头(这部分由实体头负责)。 +比如说定时刷新的Refresh头,当遇到503错误时自动重试的Retry-After头,显示服务器信息的Server头,设置COOKIE的Set-Cookie头,告诉客户端可以部分请求的Accept-Ranges头等。 + +**常见响应头如下:** + +> * **Allow**服务器支持哪些请求方法(如`GET`、`POST`等); +> * **Content-Encoding**文档的编码(Encode)方法。 + 只有在解码之后才可以得到` Content-Type头`指定的内容类型。利用`gzip压缩文档`能够显著地减少HTML文档的下载时间。Java的`GZIPOutputStream`可以很方便地进行`gzip压缩`,但只有`Unix`上的`Netscape`和`Windows`上的`IE 4`、`IE 5`才支持它。因此,`Servlet`应该通过查看`Accept-Encoding头`(即`request.getHeader`("`Accept-Encoding`"))检查浏览器是否支持`gzip`,为支持`gzip`的浏览器返回经`gzip`压缩的HTML页面,为其他浏览器返回普通页面; +> * **Content-Length**表示内容长度。只有当浏览器使用持久HTTP连接时才需要这个数据。如果你想要利用持久连接的优势,可以把输出文档写入`ByteArrayOutputStram`,完成后查看其大小,然后把该值放入`Content-Length头`,最后通过`byteArrayStream.writeTo(response.getOutputStream())`发送内容; +> * **Content-Type** 表示后面的文档属于什么`MIME类`型。`Servlet`默认为`text/plain`,但通常需要显式地指定为`text/html`。由于经常要设置`Content-Type`,因此`HttpServletResponse`提供了一个专用的方法`setContentTyep`。 可在web.xml文件中配置扩展名和MIME类型的对应关系; +> * **Date**当前的GMT时间。你可以用`setDateHeader`来设置这个头以避免转换时间格式 的麻烦; +> * **Server**服务器软件名称及版本。 +> * **Age**响应给客户端的文档可以缓存多长时间 +> * **Public** +> * **Vary** +> * **Set-Cookie** +> * **Set-Cookie2** +> * **Expires**指明应该在什么时候认为文档已经过期,从而不再缓存它。 +> * **Last-Modified**文档的最后改动时间。客户可以通过`If-Modified-Since`请求头提供一个日期,该请求将被视为一个条件`GET`,只有改动时间迟于指定时间的文档才会返回,否则返回一个**304**(`Not Modified`)状态。`Last-Modified`也可用`setDateHeader`方法来设置; +> * **Location**表示客户应当到哪里去提取文档。 + `Location`通常不是直接设置的,而是通过`HttpServletResponse`的`sendRedirect`方法,该方法同时设置状态代码为**302**; +> * **Refresh**表示浏览器应该在多少时间之后刷新文档,以秒计。 + 除了刷新当前文档之外,你还可以通过`setHeader("Refresh", "5; URL=http://host/path")`让浏览器读取指定的页面。注意这种功能通常是通过设置HTML页面HEAD区的``实现,这是因为,自动刷新或重定向对于那些不能使用CGI或`Servle`t的HTML编写者十分重要。但是,对于`Servlet`来说,直接设置`Refresh头`更加方便。注意`Refres`h的意义是“**N秒之后刷新本页面或访问指定页面**”,而不是“**每隔N秒刷新本页面或访问指定页面**”。因此,连续刷新要求每次都发送一个`Refresh头`,而发送**204**状态代码则可以阻止浏览器继续刷新,不管是使用`Refresh头`还是``。注意`Refresh头`不属于**HTTP 1.1正式规范**的一部分,而是一个扩展,但`Netscape`和`IE`都支持它。 + + +## 七、HTTP请求 +HTTP请求由三部分组成,分别是:**请求行**、**消息报头**、**请求正文** +请求行以一个方法符开头,以空格分开,后面跟着请求的URI和协议的版本,格式如下: +``` +Method Request-URI HTTP-Version CRLF +``` +* **Method** 表示请求方法; +* **Request-URI** 是一个统一资源标识符,也就是资源路径; +* **HTTP-Version** 表示HTTP协议版本; +* **CRLF** 表示回车和换行。 + +**请求方法:** +> * **GET** 请求获取Request-URI所标识的资源 +> * **POST** 在Request-URI所标识的资源后附加新的数据,常用于提交表单 +> * **HEAD** 请求获取由Request-URI所标识的资源的响应消息报头 +> * **PUT** 请求服务器存储一个资源,并用Request-URI作为其标识 +> * **DELETE** 请求服务器删除Request-URI所标识的资源 +> * **TRACE** 请求服务器回送收到的请求信息,主要用于测试或诊断 + 可以追踪一次请求中间所经过的代理服务器有哪些 +> * **CONNECT** 保留将来使用 +> * **OPTIONS** 请求查询服务器的性能,或者查询与资源相关的选项和需求 + 可以用来获取服务器端资源支持的方法 + + +## 八、HTTP响应 +在接收和解释请求消息后,服务器返回一个HTTP响应消息。 + +HTTP响应也是由三个部分组成,分别是:**状态行**、**消息报头**、**响应正文** + +**状态行格式如下**: +``` +HTTP-Version Status-Code Reason-Phrase CRLF +``` +> * **HTTP-Version** 表示服务器HTTP协议的版本; +> * **Status-Code** 表示服务器发回的响应状态代码; +> * **Reason-Phrase** 表示状态代码的文本描述。 + +状态代码有**三位数字**组成,第一个数字定义了响应的类别,且有五种可能取值: +> * **1xx**:指示信息--表示请求已接收,继续处理 +> * **2xx**:成功--表示请求已被成功接收、理解、接受 +> * **3xx**:重定向--信息不完整需要进一步补充 +> * **4xx**:客户端错误--请求有语法错误或请求无法实现 +> * **5xx**:服务器端错误--服务器未能实现合法的请求 + +**常见http响应状态码**: +#### 请求收到,继续处理: +> * **100**:客户端必须继续发出请求 +> * **101**:客户端要求服务器根据请求转换HTTP协议版本 + +#### 操作成功收到,分析,接受: +> * **200**:交易成功 +> * **201**:提示知道新文件的URL +> * **202**:接受和处理、但处理未完成 +> * **203**:返回信息不确定或不完整 +> * **204**:请求收到,但返回信息为空 +> * **205**:服务器完成了请求,用户代理必须复位当前已经浏览过的文件 +> * **206**:服务器已经完成了部分用户的GET请求 + +#### 重定向: +> * **300**:请求的资源可在多处得到 +> * **301**:永久重定向,在Location响应首部的值仍为当前URL(隐式重定向) +> * **302**:临时重定向,在Location响应首部的值仍为新的URL(显示重定向) +> * **303**:建议客户端访问其他URL或访问方式 +> * **304**:Not Modified 请求的资源没有改变 可以继续使用缓存 +> * **305**:请求的资源必须从服务器指定的地址得到 +> * **306**:前一版本HTTP中使用的代码,现行版本中不再使用 +> * **307**:声明请求的资源临时性删除 + +#### 客户端错误: +> * **400**:错误请求,如语法错误 +> * **401**:未授权 + **HTTP 401.1** - 未授权,登录失败 + **HTTP 401.2** - 未授权,服务器配置问题导致登录失败 + **HTTP 401.3** - ACL 禁止访问资源 + **HTTP 401.4** - 未授权 授权被筛选器拒绝 + **HTTP 401.5** - 未授权 ISAPI或CGI授权失败 +> * **402**:保留有效ChargeTo头响应 +> * **403**:禁止访问 + **HTTP 403.1** - 禁止访问 禁止可执行访问 + **HTTP 403.2** - 禁止访问 禁止读访问 + **HTTP 403.3** - 禁止访问 禁止写访问 + **HTTP 403.4** - 禁止访问 要求SSL + **HTTP 403.5** - 禁止访问 要求SSL 128 + **HTTP 403.6** - 禁止访问 IP地址被拒绝 + **HTTP 403.7** - 禁止访问 要求客户端证书 + **HTTP 403.8** - 禁止访问 禁止站点访问 + **HTTP 403.9** - 禁止访问 连接的用户过多 + **HTTP 403.10** - 禁止访问 配置无效 + **HTTP 403.11** - 禁止访问 密码更改 + **HTTP 403.12** - 禁止访问 映射器拒绝访问 + **HTTP 403.13** - 禁止访问 客户端证书已被吊销 + **HTTP 403.15** - 禁止访问 客户端访问许可过多 + **HTTP 403.16** - 禁止访问 客户端证书不可信或者无效 + **HTTP 403.17** - 禁止访问 客户端证书已经到期或者尚未生效 +> * **404**:没有发现文件、查询或URL +> * **405**:用户在Request-Line字段定义的方法不允许 +> * **406**:根据用户发送的Accept拖,请求资源不可访问 +> * **407**:类似401,用户必须首先在代理服务器上得到授权 +> * **408**:客户端没有在用户指定的饿时间内完成请求 +> * **409**:对当前资源状态,请求不能完成 +> * **410**:服务器上不再有此资源且无进一步的参考地址 +> * **411**:服务器拒绝用户定义的Content-Length属性请求 +> * **412**:一个或多个请求头字段在当前请求中错误 +> * **413**:请求的资源大于服务器允许的大小 +> * **414**:请求的资源URL长于服务器允许的长度 +> * **415**:请求资源不支持请求项目格式 +> * **416**:请求中包含Range请求头字段,在当前请求资源范围内没有range指示值, 请求也不包含If-Range请求头字段 +> * **417** 服务器不满足请求Expect头字段指定的期望值,如果是代理服务器,可能是下一级服务器不能满足请求长 + +#### 服务器端错误: +> * **500**部服务器错误 + **HTTP 500.100** - 内部服务器错误 + **HTTP 500-11** 服务器关闭 + **HTTP 500-12** 应用程序重新启动 + **HTTP 500-13** - 服务器太忙 + **HTTP 500-14** - 应用程序无效 + **HTTP 500-15** - 不允许请求 +> * **501**实现 +> * **502**关错误 +> * **503**务不可用 +> * **504**关超时 + +## 九、HTTP状态保持 + HTTP 协议本身是无状态的,这与HTTP协议本来的目的是相符的,客户端只需要简单的向服务器请求下载某些文件,无论是客户端还是服务器都没有必要纪录彼此过去的行为,每一次请求之间都是独立的,好比一个顾客和一个自动售货机或者一个普通的(非会员制)大卖场之间的关系一样。 + + 然而聪明(或者贪心?)的人们很快发现如果能够提供一些按需生成的动态信息会使web变得更加有用,就像给有线电视加上点播功能一样。这种需求一方面迫使HTML逐步添加了表单、脚本、DOM等客户端行为,另一方面在服务器端则出现了CGI规范以响应客户端的动态请求,作为传输载体的HTTP协议也添加了文件上载、 cookie这些特性。其中cookie的作用就是为了解决HTTP协议无状态的缺陷所作出的努力。至于后来出现的session机制则是又一种在客户端与服务器之间保持状态的解决方案。 + + +Cookie和Session都为了用来保存状态信息,都是保存客户端状态的机制,它们都是为了解决HTTP无状态的问题而所做的努力。 + +Session可以用Cookie来实现,也可以用URL回写的机制来实现。用Cookie来实现的Session可以认为是对Cookie更高级的应用。 + + +### Cookie和Session区别 + +* 1. Cookie将状态保存在客户端,Session将状态保存在服务器端; + +* 2. Cookies是服务器在本地机器上存储的小段文本并随每一个请求发送至同一个服务器。Cookie最早在RFC2109中实现,后续RFC2965做了增强。网络服务器用HTTP头向客户端发送cookies,在客户终端,浏览器解析这些cookies并将它们保存为一个本地文件,它会自动将同一服务器的任何请求缚上这些cookies。Session并没有在HTTP的协议中定义; + +* 3. Session是针对每一个用户的,变量的值保存在服务器上,用一个sessionID来区分是哪个用户session变量,这个值是通过用户的浏览器在访问的时候返回给服务器,当客户禁用cookie时,这个值也可能设置为由get来返回给服务器; + +* 4. 就安全性来说:当你访问一个使用session 的站点,同时在自己机子上建立一个cookie,建议在服务器端的SESSION机制更安全些.因为它不会任意读取客户存储的信息。 + + +### Session机制 + +Session机制是一种服务器端的机制,服务器使用一种类似于散列表的结构(也可能就是使用散列表)来保存信息。 + +当程序需要为某个客户端的请求创建一个session的时候,服务器首先检查这个客户端的请求里是否已包含了一个session标识 - 称为 session id,如果已包含一个session id则说明以前已经为此客户端创建过session,服务器就按照session id把这个 session检索出来使用(如果检索不到,可能会新建一个),如果客户端请求不包含session id,则为此客户端创建一个session并且生成一个与此session相关联的session id,session id的值应该是一个既不会重复,又不容易被找到规律以仿造的字符串,这个 session id将被在本次响应中返回给客户端保存。 + + + +### Session的实现方式 +**使用Cookie来实现:** +服务器给每个Session分配一个唯一的JSESSIONID,并通过Cookie发送给客户端。 + +当客户端发起新的请求的时候,将在Cookie头中携带这个JSESSIONID。这样服务器能够找到这个客户端对应的Session。 + +流程如下图所示: +![session](http://s3.51cto.com/wyfs02/M00/38/13/wKiom1OyXoHjWI2aAABsbAEnTIs272.jpg) +**使用URL回显来实现** +URL回写是指服务器在发送给浏览器页面的所有链接中都携带JSESSIONID的参数,这样客户端点击任何一个链接都会把JSESSIONID带会服务器。 + +如果直接在浏览器输入服务端资源的url来请求该资源,那么Session是匹配不到的。 + +Tomcat对Session的实现,是一开始同时使用Cookie和URL回写机制,如果发现客户端支持Cookie,就继续使用Cookie,停止使用URL回写。如果发现Cookie被禁用,就一直使用URL回写。jsp开发处理到Session的时候,对页面中的链接记得使用response.encodeURL()。 + +### 与Cookie相关的HTTP扩展头 +* 1. **Cookie**:客户端将服务器设置的Cookie返回到服务器; +* 2. **Set-Cookie**:服务器向客户端设置Cookie; +* 3. **Cookie2 (RFC2965))**:客户端指示服务器支持Cookie的版本; +* 4. **Set-Cookie2 (RFC2965)**:服务器向客户端设置Cookie。 + +### Cookie的流程 +服务器在响应消息中用Set-Cookie头将Cookie的内容回送给客户端,客户端在新的请求中将相同的内容携带在Cookie头中发送给服务器。从而实现会话的保持。 + + 流程如下图所示: +![cookie](http://s3.51cto.com/wyfs02/M00/38/13/wKioL1OyX1-jc2HfAABcl6JnHeo234.jpg) + +## 十、Web缓存 +### 什么是Web缓存 + WEB缓存(cache)位于Web服务器和客户端之间。 + + 缓存会根据请求保存输出内容的副本,例如html页面,图片,文件,当下一个请求来到的时候:如果是相同的URL,缓存直接使用副本响应访问请求,而不是向源服务器再次发送请求。 + + HTTP协议定义了相关的消息头来使WEB缓存尽可能好的工作。 + + + +### Web缓存的优点 +* 1. **减少相应延迟**:因为请求从缓存服务器(离客户端更近)而不是源服务器被相应,这个过程耗时更少,让web服务器看上去相应更快。 + +* 2. **减少网络带宽消耗**:当副本被重用时会减低客户端的带宽消耗;客户可以节省带宽费用,控制带宽的需求的增长并更易于管理。 + + +### 与缓存相关的HTTP消息头 +> * **Expires**:指示响应内容过期的时间,格林威治时间GMT +> * **Cache-Control**:更细致的控制缓存的内容 +> * **Last-Modified**:响应中资源最后一次修改的时间 +> * **ETag**:响应中资源的校验值,在服务器上某个时段是唯一标识的。 +> * **Date**:服务器的时间 +> * **If-Modified-Since**:客户端存取的该资源最后一次修改的时间,同Last-Modified。 +> * **If-None-Match**:客户端存取的该资源的检验值,同ETag。 + +### 客户端缓存生效的常见流程 +服务器收到请求时,会在200 OK中回送该资源的Last-Modified和ETag头,客户端将该资源保存在cache中,并记录这两个属性。当客户端需要发送相同的请求时,会在请求中携带If-Modified-Since和If-None-Match两个头。两个头的值分别是响应中Last-Modified和ETag头的值。服务器通过这两个头判断本地资源未发生变化,客户端不需要重新下载,返回304响应。 + +常见流程如下图所示: +![缓存](http://s3.51cto.com/wyfs02/M02/38/13/wKioL1OyYpTBYuovAABlmj5y42w797.jpg) +### Web缓存机制 +HTTP/1.1中缓存的目的是为了在很多情况下减少发送请求,同时在许多情况下可以不需要发送完整响应。前者减少了网络回路的数量;HTTP利用一个“过期(expiration)”机制来为此目的。后者减少了网络应用的带宽;HTTP用“验证(validation)”机制来为此目的。 + +**HTTP定义了3种缓存机制**: +> * 1. **Freshness**:允许一个回应消息可以在源服务器不被重新检查,并且可以由服务器和客户端来控制。例如,Expires回应头给了一个文档不可用的时间。Cache-Control中的max-age标识指明了缓存的最长时间; +> * 2. **Validation**:用来检查以一个缓存的回应是否仍然可用。例如,如果一个回应有一个Last-Modified回应头,缓存能够使用If-Modified-Since来判断是否已改变,以便判断根据情况发送请求; +> * 3. **Invalidation**: 在另一个请求通过缓存的时候,常常有一个副作用。例如,如果一个URL关联到一个缓存回应,但是其后跟着POST、PUT和DELETE的请求的话,缓存就会过期。 + +## 十一、HTTP代理服务器 +代理服务器英文全称是Proxy Server,其功能就是代理网络用户去取得网络信息。形象的说:它是网络信息的中转站。 + +代理服务器是介于浏览器和Web服务器之间的一台服务器,有了它之后,浏览器不是直接到Web服务器去取回网页而是向代理服务器发出请求,Request信号会先送到代理服务器,由代理服务器来取回浏览器所需要的信息并传送给你的浏览器。 + +而且,大部分代理服务器都具有缓冲的功能,就好象一个大的Cache,它有很大的存储空间,它不断将新取得数据储存到它本机的存储器上,如果浏览器所请求的数据在它本机的存储器上已经存在而且是最新的,那么它就不重新从Web服务器取数据,而直接将存储器上的数据传送给用户的浏览器,这样就能显著提高浏览速度和效率。 + +更重要的是:Proxy Server(代理服务器)是Internet链路级网关所提供的一种重要的安全功能,它的工作主要在开放系统互联(OSI)模型的对话层。 + +### HTTP代理服务器的主要功能 + +* 1. 突破自身IP访问限制,访问国外站点。如:教育网、169网等网络用户可以通过代理访问国外网站; + +* 2. 访问一些单位或团体内部资源,如某大学FTP(前提是该代理地址在该资源的允许访问范围之内),使用教育网内地址段免费代理服务器,就可以用于对教育 网开放的各类FTP下载上传,以及各类资料查询共享等服务; + +* 3. 突破中国电信的IP封锁:中国电信用户有很多网站是被限制访问的,这种限制是人为的,不同Serve对地址的封锁是不同的。所以不能访问时可以换一个国 外的代理服务器试试; + +* 4. 提高访问速度:通常代理服务器都设置一个较大的硬盘缓冲区,当有外界的信息通过时,同时也将其保存到缓冲区中,当其他用户再访问相同的信息时, 则直接由缓冲区中取出信息,传给用户,以提高访问速度; + +* 5. 隐藏真实IP:上网者也可以通过这种方法隐藏自己的IP,免受攻击。 + +HTTP代理图示: +![代理](http://s3.51cto.com/wyfs02/M01/38/14/wKioL1OyZMLCGz4sAABIrUX08kc751.jpg) +对于客户端浏览器而言,http代理服务器相当于服务器。 +而对于Web服务器而言,http代理服务器又担当了客户端的角色。 + +## 十二、虚拟主机的实现 +### 什么是虚拟主机 + 虚拟主机:是在网络服务器上划分出一定的磁盘空间供用户放置站点、应用组件等,提供必要的站点功能与数据存放、传输功能。 + + 所谓虚拟主机,也叫“网站空间”就是把一台运行在互联网上的服务器划分成多个“虚拟”的服务器,每一个虚拟主机都具有独立的域名和完整的Internet服务器(支持WWW、FTP、E-mail等)功能。一台服务器上的不同虚拟主机是各自独立的,并由用户自行管理。但一台服务器主机只能够支持一定数量的虚拟主机,当超过这个数量时,用户将会感到性能急剧下降。 + + + +### 虚拟主机的实现原理 + 虚拟主机是用同一个WEB服务器,为不同域名网站提供服务的技术。Apache、Tomcat等均可通过配置实现这个功能。 + 相关的HTTP消息头:Host。 + 例如:Host: www.baidu.com + 客户端发送HTTP请求的时候,会携带Host头,Host头记录的是客户端输入的域名。这样服务器可以根据Host头确认客户要访问的是哪一个域名。 + + +节选自 [Apache之HTTP协议](http://blog.51cto.com/shjia/1432670) \ No newline at end of file diff --git "a/Cute-Article/article/52-\345\211\215\347\253\257\346\250\241\345\235\227\345\214\226\357\274\210CommonJs,AMD\345\222\214CMD\357\274\211.md" "b/Cute-Article/article/52-\345\211\215\347\253\257\346\250\241\345\235\227\345\214\226\357\274\210CommonJs,AMD\345\222\214CMD\357\274\211.md" new file mode 100644 index 00000000..445bc076 --- /dev/null +++ "b/Cute-Article/article/52-\345\211\215\347\253\257\346\250\241\345\235\227\345\214\226\357\274\210CommonJs,AMD\345\222\214CMD\357\274\211.md" @@ -0,0 +1,273 @@ +前端模块规范有三种:**CommonJs**,**AMD**和**CMD**。 + +* CommonJs用在**服务器端**,AMD和CMD用在**浏览器环境**。 + +* AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。 +* CMD 是 SeaJS 在推广过程中对模块定义的规范化产出。 + +* AMD:**提前执行**(异步加载:依赖先执行)+**延迟执行**。 +* CMD:**延迟执行**(运行到需加载,根据顺序执行)。 + +## 模块 +* **函数写法** +```js +function f1(){ + //... +} +function f2(){ + //... +} +``` + +模块就是实现特定功能的文件,把几个函数放在一个文件里就构成了一个模块。需要的时候加载这个文件,调用其中的函数即可。 +但这样做会污染全局变量,无法保证不与其他模块发生变量名冲突,而且模块成员之间没什么关系。 + +* **对象写法** +```js +var module = { +  star : 0, +  f1 : function (){ +    //... +  }, +  f2 : function (){ +    //... +  } +}; +module.f1(); +module.star = 1; +``` + +模块写成一个对象,模块成员都封装在对象里,通过调用对象属性,访问使用模块成员。但同时也暴露了模块成员,外部可以修改模块内部状态。 + +* **立即执行函数** +```js +var module = (function(){ +  var star = 0; +  var f1 = function (){ +    console.log('ok'); +  }; +  var f2 = function (){ +    //... +  }; + return { + f1:f1, + f2:f2 + }; +})(); +module.f1(); //ok +console.log(module.star) //undefined +``` + +外部无法访问内部私有变量 + +## CommonJs +CommonJS是服务器端模块的规范,由Node推广使用。由于服务端编程的复杂性,如果没有模块很难与操作系统及其他应用程序互动。使用方法如下: +```js +// math.js +exports.add = function() { + var sum = 0, i = 0, args = arguments, l = args.length; + while (i < l) { + sum += args[i++]; + } + return sum; +}; + +increment.js +var add = require('math').add; +exports.increment = function(val) { + return add(val, 1); +}; + +index.js +var increment = require('increment').increment; +var a = increment(1); //2 +``` +根据**CommonJS**规范: + +* 一个单独的文件就是一个模块。每一个模块都是一个单独的作用域,也就是说,在该模块内部定义的变量,无法被其他模块读取,除非定义为`global对象`的属性。 + +* 输出模块变量的最好方法是使用`module.exports`对象。 + +* 加载模块使用`require`方法,该方法读取一个文件并执行,返回文件内部的`module.exports`对象 + +仔细看上面的代码,您会注意到 `require` 是同步的。模块系统需要同步读取模块文件内容,并编译执行以得到模块接口。 +然而, 这在浏览器端问题多多。 + +浏览器端,加载 JavaScript 最佳、最容易的方式是在 `document` 中插入` +``` + +我们在html页面的input元素中设置了a属性,但是在`property`中却是访问不到的;相反我们没有在html页面中设置的`title`,访问它却没有反映`undefined`! + +这是怎么回事? + +因为所有的HTML元素都由`HTMLElement`类型表示,`HTMLElement`类型直接继承自`Element`并添加了一些属性,每个HTML元素都有下面的这5个标准特性:`id`,`title`,`lang`,`dir`,`className`(在DOM中以`property`方式操作这几个特性会同步到html标签中)。 + +所以即使在html中没有指定`id`、`title`等,也会默认赋予一个空串,通过property属性(点操作符)可以访问。而除此之外在html中设置的其他属性是不能通过`Property`访问到的(`attribute`特有的属性)。 + +如果把DOM元素看成是一个普通的Object对象,那么property就是一个以名值对(`name='value'`)的形式存放在Object中的属性。要添加和删除`property`也简单多了,和普通的对象没啥分别: +```js +var a = document.getElementById('txt'); +a.age = 10; +console.log(a.age); // 10 +delete a.age; +console.log(a.age); // undefined +``` +除了`id`、`title`等5个属性(`property`)外(每个element元素都有),个别的元素还有特别的属性,比如input元素有`name`,a元素有`href`等等。 + +## 3、Attribute vs Property + +既然说有些属性既能通过`attribute`访问修改,也能通过`property`,那么有什么值得注意的地方呢? + +之所以`attribute`和`property`容易混倄在一起的原因是,很多`attribute`节点还有一个相对应的`property`属性,比如div元素的`id`和`class`既是`attribute`,也有对应的`property`(id和className),不管使用哪种方法都可以**访问和修改**,如果在TAG对这些属性进行赋值,那么这些值就会作为初始值赋给DOM的同名property。 + + +* **input元素的value** + +input元素的`value`属性是一大坑爹处,看下面代码: + +```js +var a = document.getElementById('txt'); +a.setAttribute('value', 'test'); +console.log(a.value); // test + +a.value = 'change'; +console.log(a.getAttribute('value')); // test +``` + +用**点操作符**改变`value`值,并不会更新`attribute`的`value`值;而相反用`attribute`更新`value`值,却会反映到`property`上...坑吧,谁规定的! + +* **表单元素** + +DOM元素一些默认常见的`attribute`节点都有与之对应的`property`属性,比较特殊的是一些值为`Boolean类型`的`property`,如一些表单元素。对于这些特殊的`attribute`节点,只要存在该节点,对应的`property`的值就为`true`,如: +```html + + +``` +disabled类似。 + +* **href** + +两者对于`href`的获取也有不同之处,`attribute`取到的是实际设置的值(相对路径),而`property`取得的是绝对路径: + +```html + + +``` + +## 4、总结 +`Attribute`属性在`html`上设置,会反应在`html`代码上,两者**同步**;而`Property`属性则可以看做是DOM对象的键值对,用**点操作符**对它们进行操作。 + +实际编程中,基本上的DOM操作都是使用`property`的点操作符。 + +只有两种情况不得不使用`attribute`: + +* 1. 自定义HTML Attribute,因为它不能同步到DOM property上 + +* 2. 访问内置的HTML标签的`Attribute`,这些`attribute`不能从`property`上同步过来,比如`input标签`的`value`值(可以用来检验input值是否变化) + + +## 5、参考 +* [JavaScript中的property和attribute的区别](https://www.jianshu.com/p/rRssiL) + +* [返本求源——DOM元素的特性与属性](http://www.cnblogs.com/dojo-lzz/p/4781563.html) \ No newline at end of file diff --git "a/Cute-Article/article/56-js\344\270\255\345\216\237\345\236\213\347\273\247\346\211\277\345\216\237\347\220\206.md" "b/Cute-Article/article/56-js\344\270\255\345\216\237\345\236\213\347\273\247\346\211\277\345\216\237\347\220\206.md" new file mode 100644 index 00000000..39c6924e --- /dev/null +++ "b/Cute-Article/article/56-js\344\270\255\345\216\237\345\236\213\347\273\247\346\211\277\345\216\237\347\220\206.md" @@ -0,0 +1,86 @@ +在JavaScript当中,对象A如果要继承对象B的属性和方法,那么只要将对象B放到对象A的原型链上即可。而某个对象的原型链,就是由该对象开始,通过`__proto__`属性连接起来的一串对象。`__proto__`属性是JavaScript对象中的内部属性,任何JavaScript对象,包括我们自己构建的对象,JavaScript的`built-in`对象,任何函数(在JavaScript当中,函数也是对象)都具有这个属性。如下图就是一个原型链的例子: +![图片1](http://p3nqtyvgo.bkt.clouddn.com/20180927_01.png) + +上图中,A,B,C分别代表3个对象,**蓝色箭头**串接起来的所有对象就构成了**对象C的原型链**,其中C的`_proto__`属性指向B,B的`__proto__`属性指向A,A的`__proto__`属性可能指向更高层的对象,也可能指向`null`(表示**A不继承任何对象的属性和方法**)。 + +如果我们引用了C的某个属性或者方法,那么JavaScript就会顺着C的原型链进行查找,即首先查找对象C本身,看所引用的属性名或者方法名是否存在,如果存在就停止查找直接返回,如果不存在,就通过C的`__proto__`属性找到原型链中的B对象,继续在B对象中查找,如果B对象中找到所引用的属性名或者方法名,那么就停止查找直接返回,如果B对象中也不存在,就通过对象B的`__proto__`属性找到原型链中的A对象,继续重复上述查找过程,直到找到所引用的属性或者方法为止(同时也可能查找完对象C的整个原型链也没有找到所引用的属性或者方法,那么该属性或者方法就是`undefined`的)。 + +因此,只要能够成功的为某一个对象构造出我们需要的原型链,那么就能让该对象继承我们想要它继承的方法或者属性。而想要成功构造对象的原型链,就还必须理解`prototype`属性,JavaScript当中已经存在的原型链,以及当我们创建对象时,原型链被构造的过程。 + + +### prototype属性 +prototype属性存在于JavaScript的任何函数当中,这个属性指向的对象就是所谓的原型对象,在构造原型链时需要原型对象。 + + +### JavaScript当中已经存在的原型链 + +在JavaScript当中存在`Object`,`Function`,`Array`,`String`,`Boolean`,`Number`,`Date`,`Error`,`RegExp`这9个**built-in函数**,一个`built-in`的`Math对象`,通过这上述9个`built-in函数`我们可以创建相应的对象,同时,这9个`built-in函数`的`prototype`属性所指向的原型对象也是`built-in`的。下面的图示解释了这几个函数以及各自`prototype`属性所指向的原型对象之间的关系。 +![图片2](http://p3nqtyvgo.bkt.clouddn.com/20180927_02.png) + +(如果此图看不清,可点击 [此处下载](http://p3nqtyvgo.bkt.clouddn.com/20180927_02.png)) + +  上面的图示中,**黄色方框**代表`built-in函数对象`,**深绿色方框**代表`built-in函数`的`prototype属性`指向的原型对象,名字都叫`xx prototype object`,**浅绿色方框**(即**Math对象**)代表**普通对象**,蓝色箭头连接非built-in函数对象(无论是普通对象如Math,还是原型对象)的`__proto__`属性,而土黄色箭头连接函数对象的`__proto__`属性。 + +  通过上图可以发现,所有**built-in函数对象**的原型链最终都指向`Function prototype object`,所有**非函数对象的原型链**最终都指向`Object prototype object`,并且`Function prototype object`的`__proto__`属性也指向`Object prototype object`,`Object prototype object`的`__proto__`属性指向为`null`。 + +因此,`Object prototype object`是所有原型链的顶端,通过原型链查找规则可知,所有`built-in`函数对象同时继承了`Object prototype object`和`Function prototype object`上的属性和方法,而所有非built-in函数对象只继承了`Object prototype object`上的方法。`Function prototype object`包含了所有函数共享的属性和方法,而`Object prototype object`包含了所有对象都共享额属性和方法。 + +对于上图中原型对象包含的constructor属性,下文当中有解释。 + + + +### 创建对象时,原型链的构造过程 + +在JavaScript当中创建对象有2中方式,一种是通过定义函数使用**new方法**来构造,另一种是使用**对象字面量**的方式,即: +```js +var obj = { + name: "Jim Green" +}; +``` +使用这两种方式创建对象时,对象的原型链构造过程有所不同。 + +* **1. 使用函数的方式构造对象** + +使用函数的方式构造对象分为两步:首先需要**定义一个函数**作为构造函数,然后使用**new方法构造对象**。接下来就来看一下这两个步骤会发生什么。 + +假设我们定义了一个函数名为`F`,此时JavaScript会为我们做两件事,第一:根据我们定义的函数创建一个函数对象,第二,设置这个函数的`__proto__`属性和`prototype`属性。其中`__proto__`属性指向`built-in`的`Function prototype object`,而`prototype`属性指向一个为函数对象F新创建的原型对象,这个新创建的原型对象通过调用`new Object()`构造出来,并且为这个新创建的对象添加`constructor`属性,该属性指向函数对象`F`。最后的结果如下图所示: + +![图3](http://p3nqtyvgo.bkt.clouddn.com/20180927_03.png) + +上图中为了方便,没有画出`Function prototype object`和`Object prototype object`的`constructor`属性。而`F` `prototype object`的`__proto__`属性为何指向`Object prototype object`,下文介绍**new操作符**时有解释。 + +当我们使用**new方法**调用F函数的时候,JavaScript也会为我们做两件事,第一,分配内存作为新创建的对象,第二,将新创建的对象的__proto__属性指向函数F的原型对象,结果如下图: +![图4](http://p3nqtyvgo.bkt.clouddn.com/20180927_04.png) + +上图中,`obj`就是调用**new方法**通过`函数F`创建出来的对象,我们可以看到对象obj的原型链包含了`函数F`的原型对象,以及`Object prototype object`,这样,对象`obj`通过原型链查找规则,就能继承`函数F`的原型对象,以及`Object prototype object`上面定义的属性和方法了。并且如果我们想知道一个对象是由哪个方法构建的,只需要访问这个对象的`constructor`属性即可,上例中,只要我们访问`obj.constructor`,那么就知道obj是由`函数F`创建的。同时,由于`F` `prototype object`上文中介绍是由`new Object`函数创建的,根据此处介绍,F `prototype object`的`__proto__`属性应该指向`Object函数`的原型对象,即`Object prototype object`。 + +* **2. 使用对象字面量定义对象** + +当使用对象字面量创建对象时,JavaScript会为我们做两件事: + +> 1 分配内存作为新创建的对象。 +> 2 将新创建对象的`__proto__`属性指向`Object prototype object`。 + +结果如下图所示: +![图5](http://p3nqtyvgo.bkt.clouddn.com/20180927_05.png) + +上图为了简化,同样没有画出`Object prototype object`的`constructor`属性 + +### 继承 + +理解了上面所讲的原理之后,假设目前有一个对象`A`(这个对象可以是任意的,包括JavaScript built-in的对象,任何函数对象,任何原型对象,以及我们自己new出来的对象),现在想创建一个对象`obj`,让`obj`继承`A`的属性和方法。 + +通过上面的介绍,我们知道创建对象有两种方式,但是使用对象字面量创建的对象其原型链总是只包含两个对象,一个是其自己,一个是`Object prototype object`,根本不可能包含对象`A`,无法达到让对象`obj`继承对象`A`属性和方法的效果。因此,只能使用函数的方式创建对象,让对象A包含在新创建对象obj的原型链中即可。 + +根据上面的讲解,如果是用函数的方式创建对象,那么在调用`new`方法时,新创建对象的`__proto__`属性会指向函数的原型对象。因此,只要在调用函数之前,将函数的原型对象换成`A`,然后再调用`new`方法,就可以将对象A包含在新创建的对象`obj`的原型链中,这样通过原型链查找规则,`obj`就继承了A的属性和方法。假设用来创建对象`obj`的函数为`B`,则相关代码为: +```js +B.prototype = A; +B.prototype.constructor = B; +var obj = new B(传入的参数) +``` +上面代码中的`B.prototype.constructor = B`,是因为对象A中可能没有`constructor`属性,或者`constructor`属性不指向`B`,而为了方便通过访问任何由`B`函数创建的对象的`constructor`属性,就可以正确的知道该对象是使用函数`B`构造出来的。相关图示如下图: + +![图6](http://p3nqtyvgo.bkt.clouddn.com/20180927_06.png) + + +上图中虚线框所包围的`B prototype object`就是定义函数`B`时,JavaScript为函数B生成的原型对象,但是该对象被我们用代码替换成了对象A。由于这个被替换的`B prototype object`没有其他地方再用到,因此会被回收掉。 \ No newline at end of file diff --git "a/Cute-Article/article/57-ES5ES6\346\255\243\345\210\231\350\241\250\350\276\276\345\274\217\346\200\273\347\273\223.md" "b/Cute-Article/article/57-ES5ES6\346\255\243\345\210\231\350\241\250\350\276\276\345\274\217\346\200\273\347\273\223.md" new file mode 100644 index 00000000..3d40f1e8 --- /dev/null +++ "b/Cute-Article/article/57-ES5ES6\346\255\243\345\210\231\350\241\250\350\276\276\345\274\217\346\200\273\347\273\223.md" @@ -0,0 +1,363 @@ +## 1. +**ʽ**regular expressionһֱıģʽַṹķ + +ʽ**ַʽ** + +һʹ**бܱʾʼͽ** +```js +var regex = /xyz/ +``` + +һʹRegExp캯 +```js +var regex = new RegExp('xyz'); +``` + +ǵҪǣһַʱͻ½ʽڶַʱ½ʽǰߵЧʽϸߡңǰ߱ȽϱֱۣʵӦУ϶ʽ + +## 2.ʵ +* **i** :ԴСд + +* **m** :ģʽ + +* **g** :ȫ + +## 3.ʵ +### 3.1 RegExp.prototype.test() +ʵ`test`һֵʾǰģʽ**Ƿƥ**ַ +```js +/С/.test('С ѧϰִ') // true +``` +`reg.exec(str)` ƥ飬ƥ򷵻`null`ÿִһ`exec`ƥһ + +### 3.2 RegExp.prototype.exec() +* **reg.exec(str)** ƥ飬ƥ򷵻`null`ÿִһ`exec`ƥһ +```js +var s = '_x_x';var r1 = /x/;var r2 = /y/; + +r1.exec(s) // ["x"]r2.exec(s) // null +``` +* ʽ()Ϊƥ䣬ؽУһƥÿƥĽ +```js +var s = '_x_x'; +var r = /_(x)/; + +r.exec(s) // ["_x", "x"] +``` +`exec`ķ黹ԣ +**input**ԭַ + +**index**ģʽƥɹĿʼλã0ʼ +```js +var r = /a(b+)a/; +var arr = r.exec('_abbba_aba_'); + +arr // ["abbba", "bbb"] + +arr.index // 1arr.input // "_abbba_aba_" +``` +* ʽgѡȫԶʹ exec´εƥϴεĽʼ +```js +var reg = /a/g;var str = 'abc_abc_abc'var r1 = reg.exec(str); +r1 // ["a"]r1.index // 0reg.lastIndex // 1var r2 = reg.exec(str); +r2 // ["a"]r2.index // 4reg.lastIndex // 5var r3 = reg.exec(str); +r3 // ["a"]r3.index // 8reg.lastIndex // 9var r4 = reg.exec(str); +r4 // nullreg.lastIndex // 0 +``` + +## 4.ַʵ +### 4.1 str.match(reg) + `reg.exec`ƣǣʹ`g`ѡstr.match`һԷн +```js +var s = 'abba';var r = /a/g; + +s.match(r) // ["a", "a"]r.exec(s) // ["a"] +``` +### 4.2 str.search(reg) +ƥɹĵһλ,ûκƥ䣬򷵻-1 +```js +'_x_x'.search(/x/)// 1 +``` +### 4.3 str.replace(reg,newstr) +õһ`reg`ȥƥ䣬õڶ`newstr` ȥ滻ʽ`g`η滻һƥɹֵ滻ƥɹֵ +```js +'aaa'.replace('a', 'b')// "baa" +'aaa'.replace(/a/, 'b') // "baa" +'aaa'.replace(/a/g, 'b') // "bbb" +``` +### 4.4 str.split(reg[,maxLength]) +ƥģʽиڶƷؽ + +## 5. ƥ +### 5.1 ַԪַ +󲿷ַʽУĺ壬`/a/`ƥ`a``/b/`ƥ`b`ʽ֮Уijַֻʾĺ壨ǰabôǾͽ**ַ**literal characters +ַ⣬һַ⺬壬˼ǽ**Ԫַ**metacharactersҪ¼ + +* (1) ַ`.` + +ַ.ƥسr(n) зָu2028Ͷηָu2029ַ +```js +/c.t/ +``` +Уc.tƥct֮һַֻҪַͬһУcatc2tc-tȵȣDzƥcoot + +* (2)λַ + +**^** ʾַĿʼλ + +**$** ʾַĽλ +```js +// testڿʼλ/ +^test/.test('test123') // true + +// testڽλ +/test$/.test('new test') // true + +// ӿʼλõλֻ +test/^test$/.test('test') // true + +/^test$/.test('test test') // false +``` + +* (3)ѡ| + +߷ţ`|`ʽбʾ`ϵ`ORcat|dogʾƥcatdog +```js +/11|22/.test('911') // true +``` +Уʽָƥ1122 + +### 5.2 ת +ʽЩ⺬ԪַҪƥDZҪǰҪϷбܡҪƥ+Ҫд+ +```js +/1+1/.test('1+1')// false/1\+1/.test('1+1')// true +``` +ʽУҪбתģһ12ַ`^.[$()|*+?{`͡ҪرעǣʹRegExpתҪʹбܣΪַڲתһΡ +```js +(new RegExp('1\+1')).test('1+1')// false +(new RegExp('1\\+1')).test('1+1')// true +``` +### 5.3 ַ +ַࣨclassʾһϵַɹѡֻҪƥһͿˡпɹѡַڷڣ[xyz] ʾxyz֮ѡһƥ䡣 +```js +/[abc]/.test('hello world') // false +/[abc]/.test('apple') // true +``` +ַַ⺬塣 + +* 1ַ^ +ڵĵһַ[^xyz]ʾxyz֮ⶼƥ䣺 +```js +/[^abc]/.test('hello world') // true +/[^abc]/.test('bbc') // false +``` +ûַֻ[^]ͱʾƥһַаз֮£ΪԪַ.Dzзġ +```js +var s = 'Please yes\nmake my day!'; +s.match(/yes.*day/) // nulls.match(/yes[^]*day/) // [ 'yes\nmake my day'] +``` +УַsһзŲзԵһʽƥʧܣڶʽ[^]һַƥɹ + +* 2ַ- + +ijЩ£еַַ`-`ṩдʽʾַΧ磬[abc]д[`a-c`][0123456789]д[0-9]ͬ[A-Z]ʾ26дĸ +```js +/a-z/.test('b') // false +/[a-z]/.test('b') // true +``` +¶ǺϷַдʽ +```js +[0-9.,] +[0-9a-fA-F] +[a-zA-Z0-9-] +[1-31] +``` +һַ[1-31]131ֻ13 + +⣬Ҫʹַ趨һܴķΧܿѡַ֮͵Ӿ[A-z]ѡдӴдAСдz֮52ĸ ASCII ֮УдĸСдĸ֮仹ַͻ֮Ľ +```js +/[A-z]/.test('\\') // true +``` +Уڷбܣ''ASCIIڴдĸСдĸ֮䣬ᱻѡС + +### 5.4 Ԥģʽ +ԤģʽָijЩģʽļдʽ + +**d** ƥ0-9֮һ֣൱[0-9] + +**D** ƥ0-9ַ൱[^0-9] + +**w** ƥĸֺ»ߣ൱[A-Za-z0-9_] + +**W** ĸֺ»ַ൱[^A-Za-z0-9_] + +**s** ƥո񣨰зƱոȣ[ \t\r\n\v\f] + +**S** ƥǿոַ൱[^ \t\r\n\v\f] + +**b** ƥʵı߽硣 + +**B** ƥǴʱ߽磬ڴʵڲ +```js +// \s +/\s\w*/.exec('hello world') // [" world"] + +// \b + +/\bworld/.test('hello world') // true +/\bworld/.test('hello-world') // true +/\bworld/.test('helloworld') // false +// \B +/\Bworld/.test('hello-world') // false +/\Bworld/.test('helloworld') // true +``` +ͨʽзnͻֹͣƥ䡣 +```js +var html = "Hello\nworld!"; + +/.*/.exec(html)[0] +// "Hello" +``` +Уַhtmlһзַ`.`ƥ任зƥܲԭ⡣ʱʹsַ࣬ܰз +```js +var html = "Hello\nworld!"; + +/[\S\s]*/.exec(html)[0] +// "Hello\nworld!" +``` +У[Ss]ָһַ + +### 5.5 ظ +ģʽľȷƥʹôţ`{}`ʾ`{n`}ʾǡظnΣ`{n,}`ʾظnΣ`{n,m}`ʾظnΣmΡ +```js +/lo{2}k/.test('look') // true +/lo{2,5}k/.test('looook') // true +``` +Уһģʽָo2Σڶģʽָo2ε5֮䡣 + +### 5.6 ʷ +`*. ?` ʺűʾijģʽ0λ1Σͬ{0, 1} +`*. *` Ǻűʾijģʽ0λΣͬ{0,} +`*. +` Ӻűʾijģʽ1λΣͬ{1,} + +### 5.7 ̰ģʽ +һСڵʷĬ¶ƥ䣬ƥֱһַƥΪֹⱻΪ̰ģʽ +```js +var s = 'aaa'; +s.match(/a+/) // ["aaa"] +``` +Уģʽ`/a+/`ʾƥ1aaô׻ƥ伸aأΪḬ̆ģʽһֱƥ䵽ַaΪֹƥ3a + +뽫̰ģʽΪ̰ģʽʷһʺš +```js +var s = 'aaa'; +s.match(/a+?/) // ["a"] +``` +˷̰ģʽļӺţз̰ģʽǺţ`*`ͷ̰ģʽʺţ`?` + +`+?`ʾijģʽ1λΣƥʱ÷̰ģʽ + +`*?`ʾijģʽ0λΣƥʱ÷̰ģʽ + +`??`ijģʽ0λ1Σƥʱ÷̰ģʽ + +### 5.8 ƥ +* 1 + +ʽűʾƥ䣬еģʽƥݡ +```js +/fred+/.test('fredd') // true +/(fred)+/.test('fredfred') // true +``` +Уһģʽûţ`+`ֻʾظĸdڶģʽţ`+`ͱʾƥfredʡ + +һ鲶ӡ +```js +var m = 'abcabc'.match(/(.)b(.)/); +m// ['abc', 'a', 'c'] +``` +Уʽ`/(.)b(.)/`һʹţһŲaڶŲc + +ע⣬ʹƥʱͬʱʹgηmatchᲶݡ +```js +var m = 'abcabc'.match(/(.)b(.)/g); +m // ['abc', 'abc'] +``` +ʽڲnƥݣnǴ1ʼȻʾӦ˳š +```js +/(.)b(.)\1b\2/.test("abcabc")// true +``` +ĴУ1ʾһƥݣa2ʾڶƥݣc + +* 2Dz + +`(?:x)`ΪDz飨Non-capturing groupʾظƥݣƥĽвš + +Dz뿼һٶҪƥfoofoofooʽӦд`/(foo){1, 2}/`ռһƥ䡣ʱͿʹ÷Dz飬ʽΪ`/(?:foo){1, 2}/`ǰһһģDzᵥڲݡ +```js +var m = 'abc'.match(/(?:.)b(.)/); +m // ["abc", "c"] +``` +еģʽһʹšеһǷDz飬󷵻صĽûеһţֻеڶƥݡ + +* 3ж + +`x(?=y)`ΪжԣPositive look-aheadxֻyǰƥ䣬yᱻ뷵ؽ磬ҪƥŰٷֺŵ֣д`/d+(?=%)/` + +жԡУIJDz᷵صġ +```js +var m = 'abc'.match(/b(?=c)/); +m // ["b"] +``` +ĴʹжԣbcǰԱƥ䣬ŶӦcᱻء + +* 4з񶨶 + +`x(?!y)`Ϊз񶨶ԣNegative look-aheadxֻвyǰƥ䣬yᱻ뷵ؽ磬ҪƥIJǰٷֺŵ֣Ҫд`/d+(?!%)/` +```js +/\d+(?!\.)/.exec('3.14')// ["14"] +``` +УʽָֻвСǰֲŻᱻƥ䣬˷صĽ14 + +## 6. ʵս +### 6.1 ַβ˵Ŀո +```js +var str = ' #id div.class '; +str.replace(/^\s+|\s+$/g, '') // "#id div.class" +``` +### 6.2 ֻ֤ +```js +var reg = /1[24578]\d{9}/;reg.test('154554568997'); //truereg.test('234554568997'); //false +``` +### 6.3 ֻ滻 * +```js +var reg = /1[24578]\d{9}/;var str = ' ֻ18210999999 Ա'; + +str.replace(reg, '***') //" ֻ*** Ա" +``` +### 6.4 ƥҳǩ +```js +var strHtlm = 'СС
222222@.qq.com
СС'; + +var reg = /<(.+)>.+<\/\1>/; + +strHtlm.match(reg); // ["
222222@.qq.com
"] +``` +### 6.5 滻 +```js +let str = 'ййžл񹲺͹'; +let r = str.replace(/й|/g, input => { + let t = ''; + for (let i = 0; i + Click Here + +``` +е`data-age`һԶԣȻҲͨ**JavaScript**в**HTML5**Ԫضһ`dataset`ԣһ`DOMStringMap`͵ļֵԼϣ +```js +var test = document.getElementById('test'); +test.dataset.my = 'Byron'; +``` +Ϊ`div`һ`data-my`ԶԣʹJavaScript`dataset`Ҫעĵط + +- 1. ******ȡ**ԵʱҪȥǰ׺`data-*`ûʹ`test.dataset.data-my = 'Byron';`ʽ + +- 2. лַ(`-`)Ҫת**շ**ʽCSSʹѡҪʹַʽ + +ΪղŴ׷дݣ +```html + +``` + +```js +test.dataset.birthDate = '19890615'; +``` +ͨJavaScriptdata-birth-dateԶԣCSSʽΪdivһЩʽЧ + +![ͼƬ1](http://p3nqtyvgo.bkt.clouddn.com/2018100601.png) + +![ͼƬ2](http://p3nqtyvgo.bkt.clouddn.com/2018100602.png) + +ȡʱҲͨ`dataset`ʹá`.`ȡԣͬҪȥ`data-`ǰ׺ַҪתΪշ + +```js +var test = document.getElementById('test'); +test.dataset.my = 'Byron'; +test.dataset.birthDate = '19890615'; +test.onclick = function () { + alert(this.dataset.my + ' ' + this.dataset.age+' '+this.dataset.birthDate); +} +``` +![ͼƬ3](http://p3nqtyvgo.bkt.clouddn.com/2018100603.png) + +### 2. getAttribute/setAttribute +Щͬѧܻ`getAttribute`/`setAttribute`ʲôһ£ +```js +var test = document.getElementById('test'); +test.dataset.birthDate = '19890615'; +test.setAttribute('age', 25); +test.setAttribute('data-sex', 'male'); + +console.log(test.getAttribute('data-age')); //24 +console.log(test.getAttribute('data-birth-date')); //19890516 +console.log(test.dataset.age); //24 +console.log(test.dataset.sex); //male +``` + +![ͼƬ4](http://p3nqtyvgo.bkt.clouddn.com/2018100604.png) + +![ͼƬ5](http://p3nqtyvgo.bkt.clouddn.com/20181006045.png) + + +ǿԿ߶õ`attribute`ϻҪ˼ܽԶԣҲ˵`getAttribute`/`setAttribute`Բе`dataset`ݣ`dataset`ֻ`attribute`һӼˣ`dataset`ֻд`data-`ǰ׺ԣû`age=25`Ǹ + +ôΪʲôǻҪ`data-*`أһĺôǿ԰Զ`dataset`ͳһĶŶܷ㣬ɢɢˣûDzġ + +### 3. +ȽϲõϢ`data-*`ʮֲֹۡ +* **Internet Explorer 11+** +* **Chrome 8+** +* **Firefox 6.0+** +* **Opera 11.10+** +* **Safari 6+** + +IE11+ֱϹСۣҪȫʹô·Զӡ + diff --git "a/Cute-Article/article/59-\345\211\215\347\253\257HTML5\345\207\240\347\247\215\345\255\230\345\202\250\346\226\271\345\274\217\347\232\204\346\200\273\347\273\223.md" "b/Cute-Article/article/59-\345\211\215\347\253\257HTML5\345\207\240\347\247\215\345\255\230\345\202\250\346\226\271\345\274\217\347\232\204\346\200\273\347\273\223.md" new file mode 100644 index 00000000..7980659c --- /dev/null +++ "b/Cute-Article/article/59-\345\211\215\347\253\257HTML5\345\207\240\347\247\215\345\255\230\345\202\250\346\226\271\345\274\217\347\232\204\346\200\273\347\273\223.md" @@ -0,0 +1,269 @@ +ҪúܽһЩ֪ʶȻкö֪ʶᣬǻҪŬһ£ֶ˭֪~ + +## + +h5֮ǰ洢Ҫ`cookies``cookies`ȱ**ͷϴ**С**4k֮**DomainȾ + +ҪӦãﳵͻ¼ + +IE`UserData`С64k,ֻIE֧֡ + +## Ŀ +4kĴС +ͷ洢Ϣ +ϵʹ洢 + + +### 1.ش洢localstorage +* **洢ʽ** + +`ֵ`(`Key-Value`)ķʽ洢**ô洢****ʧЧ****ֶɾ** + +* **С** + +ÿ**5M** + +* **֧** +![ͼƬ1](http://p3nqtyvgo.bkt.clouddn.com/2018100701.png) + +ע⣺IE9 `localStorage`ֱ֧ļҪĿ𵽷ſ֧֣ + +* **ⷽ** +```js +if(window.localStorage){ + alert('This browser supports localStorage'); +}else{ + alert('This browser does NOT support localStorage'); +} +``` + +* **õAPI** +``` +getItem //ȡ¼ + +setIten //ü¼ + +removeItem //Ƴ¼ + +key //ȡkeyӦֵ + +clear //¼ +``` + +![ͼƬ2](http://p3nqtyvgo.bkt.clouddn.com/2018100702.png) + + +* **洢** + +飬ͼƬjsonʽűֻҪлַݶԴ洢 + +### 2.ش洢sessionstorage +HTML5 ıش洢 API е `localStorage` `sessionStorage` ʹ÷ͬģ `sessionStorage` **رҳ󼴱** `localStorage` **һֱ** + +### 3.߻棨application cache +ػӦļ + +* **ʹ÷** + +**manifestļ** + +ҳϣ +```html + + +... + +``` + +**Manifest ļ** + +manifest ļǼ򵥵ıļ֪ݣԼݣ + +manifest ļɷΪ֣ + +1. **CACHE MANIFEST** - ڴ˱гļ״غл + +2. **NETWORK** - ڴ˱гļҪӣҲᱻ + +3. **FALLBACK** - ڴ˱гļ涨ҳ޷ʱĻҳ棨 404 ҳ棩 + +demo +``` +CACHE MANIFEST +# 2016-07-24 v1.0.0 +/theme.css +/main.js + +NETWORK: +login.jsp + +FALLBACK: +/html/ /offline.html +``` + +ϣmanifestļҪȷ`MIME-type` "`text/cache-manifest`" + +Tomcat: +```html + + manifest + text/cache-manifest + +``` + +**API** + +`applicationCache`и`status`ԣʾӦûĵǰ״̬ + +**0**UNCACHED : ޻棬 ûҳصӦû + +**1**IDLE : ãӦûδõ + +**2** CHECKING : Уļ + +**3** DOWNLOADING : УӦûļָԴ + +**4** UPDATEREADY : ɣԴ + +**5** IDLE : ӦûļѾˣҳ޷ٷӦû + + + +**ص¼** + +ʾӦû״̬ĸı䣺 + +**checking** : ΪӦûҸʱ + +**error** : ڼ»Դڼ䷢ʹʱ + +**noupdate** : ڼļļޱ仯ʱ + +**downloading** : ڿʼӦûԴʱ + +**progress**ļӦûĹгϵصش + +**updateready** : ҳµӦûϴ + +**cached** : Ӧûʱ + +**Application Cache** + +1. +2. ҳٶ + +3. ͷѹ + +**ע** +1. Իݵƿ̫ܲһijЩõÿվ 5MB +2. manifestļڲоٵijһļأ¹̽ΪʧܣȫʹϵĻ档 +3. manifesthtmlmanifestļͬԴͬһ¡ +4. ԶmanifestļHTMLļ͵HTMLݣҲҪ°汾¡ +5. manifestļCACHENETWORKFALLBACKλ˳ûйϵʽҪǰ档 +6. FALLBACKеԴmanifestļͬԴ +7. 汾󣬱ˢһβŻ°汾ˢһҳҪӼ汾¼ +8. վеҳ漴ʹûmanifestԣԴڻҲӻзʡ +9. manifestļıʱԴҲᴥ¡ + + +**߻봫ͳ** + +1. ߻Ӧãǵļ + +2. ߻˻ǿԴҳ棬治 + +3. ߻֪ͨԴ + +### 4.Web SQL +**ϵݿ**ͨSQL + +Web SQL ݿ API HTML5 淶һ֣һĹ淶һʹ SQL ͻݿ APIs + +**֧** +Web SQL ݿ° **Safari**, **Chrome** **Opera** й + +**ķ** + +1. **openDatabase**ʹеݿ½ݿⴴһݿ + +2. **transaction**ܹһԼִύ߻ع + +3. **executeSql**ִʵʵ SQL ѯ + + + +**ݿ** +```js +var db = openDatabase('mydb', '1.0', 'Test DB', 2 * 1024 * 1024,fn); +//openDatabase() ӦֱΪݿơ汾šıݿСص +``` + +**ִвѯ** +```js +var db = openDatabase('mydb', '1.0', 'Test DB', 2 * 1024 * 1024); +db.transaction(function (tx) { + tx.executeSql('CREATE TABLE IF NOT EXISTS WIN (id unique, name)'); +}); +``` + +**** +```js +var db = openDatabase('mydb', '1.0', 'Test DB', 2 * 1024 * 1024); +db.transaction(function (tx) { + tx.executeSql('CREATE TABLE IF NOT EXISTS WIN (id unique, name)'); + tx.executeSql('INSERT INTO WIN (id, name) VALUES (1, "winty")'); + tx.executeSql('INSERT INTO WIN (id, name) VALUES (2, "LuckyWinty")'); +}); +``` + +**ȡ** +```js +db.transaction(function (tx) { + tx.executeSql('SELECT * FROM WIN', [], function (tx, results) { + var len = results.rows.length, i; + msg = "

ѯ¼: " + len + "

"; + document.querySelector('#status').innerHTML += msg; + + for (i = 0; i < len; i++){ + alert(results.rows.item(i).name ); + } + + }, null); +}); +``` +ЩԿ϶SQLݿزMySQLĻӦñȽá + + +### 5.IndexedDB +ݿ (IndexedDB) APIΪ HTML5 һ֣Դзḻش洢ݵܼ͵ HTML5 Web Ӧóáͬʱ**ػ**ʹͳ Web Ӧó򣨱ƶ Web Ӧóܹ**кӦ** + +**첽API** +IndexedDB󲿷ֲdzõĵ÷ؽģʽ󡪡ӦģʽݿIJ + +![ͼƬ3](http://p3nqtyvgo.bkt.clouddn.com/2018100703.png) + +ǴݿʱʵϷһDB󣬶`result`СͼԿresult֮⡣мҪԾ`onerror``onsuccess``onupgradeneeded`򿪵ݿİ汾źѾڵݿ汾ŲһµʱãǵajaxǷ֮󲢲ȷʲôʱɹҪڻصдһЩ߼ + +**رɾ** +```js +function closeDB(db){ + db.close(); +} +function deleteDB(name){ + indexedDB.deleteDatabase(name); +} +``` + +**ݴ洢** + +**indexedDB**ûбĸ`objectStore`һݿп԰`objectStore``objectStore`һݽṹԴŶݡҲ˵һ`objectStore`൱һű洢ÿݺһ + +ǿʹÿ¼еijֶָΪ**ֵ**keyPathҲʹԶɵĵΪֵkeyGeneratorҲԲָѡͲͬobjectStoreԴ洢ݽṹҲв졣 + +е㸴ˡĽ̳̣ +1.http://www.cnblogs.com/dolphinX/p/3415761.html + +2.http://www.cnblogs.com/dolphinX/p/3416889.html +ϸAPIַhttp://www.ibm.com/developerworks/cn/web/wa-indexeddb/#ibm-pcon + +ʱȷʵЩ洢ʽδùҲֻЩ˵ˡ˽ţԺõϸ¼ɣ^_^ \ No newline at end of file diff --git "a/6-\345\205\263\344\272\216js\347\232\204\344\275\234\347\224\250\345\237\237\345\222\214\345\243\260\346\230\216\346\217\220\345\211\215.md" "b/Cute-Article/article/6-\345\205\263\344\272\216js\347\232\204\344\275\234\347\224\250\345\237\237\345\222\214\345\243\260\346\230\216\346\217\220\345\211\215.md" similarity index 100% rename from "6-\345\205\263\344\272\216js\347\232\204\344\275\234\347\224\250\345\237\237\345\222\214\345\243\260\346\230\216\346\217\220\345\211\215.md" rename to "Cute-Article/article/6-\345\205\263\344\272\216js\347\232\204\344\275\234\347\224\250\345\237\237\345\222\214\345\243\260\346\230\216\346\217\220\345\211\215.md" diff --git "a/Cute-Article/article/60-\346\207\222\345\212\240\350\275\275\345\222\214\351\242\204\345\212\240\350\275\275.md" "b/Cute-Article/article/60-\346\207\222\345\212\240\350\275\275\345\222\214\351\242\204\345\212\240\350\275\275.md" new file mode 100644 index 00000000..64aec48a --- /dev/null +++ "b/Cute-Article/article/60-\346\207\222\345\212\240\350\275\275\345\222\214\351\242\204\345\212\240\350\275\275.md" @@ -0,0 +1,143 @@ +> [ԭ](https://segmentfault.com/a/1190000016666816) + +## һ +### 1.ʲô + +Ҳ**ӳټ**ָڳҳ**ӳټͼ**һֺܺŻҳܵķʽû֮ǰͼ񲻻ءͼԤ෴ڳҳʹӳټؽʹҳظ졣ijЩ£԰**ٷ**ͼƬܶ࣬ҳܳĵվС + +### 2.ΪʲôҪ +* **û**£ûֻԱҳʱҳеͼƬҪأͼƬĿϴ󣬵ȴʱܳûԹӰû顣 + +* **ЧԴļ**Լ˷ѹҲܹСĸ + +* **ֹصԴjsļ**Ӱվʹá + +### 3.صԭ +ȽҳϵͼƬ` src `ΪַͼƬʵ·`data-original`У +ҳʱҪȥ`scroll¼``scroll¼`ĻصУжǵصͼƬǷ,ͼƬڿڽͼƬ` src `Ϊ`data-original` ֵͿʵӳټء + +### 4.ʵֲ +```html + + + + Lazyload + + + + + + + + + + + + + + + + + + +``` + +## Ԥ +### 1.ʲôԤ +ԴԤһŻǿʹøüԤȸ֪ijЩԴڽᱻʹõ**Ԥؼ˵ǽԴǰصأҪõʱֱӴӻȡԴ** + +### 2.ΪʲôҪԤ +ҳȫ֮ǰһЩҪݽмأṩûõ飬ٵȴʱ䡣һҳݹӴûʹԤؼҳͻ᳤ʱչΪһƬհףֱݼϡ + +### .ʵԤصļְ취 +* ʹHTMLǩ +```html + +``` + +* ʹ`Image` +```html + +``` +```js +//myPreload.jsļ +var image= new Image() +image.src="http://pic26.nipic.com/20121213/6168183 004444903000 2.jpg" +``` + +* ʹXMLHttpRequest,Ȼڿ⣬ᾫϸԤع +```js +var xmlhttprequest=new XMLHttpRequest(); +xmlhttprequest.onreadystatechange=callback; +xmlhttprequest.onprogress=progressCallback; +xmlhttprequest.open("GET","http://image.baidu.com/mouse,jpg",true); +xmlhttprequest.send(); +function callback(){ + if(xmlhttprequest.readyState==4&& xmlhttprequest.status==200){ + var responseText=xmlhttprequest.responseText; + }else{ + console.log("Request was unsuccessful:"+xmlhttprequest.status); + } +} +function progressCallback(e){ + e=e || event; + if(e.lengthComputable){ + console.log("Received"+e.loaded+"of"+e.total+"bytes") + } +} +``` + +* ʹPreloadJS +PreloadJSṩһԤݵһ·ʽԱHTMLӦóʹáԤؿʹHTMLǩԼXHRɡĬ£PreloadJS᳢ʹXHRݣΪṩ˶ԽȺ¼ĸ֧֣ڿ⣬ʹûڱǵļؿܸá +```js +//ʹpreload.js +var queue=new createjs.LoadQueue(); +//Ĭxhrnew createjs.LoadQueue(false)ָʹHTMLǩԿ +queue.on("complete",handleComplete,this); +queue.loadManifest([ + {id:"myImage",src:"http://pic26.nipic.com/20121213/61681830044449030002.jpg"}, + {id"myImage2"src:"http://pic9.nipic.com/20100814/28395261931471581702.jpg"} +]); +function handleComplete(){ + var image=queue.getResuLt("myImage"); + document.body.appendChild(image); +} +``` + +## غԤصĶԱ +߶ҳЧİ취ҪһǰأһdzٻءضԷǰһĻѹãԤӷǰѹ + +## ġο +[غԤ(js)](https://www.geekjc.com/post/58d94d0f16a3655650d6fafe) + +[غԤ](https://lilywei739.github.io/2017/02/06/lazyload_Img.html) \ No newline at end of file diff --git "a/Cute-Article/article/61-JS\344\270\255this\347\232\2044\347\247\215\347\273\221\345\256\232\350\247\204\345\210\231.md" "b/Cute-Article/article/61-JS\344\270\255this\347\232\2044\347\247\215\347\273\221\345\256\232\350\247\204\345\210\231.md" new file mode 100644 index 00000000..9732e30b --- /dev/null +++ "b/Cute-Article/article/61-JS\344\270\255this\347\232\2044\347\247\215\347\273\221\345\256\232\350\247\204\345\210\231.md" @@ -0,0 +1,235 @@ +> [ԭĵַ](https://segmentfault.com/a/1190000016678888) + +## this +* ES6е**ͷ**õ**ʷ** +* ΪʲôҪʹthis**ʹAPIƵøڸ** +* thisָҲָĴʷ +* thisָֻȡ**ĵ÷ʽ** + +## this󶨹 +* new > ʾ > ʽ > Ĭϰ + +### Ĭϰ + +* ʱǷڵջУthisָ**ȫֶ**Ϊwindow +* ϸģʽ£ܽȫֶĬϰ󶨡 +```js +var a = 2; +function foo(){ + console.log(this.a); +} +function bar(){ + var a = 5; + foo(); +} +bar(); // 2 +``` + +### ʽ + +* Ķʱʽ󶨹Ѻе`this`󶨵Ķ +* ֻһڵλá +* Ҫ󣺶ڲһָԣöͨԼú +```js +function foo() { + console.log( this.a ); +} + +var obj2 = { + a: 42, + foo: foo +}; + +var obj1 = { + a: 2, + obj2: obj2 +}; + +obj1.obj2.foo(); // 42 +``` + +* ʽʧ +```js +function foo() { + console.log( this.a ); +} + +var obj = { + a: 2, + foo: foo +}; + +var bar = obj.foo; // barfooԲк + +var a = "oops, global"; // aȫֶ + +bar(); // "oops, global" +``` + +ͻص£ʱʽֵ + +```js +function foo() { + console.log( this.a ); +} + +function doFoo(fn) { + // ʱ൱fn = obj.fooͺϸһ + fn(); // <-- call-site! +} + +var obj = { + a: 2, + foo: foo +}; + +var a = "oops, global"; // `a` also property on global object + +doFoo( obj.foo ); // "oops, global" +``` + +### ʽ + +* `call()``apply()`ͨһΪͣᱻװתΪװ䣩`this`󶨵ö +* Ӳ + +```js +function foo() { + console.log( this.a ); +} + +var obj = { + a: 2 +}; + +var bar = function() { + foo.call( obj ); +}; + +bar(); // 2 +setTimeout( bar, 100 ); // 2 + +// Ӳ󶨺barôãӰfoothis +bar.call( window ); // 2 +``` + +Ӳ󶨵ĵӦµİ + +```js +function foo(something) { + console.log( this.a, something ); + return this.a + something; +} + +var obj = { + a: 2 +}; + +var bar = function() { + return foo.apply( obj, arguments ); // objӲȥ +}; + +var b = bar( 3 ); // 2 3 +console.log( b ); // 5 +``` + +ڲ`apply`Ӳ󶨵ijôӰڲ`this` +`bind`£ + +```js +function foo(something) { + console.log( this.a, something ); + return this.a + something; +} + +// simple `bind` helper +function bind(fn, obj) { + return function() { + return fn.apply( obj, arguments ); // òobjȥ + }; +} + +var obj = { + a: 2 +}; + +var bar = bind( foo, obj ); // bind( foo, obj )᷵һ + +var b = bar( 3 ); // 2 3 +console.log( b ); // 5 +``` +* **ܽ**ҪֻһһظдӲķʽ²ܱãijֹҪظʹʱΪ + +## new +κκܱ캯`new`****ʱִ + 1. һ¶úJSõģ򴴽һµObject󣩣 + 2. this󶨵 + 3. ִй캯еĴ루Ϊ¶ԣ + 4. ûзԶ¶returnصǷǶԶ¶󣬼ǸǶ + +```js +function foo(a) { + this.a = a; +} + +var bar = new foo( 2 ); +console.log( bar.a ); // 2 +``` + +### ˵ +* +```js +function foo() { + console.log( this.a ); +} + +var a = 2; +var o = { a: 3, foo: foo }; +var p = { a: 4 }; + +o.foo(); // 3 +(p.foo = o.foo)(); // 2p.foo = o.fooķֵĿ꺯ãԵλfoo()p.foo()o.foo() +``` + +* **ͷ**ʹĸthis򣬸**ʷ**`this` +```js +function foo() { + // һͷ + return (a) => { + // `this` here is lexically adopted from `foo()` + console.log( this.a ); + }; +} + +var obj1 = { + a: 2 +}; + +var obj2 = { + a: 3 +}; + +// foo()Ǽͷthis󶨵obj1 +var bar = foo.call( obj1 ); // foo.call( obj1 )ؼͷbarΪͷ +bar.call( obj2 ); // 2! ͷthis޷޸ģnewҲ +``` + +Ϊͼͷһģʽ + +```js +function foo() { + var self = this; // lexical capture of `this` + setTimeout( function(){ + console.log( self.a ); + }, 100 ); +} + +var obj = { + a: 2 +}; + +foo.call( obj ); // 2 +``` + +this󶨵Ȥ⣺ +[֪-arguments](https://www.zhihu.com/question/21466212) \ No newline at end of file diff --git "a/Cute-Article/article/62-2018\345\220\204\345\244\247\345\205\254\345\217\270\350\277\221\346\234\237\351\235\242\350\257\225\351\242\230.md" "b/Cute-Article/article/62-2018\345\220\204\345\244\247\345\205\254\345\217\270\350\277\221\346\234\237\351\235\242\350\257\225\351\242\230.md" new file mode 100644 index 00000000..2fadae20 --- /dev/null +++ "b/Cute-Article/article/62-2018\345\220\204\345\244\247\345\205\254\345\217\270\350\277\221\346\234\237\351\235\242\350\257\225\351\242\230.md" @@ -0,0 +1,469 @@ +> [ԭĵַ](https://segmentfault.com/a/1190000016789897) + + +## +* ʹùkoa2м +* koa-bodyԭ +* Լдм +* û漰Cluster +* pm2 +* master˵Ļpm2ô +* κMySQLͨ +* ReactڼԼ +* React-Router +* ·ɵĶ̬ģ +* ȾSSR +* ·ɵhistory +* Redux +* Reduxʵֶ֮ͨţʹͬ״̬νй +* ֮βָԵstateÿСԼ״̬֮仹һЩ״̬Ҫά˼ +* ʹùReduxм +* ν +* Httpͷ +* ƶ1px +* flex +* cssʽôֱ +* ΪʲôҪʹtransformΪʲôʹmarginLeft/Top +* ʹùwebpackЩpluginloader +* webpackIJôʵֵ +* dev-serverô +* ĿŻ +* ȡļôõ +* Ŀδȫ +* ôʵthis + +## +* reduxҪʲô +* ļϴϵ +* Կ +* promiseasyncʲô +* δ +* +* ܹ۲ģʽ +* нģʽ +* ۲ߺͶ-𣬸 +* reactŻ +* http2.0 +* ͨʲô +* http1.1ʱθtcp +* service worker +* css3position:sticky +* reduxмδ +* Promise쳣 +* position԰CSS3 +* ¼ +* ¼Լȱ +* Reactô¼ +* React¼ԭ +* this +* ǰôƹ· +* ʹ·ʱν +* Reactôݵļͱ仯 + +## ε +* react-routerôʵ·л +* react-routerǩͱǩʲô +* ǩĬ¼֮ʲôʵת +* ReactŻ +* ǰ·ּ +* import { Button } from 'antd'ʱֻbuttonģأô +* ʹimportʱwebpacknode_modulesʲô +* JS첽ķչԼȱ +* Httpĵм +* cookiecookieʹڵļֵ +* cookietokenheader棬Ϊʲôֻٳǰ +* cookiesessionЩ +* ReactDomṹ仯ڲЩ仯 +* Reactصʱ3textComponentcomposeComponentdomComponent͹ϵDomṹ仯ʱôdataı仯ô£ôȣµʱô +* keyҪǽһ⣬Ϊʲôindexػ棩 +* Redux첽ô +* Reduxмʲôܼ˵Ŀﻯ +* ﻯ˵IJʲô +* мôõstoreactionȻô +* stateôע뵽ģreducerʲôĹ +* koaresponse.sendresponse.roundedresponse.jsonʲô£Ϊʲôʶһjsonṹhtml +* koa-bodyparserôrequest +* webpackڣloaderpluginʲô +* ASTAbstract Syntax Tree﷨ +* ׿Activity֮ôݵ +* ׿4.06.0WebViewjsԵı仯 +* WebViewԭͨ +* ôûʹùApacheȷ + +## ͷ +* asyncawait⣬ڲԭ +* Promiseڲʵ +* +* λ⣨ԶλԶλȣ +* URLҳȫ +* tcp3 +* tcpһ㣨1 -> 2 · -> 3 (ip)-> 4 (tcp) -> 5 Ӧò(http) +* redux˼ +* reduxĹ +* connectĹ +* connectԭ +* webpack +* == ===ʲô== +* bindcallapply +* ˽ +* ԭǼ̳ +* Կ˽ + +## +* Linux 754 +* ðѡðŻ +* transformֱʹlefttopıλʲôȱ +* жǷл +* ܶص +* ʱ +* ES6еmapԭĶʲô +* ۲ߺͷ-ĵ +* react첽Ⱦĸ,Time Slicing Suspense +* 16.Xڵĸı +* 16.Xpropsıĸд +* ܴ +* ǰŻ +* pureComponentFunctionComponent +* JSX +* RNڰ׿IOS˵ +* RNΪʲôԭлƳԭbundle.js +* DOM +* һlocalStorage֤ݵʵЧ +* Promise.all() +* ܸ߽ +* sum(2, 3)ʵsum(2)(3)Ч +* reactŻ +* αȽ + +## ڲ +* JSԭ +* +* callapplybind +* ͽ +* ܸ첽 +* react +* Fiber +* ǰŻ +* DOMԱ +* reactеkey +* ״̬ +* cssxsrf +* http +* ĿӦݽṹ +* nativeṩʲôRN +* ϵŻ +* shouldComponentUpdateΪ˽ʲô +* νprops㼶 +* ǰôԪ +* webpack +* webpack +* õplugins +* pm2ô̹̹ҵô +* pm2ô̹ + +## +* +* ôȥ +* jsonpҪô +* AjaxҪʲôǰˣ +* CORS֮ӷʽɹĹ +* xsrf򹥻İȫô +* ʹAsyncעЩ +* Asyncжawait󣬿ôŻǷ +* PromiseAsyncʧܵʱʲô +* Redux״̬Reactܽ +* Reduxûװ +* reactڣõ +* Ӧʲô +* һĸ +* ôŻ첽... +* дreactЩϸڿŻ +* React¼ƣһ¼һϣ +* ¼Ҫʲô +* ǰ˿õЩģʽ +* React/ReduxЩõЩģʽ +* JSͷΪ֣ʲô +* JSջʲôõ֣ô +* һô֯CSSWebpack + +## ô +* С濪ҳ +* ReactӸ֮δֵ +* Emit¼ôҪʲô +* React߽ͨʲô +* һ飬ÿӶһidnameReactȾȫname +* ĸд +* мnameڣͨ첽ӿڻȡ +* Ⱦʱkeyʲôֵʹindexidûindex +* webpacksassҪЩloader +* cssҪЩloader +* ðjscsshtmlһļ +* divֱˮƽУflexԶλ +* Ԫؿ飬һһңм10 +* ¹̶мʵ +* [1, 2, 3, 4, 5][1, 2, 3, a, b, 5] +* ȡֵES5ES6 +* applycall +* ES5ES6ʲô +* someeveryfindfiltermapforEachʲô +* ȡÿηصֵһ +* 0-595-99 +* ҳ1buttonΰ¼ +* жbutton +* ҳһbuttonҰ¼JSԭDOM +* ѭʱindexǶ٣Ϊʲôô +* ҳһinputһpǩıinputpǩ͸ű仯δ +* inputĸ¼ʲôʱ򴥷 + +## Я +* ReactûһЩ +* ԱհĿΪʲôҪñհ +* дȥغ +* дƽ +* Promise; +* PromiseCallbackʲô +* React +* д㷨 + +## ϲ +* ES6µ +* Promise +* Promiseм״̬ +* ˵һ±հ +* React +* componentWillReceivePropsĴʲô +* React16.3ڵĸı +* ReactFilberܹ +* FilberȾ +* React߽ +* ֮ͨ +* ReduxôʵԴݣԭ +* React-Router汾 +* վSEOô +* HTTP״̬ +* 403301302ʲô +* صHTTPͷ +* HTTPS +* HTTPSôȫͨ +* ǰŻJSԭReact +* ûʲôŻ +* PWAʲô˽ +* ԰ȫʲô˽ +* ǩԭ +* ǰͨʹʲô +* RESTfulõMethod +* ¿ +* Access-Control-Allow-Originڷ +* csrfվô +* ǰ˺ͺô + +## Ұ +* localStoragecookieʲô +* CSSѡЩ +* ģͣԼ׼IEµ +* ʵָ߶Ӧ +* prototype͡proto +* _constructʲô +* newôʵֵ +* promiseľ裬Լȱ +* ʵH5ֻ˵ +* remflexroot em +* empx +* React +* ȥurlе# +* Redux״̬ͱصwindowʲô +* webpackgulpȱ +* ʵ첽 +* ʵַģڣ +* ǰŻ1js css2 ͼƬ3 Ԥأ 4 SSR 5 أ6 ؾ⣩ +* Դޣ6 +* base64Ϊʲôܣȱ +* webpͼƬļʽ +* koa2 +* Promiseʵֵ +* 첽󣬵Ͱ汾fetchεͰ汾 +* ajaxδ +* CORS +* jsonpΪʲô֧post +* ͬԴ +* ReactʹùһЩ +* Immuable +* reduxԭ +* ԭ +* μ̳ + +## ΢ҽ +* JSͣͺ͵ +* ArrayObject +* ͷֱ +* var a = {name: "ǰ˿"}; var b = a; a = nullôbʲô +* var a = {b: 1} +* var a = {b: {c: 1}} +* ջͶѵ +* ʱջͶѵ +* 10ݣȡһԪغ͵10Ԫصʱ +* ջͶѾô洢 +* ܱհԼհΪʲôû +* հʹó +* JSôʵ첽 +* 첽ִ +* Promise״̬ +* Async/Awaitôʵ +* PromisesetTimeoutִȺ +* JSΪʲôҪ΢ͺ +* Promise캯ͬ첽ִУthen +* -ĺ͹۲ģʽ +* JSִйзΪЩ׶ +* ʷthis +* ƽô̳ +* dz +* loadshʵԭ +* ES6letôʵֵ +* ReactsetStateʲô +* setStateΪʲôĬ첽 +* setStateʲôʱͬ +* Ϊʲô3ܳԺͳֺܶnativeRNܣDOM +* DOMҪʲô +* DOMʲôJS +* 304ʲô +* ʱHashôɵ +* ֵһα +* ʹwebpackʱһЩԶ +* webpackʲô +* abťaba˳baaα֤abaPromise.then +* nodeӿתʲôŻ +* nodeα֤ȶԣƽ +* RNûȼ +* RNļ +* RNʵһԭ +* RNԭԭRNʲôͬ +* ʲôǵҳĿ +* ĸҵ񳡾 +* Promise.allʵԭ + +## ¿ +* Promiseԣȱ +* Redux +* RNԭΪʲôͬʱڰ׿IOS +* RNεԭһЩ +* RNȱ +* 㷨Ϳԭ +* Ѻջ +* ܱհ +* հĺʲô +* ģ +* HTTPHTTPS +* HTTPSļܹ +* SSLTLS +* DNS +* JSļ̳з +* +* cookieΪ˽ʲô +* cookielocalStorage +* ν +* ǰŻ + +## +* ʹcanvasͼʱ֯ͨ +* formDataԭajaxʲô +* ±ύformDataʲôϵ +* redux +* ruduxȫֹʲôݿɿءӦ +* RNԭͨ +* MVPô֯ +* 첽 +* promiseʵthen +* koa2мԭ +* õм +* ôͳһ״̬ +* ζ·ýŻ +* nodeļȼ +* npm2npm3+ʲô +* +* knexݿӦص +* 첽 +* δ쳣 +* Ŀιģ +* ǰŻ +* JS̳з +* жһDz +* abν +* ¼ί +*
  • ǩɵDomṹһ +* +* domת +* ܵҳӦúͶҳӦ +* redux״̬Ĺ +* localstorageAPI + +## Ģ +* html廯 +* ͵ +* Ահ +* бհʹó +* thisԭ +* ʹԭĺô +* react˼· +* ΪʲôDOMʵDOMܺ +* reactͨŷʽ +* reduxĹ +* reduxȫֶ֮ +* Reduxݻ˼· +* ۲Ŀʵʳ +* ĿʹóԼ˽ +* ջ + +## +* react +* reactŻ +* ԭ¼ƳΪʲôڴй¶ +* Щطڴй¶ +* setIntervalҪעĵ +* ʱΪʲôDzȷ +* setTimeout(1)setTimeout(2)֮ +* ܺ΢ +* promisethenִʲô +* pureComponet +* Function Component +* React +* propsstate +* react context +* classES5Լ +* ܼͷͨ +* definePropertyʲôʱҪõ +* for..in object.keys +* ܱհʹó +* ʹñհȨʹó +* getpostʲô + +## ٷֵ +* React15/16.x +* ȾrenderЩʲô +* ЩᴥreactȾ +* statepropsµڷֱʲô +* setStateͬ첽 +* ״̬ +* Redux +* ES6Ĺ +* letconstԼvar +* dz +* ܼͷthis +* Promisethen +* ܿ +* 㷨ǰKԪ + +## +* reactȱ +* ʹù⣬ν +* reactʲôúʽҳȾ +* JSʲôʽ(Ǻʽ) +* koaԭΪʲôҪkoa(expresskoaԱ) +* ʹõkoaм +* ES6ʹõ﷨ +* Promise async/await callback +* Promiseûн첽⣨promiseǿĵط +* PromisesetTimeoutEvent Loop +* ̵̺߳һnodeʵһ̣nodeǵ̣߳ͨ¼ѭʵ첽 +* DFS +* ¹۲ģʽ +* ۲ģʽʹõݽṹ(߱˳ һlist) \ No newline at end of file diff --git "a/Cute-Article/article/63-ES6\346\261\207\346\200\273.md" "b/Cute-Article/article/63-ES6\346\261\207\346\200\273.md" new file mode 100644 index 00000000..07c02f50 --- /dev/null +++ "b/Cute-Article/article/63-ES6\346\261\207\346\200\273.md" @@ -0,0 +1,3297 @@ +## 1. ES6 + +### 1.1 let 和 const命令 + +在ES6中,我们通常实用 `let` 表示**变量**,`const` 表示**常量**,并且 `let` 和 `const` 都是**块级作用域**,且在**当前作用域有效**不能重复声明。 + +#### 1.1.1 let 命令 +`let` 命令的用法和 `var` 相似,但是 `let` 只在所在代码块内有效。 +**基础用法**: +```js +{ + let a = 1; + let b = 2; +} +``` + +并且 `let` 有以下特点: + +* **不存在变量提升:** +在ES6之前,我们 `var` 声明一个**变量**一个**函数**,都会伴随着变量提升的问题,导致实际开发过程经常出现一些逻辑上的疑惑,按照一般思维习惯,变量都是需要先声明后使用。 +```js +// var +console.log(v1); // undefined +var v1 = 2; +// 由于变量提升 代码实际如下 +var v1; +console.log(v1) +v1 = 2; + +// let +console.log(v2); // ReferenceError +let v2 = 2; +``` + +* **不允许重复声明:** +`let` 和 `const` 在**相同作用域下**,都**不能重复声明同一变量**,并且**不能在函数内重新声明参数**。 +```js +// 1. 不能重复声明同一变量 +// 报错 +function f1 (){ + let a = 1; + var a = 2; +} +// 报错 +function f2 (){ + let a = 1; + let a = 2; +} + +// 2. 不能在函数内重新声明参数 +// 报错 +function f3 (a1){ + let a1; +} +// 不报错 +function f4 (a2){ + { + let a2 + } +} +``` + +#### 1.1.2 const 命令 +`const` 声明一个**只读**的**常量**。 +**基础用法**: +```js +const PI = 3.1415926; +console.log(PI); // 3.1415926 + +``` +**注意点**: +* `const` 声明后,无法修改值; +```js +const PI = 3.1415926; +PI = 3; +// TypeError: Assignment to constant variable. +``` +* `const` 声明时,必须赋值; +```js +const a ; +// SyntaxError: Missing initializer in const declaration. +``` +* `const` 声明的常量,`let` 不能重复声明; +```js +const PI = 3.1415926; +let PI = 0; +// Uncaught SyntaxError: Identifier 'PI' has already been declared +``` + +[⬆ 返回目录](#二目录) + +### 1.2 变量的解构赋值 +**解构赋值概念**:在ES6中,直接从数组和对象中取值,按照对应位置,赋值给变量的操作。 + +#### 1.2.1 数组 +**基础用法**: +```js +// ES6 之前 +let a = 1; +let b = 2; + +// ES6 之后 +let [a, b] = [1, 2]; +``` + +本质上,只要等号两边模式一致,左边变量即可获取右边对应位置的值,更多用法: + +```js +let [a, [[b], c]] = [1, [[2], 3]]; +console.log(a, b, c); // 1, 2, 3 + +let [ , , c] = [1, 2, 3]; +console.log(c); // 3 + +let [a, , c] = [1, 2, 3]; +console.log(a,c); // 1, 3 + +let [a, ...b] = [1, 2, 3]; +console.log(a,b); // 1, [2,3] + +let [a, b, ..c.] = [1]; +console.log(a, b, c); // 1, undefined, [] +``` + +**注意点**: +* 如果解构不成功,变量的值就等于`undefined`。 +```js +let [a] = []; // a => undefined +let [a, b] = [1]; // a => 1 , b => undefined +``` +* 当左边模式多于右边,也可以解构成功。 +```js +let [a, b] = [1, 2, 3]; +console.log(a, b); // 1, 2 +``` +* 两边模式不同,报错。 +```js +let [a] = 1; +let [a] = false; +let [a] = NaN; +let [a] = undefined; +let [a] = null; +let [a] = {}; +``` + +**指定解构的默认值**: +**基础用法**: +```js +let [a = 1] = []; // a => 1 +let [a, b = 2] = [a]; // a => 1 , b => 2 +``` +特殊情况: +```js +let [a = 1] = [undefined]; // a => 1 +let [a = 1] = [null]; // a => null +``` +右边模式对应的值,必须严格等于`undefined`,默认值才能生效,而`null`不严格等于`undefined`。 + +#### 1.2.2 对象的解构赋值 +与数组解构不同的是,对象解构**不需要严格按照顺序取值**,而只要按照**变量名**去取对应**属性名**的值,若取不到对应**属性名**的值,则为`undefined` 。 + +**基础用法**: +```js +let {a, b} = {a:1, b:2}; // a => 1 , b => 2 +let {a, b} = {a:2, b:1}; // a => 2 , b => 1 +let {a} = {a:3, b:2, c:1};// a => 3 +let {a} = {b:2, c:1}; // a => undefined +``` + +**注意点**: +* 若**变量名**和**属性名**不一致,则需要修改名称。 +```js +let {a:b} = {a:1, c:2}; +// error: a is not defined +// b => 1 +``` +对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。 +上面代码中,`a` 是匹配的模式,`b`才是变量。真正被赋值的是变量`b`,而不是模式`a`。 + +* 对象解构也支持**嵌套解构**。 +```js +let obj = { + a:[ 1, { b: 2}] +}; +let {a, a: [c, {b}]} = obj; +// a=>[1, {b: 2}], b => 2, c => 1 +``` + +**指定解构的默认值**: +```js +let {a=1} = {}; // a => 1 +let {a, b=1} = {a:2}; // a => 2, b => 1 + +let {a:b=3} = {}; // b => 3 +let {a:b=3} = {a:4}; // b = >4 +// a是模式,b是变量 牢记 + +let {a=1} = {a:undefined}; // a => 1 +let {a=1} = {a:null}; // a => null +// 因为null与undefined不严格相等,所以赋值有效 +// 导致默认值1不会生效。 +``` + +#### 1.2.3 字符串的解构赋值 +字符串的解构赋值中,字符串被转换成了一个**类似数组的对象**。 +**基础用法**: +```js +const [a, b, c, d, e] = 'hello'; +a // "h" +b // "e" +c // "l" +d // "l" +e // "o" + +let {length:len} = 'hello';// len => 5 +``` + +#### 1.2.4 数值和布尔值的解构赋值 +解构赋值的规则是,**只要等号右边的值不是对象或数组,就先将其转为对象**。由于`undefined`和`null`**无法转为对象**,所以对它们进行解构赋值,都会报错。 +```js +// 数值和布尔值的包装对象都有toString属性 +let {toString: s} = 123; +s === Number.prototype.toString // true +let {toString: s} = true; +s === Boolean.prototype.toString // true + +let { prop: x } = undefined; // TypeError +let { prop: y } = null; // TypeError +``` + +#### 1.2.5 函数参数的解构赋值 +**基础用法**: +```js +function fun ([a, b]){ + return a + b; +} +fun ([1, 2]); // 3 +``` +**指定默认值的解构**: +```js +function fun ({a=0, b=0} = {}){ + return [a, b]; +} +fun ({a:1, b:2}); // [1, 2] +fun ({a:1}); // [1, 0] +fun ({}); // [0, 0] +fun (); // [0, 0] + +function fun ({a, b} = {a:0, b:0}){ + return [a, b]; +} +fun ({a:1, b:2}); // [1, 2] +fun ({a:1}); // [1, undefined] +fun ({}); // [undefined, undefined] +fun (); // [0, 0] +``` + +#### 1.2.6 应用 +* **交换变量的值**: +```js +let a = 1,b = 2; +[a, b] = [b, a]; // a =>2 , b => 1 +``` + +* **函数返回多个值**: +```js +// 返回一个数组 +function f (){ + return [1, 2, 3]; +} +let [a, b, c] = f(); // a=>1, b=>2, c=>3 + +// 返回一个对象 +function f (){ + return {a:1, b:2}; +} +let {a, b} = f(); // a=>1, b=>2 +``` + +* **快速对应参数**: +快速的将一组参数与变量名对应。 +```js +function f([a, b, c]) {...} +f([1, 2, 3]); + +function f({a, b, c}) {...} +f({b:2, c:3, a:1}); +``` + +* **提取JSON数据**: +```js +let json = { + name : 'leo', + age: 18 +} +let {name, age} = json; +console.log(name,age); // leo, 18 +``` + +* **遍历Map结构**: +```js +const m = new Map(); +m.set('a':1); +m.set('b':2); +for ([k, v] of m){ + console.log(k + ' : ' + v); +} +// 获取键名 +for (let [k] of m){...} +// 获取键值 +for (let [,k] of m){...} +``` + +* **输入模块的指定方法**: +用于**按需加载**模块中需要用到的方法。 +```js +const {log, sin, cos} = require('math'); + +``` + +[⬆ 返回目录](#二目录) + + +### 1.3 字符串的拓展 +#### 1.3.1 includes(),startsWith(),endsWith() +在我们判断字符串是否包含另一个字符串时,ES6之前,我们只有`typeof`方法,ES6之后我们又多了三种方法: +* **includes()**:返回**布尔值**,表示**是否找到参数字符串**。 +* **startsWith()**:返回**布尔值**,表示参数字符串是否在原字符串的**头部**。 +* **endsWith()**:返回**布尔值**,表示参数字符串是否在原字符串的**尾部**。 +```js +let a = 'hello leo'; +a.startsWith('leo'); // false +a.endsWith('o'); // true +a.includes('lo'); // true +``` +并且这三个方法都支持第二个参数,表示起始搜索的位置。 +```js +let a = 'hello leo'; +a.startsWith('leo',1); // false +a.endsWith('o',5); // true +a.includes('lo',6); // false +``` +`endsWith` 是针对前 `n` 个字符,而其他两个是针对从第`n`个位置直到结束。 + +#### 1.3.2 repeat() +`repeat`方法返回一个新字符串,表示将原字符串重复`n`次。 +**基础用法**: +```js +'ab'.repeat(3); // 'ababab' +'ab'.repeat(0); // '' +``` +**特殊用法**: +* 参数为`小数`,则取整 +```js +'ab'.repeat(2.3); // 'abab' +``` +* 参数为`负数`或`Infinity`,则报错 +```js +'ab'.repeat(-1); // RangeError +'ab'.repeat(Infinity); // RangeError +``` +* 参数为`0到-1的小数`或`NaN`,则取0 +```js +'ab'.repeat(-0.5); // '' +'ab'.repeat(NaN); // '' +``` +* 参数为`字符串`,则转成`数字` +```js +'ab'.repeat('ab'); // '' +'ab'.repeat('3'); // 'ababab' +``` + +####1.3.3 padStart(),padEnd() +用于将字符串**头部**或**尾部**补全长度,`padStart()`为**头部补全**,`padEnd()`为**尾部补全**。 +这两个方法接收**2个**参数,第一个指定**字符串最小长度**,第二个**用于补全的字符串**。 +**基础用法** : +```js +'x'.padStart(5, 'ab'); // 'ababx' +'x'.padEnd(5, 'ab'); // 'xabab' +``` +**特殊用法**: +* 原字符串长度,大于或等于指定最小长度,则返回原字符串。 +```js +'xyzabc'.padStart(5, 'ab'); // 'xyzabc' +``` +* 用来补全的字符串长度和原字符串长度之和,超过指定最小长度,则截去超出部分的补全字符串。 +```js +'ab'.padStart(5,'012345'); // "012ab" +``` +* 省略第二个参数,则用`空格`补全。 +```js +'x'.padStart(4); // ' x' +'x'.endStart(4); // 'x ' +``` +#### 1.3.4 模版字符串 +用于拼接字符串,ES6之前: +```js +let a = 'abc' + + 'def' + + 'ghi'; +``` +ES6之后: +```js +let a = ` + abc + def + ghi +` +``` +**拼接变量**: +在**反引号(\`)**中使用`${}`包裹变量或方法。 +```js +// ES6之前 +let a = 'abc' + v1 + 'def'; + +// ES6之后 +let a = `abc${v1}def` +``` + +[⬆ 返回目录](#二目录) + + +### 1.4 正则的拓展 +#### 1.4.1 介绍 +在ES5中有两种情况。 +* 参数是**字符串**,则第二个参数为正则表达式的修饰符。 +```js +let a = new RegExp('abc', 'i'); +// 等价于 +let a = /abx/i; +``` +* 参数是**正则表达式**,返回一个原表达式的拷贝,且不能有第二个参数,否则报错。 +```js +let a = new RegExp(/abc/i); +//等价于 +let a = /abx/i; + +let a = new RegExp(/abc/, 'i'); +// Uncaught TypeError +``` +ES6中使用: +第一个参数是正则对象,第二个是指定修饰符,如果第一个参数已经有修饰符,则会被第二个参数覆盖。 +```js +new RegExp(/abc/ig, 'i'); +``` + +#### 1.4.2 字符串的正则方法 +常用的四种方法:`match()`、`replace()`、`search()`和`split()`。 + +#### 1.4.3 u修饰符 +添加`u`修饰符,是为了处理大于`uFFFF`的Unicode字符,即正确处理四个字节的UTF-16编码。 +```js +/^\uD83D/u.test('\uD83D\uDC2A'); // false +/^\uD83D/.test('\uD83D\uDC2A'); // true +``` +由于ES5之前不支持四个字节UTF-16编码,会识别为两个字符,导致第二行输出`true`,加入`u`修饰符后ES6就会识别为一个字符,所以输出`false`。 + +**注意:** +加上`u`修饰符后,会改变下面正则表达式的行为: +* (1)点字符 +点字符(`.`)在正则中表示除了**换行符**以外的任意单个字符。对于码点大于`0xFFFF`的Unicode字符,点字符不能识别,必须加上`u`修饰符。 +```js +var a = "𠮷"; +/^.$/.test(a); // false +/^.$/u.test(a); // true +``` +* (2)Unicode字符表示法 +使用ES6新增的大括号表示Unicode字符时,必须在表达式添加`u`修饰符,才能识别大括号。 +```js +/\u{61}/.test('a'); // false +/\u{61}/u.test('a'); // true +/\u{20BB7}/u.test('𠮷'); // true +``` +* (3)量词 +使用`u`修饰符后,所有量词都会正确识别码点大于`0xFFFF`的 Unicode 字符。 +```js +/a{2}/.test('aa'); // true +/a{2}/u.test('aa'); // true +/𠮷{2}/.test('𠮷𠮷'); // false +/𠮷{2}/u.test('𠮷𠮷'); // true +``` +* (4)i修饰符 +不加`u`修饰符,就无法识别非规范的`K`字符。 +```js +/[a-z]/i.test('\u212A') // false +/[a-z]/iu.test('\u212A') // true +``` + +**检查是否设置`u`修饰符:** +使用`unicode`属性。 +```js +const a = /hello/; +const b = /hello/u; + +a.unicode // false +b.unicode // true +``` + +#### 1.4.4 y修饰符 +`y`修饰符与`g`修饰符类似,也是全局匹配,后一次匹配都是从上一次匹配成功的下一个位置开始。区别在于,`g`修饰符**只要**剩余位置中存在匹配即可,而`y`修饰符是必须从**剩余第一个**开始。 +```js +var s = 'aaa_aa_a'; +var r1 = /a+/g; +var r2 = /a+/y; + +r1.exec(s) // ["aaa"] +r2.exec(s) // ["aaa"] + +r1.exec(s) // ["aa"] 剩余 '_aa_a' +r2.exec(s) // null +``` +**`lastIndex`属性**: +指定匹配的开始位置: +```js +const a = /a/y; +a.lastIndex = 2; // 从2号位置开始匹配 +a.exec('wahaha'); // null +a.lastIndex = 3; // 从3号位置开始匹配 +let c = a.exec('wahaha'); +c.index; // 3 +a.lastIndex; // 4 +``` +**返回多个匹配**: +一个`y`修饰符对`match`方法只能返回第一个匹配,与`g`修饰符搭配能返回所有匹配。 +```js +'a1a2a3'.match(/a\d/y); // ["a1"] +'a1a2a3'.match(/a\d/gy); // ["a1", "a2", "a3"] +``` +**检查是否使用`y`修饰符**: +使用`sticky`属性检查。 +```js +const a = /hello\d/y; +a.sticky; // true +``` + +#### 1.4.5 flags属性 +`flags`属性返回所有正则表达式的修饰符。 +```js +/abc/ig.flags; // 'gi' +``` + + +[⬆ 返回目录](#二目录) + + +### 1.5 数值的拓展 +#### 1.5.1 Number.isFinite(), Number.isNaN() +`Number.isFinite()` 用于检查一个数值是否是有限的,即不是`Infinity`,若参数不是`Number`类型,则一律返回`false` 。 +```js +Number.isFinite(10); // true +Number.isFinite(0.5); // true +Number.isFinite(NaN); // false +Number.isFinite(Infinity); // false +Number.isFinite(-Infinity); // false +Number.isFinite('leo'); // false +Number.isFinite('15'); // false +Number.isFinite(true); // false +Number.isFinite(Math.random()); // true +``` + +`Number.isNaN()`用于检查是否是`NaN`,若参数不是`NaN`,则一律返回`false`。 +```js +Number.isNaN(NaN); // true +Number.isNaN(10); // false +Number.isNaN('10'); // false +Number.isNaN(true); // false +Number.isNaN(5/NaN); // true +Number.isNaN('true' / 0); // true +Number.isNaN('true' / 'true'); // true +``` + +**区别**: +与传统全局的`isFinite()`和`isNaN()`方法的区别,传统的这两个方法,是先将参数转换成**数值**,再判断。 +而ES6新增的这两个方法则只对**数值**有效, `Number.isFinite()`对于**非数值**一律返回`false`,` Number.isNaN()`只有对于`NaN`才返回`true`,其他一律返回`false`。 +```js +isFinite(25); // true +isFinite("25"); // true +Number.isFinite(25); // true +Number.isFinite("25"); // false + +isNaN(NaN); // true +isNaN("NaN"); // true +Number.isNaN(NaN); // true +Number.isNaN("NaN"); // false +``` + +#### 1.5.2 Number.parseInt(), Number.parseFloat() +这两个方法与全局方法`parseInt()`和`parseFloat()`一致,目的是逐步**减少全局性的方法**,让**语言更模块化**。 +```js +parseInt('12.34'); // 12 +parseFloat('123.45#'); // 123.45 + +Number.parseInt('12.34'); // 12 +Number.parseFloat('123.45#'); // 123.45 + +Number.parseInt === parseInt; // true +Number.parseFloat === parseFloat; // true +``` + +#### 1.5.3 Number.isInteger() +用来判断一个数值是否是整数,若参数不是数值,则返回`false`。 +```js +Number.isInteger(10); // true +Number.isInteger(10.0); // true +Number.isInteger(10.1); // false +``` + +#### 1.5.4 Math对象的拓展 +ES6新增17个数学相关的**静态方法**,只能在**Math对象**上调用。 +* **Math.trunc**: +用来去除小数的小数部分,**返回整数部分**。 +若参数为**非数值**,则**先转为数值**。 +若参数为**空值**或**无法截取整数的值**,则返回**NaN**。 +```js +// 正常使用 +Math.trunc(1.1); // 1 +Math.trunc(1.9); // 1 +Math.trunc(-1.1); // -1 +Math.trunc(-1.9); // -1 +Math.trunc(-0.1234); // -0 + +// 参数为非数值 +Math.trunc('11.22'); // 11 +Math.trunc(true); // 1 +Math.trunc(false); // 0 +Math.trunc(null); // 0 + +// 参数为空和无法取整 +Math.trunc(NaN); // NaN +Math.trunc('leo'); // NaN +Math.trunc(); // NaN +Math.trunc(undefined); // NaN +``` +**ES5实现**: +```js +Math.trunc = Math.trunc || function(x){ + return x < 0 ? Math.ceil(x) : Math.floor(x); +} +``` + +* **Math.sign()**: +判断一个数是**正数**、**负数**还**是零**,对于非数值,会先转成**数值**。 +返回值: + * 参数为正数, 返回 +1 + * 参数为负数, 返回 -1 + * 参数为0, 返回 0 + * 参数为-0, 返回 -0 + * 参数为其他值, 返回 NaN +```js +Math.sign(-1); // -1 +Math.sign(1); // +1 +Math.sign(0); // 0 +Math.sign(-0); // -0 +Math.sign(NaN); // NaN + +Math.sign(''); // 0 +Math.sign(true); // +1 +Math.sign(false);// 0 +Math.sign(null); // 0 +Math.sign('9'); // +1 +Math.sign('leo');// NaN +Math.sign(); // NaN +Math.sign(undefined); // NaN +``` + +**ES5实现** +```js +Math.sign = Math.sign || function (x){ + x = +x; + if (x === 0 || isNaN(x)){ + return x; + } + return x > 0 ? 1: -1; +} +``` + +* **Math.cbrt()**: +用来计算一个数的立方根,若参数为非数值则先转成数值。 +```js +Math.cbrt(-1); // -1 +Math.cbrt(0); // 0 +Math.cbrt(1); // 1 +Math.cbrt(2); // 1.2599210498 + +Math.cbrt('1'); // 1 +Math.cbrt('leo'); // NaN +``` +**ES5实现** +```js +Math.cbrt = Math.cbrt || function (x){ + var a = Math.pow(Math.abs(x), 1/3); + return x < 0 ? -y : y; +} +``` + +* **Math.clz32()**: +用于返回一个数的 32 位无符号整数形式有多少个前导 0。 +```js +Math.clz32(0) // 32 +Math.clz32(1) // 31 +Math.clz32(1000) // 22 +Math.clz32(0b01000000000000000000000000000000) // 1 +Math.clz32(0b00100000000000000000000000000000) // 2 +``` + +* **Math.imul()**: +用于返回两个数以 32 位带符号整数形式相乘的结果,返回的也是一个 32 位的带符号整数。 +```js +Math.imul(2, 4) // 8 +Math.imul(-1, 8) // -8 +Math.imul(-2, -2) // 4 +``` + +* **Math.fround()**: +用来返回一个数的**2位单精度浮点数**形式。 +```js +Math.fround(0) // 0 +Math.fround(1) // 1 +Math.fround(2 ** 24 - 1) // 16777215 +``` + +* **Math.hypot()**: +用来返回所有参数的平方和的**平方根**。 +```js +Math.hypot(3, 4); // 5 +Math.hypot(3, 4, 5); // 7.0710678118654755 +Math.hypot(); // 0 +Math.hypot(NaN); // NaN +Math.hypot(3, 4, 'foo'); // NaN +Math.hypot(3, 4, '5'); // 7.0710678118654755 +Math.hypot(-3); // 3 +``` + +* **Math.expm1()**: +用来返回` ex - 1`,即`Math.exp(x) - 1`。 +```js +Math.expm1(-1) // -0.6321205588285577 +Math.expm1(0) // 0 +Math.expm1(1) // 1.718281828459045 +``` +**ES5实现** +```js +Math.expm1 = Math.expm1 || function(x) { + return Math.exp(x) - 1; +}; +``` + +* **Math.log1p()**: +用来返回`1 + x`的自然对数,即`Math.log(1 + x)`。如果x小于`-1`,返回`NaN`。 +```js +Math.log1p(1) // 0.6931471805599453 +Math.log1p(0) // 0 +Math.log1p(-1) // -Infinity +Math.log1p(-2) // NaN +``` +**ES5实现** +```js +Math.log1p = Math.log1p || function(x) { + return Math.log(1 + x); +}; +``` + +* **Math.log10()**: +用来返回以 `10 `为底的`x的对数`。如果x小于 0,则返回 `NaN`。 +```js +Math.log10(2) // 0.3010299956639812 +Math.log10(1) // 0 +Math.log10(0) // -Infinity +Math.log10(-2) // NaN +Math.log10(100000) // 5 +``` +**ES5实现** +```js +Math.log10 = Math.log10 || function(x) { + return Math.log(x) / Math.LN10; +}; +``` + +* **Math.log2()**: +用来返回以 `2` 为底的`x的对数`。如果`x`小于` 0`,则返回 `NaN`。 +```js +Math.log2(3) // 1.584962500721156 +Math.log2(2) // 1 +Math.log2(1) // 0 +Math.log2(0) // -Infinity +Math.log2(-2) // NaN +Math.log2(1024) // 10 +Math.log2(1 << 29) // 29 +``` +**ES5实现** +```js +Math.log2 = Math.log2 || function(x) { + return Math.log(x) / Math.LN2; +}; +``` +* **双曲函数方法**: + * `Math.sinh(x)` 返回x的**双曲正弦**(hyperbolic sine) + * `Math.cosh(x)` 返回x的**双曲余弦**(hyperbolic cosine) + * `Math.tanh(x)` 返回x的**双曲正切**(hyperbolic tangent) + * `Math.asinh(x)` 返回x的**反双曲正弦**(inverse hyperbolic sine) + * `Math.acosh(x)` 返回x的**反双曲余弦**(inverse hyperbolic cosine) + * `Math.atanh(x)` 返回x的**反双曲正切**(inverse hyperbolic tangent) + +#### 1.5.5 指数运算符 +新增的指数运算符(`**`): +```js +2 ** 2; // 4 +2 ** 3; // 8 + +2 ** 3 ** 2; // 相当于 2 ** (3 ** 2); 返回 512 +``` +指数运算符(`**`)与`Math.pow`的实现不相同,对于特别大的运算结果,两者会有细微的差异。 +```js +Math.pow(99, 99) +// 3.697296376497263e+197 + +99 ** 99 +// 3.697296376497268e+197 +``` + + +[⬆ 返回目录](#二目录) + + +### 1.6 函数的拓展 +#### 1.6.1 参数默认值 +```js +// ES6 之前 +function f(a, b){ + b = b || 'leo'; + console.log(a, b); +} + +// ES6 之后 +function f(a, b='leo'){ + console.log(a, b); +} + +f('hi'); // hi leo +f('hi', 'jack'); // hi jack +f('hi', ''); // hi leo +``` +**注意**: +* 参数变量是默认声明的,不能用`let`和`const`再次声明: +```js +function f (a = 1){ + let a = 2; // error +} +``` +* 使用参数默认值时,参数名不能相同: +```js +function f (a, a, b){ ... }; // 不报错 +function f (a, a, b = 1){ ... }; // 报错 +``` + +**与解构赋值默认值结合使用**: +```js +function f ({a, b=1}){ + console.log(a,b) +}; +f({}); // undefined 1 +f({a:2}); // 2 1 +f({a:2, b:3}); // 2 3 +f(); // 报错 + +function f ({a, b = 1} = {}){ + console.log(a, b) +} +f(); // undefined 1 +``` + +**尾参数定义默认值**: +通常在尾参数定义默认值,便于观察参数,并且非尾参数无法省略。 +```js +function f (a=1,b){ + return [a, b]; +} +f(); // [1, undefined] +f(2); // [2, undefined] +f(,2); // 报错 + +f(undefined, 2); // [1, 2] + +function f (a, b=1, c){ + return [a, b, c]; +} +f(); // [undefined, 1, undefined] +f(1); // [1,1,undefined] +f(1, ,2); // 报错 +f(1,undefined,2); // [1,1,2] +``` +在给参数传递默认值时,传入`undefined`会触发默认值,传入`null`不会触发。 +```js +function f (a = 1, b = 2){ + console.log(a, b); +} +f(undefined, null); // 1 null +``` + +**函数的length属性**: +`length`属性将返回,没有指定默认值的参数数量,并且rest参数不计入`length`属性。 +```js +function f1 (a){...}; +function f2 (a=1){...}; +function f3 (a, b=2){...}; +function f4 (...a){...}; +function f5 (a,b,...c){...}; + +f1.length; // 1 +f2.length; // 0 +f3.length; // 1 +f4.length; // 0 +f5.length; // 2 +``` + +#### 1.6.2 rest 参数 +`rest`参数形式为(`...变量名`),其值为一个数组,用于获取函数多余参数。 +```js +function f (a, ...b){ + console.log(a, b); +} +f(1,2,3,4); // 1 [2, 3, 4] +``` +**注意**: +* `rest`参数只能放在最后一个,否则报错: +```js +function f(a, ...b, c){...}; // 报错 +``` +* 函数的`length`属性不包含`rest`参数。 +```js +function f1 (a){...}; +function f2 (a,...b){...}; +f1(1); // 1 +f2(1,2); // 1 +``` + +#### 1.6.3 name 属性 +用于返回该函数的函数名。 +```js +function f (){...}; +f.name; // f + +const f = function g(){...}; +f.name; // g +``` + +#### 1.6.4 箭头函数 +使用“箭头”(`=>`)定义函数。 +**基础使用**: +```js +// 有1个参数 +let f = v => v; +// 等同于 +let f = function (v){return v}; + +// 有多个参数 +let f = (v, i) => {return v + i}; +// 等同于 +let f = function (v, i){return v + i}; + +// 没参数 +let f = () => 1; +// 等同于 +let f = function (){return 1}; +``` + +**箭头函数与变量结构结合使用**: +```js +// 正常函数写法 +function f (p) { + return p.a + ':' + p.b; +} + +// 箭头函数写法 +let f = ({a, b}) => a + ':' + b; +``` + +**简化回调函数**: +```js +// 正常函数写法 +[1, 2, 3].map(function (x){ + return x * x; +}) + + +// 箭头函数写法 +[1, 2, 3].map(x => x * x); +``` + +**箭头函数与rest参数结合**: +```js +let f = (...n) => n; +f(1, 2, 3); // [1, 2, 3] +``` + +**注意点**: +* 1.箭头函数内的`this`**总是**指向**定义时所在的对象**,而不是调用时。 +* 2.箭头函数不能当做**构造函数**,即不能用`new`命令,否则报错。 +* 3.箭头函数不存在`arguments`对象,即不能使用,可以使用`rest`参数代替。 +* 4.箭头函数不能使用`yield`命令,即不能用作**Generator**函数。 + +**不适用场景**: +* 1.在定义函数方法,且该方法内部包含`this`。 +```js +const obj = { + a:9, + b: () => { + this.a --; + } +} +``` +上述`b`如果是**普通函数**,函数内部的`this`指向`obj`,但是如果是箭头函数,则`this`会指向**全局**,不是预期结果。 + +* 2.需要动态`this`时。 +```js +let b = document.getElementById('myID'); +b.addEventListener('click', ()=>{ + this.classList.toggle('on'); +}) +``` +上诉按钮点击会报错,因为`b`监听的箭头函数中,`this`是全局对象,若改成**普通函数**,`this`就会指向被点击的按钮对象。 + +#### 1.6.5 双冒号运算符 +双冒号暂时是一个提案,用于解决一些不适用的场合,取代`call`、`apply`、`bind`调用。 +双冒号运算符(`::`)的左边是一个**对象**,右边是一个**函数**。该运算符会自动将左边的对象,作为上下文环境(即`this`对象),绑定到右边函数上。 +```js +f::b; +// 等同于 +b.bind(f); + +f::b(...arguments); +// 等同于 +b.apply(f, arguments); +``` +若双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定到该对象上。 +```js +let f = a::a.b; +// 等同于 +let f = ::a.b; +``` + +[⬆ 返回目录](#二目录) + + +### 1.7 数组的拓展 +#### 1.7.1 拓展运算符 +拓展运算符使用(`...`),类似`rest`参数的逆运算,将数组转为用(`,`)分隔的参数序列。 +```js +console.log(...[1, 2, 3]); // 1 2 3 +console.log(1, ...[2,3], 4); // 1 2 3 4 +``` +拓展运算符主要使用在函数调用。 +```js +function f (a, b){ + console.log(a, b); +} +f(...[1, 2]); // 1 2 + +function g (a, b, c, d, e){ + console.log(a, b, c, d, e); +} +g(0, ...[1, 2], 3, ...[4]); // 0 1 2 3 4 +``` +**若拓展运算符后面是个空数组,则不产生效果**。 +```js +[...[], 1]; // 1 +``` + +**替代apply方法** +```js +// ES6之前 +function f(a, b, c){...}; +var a = [1, 2, 3]; +f.apply(null, a); + +// ES6之后 +function f(a, b, c){...}; +let a = [1, 2, 3]; +f(...a); + +// ES6之前 +Math.max.apply(null, [3,2,6]); + +// ES6之后 +Math.max(...[3,2,6]); +``` + +**拓展运算符的运用** +* **(1)复制数组**: +通常我们直接复制数组时,只是浅拷贝,如果要实现深拷贝,可以使用拓展运算符。 +```js +// 通常情况 浅拷贝 +let a1 = [1, 2]; +let a2 = a1; +a2[0] = 3; +console.log(a1,a2); // [3,2] [3,2] + +// 拓展运算符 深拷贝 +let a1 = [1, 2]; +let a2 = [...a1]; +// let [...a2] = a1; // 作用相同 +a2[0] = 3; +console.log(a1,a2); // [1,2] [3,2] +``` +* **(2)合并数组**: +注意,这里合并数组,只是浅拷贝。 +```js +let a1 = [1,2]; +let a2 = [3]; +let a3 = [4,5]; + +// ES5 +let a4 = a1.concat(a2, a3); + +// ES6 +let a5 = [...a1, ...a2, ...a3]; + +a4[0] === a1[0]; // true +a5[0] === a1[0]; // true +``` +* **(3)与解构赋值结合**: +与解构赋值结合生成数组,但是使用拓展运算符需要放到参数最后一个,否则报错。 +```js +let [a, ...b] = [1, 2, 3, 4]; +// a => 1 b => [2,3,4] + +let [a, ...b] = []; +// a => undefined b => [] + +let [a, ...b] = ["abc"]; +// a => "abc" b => [] +``` + +#### 1.7.2 Array.from() +将 **类数组对象** 和 **可遍历的对象**,转换成真正的数组。 +```js +// 类数组对象 +let a = { + '0':'a', + '1':'b', + length:2 +} +let arr = Array.from(a); + +// 可遍历的对象 +let a = Array.from([1,2,3]); +let b = Array.from({length: 3}); +let c = Array.from([1,2,3]).map(x => x * x); +let d = Array.from([1,2,3].map(x => x * x)); +``` + +#### 1.7.3 Array.of() +将一组数值,转换成**数组**,弥补`Array`方法参数不同导致的差异。 +```js +Array.of(1,2,3); // [1,2,3] +Array.of(1).length; // 1 + +Array(); // [] +Array(2); // [,] 1个参数时,为指定数组长度 +Array(1,2,3); // [1,2,3] 多于2个参数,组成新数组 +``` + +#### 1.7.4 find()和findIndex() +`find()`方法用于找出第一个符合条件的数组成员,参数为一个回调函数,所有成员依次执行该回调函数,返回第一个返回值为`true`的成员,如果没有一个符合则返回`undefined`。 +```js +[1,2,3,4,5].find( a => a < 3 ); // 1 +``` +回调函数接收三个参数,当前值、当前位置和原数组。 +```js +[1,2,3,4,5].find((value, index, arr){ + // ... +}); +``` +`findIndex()`方法与`find()`类似,返回第一个符合条件的数组成员的**位置**,如果都不符合则返回`-1`。 +```js +[1,2,3,4].findIndex((v,i,a)=>{ + return v>2; +}); // 2 +``` + +#### 1.7.5 fill() +用于用指定值**填充**一个数组,通常用来**初始化空数组**,并抹去数组中已有的元素。 +```js +new Array(3).fill('a'); // ['a','a','a'] +[1,2,3].fill('a'); // ['a','a','a'] +``` +并且`fill()`的第二个和第三个参数指定填充的**起始位置**和**结束位置**。 +```js +[1,2,3].fill('a',1,2); +``` + +#### 1.7.6 entries(),keys(),values() +主要用于遍历数组,`entries()`对键值对遍历,`keys()`对键名遍历,`values()`对键值遍历。 +```js +for (let i of ['a', 'b'].keys()){ + console.log(i) +} +// 0 +// 1 + +for (let e of ['a', 'b'].keys()){ + console.log(e) +} +// 'a' +// 'b' + +for (let e of ['a', 'b'].keys()){ + console.log(e) +} +// 0 'a' +// 1 'b' +``` + +#### 1.7.7 includes() +用于表示数组是否包含给定的值,与字符串的`includes`方法类似。 +```js +[1,2,3].includes(2); // true +[1,2,3].includes(4); // false +[1,2,NaN].includes(NaN); // true +``` +第二个参数为**起始位置**,默认为`0`,如果负数,则表示倒数的位置,如果大于数组长度,则重置为`0`开始。 +```js +[1,2,3].includes(3,3); // false +[1,2,3].includes(3,4); // false +[1,2,3].includes(3,-1); // true +[1,2,3].includes(3,-4); // true +``` + +#### 1.7.8 flat(),flatMap() +`flat()`用于将数组一维化,返回一个新数组,不影响原数组。 +默认一次只一维化一层数组,若需多层,则传入一个整数参数指定层数。 +若要一维化所有层的数组,则传入`Infinity`作为参数。 +```js +[1, 2, [2,3]].flat(); // [1,2,2,3] +[1,2,[3,[4,[5,6]]]].flat(3); // [1,2,3,4,5,6] +[1,2,[3,[4,[5,6]]]].flat('Infinity'); // [1,2,3,4,5,6] +``` +`flatMap()`是将原数组每个对象先执行一个函数,在对返回值组成的数组执行`flat()`方法,返回一个新数组,不改变原数组。 + `flatMap()`只能展开一层。 +```js +[2, 3, 4].flatMap((x) => [x, x * 2]); +// [2, 4, 3, 6, 4, 8] +``` + +[⬆ 返回目录](#二目录) + + +### 1.8 对象的拓展 +#### 1.8.1 属性的简洁表示 +```js +let a = 'a1'; +let b = { a }; // b => { a : 'a1' } +// 等同于 +let b = { a : a }; + +function f(a, b){ + return {a, b}; +} +// 等同于 +function f (a, b){ + return {a:a ,b:b}; +} + +let a = { + fun () { + return 'leo'; + } +} +// 等同于 +let a = { + fun : function(){ + return 'leo'; + } +} +``` + +#### 1.8.2 属性名表达式 +`JavaScript`提供2种方法**定义对象的属性**。 +```js +// 方法1 标识符作为属性名 +a.f = true; + +// 方法2 字符串作为属性名 +a['f' + 'un'] = true; +``` +延伸出来的还有: +```js +let a = 'hi leo'; +let b = { + [a]: true, + ['a'+'bc']: 123, + ['my' + 'fun'] (){ + return 'hi'; + } +}; +// b.a => undefined ; b.abc => 123 ; b.myfun() => 'hi' +// b[a] => 'hi leo' ; b['abc'] => 123 ; b['myfun'] => 'hi' +``` +**注意**: +属性名表达式不能与简洁表示法同时使用,否则报错。 +```js +// 报错 +let a1 = 'aa'; +let a2 = 'bb'; +let b1 = {[a1]}; + +// 正确 +let a1 = 'aa'; +let b1 = { [a1] : 'bb'}; +``` + +#### 1.8.3 Object.is() +`.Object.is()` 用于比较两个值是否严格相等,在ES5时候只要使用**相等运算符**(`==`)和**严格相等运算符**(`===`)就可以做比较,但是它们都有缺点,前者会**自动转换数据类型**,后者的`NaN`不等于自身,以及`+0`等于`-0`。 +```js +Object.is('a','a'); // true +Object.is({}, {}); // false + +// ES5 ++0 === -0 ; // true +NaN === NaN; // false + +// ES6 +Object.is(+0,-0); // false +Object.is(NaN,NaN); // true +``` + +#### 1.8.4 Object.assign() +`Object.assign()`方法用于对象的合并,将原对象的所有可枚举属性复制到目标对象。 +**基础用法**: +第一个参数是**目标对象**,后面参数都是**源对象**。 +```js +let a = {a:1}; +let b = {b:2}; +Object.assign(a,b); // a=> {a:1,b:2} +``` +**注意**: +* 若目标对象与源对象有同名属性,则后面属性会覆盖前面属性。 +```js +let a = {a:1, b:2}; +let b = {b:3, c:4}; +Object.assign(a, b); // a => {a:1, b:3, c:4} +``` +* 若只有**一个**参数,则返回该参数。 +```js +let a = {a:1}; +Object.assign(a) === a; // true +``` +* 若参数**不是对象**,则先转成对象后返回。 +```js +typeof Object.assign(2); // 'object' +``` +* 由于`undefined`或`NaN`无法转成对象,所以做为参数会报错。 +```js +Object.assign(undefined) // 报错 +Object.assign(NaN); // 报错 +``` +* `Object.assign()`实现的是浅拷贝。 + +`Object.assign()`拷贝得到的是这个对象的引用。这个对象的任何变化,都会反映到目标对象上面。 +```js +let a = {a: {b:1}}; +let b = Object.assign({},a); +a.a.b = 2; +console.log(b.a.b); // 2 +``` +* 将数组当做对象处理,键名为数组下标,键值为数组下标对应的值。 +```js +Object.assign([1, 2, 3], [4, 5]); // [4, 5, 3] +``` + +[⬆ 返回目录](#二目录) + + +### 1.9 Symbol +#### 1.9.1 介绍 +ES6引入`Symbol`作为一种新的**原始数据类型**,表示**独一无二**的值,主要是为了**防止属性名冲突**。 +ES6之后,JavaScript一共有其中数据类型:`Symbol`、`undefined`、`null`、`Boolean`、`String`、`Number`、`Object`。 +简单实用: +```js +let a = Symbol(); +typeof a; // "symbol" +``` +**注意:** +* `Symbol`函数不能用`new`,会报错。由于`Symbol`是一个原始类型,不是对象,所以不能添加属性,它是类似于字符串的数据类型。 +* `Symbol`都是不相等的,即使参数相同。 +```js +let a1 = Symbol(); +let a2 = Symbal(); +a1 === a2; // false + +let a1 = Symbol('abc'); +let a2 = Symbal('abc'); +a1 === a2; // false +``` +* `Symbol`不能与其他类型的值计算,会报错。 +```js +let a = Symbol('hello'); +a + " world!"; // 报错 +`${a} world!`; // 报错 +``` + +#### 1.9.2 更多介绍 +详细介绍[参考阮一峰老师的ES6 Symbol介绍](http://es6.ruanyifeng.com/#docs/symbol) + + +[⬆ 返回目录](#二目录) + + +### 1.10 Set和Map数据结构 +#### 1.10.1 Set +**介绍**: +`Set`数据结构类似数组,但所有成员的值**唯一**。 +`Set`本身为一个构造函数,用来生成`Set`数据结构,使用`add`方法来添加新成员。 +```js +let a = new Set(); +[1,2,2,1,3,4,5,4,5].forEach(x=>a.add(x)); +for(let k of a){ + console.log(k) +}; +// 1 2 3 4 5 +``` +**基础使用**: +```js +let a = new Set([1,2,3,3,4]); +[...a]; // [1,2,3,4] +a.size; // 4 + +// 数组去重 +[...new Set([1,2,3,4,4,4])];// [1,2,3,4] +``` + +**注意**: +* 向`Set`中添加值的时候,不会类型转换,即`5`和`'5'`是不同的。 +```js +[...new Set([5,'5'])]; // [5, "5"] +``` + +**属性和方法**: +* 属性: + * `Set.prototype.constructor`:构造函数,默认就是`Set`函数。 + * `Set.prototype.size`:返回`Set`实例的成员总数。 + +* 操作方法: + * `add(value)`:添加某个值,返回 Set 结构本身。 + * `delete(value)`:删除某个值,返回一个布尔值,表示删除是否成功。 + * `has(value)`:返回一个布尔值,表示该值是否为Set的成员。 + * `clear()`:清除所有成员,没有返回值。 + +```js +let a = new Set(); +a.add(1).add(2); // a => Set(2) {1, 2} +a.has(2); // true +a.has(3); // false +a.delete(2); // true a => Set(1) {1} +a.clear(); // a => Set(0) {} +``` +**数组去重**: +```js +let a = new Set([1,2,3,3,3,3]); +``` +#### 1.10.2 Set的应用 +**数组去重**: +```js +// 方法1 +[...new Set([1,2,3,4,4,4])]; // [1,2,3,4] +// 方法2 +Array.from([1,2,3,4,4,4]); // [1,2,3,4] +``` +**遍历和过滤**: +```js +let a = new Set([1,2,3,4]); + +// map 遍历操作 +let b = new Set([...a].map(x =>x*2));// b => Set(4) {2,4,6,8} + +// filter 过滤操作 +let c = new Set([...a].filter(x =>(x%2) == 0)); // b => Set(2) {2,4} +``` +**获取并集、交集和差集**: +```js +let a = new Set([1,2,3]); +let b = new Set([4,3,2]); + +// 并集 +let c1 = new Set([...a, ...b]); // Set {1,2,3,4} + +// 交集 +let c2 = new Set([...a].filter(x => b.has(x))); // set {2,3} + +// 差集 +let c3 = new Set([...a].filter(x => !b.has(x))); // set {1,4} +``` + +* 遍历方法: + * `keys()`:返回**键名**的遍历器。 + * `values()`:返回**键值**的遍历器。 + * `entries()`:返回**键值对**的遍历器。 + * `forEach()`:使用回调函数遍历**每个成员**。 + +`Set`遍历顺序是**插入顺序**,当保存多个回调函数,只需按照顺序调用。但由于`Set`结构**没有键名只有键值**,所以`keys()`和`values()`是返回结果相同。 +```js +let a = new Set(['a','b','c']); +for(let i of a.keys()){console.log(i)}; // 'a' 'b' 'c' +for(let i of a.values()){console.log(i)}; // 'a' 'b' 'c' +for(let i of a.entries()){console.log(i)}; +// ['a','a'] ['b','b'] ['c','c'] +``` +并且 还可以使用`for...of`直接遍历`Set`。 +```js +let a = new Set(['a','b','c']); +for(let k of a){console.log(k)}; // 'a' 'b' 'c' +``` +`forEach`与数组相同,对每个成员执行操作,且无返回值。 +```js +let a = new Set(['a','b','c']); +a.forEach((v,k) => console.log(k + ' : ' + v)); +``` + + +#### 1.10.3 Map +由于传统的`JavaScript`对象只能用字符串当做键,给开发带来很大限制,ES6增加`Map`数据结构,使得**各种类型的值**(包括对象)都可以作为键。 +`Map`结构提供了“**值—值**”的对应,是一种更完善的 Hash 结构实现。 +**基础使用**: +```js +let a = new Map(); +let b = {name: 'leo' }; +a.set(b,'my name'); // 添加值 +a.get(b); // 获取值 +a.size; // 获取总数 +a.has(b); // 查询是否存在 +a.delete(b); // 删除一个值 +a.clear(); // 清空所有成员 无返回 +``` +**注意**: +* 传入数组作为参数,**指定键值对的数组**。 +```js +let a = new Map([ + ['name','leo'], + ['age',18] +]) +``` +* 如果对同一个键**多次赋值**,后面的值将**覆盖前面的值**。 +```js +let a = new Map(); +a.set(1,'aaa').set(1,'bbb'); +a.get(1); // 'bbb' +``` +* 如果读取一个未知的键,则返回`undefined`。 +```js +new Map().get('abcdef'); // undefined +``` +* **同样的值**的两个实例,在 Map 结构中被视为两个键。 +```js +let a = new Map(); +let a1 = ['aaa']; +let a2 = ['aaa']; +a.set(a1,111).set(a2,222); +a.get(a1); // 111 +a.get(a2); // 222 +``` +**遍历方法**: +Map 的遍历顺序就是插入顺序。 +* `keys()`:返回键名的遍历器。 +* `values()`:返回键值的遍历器。 +* `entries()`:返回所有成员的遍历器。 +* `forEach()`:遍历 Map 的所有成员。 +```js +let a = new Map([ + ['name','leo'], + ['age',18] +]) + +for (let i of a.keys()){...}; +for (let i of a.values()){...}; +for (let i of a.entries()){...}; +a.forEach((v,k,m)=>{ + console.log(`key:${k},value:${v},map:${m}`) +}) +``` +**将Map结构转成数组结构**: +```js +let a = new Map([ + ['name','leo'], + ['age',18] +]) + +let a1 = [...a.keys()]; // a1 => ["name", "age"] +let a2 = [...a.values()]; // a2 =>  ["leo", 18] +let a3 = [...a.entries()];// a3 => [['name','leo'], ['age',18]] +``` + +#### 1.10.4 Map与其他数据结构互相转换 +* Map 转 数组 +```js +let a = new Map().set(true,1).set({f:2},['abc']); +[...a]; // [[true:1], [ {f:2},['abc'] ]] +``` +* 数组 转 Map +```js +let a = [ ['name','leo'], [1, 'hi' ]] +let b = new Map(a); +``` +* Map 转 对象 +如果所有 Map 的键都是字符串,它可以无损地转为对象。 +如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名。 +```js +function fun(s) { + let obj = Object.create(null); + for (let [k,v] of s) { + obj[k] = v; + } + return obj; +} + +const a = new Map().set('yes', true).set('no', false); +fun(a) +// { yes: true, no: false } +``` +* 对象 转 Map +```js +function fun(obj) { + let a = new Map(); + for (let k of Object.keys(obj)) { + a.set(k, obj[k]); + } + return a; +} + +fun({yes: true, no: false}) +// Map {"yes" => true, "no" => false} +``` + +* Map 转 JSON +**(1)Map键名都是字符串,转为对象JSON:** +```js +function fun (s) { + let obj = Object.create(null); + for (let [k,v] of s) { + obj[k] = v; + } + return JSON.stringify(obj) +} +let a = new Map().set('yes', true).set('no', false); +fun(a); +// '{"yes":true,"no":false}' +``` +**(2)Map键名有非字符串,转为数组JSON:** +```js +function fun (map) { + return JSON.stringify([...map]); +} + +let a = new Map().set(true, 7).set({foo: 3}, ['abc']); +fun(a) +// '[[true,7],[{"foo":3},["abc"]]]' +``` +* JSON 转 Map +**(1)所有键名都是字符串:** +```js +function fun (s) { + let strMap = new Map(); + for (let k of Object.keys(s)) { + strMap.set(k, s[k]); + } + return strMap; + return JSON.parse(strMap); +} +fun('{"yes": true, "no": false}') +// Map {'yes' => true, 'no' => false} +``` +**(2)整个 JSON 就是一个数组,且每个数组成员本身,又是一个有两个成员的数组**: +```js +function fun2(s) { + return new Map(JSON.parse(s)); +} +fun2('[[true,7],[{"foo":3},["abc"]]]') +// Map {true => 7, Object {foo: 3} => ['abc']} +``` + +[⬆ 返回目录](#二目录) + + +### 1.11 Proxy +`proxy` 用于修改某些操作的**默认行为**,可以理解为一种拦截外界对目标对象访问的一种机制,从而对外界的访问进行过滤和修改,即代理某些操作,也称“**代理器**”。 +#### 1.11.1 基础使用 +`proxy`实例化需要传入两个参数,`target`参数表示所要拦截的目标对象,`handler`参数也是一个对象,用来定制拦截行为。 +```js +let p = new Proxy(target, handler); + +let a = new Proxy({}, { + get: function (target, handler){ + return 'leo'; + } +}) +a.name; // leo +a.age; // leo +a.abcd; // leo +``` +上述`a`实例中,在第二个参数中定义了`get`方法,来拦截外界访问,并且`get`方法接收两个参数,分别是**目标对象**和**所要访问的属性**,所以不管外部访问对象中任何属性都会执行`get`方法返回`leo`。 +**注意**: +* 只能使用`Proxy`实例的对象才能使用这些操作。 +* 如果`handler`没有设置拦截,则直接返回原对象。 +```js +let target = {}; +let handler = {}; +let p = new Proxy(target, handler); +p.a = 'leo'; +target.a; // 'leo' +``` +**同个拦截器函数,设置多个拦截操作**: +```js +let p = new Proxy(function(a, b){ + return a + b; +},{ + get:function(){ + return 'get方法'; + }, + apply:function(){ + return 'apply方法'; + } +}) +``` + +**`Proxy`支持的13种拦截操作**: +13种拦截操作的详细介绍:[打开阮一峰老师的链接](http://es6.ruanyifeng.com/#docs/proxy)。 +* `get(target, propKey, receiver)`: +拦截对象属性的读取,比如proxy.foo和proxy['foo']。 + +* `set(target, propKey, value, receiver)`: +拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值。 + +* `has(target, propKey)`: +拦截propKey in proxy的操作,返回一个布尔值。 + +* `deleteProperty(target, propKey)`: +拦截delete proxy[propKey]的操作,返回一个布尔值。 + +* `ownKeys(target)`: +拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。 + +* `getOwnPropertyDescriptor(target, propKey)`: +拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。 + +* `defineProperty(target, propKey, propDesc)`: +拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。 + +* `preventExtensions(target)`: +拦截Object.preventExtensions(proxy),返回一个布尔值。 + +* `getPrototypeOf(target)`: +拦截Object.getPrototypeOf(proxy),返回一个对象。 + +* `isExtensible(target)`: +拦截Object.isExtensible(proxy),返回一个布尔值。 + +* `setPrototypeOf(target, proto)`: +拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。 + +* `apply(target, object, args)`: +拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。 + +* `construct(target, args)`: +拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)。 + +#### 1.11.2 取消Proxy实例 +使用`Proxy.revocale`方法取消`Proxy`实例。 +```js +let a = {}; +let b = {}; +let {proxy, revoke} = Proxy.revocale(a, b); + +proxy.name = 'leo'; // 'leo' +revoeke(); +proxy.name; // TypeError: Revoked +``` + +#### 1.11.3 实现 Web服务的客户端 +```js +const service = createWebService('http://le.com/data'); +service.employees().than(json =>{ + const employees = JSON.parse(json); +}) + +function createWebService(url){ + return new Proxy({}, { + get(target, propKey, receiver{ + return () => httpGet(url+'/'+propKey); + }) + }) +} +``` + +### 1.12 Promise对象 +#### 1.12.1 概念 +主要用途:**解决异步编程带来的回调地狱问题**。 +把`Promise`简单理解一个容器,存放着某个未来才会结束的事件(通常是一个异步操作)的结果。通过`Promise`对象来获取异步操作消息,处理各种异步操作。 + +**`Promise`对象2特点**: +* **对象的状态不受外界影响**。 +> `Promise`对象代表一个异步操作,有三种状态:**pending(进行中)**、**fulfilled(已成功)**和**rejected(已失败)**。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是`Promise`这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。 + +* **一旦状态改变,就不会再变,任何时候都可以得到这个结果**。 +> Promise对象的状态改变,只有两种可能:从**pending**变为**fulfilled**和从**pending**变为**rejected**。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 **resolved**(已定型)。如果改变已经发生了,你再对**Promise**对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。 + +注意,为了行文方便,本章后面的`resolve`d统一只指`fulfilled`状态,不包含`rejected`状态。 + +**`Promise`缺点** +* **无法取消**Promise,一旦新建它就会立即执行,无法中途取消。 +* 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。 +* 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。 + +#### 1.12.2 基本使用 +`Promise`为一个构造函数,需要用`new`来实例化。 +```js +let p = new Promise(function (resolve, reject){ + if(/*异步操作成功*/){ + resolve(value); + } else { + reject(error); + } +}) +``` +`Promise`接收一个函数作为参数,该函数两个参数`resolve`和`reject`,有JS引擎提供。 +* `resolve`作用是将`Promise`的状态从pending变成resolved,在异步操作成功时调用,返回异步操作的结果,作为参数传递出去。 +* `reject`作用是将`Promise`的状态从pending变成rejected,在异步操作失败时报错,作为参数传递出去。 + +`Promise`实例生成以后,可以用`then`方法分别指定`resolved`状态和`rejected`状态的回调函数。 +```js +p.then(function(val){ + // success... +},function(err){ + // error... +}) +``` + +**几个例子来理解** : +* 当一段时间过后,`Promise`状态便成为`resolved`触发`then`方法绑定的回调函数。 +```js +function timeout (s){ + return new Promise((resolve, reject){ + setTimeout(result,ms, 'done'); + }) +} +timeout(100).then(val => { + console.log(val); +}) +``` + +* `Promise`新建后立刻执行。 +```js +let p = new Promise(function(resolve, reject){ + console.log(1); + resolve(); +}) +p.then(()=>{ + console.log(2); +}) +console.log(3); +// 1 +// 2 +// 3 +``` + +**异步加载图片**: +```js +function f(url){ + return new Promise(function(resolve, reject){ + const img = new Image (); + img.onload = function(){ + resolve(img); + } + img.onerror = function(){ + reject(new Error( + 'Could not load image at ' + url + )); + } + img.src = url; + }) +} +``` + +**`resolve`函数和`reject`函数的参数为`resolve`函数或`reject`函数**: +`p1`的状态决定了`p2`的状态,所以`p2`要等待`p1`的结果再执行回调函数。 +```js +const p1 = new Promise(function (resolve, reject) { + setTimeout(() => reject(new Error('fail')), 3000) +}) + +const p2 = new Promise(function (resolve, reject) { + setTimeout(() => resolve(p1), 1000) +}) + +p2 + .then(result => console.log(result)) + .catch(error => console.log(error)) +// Error: fail +``` + +**调用`resolve`或`reject`不会结束`Promise`参数函数的执行,除了`return`**: +```js +new Promise((resolve, reject){ + resolve(1); + console.log(2); +}).then(r => { + console.log(3); +}) +// 2 +// 1 + +new Promise((resolve, reject){ + return resolve(1); + console.log(2); +}) +// 1 +``` + +#### 1.12.3 Promise.prototype.then() +作用是为`Promise`添加状态改变时的回调函数,`then`方法的第一个参数是`resolved`状态的回调函数,第二个参数(可选)是`rejected`状态的回调函数。 +`then`方法返回一个新`Promise`实例,与原来`Promise`实例不同,因此可以使用链式写法,上一个`then`的结果作为下一个`then`的参数。 +```js +getJSON("/posts.json").then(function(json) { + return json.post; +}).then(function(post) { + // ... +}); +``` + +#### 1.12.4 Promise.prototype.catch() +`Promise.prototype.catch`方法是`.then(null, rejection)`的别名,用于指定发生错误时的回调函数。 +```js +getJSON('/posts.json').then(function(posts) { + // ... +}).catch(function(error) { + // 处理 getJSON 和 前一个回调函数运行时发生的错误 + console.log('发生错误!', error); +}); +``` +如果 `Promise` 状态已经变成`resolved`,再抛出错误是无效的。 +```js +const p = new Promise(function(resolve, reject) { + resolve('ok'); + throw new Error('test'); +}); +p + .then(function(value) { console.log(value) }) + .catch(function(error) { console.log(error) }); +// ok +``` +当`promise`抛出一个错误,就被`catch`方法指定的回调函数捕获,下面三种写法相同。 +```js +// 写法一 +const p = new Promise(function(resolve, reject) { + throw new Error('test'); +}); +p.catch(function(error) { + console.log(error); +}); +// Error: test + +// 写法二 +const p = new Promise(function(resolve, reject) { + try { + throw new Error('test'); + } catch(e) { + reject(e); + } +}); +p.catch(function(error) { + console.log(error); +}); + +// 写法三 +const p = new Promise(function(resolve, reject) { + reject(new Error('test')); +}); +p.catch(function(error) { + console.log(error); +}); +``` +一般来说,不要在`then`方法里面定义` Reject` 状态的回调函数(即`then`的第二个参数),总是使用`catch`方法。 +```js +// bad +promise + .then(function(data) { + // success + }, function(err) { + // error + }); + +// good +promise + .then(function(data) { //cb + // success + }) + .catch(function(err) { + // error + }); +``` + +#### 1.12.5 Promise.prototype.finally() +`finally`方法用于指定不管 `Promise` 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。 +```js +promise +.then(result => {···}) +.catch(error => {···}) +.finally(() => {···}); +``` +`finally`不接收任何参数,与状态无关,本质上是`then`方法的特例。 +```js +promise +.finally(() => { + // 语句 +}); + +// 等同于 +promise +.then( + result => { + // 语句 + return result; + }, + error => { + // 语句 + throw error; + } +); +``` +上面代码中,如果不使用`finally`方法,同样的语句需要为成功和失败两种情况各写一次。有了`finally`方法,则只需要写一次。 +`finally`方法总是会返回原来的值。 +```js +// resolve 的值是 undefined +Promise.resolve(2).then(() => {}, () => {}) + +// resolve 的值是 2 +Promise.resolve(2).finally(() => {}) + +// reject 的值是 undefined +Promise.reject(3).then(() => {}, () => {}) + +// reject 的值是 3 +Promise.reject(3).finally(() => {}) +``` + +#### 1.12.6 Promise.all() +用于将多个 `Promise` 实例,包装成一个新的 `Promise` 实例,参数可以不是数组,但必须是Iterator接口,且返回的每个成员都是`Promise`实例。 +```js +const p = Promise.all([p1, p2, p3]); +``` +`p`的状态由`p1`、`p2`、`p3`决定,分成**两种**情况。 +1. 只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。 +2. 只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。 + +```js +// 生成一个Promise对象的数组 +const promises = [2, 3, 5, 7, 11, 13].map(function (id) { + return getJSON('/post/' + id + ".json"); +}); + +Promise.all(promises).then(function (posts) { + // ... +}).catch(function(reason){ + // ... +}); +``` +上面代码中,`promises`是包含 6 个 Promise 实例的数组,只有这 6 个实例的状态都变成`fulfilled`,或者其中有一个变为`rejected`,才会调用`Promise.all`方法后面的回调函数。 + +**注意**:如果`Promise`的参数中定义了`catch`方法,则`rejected`后不会触发`Promise.all()`的`catch`方法,因为参数中的`catch`方法执行完后也会变成`resolved`,当`Promise.all()`方法参数的实例都是`resolved`时就会调用`Promise.all()`的`then`方法。 +```js +const p1 = new Promise((resolve, reject) => { + resolve('hello'); +}) +.then(result => result) +.catch(e => e); + +const p2 = new Promise((resolve, reject) => { + throw new Error('报错了'); +}) +.then(result => result) +.catch(e => e); + +Promise.all([p1, p2]) +.then(result => console.log(result)) +.catch(e => console.log(e)); +// ["hello", Error: 报错了] +``` + +**如果参数里面都没有catch方法,就会调用Promise.all()的catch方法。** +```js +const p1 = new Promise((resolve, reject) => { + resolve('hello'); +}) +.then(result => result); + +const p2 = new Promise((resolve, reject) => { + throw new Error('报错了'); +}) +.then(result => result); + +Promise.all([p1, p2]) +.then(result => console.log(result)) +.catch(e => console.log(e)); +// Error: 报错了 +``` + +#### 1.12.7 Promise.race() +与`Promise.all`方法类似,也是将多个`Promise`实例包装成一个新的`Promise`实例。 +```js +const p = Promise.race([p1, p2, p3]); +``` +与`Promise.all`方法区别在于,`Promise.race`方法是`p1`, `p2`, `p3`中只要一个参数先改变状态,就会把这个参数的返回值传给`p`的回调函数。 + +#### 1.12.8 Promise.resolve() +将现有对象转换成 `Promise` 对象。 +```js +const p = Promise.resolve($.ajax('/whatever.json')); +``` + +#### 1.12.9 Promise.reject() +返回一个`rejected`状态的`Promise`实例。 +```js +const p = Promise.reject('出错了'); +// 等同于 +const p = new Promise((resolve, reject) => reject('出错了')) + +p.then(null, function (s) { + console.log(s) +}); +// 出错了 +``` +注意,`Promise.reject()`方法的参数,会原封不动地作为`reject`的理由,变成后续方法的参数。这一点与`Promise.resolve`方法不一致。 +```js +const thenable = { + then(resolve, reject) { + reject('出错了'); + } +}; + +Promise.reject(thenable) +.catch(e => { + console.log(e === thenable) +}) +// true +``` + + +[⬆ 返回目录](#二目录) + +### 1.13 Iterator和 for...of循环 +#### 1.13.1 Iterator遍历器概念 +> **Iterator**是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 **Iterator** 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。 + +**Iterator三个作用**: +* 为各种数据结构,提供一个**统一**的、**简便**的访问接口; +* 使得数据结构的成员能够按某种次序排列; +* **Iterator** 接口主要供ES6新增的`for...of`消费; + +#### 1.13.2 Iterator遍历过程 +1. 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。 +2. 第一次调用指针对象的`next`方法,可以将指针指向数据结构的第一个成员。 +3. 第二次调用指针对象的`next`方法,指针就指向数据结构的第二个成员。 +4. 不断调用指针对象的`next`方法,直到它指向数据结构的结束位置。 + +每一次调用`next`方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含`value`和`done`两个属性的对象。 + +* `value`属性是当前成员的值; +* `done`属性是一个布尔值,表示遍历是否结束; + +模拟`next`方法返回值: +```js +let f = function (arr){ + var nextIndex = 0; + return { + next:function(){ + return nextIndex < arr.length ? + {value: arr[nextIndex++], done: false}: + {value: undefined, done: true} + } + } +} + +let a = f(['a', 'b']); +a.next(); // { value: "a", done: false } +a.next(); // { value: "b", done: false } +a.next(); // { value: undefined, done: true } +``` + +#### 1.13.3 默认Iterator接口 +若数据**可遍历**,即一种数据部署了Iterator接口。 +ES6中默认的Iterator接口部署在数据结构的`Symbol.iterator`属性,即如果一个数据结构具有`Symbol.iterator`属性,就可以认为是**可遍历**。 +`Symbol.iterator`属性本身是函数,是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名`Symbol.iterator`,它是一个表达式,返回`Symbol`对象的`iterator`属性,这是一个预定义好的、类型为 Symbol 的特殊值,所以要放在方括号内(参见《Symbol》一章)。 + +**原生具有Iterator接口的数据结构有**: +* Array +* Map +* Set +* String +* TypedArray +* 函数的 arguments 对象 +* NodeList 对象 + +#### 1.13.4 Iterator使用场景 +* **(1)解构赋值** +对数组和 `Set` 结构进行解构赋值时,会默认调用`Symbol.iterator`方法。 +```js +let a = new Set().add('a').add('b').add('c'); +let [x, y] = a; // x = 'a' y = 'b' +let [a1, ...a2] = a; // a1 = 'a' a2 = ['b','c'] +``` + +* **(2)扩展运算符** +扩展运算符(`...`)也会调用默认的 Iterator 接口。 +```js +let a = 'hello'; +[...a]; // ['h','e','l','l','o'] + +let a = ['b', 'c']; +['a', ...a, 'd']; // ['a', 'b', 'c', 'd'] +``` + +* **(2)yield*** +`yield*`后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。 +```js +let a = function*(){ + yield 1; + yield* [2,3,4]; + yield 5; +} + +let b = a(); +b.next() // { value: 1, done: false } +b.next() // { value: 2, done: false } +b.next() // { value: 3, done: false } +b.next() // { value: 4, done: false } +b.next() // { value: 5, done: false } +b.next() // { value: undefined, done: true } +``` + +* **(4)其他场合** +由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口。下面是一些例子。 + +* for...of +* Array.from() +* Map(), Set(), WeakMap(), WeakSet()(比如`new Map([['a',1],['b',2]])`) +* Promise.all() +* Promise.race() + +#### 1.13.5 for...of循环 +只要数据结构部署了`Symbol.iterator`属性,即具有 iterator 接口,可以用`for...of`循环遍历它的成员。也就是说,`for...of`循环内部调用的是数据结构的`Symbol.iterato`方法。 +**使用场景**: +`for...of`可以使用在**数组**,**`Set`和`Map`结构**,**类数组对象**,**Genetator对象**和**字符串**。 + +* **数组** +`for...of`循环可以代替数组实例的`forEach`方法。 +```js +let a = ['a', 'b', 'c']; +for (let k of a){console.log(k)}; // a b c + +a.forEach((ele, index)=>{ + console.log(ele); // a b c + console.log(index); // 0 1 2 +}) +``` +与`for...in`对比,`for...in`只能获取对象键名,不能直接获取键值,而`for...of`允许直接获取键值。 +```js +let a = ['a', 'b', 'c']; +for (let k of a){console.log(k)}; // a b c +for (let k in a){console.log(k)}; // 0 1 2 +``` + +* **Set和Map** +可以使用数组作为变量,如`for (let [k,v] of b){...}`。 +```js +let a = new Set(['a', 'b', 'c']); +for (let k of a){console.log(k)}; // a b c + +let b = new Map(); +b.set('name','leo'); +b.set('age', 18); +b.set('aaa','bbb'); +for (let [k,v] of b){console.log(k + ":" + v)}; +// name:leo +// age:18 +// aaa:bbb +``` + +* **类数组对象** +```js +// 字符串 +let a = 'hello'; +for (let k of a ){console.log(k)}; // h e l l o + +// DOM NodeList对象 +let b = document.querySelectorAll('p'); +for (let k of b ){ + k.classList.add('test'); +} + +// arguments对象 +function f(){ + for (let k of arguments){ + console.log(k); + } +} +f('a','b'); // a b +``` + +* **对象** +普通对象不能直接使用`for...of`会报错,要部署Iterator才能使用。 +```js +let a = {a:'aa',b:'bb',c:'cc'}; +for (let k in a){console.log(k)}; // a b c +for (let k of a){console>log(k)}; // TypeError +``` + +#### 1.13.6 跳出for...of +使用`break`来实现。 +```js +for (let k of a){ + if(k>100) + break; + console.log(k); +} +``` + +[⬆ 返回目录](#二目录) + + + +### 1.14 Generator函数和应用 +#### 1.14.1 基本概念 +`Generator`函数是一种异步编程解决方案。 +**原理**: +执行`Genenrator`函数会返回一个遍历器对象,依次遍历`Generator`函数内部的每一个状态。 +`Generator`函数是一个普通函数,有以下两个特征: +* `function`关键字与函数名之间有个星号; +* 函数体内使用`yield`表达式,定义不同状态; + +通过调用`next`方法,将指针移向下一个状态,直到遇到下一个`yield`表达式(或`return`语句)为止。简单理解,`Generator`函数分段执行,`yield`表达式是暂停执行的标记,而`next`恢复执行。 +```js +function * f (){ + yield 'hi'; + yield 'leo'; + return 'ending'; +} +let a = f(); +a.next(); // {value: 'hi', done : false} +a.next(); // {value: 'leo', done : false} +a.next(); // {value: 'ending', done : true} +a.next(); // {value: undefined, done : false} +``` + +#### 1.14.2 yield表达式 +`yield`表达式是暂停标志,遍历器对象的`next`方法的运行逻辑如下: +1. 遇到`yield`就暂停执行,将这个`yield`后的表达式的值,作为返回对象的`value`属性值。 +2. 下次调用`next`往下执行,直到遇到下一个`yield`。 +3. 直到函数结束或者`return`为止,并返回`return`语句后面表达式的值,作为返回对象的`value`属性值。 +4. 如果该函数没有`return`语句,则返回对象的`value`为`undefined` 。 + +**注意:** +* `yield`只能用在`Generator`函数里使用,其他地方使用会报错。 +```js +// 错误1 +(function(){ + yiled 1; // SyntaxError: Unexpected number +})() + +// 错误2 forEach参数是个普通函数 +let a = [1, [[2, 3], 4], [5, 6]]; +let f = function * (i){ + i.forEach(function(m){ + if(typeof m !== 'number'){ + yield * f (m); + }else{ + yield m; + } + }) +} +for (let k of f(a)){ + console.log(k) +} +``` + +* `yield`表达式如果用于另一个表达式之中,必须放在**圆括号**内。 +```js +function * a (){ + console.log('a' + yield); // SyntaxErro + console.log('a' + yield 123); // SyntaxErro + console.log('a' + (yield)); // ok + console.log('a' + (yield 123)); // ok +} +``` + +* `yield`表达式用做函数参数或放在表达式右边,可以**不加括号**。 +```js +function * a (){ + f(yield 'a', yield 'b'); // ok + lei i = yield; // ok +} +``` + +#### 1.14.3 next方法 +`yield`本身没有返回值,或者是总返回`undefined`,`next`方法可带一个参数,作为上一个`yield`表达式的返回值。 +```js +function * f (){ + for (let k = 0; true; k++){ + let a = yield k; + if(a){k = -1}; + } +} +let g =f(); +g.next(); // {value: 0, done: false} +g.next(); // {value: 1, done: false} +g.next(true); // {value: 0, done: false} +``` +这一特点,可以让`Generator`函数开始执行之后,可以从外部向内部注入不同值,从而调整函数行为。 +```js +function * f(x){ + let y = 2 * (yield (x+1)); + let z = yield (y/3); + return (x + y + z); +} +let a = f(5); +a.next(); // {value : 6 ,done : false} +a.next(); // {value : NaN ,done : false} +a.next(); // {value : NaN ,done : true} +// NaN因为yeild返回的是对象 和数字计算会NaN + +let b = f(5); +b.next(); // {value : 6 ,done : false} +b.next(12); // {value : 8 ,done : false} +b.next(13); // {value : 42 ,done : false} +// x 5 y 24 z 13 +``` + +#### 1.14.4 for...of循环 +`for...of`循环会自动遍历,不用调用`next`方法,需要注意的是,`for...of`遇到`next`返回值的`done`属性为`true`就会终止,`return`返回的不包括在`for...of`循环中。 +```js +function * f(){ + yield 1; + yield 2; + yield 3; + yield 4; + return 5; +} +for (let k of f()){ + console.log(k); +} +// 1 2 3 4 没有 5 +``` + +#### 1.14.5 Generator.prototype.throw() +`throw`方法用来向函数外抛出错误,并且在Generator函数体内捕获。 +```js +let f = function * (){ + try { yield } + catch (e) { console.log('内部捕获', e) } +} + +let a = f(); +a.next(); + +try{ + a.throw('a'); + a.throw('b'); +}catch(e){ + console.log('外部捕获',e); +} +// 内部捕获 a +// 外部捕获 b +``` + +#### 1.14.6 Generator.prototype.return() +`return`方法用来返回给定的值,并结束遍历Generator函数,如果`return`方法没有参数,则返回值的`value`属性为`undefined`。 +```js +function * f(){ + yield 1; + yield 2; + yield 3; +} +let g = f(); +g.next(); // {value : 1, done : false} +g.return('leo'); // {value : 'leo', done " true} +g.next(); // {value : undefined, done : true} +``` + +#### 1.14.7 next()/throw()/return()共同点 +相同点就是都是用来恢复Generator函数的执行,并且使用不同语句替换`yield`表达式。 +* `next()`将`yield`表达式替换成一个值。 +```js +let f = function * (x,y){ + let r = yield x + y; + return r; +} +let g = f(1, 2); +g.next(); // {value : 3, done : false} +g.next(1); // {value : 1, done : true} +// 相当于把 let r = yield x + y; +// 替换成 let r = 1; +``` +* `throw()`将`yield`表达式替换成一个`throw`语句。 +```js +g.throw(new Error('报错')); // Uncaught Error:报错 +// 相当于将 let r = yield x + y +// 替换成 let r = throw(new Error('报错')); +``` +* `next()`将`yield`表达式替换成一个`return`语句。 +```js +g.return(2); // {value: 2, done: true} +// 相当于将 let r = yield x + y +// 替换成 let r = return 2; +``` + +#### 1.14.8 yield* 表达式 +用于在一个Generator中执行另一个Generator函数,如果没有使用`yield*`会没有效果。 +```js +function * a(){ + yield 1; + yield 2; +} +function * b(){ + yield 3; + yield * a(); + yield 4; +} +// 等同于 +function * b(){ + yield 3; + yield 1; + yield 2; + yield 4; +} +for(let k of b()){console.log(k)} +// 3 +// 1 +// 2 +// 4 +``` + +#### 1.14.9 应用场景 +1. **控制流管理** +解决回调地狱: +```js +// 使用前 +f1(function(v1){ + f2(function(v2){ + f3(function(v3){ + // ... more and more + }) + }) +}) + +// 使用Promise +Promise.resolve(f1) + .then(f2) + .then(f3) + .then(function(v4){ + // ... + },function (err){ + // ... + }).done(); + +// 使用Generator +function * f (v1){ + try{ + let v2 = yield f1(v1); + let v3 = yield f1(v2); + let v4 = yield f1(v3); + // ... + }catch(err){ + // console.log(err) + } +} +function g (task){ + let obj = task.next(task.value); + // 如果Generator函数未结束,就继续调用 + if(!obj.done){ + task.value = obj.value; + g(task); + } +} +g( f(initValue) ); +``` + +2. **异步编程的使用** +在真实的异步任务封装的情况: +```js +let fetch = require('node-fetch'); +function * f(){ + let url = 'http://www.baidu.com'; + let res = yield fetch(url); + console.log(res.bio); +} +// 执行该函数 +let g = f(); +let result = g.next(); +// 由于fetch返回的是Promise对象,所以用then +result.value.then(function(data){ + return data.json(); +}).then(function(data){ + g.next(data); +}) +``` + +[⬆ 返回目录](#二目录) + + +### 1.15 Class语法和继承 +#### 1.15.1 介绍 +ES6中的`class`可以看作只是一个语法糖,绝大部分功能都可以用ES5实现,并且,**类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式**。 +```js +// ES5 +function P (x,y){ + this.x = x; + this.y = y; +} +P.prototype.toString = function () { + return '(' + this.x + ', ' + this.y + ')'; +}; +var a = new P(1, 2); + +// ES6 +class P { + constructor(x, y){ + this.x = x; + this.y = y; + } + toString(){ + return '(' + this.x + ', ' + this.y + ')'; + } +} +let a = new P(1, 2); +``` +**值得注意**: +ES6的**类**的所有方法都是定义在`prototype`属性上,调用类的实例的方法,其实就是调用原型上的方法。 +```js +class P { + constructor(){ ... } + toString(){ ... } + toNumber(){ ... } +} +// 等同于 +P.prototyoe = { + constructor(){ ... }, + toString(){ ... }, + toNumber(){ ... } +} + +let a = new P(); +a.constructor === P.prototype.constructor; // true +``` +类的属性名可以使用**表达式**: +```js +let name = 'leo'; +class P { + constructor (){ ... } + [name](){ ... } +} +``` + +**Class不存在变量提升**: +ES6中的类不存在变量提升,与ES5完全不同: +```js +new P (); // ReferenceError +class P{...}; +``` +**Class的name属性**: +`name`属性总是返回紧跟在`class`后的类名。 +```js +class P {} +P.name; // 'P' +``` + +#### 1.15.2 constructor()方法 +`constructor()`是类的默认方法,通过`new`实例化时自动调用执行,一个类必须有`constructor()`方法,否则一个空的`constructor()`会默认添加。 +`constructor()`方法默认返回实例对象(即`this`)。 +```js +class P { ... } +// 等同于 +class P { + constructor(){ ... } +} +``` + +#### 1.15.3 类的实例对象 +与ES5一样,ES6的类必须使用`new`命令实例化,否则报错。 +```js +class P { ... } +let a = P (1,2); // 报错 +let b = new P(1, 2); // 正确 +``` +与 ES5 一样,实例的属性除非显式定义在其本身(即定义在`this`对象上),否则都是定义在原型上(即定义在`class`上)。 +```js +class P { + constructor(x, y){ + this.x = x; + this.y = y; + } + toString(){ + return '(' + this.x + ', ' + this.y + ')'; + } +} +var point = new Point(2, 3); + +point.toString() // (2, 3) + +point.hasOwnProperty('x') // true +point.hasOwnProperty('y') // true +point.hasOwnProperty('toString') // false +point.__proto__.hasOwnProperty('toString') // true +// toString是原型对象的属性(因为定义在Point类上) +``` + +#### 1.15.4 Class表达式 +与函数一样,类也可以使用表达式来定义,使用表达式来作为类的名字,而`class`后跟的名字,用来指代当前类,只能再Class内部使用。 +```js +let a = class P{ + get(){ + return P.name; + } +} + +let b = new a(); +b.get(); // P +P.name; // ReferenceError: P is not defined +``` +如果类的内部没用到的话,可以省略`P`,也就是可以写成下面的形式。 +```js +let a = class { ... } +``` + +#### 1.15.5 私有方法和私有属性 +由于ES6不提供,只能变通来实现: +* 1.使用命名加以区别,如变量名前添加`_`,但是不保险,外面也可以调用到。 +```js +class P { + // 公有方法 + f1 (x) { + this._x(x); + } + // 私有方法 + _x (x){ + return this.y = x; + } +} +``` +* 2.将私有方法移除模块,再在类内部调用`call`方法。 +```js +class P { + f1 (x){ + f2.call(this, x); + } +} +function f2 (x){ + return this.y = x; +} +``` +* 3.使用`Symbol`为私有方法命名。 +```js +const a1 = Symbol('a1'); +const a2 = Symbol('a2'); +export default class P{ + // 公有方法 + f1 (x){ + this[a1](x); + } + // 私有方法 + [a1](x){ + return this[a2] = x; + } +} +``` + + +#### 1.15.6 this指向问题 +类内部方法的`this`默认指向类的实例,但单独使用该方法可能报错,因为`this`指向的问题。 +```js +class P{ + leoDo(thing = 'any'){ + this.print(`Leo do ${thing}`) + } + print(text){ + console.log(text); + } +} +let a = new P(); +let { leoDo } = a; +leoDo(); // TypeError: Cannot read property 'print' of undefined +// 问题出在 单独使用leoDo时,this指向调用的环境, +// 但是leoDo中的this是指向P类的实例,所以报错 +``` +**解决方法**: +* 1.在类里面绑定`this` +```js +class P { + constructor(){ + this.name = this.name.bind(this); + } +} +``` +* 2.使用箭头函数 +```js +class P{ + constructor(){ + this.name = (name = 'leo' )=>{ + this.print(`my name is ${name}`) + } + } +} +``` + +#### 1.15.7 Class的getter和setter +使用`get`和`set`关键词对属性设置取值函数和存值函数,拦截属性的存取行为。 +```js +class P { + constructor (){ ... } + get f (){ + return 'getter'; + } + set f (val) { + console.log('setter: ' + val); + } +} + +let a = new P(); +a.f = 100; // setter : 100 +a.f; // getter +``` + +#### 1.15.8 Class的generator方法 +只要在方法之前加个(`*`)即可。 +```js +class P { + constructor (...args){ + this.args = args; + } + *[Symbol.iterator](){ + for (let arg of this.args){ + yield arg; + } + } +} +for (let k of new P('aa', 'bb')){ + console.log(k); +} +// 'aa' +// 'bb' +``` + +#### 1.15.9 Class的静态方法 +由于类相当于实例的原型,所有类中定义的方法都会被实例继承,若不想被继承,只要加上`static`关键字,只能通过类来调用,即“**静态方法**”。 +```js +class P (){ + static f1 (){ return 'aaa' }; +} +P.f1(); // 'aa' +let a = new P(); +a.f1(); // TypeError: a.f1 is not a function +``` +如果静态方法包含`this`关键字,则`this`指向类,而不是实例。 +```js +class P { + static f1 (){ + this.f2(); + } + static f2 (){ + console.log('aaa'); + } + f2(){ + console.log('bbb'); + } +} +P.f2(); // 'aaa' +``` +并且静态方法可以被子类继承,或者`super`对象中调用。 +```js +class P{ + static f1(){ return 'leo' }; +} +class Q extends P { ... }; +Q.f1(); // 'leo' + +class R extends P { + static f2(){ + return super.f1() + ',too'; + } +} +R.f2(); // 'leo , too' +``` + +#### 1.15.10 Class的静态属性和实例属性 +ES6中明确规定,Class内部只有静态方法没有静态属性,所以只能通过下面实现。 +```js +// 正确写法 +class P {} +P.a1 = 1; +P.a1; // 1 + +// 无效写法 +class P { + a1: 2, // 无效 + static a1 : 2, // 无效 +} +P.a1; // undefined +``` +**新提案来规定实例属性和静态属性的新写法** +* 1.类的实例属性 +类的实例属性可以用等式,写入类的定义中。 +```js +class P { + prop = 100; // prop为P的实例属性 可直接读取 + constructor(){ + console.log(this.prop); // 100 + } +} +``` +有了新写法后,就可以不再`contructor`方法里定义。 +为了可读性的目的,对于那些在`constructor`里面已经定义的实例属性,新写法允许**直接列出**。 +```js +// 之前写法: +class RouctCounter extends React.Component { + constructor(prop){ + super(prop); + this.state = { + count : 0 + } + } +} + +// 新写法 +class RouctCounter extends React.Component { + state; + constructor(prop){ + super(prop); + this.state = { + count : 0 + } + } + +} +``` +* 2.类的静态属性 +只要在实例属性前面加上`static`关键字就可以。 +```js +class P { + static prop = 100; + constructor(){console.log(this.prop)}; // 100 +} +``` +新写法方便静态属性的表达。 +```js +// old +class P { .... } +P.a = 1; + +// new +class P { + static a = 1; +} +``` + +#### 1.15.11 Class的继承 +主要通过`extends`关键字实现,继承父类的所有属性和方法,通过`super`关键字来新建父类构造函数的`this`对象。 +```js +class P { ... } +class Q extends P { ... } + +class P { + constructor(x, y){ + // ... + } + f1 (){ ... } +} +class Q extends P { + constructor(a, b, c){ + super(x, y); // 调用父类 constructor(x, y) + this.color = color ; + } + f2 (){ + return this.color + ' ' + super.f1(); + // 调用父类的f1()方法 + } +} +``` +**子类必须在`constructor()`调用`super()`否则报错**,并且只有`super`方法才能调用父类实例,还有就是,**父类的静态方法,子类也可以继承到**。 +```js +class P { + constructor(x, y){ + this.x = x; + this.y = y; + } + static fun(){ + console.log('hello leo') + } +} +// 关键点1 调用super +class Q extends P { + constructor(){ ... } +} +let a = new Q(); // ReferenceError 因为Q没有调用super + +// 关键点2 调用super +class R extends P { + constructor (x, y. z){ + this.z = z; // ReferenceError 没调用super不能使用 + super(x, y); + this.z = z; // 正确 + } +} + +// 关键点3 子类继承父类静态方法 +R.hello(); // 'hello leo' +``` + +**super关键字**: +既可以当函数使用,还可以当对象使用。 +* 1.当函数调用,代表父类的构造函数,但必须执行一次。 +```js +class P {... }; +class R extends P { + constructor(){ + super(); + } +} +``` +* 2.当对象调用,指向原型对象,在静态方法中指向父类。 +```js +class P { + f (){ return 2 }; +} +class R extends P { + constructor (){ + super(); + console.log(super.f()); // 2 + } +} +let a = new R() +``` +**注意**:`super`指向父类原型对象,所以定义在父类实例的方法和属性,是无法通过`super`调用的,但是通过调用`super`方法可以把内部`this`指向当前实例,就可以访问到。 +```js +class P { + constructor(){ + this.a = 1; + } + print(){ + console.log(this.a); + } +} +class R extends P { + get f (){ + return super.a; + } +} +let b = new R(); +b.a; // undefined 因为a是父类P实例的属性 + +// 先调用super就可以访问 +class Q extends P { + constructor(){ + super(); // 将内部this指向当前实例 + return super.a; + } +} +let c = new Q(); +c.a; // 1 + +// 情况3 +class J extends P { + constructor(){ + super(); + this.a = 3; + } + g(){ + super.print(); + } +} +let c = new J(); +c.g(); // 3 由于执行了super()后 this指向当前实例 +``` + +[⬆ 返回目录](#二目录) + +### 1.16 Module语法和加载实现 +#### 1.16.1 介绍 +ES6之前用于JavaScript的模块加载方案,是一些社区提供的,主要有`CommonJS`和`AMD`两种,前者用于**服务器**,后者用于**浏览器**。 +ES6提供了模块的实现,使用`export`命令对外暴露接口,使用`import`命令输入其他模块暴露的接口。 +```js +// CommonJS模块 +let { stat, exists, readFire } = require('fs'); + +// ES6模块 +import { stat, exists, readFire } = from 'fs'; +``` + +#### 1.16.2 严格模式 +ES6模块自动采用严格模式,无论模块头部是否有`"use strict"`。 +**严格模式有以下限制**: +* 变量必须**声明后再使用** +* 函数的参数**不能有同名属性**,否则报错 +* 不能使用`with`语句 +* 不能对只读属性赋值,否则报错 +* 不能使用前缀 0 表示八进制数,否则报错 +* 不能删除不可删除的属性,否则报错 +* 不能删除变量`delete prop`,会报错,只能删除属性`delete * global[prop]` +* `eval`不会在它的外层作用域引入变量 +* `eval`和`arguments`不能被重新赋值 +* `arguments`不会自动反映函数参数的变化 +* 不能使用`arguments.callee` +* 不能使用`arguments.caller` +* 禁止`this`指向全局对象 +* 不能使用`fn.caller`和`fn.arguments`获取函数调用的堆栈 +* 增加了保留字(比如`protected`、`static`和`interface`) + +特别是,ES6中顶层`this`指向`undefined`,即不应该在顶层代码使用`this`。 + +#### 1.16.3 export命令 +使用`export`向模块外暴露接口,可以是方法,也可以是变量。 +```js +// 1. 变量 +export let a = 'leo'; +export let b = 100; + +// 还可以 +let a = 'leo'; +let b = 100; +export {a, b}; + +// 2. 方法 +export function f(a,b){ + return a*b; +} + +// 还可以 +function f1 (){ ... } +function f2 (){ ... } +export { + a1 as f1, + a2 as f2 +} +``` +可以使用`as`重命名函数的对外接口。 +**特别注意**: +`export`暴露的必须是接口,不能是值。 +```js +// 错误 +export 1; // 报错 + +let a = 1; +export a; // 报错 + +// 正确 +export let a = 1; // 正确 + +let a = 1; +export {a}; // 正确 + +let a = 1; +export { a as b}; // 正确 +``` +暴露方法也是一样: +```js +// 错误 +function f(){...}; +export f; + +// 正确 +export function f () {...}; + +function f(){...}; +export {f}; +``` + +#### 1.16.4 import命令 +加载`export`暴露的接口,输出为变量。 +```js +import { a, b } from '/a.js'; +function f(){ + return a + b; +} +``` +`import`后大括号指定变量名,需要与`export`的模块暴露的名称一致。 +也可以使用`as`为输入的变量重命名。 +```js +import { a as leo } from './a.js'; +``` +`import`不能直接修改输入变量的值,因为输入变量只读只是个接口,但是如果是个对象,可以修改它的属性。 +```js +// 错误 +import {a} from './f.js'; +a = {}; // 报错 + +// 正确 +a.foo = 'leo'; // 不报错 +``` +`import`命令具有提升效果,会提升到整个模块头部最先执行,且多次执行相同`import`只会执行一次。 + +#### 1.16.5 模块的整体加载 +当一个模块暴露多个方法和变量,引用时可以用`*`整体加载。 +```js +// a.js +export function f(){...} +export function g(){...} + +// b.js +import * as obj from '/a.js'; +console.log(obj.f()); +console.log(obj.g()); +``` +但是,不允许运行时改变: +```js +import * as obj from '/a.js'; +// 不允许 +obj.a = 'leo'; +obj.b = function(){...}; +``` + +#### 1.16.6 export default 命令 +使用`export default`命令,为模块指定默认输出,引用的时候直接指定任意名称即可。 +```js +// a.js +export default function(){console.log('leo')}; + +// b.js +import leo from './a.js'; +leo(); // 'leo' +``` +`export defualt`暴露有函数名的函数时,在调用时相当于匿名函数。 +```js +// a.js +export default function f(){console.log('leo')}; +// 或者 +function f(){console.log('leo')}; +export default f; + +// b.js +import leo from './a.js'; +``` +`export defualt`其实是输出一个名字叫`default`的变量,所以后面不能跟变量赋值语句。 +```js +// 正确 +export let a= 1; + +let a = 1; +export defualt a; + +// 错误 +export default let a = 1; +``` +`export default`命令的本质是将后面的值,赋给`default`变量,所以可以直接将一个值写在`export default`之后。 +```js +// 正确 +export detault 1; +// 错误 +export 1; +``` + +#### 1.16.7 export 和 import 复合写法 +常常在先输入后输出同一个模块使用,即转发接口,将两者写在一起。 +```js +export {a, b} from './leo.js'; + +// 理解为 +import {a, b} from './leo.js'; +export {a, b} +``` +常见的写法还有: +```js +// 接口改名 +export { a as b} from './leo.js'; + +// 整体输出 +export * from './leo.js'; + +// 默认接口改名 +export { default as a } from './leo.js'; +``` +**常常用在模块继承**。 + +#### 1.16.8 浏览器中的加载规则 +ES6中,可以在浏览器使用` +``` +另外,ES6模块也可以内嵌到网页,语法与外部加载脚本一致: +```html + +``` +**注意点**: +* 代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。 +* 模块脚本自动采用严格模式,不管有没有声明`use strict`。 +* 模块之中,可以使用`import`命令加载其他模块(`.js`后缀不可省略,需要提供`绝对 UR`L 或`相对 UR`L),也可以使用`export`命令输出对外接口。 +* 模块之中,顶层的`this`关键字返回`undefined`,而不是指向`window`。也就是说,在模块顶层使用`this`关键字,是无意义的。 +* 同一个模块如果加载多次,将只执行一次。 + +[⬆ 返回目录](#二目录) + diff --git "a/Cute-Article/article/64-ES7\346\261\207\346\200\273.md" "b/Cute-Article/article/64-ES7\346\261\207\346\200\273.md" new file mode 100644 index 00000000..c2d17715 --- /dev/null +++ "b/Cute-Article/article/64-ES7\346\261\207\346\200\273.md" @@ -0,0 +1,44 @@ +## 2. ES7 +### 2.1 Array.prototype.includes()方法 +`includes()`用于查找一个值是否在数组中,如果在返回`true`,否则返回`false`。 +```js +['a', 'b', 'c'].includes('a'); // true +['a', 'b', 'c'].includes('d'); // false +``` +`includes()`方法接收两个参数,**搜索的内容**和**开始搜索的索引**,默认值为**0**,若搜索值在数组中则返回`true`否则返回`false`。 +```js +['a', 'b', 'c', 'd'].includes('b'); // true +['a', 'b', 'c', 'd'].includes('b', 1); // true +['a', 'b', 'c', 'd'].includes('b', 2); // false +``` +与`indexOf`方法对比,下面方法效果相同: +```js +['a', 'b', 'c', 'd'].indexOf('b'); // true +['a', 'b', 'c', 'd'].includes('b') > -1; // true +``` +**includes()与indexOf对比:** +* `includes`相比`indexOf`更具语义化,`includes`返回的是是否存在的具体结果,值为布尔值,而`indexOf`返回的是搜索值的下标。 +* `includes`相比`indexOf`更准确,`includes`认为两个`NaN`相等,而`indexOf`不会。 +```js +let a = [1, NaN, 3]; +a.indexOf(NaN); // -1 +a.includes(NaN); // true +``` +另外在判断`+0`与`-0`时,`includes`和`indexOf`的返回相同。 +```js +[1, +0, 3, 4].includes(-0); // true +[1, +0, 3, 4].indexOf(-0); // 1 +``` + +### 2.2 指数操作符(**) +基本用法: +```js +let a = 3 ** 2 ; // 9 +// 等效于 +Math.pow(3, 2); // 9 +``` +`**`是一个运算符,也可以满足类似假发的操作,如下: +```js +let a = 3; +a **= 2; // 9 +``` \ No newline at end of file diff --git "a/Cute-Article/article/65-ES8\346\261\207\346\200\273.md" "b/Cute-Article/article/65-ES8\346\261\207\346\200\273.md" new file mode 100644 index 00000000..ac16efe0 --- /dev/null +++ "b/Cute-Article/article/65-ES8\346\261\207\346\200\273.md" @@ -0,0 +1,371 @@ + +## 3. ES8 +### 3.1 async函数 +#### 3.1.1 介绍 +ES8引入`async`函数,是为了使异步操作更加方便,其实它就是**Generator**函数的语法糖。 +`async`函数使用起来,只要把**Generator**函数的**(*)**号换成`async`,`yield`换成`await`即可。对比如下: +```js +// Generator写法 +const fs = require('fs'); +const readFile = function (fileName) { + return new Promise(function (resolve, reject) { + fs.readFile(fileName, function(error, data) { + if (error) return reject(error); + resolve(data); + }); + }); +}; +const gen = function* () { + const f1 = yield readFile('/etc/fstab'); + const f2 = yield readFile('/etc/shells'); + console.log(f1.toString()); + console.log(f2.toString()); +}; + +// async await写法 +const asyncReadFile = async function () { + const f1 = await readFile('/etc/fstab'); + const f2 = await readFile('/etc/shells'); + console.log(f1.toString()); + console.log(f2.toString()); +}; +``` +**对比Genenrator有四个优点:** +* (1)内置执行器 +Generator函数执行需要有执行器,而`async`函数自带执行器,即`async`函数与普通函数一模一样: +```js +async f(); +``` +* (2)更好的语义 +`async`和`await`,比起`星号`和`yield`,语义更清楚了。`async`表示函数里有异步操作,`await`表示紧跟在后面的表达式需要等待结果。 +* (3)更广的适用性 +`yield`命令后面只能是 Thunk 函数或 Promise 对象,而`async`函数的`await`命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。 +* (4)返回值是Promise +`async`函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用`then`方法指定下一步的操作。 + +进一步说,`async`函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而`await`命令就是内部`then`命令的语法糖。 + +#### 3.1.2 基本用法 +`async`函数返回一个Promise对象,可以使用`then`方法添加回调函数,函数执行时,遇到`await`就会先返回,等到异步操作完成,在继续执行。 +```js +async function f(item){ + let a = await g(item); + let b = await h(item); + return b; +} +f('hello').then(res => { + console.log(res); +}) +``` +`async`表明该函数内部有异步操作,调用该函数时,会立即返回一个Promise对象。 +另外还有个定时的案例,指定时间后执行: +```js +function f (ms){ + return new Promise(res => { + setTimeout(res, ms); + }); +} +async function g(val, ms){ + await f(ms); + console.log(val); +} +g('leo', 50); +``` +`async`函数还有很多使用形式: +```js +// 函数声明 +async function f (){...} + +// 函数表达式 +let f = async function (){...} + +// 对象的方法 +let a = { + async f(){...} +} +a.f().then(...) + +// Class的方法 +class c { + constructor(){...} + async f(){...} +} + +// 箭头函数 +let f = async () => {...} +``` + +#### 3.1.3 返回Promise对象 +`async`内部`return`返回的值会作为`then`方法的参数,另外只有`async`函数内部的异步操作执行完,才会执行`then`方法指定的回调函数。 +```js +async function f(){ + return 'leo'; +} +f().then(res => { console.log (res) }); // 'leo' +``` +`async`内部抛出的错误,会被`catch`接收。 +```js +async function f(){ + throw new Error('err'); +} +f().then ( + v => console.log(v), + e => console.log(e) +) +// Error: err +``` + +#### 3.1.4 await命令 +通常`await`后面是一个Promise对象,如果不是就返回对应的值。 +```js +async function f(){ + return await 10; +} +f().then(v => console.log(v)); // 10 +``` +我们常常将`async await`和`try..catch`一起使用,并且可以放多个`await`命令,也是防止异步操作失败因为中断后续异步操作的情况。 +```js +async function f(){ + try{ + await Promise.reject('err'); + }catch(err){ ... } + return await Promise.resolve('leo'); +} +f().then(v => console.log(v)); // 'leo' +``` + +#### 3.1.5 使用注意 +* (1)`await`命令放在`try...catch`代码块中,防止Promise返回`rejected`。 +* (2)若多个`await`后面的异步操作不存在继发关系,最好让他们同时执行。 +```js +// 效率低 +let a = await f(); +let b = await g(); + +// 效率高 +let [a, b] = await Promise.all([f(), g()]); +// 或者 +let a = f(); +let b = g(); +let a1 = await a(); +let b1 = await b(); +``` +* (3)`await`命令只能用在`async`函数之中,如果用在普通函数,就会报错。 +```js +// 错误 +async function f(){ + let a = [{}, {}, {}]; + a.forEach(v =>{ // 报错,forEach是普通函数 + await post(v); + }); +} + +// 正确 +async function f(){ + let a = [{}, {}, {}]; + for(let k of a){ + await post(k); + } +} +``` + +### 3.2 Promise.prototype.finally() +`finally()`是ES8中**Promise**添加的一个新标准,用于指定不管**Promise**对象最后状态(是`fulfilled`还是`rejected`)如何,都会执行此操作,并且`finally`方法必须写在最后面,即在`then`和`catch`方法后面。 +```js +// 写法如下: +promise + .then(res => {...}) + .catch(err => {...}) + .finally(() => {...}) +``` +`finally`方法常用在处理**Promise**请求后关闭服务器连接: +```js +server.listen(port) + .then(() => {..}) + .finally(server.stop); +``` +**本质上,finally方法是then方法的特例:** +```js +promise.finally(() => {...}); + +// 等同于 +promise.then( + result => { + // ... + return result + }, + error => { + // ... + throw error + } +) +``` + +### 3.3 Object.values(),Object.entries() +ES7中新增加的 `Object.values()`和`Object.entries()`与之前的`Object.keys()`类似,返回数组类型。 +回顾下`Object.keys()`: +```js +var a = { f1: 'hi', f2: 'leo'}; +Object.keys(a); // ['f1', 'f2'] +``` +#### 3.3.1 Object.values() +返回一个数组,成员是参数对象自身的(不含继承的)所有**可遍历属性**的键值。 +```js +let a = { f1: 'hi', f2: 'leo'}; +Object.values(a); // ['hi', 'leo'] +``` +如果参数不是对象,则返回空数组: +```js +Object.values(10); // [] +Object.values(true); // [] +``` + +#### 3.3.2 Object.entries() +返回一个数组,成员是参数对象自身的(不含继承的)所有**可遍历属性**的键值对数组。 +```js +let a = { f1: 'hi', f2: 'leo'}; +Object.entries(a); // [['f1','hi'], ['f2', 'leo']] +``` +* **用途1**: +遍历对象属性。 +```js +let a = { f1: 'hi', f2: 'leo'}; +for (let [k, v] of Object.entries(a)){ + console.log( + `${JSON.stringfy(k)}:${JSON.stringfy(v)}` + ) +} +// 'f1':'hi' +// 'f2':'leo' +``` +* **用途2**: +将对象转为真正的Map结构。 +```js +let a = { f1: 'hi', f2: 'leo'}; +let map = new Map(Object.entries(a)); +``` +手动实现`Object.entries()`方法: +```js +// Generator函数实现: +function* entries(obj){ + for (let k of Object.keys(obj)){ + yield [k ,obj[k]]; + } +} + +// 非Generator函数实现: +function entries (obj){ + let arr = []; + for(let k of Object.keys(obj)){ + arr.push([k, obj[k]]); + } + return arr; +} +``` + +### 3.4 Object.getOwnPropertyDescriptors() +之前有`Object.getOwnPropertyDescriptor`方法会返回某个对象属性的描述对象,新增的`Object.getOwnPropertyDescriptors()`方法,返回指定对象所有自身属性(非继承属性)的描述对象,所有原对象的属性名都是该对象的属性名,对应的属性值就是该属性的描述对象 +```js +let a = { + a1:1, + get f1(){ return 100} +} +Object.getOwnPropetyDescriptors(a); +/* +{ + a:{ configurable:true, enumerable:true, value:1, writeable:true} + f1:{ configurable:true, enumerable:true, get:f, set:undefined} +} +*/ +``` +实现原理: +```js +function getOwnPropertyDescriptors(obj) { + const result = {}; + for (let key of Reflect.ownKeys(obj)) { + result[key] = Object.getOwnPropertyDescriptor(obj, key); + } + return result; +} +``` +引入这个方法,主要是为了解决`Object.assign()`无法正确拷贝`get`属性和`set`属性的问题。 +```js +let a = { + set f(v){ + console.log(v) + } +} +let b = {}; +Object.assign(b, a); +Object.a(b, 'f'); +/* +f = { + configurable: true, + enumable: true, + value: undefined, + writeable: true +} +*/ +``` +`value`为`undefined`是因为`Object.assign`方法不会拷贝其中的`get`和`set`方法,使用`getOwnPropertyDescriptors`配合`Object.defineProperties`方法来实现正确的拷贝: +```js +let a = { + set f(v){ + console.log(v) + } +} +let b = {}; +Object.defineProperties(b, Object.getOwnPropertyDescriptors(a)); +Object.getOwnPropertyDescriptor(b, 'f') +/* + configurable: true, + enumable: true, + get: undefined, + set: function(){...} +*/ +``` +`Object.getOwnPropertyDescriptors`方法的配合`Object.create`方法,将对象属性克隆到一个新对象,实现浅拷贝。 +```js +const clone = Object.create(Object.getPrototypeOf(obj), + Object.getOwnPropertyDescriptors(obj)); + +// 或者 +const shallowClone = (obj) => Object.create( + Object.getPrototypeOf(obj), + Object.getOwnPropertyDescriptors(obj) +); +``` + +### 3.5 字符串填充 padStart和padEnd +用来为字符串填充特定字符串,并且都有两个参数:**字符串目标长度**和**填充字段**,第二个参数可选,默认空格。 +```js +'es8'.padStart(2); // 'es8' +'es8'.padStart(5); // ' es8' +'es8'.padStart(6, 'woof'); // 'wooes8' +'es8'.padStart(14, 'wow'); // 'wowwowwowwoes8' +'es8'.padStart(7, '0'); // '0000es8' + +'es8'.padEnd(2); // 'es8' +'es8'.padEnd(5); // 'es8 ' +'es8'.padEnd(6, 'woof'); // 'es8woo' +'es8'.padEnd(14, 'wow'); // 'es8wowwowwowwo' +'es8'.padEnd(7, '6'); // 'es86666' +``` +从上面结果来看,填充函数只有在字符长度小于目标长度时才有效,若字符长度已经等于或小于目标长度时,填充字符不会起作用,而且目标长度如果小于字符串本身长度时,字符串也不会做截断处理,只会原样输出。 + +### 3.6 函数参数列表与调用中的尾部逗号 +该特性允许我们在定义或者调用函数时添加尾部逗号而不报错: +```js +function es8(var1, var2, var3,) { + // ... +} +es8(10, 20, 30,); +``` + +### 3.7 共享内存与原子操作 +当内存被共享时,多个线程可以并发读、写内存中相同的数据。原子操作可以确保那些被读、写的值都是可预期的,即新的事务是在旧的事务结束之后启动的,旧的事务在结束之前并不会被中断。这部分主要介绍了 ES8 中新的构造函数 `SharedArrayBuffer` 以及拥有许多静态方法的命名空间对象 `Atomic` 。 +`Atomic` 对象类似于 `Math` 对象,拥有许多静态方法,所以我们不能把它当做构造函数。 `Atomic` 对象有如下常用的静态方法: + +* add /sub :为某个指定的value值在某个特定的位置增加或者减去某个值 +* and / or /xor :进行位操作 +* load :获取特定位置的值 \ No newline at end of file diff --git "a/Cute-Article/article/66-ES9\346\261\207\346\200\273.md" "b/Cute-Article/article/66-ES9\346\261\207\346\200\273.md" new file mode 100644 index 00000000..991eb667 --- /dev/null +++ "b/Cute-Article/article/66-ES9\346\261\207\346\200\273.md" @@ -0,0 +1,324 @@ +## 4. ES9 +### 4.1 对象的拓展运算符 +#### 4.1.1 介绍 +对象的拓展运算符,即对象的Rest/Spread属性,可将对象解构赋值用于从一个对象取值,搜键值对分配到指定对象上,与数组的拓展运算符类似: +```js +let {x, y, ...z} = {x:1, y:2, a:3, b:4}; +x; // 1 +y; // 2 +z; // {a:3, b:4} +``` +对象的解构赋值要求等号右边必须是个对象,所以如果等号右边是`undefined`或`null`就会报错无法转成对象。 +```js +let {a, ...b} = null; // 运行时报错 +let {a, ...b} = undefined; // 运行时报错 +``` +解构赋值必须是最后一个参数,否则报错。 +```js +let {...a, b, c} = obj; // 语法错误 +let {a, ...b, c} = obj; // 语法错误 +``` +**注意**: +* 1.解构赋值是浅拷贝。 +```js +let a = {a1: {a2: 1}}; +let {...b} = a; +a.a1.a2 = 'leo'; +b.a1.a2 = 'leo'; +``` +* 2.拓展运算符的解构赋值,不能复制继承自原型对象的属性。 +```js +let o1 = { a: 1 }; +let o2 = { b: 2 }; +o2.__proto__ = o1; +let { ...o3 } = o2; +o3; // { b: 2 } +o3.a; // undefined +``` + +#### 4.1.2 使用场景 +* 1.取出参数对象所有可遍历属性,拷贝到当前对象中。 +```js +let a = { a1:1, a2:2 }; +let b = { ...a }; +b; // { a1:1, a2:2 } + +// 类似Object.assign方法 +``` +* 2.合并两个对象。 +```js +let a = { a1:1, a2:2 }; +let b = { b1:11, b2:22 }; +let ab = { ...a, ...b }; // {a1: 1, a2: 2, b1: 11, b2: 22} +// 等同于 +let ab = Object.assign({}, a, b); +``` +* 3.将自定义属性放在拓展运算符后面,覆盖对象原有的同名属性。 +```js +let a = { a1:1, a2:2, a3:3 }; +let r = { ...a, a3:666 }; +// r {a1: 1, a2: 2, a3: 666} + +// 等同于 +let r = { ...a, ...{ a3:666 }}; +// r {a1: 1, a2: 2, a3: 666} + +// 等同于 +let r = Object.assign({}, a, { a3:666 }); +// r {a1: 1, a2: 2, a3: 666} +``` +* 4.将自定义属性放在拓展运算符前面,就会成为设置新对象的默认值。 +```js +let a = { a1:1, a2:2 }; +let r = { a3:666, ...a }; +// r {a3: 666, a1: 1, a2: 2} + +// 等同于 +let r = Object.assign({}, {a3:666}, a); +// r {a3: 666, a1: 1, a2: 2} + +// 等同于 +let r = Object.assign({a3:666}, a); +// r {a3: 666, a1: 1, a2: 2} +``` +* 5.拓展运算符后面可以使用表达式。 +```js +let a = { + ...(x>1? {a:!:{}), + b:2 +} +``` +* 6.拓展运算符后面如果是个空对象,则没有任何效果。 +```js +{...{}, a:1}; // {a:1} +``` +* 7.若参数是`null`或`undefined`则忽略且不报错。 +```js +let a = { ...null, ...undefined }; // 不报错 +``` +* 8.若有取值函数`get`则会执行。 +```js +// 不会打印 因为f属性只是定义 而不没执行 +let a = { + ...a1, + get f(){console.log(1)} +} + +// 会打印 因为f执行了 +let a = { + ...a1, + ...{ + get f(){console.log(1)} + } +} +``` + +### 4.2 正则表达式 s 修饰符 +在正则表达式中,点(`.`)可以表示任意单个字符,除了两个:用`u`修饰符解决**四个字节的UTF-16字符**,另一个是行终止符。 +**终止符**即表示一行的结束,如下四个字符属于“行终止符”: +* U+000A 换行符(\n) +* U+000D 回车符(\r) +* U+2028 行分隔符(line separator) +* U+2029 段分隔符(paragraph separator) +```js +/foo.bar/.test('foo\nbar') +// false +``` +上面代码中,因为`.`不匹配`\n`,所以正则表达式返回`false`。 +换个醒,可以匹配任意单个字符: +```js +/foo[^]bar/.test('foo\nbar') +// true +``` +ES9引入`s`修饰符,使得`.`可以匹配任意单个字符: +```js +/foo.bar/s.test('foo\nbar') // true +``` +这被称为`dotAll`模式,即点(`dot`)代表一切字符。所以,正则表达式还引入了一个`dotAll`属性,返回一个布尔值,表示该正则表达式是否处在`dotAll`模式。 +```js +const re = /foo.bar/s; +// 另一种写法 +// const re = new RegExp('foo.bar', 's'); + +re.test('foo\nbar') // true +re.dotAll // true +re.flags // 's' +``` +`/s`修饰符和多行修饰符`/m`不冲突,两者一起使用的情况下,`.`匹配所有字符,而`^`和`$`匹配每一行的行首和行尾。 + +### 4.3 异步遍历器 +在前面ES6章节中,介绍了Iterator接口,而ES6引入了“异步遍历器”,是为异步操作提供原生的遍历器接口,即`value`和`done`这两个属性都是异步产生的。 + +#### 4.3.1 异步遍历的接口 +通过调用遍历器的`next`方法,返回一个Promise对象。 +```js +a.next().then( + ({value, done}) => { + //... + } +) +``` +上述`a`为异步遍历器,调用`next`后返回一个Promise对象,再调用`then`方法就可以指定Promise对象状态变为`resolve`后执行的回调函数,参数为`value`和`done`两个属性的对象,与同步遍历器一致。 +与同步遍历器一样,异步遍历器接口也是部署在`Symbol.asyncIterator`属性上,只要有这个属性,就都可以异步遍历。 +```js +let a = createAsyncIterable(['a', 'b']); +//createAsyncIterable方法用于构建一个iterator接口 +let b = a[Symbol.asyncInterator](); + +b.next().then( result1 => { + console.log(result1); // {value: 'a', done:false} + return b.next(); +}).then( result2 => { + console.log(result2); // {value: 'b', done:false} + return b.next(); +}).then( result3 => { + console.log(result3); // {value: undefined, done:true} +}) +``` +另外`next`方法返回的是一个Promise对象,所以可以放在`await`命令后。 +```js +async function f(){ + let a = createAsyncIterable(['a', 'b']); + let b = a[Symbol.asyncInterator](); + console.log(await b.next());// {value: 'a', done:false} + console.log(await b.next());// {value: 'b', done:false} + console.log(await b.next());// {value: undefined, done:true} +} +``` +还有一种情况,使用`Promise.all`方法,将所有的`next`按顺序连续调用: +```js +let a = createAsyncIterable(['a', 'b']); +let b = a[Symbol.asyncInterator](); +let {{value:v1}, {value:v2}} = await Promise.all([ + b.next(), b.next() +]) +``` +也可以一次调用所有`next`方法,再用`await`最后一步操作。 +```js +async function f(){ + let write = openFile('aaa.txt'); + write.next('hi'); + write.next('leo'); + await write.return(); +} +f(); +``` +#### 4.3.2 for await...of +`for...of`用于遍历同步的Iterator接口,而ES8引入`for await...of`遍历异步的Iterator接口。 +```js +async function f(){ + for await(let a of createAsyncIterable(['a', 'b'])) { + console.log(x); + } +} +// a +// b +``` +上面代码,`createAsyncIterable()`返回一个拥有异步遍历器接口的对象,`for...of`自动调用这个对象的`next`方法,得到一个Promise对象,`await`用来处理这个Promise,一但`resolve`就把得到的值`x`传到`for...of`里面。 +**用途** +直接把部署了asyncIteable操作的异步接口放入这个循环。 +```js +let a = ''; +async function f(){ + for await (let b of req) { + a += b; + } + let c = JSON.parse(a); + console.log('leo', c); +} +``` +当`next`返回的Promise对象被`reject`,`for await...of`就会保错,用`try...catch`捕获。 +```js +async function f(){ + try{ + for await (let a of iterableObj()){ + console.log(a); + } + }catch(e){ + console.error(e); + } +} +``` +注意,`for await...of`循环也可以用于同步遍历器。 +```js +(async function () { + for await (let a of ['a', 'b']) { + console.log(a); + } +})(); +// a +// b +``` +#### 4.3.3 异步Generator函数 +就像 Generator 函数返回一个同步遍历器对象一样,异步 Generator 函数的作用,是返回一个异步遍历器对象。 +在语法上,异步 Generator 函数就是`async`函数与 Generator 函数的结合。 +```js +async function* f() { + yield 'hi'; +} +const a = f(); +a.next().then(x => console.log(x)); +// { value: 'hello', done: false } +``` +设计异步遍历器的目的之一,就是为了让Generator函数能用同一套接口处理同步和异步操作。 +```js +// 同步Generator函数 +function * f(iterable, fun){ + let a = iterabl[Symbol.iterator](); + while(true){ + let {val, done} = a.next(); + if(done) break; + yield fun(val); + } +} + +// 异步Generator函数 +async function * f(iterable, fun){ + let a = iterabl[Symbol.iterator](); + while(true){ + let {val, done} = await a.next(); + if(done) break; + yield fun(val); + } +} +``` +同步和异步Generator函数相同点:在`yield`时用`next`方法停下,将后面表达式的值作为`next()`返回对象的`value`。 +在异步Generator函数中,同时使用`await`和`yield`,简单样理解,`await`命令用于将外部操作产生的值输入函数内部,`yield`命令用于将函数内部的值输出。 +```js +(async function () { + for await (const line of readLines(filePath)) { + console.log(line); + } +})() +``` +异步 Generator 函数可以与`for await...of`循环结合起来使用。 +```js +async function* f(asyncIterable) { + for await (const line of asyncIterable) { + yield '> ' + line; + } +} +``` + +#### 4.3.4 yield* 语句 +`yield*`语句跟一个异步遍历器。 +```js +async function * f(){ + yield 'a'; + yield 'b'; + return 'leo'; +} +async function * g(){ + const a = yield* f(); // a => 'leo' +} +``` +与同步 Generator 函数一样,`for await...of`循环会展开`yield*`。 +```js +(async function () { + for await (const x of gen2()) { + console.log(x); + } +})(); +// a +// b +``` diff --git "a/Cute-Article/article/67-JS\347\256\255\345\244\264\345\207\275\346\225\260\347\232\204\351\200\202\347\224\250\345\222\214\344\270\215\351\200\202\347\224\250\347\232\204\345\234\272\346\231\257.md" "b/Cute-Article/article/67-JS\347\256\255\345\244\264\345\207\275\346\225\260\347\232\204\351\200\202\347\224\250\345\222\214\344\270\215\351\200\202\347\224\250\347\232\204\345\234\272\346\231\257.md" new file mode 100644 index 00000000..700beac8 --- /dev/null +++ "b/Cute-Article/article/67-JS\347\256\255\345\244\264\345\207\275\346\225\260\347\232\204\351\200\202\347\224\250\345\222\214\344\270\215\351\200\202\347\224\250\347\232\204\345\234\272\346\231\257.md" @@ -0,0 +1,210 @@ +## JavaScript 箭头函数究竟是什么? +JavaScript 箭头函数大致相当于 `python` 中的 `lambda` 函数 或 `Ruby` 中的` blocks`。 + +这些是匿名函数,它们有自己的特殊语法,接受一定数量的参数,并在其封闭的作用域的上下文(即定义它们的函数或其他代码)中操作。 + +让我们依次分解这些部分。 + +### 箭头函数语法 +箭头函数具有单一的总体结构,然后在特殊情况下可以通过多种方式简化它们。 核心结构如下所示: +```js +(argument1, argument2, ... argumentN) => { + // function body +} +``` +括号内的是参数列表,后跟“胖箭头”(`=>`),最后是函数体。 + +这与传统函数非常相似,我们只是省略 `function` 关键字并在参数后添加一个胖箭头(`=>`)。 + +然而,有许多方法可以简化箭头函数。 + +首先,如果函数体是单个表达式,则可以不使用花括号并将其置于内联中(省略大括号直接将表达式写在一行中)。 表达式的结果将由函数返回。 例如: +```js +const add = (a, b) => a + b; +``` +其次,如果只有一个参数,你甚至可以省略参数的括号。例如: +```js +const getFirst = array => array[0]; +``` +正如您所看到的,这是一些非常简洁的语法,我们将重点介绍后面的好处。 + +### 高级语法 +有一些高级语法可以了解一下。 + +首先,如果您尝试使用内联单行表达式语法,但您返回的值是对象字面量。您可能会认为这看起来应该是这样的: +```js +(name, description) => {name: name, description: description}; +``` +问题是这种语法比较含糊不清,容易引起歧义 : 看起来好像你正试图创建一个传统的函数体。 如果你碰巧想要一个对象的单个表达式,请用括号包裹该对象: +```js +(name, description) => ({name: name, description: description}); +``` + +### 封闭的上下文作用域 +与其他形式的函数不同,箭头函数没有自己的 [执行期上下文](https://blog.bitsrc.io/understanding-execution-context-and-execution-stack-in-javascript-1c9ea8642dd0)。 + +实际上,这意味着 this 和 arguments 都是从它们的父函数继承而来的。 + +例如,使用和不使用箭头函数比较以下代码: +```js +const test = { + name: 'test object', + createAnonFunction: function() { + return function() { + console.log(this.name); + console.log(arguments); + }; + }, + + createArrowFunction: function() { + return () => { + console.log(this.name); + console.log(arguments); + }; + } +}; +``` +我们有一个简单的 `test` 对象,有两个方法 – 每个方法都返回一个匿名函数。 + +不同之处在于第一个方法使用传统函数表达式,而后者中使用箭头函数。 + +如果我们使用相同参数,在控制台中运行它们,我们会得到完全不一样的结果。 +```js +const anon = test.createAnonFunction('hello', 'world'); +const arrow = test.createArrowFunction('hello', 'world'); +anon(); // undefined {} +arrow(); //test object { '0': 'hello', '1': 'world' } +``` +第一个匿名函数有自己的函数上下文,因此当您调用它时,`test` 对象的 `this.name` 没有可用的引用,也没有创建它时调用的参数。 + +另一个,箭头函数具有与创建它的函数完全相同的函数上下文,使其可以访问 `argumetns` 和 `test` 对象。 + +### 使用箭头函数改进您的代码 +传统 `lambda` 函数的主要用例之一,就是用于遍历列表中的项,现在用 `JavaScript` 箭头函数实现。 + +比如你有一个有值的数组,你想去 `map` 遍历每一项,这时使用箭头函数非常理想: +```js +const words = ['hello', 'WORLD', 'Whatever']; +const downcasedWords = words.map(word => word.toLowerCase()); +``` +一个非常常见的例子是提取对象中的某个特定值: +```js +const names = objects.map(object => object.name); +``` +类似地,当用现代迭代样式取代传统的 for 循环,一般我们使用 forEach 循环,箭头函数能够保持 `this` 来自于父级,让他们非常直观 + +类似的,当用 forEach 来替换传统 for循环的时候,实际上箭头函数会直观的保持 `this`来自于父一级 + +```js +this.examples.forEach(example => { + this.runExample(example); +}); +``` + +### Promises 和 Promise 链 +箭头函数的另一个可以使代码更清晰,更直观的地方是管理异步代码。 + +[Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises) 使得管理异步代码变得容易很多(即使你很欢快地使用 `async` / `await`,你仍然应该理解 [`async` / `await` 是建立在 Promises 之上的](https://medium.com/@bluepnume/learn-about-promises-before-you-start-using-async-await-eb148164a9c8) !) + +但是,虽然使用 `promises`仍然需要定义在异步代码或调用完成后运行的函数。 + +这是箭头函数的理想位置,特别是如果您生成的函数是有状态的,同时想引用对象中的某些内容。 例如: +```js +this.doSomethingAsync().then((result) => { + this.storeResult(result); +}); +``` + +### 对象转换 +箭头函数的另一个常见且极其强大的用途是封装对象转换。 + +例如,在 Vue.js 中,有一种通用模式,用于使用 `mapState` 将 Vuex 存储的各个部分直接包含到 Vue 组件中。 + +这涉及定义一组 “`mappers`” ,这些 “`mappers`” 将从原始的完整的 `state` 对象转换为提取所涉及组件所需的内容。 + +这些简单的转换使用箭头函数再合适不过了。比如: +```js +export default { + computed: { + ...mapState({ + results: state => state.results, + users: state => state.users, + }); + } +} +``` + +### 你不应该使用箭头函数的情景 +在许多情况下,使用箭头函数不是一个好主意。 他们不仅不会帮助你,而且会给你带来一些不必要的麻烦。 + +第一个是对象的方法。 这是一个函数上下文的例子,这对于我们理解很有帮助。 + +有一段时间使用 Class(类)属性语法和箭头函数的组合,作为创建“自动绑定方法”的方式,例如, 事件处理程序可以使用,但仍然绑定到类的方法。 + +这看起来像是这样的: + +```js +class Counter { + counter = 0; + + handleClick = () => { + this.counter++; + } +} +``` +这样,即使 `handleClick` 由事件处理程序调用,而不是在 `Counter` 实例的上下文中调用,它仍然可以访问实例的数据。 + +这种方法的缺点很多,在本文中很好地记录。 + +虽然使用这种方法确实为您提供了具有绑定函数的快捷方式,但该函数以多种不直观的方式运行,如果您尝试将此对象作为原型进行子类化/使用,则会不利于测试,同时也会产生很多问题。 + +相反,使用常规函数,如果需要,将其绑定到构造函数中的实例: + +```js +class Counter { + counter = 0; + + handleClick() { + this.counter++; + } + + constructor() { + this.handleClick = this.handleClick.bind(this); + } +} +``` + +### 深层的调用链 +箭头函数可能让你遇到麻烦的另一个地方是,它们被用于许多不同的组合,特别是在函数深层调用链中。 + +核心原因与匿名函数相同 – 它们给出了非常糟糕的堆栈跟踪。 + +如果你的函数只是向下一级,比如在迭代器里面,那也不是太糟糕,但是如果你把所有的函数定义为箭头函数,并在它们之间来回调用,你就会陷入困境 遇到一个错误的时候,只是收到错误消息,如: + +```js +{anonymous}() +{anonymous}() +{anonymous}() +{anonymous}() +{anonymous}() +``` + +### 有动态上下文的函数 +箭头函数可能让您遇到麻烦的最后一种情况就是吗, this 是动态绑定的时候。 + +如果您在这些位置使用箭头函数,那么动态绑定将不起作用,并且你(或稍后使用你的代码的其他人)可能会对事情未按预期执行的原因感到困惑。 + +一些典型的例子: + +* 事件处理程序是通过将 this 设置为事件的 currentTarget 属性来调用。 +* 如果您仍在使用 jQuery ,则大多数 jQuery 方法将 this 设置为已选择的 dom 元素。 +* 如果您正在使用 Vue.js ,则方法和计算函数通常将 this 设置为 Vue 组件。 +当然你可以故意使用箭头函数来覆盖这种行为,但特别是在 jQuery 和 Vue 的情况下,这通常会干扰正常运行,让你感到困惑的是为什么看起来与附近其他代码相同的代码不起作用。 + +## 总结 +箭头函数是 JavaScript 语言的一个非常有必要的补充,并且在许多情况下使代码更符合人们的阅读习惯。 + +然而,像所有其他特性一样,它们有优点和缺点。 我们应该将它们作为我们工具箱中的另一个工具,而不是作为所有函数的全面替代品。 + +英文原文:https://zendev.com/2018/10/01/javascript-arrow-functions-how-why-when.html +中文地址:https://www.css88.com/archives/10033 \ No newline at end of file diff --git "a/Cute-Article/article/68-\345\210\233\345\273\272\345\257\271\350\261\241\347\232\204\344\270\203\347\247\215\346\226\271\345\274\217.md" "b/Cute-Article/article/68-\345\210\233\345\273\272\345\257\271\350\261\241\347\232\204\344\270\203\347\247\215\346\226\271\345\274\217.md" new file mode 100644 index 00000000..548e09f3 --- /dev/null +++ "b/Cute-Article/article/68-\345\210\233\345\273\272\345\257\271\350\261\241\347\232\204\344\270\203\347\247\215\346\226\271\345\274\217.md" @@ -0,0 +1,212 @@ +JavaScript创建对象的方式有很多,通过Object构造函数或对象字面量的方式也可以创建单个对象,显然这两种方式会产生大量的重复代码,并不适合量产。接下来介绍七种非常经典的创建对象的方式,他们也各有优缺点。 + +## 1.工厂模式 +```js +function f(a, b) { + let a1 = new Object(); + a1.a = a; + a1.b = b; + a1.fun = function(){ + console.log(this.a); + } + return a1; +} + +let f1 = f('Leo','good'); +let f2 = f('Robin','nice'); +``` +可以无数次调用这个工厂函数,每次都会返回一个包含两个属性和一个方法的对象。 + +工厂模式虽然解决了**创建多个相似对象**的问题,但是没有解决对象识别问题,即不能知道一个对象的类型。 + +## 2.构造函数模式 +```js +function f (a, b){ + this.a = a; + this.b = b; + this.fun = function (){ + console.log(this.a) + } +} + +let f1 = new f('Leo','good'); +let f2 = new f('Robin','nice'); +``` +没有显示的创建对象,使用`new`来调用这个构造函数,使用`new`后会自动执行如下操作: +* 创建一个新对象 +* 这个新对象会被执行[[prototype]]链接 +* 这个新对象会绑定到函数调用的`this` +* 返回这个对象 + +使用这个方式创建对象可以检测对象类型 +```js +f1 instanceof Object; // true +f1 instanceof f; // true +``` +但是使用构造函数创建对象,每个方法都要在每个实例上**重新创建**一次。 + +## 3.原型模式 +```js +function f (){ + +} +f.prototype.a = 'Leo'; +f.prototype.b = 'good'; +f.prototype.fun = function (){ + console.log(this.a); +} +let f1 = new f(); +``` +将信息直接添加到原型对象上。使用原型的好处是可以让所有的实例对象共享它所包含的属性和方法,不必在构造函数中定义对象实例信息。 + +**更简单的写法** : +```js +function f (){ + +} +f.prototype = { + a : 'Leo'; + b : 'good'; + fun : function (){ + console.log(this.a); + } +} +let f1 = new f(); +``` +将`f.prototype`设置为等于一个以对象字面量形式创建的对象,但是会导致`.constructor`不在指向`f`了。 + +使用这种方式,完全重写了默认的`f.prototype`对象,因此 `.constructor`也不会存在这里。 +```js +f.prototype.constructor === f // false +``` +如果需要这个属性的话,可以手动添加: +```js +function f (){ + +} +f.prototype = { + constructor : f, + a : 'Leo'; + b : 'good'; + fun : function (){ + console.log(this.a); + } +} +let f1 = new f(); +``` +不过这种方式还是不够好,应为`constructor`属性默认是**不可枚举**的,这样直接设置,它将是可枚举的。所以可以时候,`Object.defineProperty`方法: +```js +Object.defineProperty(f.prototype, 'constructor', { + enumerable: false, + value: f +}) +``` +**缺点** + +使用原型,所有的属性都将被共享,这是个很大的优点,同样会带来一些缺点。 + +原型中所有属性实例是被很多实例共享的,这种共享对于函数非常合适。对于那些包含基本值的属性也勉强可以,毕竟实例属性可以屏蔽原型属性。但是引用类型值,就会出现问题了。 +```js +function f() { +} +f.prototype = { + a: 'leo', + b: ['good', 'nice'] +} +var f1 = new f() +var f2 = new f() +f1.b.push('hi') +console.log(f1.b) //["good", "nice", "hi"] +console.log(f2.b) //["good", "nice", "hi"] +console.log(f1.b === f2.b) // true +``` +`b`存在与原型中,实例f1和f2指向同一个原型,f1修改了引用的数组,也会反应到实例f2中。 + +## 4.组合使用构造函数模式和原型模式 +这是使用最为广泛、认同度最高的一种创建自定义类型的方法。它可以解决上面那些模式的缺点。 + +使用此模式可以让每个实例都会有自己的一份实例属性副本,但同时又共享着对方法的引用。 + +这样的话,即使实例属性修改引用类型的值,也不会影响其他实例的属性值了。 +```js +function f(a) { + this.a = a + this.friends = ['good', 'nice'] +} +f.prototype.fun = function() { + console.log(this.a) +} +var f1 = new f() +var f2 = new f() +f1.friends.push('hi') +console.log(f1.friends) //["good", "nice", "hi"] +console.log(f2.friends) // ["good", "nice"] +console.log(f1.friends === f2.friends) //false +``` + +## 5.动态原型模式 +动态原型模式将所有信息都封装在了构造函数中,初始化的时候,通过检测某个应该存在的方法时候有效,来决定是否需要初始化原型。 +```js +function f(a, b) { + // 属性 + this.a = a + this.b = b + + // 方法 + if(typeof this.fun !== 'function') { + f.prototype.fun = function() { + console.log(this.a) + } + } + +} +var f1 = new f('good', 'nice') +f1.fun() +``` +只有在`fun`方法不存在的时候,才会将它添加到原型中。这段代码只会初次调用构造函数的时候才会执行。 + +此后原型已经完成初始化,不需要在做什么修改了。 + +这里对原型所做的修改,能够立即在所有实例中得到反映。 + +其次,if语句检查的可以是初始化之后应该存在的任何属性或方法,所以不必用一大堆的if语句检查每一个属性和方法,只要检查一个就行。 + +## 6.寄生构造函数模式 +这种模式的基本思想就是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新建的对象 +```js +function f(a, b) { + var o = new Object() + o.a = a + o.b = b + o.fun = function() { + console.log(this.a) + } + return o +} +var f1 = new f('good', 'nice') +f1.fun() +``` +这个模式,除了使用`new`操作符并把使用的包装函数叫做构造函数之外,和工厂模式几乎一样。 + +构造函数如果不返回对象,默认也会返回一个新的对象,通过在构造函数的末尾添加一个`return`语句,可以重写调用构造函数时返回的值。 + +## 7.稳妥构造函数模式 +首先明白稳妥对象指的是没有公共属性,而且其方法也不引用`this`。 + +稳妥对象最适合在一些安全环境中(这些环境会禁止使用`this`和`new`),或防止数据被其他应用程序改动时使用。 + +稳妥构造函数模式和寄生模式类似,有两点不同:一是创建对象的实例方法不引用this,而是不使用new操作符调用构造函数。 +```js +function f(a, b) { + var o = new Object() + o.a = a + o.b = b + o.fun = function() { + console.log(a) + } + return o +} +var f1 = f('good', 'nice') +f1.fun() +``` +和寄生构造函数模式一样,这样创建出来的对象与构造函数之间没有什么关系,instanceof操作符对他们没有意义。 \ No newline at end of file diff --git "a/Cute-Article/article/69-\347\247\222\346\207\202this.md" "b/Cute-Article/article/69-\347\247\222\346\207\202this.md" new file mode 100644 index 00000000..79002c6e --- /dev/null +++ "b/Cute-Article/article/69-\347\247\222\346\207\202this.md" @@ -0,0 +1,317 @@ +> [打开原文](segmentfault.com/a/1190000017075730) + +日常开发中经常会遇到 `this` 指向的 bug,郁闷好久才猛然醒悟,痛定思痛,将 `this` 做个汇总,以便在日后的开发工作中少走弯路。 +注意:本文讲述只针对浏览器环境。 + +# 一:全局执行 +```js +console.log(this); +// Window +``` +可以看出在全局作用域中 `this` 指向当前的全局对象 `Window`。 + +# 二:函数中执行 +## 1. 非严格模式中 +```js +function func () { + console.log(this); +} +func(); +// Window +``` + +## 2. 严格模式中 +```js +"use strict"; +function func () { + console.log(this); +} +func(); +// undefined +``` + +# 三:作为对象的方法调用 +当一个函数被当作一个对象的方法调用的时候,`this` 指向当前的对象 `obj`: +```js +var obj = { + name: 'kk', + func: function () { + console.log(this.name); + } +} +obj.func(); +// kk +``` +如果把对象的方法赋值给一个变量,调用该方法时,`this` 指向` Window`: +```js +var obj = { + name: 'kk', + func: function () { + console.log(this); + } +} +var test = obj.func; +test(); +// Window +``` +# 四:作为一个构造函数使用 +在 JS 中,为了实现类,我们需要定义一些构造函数,在调用一个构造函数的时候加上 `new `这个关键字: +```js +function Person (name) { + this.name = name; + console.log(this); +} +var p1 = new Person('kk'); +// Person +``` +此时,`this` 指向这个构造函数调用的时候实例化出来的对象。 + +当然了,构造函数其实也是一个函数,若将构造函数当做普通函数来调用,`this` 指向 `Window`: +```js +function Person (name) { + this.name = name; + console.log(this); +} +var p2 = Person('MM'); +// Window +``` + +# 五:在定时器中使用 +```js +setInterval(function () { + console.log(this); +}, 2000) +// Window +setTimeout(function () { + console.log(this); +}, 0) +// Window +``` +如果没有特殊指向(指向更改请看下方:怎么改变 `this` 的指向),`setInterval` 和`setTimeout` 的回调函数中 `this` 的指向都是 `Window` 。这是因为 JS 的定时器方法是定义在` Window` 下的。 + +# 六:箭头函数 +在全局环境中调用: +```js +var func = () => { + console.log(this); +} +func(); +// Window +``` +作为对象的一个函数调用: +```js +var obj = { + name: 'hh', + func: function () { + console.log(this); + } +} +obj.func(); +// obj +var obj = { + name: 'hh', + func: () => { + console.log(this); + } +} +obj.func(); +// Window +``` +不难发现,普通函数作为对象的一个函数被调用,`this` 指向 `obj`,箭头函数作为对象的一个函数被调用,`this` 指向 `Window`。 + +特殊情况:结合定时器调用: +```js +var obj = { + name: 'hh', + func: function () { + setTimeout(function () { + console.log(this); + }, 0) + } +} +obj.func(); +// Window +var obj = { + name: 'hh', + func: function () { + setTimeout(() => { + console.log(this); + }, 0) + } +} +obj.func(); +// obj +``` +若在对象的函数中,普通函数作为定时器延时执行的函数调用,`this` 指向 `Window`;箭头函数作为定时器延时执行的函数调用, `this` 指向定义时所在的对象,也就是 `func `中的 `this`,即` obj`。 + +箭头函数中 `this `的值取决于该函数外部非箭头函数的`this `的值,且不能通过 `call()` 、` apply()` 和 `bind()` 方法来改变 `this` 的值。 +# 七:call、apply、bind +**1.call**: +```js +fun.call(thisArg[, arg1[, arg2[, ...]]]) +``` +它会立即执行函数,第一个参数是指定执行函数中 `this` 的上下文,后面的参数是执行函数需要传入的参数; + +**apply**: +```js +fun.apply(thisArg, [argsArray]) +``` +它也会立即执行函数,第一个参数是指定执行函数中 `this` 的上下文,第二个参数是一个数组,是传给执行函数的参数(与 `call` 的区别); + +**bind**: +```js +var foo = fun.bind(thisArg[, arg1[, arg2[, ...]]]); +``` +它不会执行函数,而是返回一个新的函数,这个新的函数被指定了 `this` 的上下文,后面的参数是执行函数需要传入的参数; + +我们来看个示例: +```js +function Person(name, age) { + this.name = name; + this.age = age; + console.log(this); +} +var obj = { + name: 'kk', + age: 6 +}; +Person.call(obj, 'mm', 10); +// obj,{name: "mm", age: 10} + +Person.apply(obj, ['mm', 10]); +// obj,{name: "mm", age: 10} + +var p1 = Person.bind(obj, 'mm', 10) +var p2 = new p1(); +// Person {name: "mm", age: 10} +``` +在这个示例中,`call`、`apply` 和 `bind` 的 `this` 都指向了` obj`,都能正常运行;`call`、`apply `会立即执行函数,`call` 和 `apply` 的区别就在于传递的参数,`call` 接收多个参数列表,`apply` 接收一个包含多个参数的数组;`bind` 不是立即执行函数,它返回一个函数,需要执行 `p2` 才能返回结果,`bind` 接收多个参数列表。 + +## 应用:怎么改变 this 的指向 +为什么讲这个模块呢,为了便于大家更加透彻的理解上面所讲述的 `this` 指向问题,以及更加彻底的理解 JS 函数中重要的三种方法:`call`、`apply`、`bind` 的使用;并且在实际的项目开发中,我们经常会遇到需要改变 this 指向的情况。 + +我们来看下都有哪些方法: + +## 1. 使用 es6 的箭头函数 +```js +var name = "hh"; +var obj = { + name : "kk", + func1: function () { + console.log(this.name) + }, + func2: function () { + setTimeout(function () { + this.func1() + }, 1000); + } +}; + +obj.func2(); +// Uncaught TypeError: this.func1 is not a function +``` +这时会报错,因为 `setTimeout` 里函数的 `thi`s 指向 `Window`,而 `Window` 对象上是没有 `func1` 这个函数的。下面我们来修改成箭头函数: +```js +var name = "hh"; +var obj = { + name : "kk", + func1: function () { + console.log(this.name) + }, + func2: function () { + setTimeout(() => { + this.func1() + }, 1000); + } +}; + +obj.func2(); +// kk +``` +这时候,没有报错,因为箭头函数的` this` 的值取决于该函数外部非箭头函数的 `this` 的值,也就是 `func2` 的 `this `的值, 即` obj`。 + +## 2. 在函数内部使用 _this = this +```js +var name = "hh"; +var obj = { + name : "kk", + func1: function () { + console.log(this.name) + }, + func2: function () { + let _this = this; + setTimeout(function () { + _this.func1() + }, 1000); + } +}; + +obj.func2(); +// kk +``` +此时,`func2` 也能正常运行。在` func`2 中,首先设置 `var _this = this`,这里的 `this` 是指向 `func2` 的对象 `obj`,为了防止在 `func2` 中的 `setTimeout` 被 `window` 调用而导致的在 `setTimeout` 中的 `this` 为 window。我们将 `this` (指向变量 `obj`) 赋值给一个变量 `_this`,这样,在 `func2` 中我们使用 `_this` 就是指向对象 `obj` 了。 + +## 3. 使用 call、apply、bind + +**call**: +```js +var name = "hh"; +var obj = { + name : "kk", + func1: function () {s + console.log(this.name) + }, + func2: function () { + setTimeout(function () { + this.func1() + }.call(obj), 1000); + } +}; + +obj.func2(); +// kk +``` + +**apply**: +```js +var name = "hh"; +var obj = { + name : "kk", + func1: function () { + console.log(this.name) + }, + func2: function () { + setTimeout(function () { + this.func1() + }.apply(obj), 1000); + } +}; + +obj.func2(); +// kk +``` + +**bind**: +```js +var name = "hh"; +var obj = { + name : "kk", + func1: function () { + console.log(this.name) + }, + func2: function () { + setTimeout(function () { + this.func1() + }.bind(obj)(), 1000); + } +}; + +obj.func2(); +// kk +``` +`call`、`apply`、`bind` 都能改变` this` 的上下文对象,所以也没有报错,可以正常执行。 + +具体原因可看上述第七点,`call`、`apply`、`bind`。 + +## 4. new 实例化一个对象 +如上:第四点,作为一个构造函数使用。 \ No newline at end of file diff --git "a/7-[\345\216\237\345\210\233]\347\274\251\345\260\217Vuejs\346\211\223\345\214\205\344\275\223\347\247\257\346\226\271\346\263\225.md" "b/Cute-Article/article/7-[\345\216\237\345\210\233]\347\274\251\345\260\217Vuejs\346\211\223\345\214\205\344\275\223\347\247\257\346\226\271\346\263\225.md" similarity index 100% rename from "7-[\345\216\237\345\210\233]\347\274\251\345\260\217Vuejs\346\211\223\345\214\205\344\275\223\347\247\257\346\226\271\346\263\225.md" rename to "Cute-Article/article/7-[\345\216\237\345\210\233]\347\274\251\345\260\217Vuejs\346\211\223\345\214\205\344\275\223\347\247\257\346\226\271\346\263\225.md" diff --git "a/Cute-Article/article/70-JS\345\244\215\346\235\202\345\210\244\346\226\255\347\232\204\346\233\264\344\274\230\351\233\205\345\206\231\346\263\225.md" "b/Cute-Article/article/70-JS\345\244\215\346\235\202\345\210\244\346\226\255\347\232\204\346\233\264\344\274\230\351\233\205\345\206\231\346\263\225.md" new file mode 100644 index 00000000..84ec490e --- /dev/null +++ "b/Cute-Article/article/70-JS\345\244\215\346\235\202\345\210\244\346\226\255\347\232\204\346\233\264\344\274\230\351\233\205\345\206\231\346\263\225.md" @@ -0,0 +1,303 @@ +> [阅读原文](https://juejin.im/post/5bdfef86e51d453bf8051bf8) + + +## 前提 +我们编写js代码时经常遇到复杂逻辑判断的情况,通常大家可以用`if/else`或者`switch`来实现多个条件判断,但这样会有个问题,随着逻辑复杂度的增加,代码中的`if/else/switch`会变得越来越臃肿,越来越看不懂,那么如何更优雅的写判断逻辑,本文带你试一下。 + +## 举个例子 +先看一段代码: +```js +/** + * 按钮点击事件 + * @param {number} status + * 活动状态:1 开团进行中 2 开团失败 3 商品售罄 4 开团成功 5 系统取消 + */ +const onButtonClick = (status)=>{ + if(status == 1){ + sendLog('processing') + jumpTo('IndexPage') + }else if(status == 2){ + sendLog('fail') + jumpTo('FailPage') + }else if(status == 3){ + sendLog('fail') + jumpTo('FailPage') + }else if(status == 4){ + sendLog('success') + jumpTo('SuccessPage') + }else if(status == 5){ + sendLog('cancel') + jumpTo('CancelPage') + }else { + sendLog('other') + jumpTo('Index') + } +} +``` +通过代码可以看到这个按钮的点击逻辑:根据不同活动状态做两件事情,发送日志埋点和跳转到对应页面,大家可以很轻易的提出这段代码的改写方案,`switch`出场: +```js +/** + * 按钮点击事件 + * @param {number} status + * 活动状态:1 开团进行中 2 开团失败 3 商品售罄 4 开团成功 5 系统取消 + */ +const onButtonClick = (status)=>{ + switch (status){ + case 1: + sendLog('processing') + jumpTo('IndexPage') + break + case 2: + case 3: + sendLog('fail') + jumpTo('FailPage') + break + case 4: + sendLog('success') + jumpTo('SuccessPage') + break + case 5: + sendLog('cancel') + jumpTo('CancelPage') + break + default: + sendLog('other') + jumpTo('Index') + break + } +} +``` +嗯,这样看起来比`if/else`清晰多了,细心的同学也发现了小技巧,`case 2`和`case 3`逻辑一样的时候,可以省去执行语句和`break`,则`case 2`的情况自动执行`case 3`的逻辑。 +这时有同学会说,还有更简单的写法: +```js +const actions = { + '1': ['processing','IndexPage'], + '2': ['fail','FailPage'], + '3': ['fail','FailPage'], + '4': ['success','SuccessPage'], + '5': ['cancel','CancelPage'], + 'default': ['other','Index'], +} +/** + * 按钮点击事件 + * @param {number} status + * 活动状态:1开团进行中 2开团失败 3 商品售罄 4 开团成功 5 系统取消 + */ +const onButtonClick = (status)=>{ + let action = actions[status] || actions['default'], + logName = action[0], + pageName = action[1] + sendLog(logName) + jumpTo(pageName) +} +``` +上面代码确实看起来更清爽了,这种方法的聪明之处在于:将判断条件作为对象的属性名,将处理逻辑作为对象的属性值,在按钮点击的时候,通过对象属性查找的方式来进行逻辑判断,这种写法特别适合一元条件判断的情况。 +是不是还有其他写法呢?有的: +```js +const actions = new Map([ + [1, ['processing','IndexPage']], + [2, ['fail','FailPage']], + [3, ['fail','FailPage']], + [4, ['success','SuccessPage']], + [5, ['cancel','CancelPage']], + ['default', ['other','Index']] +]) +/** + * 按钮点击事件 + * @param {number} status + * 活动状态:1 开团进行中 2 开团失败 3 商品售罄 4 开团成功 5 系统取消 + */ +const onButtonClick = (status)=>{ + let action = actions.get(status) || actions.get('default') + sendLog(action[0]) + jumpTo(action[1]) +} +``` +这样写用到了`es6`里的`Map`对象,是不是更爽了?`Map`对象和`Object`对象有什么区别呢? + +* 一个对象通常都有自己的原型,所以一个对象总有一个"`prototype`"键。 +* 一个对象的键只能是**字符串**或者**Symbols**,但一个`Map`的键可以是任意值。 +你可以通过`size`属性很容易地得到一个`Map`的键值对个数,而对象的键值对个数只能手动确认。 + +我们需要把问题升级一下,以前按钮点击时候只需要判断`status`,现在还需要判断用户的身份: +```js +/** + * 按钮点击事件 + * @param {number} status + * 活动状态:1开团进行中 2开团失败 3 开团成功 4 商品售罄 5 有库存未开团 + * @param {string} identity + * 身份标识:guest客态 master主态 + */ +const onButtonClick = (status,identity)=>{ + if(identity == 'guest'){ + if(status == 1){ + //do sth + }else if(status == 2){ + //do sth + }else if(status == 3){ + //do sth + }else if(status == 4){ + //do sth + }else if(status == 5){ + //do sth + }else { + //do sth + } + }else if(identity == 'master') { + if(status == 1){ + //do sth + }else if(status == 2){ + //do sth + }else if(status == 3){ + //do sth + }else if(status == 4){ + //do sth + }else if(status == 5){ + //do sth + }else { + //do sth + } + } +} +``` +原谅我不写每个判断里的具体逻辑了,因为代码太冗长了。 +原谅我又用了`if/else`,因为我看到很多人依然在用`if/else`写这种大段的逻辑判断。 +从上面的例子我们可以看到,当你的逻辑升级为二元判断时,你的判断量会加倍,你的代码量也会加倍,这时怎么写更清爽呢? +```js +const actions = new Map([ + ['guest_1', ()=>{/*do sth*/}], + ['guest_2', ()=>{/*do sth*/}], + ['guest_3', ()=>{/*do sth*/}], + ['guest_4', ()=>{/*do sth*/}], + ['guest_5', ()=>{/*do sth*/}], + ['master_1', ()=>{/*do sth*/}], + ['master_2', ()=>{/*do sth*/}], + ['master_3', ()=>{/*do sth*/}], + ['master_4', ()=>{/*do sth*/}], + ['master_5', ()=>{/*do sth*/}], + ['default', ()=>{/*do sth*/}], +]) + +/** + * 按钮点击事件 + * @param {string} identity 身份标识:guest客态 master主态 + * @param {number} status 活动状态:1 开团进行中 2 开团失败 3 开团成功 4 商品售罄 5 有库存未开团 + */ +const onButtonClick = (identity,status)=>{ + let action = actions.get(`${identity}_${status}`) || actions.get('default') + action.call(this) +} +``` +上述代码核心逻辑是:把两个条件拼接成字符串,并通过以条件拼接字符串作为键,以处理函数作为值的Map对象进行查找并执行,这种写法在多元条件判断时候尤其好用。 +当然上述代码如果用`Object`对象来实现也是类似的: +```js +const actions = { + 'guest_1':()=>{/*do sth*/}, + 'guest_2':()=>{/*do sth*/}, + //.... +} + +const onButtonClick = (identity,status)=>{ + let action = actions[`${identity}_${status}`] || actions['default'] + action.call(this) +} +``` +如果有些同学觉得把查询条件拼成字符串有点别扭,那还有一种方案,就是用`Map`对象,以`Object`对象作为`key`: +```js +const actions = new Map([ + [{identity:'guest',status:1},()=>{/*do sth*/}], + [{identity:'guest',status:2},()=>{/*do sth*/}], + //... +]) + +const onButtonClick = (identity,status)=>{ + let action = [...actions].filter(([key,value])=>(key.identity == identity && key.status == status)) + action.forEach(([key,value])=>value.call(this)) +} +``` +是不是又高级了一点点? +这里也看出来`Map`与`Object`的区别,`Map`可以用任何类型的数据作为`key`。 +我们现在再将难度升级一点点,假如`guest`情况下,`status1-4`的处理逻辑都一样怎么办,最差的情况是这样: +```js +const actions = new Map([ + [{identity:'guest',status:1},()=>{/* functionA */}], + [{identity:'guest',status:2},()=>{/* functionA */}], + [{identity:'guest',status:3},()=>{/* functionA */}], + [{identity:'guest',status:4},()=>{/* functionA */}], + [{identity:'guest',status:5},()=>{/* functionB */}], + //... +]) +``` +好一点的写法是将处理逻辑函数进行缓存: +```js +const actions = ()=>{ + const functionA = ()=>{/*do sth*/} + const functionB = ()=>{/*do sth*/} + return new Map([ + [{identity:'guest',status:1},functionA], + [{identity:'guest',status:2},functionA], + [{identity:'guest',status:3},functionA], + [{identity:'guest',status:4},functionA], + [{identity:'guest',status:5},functionB], + //... + ]) +} + +const onButtonClick = (identity,status)=>{ + let action = [...actions()].filter(([key,value])=>(key.identity == identity && key.status == status)) + action.forEach(([key,value])=>value.call(this)) +} +``` +这样写已经能满足日常需求了,但认真一点讲,上面重写了4次`functionA`还是有点不爽,假如判断条件变得特别复杂,比如`identity`有3种状态,`status`有10种状态,那你需要定义30条处理逻辑,而往往这些逻辑里面很多都是相同的,这似乎也是笔者不想接受的,那可以这样实现: +```js +const actions = ()=>{ + const functionA = ()=>{/*do sth*/} + const functionB = ()=>{/*do sth*/} + return new Map([ + [/^guest_[1-4]$/,functionA], + [/^guest_5$/,functionB], + //... + ]) +} + +const onButtonClick = (identity,status)=>{ + let action = [...actions()].filter(([key,value])=>(key.test(`${identity}_${status}`))) + action.forEach(([key,value])=>value.call(this)) + +} +``` +这里Map的优势更加凸显,可以用正则类型作为`key`了,这样就有了无限可能,假如需求变成,凡是`guest`情况都要发送一个日志埋点,不同`status`情况也需要单独的逻辑处理,那我们可以这样写: +```js +const actions = ()=>{ + const functionA = ()=>{/*do sth*/} + const functionB = ()=>{/*do sth*/} + const functionC = ()=>{/*send log*/} + return new Map([ + [/^guest_[1-4]$/,functionA], + [/^guest_5$/,functionB], + [/^guest_.*$/,functionC], + //... + ]) +} + +const onButtonClick = (identity,status)=>{ + let action = [...actions()].filter(([key,value])=>(key.test(`${identity}_${status}`))) + action.forEach(([key,value])=>value.call(this)) +} +``` +也就是说利用数组循环的特性,符合正则条件的逻辑都会被执行,那就可以同时执行公共逻辑和单独逻辑,因为正则的存在,你可以打开想象力解锁更多的玩法,本文就不赘述了。 + +## 总结 +本文已经教你了8种逻辑判断写法,包括: + +* `if/else` +* `switch` +* 一元判断时:存到`Object`里 +* 一元判断时:存到`Map`里 +* 多元判断时:将`condition`拼接成字符串存到`Object`里 +* 多元判断时:将`condition`拼接成字符串存到`Map`里 +* 多元判断时:将`condition`存为`Object`存到`Map`里 +* 多元判断时:将`condition`写作正则存到`Map`里 + +至此,本文也将告一段落,愿你未来的人生里,不只是有`if/else/switch`。 +如果你对本文感兴趣,请关注作者微信公众号:“大转转fe” \ No newline at end of file diff --git "a/Cute-Article/article/71-\345\244\215\344\271\240instanceof\350\277\220\347\256\227\347\254\246.md" "b/Cute-Article/article/71-\345\244\215\344\271\240instanceof\350\277\220\347\256\227\347\254\246.md" new file mode 100644 index 00000000..28c506fa --- /dev/null +++ "b/Cute-Article/article/71-\345\244\215\344\271\240instanceof\350\277\220\347\256\227\347\254\246.md" @@ -0,0 +1,142 @@ +最近开始在整理`ES6/ES7/ES8/ES9`的知识点(已经上传到 [我的博客](http://es.pingan8787.com) 上),碰到一些知识点是自己已经忘记(用得少的知识点),于是也重新复习了一遍。 +这篇文章要复习的 `instanceof` 是我在整理过程中遇到的,那就整理下来吧,不然容易忘记。 +要是哪里写得不妥,欢迎各位大佬指点。 + + +## 1.定义 +> `instanceof`运算符用于测试构造函数的`prototype`属性是否出现在对象的原型链中的任何位置。 —— MDN + +简单理解为:`instanceof`可以检测一个实例是否属于某种类型。 +比如: +```js +function F (){ + // ... +} +let a = new F (); + +a instanceof F; // true +a instanceof Object; // true 后面介绍原因 +``` +还可以在**继承关系**中用来判断一个实例是否属于它的父类型。 +比如: +```js +function F (){}; +function G (){}; +function Q (){}; +G.prototype = new F(); // 继承 + +let a = new G(); +a instanceof F; // true +a instanceof G; // true +a instanceof Q; // false +``` + +## 2.使用方法 +语法为: `object instanceof constructor`。 +* `object` : 需要测试的函数 +* `constructor` : 构造函数 + +即:用`instanceof`运算符来检测`constructor.prototype` 是否存在参数`object`的原型链。 +```js +function F (){}; +function G (){}; +let a = new F (); + +a instanceof F; // true 因为:Object.getPrototypeOf(a) === F.prototype +a instanceof Q; // false 因为:F.prototype不在a的原型链上 +a instanceof Object; // true 因为:Object.prototype.isPrototypeOf(a)返回true +F.prototype instanceof Object; // true,同上 +``` +**注意**: +1. `a instanceof F` 返回 `true` 以后,不一定永远都都返回为`true`,`F.prototype`属性的值有可能会改变。 +2. 原表达式`a`的值也会改变,比如 `a.__proto__ = {}`之后,`a instanceof F`就会返回`false`了。 + +**检测对象是不是特定构造函数的实例:** +```js +// 正确 +if (!(obj instanceof F)) { + // ... +} + +// 错误 因为 +if (!obj instanceof F); // 永远返回false +// 因为 !obj 在instanceof之前被处理 , 即一直使用一个布尔值检测是否是F的实例 +``` + +## 3.实现instanceof +```js +/** +* 实现instanceof +* @param obj{Object} 需要测试的对象 +* @param fun{Function} 构造函数 +*/ +function _instanceof(obj, fun) { + let f = fun.prototype; // 取B的显示原型 + obj = obj.__proto__; // 取A的隐式原型 + while (true) { + //Object.prototype.__proto__ === null + if (obj === null) + return false; + if (f === obj) // 这里重点:当 f 严格等于 obj 时,返回 true + return true; + obj = obj.__proto__; + } +} +``` + +## 4.instanceof 与 typeof 对比 +**相同**: +`instanceof`和`typeof`都能用来判断一个变量的类型。 + +**区别**: +`instanceof` 只能用来判断对象、函数和数组,不能用来判断字符串和数字等: +```js +let a = {}; +let b = function () {}; +let c = []; +let d = 'hi'; +let e = 123; + +a instanceof Object; // true +b instanceof Object; // true +c instanceof Array; // true +d instanceof String; // false +e instanceof Number; // false +``` + +`typeof` :用于判断一个表达式的原始值,返回一个字符串。 +```js +typeof 42; // "number" +typeof 'blubber'; // "string" +typeof true; // "boolean" +typeof aa; // "undefined" +``` +一般返回结果有: +* number +* boolean +* string +* function(函数) +* object(NULL,数组,对象) +* undefined。 + +**判断变量是否存在**: +不能使用: +```js +if(a){ + //变量存在 +} +// Uncaught ReferenceError: a is not defined +``` +原因是如果变量未定义,就会报**未定义**的错,而应该使用: +```js +if(typeof a != 'undefined'){ + //变量存在 +} +``` + +## 5.参考资料 +1. [MDN instanceof](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/instanceof) + +2. [IBM instanceof](https://www.ibm.com/developerworks/cn/web/1306_jiangjj_jsinstanceof/index.html) + +3. [js中typeof和instanceof用法区别](https://blog.csdn.net/qq_27626333/article/details/76146078) \ No newline at end of file diff --git "a/Cute-Article/article/72-\343\200\220\351\207\215\346\270\251\345\237\272\347\241\200\343\200\221JS\344\270\255\347\232\204\351\253\230\351\230\266\345\207\275\346\225\260.md" "b/Cute-Article/article/72-\343\200\220\351\207\215\346\270\251\345\237\272\347\241\200\343\200\221JS\344\270\255\347\232\204\351\253\230\351\230\266\345\207\275\346\225\260.md" new file mode 100644 index 00000000..76db077e --- /dev/null +++ "b/Cute-Article/article/72-\343\200\220\351\207\215\346\270\251\345\237\272\347\241\200\343\200\221JS\344\270\255\347\232\204\351\253\230\351\230\266\345\207\275\346\225\260.md" @@ -0,0 +1,206 @@ +Ps. 晚上加班到快十点,回来赶紧整理整理这篇文章,今天老大给我推荐了一篇文章,[我从写技术博客中收获到了什么?- J_Knight_](https://juejin.im/post/5c02b2bce51d4533253f2f43),感受也是很多,自己也需要慢慢养成记录博客的习惯,即使起步艰难,难以坚持,但还是要让自己加油加油。 +前两天把我整理的[【复习资料】ES6/ES7/ES8/ES9资料整理(个人整理)](https://juejin.im/post/5c02b106f265da61764aa0c1)要分享给大家啦。 + +正文内容开始: + +# 1.介绍 +个人简单理解为,一个函数可以接收其他函数作为参数,这种函数便称为**高阶函数**,而主要目的就是为了能接收其他函数作为参数。 +> Q: 为什么可以接收一个函数作为参数? +> A: 因为函数可以接收变量作为参数,而变量可以声明一个方法。 + +**简单实例:** +```js +function a (x){ + return 'hi ' + x; +} +function f (a, b){ + return a(b); +} +f(a, 'leo'); // "hi leo" +``` +**这段代码的意思**:定义方法`f`,接收两个参数,方法`a`和变量`b`,在方法`a`中返回一段字符串,当执行方法`f`并传入参数方法`a`和参数`b`的时候,返回`"hi leo"`。 + +也可以直接调用JS内置方法: +```js +let a = 3, b = -2; +function my_abs (val, fun){ + return fun(val); +} +my_abs(a, Math.abs); // 3 +my_abs(b, Math.abs); // 2 +``` + +# 2.常用高阶函数 + +## 2.1 map() +`map()`方法的作用是:接收一个函数作为参数,对数组中每个元素按顺序调用一次传入的函数并返回结果,**不改变原数组,返回一个新数组**。 +通常使用方式:`arr.map(callback())`,更多详细介绍可以参考 [MDN Array.map()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/map)。 +**参数**: +* `arr` : 需要操作的数组; +* `callback(currentValue, index, array, thisArg)` : 处理的方法,四个参数; + * 1. `currentValue` 当前处理的元素的**值** + * 2. `index` 当前处理的元素的**索引**,可选 + * 3. `array` 调用`map()`方法的**数组**,可选 + * 4. `currentVthisArgalue` 执行 `callback` 函数时使用的 `this` 值,可选 + +**返回值**: +返回一个处理后的新数组。 + +**实例**: +```js +let arr = [1, 3, -5]; +let a1 = arr.map(Math.abs); +// a1 => [1, 3, 5]; + +let a2 = arr.map(String); +// a2 => ["1", "3", "-5"] + +let a3 = arr.map(function (x){ + return x + 1; +}) +// 等价于 a3=arr.map(x => x+1) +// a3 => [2, 4, -4] +``` +对比 `for...in` 循环,`map()`书写起来更加简洁: +```js +let arr = [1, 3, -5]; +let a1 = []; +for (var i=0; i [2, 4, -4] +``` +`map()`作为高阶函数,事实上它把运算规则抽象了。 + +## 2.2 reduce() +`reduce()`方法的作用是:接收一个函数,对数组进行累加操作,把累计结果和下一个值进行操作,最后返回最终的单个结果值。 + +通常使用方式:`arr.reduce(callback(), initValue)`,更多详细介绍可以参考 [MDN Array.reduce()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce) + +**参数**: +* `callback(returnValue, currentValue, currentIndex, array)` : 累记器的方法,四个参数: + * 1. `returnValue` 上一次处理的返回值,或者初始值 + * 2. `currentValue` 当前处理的元素的**值**,可选 + * 3. `currentIndex` 当前处理的元素的**索引**,可选 + * 4. `array` 调用`reduce()`方法的**数组**,可选 +* `initValue` 初次调用`callback()`时候`returnValue`参数的初始值,默认数组第一个元素,可选 + +**返回值**: +返回一个最终的累计值。 + +**实例**: +1. 数组求和 +```js +let arr = [1, 3, -5]; +let sum1 = arr.reduce((res, cur) => res + cur); +// sum1 => -1 + +let sum2 = arr.reduce((res, cur) => res + cur , 1); +// sum1 => 0 +``` +2. 二维数组转化为一维 +```js +let arr = [[1, 2], [3, 4], [5, 6]]; +let con = arr.reduce((res, cur) => res.concat(cur)); +// con => [1, 2, 3, 4, 5, 6] +``` + +## 2.3 filter() +`filter()`方法的作用是:接收一个函数,依次作用数组每个元素,并过滤符合函数条件的元素,将剩下的数组作为一个新数组返回。 + +通常使用方式:`arr.filter(callback(), thisArg)`,更多详细介绍可以参考 [MDN Array.filter()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/filter) + +**参数**: +* `callback(ele, index, array)` : 过滤条件的方法,当返回`true`则保存该元素,反之不保留,三个参数: + * 1. `ele` 当前处理的元素 + * 2. `index` 当前处理的元素的**索引**,可选 + * 3. `array` 调用`filter()`方法的**数组**,可选 +* `thisArg` 执行 `callback` 时的用于 `this` 的值,可选 + +**返回值**: +返回一个过滤剩下的元素组成的新数组。 + +**实例**: +1. 过滤奇数值 +```js +let arr = [1, 2, 3, 4, 5, 6]; +let res = arr.filter(x => x % 2 != 0); +// res => [1, 3, 5] +``` +2. 过滤不满足条件的值 +```js +let arr = [1, 2, 3, 4, 5, 6]; +let res = arr.filter(x => x > 3); +// res => [4, 5, 6] +``` +3. 过滤空字符串 +```js +let arr = ['a', '', null, undefined, 'b', '']; +let tri = arr.filter(x => x && x.trim()); +// tri => ["a", "b"] +``` +总结下: `filter()`主要作为**筛选功能**,因此核心就是正确实现一个“筛选”函数。 + +## 2.4 sort() +`sort()`方法的作用是:接收一个函数,对数组的元素进行排序,并返回排序后的新数组。**默认排序顺序是根据字符串Unicode码点**。 + +通常使用方式:`arr.sort(fun())`,更多详细介绍可以参考 [MDN Array.sort()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) +compareFunction 可选 +用来指定按某种顺序进行排列的函数。如果省略,元素按照转换为的字符串的各个字符的Unicode位点进行排序。 +**参数**: +* `fun(a, b)` : 指定按某种顺序进行排列的函数,若省略则按照转换为的字符串的各个字符的Unicode位点进行排序,两个可选参数: +`fun()` 返回 `a`和`b`两个值的大小的比较结果,`sort()`根据返回结果进行排序: +* 若 `fun(a, b)` 小于 0 ,则 `a` 排在 `b` 前面; +* 若 `fun(a, b)` 等于 0 ,则 `a` `b` 位置不变; +* 若 `fun(a, b)` 大于 0 ,则 `a` 排在 `b` 后面; + + +**返回值**: +返回排序后的新数组,并**修改原数组**。 + +**实例**: +1. 升序降序数组 +```js +let arr = [1,5,2,3]; +let sort1 = arr.sort(); +// 等同于 let sort1 = arr.sort((a, b) => a - b); +// sort1 => [1, 2, 3, 5] + +let sort2 = arr.sort((a, b) => b - a); +// sort2 => [5, 3, 2, 1] +``` +2. 字符串排序 +```js +let arr1 = ['AA', 'CC', 'BB']; +let sort1 = arr1.sort(); +// sort1 => ["AA", "BB", "CC"] + +let arr2 = ['AA', 'aa', 'BB']; +let sort2 = arr2.sort(); +// sort2 => ["AA", "BB", "aa"] + +let arr3 = ['AA', 'aa', 'BB']; +let sort3 = arr3.sort((a, b) => a.toLowerCase() > b.toLowerCase()); +// sort3 => ["AA", "aa", "BB"] +// 也可以写成: +let sort3 = arr3.sort((a, b) => { + let s1 = a.toLowerCase(); + let s2 = b.toLowerCase(); + return s1 > s2 ? 1 : s1 < s2 ? -1 : 0; +}) +``` +总结下: `sort()`主要作为**排序功能**,因此核心就是正确实现一个“排序”函数。 + +# 3. 参考文章 +* [阮一峰 JS高阶函数](https://www.liaoxuefeng.com/wiki/001434446689867b27157e896e74d51a89c25cc8b43bdb3000/001434499355829ead974e550644e2ebd9fd8bb1b0dd721000) + +分享的内容比较简单,但是还是希望能帮助到需要的人哈。~~感谢 + + +|Author|王平安| +|---|---| +|E-mail|pingan8787@qq.com| +|博 客|www.pingan8787.com| +|微 信|pingan8787| +|每日文章推荐|https://github.com/pingan8787/Leo_Reading/issues| +|JS小册|js.pingan8787.com| diff --git "a/Cute-Article/article/73-JavaScript \351\207\215\346\226\260\345\212\240\350\275\275\351\241\265\351\235\242\347\232\204 535 \347\247\215\346\226\271\346\263\225.md" "b/Cute-Article/article/73-JavaScript \351\207\215\346\226\260\345\212\240\350\275\275\351\241\265\351\235\242\347\232\204 535 \347\247\215\346\226\271\346\263\225.md" new file mode 100644 index 00000000..1631a778 --- /dev/null +++ "b/Cute-Article/article/73-JavaScript \351\207\215\346\226\260\345\212\240\350\275\275\351\241\265\351\235\242\347\232\204 535 \347\247\215\346\226\271\346\263\225.md" @@ -0,0 +1,539 @@ +使用 JavaScript 有多少种方式重新加载页面? + +```js +location = location +location = location.href +location = window.location +location = self.location +location = window.location.href +location = self.location.href +location = location['href'] +location = window['location'] +location = window['location'].href +location = window['location']['href'] +location = window.location['href'] +location = self['location'] +location = self['location'].href +location = self['location']['href'] +location = self.location['href'] +location.assign(location) +location.replace(location) +window.location.assign(location) +window.location.replace(location) +self.location.assign(location) +self.location.replace(location) +location['assign'](location) +location['replace'](location) +window.location['assign'](location) +window.location['replace'](location) +window['location'].assign(location) +window['location'].replace(location) +window['location']['assign'](location) +window['location']['replace'](location) +self.location['assign'](location) +self.location['replace'](location) +self['location'].assign(location) +self['location'].replace(location) +self['location']['assign'](location) +self['location']['replace'](location) +location.href = location +location.href = location.href +location.href = window.location +location.href = self.location +location.href = window.location.href +location.href = self.location.href +location.href = location['href'] +location.href = window['location'] +location.href = window['location'].href +location.href = window['location']['href'] +location.href = window.location['href'] +location.href = self['location'] +location.href = self['location'].href +location.href = self['location']['href'] +location.href = self.location['href'] +location.assign(location.href) +location.replace(location.href) +window.location.assign(location.href) +window.location.replace(location.href) +self.location.assign(location.href) +self.location.replace(location.href) +location['assign'](location.href) +location['replace'](location.href) +window.location['assign'](location.href) +window.location['replace'](location.href) +window['location'].assign(location.href) +window['location'].replace(location.href) +window['location']['assign'](location.href) +window['location']['replace'](location.href) +self.location['assign'](location.href) +self.location['replace'](location.href) +self['location'].assign(location.href) +self['location'].replace(location.href) +self['location']['assign'](location.href) +self['location']['replace'](location.href) +window.location = location +window.location = location.href +window.location = window.location +window.location = self.location +window.location = window.location.href +window.location = self.location.href +window.location = location['href'] +window.location = window['location'] +window.location = window['location'].href +window.location = window['location']['href'] +window.location = window.location['href'] +window.location = self['location'] +window.location = self['location'].href +window.location = self['location']['href'] +window.location = self.location['href'] +location.assign(window.location) +location.replace(window.location) +window.location.assign(window.location) +window.location.replace(window.location) +self.location.assign(window.location) +self.location.replace(window.location) +location['assign'](window.location) +location['replace'](window.location) +window.location['assign'](window.location) +window.location['replace'](window.location) +window['location'].assign(window.location) +window['location'].replace(window.location) +window['location']['assign'](window.location) +window['location']['replace'](window.location) +self.location['assign'](window.location) +self.location['replace'](window.location) +self['location'].assign(window.location) +self['location'].replace(window.location) +self['location']['assign'](window.location) +self['location']['replace'](window.location) +self.location = location +self.location = location.href +self.location = window.location +self.location = self.location +self.location = window.location.href +self.location = self.location.href +self.location = location['href'] +self.location = window['location'] +self.location = window['location'].href +self.location = window['location']['href'] +self.location = window.location['href'] +self.location = self['location'] +self.location = self['location'].href +self.location = self['location']['href'] +self.location = self.location['href'] +location.assign(self.location) +location.replace(self.location) +window.location.assign(self.location) +window.location.replace(self.location) +self.location.assign(self.location) +self.location.replace(self.location) +location['assign'](self.location) +location['replace'](self.location) +window.location['assign'](self.location) +window.location['replace'](self.location) +window['location'].assign(self.location) +window['location'].replace(self.location) +window['location']['assign'](self.location) +window['location']['replace'](self.location) +self.location['assign'](self.location) +self.location['replace'](self.location) +self['location'].assign(self.location) +self['location'].replace(self.location) +self['location']['assign'](self.location) +self['location']['replace'](self.location) +window.location.href = location +window.location.href = location.href +window.location.href = window.location +window.location.href = self.location +window.location.href = window.location.href +window.location.href = self.location.href +window.location.href = location['href'] +window.location.href = window['location'] +window.location.href = window['location'].href +window.location.href = window['location']['href'] +window.location.href = window.location['href'] +window.location.href = self['location'] +window.location.href = self['location'].href +window.location.href = self['location']['href'] +window.location.href = self.location['href'] +location.assign(window.location.href) +location.replace(window.location.href) +window.location.assign(window.location.href) +window.location.replace(window.location.href) +self.location.assign(window.location.href) +self.location.replace(window.location.href) +location['assign'](window.location.href) +location['replace'](window.location.href) +window.location['assign'](window.location.href) +window.location['replace'](window.location.href) +window['location'].assign(window.location.href) +window['location'].replace(window.location.href) +window['location']['assign'](window.location.href) +window['location']['replace'](window.location.href) +self.location['assign'](window.location.href) +self.location['replace'](window.location.href) +self['location'].assign(window.location.href) +self['location'].replace(window.location.href) +self['location']['assign'](window.location.href) +self['location']['replace'](window.location.href) +self.location.href = location +self.location.href = location.href +self.location.href = window.location +self.location.href = self.location +self.location.href = window.location.href +self.location.href = self.location.href +self.location.href = location['href'] +self.location.href = window['location'] +self.location.href = window['location'].href +self.location.href = window['location']['href'] +self.location.href = window.location['href'] +self.location.href = self['location'] +self.location.href = self['location'].href +self.location.href = self['location']['href'] +self.location.href = self.location['href'] +location.assign(self.location.href) +location.replace(self.location.href) +window.location.assign(self.location.href) +window.location.replace(self.location.href) +self.location.assign(self.location.href) +self.location.replace(self.location.href) +location['assign'](self.location.href) +location['replace'](self.location.href) +window.location['assign'](self.location.href) +window.location['replace'](self.location.href) +window['location'].assign(self.location.href) +window['location'].replace(self.location.href) +window['location']['assign'](self.location.href) +window['location']['replace'](self.location.href) +self.location['assign'](self.location.href) +self.location['replace'](self.location.href) +self['location'].assign(self.location.href) +self['location'].replace(self.location.href) +self['location']['assign'](self.location.href) +self['location']['replace'](self.location.href) +location['href'] = location +location['href'] = location.href +location['href'] = window.location +location['href'] = self.location +location['href'] = window.location.href +location['href'] = self.location.href +location['href'] = location['href'] +location['href'] = window['location'] +location['href'] = window['location'].href +location['href'] = window['location']['href'] +location['href'] = window.location['href'] +location['href'] = self['location'] +location['href'] = self['location'].href +location['href'] = self['location']['href'] +location['href'] = self.location['href'] +location.assign(location['href']) +location.replace(location['href']) +window.location.assign(location['href']) +window.location.replace(location['href']) +self.location.assign(location['href']) +self.location.replace(location['href']) +location['assign'](location['href']) +location['replace'](location['href']) +window.location['assign'](location['href']) +window.location['replace'](location['href']) +window['location'].assign(location['href']) +window['location'].replace(location['href']) +window['location']['assign'](location['href']) +window['location']['replace'](location['href']) +self.location['assign'](location['href']) +self.location['replace'](location['href']) +self['location'].assign(location['href']) +self['location'].replace(location['href']) +self['location']['assign'](location['href']) +self['location']['replace'](location['href']) +window['location'] = location +window['location'] = location.href +window['location'] = window.location +window['location'] = self.location +window['location'] = window.location.href +window['location'] = self.location.href +window['location'] = location['href'] +window['location'] = window['location'] +window['location'] = window['location'].href +window['location'] = window['location']['href'] +window['location'] = window.location['href'] +window['location'] = self['location'] +window['location'] = self['location'].href +window['location'] = self['location']['href'] +window['location'] = self.location['href'] +location.assign(window['location']) +location.replace(window['location']) +window.location.assign(window['location']) +window.location.replace(window['location']) +self.location.assign(window['location']) +self.location.replace(window['location']) +location['assign'](window['location']) +location['replace'](window['location']) +window.location['assign'](window['location']) +window.location['replace'](window['location']) +window['location'].assign(window['location']) +window['location'].replace(window['location']) +window['location']['assign'](window['location']) +window['location']['replace'](window['location']) +self.location['assign'](window['location']) +self.location['replace'](window['location']) +self['location'].assign(window['location']) +self['location'].replace(window['location']) +self['location']['assign'](window['location']) +self['location']['replace'](window['location']) +window['location'].href = location +window['location'].href = location.href +window['location'].href = window.location +window['location'].href = self.location +window['location'].href = window.location.href +window['location'].href = self.location.href +window['location'].href = location['href'] +window['location'].href = window['location'] +window['location'].href = window['location'].href +window['location'].href = window['location']['href'] +window['location'].href = window.location['href'] +window['location'].href = self['location'] +window['location'].href = self['location'].href +window['location'].href = self['location']['href'] +window['location'].href = self.location['href'] +location.assign(window['location'].href) +location.replace(window['location'].href) +window.location.assign(window['location'].href) +window.location.replace(window['location'].href) +self.location.assign(window['location'].href) +self.location.replace(window['location'].href) +location['assign'](window['location'].href) +location['replace'](window['location'].href) +window.location['assign'](window['location'].href) +window.location['replace'](window['location'].href) +window['location'].assign(window['location'].href) +window['location'].replace(window['location'].href) +window['location']['assign'](window['location'].href) +window['location']['replace'](window['location'].href) +self.location['assign'](window['location'].href) +self.location['replace'](window['location'].href) +self['location'].assign(window['location'].href) +self['location'].replace(window['location'].href) +self['location']['assign'](window['location'].href) +self['location']['replace'](window['location'].href) +window['location']['href'] = location +window['location']['href'] = location.href +window['location']['href'] = window.location +window['location']['href'] = self.location +window['location']['href'] = window.location.href +window['location']['href'] = self.location.href +window['location']['href'] = location['href'] +window['location']['href'] = window['location'] +window['location']['href'] = window['location'].href +window['location']['href'] = window['location']['href'] +window['location']['href'] = window.location['href'] +window['location']['href'] = self['location'] +window['location']['href'] = self['location'].href +window['location']['href'] = self['location']['href'] +window['location']['href'] = self.location['href'] +location.assign(window['location']['href']) +location.replace(window['location']['href']) +window.location.assign(window['location']['href']) +window.location.replace(window['location']['href']) +self.location.assign(window['location']['href']) +self.location.replace(window['location']['href']) +location['assign'](window['location']['href']) +location['replace'](window['location']['href']) +window.location['assign'](window['location']['href']) +window.location['replace'](window['location']['href']) +window['location'].assign(window['location']['href']) +window['location'].replace(window['location']['href']) +window['location']['assign'](window['location']['href']) +window['location']['replace'](window['location']['href']) +self.location['assign'](window['location']['href']) +self.location['replace'](window['location']['href']) +self['location'].assign(window['location']['href']) +self['location'].replace(window['location']['href']) +self['location']['assign'](window['location']['href']) +self['location']['replace'](window['location']['href']) +window.location['href'] = location +window.location['href'] = location.href +window.location['href'] = window.location +window.location['href'] = self.location +window.location['href'] = window.location.href +window.location['href'] = self.location.href +window.location['href'] = location['href'] +window.location['href'] = window['location'] +window.location['href'] = window['location'].href +window.location['href'] = window['location']['href'] +window.location['href'] = window.location['href'] +window.location['href'] = self['location'] +window.location['href'] = self['location'].href +window.location['href'] = self['location']['href'] +window.location['href'] = self.location['href'] +location.assign(window.location['href']) +location.replace(window.location['href']) +window.location.assign(window.location['href']) +window.location.replace(window.location['href']) +self.location.assign(window.location['href']) +self.location.replace(window.location['href']) +location['assign'](window.location['href']) +location['replace'](window.location['href']) +window.location['assign'](window.location['href']) +window.location['replace'](window.location['href']) +window['location'].assign(window.location['href']) +window['location'].replace(window.location['href']) +window['location']['assign'](window.location['href']) +window['location']['replace'](window.location['href']) +self.location['assign'](window.location['href']) +self.location['replace'](window.location['href']) +self['location'].assign(window.location['href']) +self['location'].replace(window.location['href']) +self['location']['assign'](window.location['href']) +self['location']['replace'](window.location['href']) +self['location'] = location +self['location'] = location.href +self['location'] = window.location +self['location'] = self.location +self['location'] = window.location.href +self['location'] = self.location.href +self['location'] = location['href'] +self['location'] = window['location'] +self['location'] = window['location'].href +self['location'] = window['location']['href'] +self['location'] = window.location['href'] +self['location'] = self['location'] +self['location'] = self['location'].href +self['location'] = self['location']['href'] +self['location'] = self.location['href'] +location.assign(self['location']) +location.replace(self['location']) +window.location.assign(self['location']) +window.location.replace(self['location']) +self.location.assign(self['location']) +self.location.replace(self['location']) +location['assign'](self['location']) +location['replace'](self['location']) +window.location['assign'](self['location']) +window.location['replace'](self['location']) +window['location'].assign(self['location']) +window['location'].replace(self['location']) +window['location']['assign'](self['location']) +window['location']['replace'](self['location']) +self.location['assign'](self['location']) +self.location['replace'](self['location']) +self['location'].assign(self['location']) +self['location'].replace(self['location']) +self['location']['assign'](self['location']) +self['location']['replace'](self['location']) +self['location'].href = location +self['location'].href = location.href +self['location'].href = window.location +self['location'].href = self.location +self['location'].href = window.location.href +self['location'].href = self.location.href +self['location'].href = location['href'] +self['location'].href = window['location'] +self['location'].href = window['location'].href +self['location'].href = window['location']['href'] +self['location'].href = window.location['href'] +self['location'].href = self['location'] +self['location'].href = self['location'].href +self['location'].href = self['location']['href'] +self['location'].href = self.location['href'] +location.assign(self['location'].href) +location.replace(self['location'].href) +window.location.assign(self['location'].href) +window.location.replace(self['location'].href) +self.location.assign(self['location'].href) +self.location.replace(self['location'].href) +location['assign'](self['location'].href) +location['replace'](self['location'].href) +window.location['assign'](self['location'].href) +window.location['replace'](self['location'].href) +window['location'].assign(self['location'].href) +window['location'].replace(self['location'].href) +window['location']['assign'](self['location'].href) +window['location']['replace'](self['location'].href) +self.location['assign'](self['location'].href) +self.location['replace'](self['location'].href) +self['location'].assign(self['location'].href) +self['location'].replace(self['location'].href) +self['location']['assign'](self['location'].href) +self['location']['replace'](self['location'].href) +self['location']['href'] = location +self['location']['href'] = location.href +self['location']['href'] = window.location +self['location']['href'] = self.location +self['location']['href'] = window.location.href +self['location']['href'] = self.location.href +self['location']['href'] = location['href'] +self['location']['href'] = window['location'] +self['location']['href'] = window['location'].href +self['location']['href'] = window['location']['href'] +self['location']['href'] = window.location['href'] +self['location']['href'] = self['location'] +self['location']['href'] = self['location'].href +self['location']['href'] = self['location']['href'] +self['location']['href'] = self.location['href'] +location.assign(self['location']['href']) +location.replace(self['location']['href']) +window.location.assign(self['location']['href']) +window.location.replace(self['location']['href']) +self.location.assign(self['location']['href']) +self.location.replace(self['location']['href']) +location['assign'](self['location']['href']) +location['replace'](self['location']['href']) +window.location['assign'](self['location']['href']) +window.location['replace'](self['location']['href']) +window['location'].assign(self['location']['href']) +window['location'].replace(self['location']['href']) +window['location']['assign'](self['location']['href']) +window['location']['replace'](self['location']['href']) +self.location['assign'](self['location']['href']) +self.location['replace'](self['location']['href']) +self['location'].assign(self['location']['href']) +self['location'].replace(self['location']['href']) +self['location']['assign'](self['location']['href']) +self['location']['replace'](self['location']['href']) +self.location['href'] = location +self.location['href'] = location.href +self.location['href'] = window.location +self.location['href'] = self.location +self.location['href'] = window.location.href +self.location['href'] = self.location.href +self.location['href'] = location['href'] +self.location['href'] = window['location'] +self.location['href'] = window['location'].href +self.location['href'] = window['location']['href'] +self.location['href'] = window.location['href'] +self.location['href'] = self['location'] +self.location['href'] = self['location'].href +self.location['href'] = self['location']['href'] +self.location['href'] = self.location['href'] +location.assign(self.location['href']) +location.replace(self.location['href']) +window.location.assign(self.location['href']) +window.location.replace(self.location['href']) +self.location.assign(self.location['href']) +self.location.replace(self.location['href']) +location['assign'](self.location['href']) +location['replace'](self.location['href']) +window.location['assign'](self.location['href']) +window.location['replace'](self.location['href']) +window['location'].assign(self.location['href']) +window['location'].replace(self.location['href']) +window['location']['assign'](self.location['href']) +window['location']['replace'](self.location['href']) +self.location['assign'](self.location['href']) +self.location['replace'](self.location['href']) +self['location'].assign(self.location['href']) +self['location'].replace(self.location['href']) +self['location']['assign'](self.location['href']) +self['location']['replace'](self.location['href']) +location.reload() +location['reload']() +window.location.reload() +window['location'].reload() +window.location['reload']() +window['location']['reload']() +self.location.reload() +self['location'].reload() +self.location['reload']() +self['location']['reload']() +``` \ No newline at end of file diff --git "a/Cute-Article/article/74-\343\200\212\345\244\247\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\232\204\346\227\266\345\256\236\350\267\265\345\222\214\346\200\235\350\200\203\343\200\213\347\213\274\345\217\224\345\210\206\344\272\253.md" "b/Cute-Article/article/74-\343\200\212\345\244\247\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\232\204\346\227\266\345\256\236\350\267\265\345\222\214\346\200\235\350\200\203\343\200\213\347\213\274\345\217\224\345\210\206\344\272\253.md" new file mode 100644 index 00000000..952deec2 --- /dev/null +++ "b/Cute-Article/article/74-\343\200\212\345\244\247\345\211\215\347\253\257\345\267\245\347\250\213\345\214\226\347\232\204\346\227\266\345\256\236\350\267\265\345\222\214\346\200\235\350\200\203\343\200\213\347\213\274\345\217\224\345\210\206\344\272\253.md" @@ -0,0 +1,350 @@ +> 本文来自 狼叔分享的《大前端工程化的时实践和思考》,如有侵权,联系删除。 + +### 一、前端发展历程 + +前端发展太快了,在2004年之前,大概只要会网页三剑客(一套强大的网页编辑工具,最初是 Macromedia 公司开发的,由 Dreamweaver 、 Fireworks 、 Flash 三个软件组成)就很牛了,那时候前端还比较“纯洁”。 + +在进入以 Ajax 为代表的**异步刷新改进用户体验的 Web 2.0 时代**后,开始涌现出大量的库,比如 `Prototype`、 `jQuery`、 `MooTools`、 `YUI`、 `Ext JS`、 `kissy `等,它们都还只是**对浏览器兼容性和工具类函数的封装**,可是从 `Backbone`、 `Angular 1.x` 相继出现后,前端就开始热闹起来了,出现 `MVC` 、 `MVVM` 、 `IOC` (控制反转,Java著名框架 `Spring` 里的概念)、前端路由(类似于 `Express` 、 `Koa` 等框架的路由)、`Virtual DOM`(虚拟DOM,通过 `DOM Diff` 算法,减少对DOM操作)、JSON API(接口规范)、JavaScript Runtime(Coffee、Babel、TypeScrpt)等等,各种框架格式如雨后春笋一样冒出来,以前可能半年甚至更长时间才出一个框架,现在可能几周就有新的框架诞生,前端进入了空前的爆发阶段。 + +![langshu1](http://images.pingan8787.com/20190710langshu01.png) + +目前整个大前端还属于发展期,没有形成固定模式,所以从趋势上看是上升期,复杂、混乱、多样性的结果也导致现在的前端比较吃香,被大家戏称为“钱端”。 + +很多同学会问,学习前端技术是采用渐进式方式,还是上来就直接学 `React`/`Vue.js` 等框架呢? + +实际上,我认为这两种方式都存在,如果有经济压力就向“钱“看,可以直接学习 `React`/`Vue.js` 等框架,但学会了之后,一定要把其他3个阶段补充学习完;如果没有经济压力,又有时间和耐心的话,循序渐进的学习是最好的方式。编程没有捷径,无论是哪种,都需要脚踏实地多多练习。 + +当你代码写多了,就会发现: + +当大家HTML写烦了,开始引入模板引擎。 +当大家CSS写烦了,比如不支持嵌套,开始引入CSS预处理器SASS、Less、Stylus、PostCSS等。 +当大家JavaScript写烦了,开始引入更友好的语言,熟悉Ruby的使用CoffeeScript、熟悉Java/C#的使用TypeScript。于是,前端开发变得复杂,并进入现代Web开发时代。 + + +![langshu1](http://images.pingan8787.com/20190710langshu02.png) + + +对于 `Node.js` 来说,所有前端框架都是视图层(View)的展现技术而已,可以非常方便地和各种框架集成,并按照业务需求予以更好的实现。另外,所有的前端框架都采用 `Node.js` 和 `NPM` 作为辅助开发工具,使用了大量 `Node.js` 模块,但前端框架使用的模块大同小异,如果熟练掌握了 `Node.js` 和 `NPM` ,对于学习前端技术来说,你要学的只是纯前端的部分而已,复用价值非常高。 + +和 `Node.js` 相关的前端开发模块实在是太多了,这里简单列举一些: + +![langshu1](http://images.pingan8787.com/20190710langshu03.png) + + +由这些模块,也就引出了我们今天要聊得话题——大前端工程化实践与思考。 + +先来说说构建工具。预处理器是前端高级玩法。 + +![langshu1](http://images.pingan8787.com/20190710langshu04.png) + +我经常开玩笑说以前 `HTML` / `JavaScript` / `CSS` 的时代太纯洁了,现在随便写哪样都要编译/转译。 + +好处是可以借助高级特性,提高开发效率;缺点也是极其明显的,就是人脑要有转换思维。 + +这其实是蛮痛苦的,本来 `HTML` / `JavaScript` / `CSS` 就不够熟练,再转一次,对于新手来说需要一个适应过程。所以这件事还是要辩证地看,福祸相依。 + +说起构建工具,大概都会想到 `Make` 、 `Ant` 、 `Rake` 、 `Gradle` 等,其实 `Node.js` 世界里有更多实现。 + + +![langshu1](http://images.pingan8787.com/20190710langshu05.png) + +构建工具的源码都不是特别复杂,所以 `Node.js` 世界里有非常多的实现,还有人写过`Node.js` 版本的 `Make` 呢,玩得很嗨! + +选用构建工具最主要的目的是为了自动化。对于需要反复重复的任务,例如**压缩(minification)、编译、单元测试、linting**等,自动化工具可以减轻你的劳动,简化你的工作。尤其是工程越复杂,自动化的价值就越大。 + +这里的编译包含**模板**、**CSS预处理语言**、**JavaScript友好语言**等编译,在源码编写时用的是高级玩法。 + + +除了编码编译外,还有测试、代码风格检查、上线前优化(合并、压缩、混淆),可以说,构建系统在整个软件工程里无处不在。 + +`Grunt` 是前端领域第一个流行的 `DSL` (领域定义语言)风格的构建工具,它的出现对于前端工程化起到了非常好的引导作用。它通过 `Gruntfile` 来描述 `task` ,同时通过插件机制来扩展各种工程能力。 + +当你在 `Gruntfile` 文件正确配置好了任务,任务运行器就会自动帮你或你的小组完成大部分无聊的工作。 + +`Grunt` 除了配置复杂外,还有一个问题就性能,可能很多同学遇不到性能问题。`Grunt` 是读写文件的,所以在多文件、大文件处理时是有性能瓶颈的。 + +`Grunt` 的设计还是挺精巧的,但配置起来让人抓狂,自然会有很多不爽的人,于是 `Gulp` 就慢慢崛起了。 + +简单来讲, `Gulp `是一个 `Node.js `写的构建工具,基于Stream的流式构建工具,它包含大量插件。 `Orchestrator `是 `Gulp `底层依赖的 `task `相关的核心库,它定义了 `task `执行方式和依赖,而且支持最大可能的并发, `Gulp `的高效即来源于此。本身Stream对大文件读写就非常棒,再加上上面说的种种特性,使得 `Gulp `流行成为必然。 + + `Gulp `的应用场景非常广,前端项目或Node项目都可以使用,哪怕是 `webpack `也可以和 `Gulp `搭配使用,通过 `gulp-webpack `模块即可。如果想深入学习 `Gulp `,可以看一下 `stuq-gulp `(https://github.com/i5ting/stuq-gulp),文中以 `WeUI `里 `Gulp `用法为例,由浅入深,从用法到原理进行了阐述。 + + +### 二、前端工程化的发展 + +解耦是软件开发领域永恒的主题,而模块化是目前最好的解耦方式,所以从无到有、从有到成熟的演化,必然要经历很长的路。 + +如今的发展,源于1993年HTML创建、1995年诞生 `JavaScript `、1996年发布 `CSS1 `,之后就进入了原始而野蛮的开发阶段。 + +从互联网诞生到2000年泡沫破灭,Web技术才算真正崛起。 + +那时候特别纯洁,会写的人就很牛了,之后 `Flash `也曾超级火爆。在之后就到了Web 2.0时代,开始出现 `Ajax `,开始有了各种兼容浏览器的库,然后开始模块化,在2009年诞生了 `Node.js `,彻底改变了 `JavaScript `以及前端开发领域的开发方式,从浏览器端的 `Backbone `支持 `MVC模式 `,到 `AngluarJS `支持的 `MVVM `,到现在的 `React `/ `Vue `和 `webpack `等。 + +这里我尝试从更宏观的视角来进行归类,大致分5个阶段,分别是原始阶段、包管理器、模块规范、模块加载器、模块打包器。下面我来一一说明。 + +#### 1.原始阶段 +脚本加载还都比较原始,方式如下: +* 使用多个script标签加载 +* 手动管理顺序 +* 手动管理加载哪些 + +在Web开发里经过了很多尝试,也做过很多龌蹉的事儿,比如: + +* 动态创建Script标签 +* XHR Eval、XHR Injection、$.getScript() +* Script in Iframe +* Script DOM Element +* Script Defer + +如果说加载比较恶心,那么脚本顺序更恶心,而且JavaScript有个“特性”,一处报错,所有后面的都会崩溃,所以开发会很苦的维护脚本加载的顺序。 + +#### 2.包管理器 + +如果代码存在一个问题,更新jQuery UI版本怎么做呢?jQuery UI依赖jQuery,先下载jQuery UI代码,然后找到依赖的jQuery版本,再替换已有文件,之后测试,如果没问题就直接替换了。 + +很明显,直接进行文件操作是非常低效率的做法,对于版本、依赖都没法做更好的处理。于是就出现了Bower、NPM等包管理器,所有模块升级,依赖都有包管理负责,可以说很大程度上省去了前端的重复性工作。 + +#### 3.模块规范 +**模块化加载的本质是按需加载**。 + +比较常见的规范有3个: + +1. AMD、CommonJS和ES6 Modules。 +2. 使用标准的模块系统来处理依赖和导出,每个文件是一个模块。 +3. 使用模块加载器或打包器进行处理。 + +#### 4.模块加载器 +模块加载器需要实现两个基本功能: + +* 实现模块定义规范,这是模块系统的基础 +* 模块系统的启动与运行 + +常见的比如 `RequireJS `、 `Sea.js `和 `SystemJS `。以 `AMD `的模块加载器 `RequireJS `为例,通常使用 `RequireJS `的话,我们只需要导入 `RequireJS `即可,不需要显式导入其他的JS库,因为这个工作会交给 `RequireJS `来做。 + +#### 5.模块打包器 + +模块加载器提供运行环境,能够让遵守模块规范的代码在上面跑,所以模块化的好处是很明显的。但对于真实项目来说,你还需要构建、打包等操作。 + +对于开发来说,只需要关注业务模块,不需要了解模块加载器和构建过程,很明显这是非常理想的,也因此产生了 `webpack `这样的模块打包器。 + + `Gulp `作为通用构建工具,它已经非常完美了。但技术变革太快了,应用各种预处理器、前端组件化,导致前端无比复杂,而 `webpack `的出现刚刚好,完美解决了前端工程化的问题。 + + `webpack `是 `Node `编写的著名模块,是打包器(`bundler`),不只是支持 `CommonJS `模块,而且还支持更潮的ES6模块,是目前使用极其广泛的打包器,像前端组件化的框架( `React `、 `Vue `)大多都是使用 `webpack `的。 + +它提供了两个极其好的机制: `loaders `和 `plugins `。 + + +* `loaders`:`webpack`认为每个文件都是资源模块,针对打包构建过程中用来处理源文件的(JSX、SCSS、Less..)统称为 `loader `。 + +* `plugins`:插件可以完成更多 `loader `不能完成的功能。插件并不直接操作单个文件,它直接对整个构建过程起作用,大多数内容功能都是基于这个插件系统运行的;当然还可以开发和使用开源的 `webpack `插件,来满足各式各样的需求。比如知名插件 `autoprefixer `、 `html-webpack-plugin `、 `webpack-dev-middleware `、 `webpack-hot-middleware `。 + +**下面来看一下webpack打包过程**: + +1. 从配置文件里找到`entry point` +1. 解析模块系统 +1. 解决依赖 +1. 解决依赖管理(读取、解析、解决) +1. 合并所有使用的模块 +1. 合并模块系统的运行时环境 +1. 产生打包后的文件 + +**浏览器加载过程:** + +1. 通过 ` + + +``` + +通过这个简单的示例可以看到「在 Shadow DOM 中定义的样式,并不会影响到主文档中的元素」,如下图 + +![2019110406](http://images.pingan8787.com/blog/2019110406.png) + +`Element.attachShadow` 的参数 `shadowRootInit` 的 `mode` 选项用于设定「封装模式」。它有两个可选的值 : + +* **"open"** :可 `Host` 元素上通过 `host.shadowRoot` 获取 `shadowRoot` 引用,这样任何代码都可以通过 `shadowRoot` 来访问的子 DOM 树。 +* **"closed"**:在 `Host` 元素上通过 `host.shadowRoot` 获取的是 `null`,我们只能通过 `Element.attachShadow` 的返回值拿到 `shadowRoot` 的引用(通常可能隐藏在类中)。例如,浏览器内建的 input、video 等就是关闭的,我们没有办法访问它们。 + +## 5. 哪些元素可以附加 Shadow DOM + +并非所有 HTML 元素都可以开启 Shadow DOM 的,只有一组有限的元素可以附加 Shadow DOM。有时尝试将 Shadow DOM 树附加到某些元素将会导致 `DOMException` 错误,例如: + +```js +document.createElement('img').attachShadow({mode: 'open'}); +// => DOMException +``` + +用 `` 这样的非容器素作为 Shadow Host 是不合理的,因此这段代码将抛出 DOMException 错误。此外因为安全原因一些元素也不能附加 Shadow DOM(比如 A 元素),会出现错误的另一个原因是浏览器已经用该元素附加了 Shadow DOM,比如 Input 等。 + +下表列出了所有支持的元素: + +![2019110407](http://images.pingan8787.com/blog/2019110407.png) + +## 6. 在 React 中如何应用 Shadow DOM + +在基于 React 的项目中应该如何使用 Shadow DOM 呢?比如你正在基于 React 编写一个面向不同产品或业务,可嵌入集成使用的公共组件,比如你正在基于 React 做一个「微前端架构」应用的设计或开发。 + +我们在编写 React 应用时一般不希望到处是 DOM 操作,因为这很不 React (形容词)。那是否能封装成一下用更 React (形容词) 的组件风格去使用 Shadow DOM 呢? + + +### 6.1. 尝试写一个 React 组件: + +```js +import React from "react"; +import ReactDOM from "react-dom"; + +export class ShadowView extends React.Component { + attachShadow = (host: Element) => { + host.attachShadow({ mode: "open" }); + } + render() { + const { children } = this.props; + return
    + {children} +
    ; + } +} + +export function App() { + return + 这儿是隔离的 + +} + +ReactDOM.render(, document.getElementById("root")); +``` + +跑起来看看效果,一定会发现「咦?什么也没有显示」: + + +![2019110408](http://images.pingan8787.com/blog/2019110408.png) + +在这里需要稍注意一下,在一个元素上附加了 Shadow DOM 后,元素原本的「子元素」将不会再显示,并且这些子元素也不在 Shadow DOM 中,只有 `host.shadowRoot` 的子元素才是「子 DOM 树」中一部分。也就是说这个「子 DOM 树」的「根节点」是 `host.shadowRoot` 而非 `host`。 `host.shadowRoot` 是 ShadowRoot 的实例,而 `ShadowRoot` 则继承于 `DocumentFragment`,可通过原生 DOM API 操作其子元素。 + +我们需通过` Element.attachShadow` 附加到元素,然后就能拿到附加后的 ShadowRoot 实例。 针对 ShadowRoot 这样一个原生 DOM Node 的的引用,除了利用 `ReactDOM.render` 或 `ReactDOM.createPortal` ,我们并不能轻易的将` React.Element` 渲染到其中,除非直接接操作 DOM。 + + +### 6.2. 基于直接操作 DOM 改造一版: + +在 React 中通过 `ref` 拿到真实的 DOM 引用后,是否能通过原生的 DOM API,将 `host` 的 `children` 移动到 `host.shadowRoot` 中? + + +```js +import React from "react"; +import ReactDOM from "react-dom"; + +// 基于直接操作 DOM 的方式改造的一版 +export class ShadowView extends React.Component { + attachShadow = (host: Element) => { + const shadowRoot = host.attachShadow({ mode: "open" }); + //将所有 children 移到 shadowRoot 中 + [].slice.call(host.children).forEach(child => { + shadowRoot.appendChild(child); + }); + } + render() { + const { children } = this.props; + return
    + {children} +
    ; + } +} + +// 验证一下 +export class App extends React.Component { + state = { message: '...' }; + onBtnClick = () => { + this.setState({ message: 'haha' }); + } + render() { + const { message } = this.state; + return
    + +
    {message}
    + +
    + +
    + } +} + +ReactDOM.render(, document.getElementById("root")); +``` + +在浏览器中看看效果,可以看到是可以正常显示的。但与此同时会发现一个问题「隔离在 ShadowRoot 中的元素上的事件无法被触发了」,这是什么原因呢? + +是由于 React 的「合成事件机制」的导致的,我们知道在 React 中「事件」并不会直接绑定到具体的 DOM 元素上,而是通过在 `document` 上绑定的 `ReactEventListener` 来管理, 当时元素被单击或触发其他事件时,事件被 `dispatch` 到 `document` 时将由 React 进行处理并触发相应合成事件的执行。 + +那为什么合成事件在 Shadow DOM 中不能被正常触发?是因为当在 Shadow DOM 外部捕获时浏览器会对事件进行「重定向」,也就是说在 Shadow DOM 中发生的事件在外部捕获时将会使用 host 元素作为事件源。这将让 React 在处理合成事件时,不认为 ShadowDOM 中元素基于 JSX 语法绑定的事件被触发了。 + + +![2019110409](http://images.pingan8787.com/blog/2019110409.png) + +### 6.3. 尝试利用 ReactDOM.render 改造一下: + +`ReactDOM.render` 的第二个参数,可传入一个 DOM 元素。那是不是能通过 `ReactDOM.render` 将 `React Eements` 渲染到 Shodaw DOM 中呢?看一下如下尝试: + +```js +import React from "react"; +import ReactDOM from "react-dom"; + +// 换用 ReactDOM.render 实现 +export class ShadowView extends React.Component { + attachShadow = (host: Element) => { + const { children } = this.props; + const shadowRoot = host.attachShadow({ mode: "open" }); + ReactDOM.render(children, shadowRoot); + } + render() { + return
    ; + } +} + +// 试试效果如何 +export class App extends React.Component { + state = { message: '...' }; + onBtnClick = () => { + this.setState({ message: 'haha' }); + alert('haha'); + } + render() { + const { message } = this.state; + return +
    {message}
    + +
    + } +} + +ReactDOM.render(, document.getElementById("root")); +``` + + +可以看到通过 `ReactDOM.render` 进行 `children` 的渲染,是能够正常渲染到 `Shadow Root` 中,并且在 Shadow DOM 中合成事件也是能正常触发执行的。 + +为什么此时「隔离在 Shadow DOM 中的元素事件」能够被触发了呢? 因为在 React 在发现渲染的目标在 ShadowRoot 中时,将会将事件绑定在通过 `Element.getRootNode()` 获取的 `DocumentFragment` 的 `RootNode` 上。 + +![2019110410](http://images.pingan8787.com/blog/2019110410.png) + +看似一切顺利,但却会发现父组件的 state 更新时,而 ShadowView 组件并没有更新。如上边的示例,其中的 message 显示的还是旧的,而原因就在我们使用 `ReactDOM.render` 时,Shadow DOM 的元素和父组件不在一个 React 渲染上下文中了。 + + +### 6.4. 利用 ReactDOM.createPortal 实现一版: + +我们知道 `createPortal` 的出现为「弹窗、提示框」等脱离文档流的组件开发提供了便利,替换了之前不稳定的 API `unstable_renderSubtreeIntoContainer`。 + +`ReactDOM.createPortal` 有一个特性是「通过 `createPortal` 渲染的 DOM,事件可以从 Portal 的入口端冒泡上来」,这一特性很关键,没有父子关系的 DOM ,合成事件能冒泡过来,那通过 `createPortal` 渲染到 Shadow DOM 中的元素的事件也能正常触发吧?并且能让所有元素的渲染在一个上下文中。那就基于 `createPortal` 实现一下: + +```js +import React from "react"; +import ReactDOM from "react-dom"; + +// 利用 ReactDOM.createPortal 的实现 +export function ShadowContent({ root, children }) { + return ReactDOM.createPortal(children, root); +} + +export class ShadowView extends React.Component { + state = { root: null }; + setRoot = eleemnt => { + const root = eleemnt.attachShadow({ mode: "open" }); + this.setState({ root }); + }; + render() { + const { children } = this.props; + const { root } = this.state; + return
    + {root && + {children} + } +
    ; + } +} + +// 试试如何 +export class App extends React.Component { + state = { message: '...' }; + onBtnClick = () => { + this.setState({ message: 'haha' }); + } + render() { + const { message } = this.state; + return +
    {message}
    + +
    + } +} + +ReactDOM.render(, document.getElementById("root")); +``` + +![2019110411](http://images.pingan8787.com/blog/2019110411.png) + +Wow! 一切正常,有一个小问题是 `createPortal` 不支持 React 16 以下的版本,但大多数情况下这并不是个什么大问题。 + + +## 7. 面向 React 的 ShadowView 组件 + +上边提到了几种在 React 中实现 Shadwo DOM 组件的方法,而 ShadowView 是一个写好的可开箱即用的面向 React 的 Shadow DOM 容器组件,利用 ShadowView 可以像普通组件一样方便的在 React 应用中创建启用 Shadow DOM 的容器元素。 + +ShadowView 目前完整兼容支持 React 15/16,组件的「事件处理、组件渲染更新」等行为在两个版中都是一致的。 + +GitHub: https://github.com/Houfeng/shadow-view + + +### 7.1. 安装组件 + +```sh +npm i shadow-view --save +``` + +### 7.2. 使用组件 + +```js +import * as React from "react"; +import * as ReactDOM from "react-dom"; +import { ShadowView } from "shadow-view"; + +function App() { + return ( + + +
    这是一个测试
    +
    + ); +} + +ReactDOM.render(, document.getElementById('root')); +``` + +### 7.3. 组件属性 + +![2019110412](http://images.pingan8787.com/blog/2019110412.png) + + +![2019_07_12guild_page](http://images.pingan8787.com/2019_07_12guild_page.png) + +|Author|王平安| +|---|---| +|E-mail|pingan8787@qq.com| +|博 客|www.pingan8787.com| +|微 信|pingan8787| +|每日文章推荐|https://github.com/pingan8787/Leo_Reading/issues| +|JS小册|js.pingan8787.com| diff --git "a/8-vue-cli2\347\232\204webpack\351\205\215\347\275\256\345\210\206\346\236\220.md" "b/Cute-Article/article/8-vue-cli2\347\232\204webpack\351\205\215\347\275\256\345\210\206\346\236\220.md" similarity index 100% rename from "8-vue-cli2\347\232\204webpack\351\205\215\347\275\256\345\210\206\346\236\220.md" rename to "Cute-Article/article/8-vue-cli2\347\232\204webpack\351\205\215\347\275\256\345\210\206\346\236\220.md" diff --git "a/Cute-Article/article/80.\347\262\276\350\257\273\343\200\212react-easy-state \346\272\220\347\240\201\343\200\213.md" "b/Cute-Article/article/80.\347\262\276\350\257\273\343\200\212react-easy-state \346\272\220\347\240\201\343\200\213.md" new file mode 100644 index 00000000..1b3b6e13 --- /dev/null +++ "b/Cute-Article/article/80.\347\262\276\350\257\273\343\200\212react-easy-state \346\272\220\347\240\201\343\200\213.md" @@ -0,0 +1,176 @@ +> 参考文章:[《精读《react-easy-state 源码》》](https://juejin.im/post/5caa9c71e51d452b4043e2d8) + +## 1. 引言 + +`react-easy-state` 是个比较有趣的库,利用 `Proxy` 创建了一个非常易用的全局数据流管理方式。 + +```js +import React from "react"; +import { store, view } from "react-easy-state"; + +const counter = store({ num: 0 }); +const increment = () => counter.num++; + +export default view(() => ); +``` + +上手非常轻松,通过 `store` 创建一个数据对象,这个对象被任何 React 组件使用时,都会自动建立双向绑定,**任何对这个对象的修改,都会让使用了这个对象的组件重渲染**。 + +当然,为了实现这一点,需要对所有组件包裹一层 `view`。 + +## 2. 精读 + +这个库利用了 [nx-js/observer-util](https://github.com/nx-js/observer-util) 做 `Reaction` 基础 API,其他核心功能分别是 `store` `view` `batch`,所以我们就从这四个点进行解读。 + +### Reaction + +这个单词名叫 “反应”,是实现双向绑定库的最基本功能单元。 + +拥有最基本的两个单词和一个概念:`observable` `observe` 与自动触发执行的特性。 + +```js +import { observable, observe } from "@nx-js/observer-util"; + +const counter = observable({ num: 0 }); +const countLogger = observe(() => console.log(counter.num)); + +// 会自动触发 countLogger 函数内回调函数的执行。 +counter.num++; +``` + +在第 35 期精读[ 精读《dob - 框架实现》](https://github.com/dt-fe/weekly/blob/master/35.%E7%B2%BE%E8%AF%BB%E3%80%8Adob%20-%20%E6%A1%86%E6%9E%B6%E5%AE%9E%E7%8E%B0%E3%80%8B.md#%E6%8A%BD%E4%B8%9D%E5%89%A5%E8%8C%A7%E5%AE%9E%E7%8E%B0%E4%BE%9D%E8%B5%96%E8%BF%BD%E8%B8%AA) “抽丝剥茧,实现依赖追踪” 一节中有详细介绍实现原理,这里就不赘述了。 + +有了一个具有反应特性的函数,与一个可以 “触发反应” 的对象,那么实现双向绑定更新 View 就不远了。 + +### store + +`react-easy-state` 的 `store` 就是 `observable(obj)` 包装一下,唯一不同是,由于支持本地数据: + +```js +import React from 'react' +import { view, store } from 'react-easy-state' + +export default view(() => { + const counter = store({ num: 0 }) + const increment = () => counter.num++ + return {counter.num} +}) +``` + +所以当监测到在 React 组件内部创建 `store` 且是 `Hooks` 环境时,会返回: + +```js +return useMemo(() => observable(obj), []); +``` + +这是因为 React Hooks 场景下的 Function Component 每次渲染都会重新创建 `Store`,会导致死循环。因此利用 `useMemo` 并将依赖置为 `[] `使代码在所有渲染周期内,只在初始化执行一次。 + +更多 Hooks 深入解读,可以阅读 [精读《useEffect 完全指南》](https://github.com/dt-fe/weekly/blob/master/96.%E7%B2%BE%E8%AF%BB%E3%80%8AuseEffect%20%E5%AE%8C%E5%85%A8%E6%8C%87%E5%8D%97%E3%80%8B.md)。 + +### view + +根据 Function Component 与 Class Component 的不同,分别进行两种处理,本文主要介绍对 Function Component 的处理方式,因为笔者推荐使用 Function Component 风格。 + +首先最外层会套上 `memo`,这类似 `PureComponent` 的效果: + +```js +return memo(/**/); +``` + +然后构造一个 `forceUpdate` 用来强制渲染组件: + +```js +const [, forceUpdate] = useState(); +``` + +之后,只要利用 `observe` 包裹组件即可,需要注意两点: + +1. **使用刚才创建的 `forceUpdate` 在 `store` 修改时调用。** +2. `observe` 初始化不要执行,因为初始化组件自己会渲染一次,再渲染一次就会造成浪费。 + +所以作者通过 `scheduler` `lazy` 两个参数完成了这两件事: + +```js +const render = useMemo( + () => + observe(Comp, { + scheduler: () => setState({}), + lazy: true + }), + [] +); + +return render; +``` + +最后别忘了在组件销毁时取消监听: + +```js +useEffect(() => { + return () => unobserve(render); +}, []); +``` + +### batch + +这也是双向绑定数据流必须解决的经典问题,批量更新合并。 + +由于修改对象就触发渲染,**这个过程太自动化了,以至于我们都没有机会告诉工具,连续的几次修改能否合并起来只触发一次渲染**。 尤其是 `For` 循环修改变量时,如果不能合并更新,在某些场景下代码几乎是不可用的。 + +所以 `batch` 就是为解决这个问题诞生的,让我们有机会控制合并更新的时机: + +```js +import React from "react"; +import { view, store, batch } from "react-easy-state"; + +const user = store({ name: "Bob", age: 30 }); + +function mutateUser() { + // this makes sure the state changes will cause maximum one re-render, + // no matter where this function is getting invoked from + batch(() => { + user.name = "Ann"; + user.age = 32; + }); +} + +export default view(() => ( +
    + name: {user.name}, age: {user.age} +
    +)); +``` + +`react-easy-state` 通过 `scheduler` 模块完成 `batch` 功能,核心代码只有五行: + +```js +export function batch(fn, ctx, args) { + let result; + unstable_batchedUpdates(() => (result = fn.apply(ctx, args))); + return result; +} +``` + +利用 `unstable_batchedUpdates`,可以保证在其内执行的函数都不会触发更新,也就是之前创建的 `forceUpdate` 虽然被调用,但是失效了,等回调执行完毕时再一起批量更新。 + +同时代码里还对 `setTimeout` `setInterval` `addEventListener` `WebSocket` 等公共方法进行了 `batch` 包装,让这些回调函数中自带 `batch` 效果。 + +## 3. 总结 + +好了,`react-easy-state` 神奇的效果解释完了,希望大家在使用第三方库的时候都能理解背后的原理。 + +PS:最后,笔者目前不推荐在 Function Component 模式下使用任何三方数据流库,因为官方功能已经足够好用了! + +讨论地址是:[精读《react-easy-state》](https://github.com/dt-fe/weekly/issues/144) + +## 个人介绍 + +![2019_07_12guild_page](http://images.pingan8787.com/2019_07_12guild_page.png) + +|Author|王平安| +|---|---| +|E-mail|pingan8787@qq.com| +|博 客|www.pingan8787.com| +|微 信|pingan8787| +|每日文章推荐|https://github.com/pingan8787/Leo_Reading/issues| +|JS小册|js.pingan8787.com| diff --git "a/Cute-Article/article/81.JavaScript\344\270\255\347\232\204\344\275\215\350\277\220\347\256\227\345\222\214\346\235\203\351\231\220\350\256\276\350\256\241.md" "b/Cute-Article/article/81.JavaScript\344\270\255\347\232\204\344\275\215\350\277\220\347\256\227\345\222\214\346\235\203\351\231\220\350\256\276\350\256\241.md" new file mode 100644 index 00000000..03f1a752 --- /dev/null +++ "b/Cute-Article/article/81.JavaScript\344\270\255\347\232\204\344\275\215\350\277\220\347\256\227\345\222\214\346\235\203\351\231\220\350\256\276\350\256\241.md" @@ -0,0 +1,350 @@ +# JavaScript 中的位运算和权限设计 + +原文:[JavaScript 中的位运算和权限设计](https://juejin.im/post/5dc36f39e51d4529ed292910) + +## 1. 内容概要 +本文主要讨论以下两个问题: + +- JavaScript 的位运算:先简单回顾下位运算,平时用的少,相信不少人和我一样忘的差不多了 +- 权限设计:根据位运算的特点,设计一个权限系统(添加、删除、判断等) +## 2. JavaScript 位运算 +### 2.1. Number +在讲位运算之前,首先简单看下 JavaScript 中的 Number,下文需要用到。 +在 JavaScript 里,数字均为[基于 IEEE 754 标准的双精度 64 位的浮点数](https://zh.wikipedia.org/wiki/%E9%9B%99%E7%B2%BE%E5%BA%A6%E6%B5%AE%E9%BB%9E%E6%95%B8),引用维基百科的图片,它的结构长这样: +![](https://cdn.nlark.com/yuque/0/2019/webp/186051/1573570435363-8654c3d6-1978-4722-aa02-07ddad8000ba.webp#align=left&display=inline&height=96&originHeight=96&originWidth=594&search=&size=0&status=done&width=594) + +- sign bit(符号): 用来表示正负号 +- exponent(指数): 用来表示次方数 +- mantissa(尾数): 用来表示精确度 + +也就是说一个数字的范围只能在 -(2^53 -1) 至 2^53 -1 之间。 +> 既然讲到这里,就多说一句:0.1 + 0.2 算不准的原因也在于此。浮点数用二进制表达时是无穷的,且最多 53 位,必须截断,进而产生误差。最简单的解决办法就是放大一定倍数变成整数,计算完成后再缩小。不过更稳妥的办法是使用下文将会提到的 [math.js](https://mathjs.org/docs/datatypes/bignumbers.html#roundoff-errors) 等工具库。 + +此外还有四种数字进制: +``` +// 十进制 +123456789 +0 +// 二进制:前缀 0b,0B +0b10000000000000000000000000000000 // 2147483648 +0b01111111100000000000000000000000 // 2139095040 +0B00000000011111111111111111111111 // 8388607 +// 八进制:前缀 0o,0O(以前支持前缀 0) +0o755 // 493 +0o644 // 420 +// 十六进制:前缀 0x,0X +0xFFFFFFFFFFFFFFFFF // 295147905179352830000 +0x123456789ABCDEF // 81985529216486900 +0XA // 10 +复制代码 +``` +好了,Number 就说这么多,接下来看 JavaScript 中的位运算。 +### 2.2. 位运算 +按位操作符将其操作数当作 32 位的比特序列(由 0 和 1 组成)操作,返回值依然是标准的 JavaScript 数值。JavaScript 中的按位操作符有: + +| 运算符 | 用法 | 描述 | +| :--- | :--- | :--- | +| 按位与(AND) | `a & b` | 对于每一个比特位,只有两个操作数相应的比特位都是 1 时,结果才为 1,否则为 0。 | +| 按位或(OR) | `a | b` | 对于每一个比特位,当两个操作数相应的比特位至少有一个 1 时,结果为 1,否则为 0。 | +| 按位异或(XOR) | `a ^ b` | 对于每一个比特位,当两个操作数相应的比特位有且只有一个 1 时,结果为 1,否则为 0。 | +| 按位非(NOT) | `~a` | 反转操作数的比特位,即 0 变成 1,1 变成 0。 | +| 左移(Left shift) | `a << b` | 将 a 的二进制形式向左移 b (< 32) 比特位,右边用 0 填充。 | +| 有符号右移 | `a >> b` | 将 a 的二进制表示向右移 b (< 32) 位,丢弃被移出的位。 | +| 无符号右移 | `a >>> b` | 将 a 的二进制表示向右移 b (< 32) 位,丢弃被移出的位,并使用 0 在左侧填充。 | + +下面举几个例子,主要看下 `AND` 和 `OR`: +``` +# 例子1 + A = 10001001 + B = 10010000 +A | B = 10011001 +# 例子2 + A = 10001001 + C = 10001000 +A | C = 10001001 +复制代码 +``` +``` +# 例子1 + A = 10001001 + B = 10010000 +A & B = 10000000 +# 例子2 + A = 10001001 + C = 10001000 +A & C = 10001000 +复制代码 +``` +## 3. 位运算在权限系统中的使用 +传统的权限系统里,存在很多关联关系,如用户和权限的关联,用户和角色的关联。系统越大,关联关系越多,越难以维护。而引入位运算,可以巧妙的解决该问题。 +在讲“位运算在权限系统中的使用”之前,我们先假定两个前提,**下文所有的讨论都是基于这两个前提的**: + +1. 每种权限码都是唯一的(这是显然的) +1. 所有权限码的二进制数形式,有且只有一位值为 1,其余全部为 0(`2^n`) + +如果用户权限和权限码,全部使用二级制数字表示,再结合上面 `AND` 和 `OR` 的例子,分析位运算的特点,不难发现: + +- `|` 可以用来赋予权限 +- `&` 可以用来校验权限 + +为了讲的更明白,这里用 Linux 中的实例分析下,Linux 的文件权限分为读、写和执行,有字母和数字等多种表现形式: + +| 权限 | 字母表示 | 数字表示 | 二进制 | +| :--- | :--- | :--- | :--- | +| 读 | r | 4 | 0b100 | +| 写 | w | 2 | 0b010 | +| 执行 | x | 1 | 0b001 | + +可以看到,权限用 1、2、4(也就是 `2^n`)表示,转换为二进制后,都是只有一位是 1,其余为 0。我们通过几个例子看下,如何利用二进制的特点执行权限的添加,校验和删除。 +### 3.1. 添加权限 +``` +let r = 0b100 +let w = 0b010 +let x = 0b001 +// 给用户赋全部权限(使用前面讲的 | 操作) +let user = r | w | x +console.log(user) +// 7 +console.log(user.toString(2)) +// 111 +// r = 0b100 +// w = 0b010 +// r = 0b001 +// r|w|x = 0b111 +复制代码 +``` +可以看到,执行 `r | w | x` 后,`user` 的三位都是 1,表明拥有了全部三个权限。 +> Linux 下出现权限问题时,最粗暴的解决方案就是 `chmod 777 xxx`,这里的 `7` 就代表了:可读,可写,可执行。而三个 `7` 分别代表:文件所有者,文件所有者所在组,所有其他用户。 + +### 3.2. 校验权限 +刚才演示了权限的添加,下面演示权限校验: +``` +let r = 0b100 +let w = 0b010 +let x = 0b001 +// 给用户赋 r w 两个权限 +let user = r | w +// user = 6 +// user = 0b110 (二进制) +console.log((user & r) === r) // true 有 r 权限 +console.log((user & w) === w) // true 有 w 权限 +console.log((user & x) === x) // false 没有 x 权限 +复制代码 +``` +如前所料,通过 `用户权限 & 权限 code === 权限 code` 就可以判断出用户是否拥有该权限。 +### 3.3. 删除权限 +我们讲了用 `|` 赋予权限,使用 `&` 判断权限,那么删除权限呢?删除权限的本质其实是**将指定位置上的 1 重置为 0**。上个例子里用户权限是 `0b110`,拥有读和写两个权限,现在想删除读的权限,本质上就是将第三位的 1 重置为 0,变为 `0b010`: +``` +let r = 0b100 +let w = 0b010 +let x = 0b001 +let user = 0b010; +console.log((user & r) === r) // false 没有 r 权限 +console.log((user & w) === w) // true 有 w 权限 +console.log((user & x) === x) // false 没有 x 权限 +复制代码 +``` +那么具体怎么操作呢?其实有两种方案,最简单的就是异或 `^`,按照上文的介绍“当两个操作数相应的比特位有且只有一个 1 时,结果为 1,否则为 0”,所以异或其实是 toggle 操作,无则增,有则减: +``` +let r = 0b100 +let w = 0b010 +let x = 0b001 +let user = 0b110 // 有 r w 两个权限 +// 执行异或操作,删除 r 权限 +user = user ^ r +console.log((user & r) === r) // false 没有 r 权限 +console.log((user & w) === w) // true 有 w 权限 +console.log((user & x) === x) // false 没有 x 权限 +console.log(user.toString(2)) // 现在 user 是 0b010 +// 再执行一次异或操作 +user = user ^ r +console.log((user & r) === r) // true 有 r 权限 +console.log((user & w) === w) // true 有 w 权限 +console.log((user & x) === x) // false 没有 x 权限 +console.log(user.toString(2)) // 现在 user 又变回 0b110 +复制代码 +``` +那么如果单纯的想删除权限(而不是无则增,有则减)怎么办呢?答案是执行 `&(~code)`,先取反,再执行与操作: +``` +let r = 0b100 +let w = 0b010 +let x = 0b001 +let user = 0b110 // 有 r w 两个权限 +// 删除 r 权限 +user = user & (~r) +console.log((user & r) === r) // false 没有 r 权限 +console.log((user & w) === w) // true 有 w 权限 +console.log((user & x) === x) // false 没有 x 权限 +console.log(user.toString(2)) // 现在 user 是 0b010 +// 再执行一次 +user = user & (~r) +console.log((user & r) === r) // false 没有 r 权限 +console.log((user & w) === w) // true 有 w 权限 +console.log((user & x) === x) // false 没有 x 权限 +console.log(user.toString(2)) // 现在 user 还是 0b010,并不会新增 +复制代码 +``` +## 4. 局限性和解决办法 +前面我们回顾了 JavaScript 中的 Number 和位运算,并且了解了基于位运算的权限系统原理和 Linux 文件系统权限的实例。 +上述的所有都有前提条件:1、**每种权限码都是唯一的**;2、**每个权限码的二进制数形式,有且只有一位值为 1(`2^n`)**。也就是说,权限码只能是 1, 2, 4, 8,...,1024,...而上文提到,一个数字的范围只能在 -(2^53 -1) 和 2^53 -1 之间,JavaScript 的按位操作符又是将其操作数当作 **32 位**比特序列的。那么同一个应用下可用的权限数就非常有限了。这也是该方案的局限性。 +为了突破这个限制,这里提出一个叫“权限空间”的概念,既然权限数有限,那么不妨就多开辟几个空间来存放。 +基于权限空间,我们定义两个格式: + +1. **权限 code**,字符串,形如 `index,pos`。其中 `pos` 表示 32 位二进制数中 1 的位置(其余全是 0); `index` 表示**权限空间**,用于突破 JavaScript 数字位数的限制,是从 0 开始的正整数,每个权限code都要归属于一个权限空间。`index` 和 `pos` 使用英文逗号隔开。 +1. **用户权限**,字符串,形如 `1,16,16`。英文逗号分隔每一个**权限空间**的权限值。例如 `1,16,16` 的意思就是,权限空间 0 的权限值是 1,权限空间 1 的权限值是 16,权限空间 2 的权限是 16。 + +干说可能不好懂,直接上代码: +``` +// 用户的权限 code +let userCode = "" +// 假设系统里有这些权限 +// 纯模拟,正常情况下是按顺序的,如 0,0 0,1 0,2 ...,尽可能占满一个权限空间,再使用下一个 +const permissions = { + SYS_SETTING: { + value: "0,0", // index = 0, pos = 0 + info: "系统权限" + }, + DATA_ADMIN: { + value: "0,8", + info: "数据库权限" + }, + USER_ADD: { + value: "0,22", + info: "用户新增权限" + }, + USER_EDIT: { + value: "0,30", + info: "用户编辑权限" + }, + USER_VIEW: { + value: "1,2", // index = 1, pos = 2 + info: "用户查看权限" + }, + USER_DELETE: { + value: "1,17", + info: "用户删除权限" + }, + POST_ADD: { + value: "1,28", + info: "文章新增权限" + }, + POST_EDIT: { + value: "2,4", + info: "文章编辑权限" + }, + POST_VIEW: { + value: "2,19", + info: "文章查看权限" + }, + POST_DELETE: { + value: "2,26", + info: "文章删除权限" + } +} +// 添加权限 +const addPermission = (userCode, permission) => { + const userPermission = userCode ? userCode.split(",") : [] + const [index, pos] = permission.value.split(",") + userPermission[index] = (userPermission[index] || 0) | Math.pow(2, pos) + return userPermission.join(",") +} +// 删除权限 +const delPermission = (userCode, permission) => { + const userPermission = userCode ? userCode.split(",") : [] + const [index, pos] = permission.value.split(",") + userPermission[index] = (userPermission[index] || 0) & (~Math.pow(2, pos)) + return userPermission.join(",") +} +// 判断是否有权限 +const hasPermission = (userCode, permission) => { + const userPermission = userCode ? userCode.split(",") : [] + const [index, pos] = permission.value.split(",") + const permissionValue = Math.pow(2, pos) + return (userPermission[index] & permissionValue) === permissionValue +} +// 列出用户拥有的全部权限 +const listPermission = userCode => { + const results = [] + if (!userCode) { + return results + } + Object.values(permissions).forEach(permission => { + if (hasPermission(userCode, permission)) { + results.push(permission.info) + } + }) + return results +} +const log = () => { + console.log(`userCode: ${JSON.stringify(userCode, null, " ")}`) + console.log(`权限列表: ${listPermission(userCode).join("; ")}`) + console.log("") +} +userCode = addPermission(userCode, permissions.SYS_SETTING) +log() +// userCode: "1" +// 权限列表: 系统权限 +userCode = addPermission(userCode, permissions.POST_EDIT) +log() +// userCode: "1,,16" +// 权限列表: 系统权限; 文章编辑权限 +userCode = addPermission(userCode, permissions.USER_EDIT) +log() +// userCode: "1073741825,,16" +// 权限列表: 系统权限; 用户编辑权限; 文章编辑权限 +userCode = addPermission(userCode, permissions.USER_DELETE) +log() +// userCode: "1073741825,131072,16" +// 权限列表: 系统权限; 用户编辑权限; 用户删除权限; 文章编辑权限 +userCode = delPermission(userCode, permissions.USER_EDIT) +log() +// userCode: "1,131072,16" +// 权限列表: 系统权限; 用户删除权限; 文章编辑权限 +userCode = delPermission(userCode, permissions.USER_EDIT) +log() +// userCode: "1,131072,16" +// 权限列表: 系统权限; 用户删除权限; 文章编辑权限 +userCode = delPermission(userCode, permissions.USER_DELETE) +userCode = delPermission(userCode, permissions.SYS_SETTING) +userCode = delPermission(userCode, permissions.POST_EDIT) +log() +// userCode: "0,0,0" +// 权限列表: +userCode = addPermission(userCode, permissions.SYS_SETTING) +log() +// userCode: "1,0,0" +// 权限列表: 系统权限 +复制代码 +``` +除了通过引入**权限空间**的概念突破二进制运算的位数限制,还可以使用 [math.js](http://mathjs.org) 的 `bignumber`,直接运算超过 32 位的二进制数,具体可以看它的文档,这里就不细说了。 +## 5. 适用场景和问题 +如果按照当前使用最广泛的 [RBAC](https://zh.wikipedia.org/wiki/%E4%BB%A5%E8%A7%92%E8%89%B2%E7%82%BA%E5%9F%BA%E7%A4%8E%E7%9A%84%E5%AD%98%E5%8F%96%E6%8E%A7%E5%88%B6) 模型设计权限系统,那么一般会有这么几个实体:应用,权限,角色,用户。用户权限可以直接来自权限,也可以来自角色: + +- 一个应用下有多个权限 +- 权限和角色是多对多的关系 +- 用户和角色是多对多的关系 +- 用户和权限是多对多的关系 + +在此种模型下,一般会有用户与权限,用户与角色,角色与权限的对应关系表。想象一个商城后台权限管理系统,可能会有上万,甚至十几万店铺(应用),每个店铺可能会有数十个用户,角色,权限。随着业务的不断发展,刚才提到的那三张对应关系表会越来越大,越来越难以维护。 +而进制转换的方法则可以省略对应关系表,减少查询,节省空间。当然,省略掉对应关系不是没有坏处的,例如下面几个问题: + +- 如何高效的查找我的权限? +- 如何高效的查找拥有某权限的所有用户? +- 如何控制权限的有效期? + +所以进制转换的方案比较适合刚才提到的应用极其多,而每个应用中用户,权限,角色数量较少的场景。 +## 6. 其他方案 +除了二进制方案,当然还有其他方案可以达到类似的效果,例如直接使用一个1和0组成的字符串,权限点对应index,1表示拥有权限,0表示没有权限。举个例子:添加 0、删除 1、编辑 2,用户A拥有添加和编辑的权限,则 userCode 为 101;用户B拥有全部权限,userCode 为 111。这种方案比二进制转换简单,但是浪费空间。 +还有利用质数的方案,权限点全部为质数,用户权限为他所拥有的全部权限点的乘积。如:权限点是 2、3、5、7、11,用户权限是 5 * 7 * 11 = 385。这种方案麻烦的地方在于获取质数(新增权限点)和质因数分解(判断权限),权限点特别多的时候就快成 RSA 了,如果只有增删改查个别几个权限,倒是可以考虑。 +## 7. 参考 + +- [MDN:JavaScript 数字和日期](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Numbers_and_dates) +- [双精度浮点类型](https://zh.wikipedia.org/wiki/%E9%9B%99%E7%B2%BE%E5%BA%A6%E6%B5%AE%E9%BB%9E%E6%95%B8) +- [MDN:按位操作符](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators) +- [【小知识大道理】被忽视的位运算](https://www.jianshu.com/p/11f331d97ec2) +- [为什么不要在 JavaScript 中使用位操作符?](http://jerryzou.com/posts/do-you-really-want-use-bit-operators-in-JavaScript/) +- [角色权限设计的100种解法](https://mp.weixin.qq.com/s/MKljuxfDMmopr_MrnYljGw) +- [权限系统与RBAC模型概述](http://www.cnblogs.com/shijiaqi1066/p/3793894.html) +- [权限设计及算法](https://blog.csdn.net/yingchunhua365/article/details/14090461) +- [基于角色的访问控制](https://zh.wikipedia.org/wiki/%E4%BB%A5%E8%A7%92%E8%89%B2%E7%82%BA%E5%9F%BA%E7%A4%8E%E7%9A%84%E5%AD%98%E5%8F%96%E6%8E%A7%E5%88%B6) + diff --git "a/Cute-Article/article/82-5\344\270\252\346\226\271\346\263\225\345\210\244\346\226\255\345\217\230\351\207\217\346\230\257\345\220\246\344\270\272\346\225\260\347\273\204.md" "b/Cute-Article/article/82-5\344\270\252\346\226\271\346\263\225\345\210\244\346\226\255\345\217\230\351\207\217\346\230\257\345\220\246\344\270\272\346\225\260\347\273\204.md" new file mode 100644 index 00000000..ffcc04bd --- /dev/null +++ "b/Cute-Article/article/82-5\344\270\252\346\226\271\346\263\225\345\210\244\346\226\255\345\217\230\351\207\217\346\230\257\345\220\246\344\270\272\346\225\260\347\273\204.md" @@ -0,0 +1,137 @@ +日常开发中,我们经常遇到这种情况,需要我们判断变量是否是一个数组类型。 + +那么今天我把常用的判断**变量是否是数组类型**的方法,整理在这里: + +## 一、常用方法 + +### 1. Object.prototype.toString +通常我们可以使用 `Object.prototype.toString` 方法进行判断,详细可以查看[《Object.prototype.toString() - MDN - Mozilla》](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/toString)。 +```javascript +let arr = [1,2,3], obj = {name: "leo"}; +function isArray(a){ + return Object.prototype.toString.call(a)==='[object Array]'; +} +isArray(arr); // true +isArray(obj); // false +``` + +### 2. Array.isArray(arr) +`Array.isArray(arr)` 方法使用起来很简单: + +```javascript +let arr = [1,2,3], obj = {name: "leo"}; +Array.isArray(arr); // true +Array.isArray(obj); // false +``` + +需要注意的是 `Array.isArray(arr)` 方法虽然简单,但是存在兼容性问题,`Array.isArray`是ES 5.1推出的,不支持IE6~8,所以在使用的时候也应注意兼容问题。 + +```javascript +if(!Array.isArray){ + Array.isArray = function(a){ + return Object.prototype.toString.call(a)==='[object Array]' + } +} +``` + +## 二、不够准确的方法 + +### 1. 原型链 +使用原型链判断也比较简单: + +```javascript +let arr = [1,2,3], obj = {name: "leo"}; +arr.__proto__.constructor === Array; // true +obj.__proto__.constructor === Array; // true + +arr.constructor === Array; //true +obj.constructor === Array; //true +``` + +但是之所以说 `constructor` 不够准确,是因为在不同执行环境下, `constructor` 判断会不正确。
    比如下面这种情况,我们重写 `constructor` : + +```javascript +let arr = [1,2,3], obj = {name: "leo"}; +arr.constructor === Array // true +obj.constructor === Array // false + +obj.constructor = Array; +obj.constructor === Array // true +``` + +### 2. instanceof + +简单使用 `instanceof` 如下: + +```javascript +let arr = [1,2,3], obj = {name: "leo"}; +arr instanceof Array; // true +obj instanceof Array; // false +``` + +但是  `instanceof` 也存在局限性,它必须在**当前页面声明**,如父页面中存在一个 iframe,并且 iframe 中引用了一个子页面,在子页面中声明了一个 `arr` ,并将其赋值给父页面的一个变量,这时判断该变量,`Array == object.constructor;` 会返回 false; + +```javascript +let iframe = document.createElement('iframe'); +document.body.appendChild(iframe); + +let arr = [1,2,3]; +myArray = window.frames[0].Array; +let myArr = new myArray(4,5,6); + +myArr instanceof Array; //false +myArr.constructor == Array;// false + +Array.prototype == myArray.prototype); //false +arr instanceof myArray); //false + +myArr.constructor === Array; // false +arr.constructor === Array; // true +myArr.constructor === myArray;// true +Array.isArray(arrx); //true +``` + +## 三、错误的方法 +### 1. typeof + +`typeof` 是无法判断是否是数组的: + +```javascript +let arr = [1,2,3], obj = {name: "leo"}; +typeof arr; // "object" +typeof obj; // "object" +``` + +所以可以看出, `typeof` 适合用来判断**基本数据类型**。 + +`typeof` 的一些判断结果: + +```javascript +// 基本类型 +typeof 123; //number +typeof "leo"; //string +typeof true; //boolean +typeof undefined; //undefined +typeof null; //object +let leo = Symbol; +typeof leo; //symbol + +// 引用类型 +typeof [1,2,3]; //object +typeof {}; //object +typeof function(){}; //function +typeof Array; //function Array类型的构造函数 +typeof Object; //function Object类型的构造函数 +typeof Symbol; //function Symbol类型的构造函数 +typeof Number; //function Number类型的构造函数 +typeof String; //function String类型的构造函数 +typeof Boolean; //function Boolean类型的构造函数 +``` + +## 四、总结 + +本文主要给大家从三个角度去介绍一些判断变量是否是数组的方法,在日常开发中【一、常见方法】中的 2 个方法,已经足够我们使用了,也建议使用这 2 种方法。 + +## 参考文章 + +- [《判断是否是数组的几种方法》](https://juejin.im/post/5be52b1ae51d450b3647e766) diff --git "a/Cute-Article/article/83-\343\200\220\345\216\237\345\210\233\343\200\221JS\350\207\252\345\256\232\344\271\211\344\272\213\344\273\266\345\246\202\346\255\244\347\256\200\345\215\225\357\274\201.md" "b/Cute-Article/article/83-\343\200\220\345\216\237\345\210\233\343\200\221JS\350\207\252\345\256\232\344\271\211\344\272\213\344\273\266\345\246\202\346\255\244\347\256\200\345\215\225\357\274\201.md" new file mode 100644 index 00000000..07dfeb13 --- /dev/null +++ "b/Cute-Article/article/83-\343\200\220\345\216\237\345\210\233\343\200\221JS\350\207\252\345\256\232\344\271\211\344\272\213\344\273\266\345\246\202\346\255\244\347\256\200\345\215\225\357\274\201.md" @@ -0,0 +1,241 @@ +在前端开发世界中,JavaScript 和 HTML 之间往往通过 **事件** 来实现交互。其中多数为内置事件,本文主要介绍 JS**自定义事件概念和实现方式**,并结合案例详细分析自定义事件的原理、功能、应用及注意事项。 + +# 📚一、什么是自定义事件 +在日常开发中,我们习惯监听页面许多事件,诸如:点击事件( `click` )、鼠标移动事件( `mousemove` )、元素失去焦点事件( `blur` )等等。
    +
    事件本质是一种通信方式,是一种消息,只有在多对象多模块时,才有可能需要使用事件进行通信。在多模块化开发时,可以使用**自定义事件**进行模块间通信。
    +
    当某些基础事件无法满足我们业务,就可以尝试 **自定义事件**来解决。 + +# 📚二、实现方式介绍 + +目前实现**自定义事件**的两种主要方式是 JS 原生的 `Event()` 构造函数和 `CustomEvent()` 构造函数来创建。
    + +## 1. Event() +`Event()` 构造函数, 创建一个新的事件对象 `Event`。 + +### 1.1 语法 +```javascript +let myEvent = new Event(typeArg, eventInit); +``` + +### 1.2 参数 +`typeArg`  : `DOMString` 类型,表示创建事件的名称;
    `eventInit` :可选配置项,包括: + +| 字段名称 | 说明 | 是否可选 | 类型 | 默认值 | +| :---: | :---: | :---: | :---: | :---: | +| `bubbles` | 表示该事件**是否冒泡**。 | 可选 | `Boolean`  | false | +| `cancelable` | 表示该事件**能否被取消**。 | 可选 | `Boolean`  | false | +| `composed` | 指示事件是否会在**影子DOM根节点之外**触发侦听器。  | 可选 | `Boolean`  | false | + + +### 1.3 演示示例 +```javascript +// 创建一个支持冒泡且不能被取消的 pingan 事件 +let myEvent = new Event("pingan", {"bubbles":true, "cancelable":false}); +document.dispatchEvent(myEvent); + +// 事件可以在任何元素触发,不仅仅是document +testDOM.dispatchEvent(myEvent); +``` + +### 1.4 兼容性 +![image.png](https://user-gold-cdn.xitu.io/2020/2/22/1706b7199abd2e9e?w=1265&h=326&f=png&s=50137)
    图片来源:[https://caniuse.com/](https://caniuse.com/) + +## 2. CustomEvent() +`CustomEvent()` 构造函数, 创建一个新的事件对象 `CustomEvent`。 + +### 2.1 语法 + +```javascript +let myEvent = new CustomEvent(typeArg, eventInit); +``` + +### 2.2 参数 +`typeArg`  : `DOMString` 类型,表示创建事件的名称;
    `eventInit` :可选配置项,包括: + +| 字段名称 | 说明 | 是否可选 | 类型 | 默认值 | +| :---: | :---: | :---: | :---: | :---: | +| `detail` | 表示该事件中需要被传递的数据,在 `EventListener` 获取。 | 可选 | `Any`  | null | +| `bubbles` | 表示该事件**是否冒泡**。 | 可选 | `Boolean`  | false | +| `cancelable` | 表示该事件**能否被取消**。 | 可选 | `Boolean`  | false | + +### 2.3 演示示例 +```javascript +// 创建事件 +let myEvent = new CustomEvent("pingan", { + detail: { name: "wangpingan" } +}); + +// 添加适当的事件监听器 +window.addEventListener("pingan", e => { + alert(`pingan事件触发,是 ${e.detail.name} 触发。`); +}); +document.getElementById("leo2").addEventListener( + "click", function () { + // 派发事件 + window.dispatchEvent(myEvent); + } +) +``` +![image.png](https://user-gold-cdn.xitu.io/2020/2/22/1706b7199ae8f81e?w=457&h=140&f=png&s=7499)
    +
    我们也可以给自定义事件添加属性: + +```javascript +myEvent.age = 18; +``` + +### 2.4 兼容性 +![image.png](https://user-gold-cdn.xitu.io/2020/2/22/1706b7199af32342?w=1261&h=331&f=png&s=54279)
    图片来源:[https://caniuse.com/](https://caniuse.com/) + +### 2.5 IE8 兼容 +分发事件时,需要使用 `dispatchEvent` 事件触发,它在 IE8 及以下版本中需要进行使用 `fireEvent` 方法兼容: + +```javascript +if(window.dispatchEvent) { + window.dispatchEvent(myEvent); +} else { + window.fireEvent(myEvent); +} +``` + +## 3. Event() 与 CustomEvent() 区别 +从两者支持的参数中,可以看出:
    `Event()` 适合创建简单的自定义事件,而 `CustomEvent()` 支持参数传递的自定义事件,它支持 `detail` 参数,作为事件中**需要被传递的数据**,并在 `EventListener` 获取。 + +**注意:**
    当一个事件触发时,若相应的元素及其上级元素没有进行事件监听,则不会有回调操作执行。 
    当需要对于子元素进行监听,可以在其父元素进行事件托管,让事件在事件冒泡阶段被监听器捕获并执行。此时可以使用 `event.target` 获取到具体触发事件的元素。 + +# 📚三、使用场景 +**事件本质是一种消息**,事件模式本质上是**观察者模式**的实现,即能用**观察者模式**的地方,自然也能用**事件模式**。
    + +## 1.场景介绍 +比如这两种场景:
    + +- **场景1:单个目标对象发生改变,需要通知多个观察者一同改变。** + +如:当微博列表中点击“关注”,此时会同时发生很多事:推荐更多类似微博,个人关注数增加...
    ![image.png](https://user-gold-cdn.xitu.io/2020/2/22/1706b7199d02caa1?w=1394&h=550&f=png&s=329270) + +- **场景2:解耦多模块开协作。** + +如:小王负责A模块开发,小陈负责B模块开发,模块B需要模块A正常运行之后才能执行。 + +## 2. 代码实现 + +### 2.1 场景1实现 +**场景1:单个目标对象发生改变,需要通知多个观察者一同改变。**
    本例子模拟三个页面进行演示:
    1.微博列表页(Weibo.js)
    2.粉丝列表页(User.js)
    3.微博首页(Home.js) + +在**微博列表页(Weibo.js)**中,我们导入其他两个页面,并且监听【关注微博】按钮的点击事件,在回调事件中,创建一个自定义事件 `focusUser`,并在 `document` 上使用 `dispatchEvent` 方法派发自定义事件。 +```javascript +// Weibo.js +import UserModule from "./User.js"; +import HomeModule from "./Home.js"; +const eventButton = document.getElementById("eventButton"); +eventButton.addEventListener("click", event => { + const focusUser = new Event("focusUser"); + document.dispatchEvent(focusUser); +}) +``` + +接下来两个页面实现的代码基本一致,这里为了方便观察,设置了两者不同输出日志。 + +```javascript +// User.js +const eventButton = document.getElementById("eventButton"); +document.addEventListener("focusUser", event => { + console.log("【粉丝列表页】监听到自定义事件触发,event:",event); +}) + +// Home.js +const eventButton = document.getElementById("eventButton"); +document.addEventListener("focusUser", event => { + console.log("【微博首页】监听到自定义事件触发,event:",event); +}) +``` + +点击【关注微博】按钮后,看到控制台输出如下日志信息: + +![image.png](https://user-gold-cdn.xitu.io/2020/2/22/1706b7199d2f7350?w=715&h=267&f=png&s=29435) + +最终实现了,在 **微博列表页(Weibo.js)**组件负责派发事件,其他组价负责监听事件,这样三个组件之间耦合度非常低,完全不用关系对方,互相不影响。
    **其实这也是实现了观察者模式。** + +### 2.2 场景2实现 +**场景2:解耦多模块开协作。**
    举个更直观的例子,当微博需要加入【**一键三连**】新功能,需要产品原型和UI设计完后,程序员才能开发。
    本例子模拟四个模块:
    1.流程控制(Index.js)
    2.产品设计(Production.js)
    3.UI设计(Design.js)
    4.程序员开发(Develop.js) + +![image.png](https://user-gold-cdn.xitu.io/2020/2/22/1706b7199d71549f?w=1031&h=336&f=png&s=46607) + +在**流程控制(Index.js)模块**中,我们需要将其他三个流程的模块都导入进来,然后监听【开始任务】按钮的点击事件,在回调事件中,创建一个自定义事件 `startTask`,并在 `document` 上使用 `dispatchEvent` 方法派发自定义事件。 + +```javascript +// Index.js +import ProductionModule from "./Production.js"; +import DesignModule from "./Design.js"; +import DevelopModule from "./Develop.js"; + +const start = document.getElementById("start"); +start.addEventListener("click", event => { + console.log("开始执行任务") + const startTask = new Event("startTask"); + document.dispatchEvent(startTask); +}) +``` + +在 Production 产品设计模块中,监听任务开始事件 `startTask` 后,模拟1秒后原型设计完成,并派发一个新的事件 `productionSuccess` ,开始接下来的UI稿设计。 +```javascript +// Production.js +document.addEventListener("startTask", () => { + console.log("产品开始设计..."); + setTimeout(() => { + console.log("产品原型设计完成"); + console.log("--------------"); + document.dispatchEvent(new Event("productionSuccess")); + }, 1000); +}); +``` + +在UI稿设计和程序开发模块,其实也类似,代码实现: +```javascript +// Dedign.js +document.addEventListener("productionSuccess", () => { + console.log("UI稿开始设计..."); + setTimeout(() => { + console.log("UI稿设计完成"); + console.log("--------------"); + document.dispatchEvent(new Event("designSuccess")); + }, 1000); +}); + +// Production.js +document.addEventListener("designSuccess", function (e) { + console.log("开始开发功能..."); + setTimeout(function () { + console.log("【一键三连】开发完成"); + }, 2000) +}); +``` + +开发完成后,我们点击【开始任务】按钮后,看到控制台输出如下日志信息: + +![image.png](https://user-gold-cdn.xitu.io/2020/2/22/1706b719c40359b3?w=717&h=268&f=png&s=23968) + +最终实现了在 **流程控制(Index.js)模块**负责派发事件,其他组件负责监听事件,按流程完成其他任务。
    **可以看出,原型设计、UI稿设计和程序开发任务,互不影响,易于任务拓展。** + +# 📚四、总结 +本文详细介绍 JS**自定义事件概念和实现方式**,并结合两个实际场景进行代码演示。细心的小伙伴会发现,这两个实际场景都是用 `Event()` 构造函数实现,当然也是可以使用 `CustomEvent` 构造函数来代替。
    另外本文也详细介绍两种实现方式,包括其区别和兼容性。
    最后也希望大家能在实际开发中,多思考代码解耦,适当使用**自定义事件**来提高代码质量。 + +如有错误,欢迎指点。 + +# 📚五、参考文章 + +- 《[javascript自定义事件功能与用法实例分析](https://m.jb51.net/article/127776.htm)》 +- 《[Event - MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/Event/Event)》 +- 《[CustomEvent - MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/CustomEvent)》 + + + +|Author|王平安| +|---|---| +|E-mail|pingan8787@qq.com| +|博 客|www.pingan8787.com| +|微 信|pingan8787| +|每日文章推荐|https://github.com/pingan8787/Leo_Reading/issues| +|ES小册|js.pingan8787.com| + +## 微信公众号 +![bg](http://images.pingan8787.com/2019_07_12guild_page.png) \ No newline at end of file diff --git "a/Cute-Article/article/84-\343\200\220\345\216\237\345\210\233\343\200\221Webpack\346\217\222\344\273\266\345\274\200\345\217\221\345\246\202\346\255\244\347\256\200\345\215\225\357\274\201.md" "b/Cute-Article/article/84-\343\200\220\345\216\237\345\210\233\343\200\221Webpack\346\217\222\344\273\266\345\274\200\345\217\221\345\246\202\346\255\244\347\256\200\345\215\225\357\274\201.md" new file mode 100644 index 00000000..4e9c010c --- /dev/null +++ "b/Cute-Article/article/84-\343\200\220\345\216\237\345\210\233\343\200\221Webpack\346\217\222\344\273\266\345\274\200\345\217\221\345\246\202\346\255\244\347\256\200\345\215\225\357\274\201.md" @@ -0,0 +1,351 @@ +> 本文使用的[**Webpack-Quickly-Starter**](https://github.com/pingan8787/Webpack-Quickly-Starter)快速搭建 Webpack4 本地学习环境。 +> 建议多阅读 Webpack 文档《[**Writing a Plugin**](https://webpack.js.org/contribute/writing-a-plugin/)》章节,学习开发简单插件。 + + +本文将带你一起开发你的第一个 Webpack 插件,从 Webpack 配置工程师,迈向 Webpack 开发工程师!
    做自己的轮子,让别人用去吧。 + +完整代码存放在:[https://github.com/pingan8787/script-timestamp-webpack-plugin](https://github.com/pingan8787/script-timestamp-webpack-plugin) + +![](https://user-gold-cdn.xitu.io/2020/2/24/1707463e4ec11bce?w=800&h=400&f=png&s=30184) + +## 一、背景介绍 +本文灵感源自业务中的经验总结,**不怕神一样的产品,只怕一根筋的开发**。 + +在项目打包遇到问题:“当项目托管到 CDN 平台,希望实现项目中的 index.js 不被缓存”。因为我们需要修改 `index.js` 中的内容,不想用户被缓存。 + +思考一阵,有这么几种思路: + +1. 在 CDN 平台中过滤该文件的缓存设置; +1. 查找 DOM 元素,修改该 `script` 标签的 `src` 值,并添加时时间戳; +1. 打包时动态创建 `script` 标签引入文件,并添加时时间戳。 + +(聪明的你还有其他方法,欢迎讨论) + +思路分析: + +1. 显然修改 CDN 设置的话,治标不治本; +1. 在模版文件中,添加 `script` 标签,执行获取 Webpack 自动添加的 `script` 标签并为其 `src` 值添加时间戳。但事实是还没等你修改完, js 文件已经加载完毕,所以放弃 +1. 需要在 `index.html` 生成之前,修改 js 文件的路径,并添加时间戳。 + +于是我准备使用第三种方式,在 `index.html` 生成之前完成下面修改:
    ![image.png](https://user-gold-cdn.xitu.io/2020/2/24/170745f867efcd61?w=951&h=328&f=png&s=86717) + +问题简单,实际还是想试试开发 Webpack Plugin。 + +## 二、基础知识 +Webpack 使用阶段式的构建回调,开发者可以引入它们自己的行为到 Webpack 构建流程中。
    在开发之前,需要了解以下 Webpack 相关概念: + +### 2.1 Webpack 插件组成 +在自定义插件之前,我们需要了解,一个 Webpack 插件由哪些构成,下面摘抄文档: + +- 一个具名 JavaScript 函数; +- 在它的原型上定义 apply 方法; +- 指定一个触及到 Webpack 本身的[事件钩子](https://webpack.docschina.org/api/compiler-hooks/); +- 操作 Webpack 内部的实例特定数据; +- 在实现功能后调用 Webpack 提供的 callback。  + +### 2.2 Webpack 插件基本架构 +插件由一个构造函数实例化出来。构造函数定义 `apply` 方法,在安装插件时,`apply` 方法会被 Webpack `compiler` 调用一次。`apply` 方法可以接收一个 Webpack `compiler `对象的引用,从而可以在回调函数中访问到 `compiler` 对象。 + +官方文档提供一个简单的插件结构: + +```javascript +class HelloWorldPlugin { + apply(compiler) { + compiler.hooks.done.tap('Hello World Plugin', ( + stats /* 在 hook 被触及时,会将 stats 作为参数传入。 */ + ) => { + console.log('Hello World!'); + }); + } +} +module.exports = HelloWorldPlugin; +``` + +使用插件: + +```javascript +// webpack.config.js +var HelloWorldPlugin = require('hello-world'); + +module.exports = { + // ... 这里是其他配置 ... + plugins: [new HelloWorldPlugin({ options: true })] +}; +``` + +### 2.3 HtmlWebpackPlugin 介绍 +> HtmlWebpackPlugin 简化了 HTML 文件的创建,以便为你的 Webpack 包提供服务。这对于在文件名中包含每次会随着编译而发生变化哈希的 webpack bundle 尤其有用。 + +插件的基本作用概括:**生成 HTML 文件**。 + +`html-webapck-plugin` 插件**两个主要作用:** + +- 为 HTML 文件引入外部资源(如 `script` / `link` )动态添加每次编译后的 hash,防止引用文件的缓存问题; +- 动态创建 HTML 入口文件,如单页应用的 `index.html `文件。 + +`html-webapck-plugin` 插件**原理介绍:** + +- 读取 Webpack 中 `entry` 配置的相关入口 `chunk` 和 `extract-text-webpack-plugin` 插件抽取的 CSS 样式; +- 将样式插入到插件提供的 `template` 或 `templateContent` 配置指定的模版文件中; +- 插入方式是:通过 `link` 标签引入样式,通过 `script` 标签引入脚本文件; + +## 三、开发流程 +本文开发的 **自动添加时间戳引用脚本文件(SetScriptTimestampPlugin)** 插件实现的原理:通过 **HtmlWebpackPlugin** 生成 HTML 文件前,将模版文件**预留位置替换成脚本**,脚本中执行自动添加时间戳来引用脚本文件。 + +### 3.1 插件运行机制 +![SetScriptTimestampPlugin 运行机制.png](https://user-gold-cdn.xitu.io/2020/2/24/170745f8681eb3b6?w=2244&h=1604&f=png&s=269681) + +### 3.2 初始化插件文件 +新建 `SetScriptTimestampPlugin.js`  文件,并参考官方文档中插件的基本结构,初始化插件代码: + +```javascript +// SetScriptTimestampPlugin.js + +class SetScriptTimestampPlugin { + apply(compiler) { + compiler.hooks.done.tap('SetScriptTimestampPlugin', + (compilation, callback) => { + console.log('SetScriptTimestampPlugin!'); + }); + } +} +module.exports = SetScriptTimestampPlugin; +``` + +`apply` 方法为插件原型方法,接收 `compiler` 作为参数。 + +### 3.3 选择插件触发时机 +选择插件触发时机,其实是选择插件触发的 compiler 钩子(即何时触发插件)。
    Webpack 提供钩子有很多,这里简单介绍几个,完整具体可参考文档《[Compiler Hooks](https://webpack.js.org/api/compiler-hooks/)》: + +- `entryOption` : 在 webpack 选项中的 `entry` 配置项 处理过之后,执行插件。 +- `afterPlugins` : 设置完初始插件之后,执行插件。 +- `compilation` : 编译创建之后,生成文件之前,执行插件。。 +- `emit` : 生成资源到 `output` 目录之前。 +- `done` : 编译完成。 + +我们插件应该是要在 HTML 输出之前,动态添加 `script` 标签,所以我们选择钩入 `compilation` 阶段,代码修改: + +```diff +// SetScriptTimestampPlugin.js + +class SetScriptTimestampPlugin { + apply(compiler) { +- compiler.hooks.done.tap('SetScriptTimestampPlugin', ++ compiler.hooks.compilation.tap('SetScriptTimestampPlugin', + (compilation, callback) => { + console.log('SetScriptTimestampPlugin!'); + }); + } +} +module.exports = SetScriptTimestampPlugin; +``` + +在 `compiler.hooks` 下指定**事件钩子函数**,便会触发钩子时,执行回调函数。
    Webpack 提供三种触发钩子的方法: + +- `tap` :以**同步方式**触发钩子; +- `tapAsync` :以**异步方式**触发钩子; +- `tapPromise` :以**异步方式**触发钩子,返回 Promise; + +这三种方式能选择的钩子方法也不同,由于 `compilation` 是 `SyncHook` 同步钩子,所以采用 `tap` 触发方式。
    `tap` 方法接收两个参数:插件名称和回调函数。 + +### 3.4 添加插件替换入口 +我们原理上是将模版文件中,指定替换入口,再替换成需要执行的脚本。 + +![image.png](https://user-gold-cdn.xitu.io/2020/2/24/170745f8680672ef?w=947&h=340&f=png&s=83858) + +所以我们在模版文件 `template.html` 中添加 `` 作为标识替换入口: + +```html + + + + + Webpack 插件开发入门 + + + + + + +``` + +### 3.5 编写插件逻辑 +到这一步,才开始编写插件的逻辑。
    从上一步中,我们知道在 `tap` 第二个参数是个回调函数,并且这个回调函数有两个参数: `compilation` 和 `callback` 。 + +`compilation` 继承于`compiler`,包含 `compiler` 所有内容(也有 Webpack 的 `options`),而且也有 `plugin` 函数接入任务点。 + +```javascript +// SetScriptTimestampPlugin.js + +class SetScriptTimestampPlugin { + apply(compiler) { + compiler.hooks.compilation.tap('SetScriptTimestampPlugin', + (compilation, callback) => { + // 插件逻辑 调用compilation提供的plugin方法 + compilation.plugin( + "html-webpack-plugin-before-html-processing", + function(htmlPluginData, callback) { + // 读取并修改 script 上 src 列表 + let jsScr = htmlPluginData.assets.js[0]; + htmlPluginData.assets.js = []; + let result = ` + + `; + let resultHTML = htmlPluginData.html.replace( + "", result + ); + // 返回修改后的结果 + htmlPluginData.html = resultHTML; + } + ); + } + ); + } +} +module.exports = SetScriptTimestampPlugin; +``` + +在上面插件逻辑中,具体做了这些事: + +1. **执行 **`compilation.plugin`**  方法,并传入两个参数:插件事件和回调方法。** + +所谓“插件事件”即插件所提供的一些事件,用于监听插件状态,这里列举几个 `html-webpack-plugin` 提供的事件(完整可查看《[html-webpack-plugin](https://www.npmjs.com/package/html-webpack-plugin)》):
    Async: + +- `html-webpack-plugin-before-html-generation` +- `html-webpack-plugin-before-html-processing` +- `html-webpack-plugin-alter-asset-tags` + +Sync: + +- `html-webpack-plugin-alter-chunks` + +2. **获取脚本文件名称列表并清空。** + +在回调方法中,通过 `htmlPluginData.assets.js` 获取需要通过 `script` 引入的脚本文件名称列表,拷贝一份,并清空原有列表。 + +![image.png](https://user-gold-cdn.xitu.io/2020/2/24/170745f86a2d7bbe?w=334&h=93&f=png&s=27216) + +3. **编写替换逻辑。** + +替换逻辑即:动态创建一个 `script` 标签,将其 `src` 值设置为上一步读取到的脚本文件名,并在后面拼接 **时间戳** 作为参数。 + +4. **插入替换逻辑。** + +通过 `htmlPluginData.html` 可以获取到模版文件的字符串输出,我们只需要将模版字符串中替换入口 `` 替换成我们上一步编写的替换逻辑即可。 + +5. **返回HTML文件。** + +最后将修改后的 HTML 字符串,赋值给原来的 `htmlPluginData.html` 达到修改效果。 + +### 3.5 使用插件 +自定义插件使用方式,与其他插件一致,在 `plugins` 数组中实例化: + +```javascript +// webpack.config.js + +const SetScriptTimestampPlugin = require("./SetScriptTimestampPlugin.js"); +module.exports = { + // ... 省略其他配置 + plugins: [ + // ... 省略其他插件 + new SetScriptTimestampPlugin() + ] +} +``` + +到这一步,我们已经实现需求“当项目托管到 CDN 平台,希望实现项目中的 index.js 不被缓存”。
    ![image.png](https://user-gold-cdn.xitu.io/2020/2/24/170745f86a7fdfd0?w=467&h=291&f=png&s=44616) + +## 四、案例拓展 +这里以之前 **SetScriptTimestampPlugin** 插件为例子,继续拓展。 + +### 4.1 读取插件配置参数 +每个插件本质是一个类,跟一个类实例化相同,可以在实例化时传入配置参数,在构造函数中操作: + +```javascript +// SetScriptTimestampPlugin.js + +class SetScriptTimestampPlugin { + constructor(options) { + this.options = options; + } + apply(compiler) { + console.log(this.options.filename); // "index.js" + // ... 省略其他代码 + } +} +module.exports = SetScriptTimestampPlugin; +``` + +使用时: + +```javascript +// webpack.config.js + +const SetScriptTimestampPlugin = require("./SetScriptTimestampPlugin.js"); +module.exports = { + // ... 省略其他配置 + plugins: [ + // ... 省略其他插件 + new SetScriptTimestampPlugin({ + filename: "index.js" + }) + ] +} +``` + +### 4.2 添加多脚本文件的时间戳 +如果我们此时需要同时修改多个脚本文件的时间戳,也只需要将参数类型和执行脚本做下调整。
    具体修改脚本,这里不具体展开,篇幅有限,可以自行思考实现咯~
    这里展示使用插件时的参数: + +```javascript +// webpack.config.js + +const SetScriptTimestampPlugin = require("./SetScriptTimestampPlugin.js"); +module.exports = { + // ... 省略其他配置 + plugins: [ + // ... 省略其他插件 + new SetScriptTimestampPlugin({ + filename: ["index.js", "boundle.js", "pingan.js"] + }) + ] +} +``` + +生成结果: + +```html + + + +``` + +## 五、总结 +本文通用自定义 Webpack 插件来实现日常一些比较棘手的需求。主要为大家介绍了 Webpack 插件的基本组成和简单架构,也介绍了 HtmlWebpackPlugin 插件。并通过这些基础知识,完成了一个 HTML 文本替换插件,最后通过两个场景来拓展插件使用范围。 + +最后,关于 Webpack 插件开发,还有更多知识可以学习,建议多看看官方文档《[Writing a Plugin](https://webpack.js.org/contribute/writing-a-plugin/)》进行学习。 + +本文纯属个人经验总结,如有异议,欢迎指点。 + +## 参考文档 + +1. 《[Writing a Plugin](https://webpack.js.org/contribute/writing-a-plugin/)》 +1. 《[HtmlWebpackPlugin - Webpack](https://webpack.js.org/plugins/html-webpack-plugin/)》 +1. 《[扩展 HtmlwebpackPlugin 插入自定义的脚本](https://www.cnblogs.com/mjian/p/9250095.html)》 + +## 关于我 + +|Author|王平安| +|---|---| +|E-mail|pingan8787@qq.com| +|博 客|www.pingan8787.com| +|微 信|pingan8787| +|每日文章推荐|https://github.com/pingan8787/Leo_Reading/issues| +|ES小册|js.pingan8787.com| + +## 微信公众号 +![bg](https://user-gold-cdn.xitu.io/2020/2/22/1706bb1ea5f680ae?w=885&h=445&f=png&s=80093) \ No newline at end of file diff --git "a/Cute-Article/article/85-\345\275\273\345\272\225\346\220\236\346\207\202\345\271\266\345\256\236\347\216\260webpack\347\203\255\346\233\264\346\226\260\345\216\237\347\220\206.md" "b/Cute-Article/article/85-\345\275\273\345\272\225\346\220\236\346\207\202\345\271\266\345\256\236\347\216\260webpack\347\203\255\346\233\264\346\226\260\345\216\237\347\220\206.md" new file mode 100644 index 00000000..2a05cd56 --- /dev/null +++ "b/Cute-Article/article/85-\345\275\273\345\272\225\346\220\236\346\207\202\345\271\266\345\256\236\347\216\260webpack\347\203\255\346\233\264\346\226\260\345\216\237\347\220\206.md" @@ -0,0 +1,578 @@ +原文地址:[https://segmentfault.com/a/1190000020310371](https://segmentfault.com/a/1190000020310371) + +- [HMR是什么](https://segmentfault.com/a/1190000020310371#HMR%E6%98%AF%E4%BB%80%E4%B9%88) + - [使用场景](https://segmentfault.com/a/1190000020310371#%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF) +- [配置使用HMR](https://segmentfault.com/a/1190000020310371#%E9%85%8D%E7%BD%AE%E4%BD%BF%E7%94%A8HMR) + - [配置webpack](https://segmentfault.com/a/1190000020310371#%E9%85%8D%E7%BD%AEwebpack) + - [解析webpack打包后的文件内容](https://segmentfault.com/a/1190000020310371#%E8%A7%A3%E6%9E%90webpack%E6%89%93%E5%8C%85%E5%90%8E%E7%9A%84%E6%96%87%E4%BB%B6%E5%86%85%E5%AE%B9) + - [配置HMR](https://segmentfault.com/a/1190000020310371#%E9%85%8D%E7%BD%AEHMR) +- [HMR原理](https://segmentfault.com/a/1190000020310371#HMR%E5%8E%9F%E7%90%86) +- [debug服务端源码](https://segmentfault.com/a/1190000020310371#debug%E6%9C%8D%E5%8A%A1%E7%AB%AF%E6%BA%90%E7%A0%81) + - [服务端简易实现](https://segmentfault.com/a/1190000020310371#%E6%9C%8D%E5%8A%A1%E7%AB%AF%E7%AE%80%E6%98%93%E5%AE%9E%E7%8E%B0) + - [服务端调试阶段](https://segmentfault.com/a/1190000020310371#%E6%9C%8D%E5%8A%A1%E7%AB%AF%E8%B0%83%E8%AF%95%E9%98%B6%E6%AE%B5) +- [debug客户端源码](https://segmentfault.com/a/1190000020310371#debug%E5%AE%A2%E6%88%B7%E7%AB%AF%E6%BA%90%E7%A0%81) + - [客户端简易实现](https://segmentfault.com/a/1190000020310371#%E5%AE%A2%E6%88%B7%E7%AB%AF%E7%AE%80%E6%98%93%E5%AE%9E%E7%8E%B0) + - [客户端调试阶段](https://segmentfault.com/a/1190000020310371#%E5%AE%A2%E6%88%B7%E7%AB%AF%E8%B0%83%E8%AF%95%E9%98%B6%E6%AE%B5) +- [问题](https://segmentfault.com/a/1190000020310371#%E9%97%AE%E9%A2%98) +- [总结](https://segmentfault.com/a/1190000020310371#%E6%80%BB%E7%BB%93) + +## HMR是什么 +`HMR`即`Hot Module Replacement`是指当你对代码修改并保存后,`webpack`将会对代码进行重新打包,并将改动的模块发送到浏览器端,浏览器用新的模块替换掉旧的模块,去实现局部更新页面而非整体刷新页面。接下来将从使用到实现一版简易功能带领大家深入浅出`HMR`。
    文章首发于[@careteen/webpack-hmr](https://github.com/careteenL/webpack-hmr),转载请注明来源即可。 + +### 使用场景 +![](https://cdn.nlark.com/yuque/0/2020/jpeg/186051/1583939666973-5b57ede4-45d0-4e23-b722-ad85fcc78f1a.jpeg#align=left&display=inline&height=548&originHeight=548&originWidth=800&size=0&status=done&style=none&width=800)
    如上图所示,一个注册页面包含`用户名`、`密码`、`邮箱`三个必填输入框,以及一个`提交`按钮,当你在调试`邮箱`模块改动了代码时,没做任何处理情况下是会刷新整个页面,频繁的改动代码会浪费你大量时间去重新填写内容。预期是保留`用户名`、`密码`的输入内容,而只替换`邮箱`这一模块。这一诉求就需要借助`webpack-dev-server`的热模块更新功能。
    相对于`live reload`整体刷新页面的方案,`HMR`的优点在于可以保存应用的状态,提高开发效率。 + +## 配置使用HMR + +### 配置webpack +首先借助`webpack`搭建项目 + +- 初识化项目并导入依赖 +```bash +mkdir webpack-hmr && cd webpack-hmr +npm i -y +npm i -S webpack webpack-cli webpack-dev-server html-webpack-plugin +``` + +- 配置文件`webpack.config.js` +```javascript +const path = require('path') +const webpack = require('webpack') +const htmlWebpackPlugin = require('html-webpack-plugin') +module.exports = { + mode: 'development', // 开发模式不压缩代码,方便调试 + entry: './src/index.js', // 入口文件 + output: { + path: path.join(__dirname, 'dist'), + filename: 'main.js' + }, + devServer: { + contentBase: path.join(__dirname, 'dist') + }, + plugins: [ + new htmlWebpackPlugin({ + template: './src/index.html', + filename: 'index.html' + }) + ] +} +``` + +- 新建`src/index.html`模板文件 +```html + + + + + + + Webpack Hot Module Replacement + + +
    + + +``` + +- 新建`src/index.js`入口文件编写简单逻辑 +```javascript +var root = document.getElementById('root') +function render () { + root.innerHTML = require('./content.js') +} +render() +``` + +- 新建依赖文件`src/content.js`导出字符供index渲染页面 +```javascript +var ret = 'Hello Webpack Hot Module Replacement' +module.exports = ret +// export default ret +``` + +- 配置`package.json` +```javascript +"scripts": { + "dev": "webpack-dev-server", + "build": "webpack" + } +``` + +- 然后`npm run dev`即可启动项目 +- 通过`npm run build`打包生成静态资源到`dist`目录 + +接下来先分析下`dist`目录中的文件 + +### 解析webpack打包后的文件内容 + +- webpack自己实现的一套commonjs规范讲解 +- 区分commonjs和esmodule + +dist目录结构 +``` +. +├── index.html +└── main.js +``` + +#### 其中`index.html`内容如下 +```html + +
    + + +``` +使用`html-webpack-plugin`插件将入口文件及其依赖通过`script`标签引入 + +#### 先对`main.js`内容去掉注释和无关内容进行分析 +```javascript +(function (modules) { // webpackBootstrap + // ... +}) +({ + "./src/content.js": + (function (module, exports) { + eval("var ret = 'Hello Webpack Hot Module Replacement'\n\nmodule.exports = ret\n// export default ret\n\n"); + }), + "./src/index.js": (function (module, exports, __webpack_require__) { + eval("var root = document.getElementById('root')\nfunction render () {\n root.innerHTML = __webpack_require__(/*! ./content.js */ \"./src/content.js\")\n}\nrender()\n\n\n"); + }) +}); +``` +可见webpack打包后会产出一个自执行函数,其参数为一个对象 +```javascript +"./src/content.js": (function (module, exports) { + eval("...") +} +``` +键为入口文件或依赖文件相对于根目录的相对路径,值则是一个函数,其中使用`eval`执行文件的内容字符。 + +- 再进入自执行函数体内,可见webpack自己实现了一套`commonjs`规范 +```javascript +(function (modules) { + // 模块缓存 + var installedModules = {}; + function __webpack_require__(moduleId) { + // 判断是否有缓存 + if (installedModules[moduleId]) { + return installedModules[moduleId].exports; + } + // 没有缓存则创建一个模块对象并将其放入缓存 + var module = installedModules[moduleId] = { + i: moduleId, + l: false, // 是否已加载 + exports: {} + }; + // 执行模块函数 + modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); + // 将状态置为已加载 + module.l = true; + // 返回模块对象 + return module.exports; + } + // ... + // 加载入口文件 + return __webpack_require__(__webpack_require__.s = "./src/index.js"); +}) +``` +如果对上面> `commonjs`规范感兴趣可以前往我的另一篇文章> [手摸手带你实现commonjs规范](https://github.com/careteenL/blog/blob/master/src/20181201-node/module.md) +给出上面代码主要是先对webpack的产出文件混个眼熟,不要惧怕。其实任何一个不管多复杂的事物都是由更小更简单的东西组成,剖开它认识它爱上它。 + +### 配置HMR +接下来配置并感受一下热更新带来的便捷开发
    `webpack.config.js`配置 +```javascript +// ... + devServer: { + hot: true + } + // ... +``` +`./src/index.js`配置 +```javascript +// ... +if (module.hot) { + module.hot.accept(['./content.js'], () => { + render() + }) +} +``` +当更改`./content.js`的内容并保存时,可以看到页面没有刷新,但是内容已经被替换了。
    这对提高开发效率意义重大。接下来将一层层剖开它,认识它的实现原理。 + +## HMR原理 +![](https://cdn.nlark.com/yuque/0/2020/jpeg/186051/1583939666081-f8da2fc1-bc9b-494d-9376-0c5a4c129876.jpeg#align=left&display=inline&height=1430&originHeight=1430&originWidth=2088&size=0&status=done&style=none&width=2088)
    如上图所示,右侧`Server`端使用`webpack-dev-server`去启动本地服务,内部实现主要使用了`webpack`、`express`、`websocket`。 + +- 使用`express`启动本地服务,当浏览器访问资源时对此做响应。 +- 服务端和客户端使用`websocket`实现长连接 +- `webpack`监听源文件的变化,即当开发者保存文件时触发`webpack`的重新编译。 + - 每次编译都会生成`hash值`、`已改动模块的json文件`、`已改动模块代码的js文件` + - 编译完成后通过`socket`向客户端推送当前编译的`hash戳` +- 客户端的`websocket`监听到有文件改动推送过来的`hash戳`,会和上一次对比 + - 一致则走缓存 + - 不一致则通过`ajax`和`jsonp`向服务端获取最新资源 +- 使用`内存文件系统`去替换有修改的内容实现局部刷新 + +上图先只看个大概,下面将从服务端和客户端两个方面进行详细分析 + +## debug服务端源码 +![](https://cdn.nlark.com/yuque/0/2020/jpeg/186051/1583939666100-67768253-890b-4e3f-abd3-8b313b9dcd75.jpeg#align=left&display=inline&height=1430&originHeight=1430&originWidth=2088&size=0&status=done&style=none&width=2088)
    **现在也只需要关注上图的右侧服务端部分,左侧可以暂时忽略。下面步骤主要是debug服务端源码分析其详细思路,也给出了代码所处的具体位置,感兴趣的可以先行定位到下面的代码处设置断点,然后观察数据的变化情况。也可以先跳过阅读此步骤。** + +1. 启动`webpack-dev-server`服务器,源代码地址[@webpack-dev-server/webpack-dev-server.js#L173](https://github.com/webpack/webpack-dev-server/blob/v3.7.2/bin/webpack-dev-server.js#L173) +1. 创建webpack实例,源代码地址[@webpack-dev-server/webpack-dev-server.js#L89](https://github.com/webpack/webpack-dev-server/blob/v3.7.2/bin/webpack-dev-server.js#L89) +1. 创建Server服务器,源代码地址[@webpack-dev-server/webpack-dev-server.js#L107](https://github.com/webpack/webpack-dev-server/blob/v3.7.2/bin/webpack-dev-server.js#L107) +1. 添加webpack的done事件回调,源代码地址[@webpack-dev-server/Server.js#L122](https://github.com/webpack/webpack-dev-server/blob/v3.7.2/lib/Server.js#L122) + 1. 编译完成向客户端发送消息,源代码地址[@webpack-dev-server/Server.js#L184](https://github.com/webpack/webpack-dev-server/blob/v3.7.2/lib/Server.js#L184) +5. 创建express应用app,源代码地址[@webpack-dev-server/Server.js#L123](https://github.com/webpack/webpack-dev-server/blob/v3.7.2/lib/Server.js#L123) +5. 设置文件系统为内存文件系统,源代码地址[@webpack-dev-middleware/fs.js#L115](https://github.com/webpack/webpack-dev-middleware/blob/v3.7.0/lib/fs.js#L115) +5. 添加webpack-dev-middleware中间件,源代码地址[@webpack-dev-server/Server.js#L125](https://github.com/webpack/webpack-dev-server/blob/v3.7.2/lib/Server.js#L125) + 1. 中间件负责返回生成的文件,源代码地址[@webpack-dev-middleware/middleware.js#L20](https://github.com/webpack/webpack-dev-middleware/blob/v3.7.0/lib/middleware.js#L20) +8. 启动webpack编译,源代码地址[@webpack-dev-middleware/index.js#L51](https://github.com/webpack/webpack-dev-middleware/blob/v3.7.0/index.js#L51) +8. 创建http服务器并启动服务,源代码地址[@webpack-dev-server/Server.js#L135](https://github.com/webpack/webpack-dev-server/blob/v3.7.2/lib/Server.js#L135) +8. 使用sockjs在浏览器端和服务端之间建立一个 websocket 长连接,源代码地址[@webpack-dev-server/Server.js#L745](https://github.com/webpack/webpack-dev-server/blob/v3.7.2/lib/Server.js#L745) + 1. 创建socket服务器,源代码地址[@webpack-dev-server/SockJSServer.js#L34](https://github.com/webpack/webpack-dev-server/blob/v3.7.2/lib/servers/SockJSServer.js#L34) + +### 服务端简易实现 +上面是我通过debug得出dev-server运行流程比较核心的几个点,下面将其[抽象整合到一个文件中](https://github.com/careteenL/webpack-hmr/blob/master/dev-server.js)。 + +#### 启动webpack-dev-server服务器 +先导入所有依赖 +```javascript +const path = require('path') // 解析文件路径 +const express = require('express') // 启动本地服务 +const mime = require('mime') // 获取文件类型 实现一个静态服务器 +const webpack = require('webpack') // 读取配置文件进行打包 +const MemoryFileSystem = require('memory-fs') // 使用内存文件系统更快,文件生成在内存中而非真实文件 +const config = require('./webpack.config') // 获取webpack配置文件 +``` + +#### 创建webpack实例 +```javascript +const compiler = webpack(config) +``` +compiler代表整个webpack编译任务,全局只有一个 + +#### 创建Server服务器 +```javascript +class Server { + constructor(compiler) { + this.compiler = compiler + } + listen(port) { + this.server.listen(port, () => { + console.log(`服务器已经在${port}端口上启动了`) + }) + } +} +let server = new Server(compiler) +server.listen(8000) +``` +在后面是通过express来当启动服务的 + +#### 添加webpack的done事件回调 +```javascript +constructor(compiler) { + let sockets = [] + let lasthash + compiler.hooks.done.tap('webpack-dev-server', (stats) => { + lasthash = stats.hash + // 每当新一个编译完成后都会向客户端发送消息 + sockets.forEach(socket => { + socket.emit('hash', stats.hash) // 先向客户端发送最新的hash值 + socket.emit('ok') // 再向客户端发送一个ok + }) + }) + } +``` +`webpack`编译后提供提供了一系列钩子函数,以供插件能访问到它的各个生命周期节点,并对其打包内容做修改。`compiler.hooks.done`则是插件能修改其内容的最后一个节点。
    编译完成通过`socket`向客户端发送消息,推送每次编译产生的`hash`。另外如果是热更新的话,还会产出二个补丁文件,里面描述了从上一次结果到这一次结果都有哪些chunk和模块发生了变化。
    使用`let sockets = []`数组去存放当打开了多个Tab时每个Tab的`socket实例`。 + +#### 创建express应用app +```javascript +let app = new express() +``` + +#### 设置文件系统为内存文件系统 +```javascript +let fs = new MemoryFileSystem() +``` +使用`MemoryFileSystem`将`compiler`的产出文件打包到内存中。 + +#### 添加webpack-dev-middleware中间件 +```javascript +function middleware(req, res, next) { + if (req.url === '/favicon.ico') { + return res.sendStatus(404) + } + // /index.html dist/index.html + let filename = path.join(config.output.path, req.url.slice(1)) + let stat = fs.statSync(filename) + if (stat.isFile()) { // 判断是否存在这个文件,如果在的话直接把这个读出来发给浏览器 + let content = fs.readFileSync(filename) + let contentType = mime.getType(filename) + res.setHeader('Content-Type', contentType) + res.statusCode = res.statusCode || 200 + res.send(content) + } else { + return res.sendStatus(404) + } + } + app.use(middleware) +``` +使用expres启动了本地开发服务后,使用中间件去为其构造一个静态服务器,并使用了内存文件系统,使读取文件后存放到内存中,提高读写效率,最终返回生成的文件。 + +#### 启动webpack编译 +```javascript +compiler.watch({}, err => { + console.log('又一次编译任务成功完成了') + }) +``` +以监控的模式启动一次webpack编译,当编译成功之后执行回调 + +#### 创建http服务器并启动服务 +```javascript +constructor(compiler) { + // ... + this.server = require('http').createServer(app) + // ... + } + listen(port) { + this.server.listen(port, () => { + console.log(`服务器已经在${port}端口上启动了`) + }) + } +``` + +#### 使用sockjs在浏览器端和服务端之间建立一个 websocket 长连接 +```javascript +constructor(compiler) { + // ... + this.server = require('http').createServer(app) + let io = require('socket.io')(this.server) + io.on('connection', (socket) => { + sockets.push(socket) + socket.emit('hash', lastHash) + socket.emit('ok') + }) + } +``` +启动一个 websocket服务器,然后等待连接来到,连接到来之后存进sockets池
    当有文件改动,webpack重新编译时,向客户端推送`hash`和`ok`两个事件 + +### 服务端调试阶段 +感兴趣的可以根据上面[debug服务端源码](https://segmentfault.com/a/1190000020310371#debug%E6%9C%8D%E5%8A%A1%E7%AB%AF%E6%BA%90%E7%A0%81)所带的源码位置,并在浏览器的调试模式下设置断点查看每个阶段的值。 +```bash +node dev-server.js +``` +使用我们自己编译的`dev-server.js`启动服务,可看到页面可以正常展示,但还没有实现热更新。
    下面将调式客户端的源代码分析其实现流程。 + +## debug客户端源码 +![](https://cdn.nlark.com/yuque/0/2020/jpeg/186051/1583939666119-c372f841-c5ce-41ac-835c-ec0dbaa8031c.jpeg#align=left&display=inline&height=1430&originHeight=1430&originWidth=2088&size=0&status=done&style=none&width=2088)
    **现在也只需要关注上图的左侧客户端部分,右侧可以暂时忽略。下面步骤主要是debug客户端源码分析其详细思路,也给出了代码所处的具体位置,感兴趣的可以先行定位到下面的代码处设置断点,然后观察数据的变化情况。也可以先跳过阅读此步骤。**
    debug客户端源码分析其详细思路 + +1. webpack-dev-server/client端会监听到此hash消息,源代码地址[@webpack-dev-server/index.js#L54](https://github.com/webpack/webpack-dev-server/blob/v3.7.2/client-src/default/index.js#L54) +1. 客户端收到ok的消息后会执行reloadApp方法进行更新,源代码地址[index.js#L101](https://github.com/webpack/webpack-dev-server/blob/v3.7.2/client-src/default/index.js#L101) +1. 在reloadApp中会进行判断,是否支持热更新,如果支持的话发射webpackHotUpdate事件,如果不支持则直接刷新浏览器,源代码地址[reloadApp.js#L7](https://github.com/webpack/webpack-dev-server/blob/v3.7.2/client-src/default/utils/reloadApp.js#L7) +1. 在webpack/hot/dev-server.js会监听webpackHotUpdate事件,源代码地址[dev-server.js#L55](https://github.com/webpack/webpack/blob/v4.39.1/hot/dev-server.js#L55) +1. 在check方法里会调用module.hot.check方法,源代码地址[dev-server.js#L13](https://github.com/webpack/webpack/blob/v4.39.1/hot/dev-server.js#L13) +1. HotModuleReplacement.runtime请求Manifest,源代码地址[HotModuleReplacement.runtime.js#L180](https://github.com/webpack/webpack/blob/v4.39.1/lib/HotModuleReplacement.runtime.js#L180) +1. 它通过调用 JsonpMainTemplate.runtime的hotDownloadManifest方法,源代码地址[JsonpMainTemplate.runtime.js#L23](https://github.com/webpack/webpack/blob/v4.39.1/lib/web/JsonpMainTemplate.runtime.js#L23) +1. 调用JsonpMainTemplate.runtime的hotDownloadUpdateChunk方法通过JSONP请求获取到最新的模块代码,源代码地址[JsonpMainTemplate.runtime.js#L14](https://github.com/webpack/webpack/blob/v4.39.1/lib/web/JsonpMainTemplate.runtime.js#L14) +1. 补丁JS取回来后会调用JsonpMainTemplate.runtime.js的webpackHotUpdate方法,源代码地址[JsonpMainTemplate.runtime.js#L8](https://github.com/webpack/webpack/blob/v4.39.1/lib/web/JsonpMainTemplate.runtime.js#L8) +1. 然后会调用HotModuleReplacement.runtime.js的hotAddUpdateChunk方法动态更新模块代码,源代码地址[HotModuleReplacement.runtime.js#L222](https://github.com/webpack/webpack/blob/v4.39.1/lib/HotModuleReplacement.runtime.js#L222) +1. 然后调用hotApply方法进行热更新,源代码地址[HotModuleReplacement.runtime.js#L257](https://github.com/webpack/webpack/blob/v4.39.1/lib/HotModuleReplacement.runtime.js#L257)、[HotModuleReplacement.runtime.js#L278](https://github.com/webpack/webpack/blob/v4.39.1/lib/HotModuleReplacement.runtime.js#L278) + +### 客户端简易实现 +上面是我通过debug得出dev-server运行流程比较核心的几个点,下面将其[抽象整合成一个文件](https://github.com/careteenL/webpack-hmr/blob/master/src/client.js)。 + +#### webpack-dev-server/client端会监听到此hash消息 +在开发客户端功能之前,需要在`src/index.html`中引入`socket.io` +```html + +``` +下面连接socket并接受消息 +```javascript +let socket = io('/') +socket.on('connect', onConnected) +const onConnected = () => { + console.log('客户端连接成功') +} +let hotCurrentHash // lastHash 上一次 hash值 +let currentHash // 这一次的hash值 +socket.on('hash', (hash) => { + currentHash = hash +}) +``` +将服务端webpack每次编译所产生`hash`进行缓存 + +#### 客户端收到ok的消息后会执行reloadApp方法进行更新 +```javascript +socket.on('ok', () => { + reloadApp(true) +}) +``` + +#### reloadApp中判断是否支持热更新 +```javascript +// 当收到ok事件后,会重新刷新app +function reloadApp(hot) { + if (hot) { // 如果hot为true 走热更新的逻辑 + hotEmitter.emit('webpackHotUpdate') + } else { // 如果不支持热更新,则直接重新加载 + window.location.reload() + } +} +``` +在reloadApp中会进行判断,是否支持热更新,如果支持的话发射webpackHotUpdate事件,如果不支持则直接刷新浏览器。 + +#### 在webpack/hot/dev-server.js会监听webpackHotUpdate事件 +首先需要一个发布订阅去绑定事件并在合适的时机触发。 +```javascript +class Emitter { + constructor() { + this.listeners = {} + } + on(type, listener) { + this.listeners[type] = listener + } + emit(type) { + this.listeners[type] && this.listeners[type]() + } +} +let hotEmitter = new Emitter() +hotEmitter.on('webpackHotUpdate', () => { + if (!hotCurrentHash || hotCurrentHash == currentHash) { + return hotCurrentHash = currentHash + } + hotCheck() +}) +``` +会判断是否为第一次进入页面和代码是否有更新。 +上面的发布订阅较为简单,且只支持先发布后订阅功能。对于一些较为复杂的场景可能需要先订阅后发布,此时可以移步> [@careteen/event-emitter](https://github.com/careteenL/event-emitter)。其实现原理也挺简单,需要维护一个离线事件栈存放还没发布就订阅的事件,等到订阅时可以取出所有事件执行。 + +#### 在check方法里会调用module.hot.check方法 +```javascript +function hotCheck() { + hotDownloadManifest().then(update => { + let chunkIds = Object.keys(update.c) + chunkIds.forEach(chunkId => { + hotDownloadUpdateChunk(chunkId) + }) + }) +} +``` +上面也提到过webpack每次编译都会产生`hash值`、`已改动模块的json文件`、`已改动模块代码的js文件`,
    此时先使用`ajax`请求`Manifest`即服务器这一次编译相对于上一次编译改变了哪些module和chunk。
    然后再通过`jsonp`获取这些已改动的module和chunk的代码。 + +#### 调用hotDownloadManifest方法 +```javascript +function hotDownloadManifest() { + return new Promise(function (resolve) { + let request = new XMLHttpRequest() + //hot-update.json文件里存放着从上一次编译到这一次编译 取到差异 + let requestPath = '/' + hotCurrentHash + ".hot-update.json" + request.open('GET', requestPath, true) + request.onreadystatechange = function () { + if (request.readyState === 4) { + let update = JSON.parse(request.responseText) + resolve(update) + } + } + request.send() + }) +} +``` + +#### 调用hotDownloadUpdateChunk方法通过JSONP请求获取到最新的模块代码 +```javascript +function hotDownloadUpdateChunk(chunkId) { + let script = document.createElement('script') + script.charset = 'utf-8' + // /main.xxxx.hot-update.js + script.src = '/' + chunkId + "." + hotCurrentHash + ".hot-update.js" + document.head.appendChild(script) +} +``` +这里解释下为什么使用`JSONP`获取而不直接利用`socket`获取最新代码?主要是因为`JSONP`获取的代码可以直接执行。 + +#### 调用webpackHotUpdate方法 +当客户端把最新的代码拉到浏览之后 +```javascript +window.webpackHotUpdate = function (chunkId, moreModules) { + // 循环新拉来的模块 + for (let moduleId in moreModules) { + // 从模块缓存中取到老的模块定义 + let oldModule = __webpack_require__.c[moduleId] + // parents哪些模块引用这个模块 children这个模块引用了哪些模块 + // parents=['./src/index.js'] + let { + parents, + children + } = oldModule + // 更新缓存为最新代码 缓存进行更新 + let module = __webpack_require__.c[moduleId] = { + i: moduleId, + l: false, + exports: {}, + parents, + children, + hot: window.hotCreateModule(moduleId) + } + moreModules[moduleId].call(module.exports, module, module.exports, __webpack_require__) + module.l = true // 状态变为加载就是给module.exports 赋值了 + parents.forEach(parent => { + // parents=['./src/index.js'] + let parentModule = __webpack_require__.c[parent] + // _acceptedDependencies={'./src/title.js',render} + parentModule && parentModule.hot && parentModule.hot._acceptedDependencies[moduleId] && parentModule.hot._acceptedDependencies[moduleId]() + }) + hotCurrentHash = currentHash + } +} +``` + +#### hotCreateModule的实现 +实现我们可以在业务代码中定义需要热更新的模块以及回调函数,将其存放在`hot._acceptedDependencies`中。 +```javascript +window.hotCreateModule = function () { + let hot = { + _acceptedDependencies: {}, + dispose() { + // 销毁老的元素 + }, + accept: function (deps, callback) { + for (let i = 0; i < deps.length; i++) { + // hot._acceptedDependencies={'./title': render} + hot._acceptedDependencies[deps[i]] = callback + } + } + } + return hot +} +``` +然后在`webpackHotUpdate`中进行调用 +```javascript +parents.forEach(parent => { + // parents=['./src/index.js'] + let parentModule = __webpack_require__.c[parent] + // _acceptedDependencies={'./src/title.js',render} + parentModule && parentModule.hot && parentModule.hot._acceptedDependencies[moduleId] && parentModule.hot._acceptedDependencies[moduleId]() + }) +``` +最后调用hotApply方法进行热更新 + +### 客户端调试阶段 +经过上述实现了一个基本版的HMR,可更改代码保存的同时查看浏览器并非整体刷新,而是局部更新代码进而更新视图。在涉及到大量表单的需求时大大提高了开发效率。 + +## 问题 + +- 如何实现commonjs规范? +感兴趣的可前往> [debug CommonJs规范](https://github.com/careteenL/blog/blob/master/src/20181201-node/module.md)了解其实现原理。 + +- webpack实现流程以及各个生命周期的作用是什么? +webpack主要借助了> `tapable`这个库所提供的一系列同步/异步钩子函数贯穿整个生命周期。> ![](https://cdn.nlark.com/yuque/0/2020/jpeg/186051/1583939666129-b464ba02-d967-42db-a140-2b8321df9985.jpeg#align=left&display=inline&height=4244&originHeight=4244&originWidth=4436&size=0&status=done&style=none&width=4436)基于此我实现了一版简易的> [webpack](https://github.com/careteenL/webpack),源码100+行,食用时伴着注释很容易消化,感兴趣的可前往看个思路。 + +- 发布订阅的使用和实现,并且如何实现一个可先订阅后发布的机制? +上面也提到需要使用到发布订阅模式,且只支持先发布后订阅功能。对于一些较为复杂的场景可能需要先订阅后发布,此时可以移步> [@careteen/event-emitter](https://github.com/careteenL/event-emitter)。其实现原理也挺简单,需要维护一个离线事件栈存放还没发布就订阅的事件,等到订阅时可以取出所有事件执行。 + +- 为什么使用JSONP而不用socke通信获取更新过的代码? +因为通过socket通信获取的是一串字符串需要再做处理。而通过> `JSONP`获取的代码可以直接执行。 + +### 引用 + +- 珠峰架构课 +- [模块热替换 - webpack官网](https://webpack.docschina.org/api/hot-module-replacement/#src/components/Sidebar/Sidebar.jsx) diff --git "a/Cute-Article/article/86-\346\200\273\347\273\22312\347\247\215\347\247\273\345\212\250\347\253\257H5\344\270\216Hybrid\345\256\236\350\267\265\350\270\251\345\235\221\351\227\256\351\242\230.md" "b/Cute-Article/article/86-\346\200\273\347\273\22312\347\247\215\347\247\273\345\212\250\347\253\257H5\344\270\216Hybrid\345\256\236\350\267\265\350\270\251\345\235\221\351\227\256\351\242\230.md" new file mode 100644 index 00000000..6170d9f3 --- /dev/null +++ "b/Cute-Article/article/86-\346\200\273\347\273\22312\347\247\215\347\247\273\345\212\250\347\253\257H5\344\270\216Hybrid\345\256\236\350\267\265\350\270\251\345\235\221\351\227\256\351\242\230.md" @@ -0,0 +1,499 @@ +原文地址:[https://mp.weixin.qq.com/s/yLMA02zAq6ZMSaT0E2aEIA](https://mp.weixin.qq.com/s/yLMA02zAq6ZMSaT0E2aEIA) + + +# 前言 +作为一个开发了多个 H5 项目的前端工程师,在开发过程中难免会遇到一些兼容性等**爬过坑**的问题。现在我将这些问题一一汇总一下,并在后面给出**坑产生的原理**,和**现阶段常规的填坑方案**。由此来做一个阶段性的总结。 +> 常规操作哈,**点赞**后再观看呗!你的**点赞**就是我创作的动力之一! + + +# 问题 +下面列举了我遇到的一些常规问题,如有遇到其他问题请在评论区补充,之后我也会实践后加以补充,感谢!(经常更新该文) + +## 移动端 H5 相关问题汇总: + +- **1px 问题** +- **响应式布局** +- **iOS 滑动不流畅** +- **iOS 上拉边界下拉出现白色空白** +- **页面件放大或缩小不确定性行为** +- **click 点击穿透与延迟** +- **软键盘弹出将页面顶起来、收起未回落问题** +- **iPhone X 底部栏适配问题** +- **保存页面为图片和二维码问题和解决方案** +- **微信公众号 H5 分享问题** +- **H5 调用 SDK 相关问题及解决方案** +- **H5 调试相关方案与策略** + +## 移动端 H5 相关基础技术概览 +
    ![](https://cdn.nlark.com/yuque/0/2020/webp/186051/1584973292716-a8f6c379-9716-4603-b75d-f20da9de3b43.webp#align=left&display=inline&height=558&originHeight=558&originWidth=1080&size=0&status=done&style=none&width=1080) + +# 原理与实践 +之前两篇文章已经详细的论述了**1px** 问题与 **响应式布局**问题,并给出了原理和解决方案。 +> 防止丢失,**点赞收藏**后跳转至快捷通道:**1px**通道与响应式布局通道 + +接下来呢,我们看看其他问题的原理和解决方案吧。 +> 以下解决方案,均经过我测试成功,健康安全,请放下食用。由于篇幅原因,某些非核心解决方案的实现细节暂未谈论,需要自行研究。 + + +## iOS 滑动不流畅 + + +### 表现 +上下滑动页面会产生卡顿,手指离开页面,页面立即停止运动。整体表现就是滑动不流畅,没有滑动惯性。 + +### 产生原因 +**为什么 iOS 的 webview 中 滑动不流畅,它是如何定义的?**
    最终我在 `safari` 文档里面寻找到了答案(文档链接在参考资料项)。
    ![](https://cdn.nlark.com/yuque/0/2020/webp/186051/1584973292693-cf9fd82e-fb54-41dc-b6df-09a84ba5447d.webp#align=left&display=inline&height=241&originHeight=840&originWidth=1080&size=0&status=done&style=none&width=309)
    原来在 iOS 5.0 以及之后的版本,滑动有定义有两个值 `auto` 和 `touch`,默认值为 `auto`。 +```css +-webkit-overflow-scrolling: touch; /* 当手指从触摸屏上移开,会保持一段时间的滚动 */ +-webkit-overflow-scrolling: auto; /* 当手指从触摸屏上移开,滚动会立即停止 */ +``` + +### 解决方案 + +#### 1.在滚动容器上增加滚动 touch 方法 +将`-webkit-overflow-scrolling` 值设置为 `touch` +```css +.wrapper { + -webkit-overflow-scrolling: touch; +} +``` +> 设置滚动条隐藏: `.container ::-webkit-scrollbar {display: none;}` + +可能会导致使用`position:fixed;` 固定定位的元素,随着页面一起滚动 + +#### 2.设置 overflow +设置外部 `overflow` 为 `hidden`,设置内容元素 `overflow` 为 `auto`。内部元素超出 body 即产生滚动,超出的部分 body 隐藏。 +``` +body { + overflow-y: hidden; +} +.wrapper { + overflow-y: auto; +} +``` +> 两者结合使用更佳! + + +##
    + +## iOS 上拉边界下拉出现白色空白 + + +### 表现 +手指按住屏幕下拉,屏幕顶部会多出一块白色区域。手指按住屏幕上拉,底部多出一块白色区域。 + +### 产生原因 +在 iOS 中,手指按住屏幕上下拖动,会触发 `touchmove` 事件。这个事件触发的对象是整个 `webview` 容器,容器自然会被拖动,剩下的部分会成空白。 + +### 解决方案 + +#### 1. 监听事件禁止滑动 +移动端触摸事件有三个,分别定义为 +``` +1. touchstart :手指放在一个DOM元素上。 +2. touchmove :手指拖曳一个DOM元素。 +3. touchend :手指从一个DOM元素上移开。 +``` +显然我们需要控制的是 `touchmove` 事件,由此我在 W3C 文档中找到了这样一段话 +> Note that the rate at which the user agent sends touchmove events is implementation-defined, and may depend on hardware capabilities and other implementation details. +> If the preventDefault method is called on the first touchmove event of an active touch point, it should prevent any default action caused by any touchmove event associated with the same active touch point, such as scrolling. + +**`touchmove` 事件的速度是可以实现定义的,取决于硬件性能和其他实现细节**
    **`preventDefault` 方法,阻止同一触点上所有默认行为,比如滚动。**
    由此我们找到解决方案,通过监听 `touchmove`,让需要滑动的地方滑动,不需要滑动的地方禁止滑动。 +> 值得注意的是我们要过滤掉具有滚动容器的元素。 + +实现如下: +``` +document.body.addEventListener('touchmove', function(e) { + if(e._isScroller) return; + // 阻止默认事件 + e.preventDefault(); +}, { + passive: false +}); +``` + +#### 2. 滚动妥协填充空白,装饰成其他功能 +在很多时候,我们可以不去解决这个问题,换一直思路。根据场景,**我们可以将下拉作为一个功能性的操作**。
    **比如:下拉后刷新页面**
    ![](https://cdn.nlark.com/yuque/0/2020/webp/186051/1584973292780-06c3559e-c021-4d93-a838-7037a10062cd.webp#align=left&display=inline&height=473&originHeight=1326&originWidth=754&size=0&status=done&style=none&width=269) + +## 页面放大或缩小不确定性行为 + + +### 表现 +双击或者双指张开手指页面元素,页面会放大或缩小。 + +### 产生原因 +HTML 本身会产生放大或缩小的行为,比如在 PC 浏览器上,可以自由控制页面的放大缩小。但是在移动端,我们是不需要这个行为的。所以,我们需要禁止该不确定性行为,来提升用户体验。 + +### 原理与解决方案 +HTML `meta` 元标签标准中有个 中 `viewport` 属性,用来控制页面的缩放,一般用于移动端。如下图 MDN 中介绍
    ![](https://cdn.nlark.com/yuque/0/2020/webp/186051/1584973292670-0ea4c1d6-5493-44ce-b397-d166244a6110.webp#align=left&display=inline&height=541&originHeight=1003&originWidth=1080&size=0&status=done&style=none&width=582)
    移动端常规写法 +```html + +``` +因此我们可以设置 `maximum-scale`、`minimum-scale` 与 `user-scalable=no` 用来避免这个问题
    + +```html + +``` + +##
    + +## click 点击事件延时与穿透 + + +### 表现 +监听元素 `click` 事件,点击元素触发时间延迟约 `300ms`。
    点击蒙层,蒙层消失后,下层元素点击触发。 + +### 产生原因 + +#### 为什么会产生 click 延时? +iOS 中的 safari,为了实现双击缩放操作,在单击 300ms 之后,如果未进行第二次点击,则执行 `click` 单击操作。也就是说来判断用户行为是否为双击产生的。但是,在 App 中,无论是否需要双击缩放这种行为,`click` 单击都会产生 300ms 延迟。 + +#### 为什么会产生 click 点击穿透? +双层元素叠加时,在上层元素上绑定 `touch` 事件,下层元素绑定 `click` 事件。由于 `click` 发生在 `touch` 之后,点击上层元素,元素消失,下层元素会触发 `click` 事件,由此产生了点击穿透的效果。 + +### 原理与解决方案 + +#### 解决方案一:使用 touchstart 替换 click +前面已经介绍了,移动设备不仅支持点击,还支持几个触摸事件。那么我们现在基本思路就是用 `touch` 事件代替`click` 事件。
    将 `click` 替换成 `touchstart` 不仅解决了 `click` 事件都延时问题,还解决了穿透问题。因为穿透问题是在 `touch` 和 `click` 混用时产生。
    在原生中使用 +```javascript +el.addEventListener("touchstart", () => { console.log("ok"); }, false); +``` + +
    在 vue 中使用 +```html + +``` + +
    开源解决方案中,也是既提供了 `click` 事件,又提供了`touchstart` 事件。如 vant 中的 `button` 组件
    ![](https://cdn.nlark.com/yuque/0/2020/webp/186051/1584973292676-2ee3bed1-2ffb-48ec-94f8-271324dfef3b.webp#align=left&display=inline&height=170&originHeight=263&originWidth=1080&size=0&status=done&style=none&width=699)
    **那么,是否可以将 `click` 事件全部替换成 `touchstart` 呢?为什么开源框架还会给出 `click` 事件呢?**
    我们想象一种情景,同时需要点击和滑动的场景下。如果将 `click` 替换成 `touchstart` 会怎样? +> 事件触发顺序: `touchstart`, `touchmove`, `touchend`, `click`。 + +很容易想象,在我需要`touchmove`滑动时候,优先触发了`touchstart`的点击事件,是不是已经产生了冲突呢?
    所以呢,在具有滚动的情况下,还是建议使用 `click` 处理。
    在接下来的`fastclick`开源库中也做了如下处理。针对 `touchstart` 和 `touchend`,截取了部分源码。 +```javascript +// If the target element is a child of a scrollable layer (using -webkit-overflow-scrolling: touch) and: +// 1) the user does a fling scroll on the scrollable layer +// 2) the user stops the fling scroll with another tap +// then the event.target of the last 'touchend' event will be the element that was under the user's finger +// when the fling scroll was started, causing FastClick to send a click event to that layer - unless a check +// is made to ensure that a parent layer was not scrolled before sending a synthetic click (issue #42). +this.updateScrollParent(targetElement); +``` +```javascript +// Don't send a synthetic click event if the target element is contained within a parent layer that was scrolled +// and this tap is being used to stop the scrolling (usually initiated by a fling - issue #42). +scrollParent = targetElement.fastClickScrollParent; +if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) { + return true; +} +``` +主要目的就是,在使用 `touchstart` 合成 `click` 事件时,保证其不在滚动的父元素之下。 + +####
    + +#### 解决方案二:使用 fastclick 库 +使用 `npm/yarn` 安装后使用 +``` +import FastClick from 'fastclick'; +FastClick.attach(document.body, options); +``` +同样,使用`fastclick`库后,`click` 延时和穿透问题都没了
    按照我的惯例,只要涉及开源库,那么我们一定要去了解它实现的原理。主要是将现有的原生事件集合封装合成一个兼容性较强的事件集合。
    fastclick源码 核心代码不长, 1000 行不到。有兴趣可以了解一下! + +##
    + +## 软键盘将页面顶起来、收起未回落问题 + + +### 表现 +Android 手机中,点击 `input` 框时,键盘弹出,将页面顶起来,导致页面样式错乱。
    移开焦点时,键盘收起,键盘区域空白,未回落。 + +### 产生原因 +我们在app 布局中会有个固定的底部。安卓一些版本中,输入弹窗出来,会将解压 `absolute` 和 `fixed` 定位的元素。导致可视区域变小,布局错乱。 + +### 原理与解决方案 +软键盘将页面顶起来的解决方案,主要是通过监听页面高度变化,强制恢复成弹出前的高度。 +```javascript +// 记录原有的视口高度 +const originalHeight = document.body.clientHeight || document.documentElement.clientHeight; +window.onresize = function(){ + var resizeHeight = document.documentElement.clientHeight || document.body.clientHeight; + if(resizeHeight < originalHeight ){ + // 恢复内容区域高度 + // const container = document.getElementById("container") + // 例如 container.style.height = originalHeight; + } +} +``` +键盘不能回落问题出现在 iOS 12+ 和 wechat 6.7.4+ 中,而在微信 H5 开发中是比较常见的 Bug。
    兼容原理,1.判断版本类型 2.更改滚动的可视区域 +```javascript +const isWechat = window.navigator.userAgent.match(/MicroMessenger\/([\d\.]+)/i); +if (!isWechat) return; +const wechatVersion = wechatInfo[1]; +const version = (navigator.appVersion).match(/OS (\d+)_(\d+)_?(\d+)?/); + + // 如果设备类型为iOS 12+ 和wechat 6.7.4+,恢复成原来的视口 +if (+wechatVersion.replace(/\./g, '') >= 674 && +version[1] >= 12) { + window.scrollTo(0, Math.max(document.body.clientHeight, document.documentElement.clientHeight)); +} +``` +> `window.scrollTo(x-coord, y-coord)`,其中`window.scrollTo(0, clientHeight)`恢复成原来的视口 + + +##
    + +## iPhone X系列安全区域适配问题 + + +### 表现 +头部刘海两侧区域或者底部区域,出现刘海遮挡文字,或者呈现黑底或白底空白区域。 + +### 产生原因 +iPhone X 以及它以上的系列,都采用**刘海屏设计**和**全面屏手势**。头部、底部、侧边都需要做特殊处理。才能适配 iPhone X 的特殊情况。 + +### 解决方案 +**设置安全区域,填充危险区域,危险区域不做操作和内容展示。** +> 危险区域指头部不规则区域,底部横条区域,左右触发区域。 + +![](https://cdn.nlark.com/yuque/0/2020/webp/186051/1584973292709-970ada98-9c73-41ed-ae93-0866ceb7f069.webp#align=left&display=inline&height=375&originHeight=574&originWidth=1080&size=0&status=done&style=none&width=710)
    具体操作为:`viewport-fit` `meta` 标签设置为 `cover`,获取所有区域填充。判断设备是否属于 iPhone X,给头部底部增加**适配层** +> `viewport-fit` 有 3 个值分别为: +> - `auto`:此值不影响初始布局视图端口,并且整个web页面都是可查看的。 +> - `contain`:视图端口按比例缩放,以适合显示内嵌的最大矩形。 +> - `cover`:视图端口被缩放以填充设备显示。强烈建议使用 `safe area inset` 变量,以确保重要内容不会出现在显示之外。 + + +####
    + +#### 设置 viewport-fit 为 `cover` +```html + +``` + +####
    + +#### 增加适配层 +使用 `safe area inset` 变量 +```css +/* 适配 iPhone X 顶部填充*/ +@supports (top: env(safe-area-inset-top)){ + body, + .header{ + padding-top: constant(safe-area-inset-top, 40px); + padding-top: env(safe-area-inset-top, 40px); + padding-top: var(safe-area-inset-top, 40px); + } +} +/* 判断iPhoneX 将 footer 的 padding-bottom 填充到最底部 */ +@supports (bottom: env(safe-area-inset-bottom)){ + body, + .footer{ + padding-bottom: constant(safe-area-inset-bottom, 20px); + padding-bottom: env(safe-area-inset-bottom, 20px); + padding-top: var(safe-area-inset-bottom, 20px); + } +} +``` +> `safe-area-inset-top`, `safe-area-inset-right`, `safe-area-inset-bottom`, `safe-area-inset-left` `safe-area-inset-*`由四个定义了视口边缘内矩形的 `top`, `right`, `bottom` 和 `left` 的环境变量组成,这样可以安全地放入内容,而不会有被非矩形的显示切断的风险。对于矩形视口,例如普通的笔记本电脑显示器,其值等于零。对于非矩形显示器(如圆形表盘,`iPhoneX` 屏幕),在用户代理设置的四个值形成的矩形内,所有内容均可见。 + +其中 `env()` 用法为 `env( , ? )`,第一个参数为自定义的区域,第二个为备用值。
    其中 `var()` 用法为 `var( , ? )`,作用是在 `env()` 不生效的情况下,给出一个备用值。
    `constant()` 被 `css` 2017-2018 年为草稿阶段,是否已被标准化未知。而其他iOS 浏览器版本中是否有此函数未知,作为兼容处理而添加进去。
    详情请查看文章末尾的参考资料。 + +#### 兼容性 +![](https://cdn.nlark.com/yuque/0/2020/webp/186051/1584973292674-a2dab6c3-2d03-4b0c-be10-4e4a1e95a2bb.webp#align=left&display=inline&height=208&originHeight=303&originWidth=1080&size=0&status=done&style=none&width=746) + +##
    + +## 页面生成为图片和二维码问题 + + +### 表现 +在工作中有需要将页面生成图片或者二维码的需求。可能我们第一想到的,交给后端来生成更简单。但是这样我们需要把页面代码全部传给后端,网络性能消耗太大。 + +### 解决方案 + +#### 生成二维码 +使用 QRCode 生成二维码 +```javascript +import QRCode from 'qrcode'; +// 使用 async 生成图片 +const options = {}; +const url = window.location.href; +async url => { + try { + console.log(await QRCode.toDataURL(url, options)) + } catch (err) { + console.error(err); + } +} +``` +将 `await QRCode.toDataURL(url, options)` 赋值给 图片 `url` 即可 + +#### 生成图片 +主要是使用 `htmlToCanvas` 生成 `canvas` 画布 +```javascript +import html2canvas from 'html2canvas'; +html2canvas(document.body).then(function(canvas) { + document.body.appendChild(canvas); +}); +``` +但是不单单在此处就完了,由于是 `canvas` 的原因。移动端生成出来的图片比较模糊。
    我们使用一个新的 `canvas` 方法多倍生成,放入一倍容器里面,达到更加清晰的效果,通过超链接下载图片 **下载文件简单实现,更完整的实现方式之后更新** +```javascript +const scaleSize = 2; +const newCanvas = document.createElement("canvas"); +const target = document.querySelector('div'); +const width = parseInt(window.getComputedStyle(target).width); +const height = parseInt(window.getComputedStyle(target).height); +newCanvas.width = width * scaleSize; +newCanvas.height = widthh * scaleSize; +newCanvas.style.width = width + "px"; +newCanvas.style.height =width + "px"; +const context = newCanvas.getContext("2d"); +context.scale(scaleSize, scaleSize); +html2canvas(document.querySelector('.demo'), { canvas: newCanvas }).then(function(canvas) { + // 简单的通过超链接设置下载功能 + document.querySelector(".btn").setAttribute('href', canvas.toDataURL()); +} +``` +> 根据需要设置 `scaleSize` 大小 + + +##
    + +## 微信公众号分享问题 + + +### 表现 +在微信公众号 H5 开发中,页面内部点击分享按钮调用 SDK,方法不生效。 + +### 解决方案 + +#### 解决方法:添加一层蒙层,做分享引导。 +因为页面内部点击分享按钮无法直接调用,而分享功能需要点击右上角更多来操作。
    然后用户可能不知道通过右上角小标里面的功能分享。又想引导用户分享,这时应该怎么做呢?
    技术无法实现的,从产品出发。
    ![](https://cdn.nlark.com/yuque/0/2020/webp/186051/1584973292687-c9f1db44-c5b1-44bc-a76e-36e4376861ae.webp#align=left&display=inline&height=492&originHeight=574&originWidth=328&size=0&status=done&style=none&width=281)
    **如果技术上实现复杂,或者直接不能实现。不要强行钻牛角尖哦,学会怼产品,也是程序员必备的能力之一。** + +##
    + +## H5 调用 SDK 相关解决方案 + + +### 产生原因 +在 Hybrid App 中使用 H5 是最常见的不过了,刚接触的,肯定会很生疏模糊。不知道 H5 和 Hybrid 是怎么交互的。怎样同时支持 iOS 和 Android 呢?现在来谈谈 Hybrid 技术要点,**原生与 H5 的通信**。 + +### 解决方案 +![](https://cdn.nlark.com/yuque/0/2020/webp/186051/1584973292800-3d01c42f-0012-4518-af08-c2d6df5c6d19.webp#align=left&display=inline&height=36&originHeight=278&originWidth=1080&size=0&status=done&style=none&width=140)使用 `DSBridge` 同时支持 iOS 与 Android +> 文档见参考资料 + + +#### SDK小组 提供方法 + +1. 注册方法 `bridge.register` +```javascript +bridge.register('enterApp', function() { + broadcast.emit('ENTER_APP') +}) +``` + +2. 回调方法 `bridge.call` +```javascript +export const getSDKVersion = () => bridge.call('BLT.getSDKVersion') +``` + +#### 事件监听与触发法 +```javascript +const broadcast = { + on: function(name, fn, pluralable) { + this._on(name, fn, pluralable, false) + }, + once: function(name, fn, pluralable) { + this._on(name, fn, pluralable, true) + }, + _on: function(name, fn, pluralable, once) { + let eventData = broadcast.data + let fnObj = { fn: fn, once: once } + if (pluralable && Object.prototype.hasOwnProperty.call(eventData, 'name')) { + eventData[name].push(fnObj) + } else { + eventData[name] = [fnObj] + } + return this + }, + emit: function(name, data, thisArg) { + let fn, fnList, i, len + thisArg = thisArg || null + fnList = broadcast.data[name] || [] + for (i = 0, len = fnList.length; i < len; i++) { + fn = fnList[i].fn + fn.apply(thisArg, [data, name]) + if (fnList[i].once) { + fnList.splice(i, 1) + i-- + len-- + } + } + return this + }, + data: {} +} +export default broadcast +``` + +#### 踩坑注意 +方法调用前,一定要判断 SDK 是否提供该方法 如果 Android 提供该方法,iOS 上调用就会出现一个方法**调用失败等弹窗**。怎么解决呢?
    提供一个判断是否 Android、iOS。根据设备进行判断 +```javascript +export const hasNativeMethod = (name) => + return bridge.hasNativeMethod('BYJ.' + name) +} +export const getSDKVersion = function() { + if (hasNativeMethod('getSDKVersion')) { + bridge.call('BYJ.getSDKVersion') + } +} +``` +> 同一功能需要iOS,Android方法名相同,这样更好处理哦 + + +##
    + +## H5 调试相关方案策略 + + +### 表现 +调试代码一般就是为了**查看数据**和**定位 bug**。分为两种场景,一种是开发和测试时调试,一种是生产环境上调试。 +> 为什么有生产环境上调试呢?有些时候测试环境上没法复现这个 bug,测试环境和生产环境不一致,此时就需要紧急生产调试。 + +在 PC 端开发时,我们可以直接掉出控制台,使用浏览器提供的工具操作devtools或者查看日志。但是在 App 内部我们怎么做呢? + +### 原理与解决方案 + +#### 1. `vconsole` 控制台插件 +使用方法也很简单 +```javascript +import Vconsole from 'vconsole' +new Vconsole() +``` +![](https://cdn.nlark.com/yuque/0/2020/webp/186051/1584973292698-5c2c8fc5-cebf-4773-b0d3-1f424f1c58a9.webp#align=left&display=inline&height=557&originHeight=810&originWidth=1080&size=0&status=done&style=none&width=742)
    有兴趣看看它实现的基本原理,我们关注的点应该在 **vsconsole 如何打印出我们所有 log 的** 腾讯开源vconsole
    上述方法仅用于开发和测试。**生产环境中不允许出现,所以,使用时需要对环境进行判断。** +```javascript +import Vconsole from 'vconsole' +if (process.env.NODE_ENV !== 'production') { + new Vconsole() +} +``` + +#### 2. 代理 + spy-debugger +操作稍微有点麻烦,不过我会详细写出,大致分为 4 个步骤 + +1. 安装插件(全局安装) +``` +sudo npm install spy-debugger -g +``` + +2. 手机与电脑置于同一 wifi 下,手机设置代理 + +设置手机的 HTTP 代理,代理 IP 地址设置为 PC 的 IP 地址,端口为`spy-debugger`的启动端口 +> spy-debugger 默认端口:9888 +> Android :设置 - WLAN - 长按选中网络 - 修改网络 - 高级 - 代理设置 - 手动 +> IOS :设置 - Wi-Fi - 选中网络, 点击感叹号, HTTP 代理手动 + +3. 手机打开浏览器或者 app 中 H5 页面 +3. 打开桌面日志网站进行调试,点击 npm 控制台监听地址。查看抓包和 H5 页面结构 + +**这种方式可以调试生成环境的页面,不需要修改代码,可以应付大多数调试需求** + +# 总结 +本篇文章耗费作者一个多星期的业余时间,存手工敲打 4500 +字,同时收集,整理之前很多坑点和边写作边**思考**和**总结**。如果能对你有帮助,便是它最大的价值。都看到这里还不**点赞**,太过不去啦!😄
    由于技术水平有限,文章中如有错误地方,请在评论区指出,感谢!
    关于移动端 H5 的文章告一段落了,之后实践中遇到的问题都将在此文中更新。另外准备做一个移动端 H5 开源项目。多关注下 我的github动态哦!
    之后,应该回去研究下**开源和面试题相关内容**分享,想持续了解更多,不妨**点赞**和**关注**呗。 diff --git "a/9-\345\270\270\347\224\250\344\270\232\345\212\241\346\250\241\345\235\227\344\273\243\347\240\201\346\225\264\347\220\206.md" "b/Cute-Article/article/9-\345\270\270\347\224\250\344\270\232\345\212\241\346\250\241\345\235\227\344\273\243\347\240\201\346\225\264\347\220\206.md" similarity index 94% rename from "9-\345\270\270\347\224\250\344\270\232\345\212\241\346\250\241\345\235\227\344\273\243\347\240\201\346\225\264\347\220\206.md" rename to "Cute-Article/article/9-\345\270\270\347\224\250\344\270\232\345\212\241\346\250\241\345\235\227\344\273\243\347\240\201\346\225\264\347\220\206.md" index 0338a5fa..2abb1525 100644 --- "a/9-\345\270\270\347\224\250\344\270\232\345\212\241\346\250\241\345\235\227\344\273\243\347\240\201\346\225\264\347\220\206.md" +++ "b/Cute-Article/article/9-\345\270\270\347\224\250\344\270\232\345\212\241\346\250\241\345\235\227\344\273\243\347\240\201\346\225\264\347\220\206.md" @@ -732,4 +732,50 @@ if(Object.isArray(someobj)){} if(someobj && Object.keys(someobj).length) ``` -## (更新时间2018.04.24)持续更新中··· \ No newline at end of file +## 36、生成指定位数的随机密码 +```js +// {length} 指定返回随机密码的长度 +function CreatePassword(length) { + var password = ''; + var charlist = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + for(var i = 0; i < length; i++) { + password += charlist.charAt(parseInt(Math.random()*charlist.length)); + } + return password; +} +``` + +## 37、关于sort()方法 +```js +let a = [10, 1, 3, 20,25,8]; +// 1-默认情况 大的数值会在前面 +a.sort(); // [10, 1, 3, 20,25,8]; +// 2-升序 +a.sort((a,b)=>a-b); // [1, 3, 8, 10, 20, 25] +// 3-降序 +a.sort((a,b)=>b-a); // [25, 20, 10, 8, 3, 1] +``` +## 38、设置时间 n分钟之前 +同理还可以设置n小时等 +```js +// 设置两分钟之前 +const d = new Date(); +const n = 2; // 分钟数 +d.setMinutes(d.getMinutes()- n) +``` + +## 39、sleep函数 +``` +// {numberMillis} 睡眠时间 +export function sleep(numberMillis) { + let now = new Date(); + let exitTime = now.getTime() + numberMillis; + while (true) { + now = new Date(); + if (now.getTime() > exitTime) + return; + } +} +``` + +## (更新时间2018.07.12)持续更新中··· \ No newline at end of file diff --git "a/\343\200\220\345\270\270\347\224\250\346\212\200\346\234\257\343\200\221Mac\347\273\210\347\253\257\345\221\275\344\273\244\345\244\247\345\205\250.md" "b/Cute-Article/article/\343\200\220\345\270\270\347\224\250\346\212\200\346\234\257\343\200\221Mac\347\273\210\347\253\257\345\221\275\344\273\244\345\244\247\345\205\250.md" similarity index 100% rename from "\343\200\220\345\270\270\347\224\250\346\212\200\346\234\257\343\200\221Mac\347\273\210\347\253\257\345\221\275\344\273\244\345\244\247\345\205\250.md" rename to "Cute-Article/article/\343\200\220\345\270\270\347\224\250\346\212\200\346\234\257\343\200\221Mac\347\273\210\347\253\257\345\221\275\344\273\244\345\244\247\345\205\250.md" diff --git "a/Cute-Article/xmind/\346\267\261\345\205\245\347\220\206\350\247\243\346\265\217\350\247\210\345\231\250\347\232\204\347\274\223\345\255\230\346\234\272\345\210\266.png" "b/Cute-Article/xmind/\346\267\261\345\205\245\347\220\206\350\247\243\346\265\217\350\247\210\345\231\250\347\232\204\347\274\223\345\255\230\346\234\272\345\210\266.png" new file mode 100644 index 00000000..a1f44ff1 Binary files /dev/null and "b/Cute-Article/xmind/\346\267\261\345\205\245\347\220\206\350\247\243\346\265\217\350\247\210\345\231\250\347\232\204\347\274\223\345\255\230\346\234\272\345\210\266.png" differ diff --git a/Cute-CLI/CACDemo/README-CN.md b/Cute-CLI/CACDemo/README-CN.md new file mode 100644 index 00000000..ebecc76c --- /dev/null +++ b/Cute-CLI/CACDemo/README-CN.md @@ -0,0 +1,540 @@ +2017-07-26 9 27 05 + +[![NPM version](https://img.shields.io/npm/v/cac.svg?style=flat)](https://npmjs.com/package/cac) [![NPM downloads](https://img.shields.io/npm/dm/cac.svg?style=flat)](https://npmjs.com/package/cac) [![CircleCI](https://circleci.com/gh/cacjs/cac/tree/master.svg?style=shield)](https://circleci.com/gh/cacjs/cac/tree/master) [![Codecov](https://badgen.net/codecov/c/github/cacjs/cac/master)](https://codecov.io/gh/cacjs/cac) [![donate](https://img.shields.io/badge/$-donate-ff69b4.svg?maxAge=2592000&style=flat)](https://github.com/egoist/donate) [![chat](https://img.shields.io/badge/chat-on%20discord-7289DA.svg?style=flat)](https://chat.egoist.moe) [![install size](https://badgen.net/packagephobia/install/cac)](https://packagephobia.now.sh/result?p=cac) + +## 介绍 + +CAC 是一个 JavaScript 库,用于构建 CLI 应用。 + +## 特征 + +- **超轻量级**:无依赖,只有一个文件。 +- **易于学习**:你需要学习 4 个 API,即可构建简单的 CLI,包括:`cli.option`、`cli.version`、`cli.help`、`cli.parse`。 +- **且如此强大**:启用默认命令、类似 git 子命令、验证必填参数和选项、可变参数、点嵌套选项、自动生成帮助消息等功能。 +- **开发人员友好**:使用 TypeScript 开发。 + +## 目录 + + + +- [介绍](#介绍) +- [特征](#特征) +- [目录](#目录) +- [安装](#安装) +- [用法](#用法) + - [快速上手 - 简单解析](#快速上手---简单解析) + - [显示帮助消息和版本号](#显示帮助消息和版本号) + - [命令的特定选项](#命令的特定选项) + - [选项名称中的破折号](#选项名称中的破折号) + - [括号](#括号) + - [无效选项](#无效选项) + - [可变参数](#可变参数) + - [点嵌套选项](#点嵌套选项) + - [默认命令](#默认命令) + - [提供一个数组作为选项值](#提供一个数组作为选项值) + - [错误处理](#错误处理) + - [使用 TypeScript](#使用-typescript) + - [使用 Deno](#使用-deno) +- [使用 CAC 的项目](#使用-cac-的项目) +- [参考](#参考) + - [CLI 实例](#cli-实例) + - [cac(name?)](#cacname) + - [cli.command(name, description, config?)](#clicommandname-description-config) + - [cli.option(name, description, config?)](#clioptionname-description-config) + - [cli.parse(argv?)](#cliparseargv) + - [cli.version(version, customFlags?)](#cliversionversion-customflags) + - [cli.help(callback?)](#clihelpcallback) + - [cli.outputHelp()](#clioutputhelp) + - [cli.usage(text)](#cliusagetext) + - [Command 实例](#command-实例) + - [command.option()](#commandoption) + - [command.action(callback)](#commandactioncallback) + - [command.alias(name)](#commandaliasname) + - [command.allowUnknownOptions()](#commandallowunknownoptions) + - [command.example(example)](#commandexampleexample) + - [command.usage(text)](#commandusagetext) + - [事件](#事件) +- [FAQ](#faq) + - [这个名字是如何书写和发音的?](#这个名字是如何书写和发音的) + - [为什么不使用 Commander.js?](#为什么不使用-commanderjs) +- [项目统计](#项目统计) +- [贡献](#贡献) +- [作者](#作者) + + + +## 安装 + +```bash +yarn add cac +``` + +## 用法 + +### 快速上手 - 简单解析 + +使用 CAC 作为简单的参数解析器: + +```js +// examples/basic-usage.js +const cli = require('cac')() + +cli.option('--type ', 'Choose a project type', { + default: 'node', +}) + +const parsed = cli.parse() + +console.log(JSON.stringify(parsed, null, 2)) +``` + +2018-11-26 12 28 03 + +### 显示帮助消息和版本号 + +```js +// examples/help.js +const cli = require('cac')() + +cli.option('--type [type]', 'Choose a project type', { + default: 'node', +}) +cli.option('--name ', 'Provide your name') + +cli.command('lint [...files]', 'Lint files').action((files, options) => { + console.log(files, options) +}) + +// 当使用 `-h` 或 `--help` 参数时显示帮助信息 +cli.help() +// 当使用 `-v` 或 `--version` 参数时显示版本号,也可以用于显示帮助信息 +cli.version('0.0.0') + +cli.parse() +``` + +2018-11-25 8 21 14 + +### 命令的特定选项 + +您可以将选特定项附加到命令中。 + +```js +const cli = require('cac')() + +cli + .command('rm ', 'Remove a dir') + .option('-r, --recursive', 'Remove recursively') + .action((dir, options) => { + console.log('remove ' + dir + (options.recursive ? ' recursively' : '')) + }) + +cli.help() + +cli.parse() +``` + +上面配置的选项,会在使用命令时生效。任何位置选项都将视为错误进行提示。但是,如果是基础命令没有定义其行为,则不会验证该选项,如果您真的想使用未知选项,可以使用 [`command.allowUnknownOptions`](#commandallowunknownoptions)。 + +command options + +### 选项名称中的破折号 + +使用 kebab-case 命名的选项,在使用时需要改为 camelCase 方式: + +```js +cli + .command('dev', 'Start dev server') + .option('--clear-screen', 'Clear screen') + .action((options) => { + console.log(options.clearScreen) + }) +``` + +事实上 `--clear-screen` 和 `--clearScreen` 都映射成 `options.clearScreen`。 + +### 括号 + +在**命令名称**中使用**括号**时,**尖括号表示必选参数**,而**方括号表示可选参数**。 + +在**选项名称**中使用**括号**时,**尖括号表示必须传入一个字符串/数字值**,而**方括号表示该值也可以是true**。 + + +```js +const cli = require('cac')() + +cli + .command('deploy ', 'Deploy a folder to AWS') + .option('--scale [level]', 'Scaling level') + .action((folder, options) => { + // ... + }) + +cli + .command('build [project]', 'Build a project') + .option('--out ', 'Output directory') + .action((folder, options) => { + // ... + }) + +cli.parse() +``` + +### 无效选项 + +如果需要配置选项为 false,您需要手动指定一个无效选项: + +```js +cli + .command('build [project]', 'Build a project') + .option('--no-config', 'Disable config file') + .option('--config ', 'Use a custom config file') +``` +这将让 CAC 将默认值设置 `config` 为 true,您可以使用 `--no-config` 标志将其设置为 `false`。 + +### 可变参数 + +命令的最后一个参数可以是可变参数,并且只能是最后一个参数。要使参数可变,您必须添加 `...` 到参数名称的开头,就像 JavaScript 中的 rest 运算符一样。下面是一个例子: + +```js +const cli = require('cac')() + +cli + .command('build [...otherFiles]', 'Build your app') + .option('--foo', 'Foo option') + .action((entry, otherFiles, options) => { + console.log(entry) + console.log(otherFiles) + console.log(options) + }) + +cli.help() + +cli.parse() +``` + +2018-11-25 8 25 30 + +### 点嵌套选项 + +点嵌套选项将合并为一个选项。 + +```js +const cli = require('cac')() + +cli + .command('build', 'desc') + .option('--env ', 'Set envs') + .example('--env.API_SECRET xxx') + .action((options) => { + console.log(options) + }) + +cli.help() + +cli.parse() +``` + +2018-11-25 9 37 53 + +### 默认命令 + +注册一个将在没有其他命令匹配时使用的命令。 + +```js +const cli = require('cac')() + +cli + // 简单地省略命令名,只使用括号 + .command('[...files]', 'Build files') + .option('--minimize', 'Minimize output') + .action((files, options) => { + console.log(files) + console.log(options.minimize) + }) + +cli.parse() +``` + +### 提供一个数组作为选项值 + +```bash +node cli.js --include project-a +# 解析结果: +# { include: 'project-a' } + +node cli.js --include project-a --include project-b +# 解析结果: +# { include: ['project-a', 'project-b'] } +``` + +### 错误处理 + +全局处理命令错误: + +```js +try { + cli.parse(process.argv, { run: false }) + // 当你的命令操作返回一个 Promise,你只需要添加 await + await cli.runMatchedCommand() +} catch (error) { + // 在这里处理错误.. + // 例如 + // console.error(error.stack) + // process.exit(1) +} +``` + +### 使用 TypeScript + +首先,您需要 `@types/node` 在项目中作为开发依赖项安装: + +```bash +yarn add @types/node --dev +``` + +然后一切都开箱即用: + +```js +const { cac } = require('cac') +// OR ES modules +import { cac } from 'cac' +``` + +### 使用 Deno + +```ts +import { cac } from 'https://unpkg.com/cac/mod.ts' + +const cli = cac('my-program') +``` + +## 使用 CAC 的项目 + +这些项目使用了 **CAC**: + +- [VuePress](https://github.com/vuejs/vuepress): :memo: Minimalistic Vue-powered static site generator. +- [SAO](https://github.com/egoist/sao): ⚔️ Futuristic scaffolding tool. +- [DocPad](https://github.com/docpad/docpad): 🏹 Powerful Static Site Generator. +- [Poi](https://github.com/egoist/poi): ⚡️ Delightful web development. +- [bili](https://github.com/egoist/bili): 🥂 Schweizer Armeemesser for bundling JavaScript libraries. +- [Lad](https://github.com/ladjs/lad): 👦 Lad scaffolds a Koa webapp and API framework for Node.js. +- [Lass](https://github.com/lassjs/lass): 💁🏻 Scaffold a modern package boilerplate for Node.js. +- [Foy](https://github.com/zaaack/foy): 🏗 A lightweight and modern task runner and build tool for general purpose. +- [Vuese](https://github.com/vuese/vuese): 🤗 One-stop solution for vue component documentation. +- [NUT](https://github.com/nut-project/nut): 🌰 A framework born for microfrontends +- Feel free to add yours here... + +## 参考 + +💁 如果您想要更深入的 API 参考, 请查看源代码中 [生成文档](https://cac-api-doc.egoist.sh/classes/_cac_.cac.html) 。 + +下面简单介绍: + +### CLI 实例 + +通过调用 `cac` 函数创建 CLI 实例: + +```js +const cac = require('cac') +const cli = cac() +``` + +#### cac(name?) + +创建一个 CLI 实例,可设置可选的 `name` 属性来指定 CLI 名称,这将用于在帮助和版本消息中显示的 CLI 名称。当未设置时,默认使用 `argv[1]`。 + +#### cli.command(name, description, config?) + +- 类型: `(name: string, description: string) => Command` + +创建命令实例。 + +其第三个参数 `config` 为可选参数,值为: + +- `config.allowUnknownOptions`: `boolean` 在此命令中允许未知选项。 +- `config.ignoreOptionDefaultValue`: `boolean` 不要在解析的选项中使用选项的默认值,只在帮助消息中显示它们。 + +#### cli.option(name, description, config?) + +- 类型: `(name: string, description: string, config?: OptionConfig) => CLI` + +添加全局选项。 + +其第三个参数 `config` 为可选参数,值为: + +- `config.default`: 选项的默认值。 +- `config.type`: `any[]`,当设置为 `[]` 时,选项值返回一个数组类型。还可以使用如 `[String]` 之类的转换函数,将使用 `String` 调用选项值。 + +#### cli.parse(argv?) + +- 类型: `(argv = process.argv) => ParsedArgv` + +```ts +interface ParsedArgv { + args: string[] + options: { + [k: string]: any + } +} +``` + +当这个方法被调用时,`cli.rawArgs`、`cli.args`、`cli.options`、`cli.matchedCommand` 也将可用。 + +#### cli.version(version, customFlags?) + +- 类型: `(version: string, customFlags = '-v, --version') => CLI` + +出现标志 `-v, --version` 时输出版本号。 + +#### cli.help(callback?) + +- 类型: `(callback?: HelpCallback) => CLI` + +出现标志 `-h, --help` 时输出帮助消息。 + +可选参数 `callback` 允许在显示之前对帮助文本进行后处理: + +```ts +type HelpCallback = (sections: HelpSection[]) => void + +interface HelpSection { + title?: string + body: string +} +``` + +#### cli.outputHelp() + +- 类型: `() => CLI` + +输出帮助信息。 + +#### cli.usage(text) + +- 类型: `(text: string) => CLI` + +添加全局使用说明,并且这不会被子命令使用。 + +### Command 实例 + +通过调用 `cli.command` 方法创建 Command 实例: + +```js +const command = cli.command('build [...files]', 'Build given files') +``` + +#### command.option() + +和 `cli.option` 基本相同,但 `command.option()` 将该选项添加到特定命令中。 + +#### command.action(callback) + +- 类型: `(callback: ActionCallback) => Command` + +当命令匹配用户输入时,使用回调函数作为命令操作。 + +```ts +type ActionCallback = ( + // Parsed CLI args + // The last arg will be an array if it's a variadic argument + ...args: string | string[] | number | number[] + // Parsed CLI options + options: Options +) => any + +interface Options { + [k: string]: any +} +``` + +#### command.alias(name) + +- 类型: `(name: string) => Command` + +为该命令添加别名,此处 `name` 不能包含括号。 + +#### command.allowUnknownOptions() + +- 类型: `() => Command` + +在此命令中允许未知选项,默认情况下,当使用未知选项时,CAC 将记录错误。 + +#### command.example(example) + +- 类型: `(example: CommandExample) => Command` + +在帮助消息末尾显示一个使用示例。 + +```ts +type CommandExample = ((name: string) => string) | string +``` + +#### command.usage(text) + +- 类型: `(text: string) => Command` + +为此命令添加使用说明。 + +### 事件 + +监听命令: + +```js +// 监听 `foo` 命令 +cli.on('command:foo', () => { + // 执行特定业务 +}) + +// 监听默认命令 +cli.on('command:!', () => { + // 执行特定业务 +}) + +// 监听未知命令 +cli.on('command:*', () => { + console.error('Invalid command: %s', cli.args.join(' ')) + process.exit(1) +}) +``` + +## FAQ + +### 这个名字是如何书写和发音的? + +CAC 或 cac,发音为 `C-A-C` 。 + +这个项目是献给我们可爱的 CC sama 的。 也许 CAC 也代表 C&C :P + + + +### 为什么不使用 Commander.js? + +Basically I made CAC to fulfill my own needs for building CLI apps like [Poi](https://poi.js.org), [SAO](https://sao.vercel.app) and all my CLI apps. It's small, simple but powerful :P + +CAC 与 Commander.js 非常相似,而后者不支持**点嵌套选项**,即类似 `--env.API_SECRET foo` . 此外,您也不能在 Commander.js 中使用**未知选项**。 + +*也许更多...* + +基本上,我开发 CAC 是为了满足我自己构建 CLI 应用程序(如 [Poi](https://poi.js.org) 、 [SAO](https://sao.vercel.app) 和我所有的 CLI 应用程序)的需求。 + +它小巧、简单但功能强大:P + +## [](https://github.com/cacjs/cac#project-stats)项目统计 + +![Alt](https://repobeats.axiom.co/api/embed/58caf6203631bcdb9bbe22f0728a0af1683dc0bb.svg 'Repobeats analytics image') + +## 贡献 + +1. Fork 项目! +2. 创建您的功能分支:`git checkout -b my-new-feature` +3. 提交您的更改:`git commit -am 'Add some feature'` +4. 推送到分支: `git push origin my-new-feature` +5. 提交拉取请求 :D + +## 作者 + +**CAC** © [EGOIST](https://github.com/egoist), Released under the [MIT](./LICENSE) License.
    +Authored and maintained by egoist with help from contributors ([list](https://github.com/cacjs/cac/contributors)). + +> [Website](https://egoist.sh) · GitHub [@egoist](https://github.com/egoist) · Twitter [@\_egoistlily](https://twitter.com/_egoistlily) diff --git a/Cute-CLI/CACDemo/examples/basic-usage.js b/Cute-CLI/CACDemo/examples/basic-usage.js new file mode 100644 index 00000000..a72b9eb5 --- /dev/null +++ b/Cute-CLI/CACDemo/examples/basic-usage.js @@ -0,0 +1,9 @@ +const cli = require('cac')() + +cli.option('--type ', 'Choose a project type', { + default: 'node', +}) + +const parsed = cli.parse() + +console.log(JSON.stringify(parsed, null, 2)) \ No newline at end of file diff --git a/Cute-CLI/CACDemo/examples/help.js b/Cute-CLI/CACDemo/examples/help.js new file mode 100644 index 00000000..30960cee --- /dev/null +++ b/Cute-CLI/CACDemo/examples/help.js @@ -0,0 +1,20 @@ +const cli = require('cac')('LEO-CLI') + +cli.option('--type [type]', 'Choose a project type', { + default: 'node', +}) +cli.option('--name ', 'Provide your name') + +cli.command('lint [...files]', 'Lint files').action((files, options) => { + console.log(files, options) +}) + +// Display help message when `-h` or `--help` appears +cli.help() +// Display version number when `-v` or `--version` appears +// It's also used in help message +cli.version('0.0.0') + +const parsed = cli.parse() + +// console.log(JSON.stringify(parsed, null, 2)) \ No newline at end of file diff --git a/Cute-CLI/CACDemo/examples/specific-options.js b/Cute-CLI/CACDemo/examples/specific-options.js new file mode 100644 index 00000000..1d323da9 --- /dev/null +++ b/Cute-CLI/CACDemo/examples/specific-options.js @@ -0,0 +1,14 @@ +const cli = require('cac')() + +cli + .command('rm ', 'Remove a dir') + .option('-r, --recursive', 'Remove recursively') + .action((dir, options) => { + console.log('remove ' + dir + (options.recursive ? ' recursively' : '')) + }) + +cli.help() + +const parsed = cli.parse() + +console.log(JSON.stringify(parsed, null, 2)) \ No newline at end of file diff --git a/Cute-CLI/CACDemo/package.json b/Cute-CLI/CACDemo/package.json new file mode 100644 index 00000000..34bddec0 --- /dev/null +++ b/Cute-CLI/CACDemo/package.json @@ -0,0 +1,22 @@ +{ + "name": "CACDemo", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "t1": "node examples/basic-usage.js build --env.foo foo --env.bar bar", + "t2": "node examples/help.js -h ", + "t3": "node examples/specific-options.js --type=leo rm" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "cac": "^6.7.12" + }, + "directories": { + "example": "examples" + }, + "devDependencies": {}, + "description": "" +} diff --git a/Cute-CLI/CACDemo/pnpm-lock.yaml b/Cute-CLI/CACDemo/pnpm-lock.yaml new file mode 100644 index 00000000..50b1d9f2 --- /dev/null +++ b/Cute-CLI/CACDemo/pnpm-lock.yaml @@ -0,0 +1,14 @@ +lockfileVersion: 5.3 + +specifiers: + cac: ^6.7.12 + +dependencies: + cac: 6.7.12 + +packages: + + /cac/6.7.12: + resolution: {integrity: sha512-rM7E2ygtMkJqD9c7WnFU6fruFcN3xe4FM5yUmgxhZzIKJk4uHl9U/fhwdajGFQbQuv43FAUo1Fe8gX/oIKDeSA==} + engines: {node: '>=8'} + dev: false diff --git a/Cute-CLI/LearnCLI/DefineDemo/index.js b/Cute-CLI/LearnCLI/DefineDemo/index.js new file mode 100755 index 00000000..eebe02b2 --- /dev/null +++ b/Cute-CLI/LearnCLI/DefineDemo/index.js @@ -0,0 +1,8 @@ +#!/usr/bin/env node + +console.log(process.argv) + +module.exports = { + name: 'leo', + argv: process.argv +} \ No newline at end of file diff --git a/Cute-CLI/LearnCLI/DefineDemo/package-lock.json b/Cute-CLI/LearnCLI/DefineDemo/package-lock.json new file mode 100644 index 00000000..f5507eac --- /dev/null +++ b/Cute-CLI/LearnCLI/DefineDemo/package-lock.json @@ -0,0 +1,5 @@ +{ + "name": "leo", + "version": "1.0.0", + "lockfileVersion": 1 +} diff --git a/Cute-CLI/LearnCLI/DefineDemo/package.json b/Cute-CLI/LearnCLI/DefineDemo/package.json new file mode 100644 index 00000000..5e0ff2ff --- /dev/null +++ b/Cute-CLI/LearnCLI/DefineDemo/package.json @@ -0,0 +1,14 @@ +{ + "name": "leo", + "version": "1.0.0", + "description": "", + "main": "index.js", + "bin": "index.js", + "scripts": { + "leo": "node . --scripts", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/Cute-CLI/LearnCLI/DefineDemo/src/index.js b/Cute-CLI/LearnCLI/DefineDemo/src/index.js new file mode 100755 index 00000000..eebe02b2 --- /dev/null +++ b/Cute-CLI/LearnCLI/DefineDemo/src/index.js @@ -0,0 +1,8 @@ +#!/usr/bin/env node + +console.log(process.argv) + +module.exports = { + name: 'leo', + argv: process.argv +} \ No newline at end of file diff --git a/Cute-CLI/LearnCLI/DefineNoShebang/package-lock.json b/Cute-CLI/LearnCLI/DefineNoShebang/package-lock.json new file mode 100644 index 00000000..d4cc03db --- /dev/null +++ b/Cute-CLI/LearnCLI/DefineNoShebang/package-lock.json @@ -0,0 +1,5 @@ +{ + "name": "leo-no-shebang", + "version": "1.0.0", + "lockfileVersion": 1 +} diff --git a/Cute-CLI/LearnCLI/DefineNoShebang/package.json b/Cute-CLI/LearnCLI/DefineNoShebang/package.json new file mode 100644 index 00000000..03464715 --- /dev/null +++ b/Cute-CLI/LearnCLI/DefineNoShebang/package.json @@ -0,0 +1,10 @@ +{ + "name": "leo-no-shebang", + "version": "1.0.0", + "description": "", + "main": "./src/index.js", + "bin": "./src/index.js", + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/Cute-CLI/LearnCLI/DefineNoShebang/src/index.js b/Cute-CLI/LearnCLI/DefineNoShebang/src/index.js new file mode 100755 index 00000000..eafb901f --- /dev/null +++ b/Cute-CLI/LearnCLI/DefineNoShebang/src/index.js @@ -0,0 +1,6 @@ +console.log(process.argv) + +module.exports = { + name: 'leo', + argv: process.argv +} \ No newline at end of file diff --git a/Cute-CLI/LearnCLI/DefineNoShebangText/package-lock.json b/Cute-CLI/LearnCLI/DefineNoShebangText/package-lock.json new file mode 100644 index 00000000..8227b03d --- /dev/null +++ b/Cute-CLI/LearnCLI/DefineNoShebangText/package-lock.json @@ -0,0 +1,5 @@ +{ + "name": "leo-no-shebang-txt", + "version": "1.0.0", + "lockfileVersion": 1 +} diff --git a/Cute-CLI/LearnCLI/DefineNoShebangText/package.json b/Cute-CLI/LearnCLI/DefineNoShebangText/package.json new file mode 100644 index 00000000..c975c2f3 --- /dev/null +++ b/Cute-CLI/LearnCLI/DefineNoShebangText/package.json @@ -0,0 +1,10 @@ +{ + "name": "leo-no-shebang-txt", + "version": "1.0.0", + "description": "", + "main": "./src/index.txt", + "bin": "./src/index.txt", + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/Cute-CLI/LearnCLI/DefineNoShebangText/src/index.txt b/Cute-CLI/LearnCLI/DefineNoShebangText/src/index.txt new file mode 100755 index 00000000..eafb901f --- /dev/null +++ b/Cute-CLI/LearnCLI/DefineNoShebangText/src/index.txt @@ -0,0 +1,6 @@ +console.log(process.argv) + +module.exports = { + name: 'leo', + argv: process.argv +} \ No newline at end of file diff --git a/Cute-CLI/LearnCLI/DefineShebang/package-lock.json b/Cute-CLI/LearnCLI/DefineShebang/package-lock.json new file mode 100644 index 00000000..d1b35df8 --- /dev/null +++ b/Cute-CLI/LearnCLI/DefineShebang/package-lock.json @@ -0,0 +1,5 @@ +{ + "name": "leo-shebang", + "version": "1.0.0", + "lockfileVersion": 1 +} diff --git a/Cute-CLI/LearnCLI/DefineShebang/package.json b/Cute-CLI/LearnCLI/DefineShebang/package.json new file mode 100644 index 00000000..5ef9b5b0 --- /dev/null +++ b/Cute-CLI/LearnCLI/DefineShebang/package.json @@ -0,0 +1,10 @@ +{ + "name": "leo-shebang", + "version": "1.0.0", + "description": "", + "main": "./src/index.js", + "bin": "./src/index.js", + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/Cute-CLI/LearnCLI/DefineShebang/src/index.js b/Cute-CLI/LearnCLI/DefineShebang/src/index.js new file mode 100755 index 00000000..eebe02b2 --- /dev/null +++ b/Cute-CLI/LearnCLI/DefineShebang/src/index.js @@ -0,0 +1,8 @@ +#!/usr/bin/env node + +console.log(process.argv) + +module.exports = { + name: 'leo', + argv: process.argv +} \ No newline at end of file diff --git a/Cute-CLI/LearnCLI/DefineShebangText/package-lock.json b/Cute-CLI/LearnCLI/DefineShebangText/package-lock.json new file mode 100644 index 00000000..f51b6c94 --- /dev/null +++ b/Cute-CLI/LearnCLI/DefineShebangText/package-lock.json @@ -0,0 +1,5 @@ +{ + "name": "leo-shebang-txt", + "version": "1.0.0", + "lockfileVersion": 1 +} diff --git a/Cute-CLI/LearnCLI/DefineShebangText/package.json b/Cute-CLI/LearnCLI/DefineShebangText/package.json new file mode 100644 index 00000000..de9797f2 --- /dev/null +++ b/Cute-CLI/LearnCLI/DefineShebangText/package.json @@ -0,0 +1,10 @@ +{ + "name": "leo-shebang-txt", + "version": "1.0.0", + "description": "", + "main": "./src/index.txt", + "bin": "./src/index.txt", + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/Cute-CLI/LearnCLI/DefineShebangText/src/index.txt b/Cute-CLI/LearnCLI/DefineShebangText/src/index.txt new file mode 100755 index 00000000..eebe02b2 --- /dev/null +++ b/Cute-CLI/LearnCLI/DefineShebangText/src/index.txt @@ -0,0 +1,8 @@ +#!/usr/bin/env node + +console.log(process.argv) + +module.exports = { + name: 'leo', + argv: process.argv +} \ No newline at end of file diff --git a/Cute-CLI/LearnCLI/README.md b/Cute-CLI/LearnCLI/README.md new file mode 100644 index 00000000..15ce71bd --- /dev/null +++ b/Cute-CLI/LearnCLI/README.md @@ -0,0 +1,102 @@ +## 执行 CLI 包的 3 种方式 + +### 1. node 执行 + +可以直接通过 `node node_modules/{packageName}/{entryFile}` 执行目录下的 js 文件。 + +### 2. npm scripts 命令 + +在项目 `package.json` 文件中配置 `scripts` 属性: + +```json +// DefineDemo/package.json + +{ + //... + "scripts": { + "leo": "node . --scripts", + } +} +``` + +这样在项目根目录就可以直接通过 `npm run leo` 命令执行。 + +### 3. npx 命令 + +> 如果不清楚 npx 为何物,可以先阅读一下阮一峰老师 [npx 使用教程](https://www.ruanyifeng.com/blog/2019/02/npx.html)。 + +npx 是 Node.js 自带的模块,可以用来调用项目内部安装的模块。 + +在开发阶段,我们可以使用 [`npm link`](https://docs.npmjs.com/cli/v6/commands/npm-link/) 来为 CLI 包生成一个软链接。 + +只需 3 个步骤: +1. CLI 包的目录:配置 package.json 中 `name` 属性作为包名, `bin` 属性指定执行的入口文件; + +```json +// DefineDemo/package.json + +{ + //... + "name": "leo", + "bin": "./src/index.js" +} +``` + +2. CLI 包的目录:执行 `npm link`,将当前包添加到全局环境变量 path; + +```bash +// DefineDemo +npm link +``` + +3. 使用包的目录:执行 `npm link {packageName}`,安装需要调试的 CLI 包。 + +```bash +// UseDemo +npm link leo +``` + +当 `npm link` 生成软链接以后,可以通过配置 [Shebang](https://zh.wikipedia.org/wiki/Shebang) 来定义执行方式。 + +如果使用 Shebang,会有下列 4 种情况: +1. 配置了 Shebang 的 js 文件: + +```js +// ./DefineShebang/src/index.js +#!/usr/bin/env node + +console.log(process.argv) +``` + +执行 `npx leo-shebang` 能够正常执行,并输出日志。 + +2. 没有配置 Shebang 的 js 文件: + +```js +// ./DefineNoShebang/src/index.js + +console.log(process.argv) +``` + +执行 `npx leo-no-shebang` 报错。 + +3. 配置了 Shebang 的非 js 文件: + +```js +// ./DefineShebangText/src/index.js +#!/usr/bin/env node + +console.log(process.argv) +``` + +执行 `npx leo-shebang-txt` 能够正常执行,并输出日志。 + +4. 没有配置 Shebang 的非 js 文件: + +```js +// ./DefineNoShebangText/src/index.js + +console.log(process.argv) +``` + +执行 `npx leo-no-shebang-txt` 报错。 diff --git a/Cute-CLI/LearnCLI/UseDemo/index.js b/Cute-CLI/LearnCLI/UseDemo/index.js new file mode 100644 index 00000000..63c54001 --- /dev/null +++ b/Cute-CLI/LearnCLI/UseDemo/index.js @@ -0,0 +1,3 @@ +const Leo = require('leo'); + +console.log('[Leo]', Leo) \ No newline at end of file diff --git a/Cute-CLI/LearnCLI/UseDemo/package.json b/Cute-CLI/LearnCLI/UseDemo/package.json new file mode 100644 index 00000000..22d0a11d --- /dev/null +++ b/Cute-CLI/LearnCLI/UseDemo/package.json @@ -0,0 +1,13 @@ +{ + "name": "UseDemo", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "leo": "node . --scripts", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/Cute-CLI/LeoCLI/package.json b/Cute-CLI/LeoCLI/package.json new file mode 100644 index 00000000..45119aa4 --- /dev/null +++ b/Cute-CLI/LeoCLI/package.json @@ -0,0 +1,12 @@ +{ + "name": "LeoCLI", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/Cute-CLI/LeoCLI/src/index.js b/Cute-CLI/LeoCLI/src/index.js new file mode 100644 index 00000000..e69de29b diff --git a/Cute-Demo/.DS_Store b/Cute-Demo/.DS_Store new file mode 100644 index 00000000..79fa8489 Binary files /dev/null and b/Cute-Demo/.DS_Store differ diff --git a/Cute-Demo/1.Local-Image-Preview.html b/Cute-Demo/1.Local-Image-Preview.html new file mode 100644 index 00000000..151fdbb0 --- /dev/null +++ b/Cute-Demo/1.Local-Image-Preview.html @@ -0,0 +1,40 @@ + + + + + + 1. 图片本地预览 + + +

    1.DataURL方式:

    + + + +

    2.Blob方式:

    + + + + + + \ No newline at end of file diff --git "a/Cute-Demo/10-\345\261\217\345\271\225\346\273\232\345\212\250\347\233\221\345\220\254.html" "b/Cute-Demo/10-\345\261\217\345\271\225\346\273\232\345\212\250\347\233\221\345\220\254.html" new file mode 100644 index 00000000..a3587dc4 --- /dev/null +++ "b/Cute-Demo/10-\345\261\217\345\271\225\346\273\232\345\212\250\347\233\221\345\220\254.html" @@ -0,0 +1,46 @@ + + + + + + + Dome + + + + + +
    + + + \ No newline at end of file diff --git a/Cute-Demo/10.Learn-Array-Liked-Objects/index.html b/Cute-Demo/10.Learn-Array-Liked-Objects/index.html new file mode 100644 index 00000000..a5ab8de7 --- /dev/null +++ b/Cute-Demo/10.Learn-Array-Liked-Objects/index.html @@ -0,0 +1,28 @@ + + + + + + + Document + + + + + +
    +

    Select Members

    @微信公众号:前端自习课

    +
    + +
    +
      + Select Result +
      +
      +
      + + + + + + \ No newline at end of file diff --git a/Cute-Demo/10.Learn-Array-Liked-Objects/index.js b/Cute-Demo/10.Learn-Array-Liked-Objects/index.js new file mode 100644 index 00000000..f88c7f8a --- /dev/null +++ b/Cute-Demo/10.Learn-Array-Liked-Objects/index.js @@ -0,0 +1,106 @@ +// Demo 地址: https://codepen.io/akilathiwanka/pen/JpBObV +class SelectMember { + constructor(){ + this.MockUsers = window.MockUsers; + this.init(); + } + selectClassName = 'checked'; + toastTimer = 0; + init(){ + this.initMemberList('#MemberList', this.MockUsers); + this.initBindEvent(); + } + + initMemberList(element, memberList){ + let html = ''; + memberList.map(item => { + const selected = item.selected ? `class='${this.selectClassName}'` : ''; + html += ` +
    • +
      + +
      +
      +

      ${item.first_name}

      ${item.email} +
      + +
    • + ` + }) + $(element).html(html); + } + + selectMember(){ + const MemberListDOM = $('#MemberList'); + const checkName = this.selectClassName; + let target = event.target; + while (target !== MemberListDOM) { + if (target.tagName && target.tagName.toLowerCase() === 'li') { + let selectDOM = $(target).children('.round-checkbox').children('span'); + if (selectDOM && selectDOM.hasClass(checkName)) { + selectDOM.removeClass(checkName); + } else { + selectDOM.addClass(checkName) + } + break; + } + target = target.parentNode; + } + } + + searchMember(input){ + const memberName = input.target.value; + const targetMemberList = this.MockUsers.filter(item => item.first_name == memberName); + targetMemberList && targetMemberList.length > 0 && this.initMemberList('#MemberList', targetMemberList); + memberName === '' && this.initMemberList('#MemberList', this.MockUsers); + const renderMemberList = + memberName === '' ? this.MockUsers : + targetMemberList && targetMemberList.length > 0 ? targetMemberList : + []; + + // TODO 练习项目,暂不考虑渲染性能 + this.initMemberList('#MemberList', renderMemberList); + } + + initBindEvent(){ + $('#MemberList').click(this.selectMember.bind(this)); + $('#InputMember').bind('input porpertychange', this.searchMember.bind(this)); + $('#SubmitButton').click(this.submitSelect.bind(this)); + } + + submitSelect(){ + const memberList = Array.from($('#MemberList li')); + const result = { + text: [], + dom : [], + }; + memberList.map(item => { + const hasClass = $(item).children('.round-checkbox').children('span').hasClass(this.selectClassName); + if(hasClass){ + result.text.push($(item).children('.user-data').children('h4').text()); + result.dom.push(item); + } + }) + console.log(result) + this.showToast(`选中成员:${result.text}`); + } + + showToast(text, time = 2000){ + const toast = $('#GlocalToast'); + if(!this.toastTimer){ + toast.text(text); + toast.show(); + this.toastTimer = setTimeout(this.hideToast.bind(this), time) + } + } + + hideToast(){ + const toast = $('#GlocalToast'); + toast.hide(); + this.toastTimer = 0; + } +} + +let newMember = new SelectMember(); \ No newline at end of file diff --git a/Cute-Demo/10.Learn-Array-Liked-Objects/mock.js b/Cute-Demo/10.Learn-Array-Liked-Objects/mock.js new file mode 100644 index 00000000..2477ecdc --- /dev/null +++ b/Cute-Demo/10.Learn-Array-Liked-Objects/mock.js @@ -0,0 +1,91 @@ +window.MockUsers = [{ + "id": 1, + "first_name": "Glenn", + "mobile": true, + "email": "derrick@studiod.com", + "date": "22-Mar-2016", + "selected": true, + "time": "5:46 PM", + "image": "https://randomuser.me/api/portraits/men/1.jpg" +}, { + "id": 2, + "first_name": "Carl", + "mobile": false, + "email": "derrick@studiod.com", + "date": "22-Feb-2016", + "selected": false, + "time": "09:38 PM", + "image": "https://randomuser.me/api/portraits/women/37.jpg" +}, { + "id": 3, + "first_name": "Rick", + "mobile": true, + "email": "derrick@studiod.com", + "date": "01-Jul-2016", + "selected": false, + "time": "1:33 PM", + "image": "https://randomuser.me/api/portraits/women/13.jpg" +}, { + "id": 4, + "first_name": "Maggie", + "mobile": false, + "email": "derrick@studiod.com", + "date": "19-Feb-2016", + "selected": false, + "time": "02:59 AM", + "image": "https://randomuser.me/api/portraits/men/5.jpg" +}, { + "id": 5, + "first_name": "Michael", + "mobile": true, + "email": "derrick@studiod.com", + "date": "12-Aug-2016", + "selected": false, + "time": "9:17 AM", + "image": "https://randomuser.me/api/portraits/men/19.jpg" +}, { + "id": 6, + "first_name": "Jesus", + "mobile": false, + "email": "derrick@studiod.com", + "date": "13-Aug-2016", + "selected": false, + "time": "10:37 PM", + "image": "https://randomuser.me/api/portraits/men/18.jpg" +}, { + "id": 7, + "first_name": "Daryn", + "mobile": true, + "email": "derrick@studiod.com", + "date": "17-Nov-2016", + "selected": false, + "time": "07:32 AM", + "image": "https://randomuser.me/api/portraits/men/30.jpg" +}, { + "id": 8, + "first_name": "Fred", + "mobile": false, + "email": "derrick@studiod.com", + "date": "29-Nov-2016", + "selected": false, + "time": "12:56 AM", + "image": "https://randomuser.me/api/portraits/women/10.jpg" +}, { + "id": 9, + "first_name": "James", + "mobile": false, + "email": "derrick@studiod.com", + "date": "27-Dec-2016", + "selected": false, + "time": "9:29 PM", + "image": "https://randomuser.me/api/portraits/women/6.jpg" +}, { + "id": 10, + "first_name": "Matthew", + "mobile": true, + "email": "derrick@studiod.com", + "date": "31-Dec-2016", + "selected": false, + "time": "7:43 PM", + "image": "https://randomuser.me/api/portraits/men/18.jpg" +}] \ No newline at end of file diff --git a/Cute-Demo/10.Learn-Array-Liked-Objects/style.css b/Cute-Demo/10.Learn-Array-Liked-Objects/style.css new file mode 100644 index 00000000..58b979ff --- /dev/null +++ b/Cute-Demo/10.Learn-Array-Liked-Objects/style.css @@ -0,0 +1,194 @@ +body{ + background: #FFF7DC; + margin: 20px; + font-family: 'Avenir'; + color: #333; + font-size: 14px; +} + +h1, h2, h3, h4, h5, h6 { + color: #404553; + margin: 0; +} + +#app{ + width: 600px; + height: 820px; + margin: 0 auto; + border-radius: 5px; + overflow: hidden; + box-shadow: 0px 12px 65px rgba(32, 50, 98, .07); +} + +#AppContent{ + max-width: 460px; + margin: 0 auto; + background: #fff; + padding: 20px 26px; + color: #404553; +} + +#MultiSelectApp h3 { + font-size: 30px; + margin-top: 10px; +} + +input[type="text"]:focus { + outline: none; +} + +input[type="text"] { + box-sizing: border-box; + width: 100%; + padding: 10px 16px; + font-size: 14px; + border: 0; + border-bottom: 1px solid #eee; +} +input[type="text"]::placeholder { + color: #D4D2E1; + font-family: 'Avenir'; + font-weight: 400; +} + +.btn-done { + background: #6A65FF; + color: #fff; + width: 100%; + display: block; + line-height: 70px; + text-align: center; + text-decoration: none; + font-weight: 700; + font-size: 18px; +} + +.search-icon { + color: #C1BFD3; + position: absolute; + top: 10px; + left: 2px; + font-size: 16px; +} + +#InputContent{ + position:relative; +} + +.user-list { + list-style: none; + padding: 0; + margin-top: 20px; + height: 430px; + overflow-y: scroll; +} + +.user-list li { + padding: 20px; + cursor: pointer; + position:relative; +} + +.user-list li:hover { + background-color: #F9F9F9; +} + +.user-list li img { + width: 100%; +} + +.user-image { + float: left; + width: 40px; + height: 40px; + border-radius: 50%; + overflow: hidden; +} + +.user-list .user-data { + padding-top: 2px; + margin-left: 56px; + position: relative; +} + +.user-list .user-data h4 { + font-size: 18px; + font-weight: 500; +} + +.user-list .user-data span { + color: #A7ADBF; + font-weight: 500; +} + +.round-checkbox { + display: block; + position: absolute; + padding-left: 35px; + margin-bottom: 12px; + cursor: pointer; + font-size: 22px; + top: 6px; + right: 0px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +/* Create a custom checkbox */ +.round-checkbox span { + position: absolute; + top: 25px; + right: 20px; + height: 25px; + width: 25px; + border-radius: 25px; + border: 2px solid #0FCE8F; +} + +/* Create the checkmark/indicator (hidden when not checked) */ +.round-checkbox span:after { + content: ""; + position: absolute; + display: none; +} + +/* Show the checkmark when checked */ +span.checked:after { + display: block; +} + +/* Style the checkmark/indicator */ +.round-checkbox span:after { + left: 9px; + top: 5px; + width: 5px; + height: 10px; + border: solid #0FCE8F; + border-width: 0 2px 2px 0; + -webkit-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); +} + + +#GlocalToast { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + min-width: 100px; + max-width: 300px; + height: 100px; + line-height: 30px; + word-break: break-word; + overflow-y: scroll; + color: #fff; + text-align: center; + background: rgba(0,0,0,.6); + margin: auto; + border-radius: 6px; + display: none; +} \ No newline at end of file diff --git "a/Cute-Demo/11-JS\345\256\236\347\216\260\347\256\200\345\215\225\347\232\204vuejs\345\217\214\345\220\221\346\225\260\346\215\256\347\273\221\345\256\232.html" "b/Cute-Demo/11-JS\345\256\236\347\216\260\347\256\200\345\215\225\347\232\204vuejs\345\217\214\345\220\221\346\225\260\346\215\256\347\273\221\345\256\232.html" new file mode 100644 index 00000000..8f888088 --- /dev/null +++ "b/Cute-Demo/11-JS\345\256\236\347\216\260\347\256\200\345\215\225\347\232\204vuejs\345\217\214\345\220\221\346\225\260\346\215\256\347\273\221\345\256\232.html" @@ -0,0 +1,33 @@ + + + + + + + + Document + + + +
      + +

      +
      + + + + \ No newline at end of file diff --git "a/Cute-Demo/12-vuejs\345\217\214\345\220\221\346\225\260\346\215\256\347\273\221\345\256\232\345\216\237\347\220\206.md" "b/Cute-Demo/12-vuejs\345\217\214\345\220\221\346\225\260\346\215\256\347\273\221\345\256\232\345\216\237\347\220\206.md" new file mode 100644 index 00000000..0f78e6ee --- /dev/null +++ "b/Cute-Demo/12-vuejs\345\217\214\345\220\221\346\225\260\346\215\256\347\273\221\345\256\232\345\216\237\347\220\206.md" @@ -0,0 +1,33 @@ +## 数据双向绑定的原理 +`Object.defineProperty()` + +vue实现数据双向绑定主要是: + 采用数据劫持结合发布者-订阅者模式的方式,通过 `Object.defineProperty()` 来劫持各个属性的`setter`,`getter`,在数据变动时发布消息给订阅者,触发相应监听回调。当把一个普通 Javascript 对象传给 Vue 实例来作为它的 `data` 选项时,Vue 将遍历它的属性,用 `Object.defineProperty()` 将它们转为 `getter/setter`。用户看不到 `getter/setter`,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。 + +vue的数据双向绑定 将MVVM作为数据绑定的入口,整合`Observer`,`Compile`和`Watcher`三者,通过`Observer`来监听自己的`model`的数据变化,通过`Compile`来解析编译模板指令(vue中是用来解析 {{}}),最终利用`watcher`搭起`observer`和`Compile`之间的通信桥梁,达到数据变化 —>视图更新;视图交互变化(input)—>数据model变更双向绑定效果。 + + +## 简单实现 +```html + +
      + +

      +
      + + +``` \ No newline at end of file diff --git "a/Cute-Demo/13-\346\240\210\345\222\214\351\230\237\345\210\227.js" "b/Cute-Demo/13-\346\240\210\345\222\214\351\230\237\345\210\227.js" new file mode 100644 index 00000000..36e11627 --- /dev/null +++ "b/Cute-Demo/13-\346\240\210\345\222\214\351\230\237\345\210\227.js" @@ -0,0 +1,60 @@ +// 实现栈和队列的方法 + +// 1. 栈 LIFO +class Stack { + constructor ( arr ){ + this.arr = arr; + } + // 1. 添加一个(或几个)新元素到栈顶。 + push( ele ){ + this.arr.push( ele ); + } + // 2. 移除栈顶的元素,同时返回被移除的元素 + pop ( ele ){ + return this.arr.pop(); + } + // 3. 返回栈顶的元素,不对栈做任何修改 + peek () { + return this.arr[0]; + } + // 4. 如果栈里没有任何元素就返回true,否则返回false + isEmpty () { + return this.arr.length > 0 ? false : true; + } + // 5. 移除栈里的所有元素。 + clear () { + this.arr = []; + } + // 6. 返回栈里的元素个数。 + size () { + return this.arr.size; + } +} + +// 2. 队列 FIFO +class Queue { + constructor ( arr ){ + this.arr = arr; + } + // 1. 向队列尾部添加一个(或多个)新的项。 + enqueue ( ele ) { + this.arr.unshift( ele ); + } + // 2. 移除队列的第一(即排在队列最前面的)项,并返回被移除的元素。 + dequeue () { + return this.arr.shift(); + } + // 3. 返回队列中第一个元素——最先被添加,也将是最先被移除的元素。 + // 队列不做任何变动(不移除元素,只返回元素信息——与Stack类的peek方法非常类似)。 + front () { + return this.arr[0]; + } + // 4. 如果队列中不包含任何元素,返回true,否则返回false。 + isEmpty () { + return this.arr.length > 0 ? false : true; + } + // 5. 返回队列包含的元素个数,与数组的length属性类似。 + size () { + return this.arr.length; + } +} \ No newline at end of file diff --git "a/Cute-Demo/14-js\345\260\206canvas\344\277\235\345\255\230\346\210\220\345\233\276\347\211\207\345\271\266\344\270\213\350\275\275.html" "b/Cute-Demo/14-js\345\260\206canvas\344\277\235\345\255\230\346\210\220\345\233\276\347\211\207\345\271\266\344\270\213\350\275\275.html" new file mode 100644 index 00000000..3dddec50 --- /dev/null +++ "b/Cute-Demo/14-js\345\260\206canvas\344\277\235\345\255\230\346\210\220\345\233\276\347\211\207\345\271\266\344\270\213\350\275\275.html" @@ -0,0 +1,83 @@ + + + + + + + canvas转img并下载 + + + +
      + + +
      + + \ No newline at end of file diff --git a/Cute-Demo/15-Write-Promise.js b/Cute-Demo/15-Write-Promise.js new file mode 100644 index 00000000..97eca777 --- /dev/null +++ b/Cute-Demo/15-Write-Promise.js @@ -0,0 +1,78 @@ +// 参考文章:https://mengera88.github.io/2017/05/18/Promise%E5%8E%9F%E7%90%86%E8%A7%A3%E6%9E%90/ + +function MyPromise (fn){ + let state = 'pending', value = null, callbacks = []; + this.then = function(onFulfilled, onRejected){ + console.log("MyPromise - then") + return new MyPromise(function(resolve, reject){ + handle({ + onFulfilled: onFulfilled || null, + onRejected: onRejected || null, + resolve: resolve, + reject: reject + }) + }) + } + + function handle(callback){ + console.log("MyPromise - handle") + if(state === 'pending'){ + callbacks.push(callback); + return this; + } + let cb = state === 'fulfilled' ? callback.onFulfilled : callback.onRejected, ret; + if(cb === null){ + cb = state === 'fulfilled' ? callback.resolve : callback.reject; + cb(value); + return; + } + try { + ret = cb(value); + callback.resolve(ret); + } catch (e) { + callback.reject(e); + } + } + + function resolve(newValue){ + // 若 then 参数也是 promise 方法 + console.log("MyPromise - resolve") + if(newValue && (typeof newValue === 'object' || typeof newValue === 'function')){ + let then = newValue.then; + if(typeof then === 'function'){ + then.call(newValue, resolve); + return; + } + } + value = newValue; + state = 'fulfilled'; + execute(); + } + function reject(reason){ + console.log("MyPromise - reject") + state = "rejected"; + value = reason; + execute(); + } + function execute(){ + setTimeout(function () { + callbacks.forEach(function (callback) { + callback(value); + }); + }, 0); + } + + fn(resolve, reject); +} + +//例1 +function getUserId() { + return new MyPromise(function(resolve) { + //异步请求 + console.log("异步请求") + resolve("abc") + }) +} +getUserId().then(function(id) { + console.log("then:", id) +}) diff --git a/Cute-Demo/16-WeiteHeartPop.html b/Cute-Demo/16-WeiteHeartPop.html new file mode 100644 index 00000000..8dbfd423 --- /dev/null +++ b/Cute-Demo/16-WeiteHeartPop.html @@ -0,0 +1,73 @@ + + + + + + + 爱心666 + + + + +
      + + + + + \ No newline at end of file diff --git a/Cute-Demo/17-CSS-Sticky.html b/Cute-Demo/17-CSS-Sticky.html new file mode 100644 index 00000000..5a0f1794 --- /dev/null +++ b/Cute-Demo/17-CSS-Sticky.html @@ -0,0 +1,84 @@ + + + + + + + sticky + + + +
      + +
      + 前端业界现有的构建部署方案,常用的应该是 Jenkins,Docker,GitHub Actions 这些,而恰巧,我们公司现在就并存了前两种方案。既然已经有了稳定的构建部署方式,为什么还要自己做一套前端自己的构建平台呢?当然不是为了好玩啊,原因听我慢慢分析。 + + 前端构建使用的时候可能会碰到各种各样问题,比如: + + Eslint 跳过校验——公司里面的前端项目,随着时间的推移,不同阶段,通过新老脚手架创建出来的项目可能风格各异,并且校验规则可能也不一定统一,虽然项目本身可以有着各种的 Eslint,Stylelint 等校验拦截,但阻止不了开发者跳过这些代码校验。 + npm 版本升级不兼容——对于依赖的 npm 版本必须的一些兼容性校验,如果某些 npm 插件突然升级了不兼容的一些版本,代码上线后就会报错出错,典型的就是各类 IE 兼容。 + 无法自由添加自己想要的功能——想要优化前端构建的流程,或者方便前端使用的功能优化,但因为依赖运维平台的构建应用,想加点自己的功能需要等别人排期。 + 而这些问题,如果有了自己的构建平台,这都将不是问题,所以也就有了现在的——云长。 + + 为何起名叫“云长“呢,当然是希望这个平台能像”关云长“一样,一夫当关万夫莫开。那云长又能给我们提供什么样的一些能力呢? + 前端业界现有的构建部署方案,常用的应该是 Jenkins,Docker,GitHub Actions 这些,而恰巧,我们公司现在就并存了前两种方案。既然已经有了稳定的构建部署方式,为什么还要自己做一套前端自己的构建平台呢?当然不是为了好玩啊,原因听我慢慢分析。 + + 前端构建使用的时候可能会碰到各种各样问题,比如: + + Eslint 跳过校验——公司里面的前端项目,随着时间的推移,不同阶段,通过新老脚手架创建出来的项目可能风格各异,并且校验规则可能也不一定统一,虽然项目本身可以有着各种的 Eslint,Stylelint 等校验拦截,但阻止不了开发者跳过这些代码校验。 + npm 版本升级不兼容——对于依赖的 npm 版本必须的一些兼容性校验,如果某些 npm 插件突然升级了不兼容的一些版本,代码上线后就会报错出错,典型的就是各类 IE 兼容。 + 无法自由添加自己想要的功能——想要优化前端构建的流程,或者方便前端使用的功能优化,但因为依赖运维平台的构建应用,想加点自己的功能需要等别人排期。 + 而这些问题,如果有了自己的构建平台,这都将不是问题,所以也就有了现在的——云长。 + + 为何起名叫“云长“呢,当然是希望这个平台能像”关云长“一样,一夫当关万夫莫开。那云长又能给我们提供什么样的一些能力呢? + 前端业界现有的构建部署方案,常用的应该是 Jenkins,Docker,GitHub Actions 这些,而恰巧,我们公司现在就并存了前两种方案。既然已经有了稳定的构建部署方式,为什么还要自己做一套前端自己的构建平台呢?当然不是为了好玩啊,原因听我慢慢分析。 + + 前端构建使用的时候可能会碰到各种各样问题,比如: + + Eslint 跳过校验——公司里面的前端项目,随着时间的推移,不同阶段,通过新老脚手架创建出来的项目可能风格各异,并且校验规则可能也不一定统一,虽然项目本身可以有着各种的 Eslint,Stylelint 等校验拦截,但阻止不了开发者跳过这些代码校验。 + npm 版本升级不兼容——对于依赖的 npm 版本必须的一些兼容性校验,如果某些 npm 插件突然升级了不兼容的一些版本,代码上线后就会报错出错,典型的就是各类 IE 兼容。 + 无法自由添加自己想要的功能——想要优化前端构建的流程,或者方便前端使用的功能优化,但因为依赖运维平台的构建应用,想加点自己的功能需要等别人排期。 + 而这些问题,如果有了自己的构建平台,这都将不是问题,所以也就有了现在的——云长。 + + 为何起名叫“云长“呢,当然是希望这个平台能像”关云长“一样,一夫当关万夫莫开。那云长又能给我们提供什么样的一些能力呢? + 前端业界现有的构建部署方案,常用的应该是 Jenkins,Docker,GitHub Actions 这些,而恰巧,我们公司现在就并存了前两种方案。既然已经有了稳定的构建部署方式,为什么还要自己做一套前端自己的构建平台呢?当然不是为了好玩啊,原因听我慢慢分析。 + + 前端构建使用的时候可能会碰到各种各样问题,比如: + + Eslint 跳过校验——公司里面的前端项目,随着时间的推移,不同阶段,通过新老脚手架创建出来的项目可能风格各异,并且校验规则可能也不一定统一,虽然项目本身可以有着各种的 Eslint,Stylelint 等校验拦截,但阻止不了开发者跳过这些代码校验。 + npm 版本升级不兼容——对于依赖的 npm 版本必须的一些兼容性校验,如果某些 npm 插件突然升级了不兼容的一些版本,代码上线后就会报错出错,典型的就是各类 IE 兼容。 + 无法自由添加自己想要的功能——想要优化前端构建的流程,或者方便前端使用的功能优化,但因为依赖运维平台的构建应用,想加点自己的功能需要等别人排期。 + 而这些问题,如果有了自己的构建平台,这都将不是问题,所以也就有了现在的——云长。 + + 为何起名叫“云长“呢,当然是希望这个平台能像”关云长“一样,一夫当关万夫莫开。那云长又能给我们提供什么样的一些能力呢? + 前端业界现有的构建部署方案,常用的应该是 Jenkins,Docker,GitHub Actions 这些,而恰巧,我们公司现在就并存了前两种方案。既然已经有了稳定的构建部署方式,为什么还要自己做一套前端自己的构建平台呢?当然不是为了好玩啊,原因听我慢慢分析。 + + 前端构建使用的时候可能会碰到各种各样问题,比如: + + Eslint 跳过校验——公司里面的前端项目,随着时间的推移,不同阶段,通过新老脚手架创建出来的项目可能风格各异,并且校验规则可能也不一定统一,虽然项目本身可以有着各种的 Eslint,Stylelint 等校验拦截,但阻止不了开发者跳过这些代码校验。 + npm 版本升级不兼容——对于依赖的 npm 版本必须的一些兼容性校验,如果某些 npm 插件突然升级了不兼容的一些版本,代码上线后就会报错出错,典型的就是各类 IE 兼容。 + 无法自由添加自己想要的功能——想要优化前端构建的流程,或者方便前端使用的功能优化,但因为依赖运维平台的构建应用,想加点自己的功能需要等别人排期。 + 而这些问题,如果有了自己的构建平台,这都将不是问题,所以也就有了现在的——云长。 + + 为何起名叫“云长“呢,当然是希望这个平台能像”关云长“一样,一夫当关万夫莫开。那云长又能给我们提供什么样的一些能力呢? +
      +
      + + \ No newline at end of file diff --git "a/Cute-Demo/18-\346\234\213\345\217\213\345\234\210\347\233\270\345\206\214.html" "b/Cute-Demo/18-\346\234\213\345\217\213\345\234\210\347\233\270\345\206\214.html" new file mode 100644 index 00000000..fb513ebc --- /dev/null +++ "b/Cute-Demo/18-\346\234\213\345\217\213\345\234\210\347\233\270\345\206\214.html" @@ -0,0 +1,93 @@ + + + + + + + 微信朋友圈图片九宫格排版自适应(改编版) + + + + +
      +
      + + +
      +
      + + diff --git a/Cute-Demo/19-CaledarComponents.vue b/Cute-Demo/19-CaledarComponents.vue new file mode 100644 index 00000000..40b5c705 --- /dev/null +++ b/Cute-Demo/19-CaledarComponents.vue @@ -0,0 +1,271 @@ + + + + + diff --git a/Cute-Demo/2.Local-Image-Preview-Upload.html b/Cute-Demo/2.Local-Image-Preview-Upload.html new file mode 100644 index 00000000..eaaf2ce1 --- /dev/null +++ b/Cute-Demo/2.Local-Image-Preview-Upload.html @@ -0,0 +1,46 @@ + + + + + + 2. 图片本地预览 + 分片上传 + + + + + + + + + \ No newline at end of file diff --git "a/Cute-Demo/20-\346\273\232\345\212\250\347\251\277\351\200\217\344\270\216\346\273\232\345\212\250\346\272\242\345\207\272.html" "b/Cute-Demo/20-\346\273\232\345\212\250\347\251\277\351\200\217\344\270\216\346\273\232\345\212\250\346\272\242\345\207\272.html" new file mode 100644 index 00000000..6f6c3b7d --- /dev/null +++ "b/Cute-Demo/20-\346\273\232\345\212\250\347\251\277\351\200\217\344\270\216\346\273\232\345\212\250\346\272\242\345\207\272.html" @@ -0,0 +1,147 @@ + + + + + + + + 滚动穿透与滚动溢出 + + + + +
      点击出现弹窗
      +
      这个页面很高哦
      + + + + + + + \ No newline at end of file diff --git a/Cute-Demo/21.middler-dome.js b/Cute-Demo/21.middler-dome.js new file mode 100644 index 00000000..bc045bde --- /dev/null +++ b/Cute-Demo/21.middler-dome.js @@ -0,0 +1,31 @@ +const c1 = next => { + console.log('[c1-1]'); + next(); + console.log('[c1-2]'); +} + +const c2 = next => { + console.log('[c2-1]'); + next(); + console.log('[c2-2]'); +} + +const runCallbacks = cbs => { + if(Array.isArray(cbs)){ + let idx = 0; + const next = () => { + const cb = cbs[idx++]; + typeof cb === 'function' && cb.call(cb, next); + } + next(); + } +} + +runCallbacks([c1, c2]); +/** + 输出结果: + [c1-1] + [c1-2] + [c2-2] + [c2-1] + */ \ No newline at end of file diff --git a/Cute-Demo/3.Local-Image-Preview-Upload-Pause.html b/Cute-Demo/3.Local-Image-Preview-Upload-Pause.html new file mode 100644 index 00000000..2f979b0c --- /dev/null +++ b/Cute-Demo/3.Local-Image-Preview-Upload-Pause.html @@ -0,0 +1,62 @@ + + + + + + 3. 图片本地预览 + 分片上传 + 暂停 + 续传 + + + + + + + + + + + \ No newline at end of file diff --git a/Cute-Demo/4.Download-File-For-Online.html b/Cute-Demo/4.Download-File-For-Online.html new file mode 100644 index 00000000..1a3fe0a9 --- /dev/null +++ b/Cute-Demo/4.Download-File-For-Online.html @@ -0,0 +1,39 @@ + + + + + + 4. 从互联网下载数据 + + + + + + + + \ No newline at end of file diff --git a/Cute-Demo/5.Download-File.html b/Cute-Demo/5.Download-File.html new file mode 100644 index 00000000..8bada8d0 --- /dev/null +++ b/Cute-Demo/5.Download-File.html @@ -0,0 +1,29 @@ + + + + + + 5. 下载文件 + + + + + + + + + \ No newline at end of file diff --git a/Cute-Demo/6.Photo-Compression.html b/Cute-Demo/6.Photo-Compression.html new file mode 100644 index 00000000..e7b5669c --- /dev/null +++ b/Cute-Demo/6.Photo-Compression.html @@ -0,0 +1,77 @@ + + + + + + 6. 图片压缩 + + + + + + + + \ No newline at end of file diff --git a/Cute-Demo/7.Generate-PDF.html b/Cute-Demo/7.Generate-PDF.html new file mode 100644 index 00000000..5933cf8d --- /dev/null +++ b/Cute-Demo/7.Generate-PDF.html @@ -0,0 +1,22 @@ + + + + + + 7. 生成 PDF 文档 + + +

      客户端生成 PDF 示例

      + + + + \ No newline at end of file diff --git a/Cute-Demo/8.Use-Blob-Or-ArrayBuffer-In-Ajax.html b/Cute-Demo/8.Use-Blob-Or-ArrayBuffer-In-Ajax.html new file mode 100644 index 00000000..1f7401bb --- /dev/null +++ b/Cute-Demo/8.Use-Blob-Or-ArrayBuffer-In-Ajax.html @@ -0,0 +1,28 @@ + + + + + + Ajax 中使用 Blob 和 ArrayBuffer + + + + + + + \ No newline at end of file diff --git "a/Cute-Demo/9.\345\276\256\344\277\241\346\264\273\345\212\250\346\211\223\345\215\241\347\273\237\350\256\241\345\267\245\345\205\267.md" "b/Cute-Demo/9.\345\276\256\344\277\241\346\264\273\345\212\250\346\211\223\345\215\241\347\273\237\350\256\241\345\267\245\345\205\267.md" new file mode 100644 index 00000000..ed41d96b --- /dev/null +++ "b/Cute-Demo/9.\345\276\256\344\277\241\346\264\273\345\212\250\346\211\223\345\215\241\347\273\237\350\256\241\345\267\245\345\205\267.md" @@ -0,0 +1,68 @@ +## 一、获取微信后台打卡数据 + +首先登陆微信公众号后台,打开“留言管理”,选择指定文章,在控制台执行下面代码,获取当前页所有评论的用户名(不含追加评论): +```js +let list = $(".comment-nickname .weui-desktop-popover__target span span") +let result = []; +list.map(item => result.push(list[item].innerText)); +console.log(result); +``` + +然后将获取到的结果复制出来,循环统计出当前文章所有评论。 + +## 二、清洗评论数据,获取打卡排名 + +```js +const list = [ + ["1988", "1988", "王安安", "An", "1988", "null", "草莽", "Jane", "十三", "华少", "爱与坦诚", "李晓怡", "团子哒哒🍃", "丶晨晨虎", "李志新🇨🇳", "榭桦 - 瞧", "iRos", "G·Zhang", "溦澌", "轻舟", "埼玉", "迷鹿不迷路", "虎山行我看行", "来一碗酒", "我带你看星空", "L.Y", "Hello World", "Mr.Bear จุ๊บ", "Thee", "ρёρsi", "黄焖Jimmy饭", "d(ŐдŐ๑)", "A23-187", "Van Persie", "shawn.Yon", "阿樂", "初衷℡", "浮生未ジ歇", "尘埃"], + + ["来一碗酒", "1988", "丶晨晨虎", "虎山行我看行", "Martin Ager Adams", "王安安", "An", "爱与坦诚", "Jane", "十三", "草莽", "溦澌", "初衷℡", "榭桦 - 瞧", "黄焖Jimmy饭", "A23-187", "迷鹿不迷路", "Van Persie", "王安安", "An", "华少", "牛志伟(Vincen)", "轻舟", "陌上千尘", "iRos", "浮生未ジ歇", "团子哒哒🍃", "Mr.Bear จุ๊บ"], + + ["郁~朵", "来一碗酒", "Null", "Jane", "榭桦 - 瞧", "Van Persie", "爱与坦诚", "王安安", "An", "A23-187", "V3", "华少", "溦澌", "陌上千尘", "轻舟", "啊啊啊啊啊阿迪", "团子哒哒🍃", "迷鹿不迷路", "iRos", "浮生未ジ歇", "初衷℡", "上善", "1988", "十三", "黄焖Jimmy饭", "丶晨晨虎", "Mr.Bear จุ๊บ", "虎山行我看行"], + + ["草莽", "Van Persie", "榭桦 - 瞧", "黄焖Jimmy饭", "*^_^*小鑫", "A23-187", "王安安", "An", "浮生未ジ歇", "溦澌", "来一碗酒", "啊啊啊啊啊阿迪", "V3", "华少", "爱与坦诚", "迷鹿不迷路", "丶晨晨虎", "李晓怡", "李晓怡", "Jane", "十三", "虎山行我看行", "轻舟", "团子哒哒🍃", "初衷℡", "iRos", "陌上千尘", "1988", "Mr.Bear จุ๊บ"], + + ["Henson", "V3", "黄焖Jimmy饭", "Van Persie", "团子哒哒🍃", "啊啊啊啊啊阿迪", "爱与坦诚", "陌上千尘", "初衷℡", " ", "轻舟", "虎山行我看行", "迷鹿不迷路", "iRos", "1988", "大黄", "浮生未ジ歇", "A23-187", "PAN", "Mr.Bear จุ๊บ", "丶晨晨虎", "Jane", "Jane", "十三"], + + ["丶晨晨虎", "虎山行我看行", "轻舟", "A23-187", "团子哒哒🍃", "王安安", "An", "1988", "陌上千尘", "Van Persie", "榭桦 - 瞧", "迷鹿不迷路", "林小牛", "啊啊啊啊啊阿迪", "浮生未ジ歇", "V3", "Mr.Bear จุ๊บ", "溦澌", "Thee", "华少"], + + ["初衷℡", "A23-187", "浮生未ジ歇", "团子哒哒🍃", "榭桦 - 瞧", "Van Persie", "V3", "溦澌", "啊啊啊啊啊阿迪", "Thee", "Mr.Bear จุ๊บ", "华少"] +] + +let all = {}; +list.map(item => { + item = Array.from(new Set(item)); + item.map(a => { + if(!all[a]){ + all[a] = 1; + }else{ + all[a] += 1; + } + }) +}) + +let result = {}; +for(let key in all){ + if(all[key] >= 5){ + result[key] = all[key]; + } +} +let resultList = []; +for(let key in result){ + resultList.push({["用户昵称"]: key, ["打卡次数"]: result[key]}); +} +resultList = resultList.sort((a, b) => { + return b["打卡次数"] - a["打卡次数"]; +}) +console.log(`符合这次活动条件的人数有:${resultList.length}人!`) +console.log(`名单如下:`) +console.table(resultList) +``` + +## 三、注意 +如果出现符号错误,可以先将下面两个符号替换一下: + +``` +“ => " +” => " +``` \ No newline at end of file diff --git a/Cute-Demo/dist/21.middler-dome.dev.js b/Cute-Demo/dist/21.middler-dome.dev.js new file mode 100644 index 00000000..cc727990 --- /dev/null +++ b/Cute-Demo/dist/21.middler-dome.dev.js @@ -0,0 +1,35 @@ +"use strict"; + +var c1 = function c1(next) { + console.log('[c1-1]'); + next(); + console.log('[c1-2]'); +}; + +var c2 = function c2(next) { + console.log('[c2-1]'); + next(); + console.log('[c2-2]'); +}; + +var runCallbacks = function runCallbacks(cbs) { + if (Array.isArray(cbs)) { + var idx = 0; + + var next = function next() { + var cb = cbs[idx++]; + typeof cb === 'function' && cb.call(cb, next); + }; + + next(); + } +}; + +runCallbacks([c1, c2]); +/** + 输出结果: + [c1-1] + [c1-2] + [c2-2] + [c2-1] + */ \ No newline at end of file diff --git a/Cute-Demo/searchFiles/source/search_current_file.js b/Cute-Demo/searchFiles/source/search_current_file.js new file mode 100644 index 00000000..fd15738a --- /dev/null +++ b/Cute-Demo/searchFiles/source/search_current_file.js @@ -0,0 +1,312 @@ +/** + * @author leo + * @date 2019.02.12 + * @update 2019.03.12 + * 温馨提示: + * 代码千万行, + * 注释第一行。 + * 命名不规范, + * 同事两行泪。 + */ + + /** + * 1. 用途: + * 检索项目中包含指定标签或类名的文件,并返回文件路径列表。 + * 检索方式:查找指定的标签或者类名两种方式。 + * 2. 使用: + * 在本文件路径执行命令“node search_current_file.js”搜索和保存结果, + * 然后在本文件路径执行命令“python search_current_file_python.py”绘制可视化结果。 + * 参照生成的“search_result_number.txt”结果文件,和可视化结果的图片对比。 + */ + +var fs = require('fs'); +var path = require('path'); +var _ = require('lodash'); +var Excel = require('exceljs'); +var XLSX = require('xlsx'); // https://www.npmjs.com/package/xlsx + + +var filterFile = ['.html']; // 需要检索的文件类型 +var filterDir = ['lib']; // 需要排除的文件夹 +var labelArray = ['']; // 需要检索的标签数组 (暂不支持) +var classArray = [ // 需要检索的类名数组 (如果是标签的话 可以加个<符号表示标签开始) + 'search-holder','exe-bar-search','输入搜索内容','= 0 ){ + getCurrentFile(c_path, item); + // fileArr.push(path.resolve(__dirname, item)) + } + } + }); + }); + return fileArr; +} + +/** + * 获取当前文件内容 + * @param {string} paths 文件的路径 + * @param {string} filename 文件名 + */ +var getCurrentFile = function(paths, filename){ + fs.readFile(paths, 'utf8', function(err, data){ + allFileNumber ++; + // console.log('正在检索第'+ allFileNumber +'个文件,文件名:【'+ filename +'】'); + if (err) console.log(err); + searchCurrentFile(data, paths); + }); +}; + +/** + * 检索当前文件内容 + * @param {string} data 文件的内容 + * @param {string} paths 文件的路径 + */ +var searchCurrentFile = function(data, paths){ + _.forEach(classArray, function(val){ + // 清楚单引号和双引号的影响 + data.replace(/'/g,''); + data.replace(/"/g,''); + if(data.indexOf(val) >= 0){ + // console.log('--------------检索到结果,路径为:'+paths) + resultArray.push(paths); + resultAlassify[val].push(paths); + } + }); +}; + +/** + * 转成JSON数据,用来数据可视化 + * @param {object} data 需要处理的数据 + */ +var saveDataToJson = function (data){ + var result = {}; + console.log('*************************转成JSON数据开始*************************') + // 第一层分组 + result = _.groupBy(data, function(item){ + item = item.replace(filePath+'\\',''); + var list = item.split('\\'); + return list[0]; + }); + // 第二层分组 + for(var k in result){ + result[k] = _.groupBy(result[k], function(i){ + i = i.replace(filePath+'\\',''); + var r = i.split('\\'); + return r[1]; + }); + } + for(var i in result){ + for(var m in result[i]){ + for(var n in result[i][m]){ + var currentPath = result[i][m][n].replace(filePath+'\\',''); + currentPath = currentPath.replace(/\\/g, '/'); + var current = excelFileObj[currentPath]; + result[i][m][n] = { + title : current ? current['路由名称'] : '该文件为模块', + path : current ? current['文件路径'] : currentPath, + url : current ? current['url'] : '该文件为模块', + params: current ? current['路由参数'] : '该文件为模块', + ctrl : current ? current['控制器名称'] : '该文件为模块', + urls : current ? current['url'] : '该文件为模块', + }; + } + } + } + + setJSONFile(result); // 保存JSON文件 + setEachDirFileNum(result); // 计算每个子文件夹下文件数量 + console.log('*************************转成JSON数据结束*************************') +}; + + +/** + * 保存JSON文件 + * @param {object} data 需要处理的数据 + */ +var setJSONFile = function (data){ + fs.writeFile(saveJSONName , JSON.stringify(data), 'utf8' , function(err){ + if(err) console.log(err); + console.log(saveJSONName + ' JSON文件保存成功!'); + }) +}; + + +/** + * 计算每个子文件夹中文件数量 + * @param {object} data 需要处理的数据 + */ +var setEachDirFileNum = function(data){ + var text = '', result = {}; // 将JSON具体数据转成对象 + for(var k in data){ + text += '-----------------文件夹【' + k + '】----------------------\r\n'; + dirFileNumber[k] = {}; + result[k] = {}; + for(var i in data[k]){ + dirFileNumber[k][i] = data[k][i].length; + result[k][i] = []; + text += '---子文件夹(' + i + ')(有子文件' + data[k][i].length + '个),分别是:---\r\n' + for(var m in data[k][i]){ + result[k][i].push(data[k][i][m]); + text += data[k][i][m].path.toString().replace(/,/g,'\r\n') + '\r\n'; + } + text += '------------------------------------------------\r\n'; + } + text += '------------------------end---------------------\r\n\r\n'; + } + setExcelFile(result); // 保存Excel文件 + fs.writeFile(dirFileNumName , text, 'utf8' , function(err){ + if(err) console.log(err); + console.log(dirFileNumName+' 数量统计文件保存成功!'); + }) +}; + +/** + * 保存Excel数据 + * @param {object} data 需要处理的数据 + * return excelFileName.xlsx + */ +var setExcelFile = function(data){ + var workbook = new Excel.Workbook(); + workbook.creator = 'EXE'; + workbook.lastModifiedBy = 'Leo'; + workbook.created = new Date(); + workbook.modified = new Date(); + workbook.lastPrinted = new Date(); + for(var item in data){ // 第一层循环 外层文件夹 templates views + for(var list in data[item]){ + var worksheet = workbook.addWorksheet(list.toUpperCase()); + worksheet.columns = [ + { header: '页面标题' , key: 'title' , width: 40 }, + { header: '文件路径' , key: 'path' , width: 60 }, + { header: '路由地址' , key: 'url' , width: 40 }, + { header: '路由参数' , key: 'params', width: 40 }, + { header: '控制器名称', key: 'ctrl' , width: 40 }, + { header: 'url' , key: 'urls' , width: 40 }, + ]; + var rowData = data[item][list]; + for(var row in rowData){ + worksheet.addRow({ + title : rowData[row].title, + path : rowData[row].path, + url : rowData[row].url, + params: rowData[row].params, + ctrl : rowData[row].ctrl, + urls : rowData[row].urls, + }) + } + } + + } + + workbook.xlsx.writeFile(path.join(__dirname, excelFileName)) + .then(function() { + console.log(excelFileName + ' 文件生成成功'); + console.log('下一步生成饼图,请执行(python search_current_file_python.py)'); + }); +}; + + +/** + * 读取Excel数据 + * get excelReadName + */ +var getExcelFile = function(){ + return new Promise(function(resolve, reject){ + var excelPath = path.join(__dirname, excelReadName); + fs.exists(excelPath, function(exists){ + if(exists){ + var workbook = XLSX.readFile(excelPath, {type: 'base64'});// 获取 Excel 中所有表名 + var sheetNames = workbook.SheetNames; + resolve({workbook: workbook, sheetNames: sheetNames}); + }else{ + reject({message:'错误提示:请先获取路由列表文件!(执行node get_router.js)'}); + } + }); + }) +}; + +/** + * 解析Excel数据 + * @param {object} workbook excel工作区数据 + * @param {object} sheetNames excel工作表名数据 + */ +var getEachSheet = function(workbook, sheetNames){ + _.forEach(sheetNames,function(item,index){ + var sheet = workbook.Sheets[sheetNames[index]]; + var json = XLSX.utils.sheet_to_json(sheet); // 针对单个表,返回序列化json数据 + excelFileArr = excelFileArr.concat(json); // 不能使用lodash的_.concat 因为lodash版本太低 + }) + _.forEach(excelFileArr, function(val, key){ + excelFileObj[val['文件路径']] = val; + // fs.writeFile('excelFileArr.txt' , JSON.stringify(excelFileObj), 'utf8' , function(err){ + // if(err) console.log(err); + // console.log(' excelFileArr文件保存成功!'); + // }) + }); + +} + +/** + * 初始化项目 + */ +var init = function (){ + console.log('^-^ 程序运行开始!'); + getExcelFile().then(function(data){ + getEachSheet(data.workbook, data.sheetNames); // 读取Excel文件 + getCurrenAllFile(filePath); + setTimeout(function(){ + var text = ''; + resultArray = _.uniq(resultArray);// 对结果进行去重 + saveDataToJson(resultArray); // 整理成JSON格式 + text = '-----------------所有检索结果(已去重)(共' + resultArray.length+ '个)----------------------\r\n' + + resultArray.toString().replace(/,/g,'\r\n') + '\r\n' + + '------------------------------------------------\r\n\r\n'; + _.forEach(classArray, function(val, key){ + text = text + + '-------关键词【' + val + '】检索结果(共' + resultAlassify[val].length+ '个)-------\r\n' + + resultAlassify[val].toString().replace(/,/g,'\r\n') + '\r\n' + + '------------------------------------------------\r\n\r\n'; + }); + fs.writeFile(saveFileName , text, 'utf8' , function(err){ + if(err) console.log(err); + console.log(saveFileName + ' 检索结果文件保存成功!'); + console.log('^-^ 程序运行结束!'); + }) + },2000) + }).catch(function(err){ + console.log(err.message) + }); +}; + +init(); \ No newline at end of file diff --git a/Cute-Demo/searchFiles/source/search_current_file_1552015137658.txt b/Cute-Demo/searchFiles/source/search_current_file_1552015137658.txt new file mode 100644 index 00000000..37c08edf --- /dev/null +++ b/Cute-Demo/searchFiles/source/search_current_file_1552015137658.txt @@ -0,0 +1,196 @@ +-----------------所有检索结果(已去重)(共58个)---------------------- +G:\exe-v1-1\exe-app\develop\templates\components\search.html +G:\exe-v1-1\exe-app\develop\views\activity\expo-activities.html +G:\exe-v1-1\exe-app\develop\views\competency-task\choose-ability-modal.html +G:\exe-v1-1\exe-app\develop\views\competency-task\choose-competency.html +G:\exe-v1-1\exe-app\develop\views\competency-task\search-ability-modal.html +G:\exe-v1-1\exe-app\develop\views\competency-task\search-teacher.html +G:\exe-v1-1\exe-app\develop\views\course-center\learning-classify-detail.html +G:\exe-v1-1\exe-app\develop\views\course-center\learning-column-detail.html +G:\exe-v1-1\exe-app\develop\views\course-center\learning-library.html +G:\exe-v1-1\exe-app\develop\views\course-center\learning-search.html +G:\exe-v1-1\exe-app\develop\views\courses-distribute\courses-input.html +G:\exe-v1-1\exe-app\develop\views\courses-distribute\select-award.html +G:\exe-v1-1\exe-app\develop\views\courses-distribute\train-plan-search.html +G:\exe-v1-1\exe-app\develop\views\courses-distribute\train-plan.html +G:\exe-v1-1\exe-app\develop\views\found\employee-account-manage.html +G:\exe-v1-1\exe-app\develop\views\helpandfeedback\index.html +G:\exe-v1-1\exe-app\develop\views\helpandfeedback\search.html +G:\exe-v1-1\exe-app\develop\views\home\contact.html +G:\exe-v1-1\exe-app\develop\views\home\home.html +G:\exe-v1-1\exe-app\develop\views\home\search-model.html +G:\exe-v1-1\exe-app\develop\views\home\search.html +G:\exe-v1-1\exe-app\develop\views\im\im-contact.html +G:\exe-v1-1\exe-app\develop\views\knowledge\knowledge-all.html +G:\exe-v1-1\exe-app\develop\views\knowledge\knowledge-sort.html +G:\exe-v1-1\exe-app\develop\views\knows\add-interest-label.html +G:\exe-v1-1\exe-app\develop\views\knows\invite-contact.html +G:\exe-v1-1\exe-app\develop\views\knows\label-search.html +G:\exe-v1-1\exe-app\develop\views\knows\select-label-list.html +G:\exe-v1-1\exe-app\develop\views\learning\learn-map.html +G:\exe-v1-1\exe-app\develop\views\learning\learning-search.html +G:\exe-v1-1\exe-app\develop\views\learning\learning.html +G:\exe-v1-1\exe-app\develop\views\learning-path\index.html +G:\exe-v1-1\exe-app\develop\views\learning\teacher-search.html +G:\exe-v1-1\exe-app\develop\views\learning-path\learning-choose-path.html +G:\exe-v1-1\exe-app\develop\views\learning-path\learning-path-search.html +G:\exe-v1-1\exe-app\develop\views\learning-path\learning-path-user-map.html +G:\exe-v1-1\exe-app\develop\views\learning-path\learning-path-user-search.html +G:\exe-v1-1\exe-app\develop\views\live\live.html +G:\exe-v1-1\exe-app\develop\views\live\live-show-search.html +G:\exe-v1-1\exe-app\develop\views\mobile-report\learn-map-choice.html +G:\exe-v1-1\exe-app\develop\views\courses-distribute\courses-choose.html +G:\exe-v1-1\exe-app\develop\views\product-zone\product-search.html +G:\exe-v1-1\exe-app\develop\views\product-zone\product-zone.html +G:\exe-v1-1\exe-app\develop\views\store\check-history-search.html +G:\exe-v1-1\exe-app\develop\views\store\check-table.html +G:\exe-v1-1\exe-app\develop\views\store\contact-tree.html +G:\exe-v1-1\exe-app\develop\views\store\index.html +G:\exe-v1-1\exe-app\develop\views\store\position-contact-tree.html +G:\exe-v1-1\exe-app\develop\views\store\search-all.html +G:\exe-v1-1\exe-app\develop\views\store\store-address.html +G:\exe-v1-1\exe-app\develop\views\store\store-contact-tree.html +G:\exe-v1-1\exe-app\develop\views\store\store-member.html +G:\exe-v1-1\exe-app\develop\views\store\store-search.html +G:\exe-v1-1\exe-app\develop\views\voice\voice-search.html +G:\exe-v1-1\exe-app\develop\views\workdiary\select-cc-user.html +G:\exe-v1-1\exe-app\develop\views\workdiary\work-contact-init.html +G:\exe-v1-1\exe-app\develop\views\workdiary\work-contact.html +G:\exe-v1-1\exe-app\develop\templates\modules\found\account-add-tabs.html +------------------------------------------------ + +-------关键词【search-holder】检索结果(共8个)------- +G:\exe-v1-1\exe-app\develop\views\courses-distribute\select-award.html +G:\exe-v1-1\exe-app\develop\views\courses-distribute\train-plan.html +G:\exe-v1-1\exe-app\develop\views\home\home.html +G:\exe-v1-1\exe-app\develop\views\learning\learning.html +G:\exe-v1-1\exe-app\develop\views\learning-path\index.html +G:\exe-v1-1\exe-app\develop\views\learning-path\learning-path-user-map.html +G:\exe-v1-1\exe-app\develop\views\mobile-report\learn-map-choice.html +G:\exe-v1-1\exe-app\develop\views\courses-distribute\courses-choose.html +------------------------------------------------ + +-------关键词【exe-bar-search】检索结果(共2个)------- +G:\exe-v1-1\exe-app\develop\templates\components\search.html +G:\exe-v1-1\exe-app\develop\views\competency-task\choose-ability-modal.html +------------------------------------------------ + +-------关键词【输入搜索内容】检索结果(共41个)------- +G:\exe-v1-1\exe-app\develop\views\activity\expo-activities.html +G:\exe-v1-1\exe-app\develop\views\competency-task\choose-ability-modal.html +G:\exe-v1-1\exe-app\develop\views\competency-task\search-teacher.html +G:\exe-v1-1\exe-app\develop\views\courses-distribute\courses-input.html +G:\exe-v1-1\exe-app\develop\views\courses-distribute\select-award.html +G:\exe-v1-1\exe-app\develop\views\courses-distribute\train-plan-search.html +G:\exe-v1-1\exe-app\develop\views\courses-distribute\train-plan.html +G:\exe-v1-1\exe-app\develop\views\found\employee-account-manage.html +G:\exe-v1-1\exe-app\develop\views\helpandfeedback\index.html +G:\exe-v1-1\exe-app\develop\views\helpandfeedback\search.html +G:\exe-v1-1\exe-app\develop\views\home\contact.html +G:\exe-v1-1\exe-app\develop\views\home\home.html +G:\exe-v1-1\exe-app\develop\views\home\search.html +G:\exe-v1-1\exe-app\develop\views\knowledge\knowledge-all.html +G:\exe-v1-1\exe-app\develop\views\knowledge\knowledge-sort.html +G:\exe-v1-1\exe-app\develop\views\knows\add-interest-label.html +G:\exe-v1-1\exe-app\develop\views\knows\label-search.html +G:\exe-v1-1\exe-app\develop\views\knows\select-label-list.html +G:\exe-v1-1\exe-app\develop\views\learning\learn-map.html +G:\exe-v1-1\exe-app\develop\views\learning\learning.html +G:\exe-v1-1\exe-app\develop\views\learning-path\index.html +G:\exe-v1-1\exe-app\develop\views\learning-path\learning-choose-path.html +G:\exe-v1-1\exe-app\develop\views\learning-path\learning-path-search.html +G:\exe-v1-1\exe-app\develop\views\learning-path\learning-path-user-map.html +G:\exe-v1-1\exe-app\develop\views\learning-path\learning-path-user-search.html +G:\exe-v1-1\exe-app\develop\views\live\live-show-search.html +G:\exe-v1-1\exe-app\develop\views\mobile-report\learn-map-choice.html +G:\exe-v1-1\exe-app\develop\views\courses-distribute\courses-choose.html +G:\exe-v1-1\exe-app\develop\views\product-zone\product-search.html +G:\exe-v1-1\exe-app\develop\views\product-zone\product-zone.html +G:\exe-v1-1\exe-app\develop\views\store\check-table.html +G:\exe-v1-1\exe-app\develop\views\store\contact-tree.html +G:\exe-v1-1\exe-app\develop\views\store\index.html +G:\exe-v1-1\exe-app\develop\views\store\position-contact-tree.html +G:\exe-v1-1\exe-app\develop\views\store\search-all.html +G:\exe-v1-1\exe-app\develop\views\store\store-contact-tree.html +G:\exe-v1-1\exe-app\develop\views\store\store-search.html +G:\exe-v1-1\exe-app\develop\views\voice\voice-search.html +G:\exe-v1-1\exe-app\develop\views\workdiary\select-cc-user.html +G:\exe-v1-1\exe-app\develop\views\workdiary\work-contact-init.html +G:\exe-v1-1\exe-app\develop\templates\modules\found\account-add-tabs.html +------------------------------------------------ + +-------关键词【","path":"views/activity/expo-activities.html","url":"/expo-activity","params":"{}","ctrl":"ExpoActivityCtrl","urls":"/expo-activity"}],"competency-task":[{"title":"<代码内没有名称>","path":"views/competency-task/choose-competency.html","url":"/choose-competency","params":"{}","ctrl":"chooseCompetencyCtrl","urls":"/choose-competency"},{"title":"该文件为模块","path":"views/competency-task/choose-ability-modal.html","url":"该文件为模块","params":"该文件为模块","ctrl":"该文件为模块","urls":"该文件为模块"},{"title":"该文件为模块","path":"views/competency-task/search-ability-modal.html","url":"该文件为模块","params":"该文件为模块","ctrl":"该文件为模块","urls":"该文件为模块"},{"title":"<代码内没有名称>","path":"views/competency-task/search-teacher.html","url":"/search-teacher","params":"{}","ctrl":"searchTeacherCtrl","urls":"/search-teacher"}],"course-center":[{"title":"分类详情","path":"views/course-center/learning-classify-detail.html","url":"/learn-classify-detail","params":"{}","ctrl":"ClassifyDetailCtrl","urls":"/learn-classify-detail"},{"title":"栏目详情","path":"views/course-center/learning-column-detail.html","url":"/learn-column-detail","params":"{}","ctrl":"ColumnDetailCtrl","urls":"/learn-column-detail"},{"title":"课程中心","path":"views/course-center/learning-library.html","url":"/learning-library","params":"{}","ctrl":"LearningLibraryCtrl","urls":"/learning-library"},{"title":"课程搜索","path":"views/course-center/learning-search.html","url":"/learning-search","params":"{}","ctrl":"LearnSearchCtrl","urls":"/learning-search"}],"courses-distribute":[{"title":"选择课程","path":"views/courses-distribute/courses-choose.html","url":"/courses-choose","params":"{}","ctrl":"CoursesChooseCtrl","urls":"/courses-choose"},{"title":"选择课程","path":"views/courses-distribute/courses-input.html","url":"/courses-input","params":"{}","ctrl":"CoursesInputCtrl","urls":"/courses-input"},{"title":"添加奖励","path":"views/courses-distribute/select-award.html","url":"/select-award","params":"{}","ctrl":"SelectAwardCtrl","urls":"/select-award"},{"title":"培训安排搜索","path":"views/courses-distribute/train-plan-search.html","url":"/train-plan-search","params":"{}","ctrl":"TrainPlanSearchCtrl","urls":"/train-plan-search"},{"title":"培训安排","path":"views/courses-distribute/train-plan.html","url":"/train-plan","params":"{}","ctrl":"TrainPlanCtrl","urls":"/train-plan"}],"found":[{"title":"员工账号管理","path":"views/found/employee-account-manage.html","url":"/employee-account-manage","params":"{}","ctrl":"EmployeeAccountManageCtrl","urls":"/employee-account-manage"}],"helpandfeedback":[{"title":"帮助与反馈","path":"views/helpandfeedback/index.html","url":"/help-and-feedback","params":"{}","ctrl":"HelpAndFeedbackCtrl","urls":"/help-and-feedback"},{"title":"问题搜索","path":"views/helpandfeedback/search.html","url":"/help-and-feedback-search","params":"{}","ctrl":"HelpAndFeedbackSearchCtrl","urls":"/help-and-feedback-search"}],"home":[{"title":"通讯录","path":"views/home/contact.html","url":"/contact","params":"{}","ctrl":"ContactCtrl","urls":"/contact"},{"title":"首页","path":"views/home/home.html","url":"/index","params":"{}","ctrl":"HomeCtrl","urls":"/index"},{"title":"搜索","path":"views/home/search-model.html","url":"/search-model","params":"{}","ctrl":"SearchModelCtrl","urls":"/search-model"},{"title":"搜索","path":"views/home/search.html","url":"/search","params":"{}","ctrl":"SearchCtrl","urls":"/search"}],"knowledge":[{"title":"全部知识","path":"views/knowledge/knowledge-all.html","url":"/knowledge-all","params":"{}","ctrl":"AllKnowledgeCtrl","urls":"/knowledge-all"},{"title":"知识分类","path":"views/knowledge/knowledge-sort.html","url":"/knowledge-sort","params":"{}","ctrl":"KnowledgeSortCtrl","urls":"/knowledge-sort"}],"knows":[{"title":"添加兴趣标签","path":"views/knows/add-interest-label.html","url":"/add-interest-label","params":"{}","ctrl":"AddInterestLabelCtrl","urls":"/add-interest-label"},{"title":"邀请选择","path":"views/knows/invite-contact.html","url":"/invite-select","params":"{}","ctrl":"InviteSelectCtrl","urls":"/invite-select"},{"title":"标签搜索","path":"views/knows/label-search.html","url":"/label-search","params":"{}","ctrl":"LabelSearchCtrl","urls":"/label-search"},{"title":"选择问题标签列表","path":"views/knows/select-label-list.html","url":"/select-label-list","params":"{}","ctrl":"SelectLabelListCtrl","urls":"/select-label-list"}],"learning":[{"title":"学习地图","path":"views/learning/learn-map.html","url":"/learning-map","params":"{}","ctrl":"LearnMapCtrl","urls":"/learning-map"},{"title":"该文件为模块","path":"views/learning/learning-search.html","url":"该文件为模块","params":"该文件为模块","ctrl":"该文件为模块","urls":"该文件为模块"},{"title":"学习","path":"views/learning/learning.html","url":"/learn","params":"{}","ctrl":"LearningCtrl","urls":"/learn"},{"title":"讲师库排行榜","path":"views/learning/teacher-search.html","url":"/teacher-search","params":"{}","ctrl":"teacherSearchCtrl","urls":"/teacher-search"}],"learning-path":[{"title":"员工学习路径管理","path":"views/learning-path/index.html","url":"/learning-path","params":"{}","ctrl":"LearningPathCtrl","urls":"/learning-path"},{"title":"选择学习地图路径","path":"views/learning-path/learning-choose-path.html","url":"/learning-choose-path","params":"{}","ctrl":"LearningChoosePathCtrl","urls":"/learning-choose-path"},{"title":"学习路径图","path":"views/learning-path/learning-path-search.html","url":"/learning-path-search","params":"{}","ctrl":"LearningPathSearchCtrl","urls":"/learning-path-search"},{"title":"学习路径图","path":"views/learning-path/learning-path-user-map.html","url":"/learning-path-user-map","params":"{}","ctrl":"LearningPathUserMapCtrl","urls":"/learning-path-user-map"},{"title":"学习路径图","path":"views/learning-path/learning-path-user-search.html","url":"/learning-path-user-search","params":"{}","ctrl":"LearningPathUserSearchCtrl","urls":"/learning-path-user-search"}],"live":[{"title":"该文件为模块","path":"views/live/live.html","url":"该文件为模块","params":"该文件为模块","ctrl":"该文件为模块","urls":"该文件为模块"},{"title":"该文件为模块","path":"views/live/live-show-search.html","url":"该文件为模块","params":"该文件为模块","ctrl":"该文件为模块","urls":"该文件为模块"}],"mobile-report":[{"title":"选择地图","path":"views/mobile-report/learn-map-choice.html","url":"/learn-map-choice","params":"{}","ctrl":"LearnMapChoiceCtrl","urls":"/learn-map-choice"}],"im":[{"title":"IM","path":"views/im/im-contact.html","url":"/im-contact","params":"{}","ctrl":"IMContactCtrl","urls":"/im-contact"}],"product-zone":[{"title":"产品搜索","path":"views/product-zone/product-search.html","url":"/product-search","params":"{}","ctrl":"ProductSearchCtrl","urls":"/product-search"},{"title":"产品专区","path":"views/product-zone/product-zone.html","url":"/product-zone","params":"{}","ctrl":"ProductZoneCtrl","urls":"/product-zone"}],"store":[{"title":"该文件为模块","path":"views/store/check-history-search.html","url":"该文件为模块","params":"该文件为模块","ctrl":"该文件为模块","urls":"该文件为模块"},{"title":"选择检核表","path":"views/store/check-table.html","url":"/check-table","params":"{}","ctrl":"StoreCheckTable","urls":"/check-table"},{"title":"选择检核人","path":"views/store/contact-tree.html","url":"/contact-select","params":"{}","ctrl":"ContactSelectCtrl","urls":"/contact-select"},{"title":"门店管理","path":"views/store/index.html","url":"/store-index","params":"{}","ctrl":"StoreCtrl","urls":"/store-index"},{"title":"选择职位","path":"views/store/position-contact-tree.html","url":"/contact-position","params":"{}","ctrl":"ContactPositionCtrl","urls":"/contact-position"},{"title":"搜索","path":"views/store/search-all.html","url":"/store-search-all","params":"{}","ctrl":"SearchAllCtrl","urls":"/store-search-all"},{"title":"录入地址","path":"views/store/store-address.html","url":"/store-address","params":"{}","ctrl":"StoreAddressCtrl","urls":"/store-address"},{"title":"选择门店","path":"views/store/store-contact-tree.html","url":"/contact-store","params":"{}","ctrl":"ContactStoreCtrl","urls":"/contact-store"},{"title":"<代码内没有名称>","path":"views/store/store-member.html","url":"/store-member","params":"{}","ctrl":"StoreMemberCtrl","urls":"/store-member"},{"title":"搜索","path":"views/store/store-search.html","url":"/store-search","params":"{}","ctrl":"StoreSearchCtrl","urls":"/store-search"}],"voice":[{"title":"一线声音-反馈搜索","path":"views/voice/voice-search.html","url":"/voice-search","params":"{}","ctrl":"VoiceSearchCtrl","urls":"/voice-search"}],"workdiary":[{"title":"选择抄送对象","path":"views/workdiary/select-cc-user.html","url":"/select-cc-user","params":"{}","ctrl":"SelectCCUserCtrl","urls":"/select-cc-user"},{"title":"选择负责人","path":"views/workdiary/work-contact-init.html","url":"/task-contact-primary","params":"{}","ctrl":"TaskPrimaryCtrl","urls":"/task-contact-primary"},{"title":"选择抄送人","path":"views/workdiary/work-contact.html","url":"/task-contact","params":"{}","ctrl":"TaskContactCtrl","urls":"/task-contact"}]}} \ No newline at end of file diff --git a/Cute-Demo/searchFiles/source/search_current_file_number.txt b/Cute-Demo/searchFiles/source/search_current_file_number.txt new file mode 100644 index 00000000..dc8449ea --- /dev/null +++ b/Cute-Demo/searchFiles/source/search_current_file_number.txt @@ -0,0 +1,104 @@ +-----------------文件夹【templates】---------------------- +---子文件夹(components)(有子文件1个),分别是:--- +templates/components/search.html +------------------------------------------------ +---子文件夹(modules)(有子文件1个),分别是:--- +templates/modules/found/account-add-tabs.html +------------------------------------------------ +------------------------end--------------------- + +-----------------文件夹【views】---------------------- +---子文件夹(activity)(有子文件1个),分别是:--- +views/activity/expo-activities.html +------------------------------------------------ +---子文件夹(competency-task)(有子文件4个),分别是:--- +views/competency-task/choose-competency.html +views/competency-task/choose-ability-modal.html +views/competency-task/search-ability-modal.html +views/competency-task/search-teacher.html +------------------------------------------------ +---子文件夹(course-center)(有子文件4个),分别是:--- +views/course-center/learning-classify-detail.html +views/course-center/learning-column-detail.html +views/course-center/learning-library.html +views/course-center/learning-search.html +------------------------------------------------ +---子文件夹(courses-distribute)(有子文件5个),分别是:--- +views/courses-distribute/courses-choose.html +views/courses-distribute/courses-input.html +views/courses-distribute/select-award.html +views/courses-distribute/train-plan-search.html +views/courses-distribute/train-plan.html +------------------------------------------------ +---子文件夹(found)(有子文件1个),分别是:--- +views/found/employee-account-manage.html +------------------------------------------------ +---子文件夹(helpandfeedback)(有子文件2个),分别是:--- +views/helpandfeedback/index.html +views/helpandfeedback/search.html +------------------------------------------------ +---子文件夹(home)(有子文件4个),分别是:--- +views/home/contact.html +views/home/home.html +views/home/search-model.html +views/home/search.html +------------------------------------------------ +---子文件夹(knowledge)(有子文件2个),分别是:--- +views/knowledge/knowledge-all.html +views/knowledge/knowledge-sort.html +------------------------------------------------ +---子文件夹(knows)(有子文件4个),分别是:--- +views/knows/add-interest-label.html +views/knows/invite-contact.html +views/knows/label-search.html +views/knows/select-label-list.html +------------------------------------------------ +---子文件夹(learning)(有子文件4个),分别是:--- +views/learning/learn-map.html +views/learning/learning-search.html +views/learning/learning.html +views/learning/teacher-search.html +------------------------------------------------ +---子文件夹(learning-path)(有子文件5个),分别是:--- +views/learning-path/index.html +views/learning-path/learning-choose-path.html +views/learning-path/learning-path-search.html +views/learning-path/learning-path-user-map.html +views/learning-path/learning-path-user-search.html +------------------------------------------------ +---子文件夹(live)(有子文件2个),分别是:--- +views/live/live.html +views/live/live-show-search.html +------------------------------------------------ +---子文件夹(mobile-report)(有子文件1个),分别是:--- +views/mobile-report/learn-map-choice.html +------------------------------------------------ +---子文件夹(im)(有子文件1个),分别是:--- +views/im/im-contact.html +------------------------------------------------ +---子文件夹(product-zone)(有子文件2个),分别是:--- +views/product-zone/product-search.html +views/product-zone/product-zone.html +------------------------------------------------ +---子文件夹(store)(有子文件10个),分别是:--- +views/store/check-history-search.html +views/store/check-table.html +views/store/contact-tree.html +views/store/index.html +views/store/position-contact-tree.html +views/store/search-all.html +views/store/store-address.html +views/store/store-contact-tree.html +views/store/store-member.html +views/store/store-search.html +------------------------------------------------ +---子文件夹(voice)(有子文件1个),分别是:--- +views/voice/voice-search.html +------------------------------------------------ +---子文件夹(workdiary)(有子文件3个),分别是:--- +views/workdiary/select-cc-user.html +views/workdiary/work-contact-init.html +views/workdiary/work-contact.html +------------------------------------------------ +------------------------end--------------------- + diff --git a/Cute-Demo/searchFiles/source/search_current_file_python.py b/Cute-Demo/searchFiles/source/search_current_file_python.py new file mode 100644 index 00000000..80be779f --- /dev/null +++ b/Cute-Demo/searchFiles/source/search_current_file_python.py @@ -0,0 +1,86 @@ +import json +import matplotlib.mlab as mlab +import matplotlib.pyplot as plt +import numpy as np + +plt.rcParams['font.sans-serif']=['SimHei'] # 解决中文乱码 + +saveImgName = 'search_result.png' +file = open('./search_current_file_json.json','r', encoding='utf-8') +file = json.load(file) + +keyName = [] # 需要显示的分类图表(按外层文件夹) +selectName = '' # 用户选择的文件夹名称 + +def getKeyName(): + for name in file: + keyName.append(name) + +def pctName(pct, allvals): # 饼图数字数据格式转换 + absolute = int(round(pct/100.*np.sum(allvals))) + # print(pct, allvals,absolute) + return "{:d}个\n({:.1f}%)".format(absolute, pct) + +def drawOneChart(name, label, data): + plt_title = name + plt.figure(figsize=(6,9)) # 调节图形大小 + labels = label # 定义标签 + sizes = data # 每块值 + colors = ['antiquewhite','aquamarine','burlywood','cadetblue','chartreuse','chocolate','coral','cornflowerblue','crimson','cyan','deeppink','deepskyblue','dimgray','dodgerblue','firebrick','forestgreen','fuchsia','gainsboro','ghostwhite','gold','goldenrod','gray','green','greenyellow','honeydew','hotpink','indianred','indigo','ivory','khaki','lavender','lavenderblush','lawngreen','lemonchiffon','lightblue','lightcoral','lightcyan','lightgoldenrodyellow','lightgreen','lightgray','lightpink','lightsalmon','lightseagreen','lightskyblue','lightslategray','lightsteelblue','lightyellow','lime','limegreen','linen','magenta','maroon','mediumaquamarine','mediumblue','mediumorchid','mediumpurple','mediumseagreen','mediumslateblue','mediumspringgreen','mediumturquoise','mediumvioletred','midnightblue','mintcream','mistyrose','moccasin','navajowhite','navy','oldlace','olive','olivedrab','orange','orangered','orchid','palegoldenrod','palegreen','paleturquoise','palevioletred','papayawhip','peachpuff','peru','pink','plum','powderblue','purple','red','rosybrown','royalblue','saddlebrown','salmon','sandybrown','seagreen','seashell','sienna','silver','skyblue','slateblue','slategray','snow','springgreen','steelblue','tan','teal','thistle','tomato','turquoise','violet','wheat','white','whitesmoke','yellow','yellowgreen'] #每块颜色定义 + explode = [] # 将某一块分割出来,值越大分割出的间隙越大 + max_data = max(sizes) + for i in sizes: # 初始化每块之间间距,最大值分割出来 + if i == max_data: + explode.append(0.2) + else: + explode.append(0) + + # 控制x轴和y轴的范围 + # plt.xlim(0,14) + # plt.ylim(0,14) + + patches,text1,text2 = plt.pie(sizes, + explode = explode, + labels = labels, + colors = colors, + autopct = lambda pct: pctName(pct, data), # 数值保留固定小数位 '%3.2f'指小数点前后位数(没有用空格补齐) + frame = 1, # 是否显示饼图的图框,这里设置显示 + shadow = True, # 无阴影设置 + labeldistance = 1.1, # 图例距圆心半径倍距离 + counterclock = False, # 是否让饼图按逆时针顺序呈现; + startangle = 90, # 逆时针起始角度设置 + pctdistance = 0.6 # 数值距圆心半径倍数距离 + ) + # patches饼图的返回值,texts1饼图外label的文本,texts2饼图内部的文本 + # x,y轴刻度设置一致,保证饼图为圆形 + + # 删除x轴和y轴的刻度 + plt.xticks(()) + plt.yticks(()) + plt.axis('equal') + plt.legend() + plt.title(plt_title+'文件夹下文件分布(顺时针)', bbox={'facecolor':'0.8', 'pad':5}) + plt.savefig(plt_title+'_'+saveImgName) # 一定放在plt.show()之前 + plt.show() + +def drawAllChart(openName): + for name in keyName: + labels = [] + values = [] + for view_name in file[name]: + labels.append(view_name) + values.append(len(file[name][view_name])) + + if openName == '' or openName == name: + drawOneChart(name, labels, values) + else: + print('输入有误') + +def init(): + getKeyName() + select = ','.join(keyName) + # 用户通过输入要查看的文件夹名称,来展示对应文件夹的饼图,默认显示所有文件夹饼图 + selectName = input('检索到的文件夹有:【' + select + '】,请输入要查看的文件夹名称(默认所有):') + drawAllChart(selectName) + +init() \ No newline at end of file diff --git a/Cute-Demo/searchFiles/source/views_search_result.png b/Cute-Demo/searchFiles/source/views_search_result.png new file mode 100644 index 00000000..804f552a Binary files /dev/null and b/Cute-Demo/searchFiles/source/views_search_result.png differ diff --git "a/Cute-Demo/searchFiles/\343\200\220\346\226\207\344\273\266\346\240\267\345\274\217\346\220\234\347\264\242\347\273\237\350\256\241\343\200\221\345\267\245\345\205\267\345\274\200\345\217\221\346\265\201\347\250\213.md" "b/Cute-Demo/searchFiles/\343\200\220\346\226\207\344\273\266\346\240\267\345\274\217\346\220\234\347\264\242\347\273\237\350\256\241\343\200\221\345\267\245\345\205\267\345\274\200\345\217\221\346\265\201\347\250\213.md" new file mode 100644 index 00000000..c3c99e02 --- /dev/null +++ "b/Cute-Demo/searchFiles/\343\200\220\346\226\207\344\273\266\346\240\267\345\274\217\346\220\234\347\264\242\347\273\237\350\256\241\343\200\221\345\267\245\345\205\267\345\274\200\345\217\221\346\265\201\347\250\213.md" @@ -0,0 +1,505 @@ +## 文章目录 +* [文章目录](#文章目录) +* [一、开发介绍](#一开发介绍) + * [1.开发背景](#1开发背景) + * [2.工具文档](#2工具文档) +* [二、开发环境搭建](#二开发环境搭建) + * [1.Nodejs环境搭建](#1nodejs环境搭建) + * [2.Python环境搭建](#2python环境搭建) +* [三、开发过程](#三开发过程) + * [1.最终效果](#1最终效果) + * [2.Nodejs开发部分](#2nodejs开发部分) + * [3.Python开发部分](#3python开发部分) +* [四、总结](#四总结) + * [1.Nodejs知识点](#1nodejs知识点) + * [2.Python知识点](#2python知识点) + * [3.拓展](#3拓展) + +## 一、开发介绍 +> 作者:leo +> 更新:2019.03.14 +> 项目源码:[github](https://github.com/pingan8787/Leo-JavaScript/tree/master/demo%E7%89%87%E6%AE%B5/%E6%90%9C%E7%B4%A2%E5%B7%A5%E5%85%B7demo/source) + +[![博客](http://images.pingan8787.com/icon_my1.png)](http://www.pingan8787.com) +[![知乎](http://images.pingan8787.com/icon_zhihu1.png)](https://zhuanlan.zhihu.com/cute-javascript) +[![掘金](http://images.pingan8787.com/icon_juejin2.png)](https://juejin.im/user/586fc337a22b9d0058807d53/posts) +[![思否](http://images.pingan8787.com/icon_sf1.png)](https://segmentfault.com/blog/pingan8787) +[![CSDN](http://images.pingan8787.com/icon_csdn1.png)](https://blog.csdn.net/qq_36380426) +[![简书](http://images.pingan8787.com/icon_jianshu1.png)](https://www.jianshu.com/u/2ec5d94afd60) + +### 1.开发背景 + +由于公司 **V2项目** 需要做组件化升级,但因为 **V2项目** 项目历史包袱大,代码和文件非常多,而且嵌套较多,难以全面了解所需要调整的组件的影响范围,所以需要开发这么一个工具,来实现以下几个功能: + +* 需要能支持 **自定义关键词检索** ,便于按不同的已有 **组件名/样式名/属性名** 搜索; +* 需要能支持检索出该组件的 **影响文件范围** ,还有 **页面名称路由** 等,便于测试部门手动和自动化快速测试; +* 需要能支持 **数据可视化** ,便于判断所有影响范围的权重; +* 需要能 **导出影响范围的路由文件** 和 **必要数据** ; + +基于上面需求,我大概整理思路使用 `Nodejs` 和 `Python` 进行需求开发,原因有这几点: + +* 需求以操作文件为主,包括读写; +* 需求对数据处理操作比较多,包括过滤,组装数据格式; +* 需求对数据可视化的需求; + +起初我准备只使用 `Nodejs` 完成这个需求,后面开发到一半,发现 **数据可视化** 方面,实在找不到一个满意的可视化插件,于是想到 `Python` 的一个2D绘图库—— `Matplotlib` ,使用起来非常方便,于是便选择了它。 + +### 2.工具文档 + +* [Nodejs-官方文档](https://nodejs.org/zh-cn/docs/) +* [Python-官方文档](https://docs.python.org/3/) +* [Python-中文开发手册](http://www.iteedu.com/plang/python/pythoncnshc/index.php) +* [Matplotlib-开发教程](https://www.matplotlib.org.cn/index.html) + + +## 二、开发环境搭建 +### 1.Nodejs环境搭建 +对于 `Nodejs` 环境搭建,相信对于我们前端开发仔来说,应该是很简单,但这里考虑到可能原生的同学还不太清楚,这里我简单介绍: + +* 下载和安装 `Nodejs` + +我们到[ Nodejs官网 ](https://nodejs.org/zh-cn/download/),选择对应系统环境进行下载,然后直接打开安装。 + +* 测试 `Nodejs` 环境 + +打开命令行工具,执行 `node -v` ,看是是否输出对应 `Nodejs` 版本号,我这显示: +```bash +v10.8.0 +``` +另外在 WIN7 系统下可能会出现下面报错,则需要将 `nodejs` 安装目录,添加全局路径: +```bash +node : 无法将“node”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径正确,然后再试一次。 +``` + +* 安装完成 + +### 2.Python环境搭建 + +* 下载和安装 `Python` + +在[ Python官网 ](https://www.python.org/downloads/),选择 **3.x** 版本下载(由于Python2.x版本已经停止维护,并且即将被淘汰),下载完成直接安装。 + +* 测试 `Python` 环境 + +安装完成,打开命令行工具,执行 `python` ,看看输出结果是否是版本号和**命令行交互模式**,我这显示: + +```bash +PS C:\Users\mi> python + +Python 3.6.3 |Anaconda, Inc.| (default, Oct 15 2017, 03:27:45) [MSC v.1900 64 bit (AMD64)] on win32 +Type "help", "copyright", "credits" or "license" for more information. +>> +``` + +* 安装绘图库 `Matplotlib` + +`python` 安装其他包是用 `pip install packageName` 来安装,跟 `Nodejs` 中的 `npm install packageName` 是一样的,我们就这么安装 `Matplotlib` : +```bash +pip install Matplotlib +``` + +* 安装完成 + +## 三、开发过程 + +首先先介绍下开发的思路: + + +![20190311share4](http://images.pingan8787.com/20190311share4.png) + + +### 1.最终效果 + +最终我实现的效果是,开发 `search_current_file.js` 和 `search_current_file_python.py` 两个文件,并通过执行两个命令,来获取对应数据文件: + +* 获取 **所有包含关键词的文件的路径**、**所在文件夹内文件数量**和**所有文件对应页面的路由/参数/标题等数据** 统计的文件和表格。 +```bash +node search_current_file.js +``` + +![20190311share3](http://images.pingan8787.com/20190311share3.png) + + +* 获取 **所有文件夹中文件数量占总文件数的比例** 的饼图结果。 + +```bash +python search_current_file_python.py +``` +这里需要输入需要生成的指定文件夹的数据,默认不输入则生成所有文件夹下的数据。 + +![20190311share2](http://images.pingan8787.com/20190311share2.png) + + +### 2.Nodejs开发部分 +首先定义几个下面主要使用的变量,其他没有写在这里的变量和作用,可以查看源码。 + +```js +var Excel = require('exceljs'); +var XLSX = require('xlsx'); + +var filterFile = ['.html']; // 需要检索的文件类型 +var filterDir = ['lib']; // 需要排除的文件夹 +var classArray = [ // 需要检索的类名数组 + 'search-holder','exe-bar-search','输入搜索内容','= 0){ + resultArray.push(paths); + resultAlassify[val].push(paths); // 保存最终结果(当前关键词下的对象) + } + } +}; +``` + +#### 2.2处理搜索结果 +**目的**: 将获取到的数据,去重,格式化并保存成JSON,作为可视化的数据源。 +这里有定义两个简单方法 `unique()` 用于数据去重,和 `setEachDirFileNum()` 统计文件数量,不做具体介绍。 + +这里我们使用 `saveDataToJson()` 将数据整理成 JSON 格式,并使用 `setJSONFile()` 方法,将JSON数据保存为 `json` 文件,用于可视化操作。 + +* 核心方法 `saveDataToJson()` + +这一步主要只用 `loadsh` 的分组函数 `_.ground` 来处理 JSON 数据,我们需要的格式是: +```js +result = { + template: [ + home:[ {}, {} ], + my: [ {}, {} ] + // ... + ], + view: [ + // ... + ] +} +``` +然后还需要处理成保存 Excel 时所需要的格式,再使用 `setJSONFile()` 方法保存 JSON 文件。 +```js +/** + * 转成JSON数据,用来数据可视化 + * @param {*} data 需要处理的数据 + */ +var saveDataToJson = function (data){ + var result = {}; + // 第一层分组 外层文件夹 + result = _.groupBy(data, function(item){ + item = item.replace(filePath+'\\',''); + var list = item.split('\\'); + return list[0]; + }); + // 第二层分组 内层文件夹 + for(var k in result){ + result[k] = _.groupBy(result[k], function(i){ + i = i.replace(filePath+'\\',''); + var r = i.split('\\'); + return r[1]; + }); + } + for(var i in result){ + for(var m in result[i]){ + for(var n in result[i][m]){ + var currentPath = result[i][m][n].replace(filePath+'\\',''); + currentPath = currentPath.replace(/\\/g, '/'); + var current = excelFileObj[currentPath]; + result[i][m][n] = { + title : current ? current['路由名称'] : '该文件为模块', + path : current ? current['文件路径'] : currentPath, + url : current ? current['url'] : '该文件为模块', + params: current ? current['路由参数'] : '该文件为模块', + ctrl : current ? current['控制器名称'] : '该文件为模块', + urls : current ? current['url'] : '该文件为模块', + }; + } + } + } + setJSONFile(result); // 保存JSON文件 +}; +``` + +#### 2.3加入文件标题路由等数据 +**目的**: 解析外部路由Excel表,合并到原有数据 + +* 核心方法 `getExcelFile()` +读取 Excel 数据并通过 `resolve` 返回。 +```js +/** + * 读取Excel数据 + */ +var getExcelFile = function(){ + return new Promise(function(resolve, reject){ + var excelPath = path.join(__dirname, excelReadName); + fs.exists(excelPath, function(exists){ + if(exists){ + var workbook = XLSX.readFile(excelPath, {type: 'base64'});// 获取 Excel 中所有表名 + var sheetNames = workbook.SheetNames; + resolve({workbook: workbook, sheetNames: sheetNames}); + }else{ + reject({message:'错误提示:请先获取路由列表文件!(执行node get_router.js)'}); + } + }); + }) +}; +``` + +* 核心方法 `getEachSheet()` + +这里我们需要将 Excel 中的每个表的数据,都保存到 `excelFileObj` 中,另外需要注意,我们项目的 `lodash` 不能使用 4.0.0 以上版本的API。 +```js +/** + * 解析Excel数据 + * @param {object} workbook excel工作区数据 + * @param {object} sheetNames excel工作表名数据 + */ +var getEachSheet = function(workbook, sheetNames){ + _.forEach(sheetNames,function(item,index){ + var sheet = workbook.Sheets[sheetNames[index]]; + var json = XLSX.utils.sheet_to_json(sheet); // 针对单个表,返回序列化json数据 + excelFileArr = excelFileArr.concat(json); // 不能使用lodash的_.concat 因为lodash版本太低 + }) + _.forEach(excelFileArr, function(val, key){ + excelFileObj[val['文件路径']] = val; + }); +} +``` + +#### 2.4生成结果文件 +**目的**: 将处理后的结果生成对应的 Excel/JSON/TXT 文件: +这里生成 JSON/TXT 文件不做介绍,使用的是 Nodejs 内置的文件存储方法`fs.write` + +* 核心方法 `setExcelFile()` + +主要是整理数据为保存 Excel 的数据格式。 +```js +/** + * 保存Excel数据 + * @param {object} data 需要处理的数据 + * return excelFileName.xlsx + */ +var setExcelFile = function(data){ + var workbook = new Excel.Workbook(); + workbook.creator = 'EXE'; + workbook.lastModifiedBy = 'Leo'; + workbook.created = new Date(); + workbook.modified = new Date(); + workbook.lastPrinted = new Date(); + for(var item in data){ // 第一层循环 外层文件夹 templates views + for(var list in data[item]){ + var worksheet = workbook.addWorksheet(list.toUpperCase()), + rowData = data[item][list]; + worksheet.columns = [ + { header: '页面标题' , key: 'title' , width: 40 }, + { header: '文件路径' , key: 'path' , width: 60 }, + { header: '路由地址' , key: 'url' , width: 40 }, + { header: '路由参数' , key: 'params', width: 40 }, + { header: '控制器名称', key: 'ctrl' , width: 40 }, + { header: 'url' , key: 'urls' , width: 40 }, + ]; + for(var row in rowData){ + worksheet.addRow({ + title : rowData[row].title, + path : rowData[row].path, + url : rowData[row].url, + params: rowData[row].params, + ctrl : rowData[row].ctrl, + urls : rowData[row].urls, + }) + } + } + } + workbook.xlsx.writeFile(path.join(__dirname, excelFileName)).then(function() { + // ... 省略部分 + }); +}; +``` + +到这里我们 Nodejs 程序开发完成,我们最后会有一个文件 `search_current_file_json.json` 作为 Python 部分的数据源。 + + +### 3.Python开发部分 + +Python 部分的内容相对比较简单,做的只有 **加载数据**,**简单处理数据**和**可视化操作** 三部分。 + +同样在刚开始部分,将几个重要的定义写一下: +```python +# ... 省略一些 +import matplotlib.pyplot as plt +keyName = [] # 需要显示的分类图表(按外层文件夹) +selectName = '' # 用户选择的文件夹名称 +``` + +#### 2.1读取数据源 +我们通过使用 `python` 内置的 `open` 方法来读取文件,并导入内置方法 `json` 来读取前面 `Nodejs` 部分生成的 `search_current_file_json.json` 文件。 + +```python +file = open('./search_current_file_json.json','r', encoding='utf-8') +file = json.load(file) +``` + +#### 2.2设置命令行输入项 +设置命令行输入项的目的是:让用户通过输入要查看的文件夹名称,来展示对应文件夹的饼图,默认显示所有文件夹饼图。 + +在设置之前,我们需要先通过 `getKeyName()` 方法获取到所有第一层文件夹的名称: +```python +def getKeyName(): + for name in file: + keyName.append(name) +``` +然后才能设置命令行输入项: +```python +getKeyName() +select = ','.join(keyName) +selectName = input('检索到的文件夹有:【' + select + '】,请输入要查看的文件夹名称(默认所有):') +``` + +#### 2.3绘制单张饼图 + +接下来绘制单张饼图,这里主要就是设置饼图的参数: + +* 核心方法 `drawOneChart()` + +```python +def drawOneChart(name, label, data): + plt_title = name + plt.figure(figsize=(6,9)) # 调节图形大小 + labels = label # 定义标签 + sizes = data # 每块值 + colors = [ # 每块颜色定义 这里省略掉 + #... + ] + explode = [] # 将某一块分割出来,值越大分割出的间隙越大 + max_data = max(sizes) + for i in sizes: # 初始化每块之间间距,最大值分割出来 + if i == max_data: + explode.append(0.2) + else: + explode.append(0) + + patches,text1,text2 = plt.pie( + sizes, explode = explode, labels = labels, colors = colors, + autopct = lambda pct: pctName(pct, data), # 数值保留固定小数位 + frame = 1, # 是否显示饼图的图框,这里设置显示 + shadow = True, # 无阴影设置 + labeldistance = 1.1, # 图例距圆心半径倍距离 + counterclock = False, # 是否让饼图按逆时针顺序呈现; + startangle = 90, # 逆时针起始角度设置 + pctdistance = 0.6 # 数值距圆心半径倍数距离 + ) + plt.xticks(()) + plt.yticks(()) + plt.axis('equal') + plt.legend() + plt.title(plt_title+'文件夹下文件分布(顺时针)', bbox={'facecolor':'0.8', 'pad':5}) + plt.savefig(plt_title+'_'+saveImgName) # 一定放在plt.show()之前 + plt.show() +``` + +#### 2.4绘制多张饼图 +最后通过循环调用 `drawOneChart()` 来生成所有的饼图: + +* 核心方法 `drawAllChart()` + +这个方法中需要对之前 JSON 数据再处理,将每个文件夹中文件数量作为饼图的数据,也就是这里的 `values` 的值。 + +```python +def drawAllChart(openName): + for name in keyName: + labels = [] + values = [] + for view_name in file[name]: + labels.append(view_name) + values.append(len(file[name][view_name])) + if openName == '' or openName == name: + drawOneChart(name, labels, values) + else: + print('输入有误') + +``` + +## 四、总结 + +### 1.Nodejs知识点 + +这部分用得比较多的是 Nodejs 中的: +* 文件读/写操作 +* 正则匹配操作 +* 数据格式处理操作 + +因此为了以后开发类似或者其他类型工具,还是需要加强这三方面的知识,这部分的代码可能不够简洁,代码也不够美观,但毕竟作为自己的经验积累,对这类工具开发会有更加清晰的思路。 + +### 2.Python知识点 + +这部分用得比较多的,其实是 Python 中的一些基础语法,这部分代码,其实也是加深自己对 Python 基础语法的使用和理解,练习操作。 + +### 3.拓展 +接下来会找时间,优化项目代码,然后改造这个项目,将使用 Nodejs 和 Python 分别单独开发一套,并比较两者差距(执行效率/代码量)。 +另外 Nodejs 的绘图库还有: [node-echarts](https://www.npmjs.com/package/node-echarts) 和 [d3-node](https://www.npmjs.com/package/d3-node)。 + +![bg](http://images.pingan8787.com/2019_07_12guild_page.png) \ No newline at end of file diff --git a/Cute-Demo/stateMechine/stateMechine.html b/Cute-Demo/stateMechine/stateMechine.html new file mode 100644 index 00000000..1a15aef7 --- /dev/null +++ b/Cute-Demo/stateMechine/stateMechine.html @@ -0,0 +1,124 @@ + + + + + + + + Document + + + + + + + + \ No newline at end of file diff --git a/Cute-Demo/stateMechine/stateMechine1.html b/Cute-Demo/stateMechine/stateMechine1.html new file mode 100644 index 00000000..31790416 --- /dev/null +++ b/Cute-Demo/stateMechine/stateMechine1.html @@ -0,0 +1,138 @@ + + + + + + + + Document + + + + + + + + \ No newline at end of file diff --git a/Cute-Esbuild/Demo/demo-1/demo1.js b/Cute-Esbuild/Demo/demo-1/demo1.js new file mode 100644 index 00000000..d727e0d0 --- /dev/null +++ b/Cute-Esbuild/Demo/demo-1/demo1.js @@ -0,0 +1,4989 @@ +(() => { + var __create = Object.create; + var __defProp = Object.defineProperty; + var __getOwnPropDesc = Object.getOwnPropertyDescriptor; + var __getOwnPropNames = Object.getOwnPropertyNames; + var __getProtoOf = Object.getPrototypeOf; + var __hasOwnProp = Object.prototype.hasOwnProperty; + var __markAsModule = (target) => __defProp(target, "__esModule", { value: true }); + var __commonJS = (cb, mod) => function __require() { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; + }; + var __reExport = (target, module, copyDefault, desc) => { + if (module && typeof module === "object" || typeof module === "function") { + for (let key of __getOwnPropNames(module)) + if (!__hasOwnProp.call(target, key) && (copyDefault || key !== "default")) + __defProp(target, key, { get: () => module[key], enumerable: !(desc = __getOwnPropDesc(module, key)) || desc.enumerable }); + } + return target; + }; + var __toESM = (module, isNodeMode) => { + return __reExport(__markAsModule(__defProp(module != null ? __create(__getProtoOf(module)) : {}, "default", !isNodeMode && module && module.__esModule ? { get: () => module.default, enumerable: true } : { value: module, enumerable: true })), module); + }; + + // node_modules/.pnpm/object-assign@4.1.1/node_modules/object-assign/index.js + var require_object_assign = __commonJS({ + "node_modules/.pnpm/object-assign@4.1.1/node_modules/object-assign/index.js"(exports, module) { + "use strict"; + var getOwnPropertySymbols = Object.getOwnPropertySymbols; + var hasOwnProperty = Object.prototype.hasOwnProperty; + var propIsEnumerable = Object.prototype.propertyIsEnumerable; + function toObject(val) { + if (val === null || val === void 0) { + throw new TypeError("Object.assign cannot be called with null or undefined"); + } + return Object(val); + } + function shouldUseNative() { + try { + if (!Object.assign) { + return false; + } + var test1 = new String("abc"); + test1[5] = "de"; + if (Object.getOwnPropertyNames(test1)[0] === "5") { + return false; + } + var test2 = {}; + for (var i = 0; i < 10; i++) { + test2["_" + String.fromCharCode(i)] = i; + } + var order2 = Object.getOwnPropertyNames(test2).map(function(n) { + return test2[n]; + }); + if (order2.join("") !== "0123456789") { + return false; + } + var test3 = {}; + "abcdefghijklmnopqrst".split("").forEach(function(letter) { + test3[letter] = letter; + }); + if (Object.keys(Object.assign({}, test3)).join("") !== "abcdefghijklmnopqrst") { + return false; + } + return true; + } catch (err) { + return false; + } + } + module.exports = shouldUseNative() ? Object.assign : function(target, source) { + var from; + var to = toObject(target); + var symbols; + for (var s = 1; s < arguments.length; s++) { + from = Object(arguments[s]); + for (var key in from) { + if (hasOwnProperty.call(from, key)) { + to[key] = from[key]; + } + } + if (getOwnPropertySymbols) { + symbols = getOwnPropertySymbols(from); + for (var i = 0; i < symbols.length; i++) { + if (propIsEnumerable.call(from, symbols[i])) { + to[symbols[i]] = from[symbols[i]]; + } + } + } + } + return to; + }; + } + }); + + // node_modules/.pnpm/react@17.0.2/node_modules/react/cjs/react.development.js + var require_react_development = __commonJS({ + "node_modules/.pnpm/react@17.0.2/node_modules/react/cjs/react.development.js"(exports) { + "use strict"; + if (true) { + (function() { + "use strict"; + var _assign = require_object_assign(); + var ReactVersion = "17.0.2"; + var REACT_ELEMENT_TYPE = 60103; + var REACT_PORTAL_TYPE = 60106; + exports.Fragment = 60107; + exports.StrictMode = 60108; + exports.Profiler = 60114; + var REACT_PROVIDER_TYPE = 60109; + var REACT_CONTEXT_TYPE = 60110; + var REACT_FORWARD_REF_TYPE = 60112; + exports.Suspense = 60113; + var REACT_SUSPENSE_LIST_TYPE = 60120; + var REACT_MEMO_TYPE = 60115; + var REACT_LAZY_TYPE = 60116; + var REACT_BLOCK_TYPE = 60121; + var REACT_SERVER_BLOCK_TYPE = 60122; + var REACT_FUNDAMENTAL_TYPE = 60117; + var REACT_SCOPE_TYPE = 60119; + var REACT_OPAQUE_ID_TYPE = 60128; + var REACT_DEBUG_TRACING_MODE_TYPE = 60129; + var REACT_OFFSCREEN_TYPE = 60130; + var REACT_LEGACY_HIDDEN_TYPE = 60131; + if (typeof Symbol === "function" && Symbol.for) { + var symbolFor = Symbol.for; + REACT_ELEMENT_TYPE = symbolFor("react.element"); + REACT_PORTAL_TYPE = symbolFor("react.portal"); + exports.Fragment = symbolFor("react.fragment"); + exports.StrictMode = symbolFor("react.strict_mode"); + exports.Profiler = symbolFor("react.profiler"); + REACT_PROVIDER_TYPE = symbolFor("react.provider"); + REACT_CONTEXT_TYPE = symbolFor("react.context"); + REACT_FORWARD_REF_TYPE = symbolFor("react.forward_ref"); + exports.Suspense = symbolFor("react.suspense"); + REACT_SUSPENSE_LIST_TYPE = symbolFor("react.suspense_list"); + REACT_MEMO_TYPE = symbolFor("react.memo"); + REACT_LAZY_TYPE = symbolFor("react.lazy"); + REACT_BLOCK_TYPE = symbolFor("react.block"); + REACT_SERVER_BLOCK_TYPE = symbolFor("react.server.block"); + REACT_FUNDAMENTAL_TYPE = symbolFor("react.fundamental"); + REACT_SCOPE_TYPE = symbolFor("react.scope"); + REACT_OPAQUE_ID_TYPE = symbolFor("react.opaque.id"); + REACT_DEBUG_TRACING_MODE_TYPE = symbolFor("react.debug_trace_mode"); + REACT_OFFSCREEN_TYPE = symbolFor("react.offscreen"); + REACT_LEGACY_HIDDEN_TYPE = symbolFor("react.legacy_hidden"); + } + var MAYBE_ITERATOR_SYMBOL = typeof Symbol === "function" && Symbol.iterator; + var FAUX_ITERATOR_SYMBOL = "@@iterator"; + function getIteratorFn(maybeIterable) { + if (maybeIterable === null || typeof maybeIterable !== "object") { + return null; + } + var maybeIterator = MAYBE_ITERATOR_SYMBOL && maybeIterable[MAYBE_ITERATOR_SYMBOL] || maybeIterable[FAUX_ITERATOR_SYMBOL]; + if (typeof maybeIterator === "function") { + return maybeIterator; + } + return null; + } + var ReactCurrentDispatcher = { + current: null + }; + var ReactCurrentBatchConfig = { + transition: 0 + }; + var ReactCurrentOwner = { + current: null + }; + var ReactDebugCurrentFrame = {}; + var currentExtraStackFrame = null; + function setExtraStackFrame(stack) { + { + currentExtraStackFrame = stack; + } + } + { + ReactDebugCurrentFrame.setExtraStackFrame = function(stack) { + { + currentExtraStackFrame = stack; + } + }; + ReactDebugCurrentFrame.getCurrentStack = null; + ReactDebugCurrentFrame.getStackAddendum = function() { + var stack = ""; + if (currentExtraStackFrame) { + stack += currentExtraStackFrame; + } + var impl = ReactDebugCurrentFrame.getCurrentStack; + if (impl) { + stack += impl() || ""; + } + return stack; + }; + } + var IsSomeRendererActing = { + current: false + }; + var ReactSharedInternals = { + ReactCurrentDispatcher, + ReactCurrentBatchConfig, + ReactCurrentOwner, + IsSomeRendererActing, + assign: _assign + }; + { + ReactSharedInternals.ReactDebugCurrentFrame = ReactDebugCurrentFrame; + } + function warn(format) { + { + for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + printWarning("warn", format, args); + } + } + function error(format) { + { + for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { + args[_key2 - 1] = arguments[_key2]; + } + printWarning("error", format, args); + } + } + function printWarning(level, format, args) { + { + var ReactDebugCurrentFrame2 = ReactSharedInternals.ReactDebugCurrentFrame; + var stack = ReactDebugCurrentFrame2.getStackAddendum(); + if (stack !== "") { + format += "%s"; + args = args.concat([stack]); + } + var argsWithFormat = args.map(function(item) { + return "" + item; + }); + argsWithFormat.unshift("Warning: " + format); + Function.prototype.apply.call(console[level], console, argsWithFormat); + } + } + var didWarnStateUpdateForUnmountedComponent = {}; + function warnNoop(publicInstance, callerName) { + { + var _constructor = publicInstance.constructor; + var componentName = _constructor && (_constructor.displayName || _constructor.name) || "ReactClass"; + var warningKey = componentName + "." + callerName; + if (didWarnStateUpdateForUnmountedComponent[warningKey]) { + return; + } + error("Can't call %s on a component that is not yet mounted. This is a no-op, but it might indicate a bug in your application. Instead, assign to `this.state` directly or define a `state = {};` class property with the desired state in the %s component.", callerName, componentName); + didWarnStateUpdateForUnmountedComponent[warningKey] = true; + } + } + var ReactNoopUpdateQueue = { + isMounted: function(publicInstance) { + return false; + }, + enqueueForceUpdate: function(publicInstance, callback, callerName) { + warnNoop(publicInstance, "forceUpdate"); + }, + enqueueReplaceState: function(publicInstance, completeState, callback, callerName) { + warnNoop(publicInstance, "replaceState"); + }, + enqueueSetState: function(publicInstance, partialState, callback, callerName) { + warnNoop(publicInstance, "setState"); + } + }; + var emptyObject = {}; + { + Object.freeze(emptyObject); + } + function Component(props, context, updater) { + this.props = props; + this.context = context; + this.refs = emptyObject; + this.updater = updater || ReactNoopUpdateQueue; + } + Component.prototype.isReactComponent = {}; + Component.prototype.setState = function(partialState, callback) { + if (!(typeof partialState === "object" || typeof partialState === "function" || partialState == null)) { + { + throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables."); + } + } + this.updater.enqueueSetState(this, partialState, callback, "setState"); + }; + Component.prototype.forceUpdate = function(callback) { + this.updater.enqueueForceUpdate(this, callback, "forceUpdate"); + }; + { + var deprecatedAPIs = { + isMounted: ["isMounted", "Instead, make sure to clean up subscriptions and pending requests in componentWillUnmount to prevent memory leaks."], + replaceState: ["replaceState", "Refactor your code to use setState instead (see https://github.com/facebook/react/issues/3236)."] + }; + var defineDeprecationWarning = function(methodName, info) { + Object.defineProperty(Component.prototype, methodName, { + get: function() { + warn("%s(...) is deprecated in plain JavaScript React classes. %s", info[0], info[1]); + return void 0; + } + }); + }; + for (var fnName in deprecatedAPIs) { + if (deprecatedAPIs.hasOwnProperty(fnName)) { + defineDeprecationWarning(fnName, deprecatedAPIs[fnName]); + } + } + } + function ComponentDummy() { + } + ComponentDummy.prototype = Component.prototype; + function PureComponent(props, context, updater) { + this.props = props; + this.context = context; + this.refs = emptyObject; + this.updater = updater || ReactNoopUpdateQueue; + } + var pureComponentPrototype = PureComponent.prototype = new ComponentDummy(); + pureComponentPrototype.constructor = PureComponent; + _assign(pureComponentPrototype, Component.prototype); + pureComponentPrototype.isPureReactComponent = true; + function createRef() { + var refObject = { + current: null + }; + { + Object.seal(refObject); + } + return refObject; + } + function getWrappedName(outerType, innerType, wrapperName) { + var functionName = innerType.displayName || innerType.name || ""; + return outerType.displayName || (functionName !== "" ? wrapperName + "(" + functionName + ")" : wrapperName); + } + function getContextName(type) { + return type.displayName || "Context"; + } + function getComponentName(type) { + if (type == null) { + return null; + } + { + if (typeof type.tag === "number") { + error("Received an unexpected object in getComponentName(). This is likely a bug in React. Please file an issue."); + } + } + if (typeof type === "function") { + return type.displayName || type.name || null; + } + if (typeof type === "string") { + return type; + } + switch (type) { + case exports.Fragment: + return "Fragment"; + case REACT_PORTAL_TYPE: + return "Portal"; + case exports.Profiler: + return "Profiler"; + case exports.StrictMode: + return "StrictMode"; + case exports.Suspense: + return "Suspense"; + case REACT_SUSPENSE_LIST_TYPE: + return "SuspenseList"; + } + if (typeof type === "object") { + switch (type.$$typeof) { + case REACT_CONTEXT_TYPE: + var context = type; + return getContextName(context) + ".Consumer"; + case REACT_PROVIDER_TYPE: + var provider = type; + return getContextName(provider._context) + ".Provider"; + case REACT_FORWARD_REF_TYPE: + return getWrappedName(type, type.render, "ForwardRef"); + case REACT_MEMO_TYPE: + return getComponentName(type.type); + case REACT_BLOCK_TYPE: + return getComponentName(type._render); + case REACT_LAZY_TYPE: { + var lazyComponent = type; + var payload = lazyComponent._payload; + var init = lazyComponent._init; + try { + return getComponentName(init(payload)); + } catch (x) { + return null; + } + } + } + } + return null; + } + var hasOwnProperty = Object.prototype.hasOwnProperty; + var RESERVED_PROPS = { + key: true, + ref: true, + __self: true, + __source: true + }; + var specialPropKeyWarningShown, specialPropRefWarningShown, didWarnAboutStringRefs; + { + didWarnAboutStringRefs = {}; + } + function hasValidRef(config) { + { + if (hasOwnProperty.call(config, "ref")) { + var getter = Object.getOwnPropertyDescriptor(config, "ref").get; + if (getter && getter.isReactWarning) { + return false; + } + } + } + return config.ref !== void 0; + } + function hasValidKey(config) { + { + if (hasOwnProperty.call(config, "key")) { + var getter = Object.getOwnPropertyDescriptor(config, "key").get; + if (getter && getter.isReactWarning) { + return false; + } + } + } + return config.key !== void 0; + } + function defineKeyPropWarningGetter(props, displayName) { + var warnAboutAccessingKey = function() { + { + if (!specialPropKeyWarningShown) { + specialPropKeyWarningShown = true; + error("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://reactjs.org/link/special-props)", displayName); + } + } + }; + warnAboutAccessingKey.isReactWarning = true; + Object.defineProperty(props, "key", { + get: warnAboutAccessingKey, + configurable: true + }); + } + function defineRefPropWarningGetter(props, displayName) { + var warnAboutAccessingRef = function() { + { + if (!specialPropRefWarningShown) { + specialPropRefWarningShown = true; + error("%s: `ref` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://reactjs.org/link/special-props)", displayName); + } + } + }; + warnAboutAccessingRef.isReactWarning = true; + Object.defineProperty(props, "ref", { + get: warnAboutAccessingRef, + configurable: true + }); + } + function warnIfStringRefCannotBeAutoConverted(config) { + { + if (typeof config.ref === "string" && ReactCurrentOwner.current && config.__self && ReactCurrentOwner.current.stateNode !== config.__self) { + var componentName = getComponentName(ReactCurrentOwner.current.type); + if (!didWarnAboutStringRefs[componentName]) { + error('Component "%s" contains the string ref "%s". Support for string refs will be removed in a future major release. This case cannot be automatically converted to an arrow function. We ask you to manually fix this case by using useRef() or createRef() instead. Learn more about using refs safely here: https://reactjs.org/link/strict-mode-string-ref', componentName, config.ref); + didWarnAboutStringRefs[componentName] = true; + } + } + } + } + var ReactElement = function(type, key, ref, self, source, owner, props) { + var element = { + $$typeof: REACT_ELEMENT_TYPE, + type, + key, + ref, + props, + _owner: owner + }; + { + element._store = {}; + Object.defineProperty(element._store, "validated", { + configurable: false, + enumerable: false, + writable: true, + value: false + }); + Object.defineProperty(element, "_self", { + configurable: false, + enumerable: false, + writable: false, + value: self + }); + Object.defineProperty(element, "_source", { + configurable: false, + enumerable: false, + writable: false, + value: source + }); + if (Object.freeze) { + Object.freeze(element.props); + Object.freeze(element); + } + } + return element; + }; + function createElement2(type, config, children) { + var propName; + var props = {}; + var key = null; + var ref = null; + var self = null; + var source = null; + if (config != null) { + if (hasValidRef(config)) { + ref = config.ref; + { + warnIfStringRefCannotBeAutoConverted(config); + } + } + if (hasValidKey(config)) { + key = "" + config.key; + } + self = config.__self === void 0 ? null : config.__self; + source = config.__source === void 0 ? null : config.__source; + for (propName in config) { + if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) { + props[propName] = config[propName]; + } + } + } + var childrenLength = arguments.length - 2; + if (childrenLength === 1) { + props.children = children; + } else if (childrenLength > 1) { + var childArray = Array(childrenLength); + for (var i = 0; i < childrenLength; i++) { + childArray[i] = arguments[i + 2]; + } + { + if (Object.freeze) { + Object.freeze(childArray); + } + } + props.children = childArray; + } + if (type && type.defaultProps) { + var defaultProps = type.defaultProps; + for (propName in defaultProps) { + if (props[propName] === void 0) { + props[propName] = defaultProps[propName]; + } + } + } + { + if (key || ref) { + var displayName = typeof type === "function" ? type.displayName || type.name || "Unknown" : type; + if (key) { + defineKeyPropWarningGetter(props, displayName); + } + if (ref) { + defineRefPropWarningGetter(props, displayName); + } + } + } + return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props); + } + function cloneAndReplaceKey(oldElement, newKey) { + var newElement = ReactElement(oldElement.type, newKey, oldElement.ref, oldElement._self, oldElement._source, oldElement._owner, oldElement.props); + return newElement; + } + function cloneElement(element, config, children) { + if (!!(element === null || element === void 0)) { + { + throw Error("React.cloneElement(...): The argument must be a React element, but you passed " + element + "."); + } + } + var propName; + var props = _assign({}, element.props); + var key = element.key; + var ref = element.ref; + var self = element._self; + var source = element._source; + var owner = element._owner; + if (config != null) { + if (hasValidRef(config)) { + ref = config.ref; + owner = ReactCurrentOwner.current; + } + if (hasValidKey(config)) { + key = "" + config.key; + } + var defaultProps; + if (element.type && element.type.defaultProps) { + defaultProps = element.type.defaultProps; + } + for (propName in config) { + if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) { + if (config[propName] === void 0 && defaultProps !== void 0) { + props[propName] = defaultProps[propName]; + } else { + props[propName] = config[propName]; + } + } + } + } + var childrenLength = arguments.length - 2; + if (childrenLength === 1) { + props.children = children; + } else if (childrenLength > 1) { + var childArray = Array(childrenLength); + for (var i = 0; i < childrenLength; i++) { + childArray[i] = arguments[i + 2]; + } + props.children = childArray; + } + return ReactElement(element.type, key, ref, self, source, owner, props); + } + function isValidElement(object) { + return typeof object === "object" && object !== null && object.$$typeof === REACT_ELEMENT_TYPE; + } + var SEPARATOR = "."; + var SUBSEPARATOR = ":"; + function escape(key) { + var escapeRegex = /[=:]/g; + var escaperLookup = { + "=": "=0", + ":": "=2" + }; + var escapedString = key.replace(escapeRegex, function(match) { + return escaperLookup[match]; + }); + return "$" + escapedString; + } + var didWarnAboutMaps = false; + var userProvidedKeyEscapeRegex = /\/+/g; + function escapeUserProvidedKey(text) { + return text.replace(userProvidedKeyEscapeRegex, "$&/"); + } + function getElementKey(element, index) { + if (typeof element === "object" && element !== null && element.key != null) { + return escape("" + element.key); + } + return index.toString(36); + } + function mapIntoArray(children, array, escapedPrefix, nameSoFar, callback) { + var type = typeof children; + if (type === "undefined" || type === "boolean") { + children = null; + } + var invokeCallback = false; + if (children === null) { + invokeCallback = true; + } else { + switch (type) { + case "string": + case "number": + invokeCallback = true; + break; + case "object": + switch (children.$$typeof) { + case REACT_ELEMENT_TYPE: + case REACT_PORTAL_TYPE: + invokeCallback = true; + } + } + } + if (invokeCallback) { + var _child = children; + var mappedChild = callback(_child); + var childKey = nameSoFar === "" ? SEPARATOR + getElementKey(_child, 0) : nameSoFar; + if (Array.isArray(mappedChild)) { + var escapedChildKey = ""; + if (childKey != null) { + escapedChildKey = escapeUserProvidedKey(childKey) + "/"; + } + mapIntoArray(mappedChild, array, escapedChildKey, "", function(c) { + return c; + }); + } else if (mappedChild != null) { + if (isValidElement(mappedChild)) { + mappedChild = cloneAndReplaceKey(mappedChild, escapedPrefix + (mappedChild.key && (!_child || _child.key !== mappedChild.key) ? escapeUserProvidedKey("" + mappedChild.key) + "/" : "") + childKey); + } + array.push(mappedChild); + } + return 1; + } + var child; + var nextName; + var subtreeCount = 0; + var nextNamePrefix = nameSoFar === "" ? SEPARATOR : nameSoFar + SUBSEPARATOR; + if (Array.isArray(children)) { + for (var i = 0; i < children.length; i++) { + child = children[i]; + nextName = nextNamePrefix + getElementKey(child, i); + subtreeCount += mapIntoArray(child, array, escapedPrefix, nextName, callback); + } + } else { + var iteratorFn = getIteratorFn(children); + if (typeof iteratorFn === "function") { + var iterableChildren = children; + { + if (iteratorFn === iterableChildren.entries) { + if (!didWarnAboutMaps) { + warn("Using Maps as children is not supported. Use an array of keyed ReactElements instead."); + } + didWarnAboutMaps = true; + } + } + var iterator = iteratorFn.call(iterableChildren); + var step; + var ii = 0; + while (!(step = iterator.next()).done) { + child = step.value; + nextName = nextNamePrefix + getElementKey(child, ii++); + subtreeCount += mapIntoArray(child, array, escapedPrefix, nextName, callback); + } + } else if (type === "object") { + var childrenString = "" + children; + { + { + throw Error("Objects are not valid as a React child (found: " + (childrenString === "[object Object]" ? "object with keys {" + Object.keys(children).join(", ") + "}" : childrenString) + "). If you meant to render a collection of children, use an array instead."); + } + } + } + } + return subtreeCount; + } + function mapChildren(children, func, context) { + if (children == null) { + return children; + } + var result = []; + var count = 0; + mapIntoArray(children, result, "", "", function(child) { + return func.call(context, child, count++); + }); + return result; + } + function countChildren(children) { + var n = 0; + mapChildren(children, function() { + n++; + }); + return n; + } + function forEachChildren(children, forEachFunc, forEachContext) { + mapChildren(children, function() { + forEachFunc.apply(this, arguments); + }, forEachContext); + } + function toArray(children) { + return mapChildren(children, function(child) { + return child; + }) || []; + } + function onlyChild(children) { + if (!isValidElement(children)) { + { + throw Error("React.Children.only expected to receive a single React element child."); + } + } + return children; + } + function createContext(defaultValue, calculateChangedBits) { + if (calculateChangedBits === void 0) { + calculateChangedBits = null; + } else { + { + if (calculateChangedBits !== null && typeof calculateChangedBits !== "function") { + error("createContext: Expected the optional second argument to be a function. Instead received: %s", calculateChangedBits); + } + } + } + var context = { + $$typeof: REACT_CONTEXT_TYPE, + _calculateChangedBits: calculateChangedBits, + _currentValue: defaultValue, + _currentValue2: defaultValue, + _threadCount: 0, + Provider: null, + Consumer: null + }; + context.Provider = { + $$typeof: REACT_PROVIDER_TYPE, + _context: context + }; + var hasWarnedAboutUsingNestedContextConsumers = false; + var hasWarnedAboutUsingConsumerProvider = false; + var hasWarnedAboutDisplayNameOnConsumer = false; + { + var Consumer = { + $$typeof: REACT_CONTEXT_TYPE, + _context: context, + _calculateChangedBits: context._calculateChangedBits + }; + Object.defineProperties(Consumer, { + Provider: { + get: function() { + if (!hasWarnedAboutUsingConsumerProvider) { + hasWarnedAboutUsingConsumerProvider = true; + error("Rendering is not supported and will be removed in a future major release. Did you mean to render instead?"); + } + return context.Provider; + }, + set: function(_Provider) { + context.Provider = _Provider; + } + }, + _currentValue: { + get: function() { + return context._currentValue; + }, + set: function(_currentValue) { + context._currentValue = _currentValue; + } + }, + _currentValue2: { + get: function() { + return context._currentValue2; + }, + set: function(_currentValue2) { + context._currentValue2 = _currentValue2; + } + }, + _threadCount: { + get: function() { + return context._threadCount; + }, + set: function(_threadCount) { + context._threadCount = _threadCount; + } + }, + Consumer: { + get: function() { + if (!hasWarnedAboutUsingNestedContextConsumers) { + hasWarnedAboutUsingNestedContextConsumers = true; + error("Rendering is not supported and will be removed in a future major release. Did you mean to render instead?"); + } + return context.Consumer; + } + }, + displayName: { + get: function() { + return context.displayName; + }, + set: function(displayName) { + if (!hasWarnedAboutDisplayNameOnConsumer) { + warn("Setting `displayName` on Context.Consumer has no effect. You should set it directly on the context with Context.displayName = '%s'.", displayName); + hasWarnedAboutDisplayNameOnConsumer = true; + } + } + } + }); + context.Consumer = Consumer; + } + { + context._currentRenderer = null; + context._currentRenderer2 = null; + } + return context; + } + var Uninitialized = -1; + var Pending = 0; + var Resolved = 1; + var Rejected = 2; + function lazyInitializer(payload) { + if (payload._status === Uninitialized) { + var ctor = payload._result; + var thenable = ctor(); + var pending = payload; + pending._status = Pending; + pending._result = thenable; + thenable.then(function(moduleObject) { + if (payload._status === Pending) { + var defaultExport = moduleObject.default; + { + if (defaultExport === void 0) { + error("lazy: Expected the result of a dynamic import() call. Instead received: %s\n\nYour code should look like: \n const MyComponent = lazy(() => import('./MyComponent'))", moduleObject); + } + } + var resolved = payload; + resolved._status = Resolved; + resolved._result = defaultExport; + } + }, function(error2) { + if (payload._status === Pending) { + var rejected = payload; + rejected._status = Rejected; + rejected._result = error2; + } + }); + } + if (payload._status === Resolved) { + return payload._result; + } else { + throw payload._result; + } + } + function lazy(ctor) { + var payload = { + _status: -1, + _result: ctor + }; + var lazyType = { + $$typeof: REACT_LAZY_TYPE, + _payload: payload, + _init: lazyInitializer + }; + { + var defaultProps; + var propTypes; + Object.defineProperties(lazyType, { + defaultProps: { + configurable: true, + get: function() { + return defaultProps; + }, + set: function(newDefaultProps) { + error("React.lazy(...): It is not supported to assign `defaultProps` to a lazy component import. Either specify them where the component is defined, or create a wrapping component around it."); + defaultProps = newDefaultProps; + Object.defineProperty(lazyType, "defaultProps", { + enumerable: true + }); + } + }, + propTypes: { + configurable: true, + get: function() { + return propTypes; + }, + set: function(newPropTypes) { + error("React.lazy(...): It is not supported to assign `propTypes` to a lazy component import. Either specify them where the component is defined, or create a wrapping component around it."); + propTypes = newPropTypes; + Object.defineProperty(lazyType, "propTypes", { + enumerable: true + }); + } + } + }); + } + return lazyType; + } + function forwardRef(render) { + { + if (render != null && render.$$typeof === REACT_MEMO_TYPE) { + error("forwardRef requires a render function but received a `memo` component. Instead of forwardRef(memo(...)), use memo(forwardRef(...))."); + } else if (typeof render !== "function") { + error("forwardRef requires a render function but was given %s.", render === null ? "null" : typeof render); + } else { + if (render.length !== 0 && render.length !== 2) { + error("forwardRef render functions accept exactly two parameters: props and ref. %s", render.length === 1 ? "Did you forget to use the ref parameter?" : "Any additional parameter will be undefined."); + } + } + if (render != null) { + if (render.defaultProps != null || render.propTypes != null) { + error("forwardRef render functions do not support propTypes or defaultProps. Did you accidentally pass a React component?"); + } + } + } + var elementType = { + $$typeof: REACT_FORWARD_REF_TYPE, + render + }; + { + var ownName; + Object.defineProperty(elementType, "displayName", { + enumerable: false, + configurable: true, + get: function() { + return ownName; + }, + set: function(name) { + ownName = name; + if (render.displayName == null) { + render.displayName = name; + } + } + }); + } + return elementType; + } + var enableScopeAPI = false; + function isValidElementType(type) { + if (typeof type === "string" || typeof type === "function") { + return true; + } + if (type === exports.Fragment || type === exports.Profiler || type === REACT_DEBUG_TRACING_MODE_TYPE || type === exports.StrictMode || type === exports.Suspense || type === REACT_SUSPENSE_LIST_TYPE || type === REACT_LEGACY_HIDDEN_TYPE || enableScopeAPI) { + return true; + } + if (typeof type === "object" && type !== null) { + if (type.$$typeof === REACT_LAZY_TYPE || type.$$typeof === REACT_MEMO_TYPE || type.$$typeof === REACT_PROVIDER_TYPE || type.$$typeof === REACT_CONTEXT_TYPE || type.$$typeof === REACT_FORWARD_REF_TYPE || type.$$typeof === REACT_FUNDAMENTAL_TYPE || type.$$typeof === REACT_BLOCK_TYPE || type[0] === REACT_SERVER_BLOCK_TYPE) { + return true; + } + } + return false; + } + function memo(type, compare) { + { + if (!isValidElementType(type)) { + error("memo: The first argument must be a component. Instead received: %s", type === null ? "null" : typeof type); + } + } + var elementType = { + $$typeof: REACT_MEMO_TYPE, + type, + compare: compare === void 0 ? null : compare + }; + { + var ownName; + Object.defineProperty(elementType, "displayName", { + enumerable: false, + configurable: true, + get: function() { + return ownName; + }, + set: function(name) { + ownName = name; + if (type.displayName == null) { + type.displayName = name; + } + } + }); + } + return elementType; + } + function resolveDispatcher() { + var dispatcher = ReactCurrentDispatcher.current; + if (!(dispatcher !== null)) { + { + throw Error("Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:\n1. You might have mismatching versions of React and the renderer (such as React DOM)\n2. You might be breaking the Rules of Hooks\n3. You might have more than one copy of React in the same app\nSee https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem."); + } + } + return dispatcher; + } + function useContext(Context, unstable_observedBits) { + var dispatcher = resolveDispatcher(); + { + if (unstable_observedBits !== void 0) { + error("useContext() second argument is reserved for future use in React. Passing it is not supported. You passed: %s.%s", unstable_observedBits, typeof unstable_observedBits === "number" && Array.isArray(arguments[2]) ? "\n\nDid you call array.map(useContext)? Calling Hooks inside a loop is not supported. Learn more at https://reactjs.org/link/rules-of-hooks" : ""); + } + if (Context._context !== void 0) { + var realContext = Context._context; + if (realContext.Consumer === Context) { + error("Calling useContext(Context.Consumer) is not supported, may cause bugs, and will be removed in a future major release. Did you mean to call useContext(Context) instead?"); + } else if (realContext.Provider === Context) { + error("Calling useContext(Context.Provider) is not supported. Did you mean to call useContext(Context) instead?"); + } + } + } + return dispatcher.useContext(Context, unstable_observedBits); + } + function useState(initialState) { + var dispatcher = resolveDispatcher(); + return dispatcher.useState(initialState); + } + function useReducer(reducer, initialArg, init) { + var dispatcher = resolveDispatcher(); + return dispatcher.useReducer(reducer, initialArg, init); + } + function useRef(initialValue) { + var dispatcher = resolveDispatcher(); + return dispatcher.useRef(initialValue); + } + function useEffect(create, deps) { + var dispatcher = resolveDispatcher(); + return dispatcher.useEffect(create, deps); + } + function useLayoutEffect(create, deps) { + var dispatcher = resolveDispatcher(); + return dispatcher.useLayoutEffect(create, deps); + } + function useCallback(callback, deps) { + var dispatcher = resolveDispatcher(); + return dispatcher.useCallback(callback, deps); + } + function useMemo(create, deps) { + var dispatcher = resolveDispatcher(); + return dispatcher.useMemo(create, deps); + } + function useImperativeHandle(ref, create, deps) { + var dispatcher = resolveDispatcher(); + return dispatcher.useImperativeHandle(ref, create, deps); + } + function useDebugValue(value, formatterFn) { + { + var dispatcher = resolveDispatcher(); + return dispatcher.useDebugValue(value, formatterFn); + } + } + var disabledDepth = 0; + var prevLog; + var prevInfo; + var prevWarn; + var prevError; + var prevGroup; + var prevGroupCollapsed; + var prevGroupEnd; + function disabledLog() { + } + disabledLog.__reactDisabledLog = true; + function disableLogs() { + { + if (disabledDepth === 0) { + prevLog = console.log; + prevInfo = console.info; + prevWarn = console.warn; + prevError = console.error; + prevGroup = console.group; + prevGroupCollapsed = console.groupCollapsed; + prevGroupEnd = console.groupEnd; + var props = { + configurable: true, + enumerable: true, + value: disabledLog, + writable: true + }; + Object.defineProperties(console, { + info: props, + log: props, + warn: props, + error: props, + group: props, + groupCollapsed: props, + groupEnd: props + }); + } + disabledDepth++; + } + } + function reenableLogs() { + { + disabledDepth--; + if (disabledDepth === 0) { + var props = { + configurable: true, + enumerable: true, + writable: true + }; + Object.defineProperties(console, { + log: _assign({}, props, { + value: prevLog + }), + info: _assign({}, props, { + value: prevInfo + }), + warn: _assign({}, props, { + value: prevWarn + }), + error: _assign({}, props, { + value: prevError + }), + group: _assign({}, props, { + value: prevGroup + }), + groupCollapsed: _assign({}, props, { + value: prevGroupCollapsed + }), + groupEnd: _assign({}, props, { + value: prevGroupEnd + }) + }); + } + if (disabledDepth < 0) { + error("disabledDepth fell below zero. This is a bug in React. Please file an issue."); + } + } + } + var ReactCurrentDispatcher$1 = ReactSharedInternals.ReactCurrentDispatcher; + var prefix; + function describeBuiltInComponentFrame(name, source, ownerFn) { + { + if (prefix === void 0) { + try { + throw Error(); + } catch (x) { + var match = x.stack.trim().match(/\n( *(at )?)/); + prefix = match && match[1] || ""; + } + } + return "\n" + prefix + name; + } + } + var reentry = false; + var componentFrameCache; + { + var PossiblyWeakMap = typeof WeakMap === "function" ? WeakMap : Map; + componentFrameCache = new PossiblyWeakMap(); + } + function describeNativeComponentFrame(fn, construct) { + if (!fn || reentry) { + return ""; + } + { + var frame = componentFrameCache.get(fn); + if (frame !== void 0) { + return frame; + } + } + var control; + reentry = true; + var previousPrepareStackTrace = Error.prepareStackTrace; + Error.prepareStackTrace = void 0; + var previousDispatcher; + { + previousDispatcher = ReactCurrentDispatcher$1.current; + ReactCurrentDispatcher$1.current = null; + disableLogs(); + } + try { + if (construct) { + var Fake = function() { + throw Error(); + }; + Object.defineProperty(Fake.prototype, "props", { + set: function() { + throw Error(); + } + }); + if (typeof Reflect === "object" && Reflect.construct) { + try { + Reflect.construct(Fake, []); + } catch (x) { + control = x; + } + Reflect.construct(fn, [], Fake); + } else { + try { + Fake.call(); + } catch (x) { + control = x; + } + fn.call(Fake.prototype); + } + } else { + try { + throw Error(); + } catch (x) { + control = x; + } + fn(); + } + } catch (sample) { + if (sample && control && typeof sample.stack === "string") { + var sampleLines = sample.stack.split("\n"); + var controlLines = control.stack.split("\n"); + var s = sampleLines.length - 1; + var c = controlLines.length - 1; + while (s >= 1 && c >= 0 && sampleLines[s] !== controlLines[c]) { + c--; + } + for (; s >= 1 && c >= 0; s--, c--) { + if (sampleLines[s] !== controlLines[c]) { + if (s !== 1 || c !== 1) { + do { + s--; + c--; + if (c < 0 || sampleLines[s] !== controlLines[c]) { + var _frame = "\n" + sampleLines[s].replace(" at new ", " at "); + { + if (typeof fn === "function") { + componentFrameCache.set(fn, _frame); + } + } + return _frame; + } + } while (s >= 1 && c >= 0); + } + break; + } + } + } + } finally { + reentry = false; + { + ReactCurrentDispatcher$1.current = previousDispatcher; + reenableLogs(); + } + Error.prepareStackTrace = previousPrepareStackTrace; + } + var name = fn ? fn.displayName || fn.name : ""; + var syntheticFrame = name ? describeBuiltInComponentFrame(name) : ""; + { + if (typeof fn === "function") { + componentFrameCache.set(fn, syntheticFrame); + } + } + return syntheticFrame; + } + function describeFunctionComponentFrame(fn, source, ownerFn) { + { + return describeNativeComponentFrame(fn, false); + } + } + function shouldConstruct(Component2) { + var prototype = Component2.prototype; + return !!(prototype && prototype.isReactComponent); + } + function describeUnknownElementTypeFrameInDEV(type, source, ownerFn) { + if (type == null) { + return ""; + } + if (typeof type === "function") { + { + return describeNativeComponentFrame(type, shouldConstruct(type)); + } + } + if (typeof type === "string") { + return describeBuiltInComponentFrame(type); + } + switch (type) { + case exports.Suspense: + return describeBuiltInComponentFrame("Suspense"); + case REACT_SUSPENSE_LIST_TYPE: + return describeBuiltInComponentFrame("SuspenseList"); + } + if (typeof type === "object") { + switch (type.$$typeof) { + case REACT_FORWARD_REF_TYPE: + return describeFunctionComponentFrame(type.render); + case REACT_MEMO_TYPE: + return describeUnknownElementTypeFrameInDEV(type.type, source, ownerFn); + case REACT_BLOCK_TYPE: + return describeFunctionComponentFrame(type._render); + case REACT_LAZY_TYPE: { + var lazyComponent = type; + var payload = lazyComponent._payload; + var init = lazyComponent._init; + try { + return describeUnknownElementTypeFrameInDEV(init(payload), source, ownerFn); + } catch (x) { + } + } + } + } + return ""; + } + var loggedTypeFailures = {}; + var ReactDebugCurrentFrame$1 = ReactSharedInternals.ReactDebugCurrentFrame; + function setCurrentlyValidatingElement(element) { + { + if (element) { + var owner = element._owner; + var stack = describeUnknownElementTypeFrameInDEV(element.type, element._source, owner ? owner.type : null); + ReactDebugCurrentFrame$1.setExtraStackFrame(stack); + } else { + ReactDebugCurrentFrame$1.setExtraStackFrame(null); + } + } + } + function checkPropTypes(typeSpecs, values, location, componentName, element) { + { + var has = Function.call.bind(Object.prototype.hasOwnProperty); + for (var typeSpecName in typeSpecs) { + if (has(typeSpecs, typeSpecName)) { + var error$1 = void 0; + try { + if (typeof typeSpecs[typeSpecName] !== "function") { + var err = Error((componentName || "React class") + ": " + location + " type `" + typeSpecName + "` is invalid; it must be a function, usually from the `prop-types` package, but received `" + typeof typeSpecs[typeSpecName] + "`.This often happens because of typos such as `PropTypes.function` instead of `PropTypes.func`."); + err.name = "Invariant Violation"; + throw err; + } + error$1 = typeSpecs[typeSpecName](values, typeSpecName, componentName, location, null, "SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"); + } catch (ex) { + error$1 = ex; + } + if (error$1 && !(error$1 instanceof Error)) { + setCurrentlyValidatingElement(element); + error("%s: type specification of %s `%s` is invalid; the type checker function must return `null` or an `Error` but returned a %s. You may have forgotten to pass an argument to the type checker creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and shape all require an argument).", componentName || "React class", location, typeSpecName, typeof error$1); + setCurrentlyValidatingElement(null); + } + if (error$1 instanceof Error && !(error$1.message in loggedTypeFailures)) { + loggedTypeFailures[error$1.message] = true; + setCurrentlyValidatingElement(element); + error("Failed %s type: %s", location, error$1.message); + setCurrentlyValidatingElement(null); + } + } + } + } + } + function setCurrentlyValidatingElement$1(element) { + { + if (element) { + var owner = element._owner; + var stack = describeUnknownElementTypeFrameInDEV(element.type, element._source, owner ? owner.type : null); + setExtraStackFrame(stack); + } else { + setExtraStackFrame(null); + } + } + } + var propTypesMisspellWarningShown; + { + propTypesMisspellWarningShown = false; + } + function getDeclarationErrorAddendum() { + if (ReactCurrentOwner.current) { + var name = getComponentName(ReactCurrentOwner.current.type); + if (name) { + return "\n\nCheck the render method of `" + name + "`."; + } + } + return ""; + } + function getSourceInfoErrorAddendum(source) { + if (source !== void 0) { + var fileName = source.fileName.replace(/^.*[\\\/]/, ""); + var lineNumber = source.lineNumber; + return "\n\nCheck your code at " + fileName + ":" + lineNumber + "."; + } + return ""; + } + function getSourceInfoErrorAddendumForProps(elementProps) { + if (elementProps !== null && elementProps !== void 0) { + return getSourceInfoErrorAddendum(elementProps.__source); + } + return ""; + } + var ownerHasKeyUseWarning = {}; + function getCurrentComponentErrorInfo(parentType) { + var info = getDeclarationErrorAddendum(); + if (!info) { + var parentName = typeof parentType === "string" ? parentType : parentType.displayName || parentType.name; + if (parentName) { + info = "\n\nCheck the top-level render call using <" + parentName + ">."; + } + } + return info; + } + function validateExplicitKey(element, parentType) { + if (!element._store || element._store.validated || element.key != null) { + return; + } + element._store.validated = true; + var currentComponentErrorInfo = getCurrentComponentErrorInfo(parentType); + if (ownerHasKeyUseWarning[currentComponentErrorInfo]) { + return; + } + ownerHasKeyUseWarning[currentComponentErrorInfo] = true; + var childOwner = ""; + if (element && element._owner && element._owner !== ReactCurrentOwner.current) { + childOwner = " It was passed a child from " + getComponentName(element._owner.type) + "."; + } + { + setCurrentlyValidatingElement$1(element); + error('Each child in a list should have a unique "key" prop.%s%s See https://reactjs.org/link/warning-keys for more information.', currentComponentErrorInfo, childOwner); + setCurrentlyValidatingElement$1(null); + } + } + function validateChildKeys(node, parentType) { + if (typeof node !== "object") { + return; + } + if (Array.isArray(node)) { + for (var i = 0; i < node.length; i++) { + var child = node[i]; + if (isValidElement(child)) { + validateExplicitKey(child, parentType); + } + } + } else if (isValidElement(node)) { + if (node._store) { + node._store.validated = true; + } + } else if (node) { + var iteratorFn = getIteratorFn(node); + if (typeof iteratorFn === "function") { + if (iteratorFn !== node.entries) { + var iterator = iteratorFn.call(node); + var step; + while (!(step = iterator.next()).done) { + if (isValidElement(step.value)) { + validateExplicitKey(step.value, parentType); + } + } + } + } + } + } + function validatePropTypes(element) { + { + var type = element.type; + if (type === null || type === void 0 || typeof type === "string") { + return; + } + var propTypes; + if (typeof type === "function") { + propTypes = type.propTypes; + } else if (typeof type === "object" && (type.$$typeof === REACT_FORWARD_REF_TYPE || type.$$typeof === REACT_MEMO_TYPE)) { + propTypes = type.propTypes; + } else { + return; + } + if (propTypes) { + var name = getComponentName(type); + checkPropTypes(propTypes, element.props, "prop", name, element); + } else if (type.PropTypes !== void 0 && !propTypesMisspellWarningShown) { + propTypesMisspellWarningShown = true; + var _name = getComponentName(type); + error("Component %s declared `PropTypes` instead of `propTypes`. Did you misspell the property assignment?", _name || "Unknown"); + } + if (typeof type.getDefaultProps === "function" && !type.getDefaultProps.isReactClassApproved) { + error("getDefaultProps is only used on classic React.createClass definitions. Use a static property named `defaultProps` instead."); + } + } + } + function validateFragmentProps(fragment) { + { + var keys = Object.keys(fragment.props); + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + if (key !== "children" && key !== "key") { + setCurrentlyValidatingElement$1(fragment); + error("Invalid prop `%s` supplied to `React.Fragment`. React.Fragment can only have `key` and `children` props.", key); + setCurrentlyValidatingElement$1(null); + break; + } + } + if (fragment.ref !== null) { + setCurrentlyValidatingElement$1(fragment); + error("Invalid attribute `ref` supplied to `React.Fragment`."); + setCurrentlyValidatingElement$1(null); + } + } + } + function createElementWithValidation(type, props, children) { + var validType = isValidElementType(type); + if (!validType) { + var info = ""; + if (type === void 0 || typeof type === "object" && type !== null && Object.keys(type).length === 0) { + info += " You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports."; + } + var sourceInfo = getSourceInfoErrorAddendumForProps(props); + if (sourceInfo) { + info += sourceInfo; + } else { + info += getDeclarationErrorAddendum(); + } + var typeString; + if (type === null) { + typeString = "null"; + } else if (Array.isArray(type)) { + typeString = "array"; + } else if (type !== void 0 && type.$$typeof === REACT_ELEMENT_TYPE) { + typeString = "<" + (getComponentName(type.type) || "Unknown") + " />"; + info = " Did you accidentally export a JSX literal instead of a component?"; + } else { + typeString = typeof type; + } + { + error("React.createElement: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: %s.%s", typeString, info); + } + } + var element = createElement2.apply(this, arguments); + if (element == null) { + return element; + } + if (validType) { + for (var i = 2; i < arguments.length; i++) { + validateChildKeys(arguments[i], type); + } + } + if (type === exports.Fragment) { + validateFragmentProps(element); + } else { + validatePropTypes(element); + } + return element; + } + var didWarnAboutDeprecatedCreateFactory = false; + function createFactoryWithValidation(type) { + var validatedFactory = createElementWithValidation.bind(null, type); + validatedFactory.type = type; + { + if (!didWarnAboutDeprecatedCreateFactory) { + didWarnAboutDeprecatedCreateFactory = true; + warn("React.createFactory() is deprecated and will be removed in a future major release. Consider using JSX or use React.createElement() directly instead."); + } + Object.defineProperty(validatedFactory, "type", { + enumerable: false, + get: function() { + warn("Factory.type is deprecated. Access the class directly before passing it to createFactory."); + Object.defineProperty(this, "type", { + value: type + }); + return type; + } + }); + } + return validatedFactory; + } + function cloneElementWithValidation(element, props, children) { + var newElement = cloneElement.apply(this, arguments); + for (var i = 2; i < arguments.length; i++) { + validateChildKeys(arguments[i], newElement.type); + } + validatePropTypes(newElement); + return newElement; + } + { + try { + var frozenObject = Object.freeze({}); + /* @__PURE__ */ new Map([[frozenObject, null]]); + /* @__PURE__ */ new Set([frozenObject]); + } catch (e) { + } + } + var createElement$1 = createElementWithValidation; + var cloneElement$1 = cloneElementWithValidation; + var createFactory = createFactoryWithValidation; + var Children = { + map: mapChildren, + forEach: forEachChildren, + count: countChildren, + toArray, + only: onlyChild + }; + exports.Children = Children; + exports.Component = Component; + exports.PureComponent = PureComponent; + exports.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = ReactSharedInternals; + exports.cloneElement = cloneElement$1; + exports.createContext = createContext; + exports.createElement = createElement$1; + exports.createFactory = createFactory; + exports.createRef = createRef; + exports.forwardRef = forwardRef; + exports.isValidElement = isValidElement; + exports.lazy = lazy; + exports.memo = memo; + exports.useCallback = useCallback; + exports.useContext = useContext; + exports.useDebugValue = useDebugValue; + exports.useEffect = useEffect; + exports.useImperativeHandle = useImperativeHandle; + exports.useLayoutEffect = useLayoutEffect; + exports.useMemo = useMemo; + exports.useReducer = useReducer; + exports.useRef = useRef; + exports.useState = useState; + exports.version = ReactVersion; + })(); + } + } + }); + + // node_modules/.pnpm/react@17.0.2/node_modules/react/index.js + var require_react = __commonJS({ + "node_modules/.pnpm/react@17.0.2/node_modules/react/index.js"(exports, module) { + "use strict"; + if (false) { + module.exports = null; + } else { + module.exports = require_react_development(); + } + } + }); + + // node_modules/.pnpm/react-dom@17.0.2_react@17.0.2/node_modules/react-dom/cjs/react-dom-server.browser.development.js + var require_react_dom_server_browser_development = __commonJS({ + "node_modules/.pnpm/react-dom@17.0.2_react@17.0.2/node_modules/react-dom/cjs/react-dom-server.browser.development.js"(exports) { + "use strict"; + if (true) { + (function() { + "use strict"; + var React2 = require_react(); + var _assign = require_object_assign(); + function formatProdErrorMessage(code) { + var url = "https://reactjs.org/docs/error-decoder.html?invariant=" + code; + for (var i2 = 1; i2 < arguments.length; i2++) { + url += "&args[]=" + encodeURIComponent(arguments[i2]); + } + return "Minified React error #" + code + "; visit " + url + " for the full message or use the non-minified dev environment for full errors and additional helpful warnings."; + } + var ReactVersion = "17.0.2"; + var ReactSharedInternals = React2.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED; + function warn(format) { + { + for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + printWarning("warn", format, args); + } + } + function error(format) { + { + for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { + args[_key2 - 1] = arguments[_key2]; + } + printWarning("error", format, args); + } + } + function printWarning(level, format, args) { + { + var ReactDebugCurrentFrame2 = ReactSharedInternals.ReactDebugCurrentFrame; + var stack = ReactDebugCurrentFrame2.getStackAddendum(); + if (stack !== "") { + format += "%s"; + args = args.concat([stack]); + } + var argsWithFormat = args.map(function(item) { + return "" + item; + }); + argsWithFormat.unshift("Warning: " + format); + Function.prototype.apply.call(console[level], console, argsWithFormat); + } + } + var REACT_ELEMENT_TYPE = 60103; + var REACT_PORTAL_TYPE = 60106; + var REACT_FRAGMENT_TYPE = 60107; + var REACT_STRICT_MODE_TYPE = 60108; + var REACT_PROFILER_TYPE = 60114; + var REACT_PROVIDER_TYPE = 60109; + var REACT_CONTEXT_TYPE = 60110; + var REACT_FORWARD_REF_TYPE = 60112; + var REACT_SUSPENSE_TYPE = 60113; + var REACT_SUSPENSE_LIST_TYPE = 60120; + var REACT_MEMO_TYPE = 60115; + var REACT_LAZY_TYPE = 60116; + var REACT_BLOCK_TYPE = 60121; + var REACT_SERVER_BLOCK_TYPE = 60122; + var REACT_FUNDAMENTAL_TYPE = 60117; + var REACT_SCOPE_TYPE = 60119; + var REACT_OPAQUE_ID_TYPE = 60128; + var REACT_DEBUG_TRACING_MODE_TYPE = 60129; + var REACT_OFFSCREEN_TYPE = 60130; + var REACT_LEGACY_HIDDEN_TYPE = 60131; + if (typeof Symbol === "function" && Symbol.for) { + var symbolFor = Symbol.for; + REACT_ELEMENT_TYPE = symbolFor("react.element"); + REACT_PORTAL_TYPE = symbolFor("react.portal"); + REACT_FRAGMENT_TYPE = symbolFor("react.fragment"); + REACT_STRICT_MODE_TYPE = symbolFor("react.strict_mode"); + REACT_PROFILER_TYPE = symbolFor("react.profiler"); + REACT_PROVIDER_TYPE = symbolFor("react.provider"); + REACT_CONTEXT_TYPE = symbolFor("react.context"); + REACT_FORWARD_REF_TYPE = symbolFor("react.forward_ref"); + REACT_SUSPENSE_TYPE = symbolFor("react.suspense"); + REACT_SUSPENSE_LIST_TYPE = symbolFor("react.suspense_list"); + REACT_MEMO_TYPE = symbolFor("react.memo"); + REACT_LAZY_TYPE = symbolFor("react.lazy"); + REACT_BLOCK_TYPE = symbolFor("react.block"); + REACT_SERVER_BLOCK_TYPE = symbolFor("react.server.block"); + REACT_FUNDAMENTAL_TYPE = symbolFor("react.fundamental"); + REACT_SCOPE_TYPE = symbolFor("react.scope"); + REACT_OPAQUE_ID_TYPE = symbolFor("react.opaque.id"); + REACT_DEBUG_TRACING_MODE_TYPE = symbolFor("react.debug_trace_mode"); + REACT_OFFSCREEN_TYPE = symbolFor("react.offscreen"); + REACT_LEGACY_HIDDEN_TYPE = symbolFor("react.legacy_hidden"); + } + function getWrappedName(outerType, innerType, wrapperName) { + var functionName = innerType.displayName || innerType.name || ""; + return outerType.displayName || (functionName !== "" ? wrapperName + "(" + functionName + ")" : wrapperName); + } + function getContextName(type) { + return type.displayName || "Context"; + } + function getComponentName(type) { + if (type == null) { + return null; + } + { + if (typeof type.tag === "number") { + error("Received an unexpected object in getComponentName(). This is likely a bug in React. Please file an issue."); + } + } + if (typeof type === "function") { + return type.displayName || type.name || null; + } + if (typeof type === "string") { + return type; + } + switch (type) { + case REACT_FRAGMENT_TYPE: + return "Fragment"; + case REACT_PORTAL_TYPE: + return "Portal"; + case REACT_PROFILER_TYPE: + return "Profiler"; + case REACT_STRICT_MODE_TYPE: + return "StrictMode"; + case REACT_SUSPENSE_TYPE: + return "Suspense"; + case REACT_SUSPENSE_LIST_TYPE: + return "SuspenseList"; + } + if (typeof type === "object") { + switch (type.$$typeof) { + case REACT_CONTEXT_TYPE: + var context = type; + return getContextName(context) + ".Consumer"; + case REACT_PROVIDER_TYPE: + var provider = type; + return getContextName(provider._context) + ".Provider"; + case REACT_FORWARD_REF_TYPE: + return getWrappedName(type, type.render, "ForwardRef"); + case REACT_MEMO_TYPE: + return getComponentName(type.type); + case REACT_BLOCK_TYPE: + return getComponentName(type._render); + case REACT_LAZY_TYPE: { + var lazyComponent = type; + var payload = lazyComponent._payload; + var init = lazyComponent._init; + try { + return getComponentName(init(payload)); + } catch (x) { + return null; + } + } + } + } + return null; + } + var enableSuspenseServerRenderer = false; + var disabledDepth = 0; + var prevLog; + var prevInfo; + var prevWarn; + var prevError; + var prevGroup; + var prevGroupCollapsed; + var prevGroupEnd; + function disabledLog() { + } + disabledLog.__reactDisabledLog = true; + function disableLogs() { + { + if (disabledDepth === 0) { + prevLog = console.log; + prevInfo = console.info; + prevWarn = console.warn; + prevError = console.error; + prevGroup = console.group; + prevGroupCollapsed = console.groupCollapsed; + prevGroupEnd = console.groupEnd; + var props = { + configurable: true, + enumerable: true, + value: disabledLog, + writable: true + }; + Object.defineProperties(console, { + info: props, + log: props, + warn: props, + error: props, + group: props, + groupCollapsed: props, + groupEnd: props + }); + } + disabledDepth++; + } + } + function reenableLogs() { + { + disabledDepth--; + if (disabledDepth === 0) { + var props = { + configurable: true, + enumerable: true, + writable: true + }; + Object.defineProperties(console, { + log: _assign({}, props, { + value: prevLog + }), + info: _assign({}, props, { + value: prevInfo + }), + warn: _assign({}, props, { + value: prevWarn + }), + error: _assign({}, props, { + value: prevError + }), + group: _assign({}, props, { + value: prevGroup + }), + groupCollapsed: _assign({}, props, { + value: prevGroupCollapsed + }), + groupEnd: _assign({}, props, { + value: prevGroupEnd + }) + }); + } + if (disabledDepth < 0) { + error("disabledDepth fell below zero. This is a bug in React. Please file an issue."); + } + } + } + var ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher; + var prefix; + function describeBuiltInComponentFrame(name, source, ownerFn) { + { + if (prefix === void 0) { + try { + throw Error(); + } catch (x) { + var match = x.stack.trim().match(/\n( *(at )?)/); + prefix = match && match[1] || ""; + } + } + return "\n" + prefix + name; + } + } + var reentry = false; + var componentFrameCache; + { + var PossiblyWeakMap = typeof WeakMap === "function" ? WeakMap : Map; + componentFrameCache = new PossiblyWeakMap(); + } + function describeNativeComponentFrame(fn, construct) { + if (!fn || reentry) { + return ""; + } + { + var frame = componentFrameCache.get(fn); + if (frame !== void 0) { + return frame; + } + } + var control; + reentry = true; + var previousPrepareStackTrace = Error.prepareStackTrace; + Error.prepareStackTrace = void 0; + var previousDispatcher; + { + previousDispatcher = ReactCurrentDispatcher.current; + ReactCurrentDispatcher.current = null; + disableLogs(); + } + try { + if (construct) { + var Fake = function() { + throw Error(); + }; + Object.defineProperty(Fake.prototype, "props", { + set: function() { + throw Error(); + } + }); + if (typeof Reflect === "object" && Reflect.construct) { + try { + Reflect.construct(Fake, []); + } catch (x) { + control = x; + } + Reflect.construct(fn, [], Fake); + } else { + try { + Fake.call(); + } catch (x) { + control = x; + } + fn.call(Fake.prototype); + } + } else { + try { + throw Error(); + } catch (x) { + control = x; + } + fn(); + } + } catch (sample) { + if (sample && control && typeof sample.stack === "string") { + var sampleLines = sample.stack.split("\n"); + var controlLines = control.stack.split("\n"); + var s = sampleLines.length - 1; + var c = controlLines.length - 1; + while (s >= 1 && c >= 0 && sampleLines[s] !== controlLines[c]) { + c--; + } + for (; s >= 1 && c >= 0; s--, c--) { + if (sampleLines[s] !== controlLines[c]) { + if (s !== 1 || c !== 1) { + do { + s--; + c--; + if (c < 0 || sampleLines[s] !== controlLines[c]) { + var _frame = "\n" + sampleLines[s].replace(" at new ", " at "); + { + if (typeof fn === "function") { + componentFrameCache.set(fn, _frame); + } + } + return _frame; + } + } while (s >= 1 && c >= 0); + } + break; + } + } + } + } finally { + reentry = false; + { + ReactCurrentDispatcher.current = previousDispatcher; + reenableLogs(); + } + Error.prepareStackTrace = previousPrepareStackTrace; + } + var name = fn ? fn.displayName || fn.name : ""; + var syntheticFrame = name ? describeBuiltInComponentFrame(name) : ""; + { + if (typeof fn === "function") { + componentFrameCache.set(fn, syntheticFrame); + } + } + return syntheticFrame; + } + function describeFunctionComponentFrame(fn, source, ownerFn) { + { + return describeNativeComponentFrame(fn, false); + } + } + function shouldConstruct(Component) { + var prototype = Component.prototype; + return !!(prototype && prototype.isReactComponent); + } + function describeUnknownElementTypeFrameInDEV(type, source, ownerFn) { + if (type == null) { + return ""; + } + if (typeof type === "function") { + { + return describeNativeComponentFrame(type, shouldConstruct(type)); + } + } + if (typeof type === "string") { + return describeBuiltInComponentFrame(type); + } + switch (type) { + case REACT_SUSPENSE_TYPE: + return describeBuiltInComponentFrame("Suspense"); + case REACT_SUSPENSE_LIST_TYPE: + return describeBuiltInComponentFrame("SuspenseList"); + } + if (typeof type === "object") { + switch (type.$$typeof) { + case REACT_FORWARD_REF_TYPE: + return describeFunctionComponentFrame(type.render); + case REACT_MEMO_TYPE: + return describeUnknownElementTypeFrameInDEV(type.type, source, ownerFn); + case REACT_BLOCK_TYPE: + return describeFunctionComponentFrame(type._render); + case REACT_LAZY_TYPE: { + var lazyComponent = type; + var payload = lazyComponent._payload; + var init = lazyComponent._init; + try { + return describeUnknownElementTypeFrameInDEV(init(payload), source, ownerFn); + } catch (x) { + } + } + } + } + return ""; + } + var loggedTypeFailures = {}; + var ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame; + function setCurrentlyValidatingElement(element) { + { + if (element) { + var owner = element._owner; + var stack = describeUnknownElementTypeFrameInDEV(element.type, element._source, owner ? owner.type : null); + ReactDebugCurrentFrame.setExtraStackFrame(stack); + } else { + ReactDebugCurrentFrame.setExtraStackFrame(null); + } + } + } + function checkPropTypes(typeSpecs, values, location, componentName, element) { + { + var has = Function.call.bind(Object.prototype.hasOwnProperty); + for (var typeSpecName in typeSpecs) { + if (has(typeSpecs, typeSpecName)) { + var error$1 = void 0; + try { + if (typeof typeSpecs[typeSpecName] !== "function") { + var err = Error((componentName || "React class") + ": " + location + " type `" + typeSpecName + "` is invalid; it must be a function, usually from the `prop-types` package, but received `" + typeof typeSpecs[typeSpecName] + "`.This often happens because of typos such as `PropTypes.function` instead of `PropTypes.func`."); + err.name = "Invariant Violation"; + throw err; + } + error$1 = typeSpecs[typeSpecName](values, typeSpecName, componentName, location, null, "SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"); + } catch (ex) { + error$1 = ex; + } + if (error$1 && !(error$1 instanceof Error)) { + setCurrentlyValidatingElement(element); + error("%s: type specification of %s `%s` is invalid; the type checker function must return `null` or an `Error` but returned a %s. You may have forgotten to pass an argument to the type checker creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and shape all require an argument).", componentName || "React class", location, typeSpecName, typeof error$1); + setCurrentlyValidatingElement(null); + } + if (error$1 instanceof Error && !(error$1.message in loggedTypeFailures)) { + loggedTypeFailures[error$1.message] = true; + setCurrentlyValidatingElement(element); + error("Failed %s type: %s", location, error$1.message); + setCurrentlyValidatingElement(null); + } + } + } + } + } + var didWarnAboutInvalidateContextType; + { + didWarnAboutInvalidateContextType = /* @__PURE__ */ new Set(); + } + var emptyObject = {}; + { + Object.freeze(emptyObject); + } + function maskContext(type, context) { + var contextTypes = type.contextTypes; + if (!contextTypes) { + return emptyObject; + } + var maskedContext = {}; + for (var contextName in contextTypes) { + maskedContext[contextName] = context[contextName]; + } + return maskedContext; + } + function checkContextTypes(typeSpecs, values, location) { + { + checkPropTypes(typeSpecs, values, location, "Component"); + } + } + function validateContextBounds(context, threadID) { + for (var i2 = context._threadCount | 0; i2 <= threadID; i2++) { + context[i2] = context._currentValue2; + context._threadCount = i2 + 1; + } + } + function processContext(type, context, threadID, isClass) { + if (isClass) { + var contextType = type.contextType; + { + if ("contextType" in type) { + var isValid = contextType === null || contextType !== void 0 && contextType.$$typeof === REACT_CONTEXT_TYPE && contextType._context === void 0; + if (!isValid && !didWarnAboutInvalidateContextType.has(type)) { + didWarnAboutInvalidateContextType.add(type); + var addendum = ""; + if (contextType === void 0) { + addendum = " However, it is set to undefined. This can be caused by a typo or by mixing up named and default imports. This can also happen due to a circular dependency, so try moving the createContext() call to a separate file."; + } else if (typeof contextType !== "object") { + addendum = " However, it is set to a " + typeof contextType + "."; + } else if (contextType.$$typeof === REACT_PROVIDER_TYPE) { + addendum = " Did you accidentally pass the Context.Provider instead?"; + } else if (contextType._context !== void 0) { + addendum = " Did you accidentally pass the Context.Consumer instead?"; + } else { + addendum = " However, it is set to an object with keys {" + Object.keys(contextType).join(", ") + "}."; + } + error("%s defines an invalid contextType. contextType should point to the Context object returned by React.createContext().%s", getComponentName(type) || "Component", addendum); + } + } + } + if (typeof contextType === "object" && contextType !== null) { + validateContextBounds(contextType, threadID); + return contextType[threadID]; + } + { + var maskedContext = maskContext(type, context); + { + if (type.contextTypes) { + checkContextTypes(type.contextTypes, maskedContext, "context"); + } + } + return maskedContext; + } + } else { + { + var _maskedContext = maskContext(type, context); + { + if (type.contextTypes) { + checkContextTypes(type.contextTypes, _maskedContext, "context"); + } + } + return _maskedContext; + } + } + } + var nextAvailableThreadIDs = new Uint16Array(16); + for (var i = 0; i < 15; i++) { + nextAvailableThreadIDs[i] = i + 1; + } + nextAvailableThreadIDs[15] = 0; + function growThreadCountAndReturnNextAvailable() { + var oldArray = nextAvailableThreadIDs; + var oldSize = oldArray.length; + var newSize = oldSize * 2; + if (!(newSize <= 65536)) { + { + throw Error("Maximum number of concurrent React renderers exceeded. This can happen if you are not properly destroying the Readable provided by React. Ensure that you call .destroy() on it if you no longer want to read from it, and did not read to the end. If you use .pipe() this should be automatic."); + } + } + var newArray = new Uint16Array(newSize); + newArray.set(oldArray); + nextAvailableThreadIDs = newArray; + nextAvailableThreadIDs[0] = oldSize + 1; + for (var _i = oldSize; _i < newSize - 1; _i++) { + nextAvailableThreadIDs[_i] = _i + 1; + } + nextAvailableThreadIDs[newSize - 1] = 0; + return oldSize; + } + function allocThreadID() { + var nextID = nextAvailableThreadIDs[0]; + if (nextID === 0) { + return growThreadCountAndReturnNextAvailable(); + } + nextAvailableThreadIDs[0] = nextAvailableThreadIDs[nextID]; + return nextID; + } + function freeThreadID(id) { + nextAvailableThreadIDs[id] = nextAvailableThreadIDs[0]; + nextAvailableThreadIDs[0] = id; + } + var RESERVED = 0; + var STRING = 1; + var BOOLEANISH_STRING = 2; + var BOOLEAN = 3; + var OVERLOADED_BOOLEAN = 4; + var NUMERIC = 5; + var POSITIVE_NUMERIC = 6; + var ATTRIBUTE_NAME_START_CHAR = ":A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD"; + var ATTRIBUTE_NAME_CHAR = ATTRIBUTE_NAME_START_CHAR + "\\-.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040"; + var ROOT_ATTRIBUTE_NAME = "data-reactroot"; + var VALID_ATTRIBUTE_NAME_REGEX = new RegExp("^[" + ATTRIBUTE_NAME_START_CHAR + "][" + ATTRIBUTE_NAME_CHAR + "]*$"); + var hasOwnProperty = Object.prototype.hasOwnProperty; + var illegalAttributeNameCache = {}; + var validatedAttributeNameCache = {}; + function isAttributeNameSafe(attributeName) { + if (hasOwnProperty.call(validatedAttributeNameCache, attributeName)) { + return true; + } + if (hasOwnProperty.call(illegalAttributeNameCache, attributeName)) { + return false; + } + if (VALID_ATTRIBUTE_NAME_REGEX.test(attributeName)) { + validatedAttributeNameCache[attributeName] = true; + return true; + } + illegalAttributeNameCache[attributeName] = true; + { + error("Invalid attribute name: `%s`", attributeName); + } + return false; + } + function shouldIgnoreAttribute(name, propertyInfo, isCustomComponentTag) { + if (propertyInfo !== null) { + return propertyInfo.type === RESERVED; + } + if (isCustomComponentTag) { + return false; + } + if (name.length > 2 && (name[0] === "o" || name[0] === "O") && (name[1] === "n" || name[1] === "N")) { + return true; + } + return false; + } + function shouldRemoveAttributeWithWarning(name, value, propertyInfo, isCustomComponentTag) { + if (propertyInfo !== null && propertyInfo.type === RESERVED) { + return false; + } + switch (typeof value) { + case "function": + case "symbol": + return true; + case "boolean": { + if (isCustomComponentTag) { + return false; + } + if (propertyInfo !== null) { + return !propertyInfo.acceptsBooleans; + } else { + var prefix2 = name.toLowerCase().slice(0, 5); + return prefix2 !== "data-" && prefix2 !== "aria-"; + } + } + default: + return false; + } + } + function shouldRemoveAttribute(name, value, propertyInfo, isCustomComponentTag) { + if (value === null || typeof value === "undefined") { + return true; + } + if (shouldRemoveAttributeWithWarning(name, value, propertyInfo, isCustomComponentTag)) { + return true; + } + if (isCustomComponentTag) { + return false; + } + if (propertyInfo !== null) { + switch (propertyInfo.type) { + case BOOLEAN: + return !value; + case OVERLOADED_BOOLEAN: + return value === false; + case NUMERIC: + return isNaN(value); + case POSITIVE_NUMERIC: + return isNaN(value) || value < 1; + } + } + return false; + } + function getPropertyInfo(name) { + return properties.hasOwnProperty(name) ? properties[name] : null; + } + function PropertyInfoRecord(name, type, mustUseProperty, attributeName, attributeNamespace, sanitizeURL2, removeEmptyString) { + this.acceptsBooleans = type === BOOLEANISH_STRING || type === BOOLEAN || type === OVERLOADED_BOOLEAN; + this.attributeName = attributeName; + this.attributeNamespace = attributeNamespace; + this.mustUseProperty = mustUseProperty; + this.propertyName = name; + this.type = type; + this.sanitizeURL = sanitizeURL2; + this.removeEmptyString = removeEmptyString; + } + var properties = {}; + var reservedProps = [ + "children", + "dangerouslySetInnerHTML", + "defaultValue", + "defaultChecked", + "innerHTML", + "suppressContentEditableWarning", + "suppressHydrationWarning", + "style" + ]; + reservedProps.forEach(function(name) { + properties[name] = new PropertyInfoRecord(name, RESERVED, false, name, null, false, false); + }); + [["acceptCharset", "accept-charset"], ["className", "class"], ["htmlFor", "for"], ["httpEquiv", "http-equiv"]].forEach(function(_ref) { + var name = _ref[0], attributeName = _ref[1]; + properties[name] = new PropertyInfoRecord(name, STRING, false, attributeName, null, false, false); + }); + ["contentEditable", "draggable", "spellCheck", "value"].forEach(function(name) { + properties[name] = new PropertyInfoRecord(name, BOOLEANISH_STRING, false, name.toLowerCase(), null, false, false); + }); + ["autoReverse", "externalResourcesRequired", "focusable", "preserveAlpha"].forEach(function(name) { + properties[name] = new PropertyInfoRecord(name, BOOLEANISH_STRING, false, name, null, false, false); + }); + [ + "allowFullScreen", + "async", + "autoFocus", + "autoPlay", + "controls", + "default", + "defer", + "disabled", + "disablePictureInPicture", + "disableRemotePlayback", + "formNoValidate", + "hidden", + "loop", + "noModule", + "noValidate", + "open", + "playsInline", + "readOnly", + "required", + "reversed", + "scoped", + "seamless", + "itemScope" + ].forEach(function(name) { + properties[name] = new PropertyInfoRecord(name, BOOLEAN, false, name.toLowerCase(), null, false, false); + }); + [ + "checked", + "multiple", + "muted", + "selected" + ].forEach(function(name) { + properties[name] = new PropertyInfoRecord(name, BOOLEAN, true, name, null, false, false); + }); + [ + "capture", + "download" + ].forEach(function(name) { + properties[name] = new PropertyInfoRecord(name, OVERLOADED_BOOLEAN, false, name, null, false, false); + }); + [ + "cols", + "rows", + "size", + "span" + ].forEach(function(name) { + properties[name] = new PropertyInfoRecord(name, POSITIVE_NUMERIC, false, name, null, false, false); + }); + ["rowSpan", "start"].forEach(function(name) { + properties[name] = new PropertyInfoRecord(name, NUMERIC, false, name.toLowerCase(), null, false, false); + }); + var CAMELIZE = /[\-\:]([a-z])/g; + var capitalize = function(token) { + return token[1].toUpperCase(); + }; + [ + "accent-height", + "alignment-baseline", + "arabic-form", + "baseline-shift", + "cap-height", + "clip-path", + "clip-rule", + "color-interpolation", + "color-interpolation-filters", + "color-profile", + "color-rendering", + "dominant-baseline", + "enable-background", + "fill-opacity", + "fill-rule", + "flood-color", + "flood-opacity", + "font-family", + "font-size", + "font-size-adjust", + "font-stretch", + "font-style", + "font-variant", + "font-weight", + "glyph-name", + "glyph-orientation-horizontal", + "glyph-orientation-vertical", + "horiz-adv-x", + "horiz-origin-x", + "image-rendering", + "letter-spacing", + "lighting-color", + "marker-end", + "marker-mid", + "marker-start", + "overline-position", + "overline-thickness", + "paint-order", + "panose-1", + "pointer-events", + "rendering-intent", + "shape-rendering", + "stop-color", + "stop-opacity", + "strikethrough-position", + "strikethrough-thickness", + "stroke-dasharray", + "stroke-dashoffset", + "stroke-linecap", + "stroke-linejoin", + "stroke-miterlimit", + "stroke-opacity", + "stroke-width", + "text-anchor", + "text-decoration", + "text-rendering", + "underline-position", + "underline-thickness", + "unicode-bidi", + "unicode-range", + "units-per-em", + "v-alphabetic", + "v-hanging", + "v-ideographic", + "v-mathematical", + "vector-effect", + "vert-adv-y", + "vert-origin-x", + "vert-origin-y", + "word-spacing", + "writing-mode", + "xmlns:xlink", + "x-height" + ].forEach(function(attributeName) { + var name = attributeName.replace(CAMELIZE, capitalize); + properties[name] = new PropertyInfoRecord(name, STRING, false, attributeName, null, false, false); + }); + [ + "xlink:actuate", + "xlink:arcrole", + "xlink:role", + "xlink:show", + "xlink:title", + "xlink:type" + ].forEach(function(attributeName) { + var name = attributeName.replace(CAMELIZE, capitalize); + properties[name] = new PropertyInfoRecord(name, STRING, false, attributeName, "http://www.w3.org/1999/xlink", false, false); + }); + [ + "xml:base", + "xml:lang", + "xml:space" + ].forEach(function(attributeName) { + var name = attributeName.replace(CAMELIZE, capitalize); + properties[name] = new PropertyInfoRecord(name, STRING, false, attributeName, "http://www.w3.org/XML/1998/namespace", false, false); + }); + ["tabIndex", "crossOrigin"].forEach(function(attributeName) { + properties[attributeName] = new PropertyInfoRecord(attributeName, STRING, false, attributeName.toLowerCase(), null, false, false); + }); + var xlinkHref = "xlinkHref"; + properties[xlinkHref] = new PropertyInfoRecord("xlinkHref", STRING, false, "xlink:href", "http://www.w3.org/1999/xlink", true, false); + ["src", "href", "action", "formAction"].forEach(function(attributeName) { + properties[attributeName] = new PropertyInfoRecord(attributeName, STRING, false, attributeName.toLowerCase(), null, true, true); + }); + var isJavaScriptProtocol = /^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*\:/i; + var didWarn = false; + function sanitizeURL(url) { + { + if (!didWarn && isJavaScriptProtocol.test(url)) { + didWarn = true; + error("A future version of React will block javascript: URLs as a security precaution. Use event handlers instead if you can. If you need to generate unsafe HTML try using dangerouslySetInnerHTML instead. React was passed %s.", JSON.stringify(url)); + } + } + } + var matchHtmlRegExp = /["'&<>]/; + function escapeHtml(string) { + var str = "" + string; + var match = matchHtmlRegExp.exec(str); + if (!match) { + return str; + } + var escape; + var html = ""; + var index; + var lastIndex = 0; + for (index = match.index; index < str.length; index++) { + switch (str.charCodeAt(index)) { + case 34: + escape = """; + break; + case 38: + escape = "&"; + break; + case 39: + escape = "'"; + break; + case 60: + escape = "<"; + break; + case 62: + escape = ">"; + break; + default: + continue; + } + if (lastIndex !== index) { + html += str.substring(lastIndex, index); + } + lastIndex = index + 1; + html += escape; + } + return lastIndex !== index ? html + str.substring(lastIndex, index) : html; + } + function escapeTextForBrowser(text) { + if (typeof text === "boolean" || typeof text === "number") { + return "" + text; + } + return escapeHtml(text); + } + function quoteAttributeValueForBrowser(value) { + return '"' + escapeTextForBrowser(value) + '"'; + } + function createMarkupForRoot() { + return ROOT_ATTRIBUTE_NAME + '=""'; + } + function createMarkupForProperty(name, value) { + var propertyInfo = getPropertyInfo(name); + if (name !== "style" && shouldIgnoreAttribute(name, propertyInfo, false)) { + return ""; + } + if (shouldRemoveAttribute(name, value, propertyInfo, false)) { + return ""; + } + if (propertyInfo !== null) { + var attributeName = propertyInfo.attributeName; + var type = propertyInfo.type; + if (type === BOOLEAN || type === OVERLOADED_BOOLEAN && value === true) { + return attributeName + '=""'; + } else { + if (propertyInfo.sanitizeURL) { + value = "" + value; + sanitizeURL(value); + } + return attributeName + "=" + quoteAttributeValueForBrowser(value); + } + } else if (isAttributeNameSafe(name)) { + return name + "=" + quoteAttributeValueForBrowser(value); + } + return ""; + } + function createMarkupForCustomAttribute(name, value) { + if (!isAttributeNameSafe(name) || value == null) { + return ""; + } + return name + "=" + quoteAttributeValueForBrowser(value); + } + function is(x, y) { + return x === y && (x !== 0 || 1 / x === 1 / y) || x !== x && y !== y; + } + var objectIs = typeof Object.is === "function" ? Object.is : is; + var currentlyRenderingComponent = null; + var firstWorkInProgressHook = null; + var workInProgressHook = null; + var isReRender = false; + var didScheduleRenderPhaseUpdate = false; + var renderPhaseUpdates = null; + var numberOfReRenders = 0; + var RE_RENDER_LIMIT = 25; + var isInHookUserCodeInDev = false; + var currentHookNameInDev; + function resolveCurrentlyRenderingComponent() { + if (!(currentlyRenderingComponent !== null)) { + { + throw Error("Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:\n1. You might have mismatching versions of React and the renderer (such as React DOM)\n2. You might be breaking the Rules of Hooks\n3. You might have more than one copy of React in the same app\nSee https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem."); + } + } + { + if (isInHookUserCodeInDev) { + error("Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. You can only call Hooks at the top level of your React function. For more information, see https://reactjs.org/link/rules-of-hooks"); + } + } + return currentlyRenderingComponent; + } + function areHookInputsEqual(nextDeps, prevDeps) { + if (prevDeps === null) { + { + error("%s received a final argument during this render, but not during the previous render. Even though the final argument is optional, its type cannot change between renders.", currentHookNameInDev); + } + return false; + } + { + if (nextDeps.length !== prevDeps.length) { + error("The final argument passed to %s changed size between renders. The order and size of this array must remain constant.\n\nPrevious: %s\nIncoming: %s", currentHookNameInDev, "[" + nextDeps.join(", ") + "]", "[" + prevDeps.join(", ") + "]"); + } + } + for (var i2 = 0; i2 < prevDeps.length && i2 < nextDeps.length; i2++) { + if (objectIs(nextDeps[i2], prevDeps[i2])) { + continue; + } + return false; + } + return true; + } + function createHook() { + if (numberOfReRenders > 0) { + { + { + throw Error("Rendered more hooks than during the previous render"); + } + } + } + return { + memoizedState: null, + queue: null, + next: null + }; + } + function createWorkInProgressHook() { + if (workInProgressHook === null) { + if (firstWorkInProgressHook === null) { + isReRender = false; + firstWorkInProgressHook = workInProgressHook = createHook(); + } else { + isReRender = true; + workInProgressHook = firstWorkInProgressHook; + } + } else { + if (workInProgressHook.next === null) { + isReRender = false; + workInProgressHook = workInProgressHook.next = createHook(); + } else { + isReRender = true; + workInProgressHook = workInProgressHook.next; + } + } + return workInProgressHook; + } + function prepareToUseHooks(componentIdentity) { + currentlyRenderingComponent = componentIdentity; + { + isInHookUserCodeInDev = false; + } + } + function finishHooks(Component, props, children, refOrContext) { + while (didScheduleRenderPhaseUpdate) { + didScheduleRenderPhaseUpdate = false; + numberOfReRenders += 1; + workInProgressHook = null; + children = Component(props, refOrContext); + } + resetHooksState(); + return children; + } + function resetHooksState() { + { + isInHookUserCodeInDev = false; + } + currentlyRenderingComponent = null; + didScheduleRenderPhaseUpdate = false; + firstWorkInProgressHook = null; + numberOfReRenders = 0; + renderPhaseUpdates = null; + workInProgressHook = null; + } + function readContext(context, observedBits) { + var threadID = currentPartialRenderer.threadID; + validateContextBounds(context, threadID); + { + if (isInHookUserCodeInDev) { + error("Context can only be read while React is rendering. In classes, you can read it in the render method or getDerivedStateFromProps. In function components, you can read it directly in the function body, but not inside Hooks like useReducer() or useMemo()."); + } + } + return context[threadID]; + } + function useContext(context, observedBits) { + { + currentHookNameInDev = "useContext"; + } + resolveCurrentlyRenderingComponent(); + var threadID = currentPartialRenderer.threadID; + validateContextBounds(context, threadID); + return context[threadID]; + } + function basicStateReducer(state, action) { + return typeof action === "function" ? action(state) : action; + } + function useState(initialState) { + { + currentHookNameInDev = "useState"; + } + return useReducer(basicStateReducer, initialState); + } + function useReducer(reducer, initialArg, init) { + { + if (reducer !== basicStateReducer) { + currentHookNameInDev = "useReducer"; + } + } + currentlyRenderingComponent = resolveCurrentlyRenderingComponent(); + workInProgressHook = createWorkInProgressHook(); + if (isReRender) { + var queue = workInProgressHook.queue; + var dispatch = queue.dispatch; + if (renderPhaseUpdates !== null) { + var firstRenderPhaseUpdate = renderPhaseUpdates.get(queue); + if (firstRenderPhaseUpdate !== void 0) { + renderPhaseUpdates.delete(queue); + var newState = workInProgressHook.memoizedState; + var update = firstRenderPhaseUpdate; + do { + var action = update.action; + { + isInHookUserCodeInDev = true; + } + newState = reducer(newState, action); + { + isInHookUserCodeInDev = false; + } + update = update.next; + } while (update !== null); + workInProgressHook.memoizedState = newState; + return [newState, dispatch]; + } + } + return [workInProgressHook.memoizedState, dispatch]; + } else { + { + isInHookUserCodeInDev = true; + } + var initialState; + if (reducer === basicStateReducer) { + initialState = typeof initialArg === "function" ? initialArg() : initialArg; + } else { + initialState = init !== void 0 ? init(initialArg) : initialArg; + } + { + isInHookUserCodeInDev = false; + } + workInProgressHook.memoizedState = initialState; + var _queue = workInProgressHook.queue = { + last: null, + dispatch: null + }; + var _dispatch = _queue.dispatch = dispatchAction.bind(null, currentlyRenderingComponent, _queue); + return [workInProgressHook.memoizedState, _dispatch]; + } + } + function useMemo(nextCreate, deps) { + currentlyRenderingComponent = resolveCurrentlyRenderingComponent(); + workInProgressHook = createWorkInProgressHook(); + var nextDeps = deps === void 0 ? null : deps; + if (workInProgressHook !== null) { + var prevState = workInProgressHook.memoizedState; + if (prevState !== null) { + if (nextDeps !== null) { + var prevDeps = prevState[1]; + if (areHookInputsEqual(nextDeps, prevDeps)) { + return prevState[0]; + } + } + } + } + { + isInHookUserCodeInDev = true; + } + var nextValue = nextCreate(); + { + isInHookUserCodeInDev = false; + } + workInProgressHook.memoizedState = [nextValue, nextDeps]; + return nextValue; + } + function useRef(initialValue) { + currentlyRenderingComponent = resolveCurrentlyRenderingComponent(); + workInProgressHook = createWorkInProgressHook(); + var previousRef = workInProgressHook.memoizedState; + if (previousRef === null) { + var ref = { + current: initialValue + }; + { + Object.seal(ref); + } + workInProgressHook.memoizedState = ref; + return ref; + } else { + return previousRef; + } + } + function useLayoutEffect(create, inputs) { + { + currentHookNameInDev = "useLayoutEffect"; + error("useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI. To avoid this, useLayoutEffect should only be used in components that render exclusively on the client. See https://reactjs.org/link/uselayouteffect-ssr for common fixes."); + } + } + function dispatchAction(componentIdentity, queue, action) { + if (!(numberOfReRenders < RE_RENDER_LIMIT)) { + { + throw Error("Too many re-renders. React limits the number of renders to prevent an infinite loop."); + } + } + if (componentIdentity === currentlyRenderingComponent) { + didScheduleRenderPhaseUpdate = true; + var update = { + action, + next: null + }; + if (renderPhaseUpdates === null) { + renderPhaseUpdates = /* @__PURE__ */ new Map(); + } + var firstRenderPhaseUpdate = renderPhaseUpdates.get(queue); + if (firstRenderPhaseUpdate === void 0) { + renderPhaseUpdates.set(queue, update); + } else { + var lastRenderPhaseUpdate = firstRenderPhaseUpdate; + while (lastRenderPhaseUpdate.next !== null) { + lastRenderPhaseUpdate = lastRenderPhaseUpdate.next; + } + lastRenderPhaseUpdate.next = update; + } + } + } + function useCallback(callback, deps) { + return useMemo(function() { + return callback; + }, deps); + } + function useMutableSource(source, getSnapshot, subscribe) { + resolveCurrentlyRenderingComponent(); + return getSnapshot(source._source); + } + function useDeferredValue(value) { + resolveCurrentlyRenderingComponent(); + return value; + } + function useTransition() { + resolveCurrentlyRenderingComponent(); + var startTransition = function(callback) { + callback(); + }; + return [startTransition, false]; + } + function useOpaqueIdentifier() { + return (currentPartialRenderer.identifierPrefix || "") + "R:" + (currentPartialRenderer.uniqueID++).toString(36); + } + function noop() { + } + var currentPartialRenderer = null; + function setCurrentPartialRenderer(renderer) { + currentPartialRenderer = renderer; + } + var Dispatcher = { + readContext, + useContext, + useMemo, + useReducer, + useRef, + useState, + useLayoutEffect, + useCallback, + useImperativeHandle: noop, + useEffect: noop, + useDebugValue: noop, + useDeferredValue, + useTransition, + useOpaqueIdentifier, + useMutableSource + }; + var HTML_NAMESPACE = "http://www.w3.org/1999/xhtml"; + var MATH_NAMESPACE = "http://www.w3.org/1998/Math/MathML"; + var SVG_NAMESPACE = "http://www.w3.org/2000/svg"; + var Namespaces = { + html: HTML_NAMESPACE, + mathml: MATH_NAMESPACE, + svg: SVG_NAMESPACE + }; + function getIntrinsicNamespace(type) { + switch (type) { + case "svg": + return SVG_NAMESPACE; + case "math": + return MATH_NAMESPACE; + default: + return HTML_NAMESPACE; + } + } + function getChildNamespace(parentNamespace, type) { + if (parentNamespace == null || parentNamespace === HTML_NAMESPACE) { + return getIntrinsicNamespace(type); + } + if (parentNamespace === SVG_NAMESPACE && type === "foreignObject") { + return HTML_NAMESPACE; + } + return parentNamespace; + } + var hasReadOnlyValue = { + button: true, + checkbox: true, + image: true, + hidden: true, + radio: true, + reset: true, + submit: true + }; + function checkControlledValueProps(tagName, props) { + { + if (!(hasReadOnlyValue[props.type] || props.onChange || props.onInput || props.readOnly || props.disabled || props.value == null)) { + error("You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set either `onChange` or `readOnly`."); + } + if (!(props.onChange || props.readOnly || props.disabled || props.checked == null)) { + error("You provided a `checked` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultChecked`. Otherwise, set either `onChange` or `readOnly`."); + } + } + } + var omittedCloseTags = { + area: true, + base: true, + br: true, + col: true, + embed: true, + hr: true, + img: true, + input: true, + keygen: true, + link: true, + meta: true, + param: true, + source: true, + track: true, + wbr: true + }; + var voidElementTags = _assign({ + menuitem: true + }, omittedCloseTags); + var HTML = "__html"; + function assertValidProps(tag, props) { + if (!props) { + return; + } + if (voidElementTags[tag]) { + if (!(props.children == null && props.dangerouslySetInnerHTML == null)) { + { + throw Error(tag + " is a void element tag and must neither have `children` nor use `dangerouslySetInnerHTML`."); + } + } + } + if (props.dangerouslySetInnerHTML != null) { + if (!(props.children == null)) { + { + throw Error("Can only set one of `children` or `props.dangerouslySetInnerHTML`."); + } + } + if (!(typeof props.dangerouslySetInnerHTML === "object" && HTML in props.dangerouslySetInnerHTML)) { + { + throw Error("`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. Please visit https://reactjs.org/link/dangerously-set-inner-html for more information."); + } + } + } + { + if (!props.suppressContentEditableWarning && props.contentEditable && props.children != null) { + error("A component is `contentEditable` and contains `children` managed by React. It is now your responsibility to guarantee that none of those nodes are unexpectedly modified or duplicated. This is probably not intentional."); + } + } + if (!(props.style == null || typeof props.style === "object")) { + { + throw Error("The `style` prop expects a mapping from style properties to values, not a string. For example, style={{marginRight: spacing + 'em'}} when using JSX."); + } + } + } + var isUnitlessNumber = { + animationIterationCount: true, + borderImageOutset: true, + borderImageSlice: true, + borderImageWidth: true, + boxFlex: true, + boxFlexGroup: true, + boxOrdinalGroup: true, + columnCount: true, + columns: true, + flex: true, + flexGrow: true, + flexPositive: true, + flexShrink: true, + flexNegative: true, + flexOrder: true, + gridArea: true, + gridRow: true, + gridRowEnd: true, + gridRowSpan: true, + gridRowStart: true, + gridColumn: true, + gridColumnEnd: true, + gridColumnSpan: true, + gridColumnStart: true, + fontWeight: true, + lineClamp: true, + lineHeight: true, + opacity: true, + order: true, + orphans: true, + tabSize: true, + widows: true, + zIndex: true, + zoom: true, + fillOpacity: true, + floodOpacity: true, + stopOpacity: true, + strokeDasharray: true, + strokeDashoffset: true, + strokeMiterlimit: true, + strokeOpacity: true, + strokeWidth: true + }; + function prefixKey(prefix2, key) { + return prefix2 + key.charAt(0).toUpperCase() + key.substring(1); + } + var prefixes = ["Webkit", "ms", "Moz", "O"]; + Object.keys(isUnitlessNumber).forEach(function(prop) { + prefixes.forEach(function(prefix2) { + isUnitlessNumber[prefixKey(prefix2, prop)] = isUnitlessNumber[prop]; + }); + }); + function dangerousStyleValue(name, value, isCustomProperty) { + var isEmpty = value == null || typeof value === "boolean" || value === ""; + if (isEmpty) { + return ""; + } + if (!isCustomProperty && typeof value === "number" && value !== 0 && !(isUnitlessNumber.hasOwnProperty(name) && isUnitlessNumber[name])) { + return value + "px"; + } + return ("" + value).trim(); + } + var uppercasePattern = /([A-Z])/g; + var msPattern = /^ms-/; + function hyphenateStyleName(name) { + return name.replace(uppercasePattern, "-$1").toLowerCase().replace(msPattern, "-ms-"); + } + function isCustomComponent(tagName, props) { + if (tagName.indexOf("-") === -1) { + return typeof props.is === "string"; + } + switch (tagName) { + case "annotation-xml": + case "color-profile": + case "font-face": + case "font-face-src": + case "font-face-uri": + case "font-face-format": + case "font-face-name": + case "missing-glyph": + return false; + default: + return true; + } + } + var warnValidStyle = function() { + }; + { + var badVendoredStyleNamePattern = /^(?:webkit|moz|o)[A-Z]/; + var msPattern$1 = /^-ms-/; + var hyphenPattern = /-(.)/g; + var badStyleValueWithSemicolonPattern = /;\s*$/; + var warnedStyleNames = {}; + var warnedStyleValues = {}; + var warnedForNaNValue = false; + var warnedForInfinityValue = false; + var camelize = function(string) { + return string.replace(hyphenPattern, function(_, character) { + return character.toUpperCase(); + }); + }; + var warnHyphenatedStyleName = function(name) { + if (warnedStyleNames.hasOwnProperty(name) && warnedStyleNames[name]) { + return; + } + warnedStyleNames[name] = true; + error("Unsupported style property %s. Did you mean %s?", name, camelize(name.replace(msPattern$1, "ms-"))); + }; + var warnBadVendoredStyleName = function(name) { + if (warnedStyleNames.hasOwnProperty(name) && warnedStyleNames[name]) { + return; + } + warnedStyleNames[name] = true; + error("Unsupported vendor-prefixed style property %s. Did you mean %s?", name, name.charAt(0).toUpperCase() + name.slice(1)); + }; + var warnStyleValueWithSemicolon = function(name, value) { + if (warnedStyleValues.hasOwnProperty(value) && warnedStyleValues[value]) { + return; + } + warnedStyleValues[value] = true; + error(`Style property values shouldn't contain a semicolon. Try "%s: %s" instead.`, name, value.replace(badStyleValueWithSemicolonPattern, "")); + }; + var warnStyleValueIsNaN = function(name, value) { + if (warnedForNaNValue) { + return; + } + warnedForNaNValue = true; + error("`NaN` is an invalid value for the `%s` css style property.", name); + }; + var warnStyleValueIsInfinity = function(name, value) { + if (warnedForInfinityValue) { + return; + } + warnedForInfinityValue = true; + error("`Infinity` is an invalid value for the `%s` css style property.", name); + }; + warnValidStyle = function(name, value) { + if (name.indexOf("-") > -1) { + warnHyphenatedStyleName(name); + } else if (badVendoredStyleNamePattern.test(name)) { + warnBadVendoredStyleName(name); + } else if (badStyleValueWithSemicolonPattern.test(value)) { + warnStyleValueWithSemicolon(name, value); + } + if (typeof value === "number") { + if (isNaN(value)) { + warnStyleValueIsNaN(name, value); + } else if (!isFinite(value)) { + warnStyleValueIsInfinity(name, value); + } + } + }; + } + var warnValidStyle$1 = warnValidStyle; + var ariaProperties = { + "aria-current": 0, + "aria-details": 0, + "aria-disabled": 0, + "aria-hidden": 0, + "aria-invalid": 0, + "aria-keyshortcuts": 0, + "aria-label": 0, + "aria-roledescription": 0, + "aria-autocomplete": 0, + "aria-checked": 0, + "aria-expanded": 0, + "aria-haspopup": 0, + "aria-level": 0, + "aria-modal": 0, + "aria-multiline": 0, + "aria-multiselectable": 0, + "aria-orientation": 0, + "aria-placeholder": 0, + "aria-pressed": 0, + "aria-readonly": 0, + "aria-required": 0, + "aria-selected": 0, + "aria-sort": 0, + "aria-valuemax": 0, + "aria-valuemin": 0, + "aria-valuenow": 0, + "aria-valuetext": 0, + "aria-atomic": 0, + "aria-busy": 0, + "aria-live": 0, + "aria-relevant": 0, + "aria-dropeffect": 0, + "aria-grabbed": 0, + "aria-activedescendant": 0, + "aria-colcount": 0, + "aria-colindex": 0, + "aria-colspan": 0, + "aria-controls": 0, + "aria-describedby": 0, + "aria-errormessage": 0, + "aria-flowto": 0, + "aria-labelledby": 0, + "aria-owns": 0, + "aria-posinset": 0, + "aria-rowcount": 0, + "aria-rowindex": 0, + "aria-rowspan": 0, + "aria-setsize": 0 + }; + var warnedProperties = {}; + var rARIA = new RegExp("^(aria)-[" + ATTRIBUTE_NAME_CHAR + "]*$"); + var rARIACamel = new RegExp("^(aria)[A-Z][" + ATTRIBUTE_NAME_CHAR + "]*$"); + var hasOwnProperty$1 = Object.prototype.hasOwnProperty; + function validateProperty(tagName, name) { + { + if (hasOwnProperty$1.call(warnedProperties, name) && warnedProperties[name]) { + return true; + } + if (rARIACamel.test(name)) { + var ariaName = "aria-" + name.slice(4).toLowerCase(); + var correctName = ariaProperties.hasOwnProperty(ariaName) ? ariaName : null; + if (correctName == null) { + error("Invalid ARIA attribute `%s`. ARIA attributes follow the pattern aria-* and must be lowercase.", name); + warnedProperties[name] = true; + return true; + } + if (name !== correctName) { + error("Invalid ARIA attribute `%s`. Did you mean `%s`?", name, correctName); + warnedProperties[name] = true; + return true; + } + } + if (rARIA.test(name)) { + var lowerCasedName = name.toLowerCase(); + var standardName = ariaProperties.hasOwnProperty(lowerCasedName) ? lowerCasedName : null; + if (standardName == null) { + warnedProperties[name] = true; + return false; + } + if (name !== standardName) { + error("Unknown ARIA attribute `%s`. Did you mean `%s`?", name, standardName); + warnedProperties[name] = true; + return true; + } + } + } + return true; + } + function warnInvalidARIAProps(type, props) { + { + var invalidProps = []; + for (var key in props) { + var isValid = validateProperty(type, key); + if (!isValid) { + invalidProps.push(key); + } + } + var unknownPropString = invalidProps.map(function(prop) { + return "`" + prop + "`"; + }).join(", "); + if (invalidProps.length === 1) { + error("Invalid aria prop %s on <%s> tag. For details, see https://reactjs.org/link/invalid-aria-props", unknownPropString, type); + } else if (invalidProps.length > 1) { + error("Invalid aria props %s on <%s> tag. For details, see https://reactjs.org/link/invalid-aria-props", unknownPropString, type); + } + } + } + function validateProperties(type, props) { + if (isCustomComponent(type, props)) { + return; + } + warnInvalidARIAProps(type, props); + } + var didWarnValueNull = false; + function validateProperties$1(type, props) { + { + if (type !== "input" && type !== "textarea" && type !== "select") { + return; + } + if (props != null && props.value === null && !didWarnValueNull) { + didWarnValueNull = true; + if (type === "select" && props.multiple) { + error("`value` prop on `%s` should not be null. Consider using an empty array when `multiple` is set to `true` to clear the component or `undefined` for uncontrolled components.", type); + } else { + error("`value` prop on `%s` should not be null. Consider using an empty string to clear the component or `undefined` for uncontrolled components.", type); + } + } + } + } + var possibleStandardNames = { + accept: "accept", + acceptcharset: "acceptCharset", + "accept-charset": "acceptCharset", + accesskey: "accessKey", + action: "action", + allowfullscreen: "allowFullScreen", + alt: "alt", + as: "as", + async: "async", + autocapitalize: "autoCapitalize", + autocomplete: "autoComplete", + autocorrect: "autoCorrect", + autofocus: "autoFocus", + autoplay: "autoPlay", + autosave: "autoSave", + capture: "capture", + cellpadding: "cellPadding", + cellspacing: "cellSpacing", + challenge: "challenge", + charset: "charSet", + checked: "checked", + children: "children", + cite: "cite", + class: "className", + classid: "classID", + classname: "className", + cols: "cols", + colspan: "colSpan", + content: "content", + contenteditable: "contentEditable", + contextmenu: "contextMenu", + controls: "controls", + controlslist: "controlsList", + coords: "coords", + crossorigin: "crossOrigin", + dangerouslysetinnerhtml: "dangerouslySetInnerHTML", + data: "data", + datetime: "dateTime", + default: "default", + defaultchecked: "defaultChecked", + defaultvalue: "defaultValue", + defer: "defer", + dir: "dir", + disabled: "disabled", + disablepictureinpicture: "disablePictureInPicture", + disableremoteplayback: "disableRemotePlayback", + download: "download", + draggable: "draggable", + enctype: "encType", + enterkeyhint: "enterKeyHint", + for: "htmlFor", + form: "form", + formmethod: "formMethod", + formaction: "formAction", + formenctype: "formEncType", + formnovalidate: "formNoValidate", + formtarget: "formTarget", + frameborder: "frameBorder", + headers: "headers", + height: "height", + hidden: "hidden", + high: "high", + href: "href", + hreflang: "hrefLang", + htmlfor: "htmlFor", + httpequiv: "httpEquiv", + "http-equiv": "httpEquiv", + icon: "icon", + id: "id", + innerhtml: "innerHTML", + inputmode: "inputMode", + integrity: "integrity", + is: "is", + itemid: "itemID", + itemprop: "itemProp", + itemref: "itemRef", + itemscope: "itemScope", + itemtype: "itemType", + keyparams: "keyParams", + keytype: "keyType", + kind: "kind", + label: "label", + lang: "lang", + list: "list", + loop: "loop", + low: "low", + manifest: "manifest", + marginwidth: "marginWidth", + marginheight: "marginHeight", + max: "max", + maxlength: "maxLength", + media: "media", + mediagroup: "mediaGroup", + method: "method", + min: "min", + minlength: "minLength", + multiple: "multiple", + muted: "muted", + name: "name", + nomodule: "noModule", + nonce: "nonce", + novalidate: "noValidate", + open: "open", + optimum: "optimum", + pattern: "pattern", + placeholder: "placeholder", + playsinline: "playsInline", + poster: "poster", + preload: "preload", + profile: "profile", + radiogroup: "radioGroup", + readonly: "readOnly", + referrerpolicy: "referrerPolicy", + rel: "rel", + required: "required", + reversed: "reversed", + role: "role", + rows: "rows", + rowspan: "rowSpan", + sandbox: "sandbox", + scope: "scope", + scoped: "scoped", + scrolling: "scrolling", + seamless: "seamless", + selected: "selected", + shape: "shape", + size: "size", + sizes: "sizes", + span: "span", + spellcheck: "spellCheck", + src: "src", + srcdoc: "srcDoc", + srclang: "srcLang", + srcset: "srcSet", + start: "start", + step: "step", + style: "style", + summary: "summary", + tabindex: "tabIndex", + target: "target", + title: "title", + type: "type", + usemap: "useMap", + value: "value", + width: "width", + wmode: "wmode", + wrap: "wrap", + about: "about", + accentheight: "accentHeight", + "accent-height": "accentHeight", + accumulate: "accumulate", + additive: "additive", + alignmentbaseline: "alignmentBaseline", + "alignment-baseline": "alignmentBaseline", + allowreorder: "allowReorder", + alphabetic: "alphabetic", + amplitude: "amplitude", + arabicform: "arabicForm", + "arabic-form": "arabicForm", + ascent: "ascent", + attributename: "attributeName", + attributetype: "attributeType", + autoreverse: "autoReverse", + azimuth: "azimuth", + basefrequency: "baseFrequency", + baselineshift: "baselineShift", + "baseline-shift": "baselineShift", + baseprofile: "baseProfile", + bbox: "bbox", + begin: "begin", + bias: "bias", + by: "by", + calcmode: "calcMode", + capheight: "capHeight", + "cap-height": "capHeight", + clip: "clip", + clippath: "clipPath", + "clip-path": "clipPath", + clippathunits: "clipPathUnits", + cliprule: "clipRule", + "clip-rule": "clipRule", + color: "color", + colorinterpolation: "colorInterpolation", + "color-interpolation": "colorInterpolation", + colorinterpolationfilters: "colorInterpolationFilters", + "color-interpolation-filters": "colorInterpolationFilters", + colorprofile: "colorProfile", + "color-profile": "colorProfile", + colorrendering: "colorRendering", + "color-rendering": "colorRendering", + contentscripttype: "contentScriptType", + contentstyletype: "contentStyleType", + cursor: "cursor", + cx: "cx", + cy: "cy", + d: "d", + datatype: "datatype", + decelerate: "decelerate", + descent: "descent", + diffuseconstant: "diffuseConstant", + direction: "direction", + display: "display", + divisor: "divisor", + dominantbaseline: "dominantBaseline", + "dominant-baseline": "dominantBaseline", + dur: "dur", + dx: "dx", + dy: "dy", + edgemode: "edgeMode", + elevation: "elevation", + enablebackground: "enableBackground", + "enable-background": "enableBackground", + end: "end", + exponent: "exponent", + externalresourcesrequired: "externalResourcesRequired", + fill: "fill", + fillopacity: "fillOpacity", + "fill-opacity": "fillOpacity", + fillrule: "fillRule", + "fill-rule": "fillRule", + filter: "filter", + filterres: "filterRes", + filterunits: "filterUnits", + floodopacity: "floodOpacity", + "flood-opacity": "floodOpacity", + floodcolor: "floodColor", + "flood-color": "floodColor", + focusable: "focusable", + fontfamily: "fontFamily", + "font-family": "fontFamily", + fontsize: "fontSize", + "font-size": "fontSize", + fontsizeadjust: "fontSizeAdjust", + "font-size-adjust": "fontSizeAdjust", + fontstretch: "fontStretch", + "font-stretch": "fontStretch", + fontstyle: "fontStyle", + "font-style": "fontStyle", + fontvariant: "fontVariant", + "font-variant": "fontVariant", + fontweight: "fontWeight", + "font-weight": "fontWeight", + format: "format", + from: "from", + fx: "fx", + fy: "fy", + g1: "g1", + g2: "g2", + glyphname: "glyphName", + "glyph-name": "glyphName", + glyphorientationhorizontal: "glyphOrientationHorizontal", + "glyph-orientation-horizontal": "glyphOrientationHorizontal", + glyphorientationvertical: "glyphOrientationVertical", + "glyph-orientation-vertical": "glyphOrientationVertical", + glyphref: "glyphRef", + gradienttransform: "gradientTransform", + gradientunits: "gradientUnits", + hanging: "hanging", + horizadvx: "horizAdvX", + "horiz-adv-x": "horizAdvX", + horizoriginx: "horizOriginX", + "horiz-origin-x": "horizOriginX", + ideographic: "ideographic", + imagerendering: "imageRendering", + "image-rendering": "imageRendering", + in2: "in2", + in: "in", + inlist: "inlist", + intercept: "intercept", + k1: "k1", + k2: "k2", + k3: "k3", + k4: "k4", + k: "k", + kernelmatrix: "kernelMatrix", + kernelunitlength: "kernelUnitLength", + kerning: "kerning", + keypoints: "keyPoints", + keysplines: "keySplines", + keytimes: "keyTimes", + lengthadjust: "lengthAdjust", + letterspacing: "letterSpacing", + "letter-spacing": "letterSpacing", + lightingcolor: "lightingColor", + "lighting-color": "lightingColor", + limitingconeangle: "limitingConeAngle", + local: "local", + markerend: "markerEnd", + "marker-end": "markerEnd", + markerheight: "markerHeight", + markermid: "markerMid", + "marker-mid": "markerMid", + markerstart: "markerStart", + "marker-start": "markerStart", + markerunits: "markerUnits", + markerwidth: "markerWidth", + mask: "mask", + maskcontentunits: "maskContentUnits", + maskunits: "maskUnits", + mathematical: "mathematical", + mode: "mode", + numoctaves: "numOctaves", + offset: "offset", + opacity: "opacity", + operator: "operator", + order: "order", + orient: "orient", + orientation: "orientation", + origin: "origin", + overflow: "overflow", + overlineposition: "overlinePosition", + "overline-position": "overlinePosition", + overlinethickness: "overlineThickness", + "overline-thickness": "overlineThickness", + paintorder: "paintOrder", + "paint-order": "paintOrder", + panose1: "panose1", + "panose-1": "panose1", + pathlength: "pathLength", + patterncontentunits: "patternContentUnits", + patterntransform: "patternTransform", + patternunits: "patternUnits", + pointerevents: "pointerEvents", + "pointer-events": "pointerEvents", + points: "points", + pointsatx: "pointsAtX", + pointsaty: "pointsAtY", + pointsatz: "pointsAtZ", + prefix: "prefix", + preservealpha: "preserveAlpha", + preserveaspectratio: "preserveAspectRatio", + primitiveunits: "primitiveUnits", + property: "property", + r: "r", + radius: "radius", + refx: "refX", + refy: "refY", + renderingintent: "renderingIntent", + "rendering-intent": "renderingIntent", + repeatcount: "repeatCount", + repeatdur: "repeatDur", + requiredextensions: "requiredExtensions", + requiredfeatures: "requiredFeatures", + resource: "resource", + restart: "restart", + result: "result", + results: "results", + rotate: "rotate", + rx: "rx", + ry: "ry", + scale: "scale", + security: "security", + seed: "seed", + shaperendering: "shapeRendering", + "shape-rendering": "shapeRendering", + slope: "slope", + spacing: "spacing", + specularconstant: "specularConstant", + specularexponent: "specularExponent", + speed: "speed", + spreadmethod: "spreadMethod", + startoffset: "startOffset", + stddeviation: "stdDeviation", + stemh: "stemh", + stemv: "stemv", + stitchtiles: "stitchTiles", + stopcolor: "stopColor", + "stop-color": "stopColor", + stopopacity: "stopOpacity", + "stop-opacity": "stopOpacity", + strikethroughposition: "strikethroughPosition", + "strikethrough-position": "strikethroughPosition", + strikethroughthickness: "strikethroughThickness", + "strikethrough-thickness": "strikethroughThickness", + string: "string", + stroke: "stroke", + strokedasharray: "strokeDasharray", + "stroke-dasharray": "strokeDasharray", + strokedashoffset: "strokeDashoffset", + "stroke-dashoffset": "strokeDashoffset", + strokelinecap: "strokeLinecap", + "stroke-linecap": "strokeLinecap", + strokelinejoin: "strokeLinejoin", + "stroke-linejoin": "strokeLinejoin", + strokemiterlimit: "strokeMiterlimit", + "stroke-miterlimit": "strokeMiterlimit", + strokewidth: "strokeWidth", + "stroke-width": "strokeWidth", + strokeopacity: "strokeOpacity", + "stroke-opacity": "strokeOpacity", + suppresscontenteditablewarning: "suppressContentEditableWarning", + suppresshydrationwarning: "suppressHydrationWarning", + surfacescale: "surfaceScale", + systemlanguage: "systemLanguage", + tablevalues: "tableValues", + targetx: "targetX", + targety: "targetY", + textanchor: "textAnchor", + "text-anchor": "textAnchor", + textdecoration: "textDecoration", + "text-decoration": "textDecoration", + textlength: "textLength", + textrendering: "textRendering", + "text-rendering": "textRendering", + to: "to", + transform: "transform", + typeof: "typeof", + u1: "u1", + u2: "u2", + underlineposition: "underlinePosition", + "underline-position": "underlinePosition", + underlinethickness: "underlineThickness", + "underline-thickness": "underlineThickness", + unicode: "unicode", + unicodebidi: "unicodeBidi", + "unicode-bidi": "unicodeBidi", + unicoderange: "unicodeRange", + "unicode-range": "unicodeRange", + unitsperem: "unitsPerEm", + "units-per-em": "unitsPerEm", + unselectable: "unselectable", + valphabetic: "vAlphabetic", + "v-alphabetic": "vAlphabetic", + values: "values", + vectoreffect: "vectorEffect", + "vector-effect": "vectorEffect", + version: "version", + vertadvy: "vertAdvY", + "vert-adv-y": "vertAdvY", + vertoriginx: "vertOriginX", + "vert-origin-x": "vertOriginX", + vertoriginy: "vertOriginY", + "vert-origin-y": "vertOriginY", + vhanging: "vHanging", + "v-hanging": "vHanging", + videographic: "vIdeographic", + "v-ideographic": "vIdeographic", + viewbox: "viewBox", + viewtarget: "viewTarget", + visibility: "visibility", + vmathematical: "vMathematical", + "v-mathematical": "vMathematical", + vocab: "vocab", + widths: "widths", + wordspacing: "wordSpacing", + "word-spacing": "wordSpacing", + writingmode: "writingMode", + "writing-mode": "writingMode", + x1: "x1", + x2: "x2", + x: "x", + xchannelselector: "xChannelSelector", + xheight: "xHeight", + "x-height": "xHeight", + xlinkactuate: "xlinkActuate", + "xlink:actuate": "xlinkActuate", + xlinkarcrole: "xlinkArcrole", + "xlink:arcrole": "xlinkArcrole", + xlinkhref: "xlinkHref", + "xlink:href": "xlinkHref", + xlinkrole: "xlinkRole", + "xlink:role": "xlinkRole", + xlinkshow: "xlinkShow", + "xlink:show": "xlinkShow", + xlinktitle: "xlinkTitle", + "xlink:title": "xlinkTitle", + xlinktype: "xlinkType", + "xlink:type": "xlinkType", + xmlbase: "xmlBase", + "xml:base": "xmlBase", + xmllang: "xmlLang", + "xml:lang": "xmlLang", + xmlns: "xmlns", + "xml:space": "xmlSpace", + xmlnsxlink: "xmlnsXlink", + "xmlns:xlink": "xmlnsXlink", + xmlspace: "xmlSpace", + y1: "y1", + y2: "y2", + y: "y", + ychannelselector: "yChannelSelector", + z: "z", + zoomandpan: "zoomAndPan" + }; + var validateProperty$1 = function() { + }; + { + var warnedProperties$1 = {}; + var _hasOwnProperty = Object.prototype.hasOwnProperty; + var EVENT_NAME_REGEX = /^on./; + var INVALID_EVENT_NAME_REGEX = /^on[^A-Z]/; + var rARIA$1 = new RegExp("^(aria)-[" + ATTRIBUTE_NAME_CHAR + "]*$"); + var rARIACamel$1 = new RegExp("^(aria)[A-Z][" + ATTRIBUTE_NAME_CHAR + "]*$"); + validateProperty$1 = function(tagName, name, value, eventRegistry) { + if (_hasOwnProperty.call(warnedProperties$1, name) && warnedProperties$1[name]) { + return true; + } + var lowerCasedName = name.toLowerCase(); + if (lowerCasedName === "onfocusin" || lowerCasedName === "onfocusout") { + error("React uses onFocus and onBlur instead of onFocusIn and onFocusOut. All React events are normalized to bubble, so onFocusIn and onFocusOut are not needed/supported by React."); + warnedProperties$1[name] = true; + return true; + } + if (eventRegistry != null) { + var registrationNameDependencies = eventRegistry.registrationNameDependencies, possibleRegistrationNames = eventRegistry.possibleRegistrationNames; + if (registrationNameDependencies.hasOwnProperty(name)) { + return true; + } + var registrationName = possibleRegistrationNames.hasOwnProperty(lowerCasedName) ? possibleRegistrationNames[lowerCasedName] : null; + if (registrationName != null) { + error("Invalid event handler property `%s`. Did you mean `%s`?", name, registrationName); + warnedProperties$1[name] = true; + return true; + } + if (EVENT_NAME_REGEX.test(name)) { + error("Unknown event handler property `%s`. It will be ignored.", name); + warnedProperties$1[name] = true; + return true; + } + } else if (EVENT_NAME_REGEX.test(name)) { + if (INVALID_EVENT_NAME_REGEX.test(name)) { + error("Invalid event handler property `%s`. React events use the camelCase naming convention, for example `onClick`.", name); + } + warnedProperties$1[name] = true; + return true; + } + if (rARIA$1.test(name) || rARIACamel$1.test(name)) { + return true; + } + if (lowerCasedName === "innerhtml") { + error("Directly setting property `innerHTML` is not permitted. For more information, lookup documentation on `dangerouslySetInnerHTML`."); + warnedProperties$1[name] = true; + return true; + } + if (lowerCasedName === "aria") { + error("The `aria` attribute is reserved for future use in React. Pass individual `aria-` attributes instead."); + warnedProperties$1[name] = true; + return true; + } + if (lowerCasedName === "is" && value !== null && value !== void 0 && typeof value !== "string") { + error("Received a `%s` for a string attribute `is`. If this is expected, cast the value to a string.", typeof value); + warnedProperties$1[name] = true; + return true; + } + if (typeof value === "number" && isNaN(value)) { + error("Received NaN for the `%s` attribute. If this is expected, cast the value to a string.", name); + warnedProperties$1[name] = true; + return true; + } + var propertyInfo = getPropertyInfo(name); + var isReserved = propertyInfo !== null && propertyInfo.type === RESERVED; + if (possibleStandardNames.hasOwnProperty(lowerCasedName)) { + var standardName = possibleStandardNames[lowerCasedName]; + if (standardName !== name) { + error("Invalid DOM property `%s`. Did you mean `%s`?", name, standardName); + warnedProperties$1[name] = true; + return true; + } + } else if (!isReserved && name !== lowerCasedName) { + error("React does not recognize the `%s` prop on a DOM element. If you intentionally want it to appear in the DOM as a custom attribute, spell it as lowercase `%s` instead. If you accidentally passed it from a parent component, remove it from the DOM element.", name, lowerCasedName); + warnedProperties$1[name] = true; + return true; + } + if (typeof value === "boolean" && shouldRemoveAttributeWithWarning(name, value, propertyInfo, false)) { + if (value) { + error('Received `%s` for a non-boolean attribute `%s`.\n\nIf you want to write it to the DOM, pass a string instead: %s="%s" or %s={value.toString()}.', value, name, name, value, name); + } else { + error('Received `%s` for a non-boolean attribute `%s`.\n\nIf you want to write it to the DOM, pass a string instead: %s="%s" or %s={value.toString()}.\n\nIf you used to conditionally omit it with %s={condition && value}, pass %s={condition ? value : undefined} instead.', value, name, name, value, name, name, name); + } + warnedProperties$1[name] = true; + return true; + } + if (isReserved) { + return true; + } + if (shouldRemoveAttributeWithWarning(name, value, propertyInfo, false)) { + warnedProperties$1[name] = true; + return false; + } + if ((value === "false" || value === "true") && propertyInfo !== null && propertyInfo.type === BOOLEAN) { + error("Received the string `%s` for the boolean attribute `%s`. %s Did you mean %s={%s}?", value, name, value === "false" ? "The browser will interpret it as a truthy value." : 'Although this works, it will not work as expected if you pass the string "false".', name, value); + warnedProperties$1[name] = true; + return true; + } + return true; + }; + } + var warnUnknownProperties = function(type, props, eventRegistry) { + { + var unknownProps = []; + for (var key in props) { + var isValid = validateProperty$1(type, key, props[key], eventRegistry); + if (!isValid) { + unknownProps.push(key); + } + } + var unknownPropString = unknownProps.map(function(prop) { + return "`" + prop + "`"; + }).join(", "); + if (unknownProps.length === 1) { + error("Invalid value for prop %s on <%s> tag. Either remove it from the element, or pass a string or number value to keep it in the DOM. For details, see https://reactjs.org/link/attribute-behavior ", unknownPropString, type); + } else if (unknownProps.length > 1) { + error("Invalid values for props %s on <%s> tag. Either remove them from the element, or pass a string or number value to keep them in the DOM. For details, see https://reactjs.org/link/attribute-behavior ", unknownPropString, type); + } + } + }; + function validateProperties$2(type, props, eventRegistry) { + if (isCustomComponent(type, props)) { + return; + } + warnUnknownProperties(type, props, eventRegistry); + } + var toArray = React2.Children.toArray; + var currentDebugStacks = []; + var ReactCurrentDispatcher$1 = ReactSharedInternals.ReactCurrentDispatcher; + var ReactDebugCurrentFrame$1; + var prevGetCurrentStackImpl = null; + var getCurrentServerStackImpl = function() { + return ""; + }; + var describeStackFrame = function(element) { + return ""; + }; + var validatePropertiesInDevelopment = function(type, props) { + }; + var pushCurrentDebugStack = function(stack) { + }; + var pushElementToDebugStack = function(element) { + }; + var popCurrentDebugStack = function() { + }; + var hasWarnedAboutUsingContextAsConsumer = false; + { + ReactDebugCurrentFrame$1 = ReactSharedInternals.ReactDebugCurrentFrame; + validatePropertiesInDevelopment = function(type, props) { + validateProperties(type, props); + validateProperties$1(type, props); + validateProperties$2(type, props, null); + }; + describeStackFrame = function(element) { + return describeUnknownElementTypeFrameInDEV(element.type, element._source, null); + }; + pushCurrentDebugStack = function(stack) { + currentDebugStacks.push(stack); + if (currentDebugStacks.length === 1) { + prevGetCurrentStackImpl = ReactDebugCurrentFrame$1.getCurrentStack; + ReactDebugCurrentFrame$1.getCurrentStack = getCurrentServerStackImpl; + } + }; + pushElementToDebugStack = function(element) { + var stack = currentDebugStacks[currentDebugStacks.length - 1]; + var frame = stack[stack.length - 1]; + frame.debugElementStack.push(element); + }; + popCurrentDebugStack = function() { + currentDebugStacks.pop(); + if (currentDebugStacks.length === 0) { + ReactDebugCurrentFrame$1.getCurrentStack = prevGetCurrentStackImpl; + prevGetCurrentStackImpl = null; + } + }; + getCurrentServerStackImpl = function() { + if (currentDebugStacks.length === 0) { + return ""; + } + var frames = currentDebugStacks[currentDebugStacks.length - 1]; + var stack = ""; + for (var i2 = frames.length - 1; i2 >= 0; i2--) { + var frame = frames[i2]; + var debugElementStack = frame.debugElementStack; + for (var ii = debugElementStack.length - 1; ii >= 0; ii--) { + stack += describeStackFrame(debugElementStack[ii]); + } + } + return stack; + }; + } + var didWarnDefaultInputValue = false; + var didWarnDefaultChecked = false; + var didWarnDefaultSelectValue = false; + var didWarnDefaultTextareaValue = false; + var didWarnInvalidOptionChildren = false; + var didWarnAboutNoopUpdateForComponent = {}; + var didWarnAboutBadClass = {}; + var didWarnAboutModulePatternComponent = {}; + var didWarnAboutDeprecatedWillMount = {}; + var didWarnAboutUndefinedDerivedState = {}; + var didWarnAboutUninitializedState = {}; + var valuePropNames = ["value", "defaultValue"]; + var newlineEatingTags = { + listing: true, + pre: true, + textarea: true + }; + var VALID_TAG_REGEX = /^[a-zA-Z][a-zA-Z:_\.\-\d]*$/; + var validatedTagCache = {}; + function validateDangerousTag(tag) { + if (!validatedTagCache.hasOwnProperty(tag)) { + if (!VALID_TAG_REGEX.test(tag)) { + { + throw Error("Invalid tag: " + tag); + } + } + validatedTagCache[tag] = true; + } + } + var styleNameCache = {}; + var processStyleName = function(styleName) { + if (styleNameCache.hasOwnProperty(styleName)) { + return styleNameCache[styleName]; + } + var result = hyphenateStyleName(styleName); + styleNameCache[styleName] = result; + return result; + }; + function createMarkupForStyles(styles) { + var serialized = ""; + var delimiter = ""; + for (var styleName in styles) { + if (!styles.hasOwnProperty(styleName)) { + continue; + } + var isCustomProperty = styleName.indexOf("--") === 0; + var styleValue = styles[styleName]; + { + if (!isCustomProperty) { + warnValidStyle$1(styleName, styleValue); + } + } + if (styleValue != null) { + serialized += delimiter + (isCustomProperty ? styleName : processStyleName(styleName)) + ":"; + serialized += dangerousStyleValue(styleName, styleValue, isCustomProperty); + delimiter = ";"; + } + } + return serialized || null; + } + function warnNoop(publicInstance, callerName) { + { + var _constructor = publicInstance.constructor; + var componentName = _constructor && getComponentName(_constructor) || "ReactClass"; + var warningKey = componentName + "." + callerName; + if (didWarnAboutNoopUpdateForComponent[warningKey]) { + return; + } + error("%s(...): Can only update a mounting component. This usually means you called %s() outside componentWillMount() on the server. This is a no-op.\n\nPlease check the code for the %s component.", callerName, callerName, componentName); + didWarnAboutNoopUpdateForComponent[warningKey] = true; + } + } + function shouldConstruct$1(Component) { + return Component.prototype && Component.prototype.isReactComponent; + } + function getNonChildrenInnerMarkup(props) { + var innerHTML = props.dangerouslySetInnerHTML; + if (innerHTML != null) { + if (innerHTML.__html != null) { + return innerHTML.__html; + } + } else { + var content = props.children; + if (typeof content === "string" || typeof content === "number") { + return escapeTextForBrowser(content); + } + } + return null; + } + function flattenTopLevelChildren(children) { + if (!React2.isValidElement(children)) { + return toArray(children); + } + var element = children; + if (element.type !== REACT_FRAGMENT_TYPE) { + return [element]; + } + var fragmentChildren = element.props.children; + if (!React2.isValidElement(fragmentChildren)) { + return toArray(fragmentChildren); + } + var fragmentChildElement = fragmentChildren; + return [fragmentChildElement]; + } + function flattenOptionChildren(children) { + if (children === void 0 || children === null) { + return children; + } + var content = ""; + React2.Children.forEach(children, function(child) { + if (child == null) { + return; + } + content += child; + { + if (!didWarnInvalidOptionChildren && typeof child !== "string" && typeof child !== "number") { + didWarnInvalidOptionChildren = true; + error("Only strings and numbers are supported as