笔者之前就看过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来进行打包了
./node_modules/webpack/bin
npx webpack
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打包模式有两种:production
和 development
,默认是前者,也就是会对代码进行压缩,而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')
}
}
这里需要注意的是outputPath
和publicPath
,前者是文件输出的路径,后者是文件真实使用的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中开发业务代码和第三方库是有区别的
- 业务代码的方式: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都实现了一遍,打包体积会比较大,因此.babelrc
中useBuiltIns: '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>