小程序名称:一起打车吧

项目地址:

  • 客户端:https://github.com/jrainlau/taxi-together-client

  • 服务端:https://github.com/jrainlau/taxi-together-server

小程序二维码:

经过为期两个晚上下班时间的努力,终于把我第一个小程序开发完成并发布上线了。整个过程还算顺利,由于使用了 mpvue方案进行开发,故可以享受和 vue一致的流畅开发体验;后台系统使用了 python3+ flask框架进行,使用最少的代码完成了小程序的后台逻辑。

除了开发之外,还实实在在地体验了一把微信小程序的开发流程,包括开发者工具的使用、体验版的发布、上线的申请等等。这些开发体验都非常值得被记录下来,于是便趁热打铁,写下这篇文章。

一、需求&功能

由于公司里有相当多的同事都住在同一个小区,所以上下班的时候经常会在公司群里组织拼车。但是由于完全依赖聊天记录,且上下班拼车的同事也很多,依赖群聊很容易把消息刷走,而且容易造成信息错乱。既然如此,那么完全可以开发一个小工具把这些问题解决。

发起拼车的人把出发地点、目的地点、打车信息以卡片的形式分享出来,参与拼车的人点击卡片就能选择参加拼车,并且能看到同车拼友是谁,拼单的信息等等内容。

交互流程如下:

可以看到,逻辑是非常简单的,我们只需要保证生成拼单、分享拼单、进入拼单和退出拼单这四个功能就好。

需求和功能已经确定好,首先按照小程序官网的介绍,注册好小程序并拿到 appId,接下来可以开始进行后台逻辑的开发。

二、后台逻辑开发

由于时间仓促,功能又简单,所以并没有考虑任何高并发等复杂场景,仅仅考虑功能的实现。从需求的逻辑可以知道,其实后台只需要维护两个列表,分别存储当前所有拼车单以及当前所有参与了拼车的用户即可,其数据结构如下:

当前所有拼单列表 billsList

当前所有参与了拼车的用户列表 inBillUsers

当用户确定并分享了一个拼单之后,会直接新建一个拼单,同时把该用户添加到当前所有参与了拼车的用户列表列表里面,并且添加到该拼单的成员列表当中:

只要维护好这两个列表,接下来就是具体的业务逻辑了。

为了快速开发,这里我使用了 python3+ flask框架的方案。不懂 python的读者看到这里也不用紧张,代码非常简单且直白,看看也无妨。

首先新建一个 BillController类:

  1. class BillController:

  2.    billsList = []

  3.    inBillUsers = []

接下来会在这个类的内部添加创建拼单获取拼单参与拼单退出拼单判断用户是否在某一拼单中图片上传的功能。

1、获取拼单 getBill()

该方法接收客户端传来的拼单ID,然后拿这个ID去检索是否存在对应的拼单。若存在则返回对应的拼单,否则报错给客户端。

  1.    def getBill(self, ctx):

  2.        ctxBody = ctx.form

  3.        billId = ctxBody['billId']

  4.        try:

  5.            return response([item for item in self.billsList if item['billId'] == billId][0])

  6.        except IndexError:

  7.            return response({

  8.                'errMsg': '拼单不存在!',

  9.                'billsList': self.billsList,

  10.            }, 1)

2、创建拼单 createBill()

该方法会接收来自客户端的用户信息拼单信息,分别添加到 billsListinBillUsers当中。

  1.    def createBill(self, ctx):

  2.        ctxBody = ctx.form

  3.        user = {

  4.            'userId': ctxBody['userId'],

  5.            'billId': ctxBody['billId'],

  6.            'name': ctxBody['name'],

  7.            'avatar': ctxBody['avatar']

  8.        }

  9.        bill = {

  10.            'billId': ctxBody['billId'],

  11.            'from': ctxBody['from'],

  12.            'to': ctxBody['to'],

  13.            'time': ctxBody['time'],

  14.            'members': [user]

  15.        }

  16.        if ctxBody['userId'] in [item['userId'] for item in self.inBillUsers]:

  17.            return response({

  18.                'errMsg': '用户已经在拼单中!'

  19.            }, 1)

  20.        self.billsList.append(bill)

  21.        self.inBillUsers.append(user)

  22.        return response({

  23.            'billsList': self.billsList,

  24.            'inBillUsers': self.inBillUsers

  25.        })

创建完成后,会返回当前的 billsListinBillUsers到客户端。

3、参与拼单 joinBill()

接收客户端传来的用户信息拼单ID,把用户添加到拼单和 inBillUsers列表中。

  1.    def joinBill(self, ctx):

  2.        ctxBody = ctx.form

  3.        billId = ctxBody['billId']

  4.        user = {

  5.            'userId': ctxBody['userId'],

  6.            'name': ctxBody['name'],

  7.            'avatar': ctxBody['avatar'],

  8.            'billId': ctxBody['billId']

  9.        }

  10.        if ctxBody['userId'] in [item['userId'] for item in self.inBillUsers]:

  11.            return response({

  12.                'errMsg': '用户已经在拼单中!'

  13.            }, 1)

  14.        theBill = [item for item in self.billsList if item['billId'] == billId]

  15.        if not theBill:

  16.            return response({

  17.                'errMsg': '拼单不存在'

  18.            }, 1)

  19.        theBill[0]['members'].append(user)

  20.        self.inBillUsers.append(user)

  21.        return response({

  22.            'billsList': self.billsList,

  23.            'inBillUsers': self.inBillUsers

  24.        })

4、退出拼单 leaveBill()

接收客户端传来的用户ID拼单ID,然后删除掉两个列表里面的该用户。

这个函数还有一个功能,如果判断到这个拼单ID所对应的拼单成员为空,会认为该拼单已经作废,会直接删除掉这个拼单以及所对应的车辆信息图片。

  1.    def leaveBill(self, ctx):

  2.        ctxBody = ctx.form

  3.        billId = ctxBody['billId']

  4.        userId = ctxBody['userId']

  5.        indexOfUser = [i for i, member in enumerate(self.inBillUsers) if member['userId'] == userId][0]

  6.        indexOfTheBill = [i for i, bill in enumerate(self.billsList) if bill['billId'] == billId][0]

  7.        indexOfUserInBill = [i for i, member in enumerate(self.billsList[indexOfTheBill]['members']) if member['userId'] == userId][0]

  8.        # 删除拼单里面的该用户

  9.        self.billsList[indexOfTheBill]['members'].pop(indexOfUserInBill)

  10.        # 删除用户列表里面的该用户

  11.        self.inBillUsers.pop(indexOfUser)

  12.        # 如果拼单里面用户为空,则直接删除这笔拼单

  13.        if len(self.billsList[indexOfTheBill]['members']) == 0:

  14.            imgPath = './imgs/' + self.billsList[indexOfTheBill]['img'].split('/getImg')[1]

  15.            if os.path.exists(imgPath):

  16.                os.remove(imgPath)

  17.            self.billsList.pop(indexOfTheBill)

  18.        return response({

  19.            'billsList': self.billsList,

  20.            'inBillUsers': self.inBillUsers

  21.        })

5、判断用户是否在某一拼单中 inBill()

接收客户端传来的用户ID,接下来会根据这个用户ID去 inBillUsers里面去检索该用户所对应的拼单,如果能检索到,会返回其所在的拼单。

  1.    def inBill(self, ctx):

  2.        ctxBody = ctx.form

  3.        userId = ctxBody['userId']

  4.        if ctxBody['userId'] in [item['userId'] for item in self.inBillUsers]:

  5.            return response({

  6.                'inBill': [item for item in self.inBillUsers if ctxBody['userId'] == item['userId']][0],

  7.                'billsList': self.billsList,

  8.                'inBillUsers': self.inBillUsers

  9.            })

  10.        return response({

  11.            'inBill': False,

  12.            'billsList': self.billsList,

  13.            'inBillUsers': self.inBillUsers

  14.        })

6、图片上传 uploadImg()

接收客户端传来的拼单ID图片资源,先存储图片,然后把该图片的路径写入对应拼单ID的拼单当中。

  1.    def uploadImg(self, ctx):

  2.        billId = ctx.form['billId']

  3.        file = ctx.files['file']

  4.        filename = file.filename

  5.        file.save(os.path.join('./imgs', filename))

  6.        # 把图片信息挂载到对应的拼单

  7.        indexOfTheBill = [i for i, bill in enumerate(self.billsList) if bill['billId'] == billId][0]

  8.        self.billsList[indexOfTheBill]['img'] = url_for('getImg', filename=filename)

  9.        return response({

  10.            'billsList': self.billsList

  11.        })

完成了业务逻辑的功能,接下来就是把它们分发给不同的路由了:

  1. @app.route('/create', methods = ['POST'])

  2. def create():

  3.    return controller.createBill(request)

  4. @app.route('/join', methods = ['POST'])

  5. def join():

  6.    return controller.joinBill(request)

  7. @app.route('/leave', methods = ['POST'])

  8. def leave():

  9.    return controller.leaveBill(request)

  10. @app.route('/getBill', methods = ['POST'])

  11. def getBill():

  12.    return controller.getBill(request)

  13. @app.route('/inBill', methods = ['POST'])

  14. def inBill():

  15.    return controller.inBill(request)

  16. @app.route('/uploadImg', methods = ['POST'])

  17. def uploadImg():

  18.    return controller.uploadImg(request)

  19. @app.route('/getImg/<filename>')

  20. def getImg(filename):

  21.  return send_from_directory('./imgs', filename)

完整的代码可以直接到仓库查看,这里仅展示关键的内容。

三、前端业务开发

前端借助 vue-cli直接使用了mpvue的mpvue-quickstart来初始化项目,具体过程不再细述,直接进入业务开发部分。

首先,微信小程序的API都是callback风格,为了使用方便,我把用到的小程序API都包装成了 Promise,统一放在 src/utils/wx.js内部,类似下面这样:

  1. export const request = obj => new Promise((resolve, reject) => {

  2.  wx.request({

  3.    url: obj.url,

  4.    data: obj.data,

  5.    header: { 'content-type': 'application/x-www-form-urlencoded', ...obj.header },

  6.    method: obj.method,

  7.    success (res) {

  8.      resolve(res.data.data)

  9.    },

  10.    fail (e) {

  11.      console.log(e)

  12.      reject(e)

  13.    }

  14.  })

  15. })

1、注册全局Store

由于开发习惯,我喜欢把所有接口请求都放在store里面的 actions当中,所以这个小程序也是需要用到 Vuex。但由于小程序每一个Page都是一个新的Vue实例,所以按照Vue的方式,用全局 Vue.use(Vuex)是不会把 $store注册到实例当中的,这一步要手动来。

src/目录下新建一个 store.js文件,然后在里面进行使用注册:

  1. import Vue from 'vue'

  2. import Vuex from 'vuex'

  3. Vue.use(Vuex)

  4. export default new Vuex.Store({})

接下来在 src/main.js当中,手动在Vue的原型里注册一个 $store

  1. import Vue from 'vue'

  2. import App from './App'

  3. import Store from './store'

  4. Vue.prototype.$store = Store

这样,以后在任何的Page里都可以通过 this.$store来操作这个全局Store了。

2、构建好请求的API接口

和后台系统的逻辑对应,前端也要构造好各个请求的API接口,这样的做法能够避免把API逻辑分散到页面四处,具有清晰、易维护的优势。

  1.    /**

  2.     * @param  {} {commit}

  3.     * 获取用户公开信息

  4.     */

  5.    async getUserInfo ({ commit }) {

  6.      const { userInfo } = await getUserInfo({

  7.        withCredenitals: false

  8.      })

  9.      userInfo.avatar = userInfo.avatarUrl

  10.      userInfo.name = userInfo.nickName

  11.      userInfo.userId = encodeURIComponent(userInfo.nickName + userInfo.city + userInfo.gender + userInfo.country)

  12.      commit('GET_USER_INFO', userInfo)

  13.      return userInfo

  14.    },

  15.    /**

  16.     * @param  {} {commit}

  17.     * @param  { String } userId 用户ID

  18.     * 检查用户是否已经存在于某一拼单中

  19.     */

  20.    async checkInBill ({ commit }, userId) {

  21.      const res = await request({

  22.        method: 'post',

  23.        url: `${apiDomain}/inBill`,

  24.        data: {

  25.          userId

  26.        }

  27.      })

  28.      return res

  29.    },

  30.    /**

  31.     * @param  {} {commit}

  32.     * @param  { String } userId 用户ID

  33.     * @param  { String } name   用户昵称

  34.     * @param  { String } avatar 用户头像

  35.     * @param  { String } time   出发时间

  36.     * @param  { String } from   出发地点

  37.     * @param  { String } to     目的地点

  38.     * @param  { String } billId 拼单ID

  39.     * 创建拼单

  40.     */

  41.    async createBill ({ commit }, { userId, name, avatar, time, from, to, billId }) {

  42.      const res = await request({

  43.        method: 'post',

  44.        url: `${apiDomain}/create`,

  45.        data: {

  46.          userId,

  47.          name,

  48.          avatar,

  49.          time,

  50.          from,

  51.          to,

  52.          billId

  53.        }

  54.      })

  55.      commit('GET_BILL_INFO', res)

  56.      return res

  57.    },

  58.    /**

  59.     * @param  {} {commit}

  60.     * @param  { String } billId 拼单ID

  61.     * 获取拼单信息

  62.     */

  63.    async getBillInfo ({ commit }, billId) {

  64.      const res = await request({

  65.        method: 'post',

  66.        url: `${apiDomain}/getBill`,

  67.        data: {

  68.          billId

  69.        }

  70.      })

  71.      return res

  72.    },

  73.    /**

  74.     * @param  {} {commit}

  75.     * @param  { String } userId 用户ID

  76.     * @param  { String } name   用户昵称

  77.     * @param  { String } avatar 用户头像

  78.     * @param  { String } billId 拼单ID

  79.     * 参加拼单

  80.     */

  81.    async joinBill ({ commit }, { userId, name, avatar, billId }) {

  82.      const res = await request({

  83.        method: 'post',

  84.        url: `${apiDomain}/join`,

  85.        data: {

  86.          userId,

  87.          name,

  88.          avatar,

  89.          billId

  90.        }

  91.      })

  92.      return res

  93.    },

  94.    /**

  95.     * @param  {} {commit}

  96.     * @param  { String } userId 用户ID

  97.     * @param  { String } billId 拼单ID

  98.     * 退出拼单

  99.     */

  100.    async leaveBill ({ commit }, { userId, billId }) {

  101.      const res = await request({

  102.        method: 'post',

  103.        url: `${apiDomain}/leave`,

  104.        data: {

  105.          userId,

  106.          billId

  107.        }

  108.      })

  109.      return res

  110.    },

  111.    /**

  112.     * @param  {} {commit}

  113.     * @param  { String } filePath 图片路径

  114.     * @param  { String } billId   拼单ID

  115.     * 参加拼单

  116.     */

  117.    async uploadImg ({ commit }, { filePath, billId }) {

  118.      const res = await uploadFile({

  119.        url: `${apiDomain}/uploadImg`,

  120.        header: {

  121.          'content-type': 'multipart/form-data'

  122.        },

  123.        filePath,

  124.        name: 'file',

  125.        formData: {

  126.          'billId': billId

  127.        }

  128.      })

  129.      return res

  130.    }

3、填写拼单并实现分享功能实现

新建一个 src/pages/index目录,作为小程序的首页。

该首页的业务逻辑如下:

  1. 进入首页的时候先获取用户信息,得到userId

  2. 然后用userId去请求判断是否已经处于拼单

  3. 若是,则跳转到对应拼单Id的详情页

  4. 若否,才允许新建拼单

onShow的生命周期钩子中实现上述逻辑:

  1.  async onShow () {

  2.    this.userInfo = await this.$store.dispatch('getUserInfo')

  3.    const inBill = await this.$store.dispatch('checkInBill', this.userInfo.userId)

  4.    if (inBill.inBill) {

  5.      wx.redirectTo(`../join/main?billId=${inBill.inBill.billId}&fromIndex=true`)

  6.    }

  7.  },

当用户填写完拼单后,会点击一个带有 open-type="share"属性的button,然后会触发 onShareAppMessage生命周期钩子的逻辑把拼单构造成卡片分享出去。当分享成功后会跳转到对应拼单ID的参加拼单页。

  1.  onShareAppMessage (result) {

  2.    let title = '一起拼车'

  3.    let path = '/pages/index'

  4.    if (result.from === 'button') {

  5.      this.billId = 'billId-' + new Date().getTime()

  6.      title = '我发起了一个拼车'

  7.      path = `pages/join/main?billId=${this.billId}`

  8.    }

  9.    return {

  10.      title,

  11.      path,

  12.      success: async (res) => {

  13.        await this.$store.dispatch('createBill', { ...this.userInfo, ...this.billInfo })

  14.        // 上传图片

  15.        await this.$store.dispatch('uploadImg', {

  16.          filePath: this.imgSrc,

  17.          billId: this.billId

  18.        })

  19.        // 分享成功后,会带着billId跳转到参加拼单页

  20.        wx.redirectTo(`../join/main?billId=${this.billId}`)

  21.      },

  22.      fail (e) {

  23.        console.log(e)

  24.      }

  25.    }

  26.  },

