概述本文主要内容是阅读之前使用的prerender-spa-plugin插件的源码,看看我们应该怎么写一个webpack插件.还要了解预渲染插件是如何实现的。这个内容其实已经涉及到了prerender-spa-plugin的使用。本章内容是对上一篇文章的补充和扩展。将详细介绍Webpack插件机制是如何工作的。前面写的很简单。替换插件生效的原理是什么。如果你还没有看过之前关于如何使用prerender-spa-plugin插件预渲染页面的文章,你可以先去看一下,了解一下这个插件是干什么的,我们的插件长啥样喜欢。插件源码分析prerender-spa-plugin是开源的,源码可以在GitHub上看到。有兴趣的可以自己点击查看。首先我们简单回顾一下这个插件是如何使用的,这有助于我们了解它的内部结构。让我们使用其官方文档中提供的示例。constpath=require('path')constPrerenderSPAPlugin=require('prerender-spa-plugin')module.exports={plugins:[...newPrerenderSPAPlugin({//必需-webpack输出应用程序的路径prerender.staticDir:path.join(__dirname,'dist'),//必需-到render.routes的路由:['/','/about','/some/deep/nested/route'],})]}从上面的例子我们可以知道这个插件需要初始化一个实例,然后传入相应的参数比如输出路径staticDir,要渲染的路由等等。接下来我们简单介绍一下他的源码结构.具体代码块如下:.hooks){constplugin={name:'PrerenderSPAPlugin'}compiler.hooks.afterEmit.tapAsync(plugin,afterEmit)}else{compiler.plugin('after-emit',afterEmit)}}整个prerender-spa-pluginplugin是由2部分组成:function函数,主要用来初始化数据的获取和处理。在使用这个插件的过程中,我们需要先对其进行初始化。该函数可用于一些数据处理和解析。prototype上的apply函数,作为一个钩子函数,主要用来处理Webpack触发插件执行后的相关逻辑。下面我们就基于prerender-spa-plugin插件来看一下各个部分。初始化函数function首先让我们看一下初始化函数function。该函数主要是在获取到初始化参数后进行一些处理。具体代码如下:functionPrerenderSPAPlugin(...args){constrendererOptions={}//主要是为了向后兼容。this._options={}//普通参数对象。如果(args.length===1){this._options=args[0]||{}//向后兼容v2}else{console.warn("[prerender-spa-plugin]您似乎正在使用v2基于参数的配置选项。建议您迁移到更清晰的基于对象的配置系统.\n查看文档以获取更多信息。”)elseif(typeofarg==='object')this._options=arg})staticDir?this._options.staticDir=staticDir:空路由?this._options.routes=routes:null}//向后兼容v2。如果(this._options.captureAfterDocumentEvent){console.warn('[prerender-spa-plugin]captureAfterDocumentEvent已重命名为renderAfterDocumentEvent,应移至渲染器选项。')rendererOptions.renderAfterDocumentEvent=this._options.captureAfterDocumentEvent}if(this._options.captureAfterElementExists){console.warn('[prerender-spa-plugin]captureAfterElementExistshas已重命名为renderAfterElementExists,应移至渲染器选项。')rendererOptions.renderAfterElementExists=this._options.captureAfterElementExists}if(this._options.captureAfterTime){console.warn('[prerender-spa-plugin]captureAfterTime已重命名到renderAfterTime并且应该移动到渲染器选项。')rendererOptions.renderAfterTime=this._options.captureAfterTime}this._options.server=this._options.server||{}this._options.renderer=this._options.renderer||新的PuppeteerRenderer(Object.assign({},{headless:true},rendererOptions))if(this._options.postProcessHtml){console.warn('[prerender-spa-plugin]postProcessHtmlshouldbemigratedtopostProcess!查阅文档获取更多信息。')}}因为我们的插件被实例化并添加(即使用的新运算符实例化后),所以function函数的入参主要是给这个对象绑定一些需要的参数,这样实例化之后,可以得到SDK或者插件相关的工具,有很多相关的参数,因为它可以接受多种类型和输入参数的长度不同,所以一开始会判断参数类型,确定传入的是哪种类型的参数。从代码来看,当前记录的参数包括输出参数staticDir和需要渲染的路由。如果你自己定义渲染器函数,那么它也会被绑定和存储。同时,这个V3版本的代码也向前兼容了V2版本。Hookapply函数说完了初始化函数,我们来看最重要的apply函数。具体代码如下:PrerenderSPAPlugin.prototype.apply=function(compiler){constcompilerFS=compiler.outputFileSystem//来自https://github.com/ahmadnassri/mkdirp-promise/blob/master/lib/index.jsconstmkdirp=function(dir,opts){returnnewPromise((resolve,reject)=>{compilerFS.mkdirp(dir,opts,(err,made)=>err===null?resolve(made):reject(err))})}constafterEmit=(compilation,done)=>{...}if(compiler.hooks){constplugin={name:'PrerenderSPAPlugin'}compiler.hooks.afterEmit.tapAsync(plugin,afterEmit)}}else{compiler.plugin('after-emit',afterEmit)}}在讲apply函数之前,我们先看看参数compiler对象和apply函数接收到的mkdirp方法,以及生命周期绑定代码.complier对象的整个apply方法只接收一个complier对象作为参数。具体可以参考webpack中complier对象的描述。具体的源代码可以在这里找到。下面简单介绍一下:complier对象是webpack提供的一个全局对象,挂载了插件生命周期中会用到的一些函数和属性,比如options、loader、plugin等,我们可以使用这个对象在构建的时候获取webpack相关的数据。mkdirp方法该方法是将执行mkdir-p方法的函数转换为Promise对象。详情见代码上方的原文注释。因为比较简单,这里就不过多介绍了。生命周期绑定在最后。钩子函数生命周期完成后,需要关联最新的生命周期。该插件与afterEmit节点关联。如果想查看整个webpack相关构建过程的生命周期,可以参考这篇文档。看完了简单的部分,我们来看最重要的钩子函数。钩子函数接下来我们看一下这个插件中最核心的钩子函数。本插件关联的语句循环为节点afterEmit。接下来我们看一下具体的代码。constafterEmit=(compilation,done)=>{constPrerendererInstance=newPrerenderer(this._options);PrerendererInstance.initialize().then(()=>{returnPrerendererInstance.renderRoutes(this._options.routes||[]);})//向后兼容v2(postprocessHTML应该迁移到postProcess).then((renderedRoutes)=>this._options.postProcessHtml?renderedRoutes.map((renderedRoute)=>{constprocessed=this._options.postProcessHtml(renderedRoute);if(typeofprocessed==="string")renderedRoute.html=processed;elserenderedRoute=processed;returnrenderedRoute;}):renderedRoutes)//运行postProcess挂钩。.then((renderedRoutes)=>this._options.postProcess?Promise.all(renderedRoutes.map((renderedRoute)=>this._options.postProcess(renderedRoute))):renderedRoutes)//检查以确保postProcess挂钩正确返回renderedRoute对象。.then((renderedRoutes)=>{constisValid=renderedRoutes.every((r)=>typeofr==="object");if(!isValid){thrownewError("[prerender-spa-plugin]渲染routes是空的,你是不是忘了在postProcess中返回`context`对象?");}returnrenderedRoutes;})//如果在config中指定,则缩小html文件。.then((renderedRoutes)=>{if(!this._options.minify)returnrenderedRoutes;renderedRoutes.forEach((route)=>{route.html=minify(route.html,this._options.minify);});returnrenderedRoutes;})//如果尚未设置,则计算outputPath。.then((renderedRoutes)=>{renderedRoutes.forEach((rendered)=>{if(!rendered.outputPath){rendered.outputPath=path.join(t他的._options.outputDir||this._options.staticDir,rendered.route,"index.html");}});返回渲染路由;})//创建目录并写入预渲染文件。.then((processedRoutes)=>{constpromises=Promise.all(processedRoutes.map((processedRoute)=>{returnmkdirp(path.dirname(processedRoute.outputPath)).then(()=>{returnnewPromise((resolve,reject)=>{compilerFS.writeFile(processedRoute.outputPath,processedRoute.html.trim(),(err)=>{if(err)reject(`[prerender-spa-plugin]无法将渲染路径写入文件"${processedRoute.outputPath}"\n${err}.`);elseresolve();});});}).catch((err)=>{if(typeoferr==="string"){err=`[prerender-spa-plugin]无法为路由${创建目录${path.dirname(processedRoute.outputPath)}processedRoute.route}.\n${err}`;}throwerr;});}));回报承诺;}).then((r)=>{PrerendererInstance.destroy();done();}).catch((err)=>{PrerendererInstance.destroy();constmsg="[prerender-spa-plugin]无法预渲染所有路由!”;console.error(msg);compilation.errors.push(newError(msg));done();});};在这个方法中,出现了一个新的编译对象。这个方法的详细介绍可以参考Webpack编译对象,具体源码可以在这里找到。简单介绍一下:这个对象代表了一个文件资源的构建。每次文件更改时,都会创建一个新对象。该文件主要包含当前资源构建和变更过程中的一些属性和信息。另外一个done参数代表当前插件执行完后执行下一步的触发器,和我们常见的Node框架中的next()是一样的。接下来简单说一下这个函数执行的逻辑:初始化一个Prerenderer的实例。本实例是一个页面预渲染的工具,具体代码可以在GitHub上找到。实例初始化后,会对每条路由进行预渲染操作。根据获取的预渲染相关数据进行有效性校验。如果指定了压缩,则将相关压缩应用于预渲染数据。最后将预渲染相关数据输出到指定路径。销毁Prerenderer实例。这是一个插件执行的完整过程。小结通过prerender-spa-plugin这个插件,你应该可以了解我们现在这个插件的工作原理了。我们需要编写一个插件的核心组件:一个初始化函数。原型链上的apply方法。-一个钩子函数。-绑定生命周期的代码。有了这些,我们的一个Webpack插件就完成了。希望通过一个插件源码的例子,让大家了解我们日常使用的看似复杂的Webpack插件是如何实现的。附录Webpack官方:如何写一个插件WebpackComplierhookWebpackCompilationobjectWebpackhook
