Sketch網頁截屏插件設計開發

一、需求

在Sketch的Artboard中插入網頁截圖:javascript

1.一、輸入網址,自動截圖到Artboard中,並居中顯示;html

1.二、可截取網頁局部圖片前端

 

二、技術選型

技術的選型主要是針對截圖功能的選型,插件技術選用sketch-webview-kitjava

 

截圖技術主要有phantomjs、puppeteer、html2canvas等技術能夠實現截圖功能。node

phantomjs、puppeteer是 headless 瀏覽器技術,puppeteer依賴於node,它們的主要區別以下:
屏幕快照 2019-03-30 下午4.22.27.png                             linux

 html2canvas能夠經過獲取HTML的某個元素,而後生成Canvas,能讓用戶保存爲圖片。ios

 

經過需求分析,puppeteer更適合需求,headless + 部分截圖,且node的環境更符合前端技術。git

 

肯定使用puppeteer構建一個截圖的node服務。github

 

node服務框架採用eggjs。egg.js是阿里推出的基於koa的node開發框架,可爲截圖提供提供穩定的node服務。web

 

三、設計

3.1 架構

構建node基礎的框架egg-common-service,在egg-common-service的基礎上提供Screenshot截圖服務。

Sketch Plugin調取Screenshot截圖服務,將web page的截圖插入到sketch中。
Group 2.jpg                             

 

3.2 流程

用戶在Sketch中發起Screenshot指令;

在Sketch WebView界面中輸入截圖須要的信息,向egg-common-service 發起Screenshot截圖服務請求;

egg-common-service Screenshot服務返回截圖的base64信息給Sketch WebView;

Sketch WebView將圖片base64信息傳遞給Sketch Plugin;

Sketch Plugin將base64圖片繪製在Sketch Artboard中。

screenshot.jpeg                                   

 

3.3 交互設計

交互設計的主要在WebView部分,詳細的設計以下:
1553961288499-42812379-85bc-42c8-a5c3-24e8d95b3d41.png                             

四、開發

4.一、Sketch Plugin

4.1.一、主要功能代碼:

let win = new BrowserWindow({
        width: 408,
        height: 356,
        title:"Web Screen Shot",
        resizable:false,
        minimizable:false,
        maximizable:false,
        closable:true
    });
    win.on('closed', () => {
        win = null
    });
    const Panel = `http://localhost:8000/screenshot.html#${Math.random()}`;
    win.loadURL(Panel);

    const closeWin = () =>{
        win.destroy();
        win.close();
    };

    var contents = win.webContents;

    //監聽webview的事件:webview->plugin
    contents.on('fromwebview', function(data) {
        getImageFrame(data);//在ArtBoard中返回回來的base64圖片
        sketch.UI.message("Successfully screenshot and insert into Artboard!");
        closeWin();
    });

    contents.on('closed', function(s) {
        closeWin();
    });

 

4.1.二、請求處理

使用axios進行數據處理:

 

安裝axios:

$ npm install axios

 

使用:

const axios = require('axios');

axios.get('/user', {
    params: {
      ID: 12345
    }
  })
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  })
  .then(function () {
    // always executed
  });
  
  axios.post('/user', {
    firstName: 'Fred',
    lastName: 'Flintstone'
  })
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });

 

4.二、Sketch WebView

主要功能代碼:

<Spin spinning={spinning} tip="In the screenshot, it takes some time...">
      <div className={styles.body}>
          <div className={styles.url}>
            <span className={styles.itemName}>ArtBoard Name:</span><Input size={size} className={styles.urlInputCss} value={artBoardName} onChange={this.artBoardNameChange} placeholder={artBoardNamePlaceholder} onBlur={this.artBoardNameOnBulr}/>
          </div>
          <div className={styles.url}>
              <span className={styles.itemName}>Page Url:</span><Input size={size} className={styles.urlInputCss} value={url} onChange={this.urlChange} placeholder={urlPlaceholder}/>
          </div>
          <div className={styles.line}></div>
          <div className={styles.part}>
            <span className={styles.itemName}><Checkbox size={size} className={styles.checkbox} defaultChecked={false} checked={isPart} onChange={this.partChange} disabled={checkboxDisabled}></Checkbox>Page Part:</span>
            <span className={styles.partTips}>get part of page</span>
            <div className={styles.partPannel}>
              <RadioGroup onChange={this.onRadioChange} value={this.state.radioType} disabled={radioDisabled}>
                <Radio className={styles.radioStyle} value={1} defaultChecked={true}>
                  <span className={styles.radioName}>Default:</span>
                 <Dropdown.Button overlay={menu} size={size} disabled={dropdownDisabled}>
                   {partTypeDefalt.githubcommits.name}
                </Dropdown.Button></Radio>
                <Radio className={styles.radioStyle} value={2}>
                  <span className={styles.radioName}>Custom:</span>
                  <Input addonBefore="." size={size} className={styles.urlInputCss} value={partId} onChange={this.partIdChange} placeholder={partIdPlaceholder} disabled={partIdDisabled}/></Radio>
                  <div className={partIdDisabled?styles.partIdTips:styles.partIdTipsLight}>the class name of the part</div>
              </RadioGroup>
            </div>
            <div className={styles.line1}></div>
            <div className={styles.buttons}>
              <Button size={size} onClick={this.onCancel} className={styles.button}>{cancel}</Button>
              <Button size={size} onClick={this.insertPage} type="primary" disabled={buttonDisabled}>{button}</Button>
            </div>
          </div>
      </div>
      </Spin>

 

4.三、egg-common-service

4.3.一、參考文檔

教程

API

1)、目錄結構

屏幕快照 2019-03-30 下午10.42.29.png                             

2)、跨配置

使用egg-cors插件,配置以下:

config/plugin.js

exports.cors = {
    enable: true,
    package: 'egg-cors'
};

 

config/plugin.default.js
'use strict';

module.exports = appInfo => {
  const config = exports = {}

  // use for cookie sign key, should change to your own and keep security
  config.keys = appInfo.name + '_1513779989145_1674'

  // add your config here
  // 加載 errorHandler 中間件
  config.middleware = [ 'errorHandler' ]

  // 只對 /api 前綴的 url 路徑生效
  // config.errorHandler = {
  //   match: '/api',
  // }

  config.rpc = {
    // registry: {
    //   address: '127.0.0.1:2181',
    // },
    // client: {},
    // server: {},
  };

  config.security = {
    csrf: {
      enable: false,
    },
    domainWhiteList: [ 'http://localhost:8000' ],
  }

  config.cors = {
    origin: '*',
    allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH'
  };

  config.multipart = {
    fileExtensions: [ '.apk', '.pptx', '.docx', '.csv', '.doc', '.ppt', '.pdf', '.pages', '.wav', '.mov' ], // 增長對 .apk 擴展名的支持
  }

  return config
}

 

3)、egg接收請求參數

-get請求

let query = this.ctx.query;
let name = query.name;
let id = query.id;

 

-post請求
let query = this.ctx.request.body;
let name = query.name;
let id = query.id;

 

-接口返回值
this.ctx.body = {
    code: 0,
    data: '返回的數據',
    msg: '錯誤數據'
}

 

4.3.二、建立egg-common-service

1)、快速生成項目

$ npm i egg-init -g
$ egg-init egg-common-service --type=simple
$ cd egg-common-service
$ npm i

 

2)、啓動項目
$ npm run dev
$ open localhost:700

 

4.3.三、調試

使用VS Code開發和調試。

 

1)、調試配置,在egg-common-service根目錄下添加.vscode文件夾,向.vscode中添加launch.json,launch.json內容以下:

{
  // 使用 IntelliSense 瞭解相關屬性。 
  // 懸停以查看現有屬性的描述。
  // 欲瞭解更多信息,請訪問: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Egg Debug",
      "runtimeExecutable": "npm",
      "runtimeArgs": [
        "run",
        "debug"
      ],
      "console": "integratedTerminal",
      "restart": true,
      "protocol": "auto",
      "port": 9999
    },
    {
      "type": "node",
      "request": "launch",
      "name": "Egg Debug with brk",
      "runtimeExecutable": "npm",
      "runtimeArgs": [
        "run",
        "debug",
        "--",
        "--inspect-brk"
      ],
      "protocol": "inspector",
      "port": 9229
    },
    {
      "type": "node",
      "request": "launch",
      "name": "Egg Test",
      "runtimeExecutable": "npm",
      "runtimeArgs": [
        "run",
        "test-local",
        "--",
        "--inspect-brk"
      ],
      "protocol": "auto",
      "port": 9229
    },
    {
      "type": "node",
      "request": "attach",
      "name": "Egg Attach to remote",
      "localRoot": "${workspaceRoot}",
      "remoteRoot": "/usr/src/app",
      "address": "localhost",
      "protocol": "auto",
      "port": 9999
    }
  ]
}

 

2)、依次點擊,進入調試狀態

Xnip2019-03-30_23-06-26.jpg                             

4.四、 Puppeter

4.4.一、Puppeter能作什麼?

Puppeteer 是一個經過 DevTools Protocol 控制 headless chrome 的 high-level Node 庫,也能夠經過設置使用 非 headless Chrome。

咱們手工能夠在瀏覽器上作的事情 Puppeteer 都能勝任:

1)、生成網頁截圖或者 PDF

2)、爬取大量異步渲染內容的網頁,基本就是人肉爬蟲

3)、模擬鍵盤輸入、表單自動提交、UI 自動化測試

官方提供了一個 playground,能夠快速體驗一下。關於其具體使用不在贅述,官網的 demo 足矣讓徹底不瞭解的同窗入門:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com');
  await page.screenshot({path: 'example.png'});

  await browser.close();
})();

 

4.4.二、安裝

Puppeteer有Puppeteer與Puppeteer-Core二個版本,兩者區別:

1).Puppeteer-Core在安裝時不會自動下載 Chromium

2).Puppeteer-Core忽略全部的PUPPETEER_* env 變量.

使用npm安裝:

npm i puppeteer or puppeteer-core

 

4.4.三、使用

