Webpack 2 的文档完成就会推出 beta 版本。如果你已经知道怎么去配置它,那么你无需等待文档就可以使用 Webpack 2 了。
什么是 Webpack?
最初的时候,Webpack 仅是一个 JavaScript 的模块打包工具。随着 Webpack 日渐流行,逐渐演变成了前端代码的管理工具(不论是人为故意还是社区推动的)。
以前的运行方式是:标记文件、样式文件和 JavaScript 文件都被分割开的。你必须要独立管理每一个文件,使得所有文件可以正确的运行。
像 Gulp 这样的构建工具可以操作许多不同的预处理器和编译器,不过基本上所有的工作都看成是把一个源文件作为输入,经过处理后生成一个编译后的输出文件。Gulp 做的工作更像是一个任务接着一个任务的进行的,没有从全局的管理上考虑。这就会加重开发者的负担:在生产环境下,开发者需要知道任务在哪里结束,然后需要正确地把所有任务都组装在一起。
Webpack 尝试询问一个大胆的问题来减轻开发者的负担:是否有一个开发过程可以处理所引用到的依赖?我们是否可以简单地用某种方式去写代码,而构建程序会去管理最终所必需使用到的代码?
Webpack 的方式是:如果 Webpack 知道了它,它会把你实际用到的东西打包在构建产物里。
如果过去几年你已经混迹在 web 社区里,你应该知道解决问题更好的方式是:用 JavaScript 构建。Webpack 可以通过 JavaScript 传递依赖让构建过程更加容易。Webpack 的设计真正厉害之处并不在于代码的管理,而是它的管理层 100% 都是由 JavaScript 写的(Node 特性)。Webpack 使得你有能力在写 JavaScript 时候对系统全局有更好的把握和掌控。
换句话说:你不需要为 Webpack 写任何代码,只需要给项目写代码,然后Webpack会自动运行(当然,一些配置文件是少不了的)
简而言之,如果你已经与以下任意一个问题纠结过:
-
加载依赖项
-
在生产版本中包含了未使用的 CSS 或者 JS
-
意外多次加载同一个库
-
遇到 CSS 和 JavaScript 作用域的问题
-
在 JavaScript 里使用一个例如 Node/Bower 这样很好的管理系统,或者依赖一个可怕疯狂的配置来正确使用那些模块
-
需要更好地优化生产出资源,但又怕破坏到某些事情
那么,你可以从 Webpack 获得非常多的帮助。Webpack 可以轻易地解决上面问题,因为它可以通过 JavaScript 管理你的依赖和加载顺序,而不是通过你的开发者头脑。最棒的部分?Webpack 甚至可以直接地运行在服务端,这意味着你可以使用 Webpack 构建渐进加强的网站。
第一步
在这篇指导里,我们会使用 Yarn (brew install yarn
) 来替代 npm
,不过这个取决于你,它们都可以做到同样的事情。在项目文件夹下,我们会在终端窗口里执行以下的指令,在全局和本地项目里添加 Webpack 2。
yarn global add webpack@2.1.0-beta.25 webpack-dev-server@2.1.0-beta.10
yarn add --dev webpack@2.1.0-beta.25 webpack-dev-server@2.1.0-beta.10
在项目根目录下声明一个 webpack 配置文件 webpack.config.js
:
'use strict';
const webpack = require("webpack");
module.exports = {
context: __dirname + "/src",
entry: {
app: "./app.js",
},
output: {
path: __dirname + "/dist",
filename: "[name].bundle.js",
},
};
注意:__dirname
是指你的项目根目录
你知道 Webpack 是怎么知道项目如何运行的吗?它是通过读取你的代码来获知这一信息的,Webpack 基本上会像以下这个流程进行工作:
-
从
context
文件夹开始 -
寻找
entry
上的文件名 -
读取内容。每当遇到
import
(ES6) 或者require()
(Node) 依赖项时,它会解析这些代码,并且打包到最终构建里。接着它会不断递归搜索实际需要的依赖项,直到它到达了“树”的底部。
- 从上一步接着,Webpack 把所有东西打包到
output.path
的文件夹里,并使用output.filename
命名([name]
表示使用entry
项的 key)
所以我们的 src/app.js
文件看起来就像这样(假设我们之前执行的是 yarn add --dev moment
):
'use strict';
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
表示 “生产” 模式,这时候的输出文件会被 uglifies/minifies。
它会输出一个 dist/app.bundle.js
文件,同时在控制台里打出当前日期时间的日志。注意,Webpack 会自动知道 'moment'
模块是指向哪里(即使在目录里,你有一个 moment.js
文件,默认情况下 Webpack 还是会优先使用 moment
Node 模块)。
多文件工作
你可以通过修改 entry
对象来指定任意数量的输入或者输出点。
多个文件打包在一起
'use strict';
const webpack = require("webpack");
module.exports = {
context: __dirname + "/src",
entry: {
app: ["./home.js", "./events.js", "./vendor.js"],
},
output: {
path: __dirname + "/dist",
filename: "[name].bundle.js",
},
};
根据数组顺序,文件全部会被一起打包在 dist/app.bundle.js
里。
多个文件,多个输出
const webpack = require("webpack");
module.exports = {
context: __dirname + "/src",
entry: {
home: "./home.js",
events: "./events.js",
contact: "./contact.js",
},
output: {
path: __dirname + "/dist",
filename: "[name].bundle.js",
},
};
或者,你可以选择把你的应用打包成多个 JS 文件,上面的例子就会被打包成以下三个文件:dist/home.bundle.js
、 dist/events.bundle.js
, 和 dist/contact.bundle.js
。
先进的自动打包
如果你将应用分开打包到多个 output
文件里(如果你的应用有非常多的 JS 不需要在前期加载,这样做是非常有效的),这里面是有可能会出现冗余代码的,因为 Webpack 是独立解析每个文件的依赖的。幸运的是,Webpack 已经有个内置的 CommonsChunk 插件处理这个问题:
module.exports = {
// …
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: "commons",
filename: "commons.js",
minChunks: 2,
}),
],
// …
};
现在,纵观所有 output
文件,如果你有任何模块需要加载 2
次或者更多次(由 minChunks
设置),这些模块会被打包在 commons.js
里,那么你就可以让它在客户端里缓存起来。当然,这会导致产生一个额外的头部请求,但是你可以阻止客户端多次下载同一个库。在许多场景下,会有一个速度的净收益。
开发环境
Webpack 实际上有自己的开发服务器,因此无论你是想开发一个静态网站还是做一个前端的原型,它都可以满足你。如果想这样开发,只需要添加一个 devServer
对象到 webpack.config.js
就可以:
module.exports = {
context: __dirname + "/src",
entry: {
app: "./app.js",
},
output: {
filename: "[name].bundle.js",
path: __dirname + "/dist/assets",
publicPath: "/assets", // New
},
devServer: {
contentBase: __dirname + "/src", // New
},
};
在终端执行:
webpack-dev-server
现在你的服务器跑在 localhost:8080
。_注意 script 标签里的 /assets
路径由 _output.publicPath_
决定——你可以随便命名(如果你需要一个 CDN,这就非常有用了)_。
你无需刷新浏览器,Webpack 会热加载任何 JavaScript 的改变。但是,任何对 webpack.config.js
文件的改变都需要重启服务器才会生效。
全局调用方法
需要使用在全局作用域下的函数?只需要在 webpack.config.js
的 output.library
进行简单的设置:
module.exports = {
output: {
library: 'myClassName',
}
};
它会把你的打包文件捆绑在 window.myClassName
实例上。当设置了声明作用域时,你可以在入口处调用这个方法(更多的设置可在文档中查询)。
Loaders
直到现在,我们仅仅处理了 JavaScript 文件。从 JavaScript 开始是很重要的,因为 JavaScript 是 Webpack 唯一识别的语言。但实际上,只要是用 JavaScript 传递的文件,我们都可以使用 Loaders 来处理任何种类的文件。
loader 可以是像 Sass 这样的预处理器,也可以是像 Babel 这样的编译器。在 NPM 里,它们通常被命名为 *-loader
,例如:sass-loader
或者 babel-loader
。
Babel + ES6
如果你想在项目里通过 Babel 使用 ES6,我们首先需要本地安装合适的 loaders。
yarn add --dev babel-loader babel-core babel-preset-es2015
然后把它添加到 webpack.config.js
,以便于让 Webpack 知道在什么地方使用它。
module.exports = {
// …
module: {
rules: [
{
test: /\.js$/,
use: [{
loader: "babel-loader",
options: { presets: ["es2015"] }
}],
},
// Loaders for other file types can go here
],
},
// …
};
Webpack 的老用户需要注意一点:Loaders 的核心理念还是保持一致的,但是它的语法有所改善。直到文档完成了,我们才能知道准确合适的语法。
/\.js$/
这条正则表达式会去搜索以 .js
后缀结束的文件,并通过 Babel 加载。Webpack 使用正则表达式来让你可以完整的控制所有文件,而无需限定在某一个文件扩展名,又或者假定你是使用某种方式进行文件组织的。
CSS + Style Loader
如果我们只想加载应用所需的 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:
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
标签里。刚开始看起来是有点奇怪,但是随着深入思考你会觉的这是有意义的。因为你可以在一些网络连接上节省一个头部请求的时间,并且只要你用了 JavaScript 来加载 Dom 节点,这可以有效地减少它自身的 FOUC。
你也会发现 Webpack 黑盒的构建产物已经自动地通过将文件打包在一起,把所有的 @import
查询都解析了(而不是依靠 CSS 默认会导致浪费头部请求和加载资源非常慢的 import 功能)。
从 JS 里加载 CSS 是非常神奇的,因为这样你可以用新的方式将 CSS 模块化。也就是说你可以通过 button.js
加载 button.css
,而如果 button.js
实际上没有用到,对应的 CSS 也不会被调价到生产构建中。如果你是坚持使用面向组件 CSS 实践方法的,例如:SMACSS 或者 BEM,你会体会到将 CSS 和 标记语言 + JavaScript 更加紧密联系的价值。
CSS + Node 模块
我们可以使用 Webpack 里的 ~
前缀来引入 Node 模块。假如我们执行了 yarn add normalize.css
,那么就可以这么用:
@import "~normalize.css";
这样可以充分利用 NPM 管理第三方样式文件的版本,还可以避免了我们进行复制黏贴。更长远地看,用 Webpack 打包 CSS 相比使用 CSS 默认的 import 有明显的优势,这是因为它可以为客户端消除头部请求以及缓慢的加载时间。
更新:本章节和下一章节为了解答对通过 CSS 模块来简单引入 Node 模块的疑惑,以及提高文章的准确性进行了更新。感谢 Albert Fernández 的帮助。
CSS 模块
你可能已经听说过 CSS 模块,当你使用 JavaScript 构建 Dom 节点,它可以不出意料地运行地非常好,它通过 JavaScript 加载,然后把你的 CSS 类神奇地设置好层叠作用域。如果你准备使用 CSS 模块,可以用 css-loader
来打包(yarn add --dev css-loader
):
module.exports = {
// …
module: {
rules: [
{
test: /\.css$/,
use: [
"style-loader",
{ loader: "css-loader", options: { modules: true } }
],
},
// …
],
},
};
注意:我们通过使用对象字面量的语法给 css-loader
传递选项。你也可以用一个字符串作为简写来传递,这样就会使用默认的选项,就像我们用 style-loader
这样。
在用 Node 模块引入 CSS 模块的时候,需要注意的是你实际上可以抛弃 ~
来直接引入。但是,当你 @import
CSS 时,你可能会遇到一个构建错误。如果你得到是类似 "can’t find ..." 这样的错误,你可以尝试给 webpack.config.js
添加一个 resolve
对象,这样会让 Webpack 对预定的模块顺序有更好的理解。
const path = require("path");
module.exports = {
//…
resolve: {
modules: [path.resolve(__dirname, "src"), "node_modules"]
},
};
我们规定了源文件目录,这样的做话 Webpack 可以解析地更加好。Webpack 会根据模块名首先去寻找我们的源文件目录,然后是安装的 Node 模块目录里(也就是分别用 "src"
和 "node_modules"
来代替源文件目录和 Node 模块目录)。
Sass
需要用 Sass?没问题,可以这么安装:
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 分开打包
也许你需要处理渐进加强;又或许因为一些别的原因,你需要把 CSS 独立出来一个文件。我们无需修改任何代码,只用简单的在配置里把 extract-text-webpack-plugin
代替 style-loader
。下面的 app.js
就是例子:
import styles from './assets/stylesheets/application.css';
安装本地插件(我们需要 2016 年 10 月的 bata 版本)...
yarn add --dev extract-text-webpack-plugin@2.0.0-beta.4
添加到 webpack.config.js
:
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 里添加一个 tag
引入就可以了。
HTML
正如你想到的一样,Webpack 也有一个叫 html-loader 的插件。但是当我们准备用 JavaScript 去加载 HTML 的时候,这里有一点需要注意,我无法通过一个单独的例子就为你下一步做的事情准备好,因为这可能分出无数各种各样地方法。通常来说,你可能是为了在 React、 Angular、 Vue,又或者 Ember 这些大型框架里使用类似 JSX 、Mustache,或者 Handlebars 这样的 JavaScript-flavored 标记语言。又或者你只是在使用一个 HTML 的预处理器,例如:Pug (Jade 的前身)或者 Haml。再者你可能仅仅只是想按字面意思一样把源文件里的 HTML 丢到构建目录里。无论你想做什么,我都是无法假定。
结束语:你可以用 Webpack 加载标记语言文件,但是你需要决定自身的项目架构,这是 Webpack 和我都无法为你做这个决定的。不过根据上述例子作为参照,然后在 NPM 上搜索正确的 loaders,这些对于你来说应该是足够用的了。
用模块的方式思考
为了最大程度的使用 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 来实现更好的模块化,而不需要考虑运行作用域。