webpack从入门到进阶(二)

499 阅读10分钟

1、webpack进阶

整个目录结构为

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');

module.exports = {
	mode: 'development',
	devtool: 'cheap-module-eval-source-map',
	// mode: 'production',
	// devtool: 'cheap-module-source-map',
	entry: {
		main: './src/index.js'
	},
	devServer: {
		contentBase: './dist',
		open: true,
		port: 8080,
		hot: true,
		hotOnly: true
	},
	module: {
		rules: [{ 
			test: /\.js$/, 
			exclude: /node_modules/, 
			loader: 'babel-loader',
		}, {
			test: /\.(jpg|png|gif)$/,
			use: {
				loader: 'url-loader',
				options: {
					name: '[name]_[hash].[ext]',
					outputPath: 'images/',
					limit: 10240
				}
			} 
		}, {
			test: /\.(eot|ttf|svg)$/,
			use: {
				loader: 'file-loader'
			} 
		}, {
			test: /\.scss$/,
			use: [
				'style-loader', 
				{
					loader: 'css-loader',
					options: {
						importLoaders: 2
					}
				},
				'sass-loader',
				'postcss-loader'
			]
		}, {
			test: /\.css$/,
			use: [
				'style-loader',
				'css-loader',
				'postcss-loader'
			]
		}]
	},
	plugins: [
		new HtmlWebpackPlugin({
			template: 'src/index.html'
		}), 
		new CleanWebpackPlugin(['dist']),
		new webpack.HotModuleReplacementPlugin()
	],
	optimization: {
		usedExports: true
	},
	output: {
		filename: '[name].js',
		path: path.resolve(__dirname, 'dist')
	}
}

1-1 Tree Shaking

当我们写了一个math.js,导出add和minus两个方法,但是index.js只导入add一个方法,我们可以通过摇树,让打包的文件中自动删除没有导入得minus的代码,但是两种模式developmentproduction,稍微有些区别, 在development模式下需要额外配置optimization: {usedExports: true},并且在package.json配置"sideEffects": false表明所有的都采用Tree Shaking

由于import '@/babel/polyfill' import './style.css'这种本身就没有导入,Tree Shaking会完全忽略它们,也可以配置"sideEffects": ['@/babel/polyfill','*.css']表明遇到他们不采用Tree Shaking

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');

module.exports = {
	mode: 'development',
	devtool: 'cheap-module-eval-source-map',
	// mode: 'production',
	// devtool: 'cheap-module-source-map',
	...
	
	module: {
		...
	},
	plugins: [
		...
	],
	optimization: {
		usedExports: true
	},
	...
}
// math.js
export const add = (a, b) => {
	console.log( a + b );
}

export const minus = (a, b) => {
	console.log( a - b );
}
// index.js
// Tree Shaking 只支持 ES Module
// ES Module底层是静态导入
// CommonJs 底层是动态导入

import { add } from './math.js';
add(1, 2);

通过 npm run bundle 得到的打包文件中会有

export provided: add minus
export used: add

development模式下只会提示,但是仍然会打包所有代码,这是因为如果自动删除了没导入的代码,可能会导致源文件和打包后文件sourceMap对应不上,不方便调试

production模式只需要配置sideEffects,就可以真正做到Tree Shaking

1-2 development and production

通常情况下开发环境和生成环境webpack配置是不一样的,比如在Tree Shaking时,开发环境才需要配置optimization: {usedExports: true},devserver也是如此,devtool两者也不一样

那么我们每次打包需要手动更改webpack.config,js文件,我想死,谁也别拦我😭 因此,我们可以配置两个config.js文件

// package.json
{
	"scripts": {
      "dev": "webpack-dev-server --config ./webpack.dev.js",
      "build": "webpack --config ./webpack.prod.js"
    },
}

还可以优化,将webpack.dev.js 和 webpack.prod.js 公共的配置抽离到webpack.common.js,并新建一个build文件夹,将三个文件放在build下(有没有很像vue2的目录结构😋)

webpack.common.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = {
	entry: {
		main: './src/index.js'
	},
	module: {
		rules: [{ 
			test: /\.js$/, 
			exclude: /node_modules/, 
			loader: 'babel-loader',
		}, {
			test: /\.(jpg|png|gif)$/,
			use: {
				loader: 'url-loader',
				options: {
					name: '[name]_[hash].[ext]',
					outputPath: 'images/',
					limit: 10240
				}
			} 
		}, {
			test: /\.(eot|ttf|svg)$/,
			use: {
				loader: 'file-loader'
			} 
		}, {
			test: /\.scss$/,
			use: [
				'style-loader', 
				{
					loader: 'css-loader',
					options: {
						importLoaders: 2
					}
				},
				'sass-loader',
				'postcss-loader'
			]
		}, {
			test: /\.css$/,
			use: [
				'style-loader',
				'css-loader',
				'postcss-loader'
			]
		}]
	},
	plugins: [
		new HtmlWebpackPlugin({
			template: 'src/index.html'
		}), 
		new CleanWebpackPlugin(['dist'],{
        	{
                root: path.resolve(__dirname, '../')   //❗ 地址改成build同级
            }
        })
	],
	output: {
		filename: '[name].js',
        // path: path.resolve(__dirname, '../', 'dist')   //❗ 生成打包文件地址改成build同级
		path: path.resolve(__dirname, '../dist')   //❗ 生成打包文件地址改成build同级
	}
}

webpack.dev.js

const webpack = require('webpack');
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common.js');

const devConfig = {
	mode: 'development',
	devtool: 'cheap-module-eval-source-map',
	devServer: {
		contentBase: './dist',
		open: true,
		port: 8080,
		hot: true
	},
	plugins: [
		new webpack.HotModuleReplacementPlugin()
	],
	optimization: {
		usedExports: true
	}
}

module.exports = merge(commonConfig, devConfig);

webpack.prod.js

const merge = require('webpack-merge');
const commonConfig = require('./webpack.common.js');

const prodConfig = {
	mode: 'production',
	devtool: 'cheap-module-source-map'
}

module.exports = merge(commonConfig, prodConfig);
// package.json
{
    "scripts": {
      "dev": "webpack-dev-server --config ./build/webpack.dev.js",
      "build": "webpack --config ./build/webpack.prod.js"
    },
}

npm run dev 用于开发环境

npm run build 用于生产环境

1-3 Code Splitting

webpack 默认会将所有的模块打包到一个文件中,如 main.js 。

有时,某些第三方模块变动较少,如:jQuery、lodash 。我们可以将这些模块打包到单独的文件中。

  • 1、通过手动分割
// lodash.js
import _ from 'lodash'
window._ = _
// index.js
console.log(_.join(['a','b','c'],'---'))

然后以之前提到的配置多入口的方式进行打包,通过HtmlWebpackPlugin将两个文件引入到html中

module.exports = {
   entry: {
   	lodash: './src/lodash.js',
   	main: './src/index.js'
   },
   module: {
   	rules: [{ 
   		test: /\.js$/, 
   		exclude: /node_modules/, 
   		loader: 'babel-loader',
   	}]
   },
   plugins: [
   	new HtmlWebpackPlugin({
   		template: 'src/index.html'
   	}), 
   	new CleanWebpackPlugin(['dist'],{
       	{
               root: path.resolve(__dirname, '../')   //❗ 地址改成build同级
           }
       })
   ],
   output: {
   	filename: '[name].js',
   	path: path.resolve(__dirname, '../dist')   //❗ 生成打包文件地址改成build同级
   }
}
  • 2、import _ from 'lodash' 同步模块

只需要配置optimization即可

// index.js
import _ from 'lodash'
console.log(_.join(['a','b','c'],'---'))
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = {
	entry: {
		main: './src/index.js'
	},
	module: {
		rules: [{ 
			test: /\.js$/, 
			exclude: /node_modules/, 
			loader: 'babel-loader',
		}]
	},
	plugins: [
		new HtmlWebpackPlugin({
			template: 'src/index.html'
		}), 
		new CleanWebpackPlugin(['dist'], {
			root: path.resolve(__dirname, '../')
		})
	],
	optimization: {
		splitChunks: {
			chunks: 'all'
		}
	},
	output: {
		filename: '[name].js',
		path: path.resolve(__dirname, '../dist')
	}
}

打包出来main.js 和 vendors~main.js,并引入到html中

  • 3、异步模块
// index.js
function getComponent() {
	return import('lodash').then(({ default: _ }) => {
		var element = document.createElement('div');
		element.innerHTML = _.join(['a', 'b', 'c'], '---');
		return element;
	})
}

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

无需做任何配置,会自动进行代码分割,放置到新的文件中main.js 和 0.js

/* webpackChunkName: "jquery" */ 称为魔法注释 magic comment,它可以指定打包生成的模块的文件名。需要使用@babel/plugin-syntax-dynamic-import才可以使用magic comment

// .babelrc
{
	presets: [
		[
			"@babel/preset-env", {
				targets: {
					chrome: "67",
				},
				useBuiltIns: 'usage'
			}
		],
		"@babel/preset-react"
	],
	plugins: ["@babel/plugin-syntax-dynamic-import"]
}

1-4 Lazy Loading

懒加载并不是webpack当中的概念,而是ES中的概念

这种就是一次性加载所有的代码

// index.js
import _ from 'lodash'

document.addEventListener('click', () => {
  var element = document.createElement('div')
  element.innerHTML = _.join(['hello', 'world'], '-')
  document.body.appendChild(element)
})

