翻译 | webpack 2 的入门手册

  1. 云栖社区>
  2. 前端那些事儿>
  3. 博客>
  4. 正文

翻译 | webpack 2 的入门手册

awufan 2018-07-12 14:04:14 浏览1368
展开阅读全文

背景

一直对webpack的打包流程很感兴趣,但是无奈官网文档实在太多,搜出来的大部分文章要么偏理论要么纯粹讲过程不讲原理,最近终于找到一篇入门文章,文章对于初学者讲的很清晰,但是由于是英文的,而且我没有找到这篇文章对应的中文翻译版,所以本文主要是对那篇文章进行翻译,介绍一下webpack2的入门知识。

webpack2入门手册(译文)

Webpack是一个模块打包机

Webpack已然成为当前web开发最重要的工具之一。首先它是一个Javascript的打包工具,但同时他也能打包包括HTML,CSS,甚至是图片等形式的资源。它能更好的控制你正在编写的App的HTTP请求,并且允许你去使用更多的资源(如Jade,Sass以及ES6)。Webpack同时允许你更容易的从npm获取安装包。

这篇文章主要面向那些对于webpack完全陌生的同学,内容将包括初始安装和配置,模块,模块加载器,插件,代码拆分以及模块热替换(HMR,hot module replacement)。如果你觉得入门视频比较有用的话,我推荐Glen Maddern的Webpack初体验作为开始学习的起点,会让你理解为什么webpack如此特殊。

为了更加后续的阅读,请确保先安装了Node.js,安装可以参考Node.js安装教程。你也在Github上下载到对应的 Demo

安装

让我们用npm和webpack新建一个项目吧:


mkdir webpack-demo
cd webpack-demo
npm init -y
npm install webpack@beta --save-dev
mkdir src
touch index.html src/app.js webpack.config.js

编辑以下文件:


<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Hello webpack</title>
  </head>
  <body>
    <div id="root"></div>
    <script src="dist/bundle.js"></script>
  </body>
</html>


// src/app.js
const root = document.querySelector('#root')
root.innerHTML = `<p>Hello webpack.</p>`


// webpack.config.js
const webpack = require('webpack')
const path = require('path')

const config = {
  context: path.resolve(__dirname, 'src'),
  entry: './app.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  module: {
    rules: [{
      test: /\.js$/,
      include: path.resolve(__dirname, 'src'),
      use: [{
        loader: 'babel-loader',
        options: {
          presets: [
            ['es2015', { modules: false }]
          ]
        }
      }]
    }]
  }
}

以上的设置只是通用配置,它会指导你的webpack将我们的入口文件src/app.js编译输入为/dist/bundle.js,并且所有的.js文件都将通过Bable从ES2015转换为ES5。

为了让这个项目能运行起来,我们需要安装三个安装包,bable-core,webpack的加载器bable-loader以及预处理模块babel-preset-es2015,这些模块都是为了支持Javascript的编写。{ modules: false }可以确保使用Tree Shaking去去除掉不必要的模块,同时会降低文件大小。


npm install babel-core babel-loader babel-preset-es2015 --save-dev

最后使用下面代码更新package.json:


"scripts": {
  "start": "webpack --watch",
  "build": "webpack -p"
},

运行npm start将会以观察模式启动webpack,在这种模式下,会持续监听我们src文件夹下的.js文件。控制台的输出结果显示了生成的打包后的文件,我们应该持续关注生成的文件的大小和数量。

现在你可以在浏览器中访问index.html,将会看到“Hello webpack.”


open index.html

打开dist/bundle.js看看webpack到底做了什么事,在文件的顶部是bootstrapping模块的代码,在它下面是我们自己的模块。你可能目前还没有什么感觉webpack好处,但是你现在可以编写ES6代码并且webpack将会把各个模块打成生产所需要的包,这样所有浏览器都能访问。

使用Ctrl + C停止webpack的服务,运行npm run build,编译成生成环境所需要的包。

注意:包的大小从2.61 kB降到了585 bytes 重新看看dist/bundle.js,你会发现代码变得一团糟,UglifyJS对打包后的代码进行了压缩,运行起来是没有差别的,但同时字符数是相当少的。

模块

对于外部模块,webpack有多种方式去引入,其中比较重要的两种是:

  • ES2015的import方法
  • CommonJS的require()方法

