monorepo架构从0创建js软件包

142 阅读5分钟

用git管理代码,先创建好远程库

然后git clone拷到本地库

使用pnpm

npm install pnpm -g

pnpm init创建package.js文件

得到 package.json 初始内容,然后把 package.json 中的 name 属性删掉,并且添加一个 "private": true 属性,因为它是不需要发布的。

{
  "private": true,
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo "Error: no test specified" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

配置 pnpm 的 monorepo 工作区

在这个仓库下,我打算建立多个模块,最基础的组件源码模块、examples示例模块,就可以采用 monorepo 架构。我们在仓库的根目录下创建一个 pnpm-workspace.yaml 文件,可以在 pnpm-workspace.yaml 配置文件中指定这个仓库中有多少个项目。

packages:
  # packages/ 直接子目录中的所有包
  - 'packages/*'

安装必要依赖

  • 支持ts
  • babel转换
  • 自动更新,实时刷新页面效果
  • 打包
pnpm install -w --save-dev @babel/core @babel/cli @babel/preset-env @babel/preset-typescript nodemon lite-server concurrently typescript
依赖包作用
@babel/coreBabel 核心编译库
@babel/cliBabel 命令行工具
@babel/preset-env智能转换至目标浏览器 ES
@babel/preset-typescript支持 TS 转 JS
nodemon监听文件变化重启任务
lite-server轻量静态服务器+自动刷新
concurrently  并行执行多个命令

打包与babel

pnpm install rollup --save-dev -w
pnpm i -D -w @rollup/plugin-babel @rollup/plugin-node-resolve @rollup/plugin-typescript rollup-plugin-dts

Rollup 与其它工具的集成 | rollup.js 中文文档 | rollup.js中文网

因为采用monorepo架构,需要各个模块都配置一下rollup配置文件

组件源代码模块,该模块的要求是能实时更新,ts自动打包,并用babel转换为js,生成.d.ts类型声明文件

import resolve from '@rollup/plugin-node-resolve';
import babel from '@rollup/plugin-babel';
import typescript from '@rollup/plugin-typescript';
import dts from 'rollup-plugin-dts';
import path from "path";
import { fileURLToPath } from "url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// 主构建配置(生成 JS 和 .d.ts)
const config = {
  input: 'src/index.ts',    // 入口文件
  output: {
    file: 'dist/index.js', // 输出文件
    format: 'esm'           // 输出格式(ES Module)
  },
  plugins: [
    resolve({
      extensions: ['.ts', '.js']
    }),
    typescript({
      tsconfig: path.resolve(__dirname, '../../tsconfig.json'),
      outDir: 'dist',
      declarationDir: 'dist',
      outputToFilesystem: true,
    }),
    babel({
      babelHelpers: 'bundled',
      // 关键:指定根目录 .babelrc 绝对路径
      configFile: path.resolve(__dirname, '../../.babelrc'),
    })
  ]
};

// 单独生成 .d.ts 的配置(可选)
const dtsConfig = {
  input: 'src/index.ts',
  output: {
    file: 'dist/index.d.ts',
    format: 'esm'
  },
  plugins: [dts()]
};

export default [config, dtsConfig]


examples示例模块,该模块的要求是src有html、css、ts等示例代码,开发是能自动启动开发服务器,自动打包为浏览器能运行的格式,实时更新

在 examples 模块中安装必要的 Rollup 插件和工具:

pnpm add -D -w rollup-plugin-copy rollup-plugin-replace npm-run-all rollup-plugin-postcss @rollup/plugin-image
依赖包作用
rollup-plugin-copy复制静态文件(HTML、CSS等)
rollup-plugin-replace在 HTML 中替换 TS 引用为 JS
rollup-plugin-postcss处理 CSS(提取、压缩、模块化)
@rollup/plugin-image处理图片(Base64 或复制文件)
npm-run-all控制执行顺序
// rollup.config.mjs
import resolve from '@rollup/plugin-node-resolve';
import babel from '@rollup/plugin-babel';
import typescript from '@rollup/plugin-typescript';
import copy from 'rollup-plugin-copy';
import postcss from 'rollup-plugin-postcss';
import image from '@rollup/plugin-image';
import path from "path";
import { fileURLToPath } from "url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export default {
  input: 'src/index.ts',    // 入口文件
  output: {
    dir: 'dist',
    format: 'esm',           // 输出格式(ES Module)
    sourcemap: true,        // 生成 SourceMap
  },
  plugins: [
    resolve(),
    typescript(),
    babel({
      babelHelpers: 'bundled',
      // 关键:指定根目录 .babelrc 绝对路径
      configFile: path.resolve(__dirname, '../../.babelrc'),
    }),
    postcss(),
    image(),
    // 复制html文件到dist目录
    copy({
      targets: [
        {
          src: 'src/**/*.html',
          dest: 'dist',
          // 替换html中的.ts引用为.js
          transform: (content, filePath) => {
            return Buffer.from(
              content.toString()
                .replace(/<script.*src="(.*)\.ts".*<\/script>/g, '<script type="module" src="$1.js"></script>')
            );
          }
        },
        {
          src: 'src/**/*.{css,jpg,png}',
          dest: 'dist',
        }
      ],
      flatten: false, // 禁止扁平化路径(关键!)
    })
  ],
  watch: {                  // 监听模式配置
    include: 'src/**',
    exclude: 'node_modules/**'
  }
};


// package.json相关配置
"scripts": {
  "build": "rollup -c rollup.config.mjs",
  "watch": "rollup -c rollup.config.mjs --watch",
  "serve": "lite-server --baseDir=dist",
  "dev": "npm-run-all build --parallel watch serve"
},

解决html、css变化未更新复制dist问题

上面的配置解决了大部分问题,但还不够完美,rollup默认只会监听主文件及其依赖变化重新构建,作为静态文件的html、css变化不会触发rollup的复制插件执行

需要手动将html、css等需要监听的静态文件加入rollup的依赖图谱

安装 glob 文件匹配库

pnpm add -D globby -F examples

修改 Rollup 配置

import path from 'path';
import { globbySync } from 'globby';

export default {
  plugins: [
    // ...其他插件
    {
      name: 'watch-static-files',
      async buildStart() {
        // 批量获取所有 HTML/CSS 文件路径
        const files = await globbySync([
          'src/**/*.html',
          'src/**/*.css',
          'src/**/*.{jpg,png}' // 可选:监听图片变化
        ], { absolute: true });

        // 将文件添加到 Rollup 监听列表
        files.forEach(file => {
          this.addWatchFile(file);
        });
      },
    },
    copy({
      targets: [/* 同上 */],
      hook: 'writeBundle',
      watch: true,
    })
  ],
};

monorepo架构常用命令

# 安装到根目录
pnpm add @vueapps/utils

# 安装包到指定模块(从公共仓库找)
pnpm add @my-monorepo/core --filter @my-monorepo/app

# 本地模块安装到其他模块
pnpm add @my-monorepo/core --workspace --filter @my-monorepo/app

# 构建 core 模块(若未自动构建)
pnpm --filter @my-monorepo/core run build

# 启动 app 模块
pnpm --filter @my-monorepo/app run dev

子模块已配置好,开发时只需要根目录启动即可

"scripts": {
  "dev": "concurrently \"pnpm --filter testComponent run watch\" \"pnpm --filter examples run dev\""
},
pnpm dev

问题

使用CommonJS模块包报错问题

比如使用spark-md5组件,该组件已正确安装了,并且使用方式正确,但实际使用时,报: Uncaught (in promise) TypeError: undefined is not a constructor

如果通过 import * as SparkMD5 from 'spark-md5' 导入,但库的实际导出方式与 TypeScript 预期不符,会导致 ArrayBuffer 未被正确挂载。

解决方式:确保 rollup.config.js 包含以下插件配置

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

export default {
  plugins: [
    resolve({
      browser: true // 确保浏览器环境解析
    }),
    commonjs({
      requireReturnsDefault: 'predefined', // 关键配置
      dynamicRequireTargets: ['node_modules/spark-md5/*']
    })
  ]
};