<?xml version="1.0" encoding="utf-8" standalone="yes"?><?xml-stylesheet type="text/css" href="https://imnerd.org/css/rss.css"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Placeimg on 怡红院落</title><link>https://imnerd.org/tags/placeimg.html</link><description>Recent content in Placeimg on 怡红院落</description><language>zh-CN</language><copyright>© 2021</copyright><lastBuildDate>Sat, 18 Jun 2022 10:00:00 +0800</lastBuildDate><atom:link href="https://imnerd.org/tags/placeimg/index.xml" rel="self" type="application/rss+xml"/><item><title>如何制作 Figma 插件</title><link>https://imnerd.org/how-to-build-figma-plugin.html</link><pubDate>2022-10-18</pubDate><guid>https://imnerd.org/how-to-build-figma-plugin.html</guid><description>Figma 是一款专业的在线 UI 设计工具，它因为个人使用免费、在线跨平台、多人协作、蓬勃的 Figma Community 社区组织而广受欢迎。目前设计团队都在使用 Figma 进行 UI 设计交付。
能被 SaaS 化的终将被 SaaS 化
Figma 本身是 Web 服务，其客户端也是使用 Electron 进行的封装。所以它的插件系统是前端友好型，和日常前端开发没有什么太大的区别。
插件原理 Figma 的插件也采用了双线程的架构。UI 线程能获得完整 Web 的能力，但是无法直接操作 Figma；主线程则相反，可以通过 Figma API 对数据进行操作，但无完整的 Web 能力，仅有 JS 执行以及 Figma API 支持。两者通过 postMessage 进行通信。
根据官方文章描述，通过 WebAssembly 版的 QuickJS 来实现主线程的沙箱执行，UI 线程则是通过 iframe 执行。
采用这种方案的原因主要是既想保证代码隔离，但是又希望能方便的操作 Figma 数据。
根据官博文章描述，之前也曾有尝试使用 Web 原生的沙盒 API Realms 来实现主线程的沙盒执行，但因为该 API 的一些安全漏洞还是回退回了 QuickJS 的实现。
了解了插件的原理之后，下面我就以帮助设计师同学快速插入占位图的插件 Placeholder 为例，带大家一步一步的了解如何进行 Figma 插件开发。
需求整理 在进行插件开发之前，我们捋一捋我们需要实现的功能。http://placeimg.com/ 是一个专门用来生成占位图的网站，我们将利用该网站提供的服务制作一个生成指定大小的占位图并插入到 Sketch 画板中的功能。插件会提供一个面板，可以让使用者输入尺寸、分类等可选项，同时提供插入按钮，点击后会在画板插入一张图片图层。
项目结构 在 Figma 客户端中按照如上操作即可完成插件的初始化。除了默认的三个例子之外，官方也有一个示例插件的仓库，也可以参考。
https://github.com/figma/plugin-samples
Figma 插件默认推荐使用 TypeScript 开发，官方提供了完善的 TypeScript 类型支持。以默认的带 UI 的模板为例，初始化后进入文件夹 npm i 安装依赖后执行 npm run build 编译完成后点击插件即可看到效果。
. ├── README.md ├── code.js ├── code.ts ├── manifest.json ├── package-lock.json ├── package.json ├── tsconfig.json └── ui.html manifest.json 可以看到整体的接口和大多数 JS 项目一样，其中 manifest.json 用来记录插件的信息。manifest.json 这个文件大家可以理解为是 Figma 插件的 package.json 文件。我们来看看默认生成的 manifest.json。
{ &amp;#34;name&amp;#34;: &amp;#34;figma-placeimg&amp;#34;, &amp;#34;id&amp;#34;: &amp;#34;1117699210834344763&amp;#34;, &amp;#34;api&amp;#34;: &amp;#34;1.0.0&amp;#34;, &amp;#34;main&amp;#34;: &amp;#34;code.js&amp;#34;, &amp;#34;editorType&amp;#34;: [ &amp;#34;figma&amp;#34; ], &amp;#34;ui&amp;#34;: &amp;#34;ui.html&amp;#34; } 其中重点的是 main 和 ui 两个字段：
main：指定插件的入口文件，该文件中的代码会运行在主进程中的沙箱里。 ui: 指定插件的 UI 代码文件，该文件中的代码会运行在 iframe 中。实际上，UI 代码文件的内容会作为字符串传递给 figma 内置变量 __html__，在沙箱内可以通过 figma.showUI(__html__) 创建 iframe。 这里注意到是将UI代码文件中的内容作为字符串注入到主线程中，类似 &amp;lt;iframe srcdoc=&amp;quot;__html__&amp;quot; /&amp;gt;。这就导致了我们无法直接引用插件中的其他资源，所有插件内依赖的资源都需要内嵌到最终的字符串中。
ui 字段也支持指定多个文件，当指定多个文件的时候会注入 __uiFiles__ 对象来映射文件。 manifest.json 中还支持通过 menu 字段定义插件的菜单。如果不想写 UI 也可以通过parameters指定支持的指令，直接通过输入指令来操作也是可以的。更多的配置可以查看官方文档 Plugin Manifest。
插件开发 一些基本原理了解清楚之后我们就可以进行插件的开发了。首先我们需要用户点击插件菜单之后打开一个面板，该面板可以配置尺寸、分类等基础信息。
&amp;lt;link rel=&amp;#34;stylesheet&amp;#34; href=&amp;#34;https://unpkg.com/figma-plugin-ds@1.0.1/dist/figma-plugin-ds.css&amp;#34;&amp;gt; &amp;lt;style&amp;gt; .content { display: flex; } .icon--swap { animation: rotate 1s linear infinite; } .hide { display: none; } @keyframes rotate { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } &amp;lt;/style&amp;gt; &amp;lt;div id=&amp;#34;app&amp;#34;&amp;gt; &amp;lt;div class=&amp;#34;field&amp;#34;&amp;gt; &amp;lt;label for=&amp;#34;&amp;#34; class=&amp;#34;label&amp;#34;&amp;gt;请输入图片尺寸：&amp;lt;/label&amp;gt; &amp;lt;div class=&amp;#34;content&amp;#34; style=&amp;#34;padding-left: 10px;&amp;#34;&amp;gt; &amp;lt;div class=&amp;#34;input&amp;#34;&amp;gt; &amp;lt;input type=&amp;#34;input&amp;#34; class=&amp;#34;input__field&amp;#34; placeholder=&amp;#34;宽&amp;#34; name=&amp;#34;width&amp;#34;&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;div class=&amp;#34;label&amp;#34; style=&amp;#34;flex:0;&amp;#34;&amp;gt;×&amp;lt;/div&amp;gt; &amp;lt;div class=&amp;#34;input&amp;#34;&amp;gt; &amp;lt;input type=&amp;#34;input&amp;#34; class=&amp;#34;input__field&amp;#34; placeholder=&amp;#34;高&amp;#34; name=&amp;#34;height&amp;#34;&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;div class=&amp;#34;field&amp;#34;&amp;gt; &amp;lt;label for=&amp;#34;&amp;#34; class=&amp;#34;label&amp;#34;&amp;gt;请选择图片分类：&amp;lt;/label&amp;gt; &amp;lt;div class=&amp;#34;content&amp;#34;&amp;gt; &amp;lt;div class=&amp;#34;radio&amp;#34;&amp;gt; &amp;lt;input id=&amp;#34;radioButton1&amp;#34; type=&amp;#34;radio&amp;#34; class=&amp;#34;radio__button&amp;#34; value=&amp;#34;any&amp;#34; name=&amp;#34;category&amp;#34; checked&amp;gt; &amp;lt;label for=&amp;#34;radioButton1&amp;#34; class=&amp;#34;radio__label&amp;#34;&amp;gt;全部&amp;lt;/label&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;div class=&amp;#34;radio&amp;#34;&amp;gt; &amp;lt;input id=&amp;#34;radioButton2&amp;#34; type=&amp;#34;radio&amp;#34; class=&amp;#34;radio__button&amp;#34; value=&amp;#34;animals&amp;#34; name=&amp;#34;category&amp;#34; &amp;gt; &amp;lt;label for=&amp;#34;radioButton2&amp;#34; class=&amp;#34;radio__label&amp;#34;&amp;gt;动物&amp;lt;/label&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;div class=&amp;#34;radio&amp;#34;&amp;gt; &amp;lt;input id=&amp;#34;radioButton3&amp;#34; type=&amp;#34;radio&amp;#34; class=&amp;#34;radio__button&amp;#34; value=&amp;#34;arch&amp;#34; name=&amp;#34;category&amp;#34; &amp;gt; &amp;lt;label for=&amp;#34;radioButton3&amp;#34; class=&amp;#34;radio__label&amp;#34;&amp;gt;建筑&amp;lt;/label&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;div class=&amp;#34;radio&amp;#34;&amp;gt; &amp;lt;input id=&amp;#34;radioButton4&amp;#34; type=&amp;#34;radio&amp;#34; class=&amp;#34;radio__button&amp;#34; value=&amp;#34;nature&amp;#34; name=&amp;#34;category&amp;#34; &amp;gt; &amp;lt;label for=&amp;#34;radioButton4&amp;#34; class=&amp;#34;radio__label&amp;#34;&amp;gt;自然&amp;lt;/label&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;div class=&amp;#34;radio&amp;#34;&amp;gt; &amp;lt;input id=&amp;#34;radioButton5&amp;#34; type=&amp;#34;radio&amp;#34; class=&amp;#34;radio__button&amp;#34; value=&amp;#34;people&amp;#34; name=&amp;#34;category&amp;#34; &amp;gt; &amp;lt;label for=&amp;#34;radioButton5&amp;#34; class=&amp;#34;radio__label&amp;#34;&amp;gt;人物&amp;lt;/label&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;div class=&amp;#34;radio&amp;#34;&amp;gt; &amp;lt;input id=&amp;#34;radioButton6&amp;#34; type=&amp;#34;radio&amp;#34; class=&amp;#34;radio__button&amp;#34; value=&amp;#34;tech&amp;#34; name=&amp;#34;category&amp;#34; &amp;gt; &amp;lt;label for=&amp;#34;radioButton6&amp;#34; class=&amp;#34;radio__label&amp;#34;&amp;gt;科技&amp;lt;/label&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;div class=&amp;#34;field&amp;#34;&amp;gt; &amp;lt;label for=&amp;#34;&amp;#34; class=&amp;#34;label&amp;#34;&amp;gt;请选择图片滤镜：&amp;lt;/label&amp;gt; &amp;lt;div class=&amp;#34;content&amp;#34;&amp;gt; &amp;lt;div class=&amp;#34;radio&amp;#34;&amp;gt; &amp;lt;input id=&amp;#34;radioButton7&amp;#34; type=&amp;#34;radio&amp;#34; class=&amp;#34;radio__button&amp;#34; value=&amp;#34;none&amp;#34; name=&amp;#34;filter&amp;#34; checked&amp;gt; &amp;lt;label for=&amp;#34;radioButton7&amp;#34; class=&amp;#34;radio__label&amp;#34;&amp;gt;正常&amp;lt;/label&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;div class=&amp;#34;radio&amp;#34;&amp;gt; &amp;lt;input id=&amp;#34;radioButton8&amp;#34; type=&amp;#34;radio&amp;#34; class=&amp;#34;radio__button&amp;#34; value=&amp;#34;grayscale&amp;#34; name=&amp;#34;filter&amp;#34; &amp;gt; &amp;lt;label for=&amp;#34;radioButton8&amp;#34; class=&amp;#34;radio__label&amp;#34;&amp;gt;黑白照&amp;lt;/label&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;div class=&amp;#34;radio&amp;#34;&amp;gt; &amp;lt;input id=&amp;#34;radioButton9&amp;#34; type=&amp;#34;radio&amp;#34; class=&amp;#34;radio__button&amp;#34; value=&amp;#34;sepia&amp;#34; name=&amp;#34;filter&amp;#34; &amp;gt; &amp;lt;label for=&amp;#34;radioButton9&amp;#34; class=&amp;#34;radio__label&amp;#34;&amp;gt;老照片&amp;lt;/label&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;div class=&amp;#34;field&amp;#34; style=&amp;#34;padding:0 10px;&amp;#34;&amp;gt; &amp;lt;div id=&amp;#34;create&amp;#34; class=&amp;#34;icon-button&amp;#34; style=&amp;#34;width: 100%;&amp;#34;&amp;gt; &amp;lt;div class=&amp;#34;icon icon--image&amp;#34;&amp;gt;&amp;lt;/div&amp;gt; &amp;lt;div class=&amp;#34;type type--small type--medium type--inverse&amp;#34;&amp;gt;插入&amp;lt;/div&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;div class=&amp;#34;icon-button loading hide&amp;#34; style=&amp;#34;width: 100%;&amp;#34;&amp;gt; &amp;lt;div class=&amp;#34;icon icon--swap&amp;#34;&amp;gt;&amp;lt;/div&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;/div&amp;gt; 官方文档里有推荐 https://github.com/thomas-lowry/figma-plugin-ds 这个仓库，提供 Figma 的 UI 库组件，会让你的插件显的更加“原生”。
由于之前说过所有的资源都需要内嵌到 html 中，所以我使用了 CDN 地址的形式引入了样式文件。另外由于功能比较简单，这里也没有使用 React 等框架去进行开发。官方模板中有 React 模板可以参考 https://github.com/figma/plugin-samples/tree/master/webpack-react
获取图片 UI 完成之后接下来我们需要实现功能。我们需要将图片下载下来插入到 Figma 图层中。由于主线程没有网络能力，所以这部分工作需要在 UI 线程中完成，再通过 postMessage 传递回主线程中完成后续操作。具体的代码如下：
&amp;lt;script&amp;gt; async function loadImage(url) { const resp = await fetch(&amp;#39;http://localhost:3000/&amp;#39; + url); const buffer = await resp.arrayBuffer(); return new Uint8Array(buffer); } document.getElementById(&amp;#39;create&amp;#39;).onclick = async (e) =&amp;gt; { const width = parseInt(document.querySelector(&amp;#39;input[name=&amp;#34;width&amp;#34;]&amp;#39;).value); const height = parseInt(document.querySelector(&amp;#39;input[name=&amp;#34;height&amp;#34;]&amp;#39;).value); const category = document.querySelector(&amp;#39;input[name=&amp;#34;category&amp;#34;]:checked&amp;#39;).value; const filter = document.querySelector(&amp;#39;input[name=&amp;#34;filter&amp;#34;]:checked&amp;#39;).value; const loading = document.querySelector(&amp;#39;.icon-button.loading&amp;#39;); e.target.classList.add(&amp;#39;hide&amp;#39;); loading.classList.remove(&amp;#39;hide&amp;#39;); const imgBytes = await loadImage(`https://placeimg.com/${width}/${height}/${category}/${filter}`); parent.postMessage({ pluginMessage: { type: &amp;#39;insert&amp;#39;, bytes: imgBytes, width: width, height: height } }, &amp;#39;*&amp;#39;); loading.classList.add(&amp;#39;hide&amp;#39;); e.target.classList.remove(&amp;#39;hide&amp;#39;); } &amp;lt;/script&amp;gt; 由于 UI 线程是一个纯 Web 环境，当我们使用 XMLHttpRequest 或者 fetch 发送请求的时候，肯定会碰到跨域的问题。按照文档 https://www.figma.com/plugin-docs/making-network-requests/ 提供的解决办法，我们只能依靠服务端加层代理来解决。
当你的插件没有 UI 面板的时候，如何进行网络请求？按照文档所说，我们是可以设置 figma.ui.show() 的第二个参数，将其设置成 visible: false 的形式创建 iframe 获取数据。
// code.ts function fetch(url, options) { const html = `&amp;lt;script&amp;gt; fetch(${url}, ${JSON.stringify(options)}).then(resp =&amp;gt; resp.json()).then(resp =&amp;gt; parent.sendMessage({ pluginMessage: { type: &amp;#39;networkRequest&amp;#39;, data: resp } }); &amp;lt;/script&amp;gt;`; return new Promise(resolve =&amp;gt; { figma.ui.on(&amp;#39;message&amp;#39;, msg =&amp;gt; msg.type === &amp;#39;networkRequest&amp;#39; &amp;amp;&amp;amp; resolve(msg.data) ); figma.ui.show(html, { visible: false }); }); } 插入图片 由于只有主线程才能操作 Figma 数据，所以需要在 UI 线程 postMessage 传递数据到主线程中继续进行操作。
主线程中的步骤就比较简单了，使用 Figma API 创建好矩形并将图片填充即可完成图片的插入。
我们可以通过设置 figma.currentPage.selection 设置选中项，并使用 figma.viewport.scrollAndZoomIntoView 将刚插入的数据滚动到视野中。
figma.ui.onmessage = msg =&amp;gt; { if (msg.type === &amp;#39;insert&amp;#39;) { const rectNode = figma.createRectangle(); const image = figma.createImage(msg.bytes); rectNode.name = &amp;#39;Image&amp;#39;; rectNode.resize(msg.width, msg.height); rectNode.fills = [{ imageHash: image.hash, scaleMode: &amp;#39;FILL&amp;#39;, scalingFactor: 0.5, type: &amp;#39;IMAGE&amp;#39; }]; figma.currentPage.appendChild(rectNode); figma.currentPage.selection = [rectNode]; figma.viewport.scrollAndZoomIntoView([rectNode]); } figma.closePlugin(); }; 除了需要显示的调用 figma.ui.show 来展示 UI 之外，在执行完插件后需要显示的调用 figma.closePlugin() 告知 Figma 进行关闭插件操作。
优化插件 上面我们实现了配置宽高然后插入一张图片。但有时候我们会先插入一个矩形占位，之后才会将其替换成图片。所以我们可以优化下操作步骤，当选中到一个矩形的时候，自动获取到它的尺寸，然后点击插入后会直接插入到该矩形中。
// code.ts function initSelectionState() { if (figma.currentPage.selection.length === 1 &amp;amp;&amp;amp; figma.currentPage.selection[0].type === &amp;#39;RECTANGLE&amp;#39;) { const rectNode = figma.currentPage.selection[0]; figma.ui.postMessage({ type: &amp;#39;update&amp;#39;, width: rectNode.width, height: rectNode.height }); } } figma.on(&amp;#39;selectionchange&amp;#39;, initSelectionState); initSelectionState(); 通过在主线程中监听 selectionchange 事件，我们能实时获取到当前选中的元素。我们将尺寸信息发送到 UI 线程后让其填充到输入框中称为默认值。
window.onmessage = function(e) { if (e.data.pluginMessage.type === &amp;#39;update&amp;#39;) { document.querySelector(&amp;#39;input[name=&amp;#34;width&amp;#34;]&amp;#39;).value = e.data.pluginMessage.width; document.querySelector(&amp;#39;input[name=&amp;#34;height&amp;#34;]&amp;#39;).value = e.data.pluginMessage.height; } } 最后再插入的时候，我们也需要判断如果有选中矩形的话则优先使用选中的矩形，而不是新增矩形。
let rectNode: RectangleNode; if (figma.currentPage.selection.length === 1 &amp;amp;&amp;amp; figma.currentPage.selection[0].type === &amp;#39;RECTANGLE&amp;#39;) { rectNode = figma.currentPage.selection[0]; } else { rectNode = figma.createRectangle(); } // const rectNode = figma.createRectangle(); 插件发布 最终我们的插件的主体功能就开发完毕了。下面我们就可以进行插件的发布了。我们可以直接通过插件管理中 Publish 操作进行发布。
和 Chrome 插件有点类似，Figma 插件支持发布到社区，也支持发布到组织。支持发布到多个组织中。发布到组织不需要审核，但只有该组织的同学和文件可使用。发布到社区的需要由 Figma 官方审核。
插件调试 由于是 Web 技术向，所以 Figma 的插件调试非常简单。直接 Command + Shift + I 打开控制台即可。
不过比较麻烦的是热更新的支持不太好。之前页面资源需要编译到 html 问价中的方式也不太友好。所以有人就想到了** iframe 套娃**来解决 UI 的更新问题。
简单来说就是通过在 UI 线程中再嵌套一个在线页面，UI 线程作为主线程和新的 iframe 的消息中转。这样相当于将插件在线化，回到了纯 Web 开发模式了，热更新自然就没有什么问题了。
不过这仅能解决 UI 线程的热更新问题，主线程如果有变化还需要重新更新插件解决。基于上面的方案，其实我们能做的更“绝”一点。我们可以将主线程变成一个壳，具体的业务代码由 iframe 下发，通过这种方式来解决主线程的更新问题。
// ui.html parent.postMessage({ pluginMessage: { type: &amp;#39;MAIN_CODE&amp;#39;, code: &amp;#39;console.log(figma)&amp;#39; } }); // code.ts figma.ui.onmessage = (msg) =&amp;gt; { msg.type === &amp;#39;MAIN_CODE&amp;#39; &amp;amp;&amp;amp; eval(msg.code); } 后记 通过示例讲述了如何开发一个 Figma 插件，包含了获取 Figma 数据信息，操作 Figma 文件等双向操作。基于以上简单操作我们可以完成更多有意义的事情帮助我们更好的开发。比如快速导出多尺寸图片、导出图标自动发布到 npm 等…
以上示例代码已发布到 GitHub 中，欢迎参考。
https://github.com/lizheming/figma-placeimg</description></item><item><title>如何制作 Sketch 插件</title><link>https://imnerd.org/how-to-write-sketch-plugin.html</link><pubDate>2020-02-01</pubDate><guid>https://imnerd.org/how-to-write-sketch-plugin.html</guid><description>Sketch 是近些年比较流行的 UI 设计软件，它比起之前常用的 Illustrator 或者 Photoshop 比较好的地方在于小巧功能简单但足够，同时对 Mac 的触摸板支持更加友好。另外它的插件系统也要比 Adobe 更加友好，大量的插件帮助我们解决协同和效率上的问题。
Sketch 插件最大的好处在于可以直接使用 JavaScript 进行开发，并提供了许多配套的开发工具。下面我就以帮助设计师同学快速插入占位图的插件 Placeholder 为例，带大家一步一步的了解如何进行 Sketch 插件开发。
在进行插件开发之前，我们需要了解一些基础的知识。Sketch 是一套原生 Objective-C 开发的软件，它之所以能支持使用 JS 开发，是因为它使用 CocoaScript 作为插件的开发语言。它就像是一座桥（Bridge），能让我们在插件中写 OC 和 JS，然后 Sketch 将基础方法进行了封装，实现了一套 JavaScript API，这样我们就能使用 JS 开发 Sketch 插件了。
注： 关于如何开发插件，官方提供了一份入门教程《Create a plugin》，在阅读下文之前，也可以花 2~3min 先看看这篇官方教程，内容比较简短。
需求整理 在进行插件开发之前，我们捋一捋我们需要实现的功能。http://placeimg.com/ 是一个专门用来生成占位图的网站，我们将利用该网站提供的服务制作一个生成指定大小的占位图并插入到 Sketch 画板中的功能。插件会提供一个面板，可以让使用者输入尺寸、分类等可选项，同时提供插入按钮，点击后会在画板插入一张图片图层。
使用 skpm 初始化项目 skpm 是 Sketch 官方提供的插件管理工具，类比于 Node.js 中的 npm。它集插件的创建、开发、构建、发布等多项功能于一体，我们在很多场景都需要使用它。安装的话比较简单，直接使用 npm 全局安装即可。
npm install -g skpm 按照官方教程，安装完毕之后我们就可以使用 skpm create 命令来初始化项目目录了。当然 skpm 是支持基于模板初始化的，官方仓库也列举了一些模板，我们可以使用 --temlate 来指定模板进行初始化。不过处于教学的目的，我这里就还是使用官方默认的模板创建了。
➜ ~ skpm create sketch-placeimg ✔ Done! To get started, cd into the new directory: cd sketch-placeimg To start a development live-reload build: npm run start To build the plugin: npm run build To publish the plugin: skpm publish skpm 内部会使用 webpack 进行打包编译，运行 npm run build 会生成 sketch-placeimg.sketchplugin 目录，该目录就是最终的插件目录。双击该目录，或者将该目录拖拽到 Sketch 界面上就成功安装插件了。和 webpack --watch 类似，运行 npm run watch 的话对监听文件变化实时编译，在开发中非常有帮助。
注： 不要使用 npm start 进行开发，它携带的 --run 命令会使得构建速度特别慢。虽然它带 Live Reload 功能会很方便，但在官方未修复该问题前还是不建议大家使用。
项目结构入门 创建好的模板目录结构如下，为了帮助大家理解，我们来简单的介绍下这些目录和文件。
. ├── README.md ├── assets │ └── icon.png ├── sketch-assets │ └── icon.sketch ├── sketch-placeimg.sketchplugin │ └── Contents │ ├── Resources │ │ └── icon.png │ └── Sketch │ ├── manifest.json │ ├── my-command.js │ └── my-command.js.map ├── node_modules ├── package.json └── src ├── manifest.json └── my-command.js package.json 和大多数 JS 项目一样，skpm 创建的项目中也会有 package.json 文件。该文件除了像之前一样记录了项目的依赖和快捷命令之外，还增加了 skpm 字段用来对 skpm 进行配置，默认的值如下。
{ ... &amp;#34;skpm&amp;#34;: { &amp;#34;name&amp;#34;: &amp;#34;sketch-placeimg&amp;#34;, &amp;#34;manifest&amp;#34;: &amp;#34;src/manifest.json&amp;#34;, &amp;#34;main&amp;#34;: &amp;#34;sketch-placeimg.sketchplugin&amp;#34;, &amp;#34;assets&amp;#34;: [ &amp;#34;assets/**/*&amp;#34; ], &amp;#34;sketch-assets-file&amp;#34;: &amp;#34;sketch-assets/icons.sketch&amp;#34; }, ... } 这里指定了该插件的名称为 sketch-placeimg，插件的 manifest 文件为 src/manifest.json。main 表示的是最终生成的插件目录名称。assets 则表示的插件依赖的图片等相关素材，在编译的时候会将命中该配置的文件拷贝到 &amp;lt;main&amp;gt;/Contents/Resources 目录下。
manifest.json manifest.json 这个文件大家可以理解为是 Sketch 插件的 package.json 文件。我们来看看默认生成的 manifest.json。
{ &amp;#34;$schema&amp;#34;: &amp;#34;https://raw.githubusercontent.com/sketch-hq/SketchAPI/develop/docs/sketch-plugin-manifest-schema.json&amp;#34;, &amp;#34;icon&amp;#34;: &amp;#34;icon.png&amp;#34;, &amp;#34;commands&amp;#34;: [ { &amp;#34;name&amp;#34;: &amp;#34;my-command&amp;#34;, &amp;#34;identifier&amp;#34;: &amp;#34;sketch-placeimg.my-command-identifier&amp;#34;, &amp;#34;script&amp;#34;: &amp;#34;./my-command.js&amp;#34; } ], &amp;#34;menu&amp;#34;: { &amp;#34;title&amp;#34;: &amp;#34;sketch-placeimg&amp;#34;, &amp;#34;items&amp;#34;: [ &amp;#34;sketch-placeimg.my-command-identifier&amp;#34; ] } } 看到 $schema 就有 JSON Schema 那味了，它对应的 JSON 文件地址告诉我们可以在里面配置那些字段。其实最重要的其实就是上面列出来的 commands 和 menu 两个字段。
commands 标记了插件有哪些命令，这里只有一个命令，命令的名称（name）是 my-command，该命令的 ID（identifier）为 sketch-placeimg.my-command-identifier，对应的执行脚本为 ./my-command.js。
menu 则标记了该插件的导航菜单配置，比如示例这里它指定了该插件在插件菜单中的名称（title）为 sketch-placeimg，并拥有一个子菜单，对应的是 ID 为sketch-placeimg.my-command-identifier的命令。通过这个 ID，菜单的行为就和执行脚本关联起来了。
appcast.xml manifest.json 默认的示例中有两个比较重要的字段没有配置，那就是 version 和 appcast。version 很明显就是用来表示当前插件的版本的。而 appcast 它的值是一个 XML 的 URL 地址，该 XML 里面包含了该插件所有的版本以及该版本对应的下载地址。Sketch 会将 version 对应的版本和 appcast 对应的 XML 进行对比，如果发现有新的版本了，会使用该版本对应的下载地址下载插件，执行在线更新插件。一个 appcast.xml 文件大概是这样的格式。
&amp;lt;?xml version=&amp;#34;1.0&amp;#34; encoding=&amp;#34;UTF-8&amp;#34; standalone=&amp;#34;yes&amp;#34;?&amp;gt; &amp;lt;rss xmlns:sparkle=&amp;#34;http://www.andymatuschak.org/xml-namespaces/sparkle&amp;#34; xmlns:dc=&amp;#34;http://purl.org/dc/elements/1.1/&amp;#34; version=&amp;#34;2.0&amp;#34;&amp;gt; &amp;lt;channel&amp;gt; &amp;lt;item&amp;gt; &amp;lt;enclosure url=&amp;#34;https://github.com/lizheming/sketch-placeimg/releases/download/v0.1.1/sketch-placeimg.sketchplugin.zip&amp;#34; sparkle:version=&amp;#34;0.1.1&amp;#34;/&amp;gt; &amp;lt;/item&amp;gt; &amp;lt;item&amp;gt; &amp;lt;enclosure url=&amp;#34;https://github.com/lizheming/sketch-placeimg/releases/download/v0.1.0/sketch-placeimg.sketchplugin.zip&amp;#34; sparkle:version=&amp;#34;0.1.0&amp;#34;/&amp;gt; &amp;lt;/item&amp;gt; &amp;lt;/channel&amp;gt; &amp;lt;/rss&amp;gt; 如果是通过 skpm publish 命令去发布插件的话，会自动在根目录生成一个 .appcast.xml 文件。当然按照官方文档 《Update a plugin》 所说，你也可以手动生成。
resource 从上面的内容我们可以知道，skpm 会通过 package.json 中指定的 manifest 文件读取所有 commands 对应的 script 文件作为编译入口文件，将这些文档编译打包输出到 &amp;lt;main&amp;gt;/Contents/Sketch 目录。所有的 assets 配置对应的文件会拷贝到 &amp;lt;main&amp;gt;/Contents/Resources 目录中。最终完成插件的生成。
换句话来说只想要走 webpack 打包编译的话就必须是插件的命令才行。如果有一些依赖的非插件类资源，比如插件嵌入的 HTML 页面依赖的 JS 文件想要走编译的话，就需要使用 resource 这个配置了。resource 配置中配置的文件会走 webpack 的编译打包，并输出到 &amp;lt;main&amp;gt;/Contents/Resources 目录中。
插件开发 一些基本原理了解清楚之后我们就可以进行插件的开发了。首先我们需要用户点击插件菜单之后打开一个面板，该面板可以配置尺寸、分类等基础信息。
Sketch 插件中我们可以使用原生写法进行面板的开发，但是这样写起 UI 来说比较麻烦，而且对前端同学来说入门比较高。所以一般大家都会采用 WebView 加载网页的形式进行开发。原理基本上等同于移动端采用 WebView 加载网页一样，客户端调用 WebView 方法加载网页，通过实例的 webContents.executeJavaScript()方法进行插件到网页的通信，而网页中则使用被重定义的 window.postMessage 与插件进行通信。
sketch-module-web-view 想要在插件中加载网页，需要安装 Sketch 封装好的 sketch-module-web-view 插件。
npm install sketch-module-web-view --save-dev // src/my-command.js import BrowserWindow from &amp;#39;sketch-module-web-view&amp;#39;; export default function() { const browserWindow = new BrowserWindow({ width: 510, height: 270, resizable: false, movable: false, alwaysOnTop: true, maximizable: false, minimizable: false }); browserWindow.loadURL(require(&amp;#39;../resources/webview.html&amp;#39;)) } 当你做完这些你会发现点击插件菜单后什么都没有发生，这是因为还需要更改一下配置。大家可以看到我们最后是使用了 require() 引入了一个 HTML 文件，而官方默认的模板是没有提供 HTML 引入的支持的，所以我们需要为 HTML 文件增加对应的 webpack loader。
我们这里需要的是 html-loader 和 @skpm/extract-loader 两款 Loader。前者是用来解析处理 HTML 中存在的包括 &amp;lt;link /&amp;gt; 或者 &amp;lt;img /&amp;gt; 之类的 HTML 代码中可能存在的资源关联情况。而后者则是用来将 HTML 文件拷贝到 &amp;lt;main&amp;gt;/Contents/Resources 目录并返回对应的 file:/// 格式的文件路径 URL，用来在插件中进行关联。
npm install html-loader @skpm/extract-loader --save-dev Sketch 插件官方为我们自定义 webpack 配置也预留好了入口，在项目根目录中创建 webpack.skpm.config.js 文件，它导出的方法接收的参数中第一个则是插件最终的 webpack 配置，我们直接在这基础上进行修改即可。
// webpack.skpm.config.js module.exports = function (config, entry) { config.module.rules.push({ test: /\.html$/, use: [ { loader: &amp;#34;@skpm/extract-loader&amp;#34; }, { loader: &amp;#34;html-loader&amp;#34;, options: { attributes: { list: [ { tag: &amp;#39;img&amp;#39;, attribute: &amp;#39;src&amp;#39;, type: &amp;#39;src&amp;#39; }, { tag: &amp;#39;link&amp;#39;, attribute: &amp;#39;href&amp;#39;, type: &amp;#39;src&amp;#39; } ] } } } ] }); } html-loader 插件在新版里对配置格式做了一些修改，所以之前很多老的教程中的配置都会报错。当然如果你有更多的插件需求也可以按照这个流程往配置对象中添加。之后我们再执行 npm run watch，点击菜单就可以看到我们预期的页面了。
注： 官方是提供了一套带有 sketch-module-web-view 模块的模板的，这里只是为了能更清楚的给大家解释清楚插件的原理和流程所以和他家一步一步的进行说明。真实的开发场景中建议大家直接使用以下命令进行快速初始化。
skpm create &amp;lt;plugin-name&amp;gt; --template=skpm/with-webview React 的集成 面板这块我准备使用 React 进行开发，主要是有 React Desktoop 这个 React 组件，能够很好的在 Web 中模拟 Mac OSX 的 UI 风格（虽然也就几个表单没什么好模拟的就是了）。
令人开心的是 skpm 默认的 webpack 配置已经增加了 React 的支持，所以我们不需要额外的增加 webpack 的配置，只需要把 React 相关的依赖安装好就可以进行开发了。
npm install react react-dom react-desktop --save-dev 增加 webview.js 入口文件。由于该文件需要走 webpack 编译，但是又不是插件命令的执行文件，所以我们需要像上文说的，将入口文件加入到 package.json 的 skpm.resources 配置中。
// package.json { &amp;#34;skpm&amp;#34;: { &amp;#34;resources&amp;#34;: [ &amp;#34;resources/webview.js&amp;#34; ] } } // resources/webview.js import React from &amp;#39;react&amp;#39;; import ReactDOM from &amp;#39;react-dom&amp;#39;; function App() { return (&amp;lt;&amp;gt; &amp;lt;p&amp;gt;Hello World!&amp;lt;/p&amp;gt; &amp;lt;hr /&amp;gt; via: &amp;lt;em&amp;gt;@lizheming&amp;lt;/em&amp;gt; &amp;lt;/&amp;gt;) } ReactDOM.render(&amp;lt;App /&amp;gt;, document.getElementById(&amp;#39;app&amp;#39;)); webview.html 也需要改造一下，引入 JS 入口文件。这里需要注意一下 ../resource_webview.js 这个引用文件地址，这是 JS 入口文件编译后最终的文件地址。主要是因为 HTML 文件最终会生成到 &amp;lt;name&amp;gt;.sketchplugin/Resources/_webpack_resources 目录下，而 JS 入口文件会将 / 分隔符替换成 _ 分隔符，生成在 &amp;lt;name&amp;gt;.sketchplugin/Resources 目录下。
&amp;lt;!DOCTYPE html&amp;gt; &amp;lt;html lang=&amp;#34;zh-CN&amp;#34;&amp;gt; &amp;lt;head&amp;gt; &amp;lt;meta charset=&amp;#34;utf-8&amp;#34; /&amp;gt; &amp;lt;title&amp;gt;PlaceIMG&amp;lt;/title&amp;gt; &amp;lt;/head&amp;gt; &amp;lt;body&amp;gt; &amp;lt;div id=&amp;#34;app&amp;#34;&amp;gt;&amp;lt;/div&amp;gt; &amp;lt;script src=&amp;#34;../resources_webview.js&amp;#34;&amp;gt;&amp;lt;/script&amp;gt; &amp;lt;/body&amp;gt; &amp;lt;/html&amp;gt; 注：
HTML 文件生成到 _webpack_resources 配置 JS 入口文件生成到 Resource 目录配置 面板开发 流程打通了之后接下来我们可以专心进行面板的开发了。面板开发这块就不多描述了，无非就是前段页面的编写而已，最后插件面板大概是长这样子的。
-_-||嗯，其实我就是想和大家讲下流程硬上 React 的…
选择完毕点击插入后，调用 postMessage() 方法将最终的配置传递给插件。
//resources/webview.js import React, {useReducer} from &amp;#39;react&amp;#39;; function App() { const [{width, height, category, filter}, dispatch] = useReducer( (state, {type, ...payload}) =&amp;gt; ({...state, ...payload}), {width: undefind, height: undefined, category: &amp;#39;any&amp;#39;, filter: &amp;#39;none&amp;#39;} ); const onInsert = _ =&amp;gt; postMessage(&amp;#39;insert&amp;#39;, width, height, category, filter); return ( &amp;lt;button onClick={onInsert}&amp;gt;插入&amp;lt;/button&amp;gt; ); } 注： Web 原生的 postMessage() 方法的语法为 postMessage(message, targetOrigin, [transfer])。事件名称和事件参数都应该序列化之后通过 message 参数传入。
Sketch 插件中的 postMessage() 方法是注入方法，它对原生的方法进行了复写，所以参数格式上会与原生的不一样。注入方法的实现可参见 sketch-module-web-view 代码。
在插件中，我们监听 insert 事件，获取到用户选择的配置之后给生成图片图层插入到画板中。
//src/my-command.js import sketch, { Image, Rectangle } from &amp;#39;sketch/dom&amp;#39;; import BrowserWindow from &amp;#39;sketch-module-web-view&amp;#39;; export default function() { const browserWindow = new BrowserWindow({...}); browserWindow.webContents.on(&amp;#39;insert&amp;#39;, function(width, height, category, filter) { const url = &amp;#39;https://placeimg.com/&amp;#39; + [width, height, category, filter].join(&amp;#39;/&amp;#39;); new Image({ image: NSURL.URLWithString(url), parent: getSelectedArtboard(), frame: new Rectangle(0, 0, width, height), }); return browserWindow.close(); }); } 插件发布 最终我们的插件的主体功能就开发完毕了。下面我们就可以进行插件的发布了。我们可以直接使用 skpm publish 进行发布，它需要你通过 skpm publish --repo-url 或者是 package.json 中的 repository 字段为插件指定 Github 仓库地址。
在 Personal Access Token 页面为 skpm 申请新的 Token，记得勾选上 repo 操作的权限。使用 skpm login &amp;lt;token&amp;gt; 进行登录之后，skpm 就获得了操作项目的权限。
最后通过 skpm publish &amp;lt;version&amp;gt; 就可以成功发布了。如前文所说，发布后会在项目目录创建 .appcast.xml 文件，同时会发布一条对应版本的 Release 记录，提供插件的 zip 包下载地址。执行完 publish 操作后，如果发现你的插件还没有在插件中心仓库中列出来，还会询问你是否提交个 PR 把自己的插件增加上。
当然如果你的插件不方便发布到 Github 上，也可以使用前文所说的手工发布，执行 skpm build 后对生成的 &amp;lt;name&amp;gt;.sketchplugin 目录进行打包即可。
插件调试 上文的示例插件比较简单，所以没有使用特别多的调试手段。在官方教程《Debug a plugin》中描述了多种可以进行调试的方式。用的比较多的还是日志调试方式，可以使用系统的 Console.app 查看日志，也可以使用 skpm log -f 插件日志。
文档里说的大部分是插件的调试，WebView 内的前端代码调试会更简单一点。WebView 窗体右键审查元素即可使用 Safari 的开发者工具进行调试了。
注： 插件本身的代码本质是客户端代码，WebView 本质是前端代码，所以两者的调试和日志输出位置都是有区别的，这里要注意区分。
后记 以上就是开发 Sketch 的一些基础知识和简单流程，其它的就是多去看一下 Sketch API 文档了。不过在实际的使用中 Sketch 的这套 JavaScript API 并不是非常完美，部分功能可能还暂时需要使用原生 API 区别。这时候可以多 Google 一下，能找到很多前人的实现，节省自己的工作量。
本文主要是介绍了一套 JavaScript API + WebView 的偏前端的开发方式，代码我都已经放到 Github 上 https://github.com/lizheming/sketch-placeimg，大家可以自行查阅和下载。除了这种方式之外，我们也可以使用 OC + WebView 甚至是纯 OC 客户端的方式去开发插件。使用纯客户端开发的话性能会比 JavaScript API 的形式好一点，但是对于不了解 OC 开发的前端同学来说上手难度还是比较高的。
除了 Sketch 之外，Figma 也是一款非常棒的 UI 设计软件。它基于 Web 开发，天生跨平台，更提供了更加易用的协作模式，解决 UI 开发中的多人协作问题。感兴趣的同学也可以去了解一下。
参考资料：
《Sketch插件开发总结》</description></item></channel></rss>