站内搜索

“插件” 找到 68 条结果

如何制作 Sketch 插件

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 进行配置,默认的值如下。

{
  ...
  "skpm": {
    "name": "sketch-placeimg",
    "manifest": "src/manifest.json",
    "main": "sketch-placeimg.sketchplugin",
    "assets": [
      "assets/**/*"
    ],
    "sketch-assets-file": "sketch-assets/icons.sketch"
  },
  ...
}

这里指定了该插件的名称为 sketch-placeimg,插件的 manifest 文件为 src/manifest.json。main 表示的是最终生成的插件目录名称。assets 则表示的插件依赖的图片等相关素材,在编译的时候会将命中该配置的文件拷贝到 <main>/Contents/Resources 目录下。

manifest.json

manifest.json 这个文件大家可以理解为是 Sketch 插件的 package.json 文件。我们来看看默认生成的 manifest.json。

{
  "$schema": "https://raw.githubusercontent.com/sketch-hq/SketchAPI/develop/docs/sketch-plugin-manifest-schema.json",
  "icon": "icon.png",
  "commands": [
    {
      "name": "my-command",
      "identifier": "sketch-placeimg.my-command-identifier",
      "script": "./my-command.js"
    }
  ],
  "menu": {
    "title": "sketch-placeimg",
    "items": [
      "sketch-placeimg.my-command-identifier"
    ]
  }
}

看到 $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 文件大概是这样的格式。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
  <channel>
    <item>
      <enclosure url="https://github.com/lizheming/sketch-placeimg/releases/download/v0.1.1/sketch-placeimg.sketchplugin.zip" sparkle:version="0.1.1"/>
    </item>
    <item>
      <enclosure url="https://github.com/lizheming/sketch-placeimg/releases/download/v0.1.0/sketch-placeimg.sketchplugin.zip" sparkle:version="0.1.0"/>
    </item>
  </channel>
</rss>

如果是通过 skpm publish 命令去发布插件的话,会自动在根目录生成一个 .appcast.xml 文件。当然按照官方文档 《Update a plugin》 所说,你也可以手动生成。

resource

从上面的内容我们可以知道,skpm 会通过 package.json 中指定的 manifest 文件读取所有 commands 对应的 script 文件作为编译入口文件,将这些文档编译打包输出到 <main>/Contents/Sketch 目录。所有的 assets 配置对应的文件会拷贝到 <main>/Contents/Resources 目录中。最终完成插件的生成。

换句话来说只想要走 webpack 打包编译的话就必须是插件的命令才行。如果有一些依赖的非插件类资源,比如插件嵌入的 HTML 页面依赖的 JS 文件想要走编译的话,就需要使用 resource 这个配置了。resource 配置中配置的文件会走 webpack 的编译打包,并输出到 <main>/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 'sketch-module-web-view';
export default function() {
  const browserWindow = new BrowserWindow({
    width: 510,
    height: 270,
    resizable: false,
    movable: false,
    alwaysOnTop: true,
    maximizable: false,
    minimizable: false
  });
  browserWindow.loadURL(require('../resources/webview.html'))
}

当你做完这些你会发现点击插件菜单后什么都没有发生,这是因为还需要更改一下配置。大家可以看到我们最后是使用了 require() 引入了一个 HTML 文件,而官方默认的模板是没有提供 HTML 引入的支持的,所以我们需要为 HTML 文件增加对应的 webpack loader。

我们这里需要的是 html-loader 和 @skpm/extract-loader 两款 Loader。前者是用来解析处理 HTML 中存在的包括 <link /> 或者 <img /> 之类的 HTML 代码中可能存在的资源关联情况。而后者则是用来将 HTML 文件拷贝到 <main>/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: "@skpm/extract-loader" },
      {
        loader: "html-loader",
        options: {
          attributes: {
            list: [
              { tag: 'img', attribute: 'src', type: 'src' },
              { tag: 'link', attribute: 'href', type: 'src' }
            ]
          }
        }
      }
    ]
  });
}

html-loader 插件在新版里对配置格式做了一些修改,所以之前很多老的教程中的配置都会报错。当然如果你有更多的插件需求也可以按照这个流程往配置对象中添加。之后我们再执行 npm run watch,点击菜单就可以看到我们预期的页面了。

注: 官方是提供了一套带有 sketch-module-web-view 模块的模板的,这里只是为了能更清楚的给大家解释清楚插件的原理和流程所以和他家一步一步的进行说明。真实的开发场景中建议大家直接使用以下命令进行快速初始化。

skpm create <plugin-name> --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
{
  "skpm": {
    "resources": [
      "resources/webview.js"
    ]
  }
}

// resources/webview.js
import React from 'react';
import ReactDOM from 'react-dom';

function App() {
  return (<>
    <p>Hello World!</p>
    <hr />
    via: <em>@lizheming</em>
  </>)
}

ReactDOM.render(<App />, document.getElementById('app'));

webview.html 也需要改造一下,引入 JS 入口文件。这里需要注意一下 ../resource_webview.js 这个引用文件地址,这是 JS 入口文件编译后最终的文件地址。主要是因为 HTML 文件最终会生成到 <name>.sketchplugin/Resources/_webpack_resources 目录下,而 JS 入口文件会将 / 分隔符替换成 _ 分隔符,生成在 <name>.sketchplugin/Resources 目录下。

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8" />
    <title>PlaceIMG</title>
  </head>
  <body>
    <div id="app"></div>
    <script src="../resources_webview.js"></script>
  </body>
</html>

注:

  1. HTML 文件生成到 _webpack_resources 配置
  2. JS 入口文件生成到 Resource 目录配置

面板开发

流程打通了之后接下来我们可以专心进行面板的开发了。面板开发这块就不多描述了,无非就是前段页面的编写而已,最后插件面板大概是长这样子的。

-_-||嗯,其实我就是想和大家讲下流程硬上 React 的…

选择完毕点击插入后,调用 postMessage() 方法将最终的配置传递给插件。

//resources/webview.js
import React, {useReducer} from 'react';

function App() {
  const [{width, height, category, filter}, dispatch] = useReducer(
    (state, {type, ...payload}) => ({...state, ...payload}),
    {width: undefind, height: undefined, category: 'any', filter: 'none'}
  );
  const onInsert = _ => postMessage('insert', width, height, category, filter);
  return (
    <button onClick={onInsert}>插入</button>
  );
}

注: Web 原生的 postMessage() 方法的语法为 postMessage(message, targetOrigin, [transfer])。事件名称和事件参数都应该序列化之后通过 message 参数传入。

Sketch 插件中的 postMessage() 方法是注入方法,它对原生的方法进行了复写,所以参数格式上会与原生的不一样。注入方法的实现可参见 sketch-module-web-view 代码。

在插件中,我们监听 insert 事件,获取到用户选择的配置之后给生成图片图层插入到画板中。

//src/my-command.js
import sketch, { Image, Rectangle } from 'sketch/dom';
import BrowserWindow from 'sketch-module-web-view';

export default function() {
  const browserWindow = new BrowserWindow({...});
  browserWindow.webContents.on('insert', function(width, height, category, filter) {
    const url = 'https://placeimg.com/' + [width, height, category, filter].join('/');
    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 <token> 进行登录之后,skpm 就获得了操作项目的权限。

最后通过 skpm publish <version> 就可以成功发布了。如前文所说,发布后会在项目目录创建 .appcast.xml 文件,同时会发布一条对应版本的 Release 记录,提供插件的 zip 包下载地址。执行完 publish 操作后,如果发现你的插件还没有在插件中心仓库中列出来,还会询问你是否提交个 PR 把自己的插件增加上。

当然如果你的插件不方便发布到 Github 上,也可以使用前文所说的手工发布,执行 skpm build 后对生成的 <name>.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 开发中的多人协作问题。感兴趣的同学也可以去了解一下。

参考资料:

  1. 《Sketch插件开发总结》

使用 SVG 制作加载动画

最近我们设计师反馈,他想要做如下的一个加载动画。但是要么效果好的导出的 GIF 体积特别大,看了下有 8M 多了,要么体积小的 GIF 效果又特别不清楚。然后我看了下效果,发现其实用 SVG 动画来实现应该比较简单,于是就和设计师要了一下原始的稿子导出成 SVG 后处理了下。

将 AE 动效稿子转成 SVG 动画的话 Airbnb 有出过一款 Lottie 的工具。通过它的 AE 插件 Bodymovin 能够以 JSON 的形式导出动画信息和素材。然后在网页上使用 bodymovin.js 动画播放库载入该 JSON 素材即可完成动效的转换。具体的使用教程可以参考 Youtube 视频《How to export an animation with Bodymovin》。

使用 Bodymovin 是真的非常方便,不过介于设计师需要的效果比较简单,为了这个效果而每次去加载一个几十KB的基础库和JSON文件实在是没有必要。所以我这里就基于 SVG + CSS 动画来实现了下,最终的效果如下。最终体积也就 6KB,gzip 后会更小。 下面就来跟着我一块一步步的实现它吧!这里我不会特别详细的描述每一步的基本原理,如果大家想了解 SVG 动画的基础知识的话可以先看看我之前写的文章《SVG 动画实践》。

动画拆分分析

通过观察发现该动画主要用到了平移、旋转、透明度,宽度和颜色等属性变化等动画效果。这些都可以通过 CSS3 动画来实现,剩下的我们需要对这些动画进行拆分,先分别实现它们。最后将他们组合,通过一定的时间配合实现完整的效果。

在这里我将该动效最终拆分成了以下几个部分:

  • 外圈的波纹效果
    • 外圈1的波纹效果
    • 外圈2的波纹效果
    • 外圈3的波纹效果
  • 主体的伸展运动
    • 主体绿色部分的平移伸展
      • 主体绿色部分上的平移伸展
      • 主体绿色部分下的平移伸展
    • 主体白色圆球的渐隐效果
    • 主体蓝色部分的平移伸展
    • 主体白色横条的渐隐效果
    • 主体部分的自转
  • 蓝球的公转效果

每一个单独的动画效果我们都需要对其进行处理,所以我们需要对导出的 SVG 进行元素的整理,将我们需要进行操作的元素进行分组标记。由于 Sketch 导出的 SVG 文件会带有比较多的冗余元素,所以我一般会在手工处理 SVG 之前在走一遍 svgo 这类工具对内容进行优化。这里推荐张鑫旭老师写的 SVG 在线压缩合并工具,直接粘贴 SVG 代码过去即可,非常简单。下面是 SVG 整体结构的示意代码。

<svg width="552px" height="552px" viewBox="0 0 552 552" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  <!--外圈-->
  <g id="track-list">
    <!--外圈1-->
    <circle id="track-circle-1" />
    <!--外圈2-->
    <circle id="track-circle-2" />
    <!--外圈3-->
    <circle id="track-circle-3" />
  </g>
  <!--中间主体-->
  <g id="main">
    <!--主体绿色部分下-->
    <g id="bottom-triangel">
      <path d="..." />
    </g>
    <!--主体绿色部分上-->
    <g id="top-triangel">
      <path id="shadow" d="..." />
      <path d="..." />
      <!--主体白色圆球-->
      <circle id="white-ball" />
    </g>
    <!--主体蓝色部分-->
    <g id="right-triangel">
      <path d="..." />
      <!--主体白色横条-->
      <rect id="white-line" />
    </g>
   </g>
   <!--外圈蓝球-->
   <circle id="blue-ball" />
</svg>

可以看到我对 SVG 内的元素进行了重新的整理,将需要操作的元素都加上了 id 属性,方便后续直接使用 CSS 选择器选择对象进行操作。另外所有需要一块进行操作的元素也都使用 <g /> 分组标签进行了包裹。

外圈的波纹效果

外圈的波纹效果本质上就是圆的半径慢慢放大,效果里还伴随了圆的边框变窄的一个过程。其中三个圆最内层的那个是不需要动的,只需要动后面两个即可。从设计稿中拿到结束帧的状态之后这个动画做起来就比较容易了。

#track-circle-2 {
  animation-name: wave1;
  animation-timing-function: ease-in-out;
  animation-duration: 6s;
  animation-iteration-count: infinite;
}

#track-circle-3 {
  animation-name: wave2;
  animation-timing-function: ease-in-out;
  animation-duration: 6s;
  animation-iteration-count: infinite;
}
@keyframes wave1 {
  50% {
    stroke-width: 4;
    r: 219px;
  }
}

@keyframes wave2 {
  50% {
    stroke-width: 3;
    r: 274.5px;
  }
}

主体的伸展运动

这块是整个里面比较复杂的一部分了,不过通过拆分,我们发现实现起来也比较简单,先实现内部元素的平移,然后再补充上整体的自转效果即可。平移这块没有什么多说的,唯一麻烦的就是通过起始帧和结束帧的位置计算出需要移动的距离而已。如图最终白线标记的位置就是我们需要的平移位置啦。

#top-triangel { animation: topmove ease-in-out 2s infinite; }
#shadow { animation: shadowhide linear 2s infinite; }
#bottom-triangel { animation: bottommove ease-in-out 2s infinite; }
#right-triangel { animation-name: rightmove ease-in-out 2s infinite; }
@keyframes topmove {
  from, to { transform: translate(0, 0); }
  50% { transform: translate(-31px, -30px); }
}
@keyframes bottommove {
  from, to { transform: translate(0, 0); }
  50% { transform: translate(-31px, 30px); }
}
@keyframes rightmove {
  from, to { transform: translate(0); }
  50% { transform: translate(29px); }
}
@keyframes shadowhide {
  30%, 70% { opacity: 0; }
}

对了,别忘记我们刚才的动画拆分里还有白色圆球和白色横条的渐隐效果。渐隐效果可以使用 opacity 透明度来实现,不过这里除了渐隐之外还有一个大小的变化,可能使用呼吸效果来表述会更合适一点。圆的大小就是修改半径,横条的大小我们直接修改宽度就可以了。

#white-ball { animation: balltransparent ease-in-out 2s infinite; }
#white-line { animation: linetransparent ease-in-out 2s infinite; }
@keyframes balltransparent {
  50% {
    opacity: 0;
    transform: scale(0);
  }
}
@keyframes linetransparent {
  50% {
    opacity: 0;
    width: 0px;
  }
}

根据刚才的动画拆分,主体部分我们就还剩下一个自转没有实现了。在做这一部分的时候需要注意两点。第一,旋转默认是基于 SVG 画布的左上角进行旋转的,自转的话一般都是基于中心旋转,所以一定要记得设置 transform-origin 为中心点。第二,Sketch 导出的 SVG 会存在大量的 translate() 平移属性操作,有可能是最开始设计师画的时候是在某个位置,后来觉得不合适进行了移动,在 SVG 里就会以平移变换体现出来。这个时候如果我们直接使用 transform 进行变换的话实际上是会复写掉它们原本的平移的,这样就导致了之前设置的旋转圆心不正确的问题。

所以这种情况下需要使用联合变换,将之前的平移变换补充到 CSS 的变换中来就可以了,这也是为什么代码中会多出两个 translate() 的原因。

#main {
  animation: mainrotate linear 6s infinite;
  transform-origin: center center;
}

