从零搭建pnpm + monorepo + vuepress2.x + vue3的组件库(四)

发布于:2024-05-10 ⋅ 阅读:(16) ⋅ 点赞:(0)

在之前发布的有关组件库系列的文章,已经基本实现了组件库的核心功能,有兴趣的可以进行阅读、点赞、评论:

本文主要解决以下几个问题:

  1. 解决代码暴露问题:组件库最终发布的代码里(即在别的项目中安装之后的内容),发现仍然包含打包前的组件代码,这部分代码和我们项目中使用的组件本身无关(因为我们使用的只是被打包编译之后的组件),却被额外暴露出去。
  2. 如何管理并发布打包编译之后代码
  3. 用简单的命令实现组件库打包
  4. 用简单的命令实现组件库版本号的更新以及发布到npm

1. 如何仅发布打包编译之后的代码,而不涉及组件库本身

思路:

  1. 组件库的核心代码与打包编译之后的代码,项目结构上不存在嵌套关系,只作为同级即可避免
  2. 配置rollup,使打出的包与组件库核心代码同级,并实现一次输出多种格式。
  3. 只针对打包编译之后的代码进行版本管理

1.1. 调整目录结构

|-- project
    |-- README.md
    |-- docs(组件库文档,省略)
    |-- dist(重点:组件库打包之后生成的文件夹,用于发布组件库)
    |-- packages(组件库核心文件夹)
        |-- again-ui(发布的组件库核心文件夹)
        |   |-- component.ts(组件类)
        |   |-- utils.ts(工具类)
        |   |-- index.ts(组件库入口文件)
        |   |-- package.json(重点:组件库版本管理)
        |   |-- README.md(重点:组件库使用文档)
        |-- components (自定义组件文件夹)
        |   |-- index.ts
        |   |-- package.json
        |   |-- Table
        |   |   |-- README.md
        |   |   |-- package.json
        |   |   |-- src
        |   |       |-- index.vue
        |   |       |-- main.ts
        |   |-- base-h1
        |       |-- index.vue
        |-- utils (自定义工具文件夹)
            |-- index.ts
            |-- package.json

1.2. 配置rollup实现多种输出格式

  1. ESM (ES Modules): 使用 ES 模块的格式进行打包,适用于现代浏览器和支持 ES6 模块的环境。可以通过 output.format 设置为 'es'。
  1. CJS (CommonJS): 使用 CommonJS 的格式进行打包,适用于 Node.js 环境和一些旧版浏览器。可以通过 output.format 设置为 'cjs'。

配置如下:

import path, { resolve } from 'path'
// 让 rollup 能够处理外部依赖。
import { nodeResolve } from '@rollup/plugin-node-resolve'
import esbuild from 'rollup-plugin-esbuild'
// 支持基于 CommonJS 模块方式 npm 包
import commonjs from '@rollup/plugin-commonjs'
import vuePlugin from 'rollup-plugin-vue'
import postcss from 'rollup-plugin-postcss'
import VueSetupExtend from 'unplugin-vue-setup-extend/rollup'

const __dirname = path.resolve()
// 项目根目录
const projRoot = path.resolve(__dirname)
// 输出文件夹路径 /dist
const outputPath = path.resolve(projRoot, 'dist') 
/** `/dist/again-ui` */
export const epOutput = resolve(outputPath, 'again-ui')
// 组件库目录
export const pkgRoot = resolve(projRoot, 'packages')

export default {
  input: 'packages/again-ui/index.ts',
  output: [
    {
      format: 'cjs',
      // 指定所有生成的 chunk 被放置在哪个目录中
      dir: 'dist/again-ui/lib',
      exports: 'named',
      // 当 output.preserveModules 值为 true 时,输入模块的目录路径应从 output.dir 路径中剥离出来。
      preserveModules: true,
      // 将保留的模块放置在根路径下的此路径下
      preserveModulesRoot: resolve(__dirname, 'packages', 'again-ui'),
      name: 'lib',
      sourcemap: true,
      entryFileNames: `[name].js`,
    },
    {
      format: 'esm',
      dir: 'dist/again-ui/es',
      exports: undefined,
      preserveModules: true,
      // 将保留的模块放置在根路径下的此路径下
      preserveModulesRoot: resolve(__dirname, 'packages', 'again-ui'),
      name: 'es',
      sourcemap: true,
      entryFileNames: `[name].mjs`,
    },
  ],
  plugins: [
    VueSetupExtend(),
    nodeResolve({
      extensions: ['.mjs', '.js', '.json', '.ts'],
    }),
    vuePlugin({
      // 引用的vue插件,即上述引入的插件使用一遍,以及添加一些选项
      include: /.vue$/,
      // 把单文件组件中的样式,插入到html中的style标签
      css: true,
      // 把组件转换成 render 函数
      //compileTemplate: true
    }),
    esbuild({
      // All options are optional
      include: /.[jt]sx?$/, // default, inferred from `loaders` option
      // exclude: /node_modules/, // default
      sourceMap: process.env.NODE_ENV === 'production',
      minify: process.env.NODE_ENV === 'production',
      target: 'es2018', // default, or 'es20XX', 'esnext'
      loaders: {
        '.vue': 'ts',
      },
      // tsconfig: 'tsconfig.json', // default
    }),
    postcss({
      // 把 css 放到和js同一目录
      extract: true,
      // 是否进行代码压缩
      // minimize: true,
      // 是否生成源映射文件
      // sourceMap: true,
      plugins: [],
    }),
    // 需放在postcss后面
    commonjs(),
  ],
  external: ['vue'],
}

