用户
 找回密码
 立即注册

QQ登录

只需一步,快速开始

扫一扫,登录网站

小程序社区 首页 教程 实战教程 查看内容

微信小程序转换器(一)—— 转换器的实现

Rolan 2020-11-17 09:50

这篇大概是个webpack学习的附产物。原理其实挺简单的code -> AST -> 遍历修改AST -> 转换成目标code。总体来说分成两个部分:第一部分:文件的编译,第二部分:递归查找文件。本篇文章讲述的就是文件编译的实现和一 ...

准备

在开始前得先准备点东西:

  • 1、随便学点node的api知识,用来操作文件
  • 2、AST Explorer一个可以看到各种插件转出的AST树结构的网站
  • 3、JS的转换器:@babel/parser(code -> AST)@babel/traverse(用来遍历得到的AST)@babel/generator(AST -> code)
  • 4、HTML的转换器 htmlparser2(HTML -> AST), AST -> HTML 的则需要自己手写
  • 5、CSS的转换器 css-tree 这一个干完所有

工具准备

配置文件

需要个配置文件来标明编译入口

// analyze.config.js
const config = {
    entry: './',
    output: {
        name: 'dist',
        src: './'
    }
}
module.exports = config
复制代码

封装工具方法

// common.js
const path = require("path");
const fs = require("fs");
const config = require(path.resolve('./analyze.config.js'))

// 读文件
function readFile(url) { 
    return fs.readFileSync(url, 'utf8')
}

// 写文件
function writeFile(filename, data) { 
    return fs.writeFileSync(filename, data, 'utf8')
}

// 递归删除文件夹
function deleteall(path) { 
	var files = [];
	if(fs.existsSync(path)) {
		files = fs.readdirSync(path);
		files.forEach(function(file, index) {
			var curPath = path + "/" + file;
			if(fs.statSync(curPath).isDirectory()) { // recurse
				deleteall(curPath);
			} else { // delete file
				fs.unlinkSync(curPath);
			}
		});
		fs.rmdirSync(path);
	}
}

// 复制文件
function copyFile(src, dist) { 
    fs.writeFileSync(dist, fs.readFileSync(src));
}

// 替换属性
function replaceAtrr(option, key, aimsKey) {
    const value = option[key]
    option[aimsKey] = value
    delete option[key]
}

// 获得输入路径
function inputAppPath(url) {
    return url ? path.resolve(config.entry, url) : path.resolve(config.entry)
}

// 获得输出路径
function outputAppPath(url) {
    return url ? path.resolve(config.output.src, config.output.name, url) : path.resolve(config.output.src, config.output.name)

}

复制代码

封装json替换映射表

主要是app.json中的window属性、tabbar属性需要替换。window属性在页面的json里也需要用到。

// compares.js
const WINDOWCONVERTERCONFIG = {
    'navigationBarTitleText':{ target: 'defaultTitle' },
    'enablePullDownRefresh':{ target: 'pullRefresh', 
        handler: (config) => { 
            const enablePullDownRefresh = config['enablePullDownRefresh']
            if (enablePullDownRefresh) config['allowsBounceVertical'] = 'YES'
        }
    },
    'navigationBarTitleText':{ target: 'defaultTitle' },
    'navigationStyle': {
        handler: (config) => {
            if (config['navigationStyle'] == 'custom') {
                config['transparentTitle'] == 'always'
                delete config['navigationStyle']
            }
        }
    }, 
    'navigationBarBackgroundColor':{ target: 'titleBarColor' },
    'onReachBottomDistance':{ target: 'onReachBottomDistance' },
}

const TABBARCONVERTERCONFIG = [
    { originalKey: 'color', key: 'textColor' },
    { originalKey: 'list', key: 'items' , list: [
        { originalKey: 'text', key: 'name' },
        { originalKey: 'iconPath', key: 'icon' },
        { originalKey: 'selectedIconPath', key: 'activeIcon' },
    ]},
]

module.exports = {
    WINDOWCONVERTERCONFIG,
    TABBARCONVERTERCONFIG
}

复制代码

编译文件

编译入口

根据不同类型文件,选择不同类型入口,将入口和源码处理分开来,方便之后类似loader处理的拓展。之后编译部分只用关心输入的源码和输出的代码就可以了。

// analyze.js

// Js编译入口
function buildJs(inputPath, outputPath) {
    let source = readFile(inputPath)
    source = parseJS(source)
    writeFile(outputAppPath(outputPath), source)
    return source
}

// Json编译入口
function buildJson(inputPath, outputPath) {
    let source = readFile(inputPath)
    
    const jonParser = new JsonParser()
    source = jonParser.parser(JSON.parse(source))
    source = JSON.stringify(source)
    
    writeFile(outputAppPath(outputPath), source)
    return source
}

// Html编译入口
function buildXml(inputPath, outputPath) {
    let source = readFile(inputPath)

    parseXML(source).then(code => {
        writeFile(outputAppPath(outputPath), code)
    })
}

// Wxss编译入口
function buildWxss(inputPath, outputPath) {
    let source = readFile(inputPath)
    
    const code = parseCSS(source)
    
    writeFile(outputAppPath(outputPath), code)
}

复制代码

使用转换器

function parseJS(source) {
    const jsParser = new JsParser()
    let ast = jsParser.parse(source)
    ast = jsParser.astConverter(ast)
    return jsParser.astToCode(ast)
}

async function parseXML(source) {
    const templateParser = new TemplateParser()
    let ast = await templateParser.parse(source)
    ast = templateParser.templateConverter(ast)
    return templateParser.astToString(ast)
}

function parseCSS(source) {
    const cssParser = new CssParser()
    let ast = cssParser.parse(source)
    ast = cssParser.astConverter(ast)
    return cssParser.astToCss(ast)
}
复制代码

实现转换器

js转换器封装

// JsParser.js js转换器
const parser = require('@babel/parser')
const generate = require('@babel/generator').default
const traverse = require('@babel/traverse').default

class JsParser{
    constructor() {}

    // code -> ast
    parse(source) {
        let ast = parser.parse(source, {
            sourceType: 'module'
        })
        return ast
    }
    
    // ast 语法树编辑
    astConverter(ast) {
        traverse(ast, {
            MemberExpression(p) {
                let node = p.node
                 // 遍历wx方法调用的节点,并将其替换成my调用
                if (node.object.name == 'wx') {
                    node.object.name = 'my'
                }
            }
        })
        return ast
    }

    // ast -> code
    astToCode(ast) {
        return generate(ast).code
    }
}
复制代码

css转换器封装

// CssParser.js css转换器
const csstree = require('css-tree')

class CssParser {
    constructor() {}

    // code -> ast
    parse(source) {
        const ast = csstree.parse(source)
        return ast
    }

    // ast 语法树编辑
    astConverter(ast) {
        csstree.walk(ast, function(node) {
            if (node.type == 'Atrule' && node.name == 'import') {
                node.prelude.children.forEach(item => {
                    const value = item.value
                    item.value = value.replace('.wxss','.acss')
                });              
            }
        })
        return ast
    }
    
    // ast -> code
    astToCss(ast) {
        return csstree.generate(ast)
    }
}
复制代码

json转换器封装

// JsonParser.js json转换器
const { WINDOWCONVERTERCONFIG } = require('./compares')

class JsonParser{
    constructor() {}
    
    // 替换属性key
    parser(source) {
        function replaceAtrr(orginKey, key) {
            const value = source[orginKey]
            source[key] = value
            delete source[orginKey]
        }

        Object.keys(source).forEach(key => {
            const item = WINDOWCONVERTERCONFIG[key]
            if (item) {
                if (item.target) replaceAtrr(key, item.target)
                item.handler && item.handler(source)
            }  
        })

        return source
    }

}

复制代码

html转换器封装

html编译器比较复杂,因为他的转换库没有提供AST转换HTML的功能,需要自己去实现一下。需要替换的参照表也比较复杂。使用方法参考了这篇

// HtmlTemplateParser.js html转换器
const htmlparser = require('htmlparser2')   //html的AST类库

const ATTRCONVERTERCONFIG = {
    'wx:for':{ target:'a:for', },
    'wx:if':{ target: 'a:if' },
    'wx:elif':{ target: 'a:elif' },
    'else':{ target: 'a:else' },
    'wx:else':{ target: 'a:else' },
    'wx:for-index':{ target: 'a:for-index' },
    'wx:for-item':{ target: 'a:for-item' },
    'wx:key':{ target: 'a:key' },
    'bindtap':{ target: 'onTap' },
    'bindtouchstart':{ target: 'onTouchstart' },
    'bindtouchmove':{ target: 'onTouchMove' },
    'bindtouchend':{ target: 'onTouchEnd' },
    'bindtouchcancel':{ target: 'onTouchCancel' },
    'bindlongtap':{ target: 'onLongTap' },
    'bindlongpress':{ target: 'onLongTap' },
    'catchtap':{ target: 'catchTap' },
    'catchtouchstart':{ target: 'catchTouchstart' },
    'catchtouchmove':{ target: 'catchTouchMove' },
    'catchtouchend':{ target: 'catchTouchEnd' },
    'catchtouchcancel':{ target: 'catchTouchCancel' },
    'catchlongtap':{ target: 'catchLongTap' },
    'catchlongpress':{ target: 'catchLongTap' },
}

function comparesAtrr(attr, key) {
    function replaceAtrr(orginKey, key) {
        const value = attr[orginKey]
        attr[key] = value
        delete attr[orginKey]
    }

    if (ATTRCONVERTERCONFIG[key]) replaceAtrr(key, ATTRCONVERTERCONFIG[key].target)
}

class TemplateParser{
    constructor() {}
	
    // code -> ast
    parse(source){
        return new Promise((resolve, reject) => {
            const handler = new htmlparser.DomHandler((error, dom)=>{
                if (error) reject(error);
                else resolve(dom);
            });
            let parser = new htmlparser.Parser(handler)
            parser.write(source)
            parser.end()
        })
    }
        
    // ast -> code
    astToString (ast) {
        let str = '';
            ast.forEach(item => {
                if (item.type === 'text') {
                str += item.data;
                } else if (item.type === 'tag') {
                str += '<' + item.name;
                if (item.attribs) {
                    Object.keys(item.attribs).forEach(attr => {
                    str += ` ${attr}="${item.attribs[attr]}"`;
                    });
                }
                str += '>';
                if (item.children && item.children.length) {
                    str += this.astToString(item.children);
                }
                str += `${item.name}>`;
                }
            });
        return str;
    }
   
    // ast 语法树编辑
    templateConverter(ast){
        for(let i = 0;i){
          let node = ast[i]
          //检测到是html节点
          if(node.type === 'tag'){
            // 遍历节点属性,对比参照表有没有需要替换的部分
            Object.keys(node.attribs).forEach(key => {
                comparesAtrr(node.attribs, key)
            })
          }
          //因为是树状结构,所以需要进行递归
          if(node.children) this.templateConverter(node.children)
        }
        return ast
    }

}

复制代码
我对跨小程序想说的话和看法

先说一下我对这个事的看法,市面上有antmove已经做到这个事了,但总的来说差异都不可能完全磨平,只能说是同样小程序平台转换上需要更改的成本会比较低。然后我觉得运行时每套方法写一遍来区别不同平台的方案,可能需要同时适应n套混杂规则,当有问题了,不知道该去遵守哪一套规则,开发体验可能不是特别好。因此我更偏向于靠编译的方式来大概磨平差异,后续迭代也可以选择只编译更新了的部分内容。 以上纯属本人愚见。请不要太在意。

鲜花
鲜花
鸡蛋
鸡蛋
分享至 : QQ空间
收藏