[翻译] Webpack 解惑

1,748 阅读6分钟
原文链接: zhuanlan.zhihu.com

本文译自Webpack — The Confusing Parts,原文需翻墙。

Webpack现在是React应用程序标配的模块打包器,我估计Angular2和其他框架的用户也在大规模使用。我第一次看到Webpack配置文件的时候,感觉实在太陌生,太混乱了。在玩了一段时间后,我现在觉得是Webpack独特的语法和新的理念导致了初学阶段的复杂度。顺便说一下,也正是因为这些理念,才让Webpack如此受欢迎。

正因为Webpack入门容易让人困惑,所以我写了这篇文章,希望能够帮助其他人更轻松的入门,用起来更顺手。这是第一部分。

Webpack的核心理念

Webpack的两个核心理念是:

  1. 万物皆模块:就像JS文件可以当做模块,那么其他所有的文件(CSS,图片,HTML)都可以当做模块。这样,就可以require(“myJSfile.js”)或者require(“myCSSfile.css”)。这意味着,我们可以对模块再进行细分,分割成更小更容易管理的粒度,实现复用等等。
  2. 按需加载,异步加载:一般的模块打包器会打包所有的模块然后生成一个巨大的输出文件bundle.js。但是在许多实际应用的APP中,这个bundle.js可能会有10MB-15MB那么大,并且总是会加载!而Webpack有一些功能可以分割代码然后生成多个"bundle"文件并且可以在你需要的时候异步加载。

1.开发模式 VS 生产模式

首先要注意的事情是Webpack有太多的功能,有些只有开发模式下可以用,有些只有生产模式下可以用,还有一些开发模式和生产模式下都可以用。


一般情况下,大部分项目都会用到许多功能,所以通常有两个大型的Webpack配置文件。

执行打包的话可以像下面这样在package.json中写脚本:

 “scripts”: {
  //npm run build 生产模式打包
  “build”: “webpack --config webpack.config.prod.js”,
  
  //npm run dev 开发模式打包并且运行dev-server
  “dev”: “webpack-dev-server”
 }

2.Webpack CLI VS webpack-dev-server

需要强调的一点是Webpack这个模块打包器提供了两个接口:

  1. Webpack CLI工具-默认接口(作为Webpack的一部分自动安装)。
  2. webpack-dev-server工具-一个Node.js服务器(需要单独安装)。

Webpack CLI(适用于生产模式的构建)

此工具可以通过CLI或者配置文件(默认值:webpack.config.js)获取配置并传递给Webpack进行打包。

初学Webpack的时候可以使用CLI,大部分可能只会用来进行生产模式的构建。

用法:

//用法1: 

//全局安装
npm install webpack --g

//在终端中运行
$ webpack //<--用webpack.config.js打包

//用法2:

//本地安装并添加到package.json
npm install webpack --save

//写到package.json的脚本里
“scripts”: {
 “build”: “webpack --config webpack.config.prod.js -p”,
 ...
 }
 
//运行脚本
"npm run build"

Webpack-dev-server(适用于开发模式)

这是一个跑在8080端口上的Express服务器。这个服务器会在内部调用Webpack。用这个的好处是它提供了如刷新浏览器的"Live Reloading"功能和只替换发生改动的模块的"Hot Module Replacement"(HMR)功能。

用法:

//用法1:

//全局安装
npm install webpack-dev-server --g

//在终端中运行
$ webpack-dev-server --inline --hot

//用法2:

//写到package.json的脚本里
“scripts”: {
 “start”: “webpack-dev-server --inline --hot”,
 ...
 }

//运行脚本
$ npm start

//打开浏览器
http://localhost:8080

Webpack VS webpack-dev-server 选项

值得注意的是,有些选项,例如"inline"和"hot"是webpack-dev-server独有的选项,还有一些选项像是"hide-modules"是CLI独有的选项。

webpack-dev-server CLI 选项 VS config 选项

