整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

CSS Margin中5个经典布局解决方案,重难点知识,记得收藏复习

几天我在面试前端开发同学的时候,有问到关于margin基础布局相关内容的过程中,发现很多同学基本解释不清楚,今天刚好有点时间就整理了一篇笔记出来。就以下5点在CSS布局经常会用到的经典布局解决方案。

  1. css中margin外边距(重叠)合并现象
  2. css中margin外边距穿透现象
  3. css中margin设置负值时的特性
  4. css经典3列自适应布局:圣杯布局
  5. css经典3列自适应布局:双飞翼布局

可以尝试动手试一试,有什么疑问 !可随时交流,有问必答 。

margin 纵向重叠(合并)问题

元素垂直排列时,第一个元素的下外边距与第二个元素的上外边距会发生合并,合并后的间距就是两者中最大的那个值。

1、以下代码中,item1与item4之间的间距是多少?

<style>
    .box{
        margin-top:10px;/*上外边距*/
        margin-bottom:20px;/*下外边距*/
        height: 20px;
        background-color:skyblue;
    }
</style>
<body>
    <div class="box">item1</div>
    <div class="box"></div>
    <div class="box"></div>
    <div class="box">item4</div>
</body>

答案:

解析:item1 与 item4 之间的间距为 3个下外边距大小+2个盒子高度=20*3+20*2=100px

2、以下代码中,item1与item4之间的间距是多少 ?

<style>
    .box{
        margin-top:10px;
        margin-bottom:20px;
        background-color:skyblue;
    }
</style>
<body>
    <div class="box">item1</div>
    <div class="box"></div>
    <div class="box"></div>
    <div class="box">item4</div>
</body>

答案: item1与item4之间间距为 20px

解析:因为中间两个box中没有内容也没有边框线,所以外边距会一直重叠合并,所以最后item1和item4之间距离只有一个下外边距的大小

3、以下代码中 container 、 item 、box与浏览器顶部的间距是多少 ?

margin 穿透问题

当一个元素包含在另一个元素中时,如果父元素没有设置内边距或边框把外边距分隔开,它们的上或下外边距也会发生合并。

<style>
    body{
        margin:0;
        padding:0;
    }
    .container{
        width:300px;
        height: 300px;
        background-color: salmon;   
        margin-top:100px;/*与浏览器顶部的距离*/
        border:5px solid blue;
    }
    .container .item{
        width:200px;
        height: 200px;
        background-color: skyblue;
        margin-top:50px;/*因为container中加了border边框,所以这里的外边距不会穿透合并*/
    }
    .container .item .box{
        width:100px;
        height: 100px;
        background-color: bisque;
        margin-top:10px;/*item没有加边框线,内边距和其它内容,所以外边距会发生穿透合并*/
        border:5px solid red;
    }
</style>
<body>
    <div class="container">
        <div class="item">
            <div class="box"></div>
        </div>
    </div>
</body>



答案: 100px 、155px、155px

解析:

.container与浏览器顶部距离是100px,

.item与浏览器顶部距离100px + 5px+50px=155px

.box与浏览器顶部距离:100px+5px+50px=155px

margin负值问题

margin-left 设置负值,元素向左移动

margin-right 设置负值,自身不受影响,右边元素向左移动

margin-top设置负值,元素向上移动

margin-bottom 设置负值,自身不受影响,下方元素向上移动

1、 两元素水平排列,左右外边距设置负值时

<style>
    body{
        margin:0;
    }
    .container{
        width:500px;
        height:200px;
        padding:20px 0px;
        border:5px solid #ddd;
        margin:0px auto;
    }
    .container .common{
        width:200px;
        height: 200px;
        float: left;
    }
    .container .box1{
        background-color: skyblue;
        /* margin-left:-100px; 元素自身向左移动,右边的元素也会受影响*/
        margin-right:-100px;/*元素自身不受影响,右边元素向左移动*/
    }
    .container .box2{
        background-color: tomato;
    }
</style>
<body>
    <div class="container">
        <div class="box1 common"></div>
        <div class="box2 common"></div>
    </div>
</body>



当.container .box1中margin-left:-100px;时,如:图1

.container .box1margin-right:-100px;时,如:图2

.container .box1设置margin-left:-100px;margin-right:-100px时,如:图3

2、两元素垂直排列,上下外边距设置负值时

<style>
    body{
        margin:0;
    }
    .container{
        height: 500px;
        width: 200px;
        padding:0px 20px;
        border:5px solid #ddd;
        margin-top:100px;
    }
    .container .common{
        width:200px;
        height: 200px;
    }
    .container .box1{
        background-color: skyblue;
        /*margin-top:-100px;元素向上移动,下方元素也会受影响*/
        margin-bottom:-100px;/*自身不受影响,下方元素向上移动*/
    }
    .container .box2{
        background-color: rgba(0,0,255,0.5);
    }
</style>

<body>
    <div class="container">
        <div class="box1 common"></div>
        <div class="box2 common"></div>
    </div>
</body>



当.container .box1中margin-top:-100px时,如:图 1

当.container .box1中margin-bottom:-100px时,如:图 2

当.container .box1中同时设置margin-top:-100px; 和margin-bottom:-100px;时,如:图 3

3、经典布局:圣杯布局

这种布局的优点

中间一栏内容最重要,最先加载和渲染,同时对搜索引擎优化最利。

两边内容固定,中间内容自适应



<style>
    body{
        margin:0;
        /*核心代码*/
        min-width: 650px;/*当页面宽度不够时,出现滚动条而不会造成布局错乱*/
    }
    .clearfix::after{
        display: block;
        content: "";
        clear: both;
    }
    .fl{/*核心代码*/
        float:left;/*三个盒子一定要添加浮动*/
    }
    .header{
        height: 100px;
        background-color: tomato;
    }

    .container{
        padding-left:200px;/*左边预留200px位置  用来放left*/
        padding-right:250px;/*右边预留200px位置  用来放right*/
    }
    .container .center{
        width:100%;/*自适应container的宽度,实现自适应缩放*/
        height: 500px;
        background-color: skyblue;
    }
    .container .left{
        width:200px;
        height: 500px;
        background-color:cadetblue;
        /*核心代码*/
        margin-left:-100%;/*盒子向左移,因为加了浮动,所以会移动到上一行的最左边*/
        position: relative;/*利用相对定位,再把盒子往左移200px就占据了最左边预留的200px空间*/
        left:-200px;
    }
    .container .right{
        width:250px;
        height: 500px;
        background-color:aquamarine;
        /*核心代码*/
        margin-right:-250px;/*加上这个代码,相当于right没有一点宽度,就会移动到上的最右边位置*/
    }
    .footer{
        height: 100px;
        background-color: #000;
    }
</style>
<body>
    <div class="header">头部</div>
    <div class="container clearfix">
        <div class="center fl">中间</div>
        <div class="left fl">左边</div>
        <div class="right fl">右边</div>
    </div>
    <div class="footer">底部</div>
</body>

4、经典布局:双飞翼布局

这种布局的优点

中间一栏内容最重要,最先加载和渲染,同时对搜索引擎优化最利。

两边内容固定,中间内容自适应



<style>
    body{
        margin:0;
    }
    .fl{/*核心代码*/
        float: left;/*一定要添加浮动*/
    }
    .main{
        background-color: #ddd;
        width:100%;
    }
    .main .main-content{
        background-color: skyblue;
        height: 300px;
        /*核心代码*/
        margin:0 200px 0 200px;/*盒子左右两边余留200px,分别给left和right来占用*/
    }
    .left{
        width: 200px;
        height: 300px;
        background-color: coral;
        /*核心代码*/
        margin-left:-100%;/*往左移动浏览器的宽度,最后移动到上一行的最左边*/
    }
    .right{
        width: 200px;
        height: 300px;
        background-color: tomato;
        /*核心代码*/
        margin-left:-200px;/*相当于自身宽度为0了,因为加了浮动,然后就显示在了上一行的最右边*/
    }
</style>
<body>
    <div class="main fl">
        <div class="main-content">中间</div>
    </div>
    <div class="left fl">左边</div>
    <div class="right fl">右边</div>
</body>

为帮助到一部分同学不走弯路,真正达到一线互联网大厂前端项目研发要求,首次实力宠粉,打造了《30天挑战学习计划》,内容如下:

HTML/HTML5,CSS/CSS3,JavaScript,真实企业项目开发,云服务器部署上线,从入门到精通

  • PC端项目开发(1个)
  • 移动WebApp开发(2个)
  • 多端响应式开发(1个)

共4大完整的项目开发 !一行一行代码带领实践开发,实际企业开发怎么做我们就是怎么做。从学习一开始就进入工作状态,省得浪费时间。

从学习一开始就同步使用 Git 进行项目代码的版本的管理,Markdown 记录学习笔记,包括真实大厂项目的开发标准和设计规范,命名规范,项目代码规范,SEO优化规范

从蓝湖UI设计稿 到 PC端,移动端,多端响应式开发项目开发

  • 真机调试,云服务部署上线;
  • Linux环境下 的 Nginx 部署,Nginx 性能优化;
  • Gzip 压缩,HTTPS 加密协议,域名服务器备案,解析;
  • 企业项目域名跳转的终极解决方案,多网站、多系统部署;
  • 使用 使用 Git 在线项目部署;

这些内容在《30天挑战学习计划》中每一个细节都有讲到,包含视频+图文教程+项目资料素材等。只为实力宠粉,真正一次掌握企业项目开发必备技能,不走弯路 !

过程中【不涉及】任何费用和利益,非诚勿扰 。

如果你没有添加助理老师微信,可以添加下方微信,说明要参加30天挑战学习计划,来自头条号!老师会邀请你进入学习,并给你发放相关资料

30 天挑战学习计划 Web 前端从入门到实战 | arry老师的博客-艾编程

读:如果你的代码是用 Python 编写的,你应该使用 Textual 来帮助你编写 TUI(文本用户界面)。

快速入门使用 Textual

Python 在 Linux 上有像 TkInterdocs.python.org 这样的优秀 GUI(图形用户界面)开发库,但如果你不能运行图形应用程序怎么办?

文本终端,并非只在 Linux 上有,而且 BSD 和其它的出色的类 Unix 操作系统上也有。如果你的代码是用 Python 编写的,你应该使用 Textualtextual.textualize.io 来帮助你编写 TUI(文本用户界面)。在这个快速介绍中,我将向你展示两个你可以用 Textual 做的示例,并且介绍它未来可能的应用方向。

所以 Textual 是什么?

Textual 是一个为 Python 构建的快速应用程序开发框架,由 Textualize.ioTextualize.io 构建。它可以让你用简单的 Python API 构建复杂的用户界面,并运行在终端或网络浏览器上!

你需要的跟进这个教程的工具

你需要有以下条件:

1. 具备基础的编程经验,最好熟练使用 Python。

2. 理解基础的面向对象概念,比如类和继承。

3. 一台安装了 Linux 与 Python 3.9+ 的机器

4. 一款好的编辑器(Vim 或者 PyCharm 是不错的选择)

我尽可能简单化代码,以便你能轻松理解。此外,我强烈建议你下载代码,或至少按照接下来的说明安装相关程序。

安装步骤

首先创建一个虚拟环境:

python3 -m venv ~/virtualenv/Textualize

现在,你可以克隆 Git 仓库并创建一个可以编辑的发布版本:

. ~/virtualenv/Textualize/bin/activate
pip install --upgrade pip
pip install --upgrade wheel
pip install --upgrade build
pip install --editable .

或者直接从 Pypi.orgPypi.org 安装:

. ~/virtualenv/Textualize/bin/activate
pip install --upgrade KodegeekTextualize

我们的首个程序:日志浏览器

这个 日志浏览器 就是一款简单的应用,能执行用户 PATHmanpages.org 路径上的一系列 UNIX 命令,并在任务执行完毕后捕获输出。

以下是该应用的代码:

import shutil
from textual import on
from textual.app import ComposeResult, App
from textual.widgets import Footer, Header, Button, SelectionList
from textual.widgets.selection_list import Selection
from textual.screen import ModalScreen
# Operating system commands are hardcoded
OS_COMMANDS = {
    "LSHW": ["lshw", "-json", "-sanitize", "-notime", "-quiet"],
    "LSCPU": ["lscpu", "--all", "--extended", "--json"],
    "LSMEM": ["lsmem", "--json", "--all", "--output-all"],
    "NUMASTAT": ["numastat", "-z"]
}
class LogScreen(ModalScreen):
    # ... Code of the full separate screen omitted, will be explained next
    def __init__(self, name = None, ident = None, classes = None, selections = None):
        super().__init__(name, ident, classes)
        pass
class OsApp(App):
    BINDINGS = [
        ("q", "quit_app", "Quit"),
    ]
    CSS_PATH = "os_app.tcss"
    ENABLE_COMMAND_PALETTE = False  # Do not need the command palette
    def action_quit_app(self):
        self.exit(0)
    def compose(self) -> ComposeResult:
        # Create a list of commands, valid commands are assumed to be on the PATH variable.
        selections = [Selection(name.title(), ' '.join(cmd), True) for name, cmd in OS_COMMANDS.items() if shutil.which(cmd[0].strip())]
        yield Header(show_clock=False)
        sel_list = SelectionList(*selections, id='cmds')
        sel_list.tooltip = "Select one more more command to execute"
        yield sel_list
        yield Button(f"Execute {len(selections)} commands", id="exec", variant="primary")
        yield Footer()
    @on(SelectionList.SelectedChanged)
    def on_selection(self, event: SelectionList.SelectedChanged) -> None:
        button = self.query_one("#exec", Button)
        selections = len(event.selection_list.selected)
        if selections:
            button.disabled = False
        else:
            button.disabled = True
        button.label = f"Execute {selections} commands"
    @on(Button.Pressed)
    def on_button_click(self):
        selection_list = self.query_one('#cmds', SelectionList)
        selections = selection_list.selected
        log_screen = LogScreen(selections=selections)
        self.push_screen(log_screen)
def main():
    app = OsApp()
    app.title = f"Output of multiple well known UNIX commands".title()
    app.sub_title = f"{len(OS_COMMANDS)} commands available"
    app.run()
if __name__ == "__main__":
    main()

现在我们逐条梳理一下程序的代码:

1. 每个应用都扩展自 App 类。其中最重要的有 composemount 等方法。但在当前应用中,我们只实现了 composetextual.textualize.io。

2. 在 compose 方法中,你会返回一系列 组件textual.textualize.io(Widget),并按顺序添加到主屏幕中。每一个组件都有定制自身外观的选项。

3. 你可以设定单字母的 绑定textual.textualize.io(binding),比如此处我们设定了 q 键来退出应用(参见 action_quit_app 函数和 BINDINGS 列表)。

4. 利用 SelectionList 组件,我们展示了待运行的命令列表。然后,你可以通过 @on(SelectionList.SelectedChanged) 注解以及 on_selection 方法告知应用获取所选的内容。

5. 对于无选定元素的应对很重要,我们会根据运行的命令数量来决定是否禁用 “exec” 按钮。

6. 我们使用类似的监听器( @on(Button.Pressed) )来执行命令。我们做的就是将我们的选择送到一个新的屏幕,该屏幕会负责执行命令并收集结果。

你注意到 CSS_PATH = "os_app.tcss" 这个变量了吗?Textual 允许你使用 CSS 来控制单个或多个组件的外观(色彩、位置、尺寸):

Screen {
        layout: vertical;
}
Header {
        dock: top;
}
Footer {
        dock: bottom;
}
SelectionList {
        padding: 1;
        border: solid $accent;
        width: 1fr;
        height: 80%;
}
Button {
        width: 1fr
}

引自 Textual 官方网站:

Textual 中使用的 CSS 是互联网上常见 CSS 的简化版本,容易上手。

这真是太棒了,只需要用一个独立的 样式表textual.textualize.io,就可以轻松调整应用的样式。

好,我们现在来看看如何在新屏幕上展示结果。

在新屏幕上展示结果

以下是在新屏幕上处理输出的代码:

import asyncio
from typing import List
from textual import on, work
from textual.reactive import reactive
from textual.screen import ModalScreen
from textual.widgets import Button, Label, Log
from textual.worker import Worker
from textual.app import ComposeResult
class LogScreen(ModalScreen):
    count = reactive(0)
    MAX_LINES = 10_000
    ENABLE_COMMAND_PALETTE = False
    CSS_PATH = "log_screen.tcss"
    def __init__(
            self,
            name: str | None = None,
            ident: str | None = None,
            classes: str | None = None,
            selections: List = None
    ):
        super().__init__(name, ident, classes)
        self.selections = selections
    def compose(self) -> ComposeResult:
        yield Label(f"Running {len(self.selections)} commands")
        event_log = Log(
            id='event_log',
            max_lines=LogScreen.MAX_LINES,
            highlight=True
        )
        event_log.loading = True
        yield event_log
        button = Button("Close", id="close", variant="success")
        button.disabled = True
        yield button
    async def on_mount(self) -> None:
        event_log = self.query_one('#event_log', Log)
        event_log.loading = False
        event_log.clear()
        lst = '\n'.join(self.selections)
        event_log.write(f"Preparing:\n{lst}")
        event_log.write("\n")
        for command in self.selections:
            self.count += 1
            self.run_process(cmd=command)
    def on_worker_state_changed(self, event: Worker.StateChanged) -> None:
        if self.count == 0:
            button = self.query_one('#close', Button)
            button.disabled = False
        self.log(event)
    @work(exclusive=False)
    async def run_process(self, cmd: str) -> None:
        event_log = self.query_one('#event_log', Log)
        event_log.write_line(f"Running: {cmd}")
        # Combine STDOUT and STDERR output
        proc = await asyncio.create_subprocess_shell(
            cmd,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.STDOUT
        )
        stdout, _ = await proc.communicate()
        if proc.returncode != 0:
            raise ValueError(f"'{cmd}' finished with errors ({proc.returncode})")
        stdout = stdout.decode(encoding='utf-8', errors='replace')
        if stdout:
            event_log.write(f'\nOutput of "{cmd}":\n')
            event_log.write(stdout)
        self.count -= 1
    @on(Button.Pressed, "#close")
    def on_button_pressed(self, _) -> None:
        self.app.pop_screen()

你会注意到:

1. LogScreen 类扩展自 ModalScreen 类, 该类负责处理模态模式的屏幕。

2. 这个屏幕同样有一个 compose 方法,我们在这里添加了组件以展示 Unix 命令的内容。

3. 我们创建了一个叫做 mount 的新方法。一旦你用 compose 编排好组件,你就可以运行代码来获取数据,并再进一步定制它们的外观。

4. 我们使用 asynciodocs.python.org 运行命令,这样我们就能让 TUI 主工作线程在每个命令的结果出来时就及时更新内容。

5. 对于“工作线程”,请注意 run_process 方法上的 @work(exclusive=False) 注解,该方法用于运行命令并捕获 STDOUT + STDERR 输出。使用 工作线程textual.textualize.io 来管理并发并不复杂,尽管它们在手册中确实有专门的章节。这主要是因为运行的外部命令可能会执行很长时间。

6. 在 run_process 中,我们通过调用 write 以命令的输出内容来更新 event_log

7. 最后,on_button_pressed 把我们带回到前一屏幕(从堆栈中移除屏幕)。

