webpack

现今的很多网页其实可以看做是功能丰富的应用,它们拥有着复杂的JavaScript代码和一大堆依赖包。为了简化开发的复杂度,前端社区涌现出了很多好的实践方法

模块化,让我们可以把复杂的程序细化为小的文件;
类似于TypeScript这种在JavaScript基础上拓展的开发语言:使我们能够实现目前版本的JavaScript不能直接使用的特性,并且之后还能转换为JavaScript文件使浏览器可以识别;
Scss,less等CSS预处理器

这些改进确实大大的提高了我们的开发效率,但是利用它们开发的文件往往需要进行额外的处理才能让浏览器识别,而手动处理又是非常繁琐的,这就为WebPack类的工具的出现提供了需求。

介绍

  • 是什么
    • 打包工具
    • JavaScript 模块打包之后就可以运行在浏览器
  • 能做什么
    • webpack 可以当作是一个模块打包平台,但是它本身只能打包 JavaScript 模块
    • 对于其它的文件模块资源,则需要使用第三方 loader 来处理
    • JavaScript 资源打包
    • css 打包
    • 图片 打包
    • less
    • sass
    • babel EcmaScript 6 转 EcmaScript 5
    • 开发工具:http 服务器
    • 代码改变,自动刷新浏览器
    • 压缩代码
    • JavaScript 代码压缩
    • html 代码压缩
    • css 代码压缩
    • 。。。。
  • webpack 1.x
  • webpack 2.x
  • 官方指南

起步

hello world

全局安装:

  1. npm install --global webpack webpack-cli

查看版本号是否安装成功:

  1. # 正常的话应该会看到输出一个版本号
  2. webpack --version

准备目录结构:

  1. .
  2. ├── index.html
  3. ├── main.js
  4. └── foo.js

foo.js 文件内容如下:

  1. module.exports = function () {
  2. console.log('我是 foo 文件模块啊')
  3. }

main.js 文件内容如下:

  1. var foo = require('./foo')
  2. foo()

打包:

  1. # 默认打包到 dist/main.js 中
  2. webpack main.js

index.html 文件内容如下:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Document</title>
  6. </head>
  7. <body>
  8. <!-- <script src="js/main.js"></script> -->
  9. <!-- 最后记得把 index.html 文件中的脚本应用改为打包之后的结果文件路径。 -->
  10. <script src="js/bundle.js"></script>
  11. </body>
  12. </html>

打开查看 index.html。

为了划分目录结构的清晰,所以我们把项目中的源码和打包结果做了如下划分:

  • 把源码存储到 src 目录中
  • 把打包的结果存储到 dist 目录中

安装

全局安装:

  1. npm install --global webpack

安装在全局的 webpack 打包的时候使用的是你安装在自己电脑上的 webpack,如果到了另一个人的计算机上,它可能安装的是老板本的 webpack。那么就可能涉及到兼容性的问题。

而且如果对方没有在全局安装 webpack 则就无法打包。

所以,为了解决以上的问题,我们更推荐把 webpack 安装到本地项目中。这样的话项目到哪里,webpack 就跟到了哪里。(打包工具随着项目走)。

我们安装的时候把 webpack 安装到开发依赖(—save-dev)中,因为 webpack 只是一个打包工具,项目如果需要上线,上线的是打包的结果,而不是这个工具。所以了我们为了区分核心包依赖和开发工具依赖,这里通过 --save--save-dev 来区分。

本地安装(推荐):

  1. # 把工具(webpack、style-loader、less-loader...)相关的依赖项保存到开发依赖
  2. # 把非工具(vue、axios、vue-router、moment...)正常安装
  3. npm install --save-dev webpack

对于安装到项目中的 webpack 需要配置 npm scripts 来使用:

  1. {
  2. "name": "demo2",
  3. "version": "1.0.0",
  4. "description": "",
  5. "main": "webpack.config.js",
  6. "scripts": {
  7. "build": "webpack"
  8. },
  9. "keywords": [],
  10. "author": "",
  11. "license": "ISC",
  12. "devDependencies": {
  13. "webpack": "^3.8.1"
  14. }
  15. }

然后通过 npm run 命令来打包:

  1. npm run a
  2. # start 比较特殊,可以不加 run
  3. npm start
  4. # 打包构建
  5. # 这里使用的 webpack 就是项目中安装的 webpack
  6. npm run build

配置文件 webpack.config.js

最基本的配置项:

  1. // 该文件其实最终是要在 Node 环境下执行的
  2. const path = require('path')
  3. // 导出一个具有特殊属性配置的对象
  4. module.exports = {
  5. entry: './src/main.js', // 入口文件模块路径
  6. output: {
  7. path: path.join(__dirname, './dist/'), // 出口文件模块所属目录,path 必须是一个绝对路径
  8. filename: 'bundle.js' // 打包的结果文件名称
  9. }
  10. }

打包:

  1. # webpack 会自动读取 webpack.config.js 文件作为默认的配置文件
  2. # 也可以通过 --config 参数来手动指定配置文件
  3. webpack

npm scripts

打包 JavaScript 模块

JavaScript 模块化

  • AMD
    • Require.js
  • CMD
    • Sea.js
  • CommonJS 模块规范
  • 以上都是民间搞出来的,所以在 2015 年的 EcmaScript 6 中官方就发布了官方的 模块规范:EcmaScript 6 Module 模块规范
    • 我们更推荐在项目中使用 EcmaScript 6 模块规范
    • 以后的统一趋势
  • webpack
    • AMD
    • CMD
    • CommonJS
    • EcmaScript 6 Module

EcmaScript 6 模块规范

导入 import(require)

导出 export(module.exports)

导出默认成员:

  1. // 默认成员只能有一个,否则报错
  2. exports default 成员

加载默认成员:

  1. // 如果没有 default 成员,则加载到的就是 undefined
  2. import xxx from '模块标识'

导出多个成员:

  1. // export 必须引用到内部的一个成员
  2. export const a = 123
  3. export const b = 456
  4. export function fn () {
  5. console.log('fn')
  6. }

如果你觉得上面的方式比较麻烦,也可以以这样的方式来导出多个成员:

  1. const a = 123
  2. const b = 456
  3. function fn () {
  4. console.log('fn')
  5. }
  6. // 注意:这里不是对象的简写方式,这是导出的特殊语法
  7. // 这种方式也不是覆盖,后面还可以继续导出
  8. export {
  9. a,
  10. b,
  11. fn
  12. }
  13. // 可以继续增加导出的成员
  14. export function add (x, y) {
  15. return x + y
  16. }
  17. // 最终导出的实际上是 a、b、fn、add

按需加载指定的多个成员:

  1. import {a, b} from '模块标识'

一次性加载所有的导出成员:

  1. // 所有成员包含 default
  2. import * as xxx from '模块标识'

资源管理

webpack 不仅可以打包 JavaScript 模块,甚至它把网页开发中的一切资源都可以当作模块来打包处理。

按时它本身不知此,它只是一个打包平台,其它资源,例如 css、less、sass、img 等资源需要结合插件来实现,这些插件在 webpack 中被称之为 loader ,翻译过来就是 加载器 的意思。

Loading CSS

参考链接:

安装依赖:

  1. # CSS-loader 的作用是吧 CSS 文件转为 JavaScript 模块
  2. # style-loader 的作用是动态创建 style 节点插入到 head 中
  3. npm install --save-dev style-loader css-loader

配置:

  1. var path = require('path')
  2. module.exports = {
  3. entry: './src/main.js',
  4. output: {
  5. path: path.join(__dirname, './dist/'), // 这里必须是绝对路径
  6. filename: 'bundle.js'
  7. },
  8. module: {
  9. rules: [
  10. {
  11. test: /.css$/,
  12. use: [
  13. // 注意:这里的顺序很重要,不要乱了顺序,从咱们的角度,老外的思维是反的
  14. 'style-loader',
  15. 'css-loader'
  16. ]
  17. }
  18. ]
  19. }
  20. }

打包:

  1. npm run build

解释:打包 css 也是把 CSS 文件内容转换成了一个 JavaScript 模块,然后在运行 JavaScript 的时候,会动态的创建一个 style 节点插入到 head 头部。

Loading Images

安装依赖:

  1. npm install --save-dev file-loader

配置:

  1. module: {
  2. rules: [
  3. {
  4. test: /.css$/,
  5. use: [
  6. 'style-loader',
  7. 'css-loader'
  8. ]
  9. },
  10. {
  11. test: /.(jpg|png|gif|svg)$/,
  12. use: [
  13. 'file-loader'
  14. ]
  15. }
  16. ]
  17. }

Url Loader

优化图片打包

Loading Fonts

Loading Data

Loading Less

安装依赖:

  1. # 如果 css-loader 安装过了就不需要安装了
  2. npm i -D css-loader style-loader less less-loader

配置:

  1. module: {
  2. rules: [
  3. {
  4. test: /.less$/,
  5. use: [
  6. 'style-loader', // 3. 根据模块生成 style 节点插入 head 中
  7. 'css-loader', // 2. 在把 css 转成 JavaScript 模块
  8. 'less-loader' // 1. 先把 less 转成 css
  9. ]
  10. }
  11. ]
  12. }

Loading Sass

transpiling JavaScript

  • babel
    • http://babeljs.io/
    • babel 是一个 JavaScript 编译器,可以把 EcmaScript 6 编译成 EcmaScript 5
    • babel 可以独立使用,但是独立使用没有意义,一般是和 webpack 结合到一起来使用的
  • cacheDirectory
  • babel-polyfill
    • 默认 babel 只转换语法
    • 我们可以使用 babel-polyfill 来转换 EcmaScript 6 中的 API
  • babel runtime

安装依赖:

  1. npm install --save-dev babel-loader babel-core babel-preset-env

配置:

  1. module: {
  2. rules: [
  3. {
  4. test: /\.js$/,
  5. exclude: /(node_modules|bower_components)/, // 不转换 node_modules 中的文件模块
  6. use: {
  7. loader: 'babel-loader',
  8. options: {
  9. presets: ['env']
  10. }
  11. }
  12. }
  13. ]
  14. }

babel 只转换 ECMAScript 6 语法

let、const、箭头函数、解构赋值

不会转换 API,例如数组的 find、findIndex、字符串的 startsWith、endsWith

配置 babel-polyfill 来提供低版本浏览器中的不支持 API

安装:

  1. npm i -D babel-polyfill

配置:

  1. entry: ['babel-polyfill', './src/main.js'],

这样话就会在打包的结果中提供一个垫脚片用以兼容低版本浏览器中的不支持的 API。

配置 transform-runtime 来解决代码重复问题

在打包过程中,babel 会在某个包提供一些工具函数,而这些工具函数可能会重复的出现在多个模块。这样的话就会导报打包体积过大,所以 babel 提供了一个 babel-transform-runtime 来解决这个打包体积过大的问题。

安装:

  1. npm install babel-plugin-transform-runtime --save-dev
  2. npm install babel-runtime --save

配置:

  1. module: {
  2. rules: [
  3. {
  4. test: /\.js$/,
  5. exclude: /(node_modules|bower_components)/,
  6. use: {
  7. loader: 'babel-loader',
  8. options: {
  9. presets: ['env'],
  10. plugins: ['transform-runtime']
  11. }
  12. }
  13. }
  14. ]
  15. }

加入缓存节省编译时间

babel 编译是非常耗时的,我们可以通过开启对编译结果的缓存来提高打包速度:

  1. module: {
  2. rules: [
  3. {
  4. test: /\.js$/,
  5. exclude: /(node_modules|bower_components)/,
  6. use: {
  7. loader: 'babel-loader',
  8. options: {
  9. // 默认把打包的结果缓存到 node_modules/.cache 模板
  10. cacheDirectory: true,
  11. presets: ['env'],
  12. plugins: ['transform-runtime']
  13. }
  14. }
  15. },
  16. ]
  17. }

Url Loader

  • 小图片直接 base64 编码到 html 文件中
  • 大图片拷贝到打包目录中

输出管理

HtmlWebpackPlugin

你会发现,当你打包结束的时候,如果 index.html 在根目录直接运行的话,那么图片资源这些路径就无法访问到了。解决方案就是把 index.html 放到 dist 目录中。

但是 dist 是打包编译的结果,而非源码,所以把 index.html 放到 dist 就不合适。