http://www.javashuo.com/article/p-wsxolrzt-eb.html

http://www.mamicode.com/info-detail-2302923.html

https://blog.csdn.net/asas1314/article/details/81633423

https://www.jianshu.com/p/8e65fdcb6d85

 

4.4.四、linux下puppeteer使用要點

1)、pupper下載了一個Chromium,但並無把依賴都裝好。因而要本身把so都裝好。

官方給的是Ubuntu版本的各個so包的apt-get安裝方式,centos版本竟然沒有放!可是仍是有人給出了centos的庫名:

#依賴庫
yum install pango.x86_64 libXcomposite.x86_64 libXcursor.x86_64 libXdamage.x86_64 libXext.x86_64 libXi.x86_64 libXtst.x86_64 cups-libs.x86_64 libXScrnSaver.x86_64 libXrandr.x86_64 GConf2.x86_64 alsa-lib.x86_64 atk.x86_64 gtk3.x86_64 -y

#字體
yum install ipa-gothic-fonts xorg-x11-fonts-100dpi xorg-x11-fonts-75dpi xorg-x11-utils xorg-x11-fonts-cyrillic xorg-x11-fonts-Type1 xorg-x11-fonts-misc -y

 

2)、sandbox去沙箱

修改啓動瀏覽器的代碼,加上args:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox']});
  const page = await browser.newPage();
  await page.goto('https://example.com');
  await page.screenshot({path: 'example.png'});

  await browser.close();
})();

 

3)、Macaca-puppeteer

阿里的Macaca也順勢寫了Macaca-puppeteer,能夠在Macaca上直接寫通用的測試用例,在開發機上用圖形界面看效果,上服務器走生產。

 

Macaca順便還提供了一個基於Ubuntu的Macaca-puppeteer的Docker。

 

4)、使用await page.waitFor('div.Card');來等待頁面的指定元素加載完成

 

4.五、Screenshot 功能代碼

4.5.一、router.js

'use strict';

module.exports = app => {
  const { router, controller } = app;

  router.get('/', controller.home.index);
  router.post('/service/screenshot', controller.screenshot.screenshot);
};

 

4.5.二、新建controller screenshot.js

'use strict';

const Controller = require('egg').Controller;

class ScreentshotController extends Controller {
  constructor(ctx) {
      super(ctx)
      this.dataValidate = {
        appkey: { type: 'string', required: true, allowEmpty: false },
        url: { type: 'string', required: true, allowEmpty: false },
        isPart: { type: 'boolean', required: true, allowEmpty: false }
      }
  }
  async screenshot() {
      const { ctx, service } = this
      // 校驗參數
      ctx.validate(this.dataValidate)
      // 組裝參數
      const payload = ctx.request.body || {}

      // 調用 Service 進行業務處理
      const res = await service.screenshot.screenshot(payload)
      // ctx.body = res;
       // 設置響應內容和響應狀態碼
    ctx.helper.success({ctx, res})
  }
}

module.exports = ScreentshotController;

 

4.5.三、新建service screenshot.js

'use strict'

const Service = require('egg').Service
const puppeteer = require('puppeteer')
const fs = require('fs');
const path = require('path');
const images = require("images");
const mineType = require('mime-types');
const APPKEY = "jingwhale";
const partTypeDefalt = {
    githubcommits:".commits-listing"
};
var part = "";

class ScreenshotService extends Service {
    async base64img(file){//生成base64
        let filePath = path.resolve(file);
        let data = fs.readFileSync( path.resolve(filePath));
        let imageData = images(filePath);
        var backData = {
            base64: data,
            width: imageData.width(),
            height: imageData.height()
        }
        backData.base64 = new Buffer(data).toString('base64');
        
        return backData;
    }

    async screenshot(payload) {
        const { ctx, service } = this
        if(payload.appkey!=APPKEY){
            ctx.throw(404, 'appkey不正確!');
        }
        const browser = await puppeteer.launch();
        const page = await browser.newPage();
        var path = 'screenshot.png'
        var backData = {};
        var id = payload.id
        
        await page.goto(payload.url);
        part = page;
        var partId = payload.partId;
        if(payload.isPart){
            if(payload.partType===1){//自定義
                console.log(payload.partType)
            }else{//默認
                partId = partTypeDefalt[payload.partType];
            }
            var partArr = await page.$$(partId);
            part = partArr[0];
        }
        
        // //調用頁面內Dom對象的screenshot 方法進行截圖
        try { // 截圖 
            await part.screenshot({path: path, type: 'png'}).catch(err => {
                console.log('截圖失敗'); 
                console.log(err); 
            });
        }catch (e) { 
            console.log('執行異常'); 
            ctx.throw(404, '執行異常')
        } finally { 
            await page.close();
            await browser.close(); 
        }
        var base64imgData = this.base64img(path)
        
        return base64imgData
    }
}

module.exports = ScreenshotService

 

 

六、總結

技術給我更多的受益是解決問題的方式與思路。

不少重複單一的任務,均可以使用技術解決。

提升效率,留出更多的時間去設計。

Work Smart,Think more,Do Less,Get More.

相關文章
相關標籤/搜索