nzform-inline-模式下多类型控件打乱布局的问题

nzForm 布局被打乱nz-form 布局被打乱的原因表单样式使用行内:[nzLayout]="'inline'"表单中使用多中类型的控件(input、datepicker、select...)会出现日期选择控件没有充满,同时 select 控件会被挤到下一行,看看下面的效果 使用官方的样式类设置统一宽度解决在模板对应的CSS中使用下面的样式设置统一宽度 /* 通过设置下面两个样式的宽度解决 nz-select 打乱 form 布局的问题*//* nz-form-control 外围类 */.ant-form-item-control-wrapper{ width: 152.16px;}/* nz-form-item 样式类*/.ant-form-item{ width: 221.2px;}设置后的效果是:其他解决方法实际不该使用inline模式,而应该使用horizontal模式,一个 nz-form-item 中放置4个 label 和 control因为一个 nz-form-item 是一行

July 12, 2019 · 1 min · jiezi

基于angular7的d3柱状图模板

declare const d3;export class BarChart { svg: any; svgWidth=400; svgHeight=400; padding={top:30,bottom:30,left:30,right:30} rectWidth=30 axisWidth=300 axisHeight=300 constructor(target: HTMLElement) { this.svg=d3.select(target) .attr('width',this.svgWidth) .attr('height',this.svgHeight) } drawBar(data) { let names=['小明','小红','小圆','小贾','小猪'] //坐标轴 let xscale=d3.scalePoint() .domain(names) .rangeRound([0,this.axisWidth]) .padding(0.8) let xaxis=d3.axisBottom(xscale) this.svg.append('g').attr('transform',`translate(${this.padding.left},${this.svgHeight-this.padding.bottom})`).call(xaxis) let yscale=d3.scaleLinear([100,0],[0,this.axisHeight]) let yaxis=d3.axisLeft(yscale) this.svg.append('g') .call(yaxis) .attr('transform',`translate(${this.padding.left},${this.svgHeight-this.axisHeight-this.padding.bottom})`) //数据缩放 data=data.map(d=>yscale(d)) //console.log(data); //y轴原点的纵坐标 let y0=this.svgHeight-this.padding.bottom-this.axisHeight let rect=this.svg.selectAll('rect').data(data) .join(enter=>{ enter.append('rect').attr('fill','red') .attr('x',(d,i)=>xscale(names[i])+this.padding.left-0.5*this.rectWidth) .attr('y',d=>y0+d) .attr('width',this.rectWidth) .attr('height',d=>this.axisHeight-d) },update=>{ update.attr('fill','red') .attr('x',(d,i)=>xscale(names[i])+this.padding.left-0.5*this.rectWidth) .attr('y',d=>y0+d) .attr('width',this.rectWidth) .attr('height',d=>this.axisHeight-d) },exit=>{ exit.remove() }) //console.log(rect); let text=this.svg.selectAll('.text').data(data) .join(enter=>{ enter.append('text') .text(d=>yscale.invert(d)) .attr('x',(d,i)=>xscale(names[i])+this.padding.left) .attr('y',d=>y0+d) .attr('text-anchor','middle') },update=>{ update .text(d=>yscale.invert(d)) .attr('x',(d,i)=>xscale(names[i])+this.padding.left) .attr('y',d=>y0+d) .attr('text-anchor','middle') },exit=>{ exit.remove() }) //console.log(text); } }

July 7, 2019 · 1 min · jiezi

基于angular的组件递归

<div class='items' *ngFor='let data of dataset'> <div > {{data.name}} </div> <app-recursive *ngIf='data.children' [dataset]='data.children'></app-recursive></div>

June 28, 2019 · 1 min · jiezi

DevExtreme-DataGrid-Angular-国际化-带源码

DevExtreme 实现国际化的步骤创建项目根据官方的Getting Started的网址 https://js.devexpress.com/Documentation/Guide/Angular_Components/Getting_Started/Create_a_DevExtreme_Application/创建项目在 cmd 中使用命令 npx -p devextreme-cli devextreme new angular-app i18n 创建项目,该项目名称是 i18n创建成功后使用命令 cd i18n,切换路径到项目内,然后使用 ng serve --o,运行项目并且打开浏览器此时打开浏览器后应该能看到DevExtreme的标准项目页面,如下:下载安装国际化包打开新的CMD 路径切换到项目的根目录下,使用命令 npm install --save-dev devextreme-cldr-data globalize 安装国际化包安装完毕后在目录 D:\Java\Document\Angular\Demo\AAA.DevExtreme\i18n\node_modules 下会多出 globalize 等几个对应的用于国际化的文件夹 制作测试页面显示默认的英文通过DevExtreme CLI 创建项目时生成的模板文件 src/pages/display-data/display-data.component.html,中为表格追加属性 <dxo-editing mode="row" [allowUpdating]="true" [allowDeleting]="true" [allowAdding]="true"> </dxo-editing>最终形成本模板文件的所有代码如下: <h2 class="content-block">Display Data</h2><dx-data-grid class="dx-card wide-card" [dataSource]="dataSource" [showBorders]="false" [focusedRowEnabled]="true" [focusedRowIndex]="0" [columnAutoWidth]="true" [columnHidingEnabled]="true"> <dxo-editing mode="row" [allowUpdating]="true" [allowDeleting]="true" [allowAdding]="true"> </dxo-editing> <dxo-paging [pageSize]="10"></dxo-paging> <dxo-pager [showPageSizeSelector]="true" [showInfo]="true"></dxo-pager> <dxo-filter-row [visible]="true"></dxo-filter-row> <dxi-column dataField="Task_ID" [width]="90" [hidingPriority]="2"> </dxi-column> <dxi-column dataField="Task_Subject" [width]="190" caption="Subject" [hidingPriority]="8"> </dxi-column> <dxi-column dataField="Task_Status" caption="Status" [hidingPriority]="6"> </dxi-column> <dxi-column dataField="Task_Priority" caption="Priority" [hidingPriority]="5"> <dxo-lookup [dataSource]="priority" valueExpr="value" displayExpr="name"> </dxo-lookup> </dxi-column> <dxi-column dataField="ResponsibleEmployee.Employee_Full_Name" caption="Assigned To" [allowSorting]="false" [hidingPriority]="7"> </dxi-column> <dxi-column dataField="Task_Start_Date" caption="Start Date" dataType="date" [hidingPriority]="3"> </dxi-column> <dxi-column dataField="Task_Due_Date" caption="Due Date" dataType="date" [hidingPriority]="4"> </dxi-column> <dxi-column dataField="Task_Priority" caption="Priority" [hidingPriority]="1"> </dxi-column> <dxi-column dataField="Task_Completion" caption="Completion" [hidingPriority]="0"> </dxi-column></dx-data-grid>编译重新运行项目能看到表格右边有删除按钮了,点击后弹出的提示是英文的,如下: ...

June 22, 2019 · 2 min · jiezi

ngzorro组件库日期组件禁止手动输入日期

在使用ng-zorro组件库中的日期控件时,遇到区间输入日期时间的一系列bug,后需要将日期控件禁止手动输入,因为没有相关api所以利用jq操作dom解决1.引入jq库,2.使用日期组件的nzOpenChange事件,在日期面板打开时操作dom将输入框隐藏掉即可

June 18, 2019 · 1 min · jiezi

outlet的理解

作用:在一个组件里面动态的显示另一个组件

June 13, 2019 · 1 min · jiezi

功能齐全的轮播基于angular7

轮播有的功能,应该都有angular2及以上的版本应该都可以动画效果用的是angular自带的animationscss没有做进一步的处理,专注于js做过测试应该没有bug功能1.图片自动轮播2.每张图片对应下面的小圆圈,红色代表当前选中图片3.前进和后退按钮做了节流处理代码html<p> carousel works!</p><ul [@carousel]='state' > <li class='imgP' *ngFor='let img of imgs' (mouseenter)='stop()' (mouseleave)='start()' > <!----> <div *ngIf='img.state'> <img [src]="url+img.name" > </div> </li> </ul><div > <div class='circle' [class.color]='img.state' *ngFor='let img of imgs' (mouseenter)="circleEnter(img.id)" (mouseleave)='circleLeave()' > </div> <div class='btn' (mouseenter)='stop()' (mouseleave)='start()'> <input type="button" value="go" (click)="go()"> <input type="button" value="back" (click)="back()"> </div></div>jsimport { Component, OnInit } from '@angular/core';import {trigger,style,transition,query,animate,group} from '@angular/animations'@Component({ selector: 'app-carousel', templateUrl: './carousel.component.html', styleUrls: ['./carousel.component.css'], animations:[ trigger('carousel',[ transition(':increment',[ group([ query(':enter',[ style({transform:'translateX(-100%)'}), animate('1s') ]), query(':leave',[ animate('1s',style({transform:'translateX(100%)'})) ]) ]) ]), transition(':decrement',[ group([ query(':enter',[ style({transform:'translateX(100%)'}), animate('1s') ]), query(':leave',[ animate('1s',style({transform:'translateX(-100%)'})) ]) ]) ]), ]) ]})export class CarouselComponent implements OnInit { url:string='../../../assets/' imgs=[ {id:1,name:'1.jpg',state:true}, {id:2,name:'2.jpg',state:false}, {id:3,name:'3.jpg',state:false} ] state=0; id:number; i=0; switch=true execTime:number constructor() { } fn(){ this.i++; if(this.i>this.imgs.length-1){ this.i=0; } this.imgs.forEach(val=>{val.state=false}) this.imgs[this.i].state=true; this.state+=0.1; } ngOnInit() { this.id=setInterval(this.fn.bind(this),3000) } stop(){ clearInterval(this.id) } start(){ this.id=setInterval(this.fn.bind(this),3000) } circleEnter(id){ //console.log(id); this.i=id-1; this.imgs.forEach(val=>{val.state=false}) this.imgs[id-1].state=true; //this.state+=0.1; this.stop(); } circleLeave(){ this.start(); } go(){ this.throttle(this.fn.bind(this),1500) //this.fn() } back(){ let fn=function(){ //console.log('exec fn'); this.i--; if(this.i<0){ this.i=this.imgs.length-1; } this.imgs.forEach(val=>{val.state=false}) this.imgs[this.i].state=true; this.state-=0.1; } this.throttle(fn.bind(this),1500) } throttle(fn,interval){ if(new Date().getTime()-this.execTime>interval){this.switch=true} console.log(this.switch); if(this.switch){ fn(); this.switch=false; this.execTime=new Date().getTime(); } } }cssul{ position:relative; height:200px;}.imgP{ position:absolute; list-style:none;}.circle{ width:10px; height:10px; border-radius:50%; border:1px solid #000; float:left;}.color{ background-color:red;}

June 12, 2019 · 1 min · jiezi

angular 路由守卫

路由守卫基础路由守卫作用: 用于在导航到目标组件之前进行验证, 符合条件才会导航到目标组件。路由检查的顺序: 路由器会先按照从最深的子路由由下往上检查的顺序来检查 CanDeactivate() 和 CanActivateChild() 守卫。 然后它会按照从上到下的顺序检查 CanActivate() 守卫。 如果特性模块是异步加载的,在加载它之前还会检查 CanLoad() 守卫。 如果任何一个守卫返回 false,其它尚未完成的守卫会被取消,整个导航就被取消。 惰性加载: 1. 只在用户请求时才加载。惰性加载和重新配置工作只会发生一次,也就是在该路由首次被请求时。在后续的请求中,该模块和路由都是立即可用的。 2. 必须在启动时加载的模块,其他模块都应该惰性加载。预加载: 1. 根据预加载策略,在每次成功的导航后,路由器会在自己的配置中查找尚未加载并且可以预加载的模块进行加载。 2. 当前不需要但是随后马上就需要的模块应该预加载 (守卫接口)Guard interfacesCanActivate 和 CanActivateChildCanActivate:处理导航到某路由的情况。 CanActivateChild:处理导航到某子路由的情况。使用方法: const adminRoutes: Routes = [ { path: ‘’, component: AdminComponent, canActivate: [AuthGuard],//AuthGuard验证通过,才允许访问Admin页面 children: [ { path: ‘’, canActivateChild: [AuthGuard],//AuthGuard验证通过,才允许访问Admin页面的子页面 children: [ { path: ‘crises’, component: ManageCrisesComponent }, { path: ‘heroes’, component: ManageHeroesComponent }, { path: ‘’, component: AdminDashboardComponent } ] } ] } ];应用示例:要求认证工作流程:CanDeactivate 和 Resolve CanDeactivate:处理从当前路由离开的情况。 Resolve:在路由激活之前获取路由数据 使用方法: const crisisCenterRoutes: Routes = [ { path: ‘’, component: CrisisCenterComponent, children: [ { path: ‘’, component: CrisisListComponent, children: [ { path: ‘:id’, component: CrisisDetailComponent, canDeactivate: [CanDeactivateGuard], // CanDeactivate:处理从CrisisDetailComponent路由离开的情况 resolve: { //resolve:当所有必要数据都已经拿到之后,预先加载CrisisDetailComponent的路由数据 crisis: CrisisDetailResolverService } }, { path: ‘’, component: CrisisCenterHomeComponent } ] } ] } ];应用示例:用户尝试不保存或撤销更改就导航到外面。 工作流程:CanLoad CanLoad:处理异步导航到某特性模块的情况。 使用方法: const appRoutes: Routes = [ { path: ‘compose’, component: ComposeMessageComponent, outlet: ‘popup’ }, { path: ‘admin’, loadChildren: ‘./admin/admin.module#AdminModule’, canLoad: [AuthGuard]//保护对特性模块的未授权加载,只有在用户已登录的情况下才加载 AdminModule }, { path: ‘crisis-center’, loadChildren: ‘./crisis-center/crisis-center.module#CrisisCenterModule’, data: { preload: true } }, { path: ‘’, redirectTo: ‘/superheroes’, pathMatch: ‘full’ }, { path: ‘**’, component: PageNotFoundComponent } ]; ...

April 13, 2019 · 1 min · jiezi

Angular脚手架开发

简介写一份自定义的angular脚手架吧写之前我们先解析一下antd的脚手架前提先把 Angular Schematic这篇文章读一遍,确保了解了collection等基础antd脚手架克隆项目git clone https://github.com/NG-ZORRO/ng-zorro-antd.git开始打开项目在schematics下的collection.json为入口,查看内容一共定了了4个schematic,每个schema分别指向了各文件夹的子schema.json,factory指向了函数入口,index.tsng-add/schema.json{ // 指定schema.json的验证模式 “$schema”: “http://json-schema.org/schema", “id”: “nz-ng-add”, “title”: “Ant Design of Angular(NG-ZORRO) ng-add schematic”, “type”: “object”, // 包含的属性 “properties”: { “project”: { “type”: “string”, “description”: “Name of the project.”, “$default”: { “$source”: “projectName” } }, // 是否跳过package.json的安装属性 “skipPackageJson”: { // 类型为布尔 “type”: “boolean”, // 默认值为false “default”: false, // 这是个描述,可以看到,如果在ng add ng-zorro-antd时不希望自动安装可以加入–skipPackageJson配置项 “description”: “Do not add ng-zorro-antd dependencies to package.json (e.g., –skipPackageJson)” }, // 开始页面 “bootPage”: { // 布尔 “type”: “boolean”, // 默认为true “default”: true, // 不指定–bootPage=false的话,你的app.html将会被覆盖成antd的图标页 “description”: “Set up boot page.” }, // 图标配置 “dynamicIcon”: { “type”: “boolean”, “default”: false, “description”: “Whether icon assets should be add.”, “x-prompt”: “Add icon assets [ Detail: https://ng.ant.design/components/icon/en ]” }, // 主题配置 “theme”: { “type”: “boolean”, “default”: false, “description”: “Whether custom theme file should be set up.”, “x-prompt”: “Set up custom theme file [ Detail: https://ng.ant.design/docs/customize-theme/en ]” }, // i18n配置,当你ng add ng-antd-zorro 的时候有没有让你选择这个选项呢? “i18n”: { “type”: “string”, “default”: “en_US”, “enum”: [ “ar_EG”, “bg_BG”, “ca_ES”, “cs_CZ”, “da_DK”, “de_DE”, “el_GR”, “en_GB”, “en_US”, “es_ES”, “et_EE”, “fa_IR”, “fi_FI”, “fr_BE”, “fr_FR”, “is_IS”, “it_IT”, “ja_JP”, “ko_KR”, “nb_NO”, “nl_BE”, “nl_NL”, “pl_PL”, “pt_BR”, “pt_PT”, “sk_SK”, “sr_RS”, “sv_SE”, “th_TH”, “tr_TR”, “ru_RU”, “uk_UA”, “vi_VN”, “zh_CN”, “zh_TW” ], “description”: “add locale code to module (e.g., –locale=en_US)” }, “locale”: { “type”: “string”, “description”: “Add locale code to module (e.g., –locale=en_US)”, “default”: “en_US”, “x-prompt”: { “message”: “Choose your locale code:”, “type”: “list”, “items”: [ “en_US”, “zh_CN”, “ar_EG”, “bg_BG”, “ca_ES”, “cs_CZ”, “de_DE”, “el_GR”, “en_GB”, “es_ES”, “et_EE”, “fa_IR”, “fi_FI”, “fr_BE”, “fr_FR”, “is_IS”, “it_IT”, “ja_JP”, “ko_KR”, “nb_NO”, “nl_BE”, “nl_NL”, “pl_PL”, “pt_BR”, “pt_PT”, “sk_SK”, “sr_RS”, “sv_SE”, “th_TH”, “tr_TR”, “ru_RU”, “uk_UA”, “vi_VN”, “zh_TW” ] } }, “gestures”: { “type”: “boolean”, “default”: false, “description”: “Whether gesture support should be set up.” }, “animations”: { “type”: “boolean”, “default”: true, “description”: “Whether Angular browser animations should be set up.” } }, “required”: []}schema.ts当你进入index.ts时首先看到的是一个带options:Schema的函数,options指向的类型是Schema interface,而这个interface 恰好是schema.json中的properties,也就是cli的传入参数类.我们可以通过自定义传入参数类来完成我们需要的操作.export type Locale = | ‘ar_EG’ | ‘bg_BG’ | ‘ca_ES’ | ‘cs_CZ’ | ‘da_DK’ | ‘de_DE’ | ’el_GR’ | ’en_GB’ | ’en_US’ | ’es_ES’ | ’et_EE’ | ‘fa_IR’ | ‘fi_FI’ | ‘fr_BE’ | ‘fr_FR’ | ‘is_IS’ | ‘it_IT’ | ‘ja_JP’ | ‘ko_KR’ | ’nb_NO’ | ’nl_BE’ | ’nl_NL’ | ‘pl_PL’ | ‘pt_BR’ | ‘pt_PT’ | ‘sk_SK’ | ‘sr_RS’ | ‘sv_SE’ | ’th_TH’ | ’tr_TR’ | ‘ru_RU’ | ‘uk_UA’ | ‘vi_VN’ | ‘zh_CN’ | ‘zh_TW’;export interface Schema { bootPage?: boolean; /** Name of the project to target. / project?: string; /* Whether to skip package.json install. */ skipPackageJson?: boolean; dynamicIcon?: boolean; theme?: boolean; gestures?: boolean; animations?: boolean; locale?: Locale; i18n?: Locale;}ng-add/index.tsimport { Rule, SchematicContext, Tree } from ‘@angular-devkit/schematics’;import { NodePackageInstallTask, RunSchematicTask } from ‘@angular-devkit/schematics/tasks’;import { addPackageToPackageJson } from ‘../utils/package-config’;import { hammerjsVersion, zorroVersion } from ‘../utils/version-names’;import { Schema } from ‘./schema’;// factory指向的index.ts必须实现这个函数,一行一行看代码// 我们的函数是一个更高阶的函数,这意味着它接受或返回一个函数引用。// 在这种情况下,我们的函数返回一个接受Tree和SchematicContext对象的函数。// options:Schema上面提到了export default function(options: Schema): Rule {// tree:虚拟文件系统:用于更改的暂存区域,包含原始文件系统以及要应用于其的更改列表。// rule:A Rule是一个将动作应用于Tree给定的函数SchematicContext。 return (host: Tree, context: SchematicContext) => { // 如果需要安装包,也就是–skipPackageJson=false if (!options.skipPackageJson) { // 调用addPackageToPackageJson,传入,tree文件树,包名,包版本 addPackageToPackageJson(host, ’ng-zorro-antd’, zorroVersion); // hmr模式包 if (options.gestures) { addPackageToPackageJson(host, ‘hammerjs’, hammerjsVersion); } } const installTaskId = context.addTask(new NodePackageInstallTask()); context.addTask(new RunSchematicTask(’ng-add-setup-project’, options), [installTaskId]); if (options.bootPage) { context.addTask(new RunSchematicTask(‘boot-page’, options)); } };}addPackageToPackageJson// 看function名字就知道这是下载依赖的函数// @host:Tree 文件树// @pkg:string 包名// @vserion:string 包版本// @return Tree 返回了一个修改完成后的文件树export function addPackageToPackageJson(host: Tree, pkg: string, version: string): Tree { // 如果文件树里包含package.json文件 if (host.exists(‘package.json’)) { // 读取package.json的内容用utf-8编码 const sourceText = host.read(‘package.json’).toString(‘utf-8’); // 然后把package.json转化为对象,转为对象,转为对象 const json = JSON.parse(sourceText); // 如果package.json对象里没有dependencies属性 if (!json.dependencies) { // 给package对象加入dependencies属性 json.dependencies = {}; } // 如果package对象中没有 pkg(包名),也就是说:如果当前项目没有安装antd if (!json.dependencies[pkg]) { // 那么package的dependencies属性中加入 antd:version json.dependencies[pkg] = version; // 排个序 json.dependencies = sortObjectByKeys(json.dependencies); } // 重写tree下的package.json内容为(刚才不是有package.json对象吗,现在在转回去) host.overwrite(‘package.json’, JSON.stringify(json, null, 2)); } // 把操作好的tree返回给上一级函数 return host;}现在在回过头去看 ng-add/index.ts// 给context对象增加一个安装包的任务,然后拿到了任务idconst installTaskId = context.addTask(new NodePackageInstallTask());// context增加另一个任务,然后传入了一个RunSchematicTask对象,和一个id集合 context.addTask(new RunSchematicTask(’ng-add-setup-project’, options), [installTaskId]);RunSchematicTask(’ng-add-setup-project’)任务ng-add-setup-project定义在了schematic最外层的collection.json里,记住如下4个schematic,后文不再提及{ “$schema”: “./node_modules/@angular-devkit/schematics/collection-schema.json”, “schematics”: { “ng-add”: { “description”: “add NG-ZORRO”, “factory”: “./ng-add/index”, “schema”: “./ng-add/schema.json” }, // 在这里 “ng-add-setup-project”: { “description”: “Sets up the specified project after the ng-add dependencies have been installed.”, “private”: true, // 这个任务的函数指向 “factory”: “./ng-add/setup-project/index”, // 任务配置项 “schema”: “./ng-add/schema.json” }, “boot-page”: { “description”: “Set up boot page”, “private”: true, “factory”: “./ng-generate/boot-page/index”, “schema”: “./ng-generate/boot-page/schema.json” }, “add-icon-assets”: { “description”: “Add icon assets into CLI config”, “factory”: “./ng-add/setup-project/add-icon-assets#addIconToAssets”, “schema”: “./ng-generate/boot-page/schema.json”, “aliases”: [“fix-icon”] } }}ng-add/setup-project// 刚才的index一样,实现了一个函数export default function (options: Schema): Rule { // 这里其实就是调用各种函数的一个集合.options是上面的index.ts中传过来的,配置项在上文有提及 return chain([ addRequiredModules(options), addAnimationsModule(options), registerLocale(options), addThemeToAppStyles(options), options.dynamicIcon ? addIconToAssets(options) : noop(), options.gestures ? hammerjsImport(options) : noop() ]);}addRequiredModules// 模块字典const modulesMap = { NgZorroAntdModule: ’ng-zorro-antd’, FormsModule : ‘@angular/forms’, HttpClientModule : ‘@angular/common/http’};// 加入必须依赖模块export function addRequiredModules(options: Schema): Rule { return (host: Tree) => { // 获取tree下的工作目录 const workspace = getWorkspace(host); // 获取项目 const project = getProjectFromWorkspace(workspace, options.project); // 获取app.module的路径 const appModulePath = getAppModulePath(host, getProjectMainFile(project)); // 循环字典 for (const module in modulesMap) { // 调用下面的函数,意思就是:给appModule引一些模块,好吧,传入了tree,字典key(模块名称),字典value(模块所在包),project对象,appModule的路径,Schema配置项 addModuleImportToApptModule(host, module, modulesMap[ module ], project, appModulePath, options); } // 将构建好的tree返回给上层函数 return host; };}function addModuleImportToApptModule(host: Tree, moduleName: string, src: string, project: WorkspaceProject, appModulePath: string, options: Schema): void { if (hasNgModuleImport(host, appModulePath, moduleName)) { console.log(chalk.yellow(Could not set up "${chalk.blue(moduleName)}" + because "${chalk.blue(moduleName)}" is already imported. Please manually + check "${chalk.blue(appModulePath)}" file.)); return; } addModuleImportToRootModule(host, moduleName, src, project);}未完待续 ...

April 8, 2019 · 4 min · jiezi

在Ionic 或者 Angular 中用 Google Map API 实现自动补全地址(autocomplete)

先上效果图:Github: https://github.com/luchenwei9…实现步骤:环境安装就不提了,无非就是用npm全局安装Ionic 或者 Angular。本文是以Ionic为例。1. 安装type/googlemapsnpm install type/googlemaps -save2. 把Google API Key 声明在你的index.html里申请地址 https://developers.google.com/maps/documentation/javascript/get-api-key在key处的值替换成你的的key,然后将这段代码放到index.html里<script src=“https://maps.googleapis.com/maps/api/js?key=your-google-key&libraries=places"></script>3. 编写代码我这里直接用home了4. 运行查看效果几个注意事项1. 如果你是Angular6或者以上的版本,请一定要在相关ts文件里的第一行声明这个/// <reference types="@types/googlemaps” />如果不是,请声明这一句代码import {} from “googlemaps”;具体讨论请看这里:https://stackoverflow.com/questions/51084724/types-googlemaps-index-d-ts-is-not-a-module2. 我这里用的是<ion-input></ion-input>标签,而这个API支持的是原生的HTML<input />标签。如果不是原生的话,会报这个错误:所以我的在home.page.ts文件里的getPlaceAutocomplete()获取DOM的代码是这样写的let ele = document.getElementById(‘addresstext’).getElementsByTagName(‘input’)[0];如果是原生<input />标签,还可以这样写:(详细代码请参考github地址)html文件<input #addresstext style=“border:1px solid #111;width: 100%” />ts文件/// <reference types="@types/googlemaps" />import { Component, ViewChild, OnInit, AfterViewInit , NgZone } from ‘@angular/core’;@Component({ selector: ‘app-home’, templateUrl: ‘home.page.html’, styleUrls: [‘home.page.scss’],})export class HomePage implements OnInit, AfterViewInit { @ViewChild(‘addresstext’) addresstext: any; … private getPlaceAutocomplete() { let ele = this.addresstext.nativeElement; const autocomplete = new google.maps.places.Autocomplete(ele, { … }}顺便放一下两者的效果图,让大家看一下效果图,可以发现区别是,如果是原生的HTML标签,google会自动帮你添加placeholder :)3*.虽然是在ngAfterViewInit()里调用的googleMap初始化函数,理论上此时视图已经初始化好了,所以DOM应该渲染出来了。但实际项目中,还是会如下所示的错误猜想原因,应该一开始google找不到相关的DOM的节点,所以我在这里加了一个setTimeout()ngAfterViewInit() { setTimeout(() => { this.getPlaceAutocomplete(); },1000);} ...

March 27, 2019 · 1 min · jiezi

ng6表单动态拖拽demo

github[图片]

March 26, 2019 · 1 min · jiezi

Ionic start(4.x版本)发生bad status code 400错误以及离线创建新项目的解决方法

使用ionic start创建新项目,出现错误:Error: Encountered bad status code (400) forhttps://d2ql0qc7j8u4b2.cloudfront.net/angular-official-tabs.tar.gzThis could mean the server is experiencing difficulties right now–please tryagain later. at Request.req.on.res(C:\Users\xxx\AppData\Roaming\npm\node_modules\ionic\lib\utils\http.js:68:28) at emitOne (events.js:116:13) at Request.emit (events.js:211:7) at Request._emitResponse(C:\Users\xxx\AppData\Roaming\npm\node_modules\ionic\node_modules\superagent\lib\node\index.js:862:8) at ClientRequest.req.once.res(C:\Users\xxx\AppData\Roaming\npm\node_modules\ionic\node_modules\superagent\lib\node\index.js:412:10) at Object.onceWrapper (events.js:315:30) at emitOne (events.js:121:20) at ClientRequest.emit (events.js:211:7) at HTTPParser.parserOnIncomingClient (_http_client.js:543:21)| Downloading and extracting tabs starter (100.00%)然后就一直卡在Downloading and extracting tabs starter不动。直接原因是ionic cli无法下载ionic的template文件angular-official-tabs.tar.gz,从url看这个文件没有放在npm库中,所以使用淘宝npm镜像不能解决这个问题。在网上搜了很多文章都无法解决这个问题。后来在Ionic官网上看到,Ionic新项目模板(starter)作为一个开源项目托管在github上,于是尝试去找到starter template的源码,然后直接从starter template手工创建新项目。在github上搜索ionic-team,发现ionic-team/starters项目,这就是要找的模板源码项目。地址是:https://github.com/ionic-team/starters然后打包下载这个项目的所有源码。根目录下,有三个文件夹angular、ionic-angular和ionic1三个文件夹,显然分别对应三种ionic4项目的starter模板。angular是ionic4.x的angular项目;ionic-angular是ionic2.x/3.x的项目;ionic1是ionic1.x项目。我需要创建的是ionic4.x的项目,所以进入angular文件夹,里面有base和official两个文件夹,从前面错误信息看到下载文件名为angular-official-tabs.tar.gz,所以显然这个模板应该放在official文件夹中。打开official文件夹,里面果然有一个tabs的文件夹,里面有src和e2e两个文件夹,应该就是模板的源文件。但是angular/official/tabs文件夹下没有包含完整的ionic/angular的项目文件,显然,angular.json、package.json文件都没有。然后查找其他文件夹,发现angular/base文件夹下,有这些缺失的文件。于是做以下尝试:a.新建一个文件夹作为我们自己的项目文件夹,假设是testv4。b.把angular/base下的所有文件复制到testv4中。c.把angular/official/tabs文件夹下所有文件复制到testv4中,提示有同名文件,全部覆盖。d.打开testv4/package.json文件,修改前面几行内容为自己的应用名称等: “name”: “ionic-app-base”, “version”: “0.0.0”, “author”: “Ionic Framework”, “homepage”: “https://ionicframework.com/",e.在testv4文件夹上执行npm install,中间可能会出现错误,如果出错则删除node_module文件夹然后再次运行npm install,直到成功。f.执行npm run start,启动浏览器打开localhost:4200,成功。这个方法也可以实现不联网状态下,离线创建Ionic新项目,当然你可以说离线创建新项目没有意义,因为npm install一样需要联网,但如果能够手工建立node_module文件夹,离线创建ionic项目也是有意义的。 ...

March 25, 2019 · 1 min · jiezi

生产部署之后Angular7的assets目录文件引用url错误的解决方法

某些页面需要放置一些静态图片,按照Angular目录结构,应该放到/src/assets目录下,例如有文件/src/assets/img/menu_a.svg,在页面的html模板中这样引用<img src="/assets/img/menu_a.svg" alt="">在本地测试(ng serve -o)中,在http://localhost:4200上可以正常显示图片。但前端项目和后台整体打包部署到Tomcat后,发现图片不能正常显示。查看发现图片的链接为http://192.168.0.23:8085/assets/img/menu_a.svg,而Angular前端被放在路径http://192.168.0.23:8085/fr/上。图片路径应该是http://192.168.0.23:8085/fr/assets/img/menu_a.svg解决方法:1.前端构建时设置参数 –baseHrefng build –prod –outputHashing=all –outputPath=../public/dist –baseHref=/fr/ –build-optimizer–baseHref会使ng创建index.html的时候,添加上base标签<base href="/fr/">因为Angular是单页模式,所以所有页面和组件都放置在index.html内,使用相同的基础路径。2.把assets目录下的文件的url改为./assets,如<img src="./assets/img/menu_a.svg" alt="">这样就可以正确显示图片。发布于 13:10

March 16, 2019 · 1 min · jiezi

我理解的 core 目录

…过了一遍 Angular 文档 的小伙伴大致都会记得最佳实践中提到过的有关CoreModule的一些解释和说明,其实关于名字的命名不是强制性的,只要团队中一致 pass,你把它命名为XXXModule都无所谓。但是最主要的,还是我们需要理解“core”的作用以及在项目中发挥更好的作用和地位。我记录下我项目中对“core”的一些拙略见解和搭配。core目录纵观整个Angular项目结构以及最佳实践,我们通常把项目按功能划分文件夹,比如工具、共享、全局核心、页面模块组件、公用模块组件等等,“core”在这里相当于全局核心类型的范围,那全局核心类型到底是聚集了项目的哪些功能呢?我的理解是我们可以把 全局单例服务、只需要引入一次 的东西都归并到这里。全局单例服务:这些服务在整个应用生命周期内只存在一个实例,也就是数据是全局互通的,而在 Angular 中实现单例服务就需要一个中间提供商(module)来做中介,也就是所谓的“CoreModule”,然后在根模块引用一次便可全局使用,这也是官方推荐的一种单例服务做法。然而在 Angular 6 + 版本后,官方为 Injectable 装饰器提供了 providedIn: ‘root’ 的选项,让声明的服务直接成为单例服务,此后再不用通过“CoreModule”来提供服务,但是我们的单例服务仍然可以放在 core目录 中,通过 路径别名 配置来直接访问服务,因为实际上,单例服务只会乖乖在 core目录中 ,不会再有其他东西来干扰。只需引入一次的?:什么是项目中只需要引入一次的?举个例子,全局错误处理、根路由数据预加载、http请求拦截器等。这些都是通过一次配置就能一直用到老的东西,而且不可能会有其他兄弟来直接使用的东西,顺理成章就需要归并到 core目录 中,并且有的需要被“CoreModule”引用,有的需要被“AppModule”引用。我列举来几个更加详细的例子来说说这些类别:应用初始数据加载在开发单页应用特别是管理系统的时候,可能项目的构成除了中心主系统还衍生了很多个子项目系统,这种情况下登录授权一般都是在主系统完成,然后前后端通过单点登录确保子系统能使用。这时子系统一般都是一个新的项目,我们都知道 Angular 提供了强大的路由功能,可以通过路由守卫来预加载系统,然而我们需要的授权信息是相对整个应用而不是某个路由而言的,那这个时候我们就需要一个根级别的数据预加载功能来完成授权等功能。Angular 还是帮你开辟好了入口,这时我们只需要一个APP_INITIALIZER就可以完成预加载。前提是我们定义好了预加载的数据操作逻辑,举个例子:/** * app 初始化前身份验证操作 /@Injectable({ providedIn: ‘root’})export class AppInitAuthService { constructor( …, private userInfoService: UserInfoService, ) { } /* 验证当前token身份 / tokenAuth(): Promise<any> { return new Promise((resolve,reject) => { return this.userInfoService.getUserInfoServer().subscribe(res => { if(res.reasonCode == ’notLoggedIn’){ //未登录 //可以进行取消授权处理 … }else{ //获取了授权数据,todo … resolve(true) } }) }) }}此处声明了一个基本的用户授权信息获取服务,接下来我们可以直接通过APP_INITIALIZER来完成数据预加载功能,只需要在 CoreModule 中声明刚才提供的处理服务,Angular 会自动在根组件初始化前查询并执行 APP_INITIALIZER 所注入的所有服务函数,由于我们提供的是一个 Promise 对象,所以 Angular 会等待执行结果:@NgModule({ … providers: [ … { provide: APP_INITIALIZER, multi: true, useFactory: (appInit: AppInitAuthService) => { return () => appInit.tokenAuth() }, deps: [AppInitAuthService] } ]})export class CoreModule { constructor( … ) {}}只要AppModule引用了CoreModule,项目会自动完成预授权处理功能,完全无需其他组件掺入。全局错误处理有时候我们需要全局错误处理机制。比如我们编译更新了项目版本,多个某个模块功能,但是用户这边并没有去实时刷新,当意外去到某个原本不存在的路由时 Angular 会捕获到找不到模块的错误,这是我们就可以提前在错误处理中去对用户进行较友好的提示等等;又比如我们会想要去接入前端监控平台像 fundebug 等等,具体对实现方式也是一样通过 Angular 提供的捕错功能来实现。一个最简单的错误处理服务如下:import { ErrorHandler } from ‘@angular/core’export class HandleCommon extends ErrorHandler{ constructor(){ super() } handleError(error: Error){ //注意调基类处理函数,不然会覆盖默认行为,比如控制台不会看到报错 super.handleError(error) if (/Loading chunk [\d]+ failed/.test(error.message)) { //捕获找不到模块 (服务端目录数据变动) … } //… 各种错误处理 }}然后我们直接在AppModule中声明一个 ErrorHandler 令牌对应的服务,就可以实现全局错误监听处理:import { NgModule, ErrorHandler } from ‘@angular/core’import { HandleCommon } from ‘../core’@NgModule({ … providers: [ … { provide: ErrorHandler, useFactory: () => { return new HandleCommon() } } ], bootstrap: [ AppComponent ]})export class AppModule { }http请求拦截器尽管 Angular 提供了十分漂亮的 HttpClient 给开发者舒服地进行网络请求操作,但是有很多针对网络请求的需求需要我们自己去开发,像 http 超时拦截、token 拦截、错误处理拦截等等,这些也都属于一次引用,全局使用的范畴。更漂亮的是 Angular 为我们提供了拦截器接口,我们只管开发拦截器逻辑功能,调用及使用全部控制权都在框架内。由于拦截器涉及比较多东西,这里放一个最为简单的实现如下:import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent} from ‘@angular/common/http’import { Injectable } from ‘@angular/core’import { Observable } from ‘rxjs’import { tap } from ‘rxjs/operators’//拦截器 - 添加请求头@Injectable()export class TokenInterceptor implements HttpInterceptor { constructor() { } intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { //通过某些逻辑获取 token let token = ‘xxxx’ if (token) { token = Bearer ${token} req = req.clone({ setHeaders: { Authorization: token } }) } return next.handle(req) }}只需要在 CoreModule 中通过 HTTP_INTERCEPTORS 令牌来声明我们写好的拦截器,框架会在正确的时机自动处理和调用拦截器逻辑:@NgModule({ providers: [ …, { provide: HTTP_INTERCEPTORS, useClass: TokenInterceptor, //必须设置,拦截器是个数组集合而不仅仅只有一个 multi: true } ]})export class CoreModule { constructor() {}}一些单例服务等等应用中或多或少有一些需要在全局流通的数据,比如全局的用户信息管理:@Injectable({ providedIn: ‘root’})export class UserInfoService { //用户数据 全局共享 数据流 userInfo$: BehaviorSubject<UserInfo> = new BehaviorSubject<UserInfo>(null) constructor() { } /* * 获取用户数据 / getUserInfoServer(): Observable<UserInfo> { … } /* * 退出登录 */ getUserLogoutServer(): Observable<boolean> { … }}作为频繁被存取的介质,单例模式自然而然是它的特点,所以最好也一起归并到所谓的 core目录 中。因人而异前面列举了一些常用的类别来说明 core目录 以及 “CoreModule” 存在的意义。除了一些需要“CoreModule”来作为桥梁的例子,貌似 core目录 并不是必须要存放某些东西的,比如全局的单例对象就完全可以单独使用其他的文件夹来存放维护。是对的,没有一个统一的标准来约束我们到底是要去如何组织代码目录结构,所有项目都是因人而异,自己觉得舒服的、可维护的才最重要。本记录只是为了更加贴近官方最佳实践而如此组织,纯粹作为一个记录以及给大家的一个参考。 ...

March 7, 2019 · 2 min · jiezi

angular6根据environments配置文件更改开发所需要的环境

前端开发过程中,我们常常需要根据需求去运行或者打包不同环境的代码,幸运的是,angular给我们提供了environments配置,但是angular6.x的配置方式和angular的其他版本的配置方式是有所不同的,下面我就分别介绍在测试test、预生产pre环境下实现environments配置。一、angular6.x下environments的配置首先,在environments文件夹下创建environment.test.ts和environment.pre.ts文件,如下图然后分别在里面添加如下内容然后找到angular.json文件,在architect对象下的configurations下添加如下代码:然后在找到serve,添加如下的代码可以通过运行ng serve –configuration=test或者ng serve -c test课件在控制台打印: {production: false, path: “https://test.webapi.sxmaps.com/"} path: “https://test.webapi.sxmaps.com/" production: false proto: Object同理运行ng serve –configuration=pre或者ng serve -c pre可获取到预生产环境的域名,所以,你只需要在需要用到环境变量的地方引入即可,如import {environment} from ‘../environments/environment’;二、angular其他版本environments的配置同样的,首先在environments创建environment.test.ts和environment.pre.ts,然后按照上文的步骤,在里面添加相同的内容,然后去到angular-cli.json文件找到environments,在里面添加如何内容:然后通过运行ng s –env=test就可以运行测试环境了,在需要的地方引入environment即可。

March 5, 2019 · 1 min · jiezi

Angular7自定义属性指令控制第三方组件

最近前端框架转移到蚂蚁的Ng-Alain和Ng-Zorro上,需要把我们自己的一些前端数据逻辑对接到Ng-Zorro提供的组件上,以简化前端代码。以前的做法是定义一组自己的自定义组件来包装Ng-Zorro提供的组件,但这个方法的缺点是页面上很难直接使用Ng-Zorro组件的属性、事件和其他特性,除非自定义组件把Ng-Zorro组件的属性和事件都暴露出来,Ng-Zorro组件的属性事件非常多,全部暴露出来太麻烦,不太现实。突然想到是否能够利用Angular自定义指令控制Ng-Zorro组件,这样页面上放置的不是自定义组件,而是Ng-Zorro组件,然后附加上一个自己写的自定义指令,添加自己的一些属性和事件,页面上仍然可以使用所有的Ng-Zorro组件的属性和事件。网上找到的Angular自定义属性指令的例子大多都是注入ElementRef和Renderer2,控制html标签的属性。没找到自定义指令控制第三方组件的例子。最先想到的是通过@Input方式传入Ng-Zorro组件的实例。如下,自定义属性指令TreeSelectDirective控制Ng-Zorro的下拉树组件NzTreeSelectComponent自定义指令的ts文件:@Directive({ selector: ‘[myTreeSelect]’, exportAs:‘myTreeSelect’})export class TreeSelectDirective implements OnChanges,OnDestroy,OnInit,AfterViewInit { @Input(“myTreeSelect”) container:NzTreeSelectComponent=null; @Input(“myProp”) prop:string=’’; @Output(“ev”) e:EventEmitter<any>=new EventEmitter(); }页面的HTML模板:<nz-tree-select #t [myTreeSelect]=’t’ [myProp]=‘x’ (ev)=‘onEv($event)’ …这个方法可以在指令类TreeSelectDirective获得组件nz-tree-select的实例,并且控制nz-tree-select组件,但因为组件实例是在@Input中传入,而@Input是在生命周期的ngOnInit钩子被调用时才传入,而组件的ngOnInit钩子比指令的ngOnInit钩子先被调用,这意味着指令获得组件的实例之前,组件实例的各个输入属性已经完成初始化。这会带来一些问题,Ng-Zorro组件的有些属性初始化之后再设置就无法生效(可能是Zorro的bug),必须在Ng-zorro组件ngOnInit钩子调用前设置这些属性值才能生效,所以通过@Input传入组件实例,无法设置这些属性的值并使其生效。另外,<nz-tree-select #t [myTreeSelect]=’t’ 这样的写法也太累赘,不优雅。后来发现,nz-tree-select之类的组件也支持Angular官方ngModel指令,于是查看ngModel指令如何和nz-tree-select组件交互,发现ngModel指令构造器中注入了ControlValueAccessor实例。然后猜想,既然指令可以注入ControlValueAccessor,是不是也可以直接注入NzTreeSelectComponent,于是试验:@Directive({ selector: ‘[myTreeSelect]’, exportAs:‘myTreeSelect’})export class TreeSelectDirective implements OnChanges,OnDestroy,OnInit,AfterViewInit { @Input(“myProp”) prop:string=’’; @Output(“ev”) e:EventEmitter<any>=new EventEmitter(); constructor(private container:NzTreeSelectComponent){ // console.info(nzComp); }}页面的HTML模板:<nz-tree-select myTreeSelect [myProp]=‘x’ (ev)=‘onEv($event)’ …测试发现nz-tree-select成功注入指令实例,constructor被调用的时候,nz-tree-select组件的@Input输入属性还没有初始化,可以在constructor中设置nz-tree-select组件的属性。而且代码也更优雅简洁。

March 4, 2019 · 1 min · jiezi

Angular中修改第三方组件的样式 - zorro日期选择器右端不对齐的BUG

在一列上同时使用zorro的日期选择器和input组件会出现右端不对齐的BUG(nzSpan设置为一样)上图中3个表单项:单据日期、开票、交货方式,nzSpan数值是一样的,可以看到日期选择器和下面的“交货方式”右端没有对齐在浏览器控制台中层层展开后到达下面的路径:可以看到ant-calendar-picker是日期选择器组件的官方提供的样式,在该样式下手动追加属性width:100%,右端就可以对齐了,那么接下来只要应用到源码中就好了结果在当前组件的css文件中直接.ant-calendar-picker{width:100%;}是没有效果的,要这样使用才有效:::ng-deep .ant-calendar-picker{width:100%;}

February 20, 2019 · 1 min · jiezi

Angular7 ng-zorro-antd 制作右键菜单

没多少逻辑,就直接贴代码了下面是html模板的代码:<ul nz-menu [style.width]=“300” [nzTheme]="‘dark’" [nzMode]="‘inline’" [nzInlineCollapsed]=“isCollapsed”> <li nz-menu-item (contextmenu)=“contextMenu($event, contextTemplate)">中国</li> <li nz-menu-item (contextmenu)=“contextMenu($event, contextTemplate)">美国</li> <li nz-menu-item (contextmenu)=“contextMenu($event, contextTemplate)">英国</li></ul><ng-template #contextTemplate> <ul nz-menu nzInDropDown nzSelectable (nzClick)=“close()"> <li nz-menu-item (click)=“openRenameListModal()"> <i class=“anticon anticon-edit anticon-right-margin”></i> <span>重命名</span> </li> <li nz-menu-divider></li> <li nz-menu-item (click)=“delete()"> <i class=“anticon anticon-delete anticon-right-margin danger”></i> <span class=“danger”>删除列表</span> </li> </ul></ng-template>下面是ts文件的代码import { Component, TemplateRef } from ‘@angular/core’;import { FormsModule } from ‘@angular/forms’;import { NzDropdownContextComponent, NzDropdownService } from ’ng-zorro-antd’;@Component({ selector: ‘app-root’, templateUrl: ‘./app.component.html’, styleUrls: [’./app.component.css’]})export class AppComponent { constructor( private dropdownService:NzDropdownService ){} dropdown:NzDropdownContextComponent; contextMenu($event:MouseEvent,template:TemplateRef<void>){ this.dropdown=this.dropdownService.create($event,template); } openRenameListModal(){ console.log(“打开了重命名modal”); } close(){ this.dropdown.close(); } delete(){ console.log(“删除了一个元素”); }} ...

February 14, 2019 · 1 min · jiezi

【Angular】Angula6中的组件通信

Angula6_组件通信本文主要介绍 Angular6 中的组件通信一、父子组件通信1.1 父组件向子组件传递信息方法一 在父组件上设置子组件的属性父组件绑定信息<app-child childTitle=“可设置子组件标题”></app-child>子组件接收消息import { Component, OnInit, Input } from ‘@angular/core’;@Input childTitle: string;方法二 父组件调用子组件的方法父组件触发消息<app-child #child></app-child> <button (click)=“child.childPrint()"></button>子组件接收消息childPrint() { alert(“来自子组件的打印”);}1.2 子组件向父组件传递信息方法一 使用 EventEmitter子组件使用 EventEmitter 传递消息import { Component, OnInit, Output, EventEmitter } from ‘@angular/core’;…@Output() initEmit = new EventEmitter<string>();ngOnInit() { this.initEmit.emit(“子组件初始化成功”);}…父组件接收消息<app-child (initEmit)=“accept($event)"></app-child>accept(msg:string) { alert(msg);}方法二 使用 ViewChild子组件提供传递参数的函数sendInfo() { return ‘Message from child 1.’;}父组件使用 ViewChild 触发并接收信息<button (click)=“getInfo()">获取子组件1号的信息</button><h2>{{ info }}</h2>import { Component, OnInit, ViewChild } from ‘@angular/core’;…@ViewChild(ChildFirstComponent) private childcomponent: ChildFirstComponent;getInfo() { this.info = this.childcomponent.sendInfo();}二、非父子组件通信方法一 service缺点:需要双向的触发(发送信息 / 接收信息)service.tsimport { Component, Injectable, EventEmitter } from “@angular/core”;@Injectable()export class myService { public info: string = “”; constructor() {}}组件 1 向 service 传递信息import { myService } from ‘../../service/myService.service’;…constructor( public service: myService) { }changeInfo() { this.service.info = this.service.info + “1234”;}…组件 2 从 service 获取信息import { myService } from ‘../../service/myService.service’;…constructor( public service: myService) { }showInfo() { alert(this.service.info);}…方法二 使用 BehaviorSubject优点:真正的发布订阅模式,当数据改变时,订阅者也能得到响应serviceimport { BehaviorSubject } from ‘rxjs’;…public messageSource = new BehaviorSubject<string>(‘Start’);changemessage(message: string): void { this.messageSource.next(message);}组件调用 service 的方法传信息和接收信息changeInfo() { this.communication.changemessage(‘Message from child 1.’);}ngOnInit() { this.communication.messageSource.subscribe(Message => { window.alert(Message); this.info = Message; });}三、其他的通信方式路由传值cookie、session、storage参考文献《Angular6.x 学习笔记——组件详解之组件通讯》《angular6 组件间的交流方式》 ...

January 31, 2019 · 1 min · jiezi

安装指定版本(老版本)的PrimeNG

由于兼容问题,有时候还是要安装老版本的PrimeNG的,ng-zorro-antd也可以参照本方法卸载已有版本:npm uninstall primeng –save清理缓存:npm cache verify安装老版本:npm install primeng@x.x.x –save,其中的x.x.x是版本号截止2019年1月22日PrimeNG是7.0.4,前一个稳定的大版本号是6.1.7安装兼容版本的PrimeNG同时可以解决:node_modules/primeng/components/table/table.d.ts(15,86)诸如此类的错误提示,缺少括号或者分号的错误,都是版本兼容问题

January 22, 2019 · 1 min · jiezi

ng-zorro-antd 标签组件tag 实现回车连续新增

zorro tag 实现编辑状态、连续新增先看实现效果官网给的示例只有新增、删除,并且不可连续新增。自己制作实现这两个状态。下面直接贴代码了模板中代码:<span *ngFor=“let tag of tags;let i=index;"> <nz-tag *ngIf=“tag.visible” [nzMode]=“i === -1 ? ‘default’ : ‘closeable’” (nzAfterClose)=“handleClose(tag)” (click)=“showInputInArray(tag)” (nzOnClose)=“onCloseTagInArray()"> <span style=“font-size:1.2em;">{{ sliceTagName(tag.text) }}</span> </nz-tag> <input #inputElementInArray *ngIf="!tag.visible” type=“text” nz-input nzSize=‘small’ style=“width:78px;” [(ngModel)]=“tag.text” (blur)=“handleInputConfirmInArray(tag)” (keydown.enter)=“handleInputConfirmInArray(tag)"></span><nz-tag *ngIf="!inputVisible” class=“editable-tag” (click)=“showInput()"> <i nz-icon type=“plus”></i> <span style=“font-size:1.2em;">点我新增</span></nz-tag><input #inputElement nz-input nzSize=“small” *ngIf=“inputVisible” type=“text” [(ngModel)]=“inputValue” style=“width: 78px;” (blur)=“handleInputConfirm()” (keydown.enter)=“handleInputConfirm()">CSS代码.editable-tag ::ng-deep .ant-tag { background: rgb(255, 255, 255); border-style: dashed;}ts代码import { Component, OnInit, ViewChild, ElementRef } from ‘@angular/core’;import { NzMessageService } from ’ng-zorro-antd’;@Component({ selector: ‘app-tag-edit’, templateUrl: ‘./tag-edit.component.html’, styleUrls: [’./tag-edit.component.css’]})export class TagEditComponent implements OnInit { private tag01:CcTag = new CcTag(); private tag02:CcTag = new CcTag(); private tag03:CcTag = new CcTag(); private tags:CcTag[] = []; private skipClick:boolean = false; private newOngoing:boolean = true; inputVisible = false; inputValue = ‘’; @ViewChild(‘inputElement’) inputElement: ElementRef; @ViewChild(‘inputElementInArray’) inputElementInArray: ElementRef; //#region 系统生命周期钩子 constructor( private msg:NzMessageService ) { } ngOnInit() { this.initTags(); } //#endregion //#region 初始化数据 initTags(){ this.tag01.text=‘单袋’; this.tag01.value=‘单袋’; this.tag01.visible=true; this.tag02.text=‘双袋’; this.tag02.value=‘双袋’; this.tag02.visible=true; this.tag03.text=‘编织袋’; this.tag03.value=‘编织袋’; this.tag03.visible=true; this.tags.push(this.tag01); this.tags.push(this.tag02); this.tags.push(this.tag03); } //#endregion handleClose(removedTag: {}): void { this.tags = this.tags.filter(tag => tag !== removedTag); } sliceTagName(tag: string): string { const isLongTag = tag.length > 20; return isLongTag ? ${tag.slice(0, 20)}... : tag; } showInput(): void { this.inputVisible = true; setTimeout(() => { this.inputElement.nativeElement.focus(); }, 10); } // 数组反应到UI上的input showInputInArray(tag:CcTag){ if(this.skipClick){ this.skipClick = false; return; } tag.visible=false; setTimeout(() => { this.inputElementInArray.nativeElement.focus(); }, 10); } handleInputConfirmInArray(tag:CcTag){ tag.visible=true; } handleInputConfirm(): void { var target:CcTag = new CcTag(); target.text=this.inputValue; target.value=this.inputValue; target.visible=true; if (this.inputValue && this.tags.indexOf(target) === -1) { this.tags.push(target); this.inputValue=’’; this.showInput(); }else{ this.inputValue = ‘’; this.inputVisible = false; } } onCloseTagInArray(){ this.skipClick = true; }}export class CcTag{ public text:string; public value:string; public visible:boolean; } ...

January 13, 2019 · 2 min · jiezi

自定义a标签的不可用状态

a标签设置为不可用a标签没有disabled属性,所以要自己手动制作不可用状态设置颜色为灰色停用事件响应设置鼠标样式为默认样式 - 选择用的箭头样式使用ngClass为a标签设定样式<a (click)=“insertNodeSibling()” [ngClass]="{‘disableA’: brotherNodeDisabled}">新增同级</a>上面代码中disableA表示CSS中的样式类,当后面的变量brotherNodeDisabled为true时前面的样式disableA生效相反则不生效样式代码:.disableA{ pointer-events: none; cursor: default; color:gray;}更改样式:使用代码this.brotherNodeDisabled=true;修改变量的true和false,则会触发或者关闭CSS中的样式

January 12, 2019 · 1 min · jiezi

Angular6+ webpack自定义扩展

Angular6+ webpack自定义扩展背景在项目开发过程中,发现生产模式下console.log()日志文件依然存在,通过百度得出的结果是在生产模式下console.xx一系列方法全部重写window.console.log = ()=>{}这种方法表示一看就不舒服,无法接受。所以想着想着@angular/cli底层是webpack,而且代码压缩用的是UglifyJs,所以想着能不能扩展一配置项,让我把console全部给我过滤掉,最后去Issues上找了许久,发现angular6+不支持eject,最后有人推荐了一个工具库ngx-build-plus,不需要改很多东西就能在现有项目进行集成。接下来教大家如何使用,具体详情可以去github上找文档。如何使用1.运行 ng add ngx-build-plus,在angular7版本会自动一键配置好,但是6版本中可能会出现安装不成功,这时候请直接npm install ngx-build-plus –save-dev,然后angular.json文件中更改以下两处地方:“build”: { - “builder”: “@angular-devkit/build-angular:browser” + “builder”: “ngx-build-plus:build” …},“serve”: { - “builder”: “@angular-devkit/build-angular:dev-server” + “builder”: “ngx-build-plus:dev-server” …}2.接下来根目录下新建webpack.extra.js文件const UglifyJsPlugin = require(‘uglifyjs-webpack-plugin’);module.exports = { optimization: { minimizer: [new UglifyJsPlugin({ uglifyOptions: { compress: { drop_console: true } } })] }};记得npm install uglifyjs-webpack-plugin --save-dev3.进行生产环境编译ng build –extraWebpackConfig webpack.extra.js –prod4.好了就这么简单。写的比较简陋,有问题可以留言,实在没弄懂我就弄个示例出来。

January 6, 2019 · 1 min · jiezi

【Angular6+】属性及样式绑定

Angular6_属性及样式绑定Angular 通过 [] 来绑定数值、变量或者表达式,这种绑定是单向数据绑定。属性绑定属性绑定分为两种Property元素的常规属性,比如 src、disabled 等<img [src]=“heroImageUrl” /><button [disabled]=“isUnchanged”>Cancel is disabled</button><div [ngClass]=“classes”>[ngClass] binding to the classes property</div><app-hero-detail [hero]=“currentHero”></app-hero-detail>Attribute元素的非常规属性,比如 colspan 等<tr> <td [attr.colspan]=“1 + 1”>One-Two</td></tr>CSS 类绑定借助 CSS 类绑定,可以从元素的 class attribute 上添加和移除 CSS 类名。<!– 这是一个或者全有或者全无的替换型绑定。即当 badCurly 有值时 class 这个 attribute 设置的内容会被完全覆盖 –><div class=“bad curly special” [class]=“badCurly”>Bad curly</div><!– toggle the “special” class on/off with a property –><div [class.special]=“isSpecial”>The class binding is special</div>样式绑定样式绑定的语法与属性绑定类似。 但方括号中的部分不是元素的属性名,而由 style 前缀,一个点 (.)和 CSS 样式的属性名组成。 形如:[style.style-property]。<button [style.color]=“isSpecial ? ‘red’: ‘green’">Red</button><button [style.background-color]=“canSave ? ‘cyan’: ‘grey’">Save</button><button [style.font-size.em]=“isSpecial ? 3 : 1”>Big</button><button [style.font-size.%]="!isSpecial ? 150 : 50”>Small</button> ...

January 2, 2019 · 1 min · jiezi

angular6的table组件开发

背景及吐槽:今年有机会再次接触angualr这个框架,想起第一次接触ng还是16年读书的时候,当时还是ng1,然后学起来特别辛苦,学习曲线特别陡峭;而今年有一个项目重构直接采用了angular6,而后面该项目后面由我负责开发和维护,然后我又重新再学习了ng6,本以为有ng1的基础,学起来会好一些,然并卵,学习的曲线特别陡峭,但还是最后将ng6啃下来(很不情愿去学的,但没办法)。回归到项目,该项目没有引入其他组件库,所以很多基础组件都是自己开发(用ng开发那种酸爽很带劲),其中table组件让我思考了差不多两个星期,最后才开发出来,吐槽完毕,接下来就介绍一下我的做法,我的做法不一定最正确。形式:主要参考element里面的table组的格式:vue:<el-table :data=“tableData”> <el-table-column prop=“date” label=“日期”></el-table-column> <el-table-column label=“操作”> <template slot-scope=“scope”> <el-button @click=“handleClick(scope.row)” type=“text” size=“small”>查看</el-button> </template> </el-table-column> </el-table>所以得到了angualr的table组件的格式:<app-widget-table [data]=“tableData”> <app-widget-table-column prop=“date” label=“日期”></app-widget-table-column> <app-widget-table-column label=“操作”> <ng-template #scope let-row=“scope”> <ng-widget-button (click)=“handleClick(row)” type=“text” size=“small”>查看</el-button> </ng-template> </app-widget-table-column></app-widget-table>在angular的table组件中,最为困难就是ng-template如何将作用域绑定到ng-widget-button组件中;关键点知识讲解:ng-content: 可以将父组件中所包含的所有子组件,都插入table组件中ng-container所在的位置,跟vue中的slot很像;ng-container: 可以作为一个组件的模板,跟vue里面的template组件很像;ng-template: 该东西,是整个组件中最为麻烦的一个东西,直接使用它,会没有任何效果,必须要和TemplateRef和ngTemplateOutlet一起使用,才有有效果,主要是作为模板并引入作用域,具体原理可以看一下官方文档(https://www.angular.cn/api)TemplateRef: 主要是用来获取ng-template组件的引用;ngTemplateOutlet: 将ng-template的内容在html页面展示出来,并绑定变量,就像vue中的router-view;QueryList: 获取table组件中所有的内容指引;ContentChildren: 内容映射的接口,针对多个子元素采用ContentChild: 内容映射的接口,针对单个子元素采用先对app-widget-table-column组件进行分析:该组件的作用就是为了运输数据,并且引入内容,该组件本身是不会有任何操作和逻辑,就是一个运输工;table-column.component.html:<ng-container></ng-container>table-column.component.ts:import {Component, Input, Output, TemplateRef, ContentChild, AfterContentInit} from ‘@angular/core’;@Component({ selector: ‘app-widget-table-column’, templateUrl: ‘./table-column.component.html’, styleUrls: [’./table-column.component.less’], preserveWhitespaces: false})export class TableColumnComponent implements AfterContentInit { constructor() { } @Input() label: string; @Input() prop: string; @Input() class: string; @Input() style: object; @ContentChild(‘scope’) // 获取ng-template组件的一个本地变量,并修饰scope对象 scope: TemplateRef<any>; // 获取ng-template的指引,主要是其内容,any表示该指可以是任何内容 ngAfterContentInit(): void {}}table.component.html<div> <div> <ng-content></ng-content> // 主要是用来引入整个table组件的内容,但不会在页面显示任何内容 </div> <table class=“table”> <thead> <tr> <th *ngFor=“let label of labelList”>{{label}}</th> // 类似于v-for,主要讲table-cloumn的所有label搜集,并展示 </tr> </thead> <tbody *ngIf=“data.length > 0”> <ng-container *ngFor=“let item of data; let i = index”> <tr> <ng-container *ngFor=“let row of tableColumn[’_results’]"> <td *ngIf=“row.prop” [ngStyle]=“row.style” [ngClass]=“row.class”>{{item[row.prop]}}</td> // 直接展示 <td *ngIf=“row.scope” [ngStyle]=“row.style” [ngClass]=“row.class”> <ng-container *ngTemplateOutlet=“row.scope; context: {$implicit: {}, scope: data[i]}"> </ng-container> // 展示ng-template的内容 </td> </ng-container> </tr> </ng-container> </tbody> </table> <div *ngIf=“data.length === 0” class=“none-data”>暂无数据!</div></div>table.component.ts:import {Component, OnInit, Input, Output, ContentChildren, AfterContentInit, ViewChild, AfterViewInit, QueryList} from ‘@angular/core’;import {TableColumnComponent} from ‘../table-column/table-column.component’;@Component({ selector: ‘app-widget-table’, templateUrl: ‘./table.component.html’, styleUrls: [’./table.component.less’], preserveWhitespaces: false})export class TableComponent implements OnInit, AfterContentInit { constructor() { } @ContentChildren(TableColumnComponent) tableColumn: QueryList<TableColumnComponent>; // 获取table-cloumn组件的所有实例 @Input() data: object[]; labelList: string[] = []; ngOnInit(): void { if (!(this.data instanceof Array)) { throw new Error(’the data into TableComonent must be Array!’); } } ngAfterContentInit(): void { this.labelList = this.tableColumn[’_results’].map(item => item.label); }}虽然看起来这两个组件的代码不多,但里面的逻辑却比较绕,这也证明了ng用起来十分难上手,不过真的称赞的是,ng采用ts和rx,用上手确实是比较爽。这两个组件目前还是比较粗糙,功能和特性也不是特别多,只能满足一般表格的需求,后续会继续完善该组件以及其他项目中用ng来开发的基础组件,希望能沉淀出一套ng的组件库。以上若有不正确的地方,欢迎指出。 ...

December 25, 2018 · 2 min · jiezi

RouteReuseStrategy angular路由复用策略详解,深度刨析路由复用策略

关于路由复用策略网上的文章很多,大多是讲如何实现tab标签切换历史数据,至于如何复用的原理讲的都比较朦胧,代码样例也很难适用各种各样的路由配置,比如懒加载模式下多级嵌套路由出口网上的大部分代码都会报错。我希望能通过这篇文章把如何复用路由的原理讲明白,让小伙伴能明明白白的实用路由复用策略,文字中有不详实和错误的地方欢迎小伙伴批评指正对路由复用策略的理解路由复用策略的是对路由的父级相同节点的组件实例的复用,我们平时看到的多级嵌套路由切换时上层路由出口的实例并不会从新实例化就是因为angular默认的路由复用策略在起作用,而我们从写路由复用策略能实现很多事情,其中之一就是实现历史路由状态(数据)的存储,即jquery时代的tab页签和iframe实现操作历史的切换。我一开始认为路由复用策略就是对历史路由数据的复用策略,这个错误的观念导致我对路由复用策略接口方法理解起来异常困难,不知小伙伴和我犯没犯同样的错误。观念正确了,下面就理解起来比较方便了,写路由复用策略也就比较顺手了。下面是angular默认路由复用策略,每切换一下路由,下面代码都再默默的执行。export class DefaultRouteReuseStrategy { shouldDetach(route) { return false; } store(route, detachedTree) { } shouldAttach(route) { return false; } retrieve(route) { return null; } shouldReuseRoute(future, curr) { return future.routeConfig === curr.routeConfig; }}每个活动路由是一棵树,每个节点都有一个ActivatedRouteSnapshot,root节点是RouterModule.forRoot(Routes)Routes之上的一个默认路由,也无法配置路由复用策略解析路由复用策略方法调用顺序shouldReuseRoute(future, curr)retrieve(route)shouldDetach(route)store(route, detachedTree)shouldAttach(route)retrieve,取决一上一步的返回值store(route, detachedTree),取决第五步shouldReuseRouteshouldReuseRoute()决定是否复用路由,根据切换的前后路由的节点层级依次调用,返回值为true时表示当前节点层级路由复用,然后继续下一路由节点调用,入参为切换的下一级路由(子级)的前后路由,返回值为false时表示不在复用路由,并且不再继续调用此方法(当前路由不再复用,其子级路由也不会复用,所以不需要再询问下去),root路由节点调用一次,非root路由节点调用两次这个方法,第一次比较父级节点,第二次比较当前节点,retrieveretrieve()接上一步奏,当当前层级路由不需要复用的时候,调用一下retrieve方法,其子级路由也会调用一下retrieve方法,如果返回的是null,那么当前路由对应的组件会实例化,这种行为一直持续到末级路由。shouldDetachshouldDetach是对上一路由的数据是否实现拆离,其调用开始是当前层级路由不需要复用的时候,即shouldReuseRoute()返回false的时候,如果这时候反回false,将继续到上一路由的下一层级调用shouldDetach,直到返回true或者是最末级路由后才结束对shouldDetach的调用,当返回true时就调用一次store 方法,请看下一步奏storestore存储路分离出来的上一路由的数据,当 shouldDetach返回true时调用一次,存储应该被分离的那一层的路由的DetachedRouteHandle。注意:无论路由树上多个含有组件component路由节点,能分离出来的只能有一个,被存储的也只能有一个,感觉这种机制对使用场景有很大限制。shouldAttachshouldAttach是对当前路由的数据是否实现恢复(附加回来),其调用开始是当前层级路由不需要复用的时候,即shouldReuseRoute()返回false的时候,这和shouldDetach的调用时机很像,但是,并不是所有的路由层级都是有组件实例的,只有包含component的route才会触发shouldAttach,如果反回false,将继续到当前路由的下一带有component的路由层级调用shouldAttach,直到返回true或者是最末级路由后才结束对shouldAttach的调用,当返回true时就调用一次retrieve 方法,如果retrieve方法去获取一下当前路由的DetachedRouteHandle,返回一个DetachedRouteHandle,就再调用一次store,再保存一下retrieve返回的DetachedRouteHandle。注意注意:无论路由树上多个含有组件component路由节点,能恢复数据的只能有一个节点,这和shouldDetach是一个套路,对使用场景有很大限制。总结·这个还是实验性的路由复用策略还是不够强大路由复用策略这种调用机制对使用场景限制很大 ,比如多级路由出口嵌套就无法实现路由数据缓存。因为多级路由出口嵌套的应用切换路由时,前后路由会包含多个带component的路由节点,而每次对路由的存储和恢复只能存储和恢复某一个节点的component的DetachedRouteHandle,其他路由节点上的component就是被从新实例化。明白这一点后我就放弃了想写一个可以适用任何场景的路由复用策略的想法,如果有小伙伴能解决好这一业务场景,欢迎赐教。 如果这个路由复用策略可以存储一个路由上多个节点的DetachedRouteHandle,和恢复多个节点的DetachedRouteHandle,应该能解决上面是的多级路由出口嵌套场景,但不知道会不会带来别的问题。一个路由复用策略用例下面贴一个路由复用策略用例,应该是满足大部分人的业务要求,注意事项:只能是末级路由的缓存,且路由切换的时候路由节点上的component不能超过两个。import {ActivatedRouteSnapshot, DetachedRouteHandle, Route, RouteReuseStrategy} from “@angular/router”;export class CustomerReuseStrategy implements RouteReuseStrategy { static handlers: Map<Route, DetachedRouteHandle> = new Map(); shouldDetach(route: ActivatedRouteSnapshot): boolean { return !route.firstChild; } shouldAttach(route: ActivatedRouteSnapshot): boolean { return !!CustomerReuseStrategy.handlers.has(route.routeConfig); } shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot) { return curr.routeConfig === future.routeConfig; } retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle { return CustomerReuseStrategy.handlers.get(route.routeConfig); } store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void { CustomerReuseStrategy.handlers.set(route.routeConfig, handle); }}很精简,但是很好用,小伙伴可以根据自己的业务逻辑进行改造。如果感觉这篇文章对你有帮助,请点个赞吧 ???????????????????????????????????????????????????????????????????????? ...

December 24, 2018 · 1 min · jiezi

【前端芝士树】SPA 网站 SEO 初级优化指南(MVVM)

SPA 网站 SEO 初级优化指南(MVVM)百度 Baidu百度搜索资源平台 https://ziyuan.baidu.com/?cas…链接提交地址 https://ziyuan.baidu.com/link…百度爬虫 UAMozilla/5.0 (Linux;u;Android 4.2.2;zh-cn;) AppleWebKit/534.46 (KHTML,like Gecko) Version/5.1 Mobile Safari/10600.6.3 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/s…Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/s…)谷歌 GoogleGoogle Search Console https://search.google.com/sea…谷歌爬虫 UAMozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.96 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)SEO 优化重点(一)三个基本注意点信息架构信息架构要明晰,这个对 SEO 也非常重要,包括网站扁平化设计,友好的 URL 设计,标题书写,页面相关度信息聚合和融合,这也是影响用户体验的一大部分。网站主题为网站确定一个主题(核心关键词),一旦确定,那么全站都围绕这个关键词进行扩展和相关性来做外链和友链权重网站外链就是在别的网站导入自己网站的链接,高质量高 PR 的外链能迅速提高自身网站的权重。友链则是自己的网站包含了其他网站的链接,效果与外链类似。(二)利用好 Meta 信息标签Meta 标签用于给搜索引擎提供关于网页的信息,这些信息对于访问者而言是不可见的。参考淘宝网的做法<header> <title>淘宝网 - 淘!我喜欢</title> <meta name=“description” content=“淘宝网 - 亚洲较大的网上交易平台,提供各类服饰、美容、家居、数码、话费/点卡充值… 数亿优质商品,同时提供担保交易(先收货后付款)等安全交易保障服务,并由商家提供退货承诺、破损补寄等消费者保障服务,让你安心享受网上购物乐趣!” /> <meta name=“keyword” content=“淘宝,掏宝,网上购物,C2C,在线交易,交易市场,网上交易,交易市场,网上买,网上卖,购物网站,团购,网上贸易,安全购物,电子商务,放心买,供应,买卖信息,网店,一口价,拍卖,网上开店,网络购物,打折,免费开店,网购,频道,店铺” /> …</header>Titletitle 网站标题Meta - Descriptiondescription 给搜索引擎提供了关于这个网页的简短的描述Meta - Keywordskeywords 关于网页内容的几个关键词Meta - Robotsrobots 管理着搜索引擎是否可以进入网页如下面一段代码,禁止搜索引擎获取网页,并且阻止其进入链接。<meta name="”robots”" content="”noindex," nofollow” />SPA 网站 SEO 优化指南(一) 三种解决方案:服务器端渲染,较为复杂页面缓存服务,prerender.io,涉及收费第三方服务,或者中间层启用渲染静态页替换,phantom.js、puppeteer 或者浏览器 Copy outerHTML 就能完成,需要 nginx 配合(二)如何判断是爬虫访问还是浏览器访问爬虫访问时,会使用特殊的 user agent,以百度蜘蛛的 UA 为例,它会使用“Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/s…)”这样的UA,所以我们可以判断 UA 中含有“Baiduspider”字样则意味着是百度蜘蛛来访问了(三)如何在百度爬虫来访问时返回静态页先把静态页放置到网站的 /assets/static/ 下,配置 nginx 的配置文件 nginx.conf: location / { root C:\projects\bzbclub-dist; index index.html index.htm; if ( $http_user_agent ~* “Baiduspider”){ rewrite ^/index.html$ /assets/static/index.html last; } }保存配置文件后要使用 nginx -s reload 重新加载网站,然后使用 curl 命令的“-A”参数模拟百度蜘蛛访问首页:curl -A “Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/s…)” http://localhost:17082 > z:tempbzbclub.html打开 z:tempbzbclub.html 确认是否已经返回了静态页的内容。(四)如何生成静态页并优化Phantom.jsvar fs = require(“fs”);var page = require(“webpage”).create();phantom.outputEncoding = “utf-8”; //指定编码方式page.open(“http://localhost:17082”, function(status) { if (status === “success”) { fs.write(“z:\temp\index.html”, page.content, “w”); } else { console.log(“网页加载失败”); } phantom.exit(0); //退出系统});将此脚本保存为“phantom-static-page.js”,在命令行执行此脚本:phantomjs phantom-static-page.jsPuppeteerconst fs = require(“fs”);const puppeteer = require(“puppeteer”);(async () => { const browser = await puppeteer.launch({ headless: false // executablePath: “C:/Program Files (x86)/Google/Chrome” }); const page = await browser.newPage(); page.setViewport({ width: 1920, height: 1800 }); await page.goto(“http://localhost:3333”); await page.screenshot({ path: “example.png” }); const content = await page.content(); fs.writeFileSync("./index.html", content); // await page.close(); // await browser.close();})();将此脚本保存为“pupp-static-page.js”,在命令行执行此脚本:node pupp-static-page.js从浏览器获取静态页内容(推荐)与前两者相比,看上去没那么极客,但是非常的简单好用。首先需要新建一个static.html然后在浏览器打开需要生成静态页的页面按 F12 打开 DevTool鼠标选中<html>标签,右键 Copy > Copy OuterHTML将内容粘贴至static.html保存即可静态页压缩优化用编辑器打开static.html,删除掉所有的<script></script>以及其中的内容浏览器打开静态页,按 F12 打开 DevTool 确保没有报错体积大小优化的程序视页面的复杂度而定,一般能压缩到原有大小的十分之一参考文章链接《Meta 标签与搜索引擎优化》《SEO 网站优化的步骤和技巧有哪些?》《Angular2 网站 SEO 攻略》 ...

December 21, 2018 · 2 min · jiezi

Angular material中自定义分页信息

在项目开发中,用到了Material的分页组件,需要对该组件进行汉化。首先创建自定义汉化类:import {MatPaginatorIntl} from ‘@angular/material’;export class MatPaginatorIntlCro extends MatPaginatorIntl { /** A label for the page size selector. / itemsPerPageLabel = ‘每页条数: ‘; /* A label for the button that increments the current page. / nextPageLabel = ‘下一页’; /* A label for the button that decrements the current page. / previousPageLabel = ‘上一页’; /* A label for the button that moves to the first page. / firstPageLabel = ‘首页’; /* A label for the button that moves to the last page. / lastPageLabel = ‘尾页’; /* A label for the range of items within the current page and the length of the whole list. */ getRangeLabel = (page: number, pageSize: number, length: number) => { if (length === 0 || pageSize === 0) { return ‘0 od’ + length; } length = Math.max(length, 0); const startIndex = page * pageSize; const endIndex = startIndex < length ? Math.min(startIndex + pageSize, length) : startIndex + pageSize; return 第${startIndex + 1}-${endIndex}条, 总共${length}条; }}在app.module.ts中声明该Provider: providers: [ {provide: MatPaginatorIntl, useClass: MatPaginatorIntlCro } ]这样在再使用分页组件时,相关信息将显示中文。 ...

December 14, 2018 · 1 min · jiezi

Release ng-alain 2.0

从计划2.0开始足足进行近四个月,其中发布过八个版本。当初给2.0做的愿景基本上达到要求,当然一切都还是那句话:【让开发者更加专注于业务】。ng-zorro-antd 提供的大量的基础组件,当你熟悉这些组件以后,开发 Angular 会是一种“爽”体验,然而对于中后台而言部分高频繁组件在大多数场景下显得有点臃肿。所以 2.0 变更主要从两个方面:使 CURD 操作更“自然”开发体验更友好响应式开发CURD提供一组 Simple 系列组件:sv:查看se:编辑st:数据表格(原 simple-table 重新重构)以及基于 JSON Schema 的动态表单 sf,这四个 Simple 系列组件相比较 ng-zorro-antd 的原始写法,更易编写、阅读,基本上可以满足大多数场景;但它们并非用来替代原始的写法,特别是 st 与 sf 它们并不适合复杂交互,此时,依然应该优先使用原始方式。除此之外,2.0 对部分输入属性及接口的多态性、内聚性做一些变更。属性多态性当构建一个数据表格时,表格的数据源可能来自远程数据或本地静态数据,但我们不应该过度的将数据源做成两个不同属性加以区分,他们只是不同的数据来源而已,但对于表格而言是统一:<table [data]=“url”></table><table [data]=“list”></table>属性内聚性同一个功能的属性应该更内聚,例如我们表述一个HTTP请求时,包含:请求方法、请求域、参数等,这些属性应该统一在一个对象值体现,HttpClient 请求就是一个非常好的例子。<st [reqMethod]="‘GET’" [reqParams]="{ a: 1 }"></st><st [req]="{ method: ‘get’, params: { a: 1 } }"></st>响应式开发意指开发过程中如何使用最小的方式构建符合移动端的中后台,ng-alain 默认提供一套 响应式服务 规则,它服务于最基础的CURD组件:se、sv 等。例如:当你希望构建一行两列的表单,并且若屏幕小于 <576px 将自动转化成一列,则只需要这样:<div se-container=“2”> <se label=“Field1”></se> <se label=“Field2”></se></div>当然这一切只是简化 nz-row、nz-col 的运用而已,如果你希望布局也延续这种方式可以使用 sg 组件。除此之外,ng-alain 也将 CSS 做为开发语言非常重要的组成部分,并且将这些语言特征转化成独立的单元类,如果你是采用 VSCODE 可借由 ng-alain snippets 提供的智能提醒,减少理解它们的成本。升级Angular Cli 提供的 ng update 命令让我们可以大胆重构、改进你的组件,并且用户升级只需要简单的一行命令就可以完全升级。从 1.x 升级至 2.x 虽然无法改变 ts 代码方面的动作,但基本上可以完成 HTML 方面的升级,主要还是 ts 代码的解析无法像 HTML 那样预期。而 ng-alain 的 1.x 升至 2.0 也只需要一行命令而已,有关更多细节,请参考升级指引。未来直到 ng-zorro-antd 下一大版本更新之前,2.0 保持一段时间的休息期,不再会有新功能。之后,会根据 ng-zorro-antd 的进度,对 ng-alain 做一次大的性能重构。新尝试ng-alain 正在尝试提供商业主题服务,有兴趣可以参阅。(完) ...

November 21, 2018 · 1 min · jiezi

问道Angular——Angular刷新当前页面

onSameUrlNavigation 从angular5.1起提供onSameUrlNavigation来支持路由重新加载。、 有两个值’reload’和’ignore’。默认为’ignore’ 定义当路由器收到一个导航到当前 URL 的请求时应该怎么做。 默认情况下,路由器将会忽略这次导航。但这样会阻止类似于 “刷新” 按钮的特性。 使用该选项可以配置导航到当前 URL 时的行为。使用配置onSameUrlNavigation@NgModule({ imports: [RouterModule.forRoot( routes, { onSameUrlNavigation: ‘reload’ } )], exports: [RouterModule]}) reload实际上不会重新加载路由,只是重新出发挂载在路由器上的事件。配置runGuardsAndResolvers runGuardsAndResolvers有三个值:paramsChange: 仅在路由参数更改时触发。如/reports/:id 中id更改paramsOrQueryParamsChange: 当路由参数更改或参训参数更改时触发。如/reports/:id/list?page=23中的id或page属性更改always :始终触发const routes: Routes = [ { path: ‘’, children: [ { path: ‘report-list’, component: ReportListComponent }, { path: ‘detail/:id’, component: ReportDetailComponent, runGuardsAndResolvers: ‘always’ }, { path: ‘’, redirectTo: ‘report-list’, pathMatch: ‘full’ } ] }];组件监听router.eventsimport {Component, OnDestroy, OnInit} from ‘@angular/core’;import {Observable} from ‘rxjs’;import {Report} from ‘@models/report’;import {ReportService} from ‘@services/report.service’;import {ActivatedRoute, NavigationEnd, Router} from ‘@angular/router’;@Component({ selector: ‘app-report-detail’, templateUrl: ‘./report-detail.component.html’, styleUrls: [’./report-detail.component.scss’]})export class ReportDetailComponent implements OnInit, OnDestroy { report$: Observable<Report>; navigationSubscription; constructor( private reportService: ReportService, private router: Router, private route: ActivatedRoute ) { this.navigationSubscription = this.router.events.subscribe((event: any) => { if (event instanceof NavigationEnd) { this.initLoad(event); } }); } ngOnInit() { const id = +this.route.snapshot.paramMap.get(‘id’); this.report$ = this.reportService.getReport(id); } ngOnDestroy(): void { // 销毁navigationSubscription,避免内存泄漏 if (this.navigationSubscription) { this.navigationSubscription.unsubscribe(); } } initLoad(e) { window.scrollTo(0, 0); console.log(e); }}☞☞☞问道Angular系列☜☜☜ ...

November 20, 2018 · 1 min · jiezi

初探Subject and Subscribe (Ionic4 Angular6)

类似的东西很多,但是不得不说大部分资料中的例子不是难以理解就是形而上学,所以我想分享一点自己的代码来帮助更多初学者。起源:我打算做一个弹出登录,里面包含一个注册按钮,点击注册则切换到弹出注册,当然在注册时也能切换回登录。在实现的过程中一路踩过以下的坑:如果将一个component/page应用到modal中,那么这个component/page必须在不低于弹出modal的层级的module.ts文件中的@NgModule的entryComponents部分声明出来,不然会得到一个错误。一个component/page只能在一个module.ts文件中声明调用弹出方法的ts文件必须引用使用到的component/page如果两个ts文件中都同时引用了对方(a.ts import b, b.ts impot a)那么会得到一个循环依赖的警告,这不影响运行,但是看着闹心。所以我要再次修改一下,避免这个警告,最后选择了subject的方案。不要尝试直接复制运行,因为我去掉了和本文无关的部分,比如import { Component } from ‘@angular/core’;这样的语句。首先在某个单例模式的service中加入如下代码:import { Subject, Subscription } from ‘rxjs’;//公有属性switchModel,这是一个可供订阅的主题@Injectable({ providedIn: ‘root’})export class SiteService { public switchModel: Subject<Object> = new Subject<Object>();}为了解决问题4,所以弹出的行为将仅在app.component上发生import { ModalController } from ‘@ionic/angular’;import { SiteService } from ‘../services/site/site.service’; //自行替换为声明了subject的serviceimport { LoginComponent } from ‘../login/login.component’; //自行替换为你要弹出的内容1import { SignupComponent } from ‘../signup/signup.component’; //自行替换为你要弹出的内容2/如果有的话你可以添加更多的内容注意确认它们被正确的声明在entryComponents部分对于app.component.ts,其对应的module必然是app.module.ts/export class AppComponent { constructor( private modalController: ModalController, private siteService: SiteService ){ this.siteService.switchModel.subscribe(option => { this._switchModel(option); }); } private _switchModel(option) { let component = null; switch(option.componentName) { case ‘LoginComponent’: component = LoginComponent; break; case ‘SignupComponent’: component = SignupComponent; break; case ‘’: //如果没有指定component那就是直接关闭咯,这是为了在component内部能省掉引用ModalController和关闭modal的代码 break; default: return; //这件事和我无关,跳过。防止对其它subscriber负责的component重复处理 } this.modalController.dismiss().then(() => { this.modalController.create({ component: component, componentProps: option.params || null }).then(modal => modal.present()); }); }}然后在LoginComponent为注册按钮添加一个事件,SignupComponent做类似的处理import { SiteService } from ‘../services/site/site.service’;export class LoginComponent implements OnInit { doSwitch() { this.siteService.switchModel.next({ componentName: ‘SignupComponent’ }); } cancel() { this.siteService.switchModel.next({ componentName: ’’ }); }}逻辑描述:LoginComponent调用Subject的next方法同时传递数据,这样Subject就会将该数据通知到订阅者(AppComponent订阅了这个subject),AppComponent在得到Subject更新的消息时会根据最新的消息做出适当的处理。理论上来说,我们可以创建一个全局的主题,每个订阅者都可以通过消息的数据类型或其它特征决定自己如何处理,但是个人还是喜欢定制主题从而减少订阅。刚接触Angular6不久,不管是我这个想法本身有错误还是解决的方式有问题都请拍砖不要客气,只求大侠的砖头上绘制一下示例代码,不胜感激。 ...

November 7, 2018 · 1 min · jiezi

前端数据模型Model;适用于多人团队协作的开发模式

前言本文讲述的数据模型并不是一个库,也不是需要npm的包,仅仅只是一种在多人团队协作开发的时候拟定的规则。至少目前为止,我们的开发团队再也没用过mock(虽然一开始也没用),也不用担心后台数据的字段或结构发生变动,真正实现前后台并行开发的愉快模式。本文技术栈有 Typescript、Rxjs、AngularX定义Model类比于java里的类,我们的Model也是一个类,是TS的类,我们根据需求和设计图或原型图规划好某一个具体的模块的基类Model,并自行定义一些字段和枚举类型,方法属性等,并不需要强行和后台的字段一致,要保证百分百纯的前后端分离,举个例子比如开发某一个后台管理项目,里边有产品(Product)模块、用户(User)模块等那么我们会在model文件夹里定义BaseProduct的基类export class BaseProductModel { constructor() {} // 必有id 和 name public id: number = null; public name: string = ‘’; /…more…/}基类的定义是必要的,可以节省很多不必要的代码,并不需要写一个页面或组件就重新定义新的model,如果某一个组件里面需要对这个产品的内容进行拓展的大可直接继承,并不会影响其他有了这个基类的文件我们推崇一切基类都必须继承,不可直接构造真实的项目中产品的字段和属性肯定不止只有id和name,可能还包含版本、缩略图地址、唯一标识、产品、对应规格的价格、状态、创建时间等等;这些属性完全可以放在基类里,因为所有产品都有这些属性,说到类型和状态的定义,请注意绝对不能将可枚举性质的属性直接使用后台或第三方返回的对应属性比如,产品模块里最基础的状态(status)属性,假设后台定义的对应状态有0: 禁用1: 启用2: 隐藏3: 不可购买这四种,倘若我们在项目当中直接使用这些对应状态的数字去判断或进行逻辑处理,分不分的清另谈,如果中途或以后状态的数字变了,GG。可能大家觉得这样的情况很少,但也不是没有,一旦出现改起来BUG就一堆。所以对于这种可枚举性质的属性我们会定义一个枚举类(Enum)export enum EStatus { BAN = 0, OPEN = 1, HIDE = 2, NOTBUY = 3}然后在model里这样export class BaseProductModel { // …… public status: string = EStatus[1] // 默认启用}美滋滋,而且在进行逻辑判断的时候我们也不用去关心每个状态对应的数字是什么,我们只关心它是BAN还是OPEN,简洁明了不含糊而且我们还可以给model增加一个只读属性,用来返回这个状态对应的中文提示(这种需求很常见)public get conversionStatusHint() : string { const _ = { BAN: ‘禁用’, OPEN: ‘启用’, HIDE: ‘隐藏’, NOTBUY: ‘买不得呀’ } return _[this.status] ? _[this.status] : ‘’}这样就不用在每一个组件里面写一个方法来传参数返回中文名称了到了这里,我们的BaseProductModel已经算是定义好了,下面我们就需要给这个model定义一个方法目的是把后台返回的字段和数据结构转化为我们自己定义的字段和数据结构转化后台数据可能到了这里很多人会觉得这是多此一举,后台都直接返回数据了还转化什么,返回什么用什么就得了。但在大型的团队开发项目当中,谁也不能保证一个字段也不修改,一个字段也不删除或增加或缺失,牵一发动全身。人生苦短。而且还有一种情况就是,可能这个项目是前端先进行,后台还未介入,需要前端这边先把整体的功能和样式都先根据设计图规划开发。export class BaseProductModel { // …… // 转化后台数据 public setData( data: BaseProductModel ): void { if (data) { for (let e in this) { if ((<Object>data).hasOwnProperty(e)) { if( e == ‘status’ ) { this.status = EStatus[(<any>data)[e]] } else { this[e] = (<any>data)[e]; } } } } }}然后在调用的时候/** 假设ProductModel类继承了BaseProductModel类 /public productModel: ProductModel = new ProductModel();/…more…/this.productModel.setData(<BaseProductModel>{ // 假设后台定义的创建时间字段是create_at,model里定的创建时间是createTime createTime: data.create_at});// 即使数据结构不一致也可在这里进行统一转化做好了转化这一步,所有的数据变动和数据结构的变化都在这同一个地方修改即搞定,这个时候随便后台怎么改,欢乐改,都不影响我们后续的逻辑处理和字段的变动。同理,在post数据给后台的时候转化就显得容易多了,后台需要什么数据和字段再转化一次不就得了。以上的数据模型可以很好的降低前后台掐架的概率,mock?不需要下面是一个我们抽离出来的常用的表格数据模型基类import { BehaviorSubject } from ‘rxjs’//分页配置export interface PaginationConfig { // 当前的页码 pageIndex: number; // 总数 total: number; // 当前选中的一页显示多少个的数量 rows: number; // 可选择的每页显示多少个数量 rowsOptions?: Array<number>;}//分页配置初始数据export let PaginationInitConfig: PaginationConfig = { pageIndex: 1, total: 0, rows: 10, rowsOptions: [10, 20, 50]}//表格配置export interface TableConfig extends PaginationConfig { // 是否显示loading效果 isLoading?: boolean; // 是否处于半选状态 isCheckIndeterminate?: boolean; // 是否全选状态 isCheckAll?: boolean; // 是否禁用选中 isCheckDisable?: boolean; //没有数据的提示 noResult?: string;}//表头export interface TableHead { titles: string[]; widths?: string[]; //样式类 src/styles/ 中有公用的表格样式类 classes?: string[]; sorts?: (boolean | string)[];}//分页参数export interface PageParam { page: number; rows: number;}//排序类型export type orderType = ‘desc’ | ‘asc’ | null | ‘’//排序参数export interface SortParam { orderBy?: string; order?: orderType}// 所有表格的基类export class BaseTableModel<T> { //表格配置 tableConfig: TableConfig //表格头部配置 tableHead: TableHead //表格数据流 tableData$: BehaviorSubject<T[]> //排序类型 orderType: orderType //当前排序的标示 currentSortBy: string constructor( //选中的 key private checkKey: string = ‘isChecked’, //禁用的 key private disabledKey: string = ‘isDisabled’ ) { this.initData() } // 重置数据 public initData(): void { this.tableHead = { titles: [] } this.tableConfig = { pageIndex: 1, total: 0, rows: 10, rowsOptions: [10, 20, 50], isLoading: false, isCheckIndeterminate: false, isCheckAll: false, isCheckDisable: false, noResult: ‘暂无数据’ } this.tableData$ = new BehaviorSubject([]) } /* * 设置表格配置 * @author GR-05 * @param conf / setConfig(conf: TableConfig): void { this.tableConfig = Object.assign(this.tableConfig, conf) } /* * 设置表格头部标题 * @author GR-05 * @param titles / setHeadTitles(titles: string[]): void { this.tableHead.titles = titles } /* * 设置表格头部宽度 * @author GR-05 * @param widths / setHeadWidths(widths: string[]): void { this.tableHead.widths = widths } /* * 设置表格头部样式类 * @author GR-05 * @param classes / setHeadClasses(classes: string[]): void { this.tableHead.classes = classes } /* * 设置表格排序功能 * @author GR-05 * @param sorts / setHeadSorts(sorts: (boolean | string)[]): void { this.tableHead.sorts = sorts } /* * 设置当前排序类型 * @param ot / setSortType(ot: orderType) { this.orderType = ot } /* * 设置当前排序标识 * @param orderBy / setSortBy(orderBy: string) { this.currentSortBy = orderBy } /* * 设置当前被点击的排序标示 * @param i 排序数组索引 / sortByClick(i: number) { if (this.tableHead.sorts && this.tableHead.sorts[i]) { if (!this.orderType) { this.orderType = ‘desc’ } else { this.orderType == ‘desc’ ? this.orderType = ‘asc’ : this.orderType = ‘desc’ } this.currentSortBy = this.tableHead.sorts[i] as string } } /* * 获取当前的排序参数 / getCurrentSort(): SortParam { return { order: this.orderType, orderBy: this.currentSortBy } } /* * 设置表格loading * @author GR-05 * @param flag / setLoading(flag: boolean = true): void { this.tableConfig.isLoading = flag } /* * 设置当前表格数据总数 * @author GR-05 * @param total / setTotal(total: number): void { this.tableConfig.total = total } setPageAndRows(pageIndex: number, rows: number = 10) { this.tableConfig.pageIndex = pageIndex this.tableConfig.rows = rows } /* * 更新表格数据(新数据、单选、多选) * @author GR-05 * @param dataList / setDataList(dataList: T[]): void { this.tableConfig.isCheckAll = false this.tableConfig.isCheckIndeterminate = dataList.filter(item => !item[this.disabledKey]).some(item => item[this.checkKey] == true) this.tableConfig.isCheckAll = dataList.filter(item => !item[this.disabledKey]).every(item => item[this.checkKey] == true) this.tableConfig.isCheckAll ? this.tableConfig.isCheckIndeterminate = false : {} this.tableData$.next(dataList); if (dataList.length == 0) { this.tableConfig.isCheckAll = false } } /* * 获取已选的项 * @author GR-05 */ getCheckItem(): T[] { return this.tableData$.value.filter(item => item[this.checkKey] == true && !item[this.disabledKey]) }}我们为什么没有抽离成组件而是数据模型这么一个类上,主要是因为,组件的样式我们是不确定唯一性的,但数据和处理逻辑确是类似的,哪里地方要用到,就在哪个组件里new一个就好了;其中BaseTableModel后面的T可以是所有你想在表格上渲染的任何一个model类,比如之前的ProductModel,页面需求需要展示产品的表格列表,则export class TableModel extends BaseTableModel<ProductModel> { constructor() { super(); }}那么最后你只需要将BaseTableModel里的tableData$数据next成处理好的ProdcuModel数组就好了。 ...

October 29, 2018 · 3 min · jiezi

如何更好使用 ng-zorro-antd 图标

自 ng-zorro-antd 1.7.x 以后图标发生破坏性变更,虽然带了诸多优势,同时也带来几个劣势:若采用动态加载会产生额外的HTTP请求若静态加载需要逐一注册图标st 组件的 format 参数无法直接指定图标ng-alain 默认使用静态加载的做法,毕竟后端使用图标相对于比较有限,即使将 svg 都打包进脚本相比较之前整个 styles 体积上是所有减少,但比较并不多。而针对以上问题,ng-alain 提供几种方案。使用icon插件(推荐)尽可能从项目中分析并生成静态 Icon,插件会自动在 src 目录下生成两个文件:src/style-icons.ts 自定义部分无法解析(例如:远程菜单图标)src/style-icons-auto.ts 命令自动生成文件自动排除 ng-zorro-antd 和 @delon 已经加载的图标。ng g ng-alain:plugin icon同时,需要手动在 startup.service.ts 中导入:import { ICONS_AUTO } from ‘../../../style-icons-auto’;import { ICONS } from ‘../../../style-icons’;@Injectable()export class StartupService { constructor(iconSrv: NzIconService) { iconSrv.addIcon(…ICONS_AUTO, …ICONS); }}有效语法<i class=“anticon anticon-user”></i><i class=“anticon anticon-question-circle-o”></i><i class=“anticon anticon-spin anticon-loading”></i><i nz-icon class=“anticon anticon-user”></i><i nz-icon type=“align-{{type ? ’left’ : ‘right’}}"></i><i nz-icon [type]=“type ? ‘menu-fold’ : ‘menu-unfold’” [theme]=“theme ? ‘outline’ : ‘fill’"></i><i nz-icon [type]=“type ? ‘fullscreen’ : ‘fullscreen-exit’"></i><i nz-icon type=”{{ type ? ‘arrow-left’ : ‘arrow-right’ }}"></i><i nz-icon type=“filter” theme=“outline”></i><nz-input-group [nzAddOnBeforeIcon]=“focus ? ‘anticon anticon-arrow-down’ : ‘anticon anticon-search’"></nz-input-group>动态加载动态加载,这是为了减少包体积而提供的方式。当 NG-ZORRO 检测用户想要渲染的图标还没有静态引入时,会发起 HTTP 请求动态引入。你只需要配置 angular.json 文件:{ “assets”: [ { “glob”: “**/*”, “input”: “./node_modules/@ant-design/icons-angular/src/inline-svg/”, “output”: “/assets/” } ]}动态使用不管是静态或动态加载,依然无法解决原始方法 class=“anticon anticon-” 的便利性,毕竟字符串就是字符串并非 Angular 模板无法被解析,而针对这个问题,提供两种解决办法。类样式事实上所有 Antd 图标都可以在 iconfont 找得到,可以点选自己需要的图标并生成相应的 css 文件或 cdn,最后在项目中可以直接使用 1.7.0 之前的写法。注意: 在项目编辑里加上 anticon anticon- 前缀才能同之前的类名保持一致。// angular.json"styles”: [ “src/iconfont.css”],如果非cnd还需要将相应的 icon 图标文件复制到 assets 目录下,同时修改 iconfont.css 中 @font-face 对应的 url 路径。@angular/elements动态加载的另一种方式是使用 @angular/elements,只需要 nz-icon 指令重新封装成组件。import { Component, Input } from ‘@angular/core’;@Component({ selector: ’nz-icon’, template: &lt;i nz-icon [type]="type"&gt;&lt;/i&gt;,})export class IconComponent { @Input() type: string;}同时在根模块里注册它。import { createCustomElement } from ‘@angular/elements’;@NgModule({ declarations: [], entryComponents: []})export class AppModule { constructor(injector: Injector) { customElements.define(’nz-icon’, createCustomElement(IconComponent, { injector })); }}最后。@Component({ selector: ‘app-demo’, template: &lt;div [innerHTML]="value"&gt;&lt;/div&gt; ,})export class DemoComponent { constructor(private san: DomSanitizer) { } value = this.san.bypassSecurityTrustHtml( icon: &lt;nz-icon type="bell"&gt;&lt;/nz-icon&gt;, );}结论本文只是针对这一次 ng-zorro-antd 图标上的变更做一个总结,就我个人而言还是比较推荐静态加载的方式,这无关乎包体大小的问题,而是更加明确我需要什么所以我应加载什么。 ...

October 26, 2018 · 1 min · jiezi

富交互Web应用中的撤销和前进

在web应用中,用户在进行一些富交互行为的操作时难免会出现误操作,比如在富文本编辑器设置错了字体颜色就需要撤回,做H5活动页面的时候不小心删了一个图片也需要撤回,更比如在线设计原型图应用的时候不小心删了一个页面等,总之在交互场景非常复杂的情况下,用户操作失误的可能性非常大,这时候‘撤销’和‘前进’这两个操作就很有必要了,而且用户体验也很好思路不管是任何场景下的web应用,用户的每一次操作我们都可以看成是对某个组件或某个对象的状态和属性进行改变,一旦连续的动作操作完成正准备进行下一个动作之前,此刻的状态就是一个全新的状态A —— B —— C用户未操作的时候全局状态是A用户操作某个组件使其移动到位置X,松开鼠标之后全局状态是B用户操作另一个组件使其删除,完成后全局状态是C所以,撤销的操作就是在用户操作状态到C的时候让全局的状态回到B,回到上一次操作完的时候。那么就需要可以存放这种大量状态的列表或索引来记录每一次操作的动作但如果我用某一个数组变量来存储如此庞大的数据是不是略显不妥?数据量越大内存应该会爆吧?所以这里我推荐大家使用IndexedDB下面是利用Angular、Rxjs和IndexedDB封装好的一个服务类import { Inject } from “@angular/core”;import { IndexedDBAngular } from “indexeddb-angular”;import { Subject, Observer, Observable } from “rxjs”;export interface IDBData { widgetList: string}// 前进和后退的服务@Inject({ providedIn: ‘root’})export class PanelExtendMoveBackService { /** * 发射DB集合存储的数据,可订阅 / public launchDBDataValue$: Subject<IDBData> = new Subject<IDBData>() /* * 创建一个叫panelDataDB的本地数据库,版本号为1 / public db = new IndexedDBAngular(‘panelDataDB’, 1) /* * 记录前进和后退的存储集合项的下标key * 默认为0 / public dbCurrentIndex: number = 0 /* * 自增的DBkey / public dbKey: number = -1 // 是否允许前进 public get isMove() : boolean { return this.dbCurrentIndex < this.dbKey } // 是否允许后退 public get isBack() : boolean { return this.dbCurrentIndex > 0 } constructor() {} /* * 创建DB集合 / public createCollections(): Observable<boolean> { const _sub: Subject<boolean> = new Subject<boolean>() this.dbKey = -1 this.db.createStore(1, (db: any) => { db.currentTarget.result.createObjectStore(‘panelItem’) }).then(()=>{ this.dbClear() _sub.next(true) }) return _sub.asObservable() } /* * 往集合里添加数据 * 同时把新添加的key赋值给dbCurrentIndex, / public dbAdd(): void { this.handleDbCurrentRefreshDB(); this.dbKey += 1; // 此处存储你要保存的数据 const _widget_list = [] this.db.add(‘panelItem’, { widgetList: JSON.stringify(_widget_list) }, this.dbKey).then( _e => { if ((<Object>_e).hasOwnProperty(‘key’)) { this.dbCurrentIndex = _e.key }; }, () => { this.dbKey -= 1 throw new Error(‘添加panelItem集合失败’) } ) } /* * 在执行添加数据集操作的时候判断dbCurrentIndex当前指引的下标是否低于dbKey * 如果是说明执行了后退操作之后后续动作执行了dbAdd的操作,则清空dbCurrentIndex索引之后的数据重新添加 / public handleDbCurrentRefreshDB(): void { if (this.dbCurrentIndex < this.dbKey) { for (let i = this.dbCurrentIndex + 1; i <= this.dbKey; i++) { this.db.delete(‘panelItem’, i).then(() => {}) } this.dbKey = this.dbCurrentIndex } } /* * 执行后退操作发射DB数据集 / public acquireBackDBData(): void { if( this.isBack ) { this.dbCurrentIndex -= 1 this.db.getByKey(‘panelItem’, this.dbCurrentIndex).then(res=>{ this.launchDBDataValue$.next(res) },()=>{ }) } } /* * 执行前进操作发射DB数据集 / public acquireMoveDBData(): void { if( this.isMove ) { this.dbCurrentIndex += 1 this.db.getByKey(‘panelItem’, this.dbCurrentIndex).then(res => { this.launchDBDataValue$.next(res) }, () => { }) } } /* * 清除DB集合panelItem */ public dbClear(): void { this.db.clear(‘panelItem’).then(_e => {}) }}这里我偷懒了一下,直接采用自增的id作为key了,也方便查找每一次操作所存储的数据如下最后可以看一下我实现好了的撤销和前进操作的场景 ...

October 19, 2018 · 2 min · jiezi