webpack已成为现代Web开发中最重要的工具之一。它是一个用于JavaScript的模块打包工具,可是它也能够转换全部的前端资源,例如HTML和CSS,甚至是图片。它可让你更好地控制应用程序所产生的HTTP请求数量、容许你使用其余资源的特性(例如Jade、Sass和ES6)。webpack还可让你轻松地从npm下载包。css
本文主要针对那些刚接触webpack的同窗,将介绍初始设置和配置、模块、加载器、插件、代码分割和热模块替换。html
在继续学习下面的内容以前须要确保你的电脑中已经安装了Node.js。前端
使用npm初始化一个新项目并安装webpack:node
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
编写下面这些文件:python
<!-- 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
中、使用babel将全部的.js
文件从ES2015转换成ES5。webpack
为了让它能够转换到ES5格式的JS文件,咱们须要安装三个包:babel-core
、webpack加载器babel-loader
和预置babel-preset-es2015
。使用{ modules: false }
让Tree Shaking 从打包文件中删除未使用的导出项(exports)以减小文件大小。git
npm install babel-core babel-loader babel-preset-es2015 --save-dev
最后,用如下内容替换package.json
的scripts
部分:github
"scripts": { "start": "webpack --watch", "build": "webpack -p" },
在命令行中运行npm start
将以监视模式启动webpack,当src
目录中的.js
文件更改时,它将从新编译bundle.js。控制台中的输出展现了打包文件的信息,注意打包文件的数量和大小十分重要。web
如今当你在浏览器中加载index.html页面时会看到"Hello webpack."。npm
open index.html
打开dist/bundle.js
文件看看webpack都作了哪些事情,顶部是webpack的模块引导代码,底部是咱们的模块。到目前为止,你可能对于这个尚未一个深入的印象。可是如今你能够开始编写ES6模块,webpack将生成适用于全部浏览器的打包文件。
使用 Ctrl + C
中止webpack,运行npm run build
以生产模式编译咱们的bundle.js
。
注意,打包文件的大小从2.61 kB减小到了585字节。再看一下dist/bundle.js
文件,你会看到大量难懂的代码,由于咱们使用UglifyJS压缩了它。这样的话,咱们可使用更少的代码达到与以前同样的效果。
优秀的webpack知道如何使用各类格式的JavaScript模块,最著名的两个是:
ES2015 import
语句
CommonJS require()
语句
咱们能够经过安装、导入lodash来测试这些格式的模块。
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
这样没有相对路径的导入是导入安装在/node_modules
的模块,你本身的模块须要一个相似./people
的相对路径,你能够以此区分它们。
咱们已经介绍过,你能够经过配置像babel-loader
这样的加载器来告诉webpack在遇到不一样文件类型的导入时该怎么作。你能够将多个加载器组合在一块儿,应用到不少文件转换中。在JS中导入.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
从新启动构建。
加载器序列以相反的顺序进行处理:
sass-loader
将Sass转换为CSS。
css-loader
将CSS解析为JavaScript并解析全部依赖。
style-loader
将咱们的CSS输出到文档中的<style>标签。
你能够将它们看做函数调用,将一个加载器的输出输入到下一个加载器中。
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); }
你如今能够直接在JavaScript中导入Sass文件。
// src/app.js import './style.scss' // ...
从新加载index.html,你应该会看到一些样式。
咱们刚刚在JavaScript中将一个sass文件做为模块导入了。
打开dist/bundle.js
并搜索“pre {”
。事实上,咱们的sass已被编译成一串CSS,并做为一个模块保存在咱们的打包文件中。当咱们将这个模块导入JavaScript中时,style-loader
会将这个字符串输入到嵌入的<style>
标签中。
我知道你在想什么---为何?
我不会在这里过多的讨论这个问题,但这里有几个值得了解的缘由:
你想要包含在项目中的JavaScript组件可能依赖于其余资源才能正常运行(HTML,CSS,图片,SVG),若是这些资源均可以打包在一块儿,那么导入和使用将会更容易。
消除死代码:当你的代码再也不导入JS组件时,CSS也将再也不被导入。生成的打包文件将只会包含执行某些操做的代码。
CSS模块:CSS的全局命名空间很难保证对CSS的更改不会产生任何反作用。CSS模块经过将CSS默认设置为本地命名空间、提供能够在JavaScript中引用的惟一类名来更改这一模式。
经过打包、分割代码等巧妙的方式来减小HTTP请求的数量。
加载器的最后一个例子是使用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 } // 将大小小于10kb的图片转为base64字符串 }] }, { // ... }]
使用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) // ...
这样将引入一个图片,其中src属性包含图片自己的数据URL。
<img src="data:image/png;base64,iVBO..." style="background: #2B3A42; padding: 20px" width="32">
此外,因为css-loader
保障了使用url()引用的图片也能够经过url-loader
运行,因此能够直接在CSS中内联它。
/* 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主页上的图片所展现的那样:
虽然JavaScript是入口点,可是webpack认为其余类型资源(如HTML,CSS和SVG)都有本身的依赖关系,因此这些类型的资源应该被视为构建过程的一部分。
咱们已经看到一个内置的webpack插件的例子,在npm run build
脚本中调用的webpack -p
命令就是使用webpack附带的UglifyJsPlugin
插件以生产模式压缩打包文件。
加载器能够对单个文件运行转换,插件能够运行在更大的代码块上。
commons-chunk-plugin
是webpack附带的另外一个核心插件,用于建立一个单独的模块,为多个入口文件分享公共代码。到目前为止,咱们一直在使用单个入口文件和单个输出打包文件。在许多实际场景中,你将受益于将其分解为多个输入和输出文件。
若是你的应用程序有两个不一样的区域须要分享某个模块,例如:用于面向公共应用程序的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
注意output.filename
的变化,如今包含了[name]
,它会被替换为块名称。所以咱们能够从这个配置中获得两个输出文件、也是咱们的两个入口文件: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包括style
和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
能够看到它们自动的建立了通用模块。
另外一个流行的插件是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
须要一个依赖关系的列表,一个回调和一个可选的模块的名称。
若是你的应用中有一段依赖于应用其余部分不须要的依赖,那最好把它分离成单独的包。咱们经过添加一个名为dashboard.js
的新模块来演示一下,这个模块须要引入d3
模块。
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' }, // ... }
从新启动构建,你会看到一个神秘的新打包文件0.bundle.js
。
webpack为了提醒你,使用[big]
来突出显示较大的包,
这个0.bundle.js
将根据须要使用JSONP请求获取,所以直接从文件系统加载文件不会再加载它。咱们须要运行一个服务器,任何服务器均可以。
python -m SimpleHTTPServer 8001
打开http://localhost:8001/
加载后一秒钟,你应该看到一个指向咱们动态生成的打包文件 /dist/0.bundle.js
的GET请求和打印到控制台的“Loaded!”。成功!
实时从新加载能够经过在文件更改时自动刷新来真正改善开发人员体验。只需安装它,并使用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
中的一个样式,你会切身感觉到这一好处。
若是你对实时从新加载只是印象深入,那么热模块替换(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时,这些将被从打包文件中分离出来。
将NamedModulesPlugin
添加到webpack.config.js
中的插件列表中以改善控制台中的日志记录性能。
plugins: [ new webpack.NamedModulesPlugin(), // ... ]
最后,在页面中添加一个<input>元素,咱们能够在输入框中添加一些文本,以证实在咱们更改模块的时候不会发生全页刷新。
<body> <input /> <div id="root"></div> ...
用npm start
重启服务器来看看热加载!
在输入框中输入“HMR规则”,而后在people.js
中更更名称,你会发如今不刷新页面的状况下发生了内容更新而且输入框丢失输入聚焦状态。
这只是一个简单的示例,可是但愿你能意识到这是很是有用的。对于像React这样基于组件的开发这更是十分有用的,你有不少“笨”组件须要与其状态分离,组件能够在不丢失状态的状况下被更新并从新呈现,所以你能够不断的得到即时反馈。
更改style.scss中<pre>
元素的背景颜色,你会发现它并无被HMR更新。
pre { background: red; }
事实证实,当你使用style-loader
时,CSS的HMR能够直接使用而不须要作任何操做。咱们经过将CSS模块提取到外部的没法替代的CSS文件中来去除这个关联。
若是咱们将Sass规则恢复到初始状态,并从插件列表中删除extractCSS
,那么你也能够看到Sass的热加载。
{ test: /\.scss$/, loader: ['style-loader', 'css-loader','sass-loader'] }
使用像webpack这样的模块打包工具的好处主要是你能够经过控制资源的构建方式来帮助你提升应用性能。多年来,将文件链接起来以减小客户端上须要的请求数量一直被认为是最佳实践。但HTTP / 2如今容许在单个请求中传送多个文件,所以链接文件再也不是具备极端有效性的解决方法,可是它仍然很重要。你的应用程序实际上也能够从多个拥有单独缓存的小文件中受益,客户端能够获取单个更改的模块,而没必要再次请求存在大部分相同内容的整个包。
我但愿这个关于webpack2的介绍对你有所帮助、可以开始使用它来产生很好的效果。围绕webpack的配置、加载器和插件的学习可能须要一些时间,可是了解这个工具的工做原理确定是颇有好处的。