引言
基于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 {
...
render () {
return (
这是一个Taro项目
)
}
}
用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",
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
可以看到我们注册了一个插件集,三个插件,三个插件集分别是build
、weapp
以及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项目查看
执行钩子
在Kernel
的run
方法中,执行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.tsbuild
- Webpack启动器的两个参数appPath
:项目路劲 rawConfig
:项目参数配置(上面说的options
配置)
// packages/taro-webpack5-runner/src/index.mini.ts
async function build (appPath: string, rawConfig: MiniBuildConfig): Promise {
1.修改webpack配置
2.预编译提升编译速度
3.启动webpack编译
...
}
1.修改webpack配置
实例化MiniCombination
执行combination.make
,MiniCombination
是继承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文件