2. mu-webpack loader & plugin

496 阅读4分钟

自定义loader

  • loader就是一个函数
  • 获取参数的方式
    • this.query
    • const options = loaderUtils.getOptions(this);
/*
这里不能写成箭头函数,因为在函数里要用到this指向,
webpack在调用loader的时候会把this做一些变更
变更之后才能够使用this里的一些方法
用箭头函数 this指向会有问题
*/

// source参数指引入文件的源代码,拿到内容之后可以对内容做一些变更再返回
module.exports = function(source){
    return source.replace("hello", "hello world");
}

// webpack配置中使用自定义loader
module:{
    rules: [
        test: /\.js/,
        use:[path.resolve(__dirname, './loaders/replaceLoader.js')]
    ]
}

// webpack配置中使用自定义loader
module:{
    rules: [
        test: /\.js/,
        use:[{
            loader: path.resolve(__dirname, './loaders/replaceLoader.js'),
            options: {
                name: 'haha'
            }
        }]
}
// 自定义loader可以通过this(this.query)接收到传递过来的options里面的参数
// this.query  ==>  {name:"haha"}

module.exports = function(source){
    return source.replace("hello", this.query.name);
} // 就实现了将代码中的‘hello’替换成‘haha’

可以在自定义loader中通过loader-utils插件来分析和获取options传递的参数
npm i loader-utils --save-dev

const loaderUtils = require('loader-utils');
module.exports = {
    const options = loaderUtils.getOptions(this);
    return source.replace('hello', options.name)
}

  • 如果在自定义loader里需要写异步逻辑:
    • replaceLoader.js就是之前同步将“hello” 替换成“hello world”的loader
    • replaceLoaderAsync.js就是异步将‘hello’替换成‘haha’的loader
const loaderUtils = require('loader-utils');
module.exports = {
    const options = loaderUtils.getOptions(this);
    const callback = this.async();
    setTimeout(()=>{
        const result = source.replace('hello', options.name);   
        callback(null,result); //实际调用的是this.callback
    },1000)
}

  • 想实现的是先将‘hello’替换成‘haha’,再将‘haha’替换成‘hello world’
// 多个loader先后引入即可
module:{
    rules: [
        test: /\.js$/,
        use:[{
            loader: path.resolve(__dirname, './loaders/replaceLoader.js'),
        },
        {   
            loader: path.resolve(__dirname, './loaders/replaceLoaderAsync.js'),
            options: {
                name: 'haha'
            }
        }
    ]
}

  • 如果对于自定义loader引入时不想每个都写path.resolve(__dirname,'xxx'),借助webpack配置参数:resolveLoader
  • resolveLoader
// 打包时先去node_modules去找loader,如果没找到,去./loaders里去找
resolveLoader:{
    modules: ['node_modules', './loaders']
},
module:{
    rules:[
        test: /\.js$/,
        use: [
            {loader: 'replaceLoader'},
            {loader: 'replaceLoaderAsync', options:{name:'haha'}}
        ]
    ]
}

自定义loader帮助实现的一些功能举例:

  • 异常捕获
    • 写jquery代码的时候要对前端异常代码进行监控,需要对代码进行异常捕获,需要对jquery底层源码进行修改,里面加一些try/catch这样的代码,同时对业务代码外层也要加try/catch来捕获代码异常,及时报到线上进行预警,这个时候异常代码就要侵入业务代码之中,会让业务代码看上去很乱;通过webpack,编写自定义loader就能实现这个功能,而不需要掺在业务代码中
    • 通过抽象语法树对源代码进行分析,当发现有function(){}就替换成try(function(){})catch(e)这样的语法
  • 国际化
    • 网站要出中文版/英文版,通过占位符,加上自定义loader来实现,通过全局变量来识别要打包的是中文版还是英文版
    • if(全局变量===“中文版”) source.replace('{{title}}','中文标题')
    • source.replace('{{title}}','english title')
  • 当发现源代码需要一些包装,就可以考虑使用loader来实现

自定义plugin

  • plugin是一个类
  • plugin的核心机制是事件驱动(发布订阅)设计模式,在这个模式里,代码运行是通过事件驱动的

举例:当打包结束,在dist目录下生成一个版权文件,里面写一些版权信息

// 创建plugins文件夹,创建copyright-webpack-plugin.js
//plugin的结构定义
class CopyrightWebpackPlugin {
    constructor(options){
        //通过options接收传递过来的参数
        console.log('插件被使用了')
    }
    //compiler可以理解为是webpack的一个实例
    apply(compiler){}
}

module.exports = CopyrightWebpackPlugin;

//使用
const CopyrightWebpackPlugin = require('./plugins/copyright-webpack-plugin.js');

plugins: [
    new CopyrightWebpackPlugin({name:"zhuzhu"})
]

class CopyrightWebpackPlugin {
    constructor(){
        console.log('插件被使用了')
    }
    //compiler可以理解为是webpack的一个实例
    apply(compiler){
        //compiler里存放了配置的所有内容,包括打包的所有相关内容
        //compilation存放的只是跟这次打包相关的内容
        //emit是一个异步的时刻值(异步钩子),异步  后面可以写一个方法叫做tapAsync
        compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin',(compilation,cb) => {             //打包之后生成哪些内容是在compilation.assets里面
            console.log(compilation.asstes);
            //所以增加文件可以往assets里面添加
            compilation.assets['copyright.txt'] = {
                //文件内容
                source: function(){
                    return 'copyright by zhuzhu';
                },
                //文件大小
                size: function(){
                    return 20
                }
            }
            //用tapAsync方法要在最后执行一下cb()
            cb();            
        })
    }
}

对于同步的时刻写法:例
compiler.hooks.compile.tap('CopyrightWebpackPlugin',(compilation) => {})

Library打包

  • libraryTarget:"umd"
  • library:"library"
libraryTarget:"umd"
引入方式
import library from 'library';
const library = require("library");
require(['library'],function(){});

想通过script标签引入这个库,并通过library这个全局变量来使用
<script src="library.js"></script>
如:library.math进行使用
-----通过配置: library:"library"
/*
src
    -index.js
    -math.js
    -string.js
*/

const path = require("path");
module.exports = {
    mode:"production",
    entry: "./src/index.js",
    output:{
        path: path.resolve(__dirname, "dist"),
        filename:"library.js",
        //通过各种方式都能正确引用这个库
        libraryTarget:"umd",
        //支持script标签引入,并添加全局变量
        library:"library"
    }
}


libraryTarget:"this",
library:"library"
// 这样配置会在this上挂载library


// 如果库library本身引入了lodash,使用这个库的业务代码也是用了lodash,
// 打包会导致lodash会存在两份
// 在library打包时配置:
externals:['lodash']
// 可以在库打包时忽略lodash
// 但是在使用这个库的时候不仅要引入库本身还要引入lodash

PWA

  • pwa:如果第一次访问某个网站成功了,突然服务器意外挂了,再次访问这个网站时,它可以在本地有一份缓存,可以利用缓存将之前的页面展示出来
npm i http-server --save-dev
增加script
"start":"http-server dist"

只有要上线的代码需要pwa的处理
这个第三方插件可以实现这个pwa的功能:
npm i workbox-webpack-plugin --save-dev

const WorkboxPlugin = require('workbox-webpack-plugin');
plugins:[
    new WorkboxPlugin.GenerateSW({
        clientsClaim:true,
        skipWaiting:true
    })
]

在入口文件index.js中:
if('serviceWorker' in navigator) {
    window.addEventListener('load', ()=>{
        navigator.serviceWorker.register('/service-worker.js')
            .then(registration => {
                console.log('service-worker registed')
            })
            .catch(error => {
                console.log('service-worker register error')
            })
    })
}