配置Webpack支持(不同模块的打包,动态加载,分包实践)

1,546 阅读5分钟

不同模块的打包

1.本地项目安装webpack

npm i webpack webpack-cli --save-dev

2.在项目根目录下创建webpack.config.js,在该js中设置打包的mode、entry、otput

module.exports = {
    mode: 'development',
    entry: path.join(__dirname, 'src/page/test_a/a.js'),
    output: {
        filename: 'a.js',
        path: path.join(__dirname, 'dist/test_a'),
        publicPath: './',
        libraryTarget: 'umd' // 兼容AMD 和 COMMONJS
    },
    module: {
        rules: [
            { // 处理jsx,先将jsx转换为js,再将es6转换为es5
                test: /\.jsx?$/,
                exclude: /node_modules/,
                use: [
                    {
                        loader: 'babel-loader',
                        options: {
                            presets: [ // 从后往前
                                '@babel/preset-env', // es6转为es5
                                '@babel/preset-react' // jsx转为js
                            ]
                        }
                    }
                ]
            }, // 最后npm安装,npm i babel-loader @babel/preset-react @babel/preset-env @babel/core --save-dev
            {// 处理sass
                test: /\.(sa|sc|c)ss$/,
                use: [
                    'style-loader', // style标签插入到html中
                    {
                        loader: 'css-loader', // 处理@import等,css 转为js
                        options: {
                            importLoaders: 2 // 从后向前执行,已经执行了2个
                        }
                    },
                    'postcss-loader', // 将高级css语法转化为普通css,不过,还需要在根目录下配置一个postcss.config.js
                    'sass-loader', 
                ]
            }, // npm i style-loader css-loader postcss-loader autoprefixer sass-loader node-sass --save-dev
            {
                test: /\.(jpg|png|gif)$/i,
                type: 'asset/resource' // webpack5 之前 file-loader
            }, // 基本配置完后,通过npm run build 进行打包,会生成一个dist文件夹,如下图dist
        ]
    }

}
//postcss.config.js
module.exports = {
    plugins: [
        require('autoprefixer')
    ]
}

image.png

3.打包完后,在页面结构中,会出现很多style代码

image.png

这是因为style-loader将style插入到html中,解决方法是可以使用MiniCssExtractPlugin代替style-loader。

首先安装插件: npm install --save-dev mini-css-extract-plugin;

然后在webpack.config.js中引入: require('min-css-extract-plugin');

在module.exports中进行注册、配置

module.exports = {
    plugins: [
        new MiniCssExtractPlugin()
    ],
    module: {
        rules:[
            {
                test: /\.(sa|sc|c)ss$/,
                use: [
                    MiniCssExtractPlugin.loader,
                    // 'style-loader', // style标签插入到html中
                    {
                        loader: 'css-loader', // 处理@import等,css 转为js
                        options: {
                            importLoaders: 2 // 从后向前执行,已经执行了2个
                        }
                    },
                ]
            }, 
        ]
    }
}

通过npm run build打包之后,就会在dist目录下打包一个main.css文件;不过,可以通过filename更改文件名

plugins: [
        new MiniCssExtractPlugin({
            filename: 'a.css'
        })
],

再次打包后,会生成a.css文件,此时的页面已经没有样式效果,需要在html中单独引入

<head>
    <link rel="stylesheet" href="../../../dist/test_a/a.css"/>
</head>

查看a.css文件,会看到css代码并没有被压缩,可以再引入一个插件来压缩

安装: npm install css-minimizer-webpack-plugin --save-dev

在webpack.config.js中

const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'),
module.exports = {
    plugins: [
        new CssMinimizerPlugin(),
    ]
}

重新打包后,css会压缩到一行

4.此时会发现main.css 和a.css同时存在,可以引入第三方包,在每次打包之前先清空原来的dist目录。(rimraf)

安装: npm i rimraf --save-dev;在package.json中

"scripts": {
    "build": "rimraf ./dist/test_*/ && webpack"
}
  1. 上述的css和js都是手动引入的,可以通过HtmlWebpackPlugin自动引入;

安装:npm install --save-dev html-webpack-plugin

var HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    plugins: [
        new HtmlWebpackPlugin({
            title: '原html中title的名字',
            filename: 'test_a.html', // 新生成的html
            template: path.join(__dirname, 'src/page/test_a/test_a.html'), // 原html模板
            inject: 'body' // 将js插入到body中
        })
    ]
}

把原来html中的js、css引入删除,修改title

    <title>
        <%= htmlWebpackPlugin.options.title %>
    </title>

在打包后的html中,在body中会插入script标签,script标签中的src属性由publicPath和文件名组成,如果new HtmlWebpackPlugin中没有配置publicPath,则会读取output中的publicPath

<script src="./test_a.js"></script>
  1. 给文件增加hash值,实现增量部署(版本控制) contenthash 是根据模块内容产生的hash值,只有文件内容改变后hash才会变
module.exports = {
    output: {
        filename: 'a.[contenthash].js',
    },
    plugins: [
        new MiniCssExtractPlugin({
            filename: 'a.[contenthash].css'
        })
    ]
}

7.实现可以通过不同的打包命令打包不同的文件 (npm run build test_a 打包test_a, npm run build test_b 打包test_b)

先引入第三方插件: npm i optimist --save-dev 然后,在webpack.json的命令行中添加env

"scripts": {
    "build":  "rimraf ./dist/test_*/ &&webpack --env"
}

在webpack.config.js中

var optimist = require('optimist')
var cateName = optimist.args.env;
console.log(cateName) // 使用npm run build 'test_a'时,会打印出'test_a'

// 然后把module.exports中test_a全部替换成cateName
module.exports = {
    entry: path.join(__dirname, `src/page/${cateName}/${cateName}.js`)
}
  1. 实现开发时文件改动时,实时构建,自动刷新浏览器,实现热加载,使用webpack-dev-server

首先将webpack.config.js拆解成三个文件:webpack.config.base.js;webpack.config.build.js; webpack.config.dev.js;

在base.js中

var path = require('path');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var optimist = require('optimist');
var cateName = optimist.argv.env;

var entryObj = {};

if (cateName){
    entryObj[cateName] = `./src/page/${cateName}/${cateName}.js`
}

module.exports = {
    entry: entryObj,
    output: {
        fileName: '[name].[contenthash].js',
        path: path.join(__dirname, `./dist/${cateName}`),
        publicPath: './',
        chunkFileName:'[name].[contenthash].js',
        libraryTarget: 'umd'
    },
    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                exclude: /node_modules/,
                use: [
                    loader: 'babel-loader',
                    options: {
                        plugins: [ // 先执行插件,从前往后执行   
                            '@babel/plugin-syntax-dynamic-import',
                            '@babel/plugin-proposal-class-properties'  
                        ],
                        presets: [ // 后执行preset,从后往前执行
                            '@babel/preset-env',
                            '@babel/preset-react'
                        ]
                    }
                ]
            },
            { // 本地图片的打包处理
                test: /\.(png|jpg|gif)$/i,
                type: 'assets/resource'
            },
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            filename: `${cateName}.html`,
            template: path.resolve(__dirname, `./src/page/${cateName}/${cateName}.html`),
            title: '项目名称',
            inject: 'body'hash: false
        })
    ],
    otherInfo: {
        cateName
    }
}

在build.js中

var {otherInfo, output, plugins = [], ...webpackConfigBase} = require('./webpack.config.base.js');
var MiniCssExtractPlugin = require('mini-css-extract-plugin');
var CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
var TerserPlugin = require('terser-webpack-plugin');

module.exports = {
    ...webpackConfigBase,
    mode: 'production'output: {
        ...output,
        publicPath: '//cdn...'
    },
    module: {
        rules: [
            ...webpackConfigBase.module.rules,
            {
                test: /\.(sa|sc|c)ss$/,
                use: [
                    MiniCssExtractPlugin.loader,
                    {
                        loader: 'css-loader',
                        options: {
                            importLoader: 2
                        }
                    },
                    'postcss-loader',
                    'sass-loader',
                ]
            }
        ]
    },
    plugins: [
        newMiniCssExtractPlugin({
            filename: `${catenName}.[contenthash].css`
        }),
        new CssMinimizerPlugin()
    ]
}

在dev.js中

var {otherInfo = 'test_a', ...webpackConfigBase} = require('./webpack.config.base.js');

module.exports = {
    ...webpackConfigBase,
    mode: 'development'module: {
        rules: [
            ...webpackConfigBase.module.rules,
            {
                test: /\.(sa|sc|c)ss$/,
                use: [
                   'style-loader',
                    {
                        loader: 'css-loader',
                        options: {
                            importLoader: 2
                        }
                    },
                    'postcss-loader',
                    'sass-loader',
                ]
            }
        ]
    },
    devServer: {
        port: 1010, // 端口
        open: true, // 自动打开浏览器
        publicPath: '/', // 资源输出的路径
        hot: true // 热更新
    }
}

