用Tetra构建一个全栈式的应用程序教程

233 阅读13分钟

大多数全栈应用程序将前端和后端代码分离成不同的文件;大多数网络框架都是基于这种结构建立的。随着文件和代码行数的增加,可能会增加你的代码库的复杂性,从而使其更加难以调试。通过引入一个名为Tetra的框架,将这些独立文件造成的复杂性降至最低。

本教程将向你介绍Tetra框架及其组件。你还将学习如何使用Tetra构建一个简单的全栈博客应用,执行CRUD功能。

我们将涵盖以下内容。

什么是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库中。

titlecontent 变量是该组件的公共属性,每个变量的初始值都是空字符串。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.jsx-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 ,在浏览器上查看该页面。

下面是页面的输出:

Add blog post page with title box and content box

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 组件的样子:

PostItem component shows post with default text

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 查看该页面:

Tetra blog

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')
]

通过点击博客主页上的帖子标题,在浏览器中查看帖子的详细信息:

Post details with delete and update buttons

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,并从数据库中获取它。然后,它把它的标题和内容分别分配给titlecontent 公共属性。

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.pyurls.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 页面:

Update blog post screen with title and content boxes

Blog post title now shows "-updated"

关于Tetra的生产准备的说明

在写这篇文章的时候,Tetra仍处于早期开发阶段,目前支持Python 3.9及以上版本。然而,Tetra团队正在努力将这个框架的功能扩展到Python的旧版本。

在使用Tetra开始生产之前,你应该知道一件事,那就是框架的文档需要大量的改进。它太简洁了,因为有些依赖关系要么根本没有解释,要么不够详细。例如,文档中没有讨论如何处理图片,这就是为什么我们为这个演示建立了一个博客应用。

直到我完成这个项目后,我才意识到这个框架并不像文档中介绍的那样复杂。

总结

这篇文章向你介绍了Tetra和它的组件。你通过建立一个简单的博客应用来执行CRUD操作,了解了Tetra的功能,以及如何从一个文件中执行全栈操作。