diff --git a/docs/taro/webpack5/README.md b/docs/taro/webpack5/README.md new file mode 100644 index 0000000..40c898f --- /dev/null +++ b/docs/taro/webpack5/README.md @@ -0,0 +1,1235 @@ +--- +highlight: darcula +theme: smartblue +--- + +# Taro 源码揭秘:9. Taro 是如何使用 webpack 打包构建小程序的? + +## 1. 前言 + +大家好,我是[若川](https://ruochuan12.github.io),欢迎关注我的[公众号:若川视野](https://mp.weixin.qq.com/s/MacNfeTPODNMLLFdzrULow)。我倾力持续组织了 3 年多[每周大家一起学习 200 行左右的源码共读活动](https://juejin.cn/post/7079706017579139102),感兴趣的可以[点此扫码加我微信 `ruochuan02` 参与](https://juejin.cn/pin/7217386885793595453)。另外,想学源码,极力推荐关注我写的专栏[《学习源码整体架构系列》](https://juejin.cn/column/6960551178908205093),目前是掘金关注人数(6k+人)第一的专栏,写有几十篇源码文章。 + +截至目前(`2024-11-07`),目前最新是 [`4.0.7`](https://github.com/NervJS/taro/releases/tag/v4.0.7),官方`4.0`正式版本的介绍文章暂未发布。官方之前发过[Taro 4.0 Beta 发布:支持开发鸿蒙应用、小程序编译模式、Vite 编译等](https://juejin.cn/post/7330792655125463067)。 + +计划写一个 Taro 源码揭秘系列,博客地址:[https://ruochuan12.github.io/taro](https://ruochuan12.github.io/taro) 可以加入书签,持续关注[若川](https://juejin.cn/user/1415826704971918)。 + +- [x] [1. 揭开整个架构的入口 CLI => taro init 初始化项目的秘密](https://juejin.cn/post/7378363694939783178) +- [x] [2. 揭开整个架构的插件系统的秘密](https://juejin.cn/post/7380195796208205824) +- [x] [3. 每次创建新的 taro 项目(taro init)的背后原理是什么](https://juejin.cn/post/7390335741586931738) +- [x] [4. 每次 npm run dev:weapp 开发小程序,build 编译打包是如何实现的?](https://juejin.cn/post/7403193330271682612) +- [x] [5. 高手都在用的发布订阅机制 Events 在 Taro 中是如何实现的?](https://juejin.cn/post/7403915119448915977) +- [x] [6. 为什么通过 Taro.xxx 能调用各个小程序平台的 API,如何设计实现的?](https://juejin.cn/post/7407648740926291968) +- [x] [7. Taro.request 和请求响应拦截器是如何实现的](https://juejin.cn/post/7415911762128797696) +- [ ] 等等 + +前面 4 篇文章都是讲述编译相关的,CLI、插件机制、初始化项目、编译构建流程。 +第 5-7 篇讲述的是运行时相关的 Events、API、request 等。 +第 8 篇接着继续追随第4篇 npm run dev:weapp 的脚步,继续分析 `@tarojs/webpack5-runner`,Taro 是如何使用 webpack 打包构建小程序的? + +关于克隆项目、环境准备、如何调试代码等,参考[第一篇文章-准备工作、调试](https://juejin.cn/post/7378363694939783178#heading-1)。后续文章基本不再过多赘述。 + +学完本文,你将学到: + +```bash +1. +等等 +``` + +## 2. webpack 打包构建 + +[4. 每次 npm run dev:weapp 开发小程序,build 编译打包是如何实现的?](https://juejin.cn/post/7403193330271682612) + +在第4篇文章末尾,我们可以回顾下,如何获取 `webpack` 配置和 执行 `webpack()` 构建的。 + +## @tarojs/webpack5-runner + +暴露给 `@tarojs/cli` 的小程序/H5 Webpack 启动器。 + +这个 `npm` 包,主要要解决的问题是。把 `taro` 项目用 `webpack` 编译到小程序、h5、鸿蒙。 + +`package.json` 入口文件 `"main": "index.js"` 入口文件 `index.js` + +```js +if (process.env.TARO_PLATFORM === 'web') { + module.exports = require('./dist/index.h5.js').default +} else if (process.env.TARO_PLATFORM === 'harmony' || process.env.TARO_ENV === 'harmony') { + module.exports = require('./dist/index.harmony.js').default +} else { + module.exports = require('./dist/index.mini.js').default +} + +module.exports.default = module.exports +``` + +- Combination + - MiniCombination + - H5Combination + - HarmonyCombination +- BaseConfig + - MiniBaseConfig + - H5BaseConfig + - HarmonyBaseConfig +- webpackPlugin + - MiniWebpackPlugin + - H5WebpackPlugin + - HarmonyWebpackPlugin +- WebpackModule + - MiniWebpackModule + - H5WebpackModule + - HarmonyWebpackModule + +```ts +// packages/taro-webpack5-runner/src/index.mini.ts + +import webpack from 'webpack' +// 省略若干代码 +export default async function build (appPath: string, rawConfig: IMiniBuildConfig): Promise<Stats | void> { + const combination = new MiniCombination(appPath, rawConfig) + await combination.make() + // 省略若干代码 + + const webpackConfig = combination.chain.toConfig() + const config = combination.config + + return new Promise<Stats | void>((resolve, reject) => { + if (config.withoutBuild) return + + const compiler = webpack(webpackConfig) + + // 省略若干代码... + }) +} +``` + +```js +// 重点就以下这几句 +// 生成获取 webpack 配置,执行 webpack(webpackConfig) +const combination = new MiniCombination(appPath, rawConfig) +await combination.make() +const webpackConfig = combination.chain.toConfig() +const compiler = webpack(webpackConfig) +``` + +## Combination + +```ts +export class MiniCombination extends Combination<IMiniBuildConfig> { + process (config: Partial<IMiniBuildConfig>) { + // 省略代码 + } +} +``` + +```ts +export class Combination<T extends IMiniBuildConfig | IH5BuildConfig | IHarmonyBuildConfig = CommonBuildConfig> { + appPath: string + config: T + chain: Chain + enableSourceMap: boolean + sourceRoot: string + outputRoot: string + sourceDir: string + outputDir: string + rawConfig: T + + prebundleOptions?: IPrebundle + + isWeb = isWebPlatform() + + /** special mode */ + // 将组件打包为原生模块 + isBuildNativeComp = false + // 正常打包页面的同时,把组件也打包成独立的原生模块 + newBlended = false + + constructor (appPath: string, config: T) { + this.appPath = appPath + this.rawConfig = config + this.sourceRoot = config.sourceRoot || 'src' + this.outputRoot = config.outputRoot || 'dist' + this.sourceDir = path.resolve(appPath, this.sourceRoot) + this.outputDir = path.resolve(appPath, this.outputRoot) + this.isBuildNativeComp = !!config.isBuildNativeComp + this.newBlended = !!config.newBlended + this.enableSourceMap = config.enableSourceMap ?? config.isWatch ?? process.env.NODE_ENV !== 'production' + } + + async make () { + await this.pre(this.rawConfig) + this.process(this.config) + await this.post(this.config, this.chain) + } + + process (_config: Partial<T>) {} + + async pre (rawConfig: T) { + const preMode = rawConfig.mode || process.env.NODE_ENV + const mode = ['production', 'development', 'none'].find(e => e === preMode) || + (!rawConfig.isWatch || process.env.NODE_ENV === 'production' ? 'production' : 'development') + /** process config.sass options */ + const sassLoaderOption = await getSassLoaderOption(rawConfig) + this.config = { + ...rawConfig, + sassLoaderOption, + mode, + frameworkExts: rawConfig.frameworkExts || SCRIPT_EXT + } + } + + async post (config: T, chain: Chain) { + const { modifyWebpackChain, webpackChain, onWebpackChainReady } = config + const data: IModifyChainData = { + componentConfig + } + if (isFunction(modifyWebpackChain)) { + await modifyWebpackChain(chain, webpack, data) + } + if (isFunction(webpackChain)) { + webpackChain(chain, webpack, META_TYPE) + } + if (isFunction(onWebpackChainReady)) { + onWebpackChainReady(chain) + } + } +} +``` + +## MiniCombination + +```ts +export class MiniCombination extends Combination<IMiniBuildConfig> { + buildNativePlugin: BuildNativePlugin + fileType: IFileType + isBuildPlugin = false + optimizeMainPackage: { enable?: boolean | undefined, exclude?: any[] | undefined } = { + enable: true + } + + process (config: Partial<IMiniBuildConfig>) { + const baseConfig = new MiniBaseConfig(this.appPath, config) + const chain = this.chain = baseConfig.chain + const { + entry = {}, + output = {}, + mode = 'production', + globalObject = 'wx', + sourceMapType = 'cheap-module-source-map', + fileType = { + style: '.wxss', + config: '.json', + script: '.js', + templ: '.wxml' + }, + /** special mode */ + isBuildPlugin = false, + /** hooks */ + modifyComponentConfig, + optimizeMainPackage + } = config + + this.fileType = fileType + + modifyComponentConfig?.(componentConfig, config) + + if (isBuildPlugin) { + // 编译目标 - 小程序原生插件 + this.isBuildPlugin = true + this.buildNativePlugin = BuildNativePlugin.getPlugin(this) + chain.merge({ + context: path.join(process.cwd(), this.sourceRoot, 'plugin') + }) + } + + if (optimizeMainPackage) { + this.optimizeMainPackage = optimizeMainPackage + } + + const webpackEntry = this.getEntry(entry) + const webpackOutput = this.getOutput({ + publicPath: '/', + globalObject, + isBuildPlugin, + output + }) + const webpackPlugin = new MiniWebpackPlugin(this) + const webpackModule = new MiniWebpackModule(this) + + const module = webpackModule.getModules() + const [, pxtransformOption] = webpackModule.__postcssOption.find(([name]) => name === 'postcss-pxtransform') || [] + webpackPlugin.pxtransformOption = pxtransformOption as any + const plugin = webpackPlugin.getPlugins() + + chain.merge({ + entry: webpackEntry, + output: webpackOutput, + mode, + devtool: this.getDevtool(sourceMapType), + resolve: { + alias: this.getAlias() + }, + plugin, + module, + optimization: this.getOptimization() + }) + } + + getEntry (entry: IMiniBuildConfig['entry']) { + return this.isBuildPlugin ? this.buildNativePlugin.entry : entry + } + + getOutput ({ publicPath, globalObject, isBuildPlugin, output }) { + return { + path: this.outputDir, + publicPath, + filename: '[name].js', + chunkFilename: '[name].js', + globalObject, + enabledLibraryTypes: isBuildPlugin ? ['commonjs'] : [], + ...output + } + } + + getAlias () { + const { alias = {}, taroComponentsPath } = this.config + return { + ...alias, + [`${taroJsComponents}$`]: taroComponentsPath + } + } + + getOptimization () { + return { + usedExports: true, + runtimeChunk: { + name: 'runtime' + }, + splitChunks: { + chunks: 'all', + maxInitialRequests: Infinity, + minSize: 0, + cacheGroups: { + default: false, + defaultVendors: false, + common: { + name: 'common', + minChunks: 2, + priority: 1 + }, + vendors: { + name: 'vendors', + minChunks: 2, + test: module => { + const nodeModulesDirRegx = new RegExp(REG_NODE_MODULES_DIR) + return nodeModulesDirRegx.test(module.resource) + }, + priority: 10 + }, + taro: { + name: 'taro', + test: module => REG_TARO_SCOPED_PACKAGE.test(module.context), + priority: 100 + } + } + } + } + } +} +``` + +```ts +export class MiniBaseConfig extends BaseConfig { + defaultTerserOptions = { + // 省略 + } + + constructor(appPath: string, config: Partial<IMiniBuildConfig>) { + super(appPath, config) + } +} +``` + +## BaseConfig + +```ts +export class BaseConfig { + private _chain: Chain + + constructor (appPath: string, config: Config) { + const chain = this._chain = new Chain() + chain.merge({ + target: ['web', 'es5'], + resolve: { + extensions: ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.vue'], + symlinks: true, + plugin: { + MultiPlatformPlugin: { + plugin: MultiPlatformPlugin, + args: ['described-resolve', 'resolve', { chain }] + } + } + }, + resolveLoader: { + modules: ['node_modules'] + }, + output: { + chunkLoadingGlobal: 'webpackJsonp' + }, + plugin: { + webpackbar: WebpackPlugin.getWebpackBarPlugin({ + reporters: [ + 'basic', + 'fancy', + { + done (_context, { stats }: { stats: Stats }) { + const { warnings, errors } = formatMessages(stats) + + if (stats.hasWarnings()) { + console.log(chalk.bgKeyword('orange')('⚠️ Warnings: \n')) + warnings.forEach(w => console.log(w + '\n')) + } + + if (stats.hasErrors()) { + console.log(chalk.bgRed('✖ Errors: \n')) + errors.forEach(e => console.log(e + '\n')) + !config.isWatch && process.exit(1) + } + + if (config.isWatch) { + console.log(chalk.gray(`→ Watching... [${new Date().toLocaleString()}]\n`)) + } + + if (config.logger?.stats === true && config.mode === 'production') { + console.log(chalk.bgBlue('ℹ Stats: \n')) + console.log(stats.toString({ + colors: true, + modules: false, + children: false, + chunks: false, + chunkModules: false + })) + } + } + } + ], + basic: config.logger?.quiet === true, + fancy: config.logger?.quiet !== true, + }) + }, + watchOptions: { + aggregateTimeout: 200 + } + }) + + // 持久化缓存 + // 省略代码 + } + + get chain () { + return this._chain + } +} +``` + +## MiniBaseConfig + +```ts +export class MiniBaseConfig extends BaseConfig { + defaultTerserOptions = { + parse: { + ecma: 8, + }, + compress: { + ecma: 5, + warnings: false, + arrows: false, + collapse_vars: false, + comparisons: false, + computed_props: false, + hoist_funs: false, + hoist_props: false, + hoist_vars: false, + inline: false, + loops: false, + negate_iife: false, + properties: false, + reduce_funcs: false, + reduce_vars: false, + switches: false, + toplevel: false, + typeofs: false, + booleans: true, + if_return: true, + sequences: true, + unused: true, + conditionals: true, + dead_code: true, + evaluate: true, + }, + output: { + ecma: 5, + comments: false, + ascii_only: true, + }, + } + + constructor(appPath: string, config: Partial<IMiniBuildConfig>) { + super(appPath, config) + const mainFields = [...defaultMainFields] + const resolveOptions = { + basedir: appPath, + mainFields, + } + this.chain.merge({ + resolve: { + mainFields, + alias: { + // 小程序使用 regenerator-runtime@0.11 + 'regenerator-runtime': require.resolve('regenerator-runtime'), + // 开发组件库时 link 到本地调试,runtime 包需要指向本地 node_modules 顶层的 runtime,保证闭包值 Current 一致,shared 也一样 + '@tarojs/runtime': resolveSync('@tarojs/runtime', resolveOptions), + '@tarojs/shared': resolveSync('@tarojs/shared', resolveOptions), + }, + // [Webpack 4] config.node: { fs: false, path: false } + // [Webpack 5] config.resolve.fallback + fallback: { + fs: false, + path: false, + }, + }, + optimization: { + sideEffects: true, + }, + performance: { + maxEntrypointSize: 2 * 1000 * 1000, + }, + }) + + this.setMinimizer(config, this.defaultTerserOptions) + } +} + +``` + +## WebpackPlugin 提供应用所需插件 + +```ts +import path from 'node:path' + +import { REG_STYLE } from '@tarojs/helper' +import webpack from 'webpack' + +import { TaroWebpackBarPlugin } from '../plugins/WebpackBarPlugin' + +import type { ICopyOptions } from '@tarojs/taro/types/compile' + +export type PluginArgs = Record<string, any>[] + +export default class WebpackPlugin { + static getPlugin (plugin, args: PluginArgs) { + return { + plugin, + args + } + } + + static getCopyWebpackPlugin (appPath: string, copy: ICopyOptions) { + /** @doc https://webpack.js.org/plugins/copy-webpack-plugin */ + const CopyWebpackPlugin = require('copy-webpack-plugin') + const globalIgnores: string[] = copy.options?.ignore ?? [] + const patterns = copy.patterns.map(({ from, to, ignore = [], ...extra }) => { + return { + from, + to: path.resolve(appPath, to), + context: appPath, + globOptions: { + ignore: ignore.concat(globalIgnores) + }, + ...extra + } + }) + const args = { patterns } + return WebpackPlugin.getPlugin(CopyWebpackPlugin, [args]) + } + + static getProviderPlugin (args: Record<string, string | string[]>) { + return WebpackPlugin.getPlugin(webpack.ProvidePlugin, [args]) + } + + static getDefinePlugin (definitionsList: Record<string, string>[]) { + const definitions = Object.assign({}, ...definitionsList) + return WebpackPlugin.getPlugin(webpack.DefinePlugin, [definitions]) + } + + static getMiniCssExtractPlugin (args: Record<string, any>) { + const MiniCssExtractPlugin = require('mini-css-extract-plugin') + return WebpackPlugin.getPlugin(MiniCssExtractPlugin, [args]) + } + + static getTerserPlugin (terserOptions) { + const TerserPlugin = require('terser-webpack-plugin') + return WebpackPlugin.getPlugin(TerserPlugin, [{ + parallel: true, + terserOptions + }]) + } + + static getESBuildMinifyPlugin (esbuildMinifyOptions) { + const ESBuildMinifyPlugin = require('esbuild-loader').EsbuildPlugin + return WebpackPlugin.getPlugin(ESBuildMinifyPlugin, [esbuildMinifyOptions]) + } + + static getCssMinimizerPlugin (minimizer: 'esbuild' | 'lightningcss' | 'csso', minimizerOptions: Record<string, any>) { + const CssMinimizerPlugin = require('css-minimizer-webpack-plugin') + let minify = CssMinimizerPlugin.cssnanoMinify + if (minimizer === 'esbuild') { + minify = CssMinimizerPlugin.esbuildMinify + } else if (minimizer === 'lightningcss') { + minify = CssMinimizerPlugin.lightningCssMinify + } + const options = { + test: REG_STYLE, + parallel: true, + minify, + minimizerOptions: { + preset: [ + 'default', + minimizerOptions + ] + } + } + return WebpackPlugin.getPlugin(CssMinimizerPlugin, [options]) + } + + static getWebpackBarPlugin (webpackBarOptions = {}) { + return WebpackPlugin.getPlugin(TaroWebpackBarPlugin, [webpackBarOptions]) + } +} + +``` + +## MiniWebpackPlugin 提供小程序应用所需插件 + +```ts +import { PLATFORMS } from '@tarojs/helper' +import { isArray, isFunction, PLATFORM_TYPE } from '@tarojs/shared' +import { IPostcssOption } from '@tarojs/taro/types/compile' + +import BuildNativePlugin from '../plugins/BuildNativePlugin' +import MiniCompileModePlugin from '../plugins/MiniCompileModePlugin' +import MiniPlugin from '../plugins/MiniPlugin' +import MiniSplitChunksPlugin from '../plugins/MiniSplitChunksPlugin' +import WebpackPlugin, { PluginArgs } from './WebpackPlugin' + +import type { MiniCombination } from './MiniCombination' + +export class MiniWebpackPlugin { + combination: MiniCombination + pxtransformOption: IPostcssOption<'mini'>['pxtransform'] + + constructor (combination: MiniCombination) { + this.combination = combination + } + + getPlugins () { + const plugins: Record<string, { plugin: any, args: PluginArgs }> = { + providerPlugin: this.getProviderPlugin(), + definePlugin: this.getDefinePlugin(), + miniCssExtractPlugin: this.getMiniCssExtractPlugin() + } + + const copyWebpackPlugin = this.getCopyWebpackPlugin() + if (copyWebpackPlugin) plugins.copyWebpackPlugin = copyWebpackPlugin + + if (!this.combination.isBuildPlugin) { + /** 需要在 MiniPlugin 前,否则无法获取 entry 地址 */ + const miniSplitChunksPlugin = this.getMiniSplitChunksPlugin() + if (miniSplitChunksPlugin) plugins.miniSplitChunksPlugin = miniSplitChunksPlugin + } + + const definePluginOptions = plugins.definePlugin.args[0] + const mainPlugin = this.getMainPlugin(definePluginOptions) + plugins.miniPlugin = mainPlugin + + if (this.combination.config.experimental?.compileMode === true) { + plugins.taroCompileModePlugin = WebpackPlugin.getPlugin(MiniCompileModePlugin, [{ + combination: this.combination, + }]) + } + + return plugins + } + + getProviderPlugin () { + return WebpackPlugin.getProviderPlugin({ + window: ['@tarojs/runtime', 'window'], + document: ['@tarojs/runtime', 'document'], + navigator: ['@tarojs/runtime', 'navigator'], + requestAnimationFrame: ['@tarojs/runtime', 'requestAnimationFrame'], + cancelAnimationFrame: ['@tarojs/runtime', 'cancelAnimationFrame'], + Element: ['@tarojs/runtime', 'TaroElement'], + SVGElement: ['@tarojs/runtime', 'SVGElement'], + MutationObserver: ['@tarojs/runtime', 'MutationObserver'], + history: ['@tarojs/runtime', 'history'], + location: ['@tarojs/runtime', 'location'], + URLSearchParams: ['@tarojs/runtime', 'URLSearchParams'], + URL: ['@tarojs/runtime', 'URL'], + }) + } + + getDefinePlugin () { + const { + env = {}, + runtime = {} as Record<string, boolean>, + defineConstants = {}, + framework = 'react', + buildAdapter = PLATFORMS.WEAPP + } = this.combination.config + + env.FRAMEWORK = JSON.stringify(framework) + env.TARO_ENV = JSON.stringify(buildAdapter) + env.TARO_PLATFORM = JSON.stringify(process.env.TARO_PLATFORM || PLATFORM_TYPE.MINI) + env.SUPPORT_TARO_POLYFILL = env.SUPPORT_TARO_POLYFILL || '"disabled"' + const envConstants = Object.keys(env).reduce((target, key) => { + target[`process.env.${key}`] = env[key] + return target + }, {}) + + const runtimeConstants = { + ENABLE_INNER_HTML: runtime.enableInnerHTML ?? true, + ENABLE_ADJACENT_HTML: runtime.enableAdjacentHTML ?? false, + ENABLE_SIZE_APIS: runtime.enableSizeAPIs ?? false, + ENABLE_TEMPLATE_CONTENT: runtime.enableTemplateContent ?? false, + ENABLE_CLONE_NODE: runtime.enableCloneNode ?? false, + ENABLE_CONTAINS: runtime.enableContains ?? false, + ENABLE_MUTATION_OBSERVER: runtime.enableMutationObserver ?? false + } + + return WebpackPlugin.getDefinePlugin([envConstants, defineConstants, runtimeConstants]) + } + + getCopyWebpackPlugin () { + const combination = this.combination + const { appPath, config } = combination + const { copy } = config + let copyWebpackPlugin + + if (copy?.patterns.length) { + copyWebpackPlugin = WebpackPlugin.getCopyWebpackPlugin(appPath, copy) + } + + return copyWebpackPlugin + } + + getMiniCssExtractPlugin () { + const { fileType } = this.combination + const { miniCssExtractPluginOption = {} } = this.combination.config + const args = Object.assign({ + filename: `[name]${fileType.style}`, + chunkFilename: `[name]${fileType.style}` + }, miniCssExtractPluginOption) + return WebpackPlugin.getMiniCssExtractPlugin(args) + } + + getMiniSplitChunksPlugin () { + const { fileType } = this.combination + const { optimizeMainPackage } = this.combination + let miniSplitChunksPlugin + + if (optimizeMainPackage?.enable) { + miniSplitChunksPlugin = WebpackPlugin.getPlugin(MiniSplitChunksPlugin, [{ + exclude: optimizeMainPackage.exclude, + fileType, + combination: this.combination + }]) + } + + return miniSplitChunksPlugin + } + + getMainPlugin (definePluginOptions) { + const { combination } = this + + const options = { + commonChunks: this.getCommonChunks(), + constantsReplaceList: definePluginOptions, + pxTransformConfig: this.pxtransformOption?.config || {}, + hot: false, + combination, + } + + const plugin = combination.isBuildNativeComp ? BuildNativePlugin : MiniPlugin + return WebpackPlugin.getPlugin(plugin, [options]) + } + + getCommonChunks () { + const { buildNativePlugin, config } = this.combination + const { commonChunks } = config + const defaultCommonChunks = buildNativePlugin?.commonChunks || ['runtime', 'vendors', 'taro', 'common'] + let customCommonChunks: string[] = defaultCommonChunks + if (isFunction(commonChunks)) { + customCommonChunks = commonChunks(defaultCommonChunks.concat()) || defaultCommonChunks + } else if (isArray(commonChunks) && commonChunks.length) { + customCommonChunks = commonChunks + } + return customCommonChunks + } +} + +``` + +## WebpackModule 处理不同模块加载规则 + +```ts +export class WebpackModule { + static getLoader (loaderName: string, options: Record<string, any> = {}) { + return { + loader: require.resolve(loaderName), + options + } + } + + static getCSSLoader (cssLoaderOption) { + const defaultOptions = { + importLoaders: 1, + modules: false + } + const options = Object.assign(defaultOptions, cssLoaderOption) + return WebpackModule.getLoader('css-loader', options) + } + + static getCSSLoaderWithModule (cssModuleOptions: CssModuleOptionConfig, cssLoaderOption) { + const { namingPattern, generateScopedName } = cssModuleOptions + const defaultOptions = Object.assign( + { + importLoaders: 1, + modules: { + mode: namingPattern === 'module' ? 'local' : 'global', + namedExport: false, + exportLocalsConvention: 'as-is', + } + }, + { + modules: isFunction(generateScopedName) + ? { getLocalIdent: (context, _, localName) => generateScopedName(localName, context.resourcePath) } + : { localIdentName: generateScopedName } + } + ) + + // 更改 namedExport 默认值,以统一旧版本行为 + // https://github.com/webpack-contrib/css-loader/releases/tag/v7.0.0 + defaultOptions.modules.namedExport = false + defaultOptions.modules.exportLocalsConvention = 'as-is' + + const options = recursiveMerge({}, defaultOptions, cssLoaderOption) + return WebpackModule.getLoader('css-loader', options) + } + + static getExtractCSSLoader () { + const MiniCssExtractPlugin = require('mini-css-extract-plugin') + return { + loader: MiniCssExtractPlugin.loader + } + } + + static getStyleLoader (options) { + return WebpackModule.getLoader('style-loader', options) + } + + static getPostCSSLoader (options) { + return WebpackModule.getLoader('postcss-loader', options) + } + + static getBaseSassOptions () { + return { + sourceMap: true, + implementation: require('sass'), + sassOptions: { + outputStyle: 'expanded', + // https://github.com/sass/dart-sass/blob/main/CHANGELOG.md#js-api + silenceDeprecations: ['legacy-js-api'], + importer (url, prev, done) { + // 让 sass 文件里的 @import 能解析小程序原生样式文体,如 @import "a.wxss"; + const extname = path.extname(url) + // fix: @import 文件可以不带scss/sass缀,如: @import "define"; + if (extname === '.scss' || extname === '.sass' || extname === '.css' || !extname) { + return null + } else { + const filePath = path.resolve(path.dirname(prev), url) + fs.access(filePath, fs.constants.F_OK, (err) => { + if (err) { + console.log(err) + return null + } else { + fs.readFile(filePath) + .then(res => { + done({ contents: res.toString() }) + }) + .catch(err => { + console.log(err) + return null + }) + } + }) + } + } + } + } + } + + static getSassLoader (sassLoaderOption) { + const options = recursiveMerge<any>({}, WebpackModule.getBaseSassOptions(), { + sassOptions: { + indentedSyntax: true + } + }, sassLoaderOption) + return WebpackModule.getLoader('sass-loader', options) + } + + static getScssLoader (sassLoaderOption) { + const options = recursiveMerge({}, WebpackModule.getBaseSassOptions(), sassLoaderOption) + return WebpackModule.getLoader('sass-loader', options) + } + + static getLessLoader (options) { + return WebpackModule.getLoader('less-loader', options) + } + + static getStylusLoader (options) { + return WebpackModule.getLoader('stylus-loader', options) + } + + static getResolveUrlLoader (options = {}) { + return WebpackModule.getLoader('resolve-url-loader', options) + } + + static getScriptRule () { + return { + test: REG_SCRIPTS, + use: { + babelLoader: WebpackModule.getLoader('babel-loader', { + compact: false + }) + } + } + } + + static getMediaRule (sourceRoot: string, options) { + const maxSize = getAssetsMaxSize(options, MEDIA_LIMIT) + return { + test: REG_MEDIA, + type: 'asset', + parser: { + dataUrlCondition: { + maxSize, + } + }, + generator: { + emit: options.emitFile || options.emit, + outputPath: options.outputPath, + publicPath: options.publicPath, + filename ({ filename }) { + if (isFunction(options.name)) return options.name(filename) + return options.name || filename.replace(sourceRoot + '/', '') + } + } + } + } + + static getFontRule (sourceRoot: string, options) { + const maxSize = getAssetsMaxSize(options, FONT_LIMIT) + return { + test: REG_FONT, + type: 'asset', + parser: { + dataUrlCondition: { + maxSize, + } + }, + generator: { + emit: options.emitFile || options.emit, + outputPath: options.outputPath, + publicPath: options.publicPath, + filename ({ filename }) { + if (isFunction(options.name)) return options.name(filename) + return options.name || filename.replace(sourceRoot + '/', '') + } + } + } + } + + static getImageRule (sourceRoot: string, options) { + const maxSize = getAssetsMaxSize(options, IMAGE_LIMIT) + return { + test: REG_IMAGE, + type: 'asset', + parser: { + dataUrlCondition: { + maxSize, + } + }, + generator: { + emit: options.emitFile || options.emit, + outputPath: options.outputPath, + publicPath: options.publicPath, + filename ({ filename }) { + if (isFunction(options.name)) return options.name(filename) + return options.name || filename.replace(sourceRoot + '/', '') + } + } + } + } +} +``` + +## MiniWebpackModule 处理小程序模块加载规则 + +```ts +export class MiniWebpackModule { + combination: MiniCombination + __postcssOption: [string, any, Func?][] + + constructor (combination: MiniCombination) { + this.combination = combination + } + + getModules () { + const { appPath, config, sourceRoot, fileType } = this.combination + const { + buildAdapter, + sassLoaderOption, + lessLoaderOption, + stylusLoaderOption, + designWidth, + deviceRatio + } = config + + const { postcssOption, cssModuleOption } = this.parsePostCSSOptions() + + this.__postcssOption = getDefaultPostcssConfig({ + designWidth, + deviceRatio, + postcssOption, + alias: config.alias, + }) + + const postcssPlugins = getPostcssPlugins(appPath, this.__postcssOption) + + const cssLoaders = this.getCSSLoaders(postcssPlugins, cssModuleOption) + const resolveUrlLoader = WebpackModule.getResolveUrlLoader() + const sassLoader = WebpackModule.getSassLoader(sassLoaderOption) + const scssLoader = WebpackModule.getScssLoader(sassLoaderOption) + const lessLoader = WebpackModule.getLessLoader(lessLoaderOption) + const stylusLoader = WebpackModule.getStylusLoader(stylusLoaderOption) + + const rule: Record<string, IRule> = { + sass: { + test: REG_SASS_SASS, + oneOf: this.addCSSLoader(cssLoaders, resolveUrlLoader, sassLoader) + }, + scss: { + test: REG_SASS_SCSS, + oneOf: this.addCSSLoader(cssLoaders, resolveUrlLoader, scssLoader) + }, + less: { + test: REG_LESS, + oneOf: this.addCSSLoader(cssLoaders, lessLoader) + }, + stylus: { + test: REG_STYLUS, + oneOf: this.addCSSLoader(cssLoaders, stylusLoader) + }, + normalCss: { + test: REG_CSS, + oneOf: cssLoaders + }, + + script: this.getScriptRule(), + + template: { + test: REG_TEMPLATE, + type: 'asset/resource', + generator: { + filename ({ filename }) { + const extname = path.extname(filename) + const nodeModulesRegx = new RegExp(REG_NODE_MODULES, 'gi') + + return filename + .replace(sourceRoot + '/', '') + .replace(extname, fileType.templ) + .replace(nodeModulesRegx, 'npm') + } + }, + use: [WebpackModule.getLoader(path.resolve(__dirname, '../loaders/miniTemplateLoader'), { + buildAdapter + })] + }, + + xscript: { + test: new RegExp(`\\${this.combination.fileType.xs || 'wxs'}$`), + type: 'asset/resource', + generator: { + filename ({ filename }) { + const nodeModulesRegx = new RegExp(REG_NODE_MODULES, 'gi') + + return filename + .replace(sourceRoot + '/', '') + .replace(nodeModulesRegx, 'npm') + } + }, + use: [WebpackModule.getLoader(path.resolve(__dirname, '../loaders/miniXScriptLoader'))] + }, + + media: this.getMediaRule(), + + font: this.getFontRule(), + + image: this.getImageRule() + } + return { rule } + } + + addCSSLoader (cssLoaders, ...loader) { + const cssLoadersCopy = cloneDeep(cssLoaders) + cssLoadersCopy.forEach(item => { + if (item.use) { + item.use = [...item.use, ...loader] + } + }) + return cssLoadersCopy + } + + getCSSLoaders (postcssPlugins: any[], cssModuleOption: PostcssOption.cssModules) { + const { config } = this.combination + const { + cssLoaderOption + } = config + const extractCSSLoader = WebpackModule.getExtractCSSLoader() + const cssLoader = WebpackModule.getCSSLoader(cssLoaderOption) + const postCSSLoader = WebpackModule.getPostCSSLoader({ + postcssOptions: { + plugins: postcssPlugins + } + }) + + const cssLoaders: CSSLoaders = [{ + use: [ + extractCSSLoader, + cssLoader, + postCSSLoader + ] + }] + + if (cssModuleOption.enable) { + const cssModuleOptionConfig: CssModuleOptionConfig = recursiveMerge({}, { + namingPattern: 'module', + generateScopedName: '[name]__[local]___[hash:base64:5]' + }, cssModuleOption.config) + const cssLoaderWithModule = WebpackModule.getCSSLoaderWithModule(cssModuleOptionConfig, cssLoaderOption) + const styleModuleReg = /(.*\.module).*\.(css|s[ac]ss|less|styl)\b/ + const styleGlobalReg = /(.*\.global).*\.(css|s[ac]ss|less|styl)\b/ + let cssModuleCondition + + if (cssModuleOptionConfig.namingPattern === 'module') { + /* 不排除 node_modules 内的样式 */ + cssModuleCondition = styleModuleReg + // for vue + cssLoaders.unshift({ + resourceQuery: /module=/, + use: [ + extractCSSLoader, + cssLoaderWithModule, + postCSSLoader + ] + }) + } else { + cssModuleCondition = { + and: [ + { not: styleGlobalReg }, + { not: isNodeModule } + ] + } + } + cssLoaders.unshift({ + include: [cssModuleCondition], + use: [ + extractCSSLoader, + cssLoaderWithModule, + postCSSLoader + ] + }) + } + + return cssLoaders + } + + getScriptRule () { + const { sourceDir, config } = this.combination + const { compile = {} } = this.combination.config + const rule: IRule = WebpackModule.getScriptRule() + + rule.include = [ + sourceDir, + filename => /(?<=node_modules[\\/]).*taro/.test(filename) + ] + if (Array.isArray(compile.include)) { + rule.include.unshift(...compile.include) + } + + if (Array.isArray(compile.exclude)) { + rule.exclude = [...compile.exclude] + } + + if (config.experimental?.compileMode === true) { + rule.use.compilerLoader = WebpackModule.getLoader(path.resolve(__dirname, '../loaders/miniCompilerLoader'), { + platform: config.platform.toUpperCase(), + template: config.template, + FILE_COUNTER_MAP, + }) + } + + return rule + } + + parsePostCSSOptions () { + const { postcss: postcssOption = {} } = this.combination.config + const defaultCssModuleOption: PostcssOption.cssModules = { + enable: false, + config: { + namingPattern: 'global', + generateScopedName: '[name]__[local]___[hash:base64:5]' + } + } + + const cssModuleOption: PostcssOption.cssModules = recursiveMerge({}, defaultCssModuleOption, postcssOption.cssModules) + + return { + postcssOption, + cssModuleOption + } + } + + getMediaRule () { + const sourceRoot = this.combination.sourceRoot + const { mediaUrlLoaderOption = {} } = this.combination.config + return WebpackModule.getMediaRule(sourceRoot, mediaUrlLoaderOption) + } + + getFontRule () { + const sourceRoot = this.combination.sourceRoot + const { fontUrlLoaderOption = {} } = this.combination.config + return WebpackModule.getFontRule(sourceRoot, fontUrlLoaderOption) + } + + getImageRule () { + const sourceRoot = this.combination.sourceRoot + const { imageUrlLoaderOption = {} } = this.combination.config + return WebpackModule.getImageRule(sourceRoot, imageUrlLoaderOption) + } +} +``` + +## link + +[多编译内核生态下的极速研发体验](https://taro-docs.jd.com/blog/2023/03/29/D2_17) + +[支持使用 Webpack5 编译小程序和 H5 应用](https://github.com/NervJS/taro/discussions/11533) diff --git a/docs/taro/webpack5/images/zakary-blog_2023-03-29-Taro-D2_17_6 (2).png b/docs/taro/webpack5/images/zakary-blog_2023-03-29-Taro-D2_17_6 (2).png new file mode 100644 index 0000000..44fc667 Binary files /dev/null and b/docs/taro/webpack5/images/zakary-blog_2023-03-29-Taro-D2_17_6 (2).png differ