4、参与拼单&退出拼单功能实现

新建一个 src/pages/join目录,作为小程序的“参加拼单页”。

该页面的运行逻辑如下:

  1. 首先会获取从url里面带来的billId

  2. 其次会请求一次userInfo,获取userId

  3. 然后拿这个userId去检查该用户是否已经处于拼单

  4. 如果已经处于拼单,那么就会获取一个新的billId代替从url获取的

  5. 拿当前的billId去查询对应的拼单信息

  6. 如果billId都无效,则redirect到首页

由于要获取url携带的内容,亲测 onShow()是不行的,只能在 onLoad()里面获取:

  1.  async onLoad (options) {

  2.    // 1. 首先会获取从url里面带来的billId

  3.    this.billId = options.billId

  4.    // 2. 其次会请求一次userInfo,获取userId

  5.    this.userInfo = await this.$store.dispatch('getUserInfo')

  6.    // 3. 然后拿这个userId去检查该用户是否已经处于拼单

  7.    const inBill = await this.$store.dispatch('checkInBill', this.userInfo.userId)

  8.    // 4. 如果已经处于拼单,那么就会有一个billId

  9.    if (inBill.inBill) {

  10.      this.billId = inBill.inBill.billId

  11.    }

  12.    // 5. 如果没有处于拼单,那么将请求当前billId的拼单

  13.    // 6. 如果billId都无效,则redirect到首页,否则检查当前用户是否处于该拼单当中

  14.    await this.getBillInfo()

  15.  }

此外,当用户点击“参与拼车”后,需要重新请求拼单信息,以刷新视图拼车人员列表;当用户点击“退出拼车”后,要重定向到首页。

经过上面几个步骤,客户端的逻辑已经完成,可以进行预发布了。

四、预发布&申请上线

如果要发布预发布版本,需要运行 npm run build命令,打包出一个生产版本的包,然后通过小程序开发者工具的上传按钮上传代码,并填写测试版本号:

接下来可以在小程序管理后台→开发管理→开发版本当中看到体验版小程序的信息,然后选择发布体验版即可:

当确定预发布测试无误之后,就可以点击“提交审核”,正式把小程序提交给微信团队进行审核。审核的时间非常快,在3小时内基本都能够有答复。

值得注意的是,小程序所有请求的API,都必须经过域名备案使用https证书,同时要在设置→开发设置→服务器域名里面把API添加到白名单才可以正常使用。

五、后记

这个小程序现在已经发布上线了,算是完整体验了一把小程序的开发乐趣。小程序得到了微信团队的大力支持,以后的生态只会越来越繁荣。当初小程序上线的时候我也对它有一些抵触,但后来想了想,这只不过是前端工程师所需面对的又一个“端“而已,没有必要为它戴上有色眼镜,多掌握一些总是好的。

“一起打车吧”微信小程序依然是一个玩具般的存在,仅供自己学习和探索,当然也欢迎各位读者能够贡献代码,参与开发~


SegmentFault Hackathon 2018 盛大开场,Let's hack !

转载自:https://mp.weixin.qq.com/s/67x-OfkFzLFjTu_qyHJwAA

记一次基于 mpvue 的小程序开发及上线实战的更多相关文章

  1. 基于mpvue搭建小程序项目框架

    简介: mpvue框架对于从没有接触过小程序又要尝试小程序开发的人员来说,无疑是目前最好的选择.mpvue从底层支持 Vue.js 语法和构建工具体系,同时再结合相关UI组件库,便可以高效的实现小程序 ...

  2. mpvue微信小程序开发随笔

    mpvue上手很快,学习成本低,目前是开源的,适合技术实力不是很强的公司采用. spring boot 做后台,开发效率杠杠的.建议会java的开发尽量使用spring boot 开发,省事. 最近用 ...

  3. 基于mpvue的小程序项目搭建的步骤

    mpvue 是美团开源的一套语法与vue.js一致的.快速开发小程序的前端框架,按官网说可以达到小程序与H5界面使用一套代码.使用此框架,开发者将得到完整的 Vue.js 开发体验,同时为 H5 和小 ...

  4. 基于mpvue的小程序项目搭建的步骤一

    未标题-1.png mpvue 是美团开源的一套语法与vue.js一致的.快速开发小程序的前端框架,按官网说可以达到小程序与H5界面使用一套代码.使用此框架,开发者将得到完整的 Vue.js 开发体验 ...

  5. 为什么选择MpVue进行小程序的开发

    前言 mpvue是一款使用Vue.js开发微信小程序的前端框架.使用此框架,开发者将得到完整的 Vue.js 开发体验,同时为H5和小程序提供了代码复用的能力.如果想将 H5 项目改造为小程序,或开发 ...

  6. 微信小程序开发——进阶篇

    由于项目的原因,最近的工作一直围绕着微信小程序.现在也算告一段落,是时候整理一下这段时间的收获了.我维护的小程序有两个,分别是官方小程序和一个游戏为主的小程序.两个都是用了wepy进行开发,就是这个: ...

  7. mpvue体验微信小程序开发

    微信小程序 https://developers.weixin.qq.com/miniprogram/introduction/index.html?t=18082114 微信小程序是一种全新的连接用 ...

  8. mpvue构建小程序(步骤+地址)

    mpvue 是一个使用 Vue.js 开发小程序的前端框架(美团的开源项目).框架基于 Vue.js 核心,mpvue 修改了 Vue.js 的 runtime 和 compiler 实现,使其可以运 ...

  9. mpvue 转小程序实践总结

    介绍 Mpvue 是一个使用 Vue.js 开发小程序的前端框架.  基础介绍 框架基于 Vue.js 核心,修改了 Vue.js 的 runtime 和 compiler 实现,使其可以运行在小程序 ...

随机推荐

  1. TP5 生成数据库字段 和 路由 缓存来提升性能

    关于使用tp5框架如何提升部分性能,框架中很多影响性能的问题在于,很多请求都要重新加载,如果能避免过度加载的问题,就能提升部分性能,所以我们通过缓存来实现这一功能,具体如下. 首先说明 如果是linu ...

  2. [CF798D]Mike and distribution_贪心

    Mike and distribution 题目链接:http://codeforces.com/problemset/problem/798/D 数据范围:略. 题解: 太难了吧这个题..... 这 ...

  3. 数据结构 -- 队列Queue

    一.队列简介 定义 队列(queue)在计算机科学中,是一种先进先出的线性表. 它只允许在表的前端进行删除操作,而在表的后端进行插入操作.进行插入操作的端称为队尾,进行删除操作的端称为队头.队列中没有 ...

  4. lg 1478

    好多天没碰代码了,感觉忘得差不多了,没有学习感觉罪恶深重,从今天起开始补题啊啊! 简单零一背包,套模板就行. #include<bits/stdc++.h> using namespace ...

  5. JAVA中线程到底起到什么作用!

    这是javaeye上非常经典的关于线程的帖子,写的非常通俗易懂的,适合任何读计算机的同学. 线程同步 我们可以在计算机上运行各种计算机软件程序.每一个运行的程序可能包括多个独立运行的线程(Thread ...

  6. 数据库设计规范、E-R图、模型图

    (1)数据库设计的优劣: 糟糕的数据库设计: ①数据冗余冗余.存储空间浪费. ②数据更新和插入异常. ③程序性能差. 良好的数据库设计 ①节省数据的存储空间. ②能够保证数据的完整新. ③方便进行数据 ...

  7. hdu 4826 三维dp

    dp的问题除了递推过程的设计之外 还有数据结构的选择以及怎样合理的填充数据 这个的填充是个坑..#include<iostream> #include<cstdio> #inc ...

  8. ubuntu目录结构(转)

    /:根目录,一般根目录下只存放目录,不要存放文件,/etc./bin./dev./lib./sbin应该和根目录放置在一个分区中 /bin:/usr/bin:可执行二进制文件的目录,如常用的命令ls. ...

  9. 项目构建工具之gradle

    groovy的高级特性: 可选的类型定义 def.assert.括号是可选的.字符串 .集合API.闭包: 构建脚本 项目project : group name version apply depe ...

  10. VmWare 网络模式

    VMware虚拟机三种联网方法及原理 一.Brigde--桥接:默认使用VMnet0 1.原理: Bridge 桥"就是一个主机,这个机器拥有两块网卡,分别处于两个局域网中,同时在" ...