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.6k 声望

发布于专栏

题叶, JiyinYiyong

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

357 人关注

SegmentFault

一起探索更多未知

下载 App