这个小应用向你展示了如何一份不到 200 行的代码来编写一个简单的前端,用来运行非 Python 代码。

现在我们来看一个更复杂的例子,这个例子用到了我们还未探索过的 Textual 的新特性。

示例二:展示赛事成绩的表格

通过 Textual 创建的表格应用

本示例将展示如何使用 DataTable 组件在表格中展示赛事成绩。你能通过这个应用实现:

◈ 通过列来排序表格

◈ 选择表格中的行,完整窗口展示赛事细节,我们将使用我们在日志浏览器中看到的 “推送屏幕” 技巧。

◈ 能进行表格搜索,查看选手详情,或执行其他操作如退出应用。

下面,我们来看看应用代码:

#!/usr/bin/env python
"""
Author: Jose Vicente Nunez
"""
from typing import Any, List
from rich.style import Style
from textual import on
from textual.app import ComposeResult, App
from textual.command import Provider
from textual.screen import ModalScreen, Screen
from textual.widgets import DataTable, Footer, Header
MY_DATA = [
    ("level", "name", "gender", "country", "age"),
    ("Green", "Wai", "M", "MYS", 22),
    ("Red", "Ryoji", "M", "JPN", 30),
    ("Purple", "Fabio", "M", "ITA", 99),
    ("Blue", "Manuela", "F", "VEN", 25)
]
class DetailScreen(ModalScreen):
    ENABLE_COMMAND_PALETTE = False
    CSS_PATH = "details_screen.tcss"
    def __init__(
            self,
            name: str | None = None,
            ident: str | None = None,
            classes: str | None = None,
            row: List[Any] | None = None,
    ):
        super().__init__(name, ident, classes)
        # Rest of screen code will be show later
class CustomCommand(Provider):
    def __init__(self, screen: Screen[Any], match_style: Style | None = None):
        super().__init__(screen, match_style)
        self.table = None
        # Rest of provider code will be show later
class CompetitorsApp(App):
    BINDINGS = [
        ("q", "quit_app", "Quit"),
    ]
    CSS_PATH = "competitors_app.tcss"
    # Enable the command palette, to add our custom filter commands
    ENABLE_COMMAND_PALETTE = True
    # Add the default commands and the TablePopulateProvider to get a row directly by name
    COMMANDS = App.COMMANDS | {CustomCommand}
    def action_quit_app(self):
        self.exit(0)
    def compose(self) -> ComposeResult:
        yield Header(show_clock=True)
        table = DataTable(id=f'competitors_table')
        table.cursor_type = 'row'
        table.zebra_stripes = True
        table.loading = True
        yield table
        yield Footer()
    def on_mount(self) -> None:
        table = self.get_widget_by_id(f'competitors_table', expect_type=DataTable)
        columns = [x.title() for x in MY_DATA[0]]
        table.add_columns(*columns)
        table.add_rows(MY_DATA[1:])
        table.loading = False
        table.tooltip = "Select a row to get more details"
    @on(DataTable.HeaderSelected)
    def on_header_clicked(self, event: DataTable.HeaderSelected):
        table = event.data_table
        table.sort(event.column_key)
    @on(DataTable.RowSelected)
    def on_row_clicked(self, event: DataTable.RowSelected) -> None:
        table = event.data_table
        row = table.get_row(event.row_key)
        runner_detail = DetailScreen(row=row)
        self.show_detail(runner_detail)
    def show_detail(self, detailScreen: DetailScreen):
        self.push_screen(detailScreen)
def main():
    app = CompetitorsApp()
    app.title = f"Summary".title()
    app.sub_title = f"{len(MY_DATA)} users"
    app.run()
if __name__ == "__main__":
    main()


有哪些部分值得我们关注呢?

1. compose 方法中添加了 表头textual.textualize.io,“命令面板” 就位于此处,我们的表格(DataTabletextual.textualize.io)也在这里。表格数据在 mount 方法中填充。

2. 我们设定了预期的绑定(BINDINGS),并指定了外部的 CSS 文件来设置样式(CSS_PATH)。

3. 默认情况下,我们无需任何设置便能使用 命令面板textual.textualize.io,但在此我们显式启用了它(ENABLE_COMMAND_PALETTE = True)。

4. 我们的应用有一个自定义表格搜索功能。当用户输入一名选手的名字后,应用会显示可能的匹配项,用户可以点击匹配项查看该选手的详细信息。这需要告诉应用我们有一个定制的命令提供者(COMMANDS = App.COMMANDS | {CustomCo_ mmand}),即类 CustomCommand(Provider)

5. 如果用户点击了表头,表格内容会按照该列进行排序。这是通过 on_header_clicked 方法实现的,该方法上具有 @on(DataTable.HeaderSelected) 注解。

6. 类似地,当选中表格中的一行时, on_row_clicked 方法会被调用,这得益于它拥有 @on(DataTable.RowSelected) 注解。当方法接受选中的行后,它会推送一个新的屏幕,显示选中行的详细信息(class DetailScreen(ModalScreen))。

现在,我们详细地探讨一下如何显示选手的详细信息。

利用多屏展示复杂视图

当用户选择表格中的一行,on_row_clicked 方法就会被调用。它收到的是一个 DataTable.RowSelected 类型的事件。从这里我们会用选中的行的内容构建一个 DetailScreen(ModalScreen) 类的实例:

from typing import Any, List
from textual import on
from textual.app import ComposeResult
from textual.screen import ModalScreen
from textual.widgets import Button, MarkdownViewer
MY_DATA = [
    ("level", "name", "gender", "country", "age"),
    ("Green", "Wai", "M", "MYS", 22),
    ("Red", "Ryoji", "M", "JPN", 30),
    ("Purple", "Fabio", "M", "ITA", 99),
    ("Blue", "Manuela", "F", "VEN", 25)
]
class DetailScreen(ModalScreen):
    ENABLE_COMMAND_PALETTE = False
    CSS_PATH = "details_screen.tcss"
    def __init__(
            self,
            name: str | None = None,
            ident: str | None = None,
            classes: str | None = None,
            row: List[Any] | None = None,
    ):
        super().__init__(name, ident, classes)
        self.row: List[Any] = row
    def compose(self) -> ComposeResult:
        self.log.info(f"Details: {self.row}")
        columns = MY_DATA[0]
        row_markdown = "\n"
        for i in range(0, len(columns)):
            row_markdown += f"* **{columns[i].title()}:** {self.row[i]}\n"
        yield MarkdownViewer(f"""## User details:
        {row_markdown}
        """)
        button = Button("Close", variant="primary", id="close")
        button.tooltip = "Go back to main screen"
        yield button
    @on(Button.Pressed, "#close")
    def on_button_pressed(self, _) -> None:
        self.app.pop_screen()

