使用rollup.js封装各项目共用的工具包 gdpg-utils,并发布到npm私库

2,254 阅读7分钟

image.png

Rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码。Rollup是基于ES6 的打包方案,而不是以前的特殊解决方案,如 CommonJS 和 AMD。与Webpack偏向于应用打包的定位不同,rollup.js更专注于Javascript类库打包,我们熟知的VueReact等诸多知名框架或类库都是通过rollup.js进行打包的。

一、选择rollup的理由?

webpackrollup在不同场景下,都能发挥自身优势作用。webpack对于代码分割和静态资源导入有着“先天优势”,并且支持热模块替换(HMR),而rollup并不支持。

所以当开发应用时可以优先选择webpack,但是rollup对于代码的Tree-shakingES6模块有着算法优势上的支持,若项目只需要打包出一个简单的包,并是基于ES6模块开发的,可以考虑使用rollup

webpack2.0开始就已经支持Tree-shaking,并在使用babel-loader的情况下还可以支持es6 module的打包。实际上,rollup已经在渐渐地失去了当初的优势了。但是它并没有被抛弃,反而因其简单的API、使用方式被许多库开发者青睐,如ReactVue等,都是使用rollup作为构建工具的。

二、封装各项目共用的工具包gdpg-utils

这里以封装各项目共用的工具包 gdpg-utils 为例子讲解rollup的使用,流程细节不细讲,都已文件代码的形式展示,有需要可结合官网文档配置理解。

1.最终的目录结构:

// 目录由 tree-node-cli 生成
npm i tree-node-cli -g
tree -L 4 -I "node_modules" > dir.md
gdpg-utils
├── CHANGELOG.md
├── LICENSE
├── README.md
├── dist // 打包生产的目录
│   ├── gdpg-utils.common.js
│   ├── gdpg-utils.esm.js
│   ├── gdpg-utils.js
│   └── index.js
├── index.html // 测试用的html
├── package-lock.json
├── package.json
├── rollup.config.js // rollup 配置文件
├── scripts
│   └── publish.js // 推送到远程目录的脚本
├── src // 打包的代码
│   ├── index.ts
│   ├── modules // 模块文件夹
│   │   ├── array
│   │   │   └── index.ts
│   │   ├── brower
│   │   │   └── index.ts
│   │   ├── method
│   │   │   └── index.ts
│   │   ├── native
│   │   │   ├── android.ts
│   │   │   ├── index.ts
│   │   │   └── pc.ts
│   │   ├── number
│   │   │   └── index.ts
│   │   ├── object
│   │   │   └── index.ts
│   │   ├── storage
│   │   │   └── index.ts
│   │   ├── string
│   │   │   └── index.ts
│   │   └── tool
│   │       └── index.ts
│   └── typing.d.ts // ts模块描述文件
├── stats.html // rollup-plugin-visualizer 生成的包分析文件
└── tsconfig.json // typescript 配置
└── yarn.lock

1.1 package.json

{
  "name": "gdpg-utils",
  "version": "1.0.10",
  "description": "gdpg web js utils",
  "keywords": [
    "utils",
    "tool"
  ],
  "main": "dist/gdpg-utils.common.js",
  "module": "dist/gdpg-utils.esm.js",
  "browser": "dist/gdpg-utils.js",
  "scripts": {
    "dev": "rollup --config rollup.config.js --watch --environment ENV:dev",
    "build": "rollup --config rollup.config.js --environment ENV:prod",
    "pub": "node scripts/publish.js"
  },
  "author": "",
  "license": "ISC",
  "publishConfig": {
    "registry": "http://127.0.0.1:8081/repository/npm-hosted/"
  },
  "devDependencies": {
    "@babel/core": "^7.15.0",
    "@babel/preset-env": "^7.15.0",
    "@rollup/plugin-alias": "^3.1.5",
    "@rollup/plugin-babel": "^5.3.0",
    "@rollup/plugin-commonjs": "^20.0.0",
    "@rollup/plugin-json": "^4.1.0",
    "@rollup/plugin-node-resolve": "^13.0.4",
    "@rollup/plugin-typescript": "^8.2.5",
    "commander": "^8.1.0",
    "dayjs": "^1.10.6",
    "js-cookie": "^3.0.0",
    "lodash": "^4.17.21",
    "rollup": "^2.56.0",
    "rollup-plugin-dev": "^1.1.3",
    "rollup-plugin-livereload": "^2.0.5",
    "rollup-plugin-replace": "^2.2.0",
    "rollup-plugin-terser": "^7.0.2",
    "rollup-plugin-visualizer": "^5.5.2",
    "shelljs": "^0.8.4",
    "store": "^2.0.12",
    "tslib": "^2.3.0",
    "typescript": "^4.3.5"
  }
}

1.2 rollup.config.js

import typescript from '@rollup/plugin-typescript' // typescript插件
import json from '@rollup/plugin-json'; // 允许从json中导入数据
import nodeResolve from '@rollup/plugin-node-resolve' // 帮助寻找node_modules里的包
import commonjs from '@rollup/plugin-commonjs' // 将非ES6语法的包转为ES6可用
import babel from '@rollup/plugin-babel' // rollup 的 babel 插件,ES6转ES5
import dev from 'rollup-plugin-dev'; // 开启本地服务器
import livereload from 'rollup-plugin-livereload'; // 开启热更新
import {
  terser
} from 'rollup-plugin-terser';
import {
  visualizer
} from 'rollup-plugin-visualizer';

import pkg from './package.json'

export default {
  input: "src/index.ts", // 入口文件
  output: [{ // 不同类型的出口文件
      file: pkg.main,
      format: 'cjs', // CommonJS
      exports: 'auto'
    },
    {
      file: pkg.module,
      format: 'es', // ES模块文件
      exports: 'auto'
    },
    {
      file: pkg.browser,
      format: 'umd', // 通用模块定义,以amd,cjs和iife为一体
      name: 'gdpg-utils',
      exports: 'auto'
    },
  ],
  plugins: [
    json(),
    typescript(),
    nodeResolve({
      browser: true,
      main: true
    }),
    commonjs(),
    babel({
      exclude: 'node_modules/**', // 忽略 node_modules
      babelHelpers: true, // 开启体积优化
    }),
    process.env.ENV === 'prod' ? terser() : null,
    process.env.ENV === 'dev' ? livereload() : null,
    process.env.ENV === 'dev' ? dev({
      port: 8888,
      dirs: '',
    }) : null,
    process.env.ENV === 'prod' ? visualizer() : null,
  ],
  watch: {
    exclude: 'node_modules/**',
    include: 'src/**'
  }
};

1.3 format字段

这里的format字段大家看了可能不太理解,尤其是里面的cjs代表什么意思;由于JS有多种模块化方式,Rollup可以针对不同的模块规范打包出不同的文件,它有以下五种选项:

  • amd: 异步模块定义,用于像RequireJS这样的模块加载器
  • cjs:CommonJS,适用于 Node 和 Browserify/Webpack
  • es:ES模块文件
  • iife:自执行模块,适用于浏览器环境script标签
  • umd:通用模块定义,以amd,cjs和iife为一体

1.4 plugins字段

插件拓展了Rollup处理其他类型文件的能力,它的功能有点类似于Webpack的loaderplugin的组合;不过配置比webpack中要简单很多,不用逐个声明哪个文件用哪个插件处理,只需要在plugins中声明,在引入对应文件类型时就会自动加载。项目使用了下面这些插件

import typescript from '@rollup/plugin-typescript' // typescript插件
import json from '@rollup/plugin-json'; // 允许从json中导入数据
import nodeResolve from '@rollup/plugin-node-resolve' // 帮助寻找node_modules里的包
import commonjs from '@rollup/plugin-commonjs' // 将非ES6语法的包转为ES6可用
import babel from '@rollup/plugin-babel' // rollup 的 babel 插件,ES6转ES5
import dev from 'rollup-plugin-dev'; // 开启本地服务器
import livereload from 'rollup-plugin-livereload'; // 开启热更新

2. src目录的工具代码

2.1 src/index.ts

import * as array from './modules/array'
import * as brower from './modules/brower'
import * as method from './modules/method'
import * as number from './modules/number'
import * as object from './modules/object'
import * as string from './modules/string'
import * as native from './modules/native'
import * as tool from './modules/tool'

export default {
  ...array,
  ...brower,
  ...method,
  ...number,
  ...object,
  ...string,
  ...native,
  ...tool,
}

2.2 modules文件夹内容较多,这里简单举src/modules/number/index.tssrc/modules/tool/index.ts的例子

// src/modules/number/index.ts

/**
 * @description 生成指定范围的随机小数
 */
export const randomNumberInRange = (min: number, max: number) => Math.random() * (max - min) + min;

/**
 * @description 计算数组或多个数字的总和
 */
export const sum = (...arr) => [...arr].reduce((acc, val) => acc + val, 0);
// src/modules/tool/index.ts

/**
 * @description 深拷贝
 */
export function deepClone(data: any): any {
  const type = Object.prototype.toString.call(data);
  let result: {
    [key: string]: any
  };
  if (type === '[object Object]') {
    result = {};
  } else if (type === '[object Array]') {
    result = [];
  } else {
    return data;
  }
  Object.keys(data).forEach((key) => {
    const value = data[key];
    result[key] = deepClone(value);
  });
  return result;
}

/**
 * @description async await 优雅处理方式
 */
export const awaitWrap = <T, U = any>(promise: Promise<T>): Promise<[U | null, T | null]> => promise
  .then<[null, T]>((data: T) => [null, data])
  .catch<[U, null]>((err) => [err, null]);

/**
 * @description 对象数组去重
 */
export const unique = (sourceArr: Array<any>, data: Array<any>, key: string): Array<any> => {
  const arr = [...data, ...sourceArr];
  return arr.reduce((acc: Array<any>, cur: any) => {
    const ids = acc.map((item) => item[key]);
    return ids.includes(cur[key]) ? acc : [...acc, cur];
  }, []);
};

3.执行开发命令和打包命令

3.1 本地开发

npm run dev
// "dev": "rollup --config rollup.config.js --watch --environment ENV:dev"
// --config rollup.config.js
  • --config rollup.config.js // 使用 rollup.config.js 配置文件
  • --watch // 监控(监控范围为rollup.config.js的watch配置)src目录的变换,动态打包更新 dist 目录
  • --environment ENV:dev // 传递环境变量 ENV:dev index.html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>gdpg-utils test page</title>
</head>

<body>
  <h1>gdpg-utils</h1>
  <script type="module">
    import utils from '../dist/gdpg-utils.esm.js';
    console.log('randomNumberInRange %c⧭', 'color: #1d5673', utils.randomNumberInRange);
    console.log('deepClone %c⧭', 'color: #00bf00', utils.deepClone);
  </script>
</body>

</html>

关于调试: 我开发包的过程中用到了两种调试方式:

  • 方法1:直接通过通过 index.html 调试,(原理:借助rollup-plugin-devrollup-plugin-livereload搭建这种方式的调试环境) image.png
  • 方法2:通过npm link(软链接)在项目中调试正在开发的包

在包目录下执行npm link

在项目目录下执行npm link gdpg-utils

即可使用该包(执行npm unlink gdpg-utils可以删除包链接);

3.2 打包

npm run build

打包结果: image.png 包结构分析:未压缩时在37kb左右 image.png

3.3 推送到npm私库

推送脚本 scripts/publish.js

const path = require('path');
const shelljs = require('shelljs');
const program = require('commander');

const targetFile = path.resolve(__dirname, '../package.json');
const packagejson = require(targetFile);
const currentVersion = packagejson.version;
const versionArr = currentVersion.split('.');
const [mainVersion, subVersion, phaseVersion] = versionArr;

// 默认版本号
const defaultVersion = `${mainVersion}.${subVersion}.${+phaseVersion+1}`;

let newVersion = defaultVersion;

// 从命令行参数中取版本号
program
  .option('-v, --versions <type>', 'Add release version number', defaultVersion);

program.parse(process.argv);

if (program.versions) {
  newVersion = program.versions;
}

console.log('newVersion:', newVersion);

function publish() {
  shelljs.sed('-i', '"name": "ktools"', '"name": "@kagol/ktools"', targetFile);
  shelljs.sed('-i', `"version": "${currentVersion}"`, `"version": "${newVersion}"`, targetFile);
  shelljs.exec('npm run build');
  shelljs.exec('npm publish');
}

publish();
npm run pub

image.png

3.4 在vue项目中引入使用

image.png image.png image.png 包结构分析:代码压缩+gzip压缩后大小在5kb左右 image.png // TODO: 单元测试、文档预览、提交规范

三、Tree Shaking

由于Rollup本身支持ES6模块化规范,因此不需要额外配置即可进行Tree Shaking

四、代码分割

Rollup代码分割和Parcel一样,也是通过按需导入的方式;但是我们输出的格式format不能使用iife,因为iife自执行函数会把所有模块放到一个文件中,可以通过amd或者cjs等其他规范。

export default {
  input: "./index.ts",
  output: {
    //输出文件夹
    dir: "dist",
    format: "amd",
  },
};

这样我们通过import()动态导入的代码就会单独分割到独立的js中,在调用时按需引入;不过对于这种amd模块的文件,不能直接在浏览器中引用,必须通过实现AMD标准的库加载,比如Require.js

五、小结

通过对Rollup的使用介绍,我们发现它有以下优点:

  • 配置简单,打包速度快
  • 自动移除未引用的代码(内置tree shaking) 但是他也有以下不可忽视的缺点:
  • 开发服务器不能实现模块热更新,调试繁琐
  • 浏览器环境的代码分割依赖amd
  • 加载第三方模块比较复杂