分类 Lab 下的文章

This is not a component library. It is how you build your component library.

Shadcn/ui 像是摆在你面前的一块原石,没有直接打磨成品,而是递给你一把刻刀。

这也意味着,它并不是一个现成可用的“库”,而是一套让你自己动手打造组件体系的组件库。

01 它的“非库”特性

Shadcn/ui 给你的不是封装好的黑盒,而是组件源码。你复制到本地后,它就是你项目的一部分,与其他代码无异。

那好处显而易见:

  • 完全可控:逻辑、样式、交互你都能直接改。
  • 高度可定制:从 design token 到动效,你都可以按团队风格塑形。
  • 设计统一:早期就能与项目的 UI 语言绑定,不必事后修补。

但它的代价也很真实:维护责任全在你。

02 从“拿来”到“养成”

初用 Shadcn/ui 时,只是把它当成“官方样式集”仅此而已,复制粘贴后就直接用,这种用法短期高效,而对于长期而言失控。(((

而我的建议是,第一步先做基础设计层:

定义 design token

在 tailwind.config.ts 里声明颜色、间距、圆角、阴影的变量,组件全部吃变量,不直接写死值。

二次封装组件

官方的 Button、Card 等不要直接暴露,先包一层自己的版本,这样全局设计改动时不会满世界找文件。

03 更新与演进

Shadcn/ui 官方的组件实现会悄悄更新,但你的项目本地文件不会自动跟上。而想要最新的好东西?得自己动手去更新。

  • 按需拉取 不搞全量同步,只更新我在用的那几个组件,省得引入一堆无关 diff。
  • 手动对比 用 Git 把官方最新版本和我本地的实现做个 diff,看哪些是性能优化、哪些是 API 变更,再决定“要不要搬”。
  • 设个更新日 想起来了就自己检查下....(我很懒就是了)

这样一来,更新变成了一种可控的节奏,而不是头脑一热的全盘替换。然后发现自己搞得乱七八糟。

04 折腾的意义

Shadcn/ui 吸引我的地方,不是它有多“全能”,而是它逼你动手。
不像那些“拎包入住”的 UI 库,给你一套现成的风格,然后你只能在框框里改一点边角。它更像是说:

喏,材料和工具给你了,房子自己盖。

这种模式一开始挺累的——要自己调颜色、改间距、封装逻辑……

但慢慢地,你会发现这个过程其实很爽。因为最后产出的,不是“某某库的组件”,而是你自己项目的语言。

这篇文章主要来源于我的个人理解,不一定准确,但是我是这样用的(

1. Next.js App Router

提到Next.js的主要组成的的话,我觉得就是路由系统了。Next.js路由写起来很简单,而且很实用,先从文件系统路由来说,其次为动态的路由。

文件系统路由

  • src/app/文件夹里创建文件夹,在Next.js中即算为路由
  • page.tsx文件就是这个路由的页面
  • 比如src/app/about/page.tsx就是/about页面

动态路由

  • 用方括号[xxx]来创建动态的路由,比如/blog/[cid]/page.tsx即为一个动态的路由
  • 然后呢在组件里用useParams()可以获取到这个参数
  • 此界面的实现就是这样的:/blog/123中的123就是所获取到的cid

2. React Hooks

React Hooks 是一种函数式组件的增强机制,它允许你在不编写类组件的情况下使用 React 的特性。主要的 Hooks 包括 useState, useEffect, useContext, useReducer, useCallback, useMemo, useRef, 和 useImperativeHandle 等。这些 Hooks 提供了访问 React 特性的方式。

以下是一些我用到的React Hooks的具体实现

useState - 管理状态

useState Hook 允许你在函数组件中使用局部状态。它会返回一个状态值和更新该状态值的函数。
const [loading, setLoading] = useState(true) // 此处即表示加载状态
const [comments, setComments] = useState([]) // 评论列表

useEffect - 执行副作用操作

useEffect Hook,允许你将组件与外部系统同步(如数据获取、订阅管理、DOM 操作等)。它在每次渲染后都会执行。
useEffect(() => {
  // 页面加载时获取数据
  fetchData()
}, []) // 空数组表示只在组件挂载时执行一次

useRef - 获取DOM元素

用于创建对 DOM 元素或值的引用,可以在渲染之间保持状态。
const textareaRef = useRef(null)
// 可以直接操作textarea元素,比如设置光标位置

3. TypeScript

TypeScript 是 JavaScript 的超集,扩展了 JavaScript 的语法,因此现有的 JavaScript 代码可与 TypeScript 一起工作无需任何修改,TypeScript 通过类型注解提供编译时的静态类型检查。

TypeScript 可处理已有的 JavaScript 代码,并只对其中的 TypeScript 代码进行编译。

定义接口的一个小例子

interface Post {
  id: number
  title: string
  content: string
}

这段代码的意思是:

定义了一个叫 Post 的接口(interface),它描述了一个“帖子”的结构,里面有三个属性:

  • id:编号,是一个数字(number)
  • title:标题,是一个字符串(string)
  • content:内容,也是字符串(string)

这个接口可以用来规范对象的形状,比如下面这样:

const post: Post = {
  id: 1,
  title: "标题",
  content: "这是内容"
}

这样写,TypeScript会帮你检查这个对象是否符合 Post 的结构。

所以说TypeScript的优势就在这里

  • 写代码的时候有智能提示,能提前发现错误,不用等到运行时才知道。
  • 代码会更容易维护。

4. 组件化管理

复杂的页面的往往会出现重复的代码段,在Next.js中我们可以把这些重复的代码段拆分成单个小组件,然后直接引用,这样代码不仅更好管理,代码更加的清晰:

组件拆分的思路
例如我每个页面都有顶栏(navbar),如果每个页面都写重复的navbar代码,改起来便很麻烦。所以就将其拆分为navbar.tsx然后存放于components之中即可。然后页面头部引用该文件。

import { Navbar } from "@/components/navbar"

5.Props传递

interface CommentProps {
  comment: Comment
  onReply: (author: string) => void
}

function CommentItem({ comment, onReply }: CommentProps) {
  // 组件内容
}

这段代码演示了 在 React(Next.js)中通过 Props 向组件传递数据和函数 的方式。

1. interface CommentProps

定义了传给 CommentItem 组件的参数类型(也叫 props)。

  • comment:是一个 Comment 类型的对象,表示一条评论(例如包含作者、内容、时间等)。
  • onReply:是一个函数,接收一个字符串(作者名),用于点击“回复”按钮时执行相应操作。

2. function CommentItem({ comment, onReply }: CommentProps)

这是一个 React 函数组件。

使用了解构语法,从传入的 props 中直接取出 comment 和 onReply。

这样可以在组件内部使用 comment 数据来显示评论内容,也可以在用户点击“回复”时调用 onReply() 来触发父组件的处理逻辑。

6. 数据处理

这里以递归处理评论树来说

在评论系统中,常常会有“评论回复评论”的结构,比如:

- 评论 A(id: 1)
  - 回复 A1(parent: 1)
    - 回复 A1-1(parent: A1)
  - 回复 A2(parent: 1)、

这样的结构是树形的,每条评论通过 parent 字段来指向它的上一级评论。

function getAllReplies(comments, parentId) {
  const replies = []
  comments.forEach(comment => {
    if (comment.parent === parentId) {
      replies.push(comment)
      // 递归查找子评论
      replies.push(...getAllReplies(comments, comment.id))
    }
  })
  return replies
}

这段代码便是找到某个评论(parentId)下的所有“直接和间接”子评论,并把它们放在一个数组里返回。
假设我们调用:

getAllReplies(comments, 1)

意思是找出「ID 为 1 的评论」下面的所有子评论。

那函数怎么做的呢?

  • 1.遍历每条评论
  • 2.如果某条评论的 parent === 1(说明它是 ID 为 1 的直接回复)
  • 3.把它加进 replies 结果数组里
  • 4.然后递归调用自己:去找这条子评论的“子评论”
  • 5.一直递归下去,直到找不到更多子评论为止
  • 6.最后返回一个包含所有层级子评论的扁平数组

7. 表单处理

React的表单处理有自己的套路。它处理表单和原生HTML有些不同,核心思想是:用组件的状态(state)来控制输入框的值,这就是“受控组件”的概念。

受控组件

const [form, setForm] = useState({
  name: '',
  email: '',
  message: ''
})

<input 
  value={form.name}
  onChange={e => setForm({...form, name: e.target.value})}
/>

注意

  • useState 是 React 的一个 Hook,用来声明状态。 form
  • 是一个对象,包含三个字段:name、email、message,分别对应输入框的值。
  • setForm 是更新 form 的函数。

上半段代码表示:我们要用 form 这个 state 来控制输入框的值,每个输入框的内容都保存在 form 里。
而下半段即是把所有表单数据都保存在 state 里,我们可以随时查看、验证、提交。

表单提交

const handleSubmit = async (e) => {
  e.preventDefault() // 阻止默认提交行为
  try {
    await fetch('/api/submit', {
      method: 'POST',
      body: JSON.stringify(form)
    })
    alert('提交成功!')
  } catch (error) {
    alert('提交失败!')
  }
}

在这段代码中呢

  • handleSubmit 是你绑定在
    上的提交事件处理函数。
  • e.preventDefault():阻止表单默认的提交行为(不刷新页面)。
  • fetch('/api/submit', { ... }):用 JavaScript 发起 POST 请求,把表单数据(form)发送到后端。
  • JSON.stringify(form):把表单对象转成 JSON 字符串。 alert('提交成功!'):提交成功后弹出提示。
  • 如果出错(例如网络问题或服务器异常),就会进入 catch 显示“提交失败”。

8. 对性能进行优化

动态导入是指在用户访问页面时,根据需要按需加载组件,而不是在初始页面加载时将所有组件都打包。这种方式可以显著优化性能和用户体验。

// 组件懒加载,减少首屏加载时间
const MarkdownEditor = dynamic(() => import('./MarkdownEditor'), {
  ssr: false // 不在服务端渲染
})

同时还要避免不必要的重新渲染

  • 比如合理使用useEffect的依赖数组
  • 还有事件处理函数记得清理,避免内存泄漏

这些是我在学习Next.js/React的一些笔记与总结,不一定完全对awa(欢迎指正)。也算是一些记录,之后或许会继续更新(

这篇文章发自我重构的后台.
(基于Next.js React Shadcn/ui Typecho XML-RPC接口)
想写点东西,但总是不知道写点啥好,只能写点垃圾小文章了🌝
IMG_0835.png

前言

OwO.js是一个可爱的表情符号和表情符号键盘,项目地址:OWO.js
因为handsome模板内嵌了OwO.js所以此文是基于handsome模板撰写的。

开始

由于Handsome内嵌了OwO,所以我们只需要修改主题目录下面的usr/OwO.json文件。

新增表情栏目

"New": {
    "name": "表情包名称",//只有图片表情类型才需要加这一项
    "type": "emoticon/emoji/image",
    "container": [
        {
            "icon": "OωO",//对应图片名字如图片1.jpg则输入1
            "text": "TEST"//指的是鼠标悬停在表情上面显示的提示文字
        },
    ]
}

[scode type="red" size=""]⚠️要符合JSON的语法[/scode]

上传表情

评论表情图片存储在主题目录下的assets/img/emotion

结束

成品图~~
微信截图_20231210134931.png

前言

VestaCP 是国外的一个老牌的开源的主机控制面板,界面简洁清晰。
官网:http://www.vestacp.com

开始

首先运行指令安装VestaCP而安装速度取决于VPS的性能,程序安装需要 15 分钟左右,不过笔者这里不到10分钟即安装完了~~

**# Download installation script**
curl -O http://vestacp.com/pub/vst-install.sh
**# Run it**
bash vst-install.sh

在登录后VestaCP,单击页面顶部的“WEB”。
然后,单击“➕”。
新建网站,输入你的域名。

建议开启SSL加密
在创建网站完成后,前往“数据库”页面新建数据库。

注意,如注释所说新建的数据库名称会自动添加前缀"admin_"
例如你输入test,那么新建的数据库名称则为"admin_test"
完成以上操作,我们可以上传程序了
此处建议使用Xftp软件,因为VestaCP是没有在线文件管理的
连接上服务器后,访问目录"/home/admin/web/此处是你刚刚创建的网站的域名/public_html"
这相当于你的网站的根目录,上传程序即可。
然后按照正常流程安装就好了~~

伪静态部署

基于环境为Nginx的用户
在VestaCP,当我们添加网站的时候:可以看到有个叫做Web模板nginx的,这是Vesta关于一些程序做的预制,我们所需要的伪静态设置也包括于其中。
可见,程序没有针对Typecho做针对的预制,但是我们可以自行添加。
继续使用Xftp,访问目录"/usr/local/vesta/data/templates/web/nginx/php-fpm"

我们可见每个模板都有分别为 .stpl 和 .tpl 两个后缀的文件
.stpl 是https模式下的,同理 .tpl 是http模式下的。
以下是添加typecho伪静态规则的代码的模板,创建文件复制代码;上传至上述模板目录,然后于后台处使用即可。(注意需要.tpl和.stpl的都上传一份)

server {
    listen      %ip%:%web_port%;
    server_name %domain_idn% %alias_idn%;
    root        %docroot%;
    index       index.php index.html index.htm;
    access_log  /var/log/nginx/domains/%domain%.log combined;
    access_log  /var/log/nginx/domains/%domain%.bytes bytes;
    error_log   /var/log/nginx/domains/%domain%.error.log error;

    location / {

        location ~* ^.+\.(jpeg|jpg|png|gif|bmp|ico|svg|css|js)$ {
            expires     max;
        }

        location ~ [^/]\.php(/|$) {
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            if (!-f $document_root$fastcgi_script_name) {
                return  404;
            }

            fastcgi_pass    %backend_lsnr%;
            fastcgi_index   index.php;
            include         /etc/nginx/fastcgi_params;
        }
    }

    error_page  403 /error/404.html;
    error_page  404 /error/404.html;
    error_page  500 502 503 504 /error/50x.html;

    location /error/ {
        alias   %home%/%user%/web/%domain%/document_errors/;
    }

    location ~* "/\.(htaccess|htpasswd)$" {
        deny    all;
        return  404;
    }

    location /vstats/ {
        alias   %home%/%user%/web/%domain%/stats/;
        include %home%/%user%/conf/web/%domain%.auth*;
    }

    include     /etc/nginx/conf.d/phpmyadmin.inc*;
    include     /etc/nginx/conf.d/phppgadmin.inc*;
    include     /etc/nginx/conf.d/webmail.inc*;

    include     %home%/%user%/conf/web/nginx.%domain%.conf*;

if (-f $request_filename/index.html){
    rewrite (.*) $1/index.html break;
    }
if (-f $request_filename/index.php){
    rewrite (.*) $1/index.php;
    }
if (!-e $request_filename){
    rewrite (.*) /index.php;
    }
}
server {
    listen      %ip%:%web_ssl_port%;
    server_name %domain_idn% %alias_idn%;
    root        %sdocroot%;
    index       index.php index.html index.htm;
    access_log  /var/log/nginx/domains/%domain%.log combined;
    access_log  /var/log/nginx/domains/%domain%.bytes bytes;
    error_log   /var/log/nginx/domains/%domain%.error.log error;

    ssl         on;
    ssl_certificate      %ssl_pem%;
    ssl_certificate_key  %ssl_key%;

    location / {

        location ~* ^.+\.(jpeg|jpg|png|gif|bmp|ico|svg|css|js)$ {
            expires     max;
        }

        location ~ [^/]\.php(/|$) {
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            if (!-f $document_root$fastcgi_script_name) {
                return  404;
            }

            fastcgi_pass    %backend_lsnr%;
            fastcgi_index   index.php;
            include         /etc/nginx/fastcgi_params;
        }
    }

    error_page  403 /error/404.html;
    error_page  404 /error/404.html;
    error_page  500 502 503 504 /error/50x.html;

    location /error/ {
        alias   %home%/%user%/web/%domain%/document_errors/;
    }

    location ~* "/\.(htaccess|htpasswd)$" {
        deny    all;
        return  404;
    }

    location /vstats/ {
        alias   %home%/%user%/web/%domain%/stats/;
        include %home%/%user%/conf/web/%domain%.auth*;
    }

    include     /etc/nginx/conf.d/phpmyadmin.inc*;
    include     /etc/nginx/conf.d/phppgadmin.inc*;
    include     /etc/nginx/conf.d/webmail.inc*;

    include     %home%/%user%/conf/web/snginx.%domain%.conf*;

if (-f $request_filename/index.html){
    rewrite (.*) $1/index.html break;
    }
if (-f $request_filename/index.php){
    rewrite (.*) $1/index.php;
    }
if (!-e $request_filename){
    rewrite (.*) /index.php;
    }
}

以下是我遇到的一些问题

typecho无法上传照片

应该是权限的问题?运行代码修改权限组即可。

chown -R admin:admin /home/admin/web/你的网站/public_html/usr/