最新消息:看到那些跳动的图片、文字了吗?点击点击 O(∩_∩)O~~

Taro 使用 pnpm 的 monorepo 方案小总结

若思若想 onlyling 135浏览

背景

最近一段时间参与一个小程序项目开发,分为 A、B 两个,开始搞的时候团队暂时没有小程序开发的基础,走一步看一步。

两个小程序有很多公用的组件、函数,刚开始搞就看谁先做,后做的小程序复制一份代码过去复用。从 0 到 1 的过程比较简单,当设计师介入走查修复问题比较麻烦,一个问题两个项目同时存在,其中一个修复复制到另一个,也不能确定另一个是否也做了更新。

此时我们需要把公共组件、函数比较优雅的方式复用起来。

方案一,公共组件、函数提取成 npm 包,这种形式最常见,更新、调试不是很方便,版本更新可能造成破坏性改动,无法和项目代码联合在一起 lint。

方案二,monorepo,及时调试,不用发版本,也可以和项目代码一起 lint,公共库需要本地打包一下。

两个方案多多少少都有点不顺畅,我们的目的是把代码复用起来,就像使用本项目目录下的代码一样,同时复用的代码也能单独 lint 检测代码。最后我们使用 monorepo 混合别名一起使用。

目录结构

|-- apps
|-- -- app1
|-- -- app2
|-- packages
|-- -- components
|-- -- hooks
|-- -- utils
|-- package.json
|-- pnpm-workspace.yaml
|-- tsconfig.base.json
|-- tsconfig.json

从目录结构上看,把业务相关的代码都放到 apps 文件夹下,复用代码都放到 packages 文件夹下。

# pnpm-workspace.yaml
packages:
  - 'packages/**'
  - 'apps/**'

现在,每个文件夹都是一个 pnpm 的本地包,互相独立,可以满足各自 lint,甚至独立发版。

tsconfig.base.json 文件内容如下。

{
    // 其他配置自行补充
    "compilerOptions": {
        "baseUrl": ".",
        "rootDir": "./",
        "paths": {
          "~/*": ["packages/*"]
        }
    }
}

公共代码

在此之前或许你应该去学习一点 pnpm monorepo 相关的内容。

我们在每个 packages 的文件夹里创建 package.json 文件,添加一个 scripts 名为 lint:ts,内容为 tsc --noEmit && eslint --ext .ts,.tsx

把需要的第三方库添加为 devDependenciespeerDependencies,例如 @tarojs/components@tarojs/taro@types/reactreact,版本号使用 * 代替,不做任何限制。

我们在每个 packages 的文件夹里创建 tsconfig.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 文件中的 sassimporter 字段添加别名配置。

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"),
  },
};

一个简单的示例

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 里锁定某个版本,这样整个目录只会存在一个版本。

转载请注明:OnlyLing - Web 前端开发者 » Taro 使用 pnpm 的 monorepo 方案小总结