webpack

159 阅读3分钟

(一)、为何使用webpack

1、编译高级语法或语言(TS、ES6+,模块化、scss、react vue)

2、使代码体积更小 (Tree-shaking, 压缩、合并),加载更快

3、兼容性和错误检查(polyfill,postcss、eslint)

4、统一、高效的开发环境

5、统一的构建流程和产出标准

6、集成公司构建规范(提测、上线等)公司的项目webpack公司统一配置,自己的项目可以用脚手架工具

webpack的配置是node的语法

webpack.common.js

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { srcPath, distPath } = require('./paths')

module.exports = {
    entry: {
        index: path.join(srcPath, 'index.js'),
        other: path.join(srcPath, 'other.js')
    },
    module: {
        rules: [
            // babel-loader
        ]
    },
    plugins: [
        // new HtmlWebpackPlugin({
        //     template: path.join(srcPath, 'index.html'),
        //     filename: 'index.html'
        // })

        // 多入口 - 生成 index.html
        // 把编译后的js插入到html
        new HtmlWebpackPlugin({
            template: path.join(srcPath, 'index.html'),
            filename: 'index.html',
            // chunks 表示该页面要引用哪些 chunk (即上面的 index 和 other),默认全部引用
            chunks: ['index', 'vendor', 'common']  // 要考虑代码分割
        }),
        // 多入口 - 生成 other.html
        new HtmlWebpackPlugin({
            template: path.join(srcPath, 'other.html'),
            filename: 'other.html',
            chunks: ['other', 'vendor', 'common']  // 考虑代码分割
        })
    ]
}

webpack.dev.js

const path = require('path')
const webpack = require('webpack')
const webpackCommonConf = require('./webpack.common.js')
const { smart } = require('webpack-merge')
const { srcPath, distPath } = require('./paths')
const HotModuleReplacementPlugin = require('webpack/lib/HotModuleReplacementPlugin');

module.exports = smart(webpackCommonConf, {
    // mode 可选 development 或 production ,默认为后者
    // production 会默认压缩代码并进行其他优化(如 tree shaking)
    mode: 'development',
    entry: {
        // index: path.join(srcPath, 'index.js'),
        index: [
            'webpack-dev-server/client?http://localhost:8080/',
            'webpack/hot/dev-server',
            path.join(srcPath, 'index.js')
        ],
        other: path.join(srcPath, 'other.js')
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                loader: ['babel-loader?cacheDirectory'],
                include: srcPath,
                // exclude: /node_modules/
            },
            // 直接引入图片 url
            {
                test: /\.(png|jpg|jpeg|gif)$/,
                use: 'file-loader'
            },
            // {
            //     test: /\.css$/,
            //     // loader 的执行顺序是:从后往前
            //     loader: ['style-loader', 'css-loader']
            // },
            {
                test: /\.css$/,
                // loader 的执行顺序是:从后往前
                loader: ['style-loader', 'css-loader', 'postcss-loader'] // 加了 postcss
            },
            {
                test: /\.less$/,
                // 增加 'less-loader' ,注意顺序
                loader: ['style-loader', 'css-loader', 'less-loader']
            }
        ]
    },
    plugins: [
        new webpack.DefinePlugin({
            // window.ENV = 'production'
            ENV: JSON.stringify('development')
        }),
        new HotModuleReplacementPlugin()
    ],
    // webpack-dev-server webpack服务器跑项目代码
    devServer: {
        port: 8080,
        progress: true,  // 显示打包的进度条
        contentBase: distPath,  // 根目录
        open: true,  // 自动打开浏览器
        compress: true,  // 启动 gzip 压缩

        hot: true,

        // 设置代理
        proxy: {
            // 将本地 /api/xxx 代理到 localhost:3000/api/xxx
            '/api': 'http://localhost:3000',

            // 将本地 /api2/xxx 代理到 localhost:3000/xxx
            '/api2': {
                target: 'http://localhost:3000',
                pathRewrite: {
                    '/api2': ''
                }
            }
        }
    },
    // watch: true, // 开启监听,默认为 false
    // watchOptions: {
    //     ignored: /node_modules/, // 忽略哪些
    //     // 监听到变化发生后会等300ms再去执行动作,防止文件更新太快导致重新编译频率太高
    //     // 默认为 300ms
    //     aggregateTimeout: 300,
    //     // 判断文件是否发生变化是通过不停的去询问系统指定文件有没有变化实现的
    //     // 默认每隔1000毫秒询问一次
    //     poll: 1000
    // }
})

