webpack入门练习

记录一些webpack相关的入门练习。file-loader、url-loader、样式loader、html loader、html-webpack-plugin、devtool-sourceMap、开发中 Server(devServer)、devtool-sourceMap、开启 HMR 模块热替换特性、@babel/polyfill、@babel/plugin-transform-runtime

Posted by ddxg on September 19, 2019

webpack入门练习

在工作中经常接触webpack相关的东西,像Vue工程打包、内部封装的库和自己封装js功能库的时候都会用到webpack打包。 但是,我对webpack的接触是非常零散的,连皮毛都不及,经常不知道要将相关的配置写到webpack配置文件的哪里,对webpack太陌生了。

webpack loader

loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。

file-loader

处理静态资源模块,原理是把打包入口中识别出的资源模块,移动到输出目录,并且返回一个地址名称。 一般用在处理图片、svg、txt、excel等资源的时候。

{
    // 处理静态资源
    // 正则匹配需要用这个loader的文件
    test: /\.(png|jp?g|gif)$/,
    use: [
        {
            // 指定要使用的是那个loader
            loader: 'file-loader',
            options: {
                // 配置文件输出的名字
                name: '[name].[ext]',
                // 指定在输出的路径,相对以output.path
                outputPath: 'img/'
            }
        }
    ]
}
// 引入图片
const avatar = require('../img/avatar.jpg');
console.log('avatar', avatar); // avatar avatar.jpg

url-loader

url-loader 功能类似于 file-loader,但是在文件大小(单位 byte)低于指定的限制时,可以返回一个 DataURL。 就是在指定大小之内的图片会转换成base64的形式。

{
    // 使用url-loader
    test: /\.(png|jp?g|gif)$/,
    use: [
        {
            loader: 'url-loader',
            options: {
                // 图片大小低于1024B就转为base64
                limit: 1024,
                // 指定png格式的文件才转
                mimetype: 'image/png',
                // 配置文件输出的名字
                name: '[name].[ext]',
                // 指定在输出的路径,相对以output.path
                outputPath: 'img/'
            }
        }
    ]
}

样式loader

  • css-loader:css-loader 解释(interpret) @import 和 url() ,会 import/require() 后再解析(resolve)它们。引用资源的合适 loader 是 file-loader和 url-loader。
  • style-loader:会把css-loader生成的内容,以<style> 标签添加到htmlhead中。
  • postcss-loader:postcss 一种对css编译的工具,类似babel对js的处理,使用下一代css语法、自动补全浏览器前缀、自动把px代为转换成rem和css 代码压缩等等
  • stylus-loader:处理stylus预处理器

这里我是用了postCss的Autoprefixer自动补全的插件和压缩插件,需要创建 postcss.config.js 文件:

module.exports = {
    plugins: [
        require('autoprefixer'), // 自动补全兼容性前缀
        require('cssnano') // 压缩css代码
    ]
}

第一版:

{
    // 处理样式,使用了stylus的预处理器
    test: /\.styl$/,
    /*
    * 引入的顺序是有区别的,use数组中的顺序是从后往前的,执行的顺序是倒过来的。
    * postcss-loader:Use it after css-loader and style-loader, 
    * but before other preprocessor loaders like e.g sass|less|stylus-loader, if you use any.
    * */

    /**
     * 这种打包方式是将css引入js中的,是通过style-loader的作用将css插入到head中的,这样子会有一个时间差,会闪现一下无样式的状态,体验非常不好。
     */
    use: [
        'style-loader', // 会把css-loader生成的内容,以<style> 标签添加到html的head中
        'css-loader', // 解释(interpret) @import 和 url()
        'postcss-loader',
        'stylus-loader'
    ]
    
}

后来使用 extract-loaderfile-loader 对单独抽出了css文件,然后在html文件中引入

{
    // 处理样式,使用了stylus的预处理器
    test: /\.styl$/,
    /*
    * 分离css
    * */
    use: [
        {
            loader: 'file-loader',
            options: {
                name: '[name].css'
            }
        },
        'extract-loader',
        'css-loader', // 解释(interpret) @import 和 url()
        'postcss-loader',
        'stylus-loader'
    ]
}

分离css也可以使用 mini-css-extract-plugin插件

// loader
use: [{
    loader: MiniCssExtractPlugin.loader,
    options: {
        hmr: process.env.NODE_ENV === 'development'
    }
}]

// 插件配置
plugins: [
    /*
    * extract-text-webpack-plugin 目前不支持webpack4.x的版本,使用beta版本可以work,但是不支持生成hash文件名
    * 改用mini-css-extract-plugin
    * */
    // 传入extract-text-webpack-plugin的选项
    // new ExtractTextPlugin('index.css')

    new MiniCssExtractPlugin({
        filename: 'index.css',
        chunkFilename: "[id].css"
    })
]

