微信小程序转换器(一)—— 转换器的实现
发表时间:2021-1-5
发布人:葵宇科技
浏览次数:67
准备
在开始前得先准备点东西:
- 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 = http://www.wxapp-union.com/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 = http://www.wxapp-union.com/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 = http://www.wxapp-union.com/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 = http://www.wxapp-union.com/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套混杂规则,当有问题了,不知道该去遵守哪一套规则,开发体验可能不是特别好。因此我更偏向于靠编译的方式来大概磨平差异,后续迭代也可以选择只编译更新了的部分内容。 以上纯属本人愚见。请不要太在意。