使用VueJS进行应用开发, 脱离不了对应用间的模块进行拆分, 将大块界面拆解为组件的过程. 我们可以很方便的在单文件中使用<template>块维护组件的视图, 使用<script>维护组件的逻辑部分, 使用<style>维护组件的样式. 在我们编写 VueJS 组件样式时, 不得忽略的一点就是样式污染.
样式污染产生原因
提及样式污染, 主要要追溯到Webpack对CSS文件的打包过程, 这里我们以Vue-Element-Admin中的Webpack配置项举例:
const webpackConfig = merge(baseWebpackConfig, { plugins: [ new MiniCssExtractPlugin({ filename: utils.assetsPath('css/[name].[contenthash:8].css'), chunkFilename: utils.assetsPath('css/[name].[contenthash:8].css') }), ] })
Webpack 使用 MiniCssExtractPlugin 插件, 将文件(如Vue单文件组件)中的CSS代码, 经过处理后, 分离到形如app.hash1234.css的单独的CSS文件:
如果没有加入防止样式污染的措施的同时, 项目中存在了大量的同名 ClassName, 那么可能会产生意想不到的CSS选择器权重覆盖. 这可能使后文件中某部分选择器权重更高的类影响整个应用, 而此过程通常发生在组件的编写中, 所以一般称之为组件样式污染.
Webpack & Vue SFC Object
对于 Vue 项目而言, 使用 Webpack 将极大的优化了工作流程, 因为通过Vue Loader, Vue 单文件组件能很好的融合进 Webpack 工作流中. 通过跟踪源码, 可以发现, 我们写的单文件组件都被处理为了SFC对象, 即包含了单个HTML模块, 单个脚本模块, 一个或多个样式模块, 一个或多个自定义模块的对象:
// vue-loader/index.js const descriptor = parse({ source, compiler: options.compiler || loadTemplateCompiler(), filename, sourceRoot, needMap: sourceMap }) // vuejs/component-compiler-utils/index.js function parse(options) { const { compiler } = options output = compiler.parseComponent(source, compilerParseOptions) return output } // vue.js function parseComponent(content, options) { // ... var sfc = { template: null, script: null, styles: [], customBlocks: [] } // ... return sfc }
我们可以将SFC结构融合到Webpack进行开发的过程成中, 主要有这几点影响:
允许为 Vue 组件的每个部分使用其它的 webpack loader,例如在 <style>的部分使用 Sass Loader , 在 <customBlocks>的部分使用自定义 Loader
使用 webpack loader 将 <style>和 <template> 中引用的资源当作模块依赖来处理
模拟 Scoped CSS
在开发过程中使用热重载来保持状态
以下主要介绍Scoped CSS的原理.
Scoped CSS
大白话版本之 Scoped CSS 原理
通过 Webpack 调用 VueJS 中相应 Loader , 给组件HTML模板添加自定义属性 (Attribute) data-v-x, 以及给组件内CSS选择器添加对应的属性选择器 (Attribute Selector) [data-v-x], 达到组件内样式只能生效与组件内HTML的效果, 代码效果如下:
<div data-v-lionad></div> <style> .lionad[data-v-lionad] { background: @tiger-orange; } </style>
源码跟踪
Webpack 使用其它 CSS Loader 处理 VueJS 中对应 CSS 代码之前, Vue Loader 已经替我们做了一层简单的处理, 如果组件中 style 块包含了 scoped 属性:
<!-- 某个VueJS组件中 --> <template> <div></div> </template> <style lang="scss" scoped> .lionad { background: @tiger-orange; } </style>
下代码即判断当前SFC对象样式块中是否有scoped属性, 并插入用于 query 中, 顺带一提, 每个单文件组件被解析后, 都会生成对应组件ID, ID主要以生产/开发环境做区分, 通过文件路径+源码或是文件路径的值作为哈希特征值的形式生成, 如下:
// vue-loader/index.js const id = hash(isProduction (shortFilePath + '\n' + source) : shortFilePath) const hasScoped = descriptor.styles.some(s => s.scoped) const query = `? vue&type=template${idQuery}${scopedQuery}` const request = templateRequest = stringifyRequest(src + query) templateImport = `import { render, staticRenderFns } from ${request}`
HTML模板处理
在用于处理SFC结构中HTML模板的 templateLoader 中, 我们可以得知, query 中所设置的参数将合并为 loader options 经由 Webpack 转交 templateLoader 再转交 @vue/component-compiler-utils.compileTemplate 处理:
// vue-loader/templateLoader.js const query = qs.parse(this.resourceQuery) const { id } = query const compilerOptions = Object.assign({}, options.compilerOptions, { scopeId: query.scoped ? `data-v-${id}` : null }) const compiled = compileTemplate({ compilerOptions })