原文地址,向原作者表示感谢。由于本人并没学习过angular,所以翻译有误的地方恳请指正。


我已经受够了JS社区的那帮二货,我只是想使用Django作为后端结合Angular做一个简单的、可复用的项目,却没有一个简明的指导教我如何做到。如果你也为此挣扎,这里有一份指南教你如何构建一个由Django作为后端、Webpack进行组织的Angular程序。

问题

我想开发一个Angular1.1.x的项目并使用Django作为后端提供数据,我喜欢使用Django REST Framework(DRF)来构造RESTful API。我也想打包需要的JavaScript资源。目前,我打算使用单一的服务器上运行这个程序。

依赖

  • Python 2.x
  • a virtual Python environment
  • Django 1.9.x (pip install django)
  • npm 2.15.8+
  • Webpack 1.13.x (sudo npm i -g webpack)
  • ESLint 2.13.1+ (sudo npm i -g eslint)
  • NodeJS 4.4.7+

0.设定虚拟环境

mkvirtualenv mysite
在这步,你需要确保安装了上面所有的依赖项,包括Django。

1.创建目录

我们在开发时候就需要聚焦于模块化,因此我们最终会用到许多目录,初始时我们的目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mysite
├── backend
│ ├── docs
│ ├── requirements
└── frontend
├── app
│ ├── components
│ └── shared
├── assets
│ ├── css
│ ├── img
│ ├── js
│ └── libs
├── config
├── dist
└── js

执行:

1
2
3
4
5
6
7
8
mkdir mysite && cd mysite
mkdir -p backend/docs/ backend/requirements/ \
frontend/app/shared/ \
frontend/app/components/ \
frontend/config \
frontend/assets/img/ frontend/assets/css/ \
frontend/assets/js/ frontend/assets/libs/ \
frontend/dist/js/

注意:这个项目结构的灵感来源几个不同的项目,我认为这个结构是十分理想的,但你并不必须按这种结构组织代码。不过你在跟随文档操作时最好保持这个结构以免出现问题。

2.创建Django项目

进入mysite/backend/目录创建Django项目:

1
python django-admin.py startproject mysite

并且创建在requirements/requirements.txt文件:

1
pip freeze > requirements/requirements.txt

mysite/backend/mysite/目录内创建存放API程序的目录:

1
2
3
4
mkdir -p applications/api/v1/
touch applications/__init__.py applications/api/__init__.py \
applications/api/v1/__init__.py applications/api/v1/routes.py \
applications/api/v1/serializers.py applications/api/v1/viewsets.py

接下来创建settings目录在mysite/backend/mysite/目录:

1
2
3
4
mkdir -p mysite/settings/
touch mysite/settings/__init__.py \
mysite/settings/base.py mysite/settings/dev.py mysite/settings/prod.py \
mysite/dev.env mysite/prod.env

3.设定Django所需的环境变量

在这步我喜欢使用django-environ来管理环境变量,当然有很多其他方法来做这件事,不过django-environ极大的简化了这个过程,所以我在所有的项目中都使用它。

安装

1
pip install django-environ

mysite/dev.env中添加:

1
2
3
4
DATABASE_URL=sqlite:///mysite.db
DEBUG=True
FRONTEND_ROOT=path/to/mysite/frontend/
SECRET_KEY=_some_secret_key

接下来我们将在settings中使用这些变量,这种在单独文件中配置特定变量的好处就是提供了切换运行环境的简单方法。在这里,dev.env文件是一个我们在本地开发时候使用的变量列表,同样的prod.env包含了生产环境所需的变量。

注意:SECRET_KEY可以在执行django-admin.py startproject后生成的settings.py中获取。

4.安装Django REST Framework并配置Django使用环境变量

安装

1
pip install djangorestframework

编写mysite/backend/mysite/settings/base.py文件:

告诉Django去哪里寻找环境变量

1
2
3
4
5
6
7
8
import environ
project_root = environ.Path(__file__) - 3
env = environ.Env(DEBUG=(bool, False),)
CURRENT_ENV = 'dev' # 'dev' is the default environment
# read the .env file associated with the settings that're loaded
env.read_env('./mysite/{}.env'.format(CURRENT_ENV))

