React18组件一键转换Vue3组件(持续更新中)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 其实现在Vue也是很火的框架随着Vue3的诞生,博主其实最终目标是想整合一套React+一套Vue组件库在一起的,但是重写一遍React的组件很费工作量也不现实,因为我是单人开发,于是就萌生了写一个React组件转换Vue组件的工具,功能性将逐步开发更新到博客,喜欢的可以关注一下

您好,如果喜欢我的文章,可以关注我的公众号「量子前端」,将不定期关注推送前端好文~

需求来源

博主最近一段时间其实是在自研React组件库的业务的,目前也有了大约二十几个组件,这里顺便先留一下组件库的地址哈~

React-View-UI组件库线上链接:http://react-view-ui.com:92/#/
github:https://github.com/fengxinhhh/React-View-UI-fs
npm:https://www.npmjs.com/package/react-view-ui

组件库文档:

在这里插入图片描述
在这里插入图片描述

嗯,先卖个关子,然后回归主题,其实现在Vue也是很火的框架随着Vue3的诞生,博主其实最终目标是想整合一套React+一套Vue组件库在一起的,但是重写一遍React的组件很费工作量也不现实,因为我是单人开发,于是就萌生了写一个React组件转换Vue组件的工具,功能性将逐步开发更新到博客,喜欢的可以关注一下。

市场环境

目前市场上也有这样的转换工具,但是局限性比较高,如React Class组件转换Vue组件,类组件其实在现在用的是很少的,随着React16.8的出现,hooks的到来,函数组件火热,让React的组件写的更加灵活舒适,因此只能选择自研一个转换工具。

效果演示

目前工具已经开发一天,只能实现一些基本的语法转换,后期也会更新工具研发流程,最后会有github,可以关注一下。

目前的转换效果是这样的:
js部分:

在这里插入图片描述
h5部分:
在这里插入图片描述

目前写了除基本模板以外,react的useEffect、useState转换 -> vue的ref、recative、setup、mounted、unmounted、watch。

因此本文目的主要为了记录以及分享开发的过程和思路。

转换

转换的入口就在index.js -> transformOptions变量中,可以配置css为less/scss,配置入口源文件路径和出口路径,在终端执行node src/index即可转换。

const transformOptions = {
   
   
  sourcePath: path.resolve(__dirname, 'sourceFile', 'index.jsx'),
  outputPath: path.resolve(__dirname, 'outputFile', 'index.vue'),
  styleType: 'less',
  componentName: 'Index'
}

设计

入口函数就在index.js的第26行:

readFileFromLine(transformOptions.sourcePath, (res) => {
   
   

}

将转换的配置传入函数,进行处理。readFileFromLine函数可以将我们输入的文件按行输出,变成一个数组,让我们一行一行进行判断处理。

const fs = require('fs');
const readline = require('readline');

const readFileFromLine = (path, callback) => {
   
   
  var fRead = fs.createReadStream(path);
  var objReadline = readline.createInterface({
   
   
    input: fRead
  });
  var arr = new Array();
  objReadline.on('line', function (line) {
   
   
    arr.push(line);
  });
  objReadline.on('close', function () {
   
   
    callback(arr);
  });
}

module.exports = {
   
   
  readFileFromLine
}

这里传入了源文件的路径,并且返回了文件切割后的以行为元素的数组,并且准备了一些全局变量,用于模板的保存、导入依赖包的记录、栈的实例,这里栈主要用作记录任务优先级,举个例子吧,在useEffect中遇到了return()=>,原本在做对应vue mounted的模板处理,应在遇到return()=>之后去做vue unmounted的处理,这就是栈的需求,可以调度我们的任务分配,每行代码会去执行当前栈顶的任务。

设计变量如下:

const vueTemplateList = [];               //vue模板内容
const vueScriptList = [];                 //vue script内容
let fsContent = [];                       //react源文件
let compileStack = new Stack();              //以栈顶值作为优先级最高的编译任务调度,1 -> 常规编译 ,2 -> 副作用函数编译中, 3 -> 模板编译中, 4 -> 引用数据类型状态编译中, 5 -> 组件销毁生命周期
compileStack.unshift(1);
let reactFileHasStateType = [];         //1为基本数据类型ref,2为引用数据类型reactive,3为mounted,4为watch,5为unmounted
let allStateList = new Map();                  //所有状态的列表
let resultFileTotalLine = 0;              //结果文件总行数
let mountedContainer = "";              //mounted临时容器
let unMountedContainer = "";            //unMounted临时容器
let stateContainer = "";                  //复杂state临时容器
let jsxContainer = "";                     //jsx模板临时容器
let jsxCompileParams = {
   
   };              //jsx模板解析时的记录信息
let functionContainer = "";             //函数内容临时容器
let functionModuleNum = 0;              //函数中花括号呈对数,0表示{}成对
  1. vueTemplateList用于保存模板代码段;

  2. vueScriptList用于保存逻辑代码段;

  3. fsContent用于接收入口函数处理后的代码片段(代码行为分割的数组);

  4. compileStack用于分配任务调度优先级,默认为1。1 -> 常规编译 ,2 -> 副作用函数编译中, 3 -> 模板编译中, 4 -> 引用数据类型状态编译中, 5 -> 组件销毁生命周期;

  5. allStateList记录所有的状态,以状态名:状态值为键值对形式保存在map中;

  6. resultFileTotalLine用于在最后记录输出文件的行数;

  7. mountedContainer用于存放在mounted处理时的代码保存容器(任务标识->2);

  8. unMountedContainer用于存放在unmounted处理时的代码保存容器(任务标识->5);

  9. stateContainer用于存放复杂状态的代码段,因为它由多行代码保存(任务标识->4);

  10. jsxContainer用于存放遍历渲染的代码段。(解析到jsx .map转换到v-for时,任务标识->6);

  11. jsxCompileParams用于存放外层渲染的一些数据,如遍历的数组状态名、key值、遍历的元素名称;

  12. functionContainer用于存放函数体的代码段;

  13. functionModuleNum用于判断为函数结束还是函数内块级作用域结束;

有了栈的任务分配,因此在每行代码做处理前,我们需要确定它的任务标识号是多少,从而进行处理,而在某个react关键词结束时,如useEffect ---> ,[]),将任务栈栈顶弹出,转做新的任务,这就是栈的好处。

