chiwent / blog

个人博客,只在issue内更新
https://chiwent.github.io/blog
8 stars 0 forks source link

重学webpack #14

Open chiwent opened 5 years ago

chiwent commented 5 years ago

重学webpack

在开始接触前端工程化的时候,第一个上手的工具就是webpack。最开始使用webpack,只是用在html+css+js的场景下进行打包配置。在这种场景下,项目工程的复杂度较低,基本上参照着webpack官网,配置好相关参数就可以跑通项目了。后来接触vue后,开始使用vue-cli脚手架简单搭建主体项目环境,也就只是稍微改动一些插件、loader以及其他配置。但是在接触到vue ssr后,逐渐意识到在webpack配置对前端开发的重要性,所以也就有了这篇笔记,重学webpack。

1 初始化项目

1.1 安装依赖

首先初始化项目:

npm init
npm install webpack webpack-cli webpack-dev-server --save-dev

在执行完npm init后,项目目录下会多出一个package.json的文件,在这里可以看得到项目下安装的webpack插件,也修改npm命令。

补充说明:
npm install安装相关工具时,如果使用了--save,依赖会添加到package.json中的dependencies选项中;如果使用了--save-dev,依赖会添加到package.json中的devDependencies中,注意区分。

在上述提到的插件中,webpack-dev-server可以提供一个简单的本地服务,并且具有热重载的功能。

1.2 编辑配置

在项目路径下,新建一个名为webpack.config.js的配置文件,基本结构如下:

module.exports = {
    mode: 'development',
    entry: ['./src/index.js'],
    output: {
        path: path.join(__dirname, 'dist'),
        filename: 'bundle.js' //如果担心缓存影响热更新效果,可以使用[name].[hash:8].js
    },
    externals: {},
    devtool: 'eval',
    resolve: {
        extensions: ['.js', '.vue', '.json'],
        alias: {
            'vue$': 'vue/dist/vue.esm.js',
            '@': resolve('src'),
        }
    },
    module: {
        rules: []
    },
    plugins: [],
    optimization: {},
    devServer: {}
}

简要说明一下该配置文件的内容:

其中,开发环境推荐使用cheap-module-eval-source-map,生产环境推荐: cheap-module-source-map(这也是下版本 webpack 使用-d命令启动debug模式时的默认选项)

其他的具体配置参数可以参考:webpack官方文档 - devtool以及深入浅出的webpack构建工具---devTool中SourceMap模式详解(四)

external: {
  jquery: 'jQuery'
}

具体可参考:外部扩展(externals)
另外补充一点,如果需要全局使用jQuery,那么可以在webpack公共配置文件(webpack.base.conf.js)中设置,其中ProvidePlugin提供和暴露全局变量:

module.exports = {
  plugins: [
    new webpack.ProvidePlugin({
      $: 'jquery',
      jQuery: 'jquery'
    })
  ]
}

假如我们要单独打包类似jQuery这样的类库,可以在公共配置中加上设置如下:

module.exports = {
  plugins: [],
  optmization: {
    cacheGroup: {
      commons: {
        test: /jquery/,
        name: 'jquery',
        chunks: 'all'
      }
    }
  }
}

另外的一些概念:

关于webpack配置中的process.env

在node当中,process表示的是当前node的进程,process.env就包括了系统环境信息,但是process.env不包括NODE_ENV这个属性,她是用户自定义的变量,用来判断当前环境。
而我们在package.json配置中就有关于环境变量NODE_ENV的配置(当然你放在其他文件下也没问题),如:

{
  'script': {
    'dev': "NODE_ENV=development webpack --config webpack.dev.conf.js"
  }
}

这样,我们就将NODE_ENV绑定到了process.env上,并且按以上配置,我们只能在webpack.dev.conf.js中以及它所引入的脚本中访问到process.env.NODE_ENV,其他地方都不可访问到。

2 构建流程

webpack的构建流程是串行的,如下:

流程图可以参考这里:https://juejin.im/post/5c6b78cdf265da2da15db125

webpack打包的规则是,一个入口文件对应一个bundle,该bundle包括了入口文件模块和其他依赖模块,按需加载的模块或者需要单独加载的模块则是分开打包生成其他的bundle。在这些bundle中,有一个较为特殊,就是manifest.bundle.js,被称作webpackBootstrap,它是最先加载的,负责解析webpack生成的其他bundle。

3.具体配置

3.1 loader

3.1.1 babel

安装:

npm i babel-loader babel-core babel-preset-env babel/plugin-transform-runtime --save-dev

babel是JavaScript的转码器,可以现代js脚本内容转换为ES5、ES3。下面介绍几个常用的babel loader:

补充说明babel-stage-x对应的ECMA标准:

在webpack配置对应的babel loader时,也需要在项目下创建.babelrc文件,下面举出demo:

// vue-cli的配置:
{
    "presets": [
        "es2015",
        "stage-3"
    ],
    "plugins": [
        "syntax-dynamic-import"
    ]
}

// react配置可以参考:
{
  "presets": ["es2015", "stage-2", "react"],
  "plugins": [
    "react-hot-loader/babel",
    "transform-function-bind",
    "transform-class-properties",
    "transform-export-extensions",
    ],
    "env": {
      "backend": {
        "plugins": [
          [ "webpack-loaders",
            { "config": "./webpack.config.babel.js"
            , "verbose": true
            }
          ]
        ]
      }
    }
  }
}

webpack的配置举例如下:

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        user: {
          loader: 'babel-loader?cacheDirectory=true', // cacheDirectory用于缓存babel的编译结果,加快重新编译的速度
          options: {
            presets: ['@babel/preset-env'],
            plugins: [
              '@babel/plughin-transform-runtime',
              '@babel/plugin-transform-modules-commonjs'
            ]
          }
        }
      }
    ]
  }
}

3.1.2 样式相关的loader

常见的css预处理器有less、sass、stylus等,它们都有对应的loader,以less和css的配置为例:

rules: [
    {
        test: /\.less$/,
        exclude: /node_modules/,
        use: ['style-loader',
        {
            loader: 'css-loader',
            options: {
                importLoaders: 2
            }
        }, 'less-loader', 'postcss-loader']
    },
    {
        test: /\.scss$/,
        use: [
            "style-loader", // 将 JS 字符串生成为 style 节点
            "css-loader", // 将 CSS 转化成 CommonJS 模块
            "sass-loader" // 将 Sass 编译成 CSS,默认使用 Node Sass
        ]
    },
    {
        test: /\.css$/,
        use: ['style-loader', 'css-loader', 'postcss-loader']
    }
]

// postcss.config.js
module.exports = {
  plugins: [
    require('autoprefixer')
  ]
}

3.1.3 静态资源相关loader

如果我们要处理一些图片,就很大概率上会接触到file-loader和url-loader,二者有相似之处,二者在处理图片时可以将其打包到dist目录,接着会获取图片模块的地址,并将地址返回到引入模块的变量中。url-loader基本可以实现file-loader的功能,但是在url-loader是将图片转换为base64直接放入bundle.js下,而file-loader会将图片放到dist目录下。

rules: [
  {
    test: /\.(png|jpg|gif|jpeg)$/,
    use: {
        loader: 'url-loader',
        options: {
            name: '[name]_[hash].[ext]', // placeholder 占位符
            outputPath: 'images/', // 打包文件名
            limit: 204800, // 小于200kb则打包到js文件里,大于则使用file-loader的打包方式打包到imgages里
        },
    },
  },
  {
    test: /\.(eot|woff2?|ttf|svg)$/,
    use: {
        loader: 'url-loader',
        options: {
            name: '[name]-[hash:5].min.[ext]', // 和上面同理
            outputPath: 'fonts/',
            limit: 5000,
        }
    },
  }
]

3.2 plugin

3.2.1 常用插件

const path = require('path');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const HtmlWebpackPlugin = reuqire('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
  // 多入口文件打包
    entry: {
        index: './src/js/index.js',
        login: './src/js/login.js'
    },
    optimization: {
        minimizer: [new OptimizeCSSAssetsPlugin({})]
    },
    plugins: [
        new webpack.HotModuleReplacementPlugin(), // 开启全局的模块热替换(HMR)
        new webpack.NamedModulesPlugin(), // 当模块热替换(HMR)时在浏览器控制台输出对用户更友好的模块名字信息
        new webpack.DefinePlugin({
            'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')
        }),
        new CleanWebpackPlugin(),
        // 以下两个热重载插件
        new webpack.NamedModulesPlugin(),  
        new webpack.HotModuleReplacementPlugin(), 
        new ExtractTextPlugin('css/[name].[hash].css'),
        // 多页打包
        new HtmlWebpackPlugin({
            filename: 'index.html',
            template: './src/index.html',
            chunks: ['index']
        }),
        new HtmlWebpackPlugin({
            filename: 'login.html',
            template: './src/login.html',
            chunks: ['login']
        }),
        new MiniCssExtractPlugin({
            // Options similar to the same options in webpackOptions.output
            // both options are optional
            filename: devMode ? '[name].css' : '[name].[hash].css',
            chunkFilename: devMode ? '[id].css' : '[id].[hash].css',
        }),
        new CSSSplitWebpackPlugin({
            size: 4000,
            filename: '[name]-[part].[ext]'
        })
    ],
    module: {
        noParse:/jquery|lodash/, // 对于没有使用require和import,而是通过CDN引用的模块,不需要使用webpack进行解析依赖
        rules: [
            {
                test: /\.(sa|sc|c)ss$/,
                use: [
                devMode ? 'style-loader' : MiniCssExtractPlugin.loader,
                'css-loader',
                'postcss-loader',
                'sass-loader',
                ],
            }
        ]
    },
    output: {
        filename: 'js/[name].js',
        path: path.resolve(__dirname, 'dist')
    }
}

4 优化

4.1 巧用resolve

可以通过resolve.modules:[path.resolve(__dirname, 'node_modules')]来定位第三方模块,默认是node_modules;设置resolve.alias来生成一个快速引用符号指向目标目录;在使用loader的时候,可以通过test、exclude、include、正则来缩小搜索范围

另外,使用alias可以加快webpack查找模块的速度。

resolve: {
    extensions: ['.js', '.css', '.vue'],
    alias: {
        'vue$': 'vue/dist/vue.esm.js',
        '@': resolve('src'),
    },
    mainFields: ['style', 'main'],
    modules:[path.resolve(__dirname, node_modules), my_modules], //只能引入node_modules和mu_modules下的包
    mianFiles:['index.js','main.js']
}

4.2 抽离公共代码

如果在两个组件中都引用了共同的组件,那么我们可以将公共组件代码抽离出来

const path = require('path');
module.export = {
    entry: {
        index: './src/index',
        login: './src/login'
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js'
    },
    optimization: {
        splitChunks: {
            cacheGroups: {
                common: {
                    chunks: 'initial',
                    minChunks: 2, // 使用两个同时引用的模块时才抽离出来
                    minSize: 0  // 限制大小,小于指定值就不抽离
                }
            }
        }
    }
}

最后打包的结果中会生成index.jslogin.js

或者是:

const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
plugins:[
    new CommonsChunkPlugin({
        chunks:['chunkA','chunkB'], //从哪些chunk中提取
        name:'common',  // 提取出的公共部分生成一个新的chunk,文件为common.js
    })
]

4.3 使用DllPlugin减少基础模块的编译次数

DllPlugin是动态链接库插件,原理是将网页依赖的基础模块抽离出来打包到dll文件中,当需要导入的模块存在于某个dll文件中,该模块不再被打包,而是通过dll获取。

// webpack.dll.config.js
const path = require('path');
const webpack = require('webpack');
module.exports ={
  entry: {
    vendor: ['react', 'redux', 'react-router'],
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].dll.js',
    library: '[name]_[hash]'    //提供全局的变量
  },
  plugins: [
    new webpack.DllPlugin({
      path: path.join(__dirname, 'dist', '[name].manifest.json'),
      name: '[name]_[hash]',
    }),
  ],
};

// package.json
"scripts": {
    "dev": "webpack-dev-server",
    "build": "webpack",
    "dll":"webpack --config webpack.dll.config.js"
},

// webpack.base.conf.js
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');
module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname,'dist'),
    filename: 'index_bundle.js',
  },
  plugins: [
    new webpack.DllReferencePlugin({
      context: path.join(__dirname),
      manifest:path.resolve(__dirname,'dist','vendor.manifest.json')
    }),
    new HtmlWebpackPlugin(),
    new AddAssetHtmlPlugin({
      filepath: path.resolve(__dirname,'dist','vendor.manifest.json')
    }),
  ],
};

