在 Webpack 2 的文档 完成之后,就会推出 Webpack 2 的 beta 版本, 但这并不意味着你知道怎么配置 Webpack 2, 却不能在项目中使用 Webpack 2.
简单来说, Webpack 是一个针对 JavaScript 的打包工具. 然而, 随着 Webpack 日渐流行, 它逐渐演变成了前端代码的管理工具(不论是人为故意还是社区推动的).
之前的任务管理方式是: HTML文件、样式和JavaScript是各自独立的, 你必须分开地管理每一个文件, 并确保一切能正常运行.
类似Gulp的任务管理工具能处理多个不同的预处理器和编译器, 但是在所有情况下, 这都是将一个文件作为源输入, 经过处理后输出编译后的文件. 然而, Gulp 完成这些工作就像是一个任务接一个任务进行的, 没有从系统(或全局)的角度考虑如何完成任务的输入和输出. 这成了开发者的负担: 在生产环境下, 开发者需要找到任务结束的地方, 并通过合理地方式将所有的任务有序地组装在一起.
而Webpack则尝试询问一个大胆的问题来减轻开发者的负担: 假如在开发过程中的某一个部分能处理其所有的依赖会怎么样呢? 假如我们可以简单地用某种方式去写代码, 而构建程序去管理最终所必需使用到的代码又会怎么样呢?
Webpack的方式是: 如果webpack知道依赖的资源, 它就会将项目实际用到的资源构建到生产环境中.
如果过去几年你都混迹在 web 社区中, 你应该知道解决这个问题的更好方式是: 用 JavaScript 去构建. 而 Webpack 尝试通过JavaScript来解析依赖, 让构建过程变得更加简单. 但仅用于管理代码并不是Webpack设计的厉害之处, 其厉害之处在于Webpack的任务管路方式 100% 由JavaScript来完成的(利用了 Node 特性). Webpack 使你在写 JavaScript 时, 有能力从(项目的)全局角度掌控和把握整个项目.
换句话说: 你不是为Webpack写代码, 而是为你的项目写代码. 同时, webpack会自动运行(当然, 你需要写一些配置文件).
简而言之, 如果你曾经纠结过下面的任何一个问题:
那么, 你会从Webpack中收益, 因为 Webpack 能轻易地处理上述问题, 它会通过 JavaScript 来管理模块依赖和加载顺序而不是你的开发头脑. 此外, Webpack 能在服务端运行, 这意味着你能创建 渐进增强 的网站.
在这篇文章中, 我们将使用 Yarn ( brew install yarn
)而不是 npm
, 但这完全取决于你, 因为它们做的是同样的事. 在项目目录, 将运行下面的命令将 Webpack 2 添加到全局和本地项目中:
npm i -g webpack@beta webpack-dev-server@beta yarn add --dev webpack@beta webpack-dev-server@beta
注: 在本文中, 我们用简单的方式全局安装了 Webpack 2, 而不是通过被推荐的 NPM 脚本 . 两种方式都行, 文档 说明了二者的区别.
安装了 Webpack 2之后, 我们需要在项目的根目录下创建一个 webpack.config.js
文件:
const path = require('path'); const webpack = require('webpack'); module.exports = { context: path.resolve(__dirname, './src'), entry: { app: './app.js', }, output: { path: path.resolve(__dirname, './dist'), filename: '[name].bundle.js', }, };
注意: __dirname
是指你的项目根目录.
还记得 Webpack 是怎么知道项目如何运行的吗? 它是通过读取你的代码来获知这一信息的. Webpack 的工作流如下:
context
文件夹开始 entry
对应的文件 import
( ES6 ) 或者 require()
(Node) 依赖项时, 它会解析这些代码, 并且打包到最终构建里. 接着它会不断递归搜索实际需要的依赖项, 直到它到达了“树”的底部. output.path
对应的目录, 并将 output.filename
的值作为最终的资源名( [name]
表示使用 entry
项的 key). 如果 src/app.js
看起来像下面的样子(假设之前已运行了 yarn add moment
):
import moment from 'moment'; var rightNow = moment().format('MMMM Do YYYY, h:mm:ss a'); console.log(rightNow); // "October 23rd 2016, 9:30:24 pm"
(在项目的根目录下)运行:
webpack -p
p
表示'生产'模式, 输出文件会被 混淆/压缩.
运行命令之后, Webpack 会输出一个 dist/app.bundle.js
文件, 同时在控制台输出当前日期. 需要注意的是, Webpack 会自动找到 moment
的指向(即使你有一个 moment.js
存在于目录中, 但 Webpack 默认会优先去寻找 moment
的Node模块).
你可以修改 entry
对象, 指定任意数量的入口文件和输出文件.
const path = require('path'); const webpack = require('webpack'); module.exports = { context: path.resolve(__dirname, './src'), entry: { app: ['./home.js', './events.js', './vendor.js'], }, output: { path: path.resolve(__dirname, './dist'), filename: '[name].bundle.js', }, };
按照入口文件在数组中的顺序, 所有文件会被打包在一个 dist/app.bundle.js
里.
const path = require('path'); const webpack = require('webpack'); module.exports = { context: path.resolve(__dirname, './src'), entry: { home: './home.js', events: './events.js', contact: './contact.js', }, output: { path: path.resolve(__dirname, './dist'), filename: '[name].bundle.js', }, };
如果不想把所有打包在一个文件中, 你可以选择将多个文件打包在多个文件中. 上述例子会输出三个文件: dist/home.bundle.js
, dist/events.bundle.js
和 dist/contact.bundle.js
.
如果你将应用分开打包到多个 output
文件里(如果你的应用有非常多的 JavaScript 文件不需要在前期加载, 这样做是非常有效的), 有可能会出现很多冗余的代码, 因为 Webpack 是独立解析每个文件的依赖的. 幸运的是, Webpack 已经内置了 CommonsChunk
插件来处理这个问题:
module.exports = { // … plugins: [ new webpack.optimize.CommonsChunkPlugin({ name: 'commons', filename: 'commons.js', minChunks: 2, }), ], // … };
加上 CommonsChunk
插件后, 任何一个模块在你的 output
文件中被加载 2
次(该值由 minChunks
设置)及以上, 该模块就会被打包在 common.js
中, 你可以在客户端缓存这些模块. 虽然这会增加额外的请求, 但这能防止客户端多次下载同一个模块.
在开发环境中, Webpack 可以提供一个开发服务器, 因为无论是你正在开发一个静态网站还是仅用于项目的前端原型设计, 它都能满足你的需要. 为启动服务器, 仅需要在 webpack.config.js
中添加一个 devServer
对象:
module.exports = { context: path.resolve(__dirname, './src'), entry: { app: './app.js', }, output: { filename: '[name].bundle.js', path: path.resolve(__dirname, './dist/assets'), publicPath: '/assets', // New }, devServer: { contentBase: path.resolve(__dirname, './src'), // New }, };
在 src
目录下创建一个带如下标签的 index.html
文件:
<script src="/assets/app.bundle.js"></script>
在终端运行如下命令:
webpack-dev-server
开发服务器会运行在 localhost:8080
(打开你的浏览器访问该地址就能看到你的页面). 需要注意的是 script
标签里的 /assets
是和 output.publicPath
匹配的--你可以把它命名成任何你想要的名字(如果你使用CDN, 这会很有用).
Webpack 提供了热加载功能. 当你修改了 JavaScript 文件后, Webpack 会自动重新加载资源, 而不需要你手动去刷新浏览器. 但是, 任何对 webpack.config.js
文件的改变都需要重启服务器才会生效 .
需要使用在全局作用域下的函数? 只需在 output.library
进行简单的设置就行:
module.exports = { output: { library: 'myClassName', } };
它会把你的打包文件捆绑在 window.myClassName
实例上. 设置了作用域之后, 你可以在文件的入口处进行调用(更多设置可以查阅 文档 ).
到目前为止, 我只介绍了怎么使用 Webpack 处理 JavaScript 文件. 从处理 JavaScript 文件开始是非常重要的, 因为这是 Webpack 唯一能识别的语言. 实际上, Webpack 可以使用 Loaders 来处理各种通过 JavaScript 传递的任何类型的文件.
loader 可以是像 Sass
这样的预处理器, 也可以是像 Babel
这样的编译器. 在 NPM 里, 它们通常被命名为 *-loader
, 例如: sass-loader
或者 babel-loader
.
如果你想在项目里通过 Babel 使用 ES6, 首先需要安装合适的loader来编译 es6:
yarn add --dev babel-loader babel-core babel-preset-es2015
然后, 将loader添加到 webpack.config.js
中, 告诉 Webpack 在何处使用该loader:
module.exports = { // … module: { rules: [ { test: //.js$/, use: [{ loader: 'babel-loader', options: { presets: ['es2015'] } }], }, // Loaders for other file types can go here ], }, // … };
Webpack 1.x 的用户需要注意: Loaders 的核心理念和Webpack 2是保持一致的, 但是它的语法在Webpack 2中有所改善. 最终准确的语法需要等到 Webpack 2的文档完成之后才知道.
正则表达式 //.js$/
会去搜索任何 .js
后缀的文件, 然后通过 Babel 来加载这些文件. Webpack 依赖正则表达式来给你(对文件处理的)完全控制权---它不会限制你需要处理的文件扩展或者让你按照一定的方式来组织代码. 例如: 可能 /my_legacy_code/
目录下的文件不是 ES6 写的, 你可以修改上面的 test
字段为 /^((?!my_legacy_code).)*/.js$/
, 这样就能排除指定的文件目录里的js文件, 剩余的(js文件)则由 Babel 处理.
如果你的应用只需要加载 CSS, Webpack 也能满足需要. 创建一个 index.js
文件, 然后引入需要的 CSS 文件:
import styles from './assets/stylesheets/application.css';
然后会报错: You may need an appropriate loader to handle this file type
. 在上文说过, Webpack 仅能识别 JavaScript. 因此, 需要安装合适的loader来处理 CSS 文件:
yarn add --dev css-loader style-loader
然后在 webpack.config.js
中添加一条规则:
module.exports = { // … module: { rules: [ { test: //.css$/, use: ['style-loader', 'css-loader'], }, // … ], }, };
Loaders会按照数组的逆序运行, 也就是说, 会先运行 css-loader
, 后运行 style-loader
.
你可能会注意到, 在生产环境下, CSS 会被打包到 JavaScript 文件里, style-loader
则会把样式写在 style
标签中. 此外, Webpack 通过将这些文件打包成一个文件来自动地解析所有的 @import
查询(而不是依赖 CSS 的默认 import
功能, 这会导致额外的 header 请求, 并且加载资源非常慢).
从 JavaScript 中加载 CSS 是非常神奇的, 因为这样你可以用新的方式将 CSS 模块化. 也就是说, 可以仅通过 button.js
来加载 button.css
, 这意味着如果 button.js
没有用到, 其对应的 CSS 也不会被构建到生产环境中.
我们可以使用 Webpack 里的 ~
前缀来引入 Node 模块. 假如我们执行了 yarn add normalize.css
, 那么就可以这么用:
@import "~normalize.css";
这样就可以充分利用 NPM 管理第三方样式库的优点---版本更新和避免复制和粘贴. 更近一步, 和 CSS 默认的 import
功能相比, 用 Webpack 打包 CSS 有明显的优势, 因为它可以为客户端减少头部请求以及缓慢的加载时间.
你可能已经听过 CSS 模块 . 如果你通过 JavaScript 来构建 DOM 节点, 它能运行的很好. 从本质上来说, 它将你的CSS类扩展到加载它的JavaScript文件中了( 了解更多 ).
如果你要使用 CSS模块, 可以用 css-loader
来打包 CSS 文件( yarn add --dev css-loader
):
module.exports = { // … module: { rules: [ { test: //.css$/, use: [ 'style-loader', { loader: 'css-loader', options: { modules: true } } ], }, // … ], }, };
当启用了 CSS 的模块功能, 在 Node中引入CSS时去掉 ~
前缀是没有任何意义的(例如: @import "normalize.css";
). 然而在你通过 @import
引入自己的 CSS 文件时会碰到类似 can’t find ___
的构建错误, 解决方式是在 webpack.config.js
中添加一个 resolve
对象, 让 Webpack 对预定的模块顺序有更好的理解.
module.exports = { //… resolve: { modules: [path.resolve(__dirname, './src'), 'node_modules'] }, };
首先指定了源文件目录, 然后添加了 node_modules
目录. 这样, Webpack 在查找模块时, 会首先从源目录开始查找, (如果没找到)然后从已安装的 Node 模块中查找(你使用时, 要分别将 "src"
和 "node_modules"
目录替换成你的项目的源目录和Node模块目录).
需要Sass? 没问题, 首先安装loader:
yarn add --dev sass-loader node-sass
然后加一条规则:
module.exports = { // … module: { rules: [ { test: //.(sass|scss)$/, use: [ "style-loader", "css-loader", "sass-loader", ] } // … ], }, };
这样你就能在 JavaScript 文件里通过 import
来引用 .scss
或者 .sass
文件, 剩下的事情交给 Webpack 来处理.
你可能需要处理渐进增强的情况, 也可能因某些原因需要分离 CSS 文件. 这个也很简单, 只需将配置文件中的 style-loader
用 extract-text-webpack-plugin
代替就行. 例如:
import styles from './assets/stylesheets/application.css';
在本地安装该插件(需要安装2016年10月的Beta版本):
yarn add --dev extract-text-webpack-plugin@2.0.0-beta.4
然后修改下配置文件:
const ExtractTextPlugin = require('extract-text-webpack-plugin'); module.exports = { // … module: { rules: [ { test: //.css$/, loader: ExtractTextPlugin.extract({ loader: 'css-loader?importLoaders=1', }), }, // … ] }, plugins: [ new ExtractTextPlugin({ filename: '[name].bundle.css', allChunks: true, }), ], };
运行 webpack -p
之后, 你会发现在 output
指定的目录中会有一个 app.bundle.css
文件. 最后, 在 HTML 文件中通过 <link>
标签正常引用.
为了最大程度的使用 Webpack, 你必须用模块化、可复用性以及可独立处理的思维方式去思考, 让每个模块把各自负责的事情做好. 这意味着类似下面这样的文件:
└── js/ └── application.js // 300KB of spaghetti code
会变成这样:
└── js/ ├── components/ │ ├── button.js │ ├── calendar.js │ ├── comment.js │ ├── modal.js │ ├── tab.js │ ├── timer.js │ ├── video.js │ └── wysiwyg.js │ └── application.js // ~ 1KB of code; imports from ./components/
最后编译的结果是非常简洁且可复用的代码. 每个独立的组件通过 import
来引入依赖, 再通过 export
来暴露公共接口给其他模块. Babel + ES6就提供了上述特性, 并且你可以使用 JavaScript Classes 来实现更好的模块化, 而且不需要考虑运行作用域.
Getting Started with Webpack 2