webpack使用详解

Webpack 是当下最热门的前端资源模块化管理和打包工具。它可以将许多松散的模块按照依赖和规则打包成符合生产环境部署的前端资源。还可以将按需加载的模块进行代码分隔,等到实际需要的时候再异步加载。通过loader的转换,任何形式的资源都可以视作模块,比如 CommonJs 模块、AMD 模块、ES6 模块、CSS、图片、JSON、Coffeescript、LESS 等。

1、webpack简介

前端是基于多语言、多层次的编码和组织工作,其次前端产品的交付是基于浏览器,这些资源是通过增量加载的方式运行到浏览器端,如何在开发环境组织好这些碎片化的代码和资源,并且保证他们在浏览器端快速、优雅的加载和更新,就需要一个模块化系统。

webpack 的优势

  • 1、支持CommonJs和AMD模块,意思也就是我们基本可以无痛迁移旧项目。
  • 2、支持模块加载器和插件机制,可对模块灵活定制。babel-loader更是有效支持ES6。
  • 3、可以通过配置,打包成多个文件。有效利用浏览器的缓存功能提升性能。
  • 4、将样式文件和图片等静态资源也可视为模块进行打包。配合loader加载器,可以支持sass,less等CSS预处理器。
  • 5、内置有source map,即使打包在一起依旧方便调试。

webpack 核心概念

Webpack 具有四个核心的概念,想要入门 Webpack 就得先好好了解这四个核心概念。它们分别是Entry(入口)、Output(输出)、loader 和 Plugins(插件)。接下来详细介绍这四个核心概念。

1. Entry

Entry 是 Webpack 的入口起点指示,它指示 webpack 应该从哪个模块开始着手,来作为其构建内部依赖图的开始。可以在配置文件(webpack.config.js)中配置 entry 属性来指定一个或多个入口点,默认为./src( webpack 4开始引入默认值)。 具体配置方法:

entry: string | Array
复制代码

前者一个单独的 string 是配置单独的入口文件,配置为后者(一个数组)时,是多文件入口。

//webpack.config.js
module.exports = {
    entry: {
        app: './app.js',
        vendors: './vendors.js'
    }
};
复制代码

以上配置表示从 app 和 vendors 属性开始打包构建依赖树,这样做的好处在于分离自己开发的业务逻辑代码和第三方库的源码,因为第三方库安装后,源码基本就不再变化,这样分开打包有利于提升打包速度,减少了打包文件的个数。

2. Output

Output 属性告诉webpack在哪里输出它所创建的 bundles,也可指定 bundles 的名称,默认位置为 ./dist。整个应用结构都会被编译到指定的输出文件夹中去,最基本的属性包括 filename(文件名)和 path(输出路径)。

值得注意的是,即是你配置了多个入口文件,你也只能有一个输出点。

具体配置方法:

output: {
    filename: 'bundle.js',
    path: '/home/proj/public/dist'
}
复制代码

值得注意的是,output.filename 必须是绝对路径,如果是一个相对路径,打包时 webpack 会抛出异常。

多个入口时,使用下面的语法输出多个 bundle :

// webpack.config.js
module.exports = {
    entry: {
        app: './src/app.js',
        vendors: './src/vendors.js'
    },
    output: {
        filename: '[name].js',
        path: __dirname + '/dist'
    }
}
复制代码

3. Loaders

loader 可以理解为webpack的编译器,它使得webpack可以处理一些非 JavaScript 文件,比如 png、csv、xml、css、json 等各种类型的文件,使用合适的 loader 可以让 JavaScript 的 import 导入非 JavaScript 模块。JavaScript 只认为 JavaScript 文件是模块,而 webpack 的设计思想即万物皆模块,为了使得 webpack 能够认识其他“模块”,所以需要 loader 这个“编译器”。

webpack 中配置 loader 有两个目标:

(1)test 属性:标志有哪些后缀的文件应该被处理,是一个正则表达式。

(2)use 属性:指定 test 类型的文件应该使用哪个 loader 进行预处理。

比如webpack.config.js:

module.exports = {
    entry: '...',
    output: '...',
    module: {
        rules: [
            {
                test: /\.css$/,
                use: 'css-loader'
            }
        ]
    }
};
复制代码

该配置文件指示了所有的 css 文件在 import 时都应该经过 css-loader 处理,经过 css-loader 处理后,可以在 JavaScript 模块中直接使用 import 语句导入 css 模块。但是使用 css-loader 的前提是先使用 npm 安装 css-loader。

此处需要注意的是定义 loaders 规则时,不是定义在对象的 rules 属性上,而是定义在 module 属性的 rules 属性中。

配置多个 loader:

有时候,导入一个模块可能要先使用多个 loader 进行预处理,这时就要对指定类型的文件配置多个 loader 进行预处理,配置多个 loader,把 use 属性赋值为数组即可,webpack 会按照数组中 loader 的先后顺序,使用对应的 loader 依次对模块文件进行预处理。

{
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                    {
                        loader: 'style-loader'
                    },
                    {
                        loader: 'css-loader'
                    }
                ]
            }
        ]
    }
}
复制代码

4. Plugins

loader 用于转换非 JavaScript 类型的文件,而插件可以用于执行范围更广的任务,包括打包、优化、压缩、搭建服务器等等,功能十分强大。要是用一个插件,一般是先使用npm包管理器进行安装,然后在配置文件中引入,最后将其实例化后传递给 plugins 数组属性。

插件是 webpack 的支柱功能,目前主要是解决 loader 无法实现的其他许多复杂功能,通过 plugins 属性使用插件:

// webpack.config.js
const webpack = require('webpack');
module.exports = {
    plugins: [
        new webpack.optimize.UglifyJsPlugin()
    ]
}
复制代码

5. Mode

模式( Mode )可以通过配置对象的 mode 属性进行配置,主要值为 production 或者 development。两种模式的区别在于一个是为生产环境编译打包,一个是为了开发环境编译打包。生产环境模式下,webpack 会自动对代码进行压缩等优化,省去了配置的麻烦。

学习完以上基本概念之后,基本也就入门 webpack 了,因为 webpack 的强大就是建立在这些基本概念之上,利用 webpack 多样的 loaders 和 plugins,可以实现强大的打包功能。

2、js模块化

2.1 命名空间

命名空间是通过为项目或库创建一个全局对象,然后将所有功能添加到该全局变量中。通过减少程序中全局变量的数量,实现单全局变量,从而在具有大量函数、对象和其他变量的情况下不会造成全局污染,同时也避免了命名冲突等问题。

然而,在不同的文件中给一个命名空间添加属性的时候,首先要保证这个命名空间是已经存在的,同时不对已有的命名空间造成任何破坏。可以通过非破坏性的命名空间函数实现:

var KUI = KUI || {};
KUI.utils = KUI.utils || {};

KUI.utils.namespace = function(ns){
    var parts = ns.split("."),
        object = KUI,
        i, len;

    if(parts[0] === "KUI"){
        parts = parts.slice(1);
    }

    for(i = 0, len = parts.length; i < len; i+=1){

        if(!object[parts[i]]){
            object[parts[i]] = {};
        }

        object = object[parts[i]];
    }

    return object;
};
复制代码

用法:

KUI.utils.namespace("KUI.common");
KUI.utils.namespace("KUI.common.testing");
KUI.utils.namespace("KUI.modules.function.plugins");
KUI.utils.namespace("format");
复制代码

看一下经过上述后 KUI 都有什么:

{
    "utils": {},
    "common": {
        "testing": {}
    },
    "modules": {
        "function": {
            "plugins": {}
        }
    },
    "format": {}
}
复制代码

命名空间模式的缺点

1.需要输入更长的字符,并且需要更长的解析时间; 2.对单全局变量的依赖性,即任何代码都可以修改该全局实例,其他代码将获得修改后的实例。

2.2 CommonJs

CommonJS 是 nodejs 也就是服务器端广泛使用的模块化机制。 该规范的主要内容是,模块必须通过 module.exports 导出对外的变量或接口,通过 require() 来导入其他模块的输出到当前模块作用域中。

根据这个规范,每个文件就是一个模块,有自己的作用域,文件中的变量、函数、类等都是对其他文件不可见的。

如果想在多个文件分享变量,必须定义为 global 对象的属性。

定义模块

在每个模块内部,module 变量代表当前模块。它的 exports 属性是对外的接口,将模块的接口暴露出去。其他文件加载该模块,实际上就是读取 module.exports 变量。

var x = 5;
var addX = function (value) {
  return value + x;
};
module.exports.x = x;
module.exports.addX = addX;
复制代码

加载模块

require 方法用于加载模块,后缀名默认为.js

var app = require('./app.js');
复制代码

模块加载的顺序,按照其在代码中出现的顺序

根据参数的不同格式,require 命令去不同路径寻找模块文件。

  • 如果参数字符串以“/”开头,则表示加载的是一个位于绝对路径的模块文件。
  • 如果参数字符串以“./”开头,则表示加载的是一个位于相对路径的模块文件
  • 如果参数字符串不以“./“或”/“开头,则表示加载的是一个默认提供的核心模块( node 核心模块,或者通过全局安装或局部安装在 node_modules 目录中的模块)

入口文件

一般都会有一个主文件(入口文件),在 index.html 中加载这个入口文件,然后在这个入口文件中加载其他文件。

可以通过在 package.json 中配置 main 字段来指定入口文件。

模块缓存

第一次加载某个模块时,Node 会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的 module.exports 属性。

加载机制

CommonJS 模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。

由于 CommonJS 是同步加载模块,这对于服务器端不是一个问题,因为所有的模块都放在本地硬盘。等待模块时间就是硬盘读取文件时间很小。但是,对于浏览器而言,它需要从服务器加载模块,涉及到网速,代理等原因,一旦等待时间过长,浏览器处于”假死”状态。

2.3 AMD

AMD 是 "Asynchronous Module Definition" 的缩写,即 “异步模块定义”。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。

这里异步指的是不堵塞浏览器其他任务( dom 构建,css 渲染等),而加载内部是同步的(加载完模块后立即执行回调)。

requirejs 即为遵循AMD规范的模块化工具。

RequireJS 的基本思想是,通过 define 方法,将代码定义为模块;通过 require 方法,实现代码的模块加载。

RequireJS 主要解决两个问题:

  • 多个 js 文件可能有依赖关系,被依赖的文件需要早于依赖它的文件加载到浏览器。
  • js 加载的时候浏览器会停止页面渲染,加载文件越多,页面失去响应时间越长。

定义模块

RequireJS 定义了一个函数 define,它是全局变量,用来定义模块:

define(id?, dependencies?, factory);
复制代码

参数说明:

  • id:指定义中模块的名字,可选;如果没有提供该参数,模块的名字应该默认为模块加载器请求的指定脚本的名字。如果提供了该参数,模块名必须是“顶级”的和绝对的(不允许相对名字)。

  • 依赖 dependencies:是一个当前模块依赖的,已被模块定义的模块标识的数组字面量。 依赖参数是可选的,如果忽略此参数,它应该默认为["require", "exports", "module"]。然而,如果工厂方法的长度属性小于 3 ,加载器会选择以函数的长度属性指定的参数个数调用工厂方法。

  • 工厂方法 factory,模块初始化要执行的函数或对象。如果为函数,它应该只被执行一次。如果是对象,此对象应该为模块的输出值。

define("alpha", ["require", "exports", "beta"], function (require, exports, beta) {
      exports.verb = function() {
          return beta.verb();
          //Or:
          return require("beta").verb();
      }
  });
复制代码

加载模块

AMD 也采用 require 命令加载模块,但是不同于 CommonJS ,它要求两个参数:

require(['math'], function(math) {
  math.add(2, 3);
})
复制代码

第一个参数是一个数组,里面的成员是要加载的模块,第二个参数是加载完成后的回调函数。

配置

require 方法本身也是一个对象,它带有一个 config 方法,用来配置 require.js 运行参数。

require.config({
    paths: {
        "backbone": "vendor/backbone",
        "underscore": "vendor/underscore"
    },
    shim: {
        "backbone": {
            deps: [ "underscore" ],
            exports: "Backbone"
        },
        "underscore": {
            exports: "_"
        }
    }
});
复制代码

paths:paths 参数指定各个模块的位置。这个位置可以是同一个服务器上的相对位置,也可以是外部网址。可以为每个模块定义多个位置,如果第一个位置加载失败,则加载第二个位置。上面就是指定了 jquery 的位置,那么就可以直接在文件中

require(['jquery'],function($){})
复制代码

shim:有些库不是 AMD 兼容的,这时就需要指定 shim 属性的值。shim 可以理解成“垫片”,用来帮助require.js 加载非 AMD 规范的库。

2.4 CMD

CMD 即Common Module Definition 通用模块定义,CMD 规范是国内发展出来的,就像 AMD 有个requireJS,CMD 有个浏览器的实现 SeaJS,SeaJS 要解决的问题和 requireJS 一样,只不过在模块定义方式和模块加载(可以说运行、解析)时机上有所不同。

在 CMD 规范中,一个模块就是一个文件。代码的书写格式如下:

define(function(require, exports, module) {

  // 模块代码

});
复制代码

require 是可以把其他模块导入进来的一个参数; 而 exports 是可以把模块内的一些属性和方法导出的; module 是一个对象,上面存储了与当前模块相关联的一些属性和方法。

  • AMD 是依赖关系前置,在定义模块的时候就要声明其依赖的模块;
  • CMD 是按需加载依赖就近,只有在用到某个模块的时候再去 require ;
// CMD
define(function(require, exports, module) {
  var a = require('./a')
  a.doSomething()
  // 此处略去 100 行
  var b = require('./b') // 依赖可以就近书写
  b.doSomething()
  // ... 
})

// AMD 默认推荐的是
define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好
  a.doSomething()
  // 此处略去 100 行
  b.doSomething()
  ...
})

复制代码

2.5 ES6 Module

ES6 正式提出了内置的模块化语法,我们在浏览器端无需额外引入 requirejs 来进行模块化。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过 import 命令输入。

ES6 中的模块有以下特点:

  • 模块自动运行在严格模式下
  • 在模块的顶级作用域创建的变量,不会被自动添加到共享的全局作用域,它们只会在模块顶级作用域的内部存在;
  • 模块顶级作用域的 this 值为 undefined
  • 对于需要让模块外部代码访问的内容,模块必须导出它们

定义模块

使用 export 关键字将任意变量、函数或者类公开给其他模块。

//导出变量
export var color = "red";
export let name = "cz";
export const age = 25;

//导出函数
export function add(num1,num2){
    return num1+num2;
}

//导出类
export class Rectangle {
    constructor(length, width) {
        this.length = length;
        this.width = width;
    }
}

function multiply(num1, num2) {
    return num1 * num2;
}

//导出对象,即导出引用
export {multiply}
复制代码

重命名模块

重命名想导出的变量、函数或类的名称

function sum(num1, num2) {
    return num1 + num2;
}

export {sum as add}
复制代码

这里将本地的 sum 函数重命名为 add 导出,因此在使用此模块的时候必须使用 add 这个名称。

导出默认值

模块的默认值是使用 default 关键字所指定的单个变量、函数或类,而你在每个模块中只能设置一个默认导出。

export default function(num1, num2) {
    return num1 + num2;
}
复制代码

此模块将一个函数作为默认值进行了导出, default 关键字标明了这是一个默认导出。此函数并不需要有名称,因为它就代表这个模块自身。对比最前面使用 export 导出的函数,并不是匿名函数而是必须有一个名称用于加载模块的时候使用,但是默认导出则无需一个名字,因为模块名就代表了这个导出值。

也可以使用重命名语法来导出默认值。

function sum(num1, num2) {
    return num1 + num2;
}

export { sum as default };
复制代码

加载模块

在模块中使用 import 关键字来导入其他模块。 import 语句有两个部分,一是需要导入的标识符,二是需导入的标识符的来源模块。此处是导入语句的基本形式:

import { identifier1,identifier2 } from "./example.js"
复制代码
  • 大括号中指定了从给定模块导入的标识符
  • from 指明了需要导入的模块。模块由一个表示模块路径的字符串来指定。

当从模块导入了一个绑定时,你不能在当前文件中再定义另一个同名变量(包括导入另一个同名绑定),也不能在对应的 import 语句之前使用此标识符,更不能修改它的值。

//导入单个绑定
import {sum} from './example.js'

//导入多个绑定
import {sum,multiply} from './example.js'

//完全导入一个模块
import * as example from './example.js'
example.sum(1,2);
example.multiply(2,3);

//重命名导入
import { sum as add} from './example.js'

//导入默认值
import sum from "./example.js";
复制代码

然而要记住,无论你对同一个模块使用了多少次 import 语句,该模块都只会被执行一次。

在导出模块的代码执行之后,已被实例化的模块就被保留在内存中,并随时都能被其他 import 所引用.

import { sum } from "./example.js";
import { multiply } from "./example.js";
import { magicNumber } from "./example.js";
复制代码

尽管此处的模块使用了三个 import 语句,但 example.js 只会被执行一次。若同一个应用中的其他模块打算从 example.js 导入绑定,则那些模块都会使用这段代码中所用的同一个模块实例。

限制

export 与 import 都有一个重要的限制,那就是它们必须被用在其他语句或表达式的外部,而不能使用在if等代码块内部。原因之一是模块语法需要让 JS 能静态判断需要导出什么,正因为此,你只能在模块的顶级作用域使用 export 与 import。

3、webpack使用

3.1 打包js

webpack 对各种模块化的支持

// app.js
// es module
import sum from './sum'

// commonjs
var minus = require('./minux')

//amd
require(['muti'], function () {
    console.log(muti(2, 3))
})

console.log(sum(2, 3))
console.log(minus(3, 2))
复制代码
// sum.js
export default function () {
    return a + b
}
复制代码
// minus.js
module.exports = function (a, b) {
    a - b
}
复制代码
// muti.js
define(function() {
    'use strict';
    return function (a, b) {
        return a * b;
    }
});
复制代码

压缩JS代码:

现在你写的 JS 代码,在上线之前,都是需要进行压缩的,在没有 webpack 和 gulp 这些工具前,你可能需要找一个压缩软件或者在线进行压缩,在Webpack中可以很轻松的实现JS代码的压缩,它是通过插件的方式实现的,这里我们就先来引入一个 uglifyjs-webpack-plugin ( JS 压缩插件,简称 uglify)。

注意:虽然 uglifyjs 是插件,但是webpack版本里默认已经集成,不需要再次安装。

引入:

我们需要在 webpack.config.js 中引入 uglifyjs-webpack-glugin 插件

const uglify = require('uglifyjs-webpack-plugin');
复制代码

引入后在 plugins 配置里new一个 uglify 对象就可以了,代码如下。

plugins:[
        new uglify()
    ],
复制代码

这时候在终端中使用 webpack 进行打包,你会发现 JS 代码已经被压缩了。

3.2 编译ES6

在前端开发中都开始使用ES6的语法了,虽然说 webpack3 增加了一些 ES6 的转换支持,但是实际效果不是很好。所以我在开发中还是喜欢添加 Babel-loader 的,我也查看了一些别人的 webpack 配置也都增加了 babel-loader,所以这节课我们学习一下如何增加 Babel 支持。

Babel 是什么? Babel 其实是一个编译 JavaScript 的平台,它的强大之处表现在可以通过便宜帮你达到以下目的:

  • 使用下一代的 javaScript 代码( ES6, ES7….),即使这些标准目前并未被当前的浏览器完全支持。
  • 使用基于 JavaScript 进行了扩展的语言,比如 React 的 JSX。

Babel的安装与配置

Babel 其实是几个模块化的包,其核心功能位于称为 babel-core 的 npm 包中,webpack 可以把其不同的包整合在一起使用,对于每一个你需要的功能或拓展,你都需要安装单独的包(用得最多的是解析 ES6 的 babel-preset-es2015 包和解析 JSX 的 babel-preset-react 包)。

安装依赖包

npm install --save-dev babel-loader babel-core babel-preset-env
复制代码

在 webpack 中配置 Babel 的方法如下:

{
    test:/\.(jsx|js)$/,
    use:{
        loader:'babel-loader',
        options:{
            presets:[
                "es2015","react"
            ]
        }
    },
    exclude:/node_modules/
}
复制代码

.babelrc配置

虽然 Babel 可以直接在 webpack.config.js 中进行配置,但是考虑到 babel 具有非常多的配置选项,如果卸载 webapck.config.js 中会非常的雍长不可阅读,所以我们经常把配置卸载 .babelrc 文件里。

在项目根目录新建 .babelrc 文件,并把配置写到文件里。

. babelrc

{
    "presets":["react","es2015"]
}
复制代码

.webpack.config.js 里的 loader 配置

{
    test:/\.(jsx|js)$/,
    use:{
        loader:'babel-loader',
    },
    exclude:/node_modules/
}
复制代码

ENV:

babel-preset-env 代替 babel-preset-ES2015 , babel 官方推出了 babel-preset-env ,并建议在使用的时候选择 env 代替之前的 ES20** 。env 为我们提供了更智能的编译选择。

npm install --save-dev babel-preset-env
复制代码

然后修改 .babelrc 里的配置文件。其实只要把之前的 es2015 换成 env 就可以了。

{
    "presets":["react","env"]
}
复制代码

3.3 打包公共代码

CommonsChunkPlugin 插件,是一个可选的用于建立一个独立文件 (又称作 chunk ) 的功能,这个文件包括多个入口 chunk 的公共模块。

通过将公共模块拆出来,最终合成的文件能够在最开始的时候加载一次,便存到缓存中供后续使用。这个带来速度上的提升,因为浏览器会迅速将公共的代码从缓存中取出来,而不是每次访问一个新页面时,再去加载一个更大的文件。

公共chunk 用于 入口chunk (entry chunk)

生成一个额外的 chunk 包含入口 chunk 的公共模块。

new webpack.optimize.CommonsChunkPlugin({
  name: "commons",
  // ( 公共chunk(commnons chunk) 的名称)

  filename: "commons.js",
  // ( 公共chunk 的文件名)

  // minChunks: 3,
  // (模块必须被3个 入口 chunk 共享)

  // chunks: ["pageA", "pageB"],
  // (只使用这些 入口chunk)
})
复制代码

你必须在 入口 chunk 之前加载生成的这个公共 chunk:


                    
                    

你可能感兴趣的:(webpack使用详解)