下面这种异步代码的写法可以实现一种懒加载的行为,在点击界面的时候才会去加载需要的模块,效果就是我们在页面中,开始只会加载一个main.js,然后点击一下页面会在加载一个loadsh函数,调用这个函数的某些方法我们实现了一个字符串的拼接过程,最终呈现在了页面上。

function getComponent() {
  return import(/* webpackChunkName: "lodash" */ 'lodash').then(
    ({ default: _ }) => {
      var element = document.createElement('div')
      element.innerHTML = _.join(['hello', 'world'], '-')
      return element
    }
  )
}

document.addEventListener('click', () => {
  getComponent().then(element => {
    document.body.appendChild(element)
  })
})

使用 ES7 的 async 和 await 后,上面代码可以改写成下面这种写法,效果等同

async function getComponent() {
  const { default: _ } = await import(/* webpackChunkName: "lodash" */ 'lodash')
  const element = document.createElement('div')
  element.innerHTML = _.join(['hello', 'world'], '-')
  return element
}

document.addEventListener('click', () => {
  getComponent().then(element => {
    document.body.appendChild(element)
  })
})

1-5 打包分析

首先修改下package.json

// package.json
"scripts": {
  "dev": "webpack --profile --json > stats.json --config webpack.dev.js"
}

npm run dev生成stats.json,对打包过程的描述文件

webpack分析工具的git藏仓库地址

其他分析工具地址

1-6 preloading prefetching

写在前面,chrome浏览器有一个查看code coverage的功能,也就是F12后ctrl+shift+p,输入coverage查看代码的使用率

先来一段最原始的代码

// index.js
document.addEventListener('click', () => {
  var element = document.createElement('div')
  element.innerHTML = 'nice day!'
  document.body.appendChild(element)
})

打包之后click的function中的代码是没有使用的,修改一下

// index.js
document.addEventListener('click', () =>{
	import('./click.js').then(({default: func}) => {
		func();
	})
});

// click.js
function handleClick() {
	const element = document.createElement('div');
	element.innerHTML = 'Dell Lee';
	document.body.appendChild(element);
}

export default handleClick;

这样修改之后代码的利用率就高一些,也节约首屏加载的时间

所以 webpack 做代码分割打包配置时 chunks 的默认是 async,而不是 all 或者 initial;因为 webpack 认为只有异步加载这样的组件才能真正的提高网页的打包性能,而同步的代码只能增加一个缓存,但是第一次还是需要加载很多资源,实际上对性能的提升是非常有限的

鉴于此,我们还是希望能够通过异步加载的方式,来加载我们的模块代码,但是又怕加载很慢。比如,我们加载网站首页的时候,可以通过异步加载登录模态框模块,但是又怕点击登录,模态框模块很慢,于是就有了prefetching网络空闲去加载异步代码 preloading和主业务文件一起加载的

/* webpackPrefetch: true */
/* webpackPreload: true */

import(/* webpackPrefetch: true */'./click.js')

这样就可以在网站首页加载完成,带宽释放出来之后,默默为我们加载登录模态框模块的代码,既满足了首页加载快的需求,又满足了登录加载快的需求

1-7 CSS文件的代码分割

之前提到过style-loader是将css-loader解析的css插入到hede的<style>中,现在我需要也在dist下生成css文件

旧版本的 MiniCssExtractPlugin 因为不支持HMR,所以最好只在生产环境中使用,如果放在开发环境中,更改样式后需要手动刷新页面,会降低开发的效率;新版本已支持开发环境中使用HMR

// webpack.common.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
        filename: '[name].css',
        chunkFilename: '[name].chunk.css'
    })
  ]
}
// index.js
import './style.css'
console.log("css")

// style.css
body{background:red}

其中如果是被html直接引用的,会走filename,间接就是chunkFilename,打包之后dist/main.css main.css.map

// index.js
import './style.css'
import './style1.css'
console.log("css")

// style.css
body{background:red}

// style.css
body{font-size:20px}

打包之后main.css

body{background:red}
body{font-size:20px}

/*# sourceMappingURL=main.css.map*/
  • OptimizeCSSAssetsPlugin
// webpack.common.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader'
        ]
      }
    ]
  },
  optimization: {
      minimizer: [new OptimizeCSSAssetsPlugin({})]
  },
  plugins: [
    new MiniCssExtractPlugin({
        filename: '[name].css',
        chunkFilename: '[name].chunk.css'
    })
  ]
}

打包之后main.css

body{background:red;font-size:20px}
  • 一个入口文件引入多个 css 文件,默认将其打包合并到一个 css 里
  • 多个入口文件引入不同的 css 文件,打包默认会产生多个 css 文件,可通过配置,使其合并为一个 css 文件
