Progressive Refactoring of Vuex API Calls: From Separation of Concerns to Olive‑Shaped Interfaces and Response Normalization
This article walks through a multi‑stage refactoring of Vuex API calls for an IoT project, demonstrating how to separate concerns, create a concise olive‑shaped interface, implement custom isomorphic mappers, and normalize responses with three‑level error handling while keeping the codebase maintainable and scalable.
Editor’s note: the author, Li Songfeng, is a senior technical book translator and front‑end expert at 360 Qiwuchuan, serving on the front‑end technical committee and as a W3C AC representative.
The content originates from an internal "pan‑front‑end" sharing session on March 11, summarizing the author’s experience developing an IoT device‑linkage scenario, with all code presented as pseudo‑code.
Vuex is presented as an essential tool for complex Vue applications, providing a Vue‑native solution for cross‑component state sharing.
Separation of concerns (SoC) and maintainable code: modular design with clear responsibilities improves debugging and maintenance.
Olive‑shaped interface and isomorphic mapper: a thin external entry that encapsulates rich internal logic, mirroring Vuex helper methods.
Response normalization and three‑level error handling: unify data formats and error structures (network, service, and interface errors) and reflect them reactively in Vue components.
Creating the Demo Environment
qvk is a generic web development environment that integrates modern front‑end engineering best practices and can be used for traditional C/S web apps, SPAs, and H5 pages.
ThinkJS – an MVC‑style Node.js framework.
Vue.js – the progressive JavaScript framework for component‑based development.
Webpack – the most widely used front‑end module bundler.
Usage
1. Copy the code
git clone [email protected]:qqvk/qvk.git2. Install dependencies, initialize and start the service
cd qvk // enter project directory
npm install // install dependencies
npm run init // initialize
npm start // start the projectStage 1: Separation of Concerns and Maintainable Code
The left side of the diagram shows the Vuex architecture; the right side shows the dependency relationship of modules in the demo environment. The following code snippets illustrate the first stage (lib/service1.js and store/store1.js).
lib/endpoints.js
/**
* Default export API configuration
*/
export default {
// 1. Get user device list
getUserDeviceList: { method: 'GET', endpoint: '/getUserDeviceList' },
// 2. Get user scene list
getUserSceneList: { method: 'GET', endpoint: '/getUserSeceneList' },
// 3. Query scene
getUserScene: { method: 'GET', endpoint: '/getUserScene' }
}
/**
* Named export global environment
*/
export const ENV = 'https://the-service-address'lib/factory.js
import API, { ENV } from './endpoints'
/**
* Default export factory method: returns request URL and options based on service name and arguments
*/
export default ({ serviceName = '', serviceArguments = {} }) => {
const { method, endpoint } = API[serviceName]
const urlBase = `${ENV}${endpoint}`
const { queryString, headers } = getQueryStringAndHeaders(serviceArguments)
let url, body, options
if (method == 'POST') {
url = urlBase
body = queryString
options = { headers, method, body }
}
if (method == 'GET') {
url = `${urlBase}?${queryString}`
options = { headers, method }
}
return { url, options }
}
/**
* getQueryStringAndHeaders() and other helpers (omitted)
*/lib/service1.js
import factory from './factory'
export default {
// Get user scene list
async getUserSceneList() {
const { url, options } = factory({ serviceName: 'getUserSceneList' })
const res = await fetch(url, { ...options }).then(res => res.json())
return res
},
// Get user device list
async getUserDeviceList() {
const { url, options } = factory({ serviceName: 'getUserSceneList' })
const res = await fetch(url, { ...options }).then(res => res.json())
return res
},
// Get user scene detail
async getUserScene({ scene_id }) {
const { url, options } = factory({ serviceName: 'getUserScene', serviceArguments: { scene_id } })
const res = await fetch(url, { ...options }).then(res => res.json())
return res
}
}store/store1.js
import SERVICE from '../lib/service1'
const store = {
state: {},
actions: {
getUserSceneList() {
return SERVICE['getUserSceneList']()
},
getUserScene(store, { scene_id }) {
return SERVICE['getUserScene']({ scene_id })
}
}
}
export default new Vuex.Store(store)In this stage the service layer handles API requests while the store maps those services to Vue components, but both contain duplicated logic.
Stage 2: Olive‑Shaped Interface and Isomorphic Mapper
The second stage extracts the repeated logic into a single serve() function and then converges all API calls into a compact entry point.
lib/service2.js
import factory from './factory'
// import API from '../lib/endpoints'
function serve({ serviceName = '', serviceArguments = {} }) {
const { url, options } = factory({ serviceName, serviceArguments })
return fetch(url, { ...options }).then(res => res.json())
}
// First implementation (object reduction) – omitted for brevity
export default new Proxy({}, {
get(target, serviceName) {
return serviceArguments => serve({ serviceName, serviceArguments })
}
})store/store2.js
import { mapActions, mapMutations } from './store2.mapper'
const store = {
state: {},
mutations: {
...mapMutations({
getUserSceneList: 'scenes',
getUserScene: 'scene',
getUserDeviceList: 'devices'
})
},
actions: {
...mapActions([
'getUserSceneList',
'getUserScene',
'getUserDeviceList'
])
}
}
export default new Vuex.Store(store)store/store2.mapper.js
import SERVICE from '../lib/service2'
// Named export of isomorphic action mapper
export const mapActions = API => API.reduce((pre, serviceName) => {
pre[serviceName] = ({ commit }, serviceArguments) => {
return SERVICE[serviceName](serviceArguments).then(res => {
commit(serviceName, res.data)
return res
})
}
return pre
}, {})
// Named export of isomorphic mutation mapper
export const mapMutations = MAP => Object.keys(MAP).reduce((pre, serviceName) => {
pre[serviceName] = (state, data) => {
state[MAP[serviceName]] = data
}
return pre
}, {})This stage eliminates duplication by routing all service calls through the unified serve() function and by generating Vuex actions/mutations via custom mappers.
Stage 3: Response Normalization and Three‑Level Error Handling
store/store3.js simply imports the new mapper; the core logic resides in store3.mapper.js .
store/store3.mapper.js
import SERVICE from '../lib/service3'
export const mapActions = API => API.reduce((pre, serviceName) => {
pre[serviceName] = ({ commit }, serviceArguments) => {
return SERVICE[serviceName](serviceArguments).then(res => {
commit(serviceName, res.data)
if (res.code === 0) {
return { ok: true, payload: res }
}
return { ok: false, payload: res }
})
}
return pre
}, {})
export const mapMutations = MAP => Object.keys(MAP).reduce((pre, serviceName) => {
pre[serviceName] = (state, data) => {
state[MAP[serviceName]] = data
}
return pre
}, {})lib/service3.js implements the three‑level error handling:
import factory from './factory'
function serve({ serviceName = '', serviceArguments = {} }) {
const { url, options } = factory({ serviceName, serviceArguments })
const controller = new AbortController()
const signal = controller.signal
// Timeout guard (Promise wrapper)
return new Promise((resolve, reject) => {
TIMEOUT_GUARD({ reject, controller })
fetch(url, { ...options, signal }).then(resolve, reject)
})
.then(res => {
const { ok, status, statusText } = res
if (ok) {
return res.json().then(res => {
const { code, msg, reqid } = res
if (code === 0) return res
return { code: 9001, error: res } // interface error (level 3)
})
}
return { code: 8001, error: { status, statusText } } // service error (level 2)
})
.catch(error => {
if (error.name === 'AbortError') {
return { code: 7001, error } // timeout/network error (level 1)
}
if (error.code === 7002) return error
return { code: 7000, error } // other network errors
})
}
function TIMEOUT_GUARD({ reject, controller }) {
setTimeout(() => {
reject({ code: 7002, error: new Error('Request timed out') })
controller.abort()
}, 10)
}
export default new Proxy({}, {
get(target, serviceName) {
return serviceArguments => serve({ serviceName, serviceArguments })
}
})The third stage ties the normalized response format ( { ok: true/false, payload: ... } ) to Vuex actions, ensuring consistent handling of successful data, service‑side errors, interface errors, and network/time‑out failures.
Conclusion
The article demonstrates, using Vuex as an example, how iterative optimization—organizing, abstracting, and extracting code—leads to higher code‑to‑value ratios: fewer lines achieve more complex functionality, clearer organization eases debugging, and precise abstraction simplifies understanding. The same principles apply to Redux in React development.
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
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.