参考:

html loader

使用 html-loader 导出html文件

js代码中引入htmlrequire("html-loader!../html/index.html");

{
    test: /\.html$/,
    use: [
        {
            loader: 'file-loader',
            options: {
                name: '[name].[ext]'
            }
        },
        'extract-loader',
        {
        loader: 'html-loader',
        options: {
            minimize: true,
            removeComments: false,
            collapseWhitespace: false
        }
    }],
}

做了上面这么多我才发现webpack跟gulp的差别还是挺大的。loader一定是需要在js文件中引入才会触发功能的, 像上面的,为了导出html代码,然后在js文件中 require("../html/index.html"),这让我感到很奇怪,在js中引入html文件???

总结代码:

const path = require('path');

module.exports = {
    entry: './src/js/index.js',
    output: {
        path: path.resolve(__dirname, 'dist/js'),
        filename: 'index.js'
    },
    module: {
        rules: [
            {
                // 使用url-loader
                test: /\.(png|jp?g|gif)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            // 图片大小低于1024B就转为base64
                            limit: 1024,
                            // 指定png格式的文件才转
                            mimetype: 'image/png',
                            // 配置文件输出的名字
                            name: '[name].[ext]',
                            // 指定在输出的路径,相对以output.path
                            outputPath: '../img/'
                        }
                    }
                ]
            },
            {
                // 处理样式,使用了stylus的预处理器
                test: /\.styl$/,
                /*
                * 引入的顺序是有区别的,use数组中的顺序是从后往前的,执行的顺序是倒过来的。
                * postcss-loader:Use it after css-loader and style-loader, 
                * but before other preprocessor loaders like e.g sass|less|stylus-loader, if you use any.
                * */

                /**
                 * 这种打包方式是将css引入js中的,是通过style-loader的作用将css插入到head中的,
                 * 这样子会有一个时间差,会闪现一下无样式的状态,体验非常不好。
                 */
                /*use: [
                    'style-loader', // 会把css-loader生成的内容,以<style> 标签添加到html的head中
                    'css-loader', // 解释(interpret) @import 和 url()
                    'postcss-loader',
                    'stylus-loader'
                ]*/

                /*
                * 分离css
                * */
                use: [
                    {
                        loader: 'file-loader',
                        options: {
                            name: '[name].css',
                            outputPath: '../css/'
                        }
                    },
                    'extract-loader',
                    'css-loader', // 解释(interpret) @import 和 url()
                    'postcss-loader',
                    'stylus-loader'
                ]
            },
            {
                test: /\.html$/,
                use: [
                    {
                        loader: 'file-loader',
                        options: {
                            name: '[name].[ext]',
                            outputPath: '../'
                        }
                    },
                    'extract-loader',
                    {
                        loader: 'html-loader',
                        options: {
                            minimize: true,
                            removeComments: false,
                            collapseWhitespace: false
                        }
                    }
                ],
            }
        ]
    }
}

这个配置会将src中的html、js、css和图片都打包到dist目录。都是用loader实现的。感觉有点别扭。。。 img

我觉得css文件和html文件都不应该依赖js的引入来生成,下面是用 插件 的形式来生成html文件,并且可以选择 以内联的方式引入js和css。

使用的是 html-webpack-plugin 插件 和 mini-css-extract-plugin 结合

html-webpack-plugin 分离html

结合 mini-css-extract-plugin,可以达到的效果是会将js文件和css文件自动添加到html文件中,不需要手动引入。 里面还介绍了很多使用的功能。

尝试使用 html-webpack-plugin 的插件html-webpack-inline-source-plugin 进行内联js和css的操作但是编译报错了,好像现在还没有相关的解决方法。

最终生成的目录结构跟上面的一样,只是不需要手动引入使用的js和css了。

完整的配置如下:

const path = require('path');
// const ExtractTextPlugin = require("extract-text-webpack-plugin");
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); // 抽离css的插件
const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin'); // js和css内联进html的插件

