├── Chapter-three ├── api_learn │ ├── api_learn │ │ ├── __init__.py │ │ ├── wsgi.py │ │ ├── urls.py │ │ └── settings.py │ ├── rest_learn │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ └── 0001_initial.py │ │ ├── tests.py │ │ ├── views.py │ │ ├── apps.py │ │ ├── admin.py │ │ └── models.py │ ├── db.sqlite3 │ ├── data.py │ ├── manage.py │ └── rest_test.py ├── online_python │ ├── backend │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ └── 0001_initial.py │ │ ├── tests.py │ │ ├── apps.py │ │ ├── admin.py │ │ ├── models.py │ │ ├── serializers.py │ │ └── views.py │ ├── online_python │ │ ├── __init__.py │ │ ├── wsgi.py │ │ ├── urls.py │ │ └── settings.py │ ├── db.sqlite3 │ ├── frontend │ │ ├── index.html │ │ └── index.js │ └── manage.py ├── imgs │ ├── 实例详情.png │ ├── 提交结果.png │ ├── post 示例.png │ ├── 正确使用组件.png │ ├── 绑定属性和事件.gif │ └── API ROOT.png ├── vue_learn │ ├── grammer.html │ ├── index.html │ └── index.js ├── Django REST 系列教程(三)(中).md ├── Django REST 系列教程(三)(上).md └── Django REST 系列教程(三)(下).md ├── Chapter-two ├── online_intepreter_project │ ├── online_intepreter_app │ │ ├── __init__.py │ │ ├── models.py │ │ ├── middlewares.py │ │ ├── views.py │ │ └── mixins.py │ ├── online_intepreter_project │ │ ├── __init__.py │ │ ├── settings.py │ │ └── urls.py │ ├── db.sqlite3 │ ├── frontend │ │ ├── css │ │ │ └── main.css │ │ ├── index.html │ │ └── js │ │ │ └── main.js │ └── manage.py ├── imgs │ └── demo.gif └── Django RESTful 系列教程(二)(上).md ├── Chapter-one ├── img │ ├── final.png │ ├── test-1.png │ ├── test-2.png │ ├── test-3.png │ ├── test-4.png │ ├── test-5.png │ ├── test-6.png │ ├── test-7.png │ ├── bootstrap-1.png │ ├── bootstrap-2.png │ ├── bootstrap-3.png │ ├── bootstrap-4.png │ ├── bootstrap-5.gif │ ├── bootstrap-6.gif │ ├── bootstrap-7.png │ └── bootstrap-8.png ├── test │ └── test.py └── online_python │ ├── online_app.py │ └── index.html ├── .gitignore ├── README.md └── Chapter-four └── Django RESTful 系列教程(四).md /Chapter-three/api_learn/api_learn/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Chapter-three/api_learn/rest_learn/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Chapter-three/online_python/backend/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Chapter-three/online_python/online_python/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Chapter-three/api_learn/rest_learn/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Chapter-three/online_python/backend/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Chapter-two/online_intepreter_project/online_intepreter_app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Chapter-two/online_intepreter_project/online_intepreter_project/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Chapter-one/img/final.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ucag/django-rest/HEAD/Chapter-one/img/final.png -------------------------------------------------------------------------------- /Chapter-two/imgs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ucag/django-rest/HEAD/Chapter-two/imgs/demo.gif -------------------------------------------------------------------------------- /Chapter-one/img/test-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ucag/django-rest/HEAD/Chapter-one/img/test-1.png -------------------------------------------------------------------------------- /Chapter-one/img/test-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ucag/django-rest/HEAD/Chapter-one/img/test-2.png -------------------------------------------------------------------------------- /Chapter-one/img/test-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ucag/django-rest/HEAD/Chapter-one/img/test-3.png -------------------------------------------------------------------------------- /Chapter-one/img/test-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ucag/django-rest/HEAD/Chapter-one/img/test-4.png -------------------------------------------------------------------------------- /Chapter-one/img/test-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ucag/django-rest/HEAD/Chapter-one/img/test-5.png -------------------------------------------------------------------------------- /Chapter-one/img/test-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ucag/django-rest/HEAD/Chapter-one/img/test-6.png -------------------------------------------------------------------------------- /Chapter-one/img/test-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ucag/django-rest/HEAD/Chapter-one/img/test-7.png -------------------------------------------------------------------------------- /Chapter-three/imgs/实例详情.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ucag/django-rest/HEAD/Chapter-three/imgs/实例详情.png -------------------------------------------------------------------------------- /Chapter-three/imgs/提交结果.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ucag/django-rest/HEAD/Chapter-three/imgs/提交结果.png -------------------------------------------------------------------------------- /Chapter-three/imgs/post 示例.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ucag/django-rest/HEAD/Chapter-three/imgs/post 示例.png -------------------------------------------------------------------------------- /Chapter-three/imgs/正确使用组件.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ucag/django-rest/HEAD/Chapter-three/imgs/正确使用组件.png -------------------------------------------------------------------------------- /Chapter-three/imgs/绑定属性和事件.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ucag/django-rest/HEAD/Chapter-three/imgs/绑定属性和事件.gif -------------------------------------------------------------------------------- /Chapter-one/img/bootstrap-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ucag/django-rest/HEAD/Chapter-one/img/bootstrap-1.png -------------------------------------------------------------------------------- /Chapter-one/img/bootstrap-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ucag/django-rest/HEAD/Chapter-one/img/bootstrap-2.png -------------------------------------------------------------------------------- /Chapter-one/img/bootstrap-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ucag/django-rest/HEAD/Chapter-one/img/bootstrap-3.png -------------------------------------------------------------------------------- /Chapter-one/img/bootstrap-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ucag/django-rest/HEAD/Chapter-one/img/bootstrap-4.png -------------------------------------------------------------------------------- /Chapter-one/img/bootstrap-5.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ucag/django-rest/HEAD/Chapter-one/img/bootstrap-5.gif -------------------------------------------------------------------------------- /Chapter-one/img/bootstrap-6.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ucag/django-rest/HEAD/Chapter-one/img/bootstrap-6.gif -------------------------------------------------------------------------------- /Chapter-one/img/bootstrap-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ucag/django-rest/HEAD/Chapter-one/img/bootstrap-7.png -------------------------------------------------------------------------------- /Chapter-one/img/bootstrap-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ucag/django-rest/HEAD/Chapter-one/img/bootstrap-8.png -------------------------------------------------------------------------------- /Chapter-three/api_learn/rest_learn/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /Chapter-three/imgs/API ROOT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ucag/django-rest/HEAD/Chapter-three/imgs/API ROOT.png -------------------------------------------------------------------------------- /Chapter-three/api_learn/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ucag/django-rest/HEAD/Chapter-three/api_learn/db.sqlite3 -------------------------------------------------------------------------------- /Chapter-three/api_learn/rest_learn/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /Chapter-three/online_python/backend/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /Chapter-three/online_python/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ucag/django-rest/HEAD/Chapter-three/online_python/db.sqlite3 -------------------------------------------------------------------------------- /Chapter-two/online_intepreter_project/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ucag/django-rest/HEAD/Chapter-two/online_intepreter_project/db.sqlite3 -------------------------------------------------------------------------------- /Chapter-three/online_python/backend/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class BackendConfig(AppConfig): 5 | name = 'backend' 6 | -------------------------------------------------------------------------------- /Chapter-three/api_learn/rest_learn/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class RestLearnConfig(AppConfig): 5 | name = 'rest_learn' 6 | -------------------------------------------------------------------------------- /Chapter-two/online_intepreter_project/frontend/css/main.css: -------------------------------------------------------------------------------- 1 | #code-input, #code-output { 2 | resize: none; 3 | font-size: 25px; 4 | } /*设置输入输出框的字体大小,禁用他们的 resize 功能*/ -------------------------------------------------------------------------------- /Chapter-three/api_learn/rest_learn/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import TestModel 3 | 4 | @admin.register(TestModel) 5 | class TestModelAdmin(admin.ModelAdmin): 6 | pass -------------------------------------------------------------------------------- /Chapter-three/online_python/backend/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Code 3 | 4 | @admin.register(Code) 5 | class CodeAdmin(admin.ModelAdmin): 6 | pass 7 | 8 | -------------------------------------------------------------------------------- /Chapter-three/online_python/backend/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | class Code(models.Model): 4 | name = models.CharField(max_length=20, blank=True) 5 | code = models.TextField() 6 | -------------------------------------------------------------------------------- /Chapter-three/vue_learn/grammer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Grammer 5 | 6 | 7 |
8 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /Chapter-three/api_learn/rest_learn/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | class TestModel(models.Model): 4 | name = models.CharField(max_length=20) 5 | code = models.TextField() 6 | created_time = models.DateTimeField(auto_now_add=True) 7 | changed_time = models.DateTimeField(auto_now=True) 8 | 9 | def __str__(self): 10 | return self.name -------------------------------------------------------------------------------- /Chapter-two/online_intepreter_project/online_intepreter_app/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | # 创建 Cdoe 模型 5 | class CodeModel(models.Model): 6 | name = models.CharField(max_length=50) # 名字最长为 50 个字符 7 | code = models.TextField() # 这个字段没有文本长度的限制 8 | 9 | def __str__(self): 10 | return 'Code(name={},id={})'.format(self.name,self.id) 11 | 12 | -------------------------------------------------------------------------------- /Chapter-three/vue_learn/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Vue-learn 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Chapter-three/online_python/backend/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import Code 3 | 4 | #创建序列化器 5 | class CodeSerializer(serializers.ModelSerializer): 6 | class Meta: 7 | model = Code 8 | fields = '__all__' #序列化全部字段 9 | 10 | #用于列表展示的序列化器 11 | class CodeListSerializer(serializers.ModelSerializer): 12 | class Meta: 13 | model = Code 14 | fields = ('id', 'name') -------------------------------------------------------------------------------- /Chapter-three/online_python/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 在线 Python 解释器 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Chapter-three/api_learn/api_learn/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for api_learn project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "api_learn.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /Chapter-three/api_learn/data.py: -------------------------------------------------------------------------------- 1 | from django import setup 2 | import os 3 | os.environ.setdefault('DJANGO_SETTINGS_MODULE','api_learn.settings') 4 | setup() 5 | 6 | from rest_learn.models import TestModel 7 | 8 | data_set = { 9 | 'ls':"""import os\r\nprint(os.listdir())""", 10 | 'pwd':"""import os\r\nprint(os.getcwd())""", 11 | 'hello world':"""print('Hello world')""" 12 | } 13 | for name, code in data_set.items(): 14 | TestModel.objects.create(name=name,code=code) 15 | 16 | print('Done') -------------------------------------------------------------------------------- /Chapter-three/online_python/online_python/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for online_python project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "online_python.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | bin/ 10 | build/ 11 | develop-eggs/ 12 | dist/ 13 | eggs/ 14 | lib/ 15 | lib64/ 16 | parts/ 17 | sdist/ 18 | var/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | 23 | # Installer logs 24 | pip-log.txt 25 | pip-delete-this-directory.txt 26 | 27 | # Unit test / coverage reports 28 | .tox/ 29 | .coverage 30 | .cache 31 | nosetests.xml 32 | coverage.xml 33 | 34 | # Translations 35 | *.mo 36 | -------------------------------------------------------------------------------- /Chapter-one/test/test.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.http import HttpResponse 3 | from django.conf.urls import url 4 | from django import setup 5 | setting = { 6 | 'DEBUG':True, 7 | 'ROOT_URLCONF':__name__ 8 | } 9 | 10 | settings.configure(**setting) 11 | 12 | def home(request): 13 | return HttpResponse('Hello world!') 14 | 15 | urlpatterns = [url('^$',home,name='home')] 16 | 17 | if __name__ == '__main__': 18 | import sys 19 | from django.core.management import execute_from_command_line 20 | execute_from_command_line(sys.argv) -------------------------------------------------------------------------------- /Chapter-three/online_python/backend/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.3 on 2018-01-11 08:06 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Code', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('name', models.CharField(max_length=20)), 21 | ('code', models.TextField()), 22 | ], 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /Chapter-three/online_python/online_python/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url, include 2 | from django.contrib import admin 3 | from rest_framework.routers import DefaultRouter 4 | from backend.views import CodeViewSet, RunCodeAPIView, home, js, css 5 | 6 | router = DefaultRouter() 7 | router.register(prefix='code', viewset=CodeViewSet, base_name='code') 8 | 9 | API_V1 = [url(r'^run/$', RunCodeAPIView.as_view(), name='run')] 10 | 11 | API_V1.extend(router.urls) 12 | 13 | API_VERSIONS = [url(r'^v1/', include(API_V1))] 14 | 15 | urlpatterns = [ 16 | url(r'^admin/', admin.site.urls), 17 | url(r'^api/', include(API_VERSIONS)), 18 | url(r'^js/(?P.*\.js)$', js, name='js'), 19 | url(r'^css/(?P.*\.css)$', css, name='css'), 20 | url(r'^$', home, name='home') 21 | ] 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **Django RESTful 系列教程** 2 | 3 | 已经厌恶了做各种 Django 博客,想练习新的项目?想深入 Django ?想学习 Django REST 开发?这个教程就是为你准备的。 4 | 5 | --- 6 | 这是一个关于 Django RESTful 开发的教程。教程将会持续更新,更新进度大约为每个星期一篇(如果一章太长,可能两个星期才会发)。我们将会学习 **Django RESTful** 开发。在你阅读这个系列的教程之前,你需要注意这些: 7 | >* 笔者用的是 python3.5 ,Django 1.11 8 | >* 熟练 **python** 的使用。当文中提到**装饰器**或者**类**等概念时,请有最基本的映像。了解 **JavaScript** 的基本使用。 9 | >* 在跟随教程的任何过程中,有任何问题,大家可以评论留言,或者给我发邮件 **1130195942@qq.com** ,或者是在 [github](http://github.com/Ucag/django-rest) 上提 issue。 10 | >* 所有的代码和教程的 MakrDown 文本都可以在 [github](http://github.com/Ucag/django-rest)上找到。欢迎大家 clone 或者 star 。 11 | >* 以前做过 Django 的相关项目,对 Django 有一定的了解,至少完成过官方的入门教程。在讲到 **模型** 、**视图** 等概念时,有一定的了解。从一定程度上来说,这也是一个进阶的教程。 12 | >* 在本教程的最后,我们将会使用 **Django**、**Django REST framework**、**Vue**、**Vue-Router** 来做一个前后端分离的项目,也就是说,这篇教程会包含前端的内容,如果你对前端不了解也没关系,在拥有最基本的基础之上,大胆跟教程走可以了。 13 | --- 14 | ##转载请联系 **1130195942@qq.com** 15 | -------------------------------------------------------------------------------- /Chapter-three/api_learn/rest_learn/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.3 on 2017-12-16 05:04 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='TestModel', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('name', models.CharField(max_length=20)), 21 | ('code', models.TextField()), 22 | ('created_time', models.DateTimeField(auto_now_add=True)), 23 | ('changed_time', models.DateTimeField(auto_now=True)), 24 | ], 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /Chapter-three/api_learn/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "api_learn.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /Chapter-three/online_python/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "online_python.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /Chapter-two/online_intepreter_project/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "online_intepreter_project.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /Chapter-three/api_learn/api_learn/urls.py: -------------------------------------------------------------------------------- 1 | """api_learn URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.11/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import url, include 17 | from django.contrib import admin 18 | urlpatterns = [ 19 | url(r'^admin/', admin.site.urls), 20 | url(r'^drf-auth/',include('rest_framework.urls')) 21 | ] 22 | -------------------------------------------------------------------------------- /Chapter-three/vue_learn/index.js: -------------------------------------------------------------------------------- 1 | let navBar = { 2 | template:` 3 | 11 | `, 12 | data:function(){ 13 | return { 14 | home:'http://example.com/', 15 | about:'http://example.com/about' 16 | } 17 | } 18 | } 19 | 20 | let mainContent = { 21 | template:` 22 |

{{ content }}