我们可以通过安装lodash来测试上述方式,并且导入到 app.js中。


npm install lodash --save
// src/app.js
import {groupBy} from 'lodash/collection'

const people = [{
  manager: 'Jen',
  name: 'Bob'
}, {
  manager: 'Jen',
  name: 'Sue'
}, {
  manager: 'Bob',
  name: 'Shirley'
}, {
  manager: 'Bob',
  name: 'Terrence'
}]
const managerGroups = groupBy(people, 'manager')

const root = document.querySelector('#root')
root.innerHTML = `<pre>${JSON.stringify(managerGroups, null, 2)}</pre>`

运行npm start重启webpack并刷新index.html,你会在页面上看到一个按照manager分好组人名的数组。 接下来让我们把这个数组部分单独放在people.js这个模块里。


// src/people.js
const people = [{
  manager: 'Jen',
  name: 'Bob'
}, {
  manager: 'Jen',
  name: 'Sue'
}, {
  manager: 'Bob',
  name: 'Shirley'
}, {
  manager: 'Bob',
  name: 'Terrence'
}]

export default people

我们可以以相对路径的方式将模块导入到app.js


// src/app.js
import {groupBy} from 'lodash/collection'
import people from './people'

const managerGroups = groupBy(people, 'manager')

const root = document.querySelector('#root')
root.innerHTML = `<pre>${JSON.stringify(managerGroups, null, 2)}</pre>`

注意:导入像'lodash/collection这种不使用相对路径的,是那些通过npm安装的,从/node_modules中引入,你自定义的模块则需要像'./people'相对路径的方式引入,通过这种方式可以对两种模块进行区分。

加载器

我们已经介绍了babel-loader,它是众多loader中的一种,能够告诉webpack当遇到不同的文件时如何处理。比较好的方式是将loader进行串联,加载到一个加载器中,我们通过从Javascript中引入Sass包来看看loader是如何进行工作的。

Sass

这个转换器包括了三个单独的加载器和node-sass库:


npm install css-loader style-loader sass-loader node-sass --save-dev

在配置文件中为.scss引入新的规则:


// webpack.config.js
rules: [{
  test: /\.scss$/,
  use: [
    'style-loader',
    'css-loader',
    'sass-loader'
  ]
}, {
  // ...
}]

注意:不管什么时候你改变了webpack.config.js中的加载规则,你都需要通过Ctrl + C然后npm start的方式重启webpack。

loader以倒序的方式运行:

  • sass-loader转换Sass成CSS
  • css-loader将CSS解析Javascript并解决依赖包问题
  • style-loader将CSS导出成<tag>便签放在document下

你可以将上述过程想象成函数的调用关系,一个函数运行的结果作为另一个函数的输入:


styleLoader(cssLoader(sassLoader('source')))

接下来让我们增加一个Sass源文件:


/* src/style.scss */
$bluegrey: #2B3A42;

pre {
  padding: 20px;
  background: $bluegrey;
  color: #dedede;
  text-shadow: 0 1px 1px rgba(#000, .5);
}

现在你可以在你的app.js中直接引入Sass文件:


// src/app.js
import './style.scss'

// ...

刷新index.html你会看到样式发生了变化。

Javascript中的CSS

我们刚刚把Sass作为一个模块引入到我们的入口文件中。

打开dist/bundle.js,搜索pre {。事实上,Sass已经被编译成一段CSS的字符串,并以模块的形式存在。当我们在我们的Javascript文件中导入这个模块时,style-loader就会将其编译输出成内嵌的<style>标签。

我知道你在想什么?为什么要这么做?

关于这个问题,我在这个话题中不想说太多,但下面几个原因值得思考一下:

  • 如果你想在项目中引入一个Javascript组件并正常运行,可能需要依赖很多其他资源(如HTML, CSS, Images, SVG),如果我们将所有资源打包到一起,将会非常易于引入和使用。
  • 去除写死的代码:当一个JS组件不再被代码引入到项目中,对应的CSS也不会被引入进来。而最终打包后的结果也只会包含那些被引用的部分。
  • CSS模块:由于全局CSS命名空间的存在,使得改变CSS后是否有副作用不得而知。CSS模块默认情况下将CSS设成本地,并显示你在Javascript中可以引用的唯一类名。
  • 通过捆绑/分割代码的巧妙方式减少HTTP请求的数量。

图片

最后一个关于loader的例子是关于处理图片的url-loader。

在标准HTML文档中,图片通过<img>标签或者background-image属性获得。但是通过webpack,一些小图片可以以字符串的形式存储在Javascript中。通过这种方式,你可以在预加载的时候就获取到图片,从而不需要单独的请求去请求图片。


npm install file-loader url-loader --save-dev

在配置文件中增加一条图片的规则:


// webpack.config.js
rules: [{
  test: /\.(png|jpg)$/,
  use: [{
    loader: 'url-loader',
    options: { limit: 10000 } // Convert images < 10k to base64 strings
  }]
}, {
  // ...
}]

通过Ctrl + C和npm start重启服务。

通过下面的命令下载一个测试图片:


curl https://raw.githubusercontent.com/sitepoint-editors/webpack-demo/master/src/code.png --output src/code.png

现在可以在app.js中加载图片资源:


// src/app.js
import codeURL from './code.png'
const img = document.createElement('img')
img.src = codeURL
img.style.backgroundColor = "#2B3A42"
img.style.padding = "20px"
img.width = 32
document.body.appendChild(img)

// ...

这样页面中多了一个img,它的src属性包含了图片自身的data URI。


<img src="data:image/png;base64,iVBO..." style="background: #2B3A42; padding: 20px" width="32">

同时,因为css-loader的缘故,通过url()属性引入的图片,也通过url-loader转换成行内元素。

/* src/style.scss */
pre {
  background: $bluegrey url('code.png') no-repeat center center / 32px 32px;
}

编译后变成:


pre {
    background: #2b3a42 url("data:image/png;base64,iVBO...") no-repeat scroll center center / 32px 32px;
}

模块到静态资源

现在你可以webpack是如何帮助你对将你项目中一系列的依赖资源进行打包处理的,下面这张图是webpack官网主页上的。

虽然Javascript是入口文件,但是webpack还是倾向于你的其他类型的资源像HTML, CSS, and SVG能有自己的依赖,把它们作为构建包的一部分。

插件

我们已经看过了webpack其中一个构建插件的例子,使用UglifyJsPlugin的npm run build脚本可以调用webpack -p,它的作用是与webpack搭配压缩生成后的包。

当loader在单个文件上操作相应变换时,插件可以在各个大型代码块上交叉运行。

公共代码

commons-chunk-plugin是另一个核心插件,搭配webpack用来创建在多个入口文件中使用的拥有公共代码的单文件模块。到目前为止,我们使用的都是单一入口和单一出口文件。但是很多 real-world scenarios中更好的方法是使用多文件入口和多文件出口。

如果你在你的应用中有两个完全独立的领域但是却拥有共同的模块,举个例子,app.js是面向用户的,admin.js是面向管理员的,你就可以为他们单独创建不同的入口文件,就像下面这样:


// webpack.config.js
const webpack = require('webpack')
const path = require('path')

const extractCommons = new webpack.optimize.CommonsChunkPlugin({
  name: 'commons',
  filename: 'commons.js'
})

const config = {
  context: path.resolve(__dirname, 'src'),
  entry: {
    app: './app.js',
    admin: './admin.js'
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].bundle.js'
  },
  module: {
    // ...
  },
  plugins: [
    extractCommons
  ]
}

module.exports = config

注意对于结果文件,现在包含了名字,这样我们区分出两个不同的结果文件对应不同的入口文件:app.bundle.js和admin.bundle.js。

commonschunk插件生成了第三个文件commons.js,他包含了我们入口文件的公共模块。


// src/app.js
import './style.scss'
import {groupBy} from 'lodash/collection'
import people from './people'

const managerGroups = groupBy(people, 'manager')

const root = document.querySelector('#root')
root.innerHTML = `<pre>${JSON.stringify(managerGroups, null, 2)}</pre>`
// src/admin.js
import people from './people'

const root = document.querySelector('#root')
root.innerHTML = `<p>There are ${people.length} people.</p>`

这些入口文件将会产生下列文件:

  • app.bundle.js:包括样式和lodash/collection模块
  • admin.bundle.js:不包含任何额外模块
  • commons.js:包含了我们公共的people模块

我们可以在两个入口文件中都引入公共模块:


<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Hello webpack</title>
  </head>
  <body>
    <div id="root"></div>
    <script src="dist/commons.js"></script>
    <script src="dist/app.bundle.js"></script>
  </body>
</html>


<!-- admin.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Hello webpack</title>
  </head>
  <body>
    <div id="root"></div>
    <script src="dist/commons.js"></script>
    <script src="dist/admin.bundle.js"></script>
  </body>
</html>

试试在浏览器中重新加载index.html和admin.html,看看自动生成的公共模块部分。

抽取CSS

另一个受欢迎的插件是extract-text-webpack-plugin,它的用途是抽取模块到对应的结果文件中。

下面我们在配置文件中修改.scss的规则编译成对应的Sass文件,加载CSS,接着把他们抽取到各自的CSS包中,这样就可以把它们从Javascript包中移除。


npm install extract-text-webpack-plugin@2.0.0-beta.4 --save-dev


// webpack.config.js
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const extractCSS = new ExtractTextPlugin('[name].bundle.css')

const config = {
  // ...
  module: {
    rules: [{
      test: /\.scss$/,
      loader: extractCSS.extract(['css-loader','sass-loader'])
    }, {
      // ...
    }]
  },
  plugins: [
    extractCSS,
    // ...
  ]
}

重启webpack你会看到一个新的打包后的文件app.bundle.css,你可以照例直接引用它。


<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Hello webpack</title>
    <link rel="stylesheet" href="dist/app.bundle.css">
  </head>
  <body>
    <div id="root"></div>
    <script src="dist/commons.js"></script>
    <script src="dist/app.bundle.js"></script>
  </body>
</html>

刷新页面,确认CSS已经被编译过了,并从app.bundle.js移到了app.bundle.css,成功了!

代码拆分

我们已经看了几种代码拆分的方法:

  • 手动创建单独的入口文件
  • 自动将公共代码拆分到公共模块中
  • 使用extract-text-webpack-plugin从编译后的代码中抽取出来

拆分包还有其他方法:System.import和 require.ensure。通过在这些函数中包含代码段,你可以创建一个在运行时按需加载的模块。这个从根本上提高了性能,因为在启动过程中不需要把所有东西都发送到客户端。System.import将模块名作为参数,并返回一个Promise对象。require.ensure获取依赖关系的列表,回调函数以及可选的模块名。

如果应用程序的某一部分具有很大的依赖关系,则应用程序的其余部分就不需要了,最好的方式就是拆分到各个模块中去。我们通过新建一个需要依赖d3的模块dashboard.js来证明这点。


npm install d3 --save


// src/dashboard.js
import * as d3 from 'd3'

console.log('Loaded!', d3)

export const draw = () => {
  console.log('Draw!')
}

在app.js的顶部引入dashboard.js:


// ...

const routes = {
  dashboard: () => {
    System.import('./dashboard').then((dashboard) => {
      dashboard.draw()
    }).catch((err) => {
      console.log("Chunk loading failed")
    })
  }
}

// demo async loading with a timeout
setTimeout(routes.dashboard, 1000)

因为我们加载了异步模块,我们需要在配置文件中增加output.publicPath属性,因此webpack知道去哪里获取。


// webpack.config.js

const config = {
  // ...
  output: {
    path: path.resolve(__dirname, 'dist'),
    publicPath: '/dist/',
    filename: '[name].bundle.js'
  },
  // ...
}

运行npm run build操作,你会看到一个新的神秘的打包文件0.bundle.js。

注意webpack为了保持诚实,通过凸现[big]的包来让你保持关注。 这个0.bundle.js将会通过JSONP的请求按需加载,所以从文件目录中获取将不在有效,我们需要启动一个服务来获取文件。


python -m SimpleHTTPServer 8001


打开浏览器,输入http://localhost:8001/ 加载一秒钟后,你会获得一个GET请求,我们动态生成了/dist/0.bundle.js文件,在控制台上打印除了"Loaded!",成功!

webpack开发服务器

当文件改变时,实时地重新加载能提高开发者的开发效率。只要安装它,并且以webpack-dev-server的形式启动,就可以体验啦。


npm install webpack-dev-server@2.2.0-rc.0 --save-dev


修改package.json中的start脚本:


"start": "webpack-dev-server --inline",


重新运行npm start,在浏览器中打开http://localhost:8080

试着去改变src目录中任何文件,如改变people.js中的任意一个名字,或者style.scss中的任意样式,去看看它如何实时改变。

热模块替换(热更新)

如果你对实时重新加载印象深刻,那么hot module replacement(HMR)一定会让你吃惊不已。

现在是2017年了,你在工作中已经可以在单页面应用中使用全局状态了。在开发过程中,你可能会对组件进行许多小的修改,并且希望能在浏览器中看到修改后生成的结果,这样可以实时去更改。但是通过刷新页面或者实时热更新并不能改变全局的状态,你就必须重头开始。但是HMR永远地改变了这一问题。

最后对package.json中的start脚本做修改:


"start": "webpack-dev-server --inline --hot",


在app.js中告诉webpack去接受这个模块以及对应依赖的热更新。


if (module.hot) {
  module.hot.accept()
}

// ...

注意:webpack-dev-server --hot设置了module.hot为true,但只是在开发过程中。当以生产模式打包时,module.hot被设成了false,这样这些包就被从结果中抽离了。

在webpack.config.js中增加一个NamedModulesPlugin插件,去改善控制台的记录功能。


plugins: [
  new webpack.NamedModulesPlugin(),
  // ...
]

最后我们在页面中增加一个<input>元素,我们可以在里面增加一些文字,用来确保我们更改自己模块时页面不会刷新。


<body>
  <input />
  <div id="root"></div>
  ...

运行npm start重启服务,观察热更新如何工作吧。

为了实验,在input框中输入“HMR Rules”,接着改变一个people.js中的名字,你会发现页面在不刷新也能做出修改,而忽略input的状态。

这只是一个简单的例子,但是希望你能看到其广泛的用途。在诸如React的开发模式中,你可能有很多"哑巴"组件是与他们的状态分离开的,通过热更新,这些组件将不会失去状态,也能实时更新,因此你将获得及时的反馈。

热更新CSS

修改style.scss文件中<pre>元素的背景颜色,你发现他并没有被HMR替换。


pre {
  background: red;
}

事实证明当你使用style-loader时,CSS的热更新将会免费为你提供而不需要你做任何特殊处理。我们只需要断开CSS模块与最终抽取的包之间的链接,这个包是无法被替换的。

如果我们将Sass规则恢复到原始状态,并从插件列表中删除extractCSS,那么您也可以看到Sass的热重新加载。


{
  test: /\.scss$/,
  loader: ['style-loader', 'css-loader','sass-loader']
}

HTTP/2

使用像webpack这样的模块打包工具的主要好处之一是,您可以通过控制资源的构建方式以及在客户端上的获取方式,从而帮助你提高性能。多年以来,它被认为是最佳实践,通过连接文件减少客户端请求。现在还是有效,但是HTTP2在单一请求中发送多文件,因此连接文件的方式不不再是"银弹"。你的应用程序实际上可以从多个小文件单独缓存,但客户端可以获取单个更改的模块,而不必再次获取大部分相同内容的整个包。

Webpack的创始人Tobias Koppers的撰写了一篇内容丰富的帖子,解释了为什么打包仍然很重要,即使在HTTP/2时代。

想了解更多请参考webpack &amp;amp; HTTP/2

写在结尾的话

我真心希望你已经发现这个介绍webpack 2的文章对你有帮助,并能够开始很好使用它。围绕webpack的配置,加载程序和插件可能需要一些时间,但是了解这个工具的工作原理后会对你有很大帮助。

文档仍在进行更新中,但如果您想将现有的Webpack1项目移到Webpack2,则可以参考Migrating from v1 to v2

webpack是否是你打包的选择,从评论中你就可以知晓。

本文由Scott MolinariJoan YinJoyce Echessa进行了同行评审。 感谢SitePoint的同行评议人员,使SitePoint内容成为最棒的内容!

原文发布时间为:2017年08月02日
原文作者:Mark Brown

译文作者:Allen Gong
本文来源:掘金 如需转载请联系原作者


网友评论

登录后评论
0/500
评论
awufan
+ 关注
所属团队号: 前端那些事儿