摘要
一直以来,微信小程序都自带babel编译,帮我们解决微信小程序中的api和语法差异,其实吧,微信小程序的es6支持已经是比较完善的了,我们翻看官网的es6语法支持可以发现,小程序(下文小程序都指代微信小程序)本身除了proxy以及Array.values等方法之外,其他的es6的api大多数都已经原生支持了,但我们为什么还需要babel进行js的代码编译呢?
一个是官方文档所说的平台差异,iso和安卓之间的api支持不同,使用babel可以弥补这一点。第二个就是我们想要追求编码体验,想要使用一些新的js语法来提高自己的编程体验,但是微信小程序中无法自定义babel编译,来添加一些新的语法插件以及presets来支持新的语法,那该怎么办呢?其实我本身是没考虑过那么多的,因为我一般用微信自带的es6转es5就够了,最多加个regenerator-runtime以支持async/await,不过之前我看了一下flow,想着类型检查还是很有用的,而typeScript不是一朝一夕就可以直接上手的(小程序已经支持typeScript了),我苦思冥想,想着怎么在微信小程序中使用flow(我怎么老有这些奇怪的想法?苦笑···)研究了一段时间,想了一个办法,不过其实也不是什么办法咯:
利用babel-cli将你需要编译的js先通过自己配置的babel编译一遍,然后再交由微信小程序的es6转es5编译一遍即可。或者你可以直接完全抛弃微信自带的babel编译,完全使用自己配置的babel去编译你的代码。
其实就是很简单,使用自己的配置的babel来编译你的代码,而不是通过微信小程序自带的babel编译,说起来很简单,但是在我研究过程中,还是遇到了一些坑,不过我还是总结了一些东西出来的,这些总结可以帮你解决小程序没有自定义babel编译的困扰,不过有些东西还是需要注意的。我就先通过怎么在微信小程序中使用flow来作为切入点(希望不要翻车)来展示一下我的总结吧。
ps:flow是一个js的静态类型检查工具,这里不做详细的赘述,也不会讲flow,而是以使用babel编译flow为例子,想要了解flow的,可以看其官网:https://flow.org/
使用babel
babel其实大家都比较熟悉了啊,这里就不再介绍babel的什么了,这里使用的是babel7作为参考的,这里就简单过一下。
首先你最少需要在你小程序的根目录中,使用npm安装两个babel库,@babel/core和@babel/cli,这两个一个用来提供核心的babel编译,一个用来提供命令行操作。@babel/core就是babel编译的核心,如果你在babel的配置中(.babelrc)什么也没有配置,然后使用@babel/cli编译这个js文件,那其实他差不多就是原样输出罢了(注意这里是差不多,如果有语法错误,那肯定是通不过babel的编译,他是一个code => ast => code的过程),所以,你需要配置一些语法编译的预设和插件,来告诉babel遇到某些语法时帮我编译这些语法,我这里以编译flow为例,所以安装了一个@babel/flow,然后配置的.bablerc文件如下:
{
"presets": ["@babel/flow"]
}
好了,差不多babel的安装和配置就这些了,至少对于flow的编译来说,至于babel-cli的命令我们会在讲编译js时再列出来。
ps:babel的插件就是告诉babel遇到某些语法时,怎样编译这些语法,而babel的preset预设其实可以看作就是一些babel插件的集合。
微信小程序项目结构
既然我们需要自己使用babel进行js的编译,那么项目结构也需要变一变,一般,我的小程序的pages文件夹放在根目录,但是啊,既然自己配置编译,那么编译后的页面文件肯定就不是pages了,而且,你不可能只在pages里面写js吧,还有其他的地方也需要写啊,比如一些工具,api,组件这些,所以,目录结构需要改改,我决定沿用vue中的src目录作为我的源代码目录,里面存放一些pages页面,api,wxss,components这些文件,除了app.js,app.json这些放在根目录下,其他的源文件都放在src文件夹下。这个文件夹就是我们后续使用babel进行编译的源文件夹了。这里需要说明一下,微信小程序中的pages页面的路径不一定只能放在根目录中,你只要在app.json中正确配置你的pages字段中的页面路径即可。然后,我们在使用babel编译src文件夹后,会输出到一个dist文件夹中,dist文件夹中存放的就是我们编译后的文件。
好,我们现在在源文件中编写自己的小程序代码,并且可以使用flow的语法,比如在小程序中我们可以写下面这种类似的代码:
function test(x: number, y: number): string {
return `result: ${x * y}`;
}
console.log(test(10, 20));
熟悉flow的应该可以理解,不熟悉也没有关系,你只需要知道小程序中,这样运行肯定是会报错的(浏览器也一样),所以,你需要使用bable将其编译成普通的js代码,将和静态类型有关的代码给去掉,那么我们就来配置babel-cli来编译我们的js,我们的预期是将我们项目中的src中的js文件用babel进行编译一遍,并输出到dist目录中去(这个dist不需要自己创建,他是通过编译生成的),一般使用这个命令:
npx babel src --out-dir dist
你可以在你的项目的根目录中打开命令行去执行这个命令,至于这个npx是什么呢,这里就不详细的说明了,具体可以看这篇文章进行介绍:http://www.ruanyifeng.com/blog/2019/02/npx.html
但是,如果只使用这个命令,我们其实只编译了js文件,那么其他文件怎么办呢?尤其是像图片啊,page页面的wxss和wxml这些文件不需要编译的文件也需要在dist目录中,也就是说,src目录的中的所有源文件和dist中的文件之间的路径需要一致,这样微信小程序才可以正常工作。所幸,babel-cli有一个简单的参数可以做到:
npx babel src --out-dir dist --copy-files
添加--copy-files参数,他会把src中的js文件进行编译,然后不是js的文件原样复制到dist目录中去,这样,我们可以保证dist和src中的目录结构和文件是完全一致的。大致的话,长这样:
这样,一个简单的babel编译过程就搭建完成了,但是需要注意以下几点:
- 因为我们自己的src中的js源码是使用了flow进行静态类型检查的,那么小程序是肯定识别不了的,所以,你在配置app.js中的pages字段时,页面的路径是dist目录,而不是src目录,也就是指向的是编译后的目录,这里需要注意。
- 如果你配置app.js中的pages字段,那么对于一些小程序的默认页面路径,你需要主动指定,比如,微信小程序的生成小程序码,默认的跳转页面的路径是:pages/index/index,但是你的小程序页面可不是在根目录的pages文件夹中了,而是在dist目录中了,所以,有些拥有默认页面的配置,你需要手动指定页面路径才行,不过也不是什么大事。
- 即使你配置了app.js中的pages字段,让小程序的页面是指向dist中的pages页面,但是小程序还是会检测src中的js文件使用了他不识别的语法,因为默认小程序会检测整个根目录中的js文件,所以,你还需要在project.config.json中配置packOptions.ignore字段,即忽略src目录的所有文件,并且,这个忽略的文件夹不会参与小程序的打包和上传。如果不配配置这个忽略文件,那么开发者工具会报错,因为他遇到了不认识的语法。project.config.json的配置如下(可自行根据需要配置,小程序配置官网:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html):
{
"packOptions": {
"ignore": [
{
"type": "folder",
"value": "src/"
}
]
}
}
以上大致就是使用自定义的babel来编译flow,然后你在开发小程序时,就可以开心的使用flow来帮你进行静态类型检查了。并且,你还可以同时开启微信小程序自带的es6转es5,因为我们在使用自己配置的babel进行编译时,只是编译了flow的代码,而其他的es6代码并没有编译,而这一部分的编译完全可以交由微信小程序自带的babel去编译,你可能会问,同时存在两次编译,会不会冲突,或者同一个地方编译两次?其实并不会:
因为首先,我们只编译了flow的语法,其他的es6我们并没有配置babel去编译,而这一部分的编译工作是交由小程序自带的babel去实现的,其次,即使我们配置的编译功能和小程序自带编译功能重复了(比如同时配置了class的编译),那么class语法在第一次babel编译时就已经转为es5代码了,那么第二次的编译,根本就找不到有class语法的代码了,根本不会再编译class,所以,自己配置的babel和微信小程序自带的babel完全可以同时使用,你完全可以在你自定义的babel配置中只配置微信小程序不支持的语法插件,其他小程序支持的,你完全不用理睬,交由小程序自己去编译即可。
所以,如果你只想在微信小程序中使用flow,那么你只需要配置一个编译flow语法的babel就行了,其他的什么preset-env预设啊,不用配置,由小程序自己的babel编译即可。
比如我们想要在小程序中使用类的装饰器decorators语法,小程序内置的babel是不支持此语法的,虽然他支持class的语法,那么我们就可以配置一个@babel/plugin-proposal-decorators插件来编译装饰器语法:
下载@babel/plugin-proposal-decorators插件:
npm install --save-d @babel/plugin-proposal-decorators
然后在上面的.babelrc配置的基础上增加一个插件:
{
"presets": ["@babel/flow"],
"plugins": [
["@babel/plugin-proposal-decorators", {
{"legacy": true}
}]
]
}
然后,你就可以在项目中使用类的装饰器语法了,是不是很简单?确实很简单,不过,凡事都有两面性,尤其是这种不遵循标准, 而另辟蹊径的做法,同样,伴随着一些问题。
问题
我们的编译只能覆盖到src目录
我们配置的babel,不够强大,babel-cli命令只能编译src目录中的js,虽然一般我们也只需要将js编写在src中即可,但是,app.js就不行了,所以,这个编译不了,当然,解决方法肯定是有的。那就是单独编译一下app.js,虽然麻烦了一点,不过吧,其实app.js中也不会写很多代码,所以不是太大问题,而且后面有更好的办法。对了,你在app.js中引入js代码要引dist目录中的,不要引src中的。
编译速度变慢了,而且编译可能还麻烦了
新增加了一层编译,编译速度肯定慢了,因为要编译两次,而且,每次修改文件后,都需要手动执行一下babel-cli的命令,肯定很麻烦对不对,所以,你需要配置自定义预处理命令,这个自定义预处理命令的使用和我之前发布的"在微信小程序中使用sass编写wxss"这篇文章中的使用方法类型,这里就不再介绍,就当你了解这个功能了。不清楚的可以看我这一篇文章以及官网:
如何在微信小程序中使用sass来编写wxss代码:https://blog.****.net/qq_33024515/article/details/85100597
怎么使用呢,简单一点就是这样:
其实就是把babel-cli的命令复制到这个自定义预处理命令的编译前预处理中即可,这样,你每次编译小程序时,他都会先执行这个预处理命令,这样就不用你每次手动的在命令行来编译了(上图中的es6转es5是可以开启的,这里截图时因为我在测试功能,所以关闭了,你完全可以打开)。不过这里可以有两个可以修改的地方,
一个就是你可以关闭开发者工具的自动监听文件变化了,因为需要手动点击编译,才可以执行这些自定义预处理命令,监听文件变换的编译是不行的,不过你可以用快捷键来进行编译,这样好一些。
第二个就是你其实在src中可以把所有的不相干的其他文件给删除掉的(不删也可以,也没什么问题),只保留js文件即可。为什么要这样呢,我个人感觉可能文件夹干净一点把,或者心理上觉得babel能编译的快一点,虽然是没有依据的,看你的喜好来决定。
对了,关于app.js文件的编译,既然使用了自定义预处理命令,那么你可以直接写个bat文件来执行多个命令行的命令(window中,mac我不清楚是什么脚本命令),这样不仅可以编译src的文件,也可以编译app.js了,并且,你还可以执行其他命令,这样要灵活许多(如果你会写bat的话)
当自身配置中有babel不支持的语法,但小程序内置的babel支持时的解决方案
我在探索的过程中,发现了一个问题,我如果使用了处在实验阶段的语法时,且我自定义的babel中没有为这个实验语法配置编译插件时,那么我这个自定义babel的编译就会报错,不会给我通过,但是,小程序中却配置了相应的插件,这可能会有一个奇怪的现象,那就是你在小程序中可以使用的语法,也支持编译的语法,却通不过你自己配置的babel编译,比如,小程序支持类的属性初始化器语法,也就是小程序中你可以这么写:
class Test {
constructor(name) {
this.name = name;
}
logger() {
console.log("Hello", this.name);
}
testVal = "like";
getName = () => {
console.log(1)
}
}
看见类中属性的那个等号的写法了吗,那就是处于实验阶段的属性初始化器语法,可是如果你在代码中写了这种语法时,预处理中的babel编译会报错:
:Support for the experimental syntax 'classProperties' isn't currently enabled
大概就是说这个类的属性初始化器语法不被支持,然后还提示我安装@babel/plugin-proposal-class-properties这个插件来支持此语法,可是微信小程序内置的babel却支持,这可能会比较麻烦,因为如果你不用自定义的babel配置,那么你完全可以使用class语法和这个属性初始化器语法的,可是你自己增加了一层babel编译,而你这个自定义的babel编译却没有增加对一些实验阶段语法的支持,所以,你这一层自定义babel编译肯定就通不过了。
那么解决办法也很简单,在你自定义babel配置中增加一个@babel/plugin-proposal-class-properties插件,让他来编译实验阶段的语法即可(上文已说明了自定义babel和小程序内置babel可以同时存在,并且具有重复的babel编译功能时也不会有影响)。
ps:增加这些插件时你一定要注意插件的顺序,不然还是会出错的,不过这已经是属于babel配置方面的知识了,这里只是因为我被坑到了,所以提一嘴。
最好不要使用Polyfill来编译
其实我之前尝试使用Polyfill来编译的,可是怎么搞都感觉不太行,因为我不确定小程序中使用Polyfill进行扩展时会不会出问题,并且最好不要以来和babel编译有关的npm包,作为微信小程序的npm构建功能,因为微信小程序的npm构建把整个npm包打包为一个index.js文件了,而babel的polyfill编译是按照路径进行引用的,所以,会提示路径错误,找不到这个包,不过你可以使用直接引入polyfill.js这种打包方式来进行打包,有兴趣的各位可以尝试一下,我这里只是给一个建议,其实最好你是和内置的微信小程序babel同时使用,你的自定义babel配置中只编译一些微信小程序不支持的编译功能,比如flow,比如装饰器decorators这些,当然,如果你真的想要全部脱离小程序内置的babel进行编译去完全自定义babel编译,那也可以尝试,因为怎么说呢,像mpvue和wepy这些小程序框架其实也是可以自定义babel配置进行编译的,虽然不确定他们有没有为小程序做专门的处理。不过,值得研究吧,不过目前我个人暂时用不到。
总结
其实研究这个,不是说他真的是必须的,而是在研究过程中,我学到的东西,比如为了可以在小程序中自己配置babel编译,我仔细的翻看了babel的一个中文官网,让我对babel有了更深的理解,了解babel是怎么配置的,为什么需要这么配置,了解preset和插件到底是什么,对于babel的使用有了更深的了解(原理就算了,要词法分析,ast这种知识,目前不具备),而且碰到了坑并解决对我来说也是宝贵的经验,比如babel插件排放的顺序需要注意,否则会有坑等等。而且在对于小程序的理解上也有所提升。况且,我觉得这个自定义babel还是有点用处的,比如flow,而且自定义预处理命令其实真的挺好用的(比如小程序支持的typescript,其实也是通过自定义预处理命令进行编译成js的),我其实都想在微信小程序中再加个webpack的打包了,不说小程序开发工具内置的代码压缩,css自动补全,babel什么了都有了,用webpack来进行图片压缩总可以(请原谅我这么没志向),不过后续可以试试的。
ps:如果嫌麻烦,可以当看个热闹,或者是提供个思路,或者你可以直接使用微信小程序框架,比如mpvue或者wepy来进行小程序的开发。