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 配置的。
關於打包編譯官方有一篇博客多編譯內核生態下的極速研發體驗,主要講述思想和流程。
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)
}
}
調用配置中的鈎子函數 modifyWebpackChain、webpackChain、onWebpackChainReady。
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({
// ...
});
}
這部分代碼主要獲取入口、出口、modules、plugins 等,合併到之前的 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. 總結
我們來回顧下開頭的官方博客的配圖。
還有開頭的 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+人)第一的專欄,寫有幾十篇源碼文章。