執行安裝命令javascript
$ npx create-react-app echo-rui --typescript
複製代碼
.eslintrc.json
文件{
"extends": "react-app"
}
複製代碼
新建 .vscode/settings.json
文件css
{
"eslint.validate": [
"javascript",
"javascriptreact",
{ "language": "typescript", "autoFix": true },
{ "language": "typescriptreact", "autoFix": true }
]
}
複製代碼
在組件中使用 classname
:github.com/jedWatson/c… 執行安裝命令java
$ npm install classnames -D
$ npm install @types/classnames -D
$ npm install node-sass -D
複製代碼
使用方法示例: 若是對象的key值是變化的,能夠採用下面的中括號的形式:[btn-${btnType}
]node
// btn, btn-lg, btn-primary
const classes = classNames('btn', className, {
[`btn-${btnType}`]: btnType,
[`btn-${size}`]: size,
'disabled': (btnType === 'link') && disabled
})
複製代碼
src/components/Button/button.tsx
import React,{FC,ButtonHTMLAttributes,AnchorHTMLAttributes} from "react";
import classnames from "classnames";
// 按鈕大小
export type ButtonSize = "lg" | "sm";
export type ButtonType = "primary" | "default" | "danger" | "link";
interface BaseButtonProps {
/** 自定義類名 */
className?: string;
/** 設置Button 的禁用 */
disabled?: boolean;
/** 設置Button 的大小 */
size?: ButtonSize;
/** 設置Button 的類型 */
btnType?: ButtonType;
children: React.ReactNode;
/** 當btnType爲link時,必填 */
href?: string;
}
// 並集
type NativeButtonProps = BaseButtonProps &
ButtonHTMLAttributes<HTMLElement>;
type AnchorButtonProps = BaseButtonProps &
AnchorHTMLAttributes<HTMLElement>;
// Partial:typescript全局函數,將屬性所有變成可選的
export type ButtonProps = Partial<NativeButtonProps & AnchorButtonProps>;
// 使用react-docgen-typescript-loader的bug,只能使用FC,不能React.FC
export const 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;
複製代碼
src/components/Button/index.tsx
import Button from "./button";
export default Button;
複製代碼
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;
}
}
複製代碼
styles/variables/button.scss
文件@import "./common";
// 按鈕基本屬性
$btn-font-weight: 400;
$btn-padding-y: 0.375rem !default;
$btn-padding-x: 0.75rem !default;
$btn-font-family: $font-family-base !default;
$btn-font-size: $font-size-base !default;
$btn-line-height: $line-height-base !default;
...
...
複製代碼
@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內置函數,比$background顏色要淺上7.5%
$hover-background: lighten($background, 7.5%),
$hover-border: lighten($border, 10%),
$hover-color: $color
)
...
...
複製代碼
src/styles/index.scss
文件中引入組件樣式// index文件主要是引入全部組件的樣式。
// 不須要寫_,這是sass的一種寫法,告訴sass這些樣式不打包到css中,只能作導入,也是一種模塊化
// 按鈕樣式
@import "../components/Button/style";
複製代碼
src/styles/index.scss
文件// index文件主要是引入全部組件的樣式。
// 不須要寫_,這是sass的一種寫法,告訴sass這些樣式不打包到css中,只能作導入,也是一種模塊化
// 按鈕樣式
@import "../components/Button/style";
...
複製代碼
src/App.css
+ src/logo.svg
+ src/index.css
+ src/App.test.js
+ serviceWorker.ts
文件App.tsx
文件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;
複製代碼
src/index.tsx
文件export { default as Button } from "./components/Button";
複製代碼
執行命令react
$ npm start
複製代碼
訪問項目 能夠看到button組件成功了! webpack
##7.單元測試 新建src/Button/button.test.tsx
文件git
import React from "react";
import { render, fireEvent } from "@testing-library/react";
import Button, { ButtonProps } from "./button";
describe("Button 組件", () => {
it('默認Button', () => {
const testProps: ButtonProps = {
onClick: jest.fn(),
}
const wrapper = render(<Button {...testProps}>hello</Button>);
const element = wrapper.getByText('hello')as HTMLButtonElement;
// 元素是否被渲染在文檔中
expect(element).toBeInTheDocument();
// 判斷標籤名
expect(element.tagName).toEqual("BUTTON");
// 判斷是否有類名
expect(element).toHaveClass("echo-btn-default");
expect(element).not.toHaveClass("echo-disabled");
// 觸發點擊事件
fireEvent.click(element);
expect(testProps.onClick).toHaveBeenCalled();
expect(element.disabled).toBeFalsy();
})
it("測試傳入不一樣屬性的狀況", () => {
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("測試當btnType爲link和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("測試禁用的狀況", () => {
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();
});
});
複製代碼
執行命令github
$ npm test
複製代碼
能夠看到單元測試成功經過! web
##8.組件庫實現按需加載typescript
$npm install node-cmd -D
複製代碼
buildScss.js
文件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);
});
複製代碼
_babel.config.js
文件 libraryName:須要加載的庫,我這裏的是echo-rui libraryDirectory:須要加載的組件庫所在的目錄,當前的組件是存放在dist/components下的 style:加載的css類型,當前項目只有css,沒有less,因此這裏寫css便可,如需配置less,請看註釋module.exports = {
presets: ["react-app"],
plugins: [
[
"import",
{
libraryName: "echo-rui",
camel2DashComponentName: false, // 是否須要駝峯轉短線
camel2UnderlineComponentName: false, // 是否須要駝峯轉下劃線
libraryDirectory: "dist/components",
style: "css",
},
],
// ["import", {
// "libraryName": "antd",
// "libraryDirectory": "es",
// "style": "css" // `style: true` 會加載 less 文件
// }]
],
};
複製代碼
⚠️⚠️⚠️重要說明:在開發的時候,每一個組件目錄下面必須有一個_style.scss
樣式文件,即便他是個空文件也必須有,不然會在按需引入的時候報錯找不到css文件⚠️⚠️
##9.storybook文檔生成
$ npx -p @storybook/cli sb init
複製代碼
$ npm install @storybook/addon-info --save-dev
複製代碼
"scripts": {
...
"storybook": "start-storybook -p 9009 -s public",
"build-storybook": "build-storybook -s public"
},
複製代碼
$ npm install react-docgen-typescript-loader -D
複製代碼
.storybook/webpack.config.js
文件 shouldExtractLiteralValuesFromEnum
:storybook爬取組件屬性的時候會自動把type類型的屬性自動展開。 propFilter
:過濾掉不須要爬取的屬性的來源。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;
};
複製代碼
.storybook/style.scss
文件// 這個文件主要是對storybook文檔樣式的配置
複製代碼
.storybook/config.tsx
文件 配置插件以及須要加載的文件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>組件演示</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);
複製代碼
.stories.tsx
結尾的文件button.stories.tsx
文件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組件', module)
// .addDecorator(withInfo)
// .addParameters({
// info:{
// text:`
// 這是默認組件
// ~~~js
// const a = 12
// ~~~
// `,
// inline:true
// }
// })
// .add('默認 Button', defaultButton)
// .add('不一樣尺寸 Button', buttonWithSize,{info:{inline:false}})
// .add('不一樣類型 Button', buttonWithType)
storiesOf('Button 按鈕', module)
.addParameters({
info: {
text: `
## 引用方法
~~~js
import {Button} from ecoh-rui
~~~
`
}
})
.add('默認 Button', defaultButton)
.add('不一樣尺寸 Button', buttonWithSize)
.add('不一樣類型 Button', buttonWithType)
.add('禁用的 Button',buttonWithDisabled)
複製代碼
7.執行命令
$ npm run storybook
複製代碼
在終端能夠看到
瀏覽器打開http://localhost:9009/
,能夠看到組件庫文檔生成了。
新建tsconfig.build.json
文件
{
"compilerOptions": {
// 輸出路徑
"outDir": "dist",
// 打包模塊規範
"module": "esnext",
// 構建目標
"target": "es5",
// 生成定義文件d.ts
"declaration": true,
"jsx": "react",
// 模塊引入策略
"moduleResolution": "Node",
// 容許import React from 'react'這樣導包
"allowSyntheticDefaultImports": true
},
// 須要編譯的目錄
"include": ["src"],
// 不須要編譯的
"exclude": ["src/**/*.test.tsx", "src/**/*.stories.tsx", "src/setupTests.ts"]
}
複製代碼
爲何要這麼作? 1、是防止發佈組件庫以後別人使用了跟咱們不同的react版本形成衝突 2、是咱們在開發的時候還須要使用到react和react-dom,因此不能刪除,只能搬到devDependencies 3、還有一些跟發佈後的組件庫不相關的依賴都須要搬到devDependencies,例如storybook等
// 嘿嘿,移動以後,發現爲空了,從新install 了一下
// 運行了npm start 和npm run test 和npm run storybook發現一切正常,開心一下!!
"dependencies": {},
複製代碼
"description": "react components library",
"author": "echo",
"private": false,
// 主入口
"main": "dist/index.js",
// 模塊入口
"module": "dist/index.js",
// 類型文件聲明
"types": "dist/index.d.ts",
"license": "MIT",
// 關鍵詞
"keywords": [
"React",
"UI",
"Component",
"typescript"
],
// 首頁你的github地址
"homepage": "https://github.com/.../echo-rui",
// 倉庫地址
"repository": {
"type": "git",
"url": "https://github.com/.../echo-ruii"
},
// 須要上傳的文件,不寫就默認以.gitignore爲準
"files": [
"dist"
],
複製代碼
"husky": {
"hooks": {
// 用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",
...
複製代碼
"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"
...
複製代碼
此時執行打包命令,就能夠成功根據配置打包了。
萬事俱備,只欠發佈。
複製代碼
最後執行 npm login 登入 npm 帳號,再執行 npm publish 發佈便可,就這麼簡單的兩步就能夠,過一會在 npm 上就能搜到了。固然前提是你有個 npm 帳號,沒有的話去註冊一個吧,很 easy 的,而後還要搜下你的 npm 包名是否有人用,有的話就換一個。