博客
博客
文章目录
  1. 一、前言
  2. 二、到底怎么升级
    1. 0、初始化配置
    2. 1、零配置
    3. 2、loader 与 plugin 的升级
    4. 3、webpack4 的模块拆分
      1. 什么模块会进行提取?
      2. 提取的规则是什么?
  • 三、赠送 webpack 常见优化方式
    1. 1、一个人不行,大家一起上
    2. 2、打包再打包
    3. 3、你胖你先跑,部分代码预先运行
  • 四、总结
  • 五、参考
  • webpack4 初探

    一、前言

    2018/2/25,webpack4 正式发布,距离现在已经过去三个多月了,也逐渐趋于稳定,而且现在的最新版本都到了 4.12.0(版本迭代快得真是让人害怕)。

    很多人都说 webpack 复杂,难以理解,很大一部分原因是 webpack 是基于配置的,可配置项很多,并且每个参数传入的形式多种多样(可以是字符串、数组、对象、函数。。。),文档介绍也比较模糊,这么多的配置项各种排列组合,想想都复杂。而 gulp 基于流的方式来处理文件,无论从理解上,还是功能上都很容易上手。

    最新版本

    //gulp
    gulp.src ('./src/js/**/*.js')
    .pipe ('babel')
    .pipe ('uglifyjs')
    .dest ('./dist/js')

    //webpack
    module.exports = {
    entry: './src/main.js',
    output: __dirname + '/dist/app.js',
    module: {
    rules: [{
    test: /\.js$/,
    loader: 'babel-loader'
    }]
    },
    plugins: [
    new require('uglifyjs-webpack-plugin')()
    ]
    }

    上面简单对比了 webpack 与 gulp 配置的区别,当然这样比较是有问题的,gulp 并不能进行模块化的处理。这里主要是想告诉大家使用 gulp 的时候,我们能明确的知道 js 文件是先进行 babel 转译,然后进行压缩混淆,最后输出文件。而 webpack 对我们来说完全是个黑盒,完全不知道 plugins 的执行顺序。正是因为这些原因,我们常常在使用 webpack 时有一些不安,不知道这个配置到底有没有生效,我要按某种方式打包到底该如何配置?

    为了解决上面的问题,webpack4 引入了 零配置 的概念(Parcel ???),实际体验下来还是要写不少配置。
    但是这不是重点,重点是官方宣传 webpack4 能够提升构建速度 60%-98%,真的让人心动。

    二、到底怎么升级

    0、初始化配置

    首先安装最新版的 webpack 和 webpack-dev-server,然后再安装 webpack-cli。webpack4 将命令行相关的操作抽离到了 webpack-cli 中,所以,要使用 webpack4,必须安装 webpack-cli。当然,如果你不想使用 webpack-cli,社区也有替代方案 webpack-command,虽然它与 webpack-cli 区别不大,但是还是建议使用官方推荐的 webpack-cli。

    npm i webpack@4 webpack-dev-server@3 --save-dev
    npm i webpack-cli --save-dev

    webpack-cli 除了能在命令行接受参数运行 webpack 外,还具备 migrateinit 功能。

    1. migrate 用来升级 webpack 配置,能将 webpack1 的 api 升级到 webpack2,现在用处不大。
    $ webpack-cli migrate ./webpack.config.js
    ✔ Reading webpack config
    ✔ Migrating config from v1 to v2
    - loaders: [
    + rules: [
    - loader: 'babel',
    - query: {
    + use: [{
    + loader: 'babel-loader'
    + }],
    + options: {
    - loader: ExtractTextPlugin.extract ('style', 'css!sass')
    + use: ExtractTextPlugin.extract ({
    + fallback: 'style',
    + use: 'css!sass'
    + })
    ? Are you sure these changes are fine? Yes

    ✔︎ New webpack v2 config file is at /home/webpack-cli/build/webpack.config.js
    1. init 可以快速生成一个 webpack 配置文件的模版,不过用处也不大,毕竟现在的脚手架都集成了 webpack 的配置。
    webpack-cli init

    1. Will your application have multiple bundles? No // 如果是多入口应用,可以传入一个 object
    2. Which module will be the first to enter the application? [example: './src/index'] ./src/index// 程序入口
    3. What is the location of "app"? [example: "./src/app"] './src/app'
    4. Which folder will your generated bundles be in? [default: dist]
    5. Are you going to use this in production? No
    6. Will you be using ES2015? Yes // 是否使用 ES6 语法,自动添加 babel-loader
    7. Will you use one of the below CSS solutions? SASS // 根据选择的样式类型,自动生成 loader 配置
    8. If you want to bundle your CSS files, what will you name the bundle? (press enter to skip)
    9. Name your 'webpack.[name].js?' [default: 'config']: //webpack.config.js

    Congratulations! Your new webpack configuration file has been created!

    更详细介绍请查看 webpack-cli 的 文档

    1、零配置

    零配置就意味着 webpack4 具有默认配置,webpack 运行时,会根据 mode 的值采取不同的默认配置。如果你没有给 webpack 传入 mode,会抛出错误,并提示我们如果要使用 webpack 就需要设置一个 mode。

    没有使用 mode

    The ‘mode’ option has not been set, webpack will fallback to ‘production’ for this value. Set ‘mode’ option to ‘development’ or ‘production’ to enable defaults for each environment.
    You can also set it to ‘none’ to disable any default behavior. Learn more: https://webpack.js.org/concepts/mode/

    mode 一共有如下三种配置:

    1. none

      这个配置的意思就是不使用任何默认配置

    1. development,开发环境下的默认配置
    module.exports = {
    // 开发环境下默认启用 cache,在内存中对已经构建的部分进行缓存
    // 避免其他模块修改,但是该模块未修改时候,重新构建,能够更快的进行增量构建
    // 属于空间换时间的做法
    cache: true,
    output: {
    pathinfo: true // 输入代码添加额外的路径注释,提高代码可读性
    },
    devtools: "eval", //sourceMap 为 eval 类型
    plugins: [
    // 默认添加 NODE_ENV 为 development
    new webpack.DefinePlugin ({ "process.env.NODE_ENV": JSON.stringify ("development") }),
    ],
    optimization: {
    namedModules: true, // 取代插件中的 new webpack.NamedModulesPlugin ()
    namedChunks: true
    }
    }
    1. production,生产环境下的默认配置
    module.exports = {
    performance: {
    hints: 'warning',
    maxAssetSize: 250000, // 单文件超过 250k,命令行告警
    maxEntrypointSize: 250000, // 首次加载文件总和超过 250k,命令行告警
    }
    plugins: [
    // 默认添加 NODE_ENV 为 production
    new webpack.DefinePlugin ({ "process.env.NODE_ENV": JSON.stringify ("production") })
    ],
    optimization: {
    minimize: true, // 取代 new UglifyJsPlugin (/* ... */)
    providedExports: true,
    usedExports: true,
    // 识别 package.json 中的 sideEffects 以剔除无用的模块,用来做 tree-shake
    // 依赖于 optimization.providedExports 和 optimization.usedExports
    sideEffects: true,
    // 取代 new webpack.optimize.ModuleConcatenationPlugin ()
    concatenateModules: true,
    // 取代 new webpack.NoEmitOnErrorsPlugin (),编译错误时不打印输出资源。
    noEmitOnErrors: true
    }
    }

    其他的一些默认值:

    module.exports = {
    context: process.cwd ()
    entry: './src',
    output: {
    path: 'dist',
    filename: '[name].js'
    },
    rules: [
    {
    type: "javascript/auto",
    resolve: {}
    },
    {
    test: /\.mjs$/i,
    type: "javascript/esm",
    resolve: {
    mainFields:
    options.target === "web" ||
    options.target === "webworker" ||
    options.target === "electron-renderer"
    ? ["browser", "main"]
    : ["main"]
    }
    },
    {
    test: /\.json$/i,
    type: "json"
    },
    {
    test: /\.wasm$/i,
    type: "webassembly/experimental"
    }
    ]
    }

    如果想查看更多 webpack4 相关的默认配置,到这里来。可以看到 webpack4 把很多插件相关的配置都迁移到了 optimization 中,但是我们看看 官方文档 对 optimization 的介绍简直寥寥无几,而在默认配置的代码中,webpack 对 optimization 的配置有十几项,反正我是怕了。

    文档对 optimization 的介绍

    虽然 api 发生了一些变化,好的一面就是有了这些默认值,我们想通过 webpack 构建一个项目比以前要简单很多,如果你只是想简单的进行打包,在 package.json 中添加如下两个 script,包你满意。

    {
    "scripts": {
    "dev": "webpack-dev-server --mode development",
    "build": "webpack --mode production"
    },
    }

    开发环境使用 webpack-dev-server,边预览边打包再也不用 f5,简直爽歪歪;生产环境直接生成打包后的文件到 dist 目录

    2、loader 与 plugin 的升级

    loader 的升级就是一次大换血,之前适配 webpack3 的 loader 都需要升级才能适配 webpack4。如果你使用了不兼容的 loader,webpack 会告诉你:

    DeprecationWarning: Tapable.apply is deprecated. Call apply on the plugin directly instead

    DeprecationWarning: Tapable.plugin is deprecated. Use new API on .hooks instead

    如果在运行过程中遇到这两个警告,就表示你有 loader 或者 plugin 没有升级。造成这两个错误的原因是,webpack4 使用的新的插件系统,并且破坏性的对 api 进行了更新,不过好在这只是警告,不会导致程序退出,不过建议最好是进行升级。对于 loader 最好全部进行一次升级,反正也不亏,百利而无一害。

    关于 plugin,有两个坑,一个是 extract-text-webpack-plugin,还一个是 html-webpack-plugin

    先说说 extract-text-webpack-plugin,这个插件主要用于将多个 css 合并成一个 css,减少 http 请求,命名时支持 contenthash (根据文本内容生成 hash)。但是 webpack4 使用有些问题,所以官方推荐使用 mini-css-extract-plugin

    ⚠️ Since webpack v4 the extract-text-webpack-plugin should not be used for css. Use mini-css-extract-plugin instead.

    这里改动比较小,只要替换下插件,然后改动下 css 相关的 loader 就行了:

    -const ExtractTextPlugin = require ('extract-text-webpack-plugin')
    +const MiniCssExtractPlugin = require ('mini-css-extract-plugin')

    module.exports = {
    module: {
    rules: [
    {
    test: /\.css$/,
    - use: ExtractTextPlugin.extract ({
    - use: [{
    - loader: 'css-loader',
    - options: {
    - minimize: process.env.NODE_ENV === 'production'
    - }
    - }],
    - fallback: 'vue-style-loader'
    - })
    + use: [
    + MiniCssExtractPlugin.loader,
    + {
    + loader: 'css-loader',
    + options: {
    + minimize: process.env.NODE_ENV === 'production'
    + }
    + ],
    }
    ]
    },
    plugins:[
    - new ExtractTextPlugin ({
    + new MiniCssExtractPlugin ({
    filename: 'css/[name].css',
    }),
    ...
    ]
    }

    然后看看 html-webpack-plugin,将这个插件升级到最新版本,一般情况没啥问题,但是有个坑,最好是把 chunksSortMode 这个选项设置为 none。

    const HtmlWebpackPlugin = require('html-webpack-plugin')
    module.exports = {
    plugins:[
    new HtmlWebpackPlugin ({
    filename: 'index.html',
    template: 'index.html',
    inject: true,
    hash: true,
    chunksSortMode: 'none' // 如果使用 webpack4 将该配置项设置为 'none'
    })
    ]
    }

    官方有个 issues 讨论了这个问题,感兴趣可以去看看。目前作者还在寻找解决方案中。
    html-webpack-plugin issues

    另外,webpack-dev-server 也有个升级版本,叫做 webpack-serve,功能比 webpack-dev-server 强大,支持 HTTP2、使用 WebSockets 做热更新,暂时还在观望中,后续采坑。

    3、webpack4 的模块拆分

    webpack3 中,我们经常使用 CommonsChunkPlugin 进行模块的拆分,将代码中的公共部分,以及变动较少的框架或者库提取到一个单独的文件中,比如我们引入的框架代码 (vue、react)。只要页面加载过一次之后,抽离出来的代码就可以放入缓存中,而不是每次加载页面都重新加载全部资源。

    CommonsChunkPlugin 的常规用法如下:

    module.exports = {
    plugins: [
    new webpack.optimize.CommonsChunkPlugin ({ // 将 node_modules 中的代码放入 vendor.js 中
    name: "vendor",
    minChunks: function(module){
    return module.context && module.context.includes ("node_modules");
    }
    }),
    new webpack.optimize.CommonsChunkPlugin ({ // 将 webpack 中 runtime 相关的代码放入 manifest.js 中
    name: "manifest",
    minChunks: Infinity
    }),
    ]
    }

    之前 CommonsChunkPlugin 虽然能用,但是配置不够灵活,难以理解,minChunks 有时候为数字,有时候为函数,并且如果同步模块与异步模块都引入了相同的 module 并不能将公共部分提取出来,最后打包生成的 js 还是存在相同的 module。

    现在 webpack4 使用 optimization.splitChunks 来进行代码的拆分,使用 optimization.runtimeChunk 来提取 webpack 的 runtime 代码,引入了新的 cacheGroups 概念。并且 webpack4 中 optimization 提供如下默认值,官方称这种默认配置是保持 web 性能的最佳实践,不要手贱去修改,就算你要改也要多测试(官方就是这么自信)。

    module.exports = {
    optimization: {
    minimize: env === 'production' ? true : false, // 是否进行代码压缩
    splitChunks: {
    chunks: "async",
    minSize: 30000, // 模块大于 30k 会被抽离到公共模块
    minChunks: 1, // 模块出现 1 次就会被抽离到公共模块
    maxAsyncRequests: 5, // 异步模块,一次最多只能被加载 5 个
    maxInitialRequests: 3, // 入口模块最多只能加载 3 个
    name: true,
    cacheGroups: {
    default: {
    minChunks: 2,
    priority: -20
    reuseExistingChunk: true,
    },
    vendors: {
    test: /[\\/] node_modules [\\/]/,
    priority: -10
    }
    }
    },
    runtimeChunk {
    name: "runtime"
    }
    }
    }

    有了这些默认配置,我们几乎不需要任何成功就能删除之前 CommonChunkPlugin 的代码,好神奇。

    什么模块会进行提取?

    通过判断 splitChunks.chunks 的值来确定哪些模块会提取公共模块,该配置一共有三个选项,initialasyncall
    默认为 async,表示只会提取异步加载模块的公共代码,initial 表示只会提取初始入口模块的公共代码,all 表示同时提取前两者的代码。

    这里有个概念需要明确,webpack 中什么是初始入口模块,什么是异步加载模块。e.g.

    //webpack.config.js
    module.exports = {
    entry: {
    main: 'src/index.js'
    }
    }

    //index.js
    import Vue from 'vue'
    import(/* webpackChunkName: "asyncModule" */'./a.js')
    .then (mod => {
    console.log ('loaded module a', mod)
    })

    console.log ('initial module')
    new Vue ({})

    //a.js
    import _ from 'lodash'
    const obj = { name: 'module a' }
    export default _.clone (obj)

    上面的代码中,index.js 在 webpack 的 entry 配置中,这是打包的入口,所以这个模块是初始入口模块。再看看 index.js 中使用了动态 import 语法,对 a.js(该异步模块被命名为 asyncModule)进行异步加载,则 a.js 就是一个异步加载模块。再看看 index.jsa.js 都有来自 node_modules 的模块,按照之前的规则,splitChunks.chunks 默认为 async,所以会被提取到 vendors 中的只有 webpackChunkName 中的模块。

    chunks 为 async

    如果我们把 splitChunks.chunks 改成 all,main 中来自 node_modules 的模块也会被进行提取了。

    module.exports = {
    optimization: {
    splitChunks: {
    chunks: "all"
    }
    }
    }

    chunks 为 all

    现在我们在 index.js 中也引入 lodash,看看入口模块和异步模块的公共模块还会不会像 CommonsChunkPlugin 一样被重复打包。

    //index.js
    import Vue from 'vue'
    import _ from 'lodash'

    import(/* webpackChunkName: "asyncModule" */'./a.js')
    .then (mod => {
    console.log ('loaded module a', mod)
    })

    console.log ('initial module')
    console.log (_.map ([1,2,3], a => {
    return a * 10
    }))
    new Vue ({})

    //a.js
    import _ from 'lodash'
    const obj = { name: 'module a' }
    export default _.clone (obj)

    解决了 CommonsChunkPlugin 的问题

    可以看到之前 CommonsChunkPlugin 的问题已经被解决了,main 模块与 asyncModule 模块共同的 lodash 都被打包进了 vendors~main.js 中。

    提取的规则是什么?

    splitChunks.cacheGroups 配置项就是用来表示,会提取到公共模块的一个集合,也就是一个提取规则。像前面的 vendor,就是 webpack4 默认提供的一个 cacheGroup,表示来自 node_modules 的模块为一个集合。

    除了 cacheGroups 配置项外,可以看下其他的几个默认规则。

    1. 被提取的模块必须大于 30kb;
    2. 模块被引入的次数必须大于 1 次;
    3. 对于异步模块,生成的公共模块文件不能超出 5 个;
    4. 对于入口模块,抽离出的公共模块文件不能超出 3 个。

    对应到代码中就是这四个配置:

    {
    minSize: 30000,
    minChunks: 1,
    maxAsyncRequests: 5,
    maxInitialRequests: 3,
    }

    三、赠送 webpack 常见优化方式

    1、一个人不行,大家一起上

    webpack 是一个基于 node 的前端打包工具,但是 node 基于 v8 运行时只能是单线程,但是 node 中能够 fork 子进程。所以我们可以使用多进程的方式运行 loader,和压缩 js,社区有两个插件就是专门干这两个事的:HappyPack、ParallelUglifyPlugin。

    使用 HappyPack

    const path = require('path')
    module.exports = {
    module: {
    rules: [
    {
    test: /\.js$/,
    //loader: 'babel-loader'
    loader: 'happypack/loader?id=babel'
    }
    ]
    },
    plugins: [
    new require('happypack')({
    id: 'babel',
    loaders: ['babel-loader']
    }),
    ],
    };

    使用 ParallelUglifyPlugin

    module.exports = {
    optimization: {
    minimizer: [
    new require('webpack-parallel-uglify-plugin')({
    // 配置项
    }),
    ]
    }
    }

    2、打包再打包

    使 windows 的时候,我们经常会看到一些 .dll 文件,dll 文件被称为动态链接库,里面包含了程序运行时的一些动态函数库,多个程序可以共用一个 dll 文件,可以减少程序运行时的物理内存。

    webpack 中我们也可以引入 dll 的概念,使用 DllPlugin 插件,将不经常变化的框架代码打包到一个 js 中,比如叫做 dll.js。在打包的过程中,如果检测到某个块已经在 dll.js 中就不会再打包。之前 DllPlugin 与 CommonsChunkPlugin 并能相互兼容,本是同根生相煎何太急。但是升级到 webpack4 之后,问题就迎刃而解了。

    使用 DllPlugin 的时候,要先写另外一个 webpack 配置文件,用来生成 dll 文件。

    //webpack.vue.dll.js
    const path = require('path')

    module.exports = {
    entry: {
    // 把 vue 相关模块的放到一个单独的动态链接库
    vue: ['vue', 'vue-router', 'vuex', 'element-ui']
    },
    output: {
    filename: '[name].dll.js', // 生成 vue.dll.js
    path: path.resolve (__dirname, 'dist'),
    library: '_dll_[name]'
    },
    plugins: [
    new require('webpack/lib/DllPlugin')({
    name: '_dll_[name]',
    //manifest.json 描述动态链接库包含了哪些内容
    path: path.join (__dirname, 'dist', '[name].manifest.json')
    }),
    ],
    };

    然后在之前的 webpack 配置中,引入 dll。

    const path = require('path')

    module.exports = {
    plugins: [
    // 只要引入 manifest.json 就能知道哪些模块再 dll 文件中,在打包过程会忽略这些模块
    new require('webpack/lib/DllReferencePlugin')({
    manifest: require('./dist/vue.manifest.json'),
    })
    ],
    devtool: 'source-map'
    };

    最后生成 html 文件的时候,一定要先引入 dll 文件。

    <html>
    <head>
    <meta charset="UTF-8">
    </head>
    <body>
    <div id="app"></div>
    <script src="./dist/vue.dll.js"></script>
    <script src="./dist/main.js"></script>
    </body>
    </html>

    3、你胖你先跑,部分代码预先运行

    前面的优化都是优化打包速度,或者减少重复模块的。这里有一种优化方式,能够减少代码量,并且减少客户端的运行时间。

    使用 Prepack,这是 facebook 开源的一款工具,能够运行你的代码中部分能够提前运行的代码,减少在线上真实运行的代码。

    官方的 demo 如下:

    //input
    (function () {
    function hello() { return 'hello'; }
    function world() { return 'world'; }
    global.s = hello () + ' ' + world ();
    })();

    //output
    s = "hello world";

    想在 webpack 中接入也比较简单,社区以及有了对应的插件 prepack-webpack-plugin,目前正式环境运用较少,还有些坑,可以继续观望。

    module.exports = {
    plugins: [
    new require('prepack-webpack-plugin')()
    ]
    };

    这里简单罗列了一些 webpack 的优化策略,但是有些优化策略还是还是要酌情考虑。比如多进程跑 loader,如果你项目比较小,开了之后可能变慢了,因为本来打包时间就比较短,用来 fork 子进程的时间,说不定都已经跑完了。记住 过早的优化就是万恶之源

    四、总结

    webpack4 带了很多新的特性,也大大加快的打包时间,并且减少了打包后的文件体积。期待 webpack5 的更多新特性,比如,以 html 或 css 为文件入口(鄙人认为 html 才是前端模块化的真正入口,浏览器的入口就是 html,浏览器在真正的亲爹,不和爹亲和谁亲),默认开启多进程打包,加入文件的长期缓存,更多的拓展零配置。

    同时也要感谢前端社区其它的优秀的打包工具,感谢 rollup,感谢 parcel。

    五、参考

    1. webpack 为什么这么难用?
    2. Webpack 4 进阶
    3. RIP CommonsChunkPlugin
    4. webpack 4: mode and optimization
    5. webpack 4 不完全迁移指北
    支持一下
    扫一扫,支持一下
    • 微信扫一扫
    • 支付宝扫一扫