Monorepo项目技术选型及技术应用的细节

385 阅读21分钟

1、lerna

工程中的lerna.json配置

{
  "packages": [
    "packages/*"
  ],
  "version": "independent",
  "useWorkspaces": true,
  "npmClient": "pnpm",
  "command": {
    "run": {
      "stream": true
    },
    "bootstrap": {
      "hoist": true
    }
  }
}

Lerna 是什么?

  • Lerna 是 Babel 为实现 Monorepo 开发的工具;最擅长管理依赖关系和发布
  • Lerna 优化了多包工作流,解决了多包依赖发版手动维护版本等问题
  • Lerna 不提供构建、测试等任务,工程能力较弱,项目中往往需要基于它进行顶层能力的封装

Lerna 主要做三件事

  • 为单个包或多个包运行命令 (lerna run)
  • 管理依赖项 (lerna bootstrap)
  • 发布依赖包,处理版本管理,并生成变更日志 (lerna publish)

Lerna 能解决了什么问题?

  • 代码共享,调试便捷: 一个依赖包更新,其他依赖此包的包/项目无需安装最新版本,因为 Lerna 自动 Link
  • 安装依赖,减少冗余:多个包都使用相同版本的依赖包时,Lerna 优先将依赖包安装在根目录
  • 规范版本管理: Lerna 通过 Git 检测代码变动,自动发版、更新版本号;两种模式管理多个依赖包的版本号
  • 自动生成发版日志:使用插件,根据 Git Commit 记录,自动生成 ChangeLog

Lerna 自动检测发布,判断逻辑

  1. 校验本地是否有没有被 commit 内容?
  2. 判断当前的分支是否正常?
  3. 判断当前分支是否在 remote 存在?
  4. 判断当前分支是否在 lerna.json 允许的 allowBranch 设置之中?
  5. 判断当前分支提交是否落后于 remote

Lerna 工作模式

Lerna 允许您使用两种模式来管理您的项目:固定模式(Fixed)、独立模式(Independent)

① 固定模式(Locked mode):项目初始化时,lerna init 默认是 Locked mode

  • Lerna 把多个软件包当做一个整体工程,每次发布所有软件包版本号统一升级(版本一致),无论是否修改
{
  "version": "0.0.0"
}

② 独立模式(Independent mode):项目初始化时,lerna init --independent

  • Lerna 单独管理每个软件包的版本号,每次执行发布指令,Git 检查文件变动,只发版升级有调整的软件包
{
  "version": "independent"
}

Lerna 常用指令

lerna add -h 可以查看帮助文档

示例:
  lerna add module-1 packages/prefix-*        Adds the module-1 package to the packages in the 'prefix-' prefixed folders
  lerna add module-1 --scope=module-2         Install module-1 to module-2
  lerna add module-1 --scope=module-2 --dev   Install module-1 to module-2 in devDependencies
  lerna add module-1 --scope=module-2 --peer  Install module-1 to module-2 in peerDependencies
  lerna add module-1                          Install module-1 in all modules except module-1
  lerna add module-1 --no-bootstrap           Skip automatic `lerna bootstrap`
  lerna add babel-core                        Install babel-core in all modules

1.脚手架项目初始化

初始化npm项目→安装lerna→lerna init 初始化项目

www.npmjs.com/settings/me…

2.创建packege

lerna create 创建package→lerna add 安装依赖 → lerna link 链接依赖

① 初始化:init: lerna **init**

② 创建 package:create: lerna create <name> [location] lerna create package1

# 在 packages/pwd1 目录下,生成 package2 依赖包
lerna create package2 packages/pwd1

③ 给 package 添加依赖:add

安装的依赖,如果是本地包,Lerna 会自动 npm link 到本地包

//指定目录下安装依赖
lerna add @medicine-brand-operation-center/utils packages/landing_page_gundam

# 给所有包安装依赖,默认作为 dependencies
lerna add module-1
lerna add module-1 --dev	# 作为 devDependencies
lerna add module-1 --peer	# 作为 peerDependencies
lerna add module-1[@version] --exact  # 安装准确版本的依赖

lerna add module-1 --scope=module-2		# 给指定包安装依赖
lerna add module-1 packages/prefix-* 	# 给前缀为 xxx 的包,安装依赖

④ 给所有 package 安装依赖:bootstrap

执行 lerna bootstrap 指令:会自动为每个依赖包进行 npm installnpm link 操作

# 项目根目录下执行,将安装所有依赖
lerna bootstrap

关于冗余依赖的安装

  • npm 场景下 lerna bootstrap 会安装冗余依赖(多个 package 共用依赖,每个目录都会安装)
  • yarn 会自动 hosit 依赖包(相同版本的依赖,安装在根目录),无需关心

npm 场景下冗余依赖解决方案:

  • 方案一: lerna bootstrap --hoist
  • 方案二:配置 lerna.json/command.bootsrap.hoist = true

3.脚手架开发和测试

  • lerna exec 执行shell脚本

    • lerna exec -- rm -rf node_modules/
      
      # 指定某个包中的
      lerna exec --scope @medicine-brand-operation-center/landing_page_gundam -- rm -rf node_modules/
      
      # 删除所有包内的 lib 目录
      lerna exec -- rm -rf lib
      
      # 给xxx软件包,删除依赖
      lerna exec --scope=xxx -- yarn remove yyy
      
  • lerna run 执行npm scripts命令

    • lerna run 
      
      # 所有依赖执行 package.json 文件 scripts 中的指令 xxx
      lerna run xxx
      
      # 指定依赖执行 package.json 文件 scripts 中的指令 xxx
      lerna run --scope=my-component xxx
      
      
  • lerna clean 清空依赖

    • 注意:只是删除了包,但是package.json中的依赖并没有删除:lerna WARN No packages found where @medicine-brand-operation-center/utils can be added. 所以需要手动删除

    •     ➜  medicine_brand_operation_center_monorepo git:(master) ✗ lerna add @medicine-brand-operation-center/utils packages/landing_page_gundam
          info cli using local version of lerna
          lerna notice cli v4.0.0
          lerna info versioning independent
          lerna WARN No packages found where @medicine-brand-operation-center/utils can be added. 
      
  • lerna bootstrap 重装依赖

4.脚手架发布上线

  • lerna version bump version 提升版本号

  • lerna changed 查看上版本以来的所有变更

  • lerna diff 查看diff

  • lerna publish 项目发布: 发布软件包,自动检测

    • 运行lerna updated来决定哪一个包需要被publish

      如果有必要,将会更新lerna.json中的version

      将所有更新过的的包中的package.json的version字段更新

      将所有更新过的包中的依赖更新

      为新版本创建一个git commit或tag

      将包publish到npm上

    • git status
      
      //注意版本注意私有版本需要登录 npm login
      lerna publish
      
      

对于Lerna,你可以运行lerna list命令来查看所有的包。这个命令应该会列出你项目中的所有包。

对于Yarn Workspaces,你可以运行yarn workspaces info命令来查看所有的工作区。这个命令会列出你的所有工作区,以及它们的依赖关系。

查看自上次发布的变更:diff、changed

# 查看自上次relase tag以来有修改的包的差异
lerna diff

# 查看自上次relase tag以来有修改的包名
lerna changed

导入已有包:import

lerna import [npm 包所在本地路径]

列出所有包:list

lerna list

lerna.json 配置

{
  "packages": [
    "packages/*"
  ],
  "version": "independent",
  "useWorkspaces": true,
  "npmClient": "pnpm",
  "command": {
    "run": {
      "stream": true
    },
    "bootstrap": {
      "hoist": true
    }
  }
}

这段配置是 lerna.json 文件的内容,它定义了 Lerna 工具在管理 monorepo 时的行为。下面是每个属性的含义:

  • "packages": 这是一个数组,指定了 Lerna 应该管理哪些包。在这个例子中,"packages/*" 意味着 Lerna 将会管理 packages 目录下的所有子目录中的包。
  • "version": 设置为 "independent" 指示 Lerna 使用独立版本控制模式。在这种模式下,每个包都有自己的版本号,可以独立于其他包进行版本更新。
  • "useWorkspaces": 设置为 true 表示 Lerna 将会使用 Yarn 工作区(workspaces)或者类似的 pnpm/npm 工作区功能来管理包的依赖和链接。这允许 Lerna 利用包管理器的工作区功能来优化包之间的依赖安装和链接。
  • "npmClient": 指定 Lerna 应该使用哪个包管理器来执行命令。在这个配置中,它被设置为 "pnpm",这意味着 Lerna 将使用 pnpm 来安装依赖和运行脚本。
  • "command": 这是一个对象,用于配置 Lerna 执行不同命令时的行为。它包含以下子属性:
    • "run":
      • "stream": 设置为 true 表示 Lerna 在运行 lerna run 命令时会实时地将子包的输出流式传输到控制台。这有助于在执行脚本时跟踪每个包的输出。
    • "bootstrap":
      • "hoist": 设置为 true 表示 Lerna 将尝试提升(hoist)子包的依赖到 monorepo 的根目录中。这可以减少安装的依赖数量,节省空间并加快安装速度。Lerna 会将共享的依赖提升到根目录的 node_modules 文件夹中,并确保子包能够访问到这些依赖。

总的来说,这段配置告诉 Lerna 在管理 monorepo 时使用 pnpm 作为包管理器,并开启工作区支持。每个包可以独立地进行版本控制,且 Lerna 会尝试提升依赖以优化依赖管理。当运行命令时,Lerna 会将输出实时传输到控制台。

2、pnpm

为什么现在我更推荐 pnpm 而不是 npm/yarn?:juejin.cn/post/693204…

image-20250222221246139

项目中的.npmrc配置

# 所有的依赖项都应该被提升到工作空间的根目录
# public-hoist-pattern=*

registry=http://r.npm.sankuai.com

disturl=http://npm.sankuai.com/dist/node
sass_binary_site=http://npm.sankuai.com/dist/node-sass
phantomjs_cdnurl=https://npm.taobao.org/mirrors/phantomjs/
profiler_binary_host_mirror=https://npm.taobao.org/mirrors/node-inspector/
fse_binary_host_mirror=https://npm.taobao.org/mirrors/fsevents/

# 工作区间包的自动链接功能:true=启用;false=禁用
link-workspace-packages=false

pnpm-workspace.yaml

packages:
  # all packages in direct subdirs of packages/
  - 'packages/*'
  - 'public/*'
  - 'shared-lib'
  - 'ui-components'
  # all packages in subdirs of components/
  # - 'components/**'
  # exclude packages that are inside test directories
  # - '!**/test/**'
# 如果你想在工作区根目录安装依赖,并且不想看到警告
# settings:
#   ignore-workspace-root-check: true

安装依赖

pnpm add -D @esbuild-plugins/node-globals-polyfill @esbuild-plugins/node-modules-polyfill

如果是安装在workspace root 则需要加上 -w

3、prettier&&eslint配置等

.prettierignore

*.md

.prettierrc.js

module.exports = {
  // 使用箭头函数时,避免不必要的括号,以提高代码可读性
  arrowParens: 'avoid',
  // 不允许将对象的结束括号与其中的最后一个属性位于同一行,以保持一致性
  bracketSameLine: false,
  // 根据运行环境自动选择换行符,使得代码在不同系统中保持一致性
  endOfLine: 'auto',
  // 设置打印宽度为120字符,以确保代码行不超出此长度,提高可读性
  printWidth: 120,
  // 对象或数组内的属性或元素的键值对引号使用一致,优化代码美观度
  quoteProps: 'consistent',
  // 强制每个属性单独一行,增强代码的可读性和格式化一致性
  singleAttributePerLine: true,
  // 使用单引号替代双引号,统一代码风格
  singleQuote: true,
  // 设置每个制表符的宽度为2个空格,提高代码缩进的一致性
  tabWidth: 2,
  // 在需要的地方添加尾逗号,遵循es5语法,提高代码的可读性和后期维护性
  trailingComma: 'es5',
  // 禁止使用制表符进行缩进,统一使用空格,以确保代码在所有环境中正确对齐
  useTabs: false,
  // 定义导入语句的顺序规则,通过正则表达式匹配来组织导入顺序,可以根据自己项目的实际情况定制
  importOrder: ['^react', '^[a-z].*$', '^@[^/].*$', '^@/.*$', '^(?!.*.(css|less|scss)$)', '.*(css|less|scss)$'],
  // 禁止在导入语句之间添加额外的空行,保持代码的紧凑性
  importOrderSeparation: false,
  // 导入语句中的路径按照字母顺序进行排序,增强代码的可读性
  importOrderSortSpecifiers: true,
  // 引入第三方插件以实现导入语句的自动排序功能
  plugins: ['@trivago/prettier-plugin-sort-imports'],
};

commitlint.config.js

module.exports = {
  extends: ['@commitlint/config-conventional']
};

.editorconfig

root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.js]
indent_style = space
indent_size = 2

[*.ts, *.tsx]
indent_style = space
indent_size = 2
semicolon = always
quote_type = double
jsx_single_quote = false
jsx_bracket_same_line = false

[*.json]
indent_style = space
indent_size = 2

[*.md, *.markdown]
trim_trailing_whitespace = false

[*.yaml, *.yml]
indent_style = space
indent_size = 2

[*.html]
indent_style = space
indent_size = 2

[*.css, *.scss, *.less]
indent_style = space
indent_size = 2

[*.vue]
indent_style = space
indent_size = 2

.eslintrc.js

module.exports = {
  env: {
    browser: true,
    amd: true,
    node: true,
  },
  extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'],
  parser: '@typescript-eslint/parser',
  plugins: ['@typescript-eslint'],
  rules: {
    // 根据自己项目的实际情况加入一些自定义的 rules 的配置
    '@typescript-eslint/no-var-requires': 'off',
  },
  root: true,
  ignorePatterns: ['!.*', 'node_modules/', 'dist/', '*.min.js', '**/*.mock.ts'],
};

generate-readme.js

const fs = require('fs');
const path = require('path');

const srcPath = path.join(__dirname, 'packages');
// const srcPath = __dirname; // 设置为根目录
const readmePath = path.join(__dirname, 'README.md');

function getDirectoryTreeMarkdown(dirPath, level = 0) {
    let treeMarkdown = '';
    const indent = ' '.repeat(level * 2); // Markdown缩进使用空格
    const filesAndDirs = fs.readdirSync(dirPath);

    filesAndDirs.forEach((name) => {
         // 忽略 node_modules 目录
        if (name === 'node_modules') {
            return;
        }
        const filePath = path.join(dirPath, name);
        const stats = fs.statSync(filePath);
        const relativePath = path.relative(__dirname, filePath);

        if (stats.isDirectory()) {
            treeMarkdown += `${indent}- ${name}/\n`;
            // 递归获取子目录Markdown
            treeMarkdown += getDirectoryTreeMarkdown(filePath, level + 1);
        } else {
            treeMarkdown += `${indent}- [${name}]\n`;
            // treeMarkdown += `${indent}- [${name}](${relativePath})\n`;
        }
    });

    return treeMarkdown;
}

const directoryTreeMarkdown = getDirectoryTreeMarkdown(srcPath);

// 读取现有的README.md内容
const readmeContent = fs.readFileSync(readmePath, 'utf8');
// 正则表达式匹配README.md中的src目录结构部分
const readmeSrcSectionRegex = /<!-- src-directory-start -->([\s\S]*?)<!-- src-directory-end -->/;

const updatedReadmeContent = readmeContent.replace(readmeSrcSectionRegex, `<!-- src-directory-start -->\n${directoryTreeMarkdown}<!-- src-directory-end -->`);

// 写回更新后的README.md内容
fs.writeFileSync(readmePath, updatedReadmeContent, 'utf8');

console.log('README.md has been updated with src directory structure.');

4、lerna+workspace+pnpm

yarn workspace 更突出对依赖的管理: 依赖提升到根目录的 node_modules 下,安装更快,体积更小

Lerna 更突出工作流方面:使用 Lerna 命令来优化多个包的管理,如:依赖发包、版本管理,批量执行脚本

pnpm 是新一代 Node 包管理器,它由 npm/yarn 衍生而来,解决了 npm/yarn 内部潜在的风险,并且极大提升依赖安装速度。pnpm 内部使用基于内容寻址的文件系统,来管理磁盘上依赖,减少依赖安装;node_modules/.pnmp虚拟存储目录,该目录通过<package-name>@<version>来实现相同模块不同版本之间隔离和复用,由于它只会根据项目中的依赖生成,并不存在提升。

CAS 内容寻址存储,是一种存储信息的方式,根据内容而不是位置进行检索信息的存储方式。

Virtual store 虚拟存储,指向存储的链接的目录,所有直接和间接依赖项都链接到此目录中,项目当中的.pnpm目录

pnpm 相比于 npm、yarn 的包管理器,优势如下,同理是 Lerna + yarn + workspace 优势:

  • 装包速度极快: 缓存中有的依赖,直接硬链接到项目的 node_module 中;减少了 copy 的大量 IO 操作
  • 磁盘利用率极高: 软/硬链接方式,同一版本的依赖共用一个磁盘空间;不同版本依赖,只额外存储 diff 内容
  • 解决了幽灵依赖: node_modules 目录结构 与 package.json 依赖列表一致

常用命令梳理

  1. 确保你已经安装了 Lerna,pnpm 和 Yarn。如果没有,你可以使用 npm 安装它们:

npm install -g lerna pnpm yarn
  1. 然后,你可以创建一个新的 Lerna 项目

lerna init

这将在当前目录下创建一个新的 Lerna 项目,包括一个 packages 目录和一个 lerna.json 文件。

  1. 然后,你可以使用 pnpm 作为 Lerna 的 npmClient。在 lerna.json 文件中添加以下配置:

{
  "npmClient": "pnpm",
  "useWorkspaces": true,
  "version": "independent"
}
  1. 接下来,你需要在项目的 package.json 文件中启用 Yarn Workspaces。添加以下配置:
{
  "name": "root",
  "private": true,
  "workspaces": [
    "packages/*"
  ]
}

5.现在,你可以在 packages 目录下创建新的包。例如,你可以使用以下命令创建一个新的 npm 包:

cd packages
pnpm init -y

这将创建一个新的 package.json 文件。你可以在这个文件中添加你的包的信息和依赖。

当你添加或更新包的依赖时,你可以运行 lerna bootstrap 命令。这将在所有包中安装依赖,并链接跨包的依赖。

最后,你可以使用 lerna publish 命令来发布你的包。这将更新包的版本号,创建一个新的 git 标签,并将包推送到 npm。

6.pnpm安装workspace依赖,命令:

pnpm add lodash --filter @medicine-brand-operation-center/landing_page_gundam add lodash
pnpm add lodash --filter @medicine-brand-operation-center/landing_page_gundam add lodash
yarn workspace @medicine-brand-operation-center/landing_page_gundam add lodash

这个命令会在 "landing_page_gundam" 工作空间中添加 "lodash" 依赖。

你也可以使用以下的命令来查看你的所有工作空间:

pnpm list --filter . --depth -1

这个命令会列出你的所有工作空间及其直接依赖。

清除pnpm的缓存:这个命令会清除所有未被项目依赖的包的缓存。

pnpm store prune

查看pnpm的缓存位置:这个命令会显示pnpm的缓存路径。

pnpm store path

删除 node_modules 文件夹和 yarn.lock 文件

rm -rf node_modules yarn.lock

pnpm 来重新安装你的依赖

pnpm install

7.创建react子工程

首先,你需要在你的工作空间下创建一个新的文件夹来存放你的React项目。你可以使用以下命令来创建:

mkdir my-react-app && cd my-react-app

然后,你可以使用以下命令来初始化一个新的React项目:

pnpm create vite . --template react

pnpm install

在初始化过程中,选择 "react" 作为你的框架,然后选择 "JavaScript" 或 "TypeScript" 作为你的语言。

接下来,你需要在你的工作空间配置文件(通常是 "lerna.json" 或 "package.json")中添加你的新项目。例如,如果你的工作空间配置在 "package.json" 中,你需要添加以下内容:

{
  "workspaces": [
    "my-react-app"
    // 其他工作空间
  ]
}

然后,你可以在你的新项目中安装所有的依赖:

pnpm install

现在,你的React项目已经被创建并配置好了。你可以在你的 "my-react-app" 文件夹中找到你的项目,并使用以下命令来启动你的项目:

pnpm run dev

这会启动一个开发服务器,你可以在浏览器中通过 "http://localhost:5000" 来访问你的项目。

8.查看workspace中有哪些包

yarn workspaces info

pnpm list -r --filter ./packages

5、vite配置相关

vite-build/vite.config.ts

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { createHtmlPlugin } from 'vite-plugin-html';
// @ts-ignore
import { createHtmlConfigFun } from './html.config'; // 所有子项目通用 HTML 配置模块
const { PUBLIC_URL = '/', TALOS_SUBAPP_FLAG, AWP_DEPLOY_ENV, SOURCEMAP_PUBLIC_URL } = process.env;
interface ProxyTargets {
  [key: string]: string;
}
// @ts-ignore
// 判断是否为生产模式
const bocProdMode = ['production', 'staging'].includes(AWP_DEPLOY_ENV);
console.log('bocProdMode', typeof bocProdMode, bocProdMode);

// sourcemap URL 处理
function getSourcemapBaseUrl() {
  if (SOURCEMAP_PUBLIC_URL && !SOURCEMAP_PUBLIC_URL.startsWith('http')) {
    return new URL(`${SOURCEMAP_PUBLIC_URL}assets/`, 'https://awp-assets.sankuai.com').href;
  }
  return undefined;
}
// 获取 npm 脚本生命周期事件
const npmLifecycleEvent: string | undefined = process.env.npm_lifecycle_event;
// 定义代理目标映射
const proxyTargets:ProxyTargets = {
  'dev:sn': 'http://XXXXXXXXX.com',
  'dev:snst': 'http://CCCCC.com',
  'dev:st': 'http://cccccccccc.com',
  'dev': 'http://yyyyyyyyyy.com',
  'dev:mock': 'http://yapi.CCCC.com',
};

// 获取代理目标,如果没有匹配的,则使用默认值
const defaultProxyTarget: string = 'http://XXXXXXXXXXXX.com';
const proxyTarget: string = (npmLifecycleEvent && proxyTargets[npmLifecycleEvent]) || defaultProxyTarget;

console.log('Proxy Target:', proxyTarget);
// Vite 配置
// @ts-ignore
export default defineConfig((subprojectSpecificHtmlConfig = {}) => {
  // 模版参数
  const htmlConfig = createHtmlConfigFun({
    // @ts-ignore
    title: subprojectSpecificHtmlConfig?.title || 'XXXXXXX1',
    // @ts-ignore
    description: subprojectSpecificHtmlConfig?.description || 'XXXXXXX',
    AWP_DEPLOY_ENV,
    bocProdMode,
    // @ts-ignore
    subprojectSpecificTags: subprojectSpecificHtmlConfig?.subprojectSpecificTags || [] // 将子项目特定标签作为参数传递
  });
  return {
    base: PUBLIC_URL,
    build: {
      minify: bocProdMode ? 'terser' : false, // 使用terser进行代码压缩
      terserOptions: bocProdMode
        ? {
            compress: {
              drop_console: true,
              drop_debugger: true
            }
          }
        : {},
      outDir: TALOS_SUBAPP_FLAG ? `build/${TALOS_SUBAPP_FLAG}` : 'build',
      sourcemap: true, // 不区分环境
      // sourcemap: !bocProdMode, //优化:在生产环境中,不生成SourceMap,减少构建体积和提高加载速度。 production-false
      rollupOptions: {
        output: {
          sourcemapBaseUrl: getSourcemapBaseUrl()
        }
      }
    },
    server: {
      port: 2024,
      open: true,
      proxy: {
        '^/(health|open|api)': {
          target: proxyTarget,
          changeOrigin: true
        }
      }
    },
    css: {
      preprocessorOptions: {
        less: {
          javascriptEnabled: true
        },
        scss: {}
      }
    },
    define: {
      AWP_DEPLOY_ENV: JSON.stringify(AWP_DEPLOY_ENV),
      BOC_PROD_MODE: bocProdMode,
      VITE_ROUTE_BASE: JSON.stringify(AWP_DEPLOY_ENV ? `/monorepo/${TALOS_SUBAPP_FLAG}` : undefined)
    },
    plugins: [
      react({
        babel: {
          presets: [
            [
              '@babel/preset-env',
              {
                modules: false, // 确保输出格式为 ES 模块
                useBuiltIns: 'usage', // 自动按需引入 Polyfill
                corejs: { version: 3, proposals: true }, // 使用 core-js 版本 3
                targets: '> 0.25%, not dead' // 明确支持的浏览器范围
              }
            ]
          ]
        }
      }),
      createHtmlPlugin(htmlConfig)
    ]
  };
});

vite-build/html.config.js

// 导出模版配置函数
export function createHtmlConfigFun({ title, description, AWP_DEPLOY_ENV, bocProdMode, subprojectSpecificTags }) {
  const timestamp = Date.now();
  // yoda唤起测试用,环境参数有 dev|test|staging|pro
  const envMapping = {
    production: 'pro',
    staging: 'staging',
    newtest: 'test',
    test01: 'test',
  };
  const yodaEnv = envMapping[AWP_DEPLOY_ENV] || 'test';
  // 是否是线上环境
  const isPro = yodaEnv === 'pro';
  // 根据环境选择合适的白屏检测脚本
  const wsBundleScript = `https://xxxxx${AWP_DEPLOY_ENV === 'production' ? '' : '-dev'}/VVVVV.js`;
  // 公共配置,所有项目都需要的配置
  const commonTags = [
    // 注入 meta 标签
    {
      tag: 'meta',
      attrs: { name: 'viewport', content: 'width=device-width, initial-scale=1, shrink-to-fit=no' },
      injectTo: 'head',
    },
    {
      tag: 'meta',
      attrs: { 'http-equiv': 'Cache-Control', 'content': 'no-store,no-cache,must-revalidate' },
      injectTo: 'head',
    },
    {
      tag: 'meta',
      attrs: { 'http-equiv': 'Pragma', 'content': 'no-cache' },
      injectTo: 'head',
    },

    // 注入外部脚本
    {
      tag: 'script',
      attrs: {
        src: 'https://vvvvvvvvvvvvvvvvvvv.js', 
        defer: true, // 增加defer属性,在文档解析完成后才会执行,从而不会阻塞DOM的解析
      },
      injectTo: 'head',
    },
    {
      tag: 'script',
      injectTo: 'body',
      position: 'after', 
      attrs: {
        src: wsBundleScript,
      },
    },
  ];

  return {
    minify: true,
    inject: {
      data: {
        title,
        description,
      },
      tags: [...commonTags, ...subprojectSpecificTags], // 合并公共配置和特定子项目配置
    },
  };
}

1.alias配置

在 Vite 中,__dirname 是不可用的,因为 Vite 是基于 ES Modules 的,而 __dirname 是一个 Node.js 的全局变量,只在 CommonJS 模块中可用。

不过不用担心,你可以使用 import.meta.url 来代替 __dirname。以下是一个例子:

// vite.config.js
import { defineConfig } from 'vite'
import { resolve } from 'path'
import { fileURLToPath } from 'url'

const __dirname = fileURLToPath(new URL('.', import.meta.url))

export default defineConfig({
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src')
    }
  }
})


resolve: {
        alias: {
            '@assets': `${__dirname}/src/assets`,
            '@components': `${__dirname}/src/components`,
            '@utils': `${__dirname}/src/utils`,
            '@api': `${__dirname}/src/api`,
            '@': `${__dirname}/src`
        }
    },

这样,你就可以在 Vite 中使用 @ 来代替 src 目录了。

2.build

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
const timestamp = new Date().getTime();
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  build: {
    lib: {
      entry: './src/index.tsx',
      name: 'MyReactPackage',
      formats: ['es', 'cjs'],
      fileName: (format) => `landing_page_gundam.${timestamp}.${format}.js`,
    },
    rollupOptions: {
      external: ['react', 'react-dom'],
    },
  },
})

3.proxy配置

server: {
        port: 2023, // 将此处的2023改为你要设置的新端口
        open: true,
        proxy: {
             '^/health': {
                target: 'http://',
                changeOrigin: true,
            },
            '^/shoppingguide': {
                target: 'http://.com',
                changeOrigin: true,
            },
            '^/open': {
                target: 'http://',
                changeOrigin: true,
            }
        },
    },

4.plugins

// 解决构建打包时候报错:(node:822) [DEP0005] DeprecationWarning: Buffer() is deprecated due to security and usability issues. Please use the Buffer.alloc(), Buffer.allocUnsafe(), or Buffer.from() methods instead.
import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill';
import { NodeModulesPolyfillPlugin } from '@esbuild-plugins/node-modules-polyfill';

plugins: [
    react(),
    NodeGlobalsPolyfillPlugin({
        buffer: true
    }),
    NodeModulesPolyfillPlugin()
 ],


6、git相关

git相关的配置.gitattributes

text
* text=auto eol=lf
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.bmp binary
*.ico binary
*.tif binary
*.tiff binary

.gitignore

# dependencies
**/node_modules

# production
**/dist
**/build
# misc
.DS_Store
npm-debug.log*
yarn-error.log

# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
package-lock.json
packages/**/package-lock.json
*bak

# visual studio code
.history
*.log
**/rollup-visualizer.html

本地的一个已有项目与 Git 进行关联

  1. 首先,打开命令行,切换到你的项目目录下:
cd your_project_path
  1. 初始化 Git:
git init

这将在你的项目目录下创建一个新的 .git 子目录,所有 Git 需要的数据和资源都存储在这个目录中。

  1. 将所有文件添加到 Git:
git add .

这将把你的所有文件(除了 .gitignore 中指定的文件)添加到 Git 的暂存区。

  1. 提交你的文件:
git commit -m "Initial commit"

这将把暂存区的所有文件提交到 Git 仓库,你的文件现在已经被 Git 跟踪了。

  1. 关联远程仓库:
git remote add origin your_remote_repository_url

这将添加一个名为 "origin" 的新远程,URL 是你的远程仓库的 URL。

  1. 将你的代码推送到远程仓库:
git push -u origin master

这将把你的代码推送到远程仓库的 "master" 分支。如果你的远程仓库使用的是 "main" 作为默认分支,你应该使用 git push -u origin main

完成以上步骤后,你的本地项目就已经与 Git 仓库关联起来了。

.gitattributes

在版本控制系统(如Git)中,跨平台的行结束符问题是一个常见的困扰。这是因为不同的操作系统使用不同的行结束符:

  • Windows系统使用回车(CR)和换行(LF)两个字符序列 \r\n 作为行结束符。
  • Unix/Linux系统和Mac OS(自从OS X开始)使用单个换行符 \n 作为行结束符。

当开发者在不同平台上协作时,可能会遇到因为行结束符不一致而导致的奇怪问题,比如额外的空白行或者在文本编辑器中显示乱码。

在根目录下创建.gitattributes文件并添加以下内容:

text
* text=auto eol=lf

这个配置有以下含义:

  • * text=auto: 这条规则告诉Git,对待所有文件(* 表示所有文件)应该自动检测其是否为文本文件。如果是文本文件,Git将进一步应用下面的eol设置。
  • eol=lf: 这条规定了所有文本文件在库中的行结束符应统一为 LF(换行符\n)。

通过这样的配置,你可以确保以下几点:

  1. Git会自动识别哪些文件是文本文件,哪些是二进制文件。
  2. 对于识别为文本文件的,Git会在检入(check-in)时将所有行结束符转换为LF。
  3. 在检出(check-out)时,Git会将LF转换为操作系统的默认行结束符,因此在Windows上,你仍然会看到正确的行结束符(CR+LF)。

.gitignore

text
node_modules/
dist/
*.log

7、TS相关

tsconfig.json配置

{
  "compilerOptions": {
    "downlevelIteration": true, // 当目标是 es5 或 es3 时,提供对迭代器的全面支持,包括对 for-of 循环和扩展操作符的支持。
    "target": "esnext", // 设置编译后的代码目标为最新的 ECMAScript 版本
    "module": "esnext", // 使用 ESNext 作为模块标准
    "moduleResolution": "node", // 使用 Node.js 风格的模块解析
    "lib": ["dom", "dom.iterable", "esnext", "es2018"], // 包含的库文件,这里包括了 DOM 和最新的 ECMAScript 功能
    "allowJs": true,
    "jsx": "react-jsx", // 使用 React JSX 转换
    "allowSyntheticDefaultImports": true, // 设置import方式
    "strict": true, // 开启所有严格类型检查选项
    "esModuleInterop": true, // 允许默认导出与 CommonJS 模块互操作
    "skipLibCheck": true, // 跳过库文件(*.d.ts)的类型检查
    "forceConsistentCasingInFileNames": true, // 强制文件名大小写一致
    "isolatedModules": true, // 确保每个文件可以被单独编译
    "resolveJsonModule": true, // 允许导入 JSON 模块
    "declaration": true, // 生成 .d.ts 声明文件
    "declarationMap": true, // 生成 .d.ts.map 声明源码映射文件
    "sourceMap": true, // 生成源码映射文件 (.js.map),用于调试
    "composite": true, // 启用项目编译
    "noUnusedLocals": false, // 未使用的局部变量是否报错
    "noUnusedParameters": false, // 控制函数中未使用的参数是否报错
    "noEmitOnError": false, // 未使用的局部变量或参数而报错
    "noImplicitAny": false,// 不会对隐式的any类型进行检查
    "outDir": "./dist"
  },
  "include": [
    "packages/*/src/**/*.ts",
    "packages/*/src/**/*.tsx",
    "shared-lib/src/**/*.ts",
    "shared-lib/src/**/*.tsx",
    "ui-components/src/**/*.ts",
    "ui-components/src/**/*.tsx",
  ],
  // "include": ["./src"],
  "exclude": [
    "vite-build/html.config.js",
    "vite.config.ts",
    "build",
    "node_modules", // 排除 node_modules 目录
    "**/__tests__/*", // 排除测试文件
    "html.config.d.ts"
  ]
}

tsconfig.json 配置文件

{
  "compilerOptions": {
    "target": "esnext",                       // 设置编译后的代码目标为最新的 ECMAScript 版本
    "module": "esnext",                       // 使用 ESNext 作为模块标准
    "moduleResolution": "node",               // 使用 Node.js 风格的模块解析
    "lib": ["dom", "dom.iterable", "esnext"], // 包含的库文件,这里包括了 DOM 和最新的 ECMAScript 功能
    "jsx": "react",                           // 使用 React JSX 转换
    "strict": true,                           // 开启所有严格类型检查选项
    "esModuleInterop": true,                  // 允许默认导出与 CommonJS 模块互操作
    "skipLibCheck": true,                     // 跳过库文件(*.d.ts)的类型检查
    "forceConsistentCasingInFileNames": true, // 强制文件名大小写一致
    "isolatedModules": true,                  // 确保每个文件可以被单独编译
    "resolveJsonModule": true,                // 允许导入 JSON 模块
    "baseUrl": ".",                           // 基础目录,用于解析非相对模块名
    "paths": {                                // 路径映射,用于设置别名
      "@/*": ["./src/*"]
    },
    "outDir": "./dist",                       // 指定输出文件夹
    "declaration": true,                      // 生成 .d.ts 声明文件
    "declarationMap": true,                   // 生成 .d.ts.map 声明源码映射文件
    "sourceMap": true,                        // 生成源码映射文件 (.js.map),用于调试
    "composite": true                         // 启用项目编译
  },
  "include": [
    "src"                                     // 包含 src 目录下的所有文件
  ],
  "exclude": [
    "node_modules",                           // 排除 node_modules 目录
    "**/__tests__/*"                          // 排除测试文件
  ]
}

解释每个参数的意思:

  • target: 设置编译后的代码目标为最新的 ECMAScript 版本,这样你就可以使用最新的 JavaScript 特性。
  • module: 设置模块标准,Vite 推荐使用原生 ES 模块。
  • moduleResolution: 设置模块解析策略,Node.js 风格意味着遵循 CommonJS 的解析算法。
  • lib: 包含的库文件,这里包括了 DOM 和最新的 ECMAScript 功能。
  • jsx: 设置 JSX 转换方式,这里使用 React 模式。
  • strict: 开启所有严格类型检查选项。
  • esModuleInterop: 允许默认导出与 CommonJS 模块互操作。
  • skipLibCheck: 跳过库文件的类型检查,可以提高编译速度。
  • forceConsistentCasingInFileNames: 强制文件名大小写一致,避免在大小写敏感的文件系统中出现问题。
  • isolatedModules: 确保每个文件可以被单独编译,这对于 Babel 类型的转换是必要的。
  • resolveJsonModule: 允许导入 JSON 模块。
  • baseUrl: 设置基础目录,用于解析非相对模块名。
  • paths: 设置路径映射,这里用于设置 src 目录的别名。
  • outDir: 指定编译后文件的输出目录

8、代码规范相关

检测eslintrc

在最外层(根目录)和每个子项目中都添加.eslintrc.js文件

在使用Lerna、pnpm Workspaces、React和TypeScript以及Vite的项目中,你可以选择在最外层(根目录)和每个子项目中都添加.eslintrc.js文件。这取决于你想要实现的代码风格和规则的一致性程度。

以下是一种可能的配置方式:

  1. 在最外层(根目录)创建一个.eslintrc.js文件,用于定义整个工作区的通用规则和设置。例如:
javascript
// .eslintrc.js (位于根目录)

module.exports = {
  root: true,
  env: {
    browser: true,
    es6: true,
    node: true,
  },
  extends: [
    'eslint:recommended',
    'plugin:react/recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:prettier/recommended', // 如果你使用Prettier进行代码格式化
  ],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaVersion: 2020,
    sourceType: 'module',
    project: './tsconfig.json', // 指向根目录的tsconfig.json
  },
  plugins: ['@typescript-eslint', 'react', 'react-hooks'],
  rules: {
    // 这里可以定义一些通用的规则
    'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx', '.ts', '.tsx'] }],
    'import/extensions': [
      'error',
      'ignorePackages',
      {
        js: 'never',
        jsx: 'never',
        ts: 'never',
        tsx: 'never',
      },
    ],
    'max-len': ['error', { code: 120, tabWidth: 2, ignoreUrls: true }],
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
  },
};
  1. 在每个子项目中创建或修改.eslintrc.js文件,用于覆盖或扩展根目录中的通用规则。例如,在一个名为my-react-package的子项目中:
javascript
// my-react-package/.eslintrc.js

