webpack与SPA实践之管理CSS等资源

1,982 阅读8分钟

上一篇介绍了如何使用webpack搭建一个稳定的支持本地服务、自动刷新、模块热替换、使用ES6编写JavaScript的开发环境,本篇主要介绍webpack如何处理HTML应用三大元素的另一元素 - CSS及其他诸如图片、字体文件或者数据配置文件等资源。

前言

在学习使用webpack时,我们需要明白无论它怎么设计,它的工作原理、流程是什么,最根本的它处理的还是HTML文档中的HTML标签、JavaScript、CSS、图片等资源,而且最终的处理结果依然必须是一个HTML文档,包括DOM、JavaScript、CSS,而CSS在文档中的存在方式,有三种:行内样式,内联样式,外链样式,行内样式使用方式早已不推荐,所以webpack处理CSS方式也就两种:

  • 内联样式: 以<style>标签方式在HTML文档中嵌入样式;
  • 外链样式: 打包生成CSS文件,通过<link>标签引入样式;

webpack与CSS

我们知道,webpack本质是只能处理JavaScript的,而对于其他资源,需要使用加载器和插件将其处理成JavaScript模块,然后进行模块依赖管理。webpack提供style-loadercss-loader两个加载器支持我们模块化CSS,因此可以在其他模块内直接引入。

  • 安装

    npm install --save-dev style-loader css-loader
  • 配置

在webpack配置文件的模块加载器选项中添加如下配置:


    module: {
        loaders: [
            { test: /\.css$/, loader: "style-loader!css-loader" }
        ]
    }

当然为了方便使用引用路径,还可以配置路径片段别名:


    alias: {
        styles: path.resolve(__dirname, 'src/styles/')
    }

此时,import 'styles/index.css';等同于使用相对路径,如import '../src/styles/indx.css';

  • 使用

配置好以后,假如我们在styles目录下创建了一个index.css文件,现在可以在JavaScript文件中直接引入该CSS: import 'styles/index.css';require('styles/index.css');

css内容如下:


    html, body {
            width: 100%;
        height: 100%;
    }
    .container {
            color: red;
    }

页面展示如图:

style

内联样式

前面提到了webpack处理CSS的方式有两种,第一种是以内联方式在页面<head>标签内动态插入<style>内联样式,这种方式也是webpack的默认处理方式,只需要简单配置如下加载器:


    {
        test: /\.css$/,
        exclude: /node_modules/,
        loader: 'style-loader!css-loader'
        // or 
        // loaders: ['style-loader', 'css-loader']
    }

webpack加载器解析顺序

如上面代码所示,无论是字符串语法style-loader!css-loader,亦或是数组语法['style-loader', 'css-loader'],webpack解析规则都是从右至左,依次解析并执行加载器处理文件,前一加载器处理的输出就是下一加载器处理的输入,直到最后加载器处理完成;此处即webpack先调用css-loader加载器处理css文件,然后将处理结果传递给style-loader加载器,style-loader接受该输入继续处理。

css-loader

我们已经反复强调,webpack只能处理JavaScript,所以对于其他诸如css或图片等资源需要使用加载器将其转换输出为JavaScript模块,webpack才能继续处理。

css-loader加载器的作用就是支持我们像使用JavaScript模块一样在JavaScript文件中引用CSS文件,如require ('./index.css'),所以你可以认为其作用是将CSS文件转换成JavaScript模块,于是我们可以直接通过引入JavaScript模块的方式直接引用。

参数

css-loader有两个常用参数:

  • modules: {boolean}指定是否使用CSS模块(如:local和:global设置局部或全局样式规则),默认是false,开启设置如css-loader?modules;
  • importLoaders: {number}指定css-loader加载器之前使用的加载器数量,默认是0,设置如css-loader?importLoaders=1

style-loader

无论webpack怎么处理CSS文件,最终都需要将其输出到页面,才能实际使用该CSS规则,style-loader加载器就是将CSS以内联方式插入到页面文档,即:针对每一个输入(通过require引入,使用css-loader转换为JavaScript模块,传递给style-loader作为输入),style-loader在页面<head>标签内插入一个<style>标签,该标签内样式规则即此输入内容,如下实例:

内联样式

外链样式

当然,我们并不总是希望所有样式都以内联方式存在页面中,很多时候我们也希望通过外链方式使用样式表,特别是样式规则较多的时候,webpack开发者们当然考虑了这样的需求。

