How We Built a Scalable Vue.js Ecosystem for JD Kids Platform
This article details the challenges of integrating dozens of APIs, complex UI interactions, and rapid multi‑version releases in a JD Kids shopping ecosystem, and explains how Vue, Vuex, Webpack, and a series of custom mixins, routing tricks, and constant management strategies were used to create a maintainable, high‑performance single‑page application.
Project Overview
The project aggregates dozens of JD APIs (~90) with inconsistent fields and supports a family‑oriented shopping experience. To handle rapid multi‑version releases, complex UI states, and asynchronous updates, a Vue.js single‑page application (SPA) with Vuex for centralized state management was chosen.
Technology Selection
Declarative Rendering : Vue templates bind data to the DOM automatically.
Component System : UI is split into reusable single‑file components (template, script, style).
Client‑Side Routing : vue‑router provides hash‑based navigation and route meta data.
State Management : Vuex offers a global store; mutations update state, actions perform async API calls, and getters expose processed data.
Project Structure
Development Setup
The build pipeline uses Webpack with separate base, development, and production configurations. ESLint and Babel are integrated for code quality and modern JavaScript features.
// Development configuration (webpack.dev.conf.js)
module.exports = merge(base, {
plugins: [
new webpack.HotModuleReplacementPlugin(),
new webpack.NoErrorsPlugin(),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')
}),
new HtmlWebpackPlugin({ filename: 'index.html', template: '../index.html' })
]
});
// Production configuration (webpack.prod.conf.js)
module.exports = merge.smart(base, {
module: {
loaders: [{
test: /\.s[a|c]ss$/,
loader: ExtractTextPlugin.extract({
fallbackLoader: "style-loader",
loader: 'css!sass'
})
}]
},
plugins: [
new ExtractTextPlugin('style.css'),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production')
}),
new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', filename: 'vendor.js' }),
new webpack.LoaderOptionsPlugin(loadersConf),
new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false } })
]
});During development an Express server runs webpack-dev-middleware and webpack-hot-middleware to enable hot reloading.
const express = require('express');
const webpackDevMiddleware = require('webpack-dev-middleware');
const webpack = require('webpack');
const conf = require('./webpack.dev.conf');
const app = express();
const port = process.env.PORT || 8080;
conf.entry.app = ['webpack-hot-middleware/client', conf.entry.app];
const compiler = webpack(conf);
app.use(webpackDevMiddleware(compiler, {
publicPath: conf.output.publicPath,
stats: { colors: true, chunks: false }
}));
app.use(require('webpack-hot-middleware')(compiler));
app.listen(port, () => console.log(`server started at localhost:${port}`));Routing
Routes are defined with meta information (title, page‑view count, profile requirement, visitor flag, custom verification). Navigation guards handle authentication, data pre‑fetching, and PV reporting.
{
name: 'index',
path: '/index',
meta: {
title: '陪伴空间',
pv: 50,
profiles: true,
visitor: true,
verify() { return true; }
},
components: { default: Index2, navbar: Navbar }
}In iOS, page titles are refreshed by injecting a hidden iframe that loads a blank page after each route change.
const iframeLoad = src => {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = src;
document.body.appendChild(iframe);
iframe.addEventListener('load', () => setTimeout(() => iframe.remove(), 0));
};Componentization
Key component example demonstrates slots, props, and method‑based communication.
<template>
<div v-show="isShow" class="test">
<slot></slot>
<slot name="slot2"></slot>
<template v-if="testProp"></template>
<template v-else></template>
<div @click="changeNickname && changeNickname('小镇')"></div>
<div @click="close" class="test_btn">{{btnText}}</div>
</div>
</template>
<script>
import Utils from '@/utils';
export default {
props: {
testProp: { type: [Number, String], required: true },
changeNickname: Function
},
data() { return { isShow: false, btnText: '', closeFn: null }; },
methods: {
close() { this.isShow = false; this.closeFn && this.closeFn(); },
open(btnText = '', closeFn) { this.isShow = true; this.btnText = btnText; this.closeFn = closeFn; }
}
};
</script>
<style lang="sass">
@import "common";
.test { background-image: url(~@img/test/bg.png); }
</style>Slots allow parent components to inject custom markup, while props and function‑type props enable two‑way communication without excessive event emission.
Mixins
Global mixins are used sparingly for shared data such as analytics tracking. Heavy logic is avoided to prevent performance degradation.
Vue.mixin(mixins); // Example of registering a global mixinConstant Management
All API endpoints and data field names are centralized to avoid duplication and inconsistencies.
// URL constants
export const REBUY_LIST = `${NIGHT}/re_purchase_detail`;
export const REBUY_SWITCH = `${NIGHT}/re_purchase_switch_good`;
// Field constants
export const ID = 'id';
export const SKU = 'sku';
export const LINK = 'link';
export const NAME = 'name';
export const IMAGE = 'image';
export const JD_PRICE = 'jdPrice';
export const PRICE = 'price';Vuex Data Center
Components access state via mapGetters and trigger API calls with mapActions. Actions fetch data, mutations normalize it, and getters expose the cleaned result.
computed: {
...mapGetters({ cate1st: 'cate1st', cate2nd: 'cate2nd' })
},
methods: {
...mapActions(['getCate1st', 'getCate2nd'])
}Environment Compatibility
Different runtime environments (cart, product detail, coupons, search) require distinct configuration objects. The appropriate config is selected at runtime based on the user‑agent type.
let configs = { /* environment specific objects */ };
export default configs[UA.type];Scroll Behavior
SPA scroll restoration is handled by a mixin that stores the scroll position in sessionStorage (fallback to cookies). On view mount, the stored position is restored after a short delay. Developers can manually trigger scrolling when asynchronous data loading completes.
import Tools from '@/utils/tools';
const ss = window.sessionStorage;
export default {
data() { return { routeName: this.$route.name, liveScrollFlag: false, liveScrollFn: null, liveScrollTimer: null }; },
computed: {
liveScrollTop() {
return ss ? ss.getItem(`view-${this.routeName}`) : Tools.getCookie(`view-${this.routeName}`);
}
},
methods: {
_livescroll() {
if (this.liveScrollFlag || !this.liveScrollTop) return;
this.liveScrollFlag = true;
this.liveScrollTimer = window.setTimeout(() => {
document.body.scrollTop = document.documentElement.scrollTop = this.liveScrollTop;
}, 500);
}
},
mounted() {
document.body.scrollTop = document.documentElement.scrollTop = 0;
!this.manualTriggerLivescroll && this._livescroll();
this.liveScrollFn = () => {
ss ? ss.setItem(`view-${this.routeName}`, this.getScrollTop())
: Tools.setCookie(`view-${this.routeName}`, this.getScrollTop(), 0.2083);
};
window.addEventListener('touchend', this.liveScrollFn, false);
},
beforeDestroy() {
window.removeEventListener('touchend', this.liveScrollFn, false);
this.liveScrollTimer && window.clearTimeout(this.liveScrollTimer);
}
};Limitations: the solution does not support lazy‑loaded modules and requires explicit mixin inclusion in each view that needs scroll persistence.
Additional Technical Notes
API latency handling: a wrapper retries timed‑out requests.
Object rest/spread support: Babel plugins transform-object-rest-spread or babel-preset-env are added; ESLint parser options enable the syntax.
Dynamic publicPath: a map selects the correct path per environment (development, labs, production).
Alias usage in style resources: ~@img/… resolves image paths correctly, avoiding relative‑path errors.
CSS masking: used to reduce image size and create custom shapes; drop‑shadow requires an extra wrapper element.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Aotu Lab
Aotu Lab, founded in October 2015, is a front-end engineering team serving multi-platform products. The articles in this public account are intended to share and discuss technology, reflecting only the personal views of Aotu Lab members and not the official stance of JD.com Technology.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
