Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

webpack总汇 #33

Open
lznbuild opened this issue Jul 7, 2020 · 0 comments
Open

webpack总汇 #33

lznbuild opened this issue Jul 7, 2020 · 0 comments

Comments

@lznbuild
Copy link
Owner

lznbuild commented Jul 7, 2020

总结

此篇没有实现细节,原理,具体做法,而是从宏观角度的总结。

webpack自己配?

create-react-app 作为通用脚手架,需要考虑普适性,打包项目后,会发现业务代码和依赖什么都没加进去,脚手架默认的包就接近500k了。自己写webpack更符和自己项目的情况,打包也小。

webpack中的loader

webpack只能对js进行处理,但项目中还有很多其他类型的文件,比如css,图片,字体,jsx等,这就要用loader进行转换,webpack才能进行下一步操作。

常用loader

  • url-loader

    将limit匹配的图片转换为base64格式,其内部使用了file-loader,url-loader和file-loader类似,只是多了一步base64的转换, file-loader 默认在内部生成图片,也可以处理字体文件,并打包

  • image-webpack-loader

    压缩图片

  • postcss-loader
    处理css

    autoprefixer自动加浏览器前缀,可以做css的模块化,px到rem的转换,等等还有其他功能插件。

  • eslint-loader

    做代码格式的优化

  • css-loader, style-loader,mini-css-extract-plugin,less-loader

    处理css中的依赖,@import,url引入的文件,style-loader将css-loader解析的结果转变成js代码,运行时插入style标签,默认一个模块的js和css耦合在一起,mini-css-extract-plugin,将css代码做抽离,less-loader处理less转换,options.modifyVars定义样式的全局变量

  • expose-loader

    暴露全局变量,以jquery为例子,$表示jquery

    {
      test: require.resolve('jquery'),
      use: 'expose-loader?$'
    }

    如果不暴露在全局的话,每个文件都要这样引入jquery,太麻烦了

    import * as $ from 'jquery'
  • cache-loader

    文件较之前的没有发生变化则会直接使用缓存,不再重新loader转换。
    性能开销极大的loader选择cache-loader进行处理的话,会显著的提高打包效率。这里有坑,如果遇到了mini-css-extract-plugin.loader的话,要将cache-loader写在mini-css-extract-plugin.loader的后面。原因

loader的加载方式(按优先级大到小)

前置loader(pre)==> 普通loader(normal) ==> 内联loader(inline) ==> 后置loader(post)

前置,后置

{
  test: /\.less$/,
  use: 'less-loader',
  enforce: 'pre' // pre post  把enforce字段去掉就是普通Loader
}

行内loader

require('!inline-loader!./a.js') // import引入

性能相关

SpeedMeasurePlugin(速度) speed-measure-webpack-plugin
将每一个plugin,每一个loader的打包时间以及总时长打包统计

plugin

可以把plugin理解为一个个封装好的功能函数

常用plugin

  • webpack.DefinePlugin

    创建一些在编译时可以配置的全局常量,在代码中可以访问

  • CopyWebpackPlugin

    复制没经过webpack处理的文件

  • webpack.IgnorePlugin

    忽略指定的模块,不把指定的模块打包,例如moment.js里有大量的i18n代码,没必要全部打包

    plugins: [
        new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
      ]
  • webpack.DllPluginc

    抽离第三方模块

  • html-webpack-plugin

    自动创建HTML文件,引用构建出的JS文件

  • webpack-bundle-analyzer(体积)

    用于分析 webpack 构建打包的内容,查看各个模块的依赖关系和各个模块的代码体积,便于开发者做性能优化。使用这个可以配合 IgnorePlugin 来过滤掉部分大而无用的第三方模块,

  • BundleAnalyzerPlugin

    将各个包的内容,信息以图形化界面展现出来

  • clean-webpack-plugin

    清除上次打包的文件

  • uglifyjs-webpack-plugin

    压缩Js,不支持ES6语法,可以用terser-webpack-plugin代替,terser-webpack-plugin在optimization中使用
    
  • happypack

    分配任务到子进程,减少构建时间

  • webpack-parallel-uglify-plugin

    优化代码的压缩时间

  • HotModuleReplacementPlugin

    开发环境,局部更改刷新,HMR

  • speed-measure-webpack-plugin(速度)

    将每一个plugin,每一个loader的打包时间以及总时长打包统计

tree shaking

tree shaking(去除没有执行到的代码,css的,js的),注意,必须开启代码压缩,不然没用。

有副作用的代码即使没有使用到,还是会被保留。tree shaking 中的 sideEffects属性可以解决这个问题。

sideEffects:false ,在package.json中声明该包模块是否包含 sideEffects(副作用),从而可以为 tree-shaking 提供更大的优化空间。被标记的模块,不管是否真的有副作用,只要它没有被引用到,就被移出。

编译阶段做ast语法树解析,编译时加载则不能,因为他是值的拷贝,所以commonjs模块化引入的模块不能做tree shaking

生产环境代码特点

  • 更小的bundle,压缩

  • 轻量的source map(devtool: cheap-module-source-map)

  • 优化资源,包的分离

  • tree shaking去掉多余代码

注意,不会把项目源代码中的注释也打包进去,可以放心在代码中写注释。

开发环境代码特点

  • 热更新

  • source map(devtool:cheap-module-eval-source-map )

  • dev server

  • eslint,stylelint

关于ES6

ES6新特性主要分为两类

  • 箭头函数,class,这种需要语法上的转换;
  • Promise,Map,Set,Array.prototype.includes这种,需要加对应代码的polyfill,不加的话,自己实现Promise等在入口文件引入也是可以的。。。

babel-loader

需要装的依赖包

yarn add babel-loader  -D // 加到开发环境    

babel-loader可以做第一步,语法上的转换。比如想用ES6中的class

{
  test: /\.js$/,
  use: {
    loader: 'babel-loader',
    // 下面都是babel-loader的参数
    options: {
      // 预设环境
      presets: [],
      // 单独的插件
      plugins: [
        '@babel/plugin-proposal-class-properties' // 只做class的转换,如果还有其他的转换需求,去官网找对应的插件在这里引入
      ]
    }
  }
}

ES6有太多需要转换的东西了,一个一个插件的引入太麻烦了,所以就有了预设

yarn add  @babel/preset-env -D

@babel/preset-env是一个预设,把很多插件合到一起,这样只需要引入@babel/preset-env就可以了。注意,@babel/preset-stage[num] , 提案预设只有stage2阶段以上的特性才会在未来被使用,以下的是可能部分被废弃,babel7开始不推荐使用了。

  test: /\.js$/,
  use: {
    loader: 'babel-loader',
    // 下面都是babel-loader的参数
    options: {
      // 预设环境
      presets: [
        '@babel/preset-env'
      ],
      // 单独的插件
      plugins: [
        // 如果还需要其他插件,而@babel/preset-env中没有的,再额外引入就好了
      ]
    }
  }
}

一般都会把babel-loader的参数单独写。

// .babelrc文件
{

// 执行顺序:从后往前 
  "presets": [
    // 一个presets,表示很多plugins的集合 
    "@babel/preset-env" // 解析es6
    // 核心目的是通过配置得知目标环境的特点,然后只做必要的转换。例如目标浏览器支持 es2015,那么 es2015 这个 preset 其实是不需要的,于是代码就可以小一点(一般转化后的代码总是更长),构建时间也可以缩短一些。

// 如果不写任何配置项,env 等价于 latest,也等价于 es2015 + es2016 + es2017 三个相加(不包含 stage-x 中的插件)
    "@babel/preset-react" // 解析jsx
  ],
  // plugins中的插件在presets之前运行
  // 执行顺序:从前往后
  "plugins": [
    // 一个plugins对应一个功能
    "@babel/proposal-class-properties"
  ]
}

polyfill

引入polyfill是解决了ES6的第二种情况,需要实现的Promise,Map,Set等

这里又有很多种方式,下面一一介绍。

第一种情况,@babel/polyfill

yarn add @babel/polyfill -D
// 入口文件引入
import '@babel/polyfill';

或者

module.exports = {
//  webpack入口之前引入
  entry: ["@babel/polyfill", "xxx"],
};

这种方式是把@babel/polyfill全部引入,项目中可能用不到全部ES6的新特性,这个包大概400k,也造成了浪费,而且这个包的一些方法是直接在原型上定义的,污染全局环境,可能会有冲突,不适合写工具包。比如,我写了一个工具包,直接在原型上声明了一些方法,发到npm上,别人用了,别人的项目中也有在原型上声明一些方法的需求,是不是有可能和我写的方法造成冲突,而且一旦冲突,很难发现这个问题,试问,又有谁会看全部依赖包的源码呢。。。当然,如果没有在原型上声明一些方法的需求,对打包后的代码体积也没有要求的话,这种方式是最省事的。。

第二种情况,@babel/plugin-transform-runtime

yarn add @babel/plugin-transform-runtime  -D
yarn add @babel/runtime // 生产环境
 plugins: [
    '@babel/plugin-transform-runtime'
  ]

这个包实现了Promise,Map等构造函数,在编译中复用辅助函数,createClass这种(具体请看class编译成ES5的样子),会在局部进行polyfill, 多次使用只会打包一次,无重复引用,最终打包的体积也好了一些,但是,这个包没有实现原型上的方法,例如:数组的includes方法等,但是项目中还要用到原型的方法怎么办。

关于这个问题,和babel-polyfill直接引入体积太大的问题,统一说明。

这2个问题都可以通过单独使用 core-js 的某个文件来解决。
如果使用了babel-plugin-transform-runtime或者 babel-polyfill,你就间接的引入了 core-js。

比如,我只在项目中用到了数组的includes方法,以上两种方式都不好解决(第一个太大,第二个没有),就可以直接引入core-js的对应文件

import "core-js/modules/es.array.includes"

太麻烦了,有没有更人性化的引入polyfill的方式?

第三种

不再需要手动的在代码中引入@babel/polyfill 了,同时还能做到按需加载,代码中用到什么,打包的时候webpack就引入什么,这个属性比较新,对webpack版本有要求。而且不会注入类似fetch这种浏览器api性质的polyfill的,它只会处理es标准的polyfill

presets: [
  ["@babel/preset-env", { useBuiltIns: 'usage',corejs: 3 }]
]

useBuiltIns这个属性有3个值
false: 不对@babel/polyfill做任何处理,还是需要手动引入@babel/polyfill
entry:根据target或者browserslist中浏览器版本的支持,将polyfills拆分引入,仅引入有浏览器不支持的polyfill
usage: 仅仅加载代码中用到的polyfills,不要再次引入@babel/polyfill了,但是仍然需要安装

还是有问题。

在不支持ES6的浏览器中,引入这些polyfill很合理,但在支持ES6的浏览器中,我干嘛要引入这些额外代码,这不是又增加了打包的体积了?有没有其他更合理的方式?

有!

其实针对这个问题,@babel/preset-env可以根据我们对browserslist(package.json中)的配置,在转码时自动根据我们对转码后代码的目标运行环境的最低版本要求,采用更加“聪明”的转码,如果我们设置的最低版本的环境,已经原生实现了要转码的ES特性,则会直接采用ES标准写法;如果最低版本环境,还不支持要转码的特性,则会自动注入对应的polyfill

  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },

第四种方式,polyfill.io 动态加载polyfill

polyfill.io官方维护一台服务器,根据开发者的传参和请求头中的User-Agent判断当前浏览器是否需要polyfill,下发不同的polyfill

比如,我要用一个数组的includes

<script src="https://polyfill.io/v3/polyfill.min.js?features=Promise%2CArray.prototype.includes"></script>

我在chrome里查看引入的页面,没有返回任何资源,在IE里查看页面,有返回了!真香!

一些大公司都会维护自己的动态polyfill.io, 没这条件的直接用第三方的服务。

缺点: 有些浏览器厂商(小米,华为等手机浏览器)会对User-Agent做一个魔改,这样的话就返回不了最合适的polyfill,针对这一情况,只能加载所有,做一个降级处理。

entry可以写一个动态入口,更灵活

