没有发布过npm包的同学,可能会对NPM对开发有一种蜜汁敬畏,觉得这是一个很高大上的东西。其实在了解了之后发现,npm包就是一个我们平时经常写的一个export出来的模块而已,只不过跟其它业务代码耦合性低,具有较高的独立性。然后在 Webpack初体验 的时候,在使用html-webpack-plugin插件时刚好有个想法,能不能把引用的相关 assets 文件(如 css, js)以inline的方式引入进文件里。所以就决定尝试一下如何编写一个webpack插件,并且发布在NPM上,也就有了这个 assets-inline-plugin。
如何写一个webpack插件
首先我们要先去了解两个对象,compiler和compilation:
compiler对象包涵了Webpack环境所有的的配置信息,这个对象在Webpack启动时候被构建,并配置上所有的设置选项包括 options,loaders,plugins。当启用一个插件到Webpack环境的时候,这个插件就会接受一个指向compiler的参数。运用这个参数来获取到Webpack环境。更多详细可看 源代码。
compilation代表了一个单一构建版本的物料。在webpack中间件运行时,每当一个文件发生改变时就会产生一个新的compilation从而产生一个新的变异后的物料集合。compilation列出了很多关于当前模块资源的信息,编译后的资源信息,改动过的文件,以及监听过的依赖。compilation也提供了插件需要自定义功能的回调点。更多详细可看 源代码。
插件基本结构
官方教程基本结构:
function HelloWorldPlugin(options) { // Setup the plugin instance with options... } HelloWorldPlugin.prototype.apply = function(compiler) { compiler.plugin('done', function() { console.log('Hello World!'); }); }; module.exports = HelloWorldPlugin;
options是你在webpack配置文件里给插件的配置项参数。Plugins是可以用自身原型方法apply来实例化的对象,apply这个函数是提供给webpack运行时调用的,webpack会在这里注入compiler对象。compiler有很多生命周期钩子函数,除了done还有emit,run,afterEmit等等,我们可以在具体某个生命周期里去做相应的操作。
assets属性是compilation里最重要的部分,因为如果你想借助webpack帮你生成文件,你需要像官方教程里那样在assets上写上对应的文件信息。
或者你可以不用函数的方式,用定义一个类的方式也行,比如我这个插件就是这样的结构:
class HelloWorldPlugin{ constructor(options){ } apply(compiler){ compiler.plugin('compilation',compilation => { }); } } module.exports = HelloWorldPlugin;
assets-inline-plugin
html-webpack-plugin是会把打包的资源js文件以script标签的形式自动引入模板页里,现在我们要换成inline的形式去引入的话,其实最核心的操作就是在html-webpack-plugin插件调用模板去插入资源文件之前更改资源。
参数判断
因为这个插件比较简单,所以只设定有两个参数inline和remove。inline是需要包含的资源形式配置项,是需要inline js还是css资源文件,取值可以是boolean or regexp or object,默认是为true,格式可以如下:
inline: true
inline: /\.(css|js)$/
inline: { css: /\.css$/, js: /\.js$/ }
inline: { css: true, js: true }
remove表示是否需要清除inline进来了的文件,默认为true。
首先我们需要判断配置的参数,代码如下:
class HtmlInlinePlugin { constructor(options) { isObject(options) || (options = {}); if (!isObject(options.inline)) { if (options.hasOwnProperty('inline')) { let inline = isRegExp(options.inline) ? options.inline : !!options.inline; options.inline = { js: inline, css: inline }; } else options.inline = { js: true, css: true }; } else { // 取反操作“!”会得到与目标对象代表的布尔型值相反的布尔值,而再做一次取反“!!”就得到了与其相同的布尔值。 options.inline.js = isRegExp(options.inline.js) ? options.inline.js : !!options.inline.js; options.inline.css = isRegExp(options.inline.css) ? options.inline.css : !!options.inline.css; } this.outDir = ''; this.inlineAsserts = []; if(options.remove){ this.options = Object.assign({}, options); }else{ this.options = Object.assign({ remove: true }, options); } } apply(compiler) { ··· } function isObject(obj) { return Object.prototype.toString.call(obj) === '[object Object]'; } function isRegExp(obj) { return Object.prototype.toString.call(obj) === '[object RegExp]'; }
修改打包资源
对文件的更改操作在apply这个方法里去执行,主要是通过compilation这个对象来操作。在调用模板之前更改资源,我们借助了html-webpack-plugin的插件事件 html-webpack-alter-asset-plugin。
html-webpack-plugin允许其他插件来改变html文件内容。具体的事件如下:
Async(异步事件):
* html-webpack-plugin-before-html-generation
* html-webpack-plugin-before-html-processing
* html-webpack-plugin-alter-asset-tags
* html-webpack-plugin-after-html-processing
* html-webpack-plugin-after-emit
Sync(同步事件):
* html-webpack-plugin-alter-chunks
这个插件事件包含的参数htmlPluginData有相应的插件数据,数据里的head是一个数组,我们通过遍历这个数组来在文件头部插入css资源:
pluginData.head = pluginData.head.map(tag => { let assetUrl = tag.attributes.href; if (this.testAssertName(assetUrl, this.options.inline.css)) { tag = { tagName: 'style', closeTag: true ,attributes: {type: 'text/css'}}; this.updateTag(tag, assetUrl, compilation); } return tag; });
同理,我们循环插件数据里的body数组,来在文件主体插入js资源:
pluginData.body = pluginData.body.map(tag => { let assetUrl = tag.attributes.src; if (this.testAssertName(assetUrl, this.options.inline.js)) { tag = { tagName: 'script', closeTag: true ,attributes: {type: 'text/javascript'}}; this.updateTag(tag, assetUrl, compilation); } return tag; });
插入这些资源文件后,我们需要把这些更新进compilation里。assetUrl分别是获取到css的href和js的src路径:
updateTag(tag, assetUrl, compilation) { let publicUrlPrefix = compilation.outputOptions.publicPath || ''; //path.posix.relative(from, to) 方法返回从 from 到 to 的相对路径(基于当前工作目录) //在 POSIX 上:path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb'); // 返回: '../../impl/bbb' let assetName = path.posix.relative(publicUrlPrefix, assetUrl); let asset = compilation.assets[assetName]; let source = asset.source(); if (typeof source !== 'string') { source = source.toString(); } // remove sourcemap comments tag.innerHTML = sourceMappingURL.removeFrom(source); // mark inlined asserts which will be deleted lately this.inlineAsserts.push(assetName); return tag; }
最后,我们把内联进来的资源原文件给删除,
removeInlineAsserts() { if (!this.outDir) return; this.inlineAsserts.forEach(file => { let filePath = path.join(this.outDir, file); //如果路径存在 if (fs.existsSync(filePath)) { //删除文件 fs.unlinkSync(filePath); } rmdirSync(this.outDir, file); }) }
该函数是路径下一层层循环判断文件是否存在:
function rmdirSync(outDir, file) { //path.sep平台特定的路径片段分隔符,Windows 上是 \ file = path.join(file).split(path.sep); file.pop(); if (!file.length) return; file = file.join(path.sep); let dirPath = path.join(outDir, file); if (fs.existsSync(dirPath)) { //同步读取目录,返回一个所包含的文件和子目录的数组 let files = fs.readdirSync(dirPath); if (!files.length) { fs.rmdirSync(dirPath); } } rmdirSync(outDir, file); }
创建和发布NPM包
首先去 NPM 注册一个账户。如果是第一次发布包,执行npm adduser
,然后输入前面注册好的NPM账号,密码和邮箱,将提示创建成功。如果不是第一次发布包,执行npm login
进行登录,同样输入NPM账号,密码和邮箱。(npm adduser成功的时候默认你已经登陆了,所以不需要再进行npm login了)
接着进入项目文件夹下,然后输入npm publish
进行发布。
当终端显示如下面的信息时,就代表版本号为1.0.0的包发布成功啦!前往NPM官网就可以查到你的包了。
+ sugars_demo@1.0.0
但要注意的是,每次更新时,必须修改版本号后才能更新,比如将1.0.0修改为1.0.1后就能进行更新发布了。 这里的包版本号有一套规则,采用的是semver(语义化版本),通俗点意思就是版本号:大改.中改.小改。
Tips
有时,我们会看到一些npm包有很漂亮的版本号图标:
这些图标,其实可以在https://shields.io/ 上去获取图标地址,也可以自定义制作,拉到网站最下面:
输入相关信息后就会生成图标了:
然后我们还可以生成有关这个npm包的信息图标,就像这样的:
这个图标我们也可以在上自定义制作,只需要输入你的npm包名,然后选择喜欢的样式就行啦:
插件完整的代码可以看 这里。