侧边栏壁纸
博主头像
神奇的程序员

今天的努力只为未来

  • 累计撰写 167 篇文章
  • 累计创建 25 个标签
  • 累计收到 215 条评论

目 录CONTENT

文章目录

使用Rollup构建npm库

神奇的程序员
2023-03-19 / 2 评论 / 26 点赞 / 2,123 阅读 / 12,558 字 正在检测是否收录...

前言

前几天在调试截图插件的时候,想使用yarn link将本地打包好的文件软链接到项目里,以便于快速定位问题时,出现了错误,经过一番折腾后发现锅在Vue CLI这里。

本文就跟大家分享下我从Vue CLI迁移至Rollup的整个过程,欢迎各位感兴趣的开发者阅读本文。

为什么要更换架构

使用link语法链接本地构建好的库,需要提供esm格式的文件,Vue CLI的打包配置选项中并没有提供这个选项。可能很多开发者跟我一样有个疑惑:既然包里没提供此格式的文件,为什么使用yarn add一个包的时候就可以正常运行呢?我带着这个疑问查了下,发现在添加包的过程中,它会自己构建esm版本的出来。

排查到问题后,我想着可能我用的构建工具版本太老了,于是我就翻了下Vue CLI的官网,依然没找到相关的配置选项,在官网还看到了 Vue CLI is in Maintenance Mode!的警告。

image-20230315213947278

Vue CLI已经进入维护模式了(webpack成了旧爱),并且在极力推荐Vite(rollup成了新欢)。一个时代结束了,一开始本来还寻思着用vite呢,但是想了下用别人封装好的东西局限性还是太大了,况且它的大部分功能我是用不到的。因此,我最终决定了使用Rollup来作为项目的架构。

可能很多开发者不太了解我上面所说的“新欢旧爱”,我在这里做个解释吧:

  • Vue CLI默认使用webpack作为打包工具
  • Vite使用Rollup来处理库的打包,使用ESBuild作为默认的构建工具

预期效果

我们先来看下Vue CLI都帮我们处理了哪些事情吧:

  • 处理scss文件
  • 处理ts文件
  • 处理静态资源文件
  • 处理vue文件
  • 处理路径别名
  • 构建umd、cjs格式的文件

舍弃它,拥抱rollup生态,我们需要将这些一一实现。

如果你对我的截图插件实现原理比较感兴趣,请移步:实现Web端自定义截屏

环境搭建

看了一圈Rollup官方文档对它有了一个基本的了解,我们将项目里有关Vue CLI的依赖包以及配置文件删掉后,就可以安装rollup的基本依赖包了(此处我们使用的rollup版本为2.x,依赖的node版本为14.18.0)。

yarn add rollup@^2.59.2 -D

随后,我们在项目的根目录创建rollup.config.js文件,添加如下所示的配置代码:

  • input 项目的入口文件
  • output 打包配置
    • file 目标文件路径
    • format 打包格式
    • name 对外暴露的模块名
export default {
  input: "src/main.ts",
  output: [
    {
      file: "dist/screenShotPlugin.umd.js",
      format: "umd",
      name: "screenShotPlugin"
    },
    {
      file: "dist/screenShotPlugin.esm.js",
      format: "es",
      name: "screenShotPlugin"
    },
    {
      file: "dist/screenShotPlugin.common.js",
      format: "cjs",
      name: "screenShotPlugin"
    }
  ]
}

最后,在package.json中添加执行脚本。

{
  "scripts": {
    "build-rollup": "rollup -c"
  }
}

执行yarn run build-rollup后,我们发现报错[!] Error: Unexpected token (Note that you need plugins to import files that are not JavaScript),它提醒我们需要插件才能处理非JavaScript文件。

image-20230316071911014

根据官方文档所述,rollup提供了plugins选项供使用者对其进行扩展,使用也非常简单。

  • 安装依赖包
  • 修改配置文件

此处,我们要处理ts文件,打包cjs格式的文件,需要用到

  • rollup-plugin-typescript2 用于处理.ts类型的文件
  • @rollup/plugin-commonjs 用于将 CommonJS 模块转换为 ES6 模块
  • @rollup/plugin-node-resolve 用于将模块解析为 ES6 模块的插件,它可以帮助 Rollup 在打包时正确地解析模块的导入路径,使得打包后的代码可以在浏览器中运行。通常会将此插件放到其他插件的前面。
yarn add rollup-plugin-typescript2 @rollup/plugin-commonjs @rollup/plugin-node-resolve -D

修改rollup.config.js文件

import typescript from "rollup-plugin-typescript2";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";

export default {
  input: "src/main.ts",
  plugins: [
    nodeResolve(),
    commonjs(),    
    typescript({
      tsconfig: "tsconfig.json",
      tsconfigOverride: {
        compilerOptions: {
          declaration: true
        }
      },
      clean: true
    })
  ]  
}

再次执行构建命令后,插件成功打包,根目录出现dist文件夹。

image-20230316214005952

这就是rollup最基本的用法。如果你的项目很简单,并且只使用了ts。那么做到这些已经完全符合你的需求了。

强大的插件系统

如前所述,为了实现预期效果,我们需要使用Rollup的插件系统。在本章节中,我将跟大家分享达到预期效果所需的插件。

路径别名

我的项目中使用@来代表"src"目录,rollup在打包的时候默认是识别不了的,我们需要通过@rollup/plugin-alias插件来处理。

yarn add @rollup/plugin-alias -D

rollup.config.js

import alias from "@rollup/plugin-alias";

export default {
  // ...其他配置省略
  plugins: [
    alias({
      entries: [{ find: "@", replacement: path.resolve(__dirname, "src") }]
    })
  ]  
}

处理CSS文件

我项目中使用的CSS预处理器是scss,在rollup中处理scss需要使用rollup-plugin-postcss插件,还需要其他一些插件作为辅助。

  • postcss CSS 预处理器,它可以用 JavaScript 编写插件来对 CSS 进行转换和优化。

  • postcss-preset-env PostCSS 的一个预设,它包含了一组常用的 PostCSS 插件,可以帮助我们自动转换最新的 CSS 语法和特性,以兼容目标浏览器。

  • postcss-import 处理css文件中的@import语句

  • postcss-url 处理css中引入的静态资源

  • @rollup/plugin-url 处理静态资源,通过配置阀值来决定文件是否转base64还是拷贝文件

  • cssnano 移除注释、空格和其他不必要的字符来压缩CSS代码

  • autoprefixer 给css3的一些属性加前缀,兼容其他内核的浏览器

yarn add postcss postcss-preset-env postcss-import postcss-url @rollup/plugin-url cssnano autoprefixer -D

rollup.config.js

import postcss from "rollup-plugin-postcss";
import autoprefixer from "autoprefixer";
import postcssImport from "postcss-import";
import postcssUrl from "postcss-url";
import url from "@rollup/plugin-url";
import cssnano from "cssnano";

export default {
  // ...其他配置省略
  plugins: [
    postcss({
      // 内联css
      extract: splitCss === "true" ? "style/css/screen-shot.css" : false,
      minimize: true,
      sourceMap: false,
      extensions: [".css", ".scss"],
      // 当前正在处理的CSS文件的路径, postcssUrl在拷贝资源时需要根据它来定位目标文件
      to: path.resolve(__dirname, "dist/assets/*"),
      use: ["sass"],
      // autoprefixer: 给css3的一些属性加前缀
      // postcssImport: 处理css文件中的@import语句
      // cssnano: 它可以通过移除注释、空格和其他不必要的字符来压缩CSS代码
      plugins: [
        autoprefixer(),
        postcssImport(),
        // 对scss中的别名进行统一替换处理
        postcssUrl([
          {
            filter: "**/*.*",
            url(asset) {
              return asset.url.replace(/~@/g, ".");
            }
          }
        ]),
        // 再次调用将css中引入的图片按照规则进行处理
        postcssUrl([
          {
            basePath: path.resolve(__dirname, "src"),
            url: "inline",
            maxSize: 8, // 最大文件大小(单位为KB),超过该大小的文件将不会被编码为base64
            fallback: "copy", // 如果文件大小超过最大大小,则使用copy选项复制文件
            useHash: true, // 进行hash命名
            encodeType: "base64" // 指定编码类型为base64
          }
        ]),
        cssnano({
          preset: "default" // 使用默认配置
        })
      ]
    }),  
    // 处理通过img标签引入的图片
    url({
      include: ["**/*.jpg", "**/*.png", "**/*.svg"],
      // 输出路径
      dest: "dist/assets",
      // 超过10kb则拷贝否则转base64
      limit: 10 * 1024 // 10KB
    })
  ]  
}

如果你使用的是less或者Stylus,只需要修改上方配置中的extensions选型,将.scss换成你所使用的预处理语言即可。

JS代码转换

相信大家对babel都不陌生,它能够将ES6+代码转换为可以在浏览器中运行的ES5代码,能够最大程度的兼容低版本浏览器。在rollup中我们需要使用@rollup/plugin-babel插件实现这个转换。

yarn add @rollup/plugin-babel -D

rollup.config.js

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

export default {
   // ...其他配置省略
   plugins: [
    nodeResolve({
      // 读取.browserslist文件
      browser: true,
      preferBuiltins: false
    }),
    typescript({
      tsconfig: "tsconfig.json",
      tsconfigOverride: {
        compilerOptions: {
          // 指定目标环境为es5
          target: "es5"
        }
      },
      clean: true
    }),
    babel() 
  ]  
}

除此之外,还需要在根目录创建babel的配置文件babel.config.js。我们还需要额外安装:

  • @babel/core Babel 的核心库,提供了 Babel 的编译器、转换器等基础功能。
  • @babel/preset-env Babel 的一个预设,包含了一系列转换规则和插件的预设,能够根据指定的目标环境,自动转换最新的 JavaScript 语法和 API,以兼容目标环境。
yarn add @babel/core @babel/preset-env -D

babel.config.js

module.exports = {
  presets: ["@babel/preset-env",{ targets: "defaults" }],
  plugins: []
};

处理静态资源

在项目开发过程中,会有一些文件不需要通过打包工具来处理(字体文件、html文件等),我们希望将它原封不动的复制到目标路径下。在rollup中我们需要使用rollup-plugin-copy插件来实现。

yarn add rollup-plugin-copy -D

rollup.config.js

import copy from "rollup-plugin-copy";

export default {
  // ...其他配置省略
  plugins: [
    copy({
      targets: [
        {
          src: "src/assets/fonts/**",
          dest: "dist/assets/fonts"
        },
        {
          src: "public/**",
          dest: "dist"
        }        
      ]
    })
  ]  
}

处理Vue文件

我的截图插件还有一个vue3版本的,需要用rollup进行处理的话,需要使用6.x版本的rollup-plugin-vue插件。

yarn add rollup-plugin-vue@^6.0.0 -D

rollup.config.js文件

import vue from "rollup-plugin-vue";

export default {
  // ...其他配置省略
  output: [
    {
      file: "dist/screenShotPlugin.umd.js",
      format: "umd",
      name: "screenShotPlugin",
      globals: {
        vue: "Vue"
      }      
    },
    {
      file: "dist/screenShotPlugin.esm.js",
      format: "es",
      name: "screenShotPlugin",
      globals: {
        vue: "Vue"
      }      
    },
    {
      file: "dist/screenShotPlugin.common.js",
      format: "cjs",
      name: "screenShotPlugin",
      globals: {
        vue: "Vue"
      }      
    }
  ],  
  external: ["vue"],
  plugins: [
    vue({
      target: "browser",
      css: true,
      // 把组件转换成 render 函数
      compileTemplate: true,
      preprocessStyles: true,
      preprocessOptions: {
        scss: {
          includePaths: ["src/assets/scss"]
        }
      }
    }),    
  ]
}

注意⚠️:如果你用的是vue2.x则需要安装5.x版本的rollup-plugin-vue插件,并且他们的配置也有所差异,具体的用法请参考该插件的官方文档

清理目标文件

打包项目到目标路径时,大多数情况下,我们都需要先将目标目录进行清空。要实现这个功能,就需要用到rollup-plugin-delete插件。

import delFile from "rollup-plugin-delete";

export default {
  // ...其他配置省略
  plugins: [
    delFile({ targets: "dist/*" })
  ]  
}

模块占用分析

在一些情况下,我们可能需要了解打包后的文件各个模块的空间占用情况,便于进一步的优化。要实现这个功能,需要用rollup-plugin-visualizer插件。

yarn add rollup-plugin-visualizer -D

rollup.config.js

import visualizer from "rollup-plugin-visualizer";

export default {
  // ...其他配置省略
  plugins: [
    visualizer({
      filename: "dist/bundle-stats.html"
    })
  ]  
}

构建完成后,dist文件夹下的bundle-stats.html文件就展示了各个模块的空间占用情况。

image-20230318103746744

代码压缩

当我们要把打包后的文件部署到生产环境时,通过对打包后的文件进行压缩可以节省网络的开销,提升网站的加载速度。要实现这个功能,就需要用到rollup-plugin-terser插件。

yarn add rollup-plugin-terser -D

rollup.config.js

  • 在output配置中的输出配置对象中添加plugins属性
  • 如果你想为所有的output都添加代码压缩,那么你可以直接添加近plugins内
import { terser } from "rollup-plugin-terser";

export default {
  // ...其他配置省略
  output: [
    {
      file: "dist/screenShotPlugin.umd.js",
      format: "umd",
      name: "screenShotPlugin",
      plugins: [
        terser()
      ]    
    }
  ]
  //plugins: [
  //  terser()
  //]    
}

打包进度条

在打包的时候,我们希望知道打包的进度,此时就需要用到rollup-plugin-progress库来实现。

yarn add rollup-plugin-progress -D

rollup.config.js

import progress from "rollup-plugin-progress";

export default {
  // ...其他配置省略
  plugins: [
    progress({
      format: "[:bar] :percent (:current/:total)"
    })
  ]  
}

设置开发服务器

我们在开发库的时候,为了调试方便,希望改了后就能很快看到效果,而不是每次都打包才行。要实现这个功能我们需要用到

  • rollup-plugin-serve 插件用于在本地服务器上提供静态文件,并将其作为服务提供给浏览器访问
  • rollup-plugin-livereloa 插件用于在文件发生变化时自动刷新浏览器。它会监视构建目录中的文件,当任何文件更改时,会通知浏览器自动刷新页面。
yarn add rollup-plugin-serve rollup-plugin-livereloa -D

rollup.config.js

import serve from "rollup-plugin-serve";
import livereload from "rollup-plugin-livereload";

export default {
  // ...其他配置省略
  plugins: [
      serve({
        // 服务器启动的文件夹,访问此路径下的index.html文件
        contentBase: "dist",
        port: 8123
      }),
      // watch dist目录,当目录中的文件发生变化时,刷新页面
      livereload("dist")    
  ]  
}

注意⚠️:它访问的是dist目录下的index.html文件,里面的内容是我们自己写的,在静态资源处理章节我们拷贝了public目录下的所有文件。

因此,我们需要创建pubic目录并index.html,在head中使用相对路径引入打包好的umd文件,如下所示:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>screen shot demo</title>
  <script src="./screenShotPlugin.umd.js"></script>
  <style>
      *{
          margin: 0;
          padding: 0;
      }
      #app {
          width: 100%;
          height: 100%;
      }
  </style>
  <script type="text/javascript">
    const changeScreenShot = () => {
      new screenShotPlugin()
    }
  </script>
</head>
<body>
<div id="app">
  <div>
    截图插件文字展示
  </div>
  <br/>
  <button onclick="changeScreenShot()"> 点击截图 </button>
</div>
</body>
</html>

命令行参数解析

通常情况下我们的环境会分为开发和生产两个环境,每个环境打包出来的东西是不同的。我们在执行打包命令的时候,需要传递参数来做区分。我们想要在配置文件中读取传递过来的参数就需要用到yargs库来解析命令行参数。

yarn add yargs -D

rollup的配置文件如下所示,此处我需要进行解析的参数有:

  • splitCss 是否将css拆分成独立的文件,默认为内联
  • packagingFormat 打包格式,默认三种格式都打包
  • compressedState 打包后是否压缩js代码
  • showModulePKGInfo 是否启用包体积分析插件
  • useDevServer 是否启用开发服务器
import yargs from "yargs";

// 使用yargs解析命令行执行时的添加参数
const commandLineParameters = yargs(process.argv.slice(1)).options({
  // css文件独立状态,默认为内嵌
  splitCss: { type: "string", alias: "spCss", default: "false" },
  // 打包格式, 默认为 umd,esm,common 三种格式
  packagingFormat: {
    type: "string",
    alias: "pkgFormat",
    default: "umd,esm,common"
  },
  // 打包后的js压缩状态
  compressedState: { type: "string", alias: "compState", default: "false" },
  // 显示每个包的占用体积, 默认不显示
  showModulePKGInfo: { type: "string", alias: "showPKGInfo", default: "false" },
  // 是否开启devServer, 默认不开启
  useDevServer: { type: "string", alias: "useDServer", default: "false" }
}).argv;

// 需要让rollup忽略的自定义参数
const ignoredWarningsKey = [...Object.keys(commandLineParameters)];
const splitCss = commandLineParameters.splitCss;
const packagingFormat = commandLineParameters.packagingFormat.split(",");
const compressedState = commandLineParameters.compressedState;
const showModulePKGInfo = commandLineParameters.showModulePKGInfo;
const useDevServer = commandLineParameters.useDevServer;

export default {
  input: "src/main.ts",
  //...其他配置省略
}

我们拿到命令行的参数后,就可以在需要进行判断的地方使用了,完整配置请移步js-screen-shot/rollup.config.js

最后,在package.json中添加构建命令传递参数即可,如下所示:

{
  "scripts": {
    "build-rollup": "rollup -c --splitCss false --compState false --showPKGInfo true",
    "build-rollup:dev": "rollup -wc --splitCss false --compState false --showPKGInfo true --useDServer true --pkgFormat umd",
    "build-rollup:prod": "rollup -c --splitCss false --compState true"
  }
}

除了使用这种方式来做区分外,你还可以通过在根目录创建多个rollup的配置文件来实现,比如要做dev模式下的打包,你可以创建rollup.dev.config.js文件。

打包的时候,指定配置文件即可,如下所示:

{
  "scripts": {
    "build-rollup:dev": "rollup -c ./rollup.dev.config.js"
  }
}

配置优化

完成文章前面的所有配置后,rollup.config.js文件中的代码总数已经200多行了,整个文件看起来乱的很,可阅读性很低。

image-20230318170217780

我们可以将rollup中所有需要通过参数来判断生成的配置逻辑拆分到其他文件中,在rollup.config.js文件调用拆分出来的方法,将结果渲染。

我们在根目录创建rollup.utils.js文件,将判断逻辑封装成方法写进去,部分代码如下所示,完整代码请移步 js-screen-shot/rollup-utils.js

import serve from "rollup-plugin-serve";
import livereload from "rollup-plugin-livereload";

// ...其他代码省略

const enableDevServer = status => {
  // 默认清空dist目录下的文件
  let serverConfig = [delFile({ targets: "dist/*" })];
  if (status === "true") {
    // dev模式下不需要对dist目录进行清空
    serverConfig = [
      serve({
        // 服务器启动的文件夹,访问此路径下的index.html文件
        contentBase: "dist",
        port: 8123
      }),
      // watch dist目录,当目录中的文件发生变化时,刷新页面
      livereload("dist")
    ];
  }
  return serverConfig;
};

export {
	// ...其他代码省略
	enableDevServer
}

在rollup.config.js引入并使用即可,部分代码如下所示,完整代码请移步 rollup.config.js

import { enableDevServer } from "./rollup-utils";

const commandLineParameters = yargs(process.argv.slice(1)).options({
  //...其他配置省略
})
const useDevServer = commandLineParameters.useDevServer;

export default {
  //...其他配置省略
  plugins: [
    ...enableDevServer(useDevServer)
  ]  
}

项目地址

文章写到这里,我已经成功地完成了两个开源项目中的架构迁移,并且使用了文中所列出的技术。如果你想要参考我的经验,请移步我的开源项目。

写在最后

至此,文章就分享完毕了。

我是神奇的程序员,一位前端开发工程师。

如果你对我感兴趣,请移步我的个人网站,进一步了解。

  • 文中如有错误,欢迎在评论区指正,如果这篇文章帮到了你,欢迎点赞和关注😊
  • 本文首发于神奇的程序员公众号,未经许可禁止转载💌
26

评论区