欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Webpack入门关键知识点(代码实例讲解)

程序员文章站 2022-04-04 09:51:54
关于Webpack 总所周知,Webpack 是一个前端资源加载(打包)工具。它将根据模块的依赖关系进行静态分析,然后将这些模块按照指定的规则生成对应的静态资源。其它更多的不在这...

关于Webpack

总所周知,Webpack 是一个前端资源加载(打包)工具。它将根据模块的依赖关系进行静态分析,然后将这些模块按照指定的规则生成对应的静态资源。其它更多的不在这里赘述,如想了解更多详情,请移步至 Webpack官网 查看。下面来说说webpack的那些重要的知识点。

概念

entry

entry是用来指明入口js文件的配置项,入口文件可以是一个或多个。

// 单文件入口
module.exports = {
  entry: './path/to/my/entry/file.js'
};
// 多文件入口
module.exports = {
  entry: {
      app: './path/to/my/app/file.js',
      vendor: './path/to/my/vendor/file.js',
  }
};

output

output是输出整合后的文件的配置项。可通过此项的path和filename属性来指定输出文件的路径和文件名。

const path = require('path');
module.exports = {
  entry: './path/to/my/entry/file.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'my-webpack-file.bundle.js'
  }
};

module

模块化编程是一个很重要的编程模式,无论在何领域,模块已经成为了一个应用最根本和必不可少的构造品,就如同一个机器的零件一样。其是webpack配置的核心,里面有我们最常用到同时也是最主要的loaders,这玩意儿可为我们做不少省心的事儿呢。

我们可以通过配置里面的rules属性来为所有需要优化和打包的文件进行相关的操作。当文件类型不同或者需要用到不同loader时,我们便可在rules里另加一条。注意到,每一条rule都可配置若干属性,这取决于文件配置的具体需求。通常,我们以test的正则表达式对文件进行匹配,从而可以使用use属性对该文件进行相关loader的处理,必要时还可以通过exclude属性来排除某些文件。

请注意,loaders属性已被废除,转由use属性代替。

module: {
    rules: [
      {
        // 匹配所有css文件
        test: /\.css$/,
        // use的每一个子项代表每种不同的loader
        use: [
          { loader: 'style-loader' },
          {
            loader: 'css-loader',
            // loader 选项
            options: {
              modules: true
            }
          }
        ],
        // 排除node_modules目录下的css文件
        exclude: /node_modules/
      }
    ]
  }

plugins

plugins顾名思义是插件的意思,此项用来定制webpack进行构建的各种方法,与我们平时所用的插件(组件)差不多。插件可以是webpack自带的,也可以使用外部下载或者自己开发的。

const autoprefixer =  require('autoprefixer');
module.exports = {
    plugins: [
        // css前缀预处理插件
        autoprefixer,
        // webpack自带的js压缩插件
        new webpack.optimize.UglifyJsPlugin({
            compress: {
              warnings: false,
              drop_console: false,
            }
        }),
    ]
}

配置

package.json

通过npm init命令初始化某个项目后得到的文件。一般配置采用默认参数,理论上来说并无大碍,但实际开发中可能会踩到不少坑。

babel

随着ES6逐渐普及,为大多数人采纳和使用,因此在开发过程中,ES转码插件的使用是必不可少的。我们都知道,可以在Webpack上通过安装相关的依赖项来实现快速的项目代码的转码和打包,但实际情况并非只是简单的安装。

之前我们只需安装 babel-core 和 babel-loader 就足以实现转码了,但目前babel并不推荐这么做,而建议安装环境预设更加全面的 babel-preset-env。

首先安装依赖:

$ npm install --save-dev babel-core babel-loader babel-preset-env

然后在config里添加babel配置:

// webpack.config.js
module: {
        rules: [
            {
                test: /\.js$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            }
        ]
    }

然后在babelrc里添加presets:

//.babelrc
{
  "presets": ["env"]
}
scripts

在配置中加入scripts以便执行相关的命令(当然也可以是自定义的),这就可以解决有时候明明输入的是正确的命令却提示不存在的尴尬问题了。

下面是一个常见的配置参考:

// package.json
"scripts": {
    // 代码测试命令
    "test": "echo \"Error: no test specified\" && exit 1",
    // 项目构建命令
    "build": "webpack --config webpack.production.config.js",
    // 项目开发命令
    "dev": "set NODE_ENV=development && node dev-server.js"
  }

配置完成后,在命令行输入 npm run + 命令 试试执行效果吧!

config.js

在实际项目开发过程中,建议最好是写三份独立的配置文件。那么你可能会问,为什么要三份呢?原因很简单,这样做可以细化项目的配置管理,使之能够满足不同项目环境下的需求。

我们不妨先假设使用单一的配置文件。在这种条件下,由于Webpack 的默认配置文件只有一个,即 webpack.config.js,那么问题来了,开发期和部署前应该使用同一份 Webpack 配置吗?答案当然是否定的,既然 webpack.config.js 是一个 JS 文件,我们当然可以在文件里写 JavaScript 业务逻辑,通过读取环境变量 NODE_ENV 来判断当前是在开发(dev)时还是最终的生产环境(production)。然而很多人习惯把这两者的配置都混写在根目录下的 config配置文件中,通过很多零散的 if... else... 语句来“临时”决定某个 plugin 或者某个 loader 的配置项。随着 loaders 和 plugins 不断增多,久而久之,webpack.config.js 便也会变得原来越冗长,代码的可读性和可维护性也便大大下降。

为此,我们通过创建三个配置文件 webpack.base.config.js、webpack.dev.config.js、webpack.prod.config.js来分别用以实现项目打包的基础模式、开发模式以及生产模式的应用。

开发环境与生产环境的区别

开发环境

NODE_ENV 为 development

启用模块热更新(hot module replacement)

额外的 webpack-dev-server 配置项,API Proxy 配置项

输出 Sourcemap

生产环境

NODE_ENV 为 production

ReactjQuery 等常用库设置为 external,直接采用 CDN 线上的版本

样式源文件(如 css、less、scss 等)需要通过 ExtractTextPlugin 独立抽取成 css 文件

启用 post-css

启用 optimize-minimize(如 uglify 等)

中大型的商业网站生产环境下,一般不允许有 console.log() ,所以要为 babel 配置 Remove console transform

这里需要说明的是因为开发环境下启用了 hot module replacement,为了让样式源文件的修改也同样能被热替换,不能使用 ExtractTextPlugin而转为随 JS Bundle 一起输出。

以下是这三个配置文件的常见写法:

webpack.base.config.js

在 base 文件里,我们需要将开发环境和生产环境中通用的配置一起写在这里:

const CleanWebpackPlugin = require('clean-webpack-plugin');
const path = require('path');
const webpack = require('webpack');

// 配置常量
// 源代码的根目录(本地物理文件路径)
const SRC_PATH = path.resolve('./src');
// 打包后的资源根目录(本地物理文件路径)
const ASSETS_BUILD_PATH = path.resolve('./build');
// 资源根目录(可以是 CDN 上的绝对路径,或相对路径)
const ASSETS_PUBLIC_PATH = '/assets/';

module.exports = {
  context: SRC_PATH, // 设置源代码的默认根路径
  resolve: {
    extensions: ['.js', '.jsx']  // 同时支持 js 和 jsx
  },
  entry: {
    // 注意 entry 中的路径都是相对于 SRC_PATH 的路径
    vendor: './vendor/scripts',
    app: ['./entry-a'],
    ...
  },
  output: {
    path: ASSETS_BUILD_PATH,
    publicPath: ASSETS_PUBLIC_PATH,
    filename: './[name].js'
  },
  module: {
    rules: [
      {
        enforce: 'pre',  // ESLint 优先级高于其他 JS 相关的 loader
        test: /\.jsx?$/,
        exclude: /node_modules/,
        loader: 'eslint-loader'
      },
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        // 建议把 babel 的运行时配置放在 .babelrc 里,从而与 eslint-loader 等共享配置
        loader: 'babel-loader'
      },
      {
        test: /\.(png|jpg|gif)$/,
        use:
        [
          {
            loader: 'url-loader',
            options:
            {
              limit: 8192,
              name: 'images/[name].[ext]'
            }
          }
        ]
      },
      {
        test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
        use:
        [
          {
            loader: 'url-loader',
            options:
            {
              limit: 8192,
              mimetype: 'application/font-woff',
              name: 'fonts/[name].[ext]'
            }
          }
        ]
      },
      {
        test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
        use:
        [
          {
            loader: 'file-loader',
            options:
            {
              limit: 8192,
              mimetype: 'application/font-woff',
              name: 'fonts/[name].[ext]'
            }
          }
        ]
      }
    ]
  },
  plugins: [
    // 每次打包前,先清空原来目录中的内容
    new CleanWebpackPlugin([ASSETS_BUILD_PATH], { verbose: false }),
    // 启用 CommonsChunkPlugin
    new webpack.optimize.CommonsChunkPlugin({
      names: 'vendor',
      minChunks: Infinity
    })
  ]
};

