1. Install the component library
Executing installation Commands
$ npx create-react-app echo-rui --typescript
Copy the code
2. Component library configuration ESLint
- Configure ESlint new
.eslintrc.json
file
{
"extends": "react-app"
}
Copy the code
Create a.vscode/settings.json file
{
"eslint.validate": [
"javascript"."javascriptreact",
{ "language": "typescript"."autoFix": true },
{ "language": "typescriptreact"."autoFix": true}}]Copy the code
3. Introduce dependencies
Use className in the component: github.com/jedWatson/c… Executing installation Commands
$ npm install classnames -D
$ npm install @types/classnames -D
$ npm install node-sass -D
Copy the code
[BTN -${btnType}] [BTN -${btnType}] [BTN -${btnType}]
// btn, btn-lg, btn-primary
const classes = classNames('btn', className, {
[`btn-${btnType}`]: btnType,
[`btn-${size}`]: size,
'disabled': (btnType === 'link') && disabled
})
Copy the code
4. Write components
- new
src/components/Button/button.tsx
import React,{FC,ButtonHTMLAttributes,AnchorHTMLAttributes} from "react";
import classnames from "classnames"; // Button sizeexport type ButtonSize = "lg" | "sm";
export type ButtonType = "primary" | "default" | "danger" | "link"; Interface BaseButtonProps {/** customizing className */ className? : string; /** Disable Button */ disabled? : boolean; /** Set Button size */ size? : ButtonSize; /** Set Button type */ btnType? : ButtonType; children: React.ReactNode; /** Required when btnType is link */ href? : string; } / / and settype NativeButtonProps = BaseButtonProps &
ButtonHTMLAttributes<HTMLElement>;
typeAnchorButtonProps = BaseButtonProps & AnchorHTMLAttributes<HTMLElement>; // Partial: typescript global functions that make all attributes optionalexport typeButtonProps = Partial<NativeButtonProps & AnchorButtonProps>; // React-docgen-typescript loader can only use FC, not react. FCexportconst Button: FC<ButtonProps> = (props) => { const { disabled, size, btnType, children, href, className, ... resetProps } = props; const classes = classnames("echo-btn", className, {
[`echo-btn-${btnType}`]: btnType,
[`echo-btn-${size}`]: size,
"echo-button-disabled": btnType === "link" && disabled,
});
if (btnType === "link" && href) {
return( <a href={href} className={classes} {... resetProps}> {children} </a> ); }else {
return (
<button className={classes} disabled={disabled} {...resetProps}>
{children}
</button>
);
}
};
Button.defaultProps = {
disabled: false,
btnType: "default"};export default Button;
Copy the code
- new
src/components/Button/index.tsx
import Button from "./button";
export default Button;
Copy the code
- new
src/components/Button/_style.scss
@import ".. /.. /styles/variables/button";
@import ".. /.. /styles/mixin/button";
.echo-btn {
position: relative;
display: inline-block;
font-weight: $btn-font-weight;
line-height: $btn-line-height;
color: $body-color;
white-space: nowrap;
text-align: center;
vertical-align: middle;
background-image: none;
border: $btn-border-width solid transparent;
@include button-size(
$btn-padding-y,
$btn-padding-x,
$btn-font-size,
$btn-border-radius
);
box-shadow: $btn-box-shadow;
cursor: pointer;
transition: $btn-transition;
&.echo-button-disabled
&[disabled] {
cursor: not-allowed;
opacity: $btn-disabled-opacity;
box-shadow: none;
> * {
pointer-events: none;
}
}
}
.echo-btn-lg {
@include button-size(
$btn-padding-y-lg,
$btn-padding-x-lg,
$btn-font-size-lg,
$btn-border-radius-lg
);
}
.echo-btn-sm {
@include button-size(
$btn-padding-y-sm,
$btn-padding-x-sm,
$btn-font-size-sm,
$btn-border-radius-sm
);
}
.echo-btn-primary {
@include button-style($primary.$primary.$white);
}
.echo-btn-danger {
@include button-style($danger.$danger.$white);
}
.echo-btn-default {
@include button-style(
$white.$gray- 400.$body-color,
$white.$primary.$primary
);
}
.echo-btn-link {
font-weight: $font-weight-normal;
color: $btn-link-color;
text-decoration: $link-decoration;
box-shadow: none;
&:hover {
color: $btn-link-hover-color;
text-decoration: $link-hover-decoration;
}
&:focus {
text-decoration: $link-hover-decoration;
box-shadow: none;
}
&:disabled,
&.echo-button-disabled {
color: $btn-link-disabled-color; pointer-events: none; }}Copy the code
- new
styles/variables/button.scss
file
@import "./common"; // Basic button properties$btn-font-weight: 400;
$btn- padding - y: 0.375 rem! default;$btn- padding - x: 0.75 rem! default;$btn-font-family: $font-family-base ! default;$btn-font-size: $font-size-base ! default;$btn-line-height: $line-height-base ! default; . .Copy the code
- Create a styles/mixin/button.scss ‘file
@mixin button-size($padding-y, $padding-x, $font-size, $border-raduis) {
padding: $padding-y $padding-x;
font-size: $font-size;
border-radius: $border-raduis;
}
@mixin button-style(
$background.$border.$color// lghTEN,sass built-in function, than$backgroundThe color should be 7.5% lighter$hover-background: lighten($background, 7.5%),
$hover-border: lighten($border, 10%),
$hover-color: $color)... .Copy the code
- in
src/styles/index.scss
Introduce component styles into the file
// The index file mainly introduces styles for all components. // No need to write _, which is a way of writing sass to tell sass that these styles are not packaged into CSS, but can only be imported, which is also a modular // button style @import".. /components/Button/style";
Copy the code
- new
src/styles/index.scss
file
// The index file mainly introduces styles for all components. // No need to write _, which is a way of writing sass to tell sass that these styles are not packaged into CSS, but can only be imported, which is also a modular // button style @import".. /components/Button/style"; .Copy the code
5. Delete unnecessary files and reference components
- delete
src/App.css
+src/logo.svg
+src/index.css
+src/App.test.js
+serviceWorker.ts
file - Modify the
App.tsx
file
import React from "react";
import "./styles/index.scss";
import Button, { ButtonType, ButtonSize } from "./components/Button/button";
function App() {
return (
<div className="App">
<Button>hello</Button>
<Button disabled>hello</Button>
<Button btnType="primary" size="sm">
hello
</Button>
<Button
btnType="danger"
size="lg"
onClick={() => {
alert(111);
}}
>
hello
</Button>
<Button
btnType="link"
href="http://www.baidu.com"
target="_blank"
>
hello
</Button>
<Button disabled btnType="link" href="http://www.baidu.com">
hello
</Button>
</div>
);
}
export default App;
Copy the code
- Modify the
src/index.tsx
file
export { default as Button } from "./components/Button";
Copy the code
6. Run the project
Execute the command
$ npm start
Copy the code
Visit the project and see that the Button component is successful!
# # 7. Unit testing new SRC/Button/Button. The test. The TSX composite file
import React from "react";
import { render, fireEvent } from "@testing-library/react";
import Button, { ButtonProps } from "./button";
describe("Button component", () => {
it('the default Button', () => {
const testProps: ButtonProps = { onClick: jest.fn(), } const wrapper = render(<Button {... testProps}>hello</Button>); const element = wrapper.getByText('hello')as HTMLButtonElement; // If the element is rendered in the document expect(element).tobeinthedocument (); // Expect (element.tagname).toequal ("BUTTON"); // Check if there is a class name expect(element).tohaveclass ("echo-btn-default");
expect(element).not.toHaveClass("echo-disabled"); // Trigger the click event fireEvent.click(element); expect(testProps.onClick).toHaveBeenCalled();
expect(element.disabled).toBeFalsy();
})
it("Test for passing in different attributes", () => {
const testProps: ButtonProps = {
btnType: "primary",
size: "lg",
className: "test-name"}; const wrapper = render(<Button {... testProps}>hello</Button>); const element = wrapper.getByText("hello") as HTMLButtonElement;
expect(element).toBeInTheDocument();
expect(element).toHaveClass("echo-btn-primary");
expect(element).toHaveClass("echo-btn-lg");
expect(element).toHaveClass("test-name");
});
it("Test the presence of btnType link and href.", () => {
const testProps: ButtonProps = {
btnType: "link",
href: "http://www.baidu.com"}; const wrapper = render(<Button {... testProps}>Link</Button>); const element = wrapper.getByText("Link") as HTMLAnchorElement;
expect(element).toBeInTheDocument();
expect(element.tagName).toEqual("A");
expect(element).toHaveClass("echo-btn-link");
});
it("Test disabled condition", () => {
const testProps: ButtonProps = {
onClick: jest.fn(),
disabled: true}; const wrapper = render(<Button {... testProps}>Disabled</Button>); const element = wrapper.getByText("Disabled") as HTMLButtonElement;
expect(element).toBeInTheDocument();
expect(element.disabled).toBeTruthy();
fireEvent.click(element);
expect(testProps.onClick).not.toHaveBeenCalled();
});
});
Copy the code
Execute the command
$ npm test
Copy the code
You can see that the unit test passed successfully!
##8. Component libraries are loaded on demand
- Install dependencies
$npm install node-cmd -D
Copy the code
- new
buildScss.js
file
const cmd = require("node-cmd");
const path = require("path");
const fs = require("fs");
const entryDir = path.resolve(__dirname, "./src/components");
const outputDir = path.resolve(__dirname, "./dist/components");
function getScssEntry() {
let entryMap = {};
fs.readdirSync(entryDir).forEach(function (pathName) {
const entryName = path.resolve(entryDir, pathName);
const outputName = path.resolve(outputDir, pathName);
let entryFileName = path.resolve(entryName, "_style.scss");
let outputFileName = path.resolve(outputName, "style/index.css");
entryMap[pathName] = {};
entryMap[pathName].entry = entryFileName;
entryMap[pathName].output = outputFileName;
});
return entryMap;
}
const entry = getScssEntry();
let buildArr = [];
for (const key in entry) {
const promise = new Promise((resolve, reject) => {
cmd.get(`npx node-sass ${entry[key].entry} ${entry[key].output}`, function (
err,
data,
stderr
) {
if (err) {
reject(err);
return;
}
console.log("the current working dir is : ", data);
fs.writeFileSync(
path.join(__dirname, `./dist/components/${key}/style/css.js`),
"import './index.css'"
);
resolve();
});
});
buildArr.push(promise);
}
Promise.all(buildArr)
.then(() => {
console.log("build success");
})
.catch((e) => {
console.log(e);
});
Copy the code
- new
_babel.config.js
Dist /components style: echo-rui (); echo-rui (); echo-rui (); echo-rui (); The CSS type is loaded. The current project only has CSS, not less, so write CSS here. If you need to configure LESS, see comments
module.exports = {
presets: ["react-app"],
plugins: [
[
"import",
{
libraryName: "echo-rui",
camel2DashComponentName: falseTurn, whether you need / / hump short-term camel2UnderlineComponentName:false, // Do you want the camel to underline libraryDirectory:"dist/components",
style: "css"},], // ["import"{/ /"libraryName": "antd", / /"libraryDirectory": "es", / /"style": "css" // `style: true//}]],};Copy the code
⚠️⚠️⚠️ Important Note: During development, there must be a _style. SCSS style file under each component directory, even if it is an empty file. Otherwise, an error will be reported when importing on demand
##9. Storybook document generation
- Initialize the storyBook
$ npx -p @storybook/cli sb init
Copy the code
- Add dependencies and plug-ins
$ npm install @storybook/addon-info --save-dev
Copy the code
- Add the NPM script
"scripts": {..."storybook": "start-storybook -p 9009 -s public"."build-storybook": "build-storybook -s public"
},
Copy the code
- Configure storybook to support typescript
$ npm install react-docgen-typescript-loader -D
Copy the code
- Add the storybook configuration file
- new
.storybook/webpack.config.js
fileshouldExtractLiteralValuesFromEnum
The: storybook automatically expands properties of type type when it crawls component properties.propFilter
: Filters out the source of attributes that do not need to be climbed.
module.exports = ({ config }) => {
config.module.rules.push({
test: /\.tsx? $/, use: [ { loader: require.resolve("babel-loader"),
options: {
presets: [require.resolve("babel-preset-react-app")]
}
},
{
loader: require.resolve("react-docgen-typescript-loader"),
options: {
shouldExtractLiteralValuesFromEnum: true,
propFilter: (prop) => {
if (prop.parent) {
return! prop.parent.fileName.includes('node_modules')}return true}}}]}); config.resolve.extensions.push(".ts".".tsx");
return config;
};
Copy the code
- new
.storybook/style.scss
file
// This file is mainly the configuration of the storyBook document styleCopy the code
- new
.storybook/config.tsx
File configuration plug-in and the files that need to be loaded
import { configure,addDecorator,addParameters } from '@storybook/react';
import { withInfo } from '@storybook/addon-info'
import '.. /src/styles/index.scss'
import './style.scss'
import React from 'react'
const wrapperStyle: React.CSSProperties = {
padding: '20px 40px'} const storyWrapper = (stroyFn: Any) => (<div style={wrapperStyle}> <h3> Component demo </h3> {stroyFn()} </div>) addDecorator(storyWrapper) addDecorator(withInfo) addParameters({info: { inline:true, header: false}})
const loaderFn = () => {
const allExports = [];
const req = require.context('.. /src/components'.true, /\.stories\.tsx$/);
req.keys().forEach(fname => allExports.push(req(fname)));
return allExports;
};
configure(loaderFn, module);
Copy the code
- Create a new one under each component directory
.stories.tsx
Closing document
- New under button component
button.stories.tsx
file
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import Button from './button'
const defaultButton = () => (
<div>
<Button onClick={action('default button')}>default button</Button>
</div>
)
const buttonWithSize = () => (
<div>
<Button size='lg' btnType='primary' onClick={action('lg button')}>lg button</Button>
<Button className='ml-20' size='sm' btnType='danger' onClick={action('sm button')}>sm button</Button>
</div>
)
const buttonWithType = () => (
<div>
<Button onClick={action('danger button')} btnType='danger'>danger button</Button>
<Button onClick={action('primary button')} className='ml-20' btnType='primary'>primary button</Button>
<Button onClick={action('link')} className='ml-20' btnType='link' href='https://www.baidu.com/'>link</Button>
</div>
)
const buttonWithDisabled = () => (
<div>
<Button onClick={action('disabled button')} btnType='danger' disabled={true}>disabled button</Button>
<Button onClick={action('unDisabled button')} className='ml-20' btnType='primary'>unDisabled button</Button>
</div>
)
// storiesOf('Button component'. The module) / /. AddDecorator (withInfo) / /. AddParameters ({/ / info: {/ / text: ` / / / / ~ ~ ~ this is the default component js / / const a = 12 / / ~ ~ ~ / / `, // inline:true
// }
// })
// .add('the default Button', defaultButton)
// .add('Different size buttons', buttonWithSize,{info:{inline:false}})
// .add('Different types of buttons', buttonWithType)
storiesOf('Button Button', module)
.addParameters({
info: {
text: `
## Reference method
~~~js
import {Button} from ecoh-rui
~~~
`
}
})
.add('the default Button', defaultButton)
.add('Different size buttons', buttonWithSize)
.add('Different types of buttons', buttonWithType)
.add('Disabled Button',buttonWithDisabled)
Copy the code
7. Run the command
$ npm run storybook
Copy the code
You can see it on the terminal
http://localhost:9009/
10. Typescript compilation configuration
Create a new tsconfig.build.json file
{
"compilerOptions": {// Output path"outDir": "dist"// Package module specifications"module": "esnext"// Build the target"target": "es5"// Generate the definition file d.ts"declaration": true."jsx": "react"// The module introduces the policy"moduleResolution": "Node"// allow import React from'react'This guide package"allowSyntheticDefaultImports": true}, // The directory to compile"include": ["src"], // No need to compile"exclude": ["src/**/*.test.tsx"."src/**/*.stories.tsx"."src/setupTests.ts"]}Copy the code
11. Package. json Configuration
- Move the dependency package from Dependencies to devDependencie
Why would you do that? React and react-dom are also used during development, so we cannot delete the react and react-dom. Other dependencies that are not related to the published component library, such as Storybook, need to be added to devDependencies
// Run NPM start and NPM runtestAnd NPM Run Storybook find everything ok, have fun!!"dependencies": {},
Copy the code
- Added configuration related to NPM publishing
"description": "react components library"."author": "echo"."private": false// Main entrance"main": "dist/index.js"// Module entry"module": "dist/index.js"// Type file declaration"types": "dist/index.d.ts"."license": "MIT", // Keywords"keywords": [
"React"."UI"."Component"."typescript"], // Home your github address"homepage": "https://github.com/... /echo-rui"// Warehouse address"repository": {
"type": "git"."url": "https://github.com/... /echo-ruii"}, // If the file needs to be uploaded, the default is. Gitignore"files": [
"dist"].Copy the code
- Check your code before submitting it to Git. Here we use the husky tool
"husky": {
"hooks"Before submitting code with git"pre-commit": "npm run test:nowatch && npm run lint"}},"scripts": {..."lint": "eslint --ext js,ts,tsx src --fix --max-warnings 5"."test:nowatch": "cross-env CI=true react-scripts test".Copy the code
- Add packaged publishing configuration
"scripts": {..."clean": "rimraf ./dist"."build-ts": "tsc -p tsconfig.build.json"."build-css": "node-sass ./src/styles/index.scss ./dist/index.css"."build": "npm run clean && npm run build-ts && npm run build-css && node ./buildScss.js"."prepublishOnly": "npm run lint && npm run build".Copy the code
After you run the package command, the package is successfully packaged according to the configuration.
12. Publish to NPM
All we need is a launch.
- Complete the readme.md document. Just write two sentences about it
- Create a new.npmignore file in the root directory with the same content as.gitignore:
Copy the code
Finally, execute NPM login to login to NPM account, and then execute NPM publish publish. Of course, the premise is that you have a NPM account, no words to register a bar, very easy, and then search your NPM package name whether someone uses, if there is a change.