動態

詳情 返回 返回

Taro 源碼揭秘:9. Taro 是如何生成 webpack 配置進行構建小程序的? - 動態 詳情

1. 前言

大家好,我是若川,歡迎關注我的公眾號:若川視野。從 2021 年 8 月起,我持續組織了好幾年的每週大家一起學習 200 行左右的源碼共讀活動,感興趣的可以點此掃碼加我微信 ruochuan02 參與。另外,想學源碼,極力推薦關注我寫的專欄《學習源碼整體架構系列》,目前是掘金關注人數(6k+人)第一的專欄,寫有幾十篇源碼文章。

截至目前(2024-11-22),目前最新是 4.0.7,官方4.0正式版本的介紹文章暫未發佈。官方之前發過Taro 4.0 Beta 發佈:支持開發鴻蒙應用、小程序編譯模式、Vite 編譯等。

計劃寫一個 Taro 源碼揭秘系列,博客地址:https://ruochuan12.github.io/taro 可以加入書籤,持續關注若川。

  • [x] 1. 揭開整個架構的入口 CLI => taro init 初始化項目的秘密
  • [x] 2. 揭開整個架構的插件系統的秘密
  • [x] 3. 每次創建新的 taro 項目(taro init)的背後原理是什麼
  • [x] 4. 每次 npm run dev:weapp 開發小程序,build 編譯打包是如何實現的?
  • [x] 5. 高手都在用的發佈訂閲機制 Events 在 Taro 中是如何實現的?
  • [x] 6. 為什麼通過 Taro.xxx 能調用各個小程序平台的 API,如何設計實現的?
  • [x] 7. Taro.request 和請求響應攔截器是如何實現的
  • [x] 8. Taro 是如何使用 webpack 打包構建小程序的?
  • [x] 9. Taro 是如何生成 webpack 配置進行構建小程序的?
  • [x] 10. Taro 到底是怎樣轉換成小程序文件的?
  • [ ] 等等

前面 4 篇文章都是講述編譯相關的,CLI、插件機制、初始化項目、編譯構建流程。
第 5-7 篇講述的是運行時相關的 Events、API、request 等。
第 9 篇接着繼續追隨第4篇和第8篇的腳步,繼續分析 @tarojs/webpack5-runner,Taro 是如何生成 webpack 配置進行構建小程序的?

關於克隆項目、環境準備、如何調試代碼等,參考第一篇文章-準備工作、調試。後續文章基本不再過多贅述。

學完本文,你將學到:

1. Taro 是如何生成 webpack 配置進行構建小程序的?
等等

2. webpack 打包構建

在第4篇文章末尾,我們可以回顧下,如何獲取 webpack 配置和執行 webpack() 構建的。還有第8篇文章 把所有 webpack 配置輸出出來,並解析了一些重要配置。

// webpack 配置
{
  entry: {
    app: [
      '/Users/ruochuan/git-source/github/taro4-debug/src/app.ts'
    ]
  },
  target: ['web', 'es5'],
  // 省略若干配置項...
}

我們這篇文章主要來看是如何生成用於構建小程序的 webpack 配置的。

關於打包編譯官方有一篇博客多編譯內核生態下的極速研發體驗,主要講述思想和流程。

webpack5 編譯內核

Taro RFC 支持使用 Webpack5 編譯小程序和 H5 應用,講述了 webpack4 重構為 webpack5 主要的一些修改和優化等。

我們這篇文章主要分析 @tarojs/webpack5-runner 小程序部分的具體源碼實現。

3. @tarojs/webpack5-runner

暴露給 @tarojs/cli 的小程序/H5 Webpack 啓動器。

這個 npm 包,主要要解決的問題是:把 taro 項目用 webpack 編譯到小程序、H5、鴻蒙。會涉及到比較多代碼是類似或者可以共用的。

package.json 入口文件 "main": "index.js" 入口文件 index.js

// packages/taro-webpack5-runner/index.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

根據不同平台導出不同端的產物。由此可以得出,小程序端的源代碼文件是 packages/taro-webpack5-runner/src/index.mini.ts

我們來看 webpack 文件夾 packages/taro-webpack5-runner/src/webpack 主要文件如下:

基礎類 小程序 H5 鴻蒙
Combination MiniCombination H5Combination HarmonyCombination
BaseConfig MiniBaseConfig H5BaseConfig HarmonyBaseConfig
WebpackPlugin MiniWebpackPlugin H5WebpackPlugin HarmonyWebpackPlugin
WebpackModule MiniWebpackModule H5WebpackModule HarmonyWebpackModule
// 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)
    // 省略若干代碼...
  })
}
// 重點就以下這幾句
// 生成獲取 webpack 配置,執行 webpack(webpackConfig)
const combination = new MiniCombination(appPath, rawConfig)
await combination.make()
const webpackConfig = combination.chain.toConfig()
const compiler = webpack(webpackConfig)

調用構造函數 new MiniCombination(appPath, rawConfig) 生成 combination 對象。
其中 appPath 是項目根目錄,rawConfig 參數,是開發項目中 config/index 中的配置和 ctx.applyPlugins({name: platform, opts: {}}) 注入的各個階段的鈎子函數等。

調用 make 方法後,再讀取實例對象 combination.chain.toConfig() 即可生成 webpack 配置了。再調用 webpack 函數執行。

4. 簡易實現

如果不需要考慮小程序端、H5、鴻蒙端等因素。那麼可以直接寫在一個文件裏。

我們可以先實現一個 Combination 組合的基礎類,其他平台(小程序、H5、鴻蒙)來繼承。再來實現一個 make 方法。

4.1 簡易實現 Combination

class Combination{
    constructor(){
        // 定義一些共用的屬性
    }

    async make(){
        // 前置操作
        await this.pre();
        // 主要操作,讓子類來實現
        this.process();
        // 後置操作
        await this.post();
    }
    async pre(){},
    process(){},
    async post(){},
}

4.2 簡易實現 MiniCombination

class MiniCombination extends Combination{
    process(){
        const baseConfig = new MiniBaseConfig();
        const chain = this.chain = baseConfig.chain;
        this.chain.merge({
            // ...
        });
    }
}

我們在這裏調用 new MiniBaseConfig 類。這個類繼承自 BaseConfig 基礎配置類。process 函數執行完成,就可以用 miniCombination.chain.toConfig() 獲取最終的 webpack 配置。

4.3 簡易實現 MiniBaseConfig

export class MiniBaseConfig extends BaseConfig {
 constructor() {
    super();
    this.chain.merge({
        // 小程序端的配置
    });
 }
}

我們來簡易實現這個關鍵基礎配置類。

4.4 簡易實現 BaseConfig

會有一些基礎配置,我們先實現一個 BaseConfig 類。

import Chain from 'webpack-chain';
export class BaseConfig {
  constructor () {
    const chain = this._chain = new Chain()
    chain.merge({
      target: ['web', 'es5'],
      //   等基礎配置
    })
  }
  get chain(){
    return this._chain;
  }
}

這裏我們引入 webpack-chain 這個 npm 包,雖然到現在已經不維護了,但是下載量極大。

webpack-chain 使用鏈式 API 生成並簡化 webpack 4 配置的修改。

webpack-chain 簡易實現。

// webpack-chain
class Chain{
    constructor(){}
    static toString(){
       console.log('string');
        return JSON.stringify({});
    }
    merge(){
        // 合併配置
    }
    toConfig(){
        // 可以返回所有配置
        console.log('toConfig');
        return {};
    }
}

上文的簡易實現,基本就是整個源碼的組織結構。

我們來看真實的源碼實現。

5. Combination 組合

export class MiniCombination extends Combination<IMiniBuildConfig> {
    process (config: Partial<IMiniBuildConfig>) {
        // 省略代碼
    }
}
export class Combination<T extends IMiniBuildConfig | IH5BuildConfig | IHarmonyBuildConfig = CommonBuildConfig> {
  appPath: string
  config: T
  chain: Chain
  //   省略若干代碼...

  constructor (appPath: string, config: T) {
    this.appPath = appPath
    this.rawConfig = config
    //   省略若干代碼...
  }

  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) {
    // 拆分放到下方
  }

  async post (config: T, chain: Chain) {
    // 拆分放到下方
  }
}

5.1 pre 前置處理

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
    }
  }

5.2 post 後置處理

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)
    }
  }

調用配置中的鈎子函數 modifyWebpackChainwebpackChainonWebpackChainReady

6. MiniCombination 小程序組合類

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

    // 省略若干代碼...拆分到下方在講述

    chain.merge({
      entry: webpackEntry,
      output: webpackOutput,
      mode,
      devtool: this.getDevtool(sourceMapType),
      resolve: {
        alias: this.getAlias()
      },
      plugin,
      module,
      optimization: this.getOptimization()
    })
  }
}

最後 chain.merge 入口、出口、插件、模塊等,整個過程就結束了,完美撒花~當然,我們肯定不會只限於此。

我們先來看看 MiniBaseConfig 的具體實現。

export class MiniBaseConfig extends BaseConfig {
  defaultTerserOptions = {
    // 省略
  }

  constructor(appPath: string, config: Partial<IMiniBuildConfig>) {
    super(appPath, config)
    // ...
  }
}

可以看出繼承自 BaseConfig 基本配置類。

我們來看看 BaseConfig 類的具體實現。

7. BaseConfig 基本配置類

import Chain from 'webpack-chain'
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 }]
          }
        }
      },
      // 省略若干代碼...
    })

    // 持久化緩存
    // 省略若干代碼...
  }

  // minimizer 配置
  protected setMinimizer (config: Config, defaultTerserOptions) {
    // 省略若干代碼...
    this.chain.merge({
      optimization: {
        minimize,
        minimizer
      }
    })
  }

  get chain () {
    return this._chain
  }
}

8. MiniBaseConfig 小程序基本配置類

export class MiniBaseConfig extends BaseConfig {
  defaultTerserOptions = {
    parse: {
      ecma: 8,
    },
    compress: {
      ecma: 5,
      warnings: false,
      //   省略代碼...
    },
    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,
        },
      },
      // 省略若干代碼...
    })

    this.setMinimizer(config, this.defaultTerserOptions)
  }
}

9. 再探 MiniCombination 實例對象的 process 函數

9.1 process 第一部分

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)
    // 拆分在下方
}

9.2 process 第二部分

process (config: Partial<IMiniBuildConfig>) {
    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
    }
}

如果是編譯小程序原生插件,webpack 配置項,合入 context 配置。
optimizeMainPackage 顧名知意。優化主包,默認開啓。

9.3 process 第三部分

process (config: Partial<IMiniBuildConfig>) {
    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({
        // ...
    });
}

這部分代碼主要獲取入口、出口、modulesplugins 等,合併到之前的 webpack 配置中。

我們接下來,繼續分析。

  • getEntry
  • getOutput
  • MiniWebpackPlugin
  • MiniWebpackModule

10. getEntry 獲取入口

getEntry (entry: IMiniBuildConfig['entry']) {
    return this.isBuildPlugin ? this.buildNativePlugin.entry : entry
}

如果是編譯插件,用插件的入口,否則是用入口 {entry: app: [ 'src/app.ts' ]}

11. getOutput 獲取出口

getOutput ({ publicPath, globalObject, isBuildPlugin, output }) {
    return {
      path: this.outputDir,
      publicPath,
      filename: '[name].js',
      chunkFilename: '[name].js',
      globalObject,
      enabledLibraryTypes: isBuildPlugin ? ['commonjs'] : [],
      ...output
    }
  }

支持混入開發者傳入的 output,項目 config/index.ts 中的配置。

12. MiniWebpackPlugin 提供小程序應用所需插件

// packages/taro-webpack5-runner/src/webpack/MiniWebpackPlugin.ts
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()
    }

    // 省略若干代碼...

    return plugins
  }
}

小程序 webpack 插件設置。可以用 getPlugins 獲取 webpack 插件。

13. MiniWebpackModule 處理小程序模塊加載規則

// packages/taro-webpack5-runner/src/webpack/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 rule: Record<string, IRule> = {
      //   省略若干代碼...

      media: this.getMediaRule(),

      font: this.getFontRule(),

      image: this.getImageRule()
    }
    return { rule }
  }
}

小程序 webpack module 設置。可以用 getModules 獲取 webpack modules。

14. WebpackPlugin 提供應用所需插件

// packages/taro-webpack5-runner/src/webpack/WebpackPlugin.ts

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')
    // 省略代碼...
    return WebpackPlugin.getPlugin(CopyWebpackPlugin, [args])
  }

  // 省略若干代碼...
}

基礎的 webpack plugin 設置。提供給不同的平台(小程序、H5、鴻蒙)調用。

15. WebpackModule 處理不同模塊加載規則

// packages/taro-webpack5-runner/src/webpack/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)
  }
  // 省略若干代碼...
}

基礎的 webpack module 設置。提供給不同的平台(小程序、H5、鴻蒙)調用。

16. 總結

我們來回顧下開頭的官方博客的配圖。

webpack5 編譯內核

還有開頭的 BaseConfig 簡易實現和 MiniCombination 簡易實現。

import Chain from 'webpack-chain';
export class BaseConfig {
  constructor () {
    const chain = this._chain = new Chain()
    chain.merge({
      target: ['web', 'es5'],
      //   等基礎配置
    })
  }
  get chain(){
    return this._chain;
  }
}
class MiniCombination extends Combination{
    process(){
        const baseConfig = new MiniBaseConfig();
        const chain = this.chain = baseConfig.chain;
        this.chain.merge({
            // ...
        });
    }
}

我們通過 webpack-chain 鏈式 API 來生成 webpack 配置。

我們在這裏調用 new MiniBaseConfig 類。這個類繼承自 BaseConfig 基礎配置類。process 函數執行完成,就可以用 miniCombination.chain.toConfig() 獲取最終的 webpack 配置。

我們主要分析了 @tarojs/webpack5-runner 的生成小程序部分的 webpack 配置的實現。

下一篇應該是分析給 taro 寫的特定的 webpack 插件 TaroMiniPlugin。


如果看完有收穫,歡迎點贊、評論、分享、收藏支持。你的支持和肯定,是我寫作的動力。也歡迎提建議和交流討論

作者:常以若川為名混跡於江湖。所知甚少,唯善學。若川的博客,github blog,可以點個 star 鼓勵下持續創作。

最後可以持續關注我@若川,歡迎關注我的公眾號:若川視野。從 2021 年 8 月起,我持續組織了好幾年的每週大家一起學習 200 行左右的源碼共讀活動,感興趣的可以點此掃碼加我微信 ruochuan02 參與。另外,想學源碼,極力推薦關注我寫的專欄《學習源碼整體架構系列》,目前是掘金關注人數(6k+人)第一的專欄,寫有幾十篇源碼文章。

user avatar toopoo 頭像 grewer 頭像 haoqidewukong 頭像 kobe_fans_zxc 頭像 dirackeeko 頭像 zourongle 頭像 leexiaohui1997 頭像 longlong688 頭像 huajianketang 頭像 Dream-new 頭像 solvep 頭像 febobo 頭像
點贊 153 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.