module.exports = {
    entry: './src/js/index.js',
    output: {
        path: path.resolve(__dirname, 'dist/js'),
        filename: 'index.js'
    },
    module: {
        rules: [
            {
                // 使用url-loader
                test: /\.(png|jp?g|gif)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            // 图片大小低于1024B就转为base64
                            limit: 1024,
                            // 指定png格式的文件才转
                            mimetype: 'image/png',
                            // 配置文件输出的名字
                            name: '[name].[ext]',
                            // 指定在输出的路径,相对以output.path
                            outputPath: '../img/'
                        }
                    }
                ]
            },
            {
                // 处理样式,使用了stylus的预处理器
                test: /\.styl$/,
                /*
                * 分离css
                * */
                use: [
                    {
                        loader: MiniCssExtractPlugin.loader,
                        options: {
                            hmr: process.env.NODE_ENV === 'development'
                        }
                    },
                    'css-loader', // 解释(interpret) @import 和 url()
                    'postcss-loader',
                    'stylus-loader'
                ]
            },
        ]
    },
    plugins: [
        new MiniCssExtractPlugin({
            filename: '../css/index.css',
            chunkFilename: "[id].css"
        }),
        // 使用插件分离html
        new HtmlWebpackPlugin({
            filename: '../index.html',
            template: 'src/html/index.html',
            inlineSource: '.(js|css)$'
        }),
        // 将js和css内联进html的插件,编译有问题,暂时无法解决
        // new HtmlWebpackInlineSourcePlugin(HtmlWebpackPlugin)
    ]
}

devtool-sourceMap

主要是用来控制source-map的,我觉的主要的用处是可以看到具体出错的位置,编译后的代码基本都是混淆和压缩过后的, 浏览器的报错信息指向的是压缩后的代码。即使找到位置也很难判断是源码中的哪行代码出问题了,有了source-map就可 以定位到源码中的位置。但是这会生成额外的source-map代码,增大打包后的资源大小。

webpack 配置中的 mode: 'development' 时默认开启source-map功能,可以使用 devtool 字段来指定使用 哪种格式的source-map。官网上面也有解释各种格式的差异。

你可以直接使用 SourceMapDevToolPlugin/EvalSourceMapDevToolPlugin 来替代使用 devtool 选项,插件与 devtool 不可同时设置。

  • 开发环境适合:cheap-module-eval-source-map :只映射行数。
  • 生产环境适合:不配置 或者 cheap-module-source-map

生产环境我是觉得不需要配置source-map比较好。在浏览器中source-map文件时浏览器解析之后再展示到控制台的,它是先展示原始 的错误栈信息,解析完source-map之后再展示解析后的错误栈信息。页面中通过错误监听拿到的错误栈信息是原始的错误信息。所以 页面使用source-map对于错误上报来说是没有用的。我觉得在 错误信息入库前 或者 在错误信息出库展示 前使用source-map解析 转换一下比较合理一些,这样子的话就需要在服务器保存一份最新的source-map文件。

开发中 Server(devServer)

webpack-dev-server 这个插件是非常使用的,可以启动一个服务器,修改代码保存后可以自动刷新,不需要手动在build一下。 可以设置devServer.hot:true来启动webpack的模块热替换功能,就是修改代码之后,在页面不刷新的状态下就展示新的效果, 适合调试css和js的时候。

启动devServer

启动一个服务很简单。npm install -D webpack-dev-server,然后在 webpack.config.js 中配置devServer,在 package.json 中添加 webpack-dev-server 命令。

devServer: {
    contentBase: path.join(__dirname, "dist"), // 告诉服务器从哪里提供内容。一般就是你的html文件的目录
    compress: true, // 一切服务都启用gzip 压缩
    port: 9000 // 配置端口
  },

改动文件保存之后,自动执行build命令,页面就会自动刷新了。非常方便。

开启 HMR 模块热替换特性

可以设置 devServer.hot:true 来启动webpack的模块热替换功能,就是修改代码之后,在页面不刷新的状态下就展示新的效果,适合调试css和js的时候。

需要在 webpack.config.js 中添加一个插件 new webpack.HotModuleReplacementPlugin(),这样调样式就更方便啦。

这种功能是得益我们使用的 插件 或者 loader 中内部实现了HMR功能的, 一般css相关的loader都实现了HMR功能,像 style-loader 就实现了,所以可以直接使用。

有个问题是,如果css是外部引入的话,不是用 style-loader 直接写在 <style> 中的话,是实现不了HMR的。

还有一个问题是js,像你自己写的js代码可能是不会考虑HMR的,这时就需要你自己进行处理才能实现相应的效果了。

import counter from "./counter";
import number from "./number";

counter();
number();

if (module.hot) {
  module.hot.accept("./number", () => {
      // 需要在这个回调里面手动执行一下js代码
    document.body.removeChild(document.getElementById("number"));
    number();
  });
}

babel

babel 主要是处理js代码兼容性的问题,将高级的js语法转为古董浏览器能够识别的代码。

进入官网: https://babeljs.io -> setup -> 选择webpack

安装: npm install --save-dev babel-loader @babel/core

webpack.config.js 中 增加一个loader

{
    // 使用babel 
    test: /\.js$/, 
    exclude: /node_modules/, // 忽略依赖中的文件
    use: [
        {
            loader: "babel-loader",
            options: {
                presets: ["@babel/preset-env"]
            }
        }
    ]
}

