{"id":16347038,"url":"https://github.com/ospoon/simple-workspace","last_synced_at":"2025-07-21T16:33:35.661Z","repository":{"id":105557217,"uuid":"518383974","full_name":"OSpoon/simple-workspace","owner":"OSpoon","description":null,"archived":false,"fork":false,"pushed_at":"2023-09-14T08:17:03.000Z","size":95,"stargazers_count":7,"open_issues_count":0,"forks_count":4,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-05-19T05:05:50.041Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Vue","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/OSpoon.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2022-07-27T09:01:58.000Z","updated_at":"2024-03-28T13:51:57.000Z","dependencies_parsed_at":"2023-09-14T22:57:45.062Z","dependency_job_id":"32add6ec-2fb3-469c-bc15-1f6748508340","html_url":"https://github.com/OSpoon/simple-workspace","commit_stats":null,"previous_names":["ospoon/simple-workspace"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/OSpoon/simple-workspace","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OSpoon%2Fsimple-workspace","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OSpoon%2Fsimple-workspace/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OSpoon%2Fsimple-workspace/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OSpoon%2Fsimple-workspace/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/OSpoon","download_url":"https://codeload.github.com/OSpoon/simple-workspace/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OSpoon%2Fsimple-workspace/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":266334548,"owners_count":23913002,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-07-21T11:47:31.412Z","response_time":64,"last_error":null,"robots_txt_status":null,"robots_txt_updated_at":null,"robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-11T00:39:18.603Z","updated_at":"2025-07-21T16:33:35.639Z","avatar_url":"https://github.com/OSpoon.png","language":"Vue","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 项目越写越大，我是这样做拆分的\n\n## 写作背景：\n\n\u0026ensp;\u0026ensp;\u0026ensp;\u0026ensp;在几年前的一次 Vue 项目改造中利用原生+H5 的形式按模块菜单来拆分了多个 Vue 项目，在拆分时考虑到多项目维护带来的成本较大，我们将项目公共使用到的资源提升到项目 root 目录下，将子项目抽取为模板通过定制的脚手架创建每个子项目到 modules 下，并且支持单独打包、单独发布。这样项目结构的好处同时避免了项目庞大带来的首屏加载时间长，也避免了多人开发出现冲突的造成的矛盾。\n\n\u0026ensp;\u0026ensp;\u0026ensp;\u0026ensp;这样的项目结构在现在看来很多项目都有在使用，比如 Vue、Vite 等，它们共同使用到的 PNPM 的包管理器来组织这样的项目。同时我也在 B 站发现有伙伴使用 PNPM 组建了包含 PC 前端、PC 后端、H5 前端这样的项目模板。\n\n我们一起来搞一搞~\n\n## PNPM 介绍：\n\n### PNPM 的特点：\n\n1.  节约磁盘空间并提升安装速度；\n1.  创建非扁平化的 node_modules 文件夹。\n\n### PNPM 与 NodeJs 版本支持：\n\n| Node.js    | pnpm 4 | pnpm 5 | pnpm 6 | pnpm 7 |\n| ---------- | ------ | ------ | ------ | ------ |\n| Node.js 10 | ✔️     | ✔️     | ❌      | ❌      |\n| Node.js 12 | ✔️     | ✔️     | ✔️     | ❌      |\n| Node.js 14 | ✔️     | ✔️     | ✔️     | ✔️     |\n| Node.js 16 | 未知     | 未知     | ✔️     | ✔️     |\n| Node.js 18 | 未知     | 未知     | ✔️     | ✔️     |\n\n上述表格来自：\u003chttps://pnpm.io/zh/installation\u003e；\n\n### PNPM 与其他包管理功能对比：\n\n| 功能                 | pnpm                                                                           | Yarn                | npm                    |\n| ------------------ | ------------------------------------------------------------------------------ | ------------------- | ---------------------- |\n| 工作空间支持（monorepo）   | ✔️                                                                             | ✔️                  | ✔️                     |\n| 隔离的 node_modules   | ✔️ - 默认                                                                        | ✔️                  | ❌                      |\n| 提升的 node_modules   | ✔️                                                                             | ✔️                  | ✔️ - 默认                |\n| 自动安装 peers         | ✔️ - 通过 [auto-install-peers=true](https://pnpm.io/zh/npmrc#auto-install-peers) | ❌                   | ✔️                     |\n| Plug'n'Play        | ✔️                                                                             | ✔️ - 默认             | ❌                      |\n| 零安装                | ❌                                                                              | ✔️                  | ❌                      |\n| 修补依赖项              | ✔️                                                                             | ✔️                  | ❌                      |\n| 管理 Node.js 版本      | ✔️                                                                             | ❌                   | ❌                      |\n| 有锁文件               | ✔️ - pnpm-lock.yaml                                                            | ✔️ - yarn.lock      | ✔️ - package-lock.json |\n| 支持覆盖               | ✔️                                                                             | ✔️ - 通过 resolutions | ✔️                     |\n| 内容可寻址存储            | ✔️                                                                             | ❌                   | ❌                      |\n| 动态包执行              | ✔️ - 通过 pnpm dlx                                                               | ✔️ - 通过 yarn dlx    | ✔️ - 通过 npx            |\n| Side-effects cache | ✔️                                                                             | ❌                   | ❌                      |\n\n上述表格来自：\u003chttps://pnpm.io/zh/feature-comparison\u003e；\n\n### 安装 PNPM：\n\n```\nnpm install -g pnpm\n```\n\n### 快速开始命令：\n\n1.  在项目root目录安装所有依赖：`pnpm install`；\n1.  在项目root目录安装指定依赖：`pnpm add \u003cpkg\u003e`;\n1.  在项目root目录运行CMD命令：`pnpm \u003ccmd\u003e`;\n1.  在特定子集运行CMD命令：`pnpm -F \u003cpackage_selector\u003e \u003ccommand\u003e`；\n\n## 一起搞起来：\n\n![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/15506cf4e3e74863ac32eda047179799~tplv-k3u1fbpfcp-zoom-1.image)\n\n### 利用 vue@3 模板来创建 root 项目：\n\n```\npnpm create vue@3\n```\n\n![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3420b42b1c074caca0ef5303e2977c56~tplv-k3u1fbpfcp-zoom-1.image)\n\n### 定义工作空间目录结构\n\n使用 **pnpm** 管理的项目支持在 **root** 目录下使用 `pnpm-workspace.yaml` 文件来定义工作空间目录\n\n```\npackages:\n  # all packages in direct subdirs of packages/\n  - 'packages/*'\n  # all packages in subdirs of components/\n  - 'components/**'\n  # 获取数据相关的包在 apis 目录下\n  - 'apis/**'\n  # 通用工具相关的包在 utils 目录下\n  - 'utils/**'\n```\n\n### 使用 vite 来初始化公共模块：\n\n使用 vite 内置的基础项目模板创建 apis、utils两个公共模块\n\n#### 创建 apis 项目：\n\n```\nyarn create vite\n```\n\n![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3ac12818f28246029e75bc26e5aa34b8~tplv-k3u1fbpfcp-zoom-1.image)\n\n#### 创建 utils 项目：\n\n```\nyarn create vite\n```\n\n![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c20c28535bb8447991f4c15157ef4b77~tplv-k3u1fbpfcp-zoom-1.image)\n\n#### 调整 apis、utils 的项目名称和版本号：\n\n| **项目** | **name字段更新**          | **version字段更新** |\n| ------ | --------------------- | --------------- |\n| apis   | apis -\u003e @it200/apis   | 0.0.0 -\u003e 0.0.1  |\n| utils  | utils -\u003e @it200/utils | 0.0.0 -\u003e 0.0.1  |\n\n### 使用 vite 来初始化业务模块：\n\n业务模块创建到 packages 目录下，创建命令同上一小节，我们这次改用 vite 内置的 vue-ts 模板\n\n#### 创建三个module项目，整体的目录大致结构如下：\n\n```\nmy-workspace                       \n├─ apis                            \n│  ├─ src                                                                                   \n│  ├─ package.json                 \n│  └─ tsconfig.json     \n├─ utils                           \n│  ├─ src                                                                                  \n│  ├─ package.json                 \n│  └─ tsconfig.json  \n├─ packages                        \n│  ├─ module1                                 \n│  ├─ module2                                \n│  └─ module3                                \n├─ public                                         \n├─ src                                                         \n├─ env.d.ts                        \n├─ index.html                      \n├─ package.json                    \n├─ pnpm-lock.yaml                  \n├─ pnpm-workspace.yaml             \n├─ README.md                       \n├─ tsconfig.config.json            \n├─ tsconfig.json                   \n└─ vite.config.ts                  \n```\n\n#### 调整三个模块项目的名称和版本号\n\n| **项目**  | **name字段更新**              | **version字段更新** |\n| ------- | ------------------------- | --------------- |\n| module1 | module1 -\u003e @it200/module1 | 0.0.0 -\u003e 0.0.1  |\n| module2 | module2 -\u003e @it200/module2 | 0.0.0 -\u003e 0.0.1  |\n| module3 | module3 -\u003e @it200/module3 | 0.0.0 -\u003e 0.0.1  |\n\n#### 统一包管理器的使用：\n\n在创建的各模块的 `package.json` 中增加一条`script`，内容如下：\n\n```\n\"preinstall\": \"npx only-allow pnpm\"\n```\n\n### 开发utils模块：\n\n#### 开发Clipboard工具类（支持移动端和PC端两种提示风格）：\n\n##### 准备Clipboard工具类：\n\n```\nimport Clipboard from 'clipboard'\n\nexport const handleClipboard = (text: string, event: MouseEvent) =\u003e {\n  const clipboard = new Clipboard(event.target as Element, {\n    text: () =\u003e text\n  })\n  clipboard.on('success', () =\u003e {\n    clipboard.destroy()\n  })\n  clipboard.on('error', () =\u003e {\n    clipboard.destroy()\n  });\n  (clipboard as any).onClick(event)\n}\n```\n\n##### 配置相关依赖：\n\n1.  安装`vueuse`依赖库，监听屏幕变化；\n1.  安装`clipboard`依赖库，完成粘贴板基础功能；\n1.  安装`element-plus`PC风格组件库；\n1.  安装`vant`移动端风格组件库；\n1.  安装`vue`依赖库，因提示Issues with peer dependencies found，就先装上。\n\n##### 完善Clipboard工具类以支持不同风格提示：\n\nutils\\src\\clipboard.ts\n\n```\n// 手动导入vant中的通知组件及样式文件\nimport { Notify } from \"vant\";\nimport \"vant/es/notify/style\";\n\n// 手动导入element-plus中的通知组件及样式文件\nimport { ElMessage } from \"element-plus\";\nimport \"element-plus/es/components/message/style/css\";\n\n// 导入剪切板基础依赖\nimport Clipboard from \"clipboard\";\n// 导入vueuse/core 中监听浏览器端点变化的函数\nimport { useBreakpoints, breakpointsTailwind } from \"@vueuse/core\";\n\nconst sm = useBreakpoints(breakpointsTailwind).smaller(\"sm\");\n\n/* 依据sm值的变化来改变使用不同的通知风格 */\nexport const clipboardSuccess = () =\u003e\n  sm.value\n    ? Notify({\n        message: \"Copy successfully\",\n        type: \"success\",\n        duration: 1500,\n      })\n    : ElMessage({\n        message: \"Copy successfully\",\n        type: \"success\",\n        duration: 1500,\n      });\n\n/* 依据sm值的变化来改变使用不同的通知风格 */\nexport const clipboardError = () =\u003e\n  sm.value\n    ? Notify({\n        message: \"Copy failed\",\n        type: \"danger\",\n      })\n    : ElMessage({\n        message: \"Copy failed\",\n        type: \"error\",\n      });\n\nexport const handleClipboard = (text: string, event: MouseEvent) =\u003e {\n  const clipboard = new Clipboard(event.target as Element, {\n    text: () =\u003e text,\n  });\n  clipboard.on(\"success\", () =\u003e {\n    // 在复制成功后提示成功通知内容\n    clipboardSuccess();\n    clipboard.destroy();\n  });\n  clipboard.on(\"error\", () =\u003e {\n    // 在复制失败后提示失败通知内容\n    clipboardError();\n    clipboard.destroy();\n  });\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  (clipboard as any).onClick(event);\n};\n```\n\n##### 导出工具类的相关配置：\n\n1.  配置统一导出文件（`utils\\index.ts`）：\n\n```\nexport * from \"./src/clipboard\";\n```\n\n2.  修改`package.json`的`main`字段：\n\n```\n{\n  \"main\": \"index.ts\"\n}\n```\n\n##### 将utils模块安装到module1项目：\n\n1.  下面的命令在root目录执行，通过`-F`来执行命令执行的位置是`@it200/module1`，执行的命令是`add`。\n\n```\npnpm -F @it200/module1 add @it200/utils\n```\n\n注：当`@it200/utils`包升级后，执行执行`pnpm update`来更新相关依赖版本。\n\n2.  安装成功后的依赖信息如下：\n\n```\n{\n  \"dependencies\": {\n    \"@it200/utils\": \"workspace:^0.0.1\"\n  }\n}\n```\n\n##### 在module1项目中尝试使用Clipboard函数：\n\n1.  在模板中增加按钮：\n\n```\n\u003cbutton @click=\"copy\"\u003e复制\u003c/button\u003e\n```\n\n2.  在`setup`的`script`中增加对应函数并导入`handleClipboard`：\n\n```\nimport { handleClipboard } from \"@it200/utils\";\nconst copy = (e) =\u003e {\n  console.log(\"[ e ] \u003e\", e);\n  handleClipboard(\"haha\", e);\n};\n```\n\nPC端复制成功后提示风格：\n\n![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9dd59a76f959487da3d31b6ab1537e21~tplv-k3u1fbpfcp-zoom-1.image)\n\n移动端复制成功后提示风格：\n\n![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/62d90741d2ca43c28fa88966c9986e3b~tplv-k3u1fbpfcp-zoom-1.image)\n\n### 开发 apis 模块：\n\n#### 开发axios工具类函数：\n\n##### 准备axios工具类：\n\n```\nimport axios, { AxiosRequestConfig } from \"axios\";\n\nconst pending = {};\n\nconst CancelToken = axios.CancelToken;\nconst removePending = (key: string, isRequest = false) =\u003e {\n  if (Reflect.get(pending, key) \u0026\u0026 isRequest) {\n    Reflect.get(pending, key)(\"取消重复请求\");\n  }\n  Reflect.deleteProperty(pending, key);\n};\nconst getRequestIdentify = (config: AxiosRequestConfig, isReuest = false) =\u003e {\n  let url = config.url;\n  const suburl = config.url?.substring(1, config.url?.length) ?? \"\";\n  if (isReuest) {\n    url = config.baseURL + suburl;\n  }\n  return config.method === \"get\"\n    ? encodeURIComponent(url + JSON.stringify(config.params))\n    : encodeURIComponent(config.url + JSON.stringify(config.data));\n};\n\n// 创建一个AXIOS实例\nconst service = axios.create({\n  baseURL: import.meta.env.VITE_BASE_API, // url = base url + request url\n  // withCredentials: true, // send cookies when cross-domain requests\n  timeout: 16000, // 请求超时\n});\n\n// 请求拦截器\nservice.interceptors.request.use(\n  (config: AxiosRequestConfig) =\u003e {\n    // 拦截重复请求(即当前正在进行的相同请求)\n    const requestData = getRequestIdentify(config, true);\n    removePending(requestData, true);\n\n    config.cancelToken = new CancelToken((c: any) =\u003e {\n      Reflect.set(pending, requestData, c);\n    });\n\n    // 请求发送前的预处理(如:获取token等)\n    // if (store.getters.token) {\n    //   // let each request carry token\n    //   // ['X-AUTH-TOKEN'] is a custom headers key\n    //   // please modify it according to the actual situation\n    //   config.headers['X-AUTH-TOKEN'] = getToken()\n    // }\n    return config;\n  },\n  (error: any) =\u003e {\n    // do something with request error\n    console.log(error); // for debug\n    return Promise.reject(error);\n  }\n);\n\n// response interceptor\nservice.interceptors.response.use(\n  (response: { config: AxiosRequestConfig; data: any }) =\u003e {\n    // 把已经完成的请求从 pending 中移除\n    const requestData = getRequestIdentify(response.config);\n    removePending(requestData);\n    const res = response.data;\n    return res;\n  },\n  (error: {\n    message: string;\n    config: { showLoading: any };\n    response: { status: any };\n    request: any;\n  }) =\u003e {\n    console.log(error.message);\n    if (error) {\n      if (error.response) {\n        switch (error.response.status) {\n          case 400:\n            error.message = \"错误请求\";\n            break;\n          case 401:\n            error.message = \"未授权，请重新登录\";\n            break;\n          default:\n            error.message = `连接错误${error.response.status}`;\n        }\n        const errData = {\n          code: error.response.status,\n          message: error.message,\n        };\n        console.log(\"统一错误处理: \", errData);\n      } else if (error.request) {\n        console.log(\"统一错误处理: \", \"网络出错，请稍后重试\");\n      }\n    }\n    return Promise.reject(error);\n  }\n);\n\nexport default service;\n```\n\n##### 配置相关依赖：\n\n1.  安装`axios`依赖库，完成数据请求的发送及处理；\n1.  安装`vant`依赖库，完成请求数据后的状态提示等。\n\n说明：在`apis`模块中就不再做手机端和PC端的风格切换了；\n\n##### 完善axios工具类：\n\napis\\src\\axios.ts，部分逻辑有删减，仅保证基础功能正常\n\n```\nimport { Dialog } from \"vant\";\nimport \"vant/es/dialog/style\";\n\nimport { Toast } from \"vant\";\nimport \"vant/es/toast/style\";\n\nimport axios, { AxiosRequestConfig } from \"axios\";\n\nconst pending = {};\n\nconst CancelToken = axios.CancelToken;\nconst removePending = (key: string, isRequest = false) =\u003e {\n  if (Reflect.get(pending, key) \u0026\u0026 isRequest) {\n    Reflect.get(pending, key)(\"取消重复请求\");\n  }\n  Reflect.deleteProperty(pending, key);\n};\nconst getRequestIdentify = (config: AxiosRequestConfig, isReuest = false) =\u003e {\n  let url = config.url;\n  const suburl = config.url?.substring(1, config.url?.length) ?? \"\";\n  if (isReuest) {\n    url = config.baseURL + suburl;\n  }\n  return config.method === \"get\"\n    ? encodeURIComponent(url + JSON.stringify(config.params))\n    : encodeURIComponent(config.url + JSON.stringify(config.data));\n};\n\n// 创建一个AXIOS实例\nconst service = axios.create({\n  baseURL: import.meta.env.VITE_BASE_API, // url = base url + request url\n  // withCredentials: true, // send cookies when cross-domain requests\n  timeout: 16000, // 请求超时\n});\n\n// 请求拦截器\nservice.interceptors.request.use(\n  (config: AxiosRequestConfig) =\u003e {\n    // 拦截重复请求(即当前正在进行的相同请求)\n    const requestData = getRequestIdentify(config, true);\n    removePending(requestData, true);\n\n    config.cancelToken = new CancelToken((c: any) =\u003e {\n      Reflect.set(pending, requestData, c);\n    });\n\n    // 是否开启loading\n    if (config.showLoading) {\n      Toast.loading({\n        duration: 0,\n        mask: true,\n        forbidClick: true,\n        message: \"加载中...\",\n        loadingType: \"spinner\",\n      });\n    }\n\n    // 请求发送前的预处理(如:获取token等)\n    // if (store.getters.token) {\n    //   // let each request carry token\n    //   // ['X-AUTH-TOKEN'] is a custom headers key\n    //   // please modify it according to the actual situation\n    //   config.headers['X-AUTH-TOKEN'] = getToken()\n    // }\n    return config;\n  },\n  (error: any) =\u003e {\n    // do something with request error\n    console.log(error); // for debug\n    Toast.loading({\n      message: \"网络出错，请重试\",\n      duration: 1500,\n      type: \"fail\",\n    });\n    return Promise.reject(error);\n  }\n);\n\n// response interceptor\nservice.interceptors.response.use(\n  (response: { config: AxiosRequestConfig; data: any }) =\u003e {\n    // 把已经完成的请求从 pending 中移除\n    const requestData = getRequestIdentify(response.config);\n    removePending(requestData);\n\n    if (response.config.showLoading) {\n      Toast.clear();\n    }\n\n    const res = response.data;\n    return res;\n  },\n  (error: {\n    message: string;\n    config: { showLoading: any };\n    response: { status: any };\n    request: any;\n  }) =\u003e {\n    console.log(error.message);\n    if (error) {\n      if (error.config \u0026\u0026 error.config.showLoading) {\n        Toast.clear();\n      }\n      if (error.response) {\n        switch (error.response.status) {\n          case 400:\n            error.message = \"错误请求\";\n            break;\n          case 401:\n            error.message = \"未授权，请重新登录\";\n            break;\n          default:\n            error.message = `连接错误${error.response.status}`;\n        }\n        const errData = {\n          code: error.response.status,\n          message: error.message,\n        };\n        console.log(\"统一错误处理: \", errData);\n        Dialog({ title: \"提示\", message: errData.message || \"Error\" });\n      } else if (error.request) {\n        Toast.loading({\n          message: \"网络出错，请稍后重试\",\n          duration: 1500,\n          type: \"fail\",\n        });\n      }\n    }\n    return Promise.reject(error);\n  }\n);\n\nexport default service;\n```\n\n##### 编写userApi类，汇总关于user对象的数据读取：\n\napis\\src\\user.ts\n\n```\nimport service from \"./axios\";\n\nexport const UserApi = {\n  getUsers: () =\u003e service.get\u003cany\u003e(\"/users\"),\n};\n```\n\n##### 导出userApi类的相关配置：\n\n1.  配置统一导出文件（`apis\\index.ts`）：\n\n```\nexport * from \"./src/user\";\n```\n\n2.  修改`package.json`的`main`字段：\n\n```\n{\n  \"main\": \"index.ts\"\n}\n```\n\n##### 在module2项目中尝试使用userApi类：\n\n1.  定义模板：\n\n```\n\u003ctemplate\u003e\n  \u003cbutton @click=\"getUserList\"\u003e获取用户列表\u003c/button\u003e\n  \u003cul\u003e\n    \u003cli v-for=\"user in users\" :key=\"user.id\"\u003e\n      {{ user.name }}、{{ user.age }}\n    \u003c/li\u003e\n  \u003c/ul\u003e\n\u003c/template\u003e\n```\n\n2.  安装、导入、编写逻辑：\n\n```\npnpm -F @it200/module2 add @it200/apis\n```\n\n```\n\u003cscript setup lang=\"ts\"\u003e\nimport { UserApi } from \"@it200/apis\";\nimport { ref } from \"vue\";\nconst users = ref();\nconst getUserList = async () =\u003e {\n  const resp = await UserApi.getUsers();\n  users.value = resp;\n};\n\u003c/script\u003e\n```\n\n\u003chttps://www.awesomescreenshot.com/video/9976769?key=be6dffcf6e60e59ec5a601b34582e57b\u003e\n\n![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2a4a2552d096464aace2454703294c0b~tplv-k3u1fbpfcp-zoom-1.image)\n\n#### 使用[Mockend](https://mockend.com/)来Mock数据：\n\n1.  选择一个符合自己的方案：\n\n![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2e7bd821b9804fb888d00fcf25f4f0bc~tplv-k3u1fbpfcp-zoom-1.image)\n\n2.  选择要安装到得公共项目仓库，Github组织不支持免费的（只为截个图）：\n\n![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/af1cdddc316942e19e14975346dec5a3~tplv-k3u1fbpfcp-zoom-1.image)\n\n3.  在项目root目录新建`.mockend.json`文件：\n\n```\n{\n  \"User\": {\n    \"name\": {\n      \"string\": {}\n    },\n    \"avatarUrl\": {\n      \"regexp\": \"https://i\\.pravatar\\.cc/150\\?u=[0-9]{5}\"\n    },\n    \"statusMessage\": {\n      \"string\": [\n        \"working from home\",\n        \"watching Netflix\"\n      ]\n    },\n    \"email\": {\n      \"regexp\": \"#[a-z]{5,10}@[a-z]{5}\\.[a-z]{2,3}\"\n    },\n    \"color\": {\n      \"regexp\": \"#[0-9A-F]{6}\"\n    },\n    \"age\": {\n      \"int\": {\n        \"min\": 21,\n        \"max\": 100\n      }\n    },\n    \"isPublic\": {\n      \"boolean\": {}\n    }\n  }\n}\n```\n\n4.  通过 \u003chttps://mockend.com/OSpoon/data-mock/users\u003e 就可以获取到mock数据了；\n4.  更多配置请参考\u003chttps://docs.mockend.com/\u003e。\n\n### 开发 Components 模块：\n\n#### 开发Card组件，并应用到module3项目中：\n\n##### 使用pnpm create vue@3来创建项目模板，修改项目名称和版本号：\n\n##### 创建如下card组件目录结构：\n\n```\ncomponents                   \n├─ card                      \n│  ├─ src                    \n│  │  ├─ card.scss           \n│  │  └─ index.vue           \n│  └─ index.ts         \n```\n\n##### 组件模板及配置：\n\n组件名称通过defineComponent函数导入，在注册组件时读取使用\n\n```\n\u003cscript lang=\"ts\"\u003e\nimport { defineComponent } from \"vue\";\nexport default defineComponent({\n  name: \"it-card\",\n});\n\u003c/script\u003e\n\u003cscript setup lang=\"ts\"\u003e\nconst props = defineProps({\n  shadow: {\n    type: String,\n    default: \"always\",\n  },\n  bodyStyle: {\n    type: Object,\n    default: () =\u003e {\n      return { padding: \"20px\" };\n    },\n  },\n});\nconsole.log(\"[ props ] \u003e\", props);\n\u003c/script\u003e\n\n\u003ctemplate\u003e\n  \u003cdiv class=\"it-card\"\u003e\n    \u003cdiv :class=\"`is-${shadow}-shadow`\"\u003e\u003c/div\u003e\n    \u003cdiv class=\"it-card__body\" :style=\"bodyStyle\"\u003e\n      \u003cslot\u003e\u003c/slot\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n\u003c/template\u003e\n\n\u003cstyle lang=\"scss\" scoped\u003e\u003c/style\u003e\n```\n\n##### 组件样式文件：\n\n```\n.it-card {\n    border-radius: 4px;\n    border: 1px solid #ebeef5;\n    background-color: #fff;\n    overflow: hidden;\n    color: #303133;\n    transition: 0.3s;\n\n    .it-card__body {\n        padding: 20px;\n    }\n\n    .is-always-shadow {\n        box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);\n    }\n\n    .is-hover-shadow:hover {\n        box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);\n    }\n\n    .is-never-shadow {\n        box-shadow: none;\n    }\n}\n```\n\n##### 组件安装插件：\n\n```\nimport type { App } from \"vue\";\nimport Card from \"./src/index.vue\";\n\nexport default {\n  install(app: App) {\n    app.component(Card.name, Card);\n  },\n};\n```\n\n##### 在Components项目中尝试使用Card组件：\n\n1.  导入组件相关配置并安装，`components\\src\\main.ts`\n\n```\nimport Card from \"./components/card/index\";\nimport \"./components/card/src/card.scss\";\n\napp.use(Card);\n```\n\n2.  在`App.vue`组件中使用：\n\n```\n\u003ctemplate\u003e\n  \u003cit-card style=\"width: 235px\" :body-style=\"{ padding: '0px' }\"\u003e\n    \u003cimg\n      src=\"https://shadow.elemecdn.com/app/element/hamburger.9cf7b091-55e9-11e9-a976-7f4d0b07eef6.png\"\n      class=\"image\"\n    /\u003e\n    \u003cdiv style=\"padding: 14px\"\u003e\n      \u003cspan\u003e好吃的汉堡\u003c/span\u003e\n      \u003cdiv class=\"bottom\"\u003e\n        \u003ctime class=\"time\"\u003e\"2022-05-03T16:21:26.010Z\"\u003c/time\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n  \u003c/it-card\u003e\n\u003c/template\u003e\n```\n\n![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/224cc078990f4271aad50d2669140222~tplv-k3u1fbpfcp-zoom-1.image)\n\n##### 准备导入组件的相关配置：\n\n1.  配置统一导出文件：\n\n```\nimport Card from \"./src/components/card/index\";\nexport default {\n  Card,\n};\n```\n\n2.  修改`package.json`的`main`字段：\n\n```\n{\n  \"main\": \"index.ts\"\n}\n```\n\n##### 安装、导入到module3：\n\n1.  安装`components`组件包：\n\n```\npnpm -F @it200/module3 add @it200/components\n```\n\n2.  导入`components`组件包：\n\n```\nimport Comps from \"@it200/components\";\nimport \"@it200/components/src/components/card/src/card.scss\";\n\napp.use(Comps.Card);\n```\n\n3.  使用方式同在Components项目中验证一样，效果一样，就不再演示了。\n\n## 扩展（Changesets发布变更）：\n\n### 增加相关配置：\n\n1.  安装`changesets`到工作空间根目录：\n\n```\npnpm add -Dw @changesets/cli\n```\n\n2.  执行`changesets`初始化命令：\n\n```\npnpm changeset init\n```\n\n### 生成新的changesets：\n\n```\npnpm changeset\n```\n\n注意：第一次运行前请检查`git`分支名称和`.changeset\\config.json`中的`baseBranch`是否一致。\n\n#### 生成示例：\n\n```\nPS xxx\u003e pnpm changeset\n🦋  Which packages would you like to include? · @it200/module3\n🦋  Which packages should have a major bump? · No items were selected\n🦋  Which packages should have a minor bump? · @it200/module3\n🦋  Please enter a summary for this change (this will be in the changelogs).\n🦋    (submit empty line to open external editor)\n🦋  Summary · 增加components模块的配置和使用\n🦋\n🦋  === Summary of changesets ===\n🦋  minor:  @it200/module3\n🦋\n🦋  Note: All dependents of these packages that will be incompatible with\n🦋  the new version will be patch bumped when this changeset is applied.\n🦋\n🦋  Is this your desired changeset? (Y/n) · true\n🦋  Changeset added! - you can now commit it\n🦋\n🦋  If you want to modify or expand on the changeset summary, you can find it here\n🦋  info D:\\daydayup\\my-workspace.changeset\\purple-dodos-check.md\n```\n\n### 发布变更：\n\n执行命令，会依据先前生成的变更集来在对应的`package`中的项目中生成对应的`CHANGELOG.md`并提高对应项目的`version`，版本提升还需遵守语义化版本规范要求：\n\n```\npnpm changeset version\n```\n\n![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2bd285b359c24cfb8e046d905680d674~tplv-k3u1fbpfcp-zoom-1.image)\n\n后续的步骤还需按项目的实际情况来考虑，这里将变更日志生成、版本号提升后就先告一段落了~\n\n## 总结：\n\n\u0026ensp;\u0026ensp;\u0026ensp;\u0026ensp;这里使用了工作空间的概念来实现了大项目的拆分工作，每一个单独的模块、项目都可以独立维护、测试、构建，同时在 pnpm 的 node_modules 管理模式下节约了磁盘空间并提升安装速度。在这里只是小试牛刀，更多的特性还没有体现出来，需要后续跟进学习。项目的拆分和搭建没有特别的约定要做的一模一样，符合实际情况的考虑就是最优。\n\n我正在参与掘金技术社区创作者签约计划招募活动，[点击链接报名投稿](https://juejin.cn/post/7112770927082864653)。\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fospoon%2Fsimple-workspace","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fospoon%2Fsimple-workspace","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fospoon%2Fsimple-workspace/lists"}