「前端」看懂前端脚手架你需要这篇webpack

2,010 阅读10分钟
原文链接: github.com

本文来自尚妆前端团队南洋

发表于尚妆博客

分割webpack配置文件的多种方法

(一)

将你的配置信息写到多个分散的文件中去,然后在执行webpack的时候利用--config参数指定要加载的配置文件,配置文件利用moduleimports导出。你可以在webpack/react-starter 看到是使用这种发方法的。

// webpack 配置文件

|-- webpack-dev-server.config.js
|-- webpack-hot-dev-server.config.js
|-- webpack-production.config.js
|-- webpack.config.js

// npm 命令

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev-server": "webpack-dev-server --config webpack-dev-server.config.js --progress --colors --port 2992 --inline",
    "hot-dev-server": "webpack-dev-server --config webpack-hot-dev-server.config.js --hot --progress --colors --port 2992 --inline",
    "build": "webpack --config webpack-production.config.js --progress --profile --colors"
  },

(二)

调用第三方的webpack工具,使用其集成的api,方便进行webpack配置。HenrikJoreteg/hjs-webpack 这个repo就是这么做的。

var getConfig = require('hjs-webpack')


module.exports = getConfig({
  // entry point for the app
  in: 'src/app.js',

  // Name or full path of output directory
  // commonly named `www` or `public`. This
  // is where your fully static site should
  // end up for simple deployment.
  out: 'public',

  // This will destroy and re-create your
  // `out` folder before building so you always
  // get a fresh folder. Usually you want this
  // but since it's destructive we make it
  // false by default
  clearBeforeBuild: true
})

(三) Scalable webpack configurations

ones that can be reused and combined with other partial configurations

在单个配置文件中维护配置,但是区分好条件分支。调用不同的npm命令时候设置不同的环境变量,然后在分支中匹配,返回我们需要的配置文件。

这样做的好处可以在一个文件中管理不同npm操作的逻辑,并且可以共用相同的配置。webpack-merge这个模块可以起到合并配置的作用。


const parts = require('./webpack-config/parts');

switch(process.env.npm_lifecycle_event) {
  case 'build': 
    config = merge(common, 
      parts.clean(PATHS.build),
      parts.setupSourceMapForBuild(),
      parts.setupCSS(PATHS.app),
      parts.extractBundle({
        name: 'vendor',
        entries: ['react', 'vue', 'vuex']
      }),
      parts.setFreeVariable('process.env.NODE_ENV', 'production'),
      parts.minify()
      );
    break;
  default: 
    config = merge(common, 
      parts.setupSourceMapForDev(),
      parts.devServer(), 
      parts.setupCSS(PATHS.app));
}
// minify example
exports.minify = function () {
  return {
    plugins: [
      new webpack.optimize.UglifyJsPlugin({
        compress: {
          warnings: false,
          drop_console: true
        },
        comments: false,
        beautify: false
      })
    ]
  }
}

开发环境下的自动刷新

webpack-dev-server

webpack-dev-server在webpack的watch基础上开启服务器。

webpack-dev-server是运行在内存中的开发服务器,支持高级webpack特性hot module replacement。这对于react vue这种组件化开发是很方便的。

使用webpack-dev-server命令开启服务器,配合HMR及可以实现代码更改浏览器局部刷新的能力。

hot module replacement

Hot Module Replacement (HMR) exchanges, adds, or removes modules while an application is running without a page reload. 当应用在运行期间hmr机制能够修改、添加、或者移除相应的模块,而不使整个页面刷新。

hmr机制适用于单页应用。

要实现hmr机制,需要配合webpack-dev-server服务器,这个服务器本身就实现了监察watch文件改动的能力,再开启HMR选项,就添加了watch模块变化的能力。这是HMR机制能生效的基础。

从webpack编译器角度

每次修改一个模块的时候,webpack会生成两部分,一个是manifest.json,另一部分是关于这次模块更新编译完成的chunks。manifest.json中存着的是chunk更改前后的hash值。

