Skip to content

Commit 3fad1cf

Browse files
YiSiWangyyx990803
authored andcommitted
CSS modules (vuejs#331)
* add simple support for CSS modules * add test case * add README * add static class replacement * add styles entry in script * update README * update README * improve test case * change test case * remove class replacement * update README * remove unused codes * add support for preprocessor * improve test case * add default module support * update docs
1 parent 8301d7c commit 3fad1cf

File tree

7 files changed

+174
-14
lines changed

7 files changed

+174
-14
lines changed

docs/en/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
- Features
55
- [ES2015 and Babel](features/es2015.md)
66
- [Scoped CSS](features/scoped-css.md)
7+
- [CSS Modules](features/css-modules.md)
78
- [PostCSS and Autoprefixer](features/postcss.md)
89
- [Hot Reload](features/hot-reload.md)
910
- Configurations

docs/en/features/css-modules.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# CSS Modules
2+
3+
[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!
4+
5+
With vue-loader, you can simply use CSS Modules with `<style module>`.
6+
7+
The name-to-hash map `$style` will be injected as a computed property.
8+
9+
Example:
10+
11+
```html
12+
<style module>
13+
.red { color: red; }
14+
/*
15+
becomes
16+
._8x_KsHmyrocTNd7akA_LL { color: red; }
17+
*/
18+
</style>
19+
20+
<template>
21+
<h2 v-bind:class="$style.red"></h2>
22+
</template>
23+
24+
<script>
25+
export default {
26+
ready() {
27+
console.log(this.$style.red)
28+
// => _8x_KsHmyrocTNd7akA_LL
29+
}
30+
}
31+
</script>
32+
```
33+
34+
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.
35+
36+
Example:
37+
38+
```html
39+
<style module="foo" src="..."></style>
40+
<style module="bar" src="..."></style>
41+
42+
<template>
43+
<h2 v-bind:class="foo.red"></h2>
44+
<h2 v-bind:class="bar.red"></h2>
45+
</template>
46+
```
47+
48+
## Tips
49+
50+
1. Animation names also get transformed. So, it's recommended to use animations with CSS modules.
51+
52+
2. You can use `scoped` and `module` together to avoid problems in descendant selectors.
53+
54+
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.
55+
56+
4. You can expose the class name of component's root element for theming.

docs/en/features/scoped-css.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,4 @@ Into the following:
4848

4949
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.
5050

51-
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.
51+
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).

lib/loader.js

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ module.exports = function (content) {
6464
// disable all configuration loaders
6565
'!!' +
6666
// get loader string for pre-processors
67-
getLoaderString(type, part, scoped) +
67+
getLoaderString(type, part, index, scoped) +
6868
// select the corresponding part from the vue file
6969
getSelectorString(type, index || 0) +
7070
// the url to the actual vuefile
@@ -81,17 +81,40 @@ module.exports = function (content) {
8181
function getRequireForImportString (type, impt, scoped) {
8282
return loaderUtils.stringifyRequest(loaderContext,
8383
'!!' +
84-
getLoaderString(type, impt, scoped) +
84+
getLoaderString(type, impt, -1, scoped) +
8585
impt.src
8686
)
8787
}
8888

89-
function getLoaderString (type, part, scoped) {
89+
function addCssModulesToLoader (loader, part, index) {
90+
if (!part.module) return loader
91+
return loader.replace(/((?:^|!)css(?:-loader)?)(\?[^!]*)?/, function (m, $1, $2) {
92+
// $1: !css-loader
93+
// $2: ?a=b
94+
var option = loaderUtils.parseQuery($2)
95+
option.modules = true
96+
option.importLoaders = true
97+
option.localIdentName = '[hash:base64]'
98+
if (index !== -1) {
99+
// Note:
100+
// Class name is generated according to its filename.
101+
// Different <style> tags in the same .vue file may generate same names.
102+
// Append `_[index]` to class name to avoid this.
103+
option.localIdentName += '_' + index
104+
}
105+
return $1 + '?' + JSON.stringify(option)
106+
})
107+
}
108+
109+
function getLoaderString (type, part, index, scoped) {
90110
var lang = part.lang || defaultLang[type]
91111
var loader = loaders[lang]
92112
var rewriter = getRewriter(type, scoped)
93113
var injectString = (type === 'script' && query.inject) ? 'inject!' : ''
94114
if (loader !== undefined) {
115+
if (type === 'style') {
116+
loader = addCssModulesToLoader(loader, part, index)
117+
}
95118
// inject rewriter before css/html loader for
96119
// extractTextPlugin use cases
97120
if (rewriterInjectRE.test(loader)) {
@@ -108,7 +131,8 @@ module.exports = function (content) {
108131
case 'template':
109132
return defaultLoaders.html + '!' + rewriter + templateLoader + '?raw&engine=' + lang + '!'
110133
case 'style':
111-
return defaultLoaders.css + '!' + rewriter + lang + '!'
134+
loader = addCssModulesToLoader(defaultLoaders.css, part, index)
135+
return loader + '!' + rewriter + lang + '!'
112136
case 'script':
113137
return injectString + lang + '!'
114138
}
@@ -143,24 +167,40 @@ module.exports = function (content) {
143167

144168
var parts = parse(content, fileName, this.sourceMap)
145169
var hasLocalStyles = false
146-
var output = 'var __vue_script__, __vue_template__\n'
170+
var output = 'var __vue_script__, __vue_template__\n' +
171+
'var __vue_styles__ = {}\n'
147172

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

179+
var cssModules = {}
180+
function setCssModule (style, require) {
181+
if (!style.module) return require
182+
if (style.module in cssModules) {
183+
loaderContext.emitError('CSS module name "' + style.module + '" is not unique!')
184+
return require
185+
}
186+
cssModules[style.module] = true
187+
return '__vue_styles__["' + style.module + '"] = ' + require + '\n'
188+
}
189+
154190
// add requires for src imports
155191
parts.styleImports.forEach(function (impt) {
156192
if (impt.scoped) hasLocalStyles = true
157-
output += getRequireForImport('style', impt, impt.scoped)
193+
if (impt.module === '') impt.module = '$style'
194+
var requireString = getRequireForImport('style', impt, impt.scoped, impt.module)
195+
output += setCssModule(impt, requireString)
158196
})
159197

160198
// add requires for styles
161199
parts.style.forEach(function (style, i) {
162200
if (style.scoped) hasLocalStyles = true
163-
output += getRequire('style', style, i, style.scoped)
201+
if (style.module === '') style.module = '$style'
202+
var requireString = getRequire('style', style, i, style.scoped, style.module)
203+
output += setCssModule(style, requireString)
164204
})
165205

166206
// add require for script
@@ -203,11 +243,18 @@ module.exports = function (content) {
203243
output +=
204244
'module.exports = __vue_script__ || {}\n' +
205245
'if (module.exports.__esModule) module.exports = module.exports.default\n' +
246+
'var __vue_options__ = typeof module.exports === "function" ' +
247+
'? (module.exports.options || (module.exports.options = {})) ' +
248+
': module.exports\n' +
206249
'if (__vue_template__) {\n' +
207-
'(typeof module.exports === "function" ' +
208-
'? (module.exports.options || (module.exports.options = {})) ' +
209-
': module.exports).template = __vue_template__\n' +
210-
'}\n'
250+
'__vue_options__.template = __vue_template__\n' +
251+
'}\n' +
252+
// inject style modules as computed properties
253+
'if (!__vue_options__.computed) __vue_options__.computed = {}\n' +
254+
'Object.keys(__vue_styles__).forEach(function (key) {\n' +
255+
'var module = __vue_styles__[key]\n' +
256+
'__vue_options__.computed[key] = function () { return module }\n' +
257+
'})\n'
211258
// hot reload
212259
if (
213260
!this.minimize &&

lib/parser.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ module.exports = function (content, filename, needMap) {
4040
var lang = getAttribute(node, 'lang')
4141
var src = getAttribute(node, 'src')
4242
var scoped = getAttribute(node, 'scoped') != null
43+
var module = getAttribute(node, 'module')
4344
var warnings = null
4445
var map = null
4546

@@ -64,7 +65,8 @@ module.exports = function (content, filename, needMap) {
6465
output.styleImports.push({
6566
src: src,
6667
lang: lang,
67-
scoped: scoped
68+
scoped: scoped,
69+
module: module
6870
})
6971
} else if (type === 'template') {
7072
output.template.push({
@@ -151,6 +153,7 @@ module.exports = function (content, filename, needMap) {
151153
output[type].push({
152154
lang: lang,
153155
scoped: scoped,
156+
module: module,
154157
content: result,
155158
map: map && map.toJSON(),
156159
warnings: warnings

test/fixtures/css-modules.vue

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<style module="style">
2+
.red {
3+
color: red;
4+
}
5+
@keyframes fade {
6+
from { opacity: 1; } to { opacity: 0; }
7+
}
8+
.animate {
9+
animation: fade 1s;
10+
}
11+
</style>
12+
13+
<style scoped lang="stylus" module>
14+
.red
15+
color: red
16+
</style>
17+
18+
<script>
19+
module.exports = {}
20+
</script>

test/test.js

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ describe('vue-loader', function () {
3434

3535
function getFile (file, cb) {
3636
fs.readFile(path.resolve(outputDir, file), 'utf-8', function (err, data) {
37-
expect(err).to.be.not.exist
37+
expect(err).to.not.exist
3838
cb(data)
3939
})
4040
}
@@ -276,4 +276,37 @@ describe('vue-loader', function () {
276276
done()
277277
})
278278
})
279+
280+
it('css-modules', function (done) {
281+
test({
282+
entry: './test/fixtures/css-modules.vue'
283+
}, function (window) {
284+
var module = window.vueModule
285+
286+
// get local class name
287+
var className = module.computed.style().red
288+
expect(className).to.match(/^_/)
289+
290+
// class name in style
291+
var style = [].slice.call(window.document.querySelectorAll('style')).map(function (style) {
292+
return style.textContent
293+
}).join('\n')
294+
expect(style).to.contain('.' + className + ' {\n color: red;\n}')
295+
296+
// animation name
297+
var match = style.match(/@keyframes\s+(\S+)\s+{/)
298+
expect(match).to.have.length(2)
299+
var animationName = match[1]
300+
expect(animationName).to.not.equal('fade')
301+
expect(style).to.contain('animation: ' + animationName + ' 1s;')
302+
303+
// default module + pre-processor + scoped
304+
var anotherClassName = module.computed.$style().red
305+
expect(anotherClassName).to.match(/^_/).and.not.equal(className)
306+
var id = '_v-' + hash(require.resolve('./fixtures/css-modules.vue'))
307+
expect(style).to.contain('.' + anotherClassName + '[' + id + ']')
308+
309+
done()
310+
})
311+
})
279312
})

0 commit comments

Comments
 (0)