Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

翻译 | webpack2的入门手册 #8

Open
allenGKC opened this issue Apr 13, 2018 · 0 comments
Open

翻译 | webpack2的入门手册 #8

allenGKC opened this issue Apr 13, 2018 · 0 comments
Labels

Comments

@allenGKC
Copy link
Owner

image

背景

一直对webpack的打包流程很感兴趣,但是无奈官网文档实在太多,搜出来的大部分文章要么偏理论要么纯粹讲过程不讲原理,最近终于找到一篇入门文章,文章对于初学者讲的很清晰,但是由于是英文的,而且我没有找到这篇文章对应的中文翻译版,所以本文主要是对那篇文章进行翻译,介绍一下webpack2的入门知识。
注:本人翻译水平有限,如果有错误,欢迎指正。
原文地址:A Beginner’s Guide to Webpack 2 and Module Bundling
原文作者:Mark Brown
译文作者:Allen Gong

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 }]
          ]
        }
      }]
    }]
  }
}

module.exports = config

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

为了让这个项目能运行起来,我们需要安装三个安装包,babel-core,webpack的加载器babel-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文件。控制台的输出结果显示了生成的打包后的文件,我们应该持续关注生成的文件的大小和数量。
image

现在你可以在浏览器中访问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 + Cnpm 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官网主页上的。
image

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

插件

我们已经看过了webpack其中一个构建插件的例子,使用UglifyJsPluginnpm 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.jsadmin.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.htmladmin.html,看看自动生成的公共模块部分。

抽取CSS

另一个受欢迎的插件是extract-text-webpack-plugin,它的用途是抽取模块到对应的结果文件中。
下面我们在配置文件中修改.scss的规则编译成对应的Sass文件,加载CSS,接着把他们抽取到各自的CSS包中,这样就可以把它们从Javascript包中移除。

npm install [email protected] --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.importrequire.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 build操作,你会看到一个新的神秘的打包文件0.bundle.js
image

注意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 [email protected] --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.hottrue,但只是在开发过程中。当以生产模式打包时,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 & HTTP/2

写在结尾的话

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

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

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

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

本文翻译自A Beginner’s Guide to Webpack 2 and Module Bundling
翻译者:Allen Gong

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant