【webpack】webpack优化1

导航
前置知识
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'
          }
        }
      ]
    }
  ]