npm script 命令给 webpack 传参
1 | npm run build -- --config webpack.config.js |
加上两个杠,表示后面是字符串,是参数。
dev-server
1 | devServer:{ |
Html-webpack-plugin
1 | new HtmlWebpackPlugin({ |
loader
css-loader 主要是解决@import
、图片路径等这些语法
1 | { |
抽离 css
使用插件mini-css-extract-plugin
1 | const MniCssExtractPlugin = riquire("mini-css-extract-plugin"); |
css 自动加前缀
使用 autoprefixer 插件,但是需要 postcss-loader 对它进行解析。
在 css-loader 之前 less-loader 使用 postcss-loader
压缩 css
默认情况下,webpack 使用uglifyjs-webpack-plugin
插件在生产环境打包时已经压缩了 js,但如果使用了mini-css-extract-plugin
,则需要自己单独再配置压缩优化相关项,可查看官网。
压缩 js 可以使用uglifyjs-webpack-plugin
,也可以使用下面的。压缩 css 需要插件optimize-css-assets-webpack-plugin
。
1 | const TerserJSPlugin = require('terser-webpack-plugin'); |
转换 es6 语法
babel-loader
babel-core
babel/preset-env
@babel/plugin-transform-runtime 和 @babel/runtime 解决更高级语法 api 没有的问题,比如 generator
js 规范
使用 eslint-loader 和 eslint
如果只是使用 eslint,帮助在开发过程中进行检测,会提示文件中语法不符合规则的部分。但是在打包的时候不会做校验。使用 eslint-loader,在 webpack 进行打包的时候进行校验。
eslint-loader 通常单独配置,不建议和 bable 等其他处理 js 的 loader 写在一块。可以通过enforce
设置执行顺序,
1 | { |
暴露第三方模块给全局
比如我们安装了 jquery,我们可以在每个模块中 import 进来,然后使用,这样是没问题的,但是如果想通过 window 访问这些包,就不行了,可以通过下面的方法来实现。
下面简单说下 loader 的分类:
- pre 前置 loader,前面执行的 loader
- normal 普通的 loader
- 内联 loader
- post 后置 loader
比如使用expose-loader
。
- 通过内联 loader 的方式来实现把 jquery 暴露给 window。
1 | import $ from "expose-loader?$!jquery"; |
但是上面的写法不美观,可以在配置文件中配置。
- 在配置文件中配置
1 | { |
不用引用直接使用呢?
- 在每个模块中注入$对象
通过webpack
的ProviderPlugin
插件
1 | const webpack = require("webpack"); |
不打包指定模块
比如在index.html
中手动引入了jquery
包,那就不需要对jquery
进行打包。通过设置webpack
的externals
属性可以实现该功能。
1 | externals:{ |
即使在代码中使用 import 的方式引入,也不会进行打包。
图片处理
file-loader 默认会在内部生成一张图片到输出目录下,把生成的图片的名字返回回来。 ### 在 html 中使用 img
因为 webpack 会对模块中引用的 img 进行打包转换等处理,所以如果直接在 index.html 中使用 img 是识别不了的。如下
1 | <img src="./logo.png" /> |
因为打包后原来的 logo.png 的名字和路径都发生了变化,所以这种写法是肯定拿不到的。
使用html-withimg-loader
可以解决这个问题。
1 | { |
小图片的处理
使用url-loader
可以使用 base64 的方式减少对图片的请求。
资源分类
- 图片
1 | { |
- css
1 | new MiniCssExdtractPlugin({ |
就是在各个资源前面加上文件夹名即可
如果需要在每个资源前面统一加上一个前缀,比如 cdn,可以配置 webpack 输出路径的 publicpath
1
2
3
4
5
6
7 {
output:{
filename:'bundle.js'.
path:path.resolve(__dirname,'build'),
publicPath:'https://www.baiducdn.com/'
}
}如果需要在不同的资源前面家不同的 cdn,可以在各自 loader 上加上 publicPath
1
2
3
4
5
6
7
8
9
10
11 {
test:/\.(jpg|png|jpeg|svg)/
use:{
lader:'url-loader',
options:{
limit:2*1024,
outputPath:'img/',
publicPath:'https://www.baidu.com/'
}
}
}
多页面应用
1 | const path = require("path"); |
代码映射
配置了 sourcemap 不仅仅是开发的时候有映射,生产环境也会有(当然生产环境不配置就没有了)。
1 | { |
- source-map
源码映射,会单独生成一个 source map 文件,出错了会列出报错的列和行号。 - eval-sorce-map
不会产生单独的文件,但是可以显示出错的行和列;并且在浏览器中也能看到源码。eval 标明,这种方式是使用 eval 的方式把源码生成字符串,集成在打包后的文件中,然后 eval 出来。 - cheap-module-source-map
不显示出错的行和列,但是生成一个单独的文件,并且和调试没有关联起来。 - cheap-module-eval-source-map
不生成文件,集成在打包后的文件中, 也不会产生列
监控
1 | { |
小插件
- cleanWebpackPlugin
每次打包,先清空指定目录中的旧文件。
1 | new cleanWebpackPlugin("./dist"); |
- copyWebpackPlugin
打包的时候,有些静态资源可能不会被打包,但也是需要放到打包目录中,使用这个插件,就可以实现该功能。
1 | new copyWebpackPlugin([{ from: "doc", to: "/" }]); |
- bannerPlugin(内置的 )
给打包后的代码加上版权信息。
1 | new webpack.BannerPlugin("make by wenmu 2019 "); |
跨域问题
就是设置devServer
的proxy
1 | devServer:{ |
上面就把以/api
开头的请求链接都转到http://localhost:4567
的服务上。比如/api/user/getDetail
,发出的请求就是http://localhost:4567/api/user/getDetail
。
如果服务端的链接不带/api
怎么办?
1 | devServer: { |
前端自己 mock 数据,不借助服务端
我们知道,其实 devServer 本身就是一个 node 服务。我们可以使用这个服务直接处理请求。这需要借助 devServer 的 before 钩子函数
1 | devServer: { |
app
就是devServer
中的express
实例。
如果一个一个的请求都写到这里面,这个文件会比较大,所以可以把相关的请求函数封装到一起,在这里调用即可。通常使用第三方封装的组件mocker-api
帮助 mock 数据。mocker-api
就是这个原理。
服务端 node 启动 webpack
使用node
启动webpack
,这样在node
中写的mock
数据就和webpack
启动的前端是一个端口,就不存在跨域问题了。
在服务端中启动webpack
,需要使用webpack
的中间件webpack-dev-middleware
1 | const express = require("express"); |
resolve
resolve 的作用就是解析,项目中的各种解析都可以在这里做相关的配置。
- 第三方包的查找
默认情况下,当引入一个包时,首先在node_modules
中查找,如果找不到就再去全局包安装目录查找,再找不到就报错了。我们可以通过配置modules
来强制指定只在哪些目录中进行查找,比如只在node_modules
中
1 | resolve:{ |
- 别名
如果一个路径的比较长,引用起来比较麻烦,可以通过在 resolve 中对路径设置别名来解决。
比如应用 bootstrap 的样式,bootstrap/dist/css/bootstrap.css
1 | resolve: { |
这是可以直接引用BootstrapStyle
1 | import BootstrapStyle; |
- 优先读取的 package 字段
在引用一个包时,默认是读取包中package.json
中main
字段配置的文件,如下是bootstrap
的package.json
,当import bootstrap
时,读取的是 js 文件。
1 | style:'/dist/css/bootstrap.css', |
我们可以设置引入包时,优先读取的字段,比如优先读取style
字段,找不到了再读取main
字段对应的文件。
1 | resolve: { |
这样当import bootstrap
时,bootstrap 的样式就被加载了。
- 默认扩展名
在引入一些文件时,比如 js,后缀名是不需要写的,但是 css 的后缀是必须写的,我们是可以设置一些类型不需要些扩展名的
1 | resolve: { |
引入一个文件时,会先找 js,找不到再找 css,依次 jsx、json 等等。当匹配到一个时,就不会继续再匹配了。因此存在多种类型的文件使用一个文件名时,当引入这个文件名时,只会加载设置的第一个类型。如果引用 css,还是需要加上后缀。
环境变量
webpack 提供了一个插件可以帮助我们定义一些环境变量;webpack.DefinePlugin
。
1 | new webpack.DefinePlugin({ |
这样上面定义的变量就可以直接使用了。
细心的同学发现,为什么NAME
的值要用双引号把包括单引号呢?
因为在读取变量时,会把后面单引号中的直接复制给变量,那读取上面的NAME
就成了wenmu
,而不是'wenmu'
那就会报错,说 wenmu 未定义了,当成一个变量了。比如 Express 就是 2,不是1+1
字符串。
可以使用双引号再包裹一下,但推荐使用第二种的写法,可读性强。
区分不同的环境
通常开发和生产使用的 webapck 配置文件是不一样的,但是大部分的配置是一样的,因此写一个 base 文件,再写两个开发和生产的“继承”base 即可。继承就用到了webpack-merge
.
1 | const { smart } = require("webapck-merge"); |
webpack 优化
- noParse
noParse 指定不需要解析的包,当一个包比较大,并且没有依赖其他包时,可以使用 noParse 指定,这样可以减少打包时间。
1 | module.exports = { |
- 设置 loader 解析的范围
1 | module: { |
- ignorePlugin
在使用一个写第三方包时,包可能会引入很多文件,但是一些文件可能对我们的项目是没用的。
比如有一个 moment,这个包做了国际化处理,默认它会把所有的语言包都加载,如果我们只使用了中文,这样打包的时候就比较大,这时可以使用 webpack 提供的 IgnorePlugin 来指定这个包哪些文件不需要加载。
1 | plugins: [new webpack.IgnorePlugin(/\.\/locale/, /moment/)]; |
这时国际化就不管用了,中文的语言包需要手动引入。
1 | import "moment/locale/zh-cn"; |
动态链接库
默认在打包时,所有的包都会被到包在一起,包括第三方库,这样每次打包的时候,第三方库和有些公共的文件也需要每次都重新进行打包,这样不仅打出的包比较大,并且打包会比较慢。
因此我们可以把第三方库和一些公共的文件抽离出来,先单独进行打包。比如 react、react-dom 等等。然后开发的时候引用我们单独打好的包,这样每次打包的时候,单独抽离出来的包就不会再次进行打包了。
1 | /******/ var abc= (function(modules) { // webpackBootstrap |
上面是打包后的代码结构,大部分删除了。var abc=
是手动写上的。
简单讲解下打包后文件的代码结构。
一个模块打包后,原来的模块会被封装成一个立即执行 function,js 中其实类或模块都是 function 的语法糖吗,这个 function 会把打包的模块做一个对象返回。我们在引用模块的时候,其实就是接受这个 function 的返回值。
我们把这个函数的返回值赋给一个变量,然后
基于上面的讲解,我们可以在配置文件中指定,接收打包后返回值的变量名。
1 | const path = require("path"); |
加上library
的配置后,上面的手动写的var abc=1
就不用手动写了,打包出来的就是这样的。
动态链接库就是基于上面的原理来做的,只是我们需要打包的是 react、react-dom 等包。
- 如何让 webpack 知道上面生成的文件就是动态链接库呢?
这就需要用到webpack
的自带插件DllPlugin
,它能指定把哪个文件打包成动态链接库,并且用一个清单(manifest.json
)的方式进行管理。manifest.json
记录了如何查找动态链接库中的文件。
1 | const path = require("path"); |
使用 webpack 编译后,会生成一个_dll_react.js
和manifest.json
两个文件。
_dll_react.js
中就是打包后的react
和react-dom
manifest.json
,记录了_dll_react
中的依赖关系。这个主要是被DllReferencePlugin
使用。
打包完以后,就需要在入口 index.html 文件中引用。
1 | <script src="/_dll_react.js "></script> |
虽然引用了,但是打包的时候,react、react-dom 默认还是去node_modules
中找,找到了然后就又被打包了,那怎么办,上面的工作不是白做了吗,这就需要用到webpack
的另一个插件DllReferencePlugin
。
1 | new webpack.DllReferencePlugin({ |
注意:这个和
DllPlugin
不在一个配置文件中,这个是开发打包使用的webpack配置文件
,而DllPlugin
是专门用来提取的公共包的webpack配置
文件。
这样配置后,打包的时候会先去清单中查找包,找不到了再把包打包进当前的代码中。这样打包出来的文件往往很小,因为公用包都已经在_dll_react.js
中了
多线程打包
多现成打包主要用到了happypack
插件。这个很简单,主要就是把原来的 loader 换成 happypack 的 loader,然后在 plugin 中配置 happypack 即可。
1 | exports.plugins = [ |
非常 easy.
webpack 自带的优化功能
- tree-shaking
当我们使用 import 的方式引用一个模块时,如果只是引用了模块的部分方法,那在生产环境打包的时候,那些没有引用的方法会被删除掉。不会被打包进来。
但当使用 require 的方式引用模块时不会删除没用到的代码。
这是由require
和import
的机制相关的。 - scope hosting 作用域提升
1 | let a = 1; |
上面的代码打包的时候,不会把 a、b、c 都打包进来,打包进来的是一个结果,console.log(6)
,类似的模块引用等都会做类似的处理。
抽取公共代码
抽取公共代码,是当有多个页面的时候(不都是单页面应用 ),多个个页面之间有公共的模块引用,打包的时候,默认会把公用的包在每个引用的页面中都打一份,造成公共代码的重复;
比如现在有多个入口
1 | entry: { |
当 index.js 和 other.js 中引用的有相同的模块时,可以把这些公用的模块抽离出来,这样打包的时候每个页面只有对公共模块的引用,不会在每个包中都包含一份,并且在访问 index 的时候就会被缓存,再访问 other 的时候,就不需要再下载了,从而提高性能。
1 | module.exports = { |
上面是一个很简单的配置,可以去官网查看比较全面的配置,这里需要讲解一下
chunks
的字段含义。chunks
有三个可选值,all
、initial
和async
async
:指定抽离动态引入的公共模块initial
: 指定抽离非动态引入的公共模块all
: 所有符合条件的公共模块都进行抽离
在引入模块时,通常都是非动态的引入,比如import $ from 'jquery'
;有时候也需要动态的引入一些模块,比如:import ('lodash')
抽离第三方模块
上面讲解了抽离公用的模块,但没有区分是第三方包还是程序中开发的,我们可以把这两种情况分开,把第三方包重复使用的抽离到一起,自己开发的抽离到一起。
1 | module.exports = { |
其实就是再配置一个抽离方案,但要注意加上priority
,因为程序是从上往下执行的,如果不加priority
,下面的方案就不会被执行。
我们需要让vendors
的权重比common
的大,才能先把第三方的抽离,然后再抽离公共的。当然,也可以把vendors
放到第一个。总之把条件范围小的权重大点,让先执行。
懒加载
懒加载说白就是使用 es6 草案的import ('jquery')
语法动态的加载 js。
热更新
1 | { |
tapable
Webpack
本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是Tapable
,Tapable
有点类似于nodejs
的events
库,核心原理也是依赖订阅发布模式。
SyncHook
这个比较简单,订阅的钩子一个一个都执行。syncBailHook
任何一个钩子的返回值不是undefined
,后面的钩子就不执行了。SyncWaterfallHook
上一个钩子的返回结果传给下一个。SyncLoopHook
钩子的返回值不是undefined
,就一直执行。
上面三个都是同步的 Hook,下面介绍几个异步的 Hook。AsyncParallelHook
异步并行的钩子,当所有的异步都执行完后,才执行最后的回调。这提供了判断“所有异步都结束”的方案,就是在每个钩子的回调函数必须接收一个回调的参数,这个回调的参数就是一个计数的 function,每个钩子结束后必须执行这个计数的 function。当计数和钩子的个数相等时,说明所有异步钩子执行完了。
现在使用`promise.all 实现起来就更方便了。AsyncSeriesHook
异步串行,当上一个异步执行完以后,再执行第二个异步钩子。
promise 版本实现原理
1 | promise(...args){ |
就是使用数组的 reduce 方法和 promise 返回是仍是 promise 的特性。redux 的源码也是这个原理。
- AsyncSeriesWaterfallHook
异步串行瀑布,上一个异步执行完以后再执行下一个,但是上一个钩子的结果传给下一个,如果有错误,则停止后 面钩子的执行。