lznbuild / my-blog

自己的博客
9 stars 1 forks source link

webpack总汇 #33

Open lznbuild opened 4 years ago

lznbuild commented 4 years ago

总结

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

webpack自己配?

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

webpack中的loader

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

常用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

tree shaking

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

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

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

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

生产环境代码特点

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

开发环境代码特点

关于ES6

ES6新特性主要分为两类

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也会发生变化,原因在于我们使用了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 开启

https://github.com/happylindz/blog/issues/7

https://github.com/happylindz/blog/issues/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

https://github.com/pigcan/blog/issues/9

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

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