This article is probably a by-product of webpack learning. Code -> AST -> iterate over modify AST -> convert to target code.
In general, it is divided into two parts: the first part: file compilation, the second part: recursive search file.
This article is about the implementation of file compilation and some use of the encapsulation.
Micro channel small program converter (a) : the implementation of the converter.
Wechat applet converter (2) : recursive operation file.
Wechat applet converter (iii) : Loader design and implementation.
Wechat applet converter (four) : asynchronous Loader implementation.
To prepare
A few things to prepare before you start:
- 1, learn some node API knowledge, used to operate files
- 2, AST Explorer a site where you can see the AST tree structure of various plug-ins
- 3, JS converter:
@babel/parser(code -> AST)
,@babel/traverse(to traverse the AST)
,@babel/generator(AST -> code)
- 4. HTML converter
htmlparser2(HTML -> AST)
, AST -> HTML by hand - 5, CSS converter
css-tree
This one does all the work
Tools to prepare
The configuration file
A configuration file is required to indicate the build entry
// analyze.config.js
const config = {
entry: '/'.output: {
name: 'dist'.src: '/'}}module.exports = config
Copy the code
Package tool method
// common.js
const path = require("path");
const fs = require("fs");
const config = require(path.resolve('./analyze.config.js'))
/ / read the file
function readFile(url) {
return fs.readFileSync(url, 'utf8')}/ / write file
function writeFile(filename, data) {
return fs.writeFileSync(filename, data, 'utf8')}// Delete folders recursively
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 filefs.unlinkSync(curPath); }}); fs.rmdirSync(path); }}// Copy the file
function copyFile(src, dist) {
fs.writeFileSync(dist, fs.readFileSync(src));
}
// Replace attributes
function replaceAtrr(option, key, aimsKey) {
const value = option[key]
option[aimsKey] = value
delete option[key]
}
// Get the input path
function inputAppPath(url) {
return url ? path.resolve(config.entry, url) : path.resolve(config.entry)
}
// Get the output path
function outputAppPath(url) {
return url ? path.resolve(config.output.src, config.output.name, url) : path.resolve(config.output.src, config.output.name)
}
Copy the code
Encapsulate JSON to replace the mapping table
The window and tabbar properties in app.json need to be replaced. The window property is also used in the json of the page.
// 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
}
Copy the code
Compile the file
Compile the entrance
According to different types of files, select different types of entry, and separate the entry and source code processing to facilitate the expansion of similar loader processing later. After the compilation part only care about the input source code and output code can be.
// analyze.js
// Js compiler entry
function buildJs(inputPath, outputPath) {
let source = readFile(inputPath)
source = parseJS(source)
writeFile(outputAppPath(outputPath), source)
return source
}
// Json compiler entry
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 compiler entry
function buildXml(inputPath, outputPath) {
let source = readFile(inputPath)
parseXML(source).then(code= > {
writeFile(outputAppPath(outputPath), code)
})
}
// Wxss compiler entry
function buildWxss(inputPath, outputPath) {
let source = readFile(inputPath)
const code = parseCSS(source)
writeFile(outputAppPath(outputPath), code)
}
Copy the code
Use converter
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)
}
Copy the code
Implementation converter
Js converter package
// jsparser.js js converter
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 syntax tree editing
astConverter(ast) {
traverse(ast, {
MemberExpression(p) {
let node = p.node
// Iterate over the node of the WX method call and replace it with my call
if (node.object.name == 'wx') {
node.object.name = 'my'}}})return ast
}
// ast -> code
astToCode(ast) {
return generate(ast).code
}
}
Copy the code
CSS Converter Encapsulation
// cssparser.js CSS converter
const csstree = require('css-tree')
class CssParser {
constructor() {}
// code -> ast
parse(source) {
const ast = csstree.parse(source)
return ast
}
// ast syntax tree editing
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)
}
}
Copy the code
Json converter package
// jsonParser.js json converter
const { WINDOWCONVERTERCONFIG } = require('./compares')
class JsonParser{
constructor() {}
// Replace the attribute 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
}
}
Copy the code
HTML converter encapsulation
The HTML compiler is more complicated, because its transformation library does not provide the AST to convert HTML, you need to implement it yourself. The reference tables that need to be replaced are also complex. Refer to this article for instructions
// htmltemplateParser.js HTML converter
const htmlparser = require('htmlparser2') // HTML AST class library
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 syntax tree editing
templateConverter(ast){
for(let i = 0; i<ast.length; i++){let node = ast[i]
// An HTML node is detected
if(node.type === 'tag') {// Walk through the node attributes and compare the reference table to see if there are any parts that need to be replaced
Object.keys(node.attribs).forEach(key= > {
comparesAtrr(node.attribs, key)
})
}
// Because it is a tree structure, we need to recurse
if(node.children) this.templateConverter(node.children)
}
return ast
}
}
Copy the code
What I want to say about cross-applet
First of all, let me tell you my opinion on this matter. There are antMove on the market that have done this matter, but in general, the difference can not be completely smoothed, only that the cost of changing the same small program platform will be relatively low. Then I felt that the runtime might have to adapt to n sets of promiscuous rules at the same time by writing each set of methods to distinguish between different platforms. When there was a problem, I did not know which set of rules to follow, and the development experience might not be particularly good. Therefore, I prefer to roughly smooth out the differences by compilation, or I can choose to compile only the updated parts of the content in subsequent iterations. The above is my humble opinion. Please don’t take it too seriously.