让taro用上自定义webpack配置吧(taro的h5构建过程浅析)

Taro 是一套遵循 React 语法规范的多端开发解决方案。支持用 React 的开发方式编写一次代码,生成能运行在微信小程序和H5。

背景

事情的起源是因为需要对生成的H5应用高度个性化的 Webpack 配置,而 Taro 默认只提供了 webpack-chain 的方式来修改webpack,导致无法与组内通用的一份webpack config 直接合并(不能用webpack-merge)。不得已之下只好从taro源码寻求解决方案。

从入口开始

从运行的命令入手。查看 package.json, 可以看到与h5相关的是这两个命令:

1
2
"dev:h5": "npm run build:h5 -- --watch",
"build:h5": "taro build --type h5",

于是我们去看看 taro build 到底干了什么。方法当然是先找到 taro 命令对应的文件,在Mac上直接执行 which taro 即可(windows下执行where taro

可以看到是在全局的node_modules下

1
2
# 在我的电脑下的路径
/usr/local/bin/node_modules/@tarojs/cli/bin

马上就能发现同目录下有taro-build文件,打开看到里面的核心方法 build

1
2
3
4
5
6
7
// /bin/taro-build
const build = require('../src/build')
// 到/src/build.js 中找到
function buildForH5 (buildConfig) {
require('./h5').build(buildConfig)
}

看来关键就是src/h5.js里面的代码

H5的“编译”过程

.h5.js里的build方法很简单,就两个关键方法:buildTemp, buildDist

1
2
3
4
5
6
7
8
9
async function build (buildConfig) {
process.env.TARO_ENV = Util.BUILD_TYPES.H5
await clean()
await buildTemp(buildConfig)
await buildDist(buildConfig)
if (buildConfig.watch) {
watchFiles()
}
}

buildDist这个比较好理解,就是常规的webpack打包过程。那`buildTemp``又干了什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function buildTemp () {
// 确保项目目录下有.temp文件夹
fs.ensureDirSync(tempPath)
return new Promise((resolve, reject) => {
// 遍历源代码
klaw(sourceDir)
.on('data', file => {
const relativePath = path.relative(appPath, file.path)
if (!file.stats.isDirectory()) {
Util.printLog(Util.pocessTypeEnum.CREATE, '发现文件', relativePath)
// 加工处理
processFiles(file.path)
}
})
.on('end', () => {
resolve()
})
})
}

可以发现, buildTemp是在遍历源码,然后生成一份平台相关的代码(抹平API差异,根据平台引入不同的包),并放在项目下的.temp文件夹。

这么看来,我们似乎直接对 .temp 文件夹执行 webpack就能够进行自定义的webpack构建了!

就差一点点

当我兴高采烈地执行完webpack,打开页面后,看到的是一个两倍大小的页面。样式出问题了,看来webpack配置还差了一些东西,果断把taro生成的webpack配置打印出来看看:

1
2
3
4
5
6
7
8
9
10
/config/index.js
const config = {
// ...
h5: {
webpackChain(chain) {
// 这里是taro提供的,用于注入自定义配置的钩子
console.log(chain.toString())
}
}
}

对比发现其实是少了postcss的loader。另外需要注意的是,taro还支持配置designWidthdeviceRatio,为了保证多端样式统一,我们的postcss配置也需要根据taro的配置来生成。至于怎么生成,直接搬@taro的代码(微改)就好:

1
2
3
4
5
6
7
8
9
10
/**
* ./node_modules/@tarojs/webpack-runner/dist/config/postcss.conf.js
* 需要在新增插件的时候加上 new 关键字,以满足传统的webpack插件写法
*/
// e.g. 修改前
plugins.push(autoprefixer(Object.assign(defaultAutoprefixerOption, customAutoprefixerOption)));
// 修改后
plugins.push(new autoprefixer(Object.assign(defaultAutoprefixerOption, customAutoprefixerOption)));

最后放在你自己的webpack.config.js文件里就好:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// webpack.config.js
const merge = require("lodash.merge")
const { getPostcssPlugins } = require("./postcss.conf")
const taroConfig = require("./config")(merge)
// 从项目中读出taro的配置
const { designWidth, deviceRatio, h5 } = taroConfig
const plugins = getPostcssPlugins({
designWidth,
deviceRatio,
postcssConfig: h5.module.postcss.plugins
})
// postcss配置,px转换等
const postcssConfig = {
loader: "postcss-loader",
options: {
sourceMap: true,
ident: "postcss",
plugins
}
}
// :sparkles: 注意,真正的入口应该是.temp文件夹下的文件
const entryPath = path.resolve(process.cwd(), ".temp", "app.js")
// 后面就是正常的webpack配置部分

局限

因为目前 npm run dev:h5 命令一口气做了两件事:编译出.temp文件夹和启动webpack-dev-server。如果你的开发环境需要使用自己的dev-server,也许需要用到concurrently这个npm包来并行执行多个npm script(已建议官方拆分多一个taro compile的命令Feature Request taro-cli 能否多提供一个compile 命令, 仅生成.temp文件夹 · Issue #982 · NervJS/taro · GitHub

最后

其实灵感来源于mpvue的同构配置。对于mpvue来说,只需要分别执行vue和mpvue的webpack就好了。对taro来说,关键就是找到编译后生成的平台相关代码(.temp),然后就是熟悉的webpack构建流程了。

同时,taro多出来的这一次编译,能够在编译阶段根据环境变量来去掉平台无关的代码,大大提高了开发者对与多端兼容的控制能力。(相比于mpvue只能在运行时做判断,无法运行时判断的就只能修改mpvue-template-compiler的代码了)

最后的最后,欢迎各位小伙伴一起来交流学习~