博客
电影
宝箱
友链
关于
<
Webpack基于scss生成css独立文件
一百年来700部高分电影数据分析报告
>
Webpack深入浅出loader
作者:
Cifer
类别: 技术
时间:2019-09-30 21:17:36
字数:11394
版权所有,未经允许,请勿转载,谢谢合作~
#### 前言 假设你了解webpack的基本概念及安装了webpack。 ```` npm install webpack webpack-cli --save-dev ```` #### webpack loader 概念 loader是webpack预处理转换器,或者说,是一个导出为函数的 JavaScript 模块。它可以在构建阶段,把上一次loader产生的结果传入进去,进行特定逻辑的转换,返回转换之后的结果,结果需为String或Buffer。 一、 loader应该遵循单一原则,一个loader只做一件事。 二、loader是链式结构,由右至左依次执行,下一次的source参数是上一次的返回source。 三、loader有内置api, 方便写loader时实现同步、异步、缓存等各项功能。 #### webpack loader 使用 事先安装想使用的loader,比如与sass,css有关的loader ```` npm install style-loader css-loader sass-loader node-sass --save-dev ```` loader 有三种使用方式 ###### 配置 在根目录webpack.config.js中配置rules: ```javascript const path = require('path'); module.exports = { mode: 'development', entry: { index: './src/js/index.js', }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name].js' }, module: { rules: [ { test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] }, ] } }; ``` 上述loader生效的前提是scss文件被入口文件index.js引用 ```` import indexCss from '../css/index.scss'; ```` 它的流程大致如下: a.因为webpack中资源是以模块的形式引用的,当js中引了scss,则会加载这个scss文件 b.配置中rules正则/\.scss$/匹配上这个文件后,那么从右至左使用上述三个loader处理scss文件数据流。 c.先使用sass-loader把scss文件编辑成css d.css-loader把这个css文件生成一个js模块 e. style-loader 会把这个样式部分插入到引用了index.js的html的head中 ###### 内联 也可以直接使用内联处理单个文件,在index.js中引入: ```` import indexCss from 'style-loader!css-loader!sass-loader!../css/index.scss'; ```` 与rules同理,从右至左,但使用!分隔 ###### CLI 在启动命令行中使用loader ```` ./node_modules/.bin/webpack --module-bind 'scss=style-loader!css-loader!sass-loader' ```` 因为上述webpack没有全局安装,所以需用到npm bin 三种使用方法中,配置是我们常用也是推荐的方法。 #### 常用 webpack loader | loader | 简介 | | ------------ | ------------ | | sass-loader | scss翻译成css | | css-loader | css文件变成js模块 | | style-loader | css模块插入至html head中 | | ts-loader | typescript 转成 js | | babel-loader | es2015+转成es5 | | html-loader | html页面片的形式引入静态资源 | | file-loader | 生成一个文件至输出目录 | | url-loader | 与file-loader类似,但可返回DataUrl | | svg-inline-loader | 把svg字符串当成svg标签 | | raw-loader | 把文件作为字符串导入 | | extract-loader | 从js资源模块中提取资源 | 更多loader 见 <a href="https://webpack.js.org/loaders/" target="_blank" ref="nofollow">https://webpack.js.org/loaders/</a> ###### webpack基于scss生成css独立文件 <a href="https://www.boatsky.com/blog/89" target="_blank">https://www.boatsky.com/blog/89</a> #### 实现 webpack loader 以上一些开源的npm loader package,我们也可以自定义loader,这些自定义loader可以发布至npm或者放在项目目录中: 下面我们实现一个json格式化loader: src/loaders/json-format-loader.js ```javascript module.exports = function jsonFormattingLoader(content) { try { const contentJson = JSON.parse(content) const indentSize = this.query && this.query.indent_size ? this.query.indent_size : 2 return JSON.stringify(contentJson, null, indentSize) } catch(e) { console.log(e) return content } } ``` 这个loader接受外部的文件的string,然后接受设定格式化空格长度的参数indent_size。 webpack.config.js加入以下配置 ```javascript module.exports = { mode: 'development', entry: Object.assign(entries('./src/js/entry/**/*.js', 'js'), entries('./src/json/*.json', 'json')), output: { path: path.resolve(__dirname, 'public/static/dist'), filename: '[name].[chunkhash].js' }, resolveLoader: { modules: [ 'node_modules', path.resolve(__dirname, 'src/loaders') ] }, module: { rules: [ { test: /\.json$/, type: 'javascript/auto', use: [ { loader: 'file-loader', options: { name() { return 'json/[name].json'; }, } }, { loader: 'json-format-loader', options: { indent_size: 4 } }, ] } ] }, plugins: [ new CleanWebpackPlugin(), new FilterCreatePlugin(), ] }; ``` 解析: 一、增加json的入口,同时在FilterCreatePlugin中屏蔽生成json js。 二、loaderl默认只从项目根目录的node_modules中寻找,上例中自定义loader不在其中,因此需要在resolveLoader中多配置一个自定义loader目录。 三、调用loader的options其中就是loader内部的this.query。 四、生成json时,使用了file-loader,但是webpack新版本都不再默认把json当成js模块,所以需要指定type: 'javascript/auto'。 ###### loader 传参 上述例子return 资源本身,无法传参,但this.callback可以帮我们做到: 传参规则: ```javascript this.callback( err: Error | null, content: string | Buffer, sourceMap?: SourceMap,// 选填 meta?: any // 选填,仅用在loader之间传递,webpack无视 ); ``` 改造实例: ```javascript module.exports = function jsonFormattingLoader(content, map, meta) { try { const contentJson = JSON.parse(content) const indentSize = this.query && this.query.indent_size ? this.query.indent_size : 2 this.callback(null, JSON.stringify(contentJson, null, indentSize), map, meta) } catch(e) { console.log(e) this.callback(null, content, map, meta) } } ``` ###### 使用this.async 实现异步loader ```javascript module.exports = function jsonFormattingLoader(content, map, meta) { const callback = this.async() try { setTimeout(() => { const contentJson = JSON.parse(content) const indentSize = this.query && this.query.indent_size ? this.query.indent_size : 2 callback(null, JSON.stringify(contentJson, null, indentSize), map, meta) }, 1) } catch(e) { console.log(e) setTimeout(() => { callback(null, content, map, meta) }, 1) } } ``` 因为Node是单线程,异步可以非阻塞,提高速度。 ###### pitch loader loader的顺序是由右至左,但在pitch方法中,正好相反: ```javascript [ { loader: 'file-loader', options: { name() { return 'json/[name].json'; }, } }, 'json-format-loader', 'json-test-loader', ] ``` 原json-format-loader 修改成: ```javascript module.exports = function jsonFormattingLoader(content) { const callback = this.async() try { console.log('json-format-loader normal') console.log(this.data) const contentJson = JSON.parse(content) const indentSize = this.query && this.query.indent_size ? this.query.indent_size : 2 callback(null, JSON.stringify(contentJson, null, indentSize)) } catch(e) { console.log(e) callback(null, contents) } } module.exports.pitch = function(remainingRequest, precedingRequest, data) { console.log('json-format-loader pitch') console.log(data) data.value = 2; console.log(data) }; ``` 增加一个json-test-loader: ```javascript module.exports = function jsonFormattingLoader(content) { console.log('json-test-loader normal') console.log(this.data) this.callback(null, content) } module.exports.pitch = function(remainingRequest, precedingRequest, data) { console.log('json-test-loader pitch') console.log(data) data.value = 4 console.log(data) }; ``` 运行打印结果: ``` json-format-loader pitch {} { value: 2 } json-test-loader pitch {} { value: 4 } json-test-loader normal { value: 4 } json-format-loader normal { value: 2 } ``` 在两个自定义loader中,可以发现,其执行有顺序是 json-format-loader pitch -> json-test-loader pitch -> json-test-loader normal -> json-format-loader normal 并且data参数可以在自定义loader不同阶段共享,但不能是不同loader之间共享,想要不同loader之间传递不同的数据,应该使用this.callback的第4个参数meta 如果在某个loader的pitch 方法return,那么该loader 右边的其他loader都不会执行,起到中止或者说跳过loader的作用。 自定义loader中,this上下文还有其他的属性,请参考 <a href="https://www.webpackjs.com/api/loaders/" target="_blank" ref="nofollow">https://www.webpackjs.com/api/loaders/</a> #### webpack loader的原理 如果你不了解webpack工作流程,请查看我另一篇博客:待续。 这里假设你已经熟悉webpack的大概工作流程。 loader是在资源编译对象Compilation中执行,相关代码: ./node_modules/webpack/lib/Compilation.js ```javascript const NormalModuleFactory = require("./NormalModuleFactory"); class Compilation extends Tapable { compile(callback) { const params = this.newCompilationParams(); } createNormalModuleFactory() { const normalModuleFactory = new NormalModuleFactory( this.options.context, this.resolverFactory, this.options.module || {} ); this.hooks.normalModuleFactory.call(normalModuleFactory); return normalModuleFactory; } } ``` 可知其在编译阶段,调用模块编译的模块工厂类,该工厂模式最终调用doBuild: ./node_modules/webpack/lib/NormalModule.js ```javascript const { getContext, runLoaders } = require("loader-runner"); class NormalModule extends Module { doBuild(options, compilation, resolver, fs, callback) { runLoaders( { resource: this.resource, loaders: this.loaders, context: loaderContext, readResource: fs.readFile.bind(fs) }, (err, result) => {} ) } } ``` 可以发现它其实是使用loader-runner来实现的,并传入四个参数: resource 指文件路径 loaders 指webpack.config.js中的loader array loaderContext loader共享的数据 readResource 是读取文件方法 fs.readFile.bind(fs) #### loader-runner 原码分析 loader-runner 的入口 ./node_modules/loader-runner/lib/LoaderRunner.js ```javascript exports.runLoaders = function runLoaders(options, callback) { loaderContext.loaderIndex = 0; var processOptions = { resourceBuffer: null, readResource: readResource }; iteratePitchingLoaders(processOptions, loaderContext, function(err, result) { }); } ``` 使用pitch模块迭代触发这个资源的loader列表。 再来看看iteratePitchingLoaders方法 ```javascript function iteratePitchingLoaders(options, loaderContext, callback) { // abort after last loader if(loaderContext.loaderIndex >= loaderContext.loaders.length) return processResource(options, loaderContext, callback); var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex]; // iterate if(currentLoaderObject.pitchExecuted) { loaderContext.loaderIndex++; return iteratePitchingLoaders(options, loaderContext, callback); } // load loader module loadLoader(currentLoaderObject, function(err) { if(err) { loaderContext.cacheable(false); return callback(err); } var fn = currentLoaderObject.pitch; currentLoaderObject.pitchExecuted = true; if(!fn) return iteratePitchingLoaders(options, loaderContext, callback); runSyncOrAsync( fn, loaderContext, [loaderContext.remainingRequest, loaderContext.previousRequest, currentLoaderObject.data = {}], function(err) { if(err) return callback(err); var args = Array.prototype.slice.call(arguments, 1); if(args.length > 0) { loaderContext.loaderIndex--; iterateNormalLoaders(options, loaderContext, args, callback); } else { iteratePitchingLoaders(options, loaderContext, callback); } } ); }); } ``` 从原码可知,iteratePitchingLoaders会执行两次,第一次是为了编译,第二次是为了查看是否pictch有中断,如果有中断则第二次不再执行loadLoader,这与我们上述demo所述吻合。 但pitch模式完成或中断,则会执行对应的normal模式 ```javascript function processResource(options, loaderContext, callback) { // set loader index to last loader loaderContext.loaderIndex = loaderContext.loaders.length - 1; var resourcePath = loaderContext.resourcePath; if(resourcePath) { loaderContext.addDependency(resourcePath); options.readResource(resourcePath, function(err, buffer) { if(err) return callback(err); options.resourceBuffer = buffer; iterateNormalLoaders(options, loaderContext, [buffer], callback); }); } else { iterateNormalLoaders(options, loaderContext, [null], callback); } } function iterateNormalLoaders(options, loaderContext, args, callback) { console.log(111) if(loaderContext.loaderIndex < 0) return callback(null, args); var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex]; // iterate if(currentLoaderObject.normalExecuted) { loaderContext.loaderIndex--; return iterateNormalLoaders(options, loaderContext, args, callback); } var fn = currentLoaderObject.normal; currentLoaderObject.normalExecuted = true; if(!fn) { return iterateNormalLoaders(options, loaderContext, args, callback); } convertArgs(args, currentLoaderObject.raw); runSyncOrAsync(fn, loaderContext, args, function(err) { if(err) return callback(err); var args = Array.prototype.slice.call(arguments, 1); iterateNormalLoaders(options, loaderContext, args, callback); }); } ``` 无论是pitch模式还是noraml模式,loader最终执行的都是runSyncOrAsync方法: ```javascript function runSyncOrAsync(fn, context, args, callback) { var result = (function LOADER_EXECUTION() { return fn.apply(context, args); }()); } ``` 可知都是执行fn函数为目标,而这个fn函数在pitch模式中是loader的pitch函数,在noraml模式中是loader函数。 上述还有看到一个关键的函数loadLoader: ./node_modules/loader-runner/lib/loadLoader.js ```javascript module.exports = function loadLoader(loader, callback) { } ``` 它主要是管理loader之间的关系,最终为了执行./node_modules/webpack/lib/NormalModule.js传过来的callback,生成结果。
如果觉得有帮忙,您可以在本页底部留言。
相关推荐:
从youtube观看记录分析时长
Webpack深入浅出plugin
Webpack自动更新php静态资源文件名hash
Webpack构建流程之源码分析
Webpack基于scss生成css独立文件
移动端浏览器真机调试的几种方法
接入台湾超商门店地址选择
ember入门教程
Ember之Handlebars模板引擎
Mac高频快捷键之前端篇
简述浏览器缓存之cookie
浏览器打开页面的过程中发生了什么
Git命令简化笔记
PHP实现微信JS-SDK权限验证
SQL快速运用指南
如何用正确的姿势写HTML
正则表达式实例解析
……
更多
<
Webpack基于scss生成css独立文件
一百年来700部高分电影数据分析报告
>
全部留言
我要留言
内容:
网名:
邮箱:
个人网站:
发表
全部留言