POI 源碼閱讀

更好的觀看體驗移步飛書

https://bytedance.feishu.cn/docs/doccnOadukROqZqGS9uOj6oGVzbcss

時間花費html

時間一天兩個小時左右,讀文檔用了兩個多小時,一共花費五天 10 個小時的時間,基本把大概的邏輯(主線)理解清楚了。vue

寫文檔兩個小時。react

讀 poi 總體感覺比 sao 略微的費勁一些,須要熟悉 webpack 的操做配置。webpack

POI 是什麼?git

就是把webpack 封裝了一層的工具,能夠本地啓服務和打包。github

省去本身的一大堆配置。web

這裏我要吐槽一下,若是我用 react 和 vue 的官方的那個腳手架,不也很好?還自帶 routerexpress

若是我真的想改什麼不也得看 poi 的文檔,不也很麻煩嗎?npm

從整個大方向上,用的用戶不是不少。不過好在能夠給本身造就經驗和提高技術,從這個意義上說仍是有價值的。

import styles from './style.module.css'

const el = document.createElement('div')el.className = styles.titleel.textContent = 'Hello Poi!'

document.body.appendChild(el)

這種寫法例子,也不以爲很時尚。

POI 有什麼功能?

https://poi.js.org/guide/ 從文檔出發,讀文檔就讀了好幾個小時

題外話:這個頁面的那個淡淡的藍色,不如深色更好看,由於自己就是淺色模式,這個淡淡的色,很不容易吸引眼球。

最基礎的功能有

一個本地開發,一個打包

// 打包,你爲啥不寫 --build 呢???

poi --prod

// 本地 port 開發模式

poi --dev

create-poi-app 實現了模板搬運,初始化項目

除此以外還有一個模板搬運的過程

yarn global add create-poi-app

create-poi-app my-app

細節功能

--inspect-webpack   以默認編輯器打開webpack 配置

正如文檔所說,裏面作了各類文件的轉譯,使用 babel,其實這都是 webpack 乾的事啦,只要初始化的時候選擇相應的配置就能夠。

不用本身配置啦。

|

除此以外還擁有一些代理功能就是 webpack 提供的啦。

你能夠把本身想寫的配置寫到 poi.config.js 這樣合併到默認的 webpack.config.js 造成新的配置

POI 是怎麼實現的?

地址 https://github.com/egoist/poi

裏面使用了很常見的 lerna 實現多包管理,不過最主要的也就兩個包啦

|

一個是 create-poi-app ,一個是core/poi

create-poi-app

這個裏面比較簡單,根據以前讀過 sao 的經驗,15 分鐘就讀明白了,很少嘮叨。不過也讓我知道了 sao 能夠更靈活的運用, sao 的實現的功能就是模板搬運。

const app = sao({

generator: path.join(__dirname, '../generator'),

outDir: targetFolder,

npmClient

})

把模板傳入,把輸出目錄即 outDir 傳入,根據配置文件,問你一大堆問題,拿到問題結果搬用。

能夠舉個簡單的例子

拿到你的回答是 ts 仍是 js 而後去添加相應的文件,或是添加一些插件和配置

{

type: 'add',

templateDir: `templates/${typeChecker === 'ts' ? 'ts' : 'js'}`,

files: '**',

filters: {

'**/*.test.{js,ts}': Boolean(unit)

}

}

core/poi

下面的略微的有點難度,不過通過我屢次翻看,終於明白了核心邏輯。

必定要聚精會神的看這裏,這裏寫的纔是整篇文章最重要的。

進入

const poi = new Poi()

_await_ poi.run()

從 bin/cli 下開始進入

這裏引入了 require('v8-compile-cache'),只是爲了更快的速度。

咱們走進 lib/index.js  最複雜的就是頁面了,講清楚這個,基本整個項目都講通了。

先理清楚幾個變量

this.args = parseArgs(rawArgs)

this.args 就是 --serve --prod  --debug --test 之類的東西

this.hooks = new Hooks()

this.hooks 就是一個發佈訂閱模式,名字和 webpack 的 hook 管理有點像

module.exports = _class_ Hooks {

constructor() {

this.hooks = new Map()

}

add(name, fn) {

const hooks = this.get(name)

hooks.add(fn)

this.hooks.set(name, hooks)

}

get(name) {

_return_ this.hooks.get(name) || new Set()

}

invoke(name, ...args) {

_for_ (const hook of this.get(name)) {

hook(...args)

}

}

async invokePromise(name, ...args) {

_for_ (const hook of this.get(name)) {

_await_ hook(...args)

}

}

}

add 是添加函數 ,invoke 是執行相應的函數,還添加一個異步執行,這裏代碼能夠好好學習下,好比他使用了 set 和 map 頗有意思。

this.cwd = this.args.get('cwd')

cwd 就是你的項目路徑,是你本身的項目路徑

this.configLoader = createConfigLoader(this.cwd)

createConfigLoader 這裏仍是使用  joycon 讀取配置

傳入你要讀取的配置文件

好比

defaultConfigFiles = [

'poi.config.js',

'poi.config.ts',

'package.json',

'.poirc',

'.poirc.json',

'.poirc.js'

]

joycon 會把 path 和配置 data 給讀取到

const { path: configPath, data: configFn } = this.configLoader.load({

files: configFiles,

packageKey: 'poi'

})

this.config =

typeof configFn === 'function' ? configFn(this.args.options) : configFn

此時咱們拿到配置文件數據

this.pkg = this.configLoader.load({

files: ['package.json']

})

this.pkg.data = this.pkg.data || {}

拿到你的 package.json 數據

initPlugins

this.plugins = [

{ resolve: require.resolve('./plugins/command-options') },

{ resolve: require.resolve('./plugins/config-babel') },

{ resolve: require.resolve('./plugins/config-vue') },

{ resolve: require.resolve('./plugins/config-css') },

{ resolve: require.resolve('./plugins/config-font') },

{ resolve: require.resolve('./plugins/config-image') },

{ resolve: require.resolve('./plugins/config-eval') },

{ resolve: require.resolve('./plugins/config-html') },

{ resolve: require.resolve('./plugins/config-electron') },

{ resolve: require.resolve('./plugins/config-misc-loaders') },

{ resolve: require.resolve('./plugins/config-reason') },

{ resolve: require.resolve('./plugins/config-yarn-pnp') },

{ resolve: require.resolve('./plugins/config-jsx-import') },

{ resolve: require.resolve('./plugins/config-react-refresh') },

{ resolve: require.resolve('./plugins/watch') },

{ resolve: require.resolve('./plugins/serve') },

{ resolve: require.resolve('./plugins/eject-html') },

{ resolve: require.resolve('@poi/plugin-html-entry') }

]

.concat(mergePlugins(configPlugins, cliPlugins))

.map(plugin => {

_if_ (typeof plugin.resolve === 'string') {

plugin._resolve = plugin.resolve

plugin.resolve = require(plugin.resolve)

}

_return_ plugin

})

給 plugins 加點東西,很重要的東西。 合併了 cli 的 plugin 和配置裏的 plugin

咱們點進 plugin 看一看

有 exports.cli exports.when  exports.apply 他們分別在不一樣時機去執行,

api.hook('createWebpackChain', config => {

config.module

.rule('font')

.test(/.(eot|otf|ttf|woff|woff2)(?.*)?_$_/)

.use('file-loader')

.loader(require.resolve('file-loader'))

.options({

name: api.config.output.fileNames.font

})

})

在  apply 裏面全是 api.hook createWebpackChain ,這樣寫,只要當我觸發 invoke createWebpackChain 的時候,這些函數將會被同時執行。

serve

咱們看最最最重要的serve,看明白它也就理清核心了

// 拿到默認 webpackConfig 配置,怎麼拿到的,下面說

const webpackConfig = api.createWebpackChain().toConfig()

// api 就是 poi 實例 , const compiler = require('webpack')(config) 把配置文件傳入生成編譯後的文件

const compiler = api.createWebpackCompiler(webpackConfig)

//啓動服務的配置,上面的配置是編譯 babel 的配置

