Introduction to the

An education and training service APP of this project. Provide online browsing institutions information, teacher style and course booking and other functions.

The front-end of the project uses avm.js multi-terminal development technology, which can be simultaneously compiled into Android & iOS App and wechat applets. The back-end uses the APICloud Data Cloud 3.0 cloud function custom interface.

The main technical points

In the development process of the project, under the idea of “disassembly as soon as possible”, the project was disassembled by fine-grained components. You can learn the component splitting logic and some tricks to consolidate your custom components.

rendering

The development details

The organization of the TabBar

Those of you who have noticed the source code for the previous several template projects on Github are already familiar with TabBar’s implementation.

Create a Tabbar home page structure by defining app.json in the project root directory. In this file, you can define specific parameters for the home page structure. This section describes the path, name, and resource information of each Tab page.

If you need the native Tabbar structure for applets, this is the best choice. If the project does not have an adaptation plan for applets, the original FrameGroup can also be used to customize the related switch behavior and logic in greater depth.

Network request req.js

The data of general projects are obtained through communication with the server. Get relevant data through the local request library and process and render to the interface. The logic is handled in req.js in order to unify requests (sessions, caches, exceptions, etc.). 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 Portal home page

The home page structure is very simple, divided into four sections.

  1. Head navigation bar
  2. Head rotation diagram
  3. Middle classification slider
  4. Lower master card slider
  5. At the bottom of theAbout usThe rich text

Custom components: A-header Header navigation bar

The header navigation bar is easy to overlook. Here we have a custom a-header base component. The implementation is in components/a-header. STML.

In this component, the Template section defines the concrete UI structure.

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

The writing style (condition &&
) can be used to achieve the v-if effect. Condition is a Boolean value evaluated from a function. Since this header is not needed in applets and on the WEB, only the APP side needs to render. Specific rendering criteria can be defined in related functions to achieve the effect of “conditional rendering”.

The a-Header component is responsible for displaying the header navigation bar, and the most important elements are the “title” text, buttons on the left and right, and events.

Pass in the relevant configuration with custom parameters such as title and leftIcon. And then get the value in the template rendering. There are also scene parameters that implement whether or not a return button is required by different types and values of leftIcon.

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

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

These two methods are used in response to left-right clicks in the header navigation bar. Left-clicking has an internal judgment: see if a custom event is passed in, and if so, execute the incoming event; Instead, follow the default logic: close the window.

The right click event has no default logic. You only need to determine whether to customize the right logic.

Head slide

The header navigation picture information 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 }); })}Copy the code

After retrieving the data, use a swiper component to display the wheel map:

 <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>
Copy the code

Use the scrollable Scroll View component to render the classification menu and the master 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; '"> <img :src="item.image" class="main__menu--item-img"/> <text class="main__menu--item-text">{{ item.name }}</text> </view> </ scrollview > <a-section title=" master team "V-if =" homedata. teacher_teams" class="main__teachers"> < scrollview 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>Copy the code

Custom components: A-section section components

By looking at the project design draft as a whole, it was found that there were a number of repetitive elements in the project to reinforce the concept of sections. Because of this kind of recurring and uniform behavior, you need to think about organizing into components. The conclusion is easy to maintain and improve the code reuse, especially effective in the complex structure of large projects.

<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>Copy the code

In fact, templates can also be wrapped with custom functions to implement some custom behavior. For example extendsClassStyleEvents above:

/** * extendsClassStyleEvents(VNode) {/** * extendsClassStyleEvents(VNode) {/** * 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; }Copy the code

It is important to note that this component uses a {this.props. Children} fragment, which can also be used as a pass value, but this value is not from the property, but the internal content of the double tag, which is suitable for passing template class parameters.

Rich text rendering of “About us”

The “About us” section on the front page uses a rich text component: rich-text. Use method is very simple, just pass in nodes.

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

TAB page -1 Course list

Page level reuse of C-course -list

After observing the project, there are two highly similar pages: the tab-1 course list and the tab-0 page, which is categorized from the front page, are very similar in structure. It can even be viewed as opening different scenes on the same page. In previous Versions of APICloud 1.0, we could use the same HTML file to open different pages. However, under the current project, there is a page that opens as one of the main TabBar pages, and just writing a page is not possible.

At this time, and indeed encounter page consistent situation, direct copy of the file is certainly possible. But it’s certainly not elegant to have to maintain two.

Consider extracting the entire page into the component C-course -list and calling it in separate routing pages.

Custom swappable TAB bar

When designing the TAB component, we can first use the structure simulation, equivalent to doing a component structure design sketch:

< a - tabs > < a - TAB in the name of "TAB 1" > < / a - TAB > < a - TAB title = "the name of the TAB 2" > < / a - TAB > < a - TAB title = "TAB name 3" > < / a - TAB > < / a - tabs >Copy the code

Then create a component file to implement. This component is unique in that it uses two layers of component rendering. A-tabs act as a component container for receiving parameters, handling data distribution, and so on. Each TAB page is a custom A-Tab sub-page to receive the specific page content, and define the page name (title).

Here is the a-Tabs template section:

<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;" >... <view class="a-tabs__nav--line" style={this.lineStyle}></view> </view> <swiper autoplay={false} circular={false} circular={false} circular  :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>Copy the code

Similarly, extendsClassStyleEvents is used to inherit events, styles, and classes. Notice that the class of the second view tag is represented by a mixedClass function.

/** * mixedClass * @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(' '); }Copy the code

This function is used to handle different conditions that require different classes to be rendered. Of course, you can also do this by writing a ternary expression in the template.

The top column section of a-Tabs needs to render how many a-tabs there are in the component to render the title of the child. And bind the click event handleNavClick to switch TAB:

<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>Copy the code

At the bottom, a swiper component is used to handle the presentation of specific pages. When swiper is switched, that is, the page display of the current TAB is changed. The event handleSwiperChange needs to be reported to services for data status 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>
Copy the code

Tab-2 User home page

The user home page structure is also simple:

  1. User Information panel
  2. User operation menu

User data processing

The user information panel above uses a view combined with V-if to determine whether the user information is logged in:

<view class="user-panel" v-if="userData" @click="logout"> <img src=".. /.. /images/icon__tab--user-1.png" alt="" class="user-avatar"> <text class="user-name">{{ userData.name }}</text> </view> <view class="user-panel" v-else @click="doLogin"> <img src=".. /.. /images/icon__tab--user-0.png" Alt ="" class="user-name"> <text class="user-name">Copy the code

UserData comes from a data field:

this.UM = new UserManager();
this.data.userData = this.UM.data;
Copy the code

The UserManager in the code is a user data management class. Unified management of user data behavior in the form of encapsulated data stores.

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; }}Copy the code

Alternatively, you can use Object.defineProperty to implement a data interception to save to local preference data. Furthermore, Proxy and Reflect can be used to implement the observer mode and broadcast events to make user data more intelligent.

User menu cell component

The following user menu is a very common cell structure.

<a-cell title=" my reservation "value=" ok" link="preorder-list" imgIcon=".. /.. /images/icon__user-cell--alarm.png"/> <a-cell title=" my line class order "link=".. /order-list/order-list.stml" imgIcon=".. /.. /images/icon__user-cell--order.png"/>Copy the code

The cell bears the project name, item value, icon, and click to 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&&<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 src=".. /.. /components/img/icon__a-cell-arrow-right.png" class="a-cell__link--arrow"/>} </view> {this.props.children.length! ==0 && <view class="a-cell__content"> {this.props.children} </view> } </view> ) </template>Copy the code

In the template, there are a number of conditional judgments:

  • According to thethis.props.imgIconTo determine whether to render the project icon;
  • According to thethis.props.linkWhether or not to render the link to the right of the small arrow;
  • According to thethis.props.children.lengthTo determine whether a child container is needed to display the internal contents.

Also, bind it to a handleCellClick event to handle the forward routing behavior passed in by this.props. Link:

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);
      }
    }
Copy the code

This code handles each case of the value of this.props. Link. Make it support full object parameters like api.openwin. Custom path parameters with STML can also be supported. In most cases, following project structure specifications, pages are in a secondary directory under Pages, so it can be reduced to a string: it can be used as both a page name and a path-addressing parameter, making it easier for components to use.

Other secondary pages

// The components and structure of TODO’s other secondary pages and home pages are much the same. Specific can refer to the project source code for study.

Summary and feedback

This project is more for you to show

  • Advanced uses of components such as conditional rendering, the introduction of custom functions for processing and inheritance of template nodes, and special nodeschildrenSuch as use.
  • Component design flow: for example, implementationa-tabsFor complex components, you can define the usage facade first and then reversely populate the detail logic.
  • Component design principles: Additional repetitive page structures require refinement and generalization. Design components that are easy to use and simple. Otherwise, the cost of understanding communication usage increases due to components.