偷学VueCli和CRA脚手架,入门webpack配置工程师

1,250 阅读5分钟

背景

目前前端工程化很好,前端工程化在构建方面的核心毫无疑问是webpackwebpack工程师,是每个前端工程师都要有的头衔(狗头保命),为了学习webpack,笔者看完了webpack中文网又查阅很多webpack的文章和VueCliCRA脚手架,发掘目前基础webpack配置,希望积累webpack的最佳实践,抵御不住webpack5的诱惑,新增了本项目从webpack4(master分支)升级到webpack5(webpackV5分支)的艰难流程,最终的代码地址

安装webpack

# 安装4最新版本
yarn add webpack@"^4.0.0"

加上配置文件 webpack.config.js

module.exports = {
    entry: './src/index.js'
    // 开发模式
    mode:'development',
    // 单独提取source-map,输出后的js更可读
    devtool : 'source-map'
};

添加运行命令

package.json

{
  "name": "webpack4-best-pratice",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
     // 添加dev命令
    "dev": "webpack"
  },
  "dependencies": {
    "webpack": "^4.0.0"
  }
}

添加src目录并运行

yarn dev

javascript代码环境降级

babel通过编译es6到es5实现了开发中使用es6代码部署中又不用考虑浏览器兼容

安装babel

# @babel/core babel 转换器核心包
# @babel/preset-env babel转化配置包
# babel-loader baberl的webpack插件
yarn add babel-loader @babel/core @babel/preset-env

使用webpack编译代码

在src/index.js新增

const a = 1

执行dev命令

yarn dev

查看输出结果发现我们的const没有被编译成es5,接下来我们来解决这个问题

配置babel转译语法

新增babel.config.js

module.exports = {
    // 引入编译选项
    presets: [
        [
            '@babel/preset-env'
        ],
    ],
};

配置webpack对js文件使用babel编译

module.exports = {
    // .... 新增module字段
    module: {
        rules: [
          { 
            // 匹配.js文件
            test: /\.js$/,
            // 排除node_modules提升编译效率
            exclude: /(node_modules)/,
            // 使用babel-loader
            use: {
              loader: 'babel-loader',
            }
          }
        ]
    }
    // ...
};

执行命令验证编译结果

yarn dev

可以看到我们的const已经被转译成了var

缺失的ES6+ API编译

我们在index.js新增

Promise.resolve(1)

执行yarn dev

可以看到我们的Promise并没有转译,也就会缺少API级别的兼容性

配置ES6+ API编译

babel将编译分成了2类,一类成为语法编译,一类称为polyfill

语法:

const a = 1
// 编译
var a  =  1

polyfill

Promise.resolve(1)
// 通过引入Promise
Promise = require('Promise')

也就是我们需要找到一个实现了Promise同时符合ECMAScript的API实现包,目前推荐的是core-js,在babel的配置下是这样的

babel.config.js

module.exports = {
    presets: [
        [
            '@babel/preset-env',{
                // 配置useBuiltIns为entry,防止依赖的第三方库没声明其es6+的API导致我们应用程序出错,不建议usage选项,需要在开发中熟悉第三方包是否使用到ES6+的API
                useBuiltIns: 'entry',
                // 使用corejs3版本,corejs2很早就冻结分支了,例如Array.prototype.flat只在corejs3版本
                corejs: 3
            }
        ],
    ],
};

src/index.js

// 引入core-js/stable和regenerator-runtime/runtime,相当于已经废弃的babel-polyfill
import 'core-js/stable'
import 'regenerator-runtime/runtime'

const a = 1
Promise.resolve(1)

打包结果

css引入支持

src/css下新增globel.css,内容如下:

body {
    background-color: rebeccapurple;
}

引入到src/index.js

import './css/global.css'

执行yarn dev

出现报错提示我们可能需要这个文件类型的loader,这里需要安装css-loader,同时配置webpack

yarn add css-loader
// 新增css文件处理
module.exports = {
    module: {
        rules: [
            {
                test: /\.css$/i,
                use: ["css-loader"],
            },
        ]
    }
};

安装完毕后执行yarn dev,发现已经不报错了

在src下新增index.html验证输出结果

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    
</body>
<script src="../dist/main.js"></script>
</html>

我们在浏览器打开index.html发现样式并没有生效,这个时候我们需要引入style-loader,用来实现转换css到浏览器

yarn add style-loader

webpack.config.js

module.exports = {
    module: {
        rules: [
            {
                test: /\.css$/i,
                // 新增style-loader
                use: ["style-loader","css-loader"],
            },
        ]
    }
};

执行yarn dev并刷新页面,看到css生效

配置CSS私有前缀编译

在一些特性没有完全实现的时候,浏览器厂商常常会使用前缀允许我们使用,这块可以通过postcss帮助我们在编译时完成

安装依赖

# postcss 用来编译css
# postcss-loader postcss的webpack插件
# postcss-preset-env类似@babel/preset-env,配置编译环境
yarn add  postcss-loader postcss postcss-preset-env

配置webpack

module.exports = {
    module: {
        rules: [
            {
                test: /\.css$/i,
                // 新增postcss-loader
                use: ["style-loader","css-loader", "postcss-loader"],
            },
        ]
    }
};

新增postcss配置文件

postcss.config.js

module.exports = {
    plugins: [
      [
        "postcss-preset-env",
        {
          // 其他选项
        },
      ],
    ],
};

新增项目兼容浏览器范围配置

.browserslistrc

last 2 versions

修改global.css

body {
    background-color: rebeccapurple;
    /* 新增flex属性 */
    display: flex;
}

执行编译验证结果

编译后已经输出了flex的浏览器私有前缀

配置css预编译语言

这里我们配置sass

安装依赖

#  sass,sass的编译器,比node-sass兼容性好
# sass-loader sass的webpack插件
yarn add sass  sass-loader

配置webpack

module.exports = {
    module: {
        rules: [
            {
                test: /\.s[ac]ss$/i,
                use: [
                  "style-loader",
                  "css-loader",
                  "sass-loader",
                ],
              },
        ]
    }
};

修改文件后缀验证

将src/css/global.css后缀改为scss,同时在index.js引入的后缀也改成css,运行yarn dev,打包成功

配置svg

目前svg比较合适的方法是通过svg sprite的方式来使用

安装依赖

# svgo svg优化
# svgo-loader svgo webpack插件
# svg-sprite-loader svg-sprite插件
yarn add svgo-loader svgo svg-sprite-loader

配置webpack

webpack.config.js

module.exports = {
module: {
    rules: [
        {
            test: /\.svg$/,
            use: [
                { loader: 'svg-sprite-loader', options: {
                    
                } },
                'svgo-loader'
            ]
        }

    ]
},

新增svg目录

新增svg/index.js和svg/assets目录,在里面放入

<svg id="图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><title>我的订单</title><path d="M20.23,4h0l16.12,6.37L19.93,16.22h-.11L3.7,9.86,20.15,4h.08m0-2a2.07,2.07,0,0,0-.78.14L.76,8.78a1.1,1.1,0,0,0,0,2.06l18.33,7.25a2.06,2.06,0,0,0,.77.14,2.11,2.11,0,0,0,.78-.14l18.69-6.63a1.11,1.11,0,0,0,0-2.07L21,2.15A2.06,2.06,0,0,0,20.23,2Z"/><path d="M19.79,27.9a3.14,3.14,0,0,1-1.13-.21l-18-7.13a1,1,0,0,1,.74-1.86l18,7.13a1.25,1.25,0,0,0,.83,0l18.51-6.57a1,1,0,1,1,.67,1.89L20.89,27.7A3.19,3.19,0,0,1,19.79,27.9Z"/><path d="M19.79,37.92a3.36,3.36,0,0,1-1.13-.2l-18-7.13a1,1,0,0,1-.56-1.3,1,1,0,0,1,1.3-.56l18,7.12a1.13,1.13,0,0,0,.83,0l18.51-6.56a1,1,0,1,1,.67,1.88l-18.5,6.56A3.19,3.19,0,0,1,19.79,37.92Z"/></svg>
let req = require.context('./assets', false, /\.svg$/);

let requireAll = function (requireContext) {
    requireContext.keys().map(requireContext);
};

requireAll(req);

运行命令验证效果

yarn dev

支持静态资源

安装依赖

yarn add file-loader  url-loader

配置webpack

module.exports = {
    module: {
        rules: [
            {
                test: /\.(png|jpg|gif)$/i,
                use: [
                  {
                    loader: 'url-loader',
                    options: {
                      limit: 8192,
                    },
                  },
                ],
            },
            {
                test: /\.(png|jpe?g|gif)$/i,
                use: [
                  {
                    loader: 'file-loader',
                  },
                ],
            },
        ]
    },
};

配置Vue环境

安装依赖

# vue-loader vue-template-compiler vue 用来编译Vue文件
# @vue/babel-preset-jsx @vue/babel-helper-vue-jsx-merge-props 支持Vue JSX写法
yarn add vue-loader vue-template-compiler vue  @vue/babel-preset-jsx @vue/babel-helper-vue-jsx-merge-props

配置webpack

module.exports = {
    module: {
        rules: [
            {
                test: /\.vue$/,
                use: ['cache-loader', 'thread-loader','vue-loader'],
            },

        ]
    },
    plugins: [
        new VueLoaderPlugin()
    ],
};

新增文件验证

新增src/test.vue,在index.js引入

import testVue from './test.vue'
console.log(testVue);

执行yarn dev,看到控制台输出结果

热更新

自动注入依赖和复制html到dist目录

安装依赖

yarn add html-webpack-plugin

配置webpack

const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    // 使用HtmlWebpackPlugin
    plugins: [new HtmlWebpackPlugin()]
};

