使用 Rollup 开发 React 组件库

使用 Rollup 开发 React 组件库

说明

Rollup 是一个 JavaScript 模块打包器,他可以静态分析代码中的 import 并排除任何未实际使用的代码,可以极大的缩小项目(Tree-shaking), 与 Webpack 相比,Rollup 更多的被用于类库的开发

组件库设计要求

  1. 使用 rollup 做模块开发以及打包
  2. 打包后的格式为 es 模块
  3. Code Splitting 代码分割
  4. 第三方包排除打包 react、antd 、lodash 等
  5. 样式文件抽离
  6. 借助 babel-plugin-import 实现组件的按需引入
  7. 使用 Nexus 搭建私有仓库

rollup 配置文件简介

{
  // 入口 可以是一个字符串,也可以是对象
  input: { 
    index: 'src/index.js',
  },
  // 出口
  output: {
    dir: 'es', // 可以是 dir 表示输出目录 也可以是 file 表示输出文件
    format: 'es', // 输出的格式 可以是 cjs commonJs 规范 | es es Module 规范 | iife 浏览器可引入的规范
    sourceMap: true,
    entryFileNames: '[name]/index.js',
    exports: 'named'
  },
  // 是否开启代码分割
  experimentalCodeSplitting: true,
  // 需要引入的插件
  plugins: [
    clear({
      targets: ['es']
    })
  ],
  // 将模块视为外部模块,不会打包在库中
  external: id => external.some(e => id.indexOf(e) === 0),
  // 文件监听
  watch: {
    include: 'src/**',
    clearScreen: true
  }
}

使用到的插件

  1. rollup-plugin-clear 做打包前的文件夹清理
  2. rollup-plugin-postcss 样式文件编译处理
  3. rollup-plugin-eslint 代码规范检查
  4. rollup-plugin-flow flow 相关类型检查代码清理
  5. rollup-plugin-babel 编译
  6. rollup-plugin-commonjs | rollup-plugin-node-resolve 帮助查找以及转化外部模块

主要功能实现

1. 如何排除,不打包第三方包

主要通过 rollupConfig.external 配置项实现,但是需要 babel 做一些辅助处理

  1. external 定义那些包需要排除
/*
    external 是一个从外部传入的数组表示不需要被打包的包
    通过判断当前处理的包的名称是否已数组中的包名开头,来将其排除
*/ 
rollupConfig.external = id => external.some(e => id.indexOf(e) === 0);
  1. 配置 rollup-plugin-babel 插件不编译 node_modules 下代码
plugin: [
    babel({
        exclude: 'node_modules/**', // 只编译源代码
        runtimeHelpers: true
    })
]
  1. .babelrc 中使用 babel-plugin-external-helpers 插件协助排除
{
  "presets": [
    ["env", {
      "modules": false,
      "targets": {
        "node": "current",
        "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
      }
    }],
    "flow",
    "react",
    "stage-2"
  ],
  "plugins": [
    "external-helpers", // 这里
    ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": true }]
  ]
};

2. 如何实现 Code-splitting 代码分割

rollup 提供代码分割功能,主要原理是配置多入口文件,这样每个入口文件都会对应一个分割包,各个分割包的共有部分会被提取为 chunk

  1. 获取要拆分的模块
// 通过 mode 接口拿到 src/components 下的所有文件夹名作为打包后的模块
const fs = require('fs');
const path = require('path');
const componentDir = 'src/components';
const cModuleNames = fs.readdirSync(path.resolve(componentDir));
const cModuleMap = cModuleNames.reduce((prev, name) => {
  prev[name] = `${componentDir}/${name}/index.js`;

  return prev;
}, {});

export default cModuleMap;
  1. 定义多入口
{
    input: {
        index: 'src/index.js',
        ... cModuleMap
    }
}
  1. 出口处设置输出到一个 dir 而不是 file
{
    output: {
        dir: 'es',
        format: 'es',
        entryFileNames: '[name]/index.js', // 输出文件名
        exports: 'named'
    }
}
  1. 开启 rollup code-splitting
{
    experimentalCodeSplitting: true
}

3. 如何在 Code splitting 的情况下实现样式的按包分离

通过 rollup-plugin-postcss 可以实现 样式文件的抽离,但是只能抽离为一个包,这对组件库组件样式的按需引入是不利的

所以使用了一个折中的方式:鉴于 rollup 的配置文件既可以是一个对象也可以是一个数组,使用数组时可以依次执行其中的每一项配置,由此,我们样式抽离与js 的 分模块打包分开处理,其中一个配置负责 js 的打包,其余配置负责样式的抽离,如下:

  1. 配置文件入口处根据环境判断打包方式
/*
    rollup 配置文件
*/
import pkg from './package.json';
import createModuleConfig from './config/rollupRenderModuleConfig';
import createStyleConfig from './config/rollupRenderStyleConfig';
import cModuleMap from './config/obtainComponentsName';

const external = Object.keys(pkg.dependencies);
const isDev = process.env.NODE_ENV === 'development';

/*
    dev 情况下不做样式抽离
    其他环境下,除了基本的 js 打包外,遍历要拆分的模块,分别生成一个配置项,在这个配置项中处理各自的样式分离
*/
const rollupConfig = isDev
  ? createModuleConfig(cModuleMap, external, isDev)
  : [
    createModuleConfig(cModuleMap, external, isDev)
  ].concat(
    Object.keys(cModuleMap).map(moduleName => createStyleConfig(moduleName, external))
  );

export default rollupConfig;
  1. createModuleConfig 方法返回一个 rollupConfig ,在这里定义 js 的打包
/*
    rollup 配置文件
*/
import postcss from 'rollup-plugin-postcss';
import eslint from 'rollup-plugin-eslint';
import clear from 'rollup-plugin-clear';
import basePlugin from './rollupBasePluginConfig';

const createModuleConfig = (cModuleMap, external, isDev) => ({
  input: {
    index: 'src/index.js',
    ...cModuleMap
  },
  output: {
    dir: 'es',
    format: 'es',
    sourceMap: true,
    entryFileNames: '[name]/index.js',
    exports: 'named'
  },
  experimentalCodeSplitting: true,
  plugins: [
    clear({
      targets: ['es']
    }),
    
    postcss({
      // modules: true, // 增加 css-module 功能
      extensions: ['.less', '.css'],
      use: [
        ['less', {
          javascriptEnabled: true
        }]
      ],
      inject: isDev, // dev 环境下的 样式是入住到 js 中的,其他环境不会注入
      extract: false // 无论是 dev 还是其他环境这个配置项都不做 样式的抽离
    }),

    eslint({
      include: ['src/**/*.js']
    }),

    ...basePlugin
  ],
  // 将模块视为外部模块,不会打包在库中
  external: id => external.some(e => id.indexOf(e) === 0),
  ...(isDev ? {watch: {
    include: 'src/**',
    clearScreen: true
  }} : {})
});

export default createModuleConfig;

  1. createStyleConfig 也返回一个 rollupConfig 在这里定义 style 的编译输出
import postcss from 'rollup-plugin-postcss';
import clear from 'rollup-plugin-clear';
import basePlugin from './rollupBasePluginConfig';

const createStyleConfig = (moduleName, external) => ({
  input: `src/components/${moduleName}/index.js`,
  output: {
    file: `garbage/${moduleName}.js`,
    format: 'es',
  },
  plugins: [
    clear({
      targets: ['garbage']
    }),
    // css 处理,暂时没有加插件
    postcss({
      // modules: true, // 增加 css-module 功能
      extensions: ['.less', '.css'],
      use: [
        ['less', {
          javascriptEnabled: true
        }]
      ],
      // 样式输出到 createModuleConfig 创建的模块文件夹下
      extract: `es/${moduleName}/style/index.css` 
    }),

    ...basePlugin
  ],
  external: id => external.some(e => id.indexOf(e) === 0),
});

export default createStyleConfig;

4. 按需引入组件实现

参考了 antd 的按需引入方式:通过 babel-plugin-import 插件,在 babel 运行时,将类似 import { ModuleName } from 'libiaryName'; 的代码转化为组件所在的路径,这样实际引用的就是这个组件的模块而不是整个 Library

// babel 前
import { ModuleName } from 'libiaryName';

// babel 后
import ModuleName from 'libiaryName/ModuleName/index.js';
import 'libiaryName/ModuleName/style/index.css';

查看 babel-plugin-import 的文档,要实现同样的功能需要对打包后的文件夹做些规范,如下,具体代码查找上边 postcss 配置

// es/ 下

├── LoadingButton
│   ├── index.js
│   └── style
│       └── index.css
├── TestButton
│   ├── index.js
│   └── style
│       └── index.css
├── TestLodash
│   └── index.js
├── TestWeb
│   ├── index.js
│   └── style
│       └── index.css
├── chunk-3a85a70c.js
├── chunk-c05d6a54.js
└── index
    └── index.js

项目中引用 配置 babel

"plugins": [
    // babel 7 以下如下配置 第二个参数用数组
    ["import", [
      { "libraryName": "antd", "libraryDirectory": "es", "style": true },
      {
        "libraryName": "xxx-ui",
        "libraryDirectory": "es",
        "camel2DashComponentName": false,
        "style": true
      }
    ]]

    // babel 7 以上如下配置 可以写多个 import 配置
    ["import", { 
        "libraryName": "antd", "libraryDirectory": "es", "style": true 
    }],
    ["import", { 
        "libraryName": "xxx-ui", 
        "libraryDirectory": "es", 
        "camel2DashComponentName": false,
        "style": true 
    }]
  ]

注意 camel2DashComponentName 设置转换的文件夹格式 antd 默认是小写的带连字符的,我们这里用的是大写的驼峰命名

npm 线上发布

1. 创建 npm 账户

创建地址

2. login

npm login # 输入用户名 密码

3. publish

npm publish # 需要确保 package.json version 与上一个版本不一样

4. package.json 说明

有一些 字段需要注意

{
  "name": "xxx-ui", // 包名
  "version": "0.1.4", // 版本,每次都要不一样
  "main": "es/index/index.js", // 规范的,定义程序入口文件
  "module": "es/index/index.js", // 新的,定义使用 es 模块的入口文件
  "files": [ // npm publish 中约定可上传的文件夹
    "dist",
    "src",
    "es"
  ],
  "directories": { 
    "es": "es"
  },
  "publishConfig": { // 设置模块发布时发布到的镜像仓库地址,默认是你的 npm registry 指向的地址
    "registry": "http://localhost:8081/repository/xxx-ui/"
  }
}
  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值