【从0到1】使用React开发一个分页组件并发布到NPM (Webpack5 +React18 + Sass + TS)

640 阅读4分钟

前言

简简单单用最新的框架实战一个NPM发布React组件流程。

FullSizeRender.2022-04-25 17_55_06.gif

创建项目

创建一个空项目,信息根据自己的需求填写。

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' // 引入组件

现在就可以看到使用效果了。

demo1.png

demo2.png

处理文档

使用readme-md-generator初始化README.md

readme-md-generator是一个非常流行的readme自动生成工具。

执行

npx readme-md-generator

效果如下: 60266090-9cf9e180-98e7-11e9-9cac-3afeec349bbc.jpeg

发布

使用np来发布组件

np是一个非常简单易用的发包工具。

screenshot.gif 安装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…