前言:
本文的目的是通过一步步实现一个旅游清单项目,让大家快速入门Angular8以及百度地图API。我们将收获:
- Angular8基本用法,架构
- 使用百度地图API实现自己的地图应用
- 解决调用百度地图API时的跨域问题
- 对localStorage进行基础封装,进行数据持久化
- material UI的使用
项目简介
《旅游清单》项目的背景主要是为了让笔者更好的掌握angular8,因为之前做的项目主要是使用vue和react,作为一名合格的coder,必须博学而专一,也是因为笔者早年大学时期想要做的一个想法,可以有这样一个程序,记录自己的路途,见闻和感想。项目的首页展示的是已去过的旅游地点和路线,地图路线是通过调用百度地图api实现的,当然提供这样的api很多,大家可以根据自己的喜好去使用。其次我们可以在首页添加未来的旅游规划和预算,方便后面使用。我的大陆页面主要展示的你去过的和即将要去的路线,可以进行相关操作。
项目地址:
《旅游清单》项目架构
其中components为组件存放区,config为公共配置区,home/newMap为页面区,mock为模拟数据区,service为应用所需服务区,如http服务,存储服务,custom.modules文件为第三方组件安置区。
效果预览
添加旅游规划之后:
1.开始
- 首先假定你已经安装了node,没有安装请移步node官网进行安装。 安装脚手架:
npm install -g @angular/cli
- 创建工作空间和初始应用
ng new my-app
- 安装material UI
npm install @angular/material @angular/cdk @angular/animations
- 根据以上架构,建立对应目录文件
- 启动服务
cd my-app ng serve --open
这里cli会自动打开浏览器4200端口,并出现默认页面。
2.引入百度地图API
官方会提供不同地图功能的api地址,以下是该项目使用的地址:
<script type="text/javascript" src="http://api.map.baidu.com/api?v=2.0&ak=你的ak"></script> <script type="text/javascript" src="http://api.map.baidu.com/library/CurveLine/1.5/src/CurveLine.min.js"></script>
如果没有ak,请移步百度地图官网申请,步骤也很简单。
至此,项目的基本准备工作已经做好了,下面让我们先聊一聊angular。
3.angular基本语法和架构
1.基本语法
和vue类似,ng的基本语法如下:
- 模版语法
- 数据指令
- 属性绑定
- 事件绑定
案例如下:
<h1>{{title}}</h1> <h2 [title]="mytitle">My favorite hero is: {{ mytitle }}</h2> <p>Heroes:</p> <ul> <li *ngFor="let item of list"> {{ hero }} </li> </ul> <button (click)="onclickBtn">单机</button>
以上代码可以知道,我们用{{}}插入数据,用[属性名]绑定属性,*ngFor为循环指令,类似的*ngIf为条件判断,事件绑定用(click),我们看看组件的ts文件对应的写法:
import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './index.html', styleUrls: ['./index.scss'] }) export class AppComponent { mytitle = 'Xujiang'; list = [ 'xujaing', 'zhangjiang', 'xakeng' ]; onclickBtn() { console.log('你好') } }
2.基本架构
采用angular官方提供的架构图:
我们知道,一个完整的angular应该包括:
- 模块
Angular 定义了 NgModule,NgModule 为一个组件集声明了编译的上下文环境,它专注于某个应用领域、某个工作流或一组紧密相关的能力,每个 Angular 应用都有一个根模块,通常命名为 AppModule。根模块提供了用来启动应用的引导机制。 一个应用通常会包含很多功能模块。
- 组件
每个 Angular 应用都至少有一个组件,也就是根组件,它会把组件树和页面中的 DOM 连接起来。 每个组件都会定义一个类,其中包含应用的数据和逻辑,并与一个 HTML 模板相关联,该模板定义了一个供目标环境下显示的视图 比如:
import { Component, OnInit } from '@angular/core'; import { LocationService } from '../../service/list'; @Component({ selector: 'app-bar', templateUrl: './index.html', styleUrls: ['./index.scss'] }) export class AppBar implements OnInit { items; constructor(private locationService: LocationService) { this.items = this.locationService.getItems(); } ngOnInit() { } }
- 服务与依赖注入
对于与特定视图无关并希望跨组件共享的数据或逻辑,可以创建服务类。 服务类的定义通常紧跟在 “@Injectable()” 装饰器之后。该装饰器提供的元数据可以让你的服务作为依赖被注入到客户组件中。例如:
``` import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class Storage {} ```
- 路由
Angular 的 Router 模块提供了一个服务,它可以让你定义在应用的各个不同状态和视图层次结构之间导航时要使用的路径。如下:
import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { HomeComponent } from './home'; import { NewMapComponent } from './newMap'; // 路由不能以‘/’开始 const routes: Routes = [ { path: '', component: HomeComponent }, { path: 'newMap', component: NewMapComponent }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
4. 百度地图api及跨域问题解决
我们进入百度地图官网后,去控制台创建一个应用,此时会生成对应的应用ak,如下:
本地调试时将referer写成*即可,但是我们用ng的http或者fetch去请求api接口时仍会出现跨域,在网上搜集了各种资料,都没有达到效果,我们这里使用jquery的$.getScript(url),结合jsonp回调,即可解决该问题。
所以先安装以下jquery:
npm install jquery
解决方案如下:
1.封装http服务:
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { AK, BASE_URL } from '../config'; import * as $ from "jquery"; @Injectable({ providedIn: 'root' }) export class Http { constructor( private http: HttpClient ) { } params(data = {}) { let obj = {...data, ak: AK, output: 'json' }; let paramsStr = '?'; for(let v in obj) { paramsStr += `${v}=${obj[v]}&` }; return paramsStr.substr(0, paramsStr.length -1); } get(url, params) { return this.http.get(`${BASE_URL}${url}${this.params(params)}`) } getCors(url, params) { return new Promise((resolve, reject) => { $.getScript(`${BASE_URL}${url}${this.params(params)}`, (res, status) => { if(status === 'success') { resolve(status) } else { reject(status) } }); }) } }
定义jsonp回调和接收数据变量:
let locationData = null; window['cb'] = function(data) { locationData = data && data.results; }
使用:
async searchLocation(v) { return await this.http.getCors('/place/v2/search', { region:v, query: v, callback: 'cb' }); }
至此,应用几个主要的突破点已经解决好了,接下来我们来开发项目的核心页面和组件。
- 按需引入materialUI组件:
// custom.module.ts import { NgModule } from '@angular/core'; import { MatButtonModule, MatTooltipModule, MatBadgeModule } from '@angular/material'; @NgModule({ imports: [MatButtonModule, MatTooltipModule, MatBadgeModule], exports: [MatButtonModule, MatTooltipModule, MatBadgeModule], }) export class CustomMaterialModule { }
custom.module.ts为根目录下的文件,这里我用来做存储第三方组件的位置,定义好之后在app.module.ts中引入:
// material组件库 import { CustomMaterialModule } from './custom.module'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; @NgModule({ declarations: [ AppComponent, ], imports: [ BrowserModule, BrowserAnimationsModule, ReactiveFormsModule, AppRoutingModule, HttpClientModule, CustomMaterialModule, ], providers: [], bootstrap: [AppComponent] })
BrowserAnimationsModule主要是angular为组件提供一些动效支持的模块。 接下来我们看看入口页面:
// app.component.html <div class="app-wrap"> <app-bar></app-bar> <main class="main"> <router-outlet></router-outlet> </main> <app-footer></app-footer> </div>
app-bar,app-footer为我们定义好的页头页尾组件,如下:
// app-bar.html <nav class="nav-bar"> <div class="logo">旅游导图+</div> <a [routerLink]="['/']">首页</a> <a [routerLink]="['/newMap']"><span [matBadge]="items.length" matBadgeOverlap="false" matBadgeColor="warn">我的大陆</span></a> </nav> // app-bar.ts import { Component, OnInit } from '@angular/core'; import { LocationService } from '../../service/list'; @Component({ selector: 'app-bar', templateUrl: './index.html', styleUrls: ['./index.scss'] }) export class AppBar implements OnInit { items; constructor(private locationService: LocationService) { this.items = this.locationService.getItems(); } ngOnInit() { } } // footer.html <footer class="footer">@开发者:{{ name }}</footer> // footer.ts import { Component, OnInit } from '@angular/core'; @Component({ selector: 'app-footer', templateUrl: './index.html', styleUrls: ['./index.scss'] }) export class AppFooter implements OnInit { name = '猪先森'; constructor() { } ngOnInit() { } }
scss在这里就不引入了,因为比较简单,如果需要大家可以去我的github上现在完整项目基于angular8和百度地图API开发旅游清单项目来学习。
其次,页面头部组件用到了LocationService,我们来看看这个service:
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Storage } from './storage'; @Injectable({ providedIn: 'root' }) export class LocationService { items = [ { name: '北京', desc: '北京好,风景真的不错!', price: '2000', date: '2018-12-29', hasDone: true, location: { lat: 39.910924, lng: 116.413387 } }, { name: '苏州', desc: '苏州好,去了还想去,不错!', price: '2000', hasDone: true, date: '2018-12-29', location: { lat: 31.303565, lng: 120.592412 } }, { name: '上海', desc: '上海好,去了还想去,不错!', price: '2000', hasDone: true, date: '2018-12-29', location: { lat: 31.235929, lng: 121.48054 } }, { name: '武汉', desc: '武汉好,去了还想去,不错!', price: '2000', hasDone: true, date: '2018-12-29', location: { lat: 30.598467, lng: 114.311586 } } ]; constructor( private http: HttpClient, private store: Storage ) { if(store.get('list')) { this.items = store.get('list'); } } addToList(location) { this.items.push(location); this.store.set('list', this.items); } getItems() { return this.items; } clearList() { this.items = []; return this.items; } }
该服务主要提供访问列表,添加旅游清单,清除清单的功能,我们利用@Injectable({ providedIn: 'root' })将服务注入根组件以便共享服务。其次我们使用自己封装的Storage服务来进行持久化数据存储,storage服务如下:
import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class Storage { get(k) { return JSON.parse(localStorage.getItem(k)) } set(k, v) { localStorage.setItem(k, JSON.stringify(v)) } remove(k) { localStorage.removeItem(k) } }
实现起来比较简单,这里就不多说明了。 接下来我们看看首页核心功能的实现:
- 百度地图初始化路线图:
代码如下:
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Input } from '@angular/core'; import { Http } from '../service/http'; import { FormBuilder } from '@angular/forms'; import { LocationService } from '../service/list'; @Component({ selector: 'app-home', templateUrl: './index.html', styleUrls: ['./index.scss'] }) export class HomeComponent implements OnInit { hasDoneList; constructor( private locationService: LocationService, private http: Http, ) { this.hasDoneList = this.locationService.getItems(); } ngOnInit() { let map = new BMap.Map("js_hover_map"); // 创建地图实例 map.centerAndZoom(new BMap.Point(118.454, 32.955), 6); map.enableScrollWheelZoom(); let hasDoneLocations = []; this.locationService.getItems().forEach(item => { item.hasDone && hasDoneLocations.push(new BMap.Point(item.location.lng,item.location.lat)) }) let curve = new BMapLib.CurveLine(hasDoneLocations, {strokeColor:"red", strokeWeight:4, strokeOpacity:0.5}); //创建弧线对象 map.addOverlay(curve); //添加到地图中 curve.enableEditing(); //开启编辑功能 } }
我们在ngOninit生命周期里,初始化地图数据,根据前面我们定义的list server,把hasDone为true的数据过滤出来,显示在地图上。 接下来我们实现添加旅游清单的功能。 2. 添加旅游清单
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Input } from '@angular/core'; import { Http } from '../service/http'; import { FormBuilder } from '@angular/forms'; import { LocationService } from '../service/list'; // 获取跨域数据的回调 let locationData = null; window['cb'] = function(data) { locationData = data && data.results; } @Component({ selector: 'app-home', templateUrl: './index.html', styleUrls: ['./index.scss'] }) export class HomeComponent implements OnInit { hasDoneList; checkoutForm; constructor( private formBuilder: FormBuilder, private locationService: LocationService, private http: Http, ) { this.hasDoneList = this.locationService.getItems(); this.checkoutForm = this.formBuilder.group({ name: '', price: '', date: '' }); } ngOnInit() { ... } async searchLocation(v) { return await this.http.getCors('/place/v2/search', { region:v, query: v, callback: 'cb' }); } onSubmit(customerData) { if(customerData.name) { this.searchLocation(customerData.name).then(data => { this.locationService.addToList({...customerData, location: locationData[0].location, hasDone: false}) }); } else { alert('请填写旅游地点!'); return } this.checkoutForm.reset(); } onReset() { this.checkoutForm.reset(); } } // html <div class="home-wrap"> <section class="content"> <div class="has-done"> <div class="title">我已去过:</div> <div class="visit-list"> <button *ngFor="let item of hasDoneList" class="has-btn" mat-raised-button [matTooltip]="item.desc" aria-label="按钮当聚焦或者经过时展示工具提示框"> {{ item.name }} </button> </div> </div> <div class="has-done"> <div class="title">未来规划:</div> <div class="future-list"> <form [formGroup]="checkoutForm"> <div class="form-control"> <label>地点:</label> <input type="text" formControlName="name"> </div> <div class="form-control"> <label>预算:</label> <input type="number" formControlName="price"> </div> <div class="form-control"> <label>日期:</label> <input type="date" formControlName="date"> </div> <div class="form-control"> <button mat-raised-button color="primary" class="submit-btn" type="submit" (click)="onSubmit(checkoutForm.value)">提交</button> <button mat-raised-button color="accent" class="reset-btn" (click)="onReset()">重置</button> </div> </form> </div> </div> </section> <section class="map-wrap" id="js_hover_map"></section> </div>
我们使用angular提供的FormBuilder来处理表单数据,这里需要注意,我们在提交表单的时候,需要先调用百度地图的api去生成经纬度数据,之后一起添加到清单,这样做的目的是要想画路线图,我们需要给百度地图api提供经纬度数据。还有一点,由于访问涉及到跨域,我们要定义jsonp的回调,来拿到数据,如下:
let locationData = null; window['cb'] = function(data) { locationData = data && data.results; }
locationService的addToList方法会将数据添加到清单,并存储到storage中。 如果想了解完整代码,欢迎在我的github上查看。
接下来看看我的大陆页面,其实涉及的难点不是很多,主要是根据hasDone为true或false去显示不同的样式。
代码如下:
// html <div class="detail"> <h1>新大陆</h1> <div class="position-list"> <div class="position-item" *ngFor="let item of list"> <span class="is-new" *ngIf="!item.hasDone">新</span> <span class="title">{{item.name}}</span> <span class="date">{{item.date}}</span> <span class="desc">{{item.desc}}</span> <span class="price">预算:{{item.price}}</span> </div> </div> </div> // ts import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Input } from '@angular/core'; import { LocationService } from '../service/list'; @Component({ selector: 'app-new-map', templateUrl: './index.html', styleUrls: ['./index.scss'] }) export class NewMapComponent implements OnInit { @Input() product; // 指定product值从父组件中传递 list; constructor( private route: ActivatedRoute, private locationService: LocationService ) { this.list = locationService.getItems(); } editItem(item) { } ngOnInit() { this.route.paramMap.subscribe(params => { // this.product = products[+params.get('productId')]; }); } }
总结
该项目是基于angular8的实战入门项目,涉及到部分高级技巧以及百度地图,jsonp跨域的知识,大家有不懂的可以相互交流,我也会定期分享一些企业中常用的核心技术。
未完善的部分: 添加清单时,如果添了不符合规范的地址或者百度地图查不到的地址,因该出现错误提示,这块会在后期优化。
好啦,文章篇幅比较多,大致项目基本完成,如果想查看实际项目效果,请移步基于angular8和百度地图API开发旅游清单项目。