likes
comments
collection
share

Taro源码-项目build一个weapp的过程

作者站长头像
站长
· 阅读数 31

引言

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


创建的项目

打开此项目,如果使用过taro的开发过小程序的可以很熟悉,包括配置config、src等等

Taro源码-项目build一个weapp的过程

打开我们的首页文件pages/index/index.tsx

...

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

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

Taro源码-项目build一个weapp的过程


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:Taro源码-项目build一个weapp的过程build:Taro源码-项目build一个weapp的过程


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

Taro源码-项目build一个weapp的过程

之后执行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.getNpmPkgpackages/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.tsbuild - Webpack启动器的两个参数appPath:项目路劲 rawConfig:项目参数配置(上面说的options配置)启动webpack进行编译

// 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的钩子函数modifyWebpackChainwebpackChainonWebpackChainReady

  // 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文件存放在项目下的distTaro源码-项目build一个weapp的过程