Online education mobile multi terminal development source code sharing explanation: app + small program

Time:2021-5-30

brief introduction

This project is an education and training service app. It provides online browsing of organization information, elegant demeanor of famous teachers, course booking and other functions.

The front end of the project usesavm.jsMulti terminal development technology, which can be compiled asAndroid & iOS AppAnd wechat applet; Back end usageAPICloud Data cloud 3.0Cloud function custom interface.

Technical points

In the development process of this project, under the idea of “dismantling as soon as possible”, the project is disassembled with fine-grained components. You can learn about the component splitting logic and some operation skills, and consolidate the custom components.

design sketch

![ Uploading…] ()
Online education mobile multi terminal development source code sharing explanation: app + small program

Introduction of source code directory structure

Application of project source code in GitHub warehousewidgetUnder the directory. Complete source code, please see:https://github.com/apicloudco…
The file structure in this directory is as follows:

┌ - component // / Project Public component directory
│├ - img // / component common material
A-card.stml / [basic component] card component
A-cell.stml / [basic component] cell component
A-cell-group.stml / [basic component] cell container component
A-header.stml / [basic component] head navigation component
A-section.stml / [basic component] Chapter component
A-tab.stml / [basic component] tab component
A-tabs.stml / [basic components] tab container component
│├ - b-course.stml / [business component] course details component
│├ - b-notice.stml / [business component] reminder panel component
C-course-list.stml / [composite component] course list page
├ - images // / image material icon resource directory
ζ - pages // / new AVM page directory
│  ├─course-detail/
Process-detail.stml // course details page
│  ├─course-list/
The course-list.stml // course list page
│  ├─course-pay/
Process-pay.stml // purchase course page
│  ├─course-preorder/
The course-preorder.stml // appointment page
│  ├─order-detail/
// order-detail.stml // user order details page
│  ├─order-list/
The order-list.stml // user order list page
│  ├─pay-result/
Pay result.stml // order (payment) result page
│  ├─play-video/
└ - play-video.stml // video playback page
│  ├─preorder-detail/
// pre order-detail.stml // user appointment details page
│  ├─preorder-list/
// pre order-list.stml // user appointment list page
│  ├─tab-home/
// tab-home.stml // tab page-0 entry home page
│  ├─tab-course/
// tab-course.stml // tab page - 1 course classification list
│  ├─tab-user/
└ - tab-user.stml // tab page-2 user home page
Script // / JavaScript script directory
│├ - usermanager.js // user data management class
│ └ - req.js // project request interaction file
└ - config.xml // application configuration file

Development details

Organization of tabbar

If you noticeAPICloudOfficialgithubPrevious several template project source code students haveTabBarI’m familiar with the implementation of.

By defining theapp.jsonTo create oneTabbarThe home page structure of. In this file, you can define some specific parameters of the home page structure. Include eachTabThe path, name and bottom navigation icon resource information of the page.

If you need to adapt the native appletTabbarStructure, which would be the best choice. If the project does not have an adaptation plan for the applet, the original one can also be usedFrameGroupTo further customize the related switching behavior and logic.

Network request req.js

Generally, the data of a project is obtained by communicating with the server. Through the local request library to get the relevant data and processing and rendering to the interface. In order to unify and facilitate requests (session, cache, exception, etc.), thereq.jsTo handle the corresponding logic. The specific encapsulation method and implementation can be developed according to individual team preferences or interface communication rules. The logic in this project is for reference only.

Tab page – 0 entry home page

Online education mobile multi terminal development source code sharing explanation: app + small program
The home page structure is very simple, divided into four parts.

  1. Head navigation bar
  2. Head carousel
  3. Middle category slider
  4. Lower master card slider
  5. bottomAbout usRich text

Custom component: a-header navigation bar

The navigation bar in the head is easy to ignore. Here we customize onea-headerBasic components. The specific implementation is in thecomponents/a-header.stmlIn the middle.

In this component,templatePart of the definition of the specificUIStructure.