这样下来只是将基本的语法转换了(let、const、箭头函数等改了),但是没有做方法的兼容处理,比如 Promise\map 等都没有变化。

安装 npm install --save @babel/polyfill,使用 useBuiltIns: 'usage' 按需引入的话, 就不需要再页面中 import/require @babel/polyfill 了。不使用按需引入的话,打包后的js体积将会非常大。 useBuiltIns: 'usage' 的行为类似 babel-transform-runtime,不会造成全局污染

Polyfill提供的就是一个这样功能的补充,实现了Array、Object等上的新方法, 实现了Promise、Symbol这样的新Class等。到这里应该能明白了, 为什么安装 @babel/polyfill 没有 -dev ,因为就算代码发布后, 编译后的代码依然会依赖这些新特性来实现功能。

{
    "presets": [
        ["@babel/preset-env", {
            "useBuiltIns": "usage", // 按需引入
            "corejs": 2
        }]
    ]
}

@babel/preset-env 的配置可以查看:babel-preset-env

如何确定babel是否工作了呢?

我打包完之后发现打包出来的js文件只增大了一点点,我都不太敢相信babel是不是真的将兼容代码打包进去了,我也没有什么特别准确的方法,我对比了一下使用babel前后代码的对比,应该有下面图片中类似的代码就是打包成功改了。 img

@babel/polyfill 存在一些问题:

  • 打包体积太大:默认是全部引入,造成一些没有用到的代码也引入了。
  • 污染全局环境: 那么像Promise这样的新类就是挂载在全局上的,这样就会污染了全局命名空间。如果是开发自己的项目还是没啥为题的。但是如果你是开发工具库给别人使用的话就有问题了,你就会污染别人的全局环境。

不过还好,babel可以设置按需引入,useBuiltIns: 'usage'

@babel/plugin-transform-runtime

由于 @babel/polyfill 会存在上面的问题,在开发工具库的话就可以使用 @babel/plugin-transform-runtime

@babel/plugin-transform-runtime 的好处:

  • 避免多次编译出helper函数
  • 不会污染全局作用域

这个转换器的另一个目的是为您的代码创建一个沙箱环境。为core-js这里内建的实例提供假名,你可以无缝的使用这些新特性,而不需要使用require polyfill。

如何使用 就直接查看官方文档就行了,babel-plugin-transform-runtime

不过,babel-runtime有个缺点,它不模拟实例方法,即内置对象原型上的方法,所以类似Array.prototype.find,你通过babel-runtime是无法使用的。 “foobar”.includes(“foo”),这样的实例方法仍然是不能正常执行的,因为他在挂载在String.prototype上的,

所以什么时候用 polyfill 什么时候用 runtime 就需要自己根据项目用途来判断了

自定义Loader

可以查看 官方文档

loader处理代码:

/**
 * content: 一般是源码
 * map 和 meta 为可选项
 * see: https://www.webpackjs.com/api/loaders/
 */
module.exports = function(content, map, meta) {
    // 函数的 this 上下文将由 webpack 填充,有很多api
    // 如果这个 loader 配置了 options 对象的话,this.query 就指向这个 option 对象
    console.log('query', this.query);
    
    // return content + `console.log('${this.query.name}')`;

    // 除了直接return之外,也可以使用this.callback来返回多个结果
    const ret = content + `console.log('${this.query.name}')`;
    this.callback(null, ret);
};

webpack.config.js 中添加自定义loader

{
    // 使用babel 
    test: /\.js$/, 
    exclude: /node_modules/, // 忽略依赖中的文件
    use: [
        {
            loader: "babel-loader",
            // 配置写在独立的.babelrc
            // options: {
            //     presets: ["@babel/preset-env"]
            // }
        },
        {
            // 需要使用绝对路径
            loader: path.resolve(__dirname, './src/loader/myLoader.js'),
            options: {
                name: '自定义loader2'
            }
        }
    ]
}

获取复杂参数的话可以使用官方推荐的 loader-utils

const loaderUtils = require('loader-utils');

// 获取options
const options = loaderUtils.getOptions(this);

如果存在异步操作的话,需要使用 this.data 处理。

module.exports = function(content, map, meta) {
    const options = loaderUtils.getOptions(this);
    // 异步操作,告诉 loader-runner 这个 loader 将会异步地回调。返回 this.callback。
    const callback = this.async();
    setTimeout(function() {
        const ret = content + `console.log('${options.name}')`;
        callback(null, ret)
    }, 2000);
};

loader中自定义path路径太长优化,在 webpack.config.js 中配置 resolveLoader.modules

resolveLoader: {
    modules: ['node_modules', './src/loader']
}

// loader: path.resolve(__dirname, './src/loader/myLoader.js'),
// 配置resolveLoader.modules之后就不用再写这么长的path路径了
loader: 'myLoader',

参考: