前言
简简单单用最新的框架实战一个NPM发布React组件流程。
创建项目
创建一个空项目,信息根据自己的需求填写。
npm init
1.目录结构
添加一些目录和文件,以下为我的目录结构:
├── config # webpack配置
│ └── webpack.base.js # 公共配置
│ └── webpack.dev.config.js # 开发环境
│ └── webpack.prod.config.js # 生产环境
├── lib # 组件打包目录
├── demo # demo目录
│ └── index.js
│ └── index.html
├── src # 组件源码目录
│ └── index.d.ts
│ └── index.tsx
│ └── index.scss
└── .babelrc # babel配置文件
└── .gitignore # git提交时需要忽略的文件和文件夹
└── .npmignore # npm发布时需要忽略的文件和文件夹
└── LICENSE # 许可证
└── package-lock.json
└── package.json
└── README.md # 文档
└── tsconfig.json # ts配置文件
└── yarn.lock
2.安装依赖与配置
1.Webpack依赖
安装如下依赖包
yarn add webpack webpack-cli webpack-dev-server webpack-merge --dev
// webpack-dev-server是用于开发调试的
// webpack-merge是用于合并webpack配置的
2.支持解析和打包css、scss
安装如下依赖包
yarn add postcss postcss-loader postcss-preset-env style-loader css-loader sass-loader node-sass mini-css-extract-plugin --dev
// postcss是用来转换css的
// mini-css-extract-plugin 是用来将css单独打包的
3.babel相关
安装如下依赖包
yarn add @babel/cli @babel/core @babel/preset-env @babel/preset-react --dev
4.TS支持
安装如下依赖包
yarn add @types/react @types/react-dom ts-loader @babel/preset-typescript --dev
5.配置文件修改
在config/webpack.base.js文件里添加如下配置:
module.exports = {
resolve: {
// 定义 import 引用时可省略的文件后缀名
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
module: {
rules: [
{
// 编译处理 js 和 jsx 文件
test: /(\.js(x?))|(\.ts(x?))$/,
use: [
{
loader: 'babel-loader'
}
],
exclude: /node_modules/, // 只解析 src 目录下的文件
}
]
},
};
在config/webpack.dev.config.js文件里添加如下配置:
const path = require('path');
const { merge } = require('webpack-merge');
const baseConfig = require('./webpack.base.js'); // 公共配置
const devConfig = {
mode: 'development', // 开发模式
entry: path.join(__dirname, "../demo/src/index.tsx"), // 入口,处理资源文件的依赖关系
output: {
path: path.join(__dirname, "../demo/src/"),
filename: "dev.js",
},
module: {
rules: [
{
test: /\.s[ac]ss$/,
exclude: /\.min\.css$/,
use: [
{ loader: 'style-loader' },
{
loader: 'css-loader',
options: {
modules: {
mode: "global"
}
}
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
[
'postcss-preset-env',
{
// 其他选项
},
],
],
},
},
},
{ loader: 'sass-loader' }
]
},
{
test: /\.min\.css$/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' }
]
}
]
},
devServer: {
static: path.join(__dirname, '../demo/src/'),
compress: true,
host: '127.0.0.1',
port: 8686, // 启动端口
open: true // 打开浏览器
},
};
module.exports = merge(devConfig, baseConfig); // 合并配置
在config/webpack.prod.config.js文件里添加如下配置:
const path = require('path');
const { merge } = require('webpack-merge');
const baseConfig = require('./webpack.base.js'); // 引用公共的配置
const MiniCssExtractPlugin = require("mini-css-extract-plugin"); // 用于将组件的css打包成单独的文件输出到`lib`目录中
const prodConfig = {
mode: 'production', // 生产模式
entry: path.join(__dirname, "../src/index.tsx"),
output: {
path: path.join(__dirname, "../lib/"),
filename: "index.js",
libraryTarget: 'umd', // 采用通用模块定义
libraryExport: 'default', // 兼容 ES6 Module、CommonJS 和 AMD 模块规范
},
module: {
rules: [
{
test: /\.s[ac]ss$/,
exclude: /\.min\.css$/,
use: [
{ loader: MiniCssExtractPlugin.loader },
{
loader: 'css-loader',
options: {
modules: {
mode: "global"
}
}
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
[
'postcss-preset-env',
{
// 其他选项
},
],
],
},
},
},
{ loader: 'sass-loader' }
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: "main.min.css" // 提取后的css的文件名
})
],
externals: { // 定义外部依赖,避免把react和react-dom打包进去
react: {
root: "React",
commonjs2: "react",
commonjs: "react",
amd: "react"
},
"react-dom": {
root: "ReactDOM",
commonjs2: "react-dom",
commonjs: "react-dom",
amd: "react-dom"
}
},
};
module.exports = merge(prodConfig, baseConfig); // 合并配置
在.babelrc中添加如下配置:
{
"presets": [
"@babel/preset-env",
"@babel/preset-react",
"@babel/preset-typescript"
]
}
在.ignore中添加如下配置:
# 指定发布 npm 的时候需要忽略的文件和文件夹
# npm 默认不会把 node_modules 发上去
demo/
config/
src/
static/
.babelrc
.gitignore
package-lock.json
tsconfig.json
yarn.lock
修改package.json文件,添加如下script:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "webpack-dev-server --config config/webpack.dev.config.js",
"build": "webpack --config config/webpack.prod.config.js"
},
至此,配置部分已经完成,下面就开始开发组件逻辑。
开发组件
1.编写逻辑
在src/index.tsx中添加分页组件逻辑:
import React, { useEffect, useState } from "react";
import './index.scss';
interface PegationProps {
currentPage: number;
pageSize: number;
pageSizeOptions?: Array<number>;
total: number;
totalText?: string;
handleChangePage: (v: number) => void;
handleChangePageSize: (v: number) => void;
}
function Pagenation(props: PegationProps) {
const {
currentPage,
pageSize,
pageSizeOptions,
total,
totalText,
handleChangePage,
handleChangePageSize
} = props;
const count = Math.ceil(total / pageSize);
const [pageArr, setPageArr] = useState<(number | string)[]>([]);
useEffect(() => {
pageChange();
}, [currentPage, pageSize, pageSizeOptions, total])
const pageChange = () => {
let newArr = [];
let c = 1;
if (count <= 6) {
while (c <= count) {
newArr.push(c);
c++;
}
} else if (count === 7) {
if (currentPage <= 4) {
newArr = [1, 2, 3, 4, 5, '···', 7];
} else {
newArr = [1, '···', 3, 4, 5, 6, 7];
}
} else {
if (currentPage <= 3) {
newArr = [1, 2, 3, 4, 5, '···', count];
} else if (currentPage >= count - 2) {
newArr = [1, '···', count - 4, count - 3, count - 2, count - 1, count];
} else {
newArr = [1, '···', currentPage - 1, currentPage, currentPage + 1, '···', count]
}
}
setPageArr(newArr);
}
const makePageSizeOptions = () => {
if (pageSizeOptions) {
return (
<select name="pageSize" className="page-size" id="react-hook-pagenation-page-size" value={pageSize} onChange={e => handleChangePageSize(Number(e.target.value))}>
{pageSizeOptions.map((item, index) => (<option key={index} value={item}>{item}条/页</option>))}
</select>
)
}
return '';
}
return (
<div className="react-hook-pagenation">
<button disabled={currentPage === 1} className="prev-page" onClick={() => handleChangePage(currentPage === 1 ? 1 : currentPage - 1)}>上一页</button>
{pageArr.map((item, index) => {
if (item === '···') {
return <div className="page-item-omit" key={`${item}-${index}`}>{item}</div>
}
return <div className={`page-item ${item === currentPage ? 'current-page' : ''}`} key={`${item}-${index}`} onClick={() => handleChangePage(+item)}>{item}</div>
})}
<button disabled={currentPage === count} className="next-page" onClick={() => handleChangePage(currentPage === count ? count : currentPage + 1)}>下一页</button>
{makePageSizeOptions()}
{totalText && <div className="total">{totalText}</div>}
</div>
)
}
export default Pagenation;
在src/index.scss中编写样式代码:
.react-hook-pagenation {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
margin-top: 50px;
user-select: none;
.page-item {
box-sizing: border-box;
width: 24px;
height: 24px;
line-height: 24px;
text-align: center;
border: 1px solid #000000;
margin: 0 6px;
cursor: pointer;
}
.current-page {
color: red;
border: 1px solid red;
}
.page-item-omit {
width: 36px;
height: 24px;
line-height: 24px;
text-align: center;
}
.prev-page,
.next-page {
width: 72px;
height: 24px;
line-height: 24px;
border: 1px solid #000000;
background-color: #ffffff;
cursor: pointer;
}
.prev-page {
margin-right: 6px;
}
.next-page {
margin-left: 6px;
}
.page-item:hover,
.prev-page:hover,
.next-page:hover {
border: 1px solid #4F4F4F;
color: #4F4F4F;
background-color: #EAEAEA;
}
.prev-page:disabled,
.next-page:disabled {
border: 1px solid #C2C2C2;
color: #C2C2C2;
cursor: not-allowed;
}
.page-size {
width: 72px;
height: 24px;
line-height: 24px;
margin: 0 12px;
border: 1px solid #000000;
}
.total {
margin-left: 12px;
}
}
注意致力需要额外写一个src/index.d.ts,这样tsx文件才能支持引入css样式。
declare module '*.scss' {
const content: any;
export default content;
}
2.调试组件
编辑demo/index.html文件:
<!-- examples/src/index.html -->
<html>
<head>
<title>react-hook-pagenation</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<style>
html, body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<div id="root"></div>
<script src="dev.js"></script>
</body>
</html>
编辑demo/index.tsx文件:
/*** examples/src/app.js ***/
import React, { useState } from 'react';
import { createRoot } from 'react-dom/client';
import Pagenation from '../../src/index'; // 引入组件
// import Pagenation from 'react-hook-pagenation' // 引入组件
// import 'react-hook-pagenation/lib/main.min.css' // 引入组件
const App = () => {
const [currentPage, setCurrentPage] = useState(1);
const [list, setList] = useState([]);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(123);
const [pageSizeOptions, setPageSizeOptions] = useState([5, 10, 20, 30]);
const handleChangePage = (val) => {
setCurrentPage(val)
};
const handleChangePageSize = (val) => {
setPageSize(val)
setCurrentPage(1)
};
return (
<div>
<Pagenation currentPage={currentPage} pageSize={pageSize} pageSizeOptions={pageSizeOptions} total={total} totalText={`共${total}条`} handleChangePage={handleChangePage} handleChangePageSize={handleChangePageSize}></Pagenation>
</div>
);
}
const container = document.getElementById('root');
const root = createRoot(container); // createRoot(container!) if you use TypeScript
root.render(<App />);
执行yarn dev自动打开浏览器。
3.打包与测试:
我们打包完成之后,使用软链接来测试。 打包:
yarn build
链接,在组件根目录执行:
npm link
然后cd demo/src,执行:
npm link reack-hook-pagenation
修改demo/src/index.tsx,切换引入的组件(注意也要引入样式):
// import Pagenation from '../../src/index'; // 引入组件
import Pagenation from 'react-hook-pagenation' // 引入组件
import 'react-hook-pagenation/lib/main.min.css' // 引入组件
现在就可以看到使用效果了。
处理文档
使用readme-md-generator初始化README.md
readme-md-generator是一个非常流行的readme自动生成工具。
执行
npx readme-md-generator
效果如下:
发布
使用np来发布组件
np是一个非常简单易用的发包工具。
安装
np:
yarn add np --dev
在package.json里添加script:
"release": "np --no-yarn --no-tests --no-cleanup"
--no-yarn: 不使用yarn。--no-tests:跳过测试用例。--no-cleanup:发包时不要重新安装node_modules。 需要先登录npm,执行:
npm login
输入用户名、密码、邮箱即可。 发布执行:
npm run release
或者使用默认方式发包。在package.json里添加script:
"pub": "npm run build && npm publish"
执行npm run pub发包。
总结
这篇文章主要是给大家发布组件提供一个参考,没有实现太复杂的组件。
如果对你有所帮助,欢迎点赞Star,有问题欢迎提交issue或评论。
GitHub:github.com/flymoth/rea…