用 RollupJs 从 0 搭建一个属于自己的 vue 组件库

4,383 阅读5分钟

前言

现在前端的打包工具主要有 webpackrollupgulp等。

Gulp 是一个基于任务驱动的自动化构建工具。

Webpack 是当下最热门的前端资源模块化 管理和打包工具。它可以将许多松散的模块按照依赖和规则打包成符合生产环境部署的前端资源。还可以将按需加载的模块进行代码分割,等到实际需要的时候再异步加载。

Rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码,例如 library 或应用程序。

Rollup 对代码模块使用新的标准化格式,这些标准都包含在 JavaScript 的 ES6 版本中,而不是以前的特殊解决方案,如 CommonJS 和 AMD。ES6 模块可以使你自由、无缝地使用你最喜爱的 library 中那些最有用独立函数,而你的项目不必携带其他未使用的代码。ES6 模块最终还是要由浏览器原生实现,但当前 Rollup 可以使你提前体验。

简单点来说,就是 gulp 适合小项目,基于流程构建;webpack 适用于大型的应用项目,以模块划分,按需加载;而 rollup 适用于工具库的构建,优化代码。现在前端的 vue、 react 框架都是用 rollup 来打包的,也有一些 ui 框架的打包也是用的 rollup。

本文思路参考了 Element3 的构建,以 vue3 + rollup + ts + gulp 结合

初始化工作

用 npm init 初始化一个项目,然后安装一些必要的 npm 包:

// 初始化
npm init

// 安装必要的 rollup 的 npm 包
yarn add -D
    rollup-plugin-vue                // 类似于 webpack 的vue-loader,vue 组件的加载器
    rollup-plugin-scss               // scss 解析插件
    rollup-plugin-peer-deps-external // 打包的时候用来排除 package.json 内 peerDependencies 字段内的包
    @rollup/plugin-node-resolve      // 使用Node解析算法定位模块
    @rollup/plugin-commonjs          // CommonJS模块转换
    @rollup/plugin-json              // json 文件解析
    @rollup/plugin-replace           // 在打包文件时替换文件中的字符串
    @rollup/plugin-babel             // 转译 JavaScript 新特性
    rollup-plugin-typescript2        // 用于解析ts文件,并生成 x.d.ts 类型文件
    rollup-plugin-terser             // 包最小化生成,也就是压缩
    
// 还需要安装 @vue/compiler-sfc @babel/core typescript rollup 
    
// package.json
{
  "peerDependencies": { // 当前组件的其它依赖包
    "vue": "^3.0.11"
  }
}

在根目录新建 tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "jsx": "preserve",
    "declaration": true,
    "importHelpers": true,
    "moduleResolution": "node",
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "sourceMap": true,
    "baseUrl": ".",
    "allowJs": true,
    "lib": ["esnext", "dom", "dom.iterable", "scripthost"],
    "outDir": "./",
    "resolveJsonModule": true
  },
  "include": ["src", "packages"],
  "exclude": ["node_modules"]
}

rollup.config.js

然后再在根目录新建 rollup.config.js

// 引入 rollup 相关的包
import pkg from './package.json'
import vuePlugin from 'rollup-plugin-vue'
import scss from 'rollup-plugin-scss'
import peerDepsExternal from 'rollup-plugin-peer-deps-external'
import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import json from '@rollup/plugin-json'
import replace from '@rollup/plugin-replace'
import babel from '@rollup/plugin-babel'
import ts from 'rollup-plugin-typescript2'
import { terser } from 'rollup-plugin-terser'

// 先定义一个 base 配置
// 创建打包文件的头部信息
const createBanner = () => {
  return `/*!
  * ${pkg.name} v${pkg.version}
  * (c) ${new Date().getFullYear()} test
  * @license ISC
  */`
}

// 创建基础配置
const createBaseConfig = () => {
  return {
    input: 'src/index.ts', // 加载入口
    external: ['vue'],
    plugins: [ // 插件加载
      peerDepsExternal(),
      vuePlugin({
        css: true
      }),
      ts(),
      babel({
        exclude: 'node_modules/**',
        extensions: ['.js', '.jsx', '.vue'],
        babelHelpers: 'bundled'
      }),
      resolve({
        extensions: ['.vue', '.jsx', '.js']
      }),
      commonjs(),
      json(),
      scss()
    ],
    output: {
      sourcemap: false,
      banner: createBanner(),
      externalLiveBindings: false,
      globals: {
        vue: 'Vue'
      }
    }
  }
}

然后定义不同格式的输出及其它配置:

// 生成文件名
function createFileName(formatName) {
  return `dist/taxreview.${formatName}.js`
}

// es-bundle
const esBundleConfig = {
  plugins: [
    replace({
      preventAssignment: true,
      __DEV__: `(process.env.NODE_ENV !== 'production')`
    })
  ],
  output: {
    file: createFileName('esm-bundler'),
    format: 'es'
  }
}

