jtwang7 / Project-Note

开发小记 - 项目里的一些新奇玩意儿
1 stars 0 forks source link

发布你专属的 UI Library ( with Rollup) #19

Open jtwang7 opened 1 year ago

jtwang7 commented 1 year ago

发布你专属的 UI Library ( with Rollup)

参考文章:

步骤 (以 React 为例)

1. 创建项目

npx create-react-app ui-library --template typescript
npm install

👉 (可选) 关联 github 仓库

git add .  
git commit -m "component library first commit" 
// 关联远程repository,记得修改为你的ssh链接 
git remote add origin [git链接]
git push -u origin master 

2. 引入第三方库

npx storybook init
npm i sass
// ...

可能存在依赖安装冲突或丢失,检查安装正确的依赖

3. 配置 Rollup

👉 安装 Rollup

npm i rollup
// or global npm for mac
sudo npm i -g rollup

👉 安装 Rollup 插件

npm i rollup-plugin-peer-deps-external @rollup/plugin-node-resolve @rollup/plugin-commonjs rollup-plugin-typescript2 rollup-plugin-scss @rollup/plugin-json @rollup/plugin-babel

🔥 配置 rollup.config.js 在项目根目录下创建 rollup.config.js 配置文件,rollup 会根据该文件配置进行打包。

// rollup.config.js
import peerDepsExternal from "rollup-plugin-peer-deps-external";
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import typescript from "rollup-plugin-typescript2";
import scss from "rollup-plugin-scss";
import json from "@rollup/plugin-json";
const { babel } = require("@rollup/plugin-babel");
const packageJson = require("./package.json");

const isProd = process.env.NODE_ENV === "production";

const babelOptions = {
  presets: ["@babel/preset-env"],
  extensions: [".js", ".jsx", ".ts", ".tsx", ".less"],
  exclude: "**/node_modules/**",
};

const options = {
  // 文件入口
  input: "src/index.ts",
  // 文件出口: 此处输出了 commonJS 和 ES 两种模块的打包结果
  output: [
    {
      file: "build/index.js",
      // file: packageJson.main, 
      format: "cjs",
    },
    {
      file: "build/index.esm.js",
      // file: packageJson.module, // 也可以在 package.json 中声明一个 module 自定义字段,通过 require('package.json') 引入
      format: "es",
    },
  ],
  // 常见插件注册
  plugins: [
    peerDepsExternal({ includeDependencies: !isProd }),
    resolve(),
    commonjs({ sourceMap: !isProd }),
    typescript({ useTsconfigDeclarationDir: true }),
    scss(),
    babel(babelOptions),
    json(),
  ],
};

export default options;

4. 修改 package.json 配置

🔥 package.json docs

{
  "name": "ui-library-tian",  // npm包名
  "version": "0.1.0", // npm包版本号
  "files": ["build"], // 描述了你的 npm 包作为依赖关系被安装时,被加载的文件资源。
  "main": "build/index.js", // 依赖包执行的主入口
  "types": "build/types/index.d.ts", // 依赖包执行时所需声明文件的主入口
  "peerDependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "typescript": "^4.9.5"
  },
  "dependencies": {
    "@testing-library/dom": "^9.0.0",
    "@testing-library/jest-dom": "^5.16.5",
    "@testing-library/react": "^13.4.0",
    "@testing-library/user-event": "^13.5.0",
    "@types/jest": "^27.5.2",
    "@types/node": "^16.18.13",
    "@types/react": "^18.0.28",
    "@types/react-dom": "^18.0.11",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-scripts": "5.0.1",
    "sass": "^1.58.3",
    "typescript": "^4.9.5",
    "web-vitals": "^2.1.4"
  },
  "devDependencies": {
    "@storybook/addon-actions": "^6.5.16",
    "@storybook/addon-essentials": "^6.5.16",
    "@storybook/addon-interactions": "^6.5.16",
    "@storybook/addon-links": "^6.5.16",
    "@storybook/builder-webpack5": "^6.5.16",
    "@storybook/manager-webpack5": "^6.5.16",
    "@storybook/node-logger": "^6.5.16",
    "@storybook/preset-create-react-app": "^4.1.2",
    "@storybook/react": "^6.5.16",
    "@storybook/testing-library": "^0.0.13",
    "babel-plugin-named-exports-order": "^0.0.2",
    "prop-types": "^15.8.1",
    "webpack": "^5.75.0",
    "@rollup/plugin-babel": "^6.0.3",
    "@rollup/plugin-commonjs": "^24.0.1",
    "@rollup/plugin-json": "^6.0.0",
    "@rollup/plugin-node-resolve": "^15.0.1",
    "rollup-plugin-peer-deps-external": "^2.2.4",
    "rollup-plugin-scss": "^4.0.0",
    "rollup-plugin-typescript2": "^0.34.1"
  },

  // 配置脚本命令
  "scripts": {
    // ......
    "rollup-build": "rimraf build/* && rollup -c"
  },
  // ......
}

😃 常用 package.json 字段配置说明

😃 dependencies、devDependencies和peerDependencies区别

在项目中安装npm包的时候,会安装dependencies中的所有依赖,而不会安装devDependencies中的依赖。

作用:确保打包后的程序正常运行的同时,不占用项目体积。

5. 修改 tsconfig.json 配置

{
  "compilerOptions": {
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "declaration": true, // 是否输出声明文件
    "declarationDir": "build/types" // 声明文件输出地址
  },
  "include": [
    "src"
  ],
  "exclude": [
    "node_modules",
    "src/**/*.stories.tsx",
    "src/**/*.test.tsx"
  ]
}

默认 ts 配置没有开启 declaration 字段,只有当 declaration: true 时才会打包输出 .d.ts 声明文件。

6. 运行自定义打包脚本

npm run rollup-build

7. 发布 npm 包

第一次发包:在根目录下,运行npm adduser,输入你的账号密码及email,如果开启了OTP验证则需要输入一次你的OTP密码。

不是第一次发包:运行npm login命令登陆后发包。

npm publish

包名及版本号在 package.json 中配置。

若发布报错,很有可能是包名或版本号与存在,请尝试修改包名和版本号。

jtwang7 commented 1 year ago

发布 TS 编写的 NPM 包注意事项

TS编译器寻找声明文件路径

当你使用 npm 包的形式去引入模块时,IDE 往往会提示你缺少 ts 文件。

import foo from 'foo'

这是因为程序运行时,会根据以下路径搜索 npm 包对应的 types 声明文件:

node_modules/@types/foo
node_modules/foo/index.d.ts
node_modules/foo/package.json 当中定义的typing或者types2个字段中的一个
node_modules/foo/package.json 当中定义的main入口文件相同文件名的声明文件,例如index.js / index.d.ts,如果typings或者types存在的话,是不会去找main入口当中同文件名的声明文件的

若所有搜索路径均遍历还是未找到文件,则程序报错。因此,在打包发布时,要明确 .d.ts 文件的打包输出路径,确保其路径可以通过以上途径查询。


如果这个 npm 包没有提供声明文件,那么这个时候就需要自己去写声明文件。推荐大家在自己项目目录里面单独建一个types文件夹来存放相关的声明文件。但是依据上面的规则,TS是没法找到我们给这个 npm 包书写的声明文件的。所以需要通过tsconfig.json文件进行指定:

{
  "compilerOptions": {
    "baseUrl": "./",  // 解析的基础目录
    "paths": {
      "*": ["types/*"]
    }
  }
}

通过这样的配置后(具体有关baseUrl和paths字段的配置请参考文档),foo模块的声明文件除了会去node_modules目录下找,还会去当前项目的types/foo目录下去找:

projectRoot
├── src
│   └── index.ts
├── types
│   └── foo
│       └── index.d.ts
└── tsconfig.json

发布方式

tsconfig.json 配置

{
  "compilerOptions": {
    // ......
    "declaration": true,
    "declarationDir": "build/types"
  },
  // ......
}

package.json 配置

{
  "types": "build/types/index.d.ts",
  // ......
}
jtwang7 commented 1 year ago

peerDependencies 详解

peerDependencies 在我们进行一些插件开发的时候会经常用到,比如 jQuery-ui 的开发依赖于 jQuery,html-webpack-plugin 的开发依赖于 webpack等。 假设现在有一个 helloWorld 工程,已经在其 package.json 的 dependencies 中声明了 packageA,有两个插件 plugin1 和 plugin2 他们也依赖 packageA,如果在插件中使用 dependencies 而不是 peerDependencies 来声明 packageA,那么 $ npm install 安装完 plugin1 和 plugin2 之后的依赖图是这样的:

.
├── helloWorld
│   └── node_modules
│       ├── packageA
│       ├── plugin1
│       │   └── nodule_modules
│       │       └── packageA
│       └── plugin2
│       │   └── nodule_modules
│       │       └── packageA

从上面的依赖图可以看出,helloWorld 本身已经安装了一次packageA,但是因为因为在 plugin1 和 plugin2 中的 dependencies 也声明了 packageA,所以最后 packageA 会被安装三次,有两次安装是冗余的。 🔥 而 peerDependency 就可以避免类似的核心依赖库被重复下载的问题。

如果在 plugin1 和 plugin2 的 package.json 中使用 peerDependency 来声明核心依赖库,例如:

plugin1/package.json

{
  "peerDependencies": {
    "packageA": "1.0.1"
  }
}
plugin2/package.json

{
  "peerDependencies": {
    "packageA": "1.0.1"
  }
}

在主系统中声明一下 packageA:

helloWorld/package.json

{
  "dependencies": {
    "packageA": "1.0.1"
  }
}

此时在主系统中执行 $ npm install 生成的依赖图就是这样的:

.
├── helloWorld
│   └── node_modules
│       ├── packageA
│       ├── plugin1
│       └── plugin2

可以看到这时候生成的依赖图是扁平的,packageA 也只会被安装一次。

🔥 因此我们总结下在插件使用 dependencies 声明依赖库的特点: