Webpack打包
webpack是一款目前主流的模块化打包工具,提供了对前端开发过程中涉及的所有资源的模块化打包方案
模块化打包工具由来
- 解决开发阶段代码在实际生产运行环境中的兼容性问题
- 将零散的模块文件打包到统一的文件中,避免由于模块文件过多造成频繁的网络请求
- 实现所有前端资源的模块化,而不仅仅是JS模块化
快速上手(基本使用)
- 在项目目录中,执行
yarn add webpack webpack-cli --dev
安装webpack依赖 - 执行
yarn webpack
命令,对项目目录中的文件进行打包,打包后的文件默认输出到dist目录
配置文件及基础配置
webpack4以上版本支持按照约定零配置直接打包,以
src/index.js
为入口文件 -> 以dist/main.js
为输出文件-
使用
webpack.config.js
配置文件可以对webpack进行自定义打包配置- entry:指定入口文件
- output:指定输出文件
- filename:输出文件名
- path:输出文件路径(必须为绝对路径,可以使用
path.join(__dirname, 'output')
获取绝对路径) - publicPath:资源目录,默认值为空字符串
- mode:指定工作模式
- module:
- rules:配置loader加载规则,一个对象数组
- test:正则表达式匹配使用loader的文件
- use:指定使用的loader,可以接收loader名字符串(或指定loader文件路径)、配置对象,或者由多个loader名字符串、配置对象组成的数组,数组中配置的多个loader会按照从后往前的顺序依次执行
- loader:loader名
- options:配置选项,用于指定loader的配置参数
- rules:配置loader加载规则,一个对象数组
- plugins:配置插件,接收一个数组
// webpack.config.js const path = require('path'); module.exports = { mode: 'none', entry: './src/style.css', output: { filename: 'bundle.js', path: path.join(__dirname, 'output') }, module: { rules: [ { test: /.css$/, use: ['style-loader', 'css-loader'] } ] }, plugins: [ ] }
工作模式
webpakc可以通过命令行参数
--mode=
指定工作模式,默认不指定时执行production
模式,也可以在配置文件中通过mode配置项进行指定
- production
- 生产模式,会启用内置的插件对打包的代码进行压缩优化等操作
- development
- 开发模式,更注重打包效率,不对代码进行压缩
- none
- 纯打包模式,以最原始的状态打包代码
打包结果运行原理
以none模式打包代码,查看打包后的输出文件,并进行分析
-
入口函数
-
定义了一个对象来缓存加载的模块
installedModules
-
定义了一个函数
__webpack_require__
来加载模块,返回exports -
在定义的
__webpack_require__
函数上挂载了一些数据与工具函数 -
在最后调用
__webpack_require__
函数来加载入口模块,并返回exports
-
-
入函数参数传入的是一个由模块函数组成的数组,每个模块被包裹在一个相同结构的函数当中
function(module, __webpack_exports__, __webpack_require__){}
,用来形成模块私有作用域 -
每个模块函数中都会执行
__webpack_require__.r(__webpack_exports__)
,用来为exports对象定义一个__esModule
标记
- 模块中使用
__webpack_require__
加载其他依赖的模块,并执行模块中相应的代码
资源模块加载
webpack使用loader实现对模块的加载,默认loader只对js文件进行加载与解析,对于css等其他资源模块的加载,则可以通过配置额外的loader来实现
loader是webpack实现前端模块的核心,通过不同的loader可以实现加载任何类型的资源
在webpack.confg.js中,通过module属性,配置loader加载规则
-
module
- rules:配置loader加载规则,一个对象数组
- test:正则表达式匹配使用loader的文件
- use:指定使用的loader,可以接收loader名字符串,或者由多个loader名字符串组成的数组,数组中配置的多个loader会按照从后往前的顺序依次执行
module: { rules: [ { test: /.css$/, use: ['style-loader', 'css-loader'] } ] }
- rules:配置loader加载规则,一个对象数组
导入资源模块
通常情况下,应当使用js文件作为模块打包的入口,其他资源文件通过import的方式引入
webpack建议应当在对应的代码模块中引入当前模块所需要的资源(按需加载),而不是在全局入口引入
常用资源加载器-loader
-
file-loader:文件资源加载器、
-
url-loader:类似于file-loader,区别是当文件小于指定大小(可通过options下的limit选项配置)时,可以直接返回DataURL,将图片等转换成base64编码直接存放在js文件中,将小文件通过这种方式处理,可以减少页面加载时,浏览器请求资源文件的次数,提高加载效率
url-loader依赖于file-loader,无法单独使用
-
配置样例
{ test: /.png$/, use: { loader: 'url-loader', options: { limit: 10 * 1024 // 10KB } } }
-
css-loader:编译css文件到js中,以style形式挂载到html中
-
eslint-loader:语法格式校验
babel-loader:ES语法转换
html-loader:加载处理html文件
资源加载方式
- 遵循ES Modules标准的import声明
- 遵循CommonJS标准的require声明
- 遵循AMD标准的define函数和require函数
- css文件中的@import与css属性中使用的url(例如background-image)
- html中的src、a标签的href等
核心工作原理
- 以指定js文件为入口,查找资源文件依赖,并生成依赖树
- 遍历依赖树,将依赖的资源模块交给相应的loader进行处理
- 将最终处理的结果汇总到输出的bundle.js文件中
开发一个loader
尝试开发一个markdown文件加载器markdown-loader
创建markdown-loader.js文件,使用module.exports导出一个处理函数,接收加载的模块内容作为参数
-
loader返回的是经过处理过后的结果,最后执行的loader,必须返回js脚本
-
直接返回js脚本方式
// markdown-loader.js const marked = require('marked'); module.exports = source => { const html = marked(source) return `module.exports = ${JSON.stringify(html)}` }
-
使用html-loader来进行后续处理方式
// markdown-loader.js const marked = require('marked'); module.exports = source => { const html = marked(source) // return `module.exports = ${JSON.stringify(html)}` return html }
{ test: /.md$/, use: [ 'html-loader', './markdown-loader.js' ] }
插件及常用插件
loader用来处理资源打包,plugin则用来处理除了打包以外的其他自动化工作,例如:清除指定目录、拷贝资源文件、压缩输出代码等
通过webpack.config.js中配置plugins属性来配置plugin
webpack loader + plugin实现了前端工程化大部分工作
-
clean-webpack-plugin:清除指定目录,默认清理输出目录
const { CleanWebpackPlugin } = require('clean-webpack-plugin'); module.exports = { ... plugins: [ new CleanWebpackPlugin() ] }
-
html-webpack-plugin:自动生成html文件
filename:指定生成html文件名,默认index.html
title:html的title
meta:html的meta
template:指定生成html的模板文件,模板文件中的动态参数可以使用
<%= htmlWebpackPlugin.options.title %>
方式指定-
配置多个htmlWebpackPlugin实例,可以生成多个html文件
const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { ... plugins: [ new HtmlWebpackPlugin({ title: 'hello world', meta: { viewport: '' }, template: 'src/index.html' }), new HtmlWebpackPlugin({ filename: 'about.html' }) ] }
-
copy-webpack-plugin:拷贝指定文件到输出目录,接收一个路径数组
const CopyWebpackPlugin = require('copy-webpack-plugin'); module.exports = { ... plugins: [ // 开发阶段不建议使用,频繁的拷贝读写磁盘没有意义,同时会降低打包效率 new CopyWebpackPlugin({ patterns: [ { from: 'public' } ] }), ] }
开发一个插件
webpack的插件通过hook机制实现
webpack规定一个插件必须是一个函数,或者一个包含apply方法的对象,apply方法接收一个compiler对象参数
-
尝试实现一个去除js文件注释的插件
class MyPlugin { apply(compiler) { compiler.hooks.emit.tap('MyPlugin', compilation => { for (const name in compilation.assets) { if(name.endsWith('.js')) { const contents = compilation.assets[name].source() const cleanContents = contents.replace(/\/\*\*+\*\//g, '') compilation.assets[name] = { source: () => cleanContents, size: ()=> cleanContents.length, } } } }) } } module.exports = { ... plugins: [ new MyPlugin(), ] }
使用webpack提升开发体验
-
自动编译
- 使用
--watch
参数
- 使用
-
自动刷新浏览器
- 可以使用browser-sync工具来实现浏览器自动刷新,缺点是对于webpack来说需要额外的工具来实现,且需要webpack在监听到文件改动后频繁的写入磁盘,更好的方式是使用webpack-dev-server插件
-
Dev Server
webpack-dev-server提供了一个本地开发服务器,同时集成了自动编译、自动刷新浏览器等功能
打包编译的结果存放在内存中,由集成的http server直接读取,避免了频繁的磁盘读写,提高了编译效率
-
通过配置文件中的devServer属性,可以对webpack-dev-server进行相应的参数配置
contentBase:指定额外静态资源目录
-
proxy:代理配置
devServer: { contentBase: 'public', proxy: { '/api': { // http://localhost:8080/api/users -> https://api.github.com/api/users target: 'https://api.github.com', // http://localhost:8080/api/users -> https://api.github.com/users pathRewrite: { '^/api': '', // 正则匹配替换路径 }, // 以实际代理请求的主机名访问,localhost对于外部服务器无法识别 changeOrigin: true, } } }
-
-
Source Map
Source Map用来帮助开发者调试压缩后的js、css文件,便于进行问题定位
在压缩后的代码中,通过在最后添加指定格式的注释,来开启Souce Map的引用
例如
//# sourceMappingURL=xxxx.map
,xxxx.map指代对应的Source Map文件名通过配置文件中的devtool属性
devtool: 'source-map'
来配置在webpack中开启Source Map-
webpack中提供了多种不同模式的Source Map,不同的模式在打包速度、打包质量上存在一定的差异
- eval模式:将模块函数放到eval中执行,不生成source map文件,只能定位到代码来自哪个文件,构建速度较快
- eval-source-map模式:将模块还数放到eval中执行,生成source map文件,可以定位原始代码位置,构建速度最慢
- cheap-eval-source-map模式:定位到的代码文件经过loader转换,只能定位到行
- cheap-module-eval-source-map模式:定位到的代码文件是转换前的源码,只能定位到行,构建速度慢
- hidden-source-map模式:生成source map,但不在代码中引入,通常适合工具库开发使用
- nosources-source-map模式:可以定位到代码位置,但是无法查看源码,在生产模式下使用,保护源码
-
模式组合
- eval:是否使用eval执行代码模块
- cheap:source map是否包含行信息
- module:是否能得到loader处理前的源码
生产模式下,不建议生成source map,容易暴露源码
-
自动刷新与HMR
自动刷新功能当监视到代码资源文件发生改动,会自动重新编译打包,并刷新浏览器,此时会导致浏览器上正在编辑的内容以及操作状态会丢失,这样在实际开发过程中的体验并不是很好,更理想的方式是在浏览器不刷新的情况下实现代码资源改动的实时替换,即HMR(Hot Module Replacement)的功能,HMR极大的提升了开发效率
- webpack-dev-server集成了HMR功能,有两种开启方式
- 运行参数增加
--hot
- 配置属性devServer下增加
hot: true
,同时配置启用webpack.HotModuleReplacementPlugin
插件
- 运行参数增加
- 开启webpack的HMR后,css样式就实现了自动热更新,而js、图片资源等需要开发者使用HMR API
module.hot.accept()
去手动处理热更新,通常使用一些前端框架或者脚手架工具创建的前端项目,都已经集成了成熟完整的HRM处理方案 - 如果HMR实现上存在异常,webpack会退而使用自动刷新页面来完成更新,通过使用
hotOnly: true
配置,可以只使用HMR而不会使用自动刷新功能
- webpack-dev-server集成了HMR功能,有两种开启方式
-
生产环境优化
生产环境打包与开发环境打包通常存在较大的差异,webpack4为不同的模式提供了预设的配置,也推荐开发者为不同模式提供不同的配置文件,例如将通用配置放入webpack.common.js中,为开发环境提供webpack.dev.js,为生产环境提供webpack.prod.js,在对应模式的配置文件中,使用webpack-merge插件引入并合并配置,并在执行打包命令时使用
--config
参数指定要使用的配置文件,例如webpack --config webpack.prod.js
DefinePlugin:用于为代码注入全局成员,例如
process.env.NODE_ENV
,这是webpack内置的插件,可以在配置文件plugins中进行配置-
Tree Shaking:去除未引用代码(dead-code)、去除冗余代码,Tree-Shaking并不是一项单独的配置,是一系列优化功能组合使用的结果,production生产模式下自动开启
- 在配置文件中使用optimization属性集中配置优化选项
-
usedExports: true
只导出外部使用的成员 -
minimize: true
压缩代码 -
concatenateModules: true
尽可能将模块代码合并到一个函数中输出(默认一个模块对应一个函数),这样既提升了代码执行效率,又减少了代码的体积,这种方式又被称为Scope Hoisting(作用域提升,webpack3中添加的特性) -
sideEffects: true
:一般用于npm包标记副作用,在package.json中配置sideEffects属性来指定哪些代码包含副作用
Tree Shaking的实现基于ES Module,当使用了Babel插件或者babel-loader来处理转换es代码时,可能会将ES Module转换为CommonJS模块,此时Tree Shaking将不生效,最新的babel-loader中自动禁用了ES Module的转换,Tree Shaking可以生效
-
Code Splitting - 代码分割
随着前端工程越来越复杂,打包形成的bundle.js文件会变的越来越庞大,而并不是所有模块都需要在应用启动时使用,如果将所有模块打包到一个文件中,会使得网络加载js时间较久。降低应用加载启动的速度(首页白屏时间)
-
多入口打包:适合多页应用,每个页面作为一个打包入口,公共部分单独提取
// 多入口配置样例 const path = require('path'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { mode: 'none', entry: { //使用对象而不是数组,每个属性代表一个入口 index: './src/index.js', hello: './src/hello.js', }, output: { filename: '[name].bundle.js', path: path.join(__dirname, 'dist'), }, module: { rules: [ { test: /.css$/, use: ['style-loader', 'css-loader'] }, ] }, plugins: [ new CleanWebpackPlugin(), new HtmlWebpackPlugin({ filename: 'index.html', template: 'src/index.html', chunks: ['index'], }), new HtmlWebpackPlugin({ filename: 'hello.html', template: 'src/hello.html', chunks: ['hello'], }), ], optimization: { splitChunks: { chunks: 'all' //提取公共js、css到一个单独的bundle } } }
-
动态导入:按需加载,使用ES Module中的import()函数来实现;动态导入的模块会被自动分包,无需额外配置,适合单页应用对路由组件的按需加载
- 在import()函数中使用指定注释可以用来指定打包的bundle文件名称,例如
import(/*webpackChunkName: utils*/ './utils')
- MiniCssExtractPlugin:用来实现css文件的按需加载,使用这个插件的时候,css会被提取到一个单独的文件,可以直接通过link标签引入,而不是style标签,因此不需要再使用style-loader,而转为使用MiniCssExtractPlugin.loader;当css文件的大小并不是很大的时候,意义不是很大
- OptimizeCssAssetsWebpackPlugin:压缩css样式文件,单独的css文件不会默认被webpack压缩,需要借助额外的插件
- 压缩的插件默认不应该放在plugins中,因为开发阶段并不需要对js与css进行压缩,应当将相应的插件放入optimization属性的minimizer配置中,这样当以production生产模式进行打包时,就会压缩相应的资源,而开发模式不会进行压缩
- 在import()函数中使用指定注释可以用来指定打包的bundle文件名称,例如
-
-
Hash文件名:输出文件名使用占位符实现输出带hash值的文件名,有三种hash模式
- [hash]:全局hash,每次打包hash值都会发生变化
- [chunkhash]:相同chunk使用相同的hash值,每次修改,只有对应发生修改的chunk的hash会发生变化
- [contenthash]:不同的文件会有独立的hash值,每次修改,只有对应文件发生修改的hash会发生变化
在占位符中使用冒号+数值,可以指定hash位数,使用[contenthash: 8] 8位contenthahs来控制缓存在实际应用中较为合适