Monorepo在项目中的应用

Monorepo在项目中的应用

本文不会介绍什么monorepo,什么是pnpm。如果大家对这两个概念不是很清楚推荐学习

pnpm: pnpm.io/zh/motivati...

monorepo: juejin.cn/post/721588...

一、初始化项目

安装pnpm包

npm install pnpm

1.1 目录结构

lua 复制代码
📦my-project
 ┣ 📂packages
 ┃ ┣ 📂eslint-config-v2 (适用于vue2的统一eslint规范) 
 ┃ ┃ ┗ 📜package.json 
 ┃ ┣ 📂eslint-config-v3 (适用于vue3的统一eslint规范)
 ┃ ┃ ┗ 📜package.json
 ┃ ┣ 📂 utils  (工具包)
 ┃ ┃ ┗ 📜package.json 
 ┃ ┣ 📂vue  (vue2组件库文件夹)
 ┃ ┣ ┣ 📂component1
 ┃ ┣ ┣ ┗ 📜package.json
 ┃ ┣ ┗ 📜.eslintrc.ts (继承组eslint-config-v2的eslint规则) 
 ┃ ┣ 📂vue-next (vue3组件库文件夹) 
 ┃  ┣ ┣ 📂component1 
 ┃ ┣ ┣ ┗ 📜package.json 
 ┃ ┣ ┗ 📜.eslintrc.ts (继承自eslint-config-v3的eslint规则)
 ┣ 📂scripts (自定义脚本)
 ┃ ┣ 📂rollup (原生ts/js使用rollup打包方式)
 ┃ ┃ ┗ 📜package.json
 ┃ ┣ 📂vite-v2 (vue2项目使用vite vue2打包方式)
 ┃ ┃ ┗ 📜package.json
 ┃ ┣ 📂vite-v3 (vue3项目使用vite vue3打包方式) 
 ┃ ┃ ┗ 📜package.json
 ┣ 📜package.json
 ┣ 📜 .eslintrc.ts
 ┣ 📜 .gitignore
 ┣ 📜 .npmrc
 ┣ 📜 .prettierrc.js
 ┣ 📜 .stylelintrc.ts
 ┣ 📜 tsconfig.json
 ┣ 📜 parser-preset.cjs
 ┣ 📜 commitlint.config.ts
 ┣ 📜pnpm-workspace.yaml

项目说明: 该项目同时支持vue2和vue3版本的组件库,因此这里有vue和vue-next两个文件夹。

1.2 配置文件

arduino 复制代码
 ┣ 📜 .eslintrc.ts
 ┣ 📜 .gitignore
 ┣ 📜 .npmrc
 ┣ 📜 .prettierrc.js
 ┣ 📜 .stylelintrc.ts
 ┣ 📜 tsconfig.json
 ┣ 📜 parser-preset.cjs
 ┣ 📜 commitlint.config.ts
 ┣ 📜 pnpm-workspace.yaml

pnpm-workspace.ymal用来指定哪些文件夹支持pnpm安装依赖,配置如下:

markdown 复制代码
packages:  
  - packages/**
  - scripts/**

.npmrc用来配置指定下载依赖的registry,配置如下:

ini 复制代码
registry = https://****/  (默认源)

@{name}:registry = https://******/  (以指定name开头的依赖使用的源,根据项目自身情况而定)

commitlint.config.js用来指定对git提交代码的message进行规则,配置如下:

java 复制代码
module.exports = {

  extends: ['@commitlint/config-conventional'],

  parserPreset: './parser-preset.cjs',

  rules: {

    'type-enum': [

      // commit的type类型

      2,

      'always',

      [

        'feat', // 新功能

        'fix', // 修补bug

        'docs', // 文档变动

        'style', // 格式(不影响代码运行的提交)

        'refactor', // 重构(既不是新功能,也不是bug代码的变动)

        'test', // 增加测试

        'revert', // 回滚

        'config', // 构建工具或者辅助工具的变动

        'chore', // 其他改动

        'dev' // 开发

      ]

    ],

    'header-max-length': [2, 'always', 180],

    'type-empty': [2, 'never'], // 提交不符合规范时,可以提交,但会有警告

    'subject-empty': [2, 'never'], // 提交不符合规范时,可以提交,但会有警告

    'subject-full-stop': [0, 'never'],

    'subject-case': [0, 'never']

  }

}

parser-preset.cjs用来自定义git提交时候对提交的messgae校验规则,配置如下:

通过正则匹配,强制要求提交格式为: type: --story|bug=** --user=**; user表示提交人,story/bug跟需求相关

css 复制代码
module.exports = {

  parserOpts: {

    headerPattern:

      /^(\w+)\((\w+)\):\s--(story|bug)=(\d+)\s--user=(.+)(\s--module=(.+))?/,

    headerCorrespondence: ['type', 'scope', 'ticket', 'subject']

  }

}

1.3 代码规范

  1. 同一项目不同目录使用不同的eslint规范
  1. Husky pre-commit代码检查

pre-commit配置

bash 复制代码
#!/usr/bin/env sh
. "$(dirname -- "$0")/husky.sh"  
npm run eslint

husky.sh配置

bash 复制代码
#!/usr/bin/env sh

if [ -z "$husky_skip_init" ]; then

  debug () {

    if [ "$HUSKY_DEBUG" = "1" ]; then

      echo "husky (debug) - $1"

    fi

  }

  readonly hook_name="$(basename -- "$0")"

  debug "starting $hook_name..."

  if [ "$HUSKY" = "0" ]; then

    debug "HUSKY env variable is set to 0, skipping hook"

    exit 0

  fi
 

  if [ -f ~/.huskyrc ]; then

    debug "sourcing ~/.huskyrc"

    . ~/.huskyrc

  fi

  readonly husky_skip_init=1

  export husky_skip_init

  sh -e "$0" "$@"

  exitCode="$?"

  if [ $exitCode != 0 ]; then

    echo "husky - $hook_name hook exited with code $exitCode (error)"

  fi

  if [ $exitCode = 127 ]; then

    echo "husky - command not found in PATH=$PATH"

  fi

  exit $exitCode

fi

1.4 快速构建

CI命令

CI命令放在scripts文件目录下,便于一键部署和发布,包含build,publish,eslint等

  • 构建命令

pnpm run build --filter [packageName]

或者pnpm run build

  • 发布命令,支持major、minor、patch、beta、alpha

pnpm run publish --filter [packageName] --version [major|minor|patch|beta|alpha]

或者 pnpm run publish --version [major|minor|patch|beta|alpha]

  • eslint命令

pnpm run eslint --filter [packageName]

或者 pnpm run eslint

rollup,vue2和vue3多种方式构建

首先在组件包的package.json中添加

json 复制代码
// 自定义构建参数
"buildOptions": {
    "type": "rollup", //  构建类型,选择rollup方式构建
    "shouldEmitDeclarations": true,  // 是否输出ts声明文件
    "isTS": false  // 是否是ts编译
  },
  1. 采用rollup构建方式构建纯js/ts文件,rollup/build.js

    参考JavaScript API | Rollup (rollupjs.org)

javascript 复制代码
const path = require('path')
const { rollup } = require('rollup')
const ts = require('rollup-plugin-typescript2') // 打包typescript项目
const json = require('@rollup/plugin-json') // 从json文件中读取数据
const peerDepsExternal = require('rollup-plugin-peer-deps-external') // 防止打包node_modules下的文件
const terser = require('@rollup/plugin-terser') // 压缩打包文件
// const resolve = require('@rollup/plugin-node-resolve')
const pathResolve = (el, filePath) => {
  return path.resolve(process.cwd(), `${el.path}/${filePath}`)
} 

const getInputOptions = el => {
  // 是否输出声明文件
  const shouldEmitDeclarations = el.buildOptions.shouldEmitDeclarations
  const tsPlugin = ts({
    tsconfig: pathResolve(el, './tsconfig.json'),
    useTsconfigDeclarationDir: true,
    tsconfigOverride: {
      compilerOptions: {
        target: 'es2015',
        sourceMap: true,
        declaration: shouldEmitDeclarations,
        declarationMap: shouldEmitDeclarations
      }
    }
  })

  const plugins = [
    peerDepsExternal(),
    // resolve(pathResolve(el, `src`), ['.js', '.ts']),
    json({
      namedExports: false
    })
    // terser()
    // ...plugins
  ]

  if (el.buildOptions.isTS) {
    plugins.push(tsPlugin)
  }

  const inputOptions = {
    input: pathResolve(el, `src/index.${el.buildOptions.isTS ? 'ts' : 'js'}`),
    // Global and Browser ESM builds inlines everything so that they can be
    // used alone.
    external: [
      'axios',
      'lodash',
      'qs',
      pathResolve(el, 'node_modules'),
      pathResolve(el, '../node_modules')
    ],

    plugins,
    onwarn: (warning, warn) => {
      if (warning.code === 'THIS_IS_UNDEFINED') {
        return
      }
      if (!/Circular/.test(warning.message)) {
        warn(warning)
      }
    },

    treeshake: {
      moduleSideEffects: false
    }
  }
  return inputOptions
}