const devServerConfig = Object.assign(

{

noInfo: true,

historyApiFallback: true,

overlay: false,

disableHostCheck: true,

compress: true,

// _Silence WebpackDevServer's own logs since they're generally not useful._

// _It will still show compile warnings and errors with this setting._

clientLogLevel: 'none',

// _Prevent a WS client from getting injected as we're already including_

// _`webpackHotDevClient`._

injectClient: false,

publicPath: webpackConfig.output.publicPath,

contentBase:

api.config.publicFolder && api.resolveCwd(api.config.publicFolder),

watchContentBase: true,

stats: 'none'

},

devServer,

{

proxy:

typeof devServer.proxy === 'string'

? require('@poi/dev-utils/prepareProxy')(

devServer.proxy,

api.resolveCwd(api.config.publicFolder),

api.cli.options.debug

)

: devServer.proxy

}

)

// 啓動服務,監聽端口

const WebpackDevServer = require('webpack-dev-server')

const server = new WebpackDevServer(compiler, devServerConfig)

api.hooks.invoke('createServer', { server, port, host })

server.listen(port, host)

這裏有點不理解點地方

api.hooks.invoke('beforeDevMiddlewares', server)

api.hooks.invoke('onCreateServer', server) // _TODO:_ _remove this in the future_

api.hooks.invoke('afterDevMiddlewares', server)

api.hooks.invoke('createServer', { server, port, host })

api.hooks.invoke('createDevServerConfig', devServerConfig)

在整套代碼裏我沒有找到任何添加 hook 操做,這些也不是 webpack 的生命週期,我懷疑只是添加鉤子給其餘的引入裏用的

exports.apply = api => {

// 這裏 config 拿到的是 webpack 的 config

api.hook('createWebpackChain', config => {

_if_ (!api.cli.options.serve) _return_

// 若是有 hot,給 config 添加 hot 的配置

_if_ (api.config.devServer.hot) {

const hotEntries =

api.config.devServer.hotEntries.length > 0

? api.config.devServer.hotEntries

: config.entryPoints.store.keys()

_for_ (const entry of hotEntries) {

_if_ (config.entryPoints.has(entry)) {

config.entry(entry).prepend('#webpack-hot-client')

}

}

const { HotModuleReplacementPlugin } = require('webpack')

HotModuleReplacementPlugin.__expression = `require('webpack').HotModuleReplacementPlugin`

config.plugin('hot').use(HotModuleReplacementPlugin)

}

})

}

Plugin apply 方法

包括任何其餘 plugin  apply 方法裏,寫的都是通用的,若是有 vue ,添加 vue 的 loader

exports.apply = api => {

api.hook('createWebpackChain', config => {

const rule = config.module.rule('vue').test(/.vue_$_/)

...

rule

.use('vue-loader')

.loader(require.resolve(vueLoaderPath))

.options(

Object.assign(

{

// _TODO:_ _error with thread-loader_

compiler: isVue3

? undefined

: api.localRequire('vue-template-compiler')

},

// _For Vue templates_

api.config.cache && getCacheOptions()

)

)

config.plugin('vue').use(require(vueLoaderPath).VueLoaderPlugin)

})

}

其餘 css, html, image, babel 都差很少,這些過程非常繁瑣,須要熟悉 webpack 的配置

總結一下 plugin

在 cli 執行的 args 的命令,在 apply 的時候更改了 webpack 的配置 ,when 是控制何時加入 apply

執行 plugin cli

this.extendCLI()

//這裏執行了 plugin 的 cli,傳入了 this

extendCLI() {

_for_ (const plugin of this.plugins) {

_if_ (plugin.resolve.cli) {

plugin.resolve.cli(this, plugin.options)

}

}

}

其實控制執行的是這句話

_await_ this.cli.runMatchedCommand()

找了半天這個方法,原來是 cac 裏面的方法,以前配置了 一個 false 的參數就不會被當即執行

爲何不當即執行,爲了加入幾個鉤子

_await_ this.hooks.invokePromise('beforeRun')

_await_ this.cli.runMatchedCommand()

_await_ this.hooks.invokePromise('afterRun')

執行 plugin apply

this.mergeConfig()

// _Call plugin.apply_

this.applyPlugins()

