模块

模块是指为了完成某功能所需的程序或者子程序,模块是系统中「职责单一」且「可替换」的部分。所谓的模块化就是指把系统代码分为一系列职责单一且可替换的模块。模块化开发是指如何开发新的模块和复用已有的模块来实现应用的功能。Webpack 作为 JavaScript 模块化打包工具,自然对 JavaScript 的模块化做了不少工作,本文将从模块规范说起,逐渐介绍 Webpack 中对模块化的一些增强处理。

Webpack 中一切皆模块

在 Web 前端,我们不仅仅只有 JavaScript,还有 CSS、HTML、图片、字体、富媒体等众多资源,还有一些资源是以类似「方言」的方式存在着,比如 less、sass、各种 js 模板库等,这些资源并不能被直接用在 JavaScript 中,如果在 JavaScript 中像使用模块一样使用,那么可以极大的提高我们的开发体验:

  1. var img = require('./img/webpack.png');
  2. var style = require('./css/style.css');
  3. var template = require('./template.ejs');

在 Webpack 编译的过程中,Webpack 会对整个代码进行静态分析,分析出各个模块的类型和它们依赖关系,然后将不同类型的模块提交给对应的加载器(loader)来处理。比如一个用 Less 写的样式,可以先用 less-loader 将它转成一个 CSS 模块,然后再通过 css-loader 把他插入到页面的 <style> 标签中执行,甚至还可以通过插件将这部分 CSS 导出为 CSS 文件,使用link标签引入到页面中。

import()和神奇注释

在 Webpack 中,import不仅仅是 ES6 Module 的模块导入方式,还是一个类似require的函数(其实这是 ES2015 loader 规范的实现),我们可以通过import(‘path/to/module’)的方式引入一个模块,import()返回的是一个Promise对象。

import hello from './hello';
import(
    /*
     webpackChunkName: 'lazy-name'
     */
    './lazy'
).then(lazy => {
    console.log(lazy);
});

配置篇 - 图1

目前支持的注释有:

  • webpackInclude:如果是 import 的一个目录,则可以指定需要引入的文件特性,例如只加载 json 文件:/.json$/;
  • webpackExclude:如果是 import 的一个目录,则可以指定需要过滤的文件,例如 /.noimport.json$/;
  • webpackChunkName:这是 chunk 文件的名称,例如 lazy-name;
  • webpackPrefetch: 是否预取模块,及其优先级,可选值true、或者整数优先级别,0 相当于 true,webpack 4.6+支持;
  • webpackPreload: 是否预加载模块,及其优先级,可选值true、或者整数优先级别,0 相当于 true,webpack 4.6+支持;
  • webpackMode: 可选值lazy/lazy-once/eager/weak。

这里最复杂的是webpackMode

  • lazy:是默认的模式,为每个 import() 导入的模块,生成一个可延迟加载 chunk;

  • lazy-once:生成一个可以满足所有 import() 调用的单个可延迟加载 chunk,此 chunk 将在第一次 import() 调用时获取,随后的 import() 调用将使用相同的网络响应;注意,这种模式仅在部分动态语句中有意义,例如 import(./locales/${language}.json),其中可能含有多个被请求的模块路径;

  • eager:不会生成额外的 chunk,所有模块都被当前 chunk 引入,并且没有额外的网络请求。仍然会返回 Promise,但是是 resolved 状态。和静态导入相对比,在调用 import() 完成之前,该模块不会被执行。

  • weak:尝试加载模块,如果该模块函数已经以其他方式加载(即,另一个 chunk 导入过此模块,或包含模块的脚本被加载)。仍然会返回 Promise,但是只有在客户端上已经有该 chunk 时才成功解析。如果该模块不可用,Promise 将会是 rejected 状态,并且网络请求永远不会执行。当需要的 chunks 始终在(嵌入在页面中的)初始请求中手动提供,而不是在应用程序导航在最初没有提供的模块导入的情况触发,这对于 Server 端渲染(SSR,Server-Side Render)是非常有用的。

通过上面的神奇注释,import()不再是简单的 JavaScript 异步加载器,还是任意模块资源的加载器,举例说明:如果我们页面用到的图片都放在src/assets/img文件夹下,你们可以通过下面方式将用到的图片打包到一起:

import(/* webpackChunkName: "image", webpackInclude: /\.(png|jpg|gif)/ */ './assets/img');

Babel 原理

了解了 Babel 的使用方法,接下来简单看下 Babel 的原理。Babel 是一个 JavaScript 的静态分析编译器,所谓静态分析指的是在不需要执行代码的前提下对代码进行分析和处理的过程(执行时进行代码分析叫动态分析)。要实现 Babel 从一个语法转换成另外一个语法,需要经过三个主要步骤:解析(Parse),转换(Transform),生成(Generate)。

  • 解析:指的是首先将代码经过词法解析和语法解析,最终生成一颗 AST(抽象语法树),在 Babel 中,语法解析器是Babylon
  • 转换:得到 AST 之后,可以对其进行遍历,在此过程中对节点进行添加、更新及移除等操作,Babel 中 AST 遍历工具是@babel/traverse
  • 生成:经过一系列转换之后得到的一颗新树,要将树转换成代码,就是生成的过程,Babel 用到的是@babel/generator

