带你了解webpack

1. 前端工程化项目打包历史

前端工程化之前的时代略过

1. 半自动执行脚本来压缩合并文件

自从xmlhttprequest被挖掘出来,网页能够和服务端通讯,js能做的事越来越多,文件体积越来越大,互相引用原来越多。
然而网速只有几兆的带宽。于是想到,要把js文件狠狠的压缩,能合并的就给合并起来

主要有的js压缩工具:

JSMin 使用简单、灵活,对不同语言、环境支持好。一般配合不同的环境、语言用命令行执行压缩
YUI Compressor 雅虎推出。有JAVA版本和.NET版本,需两者环境配合
UglifyJS 基于nodejs,压缩策略较为安全,所以不会对源代码进行大幅度的改造。
Closure Compiler 谷歌出品

在我理解,压缩主要做了局部变量命名简化、空格/换行/注释消除、自动优化可简化的语法等工作。

使用压缩工具用es6语法写的js在压缩测试比较中:

  1. UglifyJS压缩率高,可以自动格式化、优化代码。所以普及率高。现在也是主流的工具
  2. YUI compressor 好处就是压缩策略安全,相比UglifyJS,自动优化代码的程度较保守
  3. Closure Compiler 的Advanced模式直接破坏代码的结构,bug多

压缩有了,但是当a文件和b文件都引用了c文件的方法时,如果把c文件分别和a、b合并,这样就只有两个文件了。这就是最开始的合并方式。
一般是通过在windows上用bat脚本或者mac/linux上的shell脚本来决定合并哪些文件、用什么工具压缩、怎么压缩

  • 进步:
  1. 解决当时网速普遍较慢的情况下网页加载资源较慢的问题
  2. 代码混淆了不容易被盗用
  • 存在的问题:
  1. 工程化的项目里相互依赖关系变得非常复杂
  2. 合并的文件里可能会有很多无用代码

2. 自动构建的尝试阶段

通过脚本构建的项目里的文件相互依赖复杂,命名什么的完全有可能一不小心就冲突了,而且依赖可能是一层一层依赖下去的,维护起来要命呀。
所以先解决js相互依赖的问题

1. js依赖关系的规范探索
CommonJS规范解决了js模块化依赖的问题
CommonJs规范的简单介绍(nodejs版)
  1. 一个文件就是一个模块
  2. 每个模块内部,都有一个module对象,代表当前模块,有规定好的一些默认属性
  3. module.exports属性:初始值是一个空对象{},这个变量把定义的变量、方法暴露出去
  4. exports变量:Node为每个模块提供一个exports变量,指向module.exports。至于两者的区别,不用去了解,平常就用module.exports
  5. require引用:把module.exports定义的变量/方法来过来用
  6. 注意:它是同步的

既然CommonJS提供了模块化的思路,也已经在服务端(nodejs)里大展手脚。那么浏览器里可不可行?
浏览器不兼容CommonJS的原因首先是,缺少了4个NodeJs环境变量:

  • module
  • exports
  • require
  • global

那么只需要提供了这些个环境变量就行了吧。Browerify就是做这的

Browserify 是目前最常用的 CommonJS 格式转换的工具

Browserify的核心思路是讲module暴露出的模块放入一个数组,require时根据模块id找到相应的module执行,总之就是给上面缺少的变量写成可执行的es5的策略

那么是不是这样就能在浏览器上愉快使用CommonJS?

CommoJS是同步require的方式获取js模块,在浏览器上会阻断主线程。页面会因加载js可能卡住

这肯定是不能容忍的

于是AMD(异步模块定义)诞生
AMD也采用require()语句加载模块,但是要传两个参数require([module], callback)。是的,回调思路

AMD规范的简单介绍(RequireJS)
  1. 解决两个问题:

(1)实现js文件的异步加载,避免网页失去响应
(2)管理模块之间的依赖性,便于代码的编写和维护

  1. 模块必须采用特定的define()函数来定义
  2. 非AMD的第三方库加载之前要用require.config()定义固有特征

CMD规范 和AMD大同小异,具体实现是seajs。没用过,应该都差不多吧,啊哈哈

2. html/css模块化的规范

less,sass,stylus 的 css 预处理器简化css语法
ejs,jade 等html的模板语法

这些真的是前端狗的福音,不多说,css-next来了,继续啃咯。

这样html/css/js 就都有了适合自动构建的扩展结构。但是这时候写一个构建这些依赖的命令太长太复杂,所以打包工具开始流行:

3.Grunt/Gulp 流处理构建工具让前端构建更容易

grunt 写法简单,插件还贼多
gulp 效率更高,可扩展性更强

nodejs配合这俩大佬做web项目的自动化构建用着都挺爽的

var gulp = require('gulp')
var nodemon = require('gulp-nodemon')
var browserSync = require('browser-sync').create()

gulp.task('nodemon', function(cb) {
    var started = false
    return nodemon({
        script: 'mswadmin.js'
        , ext: 'js'
        , env: { 'NODE_ENV': 'default' }  
    }).on('start', function() {
      if (!started) {
          cb();
        started = true;
      }
    })
});
gulp.task('serve', function(){
    browserSync.init({
    proxy: 'http://10.3.10.27:18282',
    browser: 'chrome',
    port: 18282
  })
    gulp.watch('static/**/*.+(scss|jade|ls)', ['inject'])
    .on('change', browserSync.reload);
})
gulp.task('default', ['nodemon','serve']);

上面是一个用nodemon监控本地服务+watch代码热更新的配置。可以看出,以流任务的方式一个个执行。用起来也简单

2. SPA(Single-page application)来了

js 对应的 AMD 模块,然后该 AMD 模块渲染对应的 html 到容器内

这样网页不再是传统的文档一类的页面了。而是更像一个完整的程序。一个主入口,js完成的前端路由,AMD模块完成页面内重新渲染。
虽然是做出来这个SPA了,但是小问题多:

  1. 很多成熟的第三方库不支持AMD规范,引用起来贼麻烦
  2. RequireJS在加载html依赖时,html里的img路径要使用绝对路径
  3. 只能一次性加载所有css文件
  4. 分模块打包js文件时的通用依赖项很难配置
  5. 最重要的,AMD/CMD CommonJS规范太多造成很多第三方库对规范支出不够。。。而且ES6规范都要普及了,你不用???

3. webpack来解救你

首先,webpack是静态模块打包器(bundler),grunt/gulp是流任务执行器。
区分两者可以用grunt-webpack形象说明:你可以将 webpack 或 webpack-dev-server 作为一项任务(task)执行

webpack为啥好用:

  1. webpack 能够为ES6的 import/export 提供开箱即用般的支持
  2. 还支持CommonJS CMD/AMD模块规范,做到随时可用

这两点是我觉得最突出的地方,详细对比请参考对比

浏览器环境下,用了ES6规范的话,你应该不想用其他的了

webpack的工作步骤如下:

  1. 从入口文件开始递归地建立一个依赖关系图。
  2. 把所有文件都转化成模块函数。
  3. 根据依赖关系,按照配置文件把模块函数分组打包成若干个bundle。
  4. 通过script标签把打包的bundle注入到html中,通过manifest文件来管理bundle文件的运行和加载。

打包的规则为:一个入口文件对应一个bundle。该bundle包括入口文件模块和其依赖的模块。按需加载的模块或需单独加载的模块则分开打包成其他的bundle。

除了这些bundle外,还有一个特别重要的bundle,就是manifest.bundle.js文件,即webpackBootstrap。这个manifest文件是最先加载的,负责解析webpack打包的其他bundle文件,使其按要求进行加载和执行。
无论你选择哪种模块语法,那些 import 或 require 语句现在都已经转换为 webpack_require 方法,此方法指向模块标识符(module identifier)。通过使用 manifest 中的数据,runtime 将能够查询模块标识符,检索出背后对应的模块。

webpack 怎么入门

虽然网上有很多 十分钟入门webpack 的教程。但还是推荐去撸一遍webpack官方指南

个人觉得指南里你要注意的细节:

  1. webpack 不会更改代码中除 import 和 export 语句以外的部分。如果你在使用其它 ES2015 特性,请确保你在 webpack 的 loader 系统中使用了一个像是 Babel 或 Bublé 的转译器
  2. npm脚本运行时默认可以使用npx命令
  3. source map要合理使用
  4. 留意webpack-dev-middleware,配合express做服务端渲染要用到哦
  5. HMR(模块热替代)一般用你选用的框架自带的loader(vue-loader)
  6. 用UglifyJsPlugin插件自动移除 JavaScript 上下文中的未引用代码(dead-code)。webpack4里使用 mode=production 替代。要结合SideEffects使用,webpack4又提供了SideEffects插件使用的方式
  7. process.env.NODE_ENV === 'production' ? '[name].[hash].bundle.js' : '[name].bundle.js' 这样的条件语句在配置文件里无法使用,用if/else
  8. splitChunks优化,webpack4已经移除了CommonsChunkPlugin。下文会详细解释
  9. dynamic imports(动态导入)优化,chunkFilename决定非入口 chunk 的名称,vue里的运用实例就是路由懒加载(vue-lazyload),生成了新的bundle

4. webpack的优化点的补充说明

  1. 动态导入在vue里的时间注意点:

webpack 可以使用dynamic imports的方式引用模块,我们使用 async/ await 和 dynamic import 来实现。每一个dynamic import都将作为一个单独的chunk打包。在vue中的一个例子就是路由懒加载+babel-plugin-dynamic-import-node的构建方案。使用babel-plugin-dynamic-import-node是因为开发环境下触发热更新很慢,这个插件讲import异步全部改成require同步

  1. 打包生成的文件模块标识符的问题

一般来说我们在dist生成了一下三种bundle

main bundle 会随着自身的新增内容的修改,而发生变化。
vendor bundle 会随着自身的 module.id 的修改,而发生变化。
manifest bundle 会因为当前包含一个新模块的引用,而发生变化。

然而我们并不希望vendor每次构建都生成新的hash,毕竟我们希望用到缓存的。解决方法官方有两个插件NamedModulesPlugin和HashedModuleIdsPlugin
vue里使用的是HashedModuleIdsPlugin

相信很多人从webpack3升级到4会碰到问题,接下来
### 5. 升级到webpack4你该搞明白

1. 零配置的概念把配置门槛降低了

主要使用了模式的概念。

development 模式下,默认开启了NamedChunksPlugin 和NamedModulesPlugin方便调试,提供了更完整的错误信息,更快的重新编译的速度
production 模式下,由于提供了splitChunks和minimizer,所以基本零配置,代码就会自动分割、压缩、优化,同时 webpack 也会自动帮你 Scope hoisting(作用域提升) 和 Tree-shaking

相当于把一些基本的配置当成默认配置。只需要在命令行运行时带上mode参数就搞定
#### 2. 一些插件的废除和替换

废弃了顶替者(用optimization属性)变化
uglifyjs-webpack-pluginminimizer压缩优化
CommonsChunkPluginsplitChunks代码分割,下面详解

还有一些新的插件:Tree Shaking,SideEffects。我还不知道怎么用--

3. 要注意的新的优化点

  1. extract-text-webpack-plugin -> mini-css-extract-plugin

它与extract-text-webpack-plugin最大的区别是:它在code spliting的时候会将原先内联写在每一个 js chunk bundle的 css,单独拆成了一个个 css 文件。js变得更干净了,css是根据optimization.splitChunks的配置自动拆分css文件为单独的模块的规则拆分的,不用担心过多的httlp资源请求问题

  1. 所有的[chunkhash] ->[contenthash]

这是为了解决当css与js文件有依赖时,两者有相同的chunkhash。这样js修改了,css没改的情况下chunkhash页被修改了,没法缓存了呀
contenthash 你可以简单理解为是 moduleId + content 所生成的 hash
相关issue

  1. 代码的压缩优化改成了optimization.minimizer

在optimization.minimizer里推荐使用optimize-css-assets-webpack-plugin直接配置。但是vue-cli3里的配置自己配的。嗯...反正也不想看那些配置,就这样吧~~~

4. 第三方库和业务代码分开打包策略

上面多处提到了这个optimization.splitChunks

Webpack 4 最大的改进便是Code Splitting chunk。webpack3是通过CommonsChunkPlugin拆分的。然后现在直接被废弃了,我能怎么办?,跟着学呗。

开启Code Splitting很简单,使用production的mode就行,会自动开启。并有一个设置好了的一个很合理的配置

如果同时满足下列条件,chunk 就会被拆分:

  • 新的 chunk 能被复用,或者模块是来自 node_modules 目录
  • 新的 chunk 大于 30Kb(min+gz 压缩前)
  • 按需加载 chunk 的并发请求数量小于等于 5 个
  • 页面初始加载时的并发请求数量小于等于 3 个

默认配置已经很合理了,然而当出现如下情况:
已vue-cli创建的项目为例。项目用到了第三方的UI组件库,在main.js入口处依赖了第三方库。
因为在入口引入了,所以第三方库会被打包进app.js。这样,只要我修改了app.js里的其他代码,打出来的包的hash就变了。浏览器又得再次缓存app.js。第三库相当于又被缓存了一次,这显然不是我们想要的。

看一下花裤衩的配置

splitChunks: {
  chunks: "all",
  cacheGroups: {
    libs: {
      name: "chunk-libs",
      test: /[\\/]node_modules[\\/]/,
      priority: 10,
      chunks: "initial" // 只打包初始时依赖的第三方
    },
    elementUI: {
      name: "chunk-elementUI", // 单独将 elementUI 拆包
      priority: 20, // 权重要大于 libs 和 app 不然会被打包进 libs 或者 app
      test: /[\\/]node_modules[\\/]element-ui[\\/]/
    },
    commons: {
      name: "chunk-commons",
      test: resolve("src/components"), // 可自定义拓展你的规则
      minChunks: 2, // 最小共用次数
      priority: 5,
      reuseExistingChunk: true
    }
  }
};

主要思路就是

  1. 把初始化时依赖的第三方打包成基础类库,这一类改动小,又被全局需要
  2. 把类似elementUI这一类的比较大、改动较小的抽出来
  3. 全局公用的router、函数、svg图标、layout布局组件等这些不管,直接扔app.js
  4. 业务里会经常使用但没在main.js引入的的components被打包成一个common
  5. 业务里经常使用但是体积相当较小,就直接在main.js引入,打包进app.js
  6. 其他的低频使用的组件会自动按默认splitChunks的设置来拆分

提醒: 代码的拆分一定要结合项目的实际情况,比如你就用到element里的一两个组件,完全可以按需加载在main.js,然后直接打包进app.js。所以没有最合理的拆分规则,只有最适合你的。

5. Prefetching/Preloading modules

支持了Prefetching/Preloading浏览器资源加载优化
核心思想是减少JS下载时间
学不动了学不动了,先缓缓

6. 最后推荐http://webpack.wuhaolin.cn/

相关推荐