webpack提供的style-loader加载器默认是以内联方式将样式插入文档,我们需要使用webpack extract-text-webpack-plugin插件以实现输出单独CSS文件。

Extract Text Plugin

  • 安装

首先安装该插件:


    npm install --save-dev extract-text-webpack-plugin
  • 配置

然后添加如下配置:


    var ExtractTextPlugin = require('extract-text-webpack-plugin');
    ...
    module: {
        loaders: [
            {
                test: /\.css$/,
                exclude: /node_modules/,
                // 老版本 loader: ExtractTextPlugin.extract('style-loader', 'css-loader')
                loader: ExtractTextPlugin.extract({
                    fallback:'style-loader',
                    use: 'css-loader'
                })
            }
        ]
    },
    plugins: [
        // 生成独立css文件
        new ExtractTextPlugin({
            filename: 'css/[name].css'
        })
    ]

运行webpack命令,我们会看到在dist/css/文件夹下生成相应的CSS文件。

  • 参数

    • filename {String | Function}

      Extract Text Plugin为每个入口生成一个CSS文件,所以对于多入口项目需要指定filename参数[name]或[id]或[contenthash]生成唯一识别文件名;

    • disable {Boolean}

      禁用此插件;

    • allChunks {Boolean}

      allChunks: true;时指定从所有模块中抽取CSS输出至单独CSS文件,包括异步引入的额外模块;此插件默认是只抽取初始模块内引入的CSS;

  • extract方法

该方法可以以参数指定加载器,然后接受该加载器的输出,进行处理。需要在加载器和插件配置中同时声明相关配置,才能实现效果;在加载器配置中调用其extract方法传入通常以下两个参数:

1. use: 将CSS转换成模块的加载器;
2. fallback: 对于不被抽取输出至单独CSS文件的CSS模块使用的加载器,上例中`style-loader`即说明以内联方式使用,该加载器通常在`allChunks: false`时处理额外的模块;

filename与output

在上一篇介绍了输出文件配置output相关内容,其中:

  • output.path是webpack处理文件后输出的路径,对于CSS文件输出依然适用,即CSS文件也将输出至该目录;
  • output.publicPath是指浏览器访问资源时的相对基础路径,规则是: output.publicPath + output.filename;

你可以看到在本系列文章实例中filename都添加了前缀目录,如cssscripts,你可能看到很多项目是不添加的,但文件入口较多时建议分类型目录输出,而且需要记得在浏览器访问资源时也需要添加该目录层级。

CSS预处理器

通常在开发较复杂的应用时,我们都会选择一种CSS的强化辅助工具,以更高效,更方便的使用CSS开发应用样式,这些拓展工具就是所说的CSS预处理器.

CSS预处理器(preprocessors)在CSS语法的基础上增加了变量 (variables)、嵌套 (nested rules)、混合 (mixins)、导入 (inline imports) 等高级功能,令CSS更加强大与优雅,有助于更好地组织管理样式文件,以及更高效地开发项目。

目前最常见的CSS预处理器有LESS,SASS,Stylus,个人用过的是前两种,使用SASS的还是居多。

SASS

  • 安装

    npm install --save-dev sass-loader

安装sass-loader以后会发现,package.json中多了一个node-sass依赖,这是使用SASS必须的。

  • 配置

然后添加以下配置:


    {
        test: /\.s[ac]ss$/,
        exclude: /node_modules/,
        loader: 'style-loader!css-loader!sass-loader'
    }

如上,配置中传递了三个加载器,相对于前文处理CSS文件的加载器,在最后面多了一个sass-loader,首先加载sass-loader加载器处理SASS文件成CSS代码,然后继续按照前文描述流程处理CSS文件。

Extract Text Plugin

和处理CSS文件一样,上述配置最终通过style-loader将转换后的CSS代码内联到页面,我们需要使用Extract Text Plugin生成单独CSS文件,以外链方式引用:


    {
         test: /\.s[ac]ss$/,
         exclude: /node_modules/,
         loader: ExtractTextPlugin.extract({
         fallback:'style-loader',
             use: [
                  'css-loader',
                  'sass-loader'
             ]
         })
    }

    ...

    // 生成独立css文件
        new ExtractTextPlugin({
            filename: 'css/[name].css'
        })

CSS后处理器

前面讲到CSS预处理器,如SASS,他们提供对CSS的拓展,包括语法拓展,高级特性拓展,如嵌套,变量,自动处理添加属性前缀等,使得我们可以以其定义的语法与模板方式更高效的编写CSS,然而这些预处理器都是另外对CSS进行拓展,各自定义了语法和模板,其处理流程是对代码进行解析处理,然后转换成CSS代码。