而且你也会发现,我们打包的结果文件名:bundle.js ,如果一旦我把这个文件名给改了,则 index.html 也要手动修改。

综合以上我们遇到的问题,我们就可以使用一个插件:html-webpack-plugin 来解决。

安装依赖:

  1. npm i -D html-webpack-plugin

配置:

  1. plugins: [
  2. // 该插件的所用就是把 index.html 打包到你的 bundle.js 文件所属目录
  3. // 也就是说 bundle 到哪里,index.html 就到哪里
  4. // 同时这个也会自动在 index.html 中出入 script 引用连接
  5. // 而且引用的资源名称,也取决于你的 bundle 叫什么
  6. // 这个插件还可以配置压缩 html 的处理
  7. new htmlWebpackPlugin({
  8. template: './index.html'
  9. })
  10. ],

CleanWebpackPlugin

Develoment 开发

Source maps

webpack-dev-server

每一次手动打包很麻烦,而且即便有 --watch 也不方便,还需要自己手动刷新浏览器。

所以 webpack 给你提供了一个工具:webpack-dev-server

它就可以实现监视代码改变,自动打包,打包完毕自动刷新浏览器的功能。

安装:

  1. npm i -D webpack-dev-server

配置:

  1. devServer: {
  2. // 配置 webpack-dev-server 的 www 目录
  3. contentBase: './dist'
  4. },

配置 npm scritps:

  1. "scripts": {
  2. "build": "webpack",
  3. "watch-build": "webpack --watch",
  4. "dev": "webpack-dev-server --open"
  5. },

启动开发模式:

  1. npm run dev

解释:该工具会自动帮你打包,打包完毕只有会自动开启一个服务器,默认监听 8080 端口号,同时自动打开浏览器让你访问,接下来就会自动监视代码的改变,然后自动编译,编译完毕,自动刷新浏览器。

客户端代理

https://webpack.js.org/configuration/dev-server/#devserver-proxy

ESLint 代码规范校验

  • eslint
  • eslint-loader
  • eslint-plugin-html
  • 安装
  • 初始化 .eslintrc.js 配置文件
  • 配置到 webpack

Hot Module Replacement 热更新

  1. const path = require('path')
  2. const htmlWebpackPlugin = require('html-webpack-plugin')
  3. +++ const webpack = require('webpack')
  4. module.exports = {
  5. entry: ['babel-polyfill', './src/main.js'],
  6. output: {
  7. path: path.join(__dirname, './dist/'), // 这里必须是绝对路径
  8. filename: 'bundle.js'
  9. },
  10. plugins: [
  11. // 该插件的所用就是把 index.html 打包到你的 bundle.js 文件所属目录
  12. // 也就是说 bundle 到哪里,index.html 就到哪里
  13. // 同时这个也会自动在 index.html 中出入 script 引用连接
  14. // 而且引用的资源名称,也取决于你的 bundle 叫什么
  15. // 这个插件还可以配置压缩 html 的处理
  16. new htmlWebpackPlugin({
  17. template: './index.html'
  18. }),
  19. +++ new webpack.NamedModulesPlugin(),
  20. +++ new webpack.HotModuleReplacementPlugin()
  21. ],
  22. devServer: {
  23. // 配置 webpack-dev-server 的 www 目录
  24. // 浏览器真正运行查看的是打包之后的结果
  25. // webpack-dev-server 为了提高打包效率,它把文件存储在了内存中,你看不见
  26. // 这里你只是在告诉 webpack-dev-server 让你的打包结果运行在虚拟目录 dist 中
  27. // 那这个时候你的打包结果中的 index.html 去加载资源时候确实需要相对于 dist 来找
  28. // ./ 的话,webpack-dev-server 就会直接把资源打包到项目根目录下
  29. // 但是注意:你也看不见它
  30. // 那这个时候你在 index.html 文件中请求的资源就是相对于 demo7 根目录
  31. contentBase: './',
  32. +++ hot: true
  33. },
  34. externals: {
  35. // key 就是包名
  36. // value 是全局的 jQuery 导出的接口
  37. vue: 'Vue'
  38. },
  39. module: {
  40. rules: [{
  41. test: /.css$/,
  42. use: [
  43. 'style-loader',
  44. 'css-loader'
  45. ]
  46. },
  47. {
  48. test: /.(jpg|png|gif|svg)$/,
  49. use: [
  50. 'file-loader'
  51. ]
  52. },
  53. {
  54. test: /.less$/,
  55. use: [
  56. 'style-loader', // 3. 根据模块生成 style 节点插入 head 中
  57. 'css-loader', // 2. 在把 css 转成 JavaScript 模块
  58. 'less-loader' // 1. 先把 less 转成 css,less-loader 依赖 less,所以 less 也要安装进来
  59. ]
  60. },
  61. {
  62. test: /\.js$/,
  63. exclude: /(node_modules|bower_components)/,
  64. use: {
  65. loader: 'babel-loader',
  66. options: {
  67. cacheDirectory: true,
  68. presets: ['env'],
  69. plugins: ['transform-runtime']
  70. }
  71. }
  72. },
  73. {
  74. test: /.vue$/,
  75. use: [
  76. 'vue-loader'
  77. ]
  78. }
  79. ]
  80. }
  81. }