applyPlugins() {

let plugins = this.plugins.filter(plugin => {

_return_ !plugin.resolve.when || plugin.resolve.when(this)

})

// _Run plugin's `filterPlugins` method_

_for_ (const plugin of plugins) {

_if_ (plugin.resolve.filterPlugins) {

plugins = plugin.resolve.filterPlugins(this.plugins, plugin.options)

}

}

// _Run plugin's `apply` method_

_for_ (const plugin of plugins) {

_if_ (plugin.resolve.apply) {

logger.debug(`Apply plugin: `${chalk.bold(plugin.resolve.name)}``)

_if_ (plugin._resolve) {

logger.debug(`location: ${plugin._resolve}`)

}

plugin.resolve.apply(this, plugin.options)

}

}

}

先 merge config ,而後執行 apply 方法 ,apply 方法執行,只是加入了函數 hook ,真正的執行是這句

this.hooks.invoke('createWebpackChain', config, opts)

咱們回到 initCLI

this.command = cli

.command('[...entries]', 'Entry files to start bundling', {

ignoreOptionDefaultValue: true

})

.usage('[...entries] [options]')

.action(async () => {

logger.debug(`Using default handler`)

const chain = this.createWebpackChain()

const compiler = this.createWebpackCompiler(chain.toConfig())

_await_ this.runCompiler(compiler)

})

進入  createWebpackChain, 進入 utils/webpackChain, 使用 webpack-chain 建立了起初的 webpack 配置

createWebpackChain(opts) {

const WebpackChain = require('./utils/WebpackChain')

opts = Object.assign({ type: 'client', mode: this.mode }, opts)

//加入 poi 的配置 ,configureWebpack 有興趣能夠本身去追蹤下

const config = new WebpackChain({

configureWebpack: this.config.configureWebpack,

opts

})

// 加入本地配置

require('./webpack/webpack.config')(config, this)

// 配置好config,卻根據config,添加 webpack 相應的規則

this.hooks.invoke('createWebpackChain', config, opts)

_if_ (this.config.chainWebpack) {

this.config.chainWebpack(config, opts)

}

// 若是有 --inspect-webpack, 使用 open 打開配置,使用的默認 editor

_if_ (this.cli.options.inspectWebpack) {

const inspect = () => {

const id = Math.random()

.toString(36)

.substring(7)

const outFile = path.join(

os.tmpdir(),

`poi-inspect-webpack-config-${id}.js`

)

const configString = `// ${JSON.stringify(

opts

)}nvar config = ${config.toString()}nn`

fs.writeFileSync(outFile, configString, 'utf8')

require('@poi/dev-utils/open')(outFile, {

wait: false

})

}

config.plugin('inspect-webpack').use(

_class_ InspectWebpack {

apply(compiler) {

compiler.hooks.afterEnvironment.tap('inspect-webpack', inspect)

}

}

)

}

// 返回完整的 webpack 的 config,上面所的一切都是爲了配置 webpack 的 config

_return_ config

}

const chain = this.createWebpackChain()

// 根據 config 去編譯,生成編譯後的文件

const compiler = this.createWebpackCompiler(chain.toConfig())

// 打包編譯結果

_await_ this.runCompiler(compiler)

以上最基本的服務和編譯打包跑通了

儘管在文檔裏對於 cli 的操做不多,可是實現的卻有不少

createConfigFromCLIOptions() {

const {

minimize,

sourceMap,

format,

moduleName,

outDir,

publicUrl,

target,

clean,

parallel,

cache,

jsx,

extractCss,

hot,

host,

port,

open,

proxy,

fileNames,

html,

publicFolder,

babelrc,

babelConfigFile,

reactRefresh

} = this.cli.options

}

比方說這裏 你能夠

--cwd

--debug

--port

--proxy

--require

--hot

太多太多,可是用的不多,文檔上都沒提,有些功能寫了,用的機會不多,值得反思一下,一開始開始項目的時候,是否是能夠不用考慮這些,先實現最核心的功能,後期在慢慢的維護。

總結

這個項目一開始搭建了幾個月,後來就沒動靜了。

做爲提高技術和積累經驗,學習搭建方法,仍是頗有意義的。

若是這個項目像 umi 這樣的,若是自動化router ,是否是能夠更好?

沒有提供額外的功能,感受一開始就須要作好產品。

相關文章
相關標籤/搜索