不同预处理器有各自的定义和规范,假如你需要从LESS转到SASS,源代码转换成本和学习成本颇高,而接下来要介绍的CSS后处理器并没有这个问题。

不同于预处理器预定义好一个语法和模板,然后对按照该语法和模板编写的代码进行处理转换成CSS,其输入是自定义语法文件,输出是CSS代码;后处理器(postprocessor)是对原生CSS代码根据配置进行处理,其输入输出依然是CSS代码。

postcss

现在最受欢迎的CSS后处理器,就是postcss:

PostCSS is a tool for transforming styles with JS plugins. These plugins can lint your CSS, support variables and mixins, transpile future CSS syntax, inline images, and more.
PostCSS是一个使用Js插件转换样式的根据,插件支持拓展CSS,如变量,混合,CSS属性语法兼容,行内图片等等功能。

特性

不同于SASS提供一个功能性拓展工具,postcss更多的是提供一个CSS高效开发工具解决方式,其本身只包含CSS解析器只能将CSS处理成一棵抽象语法树(AST),同时提供一个丰富的CSS节点树API,可以对语法树进行操作,另外它有一个高拓展性的插件系统支持我们通过引入不同插件对CSS进行处理,一个插件的输出还可以是下一个插件的输入,更值得一提的是,这些插件都是JavaScript插件,前端开发者们很容易就能根据项目需求定制自己的插件,所以可以总结几点一以下特性:

  1. postcss只处理CSS,概念简洁;
  2. 提供高拓展性的插件系统支持按需引入不同插件,实现不同处理;
  3. 使用JavaScript插件,开发者可以很方便定制项目插件;
  4. 提供CSS节点树API,可以高效的进行CSS处理;
  • 安装

在webpack中使用,需要先安装对应加载器:


    npm install --save-dev postcss-loader
插件

postcss目前有200+插件,足够满足绝大部分项目开发需求,可以查看postcss插件,我们介绍几个主要使用的插件。

Autoprefixer

回顾一下在预处理器中,如果我们需要为CSS代码添加属性前缀,需要这么实现呢?对于Sass,我们通常使用mixin,即混合宏,处理CSS属性前缀,如:


    // 定义
    @mixin prefix-animation($animation-name){
        animation:$animation-name;
        -webkit-animation:$animation-name;
    }

    // 使用
    body{
        @include prefix-animation(loading .5s linear infinite);
    }

如上,我们需要按照定义的语法和模板:先定义一个mixin,然后通过@include方式使用,最后才能输出添加前缀的CSS代码,当代码越来越多时,我们需要定义的mixin也会越来越多,而且不同预处理器定义的语法和模板都有差异,学习成本、转换成本都很可能令人难以接受。

那么postcss插件怎么处理的呢?postcss提供了Autoprefixer插件处理CSS属性前缀:

Autoprefixer插件基于Can I Use的数据,对CSS规则进行前缀处理。

  • 安装

首先还是要安装Autoprefixer:


    npm install --save-dev autoprefixer
  • 配置

添加如下配置:


    module: {
        loaders: [
            {
                 test: /\.css$/,
                 exclude: /node_modules/,
                 loaders: [
                    'style-loader',
                    'css-loader',
                    { 
                        loader: 'postcss-loader', 
                        options: {
                            plugins: [
                                require('autoprefixer')({
                                    browsers: ['last 2 versions']
                                })
                            ]  
                        } 
                    }
                ]
            }
        ]
    }

如上,我们知道postcss是一个样式开发解决方案,其特定功能需要引入插件实现,上例中在指定postcss-loader加载器时为其设置了插件配置autoprefixer;当然webpack还支持直接设置一个postcss配置文件,然后在项目根目录创建postcss.config.js配置文件,内容格式如下:


    module.exports = {
          plugins: [
            require('autoprefixer')({
                browsers: ['last 2 versions']
            })
            // or just require('autoprefixer')
          ]
    }

使用autoprefixer插件时可选传入browsers参数,可以设置添加前缀的适配范围,详细可查阅browsers配置说明

混合使用CSS预处理器与后处理器 - PreCSS

也许你迫不及待想在项目中引入postcss,又希望能继续使用CSS预处理器语法,而且需要保证以前按照某预处理器预定语法和模板(如SASS)编写的源代码继续稳定使用,不需要太多的迁移和学习成本,可以做到吗?当然可以,可以使用预处理器PreCSS插件包,另外我们需要安装一个postcss的scss解析器,因为postcss默认只包含一个CSS解析器,postcss配置文件更新如下:


    module.exports = {
        parser: require('postcss-scss'),
        plugins: [
            require('autoprefixer')({
                browsers: ['last 2 versions']
            }),
            require('precss')
        ]
    }

webpack配置文件更新配置:


    modules: {
        loaders: [
            {
                test: /\.s?[ac]ss$/,
                exclude: /node_modules/,
                // or 内联方式 loader: 'style-loader!css-loader!postcss-loader'
                loader: ExtractTextPlugin.extract({
                    fallback:'style-loader',
                    use: [
                        'css-loader',
                        'postcss-loader'
                    ]
                })
             }
        ]
    }

可以看到文件匹配规则,修改为/\.s?[ac]ss$/,可以匹配包括.sass, .scss, .css样式文件;在css-loader加载器之前添加了postcss-loader加载器(webpack加载器解析顺序为从右至左)。

当然你可以不使用precss,依然使用sass-loader,则只需要修改配置:


    loader: 'style-loader!css-loader!postcss-loader!sass-loader'

对于如下SCSS代码:


    $column: 200px;
    .menu {
        display: flex;
        width: calc(4 * $column);
    }

转换生成如下CSS代码:


    .menu {
        display: -webkit-box;
        display: -ms-flexbox;
        display: flex; 
        width: calc(4 * 200px);
    }

处理图片与字体文件

对于一个应用而言,除了需要开发HTML、CSS、JavaScript,通常还好使用到图片,字体文件,配置文件等诸多资源,那么前端工程化流程也就必然需要对这些资源进行处理与优化,最典型的说处理图片和字体文件。

在Grunt或Gulp中,我们对图片和字体文件的处理通常是将其从源目录压缩优化处理后输出至输出目录,通常是以文件目录整体进行处理,每次构建时,对所有资源,包括未使用的图片均进行处理,效率是有局限的;而webpack中一切资源文件都可以处理成模块,然后在编译时管理模块依赖,可以做到只处理存在依赖的资源(即,使用了的资源)。

图片与字体

当我们在Js模块中引入CSS文件时,其中样式规则中的背景图片,字体文件如何处理呢?webpack只能管理模块化的东西,需要将其模块化,然后使用webpack管理依赖,webpack提供了file-loader加载器:

File Loader

Instructs webpack to emit the required object as file and to return its public url.
通知webpack将引入的对象输出为文件并返回其公开资源路径。

  • 配置

    module: {
        loaders: [
              {
                test: /\.(png|svg|jpe?g|gif)$/,
                loader: [
                      'file-loader'
                ]
              }
        ]
      }
  • 说明

当我们在js文件中import Image from '../images/test.png'或在CSS文件中url('../images/test.png')时,file-loader将处理该图片并在output.path目录下输出文件,然后将../images/test.png路径替换成该输出文件路径。

