<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/rss-styles.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>100gle&apos;s Blog</title><description>A blog by 100gle</description><link>https://devlike.top/</link><item><title>三十而立——我所看到的世界</title><link>https://devlike.top/posts/before-30/</link><guid isPermaLink="true">https://devlike.top/posts/before-30/</guid><description>站在三十岁的当下回顾个人历程和认知观念，感悟人生的变化与财富的真谛。</description><pubDate>Wed, 31 Dec 2025 14:35:47 GMT</pubDate><content:encoded>&lt;p&gt;自打我离家上学后就再也没有过过生日，但在 2025 年结束之际才意识到 1996 年出生的我也即将 30 岁。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;三十而立。——《论语》&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;以前经常看到用「走马灯」一词来描述一个人濒死时开始一幕幕对过去回忆的境况，但没想到我也经常会有走马灯时刻，当然这不是我快死了，而是可能我是个念旧的人。&lt;/p&gt;
&lt;p&gt;我会经常怀念过去的人和事，尤其是上学时期。可能对于中国人来说校园生活承载了大部分的青春记忆，所以不论是跟小学还是初高中甚至大学同学们聊天时我们总会围绕着人或事开启话头，比如某个人做了什么事，又或者是对某个事件我们作为旁观者又是怎样的看法等。&lt;/p&gt;
&lt;p&gt;我的中小学同学会有时还会惊讶于我还清楚记得某个人的姓名，也许并不是记性好，而是这个人在我生命历程中就留下了一道特别的涟漪所以才让我印象深刻；反过来，也许我也会成为某个人生命历程里的一道特殊涟漪，有荡漾的时刻但又很快消失。&lt;/p&gt;
&lt;p&gt;转念一想才发觉到距离我和这些人从相识到离开也不过短短几年，尽管没有「一眨眼」这样的用词那么夸张，但年的单位似乎也丈量出人短暂的一生，人能有多少个几年？&lt;/p&gt;
&lt;p&gt;从中学到高考不过几年，从大学到毕业不过几年，从毕业再到出社会也不过几年，然后累加起来发现我已经三十岁了。&lt;/p&gt;
&lt;h2&gt;教育、认知和观念颠覆&lt;/h2&gt;
&lt;p&gt;不知从什么时候开始我会经常反思在过去数个几年中我所经历过的中国式教育，它帮助大部分人完成了初始的知识文化积累，但也让经历过它的人都养成了一种难以摆脱的应试学习模式。&lt;/p&gt;
&lt;p&gt;记得我大二上假期时留在上海没有回家，我刚开始学编程。由于专业的缘故接触了一些定量统计相关的内容，当时授课老师教授了大家 STATA（一款在北美学术圈很流行的统计编程软件），第一次感受敲代码这件事的魅力，那也是我第一次真切实感地体验到那种所谓的「黑客感」（即那种在 Terminal 上敲很基础代码糊弄外行的短视频内容），趁着假期后我开始搜索相关的内容，并了解到如何 Google 以及如何使用 R 语言等。这也是我从文科自学成为程序员的起点。&lt;/p&gt;
&lt;p&gt;当我用应试的思维定式去学习时发现直接失败，我记了太多不需要的 API 或函数，我总是复习这些用不到但却遗忘的内容，以为我能最终掌握它们，但毫无疑问我都以失败告终。显然用进废退和帕累托法则能够解释这个现象，但核心的问题是在于为什么我们仍然遵循应试学习的模式？因为从小到大似乎老师总向我们灌输一个观念就是把所有知识都掌握了我们才能很好地去解决、处理问题。以为无论是在表现自我、比赛还是在面试工作中都能从从容容游刃有余，但大多数时候总是匆匆忙忙连滚带爬。&lt;/p&gt;
&lt;p&gt;在大学的新生自我介绍上我讲了一句很鸡汤但至今仍有道理的话：&lt;strong&gt;人生没有彩排，每天都是现场直播&lt;/strong&gt;。尽管我不知道这句话是出自何处，但我想它将是我未来有限的人生中恪守的信条。这在我工作这几年以来显得弥足珍贵，很多时候会因惧怕自己不懂而错失机会，然而学习什么时候都可以开始，但机会却不是每时每刻都有，一旦错过了再遇上就不知是猴年马月了。「先上车，后补票」非常符合软件工程「ugly but works」的调性，执着于完美本身是一种病。&lt;/p&gt;
&lt;p&gt;学习模式上的改观很大程度上也源自于个人认知的改观。&lt;/p&gt;
&lt;p&gt;随着阅历的增长，我很确定教科书和「象牙塔」知识带来的认知无法帮助个体真正地融入社会。我经常和同我坐一条地铁下班的同事说过颠覆了她认知的观点，比如「会争会抢才有得吃，等着别人给的是什么都不会有」、「社会本质上是不公平的」、「勤劳不会致富」等，似乎对于她那个年龄层的人来说完全不敢想象。而这些知识经验没办法完全从教科书或老师口中学到，而是要参透社会运行规律并在与人博弈中悟道。&lt;/p&gt;
&lt;p&gt;从小到大我们被灌输着「人人平等」的观念本质上是一层文明的粉饰，但阶级分层仍未改变，只是阶级跨域的入口变得越来越窄，入场券的门槛也越来越高。这在我看完《人生七年》之后形成很大的落差感，尤其是当我再回顾人生中出现的人和事就这种感觉会愈发强烈：初中的同学可以穿新潮的正版 Nike Air Force 1 或是用上新款的 iPhone 4s、大学的同学可以开着奥迪 Q5 来学校、工作的同事早再几年前便已在上海买房……&lt;/p&gt;
&lt;p&gt;我并不是在怨天尤人，而是确实我对关系、资源这种以前会嗤之以鼻的概念更加具象化了，强者愈强富者更富的马太效应能够弥补代际之间的不足，充足的资源和强大的人脉关系确实可以让个人的试错成本变得无限低，哪怕亏到一无所有最后还有厚实的家底来托底。&lt;/p&gt;
&lt;p&gt;对我而言很明确的是，人生在世一是提高并丰富自己的体验努力寻找存在的意义，二是努力扩大自己的认知边界，这样才能在机会来临时利用认知杠杆撬动更大的收益。&lt;/p&gt;
&lt;h2&gt;感性动物&lt;/h2&gt;
&lt;p&gt;「有没有女朋友？什么时候结婚？」&lt;/p&gt;
&lt;p&gt;逢年过节经典的催婚、问工作情况桥段不知道是从什么时候开始盛行，直到步入社会工作后开始不绝于耳。每当跟我妈视频，她起手便是「看起来又胖了」、「工资有没有涨」、「不出去玩？有没有女朋友？」一套 Combo，我只能每次无言以对，最终脱口而出一句抱怨意味颇浓的回应——「总说这些，能不能说些别的？」。&lt;/p&gt;
&lt;p&gt;我是个念旧的人，但这很大程度上源于我有些感性。&lt;/p&gt;
&lt;p&gt;尽管我不能以老气横秋地口吻来说「上了年纪」，但我切实感受到自己因岁月沉淀的影响而变得「情感脆弱」——尤其是当我在 B 站或抖音刷视频时看到一些温馨、疾苦或是充满回忆的画面在配乐的烘托下不自觉地鼻头发酸，尽管我知道当中不乏精心设计的剧本情节，但也屡次上当。&lt;/p&gt;
&lt;p&gt;我从来不主动给家里打电话，也从来不告诉家里人我的近况，每年也仅是过年的时候告诉我妈或我哥大概什么时候回家，但随着年龄的增长我对我妈的体恤却是一直在加重。&lt;/p&gt;
&lt;p&gt;当知道挣钱不容易时，我总会惊诧于想起我妈以前起早贪黑、不厌其烦地跟着我爸吃苦，哪怕在我爸花天酒地、无度挥霍时她依旧能持家守着全家赖以生计的铺面。我妈属羊，在性格上倒是很贴合生肖的特征：温顺、内敛且任劳任怨。&lt;/p&gt;
&lt;p&gt;我记忆里她唯一一次生气还是在我刚记事时那个岁数。印象很深地是，当时铺面生意忙得不可开交而我又高烧不退，恰逢此时我爸正和他的狐朋狗友们在打牌赌钱。然后这个女人背着几近烧到昏厥的我气冲冲地在其中一人的店铺里找到我爸，紧接着用客家话当面怒斥他儿子都发烧得「快死了」都不管还有心思这打牌，盛怒之下把店铺门口停着的、我爸那辆刚挣钱时买的进口本田黑鲨摩托一把推翻在地，最后带着我找了一家诊所输液退烧。&lt;/p&gt;
&lt;p&gt;打那之后我再也没有见过我妈有如此强烈的情绪波动，即便是我爸打骂她时也不曾有过。&lt;/p&gt;
&lt;p&gt;在我妈那个年代并没有像如今这样讲究门当户对，她只知道为人母后就自动扛起了担子，不管娶他的那个人是否爱她。所以时至今日我总会跟她说，我要是她，我绝对不会嫁给我爸这种大男子主义、好吃懒做又不会顾家的人，我妈听罢也总是沉默无言。太阳照常升起，似乎在我妈的程序设计里并没有一个 goto 的逃生出口或是判断条件给可以让她跳出循环，日子仍是一天又一天地在重复。&lt;/p&gt;
&lt;p&gt;所以现在很多时候我都经常给我妈灌输人生苦短、及时行乐的观点，趁着还能出去玩玩走走，又何必还待在家里；不用管儿子们有没有成家立业有没有结婚，自己开心才是最重要的。可她的飞行目的大多数时候还是在广西老家，回来之后又总说：「也没什么好玩的。」&lt;/p&gt;
&lt;p&gt;感性表露在外不是一件好事。记得我大一时和一位女生交往时因为讲到我妈怎么不容易时情绪有点激动和落泪把人家给吓到了，现在看来这有些莫名其妙甚至渗人，最后这段感情也无疾而终。后来的一位上海姑娘交往时我的感性程度似乎又提升了一个档次，而换位思考、情绪价值和体会痛苦是这一次经历的收获。&lt;/p&gt;
&lt;p&gt;与她相恋应该是我人生第一次完整地「爱之初体验」。女方的家庭背景和我一样都是普通家庭，没有什么特殊的阶级差异，所以在一起时不论是性格还是三观上都没有太多冲突，我们在一起差不多也快有四五年的时间。&lt;/p&gt;
&lt;p&gt;我现在会经常回味这段感情经历，在这过程中女方可以说是我的导师，她包容着我的不足和直男行为，但对我优点又毫不吝啬地夸张，而这其实也就是今年盛行的「情绪价值」一词。但作为没有完整谈过恋爱的我来说刚开始总是有点不解风情，关心、呵护、体谅这些词语似乎从来没有在我的字典里出现过，每当我和她争吵完恢复如初后再去复盘，总会发现是我更多自以为是、以自己为中心而忽略对方的感受，时至今日我才明白「同理心」这样高级词汇，在低级认知人群的脑海里往往是匮乏且稀缺的，如果不明白这一点最后只会成为「巨婴」。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;痛みを感じろ，痛みを考えろ，痛みを受け取れ，痛みを知れ（感受痛苦，思考痛苦，接受痛苦，了解痛苦）——动漫《火影忍者》台词&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;思维、认知和精神这一系列进化衍生出来的概念产物丰富了人类在地球 Online 的体验，痛苦也是其中一种。不同于肉体上的痛苦，精神上的痛苦才是持续伤害，而感情世界上的痛苦更则是超级加倍，一次触碰总是两次渲染。有时会因为一些不一致的行为而争执，有时会因为对方的双标而引发不满，甚至有时会因为一点沟通言语上的用词而不满……尽管每次彼此间的争吵或冲突总是双方倍感难过，但感受痛苦也是整段感情经历重要的一环，而它最终也以女方主动提出的「我们分手吧」而划上终止符。&lt;/p&gt;
&lt;p&gt;尽管仍留有女方的微信，但正如五月天《温柔》唱的那样：「不打扰是我的温柔」。感谢她把我塑造成了一个更立体的人。&lt;/p&gt;
&lt;h2&gt;财富与投资&lt;/h2&gt;
&lt;p&gt;今年我意识里形成的一个最深的感悟莫过于：对&lt;strong&gt;财富&lt;/strong&gt;的认知太晚了。直白一点就是对&lt;strong&gt;钱&lt;/strong&gt;了解的太晚了，两个例子：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我高中那会第一次拿奖学金——9000 元——我一学期的学费（私立学校学费比较贵），我并没有意识到这笔钱在 2012 年之前是一个怎样的概念，放在今日它等价于一部 256g 的 iPhone 17 Pro，也差不多等于是本科国家一级奖学金，但我却不以为然，仅作为一次激励。&lt;/li&gt;
&lt;li&gt;然后在我高考完之后我爸曾给了我 10000 元，结果我仅用了一两个月就把它们花完了，甚至我不知道都花在了什么地方。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如今工作后知道「钱难挣，屎难吃」，感慨当时自己为什么能花钱那么快，但很大一部分原因归咎于自己对于钱并没有一个合理的认识，而那时也总是由父母支持，以为自己「无限子弹」。&lt;/p&gt;
&lt;p&gt;年轻人的第一次超前消费从大学开始，我记得那会双 11 刚拉开帷幕，外卖大战和共享单车大战正热火朝天。吃喝玩乐样样都要钱，在这种情况下还没撑到下个月我妈打来的生活费钱包就已经见底，然后开通并使用花呗顶着。&lt;/p&gt;
&lt;p&gt;紧接着暑假时第一次开始找兼职，当时在学校附近随便找了一家私人的培训机构当前台。（没错，就是类似那种公司的前台）然后暑假第一次挣到了大概 3000 元左右解点燃眉之急，尽管这样的薪资在上海非常低廉，但第一次挣到钱的感觉是真爽。后面再有这样的爽感就是我在少数派写付费栏目时挣到的副业外快，当然这都是后话了。&lt;/p&gt;
&lt;p&gt;经此一役我知道大多数时候钱都不经花，商家和平台会变着法子不知不觉从你口袋中把钱掏走，所以很长一段时间我不敢乱花钱，但也总买一些品质很差的东西。但对比于那时有家底的同学来说确实会很羡慕他们吃喝玩乐不愁，那也是我未曾体验到的另一种境界。&lt;/p&gt;
&lt;p&gt;尽管工作几年加上副业上的一些积累，可记账软件上的数字增长并没有游戏中来得快，一夜暴富的臆想症也会经常在我脑海里出现，这也证明了资本的原始积累并不能靠辛苦工作挣来，当然投资也不一定。&lt;/p&gt;
&lt;p&gt;我第一次接触投资是在 2021 年前后，那时还是疫情前时期，股市可以说非常疯狂，炙手可热的白酒基金只要钱丢进去年底一卖就是稳赚不赔，更不要说接下来的新能源和医药了。然而这都是新手韭菜没挨刀前的意淫，当我也开始高位接盘、不及时止损直至最后割肉离场后仿佛我这颗韭菜味道更纯正了。&lt;/p&gt;
&lt;p&gt;投资在我看来也是一件非常有意思的事情，不管是在股市还是基金，这都是一个「合法的赌场」，上涨时感受到人性的贪婪，下跌时感受到人性的恐惧，这某种程度上会折射出一个人在某些情景上的行为决策：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;店铺开了三个月没有挣钱反而还亏钱要不要继续投入坚持（补仓），还是再坚持两一个月看看有没有起色，如果没有起色就转让（止损）？&lt;/li&gt;
&lt;li&gt;选择跳槽时是选择总包更高但是加班更时间长的 offer 呢（杠杆），还是选择涨幅不高但是 Work Life Balance 的 offer 呢？&lt;/li&gt;
&lt;li&gt;……&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然而投资没有定式，全是仅供参考，考验的全是以有限的个人认知去判断未来会赢的概率。尤其是 AI 加速的时代，我相信个人的品味、想象力以及创造力会是至关重要的筹码，唯一能做的是不断积累，通过知识的复利直至套现离场。&lt;/p&gt;
&lt;h2&gt;结尾&lt;/h2&gt;
&lt;p&gt;2025 年我的人生除了成功减肥到 70kg 之外其实没有太多出彩的地方，反倒是工作上有些动荡。公司陆续裁员、减员，直到年底部门算上我也只有五人。我也不知道未来的路会是怎样，人生总是充满着不确定性，走一步看一步吧。&lt;/p&gt;
&lt;p&gt;2025 年 12 月 31 日，于深圳。&lt;/p&gt;
&lt;p&gt;新年快乐。&lt;/p&gt;
</content:encoded></item><item><title>Django ORM：模型设计与数据库交互实践</title><link>https://devlike.top/posts/django-orm-basics/</link><guid isPermaLink="true">https://devlike.top/posts/django-orm-basics/</guid><description>深入 Django ORM 的核心概念，包含模型定义、字段类型、查询优化和数据迁移。通过实例讲解如何快速上手了解 Django ORM 的使用方式</description><pubDate>Sun, 15 Dec 2024 07:07:34 GMT</pubDate><content:encoded>&lt;p&gt;在大多数编程语言社区中，Web 框架是最为百花齐放的一个赛道。Django 中的大多数功能都可以在 Python 社区中找到其他替代的 Web 框架，但唯独有一个功能特性是这些框架所不具备的，有人从 Django 切换到其他 Python Web 框架时还会对这个功能心心念念，甚至一度希望 Django 能将这个功能独立出来成库以便能和其他框架结合使用。&lt;/p&gt;
&lt;p&gt;这个功能就是 Django &lt;strong&gt;ORM&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;什么是 ORM？ORM 即 Object-Relational Mapping 的缩写，简单来说我们就是将数据库里面的表映射成编程语言里面的对象结构，那么通过操作对象来完成数据库操作，减少我们在某个编程语言语法和 SQL 语法之间来回切换，而专注于逻辑开发即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;direction: right

classes: {
  component: {
    style: {
      fill: &quot;#f5f5f5&quot;
      stroke: &quot;#333&quot;
      stroke-width: 2
      border-radius: 8
      font-size: 12
    }
  }
  arrow: {
    style: {
      stroke: &quot;#333&quot;
      stroke-width: 2
      font-size: 10
    }
  }
}

# Components
Application: {
  class: component
  label: &quot;应用代码\n(Python)&quot;
  icon: &quot;https://icons.terrastruct.com/dev%2Fpython.svg&quot;
  explanation: |py
    User
    .objects
    .filter(username=&apos;john_doe&apos;)
  |
}

Django ORM: {
  class: component
  label: &quot;Django ORM&quot;
  icon: &quot;https://icons.terrastruct.com/dev%2Fdjango.svg&quot;
  shape: image
}

Database: {
  class: component
  label: &quot;数据库\n(SQL)&quot;
  icon: &quot;https://icons.terrastruct.com/essentials%2F117-database.svg&quot;
  explanation: |sql
    SELECT * FROM user
    WHERE username = &apos;john_doe&apos;
  |
}

# Connections
Application -&amp;gt; Django ORM: {
  class: arrow
  label: &quot;对象操作&quot;
}

Django ORM -&amp;gt; Database: {
  class: arrow
  label: &quot;SQL转换&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然如果你已经是 Python Web 开发者，那么你就或多或少都将 &lt;a href=&quot;https://www.sqlalchemy.org/&quot;&gt;SQLAlchemy&lt;/a&gt; 库作为你操作 ORM 的首选。不过 SQLAlchemy 很强大，但文档的复杂度比较高；而 Django ORM 在易用和功能上做到了一个很好的平衡，尽管它的功能不一定有 SQLAlchemy 那样丰富，但它较为完善的文档和无缝衔接的功能可以覆盖 80% 在模型层操作的场景。&lt;/p&gt;
&lt;p&gt;由于 Django ORM 内容较多，因此将按拆分成多个篇章进行介绍。&lt;/p&gt;
&lt;h2&gt;使用 Django ORM&lt;/h2&gt;
&lt;p&gt;Django ORM 与 Django 框架相绑定，因此我们在安装 Django 时就已经是开箱即用。默认情况下 Django 将为我们使用 &lt;a href=&quot;https://www.sqlite.org/&quot;&gt;SQLite&lt;/a&gt; 数据库，并且都写在了 &lt;code&gt;DATABASES&lt;/code&gt; 这一配置中。在项目的 &lt;code&gt;settings.py&lt;/code&gt; 文件中你便可以看到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
from pathlib import Path

# Build paths inside the project like this: BASE_DIR / &apos;subdir&apos;.
BASE_DIR = Path(__file__).resolve().parent.parent

# Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases

DATABASES = {
    &apos;default&apos;: {
        &apos;ENGINE&apos;: &apos;django.db.backends.sqlite3&apos;,
        &apos;NAME&apos;: BASE_DIR / &apos;db.sqlite3&apos;,
    }
}
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Django 支持在项目中多个数据库，倘若在使用 ORM 时我们没有特别指定那么将会使用 &lt;code&gt;default&lt;/code&gt; 所表示的数据库。&lt;/p&gt;
&lt;p&gt;还记得我们之前通过 &lt;code&gt;manage.py&lt;/code&gt; 的 &lt;code&gt;startapp&lt;/code&gt; 命令创建了一个 &lt;code&gt;myapp&lt;/code&gt; 应用吗？现在我们可以到当中的 &lt;code&gt;models.py&lt;/code&gt; 文件去自定义你的模型了。假设我们要设计一个书籍相关的业务，那么我们就将在这里面得到如下的模型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from django.core.validators import MinValueValidator
from django.db import models


class Publisher(models.Model):
    name = models.CharField(max_length=100)
    address = models.CharField(max_length=200)
    city = models.CharField(max_length=100)
    state_province = models.CharField(max_length=50)
    country = models.CharField(max_length=50)
    website = models.URLField(null=True, blank=True)

    def __str__(self):
        return self.name


class Author(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    bio = models.TextField(blank=True)
    email = models.EmailField(unique=True)

    def __str__(self):
        return f&quot;{self.first_name} {self.last_name}&quot;


class Book(models.Model):
    title = models.CharField(max_length=200)
    authors = models.ManyToManyField(Author, related_name=&quot;books&quot;)
    pages = models.IntegerField(validators=[MinValueValidator(1)])
    price = models.DecimalField(
        max_digits=10, decimal_places=2, validators=[MinValueValidator(0)]
    )
    publisher = models.ForeignKey(
        Publisher, on_delete=models.CASCADE, related_name=&quot;books&quot;
    )
    publication_date = models.DateField()
    isbn = models.CharField(max_length=13, unique=True)

    def __str__(self):
        return self.title

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要使用 Django ORM 就得基于 &lt;code&gt;django.db.models.Model&lt;/code&gt; 类去定义模型，它将与数据库的同名表建立起映射关系，其中类名就是表名，而当中的属性字段就是表字段，你可以在当中设置对应的数据库字段约束或信息等。&lt;/p&gt;
&lt;p&gt;后续所有要对数据库表进行改动的地方都会在你定义模型的地方进行改动，但改动完我们还不能直接就开始在代码层进行使用，因为我们还需要将其与数据库同步，这时候我们就需要 &lt;code&gt;makemigrations&lt;/code&gt; 和 &lt;code&gt;migrate&lt;/code&gt; 这两个命令了。但在执行前请记得将 &lt;code&gt;myapp&lt;/code&gt; 添加到 &lt;code&gt;INSTALLED_APPS&lt;/code&gt; 中，以便 Django 能够找到要同步的模型信息。&lt;/p&gt;
&lt;h2&gt;ORM 迁移与同步&lt;/h2&gt;
&lt;p&gt;在第一章中我们知道了怎么用 Django 命令之后，现在就直接在 &lt;code&gt;manage.py&lt;/code&gt; 所在路径下执行 &lt;code&gt;makemigrations&lt;/code&gt; 命令，它会为我们在对应应用目录下生成一个 &lt;code&gt;migrations&lt;/code&gt; 文件夹，里面包含了所有的迁移记录，以便你在开发阶段可以随时回退到上一次迁移的状态。&lt;strong&gt;但请在迁移时注意备份你的数据。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv run manage.py makemigrations
Migrations for &apos;myapp&apos;:
  myapp/migrations/0001_initial.py
    + Create model Author
    + Create model Publisher
    + Create model Book
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在可以看到终端上输出了迁移文件的内容，它包含了我们在 &lt;code&gt;models.py&lt;/code&gt; 中定义的模型信息，并且每个迁移文件也都是一个合法的 Python 文件，&lt;strong&gt;大多数时候我们不需要手动修改&lt;/strong&gt;当中的内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Generated by Django 5.1.1 on 2024-10-06 07:37

import django.core.validators
import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name=&apos;Author&apos;,
            fields=[
                (&apos;id&apos;, models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name=&apos;ID&apos;)),
                (&apos;first_name&apos;, models.CharField(max_length=50)),
                (&apos;last_name&apos;, models.CharField(max_length=50)),
                (&apos;bio&apos;, models.TextField(blank=True)),
                (&apos;email&apos;, models.EmailField(max_length=254, unique=True)),
            ],
        ),
        migrations.CreateModel(
            name=&apos;Publisher&apos;,
            fields=[
                (&apos;id&apos;, models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name=&apos;ID&apos;)),
                (&apos;name&apos;, models.CharField(max_length=100)),
                (&apos;address&apos;, models.CharField(max_length=200)),
                (&apos;city&apos;, models.CharField(max_length=100)),
                (&apos;state_province&apos;, models.CharField(max_length=50)),
                (&apos;country&apos;, models.CharField(max_length=50)),
                (&apos;website&apos;, models.URLField(blank=True, null=True)),
            ],
        ),
        migrations.CreateModel(
            name=&apos;Book&apos;,
            fields=[
                (&apos;id&apos;, models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name=&apos;ID&apos;)),
                (&apos;title&apos;, models.CharField(max_length=200)),
                (&apos;pages&apos;, models.IntegerField(validators=[django.core.validators.MinValueValidator(1)])),
                (&apos;price&apos;, models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(0)])),
                (&apos;publication_date&apos;, models.DateField()),
                (&apos;isbn&apos;, models.CharField(max_length=13, unique=True)),
                (&apos;authors&apos;, models.ManyToManyField(related_name=&apos;books&apos;, to=&apos;myapp.author&apos;)),
                (&apos;publisher&apos;, models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name=&apos;books&apos;, to=&apos;myapp.publisher&apos;)),
            ],
        ),
    ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在我们进一步就可以执行 &lt;code&gt;migrate&lt;/code&gt; 命令了，它会自动将迁移文件同步到数据库中。需要注意的是，由于 Django 内置了一些组件应用，比如管理后台和用户鉴权等，因此在我们初次同步时也会连同它们的模型信息一起同步到数据库中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv run manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, myapp, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying myapp.0001_initial... OK
  Applying sessions.0001_initial... OK
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每次当我们对模型有修改时都必然会执行 &lt;code&gt;makemigrations&lt;/code&gt; 和 &lt;code&gt;migrate&lt;/code&gt; 两个命令，就好比使用 Git 时的 &lt;code&gt;git add&lt;/code&gt; 和 &lt;code&gt;git commit&lt;/code&gt; 操作一样。&lt;/p&gt;
&lt;p&gt;当然，Django 也会将每次迁移的记录都保存在数据库中，因此你可以通过 &lt;code&gt;showmigrations&lt;/code&gt; 命令来查看已经执行过的迁移记录。&lt;/p&gt;
&lt;h2&gt;与现有数据库同步&lt;/h2&gt;
&lt;p&gt;一般来说手动编写 ORM 映射通常是我们在开始新项目时才会这么做，但倘若你已经是有其他项目共用的数据库，而刚好你又想在 Django 中使用这些表并开发相关的业务逻辑，那么你可以使用 &lt;code&gt;inspectdb&lt;/code&gt; 命令，Django 会自动将数据库中的表拉取并形成相应的 ORM 模型。当然最好需要指定导出到哪个文件中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv run manage.py startapp legacy
uv run manage.py inspectdb --database=legacy user_action_logs user_points  &amp;gt; legacy/models.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;和自己手动编写 ORM 模型类不同的是，使用了 &lt;code&gt;inspectdb&lt;/code&gt; 的模型不会被 Django 管理，因为默认它们都被设置了 &lt;code&gt;managed=False&lt;/code&gt; 的属性，因此不论你对其如何进行修改删除或创建都不会同步到数据库中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from django.db import models

