欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Stark 组件:快速开发神器 —— 自动生成 URL

程序员文章站 2022-05-08 14:34:23
...

Stark 组件:快速开发神器 —— 自动生成 URL
说道 Stark 你是不是不会想到他——Tony Stark,超级英雄钢铁侠,这也是我的偶像。

不过我们今天要开发的 Stark 组件,倒是跟他的人工智能助手 JARVIS 有些类似,是帮助我们快速开发数据库增、删、改、查操作、应用各种功能的开发助手。

Stark 组件:快速开发神器 —— 自动生成 URL

自动生成 URL

对于数据库表来说,增删改查是必不可少的操作,如果针对每一张表的增删改查都要编写一个 URL,那工作量是非常巨大的,而且还有很多重复性的代码。

Stark 组件第一个要实现的功能就是自动生成项目中所有表的增删改查 URL。

1.预执行

在 Stark 应用下的 apps.py 模块中的 StarkConfig 类里编写 ready 方法:

from django.apps import AppConfig
from django.utils.module_loading import autodiscover_modules


class StarkConfig(AppConfig):
    name = 'Stark'
    
    def ready(self):
        autodiscover_modules("stark")

这其实是重写了父类 AppConfig 的 ready 方法,我们来看一下父类的 ready 方法:

    def ready(self):
        """
        Override this method in subclasses to run code when Django starts.
        """

父类的 ready 方法中就一句注释:在Django启动时在子类中重写此方法以运行代码。

也就是说在 Django 刚启动的时候会执行子类重写的 ready 方法,而我们在 StarkConfig 的 ready 方法中执行了 autodiscover_modules('stark'),再来看一下它的源码:

def autodiscover_modules(*args, **kwargs):
    """
    Auto-discover INSTALLED_APPS modules and fail silently when not present. 
    自动发现已安装的应用程序模块,在不存在时自动失败。
    This forces an import on them to register any admin bits they may want.
    这将强制对它们进行导入,以注册它们可能需要的任何管理位。

    You may provide a register_to keyword parameter as a way to access a registry. 
    您可以提供register_to keyword参数作为访问注册表的方法。
    This register_to object must have a _registry instance variable to access it.
    此register_to对象必须有一个注册表实例变量才能访问它。
    """
    from django.apps import apps

    register_to = kwargs.get('register_to')
    for app_config in apps.get_app_configs():
        for module_to_search in args:
            # Attempt to import the app's module.
            try:
                if register_to:
                    before_import_registry = copy.copy(register_to._registry)

                import_module('%s.%s' % (app_config.name, module_to_search))
            except Exception:
                # Reset the registry to the state before the last import
                # as this import will have to reoccur on the next request and
                # this could raise NotRegistered and AlreadyRegistered
                # exceptions (see #8245).
                if register_to:
                    register_to._registry = before_import_registry

                # Decide whether to bubble up this error. If the app just
                # doesn't have the module in question, we can ignore the error
                # attempting to import it, otherwise we want it to bubble up.
                if module_has_submodule(app_config.module, module_to_search):
                    raise

大致的意思就是 autodiscover_modules('stark')这行代码会去找到一个模块并把它导入。

也就是说,我们编写的 ready 函数会在 Django刚启动时寻找项目中所有已注册的 APP 中寻找 stark.py 模块并自动导入。

    def ready(self):
        autodiscover_modules("stark")

2.自定义路由分发

我们的第一个目标是让 Stark 帮我们自动生成数据库表的增删改查 URL,如果我们要自己写的话,就是通过 include 方法做路由分发,而让 Stark 帮我们做的话,其实就是模拟这个过程。

我们首先来看一下 include 的源码:

def include(arg, namespace=None):
    app_name = None
    if isinstance(arg, tuple):
        # Callable returning a namespace hint.
        try:
            urlconf_module, app_name = arg
        except ValueError:
            if namespace:
                raise ImproperlyConfigured(
                    'Cannot override the namespace for a dynamic module that '
                    'provides a namespace.'
                )
            raise ImproperlyConfigured(
                'Passing a %d-tuple to include() is not supported. Pass a '
                '2-tuple containing the list of patterns and app_name, and '
                'provide the namespace argument to include() instead.' % len(arg)
            )
    else:
        # No namespace hint - use manually provided namespace.
        urlconf_module = arg

    if isinstance(urlconf_module, str):
        urlconf_module = import_module(urlconf_module)
    patterns = getattr(urlconf_module, 'urlpatterns', urlconf_module)
    app_name = getattr(urlconf_module, 'app_name', app_name)
    if namespace and not app_name:
        raise ImproperlyConfigured(
            'Specifying a namespace in include() without providing an app_name '
            'is not supported. Set the app_name attribute in the included '
            'module, or pass a 2-tuple containing the list of patterns and '
            'app_name instead.',
        )
    namespace = namespace or app_name
    # Make sure the patterns can be iterated through (without this, some
    # testcases will break).
    if isinstance(patterns, (list, tuple)):
        for url_pattern in patterns:
            pattern = getattr(url_pattern, 'pattern', None)
            if isinstance(pattern, LocalePrefixPattern):
                raise ImproperlyConfigured(
                    'Using i18n_patterns in an included URLconf is not allowed.'
                )
    return (urlconf_module, app_name, namespace)