从编译器webpack的角度来讲提供了hmr的原材料。供后续使用。

从模块的角度

模块发生变化时,webpack会生成之前讲过的两部分基础文件,但是何时将变化后的模块应用到app中去?这里就需要在应用代码中编写handler去接受到模块变化信息。但是不能在所有模块中编写handler吧?这里就用到了消息冒泡机制。

如图A.js、C.js没有相关hmr代码,B.js有相关hmr代码,如果c模块发生了变化,c模块没有hmr,那么就会冒泡到a、b模块。b模块捕捉到了消息,hmr运行时会相应的执行一些操作,而a.js捕捉不到信息,会冒泡到entry.js,而一旦有消息冒泡的入口块,这就代表本次hmr失败了,hmr会降级进行整个页面的reload。

从HMR运行时的角度

HMR运行时是一些相关的操作api,运行时支持两个方法: checkapply

check发起 HTTP 请求去获取更新的 manifest,以及一些更新过后的chunk。

环境变量的设置

var env = {
  'process.env.NODE_ENV': '"production"'
}
new webpack.DefinePlugin(env)

注意这里单引号间多了个双引号 why?

以及webpack.DefinePlugin插件的原理?

开发的时候会想写很多只在开发环境出现的代码,比如接口mock等,在build命令后这些代码不会存在。

这对框架或者插件、组件的开发是很有帮助的。vue,react等都会这么做。可以在这些框架的dev模式提供很多有用的提示信息。

打包文件分割

为何要进行打包文件分割?

对于一个单页应用项目来说,有分为业务代码和第三方代码,业务代码会频繁改动,而第三方代码一般来讲变动的次数较少,如果每次修改业务代码都需要用户将整个js文件都重新下载一遍,对于加载性能来讲是不可取的,所以一般而言我们会将代码分为业务代码和第三方代码分别进行打包,虽然多了一个请求的文件,增加了一些网络开销,但是相比于浏览器能将文件进行缓存而言,这些开销是微不足道的。

我们在entry中定义了app入口,相应的业务逻辑都封装在这个入口文件里,如果我们想要第三方代码独立出来,就要再增加一个入口,我们习惯使用vendor这个命名。

// app.js

require('vue');
require('vuex');
// webpack.config.js


entry: {
    app: 'app/app.js',
    vendor: ['vue', 'vuex'],
  },

vendor入口的传参是以一个数组的形式传递的,这是一种非常方便的注入多个依赖的方式,并且能把多个依赖一起打包到一个chunk中。而且不用手动的创建真实存在的入口文件。

这相当于:

// vendor.js

require('vue');
require('vuex');

// app.js

require('vue');
require('vuex');
// webpack.config.js


entry: {
    app: 'app/app.js',
    vendor: 'app/vendor.js',
  },

但是这样做只是声明了一个vendor入口而已,对于app这个入口来说,打包完成的文件还是会有vue和vuex依赖,而新增的入口vendor打包完成的文件也有了vue和vuex两个依赖。模块依赖关系如下图所示。

这里的A可以代表vue依赖,最后生成的打包文件是两个平行关系的文件,且都包含vue的依赖。

此时需要引入CommonsChunkPlugin插件

This is a pretty complex plugin. It fundamentally allows us to extract all the common modules from different bundles and add them to the common bundle. If a common bundle does not exist, then it creates a new one.

这是个相当复杂的插件,他的基础功能是允许我们从不同的打包文件中抽离出相同的模块,然后将这些模块加到公共打包文件中。如果公共打包文件不存在,则新增一个。同时这个插件也会将运行时(runtime)转移到公共chunk打包文件中。

plugins: [
  new webpack.optimize.CommonsChunkPlugin({
    names: ['vendor', 'manifest']
  })
]

这里的name可以选择已经存在的块,这里就选择了vendor块,因为我们本来就是将vendor块当做管理第三方代码的入口的。

而names传入一个数组,数组里包含两个trunk name,表示CommonsChunkPlugin插件会执行两次这个方法,第一次将公共的第三方代码抽离移到vendor的块中,这个过程之前也讲过会将运行时runtime也转移到vendor块中,第二次执行则是将运行时runtime抽离出来转移到manifest块中。这步操作解决了缓存问题。

这样处理,最后会生成3个打包文件chunk,app.js是业务代码,vendor则是公共的第三方代码,manifest.js则是运行时。

chunk type 块的类型大揭秘

webpack1.0官网介绍中的chunk类型读起来及其拗口chunk type, 所以我这里解读一下。

chunk是webpack中最基本的概念之一,且chunk常常会和entry弄混淆。在「打包文件分割部分」我们定义了两个入口entry point -- app和vendor,而通过一些配置,webpack会生成最后的一些打包文件,在这个例子中最后生成的文件有app.js 、 vendor.js 、 manifest.js。这些文件便被称为块chunk

entry & chunk 可以简单的理解为一个入口、一个出口

在官方1.0文档中webpack的chunk类型分为三种:

  1. entry chunk 入口块
  2. normal chunk 普通块
  3. initial chunk 初始块

entry chunk 入口块

entry chunk 入口块不能由字面意思理解为由入口文件编译得到的文件,由官网介绍

An entry chunk contains the runtime plus a bunch of modules

可以理解为包含runtime运行时的块可以称为entry chunk,一旦原本存在运行时(runtime)的entry chunk失去了运行时,这个块就会转而变成initial chunk

normal chunk 普通块

A normal chunk contains no runtime. It only contains a bunch of modules.

普通块不包含运行时runtime,只包含一系列模块。但是在应用运行时,普通块可以动态的进行加载。通常会以jsonp的包装方式进行加载。而code splitting主要使用的就是普通块。

initial chunk 初始块

An initial chunk is a normal chunk.

官方对initial chunk的定义非常简单,初始块就是普通块,跟普通块相同的是同样不包含运行时runtime,不同的是初始块是计算在初始加载过程时间内的。在介绍入口块entry chunk的时候也介绍过,一旦入口块失去了运行时,就会变成初始块。这个转变经常由CommonsChunkPlugin插件实现。

例子解释

还是拿「打包文件分割」的代码做例子,

// app.js

require('vue');
require('vuex');
// webpack.config.js


entry: {
    app: 'app/app.js',
    vendor: ['vue', 'vuex'],
  },

没有使用CommonsChunkPlugin插件之前,两个entry分别被打包成两个chunk,而这两个chunk每个都包含了运行时,此时被称为entry chunk入口块。

而一旦使用了CommonsChunkPlugin插件,运行时runtime最终被转移到了manifest.js文件,此时最终打包生成的三个chunkapp.js 、 vendor.js 、 manifest.js,app.js、vendor.js失去了runtime就由入口块变成初始块。

code splitting

前文有讲到将依赖分割开来有助于浏览器缓存,提高用户加载速度,但是当业务复杂度增加,代码量大始终是一个问题。这时候就需要normal chunk普通块的动态加载能力了。

It allows you to split your code into various bundles which you can then load on demand — like when a user navigates to a matching route, or on an event from the user. code splitting 允许我们将代码分割到可以按需加载的不同的打包文件中,当用户导航到对应的路由上时,或者是用户触发一个事件时,异步加载相应的代码。

我们需要在业务逻辑中手动添加一些分割点,标明此处事件逻辑之后进行代码块的异步加载。


// test
window.addEventListener('click', function () {
  require.ensure(['vue', 'vuex'], function (require) {

  })  
})

这段代码表明当用户点击时,异步请求一个js文件,这个文件中包含该有vue vuex的依赖。

打包后会根据手动分割点的信息生成一个打包文件,就是图中第一行0开头的文件。这个文件也就是异步加载的文件。

下面是之前的一个vue项目,采用code splitting将几个路由抽离出来异步加载之后,文件由212kb减少到了137kb,同样样式文件也由58kb减少到了7kb。对于首屏渲染来说,性能是会增加不少的。

有需要交流的可以联系我微博达达的暹罗猫

参考: