。
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 | 
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: ``,
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: ``,
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 | | {{ item.name }} |
594 |
595 |
596 |
597 |
598 | |
599 |
600 |
601 |
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 |
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 | 
509 |
510 | DRF 为我们列出了 `code` API ,点击连接地址,我们就可以在跳转的页面中直接进行相关的操作。比如用 POST 创建一个新的代码实例。
511 |
512 | 
513 | 提交之后,我们来到了这样的页面。
514 | 
515 |
516 | 然后我们直接在浏览器中访问这个实例的地址,在这里,我的 id 是 46 ,你们根据自己的实例创建 id 来访问。
517 |
518 | 
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 | | {{ item.name }} |
754 |
755 |
756 |
757 |
758 | |
759 |
760 |
761 |
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 |
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 |
--------------------------------------------------------------------------------