vue 快速入门 系列 —— vue loader 上
其他章节请看:
vue loader 上
通过前面“webpack 系列”的学习,我们知道如何用 webpack 实现一个不成熟的脚手架,比如提供开发环境和生成环境,开发环境提供本地服务器,有热模块替换,能使用 sass、es6等开发项目。
实际工作中我们可能会使用声明式框架 vue 或 react 来开发项目,而它们都提供了相应的脚手架。在学习 vue-cli(vue官方的脚手架)之前,我们先来玩一下 vue loader。
Tip:本篇也可以称之为“vue loader 官网”笔记。通过本篇文章,我们能学会编写一个简单的,用于单文件组件开发的脚手架;以及对单文件组件规范有一个初步的认识和理解。
注:本文很多配置都参考笔者的另一篇文章webpack 快速入门 系列 —— 实战一,比如babel、postcss、图片、css提取等配置。
介绍
Vue Loader 是什么
Vue Loader 是一个 webpack 的 loader,它允许你以一种名为单文件组件 (SFCs)的格式撰写 Vue 组件。
<template>
<div class="example">{{ msg }}</div>
</template>
<script>
export default {
data () {
return {
msg: 'Hello world!'
}
}
}
</script>
<style>
.example {
color: red;
}
</style>
Tip: 更详细的介绍请看下面的“Vue 单文件组件 (SFC) 规范”章节。
Vue Loader 还提供了很多酷炫的特性:
- 允许为 Vue 组件的每个部分使用其它的 webpack loader,例如在
<style>
的部分使用 Sass 和在<template>
的部分使用 Pug; - 允许在一个 .vue 文件中使用自定义块,并对其运用自定义的 loader 链;
- 使用 webpack loader 将
<style>
和<template>
中引用的资源当作模块依赖来处理; - 为每个组件模拟出 scoped CSS;
- 在开发过程中使用热重载来保持状态。
简而言之,webpack 和 Vue Loader 的结合为你提供了一个现代、灵活且极其强大的前端工作流,来帮助撰写 Vue.js 应用。
Tip: 上面这些特性,下文都会详细介绍。
环境准备
直接参考"实战一->准备本篇的环境"一节,搭建好 test-vue-loader 项目。
附上项目:
test-vue-loader
- src // 项目源码
- a.css
- b.js
- c.js
- index.html // 页面模板
- index.js // 入口
- package.json // 存放了项目依赖的包
- webpack.config.js // webpack配置文件
hello-world
现在我们要把 App.vue 这个单文件组件跑起来:
// src/App.vue
<template>
<div class="example">{{ msg }}</div>
</template>
<script>
export default {
data () {
return {
msg: 'Hello world!'
}
}
}
</script>
<style>
.example {
color: red;
}
</style>
请看操作:
首先将 vue-loader 和 vue-template-compiler 一起安装。
// vue 包当然也需要
> npm install -D vue@2 vue-loader@15 vue-template-compiler@2
每个 vue 包的新版本发布时,一个相应版本的 vue-template-compiler 也会随之发布。每次升级项目中的 vue 包时,也应该匹配升级 vue-template-compiler。
接着修改配置文件,核心代码如下:
const { VueLoaderPlugin } = require('vue-loader')
module.exports = {
module: {
rules: [
// ... 其它规则
{
test: /\.vue$/,
loader: 'vue-loader'
}
]
},
plugins: [
// 请确保引入这个插件!
new VueLoaderPlugin()
]
}
在将 src/index.js 改为:
//引入Vue
import Vue from 'vue';
//引入组件
import App from './App.vue';
new Vue({
el: "body",
template: '<App/>',
components: {App}
});
通过 npm run dev
启动,浏览器控制台报错,信息如下:
[Vue warn]: You are using the runtime-only build of Vue where the template compiler is not available. Either pre-compile the templates into render functions, or use the compiler-included build.
(found in <Root>)
// 翻译
您正在使用 Vue 的仅运行时构建,其中模板编译器不可用。 要么将模板预编译为渲染函数,要么使用包含编译器的构建。
告诉我们,正使用“只包含运行时版”,可以将模板编译为渲染函数,或者使用包含编译器的构建。
Tip:vue 有不同的版本,例如:
- 完整版:同时包含编译器和运行时的版本。
- 编译器:用来将模板字符串编译成为 JavaScript 渲染函数的代码。
- 运行时:用来创建 Vue 实例、渲染并处理虚拟 DOM 等的代码。基本上就是除去编译器的其它一切。
在“模块(module)”一文中介绍了 require() 方法加载第三方模块的规则,所以 import Vue from 'vue';
就会去加载 test-vue-loader\node_modules\vue\package.json
中 main 指向的文件("main": "dist/vue.runtime.common.js"
),确实是运行时版。
可以通过以下两种方式修改 index.js 来解决这个问题。
- 使用完整版本
// inidex.js
- import Vue from 'vue';
// 前面会匹配是否核心包、第三方包、路径查找(./ 或 ../ 或 /),最后读取到项目目录下 node_modules 包里的包
// vue.esm.js ES Module (基于构建工具使用),并且是完整版
+ import Vue from 'vue/dist/vue.esm.js'
...
Tip:这种方式在浏览器中会有如下警告:
[Vue warn]: Do not mount Vue to <html> or <body> - mount to normal elements instead.
所以将 el 中的 body 改为 #app 这种形式即可。
- 改为渲染函数
// index.js
import Vue from 'vue';
import App from './App.vue';
new Vue({
el: "body",
render: h => h(App)
});
浏览器页面显示红色文字”Hello world!“,单文件组件解析成功。
处理资源路径
当 Vue Loader 编译单文件组件中的 <template>
块时,它也会将所有遇到的资源 URL 转换为 webpack 模块请求。
让 App.vue 引入一张图片:
<template>
<div class="example">
{{ msg }}
<!-- 增加图片 -->
<img src="./6.68kb.png"></img>
</div>
</template>
终端报错:
...
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file.
// 翻译
您可能需要一个合适的加载器来处理此文件类型,目前没有配置加载器来处理此文件。
下载包,并修改配置:
> npm i -D url-loader@4 file-loader@6
// webpack.config.js -> module.rules
{
test: /\.(png|jpg|gif)$/i,
use: [
{
loader: 'url-loader',
options: {
// 调整的比 6.68 要小,这样图片就不会打包成 base64
limit: 1024*6,
},
},
],
},
再次运行,浏览器还是看不到图片,查看源码:
<div class="example">
Hello world!
<img src="[object Module]">
</div>
图片的 src 有问题,这是因为 url-loader 默认采用 ES 模块语法,而 Vue 生成的是 CommonJS 模块语法,即 require('./6.68kb.png')
,解决方法是让二者采用相同的模板语法,下面将 url-loader 的 es-module 关闭:
// webpack.config.js
module: {
rules: [
...
{
test: /\.(png|jpg|gif)$/i,
use: [
{
loader: 'url-loader',
options: {
limit: 1024*6,
// +
esModule: false,
},
},
],
},
]
},
再次运行,图片正常显示。控制台查看:
<div class="example">
Hello world!
<img src="26bd867dd65e26dbc77d1e151ffd36e0.png">
</div>
转换规则
- 如果路径是绝对路径 (例如 /6.68kb.png),则会原样保留。测试如下:
修改 App.vue,将其改为绝对路径
<img src="/6.68kb.png"></img>
浏览器页面中图片没出来,检查元素:
<div class="example">
Hello world!
<img src="/6.68kb.png">
</div>
通过运行 npm run build
发现 dist 目录中也没有生成 6.68kb.png。于是我们知道使用绝对路径,不仅会原样保留,也不会将该资源打包出去。
- 如果路径以 ~ 开头,其后的部分将会被看作模块依赖。这意味着你可以用该特性来引用一个 Node 依赖中的资源。测试如下:
将图片 6.68kb.png 拷贝到一个模块 node_modules/vue 中
修改图片引用
<img src="~vue/6.68kb.png"></img>
图片正常显示
- 如果路径以 @ 开头,也会被看作模块依赖。如果你的 webpack 配置中给 @ 配置了 alias,这就很有用了。所有 vue-cli 创建的项目都默认配置了将 @ 指向 /src。测试如下:
将路径改为以 @ 开头,终端报错:
// 以 @ 开头
<img src="@vue/6.68kb.png"></img>
// 终端报错
Module not found: Error: Can't resolve '@vue/6.68kb.png' in '....\test-vue-loader\src'
@ 是指向 /src 吗?还是报错:
<img src="@/6.68kb.png"></img>
// 终端报错
Module not found: Error: Can't resolve '@/6.68kb.png' in...
给 @ 配置 alias,图片正常显示。请看代码:
<img src="@/6.68kb.png"></img>
// 给 @ 配置 alias
module.exports = {
...
resolve: {
alias: {
'@': path.resolve(__dirname, 'src/'),
},
},
}
使用预处理器
在 webpack 中,所有的预处理器需要匹配对应的 loader。Vue Loader 允许你使用其它 webpack loader 处理 Vue 组件的某一部分。它会根据 lang 特性以及你 webpack 配置中的规则自动推断出要使用的 loader。
sass
给 App.vue 增加 sass 代码:
// App.vue 尾部增加如下sass代码
<style lang="scss">
$size: 3em;
.example {
font-size: $size;
}
</style>
为了能让 sass/scss 生效,需要安装依赖包:
> npm i -D sass-loader@10 node-sass@6
修改配置文件:
// webpack.config.js -> module.rules
// 普通的 `.scss` 文件和 `*.vue` 文件中的
// `<style lang="scss">` 块都应用它
{
test: /\.scss$/,
use: [
'vue-style-loader',
'css-loader',
'sass-loader'
]
}
重启服务,你会发现页面中的 ”hello world“ 字号变大,sass 编译成功。
Tip: vue-style-loader 是一个基于 style-loader 的 fork。 与 style-loader 类似,您可以将其链接在 css-loader 之后,以将 CSS 作为样式标签动态注入文档。 但是,由于它作为依赖项包含在 vue-loader 中并默认使用,因此在大多数情况下,您不需要自己配置此加载器,即无需下载 vue-style-loader 即可使用。
Sass vs SCSS
sass-loader 默认处理不基于缩进的 scss 语法。
将 sass 改为缩进语法,终端会报错:
// 缩进语法
<style lang="sass">
$size: 3em
.example
font-size: $size;
</style>
// 终端报错
SassError: Invalid CSS after "$size: 3em": expected expression (e.g. 1px, bold), was ".example "...
注:需要将 lang 从 scss 改为 sass,配合下面的 rule 工作。
为了使用基于缩进的 sass 语法,你需要向这个 loader 传递选项:
// webpack.config.js -> module.rules
{
test: /\.sass$/,
use: [
'vue-style-loader',
'css-loader',
{
loader: 'sass-loader',
options: {
// sass-loader version >= 8
sassOptions: {
indentedSyntax: true
}
}
}
]
},
重启服务器,缩进语法生效了。
共享全局变量
sass-loader 也支持一个 prependData 选项,这个选项允许你在所有被处理的文件之间共享常见的变量,而不需要显式地导入它们。请看示例:
// webpack.config.js -> module.rules
{
test: /\.sass$/,
use: [
...
{
loader: 'sass-loader',
options: {
...,
additionalData: `$size: 3em;`,
}
}
]
},
App.vue 中直接使用 $size,而无需定义:
...
<style lang="sass">
.example
font-size: $size;
</style>
less
若直接在 App.vue 中增加如下 less 的样式,会报错:
// 给 App.vue 增加 less 语法
<style lang="less">
@size: 2em;
.example {
font-size: @size
}
</style>
// 终端报错:
ERROR in ./src/App.vue?vue&type=style&index=2&lang=less&..
Module parse failed: Unexpected character '@' (29:0)...
安装依赖,并增加 rule,重启服务即可生效。
> npm i -D less@4 less-loader@7
// webpack.config.js -> module.rules
{
test: /\.less$/,
use: [
'vue-style-loader',
'css-loader',
'less-loader'
]
}
Stylus
若直接在 App.vue 中增加如下 stylus 的样式,会报错:
// 给 App.vue 增加 stylus 语法
<style lang="stylus">
/* stylus 语法 */
$size = 3em
.example
font-size: $size
</style>
// 终端报错:
ERROR in ./src/App.vue?vue&type=style&index=3&lang=stylus&..
安装依赖,并增加 rule,重启服务即可生效。
> npm i -D stylus@0 stylus-loader@4
// webpack.config.js -> module.rules
{
test: /\.styl(us)?$/,
use: [
'vue-style-loader',
'css-loader',
'stylus-loader'
]
}
PostCSS
tip:Vue Loader v15 不再默认应用 PostCSS 变换。你需要通过 postcss-loader 使用 PostCSS。
我们的 vue loader 是 15.9.7,满足该条件。
postcss-loader 可以和上述其它预处理器结合使用。下面我们就给 less 预处理器添加 postcss。
修改 App.vue,给 less 中增加明天的 css 语法:
// lch 是明天的CSS
<style lang="less">
@size: 2em;
.example {
color: lch(100 100 100);
font-size: @size
}
</style>
浏览器查看样式,发现 color: lch(100 100 100)
没生效。
安装依赖包,并修改配置文件:
> npm i -D postcss-loader@4 postcss-preset-env@6
// webpack.config.js
// +
const postcssLoader = {
loader: 'postcss-loader',
options: {
// postcss 只是个平台,具体功能需要使用插件
postcssOptions:{
plugins:[
[
"postcss-preset-env",
{
browsers: 'ie >= 8, chrome > 10',
},
],
]
}
}
}
module: {
rules: [
{
test: /\.less$/,
use: [
'vue-style-loader',
'css-loader',
// +
postcssLoader,
'less-loader'
]
},
重新启动服务器,”Hello World!“ 显示黄色。lch 也编译成了 color: rgb(255, 255, 0)
Babel
首先编写箭头函数,如果打包后能转为普通函数,则说明 babel 配置成功。
给 App.vue 增加箭头函数:
<script>
export default {
...
};
// 箭头函数
const sum = (a, b) => (a + b);
console.log(sum(1, 10));
</script>
浏览器的控制台输出 11,但在浏览器中的源中查看 mian.js,发现箭头函数没有转为普通函数。
const sum = (a, b) => (a + b);\r\nconsole.log(sum(1, 10));\r\n\n\n/
安装依赖,并修改配置:
> npm i -D babel-loader@8 @babel/preset-env@7
// webpack.config.js -> module.rules
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env']
]
}
}
}
重新启动服务器,箭头函数就变成普通函数:
var sum = function sum(a, b) {\n return a + b;\n};\n\nconsole.log(sum(1, 10));
排除 node_modules
exclude: /node_modules/
在应用于 .js 文件的 JS 转译规则 (例如 babel-loader) 中是蛮常见的。鉴于 v15 中的推导变化,如果你导入一个 node_modules 内的 Vue 单文件组件,它的 <script>
部分在转译时将会被排除在外。
我们将 src/App.vue 拷贝一份到 node_modules/vue 目录中,并修改 index.js 中 App.vue 的引入方式:
- import App from './App.vue';
+ import App from 'vue/App.vue';
在浏览器中的源中查看 mian.js,发现箭头函数没有转为普通函数:
const sum = (a, b) => (a + b);\r\nconsole.log(sum(11, 10));
为了确保 js 的转译应用到 node_modules 的 Vue 单文件组件,你需要通过使用一个排除函数将它们加入白名单:
{
test: /\.js$/,
exclude: file => (
/node_modules/.test(file) &&
!/\.vue\.js/.test(file)
),
...
}
重启服务即可生效
注:进行下面测试之前,别忘了还原 App.vue 的引入
import App from './App.vue'
TypeScript
给 App.vue 写入 ts 代码,终端报错:
// 修改 App.vue 的 script
<script lang='ts'>
export default {
...
}
...
/* typescript */
class Greeter<T> {
greeting: T;
constructor(message: T) {
this.greeting = message;
}
greet() {
return this.greeting;
}
}
let greeter = new Greeter<string>("Hello, world");
console.log(greeter.greet())
</script>
// 终端报错
...
You may need an additional loader to handle the result of these loaders.
|
| /* typescript */
> class Greeter<T> {
| greeting: T;
| constructor(message: T) {
安装依赖包,并修改配置:
> npm i -D typescript@4 ts-loader@7
// webpack.config.js
module.exports = {
resolve: {
// 将 `.ts` 添加为一个可解析的扩展名。
extensions: ['.ts', '.js']
},
module: {
rules: [
// ... 忽略其它规则
{
test: /\.ts$/,
loader: 'ts-loader',
options: { appendTsSuffixTo: [/\.vue$/] }
}
]
},
...
}
重启服务,终端报错:
[tsl] ERROR
TS18002: The 'files' list in config file 'tsconfig.json' is empty.
新建 test-vue-loader/tsconfig.json,内容如下:
{
"compilerOptions": {
"sourceMap": true
}
}
重启服务,终端报错信息变为:
TS18003: No inputs were found in config file 'tsconfig.json'. Specified 'include' paths were '["**/*"]' and 'exclude' paths were '[]'.
可以给 tsconfig.json 增加 allowJs:
{
"compilerOptions": {
"allowJs": true,
"sourceMap": true
}
}
重启服务器,终端没有抛出错误,浏览器控制台成功输出 ”hello, TypeScript 解析成功。
Pug
Pug 是一个高性能模板引擎,深受 Haml 影响,使用 JavaScript 实现,适用于 Node.js 和浏览器;
Pug 是一种用于编写 html 的干净、对空格敏感的语法
在 App.vue 中使用 pug,重新打包,停住了:
<template>
<div class="example">
...
</div>
</template>
<!-- 多个 template,会以最后一个 template 为准-->
<template lang="pug">
div
h1 I am pug!
</template>
// 打包
test-vue-loader> npm run build
> test-vue-loader@1.0.0 build
> webpack
不动了...
猜测可能是没有配置 pug 导致的。于是安装依赖,并修改配置:
> npm i -D pug@3 pug-plain-loader@1
// webpack.config.js -> module.rules
{
test: /\.pug$/,
loader: 'pug-plain-loader'
}
重新启动服务器,浏览器页面显示 I am pug!
,pug 解析成功。
// 浏览器查看源码
<div>
<h1>I am pug!</h1>
</div>
Scoped Css
Tip: 为了减少影响,方便学习和测试,可以将 App.vue 的代码全部注释,就像这样<!-- App.vue 的所有代码 -->
当 <style>
标签有 scoped 属性时,它的 CSS 只作用于当前组件中的元素,它有一些注意事项,但不需要任何 polyfill。
修改 App.vue 内容,给 style 增加 scoped:
<template>
<div class="example">hi</div>
</template>
<style scoped>
.example {
color: red;
}
</style>
通过浏览器检查:
.example[data-v-7ba5bd90] {
color: red;
}
<div data-v-7ba5bd90="" class="example">hi</div>
Tip:文档说它通过使用 PostCSS 来实现转换,但目前我的 postcss 只结合 less 使用,这里使用的明显是 css,所以猜想 postCss 难道内置了!
混用本地和全局样式
<style scoped>
.example {
color: red;
}
</style>
<style>
.example {
font-size: 2em;
}
</style>
转换结果:
<style>
.example[data-v-7ba5bd90] {
color: red;
}
</style>
<style>
.example {
font-size: 2em;
}
子组件的根元素
使用 scoped 后,父组件的样式将不会渗透到子组件中。不过一个子组件的根节点会同时受其父组件的 scoped CSS 和子组件的 scoped CSS 的影响。这样设计是为了让父组件可以从布局的角度出发,调整其子组件根元素的样式。—— 官网
什么意思?我们做个测试就明白了
新建一个子组件:
// Box.vue
<template>
<div class='m-box'>
children
<p>m-box p1</p>
</div>
</template>
在 App.vue 中引用子组件:
<template>
<div class="s-c1">
<p>hi</p>
<Box/>
</div>
</template>
<script>
import Box from './Box.vue'
export default {
data () {
return {
msg: 'Hello world!'
}
},
components:{
Box
}
}
</script>
<style scoped>
.s-c1 {
color: red;
}
</style>
页面中三行文字全是红色。
hi
children
m-box p1
子组件明明没有写样式,而且父组件的样式也写在 scope 中,为什么子组件的文字也变成红色?
浏览器查看代码:
<style>
.s-c1[data-v-7ba5bd90] {
color: red;
}
</style>
<div data-v-7ba5bd90="" class="s-c1">
<p data-v-7ba5bd90="">hi</p>
<div data-v-1461803c="" data-v-7ba5bd90="" class="m-box">
children
<p data-v-1461803c="">m-box p1</p>
</div>
</div>
原来我们写的代码转成这种形式,子组件的文字确实应该是红色。
而上面提到:”不过一个子组件的根节点会同时受其父组件的 scoped CSS 和子组件的 scoped CSS 的影响“
指的应该是子组件的根元素上既有子组件的标记,也有父组件的标记,即同时有 data-v-1461803c=""
和 data-v-7ba5bd90=""
将 App.vue 的 style 改成下面代码,在页面中会看得更清晰:
<style scoped>
.s-c1 {
color: red;
margin:10px;padding:10px;
}
div{
border: 1px solid blue;
}
</style>
不仅父组件有边框,子组件的的根(div)也会有蓝色边框。
深度作用选择器
如果你希望 scoped 样式中的一个选择器能够作用得“更深”,例如影响子组件,你可以使用 >>>
操作符:
只调整父组件中 p 元素的字号,可以这么写:
<style scoped>
...
div p{font-size:2em;}
</style>
转换成:
div p[data-v-7ba5bd90]{font-size:2em;}
如果也希望调整子组件中 p 元素的字号:
div >>> p{font-size:2em;}
转换成:
div[data-v-7ba5bd90] p{font-size:2em;}
如果希望只作用于 div 的孩子节点:
div >>> > p{font-size:2em;}
转换成:
div[data-v-7ba5bd90] > p{font-size:2em;}
注:>>>>
不会生效
有些像 Sass 之类的预处理器无法正确解析 >>>。这种情况下你可以使用 /deep/ 或 ::v-deep 操作符取而代之——两者都是 >>> 的别名,同样可以正常工作。
动态生成的内容
通过 v-html 创建的 DOM 内容不受 scoped 样式影响,但是你仍然可以通过深度作用选择器来为他们设置样式。
我们做个测试
我们给 App.vue 和 Box.vue 都增加 v-html,代码如下:
// App.vue
<template>
<div class="s-c1">
<p>hi</p>
<!-- + -->
<span v-html='aHtml'></span>
<Box/>
</div>
</template>
<script>
...
export default {
data () {
return {
// +
aHtml: '<p>i am aHtml</p>'
}
},
}
</script>
<style scoped>
...
div >>> p{font-size:2em;}
</style>
// Box.vue
<template>
<div class='m-box'>
children
<span v-html='bHtml'></span>
<p>m-box p1</p>
</div>
</template>
<script>
export default {
data () {
return {
bHtml: '<p>i am bHtml</p>'
}
}
}
</script>
i am aHtml
和 i am bHtml
字号都是 2em
浏览器查看转换后的代码:
div[data-v-7ba5bd90] p{font-size:2em;}
<div data-v-7ba5bd90="" class="s-c1" style="margin: 10px; padding: 10px;">
<p data-v-7ba5bd90="">hi</p>
<span data-v-7ba5bd90="">
<p>i am aHtml</p>
</span>
<div data-v-1461803c="" data-v-7ba5bd90="" class="m-box">children2
<span data-v-1461803c="">
<p>i am bHtml</p>
</span>
<p data-v-1461803c="">m-box p1</p></div>
</div>
将深度作用选择器删除后测试:
div p{font-size:2em;}
i am aHtml
和 i am bHtml
字号都不在是 2em。
转换后的代码是:
div p[data-v-7ba5bd90]{font-size:2em;}
<div data-v-7ba5bd90="" class="s-c1">
<p data-v-7ba5bd90="">hi</p>
<span data-v-7ba5bd90="">
<p>i am aHtml</p>
</span>
<div data-v-7ba5bd90="" class="m-box">
children
<span>
<p>i am bHtml</p>
</span>
<p>m-box p1</p>
</div>
</div>
v-html生成的元素都不会有特殊的标记,比如这里的 data-v-7ba5bd90
。
至此,我们就理解了开头的话。
还有一些要留意
Scoped 样式不能代替 class。考虑到浏览器渲染各种 CSS 选择器的方式,当 p { color: red } 是 scoped 时 (即与特性选择器组合使用时) 会慢很多倍。如果你使用 class 或者 id 取而代之,比如 .example { color: red },性能影响就会消除。
div{}
.div{}
转换成:
div[data-v-7ba5bd90]{}
.div[data-v-7ba5bd90]{}
在递归组件中小心使用后代选择器!
- 对选择器 .a .b 中的 CSS 规则来说,如果匹配 .a 的元素包含一个递归子组件,则所有的子组件中的 .b 都将被这个规则匹配。
其他章节请看:
出处:https://www.cnblogs.com/pengjiali/p/14958905.html
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。