⚠️ this article for nuggets community first contract article, not authorized to forbid reprint

Hello everyone, my name is Luozhu 🎋, a wooden front end in Hangzhou, 🧚🏻♀️. If you like my article 📚, please click “like” to help me gather “jinli”.

preface

In the article “Every front end should have its own component library, just like watermelon every summer 🍉”, Luo Zhu led Xiaohei to build a component library project from zero, completed basic engineering work such as project structure, construction, testing, documentation and completed the first component Icon. This issue continues the theme of component engineering in the previous issue. In the summer heat, please have a cup of Yang Zhi manna and go to a Button development meeting with Luo Zhu. Here’s what you’ll get when you show up:

PS: With the warehouse and component library documentation to read this article better yo!

Button and design psychology

As a front end engineer, I have been working with designers the most. Although I didn’t learn any design tools, I had a certain understanding of design and human psychology.

Luo Zhu believes that nothing can emerge out of thin air and have its own inheritance. Button is no exception to the widespread use of basic interface elements. We have buttons everywhere in our lives. Take 🌰 as an example, the elevator button, the volume button of the mobile phone, and the small love student arouse button of mi 9 chicken. To understand why we need buttons, it’s worth exploring what they do in our lives.

The thrill of hitting a button

Imagine replacing the keyboard keys with a touch screen. What you care most is to perfectly restore the feeling of the physical keys. Like Luo Zhu, she likes to set the keys to vibrate and sound effects with her mobile phone’s virtual keyboard. It is universal human nature to hit (click) for pleasure. The button is pressed, loose from time to time rich simple sense and mutual feeling, perfectly satisfied the pleasure that people point once.

Practical utility

From beeps to Nokia to today’s smartphones, physical buttons have been reduced to volume and on/off buttons. The button is bare and has no logo, but we just know what it does. Imagine that without this ancient switch, the phone in your hand would be a brick.

Crazy hint user, achieve ulterior motives

The button that evokes Xiao Ai alone in Mi 9 is often pressed by mistake, and I didn’t understand the purpose of such a silly design before. I figured it out after a little research into design psychology. Xiao AI’s designers did this on purpose for product day work and AI training.

Although the mi 10 has removed a separate arousal button, it has replaced the original power button with a one-button multi-function. Every time I want to restart my phone, I have to arouse Xiao Ai. Have to say, little love schoolmate millet kiss daughter.

Make fun of return to make fun of, millet this button really had the task that cultivated user habit. When users know that a button can point to a certain operation, or obtain a certain type of information, over time users will form habits. If an action continues to bring value to both the user and the vendor, make the button more visible and keep users clicking.

Guide user operations

This is one of the most common usage scenarios in Web development, where buttons appear on every interactive page to guide the user to what to do next. Such as form submission and resetting.

Although button also often used as a form element, but different from other form elements, button because of its natural self explanatory, don’t need a Label on the auxiliary, repetitive so much, dig the friends should see a button, should also have tasting consciousness from the design, welcome to chart the tasting told los bamboo in the comments section.

Component thematization

Before we can begin to develop a specific component, we must first agree on a specification for component themes. Antd-mobile-rn had to be refactored in the middle of the game because of design problems. Almost all component libraries provide color, layout, and so on to the user and developer in the form of CSS variables. React Native is different in that its styles are based on CSS in JS.

// 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;
  // There are too many variables. Only some variables are shown here
}
Copy the code

With these JS constants, we can design the theme system. CSS in JS based theme design is usually implemented based on React Context, and the ThemeProvider is required to pass in the theme Context. ThemeConsumer, WithTheme (higher-order class components), WithTheme (higher-order function components), or useTheme (React Hooks) get the context for the consumer. It is not difficult to implement by ourselves, but the task is more urgent, we first based on cssinJS/Theming to implement the function, later need to come back to build the wheel is not late. Below 👇 is where we create a custom theme context based on theming’s createTheming function.

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

export const { ThemeProvider, withTheme, useTheme } = theming;
Copy the code

The theme functionality is universal, so I’m releasing the theme-related capabilities in the @vant-React-native/Theme package.

The realization of the Button

React Native’s built-in Button component styles are fixed and can only be set in a few simple ways. And the built-in Button component doesn’t work equally on Android and ios. So we need to encapsulate it according to the lower-level components. We compared ant-Design-mobile-Rn with React-Native Elements and adopted the TouchableHighlight component used by the former. Since we inherit from TouchableHighlight, our component Props are of the following type:

import { TouchableHighlightProps } from 'react-native';
interface ButtonProps extends TouchableHighlightProps {
}
Copy the code

Button type

Vant Button supports default, Primary, INFO, Warning, danger five types, default is default. Now, the basic definition of a component is as follows:

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

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

const Button: FunctionComponent<ButtonProps> = props= > {
  // ...
};
// ...
Copy the code

In order for our component to adapt to the theme requirements, the style must not be written in the component, but rather the style constant must be obtained from the context. The idea is to first use useTheme to get the theme from the context, and then write a useStyle hook for each component in a separate style.ts file because there are many styles defined:

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:
        returntheme.white; }};const getTextColor = () = > {
    if (props.type === 'default') {
      return theme.black;
    } else {
      returntheme.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;
};
Copy the code

With useStyle we can create a Button component that supports multiple types:

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>
  );
};
Copy the code

Note: A child component can be either a string or a component, so you need to determine the type.

The results are as follows:

A simple button

The plain button’s text is the button color and the background is white. We set the button to the plain button by using the plain property. We investigated ANTD and React-Native Elements and found that they both define many styles, and then calculate the values of specific styles by logical judgment within the component. Personally, I don’t like this approach. Instead of being completely CSS in JS, my approach is to encapsulate all the calculation of styles in the useStyle hooks for each component, such as when introducing the plain button properties, changing the foreground, container border, and font color relative to the normal button. So we calculate the values of all three properties by a single function. Comparing antD’s source code, it’s not only easier to read, it’s even less code.

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 {
    returntheme.white; }};Copy the code

The results are as follows:

Fine border

Vant implements thin borders by setting the hairline property to display 0.5px thin borders. However, due to the influence of the resolution on the phone, setting 0.5 hastily will lead to the compatibility problem that the border will not be displayed. React Native provides a stylesheet.HairlineWidth constant for compatibility with the thinest border. Here’s how it’s officially defined:

The hairlineWidth constant is always an integer number of pixels (the line will look as thin as a human hair) and will try to match the thinest line on the current platform. Can be used as a border or as a separator between two elements. However, you can’t “treat it as a constant” because different platforms and different screen pixel densities can lead to different results.

If you zoom in on the simulator, you might not see this thin line.

Since the hairline only affects the container’s borderWidth property, we don’t need to write a separate style calculation function:

const styles = StyleSheet.create({
  // ...
  container: {
    // ...
    borderWidth: props.hairline ? theme['border-width-hairline'] : theme['border-width-base'],}});Copy the code

The results are as follows:

Disabled state

Form elements, or touchable and clickable elements, usually have a disabled state. In Vant, buttons are disabled with the disabled property. When disabled, buttons are not clickable. TouchableHighlight inherits the disabled property, so we just need to set some buttons in the disabled state. Looking at the vant source code, we can see that we only need to change the opacity to 0.5:

const styles = StyleSheet.create({
  container: {
    // ...
    opacity: props.disabled ? 0.5 : 1.// ...}});Copy the code

The results are as follows:

Loading status

In vant, the loading property is used to set the button to the loading state. By default, the button text is hidden in the loading state. You can use loading-text to set the text in the loading state. We can easily implement this feature with the React Native ActivityIndicator component:

// ...<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>
// ...
Copy the code

The style is as follows:

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:
        returntheme.black; }}else if (props.type === 'default') {
    return theme.black;
  } else {
    returntheme.white; }};Copy the code

The results are as follows:

Button shapes

The default button has a rounded corner with a value of 2. Vant uses square to set the square button and round to set the round button. As an example, we set the style by judging:

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(),
  },
});
Copy the code

The results are as follows:

Button size

Antd RN provides only large and small sizes, while VANT supports large, Normal, Small, and mini sizes, which are normal by default. Although I am very tired to write here, and I have already finished drinking the poplar nectar, but in order to complete recovery, I still have a cup of coffee to continue my liver. We added three new styles to the vant design to get functions and dynamically specify styles:

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(),
  },
});
Copy the code

The results are as follows:

Custom color

If I didn’t copy Vant myself, I didn’t think a Button could play so many flowers, support features so much patience and code management is a challenge. Of course, luo Zhu’s style management method is quite extreme, and you can discuss it in the comments section if you have a good way.

Customize the color of the button through the color property. We can get the requirement that no matter what type is, the color attribute should always override the original style. The color can affect the background color, the font color and the border color. So we modify the getBackgroundColor, getTextColor, and getBorderColor style functions by adding the following code where appropriate:

if (props.color) {
  return props.color;
}
Copy the code

The results are as follows:

Double-click the implementation of the event

We inherit many events from the React Native built-in TouchableHighlight component, where onPress and onLongPress stand for click and long press, respectively. But only the double-click event “double-click 666” does not have a name. The double click event has been encapsulated in real business before, and this time we will just build it in.

The idea is to delay the execution of the click event (200 milliseconds by default), and then record the number of clicks and the two time interval, when identified as the second click and the time interval is less than the click delay time. Cancel the click event delay and execute the double-click event immediately. The complete code is as follows:

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; }};Copy the code

You will find that the implementation here is a combination of function damping, throttling and counter principle, interested partners can review the principle, here will not expand.

The API documentation

The
component in Dumi can automatically generate API documentation based on the component. First we write Props as follows:

interface ButtonProps extends TouchableHighlightProps {
  / * * *@description       Can be set to primary, info, warning, danger *@descriptionZh-cn. The value can be primary, INFO, Warning, or Danger */
  type? :'default' | 'primary' | 'info' | 'warning' | 'danger';
  / * * *@description       Can be set to large, small, mini *@descriptionZh-cn Size. The optional value is */size? :'large' | 'normal' | 'small' | 'mini';
}
Copy the code

Then introduce the API components in Markdown:

<API src="./index.tsx"></API>
Copy the code

The built-in component API doesn’t handle inheritance, so we’ll customize an API component later, so we won’t expand it here. Browse the Button document to see what it looks like now:

Engineering series

Since it is difficult to cover all the engineering aspects of component development in a single article, there are a few engineering aspects to cover before we begin our tour of Button.

Component create scaffolding

The lerna create command creates a module that is not what we want. We will create many components in the future. Can we write a scaffold to create component modules?

Lerna is a pain to use. The lerna create command has no way to specify templates, and it is necessary to write a scaffold for the dozens or hundreds of components that will need to be structured, Typescript configured, unit tested, Babel configured, etc.

The template parsing

When it comes to template parsing, I believe you are thinking of vue-CLI template parsing like me. By reading [email protected] generate.js source code, we can analyze that the main template parsing capability is based on the three packages metalSmith, Handlebars, and Consolidate. The unsettling thing is that the metalsmith library hasn’t been maintained for 5 years. Rozur is very sensitive to maintenance when it comes to open source projects. In line with the principle that wheels should build their own, I looked at metalSmith’s Readme and found that this plugin renders templates by recursively reading files. And its static site-generating capabilities are redundant to our template parsing needs.

After a brief communication with @Xiaoshuai Lin, I started to build handlebars- Template-compiler wheel. The main principles are as follows:

  1. Use recursive-readdir to get all file paths recursively
const files = await recursive(rootDir);
Copy the code
  1. usehandlebars.compileMethod uses metadata to render the template
const content = fs.readFileSync(file).toString();
const result = handlebars.compile(content)(meta);
Copy the code
  1. usefs.writeFileSyncAPI rewrite file

In addition, the introduction of glob pattern matching eliminates unnecessary rendering by implementing the exclude configuration and processing only files with the specified suffix (**/*.tpl.* by default). (PS: NPM has more than 300 downloads in a week, it is worth a try if you need to dig friends 😄)

The Node CLI (@vant-React-native /scripts) is set up

The source code is in the packages/scripts directory. Believe me, Node CLI is easy to use. Contact with the students can also check the omission of one or two.

  1. package.jsonOf the filebinThe field is the entrance to our scaffolding
// Specify the location and alias of the executable file
"bin": {
  "vant": "./bin/cli.js"
},
Copy the code
  1. define./bin/cli.jsFor executable file and callinitMethods.
// Since our script is written in Node, we need to specify the location of Node
#!/usr/bin/env node
const { init } = require('.. /lib');
// This is a reference to the create-React-native design
// Refactoring create-React-native with TypeScript
init();
Copy the code
  1. Then, insrc/index.tsInitializes Commander, the well-known command line framework
const init = (): void= > {
  const packageJson = require('.. /package.json');
  program.version(packageJson.version).description(packageJson.description);
  // ...
  program.parse(process.argv);
};
Copy the code
  1. To make it easier to manage commands, we place them insrc/commandsDirectory and throughfs.readdirSyncAPI dynamic scan registration.
const init = (): void= > {
  // This code is borrowed from the NeteaseCloudMusicApi project. The author's code has a good sense of design and is recommended to read.
  fs.readdirSync(path.join(__dirname, 'commands')).forEach((file: string) = > {
    if(! file.endsWith('.js')) return;
    require(path.join(__dirname, 'commands', file));
  });
  // ...
};
Copy the code
  1. Finally, incommandsCreate a new one in the directorycreate.tsFile writing command
import { program } from 'commander';
program
  .command('create <name> [loc]')
  .description('Create a new vant-react-native package')
  .action((name,loc) = > {
    console.log('Hello Luozhu');
  })
Copy the code

Scaffold implementation

In the previous section, we initialized the CLI and added the create command. In this section, we implement the scaffolding function.

We first create the component template in the Packages /scripts directory

. ├ ─ ─ the README. TPL. MdThe # TPL suffix is automatically removed when component templates are generated by handlebars-template-compiler.
├── package.tpl.json
├── src
│   └── index.ts # will not compile without the TPL suffix, which can save time if the template is large.└ ─ ─ tsconfig. JsonCopy the code

Then we define the data structure of our template metadata. My data structure here is:

interface IMeta {
  name: string;
  version: string;
  description: string;
  author: string;
  email: string;
  url: string;
  directory: string;
}
Copy the code

With the data structure, we can use the Inquirer module to guide the user to enter information.

import inquirer from 'inquirer';
// ...
// The getQuestions are too long, interested students can check: http://tny.im/UFbg
const answer: IMeta = await inquirer.prompt(getQuestions(name));
// ...
Copy the code

Next, we use the tmp-Promise module to create a system temporary folder and copy the contents of the template folder mentioned above into it:

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);
Copy the code

Finally, we compile the contents of the temporary folder and copy it to the specified location:

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

Let’s take a look at the results:

Github CODEOWENERS

The hardest thing about a large open source project is not the technology, and there is no shortage of technology giants. The hardest part is collaboration and maintenance. If you think about a project with hundreds or thousands of people and there’s a new PR, there’s no way normal people can quickly figure out who needs to review the code. Because our Vant-React-Native sends each component separately for maintenance, this problem will also arise when there are too many participants.

GitHub CODEOWNERS are there to solve this problem and can be seen on the DefinitelyTyped project with 5000+ contributors. The code owner is officially defined as follows:

You can use the CODEOWNERS file to define the individual or team responsible for the repository code. When someone modifies the code and opens a pull request, the code owner is automatically asked to review it.

The CODEOWNERS file uses a model that follows most of the rules used in the Gitignore file, and the CODEOWNERS file is typically located in the.github/ directory.

In Vant-React-Native, Luo Zhu is the final person in charge of the warehouse, so it is expected that each PR can be assigned to him for review. So let’s try this out. Create a.github/CODEOWNERS file and write the following:

# 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
Copy the code

Typically, if the file has a code owner, you can see who the code owner is before you open the pull request. In the repository, you can find the file and hover over a lock icon, which tells you who owns the file:

Then we submit a PR to see what happens:

Automatic NPM package sending

Generally, only the owner of the warehouse has the permission to send the package, but the owner maintains several NPM accounts at the same time, or the owner is suddenly busy and gives the release permission to other administrators, but it is inconvenient to inform the NPM account. The answer is to CD NPM packages (continuous deployment), which companies typically do based on Gitlab or their own built platforms. As an open source project, we use GitHub Actions, of course.

For normal single-package projects, using nPM-publish or nPm-publish-action as two GitHub actions doesn’t make sense. However, there is no ready-made plug-in for lerna based multi-package unit warehouse. As usual, let’s take a look at our own implementation steps:

  1. Determine whether the commit message ischore(release):At the beginning

    Implemented with GitHub Action startsWith(GitHub.event.head_commit. Message, ‘chore(Release):’)

  2. Log in through the NPM Publish token authentication

    Authenticated by NPM config set //registry.npmjs.org/:_authToken=${{secrets.npm_token}}

  3. performlerna publish from-package --yesrelease

    You need to run the lerna version series command locally to upgrade the version

The full GitHub Action is implemented as follows:

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 # Cache node_modules to speed up build
      - 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: The ${{ secrets.NPM_TOKEN }}
      - name: Publish package
        run: lerna publish from-package --yes
Copy the code

To get timely notification after publication, Luo zhu used the Peter-Evans/commit-Comment plug-in to comment on the corresponding commit after a failed or successful publication, so we could receive email and in-site notification.

- name: Create commit comment after publish successfully
  if: The ${{ 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: The ${{ 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
Copy the code

Thank you

As of press time, every front end deserves its own component library, just like having watermelon every summer. 🍉 has gained nearly 1600 likes and over 40,000 reads 📖. Thanks again for digg’s support, editor Zoe’s encouragement, Moon Film Master’s reprint, friends’ forwarding and my own insistence.

The recent oliver

  • Every front end deserves its own component library, like having watermelon every summer 🍉
  • Lerna – based multi-package JavaScript project setup and maintenance
  • Conventional Commits
  • Best Node.js framework to use in 2021
  • React Interview series
  • The Go Language tutorial series

This article was first published in the “Gold Column”, and was synchronized with the public account “Program Life” and the “official website of Luozhu”.