첫 Ember.js 앱 구축 가이드

게시 됨: 2022-03-11

최신 웹 응용 프로그램이 클라이언트 측에서 점점 더 많이 수행됨에 따라("웹 사이트"와 반대되는 "웹 응용 프로그램"으로 지칭한다는 사실 자체가 매우 시사하는 바 있음) 클라이언트 측 프레임워크에 대한 관심이 높아졌습니다. . 이 분야에 많은 플레이어가 있지만 기능이 많고 움직이는 부분이 많은 애플리케이션의 경우 Angular.js와 Ember.js 중 두 가지가 특히 두드러집니다.

우리는 이미 포괄적인 [Angular.js 튜토리얼][https://www.toptal.com/angular-js/a-step-by-step-guide-to-your-first-angularjs-app]을 게시했으므로 우리는' 이 게시물에서는 Ember.js에 중점을 두어 음악 컬렉션을 카탈로그화하는 간단한 Ember 애플리케이션을 구축할 것입니다. 프레임워크의 주요 빌딩 블록을 소개하고 디자인 원칙을 엿볼 수 있습니다. 읽는 동안 소스 코드를 보고 싶다면 Github에서 락앤롤로 사용할 수 있습니다.

무엇을 만들까요?

Rock & Roll 앱의 최종 버전은 다음과 같습니다.

ember.js가 포함된 앱의 최종 버전

왼쪽에는 아티스트 목록이 표시되고 오른쪽에는 선택한 아티스트의 노래 목록이 표시됩니다. 새로운 아티스트와 노래는 텍스트 상자에 입력하고 옆에 있는 버튼을 누르기만 하면 추가할 수 있습니다. 각 노래 옆에 있는 별은 iTunes라 하여 평가하는 역할을 합니다.

앱의 기본 기능을 다음 단계로 나눌 수 있습니다.

  1. '추가'를 클릭하면 '신규 아티스트' 필드에 지정된 이름을 가진 새 아티스트가 목록에 추가됩니다(특정 아티스트의 노래도 마찬가지).
  2. '신규 아티스트' 필드를 비우면 '추가' 버튼이 비활성화됩니다(특정 아티스트의 노래도 마찬가지).
  3. 아티스트 이름을 클릭하면 오른쪽에 아티스트의 노래가 나열됩니다.
  4. 별을 클릭하면 주어진 노래를 평가합니다.

이 작업을 수행하려면 갈 길이 멀기 때문에 시작하겠습니다.

경로: Ember.js 앱의 키

Ember의 두드러진 특징 중 하나는 URL을 강조한다는 것입니다. 다른 많은 프레임워크에서는 별도의 화면에 대해 별도의 URL을 갖는 것이 부족하거나 나중에 고려해야 합니다. Ember에서 라우터(URL과 URL 간의 전환을 관리하는 구성 요소)는 빌딩 블록 간의 작업을 조정하는 중심 부분입니다. 결과적으로 Ember 애플리케이션의 내부 작동을 이해하는 열쇠이기도 합니다.

다음은 응용 프로그램의 경로입니다.

 App.Router.map(function() { this.resource('artists', function() { this.route('songs', { path: ':slug' }); }); });

리소스 경로, artists 및 그 안에 중첩된 songs 경로를 정의합니다. 해당 정의는 다음 경로를 제공합니다.

경로

훌륭한 Ember Inspector 플러그인(Chrome과 Firefox 모두에 존재)을 사용하여 생성된 경로를 쉽게 읽을 수 있는 방식으로 보여줍니다. 다음은 Ember 경로에 대한 기본 규칙이며 위의 표를 참조하여 특정 사례에 대해 확인할 수 있습니다.

  1. 암시적 application 경로가 있습니다.

    이것은 모든 요청(전환)에 대해 활성화됩니다.

  2. 암시적 index 경로가 있습니다.

    사용자가 애플리케이션의 루트로 이동할 때 입력됩니다.

  3. 각 리소스 경로는 이름이 같은 경로를 만들고 그 아래에 암시적으로 인덱스 경로를 만듭니다.

    이 색인 경로는 사용자가 경로를 탐색할 때 활성화됩니다. 우리의 경우 사용자가 /artists 로 이동할 때 artists.index 가 트리거됩니다.

  4. 단순한(리소스가 아닌) 중첩 경로는 접두사로 상위 경로 이름을 갖습니다.

    this.route('songs', ...) 로 정의한 경로는 이름으로 artists.songs 를 갖습니다. 사용자가 /artists/pearl-jam 또는 /artists/radiohead 로 이동할 때 트리거됩니다.

  5. 경로를 지정하지 않으면 경로 이름과 동일한 것으로 간주됩니다.

  6. 경로에 : 가 포함되어 있으면 동적 세그먼트로 간주됩니다 .

    할당된 이름(이 경우 slug )은 해당 URL 세그먼트의 값과 일치합니다. 위의 slug 세그먼트에는 pearl-jam , radiohead 또는 URL에서 추출한 기타 값이 있습니다.

아티스트 목록 표시

첫 번째 단계로 왼쪽에 아티스트 목록을 표시하는 화면을 빌드합니다. 이 화면은 사용자가 /artists/ 로 이동할 때 표시되어야 합니다.

아티스트

화면이 어떻게 렌더링되는지 이해하기 위해 또 다른 중요한 Ember 디자인 원칙인 구성보다 관례 를 소개할 때입니다. 위 섹션에서 /artistsartists 경로를 활성화하는 것을 보았습니다. 규칙에 따라 해당 경로 개체 의 이름은 ArtistsRoute 입니다. 앱이 렌더링할 데이터를 가져오는 것은 이 경로 개체의 책임입니다. 이는 경로의 모델 후크에서 발생합니다.

 App.ArtistsRoute = Ember.Route.extend({ model: function() { var artistObjects = []; Ember.$.getJSON('http://localhost:9393/artists', function(artists) { artists.forEach(function(data) { artistObjects.pushObject(App.Artist.createRecord(data)); }); }); return artistObjects; } });

이 스니펫에서 데이터는 백엔드에서 XHR 호출을 통해 가져와서 모델 개체로 변환한 후 나중에 표시할 수 있는 배열로 푸시됩니다. 그러나 경로의 책임은 컨트롤러가 처리하는 디스플레이 논리를 제공하는 것으로 확장되지 않습니다. 한 번 보자.

흠, 사실 이 시점에서 컨트롤러를 정의할 필요는 없습니다! Ember는 필요할 때 컨트롤러를 생성하고 컨트롤러의 M.odel 속성을 모델 후크 자체의 반환 값, 즉 아티스트 목록으로 설정할 만큼 충분히 똑똑합니다. (다시 말하지만, 이것은 '구성에 대한 관례' 패러다임의 결과입니다.) 한 계층 아래로 내려가 목록을 표시하는 템플릿을 만들 수 있습니다.

 <script type="text/x-handlebars" data-template-name="artists"> <div class="col-md-4"> <div class="list-group"> {{#each model}} {{#link-to "artists.songs" this class="list-group-item artist-link"}} {{name}} <span class="pointer glyphicon glyphicon-chevron-right"></span> {{/link-to}} {{/each}} </div> </div> <div class="col-md-8"> <div class="list-group"> {{outlet}} </div> </div> </script>

이것이 친숙해 보인다면 Ember.js가 매우 간단한 구문과 도우미가 있지만 중요한 논리(예: 조건부에서 ORing 또는 ANDing 용어)를 허용하지 않는 Handlebars 템플릿을 사용하기 때문입니다.

위의 템플릿에서 우리는 모델을 반복하고(모든 아티스트를 포함하는 배열에 대한 경로로 이전에 설정) 그 안의 각 항목에 대해 해당 아티스트의 artists.songs 경로로 이동하는 링크를 렌더링합니다. 링크에는 아티스트 이름이 포함되어 있습니다. Handlebars의 #each 도우미는 내부 범위를 현재 항목으로 변경하므로 {{name}} 은(는) 항상 현재 반복 중인 아티스트의 이름을 참조합니다.

중첩 보기의 중첩 경로

위 스니펫의 또 다른 관심 지점: {{outlet}} , 콘텐츠를 렌더링할 수 있는 템플릿의 슬롯을 지정합니다. 경로를 중첩할 때 외부 리소스 경로에 대한 템플릿이 먼저 렌더링되고 내부 경로가 뒤따르며 템플릿 콘텐츠를 외부 경로에 의해 정의된 {{outlet}} 으로 렌더링합니다. 이것이 바로 여기서 일어나는 일입니다.

규칙에 따라 모든 경로는 해당 콘텐츠를 동일한 이름의 템플릿으로 렌더링합니다. 위에서 위 템플릿의 data-template-name 속성은 artists 입니다. 이는 이것이 외부 경로인 artists 에 대해 렌더링된다는 것을 의미합니다. 오른쪽 패널의 콘텐츠에 대한 출구를 지정합니다. 이 패널은 내부 경로인 artists.index 가 콘텐츠를 렌더링하는 곳입니다.

 <script type="text/x-handlebars" data-template-name="artists/index"> <div class="list-group-item empty-list"> <div class="empty-message"> Select an artist. </div> </div> </script>

요약하면 하나의 경로( artists )는 왼쪽 사이드바에 콘텐츠를 렌더링하고 해당 모델은 아티스트 목록입니다. 다른 경로인 artists.index 는 자체 콘텐츠를 artists 템플릿에서 제공하는 슬롯에 렌더링합니다. 모델로 사용할 일부 데이터를 가져올 수 있지만 이 경우 표시하려는 것은 정적 텍스트뿐이므로 필요하지 않습니다.

관련: 8가지 필수 Ember.js 인터뷰 질문

아티스트 만들기

1부: 데이터 바인딩

다음으로, 지루한 목록만 보는 것이 아니라 아티스트를 만들 수 있기를 원합니다.

아티스트 목록을 렌더링하는 artists 템플릿을 보여주었을 때 나는 약간의 속임수를 썼습니다. 나는 중요한 것에 집중하기 위해 상단 부분을 잘라 냈습니다. 이제 다시 추가하겠습니다.

 <script type="text/x-handlebars" data-template-name="artists"> <div class="col-md-4"> <div class="list-group"> <div class="list-group-item"> {{input type="text" class="new-artist" placeholder="New Artist" value=newName}} <button class="btn btn-primary btn-sm new-artist-button" {{action "createArtist"}} {{bind-attr disabled=disabled}}>Add</button> </div> < this is where the list of artists is rendered > ... </script>

간단한 텍스트 입력을 렌더링하기 위해 텍스트 유형과 함께 Ember 도우미 input 을 사용합니다. 여기에서 텍스트 입력 값을 이 템플릿을 백업하는 컨트롤러인 ArtistsControllernewName 속성에 바인딩 합니다. 결과적으로 입력의 값 속성이 변경될 때(즉, 사용자가 텍스트를 입력할 때) 컨트롤러의 newName 속성은 동기화된 상태로 유지됩니다.

또한 버튼을 클릭할 때 createArtist 작업이 시작되어야 함을 알립니다. 마지막으로 버튼의 disabled 속성을 컨트롤러의 disabled 속성에 바인딩합니다. 컨트롤러는 어떻게 생겼습니까?

 App.ArtistsController = Ember.ArrayController.extend({ newName: '', disabled: function() { return Ember.isEmpty(this.get('newName')); }.property('newName') });

newName 은 시작 시 공백으로 설정되며, 이는 텍스트 입력이 공백이 될 것임을 의미합니다. (내가 바인딩에 대해 말한 것을 기억하십니까? newName 을 변경하고 입력 필드의 텍스트로 반영되는지 확인하십시오.)

disabled 는 입력 상자에 텍스트가 없을 때 true 를 반환하므로 버튼이 비활성화되도록 구현됩니다. 끝에 있는 .property 호출은 이것을 Ember 케이크의 또 다른 맛있는 조각인 "계산된 속성"으로 만듭니다.

계산된 속성은 다른 속성에 의존하는 속성으로, 그 자체가 "정상"이거나 계산될 수 있습니다. Ember는 종속 속성 중 하나가 변경될 때까지 이러한 값을 캐시합니다. 그런 다음 계산된 속성의 값을 다시 계산하고 다시 캐시합니다.

다음은 위의 프로세스를 시각적으로 표현한 것입니다. 요약하자면: 사용자가 아티스트 이름을 입력하면 newName 속성이 업데이트되고 그 뒤에 disabled 속성이 업데이트되고 마지막으로 아티스트 이름이 목록에 추가됩니다.

우회: 진실의 단일 소스

그것에 대해 잠시 생각해보십시오. 바인딩 및 계산된 속성의 도움으로 데이터를 단일 소스 로 설정(모델링)할 수 있습니다. 위에서 새 아티스트의 이름을 변경하면 컨트롤러 속성이 변경되고 비활성화된 속성이 변경됩니다. 사용자가 새 아티스트의 이름을 입력하기 시작하면 마치 마법처럼 버튼이 활성화됩니다.

시스템이 클수록 'Single Source of Truth' 원칙에서 더 많은 영향력을 얻을 수 있습니다. 코드를 깨끗하고 강력하게 유지하고 속성 정의를 보다 선언적으로 유지합니다.

일부 다른 프레임워크는 모델 데이터가 단일 정보 소스가 되도록 강조하지만 Ember만큼 멀리 가지 않거나 철저한 작업을 수행하지 못합니다. 예를 들어 Angular에는 양방향 바인딩이 있지만 계산된 속성은 없습니다. 간단한 함수를 통해 계산된 속성을 "에뮬레이트"할 수 있습니다. 여기서 문제는 "계산된 속성"을 새로 고칠 때를 알 수 있는 방법이 없으므로 더티 검사에 의존하고 결과적으로 특히 더 큰 응용 프로그램에서 두드러지는 성능 손실을 초래한다는 것입니다.

이 주제에 대해 더 자세히 알고 싶으시면 더 짧은 버전의 eviltrout 블로그 게시물을 읽어보거나 양측의 핵심 개발자가 참여하는 더 긴 토론을 위해 이 Quora 질문을 읽어보시기 바랍니다.

2부: 작업 처리기

버튼을 누른 후 실행된 후 createArtist 작업이 어떻게 생성되는지 다시 살펴보겠습니다.

 App.ArtistsRoute = Ember.Route.extend({ ... actions: { createArtist: function() { var name = this.get('controller').get('newName'); Ember.$.ajax('http://localhost:9393/artists', { type: 'POST', dataType: 'json', data: { name: name }, context: this, success: function(data) { var artist = App.Artist.createRecord(data); this.modelFor('artists').pushObject(artist); this.get('controller').set('newName', ''); this.transitionTo('artists.songs', artist); }, error: function() { alert('Failed to save artist'); } }); } } });

작업 핸들러는 actions 개체에 래핑되어야 하며 경로, 컨트롤러 또는 보기에서 정의할 수 있습니다. 액션의 결과가 컨트롤러에 국한되지 않고 "전역"이기 때문에 여기에서 경로에 정의하기로 결정했습니다.

여기에는 멋진 일이 없습니다. 백엔드에서 저장 작업이 성공적으로 완료되었음을 알리면 다음 세 가지 작업을 순서대로 수행합니다.

  1. 템플릿의 모델(모든 아티스트)에 새 아티스트를 추가하면 다시 렌더링되고 새 아티스트가 목록의 마지막 항목으로 표시됩니다.
  2. newName 바인딩을 통해 입력 필드를 지우면 DOM을 직접 조작할 필요가 없습니다.
  3. 새 경로( artists.songs )로 전환하고 새로 생성된 아티스트를 해당 경로의 모델로 전달합니다. transitionTo 는 내부적으로 경로 사이를 이동하는 방법입니다. ( link-to helper는 사용자 작업을 통해 이를 수행합니다.)

아티스트의 노래 표시

아티스트 이름을 클릭하여 아티스트의 노래를 표시할 수 있습니다. 우리는 또한 새로운 경로의 모델이 될 아티스트를 전달합니다. 이렇게 모델 객체가 전달되면 모델을 해석할 필요가 없기 때문에 경로의 model 후크가 호출되지 않습니다.

여기서 활성 경로는 ArtistsSongsController artists.songs artists/songs 가 됩니다. 템플릿이 artists 템플릿에서 제공하는 콘센트로 렌더링되는 방식을 이미 보았으므로 현재 템플릿에만 집중할 수 있습니다.

 <script type="text/x-handlebars" data-template-name="artists/songs"> (...) {{#each songs}} <div class="list-group-item"> {{title}} {{view App.StarRating maxRating=5}} </div> {{/each}} </script>

새 아티스트를 만드는 것과 정확히 같기 때문에 새 노래를 만들기 위해 코드를 제거했습니다.

songs 속성은 서버에서 반환된 데이터의 모든 아티스트 개체에 설정됩니다. 그것이 수행되는 정확한 메커니즘은 현재 논의에 거의 관심이 없습니다. 현재로서는 각 노래에 제목과 등급이 있다는 것만 아는 것으로 충분합니다.

제목은 템플릿에 직접 표시되며 등급은 StarRating 보기를 통해 별표로 표시됩니다. 이제 봅시다.

별표 평가 위젯

노래의 등급은 1에서 5 사이이며 App.StarRating 보기를 통해 사용자에게 표시됩니다. 뷰는 컨텍스트(이 경우 노래)와 컨트롤러에 액세스할 수 있습니다. 이것은 그들이 속성을 읽고 수정할 수 있음을 의미합니다. 이는 전달된 내용에만 액세스할 수 있는 격리되고 재사용 가능한 컨트롤인 다른 Ember 빌딩 블록과 대조됩니다. (이 예에서도 별표 평가 구성 요소를 사용할 수 있습니다.)

사용자가 별 중 하나를 클릭할 때 뷰가 별 수를 표시하고 노래의 등급을 설정하는 방법을 살펴보겠습니다.

 App.StarRating = Ember.View.extend({ classNames: ['rating-panel'], templateName: 'star-rating', rating: Ember.computed.alias('context.rating'), fullStars: Ember.computed.alias('rating'), numStars: Ember.computed.alias('maxRating'), stars: function() { var ratings = []; var fullStars = this.starRange(1, this.get('fullStars'), 'full'); var emptyStars = this.starRange(this.get('fullStars') + 1, this.get('numStars'), 'empty'); Array.prototype.push.apply(ratings, fullStars); Array.prototype.push.apply(ratings, emptyStars); return ratings; }.property('fullStars', 'numStars'), starRange: function(start, end, type) { var starsData = []; for (i = start; i <= end; i++) { starsData.push({ rating: i, full: type === 'full' }); }; return starsData; }, (...) });

rating , fullStarsnumStars 는 이전에 ArtistsControllerdisabled 속성으로 논의한 계산된 속성입니다. 위에서 나는 Ember에 정의된 약 12가지의 소위 계산된 속성 매크로를 사용했습니다. 그들은 일반적인 계산 속성을 더 간결하고 오류가 발생하기 쉬운(작성하기 위해) 만듭니다. 나는 rating 을 컨텍스트(따라서 노래)의 등급으로 설정하고 fullStarsnumStars 속성을 모두 정의하여 별 등급 위젯의 컨텍스트에서 더 잘 읽을 수 있도록 했습니다.

stars 방식이 주요 매력입니다. 각 항목에 rating 속성(1에서 5까지)과 별이 가득 찼는지 여부를 나타내는 플래그( full )가 포함된 별에 대한 데이터 배열을 반환합니다. 이렇게 하면 템플릿에서 매우 간단하게 살펴볼 수 있습니다.

 <script type="text/x-handlebars" data-template-name="star-rating"> {{#each view.stars}} <span {{bind-attr data-rating=rating}} {{bind-attr class=":star-rating :glyphicon full:glyphicon-star:glyphicon-star-empty"}} {{action "setRating" target=view}}> </span> {{/each}} </script>

이 스니펫에는 몇 가지 참고 사항이 포함되어 있습니다.

  1. 먼저, each 도우미는 속성 이름 앞에 view 를 붙여서 뷰 속성(컨트롤러의 속성과 반대)을 사용하도록 지정합니다.
  2. 둘째, span 태그의 class 속성에는 동적 및 정적 클래스가 혼합되어 할당됩니다. : 접두사가 붙은 모든 것은 정적 클래스가 되는 반면 full:glyphicon-star:glyphicon-star-empty 표기법은 JavaScript의 삼항 연산자와 같습니다. 전체 속성이 truthy이면 첫 번째 클래스가 할당되어야 합니다. 그렇지 않은 경우 두 번째.
  3. 마지막으로 태그를 클릭하면 setRating 작업이 시작되어야 하지만 Ember는 새 아티스트를 만드는 경우처럼 경로나 컨트롤러가 아니라 뷰에서 이를 조회합니다.

따라서 작업은 보기에서 정의됩니다.

 App.StarRating = Ember.View.extend({ (...) actions: { setRating: function() { var newRating = $(event.target).data('rating'); this.set('rating', newRating); } } });

템플릿에 할당한 rating 데이터 속성에서 등급을 얻은 다음 이를 노래 rating 으로 설정합니다. 새 등급은 백엔드에서 유지되지 않습니다. 아티스트를 생성한 방법을 기반으로 이 기능을 구현하는 것은 어렵지 않을 것이며 동기 부여된 독자를 위한 연습으로 남겨집니다.

모든 것을 포장

앞서 언급한 Ember 케이크의 여러 재료를 맛보았습니다.

  • 우리는 라우트가 Ember 애플리케이션의 핵심인 방법과 이름 지정 규칙의 기초 역할을 하는 방법을 보았습니다.
  • 양방향 데이터 바인딩과 계산된 속성이 모델 데이터를 단일 정보 소스로 만들고 직접적인 DOM 조작을 방지하는 방법을 보았습니다.
  • 그리고 여러 가지 방법으로 작업을 실행하고 처리하는 방법과 HTML의 일부가 아닌 컨트롤을 만드는 사용자 지정 보기를 만드는 방법을 보았습니다.

아름답죠?

추가 읽기(및 시청)

Ember에게는 이 게시물에 담을 수 있는 것보다 훨씬 더 많은 것이 있습니다. 위 응용 프로그램의 좀 더 개발된 버전을 구축한 방법에 대한 스크린캐스트 시리즈를 보고 및/또는 Ember에 대해 자세히 알아보려면 내 메일링 리스트에 등록하여 매주 기사나 팁을 받을 수 있습니다.

Ember.js에 대해 더 많이 배우고자 하는 당신의 욕구를 자극하고 이 게시물에서 사용한 샘플 애플리케이션을 훨씬 뛰어넘기를 바랍니다. Ember.js에 대해 계속 배우면서 Ember 데이터에 대한 기사를 살펴보고 ember-data 라이브러리를 사용하는 방법을 배우십시오. 즐겁게 건설하세요!

관련 항목: Ember.js 및 개발자가 저지르는 가장 일반적인 8가지 실수