乐趣区

关于taro:Taro源码项目build一个weapp的过程

引言

基于 Taro3.5.5
此前,咱们学习了 cli 创立一个 Taro 我的项目,并在 packages/taro-cli/bin 文件夹下创立了简略的 Taro 我的项目 appname,接着看一下用 Taro 我的项目去build 一个微信小程序 weapp 的过程


创立的我的项目

关上此我的项目,如果应用过 taro 的开发过小程序的能够很相熟,包含配置 config、src 等等

关上咱们的首页文件 pages/index/index.tsx

...

export default class Index extends Component<PropsWithChildren> {
  ...
  render () {
    return (
      <View className='index'>
        <Text> 这是一个 Taro 我的项目 </Text>
      </View>
    )
  }
}

用 taro (dev 或者 build)命令启动这个我的项目作为微信小程序


build 和 dev

关上创立的 taro 我的项目下的package.json

  "scripts": {
    "build:weapp": "taro build --type weapp",
    ...
    "dev:weapp": "npm run build:weapp -- --watch",
    ...
  },

dev命令相较于 build 命令就是在 build 命令后多加了 --watch,以此来辨别是开发 监听热加载 还是 打包我的项目 dev 命令能够也能够间接这么写

 "dev:weapp": "taro build --type weapp --watch",

打印承受到的内置命令
dev:

build:


Cli

packages/taro-cli/src/cli.ts
与 cli 创立我的项目的 init 命令一样,build的入口也是 packages/taro-cli/bin/taro,在入口文件里执行 cli.run(),Cli 的作用就是承受 内置命令、合成内置命令、设置环境变量、针对不同的内置命令注册对应的命令插件。

build:weapp 之后合成内置命令之后进行 环境变量的设置

      // 设置环境变量
      process.env.NODE_ENV ||= args.env
      if (process.env.NODE_ENV === 'undefined' && (command === 'build' || command === 'inspect')) {
        // 依据 watch 判断是开发环境 development 还是生产环境 production
        process.env.NODE_ENV = (args.watch ? 'development' : 'production')
      }
      args.type ||= args.t
      if (args.type) {
        // 我的项目的类型:weapp、tt、qq、h5、rn...
        process.env.TARO_ENV = args.type
      }
      
      if (typeof args.plugin === 'string') {
        // plugin 小程序插件
        process.env.TARO_ENV = 'plugin'
      }
      // 咱们 build 一个 weapp 那就是 process.env.TARO_ENV = 'weapp'

实例化 Kernel 并把 presets/commands/build.ts 命令插件挂载到 kernel 上

      // command 是合成 build 内置命令失去的 'build'
      // build 插件门路
      const commandPlugins = fs.readdirSync(commandsPath)
      const targetPlugin = `${command}.js`
      ...
      // 实例化 Kernel
      const kernel = new Kernel({
        appPath,
        presets: [path.resolve(__dirname, '.', 'presets', 'index.js')
        ],
        plugins: []})
      kernel.optsPlugins ||= []

      // 注册 build 插件挂载到 kernel 上
      if (commandPlugins.includes(targetPlugin)) {kernel.optsPlugins.push(path.resolve(commandsPath, targetPlugin))
      }

针对不同的内置平台注册对应的端平台插件,咱们 build微信小程序

    ...
    let platform = args.type // weapp
    ...
    // 针对不同的内置平台注册对应的端平台插件
    switch (platform) {
      case 'weapp':
      case 'alipay':
      case 'swan':
      case 'tt':
      case 'qq':
      case 'jd':
        // 注册 weapp 微信小程序平台插件
        // kernel.optsPlugins.push(`@tarojs/plugin-platform-weapp`)
        kernel.optsPlugins.push(`@tarojs/plugin-platform-${platform}`)
        break
      default: {
        // h5, rn, plugin
        // 获取 taro-cli/src/presets/platforms 的文件目录
        const platformPlugins = fs.readdirSync(platformsPath)
        const targetPlugin = `${platform}.js`
        // 如果目录中有对应的 h5 或 rn 或 plugin 插件
        if (platformPlugins.includes(targetPlugin)) {
          // 注册对应的插件
          kernel.optsPlugins.push(path.resolve(platformsPath, targetPlugin))
        }
        break
      }
    }

依据 framework 注册对应的插件这里有vue、vue3、react,咱们创立我的项目的时候抉择的是react

   const framework = kernel.config?.initialConfig.framework
   switch (framework) {
     case 'vue':
       kernel.optsPlugins.push('@tarojs/plugin-framework-vue2')
       break
     case 'vue3':
       kernel.optsPlugins.push('@tarojs/plugin-framework-vue3')
       break
     default:
       kernel.optsPlugins.push('@tarojs/plugin-framework-react')
       break
   }

将所有的插件和插件集都注册完之后咱们打印 kernel 能够看到咱们注册了一个插件集,三个插件,三个插件集别离是 buildweapp 以及react

之后执行 customCommand 函数开调用kernel.run()

// packages/taro-cli/src/cli.ts
customCommand(command, kernel, {...})    
// packages/taro-cli/src/commands/customCommand.ts
export default function customCommand (
  command: string,
  kernel: Kernel,
  args: {_: string[], [key: string]: any }
) {if (typeof command === 'string') {
   ...
    kernel.run({...})
  }
}  

Kernel做了哪些事件能够在创立一个 Taro 我的项目查看


执行钩子

Kernelrun办法中,执行 modifyRunnerOpts 钩子

    // packages/taro-service/src/Kernel.ts
    if (opts?.options?.platform) {opts.config = this.runWithPlatform(opts.options.platform)
      await this.applyPlugins({
        name: 'modifyRunnerOpts', // 批改 webpack 参数
        opts: {opts: opts?.config}
      })
    }
  // @tarojs/plugin-framework-react 用于反对编译 React/PReact/Nerv
  // 批改 webpack 参数
  ctx.modifyRunnerOpts(({opts}) => {...})
})

执行 build 钩子,在 build 钩子里执行 weapp 钩子

 // packages/taro-service/src/Kernel.ts
 await this.applyPlugins({
   name, // name: 'build'
   opts
 })
   // packages/taro-cli/src/presets/commonds/build.ts
   await ctx.applyPlugins(hooks.ON_BUILD_START) // build_start
   await ctx.applyPlugins({
     name: platform, // name: 'weapp' // 执行 weapp 钩子
     opts: {
       config: {
          ...
          // 传入多个钩子:modifyWebpackChain(链式批改 webpack 配置)、modifyBuildAssets...
          // 在 packages/taro-service/src/platform-plugin-base.ts 调用 mini-runner 或 webpack5-runner
          // 在 @tarojs/webpack5-runner 或 @tarojs/mini-runner 作为配置项执行钩子
          async modifyWebpackChain(chain, webpack, data){
            await ctx.applyPlugins({
              name: hooks.MODIFY_WEBPACK_CHAIN, // name: 'modifyWebpackChain'
              ...
            })
          },
          async modifyBuildAssets (assets, miniPlugin) {
            await ctx.applyPlugins({
              name: hooks.MODIFY_BUILD_ASSETS, // name: 'modifyBuildAssets'
              ...
            })
          },
          ...
       }
     }
   })
// @tarojs/plugin-platform-weapp 用于反对编译为微信小程序
export default (ctx: IPluginContext, options: IOptions) => {
  ctx.registerPlatform({
    name: 'weapp',
    useConfigName: 'mini',
    async fn ({config}) {const program = new Weapp(ctx, config, options || {})
      await program.start()}
  })
}

Weapp类根底于TaroPlatformBase(packages/taro-service/src/platform-plugin-base.ts),调用program.start()

  // packages/taro-service/src/platform-plugin-base.ts
  /**
   * 调用 mini-runner 开启编译
   */
  public async start () {await this.setup()
    await this.build()}
  // packages/taro-service/src/platform-plugin-base.ts
  /**
   * 1. 清空 dist 文件夹
   * 2. 输入编译提醒
   * 3. 生成 project.config.json
   */
  private async setup () {await this.setupTransaction.perform(this.setupImpl, this)
    this.ctx.onSetupClose?.(this)
  }
  // packages/taro-service/src/platform-plugin-base.ts
  /**
   * 调用 runner 开始编译
   * @param extraOptions 须要额定传入 @tarojs/mini-runner 或 @tarojs/webpack5-runner 的配置项
   */
  private async build (extraOptions = {}) {this.ctx.onBuildInit?.(this)
    await this.buildTransaction.perform(this.buildImpl, this, extraOptions)
  }

开始编译

Weapp类(packages/taro-weapp/src/index.ts)是根底自 TaroPlatformBase 类(packages/taro-service/src/platform-plugin-base.ts),weapp.start()办法也是继承 TaroPlatformBase