<template>
    (isApp() &&
    <safe-area class="a-header">
        ...
    </safe-area>
    )
</template>

among( condition && <tag />)This way of writing can be used to achievev-ifIt’s a good effect.conditionIt’s just a Boolean value, which is evaluated from a function. Because the applet andWEBWe don’t need this head in the game, we only need itAPPThe end needs rendering. You can define the specific rendering basis in the correlation function to achieve the effect of “conditional rendering”.

a-headerThe function of the component is to display the head navigation bar. The most important elements are the “title” text, left and right buttons and events.

Through custom parameterstitleandleftIconAnd so on. And then get the value rendering in the template. There are also some scene parametersleftIconWhether the return button is needed or not is realized by different types and values of.

There are two click events in the component’s method:

  methods: {
    onClickLeft()
    {
        if (this.props.onClickLeft) {
            this.props.onClickLeft();
        } else {
            api.closeWin();
        }
    }
,
    onClickRight()
    {
        this.props.onClickRight && this.props.onClickRight();
    }
}

These two methods are used to respond to the left and right clicks in the head navigation bar. Click on the left to make an internal judgment: check whether there are incoming custom events, and if so, execute the incoming events; Instead, execute the default logic: close the window.

There is no default logic for the event on the right side. You just need to determine whether to customize the logic on the right side.

Head slide

The picture information of head navigation comes from the network request data

function getHomeData() {
    GET('i_alls/home').then(data => {
        this.data.homeData = data;
        api.setPrefs({
            key: 'course_category', value: data.course_category
        });
    })
}

After getting the data, use aswiperComponent to show the carousel diagram:

 <swiper autoplay circular class="main__swiper" style="margin: 10px 0;"
              v-if="homeData.banners">
        <swiper-item v-for="(item,index) in homeData.banners" class="main__swiper--item">
          ![](item.cover)
        </swiper-item>
 </swiper> <swiper autoplay circular class="main__swiper" style="margin: 10px 0;"
              v-if="homeData.banners">
        <swiper-item v-for="(item,index) in homeData.banners" class="main__swiper--item">
          <img :src="item.cover" class="main__swiper--img"/>
        </swiper-item>
 </swiper>

Use scrollablescroll-viewComponent to render the two structures of category menu and famous teacher team

<scroll-view class="main__menu" scroll-x v-if="homeData.course_category"
             :style="'height:'+(api.winWidth/4+20)+'px;'">
    <view class="main__menu--item" v-for="item in homeData.course_category" @click="goto(item)"
          :style="'width:'+api.winWidth/4+'px;'">
        ![](item.image)
        <text class="main__menu--item-text">{{ item.name }}</text>
    </view>
</scroll-view>

<a-section v-if="homeData.teacher_teams" class="main__teachers">
    <scroll-view class="main__teacher" scroll-x>
        <view class="main__teacher--item" v-for="item in homeData.teacher_teams" @click="test">
            ![](item.thumb)
            <text class="main__teacher--item-name">{{ item.name }}</text>
            <text class="main__teacher--item-introduction">{{ item.introduction }}</text>
        </view>
    </scroll-view>
</a-section><scroll-view class="main__menu" scroll-x v-if="homeData.course_category"
             :style="'height:'+(api.winWidth/4+20)+'px;'">
    <view class="main__menu--item" v-for="item in homeData.course_category" @click="goto(item)"
          :style="'width:'+api.winWidth/4+'px;'">
        <img :src="item.image" class="main__menu--item-img"/>
        <text class="main__menu--item-text">{{ item.name }}</text>
    </view>
</scroll-view>

<a-section v-if="homeData.teacher_teams" class="main__teachers">
    <scroll-view class="main__teacher" scroll-x>
        <view class="main__teacher--item" v-for="item in homeData.teacher_teams" @click="test">
            <img :src="item.thumb" class="main__teacher--item-img"/>
            <text class="main__teacher--item-name">{{ item.name }}</text>
            <text class="main__teacher--item-introduction">{{ item.introduction }}</text>
        </view>
    </scroll-view>
</a-section>

Custom component: a-section chapter component

Online education mobile multi terminal development source code sharing explanation: app + small program
Through the overall review of the project design draft, it is found that there are a lot of repetitive elements in the project to strengthen the concept of chapters. Because of this kind of repeated and unified behavior, it is necessary to consider sorting and inducing into components. It is easy to maintain and improve the reusability of code, and it is particularly effective in large projects with complex structure.

<template>
    extendsClassStyleEvents.call(this,
    <view class="a-section">
        {this.props.title &&
        <view class="a-section__header">
            <view class="a-section__header--solid"></view>
            <text class="a-section__header--text">{this.props.title}</text>
        </view>
        }
        <view class="a-section__content">
            {this.props.children}
        </view>
    </view>
    )
</template>

In fact, templates can also be wrapped with custom functions to implement some custom behavior processing. For example, aboveextendsClassStyleEvents

/**
 *Inherits the class, style, and on events of the parent component
 * @param VNode
 * @returns {*}
 */
function extendsClassStyleEvents(VNode) {
    this.props.class && (VNode.attributes.class += ' ' + this.props.class);
    this.props.style && (VNode.attributes.style = this.props.style);
    Object.values(this.props)
          .filter(item => typeof item === 'function' && item.name.startsWith('on'))
          .forEach(ev => VNode.attributes[ev.name] = ev);
    return VNode;
}

It is worth mentioning that a{this.props.children}It can also be regarded as a kind of value transfer, but the value does not come from the attribute, but from the attributeDouble labelIt is suitable for passing template class parameters.

Rich text rendering of “about us”

The “about us” section on the home page is the rich text component used:rich-text. It’s easy to use, and it’s easy to pass innodesNode.

<a-section v-if="homeData.aboutus" class="main__about">
    <text class="main__about--text">{{ homeData.aboutus[0].value }}</text>
</a-section>

Tab page – 1 course list

Online education mobile multi terminal development source code sharing explanation: app + small program

Page level reuse of c-course-list

After observing the project, there are two highly similar pages:tab-1List of courses and fromtab-0Home click into the classification of the page is a very similar structure. It can even be seen as different scenes on the same page. BeforeAPICloud 1.0We can use the same version directlyhtmlFile to open different pages. But under the current project, there is a page as the home pageTabBarOne of the pages opened, only write a page can not be achieved.

At this time, it is true that if the page is consistent, it is certainly possible to copy the file directly. But there are two places that need to be maintained, which is definitely not elegant enough.

So you can consider extracting the whole page to the componentc-course-listAnd then call them in different routing pages.

Custom switchable tab bar

In designtabWhen designing components, we can simulate the structure we use first, which is equivalent to making a sketch of component structure design

<a-tabs>
    <a-tab></a-tab>
    <a-tab></a-tab>
    <a-tab></a-tab>
</a-tabs>

Then create a component file to implement it. This component is special in that it uses two layers of component rendering.a-tabsAs a component container, it is used to receive parameters, process data distribution, etc. every lasttabPage is a custom onea-tabSub page to receive specific page content, and define the page name(title )。

Here’s what’s going ona-tabsInterface template part:

<template>
    extendsClassStyleEvents.call(this,
    <view class="a-tabs">
        <view class={mixedClass('a-tabs__nav',{'a-tabs__nav-scroll':this.scrollNav})}
              style="flex-flow: row nowrap;justify-content: space-around;height: 44px;align-items: center;flex-shrink: 0;">
            ... // top navigation
            <view class="a-tabs__nav--line" style={this.lineStyle}></view>
        </view>
        <swiper autoplay={false} circular={false} :current={this.props.param.current}
                onchange={this.handleSwiperChange} class="a-tabs__content">
            <swiper-item v-for="tab in this.props.children">
                <scroll-view scroll-y="true" class="a-tabs__content--scroll-view">
                    {tab}
                </scroll-view>
            </swiper-item>
        </swiper>
    </view>
    )
</template>

Similarly, we use theextendsClassStyleEventsTo inherit events, styles, andclass. Notice the second oneviewLabeledclassUse onemixedClassFunction.

/**
 *Mixed class
 * @param cls
 * @param extra
 * @returns {string}
 */
function mixedClass(cls, extra) {
    let classList = [cls];
    Object.entries(extra)
          .forEach(([key, val]) => val && classList.push(key));
    return classList.join(' ');
}

This function is used to deal with different rendering conditionsclassYes. Of course, you can also write ternary expressions in the template.

a-tabsThe top section of the component needs to render out how many children there are in the componenta-tabTo render the child’stitle. And bind the click event for ithandleNavClickClick to switchtab

<view class={mixedClass('a-tabs__nav--item',{'a-tabs__nav--item-scroll':this.scrollNav})}
     onClick={this.handleNavClick.bind(this,index)}
     v-for="(item,index) in this.props.children">
   <text :class="'a-tabs__nav-text'
 + (index===this.props.param.current?' a-tabs__nav-text---active':'')">
       {item.attributes.title}
   </text>
</view>

At the bottom, one is usedswiperComponent to handle the presentation of specific pages.swiperWhen switching, it changes the current statetabIt needs to be displayed through the eventhandleSwiperChangeFeedback to the business to achieve data state synchronization.

<swiper autoplay={false} circular={false} :current={this.props.param.current}
        onchange={this.handleSwiperChange} class="a-tabs__content">
    <swiper-item v-for="tab in this.props.children">
        <scroll-view scroll-y="true" class="a-tabs__content--scroll-view">
            {tab}
        </scroll-view>
    </swiper-item>
</swiper>

Tab-2 user home page

Online education mobile multi terminal development source code sharing explanation: app + small program
The structure of the user’s home page is also very simple

  1. User information panel
  2. User action menu

User data processing

The user information panel above uses aviewcombinationv-ifJudge whether the user information is logged in or not:

<view class="user-panel" v-if="userData" @click="logout">
    ![](../../images/icon__tab--user-1.png)
    <text class="user-name">{{ userData.name }}</text>
</view>
<view class="user-panel" v-else @click="doLogin">
    ![](../../images/icon__tab--user-0.png)
    < text class = "user name" > please log in to < / text >
</view><view class="user-panel" v-if="userData" @click="logout">
    <img class="user-avatar">
    <text class="user-name">{{ userData.name }}</text>
</view>
<view class="user-panel" v-else @click="doLogin">
    <img class="user-avatar">
    < text class = "user name" > please log in to < / text >
</view>

anduserDataFrom data field:

this.UM = new UserManager();
this.data.userData = this.UM.data;

In codeUserManagerIs a user data management class. The user data behavior is managed by encapsulating data storage.

export default class UserManager {
    userDataKey = 'USER-DATA';

    get data() {
        const userData = api.getPrefs({
            key: this.userDataKey,
            sync: true
        })

        if (userData) {
            return JSON.parse(userData);
        }
        return null;
    }

    set data(value) {
        api.setPrefs({
            key: this.userDataKey,
            value
        })
    }

    logout() {
        api.removePrefs({
            key: this.userDataKey
        });
        return this._data;
    }

}

In addition, it can also be usedObject.definePropertyTo achieve a data interception, save to the local preference data. Furthermore, it can also be usedProxyandReflectTo achieve observer mode, with broadcast events, so that user data more intelligent.

User menu cell component

The following user menu is a very common cell structure.

< A-cell value = "OK" link = "preorder list"
        imgIcon="../../images/icon__user-cell--alarm.png"/>

<a-cell
        link="../order-list/order-list.stml"
        imgIcon="../../images/icon__user-cell--order.png"/>

The cell has the functions of item name, item value, icon and click jump.

<template>
  extendsClassStyleEvents.call(this,
  <view class={mixedClass('a-cell__root',{['a-cell__root--type-'+(this.props.type||'default')]:true})}
        onclick={this.handleCellClick}>
    <view class="a-cell">
      {this.props.imgIcon&&![]({this.props.imgIcon} class=)}
      <view class={mixedClass('a-cell__main',{['a-cell__main--type-'+(this.props.type||'default')]:true})}>
        <text class="a-cell__title--text-title">{this.props.title}</text>
        <text class="a-cell__title--text-value">{this.props.value}</text>
      </view>
      {this.props.link&&![](../../components/img/icon__a-cell-arrow-right.png)}
    </view>

    {this.props.children.length!==0 &&
    <view class="a-cell__content">
      {this.props.children}
    </view>
    }

  </view>
  )
</template><template>
  extendsClassStyleEvents.call(this,
  <view class={mixedClass('a-cell__root',{['a-cell__root--type-'+(this.props.type||'default')]:true})}
        onclick={this.handleCellClick}>
    <view class="a-cell">
      {this.props.imgIcon&&<img src={this.props.imgIcon} class="a-cell__icon--img"/>}
      <view class={mixedClass('a-cell__main',{['a-cell__main--type-'+(this.props.type||'default')]:true})}>
        <text class="a-cell__title--text-title">{this.props.title}</text>
        <text class="a-cell__title--text-value">{this.props.value}</text>
      </view>
      {this.props.link&&<img class="a-cell__link--arrow"/>}
    </view>

    {this.props.children.length!==0 &&
    <view class="a-cell__content">
      {this.props.children}
    </view>
    }

  </view>
  )
</template>

In templates, there are a lot of conditional judgments

  • according tothis.props.imgIconTo decide whether to render the project icon;
  • according tothis.props.linkTo decide whether to render the small link arrow on the right;
  • according tothis.props.children.lengthTo determine whether a subcontainer is needed to display internal content.

At the same time, bind ahandleCellClickClick on the event to handle itthis.props.linkIncoming hop routing behavior:

function handleCellClick(ev) {
      if (this.props.link) {
        let options = {};
        if (typeof this.props.link === 'string') {
          if (this.props.link.endsWith('.stml')) {
            options.name = this.props.link.split('/').pop().replace('.stml', '');
            options.url = this.props.link;
          } else {
            options.name = this.props.link;
            options.url = `../${this.props.link}/${this.props.link}.stml`;
          }
        } else {
          options = this.props.link;
        }
        console.log(['a-cell:link', JSON.stringify(options)]);
        api.openWin(options);
      } else if (this.props.onClick) {
        this.props.onClick(ev);
      }
    }

This code deals withthis.props.linkAll kinds of situations of the value of. Can make it support complete with the sameapi.openWinObject parameters for. Can also support withstmlUser defined path parameters for. In most cases, following the project structure specification, the page is in thepagesSo it can also be simplified to a string: it can be used as both the page name and the path addressing parameter to improve the convenience of component use.

Other secondary pages

//The components and structure of other secondary pages and home pages of todo are similar. Specific can refer to the project source code for study and research.

Summary and feedback

This project is more to show you

  • Advanced usage of components: such as conditional rendering, introducing custom functions to process and inherit template nodes, and special nodeschildrenAnd so on.
  • Component design process: for example, implementationa-tabsFor complex components, the appearance can be defined first, and then the detail logic can be backfilled.
  • The design principle of components: if there are many duplicate page structures, we need to consider refining and induction. The designed components need to be easy to use and concise. Otherwise, the cost of understanding and communication will increase because of the components.

Recommended Today

Swift advanced (XV) extension

The extension in swift is somewhat similar to the category in OC Extension can beenumeration、structural morphology、class、agreementAdd new features□ you can add methods, calculation attributes, subscripts, (convenient) initializers, nested types, protocols, etc What extensions can’t do:□ original functions cannot be overwritten□ you cannot add storage attributes or add attribute observers to existing attributes□ cannot add parent […]