@keyframes mainrotate {
  from {
    transform: translate(0, 0) rotateZ(0deg) translate(-72px, -42px);
  }
  to {
    transform: translate(0, 0) rotateZ(360deg) translate(-72px, -42px);
  }
}

下面就是最终的实现效果。怎么样,是不是感觉已经离胜利不远了!

蓝球的公转效果

动画拆分里的最后一步就是蓝球的公转效果了。从上文我们知道,旋转我们是可以设置旋转圆心的。自转是围绕自己转的,所以旋转圆心是自己的中心,公转则是围绕太阳转的,所以旋转圆心是太阳的圆心,对应到我们的动效里其实就是整个画布的中心。

在这里我还使用了 CSS 表达角度的另外一个单位 turn,它表示的是圈数,转 360° 就表示 1turn。除了 turn 之外,CSS 角度还有 grad 梯度和 rad 弧度这两个单位。grad 则是将一个圆划分成了400等分,转 360° 就表示 400grad。而 rad 弧度则和我们数学上的弧度表示基本一致,一个圆总共是 2πrad。

#blue-ball {
  animation: spin linear 6s infinite;
  transform-origin: center center;
}

@keyframes spin {
  from { transform: rotate(0turn); }
  to { transform: rotate(1turn); }
}

后记

最后将上面的代码拼凑起来就可以实现文章开头的动画效果了,是不是还挺简单的。另外在 SVG 标签中也支持内嵌 <style> 和 <script> 标签,所以我们可以直接将样式内嵌在 SVG 文件中,这样我们就可以和引用 GIF 一样直接使用 <img> 或者背景图片的形式使用 SVG 而不需要其他负担,在一些不支持内嵌样式的 Markdown 网站比如 Github 中效果奇佳哦!

使用 React 为 Chimee 开发插件

Chimee 是由奇舞团开源的一套可扩展的H5组件化播放器框架。由于前段时间业务有视频播放的需求所以使用了它,并基于它提供的插件系统之上开发了一系列的插件,其中最复杂的是控制条插件。由于默认的样式无(实)法(在)满(是)足(太)设(难)计(看)需(了)求(!),所以我们重新开发了一套 lizheing/chimee-plugin-controlbar 并总结一些心得,希望对大家有帮助。

Chimee 插件规范

开篇之前我们先简单的了解下如何开发一款 Chimee 的插件,文档 提供了一个非常简单的示例告诉我们大概的流程。

const plugin = {
  // 插件名为 controller
  name: 'controller',
  // 插件实体为按钮
  el: '<button>play</button>',
  data: {
    text: 'play'
  },
  methods: {
    changeVideoStatus () {
      this[this.text]();
    },
    changeButtonText (text) {
      this.text = text;
      this.$dom.innerText = this.text;
    }
  },
  // 在插件创建的阶段,我们为插件绑定事件。
  create () {
    this.$dom.addEventListener('click', this.changeVideoStatus);
  },
  // 插件会在播放暂停操作发生后改变自己的文案及相应的行为
  events: {
    pause () {
      this.changeButtonText('play');
    },
    play () {
      this.changeButtonText('pause');
    }
  }
};

// 安装插件
Chimee.install(plugin);
const player = new Chimee({
  // 播放地址
  src: 'http://cdn.toxicjohann.com/lostStar.mp4',
  // dom容器
  wrapper: '#wrapper',
  // 使用插件
  plugin: ['controller'],
});

通过以上示例代码我们可以发现 Chimee 的插件的规则主要有以下几点:

  • 插件需要暴露一个对象,并带有 name 属性用来标记插件名称,方便后续实例化的时候指定。
  • HTML 模板相关的内容需要放在 el 属性中,在其它方法中可以通过 this.$dom 访问到 DOM 对象。
  • data 属性用来存放数据,methods 属性可以用来存放方法。两个属性中的数据可以直接通过 this.xx 的形式访问到。
  • events() 属性可以放置一些播放器的事件回调,Chimee 会自动做事件的绑定。

从以上发现的几点来看,我们可以大致估摸出插件的简单逻辑。无非就是将 data 和 methods 中的数据绑定到 this 上,然后插入 el 的 HTML 到页面中,然后执行一些对应的生命周期方法。

听起来似乎比较 Vue?虽然写法上是这样的,但是明眼人能看出来其实这个流程还是 DOM 操作,所以必然会碰上原生 DOM 开发的两个问题:

  • data 虽然能存放数据,但是数据无法直接映射到 el 的 HTML 模板中,导致还是需要回归原本的 DOM 操作。
  • 事件绑定需要等待 DOM 生成后在生命周期中进行手动的绑定,又回到了 DOM 操作时一堆 addEventListener 的年代了。

由于我们的业务是使用 React 开发的,所以就想是否可以通过引入 React 来解决上述 DOM 操作的问题。

React 开发

使用 React 开发虽然能解决上段提到的两个问题,但是插件本身的配置以及生命周期却是不太好在 React 中实现。要想实现插件即组件,组件即插件的场景需要对现有的一些生命周期进行映射,这成本就有点高了。所以最终退而求其次,只有 DOM 这块使用 React,外层依旧是正常的 Chimee 插件的配置。

import React from 'react';
import ReactDOM from 'react-dom';

import App from './App';
import Context from './Context';

export default {
  Context,
  name: 'plugin-demo',
  el: `<div class="plugin"></div>`,
  create() {
    ReactDOM.render(
      <Context.Provider value={this}>
        <App />
      </Context.Provider>,
      this.$dom
    );
  }
};

Context 解决访问插件方法问题

而为了解决 React 组件内部需要获取 Chimee 的方法的问题,我们通过 Context 的方式将 this 这个 Chimee 插件实例传入,内部通过 Context 获取到实例之后就可以为所欲为了。

import React, { useContext, useState, useEffect } from 'react';
import Context from '../Context';

export default function () {
  const ctx = useContext(Context);
  const [cur, setCur] = useState(0);
  useEffect(() => {
    ctx.$on('timeupdate', () => setCur(ctx.currentTime));
  }, []);

  return (
    <div className="play--time">{cur}</div>
  )
}

不得不感谢 React Hooks 的出现,让 Context 的 API 变的如此的简单。如果要像之前一样写 <Context.Consumer /> 然后再来个 render props 估计也是够烦的。当然使用 Context 只是为了方便在组件层级比较深的情况下数据的透传,这也是 Context 的本来意义。如果插件的组件层级不深直接使用 Props 传递也是可以的。

事件监听解决外部数据触发更新问题

目前这套插件的模式已经能够满足大部分日常插件的开发了,不过碰到了异步就有点问题了。我们知道的是插件的配置在 new Chimee() 那一刻就已经固定下来了,外部的数据的更新并不会影响内部的配置,也就无法造成插件的更新渲染,毕竟目前 Chimee 没有一个类似 componentWillReceiveProps() 的生命周期。如果 Chimee 自己本身配置都无法更新上,就更遑论触发 React 的更新了。

+--------+     +--------+     +---------------+     +-----------------+
| config +---->+ Chimee +---->+ Chimee Plugin +---->+ React Component |
+--------+     +----+---+     +---------------+     +-----------------+
                    ×
+----------------+  |
| updated config +--×
+----------------+

回到具体的业务场景上,我们有一个需求是控制条需要显示下一条视频的标题和封面图,而这个数据是在业务代码中通过接口获取得到的,所以在最开始实例化播放器的时候我们是没办法传入该数据。最后我们决定用事件监听的方式解决该问题,通过自定义事件进行跨组件层级的数据传递。

+--------+     +--------+     +---------------+     +-----------------+
| config +---->+ Chimee +---->+ Chimee Plugin +---->+ React Component |
+--------+     +--------+     +---------------+     +--------+--------+
                                                             ^
+----------------+this.$emit('customEvent')+-----------------+--------+
| updated config +------------------------>+ this.$on('customeEvent') |
+----------------+                         +--------------------------+

多插件先后问题

随着业务越来越复杂,播放器在同一事件要处理的事情越来越多,例如我们碰到的片尾需要先播放片尾广告,广告播放完之后显示倒计时,倒计时后再触发播放下一条的动作。如何在同一个事件中保证三者的顺序成了本次需求的关键。

好在当初 Chimee 在设计之初就已经考虑到了这个问题。先执行的回调只要返回 false 即可阻止整个事件,当该插件完成任务后只要重新触发事件即可。

import React, {useEffect} from 'react';
import Context from './context';

export default function() {
  const ctx = useContext(Context);

  useEffect(() => {
    const onEnded = event => {
      if(event === 'manual') {
        return true;
      }

      setTimeout(() => ctx.$emit('ended', 'manual'), 10000);
      return false;
    };

    ctx.$on('ended', onEnded);
    return () => ctx.$off('ended', onEnded);
  });

  return (<div>推迟10秒后结束</div>);
}

在 ended 事件触发后使用 return false 阻止事件,然后等事件执行完毕之后使用 ctx.$emit('ended', 'manual') 重新触发事件。这里有个需要注意的地方,后者触发的事件中又会再次将当前插件注册的回调事件执行一遍,所以这里增加了第二个参数用来让回调方法判断是否是插件自己触发的,如果是自己触发的则表示无须再次执行直接跳过。

后记

Chimee 的插件系统可扩展性比较强,如果对其比较感兴趣的可以看看官方的这篇《为什么要将 Chimee 设计成一个组件化框架?》。使用 React 开发插件解决了事件和数据绑定的问题,该开发模式下基本上就是废弃了官方自己提供的 data, methods 和 events 三个属性,前两者自然是由 React 内部的 state 和方法提供,后者则是自行在组件内部使用 ctx.$on 的形式手动监听。

我的第一款Drone插件

Drone 是一款基于Docker的CI工具,不仅运行环境是容器化的,甚至于 CI 流中的每一个插件也是 docker 容器。基于容器的 Drone 插件一大特色就是允许我们使用任何语言去开发插件,Python, PHP, Nodejs, bash... 这些都是可以的。每个开发者都可以使用自己熟悉的语言去开发自己想要的插件。Drone 的官方插件市场罗列了很多插件,并且在文档提供了 Golang 和 Bash 版本的插件开发文档,并开源了一个插件脚手架。

官方插件里有一个 drone-telegram 插件,不过插件目前是不支持自定义 telegrame 请求地址的,对于我这种机器部署在国内需要使用反代的方式访问 telegram 的用户来说不是很方便。搜索到 node-telegram-api 这个模块可以配置,下面我就简单说说基于 Nodejs 写一个 telegram 消息通知的插件。

原理

Docker 容器在启动的时候可以设置一个启动命令,这样我们就可以做到实例化插件容器的时候就执行插件脚本。另外 Drone 运行插件的时候会将本次 CI 构建相关的信息以及插件配置信息以环境变量的形式注入到插件容器实例中。所有的语言都是可以去读取系统环境变量的,这也让其支持所有语言成为了可能。

  • 插件配置会以 PLUGIN_xxx 的形式注入,例如 PLUGIN_TOKEN。
  • 构建相关的信息会以 CI_xxx 或者 DRONE_xxx 的形式注入,例如 DRONE_COMMIT, CI_COMMIT_AUTHOR_NAME。

编写插件

编写插件很简单,只需要注意需要获取的配置使用 PLUGIN_ 获取即可,例如 telegram 需要使用的 PLUGIN_TOKEN 等。下面是具体的插件代码:

const render = require('drone-render');
const TelegramBot = require('node-telegram-bot-api');

const {
  PLUGIN_TOKEN,
  PLUGIN_TO,
  TELEGRAM_TOKEN,
  TELEGRAM_TO,

  PLUGIN_LANG,
  PLUGIN_MESSAGE,
  PLUGIN_BASE_API_URL
} = process.env;

const TOKEN = PLUGIN_TOKEN || TELEGRAM_TOKEN;
const TO = PLUGIN_TO || TELEGRAM_TO;

if(PLUGIN_LANG) {
  render.locale(PLUGIN_LANG);
}
const bot = new TelegramBot(TOKEN, {
  baseApiUrl: PLUGIN_BASE_API_URL
});
bot.sendMessage(TO, render(PLUGIN_MESSAGE));

可以看到其实代码非常的简单。

Dockerfile

插件写好之后就需要创建 Dockerfile 文件制作镜像了。由于 Dockerfile 的每一个指令都会为容器创建一个layer,所以这里有个小技巧是将 npm install 依赖安装这种不怎么会变化的操作提前,频繁变化的插件脚本的拷贝置后。这样在多次打包发布的时候会占优势,仅仅只会传后面变化的部分,减小传输体积。

FROM mhart/alpine-node:8.9.3

WORKDIR /telegram-node
COPY package.json /telegram-node/package.json
RUN npm install

COPY index.js /telegram-node/index.js
ENTRYPOINT [ "node", "/telegram-node/index.js" ]

最后就是编译并发布我们的 docker 容器以供其它人方便使用:

docker build lizheming/drone-telegram-node .
docker push lizheming/drone-telegram-node

测试

本地使用如下命令启动镜像即可测试插件是否工作

docker run --rm \
  -e PLUGIN_TOKEN=xxxxxxx \
  -e PLUGIN_TO=xxxxxxx \
  -e PLUGIN_MESSAGE=test \
  -e PLUGIN_BASE_API_URL=xxxx \
  lizheming/drone-telegram-node

问题

secrets 参数获取

经常有些密钥之类的参数不想写在 .drone.yml 配置文件中暴露给所有项目可见的用户,这时候我们就会使用 drone secrets 添加变量,具体可参考我之前写的文章《如何使用Drone》。这里需要注意的是 secrets 定义的变量名即最后 drone 注入进入的环境变量名称,所以不能随意使用。例如你的配置是:

pipline:
  telegram:
    image: lizheming/drone-telegram-node
    secrets: [ telegram_token, telegram_to ]
    message: hello

最后你在插件内部就能使用 process.env.TELEGTAM_TOKEN 来获取对应的密钥变量。

插件不生效

在编写插件的时候我碰到的一个问题是我最开始的 Dockerfile 是如下这样的:

FROM mhart/alpine-node:8.9.3

WORKDIR /telegram-node
COPY package.json /telegram-node/package.json
RUN npm install

COPY index.js /telegram-node/index.js
ENTRYPOINT [ "node", "index.js" ]

由于我设置了 WORKDIR 工作目录,正常的 docker run 是可以找到我的入口文件为 ${WORKDIR}/index.js 的。但是 drone 在执行插件的时候由于需要访问项目代码,所以会通过命令覆盖掉镜像的 WORKDIR 配置。导致了我的插件正常运行没问题,但是上到 drone 怎么也执行不了。最后查了才发现是因为复现了工作目录配置后入口文件找不到压根就没执行我的插件的问题。

后记

插件的所有代码已经发布在 Github:https://github.com/lizheming/drone-telegram-node,镜像也已发布到 docker 平台上。本插件相对于官方的插件的优势是能够设置 telegram API 接口的地址,对于国内服务使用反代的方式来说比较适合。如果想要在国内服务器上使用 drone telegram 消息通知的可以使用。不知道如何设置 telegram API 反代的也可以参考我的文章《反代访问Telegram》。

使用 Caddy 代替 Nginx

Caddy 是一款由 Go 编写的 Web Server 工具,在折腾 Drone 的时候,我最终就是使用的它提供 Web 服务。回到标题,为什么要用 Caddy 替换掉 Nginx 呢?最主要的原因是 Caddy 能让网站自动支持 HTTPS。同样是使用 Let's Encrypt,换成 Nginx 我们就必须手工操作,并且还需要设置三个月更新证书的计划任务。而且默认还支持 http/2,很多事情都不需要我们再配置了。另外它的配置文件也比 Nginx 的要简单很多,几十行的 Nginx 配置文件 Caddy 仅需要几行就可以搞定了。

安装 Caddy

Caddy 提供了一键安装命令生成功能,访问 https://caddyserver.com/download 选择对应的平台,插件可默认为空,最后选择对应的协议后下方即会生成一键安装命令,例如:

$ curl https://getcaddy.com | bash -s personal

安装后执行 which caddy 你会发现 Caddy 已经安装到了 /usr/local/bin/caddy 位置。

caddy命令生成

$ echo 'localhost:8000' >> Caddyfile
$ caddy

执行如上命令在浏览器中访问 http://localhost:8000 发现 Caddy 启动了一个简单的 HTTP 服务器。

后台启动

首先我们为 Caddy 创建一个配置目录,这里以 /etc/caddy 为例。同时我们还需要生成一个目录用来给 Caddy 放置网站证书。

$ sudo mkdir /etc/caddy
$ sudo mkdir /etc/caddy/conf
$ echo 'import ./conf/*' >> /etc/caddy/Caddyfile
$ sudo mkdir /etc/ssl/caddy

从官方仓库中下载 caddy 的 systemctl 守护脚本并对其挂载启动。

$ sudo curl -s https://raw.githubusercontent.com/mholt/caddy/master/dist/init/linux-systemd/caddy.service -o /etc/systemd/system/caddy.service
$ sudo systemctl daemon-reload
$ sudo systemctl enable caddy
$ sudo systemctl start caddy

这样我们就完成 caddy 后台启动任务,可以使用 status 命令查看 caddy 的状态:

$ sudo systemctl status caddy

Caddyfile

