Rollup

在本章节,我们将基于上一章已经搭建好的Monorepo项目结构,来介绍如何进行rollup打包。

入口文件

在这一小节,我们先给每个packages添加一个入口文件,并添加一些必要的代码,以方便后续的测试。

首先,在packages/src/shared目录下添加index.js入口文件,并撰写如下代码:

export const add = (a, b) => {
  return a + b
}
export const reduce = (a, b) => {
  return a - b
}

接下来,在packages/reactivity/src目录下添加index.js入口文件,并撰写如下代码:

import { add } from '@MyVue/shared'

export const reactive = () => {
  return add(1, 2)
}

最后,在packages/compiler/src目录下添加index.js入口文件,并撰写如下代码:

import { reactive } from '@MyVue/reactivity'
import { reduce } from '@MyVue/shared'

export const transform = () => {
  const result1 = reactive()
  const result2 = reduce(2, 1)
  return result1 + result2
}

在添加完三个入口文件后,完整的目录结构如下所示:

|-- monorepo-demo              
|   |-- packages                  # packages目录
|   |   |-- compiler              # compiler子包
|   |   |   |-- src               # compiler源码目录
|   |   |   |   |-- index.js      # compiler源码入口文件
|   |   |   |-- package.json      # compiler子包特有的依赖
|   |   |-- reactivity            # reactivity子包
|   |   |   |-- src               # reactivity源码目录
|   |   |   |   |-- index.js      # reactivity源码入口文件
|   |   |   |-- package.json      # reactivity子包特有的依赖
|   |   |-- shared                # shared子包
|   |   |   |-- src               # shared源码目录
|   |   |   |   |-- index.js      # shared源码入口文件
|   |   |   |-- package.json      # shared子包特有的依赖
|   |-- package.json              # 所有子包都公共的依赖

依赖安装

因为Vue3采用的是rollup进行打包,所以我们需要先安装rollup相关的包。

TIP

-D--save-dev的缩写,表示依赖安装到devDependencies上,-wpnpm的参数,表示依赖安装到根目录。

# 安装rollup
$ pnpm install rollup -D -w

# 安装rollup插件
$ pnpm install @rollup/plugin-json@4.1.0 @rollup/plugin-node-resolve@13.0.6 -D -w

# 安装execa
$ pnpm install execa@5.1.1 -D -w
  • rollup是一个类似于webpack的打包工具,如果你还不是特别了解rollup,你可以点击Rollup官网open in new window去了解更多。
  • @rollup/plugin-json是一个能让我们从json文件中导入数据的插件。
  • @rollup/plugin-node-resolve是一个能让我们从node_modules中引入第三方模块的插件。
  • execa是一个能让我们手动执行脚本命令的一个工具。

依赖全部安装完毕后,根目录packages.json文件的devDependencies信息如下:

"devDependencies": {
  "@rollup/plugin-json": "4.1.0",
  "@rollup/plugin-node-resolve": "13.0.6",
  "execa": "5.1.1",
  "rollup": "^2.61.1"
}

功能拆分以及实现

脚本命令

在实现之前,我们先在根目录下创建scripts目录,并新建一个build.js文件,其内容如下:

// scripts/build.js
console.log('build.js')

然后,在根目录package.json文件中添加打包命令,如下:

"scripts": {
  "build": "node scripts/build.js"
}

最后,在控制台中执行build命令,可以在终端成功看到打印内容:

# 执行命令
$ pnpm run build

# 输出内容
node scripts/build.js
build.js

打包

现在,我们思考一个问题:因为我们packages目录下可能会存在很多个子包,所以我们需要为每一个子包都执行一次打包命令,并输出dist到对应的目录下。

基于以上问题,我们将可能面临的问题进行拆分:

  • 如何准确识别出所有的子包?

可以采用Node中的fs模块去读packages目录下的所有子文件夹/文件,然后保留所有文件夹就是我们的所有子包,实现代码如下:

// script/build.js
const fs = require('fs')

const pkgs = fs.readdirSync('packages').filter(p => {
  return fs.statSync(`packages/${p}`).isDirectory()
})

console.log(pkgs)

代码介绍:readdirSync()返回指定目录下所有文件名称组成的数组,statSync()isDirectory()返回指定文件的详细信息对象,isDirectory()方法返回当前文件是否为文件夹。

在撰写完以上代码后,我们再次执行打包命令,可以看到如下打印信息。

# 执行打包命令
$ pnpm run build

# 输出信息
node scripts/build.js
[ 'compiler', 'reactivity', 'shared' ]
  • 如何使用execa进行一次打包命令?

假设现在要给packages/shared打包,可以先这样做:

const build = async (pkg) => {
  await execa('rollup', ['-c', '--environment', `TARGET:${pkg}`], { stdio: 'inherit' })
}
build('shared')

以上execa执行的命令,相当于:

$ rollup -c --environment TARGET:shared

命令解读:-c代表制定rollup配置文件,如果其后没有跟文件名,则默认取根目录下的rollup.config.js文件。--environment表示注入一个环境变量,在我们的打包命令中注入了一个TARGET,可以使用process.env.TARGET取出来,其值为shared

现在,我们在根目录下新建rollup.config.js文件,并撰写如下代码:

const pkg = process.env.TARGET
console.log(pkg)

然后,再次运行打包命令:

TIP

因为rollup.config.js没有导出任何东西,所以运行报错是正常的。

# 执行打包命令
$ pnpm run build

# 输出信息
node scripts/build.js
[ 'compiler', 'reactivity', 'shared' ]
shared
...省略错误信息
  • 如何批量执行打包命令?

有了shared的打包经验,我们就可以实现给所有子包都打包,其实现代码如下:

const runParallel = (targets, buildFn) => {
  const res = []
  for (const target of targets) {
    res.push(buildFn(target))
  }
  return Promise.all(res)
}
const build = async (pkg) => {
  await execa('rollup', ['-c', '--environment', `TARGET:${pkg}`], { stdio: 'inherit' })
}
runParallel(pkgs, build)

再次执行打包命令,输出结果如下:

TIP

因为rollup.config.js没有导出任何东西,所以运行报错是正常的。

# 打包命令
$ pnpm run build

# 输出结果
[ 'compiler', 'reactivity', 'shared' ]
compiler
reactivity
shared
  • 如何配置rollup?

根据rollup的基础知识,我们知道需要提供inputoutput以及plugin等配置,可以撰写如下代码:

// rollup.config.js
const json = require('@rollup/plugin-json')
const { nodeResolve } = require('@rollup/plugin-node-resolve')
const pkg = process.env.TARGET
const createConfig = (name) => {
  return {
    input: 'src/index.js',
    output: {
      name,
      file: `dist/${name}.esm.js`,
      format: 'esm'
    },
    plugins: [
      json(),
      nodeResolve()
    ]
  }
}
module.exports = createConfig(pkg)

先别着急运行打包命令,因为以上代码还存在一些问题:
a. inputfile的路径不正确,需要定义一个resolve函数来表示当前package包的路径。
b. output.name的值,我们希望是VueXxx而不是xxx
c. format方式希望能够支持ESMCommonJsUMD这三种规范。

先来解决第一个问题,我们定义的resolve函数如下:

// rollup.config.js 新增代码
const path = require('path')
const pkg = process.env.TARGET
const resolve = (p) => {
  return path.resolve(`${__dirname}/packages/${pkg}`, p)
}
console.log(resolve('src/index.js'))

假如此时运行打包命令,可以得到如下输出结果:

xxxxxxxx\monorepo-demo\packages\shared\src\index.js
xxxxxxxx\monorepo-demo\packages\compiler\src\index.js
xxxxxxxx\monorepo-demo\packages\reactivity\src\index.js  

既然resolve函数已经定义好了,那么修改rollup.config.js文件后完整代码如下:

const path = require('path')
const json = require('@rollup/plugin-json')
const { nodeResolve } = require('@rollup/plugin-node-resolve')
const pkg = process.env.TARGET
const resolve = (p) => {
  return path.resolve(`${__dirname}/packages/${pkg}`, p)
}
const createConfig = (name) => {
  return {
    input: resolve('src/index.js'),
    output: {
      name,
      file: resolve(`dist/${name}.esm.js`),
      format: 'esm'
    },
    plugins: [
      json(),
      nodeResolve()
    ]
  }
}
module.exports = createConfig(pkg)

接下来解决第二个问题,既然要自定义名字,那么可以选择在当前包的package.json文件中去定义,我们以packages/shared为例:

// packages/shared目录下的packages.json
{
  "name": "@MyVue/shared",
  "buildOptions": {
    "name": "VueShared"
  },
  ...省略其他
}

在所有package.json文件都修改完毕后,我们需要去读取这个配置,代码如下:

// rollup.config.js文件
const { buildOptions } = require(resolve('package.json'))

接着,在createConfig方法中去修改output.name的值:

const createConfig = (name) => {
  return {
    output: {
      name: buildOptions.name,
      ...省略其他
    },
    ...省略其他
  }
}

最后解决第三个问题,根据前面配置name的思路,可以在buildOptions中去定义另外一个属性formats,同样以shared为例:

// packages/shared目录下的packages.json
{
  "name": "@MyVue/shared",
  "buildOptions": {
    "name": "VueShared",
    "formats": [
      "esm",
      "cjs"
    ]
  },
  ...省略其他
}

为了更好的进行区分打包的产物,我们在shared配置["esm", "cjs"],而对于reactivitycompiler配置["esm", "cjs", "umd"]

在所有formats配置完毕后,我们再次修改rollup.config.js,完整代码如下:

const path = require('path')
const json = require('@rollup/plugin-json')
const { nodeResolve } = require('@rollup/plugin-node-resolve')

const pkg = process.env.TARGET
const resolve = (p) => {
  return path.resolve(`${__dirname}/packages/${pkg}`, p)
}
const { buildOptions } = require(resolve('package.json'))
const formatMap = {
  esm: {
    file: resolve(`dist/${pkg}.esm.js`),
    format: 'esm'
  },
  cjs: {
    file: resolve(`dist/${pkg}.cjs.js`),
    format: 'cjs'
  },
  umd: {
    file: resolve(`dist/${pkg}.js`),
    format: 'umd'
  }
}
const createConfig = (output) => {
  output.name = buildOptions.name
  return {
    input: resolve('src/index.js'),
    output,
    plugins: [
      json(),
      nodeResolve()
    ]
  }
}
const configs = buildOptions.formats.map(format => createConfig(formatMap[format]))
module.exports = configs

接着,我们来运行打包命令:

# 运行打包命令
$ pnpm run build

# 输出信息
created packages\shared\dist\shared.esm.js in 18ms
created packages\shared\dist\shared.cjs.js in 9ms
...省略
created packages\compiler\dist\compiler.esm.js in 18ms
created packages\compiler\dist\compiler.cjs.js in 9ms
created packages\compiler\dist\compiler.js in 22ms

最后,如果要想发布package,还需要有一个默认的入口文件,我们以packages/shared为例,在其目录下新建index.js,并撰写如下代码:

module.exports = require('./dist/shared.cjs.js')

你可能会很疑惑,为什么只导出CommomJs规范的文件,ESMUMD规范的文件又该如何导出呢?

其实,这两个规范可以在其目录下的package.json文件中去配置导出,以reactivity为例:

{
  "name": "@MyVue/reactivity",
  "main": "index.js",                 // UMD规范导出
  "module": "dist/reactivity.esm.js", // ESM规范导出
  "unpkg": "dist/reactivity.js"       // UMD规范配合CDN
}

在所有package.json文件配置好后,此时项目完整目录如下:

|-- monorepo-demo              
|   |-- packages                  # packages目录
|   |   |-- compiler              # compiler子包
|   |   |   |-- dist              # compiler打包产物目录
|   |   |   |-- src               # compiler源码目录
|   |   |   |   |-- index.js      # compiler源码入口文件
|   |   |   |-- index.js          # compiler包入口文件
|   |   |   |-- package.json      # compiler子包特有的依赖
|   |   |-- reactivity            # reactivity子包
|   |   |   |-- dist              # reactivity打包产物目录
|   |   |   |-- src               # reactivity源码目录
|   |   |   |   |-- index.js      # reactivity源码入口文件
|   |   |   |-- index.js          # reactivity包入口文件
|   |   |   |-- package.json      # reactivity子包特有的依赖
|   |   |-- shared                # shared子包
|   |   |   |-- dist              # shared打包产物目录
|   |   |   |-- src               # shared源码目录
|   |   |   |   |-- index.js      # shared源码入口文件
|   |   |   |-- index.js          # shared包入口文件
|   |   |   |-- package.json      # shared子包特有的依赖
|   |-- package.json              # 所有子包都公共的依赖

至此、rollup打包这一小节已经全部介绍完毕了,你可以点击monorepo-demoopen in new window仓库下载源码去了解更多。

你也可以基于此目录结构进行扩展更多我们还没有实现的功能,例如:区分开发环境和生产环境支持typescript生产环境开启压缩以及ES6代码转义等。

最后更新时间:
贡献者: wangtunan