This guide is the deep reference for developers, architects, and anyone maintaining, extending, or deploying vue2-elm. Where the User Guide covers using the app, this guide covers how it is built: its architecture, its configuration and build pipeline, its data and state layers, its security posture, and the technical debt a maintainer should be aware of. It is written to be complete enough that an engineer — or an AI — could reconstruct the project from this document alone.
vue2-elm is a client-side single-page application. It has no server of its own; it is a
Vue 2 front end that consumes a separate REST backend (node-elm).
Keep that boundary in mind throughout: nearly every "operational" concern here is about
building, hosting, and configuring a static bundle, not about running a service.
Table of Contents
- Executive Summary
- Architecture
- Requirements & Toolchain
- Configuration Reference
- The Data Layer (Service & Fetch)
- State Management (Vuex Store)
- Routing
- Shared Components & Utilities
- Build & Deployment
- Security Analysis
- Code Quality & Patterns
- Operations & Troubleshooting
- Potential Improvements
- Quick Reference
1. Executive Summary
| Attribute | Value |
|---|---|
| Type | Mobile-first SPA (front end only) |
| Domain | Food-delivery storefront (an Ele.me / 饿了么 clone) |
| Framework | Vue 2 + Vuex 2 + Vue Router 2 |
| Build | webpack 1, Babel (ES6/7 → ES5), Sass |
| Routing | Hash mode (#/path) |
| Backend | External: node-elm (not in this repo) |
| Output | Static assets in elm/ |
| Scope | ~45 screens, deeply nested routes (up to 4 levels) |
| License | GPL |
| Purpose | A real-world learning project demonstrating a complex, money-handling SPA |
What makes it architecturally interesting: unlike most Vue learning demos (single-page players, to-do lists), vue2-elm implements a complete commerce flow with the hard parts — a persistent multi-restaurant cart, a multi-step checkout, address management, and an authentication flow — and it leans on Vuex precisely where local component state would break down. It is a textbook example of "when do you actually need a store."
What it intentionally omits: real payment (the flow stops at the pay step by design) and any of its own backend. Treat both as deliberate scope boundaries, not gaps.
2. Architecture
2.1 High-level topology
┌──────────────────────────────────────────┐
Browser (mobile) │ vue2-elm SPA │
┌───────────────┐ │ │
│ index.html │───▶│ main.js → new Vue({ router, store }) │
└───────────────┘ │ │ │
│ ├─ Vue Router (hash) ── pages/* │
│ ├─ Vuex Store ──────── cart, user │
│ └─ App.vue (router-view + keepAlive)│
│ │
│ service/getData.js │
│ │ (one function per endpoint) │
│ ▼ │
│ config/fetch.js (fetch + XHR fallback) │
└───────────────┼──────────────────────────┘
│ HTTP (cookies: credentials:include)
▼
┌──────────────────────────────────────────┐
│ node-elm backend (separate project) │
│ cangdu.org (prod) | localhost (local) │
└──────────────────────────────────────────┘
2.2 Application bootstrap
The entry sequence is small and explicit:
index.htmlmounts a single#appelement.src/main.jsis the program entry. It:- Registers Vue Router and constructs the router from
src/router/router.js. - Reads
routerModefromsrc/config/env.js(hash). - Imports
src/config/rem.jsfor the mobile rem layout (side-effect import). - Defines a
scrollBehaviorthat preserves scroll position forkeep-aliveroutes. - Instantiates the root
new Vue({ router, store }).$mount('#app').
- Registers Vue Router and constructs the router from
src/App.vueis the shell. It renders two<router-view>s wrapped in transitions — one inside<keep-alive>(for routes whosemeta.keepAliveis true, e.g. Msite) and one outside it — plus the global<svg-icon>sprite component.
Note
The dual
router-viewpattern is how this app gets selective view caching: only routes that opt in viameta: { keepAlive: true }are kept alive (preserving scroll and state when you navigate back), while everything else re-renders fresh.
2.3 Layered design
The src/ tree separates concerns cleanly into layers:
| Layer | Directory | Responsibility |
|---|---|---|
| Entry | src/ (main.js, App.vue) |
Bootstrap, root shell |
| Config | src/config/ |
Environment, fetch wrapper, utilities, rem layout |
| Routing | src/router/ |
Route table (lazy-loaded pages) |
| Pages | src/page/ |
One folder per screen; nested children for sub-screens |
| Components | src/components/ |
Reusable UI: common/, header/, footer/ |
| Service | src/service/ |
API surface — one exported function per endpoint |
| Store | src/store/ |
Vuex state, mutations, actions, getters, mutation-types |
| Styles | src/style/ |
Shared SCSS (common.scss, mixin.scss) |
This is a conventional and well-factored Vue 2 layout: pages compose components, call the service layer for data, and read/write shared state through the store.
2.4 Data flow for a typical screen
Using Msite (src/page/msite/msite.vue) as the canonical example:
beforeMountresolves ageohash— from the route query if present, otherwise viacityGuess()(IP-based) — and commits it to the store (SAVE_GEOHASH).- It calls
msiteAddress(geohash)to resolve the address, sets the header title, and commitsRECORD_ADDRESS(latitude/longitude) to the store. mountedcallsmsiteFoodTypes(geohash), chunks the categories into pages of 8, and initializes a Swiper carousel.- The embedded
<shop-list>component readslatitude/longitudefrom the store (mapState) and callsshopList(...)itself, paginating via infinite scroll.
This illustrates the app's consistent pattern: lifecycle hooks fetch via the service layer, shared coordinates flow through Vuex, and child components subscribe to that state.
3. Requirements & Toolchain
3.1 Runtime/build requirements
| Tool | Declared / recommended | Source |
|---|---|---|
| Node.js | engines.node >= 4.0.0; README recommends >= 6.0.0 |
package.json, README |
| npm | engines.npm >= 3.0.0 |
package.json |
| Yarn | Optional alternative (lockfile committed) | README, yarn.lock |
The author's documented development environment was macOS 10.12.3, Chrome 56, Node 6.10.0.
3.2 Runtime dependencies
| Package | Version | Role |
|---|---|---|
vue |
^2.1.0 |
Core framework |
vue-router |
^2.1.1 |
SPA routing |
vuex |
^2.0.0 |
State management |
better-scroll |
1.15.2 |
Smooth scrolling regions |
fastclick |
^1.0.6 |
Removes mobile tap delay |
iscroll |
^5.2.0 |
Scrolling fallback |
showdown |
^1.6.4 |
Markdown → HTML (used for static text content) |
3.3 Notable build dependencies
The devDependencies define a classic webpack-1 + Babel-6 pipeline: webpack@^1.13.2,
vue-loader@^10, vue-template-compiler, babel-preset-es2015 + babel-preset-stage-2,
node-sass + sass-loader, less + less-loader, autoprefixer, extract-text-webpack-plugin,
html-webpack-plugin, http-proxy-middleware (dev API proxy), webpack-dev-middleware /
webpack-hot-middleware (HMR), and cross-env (cross-platform NODE_ENV).
Warning
This is an aged toolchain.
node-sassbuilds native bindings against the active Node version and is the most common install failure on modern Node. webpack 1 and Babel 6 are both several major versions behind current. See Potential Improvements.
4. Configuration Reference
vue2-elm's configuration is concentrated in two small files plus the npm scripts. There are no
environment files (.env) and no runtime configuration service — everything is decided at
build time.
4.1 src/config/env.js — environment switching
This file exports the host configuration, selected by process.env.NODE_ENV:
| Export | Development | Production | Notes |
|---|---|---|---|
baseUrl |
'' (empty — requests are dev-proxied) |
//elm.cangdu.org |
Prefixed onto every API path in fetch.js. |
imgBaseUrl |
/img/ |
//elm.cangdu.org/img/ |
Base for image URLs. |
routerMode |
hash |
hash |
Constant; consumed by main.js. |
let baseUrl = '';
let routerMode = 'hash';
let imgBaseUrl = '';
if (process.env.NODE_ENV == 'development') {
imgBaseUrl = '/img/';
} else if (process.env.NODE_ENV == 'production') {
baseUrl = '//elm.cangdu.org';
imgBaseUrl = '//elm.cangdu.org/img/';
}
export { baseUrl, routerMode, imgBaseUrl }
Caution
Negative knowledge —
localapiandproapido not exist. Several modules (src/store/mutations.js,src/components/common/mixin.js,src/page/login/login.vue) writeimport { localapi, proapi } from 'src/config/env'.env.jsexports onlybaseUrl,routerMode, andimgBaseUrl, solocalapiandproapiresolve toundefined. They are dead leftovers from an earlier configuration scheme and are never used meaningfully. Do not document them as configuration knobs; if cleaning up, remove the imports.
4.2 config/index.js — dev server & build paths
This is the webpack-template config, split into build and dev sections.
build block (production):
| Key | Value | Meaning |
|---|---|---|
env.NODE_ENV |
"production" |
Build-time environment. |
index |
../elm/index.html |
Output HTML path. |
assetsRoot |
../elm |
Output root. |
assetsSubDirectory |
static |
Asset subfolder. |
assetsPublicPath |
/elm/ |
Public base path for assets. |
productionSourceMap |
true |
Emit source maps. |
productionGzip |
false |
Gzip is off (host handles it). |
dev block (development):
| Key | Value | Meaning |
|---|---|---|
env.NODE_ENV |
"development" |
Dev environment. |
port |
8000 |
Dev server port. |
assetsPublicPath |
/ |
Public base in dev. |
proxypath |
http://cangdu.org:8001 |
Upstream API target for the dev proxy. |
context |
see below | Path prefixes proxied to proxypath. |
cssSourceMap |
false |
CSS source maps off. |
The proxied context prefixes are exactly the API namespaces used by the service layer:
/shopping /ugc /v1 /v2 /v3 /v4 /bos /member /promotion /eus /payapi /img
Note
This proxy is why
baseUrlis empty in development: the dev server forwards those prefixed requests toproxypath, avoiding cross-origin issues. In a production build,baseUrl(//elm.cangdu.org) is prepended instead and the proxy is not involved.
4.3 npm scripts and NODE_ENV
"scripts": {
"dev": "cross-env NODE_ENV=online node build/dev-server.js",
"local": "cross-env NODE_ENV=local node build/dev-server.js",
"build": "node build/build.js"
}
dev and local both launch the same dev server; the NODE_ENV value (online vs local)
is what selects which backend the dev tooling targets. build runs the production build into
elm/.
Note
The
build/directory (dev-server.js,build.js, and webpack configs) is the standardvuejs-templates/webpackscaffolding referenced by these scripts and byconfig/index.js. It orchestrates the dev server, the API proxy, and the production build.
5. The Data Layer (Service & Fetch)
5.1 src/config/fetch.js — the HTTP client
All network traffic funnels through a single default-exported async function. Its contract:
fetch(url = '', data = {}, type = 'GET', method = 'fetch')
Behavior:
- Prepends
baseUrl(fromenv.js) tourl. - For
GET, serializesdatainto akey=value&…query string. - If
window.fetchis available andmethod == 'fetch', it issues afetch()with:credentials: 'include'(sends cookies — the session mechanism),headers: Accept/Content-Type: application/json,mode: "cors",cache: "force-cache",- JSON-stringified body for
POST.
- Otherwise it falls back to a manual
XMLHttpRequest(withActiveXObjectfor very old IE), usingapplication/x-www-form-urlencoded. - Returns the parsed JSON; throws
new Error(error)on failure.
Important
credentials: 'include'means the app relies on cookies for authentication state on the backend side, while also persistinguser_idinlocalStorageon the client. The two work together; see Security Analysis.
Warning
cache: "force-cache"tells the browser to prefer a cached response when one exists. For a data API this can serve stale results; it is a questionable default worth revisiting.
5.2 src/service/getData.js — the API surface
This file is the single, centralized catalog of every backend call — one exported function per endpoint, grouped by domain. This is an exemplary pattern: pages never construct URLs themselves; they import named functions. The complete inventory:
| Function | Method | Endpoint |
|---|---|---|
cityGuess |
GET | /v1/cities?type=guess |
hotcity |
GET | /v1/cities?type=hot |
groupcity |
GET | /v1/cities?type=group |
currentcity |
GET | /v1/cities/:id |
searchplace |
GET | /v1/pois?type=search |
msiteAddress |
GET | /v2/pois/:geohash |
msiteFoodTypes |
GET | /v2/index_entry |
shopList |
GET | /shopping/restaurants |
searchRestaurant |
GET | /v4/restaurants |
foodCategory |
GET | /shopping/v2/restaurant/category |
foodDelivery |
GET | /shopping/v1/restaurants/delivery_modes |
foodActivity |
GET | /shopping/v1/restaurants/activity_attributes |
shopDetails |
GET | /shopping/restaurant/:id |
foodMenu |
GET | /shopping/v2/menu |
getRatingList |
GET | /ugc/v2/restaurants/:id/ratings |
ratingScores |
GET | /ugc/v2/restaurants/:id/ratings/scores |
ratingTags |
GET | /ugc/v2/restaurants/:id/ratings/tags |
mobileCode |
POST | /v4/mobile/verify_code/send |
getcaptchas |
POST | /v1/captchas |
checkExsis |
GET | /v1/users/exists |
sendMobile |
POST | /v1/mobile/verify_code/send |
checkout |
POST | /v1/carts/checkout |
getRemark |
GET | /v1/carts/:id/remarks |
getAddress |
GET | /v1/carts/:id/addresses |
searchNearby |
GET | /v1/pois?type=nearby |
postAddAddress |
POST | /v1/users/:id/addresses |
placeOrders |
POST | /v1/users/:id/carts/:cartId/orders |
rePostVerify |
POST | /v1/carts/:id/verify_code |
validateOrders |
POST | /v1/users/:id/carts/:cartId/orders |
payRequest |
GET | /payapi/payment/queryOrder |
getService |
GET | /v3/profile/explain |
vipCart |
POST | /member/v1/users/:id/delivery_card/physical_card/bind |
getHongbaoNum |
GET | /promotion/v2/users/:id/hongbaos |
getExpired |
GET | /promotion/v2/users/:id/expired_hongbaos |
exChangeHongbao |
POST | /v1/users/:id/hongbao/exchange |
getUser |
GET | /v1/user |
sendLogin |
POST | /v1/login/app_mobile |
getOrderList |
GET | /bos/v2/users/:id/orders |
getOrderDetail |
GET | /bos/v1/users/:id/orders/:orderId/snapshot |
getAddressList |
GET | /v1/users/:id/addresses |
getSearchAddress |
GET | v1/pois?type=nearby |
deleteAddress |
DELETE | /v1/users/:id/addresses/:addressId |
accountLogin |
POST | /v2/login |
signout |
GET | /v2/signout |
changePassword |
POST | /v2/changepassword |
Note
The API namespaces (
/v1–/v4,/shopping,/ugc,/bos,/member,/promotion,/payapi) mirror Ele.me's real micro-service decomposition and are exactly thecontextprefixes proxied inconfig/index.js. Adding a new endpoint means adding a function here and (if it introduces a new prefix) registering that prefix in the dev proxycontextlist.
6. State Management (Vuex Store)
The store (src/store/) is split into the canonical Vuex files: index.js (state + assembly),
mutations.js, action.js, getters.js, and mutation-types.js (string constants).
6.1 State shape (src/store/index.js)
| State key | Initial | Purpose |
|---|---|---|
latitude, longitude |
'' |
Current location coordinates. |
cartList |
{} |
The cart, keyed shopid → category_id → item_id → food_id. |
shopDetail |
null |
Current restaurant detail. |
userInfo |
null |
Logged-in user object. |
shopid |
null |
Active restaurant id. |
remarkText, inputText |
null / '' |
Order remark (selected + free text). |
invoice |
false |
Whether an invoice was requested. |
newAddress, searchAddress, choosedAddress, addressIndex |
[] / null |
Address selection in checkout. |
geohash |
'31.22299,121.36025' |
Location geohash (defaults to Shanghai coords). |
needValidation |
null |
Whether the order needs extra verification. |
cartId, sig |
null |
Cart identifier + signature returned at checkout. |
orderParam, orderMessage, orderDetail |
null |
Order placement parameters and results. |
login |
true |
Login flag. |
imgPath |
null |
Avatar image path. |
removeAddress, addAddress |
[] / '' |
Profile address-book editing. |
question |
null |
Selected service-center question. |
cartPrice |
null |
VIP card price. |
6.2 Mutations (src/store/mutations.js)
Mutations are declared with computed-property names bound to the constants in
mutation-types.js. The cart mutations are the most substantial:
ADD_CARTbuilds a nested object path (cartList[shopid][category_id][item_id][food_id]), incrementingnumif the dish exists or creating it (withname,price,specs,packing_fee,sku_id,stock). It reassignscartListwith a spread to trigger reactivity and persists tolocalStorageviasetStore('buyCart', …).REDUCE_CARTdecrements, nulling the entry at zero, and re-persists.INIT_BUYCARTrehydratescartListfromlocalStorageon load.CLEAR_CARTempties one restaurant's cart.
Other mutation groups handle user info (RECORD_USERINFO persists user_id; GET_USERINFO,
RETSET_NAME, OUT_LOGIN, SAVE_AVANDER), checkout (SAVE_GEOHASH, CONFIRM_REMARK,
CONFIRM_INVOICE, CHOOSE_ADDRESS, CONFIRM_ADDRESS, NEED_VALIDATION, SAVE_CART_ID_SIG,
SAVE_ORDER_PARAM, CHANGE_ORDER_PARAM, ORDER_SUCCESS, SAVE_ORDER, SAVE_SHOPID),
addresses (SAVE_ADDRESS, SAVE_ADDDETAIL, ADD_ADDRESS), and misc (SAVE_QUESTION,
BUY_CART).
Tip
The cart's reliance on
{...spread}reassignment is a deliberate Vue 2 reactivity workaround: Vue 2 cannot detect new properties added to an object, so the code replaces the object reference to force re-render. In Vue 3 (Proxy-based reactivity) this would be unnecessary.
6.3 Actions (src/store/action.js)
Only two async actions exist — both bridge the service layer into the store:
getUserInfocallsgetUser()and commitsGET_USERINFO.saveAddressloads the user's address list (getAddressList) and commitsSAVE_ADDRESS, short-circuiting ifremoveAddressis already populated.
Most data fetching happens directly in components, not in actions — a pragmatic choice for a project of this size.
6.4 Getters (src/store/getters.js)
The getters file is empty (export default {}). All derived values are computed locally in
components (e.g. buyCart.vue's foodNum). This is worth noting for anyone expecting a getters
layer — there isn't one.
7. Routing
src/router/router.js defines the entire route tree. Two conventions dominate:
7.1 Lazy loading via require.ensure
Every page is wrapped in a webpack-1 code-split chunk:
const msite = r => require.ensure([], () => r(require('../page/msite/msite')), 'msite')
Each page becomes a named chunk ('msite', 'shop', …), so the browser only downloads a
screen's code when it is first visited. This keeps the initial bundle small despite ~45 screens.
7.2 Deeply nested routes
Routes mirror the UI's drill-down structure, nesting up to four levels. For example, the checkout address flow:
/confirmOrder
└─ chooseAddress
└─ addAddress
└─ searchAddress
and the profile address editor:
/profile → info → address → add → addDetail
All pages are children of a single top-level route whose component is App. The Msite route
sets meta: { keepAlive: true } to participate in the <keep-alive> view caching described in
§2.2.
Note
Because routing is hash mode, no server-side rewrite configuration is needed for deep links — the path after
#is handled entirely client-side. This simplifies hosting (any static server works) at the cost of#in URLs.
8. Shared Components & Utilities
8.1 Common components (src/components/common/)
| Component | Responsibility |
|---|---|
shoplist.vue |
The restaurant list used by Msite and Shop. Owns infinite scroll (the v-load-more directive), back-to-top, loading skeletons, and per-item rendering. Reads latitude/longitude from the store. |
buyCart.vue |
The add/remove cart control on each dish. Computes per-dish quantity from cartList, dispatches ADD_CART/REDUCE_CART, and emits the "drop into cart" animation event. Handles multi-spec dishes. |
ratingStar.vue |
Renders a 5-star rating using overlaid SVG stars and a width clip. |
loading.vue |
The sprite-sheet loading animation. |
alertTip.vue |
A modal alert with a single confirm button; emits closeTip. |
computeTime.vue |
A payment countdown timer. Its "go to pay" handler shows 暂不开放支付接口 — the explicit hook where payment is stubbed out. |
svg.vue |
A hidden inline SVG sprite (<symbol> defs) referenced everywhere via <use xlink:href="#id">. Defines icons: eleme, user, shop, location, star, backtop, cart icons, payment-brand icons (weixin, zhifubao), and more. |
mixin.js |
Two reusable mixins (below). |
8.2 mixin.js — shared behavior
loadMore— registers aload-moredirective implementing touch-driven infinite scroll (listens totouchstart/touchmove/touchend, computes scroll position withrequestAnimationFrame, and invokes the bound callback at the bottom).getImgPath— converts a backend image hash into a CDN URL onhttps://fuss10.elemecdn.com(splitting the hash into path segments and appending.jpegor.png); falls back to//elm.cangdu.org/img/default.jpg.
8.3 src/config/mUtils.js — utilities
| Export | Purpose |
|---|---|
setStore / getStore / removeStore |
localStorage helpers (JSON-encode objects). |
getStyle |
Cross-browser computed-style reader. |
loadMore |
Standalone infinite-scroll helper (parallel to the directive). |
showBack |
Scroll listener that toggles the back-to-top button past 500 px. |
animate |
A hand-rolled animation engine supporting ease-out, linear, and ease-in modes, rem-aware unit handling, opacity/scrollTop special-casing, and chained callbacks. |
8.4 src/config/rem.js — the mobile layout primitive
A self-invoking function that sets the document's root font size proportionally to viewport
width, recalculating on resize/orientationchange:
docEl.style.fontSize = 20 * (clientWidth / 320) + 'px';
This makes 1rem = 20px at a 320 px-wide screen and scales linearly with device width — the
mechanism behind every rem dimension in the SCSS. It is also why a desktop viewport renders
the UI oversized.
8.5 Header & footer
src/components/header/head.vue— the shared top bar (title, back button, search slot, context-specific slots used by Home/Msite/Login).src/components/footer/footGuide.vue— the bottom tab navigation.
9. Build & Deployment
9.1 Building
npm run build
Per config/index.js, this writes:
elm/index.html(HTML entry),elm/static/…(JS/CSS/img assets, with source maps),- using a public asset path of
/elm/.
The result is a self-contained static site.
9.2 Deploying
Because vue2-elm is a static SPA with hash routing, deployment is simply serving the elm/
directory from any static host (nginx, Apache, a CDN, Netlify, Surge, GitHub Pages, etc.).
No server-side routing rules are required.
Important
The production build hard-codes
baseUrl = //elm.cangdu.org(viaenv.js). If you host the front end yourself and run your own backend, you must editenv.jsso the productionbaseUrl/imgBaseUrlpoint at your API host before building. Otherwise the deployed app will callelm.cangdu.org.
9.3 Deployment checklist
- [ ] Backend (
node-elmor equivalent) deployed and reachable over HTTPS. - [ ]
src/config/env.jsproductionbaseUrl/imgBaseUrlpoint to your API + image hosts. - [ ]
npm run buildcompleted without errors. - [ ]
elm/served by a static host;assetsPublicPathmatches the serving path. - [ ] App verified in a mobile viewport over HTTPS.
Note
The project's own demo notes emphasize that the preview and its interfaces must be accessed over HTTPS. Mixed-content (HTTPS page calling HTTP API) will be blocked by browsers.
10. Security Analysis
vue2-elm is a learning project, and its security posture reflects that. A maintainer adapting it toward anything real should weigh the following.
10.1 Authentication & session
- Auth state is carried by cookies (
fetchusescredentials: 'include') plus a client-sideuser_idstored inlocalStorage. The real session authority is the backend cookie; thelocalStoragevalue is convenience state. - The login flow (
src/page/login/login.vue) supports SMS and account+captcha modes. Against the demo backend, unknown accounts auto-register on login — acceptable for a sandbox, unacceptable for production.
10.2 Transport
- HTTPS is required (the demo explicitly calls this out). The hard-coded
//-relativebaseUrlinherits the page scheme, which is the correct behavior on an HTTPS page. cache: "force-cache"on every request risks serving stale authenticated data from cache.
10.3 Client-side validation only
- Input validation (phone format via
/^1\d{10}$/, SMS code via/^\d{6}$/, required fields) is purely client-side and user-experience oriented. All real authorization must be enforced by the backend — the front end cannot be trusted.
10.4 Sensitive data exposure
productionSourceMap: trueships source maps to production, exposing readable source. Fine for an open-source learning project; disable for anything proprietary.- No secrets live in the front end (correctly) — there are no API keys in the client.
Caution
Do not treat vue2-elm as a security template. Its value is in UI/state architecture, not in auth hardening. Any production fork needs server-enforced authz, CSRF protection appropriate to its cookie auth, a sane cache policy, and removal of auto-registration.
11. Code Quality & Patterns
11.1 Strengths
- Clean layering. Config / service / store / pages / components are well separated.
- Centralized API surface.
getData.jsas the single endpoint catalog is exemplary and makes the backend contract auditable in one place. - Named-chunk lazy loading keeps the initial payload small across ~45 screens.
- Pragmatic Vuex usage. State is reserved for genuinely shared concerns (cart, user, checkout hand-off between deeply nested routes) — exactly where prop-drilling would hurt.
- localStorage persistence of cart and session gives a resilient UX.
11.2 Rough edges
- Dead imports of
localapi/proapi(see §4.1). - Empty getters layer; derived state is scattered in components.
- Two parallel infinite-scroll implementations (the
mixin.jsdirective andmUtils.loadMore). - Bilingual inline comments (Chinese-first) — informative but a barrier for non-Chinese
maintainers (mitigated by the bundled
README-en.md). cache: "force-cache"as a global default is risky for a data API.- Hand-rolled
animateduplicates what CSS transitions / a small lib would handle more robustly.
11.3 Conventions to follow when extending
- Add backend calls only in
getData.js; never build URLs in a page. - Register any new API prefix in the
config/index.jsdev-proxycontext. - Use
mutation-types.jsconstants for new mutations. - Keep dimensions in
remso the layout scales withrem.js. - Opt a route into caching with
meta: { keepAlive: true }only when scroll/state preservation is genuinely wanted.
12. Operations & Troubleshooting
Because there is no server to operate, "operations" here means build/host health and the external-backend dependency.
| Symptom | Likely cause | Resolution |
|---|---|---|
All data calls return ERROR_DATA / 404 |
Backend down or wrong host | Run local node-elm; use npm run local; verify baseUrl |
npm install fails on node-sass |
Node version mismatch | Use Node 6–8 via nvm; reinstall |
browserslist@1.7.7 warning |
Stale caniuse data | Harmless; ignore |
| UI oversized on desktop | rem layout for 320 px | View in mobile emulation |
| Mixed-content blocked | HTTPS page → HTTP API | Serve API over HTTPS |
| Login captcha image broken | Backend unreachable | Fix backend; tap "换一张" to refetch |
Deployed app calls cangdu.org unexpectedly |
Production baseUrl not changed |
Edit env.js, rebuild |
Tip
When diagnosing "nothing loads," open DevTools → Network and inspect a failing request. A blocked/
ERR_CONNECTIONor non-200 to a/v1//shopping/… path confirms a backend connectivity problem rather than a front-end bug.
13. Potential Improvements
A prioritized list for anyone modernizing the project:
- Modernize the toolchain. webpack 1 → 5 and Babel 6 → current remove the
node-sassinstall pain and unlock current tooling. This is the highest-leverage change. - Migrate Vue 2 → Vue 3. Composition API and Proxy reactivity would eliminate the
{...spread}cart workaround and the empty-getters awkwardness. - Remove dead code. Delete the
localapi/proapiimports and one of the two duplicate infinite-scroll implementations. - Externalize configuration. Replace the hard-coded hosts in
env.jswith build-time environment variables so self-hosting doesn't require editing source. - Fix the cache policy. Replace the global
cache: "force-cache"with per-request caching (or none) to avoid stale data. - Consolidate data fetching. Move component-level fetches into Vuex actions for testability, or adopt a data-fetching library.
- Replace
node-sasswithdart-sass(sass) to remove native-build fragility. - Add tests. There is no test suite; component and store unit tests would make refactors (especially the cart) safe.
- Disable production source maps for any non-open deployment.
14. Quick Reference
14.1 Project map
vue2-elm/
├── build/ webpack config, dev-server, build scripts
├── config/index.js build paths + dev proxy (port 8000, proxypath cangdu.org:8001)
├── elm/ PRE-BUILT output (not source)
├── src/
│ ├── main.js entry: router + store + rem, mounts #app
│ ├── App.vue shell: dual router-view + keep-alive + svg sprite
│ ├── config/
│ │ ├── env.js baseUrl / imgBaseUrl / routerMode (hash)
│ │ ├── fetch.js HTTP client (fetch + XHR fallback)
│ │ ├── mUtils.js localStorage, getStyle, loadMore, showBack, animate
│ │ └── rem.js root font-size = 20 * (width/320) px
│ ├── service/getData.js ALL API endpoints (one fn each)
│ ├── store/ Vuex: state, mutations, action, getters(empty), mutation-types
│ ├── router/router.js route tree (lazy require.ensure chunks)
│ ├── components/
│ │ ├── common/ shoplist, buyCart, loading, alertTip, ratingStar, computeTime, svg, mixin
│ │ ├── header/head.vue
│ │ └── footer/footGuide.vue
│ ├── page/ ~45 screens, nested children
│ └── style/ common.scss, mixin.scss
├── package.json scripts: dev / local / build
└── README.md / README-en.md
14.2 Commands
| Command | Effect |
|---|---|
npm install / yarn |
Install dependencies |
npm run dev |
Dev server → public backend (NODE_ENV=online) |
npm run local |
Dev server → local node-elm (NODE_ENV=local) |
npm run build |
Production build → elm/ |
14.3 Key facts
| Fact | Value |
|---|---|
| Dev server port | 8000 |
| Dev proxy target | http://cangdu.org:8001 |
| Production API host | //elm.cangdu.org |
| Image CDN | https://fuss10.elemecdn.com |
| Router mode | hash |
| Default geohash | 31.22299,121.36025 (Shanghai) |
| localStorage keys | buyCart, user_id |
| Payment | Intentionally not implemented |
| Backend repo | node-elm |
| Admin console | vue2-manage |
| License | GPL |