还有一点要注意的是,有两种方式向webpack-dev-server传递选项:

  1. 通过webpack.config.js里的"devServer"对象。
  2. 通过CLI选项。
//通过 CLI
webpack-dev-server --hot --inline

//通过 webpack.config.js
devServer: {
 inline: true,
 hot:true
}

我发现有时候通过devServer配置(hot: true和inline: true)不起作用,所以最好还是把命令写到package.json里,通过CLI传递选项。

//package.json
{
  scripts: 
   {“start”: “webpack-dev-server --hot --inline”}
}

注意:确保没有同时传递hot: true和--hot。

“hot” VS “inline” webpack-dev-server 选项

"inline"选项为整个页面添加了"Live Reloading"功能,而"hot"选项开启了"Hot Module Reloading"功能,这样就会尝试着重载发生变化的组件,而不是整个页面。

//当源码改变时,下面三种命令都会导致重新打包,但是
 
//1. 不会重载浏览器中的页面
$ webpack-dev-server

//2. 会重载整个页面
$ webpack-dev-server --inline

//3. reloads just the module(HMR), or the entire page if HMR fails
//3. 重载改变的模块(HMR),如果HMR失败的话就重载整个页面
$ webpack-dev-server  --inline --hot

3."entry"-String VS Array VS Object

Entry的作用是告诉Webpack根模块,或者说起点在哪。值可以是String,Array或者Object。这里可能会让你困惑,但是不同的类型有各自的用武之地。

如果只有一个entry(大部分APP都是),可以选择任何格式,结果都是一样的。

entry-Array

如果你想添加多个文件并且这些文件不会互相依赖的话,可以使用Array格式。 例如:你可能想在HTML中放入"googleAnalytics.js",可以像下图这样把它添加到到bundle.js的结尾

entry-Object

现在,假设你有一个真正的多页应用,而不是一个多页面的SPA。有多个HTML文件(index.html和profile.html)。然后可以通过对象形式的entry来让Webpack一次性生成多个包

下面的配置会生成两个JS文件,indexEntry.js和profileEntry.js,可以分别在indexEntry.html和profileEntry.html中使用。

用法:

//profile.html
<script src=”dist/profileEntry.js”></script>
//index.html
<script src=”dist/indexEntry.js”></script>

注意:文件名来自"entry"对象的键名。

entry-组合

可以在entry对象中使用数组形式的entry,举个例子,下面的配置会生成三个文件:一个包含有三个库文件的vendoe.js,一个index.js和一个profile.js。

4.output — "path" Vs "publicPath"

output的作用是告诉Webpack如何存储生成的结果文件,其中path和publicPath这两个属性可能会让人困惑。

path仅仅用来告诉Webpack在哪里存放结果文件,而publicPath被一些Webpack插件用来处理CSS,HTML文件中的URL,一般用于生产模式。

例如,CSS里,可能会用./test.png这样的url来加载本地图片,但是在生产模式中,"test.png"这种图片可能是放在CDN上的,服务器是跑在云平台上的。这就意味着,生产模式中,你不得不手动处理文件里的URL保证它们指向的是CDN地址。

现在,你可以用Webpack的publicPath和一些具有publicPath检测功能的插件来自动处理文件中的URL。

// 开发模式:服务器和图片都在本地
.image { 
  background-image: url(‘./test.png’);
 }
 
 
