如何开发一个组件库

·  阅读 228

项目结构

pp-react-ui
├── README.md
├── package-lock.json
├── package.json
└── src //项目源码
    └── components //组件目录
        ├── button
        │   ├── index.js
        │   └── style.css
        ├── hello
        │   ├── index.js
        │   └── style.css
        └── index.js
复制代码

实现组件

1、安装依赖

npm install react react-dom
复制代码

2、编写 button 组件

// src/components/button/index.js
import * as React from "react";
import "./style.css";

const Button = (props) => {
  const handleClick = () => {
    props.onClick && props.onClick();
  };

  return (
    <button className="pp-button" onClick={handleClick}>
      {props.children}
    </button>
  );
};

export default Button;

// src/components/button/style.css
.pp-button {
  border: none;
  padding: 10px 15px;
  font-size: 14px;
}
复制代码

3、编写 hello 组件

// src/components/hello/index.js
import * as React from "react";
import "./style.css";

const Hello = (props) => {
  return (
    <div className="pp-hello">
      hello <span className="pp-name">{props.name}</span>
    </div>
  );
};

export default Hello;

// src/components/hello/style.css
.pp-hello {
  font-size: 16px;
}
.pp-hello .pp-name {
  color: red;
}

复制代码

4、导出组件

// src/components/index.js

export { default as Button } from "./button";
export { default as Hello } from "./hello";

复制代码

使用 parcel 运行项目

1、安装依赖

npm install parcel-bundler --save-dev

npm install @babel/preset-react --save-dev
复制代码

2、创建 .babelrc 文件

{
  "presets": ["@babel/preset-react"]
}
复制代码

3、创建入口文件

// src/index.html
<html>
  <body>
    <div id="root"></div>
    <script src="./index.js"></script>
  </body>
</html>


// src/index.js
import React from "react";
import ReactDOM from "react-dom";

import { Button, Hello } from "./components";

const App = () => {
  return (
    <>
      <section>
        <Button onClick={() => alert('click')}>click me</Button>
      </section>
      <section>
        <Hello name="jack" />
      </section>
    </>
  );
};

ReactDOM.render(<App />, document.getElementById("root"));

复制代码

4、更新package.json

"scripts": {
   "dev": "parcel src/index.html",
},
复制代码

5、运行项目

npm run dev
复制代码

image.png

使用 rollup 打包项目

1、安装 rollup

npm install rollup --save-dev
复制代码

2、安装 rollup 插件

npm install @rollup/plugin-node-resolve --save-dev

npm install @rollup/plugin-commonjs --save-dev

npm install rollup-plugin-babel --save-dev

npm intall rollup-plugin-postcss --save-dev
复制代码

rollup插件

1、插件 @rollup/plugin-node-resolve

@rollup/plugin-node-resolve,告诉Rollup如何查找外部模块。如果引入第三方模块,会将第三方模块一起打包。

未使用 @rollup/plugin-node-resolve时,会报一个警告 image.png 我们打包出来的文件里,react并没有包含在内。

使用 @rollup/plugin-node-resolve 的配置:

// rollup.config.js
import resolve from '@rollup/plugin-node-resolve';

export default {
  input: 'src/main.js',
  output: {
    file: 'bundle.js',
    format: 'cjs'
  },
  plugins: [resolve()]
};
复制代码

2、@rollup/plugin-commonjs

@rollup/plugin-commonjs,将CommonJS转换为ES2015。

有些库公开了ES模块,您可以按原样导入这些模块。但目前,NPM上的大多数包都是以CommonJS模块的形式公开的。在此之前,我们需要先将CommonJS转换为ES2015,然后Rollup才能处理它们。

@rollup/plugin-commonjs插件就是这样做的。

请注意,大多数时候@rollup/plugin-commonjs应该在其他插件转换模块之前使用——这是为了防止其他插件做出破坏CommonJS检测的更改。这个规则的一个例外是Babel插件,如果你正在使用它,那么把它放在commonjs之前。

