前端組件化實戰之 Button

⚠️本文爲掘金社區首發簽約文章,未獲受權禁止轉載css

你們好,我是洛竹🎋,一隻住在杭城的木系前端🧚🏻‍♀️,若是你喜歡個人文章📚,能夠經過點贊幫我彙集靈力⭐️。前端

前言

《每一個前端都應該擁有本身的組件庫,就像每一個夏天都有西瓜🍉》 一文中,洛竹帶領小黑從零搭建了一個組件庫項目,完成了項目結構、構建、測試、文檔等基礎工程化工做並完成了第一個組件 Icon。本期延續上期的組件工程化的主題,夏日炎熱,點上一杯楊枝甘露,和洛竹赴一場 Button 開發之約吧。赴約後,你將會收穫如下的內容:vue

PS:配合倉庫組件庫文檔閱讀本文效果更佳喲!node

Button 與設計心理學

做爲前端工程師,入行至今接觸最多的就是設計師了。耳濡目染下雖然說沒學會什麼設計工具,可是對設計與人的心理有了必定認識。react

洛竹認爲任何事物都不可能憑空出現,自有其傳承。使用普遍的基礎界面元素 Button 也不例外,咱們生活中就有隨處可見的按鈕。舉個栗子🌰,天天上班下班必然要按的電梯按鈕、手機音量按鈕、小米 9 雞肋的小愛同窗喚起按鈕。要搞清楚爲何須要按鈕,咱們有必要探究下生活中這些按鈕的做用。ios

點一下按鈕的快感

想象一下把鍵盤按鍵換成觸摸屏,你最在意的必定是完美還原物理鍵的敲擊感,像洛竹用手機虛擬鍵盤就喜歡設置按鍵震動和音效。經過打擊(點擊)得到快感是較爲廣泛的人性。按鈕在按下、鬆開時有豐富的質感和交互感,完美知足了人們點一下的快感。git

現實的實用性

從 BB 機到諾基亞再到現在的智能機,實體按鈕削減到只剩下音量鍵和開關機鍵。按鍵雖然光禿禿沒有任何標識,但咱們就是知道它的功能。試想一下沒有這個來自遠古時代的開關鍵,你手裏的手機就是一塊板磚。github

瘋狂暗示用戶,達到不可告人目的

小米 9 單獨喚起小愛同窗的按鍵常常會被誤按,以前我還不理解這麼蠢的設計的目的。在簡單研究了點設計心理學我明白了。小愛的設計者爲了 產品日活和 AI 訓練就是故意這個設計的。web

小米 10 雖然移除了單獨的喚起鍵,卻把原來的電源鍵改爲了一鍵多用。每次想要重啓手機還得先喚起一下小愛同窗。不得不說,小愛同窗小米親女兒。面試

吐槽歸吐槽,小米這個按鈕確實起到了培養用戶習慣的任務。當用戶知悉某個按鈕能指向某個操做,或者獲取某類信息後,久而久之用戶就會造成使用習慣。若是某操做可以爲用戶和廠商持續帶來價值,那就可讓按鈕的位置更加醒目,持續培養用戶點擊習慣。

指引用戶操做

這個在 Web 開發中是最多見的使用場景,每一個可交互頁面上都有這類按鈕的出現,用來指引用戶下一步該怎麼作。好比表單的提交和重置。

雖然按鈕也常做爲表單元素,可是區別於其餘表單元素,按鈕因其自然地自說明性,不須要 Label 對其進行輔助說明,囉嗦這麼多,掘友們應該在看到一個按鈕時,應該也會有從設計上品鑑的意識了,歡迎將對下圖的品鑑在評論區告訴洛竹。

組件主題化

在開始開發具體組件以前,咱們必須先約定好組件主題化的規範。以前 antd-mobile-rn 就由於設計問題,中途花費大力氣重構。幾乎全部的組件庫都會將色彩、佈局這些以 css 變量的形式提供給使用者和開發者爲,React Native 不一樣的是樣式基於 CSS in JS,不過道理相通,參照 vant 的設計資源,咱們抽出了一套 JavaScript 常量:

// packages/themes
export interface Theme {
  'animation-duration-base': string;
  'animation-duration-fast': string;
  'animation-timing-function-enter': string;
  'animation-timing-function-leave': string;
  'font-size-xs': number;
  'font-size-sm': number;
  'font-size-md': number;
  'font-size-lg': number;
  'font-weight-bold': number;
  // 變量過多,這裏僅展現部分變量
}
複製代碼

有了這些 JS 常量,咱們就能夠設計主題系統。基於 CSS in JS 的主題化設計通常是基於 React Context 實現,須要提供 ThemeProvider 傳入主題上下文,ThemeConsumer、WithTheme(高階類組件)、withTheme(高階函數組件) 或 useTheme(React Hooks)做爲消費者獲取上下文。本身實現也不難,不過更文任務比較緊急,咱們先基於 cssinjs/theming 實現功能,後期有須要再回來造輪子也不遲。下面👇就是咱們基於 theming 的 createTheming 函數建立自定義主題上下文。

import { createTheming } from 'theming';
const context = React.createContext(defaultTheme);
const theming = createTheming(context);

export const { ThemeProvider, withTheme, useTheme } = theming;
複製代碼

主題功能是通用的,所以我將主題相關的能力都放在 @vant-react-native/theme 包中發佈。

Button 的實現

React Native 內置的 Button 組件的樣式是固定的,只能進行一些簡單的設置。且內置的 Button 組件在 Android 和 ios 兩個平臺上的表現並不一致。因此咱們須要根據更底層的組件進行封裝。咱們對比 ant-design-mobile-rn 和 react-native-elements 後採用了前者使用的 TouchableHighlight 組件。因爲繼承自 TouchableHighlight,因此咱們組件的 Props 類型以下:

import { TouchableHighlightProps } from 'react-native';
interface ButtonProps extends TouchableHighlightProps {
}
複製代碼

按鈕類型

vant 的 Button 支持 defaultprimaryinfowarningdanger 五種類型,默認爲 default。如今,組件的基本定義以下:

// ...
import React, { FunctionComponent } from 'react';
import { Text, View } from 'react-native';

interface ButtonProps {
  type?: 'default' | 'primary' | 'info' | 'warning' | 'danger';
}

const Button: FunctionComponent<ButtonProps> = props => {
  // ...
};
// ...
複製代碼

咱們的組件爲了適應主題化需求,樣式不能是寫死在組件裏的,而是要經過上下文獲取樣式常量。咱們思路是首先使用 useTheme 從上下文中獲取主題,而後因爲樣式定義較多,咱們爲每一個組件編寫一個 useStyle hook 放在單獨的 style.ts 文件中:

import { StyleSheet } from 'react-native';
import { Theme, useTheme } from '@vant-react-native/theme';

export const useStyle = props => {
  const theme = useTheme<Theme>();

  const getBackgroundColor = () => {
    switch (props.type) {
      case 'primary':
        return theme['success-color'];
      case 'info':
        return theme['primary-color'];
      case 'warning':
        return theme['warning-color'];
      case 'danger':
        return theme['danger-color'];
      default:
        return theme.white;
    }
  };

  const getTextColor = () => {
    if (props.type === 'default') {
      return theme.black;
    } else {
      return theme.white;
    }
  };

  const getBorderRadius = () => {
    if (props.round) {
      return theme['border-radius-max'];
    }
    if (props.square) {
      return 0;
    }
    return theme['border-radius-sm'];
  };

  const styles = StyleSheet.create({
    container: {
      alignItems: 'center',
      backgroundColor: getBackgroundColor(),
      borderColor: getBorderColor(),
      borderRadius: theme['border-radius-sm'],
      borderWidth: theme['border-width-base'],
      flexDirection: 'row',
      flex: 1,
      justifyContent: 'center',
      opacity: 1,
      paddingHorizontal: 15,
    },
    indicator: {
      marginRight: theme['padding-xs'],
    },
    textStyle: {
      color: getTextColor(),
      fontSize: 14,
    },
    wrapper: {
      borderRadius: theme['border-radius-sm'],
      height: 44,
    },
  });
  return styles;
};
複製代碼

