The original address

The company began to involve in the business of small programs. Since our department focuses on React technology stack, we decided to adopt the Taro framework of JINGdong after investigation. But the actual development still encountered a lot of holes, so I summarized some, also built some wheels, decided to share

Customize toast (or Modal) APIS

Although applets provide interaction apis such as Toast and Modal, Taro does the same. But products want to customize styles, ICONS, positions, and text length, so you can’t do it yourself

First, the effect demonstration

  1. Call toast’s show and hide via the global API

  2. Each interface can be called without manually importing components

2. Monitor interface routing

  1. First, each interface is callable, and maintaining a global Store is easiest. Then read the current interface dynamically, and set the state of the corresponding interface

  2. The global Store is simple and reads the current interface. Fortunately, the applet provides the getCurrentPages API

const pages = getCurrentPages()
const curPage = pages[pages.length - 1) | | {}Copy the code
  1. Routing changes the dynamic load, namely listening, pay attention to this is to monitor the global routing, Taro componentDidShow, componentDidHide is cannot handle. Looking for a long time inWechat open community for a discussionSearch thewx.onAppRoute
wx.onAppRoute(res= > {
  // Update currentPage for global store
})
Copy the code

Introduced laziness: Mixed development is the real deal

  1. Since you don’t want to manually introduce components into every interface, it’s good that applets dotemplateMechanism rematchimportCan. So make a script and append string to the end after build
WXML <block wx:if="{{$taroCompReady}}"> <view class="demo" Wx :if="{{$taroCompReady}}"> <view class="demo"> </view> </block> <import src=".. /.. /components/toast/index.wxml" /> <template is="toast" data="{{__toast__}}" />Copy the code
  1. For data interaction, manually inject templates into build files. Forget the Taro Component levelgetCurrentPagesThere are nativesetData api. Create a variable in the template:<template is="toast" data="{{__toast__}}" />

Toast

With all the technical details sorted out, I started typing code happily

  1. @/utils/page is a simple encapsulation
let _currentPage: Taro.Page = {}

export const $page = {
  get() {
    return _currentPage
  },
  update() {
    // Update current page
    const pages = getCurrentPages()
    _currentPage = pages[pages.length - 1] || {}
  },
  setData(key: string, source: any, force = false) {
    _currentPage.setData({ [key]: source }) // Native setData
    force && _currentPage.$component.forceUpdate() // Taro level forceUpdate, used on demand
  },
  getData(key: string) {
    return _currentPage.data[key]
  },
}
Copy the code
  1. The Toast class uses static + singleton because it is a global reference
const iconFactory = {
  success: successSvg,
  error: errorSvg,
}

export default class Toast {
  static instance: Toast

  page: Taro.Page
  visible = false

  static create() {
    if (!this.instance) this.instance = new Toast()
    return this.instance
  }

  // Define the convenience API
  staticsuccess(title: string, during? : number, config? : Omit<ToastConfig,'title' | 'during'>) {
    returnToast.show({ title, during, ... Object.assign({}, config, {icon: 'success'})})}// Define the convenience API
  staticerror(title: string, during? : number, config? : Omit<ToastConfig,'title' | 'during'>) {
    returnToast.show({ title, during, ... Object.assign({}, config, {icon: 'error'})})}// Define the convenience API
  staticinfo(title: string, during? : number, config? : Omit<ToastConfig,'title' | 'during'>) {
    returnToast.show({ title, during, ... Object.assign({}, config, {icon: 'none'})})}// Call API can be customized
  static async show(config: ToastConfig) {
    if (this.instance.visible) return
    this.instance.visible = true

    const { title, icon = 'none' } = config

    // Start manipulating data to the template
    $page.setData('__toast__', {
      visible: true,
      title,
      icon: iconFactory[icon] || icon,
    })
  }

  static async hide() {
    if (!this.instance.visible) return
    / / hide the toast
    $page.setData('__toast__', {
      visible: false,})this.instance.visible = false}}Copy the code
  1. It initializes when the App hangs, listens for routes, and then uses it directly
import toast from '@/utils/toast'
import $page form '@/utils/page'
class App extends Component {
  componentWillMount() {
    toast.create()

    wx.onAppRoute(res= > {
      toast.hide()
      $page.update() // Update the page after the previous interface toast is hidden
    })
  }

  render() {
    return <Index />
  }
}

class Index extends Component {
  render() {
    return (
      <View className='index'>
        <Button onClick={()= >Toast. success(' submitted request successfully ')}>success</Button>
        <Button onClick={()= > toast.hide()}>hide</Button>
      </View>)}}Copy the code
  1. Toast WXML, that’s pretty simple, native
<template name="toast"> <view class="toast{{__toast__.visible ? '' : ' hidden'}}"> <image class="toast-icon{{__toast__.icon ! == 'none' ? "' : ' hidden'}}" src="{{__toast__.icon}}"></image> <view class="toast-text">{{__toast__.title}}</view> </view> </template>Copy the code
  1. Templates inject script. Taro Taro will generate app.json, which records all registered pages, and then fetch the corresponding index.wxml
const fs = require('fs')
const path = require('path')
const outputDir = 'dist/'
const appJson = 'app.json'
const str = ` 
       
       `
let initPages = []

start()

async function start() {
  // Get all page index. WXML
  initPages = await getInjectPages()
  // Write the template in
  await injectAll(initPages, str)
}

function getInjectPages(jsonName = appJson) {
  const appJsonPath = getAbsPath(outputDir, jsonName)
  const suffix = '.wxml'

  return new Promise((resolve, reject) = > {
    // check app.json
    if (fs.existsSync(appJsonPath)) {
      const pageJson = require(appJsonPath)
      const pages = (pageJson.pages || []).map(p= > outputDir + p + suffix)

      // check all pages
      if(! pages.some(p= >! fs.existsSync(p))) resolve(pages)else reject('did not find all pages')}}}async function injectAll(pages, template) {
  const injectPromises = pages.map(p= > {
    return new Promise((resolve, reject) = > {
      fs.appendFileSync(p, template, 'utf8')
      resolve()
    })
  })

  await Promise.all(injectPromises)
}
Copy the code
  1. bootstrap
"scripts": {
  "build:weapp": "rm -rf dist && taro build --type weapp"."inject": "node scripts/import-toast.js"
}

yarn build:weapp
yarn inject
Copy the code

Load json animations using canvas

First, the effect demonstration

2. Native support

  1. Small program without SVG tag, some complex animation is not easy to implement, fortunately there is official website support Lottie – miniProgram

  2. usage

// wxml
<canvas id="canvas" type="2d"></canvas>

// js
import lottie from 'lottie-miniprogram'
wx.createSelectorQuery()
  .selectAll('#loading') // Canvas tag ID
  .node(([res]) = > {
    const canvas = res.node
    const context = canvas.getContext('2d')
    lottie.setup(canvas)
    lottie.loadAnimation({
    animationData: jsonData, // Add the json file
    rendererSettings: { context },
  })
}).exec()
Copy the code

3. API calling Loading

  1. Note that the createSelectorQuery method will fail to find the Canvas ID if the above component is selected as a component and referenced in the page, but it can be placed in the page. It is preliminarily concluded that canvas component will appear in shadow DOM when the component is referenced, and this API must be used to write canvas. Please correct any errors

  2. If the component invocation doesn’t work, use the API. Same steps as above: listen for routes, global store passes variables through setData, use in native templates, script injection

More free render props

There are always problems and limitations when using Taro Render props to pass components along with some logic

First, the effect demonstration

Second, Taro packaging research

  1. Taro wanted to package the render props as slot + template

  2. Render Normal={() =>

    Render prop
    }

// pages/index/index.wxml <block wx:if="{{$taroCompReady}}"> <view class="index"> <custom-render <view slot="normal"> <view> <template is="renderClosureNormalgzzzz" data="{{$compid__3}}"> AnonymousState__temp}}"></template> </view> </view> </custom-render> </view> </block name="renderClosureNormalgzzzz"> <block> <view>render prop</view> </block> </template> // WXML <block wx:if="{{$taroCompReady}}"> <view> </view> </block>Copy the code
  1. Slot-oriented programming! Taro will not compile lower-case components and will copy them directly, so we can do “mixed development”.
class Index extends Component {
  render() {
    return (
      <View className='index'>
        <CustomRender renderNormal={()= > <View>normal render prop</View>}> // Custom render props<view slot='gender'>
            <View>this is my slot</View>
          </view>
        </CustomRender>
      </View>)}}class CustomRender extends Component {
  render() {
    return (
      <View>// Declare socket<slot name='gender' />
        {this.props.renderNormal()}
      </View>)}}Copy the code

Dynamic slot

  1. Not free enough? It is dynamic! Come on!

Scenario: In the form form, if custom: true in data, render slot, otherwise render regular item

class Index extends Component {
  render() {
    return (
      <View className='index'>
        <CustomRender
          data={[
            { name: 'name', label:'name',value: 'lawler'}, {name: 'password', label:'password',value: 'pwd...'}, {name: 'gender', custom: true}, // Custom rendergender item
          ]}
          renderNormal={()= > <View>normal render prop</View>} > // Declare the slot content of custom in lower case<view slot='gender'>
            <View>Gender: Male Radio? Female radio</View>
          </view>
        </CustomRender>
      </View>)}}class CustomRender extends Component {
  render() {
    const { data } = this.props
    return (
      <View>{data.map(item => { const { name, label, value custom } = item if (! custom) return<View>{`normal item, ${label}: ${value}`}</View>// Dynamic slot, happy is done!! return<slot name={name} />
        })}
        {this.props.renderNormal()}
      </View>)}}Copy the code

The last

  1. Source code: Taro mini Demo

  2. As for the script injection template in Taro Watch mode, we changed the index.tsx in Pages which will re-generate index.wxml, so we must re-inject yarn, but this does not affect the final build of the project

  3. For ease of development, use fs.watch to listen for index.wxml in your build and automatically append the template if it changes. Of course, this script serves the company internal, will not share out, interested can email me ~

  4. Like small partners, remember to leave your small ❤️ oh ~

The resources

  • Wechat miniprogram: Lottie -miniprogram

  • NervJS/taro-sample-weapp