webpack多页应用架构系列(五):听说webpack连less/css也能打包?

16
本文首发于Array_Huang的技术博客——实用至上,非经作者同意,请勿转载。
原文地址:https://segmentfault.com/a/1190000006897458
如果您对本系列文章感兴趣,欢迎关注订阅这里:https://segmentfault.com/blog/array_huang

前言

过去讲前端模块化、组件化,更多还是停留在js层面,毕竟js作为一种更典型的程序语言,在这方面的想象和操作空间都更大一些。但近年来,组件化要求得更多了,HTML/CSS/JS这三件套一件可都不能少(甚至包括其它类型的资源,比如说图片),而这样的组件,无疑是高内聚的。

文章简介

本文将介绍如何使用webpack来打包less/css(没用过sass,但毕竟也是通过loader来加载的,相信与less无异),首先是介绍相关的webpack plugin&loader,然后将介绍如何加载不同应用层次的less/css。

用到什么loader了?

《webpack多页应用架构系列(二):webpack配置常用部分有哪些?》里我就说过,webpack的核心只能打包js文件,而js以外的资源都是靠loader进行转换或做出相应的处理的。下面我就来介绍打包less/css所需要的loader。

less-loader

针对less文件,我们首先需要使用less-loader来加载。less-loader会调用所依赖的less模块对less文件进行编译(包括@import语法)。至于说less-loader所接受的参数,实质上大部分是传递给less模块使用的参数,由于我本人应用less的程度不深,因此没有传任何参数、直接就使用了。如果你之前对less模块就已经有了一套配置的话,请参考less-loader的文档进行配置。

另外,less-loader并不会针对url()语法做特别的转换,因此,如果你想把url()语句里涉及到的文件(比如图片、字体文件等)也一并用webpack打包的话,就必须利用管道交给css-loader做进一步的处理。

css-loader

针对css文件,我们需要使用css-loader来加载.css-loader的功能比较强大,一些新颖的特性比如Local Scope或是CSS Modules都是支持的。

我目前只用到了css-loader的压缩功能(Minification),对于这个功能,有一点是需要注意的,那就是如果你的代码里也和我一样,有许多为了浏览器兼容性的废弃CSS代码的话,请务必关闭autoprefixer已避免你的废弃CSS代码被css-loader删除了,形如css?minimize&-autoprefixer

上面提到css-loader会对url()语句做处理,这里稍微再说两句。在less/css里的这url()语句,在css-loader看来,就跟require()语句是一样的,只要在webpack配置文件里定义好加载各类型资源的loader,那这url()语句实际上什么资源都能处理。一般我在url()语句都会以相对路径的方式(相对于此语句所在的less/css文件)来指定文件路径;请不要使用以/开头(即相对于网站根目录,因为对于文件系统来说,这明显是令人混淆的)的路径,尽管css-loader也可以通过设置root参数来适配。

postcss-loader

习惯用postcss的童鞋们有福啦,webpack可以通过postcss-loader来兼容postcss。由于postcss只算是一个加分项,因此这里也不作过多介绍,只介绍一下如何把postcss撘进webpack,不明白的童鞋麻烦先把postcss搞懂了再看。

放上我的脚手架项目的代码:

var precss       = require('precss');
var autoprefixer = require('autoprefixer');

module.exports = {
    module: {
        loaders: [
            {
                test:   /\.css$/,
                exclude: /node_modules|bootstrap/,
                loader: 'css?minimize&-autoprefixer!postcss',
            }
        ]
    },
    postcss: function () {
        return [precss, autoprefixer({
            remove: false,
            browsers: ['ie >= 8', '> 1% in CN'],
        })];
    }
}

从loader的配置'css?minimize&-autoprefixer!postcss'上看,实际上就是先让postcss-loader处理完了再传递给css-loader。而postcss项则是postcss-loader所接受的参数,实际上就是返回一个包含你所需要的postcss's plugins的数组啦,这些plugin有各自的初始化参数,不过这些都是postcss的内容了,这里就不做介绍了。

用到什么Plugin了?

加载less/css这一块主要用到的是extract-text-webpack-plugin(下文简称为ExtractTextPlugin吧),而且由于我用的是webpack 1,因此用的也是相对应webpack 1的版本(1的文档在这里不要搞错了哈)。

ExtractTextPlugin的作用是把各个chunk加载的css代码(可能是由less-loader转换过来的)合并成一个css文件并在页面加载的时候以<link>的形式进行加载。