接下来就是主体处理流程了,先看一下六个条件分支吧:

处理(7个任务)

comipleStack.peek() === 1时,是常规编译,这时会对当前行的代码进行判断,是否有react的一些关键词,如'return('开头应是template处理;'useEffect'应对应vue的mounted/watch处理。
如果捕捉到了关键字,则comipleStack.peek()会改变,同时在下一行代码的处理时直接进行2/3/4/5/6的分支进行对应的编译;如果没有捕捉到关键字,则跳过。

if (compileStack.peek() === 1) {
   
                  //常规编译,捕捉当前行是否为特殊编译
      if (lineItem === 'return(') {
   
           //开始输出模板
        compileStack.unshift(3);
      }
      else if (lineItem.startsWith('const[') && lineItem.includes('useState')) {
   
         //如果是状态声明
        const {
   
   
          returnCodeLine,
          returnAllStateList,
          returnCompileStack,
          returnReactFileHasStateType
        } = saveState(lineItem, allStateList, compileStack, reactFileHasStateType);
        vueScriptList.push(returnCodeLine);
        allStateList = returnAllStateList;
        compileStack = returnCompileStack;
        reactFileHasStateType = returnReactFileHasStateType;
      }
      else if (lineItem.startsWith('useEffect')) {
   
                //副作用函数
        compileStack.unshift(2);
      }
      else {
   
   
        //被舍弃的,跳过循环,不做编译,一些特殊react专属语法,如export default function Index()/import './index.module.less';
      }
    }

comipleStack.peek() === 2时,是副作用函数编译,也就是useEffect转mounted/watch,在这分支中每一行只要捕捉到了'return()=>'则截取上段代码,作为mounted代码片段,并在return()=>之后进行unmounted代码片段的截取,在最后截取的时候,如果是"},[])",代表这是一个mounted函数;如果是
"},[",代表这是一个状态监听函数(vue watch),则切割出副作用状态,进行处理,编译模板。

else if (compileStack.peek() === 2) {
   
             //副作用函数编译中
      const saveCodeResult = saveCodeInUseEffect(allStateList, lineItem, compileStack)
      if (saveCodeResult.action === 'unmounted') {
   
           //调度到unmounted
        compileStack = saveCodeResult.compileStack;
      } else {
   
                           //仍然在执行mounted任务
        if (lineItem.startsWith('},[])')) {
   
          //mounted结束,批量插入
          mountedContainer = '\nonMounted(() => {\n' + mountedContainer + '})\n';
          vueScriptList.push(mountedContainer);
          vueScriptList.push(unMountedContainer);
          mountedContainer = "";
          unMountedContainer = "";
          compileStack.shift();
          if (!reactFileHasStateType.includes(3)) {
   
   
            reactFileHasStateType.push(3);
          }
        } else if (lineItem.startsWith('},[')) {
   
         //编译成watch函数,批量插入
          const params = lineItem.split('[')[1].split(']')[0].split(',');
          vueScriptList.push(formatWatchToVue(params, mountedContainer));
          if (!reactFileHasStateType.includes(4)) {
   
   
            reactFileHasStateType.push(4);
          }
          compileStack.shift();
        } else {
   
   
          mountedContainer += saveCodeResult;
        }
      }
    }

compileStack.peek() === 3代表模板编译,以"return("作为开始(在compileStack.peek() === 1)中会捕获到,并改变,以结尾")"作为模板编译结束,中间对className处理为class。
这里博主功能还没有完善好,后期会做遍历渲染、条件渲染、vue语法糖的转换,这里仅做参考。

else if (compileStack.peek() === 3) {
   
                //模板编译中
      lineItem = lineItem.replace('className', 'class')
      if (lineItem.includes(')') && lineItem.replace(/\s*/g, "") === ')') {
   
                    //模板输出结束
        compileStack.shift();
      }
      else if (lineItem.includes('{') && lineItem.includes('}')) {
   
           //带状态的模板
        vueTemplateList.push(formatStateInTemplate(lineItem));
      } else {
   
   
        vueTemplateList.push(lineItem);
      }
    }
    else if (compileStack.peek() === 4) {
   
            //复杂状态编译中
      if (lineItem.includes(')')) {
   
            // 编译结束
        stateContainer += `\n${
     
     lineItem}`;
        vueScriptList.push(stateContainer);
        stateContainer = '';
        compileStack.shift();
      } else {
   
   
        stateContainer += `${
     
     lineItem}`;
      }
    }

compileStack.peek() === 4代表复杂状态编译,什么是复杂状态编译,比如有这样的状态声明:

const [newList, setNewList] = useState({
   
   
    age: 20,
    hobby: '学习'
  });

这样的状态并不在一行,因此要捕捉到从首行起"({"对应之后的"})",对复杂的状态进行存储,改变。

else if (compileStack.peek() === 4) {
   
            //复杂状态编译中
      if (lineItem.includes(')')) {
   
            // 编译结束
        stateContainer += `\n${
     
     lineItem}`;
        vueScriptList.push(stateContainer);
        stateContainer = '';
        compileStack.shift();
      } else {
   
   
        stateContainer += `${
     
     lineItem}`;
      }
    }

对应的状态处理在这里:

在这里插入图片描述

对应的saveState方法在这里:

const saveState = (lineItem, allStateList, compileStack, reactFileHasStateType) => {
   
              //保存状态
  //处理useState hook
  const stateKey = lineItem.split('[')[1].split(',')[0];
  const stateVal = lineItem.split('useState(')[1].split(')')[0];        //状态值
  let returnCodeLine = '';
  if (!lineItem.includes(')')) {
   
   
    compileStack.unshift(4);
  }
  //判断state 类型,保存
  if (stateVal.startsWith('[') || stateVal.startsWith('{')) {
   
   
    returnCodeLine = `const ${stateKey}=reactive(${stateVal}${
     
     compileStack.peek() === 4 ? '' : ')'}`
    allStateList.set({
   
    state: stateKey, stateAction: `set${
     
     formatUseStateAction(stateKey)}` }, 'reactive');
    if (!reactFileHasStateType.includes(2)) {
   
   
      reactFileHasStateType.push(2);
    }
  } else {
   
   
    returnCodeLine = `const ${stateKey}=ref(${stateVal})`;
    allStateList.set({
   
    state: stateKey, stateAction: `set${
     
     formatUseStateAction(stateKey)}` }, 'ref');
    if (!reactFileHasStateType.includes(1)) {
   
   
      reactFileHasStateType.push(1);
    }
  }
  const returnAllStateList = allStateList;
  const returnCompileStack = compileStack;
  const returnReactFileHasStateType = reactFileHasStateType
  return {
   
   
    returnCodeLine,
    returnAllStateList,
    returnCompileStack,
    returnReactFileHasStateType
  }
}

saveState中就写到了对复杂状态(多行)、简单状态(单行)的分别处理。

compileStack.peek() === 5代表unmounted函数编译,也就是在compileStack.peek() === 2中再次捕捉到的编译处理,此时的优先级unmounted会更高,因为需要处理unmounted的代码,保存,因此任务优先级栈会是[5, 2, 1],当unmounted代码被保存后,则弹出栈顶,继续useEffect的处理。

else if (compileStack.peek() === 5) {
   
              //unmounted函数编译中
      if (lineItem.startsWith('}')) {
   
            //可能unmounted结束了,需要先判断是否是块作用域
        const startIconNum = unMountedContainer.split('{').filter(item => item === '').length;
        const endIconNum = unMountedContainer.split('}').filter(item => item === '').length;
        if (startIconNum === endIconNum) {
   
            //执行unmounted
          compileStack.shift();
          unMountedContainer = 'onUnmounted(() => {\n' + unMountedContainer + '})\n';
          if (!reactFileHasStateType.includes(5)) {
   
   
            reactFileHasStateType.push(5);
          }
        }
      } else {
   
   
        unMountedContainer += saveCodeInUnmounted(allStateList, lineItem, compileStack)
      }
    }

compileStack.peek() === 6 是在任务3中分配出的子任务,也就是循环遍历的解析任务,主要效果为把react jsx的.map转换为v-for语法糖,效果是这样的:

在这里插入图片描述
实现思路是这样的:
在这里插入图片描述
在模板编译任务中,如遇到{,则表示jsx语法开始,将下一行代码的任务分配至6,将会走入我们的主体逻辑。

else if (compileStack.peek() === 6) {
   
               //模板中jsx语法编译中
      if (lineItem.replaceAll(' ', '') === '}') {
   
         //jsx编译结束
        //格式化遍历项dom
        const {
   
    mapArray, mapFnParams, key, mapDomType } = jsxCompileParams;
        if (!key) {
   
              //遍历元素的key值为必填项,否则会影响渲染
          throw new Error('please setting the map dom key!');
        }
        jsxContainer = jsxContainer.substr(0, jsxContainer.length - (jsxCompileParams.mapDomType.length + 6));
        vueTemplateList.push(`<${mapDomType} v-for='${mapFnParams} in ${mapArray}' ${key && ":key='" + key + "'"}>`);
        vueTemplateList.push(jsxContainer)
        vueTemplateList.push(`</${
     
     mapDomType}>`);
        //弹栈,将任务权交回给普通模板编译
        compileStack.shift();
      } else {
   
             //每行代码编译完,更新编译参数集
        const returnParams = compileJsxTemplate(lineItem, jsxCompileParams)
        jsxCompileParams = {
   
    ...returnParams.jsxCompileParams };
        if (returnParams.lineItem) {
   
   
          jsxContainer += returnParams.lineItem + '\n';
        }
      }
    }

这里会在代码为}时认定jsx结束,会整理所保存到的遍历内容,而外层容器(div v-for)则会根据jsxCompileParams来进行手写,先看一下jsxCompileParams有哪些内容吧,看到这张图应该就可以明白了。

在这里插入图片描述
在jsx开始的前3行代码就可以收集到这些信息。

  1. mapArray代表遍历数组;
  2. mapFnParams代表遍历中用到的参数,也就是map函数的两个参数值;
  3. key代表绑定元素的key值;

再来看一下compileJsxTemplate API:

const compileJsxTemplate = (lineItem, jsxCompileParams) => {
   
                 //jsx模板编译
  lineItem = lineItem.replaceAll(' ', '');
  if (lineItem.includes('.map')) {
   
              //遍历渲染
    jsxCompileParams.mapArray = lineItem.split('.map')[0];
    jsxCompileParams.mapFnParams = lineItem.split('.map(')[1].split('=>')[0];
    return {
   
   
      jsxCompileParams
    };
  } else if (lineItem === 'return(') {
   
   
    return {
   
   
      jsxCompileParams
    };
  } else if (lineItem.includes('<') && lineItem.includes('key')) {
   
        //存储遍历key值
    jsxCompileParams.key = lineItem.split('={')[1].split('}')[0];
    jsxCompileParams.mapDomType = lineItem.split('key')[0].split('<')[1];
    return {
   
   
      jsxCompileParams
    };
  } else if (lineItem === ')' || lineItem === '})') {
   
                   //jsx语法结束
    return {
   
   
      jsxCompileParams
    }
  } else {
   
                                       //在key容器下的内层遍历子模板,保存,通常在第三行开始
    return {
   
   
      jsxCompileParams,
      lineItem: formatStateInTemplate(lineItem)
    };
  }
}

主要做的就是解析存储jsxCompileParams以及遍历中间段代码的格式,如{name}转换为{ {name}},在每行代码解析后都会返回所收集到的内容,用于jsx结束时进行处理。

compileStack.peek() === 7是在任务为1时(常规编译)中所捕获到的,在每段代码解析时,如遇到"const"&&"=>"的关键字或者"function"的关键字都会被解析为函数定义,会开启函数的编译,在下一段代码解析时转入函数解析任务中,具体代码:

else if ((lineItem.startsWith('const') && lineItem.includes('=>')) || lineItem.includes('function')) {
   
            //编译函数
  compileStack.unshift(7);
  if ((lineItem.startsWith('const') && lineItem.includes('=>'))) {
   
          //解析出函数信息
    functionParams.functionName = lineItem.split('const')[1].split('=')[0];
    const params = lineItem.split('(')[1].split(')')[0];
    functionParams.params = params;
    if (functionParams.functionName === transformOptions.componentName) {
   
   
      compileStack.shift();
    }
  }
}

该方法会解析出函数名、函数参数列表,在函数定义结束时声明在vueScript中,在最后写入文件时插入。
完成了函数定义,也需要改变一下之前的模板渲染,在元素中遇到onClick时需转换成@click,如遇到jsx传参
onClick{() => hello('name', 12)}则需要转换成@click="hello('name', 12)",因此需要在模板编译时新增判断。

else if (compileStack.peek() === 3) {
   
                //模板编译中
  lineItem = lineItem.replace('className', 'class')
  if (lineItem.replaceAll(' ', '') === '{') {
   
               //jsx语法编译开始
    compileStack.unshift(6)                             //jsx语法编译标识符6入栈,作为最高优先级
  }
  else if (lineItem.includes(')') && lineItem.replace(/\s*/g, "") === ')') {
   
                    //模板输出结束
    compileStack.shift();
  }
  else if (lineItem.includes('{') && lineItem.includes('}')) {
   
           //带状态的模板
    vueTemplateList.push(formatMethodInDom(formatStateInTemplate(lineItem)));
  }
  else {
   
   
    vueTemplateList.push(formatMethodInDom(formatStateInTemplate(lineItem)));
  }
}

在模板编译的任务中,新增了一层函数处理——formatMethodInDom,用于处理在元素中所遇到的事件传参,代码如下:

