2022年5月28日 星期六

Vue3+jest 測試 composable 範例


Vue3 的 composable 乍看下和 mixin 用途很類似,可以提供各個元件共用程式碼。但與 mixin 相比,composable 主要有三個優勢:

第一,元件可以很明確的區分使用的 composable 來源,當使用的 mixin 一多時,追朔來源相對困難,無法一眼看出由哪個 mixin 實作。

第二,多個 mixin 無法確保使用了相同的名稱,可能造成覆蓋,但 composable 即使有相同的名稱,也能透過結構式賦值、重新命名。

最後,多個 mixin 需要交互作用時,通常會使用相同的參數命名來達到這個目的,隱性的耦合使得辨識和debug難度增加,composable可藉由其一的回傳值,作為其他composable輸入的參數達到共享的目的。(參考資料)


安裝 Jest 測試 vue 時,首先要注意版本,請參考下面的對照表安裝套件:

Vue version Jest Version npm Package

Vue 2 Jest 26 and below vue-jest@4

Vue 3 Jest 26 and below vue-jest@5

Vue 2 Jest 27 and above @vue/vue2-jest@27

Vue 3 Jest 27 and above @vue/vue3-jest@27

Vue 2 Jest 28 and above @vue/vue2-jest@28

Vue 3 Jest 28 and above @vue/vue3-jest@28


composable 的測試主要分成兩種,第一種元件比較無關,可以當作普通的 js code 測試。

// counter.js
import {ref} from 'vue';

export function useCounter() {
  const count = ref(0);
  const increment = () => count.value++;

  return {
    count,
    increment,
  };
};
// counter.test.js
import {useCounter} from './counter.js';

test('useCounter', () => {
  const {count, increment} = useCounter()
  expect(count.value).toBe(0);

  increment();
  expect(count.value).toBe(1);
});

如果 composable 牽涉到元件 lifecyle hooks 或是 provide/inject 時,需要依附一個元件進行測試。

// test-utils.js
import {createApp} from 'vue';

export function withSetup(composable) {
  let result;

  const app = createApp({
    setup() {
      result = composable();
      // suppress missing template warning
      return () => {};
    },
  });

  app.mount(document.createElement('div'));
  // return the result and the app instance
  // for testing provide / unmount
  return [result, app];
};
// counter.js
import {ref, onMounted} from 'vue';

export function useCounter() {
  const count = ref(0);
  const increment = () => count.value++;
  
  onMounted(() => {
    increment();
  });

  return {
    count,
    increment,
  };
};

result是 composable 的回傳值(return),測試裡面 app.mount();,將執行 onMounted

// counter.test.js
import {withSetup} from './test-utils';
import {useCounter} from './counter.js'

test('useCounter', () => {
  const [result, app] = withSetup(() => useCounter());

  // run assertions
  expect(result.count.value).toBe(0);

  // trigger onMounted hook if needed
  app.mount();

  expect(result.count.value).toBe(1);
});


另外還有一個常犯的錯誤,先看 composable 程式碼,有一個 computed 使用 formatFn 轉換 list:

// useListFormatter.js
import {computed, unref} from 'vue';

export function useListFormatter(list, formatFn) {
  const formattedList = computed(() => (
    unref(list).map(item => formatFn(item))
  ));

  return {
    formattedList,
  };
};

測試時需要注意回傳值被使用之前,formattedList 不會被執行。

// useListFormatter.test.js
import {useListFormatter} from './useListFormatter.js';

test('useListFormatter', () => {
  const list = ['a', 'b'];
  const formatFn = jest.fn();

  formatFn.mockImplementation(value => `new-${value}`)

  const {formattedList} = useListFormatter();
  
  // Don't do it! formatFn won't be excuted until formattedList is called.
  // expect(formatFn).toHaveBeenCalled();
  
  expect(formatFn).not.toHaveBeenCalled();

  expect(formattedList.value).toEqual(['new-a', 'new-b']);
  expect(formatFn).toHaveBeenCalledTimes(2);
  expect(formatFn).toHaveBeenNthCalledWith(1, 'a');
  expect(formatFn).toHaveBeenNthCalledWith(2, 'b');
});


參考資料:

https://vuejs.org/guide/scaling-up/testing.html#testing-composables


2022年3月5日 星期六

測試套件 Jest mock 基礎概念及 spyOn, fn 範例程式

當測試的對象有使用其他引用(import)時,我們希望能做為控制變因,設法聚焦測試的對象。Jest是常用的js單元測試套件,我們會 mock 方法spyOnfn來達成這個目的,Jest 安裝可以參考「[JavaScript] 安裝Jest單元測試」,底下說明幾種常用的情境。

jest.spyOn(object, methodName)

1. [基本用法] 監聽
單純測試某個method被呼叫的次數,或(官方說明),舉例下面範例是輸入兩個數字,並隨機輸出這個範圍內的數字。
// getRandonInt.js

export const getRandonInt = (min, max) => (
    Math.floor(Math.random(max) * (max - min) + min)
);
例如 Math.floor
jest.spyOn(Math, 'floor')
同理 Math.random ,所以可以將測試寫成:
import {getRandonInt} from './getRandonInt.js';

test('Should get random number between input min and max', () => {
    const spyFloor = jest.spyOn(Math, 'floor');
    const spyRandom = jest.spyOn(Math, 'random');

    const result = getRandonInt(1, 100);

    expect(result).toBeGreaterThanOrEqual(1);
    expect(result).toBeLessThanOrEqual(100);

    expect(spyFloor).toHaveBeenCalledTimes(1);

    expect(spyRandom).toHaveBeenCalledTimes(1);
    expect(spyRandom).toHaveBeenCalledWith(100);
});

因此可看到我們監聽的兩個method,第一 toHaveBeenCalledTimes 分別被呼叫的次數,及 toHaveBeenCalledWith 傳入的參數。


2. [進階] 替換回傳值
改變某個method的回傳值,以下面的範例是希望輸入一個值後,可以隨機產生一個數字並得到兩者相乘的結果
// multiplyNumber.js

export const multiplyNumber = num => (
    Math.random(10) * num
);
如果希望控制隨機產生的數字,可以使用 mockImplementation
import {multiplyNumber} from './multiplyNumber.js';

test('Should get the multiple of random number and input num', () => {
    const spyRandom = jest.spyOn(Math, 'random')
        .mockImplementation(() => 5);

    expect(Math.random(10)).toBe(5);
    expect(multiplyNumber(1000)).toBe(5000);

    spyRandom.mockRestore();

    expect(Math.random(10)).not.toBe(5);
});
可以理解成實際我們已經將 Math.random 變成:
Math['random'] = () => 5;

// you could definitely get parameter when method was called
// so implementation would be like ...
// Math['random'] = a => (a + 1);

mockRestore()可以恢復原本method的功能,因此直到我們呼叫 mockRestore()為止,無論我們呼叫幾次 Math.random ,都會按照我們的設定回傳數字 5。


假設希望這個設定是一次性的,則可以改用 mockImplementationOnce,這個寫法比較不容易出錯,是比較推薦的用法。

jest.spyOn(Math, 'random')
    .mockImplementationOnce(() => 5);
    .mockImplementationOnce(() => 20);

expect(Math.random(10)).toBe(5);
expect(Math.random(10)).toBe(20);


jest.fn(implementation)

用法和 spyOn非常類似,可以用來驗證呼叫次數及傳入參數等

const mockFn = jest.fn();

mockFn(100);

expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(1);
expect(mockFn).toHaveBeenCalledWith(100);
驗證的部分,toHaveBeenCalledTimestoHaveBeenCalledWith這兩個還有另一個寫法:
expect(mockFn).toHaveBeenCalledTimes(1);
expect(mockFn.mock.calls.length).toBe(1);

expect(mockFn).toHaveBeenCalledWith(100);
expect(mockFn.mock.calls[0][0]).toBe(100);

calls是一個陣列,長度等同於呼叫的次數,陣列內每一個位置也都是一個陣列,每個位置則對應到呼叫時的參數內容。可以實際印出得到 calls = [[100]],所以calls[0][0]代表的是第一次呼叫時第一個參數是什麼


依照 multiplyNumber.js 的範例,可以使用fn達到同樣的效果,測試結束前也要記得呼叫mockRestore免得影響其他測試‧。

const spyRandom = jest.fn(() => 5);
Math['random'] = spyRandom;
const spyRandom = jest.fn()
    .mockImplementation(() => 5);
Math['random'] = spyRandom;
const spyRandom = jest.fn()
    .mockReturnValueOnce(5);
Math['random'] = spyRandom;


jest.mock(module)

當測試有引入(import)其他模組、函式時,可以使用mock來替換回傳內容,我們稍微改寫剛剛的 multiplyNumber

import {getRandonInt} from './combineArray.js';

export const multiplyNumber = (min, max, num) => (
    getRandonInt(min, max) * num
);

為了清楚此時的getRandonInt已經被替換掉,我們刻意重新命名,在測試裡將回傳值設定為數字 20

import {getRandonInt as mockGetRandonInt} from './getRandonInt';
import {multiplyNumber} from './multiplyNumber';

jest.mock('./getRandonInt');

test('Should get the multiple of random number and input num', () => {
    mockGetRandonInt.mockReturnValueOnce(20);

    expect(multiplyNumber(1, 2, 3)).toBe(60);

    expect(mockGetRandonInt).toHaveBeenCalledTimes(1);
    expect(mockGetRandonInt).toHaveBeenCalledWith(1, 2);
});

另外可以利用requireActual可以取得原始的功能,當我們只需要部份取代時,可以藉此替換部分的內容,其餘皆按照原設定

const {getRandonInt} = jest.requireActual('../src/libs/combineArray');

mockGetRandonInt.mockImplementationOnce((a, b) => a + b);

expect(getRandonInt(1, 2)).toBeLessThanOrEqual(2);

expect(mockGetRandonInt(1, 2)).toBe(3);
expect(mockGetRandonInt).toHaveBeenCalledTimes(1);

最後一個重點,建立和初始化一個class的物件時,可以透過instances來得到建立的物件,它是一個陣列,會依照建立順序放在對應的位置上。

const myMock = jest.fn();

const a = new myMock();

expect(a).toBe(myMock.mock.instances[0]);

參考資料:



2022年2月22日 星期二

Cypress+Vue 元件測試 (component testing)

接續「e2e 測試工具 Cypress 安裝及介紹」,除了用在實際開啟瀏覽器的測試,cypress也支援元件測試,需要安裝相關的套件將元件render出來。


安裝
npm install --save-dev cypress @cypress/vue @cypress/webpack-dev-server webpack-dev-server

如果是 Vue3 ,請記得把 @cypress/vue 換成:

npm install --save-dev @cypress/vue@next


設定

先到 cypress/plugins/index.js 的註冊 dev-server:start 事件,再來要注意 require('@vue/cli-service/webpack.config.js')這段,你的專案不見得有安裝這個套件,你可以建立自己的webpack設定,並替換上正確的路徑。

// cypress/plugins/index.js

const { startDevServer } = require('@cypress/webpack-dev-server');
// Vue's Webpack configuration
const webpackConfig = require('@vue/cli-service/webpack.config.js');

module.exports = (on, config) => {
  on('dev-server:start', options =>
    startDevServer({ options, webpackConfig });
  );
};

若我們要測試的元件內容為:

<!-- Button.vue -->
<template>
  <button>
    <slot />
  </button>
</template>

在測試時,vue2和vue3引入 mount 的寫法相同,並且將設定元件所需的輸入

import {mount} from '@cypress/vue'
import Button from './Button.vue';

it('Button', () => {
  mount(Button, {
    slots: {
      default: 'Test button',
    },
  });

  cy.get('button').contains('Test button').click();
});


執行

提供 GUI 介面操作

npx cypress open-ct

無GUI,在 Terminal 上執行

npx cypress run-ct


參考文件: https://docs.cypress.io/guides/component-testing/introduction


2022年1月31日 星期一

e2e 測試工具 Cypress 安裝及介紹


e2e測試(End-to-End testing)是模擬使用者的行為,實際打開瀏覽器操作頁面的測試方式,因此不限於vue, react的專案,e2e的測試可以反映出頁面在不同瀏覽器的執行情形。

Cypress便是其中一個常見工具,Cypress在安裝上簡單,語法採用 mocha 的句法、chai 的斷詞(expect, should, assert),撰寫的方式跟 unit test 相似。


安裝

進到想要測試的專案目錄內,使用 npm 安裝。(官網說明)

npm install cypress --save-dev

接著在目錄裡,建立先建立一個 cypress.json,內容放一個空物件,此時你可以直接執行下面的命令(官網說明):

npx cypress open

會發現多了一個 cypress/ 的資料夾,裡面有四個目錄:

  • integration: 主要的測試檔案
  • fixtures: 固定的資料
  • plugins: 主要有 on 定義全局的不同時間點的執行工作,以及 config 提供全局使用的參數,如常用的 env
  • support: 可提供自定義的指令、或引入擴充的指令,供撰寫測試檔案使用


Cypress.json 與環境變數(.env)

使用 cypress 都必須在專案的根目錄裡新增 cypress.json,如果直接放 {} 空物件,代表直接使用預設值。

下面是我的設定,從字面上不難理解設定的內容,第一個testFiles是測試的檔案名稱,接著各個目錄的路徑,video是 Boolean 決定是否錄製測試過程的影片,screenshotOnRunFailure也是 Boolean 是否在執行失敗時擷取畫面。

{
    "testFiles": "**/*.spec.js",
    "integrationFolder": "resources/cypress/integration",
    "componentFolder": "resources/cypress/components",
    "fixturesFolder": "resources/cypress/fixtures",
    "pluginsFile": "resources/cypress/plugins",
    "supportFile": "resources/cypress/support",
    "video": false,
    "screenshotOnRunFailure": false
}

除了cypress基本的設定,常常也需要讀取環境設定檔(.env),假設內容如下(官方說明):

APP_URL=https://chenuin.github.io/
USER_NAME=chenuin

plugins/index.js可以設定:

// plugins/index.js

require('dotenv').config();

module.exports = (on, config) => {
    config.baseUrl = process.env.APP_URL;

    config.env.USER_NAME = process.env.USER_NAME;

    return config;
}
測試檔案需要使用時,可以直接呼叫:
Cypress.config('baseUrl')
Cypress.env('USER_NAME')

打開GUI,點選上方的 Settings 可以看到設定的結果,上面的顏色可以區分目前的設定是讀取哪裡,像是 baseUrl 是紫色的 plugin,

因此,可以知道下面兩種寫法都可以達到相同的效果:

// plugins/index.js
config.baseUrl = process.env.APP_URL;


// cypress.json
{
    "baseUrl": "https://chenuin.github.io/"
}


執行

提供 GUI 介面操作

npx cypress open

無GUI,在 Terminal 上執行

npx cypress run


訪問頁面 visit

測試的第一步常常是先進入某一個頁面,通常專案內都在相同的Domain下,如果有設定 baseUrl,Cypress會直接將設定作為前綴,導到相應的頁面,在寫法上簡潔許多。

// https://chenuin.github.io/webpack/2021/08/08/webpack5-getting-started.html

cy.visit('/webpack/2021/08/08/webpack5-getting-started.html')

若未設定,需要傳入完整的路徑,或者cypress會嘗試在你的web server上找到對應位置的頁面。(官方說明)


intercept與fixtures的使用

使用 intercept 來監聽由頁面上請求(request)及回應(response),下面三者是相同的意思:

cy.intercept('/users/**')
cy.intercept('GET', '/users/**')
cy.intercept({
  method: 'GET',
  url: '/users/**',
})

你也可以為這個路由(route)新增別名(alias),除了方便辨識外,如果需要在request後執行的工作,可以使用下面的寫法:

cy.intercept('POST', '/users').as('createUser');

// once a request to create user responds, 'cy.wait' will resolve
cy.wait('@createUser')

GUI上可以看到這個 route 表格

執行此請求時會留下Log記錄,所有被監聽的路由會列在route表格,表格上可以看到所有route被呼叫的次數(表格最右側的數字),對應的別名(alias)是什麼。

上面 Stubbed 是代表回應(response)是否被取代了,這個是很好用的功能,例如:當我們將 users 這個請求的回應,取代為一個列表:

// requests to '/users' will be fulfilled
// with a body of list: ['John', 'Ben', 'Mary]

cy.intercept(
  '/users',
  ['John', 'Ben', 'Mary],
)

或者可以使用 fixture ,後面填寫 fixture/ 目錄裡的檔案名稱。

// requests to '/users' will be fulfilled
// with the contents of the "users.json" fixture

cy.intercept(
  '/users',
  { fixture: 'users.json' },
)
(官方說明)


ESLint設定

請在 .eslintrc.js 加上設定,不需要安裝任何套件:

module.exports = {
  extends: [
    'plugin:cypress/recommended',
  ],
};



2022年1月25日 星期二

js 單元測試套件 Macha+Chai (es6 語法 Babel 設定)

安裝

首先安裝 mocha (官網)和 chai (官網)

npm install mocha chai --save-dev

chai 是一種斷言庫(Assertion Library),mocha 不像是jest已經內建,可以自行決定要使用哪一種套件,chai是其中最常見的一種。

package.json加入測試指令

"scripts": {
  "test": "npx mocha"
}


設定 Babel

安裝 Babel,再來的說明都會直接使用es6的語法,如果不需要,可以略過這部分。

npm install @babel/core @babel/preset-env @babel/register --save-dev

接著在根目錄新增 babel.config.js

// babel.config.js

module.exports = {
    presets: [
        '@babel/preset-env',
    ],
};

接著在根目錄新增 .mocharc.jsspec是設定 mocha 執行測試的檔案,預設為 test/ ,這裡設定成自動讀取 src/test/ 底下所有以 spec.js 為結尾的檔案。

require可以接陣列設定所需的 module

// .mocharc.js

module.exports = {
    spec: ['src/test/*.spec.js'],
    require: ['@babel/register'],
};

.mocharc.js 是 mocha 執行時會自動讀取的設定檔,也支援json 或 yaml(說明),可以選擇喜歡的格式新增,設定檔的內容可以參考:https://github.com/mochajs/mocha/tree/master/example/config

設定參考資料:
https://github.com/mochajs/mocha-examples/tree/master/packages/babel



建立測試

新增一個簡單的測試

// src/test/array.spec.js

import chai from 'chai';

describe('Array', () => {
  describe('#indexOf()', () => {
    it('should return -1 when the value is not present', function() {
      chai.assert.equal([1, 2, 3].indexOf(4), -1);
    });
  });
});

馬上執行試試看

npm run test

應該會看到下面的資訊,

> vue3-webpack5-template@1.0.0 test
> npx mocha


  Array
      #indexOf()
          should return -1 when the value is not present

1 passing (8ms)

完成第一個測試囉!



支援 async/await

請安裝套件

npm install @babel/plugin-transform-runtime --save-dev

把套件加到 babel.config.js的 plugin 設定

module.exports = {
    require: ['@babel/register'],
    plugins: ['@babel/plugin-transform-runtime'],
};


Root Hooks

mocha 提供的 hook 包含 before(), after(), beforeEach(), and afterEach(),從語意應該不難看出用途,前兩個只會執行一次,後兩個則是每項測試都會執行。

若要在所有的測試上加上 Hook,則是有這四種 beforeAll(), afterAll(), beforeEach(), and afterEach(),執行時機不變,請根據情境加上執行內容:

// src/test/hooks.js

export const mochaHooks = {
  beforeEach: () => {
    // do something before every test

  }
};

接著在 .mocharc.js 加上設定

module.exports = {
    spec: ['src/test/*.spec.js'],
    require: [
      '@babel/register',
      'src/test/hooks.js',
    ],
};

https://mochajs.org/#hooks
https://mochajs.org/#defining-a-root-hook-plugin



測試結果

mocha 是個可愛的名稱,測試結果也可以換成很多有趣的模式:

npx mocha --report landing

https://mochajs.org/#reporters



支援瀏覽器介面

加上指定的路徑執行下面的指令

npx mocha init [PATH]
  • index.html
  • mocha.css
  • mocha.js
  • test.spec.js

目錄底下會多了上述這些檔案,此時只要將 test.spec.js 加入你的測試,再重新整理 index.html,就能看到測試結果已經輸出在畫面上。


可以將 chai 加入 index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Mocha</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="mocha.css" />
    <script src="https://unpkg.com/chai/chai.js"></script>
  </head>
  <body>
    <div id="mocha"></div>
    <script src="mocha.js"></script>
    <script>
      mocha.setup('bdd');
    </script>
    <script src="tests.spec.js"></script>
    <script>
      mocha.run();
    </script>
  </body>
</html>

再來可以直接將前面新增的 src/test/array.spec.js 內容貼過去,index.html 已經載入 mocha 和 chai ,記得把第一行的 import 拿掉。



2022年1月23日 星期日

webpack 常用 plugin 介紹 - HtmlWebpackPlugin 自動產生 Html


根據「[Vue.js] 如何建立 Vue3 + webpack5 專案範例」的內容,封裝時已經先建立目錄 dist/ ,新增 index.html,預先將 main.js 加到 script 中。

接著,以下要介紹 HtmlWebpackPlugin ,這個 webpack plugin 可以自動產生 Html,並自動將所有的 js 檔加入 script 中。下面的操作會用 [vue3-webpack5-template] 這個專案操作,可以先 clone下來並 npm install。

一、安裝
npm install html-webpack-plugin --save-dev


二、新增模板

新增 index.html ,並保留一些區域方便我們可以自訂。

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8"/>
        <title>
            <%= htmlWebpackPlugin.options.title %>
        </title>
    </head>
    <body>
        <div id="<%= id %>"></div>
    </body>
</html>


三、新增 Webpack 設定

為了突顯 HtmlWebpackPlugin 功能的強大,首先刻意將輸出的 js 檔案名稱改為 [name].[contenthash].js,這時變得無法預期編譯後的檔案名稱。

再來將 HtmlWebpackPlugin 加入 plugin,完整檔案請點連結

// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  output: {
    filename: '[name].[contenthash].js',
    path: path.resolve(__dirname, 'dist'),
  },

  ...

  plugins: [
    new HtmlWebpackPlugin({
      template: 'template/index.html',
      title: 'Getting Started',
      templateParameters: {
        id: 'app',
      },
    }),
  ],
};