配置数据库,这里我们使用django-environ内置的sqlite配置

1
2
3
DATABASES = {
'default': env.db()
}

指定SECRET_KEY和DEBUG选项

1
2
SECRET_KEY = env('SECRET_KEY')
DEBUG = env('DEBUG')

添加DRF到项目中

1
2
3
4
5
6
# Application definition
INSTALLED_APPS = [
...
# Django Packages
'rest_framework',
]

配置URL

1
ROOT_URLCONF = 'mysite.urls'

指定templates和静态资源目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
STATIC_URL = '/static/'
STATICFILES_FINDERS = [
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
]
STATICFILES_DIRS = [
env('FRONTEND_ROOT')
]
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [env('FRONTEND_ROOT')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]

根据TEMPLATES的配置,Django将在frontend/目录寻找模板文件,这里也是Angular项目存放的位置。我们提供入口指令让Django使用Angular提供的模板。如果你不明白我在说什么,继续往下看……

编写settings/dev.py

1
2
from mysite.settings.base import *
CURRENT_ENV = 'dev'

这里我们告诉Django这个配置文件继承自base.py并且覆盖了CURRENT_ENV变量,意思就是说“使用这里的变量值而不是base.py中的变量值。”

5.创建API

我们需要数据来测试Angular服务,所以来创建一个简单的API。你可以跳过这一节,但我不建议你这么做。完全的知道Angular的工作设置细节去理解HTTP请求是很重要的。

mysite/backend/mysite中创建一个Django App

1
2
mkdir applications/games
manage.py startapp games applications/games

添加games app到settings文件中

1
2
3
4
INSTALLED_APPS = [
...
'applications.games'
]

mysite/backend/mysite/applications/games/models.py中创建模型

1
2
3
class Game(models.model):
title = models.CharField(max_length=255)
description = models.CharField(max_length=750)

mysite/backend/mysite/applications/api/v1/serializers.py中创建serializer

1
2
3
4
5
6
7
8
from rest_framework.serializers import ModelSerializer
from applications.games.models import Game
class GameSerializer(ModelSerializer):
class Meta:
model = Game

mysite/backend/mysite/applications/api/v1/viewsets.py中创建DRF viewset

1
2
3
4
5
6
7
8
from rest_framework import viewsets
from applications.games.models import Game
from applications.api.v1.serializers import GameSerializer
class GameViewSet(viewsets.ModelViewSet):
queryset = Game.objects.all()
serializer_class = GameSerializer

利用DRF的路由注册特性来处理路由信息在mysite/backend/mysite/applications/api/v1/routes.py

1
2
3
4
5
6
from rest_framework import routers
from applications.api.v1.viewsets import GameViewSet
api_router = routers.SimpleRouter()
api_router.register('games', GameViewSet)

mysite/backend/mysite/mysite/urls.py添加url配置

1
2
3
4
5
6
7
8
9
from django.contrib import admin
from django.conf.urls import include, url
from applications.api.v1.routes import api_router
urlpatterns = [
url(r'^admin/', admin.site.urls),
# API:V1
url(r'^api/v1/', include(api_router.urls)),
]

6.运行Django服务

1
manage.py runserver --settings=mysite.settings.dev

通过DJANGO_SETTINGS_MODULE我们告诉runserver命令使用特定的配置文件。

如果一切正常,你可以访问localhost:8000/api/v1/games并看到DRF的响应,接下来我们开始构建Angular程序。如果你卡住了,在评论区提出你的问题,我将帮助你调试。

7.初始化npm包并安装前端JS依赖项

没正确安装依赖项时Angular程序不会正常工作,这里先安装一些所需的依赖。

初始化NPM包。在mysite/frontend/目录中执行

1
npm init --yes

通过--yes参数,告诉NPM命令根据默认选项生成package.json,这样就不用回答那些无聊的问题了。

安装开发依赖项

1
npm install --save-dev eslint eslint-loader

安装通用依赖项

1
npm install --save eslint eslint-loader angular angular-resource angular-route json-loader mustache-loader lodash

