blog icon indicating copy to clipboard operation
blog copied to clipboard

【搬迁】webpack进阶学习

Open escawn opened this issue 7 years ago • 0 comments

  • 本文首发于https://escawn.github.io/(已废弃)
  • 发表时间2017/07/20

摘要

webpack是当下流行的前端工程打包工具,本文较深入地分析了其运作原理,以及一些常用功能的实现思路。


简介

Webpack 是当下最热门的前端资源模块化管理和打包工具。它可以将许多松散的模块按照依赖和规则打包成符合生产环境部署的前端资源。还可以将按需加载的模块进行代码分隔,等到实际需要的时候再异步加载。通过 loader 的转换,任何形式的资源都可以视作模块,比如 CommonJs 模块、 AMD 模块、 ES6 模块、CSS、图片、 JSON、Coffeescript、 LESS 等。

概念介绍

Entry

介绍

webpack.config.js中配置,含义为指定注入webpack的文件,由这个文件引出的依赖集合导向一个自命名chunk,默认名为main。webpack将从这个文件开始,一步步分析模块依赖,并进行模块的处理。

语法

entry是一个对象,对象里键值对的形式为

entry: {
	chunkName: entry file's path
}
// 如
entry: {
    main: './path/to/my/entry/file.js'
  }
// 多文件入口
entry: {
    app: './src/app.js',
    vendors: './src/vendors.js'
}

多文件入口用于chunk形成的依赖图是独立的情况下,多用于单页面应用+第三方库或多页面应用的情况下。

Chunks

介绍

chunk是webpack代码分离(code splitting)的产物,里面装载了一系列具有依赖关系的模块。

chunk类别
  • entry chunk:入口代码块包含了 webpack 运行时需要的一些函数,以及依赖的一系列模块。通过output.filename配置输出

  • normal chunk:普通代码块(又名非主入口文件)没有包含运行时需要的代码,主要指代那些应用运行时动态加载(比如require.ensure)的模块。

  • initial chunk:通过output.chunkfilename配置输出,是这个插件提取共有模块形成的chunk,其会在应用初始化时完成加载

对应关系

以下chunk均指entry chunk

  • chunk & entry file:一个入口文件对应一个chunk,由入口文件引出的依赖集合导向一个chunk。但是chunk不仅由入口文件导出,提取公共模块、动态加载模块也可生成chunk
  • chunk & bundle:默认情况下一个chunk最终导出一个同名bundle文件

Output

介绍

控制 webpack 如何向硬盘写入编译文件

语法

一个对象,至少含有键为filenamepath的两个键值对

output: {
    filename: 'bundle.js',
    path: '/home/proj/public/assets'
}
// 当有多个入口,使用占位符自动配置成与chunkName同名的出口文件名字和路径
output: {
    filename: '[name].js',
    path: __dirname + '/dist'
}

Bundle

bundle是 webpack 打包后的产物,可以通过配置设置其在硬盘中的存储位置。 一般一个bundle对应一个chunk。

使用webpack-dev-server进行开发时,编译在内存中进行,bundle不存储在硬盘中.

Loader

介绍

loader 用于对模块的源代码进行转换。将其他类型的资源转换成js模块,交给webpack处理

特性
  1. 支持链式调用,一个loader的输出可以是另一个Loader的输入
  2. 可以同步或异步执行
  3. 可以接受参数
  4. 通过文件扩展名(正则)绑定不同类型的文件
  5. 通过npm发布和安装
  6. 可以自己书写loader,作为一个模块导出使用
语法

loader的配置挂在module.rules(array)下,webpack 允许配置多个loader,可选并不限于以下键值对

  • 匹配类(必):匹配文件来应用规则,如test(正则语法,多用于文件后缀的匹配),include(string或array,多用于文件路径的匹配),enclude(string或array,剔除匹配的文件)
  • loader(必):值为loader name(string),多为xxx-loader的形式
  • enforce(非必):值为pre|post,指定 loader 种类。所有 loader 通过 后置, 行内, 普通, 前置 排序,并按此顺序使用。
  • options(非必):值为string或object,值可以传递到 loader 中,将其理解为 loader 选项。
  • parse(非必):值为object,是解析选项配置的集合,配置完成之后,相关插件可以被启用。如commonJs:false表示禁用CommonJs

示例

module: {
  rules: [
    // 匹配后缀为图片类的文件
    test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
    // 应用url-loader
    loader: 'url-loader',
    // 配置传递给loader的选项
    options: {
      limit: 10000,
      name: utils.assetsPath('img/[name].[hash:7].[ext]')
    }
  ]
}

Plugin

介绍

plugin是webpack核心支柱功能,通过plugin(插件)webpack可以实现loader所不能完成的复杂功能,使用plugin丰富的自定义API以及生命周期事件,可以控制webpack编译流程的每个环节,实现对webpack的自定义功能扩展。

原理概览

插件是一个具有apply方法的 JavaScript 对象。webpack有一个实现插件绑定与调用的库(tapable),贯穿整个webpack打包编译过程。不同的插件通过自身的apply方法调用tapableplugin方法,将插件添加到compiler对象,compiler对象可在整个编译生命周期访问,插件从而实现在编译的不同时期对文件进行相应的处理。

用法
  • 先安装: npm install --save html-webpack-plugin

  • 再导入:const HtmlWebpackPlugin = require('html-webpack-plugin')

  • 最后配置:

    plugins: [
    	 // 各个插件配置时需要的参数不一,请参考官方文档
      new webpack.optimize.UglifyJsPlugin(),
      new HtmlWebpackPlugin({template: './src/index.html'})
    ]
    

Modules

介绍

模块是实现特定功能的功能块。webpack的核心理念就是一切皆模块,将所有资源以及文件转化为模块。

webpack中的模块类型
  • ES2015 import 语句
  • CommonJS require() 语句
  • AMD define 和 require 语句
  • css/sass/less 文件中的 @import 语句。
  • 样式(url(...))或 HTML 文件 中的图片链接(image url)

配置应用

基础文件结构

├── node-modules
├── package.json
├── dist
│   └── bundle.js 	  	 // webpack打包好的文件
├── src
│   └── entry.js 		 // webpack打包的入口文件
├── webpack.config.js    // webpack配置文件
└── index.html 			 // 主页面,通过script标签将bundle引入

webpack.config.js详解

webpack.config.js是一个node.js模块,导出json格式的配置信息对象

包含要配置的内容:

  • entry
  • output
  • loader
  • plugin
  • resolve
详细示例

通过vue-cli创建项目,build文件夹下自动生成四个webpack配置文件

  • webpack.base.conf.js:基础,或者说公共webpack配置
  • webpack.dev.conf.js:开发环境下的webpack配置
  • webpack.prod.conf.js:生成环境下的webpack配置
  • webpack.test.conf.js:测试阶段的webpack配置

这里主要关注前三个配置文件

webpack.base.conf.js
var path = require('path')
var utils = require('./utils')
var config = require('../config')
var vueLoaderConfig = require('./vue-loader.conf')

function resolve (dir) {
  return path.join(__dirname, '..', dir)
}

module.exports = {
  entry: {                                                // 配置入口文件
	app: './src/main.js'                                  // app为chunk名,'./src/main.js'为文件路径
  },
  output: {
    path: config.build.assetsRoot,                        //  打包好的文件储存路径  
    filename: '[name].js',                                //  表示根据入口文件的chunk name自动生成对应名字的bundle
    publicPath: process.env.NODE_ENV === 'production'     //  通过判断是否在生产环境下制定内嵌文件(如css、图片)的路径前缀
      ? config.build.assetsPublicPath
      : config.dev.assetsPublicPath
  },
  resolve: {                                              //  配置模块路径的解析器
    extensions: ['.js', '.vue', '.json'],                 //  表示解析器支持以下文件后缀名的文件解析
    alias: {                                              //  创建模块别名代替路径名
      'vue$': 'vue/dist/vue.esm.js',                      //  import或require`vue.esm.js`文件时,可用`vue$`代替
      '@': resolve('src')                                 //  import或require ‘src’文件夹下的模块时,可用'@'作为路径代替  
    }
  },
  module: {
    rules: [                                              //  webpack2&webpack3中通过module.rules设置loader
      {
        test: /\.(js|vue)$/,                              //  匹配js和vue为后缀的文件,应用此条rule
        loader: 'eslint-loader',                          //  使用eslint-loader作为转换器
        enforce: 'pre',                                   //  enforce指定loader种类,pre表示此loader最先执行
        include: [resolve('src'), resolve('test')],       //  匹配此文件夹下的资源
        options: {                                        //  loader选项,里面的值可以传到loader中
          formatter: require('eslint-friendly-formatter') //  代码检查配置工具
        }
      },
      {
        test: /\.vue$/,                                   //  解析vue为后缀的文件,使用vue-loader转换
        loader: 'vue-loader',
        options: vueLoaderConfig
      },
      {
        test: /\.js$/,                                    //  解析js为后缀的文件,使用babel-loader转换(针对es6语法)
        loader: 'babel-loader',
        include: [resolve('src'), resolve('test')]
      },
      {
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,            //  转换图片类文件
        loader: 'url-loader',
        options: {
          limit: 10000,                                   //  限制文件的url字节
          name: utils.assetsPath('img/[name].[hash:7].[ext]')
        }
      },
      {
        test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,//  转换音视频类文件
        loader: 'url-loader',
        options: {
          limit: 10000,                                   //  限制文件的url字节
          name: utils.assetsPath('media/[name].[hash:7].[ext]')     
        }
      },
      {
        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,           //  转换字体类文件
        loader: 'url-loader',
        options: {
          limit: 10000,
          name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
        }
      }
    ]
  }
}

webpack.dev.conf.js
// dev-client文件里封装了与hot-reload相关的代码,将它们加进entry chunk里
Object.keys(baseWebpackConfig.entry).forEach(function (name) {
  baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
})

// merge webpack基础配置
module.exports = merge(baseWebpackConfig, {
  module: {

    // 生成源文件和压缩文件对应的sourceMap
    rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
  },

  // cheap-module-eval-source-map is faster for development
  // 选用此配置作为sourceMap的构建工具,这个工具会帮助在chrome下显示源码文件
  devtool: '#cheap-module-eval-source-map',
  plugins: [

    // 定义开发环境,代码中任何出现 process.env的地方都会被替换为config.dev.env
    new webpack.DefinePlugin({
      'process.env': config.dev.env
    }),

    // https://github.com/glenjamin/webpack-hot-middleware#installation--usage
    // 此插件用于在应用程序运行过程中代码修改时,只修改相关模块而无需重新加载整个页面
    new webpack.HotModuleReplacementPlugin(),

    // 使用此插件在编译出现错误时跳过输出阶段。这样可以确保输出资源不会包含错误。
    new webpack.NoEmitOnErrorsPlugin(),

    // https://github.com/ampedandwired/html-webpack-plugin
    // 此插件用于自动生成html文件,不需要手动创建,再通过script标签引入打包后的bundle
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'index.html',
      inject: true
    }),

    // 此插件用于识别、汇总、清理报错
    new FriendlyErrorsPlugin()
  ]
})

webpack.prod.conf.js
// merge webpack基础配置
var webpackConfig = merge(baseWebpackConfig, {
  module: {
    // utils模块里封装了样式处理loader,可传入两个键值对,是否开启sourceMap和是否启用插件ExtractTextWebpackPlugin
    rules: utils.styleLoaders({
      sourceMap: config.build.productionSourceMap,
      // 意为将所有的入口 chunk(entry chunks)中引用的样式文件,移动到独立分离的 CSS 文件
      extract: true
    })
  },

  // 使用source-map作为sourceMap的构建工具
  devtool: config.build.productionSourceMap ? '#source-map' : false,

  // 定义output导出路径
  output: {
    path: config.build.assetsRoot,
    filename: utils.assetsPath('js/[name].[chunkhash].js'),
    // 非主入口文件的命名规则,非主入口文件一般出现在require.ensure加载模块的时候(异步加载模块,而未给入口文件)
    chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
  },

  plugins: [
    // http://vuejs.github.io/vue-loader/en/workflow/production.html
    // 定义生产环境,代码中任何出现 process.env的地方都会被替换为env
    new webpack.DefinePlugin({
      'process.env': env
    }),

    // 混淆代码
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: false
      },
      sourceMap: true
    }),

    // extract css into its own file
    // 将所有的入口chunk中引用的样式*.css,移动到独立分离的 CSS 文件。
    new ExtractTextPlugin({
      filename: utils.assetsPath('css/[name].[contenthash].css')
    }),

    // Compress extracted CSS. We are using this plugin so that possible
    // duplicated CSS from different components can be deduped.
    // 压缩独立的css样式文件代码,并进行优化
    new OptimizeCSSPlugin({
      cssProcessorOptions: {
        safe: true
      }
    }),

    // generate dist index.html with correct asset hash for caching.
    // you can customize output by editing /index.html
    // see https://github.com/ampedandwired/html-webpack-plugin
    // 此插件用于自动生成html文件,不需要手动创建,再通过script标签引入打包后的bundle
    new HtmlWebpackPlugin({
    	// 生产的html的名字
      filename: process.env.NODE_ENV === 'testing'
        ? 'index.html'
        : config.build.index,
      template: 'index.html',
      // 表示js资源将放在<body>的底部
      inject: true,
      // 减少输出的配置选项
      minify: {
      	  // 移除html注释
        removeComments: true,
        // 折叠会产生文档树节点的空格
        collapseWhitespace: true,
        // 尽可能删除属性的引号
        removeAttributeQuotes: true
        // more options:
        // https://github.com/kangax/html-minifier#options-quick-reference
      },
      // necessary to consistently work with multiple chunks via CommonsChunkPlugin
      // 控制如何将chunk块在html中进行排序
      chunksSortMode: 'dependency'
    }),
    // split vendor js into its own file

    // 将多个入口起点之间共享的公共模块,生成为一些 chunk,并且分离到单独的 bundle 中
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      // minChunks这个函数会被 CommonsChunkPlugin 插件回调,并且调用函数时会传入 module 和 count 参数。控制决定模块被打包到哪里的算法
      minChunks: function (module, count) {
        // any required modules inside node_modules are extracted to vendor
        return (
          module.resource &&
          /\.js$/.test(module.resource) &&
          module.resource.indexOf(
            path.join(__dirname, '../node_modules')
          ) === 0
        )
      }
    }),
    // extract webpack runtime and module manifest to its own file in order to
    // prevent vendor hash from being updated whenever app bundle is updated
    // 声明两次CommonsChunkPlugin是为了避免bundle更新时,vendor的hash改变
    new webpack.optimize.CommonsChunkPlugin({
    	// 长期缓存webpack静态资源的方案
    	// 生成含有chunk信息的manifest文件,包括chunk hash和chunk 目录
      name: 'manifest',
      // 定位chunk
      chunks: ['vendor']
    }),

    // copy custom static assets
    // 复制静态资源地址
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, '../static'),
        to: config.build.assetsSubDirectory,
        ignore: ['.*']
      }
    ])
  ]
})

