注:这是这个系列的第二部分,主要集中在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

1. Angular


作为一个新入坑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项目中,印象最为深刻的两个特点:

  • 在导航栏中,链接到其他页面不会触发页面重载(这也是SPA应用的一大特色);
  • 在不主动刷新的情况下,整个网站可以离线使用。

2. 图片分享系统(image-sharing-system)


这个项目完全是仿照着官方的“英雄指南”教程修改并添加了一些元素构成的。修改如下:

  • 将hero换成了user;
  • 每个user多了一个属性"files",包含该用户上传的图片的编号(一个数组);
  • 添加了图片相关的数据,与user的相关数据一起放在"in-memory-data.service.ts"文件中;
  • 添加了"image.service"用来处理与图片相关的数据;
  • 修改了"user-detail"组件,使得进入详情页后可以看到每个用户分享的图片;
  • 修改了"dashboard"组件,使得可以展示分享图片最多的4位用户;

图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

3. Angular CLI


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

4. 组件(component)


组件就是“一小块TypeScript代码 + 一小块html代码 + 一小块css代码“,每个组件相对来说都可以实现一个比较独立的功能。而前端最重要的功能就是内容的展示和与用户的交互。因此组件相当于将整个大任务进行了分解:每个组件都完成一小块任务,然后将这些组件拼在一起,就可以得到整个功能完整的网站。

比如在我自己写的这个小项目中,一共有5个组件(图3橙色部分):

  • app.component:由Angular CLI创建时自动生成,直接被添加到了index.html中,相当于所有组件的父组件,控制着整个网站的基本结构,也是路由的出口;
  • users.component:/users路由到这个视图,主要用于显示user列表,增加、删除用户;
  • dashboard.component:/dashboard路由到这个视图(根路由也被重定向到这个视图),展示分享图片最多的前4位用户,按用户名搜索;
  • user-search.component:dashboard组件的子组件,负责搜索相关事务;
  • user-detail.component:/detail/user-id,用于展示每个用户的详情;

上面的5个组件,app用于组织网站的结构,确定路由的出口;其他组件要么负责一个独立的页面,或者是一个页面的一部分。其他无论是module(例如路由模块,图3绿色部分)还是service(主要用于提供数据,图3紫色部分),都是为组件的正常工作提供支持的。因此可以说,组件是位于Angular框架的中心位置的。

4.1 组件的组成

利用CLI创建一个新的component后,默认会在app文件夹下生成一个文件夹,这个文件夹内包含四个文件(以users组件为例):

  • users.component.css:负责该组件的样式;
  • users.component.html:该组件的模板;
  • users.component.spec.ts:一般用不到;
  • users.component.ts:负责逻辑的处理,可以定义变量和函数,在模板中展示或调用;此外还可以导入相应的service,通过调用service的方法获取数据;

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来调用服务和路由中的方法了。

5. 服务(service)


服务是连接服务器端和组件的桥梁,使用单独的服务可以保持组件精简,服务可以通过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 服务的实现

在这部分实现了以下操作:

  • 获取所有user的数据(三种实现,直接返回一个数组、返回一个Promise对象、利用http返回一个Observable对象再转换成Promise对象);
  • 获取单个user的数据(两种实现,返回所有user的数据再根据id过滤、直接请求单个user的数据);
  • 修改某个user的信息:利用http的put方法修改服务器端数据,使得数据可以持久化;
  • 添加新的user:利用http的post方法,将数据添加到服务器端;
  • 删除已存在的user:利用http的delete方法,删除数据;
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中,可以看到返回的数据。

6. 一个模拟的服务器端


到目前为止,我们并没有真正的服务器端,我们的服务器端是利用"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框架

Reference


中文版英雄教程: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

