03月19, 2018

webpack3配置原理浅析

20180811更新了PPT, 增加一些文字描述。希望通过本文可以让大家对webpack打包流程有一个大致了解,也对webpack4的学习有一定的帮助。

20180811更新PPT, 增加一些文字描述

webpack各版本的优点

  • 2 -> tree-shaking, 需要借助一些压缩插件
    • webapck在输出最终的文件时,会在源码中需要剔除的位置加上一段注释。
    • 然后由压缩插件,如uglify等把这些代码去掉
  • 3 -> scope hoisting, 作用域提升。减少文件大小及内存消耗
  • 4 -> 零配置,速度快98%
    • 受到parcel的影响,以及一直以来人们对webapck复杂配置的诟病

其中tree-shaking和scope hoisting都是最先由rollup.js提出的,在webpack4的production模式下默认打开。同时这两个特性都需要依赖ES6的import,在代码运行前就可以进行分析。 如果tree-shaking并不明显,那很有可能是因为使用了babel把import转成了CommonJS的require

scope hoisting

  • 减少文件大小
  • 减少内存开销,通过减少闭包。闭包函数会降低JS引擎解析的速度
  • 主要依赖ES6的模块加载机制,静态分析。
    • 通过分析出模块之前的依赖关系,检测那些被引入的链里哪些可以被打平,变成一行的方法,而又不影响原代码,尽可能的把打散的模块合并到一个函数中去。
    • 之前是wbepack自己实现的一套模块加载、执行与缓存的功能,将所有模块都用函数包裹起来了。这样做的目的是为了更容易的实现 Code Splitting、模块热替换等。
  • 在webpack3中使用scope hoisting需要使用这个插件webpack/lib/optimize/ModuleConcatenationPlugin

webpack4的变化

  • 内部构建效率提升
    • 使用最新JS语法;重构优化了一些模块和流程
  • 零配置
    • entry(src/index.js), output
  • cli 工具拆分: webpack-cli 及核心库
    • 使用前必须安装webpack-cli
  • mode模式
    • development,专注开发体验
      • 增量构建,自动开启 sourceMap 等开发插件
    • production,专注项目部署
      • 自动开启代码压缩、tree-shaking、scope hoist
      • 自动传递环境变量给 lib 包
  • 增加了sideEffectst特性
    • 通过给包/模块 的package.json 加入 sideEffects声明,在tree-sharking时告诉webapck,我这个包在设计的时候就是期望没有副作用的
    • 无论它是否有副作用,只要它没有被引用到,webpack在摇树的时候都会把它完整的删掉
  • 去掉了CommonsChunkPlugin
    • 使用optimization.splitChunks、optimization.runtimeChunk来代替
  • 插件写法更语义化,hook
    • 所以使得很多插件基本都需要重构,webpack4还能支持之前的plugin的写法??

这些名词的意思?

  • entry:webpack从配置的这里开始递归查找所以的依赖模块,会由此将entry的依赖构成一个graph,并导向一个chunk
  • module:webpack中的核心实体,需要加载的一切和所有的依赖都是module。一个或多个module构成一个chunk
  • chunk:各module经过编译之后的代码包,比如loader的处理等。可分为3类,entry chunk,normal chunk, initial chunk
    • entry chunk, 包含webpack启动(bootstrap)代码的那个chunk。直观说是包含有webpackJsonp, __webpack_require的chunk
    • normal chunk,没有包含启动代码的chunk,主要指那些应用运行时动态加载的模块
    • initial chunk,本质上还是normal chunk,往往由CommonsChunkPlugin生成
  • loader: 描述了 webpack 如何处理非 JavaScript模块。比如对img, 对css的处理
  • plugin: 服务于编译期间,是一个具有apply属性的JavaScript对象
    • 监听webpack事件点。有点类似DOM中的事件监听。

处理第三方库和不同场景下的commonChunk(见本博客另一篇文章)

webpack的关键工作流程

简单说,webpack的工作流程犹如一个生产线,在这个生产线上会广播不同的事件,而不同的插件会监听这些事件,并作出反应。

  • 获取配置参数,拿到最终的webpack参数options
  • 根据参数初始化 Compiler 对象。加载所有配置的插件,执行插件的apply方法(这里会像apply中传入compiler对象)。广播compiler.run
  • 编译模块:根据entry,查找并递归使用loader处理各个依赖。广播compile, make
  • 完成编译:递归完成之后。广播after-compile
  • 输出资源:是最后一个可以修改webpack生成内容的节点。广播emmit
  • 输出完成:将最终的代码写入文件系统中
webpack,广播事件:
  call(事件名)

插件,监听事件:
Plugin.apply(compiler) {
  compiler.plugin(事件名,callback)
}

webpack的关键对象

  • acorn: 用来分析JS源码,生成AST,进而webpack根据AST来获取依赖。rollup等都在使用。
  • Tapable:webpack的工作流插件系统基类。内部通过数组来保存同一事件节点的不同监听多个插件。
    • 所以多个插件监听同一个事件节点是按注册顺序执行。
    • compiler,complation都继承于它
  • compiler:对象代表了完整的 webpack 环境配置。一次webpack启动,整个过程中之后有一个compiler.
  • complation:包含了一次构建过程中所有的数据。每一次webpack编译都会生成一个complation实例。比如webpack watch编译的时候。就之后有一个compiler,而多个不同的complation实例。

