从零配置 Monorepo 组件库:完整指南
本文详细介绍了从零配置monorepo组件库的全流程,包含Lerna多包管理、ESLint/Stylelint代码规范、Husky/Commitlint提交规范、Webpack打包配置、VuePress文档搭建等核心环节。重点展示了如何通过自动化脚本实现组件创建和文档更新,解决monorepo项目中的依赖管理和发布问题。文章不仅提供了具体配置方法,还解释了各项工具的原理和使用场景,分享了实际配置过
简介
本文将从零开始配置一个 monorepo 类型的组件库,涵盖规范化配置、打包配置、组件库文档配置以及开发效率提升脚本等内容。对于不熟悉 monorepo 的读者,这里简单介绍一下:monorepo 是指在一个 git 仓库中包含多个独立发布的模块/包。
PS:本文涉及到的工具配置在平时开发中通常不需要手动配置,各种脚手架已经帮我们完成了这些工作。但了解这些配置的含义和原理仍然很有价值。作为有多年开发经验的前端开发者,笔者也是第一次动手配置这些工具,因此除了介绍配置方法,还会分享遇到的问题及解决方案,并尽量解释每个参数的含义和原理。
使用 Lerna 管理项目
在组件库中,每个组件都是一个独立的 npm 包,但组件之间可能存在依赖关系。当某个组件修复 bug 并发布新版本后,需要手动升级依赖该组件的其他组件,这个过程既繁琐又低效。Lerna 就是专门用于管理多包 JavaScript 项目的工具,可以自动化处理 npm 发布和 git 上传。
初始化 Lerna
首先全局安装 Lerna:
bash
npm i -g lerna
然后进入仓库目录执行:
bash
lerna init
这个命令用于创建新的 lerna 仓库或升级现有仓库的 lerna 版本。Lerna 有两种使用模式:
-
固定模式(默认):所有包的主版本号和次版本号都使用 lerna.json 配置中的 version 字段
-
独立模式:每个包使用独立的版本号
初始化后生成的目录结构如下:
text
. ├── lerna.json ├── package.json └── packages/
需要手动创建 .gitignore 文件,目前只需忽略 node_modules 目录。
包管理
所有包都放在 packages 文件夹下,可以使用 lerna create xxx 命令添加新包。对于组件库,推荐给包名添加统一的作用域(scope),避免命名冲突。
添加依赖可以使用 lerna add module-1 --scope=module-2 命令,这会将 module-1 安装到 module-2 的依赖中。如果依赖的包是项目内的,Lerna 会直接创建链接。
发布配置
发布时使用 lerna publish 命令,该命令会完成模块发布和 git 上传工作。需要注意的是,带作用域的包在 npm 发布时需要添加 --access public 参数,但 lerna publish 不支持该参数。解决方法是在所有包的 package.json 中添加:
json
{
"publishConfig": {
"access": "publish"
}
}
规范化配置
ESLint
ESLint 是配置化的 JavaScript 代码检查工具,可以约束代码风格并检测潜在错误。
安装 ESLint:
bash
npm i eslint --save-dev
在 package.json 中添加初始化命令:
json
{
"scripts": {
"lint:init": "eslint --init"
}
}
执行 npm run lint:init 创建 ESLint 配置文件,根据提示回答问题后生成配置。
创建 .eslintignore 文件:
text
node_modules docs dist assets
在 package.json 中添加检查命令:
json
{
"scripts": {
"lint": "eslint ./ --fix"
}
}
Husky
为了在 git 提交前强制进行代码检查,可以使用 Husky 工具。
安装 Husky:
bash
npm i husky@4 --save-dev
在 package.json 中配置:
json
{
"husky": {
"hooks": {
"pre-commit": "npm run lint"
}
}
}
lint-staged
为了提高检查效率,只检查暂存区的文件,可以使用 lint-staged:
bash
npm i lint-staged --save-dev
配置 package.json:
json
{
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{js,vue}": [
"eslint --fix"
]
}
}
Stylelint
Stylelint 是用于检查 CSS 语法的工具,支持 CSS 预处理语言。
安装 Stylelint:
bash
npm i stylelint stylelint-config-standard --save-dev
创建配置文件 .stylelintrc:
json
{
"extends": "stylelint-config-standard"
}
创建忽略文件 .stylelintignore:
text
node_modules
添加检查命令:
json
{
"scripts": {
"style:lint": "stylelint packages/**/*.{css,less} --fix"
}
}
在 lint-staged 中配置:
json
{
"lint-staged": {
"*.{css,less}": [
"stylelint --fix"
]
}
}
Commitlint
为了规范 commit 信息,可以使用 Commitlint 进行检查。
安装 Commitlint:
bash
npm i --save-dev @commitlint/config-conventional @commitlint/cli
创建配置文件 commitlint.config.js:
javascript
module.exports = {
extends: ['@commitlint/config-conventional']
}
配置 Husky:
json
{
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
}
}
Commitizen
Commitizen 提供交互式的 commit 信息输入。
全局安装 Commitizen:
bash
npm install commitizen -g
初始化:
bash
commitizen init cz-conventional-changelog --save-dev --save-exact
在 package.json 中添加提示:
json
{
"husky": {
"hooks": {
"prepare-commit-msg": "echo ----------------please use [git cz] command instead of [git commit]----------------"
}
}
}
打包配置
组件库可以直接发布源码,但如果需要在项目中使用,需要在项目的 vue.config.js 中添加转译配置:
javascript
{
transpileDependencies: [
'module-x'
]
}
Webpack 配置
安装相关工具:
bash
npm i webpack less less-loader css-loader style-loader vue-loader vue-template-compiler babel-loader @babel/core @babel/cli @babel/preset-env url-loader clean-webpack-plugin -D
创建打包脚本 ./bin/buildModule.js:
javascript
const webpack = require('webpack')
const path = require('path')
const fs = require('fs-extra')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const { VueLoaderPlugin } = require('vue-loader')
// 获取命令行参数
const args = process.argv.slice(2)
// 生成 webpack 配置
const createConfigList = () => {
const pkgPath = path.join(__dirname, '../', 'packages')
const dirs = args.length > 0 ? args : fs.readdirSync(pkgPath)
return dirs.map((item) => {
return {
entry: path.join(pkgPath, item, 'index.js'),
output: {
filename: 'index.js',
path: path.resolve(pkgPath, item, 'dist'),
library: item,
libraryTarget: 'umd',
libraryExport: 'default'
},
target: ['web', 'es5'],
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader']
},
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.js$/,
loader: 'babel-loader'
},
{
test: /\.(png|jpe?g|gif)$/i,
loader: 'url-loader',
options: {
esModule: false
}
}
]
},
plugins: [
new VueLoaderPlugin(),
new CleanWebpackPlugin()
]
}
})
}
// 开始打包
webpack(createConfigList(), (err, stats) => {
// 处理和结果处理...
})
并行打包优化
使用 parallel-webpack 提高打包速度:
bash
npm i parallel-webpack -D
修改配置为导出模式:
javascript
// config.js module.exports = createConfigList()
创建运行文件:
javascript
// run.js
const run = require('parallel-webpack').run
const configPath = require.resolve('./config.js')
run(configPath, {
watch: false,
maxRetries: 1,
stats: true
})
组件文档配置
使用 VuePress 搭建组件文档,如果在项目根目录安装遇到 webpack 版本冲突,可以在 ./docs 目录下单独安装:
bash
cd ./docs npm init npm install -D vuepress
配置解析路径
在 VuePress 配置文件中修改 webpack 配置,使其能够解析 packages 目录下的组件:
javascript
// config.js
const path = require('path')
module.exports = {
chainWebpack: (config) => {
const pkgPath = path.resolve(__dirname, '../../../', 'packages')
config.resolve
.modules
.add(pkgPath)
.add('node_modules')
config.resolve
.alias
.set('@zf', pkgPath)
}
}
注册组件
在 enhanceApp.js 中注册组件:
javascript
import Rate from '@zf/rate'
export default ({ Vue }) => {
Vue.use(Rate)
}
开发阶段使用源码入口:
json
{
"main": "index.js"
}
发布阶段使用打包后入口:
json
{
"main": "dist/index.js"
}
使用脚本新增组件
新增组件的传统步骤繁琐且容易遗漏,可以通过脚本自动化完成。
初始化脚本
创建 ./bin/add.js 文件:
javascript
const { exec } = require('child_process')
const inquirer = require('inquirer')
const ora = require('ora')
const scope = '@zf/'
inquirer
.prompt([{
type: 'input',
name: 'name',
message: '请输入组件名称',
validate(input) {
const done = this.async();
input = String(input).trim()
if (!input) {
return done('请输入组件名称')
}
const spinner = ora('正在检查包名是否存在').start()
exec(`npm search ${scope + input}`, (err, stdout) => {
spinner.stop()
if (err) {
done('检查包名是否存在失败,请重试')
} else {
if (/No matches/.test(stdout)) {
done(null, true)
} else {
done('该包名已存在,请修改')
}
}
})
}
}])
.then(answers => {
// 处理输入结果
console.log(answers)
})
.catch(error => {
// 错误处理
});
使用模板创建文件
在 ./bin/template 目录下创建模板文件,使用 json-templater 进行动态内容注入:
javascript
const upperCamelCase = require('uppercamelcase')
const render = require('json-templater/string')
const renderTemplateAndCreate = (file, data = {}, dest) => {
const templateContent = fs.readFileSync(path.join(templateDir, file), {
encoding: 'utf-8'
})
const fileContent = render(templateContent, data)
fs.writeFileSync(path.join(dest, file), fileContent, {
encoding: 'utf-8'
})
}
const create = ({ name }) => {
const destDir = path.join(__dirname, '../', 'packages', name)
const srcDir = path.join(destDir, 'src')
fs.ensureDirSync(destDir)
fs.ensureDirSync(srcDir)
// 复制固定文件
fs.copySync(path.join(templateDir, 'index.js'), path.join(destDir, 'index.js'))
fs.copySync(path.join(templateDir, 'style.less'), path.join(srcDir, 'style.less'))
// 渲染模板文件
renderTemplateAndCreate('package.json', {
name: scope + name
}, destDir)
renderTemplateAndCreate('index.vue', {
name: upperCamelCase(name)
}, srcDir)
}
使用 AST 修改配置文件
使用 Babel 工具链解析和修改 enhanceApp.js 和 config.js:
javascript
const parse = require('@babel/parser').parse
const traverse = require('@babel/traverse').default
const t = require("@babel/types")
const generate = require('@babel/generator').default
const updateEnhanceApp = ({ name }) => {
const filePath = path.join(__dirname, '../', 'docs', 'docs', '.vuepress', 'enhanceApp.js')
const code = fs.readFileSync(filePath, { encoding: 'utf-8' })
const ast = parse(code, { sourceType: "module" })
traverse(ast, {
Program(nodePath) {
// 添加 import 语句
let bodyNodesList = nodePath.node.body
let lastImportIndex = -1
for (let i = 0; i < bodyNodesList.length; i++) {
if (t.isImportDeclaration(bodyNodesList[i])) {
lastImportIndex = i
}
}
const newImportNode = t.importDeclaration(
[t.ImportDefaultSpecifier(t.Identifier(upperCamelCase(name)))],
t.StringLiteral(scope + name)
)
if (lastImportIndex === -1) {
let firstPath = nodePath.get('body.0')
firstPath.insertBefore(newImportNode)
} else {
let lastImportPath = nodePath.get(`body.${lastImportIndex}`)
lastImportPath.insertAfter(newImportNode)
}
},
ExportDefaultDeclaration(nodePath) {
// 添加 Vue.use 语句
let bodyNodesList = nodePath.node.declaration.body.body
let lastIndex = -1
for (let i = 0; i < bodyNodesList.length; i++) {
let node = bodyNodesList[i]
if (
t.isExpressionStatement(node) &&
t.isCallExpression(node.expression) &&
t.isMemberExpression(node.expression.callee) &&
node.expression.callee.object.name === 'Vue' &&
node.expression.callee.property.name === 'use'
) {
lastIndex = i
}
}
const newNode = t.expressionStatement(
t.callExpression(
t.memberExpression(
t.identifier('Vue'),
t.identifier('use')
),
[t.identifier(upperCamelCase(name))]
)
)
if (lastIndex === -1) {
if (bodyNodesList.length > 0) {
let firstPath = nodePath.get('declaration.body.body.0')
firstPath.insertBefore(newNode)
} else {
let bodyPath = nodePath.get('declaration.body')
bodyPath.pushContainer('body', newNode)
}
} else {
let lastPath = nodePath.get(`declaration.body.body.${lastIndex}`)
lastPath.insertAfter(newNode)
}
}
});
const newCode = generate(ast)
fs.writeFileSync(filePath, newCode.code, { encoding: 'utf-8' })
}
通过以上脚本,只需运行 npm run add 命令即可自动化创建新组件,大大提高了开发效率。
总结
本文详细介绍了从零开始配置 monorepo 组件库的完整流程,包括项目结构管理、代码规范化、打包配置、文档搭建以及效率工具开发。通过合理的工具链配置和自动化脚本,可以显著提高组件库的开发效率和代码质量。
这些配置虽然在实际开发中通常由脚手架完成,但了解其原理和实现方式对于深入理解前端工程化具有重要意义。希望本文能为读者在前端工程化方面的学习提供有价值的参考。
更多推荐


所有评论(0)