webpack配置、优化、原理,loader、plugin的开发

216 阅读5分钟

安装

  • 全局安装(不推荐)
        npm i webpack webpack-cli -g
        webpack -v
        npm uninstall webpack webpack-cli -g
    
  • 局部安装
        npm i webpack webpack-cli --save-dev
        npm info // 查看历史发布信息
        npm install webpack@xx.xx webpack-cli -S //安装指定的版本
    

配置、结构

webpack有默认的配置的文件wenpack.config.js,我们可以在这里进行修改和个性化配置

npx webpack //执行命令后,webpack会找到默认的配置文件,并使用执行
npx webpack --config customerConfig.js // 指定配置文件
// package.json
sciprts:{
    "dev": "webpack"
}
npm run dev
原理就是模块局部安装会在node_modules/.bin目录下创建一个软连接
    module.exports = {
        entry: './src/main.js' // 打包入口文件
        output: './dist',//输出结构
        mode: 'production',//打包模式 prod/dev
        module:{
            rules:[
            // loader模块处理
                {
                    test: /.css$/,
                    use: 'style-loader'
                }
            ]
        },
        //插件配置
        plugins: [
            new HtmlWebpackPlugin()
        ]
    }

webpack 核心模块

entry

指定webpack打包的入口文件,webpack执行构建的第一步将从entry开始

{
    // 单入口
    entry: {
        main: './src/main.js'
    },
    entry: './src/main.js',
    // 多入口
    entry:{
        main: './src/main.js',
        index: './src/index.js'
    }
}
output

打包转换后的文件输出位置

output:{
    filename: 'bundle.js', //输出文件的名称
    filepath: path.resolve(__dirname,'dist') // 必须是绝对路径
}
// 多入口处理
output:{
    filename: '[name][chunkhash:8].js',//文件名称
    filepath: path.resolve(__dirname,'dist)
}
loader

模块解析,把模块内容按照需求转换成新内容,webpack是模块打包,模块不仅仅是js,还可以是css、图片,但是webpack默认只知道处理js、json格式的,那么其他格式的模块处理就需要其他loader

style-loader、css-loader
less-loader、sass-loader
ts-loader 
babel-loader//转换ES6、7等js新特性语法
file-loader
eslint-loader
module

当webpack处理不认识的模块时,需要在webpack中的module处进⾏ 配置,当检测到是什么格式的模块,使⽤什么loader来处理

module:{
    rules:[
        {
            test: /.(svg|png|jpe?g|gif)/$,
            use: {
                loader: 'file-loader',
                options:{
                    name:'[name]_[hash].[ext]',
                    // 打包后存放的位置
                    outputPath: "images/",
                    publicPath: "../images"
                }
            }
        }
    ]
}

url-loader内部使⽤了file-loader,所以可以处理file-loader所有的事 情,但是遇到jpg格式的模块,会把该图⽚转换成base64格式字符串, 并打包到js⾥。对⼩体积的图⽚⽐较合适,⼤图⽚不合适。

{
    ...
    limit: 2048 // 小于2048转换成base64
}

plugins

plugin 可以在webpack运⾏到某个阶段的时候,帮你做⼀些事情,类似于 ⽣命周期的概念 扩展插件,在 Webpack 构建流程中的特定时机注⼊扩展逻辑来改变构建结 果或做你想要的事情。 作⽤于整个构建过程

    HtmlWebpackPlugin,
    clean-webpack-plugin
    mini-css-extract-plugin

sourceMap

webpackDevServer

ES6的处理

babel-loader是webpack 与 babel的通信桥梁,不会做把es6转成es5的
⼯作,这部分⼯作需要⽤到@babel/preset-env来做//@babel/preset-env⾥包含了es6转es5的转换规则。
还不够,Promise等⼀些还有转换过来,这时候需要借助
@babel/polyfill,把es的新特性都装进来,来弥补低版本浏览器中缺失的特性
polyfill造成全局污染
plugin-transform-runtime 闭包方式(开发组件库、工具库)

webpack 优化

treeShaking

//去掉没有引⽤的代码
//webpack.config.js
optimization:{
    usedExports: true
}
//package.json
"sideEffects": false//正常对所有模块进行tree shaking
"sideEffects":["*.css","@babel/polyfill"]
开发模式下,不会去掉没有引用的代码

code Splitting

optimization: {
    splitChunks: {
        chunks: 'async',//对同步 initial,异步 async,所有的模块有效 all
        minSize: 30000,//最⼩小尺⼨寸,当模块⼤大于30kb
        maxSize: 0,//对模块进⾏行行⼆二次分割时使⽤用,不不推荐使⽤用
        minChunks: 1,//打包⽣生成的chunk⽂文件最少有⼏几个chunk引⽤用了了这个模块
        maxAsyncRequests: 5,//最⼤大异步请求数,默认5
        maxInitialRequests: 3,//最⼤大初始化请求书,⼊入⼝口⽂文件同步请求,默认3
        automaticNameDelimiter: '~',//打包分割符号
        name: true,//打包后的名称,除了了布尔值,还可以接收⼀一个函数
function
    cacheGroups: {//缓存组
        vendors: {
            test: /[\\/]node_modules[\\/]/,
            name:"vendor", // 要缓存的 分隔出来的 chunk 名称
            priority: -10//缓存组优先级 数字越⼤大,优先级越⾼高
        },
        other:{chunks: "initial", // 必须三选⼀一: "initial" |"all" | "async"(默认就是async)
        test: /react|lodash/, // 正则规则验证,如果符合就提取
        chunk,
        name:"other",
        minSize: 30000,
        minChunks: 1,
    },
    commons:{
        test:/(react|react-dom)/,
        name:"react_vendors",
        chunks:"all"
    },
    default: {
        minChunks: 2,
        priority: -20,
        reuseExistingChunk: true//可设置是否重⽤用该chunk
    }
    }
}
optimization:{
 //帮我们⾃动做代码分割
 splitChunks:{
 chunks:"all",//默认是⽀持异步,我们使⽤all
 }
}

DllPlugin

//package.json
{
    "scripts":{
        "dll": "webpack --config webpack.dll.config.js",
    }
}
// webpack.dll.config.js
{
    entry: {
        vendor:["react","react-dom",...]
    },
    output:{
        path: path.join(__dirname,"./bundle"),
        filename:[name][hash:8].dll.js,
        library:[name]_library
    },
    plugins:[
        new webpack.DllPlugin({
            context: __dirname,
            name:[name]_library,
            path: path.join(__dirname,"./[name].mainfest.json")
        })
    ]
}
//webpack.config.js
{
    plugins:[
        new webpack.DllReferencePlugin({
            manifest: path.resole(__dirname, "vendor.mainfest.json")
        })
    ]
}

HMR:热模块替换

devServer:{
    contentBase: './dist',
    open: true,
    hot: true,
    hotOnly: true
}
const webpack = require('webpack')

plugins:[
    new CleanWebpackPlugin(),
    new webpack.HotModuleReplacementPlugin()
]
// index.js
import "@/style/index.css"
let btn = document.createElement('button')
btn.innerHTML = '按钮'
document.body.appendChild(btn)

处理理js模块HMR

module.hot.accept来观察模块更更新 从⽽而更更新

//counter.js
export function counter(){
    let div = document.createElement('div')
    div.innerHTML = 'hello world'
    document.body.appendChild(div)
}
export function number(){
    let div = document.createElement('div')
    div.innerHTML = 123
    document.body.appendChild(div)
}
//index.js
import {counter, number} from './counter'
number()
if(module.hot){
    module.hot.accept('./b',function(){
        document.body.removeChild(document.getElementById('number'))
        number()
    })
}

webpack打包原理分析

(function(modules) {
    installedModules = {};
    function __webpack_require__(moduleId) {
        if (installedModules[moduleId]) {
            return installedModules[moduleId].exports;
        }
        var module = (installedModules[moduleId] = {
            i: moduleId,
            l: false,
            exports: {}
        });
        modules[moduleId].call(
            module.exports,
            module,
            module.exports,
            __webpack_require__
        );
        module.l = true;
        return module.exports;
}
    return __webpack_require__((__webpack_require__.s ="./index.js"));
})({
    "./index.js": function(module, exports) {
        eval(
        '// import a from "./a";\n\nconsole.log("hello
        word");\n\n\n//# sourceURL=webpack:///./index.js?'
        );
    }
});
webpack_require 来实现模块化,代码都缓存在installedModules⾥,代码⽂文件以对象传递进来,key
是路路径,value是包裹的代码字符串串,并且代码内部的require,都被替换
成了webpack_require

实现一个bundle.js 读取入口文件,分析代码

    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 md = filename => {
        const content = fs.readFileSync(filename, 'utf-8')
        //解析成语法书
        const ast = parser.parse(content, {sourceType: 'module'})
        // 拿到依赖
        const dependencies = {}
        traverse(ast,{
            ImportDeclaration({node}){
                const dirname = path.dirname(filename)
                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
        }
    }

        <!--{-->
        <!--    filename: 'src/index.js'// 入口文件-->
        <!--    dependencies:{'./a.js','./src/a.js'},// 引入路径,项目中路径-->
        <!--    code: ''// 浏览器可执行的代码-->
        <!--}-->
    

如何编写一个loader

loader就是一个函数,生命是函数,不能用箭头函数,拿到源代码,做进一步的修饰处理,返回处理完成的源码

//customerLoader.js
const loaderUtils = require('loader-utils')//推荐处理loader/query的工具
  module.exports = function(source){
    // 注意这里的this
      const options = loaderUtils.getOptions(this)
      //return source.replace(/-_-!!!/,'')
      //使用callback可以返回多个信息
      this.callback(err:Error| null,content,)
      //对于异步的处理使用this.async,他会返回this.callback
      const callback = this.async
      settimeout(()=>{
          const options = loaderUtils.getOptions(this)
          callback(null,result)
      },100)
  }
 //webpack.config.js
 rules:[
    {
        test:/\.js$/,
        use: [
            {
                loader:path.resolve(__dirname,'customerLoader.js'),
                options:{...}
            }
        ]
    }
 ]
 ........................

创建一个plugins

plugin开始打包,在某个时刻,帮助我们处理一些事情 plugin是一个类,里面包含一个apply函数,接受一个参数compiler

class CopyPlugin {
    constructor(options){
        //options 参数
    }
    // compiler 就是webpack实例
    apply(compiler){
        // hoods.emit 定义在某个时刻(生命周期)
        compiler.hooks.emit.tapAsync("CopyPlugin",(compilation,cb)=>{
            ...
            console.log(异步写法)
            cb()
        })
        compiler.hooks.emit.tapAsync('CopyPlugin',compilation => {console.log('同步写法')})
    }
}
module.exports = CopyPlugin
// webpack.config.js
const CopyPlugin = require('./copy-plugin')
plugins:[
    new CopyPlugin({
       name: '测试' 
    })
]

compiler-hooks:https://webpack.js.org/api/compiler-hooks/

打包速度

文件编译、文件打包很影响打包的效率的, happypack
粘贴粘的好累-_-!!!!