webpack入门

53 阅读14分钟

webpack学习

1、webpack是什么?

本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。 当 webpack 处理应用程序时,它会递归地构建一个 依赖关系图(dependency graph), 其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundles,它们均为静态资源,用于展示你的内容。

2、安装

webpack 是一个使用 Node.js 实现的一个模块化代码打包工具。 所以,我们需要先安装 webpack,安装之前需要搭建好 Node.js 环境。

npm install -D webpack webpack-cli
注:不推荐全局安装

webpack-cli : 提供 webpack 命令、工具,类似 create-react-app

webpack : webpack 代码,类似 react

3、使用

./node_modules/.bin/webpack
./node_modules/.bin/webpack -v 【查看版本】

package.jsonscripts 来简化输入:

// package.json
{
	...,
	"scripts": {
		"start": "webpack"	// scripts 中可以定位到 ./node_modules/.bin/ 目录下
	}
}
 注:`scripts` 中使用 `test``start``restart``stop` 命名的时候,可以在调用的时候省略 `run`,即直接 `npm start`

更加简便的方式:npx webpack, 通过 npx 也可以帮助我们定位命令到 ./node_modules/.bin/ 目录下 【注:npm5.2+ 增加,如果没有,可以使用 npm i -g npx 来安装】

4、打包模块

1. 入口文件

就是我们项目中加载的第一个文件,比如上面的 main.js 文件,其它文件都是通过 import 等方式引入的,webpack 会从我们指定的入口文件开始分析所有需要依赖的文件,然后把打包成一个完整文件。

2. 打包命令

webpack ./js/index.js 上面命令会使用 webpack 默认的一些配置对模块文件进行打包,并把打包后的文件输出到默认创建的 ./dist 目录下,打包后的文件名称默认为 main.js。 模块文件打包以后,就可以在不支持 es6 模块语法的浏览器环境下引入使用了。

打包文件分析

  • 把分散的模块文件打包到一个文件中,不需要外部引入了
  • 内置了一个小型模块加载器(类似 requireJS),实现了打包后的代码隔离与引用

以上就是 webpack 最基础的使用于基本原理,当然强大的 webpack 远远不止这些功能。

5、打包配置

虽然,我们可以直接通过命令的来打包,但是推荐创建一个 webpack.config.js 的配置文件来实现更方便和强大的功能。
webpack 命令在运行的时候,默认会读取运行命令所在的目录下的 webpack.config.js 文件,通常我们会在项目的根目录下运行命令和创建配置文件。

我们也可以通过 —config 选项来指定配置文件路径:

webpack --config ./configs/my_webpack.config.js

配置文件: module.exports = { ... //配置项 }

注意:

  • Vue项目中已经没有webpack.config.js,因为Vue项目中 vue.config.js 文件就等同于 webpack 的 webpack.config.js。
  • vue-cli3 之后创建的时候并不会自动创建 vue.config.js,因为这个是个可选项,所以一般都是需要修改 webpack 的时候才会自己创建一个 vue.config.js。
  • Vue-cli3+ 和 Vue-cli2 的最大区别:在于内置了很多配置,没有 build 文件夹和 config 的配置。但是在开发中,避免不了的还是需要个性化的配置,这里讲一下 vue.config.js 的配置。
  1. mode-模式

模式 : "production" | "development" | "none", 不同的模式会对 webpack 打包的时候进行一些对应的优化配置。

module.exports = {
  mode: 'production'
}
  1. entry-入口

指定打包⼊口⽂文件,有三种不同的形式:string | object | array
一对一:一个入口,对应一个打包文件

module.exports = {
  entry: './src/index.js'
}


多对一:多个入口,对应一个打包文件

module.exports = {
  entry: [
    './src/index1.js',
    './src/index2.js',
  ]
}


多对多:多个入口,对应多个打包文件

module.exports = {
  entry: {
    'index1': "./src/index1.js",
    'index2': "./src/index2.js"
  }
}
  1. output-输出 打包后的文件位置
module.exports = {
  ...,
  output: {
  	 path: path.resolve(__dirname, "dist"),
    filename: "bundle.js",
	 filename: "[name].js"
	}
}

6、深入

webpack 中,有一个很重要的特性:
模块不仅仅只是 js 的文件,webpack 可以把任意文件数据作为模块进行处理,包括:非 js 文本、css、图片等等;
但是 webpack 默认情况下只能处理 js 模块,如果需要处理其它类型的模块,则需要使用它提供的一些其它功能。

  • loaderswebpack 中非常核心的内容之一,前面我们说的非 js 类型的模块处理就靠它了。webpack 可以使用 loader 来预处理文件。这允许你打包除 JavaScript 之外的任何静态资源。你可以使用 Node.js 来很简单地编写自己的 loader。
  • pluginswebpack 中另外一个核心的内容,它主要是扩展 webpack 本身的一些功能。插件可以运行在 webpack 的不同阶段(钩子 / 生命周期)。

7、Loaders 参考

webpack 碰到不识别的模块的时候,webpack 会在配置的 module 中进行该文件解析规则的查找

  • rules 就是我们为不同类型的文件定义的解析规则对应的 loader,它是一个数组
  • 每一种类型规则通过 test 选项来定义,通过正则进行匹配,通常我们会通过正则的方式来匹配文件后缀类型
  • use 针对匹配到文件类型,调用对应的 loader 进行处理
module.exports = {
  module: {
    rules: [
      { test: /.css$/, use: 'css-loader' },
      { test: /.ts$/, use: 'ts-loader' },
    ],
  },
};
  1. raw-loader
    在 webpack 中通过 import 方式导入文件内容,loader 并不是 webpack 内置的,所以首先要安装

npm install --save-dev raw-loader

在 webpack.config.js 中进行配置:
module.exports = {
  ...,
  module: {
      rules: [
      {
        test: /\.(txt|md)$/,
        use: 'raw-loader'
      }
    ]
	}
}
  1. file-loader 参考
    把识别出的资源模块,移动到指定的输出⽬目录,并且返回这个资源在输出目录的地址(字符串)

npm install --save-dev file-loader

rules: [
  ...,
	{
	 test: /\.(png|jpe?g|gif)$/,
    use: {
      loader: "file-loader",
      options: {
        //[name] 源资源模块的名称,[ext] 源资源模块的后缀
        name: "[name]_[hash].[ext]", 
        outputPath: "./images",//打包后的存放位置
        publicPath: './images',// 打包后文件的 url
      }
    }
	}
]
  1. css-loader
    分析 css 模块之间的关系,并合成⼀个 css
    npm install --save-dev css-loader
rules: [
  ...,
	{
	test: /\.css$/,
    use: {
      loader: "css-loader",
      options: {
  		url: true,   // 启用/禁用 url() 处理
  		import: true,// 启用/禁用 @import 处理
           sourceMap: false // 启用/禁用 Sourcemap
      }
    }
	}
]
  1. style-loader
    css-loader 生成的内容,用 style 标签挂载到⻚面的 head

npm install --save-dev style-loader

rules: [
  ...,
	{
	   test: /\.css$/,
      use: ["style-loader", "css-loader"] //处理顺序为:从右到左
	}
]

注:同一个任务的 loader 可以同时挂载多个,
处理顺序为:从右到左,也就是先通过 css-loader 处理,然后把处理后的 css 字符串交给 style-loader 进行处理;

8、Plugins

扩展 webpack 本身的一些功能,它们会运行在 webpack 的不同阶段(钩子 / 生命周期)。webpack 自身也是构建于你在 webpack 配置中用到的相同的插件系统之上! 插件目的在于解决 loader 无法实现的其他事

  1. HtmlWebpackPlugin
    在打包结束后,⾃动生成⼀个 html ⽂文件,并把打包生成的 js 模块引⼊到该 html

npm install --save-dev html-webpack-plugin

// webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
	...
  plugins: [
     new HtmlWebpackPlugin({
       title: "My App",
       filename: "app.html",
       template: "./src/html/index.html"
     }) 
  ]
};
注:在 `html` 模板文件中,可以通过 `<%=htmlWebpackPlugin.options.XXX%>` 的方式获取配置的值

