Webpack 怎么用

配合上一篇文章做的翻译: https://github.com/petehunt/webpack-howto


这份教程的目的

这是 Webpack 怎么做事情的烹饪书. 其中包含了我们在 Instagram 做的大部分功能, 而且都是在用的功能.

我的建议是: 把这个当成是 Webpack 的文档开始学习, 然后看官方文档的具体说明.

预备条件

  • 你懂 Broserify, RequireJS 或者类似的打包工具
  • 你注重这些东西:

    • 代码分包
    • 异步加载
    • 静态资源(图片, CSS)的打包

1. 为什么用 webpack?

  • 他像 Browserify, 但是将你的应用打包为多个文件. 如果你的单页面应用有多个页面, 那么用户只从下载对应页面的代码. 当他么访问到另一个页面, 他们不需要重新下载通用的代码.

  • 他在很多地方能替代 Grunt 跟 Gulp 因为他能够编译打包 CSS, 做 CSS 预处理, 编译 JS 方言, 打包图片, 还有其他一些.

它支持 AMD 跟 CommonJS, 以及其他一些模块系统, (Angular, ES6). 如果你不知道用什么, 就用 CommonJS.

2. Webpack 给 Browserify 的同学用

对应地:

jsbrowserify main.js > bundle.js
jswebpack main.js bundle.js

Webpack 比 Browserify 更强大, 你一般会用 webpack.config.js 来组织各个过程:

js// webpack.config.js
module.exports = {
  entry: './main.js',
  output: {
    filename: 'bundle.js'       
  }
};

这仅仅是 JavaScript, 可以随意添加要运行的代码.

3. 怎样启动 webpack

切换到有 webpack.config.js 的目录然后运行:

  • webpack 来执行一次开发的编译
  • webpack -p for building once for production (minification)
  • webpack -p 来针对发布环境编译(压缩代码)
  • webpack --watch 来进行开发过程持续的增量编译(飞快地!)
  • webpack -d 来生成 SourceMaps

4. JavaScript 方言

Webpack 对应 Browsserify transform 和 RequireJS 插件的工具称为 loader. 下边是 Webpack 加载 CoffeeScript 和 Facebook JSX-ES6 的配置(你需要 npm install jsx-loader coffee-loader):

js// webpack.config.js
module.exports = {
  entry: './main.js',
  output: {
    filename: 'bundle.js'       
  },
  module: {
    loaders: [
      { test: /\.coffee$/, loader: 'coffee-loader' },
      { test: /\.js$/, loader: 'jsx-loader?harmony' } // loaders 可以接受 querystring 格式的参数
    ]
  }
};

要开启后缀名的自动补全, 你需要设置 resolve.extensions 参数指明那些文件 Webpack 是要搜索的:

js// webpack.config.js
module.exports = {
  entry: './main.js',
  output: {
    filename: 'bundle.js'       
  },
  module: {
    loaders: [
      { test: /\.coffee$/, loader: 'coffee-loader' },
      { test: /\.js$/, loader: 'jsx-loader?harmony' }
    ]
  },
  resolve: {
    // 现在可以写 require('file') 代替 require('file.coffee')
    extensions: ['', '.js', '.json', '.coffee'] 
  }
};

5. 样式表和图片

首先更新你的代码用 require() 加载静态资源(就像在 Node 里使用 require()):

jsrequire('./bootstrap.css');
require('./myapp.less');

var img = document.createElement('img');
img.src = require('./glyph.png');

当你引用 CSS(或者 LESS 吧), Webpack 会将 CSS 内联到 JavaScript 包当中, require() 会在页面当中插入一个 `<style>标签. 当你引入图片, Webpack 在包当中插入对应图片的 URL, 这个 URL 是由require()` 返回的.

你需要配置 Webpack(添加 loader):

js// webpack.config.js
module.exports = {
  entry: './main.js',
  output: {
    path: './build', // 图片和 JS 会到这里来
    publicPath: 'http://mycdn.com/', // 这个用来成成比如图片的 URL
    filename: 'bundle.js'
  },
  module: {
    loaders: [
      { test: /\.less$/, loader: 'style-loader!css-loader!less-loader' }, // 用 ! 来连接多个人 loader
      { test: /\.css$/, loader: 'style-loader!css-loader' },
      {test: /\.(png|jpg)$/, loader: 'url-loader?limit=8192'} // 内联 base64 URLs, 限定 <=8k 的图片, 其他的用 URL
    ]
  }
};

6. 功能开关

有些代码我们只想在开发环境使用(比如 log), 或者 dogfooging 的服务器里边(比如内部员工正在测试的功能). 在你的代码中, 引用全局变量吧:

jsif (__DEV__) {
  console.warn('Extra logging');
}
// ...
if (__PRERELEASE__) {
  showSecretFeature();
}

然后配置 Webpack 当中的对应全局变量:

js// webpack.config.js

// definePlugin 接收字符串插入到代码当中, 所以你需要的话可以写上 JS 的字符串
var definePlugin = new webpack.DefinePlugin({
  __DEV__: JSON.stringify(JSON.parse(process.env.BUILD_DEV || 'true')),
  __PRERELEASE__: JSON.stringify(JSON.parse(process.env.BUILD_PRERELEASE || 'false'))
});

module.exports = {
  entry: './main.js',
  output: {
    filename: 'bundle.js'       
  },
  plugins: [definePlugin]
};

然后你在控制台里用 BUILD_DEV=1 BUILD_PRERELEASE=1 webpack 编译. 注意一下因为 webpack -p 会执行 uglify dead-code elimination, 任何这种代码都会被剔除, 所以你不用担心秘密功能泄漏.

7. 多个进入点(entrypoints)

比如你用 profile 页面跟 feed 页面. 当用户访问 profile, 你不想让他们下载 feed 页面的代码. 因此你创建多个包: 每个页面一个 "main module":

js// webpack.config.js
module.exports = {
  entry: {
    Profile: './profile.js',
    Feed: './feed.js'
  },
  output: {
    path: 'build',
    filename: '[name].js' // 模版基于上边 entry 的 key
  }
};

针对 profile, 在页面当中插入 <script src="build/Profile.js"></script>. feed 页面也是一样.

8. 优化共用代码

feed 页面跟 profile 页面共用不要代码(比如 React 还有通用的样式和 component). Webpack 可以分析出来他们有多少共用模块, 然后生成一个共享的包用于代码的缓存.

js// webpack.config.js

var webpack = require('webpack');

var commonsPlugin =
  new webpack.optimize.CommonsChunkPlugin('common.js');

module.exports = {
  entry: {
    Profile: './profile.js',
    Feed: './feed.js'
  },
  output: {
    path: 'build',
    filename: '[name].js'
  },
  plugins: [commonsPlugin]
};

在上一个步骤的 script 标签前面加上 <script src="build/common.js"></script> 你就能得到廉价的缓存了.

9. 异步加载

CommonJS 是同步的, 但是 Webpack 提供了异步指定依赖的方案. 这对于客户端的路由很有用, 你想要在每个页面都有路由, 但你又不像在真的用到功能之前就下载某个功能的代码.

声明你想要异步加载的那个"分界点". 比如:

jsif (window.location.pathname === '/feed') {
  showLoadingState();
  require.ensure([], function() { // 语法奇葩, 但是有用
    hideLoadingState();
    require('./feed').show(); // 函数调用后, 模块保证在同步请求下可用
  });
} else if (window.location.pathname === '/profile') {
  showLoadingState();
  require.ensure([], function() {
    hideLoadingState();
    require('./profile').show();
  });
}

Webpack 会完成其余的工作, 生成额外的 chunk 文件帮你加载好.

Webpack 在 HTML script 标签中加载他们时会假设这些文件是怎你的根路径下. 你可以用 output.publicPath 来配置.

js// webpack.config.js
output: {
    path: "/home/proj/public/assets", // path 指向 Webpack 编译能的资源位置
    publicPath: "/assets/" // 引用你的文件时考虑使用的地址
}

相关资源

看一看真实环境当中一个成功的团队怎么使用 Webpack: http://www.tudou.com/programs/view/6KB6lNbVzhs/ .
这是 Peter Hunt 在 OSCon 大会关于 Instagram 如何使用 Webpack 的演讲.

FAQ

Webpack 看起来不是模块化的?

