Taro 源码揭秘:12. Taro 如何编译成小程序文件的
预计阅读时间: 9 分钟
1. 前言
大家好,我是若川,欢迎关注我的公众号:若川视野。从 2021 年 8 月起,我持续组织了好几年的每周大家一起学习 200 行左右的源码共读活动,感兴趣的可以点此扫码加我微信 ruochuan02
参与。另外,想学源码,极力推荐关注我写的专栏《学习源码整体架构系列》,目前是掘金关注人数(6k+人)第一的专栏,写有几十篇源码文章。
截至目前(2025-04-16
),目前最新是 4.0.12
,官方4.0
正式版本的介绍文章暂未发布。官方之前发过Taro 4.0 Beta 发布:支持开发鸿蒙应用、小程序编译模式、Vite 编译等。
计划写一个 Taro 源码揭秘系列,博客地址:https://ruochuan12.github.io/taro 可以加入书签,持续关注若川。
时隔 3 个月才继续写第 11 篇,我会继续持续写下去,争取做全网最新最全的 Taro 源码系列。
前面 4 篇文章都是讲述编译相关的,CLI、插件机制、初始化项目、编译构建流程。
第 5-7 篇讲述的是运行时相关的 Events、API、request 等。
第 10 篇接着继续追随第 4 篇和第 8、9 篇的脚步,分析 TaroMiniPlugin webpack 的插件实现(全流程讲述)。
第 11 篇,我们继续分析 TaroMiniPlugin webpack 的插件实现。分析 Taro 是如何解析入口文件和页面的?
关于克隆项目、环境准备、如何调试代码等,参考第 1 篇文章-准备工作、调试和第 4 篇 npm run dev:weapp(本文以这篇文章中的调试为例)。后续文章基本不再过多赘述。
学完本文,你将学到:
1. Taro 是如何解析入口文件和页面的?
等等
// packages/taro-webpack5-runner/src/plugins/MiniPlugin.ts
compilation.hooks.processAssets.tapAsync(
{
name: PLUGIN_NAME,
stage: PROCESS_ASSETS_STAGE_ADDITIONAL,
},
this.tryAsync<any>(async () => {
// 如果是子编译器,证明是编译独立分包,进行单独的处理
if ((compilation as any).__tag === CHILD_COMPILER_TAG) {
await this.generateIndependentMiniFiles(compilation, compiler);
} else {
await this.generateMiniFiles(compilation, compiler);
}
})
);
// packages/taro-webpack5-runner/src/plugins/MiniPlugin.ts
/** 生成小程序相关文件 */
async generateMiniFiles (compilation: Compilation, compiler: Compiler) {
// compilation.assets[xxx] = xxx;
}
这个函数特别长,简单来说就是以下这几项
- 生成 XS 文件 generateXSFile
- 生成配置文件 generateConfigFile
- 生成模板文件 generateTemplateFile
最终通过 compilation.assets[xxx] = xxx;
赋值语句生成文件。
我们来看下具体实现。
generateMiniFiles 生成小程序文件
是 TaroMiniPlugin 类中用于生成小程序相关产物(如模板、配置、样式、资源等)的核心方法。它的主要职责是根据收集到的页面、组件、配置等信息,生成最终小程序所需的各种文件,并写入到 webpack 的 compilation.assets 中。其主要流程如下:
处理样式文件名重复问题
首先遍历所有产物,如果发现样式文件名重复(如 .wxss.wxss),则去重,保证产物中只有一个正确的样式文件名。
// packages/taro-webpack5-runner/src/plugins/MiniPlugin.ts
/** 生成小程序相关文件 */
async generateMiniFiles (compilation: Compilation, compiler: Compiler) {
const { RawSource } = compiler.webpack.sources
const { template, combination, isBuildPlugin, sourceDir } = this.options
const { modifyMiniConfigs } = combination.config
const baseTemplateName = 'base'
const isUsingCustomWrapper = componentConfig.thirdPartyComponents.has('custom-wrapper')
/**
* 与原生小程序混写时解析模板与样式
*/
compilation.getAssets().forEach(({ name: assetPath }) => {
const styleExt = this.options.fileType.style
if (new RegExp(`${styleExt}${styleExt}$`).test(assetPath)) {
const assetObj = compilation.assets[assetPath]
const newAssetPath = assetPath.replace(styleExt, '')
compilation.assets[newAssetPath] = assetObj
}
})
}
自定义配置处理
如果配置中定义了 modifyMiniConfigs 钩子,则调用该钩子允许用户自定义修改所有页面、组件的配置内容。
if (typeof modifyMiniConfigs === "function") {
await modifyMiniConfigs(this.filesConfig);
}
生成 app 配置文件
在非 blended 模式且不是插件构建时,生成主包的 app 配置文件(如 app.json),内容来自 this.filesConfig。
if ((!this.options.blended || !this.options.newBlended) && !isBuildPlugin) {
const appConfigPath = this.getConfigFilePath(this.appEntry);
const appConfigName = path
.basename(appConfigPath)
.replace(path.extname(appConfigPath), "");
this.generateConfigFile(
compilation,
compiler,
this.appEntry,
this.filesConfig[appConfigName].content
);
}
生成基础组件模板和配置
如果当前模板不支持递归(如微信、QQ 小程序),则生成基础组件(comp)和自定义包装器(custom-wrapper)的模板和配置文件。
if (!template.isSupportRecursive) {
// 如微信、QQ 不支持递归模版的小程序,需要使用自定义组件协助递归
this.generateTemplateFile(
compilation,
compiler,
baseCompName,
template.buildBaseComponentTemplate,
this.options.fileType.templ
);
const baseCompConfig = {
component: true,
styleIsolation: "apply-shared",
usingComponents: {
[baseCompName]: `./${baseCompName}`,
},
} as Config & {
component?: boolean;
usingComponents: Record<string, string>;
};
if (isUsingCustomWrapper) {
baseCompConfig.usingComponents[
customWrapperName
] = `./${customWrapperName}`;
this.generateConfigFile(compilation, compiler, customWrapperName, {
component: true,
styleIsolation: "apply-shared",
usingComponents: {
[baseCompName]: `./${baseCompName}`,
[customWrapperName]: `./${customWrapperName}`,
},
});
}
this.generateConfigFile(
compilation,
compiler,
baseCompName,
baseCompConfig
);
} else {
if (isUsingCustomWrapper) {
this.generateConfigFile(compilation, compiler, customWrapperName, {
component: true,
styleIsolation: "apply-shared",
usingComponents: {
[customWrapperName]: `./${customWrapperName}`,
},
});
}
}
this.generateTemplateFile(
compilation,
compiler,
baseTemplateName,
template.buildTemplate,
componentConfig
);
isUsingCustomWrapper &&
this.generateTemplateFile(
compilation,
compiler,
customWrapperName,
template.buildCustomComponentTemplate,
this.options.fileType.templ
);
生成全局模板和自定义包装器模板
生成全局的 base 模板和 custom-wrapper 模板(如 base.wxml、custom-wrapper.wxml),并根据配置决定是否压缩 XML。
生成全局 XS 脚本
如果平台支持 XS 脚本,则生成 utils 脚本文件。
this.generateXSFile(compilation, compiler, "utils");
生成所有组件的配置和模板
遍历所有组件,为每个组件生成配置文件和模板文件(非原生组件才生成模板)。
this.components.forEach((component) => {
const importBaseTemplatePath = promoteRelativePath(
path.relative(
component.path,
path.join(
sourceDir,
isBuildPlugin ? "plugin" : "",
this.getTemplatePath(baseTemplateName)
)
)
);
const config = this.filesConfig[this.getConfigFilePath(component.name)];
if (config) {
this.generateConfigFile(
compilation,
compiler,
component.path,
config.content
);
}
if (!component.isNative) {
this.generateTemplateFile(
compilation,
compiler,
component.path,
template.buildPageTemplate,
importBaseTemplatePath
);
}
});
生成所有页面的配置和模板
遍历所有页面,为每个页面生成配置文件和模板文件(非原生页面才生成模板),并处理分包页面的过滤。
this.pages.forEach((page) => {
const importBaseTemplatePath = promoteRelativePath(
path.relative(
page.path,
path.join(
sourceDir,
isBuildPlugin ? "plugin" : "",
this.getTemplatePath(baseTemplateName)
)
)
);
const config = this.filesConfig[this.getConfigFilePath(page.name)];
// pages 里面会混合独立分包的,在这里需要过滤一下,避免重复生成 assets
const isIndependent = !!this.getIndependentPackage(page.path);
if (isIndependent) return;
// 生成页面模板需要在生成页面配置之前,因为会依赖到页面配置的部分内容
if (!page.isNative) {
this.generateTemplateFile(
compilation,
compiler,
page.path,
template.buildPageTemplate,
importBaseTemplatePath,
config
);
}
if (config) {
const importBaseCompPath = promoteRelativePath(
path.relative(
page.path,
path.join(
sourceDir,
isBuildPlugin ? "plugin" : "",
this.getTargetFilePath(baseCompName, "")
)
)
);
const importCustomWrapperPath = promoteRelativePath(
path.relative(
page.path,
path.join(
sourceDir,
isBuildPlugin ? "plugin" : "",
this.getTargetFilePath(customWrapperName, "")
)
)
);
config.content.usingComponents = {
...config.content.usingComponents,
};
if (isUsingCustomWrapper) {
config.content.usingComponents[customWrapperName] =
importCustomWrapperPath;
}
if (!template.isSupportRecursive && !page.isNative) {
config.content.usingComponents[baseCompName] = importBaseCompPath;
}
this.generateConfigFile(
compilation,
compiler,
page.path,
config.content
);
}
});
生成 tabbar 图标资源
调用 generateTabBarFiles 方法,将 tabbar 所需的图片资源写入产物。
this.generateTabBarFiles(compilation, compiler);
注入公共样式
调用 injectCommonStyles 方法,将公共样式自动引入到 app 和各页面样式文件中。
this.injectCommonStyles(compilation, compiler);
生成暗黑模式主题文件
如果配置了暗黑模式主题,则输出对应的主题文件。
if (this.themeLocation) {
this.generateDarkModeFile(compilation, compiler);
}
插件模式下生成 plugin.json
如果是插件构建,自动生成并写入 plugin.json 文件。
if (isBuildPlugin) {
const pluginJSONPath = path.join(sourceDir, "plugin", "plugin.json");
if (fs.existsSync(pluginJSONPath)) {
const pluginJSON = fs.readJSONSync(pluginJSONPath);
this.modifyPluginJSON(pluginJSON);
compilation.assets["plugin.json"] = new RawSource(
JSON.stringify(pluginJSON)
);
}
}
generateConfigFile
generateConfigFile (compilation: Compilation, compiler: Compiler, filePath: string, config: Config & { component?: boolean }) {
const { RawSource } = compiler.webpack.sources
const fileConfigName = this.getConfigPath(this.getComponentName(filePath))
const unofficialConfigs = ['enableShareAppMessage', 'enableShareTimeline', 'enablePageMeta', 'components']
unofficialConfigs.forEach(item => {
delete config[item]
})
this.adjustConfigContent(config)
const fileConfigStr = JSON.stringify(config)
compilation.assets[fileConfigName] = new RawSource(fileConfigStr)
}
generateTemplateFile 生成模板文件
generateTemplateFile (compilation: Compilation, compiler: Compiler, filePath: string, templateFn: (...args) => string, ...options) {
const { RawSource } = compiler.webpack.sources
let templStr = templateFn(...options)
const fileTemplName = this.getTemplatePath(this.getComponentName(filePath))
if (this.options.combination.config.minifyXML?.collapseWhitespace) {
const minify = require('html-minifier').minify
templStr = minify(templStr, {
collapseWhitespace: true,
keepClosingSlash: true
})
}
compilation.assets[fileTemplName] = new RawSource(templStr)
}
generateXSFile 生产 xs 文件
generateXSFile (compilation: Compilation, compiler: Compiler, xsPath) {
const { RawSource } = compiler.webpack.sources
const ext = this.options.fileType.xs
const isUseXS = this.options.template.isUseXS
if (ext == null || !isUseXS) {
return
}
const xs = this.options.template.buildXScript()
const fileXsName = this.getTargetFilePath(xsPath, ext)
const filePath = fileXsName
compilation.assets[filePath] = new RawSource(xs)
}
generateTabBarFiles 输出 tabbar icons 文件
/**
* 输出 tabbar icons 文件
*/
generateTabBarFiles (compilation: Compilation, { webpack }: Compiler) {
const { RawSource } = webpack.sources
this.tabBarIcons.forEach(icon => {
const iconPath = path.resolve(this.options.sourceDir, icon)
if (fs.existsSync(iconPath)) {
const iconSource = fs.readFileSync(iconPath)
compilation.assets[icon] = new RawSource(iconSource)
}
})
}
总结
generateMiniFiles
负责将 Taro 编译期收集到的所有页面、组件、配置、资源等,最终生成小程序平台所需的所有产物文件,并写入 webpack 的产物集合。它是 Taro 小程序端产物生成的关键环节,确保了最终产物的完整性和平台兼容性。
启发:Taro 是非常知名的跨端框架,我们在使用它,享受它带来便利的同时,有余力也可以多为其做出一些贡献。比如帮忙解答一些 issue 或者提 pr 修改 bug 等。
在这个过程,我们会不断学习,促使我们去解决问题,带来的好处则是不断拓展知识深度和知识广度。
有些时候还是需要深入学习源码,理解源码才能更好的针对项目做相应的优化。
如果看完有收获,欢迎点赞、评论、分享、收藏支持。你的支持和肯定,是我写作的动力。也欢迎提建议和交流讨论。
作者:常以若川为名混迹于江湖。所知甚少,唯善学。若川的博客,github blog,可以点个 star
鼓励下持续创作。
最后可以持续关注我@若川,欢迎关注我的公众号:若川视野。从 2021 年 8 月起,我持续组织了好几年的每周大家一起学习 200 行左右的源码共读活动,感兴趣的可以点此扫码加我微信 ruochuan02
参与。另外,想学源码,极力推荐关注我写的专栏《学习源码整体架构系列》,目前是掘金关注人数(6k+人)第一的专栏,写有几十篇源码文章。