const formatMethodInDom = (lineItem) => {
   
              //改变一行代码中的onChange -> @change格式转换
  const endTagIndex = lineItem.indexOf('>');
  if (lineItem.includes('on') && endTagIndex > lineItem.indexOf('on')) {
   
   
    lineItem = lineItem.replaceAll('on', ' @');
    lineItem = lineItem.split('@');
    for (let i = 1; i < lineItem.length; i++) {
   
   
      const equalIndex = lineItem[i].indexOf('=');
      const key = lineItem[i].substr(0, equalIndex);
      const value = lineItem[i].substr(equalIndex + 1);
      if (value.includes('=>')) {
   
        //react模板函数传参
        let _andIndex;
        let endTag = value.includes('</') ? false : true;         //如果整个元素在一行,需要从后往前查找第二个>标签
        for (let i = value.length - 1; i >= 0; i--) {
   
   
          if (value[i] === '>' && endTag) {
   
   
            _andIndex = i;
            break;
          }
          if (value[i] === '>') endTag = true;
        }
        const template = value.substr(_andIndex);
        const params = value.split('(')[2].split(')')[0];
        const functionName = value.split('=>')[1].split('(')[0];
        lineItem[i] = `${key.toLowerCase()}="${functionName}(${
     
     params})" `;
        console.log(lineItem[i])
        if (i === lineItem.length - 1) {
   
   
          lineItem[i] += template;
        }

      } else {
   
   
        lineItem[i] = key.toLowerCase() + '=' + value;
      }
    }
    lineItem = lineItem.join('@');

  }
  return lineItem;
}

这段代码的功能其实就是接收一行代码,格式化元素中所有的事件定义。

源码

index.js(入口文件)

const fs = require('fs');
const path = require('path');
const {
   
    readFileFromLine } = require('./write');
const {
   
    Stack } = require('./stack');
const {
   
    computeFileLines } = require('./_utils/computeFileLines')

const {
   
    formatStateInTemplate, saveCodeInUseEffect, saveCodeInUnmounted, formatWatchToVue, saveState, compileJsxTemplate } = require('./compile');

const transformOptions = {
   
   
  sourcePath: path.resolve(__dirname, 'sourceFile', 'index.jsx'),
  outputPath: path.resolve(__dirname, 'outputFile', 'index.vue'),
  styleType: 'less'
}
const vueTemplateList = [];               //vue模板内容
const vueScriptList = [];                 //vue script内容
let fsContent = [];                       //react源文件
let compileStack = new Stack();              //以栈顶值作为优先级最高的编译任务调度,1 -> 常规编译 ,2 -> 副作用函数编译中, 3 -> 模板编译中, 4 -> 引用数据类型状态编译中, 5 -> 组件销毁生命周期, 6 -> 模板中jsx语法编译中
compileStack.unshift(1);
let reactFileHasStateType = [];         //1为基本数据类型ref,2为引用数据类型reactive,3为mounted,4为watch,5为unmounted
let allStateList = new Map();                  //所有状态的列表
let resultFileTotalLine = 0;              //结果文件总行数
let mountedContainer = "";              //mounted临时容器
let unMountedContainer = "";            //unMounted临时容器
let stateContainer = "";                  //复杂state临时容器
let jsxContainer = "";                 //jsx模板临时容器
let jsxCompileParams = {
   
   };          //jsx模板解析时的记录信息

readFileFromLine(transformOptions.sourcePath, (res) => {
   
   
  fsContent = res;
  fsContent = fsContent.filter(line => line !== "");
  fsContent.forEach(lineItem => {
   
   
    if (compileStack.peek() !== 3) {
   
   
      lineItem = lineItem.replace(/\s*/g, "");
    }
    if (compileStack.peek() === 1) {
   
                  //常规编译,捕捉当前行是否为特殊编译
      if (lineItem === 'return(') {
   
           //开始输出模板
        compileStack.unshift(3);
      }
      else if (lineItem.startsWith('const[') && lineItem.includes('useState')) {
   
         //如果是状态声明
        const {
   
   
          returnCodeLine,
          returnAllStateList,
          returnCompileStack,
          returnReactFileHasStateType
        } = saveState(lineItem, allStateList, compileStack, reactFileHasStateType);
        vueScriptList.push(returnCodeLine);
        allStateList = returnAllStateList;
        compileStack = returnCompileStack;
        reactFileHasStateType = returnReactFileHasStateType;
      }
      else if (lineItem.startsWith('useEffect')) {
   
                //副作用函数
        compileStack.unshift(2);
      }
      else {
   
   
        //被舍弃的,跳过循环,不做编译,一些特殊react专属语法,如export default function Index()/import './index.module.less';
      }
    }
    else if (compileStack.peek() === 2) {
   
             //副作用函数编译中
      const saveCodeResult = saveCodeInUseEffect(allStateList, lineItem, compileStack)
      if (saveCodeResult.action === 'unmounted') {
   
           //调度到unmounted
        compileStack = saveCodeResult.compileStack;
      } else {
   
                           //仍然在执行mounted任务
        if (lineItem.startsWith('},[])')) {
   
          //mounted结束,批量插入
          mountedContainer = '\nonMounted(() => {\n' + mountedContainer + '})\n';
          vueScriptList.push(mountedContainer);
          vueScriptList.push(unMountedContainer);
          mountedContainer = "";
          unMountedContainer = "";
          compileStack.shift();
          if (!reactFileHasStateType.includes(3)) {
   
   
            reactFileHasStateType.push(3);
          }
        } else if (lineItem.startsWith('},[')) {
   
         //编译成watch函数,批量插入
          const params = lineItem.split('[')[1].split(']')[0].split(',');
          vueScriptList.push(formatWatchToVue(params, mountedContainer));
          if (!reactFileHasStateType.includes(4)) {
   
   
            reactFileHasStateType.push(4);
          }
          compileStack.shift();
        } else {
   
   
          mountedContainer += saveCodeResult;
        }
      }
    }
    else if (compileStack.peek() === 3) {
   
                //模板编译中
      lineItem = lineItem.replace('className', 'class')
      if (lineItem.replaceAll(' ', '') === '{') {
   
               //jsx语法编译开始
        compileStack.unshift(6)                             //jsx语法编译标识符6入栈,作为最高优先级
      }
      else if (lineItem.includes(')') && lineItem.replace(/\s*/g, "") === ')') {
   
                    //模板输出结束
        compileStack.shift();
      }
      else if (lineItem.includes('{') && lineItem.includes('}')) {
   
           //带状态的模板
        vueTemplateList.push(formatStateInTemplate(lineItem));
      }
      else {
   
   
        vueTemplateList.push(lineItem);
      }
    }
    else if (compileStack.peek() === 4) {
   
            //复杂状态编译中
      if (lineItem.includes(')')) {
   
            // 编译结束
        stateContainer += `\n${
     
     lineItem}`;
        vueScriptList.push(stateContainer);
        stateContainer = '';
        compileStack.shift();
      } else {
   
   
        stateContainer += `${
     
     lineItem}`;
      }
    }
    else if (compileStack.peek() === 5) {
   
              //unmounted函数编译中
      if (lineItem.startsWith('}')) {
   
            //可能unmounted结束了,需要先判断是否是块作用域
        const startIconNum = unMountedContainer.split('{').filter(item => item === '').length;
        const endIconNum = unMountedContainer.split('}').filter(item => item === '').length;
        if (startIconNum === endIconNum) {
   
            //执行unmounted
          compileStack.shift();
          unMountedContainer = 'onUnmounted(() => {\n' + unMountedContainer + '})\n';
          if (!reactFileHasStateType.includes(5)) {
   
   
            reactFileHasStateType.push(5);
          }
        }
      } else {
   
   
        unMountedContainer += saveCodeInUnmounted(allStateList, lineItem, compileStack)
      }
    }
    else if (compileStack.peek() === 6) {
   
               //模板中jsx语法编译中
      if (lineItem.replaceAll(' ', '') === '}') {
   
         //jsx编译结束
        //格式化遍历项dom
        const {
   
    mapArray, mapFnParams, key, mapDomType } = jsxCompileParams;
        if (!key) {
   
              //遍历元素的key值为必填项,否则会影响渲染
          throw new Error('please setting the map dom key!');
        }
        jsxContainer = jsxContainer.substr(0, jsxContainer.length - (jsxCompileParams.mapDomType.length + 6));
        vueTemplateList.push(`<${mapDomType} v-for='${mapFnParams} in ${mapArray}' ${key && ":key='" + key + "'"}>`);
        vueTemplateList.push(jsxContainer)
        vueTemplateList.push(`</${
     
     mapDomType}>`);
        //初始化,用于下一次任务6的处理
        jsxContainer = "";
        jsxCompileParams = {
   
   };
        //弹栈,将任务权交回给普通模板编译
        compileStack.shift();
      } else {
   
             //每行代码编译完,更新编译参数集
        const returnParams = compileJsxTemplate(lineItem, jsxCompileParams)
        jsxCompileParams = {
   
    ...returnParams.jsxCompileParams };
        if (returnParams.lineItem) {
   
   
          jsxContainer += returnParams.lineItem + '\n';
        }
      }
    }
  })

  vueTemplateList.unshift('<template>');
  vueTemplateList.push('</template>\n');
  vueScriptList.unshift('<script setup>');
  vueScriptList.push('</script>\n');

  //处理import ..... from 'vue'  的导入配置
  if (reactFileHasStateType.length) {
   
            //有状态
    let importVal = ''
    if (reactFileHasStateType.includes(1)) {
   
   
      importVal = 'ref';
    }
    if (reactFileHasStateType.includes(2)) {
   
   
      importVal += ',reactive';
    }
    if (reactFileHasStateType.includes(3)) {
   
   
      importVal += ',onMounted';
    }
    if (reactFileHasStateType.includes(4)) {
   
   
      importVal += ',watch';
    }
    if (reactFileHasStateType.includes(5)) {
   
   
      importVal += ',onUnmounted';
    }
    vueScriptList.splice(1, 0, `import{
    
    ${
     
     importVal}}from'vue'`);
  }

  resultFileTotalLine = computeFileLines(vueTemplateList, vueScriptList);

  let resultFile = '';
  vueTemplateList.forEach(line => {
   
   
    resultFile += line + '\n';;
  })
  vueScriptList.forEach(line => {
   
   
    resultFile += line + '\n';
  })
  resultFile += `<style lang="${
     
     transformOptions.styleType}" scoped>\n`;
  readFileFromLine('./index.module.less', (res => {
   
          //写入样式
    if (res) {
   
   
      res.forEach(line => {
   
   
        resultFile += line + '\n';
      })
      resultFile += '</style>';
      resultFileTotalLine += res.length + 3;
      //保存文件
      fs.writeFile(transformOptions.outputPath, resultFile, (err) => {
   
   
        if (!err) {
   
   
          console.log('转换完成,vue文件共有', resultFileTotalLine, '行代码!');
          return
        }
      })
    }
  }))
});