配置热更新

安装依赖

# webpack-dev-server 热更新服务器
# webpack-cli webpack命令包
yarn add webpack-cli webpack-dev-server

开启热更新

const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    // 开启热更新服务器配置
    devServer: {
        contentBase: './dist',
        hot: true,
    },
};

修改运行命令验证

package.json

"scripts": {
"dev": "webpack serve"
},

运行yarn dev

打开提示地址,查看结果,可以看到我们的代码已经在热更新服务器上运行,此时我们随意修改样式,可以实时生效

分离生产和开发环境配置

分离配置

webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    entry: './src/index.js',
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /(node_modules)/,
                use: {
                    loader: 'babel-loader',
                },
            },
        ]
    },
    plugins: [new HtmlWebpackPlugin()]
};

webpack.dev.config.js

const baseWebpackConfig = require('./webpack.config')
const { merge } = require('webpack-merge');
module.exports =   merge(baseWebpackConfig, {
    mode:'development',
    devtool : 'eval-source-map',
    devServer: {
        contentBase: './dist',
        hot: true,
    },
    module: {
        rules: [
            {
                test: /\.css$/i,
                use: ["style-loader","css-loader", "postcss-loader"],
            },
            {
                test: /\.s[ac]ss$/i,
                use: [
                  "style-loader",
                  "css-loader",
                  "sass-loader",
                ],
            },
        ]
    },
});

webpack.prod.config.js

const baseWebpackConfig = require('./webpack.config')
const { merge } = require('webpack-merge');
module.exports =   merge(baseWebpackConfig, {
    mode:'production',
    devtool : 'source-map',
    module: {
        rules: [
            {
                test: /\.css$/i,
                use: ["style-loader","css-loader", "postcss-loader"],
            },
            {
                test: /\.s[ac]ss$/i,
                use: [
                  "style-loader",
                  "css-loader",
                  "sass-loader",
                ],
            },
        ]
    },
});

添加开发命令和生产命令

package.json

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

运行命令尝试

yarn dev

配置压缩

javascript压缩

安装依赖

# 安装4.0最新版本,5版本只支持webpack5
terser-webpack-plugin@"^4.0.0"

配置webpack

webpack.prod.config.js

const TerserPlugin = require("terser-webpack-plugin");
module.exports =   merge(baseWebpackConfig, {
    optimization: {
        minimize: true,
        minimizer: [new TerserPlugin()],
    },
});

执行命令验证

yarn build

分离css到单独文件

安装依赖

yarn add mini-css-extract-plugin

配置webpack

webpack.prod.config.js

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports =   merge(baseWebpackConfig, {
    module: {
        rules: [
            {
                test: /\.css$/i,
                // 加上loader
                use: [MiniCssExtractPlugin.loader,"css-loader", "postcss-loader"],
            },
            {
                test: /\.s[ac]ss$/i,
                // 加上loader
                use: [
                  MiniCssExtractPlugin.loader,
                  "css-loader",
                  "sass-loader",
                ],
            },
        ]
    },
    // 加上插件
    plugins: [new MiniCssExtractPlugin()],
});

执行命令验证

执行yarn build,可以看到我们的dist文件下新增了一个main.css

压缩css

安装依赖

yarn add css-minimizer-webpack-plugin

配置webpack

webpack.prod.config.js

const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
module.exports =   merge(baseWebpackConfig, {
    // 新增CssMinimizerPlugin压缩插件
    minimizer: [new TerserPlugin(),new CssMinimizerPlugin()],
});

运行命令验证

yarn dev,看到main.css已经被压缩

持久化缓存

这里建议阅读这篇文章,详细讲述了每个情况下文件缓存名变化的应对方案,最终总结下来的配置如下:

webpack.config.js

module.exports = {
    output: { 
       filename: '[name].js',
       chunkFilename: '[name].js'
    }, 
};

webpack.prod.config.js

module.exports =   merge(baseWebpackConfig, {
    plugins: 
    [   
        // 稳定css hash
        new MiniCssExtractPlugin(
            {
                filename: '[name].[contenthash:8].css',
                chunkFilename: '[name].[contenthash:8].css'
            }
        ),   
        // 稳定chunk ID
        new webpack.NamedChunksPlugin(
            chunk => chunk.name || Array.from(chunk.modulesIterable, m => m.id).join("_")
        ),
    ],
    // 稳定模块 ID
    optimization: {
        hashedModuleIds: true,
    },
    output: { 
        // 分离chunks 映射关系,避免chunk改动时主js hash变动
        runtimeChunk: true,
        // 稳定文件hash
        filename: '[name].[contenthash:8].js',
        // 稳定chunk hash
        chunkFilename: '[name].[contenthash:8].js'
    }, 
});