2. 组件库版本管理与发布

经过上面的打包配置调整,在根目录下执行 rollup -c命令即可自动打包,同时打包出两个版本,分别是es和lib(common.js),打包结果如下:

至此,打包配置完成,但如果想发布dist/again-ui到npm上还需要给again-ui添加一个package.json文件,这个package.json文件可以直接把packages/again-ui/package.json复制一份过来,重点是配置版本号以及依赖,内容如下:

{
  "name": "again-ui",
  "version": "0.0.71", // 重点:组件库版本号
  "description": "vue3 + element-plus组件库",
  "keywords": [
    "element-plus",
    "element",
    "component library",
    "ui framework",
    "ui",
    "vue3"
  ],
  "main": "lib/index.js", // 组件库入口文件
  "license": "MIT",
  "author": "",
  "publishConfig": {
    "access": "public" // 重点:必须设置access为“public”,这样才可以通过npm安装和访问这个包
  },
  "peerDependencies": {
    "vue": ">=3.2.0"
  },
  "dependencies": {
    "element-plus": "^2.2.20",
    "@element-plus/icons-vue": "^2.3.1",
    "sass": "^1.56.1"
  }
}

这样每次打包之后就可以通过修改package.json中的version来修改组件库版本进行发布了。

但是每次打包完之后都需要把package.json文件复制过来,无疑增加了操作成本,不过这些都可以通过命令实现, 同时也可以通过命令复制一份README.md,这个文档就是组件库的使用文档,同时也会发布到npm上。只需在根目录下执行:

cp ./packages/again-ui/package.json ./packages/again-ui/README.md  dist/again-ui

为了方便操作,我们可以在项目根目录下配置一个scripts命令,来帮我们实现打包命令以及打包之后的一系列操作:

"scripts": {
  "copy": "cp ./packages/again-ui/package.json ./packages/again-ui/README.md  dist/again-ui",
  "build:ui": "rm -rf ./dist  &&  rollup -c && pnpm run copy",
},

通过上面的配置,我只需在根目录下执行一个命令即可实现组件库的一键打包:

pnpm run build:ui

2.1. 通过npm发布组件库

如果你按步骤实现了上面的内容,那么你离发布npm包只要一步之遥了,在之前的文章里就已经分享过这些内容了,详细的移步:

注意:发包前需要先更新版本号,你可以手动修改,也可以通过npm version <updateType>, 来更新版本。

npm version后面参数说明:
patch:小变动,比如修复 bug 等,版本号变动 v1.0.0->v1.0.1
minor:增加新功能,不影响现有功能,版本号变动 v1.0.0->v1.1.0
major:破坏模块对向后的兼容性,版本号变动 v1.0.0->v2.0.0

最后,在dist目录下执行:npm publish就可以一键发包啦!

3. 【进阶版】用脚本实现更新组件库版本号并发布

先来总结下上面我们都做了啥:

  1. 通过调整项目目录,配置打包的输入输出,我们实现了多种格式的组件库的输出。
  2. 在项目根目录下通过pnpm run build:ui帮我们简化了打包的流程(不用手动复制package.json以及README.md)。
  3. 发版前手动修改组件库版本号或者通过命令进行版本号更新。
  4. dist目录下执行npm publish进行版本发布。

可以看到,上面的第二到四步,都是我们每次更新版本并发布的必经之路,一会儿在这个目录下操作,一会儿在那个目录下操作,人工成本很高,而且时间长了很容易忘记操作流程,所以我这种懒人,就造轮子啦,通过一个脚本把第三步和第四步通过一个命令来实现组件库的版本更新和发布,效果如下:

3.1. 脚本实现思路

  1. 进入目标目录(dist/again-ui/)下
  2. 确定版本号,提供两种方式:手动/自动(手动:读取dist目录下的pakcge.json文件的version,并更换为手动输入的版本,然后发布;自动:先获取npm上最近一个已发布的版本号,然后基于这个版本号进行再更新,比如小版本更新、大版本更新)
  3. 版本号更新之后,执行发布命令。

