AngularJS Phonecat (步驟13-步驟14)--完結(jié)篇

導(dǎo)言


最近在學(xué)AngularJS的實例教程PhoneCat Tutorial App,發(fā)現(xiàn)網(wǎng)上的中文教程都比較久遠(yuǎn),與英文版對應(yīng)不上,而且缺少組件和文件重構(gòu)兩節(jié)。所以決定自己整理一個中文簡明教程。

此篇為13-14節(jié)。

0-5節(jié):AngularJS Phonecat (步驟0-步驟5)
6-7節(jié):AngularJS Phonecat (步驟6-步驟7)
8-9節(jié):AngularJS Phonecat (步驟8-步驟9)
10-12節(jié):AngularJS Phonecat (步驟10-步驟12)

13 REST與定制服務(wù)


在這一步,我們會改變程序獲取數(shù)據(jù)的方法:
我們會自定義一個代表RESTful客戶端的服務(wù)。通過這個客戶端,我們可以更便捷地請求服務(wù)器數(shù)據(jù),不需要處理底層的$httpAPI,HTTP方法以及URL。

REST在英語原文中未多做介紹,筆者在網(wǎng)上搜索了相關(guān)資料,推薦以下內(nèi)容:
深入淺出REST
RESTful API 設(shè)計指南

依賴

RESTful功能由Angular的ngResource模塊提供,該模塊獨(dú)立于Angular框架的核心模塊,需要單獨(dú)安裝和引入。

前面我們使用Bower安裝客戶端依賴,這一步就更新bower.json配置以安裝新的依賴:

bower.json:

{
  "name": "angular-phonecat",
  "description": "A starter project for AngularJS",
  "version": "0.0.0",
  "homepage": "https://github.com/angular/angular-phonecat",
  "license": "MIT",
  "private": true,
  "dependencies": {
    "angular": "1.5.x",
    "angular-mocks": "1.5.x",
    "angular-resource": "1.5.x", //增加ngResource模塊
    "angular-route": "1.5.x",
    "bootstrap": "3.3.x"
  }
}

更新了bower.json,我們就可以用命令行安裝新模塊:

npm install

注意:如果你是在全局環(huán)境中安裝bower,你可以使用bower install進(jìn)行安裝。而這個項目我們已經(jīng)預(yù)配使用npm install來運(yùn)行bower install。

服務(wù)

我們創(chuàng)建了用于獲取服務(wù)器上手機(jī)數(shù)據(jù)的服務(wù)。我們會把該服務(wù)放到ngResource模塊中,并將該模塊放入核心模塊的依賴列表中。

app/core/phone/phone.module.js (核心模塊):

angular.module('core.phone', ['ngResource']);

app/core/phone/phone.service.js (獲取手機(jī)數(shù)據(jù)的服務(wù)):

angular.
  module('core.phone').
  factory('Phone', ['$resource',
    function($resource) {
      return $resource('phones/:phoneId.json', {}, {
        query: {
          method: 'GET',
          params: {phoneId: 'phones'},
          isArray: true
        }
      });
    }
  ]);

我們使用模塊API的factory()函數(shù)注冊了一個自定義服務(wù)。并使用'Phone'來代表這個服務(wù),調(diào)用factory()函數(shù)。factory()函數(shù)類似于一個控制器的構(gòu)造函數(shù),通過函數(shù)參數(shù)可以聲明依賴注入。Phone服務(wù)聲明了對$resource服務(wù)功能的依賴。

$resource服務(wù)只需要幾行代碼就能創(chuàng)建一個RESTful客戶端。這個客戶端可以替代低層級的$http服務(wù)。

app/core/core.module.js:

angular.module('core', ['core.phone']);

我們需要增添core.phone模塊作為核心模塊的依賴。

模板

我們在app/core/phone/phone.service.js中定制resource服務(wù),所以需要在布局模板中引入這個文件和關(guān)聯(lián)文件.module.js 。另外,我們也要加載angular-resource.js,它包含了ngRsource模塊。

app/index.html:

<head>
  ...
  <script src="bower_components/angular-resource/angular-resource.js"></script>
  ...
  <script src="core/phone/phone.module.js"></script>
  <script src="core/phone/phone.service.js"></script>
  ...