Caddyfile 是 Caddy 的配置文件,刚才我们写入了一条 import ./conf/* 命令,表示将 /etc/caddy/conf/ 下的所有文件都导入到配置文件中。下面我就以几个大家常见的需求列举一下如何编写 Caddfile 配置。

静态资源

和 Nginx 一样,Caddy 也有一个 root 配置。静态资源的访问只要设置到对应的 root 即可。同时我们也为资源配置了 gzip 压缩功能。

$ sudo cat << EOF > /etc/caddy/conf/static.example.com
static.example.com {
    gzip
    root    /var/www/static.example.com
}
EOF
$ sudo systemctl restart caddy

反向代理

反向代理请求到另外的端口算是我们比较常见的需求了,通过 proxy 指令我们可以非常简单的做到。以 ThinkJS 的配置为例,我们在静态资源配置的基础之上,增加了 proxy 指令将请求反向代理到了 http://127.0.0.1:8360 端口。由于反向代理后丢失了原始请求的 HOST,我们使用 transparent 指令增加 X-* 头。最后我们使用 except 指令排除了 /static 的请求,因为这些请求是静态资源的不需要反代。

$ sudo cat << EOF > /etc/caddy/conf/thinkjs.example.com
thinkjs.example.com {
    gzip
    root /var/www/thinkjs.example.com/www
    proxy / http://127.0.0.1:8360 {
        transparent
        except static
    }
}
$ sudo systemctl restart caddy

我们可以对比下 ThinkJS Nginx 的配置文件,以下还仅仅只是 HTTP 的配置,没有加上 HTTPS 以及 HTTP/2 的配置。所以 Caddy 的简单性不言而喻。

server {
    listen 80;
    server_name example.com www.example.com;
    root <%= ROOT_PATH %>;
    set $node_port 8360;

    index index.js index.html index.htm;
    if ( -f $request_filename/index.html ){
        rewrite (.*) $1/index.html break;
    }
    if ( !-f $request_filename ){
        rewrite (.*) /index.js;
    }
    location = /index.js {
        proxy_http_version 1.1;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-NginX-Proxy true;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_pass http://127.0.0.1:$node_port$request_uri;
        proxy_redirect off;
    }

    location ~ /static/ {
        etag         on;
        expires      max;
    }
}

Websocket

Caddy 配置 WebSocket 也非常简单,使用 websocket 指令即可。

$ sudo cat << EOF > /etc/caddy/conf/thinkjs.example.com
ws.example.com {
    gzip
    root /var/www/ws.example.com/www
    proxy / http://127.0.0.1:8360 {
        websocket
        transparent
        except static
    }
}
$ sudo systemctl restart caddy

更多的配置指令可以上 https://caddyserver.com/docs 官方文档查看,也可以上 https://github.com/caddyserver/examples 仓库中查看各种程序对应的 Caddy 配置。

总结

最近使用下来之后觉得 Caddy 真是太方便了,不用操心 SSL 证书过期的事情,也不用愁看不懂配置文件了,简单几行就能搞定大部分的功能。对于没有什么特别功能的个人网站来说真是再适合不过了,希望大家也可以试试 Caddy 这款小清新 HTTP Server。

参考资料:

如何使用Drone

在上文《DRONE安装指南》中介绍了如何安装 Drone 服务,下面我将来介绍重头戏——如何在项目中使用 Drone 完成自动化构建任务。首先我们要在 drone 中开启自动构建,实际上它会在对象的 Git 仓库中添加 webhooks 方便当开发者执行对应 git 操作后触发自动构建任务。

drone activation

.drone.yml

在《基于DOCKER的CI工具—DRONE》一文中我们有说过它的每一个构建插件都是镜像,能够模块化的配置我们的构建任务。将构建任务理解成是一条管道,项目流经管道做各种具体的任务。以下是一个简单的管道流程,代码克隆下来后执行安装并执行测试,完成后使用使用 Telegram 通知用户。

+-------+     +-------+     +------+     +----------+
| Clone | ==> | Build | ==> | Test | ==> | Telegram |
+-------+     +-------+     +------+     +----------+

通过编辑放置在项目的根目录下的 .drone.yml 文件,我们可以对构建流进行配置。下面就是上图构建流程的配置:

pipeline:
  build:
    image: node:6.6.0
    commands:
      - npm install --silent
      - npm test
      
  telegram:
    image: appleboy/drone-telegram
    token: 483467761:AAGVOmbOitx0V_1Vx4vlUwy-eoMBVYnSqP0
    to: 337635385
    message: >
      {{#success build.status}}
      主人,{{repo.owner}}/{{repo.name}}第{{build.number}}次构建成功!
      {{else}}
      主人,{{repo.owner}}/{{repo.name}}第{{build.number}}次构建失败了,快来修理下吧。
      {{/success}}

在配置文件中我们总共定义了两个管道任务:

  • build: 我们为 build 任务指定了 node:6.6.0 镜像,并在镜像中执行 npm install --silent 命令安装依赖。最后执行 npm test 来运行单元测试脚本。
  • telegram: 我们为 telegram 任务指定了 appleboy/brone-telegram 镜像,并定义了一些镜像需要的环境参数。包括 telegram 的机器人 token,指定了消息的发送对象 to 以及消息的模板。

将其保存提交后就能正常触发任务并使用 telegram 接收消息了。

build result

telegram

telegram 插件需要设置 bot token,没有的需要先关注 @BotFather 并输入 /netbot 命令创建一个 bot,这样你就能获取到 token 了。

而 to 参数需要配置的是发送用户的 ID,这个比较坑爹了。最开始以为是 username,输入之后一直没有反应,后来才反应过来可能是唯一 ID。不过 telegram ID 的获取比较困难,有人为此特地做了一个机器人。关注 @userinfobot 后和它发个消息它就会告诉你你自己的 ID 啦。这里有个小技巧是把别人的消息转发给它,它会给出对象的 ID。

关于 telegram 插件更多的配置可以参考:http://plugins.drone.io/appleboy/drone-telegram/

Secrets

由于我们将 token 直接写在了 .drone.yml 的配置文件中,项目其它人也都可见,这会带来一些潜在的安全问题。为此我们可以使用 drone 的 secrets 功能。我们小改一下配置文件:

telegram:
	image: appleboy/drone-telegram
	secrets: [ telegram_token, telegram_to ]
	token: ${TELEGRAM_TOKEN}
	to: ${TELEGRAM_TO}

同时我们在网站后台添加两个名为 telegram_token 和 telegram_to 的密钥:

drone secret

保存后重新执行构建任务即可,这样我们就做到了隐藏敏感信息的目的。当然其实这么做也有缺陷,因为在网站后台添加的密钥是对管道中所有的构建插件全局共享的,如果开发者在管道中增加其它插件并 echo 或者其它操作,一样也是能获取到密钥的。为此我们就需要使用 drone-cli 命令来添加密钥,它支持指定密钥的作用域镜像。

$ drone secrets add \
	--repository=lizheming/qalarm \
	--name=telegrame_to \
	--value=337635385 \
	--image=appleboy/telegram

参考资料:

基于Docker的CI工具—Drone

drone

我在很多 Github 项目中都会使用 travis-ci 来做自动化构建任务,其中的一项主要内容就是代码提交后自动执行单元测试计算测试覆盖率。但是 travis-ci 对于其它平台(例如 Gitlab)以及公司内网仓库来说都是不支持的,所以萌生了想要找另外一款 CI 工具代替 travis-ci 的想法。对比了几款 CI 产品之后,我最终选择了 Drone。它是一款使用 Go 开发的开源的 CI 自动构建平台,能够单独部署,支持常见的 Git 仓库,例如 Github, Gitlab, Bitbucket 以及 Gogs 等。Drone 主要有以下几个方面吸引我:

原生 Docker 支持

环境部署算是自动化构建比较复杂的一点了,目前最优解就是使用 docker 容器化技术打包我们的环境。原生 docker 的支持让我们不需要额外的在构建脚本中增加 docker 的命令,简单配置就可以为我们带来集成 docker 所引入的一系列的好处:环境隔离、标准化镜像。并且它的每一个插件都是一个镜像,让我们的自动构建环境模块化。

新版的 Drone 将其拆分成了 Server 端和 Agent 端,Server 用来处理任务分发,Agent 用来执行自动化构建任务。简单增加 Agent 容器实例,我们就可以非常方便的横向扩容 Drone。

PipeLine AS Code

在项目根目录有一个 .drone.yml 的文件,这个是 Drone 构建脚本的配置文件,它随项目一块进行版本管理,开发者不需要额外再去维护一个配置脚本。其实现代 CI 程序都是这么做了,这个主要是相对于 Jekins 来说的。虽然 Jekins 也有插件支持,但毕竟还是需要配置。

另外就是基于 yaml 的配置文件非常简单(当然也要视需求而定),例如一个简单的构建脚本如下:

pipeline:
  build:
    image: node:6.6.0
    commands:
      - npm install --silent
      - npm test

比起 Jekins 纷繁复杂的配置,Drone 几行就搞定了构建的需求。

丰富的插件支持

http://plugins.drone.io/ 这个是 Drone 的插件商店,包含了常见的一些需求插件,例如:

pipeline 中简单的增加插件镜像配置就可以支持插件,例如:

pipeline:
pipeline:
  build:
    image: node:6.6.0
    commands:
      - npm install --silent
      - npm test
  
  publish:
    image: plugins/npm
    secret: [ npm_username, npm_pwd, npm_email ]
    username: ${NPM_USERNAME}
    password: ${NPM_PWD}
    email: ${NPM_EMAIL}
    when:
      event: [ tag ]

  notify:
    image: appleboy/drone-telegram
    secret: [ telegram_token, telegram_to ]
    token: ${TELEGRAM_TOKEN}
    to: ${TELEGRAM_TO}

由于 Drone 是基于 Docker 的,甚至是所有的插件也都是一个 docker 镜像。这代表着我们可以使用任何语言来开发 Drone 插件,最后只要打包成镜像就好了。

其它 CI 工具

Drone 相对于 Jekins 来说优势比较明显,复杂的配置以及需要插件支持的构建脚本 pipeline 化都是我没考虑它的原因。不过 Jekins 其丰富的插件扩展,针对于无项目的自动化构建来说还是非常不错的。Gitlab-CI 也是一款不错的工具,不过从名字上也能看出它的缺点,就是只支持 Gitlab,而且无法扩展配置文件,对于正好在使用 Gitlab 且需求不高的同学可以试试。

总结

基本上 Drone 的流程是我想要的 CI 模式了。不过人无完人,drone 也有一些不足,主要是:

  • 不同版本的文档太多,老文档的一些功能都不支持了。
  • 社区还不够发展,找个问题比较困难。不过好在有个官方论坛,作者回复倒是挺勤快的。
  • 前端界面比较简单,很多功能还是得依靠命令行。

不过这些问题都不影响我对它的看法,相信它会越来越好。

相关资料:

台湾行之台北(二)

本篇会讲述一下两日内听到的分享的内容的感想。浪了这么多天,总算是到了干“正活”的时候了。这两天参加了 Modern Web 2017 大会。第一次参加台湾的技术大会,发现整个大会的氛围还是组织形式都和大陆的有非常大的区别,有非常多值得我们学习的地方。

大会硬件外设及周边

首先别的不说,官网 http://modernweb.tw 做的就非常的用心,居然在首页内藏了一个游戏,不得不说主办方的用心良苦。另外还有就是所有的大会都有在线的协作笔记,可以让参会者快速的录入学习内容并共享给大家。去年的时候有了解另外一个类似的台湾大会,他们也是有一样的东西。我觉得这个非常好,可以像上课一样记笔记,还可以把大家的笔记集合起来。另外还有几个非常有意思的地方:

【译】React 16 beta 版发布!

原文:http://www.zcfy.cc/article/3733

令人欣慰,React 16 首个 beta 版已经发布可以公开测试了!下面我来告诉大家如何使用以及新版的一些值得注意的地方。

如何使用

安装

由于还是 beta 版,为了保障正常 NPM 安装还是使用 15.6 正式版,beta 版我们使用 next 标签发布在 NPM 上,你需要使用如下命令安装使用 beta 版:

# Yarn
yarn add react@next react-dom@next

# NPM
npm install --save react@next react-dom@next

SVG动画实践

SVG 是基于 XML 的矢量图形描述语言,可以近似理解成 HTML,所以能和 JS 以及 CSS 进行交互。 特别是 CSS,配合 CSS Animation 就能实现一些令人心旷神怡的动画,这实在是让人兴奋。下面我们就来看看 SVG 配合 CSS 实现动画的一些方法。

本文内容主要是我之前分享的文字版,若想看重点的话可以看之前的 Slide:

动画方式

动画的本质可以看做是元素从 A 状态过渡变化到 B 状态的过程。实现 SVG 动画主要有以下几种方式:

SMIL

SMIL 全称 Synchronized Multimedia Integration Language,该语言被 SVG 原生支持,主要使用标签来描述动画。据传,由于性能的问题以及 CSS Animation 越来越强大,Chrome 会在未来的版本中废弃对 SMIL 的支持。不过 MDN 上的声明却说 Chrome 推迟了废弃时间,也不知道是什么情况。总而言之,被 Chrome 这么一搞还是不推荐大家入门 SMIL 了。

Although Chrome 45 deprecated SMIL in favor of CSS animations and Web animations, the Chrome developers have since suspended that deprecation.

via: SVG animation with SMIL

如果已经使用了 SMIL 也不必特别担心,目前网上有 SMIL 的 JS Polyfill 方案,有需要的可以看看 https://github.com/ericwilligers/svg-animation 这个项目。

JavaScript

使用 JS 来操作 SVG 动画自不必多说,网上已经有很多现成的类库。例如老牌的 Snap.svg 以及 anime.js,都能让我们快速制作 SVG 动画。当然,除了这些类库,HTML 本身也有原生的 Web Animation 实现。使用 Web Animation 也能让我们方便快捷地制作动画。

CSS3

这里主要是使用 CSS3 动画三剑客(animation, transform, transition)来实现动画。优点不言自明,它比 JS 更加简单方便,关键帧的配合可以让其实现很多复杂的动画。

本文主要就从 CSS3 的角度来描述 SVG 动画的方法。

动画方法

动画可以拆成两部分,起始状态和终止状态是一部分,两态中间的过渡是另一部分。对过渡感兴趣的同学可以去看看月影老师的动画原理与实现的分享。而两态的变换,简单的来说可以分为变换和变形两种,即线性变换和非线性变换。

变换动画

平移变换

平移变换主要是利用 translate 等方法让元素移动位置达到的动画,只要善于使用,简单的移动也能做出不错的动效,例如下面的例子:

JS Bin on jsbin.com

这里有一个小技巧,就是在元素移动的起点和终点分别设置标记元素。使用 getClientRects() 方法获取标记元素的绝对位置,通过计算标记元素的位置差,换算成平移的距离。这样利用纯 CSS 就实现了位置移动的有趣动画。

旋转变换

旋转变换是使用 rotate 方法让元素进行旋转。使用时请注意:旋转圆心默认在 SVG 的左上角。可以通过设置 transform-origin 来修改圆心。本质上是将设置的圆心移动到坐标原点后再旋转再平移回来的一个效果,所以其实以下两种写法是等同的:

/** transform-origin **/
.rotate {
    transform: rotate(45deg);
    transform-origin: 100px 100px;
}

