背景
最近一段时间参与一个小程序项目开发,分为 A、B 两个,开始搞的时候团队暂时没有小程序开发的基础,走一步看一步。
两个小程序有很多公用的组件、函数,刚开始搞就看谁先做,后做的小程序复制一份代码过去复用。从 0 到 1 的过程比较简单,当设计师介入走查修复问题比较麻烦,一个问题两个项目同时存在,其中一个修复复制到另一个,也不能确定另一个是否也做了更新。
此时我们需要把公共组件、函数比较优雅的方式复用起来。
方案一,公共组件、函数提取成 npm 包,这种形式最常见,更新、调试不是很方便,版本更新可能造成破坏性改动,无法和项目代码联合在一起 lint。
方案二,monorepo,及时调试,不用发版本,也可以和项目代码一起 lint,公共库需要本地打包一下。
两个方案多多少少都有点不顺畅,我们的目的是把代码复用起来,就像使用本项目目录下的代码一样,同时复用的代码也能单独 lint 检测代码。最后我们使用 monorepo 混合别名一起使用。
目录结构
bash
|-- apps
|-- -- app1
|-- -- app2
|-- packages
|-- -- components
|-- -- hooks
|-- -- utils
|-- package.json
|-- pnpm-workspace.yaml
|-- tsconfig.base.json
|-- tsconfig.json
从目录结构上看,把业务相关的代码都放到 apps
文件夹下,复用代码都放到 packages
文件夹下。
yaml
# pnpm-workspace.yaml
packages:
- 'packages/**'
- 'apps/**'
现在,每个文件夹都是一个 pnpm 的本地包,互相独立,可以满足各自 lint,甚至独立发版。
tsconfig.base.json
文件内容如下。
json
{
// 其他配置自行补充
"compilerOptions": {
"baseUrl": ".",
"rootDir": "./",
"paths": {
"~/*": ["packages/*"]
}
}
}
公共代码
在此之前或许你应该去学习一点 pnpm monorepo 相关的内容。
我们在每个 packages
的文件夹里创建 package.json
文件,添加一个 scripts
名为 lint:ts
,内容为 tsc --noEmit && eslint --ext .ts,.tsx
。
把需要的第三方库添加为 devDependencies
、peerDependencies
,例如 @tarojs/components
、@tarojs/taro
、@types/react
、react
,版本号使用 *
代替,不做任何限制。
我们在每个 packages
的文件夹里创建 tsconfig.json
文件。
json
{
"extends": "../../tsconfig.base",
}
现在编辑器、lint 在 packages
目录下可以识别 '~/utils'
这样的别名路径,满足像项目代码一样使用,不需要打包,如果以后想独立发版只需要把别名路径改为包名就好。
代码复用
在 apps
文件夹里初始化各个小程序,添加一个 scripts
名为 lint:ts
,内容为 tsc --noEmit && eslint --ext .ts,.tsx
。
tsconfig.json
里面补充 paths
,添加 ~/*
字段内容为 ["../../packages/*"]
,在 apps/app1/config/index.js
文件中找到 alias
字段添加构建工具自定义别名,添加 ~
字段内容为 path.resolve(__dirname, '../../../packages')
。项目代码就可以直接 import { xxx } from '~/utils'
,类似于 @/xxx
这样的别名,编辑器可以快速打开文件,构建工具也能找到文件。
本身项目使用 scss 管理样式文件,在 config.js 文件中的 sass
的 importer
字段添加别名配置。
js
const config = {
sass: {
importer: (url) => {
// ~ 开头的路径补全路径
const reg = /^~\/(.*)/;
return {
file: reg.test(url)
? path.resolve(__dirname, "../../../packages", url.match(reg)[1])
: url,
};
},
},
alias: {
"@": path.resolve(__dirname, "../src"),
"~": path.resolve(__dirname, "../../../packages"),
},
};
一个简单的示例
tsx
import type { ViewProps } from '@tarojs/components'
import { View, Text } from '@tarojs/components'
import { navigateTo } from '@tarojs/taro'
import { MansionOutline } from '@fruits-chain/icons-taro'
import { memo } from 'react'
import { gray7 } from '~/nut-extend/theme/color'
import { joinClassNames, getFirstImageUrl } from '~/utils'
import type { ExcludeUndefined } from '~/utils/types'
import Image from '@/components/image'
import Price from '@/components/price'
import type { CommodityOnlinePageQuery } from '@/graphql/operations/goods/__generated__/list.generated'
import './index.scss'
export type ListItem = ExcludeUndefined<
ExcludeUndefined<CommodityOnlinePageQuery['commodityOnlinePage']>['records']
>[0]
export interface CardGoodsProps extends ViewProps {
/**
* 图片与文案的方向
* @default 'vertical'
*/
direction?: 'vertical' | 'horizontal'
/**
* 横向布局图片大小
* @default 'm'
*/
coverSize?: 's' | 'm'
/**
* 图片圆角
* @default false
*/
coverRound?: boolean
data: ListItem
}
const CardGoods: React.FC<CardGoodsProps> = ({
direction,
coverSize = 'm',
coverRound = false,
data,
}) => {
return (
<View />
)
}
export default memo(CardGoods)
其他
在根目录 package.json
添加 scripts,字段名为 lint:all
内容为 pnpm -r lint:ts
,根目录运行 npm run lint:all
即可实现全代码 lint。
多个项目的 Taro 版本一定要统一,如果出现多个版本 Taro 可能出现控制台报错说某个模板不存在(不同版本的编译工具不通用)。在 package.json
添加 scripts,字段名为 clean
内容为 npx rimraf {packages,apps}/**/node_modules && npx rimraf ./node_modules
,根目录运行 npm run clean
,手动删除 pnpm-lock.yaml
,重新使用 pnpm 安装依赖。
Taro 版本检测范围包含 apps、packages 目录下所有有关 @tarojs
的包,packages 使用 *
作为版本,apps 里锁定某个版本,这样整个目录只会存在一个版本。