略微调整一下mysite/frontend/目录中的package.json文件,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
"name": "my-app",
"version": "0.0.1",
"description": "This is my first angular app.",
"main": "app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"eslint": "^3.1.1",
"eslint-loader": "^1.4.1"
},
"dependencies": {
"angular": "^1.5.8",
"angular-resource": "^1.5.8",
"angular-route": "^1.5.8",
"eslint": "^3.1.1",
"eslint-loader": "^1.4.1",
"json-loader": "^0.5.4",
"lodash": "^4.13.1",
"mustache-loader": "^0.3.1"
}
}
  • eslint - 语法检查器,保持javascript代码的整洁和一致。
  • eslint-loader - 通过Webpack运行eslint。我将会稍微解释一下“加载器”的概念。
  • angular - MVC框架,如果你不知道这个可以关闭页面了。
  • angular-resource - 一款angular的HTTP库,是$http的抽象。
  • json-loader - 一个加载器(同样的,给Webpack使用),通过require()语法来提取.json文件中的JSON格式内容。
  • mustache-loader - 一个加载器,用来解析mustache模板。mustache模板十分有趣。

这里我假定你不知道如何将上面的程序配合使用,别担心老兄。

8.创建Angular程序入口,声明依赖以及全局配置

mysite/frontend/app/app.js文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* Libs */
require("angular/angular");
require("angular-route/angular-route");
require("angular-resource/angular-resource");
/* Globals */
_ = require("lodash");
_urlPrefixes = {
API: "api/v1/",
TEMPLATES: "static/app/"
};
/* Components */
/* App Dependencies */
angular.module("myApp", [
"ngResource",
"ngRoute",
]);
/* Config Vars */
// @TODO in Step 13.
/* App Config */
angular.module("myApp").config(routesConfig);

app.js是Webpack寻找模块进行打包的入口点,我个人很欣赏这种声明的组织方式和函数调用方式,但这并不是必须的。这里有6部分:

  • Libs - 所需要的通用库
  • Globals - 所需要的全局变量
  • Components - 项目需要的模块
  • App Dependencies - 声明程序入口名以及依赖
  • Config Vars - 配置变量,比如路由配置
  • App Config - 我们程序所需要的变量配置

为了正常使用全局变量,你需要告诉ESLint哪些变量是全局的。编辑mysite/frontend/config/eslint.json文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{
"env": {
"node": true
},
"extends": "eslint:recommended",
"rules": {
"indent": [
"error",
2
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"double"
],
"semi": [
"error",
"always"
],
"no-console": 0
},
"globals": {
"_": true,
"_urlPrefixes": true,
"angular": true,
"inject": true,
"window": true
},
"colors": true
}

我们告诉ESLint这里有几个全局变量:

  • _代表lodash
  • _urlPrefixes是我们将在程序URL中使用的一个对象,我将晚些解释
  • angular代表我们的应用中整个AngularJS对象
  • inject将在Angular依赖注入时使用
  • window代表JavaScript的window对象,这里代表了DOM

9.配置Webpack

现在我们已经配置好了项目的依赖关系,可以创建Webpack配置文件了。Webpack将结合我们所依赖的所有文件和我们编写的模块进行打包。

编辑mysite/frontend/webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
module.exports = {
entry: "./app/app.js",
output: {
path: "./dist/js/",
filename: "bundle.js",
sourceMapFilename: "bundle.js.map",
},
watch: true,
// eslint config
eslint: {
configFile: './config/eslint.json'
},
module: {
preLoaders: [{
test: /\.js$/,
exclude: /node_modules/,
loader: "eslint-loader"
}],
loaders: [
{ test: /\.css$/, loader: "style!css" },
{ test: /\.html$/, loader: "mustache-loader" },
{ test: /\.json$/, loader: "json-loader" }]
},
resolve: {
extensions: ['', '.js']
}
};

为了让Webpack能够打包我们的静态依赖文件,我们需要告诉他去哪里获取这些文件、哪些文件需要被处理以及打包前如何管理它们。