optimization: {
    splitChunks: {
      cacheGroups: {
        styles: {
          name: 'styles', // 打包后的文件名
          test: /\.css$/, // 匹配所有 .css 结尾的文件,将其放到该组进行打包
          chunks: 'all', // 不管是同步还是异步加载的,都打包到该组
          enforce: true // 忽略默认的一些参数(比如minSize/maxSize/...)
        }
      }
    }
  }
  • 多个入口文件引入多个 css 文件的打包

走你extracting-css-based-on-entry

1-8 webpack与浏览器缓存

之前打包的文件名字都一样,浏览器已经有了缓存,现在工程化已经可以帮我们实现类似数据签名的东西,文件内容改变hash改变,这样浏览器就会读取新内容 abc.agagjlagjlkhjlj23.js

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

这样引入的第三方库的hash不会变,浏览器就可以使用缓存,提高网站加载效率

但是在老版本中,因为mainfset的原因,在main.js和vendor第三方库之间的关联代码叫manifest,打包mainfset不同,可能导致打包出来的文件内容变化,hash就不同。

可以多加一些配置

optimization: {
    runtimeChunk: {
        name: 'runtime'  //将mainfest打包到单独的js文件中 runtime.ajalgjalg.js
    },
    usedExports: true,
    splitChunks: {
  		chunks: 'all',
  		cacheGroups: {
    		vendors: {
        	test: /[\\/]node_modules[\\/]/,
        	priority: -10,
        	name: 'vendors',
    	}
  }
},
performance: false   //不提示性能的信息

1-9 Shimming

webpack是基于模块化打包的,我们知道,在一个模块中的数据,在其他模块是访问不到了

// index.js
import $ from 'jquery'
import _ from 'lodash'
import { ui } from './jq_dh.ui'

ui()
$(body).html("abcd")

// jq_dh.ui.js
export function ui(){
  $('body').css('background',_.join(['r','e','d'],''))
}

在jq_dh.ui.js中 $-找不到的,会报错,那么我们可以通过webpack提供的一个插件ProvidePlugin

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');

module.exports = {
	entry: {
		main: './src/index.js',
	},
	module: {
		rules: [{ 
			test: /\.js$/, 
			exclude: /node_modules/,
			use: [{
				loader: 'babel-loader'
			}, {
				loader: 'imports-loader?this=>window'   //模块this指向window
			}]
		}, {
			test: /\.(jpg|png|gif)$/,
			use: {
				loader: 'url-loader',
				options: {
					name: '[name]_[hash].[ext]',
					outputPath: 'images/',
					limit: 10240
				}
			} 
		}, {
			test: /\.(eot|ttf|svg)$/,
			use: {
				loader: 'file-loader'
			} 
		}]
	},
	plugins: [
		new HtmlWebpackPlugin({
			template: 'src/index.html'
		}), 
		new CleanWebpackPlugin(['dist'], {
			root: path.resolve(__dirname, '../')
		}),
		new webpack.ProvidePlugin({
			$: 'jquery',  //模块中有使用$帮引入jquery 
            _: 'lodash',  //模块中有使用_帮引入lodash
			_join: ['lodash', 'join']
		}),
	],
	optimization: {
		runtimeChunk: {
			name: 'runtime'
		},
		usedExports: true,
		splitChunks: {
	      chunks: 'all',
	      cacheGroups: {
	      	vendors: {
	      		test: /[\\/]node_modules[\\/]/,
	      		priority: -10,
	      		name: 'vendors',
	      	}
	      }
	    }
	},
	performance: false,
	output: {
		path: path.resolve(__dirname, '../dist')
	}
}

这样就可以实现Shimming垫片的功能 还有一个需求,模块中this都默认指向模块本身

// index.js
console.log(this === window)  // false

可以通过imports-loader来实现将this指向window

webpack guides

1-10 环境变量

之前提到通过不同的配置文件来打包,将公共的抽离出来放在webpack.common.js当中

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

也可以通过环境变量的使用,通过环境变量去判断导出merge(commonConfig,devConfig)还是merge(commonConfig,prodConfig)

// webpack.common.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');
const merge = require('webpack-merge');
const devConfig = require('./webpack.dev.js');
const prodConfig = require('./webpack.prod.js');

const commonConfig = {
    entry: {
        main: './src/index.js'
    },
    module: {
        rules: [{ 
            test: /\.js$/, 
            exclude: /node_modules/, 
            loader: 'babel-loader',
        }, {
            test: /\.(jpg|png|gif)$/,
            use: {
                loader: 'url-loader',
                options: {
                    name: '[name]_[hash].[ext]',
                    outputPath: 'images/',
                    limit: 10240
                }
            } 
        }, {
            test: /\.(eot|ttf|svg)$/,
            use: {
                loader: 'file-loader'
            } 
        }, {
            test: /\.scss$/,
            use: [
                'style-loader', 
                {
                    loader: 'css-loader',
                    options: {
                        importLoaders: 2
                    }
                },
                'sass-loader',
                'postcss-loader'
            ]
        }, {
            test: /\.css$/,
            use: [
                'style-loader',
                'css-loader',
                'postcss-loader'
            ]
        }]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: 'src/index.html'
        }), 
        new CleanWebpackPlugin(['dist'],{
            {
                root: path.resolve(__dirname, '../')   //❗ 地址改成build同级
            }
        })
    ],
    output: {
        filename: '[name].js',
        // path: path.resolve(__dirname, '../', 'dist')   //❗ 生成打包文件地址改成build同级
        path: path.resolve(__dirname, '../dist')   //❗ 生成打包文件地址改成build同级
    }
}
module.exports = (env) => {
	if(env && env.production) {
		return merge(commonConfig, prodConfig);
	}else {
		return merge(commonConfig, devConfig);
	}
}
// webpack.dev.js
const webpack = require('webpack');

const devConfig = {
    mode: 'development',
    devtool: 'cheap-module-eval-source-map',
    devServer: {
        contentBase: './dist',
        open: true,
        port: 8080,
        hot: true
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin()
    ],
    optimization: {
        usedExports: true
    }
}
module.exports = devConfig

// webpack.prod.js
const prodConfig = {
    mode: 'production',
    devtool: 'cheap-module-source-map'
}

module.exports = prodConfig;

在package.json中传入不同的环境变量

"scripts": {
    "dev-build": "webpack --config ./build/webpack.common.js",
    "dev": "webpack-dev-server --config ./build/webpack.common.js",
    "build": "webpack --env.production --config ./build/webpack.common.js"
  },

2、webpack实战

2-1 打包库文件

需求:想写一个自己的库文件

// index.js
import * as math from './math';
import * as string from './string';

export default { math, string }

// math.js
export function add(a, b) {
	return a + b;
}
export function minus(a, b) {
	return a - b;
}
export function multiply(a, b) {
	return a * b;
}
export function division(a, b) {
	return a / b;
}

// string.js
export function join(a, b) {
	return a + ' ' + b ;
}
const path = require('path');

module.exports = {
	mode: 'production',
	entry: './src/index.js',
	output: {
		path: path.resolve(__dirname, 'dist'),
		filename: 'libralry.js',
		library: 'root',
		libraryTarget: 'umd'
	}
}

library: 'root'可以用过<script>引入使用,并在全局挂载一个root变量

libraryTarget: 'umd'使用umd规范,可以通过ES Module CommonJs AMD CMD引入使用,还有其他值this->this.library global->global.library window->window.library

另外的需求:string.js中引入了lodash,但是在使用libralry.js时已经引入过lodash,这样就会打包两次lodash,可以通过配置externals

// string.js
import _ from 'lodash';

export function join(a, b) {
	return _.join([a, b], ' ');
}
import _ from 'lodash'
import libralry from 'libralry'
const path = require('path');

module.exports = {
	mode: 'production',
	entry: './src/index.js',
	externals: ['lodash'],
    // externals: {
  //   lodash: {
  //     root: '_', // 通过全局script标签引入,并在页面中注入_为全局变量的lodash
  //     commonjs: 'lodash'  //通过模块引入 导入名也为lodash
  //   }
  // }, // 第二种配置方式
	output: {
		path: path.resolve(__dirname, 'dist'),
		filename: 'library.js',
		library: 'root',
		libraryTarget: 'umd'
	}
}

2-2 PWA

Progressive Web Application 渐进式web应用程序,但我们第一次访问我们的应用时,后续不管服务器是不是宕机,都可以继续访问到

// index.js
console.log('hello, this is dell');

if ('serviceWorker' in navigator) {
	window.addEventListener('load', () => {
		navigator.serviceWorker.register('/service-worker.js')
			.then(registration => {
				console.log('service-worker registed');
			}).catch(error => {
				console.log('service-worker register error');
			})
	})
}

我们可以只用在webpack.prod.js中配置pwa,关注线上的体验,开发时不用管

const WorkboxPlugin = require('workbox-webpack-plugin');

plugins: [
    new MiniCssExtractPlugin({
        filename: '[name].css',
        chunkFilename: '[name].chunk.css'
    }),
    new WorkboxPlugin.GenerateSW({
        clientsClaim: true,
        skipWaiting: true
    })
]

使用 npm install http-server --save-dev开启一个简单的服务器

script:{
	"start": "http-server dist"
}

2-3 TypeScript

TypeScript是javaScript的超集,增加了类型检测系统,可以说把js从一个动态弱类型,转变成一个静态强类型吧

需要安装ts-loader typescript

const path = require('path');

module.exports = {
	mode: 'production',
	entry: './src/index.tsx',
	module: {
		rules: [{
			test: /\.tsx?$/,
			use: 'ts-loader',
			exclude: /node_modules/
		}]
	},
	output: {
		filename: 'bundle.js',
		path: path.resolve(__dirname, 'dist')
	}
}

还要在根目录下创建tsconfig.json

{
	"compilerOpitons": {
		"outDir": "./dist",
		"module": "es6",
		"target": "es5",
		"allowJs": true,
	}
}

但是需要注意一点,我们在业务代码中引入了jquery、lodash这样的模块,ts可能不会给我们检测,我们需要格外的去安装相应的类型文件@types/jquery``@types/lodash,可以从 TypeSearch 中找到并安装这些第三方库的类型声明文件

2-4 devServer请求转发

devServer: {
    contentBase: './dist',
    open: true,
    port: 8080,
    hot: true,
    hotOnly: true,
    proxy: {
        '/api': {
            target: 'localhost:3000',
            secure: false,  //https
            pathRewrite: {'^/api' : ''}
            changeOrigin: true,  //有些网站对origin做了限制,防止爬虫
            headers: {
                host: 'localhost:8080',
                cookie: 'cookie'
            }
        }
    }
}

2-5 webpack中ESlint

使用ESlint方式

  • 使用 npx eslint src命令行查看
  • 使用 vscode中安装eslint插件
// .eslintrc.js
module.exports = {
	"extends": "airbnb",
    "parser": "babel-eslint",
    "rules": {
    	"react/prefer-stateless-function": 0,  //无状态组件写成函数式
        "react/jsx-filename-extension": 0   // jsx
    },
    globals: {
    	document: false  //document不允许被覆盖
    }
}
  • 使用eslint-loader 结合 overlay:true
module.exports = {
	entry: {
    	main: 'src/index.js'
    },
    devServer: {
    	overlay: true
        contentBase: './dist',
        open: true,
        port: 8080,
        hot: true
    }
    module: {
        rules: [{
            test: /\.js$/,
            exclude: /node_modules/,
            // loader: 'babel-loader',
            // use: ['babel-loader', 'eslint-loader'],
            use: [{
            	loader: 'eslint-loader',
                options: {
                	fix: true
                },
                force: 'pre' 
            }, 'babel-loader']
            
        }]
    },
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
    }
}

第三种方式,不用依赖编辑器,也不用看命令行,是很好的一种方式,但是增加loader势必会降低打包的速度

因此有的会使用 git上传中的钩子去执行eslint,有问题不让上传,自己去改好才能上传,这就需要大家去权衡了

2-6 webpack性能优化

  • 跟上技术的迭代(node npm yarn webpack)
  • 尽可能少使用loader,合理使用exclude 和 include
  • plugins尽可能精简并且可靠
  • resolve参数合理配置
// webpack.config.js
module.exports = {
	resolve:{
    	extensions: ['.js', '.jsx'],
   		mainFiles: ['index', 'child'],
        alias: {
        	@: path.resolve(__dirname, './src/login')
        }
    }
}
  • DllPlugin提高打包速度

实现第三方包只打包一次,在我们项目中引入一些库,我们不想每次都打包他们,只需要打包一次 首先我们再build下新建webpack.dll.js,把第三方库单独进行打包到vendors.dll.js中,

// webpack.dll.js
const path = require('path');
const webpack = require('webpack');

module.exports = {
	mode: 'production',
	entry: {
		vendors: ['lodash'],
		react: ['react', 'react-dom'],
		jquery: ['jquery']
	},
	output: {
		filename: '[name].dll.js',
		path: path.resolve(__dirname, '../dll'),
		library: '[name]'  // //打包生成的库名,通过全局变量的形式暴露到全局
	},
	plugins: [
		new webpack.DllPlugin({  //对暴露到全局的代码进行分析,生成vendors.manifest.json 的映射文件
			name: '[name]',
			path: path.resolve(__dirname, '../dll/[name].manifest.json'),
		})
	]
}
// webpack.common.js
const path = require('path');
const fs = require('fs');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
const webpack = require('webpack');

const plugins = [
	new HtmlWebpackPlugin({
		template: 'src/index.html'
	}), 
	new CleanWebpackPlugin(['dist'], {
		root: path.resolve(__dirname, '../')
	})
];

const files = fs.readdirSync(path.resolve(__dirname, '../dll'));
files.forEach(file => {
	if(/.*\.dll.js/.test(file)) {
		plugins.push(new AddAssetHtmlWebpackPlugin({
			filepath: path.resolve(__dirname, '../dll', file)
		}))
	}
	if(/.*\.manifest.json/.test(file)) {
		plugins.push(new webpack.DllReferencePlugin({
			manifmest: path.resolve(__dirname, '../dll', file)
		}))
	}
})

