NutUI 3.0 助力企业业务售后门户建设
项目背景
退换/售后作为企业采购业务中不可或缺的一项流程,供企业业务各交易通路使用。为降低企业业务售后的研发成本,对售后相关数据及操作进行统一迭代和维护。我们打造了一套京东企业业务售后 SaaS 系统,业务涉及范围包含:申请售后,特色业务介绍,售后政策,报表平台。
企业售后门户 SaaS 化的平台建设,可实现以下核心功能:
文章图片
先来视觉效果上感受一下售后 SaaS 平台~
文章图片
初窥 SaaS 化平台后,除了满足退换/售后,选择售后类型等基础业务功能外,再来看下此次开发尝鲜的技术栈,以及所落地业务场景的新实现~
Vue 3.0 及其周边生态
2021 新年伊始,尤大就在知乎问答 2021 前端新变化中回复:''会有很多人抛弃 Webpack 开始用 Vite "。
随着 Vue 3.0
正式发布,其技术栈的周边生态逐渐完善,如路由插件 Vue-Router
,状态管理插件 Vuex
,以及紧随生态建设步伐的 NutUI 3.0
版本。借此机会,我们此次开发采用技术栈为:Vue 3.0 + Vuex4 + Vue-Router4.x + Vite2.2.x + NutUI 3.0 + TS,通过与 Vue3.0 完美结合,开启了新的开发体验~
其中, Vite 使用原生 ESM 文件,基于 ECMAScript
标准原生模块系统(ES Modules)实现,开发环境中无需打包;Vue 3.0 + Vite
这一开发模式构建速度比传统开发提升了 30 倍,兼具更轻、更快的 Web 应用开发工具性能;开发模式由 Option api
升级为 Vue3.0 带来的 Compisition api
,函数式编程更友好划分功能模块开发。
文章图片
全新技术栈的发布,项目基础架构的升级,区别于普通的售后模块 SaaS 化售后平台的其他特色业务特征包括:
文章图片
先来体验一波平台特色功能的实现~
特色业务开发
支持交易平台嵌入使用
京东慧采是为企业级客户打造的研发零投入的专属采购平台,旗下包括 (小程序/H5/APP)三端不同的落地场景。其中,我们使用 Taro 技术栈同时开发微信小程序和 H5,实现相同业务场景下两端落地需求。中心化、SaaS 化的售后模块,将实现一个企业业务移动端为所有商城类提供售后服务的业务赋能。
慧采移动端(小程序/H5/APP)将率先接入企业售后门户:
文章图片
在京东慧采个人中心页面,点击【退换/售后】即可进入第三方接入的售后门户,从操作上来讲一个点击跳转即可完成,那么在逻辑实现上需要做些什么操作?
文章图片
第三方平台的接入需要打通登陆态处理,在 H5/App 中使用 Cookie
来实现,而小程序的运行环境与浏览器不同,无法通过同源策略保证安全,在小程序请求接口不会自动带 Cookie
,那么怎样同时兼容两者?
H5/APP 登录态的打通
我们都知道,H5 跳转第三方页面,打开一个 iframe
标签就可以实现:
但是要实现不同账户切换下的数据连通,还需要进行登录态的打通处理。
慧采:a.jd.com
售后: b.jd.com
二者的域名不完全相同,
Cookie
是不可以跨域名的,正常情况下,同一个一级域名下的两个二级域名也不能交互使用 Cookie
。比如:如果想要跳转到售后的
b.jd.com
名下的二级域名都可以使用慧采的 Cookie
信息,需要设置 Cookie
的 domain
参数为 .jd.com
,这样使用 a.jd.com
和 b.jd.com
就能访问同一个 Cookie
了。document.cookie = "testCookie=hello;
domain=.jd.com;
path=/";
小程序登录态的特殊处理
小程序中第三方页面的跳转依赖
WebView
标签实现:
那么问题来了,小程序运行环境不是浏览器,在 H5 中登陆态使用的
Cookie
去实现登陆态,Cookie
是通过 请求的 header
带上去的,而在小程序中它的做法是取消了自动携带的 Cookie
,需要手动设置,并且没有了浏览器的同源策略限制。来看下我们的解决方式,慧采页面点击【退换/售后】前,拿到用户的
pt_key
字段进行加密后,拼接在 url
上,传递到【售后】模块,用作打通登录态的必需信息:tmpUrl = this.afterSaleUrl + `&pt_key=${pt_key}`;
Utils.openWebView(tmpUrl);
售后模块页面,获取到对应字段后,将
Cookie
种在 jd
域名下,这样的话就可以实现 Cookie
共享了~//加入cookie
const pt_key = this.queryString("pt_key") || "";
if (pt_key) {
document.cookie = `pt_key=${encodeURIComponent(
pt_key
)};
domain=.jd.com;
path=/`;
}
可定制化配置页头 作为一个 SaaS 化平台,应对不同的业务场景总会有不同的客户接入需求,就拿标题栏来讲就有多种需求:
- 隐藏/展示【售后】平台自带标题栏;
- 使用原场景自带的标题栏,但是读取【售后】标题栏信息;
- 自定义【售后】标题栏信息
- ...
文章图片
以上 H5 页面中,采用的是慧采的标题栏,读取【售后】的标题栏信息,那这是如何实现的呢?
同样是获取不同域名下提供的页面服务,简单的
document.title
, 已经不能满足我们的需求了,直觉想到的实现方式就是使用 iframe
,此时,我们可以采用 postMessage
方法来解决 iframe
直接的交互同样存在的跨域问题。postMessage 是挂载在 window 下的一个方法,用于不同域名下的两个页面的信息交互,来看下实现过程:
父子页面通过 postMessage()发送消息,再通过监听 message 事件接收信息。
售后页面(子页面)给慧采页面(父页面)传递 title 消息:
window.parent.postMessage({ title }, "*");
慧采页面监听 message 事件,子页面发送来的消息内容在
event.data
属性中:_customEvent: () => {};
customEvent(event) {
this.setState({title: event.detail});
document.title = event.detail;
}
componentDidMount() {
window.addEventListener('vsptitle', this._customEvent);
}
响应式样式,一键主题定制~ 本次开发的售后 SaaS 平台,针对不同的接入交易平台,为保持与接入平台的 UI 设计风格保持一致,我们实现了定制化配置主题功能,根据传入的主题颜色,实时渲染效果。
文章图片
相较于传统模式只能多次构建实现,我们利用了 Vue3.0 的
v-bind:css
新特性实现主题定制实时切换。我们在开发前期,也提前将
sass
变量提取了出来://主题色
$primary-color: #f0250f;
//渐变
$gradient-color: linear-gradient(
135deg,
rgba(240, 22, 14, 1) 0%,
rgba(240, 37, 14, 1) 70%,
rgba(240, 78, 14, 1) 100%
);
//被选中的淡色背景
$bg-color-selected: #fef4f3;
//不能点击时背景
$bg-color-disabled: #fcd4cf;
//背景渐变
$bg-gradient-color: $gradient-color;
//白色背景
$bg-color-white: #fff;
//边框颜色
$border-color: #ececec;
这样,在重构页面时就可以直接写变量了,也便于后期的样式维护。但是这样,还是不能满足我们的需求。
可以先了解一下 vue3 sfc),支持在单文件 style 中绑定响应式样式:
.head{ color: v-bind('theme.color') }
,直接在 css 上面绑定响应数据。通过这个特性,我们可以在初始化时,拿到用户自定义的主题色,然后赋值一个
css
变量,然后去动态修改主题~const colorState = reactive({
startColor: '#f0250f',
endColor: 'linear-gradient(135deg,rgba(240, 22, 14, 1) 0%,rgba(240, 78, 14, 1) 100%'
});
router.isReady().then(async () => {
...
const result = await peelService.getPeelInfo(peelState.peelDto);
if (result?.state === 0) {
colorState.startColor = result.value.originalColor;
colorState.endColor = result.value.terminalColor;
}
});
.primary-color {
color: v-bind(startColor);
}
.gradient-color {
background-color: linear-gradient(135deg, v-bind(startColor) 0%, v-bind(endColor) 100%);
}
这样,我们在写样式时,就可以直接使用
.primary-color
、.gradient-color
这样的类名了~NutUI 3.0 组件赋能业务新形态 京东慧采等多企业采购平台延续京东 App10.0 的设计风格,这一设计原则和 NutUI 3.0 不谋而合。NutUI 3.0 组件库,从视觉设计到技术支持为项目落地带来了新的开发体验~
技术看点
- 引入 Vue3.0 新特性
- 破坏性变更,全面升级
- 采用组合式 API Composition 语法重构,结构清晰,功能模块化
- 组件 emits 事件单独提取,增强代码可读性
- 使用 Teleport 新特性重构挂载类组件
- 构建工具升级为 Vite2.x
- 全面使用 TypeScipt
文章图片
其中,每个组件覆盖多页面多使用场景,以
Pupop
、Toast
、Switch
组件为例,看下具体在售后平台页面中的具体使用场景:文章图片
接下来,我们再以这三个组件为例,结合在页面中的使用场景,看下与 Vue 2.x 相比其内部实现原理:
Popup 挂载节点新实现
Popup
组件中有一个非常好用的功能:自行选择挂载节点,可以将组件挂到任意 DOM 节点下面。NutUI2.x 中,通过
get-container
属性指定挂载位置:
我们先来看看 NutUI 2.x 版本中挂载节点的核心实现,可以看到,基本的思路是获取到
props
中的 getContainer
,然后将当前的组件 append
进去。portal() {
const { getContainer } = this;
const el = this.$el;
let container;
if (getContainer) {
container = this.getElement(getContainer);
} else {
return;
}if (container && container !== el.parentNode) {
container.appendChild(el);
}}
}
那么,在 Vue3.0 版本中,我们是如何实现的呢?
其实非常简单,Vue 3.0 它为我们提供了一个
Teleport
,它允许我们控制当前在哪个父级下渲染了 DOM
,而不必像 2.x 中那么繁琐。
...
我们在使用的时候直接通过
props
去传递一个 selector
,然后就可以将组件挂载到任何节点下了。是不是特别方便~
app
Toast 函数式调用
Toast
的轻提示作用,在页面中的使用频率很高:先看下在 Vue2 的使用方式:
Vue.use(Toast);
// 调用
this.$toast.text("请填写收货人姓名");
在 Vue2.0 中,组件内部实现原理:
Vue.prototype["$toast"] = ToastFunction;
Vue3.0,在页面中使用方式:
app.use(Toast);
import { getCurrentInstance } from 'vue';
setup(){
const { proxy } = getCurrentInstance();
proxy.$toast.text('请填写收货人姓名');
}
先来看下,挂载全局变量看下 Vue 3.0 挂载全局变量的用法,在
main.ts
中引入全局要使用的方法,通过 app.config.globalProperties
添加到全局。在
appContext.provides
中注入了一个 Store 实例对象,每个组件都可以使用 this.$store.xxx
访问 Vuex 中的方法和属性,这时相当于根组件实例和 config
全局配置 globalProperties
中有了 Store
实例对象。3.0 组件库中,关于 Toast 内部实现原理:
//$toast 添加在全局,在 mian.js 中声明:
app.config.globalProperties.$toast = ToastFunction;
Switch 异步控制 在 Vue3.0 中新增异步控制逻辑,其实现原理如下:
利用 Vue3
v-model
和 :model-value
特性进行实现。在使用 v-model
时,代码内部执行 emit('update:value',true);
时将自动同步更新外部传入的变量值。 ;
import { ref } from "vue";
export default {
setup() {
const checked = ref(true);
return { checked };
},
};
相反我们只需要采用
:model-value
进行单向数据下发,进行外部异步控制内部状态值即可: import { ref, getCurrentInstance } from 'vue';
export default {
setup() {
let { proxy } = getCurrentInstance() as any;
const checkedAsync = ref(true);
const changeAsync = (value: boolean, event: Event) => {
proxy.$toast.text(`2秒后异步触发 ${value}`);
setTimeout(() => {
checkedAsync.value = https://www.it610.com/article/value;
}, 2000);
};
return {
checkedAsync,
changeAsync
};
}
};
除了 NutUI 3.0 赋能的业务场景外,项目基础构建层还包括以下几点:
文章图片
以数据管理和路由配置/方法封装两方面,来看下结合 Vue3.0 的使用,在企业业务中实现方式:
Vuex4 如何优雅地进行状态管理 针对售后列表中某一具体的商品来讲,后续【选择售后类型】,【申请售后】等操作的数据项几乎相同,为避免频繁读取接口和频繁使用组件传餐的方式来同步数据,减少数据的管理和维护工作,我们采用将数据值定义在 Vuex 中供其他页面使用。
文章图片
此次开发我们采用最新版本的 Vuex4,那么基于 Vue2.x 风格的 Vuex 的使用方式是否还适用于 Vue3?
Vue2.x 中,为实现代码复用采取的
getter/action
方式,以及为不方便单独编写管理模块而设计的 module。Vue3 中已经补全了 Vue2 的短板,所以 Vuex 使用方式相应发生了变化。useStore 支持 ts 使用配置 injection key
Vue3 中 Vuex 的
useStore
具有完整的 state
和 modules
类型推测,对 ts
支持也得到了加强。可是 Vuex4 对 ts 的支持却没有任何改变,使用 useStore
的类型仍然为 any
,在此官方提供了解决方案:InjectionKey 注入类型
- 定义类型 InjectionKey。
- InjectionKey 在将商店安装到 Vue 应用程序时提供类型。
- 将类型传递 InjectionKey 给 useStore 方法。
// store.ts
import { InjectionKey } from "vue";
import { createStore, createLogger, Store } from "vuex";
//手动声明state类型
export interface State {
orderItem: any;
customerExpectResultList: any;
orderSkuApplyItem: any;
//...
}
//定义注入类型
export const key: InjectionKey> = Symbol();
接下来,安装到 Vue 应用程序时传递定义的注入类型:
// main.ts
import { createApp } from "vue";
import { store, key } from "@/store";
const app = createApp(App);
//pass the injection key
app.use(store, key);
app.mount("#app");
最后,将密匙传递给
useStore
方法以检索类型化的存储。import { useStore } from 'vuex';
import { key } from '@/store';
const store = useStore(key);
onMounted(async () => {
state.provideName = await getProvideName(
store.state.orderItem.jdOrderId,
);
});
Vue 3.0 如何配置/管理页面路由 页面跳转实现依靠路由来实现,新版路由配置和之前非常相似,只有些许不同。新版本路由的
API
全部采用函数式引入的方式,配合 ts
的类型提示,让我们无需文档也能够完成配置。路由配置和 2.x 的方式保持一致:
export const routes: RouteRecordRaw[] = [
{
path: paths.saleindex,
name: "saleindex",
component: SaleIndex,
meta: {
title: "退换/售后",
keepAlive: true,
backurl: "",
style: {},
},
},//...{
path: "/:path(.*)+",
redirect: () => paths.saleindex,
},
];
此处由
new VueRouter
的方式修改为 createRoute
方式,其余无变化。路由模式的配置采用 API
调用的方式,不再是之前的字符串,此处采用的 hash
路由。import { createRouter, createWebHashHistory } from "vue-router";
import { routes } from "./routes";
const router = createRouter({
history: createWebHashHistory(),
routes,
});
export default router;
现在,一个 Vue3 的基础路由就配置完成了,接着在
main.ts
这个入口文件中插件的方式通过 Vue 引入就可以了~import { createApp } from "vue";
import App from "./App.vue";
import router from "@/router/router";
const app = createApp(App);
app.use(router);
app.mount("#app");
router 的封装
页面引入:
查询详情 export default defineComponent({
name: 'submitsuccess',
setup: () => {const { push } = useRouterHelper();
const goDetail = () => {
push(paths.saleindex, { type: 1 });
};
onMounted(async () => {});
return {
goDetail,
};
}
});
方法封装:
export const useRouterHelper = () => {
const router = useRouter();
const push = (path: T, params?: ParamsMap[K]) => {
router.push({
path,
query: params
});
};
return {
push
};
};
最后,聊聊 TS 的 Decorator ~ 我们在进行页面开发的时候,往往有这么一个需求,需要在合适的地方添加
loading
效果:估计之前大多数开发者做法是这样的:
public async request(url: string, method: string, params: any): Promise {
...
//loading start
Toast.loading();
const res = await axios(options as AxiosRequestConfig);
//loading end
Toast.hide();
return this.checkStatus(res);
} catch (error) {
return error;
}
}
我们在封装的统一请求函数中,添加页面
loading
效果,这样也能够满足需求。但是...就有产品、测试、业务揪着这个问题不放,我哪里哪里不需要 loading
。作为搬砖 的我们,不得不另找出路去解决这个烦人的问题,怎么办????? 想到了暴力解决法,也是比较麻烦和工作量大的办法,我们在请求函数中添加一个标识参数来控制是否添加
loading
,当然,这样可以解决上面奇葩的需求,有没有优雅一点的解决方案呢?关于 Decorator ~
首先,它是一种和类(class)相关的语法,了解
Java
,Python
的同学大概知道它主要是用来干嘛的。在我们的 ECMAScript
中也提到了这种语法,用来做同样的事情。基本的语法:@ decorator
有了它,上面的问题可以这样解决,增加 loading 装饰器,在需要 loading 的接口上方增加
@loading
:export class SaleIndexService {
...
/**
* @description:售后申请
* @param: OrderPageReqDTO 实体类
* **/
@loading()
findOrderPage(params: OrderPageReqDTO) {
return this.http.request('/api/afs/findOrderPage.do', 'post', params);
}/**
* @description:申请记录
* @param:ServicePageReqDTO实体类
*
* ***/
@loading()
findServicePage(params: ServicePageReqDTO) {
return this.http.request('/api/afs/find/service/page', 'post', params);
}...
}
上面我们定义了一个类,它可以在页面中初始化,然后调用相应的
API
,那么,我们可以定义这样一个loading
装饰器,在需要的请求接口方法上面添加,这样,就可以做到自定义加载 loading
的需求了,是不是看起来比刚才的实现高大上~//loading decoratorexport const loading = () => {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const method = descriptor.value;
descriptor.value = https://www.it610.com/article/async function (...args: any[]): Promise {
let res: any;
_Toast.loading("加载中");
try {
res = await method.apply(this, args);
} catch (error) {
throw error;
} finally {
_Toast.hide();
}
return res;
};
return descriptor;
};
};
结语 【NutUI 3.0 助力企业业务售后门户建设】售后 SaaS 平台这一套功能全面、用户体验良好的前端系统,不仅实现了对售后数据及操作进行统一管理和维护,完成售后系统能力输出的同时,再赋能到其它领域。随着 Vue 3.0 等技术栈应用的不断深入,其全新的特性会被逐一全面应用,NutUI 3.0 组件库随之也会得到更好的迭代和更新,为京东企业业务驱动项目开发增添了新的动力。此外,NutUI 也持续推出了小程序版本的适配,欢迎关注~本文旨在抽时间梳理一下本项目开发中的一些思考点,尽可能多共享一些经验总结,同时也能系统回顾和巩固自身。
推荐阅读
- NutUI 3.1 正式发布(开启多端开发之路)
- 1-2月热点(度目发布煤矿电子封条解决方案,AI助力生产安全,推进煤矿智能化建设)
- harmony|手把手教你移植openharmony3.0到stm32(liteos_m)
- hive|hive3.0惊天大bug发现,grouping()函数只能小写,大写直接报错,hive不是不区分大小写吗.
- 新品发布会|“子账号”功能全新上线,助力企业开发者多人协作
- 飞桨助力动车3C车载智能识别,为动车组运行保驾护航
- 每秒百万条信息查询 天翼云助力江苏核酸检测信息查询
- 疫情当下,使用在线文档,助力疫情远程办公!
- 档案管理系统平台(助力实现档案管理现代化)
- vue|vue3.0 typescript 创建项目,路由RouteConfig 报错 has no exported member ‘RouteConfig‘.ts