const getOutputOptionsList = el => {
  const outputConfigs = {
    esm: {
      file: pathResolve(el, 'lib/index.esm.js'),
      format: 'es'
    },
    cjs: {
      file: pathResolve(el, 'lib/index.cjs.js'),
      format: 'cjs'
    },
    global: {
      file: pathResolve(el, 'lib/index.global.js'),
      format: 'iife'
    }
  }

  const packageFormats = ['esm', 'cjs']
  const outputOptionsList = packageFormats.map(format => outputConfigs[format])
  return outputOptionsList
} 

const generateOutputs = async (bundle, el) => {
  console.log('getOutputOptionsList(el)', getOutputOptionsList(el))
  for (const outputOptions of getOutputOptionsList(el)) {
    // 生成特定于输出的内存中代码
    // 你可以在同一个 bundle 对象上多次调用此函数
    // 使用 bundle.write 代替 bundle.generate 直接写入磁盘
    await bundle.write(outputOptions)
  }
}

const buildComponent = async el => {
  let bundle
  try {
    // 启动一次打包,多次输出
    bundle = await rollup(getInputOptions(el))
    await generateOutputs(bundle, el)
  } catch (error) {
    bundle = 'error'
    console.log('error', error)
  }
  return bundle
} 

module.exports = buildComponent
  1. 采用vite构建方式构建纯vue2文件,vite/build.js

    参考: JavaScript API | Vite (vitejs.dev)

javascript 复制代码
const { build } = require('vite')
const path = require('path')
const dts = require('vite-plugin-dts') // 生成申明文件
const { createVuePlugin } = require('vite-plugin-vue2')
const cssInjectedByJsPlugin = require('vite-plugin-css-injected-by-js').default
const resolve = (el, filePath) => {
  return path.resolve(process.cwd(), `${el.path}/${filePath}`)
}

const getConfig = el => {
  const isPrd = false 
  const config = {
    configFile: false,
    // base: '/',
    build: {
      outDir: resolve(el, `dist`),
      assetsDir: '',
      target: 'es2015',
      minify: isPrd ? 'terser' : false,
      /** 在打包代码时移除 console.log、debugger 和 注释 */
      terserOptions: {
        compress: {
          drop_console: false,
          drop_debugger: true,
          pure_funcs: ['console.log']
        },
        format: {
          /** 删除注释 */
          comments: false
        }
      },
      rollupOptions: {
        external: [
          'lodash',
          'vue',
          '@vue/composition-api',
          'axios',
          resolve(el, 'node_modules'),
          path.resolve(process.cwd(), `node_modules`)
        ],
        output: {
          globals: {
            vue: 'Vue',
            _: 'lodash'
          }
        }
      },
      lib: {
        entry: resolve(el, 'src/index.ts'),
        name: 'index',
        formats: ['es', 'cjs'],
        fileName: format => `index.${format}.js`
      }
    },
    plugins: [
      cssInjectedByJsPlugin(),
      createVuePlugin({
        vueTemplateOptions: {}
      })
      // dts({
      //   // root: resolve(el, ''),
      //   root: process.cwd(),
      //   outputDir: 'types',
      //   exclude: [
      //     'node_modules',
      //     'package/**/node_modules',
      //     'package/vue/**/node_modules',
      //     'package/vue-next/**/node_modules'
      //   ],
      //   include: [
      //     path.resolve(process.cwd(), `packages/vue`),
      //     path.resolve(process.cwd(), `packages/shared`)
      //   ],
      //   tsConfigFilePath: './tsconfig.json'
      // })
    ]
  }
  return config
}

const buildComponent = async el => {
  try {
    await build(getConfig(el))
  } catch (error) {
    throw new Error(error)
  }
} 

module.exports = buildComponent
  1. 采用vite构建方式构建纯vue3文件,vite-next/build.js

    参考: JavaScript API | Vite (vitejs.dev)

javascript 复制代码
const { build, loadEnv } = require('vite')
const path = require('path')
const vue = require('@vitejs/plugin-vue')
const dts = require('vite-plugin-dts') // 生成申明文件
const cssInjectedByJsPlugin = require('vite-plugin-css-injected-by-js').default
const resolve = (el, filePath) => {
  return path.resolve(process.cwd(), `${el.path}/${filePath}`)
}

