POI 源码阅读

移步飞书

时间花费

时间一天两个小时左右,读文档用了两个多小时,一共花费五天 10 个小时的时间,基本把大概的逻辑(主线)理解清楚了。

写文档两个小时。

读 poi 整体感受比 sao 略微的费劲一些,需要熟悉 webpack 的操作配置。

POI 是什么?

就是把webpack 封装了一层的工具,可以本地启服务和打包。

省去自己的一大堆配置。

这里我要吐槽一下,如果我用 react 和 vue 的官方的那个脚手架,不也很好?还自带 router

如果我真的想改什么不也得看 poi 的文档,不也很麻烦吗?

从整个大方向上,用的用户不是很多。不过好在可以给自己造就经验和提升技术,从这个意义上说还是有价值的。

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 干的事啦,只要初始化的时候选择相应的配置就可以。

不用自己配置啦。

|

image

除此之外还拥有一些代理功能就是 webpack 提供的啦。

你可以把自己想写的配置写到 poi.config.js 这样合并到默认的 webpack.config.js 形成新的配置

POI 是怎么实现的?

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

里面使用了很常见的 lerna 实现多包管理,不过最主要的也就两个包啦

|

image

一个是 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'sfilterPluginsmethod*

*for* (const plugin of plugins) {

*if* (plugin.resolve.filterPlugins) {

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

}

}

// *Run plugin'sapplymethod*

*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()}\n\n``

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 ,是不是可以更好?

没有提供额外的功能,感觉一开始就需要做好产品。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270