关于webpack

webpack 其实就是一个打包工具, 他可以把css, js, 图片等等的东西都打包成一个bundle,从entry开始递归分析他的依赖图,把应用到的每一个模块打包成一个或多个bundle

webpakc 主要依赖下面几个配置

  • entry: 主入口文件
  • output: 输入文件的位置
  • modules: 里面配置的是loader, 我们可以想象loadder 为一名翻译官,把各种类型文件都翻译成浏览器可以识别的东西
  • plugins: 插件,我觉得webpack 的强大之处在于他的插件,plugin 可以针对在webpack不同的时期做不同的工作,比如CleanWebpackPlugin可以在打包之前删除清理指定目录

webpack 基础配置

const path = require('path');

export default {
    entry: './src/index.js', // 入口文件
    output: {
        filename: '[name].js',
        path: path.resolve(__dirname, 'dist'),
    },
    resolve: {
        modules: ['node_modules'], // 告诉 webpack 解析模块时应该搜索的目录。
        // 配置别名
        alias: {
            '@': path.resolve(__dirname, 'src'), // 指定src的别名为 ‘@’
        },
        ententions: ['.js', '.css'], // 添加文件猴嘴
    },
    // 定义开发环境下的webpack-dev-server 其实就是动态更新
    // 此时没有加载 HotModuleReplacementPlugin 的时候是通过loaction.reload()重新加载网页的,但有个缺点就是不能记录状态
    devServer:{
        contentBase: path.resolve(__dirname, 'dist'),
        open: true,
        port: 8000,
        hot: true
    },
    treeShaking: true, // 这里表示将没用过的代码自动删除掉
    optimization:{
        splitChunks: {
            cacheGroups: {
                vendor: {
                    test: /node_modules/,
                    priority: 1,  // 数字越大,优先级越高
                    minChunks: 2, // 表示至少有两个js同事引用的时候,就会打包成vendor。js
                    minSizes: 0, // 表示最小的大小
                }
            }
        },
    },
    modules: {
        noParse: /jquery/,  // webpack 优化, 不去递归jquery的依赖库
        rules: [
           {
                test: /\.css$/,
                use: [
                    // 请记住loader 的运行顺序是从下到上,从右到左,
                    // 另一种模式是内敛模式, import Styles from 'style-loader!css-loader?module!./styles.css', 忠中模式通过 ! 分割loadder,
                    // 'style-loader!css-loader?module!./styles.css'.split("!") => ["style-loader", "css-loader"]
                    { loader: 'style-loader' },
                    {
                        loader: 'css-loader',
                        options: {
                        modules: true
                        }
                    }
                ]
            },
            {
                test: /\.js$/,
                exclude: /(node_modules|bower_components)/,
                // 如果使用happypack的话,多线程打包,此时下面就要修改该成
                // use: 'Happypack/loader?id=js'
                use: { 
                    loader: 'babel-loader',
                    option: {
                        cacheDirectory: true,  // 开启js 打包优化
                        presets: ['@babel/preset-env', '@babel/preset-react'],
                        plugins: [require('@babel/plugin-transform-object-rest-spread')]
                    },

                }

            }
        ]
    },
    mode: 'development',  // 指定环境,
    plugins: [
        // 编译的时候指定全局变量,我们可以根据这个去定义当前环境是开发环境还是线上环境,定义不通的行为,比如url
        new webpack.DefinePlugin({
            PRODUCTION: JSON.stringify(true),  // 此时传过去的 PRODUCTION 是 字符串 “true”
            VERSION: JSON.stringify("5fa3b9"), // 
        }),
        // 多线程打包, 要是对css也启动多线程的话,再创建一个happypack, id为css
        new Happypack({
            id: 'js',
            use: [{ 
                loader: 'babel-loader',
                option: {
                    cacheDirectory: true,  // 开启js 打包优化
                    presets: ['@babel/preset-env', '@babel/preset-react'],
                    plugins: [require('@babel/plugin-transform-object-rest-spread')]
                },
            }]
        }),
        // ignorePlugin, 针对某个包的依赖不进行打包,比如moment, locale 是moment的语言包,要是我们只使用zh-cn 那么我们可以忽略掉其他,所以此时忽略掉locale
        new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),

        // 下面是热更新
        new webpack.NamedModulesPlugin(), // 告诉webpack  哪个模块更新了
        new webpack.HotModuleReplacementPlugin(); // 进行热更新
    ]
}

热更新

import test from './test';

if(module.hot) {
    module.hot.accept('./test', () => {
        console.log('文件已更新');
        require('./test');
    })
}

我们总结一下上面关于打包优化的几种方式吧

  • 配置resolve 减少目录的搜索路径
  • 同样的在loader 中设置 include 和 exclude 指定loader 编译的目录
  • 设置modules 下的 noParse属性, 这个可以在打包的时候不检查某js 的依赖,这样就可以减少打包时间了
  • 配置DllPlugin 用动态链接库的形式打包,这样的话会根据目录下的manifest.json 进行二次打包, 二次打包不会再对已生成的动态链接库进行打包
  • 使用happyPack 用多线程打包

tapable

