├── .eslintignore ├── .gitignore ├── .npmignore ├── demo └── gulp-project-demo │ ├── .gitignore │ ├── src │ ├── components │ │ ├── child1 │ │ │ ├── index.json │ │ │ ├── index.wxml │ │ │ ├── index.module.wxss │ │ │ └── index.js │ │ └── child2 │ │ │ ├── index.json │ │ │ ├── index.wxml │ │ │ ├── index.module.scss │ │ │ └── index.js │ ├── app.json │ ├── pages │ │ └── index │ │ │ ├── index.js │ │ │ ├── index.json │ │ │ ├── index.wxml │ │ │ └── index.scss │ ├── sitemap.json │ ├── app.scss │ ├── utils │ │ └── util.js │ ├── app.js │ └── project.config.json │ ├── README.md │ ├── package.json │ └── gulpfile.js ├── CHANGELOG.md ├── .eslintrc ├── lib └── gulp │ ├── remove-map.js │ ├── postcss-deal.js │ ├── himalayaStringifyChanged │ ├── stringify.js │ └── compat.js │ ├── index.js │ ├── replace-wxml.js │ ├── utils.js │ └── replace-js.js ├── LICENSE ├── package.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | demo 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | demo -------------------------------------------------------------------------------- /demo/gulp-project-demo/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /demo/gulp-project-demo/src/components/child1/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": true 3 | } -------------------------------------------------------------------------------- /demo/gulp-project-demo/src/components/child2/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": true 3 | } -------------------------------------------------------------------------------- /demo/gulp-project-demo/src/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | "pages/index/index" 4 | ] 5 | } -------------------------------------------------------------------------------- /demo/gulp-project-demo/src/pages/index/index.js: -------------------------------------------------------------------------------- 1 | //index.js 2 | //获取应用实例 3 | 4 | const app = getApp() 5 | 6 | Page({}) 7 | -------------------------------------------------------------------------------- /demo/gulp-project-demo/README.md: -------------------------------------------------------------------------------- 1 | # weapp-css-modules的gulp项目示例 2 | 3 | ## 使用 4 | 5 | ``` 6 | npm i 7 | npm run build:module 8 | 9 | ``` -------------------------------------------------------------------------------- /demo/gulp-project-demo/src/pages/index/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": { 3 | "child1": "/components/child1/index", 4 | "child2": "/components/child2/index" 5 | } 6 | } -------------------------------------------------------------------------------- /demo/gulp-project-demo/src/pages/index/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | hello,weapp-css-module 6 | -------------------------------------------------------------------------------- /demo/gulp-project-demo/src/sitemap.json: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", 3 | "rules": [{ 4 | "action": "allow", 5 | "page": "*" 6 | }] 7 | } -------------------------------------------------------------------------------- /demo/gulp-project-demo/src/app.scss: -------------------------------------------------------------------------------- 1 | /**app.wxss**/ 2 | .container { 3 | height: 100%; 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | justify-content: space-between; 8 | padding: 200rpx 0; 9 | box-sizing: border-box; 10 | } 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.3 2 | 3 | 调整 gulp 插件包名为 gulp-weapp-css-modules,更新后续适配 webpack 插件计划 4 | 5 | ## 0.1.2 6 | 7 | 适配已编译 css-modules 的场景,新增 generateSimpleScopedName 函数用以生成短类名 8 | 9 | ## 0.1.1 10 | 11 | 修复 needCssModuleTransform 参数未初始化问题 12 | 13 | 14 | ## 0.1.0 15 | 16 | Initial version 17 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es2021": true, 6 | "node": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "parserOptions": { 10 | "ecmaVersion": 12 11 | }, 12 | "rules": { 13 | "no-unused-vars": [ 14 | "error" 15 | ], 16 | } 17 | } -------------------------------------------------------------------------------- /demo/gulp-project-demo/src/pages/index/index.scss: -------------------------------------------------------------------------------- 1 | /**index.wxss**/ 2 | .userinfo { 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | } 7 | 8 | .userinfo-avatar { 9 | width: 128rpx; 10 | height: 128rpx; 11 | margin: 20rpx; 12 | border-radius: 50%; 13 | } 14 | 15 | .userinfo-nickname { 16 | color: #aaa; 17 | } 18 | 19 | .usermotto { 20 | margin-top: 200px; 21 | } -------------------------------------------------------------------------------- /lib/gulp/remove-map.js: -------------------------------------------------------------------------------- 1 | 2 | const fs = require('fs-extra') 3 | const path = require('path'); 4 | const { TEMP_PATH } = require('./utils') 5 | 6 | module.exports = function (toDelList = []) { 7 | const tempJson = fs.readFileSync(path.resolve(__dirname, TEMP_PATH)); 8 | Object.values(JSON.parse(tempJson)).forEach(list => { 9 | list.forEach(ele => { 10 | fs.removeSync(ele.source) 11 | }) 12 | }) 13 | toDelList.forEach(item => { 14 | fs.removeSync(item) 15 | }) 16 | fs.removeSync(path.resolve(__dirname, TEMP_PATH)) 17 | } -------------------------------------------------------------------------------- /demo/gulp-project-demo/src/utils/util.js: -------------------------------------------------------------------------------- 1 | const formatTime = date => { 2 | const year = date.getFullYear() 3 | const month = date.getMonth() + 1 4 | const day = date.getDate() 5 | const hour = date.getHours() 6 | const minute = date.getMinutes() 7 | const second = date.getSeconds() 8 | 9 | return [year, month, day].map(formatNumber).join('/') + ' ' + [hour, minute, second].map(formatNumber).join(':') 10 | } 11 | 12 | const formatNumber = n => { 13 | n = n.toString() 14 | return n[1] ? n : '0' + n 15 | } 16 | 17 | module.exports = { 18 | formatTime: formatTime 19 | } 20 | -------------------------------------------------------------------------------- /demo/gulp-project-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-project-demo", 3 | "version": "1.0.0", 4 | "description": "demo for weapp-css-modules with gulp", 5 | "main": "gulpfile.js", 6 | "scripts": { 7 | "build": "gulp", 8 | "build:module": "gulp build:module" 9 | }, 10 | "author": "Silencesnow", 11 | "license": "MIT", 12 | "dependencies": { 13 | "gulp-cli": "^2.3.0" 14 | }, 15 | "devDependencies": { 16 | "gulp": "^4.0.2", 17 | "del": "^6.0.0", 18 | "fs-extra": "^9.0.1", 19 | "gulp-if": "^3.0.0", 20 | "gulp-rename": "^2.0.0", 21 | "gulp-sass": "^4.1.0", 22 | "gulp-sort": "^2.0.0", 23 | "gulp-weapp-css-modules": "^0.1.3" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /demo/gulp-project-demo/src/components/child1/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /demo/gulp-project-demo/src/components/child2/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /demo/gulp-project-demo/src/app.js: -------------------------------------------------------------------------------- 1 | //app.js 2 | App({ 3 | onLaunch: function () { 4 | // 展示本地存储能力 5 | var logs = wx.getStorageSync('logs') || [] 6 | logs.unshift(Date.now()) 7 | wx.setStorageSync('logs', logs) 8 | 9 | // 登录 10 | wx.login({ 11 | success: res => { 12 | // 发送 res.code 到后台换取 openId, sessionKey, unionId 13 | } 14 | }) 15 | // 获取用户信息 16 | wx.getSetting({ 17 | success: res => { 18 | if (res.authSetting['scope.userInfo']) { 19 | // 已经授权,可以直接调用 getUserInfo 获取头像昵称,不会弹框 20 | wx.getUserInfo({ 21 | success: res => { 22 | // 可以将 res 发送给后台解码出 unionId 23 | this.globalData.userInfo = res.userInfo 24 | 25 | // 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回 26 | // 所以此处加入 callback 以防止这种情况 27 | if (this.userInfoReadyCallback) { 28 | this.userInfoReadyCallback(res) 29 | } 30 | } 31 | }) 32 | } 33 | } 34 | }) 35 | }, 36 | globalData: { 37 | userInfo: null 38 | } 39 | }) -------------------------------------------------------------------------------- /demo/gulp-project-demo/src/components/child2/index.module.scss: -------------------------------------------------------------------------------- 1 | 2 | .banner { 3 | position: relative; 4 | overflow: hidden; 5 | height: 100px; 6 | width: 350px; 7 | margin: 50px auto; 8 | &__swiper { 9 | position: absolute; 10 | top: 0; 11 | left: 0; 12 | width: 100%; 13 | height: 100%; 14 | border-radius: 5px; 15 | &-img { 16 | display: block; 17 | width: 100%; 18 | height: 100%; 19 | border-radius: 5px; 20 | } 21 | } 22 | 23 | &__dots { 24 | position: absolute; 25 | left: 0; 26 | right: 0; 27 | bottom: 5px; 28 | height: 4px; 29 | text-align: center; 30 | font-size: 0; 31 | z-index: 1; 32 | } 33 | 34 | &__dot { 35 | display: inline-block; 36 | margin: 0 2px; 37 | width: 4px; 38 | height: 4px; 39 | border-radius: 4px; 40 | opacity: 0.5; 41 | background: #FFF; 42 | transition: all 0.2s; 43 | &--cur { 44 | width: 16px; 45 | background: #FFF; 46 | opacity: 1; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 AOTU Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /demo/gulp-project-demo/src/components/child1/index.module.wxss: -------------------------------------------------------------------------------- 1 | 2 | .banner { 3 | position: relative; 4 | overflow: hidden; 5 | height: 100px; 6 | width: 350px; 7 | margin: 50px auto;} 8 | .banner__swiper { 9 | position: absolute; 10 | top: 0; 11 | left: 0; 12 | width: 100%; 13 | height: 100%; 14 | border-radius: 5px;} 15 | .banner__swiper-img { 16 | display: block; 17 | width: 100%; 18 | height: 100%; 19 | border-radius: 5px; 20 | } 21 | 22 | 23 | .banner__dots { 24 | position: absolute; 25 | left: 0; 26 | right: 0; 27 | bottom: 5px; 28 | height: 4px; 29 | text-align: center; 30 | font-size: 0; 31 | z-index: 1; 32 | composes: banner; 33 | } 34 | 35 | .banner__dot { 36 | display: inline-block; 37 | margin: 0 2px; 38 | width: 4px; 39 | height: 4px; 40 | border-radius: 4px; 41 | opacity: 0.5; 42 | background: #FFF; 43 | transition: all 0.2s; } 44 | .banner__dot--cur { 45 | width: 16px; 46 | background: #FFF; 47 | opacity: 1; 48 | } 49 | 50 | 51 | -------------------------------------------------------------------------------- /demo/gulp-project-demo/src/components/child1/index.js: -------------------------------------------------------------------------------- 1 | 2 | import styles from './index.module.wxss'; 3 | 4 | Component({ 5 | data: { 6 | styles: styles, 7 | current: 0, 8 | list: [ 9 | { 10 | "img": "https://img20.360buyimg.com/ling/jfs/t1/134524/9/15501/294288/5fa9f86aEadd02f8a/906f144f4748d16c.jpg", 11 | }, 12 | { 13 | 14 | "img": "https://img30.360buyimg.com/ling/jfs/t1/152681/11/5340/265556/5fa9f877E5e0267a6/b0a75f36bf3a1c62.jpg", 15 | 16 | }, 17 | { 18 | "img": "https://img13.360buyimg.com/ling/jfs/t1/125393/34/17977/253427/5fa9f870E55e045ce/8150d19f05e323c9.jpg", 19 | }, 20 | { 21 | "img": "https://img20.360buyimg.com/ling/jfs/t1/134524/9/15501/294288/5fa9f86aEadd02f8a/906f144f4748d16c.jpg", 22 | }, 23 | { 24 | "img": "https://img13.360buyimg.com/ling/jfs/t1/139062/9/13989/165189/5fa9f9b6Ede33f131/15128211fee1f794.jpg", 25 | }] 26 | }, 27 | methods: { 28 | onSwiperChange(e) { 29 | const { current } = e.detail; 30 | this.setData({ current }) 31 | } 32 | } 33 | 34 | }) -------------------------------------------------------------------------------- /demo/gulp-project-demo/src/components/child2/index.js: -------------------------------------------------------------------------------- 1 | 2 | import styles from './index.module.scss'; 3 | 4 | Component({ 5 | data: { 6 | styles: styles, 7 | current: 0, 8 | list: [ 9 | { 10 | "img": "https://img20.360buyimg.com/ling/jfs/t1/134524/9/15501/294288/5fa9f86aEadd02f8a/906f144f4748d16c.jpg", 11 | }, 12 | { 13 | 14 | "img": "https://img30.360buyimg.com/ling/jfs/t1/152681/11/5340/265556/5fa9f877E5e0267a6/b0a75f36bf3a1c62.jpg", 15 | 16 | }, 17 | { 18 | "img": "https://img13.360buyimg.com/ling/jfs/t1/125393/34/17977/253427/5fa9f870E55e045ce/8150d19f05e323c9.jpg", 19 | }, 20 | { 21 | "img": "https://img20.360buyimg.com/ling/jfs/t1/134524/9/15501/294288/5fa9f86aEadd02f8a/906f144f4748d16c.jpg", 22 | }, 23 | { 24 | "img": "https://img13.360buyimg.com/ling/jfs/t1/139062/9/13989/165189/5fa9f9b6Ede33f131/15128211fee1f794.jpg", 25 | }] 26 | }, 27 | methods: { 28 | onSwiperChange(e) { 29 | const { current } = e.detail; 30 | this.setData({ current }) 31 | } 32 | } 33 | 34 | }) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "weapp-css-modules", 3 | "version": "0.1.3", 4 | "description": "css-modules for weapp in gulp", 5 | "main": "lib/gulp/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "css-modules", 11 | "weapp", 12 | "wxapp" 13 | ], 14 | "author": "Silencesnow", 15 | "license": "MIT", 16 | "dependencies": { 17 | "@babel/core": "^7.12.3", 18 | "@babel/plugin-proposal-decorators": "^7.12.1", 19 | "@babel/plugin-proposal-object-rest-spread": "^7.12.1", 20 | "@babel/plugin-syntax-jsx": "^7.12.1", 21 | "babel-generator": "^6.26.1", 22 | "babel-plugin-transform-class-properties": "^6.24.1", 23 | "babel-traverse": "^6.26.0", 24 | "babel-types": "^6.26.0", 25 | "colors": "^1.4.0", 26 | "fs-extra": "^9.0.1", 27 | "himalaya-walk": "^1.0.0", 28 | "himalaya-wxml": "^1.1.0", 29 | "lodash": "^4.17.20", 30 | "postcss-modules": "^3.2.2", 31 | "postcss-scss": "^3.0.4", 32 | "through2": "^4.0.2", 33 | "gulp-postcss": "^9.0.0" 34 | }, 35 | "devDependencies": { 36 | "eslint": "^7.13.0", 37 | "gulp": "^4.0.2", 38 | "gulp-babel": "^8.0.0", 39 | "gulp-if": "^3.0.0", 40 | "gulp-order": "^1.2.0", 41 | "gulp-sort": "^2.0.0" 42 | } 43 | } -------------------------------------------------------------------------------- /demo/gulp-project-demo/gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require("gulp") 2 | const sass = require('gulp-sass'); 3 | const rename = require("gulp-rename"); 4 | const del = require('del'); 5 | const gulpif = require('gulp-if'); 6 | const sort = require('gulp-sort'); 7 | 8 | const { weappCssModule, wcmSortFn } = require('gulp-weapp-css-modules') 9 | 10 | 11 | 12 | gulp.task('clean', () => del(['./dist/**/*'])); 13 | 14 | 15 | gulp.task('scss', () => { 16 | return gulp.src('./src/**/*.scss') 17 | .pipe(sass().on('error', sass.logError)) 18 | .pipe(rename({ 19 | extname: ".wxss" 20 | })) 21 | .pipe(gulp.dest('./dist')) 22 | }) 23 | 24 | gulp.task('copy', () => { 25 | return gulp.src(['./src/**/*', '!./src/**/*.scss']) 26 | .pipe(gulp.dest('./dist')) 27 | }) 28 | 29 | function isScss(file) { 30 | // 判断文件的扩展名是否是 '.scss' 31 | return file.extname === '.scss'; 32 | } 33 | 34 | gulp.task('css-module', () => { 35 | return gulp.src('./src/**/*') 36 | .pipe(gulpif(isScss, sass())) 37 | .pipe(sort(wcmSortFn)) 38 | .pipe(weappCssModule()) 39 | .pipe(gulp.dest('./dist')) 40 | }) 41 | 42 | const originBuildSeries = [ 43 | 'clean', 44 | 'scss', 45 | 'copy' 46 | ] 47 | 48 | const moduleBuildSeries = [ 49 | 'clean', 50 | 'css-module' 51 | ] 52 | 53 | gulp.task('default', gulp.series(originBuildSeries)) 54 | 55 | gulp.task('build:module', gulp.series(moduleBuildSeries)) -------------------------------------------------------------------------------- /demo/gulp-project-demo/src/project.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "项目配置文件", 3 | "packOptions": { 4 | "ignore": [] 5 | }, 6 | "setting": { 7 | "urlCheck": true, 8 | "scopeDataCheck": false, 9 | "coverView": true, 10 | "es6": true, 11 | "postcss": true, 12 | "compileHotReLoad": false, 13 | "preloadBackgroundData": false, 14 | "minified": true, 15 | "autoAudits": false, 16 | "newFeature": true, 17 | "uglifyFileName": false, 18 | "uploadWithSourceMap": true, 19 | "useIsolateContext": true, 20 | "nodeModules": false, 21 | "enhance": false, 22 | "useCompilerModule": false, 23 | "userConfirmedUseCompilerModuleSwitch": false, 24 | "showShadowRootInWxmlPanel": true, 25 | "checkInvalidKey": true, 26 | "checkSiteMap": true, 27 | "babelSetting": { 28 | "ignore": [], 29 | "disablePlugins": [], 30 | "outputPath": "" 31 | } 32 | }, 33 | "compileType": "miniprogram", 34 | "libVersion": "2.12.1", 35 | "appid": "", 36 | "projectname": "gulp-project-demo", 37 | "debugOptions": { 38 | "hidedInDevtools": [] 39 | }, 40 | "isGameTourist": false, 41 | "simulatorType": "wechat", 42 | "simulatorPluginLibVersion": {}, 43 | "condition": { 44 | "search": { 45 | "current": -1, 46 | "list": [] 47 | }, 48 | "conversation": { 49 | "current": -1, 50 | "list": [] 51 | }, 52 | "game": { 53 | "currentL": -1, 54 | "list": [] 55 | }, 56 | "miniprogram": { 57 | "current": -1, 58 | "list": [] 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /lib/gulp/postcss-deal.js: -------------------------------------------------------------------------------- 1 | 2 | const modules = require('postcss-modules'); 3 | const postcss = require('gulp-postcss'); 4 | const fs = require('fs-extra') 5 | const colors = require('colors'); 6 | const path = require('path') 7 | 8 | const { generateSimpleScopedName } = require('./utils') 9 | 10 | function getJSONFromCssModules(cssFileName, json) { 11 | 12 | fs.writeFileSync(path.join(path.dirname(cssFileName), 'index.module.map.js'), 'module.exports=' + JSON.stringify(json)); 13 | } 14 | 15 | const wrapper = postcss([ 16 | modules({ 17 | getJSON: getJSONFromCssModules, 18 | generateScopedName: generateSimpleScopedName 19 | }), 20 | ]) 21 | 22 | module.exports = (file, _, cb) => { 23 | 24 | console.log(colors.green('postcss-modules预处理:', file.relative)) 25 | 26 | if (/\.module(\.css$|\.wxss$)/g.test(file.relative)) { // 只处理module标示的部分 27 | 28 | wrapper._transform(file, _, (err, file) => { 29 | const toDel = path.join(path.dirname(file.path), 'index.module.map.js') 30 | file.path = file.path.replace(/\.module(\.css$|\.wxss$)/, '.wxss') 31 | 32 | cb(err, file, toDel) 33 | }); 34 | return; 35 | 36 | } else if (/\.js$/g.test(file.relative)) { // 处理引入的路径变更 37 | const content = file.contents.toString(); 38 | file.contents = Buffer.from(content.replace(/\.\/index\.module(\.scss|\.wxss|\.less|\.styl)/, './index.module.map')) 39 | } 40 | cb(null, file); 41 | } -------------------------------------------------------------------------------- /lib/gulp/himalayaStringifyChanged/stringify.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.formatAttributes = formatAttributes; 7 | exports.toHTML = toHTML; 8 | 9 | var _compat = require('./compat'); 10 | 11 | function formatAttributes(attributes) { 12 | return attributes.reduce(function (attrs, attribute) { 13 | var key = attribute.key, 14 | value = attribute.value; 15 | 16 | if (value === null) { 17 | return attrs + ' ' + key; 18 | } 19 | var quoteEscape = value.indexOf('"') !== -1; 20 | var quote = quoteEscape ? '\'' : '"'; 21 | return attrs + ' ' + key + '=' + quote + value + quote; 22 | }, ''); 23 | } 24 | 25 | function toHTML(tree, options) { 26 | return tree.map(function (node) { 27 | if (node.type === 'text') { 28 | return node.content; 29 | } 30 | if (node.type === 'comment') { 31 | return ''; 32 | } 33 | var tagName = node.tagName, 34 | attributes = node.attributes, 35 | children = node.children; 36 | 37 | var isSelfClosing = (0, _compat.arrayIncludes)(options.voidTags, tagName.toLowerCase()); 38 | return isSelfClosing ? '<' + tagName + formatAttributes(attributes) + '>' : '<' + tagName + formatAttributes(attributes) + '>' + toHTML(children, options) + ''; 39 | }).join(''); 40 | } 41 | 42 | exports.default = { toHTML: toHTML }; 43 | //# sourceMappingURL=stringify.js.map 44 | -------------------------------------------------------------------------------- /lib/gulp/index.js: -------------------------------------------------------------------------------- 1 | 2 | const through2 = require('through2') 3 | const postcssDeal = require('./postcss-deal'); 4 | const replaceJs = require('./replace-js') 5 | const replaceWxml = require('./replace-wxml') 6 | const removeMap = require('./remove-map') 7 | const { checkCssFileExistsSync, wcmSortFn, generateSimpleScopedName } = require('./utils') 8 | 9 | 10 | // 主逻辑 11 | function deal(cb, _, error, file, toDelItem) { 12 | if (toDelItem) { 13 | cacheList.push(toDelItem); // 待删除的文件 14 | } 15 | 16 | if (/\.js$/g.test(file.relative)) { 17 | replaceJs(file, _, cb) 18 | return; 19 | 20 | } else if (/\.wxml$/g.test(file.relative)) { 21 | replaceWxml(file, _, cb) 22 | return; 23 | } 24 | 25 | cb(error, file) 26 | } 27 | 28 | const cacheList = [] 29 | 30 | const weappCssModule = ({ needCssModuleTransform = true } = {}) => { 31 | 32 | return through2.obj((file, _, cb) => { 33 | // 只对文件夹内包含index.module.样式文件的进行处理 34 | if (checkCssFileExistsSync(file.dirname)) { 35 | if (/(\.js)$|(\.css)$|(\.wxss)$/g.test(file.relative) && needCssModuleTransform) { 36 | postcssDeal(file, _, (...param) => deal(cb, _, ...param)) // 实现css-module的初始逻辑,回调进入主逻辑 37 | } else { 38 | deal(cb, _, null, file) // deal是主逻辑部分 39 | } 40 | return; 41 | } 42 | 43 | cb(null, file) 44 | }, (cb) => { 45 | removeMap(cacheList) 46 | cb() 47 | }) 48 | } 49 | 50 | module.exports = { 51 | weappCssModule, 52 | wcmSortFn, 53 | generateSimpleScopedName 54 | } 55 | 56 | -------------------------------------------------------------------------------- /lib/gulp/himalayaStringifyChanged/compat.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.startsWith = startsWith; 7 | exports.endsWith = endsWith; 8 | exports.stringIncludes = stringIncludes; 9 | exports.isRealNaN = isRealNaN; 10 | exports.arrayIncludes = arrayIncludes; 11 | /* 12 | We don't want to include babel-polyfill in our project. 13 | - Library authors should be using babel-runtime for non-global polyfilling 14 | - Adding babel-polyfill/-runtime increases bundle size significantly 15 | 16 | We will include our polyfill instance methods as regular functions. 17 | */ 18 | 19 | function startsWith(str, searchString, position) { 20 | return str.substr(position || 0, searchString.length) === searchString; 21 | } 22 | 23 | function endsWith(str, searchString, position) { 24 | var index = (position || str.length) - searchString.length; 25 | var lastIndex = str.lastIndexOf(searchString, index); 26 | return lastIndex !== -1 && lastIndex === index; 27 | } 28 | 29 | function stringIncludes(str, searchString, position) { 30 | return str.indexOf(searchString, position || 0) !== -1; 31 | } 32 | 33 | function isRealNaN(x) { 34 | return typeof x === 'number' && isNaN(x); 35 | } 36 | 37 | function arrayIncludes(array, searchElement, position) { 38 | var len = array.length; 39 | if (len === 0) return false; 40 | 41 | var lookupIndex = position | 0; 42 | var isNaNElement = isRealNaN(searchElement); 43 | var searchIndex = lookupIndex >= 0 ? lookupIndex : len + lookupIndex; 44 | while (searchIndex < len) { 45 | var element = array[searchIndex++]; 46 | if (element === searchElement) return true; 47 | if (isNaNElement && isRealNaN(element)) return true; 48 | } 49 | 50 | return false; 51 | } 52 | //# sourceMappingURL=compat.js.map 53 | -------------------------------------------------------------------------------- /lib/gulp/replace-wxml.js: -------------------------------------------------------------------------------- 1 | const himalayaWxml = require("himalaya-wxml"); 2 | const himalayaWalk = require('himalaya-walk'); 3 | const colors = require('colors'); 4 | const babelCore = require('@babel/core') 5 | const generate = require("babel-generator").default; 6 | const t = require('babel-types') 7 | const himalayaStringify = require("./himalayaStringifyChanged/stringify"); 8 | const { getConfigMap } = require('./utils') 9 | 10 | const reg = /[{ ](([\w_-]*)[.[]['"]?([\w_-]*)['"]?\]?)[} )]/g; 11 | 12 | function transverse(obj, array) { 13 | if (t.isBinaryExpression(obj)) { 14 | transverse(obj.left, array); 15 | transverse(obj.right, array); 16 | } else { 17 | if (t.isStringLiteral(obj)) { 18 | obj.value.trim() && array.push(obj.value) 19 | } else { 20 | array.push("{{" + generate(obj).code + "}}") 21 | } 22 | } 23 | } 24 | 25 | module.exports = (file, _, cb) => { 26 | let needReplace = false; 27 | const styleMap = getConfigMap(file.relative) 28 | 29 | 30 | console.log(colors.green('wxml替换开始:', file.relative)) 31 | 32 | const code = himalayaWxml.parse(file.contents.toString()) 33 | himalayaWalk(code, node => { 34 | if (node.attributes) { 35 | node.attributes.forEach(attribute => { 36 | if (attribute.key === 'class' && attribute.value) { 37 | let result = attribute.value; 38 | 39 | attribute.value.replace(reg, (match, $1, $2, $3) => { 40 | // 形如 {{ style.xx + '' + style.yy }} 41 | if (styleMap[$2]) {// 有对应的map值 42 | needReplace = true; 43 | if (styleMap[$2][$3]) { 44 | result = result.replace($1, "'" + styleMap[$2][$3] + "'"); 45 | } else { 46 | console.log(colors.red('wxml替换发现未匹配cssMap的类名:', $3, file.relative)) 47 | result = result.replace($1, "'" + $3 + "'"); 48 | } 49 | } 50 | }) 51 | if (needReplace && /{{.+\}\}/.test(result)) { 52 | const value = babelCore.parse(result).program.body[0].body[0].body[0].expression; // {{}}需要取2层 53 | const array = []; 54 | transverse(value, array); 55 | attribute.value = array.join(' '); 56 | } 57 | } 58 | }) 59 | } 60 | }) 61 | if (needReplace) { 62 | console.log(colors.green('wxml替换完成:', file.relative)) 63 | const ele = himalayaStringify.toHTML(code, himalayaWxml.parseDefaults); 64 | file.contents = Buffer.from(ele); 65 | } 66 | cb(null, file); 67 | } 68 | -------------------------------------------------------------------------------- /lib/gulp/utils.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const path = require('path'); 3 | 4 | const TEMP_PATH = path.resolve(__dirname, './temp.json') // css-map的缓存文件 5 | 6 | function getConfigMap(filenameRelative) { 7 | let data; 8 | // 读取配置文件,判断本文件是否有相应map 9 | try { 10 | data = fs.readFileSync(TEMP_PATH); 11 | } catch (e) { 12 | data = '{}' 13 | } 14 | 15 | const cssMap = JSON.parse(data); 16 | const fileName = filenameRelative.replace(/\.(wxml|wxss)/, '.js') 17 | if (cssMap[fileName]) { 18 | data = getMapByList(cssMap[fileName]) 19 | if (filenameRelative.indexOf('.wxss') > -1) { 20 | return data.cssMap 21 | } else { 22 | return data.styleMap 23 | } 24 | } 25 | return {}; 26 | } 27 | 28 | 29 | function getMapByList(list) { 30 | let styleMap = {}; 31 | let cssMap = {}; 32 | list.forEach((ele, index) => { 33 | let data = getMapFromLocation(ele.source, index) 34 | if (data) { 35 | styleMap[ele.name] = data.styleMap; 36 | cssMap = { ...cssMap, ...data.cssMap } // cssMap是个多map合并的对象,暂不考虑类名重叠的问题 37 | } 38 | }) 39 | return { styleMap, cssMap } 40 | } 41 | 42 | const cacheMap = {} 43 | 44 | function getMapFromLocation(location) { 45 | 46 | if (!cacheMap[location]) { 47 | const data = require(location); 48 | let styleMap = {}; 49 | // Object.keys(data).forEach((name, index) => { 50 | // const shortName = getShortName(index, order) 51 | // // styleMap[name] = shortName; // name : a; 52 | // // cssMap[data[name]] = shortName; // hash : a; 53 | // }) 54 | styleMap = data; 55 | cacheMap[location] = { 56 | styleMap, 57 | // cssMap 58 | } 59 | 60 | } 61 | return cacheMap[location] 62 | } 63 | 64 | const str = 'abcdefghijklmnopqrstuvwxyz' 65 | function getShortName(index, order = 0) { 66 | const arr = parseInt(index, 10).toString(26).split(''); 67 | let keys = arr.map(item => str[parseInt(item, 26)]).join('') 68 | if (order !== 0) { 69 | keys += order 70 | } 71 | return keys; 72 | } 73 | function checkCssFileExistsSync(filepath) { 74 | let flag = false; 75 | ['scss', 'css', 'less', 'styl', 'wxss'].forEach(item => { 76 | try { 77 | fs.accessSync(path.join(filepath, 'index.module.' + item), fs.constants.F_OK); 78 | flag = true; 79 | } catch (e) { } // eslint-disable-line 80 | }) 81 | 82 | return flag; 83 | } 84 | 85 | function getWeight(path) { 86 | if (/(\.wxss)$|(\.scss)$|(\.css)$/g.test(path)) { 87 | return 3; 88 | } else if (/(\.js)$/g.test(path)) { 89 | return 2; 90 | } else if (/(\.wxml)$/g.test(path)) { 91 | return 1; 92 | } 93 | } 94 | 95 | function wcmSortFn(a, b) { 96 | const aValue = getWeight(a.relative); 97 | const bValue = getWeight(b.relative); 98 | return bValue - aValue 99 | } 100 | 101 | let map = {} 102 | function generateSimpleScopedName(name, filename) { 103 | if (undefined === map[filename]) { 104 | map[filename] = { 105 | index: 0, 106 | list: { 107 | [name]: getShortName(0) 108 | } 109 | }; 110 | } else if (!map[filename].list[name]) { 111 | let shortName = getShortName(++map[filename].index); 112 | map[filename].list[name] = shortName 113 | } 114 | 115 | return map[filename].list[name]; 116 | } 117 | 118 | module.exports = { 119 | getConfigMap, 120 | getMapByList, 121 | getShortName, 122 | checkCssFileExistsSync, 123 | generateSimpleScopedName, 124 | wcmSortFn, 125 | TEMP_PATH 126 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # weapp-css-modules 2 | 3 | 小程序的简化版 css-modules,比标准 [css-modules](https://github.com/css-modules/css-modules) 代码量更少的优化方案 4 | 5 | ## 介绍 6 | 7 | css-modules 是一种 css 模块化方案,它在构建过程中生成一个原类名与新类名的 map,根据 map引用样式,通过设定 hash 规则,实现了对 CSS 类名作用域的限定,它通常用来解决页面类名冲突的问题。由于微信小程序内组件样式默认隔离,为什么要使用 css-modules 呢? 8 | 9 | 有以下2个原因: 10 | 11 | - hash 化后可以实现更短的命名,减少代码包体积 12 | - 跨端项目需要兼顾非小程序环境,避免样式冲突 13 | 14 | weapp-css-modules 做了哪些事? 15 | 16 | - 新类名单字母编排,减少代码量 17 | - 移除类名映射 map,替换 js 和 wxml 中变量为编译后类名 18 | 19 | 标准 css-modules 方案: 20 | 21 | ``` 22 | import style from './index.wxss' 23 | 24 | .index_banner_xkpkl { xx } 25 | module.exports ={'banner' : 'index_banner_xkpkl'} // 额外生成的 map 文件 26 | ``` 27 | weapp-css-modules 编译后效果: 28 | ``` 29 | let style = {} 30 | 31 | .a { xx } 32 | ``` 33 | 34 | ## 安装 35 | 目前只开发了适用于使用 gulp 编译小程序的 gulp 插件,后续计划开发 webpack 可用的插件实现相同功能 36 | 37 | ``` 38 | npm i gulp-weapp-css-modules gulp-sort 39 | ``` 40 | 41 | ``` 42 | // gulpfile.js 43 | const { weappCssModule, wcmSortFn } = require('gulp-weapp-css-modules') 44 | const sort = require('gulp-sort'); 45 | 46 | gulp.task('css-module', () => { 47 | return gulp.src('./src/**/*') 48 | .pipe(sort(wcmSortFn)) // 由于处理文件有顺序依赖,需要先对文件排序 49 | .pipe(weappCssModule()) 50 | .pipe(gulp.dest('./dist')) 51 | }) 52 | ``` 53 | 54 | ## 使用 55 | 56 | 小程序页面不具备隔离功能,因此只有具备样式隔离的 Component 可以改造使用 weapp-css-modules 57 | 58 | 1. css 文件改名字: weapp-css-modules 通过 css 文件是否带 module 来识别需要替换的内容 59 | 60 | `index.wxss` -> `index.module.wxss` 61 | 62 | // 或者使用scss/其他 63 | 64 | `index.scss` -> `index.module.scss` 65 | 66 | 2. js 内新增样式文件的引入,目的是建立 css-modules 的样式与 js 关系 67 | ``` 68 | import styles from './index.module.wxss 69 | 70 | data:{ 71 | ..., 72 | styles:styles 73 | } 74 | 75 | ``` 76 | 77 | 3. 修改 js 内类名的地方替换为 styles 的间接引入 78 | ``` 79 | query.select('.banner') 80 | .boundingClientRect() 81 | .exec(function (res) {...}) 82 | 83 | // 改为 84 | query.select('.' + styles['banner']) 85 | .boundingClientRect() 86 | .exec(function (res) {...}) 87 | 88 | ``` 89 | 90 | 4. 修改 wxml 内类名的使用 91 | 92 | 4.1. 普通类名 93 | ``` 94 | 95 | // 改为 96 | 97 | // 或者 98 | 99 | ``` 100 | 4.2. 三目运算符 101 | ``` 102 | 103 | 104 | // 改为 105 | 106 | // 或者 107 | 108 | ``` 109 | 110 | 这里需要注意几种有问题的写法: 111 | 112 | 4.2.1. 类名间未加空格 113 | 114 | ``` 115 | 116 | ``` 117 | 4.2.2. 三目表达式未加括号,运算优先级不明 118 | 119 | ``` 120 | 121 | ``` 122 | 4.2.3. styles 的属性需要是具体的字符串,不能使用变量表达式(这是 weapp-css-modules 需要单独关注的地方,因为编译阶段会对 styles.xx 进行求值,所以不能把表达式写在属性位置) 123 | ``` 124 | 125 | ``` 126 | 5. 构建过程中关注脚本的红色提示,类似于这种: 127 | ![image-20201026142241488](https://img11.360buyimg.com/ling/jfs/t1/154791/21/3584/20989/5f9675e1E66063a2a/ec36b4326d933405.png) 128 | 129 | 这是由于在 js/wxml 内使用了一个`banner__swiper_2`,而 css 内并没有定义`banner__swiper_2`,css-module 编译的 map 文件是根据 css 内的样式定义来生成 key 名的,因此`styles['banner__swiper_2']`是`undefined`, 针对这种情况有两种处理方式: 130 | 131 | 5.1. 如果 js 内需要通过这个类名选择到某个元素,但是 css 内不需要编写样式,那么可以将它视为不需要编译的类名,即: 132 | ``` 133 | query.selector('.banner__swiper_2') // 不改成 styles.xx 的写法 134 | // 相应的元素也不索引到 styles 135 | // 这样实现了一个组件内不会被编译的样式 136 | ``` 137 | 5.2. 如果 js 内无引用,那么删掉 wxml 内该类名的定义吧~ 138 | 139 | 6. 构建完进行检查,关注样式和交互是否正常 140 | 141 | ## 参考示例 142 | 143 | - gulp项目:路径 `/demo/gulp-project-demo` 144 | 145 | ## 联系反馈 146 | 147 | * 欢迎通过邮箱来跟我联系: smile123ing@163.com 148 | * 欢迎通过 [GitHub issue](https://github.com/o2team/weapp-css-modules/issues) 提交 BUG、以及其他问题 149 | * 欢迎给该项目点个赞 ⭐️ [star on GitHub](https://github.com/o2team/weapp-css-modules) ! -------------------------------------------------------------------------------- /lib/gulp/replace-js.js: -------------------------------------------------------------------------------- 1 | const { get } = require('lodash') 2 | const colors = require('colors'); 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const t = require('babel-types') 6 | const babelCore = require('@babel/core') 7 | 8 | const { TEMP_PATH, getMapByList } = require('./utils') 9 | 10 | module.exports = (file, _, cb) => { 11 | 12 | const fileOpts = Object.assign({}, { 13 | plugins: [ 14 | '@babel/plugin-syntax-jsx', 15 | ["@babel/plugin-proposal-decorators", { "legacy": true }], 16 | 'transform-class-properties', 17 | '@babel/plugin-proposal-object-rest-spread', 18 | replaceJs 19 | ] 20 | }, { 21 | filename: file.path, 22 | filenameRelative: file.relative, 23 | sourceMap: Boolean(file.sourceMap), 24 | sourceFileName: file.relative, 25 | caller: { name: 'babel-gulp' } 26 | }); 27 | 28 | babelCore.transformAsync(file.contents.toString(), fileOpts).then(res => { 29 | if (res) { 30 | file.contents = Buffer.from(res.code); 31 | } 32 | }).catch(err => { 33 | this.emit('error', err); 34 | }).then( 35 | () => cb(null, file), 36 | () => cb(null, file) 37 | ); 38 | } 39 | 40 | const nestedVisitor = { 41 | ImportDeclaration(_path) { 42 | if (/\.module\.map/g.test(get(_path, 'node.source.value', ''))) { // 通过这种方式识别出样式文件的引入 43 | const name = get(_path, 'node.specifiers[0].local.name'); 44 | this.list.push({ 45 | source: path.join(path.dirname(this.state.filename), _path.node.source.value), 46 | name: name, 47 | }) 48 | _path.insertAfter(t.variableDeclaration('let', [t.variableDeclarator(t.identifier(name), t.objectExpression([]))]));// 新增一句let style ={}; 用以容错未使用的一些style 49 | _path.remove() 50 | } 51 | } 52 | } 53 | 54 | function replaceJs({ types: t }) { 55 | return { 56 | pre() { 57 | this.list = []; 58 | this.styleList = []; 59 | this.styleMap = {} 60 | }, 61 | 62 | visitor: { 63 | Program(path, state) { 64 | path.traverse(nestedVisitor, { state, list: this.list }); 65 | if (this.list.length) { 66 | console.log(colors.green('js替换开始:', state.file.opts.filenameRelative)) 67 | this.styleMap = getMapByList(this.list).styleMap 68 | this.styleList = Object.keys(this.styleMap); 69 | } 70 | }, 71 | Identifier(_path, state) { // 标识符替换 72 | if (_path.isReferencedIdentifier()) { 73 | if (this.styleList.includes(_path.node.name)) { // 有对应的styleName 74 | const styleName = _path.node.name; 75 | if (t.isMemberExpression(_path.parent)) { // 有使用对应属性 76 | let name; 77 | 78 | if (t.isIdentifier(_path.parent.property)) { // 形如 style.xx 79 | name = _path.parent.property.name; 80 | } else if (t.isStringLiteral(_path.parent.property)) { // 形如 style[xx] 81 | name = _path.parent.property.value 82 | } else { 83 | console.warn('未知的属性', _path.parent) 84 | } 85 | 86 | if (this.styleMap[styleName][name]) {// 有对应的map值 87 | _path.parentPath.replaceWith(t.StringLiteral(this.styleMap[styleName][name])) 88 | 89 | } else if (this.styleMap[styleName]) { 90 | console.log(colors.red('js替换发现未匹配cssMap的类名:', name, state.opts.filenameRelative)) 91 | _path.parentPath.replaceWith(t.StringLiteral(name)) 92 | } 93 | } else { 94 | _path.replaceWith(t.ObjectExpression([])) // 替换为空表达式 95 | } 96 | } 97 | 98 | } 99 | } 100 | 101 | }, 102 | post(state) { 103 | if (this.list.length) { 104 | const { filenameRelative } = state.opts; 105 | console.log(colors.green('js替换完成,生成css配置:', filenameRelative)) 106 | 107 | let fileMap; 108 | try { 109 | fileMap = JSON.parse(fs.readFileSync(TEMP_PATH, 'utf-8')) 110 | } catch (e) { 111 | fileMap = {} 112 | } 113 | const temp = {}; 114 | temp[filenameRelative] = this.list; 115 | 116 | fs.writeFileSync(TEMP_PATH, JSON.stringify({ 117 | ...fileMap, 118 | ...temp 119 | })) 120 | } 121 | } 122 | } 123 | } --------------------------------------------------------------------------------