// cjs
const cjsConfig = {
  plugins: [
    replace({
      preventAssignment: true,
      __DEV__: true
    })
  ],
  output: {
    file: createFileName('cjs'),
    format: 'cjs'
  }
}
// cjs.prod
const cjsProdConfig = {
  plugins: [
    terser(),
    replace({
      preventAssignment: true,
      __DEV__: false
    })
  ],
  output: {
    file: createFileName('cjs.prod'),
    format: 'cjs'
  }
}
// 其它的格式还有 es-browser global 等

对于不同的环境打不同的包

// 生产
const prodFormatConfigs = [
  esBundleConfig,
  cjsConfig,
  cjsProdConfig
]
const devFormatConfigs = [esBundleConfig] // 开发

最后执行打包程序

function mergeConfig(baseConfig, configB) {
  const config = Object.assign({}, baseConfig)
  // plugin
  if (configB.plugins) {
    baseConfig.plugins.push(...configB.plugins)
  }

  // output
  config.output = Object.assign({}, baseConfig.output, configB.output)

  return config
}
function createPackageConfigs() {
  return getFormatConfigs().map((formatConfig) => {
    return mergeConfig(createBaseConfig(), formatConfig)
  })
}

function getFormatConfigs() {
  return process.env.NODE_ENV === 'development'
    ? devFormatConfigs
    : prodFormatConfigs
}

export default createPackageConfigs()

vue 组件的处理

在根目录新建一个 packages 文件夹用来存放所有组件,在 packages 内部新建一个 theme-chalk 文件夹用来存放 scss 文件,不要在 vue 文件内直接写 scss,之后 theme-chalk 内的 scss 文件用 gulp 来打包。

// packages/Test/src/Test.vue
<template>
  <div>
    {{msg}}
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue'

export default defineComponent({
  name: 'test',
  setup() {
    let msg = ref('test')

    return { msg }
  }
})
</script>
// packages/Test/index.ts
import Test from './src/Test.vue'

/* istanbul ignore next */
Test.install = function (app) {
  app.component(Test.name, Test)
}

export { Test }

然后在根目录下 src/index.ts 执行整个程序的启动

// src/index.ts
import { Test } from '../packages/Test'

import { version } from '../package.json'
import { setupGlobalOptions } from './globalConfig' // 这个文件用来设置这个ui库的一些全局属性

const components = [
  Test
]

// 这个是用 vue.use 用来注册全局组件的
const install = (app, opts = {}) => {
  app.use(setupGlobalOptions(opts))

  components.forEach((component) => {
    app.use(component)
  })

  applyOptions(app)
}

// 这个是用来挂载像 Message 这样用 js 来调用的组件
function applyOptions(app) {
  app.config.globalProperties.$test = Test
}

const taxreview = {
  version,
  install
}

// 这个是用来单独引用的
export {
  Test,
  install
}

export default taxreview
// src/globalConfig.ts  用来设置ui库的一些全局属性
import { getCurrentInstance } from 'vue'

/**
 * get globalOptions $TAXREVIEW config object
 */
export function useGlobalOptions() {
  const instance = getCurrentInstance()

  if (!instance) {
    console.warn('useGlobalOptions must be call in setup function')
    return
  }

  return instance.appContext.config.globalProperties.$TAXREVIEW || {}
}

export function setupGlobalOptions(opts: any = {}) {
  return (app) => {
    app.config.globalProperties.$TAXREVIEW = {
      size: opts.size || '',
      zIndex: opts.zIndex || 2000
    }
  }
}

我们还需要修改下 package.json

{
  "main": "dist/taxreview.cjs.js", // 引入包的主入口文件
  "types": "dist/src/index.d.ts", // 指定类型声明文件
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "rollup -c" // 启动打包
  }
  // 其它配置
}

到这里用 yarn build 就可以打包 vue 组件了

image.png

css 处理

我们的全部 css 都放入了 packages/theme-chalk/src 下面,所以我们可以在 packages/theme-chalk 构建 gulp 打包

// packages/theme-chalk/gulpfile.js
'use strict'

const { series, src, dest } = require('gulp')
const sass = require('gulp-sass')
const autoprefixer = require('gulp-autoprefixer')
const cssmin = require('gulp-cssmin')

// 将 scss 文件转译成 css 文件并压缩放入同级的 lib 文件夹内
function compile() {
  return src('./src/*.scss')
    .pipe(sass.sync())
    .pipe(
      autoprefixer({
        overrideBrowserslist: ['ie > 9', 'last 2 versions'],
        cascade: false
      })
    )
    .pipe(cssmin())
    .pipe(dest('./lib'))
}

// 用来拷贝字体文件
function copyfont() {
  return src('./src/fonts/**').pipe(cssmin()).pipe(dest('./lib/fonts'))
}

exports.build = series(compile, copyfont)

然后在根目录的 package.json 再添加一个处理 scss 的命令

// cp-cli A B: 命令是将路径 A 的文件全部拷贝到路径 B
{
  "build:theme": "node scripts/generateCssFile.js && gulp build --gulpfile packages/theme-chalk/gulpfile.js && cp-cli packages/theme-chalk/lib lib/theme-chalk"
}

// yarn build:theme

最后

最后我们将包用 npm publish 发布到npm,然后就可以用 yarn add xxx 来应用于项目了。

GitHub: github.com/554246839/r… master 分支