全网最详细的从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 shecho -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 ]]
thenecho -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) => xrequirePragma: 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的组件库,后面还有更多的文章信息,我也不知道后面文章什么时候更新,本人很赖!希望大家鼓励鼓励我!