prepare

Here’s a look at how the three platforms describe cross-platform adaptation

  • Taro
Taro is designed to unify the cross-platform development approach, and has made efforts to bridge the differences through runtime frameworks, components, and apis. However, there are still some irreconcilable differences between different platforms. In order to better achieve cross-platform development, Taro provides the following solutions: Built-in environment variables... To make it easier for people to write style code across styles, a style conditional compilation feature has been added.Copy the code
  • Chameleon
CML is a multi-terminal upper-layer application language. Under such a goal, it is particularly important to ensure the consistency of business code and communication between different ends when users extend functions. . Above, cross-ends are nice, but the biggest risk is maintainability. Polymorphic layer protocol is CML business code and the underlying components and interfaces on either side of the cut-off point, CML will strictly "controls" input type and structure of output value, at the same time will be strict inspection business layer JS code, avoid direct use of a backend interface, are not allowed in public code using a specific method, even if do not perform this code, For example, methods such as' window ', 'wx', 'my', 'swan', and 'weex' are prohibited.Copy the code
  • uniApp
Uni-app has encapsulated common components and JS apis into the framework. Developers can develop according to uni-App specifications to ensure multi-platform compatibility, and most services can be directly satisfied. But each platform has some features of its own, so there are some situations where it can't be cross-platform. - Excessive if else writing can lead to poor code performance and confusing management. - Compiling to a different project and then modifying it twice will make subsequent upgrades very troublesome. C uses #ifdef, #ifndef to compile different code for Windows, MAC, and other oss. 'UNI-app' references this idea and provides a conditional compilation method for 'Uni-App', which elegantly implements platform personalization in one project.Copy the code

Above you can see every open source framework can guarantee 100% across the end user to use the framework to completely no matter compatibility problems, just help the most compatible development to solve the problem, according to some platform features problem difficult to compatible parts, still need developers to complete, that they are how to realize the processing of this part is compatible with, we have to pull open the coat, Looking at the nitty-gritty, it’s up to you to see which implementation is the most elegant…

start

Taro

Built-in environment variables

Process.env. TARO_ENV is used to determine the current compilation type. Currently, there are eight values: appellate/swan/Alipay/h5 / RN/TT/QQ/QuickApp, which can be used to write code corresponding to some different environments. During compilation, the code that does not belong to the current compilation type will be removed, and only the code under the current compilation type will be retained. For example, different resources will be referenced in wechat applet and H5 respectively

if (process.env.TARO_ENV === 'weapp') {
  require('path/to/weapp/name')
} else if (process.env.TARO_ENV === 'h5') {
  require('path/to/h5/name')
}
Copy the code

This implementation, which is familiar to developers who have used WebPack, is implemented by injecting webpack.DefinePlugin into Webpack and enabling tree-shaking to filter out unused code from compatible platforms during webpack compilation. That Taro is where to deal with it, we look at the source of Taro

  1. First we usetaro-cliProvide the initialization project after it is inpackage.jsonProvides multiple platform compilation methods
"scripts": {
    "build:swan": "taro build --type swan",
    "build:weapp": "taro build --type weapp",
    "build:alipay": "taro build --type alipay",
	...
  },
Copy the code

You can see that when scripts are run, vscode opens the next branch of the Taro source using the value of TARO_ENV passed in by –type

// packages/taro-cli/src/cli.ts
customCommand('build', kernel, {
            _: args._,
            platform,
            plugin,
            isWatch: Boolean(args.watch),
            ...
          })
Copy the code

Taro-cli passes the type passed in from the command line to the Kernel using the platform variable. The Kernel is located in the taro-service subpackage, which provides real-time compilation as the basic service. Taro’s core is to flexibly implement different command combinations by using the Kernel + registered plug-in + lifecycle hook function

  1. The Kernel calls mini-runner to build, passing platform to buildAdapter processing
