webpack从入门到进阶(一)

678 阅读5分钟

笔者之前就看过webpack系列的文章和视频,但是奈何记忆不行,决定写一个学习笔记来总结一下,以此来缓解一下我的脑子! 官网 中文版

1、webpack认识

1-1 webpack是什么

webpack is a module bundler webapck是一个模块打包工具 现代浏览器已经支持了,es6的ES Module的模块引入方式,可以在 <script> 标签中加入 type="module" 属性 ,并且将项目运行在服务器上即可。

1-2 webpack安装

在项目中通过 npm init来生成packjson.json

npm install webpack webpack-cli --save-dev来安装局部webpack

这样我们就可以在项目中使用webpack来进行打包了

  1. ./node_modules/webpack/bin
  2. npx webpack
  3. npm scripts

详细移步:Command Line Interface npx webpack index.js 就可以打包生产dist/main.js,index.html加载这个js就可以运行成功了 但是为什么会默认生成是dist文件下的main.js呢,webpack 后面的index.js可以 不指定吗,这就要说到默认的配置文件了

1-3 webpack配置

// webpack.config.js 默认
const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
};

入口是src/index.js,所以上面的例子需要指定同级下的index.js,输出是dist/main.js 这里的entry是一种简写,等同于:

// webpack.config.js
...
module.exports = {
    entry: {
        main: './src/index.js'
    },
    ...
}

1-4 补充

1、在使用webpack是可以通过npm scripts来找到项目下的webpack 2、webpack默认配置文件是webpack.config.js,也可以通过webpack --config来进行配置 npx webpack a.webpack.config.js

3、webapck打包模式有两种:productiondevelopment,默认是前者,也就是会对代码进行压缩,而development不压缩

可以通过 mode 字段进行修改:

module.exports = {
	mode:'production',
    entry: {
        main: './src/index.js'
    },
    ...
}

2、webpack入门

2-1 loader是什么

对于js这样的文件webpack默认是可以识别的,但是对于非js文件需要安装相应的loader来解析这样格式的文件

file-loader

例如使用file-loader来打包图片文件,结果是dist/images/b417eb0f1c79e606d72c000762bdd6b7.jpg

// index.js
import img from './file.png'
console.log("img:",img)   //img:images/b417eb0f1c79e606d72c000762bdd6b7.jpg
let imgEle = new Image()
imgEle.src = img
document.body.appendChild(imgEle)
const path = require('path');

module.exports = {
	mode: 'development',
	entry: {
		main: './src/index.js'
	},
	module: {
		rules: [{
			test: /\.(jpe?g|png|gif)$/,
			use: {
				loader: 'file-loader',
				options: {
					name: '[name]_[hash].[ext]',
					outputPath: 'images/',
                    // publicPath: 'dist/images/'
				}
			} 
		}]
	},
	output: {
		filename: 'bundle.js',
		path: path.resolve(__dirname, 'dist')
	}
}

这里需要注意的是outputPathpublicPath,前者是文件输出的路径,后者是文件真实使用的url路径,也就是返回值的路径

url-loader

const path = require('path');

module.exports = {
	...
	module: {
		rules: [{
			test: /\.(jpe?g|png|gif)$/,
			use: {
				loader: 'url-loader',
				options: {
					name: '[name]_[hash].[ext]',
					outputPath: 'images/',
                    // publicPath: 'dist/images/',
                    limit: 10240
				}
			} 
		}]
	},
	...
}

url-loader包含file-loader,limit属性是设置图片转base64的阈值,当大小大于阈值时,就会生成文件,否则不会生成文件,而是以base64的方式存储js文件中直接使用(在性能优化中,就可以将小图片生成base64,减少http请求次数,但貌似base64会增大图片体积1/3,3kb的图片变成base64就是4kb)

style-loader

将css加载到head中

css-loader

处理css引用关系,并将它们合并在一起

postcss-aloder

postcss-loader 中的 autoprefixer 插件自动帮我们加上css3 属性可能需要的浏览器厂商前缀