注,对于html中引用的图片,需要使用[html-loader]加载器处理(npm.taobao.org/package/htm…

  • 参数

    1. emitFile: 是否输出文件;
    2. name: 指定输出文件的文件名,有几个可用内置变量:
      1. [name]: 引用资源的名称;
      2. [path]: 引用资源的相对路径;
      3. [ext]: 资源拓展名;
      4. [hash]: 资源内容的hash值,默认使用md5算法计算得到,可以指定长度值,如[hash:7]表示返回hash值前7个字符;
      5. [hashType:hash:digestType:length]: 指定hash值计算算法类型和摘要类型,及摘要长度,如sha512:hash:base64:7表示使用sha512加密算法计算hash值并且返回7个字符的base64编码字符
  • 实例

在配置时可以指定参数:file-loader?name=[name].[ext]?[hash:8]或者以配置对象方式:


    {
        test: /\.(png|svg|jpe?g|gif)$/,
        loaders: [
            // 'file-loader?name=[path][name].[ext]?[hash:8]'
            {
                loader: 'file-loader',
                query: {
                    name: '[path][name].[ext]?[hash:8]'
                }
            }
        ]
    }

对于CSS源代码:


    .wrapper {
        font-size: 18px;
        background: url('../images/test.png') no-repeat 0 0;
    }

输出CSS代码如下:


    .wrapper {
        font-size: 18px;
        background: url(assets/images/test.png?59427321) no-repeat 0 0;
    }

assetsoutput.publicPath指定值,images/test.png?59427321为配置文件中指定的name模板,在output.path目录下输出images/test.png,区别是,不会携带?后的参数。

另外,你也可以在js模板中这样使用:


    <img src={imgSrc} />

    ...
    import imgSrc from 'path/xxx.png';
Url Loader

你可能会发现前面并没有安装file-loader,因为有更好用的加载器url-loaderurl-loader加载器是file-loader的升级版,他们唯一的不同之处在于:

url-loader可以通过limit参数指定一个尺寸值,加载器会对小于该值的资源处理返回一个Data URL,以base64的方式嵌入HTML或CSS,如url-loader?limit=65000;对于大于该尺寸的资源将使用file-loader处理并且传递所有参数。

  • mimetype

还可以设置mimetype对处理文件进行过滤,如url-loader?mimetype=image/png将只处理png类型的资源。

  • 安装

    npm install --save-dev url-loader
  • 配置

该加载器对于图片和字体文件资源都适用:


    {
         test: /\.(png|svg|jpe?g|gif)$/,
         loaders: [
             // 'url-loader?name=[path][name].[ext]?[hash:8]'
             {
                 loader: 'url-loader',
                 query: {
                     limit: 6000,
                     name: '[path][name].[ext]?[hash:8]'
                 }
             }
          ]
    }, {
         test: /\.(woff|woff2|eot|ttf|otf)$/,
         loaders: [{
             loader: 'url-loader',
             query: {
                 limit: 10000,
                 name: '[path][name].[ext]?[hash:8]'
             }
         }]
    }

资源优化

完成以上配置后,已经可以在项目中很方便的引用各自资源了,但是通常我们还需要对图片字体等文件进行压缩优化处理,如Grunt中使用的imagemin插件一样压缩资源,webpack则提供了相关加载器img-loader

  • 安装

    npm install --save-dev img-loader
  • 配置

    {
        test: /\.(jpe?g|png|gif|svg)$/i,
        loaders: [
            'url-loader?name=[path][name].[ext]?[hash:8]',
            {
                loader: 'img-loader',
                options: {
                    // 根据环境判断是否启用资源压缩
                      enabled: process.env.NODE_ENV === 'production', 
                      gifsicle: {
                        interlaced: false // 替换使用渐进式渲染
                      },
                      mozjpeg: {
                        progressive: true, // 创建基准jpeg文件
                      },
                      optipng: {
                        optimizationLevel: 4, // 优化级别,0-7,值越大,压缩越多
                    }, 
                      pngquant: {
                        quality: '75-90', // 压缩质量,0-100,值越高,质量越高
                        speed: 3 // 执行速度,0-10,速度过高质量受损,不建议过高
                      },
                      svgo: {
                          plugins: [
                            { removeTitle: true }, // 去除标题信息
                              { convertPathData: false } // 转换路径为更短的相对或决定路径
                        ]
                      }
                }
              }
        ]
    }

以上为常见使用配置,更多详细配置信息请查看对应说明imagemin文档,特别注意的是上面使用了process.env.NODE_ENV当前环境变量,只有在生产环境启用图片压缩,因为压缩过程比较比较耗时,可能会降低开发、调试效率。

数据资源

对于数据类型文件资源,webpack内置支持加载解析.json文件,而其他类型则需要安装配置相应加载器,如.xml文件,需要安装并配置xml-loader

资源管理的思考

在传统或稍早一点的应用中,我们通常会将所有的图片,字体等资源放在一个基础目录下,如assets/images,但是对于那些在多项目间重复的插件代码或资源来说,每一次迁移,我们都得在一大堆图片,字体资源里寻找出我们需要迁移的资源,这对代码可重用和其独立性有一定限制,而且与现在提倡的组件化开发模式也不相符。

webpack对于资源的处理方式给组件化开发提供了很大便利,使得我们以组件为单位,可以在某一组件目录下存放所有相关的js,css,图片,字体等资源文件;组件的迁移公用成本很低。不过组件化开发并不是说不需要资源目录了,一些公用的资源依然放在项目的基础目录下。

说明

终于可以松口气,对于webpack管理CSS、图片、字体、数据资源的实践基本总结完成,其实感觉要介绍的还有很多,但是要尽量保证文章思路清晰,语句流畅,而且篇幅不能太长,水平有限,花费较多时间经历,希望能对读者有所帮助,后续篇章也会继续穿插介绍,力争本系列能较完整、较清晰地描述如何使用webpack开发SPA应用。

原创文章,转载请注明: 转载自 熊建刚的博客

本文链接地址: webpack与SPA实践之管理CSS等资源