class UserActionLog(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    action = models.CharField(max_length=100)
    timestamp = models.DateTimeField(auto_now_add=True)

    class Meta:
        managed = False
        db_table = &apos;user_action_logs&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;相信你注意到了上面示意的模型类中还包含了一个 &lt;code&gt;Meta&lt;/code&gt; 类，这是 Django 特殊的元数据管理方式，在后续的章节中我们会对其展开，现在先暂时按下不表。&lt;/p&gt;
&lt;h2&gt;在交互式解释器中使用 ORM&lt;/h2&gt;
&lt;p&gt;有时为了开发调试，我们也可以使用 Django 提供的 &lt;code&gt;shell&lt;/code&gt; 命令来进入到交互式的 Python 解释器界面，在当中直接就基于 ORM 进行操作，比如对数据进行增删改查，Django 会自动为我们导入模块，我们只需要像正常使用 Python 代码那样即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv run manage.py shell
Python 3.12.5 (main, Aug  6 2024, 19:08:49) [Clang 15.0.0 (clang-1500.3.9.4)] on darwin
Type &quot;help&quot;, &quot;copyright&quot;, &quot;credits&quot; or &quot;license&quot; for more information.
(InteractiveConsole)
&amp;gt;&amp;gt;&amp;gt; from myapp.models import Author, Book, Publisher
&amp;gt;&amp;gt;&amp;gt; Author.objects.all()
&amp;lt;QuerySet [&amp;lt;Author: Author 1 Last Name 1&amp;gt;, &amp;lt;Author: Author 2 Last Name 2&amp;gt;, &amp;lt;Author: Author 3 Last Name 3&amp;gt;]&amp;gt;
&amp;gt;&amp;gt;&amp;gt; Book.objects.get(pk=2)
&amp;lt;Book: Book 1&amp;gt;
&amp;gt;&amp;gt;&amp;gt; Publisher.objects.filter(name__endswith=&apos;1&apos;)
&amp;lt;QuerySet [&amp;lt;Publisher: Publisher 1&amp;gt;]&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;练习题&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;现在请你设计一个简单的学生选课记录功能。这个功能可能会涉及到学生的姓名、学号等信息，以及学生所选的课程信息。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在第一道题的基础之上，结合你前面所学的命令行知识，为两个模型添加一些模拟数据，并进行增删改查操作：&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;创建新的学生和课程记录&lt;/li&gt;
&lt;li&gt;为指定学生添加和移除课程&lt;/li&gt;
&lt;li&gt;查询某门课程的所有选课学生&lt;/li&gt;
&lt;li&gt;查询某个学生选修的所有课程&lt;/li&gt;
&lt;li&gt;统计每门课程的选课人数&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Django 核心架构详解：视图、路由与中间件的设计实现</title><link>https://devlike.top/posts/django-views-routing-middleware/</link><guid isPermaLink="true">https://devlike.top/posts/django-views-routing-middleware/</guid><description>深入理解 Django MVC 架构，掌握视图编写、URL 路由配置和中间件开发。包含函数视图与类视图对比，RESTful API 设计，以及性能优化建议</description><pubDate>Fri, 04 Oct 2024 06:18:45 GMT</pubDate><content:encoded>&lt;h2&gt;MVC 与 Django 应用结构&lt;/h2&gt;
&lt;p&gt;Django 诞生于 MVC 盛行的时代，这里为那些和我一样跳过 MVC 的朋友补充一下，MVC 三个概念的具体含义：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;M&lt;/strong&gt;odel：模型，泛指一切与数据相关的模型，你可以粗浅地将其理解为是 ORM，也可以是 Java 里面的那种 DTO、PO、DO 等；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;V&lt;/strong&gt;iew：视图，就是用户能直接看到的部分都叫视图，比如你手机端上的各种应用界面、网页等，都可以算作是视图范畴；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C&lt;/strong&gt;ontroller：控制器，这个概念可能有点抽象，它就表示一种与用户（请求）交互的层级、逻辑或区域，你可以把它想象是一个工厂，在里面根据需要来组装不同的原材料从而生产出面向不同消费者的产品。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;direction: down

# Define shapes with styles
user: 用户/浏览器 {
  shape: rectangle
  style.fill: &quot;#ffccff&quot;
}

controller: Controller 控制器 {
  shape: rectangle
  style.fill: &quot;#bbbbff&quot;
}

model: Model 模型 {
  shape: rectangle
  style.fill: &quot;#bbffbb&quot;
}

view: View 视图 {
  shape: rectangle
  style.fill: &quot;#ffbbbb&quot;
}

# Define connections
user -&amp;gt; controller: HTTP请求
controller -&amp;gt; model: 处理请求
model -&amp;gt; controller: 返回数据
controller -&amp;gt; view: 组装数据
view -&amp;gt; controller: 渲染结果
controller -&amp;gt; user: HTTP响应
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但 Django 的概念稍微有点调整，它是 &lt;strong&gt;MTV&lt;/strong&gt; 模式，它与 MVC 的差别在于后两者，其中：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;T 表示 Template 模板，其实也就是直接与 HTML 划上等号，毕竟 Django 最早主要也是用于开发新闻网站；&lt;/li&gt;
&lt;li&gt;V 还是表示 View 视图，但它实际就是 Controller 控制器。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;为了避免混淆，我这里还是统一沿用 MVC 这个名词。&lt;/p&gt;
&lt;p&gt;不过在如今前后端开发分离盛行的时代，视图层部分大多数时候都由前端开发工程师负责（比如前端框架的 Vue 读音就近似于 View）并在浏览器上进行渲染呈现，所以如果你只是一个纯后端工程师或者 CRUD Boy，那么完全可以跳过有关于模板部分的学习。&lt;/p&gt;
&lt;p&gt;在上一章中我们使用了 &lt;code&gt;manage.py&lt;/code&gt; 命令创建了一个 &lt;code&gt;myapp&lt;/code&gt; 应用，从文件树结构中你依旧可以看到 Django 保留下来了 MVC 的文件命名习惯，只不过模板则是需要用户手动添加。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;myapp
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│   └── __init__.py
├── models.py
├── tests.py
└── views.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在实际开发中这个结构可以进一步自己调整，但我们更多时候会对 &lt;code&gt;models.py&lt;/code&gt; 和 &lt;code&gt;views.py&lt;/code&gt; 文件操作，因为它们分别是应用的核心部分；前者主要涉及数据模型的逻辑，而后者则是涉及请求与响应的逻辑，这也是我们全篇的重点。&lt;/p&gt;
&lt;h2&gt;从视图到路由&lt;/h2&gt;
&lt;p&gt;在 Django 的视图函数中我们主要编写处理 HTTP 请求的逻辑。&lt;/p&gt;
&lt;p&gt;现在我们在 &lt;code&gt;myapp/views.py&lt;/code&gt; 中创建一个函数来处理一个简单的 HTTP 请求，并返回一个响应，它的代码如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from django.http import HttpResponse

def index(request):
    return HttpResponse(&quot;Hello, world!&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就这么简单：接收一个 &lt;code&gt;request&lt;/code&gt; 对象，然后返回一个特定的 &lt;code&gt;HttpResponse&lt;/code&gt; 响应对象。&lt;/p&gt;
&lt;p&gt;也许你在使用 Django 之前已经接触过了 Flask 或者 FastAPI，你已经习惯了路由装饰器与对应的视图函数相结合：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from fastapi import FastAPI

app = FastAPI()

@app.get(&quot;/&quot;)
async def root():
    return {&quot;message&quot;: &quot;Hello World!&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但在 Django 项目中通常需要我们自己手动新建一个名为 &lt;code&gt;urls.py&lt;/code&gt; 文件中对应用进行路由配置，配置通常都会保存在一个名为 &lt;code&gt;urlpatterns&lt;/code&gt; 的变量中（约定俗成），它的代码如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from django.urls import path

from . import views

urlpatterns = [
    path(&quot;&quot;, views.index, name=&quot;index&quot;),
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;简单来说，&lt;code&gt;urls.py&lt;/code&gt; 就可以理解是对应应用的路由表，方便你对所有的路由进行管理。&lt;/p&gt;
&lt;p&gt;紧接着还没完，我们需要到项目的同名文件夹下的进一步挂载到根节点上：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path(&apos;admin/&apos;, admin.site.urls),
    path(&apos;myapp/&apos;, include(&apos;myapp.urls&apos;)),
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后当我们启动 Django 服务器并在浏览器中打开 &lt;code&gt;http://127.0.0.1:8000/myapp/&lt;/code&gt; 时，你便可以看到我们的 &lt;code&gt;index&lt;/code&gt; 视图函数的响应结果了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl http://127.0.0.1:8000/myapp/
Hello, world!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Django 这种从视图到路由的添加方式对于已习惯了装饰器方式的使用者来说会觉得繁琐，但这在有几十上百个路由的中大型项目中将会凸显优势，便于集中管理。&lt;/p&gt;
&lt;p&gt;当然 Django 还允许你在配置时指定对应的路径参数，下面是一个来自于官方的示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
from django.urls import path

from . import views

urlpatterns = [
    path(&quot;articles/2003/&quot;, views.special_case_2003),
    path(&quot;articles/&amp;lt;int:year&amp;gt;/&quot;, views.year_archive),
    path(&quot;articles/&amp;lt;int:year&amp;gt;/&amp;lt;int:month&amp;gt;/&quot;, views.month_archive),
    path(&quot;articles/&amp;lt;int:year&amp;gt;/&amp;lt;int:month&amp;gt;/&amp;lt;slug:slug&amp;gt;/&quot;, views.article_detail),
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当我们访问不同层级下的路径时，Django 会自动匹配并将路径参数解析并传入到对应的视图函数中。&lt;/p&gt;
&lt;p&gt;此外，除了 &lt;code&gt;path()&lt;/code&gt; 函数之外，Django 还提供了 &lt;code&gt;re_path()&lt;/code&gt; 函数，它和 &lt;code&gt;path()&lt;/code&gt; 的功能是一样的，但是支持更复杂的正则表达式匹配：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from django.urls import path, re_path

from . import views

urlpatterns = [
    path(&quot;articles/2003/&quot;, views.special_case_2003),
    re_path(r&quot;^articles/(?P&amp;lt;year&amp;gt;[0-9]{4})/$&quot;, views.year_archive),
    re_path(r&quot;^articles/(?P&amp;lt;year&amp;gt;[0-9]{4})/(?P&amp;lt;month&amp;gt;[0-9]{2})/$&quot;, views.month_archive),
    re_path(
        r&quot;^articles/(?P&amp;lt;year&amp;gt;[0-9]{4})/(?P&amp;lt;month&amp;gt;[0-9]{2})/(?P&amp;lt;slug&amp;gt;[\w-]+)/$&quot;,
        views.article_detail,
    ),
]
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;在使用 &lt;code&gt;(?P&amp;lt;...&amp;gt;)&lt;/code&gt; 这种&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/howto/regex.html#non-capturing-and-named-groups&quot;&gt;命名组&lt;/a&gt;时，Django 会自动将匹配的值传入到对应的视图函数中，这些值可以在视图函数中通过 &lt;code&gt;kwargs&lt;/code&gt; 参数来获取或者需要在视图函数签名中添加对应的参数来接收，比如：&lt;code&gt;def article_detail(request, year, month, slug)&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;随着你的路由逐渐增多，那么清晰地划分或拆分路由就这项任务就会变得很重要。但 Django 提供了一个名为 &lt;code&gt;include()&lt;/code&gt; 的函数来辅助你对路由进行拆分，直接看对应用法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from django.urls import path, include

from . import views

extrapatterns = [
    path(&quot;order/&quot;, views.make_order),
    path(&quot;payment/&quot;, views.make_payment),
]

urlpatterns = [
    # ...
    # other routes
    path(&quot;extra/&quot;, include(extrapatterns)),
    path(&quot;ticket/&quot;, include([
        path(&quot;check/&quot;, views.check_ticket),
        path(&quot;cancel/&quot;, views.cancel_ticket),
    ])),
    path(&apos;article/&apos;, include(&apos;article.urls&apos;)),
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你既可以直接传入一个已经包含了 &lt;code&gt;path()&lt;/code&gt; 对象的列表，也可以传入一个包含对应模块的 URLconf 的字符串，Django 会自动为你整合这些路由。&lt;/p&gt;
&lt;p&gt;如果你觉得这样划分可能还是有些麻烦，那么在使用 &lt;code&gt;path()&lt;/code&gt; 或 &lt;code&gt;re_path()&lt;/code&gt;设定路由时还可以使用 &lt;code&gt;name&lt;/code&gt; 参数来为对应的路由指定别名，这样就可以使用 &lt;code&gt;reverse()&lt;/code&gt; 函数来进行反向匹配，帮助我们快速找到对应的 URL 路径，这在类似于使用指定链接或进行重定向时特别有用，下面是一个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# urls.py
from django.urls import path, include

from . import views

urlpatterns = [
    path(&quot;&quot;, views.index, name=&quot;index&quot;),
    path(&quot;auth/&quot;, include([
        path(&quot;login/&quot;, views.login, name=&quot;login&quot;),
        path(&quot;logout/&quot;, views.logout, name=&quot;logout&quot;),
    ])),
]

# views.py
from django.http import HttpResponseRedirect, reverse, HttpResponse

def index(request):
    if not request.user.is_authenticated:
        return HttpResponseRedirect(reverse(&quot;login&quot;))
    return HttpResponse(&quot;Hello, world!&quot;)

def login(request):
    if request.user.is_authenticated:
        return HttpResponseRedirect(reverse(&quot;index&quot;))
    # handle login logic
    return HttpResponse(&quot;Login&quot;)

def logout(request):
    if not request.user.is_authenticated:
        return HttpResponseRedirect(reverse(&quot;login&quot;))
    # handle logout logic
    return HttpResponseRedirect(reverse(&quot;index&quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;深入视图&lt;/h2&gt;
&lt;p&gt;了解了 Django 的路由配置之后，再让我们把话题转回到视图上。&lt;/p&gt;
&lt;h3&gt;获取请求信息&lt;/h3&gt;
&lt;p&gt;如果你有一定的计算机网络知识，那么你能快速理解或上手对大多数 Web 框架的路由视图或控制器使用方式，无非就是将来自于客户端请求在传输层之上的通过应用层协议传入服务器进行处理，而其中 HTTP 作为被广泛使用的应用层协议之一，则自然是为所有 Web 框架支持，包括 Django。&lt;/p&gt;
&lt;p&gt;从我们上述的 &lt;code&gt;index(request)&lt;/code&gt; 视图函数签名可以看出 Django 会将 HTTP 请求包装成一个特定的 &lt;a href=&quot;https://docs.djangoproject.com/en/5.1/ref/request-response/#django.http.HttpRequest&quot;&gt;HttpRequest 对象&lt;/a&gt; 传入。&lt;/p&gt;
&lt;p&gt;那么通过 &lt;code&gt;request&lt;/code&gt; 对象，我们可以拿到哪些信息呢？比如说最常用到的 HTTP 的请求方法、Cookie、HTTP 请求头、HTTP 请求体、路径以及查询参数等等。我们可以直接在视图函数中去通过 &lt;code&gt;request&lt;/code&gt; 对象来进行操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from django.http import JsonResponse

def index(request):
    method = request.method
    cookies = request.COOKIES
    headers = request.headers
    body = request.body
    path = request.path
    query = request.GET

    request_information = {
        &quot;method&quot;: method,
        &quot;cookies&quot;: cookies,
        &quot;headers&quot;: headers,
        &quot;body&quot;: body,
        &quot;path&quot;: path,
        &quot;query&quot;: query
    }

    return JsonResponse(request_information)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;类视图&lt;/h3&gt;
&lt;p&gt;除了函数视图外，Django 还提供了强大的类视图（Class-Based Views，简称 CBV）系统，在这里我们快速看一个示例来了解为什么会有这类视图的写法存在：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from django.views import View

# 基础类视图
class UserView(View):
    def get(self, request):
        # 处理 GET 请求
        return HttpResponse(&quot;GET request&quot;)

    def post(self, request):
        # 处理 POST 请求
        return HttpResponse(&quot;POST request&quot;)

    def dispatch(self, request, *args, **kwargs):
        # 自定义处理请求时的额外逻辑
        print(&quot;Before handling request&quot;)
        return super().dispatch(request, *args, **kwargs)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面例子中假设我们有一个关于用户的需求场景，通过 GET 请求时是获取用户列表，通过 POST 请求时是创建用户。根据 &lt;a href=&quot;https://en.wikipedia.org/wiki/REST&quot;&gt;RESTful&lt;/a&gt; 的设计思想，通常就会表现为同一个路由根据不同的方法来做出不同行为的区分：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /api/user
POST /api/user
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果是按照的函数视图写法那么就得要么写两个函数，要么在一个函数中根据请求方法来判断然后返回不同的响应结果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# FastAPI 示例
from fastapi import FastAPI

app = FastAPI()

@app.get(&quot;/api/user&quot;)
def get_user():
    return {&quot;message&quot;: &quot;GET request&quot;}

@app.post(&quot;/api/user&quot;)
def create_user():
    return {&quot;message&quot;: &quot;POST request&quot;}

# ------------------------------------

# Django 示例
from django.http import HttpResponse

def user(request):
    if request.method == &quot;GET&quot;:
        return HttpResponse(&quot;GET request&quot;)
    elif request.method == &quot;POST&quot;:
        return HttpResponse(&quot;POST request&quot;)
    else:
        return HttpResponse(&quot;Method Not Allowed&quot;, status=405)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到相比函数视图，CBV 提供了更好的代码复用性和可扩展性。以 HTTP 方法为分离点帮助我们快速地将不同方法逻辑分离同时又能保持代码的简洁性。&lt;/p&gt;
&lt;p&gt;只不过相比于函数视图，CBV 的对应的 URL 配置可能有点点小不同：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from django.urls import path
from .views import UserView

urlpatterns = [
    # ...
    # other routes
    path(&apos;user/&apos;, UserView.as_view()),
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在向路由表添加 CBV 时，我们需要手动调用 &lt;code&gt;as_view()&lt;/code&gt; 这个类方法，最后它将根据 HTTP 方法来调用 &lt;code&gt;dispatch()&lt;/code&gt; 方法动态派发到同名的视图函数上。&lt;/p&gt;
&lt;h3&gt;更多的类视图&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;View&lt;/code&gt; 类是 Django 中类视图基类，但也额外提供了一些封装好的视图类，比如最常用的 &lt;code&gt;ListView&lt;/code&gt; 和 &lt;code&gt;DetailView&lt;/code&gt; 类，它们就可以帮助我们快速地完成常见的列表和详情页的逻辑。你可以直接从 &lt;code&gt;django.views.generic&lt;/code&gt; 模块中导入它们。&lt;/p&gt;
&lt;p&gt;假设我们现在有一个关于博客文章的模型 &lt;code&gt;Article&lt;/code&gt;，我们将会有两个视图，一个是文章列表，一个是文章详情，在 Django 中就可以这么写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from django.views.generic import ListView, DetailView
from .models import Article
from django.urls import reverse_lazy

# 文章列表视图
class ArticleListView(ListView):
    model = Article
    template_name = &apos;myapp/article_list.html&apos;
    context_object_name = &apos;articles&apos;
    paginate_by = 10
    ordering = [&apos;-created_at&apos;]

# 文章详情视图
class ArticleDetailView(DetailView):
    model = Article
    template_name = &apos;myapp/article_detail.html&apos;
    context_object_name = &apos;article&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我们到 &lt;code&gt;myapp/urls.py&lt;/code&gt; 文件中添加两个路由，它们的代码如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from django.urls import path
from .views import ArticleListView, ArticleDetailView

urlpatterns = [
    # ...
    # other routes
    path(&apos;article/list/&apos;, ArticleListView.as_view(), name=&apos;article-list&apos;),
    path(&apos;article/&amp;lt;int:pk&amp;gt;/&apos;, ArticleDetailView.as_view(), name=&apos;article-detail&apos;),
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样当我们访问 &lt;code&gt;http://127.0.0.1:8000/myapp/article/list/&lt;/code&gt; 时，就会显示文章列表，访问 &lt;code&gt;http://127.0.0.1:8000/myapp/article/1/&lt;/code&gt; 时，就会显示文章详情。&lt;/p&gt;
&lt;p&gt;默认情况下，以上通用类视图都会和 HTML 模板文件结合到一起，由 Django 帮我们渲染到浏览器上，这也是 Django 的默认行为。如果是在前后端分离的场景下，就需要我们自己去手动扩展，后续我们可以直接使用第三方的 Django 扩展包来帮助我们更好地专注于 API 开发上而无需手动定制。&lt;/p&gt;
&lt;p&gt;但在使用第三方扩展包之前先让我们来看看如何自己手动扩展。简单来说，你可以用 Mixin 类覆盖原有类的 &lt;code&gt;render_to_response()&lt;/code&gt; 方法，然后按照 MRO 来摆放类的继承顺序即可。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.python.org/zh-cn/3.13/howto/mro.html&quot;&gt;MRO&lt;/a&gt; 是 Python 多重继承的一个特性，它是按照从左到右的继承顺序来进行属性或方法的查找，最先被找的属性或方法则会被优先使用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;from django.views.generic import ListView
from django.http import JsonResponse
from .models import Article

class JsonResponseMixin:
    def render_to_response(self, context):
        data = {
            &apos;count&apos;: len(context[&apos;object_list&apos;]),
            &apos;articles&apos;: list(context[&apos;object_list&apos;].values())
        }
        return JsonResponse(data)

class ArticleListView(JsonResponseMixin, ListView):
    model = Article
    context_object_name = &apos;articles&apos;
    paginate_by = 10
    ordering = [&apos;-created_at&apos;]

class ArticleDetailView(JsonResponseMixin, DetailView):
    model = Article
    context_object_name = &apos;article&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;于是乎我们的 &lt;code&gt;Article&lt;/code&gt; 模型就能分别以 JSON 格式返回数据了。&lt;/p&gt;
&lt;h2&gt;中间件系统&lt;/h2&gt;
&lt;p&gt;最后还要讲一讲 Django 的中间件。它是我们在使用 Django 时会经常碰到的一个概念，可以帮助我们在请求和响应处理过程中做出一些额外的逻辑处理，减少重复性代码的同时提高代码的可维护性。&lt;/p&gt;
&lt;p&gt;那么什么是中间件？中间件一词是由 Middleware 翻译而来，也可以叫中介层，是一类提供系统软件和应用软件之间连接、便于软件各部件之间的沟通的软件，应用软件可以借助中间件在不同的技术架构之间共享信息与资源。[1]&lt;/p&gt;
&lt;p&gt;简单理解就是：在开发&lt;strong&gt;过程中&lt;/strong&gt;用到组件、服务等都可以算作中间件范畴。比如我们在查询数据库前套一层 Redis 缓存，又或者面对某些长耗时任务先推到 Kafka 消息队列里，这里的 Redis 和 Kafka 就是中间件。&lt;/p&gt;
&lt;p&gt;那么对于大多数具备了中间件机制的 Web 框架而言——比如 Django——你可以借助中间件来实现一些常见的功能，比如日志记录、用户鉴权等。&lt;/p&gt;
&lt;h3&gt;如何添加中间件&lt;/h3&gt;
&lt;p&gt;Django 开箱自带了一系列的中间件，你可以在 Django 工程项目的同名模块下找到对应的 &lt;code&gt;settings.py&lt;/code&gt; 文件，当中的 &lt;code&gt;MIDDLEWARE&lt;/code&gt; 一项配置就列出了 Django 默认的中间件有哪些：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# other settings...

MIDDLEWARE = [
    &apos;django.middleware.security.SecurityMiddleware&apos;,
    &apos;django.contrib.sessions.middleware.SessionMiddleware&apos;,
    &apos;django.middleware.common.CommonMiddleware&apos;,
    &apos;django.middleware.csrf.CsrfViewMiddleware&apos;,
    &apos;django.contrib.auth.middleware.AuthenticationMiddleware&apos;,
    &apos;django.contrib.messages.middleware.MessageMiddleware&apos;,
    &apos;django.middleware.clickjacking.XFrameOptionsMiddleware&apos;,
]

# other settings...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到它们都是以某个 &lt;code&gt;包名.模块.中间件类名&lt;/code&gt; 的格式来进行引入，所以后续我们想要添加第三方库的中间件或者自定义的中间件时也如法炮制即可。&lt;/p&gt;
&lt;p&gt;假设我们在 &lt;code&gt;myapp&lt;/code&gt; 应用下创建了一个名为 &lt;code&gt;middleware.py&lt;/code&gt; 的文件，然后在 &lt;code&gt;settings.py&lt;/code&gt; 文件中添加如下配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MIDDLEWARE = [
    &apos;django.middleware.security.SecurityMiddleware&apos;,
    &apos;django.contrib.sessions.middleware.SessionMiddleware&apos;,
    &apos;django.middleware.common.CommonMiddleware&apos;,
    &apos;django.middleware.csrf.CsrfViewMiddleware&apos;,
    &apos;django.contrib.auth.middleware.AuthenticationMiddleware&apos;,
    &apos;django.contrib.messages.middleware.MessageMiddleware&apos;,
    &apos;django.middleware.clickjacking.XFrameOptionsMiddleware&apos;,
+   &apos;myapp.middleware.CustomMiddleware&apos;,  # 自定义中间件
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Django 添加中间件时，顺序很重要。因为 Django 的中间件机制就好像一个洋葱模型，由上自下层层嵌套，在执行时你可以把它想象成是一个 FIFO 队列（First In, First Out），因此最先添加的中间件会先被执行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;direction: right

# Define styles
style.fill: &quot;#f5f5f5&quot;
style.stroke: &quot;#333&quot;
style.font-size: 14
style.stroke-width: 2
style.border-radius: 4

# Middleware layers (from outside to inside)
security: SecurityMiddleware {
  style.fill: &quot;#e3f2fd&quot;

  dots: &quot;...&quot; {
    style.fill: &quot;#f5f5f5&quot;
    style.font-size: 20

    custom: CustomMiddleware {
      style.fill: &quot;#f3e5f5&quot;

      view: View {
        shape: circle
        style.fill: &quot;#fff&quot;
        style.stroke-width: 3
      }
    }
  }
}

# Request flow labels
req1: &quot;1. __call__() before get_response&quot; {
  style.stroke-width: 0
  style.font-size: 12
}

req2: &quot;2. process_view()&quot; {
  style.stroke-width: 0
  style.font-size: 12
}

req3: &quot;3. get_response(request)&quot; {
  style.stroke-width: 0
  style.font-size: 12
}

# Response flow labels
res1: &quot;4. process_template_response()/process_exception()&quot; {
  style.stroke-width: 0
  style.font-size: 12
}

res2: &quot;5. __call__() after get_response&quot; {
  style.stroke-width: 0
  style.font-size: 12
}

# Positioning and connections
req1 -&amp;gt; security: &quot;&quot;
security.dots -&amp;gt; req2: &quot;&quot;
security.dots.custom -&amp;gt; req3: &quot;&quot;
security.dots.custom.view -&amp;gt; res1: &quot;&quot;
res1 -&amp;gt; res2: &quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以如果你想把某个第三方或者自定义中间件的优先级调高，那么你最好将其放在最前面；而 Django 内置的中间件则有对应的&lt;a href=&quot;https://docs.djangoproject.com/zh-hans/5.1/ref/middleware/#middleware-ordering&quot;&gt;顺序说明&lt;/a&gt;，一般情况下我们不会轻易调整。&lt;/p&gt;
&lt;p&gt;你可能注意到了上图中标识了几个函数名称 &lt;code&gt;__call__()&lt;/code&gt;、&lt;code&gt;process_view()&lt;/code&gt;、&lt;code&gt;get_response()&lt;/code&gt; 和 &lt;code&gt;process_template_response()&lt;/code&gt;/&lt;code&gt;process_exception()&lt;/code&gt;，它们则是新版本 Django 中间件的流程 API，所以如果我们要自定义中间件就可以从它们开始入手。&lt;/p&gt;
&lt;h3&gt;自定义中间件&lt;/h3&gt;
&lt;p&gt;Django 提供了两种方式来让你自定义中间件，即函数中间件（Funcion-Based Middleware）和类中间件（Class-Based Middleware）。它们都需要接受一个 &lt;code&gt;get_response&lt;/code&gt; 的可调用对象，并返回相应的响应结果。&lt;/p&gt;
&lt;p&gt;现在假设我们需要一个用于记录请求信息的的中间件，每次都会记录包含请求的路径、HTTP 方法、请求 ID 等信息。&lt;/p&gt;
&lt;h4&gt;函数中间件&lt;/h4&gt;
&lt;p&gt;用函数式写法就需要你写成一个闭包的形式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def LoggingMiddleware(get_response):
    # 可以在这里写一些初始化配置的逻辑
    mylog = somesdk.get_tracer()

    def middleware(request):
        # 可以在这里写一些处理请求/响应前的逻辑
        # 比如：在日志记录器上添加一些上下文信息
        nonlocal mylog
        log = mylog.bind(request_id=uuid4(), method=request.method, path=request.path)
        response = get_response(request)

        # 可以在这里写一些处理请求/响应后**但在返回之前**的逻辑
        # 比如：清理上下文信息避免干扰另一个请求
        log.unbind()

        return response

    return middleware
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着只需要在你的 &lt;code&gt;settings.py&lt;/code&gt; 中引入该中间件即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MIDDLEWARE = [
    &apos;django.middleware.security.SecurityMiddleware&apos;,
    &apos;django.contrib.sessions.middleware.SessionMiddleware&apos;,
    &apos;django.middleware.common.CommonMiddleware&apos;,
    &apos;django.middleware.csrf.CsrfViewMiddleware&apos;,
    &apos;django.contrib.auth.middleware.AuthenticationMiddleware&apos;,
    &apos;django.contrib.messages.middleware.MessageMiddleware&apos;,
    &apos;django.middleware.clickjacking.XFrameOptionsMiddleware&apos;,
+   &apos;myapp.middleware.LoggingMiddleware&apos;,
]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;类中间件&lt;/h4&gt;
&lt;p&gt;相比于函数中间件而言，类中间件就是传统 OOP 的写法，但逻辑基本类似。不过流程图上提到的 &lt;code&gt;process_xxx&lt;/code&gt; 之类的钩子方法仅限于类中间件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class LoggingMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
        # 可以在这里写一些初始化配置的逻辑
        self.mylog = somesdk.get_tracer()

    def __call__(self, request):
        # 可以在这里写一些处理请求/响应前的逻辑
        # 比如：在日志记录器上添加一些上下文信息
        log = self.mylog.bind(request_id=uuid4(), method=request.method, path=request.path)
        response = self.get_response(request)

        # 可以在这里写一些处理请求/响应后**但在返回之前**的逻辑
        # 比如：清理上下文信息避免干扰另一个请求
        log.unbind()

        return response

    def process_view(self, request, view_func, view_args, view_kwargs):
        # 可以在这里写一些处理请求/响应前的逻辑，甚至修改入参函数等
        view_kwargs[&apos;boom&apos;] = True
        self.mylog.info(&apos;process_view&apos;, view_func=view_func, view_args=view_args, view_kwargs=view_kwargs)

        return view_func(request, *view_args, **view_kwargs)

    def process_exception(self, request, exception):
        exc = &apos;Oh no! Something went wrong!&apos;
        self.mylog.exception(exc, exc_info=exception)

        raise ValueError(exc) from exception
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;本章我们在了解了传统的 MVC 模式与 Django 的 MTV 模式后深入介绍了 Django 中的视图、路由与中间件这三个核心概念。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;视图。视图即处理 HTTP 请求的函数，可以是函数视图或者类视图。可以说我们所有的逻辑几乎都放在了视图中。&lt;/li&gt;
&lt;li&gt;路由。路由就是定义了 URL 和视图关系的表，在 Django 中主要以集中管理和配置的方式来组织你路由，这对于大型项目来说是十分方便的。&lt;/li&gt;
&lt;li&gt;中间件。中间件本质上是一种处理请求/响应前后的逻辑，在 Django 中可以是函数中间件或者类中间件，它们都需要接受一个 &lt;code&gt;get_response&lt;/code&gt; 的可调用对象，并返回相应的响应结果。你可以基于中间件来拆分一些公共的逻辑，比如日志记录，权限验证等，避免在不同的视图中重复写相同的逻辑，提高代码的复用性和可维护性。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;练习题&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;创建一个随机生成 5 个密码的接口 &lt;code&gt;/generate_passwords&lt;/code&gt;，要求：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;可接收指定密码长度的 &lt;code&gt;length&lt;/code&gt; 参数，最短不小于 8，最长不超过 16，如果没有传该参数那么默认就返回长度为 8 的密码&lt;/li&gt;
&lt;li&gt;返回一个 JSON 格式的响应，包含密码列表&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;提示：可以使用 Python 的 &lt;a href=&quot;https://docs.python.org/zh-cn/3.13/library/secrets.html&quot;&gt;secrets&lt;/a&gt; 模块来生成随机字符串密码&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;我现在需要实现一个通用的中间件 &lt;code&gt;RequestMiddleware&lt;/code&gt;。它可以记录每次请求的路径、HTTP 方法和头部信息；同时，记录对应视图函数的处理时间，并为每个请求的响应新增一个请求 ID（使用 UUID），然后将它们作为 &lt;code&gt;X-Process-Time&lt;/code&gt; 和 &lt;code&gt;X-Request-ID&lt;/code&gt; 的头部信息添加到响应并返回。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Django 命令行工具：基础用法与自定义扩展</title><link>https://devlike.top/posts/django-command-line-tools/</link><guid isPermaLink="true">https://devlike.top/posts/django-command-line-tools/</guid><description>掌握 Django 命令行工具的核心用法，包含项目创建、应用管理到自定义命令的完整教程。提供实用示例和最佳实践，助你提升开发效率</description><pubDate>Fri, 04 Oct 2024 05:40:57 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Django 默认为开发者集成了一系列的命令行，方便开发者去创建、开发以及维护 Django 项目及当中的不同子模块；如果你想要自定义也相当方便，只需要按对应约定去设定目录结构并编写命令的业务逻辑即可。Django 的项目结构通常都是一种非常固定的模式，可以让多人协作尽可能保持同样风格。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;django-admin&lt;/code&gt; 和 &lt;code&gt;manage.py&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;你可以利用 &lt;a href=&quot;https://docs.astral.sh/uv/&quot;&gt;uv&lt;/a&gt; 这样的现代化工具来创建 Python 虚拟环境并安装依赖，再不济直接使用内置的 &lt;code&gt;venv&lt;/code&gt; 模块并搭配 &lt;code&gt;pip&lt;/code&gt; 命令安装依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv add django

# or use pip
pip install django
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着你就能在当前解释器路径中找到 &lt;code&gt;django-admin&lt;/code&gt;，我们可以用该命令来创建一个 Django 项目工程，比如我们创建一个 &lt;code&gt;djangotutor&lt;/code&gt; 项目：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;django-admin startproject djangotutor
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;startproject&lt;/code&gt; 通常命令用于帮我们在当前路径下创建一个 Django 工程项目，当中包含了许多模版代码文件。&lt;/p&gt;
&lt;p&gt;所以运行上述命令后，在你当前路径下就会多了一个 &lt;code&gt;djangotutor&lt;/code&gt; 的文件夹，打开长这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;djangotutor
├── djangotutor
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py

2 directories, 6 files
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;django-admin&lt;/code&gt; 是一个全局命令，但在具体工程项目中我们主要使用的是 &lt;code&gt;manange.py&lt;/code&gt; 管理并运行其他命令行，这样不同的 Django 项目就不会干扰。&lt;/p&gt;
&lt;p&gt;所以当你切换到 &lt;code&gt;djangotutor&lt;/code&gt; 路径下通过它来执行 &lt;code&gt;runserver&lt;/code&gt; 命令就可以启动我们的 Django 项目了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Django，启动！&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv run manange.py runserver

# or native python interpreter
python manange.py runserver
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行之后默认会在 8000 端口启动一个本地开发调试的服务器，你可以在浏览器中打开 &lt;code&gt;http://localhost:8000/&lt;/code&gt; 来访问。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;需要注意的是，&lt;code&gt;runserver&lt;/code&gt; 命令仅适用于本地开发调试的服务器，在生产环境下我们可能需要类似于 &lt;a href=&quot;https://gunicorn.org/&quot;&gt;gunicorn&lt;/a&gt; 这样的 WSGI 或 ASGI 服务器才行。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;自定义命令&lt;/h2&gt;
&lt;p&gt;除了内置的命令行之外，Django 还允许我们自定义命令，并通过 &lt;code&gt;manange.py&lt;/code&gt; 来统一管理。但前提是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;我们需要自己创建一个应用，然后在应用目录下创建一个 &lt;code&gt;management&lt;/code&gt; 文件夹和 &lt;code&gt;commands&lt;/code&gt; 文件夹；&lt;/li&gt;
&lt;li&gt;在 Django 根项目设置中添加应用。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;现在先让我们直接基于 &lt;code&gt;manage.py&lt;/code&gt; 来运行 &lt;code&gt;startapp&lt;/code&gt; 命令，它会在当前的 Django 工程目录下新建一个应用模块：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv run manage.py startapp myapp

# or native python interpreter
python manage.py startapp myapp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后的文件目录树如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.
├── djangotutor
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── manage.py
└── myapp
    ├── __init__.py
    ├── admin.py
    ├── apps.py
    ├── migrations
    │   └── __init__.py
    ├── models.py
    ├── tests.py
    └── views.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着我们要在 &lt;code&gt;myapp&lt;/code&gt; 应用中自定义命令，即根据约定式写法创建一个 &lt;code&gt;myapp/management/commands/seed.py&lt;/code&gt; 文件作为模拟填充数据库示例数据的命令。&lt;/p&gt;
&lt;p&gt;下面是一个很简单的伪代码示例，我们只需要基于 Django 为我们封装好的 &lt;code&gt;BaseCommand&lt;/code&gt; 基类来实现我们的命令即可，其中 &lt;code&gt;add_arguments&lt;/code&gt; 方法用于添加命令行参数，而 &lt;code&gt;handle&lt;/code&gt; 方法用于执行命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from django.core.management.base import BaseCommand


class Command(BaseCommand):
    help = &quot;Seed the database with initial data.&quot;

    def add_arguments(self, parser):
        parser.add_argument(
            &quot;-n&quot;,
            &quot;--number&quot;,
            type=int,
            help=&quot;Number of people to seed. Defaults to 3.&quot;,
            default=3,
        )

    def handle(self, *args, **options):
        data = []
        self.stdout.write(&quot;Seeding the database...&quot;)
        for i in range(options[&quot;number&quot;]):
            data.append(i)
        self.stdout.write(&quot;Done!&quot;)
        self.stdout.write(f&quot;Seeding data: {data}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在我们到 &lt;code&gt;djangotutor/settings.py&lt;/code&gt; 根项目设置文件中找到 &lt;code&gt;INSTALL_APPS&lt;/code&gt; 配置项，然后将我们的应用名称添加进去即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...

INSTALLED_APPS = [
    ...
    &quot;myapp&quot;,
]

...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在当你再次调用 &lt;code&gt;manage.py&lt;/code&gt; 时就能查找到 &lt;code&gt;seed&lt;/code&gt; 命令了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv run manage.py --help

Type &apos;manage.py help &amp;lt;subcommand&amp;gt;&apos; for help on a specific subcommand.

Available subcommands:

[auth]
    changepassword
    createsuperuser

[contenttypes]
    remove_stale_contenttypes

[django]
    check
    compilemessages
    createcachetable
    dbshell
    diffsettings
    dumpdata
    flush
    inspectdb
    loaddata
    makemessages
    makemigrations
    migrate
    optimizemigration
    sendtestemail
    shell
    showmigrations
    sqlflush
    sqlmigrate
    sqlsequencereset
    squashmigrations
    startapp
    startproject
    test
    testserver

[myapp]
    seed

[sessions]
    clearsessions

[staticfiles]
    collectstatic
    findstatic
    runserver
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后让我们运行并验证一下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv run manage.py seed
Seeding the database...
Done!
Seeding data: [0, 1, 2]

uv run manage.py seed -n 10
Seeding the database...
Done!
Seeding data: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;嗒哒！成功运行。&lt;/p&gt;
&lt;p&gt;当然了，如果你觉得用 OOP 的方式来写命令行有点繁琐，恰好又有用过 &lt;a href=&quot;https://click.palletsprojects.com/en/8.1.x/&quot;&gt;Click&lt;/a&gt; 这个库，那么你可以额外集成 &lt;a href=&quot;https://github.com/GaretJax/django-click&quot;&gt;django-click&lt;/a&gt; 或 &lt;a href=&quot;https://github.com/django-commons/django-typer&quot;&gt;django-typer&lt;/a&gt; 其中之一来提高编写命令行的效率。&lt;/p&gt;
&lt;p&gt;这里以 django-click 为例，上述的命令可以改成这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import djclick as click

@click.command()
@click.option(
    &quot;-n&quot;,
    &quot;--number&quot;,
    type=int,
    help=&quot;Number of people to seed. Defaults to 3.&quot;,
    default=3,
)
def seed(number):
    data = []
    print(&quot;Seeding the database...&quot;)
    for i in range(number):
        data.append(i)
    print(&quot;Done!&quot;)
    print(f&quot;Seeding data: {data}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;练习题&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;现在请你从头开始搭建一个名为 &lt;code&gt;ailiaili&lt;/code&gt; 的 Django 项目，并成功将其运行在本地的 8080 端口上（如果端口被占用可以换一个）;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 &lt;code&gt;ailiaili&lt;/code&gt; 项目中创建一个名为 &lt;code&gt;articles&lt;/code&gt; 的应用，并为其添加一个 &lt;code&gt;create&lt;/code&gt; 命令，然后我们能通过 Django 命令行快速地创建一篇文章。这里我给出一个创建文章的基本逻辑代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from django.conf import settings

ARTICLES_DIR = settings.BASE_DIR / &quot;articles&quot;

def create_article(title: str, content: str):
    &quot;&quot;&quot;Mock article creation.
    This function will write a new article to local file.
    &quot;&quot;&quot;
    if not ARTICLES_DIR.exists():
        ARTICLES_DIR.mkdir()
    filepath = ARTICLES_DIR / f&quot;{title}.md&quot;
    filepath.write_text(content)
    print(f&quot;Created article: {filepath.absolute()}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>都 20xx 年了，为什么你开始选择 Django？</title><link>https://devlike.top/posts/django-why-django/</link><guid isPermaLink="true">https://devlike.top/posts/django-why-django/</guid><description>深入分析 Django 在微服务时代的优势与价值，探讨全栈框架 vs 微服务的选择。适合想快速构建 Web 应用的 Python 开发者，包含实际案例分析</description><pubDate>Mon, 25 Mar 2024 14:34:00 GMT</pubDate><content:encoded>&lt;p&gt;在经过长达一年的「躺平期」后我在 2023 年 11 月份入职到了深圳的一家初创公司担任 Python 开发工程师，彼时部门规模不大，正处于对 AI 产品的 POC 阶段（Proof of Concept，概念验证），根据前期讨论的需求很快便开始进入到 MVP 的开发阶段。&lt;/p&gt;
&lt;p&gt;由于项目并不是传统的 Web 应用，作为后端只需要提供对应的 API 给安卓端调用即可，那么毫无疑问我选择了目前最流行的 &lt;a href=&quot;https://fastapi.tiangolo.com/&quot;&gt;FastAPI&lt;/a&gt;。（之所以没有选择 Flask 主要还是因为 FastAPI 自带了基于 Pydantic 的类型校验和 API 文档，而使用 Flask 还需要额外配置插件）&lt;/p&gt;
&lt;p&gt;开发的进展很快，一直以「Ship fast and iterate」的节奏在持续推进，但随着多次需求的修改逐渐向一个中型项目倾斜，项目的 Python 代码库已经快接近 1w 行。开发体验上的问题也随之显现：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;我们需要经常地对一个 SQLAlchemy 的 ORM 模型结果编写对应的 Pydantic 模型进行转化，虽然 FastAPI 的作者正开发 &lt;a href=&quot;https://github.com/fastapi/sqlmodel&quot;&gt;SQLModel&lt;/a&gt; 来填补这一空缺，但就目前进度而言很难在生产环境上使用；&lt;/li&gt;
&lt;li&gt;在前期开发阶段，我们会经常需要对数据库架构进行调整，包括字段的调整、表的增删改，使用 &lt;a href=&quot;https://alembic.sqlalchemy.org/en/latest/&quot;&gt;Alembic&lt;/a&gt;，除了要进行配置之外，还需要自己维护一个数据库迁移脚本；&lt;/li&gt;
&lt;li&gt;为了应对不同开发需求或开发需要，我们需要一个统一的命令行管理工具，不希望我写 Makefile，而另一个同事编写 Python 脚本或 Shell 命令，而就得额外安装一个 &lt;a href=&quot;https://click.palletsprojects.com/en/8.1.x/&quot;&gt;Click&lt;/a&gt; 或者是 &lt;a href=&quot;https://typer.tiangolo.com/&quot;&gt;Typer&lt;/a&gt;，Python 自带的 argparse 又太过原始，得额外封装；&lt;/li&gt;
&lt;li&gt;……&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;为了解决上述开发体验上的痛点，我们不得不安装一个又一个依赖并进行额外的配置。&lt;/p&gt;
&lt;p&gt;但对使用 Python 开发的朋友们来说这些场景却又再熟悉不过了，因为解决方案的线索似乎都指向了它——Django。&lt;/p&gt;
&lt;p&gt;于是在我的推特上我发表了如下感言：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;公司的项目用 FastAPI 写了快一年，看着它从简单的小项目逐渐变成 Django 的模样，我就知道为什么 Django 的含金量还在上升。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因为在 Django 及其生态中：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用 &lt;a href=&quot;https://github.com/encode/django-rest-framework&quot;&gt;django-rest-framework&lt;/a&gt; 来实现 ORM 和校验模型的转换，不需要再重复编写字段；&lt;/li&gt;
&lt;li&gt;Django 本身就自带了 ORM 及模型迁移管理，可以无缝使用 ORM，所以也不需要额外安装 SQLAlchemy 以及 Alembic 了（顺便吐槽一句 SQLAlchemy 的文档是最难读懂的），也不需要再 SQLAlchemy Core 以及 SQLAlchemy ORM 两种风格种切来切去了；&lt;/li&gt;
&lt;li&gt;Django 提供了一种简单快速地方式让你&lt;a href=&quot;https://docs.djangoproject.com/en/5.1/howto/custom-management-commands/&quot;&gt;自定义命令&lt;/a&gt;，然后在运行时自动帮你将命令注册到 &lt;code&gt;manager.py&lt;/code&gt; 并用它统一管理。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;况且，Django 的生态是无敌的。&lt;/p&gt;
&lt;p&gt;也许有人诟病 Django 的性能问题，但在大多数时候可能对小公司来说性能往往都遥不可及，在日活连 1000 都不到、QPS 不过半百的阶段下空谈性能问题完全是杞人忧天，何况要进行性能优化时数据库、网关这类基础性的东西反而才是首要，而作为应用层来说编程语言永远屈居末位。&lt;/p&gt;
&lt;p&gt;于是我开始重新审视 Django、PHP 的 &lt;a href=&quot;https://laravel.com/&quot;&gt;Laravel&lt;/a&gt;、Ruby 的 &lt;a href=&quot;https://rubyonrails.org/&quot;&gt;Ruby on Rails&lt;/a&gt; 以及 Java 的 &lt;a href=&quot;https://spring.io/projects/spring-framework&quot;&gt;Spring&lt;/a&gt; 等这些全栈式框架的意义以及它们在如今时代里的价值所在。&lt;/p&gt;
&lt;p&gt;并且我打算重新开始学习如何使用 Django。&lt;/p&gt;
&lt;p&gt;一方面我本身就以 Python 为主，但最开始并没有深入去了解如何使用 Django；另一方面根据费曼学习法我打算通过「教」会别人来让自己掌握如何使用 Django。&lt;/p&gt;
&lt;p&gt;在这个过程中我将结合这么多年 CRUD 的经验来学习它并记录下一系列的学习教程，而不是完全照搬 Django 官方的文档内容，希望能对其他像我一样学习全栈式框架的朋友们有所帮助。&lt;/p&gt;
&lt;p&gt;需要说明的是，本教程可能仅适用于已经有了其他 Web 开发经验或者 Python 开发经验的朋友；对于初学者而言，我更推荐你先看看 Django 的&lt;a href=&quot;https://docs.djangoproject.com/en/5.1/intro/tutorial01/&quot;&gt;官方文档&lt;/a&gt;，然后再去看看一位名为 Matt Layman 的外国友人所写的 &lt;a href=&quot;https://www.mattlayman.com/understand-django/&quot;&gt;Django 教程&lt;/a&gt;。最后如果有兴趣了再回头来看看我写的东西，就当个快速查阅的学习小册。&lt;/p&gt;
&lt;p&gt;让我们开始吧！&lt;/p&gt;
</content:encoded></item><item><title>如何通过 Astro 文档翻译获得 250$奖励</title><link>https://devlike.top/posts/astro-docs-translation-open-source/</link><guid isPermaLink="true">https://devlike.top/posts/astro-docs-translation-open-source/</guid><description>想参与开源项目却不知从何下手？本文详细介绍如何通过 Astro 文档翻译入门开源贡献，获得实战经验和 250$奖励。包含完整工作流程、翻译技巧和审核要点</description><pubDate>Sat, 27 Jan 2024 03:35:07 GMT</pubDate><content:encoded>&lt;h2&gt;文档翻译——你的开源起点&lt;/h2&gt;
&lt;p&gt;可能有很多人想要参与开源做一些实质性的项目，却不知道该从哪开始。而&lt;strong&gt;文档翻译&lt;/strong&gt;是一个很好的起点，因为它不需要你有很多的技术基础，只需要你有一定的英语基础，能够看懂英文文档，然后翻译成中文就可以了。这就有点类似于我们看电影时的字幕组角色。&lt;/p&gt;
&lt;p&gt;为什么文档需要翻译？&lt;/p&gt;
&lt;p&gt;作为一名程序员，想想你在学习一门技术时，如果有中文文档，那么你会选择看中文文档还是英文文档？我想大部分人都会选择看中文文档。毕竟我们母语本身就是中文，所以能更快阅读和理解当中的内容；但当一个文档只有英文版本时，除了需要借助翻译工具之外，甚至还需要我们的脑子还将其「编译」成中文才能进一步理解文档所表达的意思。&lt;/p&gt;
&lt;p&gt;文档翻译是一件双赢的事情，不仅是满足使用者（即程序员）需要，也有利于开源项目的进一步推广，毕竟 i18n（internationalization，国际化）也是衡量一个开源项目质量的重要参考。但这个事情单靠项目作者是不可能完成对所有语言的支持，因此就得借助开源社区的力量。&lt;/p&gt;
&lt;p&gt;我最开始翻译的文档是为 Python 中目前知名的 TUI 框架 &lt;a href=&quot;https://textual.textualize.io/&quot;&gt;Textual&lt;/a&gt; 翻译其 Tutorial 文档，告诉用户该如何快速上手 Textual。可惜的是 Textual 当时还处于比较早期的阶段，并没有发布稳定版本，所以理所当然我这个 &lt;a href=&quot;https://github.com/Textualize/textual/pull/1015&quot;&gt;PR&lt;/a&gt;（Pull Request）最终就被毙掉了。&lt;/p&gt;
&lt;p&gt;这也提醒每个在参与开源项目的人，在提交 PR 之前最好先通过 Issue 或者其他方式与项目作者进行沟通，避免重复性工作或者做无用功。&lt;/p&gt;
&lt;h2&gt;给 Astro 贡献翻译&lt;/h2&gt;
&lt;p&gt;2022 年到 2023 年间我处于裸辞躺平的状态，相比于工作来说会悠闲和轻松不少，也让我探索了不少工作之外的人生道路，比如创作付费栏目和写作成为自己的第一副业，又比如给 Astro 贡献翻译以消磨时光。&lt;/p&gt;
&lt;p&gt;最开始我也并没有想过通过 Astro 贡献翻译来获得奖励金，只是恰好碰到了 &lt;a href=&quot;https://liruifengv.com/&quot;&gt;liruifengv&lt;/a&gt; 在推特上发布一则推文问是否有人愿意帮忙翻译 Astro 的文档，我就顺手回复了一下，没想到后来就真的开始了翻译。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://assets.devlike.top/blog/2024/01/%E6%88%AA%E5%B1%8F2024-01-28%2011.38.31-308b6cdc16332cd346e2139ae21cf421.png&quot; alt=&quot;liruifengv 的推文&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当天就有好多位朋友参与到了翻译中，liruifengv 直接单独开了一个 &lt;a href=&quot;https://github.com/liruifengv/docs/issues/1&quot;&gt;Issue&lt;/a&gt; 方便大家认领翻译任务，只不过那时我比较闲没有上班所以就「全力输出」完成了不少翻译任务，多线输出。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://assets.devlike.top/blog/2024/01/3741690443168_.pic-ff50f17de4717d6741292287fc033e26.jpg&quot; alt=&quot;My git branch&quot; /&gt;&lt;/p&gt;
&lt;p&gt;经过所有人的努力，我们仅大概两周的时间 Astro 的中文文档就已经和英文文档保持了绝对的同步，Astro 官方也对此进行了转发。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://assets.devlike.top/blog/2024/01/WeChat2c17936e9ce45e117f2000ccdb592995-37b4ab0c85e9cb10d29d1088272009cd.jpg&quot; alt=&quot;Astro re-post&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;翻译流程&lt;/h3&gt;
&lt;p&gt;除了时间充裕之外，翻译时我尽可能将流程标准化这样基本上每次就只是重复性地经历几个过程，而不需要太多的修改。&lt;/p&gt;
&lt;p&gt;首先，在翻译之前只需要访问 Astro 官方搭建的 &lt;a href=&quot;https://i18n.docs.astro.build/&quot;&gt;i18n 翻译状态追踪页面&lt;/a&gt;，查看中文文档哪些文档需要被更新或翻译。不得不说 Astro 官方的这套系统十分好用，它可以清楚地显示当前哪些文档需要被翻译又或是哪些文档的翻译需要被更新，以及每个文档的翻译进度如何。这也是 Astro 中文文档能在短短两个星期左右就完成翻译的重要原因之一。当然这个过程你可以做成自动化，类似于爬虫或者 RSS 订阅，这样你就可以在文档更新时第一时间得到通知。&lt;/p&gt;
&lt;p&gt;紧接就是在开源社区一套普遍的流程：Fork Astro 的&lt;a href=&quot;https://github.com/withastro/docs&quot;&gt;文档仓库&lt;/a&gt;（如果已 Fork 则省略），然后将你 Fork 后的仓库 Clone 到本地然后创建一个 Git 分支并在此之上进行翻译或更新，最后推送至 GitHub 上并创建 PR（Pull Request）等待其他人审核（Review）并合并到主分支。&lt;/p&gt;
&lt;p&gt;当然以下有一些特别的注意事项仅供参考：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在你着手开始之前，最好是先到 Astro 文档仓库的 Pull Request 页面筛选并查询一下&lt;strong&gt;看看是否有人已经在翻译了&lt;/strong&gt;，避免重复性工作或翻译冲突，如果没有人在翻译那么你就可以开始在本地进行翻译了；&lt;/li&gt;
&lt;li&gt;尽可能地将关联更新的文档放在一个 PR 中进行翻译或更新，方便追踪；&lt;/li&gt;
&lt;li&gt;偶尔要检查一下当前所翻译或更新文档的上游是否有更新，GitHub 提供了文件的&lt;a href=&quot;https://github.com/withastro/docs/commits/main/src/content/docs/en/guides/integrations-guide/alpinejs.mdx?since=2024-01-04T20:45:28.000Z&quot;&gt;历史提交记录页面&lt;/a&gt;方便你核对。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;可能以上的流程在从文字层面看起来会有些复杂，但实际上只要你熟悉了之后就很快能够快速地上手，这个过程也能帮助你加深对 Git 命令的掌握。&lt;/p&gt;
&lt;h3&gt;善用工具&lt;/h3&gt;
&lt;p&gt;除此之外，我在翻译或更新内容时也会用到一些工具进行辅助。因为我本身就是一个不折不扣的工具控，不论是在写前端还是后端项目，我总会希望通过某些工具链来帮助我提升代码质量，如 Linter 或者 Formatter 等。当然即便是写纯文本时也是如此，这也包括翻译文档。&lt;/p&gt;
&lt;p&gt;时至今日我一直在用 &lt;a href=&quot;https://github.com/huacnlee/autocorrect&quot;&gt;Autocorrect&lt;/a&gt; 来帮助我格式化 Markdown（或 MDX）中的内容，以实现更好的排版。它是一个类似于 &lt;a href=&quot;https://github.com/vinta/pangu.js&quot;&gt;盘古之白&lt;/a&gt; 的文本格式化工具，尤其适合 CJK 的文本格式化，它可以自动在中文和英文之间添加空格，并统一将英文标点符号转换成中文标点符号等。这个工具基于 Rust 编写，性能强劲并且提供了 VS Code 插件，即便我在进行长文创作时都能快速地对内容进行格式化。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/5518/192738752-89a9e4f5-75cb-40af-b84d-04889d22e834.png&quot; alt=&quot;引自 Autocorrect 官方&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当然这仅是我工具链中的一环，如果你对此感兴趣，可以阅读我早期在少数派上写过一篇的文章：&lt;a href=&quot;https://sspai.com/post/66883&quot;&gt;《如何在 VS Code 上配置一套趁手的写作环境》&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;除此之外就是使用 ChatGPT。毕竟在 AI 爆发元年，不使用 AI 工具的人会显得有些过于落伍。相信你或多或少已经在网上见识到了不少 AI 的能力展示，这里我也就不过多赘述。AI 在重复性内容上往往做的会比人类更好——比如翻译。因此在为 Astro 翻译文档时很大程度上我都会借助 ChatGPT 辅助，以加快我的翻译速度。&lt;/p&gt;
&lt;p&gt;以下是我在翻译时所使用的 Prompt：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;你是一个非常有帮助的 AI 翻译助手，可以将输入的英文内容翻译成中文。在翻译时需要保留原始内容的排版格式（通常为 Markdown 格式），并为两个中文字符之间的英文或其他符号添加空格，翻译时「you」需要翻译成「你」而不是「您」。

请不要生成其他额外的代码，只需要翻译即可！
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上的 Prompt 你仍可以进一步修改，或者使用 &lt;a href=&quot;https://weibo.com/1727858283/NlsDSpPaa&quot;&gt;宝玉 xp 的 Prompt 技巧&lt;/a&gt; 以提升 ChatGPT 的翻译质量，告别「机翻感」。&lt;/p&gt;
&lt;p&gt;看到这有人会说，那你不就是作弊了吗？&lt;/p&gt;
&lt;p&gt;诚然，即便是如今的图文出版社编辑和译者在将外国书籍进行本土化翻译时，也是先借助必要的翻译工具进行粗略的机翻，然后再由人工进行精校。毕竟那么多内容，如果像中学做英语阅读理解那样去逐字逐句翻译，效率非常低下；同理，我借助 ChatGPT 来翻译也是如此，毕竟开源本不是我们的本职工作，都全凭个人热爱利用业余时间进行，所以如何提高个人时间的利用率是门方法论。&lt;/p&gt;
&lt;p&gt;不过如果你正打算利用翻译来提升你的英语水平，那么逐字逐句翻译是一个不错的选择，因为这样你可以更好地理解原文的含义，从而更好地进行翻译。&lt;/p&gt;
&lt;p&gt;需要指出的是，即便使用了 ChatGPT 得到的翻译内容也不是完美的，仍需要我们进一步去润色，比如在翻译&lt;a href=&quot;https://docs.astro.build/en/guides/deploy/aws/#_top&quot;&gt;《Deploy your Astro Site to AWS》&lt;/a&gt; 时，原文开头中有一段内容是这样说的：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;AWS is a full-featured web app hosting platform that can be used to deploy an Astro site.
Deploying your project to AWS requires using the AWS console. (Most of these actions can also be done using the AWS CLI). This guide will walk you through the steps to deploy your site to AWS starting with the most basic method. Then, it will demonstrate adding additional services to improve cost efficiency and performance.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;通过 ChatGPT 得到的内容是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;AWS 是一个功能全面的 web 应用托管平台，可以用来部署 Astro 网站。
将你的项目部署到 AWS 需要使用 AWS 控制台。（大部分这些操作也可以通过 AWS CLI 来完成）。本指南将带领你从最基本的方法开始，一步步部署你的网站到 AWS。然后，它将展示如何添加额外的服务以提高成本效率和性能。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;翻译无功无过，但对「it will demonstrate adding additional services to improve cost efficiency and performance.」这部分我觉得 ChatGPT 翻译得过于平实。因为「improve cost efficiency and performance」完全可以用 2023 年的一个网络热词来概括——&lt;strong&gt;降本增效&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;「降本增效」这一词颇具讽刺意味，不得不说这在 Astro 中文文档中增添了很多乐趣，相信当国内的程序员来读到这里时应该会不自觉地会心一笑。所以最后我就在 ChatGPT 的基础上进行了润色，将原本「它将展示如何添加额外的服务以提高成本效率和性能。」翻译成了「它将展示如何添加额外的服务以降本增效。」&lt;/p&gt;
&lt;h2&gt;结尾&lt;/h2&gt;
&lt;p&gt;在获得 Astro 社区奖励金之前，我和 liruifengv 也曾获得过 Astro 官方所发的一套包含了贴纸和棒球帽的周边，这也是我第一从开源中获得精神外的物质奖励。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://assets.devlike.top/blog/2024/01/40191694584809_.pic-95c8ed8b1d9dafe88f90b7607337b165.jpg&quot; alt=&quot;Astro swag&quot; /&gt;&lt;/p&gt;
&lt;p&gt;但没想到 2024 年初就收到了来自于 Astro 社区的奖励金，某种程度上也算是对我去年一段时间努力的认可。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://assets.devlike.top/blog/2024/01/%E6%88%AA%E5%B1%8F2024-01-24%2019.49.31-9fcaec473c2261b73d740f2f876d3f1b.png&quot; alt=&quot;Astro award&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当然这些都还是次要的，而给 Astro 贡献文档翻译这个过程所给我带来的意义才是最重要的：开源很酷，得到了别人的认可也很酷，能在世界一隅留下自己的足迹也很酷。&lt;/p&gt;
</content:encoded></item><item><title>JavaScript 中检测中文字符的正确姿势</title><link>https://devlike.top/posts/javascript-detect-chinese-characters/</link><guid isPermaLink="true">https://devlike.top/posts/javascript-detect-chinese-characters/</guid><description>深入解析如何在 JavaScript 中使用 Unicode 和正则表达式检测中文字符，包含完整代码示例和性能对比。从 Go 到 TypeScript 的实践经验分享</description><pubDate>Fri, 08 Sep 2023 07:38:12 GMT</pubDate><content:encoded>&lt;p&gt;最近在重新复习 TypeScript 的时候打算拿之前基于 Golang 写过的一个 &lt;a href=&quot;https://github.com/100gle/wordcounter&quot;&gt;wordcounter&lt;/a&gt; 项目用 TypeScript 进行重写，以提高熟练度。&lt;/p&gt;
&lt;p&gt;这个项目的核心功能就是对一篇内容中的中文字符进行统计，所以需要一个方法来检测中文字符。由于 Go 提供了良好的 Unicode 支持，可以直接使用 &lt;code&gt;unicode&lt;/code&gt; 标准库里的 &lt;code&gt;unicode.Han&lt;/code&gt; 字符集来检测中文字符，所以它也就是构成了这个项目的核心算法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func (c *TextCounter) Count(input interface{}) error {
	str := &quot;&quot;
	switch v := input.(type) {
	case string:
		str = v
	case []byte:
		str = string(v)
	}
	if str == &quot;&quot; {
		return errors.New(&quot;no input provided&quot;)
	}
	scanner := bufio.NewScanner(strings.NewReader(str))
	for scanner.Scan() {
		c.S.Lines++
		line := scanner.Text()
		for _, r := range line {
			c.S.TotalChars++
			if unicode.In(r, unicode.Han) {
				c.S.ChineseChars++
			} else {
				c.S.NonChineseChars++
			}
		}
	}
	if err := scanner.Err(); err != nil {
		return err
	}
	return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;简单来说就是只要某个字符在中文字符集中，那么计数加一。&lt;/p&gt;
&lt;p&gt;但在 JavaScript 中这似乎没有提供像 Go 这样标准库可以使用，所以通常如果要匹配中文字符集需要利用 Unicode 的正则表达式来进行匹配，比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/[\u4e00-\u9fa5]/.test(&quot;中文&quot;); // true
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;\u4e00&lt;/code&gt; 和 &lt;code&gt;\u9fa5&lt;/code&gt; 分别是常见中文字符集的开始和结束字符，这个正则表达式通常也适用于其他语言，如 Python。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;不过 Unicode 本身提供了对中文汉字的检测方式，即通过指定 &lt;a href=&quot;https://unicode.org/reports/tr24/&quot;&gt;&lt;code&gt;Script&lt;/code&gt; 属性&lt;/a&gt; 来实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/\p{Script=Han}/u.test(&quot;中文&quot;); // true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它不仅能匹配中文汉字，也能匹配其他 CJK 字符。&lt;/p&gt;
&lt;p&gt;所以最终我选定了这种方式来在 TypeScript 版本中实现中文字符的检测：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function count(input: string | Uint8Array): Error | null {
    let str = &quot;&quot;;
    if (typeof input === &quot;string&quot;) {
      str = input;
    } else if (input instanceof Uint8Array) {
      str = new TextDecoder().decode(input);
    }
    if (str === &quot;&quot;) {
      return new Error(&quot;No input provided&quot;);
    }

    const lines = str.split(&quot;\n&quot;);
    for (const line of lines) {
      this.s.lines++;
      for (const char of line) {
        this.s.totalChars++;
        if (/\p{Script=Han}/u.test(char)) {
          this.s.chineseChars++;
        } else {
          this.s.nonChineseChars++;
        }
      }
    }

    return null;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参考：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://unicode.org/reports/tr24/&quot;&gt;Unicode Script Property&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://keqingrong.cn/blog/2020-01-29-regexp-unicode-property-escapes/&quot;&gt;在正则表达式中使用 Unicode 属性转义&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jhuang.me/2018/01/26/JavaScript-%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F%E5%8C%B9%E9%85%8D%E6%B1%89%E5%AD%97/&quot;&gt;JavaScript 正则表达式匹配汉字&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>用 React 和 Tailwind CSS 实现 3D 卡片翻转效果</title><link>https://devlike.top/posts/react-tailwind-3d-flip-card/</link><guid isPermaLink="true">https://devlike.top/posts/react-tailwind-3d-flip-card/</guid><description>手把手教你用 Tailwind CSS 和 React 实现 3D 卡片翻转效果，包含 Transform 属性详解、动画实现原理和完整代码示例。适合前端开发者提升 CSS 动画技能</description><pubDate>Sun, 27 Aug 2023 09:50:24 GMT</pubDate><content:encoded>&lt;p&gt;最近重新复习了一下在 CSS 中有关于 Transform（变换）相关的属性以及用法，这里就简单介绍一下如何通过核心代码来实现一个卡片正反面翻转效果。&lt;/p&gt;
&lt;p&gt;&amp;lt;div align=center&amp;gt;
&amp;lt;img src=&quot;https://assets.devlike.top/blog/2023/08/9979225036e601a2b90df62bcef21cf3.gif&quot; alt=&quot;flippable card&quot;/&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h2&gt;关键要点&lt;/h2&gt;
&lt;h3&gt;容器处理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;设置相对定位 &lt;code&gt;position: relative;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;设置透视距离 &lt;code&gt;perspective: 1000px;&lt;/code&gt; 并开启 3d 变换效果 &lt;code&gt;transform-style: preserve-3d;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;设置过渡动画及延时 &lt;code&gt;transition: transform 700ms;&lt;/code&gt; 让翻转效果更佳明显；&lt;/li&gt;
&lt;li&gt;设置 Y 轴角度旋转，如当鼠标悬停或点击时翻转 180 度 &lt;code&gt;transform: rotateY(180deg);&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;卡片正面（Front Face）和反面（Back Face）的处理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;正反面子元素统一设置绝对定位 &lt;code&gt;position: absolute;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;为正反面子元素设置在透视时隐藏的 &lt;code&gt;backface-visiblity: hidden;&lt;/code&gt; 属性&lt;/li&gt;
&lt;li&gt;将卡片背面（Back Face）设置 Y 轴旋转 180 度效果 &lt;code&gt;transform: rotateY(180deg);&lt;/code&gt;，而卡片正面（front face）由于是默认展示则不需要设置旋转，而由于背面已经事先旋转了 180 度，于是容器从正面切换到背面时，背面相当于旋转了 360 度此时呈现的就是正面&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了方便实现翻转状态管理，这里我使用了 React 并搭配 Tailwind CSS 来完成最终实现。&lt;/p&gt;
&lt;h2&gt;实现&lt;/h2&gt;
&lt;p&gt;除了 &lt;a href=&quot;https://react.dev/&quot;&gt;React&lt;/a&gt; 和 &lt;a href=&quot;https://tailwindcss.com/&quot;&gt;Tailwind CSS&lt;/a&gt; 之外还额外使用了 &lt;a href=&quot;https://github.com/dcastil/tailwind-merge&quot;&gt;tailwind-merge&lt;/a&gt; 用于帮我根据状态切换不同的样式。&lt;/p&gt;
&lt;h3&gt;React 与 Tailwind CSS&lt;/h3&gt;
&lt;p&gt;这里我主要通过 &lt;a href=&quot;https://vitejs.dev/&quot;&gt;Vite&lt;/a&gt; 来帮助快速搭建一个简单的脚手架，然后安装相关依赖即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm i
pnpm add -D tailwind-merge
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tailwind CSS 设置可参考 &lt;a href=&quot;https://tailwindcss.com/docs/guides/vite&quot;&gt;官方文档&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;最终实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useState } from &quot;react&quot;;
import { twMerge as tw } from &quot;tailwind-merge&quot;;

export default function FlippableCard() {
  const [hasFlipped, setHasFilpped] = useState(false);

  return (
    &amp;lt;div className=&quot;flex min-h-screen items-center justify-center&quot;&amp;gt;
      &amp;lt;div
        className={tw(
          &quot;relative h-[360px] w-[240px] cursor-pointer&quot;,
          &quot;perspective-1000 transition-transform duration-700&quot;,
          &quot;transform-style-3d perspective-origin-center&quot;,
          hasFlipped &amp;amp;&amp;amp; &quot;rotate-y-180&quot;,
        )}
        onClick={() =&amp;gt; setHasFilpped(!hasFlipped)}
      &amp;gt;
        &amp;lt;div
          className={tw(
            &quot;absolute inset-0 top-0 left-0&quot;,
            &quot;flex h-full w-full flex-col items-center justify-center p-2&quot;,
            &quot;rounded-lg shadow-[0_10px_30px_-15px_rgba(0,0,0,0.2)]&quot;,
            &quot;bg-gradient-to-b from-sky-200/50 via-purple-100 to-slate-50&quot;,
            &quot;backdrop-blur-lg backface-hidden&quot;,
          )}
        &amp;gt;
          &amp;lt;div className=&quot;font-mono text-lg&quot;&amp;gt;Front Face&amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div
          className={tw(
            &quot;absolute inset-0 top-0 left-0&quot;,
            &quot;flex h-full w-full flex-col items-center justify-center p-2&quot;,
            &quot;rounded-lg shadow-[0_10px_30px_-15px_rgba(0,0,0,0.2)]&quot;,
            &quot;bg-gradient-to-b from-sky-200/50 via-purple-100 to-slate-50&quot;,
            &quot;rotate-y-180 backdrop-blur-lg backface-hidden&quot;,
          )}
        &amp;gt;
          &amp;lt;div className=&quot;font-mono text-lg&quot;&amp;gt;Back Face&amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;原生 HTML 与 CSS&lt;/h3&gt;
&lt;p&gt;当然同样附上对应的原生 HTML5 与 CSS3 代码（基于 ChatGPT 转换）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot; /&amp;gt;
    &amp;lt;meta
      name=&quot;viewport&quot;
      content=&quot;width=device-width, initial-scale=1.0&quot;
    /&amp;gt;
    &amp;lt;style&amp;gt;
      .container {
        display: flex;
        min-height: 100vh;
        min-width: 100vw;
        align-items: center;
        justify-content: center;
      }

      .card {
        position: relative;
        width: 240px;
        height: 360px;
        cursor: pointer;
        transition: transform 700ms;
        transform-style: preserve-3d;
        perspective: 1000px;
        perspective-origin: center;
      }

      .card.is-flipped {
        transform: rotateY(180deg);
      }

      .front-face,
      .back-face {
        position: absolute;
        inset: 0;
        top: 0;
        left: 0;
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
        width: 100%;
        height: 100%;
        padding: 2px;
        border-radius: 8px;
        box-shadow: 0 10px 30px -15px rgba(0, 0, 0, 0.2);
        backdrop-filter: blur(8px);
        -webkit-backface-visibility: hidden;
        backface-visibility: hidden;
        background-image: linear-gradient(
          to bottom,
          rgba(135, 206, 235, 0.5),
          rgba(147, 112, 219, 0.5),
          rgba(119, 128, 144, 0.5)
        );
      }

      .back-face {
        transform: rotateY(180deg);
      }

      .text {
        font-family: monospace;
        font-size: 18px;
      }
    &amp;lt;/style&amp;gt;
  &amp;lt;/head&amp;gt;

  &amp;lt;body&amp;gt;
    &amp;lt;div class=&quot;container&quot;&amp;gt;
      &amp;lt;div
        id=&quot;card&quot;
        class=&quot;card&quot;
      &amp;gt;
        &amp;lt;div class=&quot;front-face&quot;&amp;gt;
          &amp;lt;div class=&quot;text&quot;&amp;gt;Front Face&amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;back-face&quot;&amp;gt;
          &amp;lt;div class=&quot;text&quot;&amp;gt;Back Face&amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;script&amp;gt;
      const card = document.getElementById(&quot;card&quot;);
      let isFlipped = false;

      card.addEventListener(&quot;click&quot;, () =&amp;gt; {
        isFlipped = !isFlipped;
        card.classList.toggle(&quot;is-flipped&quot;, isFlipped);
      });
    &amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item></channel></rss>