if (config.build.productionGzip) {
  // 在生产环境下,启动压缩代码插件
  var CompressionWebpackPlugin = require('compression-webpack-plugin')

  webpackConfig.plugins.push(
    // 配置压缩代码插件
    new CompressionWebpackPlugin({
      // 目标资源名称
      asset: '[path].gz[query]',
      // 压缩算法
      algorithm: 'gzip',
      //  所有匹配该正则的资源都会被处理
      test: new RegExp(
        '\\.(' +
        config.build.productionGzipExtensions.join('|') +
        ')$'
      ),
      threshold: 10240,
      //  只有压缩率小于这个值的资源才会被处理
      minRatio: 0.8
    })
  )
}

流程原理

简略版

简要图示 webpack根据模块的依赖关系进行静态分析,然后将这些模块按照指定的规则生成对应的静态资源。

详细版

  1. 所有代码和文件经由loader转换器,处理成模块
  2. webpack分析入口文件(entry.js),解析模块间依赖关系,构建依赖关系图(dependency graph
  3. webpack凭借依赖关系进行代码分离(code spliting)和整理
  4. webpack进行模块打包,生成对应的静态资源

细节版

  1. 命令行输入webpack,系统调用./node_modules/.bin/webpack这个脚本(shell),这个脚本调用./node_modules/webpack/bin/webpack.js并追加输入的参数,如-p(混淆压缩代码)、-w(监控自动打包)
  2. webpack通过optimist(一个node.js库,用来解析选项)将用户配置的webpack.config.jsshell脚本传来的参数整合成option对象传入下一个流程中
  3. webpack初始化,构建complilation对象,该对象负责组织整个编译过程,包含了每个构建环节所对应的方法
  4. 找到入口文件,解析入口文件,通过对应的工厂方法创建模块,保存到compilation对象上
  5. 创建依赖关系图,对依赖链上的模块依次(异步)进行build操作,调用loader处理源文件
  6. 调用seal方法封装,生成编译后的代码,每一个chunk对应一个入口文件,逐次对每个chunk整理、合并、拆分
  7. 处理生成最后的文件
  8. 有一个实现插件绑定与调用的库(tapable),贯穿整个webpack打包编译过程,不同的插件通过自身的apply方法调用tapableplugin方法,将插件添加对compiler对象,从而在编译的不同时期对文件进行相应的处理。

参考图示: webpack工作流程-来自TB前端团队

Complier的事件钩子看编译流程

webpack 的 Compiler模块是创建一个传入webpack CLIwebpack api 或 webpack 配置文件等选项的编译实例的主引擎。

这是 Compiler 暴露的所有事件钩子的参考指南

事件名称 含义
entry-options -
after-plugins 设置插件的初始配置后
after-resolvers 设置解析器后
environment -
after-environment 环境配置完成
before-run compiler.run() 开始
run 读取记录之前
watch-run 监视后开始编译之前
normal-module-factory 创建 NormalModuleFactory 后
context-module-factory 创建 ContextModuleFactory 后
before-compile 编译参数创建完成
compile 创建新编译之前
this-compilation 发送 compilation 事件之前
compilation 编译创建完成
make -
after-compile -
should-emit 此时可以返回 true/false
need-additional-pass -
emit 在发送资源到输出目录之前
after-emit 在发送资源到输出目录之后
done 完成编译
failed 编译失败
invalid 一个监控的编译变无效后

其他功能

代码分离

代码分离是 webpack 中最引人注目的特性之一。此特性能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。

常用方法

入口起点

使用 entry 选项手动分离代码

entry: {
  index: './src/index.js',
  another: './src/another-module.js'
}

这样的方案需要开发者在开发过程中就对模块的组织有清晰的规划,整理出入口文件。

防止重复

使用 CommonsChunkPlugin 去重和分离 chunk。

CommonsChunkPlugin 插件,是一个可选的用于建立一个独立文件(又称作 chunk)的功能,这个文件包括多个入口 chunk 的公共模块。通过将公共模块拆出来,最终合成的文件能够在最开始的时候加载一次,便存起来到缓存中供后续使用。这个带来速度上的提升,因为浏览器会迅速将公共的代码从缓存中取出来,而不是每次访问一个新页面时,再去加载一个更大的文件。

动态导入

通过模块的内联函数调用来分离代码

require.ensure()动态导入模块,output会生成额外的chunkFile

Webpack Dev Server介绍

webpack dev server是webpack提供的本地开发服务器,内建有liveReload功能。

概念了解
代码同步对象 webpack 浏览器刷新 是否需要服务器
watch mode webpack 自动打包 手动刷新
liveReload 浏览器视图 自动打包 自动刷新全部
hotReload 浏览器视图中对应部分 自动打包 自动刷新修改部分

webpack dev server基于Express,使用webpack-dev-middleware来支持webpack的打包,它使用内存编译,这意味着bundle不会被保存在硬盘上

使用方式
  • 安装:npm install --save-dev webpack-dev-server
  • 唤起:webpack-dev-server --open

或者在package.json中配置:

"scripts": {
    "start": "webpack-dev-server"
  }

使用npm start打开服务器,即可在http://localhost:8080中查看

关于Wepack Dev Server的详细配置

HMR(Hot-Module-Replacement)介绍

模块热替换HMR功能会在应用程序运行过程中替换、添加或删除模块,而无需重新加载整个页面。主要是通过以下几种方式,来显著加快开发速度:

  • 保留应用程序状态
  • 只更新变更内容
  • 调整样式更快
流程
  1. 应用程序要求HMR检查更新(check方法)

  2. HMR找到修改模块,下载更新,更新由两部分组成:

    • 一个或多个更新后的chunk补丁文件(.js):每个chunk都含有对应于此chunk的全部更新模块的代码。
    • 描述性文件manifest.json):包括新的编译 hash 和所有的待更新 chunk 目录
  3. 应用程序代码要求HMR应用更新(apply方法)

  4. HMR应用更新。启用webpack-dev-server服务时,server会响应客户端发起的eventStream请求,保持请求不断,服务器就可以把结果push到浏览器

使用方式(使用webpack-dev-server服务)

通过在webpack.config.jswebpack.dev.conf.js中进行配置插件使用 HotModuleReplacementPlugin插件挂载在webpack下,所有不需要另外安装

module.exports = {
plugins: [
    new webpack.HotModuleReplacementPlugin() // 启用 HMR
  ],
devServer: {
    hot: true, // 告诉 dev-server 我们在使用 HMR
    contentBase: path.resolve(__dirname, 'dist'),
    publicPath: '/'
  }
};

此时HMR的接口暴露在module.hot下,通过在module.hot下使用HMR的方法,实现在更新周期中的操作,如

if (module.hot) {
  module.hot.accept('./library.js', function() {
    // 使用更新过的 library 模块执行某些操作...
  })
}

当我们不使用任何方法时,HMR默认会进行冒泡更新,一个模块被更新时,整组依赖模块(模块树)都会重新加载

当我们有自己的服务器又想使用HMR功能时,可以采用webpack-dev-middleware + webpack-hot-middleware的解决方案

指路:Webpack Hot Middleware

常用插件介绍

名称 描述
CommonsChunkPlugin 将多个入口起点之间共享的公共模块,生成为一些 chunk,并且分离到单独的 bundle 中,例如,vendor.bundle.js 和 app.bundle.js
DefinePlugin 允许在编译时(compile time)配置的全局常量,用于允许「开发/发布」构建之间的不同行为
HtmlWebpackPlugin 用于简化 HTML 文件(index.html)的创建,提供访问 bundle 的服务
IgnorePlugin 从 bundle 中排除某些模块
ExtractTextPlugin 将所有的入口chunk中引用的 *.css,移动到独立分离的 CSS 文件。
CompressionWebpackPlugin 帮助压缩代码
UglifyJsPlugin 帮助混淆代码

更多插件:https://doc.webpack-china.org/plugins/compression-webpack-plugin/

参考文献

escawn avatar Nov 14 '17 12:11 escawn