細雨

Taro 构建项目过程

一、CLI 的解析和处理

我们以taro build --type weapp —watch 为例

{
_: [ 'build' ],
version: false,
v: false,
help: false,
h: false,
'disable-global-config': false,
type: 'weapp',
watch: true,
build: true
}

首先 ,cli 里面解析到命令行参数

然后根据命令,kerneloptsPlugins 属性里面会添加3个元素

  1. taro cli里内置的对build命令处理的插件的路径,commandsPath是所有内置命令插件的目录,targetPlugin是根据命令来的,此处值为 build.js
  2. @tarojs/plugin-platform-weapp ,小程序处理插件
  3. @tarojs/plugin-framework-reactreact处理插件

在初始化插件时,cliAndProjectPlugins就是这3个插件,extraPluginstaro 预设的5个插件,最终一起初始化了8个插件,extraPlugins 里面的插件都是调用 registerMethod 来注册方法

初始完插件后,kernel里面的hooks字段有4个键值对

  • build:build.js 通过registerCommand 注册
  • weapp: @tarojs/plugin-platform-weapp 通过registerPlatform方法注册
  • modifyWebpackChain:@tarojs/plugin-framework-react 监听的hook
  • modifyRunnerOpts:@tarojs/plugin-framework-react 监听的hook

kernelrun函数中,先触发了modifyRunnerOpts 这个钩子,进行编译的前置处理

然后又调用applyPlugins方法,触发build插件,build 插件里面又触发了当前platform也就是@tarojs/plugin-platform-weapp这个插件

weapp的插件实例化了一个Weapp类,然后执行start方法,Weapp类的start方法是从父类TaroPlatformBase 继承过来的

Weapp构造函数初始化时,新建了一个Template类的实例赋值到this.template上,后面的代码是在setup完成后调用modifyTemplatemodifyWebpackConfig

我们直接看build方法

build方法执行的是buildImpl,先根据编译配置选择使用vite还是webpack进行编译,require相应的包,然后汇总配置信息调用编译器,options内容打印如下

{
entry: { app: [ 'G:\\...\\src\\app.tsx' ] },
alias: {
'@/components': 'G:\\...\\src\\components'
},
copy: { patterns: [], options: {} },
sourceRoot: 'src',
outputRoot: 'dist/weapp',
platform: 'weapp',
framework: 'react',
compiler: {
type: 'webpack5',
prebundle: { enable: false, include: [Array], exclude: [Array] }
},
cache: { enable: false },
logger: undefined,
baseLevel: undefined,
csso: undefined,
sass: undefined,
uglify: undefined,
plugins: [],
projectName: '',
env: {
NODE_ENV: '"development"',
FRAMEWORK: '"react"',
TARO_ENV: '"weapp"',
TARO_PLATFORM: '"mini"',
TARO_VERSION: '"3.6.27"'
},
defineConstants: {},
designWidth: 750,
deviceRatio: { '640': 1.17, '750': 1, '828': 0.905 },
projectConfigName: undefined,
jsMinimizer: undefined,
cssMinimizer: undefined,
terser: undefined,
esbuild: undefined,
miniCssExtractPluginOption: { ignoreOrder: true },
postcss: {
'postcss-preset-env': { enable: true },
autoprefixer: { enable: false },
pxtransform: { enable: true, config: {} },
url: { enable: true, config: [Object] },
cssModules: { enable: false, config: [Object] }
},
isWatch: true,
mode: 'development',
blended: false,
isBuildNativeComp: false,
withoutBuild: false,
newBlended: false,
modifyWebpackChain: [Function: modifyWebpackChain],
modifyBuildAssets: [Function: modifyBuildAssets],
modifyMiniConfigs: [Function: modifyMiniConfigs],
modifyComponentConfig: [Function: modifyComponentConfig],
onCompilerMake: [Function: onCompilerMake],
onParseCreateElement: [Function: onParseCreateElement],
onBuildFinish: [Function: onBuildFinish],
nodeModulesPath: 'G:\\...\\node_modules',
buildAdapter: 'weapp',
platformType: 'mini',
globalObject: 'wx',
fileType: {
templ: '.wxml',
style: '.wxss',
config: '.json',
script: '.js',
xs: '.wxs'
},
template: Template {
_baseLevel: 16,
exportExpr: 'module.exports =',
thirdPartyPatcher: {},
supportXS: true,
isXMLSupportRecursiveReference: true,
Adapter: {
if: 'wx:if',
else: 'wx:else',
elseif: 'wx:elif',
for: 'wx:for',
forItem: 'wx:for-item',
forIndex: 'wx:for-index',
key: 'wx:key',
xs: 'wxs',
type: 'weapp'
},
internalComponents: {
View: [Object],
Icon: [Object],
... // 微信端的组件及属性,完整内容在 packages\taro-platform-weapp\src\components.ts
},
focusComponents: Set(3) { 'input', 'textarea', 'editor' },
voidElements: Set(13) {
'progress',
'icon',
'rich-text',
'input',
'textarea',
'slider',
'switch',
'audio',
'ad',
'official-account',
'open-data',
'navigation-bar',
'voip-room'
},
nestElements: Map(16) {
'view' => -1,
'catch-view' => -1,
'cover-view' => -1,
'static-view' => -1,
'pure-view' => -1,
'block' => -1,
'text' => -1,
'static-text' => 6,
'slot' => 8,
'slot-view' => 8,
'label' => 6,
'form' => 4,
'scroll-view' => 4,
'swiper' => 4,
'swiper-item' => 4,
'root-portal' => 3
},
buildPageTemplate: [Function (anonymous)],
buildBaseComponentTemplate: [Function (anonymous)],
buildCustomComponentTemplate: [Function (anonymous)],
buildXScript: [Function (anonymous)],
isSupportRecursive: false,
buildTemplate: [Function (anonymous)],
transferComponents: {},
modifyTemplateResult: [Function (anonymous)],
pluginOptions: {}
},
runtimePath: '@tarojs/plugin-platform-weapp/dist/runtime',
taroComponentsPath: '@tarojs/plugin-platform-weapp/dist/components-react'
}

二、webpack-chain

在上文执行runner方法后,便进入了打包部分

首先创建一个MiniCombination类的实例然后执行make方法,这个make方法实际上是从父类Combination继承来的

make方法内部调用了3个函数,pre是对上面传进来的options又添加了几个字段保存到this.config上,process是子类MiniCombination重载的方法,post 方法是触发modifyWebpackChainwebpackChainonWebpackChainReady 这3个钩子

process 函数首先实例化了一个MiniBaseConfig 类的实例,这个MiniBaseConfig 是在构造函数里面修改了一些配置,它继承于BaseConfig 类,在BaseConfig 类的构造函数里面,新建了一个webpack-chain的实例,并且通过chain.merge方法添加了一些基础设置

process 函数里实例化MiniWebpackPlugin类,调用getPlugins方法获取需要用到的plugin

{
providerPlugin: { plugin: [class ProvidePlugin], args: [ [Object] ] },
definePlugin: { plugin: [class DefinePlugin], args: [ [Object] ] },
miniCssExtractPlugin: {
plugin: [class MiniCssExtractPlugin] {
loader: 'G:\\...\\mini-css-extract-plugin\\dist\\loader.js'
},
args: [ [Object] ]
},
miniSplitChunksPlugin: {
plugin: [class MiniSplitChunksPlugin extends SplitChunksPlugin],
args: [ [Object] ]
},
miniPlugin: { plugin: [class TaroMiniPlugin], args: [ [Object] ] }
}

process 函数里实例化MiniWebpackModule类,调用getModules方法获取需要用到的loader

{
rule: {
sass: { test: /\.sass$/, oneOf: [Array] },
scss: { test: /\.scss$/, oneOf: [Array] },
less: { test: /\.less$/, oneOf: [Array] },
stylus: { test: /\.styl(us)?$/, oneOf: [Array] },
normalCss: { test: /\.(css|qss|jxss|wxss|acss|ttss)(\?.*)?$/, oneOf: [Array] },
script: { test: /\.[tj]sx?$/i, use: [Object], include: [Array] },
template: {
test: /\.(wxml|axml|ttml|qml|swan|jxml)(\?.*)?$/,
type: 'asset/resource',
generator: [Object],
use: [Array]
},
xscript: {
test: /\.wxs$/,
type: 'asset/resource',
generator: [Object],
use: [Array]
},
media: {
test: /\.(mp4|webm|ogg|mp3|m4a|wav|flac|aac)(\?.*)?$/,
type: 'asset',
parser: [Object],
generator: [Object]
},
font: {
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
type: 'asset',
parser: [Object],
generator: [Object]
},
image: {
test: /\.(png|jpe?g|gif|bpm|svg|webp)(\?.*)?$/,
type: 'asset',
parser: [Object],
generator: [Object]
}
}
}

getOptimization 方法里面设置了commonvendorstaro 3个chunks

最后通过 chain.merge 合并配置

三、webpack插件

(一)TaroWebpackBarPlugin

BaseConfig 类的构造函数中,webpack-chain 配置里面默认有一个插件 TaroWebpackBarPlugin,这个插件是tarowebpackbar 插件进行的一个封装

(二)ProvidePlugin

因为小程序环境没有实现DOMBOM 接口,这里自动加载了taro自己的实现

(三)DefinePlugin

设置一些全局变量

(四)mini-css-extract-plugin

引入mini-css-extract-plugin 插件

(五)MiniSplitChunksPlugin

基于 SplitChunksPlugin 插件实现的分包优化策略,可将分包依赖提取到分包中,以减小小程序主包大小

apply方法中,先判断了项目有没有设置分包结构,如果没有设置,则无需处理直接return ,随后监听thisCompilation钩子在每次初始化 compilation时进行处理

optimizeChunks钩子中获取chunks进行判断,先根据chunksname过滤出属于分包的chunks,然后遍历过滤后的结果,获取每个chunk包含的模块,再对chunk模块进行遍历,如果某个module没有在主包中用到,并且有多个分包使用了这个module,则将这个module和对应的chunks 信息记录到this.subCommonDeps中,同时对cacheGroups 的配置进行了调整,来创建要放到分包中的chunk

这里是覆盖了SplitChunksPlugingetCacheGroups 方法, normalizeCacheGroups方法是从SplitChunksPlugin 中复制而来

上图是webpackSplitChunksPlugin 插件的代码,可以看到SplitChunksPlugingetCacheGroups方法就是在构造函数中通过normalizeCacheGroups 生成的

监听afterOptimizeChunks钩子,在chunk优化完成后,获取之前抽离到分包的sub-vendorssub-commonthis.subCommonDeps 是按照模块的MD5值分组的,这里又将其转换成按照chunk分组存到 this.subCommonChunks

监听processAssets 钩子,处理分包文件,先引入sub-vendors 部分,然后获取this.subCommonChunks 指定entryName对应的sub-common,遍历sub-common ,将模块都复制到分包目录下,同时分包文件引入复制过来的资源,最后,将根目录下的sub-common资源删掉不输出

(六)TaroMiniPlugin

apply函数如下

因为我们是开发模式,所以run这个hook不会触发

watchRun里面,首先获取changedFiles 修改的文件,也就是 Compiler.modifiedFiles ,第一次启动编译这个值是undefined,后面每次修改文件都返回一个Set集合,这样通过this.isWatch来区分是否是第一次编译,后面会在第一次编译时打印一些页面信息等,随后执行了run方法

因为我们不是编译小程序原生插件,所以isBuildPluginfalse

getAppConfig方法获取app.config.js里面的内容并保存到this.appConfig

这里还调用了compileFile 方法,compileFile 方法是专门读取页面配置的,也就是以.config+扩展名结尾的文件,读取完会将数据保存到this.filesConfig

getAppConfig方法里面还调用了build插件传入的modifyAppConfig 函数来触发modifyAppConfig钩子

getPages 方法首先根据编译配置获取需要预渲染的页面保存到 this.prerenderPages 中,接着调用getTabBarFiles 方法收集tabbar的信息,收集所有页面信息保存到this.pages

getPagesConfig方法对每个页面调用了compileFile 方法收集页面配置

getDarkMode方法保存编译配置中的 themeLocationthis.themeLocation

getConfigFiles 先调用this.addEntry 方法,遍历 this.filesConfig ,将已有的文件信息以Map对象的格式存到 this.dependencies 中,key为文件路径,valueTaroSingleEntryDependency 类的实例,TaroSingleEntryDependency 类继承自 webpack.dependencies.ModuleDependency

然后监听监听compilationbeforeChunkAssets 钩子,在创建 chunk asset 之前,删掉其中的 config chunk