Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[译]Webpack 2 快速入门 #46

Open
dwqs opened this issue Jan 22, 2017 · 3 comments
Open

[译]Webpack 2 快速入门 #46

dwqs opened this issue Jan 22, 2017 · 3 comments

Comments

@dwqs
Copy link
Owner

dwqs commented Jan 22, 2017

Webpack 2 的文档 完成之后,就会推出 Webpack 2 的 beta 版本, 但这并不意味着你知道怎么配置 Webpack 2, 却不能在项目中使用 Webpack 2.

什么是 Webpack

简单来说, Webpack 是一个针对 JavaScript 的打包工具. 然而, 随着 Webpack 日渐流行, 它逐渐演变成了前端代码的管理工具(不论是人为故意还是社区推动的).

webpack

之前的任务管理方式是: HTML文件、样式和JavaScript是各自独立的, 你必须分开地管理每一个文件, 并确保一切能正常运行.

类似Gulp的任务管理工具能处理多个不同的预处理器和编译器, 但是在所有情况下, 这都是将一个文件作为源输入, 经过处理后输出编译后的文件. 然而, Gulp 完成这些工作就像是一个任务接一个任务进行的, 没有从系统(或全局)的角度考虑如何完成任务的输入和输出. 这成了开发者的负担: 在生产环境下, 开发者需要找到任务结束的地方, 并通过合理地方式将所有的任务有序地组装在一起.

而Webpack则尝试询问一个大胆的问题来减轻开发者的负担: 假如在开发过程中的某一个部分能处理其所有的依赖会怎么样呢? 假如我们可以简单地用某种方式去写代码, 而构建程序去管理最终所必需使用到的代码又会怎么样呢?

webpack2

Webpack的方式是: 如果webpack知道依赖的资源, 它就会将项目实际用到的资源构建到生产环境中.

如果过去几年你都混迹在 web 社区中, 你应该知道解决这个问题的更好方式是: 用 JavaScript 去构建. 而 Webpack 尝试通过JavaScript来解析依赖, 让构建过程变得更加简单. 但仅用于管理代码并不是Webpack设计的厉害之处, 其厉害之处在于Webpack的任务管路方式 100% 由JavaScript来完成的(利用了 Node 特性). Webpack 使你在写 JavaScript 时, 有能力从(项目的)全局角度掌控和把握整个项目.

换句话说: 你不是为Webpack写代码, 而是为你的项目写代码. 同时, webpack会自动运行(当然, 你需要写一些配置文件).

简而言之, 如果你曾经纠结过下面的任何一个问题:

  • 依赖加载混乱
  • 在生产环境下, 引入了无用的 CSS 或者 JavaScript 文件(或代码)
  • 多次加载同一个库
  • 碰到 CSS、JavaScript 的作用域问题
  • 在 JavaScript 中找到一个很好地模块管理系统来使用 Node/Bower 模块或者依赖一个疯狂的配置来正确使用那些模块
  • 需要优化资源输出, 但担心会破坏某些事情

那么, 你会从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 的工作流如下:

  1. context 文件夹开始
  2. 查找 entry 对应的文件
  3. (找到文件之后)读取文件内容. 每当遇到 import (ES6) 或者 require() (Node) 依赖项时, 它会解析这些代码, 并且打包到最终构建里. 接着它会不断递归搜索实际需要的依赖项, 直到它到达了“树”的底部.
  4. 递归完所有依赖之后, Webpack 会将所有东西打包到 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.jsdist/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 实例上. 设置了作用域之后, 你可以在文件的入口处进行调用(更多设置可以查阅文档).

Loaders

到目前为止, 我只介绍了怎么使用 Webpack 处理 JavaScript 文件. 从处理 JavaScript 文件开始是非常重要的, 因为这是 Webpack 唯一能识别的语言. 实际上, Webpack 可以使用 Loaders 来处理各种通过 JavaScript 传递的任何类型的文件.

loader 可以是像 Sass 这样的预处理器, 也可以是像 Babel 这样的编译器. 在 NPM 里, 它们通常被命名为 *-loader, 例如: sass-loader 或者 babel-loader.

Babel+ES6

如果你想在项目里通过 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+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来处理 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 也不会被构建到生产环境中.

CSS+Node 模块

我们可以使用 Webpack 里的 ~ 前缀来引入 Node 模块. 假如我们执行了 yarn add normalize.css, 那么就可以这么用:

@import "~normalize.css";

这样就可以充分利用 NPM 管理第三方样式库的优点---版本更新和避免复制和粘贴. 更近一步, 和 CSS 默认的 import 功能相比, 用 Webpack 打包 CSS 有明显的优势, 因为它可以为客户端减少头部请求以及缓慢的加载时间.

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

需要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 分开打包

你可能需要处理渐进增强的情况, 也可能因某些原因需要分离 CSS 文件. 这个也很简单, 只需将配置文件中的 style-loaderextract-text-webpack-plugin 代替就行. 例如:

import styles from './assets/stylesheets/application.css';

在本地安装该插件(需要安装2016年10月的Beta版本):

yarn add --dev [email protected]

然后修改下配置文件:

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
Webpack2 升级指南和特性摘要
A Detailed Introduction To Webpack

@ysc123
Copy link

ysc123 commented Jun 20, 2017

666

@islishude
Copy link

如果使用npm5.2以上或者yarn的话就没必要全局安装webpack了,直接运行npx webpack或者yarn run webpack就可以了,这个命令会自动寻找node_modules目录的可执行文件

@Yalhu
Copy link

Yalhu commented Jan 30, 2018

在多个入口文件中,按顺序打包是什么意思。
在下面的代码中,events.js中不能调用home.js 中函数foo。会报错Uncaught ReferenceError: foo is not defined
如果event中要用home中的代码(好多个函数),不使用import /export ,有什么解决方法吗?

//webpack.config.js
entry: {
    app: ['./home.js', './events.js', './vendor.js'],
  },
// home.js
function foo(name){
  console.log(name+'from home.js')
}
//event.js
foo('event')

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants