京东慧采 App 是企业专属移动采购平台,依托京东移动技术实现企业全采购场景移动化,为企业客户打造零研发成本的多场景一站式移动化智能采购平台。帮助企业实现采购模式的革新,加速企业数字化采购的发展进程,使企业采购变得更为阳光、高效、简单。目前覆盖的行业包含金融、运营商、大交通、能源、电网、烟草等。
帮助企业解决两大采购场景难题javascript
针对以上状况,咱们开发了线上协同采购需求,应用它就能够完全轻松解决这些难题,提升客户的采购效率。下面是咱们需求的大体流程:(分为提报人和采购人)css
以及在其中涉及到的部分页面:html
在明确了需求以后,咱们就开始正式的项目开发了。首先在框架选择上,咱们采用 Vue ;其次,在组件库方面,咱们采用团队自主研发的一套京东风格的移动端组件库 NutUI。前端
基于 Vue 的 UI 组件库,咱们选择了部门自主研发的开源组件库 NutUI。NutUI 是一套京东风格的移动端组件库,开发和服务于移动 Web 界面的企业级前中后台产品。2.0+ 更是在 1.0+ 的基础上作了全新的架构升级,组件的数量和项目覆盖率上也有了质的飞跃。在本次项目中,咱们也亲身体验到了高质量组件给开发者带来的便捷( Dialog、TimeLine、Infiniteloading、Stepper、Popup、Toast、Address )。vue
这里也特别感谢组件owner 小璐 童鞋,在咱们开发需求的同时,开发地址组件,不只没有耽误整个项目的进度,并且接入项目的过程也很顺利,组件堪称完美,点赞 666~~~java
this.$dialog({ title: "是否肯定提交", content: "采购人将第一时间看到您的提报,在下单以前可撤回从新修改" });
<nut-dialog title="清单Excel将发送至如下邮箱"> <input type="text" placeholder="请输入邮箱地址" class="inputemail"/> </nut-dialog>
标签式写法在使用时有一个遮罩层的小问题,已反馈开发者进行修复
在项目中,咱们采用的函数式写法,而且 content 里面传递的是 Html 标签。webpack
_this.$dialog({ title: "清单Excel将发送至如下邮箱", content: "<input type=\"text\" placeholder=\"请输入邮箱地址\" class=\"inputemail\"/>", });
这样使用没有问题,页面能够正常展现,有一个不太好的地方就是,在获取 input 元素的值时,不能使用 Vue 的实例,而是采用了 DOM 操做ios
let email = (document.querySelector('.inputemail') as any).value
这里,建议作一下组件优化,可使用 Vue 的实例获取内嵌的 DOM 的内容
在 API 里面定义了一系列方法,add、reduce、change、focus、blur 等。咱们能够在实际的业务场景中监听这些事件来实现不一样的逻辑。另外还支持简单的动画效果。以及里面为咱们处理了许多有关 number 的优化和逻辑处理。大大减小了咱们开发的成本。美中不足的地方有一处:git
咱们在加减数量时,有的场景下须要异步通知是否须要正常加减,而在组件中,只是同步的进行了加减的操做,没有跟接口有直接的关系,建议能够同时支持同步和异步操做供开发者选择。
Gaea:Gaea 构建工具是基于 Node.js、Webpack 模版工程等的 Vue 技术栈的整套解决方案,包含了开发、调试、打包上线完整的工做流程。Gaea 的全新升级改版,大大提高了项目构建速度,提升了咱们的开发运行效率。github
TypeScript + Vue + Vuex
TypeScript 始于 JavaScript,归于 JavaScript。它能够编译出纯净、 简洁的 JavaScript 代码,而且能够运行在任何浏览器上、Node.js 环境中和任何支持 ECMAScript 3(或更高版本)的 JavaScript 引擎中,它还具有如下特色: (1)静态类型化是一种功能,能够在开发人员编写脚本时检测错误; (2)适用于大型的开发项目; (3)类型安全是一种在编码期间检测错误的功能,而不是在编译项目时检测错误。这为开发团队建立了一个更高效的编码和调试过程; (4)干净的 ECMAScript 6 代码,自动完成和动态输入等因素有助于提升开发人员的工做效率; 选择了使用 TypeScript,而后接着就须要结合咱们本次项目选用的 Vue 技术栈来配合使用。
众所周知,Vue2.0+ 对 TS 的支持远远不如 React ,在 React 中, jsx 里面的类型提示应有尽有,能够大大提升开发效率,减小 TS 相关的不少 bug,Vue 里面虽然也支持 jsx ,可是 2.0+ 的官方仍是推荐使用模版 Template 渲染,这样就失去了 TS 的强大提示功能。固然,若是必定要使用的话,也不是不能够,在项目中咱们配合 vue-property-decorator 就可使用了。这个是 TS 官网给出的,它就是一个装饰器,利用它就能够将 Vue 和 TypeScript 结合起来使用。若是要深刻了解它的实现原理,能够参考咱们的另外一篇文章运用 NutUI - 快捷开发企业业务之酷兜 装饰器源码分析篇,里面深刻剖析了它的实现,感兴趣的童鞋能够研究研究~~
import { Vue, Component, Prop } from 'vue-property-decorator' @Component({ components: { } }) export default class ReportItem extends Vue { @Prop({ type: Object, required: true, default: {} }) itemData!: object }
固然,咱们在项目中也使用到了 Vuex ,来存储一些 State 状态值。那么咱们怎么使 Vuex 和 TypeScript 结合呢?那咱们须要借助 Vuex 的装饰器 vuex-class ,
import { createDecorator } from 'vue-class-component'; import { mapState, mapGetters, mapActions, mapMutations } from 'vuex'; export var State = createBindingHelper('computed', mapState); function createBindingHelper(bindTo, mapFn) { function makeDecorator(map, namespace) { return createDecorator(function (componentOptions, key) { if (!componentOptions[bindTo]) { componentOptions[bindTo] = {}; } var mapObject = (_a = {}, _a[key] = map, _a); componentOptions[bindTo][key] = namespace !== undefined ? mapFn(namespace, mapObject)[key] : mapFn(mapObject)[key]; var _a; }); } function helper(a, b) { if (typeof b === 'string') { var key = b; var proto = a; return makeDecorator(key, undefined)(proto, key); } var namespace = extractNamespace(b); var type = a; return makeDecorator(type, namespace); } return helper; }
其中,createBindingHelper 就是核心处理函数,它的原理和 vue-property-decorator 的实现思路是同样的,这里不作过多解释。固然,咱们在实际项目中使用也是很是简单了。
import { State, Mutation } from 'vuex-class' export default class ReportList extends Vue { @State scrollTop @Mutation saveTop }
掌握了 TypeScript 和 Vue、Vuex 的结合使用,咱们就能够在项目中大展拳脚啦~~
做为前端开发,咱们不得不打交道的就是后端接口了,不管传统开发 jQuery、Vue、React 都离不开对接口请求的封装,虽然它们实现的底层大部分都是基于 XMLHttpRequest or JSONP,但在开发者使用层面,倒是出现了各类不一样的封装库。本项目使用的 Vue 技术栈,与 Vue 结合使用的网络请求有几种:
它是 Vue.js 的一款插件,能够经过 XMLHttpRequest 或者 JSONP 发起请求并处理响应。它的特色:
然而,咱们在现阶段不会去用它,很大的一个缘由是 Vue2.0+ 不会去同步更新了,而是推荐使用 Axios 。它是基于 Promise 的 HTTP 请求客户端,能够同时在浏览器和 Node.js 中使用。
Unlike routing and state-management, ajax is not a problem domain that requires deep integration with Vue core. A pure 3rd-party solution can solve the problem equally well in most cases. There are great 3rd party ajax libraries that solve the same problem, are more actively improved/maintained, and designed to be universal/isomorphic (works in both Node and Browsers, which is important for Vue 2.0 with its server-side rendering usage).
以上是 Vue.js 做者 Evan You 给出的咱们在使用 Vue2.0+ 开发时不推荐使用 vue-resource 的缘由,大体的意思是:与路由和状态管理不一样,ajax 并不须要和 Vue 核心深度集成。在大多数状况之下,纯第三方库彻底能够很好的解决问题;有不少优秀的第三方 ajax 库能够解决一样的问题,它们一直在更加积极的改进和维护,而且设计成了通用的库,在 Node 和浏览器环境均可以很好的使用,这对于 Vue2.0+ 支持的 SSR 渲染尤为重要。
既然做者尤大都不推荐使用了,咱们使用者也应该紧跟做者脚步,放弃它!!!
fetch API Fetch 是一个现代的概念,等同于 XMLHttpRequest ,它提供了许多和 XMLHttpRequest 相同的功能。它提供的新的 API 更增强大和灵活。Fetch 的核心在于对 HTTP 接口的抽象,包括 Request、Response、Headers、Body 以及用于初始化异步请求的 global fetch。fetch(input,[, init]),其中, input 定义要获取的资源;init 是可选项,一个配置项对象,包括全部对请求的设置(method、headers、body等)。一个简单的 fetch 请求的使用以下:
const response = await fetch(reportTab, { credentials: 'include', method: 'get', cache: "force-cache" }); const data = await response.json()
以上经过一次 fetch 的简单调用,就打印出了 data。看起来挺简单,那咱们在项目中为何不使用它呢?
基于以上几点,咱们仍是选择不在本次的项目中使用~~
本项目,咱们仍是使用了 Vue 官方推荐的 axios 库。它的好处我在这里就不一一列举了。相信你们都有体会和使用的经验。
通常咱们在安装完成以后都会在本身的项目中封装一层,而后再在具体的模块中调用:
var instance = axios.create({ baseURL: "", timeout: 10000 }); instance.interceptors.request.use( return ... ); instance.interceptors.response.use( return ... ); export default function(method,url,data) { return instance[method]()... }
通常使用以上的封装或者在此基础上作必定的扩展就足以应对整个项目的请求了。 咱们在项目中并无这么作,固然上面的封装放在本项目中彻底没有问题。但,咱们项目中使用的 vue + ts,上面的封装彻底没有体现出 ts 的做用。既然要使用,那就从底层的封装开始。
首先,定义两个接口,一个是请求时的入参,一个是接口返回数据结构
export interface ReqOptions { uri?: string; query?: object | null; data?: { [key: string]: any; }; } export interface ResOptions { code: number | string; message: string; data: { [key: string] : any } }
而后,将其引入 request.ts 文件中,在 request.ts 中,咱们定义了一个 Request 类。
static instance: Request request: AxiosInstance cancel: Canceler | null methods = ['get', 'post'] curPath: string = '' constructor(options: AxiosRequestConfig) { this.request = axios.create(options) this.cancel = null this.curPath = options.baseURL || '' this.methods.forEach(method => { this[method] = (params: ReqOptions) => this.getRequest(method, params) }) this.initInterceptors()//初始化拦截器 }
在 constructor 中,建立了 axios 实例,定义了请求方法 get 、post ,并初始化拦截器 initInterceptors。其中 AxiosInstance
, Canceler
, AxiosRequestConfig
等这些都是 axios 这个库中支持 ts 定义的接口。它们都是定义在 axios/index.d.ts 下
export interface AxiosRequestConfig { url?: string; method?: Method; baseURL?: string; transformRequest?: AxiosTransformer | AxiosTransformer[]; ... }
定义 拦截器、初始化请求实例、请求方法:
initInterceptors() { this.request.interceptors.request.use((config: AxiosRequestConfig) => { ... return config }) this.request.interceptors.response.use( (res: AxiosResponse<any>) => { ... return res }) }
static getInstance(options = defaultOptions) {//初始化实例 if (!this.instance) { this.instance = new Request(options) } return this.instance }
async getRequest(method: string, options: ReqOptions = { uri: '', query: null, data: {} }): Promise<any> { ... if(method === 'get') { response = await this.request[method](url, { params: query }) } else if (method === 'post') { response = await this.request[method](https://coding.jd.com/fe-rd2/article/blob/master/2020-Q2%2F%E3%80%8A%E8%BF%90%E7%94%A8NutUI-%E5%BF%AB%E6%8D%B7%E5%BC%80%E5%8F%91%E6%85%A7%E9%87%87%E5%8D%8F%E5%90%8C%E9%87%87%E8%B4%AD%E3%80%8B-%E8%8B%8F%E5%AD%90%E5%88%9A%2Furl%2C params) } ... }
其中, getInstance 静态方法采用单例模式生成请求实例;getRequest 中具体定义了请求的方法,并返回 response。 最后导出这个实例
export let api = Request.getInstance()
const res = await this.$api.get({ uri: reportTab })
固然,在这里,this.$api 咱们须要进行类型声明。在项目中建立 shime-global.d.ts 文件
import Vue from 'vue' import VueRouter from 'vue-router'; import { Route } from 'vue-router'; declare module 'vue/types/vue' { interface Vue { $router: VueRouter $route: Route $api: any $toast: any $dialog: any } }
这样,就会顺利经过 TS 的编译,而且能够直接使用 this.$api.get 了
固然,axios 当然好用,功能强大而全面,一个 axios.js 大约在 46KB 左右,压缩的也在 14KB 左右。若是咱们在实际开发中,只是用到了一些基础的 API 功能,好比 get、post、取消请求、错误捕获等。咱们也能够考虑本身去基于 XMLHttpRequest 封装一个针对本身项目的请求接口的函数,而没有必要依赖第三方库。
项目中涉及到的不少是 sku 列表页,购物车页,详情页中也有下单 sku 的列表,咱们在加载的时候虽说是分页加载,可是不免会有网络异常或者不稳定的状况发生,为了给用户以更好的视觉体验,咱们给项目中的图片增长了懒加载的功能,咱们会采用一张默认的图片先展现并占位,网络请求图片成功以后,再换成实际的图片,这里须要一个 Vue 的指令,固然咱们能够自定义一个懒加载的指令:
Vue.directive('lazyload', { ... });
自定义指令包含 5 个 生命周期:bind 、 inserted、update 、componentUpdate 、unbind 。
咱们只须要实现这几个生命周期函数便可~~
为了方便,咱们在项目中使用了 Vue 懒加载指令 Vue-lazyload,咱们只须要在项目中安装,在入口文件中初始化,而后作一些配置就可使用了。
import VueLazyload from 'vue-lazyload' Vue.use(VueLazyload, { error: require('./asset/img/collpro/default.png'), loading: require('./asset/img/collpro/default.png') })
这里指定了加载的默认图片,而后在项目中使用 v-lazy
<img v-lazy="item.skuImgUrl" />
在页面数据返回以前呈现给用户的一个页面的轮廓,比起以前经常使用的 Loading ,在视觉效果上明显提高了不少,咱们在项目中也用了这个提高手段,考虑到是单页面应用,若是在页面上直接使用,会致使骨架屏和实际的页面截然不同。因此,咱们在几个重要的路由页面中单独使用了骨架屏,这样让用户看起来更加真实一些。在 comopnents/
下建立一个骨架屏组件 Skeleton ,分别对不一样路由页面书写不一样的布局结构,经过 Props page 去识别。
<div class="skeleton-content skulist" v-if="page === 'skulist'"> <div class="list-item" v-for="item in new Array(5)" v-bind:key="item"> <div class="left"></div> <div class="right"> ... </div> </div> </div>
<Skeleton v-if="initSkeleton" page="skulist"></Skeleton>
一般,咱们把项目开发完成,使用 Webpack 进行打包构建时,一般会打包出一个 app.js 文件,和一个 app.css 文件,把这两个文件引入对应的 Html 文件,固然能够正常去访问咱们的应用。看似没什么问题。可是在比较大型的项目中,打包出的 app.js 文件一般是很大的,就拿咱们本项目来讲吧。
若是咱们能把不一样路由对应的组件分割成不一样的代码块,而后当路由被访问的时候才加载对应组件,这样就更加高效了。咱们可使用动态 import 来定义代码分块点
const report = () => import("./../../view/collpro/C/report/reportlist.vue");
这样,咱们再结合 Webpack 就实现了组件的异步加载功能,减小了静态资源大小,提高了页面加载速度。
一般咱们使用 Vue/React 技术栈开发的项目都是 SPA 应用,可是在一些比较大型的项目或者一些业务场景特殊的项目中,SPA 已经不能知足咱们的需求了,这时候须要基于咱们的打包工具 Webpack 进行多页面打包的支持。本次项目涉及B(采购人)、C(提报人)两个角色,并且两个的请求域名和入参都有区别,因此咱们考虑采用多页面来支持。 对于多页面的配置,相信有些童鞋仍是比较陌生,由于在项目中不多用到,故在这里说明一下几个主要的配置项:
entry: { app: './src/collpro/B/app.ts' }
entry: { b: './src/collpro/B/app.ts', c: './src/collpro/C/app.ts' },
若是是多页面,采用上面第二种写法,这个也是本次项目中入口的配置。
new HtmlWebpackPlugin({ template:'./src/index.html', filename: path.resolve(__dirname, './../build/b.html'), inject: true, chunks: ['b'] }), new HtmlWebpackPlugin({ template:'./src/index.html', filename: path.resolve(__dirname, './../build/c.html'), inject: true, chunks: ['c'] })
其中,须要注意的是里面的参数 chunks ,它指的是容许你添加的模块,也就是这个页面中须要引入的 js 模块,若是这里不指定,它将会默认将全部打包出来的模块都加载进来,咱们来看一下效果:
这个是我没有指定 chunks 打包出来的静态资源引用,很明显是不对的~~
到此,多页面打包的配置就已经修改完成了,咱们能够愉快的进行项目的开发了。可是,在开发时会发现,在修改某一个文件时,会执行两次 build ,咱们在插件 emit(输出资源) 钩子中打印当前时间,而后随意修改逻辑代码:
能够看到,每一个入口文件都执行了一遍,这样,大大消耗了构建时间,咱们的指望是修改了哪一个页面,对应就打包哪一个页面就好,这样会大大提高构建效率,体现 HMR 的价值。咱们须要稍微对这个插件作一些修改,增长 muticache 参数,而后在 emit 中增长:
if (self.options.muticache && isValidChildCompilation) { return callback(); } ...
isValidChildCompilation
须要在 done 钩子中设置 true,这样才能保证在多页面状况下,修改某处代码只编译一次。
先来看一下须要实现的效果:
需求描述以下:
也就是说,提报单列表并非一直不刷新,而是会根据不一样路由的来源,作是否须要刷新的判断。这里固然会用到 Vue 中的 keep-alive,但仅仅使用它是不能知足需求的~~ 下面来一点点分析:
{ path: `${baseUrl}/reportlist`, component: report, meta:{title: '采购单提报', keepAlive: true} },
而后,在 app.vue 中,引入 keep-alive 组件
<keep-alive> <router-view v-cloak v-if="$route.meta.keepAlive"></router-view> </keep-alive>
这样仅仅是缓存了当前组件,那么怎样去记录上一次的位置呢?我在项目中是这么作的。
let top = document.documentElement.scrollTop this.saveScrollTop(top)
获取 top
,而且经过 saveScrollTop
方法将其存储在 store 中。而后再次访问的时候让其回到 top
位置
activated() { if(this.$route.meta.keepAlive) { document.documentElement.scrollTop = this.scrollTop } }
注意:只有当组件在 keep-alive 内被切换,才会有 activated 和 deactivated 这两个钩子函数。
这样,上面的需求描述一就知足了,那么需求二又该如何实现呢?
vue-router 为咱们提供的导航守卫主要用来经过跳转或取消的方式守卫导航。导航守卫分为三种:全局的、单个路由独享的、组件级的。 在这里,咱们只须要在全局作就能够了。
router.beforeEach(function(to, from, next){ if(...) { to.meta.keepAlive = true } else { to.meta.keepAlive = false } next(); });
beforeEach 注册了一个全局前置守卫,from 表示导航正要离开的路由,咱们就是利用这个 from 来动态设置 keepAlive 的值。 由此,咱们同时使用了 keep-alive 和 vue-router 的导航守卫知足了以上的需求~
在低版本的 ios 部分手机会存在一个兼容性问题,应该是属于内部机制致使。在点击 input 获取焦点后,键盘会自动弹起,将页面顶起,当输入完成后点击‘完成’按钮,键盘自动收起,可是页面没有回滚,致使点击元素还停留在键盘弹起的地方。
解决的办法就是咱们须要在 app.vue 中,在 mounted 钩子里面监听 focusout 事件,手动将页面滚动到初始位置。
document.body.addEventListener("focusout", () => { window.scrollTo({ top: 0, left: 0, behavior: "smooth" }); });
本次需求中还有一个比较常见的动画效果,在页面滑动过程当中顶部固态栏渐变。由这样↓
变成这样↓
先来捋一遍实现的思路: 这种渐变功能的实现,宽度等属性比较简单,好比 input 框的宽度,直接改变宽度值就能够了。颜色变化须要考虑的比较多:从透明到不透明,从白色到其余颜色,均可以经过控制透明度实现;颜色由白色渐变成其余的颜色,略微复杂,这样的渐变咱们能够白色的打底,其余颜色做为上层,改变上层透明度来实现。 咱们这个效果,要从红色变成白色,两个方向: 一、红色 rgba(255,0,0) ==> 白色 rgba(255,255,255)。直接渐变色值,可想而知,滑动过程确定颜色变化过多,太花,放弃! 二、不能白色打底,只能改变透明度了,能够先尝试一下看看效果。要实现这个功能,首先要监听页面的滚动:
mounted() { //首先,在mounted钩子window添加一个滚动滚动监听事件 window.addEventListener("scroll", this.handleScroll); }, //因为是在整个window中添加的事件,因此要在页面离开时摧毁掉 beforeDestroy() { window.removeEventListener("scroll", this.handleScroll); }
而后就是重点定义头部上滑事件:
const handleScroll = (that:any): void => { let _this = that; let scrollTop =window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop; let visibleDomHeight = _this.$refs.srollinfo.offsetHeight;//获取卡片得高度 }
咱们先获取页面上滑的高度,以及咱们想要让固定栏渐变完成的高度,好比咱们这个项目就须要它滑过卡片的时候渐变完成。 接下来经过滚动函数改变须要渐变的元素,咱们这个需求须要改变的元素属性比较多,咱们拿背景颜色举例:
if(scrollTop>0){ //定义固定栏头部背景 let opcity = scrollTop/visibleDomHeight <=1 ? (1-scrollTop/visibleDomHeight) : 0; _this.bgColor=`linear-gradient(270deg, rgba(250,151,97,${opcity}) 0%,rgba(247,39,28,${opcity}) 100%)`; }
scrollTop 是页面监听到到组件的滚动位置,当组件滚动的时候,scrollTop 的值就会改变,opacity 就会变,背景就会从透明度 1 变成 0 .
其实全部须要渐变的属性,均可以经过这种方式实现。如下是全部效果实现后的效果:
能够看到,效果是实现了,但总有点奇怪的感受:滑动过程,固态栏透明度变小的时候跟底层的字体重复了,不太好看
最后,通过与产品沟通,咱们选用了最干净简洁的方式:在滑动到必定高度的时候直接改变固态栏的样子,input 框根据页面不一样展现或者不展现。以下:
if(scrollTop>visibleDomHeight){ //定义固定栏头部背景 _this.bgColor="#fff"; }
更干净清爽一些,毕竟适合的才是最好的,至此,这个滑动效果就完成了~~
到这里,文章立刻接近尾声了,但咱们对项目的持续优化以及对技术的热情还远远没有结束。不管是项目技术选型、组件开发、难题攻克仍是性能优化,咱们的路还很长,但咱们需谨记,不管路有多长,咱们只能并且必须一步一个脚印,脚踏实地,在作好项目的同时,作好每个沉淀,日积月累,提高技术水平,而后服务好每个项目/需求。