让我们概览一下你通过webpack.config.js告诉Webpack些什么了:

  • entry告诉Webpack从什么位置开始构建,这可以是一个绝对路径也可以是一个相对于webpack.config.js的相对路径。这里我使用后者。
  • output是一个对象,其中path表示打包后文件存放的位置,filename则是文件名,这里我们还使用了sourceMapFilename来指明什么source map应该被调用。
  • watch告诉Webpack在它运行时监控文件的变动,如果没设置成true,Webpack将仅打包一次并退出。
  • eslint包含ESLint具体的配置,用于eslint-loader
  • module用于告诉Webpack如何处理模块。
  • module.preLoaders说明在打包前需要做什么,这里我们使用eslint去运行我们的模块。
  • module.loaders指定加载器的序列,这里我仅设定了testloadertest表明哪些模块需要运行加载器(通过正则表达式匹配文件名),loader则表明哪一个加载器将被使用。每个加载器都是字符串并且使用感叹号作为标记,比如loader!another_loader!yet_another_loader

但是老兄,preLoaders和loaders又特么的有什么区别??? 我假设你问了这个问题,爱骂人的朋友。

A preLoader 是在加载器运行之前执行的代码,这里我们检查JavaScript语法。

A loader 告诉Webpack如何打包 require() 文件。一个加载器就好象一个模块,说“当你打包这些文件到一个文件时,这些字符串应该如何转换”

A postLoader 是一个Webpack插件在打包完成后运行,为了简单这里我们没指定postLoader。

10.告诉Django加载App

现在,你已经配置了Webpack什么需要创建并如何创建了。(见鬼,此时此刻,如果你运行Webpack命令并且不报错,我会十分惊讶。如果真的成功了,我就是王八蛋。)

自从Django加强了URL管理后,我们常常在Django的摆布下管理用户在本地浏览器中访问的路径。然而我们现在使用另一个框架构建了一个单页面程序,并且希望拥有完全控制用户输入的能力,我们需要的仅仅是Django服务这个单页面程序正常运行。因此……

编辑mysite/backend/mysite/mysite/urls.py添加下列代码到urlpatterns

1
2
# Web App Entry
url(r'^$', TemplateView.as_view(template_name="app/index.html"), name='index'),

这意味着在用户访问mysite.com/时,env('FRONTEND_ROOT') + app/index.html将被STATICFILES_FINDERS找到并渲染成普通HTML模板。

11.创建Angular app模板

我们的mysite/frontend/app/components/app/index.html看起来就像一个常规的Django模板:

1
2
3
4
5
6
7
8
9
10
{% load staticfiles %}
<!DOCTYPE html>
<html ng-app="myApp">
<head>
<title>My Site</title>
<script src="{% static 'dist/js/bundle.js' %}"></script>
</head>
<body>
</body>
</html>

在此时,你应该能运行Webpack了。如果你运行了Django服务并访问localhost:8000将会看到一个空白页面。如果没有,请让我知道。

12.写一个home组件

让我们来编写第一个组件,它将在页面上展示文字当用户访问localhost:8000时。

创建components目录和基本文件在frontend/app/components/

1
mkdir home && touch home/home-controller.js home/home.js home/home.html

编辑mysite/frontend/app/components/home/home.html

1
2
3
4
5
<div ng-controller="HomeController as ctrl">
<div>
<h1>Home!</h1>
</div>
</div>

接下来编写mysite/frontend/app/components/home/home-controller.js文件:

1
2
3
4
5
6
7
8
9
10
function HomeController() {
var that = this;
that.foo = "Foo!";
console.log(that); // should print out the controller object
}
angular.module("Home")
.controller("HomeController", [
HomeController
]);

关于var that = this可以参考另一文章

Angular模块定义应该在home.js中被声明:

1
2
3
angular.module("Home", []);
require("./home-controller");

现在我们可以在定义模块依赖的地方指明”Home”了,让我们动手吧。

编辑mysite/frontend/app/app.js文件:

1
2
3
4
5
6
7
8
9
/* Components */
require("./components/home/home");
/* App Dependencies */
angular.module("myApp", [
"Home", // this is our component
"ngResource",
"ngRoute"
]);

13.为home组件和404页面编写路由

现在我们来配置路由,当用户访问localhost:8000时,Angular应该在Django渲染模板后接手控制。为了实现这一点,我们使用angular-router

编辑mysite/frontend/app/routes.js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function routesConfig($routeProvider) {
$routeProvider
.when("/", {
templateUrl: _urlPrefixes.TEMPLATES + "components/home/home.html",
label: "Home"
})
.otherwise({
templateUrl: _urlPrefixes.TEMPLATES + "404.html"
});
}
routesConfig.$inject = ["$routeProvider"];
module.exports = routesConfig;