这个类的职责很直接:

1. compose 方法取得此行数据,并利用一个 支持 Markdown 渲染的组件textual.textualize.io 来展示内容。它的便利之处在于,它会为我们自动生成一个内容目录。

2. 当用户点击 “close” 后,方法 on_button_pressed 会引导应用回到原始屏幕。注解 @on(Button.Pressed, "#close") 用来接收按键被点击的事件。

最后,我们来详细讲解一下那个多功能的搜索栏(也叫做命令面板)。

命令面板的搜索功能

任何使用了表头的 Textual 应用都默认开启了 命令面板textual.textualize.io。有意思的是,你可以在 CompetitorsApp 类中添加自定义的命令,这会增加到默认命令集之上:

COMMANDS = App.COMMANDS | {CustomCommand}

然后是执行大部分任务的 CustomCommand(Provider) 类:

from functools import partial
from typing import Any, List
from rich.style import Style
from textual.command import Provider, Hit
from textual.screen import ModalScreen, Screen
from textual.widgets import DataTable
from textual.app import App
class CustomCommand(Provider):
    def __init__(self, screen: Screen[Any], match_style: Style | None = None):
        super().__init__(screen, match_style)
        self.table = None
    async def startup(self) -> None:
        my_app = self.app
        my_app.log.info(f"Loaded provider: CustomCommand")
        self.table = my_app.query(DataTable).first()
    async def search(self, query: str) -> Hit:
        matcher = self.matcher(query)
        my_app = self.screen.app
        assert isinstance(my_app, CompetitorsApp)
        my_app.log.info(f"Got query: {query}")
        for row_key in self.table.rows:
            row = self.table.get_row(row_key)
            my_app.log.info(f"Searching {row}")
            searchable = row[1]
            score = matcher.match(searchable)
            if score > 0:
                runner_detail = DetailScreen(row=row)
                yield Hit(
                    score,
                    matcher.highlight(f"{searchable}"),
                    partial(my_app.show_detail, runner_detail),
                    help=f"Show details about {searchable}"
                )
class DetailScreen(ModalScreen):
        def __init__(
            self,
            name: str | None = None,
            ident: str | None = None,
            classes: str | None = None,
            row: List[Any] | None = None,
    ):
        super().__init__(name, ident, classes)
        # Code of this class explained on the previous section
class CompetitorsApp(App):
    # Add the default commands and the TablePopulateProvider to get a row directly by name
    COMMANDS = App.COMMANDS | {CustomCommand}
    # Most of the code shown before, only displaying relevant code
    def show_detail(self, detailScreen: DetailScreen):
        self.push_screen(detailScreen)


1. 所有继承自 Provider 的类需实现 search 方法。在我们的例子中,我们还覆盖了 startup 方法,为了获取到我们应用表格(和其内容)的引用,这里使用到了 App.query(DataTable).first()。在类的生命周期中, startup 方法只会被调用一次。

2. 在 search 方法内,我们使用 Provider.matcher 对每个表格行的第二列(即名字)进行模糊搜索,以与用户在 TUI 中输入的词条进行比较。matcher.match(searchable) 返回一个整型的评分,大于零说明匹配成功。

3. 在 search 方法中,如果评分大于零,则返回一个 Hit 对象,以告知命令面板搜索查询是否成功。

4. 每个 Hit 都有以下信息:评分(用于在命令面板中对匹配项排序)、高亮显示的搜索词、一个可调用对象的引用(在我们的案例中,它是一个可以将表格行推送到新屏幕的函数)。

5. Provider 类的所有方法都是异步的。这使你能释放主线程,只有当响应准备好后才返回结果,这个过程不会冻结用户界面。

理解了这些信息,我们就可以现在展示赛手的详细信息了。

尽管这个架构的追踪功能相对直观,但是组件间传递的消息复杂性不可忽视。幸运的是,Textual 提供了有效的调试工具帮助我们理解背后的工作原理。

Textual 应用的问题排查

对于 Python 的 Textual 应用进行 调试github.com 相较而言更具挑战性。这是因为其中有一些操作可能是异步的,而在解决组件问题时设置断点可能颇为复杂。

根据具体情况,你可以使用一些工具。但首先,确保你已经安装了 textual 的开发工具:

pip install textual-dev==1.3.0

确保你能捕捉到正确的按键

不确定 Textual 应用是否能捕捉到你的按键操作?运行 keys 应用:

textual keys

这让你能够验证一下你的按键组合,并确认在 Textual 中产生了哪些事件。

图片比千言万语更直观

如果说你在布局设计上遇到了问题,想向他人展示你当前的困境,Textual 为你的运行应用提供了截图功能:

textual run --screenshot 5 ./kodegeek_textualize/log_scroller.py

就像你所看到的,我是通过这种方式为这篇教程创建了插图。

捕获事件并输出定制消息

在 Textual 中,每一个应用实例都有一个日志记录器,可以使用如下方式访问:

my_app = self.screen.app
my_app.log.info(f"Loaded provider: CustomCommand")

想要查看这些消息,首先需要开启一个控制台:

. ~/virtualenv/Textualize/bin/activate
textual console

然后在另一个终端运行你的应用程序:

. ~/virtualenv/Textualize/bin/activate
textual run --dev ./kodegeek_textualize/log_scroller.py

在运行控制台的终端中,你可以看到实时的事件和消息输出:

