angular构建项目_使用Angular&Electron III构建音乐播放器:将所有功能整合在一起...

angular构建项目

This post is the last part of "Build a Music Player with Angular and Electron." In the previous post we discussed presentation components which are also known as UI components. We were able to build out the UI for our app from start to finish but then users don't consume pretty UI. Users will always want behavior in as much as they appreciate a good looking app.

这篇文章是“用Angular和Electron构建音乐播放器”的最后一部分。 在上一篇文章中,我们讨论了表示组件,也称为UI组件。 我们能够从头到尾为我们的应用程序构建UI,但是这样用户就不会使用漂亮的UI。 用户总是会希望他们的行为与他们喜欢的漂亮应用程序一样多。

In this last post, we are going bring everything we have done together using a container component and abstracting data request and handling to injectable services.

在上一篇文章中,我们将使用容器组件将抽象的数据请求和处理过程整合到可注入的服务中,从而将我们所做的一切结合在一起。

As a quick reminder, the image shows what we are up to:

快速提醒一下,该图像显示了我们的工作:

注射服务 ( Injectable Services )

Before we dive into building the container component, let's prepare the components' dependencies which are services that will handle few utility tasks including network requests and playing sounds with the browser's audio API.

在深入研究构建容器组件之前,让我们准备组件的依赖关系,即这些服务将处理一些实用程序任务,包括网络请求和使用浏览器的audio API播放声音。

API服务 (API Service)

The API service completes a very simple task -- makes a get request to Soundcloud depending all the url passed to it while attaching a client Id to the request:

API服务完成了一个非常简单的任务-在将客户端ID附加到请求时,根据传递给它的所有URL向Soundcloud发出get请求:

// ./src/app/music/shared/api.service.ts
import { Injectable } from '@angular/core';
import { Http } from '@angular/http';

@Injectable()
export class ApiService {

    clientId = '[CLIENT_ID]'

    constructor(
      private http: Http
    ) {}

    get(url, attachClientId?) {
      // Should attach client id if the attachToken
      // is true
      let u;
      attachClientId ? u = this.prepareUrl(url) : u = url;
      // Returns an obsrevable
      // for the HTTP get request
      return this.http.get(u);
    }

    prepareUrl(url) {
      //Attach client id to stream url
      return `${url}?client_id=${this.clientId}`
    }

}

The prepareUrl method attaches the client Id to the URL so when a GET request is made, the get method checks if the attachClient flag is raised and calls prepareUrl based the flag condition.

prepareUrl方法prepareUrl客户端ID附加到URL,因此,在发出GET请求时, get方法将检查是否引发了attachClient标志,并根据标志条件调用prepareUrl

音乐服务 (Music Service)

One more service we will need is the service that talks to API Service as well as use the audio API to play songs based on the feedback form the API Service:

我们将需要的另一项服务是与API Service对话并根据API Service的反馈使用audio API播放歌曲的服务:

// ./src/app/music/shared/music.service.ts
import { Injectable } from '@angular/core';
import { ApiService } from './api.service';

import 'rxjs/add/operator/map';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';


@Injectable()
export class MusicService {

  audio;

  constructor(
    private apiService: ApiService
  ) {
    this.audio = new Audio();
  }

  load(url) {
    this.audio.src = this.apiService.prepareUrl(url);
    this.audio.load();
  }

  play(url) {
    this.load(url);
    this.audio.play()
  }

  getPlaylistTracks () {
      //Request for a playlist via Soundcloud using a client id
      return this.apiService.get('https://api.soundcloud.com/playlists/209262931', true)
        .map(res => res.json())
        .map(data => data.tracks);
  }

  randomTrack(tracks) {
    const trackLength = tracks.length;
    // Pick a random number
    const randomNumber = Math.floor((Math.random() * trackLength) + 1);
    // Return a random track
    return tracks[randomNumber];
  }

  formatTime(seconds) {
    let minutes:any = Math.floor(seconds / 60);
    minutes = (minutes >= 10) ? minutes : "0" + minutes;
    seconds = Math.floor(seconds % 60);
    seconds = (seconds >= 10) ? seconds : "0" + seconds;
    return minutes + ":" + seconds;
  }

  findTracks(value) {
    return this.apiService.get(`${this.apiService.prepareUrl('https://api.soundcloud.com/tracks')}&q=${value}`, false)
      .debounceTime(300)
      .distinctUntilChanged()
      .map(res => res.json())
  }

  xlArtwork(url) {
    return url.replace(/large/, 't500x500');
  }

}

Let's walk through what this service by discussing each of the methods.

让我们通过讨论每种方法来逐步了解该服务。

First things first, when the service is initialized, the audio API is set up by instantiating it and setting the instance to audio property on the class.

首先,在初始化服务时,通过实例化audio API并将实例设置为类的audio属性来设置audio API。

The load and play methods call the audio API's load and play methods as well as passing in a URL to be loaded and played.

loadplay方法调用audio API的loadplay方法,并传入要加载和播放的URL。

The app is expected to load a random song and play the song when the app starts. I created a playlist (which you can of course replace) and using getPlaylistTracks to fetch the playlist's tracks. randomTrack is used to shuffle these tracks and pick one from the tracks to play.

该应用程序有望加载随机歌曲并在应用程序启动时播放该歌曲。 我创建了一个播放列表(您当然可以替换),并使用getPlaylistTracks来获取播放列表的曲目。 randomTrack用于随机播放这些曲目并从曲目中选择一个进行播放。

formatTime method prepares the time so it can be displayed by the progress elapsed and total properties while xlArtwork generates a large image of the track artwork. We will see where the artwork is useful later in this post.

formatTime方法会准备时间,以便可以通过进度和total属性来显示elapsed ,而xlArtwork生成轨道艺术品的大图像。 我们将在本文后面的地方看到艺术品的有用之处。

Finally, the findTracks will be utilized by the search component to search for tracks. The interesting thing about this method is that the returned observable is operated on with debounceTime to wait for 300ms interval between each request and distinctUntilChange to not repeat a request for the same values. These are great performance strategies.

最后,搜索组件将使用findTracks搜索曲目。 关于此方法的有趣之处在于,使用debounceTime对返回的observable进行操作,以等待每个请求之间的间隔为300ms,并且distinctUntilChange不会重复相同值的请求。 这些都是出色的绩效策略。

应用程序组件(容器组件) ( App Component (Container Component) )

Now to the real meat. The app component will serve as our container component, and with time we will see how the numbers add up.

现在到真正的肉。 应用程序组件将用作我们的容器组件,随着时间的流逝,我们将看到数字的累加方式。

应用启动时会发生什么? ( What Happens when App Starts? )

As mentioned earlier, while building our services, the plan is to play a random track from a given playlist when the app loads. To achieve this, we need to utilize Angular's lifecycle hook, ngOnInit which is a lifecycle method called when the component is ready:

如前所述,在构建我们的服务时,计划是在应用加载时播放给定播放列表中的随机曲目。 为此,我们需要利用Angular的生命周期挂钩ngOnInit ,这是在组件就绪时调用的生命周期方法:

// ./src/app/app.component.ts
import { Component, OnInit } from '@angular/core';
import { MusicService } from './music/shared/music.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit{

  tracks: any[] = [];

  constructor(
    private musicService: MusicService
  ){}

  ngOnInit() {
    this.musicService.getPlaylistTracks().subscribe(tracks => {
      this.tracks = tracks;
      this.handleRandom();
    });

    // On song end
    this.musicService.audio.onended = this.handleEnded.bind(this);
    // On play time update
    this.musicService.audio.ontimeupdate = this.handleTimeUpdate.bind(this);
  }
}

MusicService is the only service we are injecting because the APIService is already used by MusicService for whatever we need the API service for.

MusicService是我们要注入的唯一服务,因为APIService已经将MusicService用于我们需要API服务的任何地方。

The AppComponent class must implement OnInit so as to make use of the ngOnInit hook. The hook uses music service's getPlaylistTracks to get a list of tracks, set the tracks property to the returned list, and call the handleRandom method which we are yet to create.

AppComponent类必须实现OnInit才能使用ngOnInit挂钩。 该挂钩使用音乐服务的getPlaylistTracks来获取曲目列表,将tracks属性设置为返回的列表,并调用我们尚未创建的handleRandom方法。

We also set up two events -- what happens when a song ends and when the playing time updates. The events are passed handlers which we are yet to create too.

我们还设置了两个事件-歌曲结束和播放时间更新时会发生什么。 这些事件是传递给我们尚未创建的处理程序的。

随机轨道 ( Random Tracks )

The handleRandom method being used in the hook above is what we will create now. This method plucks a random song from the tracks property and plays the plucked song:

上面的挂钩中使用的handleRandom方法现在将创建。 此方法从tracks属性中抽取一首随机歌曲,然后播放该歌曲:

// . . .
export class AppComponent implements OnInit {
  title;
  tracks: any[] = [];
  backgroundStyle;

  //. . .

  handleRandom() {
    // Pluck a song
    const randomTrack = this.musicService.randomTrack(this.tracks);
    // Play the plucked song
    this.musicService.play(randomTrack.stream_url)
    // Set the title property
    this.title = randomTrack.title;
    // Create a background based on the playing song
    this.backgroundStyle = this.composeBackgroundStyle(randomTrack.artwork_url)
  }
}

One other thing that the method does is to set the title property for DetailsComponent which is the UI component that displays the title of the playing song. The method also set's a backgroundStyle property which is dynamically updates the background image of the app with the playing track's URL. composeBackgroundStyle returns the style object based on the image URL:

该方法要做的另一件事是为DetailsComponent设置title属性, DetailsComponent是显示播放歌曲标题的UI组件。 该方法还设置了backgroundStyle属性,该属性使用播放曲目的URL动态更新应用程序的背景图像。 composeBackgroundStyle根据图片网址返回样式对象:

composeBackgroundStyle(url) {
      return {
        width: '100%',
        height: '600px',
        backgroundSize:'cover',
        backgroundImage: `linear-gradient(
      rgba(0, 0, 0, 0.7),
      rgba(0, 0, 0, 0.7)
    ),   url(${this.musicService.xlArtwork(url)})`
      }
  }

The style is set on the template using NgStyle directive:

使用NgStyle指令在模板上设置样式:

<!-- ./src/app/app.component.html -->
<div [ngStyle]="backgroundStyle">
</div>

音频事件的进展 ( Progress with Audio Events )

When the component was initialized, we did set up some events for playing time update and song end. The handlers for these events were not created, let's do that now:

初始化组件后,我们确实设置了一些用于播放时间更新和歌曲结束的事件。 这些事件的处理程序尚未创建,现在让我们开始:

// . . .
export class AppComponent implements OnInit{
  title;
  position;
  elapsed;
  duration;
  tracks: any[] = [];
  backgroundStyle;

  constructor(
    private musicService: MusicService
  ){}

  ngOnInit() {
    // . . .
    this.musicService.audio.onended = this.handleEnded.bind(this);
    this.musicService.audio.ontimeupdate = this.handleTimeUpdate.bind(this);
  }

  // . . .

  handleEnded(e) {
    this.handleRandom();
  }

  handleTimeUpdate(e) {
    const elapsed =  this.musicService.audio.currentTime;
    const duration =  this.musicService.audio.duration;
    this.position = elapsed / duration;
    this.elapsed = this.musicService.formatTime(elapsed);
    this.duration = this.musicService.formatTime(duration);
  }
}

onended event is handled by handleEnded. The handler is very simple; it just calls the handleRandom method to shuffle and pluck a song.

onended事件由handleEnded处理。 处理程序非常简单; 它只是调用handleRandom方法来随机播放歌曲。

ontimeupdate is called at intervals when the song is playing. So it's the perfect event to hook in and update our progress bar as well as the elapsed and total play time. This is why we are updating the the position, elapsed and duration properties which are passed down to the ProgressComponent.

播放歌曲时会ontimeupdate调用ontimeupdate 。 因此,连接并更新进度条以及已用时间和总播放时间是完美的活动。 这就是为什么我们要更新positionelapsedduration属性,这些属性将传递给ProgressComponent

搜索组件事件和属性 ( Search Component Events & Properties )

The search component demands values for it's tracks property, and event handlers for it's query and update events. These properties and events were created in the previous article.

搜索组件需要其tracks属性的值,以及queryupdate事件的事件处理程序。 这些属性和事件是在上一篇文章中创建的。

The query event handle will be called handleQuery and will set the SearchComponent's tracks property using AppComponents's filteredTracks:

query事件句柄将称为handleQuery ,并将使用AppComponentsfilteredTracks设置SearchComponenttracks属性:

// . . .
export class AppComponent implements OnInit{
  // . . .
  filteredTracks: any[] = [];

  constructor(
    private musicService: MusicService
  ){}

  // . . .

  handleQuery(payload) {
      this.musicService.findTracks(payload).subscribe(tracks => {
        this.filteredTracks = tracks;
      });
  }
}

The handleQuery method uses MusicService's findTracks to find tracks that match the text being entered in the search text box and then sets filteredTracks to the fetched tracks. filteredTracks is what is passed as the value of tracks to the SearchComponent.

handleQuery方法使用MusicServicefindTracks查找与在搜索文本框中输入的文本匹配的轨道,然后将filteredTracks设置为获取的轨道。 filteredTracks是作为tracks的值传递到SearchComponent

The handleUpdate is called when the autocomplete's suggestion is clicked. The handler plays the selected item:

单击自动完成的建议时,将调用handleUpdate 。 处理程序播放选定的项目:

// . . .
export class AppComponent implements OnInit{
  // . . .
  filteredTracks: any[] = [];

  constructor(
    private musicService: MusicService
  ){}

  // . . .

  handleQuery(payload) {
      this.musicService.findTracks(payload).subscribe(tracks => {
        this.filteredTracks = tracks;
      });
  }
}

玩家事件与属性 ( Player Events & Properties )

The player UI component has more events than the rest of the components. It just has one property, paused which is a boolean to check if a song is paused or playing.

播放器UI组件比其他组件具有更多的事件。 它只有一个属性,“ paused ,这是一个布尔值,用于检查歌曲是否已暂停或正在播放。