module.exports = {
  extends: '../../../.eslintrc.js', // 指向根目录的.eslintrc.js
  rules: {
    // 这里可以定义子项目特有的规则,或者覆盖根目录中的规则
    'react/prop-types': 'off', // 如果你不希望在这个子项目中检查propTypes
  },
};

通过这种方式,你可以在根目录中定义通用的规则和设置,然后在每个子项目中根据需要进行定制。这样可以确保代码风格和规则在一定程度上保持一致,同时允许每个子项目有自己的特定规则。

请注意,你需要确保已经安装了必要的ESLint插件和依赖,如eslint, @typescript-eslint/eslint-plugin, @typescript-eslint/parser, eslint-plugin-react, 等。你可以在根目录运行以下命令进行安装:

**--workspace-root= ** || pnpm-workspace.yaml文件中添加 settings: ignore-workspace-root-check: true 否则跟路径下安装会告警

 pnpm install --save-dev --workspace-root=eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-plugin-react eslint-plugin-react-hooks

然后在每个子项目的package.json文件中添加一个脚本,如 "lint": "eslint .",以便通过运行 pnpm run lint 来检查代码风格和错误。

忽略检测的配置.eslintignore

.eslintignore文件用于指定哪些文件或目录应该被ESLint忽略,不进行代码风格和错误检查

txt
# .eslintignore (位于根目录)

# 忽略以下文件和目录
node_modules/
dist/
coverage/
*.min.js
**/*.d.ts

# 忽略特定子项目的特定文件
packages/my-react-package/public/*.js
packages/my-react-package/src/__tests__/*

在这个例子中:

  • node_modules/dist/coverage/是通常需要忽略的目录,因为它们包含生成的文件或第三方依赖。
  • *.min.js忽略了所有以.min.js结尾的压缩文件。
  • **/*.d.ts忽略了所有的TypeScript声明文件。
  • packages/my-react-package/public/*.jspackages/my-react-package/src/__tests__/*是针对特定子项目的文件忽略规则。你可以根据你的项目结构和需求添加类似的规则。

请注意,.eslintignore文件中的路径可以是相对路径(相对于.eslintignore文件的位置)或绝对路径。你可以使用通配符(*)和双星号(**)来匹配多个文件或目录。

.eslintignore文件放在根目录可以确保在整个工作区范围内应用这些忽略规则。如果你需要为某个子项目添加特定的忽略规则,你可以在该子项目的根目录下创建一个单独的.eslintignore文件,并在那里定义额外的规则。子项目的.eslintignore文件会与根目录的.eslintignore文件一起生效。

Package.json

"scripts": {
    "dev": "vite",
    "build": "vite build",
    "lint": "eslint --fix --ext .ts,.tsx src",
    "preview": "vite preview"
  },
  
  //我想忽略检查时候不运行 eslint 命令
  "lint": "echo 'Skipping lint'",
  

.eslintrc.cjs

module.exports = {
  root: true,
  env: { browser: true, es2020: true },
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:react-hooks/recommended',
  ],
  ignorePatterns: ['dist', '.eslintrc.cjs',"node_modules/", "build/"],
  parser: '@typescript-eslint/parser',
  plugins: ['react-refresh'],
  rules: {
    'react-refresh/only-export-components': [
      'warn',
      { allowConstantExport: true },
    ],
    "@typescript-eslint/no-explicit-any": "off",
    "react-hooks/exhaustive-deps": "off",
    "@typescript-eslint/ban-types": "off",
    "@typescript-eslint/no-var-requires": "off"
  },
}

统一代码编辑器格式设置.editorconfig

.editorconfig文件是一个用于统一代码编辑器格式设置的工具。它可以帮助你在团队中保持一致的编码风格和格式。以下是一个在使用Lerna、pnpm Workspaces、React、TypeScript和Vite的项目中的基本.editorconfig文件设置示例:

ini
# .editorconfig (位于根目录)

root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.js]
indent_style = space
indent_size = 2

[*.ts, *.tsx]
indent_style = space
indent_size = 2
semicolon = always
quote_type = double
jsx_single_quote = false
jsx_bracket_same_line = false

[*.json]
indent_style = space
indent_size = 2

[*.md, *.markdown]
trim_trailing_whitespace = false

[*.yaml, *.yml]
indent_style = space
indent_size = 2

[*.html]
indent_style = space
indent_size = 2

[*.css, *.scss, *.less]
indent_style = space
indent_size = 2

[*.vue]
indent_style = space
indent_size = 2

在这个例子中,各个属性的含义如下:

  • root = true:表示这是项目的根目录。
  • [section]:方括号定义了一个新的配置段,其中section可以是文件名、通配符或两者组合,用于指定这些设置应用到哪些文件。
  • charset:设置字符集为UTF-8。
  • end_of_line:设置行尾结束符为LF(Linux和macOS)。
  • insert_final_newline:在文件末尾插入一个新行。
  • trim_trailing_whitespace:删除行尾的空格。
  • indent_style:设置缩进样式,可以是tabspace
  • indent_size:设置缩进大小,如果indent_styletab,这个值可能被编辑器忽略。
  • semicolon:对于TypeScript和JavaScript文件,设置是否总是使用分号。
  • quote_type:对于TypeScript和JavaScript文件,设置字符串引号类型,可以是singledouble
  • jsx_single_quote:对于JSX文件,设置是否使用单引号。
  • jsx_bracket_same_line:对于JSX文件,设置标签的闭合括号是否与开始标签在同一行。

你可以根据你的项目需求和团队规范调整这些设置。将.editorconfig文件放在根目录可以确保在整个工作区范围内应用这些格式设置。大多数现代代码编辑器都支持.editorconfig,包括Visual Studio Code、Sublime Text、Atom等。