作者:梦想攻城狮
链接:https://juejin.im/post/5bd128b56fb9a05ce1729333
来源:掘金

打包后生成:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Webpack App</title>
  </head>
  <body>
    <script type="text/javascript" src="vendor-manifest.json"></script>
    <script type="text/javascript" src="index_bundle.js"></script>
  </body>
</html>

4.4 使用IgnorePlugin忽略某些模块打包

IgnorePlugin可以让webpack不打包指定的模块:

module.exports = {
    ...
    plugins: [
        new webpack.IgnorePlugin(/moment$/)
    ]
    ...
}

4.5 生产环境中将提前构建的包同步到dist

const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
    plugins: [
        new CopyWebpackPlugin([
            {
            from: path.resolve(__dirname, '../static'),
            to: config.build.assetsSubDirectory,
            ignore: ['.*']
            }
        ])
    ]
}

4.6 代码压缩

webpack内置的uglifyjs插件可以实现js代码压缩:

const UglifyJSPlugin = require('webpack/lib/optimize/UglifyJsPlugin');
//...
plugins: [
    new UglifyJSPlugin({
        compress: {
            warnings: false,  //删除无用代码时不输出警告
            drop_console: true,  //删除所有console语句,可以兼容IE
            collapse_vars: true,  //内嵌已定义但只使用一次的变量
            reduce_vars: true,  //提取使用多次但没定义的静态值到变量
        },
        output: {
            beautify: false, //最紧凑的输出,不保留空格和制表符
            comments: false, //删除所有注释
        }
    })
]

作者:superMaryyy
链接:https://juejin.im/post/5b652b036fb9a04fa01d616b
来源:掘金

另外,使用css-loader?minimize不仅可以删除样式文件的空格,还可以语义化地压缩css代码(rgb转色彩名)

4.7 使用静态资源CDN

参考:

const ExtractTextPlugin = require('extract-text-webpack-plugin');
const {WebPlugin} = require('web-webpack-plugin');
//...
output:{
 filename: '[name]_[chunkhash:8].js',
 path: path.resolve(__dirname, 'dist'),
 publicPatch: '//js.cdn.com/id/', //指定存放JS文件的CDN地址
},
module:{
 rules:[{
     test: /\.css/,
     use: ExtractTextPlugin.extract({
         use: ['css-loader?minimize'],
         publicPatch: '//img.cdn.com/id/', //指定css文件中导入的图片等资源存放的cdn地址
     }),
 },{
    test: /\.png/,
    use: ['file-loader?name=[name]_[hash:8].[ext]'], //为输出的PNG文件名加上Hash值 
 }]
},
plugins:[
  new WebPlugin({
     template: './template.html',
     filename: 'index.html',
     stylePublicPath: '//css.cdn.com/id/', //指定存放CSS文件的CDN地址
  }),
 new ExtractTextPlugin({
     filename:`[name]_[contenthash:8].css`, //为输出的CSS文件加上Hash
 })
]

作者:superMaryyy
链接:https://juejin.im/post/5b652b036fb9a04fa01d616b
来源:掘金

4.8 多进程打包

可以使用HappyPack这个插件进行webpack的多进程打包,以下放出网上找来的一段相关配置,来自:加速 Webpack,文章内也有具体的配置说明,详情可点击链接访问

const path = require('path');
    const  ExtractTextPlugin =  require('extract-text-webpack-plugin');
    const  HappyPack = require('happypack');
    module.exports = { 
        module: { 
            rules: [ 
                {    test: /\.js$/, 
                    // 把对 .js 文件的处理转交给 id 为 babel 的 HappyPack 实例 
                    use:['happypack/loader?id=babel'], 
                    // 排除 node_modules 目录下的文件,node_modules目录下的文件都是采用的 ES5 语法,没必要再通过 Babel 去转换 
                    exclude: path.resolve(__dirname, 'node_modules'),
                 }, 
                { 
                    // 把对 .css 文件的处理转交给 id 为 css 的 HappyPack 实例
                     test: /\.css$/, 
                     use:ExtractTextPlugin.extract({ 
                        use: ['happypack/loader?id=css'],
             }), 
        }, 
] },
    plugins: [ 
        new HappyPack({ 
            // 用唯一的标识符 id 来代表当前的HappyPack 是用来处理一类特定的文件 
        id: 'babel', 
            // 如何处理 .js 文件,用法和 Loader配置中一样 
        loaders: ['babel-loader?cacheDirectory'],
     }),
        new HappyPack({ 
                id: 'css', 
                    // 如何处理 .css 文件,用法和Loader 配置中一样 
                loaders: ['css-loader'], }), 
                new ExtractTextPlugin({ 
                    filename: `[name].css`, 
            }), 
        ],
    };

4.9 runtime和manifest

在webpack项目构建中,有三种基本的代码类型:

在单页应用中,客户端发起网络请求后,从服务端返回的文件主要是入口的html文件和一系列bundle文件,然后交由浏览器解析处理。经由webpack打包过后的项目,在不同的打包策略模式下,代码的执行逻辑是不同的,但是它们都需要通过webpack来进行管理模块间的交互。而runtime和manifest就是指在浏览器运行时,webpack通过连接模块化应用程序的所有代码。runtime包括了在模块交互时,连接模块所需的加载和解析逻辑,包括浏览器中已加载模块的连接、懒加载模块的执行逻辑。

为了更加清晰地理解manifest,我们简要复述一遍webpack的工作流程:

当编译器(compiler)开始执行、解析和映射应用程序时,它会保留所有模块的详细要点。这个数据集合称为 "Manifest",当完成打包并发送到浏览器时,会在运行时通过 Manifest 来解析和加载模块。无论你选择哪种模块语法,那些 import 或 require 语句现在都已经转换为 __webpack_require__ 方法,此方法指向模块标识符(module identifier)。通过使用 manifest 中的数据,runtime 将能够查询模块标识符,检索出背后对应的模块。

-- webpack官方文档

当webpack写入bundle时,它会维护一个manifest,你可以在项目中生成的bundle找到它,它内部描述了webpack应该加载的文件。如果文件的hash值改变,manifest也会改变,然后也跟着一些分离出来的代码共同打包。这显然不是我们想要的,所以我们需要提取出manifest:

new webpack.optimize.CommonsChunkPlugin({
    name: 'manifest' // 用于提取manifest
})

4.10 缓存优化

浏览器加载js文件时,如果文件名和浏览器本地缓存一致,那么就不会向后台发送请求。假如我们的业务代码中做了修改,但是webpack打包出来的文件名对应的hash值不变,那么就不会用上新代码。另外,在前端项目中一般是只有业务代码才会频繁变化,而第三方依赖是很少变化的,所以我们希望浏览器不用再重复地向服务端发送请求,这就要求我们进行代码分块打包,将一些第三方依赖集中整合进单个bundle下。所以,我们可以借助webpack.optimize.CommonsChunkPlugin将manifest、第三方依赖、业务代码分别独立打包

4.11 打包分析

可以使用webpack-bundle-analyzer对webpack打包结果进行分析,支持可视化的。

module.exports = {
  entry: './index.js',
  output: {
    filename: 'bundle[chunkhash:8].js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: function (module, count) {
        // 将node_modules的依赖集中打包,注意主次关系,依赖要放在manifest前面
        return (
          module.resource &&
          /\.js$/.test(module.resource) &&
          module.resource.indexOf(
            path.join(__dirname, '../node_modules')
          ) === 0
        )
      }
    }),
    // 抽出mainfest
    new webpack.optimize.CommonsChunkPlugin({
          name: 'manifest',
          chunks: ['vendor']
    })
  ]
}




参考:




扩展阅读:

chiwent commented 4 years ago

关于在工程中混用ES6模块 和 CommonJS规范

在工程中如果代码混用了这两种规范,可能就遇到构建失败的情况。实际上,这样的方式也是不提倡的。可以参考下面的讨论:

https://github.com/webpack/webpack/issues/4039#issuecomment-281136701

事实上,webpack内部就维护了包管理工具,支持ES Module、Commonjs、AMD 模块化规范。
如果需要处理这样的冲突问题,可以对babel的配置文件进行修改,将modules的配置选项设置为commonjs(js最终编译得到CommonJS规范)或者umd(js最终编译得到umd规范)以支持这两种模块规范的混用。