未使用 @rollup/plugin-commonjs时,打包会报错 image.png

使用 @rollup/plugin-commonjs 的配置:

// rollup.config.js
import resolve from '@rollup/plugin-node-resolve';
import commonjs from "@rollup/plugin-commonjs";

export default {
  input: 'src/main.js',
  output: {
    file: 'bundle.js',
    format: 'cjs'
  },
  plugins: [
   	commonjs(),
   	resolve()
  ]
};
复制代码

3、 rollup-plugin-postcss

rollup-plugin-postcss,rollup与postcss的无缝集成。

未使用rollup-plugin-postcss,打包会报错 image.png 使用 rollup-plugin-postcss 的配置:

// rollup.config.js
import resolve from '@rollup/plugin-node-resolve';
import commonjs from "@rollup/plugin-commonjs";
import postcss from 'rollup-plugin-postcss';

export default {
  input: 'src/main.js',
  output: {
    file: 'bundle.js',
    format: 'cjs'
  },
  plugins: [
   	commonjs(),
    resolve(),
    postcss()
  ]
};
复制代码

4、rollup-plugin-babel

rollup-plugin-babel,rollup与babel的无缝集成。

未使用 rollup-plugin-babel,打包会报错 image.png 使用 rollup-plugin-babel 的配置:

// rollup.config.js
import resolve from '@rollup/plugin-node-resolve';
import commonjs from "@rollup/plugin-commonjs";
import postcss from 'rollup-plugin-postcss';
import babel from "rollup-plugin-babel";

export default {
  input: 'src/main.js',
  output: {
    file: 'bundle.js',
    format: 'cjs'
  },
  plugins: [
  	babel({
      exclude: "node_modules/**",
      presets: ["@babel/preset-react"],
    }),
   	commonjs(),
    resolve(),
    postcss()
  ]
};
复制代码

rollup 配置文件

rollup.config.js

import commonjs from "@rollup/plugin-commonjs";
import resolve from "@rollup/plugin-node-resolve";
import postcss from 'rollup-plugin-postcss';
import babel from "rollup-plugin-babel";

export default {
  input: "./src/components/index.js", 
  output: [
    {
      format: "cjs",
      dir: "cjs",
      name: "index.js",
    },
    {
      format: "es",
      exports: "named",
      dir: "es",
      name: "index.js",
    },
  ],
  plugins: [
    babel({
      exclude: "node_modules/**",
      presets: ["@babel/preset-react"],
    }),
    commonjs(),
    resolve(),
    postcss()
  ],
};
复制代码

output核心参数:

// required (can be an array, for multiple outputs)
output: {
    // core output options
    dir,
    file,
    format, // required
    globals,
    name,
    plugins,
 }
复制代码

按组件打包

rollup.config.js

import commonjs from "@rollup/plugin-commonjs";
import resolve from "@rollup/plugin-node-resolve";
import postcss from "rollup-plugin-postcss";
import babel from "rollup-plugin-babel";
import path from "path";
import fs from "fs-extra";

const plugins = [
  babel({
    exclude: "node_modules/**",
    presets: ["@babel/preset-react"],
  }),
  commonjs(),
  resolve(),
  postcss(),
];

//获取所有组件
const getComponents = async () => {
  const componentsPath = path.join(__dirname, "src/components");

  //读取目录下的内容
  const files = await fs.readdir(componentsPath);
  // console.log(files); //[ 'button', 'hello', 'index.js' ]

  const components = await Promise.all(
    files.map(async (name) => {
      //组件目录
      const comPath = path.join(componentsPath, name);
      //组件入口文件
      const entry = path.join(comPath, "index.js");

      //获取当前文件信息
      const stat = await fs.stat(comPath);
      //不是目录,直接返回
      if (!stat.isDirectory()) {
        return null;
      }

      //判断文件是否存在
      const hasFile = await fs.pathExists(entry);
      if (!hasFile) {
        return null;
      }
      const result = { name, url: entry };
      // console.log(result);
      // {
      //   name: 'button',
      //   url: '/pp-ui/src/components/button/index.js'
      // }
      return result;
    })
  );
  return components;
};

export default async () => {
  //删除打包目录
  await fs.remove(path.join(__dirname, "es"));
  await fs.remove(path.join(__dirname, "cjs"));
  await fs.remove(path.join(__dirname, "dist"));

  //获取组件列表
  const components = await getComponents();

  //组件目录文件打包
  const componentList = components
    .filter((comp) => comp)
    .map(({ name, url }) => ({
      input: { [name]: url },
      output: [
        {
          format: "cjs",
          dir: "cjs",
          entryFileNames: "[name]/index.js",
          exports: "named",
        },
        {
          format: "es",
          dir: "es",
          entryFileNames: "[name]/index.js",
          exports: "named",
        },
      ],
      plugins,
    }));

  //组件入口文件打包
  const componetIndex = {
    input: "./src/components/index.js",
    output: [
      {
        format: "cjs",
        dir: "cjs",
        name: "index.js",
      },
      {
        format: "es",
        dir: "es",
        name: "index.js",
      },
    ],
    plugins,
  };

  return [...componentList, componetIndex];
};

复制代码

公共依赖不打包

const external = (id) => /^react|react-dom|classnames/.test(id);

export default {
  input: 'src/main.js',
  output: {
    file: 'bundle.js',
    format: 'cjs'
  },
  plugins: [],
  // indicate which modules should be treated as external
  external
};
复制代码

更新package.json

"peerDependencies": {
  "classnames": "^2.3.1",
  "react": "^17.0.2",
  "react-dom": "^17.0.2"
},
复制代码

按环境打包

1、安装 cross-env

npm install --save-dev cross-env
复制代码

2、生产环境 压缩 代码

npm install rollup-plugin-terser --save-dev
复制代码

3、更新 package.json

"scripts": {
    "build": "npm run build:prod",
    "build:dev": "cross-env BUILD_ENV=development rollup -c",
    "build:prod": "cross-env BUILD_ENV=production rollup -c"
 },
复制代码

4、更新rollup.config.js

import { terser } from 'rollup-plugin-terser';

const isProd = process.env.BUILD_ENV === 'production';

export default {
  input: 'src/main.js',
  output: {
    file: 'bundle.js',
    format: 'cjs'
  },
  plugins: [
  	isProd ? terser() : null
  ],
};
复制代码

提取CSS文件

import postcss from "rollup-plugin-postcss";

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

export default {
  input: "src/main.js",
  output: {
    file: "bundle.js",
    format: "cjs",
  },
  plugins: [
    isProd
      ? postcss({
          extract: true,
          minimize: true,
        })
      : postcss(),
  ],
};
复制代码

配置package.json

1、配置入口文件

{
	"main": "dist/index.js",
  "module": "es/index.js",
}
复制代码

pkg.main 字段指向的是commonjs模块; pkg.module 字段指向的是es6模块; ​

2、发布文件配置(files) ​

files 字段用于描述我们使用 npm publish 命令后推送到 npm 服务器的文件列表,如果指定文件夹,则文件夹内的所有内容都会包含进来

{
	 "files": [
    "/dist",
    "/es"
  ]
}
复制代码

聊聊 package.json 文件中的 module 字段 zhuanlan.zhihu.com/p/34164963

package.json 中的 Module 字段是干嘛的 github.com/sunyongjian…

package.json 文件详解 zhuanlan.zhihu.com/p/148089474

发布npm

1、注册账号 www.npmjs.com/signup

2、进入项目目录,执行 npm login,输入用户名和密码; image.png 登录成功会输出:

Logged in as pengjielee on https://registry.npmjs.org/
复制代码

3、执行 npm publish

npm常见发布错误

1、发布包名已存在 image.png 发布之前,可以先查找 www.npmjs.com/一下是否存在 ​

2、注册邮箱未核验

分类:
前端
分类:
前端