Because the development of small program is more primitive and complex, there is a big gap with our mainstream development way, so in order to improve the efficiency of our development of small program, there have been a lot of small program framework on the market: Mpvue, Taro, Uni-app, etc., these frameworks more or less take us to the modern way of development, they let you use React or Vue to develop small applications. Today, I will share a framework on how to build a small application using Vue 3.0.

Basic knowledge of

Vue 3.0

A quick look at what’s new in Vue 3.0:

Composition-API

Composition-api is an API that allows you to easily extract logical functions. Compared with the previous Options API, the code organization ability is stronger. The same logic can be written in the same place with clear boundaries between logic.

Take a look at the following examples:

<template> <div> <div>Add todo list</div> <div class="field"> <input @input="handleInput" :value="todo" /> </div> <button @click="handleAdd">Add +</button> </div> </template> <script> import { ref, reactive } from 'vue'; import { useMainStore } from '@/store'; export default { setup() { const todo = ref(''); const state = reactive({ a: 1, b: 'hello world' }); const store = useMainStore(); const handleInput = (e) => { todo.value = e.detail.value; state.a = 'dsdas'; }; const handleAdd = () => { store.addTodo(todo.value); }; return { handleInput, todo, handleAdd, }; }}; </script>Copy the code

Fragment, Teleport

The React Fragment allows us to write Vue templates without being limited to a single root node. In Vue3.0, we can have multiple root nodes.

<Fragment>
   <component-1 />
   <component-2 />
</Fragment>
Copy the code

Teleport installs child components elsewhere in the DOM in a direct declarative manner, similar to React Portal but more powerful.

<body> <div id="app" class="demo"> <h3>Move the #content with the portal component</h3> <div> <teleport to="#endofbody">  <p id="content"> This should be moved to #endofbody. </p> </teleport> <span>This content should be nested</span> </div>  </div> <div id="endofbody"></div> </body>Copy the code

Better TypeScript support

Vue 3.0 code is now written in TS, and with composition-API, you can seamlessly switch to TS when writing business code.

Custom Render API

This API makes it easy to build custom rendering layers, which we’ll focus on next.

import { createRenderer, CreateAppFunction, } from '@vue/runtime-core'; Export const {render, createApp: baseCreateApp} = createRenderer({render, createApp: baseCreateApp} =... NodeOps, // modify dom node function}); render();Copy the code

Small program

To develop a small application page we basically only need four files:

index.js

Index.js is where we write the code logic.

  1. There is a Page function, which is the object configuration, similar to the Vue options configuration, has a data property, stores the initialized data.
  2. If you want to modify the data and change the view, like react, you need to call setData to change the view.
Page({
     data: {
       text: 'hello word'
    },
    onLoad() {
        this.setData({
            text: 'xxxxx'
        })
    },
    onReady() {},
    onShow() {},
    onHide() {},
    onUnload() {},
    handleClick() {
        this.setData({
            text: 'hello word'
        })
    }
})
Copy the code

index.ttml

Index.ttml is where we write the view template.

  1. Similar to Vue’s template, we need to define the template before we can display the view
  2. Note: We can’t directly modify the DOM of the template in index.js. We can only define the DOM of the template first. This is caused by the two-thread applets architecture, which is divided into the logic layer and the rendering layer. The two threads exchange data using setData.
<view>
    <view bindtap="handleClick">{{text}}</view>
</view>
Copy the code

index.json

Configure applet pages and components where the parameters are not listed, but be sure to have this file.

index.ttss

As the name suggests, this is where you write styles, similar to CSS.

The template

To facilitate encapsulation, applets can define a template in advance, and then import templates where needed, similar to the use of EJS and PUG import template

<template name="view"> <view>{{text}}</view> </template> This will render the text data inside the template. <template is="view" data="{{text: text}}"/>Copy the code

Dynamic templates

As mentioned above, the DOM node cannot be dynamically modified in the applet. The template can only be defined in advance, and then the view can be modified through setData.

But applets have a more dynamic feature called dynamic selection templates.

/ / use this template < template is = "{{type}}" data = "{{item: the item}}" / >Copy the code

The type of the is attribute above is dynamic. It is a variable that can be used to select different templates based on the value of type. For example, when type is view, the view template defined in advance will be rendered.

Custom render layers (very important)

How to use Vue 3.0 convenient custom rendering layer combined with the dynamic selection of small program template features to write a small program framework?

import { createRenderer, CreateAppFunction, } from '@vue/runtime-core'; Export const {render, createApp: baseCreateApp} = createRenderer({render, createApp: baseCreateApp} =... NodeOps, // modify dom node function});Copy the code

We can see that the ‘createRenderer’ function takes two arguments, one is patchProp and one is nodeOps.

nodeOps

NodeOps represents operations that modify a node node so that the view can be changed. For example, in the browser environment of Vue 3.0, it is written like this:

import { RendererOptions } from '@vue/runtime-core' export const svgNS = 'http://www.w3.org/2000/svg' const doc = (typeof document ! == 'undefined' ? document : null) as Document let tempContainer: HTMLElement let tempSVGContainer: SVGElement // nodeOps export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {insert: (child, parent, anchor) => { parent.insertBefore(child, anchor || null) }, remove: child => { const parent = child.parentNode if (parent) { parent.removeChild(child) } }, createElement: (tag, isSVG, is): Element => isSVG ? doc.createElementNS(svgNS, tag) : doc.createElement(tag, is ? { is } : undefined), createText: text => doc.createTextNode(text), createComment: text => doc.createComment(text), setText: (node, text) => { node.nodeValue = text }, setElementText: (el, text) => { el.textContent = text }, parentNode: node => node.parentNode as Element | null, nextSibling: node => node.nextSibling, querySelector: selector => doc.querySelector(selector), setScopeId(el, id) { el.setAttribute(id, '') }, cloneNode(el) { return el.cloneNode(true) }, }Copy the code

In fact, Vue calls DOM apis such as doc.createElement and doc.createTextNode above to display data to the view regardless of how it changes.

VNode

Because of the limitation of the small program, we can not directly modify the DOM like the browser environment, so we can first imitate the browser environment to create a virtual DOM, we called VNode.

class VNode { id: number; type: string; props? : Record<string, any>; text? : string; children: VNode[] = []; eventListeners? : Record<string, Function | Function[]> | null; parentNode? : VNode | null; nextSibling? : VNode | null; constructor({ id, type, props = {}, text, }: { id: number; type: string; props? : Record<string, any>; text? : string; }) { this.type = type; this.props = props; this.text = text; this.id = id; } appendChild(newNode: VNode) { if (this.children.find((child) => child.id === newNode.id)) { this.removeChild(newNode); } newNode.parentNode = this; this.children.push(newNode); setState({ node: newNode, data: newNode.toJSON() }); // setData} insertBefore(newNode: VNode, anchor: VNode) {newNode.parentNode = this; newNode.nextSibling = anchor; if (this.children.find((child) => child.id === newNode.id)) { this.removeChild(newNode); } const anchorIndex = this.children.indexOf(anchor); this.children.splice(anchorIndex, 0, newNode); setState({ node: this, key: '.children', data: this.children.map((c) => c.toJSON()), }); SetData} removeChild(child: VNode) {const index = this.children.findIndex((node) => node.id === child.id); if (index < 0) { return; } if (index === 0) { this.children = []; } else { this.children[index - 1].nextSibling = this.children[index + 1]; this.children.splice(index, 1); } setState({ node: this, key: '.children', data: this.children.map((c) => c.toJSON()), }); } setText(text: string) { if (this.type === TYPE.RAWTEXT) { this.text = text; setState({ node: this, key: '.text', data: text }); return; } if (! this.children.length) { this.appendChild( new VNode({ type: TYPE.RAWTEXT, id: generate(), text, }) ); return; } this.children[0].text = text; setState({ node: this, key: '.children[0].text', data: text }); } path(): string { if (! this.parentNode) { return 'root'; } const path = this.parentNode.path(); return [ ...(path === 'root' ? ['root'] : path), '.children[', this.parentNode.children.indexOf(this) + ']', ].join(''); } toJSON(): RawNode { if (this.type === TYPE.RAWTEXT) { return { type: this.type, text: this.text, }; } return { id: this.id, type: this.type, props: this.props, children: this.children && this.children.map((c) => c.toJSON()), text: this.text, }; }}Copy the code

As you can see, the VNode we created is similar to the DOM, with some methods for manipulating Node nodes and eventually generating a Node tree. We can imitate the vue browser environment nodeOps writing method, first to modify our VNode, in the modification of Node Node at the same time we can call the small program setData method.

// Modify VNode export const nodeOps = {insert: (child: VNode, parent: VNode, anchor? : VNode) => { if (anchor ! = null) { parent.insertBefore(child, anchor); } else { parent.appendChild(child); } }, remove: (child: VNode) => { const parent = child.parentNode; if (parent ! = null) { parent.removeChild(child); } }, createElement: (tag: string): VNode => new VNode({ type: tag, id: generate() }), createText: (text: string): VNode => new VNode({ type: TYPE.RAWTEXT, text, id: generate() }), createComment: (): VNode => new VNode({ type: TYPE.RAWTEXT, id: generate() }), setText: (node: VNode, text: string) => { node.setText(text); }, setElementText: (el: VNode, text: string) => { el.setText(text); }, parentNode: (node: VNode): VNode | null => node.parentNode ?? null, nextSibling: (node: VNode): VNode | null => node.nextSibling ?? null, querySelector: (): VNode | null => getApp()._root, setScopeId(el: VNode, id: string) { if (el.props) { const className = el.props.class; el.props.class = className ? className + ' ' + id : id; }}};Copy the code

toJSON()

It’s not enough just to create a VNode, we have to render it into the applet. The applet must render the data defined in the data property in advance, and only the normal data type.

Page({ data: { root: { type: 'view', props: { class: 'xxx' }, children: [...] }}})Copy the code

The toJSON method formats a VNode into a normal object so that applets can render data.

The interface types are as follows:

interface RawNode { id? : number; type: string; // View, input, button props? : Record<string, any>; children? : RawNode[]; text? : string; / / text}Copy the code

Are you familiar with the structure of VDOM?

path()

We can see that in our VNode definition, there is a path() method. This method is to get a path of nodes in the entire Node tree, and then use path to modify a particular Node.

const path = Node.path(); // root.children[2].props. Class // Then we can update this.setData({'root.children[2].class': 'XXXXX'})Copy the code

Combine dynamic selection templates

<template name="$_TPL"> <block tt:for="{{root.children}}" tt:key="{{id}}"> <template is="{{'$_' + item.type}}" data="{{item: Item}}"/> </block> </template> <template name="$_input"> // input has three attributes: class and bindinput and value Class @input Value <input class="{{item.props['class']}}" bindinput="{{item.props['bindinput']}}" value="{{item.props['value']}}"> <block tt:for="{{item.children}}" tt:key="{{id}}"> <template is="{{'$_' + item.type}}" Data ="{{item}}"/> </block> </template> <template name="$_button"> // Button has two attributes class and bindTap corresponding to vue file <button class="{{item.props['class']}}" bindtap="{{item.props['bindtap']}}"> <block tt:for="{{item.children}}" tt:key="{{id}}"> <template is="{{'$_' + item.type}}" data="{{item}}"/> </block> </button> </template> <template name="$_view"> <view class="{{item.props['class']}}" bindtap="{{item.props['bindtap']}}"> <block tt:for="{{item.children}}" tt:key="{{id}}"> <template is="{{'$_' + item.type}}" data="{{item}}"/> </block> </view> </template> <template name="$_rawText">{{item.text}}</template>Copy the code

Compile layer

The code we wrote must be Vue code, not the above template code, so how to compile the Vue code to the above template code?

Take a look at the overall architecture:

Template 

If we were writing business code with a common Vue directive template pattern, we could parse the Vue template using @vue/compile-core underneath, and then iterate through the parse AST to collect the tags and props used in the template.

import { parse } from '@vue/compiler-sfc'; import { baseCompile, } from '@vue/compiler-core'; const { descriptor } = parse(source, { filename: this.resourcePath, }); / / traverse the ast to collect the tag and props const} {the ast = baseCompile (descriptor. The template. The content).Copy the code

JSX/TSX

If we were writing JSX/TSX business code, we could write a Babel plugin that collects tags and props. In the Babel plugin, we could iterate through the AST to collect tags and props.

The resulting TTML

Suppose we have a.vue file:

<template> <div class="container is-fluid"> <div class="subtitle is-3">Add todo list</div> <div class="field"> <div class="control"> <input class="input is-info" @input="handleInput" :value="todo" /> </div> </div> <button class="button is-primary is-light" @click="handleAdd">Add +</button> </div> </template> <script> import { ref } from 'vue'; import { useMainStore } from '@/store'; export default { setup() { const todo = ref(''); const store = useMainStore(); const handleInput = (e) => { todo.value = e.detail.value; }; const handleAdd = () => { store.addTodo(todo.value); }; return { handleInput, todo, handleAdd, }; }}; </script> <style> .container { text-align: center; margin-top: 30px; } </style>Copy the code

The following template is generated:

<template name="$_TPL"> <block tt:for="{{root.children}}" tt:key="{{id}}"> <template is="{{'$_' + item.type}}" data="{{item: Item}}"/> </block> </template> <template name="$_input"> // input has three attributes: class and bindinput and value Class @input Value <input class="{{item.props['class']}}" bindinput="{{item.props['bindinput']}}" value="{{item.props['value']}}"> <block tt:for="{{item.children}}" tt:key="{{id}}"> <template is="{{'$_' + item.type}}" Data ="{{item}}"/> </block> </template> <template name="$_button"> // Button has two attributes class and bindTap corresponding to vue file <button class="{{item.props['class']}}" bindtap="{{item.props['bindtap']}}"> <block tt:for="{{item.children}}" tt:key="{{id}}"> <template is="{{'$_' + item.type}}" data="{{item}}"/> </block> </button> </template> <template name="$_view"> <view class="{{item.props['class']}}" bindtap="{{item.props['bindtap']}}"> <block tt:for="{{item.children}}" tt:key="{{id}}"> <template is="{{'$_' + item.type}}" data="{{item}}"/> </block> </view> </template> <template name="$_rawText">{{item.text}}</template>Copy the code

As you can see, starting from the root node of $_TPL, you can select each of the templates generated below (input, button, view…) based on each item.type. Within each template there are loops, which combine VNode’s infinite recursive rendering.

Join us

We are a byte front-end game team, covering the game publishing business. We have published many games, mainly through technical means, providing efficient game publishing services and distribution channels for mobile games & small games and other kinds of games, making game development easier, game experience more extreme and game publishing more efficient.

Our technology stack ranges from PC, H5, RN to applets. Strong technical atmosphere, close technical communication within the department, every student has the opportunity to participate in the construction of basic technology. And in the direction of front-end visualization and engineering construction have in-depth exploration, in the direction of large front-end performance optimization have in-depth research, with the construction of high-performance front-end applications as the primary goal. In addition, I have accumulated a lot in the small program technology stack, deeply explored performance optimization, self-developed development framework and open source plan and vision.

Resume address: [email protected], subject: name – years of work – front-end games.


Welcome to the byte front end

Resume mailing address: [email protected]