Vue Loader

打包 .vue 单文件组件的

Vue 单文件组件

Vue Loader 官方文档

安装:

  1. # vue-loader 依赖 vue-template-compiler
  2. npm i -D vue-loader vue-template-compiler

配置:

  1. module: {
  2. rules: [
  3. {
  4. test: /.vue$/,
  5. use: [
  6. 'vue-loader'
  7. ]
  8. }
  9. ]
  10. }

组件细则

.vue 文件是一个自定义的文件类型,用类 HTML 语法描述一个 Vue 组件。每个 .vue 文件包含三种类型的顶级语言块 <template><script><style>

  1. <template>
  2. <div class="example">{{ msg }}</div>
  3. </template>
  4. <script>
  5. export default {
  6. data () {
  7. return {
  8. msg: 'Hello world!'
  9. }
  10. }
  11. }
  12. </script>
  13. <style>
  14. .example {
  15. color: red;
  16. }
  17. </style>

语言块

  • <template>
    • 默认语言:html
    • 每个 .vue 文件最多包含一个 <template> 块。
    • 内容将被提取为字符串,将编译并用作 Vue 组件的 template 选项。
  • <script>
    • 默认语言:js (在检测到 babel-loaderbuble-loader 配置时自动支持ES2015)。
    • 每个 .vue 文件最多包含一个 <script> 块。
    • 该脚本在类 CommonJS 环境中执行 (就像通过 Webpack 打包的正常 js 模块),这意味这你可以 require() 其它依赖。在 ES2015 支持下,你也可以使用 importexport 语法。
    • 脚本必须导出 Vue.js 组件对象。
  • <style>
    • 默认语言:css
    • 一个 .vue 文件可以包含多个 <style> 标签。
    • <style> 标签可以有 scoped 或者 module 属性 (查看 CSS 作用域CSS Modules) 以帮助你将样式封装到当前组件。具有不同封装模式的多个 <style> 标签可以在同一个组件中混合使用。
    • 默认情况下,将会使用 style-loader 提取内容,并通过 <style> 标签动态加入文档的 <head> 中,也可以配置 Webpack 将所有 styles 提取到单个 CSS 文件中

css 作用域

<style> 标签有 scoped 属性时,它的 CSS 只作用于当前组件中的元素。

  1. <style scoped>
  2. .example {
  3. color: red;
  4. }
  5. </style>
  6. <template>
  7. <div class="example">hi</div>
  8. </template>

也可以在一个组件中同时使用有作用域和无作用域的样式:

  1. <style>
  2. /* 全局样式 */
  3. </style>
  4. <style scoped>
  5. /* 本地样式 */
  6. </style>

如果你希望 scoped 样式中的一个选择器能够作用得“更深”,例如影响子组件,你可以使用 >>> 操作符:

  1. <style scoped>
  2. .a >>> .b { /* ... */ }
  3. </style>

Resolve

alias

参考文档:

作用:配置别名,解决文件路径改变以及加载路径过深问题。

extensions

externals

配置 webpack 不打包第三方包