记Angular与Django REST框架的一次合作(2):前端组件化——Angular的更多相关文章

  1. 记Angular与Django REST框架的一次合作(1):分离 or 不分离,it's the question

    前言:本次尝试源于我们内部的一个项目,由于前端逻辑比较复杂,就打算将前后端分开来开发.由于之前用Django开发过软件,对Angular.js(Angular 1.0版)也有一定的了解,因此就将技术路 ...

  2. [Android Pro] 终极组件化框架项目方案详解

    cp from : https://blog.csdn.net/pochenpiji159/article/details/78660844 前言 本文所讲的组件化案例是基于自己开源的组件化框架项目g ...

  3. client高性能组件化框架React简单介绍、特点、环境搭建及经常使用语法

    [本文源址:http://blog.csdn.net/q1056843325/article/details/54729657 转载请加入该地址] 明天就是除夕了 预祝大家新春快乐 [ ]~( ̄▽ ̄) ...

  4. Jhipster 一个Spring Boot + Angular/React 全栈框架

    Jhipster     一个Spring Boot + Angular/React 全栈框架: https://www.jhipster.tech/

  5. 教程:Visual Studio 中的 Django Web 框架入门

    教程:Visual Studio 中的 Django Web 框架入门 Django 是高级 Python 框架,用于快速.安全及可扩展的 Web 开发. 本教程将在 Visual Studio 提供 ...

  6. Django—— 缓存框架

    译者注:1.无用的,吹嘘的说辞不翻译:2.意译,很多地方不准确. 动态网站最为重要的一点就是好,网页是动态的.每一次用户请求页面,网站就要进行各种计算——从数据库查询,到render模板,到各种逻辑运 ...

  7. [oldboy-django][1初始django]web框架本质 + django框架 + ajax

    web框架本质 浏览器(socket客户端) - 发送请求(ip和端口,url http://www.baidu.com:80/index/) - GET 请求头(数据请求行的url上: Http1. ...

  8. 理解django的框架为何能够火起来

    理解django的框架为何能够火起来 https://www.yiibai.com/django/django_basics.html https://code.ziqiangxuetang.com/ ...

  9. Django ContentTypes框架使用场景

    Django contenttypes是一个非常有用的框架,主要用来创建模型间的通用关系(generic relation).不过由于其非常抽象, 理解起来并不容易.当你创建一个django项目的时候 ...

随机推荐

  1. 从String类型字符串的比较到StringBuffer和StringBuilder

    1. String类型 String类源码 为了从本质上理解String类型的特性所在,我们从String类型的源码看起,在源码中String类的注释中存在以下: /**Strings are con ...

  2. css3实现可以计算的自适应布局——calc()

    开始我们需要先了解什么是calc() ,calc()是一个CSS函数,你可以使用calc()给元素的margin.pading.width等属性设置 而且你还可以在一个calc()内部嵌套另一个cal ...

  3. PHP中利用redis实现消息队列处理高并发请求

    将请求存入redis 为了模拟多个用户的请求,使用一个for循环替代 //redis数据入队操作 $redis = new Redis(); $redis->connect('127.0.0.1 ...

  4. phpExcel读取excel文件数据

    require_once $_SERVER['DOCUMENT_ROOT'].'/Classes/PHPExcel.php';require_once $_SERVER['DOCUMENT_ROOT' ...

  5. 【LeetCode】225. Implement Stack using Queues

    题目: Implement the following operations of a stack using queues. push(x) -- Push element x onto stack ...

  6. 【LeetCode】217. Contains Duplicate

    题目: Given an array of integers, find if the array contains any duplicates. Your function should retu ...

  7. CSS 从入门到忘记

    CSS是Cascading Style Sheets的简称,中文称为层叠样式表,用来控制网页数据的表现,可以使网页的表现与数据内容分离. 一. css的三种引入方式 二. css的选择器(Select ...

  8. MyEclipse2014web工程项目直接复制不能访问报错处理方案

    在学习web servlet项目中,做一个项目时 , 完成到了某一阶段 实现了部分功能,有必要保存这一项目,当复制这个项目之后发现发布会报错或者不能访问. 其实复制项目主要是为了在以后的学习中如果能顺 ...

  9. Apache+Tomcat实现动静分离

    完成Tomcat集群搭建后,我们只需修改两.三处即可实现动静分离. 1.将原来httpd.conf中JkMount的路由规则都放入conf/extra/httpd-urimap.conf中: /*=l ...

  10. wget访问SOAP接口

    SOAP协议主要是XML交互,所以其访问过程类似于这样: wget --header='Content-Type: text/xml;charset=utf-8' --post-data='<s ...