更多配置

  • title: ⽤来生成⻚面的 title 元素
  • filename: 输出的 HTML ⽂件名,默认是 index.html, 也可以直接配置子目录
  • template: 模板⽂件路径,⽀持加载器(loader),⽐如 html!./index.html
  • inject: true | 'head' | 'body' | false,注⼊所有的资源到特定的 template 或者 templateContent 中,如果设置为 true 或者 body,所有的 javascript 资源将被放置到 body 元素的底部,'head' 将放置到 head 元素中
  • favicon: 添加特定的 favicon 路径到输出的 HTML 文件中
  • minify: {} | false, 传递 html-minifier 选项给 minify 输出
  • hash: true | false,如果为 true,将添加 webpack 编译生成的 hash 到所有包含的脚本和 CSS ⽂件,对于解除 cache 很有用
  • cache: true | false,如果为 true,这是默认值,仅在文件修改之后才会发布文件
  • showErrors: true | false,如果为 true,这是默认值,错误信息会写入到 HTML ⻚面中
  • chunks: 允许只添加某些块 (⽐如,仅 unit test 块)
  • chunksSortMode: 允许控制块在添加到⻚面之前的排序方式,⽀持的值:'none' | 'default' |{function}-default:'auto'
  • excludeChunks: 允许跳过某些块,(⽐如,跳过单元测试的块)
  1. clean-webpack-plugin
    删除(清理)构建目录

npm install --save-dev clean-webpack-plugin

const {CleanWebpackPlugin} = require('clean-webpack-plugin');
module.exports = {
	...
  plugins: [
    ...,
    new CleanWebpackPlugin(),
    ...
  ]
}
  1. mini-css-extract-plugin
    提取 CSS 到一个单独的文件中

npm install --save-dev mini-css-extract-plugin

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
	...,
  module: {
  	rules: [
  		{
  			test: /\.s[ac]ss$/,
  			use: [
  			    { loader: MiniCssExtractPlugin.loader },
                      'css-loader',
                      'sass-loader'
                    ]
		}
  	      ]
	},
  plugins: [
    ...,
    new MiniCssExtractPlugin({
    	filename: '[name].css'
    }),
    ...
  ]
}

9、WebpackDevServer

每次的代码修改都需要重新编译打包,刷新浏览器,特别麻烦,我们可以通过安装 webpackDevServer 来改善这方面的体验;

npm install --save-dev webpack-dev-server

启动命令:npx webpack-dev-server
或者:package.json 中添加 scripts

"scripts": {
  "server": "webpack-dev-server"
}

修改 webpack.config.js

module.exports = {
  ...,
  devServer: {
  	  contentBase: "./abc", // 扩展的虚拟路径
  	  open: true, // 自动开启浏览器
  	  port: 8081 // 端口
	}
}
注:启动服务以后,`webpack` 不在会把打包后的文件生成到硬盘真实目录中了,
而是直接存在了内存中(同时虚拟了一个存放目录路径),后期更新编译打包和访问速度大大提升;

9-1. Proxy

当下前端的开发都是前后端分离开发的,前端开发过程中代码会运行在一个服务器环境下(如当前的 WebpackDevServer),那么在处理一些后端请求的时候通常会出现跨域的问题。
WebpackDevServer 内置了一个代理服务,通过内置代理就可以把我们的跨域请求转发目标服务器上(WebpackDevServer 内置的代理发送的请求属于后端 - node,不受同源策略限制),具体如下:

//后端代码以node为例
const Koa = require('koa');
const KoaRouter = require('koa-router');

const app = new Koa();
const router = new KoaRouter();

router.get('/api/info', async ctx => {
    ctx.body = {
        username: 'zMouse',
        gender: 'male'
    }
})