23 | `, 24 | data:function(){ 25 | return { 26 | content:'This is main content!' 27 | } 28 | } 29 | } 30 | let app = { 31 | template:` 32 |
33 |
34 | 35 |
36 |
37 | 38 |
39 |
`, 40 | components:{ 41 | 'nav-bar':navBar, 42 | 'main-content':mainContent 43 | } 44 | } 45 | 46 | let root = new Vue({ 47 | el:'#app', 48 | template:``, 49 | components:{ 50 | 'app':app 51 | }, 52 | }) -------------------------------------------------------------------------------- /Chapter-two/online_intepreter_project/online_intepreter_project/settings.py: -------------------------------------------------------------------------------- 1 | # 这是我们的配置文件。在配在黑文件中,删除了其他我们用不上的配置项。 2 | 3 | import os 4 | 5 | # BASE_DIR最终的值为 `manage.py` 所在路径。这是因为 `manage.py` 时我们整个项目的入口, 6 | # 所以整个项目也是在 `manage.py` 所在路径下运行的,在写代码时,也应该考虑到跟路径是 manage.py 7 | # 所在路径。比如,当我们从 `views.py` 引入 `models` 模块时,写的是 `from .models import ..`, 8 | # 如果仅仅是 `from models import ...` 就可能会出错,因为这句话的意思是从当前路径下引入这个模块, 9 | # 但我们的项目是在 `manange.py` 运行的路径下执行的,这个路径下更本不会有 `models.py` 这个文件,所以 10 | # 我们要加上 `.` 路径符,表示是在本模块所在的路径下的模块文件。所以大家平时写代码也应该养成加上 `.` 路径符 11 | # 的习惯。 12 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 13 | 14 | SECRET_KEY = '=@_j0i9=3-93xb1_9cr)i!ra56o1f$t&jhfb&pj(2n+k9ul8!l' 15 | 16 | DEBUG = True 17 | # INSTALLED_APPS 项用来告诉 django 我们需要加载哪些 APP 的配置,同时,在我们运行 18 | # # `python manage.py makemigrations` 或者 `python manage.py migrate` 时,django 19 | # 会搜索其中应用的模型文件,并建立相应的模型。这次我们用到了模型,所以需要用到这个配置项。 20 | INSTALLED_APPS = ['online_intepreter_app'] 21 | # 由于 django 对 PUT 方法没有很好的支持,所以我们自己写了个简单的中间件,来处理我们的 PUT 请求。 22 | MIDDLEWARE = ['online_intepreter_app.middlewares.put_middleware'] 23 | # 根 URL 配置,这也是 django URL 的入口,所有的请求将会通过这个 URL 配置来访问我们应用的相应部分 24 | ROOT_URLCONF = 'online_intepreter_project.urls' 25 | # 这是我们的数据库配置,用来配置我们的应用。 26 | DATABASES = { 27 | 'default': { 28 | 'ENGINE': 'django.db.backends.sqlite3', 29 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Chapter-one/online_python/online_app.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.http import HttpResponse, JsonResponse# JsonResponse 用于返回 JSON 数据 3 | from django.conf.urls import url 4 | from django.views.decorators.http import require_POST 5 | from django.views.decorators.csrf import csrf_exempt 6 | import subprocess 7 | setting = { 8 | 'DEBUG':True, 9 | 'ROOT_URLCONF':__name__, 10 | } 11 | 12 | settings.configure(**setting) 13 | 14 | # 主视图 15 | def home(request): 16 | with open('index.html','rb') as f: 17 | html = f.read() 18 | return HttpResponse(html) 19 | # 执行客户端代码核心函数 20 | def run_code(code): 21 | try: 22 | output = subprocess.check_output(['python','-c',code], 23 | universal_newlines=True, 24 | stderr=subprocess.STDOUT, 25 | timeout=30) 26 | except subprocess.CalledProcessError as e: 27 | output = e.output 28 | except subprocess.TimeoutExpired as e: 29 | output = '\r\n'.join(['Time Out!!!',e.output]) 30 | return output 31 | # API 请求视图 32 | @csrf_exempt 33 | @require_POST 34 | def api(request): 35 | code = request.POST.get('code') 36 | output = run_code(code) 37 | return JsonResponse(data={'output':output}) 38 | # URL 配置 39 | urlpatterns = [url('^$',home,name='home'), 40 | url('^api/$',api,name='api')] 41 | 42 | if __name__ == '__main__': 43 | import sys 44 | from django.core.management import execute_from_command_line 45 | execute_from_command_line(sys.argv) -------------------------------------------------------------------------------- /Chapter-two/online_intepreter_project/online_intepreter_project/urls.py: -------------------------------------------------------------------------------- 1 | # 这是我们的 URL 入口配置,我们直接将入口配置到具体的 URL 上。 2 | 3 | from django.conf.urls import url, include # 引入需要用到的配置函数 4 | # include 用来引入其他的 URL 配置。参数可以是个路径字符串,也可以是个 url 对象列表 5 | 6 | from online_intepreter_app.views import APICodeView, APIRunCodeView, home, js, css # 引入我们的视图函数 7 | from django.views.decorators.csrf import csrf_exempt # 同样的,我们不需要使用 csrf 功能。 8 | 9 | # 注意我们这里的 csrf_exempt 的用法,这和将它作为装饰器使用的效果是一样的 10 | 11 | # 普通的集合操作 API 12 | generic_code_view = csrf_exempt(APICodeView.as_view(method_map={'get': 'list', 13 | 'post': 'create'})) # 传入自定义的 method_map 参数 14 | # 针对某个对象的操作 API 15 | detail_code_view = csrf_exempt(APICodeView.as_view(method_map={'get': 'detail', 16 | 'put': 'update', 17 | 'delete': 'remove'})) 18 | # 运行代码操作 API 19 | run_code_view = csrf_exempt(APIRunCodeView.as_view()) 20 | # Code 应用 API 配置 21 | code_api = [ 22 | url(r'^$', generic_code_view, name='generic_code'), # 集合操作 23 | url(r'^(?P\d*)/$', detail_code_view, name='detail_code'), # 访问某个特定对象 24 | url(r'^run/$', run_code_view, name='run_code'), # 运行代码 25 | url(r'^run/(?P\d*)/$', run_code_view, name='run_specific_code') # 运行特定代码 26 | ] 27 | api_v1 = [url('^codes/', include(code_api))] # API 的 v1 版本 28 | api_versions = [url(r'^v1/', include(api_v1))] # API 的版本控制入口 URL 29 | urlpatterns = [ 30 | url(r'^api/', include(api_versions)), # API 访问 URL 31 | url(r'^$', home, name='index'), # 主页视图 32 | url(r'^js/(?P.*\.js)$', js, name='js'), # 访问 js 文件,记得,最后没有 / 33 | url(r'^css/(?P.*\.css)$', css, name='css') # 访问 css 文件,记得,最后没有 / 34 | ] 35 | -------------------------------------------------------------------------------- /Chapter-two/online_intepreter_project/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 在线 Python 解释器 6 | 7 | 8 | 9 | 10 |
11 |
12 | 在线 Python 解释器 13 |
14 |
15 |
16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
文件名 选项
26 |
27 |
28 |
29 |
30 |

请在下方输入代码:

31 | 32 | 33 |

如需保存代码,建议输入代码片段名

34 | 35 |
36 |
40 |
41 |
42 |

输出

43 |
44 | 46 |
47 |
48 |
49 |
50 |
51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /Chapter-two/online_intepreter_project/online_intepreter_app/middlewares.py: -------------------------------------------------------------------------------- 1 | # 中间件是 django 很重要的一部分,它在请求和响应之间充当预处理器的角色。 2 | # 很多通用的逻辑可以放到这里,django 会自动的调用他们。 3 | # 在这里,我们写了一个简单的中间件来处理 PUT 请求。只要是 PUT 请求,我们就对它作这样的 4 | # 处理。所以,当你对某个请求都有相同的处理操作时,可以把它写在中间件里。所以,中间件是什么呢? 5 | # 中间件只是视图函数的公共部分。你把中间件的核心处理逻辑复制粘贴到视图函数中也是能够正常运行的。 6 | 7 | from django.http import QueryDict 8 | 9 | 10 | # QueryDict 是 django 专门为请求的查询字符串做的数据结构,它类似字典,但是又不是字典。 11 | # request 对象的 POST GET 属性都是这样的字典。 12 | # 类似字典,是因为 QueryDict 和 python 的 dict 有相似的 API 接口,所以你可以把它当字典 13 | # 来调用。 14 | # 不是字典,是因为 QueryDict 允许同一个键有多个直。比如 {'a':[‘1’,‘2’]},a 同时有值 1 和 2, 15 | # 所以,一般不要用 QueryDict[key] 的形式来访问相应 key 的值,因为你得到的会是一个列表,而不是 16 | # 一个单一的值,应该用 QueryDict.get(key) 来获取你想要的值,除非你知道你在干什么,你才能 17 | # 这样来取值。为什么会允许多个值呢,因为 GET 18 | # 请求中,常常有这种参数 http://www.example.com/?action=search&achtion=filter , 19 | # action 在这里有两个值,有时候我们需要对这两个值都作出响应。但是当你用 .get(key) 方法取值的 20 | # 时候,只会取到最新的一个值。如果确实需要访问这个键的多个值,应该用 .getList(key) 方法来访问, 21 | # 比如刚才的例子应该用 request.GET.getList('action') 来访问 action 的多个值。 22 | # 同理,对于 POST 请求也应该这么做。 23 | # 由于原生的 request 对象并没有 PUT 属性,所以我们需要在中间件中加上这个属性,这样我们 24 | # 就可以在视图函数中用 request.PUT 来访问 PUT 请求中的参数值了。 25 | 26 | # 中间件在 1.11 版本里是一个可调用对象,和之前的类中间件不同。既然是可调用对象,那就有 27 | # 两种写法,一种是函数,因为函数就是一个可调用对象;一种是自己用类来写一个可调用对象,也就是 28 | # 包含 __cal__() 方法的类。 29 | 30 | def put_middleware(get_response): 31 | # 在 1.11 版本中,中间件对象应该接收一个 get_response 的参数,这个参数用来获取上一个 32 | # 中间件处理之后的响应,每个中间件处理完请求之后都应该用这个函数来返回一个响应,我们不需要 33 | # 关心这个 get_response 函数是怎么写的,是什么东西,只需要记得在最后调用它,返回响应就好。 34 | # 这个最外层函数应该返回一个函数,用作真正的中间件处理。 35 | 36 | # 在这个地方写你的预处理逻辑,比如配置什么的。当然,你也可以在被返回的函数中写配置和浴池里 37 | # 但是这么做有时候就有些不直观,配置、预处理和核心逻辑分开,让看代码的人一眼就明白 38 | # 这个中间件是在做什么。 39 | # 最通常的例子是,很多的 API 会对请求做许多的处理,比如记录下这个请求的 IP 地址 40 | # 就可以先在这里做这个步骤;又比如,为了控制访问频率,可以先读取数据库中的访问数据, 41 | # 根据访问数据记录来决定要不要让这个请求进入到视图函数中。 42 | # 我们对 PUT 请求并没有什么预处理或者配置操作要进行,所以就什么都没写 43 | def middleware(request): 44 | if request.method == 'PUT': # 如果是 PUT 请求 45 | setattr(request, 'PUT', QueryDict(request.body)) # 给请求设置 PUT 属性,这样我们就可以在视图函数中访问这个属性了 46 | # request.body 是请求的主体。我们知道请求有请求头,那请求的主体就是 47 | # request.body 了。当然,你一定还会问,为什么这样就可以访问 PUT 请求的相关 48 | # 数据了呢?这涉及到了 http 协议的知识,这里就不展开了,有兴趣的同学可以自行查阅资料 49 | response = get_response(request) # 使用 get_response 返回响应 50 | return response # 返回响应 51 | 52 | return middleware # 返回核心的中间件处理函数 53 | -------------------------------------------------------------------------------- /Chapter-one/online_python/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 在线 Python 解释器 8 | 9 | 23 | 24 | 25 |
26 |
27 |
28 |

29 | 在线 Python 解释器 30 |

31 |
32 |
33 |
34 |
35 |
36 |

37 | 在下面输入代码 38 |

39 | 40 |
41 |
42 |
43 |

运行结果

44 |
45 |
46 |
47 |
48 | 49 | 50 | 82 | 83 | -------------------------------------------------------------------------------- /Chapter-three/api_learn/api_learn/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for api_learn project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.11.3. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.11/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '1@b2p(4_=lsl@uu#4_ln6jhs$%x@tx1a&ba#j*nvj2c5=z%*@3' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'rest_framework', 41 | 'rest_learn' 42 | ] 43 | 44 | MIDDLEWARE = [ 45 | 'django.middleware.security.SecurityMiddleware', 46 | 'django.contrib.sessions.middleware.SessionMiddleware', 47 | 'django.middleware.common.CommonMiddleware', 48 | 'django.middleware.csrf.CsrfViewMiddleware', 49 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 50 | 'django.contrib.messages.middleware.MessageMiddleware', 51 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 52 | ] 53 | 54 | ROOT_URLCONF = 'api_learn.urls' 55 | 56 | TEMPLATES = [ 57 | { 58 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 59 | 'DIRS': [], 60 | 'APP_DIRS': True, 61 | 'OPTIONS': { 62 | 'context_processors': [ 63 | 'django.template.context_processors.debug', 64 | 'django.template.context_processors.request', 65 | 'django.contrib.auth.context_processors.auth', 66 | 'django.contrib.messages.context_processors.messages', 67 | ], 68 | }, 69 | }, 70 | ] 71 | 72 | WSGI_APPLICATION = 'api_learn.wsgi.application' 73 | 74 | 75 | # Database 76 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases 77 | 78 | DATABASES = { 79 | 'default': { 80 | 'ENGINE': 'django.db.backends.sqlite3', 81 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 82 | } 83 | } 84 | 85 | 86 | # Password validation 87 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators 88 | 89 | AUTH_PASSWORD_VALIDATORS = [ 90 | { 91 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 92 | }, 93 | { 94 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 95 | }, 96 | { 97 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 98 | }, 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 101 | }, 102 | ] 103 | 104 | 105 | # Internationalization 106 | # https://docs.djangoproject.com/en/1.11/topics/i18n/ 107 | 108 | LANGUAGE_CODE = 'en-us' 109 | 110 | TIME_ZONE = 'UTC' 111 | 112 | USE_I18N = True 113 | 114 | USE_L10N = True 115 | 116 | USE_TZ = True 117 | 118 | 119 | # Static files (CSS, JavaScript, Images) 120 | # https://docs.djangoproject.com/en/1.11/howto/static-files/ 121 | 122 | STATIC_URL = '/static/' 123 | -------------------------------------------------------------------------------- /Chapter-three/online_python/online_python/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for online_python project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.11.3. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.11/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'o7q(ifrfwwm4tx2o7d=ulz1c1z8o+k=&i4*d_e_ax4z_x%$og@' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'rest_framework', 41 | 'backend' 42 | ] 43 | 44 | MIDDLEWARE = [ 45 | 'django.middleware.security.SecurityMiddleware', 46 | 'django.contrib.sessions.middleware.SessionMiddleware', 47 | 'django.middleware.common.CommonMiddleware', 48 | 'django.middleware.csrf.CsrfViewMiddleware', 49 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 50 | 'django.contrib.messages.middleware.MessageMiddleware', 51 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 52 | ] 53 | 54 | ROOT_URLCONF = 'online_python.urls' 55 | 56 | TEMPLATES = [ 57 | { 58 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 59 | 'DIRS': [], 60 | 'APP_DIRS': True, 61 | 'OPTIONS': { 62 | 'context_processors': [ 63 | 'django.template.context_processors.debug', 64 | 'django.template.context_processors.request', 65 | 'django.contrib.auth.context_processors.auth', 66 | 'django.contrib.messages.context_processors.messages', 67 | ], 68 | }, 69 | }, 70 | ] 71 | 72 | WSGI_APPLICATION = 'online_python.wsgi.application' 73 | 74 | 75 | # Database 76 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases 77 | 78 | DATABASES = { 79 | 'default': { 80 | 'ENGINE': 'django.db.backends.sqlite3', 81 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 82 | } 83 | } 84 | 85 | 86 | # Password validation 87 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators 88 | 89 | AUTH_PASSWORD_VALIDATORS = [ 90 | { 91 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 92 | }, 93 | { 94 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 95 | }, 96 | { 97 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 98 | }, 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 101 | }, 102 | ] 103 | 104 | 105 | # Internationalization 106 | # https://docs.djangoproject.com/en/1.11/topics/i18n/ 107 | 108 | LANGUAGE_CODE = 'en-us' 109 | 110 | TIME_ZONE = 'UTC' 111 | 112 | USE_I18N = True 113 | 114 | USE_L10N = True 115 | 116 | USE_TZ = True 117 | 118 | 119 | # Static files (CSS, JavaScript, Images) 120 | # https://docs.djangoproject.com/en/1.11/howto/static-files/ 121 | 122 | STATIC_URL = '/static/' 123 | -------------------------------------------------------------------------------- /Chapter-three/api_learn/rest_test.py: -------------------------------------------------------------------------------- 1 | from django import setup 2 | import os 3 | os.environ.setdefault('DJANGO_SETTINGS_MODULE','api_learn.settings') 4 | setup() 5 | 6 | from rest_framework import serializers 7 | 8 | class TestSerilOne(serializers.Serializer): 9 | name = serializers.CharField(max_length=20) 10 | age = serializers.IntegerField() 11 | 12 | 13 | frontend_data = { 14 | 'name':'ucag', 15 | 'age':'ucag' 16 | } 17 | 18 | # test = TestSerilOne(data=frontend_data) 19 | # if not test.is_valid(): 20 | # print(test.errors) 21 | 22 | class TestSerilTwo(serializers.Serializer): 23 | name = serializers.CharField(max_length=20) 24 | 25 | 26 | # from rest_learn.models import TestModel 27 | # codes = TestModel.objects.all() 28 | # test = TestSerilTwo(instance=codes,many=True) 29 | # print(test.data) 30 | 31 | from rest_learn.models import TestModel 32 | class TestSerilThree(serializers.ModelSerializer): 33 | class Meta: 34 | model = TestModel 35 | fields = ['name','code','created_time','changed_time','id'] 36 | read_only_fields = ['created_time','changed_time'] 37 | 38 | # code = TestModel.objects.get(name='ls') 39 | # codes = TestModel.objects.all() 40 | 41 | # 前端写入测试 42 | # frontend_data = { 43 | # 'name':'ModelSeril', 44 | # 'code':"""print('frontend test')""", 45 | # 'created_time':'2107-12-16' 46 | # } 47 | # test1 = TestSerilThree(data=frontend_data) 48 | # if test1.is_valid(): 49 | # print('Frontend test:',test1.validated_data) 50 | # 后端传出测试: 51 | # test2 = TestSerilThree(instance=code) 52 | # print('Backend single instance test:',test2.data) 53 | # test3 = TestSerilThree(instance=codes,many=True) 54 | # print('Backend multiple instances test',test3.data) 55 | 56 | class ProfileSerializer(serializers.Serializer): 57 | tel = serializers.CharField(max_length=15) 58 | height = serializers.IntegerField() 59 | 60 | class UserSerializer(serializers.Serializer): 61 | name = serializers.CharField(max_length=20) 62 | qq = serializers.CharField(max_length=15) 63 | profile = ProfileSerializer() 64 | 65 | # frontend_data = { 66 | # 'name':'ucag', 67 | # 'qq':'88888888', 68 | # 'profile':{ 69 | # 'tel':'66666666666', 70 | # 'height':'185' 71 | # } 72 | # } 73 | 74 | # test = UserSerializer(data=frontend_data) 75 | # if test.is_valid(): 76 | # print(test.validated_data) 77 | class TEL(object): 78 | """电话号码对象""" 79 | def __init__(self, num=None): 80 | self.num = num 81 | def text(self, message): 82 | """发短信功能""" 83 | return self._send_message(message) 84 | def _send_message(self,message): 85 | """发短信""" 86 | print('Send {} to {}'.format(message[:10], self.num)) 87 | class TELField(serializers.Field): 88 | def to_representation(self, tel_obj): 89 | return tel_obj.num 90 | def to_internal_value(self, data): 91 | data = data.lstrip().rstrip().strip() 92 | if 8 <= len(data) <=11: 93 | return TEL(num=data) 94 | raise serializers.ValidationError('Invalid telephone number.') 95 | 96 | class ContactSerializer(serializers.Serializer): 97 | name = serializers.CharField(max_length=20) 98 | tel = TELField() 99 | 100 | # frontend_data = { 101 | # 'name':'ucag', 102 | # 'tel':'88888888' 103 | # } 104 | # test = ContactSerializer(data=frontend_data) 105 | # if test.is_valid(): 106 | # tel = test.validated_data['tel'] 107 | # print('TEL',tel.num) 108 | # tel.text('这是一个骚字段') 109 | 110 | from rest_framework.viewsets import ModelViewSet 111 | class TestViewSet(ModelViewSet): 112 | queryset = TestModel.objects.all() 113 | 114 | from rest_framework.routers import DefaultRouter 115 | router = DefaultRouter() 116 | router.register(r'codes', TestViewSet) 117 | urlpatterns = router.urls 118 | print(urlpatterns) -------------------------------------------------------------------------------- /Chapter-three/online_python/backend/views.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from django.http import HttpResponse 3 | from django.db import models 4 | from rest_framework.viewsets import ModelViewSet 5 | from rest_framework.views import APIView 6 | from rest_framework.response import Response 7 | from rest_framework import status 8 | from .serializers import CodeListSerializer, CodeSerializer 9 | from .models import Code 10 | from rest_framework.authentication import SessionAuthentication 11 | 12 | 13 | class CsrfExemptSessionAuthentication(SessionAuthentication): 14 | """ 15 | 去除 CSRF 检查 16 | """ 17 | 18 | def enforce_csrf(self, request): 19 | return 20 | 21 | 22 | class APIRunCodeMixin(object): 23 | """ 24 | 运行代码操作 25 | """ 26 | 27 | def run_code(self, code): 28 | """ 29 | 运行所给的代码,并返回执行结果 30 | :params code: str, 需要被运行的代码 31 | :return: str, 运行结果 32 | """ 33 | try: 34 | output = subprocess.check_output(['python', '-c', code], # 运行代码 35 | stderr=subprocess.STDOUT, # 重定向错误输出流到子进程 36 | universal_newlines=True, # 将返回执行结果转换为字符串 37 | timeout=30) # 设定执行超时时间 38 | except subprocess.CalledProcessError as e: # 捕捉执行失败异常 39 | output = e.output # 获取子进程报错信息 40 | except subprocess.TimeoutExpired as e: # 捕捉超时异常 41 | output = '\r\n'.join(['Time Out!', e.output]) # 获取子进程报错,并添加运行超时提示 42 | return output # 返回执行结果 43 | 44 | 45 | class CodeViewSet(APIRunCodeMixin, ModelViewSet): 46 | queryset = Code.objects.all() 47 | serializer_class = CodeSerializer 48 | authentication_classes = (CsrfExemptSessionAuthentication,) 49 | 50 | def list(self, request, *args, **kwargs): 51 | """ 52 | 使用专门的列表序列化器,而非默认的序列化器 53 | """ 54 | serializer = CodeListSerializer(self.get_queryset(), many=True) 55 | return Response(data=serializer.data) 56 | 57 | def run_create_or_update(self, request, serializer): 58 | """ 59 | create 和 update 的共有逻辑,仅仅是简单的多了 run 参数的判断 60 | """ 61 | if serializer.is_valid(): 62 | code = serializer.validated_data.get('code') 63 | serializer.save() 64 | if 'run' in request.query_params.keys(): 65 | output = self.run_code(code) 66 | data = serializer.data 67 | data.update({'output': output}) 68 | return Response(data=data, status=status.HTTP_201_CREATED) 69 | return Response(data=serializer.data, status=status.HTTP_201_CREATED) 70 | return Response(data=serializer.errors, status=status.HTTP_400_BAD_REQUEST) 71 | 72 | def create(self, request, *args, **kwargs): 73 | serializer = self.serializer_class(data=request.data) 74 | return self.run_create_or_update(request, serializer) 75 | 76 | def update(self, request, *args, **kwargs): 77 | instance = self.get_object() 78 | serializer = self.serializer_class(instance, data=request.data) 79 | return self.run_create_or_update(request, serializer) 80 | 81 | 82 | class RunCodeAPIView(APIRunCodeMixin, APIView): 83 | authentication_classes = (CsrfExemptSessionAuthentication,) 84 | 85 | def post(self, request, format=None): 86 | output = self.run_code(request.data.get('code')) 87 | return Response(data={'output': output}, status=status.HTTP_200_OK) 88 | 89 | def get(self, request, format=None): 90 | try: 91 | code = Code.objects.get(pk=request.query_params.get('id')) 92 | except models.ObjectDoesNotExist: 93 | return Response(data={'error': 'Object Not Found'}, status=status.HTTP_404_NOT_FOUND) 94 | output = self.run_code(code.code) 95 | return Response(data={'output': output}, status=status.HTTP_200_OK) 96 | 97 | 98 | def home(request): 99 | with open('frontend/index.html', 'rb') as f: 100 | content = f.read() 101 | return HttpResponse(content) 102 | 103 | 104 | def js(request, filename): 105 | with open('frontend/{}'.format(filename), 'rb') as f: 106 | js_content = f.read() 107 | return HttpResponse(content=js_content, 108 | content_type='application/javascript') # 返回 js 响应 109 | 110 | 111 | def css(request, filename): 112 | with open('frontend/{}'.format(filename), 'rb') as f: 113 | css_content = f.read() 114 | return HttpResponse(content=css_content, 115 | content_type='text/css') 116 | -------------------------------------------------------------------------------- /Chapter-two/online_intepreter_project/online_intepreter_app/views.py: -------------------------------------------------------------------------------- 1 | from django.views import View # 引入最基本的类视图 2 | from django.http import JsonResponse, HttpResponse # 引入现成的响应类 3 | from django.core.serializers import serialize # 引入序列化函数 4 | from .models import CodeModel # 引入 Code 模型,记得加个 `.` 哦。 5 | import json # 引入 json 库,我们会用它来处理 json 字符串。 6 | from .mixins import APIDetailMixin, APIUpdateMixin, \ 7 | APIDeleteMixin, APIListMixin, APIRunCodeMixin, \ 8 | APICreateMixin, APIMethodMapMixin, APISingleObjectMixin # 引入我们编写的所有 Mixin 9 | 10 | 11 | # 定义最基本的 API 视图 12 | class APIView(View): 13 | def response(self, 14 | queryset=None, 15 | fields=None, 16 | **kwargs): 17 | """ 18 | 序列化传入的 queryset 或 其他 python 数据类型。返回一个 JsonResponse 。 19 | :param queryset: 查询集,可以为 None 20 | :param fields: 查询集中需要序列化的字段,可以为 None 21 | :param kwargs: 其他需要序列化的关键字参数 22 | :return: 返回 JsonResponse 23 | """ 24 | 25 | # 根据传入参数序列化查询集,得到序列化之后的 json 字符串 26 | if queryset and fields: 27 | serialized_data = serialize(format='json', 28 | queryset=queryset, 29 | fields=fields) 30 | elif queryset: 31 | serialized_data = serialize(format='json', 32 | queryset=queryset) 33 | else: 34 | serialized_data = None 35 | # 这一步很重要,在经过上面的查询步骤之后, serialized_data 已经是一个字符串 36 | # 我们最终需要把它放入 JsonResponse 中,JsonResponse 只接受 python 数据类型 37 | # 所以我们需要先把得到的 json 字符串转化为 python 数据结构。 38 | instances = json.loads(serialized_data) if serialized_data else 'No instance' 39 | data = {'instances': instances} 40 | data.update(kwargs) # 添加其他的字段 41 | return JsonResponse(data=data) # 返回响应 42 | 43 | 44 | # 45 | class APICodeView(APIListMixin, # 获取列表 46 | APIDetailMixin, # 获取当前请求实例详细信息 47 | APIUpdateMixin, # 更新当前请求实例 48 | APIDeleteMixin, # 删除当前实例 49 | APICreateMixin, # 创建新的的实例 50 | APIMethodMapMixin, # 请求方法与资源操作方法映射 51 | APIView): # 记得在最后继承 APIView 52 | model = CodeModel # 传入模型 53 | 54 | def list(self): # 这里仅仅是简单的给父类的 list 函数传参。 55 | return super(APICodeView, self).list(fields=['name']) 56 | 57 | 58 | class APIRunCodeView(APIRunCodeMixin, 59 | APISingleObjectMixin, 60 | APIView): 61 | model = CodeModel # 传入模型 62 | 63 | def get(self, request, *args, **kwargs): 64 | """ 65 | GET 请求仅对能获取到 pk 值的 url 响应 66 | :param request: 请求对象 67 | :param args: 位置参数 68 | :param kwargs: 关键字参数 69 | :return: JsonResponse 70 | """ 71 | instance = self.get_object() # 获取对象 72 | code = instance.code # 获取代码 73 | output = self.run_code(code) # 运行代码 74 | return self.response(output=output, status='Successfully Run') # 返回响应 75 | 76 | def post(self, request, *args, **kwargs): 77 | """ 78 | POST 请求可以被任意访问,并会检查 url 参数中的 save 值,如果 save 为 true 则会 79 | 保存上传代码。 80 | :param request: 请求对象 81 | :param args: 位置参数 82 | :param kwargs: 关键字参数 83 | :return: JsonResponse 84 | """ 85 | code = self.request.POST.get('code') # 获取代码 86 | save = self.request.GET.get('save') == 'true' # 获取 save 参数值 87 | name = self.request.POST.get('name') # 获取代码片段名称 88 | output = self.run_code(code) # 运行代码 89 | if save: # 判断是否保存代码 90 | instance = self.model.objects.create(name=name, code=code) 91 | return self.response(status='Successfully Run and Save', 92 | output=output) # 返回响应 93 | 94 | def put(self, request, *args, **kwrags): 95 | """ 96 | PUT 请求仅对更改操作作出响应 97 | :param request: 请求对象 98 | :param args: 位置参数 99 | :param kwrags: 关键字参数 100 | :return: JsonResponse 101 | """ 102 | code = self.request.PUT.get('code') # 获取代码 103 | name = self.request.PUT.get('name') # 获取代码片段名称 104 | save = self.request.GET.get('save') == 'true' # 获取 save 参数值 105 | output = self.run_code(code) # 运行代码 106 | if save: # 判断是否需要更改代码 107 | instance = self.get_object() # 获取当前实例 108 | setattr(instance, 'name', name) # 更改名字 109 | setattr(instance, 'code', code) # 更改代码 110 | instance.save() 111 | return self.response(status='Successfully Run and Change', 112 | output=output) # 返回响应 113 | 114 | 115 | # 主页视图 116 | def home(request): 117 | """ 118 | 读取 'index.html' 并返回响应 119 | :param request: 请求对象 120 | :return: HttpResponse 121 | """ 122 | with open('frontend/index.html', 'rb') as f: 123 | content = f.read() 124 | return HttpResponse(content) 125 | 126 | 127 | # 读取 js 视图 128 | def js(request, filename): 129 | """ 130 | 读取 js 文件并返回 js 文件响应 131 | :param request: 请求对象 132 | :param filename: str-> 文件名 133 | :return: HttpResponse 134 | """ 135 | with open('frontend/js/{}'.format(filename), 'rb') as f: 136 | js_content = f.read() 137 | return HttpResponse(content=js_content, 138 | content_type='application/javascript') # 返回 js 响应 139 | 140 | 141 | # 读取 css 视图 142 | def css(request, filename): 143 | """ 144 | 读取 css 文件,并返回 css 文件响应 145 | :param request: 请求对象 146 | :param filename: str-> 文件名 147 | :return: HttpResponse 148 | """ 149 | with open('frontend/css/{}'.format(filename), 'rb') as f: 150 | css_content = f.read() 151 | return HttpResponse(content=css_content, 152 | content_type='text/css') # 返回 css 响应 153 | -------------------------------------------------------------------------------- /Chapter-two/online_intepreter_project/online_intepreter_app/mixins.py: -------------------------------------------------------------------------------- 1 | # Mixin 是很很重要的代码解耦方式,让功能逻辑和具体操作的操作逻辑分开。同时,这也是很好的 2 | # 代码复用方法。在编写 Mixin 时,要注意 “单一职责” 的原则,一个 Mixin 只干一件事情, 3 | # 一个 Mixin 只操作和调用它已知的属性和方法。这样,就可以像搭积木一样来编写视图了, 4 | # 需要什么功能就继承这个 Mixin ,不需要就不继承,特别方便。 5 | # Mixin 不能单独发挥作用,只能作为对主逻辑的补充。 6 | # 在这里,我们约定,每个 Mixin 默认被 APIView 的子类所继承,所以默认每个 Mixin 都 7 | # 可以操作 response 方法和 django View 中的属性和方法。 8 | # View 中常用的属性有: 9 | # kwargs, args: 传入视图函数的参数,也就是在 url 的正则表达式中匹配到的参数。 10 | # 如果匹配到命名参数,则赋值到 kwargs,否则赋值到 args。 11 | # request: 传入视图函数的 request 对象。这样我们就可以访问请求对象了 12 | 13 | 14 | 15 | # 引入 models, 这样就可以调用模型类的异常,InetegrityError 在创建实例失败时会被抛出 16 | # 我们需要捕捉这个异常 17 | from django.db import models, IntegrityError 18 | import subprocess # 用于运行代码 19 | from django.http import Http404 # 当查询操作失败时返回404响应 20 | class APIQuerysetMinx(object): 21 | """ 22 | 用于获取查询集。在使用时,model 属性和 queryset 属性必有其一。 23 | 24 | :model: 模型类 25 | :queryet: 查询集 26 | """ 27 | model = None 28 | queryset = None 29 | 30 | def get_queryset(self): 31 | """ 32 | 获取查询集。若有 model 参数,则默认返回所有的模型查询实例。 33 | :return: 查询集 34 | """ 35 | 36 | # 检验相应参数是否被传入,若没有传入则抛出错误 37 | assert self.model or self.queryset, 'No queryset fuound.' 38 | if self.queryset: 39 | return self.queryset 40 | else: 41 | return self.model.objects.all() 42 | 43 | 44 | class APISingleObjectMixin(APIQuerysetMinx): 45 | """ 46 | 用于获取当前请求中的实例。 47 | 48 | :lookup_args: list, 用来规定查询参数的参数列表。默认为 ['pk','id] 49 | """ 50 | lookup_args = ['pk', 'id'] 51 | 52 | def get_object(self): 53 | """ 54 | 通过查询 lookup_args 中的参数值来返回当前请求实例。当获取到参数值时,则停止 55 | 对之后的参数查询。参数顺序很重要。 56 | :return: 一个单一的查询实例 57 | """ 58 | queryset = self.get_queryset() # 获取查询集 59 | for key in self.lookup_args: 60 | if self.kwargs.get(key): 61 | id = self.kwargs[key] # 获取查询参数值 62 | try: 63 | instance = queryset.get(id=id) # 获取当前实例 64 | return instance # 实例存在则返回实例 65 | except models.ObjectDoesNotExist: # 捕捉实例不存在异常 66 | raise Http404('No object found.') # 抛出404异常响应 67 | raise Http404('No object found.') # 若遍历所以参数都未捕捉到值,则抛出404异常响应 68 | 69 | 70 | class APIListMixin(APIQuerysetMinx): 71 | """ 72 | API 中的 list 操作。 73 | """ 74 | def list(self, fields=None): 75 | """ 76 | 返回查询集响应 77 | :param fields: 查询集中希望被实例化的字段 78 | :return: JsonResopnse 79 | """ 80 | return self.response( 81 | queryset=self.get_queryset(), 82 | fields=fields) # 返回响应 83 | 84 | 85 | class APICreateMixin(APIQuerysetMinx): 86 | """ 87 | API 中的 create 操作 88 | """ 89 | def create(self, create_fields=None): 90 | """ 91 | 使用传入的参数列表从 POST 值中获取对应参数值,并用这个值创建实例, 92 | 成功创建则返回创建成功响应,否则返回创建失败响应。 93 | :param create_fields: list, 希望被创建的字段。 94 | 若为 None, 则默认为 POST 上传的所有字段。 95 | :return: JsonResponse 96 | """ 97 | create_values = {} 98 | if create_fields: # 如果传入了希望被创建的字段,则从 POST 中获取每个值 99 | for field in create_fields: 100 | create_values[field]=self.request.POST.get(field) 101 | else: 102 | for key in self.request.POST: # 若未传入希望被创建字段,则默认为 POST 上传的 103 | # 字段都为创建字段。 104 | create_values[key]=self.request.POST.get(key); 105 | queryset = self.get_queryset() # 获取查询集 106 | try: 107 | instance = queryset.create(**create_values) # 利用查询集来创建实例 108 | except IntegrityError: # 捕捉创建失败异常 109 | return self.response(status='Failed to Create.') # 返回创建失败响应 110 | return self.response(status='Successfully Create.') # 创建成功则返回创建成功响应 111 | 112 | 113 | class APIDetailMixin(APISingleObjectMixin): 114 | """ 115 | API 操作中查询实例操作 116 | """ 117 | def detail(self, fields=None): 118 | """ 119 | 返回当前请求中的实例 120 | :param fields: 希望被返回实例中哪些字段被实例化 121 | :return: JsonResponse 122 | """ 123 | return self.response( 124 | queryset=[self.get_object()], 125 | fields=fields) 126 | 127 | 128 | class APIUpdateMixin(APISingleObjectMixin): 129 | """ 130 | API 中更新实例操作 131 | """ 132 | def update(self, update_fields=None): 133 | """ 134 | 更新当前请求中实例。更新成功则返回成功响应。否则,返回更新失败响应。 135 | 若传入 updata_fields 更新字段列表,则只会从 PUT 上传值中获取这个列表中的字段, 136 | 否则默认为更新 POST 上传值中所有的字段。 137 | :param update_fields: list, 实例需要被更新的字段 138 | :return: JsonResponse 139 | """ 140 | instance = self.get_object() # 获取当前请求中的实例 141 | if not update_fields: # 若无字段更新列表,则默认为 PUT 上传值的所有数据 142 | update_fields=self.request.PUT.keys() 143 | try: # 迭代更新实例字段 144 | for field in update_fields: 145 | update_value = self.request.PUT.get(field) # 从 PUT 中取值 146 | setattr(instance, field, update_value) # 更新字段 147 | instance.save() # 保存实例更新 148 | except IntegrityError: # 捕捉更新错误 149 | return self.response(status='Failed to Update.') # 返回更新失败响应 150 | return self.response( 151 | status='Successfully Update')# 更新成功则返回更新成功响应 152 | 153 | 154 | class APIDeleteMixin(APISingleObjectMixin): 155 | """ 156 | API 删除实例操作 157 | """ 158 | def remove(self): 159 | """ 160 | 删除当前请求中的实例。删除成功则返回删除成功响应。 161 | :return: JsonResponse 162 | """ 163 | instance = self.get_object() # 获取当前实例 164 | instance.delete() # 删除实例 165 | return self.response(status='Successfully Delete') # 返回删除成功响应 166 | 167 | 168 | class APIRunCodeMixin(object): 169 | """ 170 | 运行代码操作 171 | """ 172 | def run_code(self, code): 173 | """ 174 | 运行所给的代码,并返回执行结果 175 | :param code: str, 需要被运行的代码 176 | :return: str, 运行结果 177 | """ 178 | try: 179 | output = subprocess.check_output(['python', '-c', code], # 运行代码 180 | stderr=subprocess.STDOUT, # 重定向错误输出流到子进程 181 | universal_newlines=True, # 将返回执行结果转换为字符串 182 | timeout=30) # 设定执行超时时间 183 | except subprocess.CalledProcessError as e: # 捕捉执行失败异常 184 | output = e.output # 获取子进程报错信息 185 | except subprocess.TimeoutExpired as e: # 捕捉超时异常 186 | output = '\r\n'.join(['Time Out!', e.output]) # 获取子进程报错,并添加运行超时提示 187 | return output # 返回执行结果 188 | 189 | class APIMethodMapMixin(object): 190 | """ 191 | 将请求方法映射到子类属性上 192 | 193 | :method_map: dict, 方法映射字典。 194 | 如将 get 方法映射到 list 方法,其值则为 {'get':'list'} 195 | """ 196 | method_map = {} 197 | def __init__(self,*args,**kwargs): 198 | """ 199 | 映射请求方法。会从传入子类的关键字参数中寻找 method_map 参数,期望值为 dict类型。寻找对应参数值。 200 | 若在类属性和传入参数中同时定义了 method_map ,则以传入参数为准。 201 | :param args: 传入的位置参数 202 | :param kwargs: 传入的关键字参数 203 | """ 204 | method_map=kwargs['method_map'] if kwargs.get('method_map',None) \ 205 | else self.method_map # 获取 method_map 参数 206 | for request_method, mapped_method in method_map.items(): # 迭代映射方法 207 | mapped_method = getattr(self, mapped_method) # 获取被映射方法 208 | method_proxy = self.view_proxy(mapped_method) # 设置对应视图代理 209 | setattr(self, request_method, method_proxy) # 将视图代码映射到视图代理方法上 210 | super(APIMethodMapMixin,self).__init__(*args,**kwargs) # 执行子类的其他初始化 211 | 212 | def view_proxy(self, mapped_method): 213 | """ 214 | 代理被映射方法,并代理接收传入视图函数的其他参数。 215 | :param mapped_method: 被代理的映射方法 216 | :return: function, 代理视图函数。 217 | """ 218 | def view(*args, **kwargs): 219 | """ 220 | 视图的代理方法 221 | :param args: 传入视图函数的位置参数 222 | :param kwargs: 传入视图函数的关键字参数 223 | :return: 返回执行被映射方法 224 | """ 225 | return mapped_method() # 返回执行代理方法 226 | return view # 返回代理视图 227 | -------------------------------------------------------------------------------- /Chapter-two/Django RESTful 系列教程(二)(上).md: -------------------------------------------------------------------------------- 1 | #Django RESTful 系列教程(二)(上) 2 | #深入 REST 3 | 4 | 终于迎来了第二篇教程,大家久等了。本章我们将会深入 **REST**的概念。这一次,我们将会真正的遵循 REST 设计规范,将我们的应用打造为一个真正的 RESTful 应用。在上一章中,我们使用单文件的 django 制作了一个简易的在线 python 解释器,已经实现了基本的功能。这一次,我们将会把它变成一个真正意义上的应用,我们将会把代码储存进数据库,给它添加增删改查的功能。在本章,我们将会涵盖以下知识点: 5 | 6 | - REST 的一般规范。 7 | - Django 的项目结构设计。 8 | - Mixin 的编写与应用。 9 | - 了解并运用 UI “状态” 概念。 10 | - 建立起前端模块化思想。 11 | 12 | 在上部分,我们将会学习需要用到的基本技术和概念,在下部分,我们将会动手创建我们的应用。不过,大家也可以先看看我们的应用最终完成是什么样的: 13 | ![最终完成的应用](https://raw.githubusercontent.com/Ucag/django-rest/master/Chapter-two/imgs/demo.gif) 14 | 我们可以很明显的看到,虽然我们进行了诸多操作,但是页面从来没有刷新过。左边的列表 UI 是动态刷新的。大家可以自己先运行着玩玩。代码在[这里](https://github.com/Ucag/django-rest/tree/master/Chapter-two/online_intepreter_project) 15 | ## REST 是什么? 16 | 在网上能找到很多的资料,但是大多的 REST 相关的资料都显得诘屈聱牙,晦涩难懂。其实这也不怪那些作者,因为 REST 这个名字就很让人感觉到头痛。 让我们来慢慢梳理下这个在我们教程标题中的重要概念。 17 | 18 | REST 是用来干什么的? REST 这个晦涩的概念如同其它计算机概念一样,有的时候我们根本不知道它到底是怎么一回事,但是我们却可以熟练的使用它。最典型的例子就是,虽然我们大家都在写 Django ,但是又有多少人能清楚的解释 MTV 模式是什么呢?这就像是做数学题,虽然我不明白解方程中的换元思想,但是我在不知不觉中就用到了它。 当我们提到 REST 的时候,后面通常会跟一个词——API 。所以在一般的概念里,大家都把 REST 作为一种 API 的设计规范来理解。 所以 REST 就是用来设计 API 的吗?至少到目前为止,我们所了解的 REST 就是用来设计 API 的。它规定了一堆很烦人的规矩,让你去遵守它的规则,把你的代码约束在一个固定的格式里。甚至在有的公司里,他们对 REST 的态度是完全不理睬的。好吧,这是人的天性,不喜欢被各种规矩所束缚,也不能怪他们。正如我不喜欢按时起床一样,虽然起得来,但是我就是想要躺在床上。感觉自己冥冥之中在和什么东西作斗争。我们在编写 API 的时候也在作斗争,想要清晰明确的表达 API 的语义,又不想将代码写的太繁琐。有的时候为了解析参数,如果设计不当,你还会不得不自己手写一个 parser 来解析你的 URL 参数。真是烦人。 19 | 20 | REST 就是将我们从灾难中解救出来的东西。现在让我们来正式的认识一下它。REST ,全称是 Representational State Transfer,中文翻译是: 资源的表现层状态转换。好了,我想你看到这第一句就已经不想看了。WTF! 感觉中文都不通啊,这还理解个什么???淡定淡定。仅仅是为了尊重下将这个概念提出来的作者 Roy Felding ,他在他的论文《基于网络的软件架构》 中提出了这个概念。 我也不打算把这个概念解释给你听。我对于学习的哲学是:知识的习得来源于知识对你的启发。所以,我会带着大家来探索下,这个概念是个什么东西。现在,请你忘记 REST 这个概念。 忘记你曾经对 REST 的任何了解。 21 | 22 | 假设你是个仓库管理员,管着一个巨大的仓库。不仅看守着大门,还管理着从库的进进出出,虽然仅仅是个小小的管理员,但你已经俨然是个仓库经理的样子了。为了更好的管理仓库,你决定,每个进来拿货的人,都必须出示相关的凭证。凭证上必须有身份证明,如果你是来取货,那就还必须要有取货证明,如果是来运货,那就必须有运货证明。你每天守在大门前,兢兢业业的工作,但是却发现,随着生意到了旺季,每天来来往往的人多起来,你一个人恨不得分身成 8 个,4 个负责管进货的,4 个管出货的。仓库门前那惟一一条路都快被来往的汽车车轮上的泥给染成黄色了。累得够呛。感觉身体透支了。你必须想办法解决下这个问题。 23 | 24 | 为了能够快速的检查凭证,你在仓库入口的最前面搭了个小棚,专门检查他们的凭证。而且,为了节约时间,不让他们一拿就拿一堆资料出来,你规定所以的凭证相关信息都必须分门别类的写到一张纸上。你现在只看他们一张纸!是的,管理员就是可以为所欲为,通过了验证就盖个章;等他们凭证检查通过之后,再根据他们凭证上提供的信息,看看他们是要干什么。你多开了两个仓库进出口,进货的人就从左边走,出货的从右边走,来打理仓库的从中间走。不让他们都挤到一条路上。统一从后门出去。进货的顺着左边那条路只能到一个空空的仓库间,供给他们卸货;出货的顺着右边的路只能到一个小小的出货间,他们把车后面的仓库门对准出货间的出口,货物就会顺着流水线自己划到他们的车上。当然,哪些货物可以被运到车上都是由在门口小棚的你来通过电脑全权控制的。打理仓库的人只能进到自己的仓库间,随便他们在仓库间里做什么,做完了就从后门走;所有的货物都用大纸箱包装,进行统一的管理。这样,你每天就坐在小棚里,既没有以前那没累了,工作也更加的有条理。效率提高了一大截。 25 | 26 | 这个小故事编的有些蹩脚,不过它还是完美诠释了对于 REST 的理解。我们的数据库就是小故事中的仓库,而 URL 就是我们的大门。 在以前,对数据库做增加或者修改的操作都是由 POST 来完成的。 正如你改革仓库之前的那样,大家都走一条路。 在改革之后,大家各走各的路。 我们的 URL 也需要各走各的路。 往数据库增加东西,请你用 POST 方法请求,修改请你用 PUT 方法来访问。 27 | 查询数据库,请你用 GET 请求。 当然,最重要的是,把凭证都写到一张纸上。 这张纸是什么呢?就是我们的请求头,请不要 POST 里面有你的 UserAgent 信息,也不要把你的验证信息放在 POST 里,把你是谁,你想干什么,你的权限有哪些都放到请求头里。好让我一看你的请求头就知道你要干什么。 28 | 29 | 到这里,你可能还是会没多大感觉。不就是 POST 增加, PUT 修改, GET 获取详细信息吗?看看我们在故事里还遗漏了什么?那就是我们多开了出口。出口是什么?就是个管进出的大门吗?请你换种思维。进出口,也是一种资源。进出口也是你管理的众多资源之一。同样的道理,请求头也是资源。 30 | 你修路增加进出口,是在改变“进出口”这个资源,你给凭证上面盖个章,是在改变凭证这个资源。所以,URL 的 Header 也是资源。在用户访问一次之后,你给用户的 cookie 做变动的时候,就是在改变 cookie 这个资源。当你决定用 PUT 来修改资源的时候,是在改变请求方式这个资源。 不仅仅只有数据库是资源。把仓库里的货物规定统一用大纸箱包装,不管这些货物原来是什么样子,在他们从仓库里运出来的时候看到的就是一个个个大纸箱。统一他们的规格。我们的 API 也是同理,不管你后端的资源是什么形式,我可以用 JSON ,可以用 XML 来表示他们。这些资源可能原来是在数据库里的二进制数据,也可能是文本文档,也可能是一个 excel 表格。但是当他们被展示在前端时,他们都统一转化为了一模一样的形式。 31 | 32 | 所以 REST 是什么,REST 是一套对资源进行操作和展示的规范。这套规范虽然出生于一篇关于网络的论文,但是这并不妨碍将它运用到仓库管理上。 33 | 34 | ##设计 API 35 | 在了解 REST 的核心概念——一切皆资源之后,让我们来看看,它是怎么具体运用到 API 上的。 36 | 很简单,就以下几个点。 37 | 38 | 39 | - 一切皆是资源。不仅仅是对数据库的请求是资源。包括网页中的图片,音乐, cookie ,session 40 | 都是资源。 41 | - 展示资源。原来的资源可能是表格,可能是文本。但是可以用 XML, JSON 等方式来展示他们 42 | - 每一个资源的每一个状态都应该有唯一的操作方式。 43 | 44 | - POST 增加资源 45 | - GET 请求资源 46 | - PUT 修改资源 47 | - DELETE 删除资源 48 | 49 | 50 | 所以,资源——视一切为资源,展示——不管资源原来是什么形式,可以用 XML、JSON 等来展示他们,状态——被增加、被修改、被查看、被删除。连起来就是 资源的表现层状态转换。 这下就清晰多了。 51 | 52 | 在 API 的设计共识里,我们的 API 应该包含以下几点: 53 | 54 | - 版本信息。让 API 的使用者清晰的知道使用的 API 是什么版本。 55 | - 使用名词的复数形式。比如: `http://example.com/users/` 56 | - 请求动词分工明确。GET 就仅仅是请求资源,不会对被请求的资源有任何影响。 POST 就是增加资源,不能修改资源。 57 | - 命名规范统一,不能混用。 58 | - 驼峰式。`http://www.example.com/oldMan/` 59 | - 蛇形。`http://www.example.com/old_man/` 60 | - 内容协商。内容协商就是,用户希望以什么形式来展现资源。可以是 JSON 可以是 XML 可以是 IMAGE ,可以是 text 。 61 | - 请求的所有元信息都放在请求头。 62 | - 当有其它资源动作时,在不违背基本的资源操作前提下,以 URL 参数的形式传递动作。比如:`http://www.example.com/users/?age=18&sex=girl` 63 | - 返回正确的状态信息。比如大家最常见的 404 。 64 | 65 | 根据以上的知识,我们将要为我们的在线 Python 解释器应用设计 API 。 66 | 67 | 其中,`` 代表 URL 参数 arguement 。 68 | 69 | - 添加代码: 70 | + POST `/v1/codes/` 71 | - 获取所有 code 实例: 72 | + GET `/v1/codes/` 73 | - 获取指定 code 实例: 74 | + GET `/v1/codes//` 75 | - 修改指定 code 实例: 76 | + PUT `/v1/codes//` 77 | - 删除指定 code 实例: 78 | + DELETE `/v1/codes//` 79 | - 运行代码: 80 | + POST `/v1/codes/run/` 81 | 为什么用 POST ? 因为运行代码也是向后台传送新的代码资源的方式。 82 | - 运行特定代码实例: 83 | + GET `/v1/codes/run//` 84 | - 运行并保存代码实例: 85 | + POST `/v1/codes/run/?save=true` 86 | - 修改并运行特定代码实例: 87 | + PUT `/v1/codes/run//?save=true` 88 | 89 | 要是用 Django 的方式把我们的 URL 写出来的话,就是这个样子的: 90 | 91 | ```python 92 | code_api = [ 93 | url(r'^$', generic_code_view, name='generic_code'), # code 集合操作 94 | url(r'^(?P\d*)/$', detail_code_view, name='detail_code'), # 访问某个特定对象 95 | url(r'^run/$', run_code_view, name='run_code'), # 运行代码 96 | url(r'^run/(?P\d*)/$', run_code_view, name='run_specific_code') # 运行特定代码 97 | ] 98 | api_v1 = [url('^codes/', include(code_api))] # API 的 v1 版本 99 | api_versions = [url(r'^v1/', include(api_v1))] # API 的版本控制入口 URL 100 | urlpatterns = [url(r'^api/', include(api_versions))], # API 访问 URL 101 | ``` 102 | 103 | 感觉 URL 的复杂度一下子就提升了不少。淡定,仔细看看它的结构,这样的结构随时可以供我们随时进行修改。比如,我们的 API 升级换代了,随时可以添加一个 `api_v2` 版本进去,而不用去硬编码的修改原来的 API 。这样既可以做到向后兼容,也可以方便的拓展。 104 | 当然,你可以先去体验一下。本章的代码我已经写好。大家可以直接运行试试看,我已经准备好了几个数据进去,运行 `python manage.py runserver` ,访问 `http://127.0.0.1:8000`。打开控制台,看看在不同操作下都发送了哪些请求。代码在[这里](https://github.com/Ucag/django-rest/tree/master/Chapter-two/online_intepreter_project)。 105 | 106 | ## 开发应用 107 | 108 | ###前期的知识准备 109 | 上部分只会讲解最基本的技术和知识,正式的动手写代码会在下部分进行。 110 | ####Mixin 111 | Mixin 技术是面向对象编程的一个重要技能。它使得代码的结构更加灵活,提高了代码的复用性。在实现一个功能时,根据需要的 Mixin 搭积木就好了,特别的方便。在我们的应用中,我们将会大量的使用 Mixin 技术,并且,Mixin 技术在 Django 中也被频繁的应用。所以掌握 Mixin 是很必要的。 112 | Mixin 技术能够产生,还是得益于对象的可继承性。 113 | 114 | 比如我有一个对象叫做 `Man`: 115 | 116 | ```python 117 | class Man: 118 | def __init__(self,name, age, height): 119 | self.name = name 120 | self.age = age 121 | self.height = height 122 | ``` 123 | 124 | 我想让他会抽烟,于是我写了个抽烟 Mixin: 125 | 126 | ```python 127 | class SmokeMixin: 128 | def smoke(self): 129 | print('饭后一支烟,赛过活神仙!') 130 | ``` 131 | 132 | 我还想让他会挣钱,于是写了个挣钱 Mixin: 133 | 134 | ```python 135 | class MakeMoneyMixin: 136 | def make_money(self): 137 | print('面向工资编程中') 138 | ``` 139 | 140 | 我想让他有个老婆: 141 | 142 | ```python 143 | class WifeMixin: 144 | wife = None 145 | def wife(self): 146 | return '我的老婆叫{}'.format(self.wife) 147 | ``` 148 | 149 | 最后,这个 `Man` 成了这样的人: 150 | 151 | ```python 152 | class MyMan(SmokeMixin, WifeMixin, MakeMoneyMixin): 153 | wife = '王翠花' 154 | 155 | my_man = MyMan(name='老王',age=30,height=170) 156 | my_man.smoke() # 饭后一支烟,赛过活神仙! 157 | my_man.make_money() # 面向工资编程中 158 | my_man.wife() # 我的老婆叫王翠花 159 | ``` 160 | 161 | 但是每个男人都是不同的,因为有的男人也单身: 162 | 163 | ```python 164 | class SingleMan(SmokeMixin, MakeMoneyMixin): 165 | pass 166 | sig_man = SingleMan(name='老李',age=25,height=165) 167 | sig_man.smoke() # 饭后一支烟,赛过活神仙! 168 | sig_man.make_money() # 面向工资编程 169 | ``` 170 | 171 | 这样我们就可以创造出很多个不同的男人,他们有相同点,也有不同点,我们要做的仅仅是把 Mixin 积木搭上去。我们的第二个男人的定义甚至只有一个 `pass` 。这种写法在大型工程里非常常见,只有一个 `pass` ,但是却有着丰富的功能。我们项目的视图就使用了 Mixin 技术: 172 | 173 | ```python 174 | class APICodeView(APIListMixin, # 获取列表 175 | APIDetailMixin, # 获取当前请求实例详细信息 176 | APIUpdateMixin, # 更新当前请求实例 177 | APIDeleteMixin, # 删除当前实例 178 | APICreateMixin, # 创建新的的实例 179 | APIMethodMapMixin, # 请求方法与资源操作方法映射 180 | APIView): # 记得在最后继承 APIView 181 | model = CodeModel # 传入模型 182 | 183 | def list(self): # 这里仅仅是简单的给父类的 list 函数传参。 184 | return super(APICodeView, self).list(fields=['name']) 185 | 186 | ``` 187 | 188 | 这已经是 CodeAPI 完整代码了,我们并没有在视图中手动的编写功能,只是简单的继承了 Mixin ,我们甚至都没有写 `get`,`post`, 等方法,在 Mixin 中也没有写!这些神奇的功能和特性都是由 Mixin 提供的。具体的编写方法我们将会在第二章的下部分进行讲解。保持你的好奇心,或者现在翻到文章开头,去 github 看看这些神奇的 Mixin 是如何编写的。 189 | ####状态 190 | 这是前端的知识点。在大家编写前端的时候,一定要把逻辑思维转换为视觉思维。比如,当你看到一盏灯亮了的时候,你的第一反应一定是“灯亮了”,而不是“电流通过导线,让灯丝发热发光了”。你看到一辆汽车从禁止变成了运动的状态,你不可能想到的是“摩擦力反作用于车轮让汽车动了起来”。所以,当页面中的 UI 发生变化时,变化的是 UI 的状态。一盏灯有熄灭和点亮两种状态,一辆车有发动和禁止的状态,一个人有吃饭,睡觉,走路等状态。拿我们应用中的表格来说,它的状态就是有多少行。比如,原来只有 5 行,你添了一行上去,就变成了 6 行,此时这张表格从有 5 行的状态变成了 6 行的状态。 这就是状态。 191 | UI 的状态,就是它的样子,UI 的样子是由它相关联的数据决定的。这是很重要的概念,在我们编写我们项目的前端时会发挥巨大作用。同时,这也是为讲解 flux 和 Vuex 打下基础。 192 | 193 | --- 194 | 第二章的上部分就结束了。在下部分,我们将会真正开始构建我们的应用。让你的应用告别缓慢的刷新,笨搓搓的 POST 之后还要再刷新整个页面。同时,我们也将会构建我们的静态文件服务。我们下周见。 195 | 196 | 197 | 198 | 199 | 200 | 201 | -------------------------------------------------------------------------------- /Chapter-three/online_python/frontend/index.js: -------------------------------------------------------------------------------- 1 | //管理 API 2 | let api = { 3 | v1: { 4 | run: function () { 5 | return '/api/v1/run/' 6 | }, 7 | code: { 8 | list: function () { 9 | return '/api/v1/code/' 10 | }, 11 | create: function (run = false) { 12 | let base = '/api/v1/code/'; 13 | return run ? base + '?run' : base 14 | }, 15 | detail: function (id, run = false) { 16 | let base = `/api/v1/code/${id}/`; 17 | return run ? base + '?run' : base 18 | }, 19 | remove: function (id) { 20 | return api.v1.code.detail(id, false) 21 | }, 22 | update: function (id, run = false) { 23 | return api.v1.code.detail(id, run) 24 | } 25 | } 26 | } 27 | } 28 | 29 | //Store 30 | //Store中包含了 state 和 actions ,这些操作都是和数据直接相关的。 31 | let store = { 32 | state: { 33 | list: [], 34 | code: '', 35 | name: '', 36 | id: '', 37 | output: '' 38 | }, 39 | actions: { 40 | run: function (code) { //运行代码 41 | $.post({ 42 | url: api.v1.run(), 43 | data: {code: code}, 44 | dataType: 'json', 45 | success: function (data) { 46 | store.state.output = data.output 47 | } 48 | }) 49 | }, 50 | runDetail: function (id) { //运行特定的代码 51 | $.getJSON({ 52 | url: api.v1.run() + `?id=${id}`, 53 | success: function (data) { 54 | store.state.output = data.output 55 | } 56 | }) 57 | }, 58 | freshList: function () { //获得代码列表 59 | $.getJSON({ 60 | url: api.v1.code.list(), 61 | success: function (data) { 62 | store.state.list = data 63 | } 64 | }) 65 | }, 66 | getDetail: function (id) {//获得特定的代码实例 67 | $.getJSON({ 68 | url: api.v1.code.detail(id), 69 | success: function (data) { 70 | store.state.id = data.id; 71 | store.state.name = data.name; 72 | store.state.code = data.code; 73 | store.state.output = ''; 74 | } 75 | }) 76 | }, 77 | create: function (run = false) { //创建新代码 78 | $.post({ 79 | url: api.v1.code.create(run), 80 | data: { 81 | name: store.state.name, 82 | code: store.state.code 83 | }, 84 | dataType: 'json', 85 | success: function (data) { 86 | if (run) { 87 | store.state.output = data.output 88 | } 89 | store.actions.freshList() 90 | } 91 | }) 92 | }, 93 | update: function (id, run = false) { //更新代码 94 | $.ajax({ 95 | url: api.v1.code.update(id, run), 96 | type: 'PUT', 97 | data: { 98 | code: store.state.code, 99 | name: store.state.name 100 | }, 101 | dataType: 'json', 102 | success: function (data) { 103 | if (run) { 104 | store.state.output = data.output 105 | } 106 | store.actions.freshList() 107 | } 108 | }) 109 | }, 110 | remove: function (id) { //删除代码 111 | $.ajax({ 112 | url: api.v1.code.remove(id), 113 | type: 'DELETE', 114 | dataType: 'json', 115 | success: function (data) { 116 | store.actions.freshList() 117 | } 118 | }) 119 | } 120 | } 121 | } 122 | 123 | store.actions.freshList() // Store的初始化工作,先获取代码列表 124 | 125 | let list = { //代码列表组件 126 | template: ` 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 142 | 143 | 144 |
文件名选项
{{ item.name }} 138 | 139 | 140 | 141 |
145 | `, 146 | data() { 147 | return { 148 | state: store.state 149 | } 150 | }, 151 | methods: { 152 | getDetail(id) { 153 | store.actions.getDetail(id) 154 | }, 155 | run(id) { 156 | store.actions.runDetail(id) 157 | }, 158 | remove(id) { 159 | store.actions.remove(id) 160 | } 161 | } 162 | } 163 | let options = {//代码选项组件 164 | template: ` 165 |
168 | 169 | 170 | 171 | 172 |
173 | `, 174 | data() { 175 | return { 176 | state: store.state 177 | } 178 | }, 179 | methods: { 180 | run(code) { 181 | store.actions.run(code) 182 | }, 183 | update(id, run = false) { 184 | if (typeof id == 'string') { 185 | store.actions.create(run) 186 | } else { 187 | store.actions.update(id, run) 188 | } 189 | }, 190 | newOptions() { 191 | this.state.name = ''; 192 | this.state.code = ''; 193 | this.state.id = ''; 194 | this.state.output = ''; 195 | } 196 | } 197 | } 198 | let output = { //代码输出组件 199 | template: ` 200 | 202 | `, 203 | data() { 204 | return { 205 | state: store.state 206 | } 207 | }, 208 | updated() { 209 | let ele = $(this.$el); 210 | ele.css({ 211 | 'height': 'auto', 212 | 'overflow-y': 'hidden' 213 | }).height(ele.prop('scrollHeight')) 214 | } 215 | } 216 | let input = { //代码输入组件 217 | template: ` 218 |
219 | 224 | 225 |

如需保存代码,建议输入代码片段名

226 | 231 |
232 | `, 233 | data() { 234 | return { 235 | state: store.state 236 | } 237 | }, 238 | methods: { 239 | flexSize(selector) { 240 | let ele = $(selector); 241 | ele.css({ 242 | 'height': 'auto', 243 | 'overflow-y': 'hidden' 244 | }).height(ele.prop('scrollHeight')) 245 | }, 246 | inputHandler(e) { 247 | this.state.code = e.target.value; 248 | this.flexSize(e.target) 249 | } 250 | } 251 | } 252 | 253 | let app = { //整体页面布局 254 | template: ` 255 |
256 |
257 | 在线 Python 解释器 258 |
259 |
260 |
261 |
262 | 263 |
264 |
265 |
266 |
267 |

请在下方输入代码:

268 | 269 |
270 | 271 |
272 |

输出

273 |
274 | 275 |
276 |
277 |
278 |
279 |
280 | `, 281 | components: { 282 | 'code-input': input, 283 | 'code-list': list, 284 | 'code-options': options, 285 | 'code-output': output 286 | } 287 | } 288 | 289 | let root = new Vue({ //根组件,整个页面入口 290 | el: '#app', 291 | template: '', 292 | components: { 293 | 'app': app 294 | } 295 | }) 296 | 297 | -------------------------------------------------------------------------------- /Chapter-two/online_intepreter_project/frontend/js/main.js: -------------------------------------------------------------------------------- 1 | //api 2 | // 将 api 单独写在一起的好处包括但不限于: 3 | // 1. 方便维护。不把 api 写死在相应动作下,一旦 api 有变动,直接在这里变动就好 4 | // 2. 写成函数形式,这样可以动态的生成 api ,特别时对有 pk 或者 id 参数的 api 特别有用 5 | // 3. 将 api 统一化管理,为以后的模块化做好拓展准备。 6 | // 4. api 的语义清晰,大大降低人为应编码造成的错误。最常见的例子是忘了给 api 最后加上参数或者 / 。 7 | const api = { 8 | v1: { // api 版本号 9 | codes: { // api v1 版本下的 code api。 10 | list: function () { //获取实例查询集 11 | return '/api/v1/codes/' 12 | }, 13 | detail: function (pk) { // 获取单一实例 14 | return `/api/v1/codes/${pk}/` 15 | }, 16 | create: function () { // 创建实例 17 | return `/api/v1/codes/` 18 | }, 19 | update: function (pk) { // 更新实例 20 | return `/api/v1/codes/${pk}/` 21 | }, 22 | remove: function (pk) { //删除实例 23 | return `/api/v1/codes/${pk}/` 24 | }, 25 | run: function () { //运行代码 26 | return '/api/v1/codes/run/' 27 | }, 28 | runSave: function () {// 保存并运行代码 29 | return '/api/v1/codes/run/?save=true' 30 | }, 31 | runSpecific: function (pk) { // 运行特定代码实例 32 | return `/api/v1/codes/run/${pk}/` 33 | }, 34 | runSaveSpecific: function (pk) { // 运行并保存特定代码实例 35 | return `/api/v1/codes/run/${pk}/?save=true` 36 | } 37 | } 38 | } 39 | }; 40 | 41 | //接下来的代码包含了前端中极其重要的设计思想,请大家认真学习。 42 | //现在我们是在编写前端,所以需要大家从后端的思维模式中切换出来。前端,顾名思义,是为了 43 | //展示数据而存在的,当数据发生变化时, 与数据相关联的 UI 也应该随着数据的变化而作出相应的动作。 44 | //在我们的项目中,当我们点击运行按钮时,如果后端传来了输出数据,那么此时的输出框就应该作出相应的 45 | //更新操作——将结果显示在浏览器上。所以,UI 的显示“状态”应该是以数据为基础的。数据变动则 UI 变动。 46 | //所以,在前端中,把 UI 相关的数据称为 “状态” 。比如结果输出框就有两个状态,“显示结果”与“不显示结果” 47 | //的状态,如何切换状态则取决于输出框想连接的数据有没有变动。 48 | //以上,便是 “状态” 的概念。但是我们的 UI 怎么才能知道数据是否变动呢?那就是不断的查询相关的数据 49 | //是否改变。那么问题就来了,我们应该怎么定义我们的数据呢? 50 | //一种是把数据定义为全局变量,UI 不停的在全局变量中看这个值更新没有,更新了就刷新状态。看起来这样 51 | //没什么毛病,但是当你的 UI 状态多起来之后问题就逐渐的显现了。到底该怎么命名状态才能清晰的表达 52 | //每个状态的意思?到底怎么才能不让其他的变量名把我们的状态覆盖掉,不然出现莫名其妙的 bug? 53 | //第二种是把数据单独放在一个单独的对象下。就像在浏览器中,所有的全局变量都放在 window 下一样, 54 | //我们单独把状态变量放到一个变量中,然后访问这个变量就可以了。 55 | //显然,我们应该采用第二种方法。那问题又来了,我们该怎么检测状态是否变化呢? 56 | //一种是把检测的逻辑写在 UI 中,将上一次的值保存下来,然后不停对比状态的上一个值和当前值 57 | //是否相同,不同就刷新 UI 。同样的,如果同一个状态变量有多种状态,那你的检测逻辑就需要 58 | //考虑每种情况,然后依次写下来。这样做不仅很累人,而且也不够灵活。第二种,是给状态再加一个 sigin (签名) 59 | //或者 signal (信号),当状态变动时,便发出状态改变的信号或者修改签名。这样,UI 只需要检测 60 | //这个签名或者信号,只在发出改变信号时才会读取状态数据,作出相应的 UI 动作。 61 | //显然,我们应该采用第二种方法。 62 | //到这里或许已经有同学发现,这不是和我们后端的 Model 的概念很相似吗?是的,没错。它们两者 63 | // 很相似。保持你的好奇心,在我们学 vue 时会继续深入这些概念。 64 | 65 | //下面是一种实现方式,我们把储存状态的变量叫做 store,store 在英文中有储存的意思。 66 | //同时我们的后端数据库也是用来储存数据的,叫这个名字很贴切。 67 | //我们把不同的状态放在 store 每个状态有 state 和 changed 属性,state 用来储存 UI 相关联 68 | //的变量信息,changed 作为状态是否改变的信号。UI 只需要监听 chagned 变量,当 changed 为 true 69 | //时才读取并改变状态。 70 | //我们目前的状态很少,也很简单。我们在下面先初始化他们 71 | 72 | let store = { 73 | list: { //列表状态 74 | state: undefined, 75 | changed: false 76 | }, 77 | detail: { //特定实例状态 78 | state: undefined, 79 | changed: false 80 | }, 81 | output: { //输出状态 82 | state: undefined, 83 | changed: false 84 | } 85 | }; 86 | 87 | //改变状态的动作 88 | //这些动作负责调用 API ,并接受 API 返回的数据,然后将这些数据保存进 store 中。 89 | //注意,在修改完状态之后,记得将状态的 changed 属性改为 true ,不然状态不会刷新到监听的 UI 上。 90 | 91 | //从后端返回的数据中,把实例相关的数据处理成我们期望的形式,好方便我们调用 92 | function getInstance(data) { 93 | let instance = data.fields; 94 | instance.pk = data.pk; 95 | return instance 96 | } 97 | 98 | //获取 code 列表,更改 list 状态 99 | function getList() { 100 | $.getJSON({ 101 | url: api.v1.codes.list(), 102 | success: function (data) { 103 | store.list.state = data.instances; 104 | store.list.changed = true; 105 | } 106 | }) 107 | } 108 | //创建实例,并刷新 list 状态。 109 | function create(code, name) { 110 | $.post({ 111 | url: api.v1.codes.create(), 112 | data: {'code': code, 'name': name}, 113 | dataType: 'json', 114 | success: function (data) { 115 | getList(); 116 | alert('保存成功!'); 117 | } 118 | }) 119 | } 120 | //更新实例,并刷新 list 状态。 121 | function update(pk, code, name) { 122 | $.ajax({ 123 | url: api.v1.codes.update(pk), 124 | type: 'PUT', 125 | data: {'code': code, 'name': name}, 126 | dataType: 'json', 127 | success: function (data) { 128 | getList(); 129 | alert('更新成功!'); 130 | } 131 | }) 132 | } 133 | //获取实例,并刷新 detail 状态 134 | function getDetail(pk) { 135 | $.getJSON({ 136 | url: api.v1.codes.detail(pk), 137 | success: function (data) { 138 | let detail = getInstance(data.instances[0]); 139 | store.detail.state = detail; 140 | store.detail.changed = true; 141 | } 142 | }) 143 | } 144 | //删除实例,并刷新 list 状态 145 | function remove(pk) { 146 | $.ajax({ 147 | url: api.v1.codes.remove(pk), 148 | type: 'DELETE', 149 | dataType: 'json', 150 | success: function (data) { 151 | getList(); 152 | alert('删除成功!'); 153 | } 154 | }) 155 | } 156 | //运行代码,并刷新 output 状态 157 | function run(code) { 158 | $.post({ 159 | url: api.v1.codes.run(), 160 | dataType: 'json', 161 | data: {'code': code}, 162 | success: function (data) { 163 | let output = data.output; 164 | store.output.state = output; 165 | store.output.changed = true; 166 | } 167 | }) 168 | } 169 | //运行保存代码,并刷新 output 和 list 状态。 170 | function runSave(code, name) { 171 | $.post({ 172 | url: api.v1.codes.runSave(), 173 | dataType: 'json', 174 | data: {'code': code, 'name': name}, 175 | success: function (data) { 176 | let output = data.output; 177 | store.output.state = output; 178 | store.output.changed = true; 179 | getList(); 180 | alert('保存成功!'); 181 | } 182 | }) 183 | } 184 | //运行特定的代码实例,并刷新 output 状态 185 | function runSpecific(pk) { 186 | $.get({ 187 | url: api.v1.codes.runSpecific(pk), 188 | dataType: 'json', 189 | success: function (data) { 190 | let output = data.output; 191 | store.output.state = output; 192 | store.output.changed = true; 193 | } 194 | }) 195 | } 196 | //运行并保存特定代码实例,并刷新 output 和 list 状态 197 | function runSaveSpecific(pk, code, name) { 198 | $.ajax({ 199 | url: api.v1.codes.runSaveSpecific(pk), 200 | type:'PUT', 201 | dataType: 'json', 202 | data: {'code': code, 'name': name}, 203 | success: function (data) { 204 | let output = data.output; 205 | store.output.state = output; 206 | store.output.changed = true; 207 | getList(); 208 | alert('保存成功!'); 209 | } 210 | }) 211 | } 212 | 213 | //UI 的动作逻辑 214 | 215 | //使输入框随着输入内容变动大小 216 | function flexSize(selector) { 217 | let ele = $(selector); 218 | ele.css({ 219 | 'height': 'auto', 220 | 'overflow-y': 'hidden' 221 | }).height(ele.prop('scrollHeight')) 222 | } 223 | //将动态变动大小的动作绑定到输入框上 224 | $('#code-input').on('input', function () { 225 | flexSize(this) 226 | }); 227 | 228 | //渲染列表动作 229 | function renderToTable(instance, tbody) { 230 | let name = instance.name; 231 | let pk = instance.pk; 232 | let options = `\ 233 | \ 234 | \ 235 | `; 236 | let child = `${name}${options}`; 237 | tbody.append(child); 238 | } 239 | 240 | //渲染代码选项 241 | 242 | //当点击查看代码时,渲染代码选项的动作 243 | 244 | //我们使用模板字符串来让每个按钮能出发相应的状态修动作 245 | function renderSpecificCodeOptions(pk) { 246 | let options = `\ 247 | \ 248 | \ 250 | \ 252 | `; 253 | $('#code-options').empty().append(options);// 先清除之前的选项,再添加当前的选项 254 | } 255 | 256 | //当点击新建代码时,渲染代码选项的动作 257 | function renderGeneralCodeOptions() { 258 | let options = `\ 259 | \ 260 | \ 262 | \ 264 | `; 265 | $('#code-input').val('');// 清除输入框 266 | $('#code-output').val('');// 清除输出框 267 | $('#code-name-input').val('');// 清除代码片段名输入框 268 | flexSize('#code-output');// 由于我们在改变输入、输出框的内容时并没有出发 ‘input’ 事件,所以需要手动运行这个函数 269 | $('#code-options').empty().append(options);// 清除的之前的选项,再添加当前的选项 270 | } 271 | 272 | //UI 监听逻辑 273 | // watcher 会迭代 store 的每个属性,一旦某个状态发生改变,就会执行相应的 UI 动作 274 | //记得,在执行完相应的 UI 动作后,要把 changed 状态改回去,不然 UI 会一直不断的刷新,刷新会 275 | //根本停不下来。你看到的 UI 会在一瞬间就会替换为下一个 UI。 276 | function watcher() { 277 | for (let op in store) { 278 | switch (op) { 279 | case 'list':// 当 list 状态改变时就刷新页面中展示 list 的 UI,在这里,我们的 UI 一个 。 280 | if (store[op].changed) { 281 | let instances = store[op].state; 282 | let tbody = $('tbody'); 283 | tbody.empty(); 284 | for (let i = 0; i < instances.length; i++) { 285 | let instance = getInstance(instances[i]); 286 | renderToTable(instance, tbody); 287 | } 288 | store[op].changed = false; // 记得将 changed 信号改回去哦。 289 | } 290 | break; 291 | case 'detail': 292 | if (store[op].changed) {// 当 detail 状态改变时,就更新 代码输入框,代码片段名输入框,结果输出框的状态 293 | let instance = store[op].state; 294 | $('#code-input').val(instance.code); 295 | $('#code-name-input').val(instance.name); 296 | $('#code-output').val('');// 记得请空上次运行代码的结果哦。 297 | flexSize('#code-input');// 同样的,没有出发 'input' 动作,就要手动改变值 298 | renderSpecificCodeOptions(instance.pk);// 渲染代码选项 299 | store[op].changed = false;// 把 changed 信号改回去 300 | } 301 | break; 302 | case 'output': 303 | if (store[op].changed) { //当 output 状态改变时,就改变输出框的的状态。 304 | let output = store[op].state; 305 | $('#code-output').val(output); 306 | flexSize('#code-output');// 记得手动调用这个函数。 307 | store[op].changed = false // changed 改回去 308 | } 309 | break; 310 | } 311 | } 312 | } 313 | //将UI主逻辑添加到时间队列中 314 | 315 | getList();// 初始化的时候我们应该手动的调用一次,好让列表能在页面上展示出来。 316 | renderGeneralCodeOptions();// 手动调用一次,好让代码选项渲染出来 317 | setInterval("watcher()", 500);// 将 watcher 设置为 500 毫秒,也就是 0.5 秒就执行一次, 318 | // 这样就实现了 UI 在不断的监听状态的变化。 319 | 320 | 321 | -------------------------------------------------------------------------------- /Chapter-three/Django REST 系列教程(三)(中).md: -------------------------------------------------------------------------------- 1 | #Django RESTful 系列教程(三)(中) 2 | --- 3 | 在上一节中我们了解了 DRF ,现在我们要开始学习 Vue 了,这一前端神器。同样的,这不是官方文档复读机,我们会讲一些官方文档没有讲的东西,如果你对本节中所涉及到的东西想有更深的了解, [vue 官方文档](https://cn.vuejs.org/)是个好去处。一个好消息是,Vue 与 Django 的原理有着相通之处,大家应该可以很轻松的掌握,只是有一些小的知识点细节需要明确。同时 js 和 py 都支持面向对象编程,所以大家在看教程时,着重联系面向对象的编程思维,如果 js 代码不理解,那就想想同样的 python 代码是怎么写的。 4 | 5 | 本章会涵盖以下知识点: 6 | 7 | 1. Vue 原理。 8 | 2. 认识组件。 9 | 3. Vue 特色语法。 10 | 11 | >Vue 的中文文档已经非常优秀,作为国人开发的框架,对国人是非常友好的,并且官方文档的入门教程是真的不错,强烈建议大家去看看。 12 | 13 | ##Vue 原理 14 | 对于 Vue ,很多人应该听说过 [MVVM ](https://en.wikipedia.org/wiki/Model_View_ViewModel)模型,但是同 MTV 一样,很少有人能清楚的解释这到底是怎么回事。 15 | 16 | ![MVVM 模型](https://images.contentful.com/emmiduwd41v7/7zcSy3ZHmEGI0cykSa6eKy/2a6efbc6ceb05cc0484bcf6b403b7b3c/Image2.JPG) 17 | 18 | 图片来源:https://academy.realm.io/cn/posts/mobilization-lukasz-mroz-mvvm-coordinators-rxswift/ 19 | 20 | View: 展示数据的部分,也就是我们可以在页面上看到的 UI 。 View 使用 ViewModel 来做出对应的状态改变。同时,View 不会也不能进行改变数据的操作,它是通过 ViewModel 来修改数据的。也就是说,这里的 View 就真的只是个 View ,就像是被渲染之后的模板一样,就是一个 html 文件,什么数据操作都不能做。 21 | 22 | ViewModel: 数据的业务逻辑部分。所有的业务逻辑都在这里。不仅包含数据的处理逻辑,还包括 View 的逻辑都在这里了,所以叫做 ViewModel 。比如我们之前写的 `code-options` 的部分,不同的数据对应不同的 View 状态。这和 Djanogo 的模板很相似,我们在模板中编写了 html 相应的逻辑,最后由模板引擎渲染成固定的 html 文档。大家可以把这部分理解为前端的模板引擎,不仅包含视图逻辑,还包含对后端的数据处理。 23 | 24 | Model: 储存数据的地方。也就是我们的 Store 了,它负责向后端 API 发起请求,储存收到的数据。 25 | 26 | 我们来看看这样一个 MVVM 流程是怎样走完的: 27 | 28 | 1. 用户看到了一个按钮(View),点击了它(View 发出信号,ViewModel 捕捉信号)。 29 | 2. ViewModel 收到信号,根据 View 逻辑,此时应该从 Store 中获取数据,数据在处理之后,数据传到 View 中。 30 | 3. Store 收到请求,发现现在不需要重新请求数据,就直接把数据给了发起请求的 ViewModel 。 31 | 32 | 所以,总结一下,MVVM 的唯一不同之处就是把视图逻辑和数据逻辑放在了一起,称为“ViewModel”,View 的逻辑也被看成了是数据的一部分。剩下的部分和 Django 其实差不多。 33 | 34 | ##认识组件 35 | 在开始之前,我们需要做一些准备工作。建立如下文件结构: 36 | 37 | ``` 38 | vue_learn/ 39 | vue.js 40 | index.js 41 | bootstrap.js 42 | jquery.js 43 | bootstrap.css 44 | index.html 45 | ``` 46 | 47 | vue.js: vue 的源文件,可以直接从[这里](https://vuejs.org/js/vue.js)复制粘贴,也可以直接从我的 github 仓库中拉取。 48 | index.js: 空文件,我们将会在这里学习 vue 。 49 | bootstrap.js: bootstrap 的 js 文件。可以从上一章的项目中复制。 50 | bootstrap.css: bootstrap 的 css 文件。可以从上一章的项目中复制。 51 | jquery.js: bootstrap.js 的依赖,必须使用。可以从上一章的项目中复制。 52 | 在 index.html 中写入下列代码: 53 | 54 | ```html 55 | 56 | 57 | 58 | Vue-learn 59 | 60 | 61 | 62 |
63 | 64 | 65 | 66 | 67 | 68 | 69 | ``` 70 | 71 | 准备工作做完了。编辑器中打开你的 index.js 和 index.html,同时用浏览器打开 index.html ,以便我们随时查看编写的效果。这里需要你调整好自己的窗口规划,最好能够使用多任务桌面,如果你使用的 win7 ,`Dexport` 是个支持这项功能的好软件,支持快捷键切换桌面,特别方便,并且还是免费的。 72 | 73 | ###组件 74 | 75 | ####组件是什么? 76 | 正如我们在第一章中编写 html 一样,先把页面的框架搭好,再往每个部分里填 UI ,这些 UI 便被称为组件了。我们的 code-list 是一个组件,code-options 按钮组也是组件,我们在需要的时候渲染它们。就像是搭积木一样,组件是积木,网页就是我们用不同的积木搭建起来的堡垒。 77 | 78 | 把眼光放的再开一点看,我们的框架结构本身,也是个组件。只是这些组件没有形状,只有结构,等待其它组件被填充进去。在页面中,一切皆可为组件,相信大家在了解 REST 一切及资源之后理解这个应该不难。 79 | 80 | 所以,在我们之前写的前端代码中,那些包含模板字符串的函数就是我们组件了,我们可以随时调用他们来搭建页面。 81 | 82 | ####组件是实例。 83 | 因为我们使用的是 Vue ,所以应该使用 Vue 来构建我们组件。我们在 python 知道类和实例的概念,在这里我们说的实例也就是 Vue 类的实例。所以,象这样写我们就有了一个 Vue 实例: 84 | 85 | ```javascript 86 | let vueInstance = new Vue({...}) //{...} 为选项对象 87 | ``` 88 | 89 | 既然是实例,那么 Vue 类有的属性和方法,Vue 实例也就是组件也应该有这些方法。同时,他们的参数,也就是选项对象也应该相同。然而事实是,有少数几个选项只在运行 `new` 时有效。至于为什么这样做,在下面会提到, 90 | 91 | 刚才我们也提到,组件之间可以相互组合,共同构成一个“大组件”,也就是我们的网页。那怎么把 Vue 和页面中的 html 相联系起来呢? 92 | 93 | 如果我们想要使用一个组件,我们需要告诉 Vue 我们需要把这个组件放在哪里。有几种方式可以选择,我们一个一个来看看。 94 | 95 | 第一种,**只**使用 `el` 选项。仅仅在 `new` 时有效。 96 | 97 | `index.html` 98 | ```html 99 |
100 | {{ message }} 101 |
102 | ``` 103 | `index.js` 104 | ```javascript 105 | let cp = new Vue({ 106 | el: '#app', 107 | data:function(){ 108 | return { 109 | message:'Hello Vue!' 110 | } 111 | } 112 | }) 113 | ``` 114 | 保存他们并刷新你的浏览器,你会看到在 html 中本来的 `{{ message }}`部分被替换为了 `Hello Vue!` 。 115 | 116 | 我们使用 `el` 选项,告诉 Vue 我们要把匹配 `#app` 的 html 元素作为组件。把一个元素作为组件,也就是相当于告诉 Vue 我们的组件要放在这里了。 117 | 118 | 看到 `{{ }}` 我相信大家一定都非常熟悉了,这不就是模板的语法吗?那选项中的 `data` 参数是做什么用的呢?正如它的名字一样,这是给组件提供数据的地方。为什么使用的是函数来返回数据而不是直接把 `data` 定义为一个对象呢?保持你的好奇心。这个问题我们之后再来解答,现在你可以简单的就像理解我们在前端管理 API 时所做的那样,为了方便变动数据。 119 | 120 | 第二种方式,使用 `template` **和** `el` 选项。 121 | 122 | `index.html` 123 | ```html 124 |
125 | {{ message }} 126 |
127 |
128 | ``` 129 | `index.js` 130 | ```javascript 131 | let cp2 = new Vue({ 132 | el:'#app2', 133 | template:` 134 |

{{ message }}

`, 135 | data:function(){ 136 | return { 137 | message: 'Hello Component 2!' 138 | } 139 | } 140 | }) 141 | ``` 142 | 保存他们并刷新你的浏览器,你会看到在 html 中,本来的 `
` ,被替换为了 `

Hello Component 2!

` 。 143 | 144 | 我们可以把本来的组件写在 `template` 选项中,使用 `el` 选项告诉 Vue 我们会在哪里放这个组件,Vue 会用 `template` 的内容**替换**被匹配到的元素。替换,也是一种告诉 Vue 我们要把组件放到哪儿的方法。需要注意的是,`template` 只能有一个外层标签,因为有多个的话 Vue 就不知道该把哪个元素替换到目标标签上去。 145 | 146 | 第三种,使用 `$mount` **和** `template`。 147 | 148 | `index.html` 149 | ```html 150 |
151 |
152 | ``` 153 | `index.js` 154 | ```javascript 155 | let cp3 = new Vue({ 156 | template: `

{{ message }}

`, 157 | data: function(){ 158 | return { 159 | message:'Hello Component 3!' 160 | } 161 | } 162 | }) 163 | cp3.$mount('#app3') 164 | ``` 165 | 保存他们并刷新你的浏览器,你会看到在 html 中,本来的 `
` ,被替换为了 `

Hello Component 3!

` 。 166 | 167 | 当没有使用 `el` 指定要把一个组件放在哪里时,这个组件处于“**未挂载**”状态。我们可以在创建一个组件之后,使用其 `.$mount` 方法,将它“**挂载**”到一个元素上,这个元素会被 `template` **替换** 掉。 168 | 169 | ####组合组件 170 | 正如我们刚才所说,组件是可以被“组合”的。按照刚才的写法,我们应该怎样将组件们结合起来呢?也就是说,我们怎样做才能让组件知道有其它的组件存在呢? 171 | 172 | `index.html` 173 | ```html 174 |
175 | ``` 176 | 177 | `index.js` 178 | ```javascript 179 | let cp4 = { 180 | template:'

{{ message }}

', 181 | data:function(){ 182 | return { 183 | message: 'Hello Component 4!' 184 | } 185 | } 186 | } 187 | let cp5 = new Vue({ 188 | el:'#app5', 189 | template:` 190 |
191 | 192 | {{ msg }} 193 |
`, 194 | components:{ 195 | 'cp-4':cp4 196 | }, 197 | data:function(){ 198 | return { 199 | msg:"I'm Component 5!" 200 | } 201 | } 202 | }) 203 | ``` 204 | 保存他们并刷新你的浏览器,你会看到我们的组件成功的被组合在了一起,可以查看一下浏览器,看看他们是否都在同一个`div`下。 205 | 206 | 组合组件的方法就是使用 `components` 选项,我们不需要传给 `components` Vue 实例,只需要传子组件的名字作为属性,它的选项作为值就好了。以上的过程,我们称为“**注册组件**”这样,组件就可以使用在 `components` 中的其它组件了。并且,只需要像使用普通的 html 一样就可以使用它了。 207 | 208 | 刚才说的是“**局部注册**”,也就是只是把组件和某个特定的组件组合起来。但是有时候,我们希望能够“**全局注册**”这个组件,也就是说,我们希望能够在所有的组件中使用它。 Vue 为我们提供了全局注册的方法。 209 | 210 | `index.html` 211 | ```html 212 |
213 |
214 |
215 | ``` 216 | `index.js` 217 | ```javascript 218 | Vue.component('global-cp',{ 219 | template:`

{{ msg }}

`, 220 | data:function(){ 221 | return { 222 | msg:"I'm global!" 223 | } 224 | } 225 | }) 226 | let cp6 = new Vue({ 227 | el:'#app6', 228 | template:`
229 | I'm app6! 230 | 231 |
` 232 | }) 233 | let cp7 = new Vue({ 234 | el:'#app7', 235 | template:`
236 | 237 | {{ msg }} 238 |
`, 239 | data:function(){ 240 | return { 241 | msg:"I'm app7!" 242 | } 243 | } 244 | }) 245 | ``` 246 | 247 | 保存他们并刷新你的浏览器,你会看到我们的全局组件已经起作用了。我们使用的是 Vue 类的方法来添加全局组件。类的就是实例的,所以类有了某个组件,那么用这个类生成的实例也应该有这些组件。 248 | 249 | ####正确的使用组件 250 | 刚才我们说道,一个网页也可以是一个组件。也就是说,我们可以先创建一个空的组件,然后让这个组件来容纳其它的组件,这样我们就可以实现仅仅使用 Vue 就可以对网页进行全权的控制,从而实现许多酷炫的功能。SPA(Single Page Application,单页应用)就是一个很好的范例。整个应用只有一个网址,网页的所有变动都是组件的变动,同时,这也减轻了前端的压力,不用再去写那么多页面,只需要写变化的组件就行了。 251 | 252 | 所以,组件的一般写法是: 253 | 1. 先写一个空的组件作为组件入口。 254 | 2. 通过在这个空的组件中组合其它组件来达到组合成网页的目的 255 | 256 | 删除 `index.html` 中所有的 `div` 元素,删除 `index.js` 中的所有代码,编写代码如下: 257 | 258 | `index.html` 259 | ```html 260 |
261 | ``` 262 | `index.js` 263 | ```javascript 264 | let navBar = { 265 | template:` 266 | 274 | `, 275 | data:function(){ 276 | return { 277 | home:'http://example.com/', 278 | about:'http://example.com/about' 279 | } 280 | } 281 | } 282 | 283 | let mainContent = { 284 | template:` 285 |

{{ content }}

286 | `, 287 | data:function(){ 288 | return { 289 | content:'This is main content!' 290 | } 291 | } 292 | } 293 | let app = { 294 | template:` 295 |
296 |
297 | 298 |
299 |
300 | 301 |
302 |
`, 303 | components:{ 304 | 'nav-bar':navBar, 305 | 'main-content':mainContent 306 | } 307 | } 308 | 309 | let root = new Vue({ 310 | el:'#app', 311 | template:``, 312 | components:{ 313 | 'app':app 314 | } 315 | }) 316 | ``` 317 | 保存他们并刷新你的浏览器,你看到的会是这样: 318 | ![正确使用组件.png]() 319 | 320 | 我们仅仅调用了一次 `new Vue()` ,把 `root` 作为根组件。组件 `root` 基本上是组件 `app` 构成的,为什么不直接把 `app` 的逻辑放入 `root` 中呢?因为我们的 `app` 可能不止一个,可能随时会被替换成其它的 `app` 组件,比如我们的网页有两套布局模式,一套为移动端布局,一套为PC端布局,我们可能会根据需求来更换布局,要是直接写死在根组件里,会十分的不便。 321 | 322 | 关于组件,我们就暂时学到这里。在最后一节,我们将会学习更多关于组件的知识。 323 | 324 | ###Vue 组件特色语法。 325 | 有许多的特殊语法可以在组件中使用,帮助我们提高开发的效率。如同模板一样,它们都有自己的语法。 326 | 327 | ####准备工作 328 | 在 `vue_learn` 下新建一个 html 文档,名为 `grammer.html`,编写如下代码: 329 | 330 | `grammer.html` 331 | ```html 332 | 333 | 334 | 335 | Grammer 336 | 337 | 338 |
339 | 340 | 343 | 344 | 345 | ``` 346 | 我们接下来的大部分工作都将会在空的 `script` 标签中完成。 347 | 348 | ####`{{ }}` 349 | #####data 350 | 在 Django 模板中,`{{ }}` 中是用来写变量的,在渲染时,变量会被替换为相应的变量值。 在 Vue 中,`{{ }}` 也做了同样的事情,但不一样的是,这个变量值是动态的,也就是“响应式”的。 351 | 352 | `grammer.html` 353 | ```html 354 | 372 | ``` 373 | 374 | 保存并在浏览器中打开,你会看到这个效果 ![响应式的数据变化]()。 375 | 376 | 仔细观察我们代码,我们发现: 377 | 378 | 1. 我们并没有直接使用 `app.data.msg` 来改变数据,而是使用 `app.msg` 来改变 `msg` 属性的值。 379 | 2. 虽然我们是在实例化组件之后才改变的 `msg` 属性值,但是数据变化在 UI 上依然被表现出来了。 380 | 381 | 对于第一条,反应快的同学已经发现了,我们传给 Vue 的是它的选项对象,选项对象又不一定是作为 Vue 实例属性来使用的。事实上,如果想要访问实例的原始 `data` 对象,应该使用实例的 `$data` 属性。但是,我们 `data` 选项返回的**数据对象**却成了 Vue 实例的属性。所谓数据对象,就是一个纯粹的 **key/value** 对,和我们的 python 字典一样。Vue 官方也建议,数据对象最好只有纯粹的键值对,因为数据对象的原型链将会不会起作用。 382 | 383 | 为什么 `data` 返回的数据对象会成为 Vue 实例的属性呢?因为 Vue 在生成组件实例时,会把 `data` 返回的数据对象递归的设置为组件实例的 `getter/setter` 。 或许这里的 `getter/setter` 不怎么好理解,它的原理其实和 python 的描述符类似。`getter` 如同 `__get__` 方法,拦截了读取数据的操作,`setter` 如同 `__set__` ,拦截了赋值操作。当有任何的数据变动时,Vue 实例的 `setter` 在完成赋值操作之后,还会告诉另外一个负责绘制 UI 的方法“该修改 UI 上的数据啦”,这样就实现了动态的“响应式”操作,当然,真正的响应式原理还需要考虑到更多的东西。 384 | 385 | 既然我们知道了数据对象会被设置为实例的属性,所以我们完全可以在编写组件时直接使用 `this` 来访问组件属性。 386 | 387 | 回到 `{{ }}` ,当 Vue 遇到 `{{ }}` 时,它会在组件的数据对象中寻找对应的属性值。所以,如果想要在 `{{ }}` 中使用某个变量,需要**先**在数据对象中定义它,不然就会出错。 388 | 389 | #####computed 390 | `computed` 是选项对象的属性之一,我们可以使用这个选项对数据做一些复杂的处理。删除 `grammer.html` 中我们编写组件的 `script` 标签代码。重新编写如下: 391 | 392 | `grammer.html` 393 | ```html 394 | 410 | ``` 411 | 保存并在浏览器中打开,你会看到浏览器输出了 `Hello, Ucag. I am Vue Nice to meet you Ucag` 。`computed` 中的数据成功的被 `{{ }}` 语法获取。 412 | 413 | `computed` 选项中的键被成为“计算属性”,Vue 会把这些属性绑定到组件中,也就是说,我们同样也可以用 `this` 来访问他们。 那它和 `data` 选项的差别在哪里呢? 414 | 415 | 1. `computed` 中的计算结果会被缓存。通过例子,我们可以看出,`computed` 的每个属性值需要是一个函数。这个函数的返回值会被缓存。这个特性是很有用的,比如我们可以把对 API 的请求写在这里,当在使用组件时,可以放心的调用计算属性,而不用担心多余和不必要的请求。通过 2 的例子更能说明计算属性的缓存特点。 416 | 2. 更新触发机制不同。数据对象,也就是 `data` 的返回的对象被更新之后,UI 会同时更新。但是对于计算属性就不会触发更新,也就是说属性不会重新计算,得到的值还是原来的计算值。 417 | 418 | 要是此时你在浏览器的控制台输入: 419 | ``` 420 | >app.greeting = 'Good morning' 421 | <: 'Good moring' 422 | >app.gretting 423 | <: "I am Vue Nice to meet you Ucag" 424 | ``` 425 | 我们可以看到,`gretting` 的值还是原来缓存的值。那么如何才能触发计算属性的更新呢?只有在计算属性依赖的数据对象的属性改变的时候才会触发更新。 426 | 427 | ``` 428 | >app.name = 'Ace' 429 | <: 'Ace' 430 | >app.greeting 431 | <: "I am Vue Nice to meet you Ace" 432 | ``` 433 | 我们可以看到计算属性已经重新计算了。 434 | 435 | 所以,如何合理使用计算属性呢?当你遇到下列情况的时候应该使用计算属性: 436 | 437 | 1. 数据要经过复杂的处理。我们可以把复杂的数据处理步骤放在这里,在一次处理之后结果就会被缓存。 438 | 2. 不希望主动的响应式变化。我们可以看到,计算属性是“被动响应”的,只有在依赖的数据对象属性改变之后才会重新计算。 439 | 440 | 既然被称为计算属性,`computed` 提供的数据处理功能不局限于简单的调用属性对应的函数。虽然那计算属性的触发是取决于数据对象,但是我们依然可以让计算属性在被直接改变时做一些事情。我们还可以对属性设置 `getter` 和 `setter`。重新编写 `app` 组件如下: 441 | 442 | `grammer.html` 443 | ```javascript 444 | let app = new Vue({ 445 | el:'#app', 446 | template:'

Hello, {{ name }}. {{ greeting }}

', 447 | data: function(){ 448 | return { 449 | name:'Ucag' 450 | } 451 | }, 452 | computed:{ 453 | greeting: { 454 | get: function(){ 455 | return 'I am Vue. ' + 'Nice to meet you ' + this.name 456 | }, 457 | set: function(value){ 458 | console.log('You can not change greeting to ' + value) 459 | } 460 | } 461 | } 462 | }) 463 | ``` 464 | 保存并在浏览器中打开,打开你的浏览器控制台,输入下面的代码: 465 | 466 | ``` 467 | >app.greeting = 'Good morning' 468 | <: You can not change greeting to Good morning 469 | ``` 470 | 471 | `setter` 运行成攻了。 472 | 473 | #####methods 474 | 我们可以把我们需要在组件中使用的函数定义在这里,我们可以在 `{{ }}` 中调用它。修改我们的组件如下: 475 | 476 | ```javascript 477 | let app = new Vue({ 478 | el:'#app', 479 | template:`
480 |

{{ say('hello') }} to {{ name }}

481 |
`, 482 | data: function(){ 483 | return { 484 | name:'Ucag' 485 | } 486 | }, 487 | methods:{ 488 | say: function(value){ 489 | return value.toUpperCase() 490 | } 491 | } 492 | }) 493 | ``` 494 | 保存并在浏览器中打开,你会看到 `{{ }}` 成功调用了 `say` 函数。同样的,`methods` 中的函数会被绑定到组件上,可以使用 `this` 来访问它。 495 | 496 | 我们使用函数返回了一个经过处理之后的数据,并且接收了参数。那它和计算属性有什么不同呢?当`methods` 中的函数被调用时,每一次的结果都是函数**再次**运行之后的,也就是说,它的结果不会被缓存。我们可以把组件相关的 UI 动作定义在这里,比如在点击按钮之后需要执行的动作。 497 | 498 | #####JavaScript表达式 499 | 刚才我们可以看到,`{{ }}` 中除了可以写我们需要访问的属性之外,还可以执行函数。其实,`{{ }}` 还可以写 javascript 表达式。 500 | 像下面这样也是正确的。 501 | ```javascript 502 | {{ number + 1 }} 503 | {{ ok ? 'YES' : 'NO' }} 504 | {{ message.split('').reverse().join('') }} 505 | ``` 506 | ####v-bind 与 v-on 507 | 有的时候,我们希望能够对标签的属性有更多的控制。比如,动态的生成属性,触发我们自定义的事件动作。 508 | 509 | 重新编写我们的组件: 510 | 511 | `grammer.html` 512 | ```javascript 513 | let app = new Vue({ 514 | el:'#app', 515 | template: `
516 | 520 | Go Home 521 |
`, 522 | data:function(){ 523 | return { 524 | UserLink:'#' 525 | } 526 | }, 527 | methods:{ 528 | changeHref: function(event){ 529 | let username = event.target.value 530 | if (username){ 531 | this.UserLink = 'http://www.example.com/' + username 532 | } else { 533 | this.UserLink = '#' 534 | } 535 | } 536 | } 537 | }) 538 | ``` 539 | 保存并在浏览器中打开,尝试输入几个数据,你会看到这样的效果。 540 | ![绑定属性与事件](),我们把在模板中,类似这样以`v-`开头的东西称为“指令”。 541 | 542 | 我们在标签的事件上使用了 `v-on:event="handler"` 语法,来告诉 Vue ,当 `event` 事件发生时,调用 `handler` 函数。 Vue 会默认的往这个函数里传一个 `event` 参数,`event` 代事件对象。我们使用 `v-bind:property="value"` 来告诉 Vue ,`property` 属性的值等于组件的 `value` 属性值。也就是说,如果你使用了 `v-on` 或者 `v-bind` 等,那么等号后面的东西就不再会被解释为字符串,而是一个 js 表达式。 543 | 544 | 他们都有自己的缩写形式,`v-bind:property="value` 可以写为 `:property=value`,`v-on:event="handler"` 可以写为 `@event="handler"`。所以像这样写也是可以的: 545 | 546 | ```javascript 547 | let app = new Vue({ 548 | ... 549 | template: `
550 | 554 | Go Home 555 |
`, 556 | ... 557 | }) 558 | ``` 559 | ####v-if 与 v-for 560 | 我们可以在 Django 的模板中使用 `{% for item in iterable %}` 来迭代对象,使用 `{% if conditino %}` 来做条件判断。同样的,Vue 也提供了这些功能。 561 | 562 | 把我们的组件重新编写如下: 563 | 564 | ```javascript 565 | let app = new Vue({ 566 | el:'#app', 567 | template: `
    568 |
  • 569 |

    Name: {{ person.name }}

    570 |

    Age: {{ person.age }}

    571 |
  • 572 |
`, 573 | data:function(){ 574 | return { 575 | personList:[ 576 | {name:'Ucag',age:'18'}, 577 | {name:'Ace',age:'20'}, 578 | {name:'Lily',age:'22'}] 579 | } 580 | } 581 | }) 582 | ``` 583 | 保存并在浏览器中打开,你会看到浏览器渲染出了我们的列表。 584 | 585 | `v-for` 用于迭代某个标签,指令的基本语法是: 586 | 587 | ``` 588 | 589 | ``` 590 | `alias` 是当前迭代对象的别名。 591 | 592 | 当被迭代对象是 `Array` ,`string` ,`number` 时,可以使用以下两种语法迭代: 593 | ``` 594 | 595 | 596 | ``` 597 | 在第二种迭代方式中,`index` 是其索引,也就是从 0 开始。 598 | 599 | 当迭代对象是 `Object` 时,可以使用以下三种方式迭代: 600 | ``` 601 | 602 | 603 | 604 | ``` 605 | 第一种是直接迭代对象的**属性值**。第二种则包含了**属性值**和**属性**。第三种相对第二种多了一个索引值。 606 | 607 | `v-if` 用于判断某个标签,基本语法是: 608 | ``` 609 | 610 | ``` 611 | 如果条件成立则渲染这个 `tag` ,不成立则不渲染。同样的,它还有自己其它的配套语法。 612 | ``` 613 | // 和 v-else 一起使用 614 |
615 | Now you see me 616 |
617 |
618 | Now you don't 619 |
620 | 621 | //和 v-else-if 一起使用 622 |
623 | A 624 |
625 |
626 | B 627 |
628 |
629 | C 630 |
631 |
632 | Not A/B/C 633 |
634 | ``` 635 | 不过需要注意的是,`v-else` 或者 `v-else-if` 必须紧跟在 `v-if` 后使用,不然这些指令不会被识别。 636 | 637 | 当同时在一个标签中使用 `v-if` 与 `v-for` 时,总是会先执行 `v-for` ,再执行 `v-if`。也就是说,`v-for` 的优先级要高。 638 | 639 | 640 | --- 641 | 本节的 Vue 基础就讲完了。我们只是简单的入门了 Vue ,但是仅仅这些知识就已经够我们编写最基本的页面了。在下一节,我们将会运用前两节学到的知识,重构我们的 APP 。不过由于最近期末考试了,下一次更应该是在一月十号之后了。最后祝大家冬至快乐~ 642 | 643 | 644 | 645 | 646 | 647 | 648 | -------------------------------------------------------------------------------- /Chapter-four/Django RESTful 系列教程(四).md: -------------------------------------------------------------------------------- 1 | 前后端分离的好处就是可以使前端和后端的开发分离开来,如果使用 Django 的模板系统,我们需要在前端和后端的开发中不停的切换,前后端分离可以把前端项目和后端项目分离开来,各自建立项目单独开发。那么问题来了,前端怎么建项目?这就是本章需要解决的问题。 2 | 3 | 对于任何的工具,我的哲学是“工具为人服务,而不是人为工具服务”,希望大家不要为了学习某个工具而学习,任何工具的出现都是为了满足不同的需求。这是在学习前端工具链时需要牢记的一点,不然等学完了,学的东西就全部忘了。前端的世界浩瀚无比,小小的一章并不能很详尽的介绍它们,仅仅是作为一个入门的介绍,但是对于我们来说一定是够用的。 4 | 5 | ##JavaScript 的解释器 —— node 与 “模块化” 6 | 7 | js 和 python 同为脚本语言,他们都有自己的解释器。js 的解释器是 node 。 8 | 9 | 在 node 出现前 js 有没有自己的解释器呢?有的,那就是我们的浏览器中的 js 引擎,但是这个引擎的实现仅仅是针对浏览器环境的,这个引擎限制了 js 的很多功能,比如 js 在浏览器引擎下都不能进行文件的读写,当然这么做是为了用户的安全着想。如果我们想要用 js 实现 python 的许多功能呢?这时就需要 node 了。 10 | 11 | 先去[这里](http://nodejs.cn/download/)下载 node ,像安装 python 一样把 node 安装到你的电脑上,记得把安装路径添加到环境变量中。这些都是和安装 python 是一样的。 12 | 13 | python 运行 `.py` 脚本是 `python .py` 命令,node 也是同理, `node .js` 就可以运行一个 js 脚本了。 14 | 15 | 在上一章,我们在写 `index.js` 时需要考虑代码编写的顺序,这是一件烦人的事情。等到以后代码量大起来,谁知道哪个组件引用了哪个组件,还容易出现 undefined 错误。要是我们能单独把组件都写在一个地方,要用他们的时候再按照需求引入就好了。也就是,我们希望能够进行“模块化”开发。不用去考虑代码顺序,做到代码解耦。 16 | 17 | js 被创建的时候并没有考虑到模块化开发,因为当时的需求还是很简单的,随着需求变多,模块化开发成了必须。我们知道,我们可以在 python 中使用 import 来引入我们需要的包和库。 由于在 es6 之前还没有官方提供这个功能,于是 js 社区就自己实现了这项需求。这就是 `require` 和 `module.exports` 的故事,也就是 CommonJS 规范。 18 | 19 | 在 python 中,我们直接使用 `import` 就可以从一个包中直接导入我们需要的东西。但是 js 有些不同,js 需要被导入的包主动导出内部变量,然后其它的包才能导入他们。 20 | 21 | 在 CommonJS 规范中,每一个模块都默认含有一个全局**变量** `module` ,它有一个 `exports` 属性,我们可以通过这个属性来向外暴露内部的变量。`module.exports` 的默认值为一个空对象。外部可以通过全局的 `require` 函数来导入其它包内的 `module.exports` 变量。 22 | 23 | ```javascript 24 | // A.js 25 | function out(){ 26 | console.log('model A.') 27 | } 28 | 29 | module.exports = out // 导出 out 函数 30 | 31 | // B.js 32 | const out = require('./A') // 从 A.js 引入 out 33 | out() 34 | ``` 35 | 36 | 在终端里输入 `node B.js` ,你就会看到控制台打印出了 `model A.` 。 37 | 38 | 就这么简单。和 Python 的差别就是 js 需要你主动导出变量。这也是 node 引用模块的方法。 39 | 40 | 如果你不想写 `module.exports` ,还有另外一个全局变量 `exports` 供你使用,它是 `module.exports` 的**引用**,由于 `module.exports` 的默认值为一个空对象,所以它的默认值也是一个空对象。如: 41 | 42 | ```javascript 43 | // A.js 44 | exports.a = 'a'; 45 | exports.b = function(){ 46 | console.log('b') 47 | } 48 | 49 | // B.js 50 | const A = require('./A') 51 | console.log(A.a) // 'a' 52 | A.b() // 'b' 53 | ``` 54 | 55 | 有时候我们的模块不止一个文件,而是有很多个文件。我们可以直接使用 `require` 来引入模块路径,`require` 会自动搜寻引入目录下的 `index.js` 文件,它会把这个文件作为整个模块的入口。如: 56 | 57 | ``` 58 | // 模块 ucag 59 | ucag/ 60 | index.js // module.exports = { 61 | name: require('./name').name, 62 | age: require('./age').age, 63 | job: require('./job').job 64 | } 65 | 66 | age.js // exports.age = 18 67 | name.js // exports.name = 'ucag' 68 | job.js // exports.job = 'student' 69 | ``` 70 | 我们在一个文件里引入: 71 | ```javascript 72 | const ucag = require('./ucag') 73 | ucag.name // 'ucag' 74 | ucag.age // 18 75 | ucag.job // 'student' 76 | ``` 77 | 78 | 79 | 在 es6 之后,js 有了自己引用模块的方法,它有了自己的 `import` 和 `export` **关键字**。对外导出用 `export` ,对内引入用 `import`。 80 | 81 | 对于导出,需要遵循以下语法: 82 | ```javascript 83 | export expression 84 | // 如: 85 | export var a = 1, b = 2; // 导出 a 和 b 两个变量 86 | 87 | export {var1, var2, ...} //var1 var2 为导要出的变量 88 | 89 | export { v1 as var1, v2 as var2} // 使用 as 来改变导出变量的名字 90 | 91 | ``` 92 | 不过需要注意的是,当我们只想导出一个变量时,我们不能这么写: 93 | 94 | ```javascript 95 | let a = 1; 96 | export a; // 这是错误的写法 97 | export { a } // 这才是正确的写法 98 | ``` 99 | 100 | 我们可以这样来引入: 101 | 102 | ```javascript 103 | import { var1 }from 'model' // 从 model 导出 var1 变量 104 | import {v1, v2 } from 'model' // 从 model 导出多个变量 105 | import { var1 as v1 }from 'model' // 从 model 导出 var1 变量并命名为 v1 106 | import * as NewVar from 'model' // 从 model 导入全部的变量 107 | ``` 108 | 109 | 在使用 `import` 时,`import` 的变量名要和 `export` 的变量名完全相同,但是有时候我们我们并不知道一个文件导出的变量叫什么名字,只知道我们需要使用这个模块默认导出的东西,于是便出现了 `default` 关键字的使用。我们可以在 `export` 时使用这个关键字来做到“匿名”导出,在 `import` 时,随便取个变量名就可以了。 110 | 111 | ```javascript 112 | export default expression 113 | // 如: 114 | export default class {} // 导出一个类 115 | export default {} //导出一个对象 116 | export default function(){} //导出一个函数 117 | 118 | ``` 119 | 120 | 我们可以这样来引入: 121 | 122 | ```javascript 123 | import NewVar from 'model' // NewVar 是我们为 export default 导出变量取的名字。 124 | ``` 125 | 126 | 注意,默认导出和命名导出各自的导入是有区别的: 127 | ```javascript 128 | // 默认导出 129 | export default { 130 | name:'ucag' 131 | } 132 | // 默认导出对应导入 133 | import AnyVarName from 'model' // 没有花括号 134 | AnyVarName.name // 'ucag' 135 | 136 | //命名导出 137 | export var name='ucag' 138 | //命名导出对应导入 139 | import { name } from 'model' // 有花括号 140 | name // 'ucag' 141 | 142 | //两种导出方式同时使用 143 | export default { 144 | name:'ucag' 145 | } 146 | export var age=18; 147 | 148 | //两种导入 149 | import NameObj from 'model' //导入默认导出 150 | import { age } from 'model' //导入命名导出 151 | 152 | NameObj.name // 'ucag' 153 | age // 18 154 | ``` 155 | 156 | 总结一下: 157 | 158 | 1. 目前我们学了两种模块化的方式。他们是 CommonJS 的模块化方式与 es6 的模块化方式。两种方式不要混用了哦。 159 | 2. CommonJS 规范: 160 | 1. 使用 `module.exports` 或 `exports` 来导出内部变量 161 | 2. 使用 `require` 导入变量。当被导入对象是路径时,`require` 会自动搜寻并引入目录下的 `index.js` 文件,会把这个文件作为整个文件的入口。 162 | 3. es6 规范: 163 | 1. 使用 `import` 与 `export` 来导出内部变量 164 | 2. 当导入命名导出变量时,使用基于 `import { varName } from 'model'` 的语法;当导入匿名或默认导入时,使用 `import varName from 'model'` 语法; 165 | 166 | 悲催的是,node 只支持 CommonJS 方式来进行模块化编写代码。 167 | 168 | ##前端的 pip —— npm 169 | 170 | 刚才我们讲了模块化,现在我们就可以用不同的模块做很多事情了。 我们可以使用 pip 来安装 python 的相关包,在 node 下,我们可以使用 npm 来安装我们需要的库。当然,安装包的工具不止有 npm 一种,还有许多其它的包管理工具供我们使用。现在的 python 已经在安装时默认安装了 pip ,node 在安装时已经默认安装了 npm ,所以我们就用这个现成的工具。 171 | 172 | 前端项目有个特点 —— 版本更替特别快。今天页面是一个样子,明天可能就换成另外的样子了,变化特别频繁,可能今天的依赖库是一个较低的版本,明天它就更新了。所以需要把依赖的库和项目放在一起,而不是全局安装到 node 环境中。每开发一个新项目就需要重新安装一次依赖库。而真正的 node 环境下可能是什么都没有的,就一个 npm 。 173 | 174 | 在一个前端项目中,总是会把依赖库放进一个文件夹里,然后从这个文件夹里导入需要的库和依赖,这个文件夹叫做 `node_modules` 。 175 | 176 | 在 pip 中,我们可以使用 `requirements.txt` 来记录我们的项目依赖。在 npm 下,我们使用 `package.json` 来记录依赖。当我们在 `package.json` 中写好需要的依赖后,在同一路径下运行 `npm install`, npm 会自动搜寻当前目录下的 `package.json` 并且自动安装其中的依赖到 `node_modules` 中,要是当前目录没有 `node_modules` 目录,npm 就会帮我们自己创建一个。当我们想要使用别人的项目时,直接把他们的 `package.json` 拷贝过来,再 `npm install` 就可以完成开发环境的搭建了。这样是不是特别的方便。 177 | 178 | 当你在运行完了 `npm install` 时,如果在以后的开发中想要再安装新的包,直接使用 `npm install ` 安装新的包就行了,npm 会自动帮你把新的包装到当前的 `node_modules` 下。 179 | 180 | 在我们发布一个 python 项目时,我们对于依赖的说明通常是自己写一个 `requirements.txt` ,让用户们自己去装依赖。 npm 为我们提供了更加炫酷的功能。在开发项目时,你直接在含有 `package.json` 的目录下运行 `npm install --save-dev` ,npm 会自动帮你把依赖写到 `package.json` 中。以后你就可以直接发布自己的项目,都不用在 `package.json` 中手写依赖。 181 | 182 | 通过上面的内容我们知道,我们只需要在一个文件夹中创建好 `package.json` ,就可以自动安装我们的包了。 我们还可以使用 npm 自动生成这个文件。在一个空目录下,运行 `npm init` ,npm 会问你一些有的没的问题,你可以随便回答,也可以一路回车什么都不答,目录下就会自动多一个 `package.json` 文件。比如我们在一个叫做 vue-test 的路径下运行这个命令,记得以**管理员**权限运行。 183 | 184 | ``` 185 | λ npm init 186 | This utility will walk you through creating a package.json file. 187 | It only covers the most common items, and tries to guess sensible defaults. 188 | 189 | See `npm help json` for definitive documentation on these fields 190 | and exactly what they do. 191 | 192 | Use `npm install ` afterwards to install a package and 193 | save it as a dependency in the package.json file. 194 | 195 | Press ^C at any time to quit. 196 | package name: (vue-test) 197 | version: (1.0.0) 198 | description: 199 | entry point: (index.js) 200 | test command: 201 | git repository: 202 | keywords: 203 | author: 204 | license: (ISC) 205 | About to write to C:\Users\Administrator\Desktop\vue-test\package.json: 206 | 207 | { 208 | "name": "vue-test", 209 | "version": "1.0.0", 210 | "description": "", 211 | "main": "index.js", 212 | "scripts": { 213 | "test": "echo \"Error: no test specified\" && exit 1" 214 | }, 215 | "author": "", 216 | "license": "ISC" 217 | } 218 | 219 | 220 | Is this ok? (yes) 221 | 222 | ``` 223 | 224 | 如果你不想按回车,在运行 `npm init` 时加一个 `-y` 参数,`npm` 就会默认你使用它生成的答案。也就是运行 `npm init -y` 就行了。 225 | ``` 226 | λ npm init -y 227 | Wrote to C:\Users\Administrator\Desktop\vue-test\package.json: 228 | 229 | { 230 | "name": "vue-test", 231 | "version": "1.0.0", 232 | "description": "", 233 | "main": "index.js", 234 | "scripts": { 235 | "test": "echo \"Error: no test specified\" && exit 1" 236 | }, 237 | "keywords": [], 238 | "author": "", 239 | "license": "ISC" 240 | } 241 | ``` 242 | 243 | 然后在以后的安装中,我们使用 `npm install --save-dev` ,就会自动把依赖库安装到 `node_modules` 中,把相关库依赖的版本信息写入到 `package.json` 中。 244 | 245 | 还是以刚才的 vue-test 为例,在创建完了 `package.json` 后,运行: 246 | 247 | ``` 248 | λ npm install --save-dev jquery 249 | npm notice created a lockfile as package-lock.json. You should commit this file. 250 | npm WARN vue-test@1.0.0 No description 251 | npm WARN vue-test@1.0.0 No repository field. 252 | 253 | + jquery@3.2.1 254 | added 1 package in 5.114s 255 | ``` 256 | 257 | 此时,我们发现又多了一个 `package-lock.json`文件, 先不管它。我们再打开 `package.json` 看看,你会发现它的内容变成了这样: 258 | ``` 259 | λ cat package.json 260 | { 261 | "name": "vue-test", 262 | "version": "1.0.0", 263 | "description": "", 264 | "main": "index.js", 265 | "scripts": { 266 | "test": "echo \"Error: no test specified\" && exit 1" 267 | }, 268 | "keywords": [], 269 | "author": "", 270 | "license": "ISC", 271 | "devDependencies": { 272 | "jquery": "^3.2.1" 273 | } 274 | } 275 | ``` 276 | 277 | 依赖已经自动写入了 `package.json` 中。我们再删除 `node_modules` 文件夹和 `package-lock.json` ,只留下 `package.json` 。再运行 `npm install`。 278 | ``` 279 | λ npm install 280 | npm notice created a lockfile as package-lock.json. You should commit this file. 281 | npm WARN vue-test@1.0.0 No description 282 | npm WARN vue-test@1.0.0 No repository field. 283 | 284 | added 1 package in 5.4s 285 | ``` 286 | 我们发现 npm 已经为我们安装好了依赖。 287 | 288 | 当然,我们有时需要一些各个项目都会用到的工具。还是以 python 为例,我们会使用 `virtualenv` 来创建虚拟环境,在安装它时,我们直接全局安装到了系统中。npm 也可以全局安装我们的包。在任意路径下,使用 `npm install -g ` 就可以全局安装一个包了。我们在以后会用到一个工具叫做 `vue-cli` ,我们可以用它来快速的创建一个 vue 项目。为什么要用它呢,在前端项目中,有一些库是必须要用到的比如我们的 `webpack` ,比如开发 vue 需要用到的 `vue` 包,`vue-router`,`vuex` 等等,它会帮我们把这些自动写入 `package.json` 中,并且会为我们建立起最基本的项目结构。就像是我们使用 `django-admin` 来创建一个 Django 项目一样。这样的工具,在前端被称为**“脚手架”**。 289 | 290 | 任意路径下运行: 291 | ``` 292 | npm install -g vue-cli 293 | ``` 294 | vue 脚手架就被安装到了我们的 node 环境中。我们就可以在命令行中使用 `vue` 命令来创建新的项目了,不需要自己手动创建项目。大家可以试着运行 `vue --help` ,看看你是否安装成功了 `vue-cli`。 295 | ``` 296 | λ vue --help 297 | 298 | Usage: vue [options] 299 | 300 | 301 | Options: 302 | 303 | -V, --version output the version number 304 | -h, --help output usage information 305 | 306 | 307 | Commands: 308 | 309 | init generate a new project from a template 310 | list list available official templates 311 | build prototype a new project 312 | help [cmd] display help for [cmd] 313 | ``` 314 | 315 | npm 除了可以安装包之外,还可以使用 `npm run` 用来管理脚本命令。 316 | 还是以刚才安装 'jquery' 的包为例,打开 `package.json` ,把 `scripts` 字段改成这样: 317 | ```json 318 | "scripts": { 319 | "test": "echo \"Error: no test specified\" && exit 1", 320 | "vh": "vue --help" 321 | } 322 | ``` 323 | 然后在 `package.json` 路径下运行 `npm run vh` ,你就会看到控制台输出了 vue 脚手架的帮助信息。 324 | 325 | 当我们运行 `npm run `时,npm 会搜寻同目录下的 `package.json` 中的 `scripts` 中对应的属性,然后把当前的 `node_modules` 加入环境变量中,执行其中命令,这样就不用我们每次都都手动输入长长的命令了。 326 | 327 | 还是总结一下: 328 | 329 | 1. npm 是 node 中的包管理工具。 330 | 2. `npm install`: 安装 `package.json` 中的依赖到 `node_modules` 中。 331 | 3. `npm install --save-dev`:把包安装到 `node_modules` 中,并把包依赖写入 `package.json` 中。 332 | 4. `npm install -g`:全局安装某个包。 333 | 5. `npm run `: 运行当前目录下 `package.json` 的 `scripts` 中的命令。 334 | 335 | 336 | ##前端工具链 337 | 前端开发会用到许许多多的工具,有的工具是为了更加方便的开发而生,有的工具是为了使代码更好的适应浏览器环境。每个工具的出现都是为了解决特定的问题。 338 | ###解决版本差异 —— babel 339 | 版本差异一直是个很让人头痛的问题。用 python2 写的代码,大概率会在 python3 上运行失败。 js 是运行在浏览器上的,很多的浏览器更新并没有能够很稳定的跟上 js 更新的步伐,有的浏览器只支持到 es5 ,或者只支持部分 es6 特性。为了能够向下兼容,大家想了办法 —— 把 es6 的代码转换为 es5 的代码就行了!开发的时候用 es6 ,最后再把代码转换成 es5 代码就行了!于是便出现了 babel 。 340 | 341 | 创建一个叫做 `babel-test` 的文件夹,在此路径下运行: 342 | ``` 343 | npm init -y 344 | npm install --save-dev babel-cli 345 | ``` 346 | 347 | 在使用 babel 前,我们需要通过配置文件告诉它转码规则是什么。babel 默认的配置文件名为 `.babelrc`。 348 | 在 `babel-test` 下创建 `.babelrc`,写入: 349 | ```json 350 | { 351 | "presets": [ 352 | "es2015" 353 | ], 354 | "plugins": [] 355 | } 356 | ``` 357 | 转码规则是以附加规则包的形式出现的。所以在配置好了之后我们还需要安装规则包。 358 | ``` 359 | npm install --save-dev babel-preset-es2015 360 | ``` 361 | babel 是以命令行的形式使用的,最常用的几个命令如下: 362 | ``` 363 | # 转码结果打印到控制台 364 | babel es6.js 365 | 366 | # 转码结果写入一个文件 367 | babel es6.js -o es5.js # 将 es6.js 的转码结果写入 es5.js 中 368 | 369 | # 转码整个目录到指定路径 370 | babel es6js -d es5js # 将 es6js 路径下的 js 文件转码到 es5js 路径下 371 | ``` 372 | 但是由于我们的 babel 是安装在 babel-test 的 `node_modules` 中的,所以需要使用 `npm run` 来方便运行以上命令。 373 | 374 | 编辑 `package.json`: 375 | ```json 376 | "scripts": { 377 | "test": "echo \"Error: no test specified\" && exit 1", 378 | "build": "babel inputs -d outputs" 379 | } 380 | ``` 381 | 382 | 在 babel-test 下创建一个新的目录 inputs ,在其中写入新的文件 a.js: 383 | ```javascript 384 | // es6 语法,模板字符串 385 | let name = 'ucag' 386 | let greeting = `hello my name is ${name}` 387 | ``` 388 | 然后运行: 389 | ``` 390 | npm run build 391 | ``` 392 | 在转换完成之后,我们在 outputs 下找到 a.js,发现代码变成了这样: 393 | ```javascript 394 | 'use strict'; 395 | 396 | var name = 'ucag'; 397 | var greeting = 'hello my name is ' + name; 398 | ``` 399 | es6 代码已经被转换为了 es5 代码。 400 | 401 | ###整合资源 —— webpack 402 | 在一个前端项目中,会有许许多多的文件。更重要的是,最后我们需要通过浏览器来运行他们。我们用 es6 写的代码,需要转换一次之后才能上线使用。如果我们用的是 TypeScript 写的 js ,那我们还需要先把 TypeScript 转换为原生 js ,再用 babel 转换为 es5 代码。如果我们使用的是模块化开发,但是浏览器又不支持,我们还需要把模块化的代码整合为浏览器可执行的代码。总之,为了方便开发与方便在浏览器上运行,我们需要用到许许多多的工具。 403 | 404 | webpack 最重要的功能就是可以把相互依赖的模块打包。我们在模块化开发之后,可能会产生许多的 js 文件,要是一个个手写把他们引入到 html 中是一件很麻烦的事情,所以我们此时就需要 webpack 来帮我们把分离的模块组织到一起,这样就会方便很多了 405 | 创建一个新的路径 webpack-test ,在此路径下运行: 406 | ``` 407 | npm init -y 408 | npm install --save-dev webpack 409 | ``` 410 | 411 | 使用前先配置,在配置之前我们需要知道一些最基本的概念。 412 | 413 | 1. 入口 entry:不管一个项目有多复杂,它总是有一个入口的。这个入口就被称为 entry 。这就像是我们的模块有个 `index.js` 一样。 414 | 2. 出口 output:webpack 根据入口文件将被依赖的文件按照一定的规则打包在一起,最终需要一个输出打包文件的地方,这就是 output 。 415 | 416 | 这就是最基本的概念了,我们会在以后的教程中学习到更多有关 webpack 配置的知识,不过由于我们目前的需求还很简单,还用不到其它的一些功能,就算是现在讲了也难以体会其中的作用,所以我们目前不着急,慢慢来。 417 | 418 | webpack 有多种加载配置的方法,一种是写一个独立的配置文件,一种是在命令行内编写配置,还有许多其它更灵活编写配置的方法,我们以后再说。当我们在 webpack-test 下不带任何参数运行 `webpack` 命令时,`webpack` 会自动去寻找名为 `webpack.config.js` 的文件,这就是它默认的配置文件名了。 419 | 420 | 在 webpack-test 下创建一个新的文件 `webpack.config.js`: 421 | ```javascript 422 | module.exports = { 423 | entry:'./main.js', // 入口文件为当前路径下的 main.js 为文件 424 | output:{ 425 | path:__dirname, // __dirname 是 node 中的全局变量,代表当前路径。 426 | filename:'index.js' // 打包之后的文件名 427 | } 428 | } 429 | ``` 430 | 431 | 432 | 编辑 `package.json` : 433 | ```json 434 | "scripts"{ 435 | 'pkg':'webpack' // 编辑快捷命令 436 | } 437 | ``` 438 | 439 | 以第三章的 index.js 为例,当时我们把所有的代码都写到了一个文件中,现在我们可以把他们分开写了,最后再打包起来。 440 | 441 | 创建几个新文件分别为 `components.js` `api.js` `store.js` `main.js` `vue.js` `jquery.js` 442 | 443 | `vue.js`: vue 源文件 444 | `jquery`: jquery 源文件 445 | 446 | `api.js`: 447 | ```javascript 448 | let api = { 449 | v1: { 450 | run: function () { 451 | return '/api/v1/run/' 452 | }, 453 | code: { 454 | list: function () { 455 | return '/api/v1/code/' 456 | }, 457 | create: function (run = false) { 458 | let base = '/api/v1/code/'; 459 | return run ? base + '?run' : base 460 | }, 461 | detail: function (id, run = false) { 462 | let base = `/api/v1/code/${id}/`; 463 | return run ? base + '?run' : base 464 | }, 465 | remove: function (id) { 466 | return api.v1.code.detail(id, false) 467 | }, 468 | update: function (id, run = false) { 469 | return api.v1.code.detail(id, run) 470 | } 471 | } 472 | } 473 | } 474 | 475 | module.exports = api // 导出 API 476 | ``` 477 | 478 | `store.js` 479 | ```javascript 480 | const $ = require('./jquery') // 引入 jquery 481 | 482 | let store = { 483 | state: { 484 | list: [], 485 | code: '', 486 | name: '', 487 | id: '', 488 | output: '' 489 | }, 490 | actions: { 491 | run: function (code) { //运行代码 492 | $.post({ 493 | url: api.v1.run(), 494 | data: {code: code}, 495 | dataType: 'json', 496 | success: function (data) { 497 | store.state.output = data.output 498 | } 499 | }) 500 | }, 501 | runDetail: function (id) { //运行特定的代码 502 | $.getJSON({ 503 | url: api.v1.run() + `?id=${id}`, 504 | success: function (data) { 505 | store.state.output = data.output 506 | } 507 | }) 508 | }, 509 | freshList: function () { //获得代码列表 510 | $.getJSON({ 511 | url: api.v1.code.list(), 512 | success: function (data) { 513 | store.state.list = data 514 | } 515 | }) 516 | }, 517 | getDetail: function (id) {//获得特定的代码实例 518 | $.getJSON({ 519 | url: api.v1.code.detail(id), 520 | success: function (data) { 521 | store.state.id = data.id; 522 | store.state.name = data.name; 523 | store.state.code = data.code; 524 | store.state.output = ''; 525 | } 526 | }) 527 | }, 528 | create: function (run = false) { //创建新代码 529 | $.post({ 530 | url: api.v1.code.create(run), 531 | data: { 532 | name: store.state.name, 533 | code: store.state.code 534 | }, 535 | dataType: 'json', 536 | success: function (data) { 537 | if (run) { 538 | store.state.output = data.output 539 | } 540 | store.actions.freshList() 541 | } 542 | }) 543 | }, 544 | update: function (id, run = false) { //更新代码 545 | $.ajax({ 546 | url: api.v1.code.update(id, run), 547 | type: 'PUT', 548 | data: { 549 | code: store.state.code, 550 | name: store.state.name 551 | }, 552 | dataType: 'json', 553 | success: function (data) { 554 | if (run) { 555 | store.state.output = data.output 556 | } 557 | store.actions.freshList() 558 | } 559 | }) 560 | }, 561 | remove: function (id) { //删除代码 562 | $.ajax({ 563 | url: api.v1.code.remove(id), 564 | type: 'DELETE', 565 | dataType: 'json', 566 | success: function (data) { 567 | store.actions.freshList() 568 | } 569 | }) 570 | } 571 | } 572 | } 573 | 574 | store.actions.freshList() // Store的初始化工作,先获取代码列表 575 | 576 | module.exports = store // 导出 store 577 | ``` 578 | 579 | `components.js` 580 | ```javascript 581 | const store = require('./store') 582 | let list = { //代码列表组件 583 | template: ` 584 |
585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 599 | 600 | 601 |
文件名选项
{{ item.name }} 595 | 596 | 597 | 598 |
602 | `, 603 | data() { 604 | return { 605 | state: store.state 606 | } 607 | }, 608 | methods: { 609 | getDetail(id) { 610 | store.actions.getDetail(id) 611 | }, 612 | run(id) { 613 | store.actions.runDetail(id) 614 | }, 615 | remove(id) { 616 | store.actions.remove(id) 617 | } 618 | } 619 | } 620 | let options = {//代码选项组件 621 | template: ` 622 |
625 | 626 | 627 | 628 | 629 |
630 | `, 631 | data() { 632 | return { 633 | state: store.state 634 | } 635 | }, 636 | methods: { 637 | run(code) { 638 | store.actions.run(code) 639 | }, 640 | update(id, run = false) { 641 | if (typeof id == 'string') { 642 | store.actions.create(run) 643 | } else { 644 | store.actions.update(id, run) 645 | } 646 | }, 647 | newOptions() { 648 | this.state.name = ''; 649 | this.state.code = ''; 650 | this.state.id = ''; 651 | this.state.output = ''; 652 | } 653 | } 654 | } 655 | let output = { //代码输出组件 656 | template: ` 657 | 659 | `, 660 | data() { 661 | return { 662 | state: store.state 663 | } 664 | }, 665 | updated() { 666 | let ele = $(this.$el); 667 | ele.css({ 668 | 'height': 'auto', 669 | 'overflow-y': 'hidden' 670 | }).height(ele.prop('scrollHeight')) 671 | } 672 | } 673 | let input = { //代码输入组件 674 | template: ` 675 |
676 | 681 | 682 |

如需保存代码,建议输入代码片段名

683 | 688 |
689 | `, 690 | data() { 691 | return { 692 | state: store.state 693 | } 694 | }, 695 | methods: { 696 | flexSize(selector) { 697 | let ele = $(selector); 698 | ele.css({ 699 | 'height': 'auto', 700 | 'overflow-y': 'hidden' 701 | }).height(ele.prop('scrollHeight')) 702 | }, 703 | inputHandler(e) { 704 | this.state.code = e.target.value; 705 | this.flexSize(e.target) 706 | } 707 | } 708 | } 709 | 710 | module.exports = { 711 | list, input, output, options 712 | } // 导出组件 713 | ``` 714 | 715 | `main.js` 716 | ```javascript 717 | const cmp = require('./components') //引入组件 718 | const list = cmp.list 719 | const options = cmp.options 720 | const input = cmp.input 721 | const output = cmp.output 722 | const Vue = require('./vue') 723 | 724 | let app = { //整体页面布局 725 | template: ` 726 |
727 |
728 | 在线 Python 解释器 729 |
730 |
731 |
732 |
733 | 734 |
735 |
736 |
737 |
738 |

请在下方输入代码:

739 | 740 |
741 | 742 |
743 |

输出

744 |
745 | 746 |
747 |
748 |
749 |
750 |
751 | `, 752 | components: { 753 | 'code-input': input, 754 | 'code-list': list, 755 | 'code-options': options, 756 | 'code-output': output 757 | } 758 | } 759 | 760 | let root = new Vue({ //根组件,整个页面入口 761 | el: '#app', 762 | template: '', 763 | components: { 764 | 'app': app 765 | } 766 | }) 767 | ``` 768 | 769 | 在 webpack-test 下运行: 770 | ``` 771 | npm run pkg # 运行 webpack 772 | ``` 773 | 774 | 过一会儿你就会发现多了一个 `index.js` 文件,这就是我们打包的最终结果了。 6 个 js 文件被打包成了一个文件,最终打包的 `index.js` 的功能和那 6 个 js 文件的功能都是一样的。并且浏览器可以正常执行这些代码, webpack 已经为我们整合好代码,浏览器中不会出现模块化支持的问题。我们只需要在 775 | 776 | 要是你把 `index.js` 放到我们的 `index.html` 里,先不引入 `bootstrap` ,你会发现页面还是可以正常使用的。功能和前面完全相同!如果我们不使用 webpack ,那么我们需要在 html 页面按照顺序挨着写 `` 。 777 | 778 | --- 779 | 本章前端工具链的部分就简单的介绍到这里。我们并没有打包 bootstrap.js , 780 | 那 bootstrap 该怎么办呢?如果我们只是简单的把 bootstrap.js 和我们打包到一起你会发现还是会报错。这是 webpack 的问题吗?这是我们之后要解决的问题。保持你的好奇心。 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | -------------------------------------------------------------------------------- /Chapter-three/Django REST 系列教程(三)(上).md: -------------------------------------------------------------------------------- 1 | #Django REST 系列教程(三)(上) 2 | #你好 Django REST Framework 3 | 在第二章,我们学习了 REST 开发的基本知识,并且在没有借助任何框架的情况下 4 | 完成了我们的 RESTful APP 的开发,虽然我们已经考虑到了许多的情况,但是我们的 APP 5 | 依然有许多的漏洞。在本章,我们将会进入 **Vue** 和 **Django REST framework** 6 | 的学习。本章将会分为三个部分,分别是: 7 | 8 | - **你好 Django REST Framework** 9 | - **你好 Vue** 10 | - **重构 APP** 11 | 12 | 这就是我们的三个部分了。第一个部分学习 DRF ,第二个部分学习 Vue ,最后一个部分为实战部分。在上部分,我们会学习以下知识点: 13 | 14 | 1. 了解 DRF 的基本使用。 15 | 2. 了解并能灵活使用序列化器。 16 | 17 | 这个部分的知识点看起来很少,其实等大家真正进入他们的学习中时,会发现其中的知识点也不少。当然,这是一个教程,不是 DRF 官方文档复读机,所以一旦在看教程的过程中有什么不懂的地方,去查询 [DRF 文档](http://www.django-rest-framework.org/)是个好习惯。同时,本章也会涉及 python 的编程知识,由此可见,对于 web 后端的开发来说,语言的基础是多么重要。同样的,如果遇到在自己查资料之后还不懂的地方,评论留言或者提 [ISSUE](https://github.com/Ucag/django-rest)。 18 | 19 | ##准备工作 20 | 首先,我们需要安装 DRF ,在终端中运行: 21 | ``` 22 | pip install djangorestframework 23 | ``` 24 | 创建一个新的项目: 25 | ``` 26 | python django-admin.py startproject api_learn 27 | ``` 28 | 把路径切换到项目路径内,创建一个新的 APP : 29 | ``` 30 | python manage.py startapp rest_learn 31 | ``` 32 | 编辑你的 `settings.py` 文件,把我们的 APP 和 DRF 添加进去: 33 | ``` 34 | INSTALLED_APPS = [ 35 | ... 36 | 'rest_framework', 37 | 'rest_learn' 38 | ] 39 | ``` 40 | 编辑 `rest_learn` 的 `models.py` : 41 | ```python 42 | from django.db import models 43 | class TestModel(models.Model): 44 | name = models.CharField(max_length=20) 45 | code = models.TextField() 46 | created_time = models.DateTimeField(auto_now_add=True) 47 | changed_time = models.DateTimeField(auto_now=True) 48 | 49 | def __str__(self): 50 | return self.name 51 | ``` 52 | 在 `DateTimeField` 是时间与日期字段,其中的参数意思是: 53 | 54 | - `auto_now`: 在实例每次被保存的时候就更新一次值,在这里,我们把它作为修改值。所以 `changed_time` 字段的值会随着实例的每次修改和保存而发生变化,这样就可以记录实例的修改时间。 55 | - `auto_now_add`: 在实例被创建的时候就会自动赋值。`created_time` 的值就会在实例被创建时自动赋值,这样我们就可以记录实例是在什么时候被创建的。 56 | 57 | 将我们的模型添加进管理站点。 58 | 编辑 `rest_learn` 下的 `admin.py`: 59 | ```python 60 | from django.contrib import admin 61 | from .models import TestModel 62 | 63 | @admin.register(TestModel) 64 | class TestModelAdmin(admin.ModelAdmin): 65 | pass 66 | ``` 67 | `admin.register` 是一个将 `ModelAdmin` 快速注册模型到管理站点的装饰器,其中的参数 68 | 是需要被注册的模型。 69 | 70 | 编辑 `api_learn` 下的 `urls.py`: 71 | 72 | ```python 73 | from django.conf.urls import url, include 74 | from django.contrib import admin 75 | import rest_framework 76 | 77 | urlpatterns = [ 78 | url(r'^admin/', admin.site.urls), 79 | url(r'^drf-auth/',include('rest_framework.urls')) 80 | ] 81 | ``` 82 | 第二个 `url` 配置用于提供登录的功能。 83 | 84 | 在项目路径下运行: 85 | ``` 86 | python manage.py makemigrations 87 | python migrate 88 | ``` 89 | 生成我们需要的数据库文件。 90 | 91 | 创建管理员。在终端中运行: 92 | ``` 93 | (root) λ python manage.py createsuperuser 94 | Username (leave blank to use 'administrator'): admin 95 | Email address: 96 | Password: 97 | Password (again): 98 | Superuser created successfully. 99 | ``` 100 | 101 | 在项目路径下创建两个空文件,分别是 `data.py`,`rest_test.py`。 102 | 103 | `data.py` 主要用来为我们提供一些虚拟数据。 104 | 105 | 编辑 `data.py`。 106 | ```python 107 | from django import setup 108 | import os 109 | os.environ.setdefault('DJAGNO_SETTINGS_MODULE','api_learn.settings') # 在环境变量中设置配置文件 110 | setup() # 加载配置文件 111 | 112 | from rest_learn.models import TestModel 113 | 114 | data_set = { 115 | 'ls':"""import os\r\nprint(os.listdir())""", 116 | 'pwd':"""import os\r\nprint(os.getcwd())""", 117 | 'hello world':"""print('Hello world')""" 118 | } 119 | for name, code in data_set.items(): 120 | TestModel.objects.create(name=name,code=code) 121 | 122 | print('Done') 123 | ``` 124 | 125 | 直接运行 `data.py` ,当看到 Done 的时候就说明数据写入已经完成了。细心的同学已经发现了,这和我们第一章的 `单文件 Django` 很相似。记得我们在第一章说过的吗: 126 | 127 | >使用前先配置 128 | 129 | 我们需要使用 Django 的模型,所以需要先配置它,在这里我们选择了使用第一章中的第二种方法来配置。如果你忘了第一章讲了些什么,快回去看看吧。这也是我们先讲 `单文件 Django` 原因所在,知道了配置方法之后,我们就不需要再打开 shell 十分不方便的编写代码了。 130 | 131 | 为了确保万无一失,大家可以选择登录进后台看看我们的数据是否写入了数据库。 132 | 133 | 本节剩下所有的代码都会在 `rest_test.py` 中进行,所以先编写好配置。 134 | 135 | 编辑 `rest_test.py`: 136 | ```python 137 | from django import setup 138 | import os 139 | 140 | # 加载配置 141 | os.environ.setdefault('DJAGNO_SETTINGS_MODULE','api_learn.settings') 142 | setup() 143 | 144 | 145 | ``` 146 | 准备工作已经完成了,让我们正式的开始学习。 147 | 148 | ##你好 Django REST Framework 149 | 在上一章的结尾我们知道我们的 APP 其实是不安全的,因为我们并没有对上传的数据进行任何的检查,这使得我们的应用随时暴露在被攻击的安全隐患之下。同时,由于我们的应用十分的小,我们并没有考虑到其它的“内容协商”,如果在今后的应用中我们需要用到 `xml` 格式的数据,那么我们又需要重新编写一次我们的代码。我们的应用代码不仅不安全,同时也不灵活。这一次,我们需要解决这个问题。 150 | 151 | 152 | ###序列化器 153 | 刚才我们说道,我们需要对上传的数据进行检查,按照一般的思路,我们一般会编写一大堆的 `if` 语句来判断上传的数据是否符合要求。用脚指头都可以知道这么做是最最笨的方法。再好一点的办法是编写一些工具函数来检查数据是否符合要求,比如我们的 `name` 字段的长度是小于 20 个字符的,数据类型是字符串。那我可以单独编写一个这样的函数: 154 | ```python 155 | def validate_name(post_data): 156 | name = post_data.get('name') 157 | if isinstance(name, str): 158 | if len(name) <= 20: 159 | return name 160 | raise Exception('Invalid name data.') 161 | ``` 162 | 163 | 这样我们直接在视图逻辑中直接调用这个函数就可以了。这个比单独写 `if` 语句要好一些了。但是依然会有不少问题,如果中途我们的 `name` 字段的长度被修改为 30 个字符了,那我们是不是要再改一次我们的 `validate_name` 函数呢?要是我们的 `name` 字段被修改成了 `code_name` ,那我们是不是还要再改一次呢?每一次的改动都会牵扯到 `validate_name` 的改动。 更要命的是,如果我们的数据类型发生了变化,由于前端传过来的数据都是字符串,我们需要对数据进行转换才可以保存到数据库中,这样就加大了我们的工作量。那有没有更好的办法呢?有,那就是我们的 `Serializer` ,也就是序列化器。 164 | 165 | 序列化器是什么?看它的名字我们就知道了,它是用来序列化数据的,我们在第二章知道了什么是数据的“序列化”,同时它也提供了对数据的验证功能,更棒的是,这个数据验证是双向的。也就是说,它既可以验证前端上传到后端的数据,它也可以验证后端传到前端的数据。前者比较好理解,那后者怎么理解呢?比如,我们前端需要的 `created_time` 字段的日期的格式为 '月-日-年' ,那么我们就可以在序列化器中写好,提前做好格式转换的工作,把验证通过数据传给前端。所以,我们序列化器的处理流程大概是这样的: 166 | ``` 167 | client ----> views <-----> serializer <-----> model 168 | ``` 169 | 以及,序列化器还可以规定前端可以修改哪些字段,前端可以知道哪些字段。我们只希望客户端修改 `name` 和 `code` 两个字段,有的人可能会偷偷上传 `created_time` 字段,要是我们没有对它做拦截,我们的字段就会被随意修改啦,这样可不太好。 170 | 171 | 说的很抽象,我们来实际练习一下。接下来的所有代码都是在 `rest_test.py` 中进行的,大家接着刚才的代码写就行了。如果你对这些代码有任何的别扭的感觉,或者是“心头堵的慌”的感觉,或者是产生了任何“这样做好麻烦啊”之类的想法,请忍住,到后面你就明白了。 172 | 173 | ```python 174 | from rest_framework import serializers 175 | 176 | class TestSeriOne(serializers.Serializer): 177 | name = serializers.CharField(max_length=20) 178 | ``` 179 | 这样我们就创建好了一个序列化器。对 Django Form 很熟悉的同学或许已经发现了,这不就很像是 Django 表单的写法吗?是的,事实上,序列化器用的就是表单的逻辑,所以如果你熟悉 Django Form 的 API ,那你上手序列化器也会很快。同时,序列化器和表单一样,拥有很多的字段,在之后的章节中我们会慢慢学习到它们,现在我们对字段的了解就先知道一点是一点。我们来使用一下我们的序列化器。 180 | 接着在下面写: 181 | ```python 182 | frontend_data = { 183 | 'name':'ucag', 184 | 'age':18 185 | } 186 | 187 | test = TestSerilOne(data=frontend_data) 188 | if test.is_valid(): 189 | print(test.validated_data) 190 | ``` 191 | 我们假设有一个前端上传的数据为 `frontend_data` ,然后我们使用序列化器来验证上传的数据。它的使用方法和表单一样,想要获得合法的数据需要先运行 `.is_valid()` 方法,在运行这个方法后,如果验证通过,合法的数据就会被保存在 `.validated_data` 属性中。现在直接运行我们的 `rest_test.py` 脚本试试。不出意外的话,你会看到这个结果: 192 | ``` 193 | OrderedDict([('name', 'ucag')]) 194 | ``` 195 | 我们可以看到,`age` 字段被序列化器给过滤掉了,这样就可以防止前端上传一些奇奇怪怪的字段了。我们把刚才的序列化器修改一下,改成这个样子: 196 | ```python 197 | class TestSerilOne(serializers.Serializer): 198 | name = serializers.CharField(max_length=20) 199 | age = serializers.IntegerField() 200 | ``` 201 | 我们新增加了一个字段。把我们的 `frontend_data` 改成这个样子: 202 | ```python 203 | frontend_data = { 204 | 'name':'ucag', 205 | 'age':'18' 206 | } 207 | ``` 208 | 其它什么都不变,运行 `rest_test.py` ,输出变成了这样: 209 | ``` 210 | OrderedDict([('name', 'ucag'), ('age', 18)]) 211 | ``` 212 | 好像没什么不一样。。。再仔细看看?看看 `age` 变成了什么?我们传进去的数据是个字符串,但是在经过验证之后,它的类型就变成了整数型!让我们来看看,故意给它传错误的数据会发生什么。 213 | 把 `frontend_data` 改成这样: 214 | ```python 215 | frontend_data = { 216 | 'name':'ucag', 217 | 'age':'ucag' 218 | } 219 | ``` 220 | 把之前的测试改成这样: 221 | ```python 222 | test = TestSerilOne(data=frontend_data) 223 | if not test.is_valid(): 224 | print(test.errors) 225 | ``` 226 | 输出应该是这样的: 227 | ``` 228 | {'age': ['A valid integer is required.']} 229 | ``` 230 | 序列化器把不符合要求的字段的错误信息给放在了 `.errors` 属性里。我们可以通过这个属性来查看相应的错误信息,在前端上传的数据出错的时候我们就可以直接把这个错误直接发送给前端,而不用自己手写错误信息了。 231 | 232 | 刚刚体验的是验证前端的数据,现在我们来看看序列化器是怎么验证后端数据的。假设前端现在只想知道 `name` 字段的信息,比如我们之前 APP 项目的代码列表,我们需要显示的仅仅就是代码片段的名字。现在就需要对后端数据做验证了。 233 | 234 | 注释掉刚才做实验的代码,接着在下面再创建一个序列化器: 235 | ```python 236 | # test = TestSerilOne(data=frontend_data) 237 | # if not test.is_valid(): 238 | # print(test.errors) 239 | 240 | class TestSerilTwo(serializers.Serializer): 241 | name = serializers.CharField(max_length=20) 242 | 243 | ``` 244 | 现在我们来使用它来验证后端的数据,在下面接着写: 245 | ```python 246 | from rest_learn.models import TestModel 247 | code = TestModel.objects.get(name='ls') 248 | test = TestSerilTwo(instance=code) 249 | print(test.data) 250 | ``` 251 | 运行 `rest_test.py` ,你的输出会是这样的: 252 | ``` 253 | {'name': 'ls'} 254 | ``` 255 | 我们从模型中获取了一个模型实例,然后通过 `instance` 参数把它放进了序列化器里,然后,我们通过 `.data` 属性来访问验证之后的数据。可以看到,只有 `name` 字段被提取了出来,`code`、`created_time`、`changed_time` 字段都没有出现在提取之后的数据里。真的是很方便呀,那我想提取所有的模型实例该怎么办呢?因为前端的代码列表需要的是所有实例的名字信息啊。把我们之前做验证的代码改成这样: 256 | ```python 257 | from rest_learn.models import TestModel 258 | codes = TestModel.objects.all() 259 | test = TestSerilTwo(instance=codes,many=True) 260 | print(test.data) 261 | ``` 262 | 你会看到输出是这个样子的: 263 | ``` 264 | [OrderedDict([('name', 'hello world')]), OrderedDict([('name', 'pwd')]), OrderedDict([('name', 'ls')])] 265 | ``` 266 | 此时的 `.data` 属性变成了一个列表。我们提取了所有的模型实例,通过 `instance` 参数传递进了序列化器,通过 `many=True` 参数设置告诉序列化器我们传进去的是一个查询集实例,这样序列化器就会自己做相应的处理了。是不是特别方便? 267 | 268 | 到目前为止,我们的序列化器都是一个个字段手写出来的,通常,我们序列化的字段和模型的字段是统一的,那能不能通过模型来生成我们的序列化器呢,就像模型表单那样?当然是可以的。 269 | 注释掉之前的验证代码,接着在后面写: 270 | ```python 271 | # from rest_learn.models import TestModel 272 | # codes = TestModel.objects.all() 273 | # test = TestSerilTwo(instance=codes,many=True) 274 | # print(test.data) 275 | 276 | from rest_learn.models import TestModel 277 | class TestSerilThree(serializers.ModelSerializer): 278 | class Meta: 279 | model = TestModel 280 | fields = ['name','code','created_time','changed_time','id'] 281 | read_only_fields = ['created_time','changed_time'] 282 | ``` 283 | 这一次,我们继承的是 DRF 的模型序列化器,通过 `Meta` 给模型序列化器传模型,通过 `fields` 来告诉序列化器我们需要序列化哪些字段。那 `read_only_fields` 又是用来干什么的呢? 284 | 285 | 刚才我们说过,序列化器是双向验证的,对前端和后端都有验证。有时后端不希望某些字段被前端修改该,这就导致了我们对前端和后端的序列化字段会有所不同。一旦字段发生了变化,也就意味着序列化器也会发生变化,那该怎么办呢?那就是把我们不希望前端修改的字段放在 `read_only_fields` 选项里,这样,当序列化器在序列化前端的字段时,即便是前端有这些字段,序列化器也会忽略这些字段,这样就可以防止别有用心的人暴力修改我们的字段。 286 | 287 | 好像还不是很懂?别着急,我们先用它试试看,接着在下面写: 288 | ```python 289 | code = TestModel.objects.get(name='ls') 290 | codes = TestModel.objects.all() 291 | 292 | # 前端写入测试 293 | frontend_data = { 294 | 'name':'ModelSeril', 295 | 'code':"""print('frontend test')""", 296 | 'created_time':'2107-12-16' 297 | } 298 | test1 = TestSerilThree(data=frontend_data) 299 | if test1.is_valid(): 300 | print('Frontend test:',test1.validated_data) 301 | # 后端传出测试: 302 | test2 = TestSerilThree(instance=code) 303 | print('Backend single instance test:',test2.data) 304 | test3 = TestSerilThree(instance=codes,many=True) 305 | print('Backend multiple instances test',test3.data) 306 | ``` 307 | 输出应该是这样的: 308 | ``` 309 | Frontend test: OrderedDict([('name', 'ModelSeril'), ('code', "print('frontend test')")]) 310 | 311 | Backend single instance test: {'created_time': '2017-12-16T05:16:12.846759Z', 'name': 'ls', 'code': 'import os\r\nprint(os.listdir())', 'id': 3, 'changed_time': '2017-12-16T05:16:12.846759Z'} 312 | 313 | Backend multiple instances test [OrderedDict([('name', 'hello world'), ('code', "print('Hello world')"), ('created_time', '2017-12-16T05:16:12.815559Z'), ('changed_time', '2017-12-16T05:16:12.815559Z'), ('id', 1)]), OrderedDict([('name', 'pwd'), ('code', 'import os\r\nprint(os.getcwd())'), ('created_time', '2017-12-16T05:16:12.831159Z'), ('changed_time', '2017-12-16T05:16:12.831159Z'), ('id', 2)]), OrderedDict([('name', 'ls'), ('code', 'import os\r\nprint(os.listdir())'), ('created_time', '2017-12-16T05:16:12.846759Z'), ('changed_time', '2017-12-16T05:16:12.846759Z'), ('id', 3)])] 314 | ``` 315 | 我们可以看到,模型序列化器正确的序列化了我们的模型实例,包括其中的 `DateTimeField` 字段,如果是我们手写来处理,不知道会有多麻烦。 316 | 317 | 我们先看前端写入的测试的输出,虽然我们的 `frontend_data` 有一个 `created_time` 字段,但是在最后的 `.validated_data` 中根本就没有它的身影,我们的序列化器成功的过滤掉了这个非法字段。 318 | 319 | 再看后端传出测试输出,模型实例和查询集实例的输出结果都很正常。最重要的是,`created_time` 和 `changed_time` 两个字段是被正常序列化了的,这两个字段并没有受到 `read_only_fields` 的影响,所以前端只能看到这个字段,不能修改这个字段。 320 | 321 | 这样就方便许多了!接下来我们进入序列化器的进阶学习。 322 | 323 | 刚刚的序列化器结构都很简单,使用起来也很简单,要是有关系字段该怎么处理呢?我并不打算直接用模型序列化器来讲解,因为模型序列化器都帮我们把工作都完成了,我们最后什么都看不到。所以然我们来手写一个能处理关系字段的序列化器。在开始之前,注释掉之前的实验代码: 324 | ```pyton 325 | # code = TestModel.objects.get(name='ls') 326 | # codes = TestModel.objects.all() 327 | 328 | # 前端写入测试 329 | # frontend_data = { 330 | # 'name':'ModelSeril', 331 | # 'code':"""print('frontend test')""", 332 | # 'created_time':'2107-12-16' 333 | # } 334 | # test1 = TestSerilThree(data=frontend_data) 335 | # if test1.is_valid(): 336 | # print('Frontend test:',test1.validated_data) 337 | # 后端传出测试: 338 | # test2 = TestSerilThree(instance=code) 339 | # print('Backend single instance test:',test2.data) 340 | # test3 = TestSerilThree(instance=codes,many=True) 341 | # print('Backend multiple instances test',test3.data) 342 | ``` 343 | 在开始编写之前,我们需要搞懂一个问题,序列化器到底是什么?它用起来的确很方便,但是当我们遇到问题时却不知道从何下手,就像刚才的问题,如何利用序列化器处理关系字段?如果你去查看官方文档,官方文档会告诉你,使用 `PrimaryKeyRelatedField` ,我相信第一次看到这个答案的你一定是一脸懵逼,为什么???为什么我的关系模型就成了一个字段了????我明明想要的是关系模型相关联的实例对象啊。。。你知道 `PrimaryKeyRelatedField` 是关系模型的主键。比如我们的 `TestModel` 和 `User` 表是关联的,如果我使用的是 `PrimaryKeyRelatedField` 字段,那序列化的结果出来就会是类似这样的: 344 | ```python 345 | { 346 | user:1, 347 | code:'some code', 348 | name:'script name' 349 | } 350 | ``` 351 | 和 `TestModel` 相关联的 `User` 实例就变成了一个主键,我们可以通过访问这个主键来访问 `User` 与 `TestModel` 相关联的实例。但是一般,我们想要的效果是这样的: 352 | ```python 353 | { 354 | user:{ 355 | 'id':1, 356 | 'email':'email@example.com', 357 | 'name':'username' 358 | }, 359 | code:'some code', 360 | name:'script name' 361 | } 362 | ``` 363 | 我们想要的是 `User` 实例的详细信息,而不是再麻烦一次,用 `PrimaryKeyRelatedField` 的值再去查询一次。而且更头痛的是,如果使用 `PrimaryKeyRelatedField`, 在创建实例的时候,你必须要先有一个相关联的 `User` ,在创建 `TestModel` 时候再把这个 `User` 的主键给传进去。也就是说,你不能一次性就创建好 `TestModel` 和 `User` ,要先创建 `User` 再创建 `TestModel`,这个流程简直是让人头皮发麻。如果我们想一次性创建好他们该怎么办呢?如果有心的同学去看看 DRF 的 release note ,就会知道,把 `User` 模型的序列化器当作一个字段就行了。什么???序列化器当成一个字段???这种操作也可以??从来没见过这种操作啊。。在 Django 表单中也没有见过这种操作啊。。怎么回事啊?? 364 | 365 | 淡定,同样的,我们先来做个实验,先体验下“序列化器当作字段”是怎么回事。假设我们希望能在创建 `User` 的同时也能够同时创建`Profile`。 在 `rest_test.py` 下面接着写: 366 | ```python 367 | class ProfileSerializer(serializers.Serializer): 368 | tel = serializers.CharField(max_length=15) 369 | height = serializers.IntegerField() 370 | 371 | class UserSerializer(serializers.Serializer): 372 | name = serializers.CharField(max_length=20) 373 | qq = serializers.CharField(max_length=15) 374 | profile = ProfileSerializer() 375 | ``` 376 | 我们可以看到,`UserSerializer` 的 `profile` 字段是 `ProfileSerializer` 。现在我们使用下这个序列化器。接着在下面写: 377 | ```python 378 | frontend_data = { 379 | 'name':'ucag', 380 | 'qq':'88888888', 381 | 'profile':{ 382 | 'tel':'66666666666', 383 | 'height':'185' 384 | } 385 | } 386 | 387 | test = UserSerializer(data=frontend_data) 388 | if test.is_valid(): 389 | print(test.validated_data) 390 | ``` 391 | 我们可以看到输出是这样的: 392 | ``` 393 | OrderedDict([('name', 'ucag'), ('qq', '88888888'), ('profile', OrderedDict([('tel', '66666666666'), ('height', 185)]))]) 394 | ``` 395 | 可以看到,我们的字段都被正确的序列化了。我们同时创建了 `User` 和 `Profile` 。并且他们也是正确的关联在了一起。 396 | 397 | 现在可以问,这是怎么回事呢?这是因为序列化器其实就是一个特殊的“序列化器字段”。怎么理解呢?再说的容易懂一点,因为序列化器和序列化字段都是 python 的同一种数据结构——描述符。那描述符又是什么东西呢?官方文档是这么说的: 398 | 399 | >In general, a descriptor is an object attribute with “binding behavior”, one whose attribute access has been overridden by methods in the descriptor protocol. Those methods are `__get__()`, `__set__()`, and `__delete__()`. If any of those methods are defined for an object, it is said to be a descriptor. 400 | 401 | >一般地,描述符是拥有“绑定行为”的对象属性,当这个属性被访问时,它的默认行为会被描述符内的方法覆盖。这些方法是 `__get__()`, `__set__()`, `__delete__()` 。任何一个定义的有以上方法的对象都可以被称为描述符。 402 | 403 | 说的太绕了,我们来简化一下。 404 | 405 | 1. 描述符是有默认行为的属性。 406 | 2. 描述符是拥有 `__get__()`, `__set__()`, `__delete__()` 三者或者三者之一的对象。 407 | 408 | 所以,描述符是属性,描述符也是对象。 409 | 410 | 我们先来理解第一条。描述符是属性。什么是属性呢?对于 `a.b` 来说,`b` 就是属性。这个属性可以是任何东西,可以是个方法,也可以是个值,也可以是任何其它的数据结构。当我们写 `a.b` 时就是在访问 `b` 这个属性。 411 | 再理解第二条。描述符是对象。对象是什么呢?通常,我们都是使用 `class` 来定义一个对象。根据描述符定义,有 `__get__()`, `__set__()`, `__delete__()` 之一或全部方法的对象都是描述符。 412 | 413 | 满足以上两个条件,就可以说这个对象是描述符。 414 | 415 | 一般地,`__get__()`, `__set__()`, `__delete__()` 应该按照如下方式编写: 416 | ``` 417 | descr.__get__(self, obj, type=None) --> value 418 | 419 | descr.__set__(self, obj, value) --> None 420 | 421 | descr.__delete__(self, obj) --> None 422 | ``` 423 | 424 | 一般地,描述符是作为对象属性来使用的。 425 | 426 | 当描述符是一个对象的属性时,如 `a.b` ,`b` 为一个描述符,则执行`a.b` 相当于执行`b.__get__(a)`。 而 `b.__get__(a)` 的具体实现为 `type(a).__dict__['b'].__get__(a, type(a))` 。以上这个过程没有为什么,因为 python 的实现就是这样的。我们唯一需要理解的就是,为什么会这样实现。首先我们需要读懂这个实现。 427 | 428 | 假设,`a.b` 中,`a` 是 `A` 的子类,`b` 是描述符 `B` 的实例。 429 | 430 | 1. `type(a)` 返回的是 `a` 的类型 `A`。那就变成了 `A.__dict__['b'].__get__(a, type(a))` 。 431 | 432 | 2. `A.__dict__['b']` 返回的是 `A` 的**类属性** `b` 的值。假设 `A.__dict__['b']` 的值为 `Ab`,那么就变成了 `Ab.__get__(a, type(a))`。 433 | 434 | 我们在这里暂停一下。注意 **`A.__dict__['b']` 返回的是 `A` 的类属性**,`b` 的值是一个描述符,也就是说,`Ab` 是个描述符。那么连起来,就变成了: 435 | 436 | - `Ab` ,也就是 `b` ,是一个**类属性**,这个类属性是个描述符。也就是**描述符** `B` 的实例**是** `A` 的**类属性**。 437 | 438 | 3. 最后一步就很简单了,就是调用描述符的 `__get__()` 方法,也就是 `Ab.__get__(a, A)`,也就是 `b.__get__(a, A)` 。到这里,大家可能会问一个问题,`__get__` 的参数也就是 `a` 和 `A` 是谁传进去的呢?,答案说出来很简单,但是很多时候有的同学容易绕进去就出不来了。答案就是: 439 | python 解释器自己传进去的。就像是类方法的 `self` 一样,没谁手动传 `self` 进去,这都是 python 的设计者这样设计的。 440 | 441 | 一句话总结一下。 **当 `b` 为 `A` 类属性且为描述符时,`A` 的实例 `a` 对于 `b` 访问也就是`a.b` 就相当于 `b.__get__(a, A)`。**所以,此时,对于 `b` 属性的访问结果就取决于 `b` 的 `__get__()` 返回的结果了。 442 | 443 | 我们稍微再推理一下,就可以知道,如果一个对象的**属性**是描述符**对象**,而且这个对象本身也是描述符的话,那么,这个对象的各种子类就可以相互作为彼此的属性。说的很复杂,举个简单的例子。 444 | 445 | 我们来简单的运用下刚才学到的知识,在解释器里输入以下代码: 446 | ``` 447 | In [1]: class Person: # 定义 Person 描述符 448 | ...: def __init__(self, name=None): 449 | ...: self.name = name 450 | ...: def __set__(self, obj, value): 451 | ...: if isinstance(value, str): 452 | ...: self.name = value 453 | ...: else: 454 | ...: print('str is required!') 455 | ...: def __get__(self, obj, objtype): 456 | ...: return 'Person(name={})'.format(s 457 | ...: elf.name) 458 | ...: class Dad(Person): 459 | ...: kid = Person('Son') 460 | ...: class Grandpa(Person): 461 | ...: kid = Dad('Dad') 462 | ...: 463 | ...: dad = Dad('Dad') 464 | ...: gp = Grandpa('Granpa') 465 | ...: 466 | ...: print("Dad's kid:",dad.kid) 467 | ...: print("Grandpa's kid:",gp.kid) 468 | ...: 469 | Dad's kid: Person(name=Son) 470 | Grandpa's kid: Person(name=Dad) 471 | 472 | In [2]: dad.kid = 18 473 | str is required! 474 | 475 | In [3]: dad.kid 476 | Out[3]: 'Person(name=Son)' 477 | ``` 478 | 可以看到,我们在定义描述符之后,除了直接实例化使用他们之外,还把他们作为其它描述符的属性。描述符 `Dad` 的属性 `kid` 也是一个描述符。 我们的对 `kid` 的赋值成功被 `__set__` 拦截,并在赋值类型不规范时给出了我们事先写好的警告,并且原来的值也没有被改变。 479 | 480 | 现在我们回到序列化器中来。序列化器和序列化器字段就是像这样的描述符,他们完全是同一种东西。所以他们完全可以作为彼此的类属性来使用。一旦明白了这一点,就可以有各种“骚操作”了。序列化器最基本的字段描述符定义了字段的操作,所以不用我们自己重新去编写 `__get__` `__set__` `__delete__` ,DRF 已经编写好基本的逻辑,我们只需要调用现成的接口就可以实现自定义字段。在简单的继承 `serializers.Field` 后就可以使用这些现成的接口了,这个接口是: 481 | `.to_representation(obj)` 和 `.to_internal_value(data)` 。 482 | 483 | - `.to_representation(obj)`: 它决定在访问这个字段时的返回值应该如何展示,obj 是 `to_internal_value` 返回的对象。 作用如同描述符的`__get__`。应该返回能够被序列化的数据结构。如数字,字符串,布尔值 `date`/`time`/`datetime` 或者 `None` 。 484 | - `.to_internal_value(data)`: 它决定在对这个字段赋值时应该进行的操作,data 是前端传过来的字段值。作用如同描述符的`__set__` 操作,应该返回一个 python 数据结构。在发生错误时,应抛出 `serializers.ValidationError`。 485 | 486 | 我们现在可以自己定义一个字段试试看,注释掉之前的测试,接着 `rest_test.py` 写: 487 | ```python 488 | # frontend_data = { 489 | # 'name':'ucag', 490 | # 'qq':'88888888', 491 | # 'profile':{ 492 | # 'tel':'66666666666', 493 | # 'height':'185' 494 | # } 495 | # } 496 | 497 | # test = UserSerializer(data=frontend_data) 498 | # if test.is_valid(): 499 | # print(test.validated_data) 500 | class TEL(object): 501 | """电话号码对象""" 502 | def __init__(self, num=None): 503 | self.num = num 504 | def text(self, message): 505 | """发短信功能""" 506 | return self._send_message(num, message) 507 | def _send_message(self,message): 508 | """发短信""" 509 | print('Send {} to {}'.format(message[:10], self.num)) 510 | class TELField(serializers.Field): 511 | def to_representation(self, tel_obj): 512 | return tel_obj.num 513 | def to_internal_value(self, data): 514 | data = data.lstrip().rstrip().strip() 515 | if 8 <= len(data) <=11: 516 | return TEL(num=data) 517 | raise serializers.ValidationError('Invalid telephone number.') 518 | ``` 519 | 这样就完成了我们的“骚操作”字段。我们就可以这样使用它,接着在下面写: 520 | ```python 521 | class ContactSerializer(serializers.Serializer): 522 | name = serializers.CharField(max_length=20) 523 | tel = TELField() 524 | 525 | frontend_data = { 526 | 'name':'ucag', 527 | 'tel':'88888888' 528 | } 529 | test = ContactSerializer(data=frontend_data) 530 | if test.is_valid(): 531 | tel = test.validated_data['tel'] 532 | print('TEL',tel.num) 533 | tel.text('这是一个骚字段') 534 | ``` 535 | 直接运行 `rest_test.py`,输出如下: 536 | ``` 537 | TEL 88888888 538 | Send 这是一个骚字段 to 88888888 539 | ``` 540 | 我们自定义的字段就完成了。 541 | 542 | 以上就是我们对序列化器的学习。目前我们就学习到这个程度,序列化器剩下知识的都是一些 API 相关的信息,需要用到的时候直接去查就是了。我们已经明白了序列化器的原理。以后遇到什么样的数据类型处理都不怕了,要是遇到太奇葩的的需求,大不了我们自己写一个字段。相关的细节我们在以后的学习中慢慢学习。 543 | 544 | ###API View 与 URL 配置 545 | 这是 DRF 的又一个很重要的地方,在第二章,我们自己编写了 APIView ,并且只支持一种内容协商,DRF 为我们提供了功能更加完备的 APIView, 不仅支持多种内容协商,还支持对 API 访问频率的控制,对查询结果过滤等等。 546 | 547 | DRF 的 API 视图有两种使用方式,一种是利用装饰器,一种是使用类视图。 548 | 549 | 我们主要讲类视图 API ,装饰器放在后面作为补充。 550 | 551 | 我们知道 Django 的视图返回的是 `HttpResponse` 对象,并且默认接收一个 `HttpRequest` 对象,我们可以通过这个请求对象访问到请求中响应的数据。同样的,DRF 的 APIView 也是接收一个默认的请求对象,返回一个响应对象。只是在 APIView 中的请求和响应对象变成了 DRF 的请求和响应对象。 552 | 553 | DRF 的请求对象功能比 Django 自带的要完备很多,也强大很多。不仅原生支持 `PUT` 方法,还支持对 `POST` URL 的参数解析等众多功能。 554 | 555 | 我们来看看 DRF 的请求对象都有哪些功能: 556 | 557 | 1. `.data`: DRF 请求对象的 `data` 属性包含了所有的上传对象,甚至包括文件对象!也就是说,我们可以只通过访问 `resquest.data` 就能得到所有的上传数据,包括 `PUT` 请求的!还支持多种数据上传格式,前端不仅可以以 form 的形式上传,还可以以 `json` 等众多其它形式上传数据! 558 | 2. `.query_params`: `query_params` 属性包含了所有的 URL 参数,不仅仅是 GET 请求的参数,任何请求方法 URL 参数都会被解析到这里。 559 | 3. `.user`: 和原生的 `user` 属性作用相同。 560 | 4. `.auth`: 包含额外的认证信息。 561 | 562 | 当然,DRF 的请求对象不止有这些功能,还有许多其它的功能,大家可以去文档里探索一下。 563 | 564 | DRF 的响应对象: 565 | 566 | DRF 响应对象接收以下参数: 567 | 1. `data`: 被序列化之后的数据。将被用作响应数据传给前端。 568 | 2. `status`: 状态码。 569 | 3. `headers`: 响应头。 570 | 4. `content_type`: 响应类型。一般不需要我们手动设置这个字段。 571 | 572 | 让我们来看看 DRF 的 APIView 具体的应用方法: 573 | 574 | ```python 575 | from rest_framework.views import APIView 576 | from rest_framework.response import Response 577 | from django.contrib.auth import get_user_model 578 | User = get_user_model() 579 | class ListUsers(APIView): 580 | def get(self, request, format=None): 581 | usernames = [user.username for user in User.objects.all()] 582 | return Response(usernames) 583 | ``` 584 | 这就是最简单的 APIView 了。我们的 `get` 函数的 `format` 参数是用于控制和前端的内容协商的,我们可以通过判断这个值来决定返回什么样类型的数据。同时,`APIView` 还有许多其它的参数供我们使用,但是目前我们就暂时先了解到这里。 585 | 586 | 别忘了,我们学习的是 REST 开发,对应的请求有对应普适的规则。所以,在 APIView 基础之上的类视图是十分有用的——`GenericView`,它就相当与我们之前编写的加了各种 Mixin 操作的 APIView,只不过 `GenericView` 提供的操作和功能比我们自己编写的要丰富很多。同样的,`GenericView` 也是通过提供各种通用使用各种 `Mixin` 的类属性和方法来提供不同的功能。所以我们就在这里简单的介绍一下这些类属性和方法: 587 | 588 | `GenericView` 提供的属性有: 589 | 590 | 1. `queryset`: 和我们的 `APIView` 中的 `queryset` 作用是相同的。 591 | 2. `serializer_class`: 序列化器,`GenericView` 将会自动应用这个序列化器进行相应的数据处理工作。 592 | 3. `lookup_field`: 和我们之前编写的 `lookup_args` 作用相同,只是它只有一个值,默认为 'pk' 。 593 | 4. `lookup_url_kwarg`: 在 URL 参数中用于查询实例的参数,在默认情况下,它的值等于 `lookup_field`。 594 | 5. `pagination_class`: 用于分页的类,默认为 `rest_framework.pagination.PageNumberPagination`,如果使它为 `None` ,就可以禁用分页功能。 595 | 6. `filter_backends`: 用于过滤查询集的过滤后端,可以在 `DEFAULT_FILTER_BACKENDS` 中配置。 596 | 597 | 提供的方法有: 598 | 599 | 1. `get_queryset(self)`: 获取查询集,默认行为和我们编写的 `get_queryset` 相同。 600 | 2. `get_object(self)`: 获取当前实例对象。 601 | 3. `filter_queryset(self, queryset)`: 在每一次查询之后都使用它来过滤一次查询集。并返回新的查询集。 602 | 4. `get_serializer_class(self)`: 获取序列化器,可以通过编写这个方法做到动态的序列化器的更换。 603 | 5. `get_serializer_context(self)`: 返回一个作用于序列化器的上下文字典。默认包含了 `request`, `view`,`format` 键。如果这里不懂没关系,我们后面还会讲到。 604 | 605 | 当然,`GenericView` 还提供了许多其它的功能,所以想要更多了解的同学可以去查阅官方文档。没看的也不用担心,我们在之后会慢慢的涉及到更多的知识点。 606 | 607 | 以上都介绍的很简单,我们要重点介绍的是下面的 `ViewSet`。 608 | 什么是 `ViewSet` ,`ViewSet` 顾名思义就是一大堆的视图集合。为什么要把一大推的视图集合到一起呢?因为他们都是通用的。具体体现在哪些地方呢?他们都是符合 REST 规范的视图,所以只需要按照 REST 规范就可以使用这些视图了。 609 | 610 | 比如,像这样,这是官方文档的例子: 611 | ```python 612 | # views.py 613 | from django.contrib.auth.models import User 614 | from django.shortcuts import get_object_or_404 615 | from myapps.serializers import UserSerializer 616 | from rest_framework import viewsets 617 | from rest_framework.response import Response 618 | 619 | class UserViewSet(viewsets.ViewSet): 620 | def list(self, request): 621 | queryset = User.objects.all() 622 | serializer = UserSerializer(queryset, many=True) 623 | return Response(serializer.data) 624 | 625 | def retrieve(self, request, pk=None): 626 | queryset = User.objects.all() 627 | user = get_object_or_404(queryset, pk=pk) 628 | serializer = UserSerializer(user) 629 | return Response(serializer.data) 630 | 631 | # urls.py 632 | user_list = UserViewSet.as_view({'get': 'list'}) 633 | user_detail = UserViewSet.as_view({'get': 'retrieve'}) 634 | ``` 635 | 看,我们使用了 `ViewSet` 之后就不用手动的编写 `get` 等等方法了,只需要编写对应的操作函数就可以了。更让人惊喜的是,`ViewSet` 的使用方法和我们之前使用了 `MethodMapMixin` 的 `APIView` 是一模一样的。通过方法映射到具体的操作函数上来。但是含有比这样写更酷的方法: 636 | ```python 637 | # urls.py 638 | from myapp.views import UserViewSet 639 | from rest_framework.routers import DefaultRouter 640 | 641 | router = DefaultRouter() 642 | router.register(r'users', UserViewSet, base_name='user') 643 | urlpatterns = router.urls 644 | ``` 645 | 通过使用 `Router` 来自动生成我们需要的 API 接口。这个等会儿再说。我们先说说 `GenericViewSet` 和 `ModelViewSet` 。 646 | `GenericViewSet`: 只是简单的添加上了 `GenericView` 的功能。我们重点说 `ModelViewSet`。 647 | 如果我们想要提供的 API 功能就是默认符合 REST 规范的 API ,要是使用 `ModelViewSet` 的话,我们就只需要提供一个参数就可以解决所有问题: 648 | ```python 649 | class UserViewSet(viewsets.ModelViewSet): 650 | queryset = User.objects.all() 651 | ``` 652 | 是的,我们的视图就这样就写完了。简化到两行,要是你愿意,也可以简化到一行。再配合 `Router` 使用,一共不超过十行代码就可以完成我们之前写了好几百行的代码完成的功能。 653 | 654 | 这里我们只是做简单的了解,等到真正需要用的时候大家才可以学习到其中的奥妙。我们接下来说说 `Router` 。 655 | 656 | `Router` 是用来帮我们自动生成 REST API 的。就像这种: 657 | ```python 658 | url(r'^users/$', name='user-list'), 659 | url(r'^users/{pk}/$', name='user-detail'), 660 | url(r'^accounts/$', name='account-list'), 661 | url(r'^accounts/{pk}/$', name='account-detail') 662 | ``` 663 | 自动生成这些 API ,这些 API 都符合 REST 规范。 664 | 665 | `Router` 的使用要结合我们上面学到的知识,本节我们就以 `Roter` 的使用收尾。 666 | 667 | 注释掉之前的验证代码,接着在后面写: 668 | ```python 669 | # frontend_data = { 670 | # 'name':'ucag', 671 | # 'tel':'88888888' 672 | # } 673 | # test = ContactSerializer(data=frontend_data) 674 | # if test.is_valid(): 675 | # tel = test.validated_data['tel'] 676 | # print('TEL',tel.num) 677 | # tel.text('这是一个骚字段') 678 | 679 | from rest_framework.viewsets import ModelViewSet 680 | class TestViewSet(ModelViewSet): 681 | queryset = TestModel.objects.all() 682 | 683 | from rest_framework.routers import DefaultRouter 684 | router = DefaultRouter() 685 | router.register(r'codes', TestViewSet) 686 | urlpatterns = router.urls 687 | print(urlpatterns) 688 | ``` 689 | 使用过程不多说,都是机械式的使用,先使用 `register` 注册 url,第一个参数是 url 的前缀,就是想用什么开头,比如 `url(r'^users/$', name='user-list')` 就是以 `users` 开头。视图的名字 `Router` 会自己帮你加上,就两种名字。一个是 `-list`,一个是`-detail` 。当然,如果你想改也是可以改的。这个留到我们以后说。 690 | 691 | 直接运行 `rest_test.py` ,你应该会看到以下输出。 692 | ``` 693 | [, 694 | [a-z0-9]+)/?$>, 695 | [^/.]+)/$>, 696 | [^/.]+)\.(?P[a-z0-9]+)/?$>, 697 | , [a-z0-9]+)/?$>] 698 | ``` 699 | 这就是 `Router` 为我们生成的 API 了。细心的同学或许已经发现了,还有个 `api-root` 的视图,访问这个 API 会返回所有的 list 视图的 API 。可以通过这些链接访问到所有的实例。 700 | 701 | --- 702 | 我们对 DRF 的初步学习就到这里。很明显,我们的本节的重点就是序列化器,所以大家务必掌握序列化器的相关知识点,对视图和 URL 配置不是怎么懂都没有什么大的问题,这些都只是 DRF API 调用的问题,唯独序列化器的使用和原理需要大家十分扎实的掌握。所以,最低的要求是起码在本节结束后看到使用 DRF 的代码,能够明白它是什么意思,能够模仿着写出东西,最好能够举一反三。本节涉及的知识点的确有些难,不过一个星期理解这些知识点的时间也应该足够了。下一节我们就要开始 `Vue` 的学习,相对来说会轻松一些了。大家加油 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | -------------------------------------------------------------------------------- /Chapter-three/Django REST 系列教程(三)(下).md: -------------------------------------------------------------------------------- 1 | #Django REST 系列教程(三)(下) 2 | 终于考完试了。今天有空把这部分完成了,大家久等了。 3 | 4 | 5 | --- 6 | ##设计 7 | 交互还是和以前一样,只是 API 略微有些变动。如果需要 创建时就运行一次代码 ,我们只需要在对应操作之后加上 `run` 参数就可以了。 比如 向 `/code/?run` post 就可以创建并运行代码了,更新并运行也是同理。 8 | 9 | 如果需要单独运行代码 向 `/run/` post 代码解可以了,如果需要运行特定的实例,只需使用 get 请求在后面加上 `id` 参数就行。比如 GET `/run/?id=1` 就会得到代码实例 id 为 1 的运行结果。 10 | 11 | ##准备工作。 12 | 创建一个新的项目 `online_python` 13 | 14 | ``` 15 | django-admin startproject online_python 16 | ``` 17 | 18 | 然后在 `online_python` 项目内创建一个 APP 19 | 20 | ``` 21 | python manage.py startapp backend 22 | ``` 23 | 24 | 然后创建如下的目录结构 25 | 26 | ``` 27 | online_python/ 28 | frontend/ 29 | index.js # 空文件 30 | index.html # 空文件 31 | vue.js # vue 的源文件 32 | bootstrap.js # bootstrap 的 js文件 33 | jquery.js # bootstrap.js 的依赖 34 | bootstrap.css # bootstrap 核心 css 文件 35 | backend/ 36 | ... # APP 相关 37 | manage.py 38 | ``` 39 | 40 | 41 | 编写配置,把我们的 APP 和 DRF 添加进去。 42 | 43 | `settings.py` 44 | ```python 45 | ... 46 | INSTALLED_APPS = [ 47 | 'django.contrib.admin', 48 | 'django.contrib.auth', 49 | 'django.contrib.contenttypes', 50 | 'django.contrib.sessions', 51 | 'django.contrib.messages', 52 | 'django.contrib.staticfiles', 53 | 'rest_framework', 54 | 'backend' 55 | ] 56 | ... 57 | ``` 58 | 59 | 准备完毕。 60 | 61 | ##后端开发 62 | 我们还是先从后端写起。 63 | 64 | 创建模型: 65 | 66 | `models.poy` 67 | ```python 68 | 69 | from django.db import models 70 | 71 | class Code(models.Model): 72 | name = models.CharField(max_length=20, blank=True) 73 | code = models.TextField() 74 | 75 | ``` 76 | 77 | 78 | 在模型创建完成之后,我们需要创建首次迁移。 79 | 80 | 回到项目根目录,创建并运行迁移,同时把管理员账户创建好。 81 | 82 | ``` 83 | python manage.py makemigrations 84 | python manage.py migrate 85 | 86 | python manage.py craetesuperuser 87 | ``` 88 | 89 | 90 | 创建序列化器: 91 | 92 | 在 `backend` 下新建文件 `serializers.py`: 93 | 94 | `serializers.py` 95 | ```python 96 | from rest_framework import serializers 97 | from .models import Code 98 | 99 | #创建序列化器 100 | class CodeSerializer(serializers.ModelSerializer): 101 | class Meta: 102 | model = Code 103 | fields = '__all__' #序列化全部字段 104 | 105 | #用于列表展示的序列化器 106 | class CodeListSerializer(serializers.ModelSerializer): 107 | class Meta: 108 | model = Code 109 | fields = ('id', 'name') 110 | ``` 111 | 112 | 为什么会有两个序列化器? 113 | 114 | 因为我们请求 `list` 时,我们只需要 `Code` 实例的 `name` 和 `id` 字段,在其它的情况下又需要用到全部的字段。所以我们需要两个序列化器。 115 | 116 | 现在就可以开始编写视图了。 117 | 118 | 在顶部引入我们需要的包。 119 | 120 | `views.py` 121 | ```python 122 | import subprocess 123 | from django.http import HttpResponse 124 | from django.db import models 125 | from rest_framework.viewsets import ModelViewSet 126 | from rest_framework.views import APIView 127 | from rest_framework.response import Response 128 | from rest_framework import status 129 | from .serializers import CodeListSerializer, CodeSerializer 130 | from .models import Code 131 | from rest_framework.authentication import SessionAuthentication 132 | ``` 133 | 134 | `subprocess` 用于运行客户端代码的包。 135 | 136 | `HttpResponse` 用于静态文件服务视图。 137 | 138 | `models` 主要是为了使用它的 `ObjectDoseNotExist` 异常。 139 | 140 | `APIView` 最基本的 DRF API 视图。 141 | 142 | `Response` DRF 响应对象。 143 | 144 | `status` DRF 为我们封装好的状态响应码。 145 | 146 | `CodeSerializer`、`CodeListSerializer` 需要用到的序列化器。 147 | 148 | `Code` Code 模型。 149 | 150 | `SessionAuthentication` 用于编写禁止 CSRF 的认证后端。我们会在下面详细的说明。 151 | 152 | 我们先把运行代码的 Mixin 给复制粘贴过来。 153 | 154 | `views.py` 155 | ```python 156 | class APIRunCodeMixin(object): 157 | """ 158 | 运行代码操作 159 | """ 160 | 161 | def run_code(self, code): 162 | """ 163 | 运行所给的代码,并返回执行结果 164 | :params code: str, 需要被运行的代码 165 | :return: str, 运行结果 166 | """ 167 | try: 168 | output = subprocess.check_output(['python', '-c', code], # 运行代码 169 | stderr=subprocess.STDOUT, # 重定向错误输出流到子进程 170 | universal_newlines=True, # 将返回执行结果转换为字符串 171 | timeout=30) # 设定执行超时时间 172 | except subprocess.CalledProcessError as e: # 捕捉执行失败异常 173 | output = e.output # 获取子进程报错信息 174 | except subprocess.TimeoutExpired as e: # 捕捉超时异常 175 | output = '\r\n'.join(['Time Out!', e.output]) # 获取子进程报错,并添加运行超时提示 176 | return output # 返回执行结果 177 | 178 | ``` 179 | 180 | 创建 CodeViewSet 181 | 182 | `views.py` 183 | ```python 184 | class CodeViewSet(APIRunCodeMixin, ModelViewSet): 185 | queryset = Code.objects.all() 186 | serializer_class = CodeSerializer 187 | ``` 188 | 189 | 190 | 这是最最基本的 CodeViewSet 。 DRF 的 ViewSet 为我们默认编写好了各个请求方法对应的操作映射。不带参数的 `get` 请求对应 `list` 操作,`post` 请求对应 `create` 操作等等。这也是它叫做 `ViewSet` (视图集)的原因,它帮我们完成了基本的几个视图原型。`ModelViewSet` 让我们可以直接把视图和模型相关联起来,比如 `list` 会直接返回模型序列化之后的结果,而不需要我们手动编写这些动作。 191 | 192 | `list` 默认使用的是 `serializer_class` 指定的序列化器,但是由于我们需要在 `list` 动作的时候用另一个序列化器,所以我们需要简单的重写这个动作。 193 | 194 | 195 | `views.py` 196 | ```python 197 | def list(self, request, *args, **kwargs): 198 | """ 199 | 使用专门的列表序列化器,而非默认的序列化器 200 | """ 201 | serializer = CodeListSerializer(self.get_queryset(), many=True) 202 | return Response(data=serializer.data) 203 | ``` 204 | 205 | 206 | `create` 操作需要判断是否有 `run` 参数,所以我们也需要重写 `create` 操作。 207 | 208 | `views.py` 209 | ```python 210 | def create(self, request, *args, **kwargs): 211 | serializer = self.serializer_class(data=request.data) 212 | 213 | if serializer.is_valid(): 214 | code = serializer.validated_data.get('code') 215 | serializer.save() 216 | if 'run' in request.query_params.keys(): 217 | output = self.run_code(code) 218 | data = serializer.data 219 | data.update({'output': output}) 220 | return Response(data=data, status=status.HTTP_201_CREATED) 221 | return Response(data=serializer.data, status=status.HTTP_201_CREATED) 222 | return Response(data=serializer.errors, status=status.HTTP_400_BAD_REQUEST) 223 | ``` 224 | 225 | 我们知道 Django 会向视图函数默认传入一个 `Request` 对象,但是这里的 `Request` 对象是 DRF 的请求对象。 226 | 227 | `request.query_params` 是 DRF 请求对象获取请求参数的方式,`query_params` 保存了所有的请求参数。 228 | 229 | 在 Django 的表单中,我们可以使用 `form.save()` 来直接把数据保存到模型中。在序列化器中也是同理,我们可以使用 `serializer.save()` 把序列化器中的数据直接保存到模型中。 230 | 231 | 同样的,我们的 `update` 操作也需要做同样的事情。 232 | 233 | `views.py` 234 | ```python 235 | def update(self, request, *args, **kwargs): 236 | instance = self.get_object() 237 | serializer = self.serializer_class(instance, data=request.data) 238 | 239 | if serializer.is_valid(): 240 | code = serializer.validated_data.get('code') 241 | serializer.save() 242 | if 'run' in request.query_params.keys(): 243 | output = self.run_code(code) 244 | data = serializer.data 245 | data.update({'output': output}) 246 | return Response(data=data, status=status.HTTP_201_CREATED) 247 | return Response(data=serializer.data, status=status.HTTP_201_CREATED) 248 | return Response(data=serializer.errors, status=status.HTTP_400_BAD_REQUEST) 249 | ``` 250 | 251 | `get_object` 是用于提取当前请求对应的实例方法。 252 | 253 | 我们发现,`create` 和 `update` 除了在创建序列化器实例不同之外,我们完全可以把他们的逻辑放在一起。 254 | 255 | `views.py` 256 | ```python 257 | def run_create_or_update(self, request, serializer): 258 | """ 259 | create 和 update 的共有逻辑,仅仅是简单的多了 run 参数的判断 260 | """ 261 | if serializer.is_valid(): 262 | code = serializer.validated_data.get('code') 263 | serializer.save() 264 | if 'run' in request.query_params.keys(): 265 | output = self.run_code(code) 266 | data = serializer.data 267 | data.update({'output': output}) 268 | return Response(data=data, status=status.HTTP_201_CREATED) 269 | return Response(data=serializer.data, status=status.HTTP_201_CREATED) 270 | return Response(data=serializer.errors, status=status.HTTP_400_BAD_REQUEST) 271 | 272 | def create(self, request, *args, **kwargs): 273 | serializer = self.serializer_class(data=request.data) 274 | return self.run_create_or_update(request, serializer) 275 | 276 | def update(self, request, *args, **kwargs): 277 | instance = self.get_object() 278 | serializer = self.serializer_class(instance, data=request.data) 279 | return self.run_create_or_update(request, serializer) 280 | 281 | ``` 282 | 283 | 到这里我们主要的 API 就完成了。 284 | 285 | 现在需要完成可以直接运行代码的 API 。 286 | 287 | `views.py` 288 | ```python 289 | class RunCodeAPIView(APIRunCodeMixin, APIView): 290 | 291 | def post(self, request, format=None): 292 | output = self.run_code(request.data.get('code')) 293 | return Response(data={'output': output}, status=status.HTTP_200_OK) 294 | 295 | def get(self, request, format=None): 296 | try: 297 | code = Code.objects.get(pk=request.query_params.get('id')) 298 | except models.ObjectDoesNotExist: 299 | return Response(data={'error': 'Object Not Found'}, status=status.HTTP_404_NOT_FOUND) 300 | output = self.run_code(code.code) 301 | return Response(data={'output': output}, status=status.HTTP_200_OK) 302 | 303 | ``` 304 | 305 | 306 | 接下来完成静态文件服务的请求视图,把之前写的代码复制粘贴过来,稍稍做一些更改。 307 | 308 | `views.py` 309 | ```python 310 | def home(request): 311 | with open('frontend/index.html', 'rb') as f: 312 | content = f.read() 313 | return HttpResponse(content) 314 | 315 | 316 | def js(request, filename): 317 | with open('frontend/{}'.format(filename), 'rb') as f: 318 | js_content = f.read() 319 | return HttpResponse(content=js_content, 320 | content_type='application/javascript') 321 | 322 | 323 | def css(request, filename): 324 | with open('frontend/{}'.format(filename), 'rb') as f: 325 | css_content = f.read() 326 | return HttpResponse(content=css_content, 327 | content_type='text/css') 328 | 329 | ``` 330 | 331 | 332 | 完成我们的 url 配置。 333 | 334 | `urls.py` 335 | ```python 336 | from django.conf.urls import url, include 337 | from django.contrib import admin 338 | from rest_framework.routers import DefaultRouter 339 | from backend.views import CodeViewSet, RunCodeAPIView, home, js, css 340 | 341 | router = DefaultRouter() 342 | router.register(prefix='code', viewset=CodeViewSet, base_name='code') 343 | 344 | API_V1 = [url(r'^run/$', RunCodeAPIView.as_view(), name='run')] 345 | 346 | API_V1.extend(router.urls) 347 | 348 | API_VERSIONS = [url(r'^v1/', include(API_V1))] 349 | 350 | urlpatterns = [ 351 | url(r'^admin/', admin.site.urls), 352 | url(r'^api/', include(API_VERSIONS)), 353 | url(r'^js/(?P.*\.js)$', js, name='js'), 354 | url(r'^css/(?P.*\.css)$', css, name='css'), 355 | url(r'^$', home, name='home') 356 | ] 357 | ``` 358 | 359 | 在之前,我们对于 csrf 的处理都是使用的 `csrf_exempt` ,现在我们的 API 都是使用 Router 来生成了。该怎么办呢? 360 | 361 | 在 Django 中,一个请求在到达视图之前,会先经过中间件的处理。在 DRF 中,所有的请求会先经过认证处理,如果请求认证通过,则会让请求访问视图,如果认证不通过,请求就无法到达视图。所以,我们采用的方法是重写认证。 362 | 363 | 在 APIView 中,如果提供了 `authentication_classes` ,则会使用提供的认证后端来进行认证。如果没有提供,则会使用默认的认证后端。有关的细节我们将会在之后的章节中讨论,大家就先了解到这里。提供 csrf 验证的是一个叫做 `SessionAuthentication` 的认证后端,我们需要重新改写其中验证 csrf 的方法。 364 | 365 | `views.py` 366 | ```python 367 | class CsrfExemptSessionAuthentication(SessionAuthentication): 368 | """ 369 | 去除 CSRF 检查 370 | """ 371 | 372 | def enforce_csrf(self, request): 373 | return 374 | ``` 375 | 376 | 这样就完成了。 377 | 378 | 379 | 然后把它放进我们视图中。 380 | 381 | 整个 `views.py` 的代码就是这样的。 382 | 383 | `views.py` 384 | ```python 385 | import subprocess 386 | from django.http import HttpResponse 387 | from django.db import models 388 | from rest_framework.viewsets import ModelViewSet 389 | from rest_framework.views import APIView 390 | from rest_framework.response import Response 391 | from rest_framework import status 392 | from .serializers import CodeListSerializer, CodeSerializer 393 | from .models import Code 394 | from rest_framework.authentication import SessionAuthentication 395 | 396 | 397 | class CsrfExemptSessionAuthentication(SessionAuthentication): 398 | """ 399 | 去除 CSRF 检查 400 | """ 401 | 402 | def enforce_csrf(self, request): 403 | return 404 | 405 | 406 | class APIRunCodeMixin(object): 407 | """ 408 | 运行代码操作 409 | """ 410 | 411 | def run_code(self, code): 412 | """ 413 | 运行所给的代码,并返回执行结果 414 | :params code: str, 需要被运行的代码 415 | :return: str, 运行结果 416 | """ 417 | try: 418 | output = subprocess.check_output(['python', '-c', code], # 运行代码 419 | stderr=subprocess.STDOUT, # 重定向错误输出流到子进程 420 | universal_newlines=True, # 将返回执行结果转换为字符串 421 | timeout=30) # 设定执行超时时间 422 | except subprocess.CalledProcessError as e: # 捕捉执行失败异常 423 | output = e.output # 获取子进程报错信息 424 | except subprocess.TimeoutExpired as e: # 捕捉超时异常 425 | output = '\r\n'.join(['Time Out!', e.output]) # 获取子进程报错,并添加运行超时提示 426 | return output # 返回执行结果 427 | 428 | 429 | class CodeViewSet(APIRunCodeMixin, ModelViewSet): 430 | queryset = Code.objects.all() 431 | serializer_class = CodeSerializer 432 | authentication_classes = (CsrfExemptSessionAuthentication,) 433 | 434 | def list(self, request, *args, **kwargs): 435 | """ 436 | 使用专门的列表序列化器,而非默认的序列化器 437 | """ 438 | serializer = CodeListSerializer(self.get_queryset(), many=True) 439 | return Response(data=serializer.data) 440 | 441 | def run_create_or_update(self, request, serializer): 442 | """ 443 | create 和 update 的共有逻辑,仅仅是简单的多了 run 参数的判断 444 | """ 445 | if serializer.is_valid(): 446 | code = serializer.validated_data.get('code') 447 | serializer.save() 448 | if 'run' in request.query_params.keys(): 449 | output = self.run_code(code) 450 | data = serializer.data 451 | data.update({'output': output}) 452 | return Response(data=data, status=status.HTTP_201_CREATED) 453 | return Response(data=serializer.data, status=status.HTTP_201_CREATED) 454 | return Response(data=serializer.errors, status=status.HTTP_400_BAD_REQUEST) 455 | 456 | def create(self, request, *args, **kwargs): 457 | serializer = self.serializer_class(data=request.data) 458 | return self.run_create_or_update(request, serializer) 459 | 460 | def update(self, request, *args, **kwargs): 461 | instance = self.get_object() 462 | serializer = self.serializer_class(instance, data=request.data) 463 | return self.run_create_or_update(request, serializer) 464 | 465 | 466 | class RunCodeAPIView(APIRunCodeMixin, APIView): 467 | authentication_classes = (CsrfExemptSessionAuthentication,) 468 | 469 | def post(self, request, format=None): 470 | output = self.run_code(request.data.get('code')) 471 | return Response(data={'output': output}, status=status.HTTP_200_OK) 472 | 473 | def get(self, request, format=None): 474 | try: 475 | code = Code.objects.get(pk=request.query_params.get('id')) 476 | except models.ObjectDoesNotExist: 477 | return Response(data={'error': 'Object Not Found'}, status=status.HTTP_404_NOT_FOUND) 478 | output = self.run_code(code.code) 479 | return Response(data={'output': output}, status=status.HTTP_200_OK) 480 | 481 | 482 | def home(request): 483 | with open('frontend/index.html', 'rb') as f: 484 | content = f.read() 485 | return HttpResponse(content) 486 | 487 | 488 | def js(request, filename): 489 | with open('frontend/{}'.format(filename), 'rb') as f: 490 | js_content = f.read() 491 | return HttpResponse(content=js_content, 492 | content_type='application/javascript') 493 | 494 | 495 | def css(request, filename): 496 | with open('frontend/{}'.format(filename), 'rb') as f: 497 | css_content = f.read() 498 | return HttpResponse(content=css_content, 499 | content_type='text/css') 500 | 501 | ``` 502 | 503 | 504 | DRF 还为我们提供了可视化的 API 。运行开发服务器,直接访问 `http://127.0.0.1:8000/api/v1/` 505 | 506 | 你会看到这样的页面 507 | 508 | ![API 根路径](https://raw.githubusercontent.com/Ucag/django-rest/master/Chapter-three/imgs/API%20ROOT.png) 509 | 510 | DRF 为我们列出了 `code` API ,点击连接地址,我们就可以在跳转的页面中直接进行相关的操作。比如用 POST 创建一个新的代码实例。 511 | 512 | ![post 示例](https://raw.githubusercontent.com/Ucag/django-rest/master/Chapter-three/imgs/post%20%E7%A4%BA%E4%BE%8B.png) 513 | 提交之后,我们来到了这样的页面。 514 | ![提交结果](https://raw.githubusercontent.com/Ucag/django-rest/master/Chapter-three/imgs/%E6%8F%90%E4%BA%A4%E7%BB%93%E6%9E%9C.png) 515 | 516 | 然后我们直接在浏览器中访问这个实例的地址,在这里,我的 id 是 46 ,你们根据自己的实例创建 id 来访问。 517 | 518 | ![实例详情](https://raw.githubusercontent.com/Ucag/django-rest/master/Chapter-three/imgs/%E5%AE%9E%E4%BE%8B%E8%AF%A6%E6%83%85.png) 519 | 520 | 这个可视化 API 有什么用呢?最大的用处莫过于在前端开发的时候,看看不同的接口会返回什么样的数据类型,具体的格式是什么,这样前端才好对相应的数据做正确的处理。当前端在开发时对接口有什么疑问,可以自行用它来做实验。方便了前后端的接口协作处理。 521 | 522 | ##前端开发 523 | 首先在入口 html 页面中写好组件入口。 524 | 525 | `index.html` 526 | ```html 527 | 528 | 529 | 530 | 531 | 在线 Python 解释器 532 | 533 | 534 | 535 |
536 | 537 | 538 | 539 | 540 | 541 | 542 | ``` 543 | 544 | 接下来的工作都将会在 `index.js` 中完成。 545 | 546 | 先编写好 API : 547 | `index.js` 548 | ```javascript 549 | let api = { 550 | v1: { 551 | run: function () { 552 | return '/api/v1/run/' 553 | }, 554 | code: { 555 | list: function () { 556 | return '/api/v1/code/' 557 | }, 558 | create: function (run = false) { 559 | let base = '/api/v1/code/'; 560 | return run ? base + '?run' : base 561 | }, 562 | detail: function (id, run = false) { 563 | let base = `/api/v1/code/${id}/`; 564 | return run ? base + '?run' : base 565 | }, 566 | remove: function (id) { 567 | return api.v1.code.detail(id, false) 568 | }, 569 | update: function (id, run = false) { 570 | return api.v1.code.detail(id, run) 571 | } 572 | } 573 | } 574 | } 575 | ``` 576 | 577 | 我们还需要 Store 来管理状态。我们知道 Store 是管理和储存公共数据的地方,同时我们对于 API 的操作其实就是对于数据的操作,我们应该把所有直接和 API 相关的请求和操作都定义在这里。 578 | 579 | `index.js` 580 | ```javascript 581 | let store = { 582 | state: { 583 | list: [], 584 | code: '', 585 | name: '', 586 | id: '', 587 | output: '' 588 | }, 589 | actions: { 590 | run: function (code) { //运行代码 591 | $.post({ 592 | url: api.v1.run(), 593 | data: {code: code}, 594 | dataType: 'json', 595 | success: function (data) { 596 | store.state.output = data.output 597 | } 598 | }) 599 | }, 600 | runDetail: function (id) { //运行特定的代码 601 | $.getJSON({ 602 | url: api.v1.run() + `?id=${id}`, 603 | success: function (data) { 604 | store.state.output = data.output 605 | } 606 | }) 607 | }, 608 | freshList: function () { //获得代码列表 609 | $.getJSON({ 610 | url: api.v1.code.list(), 611 | success: function (data) { 612 | store.state.list = data 613 | } 614 | }) 615 | }, 616 | getDetail: function (id) {//获得特定的代码实例 617 | $.getJSON({ 618 | url: api.v1.code.detail(id), 619 | success: function (data) { 620 | store.state.id = data.id; 621 | store.state.name = data.name; 622 | store.state.code = data.code; 623 | store.state.output = ''; 624 | } 625 | }) 626 | }, 627 | create: function (run = false) { //创建新代码 628 | $.post({ 629 | url: api.v1.code.create(run), 630 | data: { 631 | name: store.state.name, 632 | code: store.state.code 633 | }, 634 | dataType: 'json', 635 | success: function (data) { 636 | if (run) { 637 | store.state.output = data.output 638 | } 639 | store.actions.freshList() 640 | } 641 | }) 642 | }, 643 | update: function (id, run = false) { //更新代码 644 | $.ajax({ 645 | url: api.v1.code.update(id, run), 646 | type: 'PUT', 647 | data: { 648 | code: store.state.code, 649 | name: store.state.name 650 | }, 651 | dataType: 'json', 652 | success: function (data) { 653 | if (run) { 654 | store.state.output = data.output 655 | } 656 | store.actions.freshList() 657 | } 658 | }) 659 | }, 660 | remove: function (id) { //删除代码 661 | $.ajax({ 662 | url: api.v1.code.remove(id), 663 | type: 'DELETE', 664 | dataType: 'json', 665 | success: function (data) { 666 | store.actions.freshList() 667 | } 668 | }) 669 | } 670 | } 671 | } 672 | 673 | store.actions.freshList() // Store的初始化工作,先获取代码列表 674 | ``` 675 | 相比我们之前结构,把统一的数据操作都放在 Store 中,这样就不会显得很混乱,并且 API 也简洁了不少。 676 | 677 | 下面改编写组件了。 678 | 679 | 在写代码的时候,我们需要按照“人类思维”来写代码,但是在具体组织代码的时候,我们需要按照“程序思维”来组织代码。根组件会引用前面的组件,但是前面的组件我们都还没有实现,所以根组件事实上是应该放在所有代码之后的。所以大家在写的时候注意自己代码的该写在哪里。不要代码全对而产生 `undefined` 错误。 680 | 681 | 先编写根组件: 682 | 683 | `index.js` 684 | ```javascript 685 | let root = new Vue({ //根组件,整个页面入口 686 | el: '#app', 687 | template: '', 688 | components: { 689 | 'app': app 690 | } 691 | }) 692 | ``` 693 | 694 | `app` 是我们的页面框架,我们在下面实现它。 695 | 696 | 然后在 root **上面** 编写页面框架: 697 | 698 | `index.js` 699 | ```javascript 700 | let app = { //整体页面布局 701 | template: ` 702 |
703 |
704 | 在线 Python 解释器 705 |
706 |
707 |
708 |
709 | 710 |
711 |
712 |
713 |
714 |

请在下方输入代码:

715 | 716 |
717 | 718 |
719 |

输出

720 |
721 | 722 |
723 |
724 |
725 |
726 |
727 | `, 728 | components: { 729 | 'code-input': input, 730 | 'code-list': list, 731 | 'code-options': options, 732 | 'code-output': output 733 | } 734 | } 735 | ``` 736 | `app` 组件是所有组件被组织在一起的地方,但是用到的组件都还没有实现,所以还没有被实现的组件代码都应该放在它的 **上面**。 737 | 738 | list 组件: 739 | 740 | `index.js` 741 | ```javascript 742 | let list = { //代码列表组件 743 | template: ` 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 759 | 760 | 761 |
文件名选项
{{ item.name }} 755 | 756 | 757 | 758 |
762 | `, 763 | data() { 764 | return { 765 | state: store.state 766 | } 767 | }, 768 | methods: { 769 | getDetail(id) { 770 | store.actions.getDetail(id) 771 | }, 772 | run(id) { 773 | store.actions.runDetail(id) 774 | }, 775 | remove(id) { 776 | store.actions.remove(id) 777 | } 778 | } 779 | } 780 | ``` 781 | 782 | options 组件: 783 | 784 | `index.js` 785 | ```javascript 786 | let options = {//代码选项组件 787 | template: ` 788 |
791 | 792 | 793 | 794 | 795 |
796 | `, 797 | data() { 798 | return { 799 | state: store.state 800 | } 801 | }, 802 | methods: { 803 | run(code) { 804 | store.actions.run(code) 805 | }, 806 | update(id, run = false) { 807 | if (typeof id == 'string') { 808 | store.actions.create(run) 809 | } else { 810 | store.actions.update(id, run) 811 | } 812 | }, 813 | newOptions() { 814 | this.state.name = ''; 815 | this.state.code = ''; 816 | this.state.id = ''; 817 | this.state.output = ''; 818 | } 819 | } 820 | } 821 | ``` 822 | 823 | input 组件: 824 | 825 | `index.js` 826 | ```javascript 827 | let input = { //代码输入组件 828 | template: ` 829 |
830 | 835 | 836 |

如需保存代码,建议输入代码片段名

837 | 842 |
843 | `, 844 | data() { 845 | return { 846 | state: store.state 847 | } 848 | }, 849 | methods: { 850 | flexSize(selector) { 851 | let ele = $(selector); 852 | ele.css({ 853 | 'height': 'auto', 854 | 'overflow-y': 'hidden' 855 | }).height(ele.prop('scrollHeight')) 856 | }, 857 | inputHandler(e) { 858 | this.state.code = e.target.value; 859 | this.flexSize(e.target) 860 | } 861 | } 862 | } 863 | ``` 864 | 865 | 我们把之前的 `flexSize`直接复制粘贴过来了。这样做的好处是,和组件有关的东西都在组件内,而不需要去到处找。 866 | 867 | output 组件: 868 | 869 | `index.js` 870 | ```javascript 871 | let output = { //代码输出组件 872 | template: ` 873 | 875 | `, 876 | data() { 877 | return { 878 | state: store.state 879 | } 880 | }, 881 | updated() { 882 | let ele = $(this.$el); 883 | ele.css({ 884 | 'height': 'auto', 885 | 'overflow-y': 'hidden' 886 | }).height(ele.prop('scrollHeight')) 887 | } 888 | } 889 | ``` 890 | 891 | 在这里我们选择了完全不同的动态大小方案。在 input 组件中,我们选择的是使用 input 事件来触发调整大小的函数。而在这里,我们选择在 output 组件**更新**完毕之后之后再触发这个函数。 892 | 893 | `.$el` 是这个组件最外层的 html 标签。在这里就是我们的 `textarea` 标签了。 894 | 895 | 如果我们需要组件在更新完毕之后做什么事情,就在选项对象里定义 `updated` 属性,组件会在更新完毕后调用它。这属于组件的生命周期的一部分。 896 | 897 | 生命周期有点类似 Django 的信号系统。比如有的同学可能知道 `post_save` ,我们可以用它来让一个模型保存完毕之后做些事情。而组件则有许多这样的东西。 898 | Vue 给我们提供了组件在不同阶段的接口。 899 | 900 | 关于生命周期更详细的细节,我们会在后面的章节里讨论。 901 | 902 | 到这里我们就完成了这次重构。赶紧试试效果吧。 903 | 904 | --- 905 | 本章我们初次接触了 DRF 和 Vue ,并且重构了一下试了试效果。DRF 则节约了我们不少接口开发的时间。vue 使我们的开发更加有调理,页面不再是一团乱麻。在下一章,我们将学习前端工具链。要一路从 node 学到 webpack 。 906 | 907 | 908 | 909 | 910 | 911 | --------------------------------------------------------------------------------