最重要的一点是它的返回值return (urlconf_module, app_name, namespace),这是一个含有三个元素的元组,第一个参数是一个 APP 的 urlconf 对象,也就是 APP 下的 urls.py 文件对象,第二个参数是 APP 的名字,第三个参数是 namespace 是名称空间。

也就是说,include 方法其实就是帮我们去某个 APP 下的 urls.py 模块中导入其中编写的 urlpatterns ,那我们完全可以自己编写一个类似 include 方法的函数,也返回三个元素的元组,只不过在返回之前让 Stark 自动生成 URL 放到要返回的 urlconf_module 中。

在 Stark 中定义一个 StarkSite,编写一个 urls 方法返回三元组,Python 内置的 @property 装饰器可以把一个方法变成属性,然后实例化一个 starkSite 对象。

# Stark/main.py
class StarkSite:
	def __init__(self):
		self.app_name = "stark"
		self.namespace = "stark"
		self._registry = []

	@property
	def urls(self):
		return self.getUrls(), self.app_name, self.namespace

	def getUrls(self):
		pass


starkSite = StarkSite()

然后在项目文件夹下的 urls.py 中做引入我们的 starkSite:

from django.contrib import admin
from django.urls import path

from Stark.main import starkSite

urlpatterns = [
    path('admin/', admin.site.urls),
    path('stark/', starkSite.urls),
]

3.单例模式

单例模式,属于创建类型的一种常用的软件设计模式。

通过单例模式的方法创建的类在当前进程中只有一个实例。

而 Python 的模块导入具有一个特性:如果已经导入过的文件再次被重新导入时,python 不会再重新执行一遍,而是选择从内存中直接将原来导入的值拿来用。

我们在业务 APP,也就是本项目的 index APP 下创建一个 stark.py 文件,这样在 Django 刚启动时就会通过 Stark 应用下的 apps.py 定义的 ready 方法找到 index APP 下的 stark.py 文件并执行。

我们要在 stark.py 模块中做两件事:

  1. 注册要自动生成 URL 的数据库表;
  2. 注册该表的自定义处理类。

对于一些比较普遍的数据库表,可以先定义一个基类处理通用操作:

# Stark/main.py
class StarkHandler(object):
	def __init__(self, site, modelClass, prefix):
		self.site = site
		self.request = None
		self.prefix = prefix
		self.modelClass = modelClass


class StarkSite(object):
	def __init__(self):
		self.app_name = "stark"
		self.namespace = "stark"
		self._registry = []

	@property
	def urls(self):
		return self.getUrls(), self.app_name, self.namespace

	def register(self, modelClass, handlerClass=None, prefix=None):
		"""
		:param modelClass: models 中数据库表对应的类
		:param handlerClass: 处理请求的视图函数所在的类
		:param prefix: 生成 URL 的前缀
		"""
		self._registry.append({
			"modelClass": modelClass,
			"handlerClass": handlerClass(self, modelClass, prefix) if handlerClass else StarkHandler(self, modelClass, prefix),
			"prefix": prefix
		})

	def getUrls(self):
		pass


starkSite = StarkSite()

而在 stark.py 模块中注册所有的数据库表和相应的处理类:

# index/stark.py
from index import models
from Stark.main import starkSite

from index.views.user import UserHandler
from index.views.course import CourseHandler
from index.views.classes import ClassesHandler
from index.views.student import StudentHandler
from index.views.project import ProjectHandler
from index.views.publicProject import PublicProjectHandler
from index.views.privateProject import PrivateProjectHandler
from index.views.projectRecord import ProjectRecordHandler
from index.views.paymentRecord import PaymentRecordHandler

starkSite.register(models.User, UserHandler)
starkSite.register(models.Course, CourseHandler)
starkSite.register(models.Classes, ClassesHandler)
starkSite.register(models.Student, StudentHandler)
starkSite.register(models.Project, ProjectHandler)
starkSite.register(models.Project, PublicProjectHandler)
starkSite.register(models.Project, PrivateProjectHandler)
starkSite.register(models.ProjectRecord, ProjectRecordHandler)
starkSite.register(models.PaymentRecord, PaymentRecordHandler)

对于 RBAC 模块也做相应的处理:

# RBAC /stark.py
from RBAC import models
from Stark.main import starkSite

from RBAC.views.rbacUserinfo import RbacUserHandler
from RBAC.views.team import TeamHandler
from RBAC.views.department import DepartmentHandler
from RBAC.views.role import RoleHandler
from RBAC.views.permission import PermissionHandler
from RBAC.views.menu import MenuHandler
from RBAC.views.scoreRecord import ScoreRecordHandler
from RBAC.views.attendance import AttendanceHandler

