旅遊清單一步搭建,Angular助力你的踏春計劃

maosushi發表於2022-03-23

春天的腳步愈發臨近,相信很多小夥伴已經開始規劃自己的踏春計劃了,無論是欣賞名勝古蹟,還是走訪風土人文,你都需要提前準備一份旅遊清單!有了這款Angular旅遊計劃應用,從地點到預算,它都能幫助你建立自己的踏春足跡!

踏春正當時,馬上跟隨本文,從雲平臺建立應用模版,本地編寫《旅遊清單》專案,到一鍵部署上線開始,一步一步建立自己的踏春計劃,讓一場說走就走的旅行,從當下發生吧!

一 、通過雲開發平臺快速建立初始化應用

1.建立相關應用模版請參考連結: https://developer.aliyun.com/article/874644?spm=a2c6h.12873581.0.dArticle874644.47cf53e8jQjrWo

2.完成建立後就可以在github中檢視到新增的Angular 倉庫

二 、 本地編寫《旅遊清單》專案

1.將應用模版克隆到本地

• 首先假定你已經安裝了Git、node,沒有安裝請移步node官網進行安裝。克隆專案:

git clone + 專案地址

• 進入專案檔案

cd Angular

• 切換到feature/1.0.0 分支上

git checkout feature/1.0.0

• 使用一下命令全域性安裝 Angular CLI :

npm install -g @angular/cli

• 安裝依賴包

npm install

• 啟動服務

ng serve

這裡開啟瀏覽器4200埠,並出現預設頁面。

2.架構與效果預覽

• 《旅遊清單》專案架構

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

新增旅遊規劃之後:

3.專案編寫

• 引入地圖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>

至此,專案的基本準備工作已經做好了,下面讓我們先聊一聊angular。

4.angular基本語法和架構

• 基本語法
和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('你好')
	}
}

• 基本架構
採用angular官方提供的架構圖:

我們知道,一個完整的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() {
			}
	}

• 服務於依賴注入
對於與特定檢視無關並希望跨元件共享的資料或邏輯,可以建立服務類。 服務類的定義通常緊跟在 “@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 { }

• 百度地圖api及跨域問題解決
我們進入百度地圖官網後,去控制檯建立一個應用,此時會生成對應的應用ak,如下:

本地除錯時將referer寫成*即可,但是我們用ng的http或者fetch去請求api介面時仍會出現跨域,在網上搜集了各種資料,都沒有達到效果,我們這裡使用jquery的$.getScript(url),結合jsonp回撥,即可解決該問題。
所以先安裝以下jquery:

npm install jquery

解決方案如下:
• 封裝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() {
		}
}

其次,頁面頭部元件用到了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的資料過濾出來,顯示在地圖上。 接下來我們實現新增旅遊清單的功能。

  1. 新增旅遊清單
    表單空間我們都用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去顯示不同的樣式。

程式碼如下:

// 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')];
					});
		}
}

大致專案基本完成,如果想檢視實際專案效果,可參考原專案作者的程式碼:
https://github.com/MrXujiang/angularDemo

三 、 雲端一鍵部署上線應用

1.上傳程式碼

git add . 
git commit -m '新增你的註釋'
git push

2.在日常環境部署

一鍵進行應用部署。在應用詳情頁面點選日常環境的「部署」按鈕進行一鍵部署,部署狀態變成綠色已部署以後可以點選訪問部署網站檢視效果。

3.配置自定義域名線上上環境上線

• 配置線上環境自定義域名。在功能開發驗證完成後要線上上環境進行部署,線上上環境的「部署配置」-「自定義域名」中填寫自己的域名。例如我們新增一個二級域名 company.workbench.fun 來繫結我們部署的前端應用。然後複製自定義域名下方的API閘道器地址對新增的二級域名進行CNAME配置。

• 配置CNAME地址。複製好 API閘道器域名地址後,來到你自己的域名管理平臺(此示例中的域名管理是阿里雲的域名管理控制檯,請去自己的域名控制檯操作)。新增記錄的「記錄型別」選擇「CNAME」,在「主機記錄」中輸入你要建立的二級域名,這裡我們輸入「company」,在「記錄值」中貼上我們之前複製的 API閘道器域名地址,「TTL」保留預設值或者設定一個你認為合適的值即可。

• 線上上環境部署上線。回到雲開發平臺的應用詳情頁面,按照部署的操作,點選線上環境的「部署按鈕」,部署完成以後就在你自定義的域名進行了上線。CNAME 生效之後,我們輸入 company.workbench.fun(示例網址) 可以開啟部署的頁面。至此,如何部署一個應用到線上環境,如何繫結自己的域名來訪問一個線上的應用就完成了,趕緊部署自己的應用到線上環境,用自己的域名玩起來吧 ;)

一鍵建立angular應用模版連結 :https://workbench.aliyun.com/application/front/create?fromConfig=24&fromRepo=sol_github_24

參考文獻:https://juejin.cn/post/6844903873212055560#heading-5

相關文章