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 ,是否是能夠更好?
沒有提供額外的功能,感受一開始就須要作好產品。