京東慧採 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"; }
更乾淨清爽一些,畢竟適合的纔是最好的,至此,這個滑動效果就完成了~~
到這裏,文章立刻接近尾聲了,但咱們對項目的持續優化以及對技術的熱情還遠遠沒有結束。不管是項目技術選型、組件開發、難題攻克仍是性能優化,咱們的路還很長,但咱們需謹記,不管路有多長,咱們只能並且必須一步一個腳印,腳踏實地,在作好項目的同時,作好每個沉澱,日積月累,提高技術水平,而後服務好每個項目/需求。