wepy 是騰訊開源的一款小程式框架,主要通過預編譯的手段,讓開發者採用類 Vue 風格開發。 讓我們一起看看, wepy 是如何實現預編譯的。先放上一張官網的流程圖,後面的分析可以參考該圖。
wepy-cli 主要負責 .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
中 - 獲取
props
和events
,放入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如下:
最後,會把 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.defaults*=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 `
Page(require(`wepy`).default.$createPage(${defaultExport} , `${pagePath}`));
`;
} else {
appPath = opath;
let appConfig = JSON.stringify(config.appConfig || {});
let appCode = `
App(require(`wepy`).default.$createApp(${defaultExport}, ${appConfig}));
`;
if (config.cliLogs) {
appCode += `require(`./_wepylogs.js`)
`;
}
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~