//packages/taro-service/ SRC /platform-plugin-base.ts /** * Prepare mini-runner parameters * @param extraOptions requires additional configuration items to be added to Options */  protected getOptions (extraOptions = {}) { const { ctx, config, globalObject, fileType, template } = this return { ... buildAdapter: config.platform, ... @param extraOptions requires additional @tarojs/mini-runner configuration items */ private Async build (extraOptions)  = {}) { this.ctx.onBuildInit? .(this) await this.buildTransaction.perform(this.buildImpl, this, extraOptions) } private async buildImpl (extraOptions) { const runner = await this.getRunner() const options = this.getOptions(Object.assign({ runtimePath: this.runtimePath, taroComponentsPath: this.taroComponentsPath }, extraOptions)) await runner(options) }Copy the code

3. Taro -mini-runner execute build method in build.config.ts and use it in buildexport const getDefinePlugin = pipe(mergeOption, listify, partial(getPlugin, webpack.DefinePlugin))The introduction ofwebpack.DefinePlugin

export default (appPath: string, mode, config: Partial<IBuildConfig>): any => { const chain = getBaseConf(appPath) const { buildAdapter = PLATFORMS.WEAPP, ... } = config ... env.TARO_ENV = JSON.stringify(buildAdapter) const runtimeConstants = getRuntimeConstants(runtime) const constantsReplaceList = mergeOption([processEnvOption(env), defineConstants, runtimeConstants]) const entryRes = getEntry({ sourceDir, entry, isBuildPlugin }) ... plugin.definePlugin = getDefinePlugin([constantsReplaceList]) chain.merge({ mode, devtool: getDevtool(enableSourceMap, sourceMapType), entry: entryRes! .entry, ... plugin, optimization: { ... }})... return chain }Copy the code

Pass buildAdapter as env.taro_env to webpack.definePlugin and use WebPack to pack and do difference handling

Conditional compilation of styles

The above webpack.DefinePlugin can be tree-shaking for TS/JS code.

  • For styling, styling RN directly replaces the entire CSS code
When referencing the style file 'import './index.scss'' in a JS file, RN will find and introduce 'index.rn. SCSS', and other platforms will introduce: 'index.scss' to make it easier for people to write cross-end styles that are more compatible with RN.Copy the code

If we, as Taro developers, determine whether the SCSS file corresponding to webpack has.rn. SCSS file in the loader plugin, we can directly change the imported file. Where to implement these operations in Taro?

  1. Define the CSS suffix file
// packages/taro-helper/src/constants.ts
export const CSS_EXT: string[] = ['.css', '.scss', '.sass', '.less', '.styl', '.stylus', '.wxss', '.acss']
export const JS_EXT: string[] = ['.js', '.jsx']
export const TS_EXT: string[] = ['.ts', '.tsx']
Copy the code
  1. Determine the compilation platform and select the style file for the platform first
//packages/taro-rn-supporter/src/utils.ts // lookup modulePath if the file path exist // import './a.scss'; import './app'; import '/app'; import 'app'; import 'C:\\\\app'; function lookup (modulePath, platform, isDirectory = false) { const extensions = ([] as string[]).concat(helper.JS_EXT, helper.TS_EXT, helper.CSS_EXT) const omitExtensions = ([] as string[]).concat(helper.JS_EXT, helper.TS_EXT) const ext = path.extname(modulePath).toLowerCase() const extMatched = !! extensions.find(e => e === ext) // when platformExt is empty string('') it means find modulePath itself const platformExts = [`.${platform}`, '.rn', ''] // include ext if (extMatched) { for (const plat of platformExts) { const platformModulePath = Modulepath.replace (ext, '${plat}${ext}') // Check whether there is a platform suffix file, and return the platform suffix file if there is one. Replace the default if (fs.existssync (platformModulePath)) {return platformModulePath}}} // handle some omit situations for (const plat of platformExts) { for (const omitExt of omitExtensions) { const platformModulePath = `${modulePath}${plat}${omitExt}` if (fs.existsSync(platformModulePath)) { return platformModulePath } } } // it is lookup in directory and the file path not exists, then return origin module path if (isDirectory) { return path.dirname(modulePath) // modulePath.replace(/\/index$/, '') } // handle the directory index file const moduleIndexPath = path.join(modulePath, 'index') return lookup(moduleIndexPath, platform, true) }Copy the code
  • Taro has introduced conditional compilation to deal with the problem of using different platform-compatible styles in a single style file as follows:
/* #ifdef %PLATFORM% */ #endif */ #ifndef %PLATFORM% */Copy the code

Taro uses the postCSS plugin to determine whether to intercept valid code inside comments through css.walkComments

/* #ifdef %PLATFORM% */ * #endif */ css.walkComments(comment => { If (wordlist.indexof ('#ifdef') > -1) {const wordList = comment.text.split(" ") // specify platform to keep if (wordlist.indexof ('#ifdef') > -1) {// specify platform to keep if (wordlist.indexof (options.platform) === -1) {let next = comment.next() While (next) {if (next-.type === 'comment' &amp; &amp; next.text.trim() === '#endif') { break } const temp = next.next() next.remove() next = temp } } } }) /* #ifndef /* #endif */ css.walkComments(comment => {const wordList = comment.text.split(' ') // specify PLATFORM to discard if (wordlist.indexof ('#ifndef') > -1) {if (wordlist.indexof (options.platform) > -1) {let next = comment.next() While (next) {if (next-.type === 'comment' &amp; &amp; next.text.trim() === '#endif') { break } const temp = next.next() next.remove() next = temp } } } })Copy the code

conclusion

  • advantages

Easy to understand, for front-end developers are more familiar, are more traditional concepts, easy to understand

  • disadvantages
  1. There are a lot of if/else in TS/JS code, which can become difficult to maintain later
  2. When using external NPM packages, require is required, import is not allowed, tree-shaking will not work (the principle of tree-shaking is dependent on ES6 modules)

Chameleon

Chameleon proposed the concept of polymorphic protocol, which can be divided into four types: polymorphic interface/polymorphic component/polymorphic template/style polymorphism to distinguish the adaptation of multi-platform side.

  1. More than one interface
<script cml-type="wx">
class Method implements UtilsInterface {
 getMsg(msg) {
   return 'wx:' + msg;
 }
}
export default new Method();
</script>
Copy the code
  1. Polymorphic components
< template c - else - if = "{{ENV = = = 'wx'}}" > / / suppose wx - the list is WeChat applet native components < wx - list data = "{{list}}" > < / wx - list > < / template >Copy the code
  1. Polymorphic template
<template class="demo-com"> < CML type="wx"> <view> WX side render with this code </view> <demo-com title=" I am the title wx"></demo-com> </ CML >< CML Type ="base"> <view > Render with type='base' if the corresponding code is not found, </view >< demo-com title=" I am the title base"></demo-com> </ CML ></ template>Copy the code
  1. The style of polymorphic
<style> @media cmL-type (supported platform) {}. Common {/**/} <style>Copy the code

Source code core library:

Here, we only need to pay attention to Chameleon’s conversion of code into DSL protocol for multi-platform condition judgment, and use Babel to transform into AST syntax tree. During the parsing of AST syntax tree, tapable controls the processing mode of each node. For example, tag parsing, style syntax parsing, circular statements, conditional statements, native component use, dynamic component parsing, etc., to meet the requirements of different end adaptation, each end adaptation independent of each other, support rapid adaptation multi-end. The overall architecture of CML template parsing is shown below

// packages/chameleon-template-parse/ SRC /common/process-template.js /* Provides code to chameleon-loader that is used to remove unnecessary code from other parts of a modal template @params:source template content @params:type The platform currently being compiled for intercepting polymorphic templates @params:options needTranJSX needs to be converted to a template that JSX can parse; NeedDelTemplate need to delete the template node * / exports in preParseMultiTemplate = function (the source, type, Options = {}) {try {if (options.needtranjsx) {// If JSX is not escaped before calling this method, then let callbacks = should be escaped ['preDisappearAnnotation', 'preParseGtLt', 'preParseBindAttr', 'preParseVueEvent', 'preParseMustache', 'postParseLtGt'];  source = exports.preParseTemplateToSatisfactoryJSX(source, callbacks); } let isEmptyTemplate = false; const ast = babylon.parse(source, { plugins: ['jsx'] }) traverse(ast, { enter(path) { let node = path.node; if (t.isJSXElement(node) &amp; &amp; (node.openingElement.name &amp; &amp; typeof node.openingElement.name.name === 'string' &amp; &amp; node.openingElement.name.name === 'template')) { path.stop(); / / don't child nodes of traversal, because the only need to deal with the template let {hasCMLTag hasOtherTag, jsxElements} = exports. CheckTemplateChildren (path); if (hasCMLTag &amp; &amp; HasOtherTag) {throw new Error(' In polymorphic templates only CML tags are allowed under the template tag '); } if (hasCMLTag &amp; &amp; ! HasOtherTag) {/ / in line with the polymorphism of the template structure format let currentPlatformCML = exports. GetCurrentPlatformCML (jsxElements, type); if (currentPlatformCML) { currentPlatformCML.openingElement.name.name = 'view'; // There is no closingElement, so make a judgment; currentPlatformCML.closingElement &amp; &amp; (currentPlatformCML.closingElement.name.name = 'view'); node.children = [currentPlatformCML]; If (options.needdelTemplate) {// Replace the template node with the found CML type node; Path. replaceWith(currentPlatformCML)}} else {// Throw new if CML type=' XXX 'or CML type='base' Error(' there is no platform template or base template ')}} else {// Not polymorphic template // Note that empty templates are considered if (options.needdelTemplate &amp; &amp; JsxElements. Length === 1) {// Replace the template node with the found CML type node; path.replaceWith((jsxElements[0])); } else { isEmptyTemplate = true; }}}}}); // When you pass through Babel, you need to escape it; if (isEmptyTemplate) { return ''; } source = exports.postParseUnicode(generate(ast).code); if (/; $/.test(source)) {$/.test(source)) {$/.test(source)) { The string; But in HTML; Is unresolvable; source = source.slice(0, -1); } return source; } catch (e) { console.log('preParseMultiTemplate', e) } }Copy the code

Ast code analysis is performed here to remove code that is not on the corresponding platform to ensure the purity of the code packaged to the corresponding platform. And the processing of style polymorphism, directly use the regular match to determine whether to delete the style, and then loop iteration to intercept the platform style, delete the style is not needed!! #ff0000 (code with detailed algorithm description)!!

