您的位置首页  散文精选

加载出错(加载出错,请稍后重试:-10)


写作背景:    在几年前的一次 Vue 项目改造中利用原生+H5 的形式按模块菜单来拆分了多个 Vue 项目,在拆分时考虑到多项目维护带来的成本较大,我们将项目公共使用到的

加载出错(加载出错,请稍后重试:-10)

 

写作背景:    在几年前的一次 Vue 项目改造中利用原生+H5 的形式按模块菜单来拆分了多个 Vue 项目,在拆分时考虑到多项目维护带来的成本较大,我们将项目公共使用到的资源提升到项目 root 目录下,将子项目抽取为模板通过定制的脚手架创建每个子项目到 modules 下,并且支持单独打包、单独发布。

这样项目结构的好处同时避免了项目庞大带来的首屏加载时间长,也避免了多人开发出现冲突的造成的矛盾    这样的项目结构在现在看来很多项目都有在使用,比如 Vue、Vite 等,它们共同使用到的 PNPM 的包管理器来组织这样的项目。

同时我也在 B 站发现有伙伴使用 PNPM 组建了包含 PC 前端、PC 后端、H5 前端这样的项目模板我们一起来搞一搞~PNPM 介绍:PNPM 的特点:节约磁盘空间并提升安装速度;创建非扁平化的 node_modules 文件夹。

PNPM 与 NodeJs 版本支持:

PNPM 与其他包管理功能对比:

安装 PNPM:npm install -g pnpm快速开始命令:在项目root目录安装所有依赖:pnpm install在项目root目录安装指定依赖:pnpm add 在项目root目录运行CMD命令:

pnpm 在特定子集运行CMD命令:pnpm -F 一起搞起来:

利用 vue@3 模板来创建 root 项目:pnpm create vue@3

定义工作空间目录结构使用 pnpm 管理的项目支持在 root 目录下使用 pnpm-workspace.yaml 文件来定义工作空间目录packages: # all packages in direct subdirs of packages/

- packages/* # all packages in subdirs of components/ - components/** # 获取数据相关的包在 apis 目录下 - apis/**

# 通用工具相关的包在 utils 目录下 - utils/**使用 vite 来初始化公共模块:使用 vite 内置的基础项目模板创建 apis、utils两个公共模块创建 apis 项目:yarn create vite

创建 utils 项目:yarn create vite

调整 apis、utils 的项目名称和版本号:

使用 vite 来初始化业务模块:业务模块创建到 packages 目录下,创建命令同上一小节,我们这次改用 vite 内置的 vue-ts 模板创建三个module项目,整体的目录大致结构如下:my-workspace

├─ apis │ ├─ src

│ ├─ package.json │ └─ tsconfig.json ├─ utils │ ├─ src

│ ├─ package.json │ └─ tsconfig.json ├─ packages │ ├─ module1

│ ├─ module2 │ └─ module3 ├─ public

├─ src ├─ env.d.ts ├─ index.html

├─ package.json ├─ pnpm-lock.yaml ├─ pnpm-workspace.yaml

├─ README.md ├─ tsconfig.config.json ├─ tsconfig.json

└─ vite.config.ts 调整三个模块项目的名称和版本号

统一包管理器的使用:在创建的各模块的package.json中增加一条script,内容如下:"preinstall": "npx only-allow pnpm"开发utils模块:开发Clipboard工具类(支持移动端和PC端两种提示风格):

准备Clipboard工具类:import Clipboard from clipboardexport const handleClipboard = (text: string, event: MouseEvent) => {

const clipboard = new Clipboard(event.target as Element, { text: () => text }) clipboard.on(success, () => {

clipboard.destroy() }) clipboard.on(error, () => { clipboard.destroy() }); (clipboard as any).onClick(event)

}配置相关依赖:安装vueuse依赖库,监听屏幕变化;安装clipboard依赖库,完成粘贴板基础功能;安装element-plusPC风格组件库;安装vant移动端风格组件库;安装vue依赖库,因提示Issues with peer dependencies found,就先装上。

完善Clipboard工具类以支持不同风格提示:utils\src\clipboard.ts// 手动导入vant中的通知组件及样式文件import { Notify } from "vant";import "vant/es/notify/style";

// 手动导入element-plus中的通知组件及样式文件import { ElMessage } from "element-plus";import "element-plus/es/components/message/style/css";

// 导入剪切板基础依赖import Clipboard from "clipboard";// 导入vueuse/core 中监听浏览器端点变化的函数import { useBreakpoints, breakpointsTailwind } from "@vueuse/core";

const sm = useBreakpoints(breakpointsTailwind).smaller("sm");/* 依据sm值的变化来改变使用不同的通知风格 */export const clipboardSuccess = () =>

sm.value ? Notify({ message: "Copy successfully", type: "success", duration: 1500,

}) : ElMessage({ message: "Copy successfully", type: "success", duration: 1500,

});/* 依据sm值的变化来改变使用不同的通知风格 */export const clipboardError = () => sm.value ? Notify({ message: "Copy failed",

type: "danger", }) : ElMessage({ message: "Copy failed", type: "error",

});export const handleClipboard = (text: string, event: MouseEvent) => { const clipboard = new Clipboard(event.target as Element, {

text: () => text, }); clipboard.on("success", () => { // 在复制成功后提示成功通知内容 clipboardSuccess();

clipboard.destroy(); }); clipboard.on("error", () => { // 在复制失败后提示失败通知内容 clipboardError();

clipboard.destroy(); }); // eslint-disable-next-line @typescript-eslint/no-explicit-any (clipboard as any).onClick(event);

};导出工具类的相关配置:配置统一导出文件(utils\index.ts):export * from "./src/clipboard";修改package.json的main字段:{ "main": "index.ts"

}将utils模块安装到module1项目:下面的命令在root目录执行,通过-F来执行命令执行的位置是@it200/module1,执行的命令是addpnpm -F @it200/module1 add @it200/utils。

注:当@it200/utils包升级后,执行执行pnpm update来更新相关依赖版本安装成功后的依赖信息如下:{ "dependencies": { "@it200/utils": "workspace:^0.0.1"。

}}在module1项目中尝试使用Clipboard函数:在模板中增加按钮:复制在setup的script中增加对应函数并导入handleClipboard

:import { handleClipboard } from "@it200/utils";const copy = (e) => { console.log("[ e ] >", e); handleClipboard("haha", e);

};PC端复制成功后提示风格:

移动端复制成功后提示风格:

开发 apis 模块:开发axios工具类函数:准备axios工具类:import axios, { AxiosRequestConfig } from "axios";const pending = {};

const CancelToken = axios.CancelToken;const removePending = (key: string, isRequest = false) => { if (Reflect.get(pending, key) && isRequest) {

Reflect.get(pending, key)("取消重复请求"); } Reflect.deleteProperty(pending, key);};const getRequestIdentify = (config: AxiosRequestConfig, isReuest = false) => {

let url = config.url; const suburl = config.url?.substring(1, config.url?.length) ?? ""; if (isReuest) {

url = config.baseURL + suburl; } return config.method === "get" ? encodeURIComponent(url + JSON.stringify(config.params))

: encodeURIComponent(config.url + JSON.stringify(config.data));};// 创建一个AXIOS实例const service = axios.create({

baseURL: import.meta.env.VITE_BASE_API, // url = base url + request url // withCredentials: true, // send cookies when cross-domain requests

timeout: 16000, // 请求超时});// 请求拦截器service.interceptors.request.use( (config: AxiosRequestConfig) => {

// 拦截重复请求(即当前正在进行的相同请求) const requestData = getRequestIdentify(config, true); removePending(requestData, true);

config.cancelToken = new CancelToken((c: any) => { Reflect.set(pending, requestData, c); });

// 请求发送前的预处理(如:获取token等) // if (store.getters.token) { // // let each request carry token

// // [X-AUTH-TOKEN] is a custom headers key // // please modify it according to the actual situation

// config.headers[X-AUTH-TOKEN] = getToken() // } return config; }, (error: any) => { // do something with request error

console.log(error); // for debug return Promise.reject(error); });// response interceptorservice.interceptors.response.use(

(response: { config: AxiosRequestConfig; data: any }) => { // 把已经完成的请求从 pending 中移除 const requestData = getRequestIdentify(response.config);

removePending(requestData); const res = response.data; return res; }, (error: { message: string;

config: { showLoading: any }; response: { status: any }; request: any; }) => { console.log(error.message);

if (error) { if (error.response) { switch (error.response.status) { case 400:

error.message = "错误请求"; break; case 401: error.message = "未授权,请重新登录";

break; default: error.message = `连接错误${error.response.status}`; }

const errData = { code: error.response.status, message: error.message, };

console.log("统一错误处理: ", errData); } else if (error.request) { console.log("统一错误处理: ", "网络出错,请稍后重试");

} } return Promise.reject(error); });export default service;配置相关依赖:安装axios依赖库,完成数据请求的发送及处理;