compile.js(index.js中提供的工具函数,与index.js交互,处理代码段)

const {
   
    formatUseStateAction } = require('./_utils/formatuseState')

const formatStateInTemplate = (lineItem) => {
   
            //编译模板中的{
   
   {state}}
  let startIndex = lineItem.indexOf('{');
  let endIndex = lineItem.indexOf('}');
  lineItem = lineItem.split('');
  lineItem.splice(startIndex + 1, 0, '{');
  lineItem.splice(endIndex + 2, 0, '}');
  lineItem = lineItem.join('');
  return lineItem;
}

const saveCodeInUseEffect = (allStateList, lineItem, compileStack) => {
   
                //在useEffect中保存代码片段
  if (lineItem.includes('return()=>')) {
   
                //组件销毁,把任务权先交给销毁
    compileStack.unshift(5);
    return {
   
    compileStack, action: 'unmounted' };
  }
  allStateList.forEach((stateType, key) => {
   
   
    if (lineItem.includes(key.stateAction)) {
   
          //携带状态的特殊副作用函数语句
      let newState = lineItem.match(/\((.+?)\)/gi)[0].replace(/[(|)]/g, "");
      if (newState.includes(key.state)) {
   
   
        newState = newState.replace(key.state, `${
     
     key.state}.value`)
      }
      lineItem = `${key.state}${stateType === 'ref' ? '.value' : ''} = ${
     
     newState}`;
    }
  })
  return `${
     
     lineItem}\n`;
}
const saveCodeInUnmounted = (allStateList, lineItem, compileStack) => {
   
          //在unmounted中保存代码片段
  allStateList.forEach((stateType, key) => {
   
   
    if (lineItem.includes(key.stateAction)) {
   
          //携带状态的特殊副作用函数语句
      const newState = lineItem.match(/\((.+?)\)/gi)[0].replace(/[(|)]/g, "");
      lineItem = `${key.state}${stateType === 'ref' ? '.value' : ''} = ${
     
     newState}`;
    }
  })

  return `${
     
     lineItem}\n`;
}
const formatWatchToVue = (params, code) => {
   
   
  const returnCode = `watch([${params}],(${new Array(params.length).fill('').map((item, index) => {
    return `[{
   
   mathJaxContainer[10]}{
   
   'newValue' + index}]`
  }
  )})=>{\n${
     
     code}})`;
  return returnCode;
}

const saveState = (lineItem, allStateList, compileStack, reactFileHasStateType) => {
   
              //保存状态
  //处理useState hook
  const stateKey = lineItem.split('[')[1].split(',')[0];
  const stateVal = lineItem.split('useState(')[1].split(')')[0];        //状态值
  let returnCodeLine = '';
  if (!lineItem.includes(')')) {
   
   
    compileStack.unshift(4);
  }
  //判断state 类型,保存
  if (stateVal.startsWith('[') || stateVal.startsWith('{')) {
   
   
    returnCodeLine = `const ${stateKey}=reactive(${stateVal}${
     
     compileStack.peek() === 4 ? '' : ')'}`
    allStateList.set({
   
    state: stateKey, stateAction: `set${
     
     formatUseStateAction(stateKey)}` }, 'reactive');
    if (!reactFileHasStateType.includes(2)) {
   
   
      reactFileHasStateType.push(2);
    }
  } else {
   
   
    returnCodeLine = `const ${stateKey}=ref(${stateVal})`;
    allStateList.set({
   
    state: stateKey, stateAction: `set${
     
     formatUseStateAction(stateKey)}` }, 'ref');
    if (!reactFileHasStateType.includes(1)) {
   
   
      reactFileHasStateType.push(1);
    }
  }
  const returnAllStateList = allStateList;
  const returnCompileStack = compileStack;
  const returnReactFileHasStateType = reactFileHasStateType
  return {
   
   
    returnCodeLine,
    returnAllStateList,
    returnCompileStack,
    returnReactFileHasStateType
  }
}