▌Textual Development Console v0.46.0
▌Run a Textual app with textual run --dev my_app.py to connect.
▌Press Ctrl+C to quit.
─────────────────────────────────────────────────────────────────────────────── Client '127.0.0.1' connected ────────────────────────────────────────────────────────────────────────────────
[20:29:43] SYSTEM                                                                                                                                                                 app.py:2188
Connected to devtools ( ws://127.0.0.1:8081 )
[20:29:43] SYSTEM                                                                                                                                                                 app.py:2192
---
[20:29:43] SYSTEM                                                                                                                                                                 app.py:2194
driver=<class 'textual.drivers.linux_driver.LinuxDriver'>
[20:29:43] SYSTEM                                                                                                                                                                 app.py:2195
loop=<_UnixSelectorEventLoop running=True closed=False debug=False>
[20:29:43] SYSTEM                                                                                                                                                                 app.py:2196
features=frozenset({'debug', 'devtools'})
[20:29:43] SYSTEM                                                                                                                                                                 app.py:2228
STARTED FileMonitor({PosixPath('/home/josevnz/TextualizeTutorial/docs/Textualize/kodegeek_textualize/os_app.tcss')})
[20:29:43] EVENT

此外,以开发者模式运行的另一大好处是,如果你更改了 CSS,应用会尝试重新渲染,而无需重启程序。

如何编写单元测试

为你全新开发的 Textual 应用编写 单元测试docs.python.org,应该如何操作呢?

在 官方文档textual.textualize.io 展示了几种用于测试我们应用的方式。

我将采用 unittestdocs.python.org 进行测试。为了处理异步例程,我们会需要特别的类 unittest.IsolatedAsyncioTestCase

import unittest
from textual.widgets import Log, Button
from kodegeek_textualize.log_scroller import OsApp
class LogScrollerTestCase(unittest.IsolatedAsyncioTestCase):
    async def test_log_scroller(self):
        app = OsApp()
        self.assertIsNotNone(app)
        async with app.run_test() as pilot:
            # Execute the default commands
            await pilot.click(Button)
            await pilot.pause()
            event_log = app.screen.query(Log).first()  # We pushed the screen, query nodes from there
            self.assertTrue(event_log.lines)
            await pilot.click("#close")  # Close the new screen, pop the original one
            await pilot.press("q")  # Quit the app by pressing q
if __name__ == '__main__':
    unittest.main()

现在让我们详细看看 test_log_scroller 方法中的操作步骤:

1. 通过 app.run_test() 获取一个 Pilot 实例。然后点击主按钮,运行包含默认指令的查询,随后等待所有事件的处理。

2. 从我们新推送出的屏幕中获取 Log,确保我们已获得几行返回的内容,即它并非空的。

3. 关闭新屏幕并重新呈现旧屏幕。

4. 最后,按下 q,退出应用。

可以测试表格吗?

import unittest
from textual.widgets import DataTable, MarkdownViewer
from kodegeek_textualize.table_with_detail_screen import CompetitorsApp
class TableWithDetailTestCase(unittest.IsolatedAsyncioTestCase):
    async def test_app(self):
        app = CompetitorsApp()
        self.assertIsNotNone(app)
        async with app.run_test() as pilot:
            """
            Test the command palette
            """
            await pilot.press("ctrl+\\")
            for char in "manuela".split():
                await pilot.press(char)
            await pilot.press("enter")
            markdown_viewer = app.screen.query(MarkdownViewer).first()
            self.assertTrue(markdown_viewer.document)
            await pilot.click("#close")  # Close the new screen, pop the original one
            """
            Test the table
            """
            table = app.screen.query(DataTable).first()
            coordinate = table.cursor_coordinate
            self.assertTrue(table.is_valid_coordinate(coordinate))
            await pilot.press("enter")
            await pilot.pause()
            markdown_viewer = app.screen.query(MarkdownViewer).first()
            self.assertTrue(markdown_viewer)
            # Quit the app by pressing q
            await pilot.press("q")
if __name__ == '__main__':
    unittest.main()

如果你运行所有的测试,你将看到如下类似的输出:

(Textualize) [josevnz@dmaf5 Textualize]$ python -m unittest tests/*.py
..
----------------------------------------------------------------------
Ran 2 tests in 2.065s
OK

这是测试 TUI 的一个不错的方式,对吧?

打包 Textual 应用

打包 Textual 应用与打包常规 Python 应用并没有太大区别。你需要记住,需要包含那些控制应用外观的 CSS 文件:

. ~/virtualenv/Textualize/bin/activate
python -m build
pip install dist/KodegeekTextualize-*-py3-none-any.whl

这个教程的 pyproject.tomltutorials.kodegeek.com 文件是一个打包应用的良好起点,告诉你需要做什么。

[build-system]
requires = [
    "setuptools >= 67.8.0",
    "wheel>=0.42.0",
    "build>=1.0.3",
    "twine>=4.0.2",
    "textual-dev>=1.2.1"
]
build-backend = "setuptools.build_meta"
[project]
name = "KodegeekTextualize"
version = "0.0.3"
authors = [
    {name = "Jose Vicente Nunez", email = "kodegeek.com@protonmail.com"},
]
description = "Collection of scripts that show how to use several features of textualize"
readme = "README.md"
requires-python = ">=3.9"
keywords = ["running", "race"]
classifiers = [
    "Environment :: Console",
    "Development Status :: 4 - Beta",
    "Programming Language :: Python :: 3",
    "Intended Audience :: End Users/Desktop",
    "Topic :: Utilities"
]
dynamic = ["dependencies"]
[project.scripts]
log_scroller = "kodegeek_textualize.log_scroller:main"
table_detail = "kodegeek_textualize.table_with_detail_screen:main"
[tool.setuptools]
include-package-data = true
[tool.setuptools.packages.find]
where = ["."]
exclude = ["test*"]
[tool.setuptools.package-data]
kodegeek_textualize = ["*.txt", "*.tcss", "*.csv"]
img = ["*.svg"]
[tool.setuptools.dynamic]
dependencies = {file = ["requirements.txt"]}

未来计划

这个简短的教程只覆盖了 Textual 的部分方面。还有很多需要探索和学习的内容:

◈ 强烈建议你查看 官方教程textual.textualize.io。有大量的示例和指向参考 APItextual.textualize.io 的链接。

◈ Textual 可以使用来自 Richgithub.com 项目的组件,这个项目是一切的起源。我认为其中一些甚至可能所有这些组件在某些时候都会合并到 Textual 中。Textual 框架对于使用高级 API 的复杂应用更能胜任,但 Rich 也有很多漂亮的功能。

◈ 创建你自己的组件!同样,在设计 TUI 时,拿一张纸,画出你希望这些组件如何布局的textual.textualize.io,这会为你后期省去很多时间和麻烦。

◈ 调试 Python 应用可能会有点复杂。有时你可能需要 混合使用不同的工具github.com 来找出应用的问题所在。

◈ 异步 IO 是一个复杂的话题,你应该 阅读开发者文档docs.python.org 来了解更多可能的选择。

◈ Textual 被其他项目所使用。其中一个非常易于使用的项目是 Trogongithub.com。它会让你的 CLI 可以自我发现github.com。

◈ Textual-webgithub.com 是个很有前景的项目,能让你在浏览器上运行 Textual 应用。尽管它不如 Textual 成熟,但它的进化速度非常快。

◈ 最后,查看这些外部项目www.textualize.io。在项目组合中有许多有用的开源应用。


via: https://fedoramagazine.org/crash-course-on-using-textual/

展阅读

blog-engine-01-常见博客引擎 jekyll/hugo/Hexo/Pelican/Gatsby/VuePress/Nuxt.js/Middleman 对比[1]

blog-engine-02-通过博客引擎 jekyll 构建 github pages 博客实战笔记[2]

blog-engine-02-博客引擎jekyll-jekyll 博客引擎介绍[3]

blog-engine-02-博客引擎jekyll-jekyll 如何在 windows 环境安装,官方文档[4]

blog-engine-02-博客引擎jekyll-jekyll SEO[5]

blog-engine-04-博客引擎 hugo intro 入门介绍+安装笔记[6]

blog-engine-05-博客引擎 Hexo 入门介绍+安装笔记[7]

blog-engine-06-pelican 静态网站生成 官方文档[8]

blog-engine-06-pelican 静态网站生成 windows 安装实战[9]

blog-engine-07-gatsby 建极速网站和应用程序 基于React的最佳框架,具备性能、可扩展性和安全性[10]

blog-engine-08-vuepress 以 Markdown 为中心的静态网站生成器[11]

blog-engine-09-nuxt 构建快速、SEO友好和可扩展的Web应用程序变得轻松[12]

blog-engine-10-middleman 静态站点生成器,利用了现代 Web 开发中的所有快捷方式和工具[13]

前言

由于个人一直喜欢使用 markdown 来写 个人博客[14],最近就整理了一下有哪些博客引擎。

感兴趣的小伙伴也可以选择自己合适的。

pelican

Pelican[15] 是一个静态网站生成器,用Python编写,它允许您通过编写Markdown、reStructuredText和HTML等格式的文本文件来创建网站。

使用Pelican,您可以创建网站而无需担心数据库或服务器端编程。Pelican生成可以通过任何网络服务器或托管服务提供的静态站点。

您可以使用Pelican执行以下功能:

使用您选择的编辑器在Markdown或reStructuredText中编写内容

简单的命令行工具重新生成HTML、CSS和JS源内容

易于与版本控制系统和Web挂钩进行接口

完全静态的输出可以简单地托管在任何地方

功能特点

Pelican的功能亮点包括:

时间排序的内容(例如,文章、博客文章)以及静态页面

与外部服务的集成

站点主题(使用Jinja2模板创建)

在多种语言中发布文章

生成Atom和RSS订阅源

通过Pygments进行代码语法高亮显示

从WordPress、Dotclear或RSS订阅源导入现有内容

由于内容缓存和选择性输出编写,重建速度快

可通过丰富的插件生态系统进行扩展:Pelican插件

查看 Pelican 文档[16] 以获取更多信息。

为什么叫“Pelican”?

“Pelican”是“calepin”的一个变位词,法语中意为“笔记本”。

Pelican主题

该存储库包含了用于Pelican的主题。请随意克隆、添加您自己的主题,并提交拉取请求。这是由社区管理的!

您可以在 http://www.pelicanthemes.com 查看实时版本。

使用主题

以下说明假定您已经阅读了所有Pelican文档,有一个工作站点,并且现在想要应用一个非默认主题。

首先,选择一个位置来存放您的主题。对于这个示例,我们将使用目录~/pelican-themes,但您的目录可能不同。

在您的本地机器上将pelican-themes存储库克隆到该位置:

git clone --recursive https://github.com/getpelican/pelican-themes ~/pelican-themes

现在您应该在 ~/pelican-themes/ 下存储您的 pelican-themes 存储库。

要使用其中一个主题,请编辑您的Pelican设置文件以包含以下行:

THEME = "/home/user/pelican-themes/theme-name"

所以,例如,要使用mnmlist主题,您将编辑您的设置文件以包含:

THEME = "/home/user/pelican-themes/mnmlist"

保存对设置文件的更改,然后使用您已经设置的 pelican-quickstart Makefile 重新生成您的站点:

make html

也可以通过-pelican命令的 -t ~/pelican-themes/theme-name 参数直接指定主题。

如果您想要编辑您的主题,请确保您所做的任何编辑都是针对存储在~/pelican-themes/theme-name中的副本进行的。

对于存储在站点输出目录中的文件所做的任何更改都将在下次生成站点时被删除。

Pelican插件

重要提示:我们正在将插件从这个单一的存储库迁移到它们自己的独立存储库,这些存储库位于新的Pelican插件组织下,这是一个供插件作者与Pelican维护者和社区其他成员更广泛合作的地方。我们的意图是让所有新组织下的插件都采用新的“命名空间插件”格式,这意味着这些插件可以轻松地通过Pip安装,并且Pelican 4.5+可以立即识别它们——而不必显式启用它们。

这个过渡过程需要一些时间,因此我们感谢您在此期间的耐心等待。如果您想帮助加速这个过渡,以下内容将非常有帮助:

•如果您在这里找到一个尚未迁移到新组织的插件,请在这个存储库下创建一个新的问题,并说明您想要帮助迁移的插件,之后Pelican维护者将指导您完成此过程。•如果您来到这里提交一个拉取请求以添加您的插件,请考虑将您的插件移动到Pelican插件组织下。要开始,请在这个存储库下创建一个新的问题,提供您插件的详细信息,之后Pelican维护者将指导您完成此过程。•无论您是创建新插件还是迁移现有插件,请使用提供的Cookiecutter模板生成符合社区约定的脚手架命名空间插件。查看Simple Footnotes存储库,以查看一个已迁移插件的示例。

以下其余信息与传统插件相关,但不适用于Pelican插件组织中的新命名空间插件。

如何使用插件

安装和使用这些插件的最简单方法是克隆这个存储库:

git clone --recursive https://github.com/getpelican/pelican-plugins

并在您的设置文件中激活您想要的插件:

PLUGIN_PATHS = ['path/to/pelican-plugins']
PLUGINS = ['assets', 'sitemap', 'gravatar']

PLUGIN_PATHS可以是相对于您的设置文件的路径,也可以是绝对路径。

或者,如果插件位于可导入的路径中,您可以省略PLUGIN_PATHS并列出它们:

PLUGINS = ['assets', 'sitemap', 'gravatar']

或者您可以直接导入插件并给出:

import my_plugin
PLUGINS = [my_plugin, 'assets']

插件描述

迁移状态:

(blank):本地托管插件仍在等待迁移工作。

⚠️:已弃用。可以安全地从此存储库中删除。

❓:由外部维护的插件,不需要从单一存储库显式迁移。迁移工作需要在原始所有者的存储库中进行。

✔️:存储库已迁移到Pelican插件组织。

插件

状态

描述

Ace Editor

将默认的替换为在pelicanconf.py上配置的Ace代码编辑器。

Always modified


将创建日期元数据复制到修改日期,以便在“最新更新”索引中轻松查找。

AsciiDoc reader


使用AsciiDoc编写您的帖子。

Asset management

使用Webassets模块管理资产,如CSS和JS文件。

Author images


添加对作者图片和头像的支持。

Auto Pages


为生成的作者、分类和标签页面生成自定义内容(例如作者传记)。

Backref Translate

为每篇文章/页面(作为翻译的一部分)添加一个新属性(is_translation_of),指向原始文章/页面。

Better code samples

使用div > .hilitewrapper > .codehilitetable类属性包装表格块,允许滚动代码块。

Better code line numbers


允许带有行号的代码块换行。

Better figures/samples


为内容中的任何标签添加style="width: ???px; height: auto;"属性。

Better tables


删除reST生成的HTML表中的多余属性和元素。

bootstrap-rst


提供大多数(尽管不是全部)Bootstrap的rst指令。

bootstrapify

自动将bootstrap的默认类添加到您的内容中。

Category meta


从该类别目录中的索引文件读取每个类别的元数据。

Category Order

按照该类别(或标签)中的文章数量对类别(或标签)进行排序。

CJK auto spacing

在中文/日文/韩文字符和英文单词之间插入空格。

Clean summary


清除摘要中多余的图像。

Code include


在reStructuredText中包含Pygments突出显示的代码。

Collate content


将内容的类别作为列表通过collations属性提供给模板。

Creole reader


使用wikicreole语法编写您的帖子。

CSS HTML JS Minify


在站点生成后,对所有CSS、HTML和JavaScript文件进行最小化。

CTags generator


生成一个“tags”文件,按照“content/”目录中的CTags,以提供对支持它的代码编辑器的自动完成。

Custom article URLs


支持为不同的类别定义不同的默认URL。

Dateish


将任意元数据字段视为datetime对象。

Dead Links

管理失效的链接(网站不可用,错误如403、404)。

Disqus static comments


向所有文章添加disqus_comments属性。评论在生成时使用disqus API获取。

Encrypt content

为页面和文章设置密码保护。

Events


将事件开始、持续时间和位置信息添加到帖子元数据中,以生成iCalendar文件。

Extract table of content


从文章内容中提取目录(ToC)。

Feed summary

⚠️

允许将文章摘要用于ATOM和RSS订阅源,而不是整篇文章。

Figure References

提供一个系统来编号和引用图像。

Filetime from Git


使用Git提交确定页面日期。

Filetime from Hg


使用Mercurial提交确定页面日期。

Footer Insert


在每篇文章的末尾添加标准化的页脚(例如作者信息)。

GA Page View

在个别文章和页面上显示Google Analytics页面视图。

Gallery


允许一篇文章包含一个相册。

Gist directive


此插件添加了一个gist reStructuredText指令。

GitHub wiki


将平面的github wiki转换为结构化的只读wiki,放在您的站点上。

GitHub activity


在模板方面,您只需迭代github_activity变量。

Global license


允许您定义一个LICENSE设置,并将该许可变量的内容添加到文章的上下文中。

Glossary


添加包含从文章和页面中的定义列表中提取的定义的变量。此变量对所有页面模板可见。

Goodreads activity


列出您的Goodreads书架上的书籍。

GooglePlus comments


向Pelican添加GooglePlus评论。

Gravatar

此插件的功能已由更新的Avatar插件取代。

Gzip cache


启用某些网络服务器(例如Nginx)使用gzip压缩文件的静态缓存,以防止在HTTP调用期间服务器对文件进行压缩。

Headerid


此插件为每个标题添加一个锚点,以便您可以在reStructuredText文章中进行深度链接。

HTML entities


允许您在RST文档中内联输入HTML实体,如©、<、•。

HTML tags for rST


允许您在reST文档中使用HTML标签。

I18N Sub-sites


通过为默认站点创建国际化子站点来扩展翻

References

[1] blog-engine-01-常见博客引擎 jekyll/hugo/Hexo/Pelican/Gatsby/VuePress/Nuxt.js/Middleman 对比: https://houbb.github.io/2016/04/13/blog-engine-01-overview
[2] blog-engine-02-通过博客引擎 jekyll 构建 github pages 博客实战笔记:
https://houbb.github.io/2016/04/13/blog-engine-02-jekyll-01-install
[3] blog-engine-02-博客引擎jekyll-jekyll 博客引擎介绍:
https://houbb.github.io/2016/04/13/blog-engine-03-jekyll-02-intro
[4] blog-engine-02-博客引擎jekyll-jekyll 如何在 windows 环境安装,官方文档:
https://houbb.github.io/2016/04/13/blog-engine-03-jekyll-03-install-on-windows-doc
[5] blog-engine-02-博客引擎jekyll-jekyll SEO:
https://houbb.github.io/2016/04/13/blog-engine-03-jekyll-04-seo
[6] blog-engine-04-博客引擎 hugo intro 入门介绍+安装笔记:
https://houbb.github.io/2016/04/13/blog-engine-04-hugo-intro
[7] blog-engine-05-博客引擎 Hexo 入门介绍+安装笔记:
https://houbb.github.io/2017/03/29/blog-engine-05-hexo
[8] blog-engine-06-pelican 静态网站生成 官方文档:
https://houbb.github.io/2016/04/13/blog-engine-06-pelican-01-intro
[9] blog-engine-06-pelican 静态网站生成 windows 安装实战:
https://houbb.github.io/2016/04/13/blog-engine-06-pelican-02-quick-start
[10] blog-engine-07-gatsby 建极速网站和应用程序 基于React的最佳框架,具备性能、可扩展性和安全性:
https://houbb.github.io/2016/04/13/blog-engine-07-gatsby-01-intro
[11] blog-engine-08-vuepress 以 Markdown 为中心的静态网站生成器:
https://houbb.github.io/2016/04/13/blog-engine-08-vuepress-01-intro
[12] blog-engine-09-nuxt 构建快速、SEO友好和可扩展的Web应用程序变得轻松:
https://houbb.github.io/2016/04/13/blog-engine-09-nuxt-01-intro
[13] blog-engine-10-middleman 静态站点生成器,利用了现代 Web 开发中的所有快捷方式和工具:
https://houbb.github.io/2016/04/13/blog-engine-10-middleman-01-intro
[14] 个人博客:
https://houbb.github.io/
[15] Pelican:
https://github.com/getpelican/pelican
[16] Pelican 文档:
https://docs.getpelican.com/en/latest/