webpack 本质上是一种事件流机制,它的工作流程就是把各个插件串联起来, 他的核心就是tapable, tapable 有点像nodejs 的event库, 就是观察者模式

先来看看一个简单的events 库

class EventBus {
    constructor() {
        this.maps = {}
    }

    on(name, fn) {
        this.maps[name] = fn;
    }

    fire(name, data) {
        this.maps[name] && this.maps[name](data);
    }
}

// 测试

const eventBus = new EventBus();
eventBus.on("click", (data) => {
    console.log("click", data)
})

eventBus.fire("click", {a: 1, b: 2})

简单的观察者模式

发布订阅其实很简单, 我可以想象成天文台, 当温度改变时, 天文台的数据改变(changes)的时候,我们用户需要做什么,他下雨了,我们需要收衣服,

  • 被观察者是 天文台,
  • 观察者 是我们用户, 具体做法是我们要收衣服, 就是对应下面的update, 简单说就是具体的做法就是观察者了
class Subject {
    constructor() {
        this.watchers = []
    }

    addWatch(watcher) {
        console.log(this.watcher)
        this.watchers.push(watcher)
    }

    removeWatcher(watcher) {
        let index = this.watchers.indexOf(watcher);
        if(index > -1) {
            this.watchers.splice(index, 1)
        }
    }

    notify() {
        this.watchers.forEach((watcher) => watcher.update())
    }
}

class Watcher {
    subscribeTo(subject) {
        subject.addWatch(this);
    }

    update() {}
}

let subject = new Subject()
let watcher = new Watcher()
watcher.update = function() {
  console.log('observer update')
}
watcher.subscribeTo(subject)  //观察者订阅主题

let watcher2 = new Watcher()
watcher2.update = function() {
  console.log('我是另一个观察者,我要做其他事情')
}
watcher2.subscribeTo(subject) 

subject.notify()

实现 SyncHook

我们以上面的例子,实现一个SyncHook

class SyncHook {
    constructor(args) {
        this.tasks = []

    }

    // 绑定时间
    tap(name, fn) {
        this.tasks.push(fn)
    }

    // 运行函数, 在tapabel
    call(...args) {
        this.tasks.forEach((task) => task(...args));
    }
}

let hook = new SyncHook(['name']) // ['name'] 指的是我在创建hook的时候, 我tap需要传递的参数
hook.tap("test", (name) => {
    console.log('test', name)
})
hook.tap("test2", (name) => {
    console.log("test2", name)
})
hook.call("hello"); // 这里的hello 对应的是上面的name

webpack 原理

webapck其实就是自己实现了一个require方法,这里需要对AST进行一部分的了解, AST就是抽象语法树, 简单说就是将 js 转换成 语法树,转换成 方法, 变量等等的属性

我们看看AST 的步骤

  • ASTjs 转换成 语法树
  • 修改语法树的值
  • AST 转换成浏览器可以识别的 语法

AST 依赖包

  • babylon 将 源码 解析成 AST(抽象语法树)
  • @babel/traverse 遍历 AST 中的节点
  • @babel/types 替换 AST 节点
  • @babel/generator 将替换的结果生成成js

我们想想webpack 的运行过程, 我们首先配置 webpack.config.js, 然后运行的是 webpack --config webpack.config.js 然后webpack 会根据 entry入口文件
进行分析,对它进行AST解析, 如果entry入口文件还有require, 那么继续进行依赖遍历。

loader

其实loader 就是一个方法,我们看两个例子, 一个是less-loader, 另一个是style-loader
其中 loader-utils 获取loader 的参数就是 loader 的 options

less-loader

// less-loader

/**
 * 我们less-loader 当然要转换成css,那么我们使用的是less.render
 * 下面使用less那么肯定需要 npm install less -=save-dev
 * @param {string} source  这里的source就是指 less源码
*/
const loaderUtils = require('loader-utils');
function loader(source) {
    let css = "";
    // loaderUtils.getOption(this) 可以拿到他的参数
    less.render(source, (err, lessSource) => {
        css = lessSource.css
    })
    return css;
}

style-loader

/**
 * 我们style-loader 的作用是将css 写在html 的head 下面的style标签下
*/
function loader(source) {
    let styles = `
        const el = document.createElement("style");
        const css = ${source.replace(/\s*/g, "")}
        el.innerHTML = css;
        document.head.appendChild(css);
    `
    return styles
}

plugin

webpack 是基于tapable事件流, 你把 plugin 想象成在webapck 中不同的生命周期做不同的事情,我们看看webpack 的hooks吧

  • entryOption 入口hooks
  • compile 编译时期
  • afterCompile 完成编译后
  • afterPlugins 插件完成编译后
  • run 运行
  • emit 生成编译文件时
  • done 执行完成

自定义plugin

class Plugin() {

    apply(compiler) { // 此时的compiler 是webpack实例
        compiler.hooks.done.tap("run", () => {
            console.log("此时是webpack 运行时运行的时间")
        })
        compiler.hooks.done.tap("name", () => {
            console.log("此时是注册事件,指的是在整个wepack执行完成之后的回调函数")
        })
    }
}