上面共使用了三個參數:

1. template - 顧名思義是模板的路徑
2. title - html 檔案的 <title>
也說明為什麼在 index.html 有這一段

<%= htmlWebpackPlugin.options.title %>

3. templateParameters - 這裡傳入一個物件,將 id 訂為 app,因此模板這段程式碼,將會使用這裡的設定複寫

<div id="<%= id %>"></div>

若要了解更多參數的使用,可以參考文件



四、執行

執行下面的指令開始打包

npm run build

程式碼已經 push 到分支test-webpack-plugin,或者參考變更可以按這裡


2021年12月15日 星期三

取代 window resize 事件? 如何使用 ResizeObserver 偵測元素變化

ResizeObserver 用來監控元素大小的變動,與 window 的 resize 事件(如下)的用途相似,通常用在監控瀏覽器視窗大小的變動。ResizeObserver 使用範圍更廣,可以綁在 HTMLElement 上。

window.addEventListener('resize', callback);

首先要建立一個 ResizeObserver 物件

const resizeObserver = new ResizeObserver(callback);

callback 是定義當指定的元素偵測到變化時,需要執行的動作,分為:

  • entries:型態為陣列(array)。因為允許偵測多個變動元素,每個元素變動的大小、位置等資訊,會以 ResizeObserverEntry 物件裝在陣列內。
  • observer:ResizeObserver 物件本身
const resizeObserver = new ResizeObserver((entries, observer) => {
    entries.forEach(entry => {
        // Do something to each entry
        // and possibly something to the observer itself
    });
});

再來,使用 oberve()開始監看元素

resizeObserver.observe(target, options);

target為需要偵測的元素


unobserve() 取消追蹤指定的元素

resizeObserver.unobserve(target);

可以使用disconnect()取消所有元素的追蹤

resizeObserver.disconnect();

接著,請參考 window resize 事件的範例,範例將視窗大小顯示,若resize發生時,將異動更新到畫面上,如果使用 ResizeObserver 要怎麼改寫:

<p>Resize the browser window to fire the <code>resize</code> event.</p>
<p>Body height: <span id="height"></span></p>
<p>Body width: <span id="width"></span></p>
const heightOutput = document.querySelector('#height');
const widthOutput = document.querySelector('#width');
  
const resizeObserver = new ResizeObserver(entries => {
  entries.forEach(entry => {
    heightOutput.textContent = entry.contentRect.height;
    widthOutput.textContent = entry.contentRect.width;
  });
});

resizeObserver.observe(document.querySelector('body'));

Resize the browser window to fire the resize event.

Body height:

Body width:


底下實作一個範例,用slider控制 div 區域的寬度,當增測的 div 變化時,依據寬度的數值改變 div 背景的透明度。

See the Pen ResizeObserver Example by chenuin (@chenuin) on CodePen.


參考資料: https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver

2021年10月1日 星期五

如何將 Github 專案部屬到 Heroku (Creating a 'Deploy to Heroku' Button)

先前「[django] 將Django專案部署到Heroku」提到如何一步一步建立 Django 專案,並部屬到 Heroku。若是Github現有的專案已經滿足 Heroku 基本所需的設定,可以透過網頁操作,將現有專案快速地複製到 Heroku 上。

操作說明使用 heroku-startup-settings 這個專案 ,可以到 Github 查看完整的程式碼。首先,Github專案必須在根目錄先建立 app.json 這個檔案:

{
  "name": "Heroku startup settings",
  "description": "Getting started with Django project",
  "repository": "https://github.com/chenuin/heroku-startup-settings",
  "logo": "https://node-js-sample.herokuapp.com/node.png",
  "keywords": ["heroku", "python", "django"]
}
(app.json)

內容很好理解,檔案沒有必填的欄位,通常會寫namedescriptionlogo有助於其他人理解這個專案的內容或目的,更多的設定可以參考 app.json schema

其中設定環境參數 env 應該是最常使用的,可以依照需求調整部屬。


app.json 正確建立後,就可以開啟這個連結。
https://heroku.com/deploy?[REPO_WEB_URL]/tree/[BRANCH_NAME]
請替換專案的路徑和分支名稱,因此連結為:

接者可以在 README.md 新增這段文字,
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/chenuin/heroku-startup-settings/tree/master)

請記得替換成自己的連結,按鈕效果就像這樣:

Deploy

點選按鈕開啟連結並填寫專案名稱,按下「Deploy app」,就會自動在 Heroku 建立一個一樣的新專案。


相關文章:

[django] 將Django專案部署到Heroku

參考資料:

https://devcenter.heroku.com/articles/heroku-button https://devcenter.heroku.com/articles/app-json-schema

2021年8月17日 星期二

Github Actions 自動 Jekyll 專案部屬 Github Pages

自從完成「Travis CI 自動 Jekyll 專案部屬 Github Pages 完整範例」很久沒有更新 Github Pages,近期上傳時卻發現CI執行失敗,並不是腳本有問題或編譯過程有錯誤,而是request被拒,所以即使換了Github Token依舊無法解決。

直到我發現 Travis CI 帳戶設定突然多了「Plan」,有免費的方案可以選擇,選擇之後再讓Job重跑居然就成功了,所謂的免費方案是給 10000 available credits,每次執行Job時就會扣點直到用完為止,之後可以一次性購買或者選擇月費方案。


Github Actions 是 Github 內建的功能,可以定義Repo自動化、執行CI/CD相關工作,因此決定將 Travis CI 的功能由 Github Actions 取代,以下將說明如何移除 myblog 原本 .travis.yml 的設定 ,以 Github Actions 自動部屬至 Github Pages。


一、選擇 Workflow 建立方式

方法一:

在專案根目錄

新增目錄 .github/workflow/,並在 workflow/ 新增副檔名為 .yml的檔案。

方法二:

到 Github repo上設定

可更改檔名和底下的內容


二、新增 Workflow

name 命名,可自行決定名稱

name: website

on 必填的設定,Github Action提供各種事件供選擇觸發時機,這邊設定為 push 時執行,也可以針對多個事件指定分支。

# Triggered when code is pushed to any branch in a repository
on: push


