Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/en/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
- Features
- [ES2015 and Babel](features/es2015.md)
- [Scoped CSS](features/scoped-css.md)
- [CSS Modules](features/css-modules.md)
- [PostCSS and Autoprefixer](features/postcss.md)
- [Hot Reload](features/hot-reload.md)
- Configurations
Expand Down
56 changes: 56 additions & 0 deletions docs/en/features/css-modules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# CSS Modules

[CSS Modules](https://github.com/css-modules/css-modules) aims to solve class & animation name conflicts. It replaces all the local names with unique hashes and provides a name-to-hash map. So you can write short and general names without worrying any conflict!

With vue-loader, you can simply use CSS Modules with `<style module>`.

The name-to-hash map `$style` will be injected as a computed property.

Example:

```html
<style module>
.red { color: red; }
/*
becomes
._8x_KsHmyrocTNd7akA_LL { color: red; }
*/
</style>

<template>
<h2 v-bind:class="$style.red"></h2>
</template>

<script>
export default {
ready() {
console.log(this.$style.red)
// => _8x_KsHmyrocTNd7akA_LL
}
}
</script>
```

If you need mutiple `<style>` tags with `module` (or you hate `$style` being injected), you can specify the module name with `<style module="moduleName">`. `moduleName` will get injected instead.

Example:

```html
<style module="foo" src="..."></style>
<style module="bar" src="..."></style>

<template>
<h2 v-bind:class="foo.red"></h2>
<h2 v-bind:class="bar.red"></h2>
</template>
```

## Tips

1. Animation names also get transformed. So, it's recommended to use animations with CSS modules.

2. You can use `scoped` and `module` together to avoid problems in descendant selectors.

3. Use `module` only (without `scoped`), you are able to style `<slot>`s and children components. But styling children components breaks the principle of components. You can put `<slot>` in a classed wrapper and style it under that class.

4. You can expose the class name of component's root element for theming.
2 changes: 1 addition & 1 deletion docs/en/features/scoped-css.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,4 @@ Into the following:

4. **Scoped styles do not eliminate the need for classes**. Due to the way browsers render various CSS selectors, `p { color: red }` will be many times slower when scoped (i.e. when combined with an attribute selector). If you use classes or ids instead, such as in `.example { color: red }`, then you virtually eliminate that performance hit. [Here's a playground](http://stevesouders.com/efws/css-selectors/csscreate.php) where you can test the differences yourself.

5. **Be careful with descendant selectors in recursive components!** For a CSS rule with the selector `.a .b`, if the element that matches `.a` contains a recursive child component, then all `.b` in that child component will be matched by the rule.
5. **Be careful with descendant selectors in recursive components!** For a CSS rule with the selector `.a .b`, if the element that matches `.a` contains a recursive child component, then all `.b` in that child component will be matched by the rule. To avoid class name conflicts, you can use [CSS Modules](features/css-modules.md).
69 changes: 58 additions & 11 deletions lib/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ module.exports = function (content) {
// disable all configuration loaders
'!!' +
// get loader string for pre-processors
getLoaderString(type, part, scoped) +
getLoaderString(type, part, index, scoped) +
// select the corresponding part from the vue file
getSelectorString(type, index || 0) +
// the url to the actual vuefile
Expand All @@ -81,17 +81,40 @@ module.exports = function (content) {
function getRequireForImportString (type, impt, scoped) {
return loaderUtils.stringifyRequest(loaderContext,
'!!' +
getLoaderString(type, impt, scoped) +
getLoaderString(type, impt, -1, scoped) +
impt.src
)
}

function getLoaderString (type, part, scoped) {
function addCssModulesToLoader (loader, part, index) {
if (!part.module) return loader
return loader.replace(/((?:^|!)css(?:-loader)?)(\?[^!]*)?/, function (m, $1, $2) {
// $1: !css-loader
// $2: ?a=b
var option = loaderUtils.parseQuery($2)
option.modules = true
option.importLoaders = true
option.localIdentName = '[hash:base64]'
if (index !== -1) {
// Note:
// Class name is generated according to its filename.
// Different <style> tags in the same .vue file may generate same names.
// Append `_[index]` to class name to avoid this.
option.localIdentName += '_' + index
}
return $1 + '?' + JSON.stringify(option)
})
}

function getLoaderString (type, part, index, scoped) {
var lang = part.lang || defaultLang[type]
var loader = loaders[lang]
var rewriter = getRewriter(type, scoped)
var injectString = (type === 'script' && query.inject) ? 'inject!' : ''
if (loader !== undefined) {
if (type === 'style') {
loader = addCssModulesToLoader(loader, part, index)
}
// inject rewriter before css/html loader for
// extractTextPlugin use cases
if (rewriterInjectRE.test(loader)) {
Expand All @@ -108,7 +131,8 @@ module.exports = function (content) {
case 'template':
return defaultLoaders.html + '!' + rewriter + templateLoader + '?raw&engine=' + lang + '!'
case 'style':
return defaultLoaders.css + '!' + rewriter + lang + '!'
loader = addCssModulesToLoader(defaultLoaders.css, part, index)
return loader + '!' + rewriter + lang + '!'
case 'script':
return injectString + lang + '!'
}
Expand Down Expand Up @@ -143,24 +167,40 @@ module.exports = function (content) {

var parts = parse(content, fileName, this.sourceMap)
var hasLocalStyles = false
var output = 'var __vue_script__, __vue_template__\n'
var output = 'var __vue_script__, __vue_template__\n' +
'var __vue_styles__ = {}\n'

// check if there are any template syntax errors
var templateWarnings = parts.template.length && parts.template[0].warnings
if (templateWarnings) {
templateWarnings.forEach(this.emitError)
}

var cssModules = {}
function setCssModule (style, require) {
if (!style.module) return require
if (style.module in cssModules) {
loaderContext.emitError('CSS module name "' + style.module + '" is not unique!')
return require
}
cssModules[style.module] = true
return '__vue_styles__["' + style.module + '"] = ' + require + '\n'
}

// add requires for src imports
parts.styleImports.forEach(function (impt) {
if (impt.scoped) hasLocalStyles = true
output += getRequireForImport('style', impt, impt.scoped)
if (impt.module === '') impt.module = '$style'
var requireString = getRequireForImport('style', impt, impt.scoped, impt.module)
output += setCssModule(impt, requireString)
})

// add requires for styles
parts.style.forEach(function (style, i) {
if (style.scoped) hasLocalStyles = true
output += getRequire('style', style, i, style.scoped)
if (style.module === '') style.module = '$style'
var requireString = getRequire('style', style, i, style.scoped, style.module)
output += setCssModule(style, requireString)
})

// add require for script
Expand Down Expand Up @@ -203,11 +243,18 @@ module.exports = function (content) {
output +=
'module.exports = __vue_script__ || {}\n' +
'if (module.exports.__esModule) module.exports = module.exports.default\n' +
'var __vue_options__ = typeof module.exports === "function" ' +
'? (module.exports.options || (module.exports.options = {})) ' +
': module.exports\n' +
'if (__vue_template__) {\n' +
'(typeof module.exports === "function" ' +
'? (module.exports.options || (module.exports.options = {})) ' +
': module.exports).template = __vue_template__\n' +
'}\n'
'__vue_options__.template = __vue_template__\n' +
'}\n' +
// inject style modules as computed properties
'if (!__vue_options__.computed) __vue_options__.computed = {}\n' +
'Object.keys(__vue_styles__).forEach(function (key) {\n' +
'var module = __vue_styles__[key]\n' +
'__vue_options__.computed[key] = function () { return module }\n' +
'})\n'
// hot reload
if (
!this.minimize &&
Expand Down
5 changes: 4 additions & 1 deletion lib/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ module.exports = function (content, filename, needMap) {
var lang = getAttribute(node, 'lang')
var src = getAttribute(node, 'src')
var scoped = getAttribute(node, 'scoped') != null
var module = getAttribute(node, 'module')
var warnings = null
var map = null

Expand All @@ -64,7 +65,8 @@ module.exports = function (content, filename, needMap) {
output.styleImports.push({
src: src,
lang: lang,
scoped: scoped
scoped: scoped,
module: module
})
} else if (type === 'template') {
output.template.push({
Expand Down Expand Up @@ -151,6 +153,7 @@ module.exports = function (content, filename, needMap) {
output[type].push({
lang: lang,
scoped: scoped,
module: module,
content: result,
map: map && map.toJSON(),
warnings: warnings
Expand Down
20 changes: 20 additions & 0 deletions test/fixtures/css-modules.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<style module="style">
.red {
color: red;
}
@keyframes fade {
from { opacity: 1; } to { opacity: 0; }
}
.animate {
animation: fade 1s;
}
</style>

<style scoped lang="stylus" module>
.red
color: red
</style>

<script>
module.exports = {}
</script>
35 changes: 34 additions & 1 deletion test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe('vue-loader', function () {

function getFile (file, cb) {
fs.readFile(path.resolve(outputDir, file), 'utf-8', function (err, data) {
expect(err).to.be.not.exist
expect(err).to.not.exist
cb(data)
})
}
Expand Down Expand Up @@ -276,4 +276,37 @@ describe('vue-loader', function () {
done()
})
})

it('css-modules', function (done) {
test({
entry: './test/fixtures/css-modules.vue'
}, function (window) {
var module = window.vueModule

// get local class name
var className = module.computed.style().red
expect(className).to.match(/^_/)

// class name in style
var style = [].slice.call(window.document.querySelectorAll('style')).map(function (style) {
return style.textContent
}).join('\n')
expect(style).to.contain('.' + className + ' {\n color: red;\n}')

// animation name
var match = style.match(/@keyframes\s+(\S+)\s+{/)
expect(match).to.have.length(2)
var animationName = match[1]
expect(animationName).to.not.equal('fade')
expect(style).to.contain('animation: ' + animationName + ' 1s;')

// default module + pre-processor + scoped
var anotherClassName = module.computed.$style().red
expect(anotherClassName).to.match(/^_/).and.not.equal(className)
var id = '_v-' + hash(require.resolve('./fixtures/css-modules.vue'))
expect(style).to.contain('.' + anotherClassName + '[' + id + ']')

done()
})
})
})