Remax of small program cross end framework practice

Remax of small program cross end framework practice

Original 2021-11-05 13:23· Ctrip Technology

1, Project background

With the great success of small programs in user scale and commercialization, major platforms have launched their own small programs. However, the syntax of small program development on these platforms is different. The maintenance of small program code on different platforms needs a lot of energy, and it is difficult to achieve a unified effect in logic. Although there are also various conversion tools that can convert the code of other platforms based on one platform, the conversion effect is not satisfactory and often needs to be modified manually. It has become a strong and urgent need for developers to use the applet cross-end development framework to realize one-time development and run everywhere to improve efficiency.

At present, the cross-end development framework of applet can be classified according to two dimensions: Technology stack and implementation principle. In terms of technology stack, the mainstream cross end frameworks basically follow React and Vue, the two most commonly used frameworks for front-end development. Because the team mainly uses React, this article mainly introduces the framework using React syntax. From the implementation principle, the cross end framework of the open source community can be divided into compile time and runtime.

The mainstream framework and its characteristics are shown in Table 1-1 below:

Table 1-1 example of cross end framework of react syntax applet

frame

manufactor

features

Kbone

tencent

Unlimited technology stack, wechat applet and Web isomorphic runtime solution, simulates a set of dom and bom interfaces to be compatible with the existing front-end system. It can only be used for Web compatible wechat applet, which can not meet the development of applet on other platforms

Taro1/2

JD.COM

Class React. The statically compiled framework only follows React syntax during development, and the runtime after compilation has nothing to do with React

Nanachi

Where are you going?

React, statically compiled framework

Rax

Alibaba

Based on the runtime scheme, it supports the use of compile time scheme in local scenarios. The runtime support is based on Kbone and uses a Rax framework similar to React syntax

Remax

Ant gold suit

Use native React to build applets, run-time framework, and support the construction of Web applications from Remax2.0

Taro3

JD.COM

Unlimited technology stack, using a set of runtime layers to be compatible with various DSL s, which was born after Remax

compile time is a cross end framework during compilation. The main workload is in the compilation stage. The framework parses the business code written by the user into an AST tree, and then converts the original code written by the user into code conforming to the rules of the applet through syntax analysis. The cross end framework of runtime mode realizes the custom renderer through the adaptation layer. It is a real way to run React or Vue framework in the logic layer of applet. This method has natural advantages over static compilation.

There are the following problems during compilation: the flexible JSX syntax can not only write very complex and flexible components, but also increase the difficulty of analysis and Optimization in the compilation stage framework. This leads to huge adaptation workload and high maintenance cost. Even so, it is impossible to adapt all writing methods. For example, Taro 1/2 of JD has adapted the possible writing methods of JSX one by one in an exhaustive way, but developers still need to follow a lot of syntax constraints to avoid many dynamic writing methods. Otherwise, the code cannot be compiled and run normally, and the development efficiency is difficult to guarantee. In addition, due to the lack of DOM and BOM API s, various front-end ecosystems accumulated on the Web can hardly be reused in compile time applets. Taro 1/2 of JD and Nanachi of qunar are statically compiled React or React like cross end frameworks.

In contrast, the advantage of the runtime scheme is that it can directly reuse the existing front-end ecosystem. Taking Remax as an example, its biggest advantage is that it can use React syntax to complete code almost without restriction, just like its slogan - use real React to build cross platform applets. In addition, starting from Remax2.0, remax/one supports the construction of Web applications.

When our team made the selection, Taro3 was still to be released, so we didn't think too much about it. Let's focus on comparing Rax and Remax. Both Rax and Remax are from Ali system, but the two frameworks are completely different in design ideas.

From the beginning of its birth, Remax is to support the cross-end framework of small programs. In order to compress the volume of React as much as possible, Rex rewrites React and introduces the Driver mechanism to adapt to multiple terminals, which means that Rex has additional learning costs and cannot be updated with the iteration of React. Although Rex seems to be relatively complete and provides a set of cross-end API s and complete cross-end UI control support out of the box, it relies too much on Ali's construction system and seems not suitable for being the choice of open source framework.

Considering the above points, we finally chose Remax.

2, Effect display

Figures 2-1 to 2-3 show the operation effects of the home page, list page and detail page on the Web and wechat applet respectively, realizing the multi terminal operation of a set of code and synchronous update at the same time.

 

 

Figure 2-1 running results of web and wechat applet on Ctrip ticket home page

 

 

Figure 2-2 running results of web and wechat applet on Ctrip ticket list page

 

 

Figure 2-3 running results of web and wechat applet on Ctrip ticket details page

Here is the basic usage of Remax.

3, Basic usage

3.1 create / install dependency / run

Applet created using create REMAX app

npxcreate-remax-app my-app
? name my-app //Fill in your appName
? author test<test@test.c>
? descriptionRemax Project  //Fill in project description
? platform 
❯ Cross platform applet
  Wechat applet
  Ali (Alipay) applet
  Headline applet
  Baidu applet

Because we are introducing cross platform applets, we choose cross platform applets here.

Run project

cd my-app&& npm install
npm run dev web//web side Preview
npm run devwechat //Wechat applet

After the applet side runs, the products of dist/wechat and dist/ali applets will be generated in the project directory. You can preview them by importing the corresponding directory with the IDE of the applet.

The directory structure of the project is roughly as follows:

 

The public directory will be copied to the dist directory during compilation, and the native page pages directory will also be merged with the pages directory of Remax. This part will be described in detail later.

3.2 Remax's cross platform mechanism

The following details Remax's cross platform mechanism

 

The above is the basic file directory structure of a page, but when we look through Remax's documents, we will find that it provides very few cross platform interfaces, only 9 components and 5 routing related API s. Therefore, Remax also provides a method to distinguish different platform codes by file name suffix:

 

As shown in the above directory, adding files with corresponding suffixes in the directory of the page will give priority to the files with corresponding suffixes when build ing corresponding platforms.

Remax also provides a mechanism for environment variables to distinguish platforms, which can be used directly in the code
process.env.REMAX_PLATFORM differentiates platforms. For example:

if (process.env.REMAX_PLATFORM==='wechat') {}

The above code will only retain the part of the corresponding platform after compilation, so there is no need to worry about the additional code size increase caused by compatibility with multiple platforms.

In addition to the 9 cross platform components and 5 cross platform APIs mentioned above, Remax can also directly use the components and APIs of each platform without using the useComponents declaration.

import React from'react';
import { View } from'remax/one';
import NativeCard from'./native-card';//Native card is a native custom component
// assembly
exportdefault () => (
  <View>
    <NativeCard />
  </View>
);
// API
if (process.env.REMAX_PLATFORM==='ali') {
    systemInfo = my.getSysyemInfoSync();
} elseif (process.env.REMAX_PLATFORM==='wechat') {
    systemInfo = my.getSysyemInfoSync();
}

According to the above mechanism, we can customize any cross platform API and components we need.

According to Remax, the reason why they do not provide more cross platform components and APIs is that they have no standards to smooth out the differences between various platforms at the beginning of design. Of course, this brings some trouble to developers using the framework. Many components and APIs can't be used out of the box and need an additional layer of encapsulation. But this is also the advantage of Remax framework, which only retains the core components and APIs, so that it does not occupy too much size, and it is very easy to be compatible with a new platform.

4, Practical dry goods

4.1 mixed primary

We mentioned earlier that the native code in the public directory will be copied to the dist directory and the pages directory will be merged. We can use this mechanism to reuse the code of existing applets to the greatest extent, which is one of the reasons why our team chose the Remax framework.

Let's experience it. First create a native applet, taking wechat applet as an example:

The file directory of the applet is roughly as follows

 

Now put the code of the whole applet into the public directory

 

At this time, npm run dev wechat will copy pages and utils to the dist directory and merge the pages directory.

 

Although it has been merged, we found that the content of app.json of the native applet is missing. Do you want to write all app.json in Remax? With questions, we carefully read Remax's documents and found that remax.config.js can be configured to dynamically generate app.json:

onAppConfig() {
    ...
      // Get the original applet configuration originConfig and remax app.config.js configuration tmp
    // Do merge processing
    const appJSON =JSON.parse(JSON.stringify(originConfig));
    tmp.pages.forEach(function (item) {
        if (appJSON.pages.indexOf(item) ==-1) {
            appJSON.pages.push(item);
        }
    });
    tmp.subPackages.forEach(function (item) {
        let needAdd =true;
        for (let i =0, a = appJSON.subPackages; i < a.length; i++) {
            if (a[i].root=== item.root) {
                needAdd =false; a[i] = item;break;
            }
        }
        if (needAdd) {
            appJSON.subPackages.push(item);
        }
    });
    ...
    return appJSON;
}

After the above processing, the app.json content of the original applet is merged with the remax.config.js content. The above code only deals with pages and subPackages. If you think there is anything else to be merged, you can also deal with it here. At this time, the product app.json generated by build retains the contents of the original applet and combines the contents of the Remax applet.

The app.js in the product does not have the code in the native applet. What about the original logic. Write it again in Remax? You don't have to.

We can customize a runtime plug-in:

function createBothFun(remaxConfig, originConfig, key) {
    const remaxFun = remaxConfig[key];
    const originFun = originConfig[key];
    return function () {
        // This here is this in the wechat app
        remaxFun.apply(this,arguments);
        originFun.apply(this,arguments);
    };
}
const lifeCycles=['onLaunch','onShow','onHide','onPageNotFound','onError']
function tryMergeConfig(remaxConfig, originConfig) {
    for (const key in originConfig) {
        if (key ==='constructor') {
            console.log('xxx');
        } elseif (lifeCycles.indexOf(key) >=0) {
            remaxConfig[key] =createBothFun(remaxConfig, originConfig, key);
        } else {
            remaxConfig[key] = originConfig[key];
        }
    }
}
const mergeConfig = (remaxConfig, originConfig) => {
    tryMergeConfig(remaxConfig, originConfig);
    return remaxConfig;
};
export default {
    onAppConfig({ config }) {
        let __app = App;
        let originConfig;
        App =function (origin) {
            originConfig = origin;
        };
        __non_webpack_require__('./app-origin.js');
        App = __app;
        //merge config
        config =mergeConfig(config, originConfig);
        const onLaunch = config.onLaunch;
        config.onLaunch= (...args) => {
            if (onLaunch) {
                onLaunch.apply(config, args);
            }
        };
        return config;
    },
};

Rename the original app.js to app-origin.js and use it in the onAppConfig function__ non_webpack_require__('./app-origin.js'); Please note that the relative path here is the relative path in the product. After the above operations, our original applet can really run mixed with Remax.

But in this way, it seems that our Remax cannot cross the end, because it can only be compiled into the native applet types placed in your public directory.

Can mixing and straddling only be fish and bear's paw? Later, we will introduce the use of engineering methods to achieve both fish and bear's paw.

4.2 modular API

You may have noticed that the Remax document lists 10 controls, but I say it has only 9 controls, and the official document also says there are only 9 controls. Why? Because Modal is not strictly a control.

Modal actually calls the createPortal API to create a node covering other contents. On the web side, it uses the createPortal of ReactDOM, and on the applet side, it uses the method with the same name provided in the @ remax/runtime package. In fact, the portal is mounted at different positions at both ends. On the web side, a new div is created directly on the body, while on the applet, it is hung on a node called modalContainer of the page instance. In actual use, it is very inconvenient to use modal components to display pop-up windows, so we still have to turn it into an API call.

Take the applet side as an example:

import...
import { createPortal } from'@remax/runtime';
import {ReactReconcilerInst } from'@remax/runtime/esm/render';


let createPortal__ = createPortal;
let ReactReconcilerInst__ = ReactReconcilerInst;
const styles = {
    modalContainer: {
        ...
    },
};
export default function withModal(TargetComponent) {
    const WrappedModalComponent = (props) => {
        const { mask,...other } = props;
        const component =useRef();
        const container =getCurrentPage().modalContainer;
        return createPortal__(
                  <View style={{ ...styles.modalContainer,pointerEvents: mask ?'auto':'none' }}>
                      <TargetComponent {...other} mask={mask} show={show} close={close} ref={component} />
                  </View>,
                  container
              )
    };
    WrappedModalComponent.hide= (conext) => {
        const container =getCurrentPage().modalContainer;
        if (container._rootContainer) {
            ReactReconcilerInst__.updateContainer(null, container._rootContainer,null,function () {
            });
        }
        container.applyUpdate();
    };
    WrappedModalComponent.show= (props) => {
        const container =getCurrentPage().modalContainer;
        if (!container._rootContainer) {
            container._rootContainer=ReactReconcilerInst__.createContainer(container,false,false);
        }
        const element = React.createElement(WrappedModalComponent, props);
        ReactReconcilerInst__.updateContainer(element, container._rootContainer,null,function () {
        });
        context.modalContainer.applyUpdate();
    };
    return WrappedModalComponent;
}
export { withModal };

Use example:

//Use withModal decorator on components requiring pop-up window
@withModal
export default MyComponent(props) {
  ...
  return<View>{...}</View>
}
//It can also be called directly in the mode that does not support decorators
function MyComponent(props) {
    ...
  return<View>{...}</View>
} 
const ModaledComponent =withModal(MyComponent)
//Use where pop-up windows are required
ModaledComponent.show(props);//Display pop-up window
ModaledComponent.hide();

4.3 Engineering

Considering that our applet is a multi department and multi team project, it is impossible for the whole company to rewrite the original business with Remax at the same time, which will have great uncontrollable risks. Therefore, Remax can only be tried in some businesses and can gradually switch the original businesses, which requires us to have an engineering scheme.

The expected structure of the applet product is as follows:

 

The product structure of the Web side is as follows:

 

This means that the applet side depends on the original applet, and the Web side can publish a single business separately, so we generate two different sets of shell projects for the applet and the Web during the compilation process.

In the process of compiling the applet, pull the shell project. The directory structure of the shell project is roughly as follows:

 

The page codes of remaxA and remaxB are dynamically generated when pulling the shell project. We put a configuration file bundle.js in the shell project to describe the Remax business codes of the shell project:

module.exports= {
  remaxA: {
    git:"git@remaxA.git"
  },
  remaxB: {
    gitL "git@remaxB.git"   
    }
}

While pulling the shell project, the warehouse configured by clonebundle.js is to the temporary directory packages. At this time, the packages directory is as follows:

 

Then, according to app.config.js in Remax business code, new pages and page configurations are re generated in the shell project. The core logic is as follows:

const template = (projectName, path) => {
    return`import ${projectName} from'~packages/${projectName}/src/pages${path ?`/${path}`:''}';
${projectName}.prototype.onShareAppMessage;
${projectName}.prototype.onPageScroll;
${projectName}.prototype.onShareTimeline;
exportdefault ${projectName};
`;
}


const pageHandler = (projectName) => {
    const projectPath =`${rootDir}/packages/${projectName}`;
    shell.cd(projectPath);
    let conf =require(`${projectPath}/src/app.config.js`);
    let platConf = conf[platform] || conf;
    const projectAllPages = [];
    ...
    // Traverse the pages and subPackages configuration and replace the path
    pagePath.replace('pages/',`pages/${projectName}/`);
    ...
    subPackage.root= subPackage.root.replace('pages/',`pages/${projectName}/`);
    ...
    // Merge all page paths in pages subPackages
    let allPages = [...platConf.pages]
    allPages.push(path.join(subPackage.root, page));
    // Traverse the page configuration to generate a new page
    allPages.forEach((mapPath) => {
        const pagePath = path.resolve(rootDir,'src','pages', projectName,`${mapPath}.js`);
        fse.ensureFileSync(pagePath);
        const data =template(projectName, mapPath);
        fs.writeFileSync(pagePath, data);
    });
};
const complier = () => {
    ...
    //Get the git address of the subproject and download it to the packages directory
    const packagesPath = path.resolve(rootDir,'packages');
    const subDirs = fs.readdirSync(packagesPath);
    // Traverse packages and regenerate the page after the merged path according to app.config.js in packages
    subDirs.forEach((name) => {
        let file = fs.statSync(`${packagesPath}/${name}`);
        if (file.isDirectory()) {
            pageHandler(name);
        }
    });
    ...
};
module.exports= complier

The generated page code is as follows:

import remaxA from'~packages/remaxA/src/pages/index/index';
remaxA.prototype.onShareAppMessage;
remaxA.prototype.onPageScroll;
remaxA.prototype.onShareTimeline;
exportdefault remaxA;

You can modify the template in the above code as needed. The reason why this code is similar
remaxA.prototype.onShareAppMessage; This useless code is because Remax will collect the keywords of the life cycle function in the page code during the compilation process, and there will be no unnecessary life cycle in the compiled product when the page code does not appear.

The generated page path is as follows:

 

Similarly, there will be similar operations on the Web side. We publish the Web using the node container, so the shell project becomes the node project. If you use static publishing, you don't need shell engineering. You can publish the product directly by build.

In addition, due to the limitation of the single package size of the applet, some additional configuration needs to be made in the configuration of the applet webpack to avoid the code that multiple Remax services do not jointly rely on being sent to the main package, resulting in the single package size of the main package exceeding the limit. Here is an example for reference only:

 configWebpack:function (options) {
      let config = options.config;
      let subpackageGroups = {};
      Object.keys(projects).forEach((key) => {
          let packagePages = projectsPages[key];
          let allPages = packagePages.allPages.map((page) =>`pages/${key}/${page}`);
          let pages = packagePages.pages;
          subpackageGroups[`${key}Common`] = {
              name:`package-${key}-common`,
              test: (module) =>newRegExp(`[\\/]packages[\\/]${key}[\\/]src[\\/]`).test(module.userRequest),
              chunks:'all',
              minChunks:2,
              minSize:0,
              priority:91,
              filename:`pages/${key}/package-${key}-common.js`,
          };
      });
      config.optimization.merge({
          splitChunks: {
              maxAsyncRequests:100,
              maxInitialRequests:100,
              automaticNameDelimiter:'-',
              enforceSizeThreshold:50000,
              cacheGroups: {
                ...,
                ...subpackageGroups
              },
          },
      });
    },

5, Experience summary

  • Through practice, we find that Remax can write in one place and run everywhere. The disadvantage is that Remax does not provide a complete set of cross-end API s and controls out of the box. Using this framework may have more preliminary basic work. But this is also its advantage. The core dependence is less, and it is completely open. It won't be said that using it will bring a whole family bucket of dependence.
  • Due to the limitation of Remax implementation principle, there will be deficiencies in the performance of complex pages in small programs. Of course, this is also the problem of all cross end frameworks. Fortunately, it can be solved by using custom native components.
  • Remax currently does not support DOM and BOM interfaces, nor does it support the direct use of webhostcomponent to write cross end applets. Therefore, in order to achieve cross end, we still need to make some modifications to the existing React application.
  • Remax does not support the RN platform yet. To say it does not support it does not mean it cannot be supported. If it is compatible with RN, it may have to make some restrictions on the existing React. For example, RN supports css and needs to implement a complete set of RN controls to be compatible with the web side and applet side, which makes Remax less pure.

6, Write at the end

This paper aims to provide you with some new ideas. In terms of type selection, we should consider from many aspects. There may be no obvious difference between good and bad schemes, and the suitable one is the best. Take Taro for example. Thanks to the support of the official team, Taro 3 is developing very fast and has done quite well in all aspects. The Remax community does not seem to be so active, so the development speed is relatively slow. Look forward to more friends to participate in the contribution of open source framework.

[about the author] the front-end R & D team of bus tickets is committed to providing more convenient and intelligent travel modes, and pays attention to the exploration and practice of front-end technology.

Posted on Sun, 07 Nov 2021 00:07:19 -0400 by naveendk.55