相对于使用style-loader直接把css代码段跟js打包在一起并在页面加载时以inline的形式插入DOM,我还是更喜欢ExtractTextPlugin生成并加载CSS文件的形式;倒不是看不惯inline的css,只是用文件形式来加载的话会快很多,尤其后面介绍用webpack来生成HTML的时候,这<link>会直接生成在<head>里,那么在CSS的加载上就跟传统的前端页面没有差别了,体验非常棒。

ExtractTextPlugin的初始化参数不多,唯一的必填项是filename参数,也就是如何来命名生成的CSS文件。跟webpack配置里的output.filename参数类似,这ExtractTextPlugin的filename参数也允许使用变量,包括[id]、[name]和[contenthash];理论上来说如果只有一个chunk,那么不用这些变量,写死一个文件名也是可以的,但由于我们要做的是多页应用,必然存在多个chunk(至少每个entry都对应一个chunk啦)。这里我是这么设置的:

new ExtractTextPlugin('[name]/styles.css'), // [name]对应的是chunk的name,我在webpack配置中是这样

[name]对应的是chunk的name,我在webpack配置中把各个entry的name都按index/indexindex/login这样的形式来设置了,那么最后css的路径就会像这样:build/index/index/styles.css,也就是跟chunk的js文件放一块了(js文件的路径形如build/index/index/entry.js)。

除了要把这初始化后的ExtractTextPlugin放到webpack配置中的plugins参数里,我们还要在loader配置里做相应的修改:

module.exports = {
    module: {
        loaders: [
            {
                test:   /\.css$/,
                exclude: /node_modules|bootstrap/,
                loader: ExtractTextPlugin.extract('css?minimize&-autoprefixer!postcss'),
            }
        ]
    },
}

如此一来,ExtractTextPlugin就算是配置好了。

如何加载不同应用层次的less/css

在我的设计中,有三种应用层次的less/css代码段:

  • 基础的、公用的代码段,包括CSS框架、在CSS框架上进行定制的CSS theme,基本上每个页面都会应用到这些CSS代码段。
  • 组件的代码段,这里的组件指的是你自己写的组件,而且组件本身含有js,并负责加载css以及其它逻辑。
  • 每个页面独有的CSS代码段,很可能只是对某些细节进行微调。

首先来回顾一下我设计的文件目录结构:

├─src # 当前项目的源码
    ├─pages # 各个页面独有的部分,如入口文件、只有该页面使用到的css、模板文件等
    │  ├─alert # 业务模块
    │  │  └─index # 具体页面
    │  ├─index # 业务模块
    │  │  ├─index # 具体页面
    │  │  └─login # 具体页面
    │  │      └─templates # 如果一个页面的HTML比较复杂,可以分成多块再拼在一起
    │  └─user # 业务模块
    │      ├─edit-password # 具体页面
    │      └─modify-info # 具体页面
    └─public-resource # 各个页面使用到的公共资源
        ├─components # 组件,可以是纯HTML,也可以包含js/css/image等,看自己需要
        │  ├─footer # 页尾
        │  ├─header # 页头
        │  ├─side-menu # 侧边栏
        │  └─top-nav # 顶部菜单
        ├─config # 各种配置文件
        ├─iconfont # iconfont的字体文件
        ├─imgs # 公用的图片资源
        ├─layout # UI布局,组织各个组件拼起来,因应需要可以有不同的布局套路
        │  ├─layout # 具体的布局套路
        │  └─layout-without-nav # 具体的布局套路
        ├─less # less文件,用sass的也可以,又或者是纯css
        │  ├─base-dir
        │  ├─components-dir # 如果组件本身不需要js的,那么要加载组件的css比较困难,我建议可以直接用less来加载
        │  └─base.less # 组织所有的less文件
        ├─libs # 与业务逻辑无关的库都可以放到这里
        └─logic # 业务逻辑

基础代码段

基础的CSS代码(实际上我的项目中用的都是less)我统一都放到src/public-resource/less目录里。我使用一个抽象的文件base.less将所有的less文件组织起来(利用@import),这样的话我用js加载起来就方便多了。

在我的脚手架项目(Array-Huang/webpack-seed)里,CSS框架我用的是bootstrap,并且使用了bootstrap-loader进行加载,因此就没有把bootstrap的CSS文件放到src/public-resource/less/base-dir目录里,这个目录里放的都是我定制的theme了。

src/public-resource/less/components-dir目录放的是某些第三方组件所用到的css,又或是不含js的组件所用到的css。其实这部分CSS是否应该归在下一类,我也考虑良久,只是由于归到下一类的话加载起来不方便,不方便原因如下:

  • 某些第三方库是要你自己加载CSS的,如果你打算写适配器来封装这些第三方库,那自然可以直接在适配器来加载CSS,这就属于下一类了;然而,有一些使用起来很简单的库,你还写适配器那就有点画蛇添足了。
  • 某些自己写的组件可能仅包含HTML和CSS,那么谁来加载CSS?

所以干脆还是交由base.less一并加载了算了。

我设计了一个common.page.js,并在每一个页面的入口文件里都首先加载这common.page.js,那么,只要我在这common.page.js里加载base.less,所有的页面都能享受到这份基础CSS代码段。

组件代码段

组件的代码我都放在了src/public-resource/components,每一个组件统一放在一个独立的目录,并由该组件的js负责加载其CSS。

页面代码段

页面独有的CSS我自然是放在该页面自己的目录里,利用该页面的入口文件进行加载。

最终生成的CSS代码都在哪?

由于我使用了ExtractTextPlugin,因此这些CSS代码最终都会生成到所属chunk的目录里成为一个CSS文件。

  • 基础代码段肯定是保存在CommonsChunkPlugin所生成的公共代码chunk所在的目录里了,在我的脚手架项目(Array-Huang/webpack-seed)里就是build/commons了(我的公共代码chunk的name是'commons')。
  • 组件代码段看情况,该组件用的页面多的话(大于CommonsChunkPlugin的minChunks参数)就会被归到跟基础代码段一起咯;反之,则哪个页面用到它,就放到哪个页面chunk的目录里咯。
  • 页面代码段就不用想了,肯定是在那个页面chunk的目录里了,毕竟才用了1次。

示例代码