基於 useStyle 咱們即可完成一個支持多類型的 Button 組件:

const Button: FunctionComponent<ButtonProps> = props => {
  const styles = useStyle(props);
  const { style, ...restProps } = props;
  return (
    <TouchableHighlight style={[styles.wrapper, style]} {...restProps}> <View style={styles.container}> {typeof props.children === 'string' ? ( <Text style={styles.textStyle}>{props.children}</Text> ) : ( props.children )} </View> </TouchableHighlight>
  );
};
複製代碼

注意:子組件多是字符串,也多是組件,因此須要判斷類型。

實現效果以下:

樸素按鈕

樸素按鈕的文字爲按鈕顏色,背景爲白色,咱們經過 plain 屬性將按鈕設置爲樸素按鈕。調研了 antd 和 react-native-elements 發現它們都是定義了不少樣式,而後在組件內經過邏輯判斷計算具體樣式的值。我的很不喜歡這種方式,不是完全的 CSS in JS,個人處理方式是將全部有關樣式計算的都封裝在每一個組件的 useStyle 鉤子中,好比當引入樸素按鈕屬性時,相對於普通按鈕改變的有容器背景色、容器邊框和字體顏色。因此咱們將這三個屬性的值都經過一個單獨的函數計算。對比 antd 的源碼,會發現不只代碼更易讀,甚至代碼量也少了。

const getBackgroundColor = () => {
  if (props.plain) {
    return theme.white;
  }
  // ...
};

const getTextColor = () => {
  if (props.plain) {
    switch (props.type) {
      case 'primary':
        return theme['success-color'];
      case 'info':
        return theme['primary-color'];
      case 'warning':
        return theme['warning-color'];
      case 'danger':
        return theme['danger-color'];
      default:
        return theme['gray-3'];
    }
  } else if (props.type === 'default') {
    return theme.black;
  } else {
    return theme.white;
  }
};
複製代碼

實現效果以下:

細邊框

vant 實現細邊框是經過設置 hairline 屬性能夠展現 0.5px 的細邊框。可是手機上因爲分辨率的影響,貿然設置 0.5 會致使邊框不顯示的兼容問題。好在 React Native 爲咱們提供了 StyleSheet.hairlineWidth 常量來兼容最細邊框問題,下面是官方對它的定義:

hairlineWidth 這一常量始終是一個整數的像素值(線看起來會像頭髮絲同樣細),並會盡可能符合當前平臺最細的線的標準。能夠用做邊框或是兩個元素間的分隔線。然而,你不能把它「視爲一個常量」,由於不一樣的平臺和不一樣的屏幕像素密度會致使不一樣的結果。

若是模擬器縮放過,可能會看不到這麼細的線。

因爲 hairline 隻影響了容器 borderWidth 屬性,咱們不須要編寫單獨的樣式計算函數:

const styles = StyleSheet.create({
  // ...
  container: {
    // ...
    borderWidth: props.hairline ? theme['border-width-hairline'] : theme['border-width-base'],
  },
});
複製代碼

實現效果以下:

禁用狀態

表單元素或者說可觸摸可點擊的元素通常都有禁用狀態,vant 中是經過 disabled 屬性來禁用按鈕,禁用狀態下按鈕不可點擊。TouchableHighlight 繼承地有 disabled 屬性,咱們只須要設置一些禁用狀態下的按鈕樣式就能夠,查看 vant 源碼咱們發現只須要修改透明度爲 0.5 便可:

const styles = StyleSheet.create({
  container: {
    // ...
    opacity: props.disabled ? 0.5 : 1,
    // ...
  },
});
複製代碼

實現效果以下:

加載狀態

vant 是經過 loading 屬性設置按鈕爲加載狀態,加載狀態下默認會隱藏按鈕文字,能夠經過 loading-text 設置加載狀態下的文字。咱們藉助 React Native 的 ActivityIndicator 組件能夠輕鬆實現這個特性:

// ...
<TouchableHighlight {...restProps}>
  <View style={styles.contentWrapper}> {props.loading ? ( <> <ActivityIndicator size="small" color={indicatorColor} style={styles.indicator} /> {props.loadingText ? <Text style={styles.textStyle}>{props.loadingText}</Text> : null} </>
    ) : null}
  </View>
</TouchableHighlight>
// ...
複製代碼

樣式以下:

export const useIndicatorColor = (props: ButtonProps): string => {
  const theme = useTheme<Theme>();
  if (props.plain) {
    switch (props.type) {
      case 'primary':
        return theme['success-color'];
      case 'info':
        return theme['primary-color'];
      case 'warning':
        return theme['warning-color'];
      case 'danger':
        return theme['danger-color'];
      default:
        return theme.black;
    }
  } else if (props.type === 'default') {
    return theme.black;
  } else {
    return theme.white;
  }
};
複製代碼

實現效果以下:

按鈕形狀

默認的按鈕有值爲 2 的圓角,vant 中經過 square 設置方形按鈕,經過 round 設置圓形按鈕。按例,咱們經過判斷設置樣式:

const getBorderRadius = () => {
  if (props.round) {
    return theme['border-radius-max'];
  }
  if (props.square) {
    return 0;
  }
  return theme['border-radius-sm'];
};
const styles = StyleSheet.create({
  container: {
    borderColor: getBorderColor(),
  },
  wrapper: {
    borderRadius: getBorderRadius(),
  },
});
複製代碼

實現效果以下:

按鈕尺寸

Antd RN 只提供了 large、small 兩個尺寸,而在 vant 中支持 large、normal、small、mini 四種尺寸,默認爲 normal。雖然寫到這裏已經很疲倦了,楊枝甘露也早喝完了,可是爲了完整復原,仍是續上一杯咖啡繼續肝。根據 vant 設計稿咱們新增三個樣式獲取函數並動態化指定樣式:

const getSizeHeight = () => {
  switch (props.size) {
    case 'large':
      return 50;
    case 'small':
      return 32;
    case 'mini':
      return 24;
    default:
      return 44;
  }
};
const getSizePadding = () => {
  switch (props.size) {
    case 'small':
      return 8;
    case 'mini':
      return 4;
    default:
      return 15;
  }
};
const getSizeFontSize = () => {
  switch (props.size) {
    case 'large':
      return 16;
    case 'small':
      return 12;
    case 'mini':
      return 10;
    default:
      return 14;
  }
};

const styles = StyleSheet.create({
  container: {
    paddingHorizontal: getSizePadding(),
  },
  textStyle: {
    fontSize: getSizeFontSize(),
  },
  wrapper: {
    height: getSizeHeight(),
  },
});
複製代碼

實現效果以下:

自定義顏色

若是不是本身親自復刻 Vant,是沒想到一個 Button 能玩出這麼多花,支持特性這麼多耐心和代碼管理都是一個挑戰。固然了,洛竹採起的樣式管理方式比較偏激,你們有好的方式也能夠在評論區討論。

經過 color 屬性自定義按鈕的顏色。咱們能夠得出需求,無論 type 是什麼,color 屬性需始終覆蓋原有樣式,color 能影響的就是背景色、字體顏色和邊框顏色,因此咱們修改 getBackgroundColorgetTextColorgetBorderColor 樣式函數在合適的地方加上如下代碼便可:

if (props.color) {
  return props.color;
}
複製代碼

實現效果以下:

雙擊事件的實現

咱們從 React Native 內置的 TouchableHighlight 組件繼承了不少事件,其中 onPress、onLongPress 分別表明單擊和長按。但惟獨「雙擊 666」的雙擊事件沒有姓名。以前在實際業務曾經封裝過雙擊事件,此次咱們就直接就內置了。

實現思路是延時執行單擊事件(默認 200 毫秒),而後記錄點擊次數和兩次時間間隔,當識別爲第二次點擊且時間間隔小於單擊延時時間。那麼就取消單擊事件延時,並當即執行雙擊事件。完整代碼以下:

