全网最详细的从0到1的turbo pnpm monorepo的前端工程化项目[搭建篇]
- 引言
- 相关环境
- 技术栈
- 初始化工程
- 安装turbo
- 配置pnpm-workspace
- 安装husky
- 安装lint-staged
- 安装eslint
- 安装prettier
- 配置 .editorconfig
- 配置 .gitignore
- 初步项目结构
- 结语
引言
最近各种原因,生活上的,工作的上的都有,特意在业余时间做一点事情!
这是一个使用 pnpm monorepo 来搭建的demo级别的组件库
相关环境
- nodejs: node V20.11.0
- pnpm: pnpm V8.15.0
- win10: win10 WSL Ubuntu22
- vscode: V1.85.0
- git: V2.39.1
技术栈
- pnpm monorepo 来进行项目管理
选择 pnpm, 正如官网所描述的 “快、省” 优秀的包管理机制,和 workspace 功能,对 monorepo 有良好的支持。monorepo 是一种项目架构,简单的来说:一个仓库内包含多个开发项目(模块,包)。为了以后更好拆分内容,又能使用公共代码,,公共的工具方法也可以在组件之间共用,其实核心就是简化代码、更有效的管理代码,而且还可以进行单独发布,monorepo 就可以很好地来实现。市面上大量开源框架都在使用 pnpm monorepo,也说明的他的优秀。
- turbo 任务编排
选择turbo 主要是解决项目之间的依赖关系,编排构建顺序。比如我们有一个 app 依赖了我们的ui库,那么在都有更改的情况下,build 顺序就有了要求,通过 turbo 的话,一行命令就可以解决,并且可以提高构建效率。
不熟悉的turbo的,也可以看看Turborepo 其中提供了一个创建组件库的模板工程 。各种各样的模板工程
!
初始化工程
我这次并没有使用Turborepo模板来处理,因为模板下载来依赖弄完之后还要删除,不太适合我!
- 新建文件夹
robin-design
$ mkdir robin-design
- 初始化
package
$ cd robin-design
$ pnpm init
- 完善
package.json
根据自己的情况补全相应的信息,我这里是首次,只是补充一点信息,删减比较厉害,可以大概写下,后面慢慢补全!
- 新建文件夹
apps
,packages
$ mkdir apps
$ mkdir apps/admin
$ mkdir apps/docs
$ mkdir packages
$ mkdir packages/components
$ mkdir packages/tsconfig
$ mkdir packages/eslint-config-common
$ mkdir packages/eslint-config-vue
packages
放一些ui库、工具库等相关库;apps 放一些文档库以及一些应用,比如组件开发完成之后基于UI的一套的UI admin系统,包括我后期想做一体化的管理系统,类似于jekins,不知道有没有时间去做啦!也有可能是一个空话,说这些的原因,就是你的工程你做主,只要规范、规整、简洁一目了然就行!以上就是我初步的一些东西。其他的一些文件不在写啦,后面慢慢补充吧!
安装turbo
我这边没有想官网上,进行全局安装,而是直接安装到依赖上!这个是官网的列子
- turbo依赖
$pnpm install turbo -Dw
- 创建turbo.json
$echo '{ "$schema": "https://turbo.build/schema.json"}' > turbo.json
- 编写turbo.json
下面是官网的turbo.json说明,详细官网更直接pipeline
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
// A package's `build` script depends on that package's
// dependencies and devDependencies
// `build` tasks being completed first
// (the `^` symbol signifies `upstream`).
"dependsOn": ["^build"],
// note: output globs are relative to each package's `package.json`
// (and not the monorepo root)
"outputs": [".next/**", "!.next/cache/**"]
},
"deploy": {
// A package's `deploy` script depends on the `build`,
// `test`, and `lint` scripts of the same package
// being completed. It also has no filesystem outputs.
"dependsOn": ["build", "test", "lint"]
},
"test": {
// A package's `test` script depends on that package's
// own `build` script being completed first.
"dependsOn": ["build"],
// A package's `test` script should only be rerun when
// either a `.tsx` or `.ts` file has changed in `src` or `test` folders.
"inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts", "test/**/*.tsx"]
},
// A package's `lint` script has no dependencies and
// can be run whenever. It also has no filesystem outputs.
"lint": {},
"dev": {
"cache": false,
"persistent": true
}
}
}
下面是我项目里,后面继续完善
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": [
"^build"
],
"outputs": [
"dist/**"
]
},
"lint": {},
"dev": {
"cache": false,
"persistent": true
},
"test": {},
"test:watch": {
"cache": false
}
}
}
tips
:
- $schema:定义了 json 的类型,类似于 ts 对于 js 的约束
- dependsOn:表示当前命令所依赖的相关流程
- pipeline.build:表示当执行 turbo build 时会预先执行 ^build, ^build 就是所有项目的 package.json 里的那个 build 脚本,^ 是一个标记。如果像 lint 中的 build,他就指的是 pipeline 中的第一个 build 命令。
- outputs:指定缓存输出目录
- inputs: 配置的目录下文件有变化,才会重新执行此命令
配置pnpm-workspace
没啥可说的,看官网配置,在根目录下新建一个
pnpm-workspace.yaml
,配置如下
packages:
- "apps/*"
- "packages/*"
安装husky
husky做的事情就是在git工作流的某个时机触发脚本,也就是git hook,比如我们在git commit之前进行eslint语法检查,eslint检查过程中报错或者警告太多是会中断指令(git commit)执行,所以这样就保证了提交到远程的代码是通过eslint检查的。
$pnpm install husky -Dw
$git init
$pnpm exec husky init
$cd .husky
$echo '' > commit-msg
编写 commit-msg脚本,你也可以不用编写,安装 @commitlint/config-conventional @commitlint/cli插件,在commit-msg里写上npx --no – commitlint --edit ${1}也行,具体用法,自行摸索,我这里是自定义实现,符合基本格式验证就ok啦!脚本如下
#!/usr/bin/env sh
echo -e "\033[33m ------------------- 正在校验提交信息格式 -------------------- \033[0m"
# 获取当前提交的 commit msg
commit_msg=$(cat "$1")
# 获取用户 email
email=$(git config user.email)
msg_re="^(feat|fix|perf|refactor|merge|docs|style|test|build|revert|ci|chore|release|workflow)(\(.+\))?: .{1,100}"
if [[ ! $commit_msg =~ $msg_re ]]
then
echo -e "\033[35m不合法的 commit 消息提交格式,请使用正确的格式,请使用 feat|fix|perf|refactor|merge|docs|style 加冒号等开头格式\033[0m"
# 异常退出
exit 1
fi
husky 触发的相关命令后面在继续完善
tips
: 注意husky8
和husky9
生成还是有区别的, 请看这里
安装lint-staged
我们在实际使用中并不是针对全部的文件进行校验,因为也有可能是当前为完成不做检查和体检,所以我们只想对git add到暂存区(staged area)的文件进行lint检查,所以借助lint-staged来实现。
$pnpm add lint-staged -Dw
编写 lint-staged 指令, 在package.json中新增配置项
{
...
"lint-staged": {
"<glob-pattern>": "<command>"
}
}
其中 可以是原生的工具库命令,也可以是 Scripts 里的自定义命令。原生命令可以省略 pnpm run 前缀,但自定义命令必须是完整的,比如 eslint --fix 和 pnpm run lint:eslint。命令的类型可以是单个命令的字符串,也可以多个命令组成的数组。比如如下
{
...
"lint-staged": {
"packages/**/__tests__/*.ts": "npm run test",
"packages/**/*.ts": [
"pnpm run lint:eslint", // scripts 自定义的命令
"prettier --write" // prettier 原生的命令
]
}
}
安装eslint
eslint本质就是一个内置有解析器的工具,它可以将项目代码解析成AST,然后根据AST抽象语法树分析出代码里存在的问题然后给出警告或者报错。eslint的初衷就是在代码编写阶段尽早得发现存在的错误;除了语法检查外,eslint也具有一定的代码格式化能力,但是不是其能力的重心(prettier在代码格式方面更加专业)。eslint官网
这个eslint 用的相对多些,配置相对复杂一点,环境也相对复杂,针对这个配置,以及适应后面的变化,在
packages
文件下拆成多个项目进行处理。有eslint-config-common
、eslint-config-custom
、eslint-config-vue
eslint-config-common
公共 eslint rules 配置, 、安装 @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-plugin-import eslint-plugin-unicorn这些依赖,在这里不针对这些一一说明啦,不懂官网上去看吧!不说说的太多啦!
$pnpm add -Dw @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-plugin-import eslint-plugin-unicorn --filter=eslint-config-common
如下是我使用的配置
module.exports = {
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint", "import", "unicorn"],
rules: {
"arrow-body-style": "off", // 强制或禁止箭头函数体使用大括号
"prefer-arrow-callback": "off", // 要求使用箭头函数作为回调
semi: ["error", "always"], // 半风格-强制一致地使用反引号、双引号或单引号
quotes: ["error", "double"], // 引号-强制一致地使用反引号、双引号或单引号
eqeqeq: ["error", "always"], // 需要使用 === 和 !== (消除类型不安全的相等运算符)
"object-shorthand": ["error", "always"],
"no-sequences": [
"error",
{
allowInParentheses: false
}
],
"prefer-template": "error", // 字符串拼接使用字符串模板而不是+
curly: "error", // 确保将块语句包裹在花括号中来防止错误并提高代码清晰度
"padding-line-between-statements": [
"error",
{
blankLine: "always",
prev: ["function", "class", "const", "let", "var", "block-like"],
next: "*"
},
{
blankLine: "always",
prev: "*",
next: ["return", "block-like"]
},
{
blankLine: "any",
prev: ["const", "let", "var"],
next: ["const", "let", "var"]
}
],
"padded-blocks": ["error", "never"],
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
varsIgnorePattern: "^_",
argsIgnorePattern: "^_",
ignoreRestSiblings: true
}
],
"no-console": "error",
"no-restricted-imports": [
"error",
{
patterns: [
{
group: ["~/localization/*"],
message: "Don't import any internals from a module, only use its public api"
}
]
}
],
// 对比排序前后代码,排序后的代码看起来更整洁
"import/order": [
"error",
{
groups: ["builtin", "external", "internal", "parent", "sibling", "index"],
pathGroups: [
{
pattern: "~/**",
group: "internal",
},
],
alphabetize: {
order: "asc",
caseInsensitive: true,
},
"newlines-between": "never",
},
],
"import/named": "error", // 验证所有命名导入是否是引用模块中命名导出集的一部分
"import/no-duplicates": "error", // 导入/无重复
"import/no-useless-path-segments": [ // 防止在 import 和 require 语句中出现不必要的路径段
"error",
{
noUselessIndex: true,
},
],
"import/newline-after-import": "error", // 顶级导入语句或要求调用之后有一个或多个空行
"unicorn/no-for-loop": "error", // 不要使用for,可以用循环替换的for-of循环
"unicorn/consistent-function-scoping": "error", // 将函数定义移动到可能的最高范围
"unicorn/explicit-length-check": "error", // 强制显式比较值的length or size属性
"unicorn/no-array-instanceof": "error", // 需要Array.isArray()而不是instanceof Array
"unicorn/prefer-array-find": "error", // 优先使用.find(…)或.findLast(…)而不是.filter(…)
"unicorn/prefer-includes": "error", // 优先使用.includes()而不是.indexOf()
"unicorn/prefer-string-slice": "error", // 字符串优先使用String#slice() 而不是 String#substr() 和 String#substring()
"unicorn/consistent-destructuring": "error", // 在属性上使用解构变量
"unicorn/no-nested-ternary": "error", // 禁止嵌套三元表达式
"import/no-default-export": "error", // 禁用 export default 规则
"no-await-in-loop": "off",// 禁止在循环中出现 await
"@typescript-eslint/no-explicit-any": "error", // 不允许any类型
"no-empty-function": "off",// Note: you must disable the base rule as it can report incorrect errors
"@typescript-eslint/no-empty-function": "off"
}
}
eslint-config-custom
基础项目 eslint 配置,如apps,utils等, 安装 eslint-config-prettier、eslint-plugin-prettier 这些依赖,在这里不针对这些一一说明啦,不懂官网上去看吧!不说说的太多啦!,并集成eslint-config-custom
的配置
$pnpm add -Dw eslint-config-prettier eslint-plugin-prettier --filter=eslint-config-custom
如下是我使用的配置
module.exports = {
env: {
browser: true,
es2021: true,
node: true
},
extends: ["common", "prettier"],
overrides: [
{
files: ["*.config.{ts,js}"],
rules: {
"import/no-default-export": "off"
}
}
]
}
eslint-config-vue
vue项目 eslint 配置,校验vue,这里跟官网插件一个名字,不要混交,只是针对官网的eslint-config-vue
提取配置, 安装 @vue/eslint-config-prettier,eslint-config-vue 依赖,在这里不针对这些一一说明啦,不懂官网上去看吧!不说说的太多啦!,并集成eslint-config-custom
的配置
$pnpm add -Dw @vue/eslint-config-prettier --filter=eslint-config-custom
$pnpm add -w eslint-config-vue --filter=eslint-config-custom
如下是我使用的配置
module.exports = {
root: true,
env: { node: true },
globals: {
defineEmits: "readonly",
defineProps: "readonly"
},
extends: ["common", "plugin:vue/vue3-recommended", "@vue/prettier"],
overrides: [
{
files: ["*.config.{ts,js}"],
rules: {
"import/no-default-export": "off"
}
}
],
parser: "vue-eslint-parser",
parserOptions: {
parser: "@typescript-eslint/parser",
ecmaVersion: 2020
},
rules: {
// 在<template>中强制一致缩进
"vue/html-indent": ["error", 2],
// "vue/html-indent": ["error", "tab"], // enforce tabs in template
// indent: ["error", "tab"], // enforce tabs in script and js files
"vue/html-self-closing": "off",
// 要求单行元素的内容前后有一个换行符
"vue/singleline-html-element-content-newline": "off",
"vue/multi-word-component-names": [
"error",
{
ignores: ["index"] //需要忽略的组件名
},
],
// 执行自闭合的风格
"vue/max-attributes-per-line": [
"off",
{
singleline: 4,
multiline: 1
}
]
}
}
到这里项目用的eslint 基本编写的差不多啦!
补充说明
:eslint-config-common 忘记配置turbo 的插件啦,在这里安装下,
$pnpm add -Dw eslint-config-turbo --filter=eslint-config-common
// index.js 补充
module.exports = {
extends: ["turbo", "eslint:recommended", "plugin:@typescript-eslint/recommended"],
....
}
安装prettier
prettier 并没有提供太多的配置选项给我们选择,但是相对上面的eslint,更简单更温和一点,我们根据自己的喜好做一个适合自己的配置就行。prettier官网
$pnpm add prettier -Dw
prettier 一些常用的配置,如下配置中基本都是默认值
printWidth: 80, //单行长度
tabWidth: 2, //缩进长度
useTabs: false, //使用空格代替tab缩进
semi: true, //句末使用分号
singleQuote: true, //使用单引号
quoteProps: 'as-needed', //仅在必需时为对象的key添加引号
jsxSingleQuote: true, // jsx中使用单引号
trailingComma: 'all', //多行时尽可能打印尾随逗号
bracketSpacing: true, //在对象前后添加空格-eg: { foo: bar }
jsxBracketSameLine: true, //多属性html标签的‘>’折行放置
arrowParens: 'always', //单参数箭头函数参数周围使用圆括号-eg: (x) => x
requirePragma: false, //无需顶部注释即可格式化
insertPragma: false, //在已被preitter格式化的文件顶部加上标注
proseWrap: 'preserve', //不知道怎么翻译
htmlWhitespaceSensitivity: 'ignore', //对HTML全局空白不敏感
vueIndentScriptAndStyle: false, //不对vue中的script及style标签缩进
endOfLine: 'lf', //结束行形式
embeddedLanguageFormatting: 'auto', //对引用代码进行格式化
tips
: 这个是在线的格式配置信息,可以在上面配置好直接复制到项目中,点击这里
如下是我此次的初次配置,后面根据需要慢慢补充
{
"printWidth": 120,
"tabWidth": 2,
"useTabs": false,
"semi": false,
"singleQuote": true,
"quoteProps": "consistent",
"jsxSingleQuote": false,
"trailingComma": "none",
"bracketSpacing": true,
"jsxBracketSameLine": false,
"htmlWhitespaceSensitivity": "css",
"proseWrap": "preserve"
}
测试 prettier命令, 在package.json的 scripts 里加入 “format”: “turbo run prettier --write .”
....
"scripts": {
"format": "prettier --write ."
...
然后控制台运行 pnpm run format, 如下
测试一切正常,然后优化,加入的 lint-staged里, 去掉 scripts的format,最终 package.json如下
{
"name": "robin-design",
"private": true,
"version": "1.0.0",
"description": "robin-design",
"type": "module",
"scripts": {
"preinstall": "npx only-allow pnpm",
"prepare": "husky"
},
"workspaces": [
"apps/*",
"packages/*"
],
"keywords": [],
"author": "wanglb",
"license": "MIT",
"packageManager": "pnpm@8.15.0",
"engines": {
"pnpm": ">=8.0"
},
"lint-staged": {
"**/*.{js,jsx,ts,tsx,vue,json,css,less,md}": [
"prettier --write ."
]
},
"devDependencies": {
"eslint": "^8.56.0",
"eslint-config-common": "workspace:*",
"husky": "^9.0.10",
"lint-staged": "^15.2.1",
"prettier": "^3.2.4"
}
}
配置 .editorconfig
用来帮助开发者定义和维护代码风格的。但是它与 Prettier 不同的是,Prettier 是 JS 特有的格式化工具,里面有很多配置项是 JS 语言特有的规范,而 editorconfig 适应性更广泛,它可以跨编辑器(或 )维护统一的代码风格,专注于比较基础的格式化,比如 Tab 缩进、文件编码、末尾换行符等,这些规范与使用哪种编程语言无关。官网地址
root = true # 根目录的配置文件,编辑器会由当前目录向上查找,如果找到 `roor = true` 的文件,则不再查找
[*]
indent_style = space # 空格缩进,可选"space"、"tab"
indent_size = 2 # 缩进空格为4个
end_of_line = lf # 结尾换行符,可选"lf"、"cr"、"crlf"
charset = utf-8 # 文件编码是 utf-8
trim_trailing_whitespace = true # 不保留行末的空格
insert_final_newline = true # 文件末尾添加一个空行
curly_bracket_next_line = false # 大括号不另起一行
spaces_around_operators = true # 运算符两遍都有空格
indent_brace_style = 1tbs # 条件语句格式是 1tbs
[*.js] # 对所有的 js 文件生效
quote_type = single # 字符串使用单引号
[*.{html,less,css,json}] # 对所有 html, less, css, json 文件生效
quote_type = double # 字符串使用双引号
[package.json] # 对 package.json 生效
indent_size = 4 # 使用2个空格缩进
如下是我此次的初次配置,后面根据需要慢慢补充
# http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[package.json]
indent_size = 2
配置 .gitignore
没啥好说的,只要不想提交的,一股脑加进去
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
node_modules
.pnp
.pnp.js
# testing
coverage
# build
dist
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# turbo
.turbo
# orther
.vscode
.husky
初步项目结构
robin-design // husky 钩子配置目录
├─ .editorconfig // 编辑器配置
├─ .git // git初始换信息
├─ .gitignore // git 排除文件
├─ .husky // husky 钩子配置目录
│ ├─ commit-msg // 校验提交信息
│ ├─ pre-commit // 提交之前验证
│ └─ _
├─ .prettierrc // 格式化配置
├─ apps // 应用目录
├─ package.json
├─ packages // 系统通用包
│ ├─ eslint-config-common // 公共 eslint rules 配置
│ │ ├─ index.js
│ │ └─ package.json
│ ├─ eslint-config-custom // 基础项目 eslint 配置
│ │ ├─ index.js
│ │ └─ package.json
│ └─ eslint-config-vue // 基础 eslint vue 配置
│ ├─ index.js
│ └─ package.json
├─ pnpm-lock.yaml
├─ pnpm-workspace.yaml
├─ README.md
└─ turbo.json // turbo repo 配置文件
结语
本文从实战出发,初步单间一个为 turbo的基础项目工程项目,本文我估计,是全网唯一一个手把手第一次真正从0到一的完整的教程,纯属个人见解,因为我没有看到像我这么全的!,本文旨意在搭建一个vue3的组件库,后面还有更多的文章信息,我也不知道后面文章什么时候更新,本人很赖!希望大家鼓励鼓励我!