前言
大家好,我是Fly哥, 之前写博客的仓库,还是用的原生的html 和js 也没有引入 ts , 和一些工程化的东西, 所以自己重新搭建了一套前端项目架构 基于 lerna + yarn 的 monrepo的仓库, 主要是后面会学习输出的一些东西, 整个架子先搭建起来。
- 2d 和 3d 公共 util 的封装
- 个人 npm 包的发布 (rollup)
- 2d react 项目 搭建(vite)
- 3d react 项目 搭建 (webpack)
- 搭建一套基于webpack 5 的cli
每个项目都有一些特定的依赖, 但是也会有一些相同的依赖。比如eslint、 babel 的一些基础配置,或者一些通用的脚本文件。读完本篇文章你可以学到 从0 到 1 搭建
monorepo 前端工程化项目 如下图所示:
项目架构图
为什么使用monorepo
monorepo 是一种将多个项目代码存储在一个仓库里的软件开发策略("mono" 来源于希腊语 μόνος 意味单个的,而 "repo",显而易见地,是 repository 的缩写)。将不同的项目的代码放在同一个代码仓库中,这种「把鸡蛋放在同一个篮子里」的做法可能乍看之下有些奇怪,但实际上,这种代码管理方式有很多好处,无论是世界一流的互联网企业 Google,Facebook,还是社区知名的开源项目团队 Babe、都使用了 monorepo 策略管理他们的代码。这是Taro 的官方源码库:
taro
至于他的优点如下:
- 代码重用将变得非常容易:由于所有的项目代码都集中于一个代码仓库,我们将很容易抽离出各个项目共用的业务组件或工具,并通过 TypeScript,Lerna 或其他工具进行代码内引用;
- 赖管理将变得非常简单, 可以轻松的做到版本依赖管理 和版本号自动升级
- 发布npm 包 也很特别简单, 提取公共方法 直接公共包,可以快速发布到npm 上
- 还有一个 最大的 优点 就是 避免重复安装包, 减少的磁盘空间, 降低了构建时间
这两项好处全部都可以由一个成熟的包管理工具来完成,对前端开发而言,即是 yarn(1.0 以上)或 npm(7.0 以上)通过名为 workspaces 的特性实现的(⚠️ 注意:支持 workspaces 特性的 npm 目前依旧不是 LTS 版本)。
yarn
这里的话 我们全局安装 yarn
npm install yarn -g
然后新建一个文件夹 进入到目录中执行
yarn init
会在项目的根目录生成 package.json
这时候我们在项目根目录
新建packages 目录
在package.json 新增下面字段 workspace
{ "name": "yarn-test", "version": "1.0.0", "private": true, "workspaces": [ "packages/*" ], "main": "index.js", "license": "MIT" }
表示工作区是packages 下的所有子目录,
private: true 表示项目的根目录 不会被发布出去
假设项目中有foo和bar两个package:
mono-demo/ |--package.json |--packages/ | |--foo/ | | |--package.json | |--bar/ | | |--package.json
yarn workspace <workspace_name>
在指定的package中运行指定的命令。
# 在foo中添加react,react-dom作为devDependencies yarn workspace foo add react react-dom --dev # 移除bar中的lodash依赖 yarn workspace bar remove lodash # 运行bar中package.json的 scripts.test 命令 yarn workspace bar run test
yarn workspaces run
在所有package中运行指定的命令,若某个package中没有对应的命令则会报错。
# 运行所有package(foo、bar)中package.json的 scripts.build 命令 yarn workspaces run build
yarn workspaces info [--json]
查看项目中的workspace依赖树。
例如我的bar依赖了foo,如下:
// bar/package.json { "name": "bar", "version": "1.0.0", "dependencies": { "foo": "^1.0.0" } }
在项目中的依赖结构是这样的(假设foo/package.json的版本匹配bar的依赖版本,否则会另外安装一个匹配的foo):
/package.json /yarn.lock /node_modules /node_modules/foo -> /packages/foo /packages/foo/package.json /packages/bar/package.json
那么运行yarn workspaces info
会得到如下输出:
yarn workspaces { "bar": { "location": "packages/bar", "workspaceDependencies": [ "foo" ], "mismatchedWorkspaceDependencies": [] }, "foo": { "location": "packages/foo", "workspaceDependencies": [], "mismatchedWorkspaceDependencies": [] } }
比如我的一些依赖是所有package 通用的 比如 eslint、babel... 我们就使用下面的这个命令 加一个 -W 就可以了
yarn <add|remove>-W
- -W: --ignore-workspace-root-check ,允许依赖被安装在workspace的根目录
管理根目录的依赖。
# 安装eslint作为根目录的devDependencies yarn add eslint -D -W
lerna
**Lerna**是社区主流的monorepo管理工具之一,集成了依赖管理、版本发布管理等功能。
使用Learn管理的项目的目录结构和yarn workspace类似。
我们根目录安装
yarn add lerna -D -W
然后执行
npx lerna init
然后项目中就会生成lerna.json
我们进行下面配置
{ "packages": ["packages/*"], "command": { "run": { "npmClient": "yarn" }, "publish": { "ignoreChanges": ["ignored-file", "*.md"], "message": "chore(release): publish", "registry": "https://npm.pkg.github.com" } }, "version": "independent", "useWorkspaces": true, "npmClient": "yarn" }
这里 同样使用 workspace, 指定项目 使用yarn 进行包管理
这里有一个很重要的字段 "version": "independent",
这是表示使用 独立模式 Lerna 项目允许维护人员彼此独立地增加包版本。每次发布时,您都会收到有关已更改的每个包的提示,以指定它是补丁、次要、主要还是自定义更改。独立模式允许您更具体地更新每个包的版本,并且对一组组件有意义。这里搭配 semantic-release 这个npm包 感兴趣的可以去了解下。
下面我介绍一些lerna 的一些命令: 大家可以去github lerna 看的更多
lerna bootstrap:等同于 lerna link + yarn install,用于创建符合链接并安装依赖包;
lerna run:会像执行一个 for 循环一样,在所有子项目中执行 npm script 脚本,并且,它会非常智能的识别依赖关系,并从根依赖开始执行命令;
lerna exec:像 lerna run 一样,会按照依赖顺序执行命令,不同的是,它可以执行任何命令,例如 shell 脚本;
lerna publish:发布代码有变动的 package,因此首先您需要在使用 Lerna 前使用 git commit 命令提交代码,好让 Lerna 有一个 baseline;
lerna add:将本地或远程的包作为依赖添加至当前的 monorepo 仓库中,该命令让 Lerna 可以识别并追踪包之间的依赖关系,因此非常重要
tsconfig
作为一个ts 项目, 在项目根目录安装 ts
yarn add typesript -D -W
首先在项目中生成 tsconfig.json
npx tsc --init
然后在项目根目录生成tsconfig.json 这里 划重点 我们把基础的 tsconfig.json 放在这里 ,然后 新建一个项目 生成tsconfig.json 都是继承根目录的 tsconfig.json 类似于这样
{ "extends": "../../tsconfig.json", "compilerOptions": { "target": "es2018", "module": "ESNext", "outDir": "./dist" }, "include": ["./src/**/*.ts"] // * 匹配0或者多个字符 (不包括目录分割符) **/递归匹配任意子目录 }
这是一个子项目的tsconfig.json
至于tsconfig.json 文件 详细配置,你可以自己百度。
Babel
Babel 配置文件合并的方式与 TypeScript 如出一辙,甚至更加简单,我们只需在子项目中的 .babelrc 文件中这样声明即可:
{ "extends": "../.babelrc" }
scripts
我们在全局新建一个scripts 文件夹 可能 是 shell 文件 也有可能是ts 文件。我们都知道 ts 文件 是不能直接执行, 都是先编译成js 然后再执行, 这也太麻烦了吧。好在社区已经提供的 ts-node 可以允许你直接运行ts 文件 这东西实现的原理 大概就是
我们可以使用 ts-node + 某个 ts 文件,来直接执行这个 ts 文件,它的原理就是修改了 require hook,也就是 Module._extensions['.ts']
来实现的。
在 require hook 里面做 ts 的编译,然后后面直接执行编译后的 js,这样就能达到直接执行 ts 文件的效果。
所以我们重写 Module._extensions['.ts']
方法,在里面读取文件内容,然后调用 ts.transpileModule 来把 ts 转成 js,之后调用 Module._compile 来处理编译后的 js。
yarn add ts-node -D -W
新建一个 test.ts 文件
const foo = { baz: { a: 1, }, } console.log(foo)
然后 在package.json
编写如下脚本:
"test": "ts-node ./scripts/test.ts "
然后执行 yarn test
res
其实这个ts-node 有一个坑 就是 文件引用问题 当你的 ts 脚本文件 引用了当前项目的其他包 可能就会出现 执行报错
我们在package 新建一个util 然后 新建了一个 index.ts 文件
const add = (a: number, b: number) => a + b export default add
然后我在根目录的 tsconfig.json 进行别名配置
"baseUrl": "./packages", // 根路径 路径映射, "paths": { "@fly/util": ["./util/index.ts"] }
我们在 脚本 引入 这个加法 函数
import add from '@fly/util' console.error(add(2, 3))
然后继续执行
会报下面这个错误
error
大家注意看 我画框的地方,大体就是 由于 我们 ts-node 在执行的过程中, 由于tsconfig.json 的 "module": "CommonJS", 会将
import add from '@fly/util' 编译成
const add = require("@fly/util")
由于我们这个是ts 别名配置 当然找不到 这个模块, 如果你是webpack 项目的话 ,可以 配置别名 解决, 会进行路径替换,
但是我们在写脚手架的时候,不可能用到webpack 就是 node 环境, 这里我怎么去解决呢
社区也提供了解决方案
ts-config
大概意思就是:使用它来加载位置在 tsconfig.json 的路径部分中指定的模块。支持在运行时加载和通过 API 加载。
Typescript 默认模仿模块的 Node.js 运行时解析策略。但它也允许使用路径映射,允许指定任意模块路径(不以“/”或“.”开头)并将其映射到文件系统中的物理路径。
typescript 编译器可以从 tsconfig 解析这些路径,因此它可以编译。但是,如果您尝试使用 node(或 ts-node)执行编译后的文件,它只会在 node_modules 文件夹中一直查找到文件系统的根目录,因此不会找到 tsconfig 中路径指定的模块 这句话 很重要, 所以我们刚才的报错,就是这个原因 而这个库 就是帮我们解决的。
yarn add tsconfig-paths -D -W
如何使用呢
ts-node --project customLocation/tsconfig.json -r tsconfig-paths/register "test/**/*.ts"
这里的话最好ts config.json 的 common js 因为我们是在node 环境
所以我在项目新建一个tsconfigs 用来 存放不同的 ts配置 同样继承 根目录
{ "extends": "../tsconfig", "compilerOptions": { "module": "CommonJS" } }
执行命令
ts-node --project ./tsconfigs/cmj.json -r tsconfig-paths/register ./scripts/test.ts
res
执行成功 很舒服哇。看到这里觉得有帮助的话 可以帮忙点个赞吧
但是这里还会有个问题 如图:
eslint
这其实是eslint import 的配置 ,如果你配置了的话 安装下面这个npm
yarn add eslint-import-resolver-typescript -D -W
光从名字就可以看出和这个问题极为相关。从项目 README 可以发现,这个 lib 可以在 TypeScript 项目使 eslint-plugin-import 找到正确的 .ts 和 .tsx 文件,也能识别 tsconfig.json 的 path 配置(路径别名 2),甚至 monorepo 这类一个 git 仓库多个项目的工程也支持。
用法也很简单在 eslint 的"import/resolver":
指向当前配置了 path 的 tsconfig 的路径即可,eslint 就会自动识别就不会报错了。
{ "plugins": ["import"], "rules": { "import/no-unresolved": "error" }, "settings": { "import/parsers": { // 使用 TypeScript parser "@typescript-eslint/parser": [".ts", ".tsx"] }, "import/resolver": { // 默认使用根目录 tsconfig.json "typescript": { // 从 <roo/>@types 读取类型定义 "alwaysTryTypes": true, }, // 使用指定路径 tsconfig.json, <root>/path/to/folder/tsconfig.json "typescript": { "directory": "./path/to/folder" }, // monorepos 这类多 tsconfig.json // 可以用 glob 这类匹配模式 "typescript": { "directory": "./packages/*/tsconfig.json" }, // 或者数组 "typescript": { "directory": [ "./packages/module-a/tsconfig.json", "./packages/module-b/tsconfig.json" ] }, // 也可以混合使用 "typescript": { "directory": [ "./packages/*/tsconfig.json", "./other-packages/*/tsconfig.json" ] } } } }
上面就是官方的用法, 下面我们就开始 eslint 详细用法吧