大家好,教程更新割了整整一年之后,我又回来了。现在是2025年春节假期期间,实在有点闲着无聊,就打算把我的CEP插件迁移到UXP,顺便把教程更新一下。从昨天开始重新去翻官方的文档,发现一年多过去了,UXP的功能支持没有什么新的变化,文档也没有什么新的内容,Adobe爸爸的更新速度真是让人捉急。
今天借着把我的切图插件从CEP迁移到UXP的契机,给大家逐步介绍一下我的整个UXP面板开发过程,期间我会穿插一些UXP的关键开发知识,让大家能够更加系统地了解UXP的开发。
1. 项目框架选择
虽然我前面的文章有写过UXP的一些开发框架选型,比如VUE,React,Webpack, Vite等,我最初选择的还是我自己从0搭建的ReactJs + Webpack + typescript的组合模式,但是昨天我决定重新开始,并想到这么多年过去了,市面上应该有一些大佬搞出来了更好的解决方案,于是我决定去找一找,还真的让我看到了一个蛮不错的解决方案bolt-uxp
这是一个开箱即用的UXP项目框架,你可以选择自己喜欢的诸如Svelte, React, Vue等框架,它集成了Vite + Typescript,非常适合我这种无TS不欢的开发者。使用的方法大家参考官方文档就好了,这里就不多说了。我由于习惯了使用React,所以选择了React作为项目框架,大家习惯用Vue的就选择Vue就好了,区别不大,下图是我用它启动的工程项目
2. UXP的主题适配
由于Ps有4个颜色主题,以前在做CEP开发的时候,我们通过监听Ps的主题变化,获取背景颜色来动态设置面板的背景颜色。但是到UXP就不用这么复杂了,官方提供了几个内置的CSS颜色变量,直接用就可以了。
1 2 3 4 5 6
| body { background-color: var(--uxp-host-background-color); color: var(--uxp-host-text-color); border-color: var(--uxp-host-border-color); font-size: var(--uxp-host-font-size); }
|
它不仅仅只有上面几个颜色,还有其它一些包括字体大小等变量,大家参考官方文档就好。对应的颜色效果如下:
使用这些颜色好处是它可以和Ps的主题颜色融合在一起,让面板的视觉效果看起来更专业,还有的就是它可以自动随着Ps的主题变化和切换,不需要自己去处理不同主题下的颜色问题,非常省心。
3. Spectrum UXP兼容
Spectrum UXP是Adobe自己搞了一套UI的控件,它和传统的web控件不兼容,并且控件的数量也有限,不过它由于是内置的,渲染性能比较好,我关注到Adobe目前正在打算逐步迁移到**Spectrum Web Component
**,它应该会是下一代的UI控件库,目前已经部分可用了。
当你使用Typescript的时候,在使用sp-button这样的控件就会报错,提示找不到sp-button,这是因为Spectrum UXP的控件库还没有完全支持Typescript,所以需要手动去定义这些控件,比如sp-button的类型定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| declare global { namespace JSX { interface IntrinsicElements { 'sp-button': { children?: React.ReactNode; ref?: React.RefObject<HTMLElement>; class?: string; disabled?: boolean; quiet?: boolean; variant?: Spectrum.ButtonVariant; }; } } }
|
这样每个控件都定义一遍也挺繁琐的,所以第一个想法就是去看看是不是别人已经帮你写好了……于是我找到了这个项目react-uxp-spectrum,它已经帮我们定义好了所有Spectrum UXP控件的类型,我们只需要安装它,然后就可以直接使用了。
1
| npm install react-uxp-spectrum
|
并且它用React将这些控件包装了一遍,用起来就和传统的React组件一样,非常方便。不过它也有一些缺点就是它的属性只对标官方文档,但是官方文档其实并不完整,很多本来支持的属性并没有在文档中写出来,是的这个库也有部分的属性是缺失的,我后续准备fork一个出来补充一下。
1 2 3 4 5
| <Textfield className='email' placeholder='请输入邮箱账号' value={email} onChange={(e) => setEmail(e.target!.value)} /> <Textfield className='password' type='password' placeholder='请输入密码' value={password} onChange={(e) => setPassword(e.target!.value)} /> <Button variant='cta' className='login-btn' disabled={loading} onClick={onSubmit}> {loading ? '登录中...' : '登录'} </Button>
|
4. 登录页面
基于这个脚手架,使用React就能很快的写出来一个登录页面,并且使用Spectrum UXP控件,效果如下:
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| import * as React from 'react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faUser, faLock } from '@fortawesome/free-solid-svg-icons'; import '../assets/css/login.scss'; import { Button, Link, Textfield } from 'react-uxp-spectrum'; import { login, User } from '../lib/http'; import { writeFile } from '../lib/storage';
export default function Login({ onLogin }: { onLogin: (user: User) => void }) { const [email, setEmail] = React.useState(''); const [password, setPassword] = React.useState(''); const [error, setError] = React.useState(''); const [success, setSuccess] = React.useState(false); const [loading, setLoading] = React.useState(false); const buttonRef = React.useRef<HTMLElement>(null);
React.useEffect(() => { const button = buttonRef.current; if (button) { button.addEventListener('click', onSubmit); return () => button.removeEventListener('click', onSubmit); } }, []);
const validateEmail = (email: string) => { return email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/); };
const onSubmit = async () => { try { setError(''); setLoading(true);
if (!email || !password) { setError('请输入邮箱和密码'); return; }
if (!validateEmail(email)) { setError('请输入有效的邮箱地址'); return; }
const response = await login(email, password); if (response.errno !== 0) { setError(response.info || '登录失败,请重试'); return; }
await writeFile('user.json', JSON.stringify(response));
setSuccess(true); setTimeout(() => { onLogin(response); }, 1500); } catch (err) { setError('登录失败,请稍后重试'); console.error('登录错误:', err); } finally { setLoading(false); } };
return ( <div className='login'> <div className='avatar'> <FontAwesomeIcon icon={faUser} size={'4x'} /> </div> <div className='title'>账号登录</div> <Textfield className='email' placeholder='请输入邮箱账号' value={email} onChange={(e) => setEmail(e.target!.value)} /> <Textfield className='password' type='password' placeholder='请输入密码' value={password} onChange={(e) => setPassword(e.target!.value)} /> <Button variant='cta' className='login-btn' disabled={loading} onClick={onSubmit}> {loading ? '登录中...' : '登录'} </Button> {error && <div className='error-message'>{error}</div>} {success && <div className='success-message'>登录成功</div>} <div className='link-box'> <Link variant='overBackground'>注册账号</Link> <span>|</span> <Link variant='overBackground'>忘记密码</Link> </div> </div> ) }
|
有了脚手架的加持,写一个页面布局还是比较简单的,就是uxp的控件能够支持的样式定义比较受限,不是所有的效果都能实现,只能尽量匹配系统主题的控件样式。
哦,如果你们还没有使用过Cursor编辑器的话,我这里强烈推荐一下,基本上我现在60%的代码,都是靠AI来写的,效率提升很多。
好了,今天的这篇文章就到这里,内容不是很多,下一篇会继续主面板的功能开发,然后给大家介绍一下UXP的本地目录存储和相关的操作,敬请期待。