性能优化-速度

并发

javascript

目前推荐使用官方的thread-loader,由于多线程有通信损耗,建议用在消耗大的loader,比如babel-loader

yarn add thread-loader

webpack.config.js

module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /(node_modules)/,
                // 为babel添加thread-loader,多进程编译
                use: ['thread-loader','babel-loader']
            },
        ]
    },
};

sass

官方推荐使用使用fibers提升sass编译速度

yarn add fibers
{
    loader: "sass-loader",
    options: {
        sassOptions: {
            require("fibers"),
        },
    },
},

缓存

目前推荐使用cache-loader

yarn add  cache-loader

webpack.config.js

module.exports = {
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /(node_modules)/,
                // 添加cache-loader提高二次编译速度
                use: ['cache-loader', 'thread-loader','babel-loader']
            },
        ]
    },
};

性能优化 - 体积

分包

webpack的默认分包只是分包异步块,我们需要自己调整一下

webpack.config.js

module.exports = {
    optimization: {
        splitChunks: {
            cacheGroups: {
                // node_modules打包在一个文件,提高缓存率
                vendors: {
                  name: `chunk-vendors`,
                  test: /[\\/]node_modules[\\/]/,
                  priority: -10,
                  chunks: 'initial'
                },
                // 提取引入超过2次的代码,减少打包体积
                common: {
                  name: `chunk-common`,
                  minChunks: 2,
                  priority: -20,
                  chunks: 'initial',
                  reuseExistingChunk: true
                }
              }
        }
    }
};

执行yarn build可以看到dist下增加了一个chunk-vendors文件

升级到webpack5

升级webpack

yarn add webpack

升级terser-webpack-plugin

yarn add terser-webpack-plugin

移除持久化缓存选项

webpack已经默认支持了moduleID和chunkID的稳定算法,所以这2个插件移除

module.exports =   merge(baseWebpackConfig, {
    plugins: 
    [ 
        // new webpack.NamedChunksPlugin(
        //     chunk => chunk.name || Array.from(chunk.modulesIterable, m => m.id).join("_")
        // ),
    ],
    optimization: {
        //hashedModuleIds: true,
    },
 
});

删除cache-loader

webpack5内置了缓存机制,缓存效果和缓存安全性更好,cache-loader可以删除

更新html-webpack-plugin

yarn add  html-webpack-plugin@next

废弃file-loader和url-loader

webpack 推出了资源这个概念,之前的file-loaderurl-loader已经被视为资源,如果资源配置满足你的话,迁移这个2个loader到对应的资源类型

{
    test: /\.(png|jpg|gif)$/i,
    type: 'asset/resource'
},
// {
//     test: /\.(png|jpg|gif)$/i,
//     use: [
//       {
//         loader: 'url-loader',
//         options: {
//           limit: 8192,
//         },
//       },
//     ],
// },
{
    test: /\.(png|jpg|gif)$/i,
    type: 'asset/inline'
},

// {
//     test: /\.(png|jpe?g|gif)$/i,
//     use: [
//       {
//         loader: 'file-loader',
//       },
//     ],
// },

热更新失效

需要把target设置为web平台

webpack.config.js

module.exports = {
    target: 'web'
}

热更新overlay失效

配置失效

在尝试触发一个错误并且配置了devServeroverlay属性,发现错误弹窗没有显示错误信息

devServer: {
    static: {
        directory: './dist',
    },
    // 配置了错误弹窗
    overlay: {
        warnings: true,
        errors: true
    }
},

webpack-dev-server未适配

经过调试和阅读源码发现问题是webpack-dev-server3.0还没适配webpack5

升级适配的webpack-dev-server版本

可以升级到正常适配webpack5beta版,执行

yarn add webpack-dev-server@next 

调试打补丁

安装完还是发现显示不了,继续调试,发现是这里的变量没有赋值,由于先用patch-package自己先打个补丁

安装依赖

yarn add patch-package

修改文件

node_modules\webpack-dev-server\lib\utils\normalizeOptions.js

  options.clientOverlay =
    typeof options.overlay !== 'undefined' ? options.overlay : false;

执行patch

npx patch-package webpack-dev-server

重新运行查看结果

给webpack-dev-server提PR

PR步骤:

  • fork开源仓库

  • 修改代码,通过仓库的规范检查(风格、质量、类型检查),同时要新增单元测试并通过当时的所有测试,在webpack-dev-server表现为

  • 推送到自己的远程仓库

  • 在原仓库发起PR

  • 给维护者用三级英语交流

PR地址