sass-loader

可以解析scss文件

const path = require('path');

module.exports = {
	mode: 'development',
	entry: {
		main: './src/index.js'
	},
	module: {
		rules: [{
			test: /\.(jpe?g|png|gif)$/,
			use: {
				loader: 'url-loader',
				options: {
					name: '[name]_[hash].[ext]',
					outputPath: 'images/',
					limit: 10240
				}
			} 
		},{
			test: /\.scss$/,
			use: [
				'style-loader', 
				'css-loader', 
				'sass-loader',
				'postcss-loader'
			]
		}]
	},
	output: {
		filename: 'bundle.js',
		path: path.resolve(__dirname, 'dist')
	}
}

这里注意loader的执行顺序是从右往左,和css选择器的解析顺序是一样的

postcss-loader需要一个postcss.config.js

module.exports = {
  plugins: [
  	require('autoprefixer')
  ]
}

css module

css模块化,因为css-loader最终由style-loader插入到style中,所以可能会产生样式冲突,可以在css-loader中配置 modules:true

const path = require('path');

module.exports = {
	...
	module: {
		rules: [...,{
			test: /\.scss$/,
			use: [
				'style-loader', 
				{
                	loader: 'css-loader',
                    options: {
						importLoaders: 2,
						modules: true
					}
				},
				'sass-loader',
				'postcss-loader'
			]
		}]
	},
	...
}

引入时,不能import from './index.scss',而是import stylecss from './index.scss'

// index.js
import stylecss from './index.scss'
let div = document.createElement('div')
div.innerHTML = '我是一个块'
div.classList.add(stylecss.red)
document.body.appendChild(div)
// index.scss
body{
	.red{
    	color: red
    }
}

打包之后dom结构: <div class="_17cnVz87yzSOO5TpFdnLsk">我是一个块</div>

file-loader打包字体文件

得益于 css3 中的 @font-face ,使得我们可以在网页中使用我们喜欢的任何字体。还有一些专门的网站(如 iconfont),可以将一些图标制作成字体文件。

如果你使用了 @font-face ,那么你就需要指定 webpack 打包这些字体文件了。同打包图片一样,打包字体文件也可以使用 file-loader

const path = require('path');

module.exports = {
	...
	module: {
		rules: [{
			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'
			]
		}]
	},
	...
}

2-2 plugins是什么

plugins可以在webpack运行到某个时刻(打包结束后),帮你做一些事情,

HtmlWebpackPlugin

会在打包结束后,自动生成一个html文件,并把打包生成的js文件自动引入到html中

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

module.exports = {
	mode: 'development',
	entry: {
		main: './src/index.js'
	},
	module: {
		...
	},
	plugins: [new HtmlWebpackPlugin()],
	output: {
		...
	}
}

但是这样只会生成一个空模板的文件

<!-- dist/index.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Webpack App</title>
  </head>
  <body>
  <script type="text/javascript" src="bundle.js"></script></body>
</html>

为了满足需求,我们还可以指定一个html模板文件

// webpack.config.js
module.exports = {
    ...
    plugins: [new HtmlWebpackPlugin({
        template: "src/index.html"
    })]
}

AddAssetHtmlWebpackPlugin

可以帮我们在生成的 html 文件中添加一些静态资源。

// webpack.config.js
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin')
module.exports = {
    ...
    plugins: [new HtmlWebpackPlugin({
        template: "src/index.html"
    }),
    new AddAssetHtmlWebpackPlugin({
        filepath: path.resolve(__dirname, 'assets/js/mj.js')
    })]
}

这样,在生成的 index.html 文件中会自动帮你引入 mj.js

CleanWebpackPlugin

会在打包之前,自动删除dist文件

// webpack.config.js
const CleanWebpackPlugin = require('clean-webpack-plugin')
module.exports = {
    ...
    plugins: [new HtmlWebpackPlugin({
        template: "src/index.html"
    }),
    new AddAssetHtmlWebpackPlugin({
        filepath: path.resolve(__dirname, 'assets/js/mj.js')
    }),
    new CleanWebpackPlugin(['dist'])]
}

2-3 entry and output的配置

之前提到过 entry: 'src/index.js'等同于

entry:{
	main:'src/index.js'
}

打包输出的文件是dist/main.js

output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },

那我们想要多个入口呢,可以这样配置

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

module.exports = {
	mode: 'development',
	entry: {
		main: './src/index.js',
		sub-main: './src/index.js'
	},
	module: {
		...
	},
	plugins: [new HtmlWebpackPlugin({
		template: 'src/index.html'
	}), new CleanWebpackPlugin(['dist'])],
	output: {
		publicPath: 'http://abc.com',
		filename: '[name].js',
		path: path.resolve(__dirname, 'dist')
	}
}

这样我们就可以在dist下生成main.js和sub-main.js,并且HtmlWebpackPlugin会将这两个js文件同时引入到html中,如果配置了publicPath: 'http://abc.com',指定资源的公共域名,那么引入的js文件地址将会是http://abc.com/main.js http://abc.com/sub-main.js

详细:entry output

2-4 SourceMap

我们在编写js文件的时候,不小心拼错了一个语法单词,打包之后运行时,会报打包后的js文件错误,比如main.js1995行出错了,但是我们不知道源代码时哪一行

SourceMap其实是一个映射关系

只需要配置devtool:value

  • source-map:会生成.map文件
  • inline-source-map:在js中多一段map映射,精确到哪一行哪一个字符
  • cheap-inline-source-map: 精确到行就行,只针对自己的业务代码
  • cheap-module-inline-source-map:对第三方的代码也管
  • eval:最快的,性能最好的,但是针对比较复杂的代码,可能提示没那么全

development devtool: 'cheap-module-eval-source-map'

production devtool: 'cheap-module-source-map'

详细移步: devtool

2-5 WebpackDevServer

  • 监听文件变化 --watch (文件改变,自动打包,手动刷新,不开本地服务器)
  • devServer (自动打包,自动更新,开本地服务器,vue,react使用这种)
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = {
	mode: 'development',
	devtool: 'cheap-module-eval-source-map',
	entry: {
		main: './src/index.js'
	},
	devServer: {
		contentBase: './dist',
		open: true,
		port: 8080,
        proxy: {
        '/api': {
          target: 'http://localhost:3000',
          pathRewrite: {'^/api' : ''}
        }
    }
	},
	module: {
		rules: [{
			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'
			]
		}]
	},
	plugins: [new HtmlWebpackPlugin({
		template: 'src/index.html'
	}), new CleanWebpackPlugin(['dist'])],
	output: {
		filename: '[name].js',
		path: path.resolve(__dirname, 'dist')
	}
}

start test stop restart 不需要run

webpackdevserver不仅会帮助我们进行打包,并且会把生成的文件放入电脑内存中,提高打包的速度

详细移步:devserver

详细移步:development

2-6 Hot Module Replacement

热模块替换 简写HMR 之前配置devserver的时候,提到会自动打包文件,自动更新,但有时候我们调试样式,更新数据就没了,不方便调试,我们需要在devserver中配置几个选项

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',
	entry: {
		main: './src/index.js'
	},
	devServer: {
		contentBase: './dist',
		open: true,
		port: 8080,
        proxy: {
        '/api': {
          target: 'http://localhost:3000',
          pathRewrite: {'^/api' : ''}
        },
        hot: true,
        hotOnly: true
    }
	},
	module: {
		rules: [{
			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'
			]
		}]
	},
	plugins: [new HtmlWebpackPlugin({
		template: 'src/index.html'
	}), 
    new CleanWebpackPlugin(['dist']),
    new webpack.HotModuleReplacementPlugin()],
	output: {
		filename: '[name].js',
		path: path.resolve(__dirname, 'dist')
	}
}

HotModuleReplacementPlugin 是 webpack 自带的一个插件。hot: true 代表开启 HRM ,hotOnly: true 代表只进行 HRM ,不自动刷新。

配置完之后修改css,不刷新也可以看到更改的效果了 但是js文件还需要使用api

// index.js
import abc from './abc.js'
if (module.hot) { // 检查是否开启了 hot 功能
  module.hot.accept('./abc.js', function() {
    // 当 abc.js 发生变化时的回调
  });
}

css之所以不需要这样写,是因为css-loader已经内置了这样的功能,vue-loader也同样如此

详细移步:HMR

详细移步:HMR api

2-7 Babel

在ES6横行的这个时代,不会ES6都不好意思说自己会前端,由于ES6诸多新特性特别好用,但是一些低版本的浏览器不支持,因此我们需要通过babel来将ES6转成能在浏览器运行的ES5或更低版本 Babel官网

在webpack中开发业务代码和第三方库是有区别的

  1. 业务代码的方式:preset-env + polyfill
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');T
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');

module.exports = {
	...
	module: {
		rules: [{ 
			test: /\.js$/, 
			exclude: /node_modules/, 
            use: {
            	loader: 'babel-loader',
                options: {
                  presets: [['@babel/preset-env', {
                      targets: {
                      	chrome: "67",
                  	  },
                      useBuiltIns: 'usage'
                   }]]
                }
            }
		}]
	}
}

由于babel配置太多,可以通过Create .babelrc configuration file来抽离出来

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

根目录下创建.babelrc

// .babelrc
{
	presets: [['@babel/preset-env', {
		targets: {
			chrome: "67",
		},
		useBuiltIns: 'usage'
		}]]
}

如此就可以将ES6转ES5了,但是对于promise这些,浏览器可能没有实现,因此还需要babel-polyfill,作用就是对于没有实现的浏览器做底层的实现

// index.js
import "@babel/polyfill";
function sleep(wait=2000){
	return new Promise((resolve,reject)=>{
    	setTimeout(()=>{
        	resolve()
        },wait)
    })
}
(async function(){
	await sleep(3000)
    console.log('let me exec')
})()

这样就可以随心所欲写业务代码了,由于将所有的polyfill都实现了一遍,打包体积会比较大,因此.babelrcuseBuiltIns: 'usage'表示按需的意思,我用到哪些实现哪些,体积会缩小。target:{chrome:"67"}表明打包后的文件会运行在chrome67以上,此时可能就不会实现polyfill,因为67以上可能都已经支持了

2.第三方库:transform-runtime

npm install babel-loader @babel/core @babel/plugin-transform-runtime --save-dev
npm install --save @babel/runtime-corejs2
// .babelrc
{
	"plugins": [["@babel/plugin-transform-runtime", {
	"corejs": 2,
    "helpers": true,
    "regenerator": true,
    "useESModules": false
	}]]
}

polyfill 方式生成的 ES5 代码会直接作用于全局,可能会产生污染。runtime 方式会以类似闭包的形式生成ES5。

2-8 react打包

@babel/preset-react可以对jsx进行编译

// webpack.config.js
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',
	entry: {
		main: './src/index.js'
	},
	devServer: {
		contentBase: './dist',
		open: true,
		port: 8080,
		hot: true,
		hotOnly: true
	},
	module: {
		rules: [{ 
			test: /\.jsx?$/, 
			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()
	],
	output: {
		filename: '[name].js',
		path: path.resolve(__dirname, 'dist')
	}
}
// .babelrc
{
	presets: [['@babel/preset-env', {
		targets: {
			chrome: "67",
		},
		useBuiltIns: 'usage'
		}],
     "@babel/preset-react"
    ]
}
// index.jsx
import '@babel/polyfill'
import React, {Component} from 'react'
import ReactDom from 'react-dom'

class App extends Component {
    render() {
        return <h1>React</h1>
    }
}

ReactDom.render(<App/>, document.getElementById('app'))

npm start ---> webpack-dev-server

打开localhost:8080,就会看到加载的<h1>React</h1>