start 办法

  // packages/taro-service/src/platform-plugin-base.ts
  /**
   * 调用 mini-runner 开启编译
   */
  public async start () {await this.setup() // 1. 清空 dist 文件夹,2. 输入编译提醒,3. 生成 project.config.json
    await this.build() // 调用 mini-runner 开始编译}

setup 办法

  // packages/taro-service/src/platform-plugin-base.ts
  private async setup () {// perform 办法是 Transaction 的办法这路的作用就是执行 this.setupImpl 办法,this.setupImpl()
    await this.setupTransaction.perform(this.setupImpl, this)
    this.ctx.onSetupClose?.(this)
  }

Transaction 的 perform 办法

// packages/taro-service/src/platform-plugin-base.ts
class Transaction {
  ...
  async perform (fn: (...args: any[]) => void, scope: TaroPlatformBase, ...args) {
    ...
    // 这里就是执行 this.setupImpl(),外部还做了一些状态治理(感觉没什么用,删了当前执行的成果不变)// 之后 setbuild 的时候就是 this.buildImpl(extraOptions)
    await fn.call(scope, ...args)
    ...
  }
  ...
}

setupImpl 办法

 // packages/taro-service/src/platform-plugin-base.ts
 private setupImpl () {const { needClearOutput} = this.config
    if (typeof needClearOutput === 'undefined' || !!needClearOutput) {
      // 如果 dist 文件存在,清空 dist 文件夹
      this.emptyOutputDir()}
    // 输入编译提醒
    this.printDevelopmentTip(this.platform)
    if (this.projectConfigJson) {
      // 生成 project.config.json
      this.generateProjectConfig(this.projectConfigJson)
    }
    if (this.ctx.initialConfig.logger?.quiet === false) {const { printLog, processTypeEnum} = this.ctx.helper
      printLog(processTypeEnum.START, '开发者工具 - 我的项目目录', `${this.ctx.paths.outputPath}`)
    }
  }

build 办法

 // packages/taro-service/src/platform-plugin-base.ts
 private async build (extraOptions = {}) {this.ctx.onBuildInit?.(this)
    // 与 setup 办法一样这里就是 this.buildImpl(extraOptions);
    await this.buildTransaction.perform(this.buildImpl, this, extraOptions)
  }

buildImpl 办法

  // packages/taro-service/src/platform-plugin-base.ts
  private async buildImpl (extraOptions) {
    // 获取裸露给 @tarojs/cli 的小程序 /H5 Webpack 启动器
    const runner = await this.getRunner()
    // options 配置
    const options = this.getOptions(Object.assign({
      runtimePath: this.runtimePath,
      taroComponentsPath: this.taroComponentsPath
    }, extraOptions))
    await runner(options) // 执行启动器并传入第二个参数 options runner(appPath, options)
  }

getRunner 办法

  // packages/taro-service/src/platform-plugin-base.ts
  /**
   * 返回以后我的项目内的 @tarojs/mini-runner 包
   */
  protected async getRunner () {const { appPath} = this.ctx.paths
    const {npm} = this.helper
    // 获取包名
    let runnerPkg: string
    switch (this.compiler) {
      case 'webpack5':
        runnerPkg = '@tarojs/webpack5-runner'
        break
      default:
        runnerPkg = '@tarojs/mini-runner'
    }
    // 裸露给 `@tarojs/cli` 的小程序 /H5 Webpack 启动器
    // 获取启动器在 node_modules 里两个参数包名和包所在的根目录门路
    const runner = await npm.getNpmPkg(runnerPkg, appPath)
    return runner.bind(null, appPath) // 启动器传入的第一个参数我的项目门路 runner(appPath, options)
  }

启动器的第二个参数 options 配置打印如下:

{entry: { ...}, // 入口 appname/src/app.ts
  alias: {}, // 别名像 @src 代表门路 src 目录下
  copy: {patterns: [], options: {}},
  sourceRoot: 'src', // 寄存主代码根
  outputRoot: 'dist',// 我的项目根
  platform: 'weapp', // 我的项目类型
  framework: 'react', // 平台
  compiler: {
    type: 'webpack5',
    prebundle: {include: [Array], exclude: [Array], esbuild: [Object] }
  },// 批改 webpack 参数
  cache: {enable: true}, // webpack 长久化缓存配置我的项目的 config 配置的
  logger: undefined,
  baseLevel: undefined,
  csso: undefined,
  sass: undefined,
  uglify: undefined,
  plugins: [], // 自定义的插件
  projectName: 'appname',
  env: {NODE_ENV: '"production"'},
  defineConstants: {},
  designWidth: 750,
  deviceRatio: {'640': 1.17, '750': 1, '828': 0.905},
  projectConfigName: undefined,
  jsMinimizer: undefined, // js 压缩器
  cssMinimizer: undefined, // css 压缩器
  terser: undefined,
  esbuild: undefined,
  postcss: {pxtransform: { enable: true, config: {} },
    url: {enable: true, config: [Object] },
    cssModules: {enable: false, config: [Object] }
  }, // 款式处理器
  isWatch: false,
  mode: 'production',
  blended: false,
  isBuildNativeComp: false,
  // 一系列钩子
  modifyWebpackChain: [Function: modifyWebpackChain],
  modifyBuildAssets: [Function: modifyBuildAssets],
  modifyMiniConfigs: [Function: modifyMiniConfigs],
  modifyComponentConfig: [Function: modifyComponentConfig],
  onCompilerMake: [Function: onCompilerMake],
  onParseCreateElement: [Function: onParseCreateElement],
  onBuildFinish: [Function: onBuildFinish],
  nodeModulesPath: '.../taro/packages/taro-cli/bin/appname/node_modules',
  buildAdapter: 'weapp',
  globalObject: 'wx',
  fileType: {
    templ: '.wxml',
    style: '.wxss',
    config: '.json',
    script: '.js',
    xs: '.wxs'
  },// 微信小程序的文件类型
  template: Template {...}, // weapp 代码模板 RecursiveTemplate/UnRecursiveTemplate(@tarojs/shared/src/template.ts)runtimePath: '@tarojs/plugin-platform-weapp/dist/runtime',// 通过 webpack 注入,疾速的 dom、api... 生成器(react -> weapp)taroComponentsPath: '@tarojs/plugin-platform-weapp/dist/components-react'// react 编译 weapp
}

简略的看一下npm.getNpmPkg
packages/taro-helper/src/npm.ts

// packages/taro-helper/src/npm.ts
export async function getNpmPkg (npmName: string, root: string) {
  let npmPath
  try {
    // 检测并返回可找到的包门路(webpack5-runner 的包门路)npmPath = resolveNpmSync(npmName, root)
  } catch (err) {
    // 这里找不到就去下载安装
    ...
  }
  // 获取包并返回
  const npmFn = require(npmPath)
  return npmFn // webpack5-runner 里的 build 函数
}

webpack5-runner – Webpack 启动器

packages/taro-webpack5-runner/src/index.mini.ts
build – Webpack 启动器的两个参数 appPath:我的项目路劲 rawConfig:我的项目参数配置(下面说的options 配置)

// packages/taro-webpack5-runner/src/index.mini.ts
async function build (appPath: string, rawConfig: MiniBuildConfig): Promise<Stats> {
  1. 批改 webpack 配置
  2. 预编译晋升编译速度
  3. 启动 webpack 编译
  ...
}

1. 批改 webpack 配置
实例化 MiniCombination 执行 combination.makeMiniCombination 是继承Combination

  // packages/taro-webpack5-runner/src/index.mini.ts
  // 批改 webpack 配置
  const combination = new MiniCombination(appPath, rawConfig)
  await combination.make()

MiniCombination(Combination)
webpack-chain 批改 webpack 配置,执行批改 webpack 的钩子函数modifyWebpackChain...

  // packages/taro-webpack5-runner/src/webpack/Combination.ts
  async make () {
    // 获取 sass 预处理器 loader 并整顿 this.config
    await this.pre(this.rawConfig)
    // 在 MiniCombination 里重写了 process 办法,重写之后作用是用 webpack-chain 包的 chain.merge 去批改 webpack
    this.process(this.config, this.appPath)
    // 执行钩子批改 webpack:modifyWebpackChain、webpackChain、onWebpackChainReady
    await this.post(this.config, this.chain)
  }

2. 预编译晋升编译速度
WebpackModuleFederation

// packages/taro-webpack5-runner/src/index.mini.ts
// taro-webpack5-prebundle 预编译晋升编译速度(packages/taro-webpack5-prebundle/src/mini.ts)const prebundle = new Prebundle({
  appPath,
  sourceRoot: combination.sourceRoot,
  chain: combination.chain,
  enableSourceMap,
  entry,
  runtimePath
})
await prebundle.run(combination.getPrebundleOptions())
  // packages/taro-webpack5-prebundle/src/mini.ts
  async run () {
    ...
    /** 应用 esbuild 对 node_modules 依赖进行 bundle */
    await this.bundle()
    /** 把依赖的 bundle 产物打包成 Webpack Module Federation 格局 */
    await this.buildLib()
    /** 我的项目 Host 配置 Module Federation */
    this.setHost()
    await super.run()}

3. 启动 webpack 编译

// packages/taro-webpack5-runner/src/index.mini.ts
// 这里调用 webpack(webpackConfig)会在控制台呈现进度条
const compiler = webpack(webpackConfig)

辨别是否是开发环境watch 热更新监听

  // packages/taro-webpack5-runner/src/index.mini.ts
  if (config.isWatch) {
      // watch 热更新监听而后回调 callback
      compiler.watch({
        aggregateTimeout: 300,
        poll: undefined
      }, callback)
    } else {
      // 打包实现后敞开而后回调 callback
      compiler.run((err: Error, stats: Stats) => {compiler.close(err2 => callback(err || err2, stats))
      })
    }

编译最终是由 webpack 实现的

//packages/taro-webpack5-runner/src/index.mini.ts
// 引入 webpack
import webpack 返回, {Stats} from 'webpack'
...
// 传入 webpackConfig 调用 webpack 返回 compiler 
const compiler = webpack(webpackConfig)
... 
// 执行 compiler.run 进行编译
compiler.run((err: Error, stats: Stats) => {compiler.close(err2 => callback(err || err2, stats))
})

打印webpackConfig 外面有必须的入口配置、进口配置、插件配置 …

{target: [ 'web', 'es5'],
  watchOptions: {aggregateTimeout: 200},
  cache: {
    type: 'filesystem',
    buildDependencies: {config: [Array] },
    name: 'production-weapp'
  },
  mode: 'production',
  devtool: false,
  output: {
    chunkLoadingGlobal: 'webpackJsonp',
    path: '.../taro/packages/taro-cli/bin/appname/dist',
    publicPath: '/',
    filename: '[name].js',
    chunkFilename: '[name].js',
    globalObject: 'wx',
    enabledLibraryTypes: [],
    devtoolModuleFilenameTemplate: [Function (anonymous)]
  },
  resolve: {
    symlinks: true,
    fallback: {fs: false, path: false},
    alias: {
      'regenerator-runtime': '.../taro/packages/taro-cli/bin/appname/node_modules/regenerator-runtime/runtime-module.js',
      '@tarojs/runtime': '.../taro/packages/taro-cli/bin/appname/node_modules/@tarojs/runtime/dist/runtime.esm.js',
      '@tarojs/shared': '.../taro/packages/taro-cli/bin/appname/node_modules/@tarojs/shared/dist/shared.esm.js',
      '@tarojs/components$': '@tarojs/plugin-platform-weapp/dist/components-react',
      'react-dom$': '@tarojs/react'
    },
    extensions: ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.vue'],
    mainFields: ['browser', 'module', 'jsnext:main', 'main'],
    plugins: [[MultiPlatformPlugin] ]
  },
  resolveLoader: {modules: [ 'node_modules'] },
  module: {
    rules: [[Object], [Object],
      [Object], [Object],
      [Object], [Object],
      [Object], [Object],
      [Object], [Object]
    ]
  },
  optimization: {
    sideEffects: true,
    minimize: true,
    usedExports: true,
    runtimeChunk: {name: 'runtime'},
    splitChunks: {
      chunks: 'all',
      maxInitialRequests: Infinity,
      minSize: 0,
      cacheGroups: [Object]
    },
    minimizer: [[TerserPlugin], [CssMinimizerPlugin] ]
  },
  plugins: [
    TaroWebpackBarPlugin {
      profile: false,
      handler: [Function (anonymous)],
      modulesCount: 5000,
      dependenciesCount: 10000,
      showEntries: true,
      showModules: true,
      showDependencies: true,
      showActiveModules: true,
      percentBy: undefined,
      options: [Object],
      reporters: [Array]
    },
    ProvidePlugin {definitions: [Object] },
    DefinePlugin {definitions: [Object] },
    MiniCssExtractPlugin {_sortedModulesCache: [WeakMap],
      options: [Object],
      runtimeOptions: [Object]
    },
    MiniSplitChunksPlugin {options: [Object],
      _cacheGroupCache: [WeakMap],
      tryAsync: [Function (anonymous)],
      subCommonDeps: Map(0) {},
      subCommonChunks: Map(0) {},
      subPackagesVendors: Map(0) {},
      distPath: '',
      exclude: [],
      fileType: [Object]
    },
    TaroMiniPlugin {filesConfig: {},
      isWatch: false,
      pages: Set(0) {},
      components: Set(0) {},
      tabBarIcons: Set(0) {},
      dependencies: Map(0) {},
      pageLoaderName: '@tarojs/taro-loader/lib/page',
      independentPackages: Map(0) {},
      options: [Object],
      prerenderPages: Set(0) {}}
  ],
  performance: {maxEntrypointSize: 2000000},
  entry: {
    app: ['.../taro/packages/taro-cli/bin/appname/src/app.ts']
  }
}

编译实现后就会生成 weapp 文件

退出移动版