诸位看本系列文章,搭配我在Github上的脚手架项目食用更佳哦(笑):Array-Huang/webpack-seed(https://github.com/Array-Huang/webpack-seed)

附系列文章目录(同步更新)

本文首发于Array_Huang的技术博客——实用至上,非经作者同意,请勿转载。
原文地址:https://segmentfault.com/a/1190000006897458
如果您对本系列文章感兴趣,欢迎关注订阅这里:https://segmentfault.com/blog/array_huang

如果觉得我的文章对你有用,请随意赞赏

你可能感兴趣的

38 条评论
mythshuazi · 2017年01月04日

真的很全面!

+1 回复

array_huang 作者 · 2016年09月13日

没写完,小手一抖就发出去了[笑哭]

回复

array_huang 作者 · 2016年09月13日

写完了写完了

回复

点点无眠 · 2016年09月15日

url-loader处理后css文件的里面图片路径

回复

点点无眠 · 2016年09月15日

怎么处理比较好

回复

array_huang 作者 · 2016年09月15日

看我这篇文章《webpack多页应用架构系列(六):听说webpack连图片和字体也能打包?》https://segmentfault.com/a/11...
在webpack配置文件里写好针对图片的url-loader配置就OK了

回复

chenjsh36 · 2016年10月13日

大牛你好,我clone了你github上的脚手架,遇到了一些问题:

module.exports = {
    module: {
        loaders: [
            {
                test:   /\.css$/,
                exclude: /node_modules|bootstrap/,
                loader: ExtractTextPlugin.extract('css?minimize&-autoprefixer!postcss'),
            }
        ]
    },
}

关于css-loader的配置,minimize 为什么我改为 -minimize 打包出来的文件还是被压缩了的,而将 -autoprefixer 改为 autoprefixer 也没有自动补全设置,我查看了 css-loader 的文档,貌似配置只有 minimize, 没有 autoprefixer的配置。

回复

一只会飞的猪 · 2016年12月05日
var ExtractTextPlugin = require("extract-text-webpack-plugin");

var packCSS = new ExtractTextPlugin('./css/style.css');
plugins: [

    new webpack.HotModuleReplacementPlugin(),
    packCSS
],

{

            test: /\.css$/,
            exclude: /node_modules/,
            // loader:'style!css'
            loader:ExtractTextPlugin.extract(['style','css'])
        },

这样写 对吗?

回复

一只会飞的猪 · 2016年12月05日

[name]那块我不会弄,填字符串吗? 你说的chunk的name是什么意思

回复

array_huang 作者 · 2016年12月05日

minimize大概是因为在webpack1下,用了UglifyJsPlugin会导致所有loader添加min配置,而autoprefixser也被定格到某个browers配置,后来我就不用了;-autoprefixer的意思是不让css-loader启用autoprefixer把一些它认为已经过期失效的css值去掉。起真正补全css前缀的其实是postcss-loader

回复

array_huang 作者 · 2016年12月05日

你可以简单地认为一个entry对应一个chunk,如果你只有一个entry的,那你可以写死这个css的名字,但是如果你有多个entry的,你就需要按'[name]/styles.css'来命名你的多个chunk的css文件。至于为什么会有多个chunk的css文件,是因为ExtractTextPlugin的机制就是按每个chunk的css来分别生成css文件的。

回复

youkelike · 2017年03月02日

请问,在css-loader处理url()的时候,可以这样理解么?
css-loader会把url(a.jpg)它转成require('./a.jpg')语句,从而触发在webpack配置文件里定义好的、可以处理这类资源的其它loader(比如url-loader,file-loader)继续做进一步处理。

回复

0

对,没错

array_huang 作者 · 2017年03月03日
0

灰常感谢

youkelike · 2017年03月03日
dlv_201 · 2017年03月26日

"我设计了一个common.page.js,并在每一个页面的入口文件里都首先加载这common.page.js,那么,只要我在这common.page.js里加载base.less,所有的页面都能享受到这份基础CSS代码段。"
这是个很巧妙的办法。但是除此之外有没有其他途径来处理 common css 的情况,比如 file1.js 中导入了 base.css,file2.js 中也导入了 base.css,如何能够让 webpack 来智能生成同一个 base.css,就像 CommonsChunkPlugin 所做的那样,只不过 CommonsChunkPlugin 貌似没法处理 css。我目前的做法是使用 npm scripts 来复制 base.css 到发布目录,总感觉怪怪的,但是也没找到其他方案。

回复

0

所以就是用这个common.page.js嘛

array_huang 作者 · 2017年03月26日
0

你这个我目前有个解决办法,不过持久化缓存有点问题,改一下CommonsChunkPlugin 的配置就行了

new webpack.optimize.CommonsChunkPlugin({

        name: "vendor",
        minChunks: function (module, count) {
            var resource = module.resource
            return resource && (/\.(css|scss|less)$/).test(resource) && count > 1
        }
    }),
独木桥先生 · 2017年12月22日
西安小哥 · 2017年09月02日

我发布打包的时候,某些样式没有打包进来

回复

0

你没引用这些样式吧,大概

array_huang 作者 · 2017年09月02日
0

你现在方便不?这个bug难倒我了

西安小哥 · 2017年09月02日
0

我的是一个多页面系统,别的页面引入这个组件是样式加进来了。但是有一个页面引入这个组件时,这个组件样式没有进来

西安小哥 · 2017年09月02日
momo · 2017年09月04日

很不错 收藏了

回复

callmeHEN · 2017年09月12日

大哥 我想问下 build目录会生成在哪,我运行起来了,但是并没有发现生成的build目录呢

回复

0

webpack-config/base/dir-vars.config.js里面定义了

array_huang 作者 · 2017年09月12日
0

@array_huang
我看到了的
moduleExports.staticRootDir = path.resolve(__dirname, '../../'); // 项目根目录
moduleExports.buildDir = path.resolve(moduleExports.staticRootDir, './build'); // 存放编译后生成的所有代码、资源(图片、字体等,虽然只是简单的从源目录迁移过来)
但是我在项目根目录没有看到build文件夹产生。我只是想看看build里的文件。这个没搞懂

callmeHEN · 2017年09月12日
0

@callmeHEN 你要npm run build

array_huang 作者 · 2017年09月12日
vcxiaohan · 2017年12月14日

你好, 请问我如何把后缀名为vue的单文件里面的css进行提取,能实现吗

回复

0

vue-loader本来就有这个功能哇,去看vue-loader的文档

array_huang 作者 · 2017年12月15日
独木桥先生 · 2017年12月22日

你这个不能把公共的css单独提取出来吧

回复

0

结合这一篇就可以

array_huang 作者 · 2017年12月22日
0

我目前是这样做的
不过加了manifest之后公共js文件的hash值每次打包后会变,公共css的hash不会变,对做缓存有点影响
new webpack.optimize.CommonsChunkPlugin({

        name: "vendor",
        minChunks: function (module, count) {
            var resource = module.resource
            return resource && (/\.(css|scss|less)$/).test(resource) && count > 1
        }
    }),
独木桥先生 · 2017年12月22日
0

你这里给的另外一篇其实也没有去提取公共css吧,还是只提取了js

独木桥先生 · 2017年12月22日
gaollard · 2018年02月27日

我认为博主这里应该说明一下,你的 base.less 导出的不是一系列的 css 变量定义,而是导出 一些公用的 css 类名。

回复

载入中...