前言 作为前端每天都在打交道的 webpack
,学精是很有必要的,尤其是负责文件解析的 webpack-loader
(以下简称 loader),它作为 webpack
的加载器成了打包必不可少的一环。本文将从实现层面洞察 loader
的实现原理,相信看完本文,你自己也可以写一个属于自己的 loader
,废话不多说,让我们开始吧!
准备工作 1. 我们需要个能调试 loader
的 webpack
环境,控制台执行以下指令: 1 2 npm init npm install webpack webpack-cli webpack-dev-server babel-loader @babel/core -D
2. 创建 webpack.config.js
、打包入口 main.js
和我们需要加载的 css 文件 color.css
1 2 3 touch webpack.config.js touch main.js touch color.css
3. 写入基本的打包配置 1 2 3 4 5 6 7 8 9 10 11 12 const path = require ("path" );module .exports = { entry : "./main.js" , output : { path : path.resolve(__dirname, "dist" ), filename : "output.bundle.js" , }, devServer : { contentBase : path.join(__dirname, "dist" ), port : 9000 , }, };
4. 创建我们的 loader
文件夹和 loader
文件 1 2 3 4 mkdir my-loader cd my-loader touch css-loader.js touch style-loader.js
5. 由于 style-loader
是为了作用于浏览器端 ,我们需要通过页面来看效果,创建html 文件,再对webpack配置
进行修改 1 2 npm install html-webpack-plugin -D touch index.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const path = require('path'); + const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: './main.js', output: { path: path.resolve(__dirname, 'dist'), filename: 'output.bundle.js' }, module: { rules: [ { test: /\.js$/, use: 'babel-loader' } ] }, devServer: { contentBase: path.join(__dirname, 'dist'), port: 9000 }, + plugins: [new HtmlWebpackPlugin({ template: './index.html' })] };
6. 创建 loader
配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: './main.js', output: { path: path.resolve(__dirname, 'dist'), filename: 'output.bundle.js' }, module: { rules: [ { test: /\.js$/, use: 'babel-loader' }, + { + test: /\.css$/, + use: [ + { + loader: path.resolve('./my-loader/style-loader') + }, + { + loader: path.resolve('./my-loader/css-loader') + } + ] + } ] }, devServer: { contentBase: path.join(__dirname, 'dist'), port: 9000 }, plugins: [new HtmlWebpackPlugin({ template: './index.html' })] };
至此我们的环境搭建已经完成,不出意外的话目录应该如下所示 1 2 3 4 5 6 7 8 9 ├── color.css ├── index.html ├── main.js ├── my-loader | ├── css-loader.js | └── style-loader.js ├── package-lock.json ├── package.json └── webpack.config.js
实现 css-loader css-loader
作为解析 css 文件的主要 loader
,主要目的是为了解析通过 import/requrie
引入的 css
样式文件,根据 webpack 官网说明 ,所有 loader
都是导出为一个函数的 node
模块。
1 2 3 4 module .exports = function (source ) { return source; };
现在让我们将它变成一个能处理 css
文件的 loader
!
准备工作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 body { background-color : #20232a ; } span { font-size : 40px ; font-weight : bold; margin : 0 16px ; text-transform : capitalize; } .react { color : #61dafb ; } .vue { color : #4fc08d ; } .angular { color : #f4597b ; }
1 2 3 4 5 6 7 8 9 10 11 import style from "./color.css" ; window .onload = () => { const body = document .body; const frameworks = ["react" , "vue" , "angular" ]; frameworks.forEach((item ) => { const span = document .createElement("span" ); span.innerText = item; span.setAttribute("class" , style[item]); body.appendChild(span); }); };
编写 loader
获取 css文本
,这一步 webpack
已经自动帮我们处理了,通过匹配 .css
文件后缀,自动获取 css文本
,也就是 source
参数
1 "body {\n background-color: #20232a;\n}\nspan {\n font-size: 40px;\n font-weight: bold;\n margin: 0 16px;\n text-transform: capitalize;\n}\n.react {\n color: #61dafb;\n}\n.vue {\n color: #4fc08d;\n}\n.angular {\n color: #f4597b;\n}\n"
解析 css文本
,通过正则提取其中的 css类选择器
(注:因本文只是简易实现,所以只考虑单类名的情况)
1 2 3 4 5 6 7 8 module .exports = function (source ) { const reg = /(?<=\.)(.*?)(?={)/g ; const classKeyMap = Object .fromEntries( source.match(reg).map((str ) => [str.trim(), str.trim()]) ); return source; };
得到如下 classKeyMap
1 2 3 4 5 { react : "react" , vue : "vue" , angular : "angular" , }
根据 loader 的返回定义, loader
返回的结果应该是 String
或者 Buffer
(被转换为一个 string),所以我们输出的结果应该转成 string
的形式,需要输出的有两个东西,一个处理过的 css
的源文件,另一个是类名的 映射Map
(为了让 js 文件读取到 css),为了标识这两个变量,用特殊的 key
来标注,如下所示
1 2 3 4 5 module .exports = function (source ) { const reg = /(?<=\.)(.*?)(?={)/g ; const classKeyMap = Object .fromEntries(source.match(reg).map((str ) => [str.trim(), str.trim()])); return `/**__CSS_SOURCE__${source} *//**__CSS_CLASSKEYMAP__${JSON .stringify(classKeyMap)} */` ;
至此一个简易的 css-loader
就完成了!
添加 css-module
我们现在再尝试给他加上 css-module
的功能,新增 webpack
配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 { test: /\.css$/, use: [ { loader: path.resolve('./my-loader/style-loader') }, { loader: path.resolve('./my-loader/css-loader'), + options: { + module: true + } } ] }
为了解析 loader
的配置,官方提供了读取配置的 loader-utils
和 校验配置的 schema-utils
,我们先安装他们
1 npm install loader-utils schema-utils -D
改造一下我们之前的 loader
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const getOptions = require ("loader-utils" ).getOptions;const validateOptions = require ("schema-utils" ).validate;const schema = { type : "object" , properties : { module : { type : "boolean" , }, }, }; module .exports = function (source ) { const options = getOptions(this ); validateOptions(schema, options, "css-loader" ); const reg = /(?<=\.)(.*?)(?={)/g ; const classKeyMap = Object .fromEntries( source.match(reg).map((str ) => [str.trim(), str.trim()]) ); return `/**__CSS_SOURCE__${source} *//**__CSS_CLASSKEYMAP__${JSON .stringify( classKeyMap )} */` ;};
schema-utils
保证我们参数的可靠性,如果不符合 schema
的类型预期,webpack
会抛出异常
1 2 3 4 5 6 Module build failed (from ./my-loader/css-loader.js): ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. - configuration.module should be a boolean. at validate (/Users/ redjue/Desktop/Webpack Loader/node_modules/schema-utils/dist/validate.js:104 :11 ) at Object .module.exports (/Users/ redjue/Desktop/Webpack Loader/my-loader/css-loader.js:19 :5 ) @ ./main.js 1 :0 -32 9 :31 -36
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 const getOptions = require ("loader-utils" ).getOptions;const validateOptions = require ("schema-utils" ).validate;const schema = { type : "object" , properties : { module : { type : "boolean" , }, }, }; function hash ( ) { const s4 = () => (((1 + Math .random()) * 0x10000 ) | 0 ).toString(16 ).substring(1 ); return s4() + s4(); } module .exports = function (source ) { const options = getOptions(this ); validateOptions(schema, options, "css-loader" ); const reg = /(?<=\.)(.*?)(?={)/g ; const classKeyMap = Object .fromEntries( source.match(reg).map((str ) => [str.trim(), str.trim()]) ); if (options.module) { const cssHashMap = new Map (); source = source.replace(reg, (result ) => { const key = result.trim(); const cssHash = hash(); cssHashMap.set(key, cssHash); return `${key} -${cssHash} ` ; }); Object .entries(classKeyMap).forEach((item ) => { classKeyMap[item[0 ]] = `${item[1 ]} -${cssHashMap.get(item[0 ])} ` ; }); } return `/**__CSS_SOURCE__${source} *//**__CSS_classKeyMap__${JSON .stringify( classKeyMap )} */` ;};
至此支持 css-module
的 css-loader
就编写完成了!接下来让我们编写 style-loader
,让样式展现到页面上
实现 style-loader style-loader
负责把 css样式
放进 dom 中,实现相对比 css-loader
容易些
1 2 3 4 5 6 7 8 9 10 11 12 13 14 module .exports = function (source ) { const cssSource = source.match(/(?<=__CSS_SOURCE__)((.|\s)*?)(?=\*\/)/g ); const classKeyMap = source.match( /(?<=__CSS_classKeyMap__)((.|\s)*?)(?=\*\/)/g ); let script = `var style = document.createElement('style'); style.innerHTML = ${JSON .stringify(cssSource)} ; document.head.appendChild(style); ` ; if (classKeyMap !== null ) { script += `module.exports = ${classKeyMap} ` ; } return script; };
有了 css-loader
解析的数据,style-loader
做的事情很简单,负责把样式放到页面上,以及对 classkeyMap
的导出。
使用 loader 完成了 css-loader
和 style-loader
的编写,让我们看看他实际运作的效果!
由于默认会安装 webpack5.x
的版本,dev-server
的指令已经被 webpack serve
指令所替代,所以我们执行以下命令启动服务
1 2 3 4 5 redjue@fengji:Webpack Loader ⍉ ➜ webpack serve Debugger attached. ℹ 「wds」: Project is running at http://localhost:9000/ ℹ 「wds」: webpack output is served from undefined ℹ 「wds」: Content not from webpack is served from /Users/redjue/Desktop/Webpack Loader/dist
打开浏览器访问 http://localhost:9000/ 如果看到以下效果,说明我们的 style-loader
生效了!
让我们再看看 css-module
有没有生效,打开控制台
很好也生效了,至此我们成功运行了自己编写的 loader
写在最后 当然官方的css-loader
和 style-loader
还要复杂的多,本文主要是为了让大家了解怎么去编写一个 loader
,码字不易,点个赞再走呗~
欢迎关注笔者的公粽号 【前端可真酷】,不定期分享原创好文,让我们一起玩最 cool 的前端!