Webpack 是非常地模块化. Webpack 的优秀之处在于相比 Browserify 跟 RequireJS 之类方案, 能让插件将自身集成到编译过程当中的更多地方. 很多看起来像是编写在核心代码的功能, 其实就是默认加载的插件, 他们可以被覆盖.(比如, CommonJS 的 require() parser)

如果觉得我的文章对你有用,请随意赞赏

你可能感兴趣的文章

33 条评论
题叶 作者 · 2015年04月21日

Webpack 是针对前端资源打包的, 把 js, CSS, 图片, 字体等资源当作模块, 然后用一份配置控制怎么加载. 他的插件体系 abcdefg-loader 一般就是用来加载某种类型的资源.

Gulp 是一个任务管理的流程, 可以把各种各样的插件串在一起. 比如 js 压缩, css 编译合并, 图片压缩.. 变成一个任务. gulp-abcdefg 一般就是专门做读取/转换/写入的功能.

所以两个也可以合在一起..

+2 回复

王道中强流 · 2015年03月07日

怎么在 webpack 中使用 uikit ?http://segmentfault.com/q/1010000002583716

回复

flybear · 2015年04月21日

想请教下,webpack的异步加载和seajs requirejs的区别,以及有没有webpack异步加载的完整实例,想看看了解下!

回复

题叶 作者 · 2015年04月21日

异步加载是我没有实践过的, 所以回答不上啊. 到这边问下看看.. http://react-china.org/

回复

flybear · 2015年04月21日

好的...

回复

黑色技术 · 2015年04月21日

想问下具体作用,因为还是看不懂,从我个人理解其中一个功能是把js文件合并?他跟gulp有什么区别吗?我在npm上找到一款gulp-webpack插件 https://www.npmjs.com/package/gulp-webpack 对于新东西,还是不理解,见笑.

回复

RayLiao · 2015年06月03日

webpack类同requirejs,gulp是另外一回事,类同grunt。

回复

ZheX · 2015年06月07日

在代码中require了太多的资源,比如css,image,就会影响单元测试代码了,不知道这个问题有什么解决方案?

回复

题叶 作者 · 2015年06月07日

表示没有尝试过..

回复

RayLiao · 2015年06月11日

请问,第三方的插件or框架要怎么引用?像jquery,我要全局引用的话。

回复

RayLiao · 2015年06月11日

require("imports?$=jquery!./file.js"),是这个吗?感觉语法有点复杂,习惯了requirejs,配置个路径,然后依赖直接引用jquery就可以了。

回复

题叶 作者 · 2015年06月11日

我记得 jQuery 也有 CommonJS 版本的模块的, 不是非要在 require 当中写 loader.
另外在 webpack.config.js 当中可以针对 jQuery 专门写规则的, 这样也可以避免掉重复.

回复

RayLiao · 2015年06月11日

jQuery是支持CommonJS的,config.js那不知道要怎么写,Make $ and jQuery available in every module without writing require("jquery").new webpack.ProvidePlugin({ $: "jquery", jQuery: "jquery", "window.jQuery": "jquery" })是这个吗?

回复

题叶 作者 · 2015年06月11日

没有研究那么细, 可能我记错了, jQuery 从 npm 安装的版本还是有一些 AMD 代码在里边..
我们项目里边用的配置是这样的, expose-loader 去暴露 jQuery 给全局.

js{test: require.resolve('jquery'), loader: 'expose?jQuery'},

不过照说直接引入应该也能用才对.. 不够深入, 只能帮到这里了
另外你给的那份配置我不清楚具体到什么效果, 提个问题问一下吧

回复

RayLiao · 2015年06月11日

ok,谢谢。

回复

Fakefish · 2015年06月26日

刚看到这篇文章。。我觉得cookbook翻译成小书会更好

回复

题叶 作者 · 2015年06月27日

回复

Fakefish · 2015年06月28日

啊呀 是说cookbook的翻译啦,翻译成烹饪书太英文化了,就是翻译成小书会好理解一点。之前存志翻译的 coffee cookbook就翻译成coffee小书,感觉很不错啊。

回复

Fakefish · 2015年06月30日

准备翻译那个了

回复

载入中...
题叶 题叶

14.9k 声望

发布于专栏

题叶, JiyinYiyong

FP, GUI & Writing, http://tiye.me

376 人关注

SegmentFault

一起探索更多未知

下载 App