</head>

組件控制器

通過factory()函數(shù),我們可以用Phone服務(wù)替代低層級的$http服務(wù),這樣就簡化了組件控制器(PhoneListController 和 PhoneDetailController)。Angular的$resource服務(wù)利用RESTful資源,提供了比$http簡便的數(shù)據(jù)資源交互。現(xiàn)在,我們也更容易了解控制器的代碼是如何工作的。

app/phone-list/phone-list.module.js 手機(jī)列表模塊:

angular.module('phoneList', ['core.phone']);

app/phone-list/phone-list.component.js 手機(jī)列表組件:

angular.
  module('phoneList').
  component('phoneList', {
    templateUrl: 'phone-list/phone-list.template.html',
    controller: ['Phone',
      function PhoneListController(Phone) {
        this.phones = Phone.query();  //變化點(diǎn)
        this.orderProp = 'age';
      }
    ]
  });

app/phone-detail/phone-detail.module.js 手機(jī)詳情模塊:

angular.module('phoneDetail', ['core.phone']);

app/phone-detail/phone-detail.component.js 手機(jī)詳情組件:

angular.
  module('phoneDetail').
  component('phoneDetail', {
    templateUrl: 'phone-detail/phone-detail.template.html',
    controller: ['$routeParams', 'Phone',
      function PhoneDetailController($routeParams, Phone) {
        var self = this;
        self.phone = Phone.get({phoneId: $routeParams.phoneId}, function(phone) {
          self.setImage(phone.images[0]);
        });

        self.setImage = function setImage(imageUrl) {
          self.mainImageUrl = imageUrl;
        };
      }
    ]
  });

注意在手機(jī)列表控制器中,我們將

$http.get('phones/phones.json').then(function(response) {
  self.phones = response.data;
});

替換成:

this.phones = Phone.query();

這是一個簡單的聲明:我們需要查詢所有手機(jī)。

注意:在上面代碼中,調(diào)用Phone服務(wù)方法時,沒有傳遞回調(diào)函數(shù)。雖然這看起來就像同步獲得了返回值,但實際并非如此。同步返回的是一個對象"future",在接收到XHR響應(yīng)時,數(shù)據(jù)才會填充到"future"對象中。由于Angular的數(shù)據(jù)綁定,我們可以將該"future"對象綁定到模板上。這樣,當(dāng)數(shù)據(jù)返回時,視圖就會自動更新。

有時,依靠future對象和數(shù)據(jù)綁定不能很好地滿足我們的需求。所以,我們添加了回調(diào)函數(shù)來處理服務(wù)器響應(yīng)。比如,手機(jī)詳情組件的控制器就在回調(diào)函數(shù)中設(shè)置mainImageUrl。

測試

我們使用了ngResource模塊,所以需要更新Karma配置文件。

karma.conf.js:

files: [
  'bower_components/angular/angular.js',
  'bower_components/angular-resource/angular-resource.js',
  ...
],

我們增加一個單元測試驗證新服務(wù)是否能正確發(fā)出HTTP請求并返回預(yù)期的"future"對象/數(shù)組。

$resource服務(wù)擴(kuò)充了響應(yīng)對象:使用額外方法(如更新和刪除資源)、利用屬性(其中一些只能由Angular訪問)。如果我們使用Jasmine的.toEqual()進(jìn)行匹配,測試將會失敗, 這是因為測試值不會與響應(yīng)指完全匹配。

為了解決這個問題,我們使用自定義的等價測試用比較對象。自定義等價測試即angular.equals,它會忽略方法和帶$-前綴的屬性,比如由$resource服務(wù)注入的屬性(記住,Angular的專有API會使用$前綴)。

app/core/phone/phone.service.spec.js:

describe('Phone', function() {
  ...
  var phonesData = [...];

  // 每次測試前增加自定義等價測試
  beforeEach(function() {
    jasmine.addCustomEqualityTester(angular.equals);
  });

  // 每次測試前加載包含`Phone`服務(wù)的功能模塊
  ...

  // 每次測試前實例化服務(wù)和`$httpBackend`
  ...

  // 每次測試后確認(rèn)沒有其他期望或請求。
  afterEach(function () {
    $httpBackend.verifyNoOutstandingExpectation();
    $httpBackend.verifyNoOutstandingRequest();
  });

  it('should fetch the phones data from `/phones/phones.json`', function() {
    var phones = Phone.query();

    expect(phones).toEqual([]);

    $httpBackend.flush();
    expect(phones).toEqual(phonesData);
  });

});

這里,我們使用$httpBackend的verifyNoOutstandingExpectation() 和verifyNoOutstandingRequest()方法驗證所有預(yù)期的請求成功發(fā)送且后續(xù)沒有其他的請求
注意:我們還修改了組件測試,在適當(dāng)?shù)臅r候使用自定義匹配。

現(xiàn)在,你會看到命令窗口輸出下面信息:

Chrome 49.0: Executed 5 of 5 SUCCESS (0.123 secs / 0.104 secs)

14 動畫


在最后一節(jié),我們要在模板代碼中增加CSS和JS實現(xiàn)動畫效果,增強(qiáng)我們的web程序。
我們使用ngAnimate模塊實現(xiàn)動畫。Angular內(nèi)置指令通過鉤子(hooks)來觸發(fā)動畫,對應(yīng)的DOM元素會執(zhí)行操作,例如利用ngRepeat插入/刪除節(jié)點(diǎn),利用ngClass添加/移除類。

依賴

動畫功能由Angular的ngAnimate模塊提供,它獨(dú)立于Angular框架核心。另外,我們會用jQuery來實現(xiàn)JavaScript動畫。
這一步我們會更新bower.json配置文件來包含新的依賴關(guān)系:

bower.json:

{
  "name": "angular-phonecat",
  "description": "A starter project for AngularJS",
  "version": "0.0.0",
  "homepage": "https://github.com/angular/angular-phonecat",
  "license": "MIT",
  "private": true,
  "dependencies": {
    "angular": "1.5.x",
    "angular-animate": "1.5.x",//新的依賴,動畫模塊
    "angular-mocks": "1.5.x",
    "angular-resource": "1.5.x",
    "angular-route": "1.5.x",
    "bootstrap": "3.3.x",//bootstrap
    "jquery": "2.2.x" //jquery
  }
}

我們配置"angular-animate"為 "1.5.x"版本,"jquery"為 "2.2.x"版本。這里引入的jQuery并不是Angular函數(shù)庫,而是標(biāo)準(zhǔn)的jQuery庫。我們可以使用bower安裝各種第三方函數(shù)庫。
現(xiàn)在我們就讓bower下載和安裝依賴:

npm install

如何利用ngAnimate實現(xiàn)動畫

請參閱 Animations

模板

為了實現(xiàn)動畫,我們需要更新index.html,加載必要的依賴(angular-animate.js 和 jquery.js)、CSS和JS文件。ngAnimate包含了程序使用動畫的必要代碼。

app/index.html:

...

<!-- 引入CSS-->
<link rel="stylesheet" href="app.animations.css" />

...

<!-- 用于JS動畫,在angular.js之前引入-->
<script src="bower_components/jquery/dist/jquery.js"></script>

...

<!-- 增加AngularJS的動畫支持-->
<script src="bower_components/angular-animate/angular-animate.js"></script>

<!-- 定義JS動畫 -->
<script src="app.animations.js"></script>

...

重要提醒:在Angular 1.5中必須要使用jQuery 2.1以上版本,jQuery1.x版本不被正式支持的。一定要在所有AngularJS腳本之前加載jQuery,否則AngularJS可能無法檢測到j(luò)Query并利用jQuery的方法。

動畫通過CSS代碼(app.animations.css)和JS代碼(app.animations.js)創(chuàng)建。在此之前我們需要創(chuàng)建一個ngAnimate模塊。

依賴

我們需要在主模塊中增加一個ngAnimate依賴:

app/app.module.js:

angular.
  module('phonecatApp', [
    'ngAnimate',
    ...
  ]);

現(xiàn)在我們的程序就可以應(yīng)用動畫了,讓我們來寫些有趣的動畫。

CSS過渡動畫:Animating ngRepeat(用于手機(jī)列表頁面)

對于phoneList組件模板,我們會把CSS過渡動畫添加到ngRepeat指令中。我們需要給重復(fù)元素增加一個CSS類,這樣就可以將它與CSS動畫代碼掛鉤。

app/phone-list/phone-list.template.html:

...
<ul class="phones">
  //新增class
  <li ng-repeat="phone in $ctrl.phones | filter:$ctrl.query | orderBy:$ctrl.orderProp"
      class="thumbnail phone-list-item">
    <a href="#!/phones/{{phone.id}}" class="thumb">
      <img ng-src="{{phone.imageUrl}}" alt="{{phone.name}}" />
    </a>
    <a href="#!/phones/{{phone.id}}">{{phone.name}}</a>
    <p>{{phone.snippet}}</p>
  </li>
</ul>
...

注意我們是怎么添加phone-list-item類的,這是我們要實現(xiàn)動畫所需的HTML代碼。

CSS過渡動畫代碼:

app/app.animations.css:

.phone-list-item.ng-enter,
.phone-list-item.ng-leave,
.phone-list-item.ng-move {
  transition: 0.5s linear all;
}

.phone-list-item.ng-enter,
.phone-list-item.ng-move {
  height: 0;
  opacity: 0;
  overflow: hidden;
}

.phone-list-item.ng-enter.ng-enter-active,
.phone-list-item.ng-move.ng-move-active {
  height: 120px;
  opacity: 1;
}

.phone-list-item.ng-leave {
  opacity: 1;
  overflow: hidden;
}

.phone-list-item.ng-leave.ng-leave-active {
  height: 0;
  opacity: 0;
  padding-bottom: 0;
  padding-top: 0;
}

正如你看到的,phone-list-item類通過下面幾個類來觸發(fā)動畫鉤子,實現(xiàn)顯示/隱藏元素的動畫:

  • ng-enter,用于顯示一個新加入頁面的手機(jī)元素。
  • ng-move,用于改變手機(jī)元素位置。
  • ng-leave,用于從頁面移除一個手機(jī)元素。

手機(jī)列表根據(jù)ng-repeat指令添加或者刪除元素。比如,轉(zhuǎn)換器數(shù)據(jù)改變,則列表中項目會有添加和刪除手機(jī)項目的動畫。

需要特別注意的是,當(dāng)動畫發(fā)生時,兩套CSS類會被加入元素:

  • "starting"類,代表動畫的開始樣式
  • "active"類,代表動畫的結(jié)束樣式

starting類會觸發(fā)一些帶ng-前綴的事件(例如enter、move、leve),enter事件就會讓元素增加ng-enter類。

active類會觸發(fā)一些帶-active后綴的事件。

這兩套CSS類允許開發(fā)者指定動畫的實現(xiàn),是開始還是結(jié)束。

上面的例子中,在列表添加手機(jī)項目時,元素的高度會從0px變?yōu)?20px;當(dāng)刪除手機(jī)項目時,元素高度則從120px變?yōu)?px,同時有淡入淡出的效果。這些都是由CSS過渡動畫實現(xiàn)的。

盡管許多現(xiàn)代瀏覽器都能很好的支持CSS過渡和CSS動畫,但I(xiàn)E9及以前版本是不支持的。如果你想對兼容老瀏覽器,可以使用JavaScript動畫,我們會在后面講到。

CSS關(guān)鍵幀動畫:Animating ngView

這一步,我們要在ngView中增加切換動畫。

在HTML模板中添加新的CSS類到ng-view元素中。為了讓動畫更具表現(xiàn)力,我們還需將ng-view元素放入contianer元素中。

app/index.html:

<div class="view-container">
  <div ng-view class="view-frame"></div>
</div>

將CSS樣式用.view-container包裹起來,這樣我們會更容易在動畫過程中改變.view-frame元素的位置。

一切準(zhǔn)備就緒,我們可以增加過渡動畫的CSS樣式了。

app/app.animations.css:

...

.view-container {
  position: relative;
}

.view-frame.ng-enter,
.view-frame.ng-leave {
  background: white;
  left: 0;
  position: absolute;
  right: 0;
  top: 0;
}

.view-frame.ng-enter {
  animation: 1s fade-in;
  z-index: 100;
}

.view-frame.ng-leave {
  animation: 1s fade-out;
  z-index: 99;
}

@keyframes fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes fade-out {
  from { opacity: 1; }
  to   { opacity: 0; }
}
/* 舊版本瀏覽器需要在幀和動畫前面加前綴*/

代碼并不復(fù)雜,只是實現(xiàn)簡單的淡入淡出效果。比較特別的是,我們使用絕對定位將新頁面(有ng-enter類的標(biāo)識)放到舊頁面(有ng-leave類的標(biāo)識)的上方。當(dāng)舊頁面淡出時,新頁面也會淡入(而下一個頁面也放到了新頁面的上方)。

當(dāng)?shù)鰟赢嫿Y(jié)束時,該元素就重DOM樹中移除了。而淡入動畫完成時,ng-enter和ng-enter-active類都會從該元素中刪除,讓該元素以默認(rèn)CSS樣式重繪、回流(即不再有絕對定位樣式)。這個過程很流暢,讓頁面在路由改變時自然地切換,不會有跳躍感。

在ngRepeat中使用這些CSS類也是一樣的。每次頁面加載,ngView都會創(chuàng)建一個副本,下載模板并添加內(nèi)容。這就保證所有視圖都包含在一個HTML元素中,也更容易實現(xiàn)動畫控制。

JavaScript實現(xiàn)ngClass動畫(用于手機(jī)詳情頁面)

在phone-detail.template.html視圖,我們有一個不錯的縮略圖切換效果:點(diǎn)擊縮略圖,手機(jī)大圖進(jìn)行切換?,F(xiàn)在我們需要給它加一個動畫效果。

先想一下整體過程:當(dāng)用戶點(diǎn)擊縮略圖,大圖就切換最新點(diǎn)擊的圖片。而在HTML中改變圖片狀態(tài)的最好方式是使用CSS類。就像前面一樣,我們可以用一個CSS類來驅(qū)動動畫,這一次會在CSS類改變時進(jìn)行動畫。每當(dāng)選中一個手機(jī)縮略圖,.selected類就會添加到匹配的圖片上,并播放動畫。

首先,修改phone-detail.template.html中的HTML代碼。注意,我們改變了顯示大圖的方式:

app/phone-detail/phone-detail.template.html:

<div class="phone-images">
  <img ng-src="{{img}}" class="phone"
      ng-class="{selected: img === $ctrl.mainImageUrl}"
      ng-repeat="img in $ctrl.phone.images" />
</div>
...

和縮略圖一樣,我們用一個迭代器顯示所有的概要文件列表。但是我們沒有重復(fù)關(guān)聯(lián)動畫。相反的,我們會著眼與每個元素的類,特別是selected類,因為該類決定了元素處于可見/不可見狀態(tài)。selected類由ngClass指令管理,根據(jù)特定的條件(img === $ctrl.mainImageUrl)。在這個例子中,總會有一個元素是selected的,并顯示在視圖中。

當(dāng)一個元素添加selected類,在selected-add和selected-add-active類被添加之前,AngularJS會觸發(fā)一個動畫。當(dāng)selected類移除時,selected-remove 和 selected-remove-active類也會添加到該元素中,這樣就觸發(fā)了另一個動畫。

最后,為了確保頁面第一次加載時手機(jī)圖片可以正確顯示,我們也修改了詳情頁的CSS樣式:

app/app.css:

...

.phone {
  background-color: white;
  display: none;
  float: left;
  height: 400px;
  margin-bottom: 2em;
  margin-right: 3em;
  padding: 2em;
  width: 400px;
}

.phone:first-child {
  display: block;
}

.phone-images {
  background-color: white;
  float: left;
  height: 450px;
  overflow: hidden;
  position: relative;
  width: 450px;
}

...

你可能在想,我們是不是要創(chuàng)建另一個CSS動畫?好吧,雖然可以這么做,但我們還是看一下怎么使用.animation()方法創(chuàng)建基于JS的動畫吧。

app/app.animations.js:

