webpack基本配置学习

118 阅读7分钟

本文档是我在b站看到的一套学习视频,我觉着这套视频讲的很棒,就认真的跟着视频一个案例一个案例的敲下来,并用自己总结写出自己的文档,这样可以更印象深刻,初学者建议直接视频链接跳走,自己跟着敲一遍!

1.0 为什么使用webpack?

传统前端html页面通过script标签引入js文件会带来很多问题,比如js文件太多代码可维护性变差,js文件太大会影响页面渲染,一些库会在window上绑定对象,比如jq.会有全局污染等等一系列问题,那这些问题怎么解决?早些年的项目有GRUNT和GULP,这两个工具是任务执行器,他们是将所有的项目文件拼接在一起,利用的是立即调用函数表达式,来解决了作用域问题,refine.js来解决代码模块化的问题,这些就不详细介绍了,来介绍主角webpack。

1.1 自定义webpack.config.js

安装nodejs,webpack以及webpack-cli不再赘述,从webpack的入口文件讲起,webpack.config.js需要建立在文件根目录下,由于webpack是在nodejs中运行的,所以定义模块的时候要使用nodejs的commonjs去定义,先建一个index.html,引入一个src下的index.js,index.js中只做一件事引入平级目录下的main.js

--  index.js --
import hello from "./main"
hello();

-- main.js --
function hello(){
    console.log('hello');
}
export default hello;


-- webpack.config.js -- 
const path = require('path');
module.exports = {
    //入口 
    entry:"./src/index.js",
    //出口
    output:{
        filename:"bundle.js",
        clean: true,  //每次打包删除之前的文件
        path: path.resolve(__dirname,'./dist')
    },
    
    mode: "none",
}

现在我们就可以执行一下webpack执行,去打包一下这一个方法了,执行完毕以后可以看到dist/bundle.js已经打包出来了,可以正常引入执行方法。小试牛刀已经可以了,接下来的demo都是在此基础上增加配置,继续学习!

1.2 插件的使用

1.1中我们只是打包了一个js文件,还需要手动引入打包后的文件才能看到效果,接下来我们使用插件功能来自动帮我们操作这些,这里用到HtmlWebpackPlugin,首先npm安装这个插件,然后引入并添加到config中

const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    ...,//重复的地方我就省去了
    plugins:[
        new HtmlWebpackPlugin({
            template :"./index.html",  //模板文件
            filename :"app.html",      //新模板文件名称
            inject: "body"             //添加到的节点位置
        })
    ],
}

1.3 环境

我们可以通过webpack.config.js中的mode设置打包的模式,当我们把mode值设置成development的时候,我们会发现此时bundle.js中打包的js十分不便于我们观看,我们可以使用source-map,增加配置devtool设置,同时我们可以按照webpack-dev-server帮助我们启动本地环境,并且监听文件变化自动刷新页面

module.exports = {
    ...,//重复的地方我就省去了
    mode: "development",
    devtool: "inline-source-map",
    devSrever: {
        static: "./dist"
    }
}

1.4 资源模块

以上我们学习到的内容只能是去帮助我们打包js,那么其他的资源如何引入呢?我有做过一个layui的项目,在layui-admin的这个框架中,我配置的gulpfile.js中对于图片的处理是直接移动文件到指定的文件夹下,接下来我们看一下webpack是如何处理的。

webpack使用内置资源模块(asset modules)来引入其他任何类型的资源,一共有四种资源类型模块

  • asset/resource 发送一个单独的文件导出url
  • asset/inline 导出一个资源的data url
  • asset/source 会导出资源的源代码
  • asset 会在resource和inline中自动选择

资源模块的使用:

module.exports = {
    ...,
    module : {
        rules: [
            {
                test : /\.png$/,
                type: 'asset/resource',
                generator:{
                    filename: "images/[contenthash][ext]"
                }
            },
            {
                test : /\.svg$/,
                type: 'asset/inline'
            },
            {
                test : /\.txt$/,
                type: 'asset/source'
            },
            {
                test : /\.jpg$/,
                type: 'asset',
                parser: {
                    dataUrlCondition : {
                        maxSize: 4 * 1024 *1024
                    }
                }
            }
        ]
    }
}

1.5 关于loader

webpack除了资源模块以外,还可以通过loader来引入外部资源,loader可以让webpack可以理解除js,json以外的其他类型文件,loader定义有两个重要的属性,一个是test判断类型格式,一个是use,指定我们使用什么loader。

78c4d3d890aa1b2d743fb39e8d6b7b9.png

以css-loader为例我们使用一下,首先npm安装css-loader和style-loader,配置一下rules

module.exports = {
    ...,
    module : {
        rules: [
            {
                test : /\.css$/,
                /**
                先要写style-loader后写css-loader,这里是因为要先使用css-loader
                打包识别css文件,然后通过style-loader把样式放置到页面上
                */
                use: [
                    'style-loader',
                    'css-loader'
                ],
            }
        ]
    }
}

再加入less-loader和less

module.exports = {
    ...,
    module : {
        rules: [
            {
                test : /\.(css|less)$/,
                use: [
                    'style-loader',
                    'css-loader',
                    'less-loader'
                ],
            }
        ]
    }
}

mini-css-extract-plugin插件抽离css文件

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
    ...,
    plugins:[
        new MiniCssExtractPlugin({
            filename: "styles/[contenthash].css"
        })
    ],
    module : {
        rules: [
            {
                test : /\.(css|less)$/,
                use: [
                    MiniCssExtractPlugin.loader,
                    'css-loader',
                    'less-loader'
                ],
            }
        ]
    },
}

压缩css文件,安装css-minimizer-webpack-plugin,并把mode改为production,增加到optimization优化配置中

const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin');
module.exports = {
    ...,
    mode: "production",
    optimization: {
        minimizer: [
            new CssMinimizerWebpackPlugin()
        ]
    }
}

1.6 babel-loader

安装babel的三个包 babel-loader @babel/core @babel/preset-env 编译async await语法时会报错 还需要再安装 @babel/runtime 和 @babel/plugin-transform-runtime

module.exports = {
    ...,
    module : {
        rules: [
            {
                test : /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: [ '@babel/preset-env' ],
                        plugins:[
                            [
                                '@babel/plugin-transform/runtime'
                            ]
                        ]
                    }
                }
            },
        ]
    },
}

1.7 代码分离

常见的代码分离的方法

  • 使用entry手动配置入口节点
module.exports = {
    ...,
    entry:{
        index: "./src/index.js",
        another: "./src/another.js",
    },

    output:{
        filename:'[name].bundle.js',
        path: path.resolve(__dirname,'./dist'),
        clean: true,
        assetModuleFilename: "images/[contenthash][ext]"
    }
}

以上代码配置了两个入口,index.js和another.js都引入了lodash库,那么lodash会打包到各自的包中

  • 防止重复的分离方法,在入口的地方通过entry dependencies 或 splitChunksPlugin 去重和分离代码
// dependencies
module.exports = {
    ...,
    entry:{
        index: {
            import: "./src/index.js",
            dependOn: "shared"
        },
        another: {
            import: "./src/another.js",
            dependOn: "shared"
        },
        shared: "lodash"
    },
}

//splitChunksPlugin
module.exports = {
    ...,
    entry:{
        index: "./src/index.js",
        another: "./src/another.js"
    },
    optimization: {
        splitChunks: {
            chunks: "all"
        }
    }
}

  • 动态导入:通过模块的内联函数调用来分离代码
function getCompoment(){
    return import('lodash').then(({ default: _ })=>{
        const element = document.createElement('div');
        element.innerHTML = _.join(['1','2','3'],'*');
        return element
    })
}

getCompoment().then((element)=>{
    document.body.appendChild(element);
})

动态导入的应用:懒加载:点击按钮的时候引入方法,不点击就不加载

const button = document.createElement('button');

button.textContent = "点击+++"

button.addEventListener('click',()=>{
    import('./math.js').then(({ add })=>{
        console.log(add(3,4));
    })
})
document.body.appendChild(button);

预获取webpackPrefetch和预加载webpackPreload,webpackPrefetch会在我们点击按钮的时候加载js,并在头部创建一个标签prefetch加载js

<link rel="prefetch" as="script" href="http://localhost:8080/src_math_js.bundle.js">
import(/* webpackPrefetch: true */'./math.js')

1.8 缓存

我们通过output.filename设置contenthash,每当文件内容发生变化的时候输入contenthash的文件名,来使浏览器重新请求文件,解决缓存的问题,但像是第三方的库,一般很少修改,我们可以把他们提取到单独的vendor chunk文件中,利用浏览器的长效缓存机制,命中缓存来消除请求,减少向server获取资源。

optimization: {
    minimizer: [
        new CssMinimizerWebpackPlugin()
    ],
    splitChunks: {
        cacheGroups: {
            vendor: {
                test: /[\\/]node_modules[\\/]/,
                name: 'vendors',
                chunks: 'all'
            }
        }
    }
}

1.9 环境变量

我们可以在执行打包命令时传入参数控制环境

// npx webpack --env production
module.exports = (env) => {
    return {
        mode: env.production ? "production" : "development"
    }
}

拆分配置文件,新建config文件夹里面新建webpack.config.dev.js和.prod.js,指定script脚本运行时的配置文件

"scripts": {
    "start": "webpack serve -c ./config/webpack.config.dev.js",
    "build": "webpack -c ./config/webpack.config.prod.js"
},

提取公共配置合并配置文件:新建webpack.config.common.js里面保留公共的部分,并删除dev和prod中公共的部分,安装webpack-marge

const { merge } = require('webpack-merge');

const commonConfig = require('./webpack.config.common');
const productionConfig = require('./webpack.config.prod');
const developmentConfig = require('./webpack.config.dev');

module.exports = (env) => {
    switch(true) {
        case env.development:
            return merge(commonConfig, developmentConfig)

        case env.production:
            return merge(commonConfig, productionConfig)

        defult:
            return new Error('error')
    }
}

"scripts": {
    "start": "webpack serve -c ./config/webpack.config.js --env development",
    "build": "webpack -c ./config/webpack.config.js --env production"
},

2.0 souce-map

SourceMap是一种映射关系。当项目运行后,如果出现错误,错误信息只能定位到打包后文件中错误的位置。如果想查看在源文件中错误的位置,则需要使用映射关系,找到对应的位置。

3a1369608e1b0ab2b4c77fc9452c8ff.png

推荐方式开发环境使用cheap-module-eval-source-map 生产环境不开启souce-map

2.1 devSrever

开发环境中,为我们启动一个web服务器,模拟用户从浏览器读取我们的web服务,首先npm安装webpack-dev-server

devServer: {
    static : "./dist",
    compress: true,  //设置content-Encoding:gzip压缩代码  提到效率
    port: 3000,  //配置端口号
    headers : {  //设置Response Headers
        'X-Access-Token': 'abcd123'
    },
    proxy:{
        '/api': 'http://localhost:9000'
    },
    https: true,
    historyApiFallback: true,  //解决spa项目history模式刷新报错问题
    hot: true,  //模块热替换:会在应用程序运行中替换添加或删除模块,而无需加载整个页面
}

2.2 模块解析

//相对路径
const math = require('./math2.js');
//绝对路径
import getHeader from "/src/components/header.js"
import body from "/src/components/a/b/body.js"
//模块路径
import _ from 'lodash';
console.log(math.add(5,4));
console.log(getHeader());
console.log(_.join(['1','2','3'],'*-*'));
body();

//components/a/b/body.js
const math = require('@/math2.js');
resolve : {
    //别名
    alias: {
        '@': path.resolve(__dirname, '../src')
    },
    //引入文件格式优先级设置
    extensions: [ '.json', '.js', '.vue' ]
}

2.3 外部扩展

引入第三方库,不希望打包到项目中,比如jq

externals: {
    jquery : 'jQuery'
}

2.4 web works

//work.js
self.onmessage = (message) => {
    self.postMessage({
        answer: 1111
    })
}

const worker = new Worker(new URL('./work.js', import.meta.url));
worker.postMessage({
    question: "请求问题"
})
worker.onmessage = (message) => {
    console.log(message.data.answer);
}