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+人)第一的专栏,写有几十篇源码文章。