Webpack-学习笔记01

loading 2023年01月12日 98次浏览

1. 第一次打包

通过npm安装好webpack和webpack-cli后,可以开始尝试打包工作。

先写好两个js文件:

helloworld.js

let helloWorld = () =>{
    console.log('hello world')
}

export default helloWorld;

index.js

import helloWorld from "./helloword";

helloWorld()

再提前配置好webpack的配置文件

webpack.config.js

const path = require('path')
module.exports = {
    // 打包入口
    entry: './src/index.js',

    // 输出的打包文件名和路径(要用绝对路径)
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, './dist')
    },

    // 打包模式
    mode: 'none'
}

然后在命令行中运行npx webpack(加npx是因为npm安装时是局部安装而非全局安装),会发现打包成功:

image.png

生成了一个文件夹和文件

通过在index.html中引入该js文件,即可成功在浏览器中打印出helloworld。

2. HtmlWebpackPlugin

每次都手动修改index.html里的script src路径太麻烦了,这个plugin可以帮助我们解决这个问题。

首先本地安装这个插件

npm i html-webpack-plugin -D

在config中添加这个插件

const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    // 打包入口
    entry: './src/index.js',

    // 输出的打包文件名和路径(要用绝对路径)
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, './dist')
    },

    // 打包模式
    mode: 'none',

    plugins: [
        new HtmlWebpackPlugin()
    ]
}

再次运行npx webpack,会发现dist文件中除了bundle.js还生成了一个index.html,里面已经自动引入了script标签。

当然也可以对这个插件进行一些参数配置:

plugins: [
        new HtmlWebpackPlugin({
            // html模板
            template: './index.html',
            // 输出文件名
            filename: 'app.html',
            // script标签位置(默认在head中)
            inject: 'body'
        })
    ]

这样的话就会生成一个app.html了,以外层的index.html作为模板。

3. clean

要想使得生成新的html后自动删除之前打包的html,给output添加clean属性即可:

    // 输出的打包文件名和路径(要用绝对路径)
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, './dist'),
        clean: true
    }

4. devtool

要想在浏览器中精准定位到代码出错位置,需要将bundle.js中代码解析,config中加入这个配置项即可:

devtool: 'inline-source-map'

解析前

解析后

5. webpack-dev-server

实现实时的热更新,先通过npm安装:

npm install webpack-dev-server -D

再在config中配置:

    devServer: {
        static: './dist'
    }

服务会默认启动在8080端口,打开网址:

dist内文件都已经被存储到内存中,点进html文件控制台会打印hello world且实时热更新。

6. 资源模块

6.1 asset/resource

之前学习的webpack只能打包js文件,通过资源模块实现加载图片等内容。

加入新配置项,实现打包png格式的图片文件,并指定生成的路径和文件名(文件名由webpack随机生成一个哈希值):

    module: {
        rules: [
            {
                test: /\.png$/,
                type: 'asset/resource',
                generator: {
                    filename: 'images/[contenthash][ext]'
                }
            },
        ]
    }

生成的路径和文件名也可以配置在output中:

    // 输出的打包文件名和路径(要用绝对路径)
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, './dist'),
        clean: true,
        assetModuleFilename: 'images/[contenthash][ext]'
    }

随后修改index.js,将图片插入到页面中:

import helloWorld from "./helloword";
import imgSrc from './assets/img-1.png'

helloWorld()

const img = document.createElement('img');
img.src = imgSrc;
document.body.appendChild(img)

此时就实现了将图片也打包起来,并插入到app.html中。

6.2 asset/inline

通过asset/inline打包svg文件,特点在于资源被打包后不会显示在vscode文件夹中,而是生成base64的一个链接,直接将效果呈现在浏览器中:

module: {
        rules: [
            {
                test: /\.svg$/,
                type: 'asset/inline'
            },
        ]
    }

index.js

const img2 = document.createElement('img');
img2.style.cssText = 'width:600px ; height:200px'
img2.src = svgSrc;
document.body.appendChild(img2)

6.3 asset/source

用于加载文本文件,打包后也是不会显示在文件夹中。

module: {
        rules: [
            {
                test: /\.txt$/,
                type: 'asset/source'
            },
        ]
    }
const block = document.createElement('div')
block.style.cssText = 'width: 200px; height: 200px; background: aliceblue'
block.textContent = exampleText
document.body.appendChild(block)

6.4 asset

在 asset/inline 和 asset/resource 之间自动选择。默认情况下文件大于8k就会选择resource,从而在images中创建图片,但是可以通过自己修改这个临界值使得创建方式改为inline。

    module: {
        rules: [
            {
                test: /\.jpg$/,
                type: 'asset',
                parser: {
                    dataUrlCondition: {
			// 文件大小大于4m才生成resource,否则inline
                        maxSize: 4 * 1024 * 1024
                    }
                }
            }
        ]
    }

7. loader

webpack默认只能理解JavaScript和JSON文件,但实际工作中各种需求层出不穷,文件类型也多种多样,比如.vue、.ts、图片、.css等。这就需要loader增强webpack处理文件的能力。

7.1 加载CSS

必须通过loader实现打包css文件

npm install css-loader -D

新增一条rules

module: {
        rules: [
            {
                test: /\.css$/,
                use: 'css-loader'
            },
        ]
    }

写一个css

.hello {
    color: lightgreen;
}

将css引入到js中并给body添加类名

import './style.css'
...
document.body.classList.add('hello')

但是运行后会发现字体并没有变色,因为还需要引入另一个loader

npm install style-loader -D

然后将rules替换成:

            {
                test: /\.css$/,
                use: ['style-loader','css-loader']
            },

注意这里的style-loader和css-loader顺序不能颠倒,因为这里的loader是链式调用,从右往左执行。

7.2 加载less/sass

和加载css是比较类似的,首先安装包

npm i less less-loader -D

写一个less文件

@color: #f9efd4;

body {
    background-color: @color;
}

导入到js中后,修改config,也是不能乱调loader的顺序,打包后重启webpack-dev-server即生效

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

7.3 抽离压缩CSS

之前的css代码是直接放到html代码中的,希望可以实现抽离出去通过link导入,需要通过一个插件来实现:

npm i mini-css-extract-plugin -D

下载好后实例化到plugins中:

    plugins: [
        new HtmlWebpackPlugin({
            // html模板
            template: './index.html',
            // 输出文件名
            filename: 'app.html',
            // script标签位置(默认在head中)
            inject: 'body'
        }),
        new MiniCssExtractPlugin({
            filename: 'styles/[contenthash].css'
        })
    ],

此时要抽离出css,则style-loader就无效了,需要一个新的loader:MiniCssExtractPlugin.loader

            {
                test: /\.(css|less)$/,
                use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader']
            },

重新打包后会发现此时css已经是通过link标签引入

但是会发现此时被打包的css文件没有被压缩,再引入一个插件来压缩css:

npm i css-minimizer-webpack-plugin -D

注意这里不是在plugins中导入了,而是新建一个属性optimization:

    optimization: {
        minimizer: [
            new CssMinimizerPlugin()
        ]
    }

同时还要把打包模式改成生产环境:

    // 打包模式
    mode: 'production'

7.4 加载字体图标

其实一样的 先写css 然后加到js里 然后配config 直接一起放上来了

@font-face {
    font-family: 'iconfont';
    src: url('./assets/iconfont.ttf') format('truetype');
}

.icon {
    font-family: 'iconfont';
    font-size: 30px;
}
const span = document.createElement('span')
span.classList.add('icon')
span.innerHTML = ''
document.body.appendChild(span)
            {
                test: /\.(woff|woff2|eot|ttf|otf)$/,
                type: 'asset/resource'
            },

7.5 加载csv & xml

webpack原生支持处理json类型数据,别的数据需要依赖loader处理,先安装:

npm i csv-loader xml-loader -D

写config

            {
                test: /\.(csv|tsv)$/,
                use: 'csv-loader'
            },

            {
                test: /\.xml$/,
                use: 'xml-loader'
            },

然后在index.js中打印出数据测试即可

import Data from './assets/data.xml'
import Notes from './assets/data.csv'

console.log(Data)
console.log(Notes)

7.6 自定义json模块parser

要解析yaml,toml,json5格式数据时需用到。

npm过程略

写config时需要注意,多了一个parser属性

// 注意这要导入
const toml = require('toml')
const yaml = require('yaml')
const json5 = require('json5')

            {
                test: /\.toml$/,
                type: 'json',
                parser: {
                    parse: toml.parse
                }
            },
            {
                test: /\.yaml$/,
                type: 'json',
                parser: {
                    parse: yaml.parse
                }
            },
            {
                test: /\.json5$/,
                type: 'json',
                parser: {
                    parse: json5.parse
                }
            }

后续打印测试同上,略。

8. babel-loader

有的js文件也需要loader解析,比如async/await,举个例子,把之前的hellowworld文件改一下:

function getString() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('promise!!!')
        }, 2000)
    })
}

async function helloWorld() {
    let string = await getString()
    console.log(string) // 'promise!!!'
}

export default helloWorld;

会发现helloworld的代码原封不动地被搬到了浏览器上,如果是支持es6的浏览器倒无所谓,但是如果是不支持es6的浏览器就会出问题。 因此需要通过babel-loader将代码转换成es5的。

首先npm安装三个包

npm i babel-loader @babel/core @babel/preset-env -D

都安装好后运行打包会发现报错少了一个插件:regeneratorRuntime,因此还要安装两个包(第一个包包含了那个插件):

npm i @babel/runtime -D
npm i @babel/plugin-transform-runtime -D

然后写config

            {
                test: /\.js$/,
		// node_modules里的代码不用转化
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env'],
                        plugins: [
                            [
                                '@babel/plugin-transform-runtime'
                            ]
                        ]
                    }
                }
            }

9. 代码分离

代码分离是 webpack 中最引人注目的特性之一。此特性能够把代码分离到不同的bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。

代码分离主要有三种方法:

  • 入口起点:使用 entry 配置手动地分离代码。
  • 防止重复:使用 Entry dependencies 或者 SplitChunksPlugin 去重和分离chunk。
  • 动态导入:通过模块的内联函数调用来分离代码。

9.1 入口起点

现在新增一个文件another-module.js

import _ from 'lodash'

console.log(_.join(['Another', 'module', 'loaded!'], ' '))

需要修改一下config,entry新增一个地址,以及output需要根据entry名给filename加上不同前缀,保证不同入口输出不同名字的文件。

    // 打包入口
    entry: {
        index : './src/index.js',
        another : './src/another-module.js'
    },

    // 输出的打包文件名和路径(要用绝对路径)
    output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, './dist'),
        clean: true,
        assetModuleFilename: 'images/[contenthash][ext]'
    },

这种方法最大的问题是不同js如果用了同样的包,比如loadsh,axios之类,这些包会被分开打包两次,本来应该是公用的只打包一次,导致了内存的浪费

9.2 防止重复

可以更改一下entry的写法来防止上面的重复打包现象:

    // 打包入口
    entry: {
        index: {
            import: './src/index.js',
            dependOn: 'shared'
        },

        another: {
            import: './src/another-module.js',
            dependOn: 'shared'
        },

        shared: 'lodash',
    },

这样就可以实现把不同模块依赖的公共库抽离成chunk

同样的效果还可以通过optimization来实现:

    optimization: {
        splitChunks: {
            chunks: 'all'
        }
    }

此时entry改回之前的形式也能实现同样的效果了:

    // 打包入口
    entry: {
        index: './src/index.js',
        another: './src/another-module.js'
    },

9.3 动态导入

import返回一个promise对象,因此可以用then方法处理调用

function getComponent() {
    return import('lodash')
        .then(({ default: _ }) => {
            const element = document.createElement('div')
            element.innerHTML = _.join(['Hello', 'webpack'], ' ')
            return element
        })
}

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

直接在index.js中引入该文件,页面就会显示出Hello webpack

9.4 动态导入实现懒加载

首先写一个math.js文件

export const add = (x, y) => {
    return x + y
}

export const minus = (x, y) => {
    return x - y
}

然后在index.js中引入,使用动态加载:

const button = document.createElement('button')
button.textContent = '点击执行加法运算'
button.addEventListener('click', () => {
    import('./math.js').then(({ add }) => {
        console.log(add(4, 5))
    })
})
document.body.appendChild(button)

来到浏览器中我们会发现,在点击按钮之前被打包出的该模块bundle.js模块代码是不会被加载的,只有被点击后才会被加载。

9.5 prefetch & preload

给刚刚的动态加载代码加上这么一句:/* webpackPrefetch: true */

button.addEventListener('click', () => {
    import(/* webpackPrefetch: true */'./math.js').then(({ add }) => {
        console.log(add(4, 5))
    })
})

会发现打开浏览器时包就已经被加载了,那这么说起来这种方式还不如懒加载?

其实不是的,prefetch会在网络其他内容加载完,网络空闲时才下载这个包,不会影响性能。

如果把prefetch换成preload,会发现效果和上面的懒加载是基本一样的,也是一开始不加载,点击了才加载。

button.addEventListener('click', () => {
    import(/* webpackPreload: true */'./math.js').then(({ add }) => {
        console.log(add(4, 5))
    })
})