组件库开发实战 | 手把手教你从0到1开发并发布react组件库

1,642 阅读8分钟

目标

将xgplayer封装为一个组件,传入url即可播放,大小自适应,再进行样式定制化,使用者以react组件方式引入即可使用,无需进行额外配置

技术选型:rollup+react+typescript

why:rollup配置使用简便,生成的代码相对于Webpack更简洁。可以指定生成生产中使用的各种不同的模块(amd,commonjs,es,umd)。

解答的问题

通过阅读本文,你将找到以下问题的答案:

  1. 如何开发一个react组件库
  2. 如何在本地调试开发的组件库
  3. package中main、module、peerDependencies的含义
  4. 组件库如何生成ts声明文件
  5. rollup组件库开发常用的npm包及其作用

一、初始化

  1. 新建文件夹,执行命令yarn init(一路回车)
  1. 修改package.json文件
{
  "name": "demo",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "module": "./dist/es/index.js",
  "main": "./dist/lib/index.js",
}
//main : 定义了 npm 包的入口文件,browser 环境和 node 环境均可使用
//module : 定义 npm 包的 ESM 规范的入口文件,browser 环境和 node - 环境均可使用

二、创建组件

  1. 装包

yarn add react react-dom @types/react @types/react-dom typescript xgplayer -D
  1. 新建src文件夹

在下面分别创建index.ts文件和player.tsx文件

  1. index.ts

export { default as PlayerDemo} from './player'
  1. player.tsx

import React, { useEffect, useRef } from 'react'
import { IPlayerOptions } from 'xgplayer'
import Player from 'xgplayer'

const PlayerDemo: React.FC<IPlayerOptions> = (props) => {
  const ref = useRef<HTMLDivElement>(null)
  useEffect(() => {
    new Player({
      el: ref.current,
      height: '100%',
      width: '100%',
      pip: true,
      playbackRate: [0.5, 0.75, 1, 1.5, 2],
      ...props
    })
  }, [])
  return <div ref={ref}></div>
}
export default PlayerDemo

三、rollup打包

  1. 装包

yarn add @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript @rollup/plugin-babel @rollup/plugin-commonjs @rollup/plugin-node-resolve rollup rollup-plugin-typescript2 rollup-plugin-filesize -D
  1. 根目录新建tsconfig.json文件

{
  "compilerOptions": {
    "baseUrl": "./",    //你的工程src根目录
    "traceResolution": false,   //在debug的时候可以设置为true,这个属性的具体用法见下文
    "sourceMap": true,   //这个都知道,debug的时候打开吧
    "allowJs": true,   //是否允许工程中js和ts同时存在。
    "checkJs": false,   //是否对js文件开启静态检查,如果true的话,你的js文件中就可能很多红色的波浪线了。
    "jsx": "react",    //react工程必备
    "target": "es5",    //编译的目标语言,当然是最老的es5
    "module": "es6",   //模块引入方式,如果你想用import的话
    "moduleResolution": "node",    //模块搜索方式,按照node的来,一般没有说明异议
    "allowSyntheticDefaultImports": true,   //见下文
    "noImplicitAny": false,   //见下文
    "noUnusedLocals": false,   // true: 如果有未使用的块级变量,编译器会报错。
    "noUnusedParameters": false,   // true: 如果有未使用的参数,编译器会报错。鉴于js的动态性,这个我一般关掉
    "removeComments": false,   // 删除注释,debug的时候不开启
    "preserveConstEnums": false,   // 见下文
    "skipLibCheck": false,    // 跳过lib文件的静态检查,哎,不是所有的lib都给你写得规规整整的。
    "declaration" : true , // 是否自动创建类型声明文件
    "paths": {
      "@/*": ["src/*"]
      }
  },
  "include": [
    "src/**/*"
  ],
  "exclude": ["dist", "node_modules"]  //编译时排除 dist、node_modules文件夹
}
  1. 根目录下新建rollup.config.js文件

// 根据tsconfig.json配置执行ts转换
import typescript from 'rollup-plugin-typescript2';
// 帮助 Rollup 查找外部模块
import resolve from '@rollup/plugin-node-resolve';
// 将CommonJS模块转换为 ES2015 供 Rollup 处理
import commonjs from '@rollup/plugin-commonjs';
// Rollup 集成 Babel,转义代码
import babel from '@rollup/plugin-babel';
// 查看打包文件大小
import filesize from 'rollup-plugin-filesize';
const packageJson = require('./package.json');

export default {
  input: 'src/index.ts',
  output: [
    {
      file: packageJson.main,
      format: 'cjs',
      sourcemap: true,
    },
    {
      file: packageJson.module,
      format: 'esm',
      sourcemap: true,
    },
  ],
  plugins: [
    resolve(),
    commonjs(),
    typescript({ tsconfig: 'tsconfig.json' }),
    babel({
      presets: [
        '@babel/preset-env',
        '@babel/preset-typescript',
        '@babel/preset-react',
      ],
      extensions: ['.js', '.jsx', '.ts', '.tsx'],
      exclude: '**/node_modules/**',
    }),
    filesize(),
  ],
};
  1. 修改package.json(新增scripts,modules,修改main)

👉文章中采用代码后添加//++注释的方式来表示新增或者修改的代码所在位置

{
  "name": "demo",
  "version": "1.0.0",
  "license": "MIT",
  "module": "./dist/es/index.js", //++
  "main": "./dist/lib/index.js",//++
  "scripts": {//++
    "build": "yarn run rollup -c",//++
    "start": "yarn rollup -c -w"//++
  },//++
  "devDependencies": {
    "@babel/core": "^7.18.5",
    "@babel/preset-env": "^7.18.2",
    "@babel/preset-react": "^7.17.12",
    "@babel/preset-typescript": "^7.17.12",
    "@rollup/plugin-babel": "^5.3.1",
    "@rollup/plugin-commonjs": "^22.0.0",
    "@rollup/plugin-node-resolve": "^13.3.0",
    "@types/react": "^18.0.12",
    "@types/react-dom": "^18.0.5",
    "react": "^18.1.0",
    "react-dom": "^18.1.0",
    "rollup": "^2.75.6",
    "rollup-plugin-typescript2": "^0.32.1",
    "typescript": "^4.7.3",
    "xgplayer": "^2.31.6"
  }
}
  1. 执行命令yarn build,打包成功

  1. 压缩打包文件

执行命令

yarn add rollup-plugin-filesize -D

修改rollup.config.js

...
// 在生产环境下,压缩js代码
import { terser } from  'rollup-plugin-terser';  // ++


export default {
 ...
  plugins: [
    resolve(),
    commonjs(),
    typescript({ tsconfig: 'tsconfig.json' }),
    babel({
      presets: [
        '@babel/preset-env',
        '@babel/preset-typescript',
        '@babel/preset-react',
      ],
      extensions: ['.js', '.jsx', '.ts', '.tsx'],
      exclude: '**/node_modules/**',
    }),
    terser(),  // ++
    filesize(),
  ],
};

执行命令yarn build打包,结果如下,可以看到文件大小比之前小了不少

四、本地调试

注册当前包

在当前组件库根目录执行yarn link,提示如下

在其他项目引入

在其他react项目引入当前包进行验证,例如验证项目为test,在test根目录下执行

yarn link demo

然后引入组件

import { PlayerDemo } from 'demo'
...
return (
    <PlayerDemo url="url" />
   )

不出意外,项目会报错

index.js:10 Warning: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:

  1. You might have mismatching versions of React and the renderer (such as React DOM)

  2. You might be breaking the Rules of Hooks

  3. You might have more than one copy of React in the same app

See reactjs.org/link/invali… for tips about how to debug and fix this problem.

这是因为组件库demo自己有了一个react实例,验证项目test也有一个react实例,两个react实例不同导致报错。

解放方法如下:

  • 在验证项目test的目录下 执行 cd node_modules/react && yarn link
  • 在组件库项目目录下执行 yarn link react
  • 重新启动下验证项目即可

Ps: 解除关联引用可以使用yarn unlink [包名]

五、其他优化

peerDependencies

这里又有一个新的问题,既然组件库demo的react都不会使用,那么我们还打包他干什么,不是白占空间了吗?因此我们把依赖里的react和react-dom删除,同时也为了能和主项目(这里是验证项目test)的react版本号统一,我们需要引入Peer Dependencies | Node.js

peerDependencies是package.json中的依赖项,可以解决核心库被下载多次,以及统一核心库版本的问题。

  1. 修改package.json(peerDependencies)
{
  ...
  "devDependencies": {
  ...
  },
  "peerDependencies": { //++
    "react": ">=17.0.0", //++
    "react-dom": ">=17.0.0" //++
  } //++
}
  1. 添加rollup-plugin-peer-deps-external插件(依赖package.jsonpeerDependencies字段)
yarn add rollup-plugin-peer-deps-external -D
  1. 修改rollup.config.js
...
// 配合peerDependencies使用
import peerDepsExternal from  'rollup-plugin-peer-deps-external'; //++
...

export default {
  ...
  plugins: [
   ...
    peerDepsExternal(),  //++
   ...
  ],
};

输出声明文件到指定目录

👉要使用rollup-plugin-typescript2,一定要带2,不带2的包有bug,不建议使用

  1. 修改tsconfig.json
{
  "compilerOptions": {
    ...
    "declaration": true, // 是否自动创建类型声明文件
    "declarationDir"  :  "./dist/types"  ,  /* '.d.ts' 文件输出目录 */ //++
    "paths": {
      "@/*": ["src/*"]
      }
  },
...
}
  1. 修改rollup.config.js
...
  plugins: [
    ...
    typescript({ tsconfig: 'tsconfig.json', useTsconfigDeclarationDir : true }), //++
    ...
  ],
};
  1. 修改package.json(新增types,不然引用组件时会提示无法找到模块的声明文件)
{
  "name": "@bytepack/deliver-player",
  "version": "2.0.25",
  "license": "MIT",
  "module": "./dist/es/index.js",
  "main": "./dist/lib/index.js",
  "types"  :  "./dist/types/index.d.ts", //++
  ...
}

提取打包样式文件(scss)

  1. 装包
yarn add postcss rollup-plugin-postcss -D
  1. 修改rollup.config.js
...
// 分离编译打包样式文件,css/scss/less
import postcss from  'rollup-plugin-postcss'  ;  //++
const packageJson = require('./package.json');

// 新增以下处理函数
const processScss = function (context) {
  return new Promise((resolve, reject) => {
    sass.compile(
      {
        file: context,
      },
      function (err, result) {
        if (!err) {
          resolve(result);
        } else {
          reject(result);
        }
      }
    );
    sass.compile(context, {}).then(
      function (output) {
        if (output && output.css) {
          resolve(output.css);
        } else {
          reject({});
        }
      },
      function (err) {
        reject(err);
      }
    );
  });
};

export default {
  
  plugins: [
    resolve(),
    commonjs(),
    postcss({ process : processScss }),  //++
    typescript({ tsconfig: 'tsconfig.json'useTsconfigDeclarationDir: true}),
    peerDepsExternal(),
    babel({
      presets: [
        '@babel/preset-env',
        '@babel/preset-typescript',
        '@babel/preset-react',
      ],
      extensions: ['.js', '.jsx', '.ts', '.tsx'],
      exclude: '**/node_modules/**',
    }),
    terser(),
    filesize(),
  ],
};

👉开发时踩了一个坑,组件库样式不生效,原因如下:

plugins: [ postcss({ exact: true, process: processScss }) ]

如果配置了exact:true,样式文件会被单独提取到一个css文件里面,因此当引入组件时还需要引入组件的.css文件,否则样式不生效,不配置exact,默认样式打包到js文件中,无需单独引入css文件。

打包svg

推荐使用rollup-plugin-svg,而不是@rollup/plugin-image

  1. 装包
yarn add rollup-plugin-svg
  1. 修改rollup.config.js
// 加载svg
import svg from  'rollup-plugin-svg';  //++
...

  plugins: [
    resolve(),
    commonjs(),
    postcss({ exact: true, process: processScss }),
    typescript({ tsconfig: 'tsconfig.json', useTsconfigDeclarationDir: true }),
    peerDepsExternal(),
    svg(),  //++
    babel({
      presets: [
        '@babel/preset-env',
        '@babel/preset-typescript',
        '@babel/preset-react',
      ],
      extensions: ['.js', '.jsx', '.ts', '.tsx'],
      exclude: '**/node_modules/**',
    }),
    terser(),
    filesize(),
  ],

rollup svg 导入问题,使用@rollup/plugin-image打包导入svg失败,原因参考: Outputting svgs to bundle with RollupJS using @rollup/plugin-image in a LitElement project

六、发布流程

  1. 注册(已有则跳过)

登录 npm官网 ****注册账户,顺便确认一下你的包名(package.json中的name)有没有被人占用,被占用就换一个。

  1. 登陆

在npm模块包目录下运行 npm login 依次输入用户名,密码和邮箱

  1. 发布文件配置

发布到npm上的包,默认是不上传node_modules文件夹中的内容的。同时呢,你还可以创建一个.npmignore文件,来确定更多的忽略文件和文件夹。 如果你的模块里还有 .gitignore 文件时,同样也会忽略这里面包含的文件和文件夹(很有可能这里面包含了dist,导致dist没有发布上去)

但是,通常编译后产生的目录(我这里的是dist目录),我们是不加入git追踪的,把dist目录添加到.gitignore中。可是这样也就上传不到npm上了,我们就需要在package.json中,添加一个files数组字段:

{
    "files":[
        "dist"
    ]
}

ignore方式是利用黑名单的方式控制文件的提交,package.json中的files属性则是利用白名单的方式去控制。他可以是字符串或数组,写入的是需要提交到npm官网的文件或文件夹。

这样就只会提交dist文件夹以及默认的npm永不会忽略的那几个文件,不会提交src文件夹。

几种配置方式的优先级:files属性 > .npmignore > .gitignore

  1. 发布

执行npm publish

👉注意:必需用 npm的原始镜像 如果修改过,要改一下 npm config set registry registry.npmjs.org/

七、踩坑定位

  1. 本地调试报错,参考[四、本地调试部分]
  2. TS生成声明文件报错,参考[五、其他优化,输出声明文件到指定目录]
  3. svg图片无法正常显示,参考[五、其他优化,打包svg]

如果开发过程中有遇到其他问题,欢迎留言交流😄