这是用于开发环境的 Webpack 配置,继承自 base:

const webpack = require('webpack');

// 读取同一目录下的 base config
const config = require('./webpack.base.config');

// 添加 webpack-dev-server 相关的配置项
config.devServer = {
  contentBase: './',
  hot: true,
  publicPath: '/assets/'
};
// 有关 Webpack 的 API 本地代理,另请参考 https://webpack.github.io/docs/webpack-dev-server.html#proxy 

config.module.rules.push(
  {
    test: /\.css$/,
    use: [
      'style-loader',
      'css-loader',
      // 这里只写css的,less和sass的loader写法同理
    ],
    exclude: /node_modules/
  }
);

// 真实场景中,React、jQuery 等优先走全站的 CDN,所以要放在 externals 中
config.externals = {
  react: 'React',
  'react-dom': 'ReactDOM'
};

// 添加 Sourcemap 支持
config.plugins.push(
  new webpack.SourceMapDevToolPlugin({
    filename: '[file].map',
    exclude: ['vendor.js'] // vendor 通常不需要 sourcemap
  })
);

// Hot module replacement
Object.keys(config.entry).forEach((key) => {
  // 这里有一个私有的约定,如果 entry 是一个数组,则证明它需要被 hot module replace
  if (Array.isArray(config.entry[key])) {
    config.entry[key].unshift(
      'webpack-dev-server/client?https://0.0.0.0:8080',
      'webpack/hot/only-dev-server'
    );
  }
});
config.plugins.push(
  new webpack.HotModuleReplacementPlugin()
);

module.exports = config;
webpack.prod.config.js

这是用于生产环境的 webpack 配置,同样继承自 base:

const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

// 读取同一目录下的 base config
const config = require('./webpack.base.config');

config.module.rules.push(
  {
    test: /\.css$/,
    use: ExtractTextPlugin.extract(
      {
        use: [
          'css-loader',
        ],
        fallback: 'style-loader'
      }
    ),
    exclude: /node_modules/
  }
);

config.plugins.push(
  // 官方文档推荐使用下面的插件确保 NODE_ENV
  new webpack.DefinePlugin({
    'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production')
  }),
  // 抽取 CSS 文件
  new ExtractTextPlugin({
    filename: '[name].css',
    allChunks: true,
    ignoreOrder: true
  })
  // 启用js压缩
  new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: false
      },
      exclude: /\.min\.js$/
  })
  // 更多实用的插件使用下面将会揭晓
    ...
);

module.exports = config;

最后,还需要在 package.json 里添加相应的配置:

// package.json
{
  ...
  "scripts": {
    "build": "webpack  --config webpack.dev.config.js",
    "dev": "webpack-dev-server --config webpack.dev.config.js",
    "start": "npm run dev" // 或添加你自己的 start 逻辑
  },
  ...
}

参考: Henry Li - 为什么我们要做三份 Webpack 配置文件

进阶

前言

随着项目逐渐变得越来越大,我们就会发现,使用webpack进行打包后的bundle文件也越来越大了,特别是项目中有使用到不少的第三方库,这些库往往不会经常更新,但文件总是将其一五一十且丝毫不差地重新再打包整合成最新版本的文件。因此造成了很不必要的打包时间消耗,而且更为致命的是,由于文件实在太大,导致用户等待时间过长… 那么,有什么办法可以解决这种无脑打包的问题吗?

解决方案

我们可以通过指定多个入口文件将主要代码和第三方库(或者还有更多的…)解耦,并通过文件内容修改与否的检测,给每个打包文件加上hash命名,以实现跳过无更改文件进行无重复打包,最后在浏览器端,通过按需加载的方式进行呈现。一方面,这将大大缩短打包过程所消耗的时间,另一方面,浏览器通过识别hash值(或无更改名称)作出是否直接读取本地缓存的304:not modified判断,此外还能根据相关的需求实现“逢用必加载,不用则不载”,这亦将大大缩短用户浏览页面所消耗的时间。整体的效率提升是非常可观的!

准备工作

首先我们需要明确目录的结构,在这里举个简单的示例:

node_modules
vendors
  scripts
    - swiper.js
    - parallex.js
  styles
    - animate.css
    - wonder.scss
webpack.config.js
package.json
index.html
app.js
...

多个入口文件

上面已经提到过,可通过配置entry来指定多个入口文件。

const path = require('path');
module.exports={
    entry: {
        vendor: [
            path.resolve(__dirname, "vendors/scripts/swiper"),
            path.resolve(__dirname, "vendors/scripts/parallex")
            ...
        ]
        app: path.resolve(__dirname, 'app')
    }
}

有一点需要注意的是,假如第三方库的数量比较多时,通过上述的写法未免太死板和冗长,我们可以自己定义一个方法来获取scripts目录下的所有第三方库。

这里需要用到node的一个模块glob,用以获取目录文件的路径。

const path = require('path');

 // getEntries方法
function getEntries(pattern,options){
  const glob = require('glob');
  // 注意:必须使用glob.sync方法 !!
  // _entryList为文件路径列表,
  // _entryMap为以“{ filename: filepath }“格式化后的资源图
  let _entryList = glob.sync(pattern,options); 
  let _entryMap = {},
    // 获取文件扩展名
    ext = pattern.match(/\.[^\s\.]+$/)[0];
  for (let e of _entryList){
    let name = e.split(/\//).pop().replace(ext,'');
    _entryMap[name] = e;
  }
  // 返回对应属性值
  return {
    list: _entryList,
    map: _entryMap
  };
}

// 然后便可省心地将vendor目录下的js文件一次全部引入了
module.exports = {
  entry: {
    vendor: getEntries('./vendors/scripts/*.js',{}).list,
    app: path.resolve(__dirname,'app')
  },

缓存处理

一般的,使用CommonsChunkPlugin后,我们都会很自然地通过下面的配置,将打包后的文件名添加hash值,这样就可以做到每次修改后打包的文件hash值会随之改变了。

...
output: {
    filename: '[name].[chunkHash].js',
    path: path.resolve(__dirname, 'dist')
},
plugins:[
    new webpack.optimize.CommonsChunkPlugin({
         name: 'vendor'
     })
]
...

正当我们趁着打包的等待中得意之时,往往会掉进一个大坑。事情可并不像想象中那么简单。

一个意外

仔细检查了一遍后,我们不难发现,每当改变了业务逻辑的代码时,vendor.js的hash值无论如何都会发生改变。也就是说,即使vendor.js这个文件并没有发生任何改变,用户在浏览时还是需要重新下载这份东西,这显然不是我们想要的!!

如何解决

当这类的issue被提出来之后,网上便很快涌现了很多种解决的方法,例如使用md5hash来代替chunkhash,从打包文件中抽离Webpack运行时的代码,使用NamedModulesPlugin或这dllPlugin等等…

结合以上的方案可以得到一个大致的解决思路:

1. 使用插件替换默认的数字类型的模块id,避免增加或删除模块对其他模块的id产生影响;

2. 需要从打包文件中抽离Webpack的运行代码,以保证打包文件的hash不受影响。

最终解决方案

多数开发者的实践表明,可以通过HashedModuleIdsPlugin来很好的解决这个问题。

HashedModuleIdsPlugin是由Webpack的作者介绍和推荐的一款插件,其根据模块的相对路径生成一个长度只有4位的字符串作为模块的id,既能够隐藏模块的路径信息,又减少了模块id的长度。该插件自Webpack 2.x后集成,可以直接使用。

需要做的

接着上一个步骤,使用CommonsChunkPlugin后,修改配置文件如下:

// webpack.config.js
...
output: {
    path: path.resolve(__dirname, "dist"),
    filename: "[name].[chunkhash].js",
    // 通过chunkhash使每一个chunk保持唯一性
    chunkFilename:"[chunkhash].js"
},
plugins: [
    // 使用插件
    new webpack.HashedModuleIdsPlugin(),
    new webpack.optimize.CommonsChunkPlugin(
    {
      name: 'vendor'
    })
  ]

通过项目的修改、增加或删除业务代码模块等多种情形的测试,并对比了每次编译后的文件的 hash后,你会发现,vendor.js 和异步加载的文件的 hash 都没有改变。这才是我们真正想要的,大写的赞!

如果想了解其它解决方案和更多,请点击这里。

代码拆分与按需加载

将代码进行合理拆分以及合理使用按需加载是非常必要的,这不仅有利于我们规范代码书写,调整代码的加载和处理逻辑,也有助于缩短页面加载的时间以及提高资源加载的整体效率。

Webpack中的require.ensure语句为我们提供了按需加载的方法。因此可以利用code splitting(代码拆分)特性,将各个步骤的代码进行拆分,从而实现按需加载。

简单的,把异步加载的代码,放到 require.ensure 里面的回调函数便可实现:

// 第一个参数为依赖项,第三个参数为文件命名
require.ensure([], function(){
    require('./js/the-js-file.js');
}, 'file-name');

然后,千万不要忘记在output中添加 publicPath 和 chunkFilename属性值:

output: {
    publicPath: '/dist/', 
    chunkFilename: 'js/apps/[name].min.js'
}

至此,所有进阶配置已经完成。接下来便可好好感受一下该配置所带来的便利了…

优化

压缩代码

使用Webpack的内置插件UglifyJsPlugin,配置如下:

// webpack.config.js
...
plugins:[
    new webpack.optimize.UglifyJsPlugin({
        compress: {
            warnings: false
        },
        exclude: /\.min\.js$/
    })
]

样式分离及处理

很多情况下,我们需要将样式文件进行单独打包,作为CDN来使用。如果js文件中引入了样式文件 ,那将会导致页面从开始加载直至js文件解析完毕的过程中没有样式。因此我们一般都会考虑将样式文件分离出来,然后对其进行一些相关的处理。

首先需要安装extract-text-webpack-plugin插件

    npm install extract-text-webpack-plugin –save-dev

然后在webpack.config.js 中声明插件:

var ExtractTextPlugin = require("extract-text-webpack-plugin");
...
plugins: [
     new ExtractTextPlugin("[name].min.css")
]

最后该插件会自动将js中的样式文件提取出来,生成单独的文件。

图片处理

在一些项目里,往往会涉及图片的处理工作。最为常见的,当项目需要的图片大小不会太大时,我们可以采用base64进行图片编码处理。

首先需要安装 url-loader模块:

npm install url-loader --save-dev

然后在配置文件中添加配置:

//webpack.config.js
...
module:{
    rules:[
        ...
        {
            test: /\.(png|jpg)$/,
            use:[ { loader: 'url?limit=8192&name=images/[hash:8].[name].[ext]' } ]
        }
    ]
}

参数解释

我们可以通过 limit 设置一个阈值,当小于这个值时就会自动启用 base64 编码的图片,大于这个值的图片会打包到 name 参数对应的路径,图片名称就会包括8位md5编码 、文件名name 以及对应的扩展名ext。

去除build文件中的残余文件

前面我们的项目里添加了hash,但在这之后,会导致文件内容变更后重新打包,文件名不同而内容越来越多,因此我们需要清除build文件中的残余文件,这里介绍一个很好用的插件clean-webpack-plugin。

首先安装插件:

npm install clean-webpack-plugin --save-dev

然后引入clean-webpack-plugin插件,在配置文件的plugins中做相应配置即可:

    const CleanWebpackPlugin = require("clean-webpack-plugin");
     plugins: [
       ...
       new CleanWebpackPlugin('build/*.*', {
         root: __dirname,
         verbose: true,
         dry: false
     })
     ]