3.2. 脚本实现:

核心代码:

import fs from 'fs'
// node的询问交互
import inquirer from 'inquirer'
import { exec } from 'child_process'
import {
  errorLog,
  execPublish,
  inputVersion,
  updateChoices,
  writeVersion,
} from './utils/index.js'

const packageName = 'again-ui'
// 指定目标目录
const targetDir = 'dist/again-ui'

// 检查目标目录是否存在
if (fs.existsSync(targetDir)) {
  // 进入目标目录
  process.chdir(targetDir)
  inquirer
    .prompt([
      {
        type: 'list',
        name: 'version',
        message: '请选择您要发布的版本类型:',
        default: 0,
        choices: updateChoices,
      },
    ])
    .then((answers) => {
      if (answers.version === 'input') {
        inputVersion()
      } else {
        const npmUpdateVersion = () => {
          exec(`npm version ${answers.version}`, (error, stdout, stderr) => {
            if (error) {
              console.error(`执行 npm install 出错: ${error}`)
              return
            }
            execPublish()
          })
        }
        // 读取已发布的版本号
        exec(`npm view ${packageName} version`, (error, stdout, stderr) => {
          if (error) {
            console.error(`执行 npm view 出错: ${error}`)
            return
          }

          const latestVersion = stdout.trim()
          writeVersion(latestVersion, npmUpdateVersion)
        })
      }
    })
} else {
  errorLog('请先执行打包命令:pnpm run build:ui!')
}

工具函数:

// 命令行颜色
import chalk from 'chalk'
// node的询问交互
import inquirer from 'inquirer'
import fs from 'fs'
import { exec } from 'child_process'

export const log = (message) => console.log(chalk.green(message))
export const errorLog = (message) => console.log(chalk.red(message))

export const updateChoices = [
  {
    value: 'patch',
    name: 'patch:小变动,比如修复 bug 等,版本号变动 v1.0.0->v1.0.1',
  },
  {
    value: 'minor',
    name: 'minor:增加新功能,不影响现有功能,版本号变动 v1.0.0->v1.1.0',
  },
  {
    value: 'major',
    name: 'major:破坏模块对向后的兼容性,版本号变动 v1.0.0->v2.0.0',
  },
  {
    value: 'input',
    name: '手动更新',
  },
]

// 执行npm包发布命令
export const execPublish = () => {
  exec(`npm publish`, (error, stdout, stderr) => {
    if (error) {
      console.error(`执行 npm publish 出错: ${error}`)
      return
    }
    log(`${stdout}当前版本发布成功`)
  })
}

// 写入更新后的 package.json 文件
export const writeVersion = (version, callback) => {
  // 读取 package.json 文件
  fs.readFile('package.json', 'utf8', (err, data) => {
    if (err) {
      console.error('读取 package.json 文件出错:', err)
      return
    }

    // 解析 JSON 数据
    const packageJson = JSON.parse(data)

    // 替换 version 字段
    packageJson.version = version

    // 写入更新后的 package.json 文件
    fs.writeFile(
      'package.json',
      JSON.stringify(packageJson, null, 2),
      'utf8',
      (err) => {
        if (err) {
          console.error('写入 package.json 文件出错:', err)
          return
        }
        log(`版本号更新成功,正在发布...`)
        if (callback) callback()
      }
    )
  })
}
// 手动输入版本号
export const inputVersion = () => {
  inquirer
    .prompt([
      {
        type: 'input',
        name: 'input',
        message: '请输入版本号(例如:1.2.0):',
      },
    ])
    .then((answers) => {
      console.log(`你输入的内容是:${answers.input}`)
      writeVersion(answers.input, execPublish)
    })
    .catch((error) => {
      console.error(error)
    })
}

4. Q&A

  1. 使用css module来控制样式隔离,如果要覆盖样式需要使用:global,rollup编译错误的写法:
:global{
  .el-table__column-filter-trigger {
    display: none;
  }
}

正确的写法:

:global .el-table__column-filter-trigger {
  display: none;
}
  1. 如果使用scoped的方式来样式隔离的话,需要注意deep覆盖样式的问题,可能rollup会报错,需要做一些配置来支持

5. 总结

本文先是通过调整项目目录,将组件库的核心代码与打包编译之后的代码进行拆离,以防止将组件库的源码发布出去;然后通过rollup配置实现组件库的多种格式输出,以便于在不同的环境下使用;最终通过一些脚本和命令简化组件库的更新以及发布的流程。从而实现:当你需要更新组件库的时候,不再需要关心组件库如何打包如何发布,只需要两个命令就能实现组件库的打包和发布,即:

// 打包编译组件库
pnpm run build:ui
// 更新组件库版本并发布到NPM
pnpm run update:version

网站公告

今日签到

点亮在社区的每一天
去签到