【UXP教程-3】 插件脚手架搭建

【UXP教程-3】 插件脚手架搭建

上篇教程我们介绍了如何通过官方提供的Samples来快速跑出来一个hello world插件面板,这样虽然可以让我们快速启动开发,直接在样例的基础上修改我们需要的代码就可以了。但是官方提供的样例,依然还是非常的简陋,提供的配置大体也就是能把项目跑起来而已,而我们实际的插件开发中还会涉及到文件监听、拷贝、混淆、加密打包等等任务,那我们自己去完成一个脚手架的搭建就很有必要了。

这篇文章我来介绍一下如何搭建一个脚手架,让我们的插件开发更加的规范化。

1. 技术方案选型

在搭建脚手架之前,我们需要先确定技术方案选型,比如用Vue还是React,用Js还是TS,用webpack还是vite等等。

我之前在开发CEP插件的时候采用的方案是React + Typesciprt + Webpack,有这么一些考虑:

  1. React是目前最流行的前端框架,生态和相关资源都非常丰富,当然你也会反驳说Vue也很牛逼呀,我这里不想争论两个框架的优劣,大家可以挑自己喜欢的就行。我更偏向于React的原因是官方有一个在维护的基于React的组件库React Spectrum,我当前CEP的插件一直在用,虽然它目前还不支持UXP,但是保不准以后会支持呢?
  2. Typescript是Js的超级,在类型验证和代码提示方面有非常好的效果,能够大幅减少你代码的出错率,强烈推荐。
  3. Webpack就不用多说了,它是前端开发中最流行的打包工具,也是目前最成熟的打包工具,它的生态和相关资源也是非常丰富的。

但是这次,我想试试不一样的!

Vite是一个比较新的打包构建工具,相比流行了许多年的webpack,在性能和先进性上有较多的优势,我想既然UXP已经是从新开始了,为什么不用一个更新的构建工具呢?

但是我在网上一搜,发现几乎没有任何Vite + UXP的一些案例的文章?

Vite UXP

不管三七二十八,我们先来动手试一试!

2. 使用Vite构建UXP项目

第一步,我们通过Vite提供的基础能力,帮助我们创建一个基于React + Typescript的项目,我们可以用官方提供的模板来完成

1
npm init vite@latest vite-uxp-panel --template react-ts

创建完成之后,根据提示进入到目录,执行

1
2
npm install
npm run dev

我们就可以通过浏览器看到默认的网页效果了,这个是前端页面开发的第一步骤,并且它给我们提供了一个虚拟server,我们只要改动代码,浏览器页面的内容就会发生变化。

然而,这并不是我们想要的,我们需要的是一个能够在Photoshop中运行的插件,上一个教程我们讲述了UXP插件的目录结构,并且使用UXP Developer Tool来进行加载此插件,也就是说我们需要每次能够通过vite将代码编译出来,而不是放在内存中让浏览器去刷新。

我们通过修改package.json添加一个命令来完成此操作,这行命令的意思是,先执行tsc命令,将Typescript编译成Javascript,然后再执行vite build --watch命令,将Javascript编译成ESM,并且监听文件变化,实时编译输出到output当中。

1
2
3
4
5
{
"scripts": {
"watch": "tsc && vite build --watch"
}
}

上面这个步骤只是将基础的页面内容编译出来了,但是我们还需要将UXP插件的manifest.json放到编译目录下,这样才能够被UXP Developer Tool加载到。我们可以通过一些拷贝的插件来完成此操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// vite.config.js
import { viteStaticCopy } from 'vite-plugin-static-copy'
export default defineConfig({
// ...
plugins: [
viteStaticCopy({
assets: [
{
src: 'src/manifest.json',
dest: 'manifest.json'
}
]
})
]
});

另外,由于vite默认输出的文件都放在了dist目录下,我这边希望它能够编译出来一个插件id的文件夹,类似这样的结构

1
2
3
4
5
| dist
| --- {插件ID} |
| --- | --- | manifest.json
| --- | --- | index.html
| --- | --- | index.js

我们继续修改vite.config.js,添加一个build的配置,来实现这个目的

1
2
3
4
5
6
7
8
// vite.config.js
const pluginId = "xxxxxxxx";
export default defineConfig({
// ...
build: {
outDir: `./dist/${pluginId}`
},
});

这样我们的插件就编译输出来,可以通过UXP Developer Tool进行加载。不过加载之后我们会发现页面是空白的?

empty panel

查看debug发现引用的Js是通过module的形式加载的,但是这个JS文件并没有被加载进来。

Native ESM

查了一下,这是因为Vite面向的是现代浏览器,要求宿主环境支持Native ESM,它打包出来的Js文件引用,都是使用type="module"模式,而我们的UXP本身就不是一个正规的浏览器环境,不支持Native ESM,就无法使用module形式加载JS

vite es module

那怎么办呢?我们需要做的是,将打包出来的Js文件,改成type="text/javascript",这样就可以正常加载了。由于Vite默认就不支持输出text/javascript了,我们只能通过一个插件来实现这个功能。

1
2
3
4
5
6
7
8
9
10
// vite.config.js
import legacy from '@vitejs/plugin-legacy'
export default defineConfig({
// ...
plugins: [
legacy({
targets: ['defaults', 'not IE 11'],
}),
]
});

这个是官方提供的面向老旧浏览器的降级方案,这样我们编译出来的内容就可以正常被UXP加载了,我们重新Load插件,就可以看到一个熟悉的界面。

UXP plugin starter

但是呢,我又发现了问题,当使用plugin-legacy插件之后,之前配置的vite build --watch不好使了。使用监听会编译报错,并且会自动删掉index.html文件!!!!

watch build error

auto remove index.html

整个人都不好了!没办法,继续Google之,在官方的issue里头找到许多同样的反馈。然而,并没有人提供解决办法:(,看起来更像是legacy插件的bug,官方看起来也不想管这个插件了……

issue

没有watch功能的话,我们插件开发起来肯定是效率不行的,不能忍,那怎么办呢?

我们迂回前进!既然默认的vite build --watch有问题,那我不用你的watch还不行?我单独弄一个watch!我们可以使用单纯第三方node-watch来完成文件监听的操作。当发生文件变化的时候,调用vite build,就可以正常输出了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var watch = require('node-watch');
const { exec } = require("child_process");

watch('./', { recursive: true, filter: f => !/node_modules|dist/.test(f) }, function(evt, name) {
console.log('%s changed.', name);
exec("npm run build", (error, stdout, stderr) => {
if (error) {
console.error(error);
return;
}
if (stderr) {
console.error(error);
return;
}
console.log(stdout);
});
});

在把这个watch命令配置到package.json当中

1
2
3
4
5
6
7
8
{
"scripts": {
"dev": "vite",
"watch": "node watch.js",
"build": "tsc && vite build",
"preview": "vite preview"
}
}

最后,整体的配置文件如下:

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
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { viteStaticCopy } from 'vite-plugin-static-copy'
import legacy from '@vitejs/plugin-legacy'

const pluginId = "11aa22bb33";

// https://vitejs.dev/config/
export default defineConfig({
build: {
outDir: `./dist/${pluginId}`
},
base: "./",
rollupOptions: {
output: {
esModule: false,
preserveModules: false,
format: "cjs",
}
},
plugins: [
react(),
legacy({
targets: ['defaults', 'not IE 11'],
}),
viteStaticCopy({
targets: [
{
src: "src/plugin/*",
dest: "./"
}
]
})
],
})

至此,在踩了好几个神坑之后,我总算将Vite + React + TS的开发环境配置好了,能够正常撸代码,插件也能做到实时刷新。

但是总体看下来,你会发现这里面的方案都非常得挫,一方面要使用plugin-legacy来做编译降级,这个东西就很明显属于即将被时代淘汰的玩意,同时还没法用默认的watch,自己弄的node-watch虽然能跑,但总归感觉不是亲生的娃那样心里隔应。

嗯,大概就到这里,看来我不配用Vite这种高级玩意,大家看看自己是否需要吧~~

3. 使用Webpack构建UXP项目

这就来到咱主战场了,过程非常简单,几个步骤完事:

  1. 安装相关依赖库
1
2
yarn add react react-dom @types/react-dom @types/react
yarn add -D webpack webpack-cli url-loader css-loader ts-loader json-loader html-webpack-plugin copy-webpack-plugin
  1. 配置tsconfig.json文件
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"compilerOptions": {
"target": "es5",
"module": "es2015",
"jsx": "react",
"skipLibCheck": true,
"noEmit": false,
"noEmitOnError": false,
"moduleResolution": "node",
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true
}
}
  1. 配置webpacke.config.js文件
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

const HtmlWebpackPlugin = require('html-webpack-plugin')
const copyWebpackPlugin = require('copy-webpack-plugin');
const webpack = require('webpack');
const path = require("path");

const panelName = `b800bf58`;
const dist = path.join(__dirname, 'dist');

function createConfig(mode, entry, output, plugins) {
return {
entry,
module: {
rules: [
{
test: /\.tsx?$/,
exclude: /node_modules/,
use: [ { loader: 'ts-loader', options: { transpileOnly: true, configFile: "tsconfig.json" } }],
},
{ test: /\.css$/, use: ['style-loader', 'css-loader'] },
{ test: /\.(png|jpg|gif|webp|svg|zip|otf)$/, use: ['url-loader'] },
],
},

resolve: { extensions: ['.tsx', '.ts', '.jsx', '.js', '.json'] },
externals: {
_require: "require",
photoshop: 'commonjs2 photoshop',
uxp: 'commonjs2 uxp',
os: 'commonjs2 os',
},
output: {
filename: '[name].js',
path: output
},

plugins,
}
}

module.exports = (env, argv) => {
const panelOutput = path.join(dist, `${panelName}.unsigned`);
const uxpPanelConfig = createConfig(argv.mode, { uxp: "./src/index.tsx" }, path.join(dist, panelName), [
new webpack.ProvidePlugin({
_require: "_require"
}),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html',
/*inlineSource: '.(js)$',*/
chunks: ['uxp'],
}),
new copyWebpackPlugin({
patterns: [
{ from: "./manifest.json", to: "." },
{ from: "./src/assets/icons", to: "./icons" },
]
}),
{
apply: (compiler) => {
compiler.hooks.afterEmit.tap('AfterEmitPlugin', async (compilation) => {
});
}
}
]);
return [uxpPanelConfig];
}

就完事了,可用webpack --watch来实现代码监听编译。

详细的工程示例在下面,大家自行取用。

4. 总结

这篇文章介绍如何来搭建自己的插件开发脚手架,方案的选型不是唯一的,你可以按照自己的喜好去挑选和配置,相比与CEP而言,UXP由于不需要引入JSX,在编译构建上更偏向web也更简单一些。

我相信有许多小伙伴自己都试过去搭建各种技术方案的脚手架,也欢迎你在评论区分享你的经验,如果需要交流和讨论,可以加入这个微信群。

下一篇,我们就要开始真正干活了,开始上手撸插件的界面和功能,敬请期待。

评论