css 打包内部原理

假设有一个 css 文件,在src文件夹下

  1. /** ./src/index.css **/
  2. body{
  3. background: red;
  4. }
  5. .cc{
  6. background: green;
  7. }

通常我们使用 import 导入css,如下:

/** ./src/index.js **/
import './index.css'

let div = document.createElement('div');
div.innerHTML = 'hello world';
div.className = 'cc';
document.body.appendChild(div);

这样 css 就生效了。
image.png
其实它中间经过了两个步骤,如下:

  1. 将 css 文件里的内容转化为字符串,通过 module.exports 导出,如下:

    module.exports = `
    body{
     background: red;
    }
    `
    
  2. 然后创建一个style标签,将该字符串放入其中,最后将它插入到html中,如下: ``javascript let style = document.createElement('style').innerHTML( body{ background: red; } `);

document.head.appendChild(style);

以上两个步骤这就是 css 打包的过程原理。分别对应了两个 loader (css-loader,style-loader)。
<a name="SLEWd"></a>
## css 打包实现
loader 其实就是翻译官(加载器),主要用于转化模块。<br />首先要安装css-loader,style-loader。
```shell
yarn add css-loader style-loader --dev

下面是 webpack 配置

/** ./webpack.config.js **/
let path = require('path');
let { CleanWebpackPlugin } = require('clean-webpack-plugin');
let HtmlWebpackPlugin = require('html-webpack-plugin');
let htmlPlugin = ['index'].map(chunkName => {
    return new HtmlWebpackPlugin({
        filename: `${chunkName}.html`,
        inject: 'body',
        chunks: [chunkName]
    })
})
module.exports = {
    mode: 'development',
    entry: {
        index: './src/index.js'
    },
    output: {
        filename: '[name].[contenthash:8].js',
        path: path.resolve(__dirname, 'dist')
    },
    module: {
        rules: [
            {
                test: /\.css$/,
                use: ['style-loader', 'css-loader']
            }
        ]
    },
    plugins: [
        new CleanWebpackPlugin({
            cleanOnceBeforeBuildPatterns: ['**/*']
        }),
        ...htmlPlugin
    ]

}

style-loader

style-loader 的内部原理是将 css 文件转换成字符串放入 style 标签中,然后将style 标签插入 header。
后面我们将自己实现一个 style-loader。

css-loader

css-loader的作用是帮我们分析出各个css文件之间的关系,把各个css文件合并成一段css。
下面是 css-loader 的一些常用属性:

module.exports = {
  //...
  module:{
    rules:[
      {
        test:/\.css$/,
        use:['style-loader', {
          loader: 'css-loader',
          options:{
            url: false, //启用/禁用 url 解析 url(./rj.jpg)
            import: false,//是否允许或者说禁用 @import语法处理 @import "base.css"
            modules: true,//是否允许 css 模块
            sourceMap: true, //是否生成 source-map
            //importLoaders: true, 放在 css 兼容性的时候演示
            esModule: true,// 默认情况下,css-loader 生成使用 ES Module 的模块对象,如果是false的话,不包装成ESMODULE  
          }
        }]

      }
    ]
  }

  //...
}

实现模块化(modules)

实现模块化需要用到上述的 modules 属性。

背景

不同的css文件,相同的类名可能导致样式被意外覆盖。css模块化可以使样式只作用于当前模块。原理就是将类名唯一化。

实现

设置 css-loader 的 modules 选项为 true。

let path = require('path');
let { CleanWebpackPlugin } = require('clean-webpack-plugin');
let HtmlWebpackPlugin = require('html-webpack-plugin');

let htmlPlugins = ['index'].map(chunkName => {
  return new HtmlWebpackPlugin({
    filename:`${chunkName}.html`,
    inject: 'body',
    chunks:[chunkName]
  })
});

module.exports = {
  mode: 'development',
  entry: {
    index: './src/index.js'
  },
  output: {
    filename: '[name].[contenthash:8].js',
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test:/\.css$/,
        use: ['style-loader', {
          loader: 'css-loader',
          options: {
            modules: true
          }
        }]
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin({
      cleanOnceBeforeBuildPatterns: ['**/*']
    }),
    ...htmlPlugins
  ]
}

配置完成之后,模块化的 css 引用也要改变如下:

/** ./src/index.js **/
import style from './index.css';

let div = document.createElement('div');
div.innerHTML = 'hello world';
div.className = style['cc'];

document.body.appendChild(div);

结果

打包之后我们就会发现我们的 cc 类名变成了一串唯一的hash码,这就保证了在其他页面不会被样式覆盖或覆盖其他页面的样式。
image.png

解析路径(url)

案例

/** ./src/index.js 入口文件 **/
import './index.css';
/** ./src/index.css **/
body {
  background: red;
  background-image: url(./test.jpeg);
}
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
  devServer: {
    port: 8080,
    open: true,
    compress: true,
    static: './dist',
  },
  entry: {
    index: './src/index.js'
  },
  output:{
    path: path.resolve(__dirname, './dist'),
    filename: '[name].[hash:8].js'
  },
  module:{
    rules: [
      {
        test: /\.css$/,
        use:[
          'style-loader',
          { 
            loader:'css-loader',
            options: {
              url: false
            }
          }
        ]
      }
    ]
  },
  plugins: [
    new webpack.CleanPlugin(),
    new HtmlWebpackPlugin({
      filename:'index.html'
    })
  ]
}

不解析的情况

我们们先将 url 设置为false,然后打包看结果。
打包文件如下:
image.png
页面结果如下:
image.png
我们发现 css 并没有解析 url上的路径。

解析的情况

下面我们设置为 true 试一下,如下:

const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
  devServer: {
    port: 8080,
    open: true,
    compress: true,
    static: './dist',
  },
  entry: {
    index: './src/index.js'
  },
  output:{
    path: path.resolve(__dirname, './dist'),
    filename: '[name].[hash:8].js'
  },
  module:{
    rules: [
      {
        test: /\.css$/,
        use:[
          'style-loader',
          { 
            loader:'css-loader',
            options: {
              url: true
            }
          }
        ]
      }
    ]
  },
  plugins: [
    new webpack.CleanPlugin(),
    new HtmlWebpackPlugin({
      filename:'index.html'
    })
  ]
}

打包文件如下:
image.png
页面文件如下:
image.png
这是我们就会发现图片被打包了,并且在使用时解析了url。

解析 @import 语法(import)

案例

/** ./src/index.js 入口文件 **/
import './index.css';
/** ./src/index.css **/
@import url('./other.css');
body {
  background: red;
  background-image: url(./test.jpeg);
}

.index {
  color: blue;
}
/** ./src/other.css **/
.other{
  color: green;
}
/** ./webpack.config.json **/
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
  devServer: {
    port: 8080,
    open: true,
    compress: true,
    static: './dist',
  },
  entry: {
    index: './src/index.js'
  },
  output:{
    path: path.resolve(__dirname, './dist'),
    filename: '[name].[hash:8].js'
  },
  module:{
    rules: [
      {
        test: /\.css$/,
        use:[
          'style-loader',
          { 
            loader:'css-loader',
            options: {
              url: false,
              import: false
            }
          }
        ]
      }
    ]
  },
  plugins: [
    new webpack.CleanPlugin(),
    new HtmlWebpackPlugin({
      filename:'index.html',
      inject: 'body',
      template:'./public/index.html'
    })
  ]
}

不解析的情况

我们先把 import 设置为 false,看一下打包结果
image.png
再看一下页面结果
image.png
我们发现 @import 语法被当作字符串插入,并没有被解析,字也没有改变颜色。

解析的情况

我们把 import 改成 true 试试看。如下:

const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
  devServer: {
    port: 8080,
    open: true,
    compress: true,
    static: './dist',
  },
  entry: {
    index: './src/index.js'
  },
  output:{
    path: path.resolve(__dirname, './dist'),
    filename: '[name].[hash:8].js'
  },
  module:{
    rules: [
      {
        test: /\.css$/,
        use:[
          'style-loader',
          { 
            loader:'css-loader',
            options: {
              url: false,
              import: true
            }
          }
        ]
      }
    ]
  },
  plugins: [
    new webpack.CleanPlugin(),
    new HtmlWebpackPlugin({
      filename:'index.html',
      inject: 'body',
      template:'./public/index.html'
    })
  ]
}

看一下打包结果,发现 @import 已经没有的,说明被解析了
image.png
再看一下页面结果
image.png
发现 other 变成了绿色,样式也被成功解析。

ES模块化(esModule)

esModule 和 modules 不是一个东西,不能混为一谈。
esModule 是前端模块化,js 和 css 都有。而 modules 属性只是针对 css 作用域的。
当 esModule 设置为 true 时,通过 require 引入,会默认包一层 default。我们用找几个例子展示一下。

js 案例

/** ./title.js **/
module.exports = "title";
/** ./use.js **/
let title = require('./title');
console.log(title);

//如果 esModule 为 true,打印的值为 { defalut : 'title' }
//如果 esModule 为 false,则会直接打印 'title'