自学笔记--从0独立开发企业级电商系统

  • Vue全家桶实现小米商城(一)
  • 跨域CORS
    • 什么是前端跨域
    • 怎么去解决跨域
    • CORS跨域
    • JSONP跨域
      • jsonp与cors的区别:
    • 接口代理
  • 项目准备
    • 需求梳理
    • 分析目录
    • 构建项目目录
    • 安装依赖
    • 路由封装
    • Storage封装
      • cookie,localStorage,sessionStorage三者区别(分六点)?
      • 为什么要封装Storage?本身不是有api吗?
      • 手写Storage封装
      • 封装Storage完整代码
    • 接口错误拦截
    • 使用[Axios拦截器](https://www.kancloud.cn/yunye/axios/234845)
    • 设置baseURL及接口环境
      • 使用JSONP或CORS跨域以及接口代理时的baseURL
      • JSONP或CORS跨域接口的环境设置(不是接口代理的情况下统一管理baseURL)
    • Mock设置(模拟数据)
      • 使用JSON文件
      • 使用easy-mock
      • 使用本地集成Mockjs API
  • 总结接口代理,接口拦截,mock.js间的联系

Vue全家桶实现小米商城(一)

Vue全家桶实现小米商城(一)

跨域CORS

什么是前端跨域

前端独有,是浏览器为了安全而做出的限制策略。

浏览器请求必须遵循同源策略:同域名、同协议、同端口。

怎么去解决跨域

CORS跨域
JSONP跨域
代理跨域

CORS跨域

服务端设置,前端直接调用。
说明:后端允许前端某个站点进行访问。如Easy Mock:开源的公共MOCK平台,公共接口。
在axios中访问mock的接口后

    let url = 'https://www.easy-mock.com/mock/5e664553f4760626a3a312e0/example/'axios.get(url).then((data)=>{console.log(data)})

控制台查看network

响应头中:
Access-Control-Allow-Credentials:允许前端将cookie带过去
Access-Control-Allow-Origin:表示允许指定的这个网址访问mock的接口

JSONP跨域

前端适配,后台配合
说明:需要前后端同时改造

安装jsonp

npm i jsonp -S


jsonp不是请求在network中的XHR里面没有,JS里面有,是js的一段脚本。

jsonp与cors的区别:

let url2 = 'https://order.imooc.com/pay/cartorder'axios.get(url2).then((data)=>{console.log(data)})jsonp(url2,{},(err,res)=>{console.log(res)})

当使用axios时控制台输出信息,表示不是允许的网站,无Access-Control-Allow-Origin请求头。

当使用jsonp时:

接口代理

实现:通过修改nginx服务器配置来实现;
说明:前端修改,后台不动;

具体操作:
1.在整个项目项目中新增vue.config.js的配置文件
2.文件中写入:

// 开发环境下的接口代理  访问/a代理到/b,则实际访问的/b
// webpack的配置表传送给nodejs服务器
// nodejs遵循commonjs规范抛出就不用import了
module.exports = {  // 自动加载devServer中的配置表devServer:{host:'localhost',port:8080,// 代理proxy:{// 访问/pay时实现拦截转发到target中'/api':{// 目标网址,内部访问到慕课网的接口target:'https://order.imooc.com',changeOrigin:true,pathRewrite:{'/api':''}}}}
}

实现原理:

  1. 书写好config.js后重启项目,vue会自动加载此配置文件下的devServer配置表
  2. 针对proxy中做路由统一拦截(此处是统一拦截的/api),在.vue文件中定义url时都统一定义为如下:let url2 = ‘/api/pay/cartorder’,在拦截后进行访问目标地址时由**changeOrigin:true,pathRewrite:{’/api’:’ '}**自动去掉/api进行访问。
  3. 此时看似访问的localhost:8080/api/pay/cartorder实则访问的https://order.imooc.com/pay/cartorder

项目准备

需求梳理

  1. 熟悉文档、查看原型、读懂需求
  2. 了解前端设计稿-设计前端业务架构
  3. 了解后台接口文档-制定相关对接规范
  4. 协调资源
  5. 搭建前端架构

分析目录

  1. 查看原型图,分析哪些板块会封装为一个组件,进行复用。
  2. 综合分析后先搭建大的目录再去创建小的。
  3. 静态图片建议src下的assets放小的,public下放大的。
  4. src下的components就是组件

构建项目目录

  1. 初始化VScode内容:
    删除components文件夹下的最初的HelloWord组件,以及删除App.vue下对HelloWord组件的所有引用,并增加router-view的入口。
  2. 在src目录下新建api文件夹统一管理路由api
  3. src下创建util文件夹存放公共方法,如格式化,数组转换之类的方法等。
  4. src下创建storage文件夹存放对数据存值取值等的工具箱。
  5. src下创建store文件夹存放vuex的内容。
  6. src下创建router.js存放路由
  7. src下创建pages文件夹存放页面文件
  8. pages创建index.vue(首页)、home.vue(主结构、存放公共的头部和尾部),product.vue(产品栈)、detail.vue(商品详情)、orderList.vue(订单页)、order.vue(订单主结构)、orderConfirm.vue(订单确认)、cart.vue(购物车)、login.vue(登录)、orderPay.vue(支付)、alipay.vue(支付跳转中间页)
  9. components下创建NavHeader.vue公共头部NavFooter公共底部。

安装依赖

懒加载:vue-lazyload
ui库:element-ui
sass编译:node-sass sass-loader
轮播:vue-awesome-swiper
axios:vue-axios
cookie:vue-cookie

npm i vue-lazyload element-ui node-sass sass-loader vue-awesome-swiper vue-axios vue-cookie --save-dev

路由封装

由页面划分路由,找共性,拆分父子组件。
在router.js中封装所有的路由,并重定向到index

import Vue from 'vue'
import router from 'vue-router'
import Home from './pages/home.vue'
import Index from './pages/index.vue'
import Product from './pages/product.vue'
import Detail from './pages/detail.vue'
import Cart from './pages/cart.vue'
import Order from './pages/order.vue'
import OrderList from './pages/orderList.vue'
import OrderConfirm from './pages/orderConfirm.vue'
import OrderPay from './pages/orderPay.vue'Vue.use(router);export default new router({routes:[{path:'/',name:'home',component:Home,redirect:'/index',children:[{path:'/index',name:'index',component:Index},{path:'/product/:id',name:'product',component:Product},{path:'/detail/:id',name:'detail',component:Detail}]},{path:'/cart',name:'cart',component:Cart},{path:'/order',name:'order',component:Order,children:[{path:'/list',name:'order-list',cmponent:OrderList},{path:'/confirm',name:'order-confirm',cmponent:OrderConfirm},{path:'/pay',name:'order-pay',cmponent:OrderPay},{path:'alipay',name:'alipay',component:AliPay}]}]
})

在main.js中引入,并全局使用

定义index是home的子页面,在home中写入router-view用来显示子页面内容:

index.vue:

访问路由:http://localhost:8080/#/index
页面显示为:青色框为公共组件,红色为视图层,home相当于整个容器。

同理定义order页面,放置公共顶部尾部和视图:

访问路由:http://localhost:8080/#/order/list

Storage封装

cookie,localStorage,sessionStorage三者区别(分六点)?

  1. 大小:cookie 4k,storage 5m

  2. 有效期:cookie拥有有效期可以通过expires设置失效时间,不设置默认关闭浏览器即失效,localStorage永久存储需要手动清除,sessionStorage会话存储,关闭网页就清除了信息。

  3. http请求:cookie会携带在http头中,发送到服务器端,如果使用cookie保存过多数据会带来性能问题,存储在内存中,Storage只存储在浏览器端不参与和服务器的通信。

  4. 路径:Cookie有路径限制,Storage只存储在域名下。

  5. API:Cookie没有特定的API,Storage有对应的API

  • API:setItem,getItem,removeItem,clear

    1. 设置key,value
      sessionStorage.setItem(“key”, “value”);
      localStorage.setItem(“site”, “js8.in”);

    2. 通过key获取value
      var value = sessionStorage.getItem(“key”);
      var site = localStorage.getItem(“site”);

    3. 删除对应key
      sessionStorage.removeItem(“key”); localStorage.removeItem(“site”);

    4. 清除所有的key/value
      sessionStorage.clear();
      localStorage.clear();

  1. 应用场景:
    cookie:登录
    localStorage:跨页面传递参数
    sessionStorage:保存临时数据,防止用户刷新页面后丢失参数。

为什么要封装Storage?本身不是有api吗?

  1. Storage本身有API,但是只是简单地key/value形式。
  2. Storage只存储字符串,需要人工转为json对象。
  3. Storage只能一次性清空,不能单个清空。

手写Storage封装

在storage文件夹下的index.js中,抛出四个操控Storage的函数,存储值——setItem,获取值——getItem,获取整个浏览器的缓存信息——getStorage,清空某一个值——clear

  1. getStorage:
    运行项目,在Application中添加sessionStorage,注意Value中格式为JSON格式(“key”:“value”)类似于{“data”:{“name”:“DDDZ”,“age”:3,“blog”:“SuperITZ”}}

    在控制台查看对应的sessionStorage的值:

    转换为JSON:

    函数中为:
getStorage(){return JSON.parse(window.sessionStorage.getItem(STORAGR_KEY) || '{}');}
  1. setItem:
    往sessionStorage设置值,分两种情况:
  • 在对应Storage_Key下直接创建value值,如mail下直接创建data:{},id:‘001’

//storage/index.js (第二种情况返回的值:'data','this.getStorage()['data']')
setItem(key,value){//此时的val为sessionStorage的JSON格式let val = this.getStorage();//往大的模块中覆盖旧的值 this.getStorage()['data']=this.getStorage()['data']val[key] = value;//存完之后转换为字符串写入Storage信息中覆盖原来的Storage_keywindow.sessionStorage.setItem(Storage_Key,JSON.stringify(val));
},//App.vue中导入
import storage from './storage/index'
//App.vue中mounted中使用
storage.setItem('id','001')
storage.setItem('data',{})

val[key]相当于JSON.parse(window.sessionStorage.getItem(‘mail’))[‘id’] //打印001

  • 在已有的模块中再添加内容,如往data中添加username,以及userblogs(包含blog1,blog2)
//App.vue
//参数:key,value,添加到哪里的模块
storage.setItem('username','DDDZ','data')
storage.setItem('userblogs',{'blog1':'name1','blog2':'name2'
},'data')//storage/index.js
setItem(key,value,module_name){   //拿到data下面的所有值引入getItem第一种情况返回为this.getStorage()['data']let val = this.getItem(module_name);//等同于this.getStorage()['data']['username']='DDDZ'val[key] = value;//module_name成为key,val为value执行第一种情况 ('data','this.getStorage()['data']')this.setItem(module_name,val);
},
  1. getItem:
    分两种情况,一种类似于sessionStorage下获取data或者id的值,另一种类似于获取data里面userblogs的值。
    第一种:
getItem(key){return this.getStorage()[key]
}

第二种:

//如传参:('username','data')
getItem(key,module_name){//递归获取 module_name变成key this.getItem('data');到第一种情况返回this.getStorage()['data'] 为vallet val = this.getItem(module_name);//return this.getStorage()['data']['username']if(val) return val[key];
}
  1. claer:
    分两种情况,一种类似于直接删除sessionStorage下的id,一种删除data里面的userName。
    第一种:
//storage/index.js
clear(key){//获取let val = this.getStorage()//删除 this.getStorage()['id']delete val[key]//更新window.sessionStorage.setItem(Storage_Key, JSON.stringify(val))
}

第二种:

//storage/index.js
clear(key,module_name){//获取let val = this.getStorage()//this.getStorage()['data'] 判断如果没有值,返回if (!val[module_name]) return;//this.getStorage()['data']['username'] 删除delete val[module_name][key];//更新window.sessionStorage.setItem(Storage_Key, JSON.stringify(val))
}//App.vue
storage.clear('id')
storage.clear('username','data')

封装Storage完整代码

//storage/index.js
//Storage封装 如:value为{"user":{"username":"jack","age":30,"sex":1}}
const Storage_Key = 'mail';
export default {//module_name是某个模块如user//存储值//storage/index.js (第二种情况返回的值:'data','this.getStorage()['data']')setItem(key, value, module_name) {//获取某个模块下面的属性user下面的usernameif (module_name) {//拿到data下面的所有值引入getItem  this.getStorage()['data']let val = this.getItem(module_name);//this.getStorage()['data']['username']='DDDZ'//val[key]相当于JSON.parse(window.sessionStorage.getItem('mail'))['id'] //打印001val[key] = value;//module_name成为key,val为value执行第一种情况 ('data','this.getStorage()['data']')this.setItem(module_name, val);}else {//此时的val为sessionStorage的JSON格式let val = this.getStorage();//往大的模块中覆盖旧的值 this.getStorage()['data']=this.getStorage()['data']val[key] = value;//存完之后转换为字符串写入Storage信息中覆盖原来的Storage_keywindow.sessionStorage.setItem(Storage_Key, JSON.stringify(val));}},getItem(key, module_name) {if (module_name) {//递归获取 module_name变成key this.getItem('data');到第一种情况返回this.getStorage()['data'] 为vallet val = this.getItem(module_name);//return this.getStorage()['data']['username']if (val) return val[key];}//获取的user相当于(JSON.parse(window.sessionStorage.getItem("mall") || '{}'))["user"]return this.getStorage()[key];},//获取整个缓存信息getStorage() {return JSON.parse(window.sessionStorage.getItem(Storage_Key) || '{}');},clear(key, module_name) {//获取let val = this.getStorage();if (module_name) {//this.getStorage()['data'] 判断如果没有值,返回if (!val[module_name]) return;//this.getStorage()['data']['username'] 删除delete val[module_name][key];} else {//删除 this.getStorage()['id']delete val[key];}//更新window.sessionStorage.setItem(Storage_Key, JSON.stringify(val));}
}

接口错误拦截

统一报错、未登录统一拦截、请求值,返回值统一处理。

使用Axios拦截器

  1. 安装:npm install axios
  2. 在main.js引入Axios,导入vue-axios
    import axios from ‘axios’
    import VueAxios from ‘vue-axios’
  3. 挂载导入的vue-axios,方便项目中用this.axios直接使用axios不然每次使用前都要去import axios就很麻烦。
    Vue.use(VueAxios,axios)
  4. 添加拦截器interceptors,注意response.data,是响应拦截器的data,而res.data是前后端接口字段中自定义的data,state也是自定义的。
  5. 在设置status时应当做统一分类处理,比如订单一类的都是2001,2002之类的。
// 添加响应拦截器(返回值拦截)
axios.interceptors.response.use((response)=>{//获取所有的接口数据let res = response.data;//检测已经登陆,返回数据if(res.status == 0){//返回接口里面data包含的值return res.data}//检测未登录,拦截跳转到登录页else if(res.status == 10){window.location.href = '/#/login';}//登录失败,错误的提示信息else{alert(res.msg)}}, function (error) {// 对响应错误做点什么return Promise.reject(error);}
)

设置baseURL及接口环境

使用JSONP或CORS跨域以及接口代理时的baseURL

  1. 设置请求默认地址baseURL和响应超时时间

baseURL根据前端的跨域方式做调整:

JSONP或CORS跨域:后端域名和前端不一样时用baseURL用’http://完整的url地址‘ 。

接口代理:后端与前端一样时,前端在这里定义了/api后端接口也要统一为/api,后端没有/api的话可以前端在转发时统一去掉/api。拦截时接口代理中用:changeOrigin:true,pathRewrite:{’/api’:’’}去掉/api。

//接口代理
axios.defaults.baseURL = '/api';//CORS或JSONP跨域
axios.defaults.baseURL = 'https://order.imooc.com/pay/cartorder'//设置超时时间
axios.defaults.timeout = 8000;

JSONP或CORS跨域接口的环境设置(不是接口代理的情况下统一管理baseURL)

为什么要去设置?

  1. 开发上线不同阶段,需要不同的配置。
  2. 不同的跨域方式,配置不同。
  3. 打包时候统一注入环境参数,统一管理环境,输出不同的版本包。

如何去做?

  1. src文件夹下创建env.js文件
  2. 在package.json中注入环境参数,通过–mode=环境参数的值,来赋值。
"scripts": {"serve": "vue-cli-service serve --mode=development",//自定义的环境变量prev"prev":"vue-cli-service serve --mode=prev","build": "vue-cli-service build --mode=production","lint": "vue-cli-service lint"},
  1. 在env.js中去拿到环境参数:node.js中的process进程通过process.env.NODE_ENV获取当前nodejs服务器下的环境变量。
  2. 自定义环境变量xxx时要在src下加一个文件.env.xxx,并且在里面配置NODE_ENV = ‘xxx’
let baseUrl;
//根据不同的环境输出不同的url地址
//在package.json中增加 --mode=XXX参数(环境变量传给参数)
//process.env取到当前nodejs服务器下的环境变量
switch (process.env.NODE_ENV) {//开发development,测试test,生产productioncase 'development':baseUrl = '  https://dev-www.easy-mock.com/mock/5e664553f4760626a3a312e0/example/api'break;case 'test':baseUrl = 'https://test-www.easy-mock.com/mock/5e664553f4760626a3a312e0/example/api'break;case 'production':baseUrl = 'pro-https://www.easy-mock.com/mock/5e664553f4760626a3a312e0/example/api'break;//自定义环境变量prev一定要在加一个文件.env.prev,并且在里面配置case 'prev':baseUrl = 'https://pre-www.easy-mock.com/mock/5e664553f4760626a3a312e0/example/api'break;default:baseUrl = 'https://dev-www.easy-mock.com/mock/5e664553f4760626a3a312e0/example/api'break;
}export default{baseUrl
}
  1. 在main.js导入env,并使用由环境变量获取到的baseURL地址
import env from './env'
axios.defaults.baseURL = env.baseUrl;
  1. npm run serve/pre/build 就会使用不同的环境变量,从而使用不同的baseURL。

Mock设置(模拟数据)

为什么要用Mock?

  1. 开发阶段,为了高效率,需要提前Mock
  2. 减少代码冗余,灵活插拔
  3. 减少沟通,减少接口联调时间

如何创建Mock?

  1. 本地创建JSON(接口文档已经规定好)
  2. easy-mock平台
  3. 集成Mock API

使用JSON文件

public文件夹下创建mock文件夹,创建一个user文件夹存放login.json文件作为mock。

在app.vue中发起axios请求,在main.js中注释掉baseURL

浏览器可以查看到拿到的JSON Mock数据

使用easy-mock

easy-mock官网

//App.vue
this.axios.get('/user/login').then((res)=>{this.res = res
})//main.js
axios.defaults.baseURL = '换成自己在easy-mock的baseURL地址';

使用本地集成Mockjs API

  1. 搭建自己的mock平台
  2. 或者使用mockjs平台
  • 安装mockjs,在src下创建mock文件夹存放api.js,定义mock文件。
  • 使用mockjs,不会发请求但是会有数据。
//安装mockjs
npm i mockjs --S//main.js
//mockjs 开关,设置按需加载
const mock = true
if(mock){require('./mock/api')  //加载mock文件
}axios.defaults.baseURL = '/api';//App.vuethis.axios.get('/user/login').then((res)=>{this.res = res
})

查看返回的数据:

总结接口代理,接口拦截,mock.js间的联系

接口代理:在vue.config.js中其实也是一种拦截,识别到url中的/api后拦截转发到真实的接口地址获取数据

接口拦截:用axios,在main.js中添加baseURL统一接口地址的起始头为/api,添加interceptors拦截器对接口返回的数据进行判定处理,如识别到未登录、已登录、登录错误等。

mock.js:获取本地模拟数据,使用jsonp时mockjs拦截不到,访问接口返回在Network中的JS里。使用axios时,mock 开关,关闭拦截,则没有被拦截,访问接口返回真实接口数据,返回信息在Network的XHR中。mock 开关,打开拦截则URL被拦截访问本地数据

举例:

//App.vue
let url2 = '/pay/cartorder'this.axios.get(url2).then((res)=>{this.res2 = resconsole.log(res)
})//main.js
axios.defaults.baseURL = '/api';
//mock开关打开,axios发url请求时被拦截去访问./mock/api文件里面的/api/pay/cartorder下的模拟接口数据而不会去访问vue.config.js内的真实接口地址
const mock = true
if(mock){require('./mock/api')
}//vue.config.js
module.exports = {  // 自动加载devServer中的配置表devServer:{host:'localhost',port:8080,// 代理proxy:{// 访问/pay时实现拦截转发到target中'/api':{// 目标网址,内部访问到慕课网的接口target:'https://order.imooc.com',changeOrigin:true,pathRewrite:{'/api':''}}}}
}

DaZeng:Vue全家桶实现小米商城(二)相关推荐

  1. Vue开发入门(二) | 说说Vue全家桶有哪些~

    全家桶,顾名思义,就是一个系列,可以组合开发成完整强大的Vue项目 前言: *Vue两大核心思想:组件化和数据驱动. 组件化:把整体拆分为各个可以复用的个体 数据驱动:通过数据变化直接影响bom展示, ...

  2. Vue 全家桶 + Electron 开发的一个跨三端的应用

    GitHub Repo:vue-objccn Follow: halfrost · GitHub 项目地址:https://github.com/halfrost/vue-objccn 利用 Vue. ...

  3. Vue全家桶 + webpack 构建单页应用初体验

    文章指南 主题   承接这上一篇Vue + Webpack 构建模块化开发框架详解,我们知道了如何使用webpack对vue进行打包,从而开始我们的前端模块化开发之路,这一篇在上一篇的基础上讲解 Vu ...

  4. Vue 全家桶 + Electron 开发的一个跨三端的应用 1

    代码地址如下: http://www.demodashi.com/demo/11738.html GitHub Repo:vue-objccn Follow: halfrost · GitHub 利用 ...

  5. VUE全家桶项目实战-- 4.后台首页布局

    VUE全家桶项目实战-- 4.后台首页布局 一.页面布局 二.创建Home组件 三.路由index.js 文件配置主页路径 四.添加welcome组件 一.页面布局 <el-container& ...

  6. vue全家桶+Echarts+百度地图,搭建数据可视化系统

    本文章篇幅略长,内容有点多 大佬可根据目录选择性查阅 新人可一步步来阅读 1 前言 1.1 业务场景 突然接到产品说要做一个数据监控的系统.有线图.柱状图.地图,类似于数据可视化的方式. 本人之前从未 ...

  7. Vue全家桶构建项目

    步骤一.安装vue-cli 首先,我们可以通过npm安装vue-clic,前提是我们需要有node环境,如果电脑还没安装node,先安装,可通过 node -v 查询node的版本号,有版本号则已经安 ...

  8. Vue 全家桶实现一个移动端酷狗音乐

    Vue 已经用了不少时间,最近抽空把以前的未完成的酷狗音乐做完了,过来分享下,也可以直接点这里预览,注意切换成手机模式. 技术栈: vue-router.eventBus.vuex.vue-aweso ...

  9. 【 Vue全家桶 · Vue CLI(四)】Vue项目的详细目录结构解析

    文章目录 前言 -- 一级目录解析 一. dist 二. node_modules 三. public 四. src(基础版) 4.1 main.js 4.2 App.vue 4.3 src / as ...

  10. 使用Vue-cli从零开始搭建Vue全家桶(仿b站客户端)项目(1.环境配置、实现登录功能)

    1.前言         技术栈:Vue全家桶+Element.ui组件库+Axios 功能:具有登录.配置个人信息.修改个人头像.发布评论.发布动态等功能 话不多说,先看成品动图,也可点击此链接进行 ...

最新文章

  1. 【百度地图API】——如何用label制作简易的房产标签
  2. 1151 LCA in a Binary Tree (含求LCA的通法)
  3. python管理系统-员工管理系统源程序(python实现)
  4. 架构师速成8.3-架构师必须要了解的规则(转)
  5. 如何对手机使用adb
  6. 理论基础 —— 栈 —— 链栈
  7. 用html和CSS做个人简历
  8. arduino霍尔编码器蓝牙小车代码
  9. dyn_threshold 算子
  10. 『地铁交换机作用』地铁交换机用哪个国家的
  11. 淘宝2011春季校园招聘笔试试题(回忆版)(附个人简历)
  12. 【中科院信工所】-2021考研经验-记录一段每天都在思考如何学习的日子
  13. 网络游戏服务器开发(一)
  14. OtterCTF 3-4
  15. python实现词云(爬取豆瓣影评)
  16. 英特尔携手百度飞桨,共创软硬一体人工智能生态
  17. 【离散数学】数学归纳法
  18. 计算机设备更新理由,电脑硬件明明升级了,为什么速度还这么慢?四种原因在背后作怪!...
  19. 【Python】照片扩展信息提取
  20. 考研数据结构复试题目整理

热门文章

  1. xpath 爬取51job,存于excel
  2. 如何选取最佳前缀索引长度
  3. 如何搭建个人私有云盘
  4. C语言------冲突声明(conflicting declaration)
  5. 阿里云ecs服务器安装RabbitMQ
  6. ffmpeg合并mp4脚本
  7. excel学习-日期计算函数DATEDIF函数(计算相隔年数、月数、天数)
  8. Scratch软件编程等级考试一级——20220918
  9. MathType 公式编辑器公式大小调整
  10. AppCan西游汇“移动互联网创业者技术沙龙” (重庆站)