Rollup 插件开发牛刀小试

544 阅读5分钟

哈喽,大家好,我是 SuperYing。今天我们来聊聊 Rollup 插件开发,整点代码,小试牛刀。

不知道大家是否了解过 Rollup 插件相关的东西,感兴趣的话可以到 Rollup 官网 瞅瞅。

简单点理解,Rollup 的插件就是一个函数,返回值是一个对象,这个对象需要包括 Rollup 规定的属性编译 hooks输出 hooks 等。这部分规定的内容都可以在官网的 plugin-development 部分找到,我就不多赘述了哈。Rollup 插件可以实现诸如在打包前传输代码,在 node_modules 文件夹中查找第三方模块等功能

最近在做一个远程加载组件库的小玩意,为什么要整它呢?现在比较流程的使用组件库方式就是 npm 模块,但是这种方式需要先将框架代码下载到本地,然后在工程中引用。那么也就是说,我自己开发的组件库,要经过 【打包 - 发布 - 卸载旧版本 - 安装新版本】这样一系列的操作过程,循环往复,略微有些麻烦。因此呢,就想将组件库的引用改为远程加载,每次组件库升级仅需要发布即可,哈哈哈,理想很丰满.....

好啦,废话不多说,开整....

1.创建插件

新增文件 plugins/roll-plugin-remote-ui.js。(不打算将插件单独发布,与远程加载功能放在同一个库中)。
为什么 ts 工程插件要用 js 写插件? 因为要直接在 rolluo.config.js 中使用,而 ts 文件需要经过编译才可用,所以用 js。

image.png

2.实现插件

首先我们需要思考一下,远程加载需要什么
1.import 引入
2.请求远程资源,即 http/https
3.export 导出

我想大致就上面那么多吧,那我们就来实现它。

Rollup 提供一个 build hook --- resolveId,我们可以通过该 hook 定义一个解析器,在该解析器中捕获远程加载的请求地址,然后利用 node 的能力将远程的资源下载到本地,最终将 import 的目标地址指向生成的本地资源。 思路很清晰,是不是,但是有一个问题,我们怎么判断当前 import 的是资源是需要远程加载呢?判断 sourceimport 语句中的导入对象地址,如 import foo from '../foo.js',source 就是 '../foo.js') 是否存在 http/https 吗?可以,但是个人感觉不够精准,因为我们是有上面一套处理流程的,要请求资源并下载到本地的,只要 source 包括 http/https 就一定需要走这一套吗,答案是不一定。这个时候就体现了约定的重要性,我们规定远程加载的组件库地址以 @remote 为前缀,凡是 source 存在该前缀的资源,一律视为远程加载。

下面是插件的代码:

/**
* 远程 UI 资源插件
* 拦截 import,若为远程资源,下载到本地并指向本地资源地址
*/
import path from 'path'
import fs from 'fs-extra'
import request from 'request'

export default function remoteUiPlugin() {
    return {
        // rollup plugin 约定,需要包含 name 属性,值以 rollup-plugin 开头
        name: 'rollup-plugin-remote-ui',
        async resolveId(id) {
            // 若为 @remote/ 开头,说明引用的是远程资源
            if (/@remote\//.test(id)) {
            const [url] = id.match(/https?.*?$/igm) || []
            if (!url) return id
            const timeStamp = new Date().getTime()
            const finalUrl = `${url}${url.indexOf('?') > -1 ? '&' : '?'}timestamp=${timeStamp}`
            return await generateLocal(finalUrl)
            }
        }
    }
}

// 生成本地资源
function generateLocal(url, localPath = './remote-ui') {
    const folder = path.resolve(__dirname, localPath)
    // 同步确认该目录是否存在,若不存在,则创建
    fs.ensureDirSync(folder)
    const queryIndex = path.basename(url).indexOf('?')
    const fileName = path.basename(url).substring(0, queryIndex)
    const local = path.resolve(folder, `./${fileName}`)
    return new Promise((resolve, reject) => {
        const stream = fs.createWriteStream(local)
        request(url).pipe(stream).on("close", function (err) {
            if (err) reject(err)
            // 返回本地资源地址
            resolve(local)
        });
    })
}

好啦,以上就是整个插件的实现代码,很简陋有木有,还有需要需要改进的地方。如果各位大佬有更好的思路欢迎评论区分享哈。

3.应用插件

这一步涉及到 Rollup 配置文件 rollup.config.js,引入插件文件后,在 plugins 配置项下添加即可。
代码如下:

import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import babel from '@rollup/plugin-babel';
import { DEFAULT_EXTENSIONS } from '@babel/core';
import rollupTypescript from 'rollup-plugin-typescript2';
import { eslint } from 'rollup-plugin-eslint';
import { terser } from 'rollup-plugin-terser'
import remoteUiPlugin from './plugins/rollup-plugin-remote-ui.js'; // 引入插件
import pkg from './package.json';
import path from 'path'

// 环境
const env = process.env.NODE_ENV
const name = 'RemoteUI'
const config = {
    input: path.join(__dirname, 'src/index.ts'),
    output: [
        // umd
        {
            name,
            file: pkg.umd,
            format: 'umd'
        },
        // commomjs
        {
            file: pkg.main,
            format: 'cjs'
        },
        // esm
        {
            file: pkg.module,
            format: 'es'
        }
    ],
    plugins: [
        eslint({
            throwOnError: true, // lint 结果有错误将会抛出异常
            throwOnWarning: true,
            include: ['src/*.ts', 'src/**/*.ts'],
            exclude: ['node_modules/**', 'dist/**', '*.js'],
        }),
        resolve(), // so Rollup can find `ms`、
        commonjs(), // so Rollup can convert `ms` to an ES module
        rollupTypescript(),
        remoteUiPlugin(), // 使用插件
        babel({
            // 编译库使用 runtime
            babelHelpers: 'runtime',
            // 只转换源代码,不运行外部依赖
            exclude: 'node_modules/**',
            // babel 默认不支持 ts 需要手动添加
            extensions: [
                ...DEFAULT_EXTENSIONS,
                '.ts',
            ],
        }),
    ],
    external: ['vue']
}
export default config

4.测试插件

到此我们的插件就算是用起来了,我们来写点代码测试一下,瞅瞅效果。
src/index.ts 中添加如下代码:

// 这里的远程地址我实在本地 nginx 部署的组件库,大家自己测试的时候需要改成可用的地址哦
export * from '@remote/http://localhost:9090/dist/bgy-plus/dist/index.full.mjs'

test/test.js 中添加如下代码:

// eslint-disable-next-line @typescript-eslint/no-var-requires
const RemoteUI = require('../dist/index.js')
// RemoteUI 即为最终 import 的组件库对象,我们控制台打印一下其中的 ElButton 组件
console.log('RemoteUI', RemoteUI.ElButton)

打开终端,执行 npm run test关于整个目录结构及 npm script 等,请参考 从0到1搭建 Rollup + TypeScript 模板工程)。

我们可以看到,组件对象正常打印,左侧 remote-ui 目录中,也生成了本地资源文件。 image.png

OK,整完收工!以上便是「 Rollup 插件开发牛刀小试 」的全部内容,感谢阅读。

欢迎各路大佬讨论、批评、指正,共同进步才是硬道理!