深入wepy原始碼:wpy檔案編譯過程

yingye發表於2019-03-03

wepy 是騰訊開源的一款小程式框架,主要通過預編譯的手段,讓開發者採用類 Vue 風格開發。 讓我們一起看看, wepy 是如何實現預編譯的。先放上一張官網的流程圖,後面的分析可以參考該圖。

深入wepy原始碼:wpy檔案編譯過程

wepy-cli 主要負責 .wpy 檔案的編譯,目錄結構如下:

深入wepy原始碼:wpy檔案編譯過程

編譯的入口是 src/compile.js 中的 compile() 方法,該方法主要是根據檔案型別,執行不同的 compiler ,比如 .wpy 檔案會走 compile-wpy.js 下的 compile() 方法。

compile(opath) {
  ...
  switch(opath.ext) {  
    case ext:  
      cWpy.compile(opath);  
      break;  
    case '.less':  
      cStyle.compile('less', opath);  
      break;  
    case '.sass':  
      cStyle.compile('sass', opath);  
      break;  
    case '.scss':  
      cStyle.compile('scss', opath);  
      break;  
    case '.js':  
      cScript.compile('babel', null, 'js', opath);  
      break;  
    case '.ts':  
      cScript.compile('typescript', null, 'ts', opath);  
      break;  
    default:  
      util.output('拷貝', path.join(opath.dir, opath.base)); 
    ... 
  } 
}  
複製程式碼

.wpy檔案拆解

compile-wpy.js 下的 compile() 方法,核心呼叫了 resolveWpy() 方法。

resolveWpy() 方法,主要是將 .wpy 拆解成 rst 物件,並對其中的 template、script 做一些預處理,然後將 template、 script、 style 三部分移交給不同的 compiler 處理。

生成rst物件

通過 xmldom 獲取 xml 物件,然後遍歷節點,拆解為 rst物件。

import {DOMParser} from 'xmldom';
export default {
  createParser (opath) {
	return new DOMParser({
	  ...
	})
  },
  ...
  resolveWpy () {
    let xml = this.createParser(opath).parseFromString(content);
  }
}
複製程式碼

rst物件結構如下:

let rst = {
  moduleId: moduleId,
  style: [],
  template: {
    code: '',
    src: '',
    type: ''
  },
  script: {
    code: '',
    src: '',
    type: ''
  }
}; 
複製程式碼

此外,還對 template 做了如下一些預處理:

  • pug 預編譯
  • 獲取檔案中的 import ,放入 rst.template.components
  • 獲取 propsevents ,放入 rst.script.code

compile-template

compile-template.js 中的 compile() 方法,根據 template 的 lang 值,執行不同的 compiler ,比如 wepy-compile-typescript 。編譯完成後,執行 compileXML 方法,做了如下的操作:

  • updateSlot 方法: 替換 slot 內容
  • updateBind 方法: 在 {{}} 和 attr 上加入元件的字首,例如:{{width}} -> {{$ComponentName$width}}
  • 把自定義的標籤、指令轉換為 wxml 語法,例如:
<repeat for="xxx" index="idx" item="xxx" key="xxx"></repeat>
<!-- 轉換為 -->
<block wx:for="xxx" wx:for-index="xxx" wx:for-item="xxx" wx:key="xxxx"></block>
複製程式碼

compile-style

依舊先是根據 lang 值,先執行不同的 compiler ,比如 wepy-compile-less 。編譯完成後,執行 src/style-compiler/scope.js 中的 scopedHandler() 方法,處理 scoped

import postcss from 'postcss';
import scopeId from './scope-id';

export default function scopedHandler (id, content) {
  console.log('id is: ', id)
  console.log('css content is: ', content)
  return postcss([scopeId(id)])
    .process(content)
    .then(function (result) {
      console.log('css result is: ', result.css)
      return result.css
    }).catch((e) => {
      return Promise.reject(e)
    })
}
複製程式碼

這裡主要是利用 add-id 的 postcss 外掛,外掛原始碼可參考 src/style-compiler/scope-id.js。根據上面的程式碼,列印出來的log如下:

深入wepy原始碼:wpy檔案編譯過程

最後,會把 requires 由絕對路徑替換為相對路徑,並在 wxss 中引入,最終生成的 wxss 檔案為:

@import "./../components/demo.wxss";

Page{background:#F4F5F7} ...  
複製程式碼

compile-script

依舊先是根據 lang 值,執行不同的 compiler。compiler 執行完之後,判斷是否是 npm 包,如果不是,依據不同的 type 型別,加入 wepy 初始化的程式碼。

if (type !== 'npm') {
  if (type === 'page' || type === 'app') {
    code = code.replace(/exports\.default\s*=\s*(\w+);/ig, function (m, defaultExport) {
      if (defaultExport === 'undefined') {
        return '';
      }
      if (type === 'page') {
        let pagePath = path.join(path.relative(appPath.dir, opath.dir), opath.name).replace(/\\/ig, '/');
        return `\nPage(require('wepy').default.$createPage(${defaultExport} , '${pagePath}'));\n`;
      } else {
        appPath = opath;
        let appConfig = JSON.stringify(config.appConfig || {});
        let appCode = `\nApp(require('wepy').default.$createApp(${defaultExport}, ${appConfig}));\n`;
        if (config.cliLogs) {
          appCode += 'require(\'./_wepylogs.js\')\n';
        }
        return appCode;
      }
    });
  }
}
複製程式碼

接下來會執行 resolveDeps() 方法,主要是處理 requires。根據 require 檔案的型別,拷貝至對應的目錄,再把 code 中的 require 程式碼替換為 相對路徑。

處理好的 code 最終會寫入 js 檔案中,檔案儲存路徑會判斷型別是否為 npm。

let target;
if (type !== 'npm') {
  target = util.getDistPath(opath, 'js');
} else {
  code = this.npmHack(opath, code);
  target = path.join(npmPath, path.relative(opath.npm.modulePath, path.join(opath.dir, opath.base)));
}
複製程式碼

plugin

根據上面的流程圖,可以看出所有的檔案生成之前都會經過 Plugin 處理。先來看一下,compiler 中是如何載入 Plugin 的。

let plg = new loader.PluginHelper(config.plugins, {
  type: 'css',
  code: allContent,
  file: target,
  output (p) {
    util.output(p.action, p.file);
  },
  done (rst) {
    util.output('寫入', rst.file);
    util.writeFile(target, rst.code);
  }
});
複製程式碼

其中,config.plugins 就是在 wepy.config.js 中定義的 plugins。讓我們來看一下 PluginHelper 類是如何定義的。

class PluginHelper {
  constructor (plugins, op) {
    this.applyPlugin(0, op);
    return true;
  }
  applyPlugin (index, op) {
    let plg = loadedPlugins[index];

    if (!plg) {
      op.done && op.done(op);
    } else {
      op.next = () => {
        this.applyPlugin(index + 1, op);
      };
      op.catch = () => {
        op.error && op.error(op);
      };
      if (plg)
        plg.apply(op);
    }
  }
}
複製程式碼

在有多個外掛的時候,不斷的呼叫 next(),最後執行 done()

編寫plugin

wxss 與 css 相比,擴充了尺寸單位,即引入了 rpx 單位。但是設計童鞋給到的設計稿單位一般為 px,那現在我們就一起來編寫一個可以將 px 轉換為 rpx 的 wepy plugin。

從 PluginHelper 類的定義可以看出,是呼叫了 plugin 中的 apply() 方法。另外,只有 .wxss 中的 rpx 才需要轉換,所以會加一層判斷,如果不是 wxss 檔案,接著執行下一個 plugin。rpx 轉換為 px 的核心是,使用了 postcss-px2units plugin。下面就是設計好的 wepy-plugin-px2units,更多原始碼可參考 github 地址

import postcss from 'postcss';
import px2units from 'postcss-px2units';

export default class {

  constructor(c = {}) {
    const def = {
      filter: new RegExp('\.(wxss)$'),
      config: {}
    };

    this.setting = Object.assign({}, def, c);
  }
  apply (op) {

    let setting = this.setting;

    if (!setting.filter.test(op.file)) {
      op.next();
    } else {
      op.output && op.output({
        action: '變更',
        file: op.file
      });

      let prefixer = postcss([ px2units(this.setting.config) ]);

      prefixer.process(op.code, { from: op.file }).then((result) => {
        op.code = result.css;
        op.next();
      }).catch(e => {
        op.err = e;
        op.catch();
      });
    }
  }
}
複製程式碼

最後

本文分析的原始碼以 wepy-cli@1.7.1 版本為準,更多資訊可參考 wepy github (即 github 1.7.x 分支)。另外,文中有任何表述不清或不當的地方,歡迎大家批評指正。

本文首發於:github.com/yingye/Blog…

歡迎各位關注我的Blog,正文以issue形式呈現,喜歡請點star,訂閱請點watch~

相關文章