2017-10-12 @sunderls
最近突然想做一个file splitting的功能,具体而言如下:
[compile之前]
在class的方法中添加@grow
的decorator
a.js
class A {
methodA(){
...
}
@grow
methodB(){
...
}
}
希望编译结果能和如下等同
a.js
class A {
methodA() {
...
}
grow() {
import(`./b.js`).then(({default: grown}) => {
Object.assign(this, grown);
});
}
}
b.js
export default {
methodB() {
...
}
}
也就是说通过@grow
的指示,一个file被分解为了两个file,这样传递给webpack compile的时候就能够使用dynamic import。
如何做到呢? 我也不知道,现在开始一点点研究,首先我来看看babel plugin如何写。 google一下可以找到如下手册:
参考一下试试。
babel是将最新的js 语法转换为es5的transpiler,是一个编译器, 这一点可以参考我的另外一篇文章 compiler in JavaScript 学习笔记 1 - 一个非常简单的compiler。
我的理解是: babel接收一段js代码字符串 => 理解分析其语法 => 将某些es6语法转换为es5的语法 => 重新输出代码。
由于我知道一些内容,所以这里不详细说了。具体请到上面的插件手册链接阅读。
需要创建两个project,一个是插件babel-plugin-first-plugin
和一个测试用的project test
babel-plugin-first-plugin
> mkdir babel-plugin-first-plugin
> cd babel-plugin-first-plugin
> npm init
> ...
由于plugin也用es6编写,所以需要install babel和preset
> npm install babel-cli babel-preset-env -D
添加.babelrc
{
"presets": [
["env"]
]
}
在package.json写入build命令
"scripts": {
"build": "babel index.js -d lib"
}
也就是说把index.js(es6)编译为lib/
下的index.js
然后是最重要的一步,本地开发的module需要link一下,然后被test project使用
> npm link
test
> mkdir test
> cd test
> npm init
> ...
> npm install babel-cli babel-preset-env -D
同样的添加.babelrc
,这里加入我们的新plugin.
{
"presets": [
["env"]
],
"plugins": [
"first-plugin"
]
}
然后在package.json中添加build命令
"scripts": {
"build": "babel index.js -d dist"
},
然后link我们之前的plugin
> npm link babel-plugin-first-plugin
在index.js中添加如下内容:
foo === bar
测试一下:
> npm run build
可以看出没啥问题,dist/
中出现了编译好的文件,只不过内容还是foo === bar
没有变化。
这是因为我们的plugin中啥都没干。
根据手册上所述,first-plugin的index.js中添加如下代码:
export default ({types: t}) => {
return {
visitor: { // 一个遍历的处理器,想象为一个在语法树上不停走动的机器人
BinaryExpression(path) { // 当遇到二项式 operand1 operator operand2,比如 a * b , a = b
if (path.node.operator !== "===") { // 只处理 a === b
return;
}
path.node.left = t.identifier("sebmck"); // 将左边改名
path.node.right = t.identifier("dork"); // 将右边改名
}
}
}
};
在plugin中npm run build
,然后在test project中npm run build
(以下简称这个过程为「测试一下」),
可以看到编译的结果发生了变化:
"use strict";
sebmck === dork;
嗯,第一个plugin就算完成了!
我们的目标是webpack环境,所以在test 项目中加入webpack
> npm install webpack babel-loader -D
babel-cli
可以删除了。
package.json 的build修改为webpack
"scripts": {
"build": "webpack"
},
添加webpack.config.js
const webpack = require('webpack');
module.exports = {
entry: {
index: ['./index.js']
},
output: {
path: __dirname + '/dist/',
filename: '[name].js'
},
module: {
rules: [
{
test: /js$/,
use: 'babel-loader'
}
]
}
};
最后测试一下 npm run build
,可以看到这次编译的结果多了很多webpack的代码,直接跳到最后几行,可以发现我们的first-plugin仍然有发生了作用。
"use strict";
sebmck === dork;
首先测试一下class的编译结果
class A {
methodA() {
console.log('method A');
}
}
编译后为
// 创建class的helper function
var _createClass = function () {
//给一个object添加属性
function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
// 给class 构造函数的prototype 添加方法
return function (Constructor, protoProps, staticProps) {
if (protoProps) defineProperties(Constructor.prototype, protoProps);
if (staticProps) defineProperties(Constructor, staticProps);
return Constructor; };
}();
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function"); } }
var A = function () {
function A() {
_classCallCheck(this, A);
}
_createClass(A, [{
key: 'methodA',
value: function methodA() {
console.log('method A');
}
}]);
return A;
}();
代码有点长,不过其实挺简单。首先理解es5环境下我们是怎么做class的。js下没有严格的class,只有prototype和构造方法。
比如一个构造方法:
function A(){
this.name = 'name';
}
var a = new A();
为了让所有实例都有某个方法methodA,我们可以在构造方法中直接添加。
function A() {
this.name = 'name';
this.yell = function() {
alert(this.name);
}
}
var a = new A();
a.yell();
但是这么写每次都会创建一个新的yell方法,更好的写法是将yell写在A的prototype上
function A() {
this.name = 'name';
}
A.prototype.yell = function() {
alert(this.name);
}
var a = new A();
a.yell();
所以class编译结果就是把class中定义的method,assign到构造方法到prototype上而已。
然后我们看一看class的语法,我们使用astexplorer:
注意在菜单中选择 babylon6
输入:
class A {
methodA(){
console.log('methodA');
}
methodB(){
console.log('methodB')
}
}
可以看到class的定义如下(部分省略):
ClassDeclaration:
id:
name: "A"
body:
ClassMethod:
key:
name: "methodA"
value:
body:
body:
- ExpressionStatement:
expression:
....
嗯,所以找到目标ClassDeclaration就可以了。
同样在astexplorer中,输入:
class A {
@grow
methodA(){
}
}
可以看到所在的ClassMethod中新增了decorators一栏,其中定义了decorator的语法。
ClassDeclaration:
id:
name: "A"
body:
ClassMethod:
key:
name: "methodA"
decorators:
-
expression:
name: "grow"
@grow
的classvar sub = {}
这并不难:
export default ({types: t}) => {
return {
visitor: {
// 遇到class的时候进行检查
ClassDeclaration(path){
const body = path.node.body.body;
// 检查class 的body
const targetMethods = body.filter(item => {
// 搜索其中有@grow的class method
return item.type === 'ClassMethod' && item.decorators && item.decorators.filter((decorator) => {
return decorator.expression.name === 'grow'
}).length > 0;
});
// 如果找到了这样的class
if (targetMethods.length > 0) {
// 创建一个新的节点,代表 `var sub = {}`
const sub = t.variableDeclaration('const', [
t.variableDeclarator(t.identifier('sub'), t.objectExpression([]))
]);
// 将其插入class的夫节点的body
path.parentPath.unshiftContainer('body', sub);
}
}
}
}
};
有两个api 文档要看
测试一下,可以看见如下的结果,完美。
...
var sub = {};
var A = (_class = function () {
function A() {
_classCallCheck(this, A);
}
...
先就到这里,我已经学会了最基本的babel插件知识,然后尝试性的进行了查询和节点操作。
接下来还需要完成的有:
这些内容将在以后继续尝试。
如果觉得有帮助到你的话,
欢迎支付宝donate