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 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

我理解的 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

Angular6错误 Service: No provider for Renderer2

这个错误一般是在构造函数中 注入 Renderer2 引起的。解决办法很简单,既然无法注入Renderer2就不注入。可以利用工厂类RendererFactory2 代替直接注入Renderer2的方式。示例代码如下:import { Renderer2, RendererFactory2 } from ‘@angular/core’;@Injectable()class Service { private renderer: Renderer2; constructor(rendererFactory: RendererFactory2) { this.renderer = rendererFactory.createRenderer(null, null); }}利用renderer对象去操作元素即可

February 27, 2019 · 1 min · jiezi

使用Angular构建Chrome插件

最近公司有业务需求需要做一个Chrome插件,原本想着用jQuery + Bootstarp搞定。但是由于已经使用Angular已经太久了,重新再用jQuery有点蛋疼。后面查了一下资料,发现有人用React写过Chrome插件。仔细看了下,觉得实际上应该是一样的原理。尝试了一下Angular + Material,果然可以,过程如下:1.先贴配置Angular CLI: 7.1.3Angular: 7.1.3Node: 10.11.0Material UI: 7.2.1配置理论上不需要跟我一样,我贴出来的原因是方便有个参考而已。Angular 4以上的版本应该都是可以通用的。2.Angular项目打包编译没有项目的话用CLI新建一个ng new OneAngularProject已有项目的直接在项目下buildng build注意,build时不用任何参数,以往我们为了压缩合并js,css等文件会添加参数,以保证build出来的是最小版本。但如果是构建Chrome插件的话,就不能加参数了。3.编辑manifest.json我项目里的mainfest.json如下{ “manifest_version”: 2, “name”: “Angular Chrome Extension”, “description”: “Miner app as chrome extension using angular7”, “version”: “1.0”, “browser_action”: { “default_icon”: “favicon.ico”, “default_popup”: “index.html” }, “permissions”: [], “content_security_policy”: “script-src ‘self’ ‘unsafe-eval’; object-src ‘self’”, “web_accessible_resources”: [ “assets/css/”, “assets/js/”, “assets/fonts/*” ]}“content_security_policy”: “script-src ‘self’ ‘unsafe-eval’; object-src ‘self’” 注意这句一定要加上,不加上的话会报错web_accessible_resources 字段看自己项目里的业务需求请自定义4.将build好的文件放入manifest.json所在文件夹下在Chrome浏览器中加载该插件即可,记得要打开开发者模式效果如图:具体如何开发Chrome插件和一些细节,这里就不提了。有兴趣的可以看这个博客里的文章,个人觉得写得还是很不错的。chrome插件开发教程

January 16, 2019 · 1 min · jiezi

微服务实战 - docker-compose实现mysql+springboot+angular

前言这是一次完整的项目实践,Angular页面+Springboot接口+MySQL都通过Dockerfile打包成docker镜像,通过docker-compose做统一编排。目的是实现整个项目产品的轻量级和灵活性,在将各个模块的镜像都上传公共镜像仓库后,任何人都可以通过 “docker-compose up -d” 一行命令,将整个项目的前端、后端、数据库以及文件服务器等,运行在自己的服务器上。本项目是开发一个类似于segmentfault的文章共享社区,我的设想是当部署在个人服务器上时就是个人的文章库,部署在项目组的服务器上就是项目内部的文章库,部署在公司的服务器上就是所有职工的文章共享社区。突出的特点就是,项目相关的所有应用和文件资源都是灵活的,用户可以傻瓜式地部署并使用,对宿主机没有任何依赖。目前一共有三个docker镜像,考虑以后打在一个镜像中,但目前只能通过docker-compose来编排这三个镜像。MySQL镜像:以MySQL为基础,将项目所用到的数据库、表结构以及一些基础表的数据库,通过SQL脚本打包在镜像中。用户在启动镜像后就自动创建了项目所有的数据库、表和基础表数据。SpringBoot镜像:后台接口通过SpringBoot开发,开发完成后直接可以打成镜像,由于其内置tomcat,可以直接运行,数据库指向启动好的MySQL容器中的数据库。Nginx(Angular)镜像:Nginx镜像中打包了Angular项目的dist目录资源,以及default.conf文件。主要的作用有:部署Angular项目页面;挂载宿主机目录作为文件服务器;以及反向代理SpringBoot接口,解决跨域问题等等。最后三个docker容器的编排通过docker-compose来实现,三个容器之间的相互访问都通过容器内部的别名,避免了宿主机迁移时ip无法对应的问题。为了方便开发,顺便配了个自动部署。MySQL镜像初始化脚本在项目完成后,需要生成项目所需数据库、表结构以及基础表数据的脚本,保证在运行该docker容器中,启动MySQL数据库时,自动构建数据库和表结构,并初始化基础表数据。Navicat for MySQL的客户端支持导出数据库的表结构和表数据的SQL脚本。如果没有安装Navicat,可以在连接上容器中开发用的MySQL数据库,通过mysqldump 命令导出数据库表结构和数据的SQL脚本。下文中就是将数据库的SQL脚本导出到宿主机的/bees/sql 目录:docker exec -it mysql mysqldump -uroot -pPASSWORD 数据库名称 > /bees/sql/数据库名称.sql以上只是导出 表结构和表数据的脚本,还要在SQL脚本最上方加上 生成数据库的SQL:drop database if exists 数据库名称;create database 数据库名称;use 数据库名称;通过以上两个步骤,数据库、表结构和表数据三者的初始化SQL脚本就生成好了。Dockerfile构建镜像我们生成的SQL脚本叫 bees.sql,在MySQL官方镜像中提供了容器启动时自动执行/docker-entrypoint-initdb.d文件夹下的脚本的功能(包括shell脚本和sql脚本),我们在后续生成镜像的时候,将上述生成的SQL脚本COPY到MySQL的/docker-entrypoint-initdb.d目录下就可以了。现在我们写Dockerfile,很简单:FROM mysqlMAINTAINER kerry(kerry.wu@definesys.com)COPY bees.sql /docker-entrypoint-initdb.d将 bees.sql 和 Dockerfile 两个文件放在同一目录,执行构建镜像的命令就可以了:docker build -t bees-mysql .现在通过 docker images,就能看到本地的镜像库中就已经新建了一个 bees-mysql的镜像啦。SpringBoot镜像springboot构建镜像的方式很多,有通过代码生成镜像的,也有通过jar包生成镜像的。我不想对代码有任何污染,就选择后者,通过生成的jar包构建镜像。创建一个目录,上传已经准备好的springboot的jar包,这里名为bees-0.0.1-SNAPSHOT.jar,然后同样编写Dockerfile文件:FROM java:8VOLUME /tmpADD bees-0.0.1-SNAPSHOT.jar /bees-springboot.jarEXPOSE 8010ENTRYPOINT [“java”,"-Djava.security.egd=file:/dev/./urandom","-jar","-Denv=DEV","/bees-springboot.jar"]将bees-0.0.1-SNAPSHOT.jar和Dockerfile放在同一目录执行命令开始构建镜像,同样在本地镜像库中就生成了bees-springboot的镜像:docker build -t bees-springboot .Nginx(Angular)镜像Nginx的配置该镜像主要在于nginx上conf.d/default.conf文件的配置,主要实现三个需求:1、Angualr部署Angular的部署很简单,只要将Angular项目通过 ng build –prod 命令生成dist目录,将dist目录作为静态资源文件放在服务器上访问就行。我们这里就把dist目录打包在nginx容器中,在default.conf上配置访问。2、文件服务器项目为文章共享社区,少不了的就是一个存储文章的文件服务器,包括存储一些图片之类的静态资源。需要在容器中创建一个文件目录,通过default.conf上的配置将该目录代理出来,可以直接访问目录中的文件。当然为了不丢失,这些文件最好是保存在宿主机上,在启动容器时可以将宿主机本地的目录挂载到容器中的文件目录。3、接口跨域问题在前后端分离开发的项目中,“跨域问题”是较为常见的,SpringBoot的容器和Angular所在的容器不在同一个ip和端口,我们同样可以在default.conf上配置反向代理,将后台接口代理成同一个ip和端口的地址。话不多说,结合上面三个问题,我们最终的default.conf为:server { listen 80; server_name localhost; location / { root /usr/share/nginx/html; index index.html index.htm; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://beesSpringboot:8010/; } location /file { alias /home/file; index index.html index.htm; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; }}location / :代理的是Angular项目,dist目录内通过Dockerfile COPY在容器内的/usr/share/nginx/html目录;location /file :代理/home/file 目录,作为文件服务器;location /api/ :是为了解决跨域而做的反向代理,为了脱离宿主机的限制,接口所在容器的ip通过别名beesSpringboot来代替。别名的设置是在docker-compose.yml中设置的,后续再讲。Dockerfile构建镜像同样创建一个目录,包含Angualr的dist目录、Dockerfile和nginx的default.conf文件,目录结构如下:[root@Kerry angular]# tree.├── dist│ └── Bees│ ├── 0.cb202cb30edaa3c93602.js│ ├── 1.3ac3c111a5945a7fdac6.js│ ├── 2.99bfc194c4daea8390b3.js│ ├── 3.50547336e0234937eb51.js│ ├── 3rdpartylicenses.txt│ ├── 4.53141e3db614f9aa6fe0.js│ ├── assets│ │ └── images│ │ ├── login_background.jpg│ │ └── logo.png│ ├── favicon.ico│ ├── index.html│ ├── login_background.7eaf4f9ce82855adb045.jpg│ ├── main.894e80999bf907c5627b.js│ ├── polyfills.6960d5ea49e64403a1af.js│ ├── runtime.37fed2633286b6e47576.js│ └── styles.9e4729a9c6b60618a6c6.css├── Dockerfile└── nginx └── default.confDockerfile文件如下:FROM nginxCOPY nginx/default.conf /etc/nginx/conf.d/RUN rm -rf /usr/share/nginx/html/*COPY /dist/Bees /usr/share/nginx/htmlCMD [“nginx”, “-g”, “daemon off;"]以上,通过下列命令,构建bees-nginx-angular的镜像完成:docker build -t bees-nginx-angular . docker-compose容器服务编排上述,我们已经构建了三个镜像,相对应的至少要启动三个容器来完成项目的运行。那要执行三个docker run?太麻烦了,而且这三个容器之间还需要相互通信,如果只使用docker来做的话,不光启动容器的命令会很长,而且为了容器之间的通信,docker –link 都会十分复杂,这里我们需要一个服务编排。docker的编排名气最大的当然是kubernetes,但我的初衷是让这个项目轻量级,不太希望用户安装偏重量级的kubernetes才能运行,而我暂时又没能解决将三个镜像构建成一个镜像的技术问题,就选择了适中的一个产品–docker-compse。安装docker-compose很简单,这里就不赘言了。安装完之后,随便找个目录,写一个docker-compose.yml文件,然后在该文件所在地方执行一行命令就能将三个容器启动了:#启动docker-compose up -d#关闭docker-compose down这里直接上我写的docker-compose.yml文件version: “2"services: beesMysql: restart: always image: bees-mysql ports: - 3306:3306 volumes: - /bees/docker_volume/mysql/conf:/etc/mysql/conf.d - /bees/docker_volume/mysql/logs:/logs - /bees/docker_volume/mysql/data:/var/lib/mysql environment: MYSQL_ROOT_PASSWORD: kerry beesSpringboot: restart: always image: bees-springboot ports: - 8010:8010 depends_on: - beesMysql beesNginxAngular: restart: always image: bees-nginx-angular ports: - 8000:80 depends_on: - beesSpringboot volumes: - /bees/docker_volume/nginx/nginx.conf:/etc/nginx/nginx.conf - /bees/docker_volume/nginx/conf.d:/etc/nginx/conf.d - /bees/docker_volume/nginx/file:/home/fileimage:镜像名称ports:容器的端口和宿主机的端口的映射services:文中三个service,在各自容器启动后就会自动生成别名,例如:在springboot中访问数据库,只需要通过“beesMysql:3306”就能访问。depends_on:会设置被依赖的容器启动之后,才会启动自己。例如:mysql数据库容器启动后,再启动springboot接口的容器。volumes:挂载卷,一些需要长久保存的文件,可通过宿主机中的目录,挂载到容器中,否则容器重启后会丢失。例如:数据库的数据文件;nginx的配置文件和文件服务器目录。其他自动部署为了提高开发效率,简单写了一个自动部署的脚本,直接贴脚本了:#!/bin/bashv_springboot_jar=find /bees/devops/upload/ -name "*.jar"echo “找到jar:"$v_springboot_jarv_angular_zip=find /bees/devops/upload/ -name "dist.zip"echo “找到dist:"$v_angular_zipcd /bees/conf/docker-compose downecho “关闭容器"docker rmi -f $(docker images | grep “bees-springboot” | awk ‘{print $1}’)docker rmi -f $(docker images | grep “bees-nginx-angular” | awk ‘{print $1}’)echo “删除镜像"cd /bees/devops/dockerfiles/springboot/rm -f *.jarcp $v_springboot_jar ./bees-0.0.1-SNAPSHOT.jardocker build -t bees-springboot .echo “生成springboot镜像"cd /bees/devops/dockerfiles/angular/rm -rf dist/cp $v_angular_zip ./dist.zipunzip dist.ziprm -f dist.zipdocker build -t bees-nginx-angular .echo “生成angular镜像"cd /bees/conf/docker-compose up -decho “启动容器"docker ps遇到的坑一开始在docker-compose.yml文件中写services时,每个service不是驼峰式命名,而是下划线连接,例如:bees_springboot、bees_mysql、bees_nginx_angular 。在springboot中访问数据库的别名可以,但是在nginx中,反向代理springboot接口地址时死活代理不了 bees_springboot的别名。能在bees_nginx_angular的容器中ping通bees_springboot,但是代理不了bees_springboot地址的接口,通过curl -v 查看原因,是丢失了host。最后发现,nginx默认request的header中包含“_”下划线时,会自动忽略掉。我因此把docker-compose.yml中service名称,从下划线命名都改成了驼峰式。当然也可以通过在nginx里的nginx.conf配置文件中的http部分中添加如下配置解决:underscores_in_headers on; ...

January 6, 2019 · 2 min · jiezi

Angular中ngTemplateOutlet和指令结合使用

在kendo的Grid组件中,可以看到有大量类似kendoGridCellTemplate这样的指令。 那么这些指令是怎样实现的。 <kendo-grid …> <kendo-grid-column …> <ng-template kendoGridCellTemplate let-dataItem let-i=“index”> …. </ng-template> </kendo-grid-column> </kendo-grid>下面是实现 在HelloComponent组件中使用 helloTemplate指令的代码@Component({ selector: ‘hs-hello’, template: &lt;div&gt; ... &lt;ng-container *ngTemplateOutlet="ct; context:ctx"&gt;&lt;/ng-container&gt; &lt;/div&gt; })export class HelloComponent { ctx = { $implicit: ‘aaa’, hello: ‘bbb’ }; @ContentChild(‘cellTemplate’) ct: TemplateRef<any>; constructor() {}} @Directive({ selector: ‘[helloTemplate]’,})export class HelloTemplateDirective implements OnInit { constructor( private template: TemplateRef<any>, @Host() @Optional() public hc: HelloComponent ) { } ngOnInit() { this.hc.headerTemplate = this.template; }} <hs-hello> <ng-template helloTemplate let-dataItem let-hello=“hello”> <p>{{dataItem}} – {{hello}}</p> </ng-template> </hs-hello> ...

December 28, 2018 · 1 min · jiezi

angular2 使用swiper

第一步:npm install swiper –save第二步:下载swiper ts支持(ts声明文件:http://microsoft.github.io/Ty…)npm install @types/swiper –savets文件里:import swiper from “Swiper”;swiperSlide:any swiperw() { let ss = new swiper(’.swiper-container’, { loop: true, // 循环模式选项 effect : ‘flip’, // effect: ‘cube’ }) this.swiperSlide = ss } goto(e) { this.swiperSlide.slideToLoop(e, 1000, false);//切换到第一个slide,速度为1秒 }html<div class=“swiper-container swiper-no-swiping”> <div class=“swiper-wrapper”> <div class=“swiper-slide” style=“background:red” (click)=“goto(1)">Slide 1</div> <div class=“swiper-slide” style=“background:blue”>Slide 2</div> </div></div><button (click)=“goto(1)” >1111111</button><button (click)=“goto(0)” >222222</button>css.swiper-container { width: 600px; height: 300px;}

December 19, 2018 · 1 min · jiezi

angular4父组件向子组件传值,子组件向父组件传值的方法

父组件向子组件传值 @Input文件目录父组件:father.template.html<h1>父组件</h1><cmt-child [data]=‘data’></cmt-child>father.component.tsimport { Component, OnInit } from ‘@angular/core’;@Component({ selector: ‘cmt-father’, templateUrl: ‘./father.template.html’})export class FatherComponent implements OnInit { data: any = ‘我是传往子组件的值’ ngOnInit() { } ngOnChanges() { }}子组件:(使用@Input修饰器去接收)childcomponent.tsimport { Component, OnInit, Input } from ‘@angular/core’;@Component({ selector: ‘cmt-child’, templateUrl: ‘./child.template.html’})export class ChildComponent implements OnInit { @Input() data: any;//接收父组件的值 ngOnInit() { console.log(this.data) } ngOnChanges() { console.log(this.data) }}这样就把值从父组件传到了子组件!子组件向父组件传值(子组件通过方法借助修饰器@output传值给父组件)子组件childcomponent.tsimport { Component, OnInit, Input, Output, EventEmitter } from ‘@angular/core’;@Component({ selector: ‘cmt-child’, templateUrl: ‘./child.template.html’})export class ChildComponent implements OnInit { @Output(‘checked’) checkedBack = new EventEmitter<any>(); id:any =“我是传给父组件的值” ngOnInit() { } ngOnChanges() { } checkedCallback() { this.checkedBack.emit(this.id); }}child.template.html.html<h1>子组件</h1><button (click)=‘checkedCallback()’>点击传值给父组件</button>父组件father.template.html<h1>父组件</h1><cmt-child (checked)=“checkedBack($event)"></cmt-child>father.component.tsimport { Component, OnInit } from ‘@angular/core’;@Component({ selector: ‘cmt-father’, templateUrl: ‘./father.template.html’})export class FatherComponent implements OnInit { ngOnInit() { } ngOnChanges() { } checkedBack(event) { console.log(event) }}这样子组件通过点击就把值传递给了父组件! ...

December 19, 2018 · 1 min · jiezi