angular.
  module('phonecatApp').
  animation('.phone', function phoneAnimationFactory() {
    return {
      addClass: animateIn,
      removeClass: animateOut
    };

    function animateIn(element, className, done) {  //注意:done參數(shù)
      if (className !== 'selected') return;

      element.
        css({
          display: 'block',
          position: 'absolute',
          top: 500,
          left: 0
        }).
        animate({
          top: 0
        }, done);

      return function animateInEnd(wasCanceled) {
        if (wasCanceled) element.stop();
      };
    }

    function animateOut(element, className, done) {
      if (className !== 'selected') return;

      element.
        css({
          position: 'absolute',
          top: 0,
          left: 0
        }).
        animate({
          top: -500
        }, done);

      return function animateOutEnd(wasCanceled) {
        if (wasCanceled) element.stop();
      };
    }
  });

我們將通過CSS類選擇器(.phone)和一個動畫工廠函數(shù)(phoneAnimationFactory())為目標(biāo)元素創(chuàng)建自定義動畫。工廠函數(shù)會返回一個對象,該對象關(guān)聯(lián)著特定事件(object keys)和動畫回調(diào)函數(shù)(object values)。事件的DOM操作由ngAnimate識別并鉤?。▓?zhí)行),如addClass/removeClass/setClass、 enter/move/leave 和動畫。相關(guān)的回調(diào)函數(shù)也由ngAnimate適時調(diào)用。

更多動畫工廠函數(shù)的信息,請查看API Reference.

例子中,當(dāng)一個元素通過ngClass指令添加了selected類時,會執(zhí)行animateIn()這個回調(diào)函數(shù),其中元素作為參數(shù)傳遞進(jìn)來。animateIn()函數(shù)的最后一個參數(shù)是done函數(shù)。調(diào)用done(),會通知Angular自定義的JS動畫已經(jīng)結(jié)束。移除seleted類時,則執(zhí)行animateOut()函數(shù),原理一樣。

注意,這里我們使用jQuery實現(xiàn)動畫。在AngularJ中實現(xiàn)動畫,jQuery并不是必須的,只是用原生JS實現(xiàn)動畫其實已經(jīng)超出了本教程的范圍。如需了解jQuery.animat請查看jQuery animate

通過事件回調(diào)函數(shù),我們操作DOM并創(chuàng)建了動畫。上面的代碼中,使用的是.css()和.element.animate()操作DOM。結(jié)果是,新圖片移動了500px,且前后兩張圖片同步移動500px,這樣就實現(xiàn)了傳送帶動畫。在animate()函數(shù)完成后,調(diào)用done函數(shù)通知Angular動畫結(jié)束。

你可能已經(jīng)注意到,每個動畫回調(diào)函數(shù)都返回了一個函數(shù),這是一個可選項。如果有設(shè)置這一項,當(dāng)動畫結(jié)束(完成/取消)時,它將被調(diào)用。該函數(shù)有一個布爾值參數(shù),用于讓開發(fā)者了解動畫是否被取消了。該函數(shù)常用于在動畫結(jié)束后執(zhí)行必要的清理工作。

結(jié)語


我們的程序已經(jīng)完成了。你可以使用 git checkout命令跳到某個步驟,隨意試驗?zāi)愕拇a。
更多的Angular概念,請參閱Developer Guide。
如果你準(zhǔn)備用AngularJS開發(fā)一個項目,建議你先從angular-seed項目開始。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,191評論 25 708
  • 本文從 這里 翻譯過來的。 2048這個游戲有一段時間特別火,Github上有其原始版本,游戲看起來很簡單,但是...
    江楓閱讀 1,600評論 2 7
  • 家禽生產(chǎn)中的理念更新與發(fā)展策略…… 養(yǎng)雞業(yè)經(jīng)過幾十年的發(fā)展,在廣大農(nóng)村經(jīng)歷了從散養(yǎng)到養(yǎng)殖專業(yè)戶,再到家庭化規(guī)模養(yǎng)殖...
    04534cdd7064閱讀 430評論 0 0
  • 時間,仿佛是越走越快了。 在這個繁忙的城市中,每個人都走得那么快,像是決心要在一代人的時間里活千百次。夾竹桃的花開...
    云笙丨寒楓閱讀 446評論 0 0
  • 林州市永和希望小學(xué)非常注重師生寫字水平的提升。對學(xué)生,不但開設(shè)了每周兩次的“寫字提升班”,而且讓學(xué)生堅持“每日午寫...
    甲午之印閱讀 90評論 0 0

友情鏈接更多精彩內(nèi)容