配置篇 - 图2

AST
在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。比如,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现;而类似于 if-condition-then 这样的条件跳转语句,可以使用带有两个分支的节点来表示。

// index.js
const fs = require('fs');
const babel = require('@babel/core');
const traverse = require('@babel/traverse').default;
// 读取 source.js内容
let source = fs.readFileSync('./source.js');

// 使用 babel.parse方法
babel.parse(source, (err, ast) => {
    // ast就是树
    console.log(ast);
});

遍历

如果想处理 AST 那么我们就需要进行树的遍历,学过算法的应该知道树的遍历包括深度优先和广度优先。。。慢点,这里 Babel 提供了@babel/traverse,可以直接来遍历,不需要我们手动来写遍历代码。

const fs = require('fs');
const babel = require('@babel/core');
const traverse = require('@babel/traverse').default;

let source = fs.readFileSync('./source.js');

babel.parse(source, (err, ast) => {
    // console.log(ast)
    let indent = '';
    traverse(ast, {
        // 进入节点
        enter(path) {
            console.log(indent + '<' + path.node.type + '>');
            indent += '  ';
        },
        // 退出节点
        exit(path) {
            indent = indent.slice(0, -2);
            console.log(indent + '<' + '/' + path.node.type + '>');
        }
    });
});

生成

const fs = require('fs');
const babel = require('@babel/core');
const traverse = require('@babel/traverse').default;
const gen = require('@babel/generator').default;

let source = fs.readFileSync('./source.js');

babel.parse(source, (err, ast) => {
    // console.log(err, ast)
    let indent = '';
    traverse(ast, {
        // 一顿操作猛如虎。。
    });
    // 生成新的 ast,然后使用generator生成 code
    console.log(gen(ast).code);
});

css-loader

识别css文件

style-loader

有了 css-loader 可以识别 CSS 语法了,下面就需要 style-loader 出场了。简单来说,style-loader 是将 css-loader 打包好的 CSS 代码以<style>标签的形式插入到 HTML 文件中,所以style-loader是和css-loader成对出现的,并且style-loader是在css-loader之后。

mini-css-extract-plugin

CSS 作为<style>标签放到 HTML 内还是不够的,我们还需要将 CSS 以<link>的方式通过 URL 的方式引入进来,这时候就需要使用mini-css-extract-plugin这个插件了。

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
    plugins: [
        // 添加 plugin
        new MiniCssExtractPlugin({
            filename: '[name].css',
            chunkFilename: '[id].css'
        })
    ],
    module: {
        rules: [
            {
                test: /\.css$/,
                // 添加 loader
                use: [MiniCssExtractPlugin.loader, 'css-loader']
            }
        ]
    }
};

CSS Modules

CSS Modules 指的是所有的 CSS 类名及其动画名都只是局部作用域的 CSS 文件。

  1. 解决 CSS 类都是全局的,容易造成全局污染(样式冲突);
  2. JS 和 CSS 共享类名;
  3. 可以方便的编写出更加健壮和扩展方便的 CSS。

PostCSS:CSS 后处理器

PostCSS 是一个使用 JavaScript 插件来转换 CSS 的工具,PostCSS 核心是将 CSS 解析成 AST,然后通过各种插件做各种转换,最终生成处理后的新 CSS,跟 Babel 在功能和实现上都类似,这里就不再详细讲解实现原理了。在语法转换上还有一个开源项目cssnext,使用最新的 CSS 标准来写 CSS,通过 cssnext 可以转换成对应的 CSS 版本。

前端项目离不开各种静态资源,静态资源指前端中常用的图片、富媒体(Video、Audio 等)、字体文件等。Webpack 中静态资源也是可以作为模块直接使用的,本小节将介绍下 Webpack 中对静态资源的管理。

使用 loader 来加载图片资源

  • file-loader:能够根据配置项复制使用到的资源(不局限于图片)到构建之后的文件夹,并且能够更改对应的链接;
  • url-loader:包含 file-loader 的全部功能,并且能够根据配置将符合配置的文件转换成 Base64 方式引入,将小体积的图片 Base64 引入项目可以减少 http 请求,也是一个前端常用的优化方式。
    {
      test: /\.(png|svg|jpg|gif)$/,
      use: {
          loader: 'url-loader',
          options: {
              limit: 3*1024 // 3k
          }
      }
    }
    

    配置 CDN 域名

    一般静态资源上线的时候都会放到 CDN,假设我们的 CDN 域名和路径为:http://bd.bxstatic.com/img/,这时候只需要修改output.publicPath即可:
    module.exports = {
      //..
      output: {
          publicPath: 'http://bd.bxstatic.com/img/'
      }
      //..
    };
    

    图片优化

    图片体积是个经常诟病的问题,一个页面中,完全一样内容的图片,在肉眼可见的范围内并不一定有差异但是体积却相差甚大,例如下面的图片:
    配置篇 - 图3
    所以图片优化也是我们在前端项目中经常做的事情,在 Webpack 中可以借助img-webpack-loader来对使用到的图片进行优化。它支持 JPG、PNG、GIF 和 SVG 格式的图片,因此我们在碰到所有这些类型的图片都会使用它。
    npm install image-webpack-loader --save-dev
    // webpack.config.js
    module.exports = {
      module: {
          rules: [
              {
                  test: /\.(jpe?g|png|gif|svg)$/,
                  loader: 'image-webpack-loader',
                  // 这会应用该 loader,在其它之前
                  enforce: 'pre'
              }
          ]
      }
    };
    

    数据

    如果我们项目需要加载的类似 JSON、CSV、TSV 和 XML 等数据,那么我们需要单独给它们配置相应的 loader。对 JSON 的支持实际上是内置的,类似于 Node.js,这意味着import Data from'./data.json'导入数据默认情况将起作用。要导入 CSV,TSV 和 XML,可以使用csv-loaderxml-loader

使用 HTML 插件来做页面展现

npm i html-webpack-plugin --save-dev
const HtmlWebPackPlugin = require('html-webpack-plugin');

module.exports = {
    mode: 'development',
    entry: {
        main: './src/index.js'
    },
    plugins: [new HtmlWebPackPlugin()]
};
const HtmlWebPackPlugin = require('html-webpack-plugin');

module.exports = {
    mode: 'development',
    entry: {
        main: './src/index.js'
    },
    plugins: [
        new HtmlWebPackPlugin({
            template: './src/index.html'
        })
    ]
};
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <meta http-equiv="X-UA-Compatible" content="ie=edge" />
        <title>Webpack</title>
    </head>
    <body>
        <h1>hello world</h1>
        <div id="app"></div>
    </body>
</html>

多页项目配置

Webpack Dev Server 本地开发服务

webpack-dev-server是一个基于 Express 的本地开发服务器(看 Roadmap 下个版本内核会从 Express 切换到 Koa)。它使用 webpack-dev-middleware 中间件来为通过 Webpack 打包生成的资源文件提供 Web 服务。它还有一个通过 Socket IO 连接着 webpack-dev-server 服务器的小型运行时程序。webpack-dev-server 发送关于编译状态的消息到客户端,客户端根据消息作出响应。

简单来说 webpack-dev-server 就是一个 Express 的小型服务器,它是通过 Express 的中间件 webpack-dev-middleware和 Webpack 进行交互的。

Webpack Dev Server 常用配置

  • devServer.historyApiFallback:配置如果找不到页面就默认显示的页面;
  • devServer.compress:启用 gzip 压缩;
  • devServer.hotOnly:构建失败的时候是否不允许回退到使用刷新网页;
  • devServer.inline:模式切换,默认为内联模式,使用false切换到 iframe 模式;
  • devServer.open:启动后,是否自动使用浏览器打开首页;
  • devServer.openPage:启动后,自动使用浏览器打开设置的页面;
  • devServer.overlay:是否允许使用全屏覆盖的方式显示编译错误,默认不允许;
  • devServer.port:监听端口号,默认 8080;
  • devServer.host:指定 host,使用0.0.0.0可以让局域网内可访问;
  • devServer.contentBase:告诉服务器从哪里提供内容,只有在你想要提供静态文件时才需要;
  • devServer.publicPath:设置内存中的打包文件的虚拟路径映射,区别于output.publicPath;
  • devServer.staticOptions:为 Expressjs 的 express.static配置参数,参考文档: http://expressjs.com/en/4x/api.html#express.static
  • devServer.clientLogLevel:在 inline 模式下用于控制在浏览器中打印的 log 级别,如error, warning, info or none;
  • devServer.quiet:静默模式,设置为true则不在控制台输出 log;
  • devServer.noInfo:不输出启动 log;
  • devServer.lazy: 不监听文件变化,而是当请求来的时候再重新编译;
  • devServer.watchOptions:watch 相关配置,可以用于控制间隔多少秒检测文件的变化;
  • devServer.headers:自定义请求头,例如自定义 userAgent 等;
  • devServer.https:https 需要的证书签名等配置。

开发环境和生产环境需要注意的区别

  • 生产环境可能需要分离 CSS 成单独的文件,以便多个页面共享同一个 CSS 文件;
  • 生产环境需要压缩 HTML/CSS/JS 代码;
  • 生产环境需要压缩图片;
  • 开发环境需要生成 SourceMap 文件;
  • 开发环境需要打印 debug 信息;
  • 开发环境需要 HMR、devServer 等功能…

按环境划分 Webpack 配置文件

webpack.config.js:所有环境的默认入口配置文件;
webpack.base.js:基础部分,即多个文件中共享的配置;
webpack.development.js:开发环境使用的配置;
webpack.production.js:生产环境使用的配置。