通常情况下我们不打包第三方包,因为第三方包太大,和 bundle 打包到一起会造成资源体积过大,所以我们还是通过 script 标签的方式把第三方资源引入到页面中,只需要通过以下配置即可,这里以 jQuery 为例:

  1. 下载第三方包
  1. npm i jquery
  1. 在页面中引入资源
  1. <script src="node_modules/jquery/dist/jquery.js"></script>
  1. 配置
  1. externals: {
  2. // key 是第三方包名称,value 是全局中的 jQuery 对象
  3. // 这里配置的含义就是:当你在代码中 import jquery 的时候,不会把 jquery 打包到 bundle 中,而是使用我指定的全局中的 jQuery 对象
  4. jquery: 'jQuery'
  5. }
  1. 加载使用
  1. import $ from 'jquery'
  2. $('#app', {
  3. width: 200,
  4. height: 200,
  5. backgroundColor: 'pink'
  6. })
  1. 打包测试
  1. npm run build

--save--save-dev 的区别

我们把开发工具相关的依赖信息保存到 devDependencies 选项中。把核心依赖(例如 vue)的依赖信息保存到 dependencies 选项中。

这样做的话,就是把开发依赖和核心依赖分开了,因为开发依赖在打包结束之后上线的话就不需要。

最后项目上线,我们真正需要安装发布的是 dependencies 依赖项中的包。

我们可以通过命令来只安装 dependencies 依赖项中的包:

  1. npm install --production

vue-webpack-starter

在模块化环境中使用 vue-router

  1. 下载
  1. npm i vue-router
  1. 引用资源
  1. <script src="node_modules/vue-router/dist/vur-router.js"></script>
  1. 配置 externals
  1. externals: {
  2. 'vue-router': 'VueRouter'
  3. }
  1. router.js 文件中加载使用
  1. import VueRouter from 'vue-router'
  2. import Foo from './components/Foo.vue'
  3. import Bar from './components/Bar.vue'
  4. // 这里直接默认导出 new 出来的 router 实例
  5. export default new VueRouter({
  6. routes: [
  7. {
  8. path: '/foo',
  9. component: Foo
  10. },
  11. {
  12. path: '/bar',
  13. component: Bar
  14. }
  15. ]
  16. })
  1. main.js 文件中配置使用路由对象
  1. import Vue from 'vue'
  2. import App from './App.vue'
  3. +++ import router from './router'
  4. new Vue({
  5. components: { App },
  6. template: '<App />',
  7. +++ router
  8. }).$mount('#app')
  1. App.vue 中设置路由出口
  1. <template>
  2. <div id="app">
  3. <h1>根组件 hh </h1>
  4. <ul>
  5. <li><a href="#/foo">Go to Foo</a></li>
  6. <li><a href="#/bar">Go to Bar</a></li>
  7. </ul>
  8. +++ <router-view></router-view>
  9. </div>
  10. </template>

webpack 打包优化发布策略

webpack 优化还是很有必要的

  • 开发配置文件:webpack.dev.conf.js
  • 发布配置文件:webpack.prod.conf.js
  • 使用 url-loader 减少网站中图片资源的请求数量
    • 小图片直接 base64 内置到模板中
    • 大图片拷贝到结果目录中
    • 把图片资源分离到特定的目录中
  • 使用 clean-webpack-plugin 清除 dist 目录
  • 分离第三方包(解决 js 文件体积过大的问题)
    • 多入口
    • webpack.optimize.CommonsChunkPlugin()
  • 压缩 html
    • 减小文件体积,提高加载速度
  • 压缩 JavaScript
    • 减小文件体积,提高加载速度
    • webpack.optimize.UglifyJsPlugin()
    • webpack.optimize.DedupePlugin() 配置插件环境变量
    • 把 JavaScript 分离到指定的目录中
  • 抽离 CSS 样式文件
    • 为了让浏览器尽快的优先加载样式,也是为了解决js 文件体积过大的问题
    • 把 css 分离到指定的目录结构中
    • extract-text-webpack-plugin
  • 压缩抽取出来的 CSS 文件:https://github.com/webpack-contrib/css-loader#minimize
    • 压缩 CSS 也是为了减小文件题解,提高响应速度