IdbI

第40章—组件库实战:构建esm和cjs产物,发布到npm

我们已经写了很多组件了,比如 Calendar、Watermark、OnBoarding 等,但都是用 cra 或者 vite 单独创建项目来写的。

这节我们把它们整合一下,加上构建脚本,发布到 npm,做成和 Ant Design 一样的组件库。

组件库的构建我们上节分析过,就是构建出 esm、commonjs、umd 3 种格式的代码,再加上 css 的构建就好了。

ant design、arco design、semi design 都是这样。

我们再看几个组件库:

mkdir tmp
cd tmp
npm init -y

安装 react-bootstrap:

pnpm install react-bootstrap

(用 pnpm 安装,node_modules 下目录比较简洁)

看下 node_modules/react-bootstrap 的 package.json

可以看到,它也有 main 和 module,也就是 commonjs 和 es module 两种入口。

当你 import 的时候,引入的是 esm 的代码。

当你 require 的时候,引入的是 commonjs 的代码。

看一下 esm 和 cjs 下的代码:

当然,它也是支持 umd 的:

看下 build 脚本

就是分别用 babel 编译出 commonjs 和 esm 的代码就可以了:

用 tsc 也行。

umd 格式代码也同样是 webpack 打包的

不同于 antd、arco design 和 semi design,它就没有用 gulp 来组织流程。

gulp 本来就不是必须的,可用可不用。

甚至连单独的脚本都不需要,直接 tsc 编译就行:

比如这个 blueprint 组件库

之前总结的组件库的构建流程是没问题的:

然后我们新建一个项目来试一下:

npx create-vite guang-components

进入项目,复制几个之前的组件过来:

复制 Calendar、Watermark、Message 这三个组件:

然后安装下依赖:

npm install

npm install --save react-transition-group lodash-es dayjs classnames

npm i --save-dev @types/react-transition-group

然后去掉 Calendar 和 Message 组件里样式的引入,css 和 js 是分开编译的:

把这些没用的文件删掉:

加一个 index.ts 来导出组件:

import Calendar, { CalendarProps } from './Calendar';
import Watermark, { WatermarkProps } from './Watermark';
import { MessageProps, Position, MessageRef} from './Message';
import { useMessage } from './Message/useMessage';
import { ConfigProvider } from './Message/ConfigProvider';

export {
    Calendar,
    Watermark,
    ConfigProvider,
    useMessage
}

export type {
    CalendarProps,
    WatermarkProps,
    MessageProps,
    Position,
    MessageRef
}

接下来加上 tsc 和 sass 的编译:

添加一个 tsconfig.build.json 的配置文件:

{
    "compilerOptions": {
      "declaration": true,
      "allowSyntheticDefaultImports": true,
      "target": "es2015",
      "lib": ["ES2020", "DOM", "DOM.Iterable"],
      "module": "ESNext",
      "skipLibCheck": true,
      "moduleResolution": "Node",
      "resolveJsonModule": true,
      "isolatedModules": true,
      "jsx": "react-jsx",  
      "allowImportingTsExtensions":null,
      "strict": true,
    },
    "include": [
      "src"
    ],
    "exclude": [
      "src/**/*.test.tsx",
      "src/**/*.stories.tsx"
    ]
}

然后编译下:

npx tsc -p tsconfig.build.json --module ESNext --outDir dist/esm

npx tsc -p tsconfig.build.json --module commonjs --outDir dist/cjs

看下 dist 的产物:

没啥问题,esm 和 commonjs 格式的代码都生成了。

然后再编译下样式:

npx sass ./src/Calendar/index.scss ./dist/esm/Calendar/index.css

npx sass ./src/Calendar/index.scss ./dist/cjs/Calendar/index.css

npx sass ./src/Message/index.scss ./dist/esm/Message/index.css

npx sass ./src/Message/index.scss ./dist/cjs/Message/index.css

看下产物:

没问题。

当然,sass 文件多了以后你可以写个 node 脚本来自动查找所有 sass 文件然后编译。

接下来只要把这个 dist 目录发到 npm 仓库就可以了。

"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"types": "dist/esm/index.d.ts",
"files": [
    "dist",
    "package.json",
    "README.md"
],

main 和 module 分别是 commonjs 和 es module 的入口。

types 是 dts 的路径。

files 是哪些文件发布到 npm 仓库,没列出来的会被过滤掉。

并且,还需要把 private: true 和 type: module 的字段给去掉。

然后我们来发布 npm 包:

npm adduser

执行 npm adduser 命令,会打印一个链接让你登录或者注册:

登录后就可以 npm publish 了:

npm publish

发布完之后,在 https://www.npmjs.com 就可以搜索到:

我们新建个项目来用用看:

npx create-vite guang-test

安装依赖:

pnpm install

pnpm install guang-components

在 App.tsx 里用一下:

import { Watermark } from 'guang-components';

const App = () => {
  return <Watermark
    content={['测试水印', '神说要有光']}
  >
   <div style={{height: 800}}>
    <p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quos quod deserunt quidem quas in rem ipsam ut nesciunt asperiores dignissimos recusandae minus, eaque, harum exercitationem esse sapiente? Eveniet, id provident!</p>
    <p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quos quod deserunt quidem quas in rem ipsam ut nesciunt asperiores dignissimos recusandae minus, eaque, harum exercitationem esse sapiente? Eveniet, id provident!</p>
    <p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quos quod deserunt quidem quas in rem ipsam ut nesciunt asperiores dignissimos recusandae minus, eaque, harum exercitationem esse sapiente? Eveniet, id provident!</p>
    <p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quos quod deserunt quidem quas in rem ipsam ut nesciunt asperiores dignissimos recusandae minus, eaque, harum exercitationem esse sapiente? Eveniet, id provident!</p>
    <p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quos quod deserunt quidem quas in rem ipsam ut nesciunt asperiores dignissimos recusandae minus, eaque, harum exercitationem esse sapiente? Eveniet, id provident!</p>
    <p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quos quod deserunt quidem quas in rem ipsam ut nesciunt asperiores dignissimos recusandae minus, eaque, harum exercitationem esse sapiente? Eveniet, id provident!</p>
    <p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quos quod deserunt quidem quas in rem ipsam ut nesciunt asperiores dignissimos recusandae minus, eaque, harum exercitationem esse sapiente? Eveniet, id provident!</p>
   </div>
  </Watermark>
};

export default App;

跑下开发服务:

npm run dev

浏览器访问下:

再试下另外两个组件:

import dayjs from 'dayjs';
import {Calendar} from 'guang-components';
import 'guang-components/dist/esm/Calendar/index.css';

function App() {
  return (
    <div>
      <Calendar value={dayjs('2024-07-01')}></Calendar>
    </div>
  );
}

export default App;

这里用到了 dayjs:

pnpm install dayjs

这里样式受 index.css 影响了,去掉就好了:

再来试下 Message 组件:

import { ConfigProvider, useMessage } from "guang-components"
import 'guang-components/dist/esm/Message/index.css';

function Aaa() {
  const message = useMessage();

  return <button onClick={() =>{
    message.add({
      content:'请求成功'
    })
  }}>成功</button>
}

function App() {

  return (
    <ConfigProvider>
      <div>
        <Aaa></Aaa>
      </div>
    </ConfigProvider>
  );
}

export default App;

没啥问题。

然后我们优化下依赖:

其实用到 guang-components 的项目都会安装 react 和 react-dom 包,不需要把它放在 dependencies 里。

而是放在 peerDependencies 里:

它和 dependencies 一样,都是依赖,但是 dependencies 是子级,而 peerDependencies 是平级。

如果和其他 react 包的版本冲突时,dependencies 会保留一份副本,而 peerDependencies 会提示开发者去解决冲突,不会保留副本。

改下版本号,重新 npm publish:

这样,我们的组件库的 npm 包就发布成功了!

再测试下 commonjs 的代码。

用 cra 创建个项目:

npx create-react-app --template=typescript tmp4

进入项目,安装组件库:

npm install --save guang-components

在 App.tsx 用一下:

const { Watermark } = require('guang-components');

const App = () => {
  return <Watermark
    content={['测试水印', '神说要有光']}
  >
   <div style={{height: 800}}>
    <p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quos quod deserunt quidem quas in rem ipsam ut nesciunt asperiores dignissimos recusandae minus, eaque, harum exercitationem esse sapiente? Eveniet, id provident!</p>
    <p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quos quod deserunt quidem quas in rem ipsam ut nesciunt asperiores dignissimos recusandae minus, eaque, harum exercitationem esse sapiente? Eveniet, id provident!</p>
    <p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quos quod deserunt quidem quas in rem ipsam ut nesciunt asperiores dignissimos recusandae minus, eaque, harum exercitationem esse sapiente? Eveniet, id provident!</p>
    <p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quos quod deserunt quidem quas in rem ipsam ut nesciunt asperiores dignissimos recusandae minus, eaque, harum exercitationem esse sapiente? Eveniet, id provident!</p>
    <p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quos quod deserunt quidem quas in rem ipsam ut nesciunt asperiores dignissimos recusandae minus, eaque, harum exercitationem esse sapiente? Eveniet, id provident!</p>
    <p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quos quod deserunt quidem quas in rem ipsam ut nesciunt asperiores dignissimos recusandae minus, eaque, harum exercitationem esse sapiente? Eveniet, id provident!</p>
    <p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quos quod deserunt quidem quas in rem ipsam ut nesciunt asperiores dignissimos recusandae minus, eaque, harum exercitationem esse sapiente? Eveniet, id provident!</p>
   </div>
  </Watermark>
};

export default App;

注意,这次用 require 引入代码。

然后把开发服务跑起来:

npm run start

浏览器里看一下:

没啥问题。

案例代码上传了小册仓库

总结

今天我们把之前写过的部分组件封装成了组件库并发布到了 npm 仓库。

可以直接在项目里引入来用,和 antd 等组件库一样。

构建部分我们分析过很多组件库,都是一样的:

  • commonjs 和 esm 的代码通过 tsc 或者 babel 编译产生
  • umd 代码通过 webpack 打包产生
  • css 代码通过 sass 或者 less 等编译产生
  • dts 类型也是通过 tsc 编译产生

我们在 package.json 里配置了 main 和 module,分别声明 commonjs 和 es module 的入口,配置了 types 指定类型的入口。

然后通过 npm adduser 登录,之后 npm publish 发布到 npm。

这样,react 项目里就可以引入这个组件库来用了,之前写过的所有组件都可以加到这个组件库里。

返回上级