手把手教你如何使用 rollup + vue单文件 工作流开发 3.x 插件

rollup的介绍

什么是rollup

Rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码

为什么用rollup

  • rollup 打包出来的体积都比 webpack 略小一些,通过查看打包出来的代码,webpack打包出来的文件里面有很多 __webpack_require__ 工具函数的定义,可读性也很差,而rollup打包出来的js会简单一点。

  • 如果你将项目拆分成小的单独文件中,这样开发软件通常会很简单,因为这通常会消除无法预知的相互影响(remove unexpected interaction),以及显著降低了所要解决的问题的复杂度(complexity of the problem),并且可以在项目最初时,就简洁地编写小的项目(不一定是标准答案)。不幸的是,JavaScript 以往并没有将此功能作为语言的核心功能。

  • 可以使用 rollup 插件解析 .vue 文件。

Tree-shaking#

除了使用 ES6 模块之外,Rollup 还静态分析代码中的 import,并将排除任何未实际使用的代码。这允许您架构于现有工具和模块之上,而不会增加额外的依赖或使项目的大小膨胀。

例如,在使用 CommonJS 时,必须导入(import)完整的工具(tool)或库(library)对象


  // 使用 CommonJS 导入(import)完整的 utils 对象

  var utils = require( 'utils' );

  var query = 'Rollup';

  // 使用 utils 对象的 ajax 方法

  utils.ajax( 'https://api.example.com?search=' + query ).then( handleResponse );

但是在使用 ES6 模块时,无需导入整个 utils 对象,我们可以只导入(import)我们所需的 ajax 函数:


  // 使用 ES6 import 语句导入(import) ajax 函数

  import { ajax } from 'utils';

  var query = 'Rollup';

  // 调用 ajax 函数

  ajax( 'https://api.example.com?search=' + query ).then( handleResponse );

因为 Rollup 只引入最基本最精简代码,所以可以生成轻量、快速,以及低复杂度的 library 和应用程序。因为这种基于显式的 importexport 语句的方式,它远比「在编译后的输出代码中,简单地运行自动 minifier 检测未使用的变量」更有效。

兼容性(Compatibility)#

导入 CommonJS(Importing CommonJS)#

Rollup 可以通过插件导入已存在的 CommonJS 模块。

发布 ES6 模块(Publishing ES6 Modules)#

为了确保你的 ES6 模块可以直接与「运行在 CommonJS(例如 Node.js 和 webpack)中的工具(tool)」使用,你可以使用 Rollup 编译为 UMD 或 CommonJS 格式,然后在 package.json 文件的 main 属性中指向当前编译的版本。如果你的 package.json 也具有 module 字段,像 Rollup 和 webpack 2 这样的 ES6 感知工具(ES6-aware tools)将会直接导入 ES6 模块版本

如何使用rollup

cocos-vue2.x-vuex-template插件案例为例子。

在rollup.config.js中可以定义打包的入口以及使用各种插件来加强打包的功能,如导入.vue文件,导入文件转成 url。

可以看到该脚本是通过 Package.json 中获得多个打包的入口,Rollup 通过 Tree-shaking 解析 import 语法将导入的文件合并到入口,如图所示。

main.ts

我们来看看配置文件的内容。


import VuePlugin from 'rollup-plugin-vue';

import commonjs from '@rollup/plugin-commonjs';

import postcss from 'rollup-plugin-postcss';

import babel from '@rollup/plugin-babel';

import path from 'path';

import typescript from 'rollup-plugin-typescript2';

import { terser } from 'rollup-plugin-terser';

import json from '@rollup/plugin-json';

import url from '@rollup/plugin-url';

import del from 'rollup-plugin-delete';

import { join, relative, dirname } from 'path';

import node from '@rollup/plugin-node-resolve';

import tsConfig from './tsconfig.json';

import replace from '@rollup/plugin-replace';

const PKGJSON = require('./package.json');

const OUTPUT_DIR = tsConfig.compilerOptions.outDir;

const ROOT_DIR = tsConfig.compilerOptions.rootDir;

const NORMALIZER_PATH = join(__dirname, 'rollup-utils', 'normalize-component.js').replace(/\\/g, '/');

const STYLE_INJECTOR_PATH = join(__dirname, 'rollup-utils', 'style-injector.js').replace(/\\/g, '/');

// #region input

/**

 * @returns {Object.<string,string>}

 */

function getRollupInput() {

    const input = {};

    function assignToInput(path) {

        if (typeof path === 'string') {

            const key = relative(OUTPUT_DIR, path).replace('.js', '');

            const value = join(ROOT_DIR, relative(OUTPUT_DIR, path).replace('js', 'ts'));

            input[key] = value;

        }

    }

    const panels = PKGJSON.panels;

    if (panels) {

        for (const key in panels) {

            if (Object.hasOwnProperty.call(panels, key)) {

                const element = panels[key];

                const main = element.main;

                if (main) {

                    assignToInput(main);

                }

            }

        }

    }

    const main = PKGJSON.main;

    if (main) {

        assignToInput(main);

    }

    const contributions = PKGJSON.contributions;

    if (contributions) {

        const scene = PKGJSON.contributions.scene;

        if (scene) {

            const script = scene.script;

            if (script) {

                assignToInput(script);

            }

        }

        const builder = PKGJSON.contributions.builder;

        if (builder) {

            assignToInput(builder);

        }

        const preferences = PKGJSON.contributions.preferences;

        if (preferences) {

            const custom = preferences.custom;

            if (custom) {

                assignToInput(custom);

            }

        }

        const project = PKGJSON.contributions.project;

        if (project) {

            const custom = project.custom;

            if (custom) {

                assignToInput(custom);

            }

        }

    }

    return input;

}

// #endregion

const INPUT = getRollupInput();

// #region Plugins setting

// ** READ THIS ** before editing `knownAssetTypes`.

//   If you add an asset to `knownAssetTypes`, make sure to also add it

//   to the TypeScript declaration file `src/shims.d.ts`.

export const KNOWN_ASSET_TYPES = [

    // images

    'png',

    'jpe?g',

    'gif',

    'svg',

    'ico',

    'webp',

    'avif',

    // media

    'mp4',

    'webm',

    'ogg',

    'mp3',

    'wav',

    'flac',

    'aac',

    // fonts

    'woff2?',

    'eot',

    'ttf',

    'otf',

    // other

    'wasm',

    'webmanifest',

    'pdf',

    'txt',

];

const DEFAULT_ASSETS_REG_EXP = new RegExp(

    '\\.(' + KNOWN_ASSET_TYPES.join('|') + ')(\\?.*)?$'

);

const VUEPLUGIN = VuePlugin({

    defaultLang: { script: 'ts' },

    /**

     * @en Inject style

     * @zh 注入样式

     */

    css: true,

    /**

     * @en Injector style to shadow dom

     * @zh 往shadow dom 注入样式

     */

    isWebComponent: true,

    /**

     * @en This script describes which node to inject style into

     * @zh 这个脚本描述了往哪个节点注入样式

     */

    normalizer: `~${NORMALIZER_PATH}`,

    /**

     * @en This script describes how to inject style

     * @zh 这个脚本描述了如何注入样式

     */

    styleInjectorShadow: `~${STYLE_INJECTOR_PATH}`,

});

/**

 * @en Set the value of "import.meta.relativePath" to the relativePath from the directory where "bundle" is located to the root directory

 * @zh 设置 import.meta.relativePath 的值为 bundle 所在目录到根目录的相对路径

 * @returns

 */

function RelativeRoot() {

    return {

        name: 'relative-root', // this name will show up in warnings and errors

        resolveImportMeta(property, options) {

            if (property === 'relativePath') {

                return `"${relative(dirname(join(OUTPUT_DIR, options.chunkId)), '').replace(/\\/g, '/')}"`;

            }

            return null;

        },

    };

}

/**

 * @en Parse typescript

 * @zh 解析 typescript

 */

const TYPESCRIPT = typescript({

    tsconfig: path.resolve('tsconfig.json'),

});

const NODE = node({});

const COMMONJS = commonjs();

/**

 * @en Parsing styles so that styles can be imported

 * @zh 解析样式文件,可以导入样式

 */

const POSTCSS = postcss({

    inject: false,

    extract: false,

});

/**

 * @en parse json

 * @zh 解析 json

 */

const JSON_Plugin = json();

/**

 * A Rollup plugin which imports files as data-URIs or ES Modules.

 * @see https://www.npmjs.com/package/@rollup/plugin-url

 */

const URL = url({

    limit: 0,

    fileName: '[dirname][name][extname]',

    destDir: OUTPUT_DIR,

    publicPath: OUTPUT_DIR + '/',

    sourceDir: path.join(__dirname, ROOT_DIR),

    include: DEFAULT_ASSETS_REG_EXP,

});

/**

 * @en delete output dir once before compilation

 * @zh 编译前清空输出目录

 */

const DEL_ONCE = del({

    'targets': OUTPUT_DIR + '/*',

    runOnce: true,

});

const REPLACE = replace({

    'process.env.NODE_ENV': JSON.stringify('production'),

});

const RELATIVE_ROOT = RelativeRoot();

/**

 * @en ES6 syntax => ES5 syntax

 * @zh 将 ES6 格式转换为 ES5 格式

 */

const BABEL = babel();

// #endregion

/**

 * @type {import('rollup').RollupOptions['plugins']}

 */

const COMMON_PLUGINS = [

    VUEPLUGIN, TYPESCRIPT, COMMONJS, POSTCSS, BABEL, URL, JSON_Plugin, RELATIVE_ROOT, DEL_ONCE, NODE,

];

/**

 *

 * @param {string} id

 * @en Declare external modules

 * @zh 声明外部模块

 */

function external(id) {

}

/**

 * @type {import('rollup').RollupOptions}

 */

const DEBUG_CONFIG = {

    input: INPUT,

    output: {

        format: 'cjs',

        dir: path.resolve(__dirname, OUTPUT_DIR),

    },

    plugins: [...COMMON_PLUGINS],

    external,

};

/**

 * @type {import('rollup').RollupOptions}

 */

const RELEASE_CONFIG = {

    input: INPUT,

    output: {

        format: 'cjs',

        dir: path.join(__dirname, OUTPUT_DIR),

    },

    plugins: [

        ...COMMON_PLUGINS,

        /**

         * @en uglify scripts

         * @zh 代码混淆

         */

        terser(),

        REPLACE,

    ],

    external,

};

export default (commandLineArgs) => {

    if (commandLineArgs.configDebug === true) {

        return DEBUG_CONFIG;

    }

    return RELEASE_CONFIG;

};

这里有两种不同的打包配置分别对应着开发版本以及发布版本,通过命令行判断使用哪种配置。

以下表格简单说明了插件的作用。

功能 使用的插件 dev(开发) release(发布)
解析 vue 文件 rollup-plugin-vue
CommonJS 转换成 ES2015 模块 rollup-plugin-commonjs
允许导入 css\less\sass 文件,导入后解析为 css 字符串 rollup-plugin-postcss
ES6 语法=> ES5 语法 @rollup/plugin-babel
解析 typescript rollup-plugin-typescript2
解析 json 文件 @rollup/plugin-json
允许导入指定类型的资源文件,当前配置的效果是,导入资源后返回资源相对于扩展根目录的路径,并且将文件拷贝到 dist 目录下的对应文件 @rollup/plugin-url
编译前删除 dist 目录 rollup-plugin-delete
A Rollup plugin which locates modules using the Node resolution algorithm, for using third party modules in node_modules @rollup/plugin-node-resolve
混淆脚本 rollup-plugin-terser
替换 ‘process.env.NODE_ENV’ 为 ‘production’ 使得 vue 启用生产环境模式 @rollup/plugin-replace

原理说明

设置打包入口

rollup.config.js 中使用了 getRollupInput 扫描 package.json 常见的脚本来获取打包的入口,
可以这么设置 rollup 打包的入口。

const input = {
    "main":"src/main.ts"
}
/**
 * @type {import('rollup').RollupOptions}
 */
const config ={
    input,
    output: {
        format: 'cjs',
        dir: path.resolve(__dirname, './dist'),
    }
}

最终 src/main.ts 将被打包到 dist/main.js
如果有需要别的打包入口,可以自行设置配置中的 input。

Vue文件样式注入原理

creator的面板使用了 shadow dom

而 rollup-plugin-vue 默认往 header 注入样式,这并不能满足我们的需求。

所以我们需要在 rollup-plugin-vue 插件进行配置


const VUEPLUGIN = VuePlugin({

    defaultLang: { script: 'ts' },

    /**

     * @en Inject style

     * @zh 注入样式

     */

    css: true,

    /**

     * @en Injector style to shadow dom

     * @zh 往shadow dom 注入样式

     */

    isWebComponent: true,

    /**

     * @en This script describes which node to inject style into

     * @zh 这个脚本描述了往哪个节点注入样式

     */

    normalizer: `~${NORMALIZER_PATH}`,

    /**

     * @en This script describes how to inject style

     * @zh 这个脚本描述了如何注入样式

     */

    styleInjectorShadow: `~${STYLE_INJECTOR_PATH}`,

});


/// rollup-utils/normalize-component.js

    .... line 52 ....

    } else if (style) {

        hook = shadowMode ? function(context) {

            // 我们将样式注入到了 vue 实例的 $root.$el 的父节点中

            style.call(this, createInjectorShadow(context, this.$root.$el.parentNode));

        } : function(context) {

            style.call(this, createInjector(context));

        };

    }


/// rollup-utils/style-injector.js

// 此处暴露的方法对应着 creatorInjectorShadow ,我们将在 shadowRoot 节点下生成样式节点

module.exports = function(context, shadowRoot) {

...

使用相对路径获取文件绝对路径的原理

rollup 后脚本将会被合并到入口中,这代表使用 __dirname 以及 __filename 是不可靠的,我们需要注意这点,那么问题来了,如果需要扩展中读写文件,而相对路径是不可靠的那应该如何操作呢?

首先要解决的是拿到扩展的根目录


/**

 * @en Set the value of "import.meta.relativePath" to the relativePath from the directory where "bundle" is located to the root directory

 * @zh 设置 import.meta.relativePath 的值为 bundle 所在目录到根目录的相对路径

 * @returns

 */

function RelativeRoot() {

    return {

        name: 'relative-root', // this name will show up in warnings and errors

        resolveImportMeta(property, options) {

            if (property === 'relativePath') {

                return `"${relative(dirname(join(OUTPUT_DIR, options.chunkId)), '').replace(/\\/g, '/')}"`;

            }

            return null;

        },

    };

}

我们使用这个 Rollup 插件后,在源码中能够通过 import.meta.relativePath 获取打包后的 bundle 到扩展的相对路径。

const rootPath = join(__dirname, import.meta.relativePath)

通过以上方式即可获取扩展的根目录的绝对路径。

这里提供了两种解决方案读写文件

假设 tsConfig.compilerOptions.rootDir./src

假设 tsConfig.compilerOptions.outputDir./dist

1、使用 @rollup/plugin-url 插件 ,把文件资源存放在我们的 tsConfig.compilerOptions.rootDir 指定的目录下。

例如希望能够在插件中读写某个 txt 文件,然后配置 rollup.config.js


/// rollup.config.js

const KNOWN_ASSET_TYPES = ['txt']

const DEFAULT_ASSETS_REG_EXP = new RegExp(

    '\\.(' + KNOWN_ASSET_TYPES.join('|') + ')(\\?.*)?$'

);

/**

 * A Rollup plugin which imports files as data-URIs or ES Modules.

 * @see https://www.npmjs.com/package/@rollup/plugin-url

 */

const URL = url({

 // 导入任意大小的资源都返回相对路径,并且将文件拷贝到对应的 destDir 中

    limit: 0,

    fileName: '[dirname][name][extname]',

    destDir: tsConfig.compilerOptions.outDir,

    publicPath: tsConfig.compilerOptions.outDir + '/',

    sourceDir: path.join(__dirname, tsConfig.compilerOptions.rootDir),

    include: DEFAULT_ASSETS_REG_EXP,

});

shims.d.ts 声明 txt 模块,以免报错。


/// shims.d.ts

declare module '*.txt'

{

    export default string;

}

然后我们通过以下代码可以获取 txt 文件的绝对路径。


/// src/main.ts

/*

 * @zh 从扩展根目录到文件的路径

 * @en relative path from extension's root directory to file

 */

import relativePathFromRootToTXT from './my-number.txt';

import { join } from 'path';

/**

 * @zh 获取扩展的根目录的绝对路径

 * @en Gets the absolute path of the extension's root directory

 */

const rootPath = join(__dirname, import.meta.relativePath);

/**

 * @zh txt 文件的绝对路径

 * @en absolute path of txt file

 */

const filePath = join(rootPath, relativePathFromRootToTXT);

2、把文件资源存放在扩展的目录下,不放在 tsConfig.compilerOptions.rootDirtsConfig.compilerOptions.outputDir目录下

例如我们扩展的目录结构如下

my-extension

  • src

  • dist

  • public-path

将文件资源存放在 public-path 中,然后再脚本中通过以下方式获取文件资源的绝对路径。


/// src/main.ts

import { join } from 'path';

/**

 * @zh 获取扩展的根目录的绝对路径

 * @en Gets the absolute path of the extension's root directory

 */

const rootPath = join(__dirname, import.meta.relativePath);

/**

 * @zh txt 文件的绝对路径

 * @en absolute path of txt file

 */

const filePath = join(rootPath, 'public-path', 'my-number.txt');

压缩扩展包需要注意的事

rollup 已将 node_modules 引用到的第三方库整合进了源码中,如果没有声明某个包为 external ,是不需要将 node_modules 带入压缩包中的。

3.3 插件示例
rollup-vue2.zip (190.1 KB)

8赞

坐小板凳上MARK.

马克马克马克

好耶好耶~

楼主太厉害了!!!,能说一说如果是vue3的话要怎么才能做到