首页 > JavaScript > 文章正文

POI 源码阅读

更新时间:2020-10-25

更好的观看体验移步飞书

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

时间花费

时间一天两个小时左右,读文档用了两个多小时,一共花费五天 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 干的事啦,只要初始化的时候选择相应的配置就可以。

不用自己配置啦。

|

除此之外还拥有一些代理功能就是 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 ,是不是可以更好?

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

相关文章
相关标签