let lastTime = 0;
let clickCount = 1;
let timeout = null;
const _onPress = (event: GestureResponderEvent) => {
  const now = Date.now();
  if (timeout) {
    clearTimeout(timeout);
  }
  timeout = setTimeout(() => {
    props.onPress(event);
    clickCount = 1;
    lastTime = 0;
  }, props.delayDoublePress);
  if (clickCount === 2 && now - lastTime <= props.delayDoublePress) {
    clearTimeout(timeout);
    clickCount = 1;
    lastTime = 0;
    props.onDoublePress(event);
  } else {
    clickCount++;
    lastTime = now;
  }
};
複製代碼

你們會發現這裏的實現糅合了函數防抖、節流以及計數器的原理,有興趣的小夥伴能夠自行復習下原理,這裏就不展開了。

API 文檔

一個組件的文檔,除了 Demo,還須要展現出來可用的 Props,Dumi 內置的 <API></API> 組件能夠根據組件自動生成 API 文檔。首先咱們像下面同樣編寫 Props 註釋:

interface ButtonProps extends TouchableHighlightProps {
  /** * @description Can be set to primary、info、warning、danger * @description.zh-CN 類型,可選值爲 primary、info、warning、danger */
  type?: 'default' | 'primary' | 'info' | 'warning' | 'danger';
  /** * @description Can be set to large、small、mini * @description.zh-CN 尺寸,可選值爲 */
  size?: 'large' | 'normal' | 'small' | 'mini';
}
複製代碼

而後在 Markdown 中引入 API 組件便可:

<API src="./index.tsx"></API>
複製代碼

內置組件 API 沒有處理繼承的狀況,咱們後續會自定義一個 API 組件,這裏就不展開了,瀏覽 Button 文檔 能夠查看如今的效果:

工程化串講

因爲很難在一篇文章中將組件開發相關的工程化講完,咱們須要在每篇實戰中串講一下。

組件建立腳手架

小黑:洛竹,lerna create 命令建立出來的模塊並非咱們想要的,之後要建立不少不少組件,咱們能夠寫一個建立組件模塊的腳手架嗎?

lerna 使用起來是有很多痛點的,lerna create 命令沒辦法指定模板,考慮到以後的幾十上百個組件每次建立都要進行項目結構、Typescript 配置、單元測試配置、Babel 配置等等工做步驟,咱們有必要寫一個腳手架。

模板解析

說到模板解析,相信你們和我同樣想到的是 vue-cli 的 template 解析。經過閱讀 vue-cli@2.9.6 generate.js 源碼,咱們能夠分析出尤大是基於 metalsmith、handlebars、consolidate 這三個包來實現模板解析能力的。讓人不安的是其中 metalsmith 庫有長達 5 年沒有維護了,洛竹挑選開源項目通常對維護度很敏感,本着輪子要用本身造的原則,我翻看了 Metalsmith 的 Readme 發現這個插件無非是經過遞歸讀文件的方式渲染模板,而且它的靜態網站生成的能力對咱們模板解析的需求也是多餘的。

說幹就幹,在和 @林小帥 同窗簡單溝通後,我動手造了 handlebars-template-compiler 這個輪子,其主要原理以下:

  1. 使用 recursive-readdir 遞歸獲取全部文件路徑
const files = await recursive(rootDir);
複製代碼
  1. 使用 handlebars.compile 方法使用元數據對模板進行渲染
const content = fs.readFileSync(file).toString();
const result = handlebars.compile(content)(meta);
複製代碼
  1. 使用 fs.writeFileSync API 重寫文件

