使用Angular8和百度地圖api開發《旅遊清單》

徐小夕發表於2019-06-23

使用Angular8和百度地圖api開發《旅遊清單》

前言:

本文的目的是通過一步步實現一個旅遊清單專案,讓大家快速入門Angular8以及百度地圖API。我們將收穫:

    1. Angular8基本用法,架構
    1. 使用百度地圖API實現自己的地圖應用
    1. 解決呼叫百度地圖API時的跨域問題
    1. 對localStorage進行基礎封裝,進行資料持久化
    1. material UI的使用

專案簡介

《旅遊清單》專案的背景主要是為了讓筆者更好的掌握angular8,因為之前做的專案主要是使用vue和react,作為一名合格的coder,必須博學而專一,也是因為筆者早年大學時期想要做的一個想法,可以有這樣一個程式,記錄自己的路途,見聞和感想。專案的首頁展示的是已去過的旅遊地點和路線,地圖路線是通過呼叫百度地圖api實現的,當然提供這樣的api很多,大家可以根據自己的喜好去使用。其次我們可以在首頁新增未來的旅遊規劃和預算,方便後面使用。我的大陸頁面主要展示的你去過的和即將要去的路線,可以進行相關操作。

專案地址:

基於angular8和百度地圖API開發旅遊清單專案

《旅遊清單》專案架構

使用Angular8和百度地圖api開發《旅遊清單》

其中components為元件存放區,config為公共配置區,home/newMap為頁面區,mock為模擬資料區,service為應用所需服務區,如http服務,儲存服務,custom.modules檔案為第三方元件安置區。

效果預覽

使用Angular8和百度地圖api開發《旅遊清單》

使用Angular8和百度地圖api開發《旅遊清單》
新增旅遊規劃之後:

使用Angular8和百度地圖api開發《旅遊清單》

使用Angular8和百度地圖api開發《旅遊清單》

1.開始

  1. 首先假定你已經安裝了node,沒有安裝請移步node官網進行安裝。 安裝腳手架:
npm install -g @angular/cli
複製程式碼
  1. 建立工作空間和初始應用
ng new my-app
複製程式碼
  1. 安裝material UI
npm install @angular/material @angular/cdk @angular/animations
複製程式碼
  1. 根據以上架構,建立對應目錄檔案
  2. 啟動服務
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的基本語法如下:

  1. 模版語法
  2. 資料指令
  3. 屬性繫結
  4. 事件繫結

案例如下:

<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官方提供的架構圖:

使用Angular8和百度地圖api開發《旅遊清單》
我們知道,一個完整的angular應該包括:

  1. 模組

    Angular 定義了 NgModule,NgModule 為一個元件集宣告瞭編譯的上下文環境,它專注於某個應用領域、某個工作流或一組緊密相關的能力,每個 Angular 應用都有一個根模組,通常命名為 AppModule。根模組提供了用來啟動應用的引導機制。 一個應用通常會包含很多功能模組。

  2. 元件

    每個 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() {

    }
}
複製程式碼
  1. 服務與依賴注入

    對於與特定檢視無關並希望跨元件共享的資料或邏輯,可以建立服務類。 服務類的定義通常緊跟在 “@Injectable()” 裝飾器之後。該裝飾器提供的後設資料可以讓你的服務作為依賴被注入到客戶元件中。例如:

```
import { Injectable } from '@angular/core';

@Injectable({
    providedIn: 'root'
  })
export class Storage {}
```
複製程式碼
  1. 路由

    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,如下:

使用Angular8和百度地圖api開發《旅遊清單》
本地除錯時將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' });
}
複製程式碼

至此,應用幾個主要的突破點已經解決好了,接下來我們來開發專案的核心頁面和元件。

  1. 按需引入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)
    }
}
複製程式碼

實現起來比較簡單,這裡就不多說明了。 接下來我們看看首頁核心功能的實現:

  1. 百度地圖初始化路線圖:

使用Angular8和百度地圖api開發《旅遊清單》
程式碼如下:

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. 新增旅遊清單

使用Angular8和百度地圖api開發《旅遊清單》
表單空間我們都用h5原生控制元件,我們使用angular提供的form模組,具體程式碼如下:

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去顯示不同的樣式。

使用Angular8和百度地圖api開發《旅遊清單》

程式碼如下:

// 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開發旅遊清單專案

最後,更多技術優質文章,技術資料,歡迎關注《趣談前端公眾號》:

使用Angular8和百度地圖api開發《旅遊清單》

歡迎加入前端技術群,一起探討前端的魅力:

使用Angular8和百度地圖api開發《旅遊清單》

相關文章