webpack.prod.js

const path = require('path')
const webpack = require('webpack')
const { smart } = require('webpack-merge')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const TerserJSPlugin = require('terser-webpack-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const HappyPack = require('happypack')
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin')
const webpackCommonConf = require('./webpack.common.js')
const { srcPath, distPath } = require('./paths')

module.exports = smart(webpackCommonConf, {
    mode: 'production',
    output: {
        // filename: 'bundle.[contentHash:8].js',  // 打包代码时,加上 hash 戳
        filename: '[name].[contentHash:8].js', // name 即多入口时 entry 的 key
        path: distPath,
        // publicPath: 'http://cdn.abc.com'  // 修改所有静态文件 url 的前缀(如 cdn 域名),这里暂时用不到
    },
    module: {
        rules: [
            // js
            {
                test: /\.js$/,
                // 把对 .js 文件的处理转交给 id 为 babel 的 HappyPack 实例
                use: ['happypack/loader?id=babel'],
                include: srcPath,
                // exclude: /node_modules/
            },
            // 图片 - 考虑 base64 编码的情况
            {
                test: /\.(png|jpg|jpeg|gif)$/,
                use: {
                    loader: 'url-loader',
                    options: {
                        // 小于 5kb 的图片用 base64 格式产出
                        // 否则,依然延用 file-loader 的形式,产出 url 格式
                        limit: 5 * 1024,

                        // 打包到 img 目录下
                        outputPath: '/img1/',

                        // 设置图片的 cdn 地址(也可以统一在外面的 output 中设置,那将作用于所有静态资源)
                        // publicPath: 'http://cdn.abc.com'
                    }
                }
            },
            // 抽离 css
            {
                test: /\.css$/,
                loader: [
                    MiniCssExtractPlugin.loader,  // 注意,这里不再用 style-loader
                    'css-loader',
                    'postcss-loader'
                ]
            },
            // 抽离 less
            {
                test: /\.less$/,
                loader: [
                    MiniCssExtractPlugin.loader,  // 注意,这里不再用 style-loader
                    'css-loader',
                    'less-loader',
                    'postcss-loader'
                ]
            }
        ]
    },
    plugins: [
        new CleanWebpackPlugin(), // 会默认清空 output.path 文件夹
        new webpack.DefinePlugin({ 
            // window.ENV = 'production'
            // 全局可以使用的变量
            ENV: JSON.stringify('production')
        }),

        // 抽离 css 文件
        new MiniCssExtractPlugin({
            filename: 'css/main.[contentHash:8].css'
        }),

        // 忽略 moment 下的 /locale 目录
        new webpack.IgnorePlugin(/\.\/locale/, /moment/),

        // happyPack 开启多进程打包
        new HappyPack({
            // 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
            id: 'babel',
            // 如何处理 .js 文件,用法和 Loader 配置中一样
            loaders: ['babel-loader?cacheDirectory']
        }),

        // 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码
        new ParallelUglifyPlugin({
            // 传递给 UglifyJS 的参数
            // (还是使用 UglifyJS 压缩,只不过帮助开启了多进程)
            uglifyJS: {
                output: {
                    beautify: false, // 最紧凑的输出
                    comments: false, // 删除所有的注释
                },
                compress: {
                    // 删除所有的 `console` 语句,可以兼容ie浏览器
                    drop_console: true,
                    // 内嵌定义了但是只用到一次的变量
                    collapse_vars: true,
                    // 提取出出现多次但是没有定义成变量去引用的静态值
                    reduce_vars: true,
                }
            }
        })
    ],

    optimization: {
        // 压缩 css
        minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],

        // 分割代码块
        splitChunks: {
            chunks: 'all',
            /**
             * initial 入口chunk,对于异步导入的文件不处理
                async 异步chunk,只对异步导入的文件处理
                all 全部chunk
             */

            // 缓存分组
            cacheGroups: {
                // 第三方模块
                vendor: {
                    name: 'vendor', // chunk 名称
                    priority: 1, // 权限更高,优先抽离,重要!!!
                    test: /node_modules/,
                    minSize: 0,  // 大小限制
                    minChunks: 1  // 最少复用过几次
                },

                // 公共的模块
                common: {
                    name: 'common', // chunk 名称
                    priority: 0, // 优先级
                    minSize: 0,  // 公共模块的大小限制
                    minChunks: 2  // 公共模块最少复用过几次
                }
            }
        }
    }
})

@babel 一个组 @bable/preset-env babel的很多配置插件的一个集合配置 webpack配置文件中只需引入babel-loader,其他babel的配置写到.babelrc中 .babelrc

{
    "presets": ["@babel/preset-env"],
    "plugins": []
}

babel-loader是babel给webpack提供的插件,真正做转译的是babel/core presets 是很多 plugin的集合 preset-env 转 es6 preset-react 转jsx preset-typescript 转ts

(二)loader 模块转换器

css也是一个模块 先用css-loader把css模块引进来 在用 style-loader把css插入html代码
post-css 编译成兼容所有浏览器的css

image 大小 小的用base64 url-loader image 图片命名 file-loader

(三)plugin 扩展插件

(四)module

执行顺序

当我们在 webpack 中集成 eslint 和 babel 的时候,一般会采用如下的写法:

module.exports = {
	// ...
	module: {
		rules: [
			{
				test: /\.js$/,
				exclude: /node_modules/,
				use: ['babel-loader', 'eslint-loader']
			}
		]
	}
	// ...

这种写法需要注意 use 中的执行顺序是从右往左,一旦写反了,eslint 就会校验 babel 转换后的代码。

如果你对 use 的执行顺序不清楚,可以采用下面的写法:


module.exports = {
	// ...
	module: {
		rules: [
			{
				enforce: 'pre',
				test: /\.js$/,
				exclude: /node_modules/,
				loader: 'eslint-loader'
			},
			{
				test: /\.js$/,
				exclude: /node_modules/,
				loader: 'babel-loader'
			}
		]
	}
	// ...
}

可以通过一个 enforce 属性,默认有以下几个值

1. pre 优先处理
2. normal 正常处理(默认)
3. inline 其次处理
4. post 最后处理

(五) module chunk boundle的区别

module--各个源码文件,webpack中一切皆模块

chunk--多个模块合并成的, entry、import、splitChunk

boundle -- 最终输出的文件

(六)、 Resolve

Webpack 在启动后会从配置的入口模块出发找出所有依赖的模块,Resolve 配置 Webpack 如何寻找模块所对应的文件。 Webpack 内置 JavaScript 模块化语法解析功能,默认会采用模块化标准里约定好的规则去寻找,但你也可以根据自己的需要修改默认的规则。

alias

 resolve.alias  配置项通过别名来把原导入路径映射成一个新的导入路径。例如使用以下配置:

// Webpack alias 配置
resolve:{
  alias:{
    components: './src/components/'
  }
}

当你通过  import Button from 'components/button 导入时,实际上被 alias 等价替换成了  import Button from './src/components/button' 。

以上 alias 配置的含义是把导入语句里的  components  关键字替换成  ./src/components/ 。

这样做可能会命中太多的导入语句,alias 还支持 $ 符号来缩小范围到只命中以关键字结尾的导入语句:

resolve:{
  alias:{
    'react$': '/path/to/react.min.js'
  }
}

 react$  只会命中以  react  结尾的导入语句,即只会把  import 'react'  关键字替换成  import '/path/to/react.min.js' 。

mainFields

有一些第三方模块会针对不同环境提供几分代码。 例如分别提供采用 ES5 和 ES6 的2份代码,这2份代码的位置写在  package.json  文件里,如下:

{
  "jsnext:main": "es/index.js",// 采用 ES6 语法的代码入口文件
  "main": "lib/index.js" // 采用 ES5 语法的代码入口文件
}

Webpack 会根据  mainFields  的配置去决定优先采用那份代码, mainFields  默认如下:

mainFields: ['browser', 'main']

Webpack 会按照数组里的顺序去 package.json  文件里寻找,只会使用找到的第一个。

假如你想优先采用 ES6 的那份代码,可以这样配置:

mainFields: ['jsnext:main', 'browser', 'main']
extensions

在导入语句没带文件后缀时,Webpack 会自动带上后缀后去尝试访问文件是否存在。  resolve.extensions 用于配置在尝试过程中用到的后缀列表,默认是:

extensions: ['.js', '.json']

也就是说当遇到  require('./data')  这样的导入语句时,Webpack 会先去寻找  ./data.js  文件,如果该文件不存在就去寻找  ./data.json  文件, 如果还是找不到就报错。

假如你想让 Webpack 优先使用目录下的 TypeScript 文件,可以这样配置:

extensions: ['.ts', '.js', '.json']
modules

 resolve.modules  配置 Webpack 去哪些目录下寻找第三方模块,默认是只会去  node_modules  目录下寻找。 有时你的项目里会有一些模块会大量被其它模块依赖和导入,由于其它模块的位置分布不定,针对不同的文件都要去计算被导入模块文件的相对路径, 这个路径有时候会很长,就像这样  import '../../../components/button'  这时你可以利用  modules  配置项优化,假如那些被大量导入的模块都在  ./src/components  目录下,把  modules  配置成

modules:['./src/components','node_modules']

后,你可以简单通过  import 'button'  导入。

descriptionFiles

 resolve.descriptionFiles  配置描述第三方模块的文件名称,也就是  package.json  文件。默认如下:

descriptionFiles: ['package.json']
enforceExtension

 resolve.enforceExtension  如果配置为  true  所有导入语句都必须要带文件后缀, 例如开启前  import './foo'  能正常工作,开启后就必须写成  import './foo.js' 。

enforceModuleExtension

 enforceModuleExtension  和  enforceExtension  作用类似,但  enforceModuleExtension  只对  node_modules  下的模块生效。  enforceModuleExtension  通常搭配  enforceExtension  使用,在  enforceExtension:true  时,因为安装的第三方模块中大多数导入语句没带文件后缀, 所以这时通过配置  enforceModuleExtension:false  来兼容第三方模块。

(七)、 performance

编译时警告 entrypoint size limit 配置如何展示性能提示。例如,如果一个资源超过 250kb,webpack 会对此输出一个警告来通知你。

//不展示警告或错误提示。
performance: {
  hints: false
}

(八) webpack打包---报错内存溢出javaScript heap out of memory

CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory JavaScript堆内存不足,这里说的 JavaScript 其实就是 Node,我们都知道 Node 是基于V8引擎,在一般的后端开发语言中,在基本的内存使用上没有什么限制,但是我去查阅了相关的资料才发现,在 Node 中通过 JavaScript 使用内存时只能使用部分内存(64位系统下约为1.4 GB,32位系统下约为0.7 GB),这就是我们编译项目时为什么会出现内存泄露了,因为前端项目如果非常的庞大,webpack 编译时就会占用很多的系统资源,如果超出了V8对 Node 默认的内存限制大小就会出现刚刚我截图的那个错误了,那怎么解决呢?V8依然提供了选项让我们使用更多的内存。Node 在启动时可以传递 --max-old-space-size 或 --max-new-space-size 来调整内存大小的使用限制。

1、修改

\node_modules.bin\webpack-dev-server.cmd 添加--max_old_space_size=32768

@IF EXIST "%~dp0\node.exe" (
  "%~dp0\node.exe"  "%~dp0\..\webpack-dev-server\bin\webpack-dev-server.js" %*
) ELSE (
  @SETLOCAL
  @SET PATHEXT=%PATHEXT:;.JS;=;%
  node --max_old_space_size=32768  "%~dp0\..\webpack-dev-server\bin\webpack-dev-server.js" %*
)

(九) webpack性能优化 --构建速度

1、优化babel-loader

 module: {
        rules: [
            {
                test: /\.js$/,
                loader: ['babel-loader?cacheDirectory'], //开启缓存
                include: srcPath, //明确范围
                // exclude: /node_modules/
            },

2、IgnorePlugin

 // 忽略 moment 下的 /locale 目录
        new webpack.IgnorePlugin(/\.\/locale/, /moment/),

3、noParse

module: {
 // 独立完整的‘react.min.js’文件就没有采用模块化
 // 忽略对‘react.min.js’文件的递归解析处理
 noParse: [/react\.min\.js$/]
}

4、happyPack 开启多线程打包

5、ParalleUglifyPlugin 多进程压缩js 多进程都要按需使用,如果项目很大可以开启,如果项目不大就不用,开启这个也浪费性能

6、自动刷新

7、热更新

8、DllPlugin动态链接插件库

前端框架如vue React,体积大,构建慢,较稳定,不常升级版本,同一个版本只构建一次即可,不用每次都重新构建

(十) webpack 性能优化--产出代码

1、体积小

2、合理分包,不重复加载

3、速度更快,内存使用更少

4、小图片 base64编码

5、boundle加hash

6、懒加载

7、提取公共代码

8、IngorePlugin

9、使用CDN加速

10、使用mode:“production” 自动开启代码压缩,Vue、React等会自动删掉调试代码,启动tree-shaking和scope-hoisting,其他不会优化

11、scope Hosting 代码模块合并

(十一)ES6 module和 Commonjs的区别

1、es6 module静态引入,编译时引入 import 是能在模块的最顶上,不能按后来的条件按需引入

2、Commonjs 动态引入,执行时引入 require

3、只有ES6Module才能静态分析,实现Tree-shaking

(十二)babel

1、babel-polyfill(polyfill 补丁)

已经不推荐使用 可以直接用core-js和genetator。 babel-polyfill会污染环境,会生成i个Promise函数,后面代码使用Promise就不会报错

2、core-js 集成了很多es6 es7语法的polyfill 但core-js不支持 gennerator语法 但有个regenerator库

3、babel-runtime 对表babel-polyfill 但是不会污染全局环境例如 生成Promise1,后面的使用Promise都改成Promise1,产出第三方lib 要用babel-runtime

(十三)定义全局变量

package.json

 "dev:e": "cross-env WEB_ENV=electron webpack-dev-server --config config/webpack.dev.config.js",
    "build": "cross-env NODE_ENV=production webpack --config config/webpack.prod.config.js",

或者 webapck.json

  new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: JSON.stringify('production'),
      },
      WEB_ENV: JSON.stringify(process.env.WEB_ENV),
    }),

xxx.jsx

if (process.env.NODE_ENV !== 'production') {
    middlewares.push(createLogger());
  }

(十四) 使用 depcheck 清理 package.json 中用不到的安装包

安装环境必须为 node.js >= 10

npm install -g depcheck

使用: depcheck <你的目录>

$ depcheck ./
Unused dependencies  
* csurf
Unused devDependencies  
* moment

cnblogs.com/chengxs/p/11022842.html

(十五)、devtool

// js 文件sourceMap
devtool: 'source-map'
// 'cheap-module-eval-source-map'  时 会打不到index.vue文件里
// 'eval-source-map' 可以正常打断点
// css 文件 sourceMap 'style-loader', 'css-loader?sourceMap', 'sass-loader?sourceMap'

会生成 map文件

RvoxpLbXGb.png

浏览器展示加载js文件。开发人员打断点的时候会打进js.map文件,这里可以看到vue原始文件,方便调代码

segmentfault.com/a/119000001…

zhuanlan.zhihu.com/p/615063944