时间花费
时间一天两个小时左右,读文档用了两个多小时,一共花费五天 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
filterPluginsmethod*
*for* (const plugin of plugins) {
*if* (plugin.resolve.filterPlugins) {
plugins = plugin.resolve.filterPlugins(this.plugins, plugin.options)
}
}
// *Run plugin's
applymethod*
*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 ,是不是可以更好?
没有提供额外的功能,感觉一开始就需要做好产品。