翻译—Build A Real World Beautiful
不再有丑陋的辅导项目!也不再有虚构的品牌案例
今天我要帮你了解怎么样去搭建一个真实世界中漂亮的天气预报的app,从零开始准备:设计到开发再到部署使用Adobe XD,Angular6,Firebase!
为什么这么说呢?
我厌倦了开发人员使用红色背景和丑陋的UI CSS来教人们开发准备不足的东西,当我在Behance和Dribble上面看到上千个漂亮但无法使用的设计时感觉很心烦,那些设计者绝不会花时间补充额外的步骤(构建和部署apps以使人们从设计中更加受益)让这些设计变的有用。
1个人 + 1个完整的项目 + 1个博客 + 可能很快还有一个视频教程 :)
⚡️充满了钟声和哨声 😮 🔔
· 基于最新的最好的angular v6版本
· Firebase验证和Firestore(使用AngularFire Lite🔥)
· 服务端渲染(SEO)
· Lighthouse PWA 评分 100/100
· 推送通知
· CSS模式:Grid Layout&Flex Box
· 手机又好,响应全面
· 2个模式:夜间模式&白天模式
· Jasmine&Karma单元测试
· Protractor的End-2-End测试
· 漂亮的简约风格设计
· 注意:本篇是一系列教你怎么样构建这个app的文章的其中之一,是以上列表中的之一。如果你想要我在更新教程的时候通知你,请登录我的新闻通讯。signup to my newsletter here. 💌
step1:设计
我已经设计了最新版本(Adobe XD 2018)的天气预报app。你可以下载
)它。你将会看到不同层如何叠加而形成最终的设计。
A.商标
我想要通过设计来反映出它的核心价值是,你可以看到它提倡简约、简洁、干净和易于使用。
· Colors
2中饱和的原色赋予它清新的现代感。
· 字体
这里没有自定义字体只有'Sans Serif'字体,我们不必加载CDN的任何字体以获得最佳性能。
· logo
logo不是火箭科学!但logo的设计者也会尝试努力让你相信它的理念!实际上他们大都是为了他们创作的仪式和制作的电影图标能获得报酬来催眠客户。
考虑一下Nike的logo(复选标记形状)的设计为35,设计者就是有100万$的人了!)
无论如何我这里设计的简单的M形状的图标(使用2张相交的卡片和商标的原腭着色)简约、高效很可能值0$😄
B.UI/UX
这个app主要使用带有柔和阴影的卡片,因为它是一些浮动的纸张。仅仅预先显示最重要的信息避免界面看起来混乱,流畅的动画在UX部分可以让我们在用户体验上加分。
· 白天模式(默认)
· 小图标
用户必须可以对天气状况一目了然,所以我设计了自定义的图标包,以便与整个网站设计相结合,下面是图标包的最初版本。
· 插图
我们希望尽可能减少用户去猜测天气预报的可能性,同时也想使得每个东西都用令人愉悦的视觉效果来填充空白空间。
我设计的插图应该可以帮助用户不用专门查看就可以了解到自己在哪个城市(因为现在的人是很懒惰的!)
对于城市的插图,我倾向于采用带有饱和调色板的渐变浮华设计风格,用于视觉丰富的城市细节页面。
我知道你认为我去为每个城市设计了然的插图是多么疯狂。的确,这是一项疯狂的工作,因为一共195个城市而我现在只设计了4个插图,为还会在几年中陆续设计191张😅。
1.Tunisia
2.Qatar
3.Japan
4.France
完整的插图可以点解这里(https://www.behance.net/gallery/62966175/Minimus-Weather-App-Illustrations)来查看。
Step2:开发
Angular大多数的指导教程跳过了早期的一些步骤,如果你了解了一切,他们也仅仅会展示结果令人印象深刻而已。
但是我打算尽我最大的努力让每个人都可以按照本教程可以实现这个app,而教程也不会特别长。让我们安装nodejs和angualr客户端,它将为我们的Angular6提供基本支持。
从官方网站nodejs,然后在你的终端使用下面的命令下载Angular客户端和typescript。
npm i -g typescript
npm i -g @angular/cli
在运行上述命令后会使用Angular客户端生成app,不要忘记添加路径标记,它为应用程序分页和路由创建了一个良好的起点。执行下面。
ng new Minimus --routing
一旦客户端完成后会生成你的工程文件并下载好NPM需要的所有的依赖,我们将启动开发服务器并使用以下命令在浏览器中打开我们的应用程序(-o 表示自动打开一个新的浏览器选项卡指向应用的正确网址)。
ng serve -o
A.模版和风格
但是在开始之前,我想确定你不要仅仅复制粘贴,读一下代码然后打开你的编辑器或浏览器然后记下每一步。因为这是你的学习方法。我自己键入了所有步骤搭建这个app,你也应该这样做,以便使你了解从头到尾做的每一件事。
现在回到构建工程上,我们只是完成了基本的app环境搭建,然后我们会开始写HTML和CSS。所以在你最喜欢的text编辑器上打开你的项目,让我们马上开始!
· App 组件
我们打算使用root组件app.component
作为navbar组件,我们将根据用户是否登录来显示或隐藏它。(我们在Part2部分使用Sires的Angularfire Lite来实现登录验证)。
在这里我想用Angular库中的一些组件,但我最后还是决定尽可能避免任何第三方库来搭建app,除非像Angularfire Lite一样重要我才会使用。
首先我们编辑app.component.html
删掉所有的自动生成的HTML代码,把下面的代码填进去:
<!-- Slide Menu-->
<aside class="side-menu__container" [ngClass]="{'side-menu__container-active': showMenu}" (click)="toggleMenu()">
<nav class="slide-menu" [ngClass]="{'slide-menu-active': showMenu}" (click)="$event.stopImmediatePropagation();">
<section class="menu-header">
<span class="greeting__text">Welcome Back</span>
<div class="profile-image__container">
<img src="https://avatars3.githubusercontent.com/u/5658460?s=460&v=4" alt="profile-image"
class="profile__image">
</div>
<div class="account-details">
<span class="name__text">Hamed Baatour</span>
<span class="email__text">hamedbaatour@gmail.com</span>
</div>
</section>
<section class="menu-body">
</section>
<section class="menu-footer">
</section>
</nav>
</aside>
<div class="root__container" >
<header [ngClass]="{'main__header-dark': darkModeActive}" class="main__header">
<div class="left__section">
<svg (click)="toggleMenu()" class="hamburger__icon" id="Menu_Burger_Icon">
<!-- hamburger icon svg code goes here-->
</svg>
<svg class="logo__icon">
<!-- logo svg code goes here-->
</svg>
</div>
<h3 class="date__text">Today</h3>
<div class="mode-toggle__container">
<span class="mode-toggle__text">Light</span>
<label class="toggle-button__container">
<input (click)="modeToggleSwitch()" type="checkbox" class="mode-toggle__input" />
<span [ngClass]="{'mode-toggle__bg-checked': darkModeActive}" class="mode-toggle__bg"></span>
<span [ngClass]="{'mode-toggle__circle-checked': darkModeActive}" class="mode-toggle__circle"></span>
</label>
<span class="mode-toggle__text">Dark</span>
</div>
</header>
<!-- Main Content -->
<!--<router-outlet></router-outlet>-->
<main class="main__container">
<div class="main-container__bg" [ngClass]="{'main-container__bg-dark': darkModeActive}"></div>
<router-outlet></router-outlet>
</main>
<!-- Footer -->
<footer class="main__footer">
<small class="copyright__text">Copyright © 2018 Minimus</small>
</footer>
</div>
额外提示(可选)
如果你想要使用Emmet(编辑插件)来更快的键入HTML,你可以参照这篇 plugin cheat sheet
SVG 小图标
为了获得我制作的SVG小图标和logo列表,你可以在我的Github文件上面找到它们:
· * hamburger icon
· * logo
· * add icon light
· * add card illustration light
—设置root组件的样式
是时候为我们的navbar加一下css样式了,快速开始用一下css代码。当你把css从头写到尾后查看实现的结果,不需要和我的一模一样。
希望不要copy我的,因为我相信每个人都是一名艺术家。
.root__container {
width: 100vw;
height: 100vh;
display: grid;
grid-template-columns: auto;
grid-template-rows: 0.5fr auto;
position: relative;
}
/*
================
Header
================
*/
/*
Slide Menu
= = = = = = = = =
*/
.side-menu__container {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: hidden;
pointer-events: none;
z-index: 25;
}
.side-menu__container-active {
pointer-events: auto;
}
.side-menu__container::before {
content: '';
cursor: pointer;
position: absolute;
display: block;
top: 0;
left: 0;
height: 100%;
width: 100%;
background-color: #0c1066;
opacity: 0;
transition: opacity 300ms linear;
will-change: opacity;
}
.side-menu__container-active::before {
opacity: 0.3;
}
.slide-menu {
box-sizing: border-box;
transform: translateX(-103%);
position: relative;
top: 0;
left: 0;
z-index: 10;
height: 100%;
width: 90%;
max-width: 26rem;
background-color: white;
box-shadow: 0 0 2rem rgba(0, 0, 255, 0.1);
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 2fr 4fr 1fr;
grid-gap: 1rem;
transition: transform 300ms linear;
will-change: transform;
}
.slide-menu-active {
transform: none;
}
.menu-header {
background: linear-gradient(to right, #00FF9B, #5f84fb);
display: grid;
grid-template-rows: 1fr 4fr;
grid-template-columns: 1fr 4fr;
grid-template-areas: "greeting greeting" "image details";
box-sizing: border-box;
width: 100%;
align-content: center;
color: white;
box-shadow: 0 0.5rem 2rem rgba(0, 0, 255, 0.2);
}
.greeting__text {
grid-area: greeting;
font-size: 1.25rem;
letter-spacing: 0.15rem;
text-transform: uppercase;
margin-top: 1rem;
justify-self: center;
align-self: center;
}
.account-details {
grid-area: details;
display: flex;
flex-flow: column;
margin-left: 1rem;
align-self: center;
}
.name__text {
font-size: 1.15rem;
margin-bottom: 0.5rem;
}
.email__text {
font-size: 0.9rem;
letter-spacing: 0.1rem;
}
.menu-body {
display: grid;
width: 100%;
}
.profile-image__container {
grid-area: image;
margin-right: 0.5rem;
border-radius: 50%;
height: 4rem;
width: 4rem;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
background-color: white;
align-self: center;
margin-left: 2rem;
}
.profile__image {
max-width: 4rem;
}
/*Header*/
.main__header {
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr 0.25fr;
grid-template-rows: 1fr;
box-shadow: 0 0 2rem rgba(0, 0, 255, 0.1);
height: 4rem;
margin: 0;
align-items: center;
transition: background-color 500ms linear;
animation: 1s ease-in-out 0ms 1 fadein;
}
.main__header-dark {
background-color: #2B244D;
color: white;
}
.toggle-button__container {
cursor: pointer;
position: relative;
margin: 0 0.5rem;
}
.mode-toggle__input {
-webkit-appearance: none;
-moz-appearance: none;
}
.mode-toggle__bg {
height: 1rem;
width: 2rem;
border-radius: 0.5rem;
background-color: rgba(0, 0, 0, 0.5);
display: inline-block;
transition: background-color 300ms linear;
}
.mode-toggle__circle {
height: 1.30rem;
width: 1.30rem;
background-color: #2B244D;
position: absolute;
top: -0.2rem;
border-radius: 50%;
box-shadow: 0 0 0 rgba(0, 0, 255, 0.5);
transition: left 300ms linear;
left: 0.1rem;
}
.mode-toggle__circle-checked {
background-color: white;
left: 1.75rem;
}
.mode-toggle__bg-checked {
background-color: #FF0070;
}
.mode-toggle__text {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.1rem;
}
/*Content*/
.left__section {
display: grid;
grid-template-rows: 1fr;
grid-template-columns: 1fr 1fr;
max-width: 5rem;
}
.date__text {
text-transform: uppercase;
letter-spacing: 0.1rem;
display: inline;
margin: 0.5rem 0;
}
/*SVGs*/
.hamburger__icon {
position: relative;
z-index: 35;
height: 1rem;
padding: 0.5rem 1.5rem;
margin-right: 1rem;
cursor: pointer;
}
.logo__icon {
height: 2rem;
margin-left: 1rem;
}
.logo__text {
fill: #2B244D;
}
.logo__text-dark {
fill: #ffff;
}
.hamburger__icon__fill {
fill: #2B244D;
}
.hamburger__icon__fill-dark {
fill: #ffff;
}
/*
================
Body
================
*/
.main-container__bg {
height: 100%;
width: 100%;
position: absolute;
top: 0;
left: 0;
z-index: -2;
opacity: 0;
background: white;
transition: opacity 300ms linear;
}
.main-container__bg-dark {
opacity: 1;
background: linear-gradient(to bottom, #B290FF, #2E1D65);
transition: opacity 300ms linear;
}
/*
================-
Footer
================
*/
.main__footer {
background: transparent;
position: absolute;
bottom: 1rem;
left: 1.5rem;
z-index: 100;
}
.copyright__text {
letter-spacing: 0.1rem;
color: white;
}
@media only screen and (max-width: 300px) {
.slide-menu {
width: 100%;
}
}
CSS 扩展
· 排版
display: grid;
grid-template-columns: auto;
grid-template-rows: 0.5fr auto;
这里我基本上使用的css网格布局来划分页面,以便为导航栏设置一个小的顶部行,并使用更大的第二行来包含我们的路由器出口(这里是页面的主要内容的标题)。就像下面这样:
· sidenav
.side-menu__conatiner {
position: fixed;
left: 0;
top: 0 }
在视口的左上角放置sidenav容器
.side-menu__container::before {...}
用于在侧边栏滑入时蓝色淡化背景
will-change: opacity;
用于提前通知浏览器整个背景不透明度将发生变化以便我们实现更好的渲染性能,你可以在这里(https://developer.mozilla.org/en-US/docs/Web/CSS/will-change)阅读更多详细内容。
.slide-menu { transform: translateX(-103%); }
还应该将侧面菜单拉出视图端口,当我们点击汉堡包菜单时,我们应该添加一个.slide-menu-active的class,它将重置转换css属性并从视口的左侧滑动菜单:
.slide-menu-active { transform: none; }
· 模式切换
我想介绍一个UI方面上我怎么样设计主题切换按钮的小技巧。基本上我把一个标准的复选框输入的css样式都设置为none,为了完全移除输入的任何默认样式(这与display:none完全隐藏是不同的),然后我使用两个不同的class选择器来切换按钮的背景着色、完成切换时改变颜色。圆圈的位置取决于使用Angular中内置的ngClass指令存储在组件上的一个boolean变量,它可以让你轻松切换css class。
· 主题组件
这是我们根据不同用户的喜爱城市来展示主题的组件,并且用户可以自己添加城市卡,将他路由到添加城市组件然后在他的主页上添加一个新城市。
首先我们需要生产它的主页组件,这里使用客户端(CLI)命令:
ng g c home
HTML标记只有一个容器和两个其他的组件,但我们将在本教程的后续部分中根据用户的喜好城市来动态添加卡片:
<div class="main__container">
<app-weather-card></app-weather-card>
<app-add-card></app-add-card>
</div>
· 天气卡片组件
使用ngSwitch指令来检查天气状况的变化:
<section class="weather__card" (click)="openDetails()" [ngClass]="{'weather__card-dark': darkMode}">
<!-- TODO: make the city name dynamic -->
<span class="city-name__text">Paris</span>
<div class="weather-icon__container" [ngSwitch]="true">
<svg *ngSwitchCase="condition === 'Clouds'">
</svg>
<svg *ngSwitchCase="condition === 'Rain' || condition === 'Drizzle'">
</svg>
<svg *ngSwitchCase="condition === 'Storm'">
</svg>
<svg *ngSwitchCase="condition === 'Sunny' || condition === 'Clear'">
</svg>
<svg *ngSwitchCase="condition === 'Fog'"></svg>
</div>
<div class="temperature-text__container">
<span class="temperature__text">{{ currentTemp }}</span>
<span class="temperature-metric__text">°</span>
<span class="weather-condition__text">{{ condition }}</span>
</div>
<section class="min-max__container">
<div class="min__container">
<svg class="min-arrow__icon" viewBox="188.5 807 21 21">
<path fill="#00ff9b" d="M209.5 817.5h-21L199 828z" data-name="Min Arrow"/>
</svg>
<span class="min-temperature__text">{{ minTemp }}</span>
<span class="min__text">Min</span>
</div>
<div class="max__container">
<svg class="max-arrow__icon" viewBox="449.5 820 21 21">
<path fill="red" d="M449.5 830.5h21L460 820z" data-name="Max Arrow"/>
</svg>
<span class="max-temperature__text">{{ maxTemp }}</span>
<span class="max__text">Max</span>
</div>
</section>
</section>
下面是一些组件的css样式:
/*
====================
Weather Card Styling
====================
*/
.weather__card {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr 1fr 1fr;
box-shadow: 0 0 2rem rgba(0, 0, 255, 0.1);
justify-items: center;
padding: 2rem;
margin: 2rem;
width: 19rem;
height: 30rem;
cursor: pointer;
background-color: white;
border-radius: 1.75rem;
animation: 1.25s ease-in-out 0ms 1 fadein;
}
.weather__card-dark {
background: linear-gradient(to bottom, #711B86, #00057A);
color: white;
}
.city-name__text {
text-transform: uppercase;
font-size: 1.4rem;
letter-spacing: 0.1rem;
margin-bottom: 1rem;
}
.temperature__text {
align-self: end;
width: 100%;
font-size: 4rem;
font-weight: 100;
letter-spacing: 0.1rem;
}
.temperature-metric__text {
text-align: start;
font-size: 3rem;
}
.min-max__container {
display: grid;
grid-template-rows: 1fr;
grid-template-columns: 1fr 1fr;
align-items: center;
}
.min__container, .max__container {
margin: 1rem 3rem;
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr 1fr;
}
.min-arrow__icon, .max-arrow__icon {
height: 1.25rem;
margin: auto;
}
.max-arrow__icon {
margin-bottom: -0.05rem;
}
.weather-condition__text {
display: block;
font-size: 1rem;
text-transform: uppercase;
letter-spacing: 0.1rem;
text-align: center;
}
.max__text {
color: #FF0070;
}
.min__text {
color: #00FF9B;
}
.max__text, .min__text {
font-size: 1rem;
text-align: center;
}
.max-temperature__text, .min-temperature__text {
text-align: center;
font-size: 2rem;
}
.weather-icon__container {
width: 10rem;
margin-bottom: 2rem;
display: flex;
justify-content: center;
}
.weather-icon__container > svg {
width: 10rem;
}
黑夜主题
在css样式中你会注意到我为大多数到UI元素添加了两个class,原因是我们想要添加额外的css class让它可以切换到黑夜主题,这样我们可以再次使用基于主题切换按钮的状态的ngClass指令切换它们。
· 添加卡片组件
我添加了一个div包装器,它具有黑暗模式ngClass指令,就像大多数都UI元素一样,我添加了Angular路由器routerLink属性以便在用户点击卡片时将他们导航到添加城市页面:
<div class="add__card" routerLink="/add" [ngClass]="{'add__card-dark': darkMode}">
<div class="header__container">
<span class="card__title">Add city</span>
</div>
<div class="body__container">
<svg class="add__icon"></svg>
<svg class="city__illustration"></svg>
</div>
</div>
在css上面就没有什么复杂的了,因为主卡使用网格布局创建2行,以均匀的空开内容。不要忘了添加box-shadow属性给卡片添加一些光投影:
.add__card {
background-color: #ffffff;
box-shadow: 0 0 2rem rgba(0, 0, 255, 0.1);
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr 1fr;
padding: 2rem;
margin: 2rem;
width: 19rem;
height: 30rem;
justify-items: center;
cursor: pointer;
border-radius: 1.75rem;
animation: 1.25s ease-in-out 0ms 1 fadein;
color: #443282;
}
.add__card-dark {
background: linear-gradient(to bottom, #711B86, #00057A);
color: white;
}
.card__title {
text-transform: uppercase;
letter-spacing: 0.1rem;
}
.city__illustration {
width: 20rem;
}
.body__container {
align-self: end;
display: flex;
justify-content: space-between;
align-items: center;
flex-flow: column;
}
.add__icon {
width: 10rem;
margin-bottom: 1.15rem;
}
· 细节组件
我注入了天气服务用来获取天气的数据(之后再说)设置每天的天气名、温度和天气状况在一个独立的公共变量中,我可以在模版中访问该公共变量来显示它:
import {Component, OnDestroy, OnInit} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
import {WeatherService} from '../../services/weather/weather.service';
import {Subscription} from 'rxjs';
@Component({
selector: 'app-details',
templateUrl: './details.component.html',
styleUrls: ['./details.component.css']
})
export class DetailsComponent implements OnInit, OnDestroy {
city: string;
state: string;
temp: number;
hum: number;
wind: number;
today: string;
day1Name: string;
day1State: string;
day1Temp: number;
day2Name: string;
day2State: string;
day2Temp: number;
day3Name: string;
day3State: string;
day3Temp: number;
day4Name: string;
day4State: string;
day4Temp: number;
day5Name: string;
day5State: string;
day5Temp: number;
sub1: Subscription;
sub2: Subscription;
sub3: Subscription;
sub4: Subscription;
sub5: Subscription;
constructor(public activeRouter: ActivatedRoute, public weather: WeatherService) {
}
ngOnInit() {
const todayNumberInWeek = new Date().getDay();
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
this.today = days[todayNumberInWeek];
this.activeRouter.paramMap.subscribe((route: any) => {
this.city = route.params.city;
this.sub1 = this.weather.getWeatherState(this.city).subscribe((state) => this.state = state);
this.sub2 = this.weather.getCurrentTemp(this.city).subscribe((temperature) => this.temp = temperature);
this.sub3 = this.weather.getCurrentHum(this.city).subscribe((humidity) => this.hum = humidity);
this.sub4 = this.weather.getCurrentWind(this.city).subscribe((windspeed) => this.wind = windspeed);
this.sub5 = this.weather.getForecast(this.city).subscribe((data: any) => {
console.log(data);
for (let i = 0; i < data.length; i++) {
const date = new Date(data[i].dt_txt).getDay();
console.log(days[date]);
if (((date === todayNumberInWeek + 1) || (todayNumberInWeek === 6 && date === 0)) && !this.day1Name) {
this.day1Name = days[date];
this.day1State = data[i].weather[0].main;
this.day1Temp = Math.round(data[i].main.temp);
} else if (!!this.day1Name && !this.day2Name && days[date] !== this.day1Name) {
this.day2Name = days[date];
this.day2State = data[i].weather[0].main;
this.day2Temp = Math.round(data[i].main.temp);
} else if (!!this.day2Name && !this.day3Name && days[date] !== this.day2Name) {
this.day3Name = days[date];
this.day3State = data[i].weather[0].main;
this.day3Temp = Math.round(data[i].main.temp);
} else if (!!this.day3Name && !this.day4Name && days[date] !== this.day3Name) {
this.day4Name = days[date];
this.day4State = data[i].weather[0].main;
this.day4Temp = Math.round(data[i].main.temp);
} else if (!!this.day4Name && !this.day5Name && days[date] !== this.day4Name) {
this.day5Name = days[date];
this.day5State = data[i].weather[0].main;
this.day5Temp = Math.round(data[i].main.temp);
}
}
});
});
}
ngOnDestroy() {
this.sub1.unsubscribe();
this.sub2.unsubscribe();
this.sub3.unsubscribe();
this.sub4.unsubscribe();
this.sub5.unsubscribe();
}
}
显然,对天气服务的数据进行了大量的过滤和修改,在下一部分中我们将一些逻辑转移到服务中。
请别忘了取消订阅组件的ngOnDestroy生命周期钩子中每个订阅,以避免内存泄漏。
细节组件有大量的svgs使得HTML特别长,所以请看这里
我现在的黑夜模式的css样式是这样的:
.details-page__wrapper-dark {
background: linear-gradient(#FC7DB8, #495CFC);
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
position: relative;
overflow: hidden;
}
.background-gradient__circle {
position: absolute;
top: 50%;
right: 0;
transform: translateY(-50%);
z-index: 1;
height: 120%;
}
.main-weather__card-dark {
background-color: white;
height: 85%;
width: 60%;
border-radius: 1rem;
position: relative;
z-index: 3;
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 0.5fr 1.25fr;
justify-items: center;
}
.card-header__container-dark {
width: 100%;
max-height: 20rem;
position: relative;
z-index: 1;
}
.back__button {
position: absolute;
top: 2rem;
left: 2.25rem;
width: 5rem;
cursor: pointer;
z-index: 3;
}
.city__illustration {
width: 100%;
border-radius: 1rem 1rem 0 0;
position: relative;
}
.header-content__wrapper {
position: absolute;
z-index: 2;
color: white;
top: 0;
display: grid;
grid-template-rows: 1fr;
grid-template-columns: repeat(2, 1fr);
width: 100%;
height: 100%;
}
.temperature__text {
font-size: 6rem;
letter-spacing: 0.75rem;
}
.city-name__container {
display: flex;
justify-content: center;
align-items: center;
padding-bottom: 25%;
}
.city-name__underline {
background: transparent;
border-radius: 5px;
height: 5px;
box-shadow: 0 3rem 0 0 #ffffff;
}
.city-name__text {
text-transform: uppercase;
letter-spacing: 0.3rem;
font-size: 1.75rem;
padding-bottom: 2rem;
}
.today-weather__container {
align-self: center;
justify-self: center;
display: grid;
width: 100%;
grid-template-rows: 3fr 1fr;
grid-template-columns: 1fr;
justify-items: center;
grid-gap: 2rem;
}
.temp-state__container {
display: flex;
justify-content: center;
flex-flow: column;
}
.weather-state__text {
letter-spacing: 0.5rem;
font-size: 1.15rem;
text-transform: uppercase;
margin-top: 0.25rem;
}
.hum-wind__container {
display: flex;
align-items: center;
margin-left: -4rem;
}
.hum-wind__separator {
margin: 0 2rem;
width: 2px;
height: 2.5rem;
background-color: white;
}
.hum__text, .wind__text {
text-transform: uppercase;
letter-spacing: 0.2rem;
font-size: 0.8rem;
margin-bottom: 1rem;
}
.hum__container, .wind__container {
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
}
/*
================
BODY
================
*/
.body-content__wrapper {
display: grid;
grid-template-columns: 1fr 1.75fr;
grid-template-rows: 1fr;
justify-items: center;
box-sizing: border-box;
grid-column-gap: 1rem;
width: 100%;
padding: 2rem;
}
.forecast__container {
display: flex;
flex-flow: row;
align-items: center;
align-self: center;
justify-self: center;
}
.twitter-feed__container {
margin-top: 1rem;
width: 100%;
}
.twitter-feed__text {
color: #0c1066;
font-size: 1.25rem;
}
.twitter__icon {
width: 1.5rem;
}
.twitter-feed-tag__text {
font-size: 0.85rem;
color: #5f84fb;
letter-spacing: 0.1rem;
text-transform: uppercase;
}
.twitter-feed__header {
display: grid;
grid-template-rows: 2rem;
grid-template-columns: 0.5fr 1.5fr 1fr;
align-items: center;
justify-items: center;
width: 100%;
}
.day-weather__container {
display: flex;
flex-flow: column;
margin: 2rem 1.5rem;
justify-content: center;
align-items: center;
}
.day-name__text {
font-size: 1.5rem;
color: #39437a;
font-weight: bold;
text-transform: uppercase;
margin-bottom: 0.5rem;
}
.forecast-condition__icon {
height: 4rem;
}
.day-temp__text {
font-size: 1.85rem;
color: #0c1066;
letter-spacing: 0.25rem;
margin: 0.75rem 0;
text-align: center;
padding-left: 0.35rem;
}
.day-state__text {
font-size: 0.65rem;
text-transform: uppercase;
letter-spacing: 0.2rem;
color: #2B244D;
}
B.服务
我们想要从特定组件中分离检索API天气数据的逻辑,并将其移动到独立的服务中(这个服务我们可以在整个应用程序使用),并且我们将再次使用简写格式用CLI生成服务。
· 天气服务
ng g s weather
这个服务使用OpenWeatherMapAPI来检索天气信息,在数据传输到最后的组件之前做一些修改。API没用通知我们最高和最低温度值,免费版也限制我们只访问5天/3小时的预测数据,所以我们最终做的是我循环3小时的间隔温度并提取最大值和最小值的近似值。
下面是weather.service.ts
代码:
mport {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {Subject} from 'rxjs';
@Injectable()
export class WeatherService {
constructor(public http: HttpClient) {
}
getCityWeatherByName(city: string, metric: 'metric' | 'imperial' = 'metric'): Subject<string> {
const dataSub = new Subject<string>();
this.http.get(
`https://api.openweathermap.org/data/2.5/weather?q=${city}&units=${metric}&APPID=952d6b1a52fe15a7b901720074680562`)
.subscribe((data) => {
dataSub.next(data['weather']);
}, (err) => {
console.log(err);
});
return dataSub;
}
getCitiesWeathersByNames(cities: Array<string>, metric: 'metric' | 'imperial' = 'metric'): Subject<any> {
const citiesSubject = new Subject();
cities.forEach((city) => {
citiesSubject.next(this.http.get(
`https://api.openweathermap.org/data/2.5/weather?q=${city}&units=${metric}&APPID=952d6b1a52fe15a7b901720074680562`));
});
return citiesSubject;
}
getWeatherState(city: string): Subject<string> {
const dataSubject = new Subject<string>();
this.http.get(
`https://api.openweathermap.org/data/2.5/weather?q=${city}&APPID=952d6b1a52fe15a7b901720074680562`)
.subscribe((data) => {
dataSubject.next(data['weather'][0].main);
});
return dataSubject;
}
getCurrentTemp(city: string, metric: 'metric' | 'imperial' = 'metric'): Subject<number> {
const dataSubject = new Subject<number>();
this.http.get(
`https://api.openweathermap.org/data/2.5/weather?q=${city}&units=${metric}&APPID=952d6b1a52fe15a7b901720074680562`)
.subscribe((weather: any) => {
dataSubject.next(Math.round(Number(weather.main.temp)));
});
return dataSubject;
}
getCurrentHum(city: string, metric: 'metric' | 'imperial' = 'metric'): Subject<number> {
const dataSubject = new Subject<number>();
this.http.get(
`https://api.openweathermap.org/data/2.5/weather?q=${city}&units=${metric}&APPID=952d6b1a52fe15a7b901720074680562`)
.subscribe((weather: any) => {
console.log(weather);
dataSubject.next(weather.main.humidity);
});
return dataSubject;
}
getCurrentWind(city: string, metric: 'metric' | 'imperial' = 'metric'): Subject<number> {
const dataSubject = new Subject<number>();
this.http.get(
`https://api.openweathermap.org/data/2.5/weather?q=${city}&units=${metric}&APPID=952d6b1a52fe15a7b901720074680562`)
.subscribe((weather: any) => {
dataSubject.next(Math.round(Math.round(weather.wind.speed)));
});
return dataSubject;
}
getMaxTemp(city: string, metric: 'metric' | 'imperial' = 'metric'): Subject<number> {
const dataSubject = new Subject<number>();
let max: number;
this.http.get(
`https://api.openweathermap.org/data/2.5/forecast?q=${city}&units=${metric}&APPID=952d6b1a52fe15a7b901720074680562`)
.subscribe((weather: any) => {
max = weather.list[0].main.temp;
weather.list.forEach((value) => {
if (max < value.main.temp) {
max = value.main.temp;
}
});
dataSubject.next(Math.round(max));
});
return dataSubject;
}
getMinTemp(city: string, metric: 'metric' | 'imperial' = 'metric'): Subject<number> {
const dataSubject = new Subject<number>();
let min: number;
this.http.get(
`https://api.openweathermap.org/data/2.5/forecast?q=${city}&units=${metric}&APPID=952d6b1a52fe15a7b901720074680562`)
.subscribe((weather: any) => {
min = weather.list[0].main.temp;
weather.list.forEach((value) => {
if (min > value.main.temp) {
min = value.main.temp;
}
});
dataSubject.next(Math.round(min));
});
return dataSubject;
}
getForecast(city: string, metric: 'metric' | 'imperial' = 'metric'): Subject<Array<any>> {
const dataSubject = new Subject<Array<any>>();
this.http.get(
`https://api.openweathermap.org/data/2.5/forecast?q=${city}&units=${metric}&APPID=952d6b1a52fe15a7b901720074680562`)
.subscribe((weather: any) => {
dataSubject.next(weather.list);
});
return dataSubject;
}
}
正如你所看到的,所有的函数都返回Subject
,我们将使用广播把修改的数据传输到所有订阅它的组件上。免费的天气API很糟糕,我可能会写一个关于如何将这个丑陋的REST API转换为更好的GranphQL的教程,敬请期待(https://tinyletter.com/hamedbaatour)。
快速浏览服务的不同功能:
—getWeatherState
:当前天气的状态(多云、晴天……)
—getCurrentTemp
:当前的温度
—getMinTemp
:温度的最小值(3小时的间隔)
—getMaxTemp
:温度的最大值(3小时间隔)
—getCurrentHum
:当前的湿度
—getCurrentWind
:当前的风速
—getForecast
:获取后续5天的天气信息
—getCityWeatherByName
:传入一个字符串格式的城市,返回整个天气数据。
—getCitiesWeatersByNames
:传入一个数组的城市名,返回这些城市的天气数据。
· UI服务
UI服务有一些函数,我们可以利用这些函数来共享UI的状态(比如被选择的主题模式):
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable()
export class UiService {
darkModeState: BehaviorSubject<boolean>;
constructor() {
// TODO: if the user is signed in get the default value from Firebase
this.darkModeState = new BehaviorSubject<boolean>(false);
}
}
· 路由
我们在使用CLI创建我们的app的时候就已经生成了路由模块,但是我们必须做一些修改routing.module.ts
来告诉Angular不同的路由(URLs)和他们的关联组件:
import {NgModule} from '@angular/core';
import {Routes, RouterModule} from '@angular/router';
import {HomeComponent} from './pages/home/home.component';
import {DetailsComponent} from './pages/details/details.component';
import {AddComponent} from './pages/add/add.component';
import {LoginComponent} from './pages/login/login.component';
import {SignupComponent} from './pages/signup/signup.component';
const routes: Routes = [
{path: '', component: HomeComponent},
{path: 'details/:city', component: DetailsComponent},
{path: 'add', component: AddComponent},
{path: 'login', component: LoginComponent},
{path: 'signup', component: SignupComponent},
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {
}
总结
在构建Minimus天气APP的第一部分我们已经有很好的进展了。我们获得了从图标到UI(用户界面)和UX(用户体验)决策的大部分设计工作,我们写了很多的HTML和CSS,让它看起来很漂亮。
现场Demo:https://minimus-app.firebaseapp.com/
Github仓库:https://github.com/hamedbaatour/Minimus
根据我们所有的事项列表,到现在我们已经完成了:
· 基于最新和最好的版本Angular6
· 现代CSS: Grid Layout & Flex Box
· 2中模式:白天和黑夜模式
· 漂亮的简约设计