导航
前置知识
wepback打包优化 ( 注意第4点splitChunks和dllPlugin的区别 )
手写一个loader
(1) 前置知识
一. 从零开始配置webpack可能会用到的依赖
- webpack webpack-cli
- html-weback-plugin
- 优化项:minify:{ collapseWhitespace, removeAttributeQuotes} 折叠成一行,删除html属性的引号
- 注意:优化项minify主要用于mode: 'production'因为压缩会增加打包时间
- collapse 是坍塌的意思
- hash: true // html文件引入资源时,在资源后面添加hash串
- template
- filename
- webpack-dev-server
- contentBase // 启动服务的文件夹
- open: true //打开浏览器
- port
- compress: true // 开启gzip压缩
- progress: true //打包进程
- hot: true //开启热更新,主要用于 webpack.HotModuleReplacementPlugin()
- proxy // 设置代理
- before // 钩子函数,用来模拟数据
- webpack-dev-server除了在配置文件中配置,还可以写成命令行,如下
- webpack-dev-server --open --compress --progress --color
- css相关
- style-loader
- css-loader
- less-loader
- 抽离css
- mini-css-extract-plugin 抽离css
- MiniCssExtractPlugin.loader代替style-loader,因为不用style方式插入,而是抽离单独文件
- 在plugins中 new MiniCssExtractPlugin({ filename, chunkFilname, ignoreOrder })
- 前缀
- postcss-loader 解决css前缀,需要另外配置postcss.config.js,注意顺序,放在css-loader下面,先加载
- 单独配置 postcss.config.js
- 需要配合 autoprefixer
- 注意:autoprefixer需要给出浏览器的一些信息,所以要在 package.json 中添加 browserslist
- autoprefixer 解决css前缀,还需要package.json中配置 browserslist
- "browserslist": [ // 在package.json中添加
"defaults",
"not ie < 9",
"last 3 version",
">1%",
"iOS 7",
"last 3 iOS versions"
]
- 压缩css
- (注意用于生产环境才去压缩css,因为影响打包速度)
- optimize-css-assets-webapck-plugin 优化css,比如压缩,压缩完后,production环境还需使用uglifyjs压缩js
- uglifyjs-webpack-plugin 压缩js
- optimization: { // 需要把 mode: 'production’才能看到压缩css和js的效果,和上面的html的优化一样
minimizer: [
new OptimizeCssAssetsWebpackPlugin({}),
new UglifyjsWebpackPlugin({
cache: true,
parallel: true, // 平行,并行的意思
sourceMap: true, // 调试映射
})
]
}
- css: 如何把所要打包的css抽离到css/目录下 => MiniCssExtractPlugin => filename => 'css/[name].css'
- https://webpack.js.org/plugins/mini-css-extract-plugin/#root
- js相关
- babel-loader
- @babel/core
- @babel/preset-env
- @babel/plugin-proposal-decorators 注意顺序和 legacy 配置项 // legacy: 是遗产的意思
- @babel/plugin-proposal-class-properties 注意loose配置项
- @babel/plugin-transform-runtime
- @babel/runtime 注意是 dependencies 而不是 devDependencies
- @babel/polyfill 注意是 dependencies 不是 devDependencies,还需在入口js文件引入,才能解析更高级的js语法,如includes
- .babelrc
-
{
"presets": [
["@babel/preset-env", {
"modules": false // 关闭babel的模块转换功能,保留 es6 模块化语法
}]
],
"plugins": [
["@babel/plugin-proposal-decorators", {"legacy": true}], // 注意顺序,需要写在class插件的前面,legacy不能少
["@babel/plugin-proposal-class-properties", {"loose": true}], // proposal:是提案的意思
["@babel/plugin-transform-runtime"], // 配置项都可以写成数组形式,后面还可根一个配置对象
["@babel/plugin-syntax-dynamic-import"] // 用于来加载模块,语法动态加载插件
]
}
- eslint eslint-loader babel-eslint
- 需要单独配置 .eslintrc.json 文件,配置rules
- eslint-loader如果需要保证加载的顺序最先执行,要设置配置项: enforce: 'pre'
- babel-eslint // 需要安装bebel-eslint插件 Cannot find module 'babel-eslint'
{
"parser": "babel-eslint", // 这里必须设置
"parserOptions": {
"sourceType": "module",
"allowImportExportEverywhere": true
},
"rules": {
"indent": "off"
},
"env": {}
}
- loader的类型
- pre 前置loader
- post 后置loader
- normal 普通loader
- inline 内联loader
- expose-loader
- expose-loader用来暴露全局对象,即可以通过window.属性的方式访问
- expose: 是暴露的意思
- 书写方式有三种
- (1)
- 直接在入口js中引入,require('expose-loader?$!jquery') // 把jquery用变量 $ 暴露给全局
- (2)
- 在webpack.config.js的module中配置expose-loader
- {
test: require.resolve('jquery'), // require.resolve()是nodejs中的函数
use: [
{
loader: 'expose-loader',
options: 'jquery' // 暴露成window.jquery
},
{
loader: 'expose-loader',
options: '$' // 暴露成window.$
}
]
},
- (3)
- 在每个模块中注入$,不需要在每个模块中再引入,可以使用 webpack.ProvidePlugin插件,再plugins中加入
- new webpack.ProvidePlugin({
$: 'jquery'
})
- 图片相关
- file-loader
- url-loader 可以设置大小限制,小于时用url-loader转成base64,大于时使用file-loader加载图片
- html-withimg-loader 在html中通过路径加载图片,在打包后能不受影响
- 图片:如何把所有要打包的图片都抽离到 img/目录下 => url-loader => options => outputPath
- css: 如何把所要打包的css抽离到css/目录下 => MiniCssExtractPlugin => filename => 'css/[name].css'
- 注意:当图片是babes64时即在limit范围内时,使用 outputPath 输出到指定文件夹无效,因为没有静态图片资源而是base64
- 公共路径
- 在output中设置 publicPath 即所有引用都会加上公共路径前缀
- 如果只是想给图片添加公共路径的话: url-loader => options => publicPath:- 打包7多页面应用
- entry对象中使用不同的key值
- output中 => filename: '[name].[hash:8].js'
- html则要多次 new HtmlWebpckPlugin()
- 但是上面生成多个html会出现每个html都应用所有的js,而不是只引用相对应的js => 通过chunks:
- source-map源码映射
- devtool: 'source-map' // 显示行数,产生map文件
- devtool: 'eval-source-amp' // 显示函数,不产生map文件
- 时时打包
- watch: true
- watchOptions: {
aggregateTimeout: 300, // 防抖
poll: 1000, // 每秒询问1000次
ignored: /node_modules/
}
- aggregate: 总计,合计的意思
- poll: 投票数的意思
- https://www.cnblogs.com/chris-oil/p/8856020.html
- clean-wepack-plugin // https://github.com/johnagan/clean-webpack-plugin
- copy-webpack-plugin // https://github.com/webpack-contrib/copy-webpack-plugin
- BannerPlugin 是 webpack自带的plugin,用于在js文件开头注释一些说明内容
- new webpack.BannerPlugin({ banner: ' by woow.wu'})
- wepack设置代理
- devServer.proxy
- proxy: {
'/api': {
target: 'http://localhost:6000',
pathRewrite: {
'/api': '' // 将/api重写成空字符串,即虽然前端请求时加了/api,但是真正代理到后端时,是去掉了/api的
}
}
}
- wepback中moke数据
- devServer.before // https://webpack.js.org/configuration/dev-server/#devserverproxy
- 这样起始就不用在自己写server了,并且也不用启动node服务,很方便
- devServer: {
before: function(app, server) {
app.get('/some/path', function(req, res) {
res.json({ custom: 'response' });
});
}
}
- resolve
- resolve.alias // 配置别名
- resolve.extensions // 引入资源时,省略后缀的加载顺序
- alias: {
component: path.resolve(__dirname, 'src/component/')
},
- extensions: ['.js', '.less', '.css']
- 环境变量
- wepack.DefinePlugin // https://webpack.js.org/plugins/define-plugin/#root
- new webpack.DefinePlugin({
DEV: JSON.stringify('dev'),
FEATURE: JSON.stringify(true)
})
- webpack-merge 在base的基础上定制化config文件
- module.exports = merge(base, devConfig)
- // https://webpack.js.org/guides/production/#setup
- happypack 多线程打包
- // https://github.com/amireh/happypack
- {
test: /\.js$/,
use: 'happypack/loader?id=js',
},
new HappyPack({
id: 'js',
use: [{
loader: 'babel-loader'
}]
}),
- tree-shaking
- tree-shaking能在打包的时候,自动去除没用的代码
- 比如一个sum,sub函数,在import后只用到了sum,则sub函数在生产环境mode:production打包会被去除
- shaking: 是摇晃的意思
- 注意:
- 只有 import 和 export 语法是支持 tree-shaking的
- require和modue.exports并不支持 tree-shaking
- 注意:
- 当使用了 optimize-css-assets-webpack-plugin后,tree-shaking会不生效,则需要下面的插件 terser-webpack-plugin
- terser-webpack-plugin
- terser: 是简要的意思
- // https://segmentfault.com/q/1010000014940767
- 抽离公共组件和第三方组件,可以缓存,则不需要重复加载
- optimization => splitChunks => cacheGroups => vendors和commons
- priority: 是优先的意思
- optimization: {
minimizer: {}, // 压缩css和js的配置项
splitChunks: {
cacheGroups: {
commons: {
name: 'commons',
chunks: 'initial',
minChunks: 1,
priority: 10,
minSize: 0,
},
vendors: { // vendor是小贩的意思
test: /node_modules/, // 范围是node_modules中的第三方依赖,注意zhe
name: 'vendors', // 抽离出来的包的名字
chunks: 'initial', // 初始化加载的时候就抽离公共代码
minChunks: 1, // 被引用的次数
priority: 11, // priority: 是优先级的意思,数字越大表示优先级越高
minSize: 0,
}
}
}
}
- 热更新
- new webpack.HotModuleReplacementPlugin() // 热更新
- new webpack.NameModulesPlugin() // 打印热更新模块的路径
- 1. 首先在 devServer 配置中增加 hot: true,表示开启热更新
- 2. 在plugins数组中 new new webpack.HotModuleReplacementPlugin()
- 3. 在入口js文件中:
- if (module.hot) { // 如果开启了热更新
module.hot.accept('./component/index.js', function () { // 路径中的js文件改变,则执行回调函数
const res = require('./component/index.js')
console.log(res.default.a, '模块热更新, 最新的a值')
})
}
- 懒加载
- @babel/plugin-syntax-dynamic-import // 语法动态引入插件
- syntax: 是语法的意思
- dynamic:是动态的意思
- import只能用在顶部报错:解决需要安装 babel-eslint 插件
- 安装,babel-eslint插件,并且在 .eslintrc.json中做如下配置
- {
"parser": "babel-eslint",
"parserOptions": {
"sourceType": "module",
"allowImportExportEverywhere": true
},
"rules": {
"indent": "off"
},
"env": {}
}
- https://stackoverflow.com/questions/39158552/ignore-eslint-error-import-and-export-may-only-appear-at-the-top-level
- 具体使用懒加载如下:
- // 懒加载
const button = document.createElement('button')
button.addEventListener('click', () => {
// console.log('button clicked')
import('./component/index.js').then(res => {
console.log(res.default.a)
})
})
button.innerHTML = 'button'
document.body.appendChild(button)
- 本质上import().then()使用 jsonp实现的
wepback 打包优化
1. exclude 和 include
- 使用 babel-loader 解析js文件时
- 如果在js中文件中,引用了第三方模块,这个时候,时不需要再用 babel-loader 进行编译的,因为已经打包过了的
- 所以添加 exclude: /node_modules/ 配置项,排除node_modules文件夹
- 注意:include,exclude没必要用于 (图片) 相关的loader
- 原理:降低loader的使用频率,从而提高webpack的打包速度
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
}
]
}
2. 在dev环境中,没有必要使用 optimize-css-assets-webpack-plugin , mini-css-extract-plugin
- 尽量减少 plugin 的使用,选择性能比较好的,官方推荐的plugin
3. resolve
- extensions
- resolve中的 extensions 中一般只对 js或者jsx这样的逻辑型文件时,使用 extensions
- 因为如果把很多后缀都配置在这里,就会调用多次底层去判断是否命中,消耗性能
- extensions: ['.js', '.jsx']
- 在引入 import A from 'component/A' 这里可以省略扩展名 .js 或者 .jsx
- mainFiles
- mainFiles: ['index']
- 如果配置了index,则在引入的时候可以这样写 import a from './src/component/'
resolve: {
modules: ['node_modules'],
alias: {
component: path.resolve(__dirname, 'src/component/')
},
extensions: ['.js', '.jsx'] // extensions是扩展的意思,扩展名
mainFiles: ['index'] // 先找以index开头的文件
}
4. DllPluguin和 DllReferencePlugin 提高打包速度 // 动态链接库
- add-asset-html-webpack-plugin // 一个在html引入静态文件的插件
- filepath: 需要引入的文件路径
- webpack.DllPlugin() // 主要是用于生成映射文件
- name: '[name]' // 必须和output中的library相同,表示暴露到全局的库的名称
- path: path.resolve(__dirname, 'dll', '[name].manifest.json') // .manifest.json就是映射文件
- webpack.DllReferencePlugin() // 最新引入的是映射文件中的依赖,而不是在node_modules中去查找
- manifest: path.resolve(__dirname, 'dll', 'vendors.manifest.json')
- 原理:
- 使用splitChunks可以单独抽离出第三方包,但是每次打包都会执行抽离的过程
- 所以需要抽离并且第一次打包后,第二次打包就不需要再打包抽离的包了
- 所以要经过两步:抽离 和 只打包一次
- 具体步骤:
- 1. 新建webpack.dll.js // 生成两类文件,一个是js文件,一个是manifest.json映射文件
- 2. 在html中引入抽离出来的js文件 // 这里可以使用插件,add-asset-webpack-plugin
- 3. 在webpack.common.js中引用manifest.json文件
- 调试
- 在webpack.common.js中注释掉 new wepback.DllReferencePlugin前后对比打包时间
- 代码
webpack.dll.js
module.exports = {
mode: 'development',
entry: {
react: ['react', 'react-dom']'lodash', 'moment']
},
output: {
filename: '[name].dll.js',
path: path.resolve(__dirname, 'dll'),
library: '[name]', // 暴露到全局的变量名
libraryTarget: 'var' // 以var的方式暴露
},
plugins: [
new webpack.DllPlugin({
name: '[name]', // name === library 必须一样 (在manifest.json中对应的各个包的名字)
path: path.resolve(__dirname, 'dll', '[name].manifest.json') // 生成在dll文件夹下的name.manifest.json文件
})
]
}
webpack.common.js
new AddAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, 'dll', '*.dll.js')
}),
new wepback.DllReferencePlugin({
manifest: path.resolve(__dirname, 'dll', 'vendors.manifest.json')
})
// https://webpack.js.org/plugins/dll-plugin/#root
- 优化
- 当webpack.dll.js中的entry每个第三方包都单独生成 .dll.js和 .manifest.json文件后,需要多次使用上面两个插件
- 所有可以用 nodejs 的 fs.readdirSync(path)去获取dll文件夹的所有文件数组
- 代码如下:
const dllPlugins = []
fs.readdirSync(path.resolve(__dirname, 'dll')).forEach(item => {
console.log(item, 'item')
if (/\.dll\.js/.test(item)) {
dllPlugins.push(
new AddAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, 'dll', item)
})
)
}
if (/\.manifest\.json/.test(item)) {
dllPlugins.push(
new wepback.DllReferencePlugin({
manifest: path.resolve(__dirname, 'dll', item)
})
)
}
})
plugins: [
...dllPlugins
]
5. happypack多线程打包
6. sourcmap调试映射文件的各种类型
7. 结合stats分析打包结果
手写一个loader
- loader-utils
- this.query
- this.callback
- this.async
- resolveLoader // webpack配置对象,moudles属性
前置知识:
1. loader函数中的第一个参数表示源代码
2. 不能写成箭头函数,因为需要通过 this 获取很多api
3. 如何获取loader中的配置参数 options对象
- this.query // 指向的就是 options对象
// 如果 loader 中没有 options,而是以 query 字符串作为参数调用时,this.query 就是一个以 ? 开头的字符串
- 注意:
- this.query已经废弃,使用 loader-utils 中的 getOptions 来获取 options对象
4. loader-utils
- 通过loader-utils中的 getOptions 获取 loader的options配置对象
5. this.callback
- 参数
- 第一个参数:err // Error 或者 null
- 第二个参数:content // string或者buffer
- 第三个参数:sourceMap // 可选,必须是一个可以被这个模块解析的 source map
- 第四个参数:meta //可选
- // https://www.webpackjs.com/api/loaders/#this-callback
this.callback(
err: Error | null,
content: string | Buffer,
sourceMap?: SourceMap,
meta?: any
);
6. this.async // 处理loader中有异步操作
- this.async()方法返回 this.callback
7. resolveLoader // webpack配置项
- modules: ['node_modules', './src/loaders']
// 在寻找loader的时候,先去node_modules文件夹中共寻找,没找到再去'./src/loaders'文件夹中找
------------
代码:replace-loader.js // 如下
------------
const loaderUtils = require('loader-utils')
module.exports = function (source) {
console.log(this.query, 'this.query')
const options = loaderUtils.getOptions(this) // 获取laoder的options对象
console.log(options, 'loader-utils中的getOtions')
const callback = this.async(); // this.async()方法返回的是 this.callback
setTimeout(() => { // 注意这里的setTimeout的环境
const result = source.replace('guoqing', options.replacename) // 拿到options对对象中设置的replacename属性
callback(null, result)
}, 1000);
// return source.replace('guoqing', this.query.name)
// return source.replace('guoqing', options.name)
}
------------
代码:webpack.dev.js // 如下
------------
resolveLoader: {
modules: ['node_modules', './src/loaders'] // 先找node_module再找后面的文件夹
},
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: 'replace-loader.js', // 配置了resolveLoader的modules后,这里就直接写了
options: {
replacename: '2019 new new good'
}
}
]
}
]