是否需要升级到webpack4

  • 7月20日,webpack的作者说,他已经在写webpack5了。。emmm。。
  • 中文文档已更新
  • 常用的插件都已适配
    • 老版本处于废弃或维护状态,新版本提供新特效
  • 构建时间平均能提高20%-30%,还是值得一试

  • 可能遇到的问题:

    • 调用webpack的启动方式变了
    • 插件是否满足业务需求,能否找到可替代的

webpack与团队构建工具

  • 这里讨论的团队构建工具是指,基于webpack,加上自己业务的特点,重新定制的构建工具。
  • 对于业务来说,团队构建工具是一个黑盒,里面无论是使用webpack或是rollup,或是webpack3,还是webpack4,只要团队构建工具能够满足开发需求,好用即可。
  • 类似于angular-cli一样。
  • 就目前构建工具的发展趋势来看,将整个体系拆分成xxx-cli和xxx-core两部分会定制化更强。
  • 且,webapck的插件机制非常值得学习。
  • 了解业务的构建工具,目的有这么几方面:
    • 构建工具不再是黑盒
    • 知其然,知其所以然
    • 帮助定位发现问题
    • 有利于生态建设,从工程角度,插件编写,提高开发体验和效率。

自己的思考

  • 构建工具日渐成熟, 0配置,降低使用门槛。但如果是满足自己的业务需求还是需要定制,并且不同业务的复用性比较低,因为不同业务的特效比较低,有自己的特点。
  • 并且构建工具也在相互借鉴,不断发展。比如webpack4里就借鉴了rollup和parcel的很多好的优势。
  • 另外,现在不管出现了什么样的新的构建工具,都也不必慌张,万变不离其宗,了解其中一个原理,其他的也会触类旁通。

这大概是拖了半年有余的博客。基于之前的ppt,内容有所增加和调整,现整理成文章补上。

参考

附:

webpack3的“非典型”配置

const path = require('path');
const glob = require('glob');
const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
const DashboardPlugin = require('webpack-dashboard/plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const CommonsChunkPlugin = webpack.optimize.CommonsChunkPlugin;

const pagesDir = path.resolve(__dirname, './tpls/pages');
const buildDir = path.resolve(__dirname, './build');

// 查找入口文件,多页
let configEntry = {};
let globInstance = new glob.Glob('**/+(*.page.js|page.js)', {
  cwd: pagesDir, 
  sync: true, 
});
globInstance.found.forEach((page) => {
  let split = page.match(/(.*)\.js$/);
  let key = split[1];
  configEntry[key] = path.resolve(pagesDir, page);
});

module.exports = {
  entry: Object.assign({}, {'vendor': ['d3', 'd3-selection-multi']}, configEntry),
  output: {
    path: buildDir,
    publicPath: process.env.NODE_ENV === 'production' ? '/' : '/',
    filename: `js/[name].js`,
    chunkFilename: '[id].[chunkhash].bundle.js',
  },
  resolve: {
    extensions: ['.js', '.json'],
    modules: [path.resolve(__dirname, 'src'), path.resolve(__dirname, 'node_modules')],
    alias: {
      'Components': path.resolve(__dirname, 'tpls/components'),
      'IncJs': path.resolve(__dirname, 'tpls/common/js'),
      'IncCss': path.resolve(__dirname, 'tpls/common/css'),
    },
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        include: [path.resolve(__dirname, 'tpls')],
        use: ExtractTextPlugin.extract({
          fallback: 'style-loader',
          use: ['css-loader',
            {
              loader: 'postcss-loader',
              options: {
                // 里面有配css-lint
                config: {path: './postcss.config.js'},
              },
            },
          ],
        }),
      },
      // eslint
      {
        enforce: 'pre',
        test: /\.js$/,
        include: [path.resolve(__dirname, 'tpls'), /\*.config.js/],
        exclude: /node_modules/,
        loader: 'eslint-loader',
        options: {
          failOnWarning: false,
          failOnError: false,
          formatter: require('eslint-friendly-formatter'),
        },
      },
      {
        test: /\.js$/,
        include: [path.resolve(__dirname, 'tpls'), /\*.config.js/],
        loader: 'babel-loader',
        options: {
          presets: [['env', {
            'targets': {
              'browsers': ['> 1%', 'ie >= 9'],
            },
          }]],
          cacheDirectory: true,
          plugins: ['transform-runtime'],
        },
      },
      {
        test: /\.(png|jpe?g|gif)(\?.*)?$/,
        loader: 'url-loader',
        options: {
          limit: 512,
          outputPath: 'img/',
        },
      },
      {
        test: /\.(eot|ttf|woff|woff2)(\?\S*)?$/,
        loader: 'file-loader',
        options: {
          outputPath: 'img/',
        },
      },

    ],
  },
  plugins: [
    new CleanWebpackPlugin(['build/css', 'build/js', 'build/img', 'build/svg']),
    new CommonsChunkPlugin({
      name: 'vendor',
      filename: `js/vendor.js`,
      minChunks: Infinity,
    }),
    new CommonsChunkPlugin({
      name: 'common',
      filename: `js/common.js`,
      chunks: [...Object.keys(configEntry)],
      minChunks: 4,
    }),
    new ExtractTextPlugin(`css/[name].css`),
  ],
  externals: {
    'jquery': 'window.jQuery',
  },
};

if (process.env.NODE_ENV === 'production') {
  module.exports.plugins = module.exports.plugins || [];
  module.exports.plugins = module.exports.plugins.concat([
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: '"production"',
        DEBUG_ENV: JSON.stringify(process.env.DEBUG_ENV),
      },
    }),
  ]);

  module.exports.plugins = module.exports.plugins.concat([
    new ParallelUglifyPlugin({
      uglifyJS: {},
    }),
  ]);
} else {
  module.exports.plugins = module.exports.plugins || [];

  module.exports.plugins = module.exports.plugins.concat([
    new DashboardPlugin(),
  ]);
}

webpack4的配置(with vue2)

var webpack = require('webpack');
var path = require("path");
// mini-css-extract-plugin 替换 extract-text-webpack-plugin
var MiniCssExtractPlugin = require('mini-css-extract-plugin');
var HtmlWebpackPlugin = require('html-webpack-plugin');
const { VueLoaderPlugin } = require('vue-loader')

var UglifyJsPlugin = require('uglifyjs-webpack-plugin');
var srcDir = path.join(__dirname, '/src');

var entries = (function () {
  var map = {};
  var fileName = (process.env.NODE_ENV === 'production')
    ? ['index', 'report']
    : ['index', 'report'];

  fileName.forEach(function (ele) {
    var filePath = path.join(srcDir, ele + '.js');
    map[ele] = filePath;
  });
  return map;
})();

var echartsLib = ["echarts/lib/echarts", "echarts/lib/chart/bar", "echarts/lib/chart/pie", "echarts/lib/chart/map", "echarts/lib/component/tooltip", "echarts/lib/component/title", "echarts/lib/component/legend"]
entries['vendor'] = ["jquery", "vue", "vue-router", "vue-resource"].concat(echartsLib);

module.exports = {
  mode: process.env.NODE_ENV === 'production' ? "production" : "development",
  entry: Object.assign(entries),
  output: {
    path: path.join(__dirname + "./../www/static/js"),
    publicPath: process.env.NODE_ENV === 'production' ? "/static/js/" : "/build/",
    filename: '[name].js',
    chunkFilename: "[name].chunk.js"
  },
  resolve: {
    mainFields: ['browser', 'module', 'main'],
    extensions: ['.js', '.vue'],
    modules: [path.resolve(__dirname, 'src'), path.resolve(__dirname, 'node_modules')],
    alias: {
      'vue': process.env.NODE_ENV === 'production' ? 'vue/dist/vue.min.js' : 'vue/dist/vue.js',
    }
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      },
      {
        test: /\.css/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader'
        ]
      },
      {
        test: /\.jsx?$/,
        use: 'babel-loader?cacheDirectory',
        exclude: /node_modules/
      },
      {
       test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
       use: [
         {
          loader: 'url-loader',
          options: {
            limit: 100
          }
         }
       ]
      }
    ]
  },
  optimization: {
    // 单独提出webpack的runtime 启动代码
    runtimeChunk: {
      name: 'manifest'
    },
    // 打出工具包JS
    splitChunks: {
      cacheGroups: {
        vendor: {
          chunks: "initial",
          test: "vendor",
          name: "vendor",
          enforce: true
        }
      }
    }
  },
  plugins: [
    new VueLoaderPlugin(),
    // 单独提出css文件
    new MiniCssExtractPlugin({
      filename: '[name].css',
      chunkFilename: "[name].[contenthash:12].css"
    }),
  ]
}

if (process.env.NODE_ENV === 'production') {
  module.exports.plugins = (module.exports.plugins || []).concat([
    new HtmlWebpackPlugin({
        filename: path.resolve(__dirname, './../view/home/index_index.html'),
        template: path.resolve(__dirname, './tpl_demo.html'),
        inject: true,
        chunks: ['vendor', 'index', 'manifest']
    }),
    new HtmlWebpackPlugin({
      filename: path.resolve(__dirname, './../view/home/index_report.html'),
      template: path.resolve(__dirname, './tpl_demo.html'),
      inject: true,
      chunks: ['vendor', 'report','manifest']
  }),
])

} else {
  module.exports.devtool = '#cheap-module-eval-source-map';

  var proxyObj = [
    // '/',
    '/index/getreport',
    '/index/empty',
  ].reduce(function (o, u) {
    o[u] = {
      target: 'http://127.0.0.1:5893',
      secure: false
    };
    return o;
  }, {})


  module.exports.devServer = {
    inline: true,
    hot: true,
    proxy: proxyObj,
    historyApiFallback: true,
    contentBase: './',
  }
}

本文链接:https://imjiaolong.cn/post/webpack-sharing.html.html

-- EOF --

Comments

评论加载中...

注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。