app.use( router.routes() );
app.listen(8787);
//前端代码
axios({
  url: 'http://localhost:8787/api/info'
}).then(res => {
  console.log('res',res.data);
})
//默认情况下,该代码运行以后会出现跨域请求错误,修改 `webpack` 配置
module.exports = {
  ...,
  devServer: {
  	contentBase: "./dist", // 生成的虚拟目录路径
  	open: true, // 自动开启浏览器
  	port: 8081,// 端口
  	proxy: {
      '/api': {
      	target: 'http://localhost:8787'
       }
    }
	}
}
注:通过 `proxy` 设置,当我们在当前 `WebpackDevServer` 环境
下发送以 `/api` 开头的请求都会被转发到 <http://localhost:8787> 目标服务器下
//修改前端代码
axios({
  //url: 'http://locahost:8081/api/info',
  url: '/api/info'
}).then(res => {
  console.log('res',res.data);
})

注意: url 地址要填写 WebpackDevServer 域,比如当前 WebpackDevServer 开启的是 http://localhost:8081,也就是我们当前前端代码运行的环境,那么请求的 url 也必须发送到这里,当我们的请求满足了 proxy 中设置的 /api 开头,那么就会把请求转发到 target ,所以最后的实际请求是:http://lcoahost:8787/api/info

9-2. Hot Module Replacement

在之前当代码有变化,我们使用的 live reload,也就是刷新整个页面,虽然这样为我们省掉了很多手动刷新页面的麻烦,但是这样即使只是修改了很小的内容,也会刷新整个页面,无法保持页面操作状态。
HMR 随之就出现了,它的核心的局部(模块)更新,也就是不刷新页面,只更新变化的部分。

module.exports = {
  ...,
  devServer: {
  	 contentBase: "./dist",// 生成的虚拟目录路径
  	 open: true,// 自动开启浏览器
  	 port: 8081,// 端口
  	 hot:true,// 开启热更新
    hotOnly:true,// 即使 HMR 不生效,也不去刷新整个页面(选择开启)
  	 proxy: {
      '/api': {
      	target: 'http://localhost:8787'
       }
    }
	}
}

开启 HMR 以后,当代码发生变化,webpack 即会进行编译,并通过 websocket 通知客户端(浏览器),我们需要监听处理来自 webpack 的通知,然后通过 HMR 提供的 API 来完成我们的局部更新逻辑。

//fn1.js
export default function() {
    console.log('start1!');
}

//index.js
import fn1 from './fn1.js';
box1.onclick = fn1;
if (module.hot) {//如果开启 HMR
    module.hot.accept('./fn1.js', function() {
      // 更新逻辑
      box1.onclick = fn1;
    })
}
上面代码就是 当 ./fn1.js 模块代码发生变化的时候,
把最新的 fn1 函数绑定到 box1.onclick 上.

从上面就可以看到,HMR 其实就是以模块为单位,当模块代码发生修改的时候,通知客户端进行对应的更新,而客户端则根据具体的模块来更新我们的页面逻辑(这些逻辑需要自己去实现),好在当前一些常用的更新逻辑都有了现成的插件。

9-3.CSS热更新

样式热更新比较简单,style-loader 中就已经集成实现了,我们只需要在 use 中使用就可以了

9-4.React热更新

9-5.Vue热更新

10、sourceMap

我们实际运行在浏览器的代码是通过 webpack 打包合并甚至是压缩混淆过的代码,所生成的代码并不利于我们的调试和错误定位,我们可以通过 sourceMap 来解决这个问题,
sourceMap 本质是一个记录了编译后代码与源代码的映射关系的文件,我们可以通过 webpackdevtool 选项来开启 sourceMap

Tip

验证 devtool 名称时, 我们期望使用某种模式, 注意不要混淆 devtool 字符串的顺序, 模式是: [inline-|hidden-|eval-] [nosources-] [cheap-[module-]] source-map.

module.exports = {
  mode: 'production',
  devtool: 'source-map',
  ...
}

首先,编译后会为每一个编译文件生成一个对应的 .map 文件,同时在编译文件中添加一段对应的 map 文件引入代码--如下,
同时,现代浏览器都能够识别 sourceMap 文件,如 chrome,会在 Sources 面板中显示根据编译文件与对应的 map 文件定位到源文件中,有利于我们的调试和错误定位.

//# sourceMappingURL=xx.js.map
//# sourceMappingURL=xx.css.map

eval不支持IE8 可以设断点调试,不显示列信息,每个js模块代码用eval()执行,并且在生成的每个模块代码尾部加上注释,如WEBPACK FOOTER;module id(模块在数组中的索引) ;sourceURL(原js路径)。不会生成 .map文件
source-map可以设断点调试,不显示列信息,生成相应的.Map文件,并在合并后的代码尾部加上注释//# sourceMappingURL=**.js.map
eval-source-map不能设断点调试,不显示列信息,可以用手动加入debugger调试;参考第一种eval模式,跟eval不一样的是其用base64存储map信息,不会生成.Map文件,Map信息以Base64格式存放在每个模块代码的尾部
cheap-source-map可以设断点调试,不显示列信息,生成相应的.Map文件,可参考source-map,不包含 loader 的 sourcemap
cheap-module-source-map不包含列信息,同时 loader 的 sourcemap 也被简化为只包含对应行的。最终的 sourcemap 只有一份,它是 webpack 对 loader 生成的 sourcemap 进行简化,然后再次生成的

11、Code Spliting

将代码分割到多个不同的bundle(打包后)文件中,可以通过按需加载等方式对资源进行加载,使用合理的情况下可以极大影响加载速度。

11.1 入口起点

通过设置多个入口文件的方式实现最简单的代码分割

entry: {
  index: "./src/index.js",
  list: "./src/list.js",
},
output: {
  path: resolve(__dirname, "../dist"),
  filename: "js/[name].js", // 多入口文件的filename不能写死名称,需要通过[name]配置
}

11.2 防止重复

通过设置dependOn配置多个模块之间的共享

entry: {
  index: {
    import: "./src/index.js",
    dependOn: "axios",
  },
  list: {
  	import: "./src/list.js",
  	dependOn: "axios",
  },
  axios: "axios",
},

SplitChunksPlugin
将公共的依赖模块提取到已有的入口chunk文件或新的chunk文件当中;

entry: {
  index: "./src/index.js",
  list: "./src/list.js",
},
optimization: {
  splitChunks: {
    // async表示只从异步加载得模块(动态加载import())里面进行拆分
    // initial表示只从入口模块进行拆分
    chunks: "all", // all表示以上两者都包括
  },
}
// 默认配置
optimization: {
  splitChunks: {
    chunks: 'async',
    minSize: 20000,
    minRemainingSize: 0,
    minChunks: 1,
    maxAsyncRequests: 30,
    maxInitialRequests: 30,
    enforceSizeThreshold: 50000,
    cacheGroups: {
      defaultVendors: {
        test: /[\\/]node_modules[\\/]/,
        priority: -10,
        reuseExistingChunk: true,
      },
      default: {
        minChunks: 2,
        priority: -20,
        reuseExistingChunk: true,
      },
    },
  },
},

11.3 动态导入

通过import()动态导入模块,可以通过内联注释对chunk进行一些配置;
模块方法 | webpack 中文文档 (docschina.org)

import(/* webpackChunkName: 'data', webpackPreload: true*/  './data')
	.then(data => {
	    console.log(data);
	})

12、预加载/预获取

通过内联注释webpackPrefetchwebpackPreload两种资源提示告知浏览器对资源进行不同的加载处理

const data = import(/* webpackChunkName: 'data', webpackPreload: true */ './data.js')
const data = import(/* webpackChunkName: 'data', webpackPrefetch: true */ './data.js')

与 prefetch 指令相比,preload 指令有许多不同之处:

  • preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
  • preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。
  • preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻。
  • 浏览器支持程度不同。

13、外部扩展

通过externals配置在输出的bundle中排除某些依赖,这些依赖将会在用户环境中所包含。

externals: {  lodash: '_' },

14、tree shaking

将上下文中的dead-code移除,就像摇树上的枯叶使其掉落一样

optimization: { usedExports: true }