播放器:暂停和播放 (Player: Pause & Play)

The pause and play action are controlled by one event, and the paused flag property is used to check the playing status of a song and act accordingly:

暂停和播放动作由一个事件控制,并且paused flag属性用于检查歌曲的播放状态并采取相应的措施:

// . . .
export class AppComponent implements OnInit{
  // . . .
  paused = true;

  constructor(
    private musicService: MusicService
  ){}

  // . . .

  handlePausePlay() {
      if(this.musicService.audio.paused) {
        this.paused = true;
        this.musicService.audio.play()
      } else {
        this.paused = false;
        this.musicService.audio.pause()
      }
  }
}

The handler checks if the song is paused and plays the song. Otherwise the reverse is the case.

处理程序检查歌曲是否暂停并播放歌曲。 否则情况相反。

玩家:停止 (Player: Stop)

The stop events resets the song to beginning:

停止事件会将歌曲重置为开始:

// . . .
export class AppComponent implements OnInit{
  // . . .

  constructor(
    private musicService: MusicService
  ){}

  // . . .

  handleStop() {
    this.musicService.audio.pause();
    this.musicService.audio.currentTime = 0;
    this.paused = false;
  }
}

It's just a trick. We pause the song, reset the time to beginning (0) and turn the paused flag down.

这只是一个把戏。 我们暂停歌曲,将时间重置为开始时间(0),然后将已paused标志调低。

播放器:后退和前进 (Player: Backward & Forward)

The backward and forward events are used to rewind or fast forward the song based on a given interval:

后退和前进事件用于根据给定的间隔倒退或快进歌曲:

// . . .
export class AppComponent implements OnInit{
  // . . .

  constructor(
    private musicService: MusicService
  ){}

  // . . .

  handleBackward() {
    let elapsed =  this.musicService.audio.currentTime;
    console.log(elapsed);
    if(elapsed >= 5) {
      this.musicService.audio.currentTime = elapsed - 5;
    }
  }

  handleForward() {
    let elapsed =  this.musicService.audio.currentTime;
    const duration =  this.musicService.audio.duration;
    if(duration - elapsed >= 5) {
      this.musicService.audio.currentTime = elapsed + 5;
    }
  }
}

For backward, we first check if we are 5 seconds into the song before attempting to take it back 5 seconds and for forward we check if we have up to 5 seconds left to be played in the song before trying to push it forward.

对于后退,我们首先检查是否在歌曲中5秒钟,然后再尝试将其退回5秒钟;对于前进,我们先检查在歌曲中是否还剩5秒钟,然后再尝试将其前进。

球员:随机 (Player: Random)

The random event just calls the random handler we created earlier to manually pluck a random track and play.

随机事件只是调用我们之前创建的随机处理程序,以手动执行随机跟踪和播放。

应用程序组件模板 ( App Component Template )

The app component template assembles all the UI component while passing them their respective properties and event handlers. It is also wrapped by a div which takes the NgStyle directive for dynamic background images.

应用程序组件模板将组装所有UI组件,同时将它们各自的属性和事件处理程序传递给它们。 它也由div包装,该div使用NgStyle指令获取动态背景图像。

<!-- ./src/app/app.component.html -->
<div [ngStyle]="backgroundStyle">
  <music-search
    (query)="handleQuery($event)"
    (update)="handleUpdate($event)"
    [tracks]="filteredTracks"
  ></music-search>

  <music-details
    [title]="title"
  ></music-details>

  <music-player
    (random)="handleRandom($event)"
    (backward)="handleBackward()"
    (forward)="handleForward()"
    (pauseplay)="handlePausePlay()"
    (stop)="handleStop()"
    [paused]="paused"
  ></music-player>

  <music-progress
    [current]="position"
    [elapsed]="elapsed"
    [total]="duration"
  ></music-progress>

  <music-footer></music-footer>

</div>

结论 ( Conclusion )

There are two important things to take home from this long journey -- how components interact and code reuse. We were able to divide and conquer by using different levels of component which come together to make an awesome product.

在这段漫长的旅程中,有两点很重要的东西-组件如何交互以及代码重用。 我们能够通过使用不同级别的组件来进行分而治之,这些组件共同构成了出色的产品。

You must have noticed how many times a member of the music or API service got called and how many times we called handleRandom. If not for good structure which is cheap to afford, we would have got ourselves in a deep mess and probably given up on the just completed journey.

您一定已经注意到音乐或API服务的成员被调用了多少次,以及我们调用handleRandom 。 如果不是因为价格低廉的良好结构,我们将陷入困境,并可能放弃了刚刚完成的旅程。

翻译自: https://scotch.io/tutorials/build-a-music-player-with-angular-electron-iii-bringing-it-all-together

angular构建项目

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值