# Or ....
on:
  # Trigger the workflow on push,
  # but only for the main branch
  push:
    branches:
      - main

jobs 接下來是最重要的部分,workflow是由一個或多的Job組成,預設情況下會同時執行,每個 Job 必須給一個唯一的 job_id,是由數字、字母或 _- 組成的字串,且字首只能由字母或是 _。因此我們第一個發布 - publish 的動作:

jobs:
  publish:

再來是 publish 內需要哪些設定:

runs-on 必填的設定,使用哪種機器執行 workflow,像是 Ubuntu、macOS、windows server等 Github 提供的虛擬環境。

runs-on: ubuntu-latest

steps 定義有哪些任務,Job 由一連串的任務組成,可以替任務命名,會在 Github 上顯示。


uses 指定動作(Action),通常是一些重複性的執行動作,被打包成一個套件,可以在這裡直接使用,更多相關的套件可以到 Github marketplace 搜尋 。底下使用的是checkout一個新的分支並安裝Ruby。

steps:
  # Reference the major version of a release
  - uses: actions/checkout@v2   # Downloads a prebuilt ruby
  - uses: ruby/setup-ruby@v1

run 在虛擬機器 shell 上執行 command-line

steps:
  - run: bundle install
  - run: chmod +x ./script/cibuild
  - run: ./script/cibuild

with 執行動作時可以加入參數,利用 github-pages 可以快速設定,我們指定將網站部署到 chenuin/chenuin.github.io 這個 repo 的 master 分支,需要上傳的目錄為 ./_site ,也就是Jekyll編譯好的檔案目錄。

steps:
  - uses: crazy-max/ghaction-github-pages@v2
    with:
      repo: chenuin/chenuin.github.io
      target_branch: master
      build_dir: ./_site
    env:
      GITHUB_TOKEN: ${{ secrets.GH_PAT }}

env 這裡分為 GITHUB_TOKENGH_PAT兩種,前者不需要任何設定、僅限於同一個 repo 內的操作,後者則是部署到其他 repo 使用的。(說明)

GH_PAT 設定方式,請先至 Settings => Developer settings => Personal access tokens (或下方連結),按右上角新增Token,scopes 可直接勾選 repo 並將Token複製。

https://github.com/settings/tokens


再到專案的 Settings => Secrets => Actions (或下方連結),按右上角新增,命名 GH_PAT 並將剛剛複製的Token貼上。

https://github.com/[USER_NAME]/[REPO_NAME]/settings/secrets/actions

關於 GITHUB_TOKEN 可以看這部影片了解更多:


完整檔案如下:

name: website

on: push

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Set up Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: 2.6
      - name: Install dependencies
        run: bundle install
      - name: Before srcipt
        run: chmod +x ./script/cibuild
      - name: Srcipt
        run: ./script/cibuild
      - name: Deploy to GitHub Pages
        if: success()
        uses: crazy-max/ghaction-github-pages@v2
        with:
          repo: chenuin/chenuin.github.io
          target_branch: master
          build_dir: ./_site
        env:
          GITHUB_TOKEN: ${{ secrets.GH_PAT }}

檔案異動內容已經上傳至 Github ,可點選連結查看。


三、查看執行結果

https://github.com/[USER_NAME]/[REPO_NAME]/actions



相關文章:

參考連結:


2021年8月15日 星期日

CloudCannon 支援 Jekyll 網站後台管理CMS系統

With CloudCannon as your Jekyll CMS, you will have all the tools you need to create amazing static websites.

CloudCannon 是線上網站後台管理CMS系統,可以作為 Jekyll、HUGO等後台管理,除了提供圖形化操作介面新增文章,自動發布、排程的設定,也有免費的網域名(Domain) .cloudvent.net供即時瀏覽。

先註冊或使用 Github/Gitlab/Bitbucket 帳號登入,可以匯入已有的專案並支援雙向的同步,現在馬上開始!



輸入專案名稱:



選擇Github的同步的專案及分支:



編譯類型為Jekyll,其他安裝、編譯參數也在這邊設定。



完成後,可以看到後台。



這個測試網站是公開的,可以加上 Settings => Authentication 改用帳密登入。更多功能請參考 CloudCannon 官方文件