codewhy-vue2.0移动电商项目回顾

在这里插入图片描述

1、前言(废话)

随着codewhy的vue2.0学习结束,想着做个项目练练手便跟着老师的视频项目学习一下思路。本篇记录一下项目的大概思路。
首先还是来划分一下目录结构,这样做项目会格外清晰,不至于找不到自己写的都是个啥。(vuecli3.0为例)
在脚手架自动搭建好之后,大目录分为:

1、node_modules文件夹(不用细究,npm install自动生成,跟node有关)
2、public文件夹(相当于vue2.0的static文件夹,一般是.html,.icno之类的文件)
3、src文件夹(这就是我们写源码的主要部分了)。接下来我们重点分析src文件夹里的目录结构。(1)assets文件夹是资源文件夹,主要放图片资源和css文件资源。(2)common文件夹主要为混入(mixin)与数据处理的函数。(3)components文件夹主要是组件所放位置。common文件夹是可以通用的组件,就是别人项目也能用。content文件夹是放具有本项目特色的组件。(4)network文件夹是存放网络请求相关的函数。(5)router则是该项目路由配置文件的项目。(6)store文件夹是vuex用来管理各组件公共变量的。(7)views文件夹则是项目各个页面进行分类编码,属于该页面的个性化组件也可以定义在该文件夹。(8)APP.vue就是进行组件展示和渲染。(9)main.js就是项目入口文件。相当于vue的入口,系统进入vue中,先起作用的就是main. js,通过main. js内的代码引导系统下一步怎么做。
4、各种配置文件

配置别名,避免重复写多余路径。

module.exports = {
    
    
  configureWebpack: {
    
    
    resolve: {
    
    
      alias: {
    
    
        'assets': '@/assets',
        'common': '@/common',
        'components': '@/components',
        'network': '@/network',
        'router': '@/router'
      }
    }
  }
 }

2、底部导航栏(TabBar)

废话不多说,直接进入正题。做这类移动商城类项目,底部导航栏写好,整个项目的框架也就大致搭建完成。
1、封装一个单独的TabBar组件,让TabBar位于底部,并且设置相关的样式(就是位于底部就可以)。
在这里需要引入一些公共的css样式文件,目前在APP.vue的style中通过@import处理。

@import url('./assets/css/base.css');

通过display:fixed布局到页面底部,然后就是弄背景色调阴影。
2、为了实现重用性和个性化,自定义TabBarItem,使其可以传入图片和文字。这就需要slot插槽的帮助。
定义一定数量的插槽,使用flex布局进行布局,在每个插槽外层包装div,用于设置插槽的样式。且每个插槽提供图片和文字两部分。
在TabBarItem里定义三个插槽,前两个是具有选择关系的图片插槽,最后一个文字插槽。

在这里插入图片描述
然后对该项目配置路由,在router文件夹下配置每个组件的路由,然后对TabBarItem进行点击事件,根据项目需要配置路由,

this. r o u t e r . r e p l a c e ( t h i s . p a t h ) (不可返回)或者 t h i s . router.replace(this.path)(不可返回) 或者 this. router.replace(this.path)(不可返回)或者this.router.push(this.path)(可返回)

这样TabBar这一部分算基本结束。

3、首页(home页面)

3.1、首页导航栏

鉴于该项目多处都用到导航栏的部分,所以在此处封装一个NavBar的组件。考虑到导航栏的个性化,同时放三个具名插槽,为左中右三部分。同时注意,为了给插槽调整样式,但如果直接给slot添加class类名,以后会出现很多奇奇怪怪的报错,所以在这里用一个div套一个slot,然后进行样式的调整。
在这里插入图片描述

3.2、首页网络数据的请求

目前vue中经常使用的网络请求是axios,但难免以后网络请求淘汰axios,而选择一个新的网络请求库。为了以后修改方便以及方便给axios修改一些默认配置,在这里选择二次封装网络请求。
在network文件夹,新建request.js(随便取)文件,对axios进行二次封装。(接口别期待找到了,需要找老师,10yuan体验)因为请求回来的数据很多,也很乱。拦截处理,返回有用的data里面的数据。

在这里插入图片描述

新建home.js,以后与home网络请求相关的都放这。(美观,也方便管理)
在这里插入图片描述
把请求下来的数据放在各个组件的data中保存,避免垃圾处理回收。

getHomeMultidata() {
    
    
      getHomeMultidata().then(res => {
    
    
        // console.log(res);
        this.banners = res.data.banner.list;
        this.recommends = res.data.recommend.list;
      });
    },

3.3、首页轮播图

这里老师有写好的,可以直接拿来用(但有一定的bug照片第一次刷新的时候,有时候出现,有时候不出现,很无语),也可以用现成的ui库(比如vant库)。但我还是建议手撸一遍源码,保证自己会写的前提下,再使用组件库。在这里,我用的是vant组件库,只把轮播图数据传进去即可。
在这里插入图片描述

3.4、首页推荐模块

这一部分很简单,继续封装成一个属于home的独立组件,然后div包装,flex布局,因为图片和文字放在一个超链接中,后面的页面直接用的人家的连接,咱也不用写,也不用管。

3.5、首页本周流行模块

老师懒省事,就是一张图片放那了,自己改改样式和布局,弄成一个组件即可。

3.6、首页商品展示模块

兄弟萌,它来了它来了,它迈着嚣张的步子走来了——better-scroll(bug-scroll,可能是我菜,反正用的不舒服)。

3.6.1、展示模块的TabControl

由于该项目,这种控制模块长得都一样,就文字不一样。这里就不采用插槽而是固定格式三个位置,父与子传递数据,传入文字数组即可。

在这里插入图片描述

吸顶效果: 老师用了一个很巧妙的方式,放两个TabControl组件,一个紧跟导航栏,一个放在推荐模块下,通过实时位置与TabControl组件位置进行比较,小于第二个TabControl组件高度,第二个出现,大于TabControl组件高度,第一个出现。

3.6.2、展示模块的数据请求与划分

对数据请求划分三类,流行,新款,精选。每一类按页进行划分与展示。

 goods: {
    
    
        pop: {
    
     page: 0, list: [] },
        new: {
    
     page: 0, list: [] },
        sell: {
    
     page: 0, list: [] }
      }

在home.js对展示数据进行按页请求


export function getHomeGoods(type,page) {
    
    
  return request({
    
    
    url:'/home/data',
    params:{
    
    
      type,
      page
    }
  })
}

在vue的生命周期函数created,一旦开始创建,就需要申请数据。
在这里插入图片描述
用于请求更多的数据
在这里插入图片描述

3.6.3、数据的展示结构

然后封装GoodsList和GoodsListItem两个组件。把对应的数据传进去进行展示,然后调样式。

GoodsList组件:



GoodsListItem组件
在这里插入图片描述

3.6.4、TabControl控制不同类型数据展示

在TabControl组件的点击事件中,发送事件,暴露点击的选项参数。然后在home组件对参数进行处理。

tabClick(index) {
    
    
      switch (index) {
    
    
        case 0:
          this.currenttype = "pop";
          break;
        case 1:
          this.currenttype = "new";
          break;
        case 2:
          this.currenttype = "sell";
          break;
      }

然后通过currenttype这个变量把对应的数据传给goodList。

<goods-list :goods="showGoods"></goods-list>


 computed: {
    
    
    showGoods() {
    
    
      return this.goods[this.currenttype].list;
    }
  }

从而实现TabControl控制不同类型数据展示。
但有一个小问题,就是两个TabControl状态无法保持一致。在TabControl的点击事件中保证两个选项相同即可。

this.$refs.tabControl1.currentIndex = index;
this.$refs.tabControl2.currentIndex = index;

3.6.5、better-scroll进行对滚动重构

2.0版本的better-scroll我个人感觉不是太好用,这里采用1.13.2版本的。通过npm install @[email protected] --save进行局部安装。同样为了避免以后滚动更换库,对better-scroll封装成一个组件scroll。

<template>
  <div ref="wrapper">
    <div class="content">
      <slot></slot>
    </div>
  </div>
</template>


import BScroll from "better-scroll";

export default {
    
    
  name: "myScroll",

  data() {
    
    
    return {
    
    
      scroll: null,
      message: "哈哈哈",
    };
  },
  props: {
    
    
    probeType: {
    
    
      type: Number,
      default: 0,
    },
    pullUpLoad: {
    
    
      type: Boolean,
      default: false
    }
  },
  //之前是mouted
  mounted() {
    
    
    // 1.创建BScroll对象
    this.scroll = new BScroll(this.$refs.wrapper, {
    
    
      observeDOM: true,
      observeImage: true,
      click: true,
      /*1不可以监听
        2 监听滚动的位置,惯性不记录
        3监听滚动的位置,惯性记录*/
      probeType: this.probeType,
      pullUpLoad: this.pullUpLoad,
      mouseWheel: true, //开启鼠标滚轮
      disableMouse: false, //启用鼠标拖动
      disableTouch: false, //启用手指触摸
    });
    //监听滚动的位置
    if (this.probeType === 2 || this.probeType === 3) {
    
    
      this.scroll.on("scroll", (position) => {
    
    
        this.$emit("scroll", position);
      });
    }
    if (this.pullUpLoad) {
    
    
      this.scroll.on("pullingUp", () => {
    
    
        this.$emit("pullingUp");
      })
    }

绿色部分是包装器,也称为父容器,具有固定的高度。黄色部分是内容,它是父容器的第一个子元素,其高度会随着内容的大小而增长。然后,当内容的高度不超过父容器的高度时,内容不会滚动。一旦超过,内容可以滚动。这就是 BetterScroll 的原理。

所以我们在使用better-scroll的时候,一定要对wrapper这个div设置一个固定高度。设置固定高度有两种办法。
在这里插入图片描述
或者

.content {
    
    
	height:calc(100% - 44px - 49px);
	overflow:hidden;
	margin-top:44px;	
}

但在这里我们往往会出现无法滚动的bug,这是因为对于better-scroll如果您的内容元素包含非固定大小的图像,则必须调用该refresh()方法以确保在加载图像后正确计算高度。
这里有一个小tips,在vue中可以用@load来监听是否图片加载完成,后面可以绑定一个函数<img v-lazy="showImage" alt="" @load="imageLoad">

在这里插入图片描述
通过refresh()重新计算一下高度即可。

3.6.6、首页回到顶部小按钮

效果:当高度下降到一定位置右下方出现一个小按钮,点击可以回到顶部。
Alt
继续封装啊,兄弟们,封装一个backTop的组件,里面设置样式,通过fixed设置布局,使其固定在页面上。正常情况点击backtop小组件会调用this.$refs.scroll.scrollTo(x坐标,y坐标,返回用时毫秒);返回顶部,然而scroll和backtop都是home的一个组件彼此互相通信不太方便(当然可以使用事件总线 $ bus),在这时候直接选择对组件进行click监听,但一般情况click是无法监听组件的,这就用到了v-on的修饰符——.native:监听组件根元素的原生事件。然后通过this.$refs.scroll拿到scroll对象,最后返回顶部。
而想实现backTop在一定高度消失和出现,就需要用到better-scroll内部的一个方法。

 if (this.probeType === 2 || this.probeType === 3) {
    
    
      this.scroll.on("scroll", (position) => {
    
    
        this.$emit("scroll", position);
      });
    }

将实时位置发送给home组件,通过this.tapOffsetTop = this.$refs.tabControl2.$el.offsetTop; 获取组件的位置,从而与实时位置进行比较,然后通过v-show对其进行展示。

3.6.7、首页上拉加载更多

同样借用了better-scroll里的一个方法。为实现个性化,有的需要下拉加载更多,有的不需要,根据父元素传的pullUpLoad决定

 if (this.pullUpLoad) {
    
    
      this.scroll.on("pullingUp", () => {
    
    
        this.$emit("pullingUp");
      })
    }

一旦到底,向home发送一个pullingUp事件,与loadMore进行绑定(自定义)。

loadMore() {
    
    
      this.getHomeGoods(this.currenttype);
    },


getHomeGoods(type) {
    
    
      const page = this.goods[type].page + 1;
      getHomeGoods(type, page).then(res => {
    
    
        this.goods[type].list.push(...res.data.list);
        this.goods[type].page += 1;
        //完成上拉加载更多,保障下一次下拉加载更多
        this.$refs.scroll.finishPullUp();
      });
    }

3.6.8、home组件离开时记录状态和位置

通常保存历史状态用keep-alive即可,详情页

<keep-alive exclude="Detail">
      <router-view />
    </keep-alive>

但幸好有“bug-scroll”,导致出现状态无法保存。这里就要借用vue生命周期钩子函数之activated和deactivated,在组件离开时,记录y的位置,当组件回来时,直接跳转到离开的位置(但注意不要设置为0),这里离开的位置可以通过this.$refs.scroll.scroll.y来获取。当回归后,一般强制刷新一下。

activated() {
    
    
    this.$refs.scroll.scrollTo(0,this.saveY,1);
    this.$refs.scroll.refresh();
  },
  deactivated(){
    
    
    this.saveY = this.$refs.scroll.scroll.y;
  },

4、详情页

4.1、跳转到详情页并且携带商品id

首先需要配置详情页detail的路由,保证可以进行跳转。

 {
    
    
    path: '/detail/:id',
    component: Detail
  }

因为需要传参数,此处采用动态路由。

this.$router.push('/detail/' + this.goodsItem.iid);

但也可以用query方法。

this.$router.push({
    
    
path: '/detail',
query:{
    
    
	id: this.goodsItem.iid
	}
})

4.2、详情页导航栏

因为此处导航栏没首页那么简单,所以继续封。对居中的插槽,和左边插槽进行编写。
在这里插入图片描述

<div class="product-detail-nav-bar">
    <nav-bar>
      <div @click="backHomePage" class="back-img" slot="left">
        <img alt="" src="@/assets/img/common/back.svg" />
      </div>
      <div class="title" slot="center">
        <span
          :class="{ active: index === currentIndex }"
          :key="index"
          @click="titleItemClick(index)"
          class="title-item"
          v-for="(item, index) in titles"
        >
          {
    
    {
    
     item }}
        </span>
      </div>
    </nav-bar>
  </div>

4.3、详情页数据请求与存储

商品的详细信息:

import {
    
     request } from "./request";

export function getDetail(iid){
    
    
  return request({
    
    
    url:'/detail',
    params:{
    
    
      iid
    }
  })
}

将数据以类的方式分类保存。

export class Goods {
    
    
  constructor(itemInfo, columns, services) {
    
    
    this.title = itemInfo.title;
    this.desc = itemInfo.desc;
    this.newPrice = itemInfo.price;
    this.lowNowPrice = itemInfo.lowNowPrice;
    this.oldPrice = itemInfo.oldPrice;
    this.discount = itemInfo.discountDesc;
    this.discountBgColor = itemInfo.discountBgColor;
    this.columns = columns;
    this.services = services;
    this.relPrice = itemInfo.lowNowPrice;
  }
}

// 店铺数据
export class Shop {
    
    
  constructor(shopInfo) {
    
    
    this.logo = shopInfo.shopLogo;
    this.name = shopInfo.name;
    this.fans = shopInfo.cFans;
    this.sells = shopInfo.cSells;
    this.score = shopInfo.score;
    this.goodsCount = shopInfo.cGoods;
    this.shopurl = shopInfo.allGoodsUrl;
  }
}


// 尺寸数据
export class GoodsParams {
    
    
  constructor(info, rule) {
    
    
    // 注: images可能没有值(某些商品有值, 某些没有值)
    this.image = info.images ? info.images[0] : "";
    this.infos = info.set;
    this.sizes = rule.tables;
  }
}

然后将数据进行保存。

//根据iid请求所需要的数据
      getDetail(this.iid).then(res => {
    
    
        console.log(res);
        const data = res.result;
        //获取轮播图数据
        this.topImages = data.itemInfo.topImages;
        //获取商品信息
        this.goods = new Goods(data.itemInfo, data.columns, data.shopInfo.services);
        //获取店铺信息
        this.shop = new Shop(data.shopInfo);
        //获取商品图片详细信息
        this.detailInfo = data.detailInfo;
        //获取参数信息
        this.paramInfo = new GoodsParams(data.itemParams.info, data.itemParams.rule);

        // 获取评论数据
        if (data.rate.cRate !== 0) {
    
    
          this.commentInfo = data.rate.list[0] || {
    
    };
        }
      })getRecommend().then(res => {
    
    
        console.log(res);
        this.recommend = res.data.list
      })

4.4、详情页轮播图

这里老师的swiper封装的组件有问题,在这里我用的是vant组件库,把上述请求到的topImages数据传到轮播图组件,然后进行渲染。

<detail-swiper :top-images="topImages" class="detail-set-scroll"></detail-swiper>
<van-swipe :autoplay="2500" indicator-color="#ff0000" class="swiper-list" duration="400">
      <van-swipe-item :key="index" v-for="(item, index) in topImages">
        <img :src="item" @load="swiperLoad" alt="" />
      </van-swipe-item>
    </van-swipe>

4.5、详情页基本信息展示

这一部分,包括商品的基本信息,店铺信息,商品详情数据,商品参数信息,商品推荐信息的展示。都是一个思路,封装组件,把分好类的数据分别传入,改样式展现数据。

4.6、导入scroll组件

把已经封装好的scroll组件导入,注意一定要给scroll一个固定高度。

.scroll-height {
    
    
  position: absolute;
  top: 44px;
  right: 0;
  bottom: 50px;
  left: 0;
  overflow: hidden;
  width: 100%;
  background-color: white;
}

4.7、标题与内容的联动效果

点击标题滚动到相应位置:
我们可以通过detail中监听标题的点击,获取index,然后在点击事件中滚动到对应的内容,所以如何获取各部分的内容成为了难题。

  • 如何正确获取各部分内容的offsetTop
  • 1、在created中,不可以,此时的dom元素都没挂载,this.$refs.comment.$el根本不存在。
  • 2、在mounted中,不可以,此时仅仅简单挂载dom元素,还无法获取数据。
  • 3、在updated中,不可以,获取到数据的回调也不行,dom还没渲染完。
  • 4、在$nextTick中,不可以,dom渲染完了,但图片的高度可能无法计算在内。
  • 5、最终选择,在图片加载完成的监听中,进行赋值,因为频率过高,选择防抖来进行处理。
created(){
    
    
this.getThemeTopY = debounce(() => {
    
    
      this.themeTopYs = [];
      this.themeTopYs.push(0);
      this.themeTopYs.push(this.$refs.params.$el.offsetTop);
      this.themeTopYs.push(this.$refs.comment.$el.offsetTop);
      this.themeTopYs.push(this.$refs.recommend.$el.offsetTop);
      this.themeTopYs.push(Number.MAX_VALUE)
      console.log(this.themeTopYs);
    }, 250)
  },
methods:{
    
    
	imgLoad() {
    
    
      	this.$refs.scroll.refresh();
      	this.getThemeTopY();
    },
}

Number.MAX_VALUE是极大值的意思。

滚动内容,标题相应选中:

contentScroll(position) {
    
    
      this.isShowBackTop = (-position.y) > 1000;
      const positionY = -position.y;
      const length = this.themeTopYs.length;
      for (let i = 0; i < length - 1; i++) {
    
    
        //这里的i是string类型
        // if (this.currentIndex !== i && (i < length - 1 && positionY >= this.themeTopYs[i] && positionY < this.themeTopYs[i + 1]) ||
        //   (i === length - 1 && positionY > this.themeTopYs[i])) {
    
    
        //   this.currentIndex = i;
        //   this.$refs.nav.currentIndex = this.currentIndex;
        // }
        if (this.currentIndex !== i && (positionY >= this.themeTopYs[i] && positionY < this.themeTopYs[i + 1])) {
    
    
          this.currentIndex = i;
          this.$refs.nav.currentIndex = this.currentIndex;

        }
      }
    },

4.8、详情页底部工具栏

在这里我用的是vant组件库,继续封装。

<div class="bottom-bar">
    <van-goods-action>
      <van-goods-action-icon icon="chat-o" text="客服" />
      <!-- :badge="cartLength" -->
      <van-goods-action-icon icon="shop-o" text="店铺" />
      <van-goods-action-icon :color="isShouCang ? '#ff5000' : '#000000'" :icon="isShouCang ? 'star' : 'star-o'"
        :text="isShouCang ? '已收藏' : '收藏'" @click="starClick" />
      <van-goods-action-button @click="addToCart" text="加入购物车" type="warning" />
      <van-goods-action-button @click="$router.push('/shopcart')" text="立即购买" type="danger" />
      
    </van-goods-action>
  </div>

4.9、加入回到顶部的按钮

因为backTop组件已经封装好,在其他很多组件都会复用,代码格式都一样,为了避免代码的重复性,这里使用mixin混入。

export const backTopMixin = {
    
    
  components:{
    
    
    backTop
  },
  data(){
    
    
    return {
    
    
      isShowBackTop: false
    }
  },
  methods:{
    
    
    backClick() {
    
    
      this.$refs.scroll.scrollTo(0, 0, 500);
    },
  }
}

5、购物车页面

5.1、将商品添加到购物车

通过监听底部导航栏加入购物车选项的监听,来把数据传给购物车。

addToCart(){
    
    
      //获取购物车需要获取的信息
      const product = {
    
    };
      product.image = this.topImages[0];
      product.title = this.goods.title;
      product.desc = this.goods.desc;
      product.price = this.goods.relPrice;
      product.iid = this.iid;
      //将商品添加到购物车
      //this.$store.commit('addCart',product)
      this.$store.dispatch("addCart",product).then(res => {
    
    
        this.$toast.show(res);
      })
    }

由于详情页的子组件与购物车组件不方便传递,一般使用vuex来进行管理。

export default {
    
    
  addCounter(state,payload){
    
    
    payload.count++;
  },
  addToCart(state,payload){
    
    
    payload.checked = false;
    state.cartList.push(payload);
  },
  clearCarList(state) {
    
    
    // 判断选中哪些数据,过滤没选中的数组返回一个新数组即可
    let result = state.cartList.filter(item => item.checked !== true);
    if (result.length === 0) {
    
    
      localStorage.removeItem("cartList");
      state.cartList = [];
    } else {
    
    
      state.cartList = result;
      localStorage.setItem("cartList", JSON.stringify(state.cartList));
    }
  },
  setCartList(state, data) {
    
    
    state.cartList = data;
  }
}

5.2、购物车导航栏

有一个知识点需要注意,就是…mapGetters。

computed: {
    
    
    //使用mapGetters可以让getters中的方法自动转换到计算属性一共两种用法
    //不起别名
    //...mapGetters(['cartLength','cartList']),
    //起别名
    ...mapGetters({
    
    
      length: 'cartLength',
      list: 'cartList'
    })
  }

5.3、商品列表展示

同之前,封装,调样式。

5.4、底部工具栏

计算总价钱

totalPrice() {
    
    
      return this.$store.state.cartList
        .filter(item => item.checked)
        .reduce((preValue, item) => preValue += item.price * item.count, 0)
        .toFixed(2);
    },

全选的双向绑定
通过判断每一个添加的商品的checked属性,如果全部选中,则全选框选中。点击全选框,则选中商品的checked属性与全选框的属性保持一致。

5.5、Toast(吐司)插件模式

这里同样先弄一个Toast组件,但不是常规的导入了,而是要把他弄成一个插件。第一步先封装一个Toast组件。
在这里插入图片描述
然后建立Toast.js文件,保证main.js导入的时候,Toast.js也可以导入。

import toast from './components/common/toast'

Vue.use(toast);

在Toast.js内部,对安装函数进行重构。

import Toast from "./Toast.vue";

const obj = {
    
    }

// 将对象安装到Vue上
obj.install = function (Vue) {
    
    
  // 1.创建组件构造器
  const toastConstructor = Vue.extend(Toast);

  // 2.new的方式,根据组件构造器,可以创建出来一个组件对象
  const toast = new toastConstructor();

  // 3.将组件对象手动挂载到某个元素上
  toast.$mount(document.createElement("div"));

  // 4.myToast.$el就已经挂载到上面创建的div了,然后将div挂载到body上即可
  document.body.appendChild(toast.$el);

  // 最后将myToast挂载到Vue的原型上
  Vue.prototype.$toast = toast;
}

export default obj

6、一些优化的方面

6.1、解决移动端点击300ms延迟

进行安装

npm install fastclick --save

进行使用,在main.js文件里。

FastClick.attach(document.body);

6.2、图片懒加载

图片懒加载就是当图片出现在视图再进行加载。

Lazyload官方文档

官方讲的很明白。这里最简单的使用
安装


$ npm i vue-lazyload -S

导入

import VueLazyload from 'vue-lazyload'

Vue.use(Lazyload, {
    
    
  // 未加载的占位图片
  loading: require("@/assets/img/common/placeholder.png")
})

7、大结局

本来准备随便写写的,结果洋洋洒洒写了这么多。做这个项目bug不少,但是根据弹幕还是可以解决的,大部分老师都带着解决。注意弹幕!注意弹幕!注意弹幕!另外,还有两个部分,老师没做,所以不适合放简历上,但有兴趣的大佬可以补了。此文章供个人以后的移动电商的思路加上第一次写,可能有很多部分写的不清楚,实在抱歉!

猜你喜欢

转载自blog.csdn.net/m0_59722204/article/details/127038544