大多数全栈应用程序将前端和后端代码分离成不同的文件;大多数网络框架都是基于这种结构建立的。随着文件和代码行数的增加,可能会增加你的代码库的复杂性,从而使其更加难以调试。通过引入一个名为Tetra的框架,将这些独立文件造成的复杂性降至最低。
本教程将向你介绍Tetra框架及其组件。你还将学习如何使用Tetra构建一个简单的全栈博客应用,执行CRUD功能。
我们将涵盖以下内容。
- 什么是Tetra?
- 让我们建立一个Tetra博客应用程序
- 项目设置
- Tetra的安装和配置
- 博客文章模型
AddPost组件PostItem组件ViewPosts组件- The
PostDetailcomponent UpdatePost组件- 关于Tetra的生产准备的说明
什么是Tetra?
Tetra是一个全栈框架,在服务器端使用Django,在前端使用Alpine.js来执行前端逻辑。Tetra允许你将前端和后端逻辑放在一个统一的位置,并减少你的应用程序中的代码复杂性。它使用一个被称为 "组件 "的类将后端实现与前端连接起来。
Tetra组件
Tetra组件是一个代码单元,它将其Python、HTML、CSS和JavaScript逻辑作为一个实体处理在一个Python文件中。如果你熟悉React框架,你可以将其组件的行为比作Tetra组件,只不过React组件只执行前端的功能。
组件之间既可以相互依赖,也可以相互独立。这意味着你可以从另一个组件中调用一个组件,或者让它作为一个独立的组件。你可以在这里阅读更多关于tetra组件的信息。
让我们建立一个Tetra博客应用程序
本教程的其余部分将指导你如何在你的Django应用程序中安装Tetra,以及如何使用Tetra建立一个博客应用程序的逐步流程。这个博客应用将从管理的角度呈现,在这里你可以创建一个新的帖子,更新一个现有的帖子,删除一个帖子,并查看所有的博客帖子。
该应用程序将不包括任何认证或授权层。我们的目的是在专注于Tetra的核心功能的同时,尽可能地保持它的简单。
前提条件
- 熟练掌握使用Django构建单体应用的能力
- 具有HTML、CSS和JavaScript的工作知识
- 任何合适的IDE或文本编辑器
- 在你的机器上安装了3.9或更高版本的Python
- 在你的机器上安装npm软件包管理器
项目设置
第一步是为该应用程序创建一个虚拟环境。在你的终端上运行以下命令来设置你的项目目录和虚拟环境:
mkdir tetra
cd tetra
python -m venv tetra
cd tetra
Scripts/activate
下一步是安装Django。因为Tetra是在Django框架上运行的,所以需要在你的应用程序中整合Django:
pip install django
django-admin startproject tetra_blog
cd tetra_blog
接下来,创建博客应用:
python manage.py startapp blog
将博客应用添加到settings.py 文件中的INSTALLED_APPS 列表中,如下图所示。
INSTALLED_APPS = [
'blog.apps.BlogConfig',
...
]
在app目录下,创建一个components.py 文件,该文件将包含你将在项目中构建的所有组件。
Tetra的安装和配置
在成功设置了Django项目之后,下一步就是在你的应用程序中安装Tetra框架。
pip install tetraframework
在settings.py 文件中,将tetra 加入到INSTALLED_APPS 列表中,如下图所示。
INSTALLED_APPS = [
...
'tetra',
'django.contrib.staticfiles',
...
]
确保tetra 被列在django.contrib.staticfiles 元素之前。
接下来,你要在MIDDLEWARE 列表的最后加入tetra.middleware.TetraMiddleware 。这将把你的组件的JavaScript和CSS添加到HTML模板中。
MIDDLEWARE = [
...
'tetra.middleware.TetraMiddleware'
]
将下面的修改应用于根urls.py 文件,通过你的公共方法暴露Tetra的端点。
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
...
path('tetra/', include('tetra.urls')),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
安装esbuild
Tetra使用esbuild构建你的JS/CSS组件并将其打包。这样你就可以追踪前端实现中可能出现的任何错误到你的Python源文件:
npm init
npm install esbuild
如果你使用的是Windows操作系统,你就必须在你的settings.py 文件中明确声明esbuild 的构建路径:
TETRA_ESBUILD_PATH = '<absolute-path-to-project-root-directory>/node_modules/.bin/esbuild.cmd'
博客文章模型
该应用程序将对一篇博客文章执行CRUD功能。Post 模型将由三个属性组成:标题、内容和日期。
在models.py 文件中添加以下代码来设置Post 模型:
from django.db import models
from django.utils import timezone
from django.urls import reverse
class Post(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
date_posted = models.DateTimeField(default=timezone.now)
def __str__(self):
return self.title
// generate a reverse url for the model
def get_absolute_url(self):
return reverse('post-detail', kwargs={'pk': self.id})
执行迁移命令,为该模型创建一个表:
python manage.py makemigrations
python manage.py migrate
AddPost 组件
这个组件负责渲染用户界面以创建一个新的帖子。它还将包含我们需要的Python逻辑,以创建一个Post 模型并在数据库中持久化数据。
在components 文件夹中创建add_post.py 文件,并在该文件中添加以下代码:
from sourcetypes import javascript, css, django_html
from tetra import Component, public, Library
from ..models import Post
default = Library()
@default.register
class AddPost(Component):
title=public("")
content=public("")
def load(self):
self.post = Post.objects.filter(id=0)
@public
def add_post(self, title, content):
post = Post(
title = title,
content = content
)
post.save()
在上面的代码中,AddPost 类是 Tetra 组件类的一个子类,它是你建立自定义组件的基础类。使用@default.register 装饰器,你将你的AddPost 组件注册到Tetra库中。
title 和content 变量是该组件的公共属性,每个变量的初始值都是空字符串。public attributes 的值对模板、JavaScript和服务器逻辑都是可用的。
load 方法在组件启动时和从保存状态恢复时运行。你可以把load 方法看作是组件的构造函数;它在你从模板中调用组件时运行。
add_post 方法是一个公共方法,它接收标题和内容作为参数来创建一个Post 实例,然后将其保存到数据库中。就像公共属性一样,公共方法也暴露在模板、JavaScript 和 Python 中。你通过在方法签名上方添加@public 装饰器来声明一个方法为公共的。
下面是你应该包含在add_post.py 文件中的HTML代码,作为AddPost 组件的一部分。
template: django_html = """
<div class="container">
<h2>Add blog post</h2>
<label> Title
<em>*</em>
</label>
<input type="text" maxlength="255" x-model="title" name="title" placeholder="Input post title" required/>
<label> Content
<em>*</em>
</label>
<textarea rows="20" cols="80" x-model="content" name="content" placeholder="Input blog content" required /> </textarea>
<button type="submit" @click="addPost(title, content)"><em>Submit</em></button>
</div>
"""
输入字段接收文章标题,并通过Alpine.js的x-model属性将其绑定到title public属性上。同样地,textarea 接收博文的内容,并将其值绑定到组件的content 公共属性。
使用按钮标签内的Alpine.js@click 指令,模板调用了JavaScriptaddPost 方法:
script: javascript = """
export default {
addPost(title, content){
this.add_post(title, content)
}
}
"""
JavaScriptaddPost 方法将从标题和内容获得的值作为参数传递给组件的add_post 公共方法。你也可以直接从上面的HTML模板中调用add_post 公共方法。
在这里通过JavaScript函数传递的目的是为了演示你如何在你的Tetra组件中执行JavaScript操作。这对于你想对用户的行为有更多控制的情况是有帮助的,比如在用户点击一个按钮后可能会禁用它,以防止他们在处理之前的请求时发送多个请求。
这里是为模板设计样式的CSS代码:
style: css = """
.container {
display: flex;
flex-direction: column;
align-items: left;
justify-content: center;
border-style: solid;
width: fit-content;
margin: auto;
margin-top: 50px;
width: 50%;
border-radius: 15px;
padding: 30px;
}
input, textarea, label{
margin-bottom: 30px;
margin-left: 20px;
;
}
label {
font-weight: bold;
}
input{
height: 40px;
}
h2 {
text-align: center;
}
button {
width: 150px;
padding: 10px;
border-radius: 9px;
border-style: none;
background: green;
color: white;
margin: auto;
}
"""
下一步是在Django视图模板中调用AddPost 组件。在你在本教程上一节中创建的博客应用templates 文件夹中创建一个add_post.html 。在该文件中添加以下代码:
{% load tetra %}
<!Doctype html>
<html lang="en">
<head>
<title> Add post </title>
{% tetra_styles %}
{% tetra_scripts include_alpine=True %}
</head>
<body>
<form enctype="multipart/form-data" method="POST">
{% csrf_token %}
{% @ add_post / %}
</form>
</body>
</html>
这个模板首先将Tetra模板标签加载到模板中。它是通过代码顶部描述的{% load tetra %} 命令实现的。你还需要通过{% tetra_styles %} 和{% tetra_scripts} ,分别向模板注入CSS和JavaScript。
默认情况下,Tetra不会在你的模板中包含Alpine.js。你必须在注入组件的JavaScript时,通过添加include_alpine=True ,明确地声明它。
form 标签内的{% @ add_post / %} ,会调用AddPost 组件的load 方法,并渲染你在创建该组件时声明的HTML内容。
注意,用于加载组件的组件名称是蛇形大小写的。这是从模板中调用组件的默认配置。你也可以在创建组件时设置一个自定义名称,如下图所示:
...
@default.register(name="custom_component_name")
class AddPost(Component):
...
然后你可以使用{% @ custom_component_name / %} 来加载组件。
接下来,将下面的片段添加到views.py 文件中。
from django.shortcuts import render
def add_post(request):
return render(request, 'add_post.html')
在博客应用目录下创建一个urls.py 文件,并在该文件中添加以下代码段:
from django.urls import path
from . import views
urlpatterns = [
path("add", views.add_post, name='add-post'),
]
在根urls.py 文件中,添加下面的路径:
urlpatterns = [
...
path('tetra/', include('tetra.urls')),
path('post/', include('blog.urls'))
]
用python manage.py runserver command 运行应用程序。通过localhost:8000/post/add ,在浏览器上查看该页面。
下面是页面的输出:
PostItem 组件
PostItem 组件包含在主屏幕上渲染创建的帖子的模板。
@default.register
class PostItem(Component):
def load(self, post):
self.post = post
load 方法接收Post 实例作为其参数,并将其暴露给HTML模板,在屏幕上渲染其标题和内容。
template: django_html = """
<article class="post-container" {% ... attrs %}>
<small class="article-metadata">{{ post.date_posted.date}}</small>
<p class="article-title"> {{ post.title }}</p>
<p class="article-content">{{ post.content }}</p>
</article>
"""
{% ... attrs %} 标签是一个Tetra属性标签,模板用它来接收调用组件时传给它的参数。当使用属性标签接收参数时,你应该在HTML模板的根节点中声明该标签,就像上面的片段中的文章标签那样。
这里是模板的CSS实现:
style: css = """
.article-metadata {
padding-bottom: 1px;
margin-bottom: 4px;
border-bottom: 1px solid #e3e3e3;
}
.article-title{
font-size: x-large;
font-weight: 700;
}
.article-content {
white-space: pre-line;
}
.post-container{
margin: 50px;
}
a.article-title:hover {
color: #428bca;
text-decoration: none;
}
.article-content {
white-space: pre-line;
}
a.nav-item{
text-align: right;
margin-right: 100px;
}
h1 {
text-align: center;
}
"""
下面是一个帖子通过PostItem 组件的样子:
ViewPosts 组件
这个组件负责渲染所有创建的帖子。在components.py 文件中加入以下片段:
@default.register
class PostView(Component):
def load(self):
self.posts = Post.objects.all()
template: django_html = """
<div>
<h1> Tetra blog </h1>
<div class="navbar-nav">
<a class="nav-item nav-link" href="{% url 'add-post' %}">New Post</a>
<div>
<div class="list-group">
{% for post in posts %}
{% @ post_item post=post key=post.id / %}
{% endfor %}
</div>
</div>
"""
组件中的load 方法从数据库中检索所有创建的帖子。HTML模板包含一个锚标签,指向add-post URL来创建一个新的帖子。
对于每一个从数据库中获取的帖子,HTML通过在for-loop中传递帖子对象作为其参数来创建一个PostItem 组件。
接下来,从Django视图模板中调用ViewPost 组件。在博客应用的templates 文件夹中创建一个home.html 文件,并在该文件中添加以下代码:
{% load tetra %}
<!Doctype html>
<html lang="en">
<head>
<title> Blog home </title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
{% tetra_styles %}
{% tetra_scripts include_alpine=True %}
</head>
<body>
{% @ view_post / %}
</body>
</html>
接下来,在views.py 文件中添加以下内容:
def home(request):
return render(request, 'home.html')
最后,更新博客应用urls.py 文件中的urlpatterns 列表:
urlpatterns = [
path("", views.home, name='home'),
...
]
你可以通过localhost:8000/post 查看该页面:
PostDetail 组件
这个组件将在一个页面上呈现完整的文章。该页面还将包含两个按钮:删除和更新帖子各一个。在components.py 文件中添加以下代码:
@default.register
class PostDetail(Component):
def load(self, pk):
self.post = Post.objects.filter(id=pk)[0]
@public(update=False)
def delete_item(self):
Post.objects.filter(id=self.post.id).delete()
self.client._removeComponent()
load 方法通过pk 变量接收帖子的id ,并获取Post 实例,其ID与数据库中的pk 值一致。
delete_item 方法从数据库中删除Post 实例,自动将其从主屏幕中删除。默认情况下,当你调用一个公共方法时,它会重新渲染一个组件。通过在@public 装饰器中把update 属性设置为False ,可以确保它不会试图重新渲染一个先前删除的帖子。
这里是HTML模板:
template: django_html = """
<article >
<small class="text-muted">{{ post.date_posted.date}}</small>
<h2 class="article-title">{{ post.title }}</h2>
<p class="article-content">{{ post.content }}</p>
<div class="post-buttons">
<button id="delete-button" type="submit" @click="delete_item()"><em>Delete</em></button>
<button id="update-button"> <em>Update</em> </button>
</div>
</article>
"""
该模板检索了从load 方法中获取的帖子的日期、标题和内容,并渲染了这些值。它还包含删除和更新帖子的按钮。删除按钮调用delete_item 方法来执行对帖子的删除操作。我们将在下一节中实现更新按钮。
这是该模板的CSS:
style: css = """
article{
margin: 100px;
}
.post-buttons{
position: absolute;
right: 0;
}
#delete-button, #update-button{
width: 150px;
padding: 10px;
border-radius: 9px;
border-style: none;
font-weight: bold;
margin: auto;
}
#update-button{
background: blue;
color: white;
}
#delete-button{
background: red;
color: white;
}
"""
在上一节创建的PostItem 模板中,更新HTML代码,加入一个anchor 标签,将用户从主屏幕引导到帖子详情页:
@default.register
class PostItem(Component):
...
template: django_html = """
<article class="post-container" >
...
<a href="{% url 'post-detail' pk=post.id %}"> {{ post.title }}</a>
...
</article>
"""
在模板文件夹中,创建一个post-detail.html 文件,作为帖子详情页的根HTML文件,并在该文件中包括以下代码。
接下来,更新views.py 和urls.py 文件,以包括通往帖子详情页的路径。
def post_detail(request, **kwargs):
return render(request, 'post_detail.html', kwargs)
urlpatterns = [
path("<int:pk>/", views.post_detail, name='post-detail')
]
通过点击博客主页上的帖子标题,在浏览器中查看帖子的详细信息:
UpdatePost 组件
这个组件负责更新现有文章的标题和内容:
@default.register
class PostUpdate(Component):
title=public("")
content=public("")
def load(self, pk):
self.post = Post.objects.filter(id=pk)[0]
self.title=self.post.title
self.content=self.post.content
@public
def update_post(self, title, content):
self.post.title = title
self.post.content = content
self.post.save()
load 方法接收你要更新的帖子的ID,并从数据库中获取它。然后,它把它的标题和内容分别分配给title 和content 公共属性。
update_post 方法接收更新的标题和内容,并将其分配给获取的帖子,然后将其保存到数据库。
下面是该组件的HTML模板:
template: django_html = """
<div class="container">
<h2>Update blog post</h2>
<label> Title
<em>*</em>
</label>
<input type="text" maxlength="255" x-model="title" name="title" required/>
<label> Content
<em>*</em>
</label>
<textarea rows="20" cols="80" x-model="content" name="content" required> </textarea>
<button type="submit" @click="update_post(title, content)"><em>Submit</em></button>
</div>
"""
上述模板通过Alpine.jsx-model 属性渲染标题和内容的公共属性值,而按钮使用Alpine.js@click 函数来调用update_post 方法,并将标题和内容的新值作为参数传递。
在上一节创建的PostDetail 模板中,更新HTML代码,加入一个anchor 标签,将用户从主屏幕引导到帖子更新页面:
@default.register
class PostDetail(Component):
...
template: django_html = """
<article {% ... attrs %} >
...
<a class="nav-item nav-link" href="{% url 'update-post' pk=post.id %}"><button id="update-button"> <em>Update</em> </button></a>
...
</article>
"""
接下来,在模板文件夹中,创建一个post_update.html 文件,作为PostUpdate 组件的根HTML模板。在该文件中添加以下片段:
{% load tetra %}
<!Doctype html>
<html>
<head>
<title> Update post </title>
{% tetra_styles %}
{% tetra_scripts include_alpine=True %}
</head>
<body>
<form enctype="multipart/form-data" method="POST">
{% csrf_token %}
{% @ post_update pk=pk / %}
</form>
</body>
</html>
最后,分别用以下代码更新views.py 和urls.py 文件。
def update_post(request, **kwargs):
return render(request, 'post_update.html', kwargs)
urlpatterns = [
...
path("<int:pk>", views.update_post, name='update-post'),
...
]
你可以通过点击帖子详情屏幕上的更新按钮,导航到update-post 页面:
关于Tetra的生产准备的说明
在写这篇文章的时候,Tetra仍处于早期开发阶段,目前支持Python 3.9及以上版本。然而,Tetra团队正在努力将这个框架的功能扩展到Python的旧版本。
在使用Tetra开始生产之前,你应该知道一件事,那就是框架的文档需要大量的改进。它太简洁了,因为有些依赖关系要么根本没有解释,要么不够详细。例如,文档中没有讨论如何处理图片,这就是为什么我们为这个演示建立了一个博客应用。
直到我完成这个项目后,我才意识到这个框架并不像文档中介绍的那样复杂。
总结
这篇文章向你介绍了Tetra和它的组件。你通过建立一个简单的博客应用来执行CRUD操作,了解了Tetra的功能,以及如何从一个文件中执行全栈操作。