另外,經過引入 glob 模式匹配實現了 exclude 配置以及只處理指定後綴(默認 **/*.tpl.*)的文件來避免沒必要要的渲染。(PS:NPM 一週有了 300 多下載,有須要的掘友值得一試😄)

Node CLI(@vant-react-native/scripts)搭建

這裏洛竹嘗試用最簡潔的語言爲你們描述一個腳手架的誕生,源碼在 packages/scripts 目錄下,沒有接觸過 CLI 的掘友請相信我,Node CLI 很容易上手的。接觸過的同窗也能夠查漏補缺借鑑一二。

  1. package.json 文件的 bin 字段是咱們腳手架的入口
// 指定可執行文件的位置以及別名
"bin": {
  "vant": "./bin/cli.js"
},
複製代碼
  1. 定義 ./bin/cli.js 爲可執行文件並調用 init 方法。
// 因爲咱們的腳本是 Node 編寫的,因此須要指定 node 所在位置
#!/usr/bin/env node
const { init } = require('../lib');
// 這個地方參考了 create-react-native 的設計
// 本文點贊過 300,下一篇洛竹帶小黑爲你們帶來《基於 TypeScript 重構 create-react-native》
init();
複製代碼
  1. 而後在 src/index.ts 中初始化 commander 這個久負盛名的命令行框架
const init = (): void => {
  const packageJson = require('../package.json');
  program.version(packageJson.version).description(packageJson.description);
  // ...
  program.parse(process.argv);
};
複製代碼
  1. 爲了方便管理命令,咱們將命令都放置在 src/commands 目錄下並經過 fs.readdirSync API 動態掃描註冊。
const init = (): void => {
  // 這段代碼借鑑自 NeteaseCloudMusicApi 項目,做者的代碼頗有設計感,推薦閱讀。
  fs.readdirSync(path.join(__dirname, 'commands')).forEach((file: string) => {
    if (!file.endsWith('.js')) return;
    require(path.join(__dirname, 'commands', file));
  });
  // ...
};
複製代碼
  1. 最後在 commands 目錄下新建一個 create.ts 文件編寫命令
import { program } from 'commander';
program
  .command('create <name> [loc]')
  .description('Create a new vant-react-native package')
  .action((name,loc) => {
    console.log('Hello Luozhu');
  })
複製代碼

腳手架實現

上一小結,咱們初始化了 CLI 並添加了 create 命令,這一小節咱們就來實現一下腳手架功能。

咱們首先在 packages/scripts 目錄下建立組件模板

.
├── README.tpl.md # tpl 後綴在生成組件模板的時候會被 handlebars-template-compiler 自動去掉。
├── package.tpl.json
├── src
│   └── index.ts # 沒有 tpl 後綴則不會被編譯,模板很大時能夠節省時間。
└── tsconfig.json
複製代碼

而後咱們明確咱們的模板元數據的數據結構,我這裏的數據結構是:

interface IMeta {
  name: string;
  version: string;
  description: string;
  author: string;
  email: string;
  url: string;
  directory: string;
}
複製代碼

有了數據結構,咱們就可使用 inquirer 模塊引導用戶輸入信息。

import inquirer from 'inquirer';
// ...
// getQuestions 過長,感興趣的同窗能夠查看:http://tny.im/UFbg
const answer: IMeta = await inquirer.prompt(getQuestions(name));
// ...
複製代碼

下一步,咱們使用 tmp-promise 模塊建立一個系統臨時文件夾,並將前文提到的 template 文件夾的內容拷貝進去:

import tmp from 'tmp-promise';
import fs from 'fs-extra';
import path from 'path';
// ...
const tmpdir = await tmp.dir({ unsafeCleanup: true });
fs.copySync(path.join(__dirname, '../../template'), tmpdir.path);
複製代碼

最後,咱們對臨時文件夾的內容進行編譯再拷貝到指定位置便可:

import htc from 'handlebars-template-compiler';
// ...
await htc<IMeta>(answer, tmpdir.path);
fs.copySync(tmpdir.path, `${process.cwd()}/packages/${locPath}`);
// ...
複製代碼

折騰這一頓,讓咱們來看下成果吧:

Github CODEOWENERS

大型的開源項目最難的不是技術問題,技術大咖永遠不會缺。最難的實際上是協做和後期維護。試想一下一個成百上千人蔘與的項目當有新的 pr 時,正常人根本無力去快速檢索出須要誰去 review 代碼。咱們的 vant-react-native 因爲是將每一個組件單獨發包維護,當參與的小夥伴多了也會產生這個困擾。

而 GitHub CODEOWNERS(代碼全部者)就是爲了解決這個問題的,在 5000+ 貢獻者參與的 DefinitelyTyped 項目中咱們就能夠看到它的身影。官方對代碼全部者定義以下:

你可使用 CODEOWNERS 文件定義負責倉庫代碼的我的或團隊。當有人修改代碼並打開一個 pull request 時,將自動請求代碼全部者進行審查。

CODEOWNERS 文件使用遵循 gitignore 文件中所用大多數規則的模式,CODEOWNERS 文件位置通常位於 .github/ 目錄下。

在 vant-react-native,洛竹是倉庫的最終負責人,因此是指望每一個 pr 均可以分配給本身審查一下的。那麼咱們這就來實驗一下吧,新建一個 .github/CODEOWNERS 文件並寫入如下內容:

# This is a comment.
# Each line is a file pattern followed by one or more owners.

# These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence,
# @youngjuning will be requested for review when someone opens a pull request.
*       @youngjuning

# In this example, @doctocat owns any files in the build/logs
# directory at the root of the repository and any of its
# subdirectories.
/packages/ @luozhu1994
複製代碼

通常若是文件具備代碼全部者,則在打開拉取請求以前能夠看到代碼全部者是誰。在倉庫中,你能夠找到文件並懸停於一個鎖圖標上,懸浮以後會告訴你該文件全部者是誰:

而後咱們提交一個 pr 看看效果:

NPM 發包自動化

發包權限通常只有倉庫全部者一我的擁有,可是 owner 同時維護好幾個 NPM 帳號,或者是 owner 突然很忙將發佈權限交給其餘人管理員可是不便告知 NPM 帳號該怎麼辦呢?答案是將 NPM 發包 CD(持續部署)化,公司通常會基於 Gitlab 或自建平臺實現該功能。做爲開源項目,咱們固然是使用 GitHub Action。

正常的單包項目,使用 npm-publishnpm-publish-action 這兩個 GitHub Action,這並無好講的。可是基於 lerna 的多包單體倉庫並無現成的插件能夠用,照例,咱們來看下本身實現的步驟:

  1. 判斷 commit message 是否以 chore(release): 開頭

    經過 GitHub Action startsWith(github.event.head_commit.message, 'chore(release):') 實現

  2. 經過 NPM publish token 認證登陸

    經過 npm config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }} 認證

  3. 執行 lerna publish from-package --yes 發佈

    須要本地先執行 lerna version 系列命令提高版本

完整 GitHub Action 實現以下:

name: npm-publish

on:
  push:
    branches:
      - main

jobs:
  npm-publish:
    runs-on: ubuntu-latest
    if: startsWith(github.event.head_commit.message, 'chore(release):')
    steps:
      - uses: actions/checkout@v2
      - uses: c-hive/gha-yarn-cache@v2 # 緩存 node_modules 加快構建速度
      - name: Install Packages
        run: yarn install --registry=https://registry.npmjs.org/
      - name: Authenticate with Registry
        run: | npm config set //registry.npmjs.org/:_authToken=${NPM_TOKEN}         env:
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
      - name: Publish package
        run: lerna publish from-package --yes
複製代碼

爲了在發佈後及時獲取通知,洛竹使用了 peter-evans/commit-comment 插件在發佈失敗或成功後對相應 commit 進行評論,這樣咱們就能夠收到郵件和站內通知。

- name: Create commit comment after publish successfully
  if: ${{ success() }}
  uses: peter-evans/commit-comment@v1
  with:
    body: | Hello Dear @youngjuning. This commit has been publish to NPM successfully. > Created by [commit-comment][1] 
      [1]: https://github.com/peter-evans/commit-comment
- name: Create commit comment after publish unsuccessfully
  if: ${{ failure() }}
  uses: peter-evans/commit-comment@v1
  with:
    body: | Hello Dear @youngjuning. This commit has been publish to NPM unsuccessfully. > Created by [commit-comment][1] 
      [1]: https://github.com/peter-evans/commit-comment
複製代碼

致謝

截止發稿時,每一個前端都值得擁有本身的組件庫,就像每一個夏天都擁有西瓜🍉 已得到近 1600 贊、超 4 萬閱讀📖,再次再次感謝掘友的支持、編輯 Zoe 的鞭策,月影大佬的轉載、朋友的轉發以及本身的堅持。

近期好文

本文首發於「掘金專欄」,同步於公衆號「程序人生」和「洛竹的官方網站」。

相關文章
相關標籤/搜索