const getConfig = el => {
  const isPrd = false
  const config = {
    configFile: false,
    // base: '/',
    build: {
      outDir: resolve(el, `dist`),
      assetsDir: '',
      target: 'es2015',
      minify: isPrd ? 'terser' : false,
      /** 在打包代码时移除 console.log、debugger 和 注释 */
      terserOptions: {
        compress: {
          drop_console: false,
          drop_debugger: true,
          pure_funcs: ['console.log']
        },
        format: {
          /** 删除注释 */
          comments: false
        }
      },
      rollupOptions: {
        external: [
          'lodash',
          'vue',
          'axios',
          resolve(el, 'node_modules'),
          path.resolve(process.cwd(), `node_modules`)
        ]
      },
      lib: {
        entry: resolve(el, 'src/index.ts'),
        name: 'index',
        formats: ['es', 'umd', 'cjs'],
        fileName: format => `index.${format}.js`
      },
      // 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
      globals: {
        vue: 'Vue'
      }
    },
    plugins: [
      cssInjectedByJsPlugin(),
      vue()
      // dts({
      //   root: process.cwd(),
      //   outputDir: 'types',
      //   exclude:[],
      //   tsConfigFilePath: './tsconfig.json'
      // })
    ]
  }
  return config
}

const buildComponent = async el => {
  try {
    await build(getConfig(el))
  } catch (error) {
  }
}
module.exports = buildComponent

版本管理

  1. 实现

依赖standardVersion包自动升级版本号,核心代码如下

typescript 复制代码
const options = {
    // 设置md的路径
    infile: pathResolve(el, 'CHANGELOG.md'),
    // 设置package.json的路径
    packageFiles: [pathResolve(el, 'package.json')],
    message, // 打tag的message
    silent: false,
    tagPrefix: el.path
  }

  if (['major', 'minor', 'patch'].includes(version)) {
    options.releaseAs = version || 'patch'
  } else {
    options.prerelease = version || 'beta'
  }

  if (!tag) {
    tag = version === 'beta' ? 'beta' : 'latest'
  }

  standardVersion(options)
    .then(() => {
      // standard-version is done
      shell.exec(
        `pnpm --filter ${el.name} publish --no-git-checks --tag ${tag}`,
        (code, stout, stderr) => {
          console.log('stout', stout)
          console.log('code', code)
        }
      )
    })
    .catch(err => {
      console.error(`standard-version failed with message: ${err.message}`)
    })
  1. 规则
  • 版本号自动累加。如:1.0.2 -> 1.0.3 1.0.2.beta.1 -> 1.0.2.beta.2
  • 只有 latestbeta 两个标签
  • beta版本号从0开始,比如:1.0.0-beta.0 -> 1.0.0-beta.1
  • latest tag永远指向最新的稳定版本
  • beta tag永远指向最新的公测版本
  • 提交beta版本时,pnpm publish时必须加上 --version beta 参数

changelog记录变动

安装

pnpm install @changesets/cli

pnpm install @commitlint/config-conventional

配置

单独组件拥有自己的changelog

二、问题记录

  1. 同一个项目同时安装vue2和vue3,在启动过程会遇到解析错误;

根本原因,vue2在对模板解析的时候通过require('vue')查到vue3版本

解决办法: 通过打补丁的方式,将这个引用改为require('vue@2.6.14')

这是打完补丁之后的文件,将这个文件提交上去就可以了!

相关推荐
awonw3 分钟前
[前端][easyui]easyui select 默认值
前端·javascript·easyui
九圣残炎23 分钟前
【Vue】vue-admin-template项目搭建
前端·vue.js·arcgis
柏箱1 小时前
使用JavaScript写一个网页端的四则运算器
前端·javascript·css
TU^1 小时前
C语言习题~day16
c语言·前端·算法
弥琉撒到我3 小时前
微服务swagger解析部署使用全流程
java·微服务·架构·swagger
学习使我快乐014 小时前
JS进阶 3——深入面向对象、原型
开发语言·前端·javascript
bobostudio19954 小时前
TypeScript 设计模式之【策略模式】
前端·javascript·设计模式·typescript·策略模式
黄尚圈圈5 小时前
Vue 中引入 ECharts 的详细步骤与示例
前端·vue.js·echarts
浮华似水6 小时前
简洁之道 - React Hook Form
前端