其次,在上面的dev.configh中加入devServer;

然后,在package.json中使用config明确告诉webpack打包时要使用的配置文件

"script:" {
    "build": "rimraf ./dist/test_*/ && webpack --config ./webpack.config.build.js --env"
    "dev": "webpack serve --config ./webpack.config.dev.js --env"
}

运行npm run dev test_a, 会发现资源都放在根目录下(devServer中的publicPath), 输入localhost:1010/test_a.html 即可看到页面。

其实devServer并没有真的输出资源,而只是把资源放到了内存中。

动态加载

实现点击下一页时才去加载下一页的代码

安装插件: npm i @babel/plugin-syntax-dynamic-import --save-dev

然后配置babel-loader,如下: image.png

最后根据vue或react的特性设置不同的路由

分包实践

原因: bundle打包后,是由我们的业务代码和第三方库构成。而第三方库比较稳定,所以可以单独打包,然后在页面中以cdn的方式引入,并且可以缓存起来,减少用户的请求次数。而业务代码单独打包后,体积也会缩小,打包速度也会加快。

首先可以先通过Webpack Bundle Ananlyzer插件分析各个bundle的大小: npm install --save -dev webpack-bundle-ananlyzer

然后在webpack.config.build.js中:

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
    plugins: [
        new BundleAnalyzerPlugin()
    ]
}

运行npm run build即可查看效果

下面,使用splitChunks进行分包

(base.js)
module.exports = {
    optimization: {
        // 第一种分包方式
        splitChunks: {
            cacheGroups: {
                reactBase: {
                   name: 'reactBase',
                   test: (module) => {
                       return /react/.test(module.context);
                   },
                   chunks: 'all',
                   priority: 1,
                },
                polyfillBase: {
                    name: 'polyfillBase',
                    test: (module) => {
                        return /core|babel|es6|promise/.test(module.context);
                    },
                    chunks: 'all',
                    priority: 2,
                },
                fetchBase: {
                    name: 'fetchBase',
                    test: (module)=> {
                        return /fetch|jsonp/.test(module.context);
                    },
                    chunks: 'all',
                    priority: 3,
                },
            }
        },
    }
}

运行npm run build之后即可看到打包效果。但打包时长并没有太大的变化。下面看手动分包,将业务代码和第三方代码分开打包。

首先,新建一个webpack.config.dll.js,用来打包第三方包

const path = require('path');
const webpack = require('webpack');
var TerserPlugin = require('terser-webpack-plugin');

module.exports = {
    mode: 'production',
    entry: {
        react: [
            'react',
            'react-dom',
            'react-router',
            'react-router-dom',
        ],
        polyfill: [
            '@babel/polyfill',
            'es6-promise'
        ],
        common: [
            'fetch-jsonp',
            'whatwg-fetch',
            'classnames',
            'prop-types',
            'better-scroll'
        ],
    },
    output: {
        path: path.resolve('./dist/vendor'),
        filename: '[name].[contenthash].js',
        library: '[name]_library'
    },
    plugins: [
        new webpack.DllPlugin({
            path: path.resolve('./dist/vendor', '[name]-mainfest.json'),
            name: '[name]_library'
        }),
    ],
    optimization: {
        minimize: true,
        minimizer: [
            new TerserPlugin({
                parallel: true,
                extractComments: false
            })
        ]
    }
}

然后在package.json中

"scripts": {
    "build:dll": "rimraf ./dist/vendor && webpack --config webpack.config.dll.js"
}

在webpack.config.base.js中

module.expors = {
    plugins: [
        new Wepck.DllReferencePlugin({
            maifest: require('./dist/vendor/common-mainfest.json')
        }),
        new Wepck.DllReferencePlugin({
            maifest: require('./dist/vendor/react-mainfest.json')
        }),
        new Wepck.DllReferencePlugin({
            maifest: require('./dist/vendor/polyfill-mainfest.json')
        }),
    ]
}

最后需要在html中通过script的方式引入

image.png

第三种分包方式: 在config中排除第三方包,在html中通过script的方式引入cdn链接

module.exports = {
    externals: {
        'react-router-dom': 'ReactRouterDOM',
        'react': 'React',
        'react-dom': 'ReactDom'
    }
}