(建议收藏)使用React+Typescript开发组件并发布到npm仓库

举报
小黑豆豆 发表于 2022/07/27 14:56:40 2022/07/27
【摘要】 这篇文章将来介绍如何使用React+Typescript开发一个组件并发布到npm仓库。在此实践流程中,踩了许多坑才成功将其发布,因而记录下整个流程,希望能帮助到想发布包到npm的各位小伙伴。

前言

这篇文章将来介绍如何使用React+Typescript开发一个组件并发布到npm仓库,此文中开发的组件是在实际项目中已开发使用的,将不再这赘述,下篇文章将具体讲解它的代码实现逻辑,本文主要介绍使用npm发布一个React组件供其他开发者下载使用的过程。在此实践流程中,踩了许多坑才成功将其发布,因而记录下整个流程,希望能帮助到想发布包到npm的各位小伙伴。

开发

这次要发布的组件是一个文本宽度自适应标签组件。当文本的容器固定之后,文字长度超过容器宽度时不想让文字省略显示而是需要文字全部显示出来,实现方案就是使用canvas来绘制文字可避免去做动态改变字号的问题。

一、创建项目

首先新建项目文件夹,并使用npm init命令初始化package.json配置文件。如下:

mkdir textWidthAutoLabel
cd textWidthAutoLabel
npm init

在使用npm init命令时,会提示输入项目的名称、版本号、关键词、作者等,可以一路回车结束,后续也可对package.json文件进行修改。

这一步完成后,项目文件夹中就会新增一个package.json文件。接下来添加一些目录和文件,以下为我的目录结构:

├── config  # webpack配置
     ├── webpack.base.js # 公共配置
     ├── webpack.dev.config.js # 开发环境配置
     └── webpack.prod.config.js # 打包发布环境配置
├── demo # 开发时预览代码
       ├── src # 示例代码目录
            ├── index.jsx # 入口 jsx 文件
            ├── index.html # 入口 html 文件
            └── index.scss # 样式文件
├── lib # 组件打包结果目录
├── node_modules # 安装依赖时自动生成
├── src # 组件源代码目录
     └── index.tsx  # 组件源代码
├── .babelrc # babel 配置
├── .gitignore # git 忽略
├── .npmignore # 指定发布 npm 的时候须要忽略的文件和文件夹
├── README.md
├── package-lock.json
└── package.json

二、安装依赖与配置

接下来就是安装项目所需要的的依赖和对一些文件进行配置,请往下看→

2.1 安装React依赖

首先安装react相关的依赖:

npm i react react-dom -D

2.2 安装webpack依赖

本项目采用webpack进行构建,使用web-pack-dev-server作为本地开发服务器,使用webpack-merge合并webpack配置,所以需要安装以下依赖:

npm i webpack webpack-cli webpack-dev-server webpack-merge -D

2.3 安装css相关依赖

此项目的组件代码是不含css样式代码的,但是为了后续需要用到样式写组件的小伙伴,这里也安装处理样式的依赖,需要的自取就行,如下:

npm i postcss postcss-loader postcss-preset-env style-loader css-loader sass-loader node-sass mini-css-extract-plugin -D

2.4 安装babel相关依赖

接着安装babel编译相关的依赖:

npm i @babel/cli @babel/core @babel/preset-env @babel/preset-react -D

2.5 typescript依赖

本项目的组件使用了typescript进行类型限制,所以也需要安装支持TS的依赖包:

npm i @types/react @types/react-dom ts-loader @babel/preset-typescript -D

2.6 配置文件修改

当执行完以上命令,会在package.json中看到包含的依赖信息如下:

"devDependencies": {
  // babel用于将es6+的代码转换成es5
  "@babel/cli": "^7.17.10",
  "@babel/core": "^7.18.5",
  "@babel/preset-env": "^7.18.2", // 根据目标环境实现按需转码
  "@babel/preset-react": "^7.17.12", // 让babel支持react语法
  "@babel/preset-typescript": "^7.17.12",
  "@types/react": "^18.0.14",
  "@types/react-dom": "^18.0.5",
  "babel-loader": "^8.2.5", // 编译jsx
  "mini-css-extract-plugin": "^2.6.1", // 提取css
  "react": "^18.2.0",
  "react-dom": "^18.2.0",
  "style-loader": "^3.3.1", // 将css编译成js
  "postcss": "^8.4.14",
  "postcss-loader": "^7.0.0",
  "postcss-preset-env": "^7.7.2",
  "css-loader": "^6.7.1",
  "sass-loader": "^13.0.0",
  "node-sass": "^7.0.1",
  "ts-loader": "^9.3.1", // 支持ts
  "typescript": "^4.7.4", 
  "webpack": "^5.73.0",
  "webpack-cli": "^4.10.0", // webpack4以后须要额外安装webpack-cli
  "webpack-dev-server": "^4.9.2", // 开发是预览组件所用的服务,在文件变化时会自动刷新页面
  "webpack-merge": "^5.8.0" // 用于合并webpack配置
}

接下来就是根据前面给出的目录结构,在config目录下配置3webpack文件:

  • 因为开发和发布打包时webpack配置有一部分是公共并且重复的,因此把这部分的配置单独拿出来放到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 目录下的文件
            }
        ]
    },
}; 
  • 开发时采用的webpack配置写在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.jsx"), // 入口,处理资源文件的依赖关系
    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); // 合并配置
  • 打包组件时采用的webpack配置写在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: "index.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); // 合并配置
  • 使用babel把代码编译成es5版本,在项目根目录下建好的.babelrc文件内补充如下内容:
{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-react",
    "@babel/preset-typescript"
  ]
}
  • git提交代码进行版本管理时需要在.gitignore中添加如下配置:
# 指定发布 npm 的时候需要忽略的文件和文件夹
# npm 默认不会把 node_modules 发上去
# dependencies
/node_modules

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*

.idea
  • 完成webpack配置文件的编写以后,须要在package.json中的scripts字段中配置相关的启动、打包和发布命令,在package.json中添加的脚本内容以下:
"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"
}

至此,依赖和配置就已完成,下面就开始开发组件的逻辑。

三、开发组件

这一部分不讲解具体代码的实现逻辑,只是贴出来让小伙伴们能正常往下进行,请往下看→

3.1 编写组件代码

src/index.tsx中添加组件逻辑代码:

import React, { useEffect, useRef } from 'react';

interface WidthAutoLabelProps {
    children: string;  // 要绘制的文本
}


/**
 * 文本宽度自适应标签组件
 * @param props
 * @constructor
 */
const WidthAutoLabel = (props: WidthAutoLabelProps) => {
    const { children = '' } = props;
    const ref = useRef<any>(null);

    useEffect(() => {
        drawText();
    }, [children]);


    const drawText = () => {
        let canvas = ref.current;
        if (!canvas) {
            return;
        }
        let parentNode = canvas.parentNode;
        if (!parentNode) {
            return;
        }
        const { fontSize, fontStyle, fontWeight, fontFamily } = getComputedStyle(parentNode, null);
        const { clientWidth, clientHeight } = parentNode;
        canvas.width = clientWidth;
        canvas.height = clientHeight;
        let ctx = canvas.getContext("2d");
        // 设置文本样式
        ctx.font = `${fontStyle} ${fontWeight} ${fontSize} ${fontFamily}`;
        ctx.textBaseline = 'top';
        let y = Math.ceil((clientHeight - Number(fontSize.replace('px', ''))) / 2);
        let x = 0;
        // 居中显示
        const { width: textWidth } = ctx.measureText(children);
        if (textWidth < clientWidth) {
            x = (clientWidth - textWidth) / 2;
        }
        // 绘制文本
        ctx.fillText(children, x, y, clientWidth);
    }

    return (
        <canvas ref={ref}></canvas>
    )
}

export default WidthAutoLabel;

如果你写的组件用到了样式代码,那需要在src文件中增加index.d.ts文件,这样tsx文件才能支持引入css样式:

declare module '*.scss' { 
    const content: any; 
    export default content; 
}

3.2 调试组件

为了能在开发时实时预览组件的效果,需要在demo目录下新建index.htmlindex.jsx两个文件,index.html的内容以下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>text-width-auto-label</title>
    <style>
        html, body, p {
            margin: 0;
            padding: 0;
        }
    </style>
</head>
<body>
    <div id="root"></div>
    <script src="dev.js"></script>
</body>
</html>

index.html文件中引入了文件名为 dev.js的脚本,缘由是在开发过程(使用webpack-dev-sevrer启动开发服务)时,并不会实际在src目录下生成 dev.js,打包好的文件是在内存中的,若是要实时预览效果,须要在html中引入。

index.jsx的内容以下:

import React from "react"
import { createRoot } from 'react-dom/client';
import WidthAutoLabel from '../../src/index'; // 引入组件
import './index.scss';
// import WidthAutoLabel from 'text-width-auto-label';

const App = () => {
    return (
        <div className="container">
            <div className="text">
                <p>示例文本:</p><p>文本宽度自适应标签组件</p>
            </div>
            <div className="normal">
                <p>宽度100px:</p><p>文本宽度自适应标签组件</p>
            </div>
            <div className="style1">
                <p>宽度100px:</p><p><WidthAutoLabel>文本宽度自适应标签组件</WidthAutoLabel></p>
            </div>
            <div className="style2">
                <p>宽度80px:</p><p><WidthAutoLabel>文本宽度自适应标签组件</WidthAutoLabel></p>
            </div>
            <div className="style3">
                <p>宽度50px:</p><p><WidthAutoLabel>文本宽度自适应标签组件</WidthAutoLabel></p>
            </div>
        </div>
    );
}
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />);

index.scss的内容以下:

.container {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    margin-top: 30px;
    font-size: 14px;
    font-weight: bold;
    color: #333;
    .text {
        display: flex;
        align-items: center;
        p:nth-child(2) {
            color: #f00;
        }
    }
    .normal {
        display: flex;
        align-items: center;
        margin-top: 10px;
        p:nth-child(2) {
            width: 100px;
            height: 32px;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
            border: 1px solid #ccc;
        }
    }
    .style1 {
        display: flex;
        align-items: center;
        margin-top: 10px;
        p:nth-child(2) {
            width: 100px;
            height: 32px;
            border: 1px solid #ccc;
        }
    }
    .style2 {
        display: flex;
        align-items: center;
        margin-top: 10px;
        p:nth-child(2) {
            width: 70px;
            height: 32px;
            border: 1px solid #ccc;
        }
    }
    .style3 {
        display: flex;
        align-items: center;
        margin-top: 10px;
        p:nth-child(2) {
            width: 40px;
            height: 32px;
            border: 1px solid #ccc;
        }
    }
}

如上文件的代码编写完,就可在根目录下打开cmd,执行npm run dev命令,当看到下面的截图结果,则表示编译成功:

image.png

然后在浏览器上输入http://127.0.0.1:8686/,则会看到下图的结果:

image.png

3.3 打包与测试

完成组件的开发,在发包以前,需要对本身组件的功能进行打包和测试。前面已经在package.json中添加了打包组件的命令,然而在执行npm run build以前,还需要在package.json中修改main字段,用作是声明组件的入口文件。

"main": "/src/index.tsx",

至此,package.json的完整代码如下:

{
  "name": "text-width-auto-label",
  "version": "0.0.1",
  "description": "文本宽度自适应标签组件",
  "main": "/src/index.tsx",
  "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"
  },
  "keywords": [
    "react",
    "text-auto",
    "width-auto",
    "text-width-auto"
  ],
  "author": "jacky010",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.17.10",
    "@babel/core": "^7.18.5",
    "@babel/preset-env": "^7.18.2",
    "@babel/preset-react": "^7.17.12",
    "@babel/preset-typescript": "^7.17.12",
    "@types/react": "^18.0.14",
    "@types/react-dom": "^18.0.5",
    "babel-loader": "^8.2.5",
    "mini-css-extract-plugin": "^2.6.1",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "style-loader": "^3.3.1",
    "postcss": "^8.4.14",
    "postcss-loader": "^7.0.0",
    "postcss-preset-env": "^7.7.2",
    "css-loader": "^6.7.1",
    "sass-loader": "^13.0.0",
    "node-sass": "^7.0.1",
    "ts-loader": "^9.3.1",
    "typescript": "^4.7.4",
    "webpack": "^5.73.0",
    "webpack-cli": "^4.10.0",
    "webpack-dev-server": "^4.9.2",
    "webpack-merge": "^5.8.0"
  }
}

接下来,则可使用npm run build命令打包开发好的组件,出现下面的结果则表明打包成功:

image.png

此时,项目根目录的lib目录下会出现index.js文件(如果组件含有样式文件,那将会出现index.css文件),这里我的组件没有样式文件,则不会有index.css文件:

image.png

打包的任务已完成,现在是要来进行发布前的测试了,使用npm link把打包以后的组件引入到全局node-modules中,如下:

image.png

然后进入demo/src文件夹下,执行npm link text-width-auto-label命令(注意:这里的text-width-auto-label一定要是package.json配置的name,如果自己再取一个包名一定要去修改package.json中的name才会成功,否则将会报错,切记!切记!切记!);而后修改demo/src/index.tsx的内容:

import React from "react"
import { createRoot } from 'react-dom/client';
// import WidthAutoLabel from '../../src/index'; // 注释它引入组件
import './index.scss';
import WidthAutoLabel from 'text-width-auto-label'; // 引入node-modules中的组件

const App = () => {
    return (
        <div className="container">
            <div className="text">
                <p>示例文本:</p><p>文本宽度自适应标签组件</p>
            </div>
            <div className="normal">
                <p>宽度100px:</p><p>文本宽度自适应标签组件</p>
            </div>
            <div className="style1">
                <p>宽度100px:</p><p><WidthAutoLabel>文本宽度自适应标签组件</WidthAutoLabel></p>
            </div>
            <div className="style2">
                <p>宽度80px:</p><p><WidthAutoLabel>文本宽度自适应标签组件</WidthAutoLabel></p>
            </div>
            <div className="style3">
                <p>宽度50px:</p><p><WidthAutoLabel>文本宽度自适应标签组件</WidthAutoLabel></p>
            </div>
        </div>
    );
}
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />);

同样能看到下图,则表示打包的组件测试验证通过;

image.png

四、编写Readme文档

接下来,就要做发布组件到npmreadme文档的编写了。这里采用的是readme-md-generator插件,它是一个非常流行的readme自动生成工具。

执行npx readme-md-generator命令,接着可以做一些配置,也可以一路enter键,最后再去生成的文件中做详细配置:

image.png

五、发布

万事俱备只欠东风,接下来就是发布组件的最后一步了。

5.1 注册npm账号

要发布一个npm包,咱们需使用以下命令添加一个npm的帐号(若是已经添加过的这一步能够跳过):

npm adduser

按提示信息输入本身的用户名、密码及邮箱后,若是命令行输出Logged in as <user> on https://registry.npmjs.org/,则说明帐号注册并登录成功。若是已经有npm帐号,则直接使用npm login登录。

因为国内使用npm官方源安装包的时候比较慢,基本上在国内开发都会修改npm源地址,在发包以前,必定要切换到npm源才能够,否则就会报出以下错误:

error: no_perms Private mode enable, only admin can publish this module

可使用npm config list查看当前使用的源地址,若是不是官方地址,则能够经过下面的命令切换npm源:

npm config set registry http://registry.npmjs.org

然后登录npm,执行npm login命令,输入用户名、密码、邮箱即可;最后使用默认的发包方式,执行package.json中的脚本:

npm run pub

当看到下图就表示发包成功:

image.png

这时登录npm就可以看到你刚刚发布的包了,如下:

image.png

如果别人对你发布的包感兴趣或者适用于他的项目场景,他可以通过以下命令下载你的包并在他的项目中进行使用:

npm i text-width-auto-label

下面就是我新创建的react项目,并下载了text-width-auto-label组件测试得到的效果图,如下:

image.png

经过测试,该组件可以在项目中使用,所以小伙伴们可以放心下载使用。但是如果你的项目的tswebpack的版本不是最新版本可能会有问题,因为我这个组件使用的插件均是最新版本,所以可能会存在兼容版本问题,若出现报错问题可以自行更新相应的插件版本,若不想更新版本,那就将代码复制出来自行在项目中创建组件使用吧……

image.png

若出现上图ts标红提示,可以选择ts选择忽略或继续向下选择文件:

image.png

image.png

最后,这篇文章要聊的主题就聊完了,如果你也想发布包到npm但苦于没有很好的讲解文章,你不妨可以参考此篇文章,我相信如果认真读完这篇你一定会成功地把自己的组件包发布成功到npm上的,赶紧进来码住并按着方法一步步去实现吧。(至于文中提到的组件没有细致的去讲解他的实现过程,将会在下一篇文章进行讲解,敬请期待^_^)

资源

之前发布的包资源请前往以下地址:

npm地址:https://www.npmjs.com/settings/jacky010/packages

github仓库:https://github.com/Jacky010

后语

伙伴们,如果觉得本文对你有些许帮助,点个👍或者➕个关注再走呗^_^ 。另外如果本文章有问题或有不理解的部分,欢迎大家在评论区评论指出,我们一起讨论共勉。

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。