module.exports = {
	entry: {
		main: './src/index.js',
	},
	resolve: {
		extensions: ['.js', '.jsx'],
	},
	module: {
		rules: [{ 
			test: /\.jsx?$/, 
			include: path.resolve(__dirname, '../src'),
			use: [{
				loader: 'babel-loader'
			}]
		}, {
			test: /\.(jpg|png|gif)$/,
			use: {
				loader: 'url-loader',
				options: {
					name: '[name]_[hash].[ext]',
					outputPath: 'images/',
					limit: 10240
				}
			} 
		}, {
			test: /\.(eot|ttf|svg)$/,
			use: {
				loader: 'file-loader'
			} 
		}]
	},
	plugins,
	optimization: {
		runtimeChunk: {
			name: 'runtime'
		},
		usedExports: true,
		splitChunks: {
        chunks: 'all',
        cacheGroups: {
          vendors: {
              test: /[\\/]node_modules[\\/]/,
              priority: -10,
              name: 'vendors',
          }
        }
    }
	},
	performance: false,
	output: {
		path: path.resolve(__dirname, '../dist')
	}
}

webpack.common.js中通过AddAssetHtmlWebpackPlugin插件将生成的xx.dll.js文件引入到html中,通过DllReferencePlugin插件将js文件的映射文件导入,这样就可以手动打包第三方库,项目中直接使用

在package.json中

"scripts": {
    "dev-build": "webpack --config ./build/webpack.dev.js",
    "dev": "webpack-dev-server --config ./build/webpack.dev.js",
    "build": "webpack --config ./build/webpack.prod.js",
    "build:dll": "webpack --config ./build/webpack.dll.js"
  },

先通过npm run build:dll生成js文件和相应的mainfest文件,然后npm run dev-build就可以不用打包第三方库直接使用

  • 控制包文件大小

项目中不使用的第三方模块要通过Tree Shaking去不打包,也可以就不要引入,也可以通过splitChunkPlugin对代码进行拆分

  • thread-loader paraller-webpack happypack

webpack是基于node的,所以是单进程的,我们可以使用thread-loader多进程打包,或者paraller-webpack进行多页面打包,happypack 等来进行多进程打包,从而提高打包速度

HappyPack就能让Webpack把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程

module: {
   rules: [{
       test: /\.js$/,  //把对.js文件的处理转交给id为babel的HappyPack实例
       use: 'happypack/loader?id=babel',
       include: path.resolve(__dirname, 'src'),
       exclude: /node_modules/
   }, {
       test: /\.css$/,  //把对.css文件的处理转交给id为css的HappyPack实例
       use: 'happypack/loader?id=css',
       include: path.resolve(__dirname, 'src')
   }],
   noParse: [/react\.min\.js/]
}
plugins: [
   //用唯一的标识符id来代表当前的HappyPack是用来处理一类特定文件
   new HappyPack({
       id: 'babel',
       //如何处理.js文件,和rules里的配置相同
       loaders: [{
           loader: 'babel-loader',
           query: {
               presets: [
                   "env", "react"
               ]
           }
       }]
   }),
   new HappyPack({
       id: 'css',
       loaders: ['style-loader', 'css-loader'],
       threads: 4, //代表开启几个子进程去处理这一类型的文件
       verbose: true //是否允许输出日子
   })
]
  • 合理使用SourceMap
  • 结合stats分析打包结果
  • 开发环境内存编译
  • 开发环境无用插件剔除

2-7 多页面配置

多页面配置实际上是配置多个entry,在之前Code Splitting中也提到过这样生成的两个js会引入到html中,现在我们要通过多配置几个HtmlWebpackPlugin插件,将js文件分别引入就可以了

// webpack.common.js
const path = require('path');
const fs = require('fs');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
const webpack = require('webpack');

const makePlugins = (configs) => {
	const plugins = [
		new CleanWebpackPlugin(['dist'], {
			root: path.resolve(__dirname, '../')
		})
	];
	Object.keys(configs.entry).forEach(item => {
		plugins.push(
			new HtmlWebpackPlugin({
				template: 'src/index.html',
				filename: `${item}.html`,
				chunks: ['runtime', 'vendors', item]
			})
		)
	});
	const files = fs.readdirSync(path.resolve(__dirname, '../dll'));
	files.forEach(file => {
		if(/.*\.dll.js/.test(file)) {
			plugins.push(new AddAssetHtmlWebpackPlugin({
				filepath: path.resolve(__dirname, '../dll', file)
			}))
		}
		if(/.*\.manifest.json/.test(file)) {
			plugins.push(new webpack.DllReferencePlugin({
				manifest: path.resolve(__dirname, '../dll', file)
			}))
		}
	});
	return plugins;
}

const configs = {
	entry: {
		index: './src/index.js',
		list: './src/list.js',
		detail: './src/detail.js',
	},
	resolve: {
		extensions: ['.js', '.jsx'],
	},
	module: {
		rules: [{ 
			test: /\.jsx?$/, 
			include: path.resolve(__dirname, '../src'),
			use: [{
				loader: 'babel-loader'
			}]
		}, {
			test: /\.(jpg|png|gif)$/,
			use: {
				loader: 'url-loader',
				options: {
					name: '[name]_[hash].[ext]',
					outputPath: 'images/',
					limit: 10240
				}
			} 
		}, {
			test: /\.(eot|ttf|svg)$/,
			use: {
				loader: 'file-loader'
			} 
		}]
	},
	optimization: {
		runtimeChunk: {
			name: 'runtime'
		},
		usedExports: true,
		splitChunks: {
      chunks: 'all',
      cacheGroups: {
      	vendors: {
      		test: /[\\/]node_modules[\\/]/,
      		priority: -10,
      		name: 'vendors',
      	}
      }
    }
	},
	performance: false,
	output: {
		path: path.resolve(__dirname, '../dist')
	}
}

configs.plugins = makePlugins(configs);

module.exports = configs

3 webpack深入

3-1 loader的编写

新建一个空项目,src下写业务,在根目录下新建loaders文件夹,写一个replaceLoader.js

// index.js
console.log("hello world")
// replaceLoader.js
const loaderUtils = require('loader-utils');

module.exports = function(source) {
	// const options = this.query
	const options = loaderUtils.getOptions(this)
	// return source.replace('world', options.name);
    const result = source.replace("world", options.name)
    this.callback(null,result)
}

这里需要注意的是函数不要写箭头函数,因为this。options的参数可以通过this.query来取,但是官方推荐使用loader-utils。同步loader可以直接return,仅仅只能返回内容,也可以通过this.callback()来返回更多内容

callback地址

const path = require('path');

module.exports = {
	mode: 'development',
	entry: {
		main: './src/index.js'
	},
    // module: {
  // 	rules: [{
  // 		test: /\.js$/,
  // 		use: [
  // 			{
  // 				loader: 'path.resolve(__dirname, './loaders/replaceLoader.js')',  //第一种方式
  //                 options: {
  //                 	name: "世界"
  //                 }
  // 			}
  // 		]
  // 	}]
  // },
	resolveLoader: {
		modules: ['node_modules', './loaders']  //先在node_modules中找loader,没找到,再在loaders里找
	},
	module: {
		rules: [{
			test: /\.js$/,
			use: [
				{
					loader: 'replaceLoader',  //第二种方式引入loader
                    options: {
                    	name: "世界"
                    }
				}
			]
		}]
	},
	output: {
		filename: '[name].js',
		path: path.resolve(__dirname, 'dist')
	}
}

这样使用了自己编写的loader就可以将业务代码中的world改成世界

以上的还有一种异步loader,通过this.async来提示这是一个异步loader,但是需要注意:如果你使用了use:['loader','asyncLoader']那么需要先等asyncLoader解析完

// replaceLoaderAsync.js
const loaderUtils = require('loader-utils');

module.exports = function(source) {
	const options = loaderUtils.getOptions(this);
	const callback = this.async();
	setTimeout(() => {
		const result = source.replace('world', options.name);
		callback(null, result);
	}, 1000);
}

自定义loader场景

  • 异常捕获,将我们业务代码的function(){},通过loader打包成try{function(){}}catch(e){}
  • 中英文切换,假设我们有一个{{title}}
if(node全局变量 === '中文'){
	source.replace('{{title}}', '中文标题')
}else{
	source.replace('{{title}}', 'english title')
}

3-2 plugin的编写

新建一个空项目,src下写业务,在根目录下新建plugins文件夹,写一个copyright-webpack-plugin.js

 class CopyrightWebpackPlugin {

	apply(compiler) {
		compiler.hooks.compile.tap('CopyrightWebpackPlugin', (compilation) => {
			console.log('compiler');
		})
		compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (compilation, cb) => {
			// debugger;
			compilation.assets['copyright.txt']= {
				source: function() {
					return 'copyright by dell lee'
				},
				size: function() {
					return 21;
				}
			};
			cb();
		})
	}

}

module.exports = CopyrightWebpackPlugin;
const path = require('path');
const CopyRightWebpackPlugin = require('./plugins/copyright-webpack-plugin');

module.exports = {
	mode: 'development',
	entry: {
		main: './src/index.js'
	},
	plugins: [
		new CopyRightWebpackPlugin()
	],
	output: {
		path: path.resolve(__dirname, 'dist'),
		filename: '[name].js'
	}
}

loaderplugin的区别

当需要引用js文件或者其他格式的文件时,可以使用loader去帮助处理这个文件,帮助我们去处理模块

plugin在我们打包的时候,就会生效,比如打包生成个html文件,就使用htmlWebpackplugin,当打包之前需要清理文件夹,就使用cleanWebpackPlugin

loader本质上是一个函数,plugin本质上是一个类(所以使用时才需要new)

// 调试
"scripts": {
    "debug": "node --inspect --inspect-brk node_modules/webpack/bin/webpack.js",
    "build": "webpack"
  },