詳解前端編譯原理助你成爲高薪前端架構師

編譯器是如何工做的

平常工做中咱們接觸最多的編譯器就是Babel,Babel能夠將最新的Javascript語法編譯成當前瀏覽器兼容的JavaScript代碼,Babel工做流程分爲三個步驟,由下圖所示:css


抽象語法樹AST是什麼

在計算機科學中,抽象語法樹(Abstract Syntax Tree,AST),或簡稱語法樹(Syntax tree),是源代碼語法結構的一種抽象表示。它以樹狀的形式表現編程語言的語法結構,樹上的每一個節點都表示源代碼中的一種結構,詳見維基百科。這裏以const a = 1轉成var a = 1操做爲例看下Babel是如何工做的。html

將代碼解析(parse)成抽象語法樹AST

Babel提供了@babel/parser將代碼解析成AST。前端

const parse = require('@babel/parser').parse;

const ast = parse('const a = 1');
複製代碼

通過遍歷和分析轉換(transform)對AST進行處理

Babel提供了@babel/traverse對解析後的AST進行處理。@babel/traverse可以接收AST以及visitor兩個參數,AST是上一步parse獲得的抽象語法樹,visitor提供訪問不一樣節點的能力,當遍歷到一個匹配的節點時,可以調用具體方法對於節點進行處理。@babel/types用於定義AST節點,在visitor裏作節點處理的時候用於替換等操做。在這個例子中,咱們遍歷上一步獲得的AST,在匹配到變量聲明(VariableDeclaration)的時候判斷是否const操做時進行替換成vart.variableDeclaration(kind, declarations)接收兩個參數kinddeclarations,這裏kind設爲var,將const a = 1解析獲得的AST裏的declarations直接設置給declarationsvue

const traverse = require('@babel/traverse').default;
const t = require('@babel/types');

traverse(ast, {
  VariableDeclaration: function(path) { //識別在變量聲明的時候
    if (path.node.kind === 'const') { //只有const的時候才處理
      path.replaceWith(
        t.variableDeclaration('var', path.node.declarations) //替換成var
      );
    }
    path.skip();
  }
});
複製代碼

將最終轉換的AST從新生成(generate)代碼

Babel提供了@babel/generator將AST再還原成代碼。node

const generate = require('@babel/generator').default;

let code = generate(ast).code;
複製代碼

Vue和React的異同

咱們來看下Vue和React的異同,若是須要作轉化須要有哪些處理,Vue的結構分爲style、script、template三部分編程

style

樣式這部分不用去作特別的轉化,Web下都是通用的小程序

script

Vue某些屬性的名稱和React不太一致,可是功能上是類似的。例如data須要轉化爲stateprops須要轉化爲defaultPropspropTypescomponents的引用須要提取到組件聲明之外,methods裏的方法須要提取到組件的屬性上。還有一些屬性比較特殊,好比computed,React裏是沒有這個概念的,咱們能夠考慮將computed裏的值轉化成函數方法,上面示例中的length,能夠轉化爲length()這樣的函數調用,在React的render()方法以及其餘方法中調用。 Vue的生命週期和React的生命週期有些差異,可是基本都能映射上,下面列舉了部分生命週期的映射 created -> componentWillMount mounted -> componentDidMount updated -> componentDidUpdate beforeDestroy -> componentWillUnmount 在Vue內函數的屬性取值是經過this.xxx的方式,而在Rax內須要判斷是否stateprops仍是具體的方法,會轉化成this.statethis.props或者this.xxx的方式。所以在對Vue特殊屬性的處理中,咱們對於datapropsmethods須要額外作標記。瀏覽器

template

針對文本節點和元素節點處理不一致,文本節點須要對內容{{title}}進行處理,變爲{title} 。 Vue裏有大量的加強指令,轉化成React須要額外作處理,下面列舉了部分指令的處理方式 事件綁定的處理,@click -> onClick 邏輯判斷的處理,v-if="item.show" -> {item.show && ……} * 動態參數的處理,:title="title" -> title={title}bash

還有一些是正常的html屬性,可是React下是不同的,例如style -> className。 指令裏和model裏的屬性值須要特殊處理,這部分的邏輯其實和script裏同樣,例如須要{{title}}轉變成{this.props.title}前端框架

Vue代碼的解析

如下面的Vue代碼爲例

<template>
  <div>
    <p class="title" @click="handleClick">{{title}}</p>
    <p class="name" v-if="show">{{name}}</p>
  </div>
</template>

<style>
.title {font-size: 28px;color: #333;}
.name {font-size: 32px;color: #999;}
</style>

<script>
export default {
  props: {
    title: {
      type: String,
      default: "title"
    }
  },
  data() {
    return {
      show: true,
      name: "name"
    };
  },
  mounted() {
    console.log(this.name);
  },
  methods: {
    handleClick() {}
  }
};
</script>複製代碼

咱們須要先解析Vue代碼變成AST值。這裏使用了Vue官方的vue-template-compiler來分別提取Vue組件代碼裏的templatestylescript,考慮其餘DSL的通用性後續能夠遷移到更加適用的html解析模塊,例如parse5等。經過require('vue-template-compiler').parseComponent獲得了分離的templatestylescriptstyle不用額外解析成AST了,能夠直接用於React代碼。template能夠經過require('vue-template-compiler').compile轉化爲AST值。script@babel/parser來處理,對於script的解析不只僅須要得到整個script的AST值,還須要分別將datapropscomputedcomponentsmethods等參數提取出來,以便後面在轉化的時候區分具體屬於哪一個屬性。以data的處理爲例:

const traverse = require('@babel/traverse').default;
const t = require('@babel/types');

const analysis = (body, data, isObject) => {
  data._statements = [].concat(body); // 整個表達式的AST值

  let propNodes = [];
  if (isObject) {
    propNodes = body;
  } else {
    body.forEach(child => {
      if (t.isReturnStatement(child)) { // return表達式的時候
        propNodes = child.argument.properties;
        data._statements = [].concat(child.argument.properties); // 整個表達式的AST值
      }
    });
  }

  propNodes.forEach(propNode => {
    data[propNode.key.name] = propNode; // 對data裏的值進行提取,用於後續的屬性取值
  });
};

const parse = (ast) => {
  let data = {
  };

  traverse(ast, {
    ObjectMethod(path) {
      /*
      對象方法
      data() {return {}}
      */
      const parent = path.parentPath.parent;
      const name = path.node.key.name;

      if (parent && t.isExportDefaultDeclaration(parent)) {
        if (name === 'data') {
          const body = path.node.body.body;

          analysis(body, data);

          path.stop();
        }
      }
    },
    ObjectProperty(path) {
      /*
      對象屬性,箭頭函數
      data: () => {return {}}
      data: () => ({})
      */
      const parent = path.parentPath.parent;
      const name = path.node.key.name;

      if (parent && t.isExportDefaultDeclaration(parent)) {
        if (name === 'data') {
          const node = path.node.value;

          if (t.isArrowFunctionExpression(node)) {
            /*
            箭頭函數
            () => {return {}}
            () => {}
            */
            if (node.body.body) {
              analysis(node.body.body, data);
            } else if (node.body.properties) {
              analysis(node.body.properties, data, true);
            }
          }
          path.stop();
        }
      }
    }
  });

  /*
    最終獲得的結果
    {
      _statements, //data解析AST值
      list //data.list解析AST值
    }
  */
  return data;
};

module.exports = parse;
複製代碼

最終處理以後獲得這樣一個結構:

app: {
  script: {
    ast,
    components,
    computed,
    data: {
      _statements, //data解析AST值
      list //data.list解析AST值
    },
    props,
    methods
  },
  style, // style字符串值
  template: {
    ast // template解析AST值
  }
}
複製代碼

React代碼的轉化

最終轉化的React代碼會包含兩個文件(css和js文件)。用style字符串直接生成index.css文件,index.js文件結構以下圖,transform指將Vue AST值轉化成React代碼的僞函數。

import { createElement, Component, PropTypes } from 'React';
import './index.css';

export default class Mod extends Component {
  ${transform(Vue.script)}

  render() {
    ${transform(Vue.template)}
  }
}複製代碼

script AST值的轉化不一一說明,思路基本都一致,這裏主要針對Vue data繼續說明如何轉化成React state,最終解析Vue data獲得的是{_statements: AST}這樣的一個結構,轉化的時候只須要執行以下代碼

const t = require('@babel/types');

module.exports = (app) => {
  if (app.script.data && app.script.data._statements) {
    // classProperty 類屬性 identifier 標識符 objectExpression 對象表達式
    return t.classProperty(t.identifier('state'), t.objectExpression(app.script.data._statements));
  } else {
    return null;
  }
};
複製代碼

針對template AST值的轉化,咱們先看下Vue template AST的結構:

{
  tag: 'div',
  children: [{
    tag: 'text'
  },{
    tag: 'div',
    children: [……]
  }]
}
複製代碼

轉化的過程就是遍歷上面的結構針對每個節點生成渲染代碼,這裏以v-if的處理爲例說明下節點屬性的處理,實際代碼中會有兩種狀況: 不包含v-else的狀況,<div v-if="xxx"/>轉化爲{ xxx && <div /> } 包含v-else的狀況,<div v-if="xxx"/><text v-else/>轉化爲{ xxx ? <div />: <text /> }

通過vue-template-compiler解析後的template AST值裏會包含ifConditions屬性值,若是ifConditions的長度大於1,代表存在v-else,具體處理的邏輯以下:

if (ast.ifConditions && ast.ifConditions.length > 1) {
  // 包含v-else的狀況
  let leftBlock = ast.ifConditions[0].block;
  let rightBlock = ast.ifConditions[1].block;

  let left = generatorJSXElement(leftBlock); //轉化成JSX元素
  let right = generatorJSXElement(rightBlock); //轉化成JSX元素

  child = t.jSXExpressionContainer( //JSX表達式容器
    // 轉化成條件表達式
    t.conditionalExpression(
      parseExpression(value),
      left,
      right
    )
  );
} else {
  // 不包含v-else的狀況
  child = t.jSXExpressionContainer( //JSX表達式容器
    // 轉化成邏輯表達式
    t.logicalExpression('&&', parseExpression(value), t.jsxElement(
      t.jSXOpeningElement(
        t.jSXIdentifier(tag), attrs),
      t.jSXClosingElement(t.jSXIdentifier(tag)),
      children
    ))
  );
}
複製代碼

template裏引用的屬性/方法提取,在AST值表現上都是標識符(Identifier),能夠在traverse的時候將Identifier提取出來。這裏用了一個比較取巧的方法,在template AST值轉化的時候咱們不對這些標識符作判斷,而在最終轉化的時候在render return以前插入一段引用。如下面的代碼爲例

<text class="title" @click="handleClick">{{title}}</text>
<text class="list-length">list length:{{length}}</text>
<div v-for="(item, index) in list" class="list-item" :key="`item-${index}`">
  <text class="item-text" @click="handleClick" v-if="item.show">{{item.text}}</text>
</div>複製代碼

咱們能解析出template裏的屬性/方法如下面這樣一個結構表示:

{
  title,
  handleClick,
  length,
  list,
  item,
  index
}
複製代碼

在轉化代碼的時候將它與app.script.data、app.script.props、app.script.computed和app.script.computed分別對比判斷,能獲得title是props、list是state、handleClick是methods,length是computed,最終咱們在return前面插入的代碼以下:

let {title} = this.props;
let {state} = this.state;
let {handleClick} = this;
let length = this.length();
複製代碼

最終示例代碼的轉化結果

import { createElement, Component, PropTypes } from 'React';

export default class Mod extends Component {
  static defaultProps = {
    title: 'title'
  }
  static propTypes = {
    title: PropTypes.string
  }
  state = {
    show: true,
    name: 'name'
  }
  componentDidMount() {
    let {name} = this.state;
    console.log(name);
  }
  handleClick() {}
  render() {
    let {title} = this.props;
    let {show, name} = this.state;
    let {handleClick} = this;

    return (
      <div>
        <p className="title" onClick={handleClick}>{title}</p>
        {show && (
          <p className="name">{name}</p>
        )}
      </div>
    );
  }
}複製代碼

總結與展望

本文從Vue組件轉化爲React組件的具體案例講述了一種經過代碼編譯的方式進行不一樣前端框架代碼的轉化的思路。咱們在生產環境中已經將十多個以前的Vue組件直接轉成React組件,可是實際使用過程當中研發同窗的編碼習慣差異也比較大,須要處理不少特殊狀況。這套思路也能夠用於小程序互轉等場景,減小編碼的重複勞動,可是在這類跨端的非保準Web場景須要考慮更多,例如小程序環境特有的組件以及API等,閒魚技術團隊也會持續在這塊作嘗試。

相關文章
相關標籤/搜索