电商网站项目开发
本项目结合 webpack、Vuex、vue-router 等知识点来开发一个具有代表性的电商网站项目. 所涉及的内容涵盖了许多典型场景, 如商品类表按照价格、销量排序; 商品列表按照品牌、价格过滤; 动态的购物车; 使用优惠码等.
项目工程搭建
新建目录 shopping, 复制 webpack.config.js、webpack.prod.config.js 和 package.json 等核心文件, 并通过 NPM 完成安装. 项目的主要配置在于 main.js 文件
本项目会使用 Vue.js 的路由插件 vue-router 和 状态管理插件 Vuex, 首先在 main.js 中导入并做初始化配置,
1 | // main.js |
其中, 路由的页面配置放在了 router.js 文件内单独维护; Vuex 默认设置了 state getters mutations actions, 之后随项目需求持续增加.
项目中全局使用的一些 CSS 样式写在了 style.css 文件内, 在 main.js 中直接导入, webpack 打包时, 会将此 CSS 文件与.vue 单文件中的 CSS 一同提取, 输出到 main.css 文件.
在 shopping 目录下新建 views 目录, 用于存放每个路由页面的 .vue 文件; 新建 components 目录用来存放公共组件; 新建 images 目录用来存放项目中用到的图片. 配置完这些后, 通过 NPM 运行 npm run dev 命令, 启动 webpack 服务. 这样就完成了基础工程的搭建.
商品列表页
需求分析与模块拆分
商品列表页面用于展示相关的所有商品, 一般具有筛选和排序两种过滤方法. 比如可以按照品牌筛选或颜色筛选, 筛选条件可以叠加. 可以按照价格、销量等在筛选的基础上再进行排序, 最终过滤出符合要求的商品.
排序为单选, 初始按 “默认” 进行排序, 其中价格可以分为升序和降序两种排序, 销量则只有降序.
品牌和颜色都是单选, 单次点击选中, 再次点击取消选中.
初次打开商品类表会请求一次远程数据 ( 使用 setTimeout 模拟异步 ) 获取到的全量的商品数据, 然后筛选和排序都是在本地完成 (真实场景也有在服务端进行筛选和排序的做法, 因为商品很有可能会分页, 前端一次性拿到所有数据不现实)
商品列表主要有两个模块, 一个是路由组件(views/list.vue), 负责数据的请求、过滤相关的逻辑; 另一个是商品简介组件 (components/product.vue), 鼠标经过时, 显示出加入购物车的按钮. 这两个模块的样式都直接写在格子的 .vue 文件的 style scopen 部分.
商品简介组件
在 components 目录下新建 product.vue 文件. 每个商品的选项比较多, 比如标题、价格、颜色等, 为方便父子组件之间传递, 直接在 product.vue 中设置一个 property: info 来接收一个对象格式的数据, 这样扩展性较高, 父级也可以直接将获取到的数据传递过来, 省去了拆分的工作.
info 数据结构如下:
1 | // info |
其中 id 是商品的 id, 点击卡片会进入该商品的详情页面. name 是商品名称, brand 为品牌, sales 是销量, cost 为单价. 颜色比较特殊, 因为直接返回的中文无法对应到具体的色值, 所以在 product.vue 的 data 选项中定义一个 map, 用于映射颜色和色值.
1 | // product.vue |
鼠标悬停在卡片上时会显示 “加入购物车” 按钮.
1 | // product.vue |
router-link 最终会渲染为一个 a 标签, 链接到 :to 定义的 url, 也就是商品详情页, id 会作为参数通过 vue-router 传递
“加入购物车” 按钮对 @click 事件使用了 prevent 修饰符来阻止冒泡, 否则在点击按钮的同时, 也会点击到 a 标签进入详情页.
“加入购物车” 按钮设置了一个 handleCart 方法, 通过 Vuex 触发 mutation 保存到购物车, 参数为商品的 id
1 | // product.vue |
使用 Less 修改样式文件
1 | // product.vue |
使用 CSS 预编译的好处有很多, 比如支持变量, 封装相同样式为函数、循环等. 但是 webpack 默认是不支持的, 需要配置 less-loader.
配置 less 的方法
首先通过 NPM 安装 less 和 less-loader:
npm install less –save-dev
npm install less-loader –save-dev
然后在 webpack 中配置 less-loader
1 | module: { |
列表按照价格、销量排序
在 views 目录下新建 list.vue 文件, 并在 router.js 中添加商品类表的路由配置:
1 | // router.js |
先把数据搞定, 再来看看 list.vue. 列表相关的数据都通过 Vuex 来维护
首先需要获取商品类表的数据, 获取是异步的, 所以要写在 Vuex 的 actions 里. 在真实场景中, 数据应当是通过 Ajax 从服务器端获取的, 这里使用 setTimeout 来模拟异步, 并用本地数据来 mock.
在跟目录 shopping 下新建文件 product.js, 并写入以下数据:
1 | // product.js |
在 main.js 中导入数据, 并在 Vuex 中声明数据列表相关的 state、mutations、actions:
1 | // main.js |
首先通过 action 的 getProductList 方法获取数据, 然后由 mutation 的 setProduction 方法将数据设置到 productList.
准备好数据, 在来看视图部分. 先在跟实例 app.vue 中挂载路由并设置导航条:
1 | // app.vue |
数据 cartList 是购物车中添加的商品, 路由视图 router-view 挂载了所有的路由组件.
app.vue 的样式在 style.css 中全局定义:
1 | // style.css |
商品类表页 list.vue 在初始化时调用 Vuex 的 action 触发请求数据操作, 并设置计算属性从 Vuex 中读取数据 productList
1 | // list.vue |
打开浏览器, 现在已经可以渲染出商品类表了
实现按照价格、销量排序, 就不能直接使用数据 list, 也不能直接重置 list (因为过滤不是一次性的,所以不能破坏原数据, 否则无法复原), 所以用计算属性来动态返回过滤后的数据
1 | // list.vue |
计算属性 filteredAndOrderedList 将 list 进一步过滤, 返回筛选、排序后的数据, 排序依据于 data: order, 默认为空, 即默认的排序为 sales、cost-desc、cost-asc 时则分别按照销量、价格降序、价格升序来排序. 排序直接使用 JavaScript 数组的 sort 方法对前后两个值比较大小.
把 Product 循环的数据由 list 改为 filteredAndOrderedList 后, 显示的就是过滤后的数据. 剩余工作只要在视图中通过操作改变 order 即可.
在模板里加入排序按钮, 并绑定相关事件
1 | // list.vue |
“默认” 和 “销量” 只能单次点击, “价格” 按钮可以点击切换为升序和降序两种状态. 通过判断 order 的状态, 给 3 个按钮动态绑定了 class (.on) 来高亮显示当前排序的按钮. 刷新页面, 点击切换排序状态, 商品类表已经可以动态更新了.
列表按照品牌、颜色筛选
首先准备数据
品牌和颜色的数据可以作为 getters 从 Vuex 的 productList 里遍历获取
1 | // main.js |
使用 map 方法把 productList 里的 brand 或 color 数据过滤出来, 然后用 getFilterArray 方法对数组去重.
getters 里的 brands 和 colors 依赖数据 productList, 与计算属性原理类似, 所以只要维护好 productList、brands 和 colors 就可以自动跟新.
然后在 list.vue 中把 Vuex 里的品牌和颜色数据引入, 并完成列表的过滤.
1 | // list.vue |
品牌和颜色都是单选, 但是可以协同过滤, 最后只需要根据操作设置正确的 filterBrand 和 filterColor, 商品列表就可以自动完成对品牌、颜色的筛选以及价格、销量的排序
1 | // list.vue |
商品详情页
在 views 目录下新建 product.vue 文件, 并在 router.js 中添加商品详情的路由配制:
1 | // router.js |
商品详情的路由接收一个参数 商品的 id. 常见的业务场景中, 会以 id 作为接口的索引, 查询出所有相关的数据. 为了使业务更好的解耦, 从商品类表页跳转至详情页时, 只传递一个商品的 id, 不需要其他任何数据
通过 $route 可以获取当前路由的参数, 并在页面初始化时请求该商品的数据, 从数据源 (product.js) 里通过数组的 find() 方法拿到指定 id 的数据
1 | // views/product.vue |
然后将数据写入模板即可, 需要注意的是, 电商网站的详情页一般为自定义的文本和图片, 商家通过富文本编辑器以可视化的形式编辑好商品内容, 接口返回的是 HTML 片段, 可以直接用 v-html 指令渲染 HTML 内容, 但在服务器端要对提交的 HTML 做处理, 避免发生 XSS 攻击.
1 | // views/product.vue |
购物车
最后也是最重要的一个环节, 就是在购物车完成结算
在购物车中, 每件商品最少要选 1 件, 不过可以删除, 每件商品会有价格小计. 可以使用优惠码, 使用后在总价的基础上减少 500 元, 总价会根据购买商品的数量动态计算. 右上角的购物车入口也会显示当前购物车商品的数量.
准备数据
因为将商品加入到购物车是通过 Vuex 来完成的. 在 main.js 中, 先来定义 Vuex 中的 state 和 mutations
1 | // main.js |
数据 cartList 中保存购物车记录, 数据格式为数组, 每项是对象, 包含商品 id 和购买数量两个数据. addCart 方法接收参数为商品 id, 添加前先判断 cartList 中是否已存在商品, 存在则数量加 1, 不存在则写入
有了购物车数据, 剩余工作就是把数据显示出来, 并动态修改数据. 在 app.vue 中定义购物车入口和已添加数量
1 | // app.vue |
在 views 目录中新建 cart.vue 文件, 并添加购物车路由:
1 | // router.js |
cart.vue 中可以先准备好以下动态数据:
- Vuex 中的购物车数据 cartList
- product.js 中所有的商品数据
- 将 product.js 中的数组转换为字典 productDictList 方便快速选取
- 商品总数 countAll
- 总费用 (不含优惠码) costAll
1 | // cart.vue |
这些数据都使用了计算属性, 因此彼此互相依赖.
productDictList 是对象, key 是商品 id, value 是商品信息, 数据即为 product.js 中每项的内容, 通过 id 可以快捷地获取对应商品信息.
显示和操作数据
在下单前, 可以对每个商品的数量进行加减, 或者删除商品. 先将购物车数据 cartList 循环渲染, 并完成表格的样式.
1 | // cart.vue |
handleCount 方法用于修改购物车商品数量, 最小为 1; handleDelete 方法用于删除商品. 两者都根据接收的参数 index, 并从数据 cartList 中获取具体商品信息. 只传入 index, 而不是具体数据的好处是更灵活、便于扩展, 如果需求有所改变, 就需要修改 handleCount 里的逻辑, 不需要维护模板部分
这两个方法都交给了 Vuex 中的 mutations 来操作数据
1 | // main.js |
###
使用优惠码
优惠码功能使用到两个数据, promotionCode 和 promotion, 前者用于双向绑定输入框数据, 后者是优惠金额.
1 | // cart.vue |
应付总额是实际商品总价减去优惠的价格, 因为优惠价 promotion 默认是 0, 可以不用判断是否使用了优惠码.
下单的操作通过 Vuex 的 action 完成, 下单成功后, 清空购物车数据. 因为下单要通知服务端, 所以要在 action 内完成.
1 | // main.js |
在 action 中, 使用 setTimeout 模拟异步, 并通过返回一个 Promise 对象来通知 cart.vue 的handleOrder 购物完成
总结
在大中型项目中, 尤其是多人协同开发时, 最重要的是模块解耦. 对于公共组件, 要定义好 API, 公用数据要在 Vuex 或 bus 中统一维护. 在业务中, 要尽可能避免直接操作父链和子链来修改组件的状态, 对于跨级通信最好通过 Vuex 或 bus 完成.
在协同开发时, 可以将路由组件的内容拆分为多个组件, 由不同的人维护, 这样可以避免冲突, 是模块更清晰, 寻找 bug 也更有针对性. 公共配置还可以使用混合