W-L(theory)

176 阅读6分钟

编写一个loader

创建loaders/replaceLoader.js(实现当遇见‘hello’字符串,就改成‘hello world’)

loader就是一个函数

这里不能写成箭头函数,因为在函数里要用到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可以通过thisthis.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里需要写异步逻辑:

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)
}

现在在loaders文件夹里面创建两个文件:replaceLoader.js 和replaceLoaderAsync.js

replaceLoader.js就是之前同步将“hello” 替换成“hello world”的loader

replaceLoaderAsync.js就是异步将‘hello’替换成‘haha’的loader

想实现的是先将‘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')

正常情况下会去node_modules里去找配置的loader

借助webpack配置参数: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就能实现这个功能,而不需要掺在业务代码中

在自定义loader中:

通过抽象语法树对源代码进行分析,当发现有function(){}就替换成try(function(){})catch(e)这样的语法

再例如国际化

网站要出中文版/英文版,通过占位符,加上自定义loader来实现,通过全局变量来识别要打包的是中文版还是英文版

if(全局变量===“中文版”) source.replace('{{title}}','中文标题')

source.replace('{{title}}','english title')

总结:当发现源代码需要一些包装,就可以考虑使用loader来实现

如何编写一个plugin

loader帮助处理模块(文件),是一个函数

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) => {})

利用node调试工具在浏览器端查看compilation里具体有哪些内容

"script":{
    "build": "node node_modules/webpack/bin/webpack.js",
    "build": "webpack",
    "debug": "node --inspect --inspect-brk node_modules/webpack/bin/webpack.js",
}
compiler.hooks.compile.tap('CopyrightWebpackPlugin',(compilation) => {
+    debugger;
})

Bundler源码编写(模块分析)[对入口文件的分析]

对一个项目进行打包,首先就是读取项目入口文件,然后分析入口文件里的代码

创建bundler.js
1要分析文件,首先要拿到文件里的内容;
2分析一个模块要准备比较多的内容,首先要拿到它的依赖,
借助babel(npm i @babel/parser 帮助分析源代码)
3.对代码进行编译,让浏览器可识别(npm i @babel/core @babel/preset-env --save)
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const babel = require('@babel/core');
const moduleAnalyser = (filename) => {
    //获取文件内容
    const content = fs.readFileSync(filename, 'utf-8');
    console.log(content);
    //分析文件内容
    const ast= parser.parse(content, {sourceType:"module"});//得到的是抽象语法树(AST)
    console.log(ast.program.body);//抽象语法树将js代码转化成了js对象
    //拿到代码里所有的依赖关系:可以遍历body,找到body里面的type为ImportDeclaration的内容
    //npm i @babel/traverse --save

    /* 
    const dependencies = [];
    traverse(ast, {        ImportDeclaration({node}) {
            console.log(node);
            dependencies.push(node.source.value);

        }
    }); 
    //得到依赖的文件的文件路径数组(文件路径是相对于入口文件的相对路径)
    console.log(dependencies);
    //文件路径需要是绝对路径或者相对于bundle.js的路径,所以不是直接push node.source.value
*/    const dependencies = {};
    traverse(ast, {
        ImportDeclaration({node}) {
            const dirname = path.dirname(filename);//'./src'
            const newFile = path.join(dirname,node.source.value)
            dependencies[node.source.value] = newFile;

        }
    });
    //对文件进行编译
    const {code} = babel.transformFromAst(ast, null, {
        presets: ["@babel/preset-env"]
    })
    return {filename, dependencies, code}
}
const moduleInfo = moduleAnalyser('./src/index.js');
console.log(moduleInfo);

【npm i cli-highlight -g     运行node bundler.js | highlight  在终端打印出来的信息会高亮区分】

总结:实现对一个模块进行代码分析,当把一个文件传递给moduleAnalyser函数之后,会分析出这个文件的依赖,编译源代码

Bundler源码编写(Dependencies Graph)

分析出所有模块之间的依赖关系,并存储在一个地方

//依赖图谱:存放所有模块依赖关系
const makeDependenciesGraph = (entry) => {
    //对入口文件做分析
    const entryModule = moduleAnalyser(entry);
    //还需要对依赖的文件进行分析(通过队列的方式来实现类似递归的效果)
    const graphArray = [entryModule];
    for(let i = 0; i < graphArray.length; i ++){
        const item = graphArray[i];
        const {dependencies} = item;
        if(dependencies){
            for(let j in dependencies){
                graphArray.push(
                    moduleAnalyser(dependencies[j])
                )
            }
        }
    }
    //循环结束,graphArray所有模块文件的依赖图谱数组,转换成对象    const graph = {};
    graphArray.forEach(item => {
        graph[item.filename] = {
            dependencies: item.dependencies,
            code: item.code
        }
    });
    return graph;
}

const graphInfo = makeDependenciesGraph('./src/index.js');
console.log(graphInfo);

Bundler源码编写(生成代码)

const generateCode = (entry) => {
    const graph = JSON.stringify(makeDependenciesGraph(entry));
    //return出的结果是一个字符串(一段代码)
    //首先网页上的代码应该放在一个闭包里 防止污染全局环境
    图谱里每个文件对应的code里会存在require和exports,如果直接执行,浏览器识别不了
    //在闭包里面构造require函数和exports对象
    //对code里的相对路径进行转换,才能在图谱里找到对应的code代码
    传递一个localrequire函数,eval再执行的时候调用的是传入的require(是一个相对路径转换的函数)
    return `
        (function(graph){
            function require(module) {
                function localRequire(relativePath){
                    return require(graph[module].dependencies[relativePath])
                }                var exports = {};
                (function(require,exports,code){
                    eval(code)
                })(localRequire,exports,graph[module].code);
                return exports;
            }
            require('${entry}')
        })(${graph})
    `

}

const code = generateCode('./src/index.js');