本文会带你简单的认识一下webpack的loader,动手实现一个利用md转成抽象语法树,再转成html字符串的loader。顺便简单的了解一下几个style-loader,vue-loader,babel-loader的源码以及工作流程。
webpack允许我们使用loader来处理文件,loader是一个导出为function的node模块。可以将匹配到的文件进行一次转换,同时loader可以链式传递。loader文件处理器是一个CommonJs风格的函数,该函数接收一个 String/Buffer 类型的入参,并返回一个 String/Buffer 类型的返回值。
方案1:
// webpack.config.js
module.exports = {
...
module: {
rules: [{
test: /.vue$/,
loader: 'vue-loader'
}, {
test: /.scss$/,
// 先经过 sass-loader,然后将结果传入 css-loader,最后再进入 style-loader。
use: [
'style-loader',//从JS字符串创建样式节点
'css-loader',// 把 CSS 翻译成 CommonJS
{
loader: 'sass-loader',
options: {
data: '$color: red;'// 把 Sass 编译成 CSS
}
}
]
}]
}
...
}
方法2(右到左地被调用)
// module
import Styles from 'style-loader!css-loader?modules!./styles.css';
当链式调用多个 loader 的时候,请记住它们会以相反的顺序执行。取决于数组写法格式,从右向左或者从下向上执行。像流水线一样,挨个处理每个loader,前一个loader的结果会传递给下一个loader,最后的 Loader 将处理后的结果以 String 或 Buffer 的形式返回给 compiler。
import { getOptions } from 'loader-utils';
import { validateOptions } from 'schema-utils';
const schema = {
// ...
}
export default function(content) {
// 获取 options
const options = getOptions(this);
// 检验loader的options是否合法
validateOptions(schema, options, 'Demo Loader');
// 在这里写转换 loader 的逻辑
// ...
return content;
};
同步 loader,我们可以通过return和this.callback返回输出的内容
module.exports = function(content, map, meta) {
//一些同步操作
outputContent=someSyncOperation(content)
return outputContent;
}
如果返回结果只有一个,也可以直接使用 return 返回结果。但是,如果有些情况下还需要返回其他内容,如sourceMap或是AST语法树,这个时候可以借助webpack提供的api this.callback
module.exports = function(content, map, meta) {
this.callback(
err: Error | null,
content: string | Buffer,
sourceMap?: SourceMap,
meta?: any
);
return;
}
第一个参数必须是 Error 或者 null 第二个参数是一个 string 或者 Buffer。可选的:第三个参数必须是一个可以被这个模块解析的 source map。可选的:第四个选项,会被 webpack 忽略,可以是任何东西【可以将抽象语法树(abstract syntax tree - AST)(例如 ESTree)作为第四个参数(meta),如果你想在多个 loader 之间共享通用的 AST,这样做有助于加速编译时间。】。
异步loader,使用 this.async 来获取 callback 函数。
// 让 Loader 缓存
module.exports = function(source) {
var callback = this.async();
// 做异步的事
doSomeAsyncOperation(content, function(err, result) {
if(err) return callback(err);
callback(null, result);
});
};
详情请参考官网API
const marked = require("marked");
const loaderUtils = require("loader-utils");
module.exports = function (content) {
this.cacheable && this.cacheable();
const options = loaderUtils.getOptions(this);
try {
marked.setOptions(options);
return marked(content)
} catch (err) {
this.emitError(err);
return null
}
};
上述的例子是通过现成的插件把markdown文件里的content转成html字符串,但是如果没有这个插件,改怎么做呢?这个情况下,我们可以考虑另外一种解法,借助 AST 语法树,来协助我们更加便捷地操作转换。
markdown-ast是将markdown文件里的content转成数组形式的抽象语法树节点,操作 AST 语法树远比操作字符串要简单、方便得多:
//通过正则的方法把字符串处理成直观的AST语法树
const md = require('markdown-ast');
module.exports = function(content) {
this.cacheable && this.cacheable();
const options = loaderUtils.getOptions(this);
try {
console.log(md(content))
const parser = new MdParser(content);
return parser.data
} catch (err) {
console.log(err)
return null
}
};
const md = require('markdown-ast');
const hljs = require('highlight.js');//代码高亮插件
// 利用 AST 作源码转换
class MdParser {
constructor(content) {
this.data = md(content);
console.log(this.data)
this.parse()
}
parse() {
this.data = this.traverse(this.data);
}
traverse(ast) {
console.log("md转抽象语法树操作",ast)
let body = '';
ast.map(item => {
switch (item.type) {
case "bold":
case "break":
case "codeBlock":
const highlightedCode = hljs.highlight(item.syntax, item.code).value
body += highlightedCode
break;
case "codeSpan":
case "image":
case "italic":
case "link":
case "list":
item.type = (item.bullet === '-') ? 'ul' : 'ol'
if (item.type !== '-') {
item.startatt = (` start=${item.indent.length}`)
} else {
item.startatt = ''
}
body += '<' + item.type + item.startatt + '>\n' + this.traverse(item.block) + '</' + item.type + '>\n'
break;
case "quote":
let quoteString = this.traverse(item.block)
body += '<blockquote>\n' + quoteString + '</blockquote>\n';
break;
case "strike":
case "text":
case "title":
body += `<h${item.rank}>${item.text}</h${item.rank}>`
break;
default:
throw Error("error", `No corresponding treatment when item.type equal${item.type}`);
}
})
return body
}
}
md 转成抽象语树
ast抽象语法数转成html字符串
md2html-loader源码地址(https://github.com/6fedcom/fe-blog/blob/master/webpack-loader/loaders/md-loader.js)
this.cacheable&&this.cacheable(false);
// 创建 run-loader.js
const fs = require("fs");
const path = require("path");
const { runLoaders } = require("loader-runner");
runLoaders(
{
resource: "./readme.md",
loaders: [path.resolve(__dirname, "./loaders/md-loader")],
readResource: fs.readFile.bind(fs),
},
(err, result) =>
(err ? console.error(err) : console.log(result))
);
执行 node run-loader
style-loader源码简析
作用:把样式插入到DOM中,方法是在head中插入一个style标签,并把样式写入到这个标签的 innerHTML 里 看下源码。
先去掉option处理代码,这样就比较清晰明了了
返回一段js代码,通过require来获取css内容,再通过addStyle的方法把css插入到dom里 自己实现一个简陋的style-loader.js
module.exports.pitch = function (request) {
const {stringifyRequest}=loaderUtils
var result = [
//1. 获取css内容。2.// 调用addStyle把CSS内容插入到DOM中(locals为true,默认导出css)
'var content=require(' + stringifyRequest(this, '!!' + request) + ')’,
'require(' + stringifyRequest(this, '!' + path.join(__dirname, "addstyle.js")) + ')(content)’,
'if(content.locals) module.exports = content.locals’
]
return result.join(';')
}
需要说明的是,正常我们都会用default的方法,这里用到pitch方法。pitch 方法有一个官方的解释在这里 pitching loader。简单的解释一下就是,默认的loader都是从右向左执行,用 pitching loader 是从左到右执行的。
{
test: /\.css$/,
use: [
{ loader: "style-loader" },
{ loader: "css-loader" }
]
}
为什么要先执行style-loader呢,因为我们要把css-loader拿到的内容最终输出成CSS样式中可以用的代码而不是字符串。
addstyle.js
module.exports = function (content) {
let style = document.createElement("style")
style.innerHTML = content
document.head.appendChild(style)
}
首先看下跳过loader的配置处理,看下babel-loader输出
上图我们可以看到是输出transpile(source, options)的code和map 再来看下transpile方法做了啥
babel-loader是通过babel.transform来实现对代码的编译的, 这么看来,所以我们只需要几行代码就可以实现一个简单的babel-loader
const babel = require("babel-core")
module.exports = function (source) {
const babelOptions = {
presets: ['env']
}
return babel.transform(source, babelOptions).code
}
vue-loader源码简析
vue单文件组件(简称sfc)
<template>
<div class="text">
{{a}}
</div>
</template>
<script>
export default {
data () {
return {
a: "vue demo"
};
}
};
</script>
<style lang="scss" scope>
.text {
color: red;
}
</style>
webpack配置
const VueloaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
...
module: {
rules: [
...
{
test: /\.vue$/,
loader: 'vue-loader'
}
]
}
plugins: [
new VueloaderPlugin()
]
...
}
VueLoaderPlugin作用:将在webpack.config定义过的其它规则复制并应用到 .vue 文件里相应语言的块中。plugin-webpack4.js
const vueLoaderUse = vueUse[vueLoaderUseIndex]
vueLoaderUse.ident = 'vue-loader-options'
vueLoaderUse.options = vueLoaderUse.options || {}
// cloneRule会修改原始rule的resource和resourceQuery配置,
// 携带特殊query的文件路径将被应用对应rule
const clonedRules = rules
.filter(r => r !== vueRule)
.map(cloneRule)
// global pitcher (responsible for injecting template compiler loader & CSS
// post loader)
const pitcher = {
loader: require.resolve('./loaders/pitcher'),
resourceQuery: query => {
const parsed = qs.parse(query.slice(1))
return parsed.vue != null
},
options: {
cacheDirectory: vueLoaderUse.options.cacheDirectory,
cacheIdentifier: vueLoaderUse.options.cacheIdentifier
}
}
// 更新webpack的rules配置,这样vue单文件中的各个标签可以应用clonedRules相关的配置
compiler.options.module.rules = [
pitcher,
...clonedRules,
...rules
]
获取webpack.config.js的rules项,然后复制rules,为携带了?vue&lang=xx...query参数的文件依赖配置xx后缀文件同样的loader 为Vue文件配置一个公共的loader:pitcher 将[pitchLoder, ...clonedRules, ...rules]作为webapck新的rules。
再看一下vue-loader结果的输出
当引入一个vue文件后,vue-loader是将vue单文件组件进行parse,获取每个 block 的相关内容,将不同类型的 block 组件的 Vue SFC 转化成 js module 字符串。
// vue-loader使用`@vue/component-compiler-utils`将SFC源码解析成SFC描述符,,根据不同 module path 的类型(query 参数上的 type 字段)来抽离 SFC 当中不同类型的 block。
const { parse } = require('@vue/component-compiler-utils')
// 将单个*.vue文件内容解析成一个descriptor对象,也称为SFC(Single-File Components)对象
// descriptor包含template、script、style等标签的属性和内容,方便为每种标签做对应处理
const descriptor = parse({
source,
compiler: options.compiler || loadTemplateCompiler(loaderContext),
filename,
sourceRoot,
needMap: sourceMap
})
// 为单文件组件生成唯一哈希id
const id = hash(
isProduction
? (shortFilePath + '\n' + source)
: shortFilePath
)
// 如果某个style标签包含scoped属性,则需要进行CSS Scoped处理
const hasScoped = descriptor.styles.some(s => s.scoped)
然后下一步将新生成的 js module 加入到 webpack 的编译环节,即对这个 js module 进行 AST 的解析以及相关依赖的收集过程。
来看下源码是怎么操作不同type类型(template/script/style)的,selectBlock 方法内部主要就是根据不同的 type 类型,来获取 descriptor 上对应类型的 content 内容并传入到下一个 loader 处理
这三段代码可以把不同type解析成一个import的字符串
import { render, staticRenderFns } from "./App.vue?vue&type=template&id=7ba5bd90&"
import script from "./App.vue?vue&type=script&lang=js&"
export * from "./App.vue?vue&type=script&lang=js&"
import style0 from "./App.vue?vue&type=style&index=0&lang=scss&scope=true&"
总结一下vue-loader的工作流程
文会带你简单的认识一下webpack的loader,动手实现一个利用md转成抽象语法树,再转成html字符串的loader。顺便简单的了解一下几个style-loader,vue-loader,babel-loader的源码以及工作流程。
webpack允许我们使用loader来处理文件,loader是一个导出为function的node模块。可以将匹配到的文件进行一次转换,同时loader可以链式传递。 loader文件处理器是一个CommonJs风格的函数,该函数接收一个 String/Buffer 类型的入参,并返回一个 String/Buffer 类型的返回值。
方案1:
// webpack.config.js
module.exports = {
...
module: {
rules: [{
test: /.vue$/,
loader: 'vue-loader'
}, {
test: /.scss$/,
// 先经过 sass-loader,然后将结果传入 css-loader,最后再进入 style-loader。
use: [
'style-loader',//从JS字符串创建样式节点
'css-loader',// 把 CSS 翻译成 CommonJS
{
loader: 'sass-loader',
options: {
data: '$color: red;'// 把 Sass 编译成 CSS
}
}
]
}]
}
...
}
方法2(右到左地被调用)
// module
import Styles from 'style-loader!css-loader?modules!./styles.css';
当链式调用多个 loader 的时候,请记住它们会以相反的顺序执行。取决于数组写法格式,从右向左或者从下向上执行。像流水线一样,挨个处理每个loader,前一个loader的结果会传递给下一个loader,最后的 Loader 将处理后的结果以 String 或 Buffer 的形式返回给 compiler。
import { getOptions } from 'loader-utils';
import { validateOptions } from 'schema-utils';
const schema = {
// ...
}
export default function(content) {
// 获取 options
const options = getOptions(this);
// 检验loader的options是否合法
validateOptions(schema, options, 'Demo Loader');
// 在这里写转换 loader 的逻辑
// ...
return content;
};
同步 loader,我们可以通过return和this.callback返回输出的内容
module.exports = function(content, map, meta) {
//一些同步操作
outputContent=someSyncOperation(content)
return outputContent;
}
如果返回结果只有一个,也可以直接使用 return 返回结果。但是,如果有些情况下还需要返回其他内容,如sourceMap或是AST语法树,这个时候可以借助webpack提供的api this.callback
module.exports = function(content, map, meta) {
this.callback(
err: Error | null,
content: string | Buffer,
sourceMap?: SourceMap,
meta?: any
);
return;
}
第一个参数必须是 Error 或者 null 第二个参数是一个 string 或者 Buffer。 可选的:第三个参数必须是一个可以被这个模块解析的 source map。 可选的:第四个选项,会被 webpack 忽略,可以是任何东西【可以将抽象语法树(abstract syntax tree - AST)(例如 ESTree)作为第四个参数(meta),如果你想在多个 loader 之间共享通用的 AST,这样做有助于加速编译时间。】。
异步loader,使用 this.async 来获取 callback 函数。
// 让 Loader 缓存
module.exports = function(source) {
var callback = this.async();
// 做异步的事
doSomeAsyncOperation(content, function(err, result) {
if(err) return callback(err);
callback(null, result);
});
};
详情请参考官网API
const marked = require("marked");
const loaderUtils = require("loader-utils");
module.exports = function (content) {
this.cacheable && this.cacheable();
const options = loaderUtils.getOptions(this);
try {
marked.setOptions(options);
return marked(content)
} catch (err) {
this.emitError(err);
return null
}
};
上述的例子是通过现成的插件把markdown文件里的content转成html字符串,但是如果没有这个插件,该怎么做呢?这个情况下,我们可以考虑另外一种解法,借助 AST 语法树,来协助我们更加便捷地操作转换。
markdown-ast是将markdown文件里的content转成数组形式的抽象语法树节点,操作 AST 语法树远比操作字符串要简单、方便得多:
const md = require('markdown-ast');//通过正则的方法把字符串处理成直观的AST语法树
module.exports = function(content) {
this.cacheable && this.cacheable();
const options = loaderUtils.getOptions(this);
try {
console.log(md(content))
const parser = new MdParser(content);
return parser.data
} catch (err) {
console.log(err)
return null
}
};
md通过正则切割的方法转成抽象语树
const md = require('markdown-ast');//md通过正则匹配的方法把buffer转抽象语法树
const hljs = require('highlight.js');//代码高亮插件
// 利用 AST 作源码转换
class MdParser {
constructor(content) {
this.data = md(content);
console.log(this.data)
this.parse()
}
parse() {
this.data = this.traverse(this.data);
}
traverse(ast) {
console.log("md转抽象语法树操作",ast)
let body = '';
ast.map(item => {
switch (item.type) {
case "bold":
case "break":
case "codeBlock":
const highlightedCode = hljs.highlight(item.syntax, item.code).value
body += highlightedCode
break;
case "codeSpan":
case "image":
case "italic":
case "link":
case "list":
item.type = (item.bullet === '-') ? 'ul' : 'ol'
if (item.type !== '-') {
item.startatt = (` start=${item.indent.length}`)
} else {
item.startatt = ''
}
body += '<' + item.type + item.startatt + '>\n' + this.traverse(item.block) + '</' + item.type + '>\n'
break;
case "quote":
let quoteString = this.traverse(item.block)
body += '<blockquote>\n' + quoteString + '</blockquote>\n';
break;
case "strike":
case "text":
case "title":
body += `<h${item.rank}>${item.text}</h${item.rank}>`
break;
default:
throw Error("error", `No corresponding treatment when item.type equal${item.type}`);
}
})
return body
}
}
完整的代码参考这里
ast抽象语法数转成html字符串
this.cacheable&&this.cacheable(false);
// 创建 run-loader.js
const fs = require("fs");
const path = require("path");
const { runLoaders } = require("loader-runner");
runLoaders(
{
resource: "./readme.md",
loaders: [path.resolve(__dirname, "./loaders/md-loader")],
readResource: fs.readFile.bind(fs),
},
(err, result) =>
(err ? console.error(err) : console.log(result))
);
执行 node run-loader
作用:把样式插入到DOM中,方法是在head中插入一个style标签,并把样式写入到这个标签的 innerHTML 里 看下源码。
先去掉option处理代码,这样就比较清晰明了了
返回一段js代码,通过require来获取css内容,再通过addStyle的方法把css插入到dom里 自己实现一个简陋的style-loader.js
module.exports.pitch = function (request) {
const {stringifyRequest}=loaderUtils
var result = [
//1. 获取css内容。2.// 调用addStyle把CSS内容插入到DOM中(locals为true,默认导出css)
'var content=require(' + stringifyRequest(this, '!!' + request) + ')’,
'require(' + stringifyRequest(this, '!' + path.join(__dirname, "addstyle.js")) + ')(content)’,
'if(content.locals) module.exports = content.locals’
]
return result.join(';')
}
需要说明的是,正常我们都会用default的方法,这里用到pitch方法。pitch 方法有一个官方的解释在这里 pitching loader。简单的解释一下就是,默认的loader都是从右向左执行,用 pitching loader 是从左到右执行的。
{
test: /\.css$/,
use: [
{ loader: "style-loader" },
{ loader: "css-loader" }
]
}
为什么要先执行style-loader呢,因为我们要把css-loader拿到的内容最终输出成CSS样式中可以用的代码而不是字符串。
addstyle.js
module.exports = function (content) {
let style = document.createElement("style")
style.innerHTML = content
document.head.appendChild(style)
}
首先看下跳过loader的配置处理,看下babel-loader输出
上图我们可以看到是输出transpile(source, options)的code和map 再来看下transpile方法做了啥
babel-loader是通过babel.transform来实现对代码的编译的, 这么看来,所以我们只需要几行代码就可以实现一个简单的babel-loader
const babel = require("babel-core")
module.exports = function (source) {
const babelOptions = {
presets: ['env']
}
return babel.transform(source, babelOptions).code
}
vue单文件组件(简称sfc)
<template>
<div class="text">
{{a}}
</div>
</template>
<script>
export default {
data () {
return {
a: "vue demo"
};
}
};
</script>
<style lang="scss" scope>
.text {
color: red;
}
</style>
webpack配置
const VueloaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
...
module: {
rules: [
...
{
test: /\.vue$/,
loader: 'vue-loader'
}
]
}
plugins: [
new VueloaderPlugin()
]
...
}
VueLoaderPlugin 作用:将在webpack.config定义过的其它规则复制并应用到 .vue 文件里相应语言的块中。
plugin-webpack4.js
const vueLoaderUse = vueUse[vueLoaderUseIndex]
vueLoaderUse.ident = 'vue-loader-options'
vueLoaderUse.options = vueLoaderUse.options || {}
// cloneRule会修改原始rule的resource和resourceQuery配置,
// 携带特殊query的文件路径将被应用对应rule
const clonedRules = rules
.filter(r => r !== vueRule)
.map(cloneRule)
// global pitcher (responsible for injecting template compiler loader & CSS
// post loader)
const pitcher = {
loader: require.resolve('./loaders/pitcher'),
resourceQuery: query => {
const parsed = qs.parse(query.slice(1))
return parsed.vue != null
},
options: {
cacheDirectory: vueLoaderUse.options.cacheDirectory,
cacheIdentifier: vueLoaderUse.options.cacheIdentifier
}
}
// 更新webpack的rules配置,这样vue单文件中的各个标签可以应用clonedRules相关的配置
compiler.options.module.rules = [
pitcher,
...clonedRules,
...rules
]
获取webpack.config.js的rules项,然后复制rules,为携带了?vue&lang=xx...query参数的文件依赖配置xx后缀文件同样的loader 为Vue文件配置一个公共的loader:pitcher 将[pitchLoder, ...clonedRules, ...rules]作为webapck新的rules。
再看一下vue-loader结果的输出
当引入一个vue文件后,vue-loader是将vue单文件组件进行parse,获取每个 block 的相关内容,将不同类型的 block 组件的 Vue SFC 转化成 js module 字符串。
// vue-loader使用`@vue/component-compiler-utils`将SFC源码解析成SFC描述符,,根据不同 module path 的类型(query 参数上的 type 字段)来抽离 SFC 当中不同类型的 block。
const { parse } = require('@vue/component-compiler-utils')
// 将单个*.vue文件内容解析成一个descriptor对象,也称为SFC(Single-File Components)对象
// descriptor包含template、script、style等标签的属性和内容,方便为每种标签做对应处理
const descriptor = parse({
source,
compiler: options.compiler || loadTemplateCompiler(loaderContext),
filename,
sourceRoot,
needMap: sourceMap
})
// 为单文件组件生成唯一哈希id
const id = hash(
isProduction
? (shortFilePath + '\n' + source)
: shortFilePath
)
// 如果某个style标签包含scoped属性,则需要进行CSS Scoped处理
const hasScoped = descriptor.styles.some(s => s.scoped)
然后下一步将新生成的 js module 加入到 webpack 的编译环节,即对这个 js module 进行 AST 的解析以及相关依赖的收集过程。
来看下源码是怎么操作不同type类型(template/script/style)的,selectBlock 方法内部主要就是根据不同的 type 类型,来获取 descriptor 上对应类型的 content 内容并传入到下一个 loader 处理
这三段代码可以把不同type解析成一个import的字符串
import { render, staticRenderFns } from "./App.vue?vue&type=template&id=7ba5bd90&"
import script from "./App.vue?vue&type=script&lang=js&"
export * from "./App.vue?vue&type=script&lang=js&"
import style0 from "./App.vue?vue&type=style&index=0&lang=scss&scope=true&"
总结一下vue-loader的工作流程
点击github仓库
面简单了解了webpack4的安装、入口、输出、以及插件,似乎webpack是通过这个入口文件以及各种插件的配置将各种依赖关系(例如前面的html和js之间的关系)整理后输出,成为一个经过优化了的项目,其实这就是webpack的本质,只不过我们的例子非常简单没有体现出webpack的强大而已。
一、整理项目
前面我们的项目建立的相对随意,现在我们把项目的目录规整以下,让他尽量的符合规范一些。项目结构如下:
从图中我们看出src目录我们将资源分了类,有js文件夹、less文件夹、imgs文件夹,分别对应各自类型的文件。这时输出也应该是这样的结构,所以webpack.config.js也需要做相应的调整,主要是路径的调整,代码如下:
const { CleanWebpackPlugin }=require('clean-webpack-plugin'); const HtmlWebpackPlugin=require('html-webpack-plugin'); module.exports={ mode:"development", entry:{ "common":"./src/js/common.js", "index":"./src/js/index.js", "login":"./src/js/login.js" }, output:{ filename:"js/[name].js", }, plugins:[ new CleanWebpackPlugin(), new HtmlWebpackPlugin({ template:'./src/index.html', filename:'index.html', chunks:["common","index"], hash:true }), new HtmlWebpackPlugin({ template:'./src/login.html', filename:'login.html', chunks:["common","login"], hash:true }) ] }
入口对应的是各自目录下的js文件,输出的话filename看起来像路径,其实这种写法也是可以的,webpack会自动在默认的dist目录下创建js文件夹以及js文件。
其实output项中有一个path配置项,这个配置项是定义所有输出文件的根目录,不能定义子目录。
再来看mode:"development"这项设置,这个是告诉webpack现在的开发模式是什么,他的值有两个一个是development,一个是production,如果使用production那么webpack在处理时会启用自己的一些插件对项目进行优化,例如压缩js文件,意思就是如果设置成production的话打包后的js就已经时经过压缩的文件了,不用在特意安装针对js的压缩插件了。
我们前面一直在运行ngx webpack命令时,命令行工具中老是有一段黄色的警告其实就是因为没有设置它而产生的。
二、添加样式使用loader
loader 用于对模块的源代码进行转换。loader 可以使你在 import 或"加载"模块时预处理文件。loader 可以将文件从不同的语言(如 typescript)转换为 javascript,或将内联图像转换为 data URL。loader 甚至允许你直接在 JavaScript 模块中 import css文件!
下面我们要给项目中的页面添加样式,这个样式是使用less写的,我们不光要使less文件打包成css文件,还要在js模块中引入css,不在像以前一样直接在html页面中link样式了,因为webpack就是处理这些资源依赖关系的。less文件内容如下:
@base-color:red; body{ color:@base-color; }
处理上面的这些问题,我们需要一系列的loader,首先需要将less转换成css 需要less,加载less文件又需要less-loader。
我们在index.js中使用的是 import "../less/index.less"; 来引入的css,所以需要css-loader。index.js内容如下:
import "../less/index.less"; console.log('index');
将样式写入页面又需要style-loader。
所以我们需要安装less、less-loader、css-loader、style-loader。安装命令如下:
cnpm install less less-loader css-loader style-loader --save-dev
然后修改webpack.config.js,内容如下:
const { CleanWebpackPlugin }=require('clean-webpack-plugin'); const HtmlWebpackPlugin=require('html-webpack-plugin'); module.exports={ mode:"development", entry:{ "common":"./src/js/common.js", "index":"./src/js/index.js", "login":"./src/js/login.js" }, output:{ filename:"js/[name].js", }, plugins:[ new CleanWebpackPlugin(), new HtmlWebpackPlugin({ template:'./src/index.html', filename:'index.html', chunks:["common","index"], hash:true }), new HtmlWebpackPlugin({ template:'./src/login.html', filename:'login.html', chunks:["common","login"], hash:true }) ], module:{ rules:[ { test:/\.less$/, use:[ {loader:"style-loader"}, {loader:"css-loader"}, {loader:"less-loader"} ] } ] } }
我们将这些处理的内容写到module的配置中,下面定义了一个规则,意思就是遇到以.less为结尾的文件名时使用下面的loader来处理。
然后命令行中运行如下命令:
npx webpack
我们会发现样式确实添加到了页面中,但是这个样式是以style的方式添加到index.html文件中的,如下图:
从上面的过程中我们可以理解,各种loader 就是让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript),loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后在利用 webpack 的打包能力,对它们进行处理。
下一节中我们将了解怎么将怎么将样式提取出来成为一个样式文件,并且以link的方式添加到index.html文件中,以及如何处理页面中的图片以及样式中的图片地址等图片资源。
*请认真填写需求信息,我们会在24小时内与您取得联系。