如果我们不添加_urlPrefixes.TEMPLATES,angular-router将认为components/home/home.html是一个服务器提供的实际URL路径,因为Django中配置了STATIC_URLlocalhost:8000/components/home/home.html将不会正常工作。

同样,你应该注意到了otherwise({...})配置,这是用来控制404页面的。

编辑mysite/frontend/app/404.html

1
<h1>NOT FOUND</h1>

最终在mysite/frontend/app/app.js添加如下代码

1
2
/* Config Vars */
var routesConfig = require("./routes");

14.在入口页面添加angular-router指令

现在我们需要告诉Angular在用户点击导航时如何切换页面。为了做的这点,我们需要利用angular-router。

mysite/frontend/app/index.html的标签中添加下列代码

1
<base href="/">

在中添加

1
<div ng-view></div>

你的index.html看起来应该类似这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{% load staticfiles %}
<!DOCTYPE html>
<html ng-app="myApp">
<head>
<title>My Site</title>
<script src="{% static 'dist/js/bundle.js' %}" ></script>
<base href="/">
</head>
<body>
<div>
<div ng-view></div>
</div>
</body>
</html>

运行Webpack命令,访问localhost:8000,你应该看到home/home.html中的内容了。(如果没有让我知道)

15.结合REST API和Angular app

如果上面的都没问题,现在应该可以为Django API写Angular服务了。让我们来编写一个简单的组件,这个组件用来展示游戏列表,我假设你已经在数据库中添加了数据,访问localhost:8000/api/v1/games可以得到游戏列表。

frontend/app/components/中创建组件

1
mkdir -p game/list/ && touch game/list/game-list-controller.js game/list/game-list-controller_test.js game/game-service.js game/game.js game/game.html

编辑game/game-service.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function GameService($resource) {
/**
* @name GameService
*
* @description
* A service providing game data.
*/
var that = this;
/**
* A resource for retrieving game data.
*/
that.GameResource = $resource(_urlPrefixes.API + "games/:game_id/");
/**
* A convenience method for retrieving Game objects.
* Retrieval is done via a GET request to the ../games/ endpoint.
* @param {object} params - the query string object used for a GET request to ../games/ endpoint
* @returns {object} $promise - a promise containing game-related data
*/
that.getGames = function(params) {
return that.GameResource.query(params).$promise;
};
}
angular.module("Game")
.service("GameService", ["$resource", GameService]);

注意$resource的配置,这就是之前设定的HTTP请求的路径。

编辑game/list/game-list-controller.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function GameListController(GameService) {
var that = this;
/* Stored game objects. */
that.games = [];
/**
* Initialize the game list controller.
*/
that.init = function() {
return GameService.getGames().then(function(games) {
that.games = games;
});
};
}
angular.module("Game")
.controller("GameListController", [
"GameService",
GameListController
]);

编辑game/game.html

1
2
3
4
5
6
7
8
9
<div ng-controller="GameListController as ctrl" ng-init="ctrl.init()">
<div>
<h1>Games</h1>
<!-- Test List -->
<ul>
<li ng-repeat="game in ctrl.games">{{ game.title }}</li>
</ul>
</div>
</div>

编辑game/game.js

1
2
3
4
angular.module("Game", []);
require("./list/game-list-controller");
require("./game-service");

接下来修改app.js

1
2
3
4
5
6
7
8
9
10
/* Components */
require("./components/game/game");
/* App Dependencies */
angular.module("myApp", [
"Home",
"Game",
"ngResource",
"ngRoute"
]);

最终,我们还需要游戏列表配置路由,在mysite/frontend/app/routes.js文件中添加下面的$routeProvider对象;

1
2
3
4
.when("/game", {
templateUrl: _urlPrefixes.TEMPLATES + "components/game/list/game-list.html",
label: "Games"
})

再次运行webpack,所有编译都应该成功,如果没有,请让我知道。

访问localhost:8000/#/games你将看到游戏列表。


关于非主要部分这里我就不翻译了,各位可以去原博客中评论或提问。