NutUI 落地实践-让组件库服务慧采协同采购业务

京东慧采 App 是企业专属移动采购平台,依托京东移动技术实现企业全采购场景移动化,为企业客户打造零研发成本的多场景一站式移动化智能采购平台。帮助企业实现采购模式的革新,加速企业数字化采购的发展进程,使企业采购变得更为阳光、高效、简单。目前覆盖的行业包含金融、运营商、大交通、能源、电网、烟草等。

帮助企业解决两大采购场景难题javascript

  • 移动端采购。适用于移动流动性办公、网络限制等工做场景,突破时空限制,支持多帐号体系,随时随地采购、审批等。
  • 员工福利/渠道奖励发放。支持员工福利商城、积分兑换、渠道奖励等 B2B2C 场景,支持APP统一登陆,活动集中展现,一键领取。

[](https://coding.jd.com/fe-rd2/...

  • 目前慧采客户进行集中采购中,经过 excel 表格、纸质填报等方式进行线下收集采购需求,致使集中采购前期工做重复性高、复杂繁琐;同时收集过程当中没法精确到 skuid 维度,采购人在选品加购过程不肯定性大,搜索选择难度大,需求和采买商品出入大。严重增长了客户及运营人员的工做量。
  • 目前慧采批量下单的功能(支持多地址下单),存在前期 excel 录入工做繁杂,经常须要运营人员协同,同时线下沟通确认成本大,可视化程度低。
  • 现有客户场景:①企业副食品供应场景;②企业平常集采场景等;③教育行业办公集采场景;④铁路移动集采场景等

[](https://coding.jd.com/fe-rd2/...

针对以上状况,咱们开发了线上协同采购需求,应用它就能够完全轻松解决这些难题,提升客户的采购效率。下面是咱们需求的大体流程:(分为提报人和采购人)css

以及在其中涉及到的部分页面:html

在明确了需求以后,咱们就开始正式的项目开发了。首先在框架选择上,咱们采用 Vue ;其次,在组件库方面,咱们采用团队自主研发的一套京东风格的移动端组件库 NutUI前端

[](https://coding.jd.com/fe-rd2/...

[](https://coding.jd.com/fe-rd2/... 组件库

基于 Vue 的 UI 组件库,咱们选择了部门自主研发的开源组件库 NutUI。NutUI 是一套京东风格的移动端组件库,开发和服务于移动 Web 界面的企业级前中后台产品。2.0+ 更是在 1.0+ 的基础上作了全新的架构升级,组件的数量和项目覆盖率上也有了质的飞跃。在本次项目中,咱们也亲身体验到了高质量组件给开发者带来的便捷( Dialog、TimeLine、Infiniteloading、Stepper、Popup、Toast、Address )。vue

  • Address:四级地址组件。咱们在作移动端购物类需求时,必然会有选择地址的交互,故咱们将此业务组件添加在咱们的 NutUI 组件库中。方便有相似需求的直接拿来使用,方便又快捷。

这里也特别感谢组件owner 小璐 童鞋,在咱们开发需求的同时,开发地址组件,不只没有耽误整个项目的进度,并且接入项目的过程也很顺利,组件堪称完美,点赞 666~~~java

  • Dialog:弹窗组件也是在项目中使用率较高的一个组件,咱们在项目中也是频繁的在使用它。组件的功能仍是很强大的。首先它支持标签式和函数式写法,还支持图片弹窗等功能。
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 的内容
  • Stepper:步进器,一般在购物车页加减数量使用。

在 API 里面定义了一系列方法,add、reduce、change、focus、blur 等。咱们能够在实际的业务场景中监听这些事件来实现不一样的逻辑。另外还支持简单的动画效果。以及里面为咱们处理了许多有关 number 的优化和逻辑处理。大大减小了咱们开发的成本。美中不足的地方有一处:git

咱们在加减数量时,有的场景下须要异步通知是否须要正常加减,而在组件中,只是同步的进行了加减的操做,没有跟接口有直接的关系,建议能够同时支持同步和异步操做供开发者选择。

[](https://coding.jd.com/fe-rd2/... Gaea

Gaea:Gaea 构建工具是基于 Node.js、Webpack 模版工程等的 Vue 技术栈的整套解决方案,包含了开发、调试、打包上线完整的工做流程。Gaea 的全新升级改版,大大提高了项目构建速度,提升了咱们的开发运行效率。github

  1. 新版的 Gaea 将 Webpack 升级到了 4.0+,而且将以前只有一个 webpack.config.js 配置文件进行了拆分,这样不一样的命令执行不一样的操做,看上去也清晰不少。而且在构建速度方便加入了 Happypack ,将文件解析任务分解成多个子进程并发执行。子进程处理完任务后再将结果发送给主进程。这样大大提高了构建速度,同时也加入了 progress-bar-webpack-plugin、webpack-build-notifier 等插件,使得在构建过程当中既能实时了解构建进度,又能在结束以后收到成功/失败的通知,还能够对构建以后的文件经过图文的方式进行分析,从而可以很清晰得看到每一个文件的占比。
  2. 在执行 npm run dev/upload/build 时支持打本地包,这种适合前端只支持重构工做,而后将 Html、Css 交付给研发,直接能够在本地打开 Html 页面看效果,而无需再配置 Host ,是否是很方便~~

[](https://coding.jd.com/fe-rd2/...

  • TypeScript + Vue + Vuex

    [](https://coding.jd.com/fe-rd2/... VS Javascript:

    TypeScript 始于 JavaScript,归于 JavaScript。它能够编译出纯净、 简洁的 JavaScript 代码,而且能够运行在任何浏览器上、Node.js 环境中和任何支持 ECMAScript 3(或更高版本)的 JavaScript 引擎中,它还具有如下特色: (1)静态类型化是一种功能,能够在开发人员编写脚本时检测错误; (2)适用于大型的开发项目; (3)类型安全是一种在编码期间检测错误的功能,而不是在编译项目时检测错误。这为开发团队建立了一个更高效的编码和调试过程; (4)干净的 ECMAScript 6 代码,自动完成和动态输入等因素有助于提升开发人员的工做效率; 选择了使用 TypeScript,而后接着就须要结合咱们本次项目选用的 Vue 技术栈来配合使用。

    [](https://coding.jd.com/fe-rd2/... + TypeScript

    众所周知,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
    }
    [](https://coding.jd.com/fe-rd2/... + TypeScript

    固然,咱们在项目中也使用到了 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 的结合使用,咱们就能够在项目中大展拳脚啦~~

[](https://coding.jd.com/fe-rd2/... axios

做为前端开发,咱们不得不打交道的就是后端接口了,不管传统开发 jQuery、Vue、React 都离不开对接口请求的封装,虽然它们实现的底层大部分都是基于 XMLHttpRequest or JSONP,但在开发者使用层面,倒是出现了各类不一样的封装库。本项目使用的 Vue 技术栈,与 Vue 结合使用的网络请求有几种:

  • vue-resource
  • axios
  • fetch
[](https://coding.jd.com/fe-rd2/...

它是 Vue.js 的一款插件,能够经过 XMLHttpRequest 或者 JSONP 发起请求并处理响应。它的特色:

  1. 体积小(压缩以后只有 12 KB);
  2. 支持主流的浏览器(支持 IE 9+ 浏览器);
  3. 支持 Promise API ;
  4. 支持拦截器(能够在发送前和发送后作一些处理)

然而,咱们在现阶段不会去用它,很大的一个缘由是 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 渲染尤为重要。

既然做者尤大都不推荐使用了,咱们使用者也应该紧跟做者脚步,放弃它!!!

[](https://coding.jd.com/fe-rd2/...

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。看起来挺简单,那咱们在项目中为何不使用它呢?

  1. 先来看看它的兼容性吧~~ 咱们若是在项目中使用 fetch ,那么在前期封装请求方法时须要考虑到兼容性问题,须要同时支持 fetch 和 ajax 两种方式
  2. fetch 对一些错误处理不敏感,它只是对网络请求报错,对一些非 200 状态码,如 400 ,500 等都会看成成功请求去处理,这个须要在咱们本身封装时作特殊处理
  3. 默认不会携带 cookie ,须要咱们手动添加配置项 credentials: ‘include’
  4. 不支持超时控制和取消请求处理,容易形成流量浪费,尤为对于移动端产品及其不友好
  5. 请求的进度不能实时监测到,这个 XHR 能够作到

基于以上几点,咱们仍是选择不在本次的项目中使用~~

[](https://coding.jd.com/fe-rd2/...

本项目,咱们仍是使用了 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 封装一个针对本身项目的请求接口的函数,而没有必要依赖第三方库。

[](https://coding.jd.com/fe-rd2/...

[](https://coding.jd.com/fe-rd2/...

项目中涉及到的不少是 sku 列表页,购物车页,详情页中也有下单 sku 的列表,咱们在加载的时候虽说是分页加载,可是不免会有网络异常或者不稳定的状况发生,为了给用户以更好的视觉体验,咱们给项目中的图片增长了懒加载的功能,咱们会采用一张默认的图片先展现并占位,网络请求图片成功以后,再换成实际的图片,这里须要一个 Vue 的指令,固然咱们能够自定义一个懒加载的指令:

Vue.directive('lazyload', {
    ...
});

自定义指令包含 5 个 生命周期:bind 、 inserted、update 、componentUpdate 、unbind 。

  • 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" />

[](https://coding.jd.com/fe-rd2/...

在页面数据返回以前呈现给用户的一个页面的轮廓,比起以前经常使用的 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>

[](https://coding.jd.com/fe-rd2/...

一般,咱们把项目开发完成,使用 Webpack 进行打包构建时,一般会打包出一个 app.js 文件,和一个 app.css 文件,把这两个文件引入对应的 Html 文件,固然能够正常去访问咱们的应用。看似没什么问题。可是在比较大型的项目中,打包出的 app.js 文件一般是很大的,就拿咱们本项目来讲吧。

若是咱们能把不一样路由对应的组件分割成不一样的代码块,而后当路由被访问的时候才加载对应组件,这样就更加高效了。咱们可使用动态 import 来定义代码分块点

const report = () => import("./../../view/collpro/C/report/reportlist.vue");

这样,咱们再结合 Webpack 就实现了组件的异步加载功能,减小了静态资源大小,提高了页面加载速度。

[](https://coding.jd.com/fe-rd2/... 多页面打包

一般咱们使用 Vue/React 技术栈开发的项目都是 SPA 应用,可是在一些比较大型的项目或者一些业务场景特殊的项目中,SPA 已经不能知足咱们的需求了,这时候须要基于咱们的打包工具 Webpack 进行多页面打包的支持。本次项目涉及B(采购人)、C(提报人)两个角色,并且两个的请求域名和入参都有区别,因此咱们考虑采用多页面来支持。 对于多页面的配置,相信有些童鞋仍是比较陌生,由于在项目中不多用到,故在这里说明一下几个主要的配置项:

  • 首先,多页面确定是须要有多个入口,那么咱们就先从 entry 这里入手
entry: {
        app: './src/collpro/B/app.ts'
    }
entry: {
        b: './src/collpro/B/app.ts',
        c: './src/collpro/C/app.ts'
    },

若是是多页面,采用上面第二种写法,这个也是本次项目中入口的配置。

  • 完善 html-webpack-plugin ,这个插件的做用是会生成一个 Html 文件,里面包含 js、css 等静态资源,那么若是是多页面,这个插件就须要初始化屡次
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,这样才能保证在多页面状况下,修改某处代码只编译一次。

[](https://coding.jd.com/fe-rd2/...

[](https://coding.jd.com/fe-rd2/...

先来看一下须要实现的效果:

需求描述以下:

  • 提报单编辑页点击返回,跳转至提报单列表,不需刷新页面,而且需记录上一次浏览的位置
  • 提报成功以后,点击返回列表,这时候须要刷新数据来改变提报单状态

也就是说,提报单列表并非一直不刷新,而是会根据不一样路由的来源,作是否须要刷新的判断。这里固然会用到 Vue 中的 keep-alive,但仅仅使用它是不能知足需求的~~ 下面来一点点分析:

[](https://coding.jd.com/fe-rd2/...,先设置 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 这两个钩子函数。

这样,上面的需求描述一就知足了,那么需求二又该如何实现呢?

[](https://coding.jd.com/fe-rd2/...,引入导航守卫

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 的导航守卫知足了以上的需求~

[](https://coding.jd.com/fe-rd2/... 低版本键盘弹出和收起形成页面不能还原

在低版本的 ios 部分手机会存在一个兼容性问题,应该是属于内部机制致使。在点击 input 获取焦点后,键盘会自动弹起,将页面顶起,当输入完成后点击‘完成’按钮,键盘自动收起,可是页面没有回滚,致使点击元素还停留在键盘弹起的地方。

解决的办法就是咱们须要在 app.vue 中,在 mounted 钩子里面监听 focusout 事件,手动将页面滚动到初始位置。

document.body.addEventListener("focusout", () => {
        window.scrollTo({ top: 0, left: 0, behavior: "smooth" });
    });

[](https://coding.jd.com/fe-rd2/...、详情滑动头部渐变

本次需求中还有一个比较常见的动画效果,在页面滑动过程当中顶部固态栏渐变。由这样↓

变成这样↓

先来捋一遍实现的思路: 这种渐变功能的实现,宽度等属性比较简单,好比 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";
 }

更干净清爽一些,毕竟适合的才是最好的,至此,这个滑动效果就完成了~~

[](https://coding.jd.com/fe-rd2/...

到这里,文章立刻接近尾声了,但咱们对项目的持续优化以及对技术的热情还远远没有结束。不管是项目技术选型、组件开发、难题攻克仍是性能优化,咱们的路还很长,但咱们需谨记,不管路有多长,咱们只能并且必须一步一个脚印,脚踏实地,在作好项目的同时,作好每个沉淀,日积月累,提高技术水平,而后服务好每个项目/需求。

相关文章
相关标签/搜索