// 生产模式:服务器跑在云平台上,图片在CDN上
.image { 
  background-image: url(‘https://someCDN/test.png’);
 }

Loader 和 Loader链

Loader是用来加载(load)或输入(import)文件的Node模块,不同的loader可以将各种类型的文件转换为浏览器能够接受的格式如JS,Stylesheets等等。更进一步来说,loader允许通过require或ES6的import语句在JS文件中引入各种文件。

例如,可以用babel-loader把ES6语法写的JS转换成浏览器能够兼容的ES5形式:

module: {
 loaders: [{
  test: /\.js$/, ←检测".js"文件, 通过的话,则使用对应loader
  exclude: /node_modules/, ←排除 node_modules 文件夹
  loader: ‘babel’ ←使用 babel (‘babel-loader’的简写形式)
 }]

Loader链(从右向左工作)

多个loader可以链式调用,作用于同一种文件类型。工作链的调用顺序是从右向左,各个loader之间使用"!"分开。

例如,有一个CSS文件myCSSFile.css,我们想把文件里的内容放到HTML文件的标签中。想实现这一点,需要用到两个loader:css-loader和style-loader。

工作流程如下图:


  1. Webpack搜索模块内依赖的CSS文件,通过检查JS文件中是否有require(myCSSFile.css)语句。如果找到了这个依赖文件,Webpack首先把文件传给css-loader。
  2. css-loader载入所有的CSS语句,和CSS文件内的依赖(例如@import otherCSS),处理为JSON,接着Webpack会把这个JSON传给style-loader。
  3. style-loader拿到这个JSON之后会把它放到一个style标签中-,然后把这个style插入index.html文件中。

6.Loader本身也是可以配置的

Loader本身也是可以配置的,传入不同的参数可以实现不同的功能。

在下面的例子中,我们配置url-loader使小于1024字节的图片使用DataURL,大于1024字节的图片使用URL。如下图,有两种方式传入limit参数来实现此功能。

7. .babelrc文件

babel-loader用presets配置将ES6转换为ES5,把React JSX解析为JS。我们可以像下面这样传入query参数来配置:

module: {
  loaders: [
    {
      test: /\.jsx?$/,
      exclude: /(node_modules|bower_components)/,
      loader: 'babel',
      query: {
        presets: ['react', 'es2015']
      }
    }
  ]
}

然而许多项目中babel的配置会很复杂,所以最好把所有的babel-loader配置写在.babelrc文件里。babel-loader会自动加载.babelrc文件如果该文件存在的话。 所以在许多例子中,你会看到下面这种写法:

//webpack.config.js 
module: {
  loaders: [
    {
      test: /\.jsx?$/,
      exclude: /(node_modules|bower_components)/,
      loader: 'babel'
    }
  ]
}

//.bablerc
{
 “presets”: [“react”, “es2015”]
}

8.插件

插件是用来处理打包结果的额外模块。

例如,uglifyJSPlugin会对bundle.js进行压缩和混淆处理,可以压缩文件体积。

还有extract-text-webpack-plugin会在内部调用css-loader和style-loader把所有的CSS收集在一起,最后把结果抽取到一个单独的外部styles.css文件并且在index.html中链接styles.css。

//webpack.config.js
//收集所有的.css文件,合并其中的内容并且抽取到一个单独的"styles.css"
var ETP = require("extract-text-webpack-plugin");

module: {
 loaders: [
  {test: /\.css$/, loader:ETP.extract("style-loader","css-loader") }
  ]
},
plugins: [
    new ExtractTextPlugin("styles.css") //Extract to styles.css file
  ]
}

注意:如果想要把CSS放到HTML的style标签中,可以不使用extract-text-webpack-plugin,只要用css-loader和style-loader就可以了。就像下面这样:

 loaders: [{
  test: /\.css$/,
  loader: ‘style!css’ <--(style-loader!css-loader的简写形式)
 }]

9.Loader VS 插件

你可能会意识到,Loader是在打包前或打包的过程中作用于单个文件插件通常在打包过程结束后,作用于包或者chunk级别。还有一些插件例如commonChunksPlugins,更进一步,会修改如何创建包。

10.解析文件扩展名

许多Webpack配置文件都有一个resolve extensions属性,其中包含一个像那面那样的空字符串。这个空字符串是用来帮助解析没有扩展名的文件输入,例如:require('./myJSFile')或者import myJSFile from './myJSFile'。

{
 resolve: {
   extensions: [‘’, ‘.js’, ‘.jsx’]
 }
}

我的其他文章

延伸阅读