// packages/chameleon-css-loader/parser/media.js module.exports = function parse(source = '', targetType) { let reg = /@media\s*cml-type\s*\(([\w\s,]*)\)\s*/g; if (! reg.test(source)) { return source; } reg.lastIndex = 0; /** * If: @media cmL-type (wx) {body {}} * */ while (true) {// eslint-disable-line // Find all @media CML-type (wx) styles in the style, Let result = reg.exec(source); if (! result) {break; } let cmlTypes = result[1] || ''; cmlTypes = cmlTypes.split(',').map(item => item.trim()); let isSave = ~cmlTypes.indexOf(targetType); let startIndex = result.index; // @media let currentIndex = source.indexof ('{', startIndex); // let signStartIndex = currentIndex; If (currentIndex == -1) {throw new Error("@media cmL-type format err"); } let signStack = []; Signstack.push (0); signstack.push (0); /* Check @media cmL-type (wx) {}, and find the position of @media {}. Index1 and index2 are not negative 1, so index is the one after body, SignStack = [0, 1] For the second loop, currentIndex starts from body {next character, index1 is -1, CurrenIndex = 1; currenIndex = 1; currenIndex = 1; currenIndex = 1; currenIndex = 1; SignStack = [0]; signStack = [0]; signStack = [0]; CurrentIndex starts after body {}, index1 is -1, index2 matches body {},} is not -1, CurrenIndex = body{}; currenIndex = body{}; SignStack = []; signStack = 1; Matches a {push signStack + 1, */ while (signstack.length > 0) {let index1 = source.indexOf('{', currentIndex + 1); Let index2 = source.indexof ('}', currentIndex + 1); } let index; // if (index1! == -1 &amp; &amp; index2 ! == -1) { index = Math.min(index1, index2); } else { index = Math.max(index1, index2); } if (index === -1) { throw new Error("@media cml-type format err"); } let sign = source[index]; currentIndex = index; If (sign === '{') {signstack.push (signstack.length); if (sign === '{') {signstack.push (signstack.length); } else if (sign === '}') { signStack.pop(); }} @media var sourceArray = array. from(source); /** * array.splice (index, remove_count, item_list) * startIndex @media, Currentindex-startindex + 1: @media {... } * source.slice(signStartIndex + 1, currentIndex) {} */ sourcearray.splice (startIndex, sourcearray.splice) currentIndex - startIndex + 1, source.slice(signStartIndex + 1, currentIndex)); source = sourceArray.join(''); } else {// delete source.slice(0, startIndex) from @media {... } * source.slice(currentIndex + 1); */ source = source.slice(0, startIndex) + source.slice(currentIndex + 1); } reg.lastIndex = 0; } return source; }Copy the code

conclusion

  • advantages
  1. Code isolation is clear and does not pollute code
  2. Strong isolation is implemented after compilation, with no unwanted code introduced
  • disadvantages
  1. Since the development of polymorphic protocol, front-end new concept, start the threshold
  2. Based on the underlying DSL parsing and AST parsing, the underlying architecture is highly dependent

uniapp

Uniapp ADAPTS to different platforms through API/ component/style conditional compilation

  1. API conditional compilation
// #ifdef ** %PLATFORM%** #endifCopy the code
  1. Component conditional compilation
<! -- #ifdef ** %PLATFORM%** --> PLATFORM specific components <! -- #endif -->Copy the code
  1. Style conditional compilation
/* #ifdef ** %PLATFORM% ** */Copy the code

Uniapp uses XRegExp regular library to match file string to loop out the content of corresponding platform and delete the content of other platforms. XRegExp provides enhanced and extensible JavaScript regular expressions. The address is github.com/slevithan/x… The conditional judgment re used is as follows:

//packages/uni-cli-shared/lib/preprocess/lib/regexrules.js js : { if : { start : "[ \t]*(? ://|/\\*)[ \t]*#(ifndef|ifdef|if)[ \t]+([^\n*]*)(? : \ \ * (? : \ \ * | /))? (? :[ \t]*\n+)?" , end : "[ \t]*(? ://|/\\*)[ \t]*#endif[ \t]*(? : \ \ * (? : \ \ * | /))? (? :[ \t]*\n)?" },... }}Copy the code

Uni-app contains a lot of packages. The conditional compiled package in master repository is webpack-preprocess-loader. The next version using packages/uni – cli – Shared/lib/the preprocess/lib/the preprocess js directly in the compile phase processing, master used in webpack plug-in conditional compilation in the form of processing, In the next version, you can switch to the next generation of packaging tools, such as Vite, to experience the second-level development experience of the development environment

conclusion

  • advantages
  1. Simple operation, C language development experience is more friendly, isolation effect is strong, will not cause code pollution
  2. Conditional compilation can also be done on the import mode after compilation phase processing
  • disadvantages
  1. There was some code intrusion

    Thank you waste valuable time to read here, as a comprehensive frame, and the natural faces multiple platforms adaptation, although framework is from the bottom to help developers solve most of the cross-platform compatible, then no rounded melons, no pure gold gold, met some questions platform features, still need a developer to on-demand adaptation. The above is all the content of the difference realization, there are improper, please criticize and correct…