const compileJsxTemplate = (lineItem, jsxCompileParams) => {
   
                 //jsx模板编译
  lineItem = lineItem.replaceAll(' ', '');
  if (lineItem.includes('.map')) {
   
              //遍历渲染
    jsxCompileParams.mapArray = lineItem.split('.map')[0];
    jsxCompileParams.mapFnParams = lineItem.split('.map(')[1].split('=>')[0];
    return {
   
   
      jsxCompileParams
    };
  } else if (lineItem === 'return(') {
   
   
    return {
   
   
      jsxCompileParams
    };
  } else if (lineItem.includes('<') && lineItem.includes('key')) {
   
        //存储遍历key值
    jsxCompileParams.key = lineItem.split('={')[1].split('}')[0];
    jsxCompileParams.mapDomType = lineItem.split('key')[0].split('<')[1];
    return {
   
   
      jsxCompileParams
    };
  } else if (lineItem === ')' || lineItem === '})') {
   
                   //jsx语法结束
    return {
   
   
      jsxCompileParams
    }
  } else {
   
                                       //在key容器下的内层遍历子模板,保存,通常在第三行开始
    return {
   
   
      jsxCompileParams,
      lineItem: formatStateInTemplate(lineItem)
    };
  }
}

module.exports = {
   
   
  formatStateInTemplate,
  saveCodeInUseEffect,
  saveCodeInUnmounted,
  formatWatchToVue,
  saveState,
  compileJsxTemplate
}

这里文中主要涉及到的源码就是这两个文件,具体的可以看一下github。

总结

React组件转换Vue组件的工具(react to vue)目前开工不久,博主在每一段大的变动时都会记录总结,也可以关注github,更新的会比较频繁。

github地址:https://github.com/fengxinhhh/react-to-vue

喜欢可以关注一下,有任何问题或者开发设计的意见也欢迎留言。

目录
相关文章
|
1天前
|
前端开发 JavaScript 开发者
React 按钮组件 Button
本文介绍了 React 中按钮组件的基础概念,包括基本的 `&lt;button&gt;` 元素和自定义组件。详细探讨了事件处理、参数传递、状态管理、样式设置和可访问性优化等常见问题及其解决方案,并提供了代码示例。帮助开发者避免易错点,提升按钮组件的使用体验。
99 77
|
2天前
|
前端开发 UED 开发者
React 对话框组件 Dialog
本文详细介绍了如何在 React 中实现一个功能完备的对话框组件(Dialog),包括基本用法、常见问题及其解决方案,并通过代码案例进行说明。从安装依赖到创建组件、添加样式,再到解决关闭按钮失效、背景点击无效、键盘导航等问题,最后还介绍了如何添加动画效果和处理异步关闭操作。希望本文能帮助你在实际开发中更高效地使用 React 对话框组件。
96 75
|
1月前
|
前端开发 JavaScript 测试技术
React 分页组件 Pagination
本文介绍了如何在 React 中从零构建分页组件,涵盖基础概念、常见问题及解决方案。通过示例代码详细讲解了分页按钮的创建、分页按钮过多、初始加载慢、状态管理混乱等常见问题的解决方法,以及如何避免边界条件、性能优化和用户反馈等方面的易错点。旨在帮助开发者更好地理解和掌握 React 分页组件的开发技巧,提升应用的性能和用户体验。
69 0
|
26天前
|
存储 缓存 JavaScript
如何优化React或Vue应用的性能
需要注意的是,性能优化是一个持续的过程,需要根据具体的应用场景和性能问题进行针对性的优化。同时,不同的项目和团队可能有不同的优化重点和方法,要结合实际情况灵活运用这些优化策略,以达到最佳的性能效果。
107 51
|
7天前
|
前端开发 Java API
React 进度条组件 ProgressBar 详解
本文介绍了如何在 React 中创建进度条组件,从基础实现到常见问题及解决方案,包括动态更新、状态管理、性能优化、高级动画效果和响应式设计等方面,帮助开发者构建高效且用户体验良好的进度条。
35 18
|
22天前
|
存储 前端开发 测试技术
React组件的最佳实践
React组件的最佳实践
|
20天前
|
前端开发 API 开发者
React 文件上传组件 File Upload
本文详细介绍了如何在 React 中实现文件上传组件,从基础的文件选择和上传到服务器,再到解决文件大小、类型限制、并发上传等问题,以及实现多文件上传、断点续传和文件预览等高级功能,帮助开发者高效构建可靠的应用。
48 12
|
15天前
|
存储 前端开发 JavaScript
React 表单输入组件 Input:常见问题、易错点及解决方案
本文介绍了在 React 中使用表单输入组件 `Input` 的基础概念,包括受控组件与非受控组件的区别及其优势。通过具体代码案例,详细探讨了创建受控组件、处理多个输入字段、输入验证和格式化的方法,并指出了常见易错点及避免方法,旨在提升表单的健壮性和用户体验。
27 4
|
22天前
|
前端开发 JavaScript API
React 文件下载组件 File Download
本文介绍了在React中实现文件下载组件的方法,包括使用`a`标签和JavaScript动态生成文件,解决了文件路径、文件类型、大文件下载及文件名乱码等问题,并展示了使用第三方库`file-saver`和生成CSV文件的高级用法。
35 6
|
19天前
|
前端开发 JavaScript API
React 文件下载组件:File Download
本文详细介绍了如何在React应用中实现文件下载组件,包括基本概念、实现步骤和代码示例。同时,探讨了常见问题如文件类型不匹配、文件名乱码等及其解决方法,旨在提升用户体验和代码可维护性。
39 2