本文为稀土技术社区首发签约文章,14天内制止转载,14天后未获授权制止转载,侵权必究!

专栏上篇文章传送门:完结一个靠谱好用的全屏组件,顺手入门 Headless 组件

专栏下篇文章传送门:函数库Rollup构建优化

本节触及的内容源码可在vue-pro-components c6 分支找到,欢迎 star 支撑!

前言

本文是 基于Vite+AntDesignVue打造业务组件库 专栏第 7 篇文章【在发布组件库之前,你需求先把握构建和发布函数库】,聊聊怎样构建和发布一个函数库。

如上篇文章结语所述,开发组件发布可用的组件之间还隔着一条距离,这便是从开发环境到生产环境必经的路,也是组件库研发进程中最杂乱的部分。要跳过这条距离,就必须把握一些工程化能力。

可是,构建和发布组件库是一个较杂乱的体系化的工程,构建组件库不只要处理 js, ts,或许还要处理 jsx, tsx, 款式等内容,假如选用的开发结构是 Vue,你或许还需求处理 SFC 的 parse, transpile 等进程。总归,这中心会触及许多种 DSL(领域特定言语)的处理,还要注意各个工序的顺序问题,这听起来似乎不是很简略的一件事,容易让初学者摸不着头脑。

在发布组件库之前,你需求先把握构建和发布函数库

为了打破这种迷茫,咱们能够将构建整个组件库的作业拆解出来,挑选从某一个方向切入,由点到面逐个打破,终究形成构建组件库的全局思想。那么最适合作为咱们学习进口的当然是函数库的构建,由于它一般只触及 JS/TS,这是咱们最了解的领域。

构建函数库

为什么要做构建作业?

到到现在,咱们在本专栏中完结的一些组件/函数/Hook等内容都还停留在源码层面,根本上是以.ts, .tsx, .vue等方式存在的,而且咱们能够发现,package.json中的main进口都是index.ts

在发布组件库之前,你需求先把握构建和发布函数库

而在咱们的认知中,咱们用的一些常见的库,它们供给的main, module等进口一般是xxx.js,而不是用一个.ts文件作为进口。

这并不是说,不能把 TS 之类的源码发布到 npm 上并作为引证进口,实际上只要运用依靠的项目方把构建的流程打通,也不是不可行。可是关于项目方来说,我引证一个依靠,便是要用标准化的东西,拿来即用,假如你让我自己把构建流程做出来,那我或许就不想用了。

简略的库还好说,或许接入 Webpack 或者 Vite 之类的东西就搞定了。可是关于一些杂乱的库来说,从源码到输出标准化的制品会经历许多道工序,你不能寄希望于调用方把这个作业做了,因而库的保护者非常有必要做好构建作业。

做哪些构建作业?

一个典型的 npm 包,或许会在其package.json中包括以下关键字段:

{
  // ...省掉部分字段
  "main": "lib/index.js",
  "module": "es/index.js",
  "types": "types/index.d.ts",
  "unpkg": "dist/index.js",
  "jsdelivr": "dist/index.js",
  "files": [
    "dist",
    "lib",
    "es",
    "types"
  ],
  // ...剩下字段
}
  • lib 目录下输出的是契合 CJS 模块标准的产品,经过main字段指定。
  • es 目录下输出的是契合 ES 模块标准的产品,经过module字段指定。
  • types 目录用于放置类型声明文件,也能够经过@types/xxx来供给类型声明。
  • unpkg 和 jsdelivr 用于经过 cdn 拜访发布在 npm 上的 umd 内容。以我之前发布的一个进度条组件为例,你只要按这个格式去拜访,就能得到你发布的内容。
https://unpkg.com/vue-awesome-progress@1.9.7/dist/vue-awesome-progress.min.js

在发布组件库之前,你需求先把握构建和发布函数库

jsdelivr 也是相似的,只不过是途径前缀有点区别。

https://cdn.jsdelivr.net/npm/vue-awesome-progress@1.9.7/dist/vue-awesome-progress.min.js

在发布组件库之前,你需求先把握构建和发布函数库

相同地,以 vue-pro-components 这个包为例,之前解说简略发布流程时也发布到了 npm,因而也能够经过 cdn 拜访到。

https://unpkg.com/vue-pro-components@0.0.2/lib/vue-pro-components.js

在发布组件库之前,你需求先把握构建和发布函数库

由于先前没有写什么实际内容就以教程的方式发布了,纯属是浪费资源了。建议不要随意发布没有意义的包。

  • files 则是指定发布和装置时包括哪些文件或目录(支撑 glob pattern),合理的装备能够减少 publish 和 install 的资源数。假如不做任何装备,就会发布和装置整个工程,这实际上是一种浪费。

依据前面的叙述,咱们能够知道,一个函数库大体上要供给契合 ESM, CJS, UMD 模块标准的制品。

从 TS 源码到 ESM, CJS, UMD 等标准下的制品,其实便是对应打包构建的进程了。

怎样构建函数库?

先画个图罗列一下咱们要做什么作业:

在发布组件库之前,你需求先把握构建和发布函数库

再确定哪些作业是串行的,哪些作业能够并行做。

细心品味,不难想明白除了整理目录(dist, es, lib, types 等目录)的作业需求先行,其他的作业都能够并行履行(由于它们之间没有依靠关系)。所以,整个构建的使命流大约是这样的:

在发布组件库之前,你需求先把握构建和发布函数库

大约的流程梳理清楚后,就能够逐个完结使命,而且把所有使命有序安排起来。

在打包函数库这方面,rollup 是一个绝佳的挑选。

yarn add -DW rollup

为了安排使命流,咱们需求选用一个好用的东西,而 gulp 便是这个不贰之选。

yarn add -DW gulp

gulp 默许选用的是 CJS 模块标准,这是履行 Node 脚本时的常规操作。

而 Rollup 默许支撑 ES6 的装备写法,这是由于 Rollup Cli 内部会处理装备文件。

引证自 rollup 官网

Note: Rollup itself processes the config file, which is why we’re able to useexport defaultsyntax –the code isn’t being transpiled with Babel or anything similar, so you can only use ES2015 features that are supported in the version of Node.js that you’re running.

一个是 CJS,一个是 ESM,这让两者的结合呈现了一点阻止。还好,gulp 4.x 版本也供给了运用 ESM 编写使命的指导性文档,

在发布组件库之前,你需求先把握构建和发布函数库

而且推荐咱们选用gulpfile.babel.js来安排咱们的装备文件,这背后依靠了@babel/register,而@babel/register底层是用到了 NodeJS 的 require hook。

引证自 babel 官网

@babel/registeruses Node’srequire()hook system to compile files on the fly when they are loaded.

其他可选的方案还有 sucrase/register。

基于此,咱们能够做到统一运用 ESM 来安排构建流程。

整理目录

由于在开端新的构建作业之前或许存在上一次构建的产品,所以关于构建产生的 dist, es, lib, types 等目录,咱们需求将其整理干净,这本质上是文件操作,可是在 gulp 生态中有许多插件能够让咱们挑选,就没必要自己手撸一个文件整理的流程了。这儿咱们选用gulp-clean。

文件处理最重要的是把途径设置正确,不然一波相似rm -rf的操作,或许就真的啥都没了,特别是当你写完的代码还没提交到 git 时,一波指令行操作那便是血与泪的经验(本人亲身经历,二次撸码真的苦楚)。

咱们把常用的途径放在build/path.js中保护。

import { resolve } from "path";
// 工程根目录
export const ROOT_PATH = resolve(__dirname, "../");
// utils 包的根目录
export const UTILS_PATH = resolve(ROOT_PATH, "./packages/utils");

接着就能够写 gulpfile 了。

import { src } from "gulp";
import clean from "gulp-clean";
import { UTILS_PATH } from "./build/path";
// 待整理的方针目录
const ARTIFACTS_DIRS = ["dist", "es", "lib", "types"]
// 把整理的进程稍微封装下,便于各个子包都能用上
function cleanDir(dir = "dist", options = {}) {
    return src(dir, { allowEmpty: true, ...options }).pipe(clean({ force: true }))
}
// 暴露出整理 utils 包产品目录的办法
export const cleanUtils = cleanDir.bind(null, ARTIFACTS_DIRS, { cwd: UTILS_PATH })

咱们现在还没有完结打包进程,能够先加几个临时文件测验一下。

在发布组件库之前,你需求先把握构建和发布函数库

构建方针产品

构建作业便是 Rollup 的舞台了,咱们把各个构建的子使命用 Rollup 安排好后让 gulp 去调用即可。

咱们先看看 Rollup 会干什么,

Rollup is a module bundler for JavaScript which compiles small pieces of code into something larger and more complex, such as a library or application.

看这意思,应该是会把多个文件打包成一个 bundle。一个进口文件,引证了其他模块,模块下面或许还有引证其他的依靠,这会形成一个依靠图,终究依据 format 参数打包成一个契合指定模块标准的 bundle,这比较契合咱们的常规思想。可是,关于库开发者来说,我不只要打包出契合模块标准的内容,一般还要生成独立的文件,用于支撑按需加载等场景。就像 lodash,它有许多个东西函数,打包后除了供给 bundle,也会供给许多独立的 js 模块,咱们能够独自引证某一个模块,配合一些东西,还能做到按需引进。

在发布组件库之前,你需求先把握构建和发布函数库

构建 UMD bundle

凡事从易到难,咱们仍是先从最简略的生成 UMD bundle 开端。

由于咱们的源码是用 ts 写的,所以要引进一个插件@rollup/plugin-typescript。

进口文件就用packages/utils/src/index.ts即可,它引证了其他独立的模块,这样就能把 utils 的各个东西函数都打包到一起。

// packages/utils/src/index.ts
export * from './install'
export * from './fullscreen'

考虑到要用 gulp 集成,咱们选用的是 Rollup 供给的 Javascript API 来编写构建流程。

import { rollup } from 'rollup'
import rollupTypescript from '@rollup/plugin-typescript'
import { resolve } from 'path'
import { UTILS_PATH } from './path'
export const buildBundle = async () => {
    // 调用 rollup api 得到一个 bundle 目标
    const bundle = await rollup({
        input: resolve(UTILS_PATH, 'src/index.ts'),
        plugins: [rollupTypescript()],
    })
    // 依据 name, format. dir 等参数调用 bundle.write 输出到磁盘
    await bundle.write({
        name: 'VpUtils',
        format: 'umd',
        dir: resolve(UTILS_PATH, 'dist'),
        sourcemap: true
    })
}

接着,就能够把这个buildBundle函数集成到 gulp 中起来运用了。gulp 是支撑经过 Promise 来符号使命完结信号的,相同也能够用异步函数。

在发布组件库之前,你需求先把握构建和发布函数库

在发布组件库之前,你需求先把握构建和发布函数库

import { series, src } from "gulp";
// ...省掉其他代码
// 先 cleanUtils,再 buildBundle,经过 series 按顺序履行
export const buildUtils = series(cleanUtils, buildBundle);

测验一下作用,发现现已能够构建出契合 UMD 模块标准的产品了,第一小步算是迈出去了。

构建 ESM & CJS,支撑按需加载

接下来便是看怎样构建契合 ESM 和 CJS 标准的产品,一起要支撑多文件独立输出,以支撑按需加载。

要输出多个文件,其实能够考虑指定多个构建进口,以单个模块作为进口,就能输出这个模块对应的构建结果。Rollup 本身也支撑指定数组或目标方式的 input 参数作为多进口,这和 Webpack 也是相似的。

在发布组件库之前,你需求先把握构建和发布函数库

咱们用到一个fast-glob,这能够让咱们防止繁琐的文件罗列。

import fastGlob from 'fast-glob'
import { UTILS_PATH } from './path'
// 经过 fast-glob 快速得到多进口,防止繁琐的文件罗列
const getInputs = async (glob = 'src/**/*.ts') => {
    return await fastGlob(glob, {
        cwd: UTILS_PATH,
        absolute: true,
        onlyFiles: true,
        ignore: ['node_modules'],
    })
}

接着便是把构建流程写好。其实构建 ESM 和 CJS 模块有许多相似性,由于它们的输入都是一样的,只不过输出不一样。所以,咱们能够在同一个函数buildModules中把这两件作业一起做了。

export const buildModules = async () => {
    // 得到多文件进口
    const input = await getInputs()
    // 得到公共的 bundle 目标
    const bundle = await rollup({
        input,
        plugins: [rollupTypescript()],
    })
    // 用 Promise.all 标识:ESM 和 CJS 都完结了,才算 buildModules 完结
    await Promise.all([
        // 输出 ESM 到 es 目录
        bundle.write({
            format: 'esm',
            dir: resolve(UTILS_PATH, 'es'),
        }),
        // 输出 CJS 到 lib 目录
        bundle.write({
            format: 'cjs',
            dir: resolve(UTILS_PATH, 'lib'),
        })
    ])
}

然后,咱们能够在build/build-utils.js新增一个startBuildUtils函数,作为对外供给的调用接口

startBuildUtils函数经过 gulp 的 parallel 办法并行履行构建buildModulesbuildBundle的使命。由于buildModules内部是经过Promise.all并行履行 ESM 和 CJS 的输出,所以本质上 ESM, CJS, UMD 模块的构建都是并行的,这也契合咱们最开端的规划。

gulpfile.babel.js能够改造为:

export const buildUtils = series(cleanUtils, startBuildUtils);

咱们看看作用,能够发现生成的内容完全契合预期,

  • 既能够支撑咱们经过@vue-pro-components/utils/es/install或者@vue-pro-components/utils/es/fullscreen按需引进独立的模块。
  • 也能够直接import { enterFullscreen } from "@vue-pro-components/utils"
  • 配合一些东西,也能完结后者到前者的转化,一起保证开发效率和生产质量。

在发布组件库之前,你需求先把握构建和发布函数库

构建类型声明文件

到这儿,咱们发现还缺少的便是类型声明晰,我试着在buildBundle时一起把declaration给生成了,可是报了一个错,生成的 types 目录不能在bundle.write指定的dir目录之外。

在发布组件库之前,你需求先把握构建和发布函数库

declarationDir改为resolve(UTILS_PATH, './dist/types')却是能够,不过生成到 dist/types 目录下不契合我的预期。

于是我就考虑加一个buildTypes办法用于独自生成类型声明。

export const buildTypes = async () => {
    const bundle = await rollup({
        input: resolve(UTILS_PATH, 'src/index.ts'),
        plugins: [
            rollupTypescript({
                compilerOptions: {
                    rootDir: resolve(UTILS_PATH, "src"),
                    declaration: true,
                    declarationDir: resolve(UTILS_PATH, './types'),
                    emitDeclarationOnly: true,
                },
            }),
        ],
    })
    await bundle.write({
        dir: resolve(UTILS_PATH, 'types'),
    })
}

不过我发现,即使我装备了emitDeclarationOnly,终究生成的 types 目录下仍是有一个index.js文件。

在发布组件库之前,你需求先把握构建和发布函数库

看了一下@rollup/plugin-typescript的文档,发现是插件疏忽了这部分装备。

在发布组件库之前,你需求先把握构建和发布函数库

来不及想为什么了,这儿直接改用一个专门用于生成类型声明的插件rollup-plugin-dts,buildTypes函数改造成如下:

export const buildTypes = async () => {
    const input = await getInputs()
    const bundle = await rollup({
        input,
        plugins: [dts()],
    })
    await bundle.write({
        dir: resolve(UTILS_PATH, 'types'),
    })
}

startBuildUtils函数中也能够参加buildTypes使命了。

export const startBuildUtils = parallel(buildModules, buildBundle, buildTypes)

在发布组件库之前,你需求先把握构建和发布函数库

发布函数库

构建的作业做好之后,就能够准备发布到 npm 上了。

首先将package/utils的版本号修正一下,咱们能够依据lerna version的提示修正版本号。

在发布组件库之前,你需求先把握构建和发布函数库

接着运转package.json中界说的publish:package脚本,就能够发布到 npm 上了。

在发布组件库之前,你需求先把握构建和发布函数库

接着咱们能够找个地方验证一下@vue-pro-components/utils这个包是不是能够正常运用,在线 IDE 或许是最直观的。

在发布组件库之前,你需求先把握构建和发布函数库

由于某在线 IDE 的 iframe 没有 allow fullscreen 特性,咱们需求手动给它修正一下。

在发布组件库之前,你需求先把握构建和发布函数库

作用这就有了:

在发布组件库之前,你需求先把握构建和发布函数库

结语

本文首要介绍了一个函数库的构建和发布的根本流程,尽管打通了根本流程,但也还存在许多优化的空间,比如怎样把构建和发布的流程串起来,而不是一条接一条指令地手动履行。不过,以此为基础,咱们就能够继续探究更为杂乱的组件库的构建和发布流程了。假如您对我的专栏感兴趣,欢迎您订阅关注本专栏,接下来能够一起讨论和沟通组件库开发进程中遇到的问题。