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 代码规范
- 同一项目不同目录使用不同的eslint规范
- 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编译
},
-
采用rollup构建方式构建纯js/ts文件,rollup/build.js
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
-
采用vite构建方式构建纯vue2文件,vite/build.js
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
-
采用vite构建方式构建纯vue3文件,vite-next/build.js
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
版本管理
- 实现
依赖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.0.2 -> 1.0.3 1.0.2.beta.1 -> 1.0.2.beta.2
- 只有
latest
和beta
两个标签 - 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
二、问题记录
- 同一个项目同时安装vue2和vue3,在启动过程会遇到解析错误;
根本原因,vue2在对模板解析的时候通过require('vue')查到vue3版本
解决办法: 通过打补丁的方式,将这个引用改为require('vue@2.6.14')
这是打完补丁之后的文件,将这个文件提交上去就可以了!