安装vant依赖库,完成请求数据后的状态提示等说明:在apis模块中就不再做手机端和PC端的风格切换了;完善axios工具类:apis\src\axios.ts,部分逻辑有删减,仅保证基础功能正常import { Dialog } from "vant";。

import "vant/es/dialog/style";import { Toast } from "vant";import "vant/es/toast/style";import axios, { AxiosRequestConfig } from "axios";

const pending = {};const CancelToken = axios.CancelToken;const removePending = (key: string, isRequest = false) => {

if (Reflect.get(pending, key) && isRequest) { Reflect.get(pending, key)("取消重复请求"); } Reflect.deleteProperty(pending, key);

};const getRequestIdentify = (config: AxiosRequestConfig, isReuest = false) => { let url = config.url;

const suburl = config.url?.substring(1, config.url?.length) ?? ""; if (isReuest) { url = config.baseURL + suburl;

} return config.method === "get" ? encodeURIComponent(url + JSON.stringify(config.params)) : encodeURIComponent(config.url + JSON.stringify(config.data));

};// 创建一个AXIOS实例const service = axios.create({ baseURL: import.meta.env.VITE_BASE_API, // url = base url + request url

// withCredentials: true, // send cookies when cross-domain requests timeout: 16000, // 请求超时});// 请求拦截器

service.interceptors.request.use( (config: AxiosRequestConfig) => { // 拦截重复请求(即当前正在进行的相同请求) const requestData = getRequestIdentify(config, true);

removePending(requestData, true); config.cancelToken = new CancelToken((c: any) => { Reflect.set(pending, requestData, c);

}); // 是否开启loading if (config.showLoading) { Toast.loading({ duration: 0, mask: true,

forbidClick: true, message: "加载中...", loadingType: "spinner", }); } // 请求发送前的预处理(如:获取token等)

// if (store.getters.token) { // // let each request carry token // // [X-AUTH-TOKEN] is a custom headers key

// // please modify it according to the actual situation // config.headers[X-AUTH-TOKEN] = getToken()

// } return config; }, (error: any) => { // do something with request error console.log(error); // for debug

Toast.loading({ message: "网络出错,请重试", duration: 1500, type: "fail", }); return Promise.reject(error);

});// response interceptorservice.interceptors.response.use( (response: { config: AxiosRequestConfig; data: any }) => {

// 把已经完成的请求从 pending 中移除 const requestData = getRequestIdentify(response.config); removePending(requestData);

if (response.config.showLoading) { Toast.clear(); } const res = response.data; return res;

}, (error: { message: string; config: { showLoading: any }; response: { status: any }; request: any;

}) => { console.log(error.message); if (error) { if (error.config && error.config.showLoading) {

Toast.clear(); } if (error.response) { switch (error.response.status) { case 400:

error.message = "错误请求"; break; case 401: error.message = "未授权,请重新登录";

break; default: error.message = `连接错误${error.response.status}`; }

const errData = { code: error.response.status, message: error.message, };

console.log("统一错误处理: ", errData); Dialog({ title: "提示", message: errData.message || "Error" });

} else if (error.request) { Toast.loading({ message: "网络出错,请稍后重试", duration: 1500,

type: "fail", }); } } return Promise.reject(error); });export default service;

编写userApi类,汇总关于user对象的数据读取:apis\src\user.tsimport service from "./axios";export const UserApi = { getUsers: () => service.get("/users"),

};导出userApi类的相关配置:配置统一导出文件(apis\index.ts):export * from "./src/user";修改package.json的main字段:{ "main": "index.ts"

}在module2项目中尝试使用userApi类:定义模板: 获取用户列表

{{ user.name }}、{{ user.age }} 安装、导入、编写逻辑:pnpm -F @it200/module2 add @it200/apis

import { UserApi } from "@it200/apis";import { ref } from "vue";const users = ref();

const getUserList = async () => { const resp = await UserApi.getUsers(); users.value = resp;};

https://www.awesomescreenshot.com/video/9976769?key=be6dffcf6e60e59ec5a601b34582e57b

使用Mockend来Mock数据:选择一个符合自己的方案:

选择要安装到得公共项目仓库,Github组织不支持免费的(只为截个图):

在项目root目录新建.mockend.json文件:{ "User": { "name": { "string": {} }, "avatarUrl": { "regexp": "https://i\.pravatar\.cc/150\?u=[0-9]{5}"

}, "statusMessage": { "string": [ "working from home", "watching Netflix" ]

}, "email": { "regexp": "#[a-z]{5,10}@[a-z]{5}\.[a-z]{2,3}" }, "color": { "regexp": "#[0-9A-F]{6}"

}, "age": { "int": { "min": 21, "max": 100 } }, "isPublic": { "boolean": {}

} } }通过https://mockend.com/OSpoon/data-mock/users就可以获取到mock数据了;更多配置请参考https://docs.mockend.com/

开发 Components 模块:开发Card组件,并应用到module3项目中:使用pnpm create vue@3来创建项目模板,修改项目名称和版本号:创建如下card组件目录结构:components

├─ card │ ├─ src │ │ ├─ card.scss │ │ └─ index.vue

│ └─ index.ts 组件模板及配置:组件名称通过defineComponent函数导入,在注册组件时读取使用import { defineComponent } from "vue";

export default defineComponent({ name: "it-card",});const props = defineProps({

shadow: { type: String, default: "always", }, bodyStyle: { type: Object, default: () => {

return { padding: "20px" }; }, },});console.log("[ props ] >", props);

组件样式文件:.it-card { border-radius: 4px;

border: 1px solid #ebeef5; background-color: #fff; overflow: hidden; color: #303133; transition: 0.3s;

.it-card__body { padding: 20px; } .is-always-shadow { box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);

} .is-hover-shadow:hover { box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%); } .is-never-shadow {

box-shadow: none; }}组件安装插件:import type { App } from "vue";import Card from "./src/index.vue";

export default { install(app: App) { app.component(Card.name, Card); }, }在Components项目中尝试使用Card组件:

导入组件相关配置并安装components\src\main.tsimport Card from "./components/card/index";import "./components/card/src/card.scss";

app.use(Card);在App.vue组件中使用:

class="image" />

好吃的汉堡

"2022-05-03T16:21:26.010Z"

准备导入组件的相关配置:配置统一导出文件:import Card from"./src/components/card/index";exportdefault { Card,};修改package.json

的main字段:{ "main": "index.ts"}安装、导入到module3:安装components组件包:pnpm -F @it200/module3 add @it200/components

导入components组件包:import Comps from "@it200/components";import "@it200/components/src/components/card/src/card.scss";

app.use(Comps.Card);使用方式同在Components项目中验证一样,效果一样,就不再演示了扩展(Changesets发布变更):增加相关配置:安装changesets到工作空间根目录:。

pnpm add -Dw @changesets/cli执行changesets初始化命令:pnpm changeset init生成新的changesets:pnpm changeset注意:第一次运行前请检查

git分支名称和.changeset\config.json中的baseBranch是否一致生成示例:PS xxx> pnpm changeset  Which packages would you like to include? · 。

@it200/module3 Which packages should have a major bump? · No items were selected  Which packages should have a minor bump? · 

@it200/module3 Please enter a summary forthis change (this will be in the changelogs).     (submit empty line to 

openexternal editor) Summary · 增加components模块的配置和使用 === Summary of changesets ===      minor:  @it200

/module3 Note: All dependents of these packages that will be incompatible with the new version will be patch bumped 

whenthis changeset is applied.     Is this your desired changeset? (Y/n) · true  Changeset added! - you can now commit it

If you want to modify or expand on the changeset summary, you can find it here info D:\daydayup\my-workspace.changeset\purple-dodos-check.md

发布变更:执行命令,会依据先前生成的变更集来在对应的package中的项目中生成对应的CHANGELOG.md并提高对应项目的version,版本提升还需遵守语义化版本规范要求:pnpm changeset version

后续的步骤还需按项目的实际情况来考虑,这里将变更日志生成、版本号提升后就先告一段落了~总结:    这里使用了工作空间的概念来实现了大项目的拆分工作,每一个单独的模块、项目都可以独立维护、测试、构建,同时在

pnpm 的 node_modules 管理模式下节约了磁盘空间并提升安装速度在这里只是小试牛刀,更多的特性还没有体现出来,需要后续跟进学习项目的拆分和搭建没有特别的约定要做的一模一样,符合实际情况的考虑就是最优。

关注我们了解更多MISBoot低代码开发平台的相关信息,长按识别二维码添加客服微信获取MISBoot低代码开发平台相关资料!

免责声明:本站所有信息均搜集自互联网,并不代表本站观点,本站不对其真实合法性负责。如有信息侵犯了您的权益,请告知,本站将立刻处理。联系QQ:1640731186