starkSite.register(models.RbacUserInfo, RbacUserHandler)
starkSite.register(models.Team, TeamHandler)
starkSite.register(models.Department, DepartmentHandler)
starkSite.register(models.Role, RoleHandler)
starkSite.register(models.Permission, PermissionHandler)
starkSite.register(models.Menu, MenuHandler)
starkSite.register(models.ScoreRecord, ScoreRecordHandler)
starkSite.register(models.Attendance, AttendanceHandler)

当然,对于每一个处理类都需要编写视图函数,暂时不编写具体的内容,直接让它继承 StarkHandler,这里提供一个简单的模板:

# RBAC/views/rbacUserinfo.py
from Stark.main import StarkHandler


class RbacUserHandler(StarkHandler):
	pass

4.批量生成 URL

这时候,就可以针对每一张注册的数据表批量生成 URL 了,现在可以编写 StarkSite 类的 getUrls 方法了:

# Stark/main.py
class StarkSite(object):
	def __init__(self):
		self.app_name = "stark"
		self.namespace = "stark"
		self._registry = []

	@property
	def urls(self):
		return self.getUrls(), self.app_name, self.namespace

	def register(self, modelClass, handlerClass=StarkHandler, prefix=None):
		"""
		:param modelClass: models 中数据库表对应的类
		:param handlerClass: 处理请求的视图函数所在的类
		:param prefix: 生成 URL 的前缀
		"""
		self._registry.append({
			"modelClass": modelClass,
			"handlerClass": handlerClass(self, modelClass, prefix),
			"prefix": prefix
		})

	def getUrls(self):
		urlconf_module = []
		for item in self._registry:
			model, handler, prefix = item["modelClass"], item["handlerClass"], item["prefix"]
			app_label, model_name = model._meta.app_label, model._meta.model_name
			url = "%s/%s/%s/" % (app_label, model_name, prefix) if prefix else "%s/%s/" % (app_label, model_name)
			urlconf_module.append(re_path(url, (handler.getUrls(), None, None)))
		return urlconf_module

StarkSite 类的 getUrls 方法只做第一层路由分发,也就是以 APP名/数据库表名/前缀 为 URL 的开头,然后再做一层路由分发,通过每一个数据库表的处理类定义的 getUrls 方法获得增删改查或其它的 URL。

另外,我们要在 StarkHandler 类中编写 getUrls、处理请求的视图等:

# Stark/main.py
class StarkHandler(object):
	def __init__(self, site, modelClass, prefix):
		self.request = None
		self.site, self.model, self.prefix = site, modelClass, prefix
		self.app_label, self.model_name = self.model._meta.app_label, self.model._meta.model_name

	def getUrls(self):
		"""数据库表基础增删改查 Url"""
		patterns = [
			re_path(r'^add/$', self.wrapper(self.addView), name=self.addUrlName),
			re_path(r'^delete/(?P<pk>\d+)/$', self.wrapper(self.deleteView), name=self.deleteUrlName),
			re_path(r'^change/(?P<pk>\d+)/$', self.wrapper(self.changeView), name=self.changeUrlName),
			re_path(r'^check/$', self.wrapper(self.checkView), name=self.checkUrlName),
		]
		patterns.extend(self.extraUrls())
		return patterns

	def wrapper(self, function):
		@functools.wraps(function)
		def inner(request, *args, **kwargs):
			self.request = request
			return function(request, *args, **kwargs)
		return inner

	def extraUrls(self):
		"""用于子代扩展 Url"""
		return []

	def addView(self, request, *args, **kwargs):
		"""添加功能视图函数"""
		pass

	def deleteView(self, request, *args, **kwargs):
		"""删除功能视图函数"""
		pass

	def changeView(self, request, *args, **kwargs):
		"""修改功能视图函数"""
		pass

	def checkView(self, request, *args, **kwargs):
		"""查看功能视图函数"""
		pass

	def urlName(self, param):
		return "%s_%s_%s_%s" % (self.app_label, self.model_name, self.prefix, param) \
			if self.prefix else "%s_%s_%s" % (self.app_label, self.model_name, param)

	@property
	def addUrlName(self):
		"""获取增加页面 URL 的别名"""
		return self.urlName("add")

	@property
	def deleteUrlName(self):
		"""获取删除页面 URL 的别名"""
		return self.urlName("delete")

	@property
	def changeUrlName(self):
		"""获取修改页面 URL 的别名"""
		return self.urlName("change")

	@property
	def checkUrlName(self):
		"""获取查看页面 URL 的别名"""
		return self.urlName("check")

到目前为止,我们就做好了 Stark 组件批量生成 URL 的功能,可以运行项目打开看一下:
Stark 组件:快速开发神器 —— 自动生成 URL
针对不同数据库表的增删改查 URL 也生成了:
Stark 组件:快速开发神器 —— 自动生成 URL