怪异的尝试

上篇博文回顾了Python。接着了解一下Django,以及后面不相关的React…

本文运行环境为Python-3.6,Django-2.03。大多数发行版预装python-2.x,你可能需要使用python3, pip3类似的命令。

Django快捷搭建

    • 安装:pip3 install django==2.03
    • 术后检查:python3 -m django –version,我这里打印2.0.3
    • 新建项目:django-admin startproject mysite,mysite指你的项目名称。django会自动创建如下文件结构:
      mysite/
          manage.py
          mysite/
              __init__.py
              settings.py
              urls.py
              wsgi.py
    • 测试运行:根目录下,python manage.py runserver,或者./manage.py runserver。这样可能需要改动manage.py头部,将python改为python3。后续亦是如此。
    • 我尝试不创建app,结果orm,静态文件等没法用。所以./manage.py startapp vocab,这就是本次的玩具,统计单词的小程序。根目录会多一个文件夹,结构是这样:
      vocab/
          __init__.py
          admin.py
          apps.py
          migrations/
              __init__.py
          models.py
          tests.py
          views.py
      
    • 创建views。
      from django.http import HttpResponse
      # Create your views here.
      def index(request):
      	return HttpResponse('haj') #for test
      
    • 关联url。修改项目urls.py
      from django.urls import path, include
      
      urlpatterns = [
          # path('admin/', admin.site.urls),
          path('vocab/',include('vocab.urls')),
      ] 
      

      接着创建vocab项目的urls.py

      from django.urls import include, path
      from . import views
      from django.conf import settings
      from django.conf.urls.static import static
      
      app_name='vocab'
      
      urlpatterns = [
      	path('', views.index, name = 'index')
      ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
      

       

    • 现在,访问localhost:8000/vocab,看到‘haj’,说明成功了。上面顺便设置了静态文件。接下来的静态页面会放在mysite/vocab/static,浏览器通过/static/*访问。
    • 接下来再app根目录创建templates/index.html,修改一下views让其返回静态文件:
      from django.shortcuts import render_to_response
      
      def index(request):
      	return render_to_response('index.html')
      

      现在访问/vocab返回的就是index.html。

P.S. 我个人钟意前端渲染,彻底的前后端分离。再者我觉得Django的模板并不是很方便。

至此,简单Django app已经成型。接下来看看ORM有什么有趣的东西。

ORM

看了Django的ORM,无法再直视Hibernate。这套设计,深入我心。假设让我实现,也是趋于它。闲话少说,看实例:

# /vocab/models.py
from django.db import models

# Create your models here.
class Category(models.Model):
	tag = models.CharField(max_length=32)
class Word(models.Model):
	category = models.ForeignKey(Category,
		on_delete=models.CASCADE)
	vocabulary = models.CharField(max_length=128)
	explanation = models.CharField(max_length=512)

我觉得这已经简单到无需解释。接下来涉及到migration,这是django特色,用于配置表格。你需要先创建migrations,./manage.py makemigrations vocab,然后通过它创建表格,./manage.py migrate

之前打算,如果用C++去写,就使用sqlite或者levelDB,不搞复杂。刚好,django的数据库默认配置就是sqlite,就不改了。

P.S. 我有想法给每个word打上N个tags,类似 {tags:[‘food’, ‘material’, ‘animal’]}。这东西用SQL就只能搞中间表。或许可以考虑PostgreSQL的数组字段,容我稍后研究。

分离前端

接着来到了前端,这也是为什么本文名为“怪异的组合”,因为我要用React.js。现时React版本为16.3,不久前正式接纳了Context。本文将尝试这钦定的上下文管理组件。

Yarn

首先,我对npm已经有了阴影,不管5.x版本怎么改,我还是决定使用yarn。既然这样就简单看一下yarn的使用方式。

  • 对比npm,install/uinstall变成了add/remove

考虑到大量boilerplate当然还是用create-react-app。yarn的全局安装稍稍不同

  • yarn global add create-react-app –prefix /usr/local

不指定安装位置,不知道会装到哪里,有缘再研究。

脚手架*

P.S. 我原本的设想是完全使用前端渲染,后端只提供数据。用webpack的目的是减少文件,方便在Django部署。结果webpack对service-worker的处理非常绕,django的静态文件配置也有些混乱,url匹配优先于普通view…不予置评。

最终我决定不使用静态文件。起两个服务,跨域拿数据。接下来这一节可选跳过。

前端生态真是时过境迁。以前配置的脚本很多都失效了,本次正好重新记录。我个人倾向于打包所有文件,所以接下来安装webpack以及相关依赖。

  • yarn global add webpack webpack-cli –prefix /usr/local
  • yarn add babel-core babel-loader babel-preset-es2015 babel-preset-es2017 babel-preset-react babel-plugin-transform-object-rest-spread css-loader file-loader style-loader  –dev

ES6,ES7太好用了,当然要安装插件支持。

  • 在根目录创建babel配置文件.babelrc
    {
      "presets": [
        "es2015",
        "es2017",
        "react"
      ],
       "plugins": ["transform-object-rest-spread"]
    }
    
  • 最后创建webpack的配置文件,webpack.config.js
    var webpack = require('webpack');
    var path = require('path');
    
    var BUILD_DIR = path.resolve(__dirname, 'app');
    var APP_DIR = path.resolve(__dirname, 'src/');
    
    var config = {
      entry: APP_DIR + '/index.js',
      output: {
        path: BUILD_DIR,
        filename: 'bundle.js'
      },
      module: {
        rules: [
          {
            test: /\.js$/,
            include: APP_DIR,
            loader: 'babel-loader'
          },
          {
          	test: /\.css$/,
          	include: APP_DIR,
          	use: ['style-loader', 'css-loader']
          },
        {
           test: /\.(png|svg|jpg|gif)$/,
    	    include: APP_DIR,
           use: [
             'file-loader'
           ]
         }
        ]
      }
    };
    
    module.exports = config;
    

如果我没漏写什么,执行webpack -p,就可以拿到打包文件(p指production)。

跨域

以往的经历,很多人告诉我跨域是JS的问题。真实情况发生在服务端

首先python有跨域防御机制,这里直接禁用。注释掉settings.py中MIDDLEWARE数组的 ‘django.middleware.csrf.CsrfViewMiddleware’

接着在每个response头部加一些内容,我觉得没必要搞什么过滤器,直接打包一个响应函数,顺便序列化json

def res(data):
  response = JsonResponse(data)
  response["Access-Control-Allow-Origin"] = '*'
  response["Access-Control-Allow-Methods"] = 'GET, POST, PUT, DELETE, OPTIONS'
  response["Access-Control-Allow-Headers"] = '*'
  return response
class categories(View):
  def get(self, request):
    return res({'get':'suc'})
  def post(self, request):
    data = request.POST.get['data']
    return res({'post': 'suc'})
  def put(self, request):
    return res({'put': 'suc'})
  def delete(self, request, data):
    return res({'delete': 'suc'})
  def options(self, request, data):
    return res({'options':'ok'})

可以看到,我加入了OPTIONS的選項。因为,PUTDELETE会做一个preflight,在前端PUT,后端实际上收到了个请求:

[03/Apr/2018 16:39:21] "OPTIONS /categories HTTP/1.1" 200 17
[03/Apr/2018 16:39:21] "PUT /categories HTTP/1.1" 200 1

而且要拿uri参数,必须统一options的型参。

搞明白跨域机制的细节花了我几个小时,

不知道与React配合有什么更好的ajax包,所以我还是用jquery,很多人对这个纯小写jquery有意见,不过3.X之后的版本真就是这么写)。测过四种接口,都通,没什么技术含量。

Context

看了一下React这套Context,这个也太轻量级了。所谓的Provider/Consumer就是个装饰器,貌似仅解决传递链条太长的问题。虽然redux如同火葬场,有些功能还是必要的。

下面是我暂时想到的写法,绕了一点,但是便于管理全局状态:

import React, { Component } from 'react';
import $ from 'jquery'

export const { Provider, Consumer: ModelCon } = React.createContext([])

const URL = 'http://localhost:8000/'

export class ModelPro extends Component {
  constructor(props) {
    super(props)
    this.state = {
      data: {
        categories: []
      },
      controller: {
        postCategory: (data, callback = function () { }) => {
          $.post(URL + 'categories', { data: data }, () => {
            callback()
            this.getCategories()
          })
        },
        getCategories: this.getCategories,
        deleteCategory: (dt) => {
          $.ajax({
            url: URL + 'categories/' + dt + '/',
            type: "DELETE",
            success: this.getCategories
          })
        }
      }
    }
    this.getCategories = () => {
      $.getJSON(URL + 'categories', (res) => {
        const tags = res['tags']
        this.setState({
          data: { categories: tags }
        })
      })
    }
  }
  componentDidMount() {
    this.getCategories();
  }

  render() {
    return <Provider value={this.state}>
      {this.props.children}
    </Provider>
  }
}

看起来有点怪,我将与后端交互的行为也存放在Context中,这么做现在看至少有效。视图组件从Context拿回调函数,嗯…看起来有点Controller的样子,可考虑将代码放在controllers文件夹中,最后再绑定,这不过是形式上的区别。看代码:

getData() {
  return <ModelPro >
    <ModelCon>{
      context =>
        <button onClick={ () => context.controller.
              postCategory(this.state.addCate)
        }>add</button>}
    </ModelCon>
  </ModelPro>
}

这样就很简单了。其实本来没有Context也可以搞出类似的东西…

打通通道

也就是剩下的views,在我改造之后已经不像是view层:

class categories(View):
  def get(self, request):
    tags = list(Category.objects.values_list('tag', flat=True))
    return res({'tags':tags})
  def post(self, request):
    data = request.POST.get('data')
    c = Category(tag=data)
    c.save()
    return res({'post': 'suc'})
  def put(self, request):
    return res({'put': 'suc'})
  def delete(self, request ,cate):
    c = Category.objects.get(tag=cate)
    c.delete()
    return res({'delete':'suc'})
  def options(self, request,cate):
    return res({'options':'ok'})

这些CRUD都很符合直觉,那个save()和delete()应该是异步的。之前使用MongoDB的驱动也是类似的设计。

P.S. 后来发现有个django-rest的包。或许能简化一点问题。不过Django不来就不适合这么玩,用全家桶是最方便的。

结束语

Django全家桶固然很好用。但我还是喜欢灵活一点,有缘可能会看看Flask。

Leave a Reply

Your email address will not be published. Required fields are marked *