/** translate **/
.rotate {
    transform: translate(-100px, -100px) 
               rotate(45deg) 
               translate(100px, 100px);
}

由于旋转圆心如此重要,所以正如下面的这个例子,我们首要确认的是旋转圆心,剩下的就是简单的旋转角度的控制问题了。

JS Bin on jsbin.com

缩放变换

缩放变换则是使用 scale 方法让元素进行变大变小, 它的本质实际上是元素点 (x, y) 被乘以缩放系数了,即 (x * sacle, y * scale)。所以当设置了 transform: scale(2) 时,它相当于元素所有的坐标点变成 (2x, 2y) 了。

讲的这么详细主要是想让大家注意一个问题,当放大系数为负数的时候,例如 -1 那元素坐标点就会变成 (-x, -y),这就相当于元素针对横纵坐标分别做了一次镜像。对于一些需要实现镜像效果的元素这招就再简单不过了。

JS Bin on jsbin.com

斜歪变换

斜歪变换是使用 skew 方法使元素进行倾斜变形的方法。使用起来也是比较简单的,不过由于使用次数不是很多,本文这里就略过不表了。若有不清楚的同学可以上 MDN 或者 Google 搜索一下。

JS Bin on jsbin.com

矩阵变换

矩阵变换是所有变换方法的底层实现,上述变换方法都是矩阵变换某个方向的特例,完全可以使用矩阵变换实现。矩阵变换的使用方法就不多介绍了,不清楚的可以查下文档。一个 2D 的矩阵变换如下所示:

matrix

关于为何使用矩阵来进行矩阵变换计算,感兴趣的小伙伴可以参考《从矩阵与空间操作的关系理解CSS3的transform》,这篇文章浅显易懂的解释了使用矩阵表示的精妙之处。

JS Bin on jsbin.com

变形动画

变形动画即非线性变换动画,本文主要讲述描边,路径轨迹和路径变形三种变形动画。

描边动画

描边动画算是经典的 SVG 动画了,给人一种绘画的视觉效果,所以描边动画的名字因此得来。它主要是利用 Path 的描边长度 stroke-dasharray 配合描边起始位置偏移量 stroke-dashoffset 来实现的动画。首先设置 stroke-dasharray 等于路径长度,然后将 stroke-dashoffset 从路径长度减少到 0 ,就完成了一次描边。

可以看到,描边动画其实主要在于设置描边的长度,除了在 AI/Sketch 等软件中获取路径长度外,我们还可以使用 JS 的 getTotalLength() 方法获取。除了使用 CSS3 Animation 来实现动画外,我们也可以使用 JS 来绘制动画,网上有很多现成的类库。其中比较推荐大家使用 drawsvg 这款描边库,能简单快速地实现很多复杂的描边特效。

JS Bin on jsbin.com

路径轨迹动画

路径轨迹动画简单来说就是让元素沿着某条路径进行运动的动画。如果用其它方法实现这个效果的话可能会略复杂,但是用 SVG 就非常得心应手了,有多种方法能用来快速实现。使用原生的 SMIL <animateMotion> 标签,或者 CSS3 的 offset-path 属性,亦或是使用 JS 动态计算都是可以的。其中 SMIL 和 CSS3 的实现方法非常类似,这里主要说一下 CSS3 方法。

简单来说,设置 offset-path 属性为某一条 Path 路径,然后通过改变 offset-distance 的距离值,就可以实现元素沿着 offset-path 运动的动画。这种搭配方法让人不由得想到了描边动画。至于 offset-path 属性的前世今生,可以参考张鑫旭老师的《使用CSS offset-path让元素沿着不规则路径运动》。

JS Bin on jsbin.com

路径变形动画

严格意义上,路径变形动画才真正算的上“变形”动画,它表示的是路径之间不规则变化的动画。从 A 路径变形成 B 路径,我们现在可以直接使用 CSS3 Animation 来实现,浏览器会自动帮我们做补间动画。当然我们也可以使用一些 JS 库例如 Snap.SVG 来制作。

不管是使用 CSS3 还是使用 Snap.SVG,都需要注意一个问题,那就是两条变形路径内的描点数需要保证一致,而且需要都是简单的描点,不能存在弧线等高级描点。这么实现的原因很好理解,能够极大减少补间动画的运算量,本质上可以归类为每个描点的平移动画。

但是这种限制对于开发者来说还是颇为麻烦的,所以很多类库就想要解决这个问题。其中比较著名的有 GreenSock,它们编写的 GSAP 插件据说能够对路径进行任意变形,从官网首页的动画也可窥见一二。另外一款插件是 SVG Morpheus,也能实现同样的效果,不过可能因为算法的问题,补间动画似乎并没有 GSAP 那么流畅。另外最近腾讯 AlloyTeam 发布了一款插件 Pasition ,用来实现路径过渡效果,使用起来也很方便,感兴趣的同学可以试一试!

JS Bin on jsbin.com

后记

本文只是涉猎了一些常用的变换方法以及一些基本的变形动画,SVG 还有很多例如遮罩、滤镜、填充动画方法,感兴趣的同学可以去了解下,都能实现非常有趣的动画特效。不过万变不离其宗,只要你富有想象力,能够设计出一些别出心裁的动效,即使只使用上文提及的一些简单的动画方法,我相信最终的动效也一定是非常有意思的!

学习资料