注:这是这个系列的第二部分,主要集中在 Angular 的使用方面。之前使用过 AngularJS(Angular 1.x),混在 Django 的模板中使用,这些页面一般完全是结果展示页。在有 Django 表单输入的页面中,就很难将两者很好的结合起来。自己在学习新版的 Angular 时,跟了 2 遍官方网站的 "英雄指南" 教程。第 1 次完全是照搬,熟悉了一下基本概念;第 2 次自己做了一些修改,写了一个图片分享系统(只有一个雏形,还不是特别完善)。
推荐 IDE:Visual Studio Code
代码: github 地址(如果喜欢,记得 star 哦~)
第一部分:记 Angular 与 Django REST 框架的一次合作(1):分离 or 不分离,it's the question
作为一个新入坑 web 开发的人,本来一开始想选择一个轻量级的前端 + Django 来开发网站。开始比较了几个框架(主要是纠结于 React 和 Angular),最后还是选择了 Angular(当时用的是 1.x 版)。之所以作出这样的决定,在 Stock Overflow 中 Josh David Miller 对问题 "Thinking in AngularJS" if I have a jQuery background? 的回答对我的影响很大。其中有一句话我很认同。
Don't design your page, and then change it with DOM manipulations
上面的问题是比较 Angular 和 jQuery 的,但是同样适用从某些方面来比较 Angualr 和 React:Angular 是以 html 为中心的,从某种程度上可以看做是对 html 的一种扩展;React 是以 JS 为中心的,在 JS 中混入了 html。由于是初学,当时发现如果没有先用 html 搭个架子,就完全搞不清楚网站的结构。
现在学习新版的 Angular,反而觉得与 React 更像了。不得不说,组件(Component)是一种恰到好处的组织代码的结构单元,而且感觉经过重新设计的 Angular 学习起来比 AngularJS 更容易上手,学习曲线也更加平滑(也许是因为之前的经验)。Angular 中有许多重要的基本概念,但是最重要的应该就是组件了。
1.1 突出的特点
目前了解到的关于 Angualr 项目中,印象最为深刻的两个特点:
这个项目完全是仿照着官方的 "英雄指南" 教程修改并添加了一些元素构成的。修改如下:
图 1:http://localhost:4201/dashboard 视图
图 2-1:http://localhost:4201/users 视图
图 2-2:http://localhost:4201/detail/19 视图
整个项目实现的功能:进入主页(dashboard 视图,图 1),可以看到分享图片最多的 4 位用户,点击每位用户可以进入用户的详情页,在主页可以按照用户名搜索用户并进入详情页;点击导航栏中的 user,可以查看所有用户的列表(users 视图,图 2-1),点击每个 user,可以从下方的 View Details 进入该 user 的详情页(detail 视图,图 2-2)。在 users 视图中还可以添加、删除用户。
2.1 本地运行的方法
github:https://github.com/OnlyBelter/image-sharing-system
首先要安装 Node.js 和 Angular CLI,将项目 clone 到本地,然后运行下面的命令
- npm install
- ng serve --host 0.0.0.0 --port 4201
如果运行正常,就可以在浏览器中查看了,http://localhost:4201/dashboard
2.2 整个程序的结构
图 3:图片分享系统程序的结构,查看 pdf
CLI 是 Angular 的命令行接口,使用 CLI 可以通过命令行创建项目,创建新的组件,服务,模块等;而且可以用来实时编译,测试,发布。CLI 创建组件时(或服务等)会自动将组件 import 到 app.module.ts 文件中,并在 NgModule 中声明。创建一个新项目后的文件结构如下图:
图 4:Angular CLI 生成的文件结构(VS Code 中打开)
后面所有的修改都在 app 文件夹中,其他的文件一般不需要修改。
下面是一些常用的命令:
- # 安装cli
- npm install -g @angular/cli
- # 创建一个新项目
- ng new my-project
- cd my-project
- # 启动项目,可以实时编译
- ng serve --host 0.0.0.0 --port 4201
- # 创建新的component
- ng g component my-new-component
- # 创建新的服务
- ng g service my-new-service
CLI 的文档:https://github.com/angular/angular-cli/wiki
组件就是 "一小块 TypeScript 代码 + 一小块 html 代码 + 一小块 css 代码",每个组件相对来说都可以实现一个比较独立的功能。而前端最重要的功能就是内容的展示和与用户的交互。因此组件相当于将整个大任务进行了分解:每个组件都完成一小块任务,然后将这些组件拼在一起,就可以得到整个功能完整的网站。
比如在我自己写的这个小项目中,一共有 5 个组件(图 3 橙色部分):
上面的 5 个组件,app 用于组织网站的结构,确定路由的出口;其他组件要么负责一个独立的页面,或者是一个页面的一部分。其他无论是 module(例如路由模块,图 3 绿色部分)还是 service(主要用于提供数据,图 3 紫色部分),都是为组件的正常工作提供支持的。因此可以说,组件是位于 Angular 框架的中心位置的。
4.1 组件的组成
利用 CLI 创建一个新的 component 后,默认会在 app 文件夹下生成一个文件夹,这个文件夹内包含四个文件(以 users 组件为例):
4.2 组件中的构造函数(constructor)
组件的构造函数用来解决依赖注入,初始化服务或路由。其他变量的初始化不应该放在这里,而应该放在 ngOnInit 中。下面是 users 组件中的构造函数:
- // the constructor itself does nothing, the parameter simultaneously deinfes
- // a private userService property and identifies it as a UserService injection
- constructor(
- private userService: UserService, // 组件在构造函数中请求服务
- private router: Router // 在构造函数中注入Router
- ) { }
初始化服务和路由后,就可以在后面通过 this.userService 和 this.router 来调用服务和路由中的方法了。
服务是连接服务器端和组件的桥梁,使用单独的服务可以保持组件精简,服务可以通过 http 协议中的方法(get, post 等)向服务器请求资源或修改、添加资源。
服务也可以通过 CLI 直接创建,服务的标志是在 export 前有一个 @Injectable() 修饰符。当 TypeScript 看到 @Injectable() 装饰器时,就会记下本服务的元数据。 如果 Angular 需要往这个服务中注入其它依赖,就会使用这些元数据。像上面组件的构造函数中介绍的那样,服务可以注入到组件中,从而为组件提供数据服务。
5.1 承诺
服务总是异步的。Angular 的 http.get 返回一个 RxJS 的 Observable 对象。Observable(可观察对象)是一个管理异步数据流的强力方式。可以利用 toPromise 操作符把 Observable 转换成 Promise 对象。
一个 Observable 对象是一个数组,其中的元素随着时间的流逝异步地到达。 Observable 帮助我们管理异步数据,例如来自后台服务的数据。 Angular 自身使用了 Observable,包括 Angular 的事件系统和它的 http 客户端服务。为了使用 Observable, Angular 采用了名为 Reactive Extensions (RxJS) 的第三方包。
这部分应该是官方教程中最复杂的一块儿了。我打算后面单独写一篇博客,介绍这部分的内容。下面看一下我自己改写的项目中 user.service 的实现:
5.2 服务的实现
在这部分实现了以下操作:
- import { Injectable } from '@angular/core';
- import { Headers, Http } from "@angular/http";
- // 有很多像toPromise这样的操作符,用于扩展Observable,为其添加有用的能力
- import 'rxjs/add/operator/toPromise';
- import { USERS } from './mock-users';
- import { User } from "./user";
- @Injectable()
- export class UserService {
- private usersUrl = 'api/users';
- constructor(private http: Http) { }
- //UserService暴露了getUsers方法,返回跟以前一样的模拟数据,但它的消费者不需要知道这一点
- //服务是一个分离关注点,建议你把代码放到它自己的文件里
- getUsers(): Promise<User[]> {
- // return USERS; // 直接返回一个数组
- return Promise.resolve(USERS); // 返回一个Promise对象
- }
- // 延迟6s后返回
- getUsersSlowly(): Promise<User[]> {
- return new Promise(resolve => setTimeout(() => resolve(USERS), 6000));
- }
- // 返回所有user的数据再过滤
- getUser(id: number): Promise<User> {
- return this.getUsers()
- .then(rep => rep.find(user => user.id === id));
- }
- //Angular 的http.get返回一个 RxJS 的Observable对象
- getUsersByHttp(): Promise<User[]> {
- return this.http.get(this.usersUrl)
- .toPromise()
- .then(res => res.json().data as User[])
- .catch(this.handleError);
- }
- // 来发起一个 get-by-id 请求,直接请求单个user的数据
- getUserByHttp(id: number): Promise<User> {
- const url = `${this.usersUrl}/${id}`;
- return this.http.get(url)
- .toPromise()
- .then(res => res.json().data as User)
- .catch(this.handleError);
- }
- private headers = new Headers({'Content-Type': 'application/json'});
- // 使用 HTTP 的 put() 方法来把修改持久化到服务端
- update(user: User): Promise<User> {
- const url = `${this.usersUrl}/${user.id}`;
- return this.http.put(url, JSON.stringify(user), {headers: this.headers})
- .toPromise()
- .then(() => user) // ()
- .catch(this.handleError);
- }
- create(name: string): Promise<User> {
- return this.http
- .post(this.usersUrl, JSON.stringify({name: name}), {headers: this.headers})
- .toPromise()
- // 下面的.then方法对默认返回的数据进行了加工,得到了一个完整的User对象
- .then(res => res.json().data as User)
- .catch(this.handleError);
- }
- delete(id: number): Promise<void> {
- const url = `${this.usersUrl}/${id}`;
- return this.http.delete(url, {headers: this.headers})
- .toPromise()
- .then(() => null) // 什么也不返回
- .catch(this.handleError);
- }
- private handleError(error: any): Promise<any> {
- console.error('An error occurred', error); // for demo purposes only
- return Promise.reject(error.message || error);
- }
- }
5.3 http 请求与响应
在上面的代码中,每次调用 http.get(url),或其他 http 方法(post, put, delete),就相当于对相应的 url 发送了一次请求(Request)。发出这个请求后,收到请求的一方(一般是服务器端)总会给出一个响应(Response),这个响应可以是各种不同的形式。上面的 getUsersByHttp 方法中,就返回了一个 User[] 数组(由 res.json().data 得到),如果我们做一些修改:
- //Angular 的http.get返回一个 RxJS 的Observable对象
- getUsersByHttp(): Promise<User[]> {
- return this.http.get(this.usersUrl)
- .toPromise()
- // .then(res => res.json().data as User[])
- .then(res => res)
- .catch(this.handleError);
- }
现在返回的是一个原生态的 Response,如果在 users 组件中打印出这个 Response:
- getUsers() : void {
- // res是UserService返回的User数组,作为参数传递并赋值给组件的users属性
- // 使用.then(res => console.log(res))可以将res打印到终端
- this.userService.getUsersByHttp()
- // .then(res => this.users = res);
- .then(res = >console.log(res));
- }
我们可以看到下面的结果:
图 5:一个标准的 Response 类
我们可以看到 status 为 200,表示我们请求成功了。在_body 的 data 中,可以看到返回的数据。
到目前为止,我们并没有真正的服务器端,我们的服务器端是利用 "angular-in-memory-web-api" 模拟出来的一个内存数据库。因此数据只是保存到了内存,在不刷新的情况下,暂时做到了对数据的持久化。下面是 "in-memory-data.service.ts" 文件中的内容:
- import { InMemoryDbService } from 'angular-in-memory-web-api';
- export class InMemoryDataService implements InMemoryDbService {
- // 由于没有后端,这里创建了一个内存数据库来存放数据
- createDb() {
- let users = [
- { id: 11, name: 'Mr. Nice', files: [1, 2] },
- { id: 12, name: 'Narco', files: [32] },
- { id: 13, name: 'Bombasto', files: [11, 5] },
- { id: 14, name: 'Celeritas', files: [4, 12] },
- { id: 15, name: 'Magneta', files: [6] },
- { id: 16, name: 'RubberMan', files: [21] },
- { id: 17, name: 'Dynama', files: [3, 7, 9] },
- { id: 18, name: 'Dr IQ', files: [] },
- { id: 19, name: 'Magma', files: [10] },
- { id: 20, name: 'Tornado', files: [8, 13, 14, 16] }
- ];
- let images = [
- { id: 1, userId: 11, des: '', fileUrl: 'https://raw.githubusercontent.com/OnlyBelter/learn_neuralTalk/master/self_pic/img/butterfly1.jpg' },
- { id: 2, userId: 11, des: '', fileUrl: 'https://raw.githubusercontent.com/OnlyBelter/learn_neuralTalk/master/self_pic/img/cat1.jpg' },
- { id: 3, userId: 17, des: '', fileUrl: 'https://raw.githubusercontent.com/OnlyBelter/learn_neuralTalk/master/self_pic/img/cloud1.jpg' },
- { id: 4, userId: 14, des: '', fileUrl: 'https://raw.githubusercontent.com/OnlyBelter/learn_neuralTalk/master/self_pic/img/river1.jpg' },
- { id: 5, userId: 13, des: '', fileUrl: 'https://raw.githubusercontent.com/OnlyBelter/learn_neuralTalk/master/self_pic/img/flower1.jpg' },
- { id: 6, userId: 15, des: '', fileUrl: 'https://raw.githubusercontent.com/OnlyBelter/learn_neuralTalk/master/self_pic/img/disney1.jpg' },
- { id: 7, userId: 17, des: '', fileUrl: 'https://raw.githubusercontent.com/OnlyBelter/learn_neuralTalk/master/self_pic/img/cloud2.jpg' },
- { id: 8, userId: 20, des: '', fileUrl: 'https://raw.githubusercontent.com/OnlyBelter/learn_neuralTalk/master/self_pic/img/panda1.jpg' },
- { id: 9, userId: 17, des: '', fileUrl: 'https://raw.githubusercontent.com/OnlyBelter/learn_neuralTalk/master/self_pic/img/sunfei2.jpg' },
- { id: 10, userId: 19, des: '', fileUrl: 'https://raw.githubusercontent.com/OnlyBelter/learn_neuralTalk/master/self_pic/img/panda2.jpg' },
- { id: 11, userId: 13, des: '', fileUrl: 'https://raw.githubusercontent.com/OnlyBelter/learn_neuralTalk/master/self_pic/img/flower2.jpg' },
- { id: 12, userId: 14, des: '', fileUrl: 'https://raw.githubusercontent.com/OnlyBelter/learn_neuralTalk/master/self_pic/img/IMG_20161105_100414_A19.jpg' },
- { id: 13, userId: 20, des: '', fileUrl: 'https://raw.githubusercontent.com/OnlyBelter/learn_neuralTalk/master/self_pic/img/panda3.jpg' },
- { id: 14, userId: 20, des: '', fileUrl: 'https://raw.githubusercontent.com/OnlyBelter/learn_neuralTalk/master/self_pic/img/shanghai4.jpg' },
- { id: 16, userId: 20, des: '', fileUrl: 'https://raw.githubusercontent.com/OnlyBelter/learn_neuralTalk/master/self_pic/img/shanghai5.jpg' },
- { id: 21, userId: 16, des: '', fileUrl: 'https://raw.githubusercontent.com/OnlyBelter/learn_neuralTalk/master/self_pic/img/grass.jpg' },
- { id: 32, userId: 12, des: '', fileUrl: 'https://raw.githubusercontent.com/OnlyBelter/learn_neuralTalk/master/self_pic/img/lamp1.jpg' },
- ];
- return {users, images};
- }
- }
查看上面的代码,我们在内存数据库中定义了两个数据库 users 和 images;因此,我们可以在 service 中利用 http 协议中的动词(get, post, put, delete)通过 "api/users" 和 "api/images" 这两个 url 地址对这两数据库进行操作。在 service 中,对这种内存数据库的操作和对真正的利用 Django REST 框架搭建的 API 的操作是没有差别的。下个部分,我会尝试用 Django REST Framework 搭建一个可以替代 "angular-in-memory-web-api" 构建的内存数据库的,真正意义上的后端。
接下来...
第三部分:后端服务化——Django REST 框架
中文版英雄教程:https://angular.cn/tutorial
https://stackoverflow.com/a/15012542/2803344
https://angular.cn/guide/glossary#observable - 对象
https://stackoverflow.com/questions/35763730/difference-between-constructor-and-ngoninit/35763811#35763811
https://github.com/OnlyBelter/image-sharing-system
来源: http://www.cnblogs.com/Belter/p/7300704.html