这里有一份简洁的前端知识体系等待你查收,看看吧,会有惊喜哦~若是以为不错,麻烦star哈~css
webpack 是当下最好用的前端模块打包工具,前端开发人员平常都要跟脚手架打交道,而手脚架就是基于webpack构建的,深刻理解webpack,对咱们平常工做意义重大。html
本文将系统讲解webpack,老规矩,从应用维度跟设计维度展开,因为做者能力有限,有些部分暂时整理不出来,若是你有所补充,欢迎push~前端
从技术的应用维度看,首先考虑的是要解决什么问题,这是技术产生的缘由。问题这层,用来回答“干什么用”。node
随着前端的演变,网页已经至关于一个功能丰富的应用,前端面临诸多挑战:react
因为前端界存在种种问题,webpack应运而生。jquery
webpack官方定义就是一个模块打包工具。webpack不只支持 ES module 的语法,也支持 CommonJS 的语法。webpack
webpack 不只能够打包JS,也能够打包其余格式的文件, 好比css、image等。git
技术被研发出来,人们怎么用它才能解决问题呢?这就要看技术规范,能够理解为技术使用说明书。技术规范,回答“怎么用”的问题,反映你对该技术使用方法的理解深度。github
这里要重点讲解webpack的配置。web
webpack 配置文件须要指定 mode,默认是production,打包后的文件会被压缩。能够指定成 development。不设置mode,会有警告
module.exports = {
// ……
mode: 'production',
}
复制代码
entry 支持配置多项,每一项的key是文件名,当entry配置多个入口文件时,output的filename不能写死,否则会报错,能够写成 filename: [name].js
。
output 还能够配置publicPath,从而设置导出JS文件的前缀,经过这个配置,能够设置CDN地址。
module.exports = {
entry: {
main: './src/index.js',
sub: './src/index.js'
},
output: {
filename: '[name].js', // 设置成 index.js 会报错
chunkFilename: '[name].chunk.js',
publicPath: 'http://cdn.com',
path: path.resolve(__dirname, '../dist')
}
}
复制代码
loader就是打包方案。
好比,打包jpg图片时,可使用 file-loader 进行打包,也可使用url-loader,url-loader包含了 file-loader 全部的功能,并且,他默认会将图片转成base64格式。若是不但愿转成base64位,能够设置 limit,这样的话,当图片大于设置值的时候,就不会转成base64。
// webpack.config.js
module.exports = {
module: {
rules: [{
test: /\.(jpg|png|gif)$/,
use: {
loader: 'url-loader',
options: {
name: '[name]_[hash].[ext]',
outputPath: 'images/',
limit: 10240
}
}
}]
}
}
复制代码
又好比,打包css文件时,须要同时引入两个loader:css-loader跟style-loader。css-loader会分析css文件的引用关系,最后把css文件合并成一段css。而style-loader的做用是把这段css放到head下的style标签下。
若是是scss文件,还须要引入 scss-loader。若是须要css自动添加浏览器产商前缀,则须要引入postcss-loader。
因为css中还能够引用其余的css,为了不出错,须要在css-loader 中设置importLoaders。
为了防止css全局污染,咱们须要引入css modules的概念,也就是css模块化。一样须要在css-loader 中设置 modules 为true。引入时可使用相似的方式: import style from './index.css'
若是css中引用了字体文件,还须要对 字体文件的格式设置loader,使用 file-loader 便可。
module.exports = {
module: {
rules: [{
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2
}
},
'sass-loader',
'postcss-loader'
]
}, {
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: true
}
},
'postcss-loader'
]
}, {
test: /\.(eot|ttf|svg)$/,
use: {
loader: 'file-loader'
}
}]
},
}
复制代码
// postcss.config.js
module.exports = {
plugins: [
require('autoprefixer')
]
}
复制代码
若是有多个loader时,会从后往前执行。
使用plugins让打包更便捷。
plugin 很像生命周期函数,能够在webpack 运行到某一个时刻,帮你处理一些事情。
htmlWebpackPlugin 会在打包结束后,自动生成一个HTML文件,并把打包生成的JS自动引入到HTML中。
cleanWebpackPlugin 会在打包以前,删除某一个文件夹(好比dist文件夹)
module.exports = {
// ……
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html'
}),
new CleanWebpackPlugin(['dist'], {
root: path.resolve(__dirname, '../')
})
]
}
复制代码
sourceMap 是一个映射关系,当咱们打包的文件报错时,它能提示咱们是源文件的出错位置,而不是打包后文件的出错位置,这样就利于调试。
使用方式,就是在 webpack 中配置 devtool: 'source-map'
development 环境,推荐使用 devtool: 'cheap-module-eval-source-map'
production 环境,推荐使用 devtool: 'cheap-module-source-map'
module.exports = {
// ……
mode: 'development',
devtool: 'cheap-module-eval-source-map',
}
复制代码
每次修改代码都要从新打包,这是很是繁琐的,咱们能够修改npm scripts成webpack --watch
。
但若是咱们想实现更酷炫的效果,好比自动打开浏览器、自动刷新浏览器等,这个操做就作不到。此时,能够借助webpackDevServer 来实现。
webpack支持 devServer , 能够帮咱们启动了一个服务器。咱们在平常开发中,常常须要发ajax请求,这大大提升了咱们的开发效率。此外,devServer打包后的文件,其实保存在内存中,这样打包的速度更快。
以前 devServer 还不够完善,因此不少脚手架工具会本身实现一个devServer,如今webpack的devServer已经很是完善了。
更多请参考官网
module.exports = {
devServer: {
contentBase: './dist', // 服务器的根目录
open: true, // 自动打开浏览器
port: 8080,
hot: true,
proxy: { // 设置代理,访问/api,直接转发到3000端口
"/api": "http://localhost:3000"
}
},
}
复制代码
咱们也能够手写一个 devServer,新建server.js,增长 npm scripts : node server.js
。
const express = require('express);
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const config = require('./webpack.config.js');
const complier = webpack(config)
const app = express(); // 启动一个http服务器
app.use(webpackDevMiddleware(complier, {}))
app.listen(3000, ()=>{
console.log("server is running");
})
复制代码
经过这个例子,也能够看到webpack有两种使用方式,一种是在命令行中使用,一种是在node中直接使用webpack。
在命令行使用webpack,能够参考官网资料
在node中使用webpack,能够参考官网资料
咱们每一次修改代码,devServer都会帮咱们刷新页面,但咱们只但愿显示修改的内容,而不刷新页面,此时就要用到热更新。
const webpack = require('webpack');
module.exports = {
devServer: {
contentBase: './dist',
open: true,
port: 8080,
hot: true,
hotOnly: true, // 可选,即使hot功能不生效,浏览器也不刷新
},
plugins: [
new webpack.HotModuleReplacementPlugin()
],
}
复制代码
有些浏览器还不支持ES6的语法,此时就须要用babel转义,将ES6语法转成ES5。
babel提供了详细的使用指南,在官网setup页面选择webpack选项就能够看到。
babel的使用分为三步:
npm install --save-dev babel-loader @babel/core
复制代码
babel-loader @babel/core 是 babel 跟 webpack 之间的桥梁
module: {
rules: [
{ test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" }
]
}
复制代码
npm install @babel/preset-env --save-dev
复制代码
// .babelrc
{
"presets": ["@babel/preset-env"]
}
复制代码
@babel/preset-env 将ES6的语法转义成ES5语法,好比let转成var
@babel/polyfill 为低级浏览器注入了ES6的对象或方法,好比promise
引入 @babel/polyfill 会让咱们的文件变得很是大,能够配置 useBuiltIns 作到按需加载
babel 内容很是多,建议直接看官网。
module.exports = {
// ……
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: [["@babel/preset-env", {
targets: {
chrome: '67', // 告诉 babel 目标浏览器,babel能够根据目标浏览器决定是否作转化,这样就能够减小最终输出文件的大小
},
useBuiltIns: 'usage // 提升性能:用到的才引用
}]]
}
// use: [{
// loader: 'babel-loader'
// }, {
// loader: 'imports-loader?this=>window'
// }]
}]
},
}
复制代码
babel-loader 中 options 的内容会很是多,能够把 options 的内容放到 .babelrc 中
咱们引入一个模块,只会引入该模块中的部分方法,tree shaking会帮咱们把不须要的方法过滤掉,这样打包后的文件将显著变小。
tree shaking 只支持 ES Module 的引用方式。
development 模式下,默认是不支持 tree shaking,咱们须要配置 optimization 的 usedExports 为true。
咱们还须要给 package.json 增长一项配置 "sideEffects": ["@babel/polyfill", "*.css"]
,这样打包时 tree shaking 对 @babel/polyfill就不会有做用。这是由于咱们引用@babel/polyfill时,是import @babel/polyfill
,tree shaking 会认为 @babel/polyfill 不须要引用任何东西,从而把它忽略掉。同理,咱们可使用 import "index.css"
,咱们一样不但愿tree shaking生效,咱们能够在sideEffects中增长 *.css 的配置。
若是设置"sideEffects": false
,表示对全部模块都作 tree shaking。
development 模式下,为了调试方便,虽然设置了 tree shaking,但打包出来的文件同样包含没有引用的模块。当咱们上线时,会设置 mode 为 production,此时 tree shaking 就会把不须要模块的代码过滤掉。
module.exports = {
// ……
mode: 'development',
optimization: {
usedExports: true,
},
}
复制代码
因为 development 和 production 模式下的配置是不同的,咱们能够抽离出公共的 webpack.common.js,以及Dev下的 webpack.dev.js 和 production下的webpack.prod.js。最后使用 webpack.merge 合并。
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common.js');
const devConfig = {
// ……
}
module.exports = merge(commonConfig, devConfig);
复制代码
webpack代码分割底层使用了 splitChunksPlugin 插件。官网资料地址。
module.exports = {
splitChunks: {
chunks: "async", // 可选:all。async 只代码分割异步代码,all同时支持同步跟异步的方式
minSize: 30000, // 小于 30KB 不作代码分割,大于 30KB,还要根据cacheGroups的规则决定是否作代码分割
maxSize: 20000, // 大于 20KB 尝试作代码分割,但因为基础库是不容许分割的,因此通常不生效
minChunks: 1, // 引用超过1次,会作代码分割。
maxAsyncRequests: 5, // 遇到的前5个库,会作代码分割,超过5个的不作代码分割
maxInitialRequests: 3, // 入口文件最多只能作三次代码分割
automaticNameDelimiter: '~', // 文件链接符
name: true, // 为true时,cacheGroups的filename才会生效
cacheGroups: { // 缓存组,知足以上要求的文件,会根据缓存组中的要求,加入缓存组,最终打包成文件。这样的好处是能够把多个库,输出成一个文件。此外,若是配置了上面的参数却没有代码分割,极可能就是缓存组的配置不知足
vendors: {
test: /[\\/]node_modules[\\/]/, // node_modules目录下的文件会被匹配
priority: -10, // 优先级,值越大优先级越大
// filename: 'vendors.js'
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true // 若是一个模块已经被打包了,则再打包时会忽略该模块
}
}
},
}
复制代码
小技巧:splitChunks 若是不配置,默认值就是上面的这些选项。简便写法是:
module.exports = {
// ……
splitChunks: {
chunks: "all", // 咱们要对同步跟异步代码都作代码分割,因此改为all
},
}
复制代码
咱们也能够为css进行代码分割,使用到的插件是MiniCssExtractPlugin,只须要用MiniCssExtractPlugin提供的loader 代替 style-loader 便可,具体内容看官网。
最佳实践回答“怎么能用好”的问题,反映你实践经验的丰富程度。
webpack 不建议全局安装,由于每一个项目依赖的webpack版本可能不一样,全局安装可能致使项目依赖的webpack版本不对而没法运行,建议局部安装,也就是 npm i webpack webpack-cli -D
。
局部安装完webpack后,若是要查看webpack的版本,执行 webpack -v
是得不到预期的结果,由于webpack并没全局安装,此时要执行 npx webpack -v
,npx是npm提供的命令,它会在咱们当前目录下的node_modules文件下寻找安装过的依赖。
若是咱们是在package.json文件中配置 npm scripts,则不须要npx这个指令,由于 npm scripts 默认会在当前目录下的 node_modules 寻找依赖。
安装webpack时,能够指定webpack的版本号,若是不清楚webpack有什么版本,可使用npm info webpack
查看
webpack-cli 容许咱们在命令里使用webpack这个命令。
若是把全部的代码都打包到一个文件里,会带来两个问题:
code splitting 是代码分割,没有webpack咱们也能够手动作代码分割,从而提高性能。webpack能帮咱们自动完成代码分割。
webpack 中实现代码分割有两种方式:
module.exports = {
// ……
optimization: {
splitChunks: {
chunks: 'all',
}
},
}
复制代码
咱们可使用webpack提供的analyse工具进行打包分析.
咱们在打包时,须要增长命令webpack --profile --json > stats.json
,也就是 webpack --profile --json > stats.json --config ./build/webpack.dev.js
打包完的文件中,就会出现stats.json的文件。
咱们打开连接,上次stats.json,就会出现打包的分析报告。
webpack官方提供的分析工具,除了analyse,还有这些:
webpack 建议咱们写异步加载代码,也就是异步import。当代码执行时,才会去加载相应的代码,这样首屏的代码利用率就能够提升。相似:
document.addEventListener('click', () => {
import('./click.js').then(({ default: func }) => {
func()
})
})
复制代码
咱们能够用Chrome的coverage工具看代码的利用率。
但咱们不必定要等到用户操做时,才去加载相应的代码,而是在网络空闲时就去加载。咱们能够在webpack中进行配置。具体作法以下:
// 关注下魔法注释,js的新特性
document.addEventListener('click', () => {
import(/* webpackPrefetch: true */ './click.js').then(({ default: func }) => {
func()
})
})
复制代码
Prefetching/Preloading 是有区别的:
因此Prefetching会更合适。
咱们打包的文件,浏览器是会缓存的,当咱们修改了内容,用户刷新页面,此时加载的仍是缓存中的文件。为了解决这个问题,咱们须要修改production模式下的配置文件。
const commonConfig = require('./webpack.common.js');
const prodConfig = {
mode: 'production',
devtool: 'cheap-module-source-map',
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].js'
},
optimization: {
runtimeChunk: {
name: 'runtime' // 旧版本必须配置此项,不然即使文件内容没有发生改变,hash值也会改变
},
}
}
module.exports = merge(commonConfig, prodConfig);
复制代码
contenthash 会根据文件内容生成hash值,当咱们文件内容改变时,contenthash就会改变,从而通知浏览器从新向服务器请求文件。
之因此在旧版webpack下,文件代码没变,生成文件的hash值也会改变的缘由是:
咱们业务逻辑的代码打包到main.js里,依赖库的代码打包到vendors.js里,但main.js跟vendors.js是有依赖关系的,这些依赖关系的代码会保存在manifest文件里。manifest文件既存在于main.js,也存在vendors.js里。而在旧版webpack每次打包manifest可能会有差别,这个差别致使vendors.js的hash值也会改变。设置runtimeChunk后,manifest相关的代码会被抽离出来,放到runtime文件里去,这样就能解决这个问题。
jQuery时代,咱们须要先引入jQuery,再引入其余依赖jQuery的类库,好比jQuery.ui.js。这在webpack中就有问题。好比这样:
// a.js
import $ from 'jquery'
import 'jquery.ui'
复制代码
这样引用,jquery.ui.js会报错:找不到只能在a.js里引用,jquery.ui.js是引用不到$的,而jquery.ui.js是第三方库,咱们又不能去修改jquery.ui.js的代码,怎么办呢?
咱们能够添加ProvidePlugin插件。在ProvidePlugin里咱们设置了,在当前又没引用
指向jquery。
关于ProvidePlugin,能够到官网看资料
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.ProvidePlugin({
$: 'jquery',
_join: ['lodash', 'join']
}),
],
performance: false, // 额外补充:设置为false,打包时不会警告性能方面的问题
}
复制代码
关于垫片机制,还有其余用法。好比咱们在一个模块中全局打印this,发现this指向的是模块自己,而不是window对象,若是想要让this指向window,能够这样:
module.exports = {
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
use: [{
loader: 'babel-loader'
}, {
loader: 'imports-loader?this=>window'
}]
}]
},
}
复制代码
咱们须要安装imports-loader,设置this指向window。
关于shimming,能够看官网资料
咱们写的库,要支持多种方式的引用,诸如:
// library.js
export function math (){
console.log(1);
}
复制代码
import library from './library' // ES module
const library = require('library'); // commonjs
require(['library'], function(){}) // AMD
复制代码
咱们能够配置libraryTarget:
module.exports = {
mode: 'production',
entry: {
main: './src/index.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
libraryTarget: 'umd' // umd 是通用的意思。配置了umd,就能够支持不一样的引用方式
}
}
复制代码
若是你还想经过script标签引用,好比<script src="./library.js"></script>
,那还须要配置
module.exports = {
mode: 'production',
entry: {
main: './src/index.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
library: 'library', // 打包好的代码,挂载到页面library这个全局变量上
libraryTarget: 'umd' // umd 是通用的意思。配置了umd,就能够支持不一样的引用方式
}
}
复制代码
打包后到浏览器控制台输入library,就会打印出全局变量
这里要重点讲下 library 跟 libraryTarget 的关系。library的含义是要生成一个全局变量, libraryTarget 的含义是这个全局变量挂载到哪里。若是libraryTarget设置为umd,则二者关系不大。若是咱们把libraryTarget 设置成this,则打包出来的文件不支持ES module、commonjs等的引用,他表示全局变量挂载到全局的this上。此时咱们在浏览器控制台,输入this.library
,就能够找到该全局变量。libraryTarget 还设置成 window、global。
此外,咱们在打包时,还会出现一种状况:
// library.js
import _ from lodash;
// 其余代码
复制代码
// a.js
import _ from lodash;
import library from './library';
复制代码
这就形成lodash被打包了两次,咱们但愿library.js打包时,不要把lodash打包进去,能够这样配置:
module.exports = {
externals: ['lodash] // 打包时,遇到lodash,自动忽略
}
复制代码
咱们把生成的文件放到线上,用户就能够访问,若是服务器挂了,页面就会显示异常。这种时候,咱们但愿即使服务器挂了,用户仍是能够看到以前访问的页面,而这就是PWA。
要作到PWA很简单,首先配置webpack。
const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
module.exports = {
plugins: [
new WorkboxWebpackPlugin.GenerateSW({
clientsClaim: true,
skipWaiting: true
}),
]
}
复制代码
而后在业务代码中,使用 serviceWorker。
if('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
.then(registration => {
console.log('service-worker register')
}).catch(error => {
console.log('service-worker register error')
})
})
}
复制代码
咱们在开发时,常常会遇到跨域的问题,devServer 能够帮助咱们实现请求转发,具体看官网资料
咱们只须要在webpack进行以下配置:
module.exports = {
devServer: {
proxy: {
'/react/api': {
target: 'https://baidu.com/',
secure: false, // false 时才能够对https请求的转发
pathRewrite: {
'header.json': 'demo.json'
}
}
}
},
}
复制代码
devServer 中的proxy容许咱们作请求转发。当咱们请求'/react/api'就会被转发到http://baidu.com/
的域名下。此外,咱们在平常开发中,可能须要用到header.json这个API时,该API还没开发完,咱们须要先访问demo.json进行开发,此时就能够设置pathRewrite达到效果。
webpack的proxy内容很是多,底层使用的是http-proxy-middleware,能够上GitHub看相应的资料
咱们在写react应用时,须要用到react-router-dom实现路由,可是咱们会发现,当咱们访问/list时,预期是要访问到/list路由,但浏览器会发送请求到/list,从而页面显示出错。
要解决这个问题,就要用到devServer的historyApiFallback这个API了。
当咱们在devServer里配置historyApiFallback:true
后,咱们无论访问什么url,都会执行根目录(也就是/)下的代码,从而解决了单页路由的问题。
更多内容,看官网
eslint 跟 webpack关系不大,不少编辑器是能够支持eslint插件,但可能咱们为了调试方便,但愿eslint的错误显示在浏览器上,这样即使咱们编辑器没有eslint插件,也能够看到问题。
操做起来也很简单,咱们先在本身的工程里安装eslint跟eslint-loader.
module.exports = {
devServer: {
overlay: true // 必须配置
},
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: ["babel-loader", "eslint-loader"], // 使用 eslint-loader
},
]
}
复制代码
关于eslint-loader,能够看官网资料
跟上技术迭代
webpack运行于node,node版本更新,天然会提升webpack的打包效率,npm跟yarn等包管理工具也是同理。
在尽量少的模块上应用loader:
为loader配置exclude,例如: exclude: /node_moudles/
为loader配置include,例如:include: path.resolve(__dirname, '../src')
plugin 尽量精简并确保可靠
好比开发环境下,不须要对代码进行压缩,这样就不须要在webpack.dev.js里引入相关的plugin
合理配置resolve
当咱们没有写js后缀时,webpack默认会帮咱们补上,若是咱们的jsx文件也不但愿写上后缀,但愿webpack会帮咱们默认补上,能够设置resolve。
module.exports = {
resolve: {
extensions: ['.js', '.jsx'],
mainFiles: ['index', 'child'] // 当咱们引用的是某个路径,webpack默认会帮咱们读取index文件,若是咱们设置了['index', 'child'],webpack会逐一帮咱们查找index.js index.jsx child.js child.jsx
}
}
复制代码
随着技术生态的发展,和应用问题的变迁,技术的应用场景和流行趋势会受到影响。这层回答“谁用,用在哪”的问题,反映你对技术应用领域的认识宽度。
为了解决用户的问题,技术自己要达成什么目标。这层定义“作到什么”。
webpack就是个模块打包工具,咱们须要使用webpack打包不限于JS、CSS、image等文件。
不一样的文件,打包策略是不一样的,因此咱们须要配置loader。
打包过程当中,咱们可能还须要作一些额外的操做,因此咱们须要配置 plugin。
因此学习webpack的重点,就是理解与掌握loader跟plugin。
webpack 默认会读取 webpack.config.js文件,咱们也能够更改默认的文件名npx webpack --config webpack.xxx.js
注:webpack开发团队为了提升开发体验,一直在丰富webpack的默认配置,因此咱们虽然没有指定JS的打包策略,但同样能够打包成功。
为了达到设计目标,该技术采用了什么原理和机制。实现原理层回答“怎么作到”的问题。把实现原理弄懂,而且讲清楚,是技术人员的基本功。
每种技术实现,都有其局限性,在某些条件下能最大化的发挥效能,缺乏了某些条件则暴露出其缺陷。优劣局限层回答“作得怎么样”的问题。对技术优劣局限的把握,更有利于应用时总结最佳实践,是分析各类“坑”的基础。
技术是在迭代改进和不断淘汰的。了解技术的前生后世,分清技术不变的本质,和变化的脉络,以及与其余技术的共生关系,能体现你对技术发展趋势的关注和思考。这层体现“将来如何”。