const path = require('path');
const fs = require('fs');

// src/pages 目录为页面入口的根目录
const pagesRoot = path.resolve(__dirname, './src/pages');
// fs 读取 pages 下的所有文件夹来作为入口,使用 entries 对象记录下来
const entries = fs.readdirSync(pagesRoot).reduce((entries, page) => {
  // 文件夹名称作为入口名称,值为对应的路径,可以省略 `index.js`,webpack 默认会寻找目录下的 index.js 文件
  entries[page] = path.resolve(pagesRoot, page);
  return entries;
}, {});

module.exports = {
  // 将 entries 对象作为入口配置
  entry: entries,

  // ...
};

为什么import React from 'react'就会去找node_modules里的包?

// webpack默认指定 
resolve: {
  modules: ['node_modules']
}

文件指纹 (hash值)做持久化缓存

做版本管理

  • hash

    和整个项目的构建相关 ,只要项目文件有修改,整个项目构建的hash值就会更改。对于没有改变的模块而言,这样做显然不恰当,因为缓存失效了

  • chunkhash

    和webpack 打包的chunk有关,不同的entry会生成不同的chunkhash值。chunkhash根据不同的入口Entry,进行依赖文件解析、构建对应的chunk,生成对应的哈希值。在生产环境构建时,会把公共库和程序入口文件区分开,单独打包构建,接着我们采用chunkhash的方式生成哈希值,那么只要我们不改动公共库的代码,就可以保证其哈希值不会受影响。

  • contenthash

    根据文件内容来定义hash,文件内容不变,则contenthash不变 (某个页面既有js资源,又有css资源。如果css资源也使用Chunkhash。如果修改了js。由于css资源使用了Chunkhash,就会导致css内容没有变化,发布上线的文件却发生了变化。因此,通常对css资源使用Contenthash。这个时候可以使用mini-css-extract-plugin里的contenthash值,保证即使css文件所处的模块里就算其他文件内容改变,只要css文件内容不变,那么不会重复构建)

js一般指定chunkhash或contenthash 在出口指定hash的话,最终打包的js有很多个,不一定当前更改了所有的js文件,但是每次重新打包都统一更改了,所以hash不合适。

css一般指定contenthash。因为我们css也是模块引入到js里面的,所以js和css的hash是一样的 如:test2.js和test.css。这就导致 ,如果我css没更改,只改了js,css的hash也会变,或者只改了js,css没改,js的hash值也会变;这时候就需要contenthash了。

有时候,全部代码内容不改变的情况下,多次打包hash也会发生变化,原因在于我们使用了extract抽离代码。extract-text-plugin 提供了contenthash

压缩文件

图片压缩: image-webpack-loader

css压缩: optimize-css-assets-webpack-plugin

js压缩: 生产环境自动压缩

html压缩: htmlwebpackplugin指定参数

es module 与 commonjs 为何可以混用

因为 babel 会把 es module 转换成 commonjs 规范的代码。详细

require 引入的模块 webpack 能做 Tree Shaking 吗

不能,Tree Shaking 需要静态分析(编译时分析),只有 ES6 的模块才支持。

import 导入不能再对此变量修改

browserslist

作用于babel-preset-env, autoprefixer, stylelint

mode为development,production分别会做哪些默认处理

development

process.env.NODE_ENV:development 环境变量指定

production

process.env.NODE_ENV:production 环境变量指定

ModuleConcatenationPlugin 开启scope hoisting

NoEmitOnErrorsPlugin 不提示报错信息

terser-webpack-plugin 代码压缩

Code Splitting 开启

happylindz/blog#7

happylindz/blog#6

https://www.zoo.team/article/babel-2
https://segmentfault.com/a/1190000018721165

https://juejin.im/post/5de87444518825124c50cd36#heading-23

https://juejin.im/post/5cfe4b13f265da1bb13f26a8

https://zhuanlan.zhihu.com/p/43249121
https://zhuanlan.zhihu.com/p/44174870

soda-x/blog#9

https://juejin.im/post/5b304f1f51882574c72f19b0

https://juejin.im/post/5b304f1f51882574c72f19b0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant