├── docs ├── .nojekyll ├── CNAME ├── _navbar.md ├── _media │ ├── cover-image.jpg │ ├── wechat-qrcode.jpg │ └── custom.css ├── _coverpage.md └── index.html ├── ARCHITECTURE.md ├── packages ├── cli │ ├── templates │ │ ├── generators │ │ │ ├── page │ │ │ │ ├── page.wxss │ │ │ │ ├── page.json │ │ │ │ ├── page.wxml │ │ │ │ └── page.js │ │ │ ├── component │ │ │ │ ├── component.wxss │ │ │ │ ├── component.wxml │ │ │ │ ├── component.json │ │ │ │ └── component.js │ │ │ └── template │ │ │ │ ├── template.wxss │ │ │ │ └── template.wxml │ │ └── wechat-app │ │ │ ├── .ewa │ │ │ └── .keep │ │ │ ├── src │ │ │ ├── templates │ │ │ │ └── .keep │ │ │ ├── components │ │ │ │ └── .keep │ │ │ ├── pages │ │ │ │ ├── logs │ │ │ │ │ ├── logs.json │ │ │ │ │ ├── logs.wxss │ │ │ │ │ ├── logs.wxml │ │ │ │ │ └── logs.js │ │ │ │ └── index │ │ │ │ │ ├── index.wxss │ │ │ │ │ ├── index.wxml │ │ │ │ │ └── index.js │ │ │ ├── project.alipay.json │ │ │ ├── app.wxss │ │ │ ├── app.json │ │ │ ├── project.tt.json │ │ │ ├── utils │ │ │ │ └── util.js │ │ │ ├── project.swan.json │ │ │ ├── project.config.json │ │ │ ├── project.qq.json │ │ │ └── app.js │ │ │ ├── package.json │ │ │ ├── .gitignore │ │ │ ├── gitignore │ │ │ ├── ewa.config.js │ │ │ └── .eslintrc.js │ ├── README.md │ ├── lib │ │ ├── commands │ │ │ ├── upgrade.js │ │ │ ├── clean.js │ │ │ ├── start.js │ │ │ ├── build.js │ │ │ ├── create.js │ │ │ ├── generate.js │ │ │ ├── install.js │ │ │ └── init.js │ │ ├── cli.js │ │ └── utils.js │ ├── package.json │ └── CHANGELOG.md ├── webpack │ ├── tsconfig.json │ ├── lib │ │ ├── loaders │ │ │ ├── fix-regenerator-loader.js │ │ │ ├── wxs-transform-loader.js │ │ │ ├── wxml-transform-loader.js │ │ │ ├── fix-import-wxss-loader.js │ │ │ ├── js-transform-loader.js │ │ │ ├── json-transform-loader.js │ │ │ ├── fix-unary-element-loader.js │ │ │ └── import-wxss-loader.js │ │ ├── run.js │ │ ├── rules │ │ │ ├── image.js │ │ │ ├── wxs.js │ │ │ ├── js.js │ │ │ ├── json.js │ │ │ ├── ts.js │ │ │ ├── wxml.js │ │ │ └── css.js │ │ ├── parsers │ │ │ ├── wxssParser.js │ │ │ ├── jsParser.js │ │ │ ├── jsonParser.js │ │ │ ├── alipayJsonParser.js │ │ │ └── wxmlParser.js │ │ ├── run.prod.js │ │ ├── utils │ │ │ ├── babelConfig.js │ │ │ └── index.js │ │ ├── plugins │ │ │ ├── EnsureVendorsExistancePlugin.js │ │ │ ├── AutoCleanUnusedFilesPlugin.js │ │ │ └── NodeCommonModuleTemplatePlugin.js │ │ ├── run.dev.js │ │ └── config.js │ ├── CHANGELOG.md │ └── package.json └── ewa │ ├── src │ ├── utils │ │ ├── buildArgs.js │ │ └── Queue.js │ ├── ewa.js │ ├── mixins │ │ └── mixin.js │ ├── polyfills │ │ ├── alipayStorage.js │ │ ├── alipaySelectorQuery.js │ │ └── alipayComponent.js │ └── plugins │ │ ├── createStore │ │ ├── reactive.js │ │ ├── index.js │ │ ├── Watcher.js │ │ ├── injectStore.js │ │ └── Observer.js │ │ ├── apiPromisify.js │ │ └── enableState.js │ ├── .babelrc │ ├── CHANGELOG.md │ ├── package.json │ ├── .eslintrc.json │ └── README.md ├── TODOS.md ├── lerna.json ├── package.json ├── LICENSE ├── .eslintrc.js ├── .gitignore ├── CHANGELOG.md └── README.md /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ARCHITECTURE.md: -------------------------------------------------------------------------------- 1 | 待完善 ~ 2 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | ewa.js.org 2 | -------------------------------------------------------------------------------- /docs/_navbar.md: -------------------------------------------------------------------------------- 1 | - [指南](#main) 2 | -------------------------------------------------------------------------------- /packages/cli/templates/generators/page/page.wxss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/cli/templates/wechat-app/.ewa/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/cli/templates/wechat-app/src/templates/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/cli/templates/generators/component/component.wxss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/cli/templates/generators/page/page.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /packages/cli/templates/generators/template/template.wxss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/cli/templates/wechat-app/src/components/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/cli/templates/generators/page/page.wxml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/cli/templates/generators/component/component.wxml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/cli/templates/generators/component/component.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": true 3 | } 4 | -------------------------------------------------------------------------------- /packages/cli/templates/generators/template/template.wxml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /docs/_media/cover-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/ewa/master/docs/_media/cover-image.jpg -------------------------------------------------------------------------------- /docs/_media/wechat-qrcode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icai/ewa/master/docs/_media/wechat-qrcode.jpg -------------------------------------------------------------------------------- /packages/cli/README.md: -------------------------------------------------------------------------------- 1 | EWA 命令行工具 2 | ============ 3 | 4 | 参见 [项目文档](https://github.com/lyfeyaj/ewa) 5 | -------------------------------------------------------------------------------- /packages/cli/templates/wechat-app/src/pages/logs/logs.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationBarTitleText": "查看启动日志" 3 | } -------------------------------------------------------------------------------- /packages/cli/templates/generators/page/page.js: -------------------------------------------------------------------------------- 1 | Page({ 2 | data: {}, 3 | onLoad(options) {}, 4 | onShow() {} 5 | }); 6 | -------------------------------------------------------------------------------- /packages/cli/templates/generators/component/component.js: -------------------------------------------------------------------------------- 1 | Component({ 2 | properties: {}, 3 | data: {}, 4 | attached() {}, 5 | methods: {} 6 | }); 7 | -------------------------------------------------------------------------------- /packages/cli/templates/wechat-app/src/project.alipay.json: -------------------------------------------------------------------------------- 1 | { 2 | "axmlStrictCheck": false, 3 | "enableParallelLoader": false, 4 | "component2": true 5 | } -------------------------------------------------------------------------------- /packages/webpack/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "experimentalDecorators": true, 6 | "allowJs": true 7 | } 8 | } -------------------------------------------------------------------------------- /packages/cli/templates/wechat-app/src/pages/logs/logs.wxss: -------------------------------------------------------------------------------- 1 | .log-list { 2 | display: flex; 3 | flex-direction: column; 4 | padding: 40rpx; 5 | } 6 | .log-item { 7 | margin: 10rpx; 8 | } 9 | -------------------------------------------------------------------------------- /packages/ewa/src/utils/buildArgs.js: -------------------------------------------------------------------------------- 1 | module.exports = function buildArgs() { 2 | let args = [], len = arguments.length; 3 | while (len--) args[len] = arguments[len]; 4 | return args; 5 | }; 6 | -------------------------------------------------------------------------------- /packages/cli/templates/wechat-app/src/pages/logs/logs.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{index + 1}}. {{log}} 5 | 6 | 7 | -------------------------------------------------------------------------------- /packages/cli/templates/wechat-app/src/app.wxss: -------------------------------------------------------------------------------- 1 | .container { 2 | height: 100%; 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | justify-content: space-between; 7 | padding: 200rpx 0; 8 | box-sizing: border-box; 9 | } 10 | -------------------------------------------------------------------------------- /packages/ewa/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", {"targets": {"node": "0.10"}}] 4 | ], 5 | "plugins": [ 6 | [ 7 | "add-module-exports", 8 | { 9 | "addDefaultProperty": true 10 | } 11 | ] 12 | ] 13 | } -------------------------------------------------------------------------------- /docs/_coverpage.md: -------------------------------------------------------------------------------- 1 | # EWA 2 | 3 | 微信小程序增强开发工具 4 | 5 | [ GitHub](https://github.com/lyfeyaj/ewa) 6 | [马上开始 ](#main) 7 | 8 | 9 | ![](https://raw.githubusercontent.com/lyfeyaj/ewa/master/docs/_media/cover-image.jpg) 10 | -------------------------------------------------------------------------------- /packages/cli/templates/wechat-app/src/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages":[ 3 | "pages/index/index", 4 | "pages/logs/logs" 5 | ], 6 | "window":{ 7 | "backgroundTextStyle":"light", 8 | "navigationBarBackgroundColor": "#fff", 9 | "navigationBarTitleText": "WeChat", 10 | "navigationBarTextStyle":"black" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/cli/templates/wechat-app/src/pages/logs/logs.js: -------------------------------------------------------------------------------- 1 | //logs.js 2 | const util = require('../../utils/util.js'); 3 | 4 | Page({ 5 | data: { 6 | logs: [] 7 | }, 8 | onLoad: function () { 9 | this.setData({ 10 | logs: (wx.getStorageSync('logs') || []).map(log => { 11 | return util.formatTime(new Date(log)); 12 | }) 13 | }); 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /packages/ewa/src/ewa.js: -------------------------------------------------------------------------------- 1 | const apiPromisify = require('./plugins/apiPromisify'); 2 | const enableState = require('./plugins/enableState'); 3 | const createStore = require('./plugins/createStore'); 4 | const mixin = require('./mixins/mixin'); 5 | 6 | const ewa = { 7 | mixin, 8 | enableState, 9 | createStore, 10 | }; 11 | 12 | apiPromisify(ewa); 13 | 14 | module.exports = ewa; 15 | -------------------------------------------------------------------------------- /packages/webpack/lib/loaders/fix-regenerator-loader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function fixRegeneratorLoader(content = '') { 4 | // 小程序环境不支持 Function, 支付宝会报错, 故这里删除这一行代码 5 | if (/Function\("r", ?"regeneratorRuntime = r"\)/g.test(content)) { 6 | return content.replace(/Function\("r", ?"regeneratorRuntime = r"\)/g, ''); 7 | } 8 | 9 | return content; 10 | }; 11 | -------------------------------------------------------------------------------- /packages/webpack/lib/run.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const NODE_ENV = process.env.NODE_ENV || 'development'; 4 | 5 | let webpackBin = require.resolve('webpack-cli'); 6 | 7 | // Support windows and *nix like os 8 | webpackBin = `node "${webpackBin}"`; 9 | 10 | if (NODE_ENV === 'development') { 11 | require('./run.dev')(webpackBin); 12 | } else { 13 | require('./run.prod')(webpackBin); 14 | } 15 | -------------------------------------------------------------------------------- /packages/cli/templates/wechat-app/src/project.tt.json: -------------------------------------------------------------------------------- 1 | { 2 | "setting": { 3 | "urlCheck": false, 4 | "es6": false, 5 | "postcss": false, 6 | "minified": false, 7 | "newFeature": false 8 | }, 9 | "appid": "testappId", 10 | "projectname": "bytedance mini app sample", 11 | "condition": { 12 | "miniprogram": { 13 | "current": -1, 14 | "list": [ 15 | ] 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /packages/webpack/lib/rules/image.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // 处理图片, 小程序不支持本地的背景图片, 这里采用 base64 编码的 datauri 4 | module.exports = function imageRule() { 5 | return { 6 | test: /\.(jpe?g|png|gif|ico|svg|webp|apng)$/i, 7 | use: [ 8 | { 9 | loader: 'url-loader', 10 | options: { 11 | // 16k 12 | limit: 8192 * 2, 13 | esModule: false 14 | } 15 | } 16 | ] 17 | }; 18 | }; -------------------------------------------------------------------------------- /packages/cli/lib/commands/upgrade.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const utils = require('../utils'); 4 | const execSync = require('child_process').execSync; 5 | 6 | // 升级 EWA 工具 7 | module.exports = function upgrade() { 8 | // 升级全局 ewa-cli 9 | utils.log('正在升级 EWA 工具...'); 10 | 11 | execSync('npm i ewa-cli@latest -g'); 12 | 13 | if (utils.isEwaProject()) { 14 | execSync('npm i ewa@latest'); 15 | } 16 | 17 | utils.log('升级完成!', 'success'); 18 | }; 19 | -------------------------------------------------------------------------------- /packages/webpack/lib/loaders/wxs-transform-loader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function wxsTransformLoader(content) { 4 | let { type } = this.query || {}; 5 | 6 | if (type === 'alipay') { 7 | // alipay中, sjs仅支持esmodule导出 8 | content = content.replace(/module\.exports\s*=/, 'export default'); 9 | } 10 | 11 | // 替换EWA_ENV 12 | content = content.replace(/process\.env\.EWA_ENV/g, `'${type}'`); 13 | 14 | return content; 15 | 16 | }; 17 | -------------------------------------------------------------------------------- /packages/cli/templates/wechat-app/src/pages/index/index.wxss: -------------------------------------------------------------------------------- 1 | /** index.wxss **/ 2 | /** 支持在 wxss 文件中直接使用 scss 或 less 语法 **/ 3 | 4 | .userinfo { 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | } 9 | 10 | .userinfo-avatar { 11 | width: 128rpx; 12 | height: 128rpx; 13 | margin: 20rpx; 14 | border-radius: 50%; 15 | } 16 | 17 | .userinfo-nickname { 18 | color: #aaa; 19 | } 20 | 21 | .usermotto { 22 | margin-top: 200px; 23 | } 24 | -------------------------------------------------------------------------------- /packages/webpack/lib/parsers/wxssParser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function wxssParser(content, type) { 4 | // 根据构建类型修改文件引入后缀名 5 | if (type === 'swan') content = content.replace(/\.wxss/g, '.css'); 6 | if (type === 'tt') content = content.replace(/\.wxss/g, '.ttss'); 7 | if (type === 'alipay') content = content.replace(/\.wxss/g, '.acss'); 8 | if (type === 'qq') content = content.replace(/\.wxss/g, '.qss'); 9 | 10 | return content; 11 | }; 12 | -------------------------------------------------------------------------------- /packages/cli/templates/wechat-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wechat_app_sample", 3 | "version": "1.0.0", 4 | "description": "Wechat App Sample", 5 | "scripts": { 6 | "clean": "ewa clean", 7 | "start": "ewa clean && ewa start", 8 | "build": "ewa clean && ewa build", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "license": "ISC", 12 | "dependencies": { 13 | "ewa": "^1.2.0" 14 | }, 15 | "devDependencies": {} 16 | } 17 | -------------------------------------------------------------------------------- /docs/_media/custom.css: -------------------------------------------------------------------------------- 1 | section.cover.has-mask .mask { 2 | background-image: linear-gradient(hsla(0, 0%, 100%, 0.25),hsla(0, 0%, 100%, 0.75)); 3 | background-color: transparent; 4 | opacity: 1; 5 | } 6 | 7 | section.cover h1 .anchor span{ 8 | font-family: 'Lobster', cursive; 9 | color: var(--theme-color); 10 | } 11 | 12 | section.cover .cover-main>p:last-child a .iconfont { 13 | font-size: 1em; 14 | } 15 | 16 | .sidebar>h1 a { 17 | font-family: 'Lobster', cursive; 18 | } 19 | -------------------------------------------------------------------------------- /packages/webpack/lib/loaders/wxml-transform-loader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const wxmlParser = require('../parsers/wxmlParser'); 5 | 6 | module.exports = function wxssTransformLoader(content) { 7 | let { type, ENTRY_DIR } = this.query || {}; 8 | 9 | let file = path.relative(ENTRY_DIR, this.resourcePath); 10 | 11 | // 多端支持 12 | if (type !== 'weapp') return wxmlParser(content, file, type); 13 | 14 | // 如果是 微信小程序,不做转换 15 | return content; 16 | 17 | }; 18 | -------------------------------------------------------------------------------- /packages/webpack/lib/run.prod.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint no-console: "off" */ 4 | 5 | const path = require('path'); 6 | const execSync = require('child_process').execSync; 7 | 8 | const configFile = path.resolve(__dirname, 'config.js'); 9 | 10 | module.exports = function(webpack) { 11 | let cmd = `${webpack} --config "${configFile}" --colors --display=errors-only` 12 | 13 | execSync(cmd, { 14 | env: process.env, 15 | stdio: ['pipe', process.stdout, process.stderr] 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /packages/webpack/lib/loaders/fix-import-wxss-loader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const wxssParser = require('../parsers/wxssParser'); 4 | 5 | function fixImportWxssLoader(content, map, meta) { 6 | let re = /(@import\s*)url\(([^;)]+)\)(\s*;)/gi; 7 | content = content.replace(re, '$1$2$3'); 8 | 9 | const { type } = this.query || {}; 10 | 11 | // 多端支持 12 | if (type !== 'weapp') content = wxssParser(content, type); 13 | 14 | this.callback(null, content, map, meta); 15 | } 16 | 17 | module.exports = fixImportWxssLoader; 18 | -------------------------------------------------------------------------------- /packages/webpack/lib/rules/wxs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 4 | 5 | // 处理 wxs 文件 6 | module.exports = function wxsRule(options) { 7 | return { 8 | test: /\.wxs$/i, 9 | use: ExtractTextPlugin.extract([ 10 | { 11 | loader: 'raw-loader', 12 | options: { esModule: false } 13 | }, 14 | { 15 | loader: './loaders/wxs-transform-loader', 16 | options: { type: options.EWA_ENV } 17 | } 18 | ]) 19 | }; 20 | }; -------------------------------------------------------------------------------- /packages/ewa/src/mixins/mixin.js: -------------------------------------------------------------------------------- 1 | const assign = require('lodash.assign'); 2 | const buildArgs = require('../utils/buildArgs'); 3 | 4 | module.exports = function mixin(...args) { 5 | let mixins = buildArgs(...args); 6 | 7 | let mixes = {}; 8 | let kls = mixins.pop(); 9 | 10 | for (let i = 0; i < mixins.length; i++) { 11 | let item = mixins[i]; 12 | mixes = assign({}, mixes, typeof item === 'function' ? item() : item); 13 | } 14 | 15 | let newKls = assign({ parent: mixes }, mixes, kls); 16 | 17 | return newKls; 18 | }; 19 | -------------------------------------------------------------------------------- /packages/webpack/lib/parsers/jsParser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function jsParser(content, type) { 4 | // NOTE: 替换为 swan / tt / alipay / qq,有些粗暴,需要优化 5 | // 强烈建议使用 ewa.api 来代替原生的接口调用对象 6 | // 考虑使用 babel 转换为语法树,然后替换,但需要消耗额外的转换性能 7 | if (type === 'swan') return content.replace(/wx\./gi, 'swan.'); 8 | if (type === 'tt') return content.replace(/wx\./gi, 'tt.'); 9 | if (type === 'alipay') return content.replace(/wx\./gi, 'my.'); 10 | if (type === 'qq') return content.replace(/wx\./gi, 'qq.'); 11 | 12 | return content; 13 | }; 14 | -------------------------------------------------------------------------------- /packages/cli/lib/commands/clean.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint no-console: "off" */ 4 | 5 | const fs = require('fs-extra'); 6 | const path = require('path'); 7 | const utils = require('../utils'); 8 | 9 | module.exports = function clean(type) { 10 | utils.ensureEwaProject(type); 11 | 12 | const ROOT = process.cwd(); 13 | 14 | const distDirName = utils.outputDirByType(type); 15 | const distDirPath = path.resolve(ROOT, distDirName); 16 | 17 | utils.log(`正在清理 ${distDirName} 目录... `); 18 | 19 | fs.emptyDirSync(distDirPath); 20 | 21 | utils.log('清理完成 !', 'success'); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/webpack/lib/rules/js.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // 解析 js 文件 6 | module.exports = function jsRule(options = {}) { 7 | return { 8 | test: /\.js$/, 9 | use: [{ 10 | loader: './loaders/js-transform-loader', 11 | options: { type: options.EWA_ENV } 12 | }, { 13 | loader: 'babel-loader', 14 | options: { 15 | cacheDirectory: true, 16 | 17 | // 指定 babel 配置文件 18 | configFile: path.resolve(__dirname, '../utils/babelConfig.js') 19 | } 20 | }], 21 | exclude: /node_modules/, 22 | }; 23 | }; -------------------------------------------------------------------------------- /packages/cli/templates/wechat-app/src/pages/index/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{userInfo.nickName}} 8 | 9 | 10 | 11 | {{motto}} 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/cli/templates/wechat-app/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 | -------------------------------------------------------------------------------- /packages/webpack/lib/rules/json.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 4 | 5 | // 处理 json 文件 6 | module.exports = function jsonRule(options = {}) { 7 | return { 8 | type: 'javascript/auto', 9 | test: /\.json$/i, 10 | use: ExtractTextPlugin.extract([ 11 | { 12 | loader: 'raw-loader', 13 | options: { esModule: false } 14 | }, 15 | { 16 | loader: './loaders/json-transform-loader', 17 | options: { type: options.EWA_ENV, ENTRY_DIR: options.ENTRY_DIR, GLOBAL_COMPONENTS: options.GLOBAL_COMPONENTS } 18 | } 19 | ]) 20 | }; 21 | }; -------------------------------------------------------------------------------- /TODOS.md: -------------------------------------------------------------------------------- 1 | #### 2021-05-31 2 | 3 | - [ ] 优化 css 代码压缩,支持 class 名称压缩 4 | 5 | #### 2021-05-21 6 | 7 | - [x] 插件 createStore 支持自定义方法或属性名称 8 | 9 | #### 2021-05-20 10 | 11 | - [ ] 抽象多端转换能力为不同的插件,便于维护 12 | - [ ] 支持代码行内条件编译,进一步强化多端支持 13 | - [ ] 优化生成产物,进一步降低大小占用 14 | - [ ] 优化运行插件的代码注入可能导致的污染问题,结合 mixin 15 | 16 | #### 2021-04-26 17 | 18 | - [ ] 支持生成 ts 的文件模板 19 | 20 | #### 2021-04-25 21 | 22 | - [x] 支持自定义环境变量 - v1.1.0 及以上已支持 23 | - [ ] 支持不同小程序的差异化构建, 如通过文件后缀区分构建类型: *.alipay.{js,ts} 24 | 25 | #### 2021-04-16 26 | 27 | - [ ] 可跨项目复用的小程序组件或页面(通过NPM包管理) 28 | - [ ] Redux 支持 29 | - [ ] Mixin 支持 30 | - [ ] 允许使用 `webpack-chain` 来修改 webpack 配置 31 | - [ ] 增加不同平台的接口差异性描述和兼容 32 | -------------------------------------------------------------------------------- /packages/webpack/lib/loaders/js-transform-loader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const jsParser = require('../parsers/jsParser'); 4 | 5 | module.exports = function jsTransformLoader(content = '') { 6 | const { type } = this.query || {}; 7 | 8 | if (type === 'alipay' && /src\/app\.js$/.test(this.resourcePath)) { 9 | content = ` 10 | require('ewa/lib/polyfills/alipayComponent')(); 11 | require('ewa/lib/polyfills/alipaySelectorQuery')(); 12 | require('ewa/lib/polyfills/alipayStorage')(); 13 | ${content}`; 14 | } 15 | 16 | // 多端支持 17 | if (type !== 'weapp') content = jsParser(content, type); 18 | 19 | return content; 20 | }; 21 | -------------------------------------------------------------------------------- /packages/cli/templates/wechat-app/src/project.swan.json: -------------------------------------------------------------------------------- 1 | { 2 | "appid": "touristappid", 3 | "compilation-args": { 4 | "common": { 5 | "ignorePrefixCss": false, 6 | "ignoreTransJs": true, 7 | "ignoreUglify": true, 8 | "imgCompress": false, 9 | "lint": true, 10 | "useStrict": true 11 | }, 12 | "options": [], 13 | "selected": -1 14 | }, 15 | "host": "baiduboxapp", 16 | "projectname": "baidu mini app sample", 17 | "setting": { 18 | "urlCheck": false 19 | }, 20 | "swan": { 21 | "baiduboxapp": { 22 | "extensionJsVersion": "1.18.3", 23 | "swanJsVersion": "3.220.16" 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /packages/ewa/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | # [1.2.0](https://github.com/lyfeyaj/ewa/tree/master/packages/ewa/compare/v1.1.0...v1.2.0) (2021-05-21) 7 | 8 | 9 | ### Bug Fixes 10 | 11 | * **ewa:** 修复变量引用错误 ([b6be282](https://github.com/lyfeyaj/ewa/tree/master/packages/ewa/commit/b6be2827fe1f12f478dc17db155ed54dd5115a80)) 12 | 13 | 14 | ### Features 15 | 16 | * **ewa:** 允许 createStore 自定义注入的方法和属性名称 ([9215776](https://github.com/lyfeyaj/ewa/tree/master/packages/ewa/commit/92157769e07a562006d889726573b201f59a42cc)) 17 | -------------------------------------------------------------------------------- /packages/cli/lib/commands/start.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const execSync = require('child_process').execSync; 4 | const utils = require('../utils'); 5 | 6 | module.exports = async function start(type) { 7 | utils.ensureEwaProject(type); 8 | 9 | utils.checkUpdates(); 10 | 11 | const ROOT = process.cwd(); 12 | 13 | const script = require.resolve('ewa-webpack/lib/run.js'); 14 | 15 | utils.log('正在启动项目实时编译...'); 16 | 17 | execSync( 18 | `node "${script}"`, 19 | { 20 | cwd: ROOT, 21 | env: Object.assign({}, { 22 | NODE_ENV: 'development', 23 | EWA_ENV: type 24 | }, process.env), 25 | stdio: ['pipe', process.stdout, process.stderr] 26 | } 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "command": { 6 | "create": { 7 | "homepage": "https://github.com/lyfeyaj/ewa", 8 | "license": "MIT" 9 | }, 10 | "version": { 11 | "conventionalCommits": true, 12 | "exact": true, 13 | "message": "chore(release): %s", 14 | "createRelease": "github", 15 | "private": false 16 | }, 17 | "publish": { 18 | "npmClient": "npm", 19 | "allowBranch": [ 20 | "master" 21 | ], 22 | "registry": "https://registry.npmjs.org/" 23 | } 24 | }, 25 | "ignoreChanges": [ 26 | "**/__fixtures__/**", 27 | "**/__tests__/**", 28 | "**/*.md" 29 | ], 30 | "version": "1.2.4" 31 | } 32 | -------------------------------------------------------------------------------- /packages/webpack/lib/loaders/json-transform-loader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const jsonParser = require('../parsers/jsonParser'); 5 | const EXCLUDE_URL_MATCHER = /dynamicLib:\/\//; 6 | 7 | module.exports = function jsonTransformLoader(content) { 8 | let { type, ENTRY_DIR, GLOBAL_COMPONENTS } = this.query || {}; 9 | 10 | // 1.包含需要删除的组件路径,需要删除 11 | // 2.在swan中,替换相对路径为绝对路径 12 | // 3.在alipay中,将app.json中的全局组件写入各个页面的json文件 13 | if (EXCLUDE_URL_MATCHER.test(content) || type === 'swan' || type === 'alipay') { 14 | let file = '/' + path.relative(ENTRY_DIR, this.resourcePath); 15 | content = jsonParser(content, file, type, GLOBAL_COMPONENTS, ENTRY_DIR); 16 | } 17 | 18 | return content; 19 | }; 20 | -------------------------------------------------------------------------------- /packages/cli/lib/commands/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint no-console: "off" */ 4 | 5 | const execSync = require('child_process').execSync; 6 | const utils = require('../utils'); 7 | 8 | module.exports = function build(type) { 9 | utils.ensureEwaProject(type); 10 | 11 | const ROOT = process.cwd(); 12 | 13 | const script = require.resolve('ewa-webpack/lib/run.js'); 14 | 15 | utils.log('正在以生产模式编译项目...'); 16 | 17 | execSync( 18 | `node "${script}"`, 19 | { 20 | cwd: ROOT, 21 | env: Object.assign({}, { 22 | NODE_ENV: 'production', 23 | EWA_ENV: type 24 | }, process.env), 25 | stdio: ['pipe', process.stdout, process.stderr] 26 | } 27 | ); 28 | 29 | utils.log('编译完成 !', 'success'); 30 | }; 31 | -------------------------------------------------------------------------------- /packages/cli/templates/wechat-app/src/project.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "项目配置文件。", 3 | "packOptions": { 4 | "ignore": [] 5 | }, 6 | "setting": { 7 | "urlCheck": false, 8 | "es6": false, 9 | "postcss": false, 10 | "minified": false, 11 | "newFeature": false 12 | }, 13 | "compileType": "miniprogram", 14 | "libVersion": "2.1.1", 15 | "appid": "touristappid", 16 | "projectname": "wechat app sample", 17 | "condition": { 18 | "search": { 19 | "current": -1, 20 | "list": [] 21 | }, 22 | "conversation": { 23 | "current": -1, 24 | "list": [] 25 | }, 26 | "game": { 27 | "currentL": -1, 28 | "list": [] 29 | }, 30 | "miniprogram": { 31 | "current": -1, 32 | "list": [] 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/webpack/lib/rules/ts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | 6 | module.exports = function tsRule(options) { 7 | const defaultConfigPath = path.resolve(__dirname, '../../tsconfig.json'); 8 | const userConfigPath = path.resolve(options.ROOT, './tsconfig.json'); 9 | 10 | function fetchTsConfigFile(path) { 11 | try { 12 | fs.accessSync(path); 13 | } catch(err) { 14 | return defaultConfigPath; 15 | } 16 | return path; 17 | } 18 | 19 | const configFile = fetchTsConfigFile(userConfigPath); 20 | 21 | return { 22 | test: /\.ts$/, 23 | use: [ 24 | { 25 | loader: 'ts-loader', 26 | options: { 27 | transpileOnly: true, 28 | configFile 29 | } 30 | } 31 | ] 32 | }; 33 | }; -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ewa-cli", 3 | "version": "1.2.4", 4 | "description": "EWA Command line tool", 5 | "main": "lib/cli.js", 6 | "bin": { 7 | "ewa": "lib/cli.js" 8 | }, 9 | "scripts": { 10 | "test": "mocha", 11 | "lint": "eslint lib", 12 | "lint:fix": "eslint --fix lib" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/lyfeyaj/ewa/tree/master/packages/cli" 17 | }, 18 | "keywords": [ 19 | "ewa", 20 | "cli" 21 | ], 22 | "author": "Felix Liu", 23 | "license": "MIT", 24 | "dependencies": { 25 | "chalk": "^4.1.0", 26 | "ewa-webpack": "1.2.4", 27 | "fs-extra": "^9.0.1", 28 | "glob": "^7.1.7", 29 | "semver": "^7.3.2", 30 | "yargs": "^16.1.0" 31 | }, 32 | "gitHead": "a280118f8557069fc383c105d1301b8175d74c13" 33 | } 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ewa-monorepo", 3 | "license": "ISC", 4 | "author": { 5 | "name": "Felix Liu", 6 | "email": "lyfeyaj@gmail.com", 7 | "url": "https://github.com/lyfeyaj" 8 | }, 9 | "private": true, 10 | "bugs": "https://github.com/lyfeyaj/ewa/issues", 11 | "engines": { 12 | "node": ">=10" 13 | }, 14 | "keywords": [ 15 | "ewa", 16 | "wechat mini program" 17 | ], 18 | "scripts": { 19 | "commit": "cz", 20 | "build": "cd packages/ewa && npm run lint:fix && npm run build" 21 | }, 22 | "devDependencies": { 23 | "@babel/eslint-parser": "^7.13.14", 24 | "commitizen": "^4.2.4", 25 | "cz-conventional-changelog": "^3.3.0", 26 | "lerna": "^4.0.0" 27 | }, 28 | "config": { 29 | "commitizen": { 30 | "path": "./node_modules/cz-conventional-changelog" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/webpack/lib/loaders/fix-unary-element-loader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ELEMENT_MATCHER = /(<\/?)(area|base|basefont|br|col|embed|frame|hr|img|input|isindex|keygen|link|meta|param|source|track|wbr)( |>)/gi; 4 | const REPLACER = '___unary___'; 5 | const REPLACER_MATCHER = /___unary___/g; 6 | 7 | // 修复一元元素在压缩wxss的时候解析错误的问题 8 | // 解决方案:重命名一元元素,等待压缩完成后,恢复之前的命名 9 | // 仅适用于 wxml 10 | function fixUnaryElementLoader(content, map, meta) { 11 | let { action } = this.query || {}; 12 | if (action && content) { 13 | // 如果包含替换后的一元元素,则删除占位符 14 | if (action === 'removePrefix') { 15 | content = content.replace(REPLACER_MATCHER, ''); 16 | } else if (action === 'addPrefix') { 17 | // 反之,则添加占位符 18 | content = content.replace(ELEMENT_MATCHER, `$1${REPLACER}$2$3`); 19 | } 20 | } 21 | this.callback(null, content, map, meta); 22 | } 23 | 24 | module.exports = fixUnaryElementLoader; 25 | -------------------------------------------------------------------------------- /packages/webpack/lib/utils/babelConfig.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | // 让 babel 根据文件判断是 commonjs 或者 esmodule 5 | sourceType: 'unambiguous', 6 | presets: [[require('@babel/preset-env'), { targets: { ios: '7' } }]], 7 | plugins: [ 8 | [ 9 | require('@babel/plugin-transform-runtime'), 10 | { 11 | helpers: true, 12 | corejs: false, 13 | regenerator: true 14 | } 15 | ], 16 | [ 17 | require('@babel/plugin-proposal-decorators'), 18 | { decoratorsBeforeExport: true } 19 | ], 20 | require('@babel/plugin-proposal-function-sent'), 21 | require('@babel/plugin-proposal-throw-expressions'), 22 | require('@babel/plugin-syntax-import-meta'), 23 | require('@babel/plugin-proposal-do-expressions'), 24 | require('@babel/plugin-proposal-export-default-from'), 25 | [require('@babel/plugin-proposal-pipeline-operator'), { 'proposal': 'minimal' }], 26 | ] 27 | } -------------------------------------------------------------------------------- /packages/webpack/lib/plugins/EnsureVendorsExistancePlugin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | 6 | module.exports = class EnsureVendorsExistancePlugin { 7 | constructor(options = {}) { 8 | this.options = options; 9 | } 10 | 11 | apply(compiler) { 12 | const { commonModuleName } = this.options; 13 | 14 | const outputPath = compiler.options.output.path; 15 | 16 | compiler.hooks.done.tap('EnsureVendorsExistancePlugin', () => { 17 | const vendorsOutputPath = path.join(outputPath, commonModuleName); 18 | 19 | // 检查 公共文件是否存在,如果存在则跳过,如果不存在,则新建一个 20 | if (!fs.existsSync(vendorsOutputPath)) { 21 | fs.writeFileSync(vendorsOutputPath, this.buildCommonModuleTemplate()); 22 | } 23 | }); 24 | } 25 | 26 | buildCommonModuleTemplate() { 27 | const { commonModuleName } = this.options; 28 | return `exports.ids = ["${commonModuleName}"];\nexports.modules = {};`; 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /packages/cli/templates/wechat-app/src/project.qq.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectid": "qq miniprogram sample id", 3 | "setting": { 4 | "urlCheck": false, 5 | "es6": true, 6 | "postcss": false, 7 | "minified": true, 8 | "newFeature": true, 9 | "autoAudits": false, 10 | "nodeModules": true, 11 | "uploadWithSourceMap": true, 12 | "uglifyFileName": true, 13 | "remoteDebugLogEnable": false, 14 | "prefetch": false 15 | }, 16 | "qqLibVersion": "1.24.0", 17 | "compileType": "miniprogram", 18 | "packOptions": { 19 | "ignore": [] 20 | }, 21 | "debugOptions": { 22 | "hidedInDevtools": [] 23 | }, 24 | "qqappid": "testappid", 25 | "projectname": "qq miniprogram sample", 26 | "scripts": {}, 27 | "condition": { 28 | "search": { 29 | "current": -1, 30 | "list": [] 31 | }, 32 | "conversation": { 33 | "current": -1, 34 | "list": [] 35 | }, 36 | "game": { 37 | "currentL": -1, 38 | "list": [] 39 | }, 40 | "miniprogram": { 41 | "current": -1, 42 | "list": [] 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /packages/cli/templates/wechat-app/src/app.js: -------------------------------------------------------------------------------- 1 | App({ 2 | onLaunch: function () { 3 | // 展示本地存储能力 4 | var logs = wx.getStorageSync('logs') || []; 5 | logs.unshift(Date.now()); 6 | wx.setStorageSync('logs', logs); 7 | 8 | // 登录 9 | wx.login({ 10 | success: res => { 11 | // 发送 res.code 到后台换取 openId, sessionKey, unionId 12 | } 13 | }); 14 | // 获取用户信息 15 | wx.getSetting({ 16 | success: res => { 17 | if (res.authSetting['scope.userInfo']) { 18 | // 已经授权,可以直接调用 getUserInfo 获取头像昵称,不会弹框 19 | wx.getUserInfo({ 20 | success: res => { 21 | // 可以将 res 发送给后台解码出 unionId 22 | this.globalData.userInfo = res.userInfo; 23 | 24 | // 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回 25 | // 所以此处加入 callback 以防止这种情况 26 | if (this.userInfoReadyCallback) { 27 | this.userInfoReadyCallback(res); 28 | } 29 | } 30 | }); 31 | } 32 | } 33 | }); 34 | }, 35 | globalData: { 36 | userInfo: null 37 | } 38 | }); 39 | -------------------------------------------------------------------------------- /packages/cli/lib/commands/create.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint no-console: "off" */ 4 | 5 | // Modules 6 | const path = require('path'); 7 | const fs = require('fs-extra'); 8 | const utils = require('../utils'); 9 | const install = require('./install'); 10 | 11 | // Constants 12 | const ROOT = process.cwd(); 13 | const TEMPLATE_DIR = path.resolve(__dirname, '../../templates/wechat-app'); 14 | 15 | module.exports = function create(argv) { 16 | const projectName = argv.projectName || ''; 17 | const projectDir = path.resolve(ROOT, projectName); 18 | 19 | if (!projectName) return utils.log('请输入项目名称', 'error'); 20 | 21 | if (fs.existsSync(projectDir)) { 22 | return utils.log('文件或文件夹已存在, 请尝试更换项目名称', 'error'); 23 | } 24 | 25 | utils.log(`初始化 ewa 项目: ${projectName}`); 26 | 27 | // 创建项目文件夹 28 | fs.ensureDirSync(projectDir); 29 | 30 | // 拷贝项目文件 31 | fs.copySync(TEMPLATE_DIR, projectDir, { 32 | filter: function(src) { 33 | if (/template(\/|\\)(dist|node_modules)(\/|\\)/i.test(src)) return false; 34 | return true; 35 | } 36 | }); 37 | 38 | // 执行安装流程 39 | install(projectDir); 40 | }; 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2021 Felix Liu 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 | -------------------------------------------------------------------------------- /packages/ewa/src/polyfills/alipayStorage.js: -------------------------------------------------------------------------------- 1 | // 抹平wx小程序和支付宝小程序 storageSync API的差异 2 | function alipayStorage() { 3 | /** 4 | * ======= setStorageSync、removeStorageSync ======= 5 | * 微信中 wx.setStorageSync('key', 'data') 6 | * 支付宝 my.setStorageSync({ key, data }) 7 | * */ 8 | ['setStorageSync', 'removeStorageSync'].forEach((methodName) => { 9 | const _cacheFn = my[methodName]; 10 | my[methodName] = function (key, data) { 11 | if (typeof key === 'object') return _cacheFn(key); 12 | return _cacheFn({ key, data }); 13 | }; 14 | }); 15 | 16 | /** 17 | * ======= getStorageSync ======= 18 | * 调用: 19 | * 微信中 wx.getStorageSync('key') 20 | * 支付宝 wx.getStorageSync({ key }) 21 | * 22 | * 返回: 23 | * 微信中直接返回 data 24 | * 支付宝返回 {success: true, data} 25 | * */ 26 | const _getStorageSync = my.getStorageSync; 27 | my.getStorageSync = function (key) { 28 | let res = null; 29 | if (typeof key === 'object') { 30 | res = _getStorageSync(key); 31 | } else { 32 | res = _getStorageSync({ key }); 33 | } 34 | if (res.success) return res.data; 35 | }; 36 | } 37 | 38 | module.exports = alipayStorage; 39 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "commonjs": true, 6 | "es6": true, 7 | "node": true 8 | }, 9 | "globals": { 10 | "wx": true, 11 | "qq": true, 12 | "tt": true, 13 | "swan": true, 14 | "my": true, 15 | "App": true, 16 | "Page": true, 17 | "getApp": true, 18 | "Component": true, 19 | "Behavior": true, 20 | "WeixinJSBridge": true, 21 | "getCurrentPages": true 22 | }, 23 | "requireConfigFile": false, 24 | "parser": "@babel/eslint-parser", 25 | "extends": ["eslint:recommended"], 26 | "parserOptions": { 27 | "ecmaFeatures": { 28 | "experimentalObjectRestSpread": true 29 | }, 30 | "sourceType": "module" 31 | }, 32 | "rules": { 33 | "no-unused-vars": [ 34 | 1 35 | ], 36 | "no-console": [ 37 | 1 38 | ], 39 | "indent": [ 40 | 2, 41 | 2, 42 | { "SwitchCase": 1, "MemberExpression": 1 } 43 | ], 44 | "linebreak-style": [ 45 | 2, 46 | "unix" 47 | ], 48 | "quotes": [ 49 | 1, 50 | "single" 51 | ], 52 | "semi": [ 53 | 2, 54 | "always" 55 | ] 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /packages/cli/templates/wechat-app/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | *.log 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | *.pid.lock 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # nyc test coverage 20 | .nyc_output 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # Bower dependency directory (https://bower.io/) 26 | bower_components 27 | 28 | # node-waf configuration 29 | .lock-wscript 30 | 31 | # Compiled binary addons (https://nodejs.org/api/addons.html) 32 | build/Release 33 | 34 | # Dependency directories 35 | node_modules/ 36 | jspm_packages/ 37 | 38 | # TypeScript v1 declaration files 39 | typings/ 40 | 41 | # Optional npm cache directory 42 | .npm 43 | 44 | # Optional eslint cache 45 | .eslintcache 46 | 47 | # Optional REPL history 48 | .node_repl_history 49 | 50 | # Output of 'npm pack' 51 | *.tgz 52 | 53 | # Yarn Integrity file 54 | .yarn-integrity 55 | 56 | # dotenv environment variables file 57 | .env 58 | 59 | # next.js build output 60 | .next 61 | 62 | # Directory for built wechat app 63 | dist* 64 | -------------------------------------------------------------------------------- /packages/cli/templates/wechat-app/gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | *.log 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | *.pid.lock 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # nyc test coverage 20 | .nyc_output 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # Bower dependency directory (https://bower.io/) 26 | bower_components 27 | 28 | # node-waf configuration 29 | .lock-wscript 30 | 31 | # Compiled binary addons (https://nodejs.org/api/addons.html) 32 | build/Release 33 | 34 | # Dependency directories 35 | node_modules/ 36 | jspm_packages/ 37 | 38 | # TypeScript v1 declaration files 39 | typings/ 40 | 41 | # Optional npm cache directory 42 | .npm 43 | 44 | # Optional eslint cache 45 | .eslintcache 46 | 47 | # Optional REPL history 48 | .node_repl_history 49 | 50 | # Output of 'npm pack' 51 | *.tgz 52 | 53 | # Yarn Integrity file 54 | .yarn-integrity 55 | 56 | # dotenv environment variables file 57 | .env 58 | 59 | # next.js build output 60 | .next 61 | 62 | # Directory for built mini program app 63 | dist 64 | dist-* 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | *.log 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | *.pid.lock 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # nyc test coverage 20 | .nyc_output 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # Bower dependency directory (https://bower.io/) 26 | bower_components 27 | 28 | # node-waf configuration 29 | .lock-wscript 30 | 31 | # Compiled binary addons (https://nodejs.org/api/addons.html) 32 | build/Release 33 | 34 | # Dependency directories 35 | node_modules/ 36 | jspm_packages/ 37 | 38 | # TypeScript v1 declaration files 39 | typings/ 40 | 41 | # Optional npm cache directory 42 | .npm 43 | 44 | # Optional eslint cache 45 | .eslintcache 46 | 47 | # Optional REPL history 48 | .node_repl_history 49 | 50 | # Output of 'npm pack' 51 | *.tgz 52 | 53 | # Yarn Integrity file 54 | .yarn-integrity 55 | 56 | # dotenv environment variables file 57 | .env 58 | 59 | # direnv environment variables file 60 | .envrc 61 | 62 | # next.js build output 63 | .next 64 | 65 | package-lock.json 66 | yarn.lock 67 | 68 | */.DS_Store 69 | .DS_Store 70 | 71 | # ignore ewa lib 72 | packages/ewa/lib 73 | -------------------------------------------------------------------------------- /packages/cli/templates/wechat-app/ewa.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | // 公用代码库 (node_modules 打包生成的文件)名称,默认为 vendors.js 5 | commonModuleName: 'vendors.js', 6 | 7 | // 通用模块匹配模式,默认为 /[\\/](node_modules|utils|vendor)[\\/].+\.js/ 8 | // 如需添加多个文件夹,可自定义正则,如 /[\\/](node_modules|utils|custom_dirname)[\\/].+\.js/ 9 | commonModulePattern: /[\\/](node_modules|utils|vendor)[\\/].+\.js/, 10 | 11 | // 是否简化路径,作用于 page 和 component,如 index/index.wxml=> index.wxml,默认为 false 12 | simplifyPath: false, 13 | 14 | // 文件夹快捷引用 15 | aliasDirs: [ 16 | 'apis', 17 | 'assets', 18 | 'constants', 19 | 'utils' 20 | ], 21 | 22 | // 需要拷贝的文件类型 23 | copyFileTypes: [ 24 | 'png', 25 | 'jpeg', 26 | 'jpg', 27 | 'gif', 28 | 'svg', 29 | 'ico', 30 | 'webp', 31 | 'apng' 32 | ], 33 | 34 | // webpack loader 规则 35 | rules: [], 36 | 37 | // webpack 插件 38 | plugins: [], 39 | 40 | // 开发环境下是否自动清理无用文件,默认为 true 41 | autoCleanUnusedFiles: true, 42 | 43 | // css 解析器,sass 或者 less,默认为 sass 44 | cssParser: 'sass', 45 | 46 | // 是否开启 hashed module id 47 | hashedModuleIds: true, 48 | 49 | // 是否开启缓存,默认为 true 50 | cache: true, 51 | 52 | // 自定义环境变量, 默认为 ['NODE_ENV', 'EWA_ENV'] 53 | customEnvironments: [], 54 | 55 | // 嫌不够灵活?直接修改 webpack 配置 56 | webpack: function(config) { 57 | return config; 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /packages/cli/templates/wechat-app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "commonjs": true, 6 | "es6": true, 7 | "node": true 8 | }, 9 | "globals": { 10 | "wx": true, 11 | "qq": true, 12 | "tt": true, 13 | "swan": true, 14 | "my": true, 15 | "App": true, 16 | "Page": true, 17 | "getApp": true, 18 | "Component": true, 19 | "Behavior": true, 20 | "WeixinJSBridge": true, 21 | "getCurrentPages": true 22 | }, 23 | "parser": "@babel/eslint-parser", 24 | "extends": ["eslint:recommended"], 25 | "parserOptions": { 26 | "ecmaFeatures": { 27 | "experimentalObjectRestSpread": true 28 | }, 29 | "sourceType": "module" 30 | }, 31 | "rules": { 32 | "no-unused-vars": [ 33 | 1 34 | ], 35 | "no-console": [ 36 | 1 37 | ], 38 | "indent": [ 39 | 2, 40 | 2, 41 | { "SwitchCase": 1, "MemberExpression": 1 } 42 | ], 43 | "linebreak-style": [ 44 | 2, 45 | "unix" 46 | ], 47 | "quotes": [ 48 | 1, 49 | "single" 50 | ], 51 | "semi": [ 52 | 2, 53 | "always" 54 | ] 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /packages/cli/templates/wechat-app/src/pages/index/index.js: -------------------------------------------------------------------------------- 1 | //index.js 2 | //获取应用实例 3 | const app = getApp(); 4 | 5 | Page({ 6 | data: { 7 | motto: 'Hello World', 8 | userInfo: {}, 9 | hasUserInfo: false, 10 | canIUse: wx.canIUse('button.open-type.getUserInfo') 11 | }, 12 | //事件处理函数 13 | bindViewTap: function() { 14 | wx.navigateTo({ 15 | url: '../logs/logs' 16 | }); 17 | }, 18 | onLoad: function () { 19 | if (app.globalData.userInfo) { 20 | this.setData({ 21 | userInfo: app.globalData.userInfo, 22 | hasUserInfo: true 23 | }); 24 | } else if (this.data.canIUse){ 25 | // 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回 26 | // 所以此处加入 callback 以防止这种情况 27 | app.userInfoReadyCallback = res => { 28 | this.setData({ 29 | userInfo: res.userInfo, 30 | hasUserInfo: true 31 | }); 32 | }; 33 | } else { 34 | // 在没有 open-type=getUserInfo 版本的兼容处理 35 | wx.getUserInfo({ 36 | success: res => { 37 | app.globalData.userInfo = res.userInfo; 38 | this.setData({ 39 | userInfo: res.userInfo, 40 | hasUserInfo: true 41 | }); 42 | } 43 | }); 44 | } 45 | }, 46 | getUserInfo: function(e) { 47 | console.log(e); 48 | app.globalData.userInfo = e.detail.userInfo; 49 | this.setData({ 50 | userInfo: e.detail.userInfo, 51 | hasUserInfo: true 52 | }); 53 | } 54 | }); 55 | -------------------------------------------------------------------------------- /packages/ewa/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ewa", 3 | "version": "1.2.0", 4 | "description": "Enhanced wechat app development toolkit", 5 | "main": "lib/ewa.js", 6 | "scripts": { 7 | "test": "mocha", 8 | "build": "babel src -d ./lib --no-comments", 9 | "lint": "eslint src", 10 | "lint:fix": "eslint --fix src", 11 | "clean": "rm -rf lib" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/lyfeyaj/ewa/tree/master/packages/ewa" 16 | }, 17 | "keywords": [ 18 | "ewa", 19 | "wechat", 20 | "toolkit" 21 | ], 22 | "author": "Felix Liu", 23 | "license": "MIT", 24 | "dependencies": { 25 | "deep-diff": "^1.0.2", 26 | "lodash.assign": "^4.2.0", 27 | "lodash.clonedeep": "^4.5.0", 28 | "lodash.get": "^4.4.2", 29 | "lodash.has": "^v4.5.2", 30 | "lodash.isfunction": "^v3.0.9", 31 | "lodash.isobject": "^v3.0.2", 32 | "lodash.isplainobject": "^v4.0.6", 33 | "lodash.keys": "^4.2.0", 34 | "lodash.set": "^4.3.2" 35 | }, 36 | "devDependencies": { 37 | "@babel/cli": "^7.13.16", 38 | "@babel/core": "^7.13.16", 39 | "@babel/eslint-parser": "^7.13.14", 40 | "@babel/preset-env": "^7.13.15", 41 | "@babel/register": "^7.13.16", 42 | "babel-plugin-add-module-exports": "^1.0.4", 43 | "eslint": "^7.25.0", 44 | "eslint-config-airbnb-base": "^14.2.1", 45 | "eslint-plugin-import": "^2.22.1" 46 | }, 47 | "gitHead": "a280118f8557069fc383c105d1301b8175d74c13" 48 | } 49 | -------------------------------------------------------------------------------- /packages/webpack/lib/loaders/import-wxss-loader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const os = require('os'); 5 | const helpers = require('../utils'); 6 | 7 | const IS_WINDOWS = os.platform() === 'win32'; 8 | 9 | function importWxssLoader(content, map, meta) { 10 | let re = /(@import\s*)([^;]+);/gi; 11 | 12 | let callback = this.async(); 13 | const options = this.query || {}; 14 | 15 | let urls = []; 16 | content = content.replace(re, (str, m1, m2) => { 17 | if (m2 && /.wxss/.test(m2)) { 18 | urls.push(m2); 19 | return `${m1}url(${m2});`; 20 | } else { 21 | return str; 22 | } 23 | }); 24 | 25 | Promise.all(urls.map((url) => { 26 | let _url = url.replace(/'|"/gi, '').replace(/^~/, ''); 27 | 28 | return new Promise((resolve, reject) => { 29 | this.resolve(this.context, _url, (err, result) => { 30 | if (err) return reject(err); 31 | let context = path.dirname( 32 | helpers.resolveOrSimplifyPath( 33 | null, 34 | this.resourcePath, 35 | options.simplifyPath 36 | ) 37 | ); 38 | result = helpers.resolveOrSimplifyPath( 39 | null, 40 | result, 41 | options.simplifyPath 42 | ); 43 | let relativePath = path.relative(context, result); 44 | // 增加 windows 支持 45 | if (IS_WINDOWS) relativePath = relativePath.replace(/\\/g, '/'); 46 | content = content.replace(url, `'${relativePath}'`); 47 | resolve(); 48 | }); 49 | }); 50 | })).then(() => { 51 | callback(null, content, map, meta); 52 | }).catch(err => { 53 | callback(err); 54 | }); 55 | } 56 | 57 | module.exports = importWxssLoader; 58 | -------------------------------------------------------------------------------- /packages/webpack/lib/parsers/jsonParser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const alipayJsonParser = require('./alipayJsonParser'); 5 | 6 | // 组件引用路径中,匹配到以下字符串,百度中不需要替换为绝对路径,其他平台直接删除此引用 7 | const EXCLUDE_URL_MATCHER = /dynamicLib:\/\//; 8 | 9 | // 百度小程序组件不支持相对路径,全部转换为绝对路径 10 | // 解析 usingComponents 并转换为绝对路径 11 | module.exports = function jsonParser(content, file, type, GLOBAL_COMPONENTS, ENTRY_DIR) { 12 | 13 | if (!EXCLUDE_URL_MATCHER.test(content) && type !== 'swan' && type !== 'alipay') return content; 14 | 15 | let json = JSON.parse(content); 16 | let usingComponents = json.usingComponents || {}; 17 | Object.keys(usingComponents).forEach(function (component) { 18 | if (requireDelete(usingComponents[component], type)) { 19 | // 检测是否需要删除 20 | delete usingComponents[component]; 21 | } else if (requireReplace(usingComponents[component], type)) { 22 | // 检测是否需要替换为绝对路径 23 | usingComponents[component] = path.resolve(path.dirname(file), usingComponents[component]); 24 | } 25 | }); 26 | 27 | json.usingComponents = usingComponents; 28 | 29 | if (type === 'alipay') { 30 | // 将app.json中的全局组件 写入每个的page.json 31 | Object.keys(GLOBAL_COMPONENTS || {}).forEach(name => { 32 | json.usingComponents[name] = '/' + path.relative(ENTRY_DIR, path.resolve(ENTRY_DIR, GLOBAL_COMPONENTS[name])); 33 | }); 34 | // 处理支付宝平台json文件中字段不一致的问题 35 | json = alipayJsonParser(json, type); 36 | } 37 | 38 | return JSON.stringify(json, null, ' '); 39 | }; 40 | 41 | // 组件引用url是否包含特殊字符串, 如果包含,则在非百度小程序删除此组件引用 42 | function requireDelete(url, type) { 43 | return type !== 'swan' && EXCLUDE_URL_MATCHER.test(url); 44 | } 45 | 46 | // 百度平台 匹配不到特殊字符串, 需要替换为绝对路径 47 | function requireReplace(url, type) { 48 | return type === 'swan' && !EXCLUDE_URL_MATCHER.test(url); 49 | } 50 | -------------------------------------------------------------------------------- /packages/ewa/src/plugins/createStore/reactive.js: -------------------------------------------------------------------------------- 1 | const isObject = require('lodash.isobject'); 2 | const Observer = require('./Observer'); 3 | 4 | const obInstance = Observer.getInstance(); 5 | 6 | // 劫持属性 7 | function defineReactive(obj, key, path) { 8 | const property = Object.getOwnPropertyDescriptor(obj, key); 9 | 10 | if (property && property.configurable === false) return; 11 | 12 | // 兼容自定义getter/setter 13 | const getter = property && property.get; 14 | const setter = property && property.set; 15 | 16 | let val = obj[key]; 17 | 18 | // 记录遍历层级 19 | if (path) path = `${path}.${key}`; 20 | 21 | // 深度遍历 22 | if (isObject(val)) reactive(val, path || key); 23 | 24 | Object.defineProperty(obj, key, { 25 | enumerable: true, 26 | configurable: true, 27 | get: function reactiveGetter() { 28 | const value = getter ? getter.call(obj) : val; 29 | return value; 30 | }, 31 | set: function reactiveSetter(newVal) { 32 | const value = getter ? getter.call(obj) : val; 33 | if (newVal === value) return; 34 | if (getter && !setter) return; 35 | if (setter) { 36 | setter.call(obj, newVal); 37 | } else { 38 | val = newVal; 39 | obInstance.emitReactive(path || key, val); 40 | } 41 | }, 42 | }); 43 | } 44 | 45 | /** 46 | * 使 obj 响应式化 47 | * @param {Object} obj - 数据对象 48 | * @param {string} key - 属性 key 49 | * @example 50 | * 使obj响应式化,即 obj 修改 => 全局 data (同字段)更新 51 | * 支持默认修改: 52 | * obj.a = 'xxx' 53 | * 支持属性嵌套修改: 54 | * obj.a.b.c = 'yyy' 55 | * 支持数组下标修改: 56 | * obj.a[3] = 'zzz' 57 | * obj.a.3 = 'zzz' 58 | */ 59 | function reactive(obj, key) { 60 | const keys = Object.keys(obj); 61 | for (let i = 0; i < keys.length; i++) { 62 | defineReactive(obj, keys[i], key); 63 | } 64 | } 65 | 66 | module.exports = reactive; 67 | -------------------------------------------------------------------------------- /packages/webpack/lib/rules/wxml.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 4 | 5 | // 处理 wxml 文件 6 | module.exports = function wxmlRule(options = {}) { 7 | let htmlOptions = { 8 | attributes: false, 9 | minimize: false, 10 | esModule: false 11 | }; 12 | 13 | // 非开发环境开启压缩 14 | if (!options.IS_DEV) { 15 | htmlOptions.minimize = { 16 | collapseWhitespace: true, 17 | conservativeCollapse: true, 18 | caseSensitive: true, 19 | minifyCSS: false, 20 | removeComments: true, 21 | keepClosingSlash: true, 22 | removeAttributeQuotes: false, 23 | removeEmptyElements: false, 24 | ignoreCustomFragments: [ 25 | /<%[\s\S]*?%>/, 26 | /<\?[\s\S]*?\?>/, 27 | 28 | // 忽略 wxs、qs 和 sjs 标签的处理 29 | //, 30 | //, 31 | //, 32 | 33 | // 忽略 {{ }} 中间内容的处理 34 | /{{[\s\S]*?}}/, 35 | ], 36 | }; 37 | } 38 | 39 | let htmlRules = [ 40 | { 41 | loader: 'raw-loader', 42 | options: { esModule: false } 43 | }, 44 | 'extract-loader', 45 | { 46 | loader: './loaders/fix-unary-element-loader', 47 | options: { action: 'removePrefix' } 48 | }, 49 | { 50 | loader: 'html-loader', 51 | options: htmlOptions 52 | }, 53 | { 54 | loader: './loaders/fix-unary-element-loader', 55 | options: { action: 'addPrefix' } 56 | }, 57 | { 58 | loader: './loaders/wxml-transform-loader', 59 | options: { type: options.EWA_ENV, ENTRY_DIR: options.ENTRY_DIR } 60 | }, 61 | ]; 62 | 63 | // 开启缓存 64 | if (options.cache) htmlRules = ['cache-loader'].concat(htmlRules); 65 | 66 | return { 67 | test: /\.wxml$/i, 68 | use: ExtractTextPlugin.extract(htmlRules) 69 | }; 70 | }; -------------------------------------------------------------------------------- /packages/webpack/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [1.2.4](https://github.com/lyfeyaj/ewa/tree/master/packages/webpack/compare/v1.2.3...v1.2.4) (2021-07-06) 7 | 8 | 9 | ### Bug Fixes 10 | 11 | * **ewa-webpack:** 修复因为路径中包含空格导致脚本执行报错的问题 ([62c1e4f](https://github.com/lyfeyaj/ewa/tree/master/packages/webpack/commit/62c1e4fda10f8d46f6a6019400e2dbf37f9e6e81)) 12 | 13 | 14 | 15 | 16 | 17 | ## [1.2.3](https://github.com/lyfeyaj/ewa/tree/master/packages/webpack/compare/v1.2.2...v1.2.3) (2021-07-01) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * **ewa-webpack:** 修复 wxml 绕过缓存的问题 ([624f13e](https://github.com/lyfeyaj/ewa/tree/master/packages/webpack/commit/624f13e52992c7b6e988a3bc4687f9cdd2ce4475)) 23 | 24 | 25 | 26 | 27 | 28 | ## [1.2.2](https://github.com/lyfeyaj/ewa/tree/master/packages/webpack/compare/v1.2.0...v1.2.2) (2021-07-01) 29 | 30 | 31 | ### Bug Fixes 32 | 33 | * **ewa-webpack:** 修复 wxml 中的 < 或者 > 可能导致解析报错的问题 ([83d8bda](https://github.com/lyfeyaj/ewa/tree/master/packages/webpack/commit/83d8bda4180b8709976a5a8c13602318fee02223)) 34 | 35 | 36 | 37 | 38 | 39 | # [1.2.0](https://github.com/lyfeyaj/ewa/tree/master/packages/webpack/compare/v1.1.0...v1.2.0) (2021-05-21) 40 | 41 | 42 | ### Bug Fixes 43 | 44 | * **ewa-webpack:** 修复 ts 支持,以及入口文件自动忽略 .d.ts 文件 ([0dd8922](https://github.com/lyfeyaj/ewa/tree/master/packages/webpack/commit/0dd89224d53db452e58a190badfce0eff215d6c8)) 45 | 46 | 47 | 48 | 49 | 50 | # [1.1.0](https://github.com/lyfeyaj/ewa/tree/master/packages/webpack/compare/v1.0.10...v1.1.0) (2021-04-26) 51 | 52 | 53 | ### Features 54 | 55 | * **ewa-webpack:** add custom environments support ([2e17f6a](https://github.com/lyfeyaj/ewa/tree/master/packages/webpack/commit/2e17f6a82d01ada675ca076e115faf5ddb56ed8e)) 56 | -------------------------------------------------------------------------------- /packages/ewa/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": "airbnb-base", 4 | "parser": "@babel/eslint-parser", 5 | "parserOptions": { 6 | "ecmaVersion": 6, 7 | "sourceType": "module", 8 | "requireConfigFile": false 9 | }, 10 | "globals": { 11 | "wx": true, 12 | "qq": true, 13 | "tt": true, 14 | "swan": true, 15 | "my": true, 16 | "App": true, 17 | "Page": true, 18 | "getApp": true, 19 | "Component": true, 20 | "WeixinJSBridge": true, 21 | "getCurrentPages": true 22 | }, 23 | "env": { 24 | "es6": true, 25 | "browser": true, 26 | "node": true, 27 | "mocha": true 28 | }, 29 | "rules": { 30 | "camelcase": 0, 31 | "no-param-reassign": 0, 32 | "one-var": 0, 33 | "one-var-declaration-per-line": 0, 34 | "func-names": 0, 35 | "no-continue": 0, 36 | "no-void": 0, 37 | "no-underscore-dangle": 0, 38 | "newline-per-chained-call": 0, 39 | "array-callback-return": 0, 40 | "no-plusplus": 0, 41 | "prefer-const": 0, 42 | "no-multi-assign": 0, 43 | "linebreak-style": 0, 44 | "consistent-return": 0, 45 | "prefer-rest-params": 0, 46 | "no-bitwise": 0, 47 | "prefer-spread": 0, 48 | "no-restricted-syntax": [2, "DebuggerStatement", "LabeledStatement", "WithStatement"], 49 | "no-restricted-globals": 0, 50 | "prefer-destructuring": 0, 51 | "comma-dangle": [2, { 52 | "arrays": "always-multiline", 53 | "objects": "always-multiline", 54 | "imports": "always-multiline", 55 | "exports": "always-multiline", 56 | "functions": "never" 57 | }], 58 | "no-prototype-builtins": 0, 59 | "no-useless-escape": 0, 60 | "class-methods-use-this": "off", 61 | "no-use-before-define": ["error", { 62 | "functions": false, 63 | "classes": true 64 | }], 65 | "no-console": ["warn", { 66 | "allow": ["warn", "error"] 67 | }] 68 | } 69 | } -------------------------------------------------------------------------------- /packages/webpack/lib/parsers/alipayJsonParser.js: -------------------------------------------------------------------------------- 1 | const TAB_BAR_LIST_MAPPER = { 2 | pagePath: 'pagePath', 3 | text: 'name', 4 | iconPath: 'icon', 5 | selectedIconPath: 'activeIcon' 6 | }; 7 | 8 | const WINDOW_ATTR_MAPPER = { 9 | navigationBarBackgroundColor: 'titleBarColor', 10 | navigationBarTitleText: 'defaultTitle', 11 | enablePullDownRefresh: 'pullRefresh' 12 | }; 13 | 14 | // 支付宝平台 app.json 15 | // 1. 无tabBar.list,需要改为items字段,并且属性名称需要兼容 16 | // 2. window下的属性也需要映射 17 | module.exports = function alipayJsonParser(json, type) { 18 | if (type !== 'alipay') return json; 19 | 20 | tabBarParser(json); 21 | windowParser(json); 22 | 23 | return json; 24 | }; 25 | 26 | // tabBar 属性兼容 27 | function tabBarParser(json) { 28 | if (!json.tabBar) return; 29 | 30 | json.tabBar.items = (json.tabBar.list || []).map(bar => { 31 | const item = {}; 32 | Object.keys(TAB_BAR_LIST_MAPPER).forEach(key => item[TAB_BAR_LIST_MAPPER[key]] = bar[key]); 33 | return item; 34 | }); 35 | } 36 | 37 | // app.json的window属性和页面json的属性兼容 38 | function windowParser(json) { 39 | const pageConfig = json.pages ? (json.window || {}) : (json || {}); 40 | 41 | Object.keys(WINDOW_ATTR_MAPPER).forEach(key => { 42 | pageConfig[WINDOW_ATTR_MAPPER[key]] = pageConfig[key]; 43 | delete pageConfig[key]; 44 | }); 45 | 46 | // 支付宝小程序 自定义导航栏 字段为transparentTitle 47 | if (pageConfig.navigationStyle === 'custom') { 48 | pageConfig.transparentTitle = 'always'; 49 | // 导航栏设置透明后,还会显示标题,为了保持和微信小程序体验一致, 删除json文件中的标题字段 50 | delete pageConfig.defaultTitle; 51 | delete pageConfig.navigationBarTitleText; 52 | } else if (pageConfig.navigationStyle === 'default') { 53 | pageConfig.transparentTitle = 'none'; 54 | } 55 | delete pageConfig.navigationStyle; 56 | 57 | // 打开下拉刷新的同时,必须将allowsBounceVertical属性设置为'YES' 58 | if (pageConfig.pullRefresh) { 59 | pageConfig.allowsBounceVertical = 'YES'; 60 | } 61 | 62 | // 默认支持导航栏点击穿透 63 | pageConfig.titlePenetrate = pageConfig.titlePenetrate || 'YES'; 64 | } 65 | -------------------------------------------------------------------------------- /packages/webpack/lib/plugins/AutoCleanUnusedFilesPlugin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const del = require('del'); 5 | const utils = require('../utils'); 6 | 7 | // 自动清理无用的文件 8 | module.exports = class AutoCleanUnusedFilesPlugin { 9 | constructor(options) { 10 | this.options = Object.assign({ 11 | info: true, 12 | exclude: [], 13 | include: ['**'] 14 | }, options || {}); 15 | } 16 | 17 | apply(compiler) { 18 | const opts = this.options; 19 | 20 | const outputPath = compiler.options.output.path; 21 | 22 | compiler.hooks.done.tapPromise('AutoCleanUnusedFilesPlugin', stats => { 23 | // no clean on errors 24 | if (stats.hasErrors()) { 25 | utils.log(''); 26 | utils.log('AutoCleanUnusedFilesPlugin skipped due to errors.'); 27 | return Promise.resolve(); 28 | } 29 | 30 | // collect compiled files 31 | const assetNames = stats.toJson().assets.map(asset => asset.name); 32 | 33 | // include files, default is all files (**) under working folder 34 | const includePatterns = opts.include.map(n => path.join(outputPath, n)); 35 | 36 | // exclude files 37 | const excludePatterns = [ 38 | outputPath 39 | ].concat( 40 | opts.exclude.map(name => path.join(outputPath, name)) 41 | ).concat( 42 | assetNames.map(name => path.join(outputPath, name)) 43 | ); 44 | 45 | // run delete 46 | // NOTE: del 5.0 版本开始更换了 glob 的库,导致现有的 glob 失效 47 | // 所以暂时锁定 del 版本为 4.1.1 48 | return del(includePatterns, { 49 | ignore: excludePatterns, 50 | nodir: true 51 | }).then(paths => { 52 | if (opts.info && paths && paths.length) { 53 | utils.log(`Auto cleaned files: (${paths.length})`, 'warning'); 54 | paths.map(name => utils.log(` ${path.relative(outputPath, name)}`, 'warning')); 55 | } else { 56 | utils.log('Nothing to clean.'); 57 | } 58 | }); 59 | }); 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /packages/cli/lib/commands/generate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs-extra'); 5 | const utils = require('../utils'); 6 | 7 | const SUPPORT_TYPES = ['page', 'component', 'template']; 8 | const ROOT = process.cwd(); 9 | const BASE_GENERATOR_DIR = path.resolve(__dirname, '../../templates/generators'); 10 | 11 | module.exports = function generate(type, name, dest, index) { 12 | utils.ensureEwaProject(); 13 | 14 | if (SUPPORT_TYPES.indexOf(type) === -1) { 15 | utils.log( 16 | `无法生成此类型: \`${type}\` 的文件, 允许的值为 ${SUPPORT_TYPES.join(', ')}`, 17 | 'error' 18 | ); 19 | process.exit(0); 20 | } 21 | 22 | name = (name || '').trim(); 23 | 24 | if (!name) { 25 | utils.log(`缺少名称, 无法生成 ${type}`, 'error'); 26 | process.exit(0); 27 | } 28 | 29 | const baseDir = path.resolve(ROOT, 'src'); 30 | 31 | let typeDir = `${type}s`; 32 | dest = (dest || '').trim(); 33 | let regexp = new RegExp(`${typeDir}[\\/]?$`); 34 | if (!regexp.test(dest)) dest = path.join(dest, typeDir); 35 | dest = path.resolve(baseDir, dest); 36 | let fileDir = path.resolve(dest, name); 37 | 38 | // 文件名称 39 | name = index ? 'index' : path.basename(fileDir); 40 | 41 | // 生成文件夹 42 | fs.ensureDirSync(fileDir); 43 | 44 | let fileMappings = { 45 | [`${type}/${type}.wxml`]: `${name}.wxml`, 46 | [`${type}/${type}.wxss`]: `${name}.wxss` 47 | }; 48 | 49 | if (type === 'component' || type === 'page') { 50 | fileMappings = Object.assign(fileMappings, { 51 | [`${type}/${type}.js`]: `${name}.js`, 52 | [`${type}/${type}.json`]: `${name}.json` 53 | }); 54 | } 55 | 56 | let source; 57 | for (source in fileMappings) { 58 | let target = path.resolve(fileDir, fileMappings[source]); 59 | 60 | if (fs.existsSync(target)) { 61 | utils.log(` 跳过, 文件 ${target} 已存在`, 'warning'); 62 | } else { 63 | fs.copySync( 64 | path.resolve(BASE_GENERATOR_DIR, source), 65 | target 66 | ); 67 | utils.log(`已生成文件: ${target}`); 68 | } 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /packages/ewa/src/polyfills/alipaySelectorQuery.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-proto */ 2 | 3 | /** 4 | * 抹平wx小程序和支付宝小程序 SelectorQuery API的差异 5 | * 6 | * =========================== 7 | * 微信小程序使用SelectorQuery API有两种写法 8 | * 1.my.createSelectorQuery(callback).exec() 9 | * 2.my.createSelectorQuery().exec(callback) 10 | * 11 | * 支付宝小程序不兼容第一种写法, 导致callback不执行,这里对此情况进行了兼容 12 | * =========================== 13 | */ 14 | function alipaySelectorQuery() { 15 | // 重写选择节点的函数, 选择节点时新建一个用户缓存callback的数组 16 | const query = my.createSelectorQuery(); 17 | const overrideQueryFns = ['in', 'select', 'selectAll', 'selectViewport']; 18 | overrideQueryFns.forEach((name) => { 19 | const _cacheFn = query.__proto__[name]; 20 | query.__proto__[name] = function (selector) { 21 | const node = _cacheFn.call(this, selector); 22 | if (!node.__query) { 23 | node.__query = this; 24 | // 新建一个用于缓存callback的数组 25 | this.cacheCallbacks = []; 26 | } 27 | return node; 28 | }; 29 | }); 30 | 31 | // 重新获取节点信息函数,将回调函数放入缓存数组 32 | const node = query.selectViewport(); 33 | const overrideNodeFns = ['boundingClientRect', 'scrollOffset']; 34 | overrideNodeFns.forEach((name) => { 35 | const _cacheFn = node.__proto__[name]; 36 | 37 | node.__proto__[name] = function (cb) { 38 | // 将 callback 缓存 39 | if (this.__query) this.__query.cacheCallbacks.push(cb); 40 | return _cacheFn.call(this, cb); 41 | }; 42 | }); 43 | 44 | // 重写exec 缓存执行的callback 45 | const _exec = query.__proto__.exec; 46 | query.__proto__.exec = function (cb) { 47 | const _this = this; 48 | 49 | return _exec.call(this, function (rects) { 50 | if (typeof cb === 'function') cb.call(this, rects); 51 | if (_this.cacheCallbacks && _this.cacheCallbacks.length) { 52 | _this.cacheCallbacks.forEach((cacheCallback, i) => { 53 | if (cacheCallback) cacheCallback(rects[i]); 54 | }); 55 | } 56 | _this.cacheCallbacks = null; 57 | }); 58 | }; 59 | } 60 | 61 | module.exports = alipaySelectorQuery; 62 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | EWA | 微信小程序增强开发工具 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /packages/cli/lib/commands/install.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint no-console: "off" */ 4 | 5 | const path = require('path'); 6 | const fs = require('fs-extra'); 7 | const exec = require('child_process').exec; 8 | const utils = require('../utils'); 9 | 10 | module.exports = function install(projectDir, successTip) { 11 | const projectName = path.basename(projectDir); 12 | 13 | // 重命名 gitignore => .gitignore 14 | // https://github.com/npm/npm/issues/3763 15 | // 仅当 有 gitignore 但 没有 .gitignore 的时候 16 | let gitignoreFile = path.resolve(projectDir, 'gitignore'); 17 | let dotGitignoreFile = path.resolve(projectDir, '.gitignore'); 18 | if (fs.existsSync(gitignoreFile) && !fs.existsSync(dotGitignoreFile)) { 19 | fs.moveSync(gitignoreFile, dotGitignoreFile); 20 | } 21 | 22 | successTip = successTip || `cd ${projectName} && npm start`; 23 | 24 | // 修改项目名称 25 | const targetPackageFile = path.resolve(projectDir, 'package.json'); 26 | const packageInfo = fs.readJsonSync(targetPackageFile); 27 | packageInfo.name = packageInfo.description = projectName; 28 | fs.writeJsonSync(targetPackageFile, packageInfo); 29 | 30 | utils.log(`项目: ${projectName} 创建成功`, 'success'); 31 | utils.log('正在安装依赖...'); 32 | 33 | let loadingTip; 34 | let tip = setTimeout(function() { 35 | utils.log('努力安装中, 请耐心等待...'); 36 | 37 | loadingTip = setInterval(function() { 38 | utils.log('.........................'); 39 | }, 10000); 40 | }, 10000); 41 | 42 | exec( 43 | 'npm i', 44 | { cwd: projectDir }, 45 | function(err) { 46 | if (err) return utils.log(err, 'error'); 47 | 48 | if (tip) clearTimeout(tip); 49 | if (loadingTip) clearInterval(loadingTip); 50 | 51 | utils.log('安装完成 ^_^ !', 'success'); 52 | utils.log('~~~~~~~~~~~~~~~~~~~~~~~~~~~', 'success'); 53 | utils.log('欢迎使用 ewa 工具, 运行命令: ', 'success'); 54 | console.log(''); 55 | console.log(` ${successTip}`); 56 | console.log(''); 57 | utils.log('即可启动项目 ~ Enjoy!', 'success'); 58 | utils.log('~~~~~~~~~~~~~~~~~~~~~~~~~~~', 'success'); 59 | } 60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /packages/cli/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [1.2.4](https://github.com/lyfeyaj/ewa/tree/master/packages/cli/compare/v1.2.3...v1.2.4) (2021-07-06) 7 | 8 | 9 | ### Bug Fixes 10 | 11 | * **ewa-cli:** 修复因为路径中包含空格导致脚本执行报错的问题 ([264cb9a](https://github.com/lyfeyaj/ewa/tree/master/packages/cli/commit/264cb9a3ed76fcc0be2516b12bd561cf910be4fa)) 12 | 13 | 14 | 15 | 16 | 17 | ## [1.2.3](https://github.com/lyfeyaj/ewa/tree/master/packages/cli/compare/v1.2.2...v1.2.3) (2021-07-01) 18 | 19 | **Note:** Version bump only for package ewa-cli 20 | 21 | 22 | 23 | 24 | 25 | ## [1.2.2](https://github.com/lyfeyaj/ewa/tree/master/packages/cli/compare/v1.2.0...v1.2.2) (2021-07-01) 26 | 27 | 28 | ### Bug Fixes 29 | 30 | * **ewa-cli:** fix for [#49](https://github.com/lyfeyaj/ewa/tree/master/packages/cli/issues/49) ([9ba99a2](https://github.com/lyfeyaj/ewa/tree/master/packages/cli/commit/9ba99a2e9094174a5d275dcfe4b3171c1c129af9)) 31 | 32 | 33 | 34 | 35 | 36 | ## [1.2.1](https://github.com/lyfeyaj/ewa/tree/master/packages/cli/compare/v1.2.0...v1.2.1) (2021-06-21) 37 | 38 | 39 | ### Bug Fixes 40 | 41 | * **ewa-cli:** fix for [#49](https://github.com/lyfeyaj/ewa/tree/master/packages/cli/issues/49) ([9ba99a2](https://github.com/lyfeyaj/ewa/tree/master/packages/cli/commit/9ba99a2e9094174a5d275dcfe4b3171c1c129af9)) 42 | 43 | 44 | 45 | 46 | 47 | # [1.2.0](https://github.com/lyfeyaj/ewa/tree/master/packages/cli/compare/v1.1.0...v1.2.0) (2021-05-21) 48 | 49 | **Note:** Version bump only for package ewa-cli 50 | 51 | 52 | 53 | 54 | 55 | # [1.1.0](https://github.com/lyfeyaj/ewa/tree/master/packages/cli/compare/v1.0.10...v1.1.0) (2021-04-26) 56 | 57 | 58 | ### Bug Fixes 59 | 60 | * **ewa-cli:** fix ewa-webpack dependency version ([71d937f](https://github.com/lyfeyaj/ewa/tree/master/packages/cli/commit/71d937f4f6476971ce48dd21cc48eec41dc9a89b)) 61 | 62 | 63 | ### Features 64 | 65 | * **ewa-cli:** update templates for custom environments and update deps ([9aae389](https://github.com/lyfeyaj/ewa/tree/master/packages/cli/commit/9aae389cf68107215d011cebda846f2fc37a02ed)) 66 | -------------------------------------------------------------------------------- /packages/ewa/src/polyfills/alipayComponent.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | const keys = require('lodash.keys'); 4 | const assign = require('lodash.assign'); 5 | 6 | function alipayComponent() { 7 | const __Component = Component; 8 | 9 | // 覆盖 Component 以支持微信 Component 转换为 支付宝 的 Component 10 | Component = function (obj) { 11 | // NOTE: 需要更加精确的控制生命周期函数, 兼容性未测试 12 | obj.onInit = function () { 13 | this.properties = this.props || {}; 14 | 15 | // 兼容微信组件中的 triggerEvent 16 | this.triggerEvent = (name, params) => { 17 | name = name.replace(/^[a-zA-Z]{1}/, (s) => s.toUpperCase()); 18 | // 支付宝组件传递函数时 必须以on开头并且on后的第一个字母必须大写(微信必须全小写) 19 | this.props[`on${name}`]({ detail: params }); 20 | }; 21 | 22 | obj.created.apply(this); 23 | obj.attached.apply(this); 24 | obj.didMount = obj.ready && obj.ready.bind(this); 25 | obj.didUnmount = obj.detached && obj.detached.bind(this); 26 | }; 27 | 28 | // obj.didMount = obj.created; 29 | 30 | // 遍历 properties 31 | // properties: { name: { type: String, value: '', observer: 'handleNameChange' } } 32 | // 转换为 33 | // props: { name: '' } 34 | let props = {}; 35 | obj.properties = obj.properties || {}; 36 | keys(obj.properties).forEach((key) => { 37 | let prop = obj.properties[key] || {}; 38 | if ('value' in prop) props[key] = prop.value; 39 | }); 40 | obj.props = props; 41 | 42 | // 接收变更,需要开启 component2 支持 43 | obj.deriveDataFromProps = function (nextProps = {}) { 44 | // 更新 properties 45 | this.properties = assign(this.properties, nextProps || {}); 46 | 47 | // 遍历所有更新的 prop 并触发更新 48 | keys(nextProps).forEach((key) => { 49 | let prop = obj.properties[key] || {}; 50 | if (prop.observer) { 51 | let observer; 52 | if (typeof prop.observer === 'string') { 53 | observer = obj.methods[prop.observer]; 54 | } else if (typeof prop.observer === 'function') { 55 | observer = prop.observer; 56 | } 57 | 58 | // 执行更新 59 | if (observer) { 60 | try { 61 | observer.call(this, this.properties[key]); 62 | } catch (e) { 63 | console.log(e); 64 | } 65 | } 66 | } 67 | }); 68 | }; 69 | 70 | return __Component(obj); 71 | }; 72 | } 73 | 74 | module.exports = alipayComponent; 75 | -------------------------------------------------------------------------------- /packages/webpack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ewa-webpack", 3 | "version": "1.2.4", 4 | "description": "Enhanced wechat app development toolkit", 5 | "main": "lib/run.js", 6 | "scripts": { 7 | "test": "mocha", 8 | "lint": "eslint --fix ./lib/**/*.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/lyfeyaj/ewa/tree/master/packages/webpack" 13 | }, 14 | "keywords": [ 15 | "ewa", 16 | "webpack", 17 | "wechat", 18 | "toolkit" 19 | ], 20 | "author": "Felix Liu", 21 | "license": "MIT", 22 | "engines": { 23 | "node": ">= 10.13.0" 24 | }, 25 | "dependencies": { 26 | "@babel/cli": "^7.13.16", 27 | "@babel/core": "^7.13.16", 28 | "@babel/eslint-parser": "^7.13.14", 29 | "@babel/plugin-proposal-decorators": "^7.13.15", 30 | "@babel/plugin-proposal-do-expressions": "^7.12.13", 31 | "@babel/plugin-proposal-export-default-from": "^7.12.13", 32 | "@babel/plugin-proposal-function-sent": "^7.12.13", 33 | "@babel/plugin-proposal-pipeline-operator": "^7.12.13", 34 | "@babel/plugin-proposal-throw-expressions": "^7.12.13", 35 | "@babel/plugin-syntax-import-meta": "^7.10.4", 36 | "@babel/plugin-transform-runtime": "^7.13.15", 37 | "@babel/preset-env": "^7.13.15", 38 | "@babel/runtime": "^7.13.17", 39 | "autoprefixer": "^10.2.5", 40 | "babel-loader": "^8.2.2", 41 | "cache-loader": "^4.1.0", 42 | "chalk": "^4.1.1", 43 | "chokidar": "^3.5.1", 44 | "copy-webpack-plugin": "^6.4.1", 45 | "css-loader": "^5.2.4", 46 | "cssnano": "^5.0.1", 47 | "del": "^4.1.1", 48 | "dom-serializer": "^1.3.1", 49 | "eslint": "^7.25.0", 50 | "eslint-webpack-plugin": "^2.5.4", 51 | "extract-loader": "^5.1.0", 52 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 53 | "file-loader": "^6.2.0", 54 | "hard-source-webpack-plugin": "^0.13.1", 55 | "html-loader": "^1.3.2", 56 | "htmlparser2": "^6.1.0", 57 | "less": "^4.1.1", 58 | "less-loader": "^7.3.0", 59 | "nodemon": "^2.0.7", 60 | "postcss": "^8.2.12", 61 | "postcss-loader": "^4.2.0", 62 | "raw-loader": "^4.0.2", 63 | "resolve-url-loader": "^3.1.2", 64 | "sass": "^1.32.11", 65 | "sass-loader": "^10.1.1", 66 | "style-loader": "^2.0.0", 67 | "ts-loader": "^8.3.0", 68 | "typescript": "^4.2.4", 69 | "url-loader": "^4.1.1", 70 | "webpack": "^4.46.0", 71 | "webpack-cli": "^3.3.12", 72 | "webpackbar": "^4.0.0" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /packages/cli/lib/commands/init.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint no-console: "off" */ 4 | 5 | // Modules 6 | const path = require('path'); 7 | const fs = require('fs-extra'); 8 | const glob = require('glob'); 9 | const utils = require('../utils'); 10 | const install = require('./install'); 11 | 12 | // Constants 13 | const ROOT = process.cwd(); 14 | const TEMPLATE_DIR = path.resolve(__dirname, '../../templates/wechat-app'); 15 | const TMP_SRC = path.resolve(ROOT, '__tmp_src__'); 16 | const COPY_FILES_OR_DIRS =[ 17 | '.ewa', 18 | '.eslintrc.js', 19 | 'ewa.config.js', 20 | ['gitignore', '.gitignore'], 21 | 'package.json' 22 | ]; 23 | 24 | function isDir(file) { 25 | return fs.statSync(file).isDirectory(); 26 | } 27 | 28 | module.exports = function init() { 29 | utils.log('正在初始化 ewa 项目...'); 30 | 31 | // 创建临时源代码文件夹 32 | utils.log(`创建临时文件夹 ${TMP_SRC}`); 33 | fs.ensureDirSync(TMP_SRC); 34 | 35 | // 拷贝文件至 src 中 36 | utils.log('准备移动文件或文件夹...'); 37 | let sourceFiles = glob.sync(path.resolve(ROOT, '*')) || []; 38 | let isEmptySrc = true; 39 | 40 | // 如果存在源文件 41 | sourceFiles.map(source => { 42 | // Fix for #49, glob 输出的地址为 posix 地址,这里需要使用 path.resolve 自动转换一下 43 | const _source = path.resolve(source); 44 | 45 | if (_source === TMP_SRC) return; 46 | 47 | if (isEmptySrc) isEmptySrc = false; 48 | 49 | let basename = path.basename(_source); 50 | 51 | let dest = path.resolve(TMP_SRC, basename); 52 | 53 | utils.log(`正在移动 ${path.relative(ROOT, _source)} 至 ${path.relative(ROOT, dest)}`); 54 | fs.moveSync(_source, dest, { overwrite: true }); 55 | }); 56 | 57 | utils.log('重命名 __tmp_src__ 为 src'); 58 | fs.moveSync(TMP_SRC, path.resolve(ROOT, 'src')); 59 | utils.log('文件移动完成', 'success'); 60 | 61 | utils.log('正在添加必要的文件...'); 62 | COPY_FILES_OR_DIRS.map(file => { 63 | let source; 64 | let dest; 65 | 66 | if (Array.isArray(file)) { 67 | source = path.resolve(TEMPLATE_DIR, file[0]); 68 | dest = path.resolve(ROOT, file[1]); 69 | } else { 70 | source = path.resolve(TEMPLATE_DIR, file); 71 | dest = path.resolve(ROOT, file); 72 | } 73 | 74 | // 如果文件已存在,则不拷贝 75 | if (fs.existsSync(dest)) return; 76 | 77 | // 创建文件夹 78 | if (isDir(source)) fs.ensureDirSync(dest); 79 | 80 | fs.copySync(source, dest); 81 | }); 82 | 83 | // 如果 src 为空,则拷贝小程序模版文件 84 | if (isEmptySrc) { 85 | fs.copySync(path.resolve(TEMPLATE_DIR, 'src'), path.resolve(ROOT, 'src')); 86 | } 87 | 88 | utils.log('文件添加完成', 'success'); 89 | 90 | // 执行安装流程 91 | install(ROOT, 'npm start'); 92 | }; 93 | -------------------------------------------------------------------------------- /packages/ewa/src/utils/Queue.js: -------------------------------------------------------------------------------- 1 | class Queue { 2 | constructor(concurrency) { 3 | // 队列, 用数组来代替队列 4 | this._queue = []; 5 | 6 | // 并发数 7 | this._concurrency = concurrency || 8; 8 | 9 | // 当前并发数 10 | this._pendingCount = 0; 11 | } 12 | 13 | get concurrency() { 14 | return this._concurrency; 15 | } 16 | 17 | set concurrency(num) { 18 | this._concurrency = num; 19 | while (this._canProcessNext()) this._processQueue(); 20 | } 21 | 22 | // 队列长度 23 | get size() { 24 | return this._queue.length; 25 | } 26 | 27 | // 队列是否为空 28 | isEmpty() { 29 | return this.size === 0; 30 | } 31 | 32 | _canProcessNext() { 33 | return this.size > 0 && this._pendingCount < this._concurrency; 34 | } 35 | 36 | // 处理队列 37 | _processQueue() { 38 | // 当前并发数大于最大并发数时, 不做任何操作 39 | if (!this._canProcessNext()) return Promise.resolve(); 40 | 41 | // 执行队列下一个请求 42 | let next = this._queue.shift(); 43 | 44 | // 如果队列为空,则什么都不做 45 | if (!next) return Promise.resolve(); 46 | 47 | // pending +1 48 | this._pendingCount++; 49 | 50 | // 处理队列 51 | try { 52 | return Promise.resolve(next()).then( 53 | () => this._handleNext() 54 | ).catch((e) => { 55 | this._handleError(e, next); 56 | return this._handleNext(); 57 | }); 58 | } catch (e) { 59 | this._handleError(e, next); 60 | return this._handleNext(); 61 | } 62 | } 63 | 64 | _handleNext() { 65 | // pending -1 66 | this._pendingCount--; 67 | 68 | // 继续处理队列 69 | return this._processQueue(); 70 | } 71 | 72 | _handleError(e, fn) { 73 | const logError = (err = '') => { 74 | // eslint-disable-next-line 75 | console.log(`Error on executing ${fn.name}: ${err.message}`); 76 | }; 77 | 78 | // 仅做错误信息打印,如需要做其他操作,可以覆盖 onError 方法 79 | if (typeof this.onError !== 'function') return logError(e); 80 | 81 | try { 82 | Promise.resolve(this.onError(e, fn)).catch(logError); 83 | } catch (err) { 84 | logError(err); 85 | } 86 | } 87 | 88 | // 加入队列 89 | enqueue(fn) { 90 | if (typeof fn !== 'function') throw new Error(`${fn} is not a valid function`); 91 | 92 | // 添加到队列 93 | this._queue.push(fn); 94 | 95 | // 尝试触发队列 96 | this._processQueue(); 97 | 98 | return this; 99 | } 100 | 101 | // enqueue 方法别名 102 | push(fn) { 103 | return this.enqueue(fn); 104 | } 105 | 106 | // enqueue 方法别名 107 | add(fn) { 108 | return this.enqueue(fn); 109 | } 110 | } 111 | 112 | module.exports = Queue; 113 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [1.2.4](https://github.com/lyfeyaj/ewa/compare/v1.2.3...v1.2.4) (2021-07-06) 7 | 8 | 9 | ### Bug Fixes 10 | 11 | * **ewa-cli:** 修复因为路径中包含空格导致脚本执行报错的问题 ([264cb9a](https://github.com/lyfeyaj/ewa/commit/264cb9a3ed76fcc0be2516b12bd561cf910be4fa)) 12 | * **ewa-webpack:** 修复因为路径中包含空格导致脚本执行报错的问题 ([62c1e4f](https://github.com/lyfeyaj/ewa/commit/62c1e4fda10f8d46f6a6019400e2dbf37f9e6e81)) 13 | 14 | 15 | 16 | 17 | 18 | ## [1.2.3](https://github.com/lyfeyaj/ewa/compare/v1.2.2...v1.2.3) (2021-07-01) 19 | 20 | 21 | ### Bug Fixes 22 | 23 | * **ewa-webpack:** 修复 wxml 绕过缓存的问题 ([624f13e](https://github.com/lyfeyaj/ewa/commit/624f13e52992c7b6e988a3bc4687f9cdd2ce4475)) 24 | 25 | 26 | 27 | 28 | 29 | ## [1.2.2](https://github.com/lyfeyaj/ewa/compare/v1.2.0...v1.2.2) (2021-07-01) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * **ewa-cli:** fix for [#49](https://github.com/lyfeyaj/ewa/issues/49) ([9ba99a2](https://github.com/lyfeyaj/ewa/commit/9ba99a2e9094174a5d275dcfe4b3171c1c129af9)) 35 | * **ewa-webpack:** 修复 wxml 中的 < 或者 > 可能导致解析报错的问题 ([83d8bda](https://github.com/lyfeyaj/ewa/commit/83d8bda4180b8709976a5a8c13602318fee02223)) 36 | 37 | 38 | 39 | 40 | 41 | ## [1.2.1](https://github.com/lyfeyaj/ewa/compare/v1.2.0...v1.2.1) (2021-06-21) 42 | 43 | 44 | ### Bug Fixes 45 | 46 | * **ewa-cli:** fix for [#49](https://github.com/lyfeyaj/ewa/issues/49) ([9ba99a2](https://github.com/lyfeyaj/ewa/commit/9ba99a2e9094174a5d275dcfe4b3171c1c129af9)) 47 | 48 | 49 | 50 | 51 | 52 | # [1.2.0](https://github.com/lyfeyaj/ewa/compare/v1.1.0...v1.2.0) (2021-05-21) 53 | 54 | 55 | ### Bug Fixes 56 | 57 | * **ewa:** 修复变量引用错误 ([b6be282](https://github.com/lyfeyaj/ewa/commit/b6be2827fe1f12f478dc17db155ed54dd5115a80)) 58 | * **ewa-webpack:** 修复 ts 支持,以及入口文件自动忽略 .d.ts 文件 ([0dd8922](https://github.com/lyfeyaj/ewa/commit/0dd89224d53db452e58a190badfce0eff215d6c8)) 59 | 60 | 61 | ### Features 62 | 63 | * **ewa:** 允许 createStore 自定义注入的方法和属性名称 ([9215776](https://github.com/lyfeyaj/ewa/commit/92157769e07a562006d889726573b201f59a42cc)) 64 | 65 | 66 | 67 | 68 | 69 | # [1.1.0](https://github.com/lyfeyaj/ewa/compare/v1.0.10...v1.1.0) (2021-04-26) 70 | 71 | 72 | ### Bug Fixes 73 | 74 | * **ewa-cli:** fix ewa-webpack dependency version ([71d937f](https://github.com/lyfeyaj/ewa/commit/71d937f4f6476971ce48dd21cc48eec41dc9a89b)) 75 | 76 | 77 | ### Features 78 | 79 | * **ewa-cli:** update templates for custom environments and update deps ([9aae389](https://github.com/lyfeyaj/ewa/commit/9aae389cf68107215d011cebda846f2fc37a02ed)) 80 | * **ewa-webpack:** add custom environments support ([2e17f6a](https://github.com/lyfeyaj/ewa/commit/2e17f6a82d01ada675ca076e115faf5ddb56ed8e)) 81 | -------------------------------------------------------------------------------- /packages/cli/lib/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* eslint "no-console": "off" */ 4 | 5 | 'use strict'; 6 | 7 | const BUILD_TARGET_TYPES_CONFIG = { 8 | alias: 't', 9 | describe: '构建目标 `weapp` 或 `swan` 或 `alipay` 或 `tt` 或 `qq`', 10 | type: 'string', 11 | choices: ['weapp', 'swan', 'alipay', 'tt', 'qq'], 12 | default: 'weapp', 13 | demandOption: false 14 | }; 15 | 16 | // 命令行配置 17 | require('yargs') 18 | .locale('zh_CN') 19 | .usage('$0 [args]') 20 | .command(['new ', 'create'], '创建新的微信小程序项目', (yargs) => { 21 | yargs 22 | .positional('projectName', { 23 | describe: '项目名称' 24 | }); 25 | }, (argv) => { 26 | require('./commands/create')(argv); 27 | }) 28 | .command('init', '在现有的小程序项目中初始化 EWA', {}, (argv) => { 29 | require('./commands/init')(argv); 30 | }) 31 | .command(['start', 'dev'], '启动 EWA 小程序项目实时编译', (yargs) => { 32 | yargs.option('type', BUILD_TARGET_TYPES_CONFIG); 33 | }, (argv) => { 34 | require('./commands/start')(argv.type); 35 | }) 36 | .command('build', '编译小程序静态文件', (yargs) => { 37 | yargs.option('type', BUILD_TARGET_TYPES_CONFIG); 38 | }, (argv) => { 39 | require('./commands/build')(argv.type); 40 | }) 41 | .command('clean', '清理小程序静态文件', (yargs) => { 42 | yargs.option('type', BUILD_TARGET_TYPES_CONFIG); 43 | }, (argv) => { 44 | require('./commands/clean')(argv.type); 45 | }) 46 | .command('upgrade', '升级 EWA 工具', {}, (argv) => { 47 | require('./commands/upgrade')(argv); 48 | }) 49 | .command(['generate ', 'g'], '快速生成模版', (yargs) => { 50 | yargs 51 | .positional('type', { 52 | describe: '类型 `page` 或 `component` 或 `template`', 53 | choices: ['page', 'component', 'template'], 54 | type: 'string' 55 | }).positional('name', { 56 | describe: '名称', 57 | type: 'string' 58 | }).option('target-dir', { 59 | alias: 'd', 60 | describe: '目标文件夹,默认为 src,也可以指定为 src 中的某个子目录', 61 | type: 'string', 62 | demandOption: false 63 | }).option('index', { 64 | alias: 'i', 65 | describe: '生成的文件名称为 [name]/index,默认为 [name]/[name]', 66 | type: 'boolean', 67 | demandOption: false 68 | }); 69 | }, (argv) => { 70 | require('./commands/generate')( 71 | argv.type, 72 | argv.name, 73 | argv.targetDir, 74 | argv.index 75 | ); 76 | }) 77 | .options({ 78 | version: { 79 | alias: 'v', 80 | describe: '当前版本号' 81 | } 82 | }) 83 | .help('help', '获取使用帮助') 84 | .alias('help', 'h') 85 | .default('help') 86 | .demandCommand(1, '请输入命令来启动相应的功能') 87 | .fail(function (msg, err, yargs) { 88 | if (err) throw err; 89 | 90 | console.error('出错啦!'); 91 | console.error(msg); 92 | console.error('\n用法: ', yargs.help()); 93 | 94 | process.exit(1); 95 | }) 96 | .strict(true) 97 | .argv; -------------------------------------------------------------------------------- /packages/webpack/lib/rules/css.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 4 | 5 | // 解析并抽离 wxss 文件 6 | module.exports = function cssRule(options = {}) { 7 | let cssPattern; 8 | let cssRules = []; 9 | let cssExtensions = ['.css', '.wxss']; 10 | if (options.cssParser === 'sass') { 11 | cssPattern = /\.(css|scss|sass|wxss)$/; 12 | cssRules = [ 13 | { loader: 'resolve-url-loader' }, 14 | { 15 | loader: './loaders/fix-import-wxss-loader.js', 16 | options: { type: options.EWA_ENV } 17 | }, 18 | { 19 | loader: 'sass-loader', 20 | options: { 21 | // resolve-url-loader 需要开启 sourceMap 才能工作 22 | sourceMap: true, 23 | // NOTE: sass-loader 7.3.0 版本开始 24 | // 生产环境会默认修改为 compressed 25 | // 这会导致地址转换失败,这里强制为 expanded 26 | // 压缩的任务,交给 postcss 27 | sassOptions: { 28 | outputStyle: 'expanded' 29 | } 30 | } 31 | }, 32 | { 33 | loader: './loaders/import-wxss-loader.js', 34 | options: { simplifyPath: options.simplifyPath } 35 | } 36 | ]; 37 | cssExtensions = cssExtensions.concat(['.scss', '.sass']); 38 | } else if (options.cssParser === 'less') { 39 | cssPattern = /\.(css|less|wxss)$/; 40 | cssRules = [ 41 | { loader: './loaders/fix-import-wxss-loader.js' }, 42 | { loader: 'less-loader', options: { sourceMap: true } }, 43 | { 44 | loader: './loaders/import-wxss-loader.js', 45 | options: { simplifyPath: options.simplifyPath } 46 | } 47 | ]; 48 | cssExtensions = cssExtensions.concat(['.less']); 49 | } 50 | 51 | // PostCSS 插件 52 | let postCssPlugins = [ 53 | require('autoprefixer')({ remove: false, overrideBrowserslist: ['iOS 7']}) 54 | ]; 55 | if (!options.IS_DEV) { 56 | postCssPlugins.push( 57 | require('cssnano')({ 58 | preset: [ 59 | 'default', 60 | { 61 | discardComments: { removeAll: true }, 62 | // calc 无法计算 rpx,此处禁止 63 | calc: false 64 | } 65 | ] 66 | }) 67 | ); 68 | } 69 | 70 | // 构建 CSS rule 71 | cssRules = [ 72 | { 73 | loader: 'css-loader', 74 | options: { 75 | // 不处理 css 的 @import 76 | // 充分利用小程序 wxss 本身的 @import 77 | // 降低 css 的重复合并,降低样式文件大小 78 | import: false, 79 | esModule: false 80 | } 81 | }, 82 | // PostCSS 配置 83 | { 84 | loader: 'postcss-loader', 85 | options: { 86 | postcssOptions: { 87 | plugins: postCssPlugins 88 | } 89 | } 90 | } 91 | ].concat(cssRules); 92 | 93 | // 开启 cache 94 | if (options.cache) cssRules = ['cache-loader'].concat(cssRules); 95 | 96 | return { 97 | cssRule: { 98 | test: cssPattern, 99 | use: ExtractTextPlugin.extract({ 100 | fallback: 'style-loader', 101 | use: cssRules 102 | }) 103 | }, 104 | cssExtensions 105 | }; 106 | }; -------------------------------------------------------------------------------- /packages/webpack/lib/run.dev.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint no-console: "off" */ 4 | 5 | const nodemon = require('nodemon'); 6 | const chokidar = require('chokidar'); 7 | const path = require('path'); 8 | const glob = require('glob'); 9 | const fs = require('fs'); 10 | const utils = require('./utils'); 11 | 12 | const ROOT = process.cwd(); 13 | 14 | const FAKE_WATCH_DIR = path.resolve(ROOT, '.ewa') + '/'; 15 | 16 | const CONFIG_FILE = path.resolve(__dirname, 'config.js'); 17 | 18 | // 需要监听的文件夹 19 | const WATCH_PATTERNS = [ 20 | 'pages', 21 | 'components', 22 | 'templates', 23 | 'packages' 24 | ].map(dir => { 25 | return path.resolve(ROOT, `src/${dir}/**/*.{ts,js,wxml,wxss,json,wxs}`); 26 | }); 27 | 28 | module.exports = function(webpack) { 29 | // nodemon 实例 30 | const script = nodemon({ 31 | exec: `${webpack} --config "${CONFIG_FILE}" --watch`, 32 | watch: [FAKE_WATCH_DIR], 33 | ext: 'js' 34 | }); 35 | 36 | // 添加文件夹监听 37 | const watcher = chokidar.watch( 38 | WATCH_PATTERNS.concat(FAKE_WATCH_DIR), 39 | { 40 | ignored: /(^|[/\\])\../, 41 | persistent: true 42 | } 43 | ); 44 | 45 | // 额外添加文件夹监听 46 | // 遍历所有pattern,搜集所有的文件夹 47 | let watchedDirs = []; 48 | WATCH_PATTERNS.map(pattern => { 49 | glob.sync(pattern).map(file => { 50 | let dir = fs.statSync(file).isDirectory() ? file : path.dirname(file); 51 | if (watchedDirs.indexOf(dir) === -1) { 52 | watchedDirs.push(dir); 53 | watcher.add(dir); 54 | } 55 | }); 56 | }); 57 | 58 | function restart(file) { 59 | script.emit('restart', [file]); 60 | } 61 | 62 | // 监听文件夹 63 | function addDir(file) { 64 | // 监听文件夹 65 | if (fs.statSync(file).isFile()) { 66 | let dir = path.dirname(file); 67 | if (watchedDirs.indexOf(dir) === -1) { 68 | utils.log(`Watching directory: ${path.relative(ROOT, dir)}`); 69 | watchedDirs.push(dir); 70 | watcher.add(dir); 71 | } 72 | } 73 | } 74 | 75 | // 去除监听文件夹 76 | function unlinkDir(dir) { 77 | utils.log(`Watching directory deleted: ${path.relative(ROOT, dir)}`); 78 | 79 | let index = watchedDirs.indexOf(dir); 80 | if (index !== -1) { 81 | watchedDirs.splice(index, 1); 82 | } 83 | 84 | restart(); 85 | } 86 | 87 | // Delay 5 seconds watching target files 88 | setTimeout(() => { 89 | watcher 90 | .on('add', file => { 91 | utils.log(`Watching file: ${path.relative(ROOT, file)}`); 92 | restart(); 93 | }) 94 | .on('add', addDir) 95 | .on('unlink', file => { 96 | utils.log(`Watching file deleted: ${path.relative(ROOT, file)}`); 97 | restart(); 98 | }) 99 | .on('unlinkDir', unlinkDir); 100 | }, 5000); 101 | 102 | // Capture ^C 103 | process.once('SIGINT', function() { 104 | script.emit('quit', 130); 105 | }); 106 | 107 | script.on('quit', function() { 108 | process.exit(0); 109 | }); 110 | 111 | // Forward log messages and stdin 112 | script.on('log', function(log) { 113 | utils.log(log.colour); 114 | }); 115 | }; 116 | -------------------------------------------------------------------------------- /packages/webpack/lib/plugins/NodeCommonModuleTemplatePlugin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Template = require('webpack/lib/Template'); 4 | const path = require('path'); 5 | 6 | // Constants 7 | const DEFAULT_COMMON_MODULE_NAME = 'vendors.js'; 8 | 9 | const GLOBAL_CACHED_VAR = 'g.$x$'; 10 | 11 | const VENDOR_MODULE_NAME = '$EWA_MODULES'; 12 | 13 | // 判断是否为 js 或 ts 文件 14 | function isJsFile(name) { 15 | return /\.(js|ts)$/.test(name || ''); 16 | } 17 | 18 | // Inject common module into entry files 19 | module.exports = class NodeCommonModuleTemplatePlugin { 20 | constructor(options = {}) { 21 | this.options = Object.assign({ 22 | commonModuleName: DEFAULT_COMMON_MODULE_NAME 23 | }, options); 24 | } 25 | 26 | apply(compiler) { 27 | const { OUTPUT_GLOBAL_OBJECT } = this.options; 28 | 29 | compiler.hooks.thisCompilation.tap('NodeCommonModuleTemplatePlugin', compilation => { 30 | const mainTemplate = compilation.mainTemplate; 31 | 32 | // 干预编译的 render 阶段, 在 js 头部插入公共模块引用 33 | mainTemplate.hooks.render.tap('NodeCommonModuleTemplatePlugin', (source, chunk) => { 34 | if (!isJsFile(chunk.name)) return source; 35 | 36 | // 计算 vendor 的相对位置 37 | let vendorPath = path.relative(path.dirname(chunk.name), this.options.commonModuleName); 38 | 39 | // remove js ext for saving dist space 40 | vendorPath = vendorPath.replace(/\.js$/, '').replace(/\\/g, '/'); 41 | 42 | // 转换地址, vendor.js => ./vendor.js 43 | if (!/^((\.\/)|(\.\.\/))/.test(vendorPath)) vendorPath = `./${vendorPath}`; 44 | 45 | // 插入到 js 文件最前阿敏 46 | if (source && source.children) source.children.unshift(`var ${VENDOR_MODULE_NAME} = require('${vendorPath}');\n`); 47 | 48 | return source; 49 | }); 50 | 51 | mainTemplate.hooks.bootstrap.tap('NodeCommonModuleTemplatePlugin', (source, chunk) => { 52 | if (!isJsFile(chunk.name)) return source; 53 | 54 | return Template.asString([ 55 | source, 56 | '', 57 | 'var freeGlobal = typeof global == "object" && global && global.Object === Object && global;', 58 | 'var freeSelf = typeof self == "object" && self && self.Object === Object && self;', 59 | `var g = freeGlobal || freeSelf || ${OUTPUT_GLOBAL_OBJECT} || {};`, 60 | '', 61 | '// require common modules', 62 | '(function loadVendorModules() {', 63 | Template.indent([ 64 | `var extraModules = { __proto__: ${VENDOR_MODULE_NAME}.modules || {} };`, 65 | 'for (var name in modules) {', 66 | Template.indent([ 67 | 'if (!extraModules[name]) extraModules[name] = modules[name];' 68 | ]), 69 | '}', 70 | 'modules = extraModules;' 71 | ]), 72 | '})();', 73 | 74 | // Add global cachedInstalledModules 75 | '', 76 | `${GLOBAL_CACHED_VAR} = ${GLOBAL_CACHED_VAR} || {};` 77 | ]); 78 | }); 79 | 80 | // Add global cachedInstalledModules 81 | mainTemplate.hooks.localVars.tap('NodeCommonModuleTemplatePlugin', (source, chunk) => { 82 | if (!isJsFile(chunk.name)) return source; 83 | 84 | return Template.asString([ 85 | source, 86 | '// replace with global cache', 87 | `installedModules = ${GLOBAL_CACHED_VAR} || {};` 88 | ]); 89 | }); 90 | }); 91 | } 92 | }; 93 | -------------------------------------------------------------------------------- /packages/ewa/src/plugins/createStore/index.js: -------------------------------------------------------------------------------- 1 | const isPlainObject = require('lodash.isplainobject'); 2 | const injectStore = require('./injectStore'); 3 | const reactive = require('./reactive'); 4 | const observer = require('./Observer').getInstance(); 5 | 6 | // 确保仅初始化一次 7 | let hasStore = false; 8 | 9 | /** 10 | * 创建响应式 Store 11 | * 12 | * @param {Object} obj - store 对象 13 | * @param {Object} propNames - 自定义属性或方法名称 14 | * @param {string} propNames.$set - 自定义 $set 方法名称 15 | * @param {string} propNames.$on - 自定义 $on 方法名称 16 | * @param {string} propNames.$emit - 自定义 $emit 方法名称 17 | * @param {string} propNames.$off - 自定义 $off 方法名称 18 | * @param {string} propNames.$once - 自定义 $once 方法名称 19 | * @param {string} propNames.$watch - 自定义 $watch 属性名称 20 | * @returns {Object} 响应式 store 对象 21 | * @example 22 | * 使用方法: 23 | * 1. 创建store:对任意纯对象调用 createStore 使其响应式化(以 app.js 中 globalData 为例) 24 | * // app.js 25 | * const { createStore } = require('ewa'); 26 | * ... 27 | * App({ 28 | * ... 29 | * globalData: createStore ({ 30 | * a: 'old1', 31 | * b: { 32 | * c: 'old2' 33 | * } 34 | * }, { 35 | * $set: 'yourCustomSet', 36 | * $on: 'yourCustomOn', 37 | * $emit: 'yourCustomEmit', 38 | * $off: 'yourCustomOff', 39 | * $once: 'yourCustomOnce', 40 | * $watch: 'yourCustomWatch' 41 | * }) 42 | * }) 43 | * 44 | * 如果使用了自定义的属性名称,则下方示例中对应的方法名称需要做相应的修改 45 | * 46 | * 2. 改变 globalData, globalData 以及全局状态更新(支持嵌套属性和数组下标修改) 47 | * // pageA.js 48 | * Page({ 49 | * data: { 50 | * a: '', 51 | * b: { 52 | * c: '' 53 | * } 54 | * } 55 | * }) 56 | * 57 | * onLoad() { 58 | * App().globalData.a = 'new1' 59 | * console.log(this.data.a === 'new1') // true 60 | * 61 | * App().globalData.b.c = 'new2' 62 | * console.log(this.data.b.c === 'new2') // true 63 | * } 64 | * 65 | * 3. 注入全局方法 使用示例: 66 | * this.$on('test', (val) => { console.log(val) }) 67 | * 68 | * this.$emit('test', 'value') // 'value' 69 | * 70 | * this.$once 使用方法同 this.$on 只会触发一次 71 | * 72 | * this.$off('test') 解绑当前实例通过 this.$on(...) 注册的事件 73 | * 74 | * 以上方法适用于 1.页面与页面 2.页面与组件 3.组件与组件 75 | * 注: 所有页面或组件销毁时会自动解绑所有的事件(无需使用this.$off(...)) 76 | * 77 | * this.$set('coinName', '金币') 更新所有页面和组件data中'coinName'的值为'金币'(支持嵌套属性和数组下标修改) 78 | * 79 | * 2020/07 更新 80 | * $watch 监听页面或组件data中属性 支持监听属性路径形式如'a[1].b' 使用示例: 81 | * 82 | * data: { 83 | * prop: '', 84 | * obj: { 85 | * key: '' 86 | * } 87 | * }, 88 | * ... 89 | * $watch: { 90 | * // 方式一 91 | * 'prop': function(newVal, oldVal) { 92 | * ... 93 | * }, 94 | * // 方式二 95 | * 'obj': { 96 | * handler: function(newVal, oldVal) { 97 | * ... 98 | * }, 99 | * deep: Boolean, // 深度遍历 100 | * immediate: Boolean // 立即触发 101 | * } 102 | * } 103 | */ 104 | function createStore(obj, propNames = {}) { 105 | if (hasStore) return; 106 | hasStore = true; 107 | 108 | // 初始化 109 | injectStore(propNames); 110 | 111 | if (isPlainObject(obj)) { 112 | observer.reactiveObj = obj; 113 | reactive(obj); 114 | return obj; 115 | } 116 | 117 | if (console && console.warn) console.warn('createStore 方法只能接收纯对象,请尽快调整'); 118 | } 119 | 120 | module.exports = createStore; 121 | -------------------------------------------------------------------------------- /packages/ewa/src/plugins/createStore/Watcher.js: -------------------------------------------------------------------------------- 1 | const isFunction = require('lodash.isfunction'); 2 | const isObject = require('lodash.isobject'); 3 | const get = require('lodash.get'); 4 | const Observer = require('./Observer'); 5 | 6 | const obInstance = Observer.getInstance(); 7 | 8 | let uid = 0; 9 | 10 | class Watcher { 11 | /** 12 | * 13 | * @param {Page | Component} ctx 上下文环境,小程序 Page 或者 Component 实例 14 | * @param {Object} options 参数 15 | * @param {String} options.watchPropName 监听数据自定义属性名称 16 | */ 17 | constructor(ctx, options = {}) { 18 | const { watchPropName = '$watch' } = options; 19 | 20 | // 执行环境 21 | this.ctx = ctx; 22 | 23 | // data数据 24 | this.$data = ctx.data || {}; 25 | 26 | // $watch数据 27 | this.$watch = ctx[watchPropName] || {}; 28 | 29 | // 更新函数 30 | this.updateFn = ctx.setState || ctx.setData; 31 | 32 | // watcherId 33 | this.id = ++uid; 34 | 35 | // 收集data和globalData的交集作为响应式对象 36 | this.reactiveData = {}; 37 | 38 | // 初始化操作 39 | this.initReactiveData(); 40 | this.createObserver(); 41 | this.setCustomWatcher(); 42 | 43 | // 收集watcher 44 | obInstance.setGlobalWatcher(this); 45 | } 46 | 47 | // 初始化数据并首次更新 48 | initReactiveData() { 49 | const { reactiveObj } = obInstance; 50 | Object.keys(this.$data).forEach((key) => { 51 | if (key in reactiveObj) { 52 | this.reactiveData[key] = reactiveObj[key]; 53 | this.update(key, reactiveObj[key]); 54 | } 55 | }); 56 | } 57 | 58 | // 添加订阅 59 | createObserver() { 60 | Object.keys(this.reactiveData).forEach((key) => { 61 | obInstance.onReactive(key, this); 62 | }); 63 | } 64 | 65 | // 初始化收集自定义watcher 66 | setCustomWatcher() { 67 | const watch = this.$watch; 68 | /* $watch为一个对象,键是需要观察的属性名或带参数的路径,值是对应回调函数,值也可以是包含选项的对象, 69 | 其中选项包括 {function} handler {boolean} deep {boolean} immediate 70 | 回调函数中参数分别为新值和旧值 71 | $watch: { 72 | 'key': function(newVal, oldVal) {}, 73 | 'obj.key': { 74 | handler: function(newVal, oldVal) {}, 75 | deep: true, 76 | immediate: true 77 | } 78 | } */ 79 | Object.keys(watch).forEach((key) => { 80 | // 记录参数路径 81 | const keyArr = key.split('.'); 82 | let obj = this.$data; 83 | for (let i = 0; i < keyArr.length - 1; i++) { 84 | if (!obj) return; 85 | obj = get(obj, keyArr[i]); 86 | } 87 | if (!obj) return; 88 | const property = keyArr[keyArr.length - 1]; 89 | // 兼容两种回调函数的形式 90 | const cb = watch[key].handler || watch[key]; 91 | // deep参数 支持对象/数组深度遍历 92 | const deep = watch[key].deep; 93 | this.reactiveWatcher(obj, property, cb, deep); 94 | // immediate参数 支持立即触发回调 95 | if (watch[key].immediate) this.handleCallback(cb, obj[property]); 96 | }); 97 | } 98 | 99 | // 响应式化自定义watcher 100 | reactiveWatcher(obj, key, cb, deep) { 101 | let val = obj[key]; 102 | // 如果需要深度监听 递归调用 103 | if (isObject(val) && deep) { 104 | Object.keys(val).forEach((childKey) => { 105 | this.reactiveWatcher(val, childKey, cb, deep); 106 | }); 107 | } 108 | Object.defineProperty(obj, key, { 109 | enumerable: true, 110 | configurable: true, 111 | get: () => val, 112 | set: (newVal) => { 113 | if (newVal === val) return; 114 | // 触发回调函数 115 | this.handleCallback(cb, newVal, val); 116 | val = newVal; 117 | // 如果深度监听 重新监听该对象 118 | if (deep) this.reactiveWatcher(obj, key, cb, deep); 119 | }, 120 | }); 121 | } 122 | 123 | // 执行自定义watcher回调 124 | handleCallback(cb, newVal, oldVal) { 125 | if (!isFunction(cb) || !this.ctx) return; 126 | try { 127 | cb.call(this.ctx, newVal, oldVal); 128 | } catch (e) { 129 | console.warn(`[$watch error]: callback for watcher \n ${cb} \n`, e); 130 | } 131 | } 132 | 133 | // 移除订阅 134 | removeObserver() { 135 | // 移除相关依赖并释放内存 136 | obInstance.removeReactive(Object.keys(this.reactiveData), this.id); 137 | obInstance.removeEvent(this.id); 138 | obInstance.removeWatcher(this.id); 139 | } 140 | 141 | // 更新数据和视图 142 | update(key, value) { 143 | if (isFunction(this.updateFn) && this.ctx) { 144 | this.updateFn.call(this.ctx, { [key]: value }); 145 | } 146 | } 147 | } 148 | 149 | module.exports = Watcher; 150 | -------------------------------------------------------------------------------- /packages/ewa/src/plugins/createStore/injectStore.js: -------------------------------------------------------------------------------- 1 | const Watcher = require('./Watcher'); 2 | const observer = require('./Observer').getInstance(); 3 | 4 | function noop() {} 5 | 6 | // 警告日志打印 7 | function warn(...messages) { 8 | if (console && console.warn) console.warn(...messages); 9 | } 10 | 11 | // 检查方法名是否冲突 12 | function checkExistedProps(ctx, methodsArr) { 13 | let noConflicts = true; 14 | methodsArr.forEach((fn) => { 15 | if (fn in ctx) { 16 | noConflicts = false; 17 | warn(`${fn} 属性或方法将被覆盖, 请尽快调整。`); 18 | } 19 | if (noConflicts) return; 20 | warn(` 21 | 也可以通过指定属性或函数名称,如下: 22 | 23 | createStore({}, { 24 | $set: 'yourCustomSet', 25 | $on: 'yourCustomOn', 26 | $emit: 'yourCustomEmit', 27 | $off: 'yourCustomOff', 28 | $once: 'yourCustomOnce', 29 | $watch: 'yourCustomWatch' 30 | }) 31 | 32 | 来避免冲突。 33 | `); 34 | }); 35 | } 36 | 37 | // 注入接口方法 38 | const DEFAULT_PROP_NAMES = { 39 | $set: '$set', 40 | $on: '$on', 41 | $emit: '$emit', 42 | $off: '$off', 43 | $once: '$once', 44 | $watch: '$watch', 45 | }; 46 | 47 | // 注入方法和属性, 支持自定义方法名称 48 | const injectStoreMethods = (ctx, propNames = {}) => { 49 | // 获取自定义属性名称 50 | const _propNames = { ...DEFAULT_PROP_NAMES, ...propNames }; 51 | 52 | // 检查方法 53 | checkExistedProps(ctx, Object.keys(_propNames)); 54 | 55 | // 手动更新全局data 56 | ctx[_propNames.$set] = function (key, value) { 57 | observer.handleUpdate(key, value); 58 | }; 59 | 60 | // 添加注册事件函数 61 | ctx[_propNames.$on] = function (key, callback) { 62 | if (this.__watcher && this.__watcher.id) { 63 | observer.onEvent(key, callback, ctx, this.__watcher.id); 64 | } 65 | }; 66 | 67 | // 添加通知更新函数 68 | ctx[_propNames.$emit] = function (key, obj) { 69 | observer.emitEvent(key, obj); 70 | }; 71 | 72 | // 添加解绑事件函数 73 | ctx[_propNames.$off] = function (key) { 74 | if (this.__watcher && this.__watcher.id) { 75 | observer.off(key, this.__watcher.id); 76 | } 77 | }; 78 | 79 | // 添加执行一次事件函数 80 | ctx[_propNames.$once] = function (key, callback) { 81 | if (this.__watcher && this.__watcher.id) { 82 | observer.once(key, callback, this.__watcher.id); 83 | } 84 | }; 85 | }; 86 | 87 | // 初始化 store 88 | function initStore(propNames = {}) { 89 | // 获取自定义 $watch 名称 90 | const watchPropName = propNames.$watch; 91 | 92 | // 注入方法和属性到 Page 和 Component 中 93 | // NOTE: 优化运行时,避免使用这种覆盖的方式,会导致污染 94 | try { 95 | const oPage = Page; 96 | Page = function (obj = {}) { 97 | const _onLoad = obj.onLoad || noop; 98 | const _onUnload = obj.onUnload || noop; 99 | 100 | obj.onLoad = function () { 101 | // 页面初始化添加watcher 102 | if (!this.__watcher || !(this.__watcher instanceof Watcher)) { 103 | this.__watcher = new Watcher(this, { watchPropName }); 104 | } 105 | // 注入内置函数 106 | injectStoreMethods(this, propNames); 107 | return _onLoad.apply(this, arguments); 108 | }; 109 | obj.onUnload = function () { 110 | _onUnload.apply(this, arguments); 111 | // 页面销毁时移除 watcher 112 | if (this.__watcher && (this.__watcher instanceof Watcher)) { 113 | this.__watcher.removeObserver(); 114 | } 115 | }; 116 | 117 | return oPage(obj); 118 | }; 119 | 120 | const oComponent = Component; 121 | Component = function (obj = {}) { 122 | obj.lifetimes = obj.lifetimes || {}; 123 | const _attached = obj.lifetimes.attached || obj.attached || noop; 124 | const _detached = obj.lifetimes.detached || obj.detached || noop; 125 | 126 | obj.lifetimes.attached = obj.attached = function () { 127 | // 组件初始化添加 watcher 兼容 $watch 属性 128 | if (!this.__watcher || !(this.__watcher instanceof Watcher)) { 129 | this[watchPropName] = obj[watchPropName]; 130 | this.__watcher = new Watcher(this, { watchPropName }); 131 | } 132 | // 注入内置函数 133 | injectStoreMethods(this, propNames); 134 | return _attached.apply(this, arguments); 135 | }; 136 | obj.lifetimes.detached = obj.detached = function () { 137 | _detached.apply(this, arguments); 138 | // 页面销毁时移除watcher 139 | if (this.__watcher && (this.__watcher instanceof Watcher)) { 140 | this.__watcher.removeObserver(); 141 | } 142 | }; 143 | 144 | return oComponent(obj); 145 | }; 146 | } catch (e) { 147 | warn('覆盖小程序 Page 或 Component 出错', e.message, e.stack); 148 | } 149 | } 150 | 151 | module.exports = initStore; 152 | -------------------------------------------------------------------------------- /packages/ewa/src/plugins/createStore/Observer.js: -------------------------------------------------------------------------------- 1 | const isFunction = require('lodash.isfunction'); 2 | const get = require('lodash.get'); 3 | const set = require('lodash.set'); 4 | const has = require('lodash.has'); 5 | 6 | class Observer { 7 | constructor() { 8 | // 初始化响应式对象 9 | this.reactiveObj = {}; 10 | 11 | // 响应式对象集合 12 | this.reactiveBus = {}; 13 | 14 | // 自定义事件集合 15 | this.eventBus = {}; 16 | 17 | // 全局watcher集合 18 | this.globalWatchers = []; 19 | } 20 | 21 | // 获取唯一实例 22 | static getInstance() { 23 | if (!this.instance) { 24 | this.instance = new Observer(); 25 | } 26 | return this.instance; 27 | } 28 | 29 | // 收集全局 watcher 30 | setGlobalWatcher(obj) { 31 | if (!this.isExistSameId(this.globalWatchers, obj.id)) this.globalWatchers.push(obj); 32 | } 33 | 34 | // 收集响应式数据 35 | onReactive(key, obj) { 36 | if (!this.reactiveBus[key]) this.reactiveBus[key] = []; 37 | if (!this.isExistSameId(this.reactiveBus[key], obj.id)) this.reactiveBus[key].push(obj); 38 | } 39 | 40 | // 收集自定义事件 41 | onEvent(key, callback, ctx, watcherId) { 42 | if (!this.eventBus[key]) this.eventBus[key] = []; 43 | if (this.isExistSameId(this.eventBus[key], watcherId)) { 44 | if (console && console.warn) console.warn(`自定义事件 '${key}' 无法重复添加,请尽快调整`); 45 | } else { 46 | this.eventBus[key].push(this.toEventObj(watcherId, callback.bind(ctx))); 47 | } 48 | } 49 | 50 | // 收集仅执行一次事件 51 | once(key, callback, watcherId) { 52 | // 创建一个调用后立即解绑函数 53 | const wrapFanc = (args) => { 54 | callback(args); 55 | this.off(key, watcherId); 56 | }; 57 | this.onEvent(key, wrapFanc, watcherId); 58 | } 59 | 60 | // 转为eventBus对象 61 | toEventObj(id, callback) { 62 | return { 63 | id, 64 | callback, 65 | }; 66 | } 67 | 68 | // 解绑自定义事件 69 | off(key, watcherId) { 70 | if (!has(this.eventBus, key)) return; 71 | this.eventBus[key] = this.removeById(this.eventBus[key], watcherId); 72 | this.removeEmptyArr(this.eventBus, key); 73 | } 74 | 75 | // 移除reactiveBus 76 | removeReactive(watcherKeys, id) { 77 | watcherKeys.forEach((key) => { 78 | this.reactiveBus[key] = this.removeById(this.reactiveBus[key], id); 79 | this.removeEmptyArr(this.reactiveBus, key); 80 | }); 81 | } 82 | 83 | // 移除eventBus 84 | removeEvent(id) { 85 | const eventKeys = Object.keys(this.eventBus); 86 | eventKeys.forEach((key) => { 87 | this.eventBus[key] = this.removeById(this.eventBus[key], id); 88 | this.removeEmptyArr(this.eventBus, key); 89 | }); 90 | } 91 | 92 | // 移除全局watcher 93 | removeWatcher(id) { 94 | this.globalWatchers = this.removeById(this.globalWatchers, id); 95 | } 96 | 97 | // 触发响应式数据更新 98 | emitReactive(key, value) { 99 | const mergeKey = key.indexOf('.') > -1 ? key.split('.')[0] : key; 100 | if (!has(this.reactiveBus, mergeKey)) return; 101 | this.reactiveBus[mergeKey].forEach((obj) => { 102 | if (isFunction(obj.update)) obj.update(key, value); 103 | }); 104 | } 105 | 106 | // 触发自定义事件更新 107 | emitEvent(key, value) { 108 | if (!has(this.eventBus, key)) return; 109 | this.eventBus[key].forEach((obj) => { 110 | if (isFunction(obj.callback)) obj.callback(value); 111 | }); 112 | } 113 | 114 | // 手动更新 115 | handleUpdate(key, value) { 116 | // key在reactiveObj中 更新reactiveObj 117 | if (has(this.reactiveObj, key)) { 118 | if (get(this.reactiveObj, key) !== value) { 119 | set(this.reactiveObj, key, value); 120 | } else { 121 | this.emitReactive(key, value); 122 | } 123 | } else { 124 | // key不在reactiveObj中 手动更新所有watcher中的$data 125 | this.globalWatchers.forEach((watcher) => { 126 | if (has(watcher.$data, key)) { 127 | watcher.update(key, value); 128 | } 129 | }); 130 | } 131 | } 132 | 133 | // 判断数组中是否存在相同id的元素 134 | isExistSameId(arr, id) { 135 | if (Array.isArray(arr) && arr.length) { 136 | return arr.findIndex((item) => item.id === id) > -1; 137 | } 138 | return false; 139 | } 140 | 141 | // 根据id删除数组中元素 142 | removeById(arr, id) { 143 | if (Array.isArray(arr) && arr.length) { 144 | return arr.filter((item) => item.id !== id); 145 | } 146 | return arr; 147 | } 148 | 149 | // 删除对象中空数组的属性 150 | removeEmptyArr(obj, key) { 151 | if (!obj || !Array.isArray(obj[key])) return; 152 | if (obj[key].length === 0) delete obj[key]; 153 | } 154 | } 155 | 156 | module.exports = Observer; 157 | -------------------------------------------------------------------------------- /packages/cli/lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint no-console: "off" */ 4 | 5 | const path = require('path'); 6 | const fs = require('fs-extra'); 7 | const chalk = require('chalk'); 8 | const https = require('https'); 9 | const semver = require('semver'); 10 | 11 | // 项目根目录 12 | let ROOT = process.cwd(); 13 | 14 | // NPM 地址 15 | // NOTE: 后续需要支持根据用户本地设置的 npm registry 自动替换 16 | const NPM_BASE_URL = 'https://registry.npmjs.org/'; 17 | 18 | // 日志打印类型 19 | const DEBUG_TYPES = { 20 | error: 'red', 21 | info: 'blue', 22 | warning: 'yellow', 23 | success: 'green' 24 | }; 25 | 26 | // 小程序开发者工具配置文件映射 27 | const DEV_TOOL_CONFIG_FILES = { 28 | // 微信小程序 29 | weapp: 'project.config.json', 30 | // 百度小程序 31 | swan: 'project.swan.json', 32 | // 头条小程序 33 | tt: 'project.tt.json', 34 | // 支付宝小程序 35 | alipay: 'project.alipay.json', 36 | // QQ小程序 37 | qq: 'project.qq.json' 38 | }; 39 | 40 | // 模版文件地址 41 | const TEMPLATE_DIR = path.resolve(__dirname, '../templates/wechat-app'); 42 | 43 | // 小程序类型名称映射 44 | const TYPE_NAME_MAPPINGS = { 45 | weapp: '微信', 46 | swan: '百度', 47 | tt: '头条', 48 | alipay: '支付宝', 49 | qq: 'QQ' 50 | }; 51 | 52 | // 判断是否为 ewa 目录 53 | function isEwaProject() { 54 | return fs.existsSync(path.resolve(ROOT, '.ewa')); 55 | } 56 | 57 | // 根据构架类型选择输出文件夹 58 | function outputDirByType(type) { 59 | if (!type) throw new Error('无效构建类型'); 60 | if (type === 'weapp') return 'dist'; 61 | return `dist-${type}`; 62 | } 63 | 64 | // 确保当前目录为 ewa 项目目录,否则报错 65 | function ensureEwaProject(type = 'weapp') { 66 | if (isEwaProject()) { 67 | // 模版里面增加支付宝,头条,百度配置 68 | // 配置文件映射关系为 project.[type].json 69 | // 如: 70 | // 微信 project.config.json => project.config.json 71 | // 百度 project.swan.json => project.swan.json 72 | // 头条 project.tt.json => project.config.json 73 | // qq project.qq.json => project.config.json 74 | // 支付宝 project.alipay.json => mini.project.json 75 | let configFileName = DEV_TOOL_CONFIG_FILES[type]; 76 | let configFile = path.resolve(ROOT, `src/${configFileName}`); 77 | 78 | // 如果开发者工具配置文件不存在, 则初始化一个 79 | if (!fs.existsSync(configFile)) { 80 | log( 81 | `${TYPE_NAME_MAPPINGS[type]}小程序开发者工具配置文件不存在:${configFileName}, 已为您创建`, 82 | 'warning' 83 | ); 84 | fs.copySync( 85 | path.resolve(TEMPLATE_DIR, `src/${configFileName}`), 86 | configFile 87 | ); 88 | } 89 | } else { 90 | log('无法执行命令,不是一个有效的 ewa 项目', 'error'); 91 | process.exit(0); 92 | } 93 | } 94 | 95 | // Log 96 | function log(msg, type = 'info') { 97 | let color = DEBUG_TYPES[type] || 'blue'; 98 | console.log( 99 | `[${new Date().toString().split(' ')[4]}]`, 100 | chalk[color]('[ewa] ' + msg) 101 | ); 102 | } 103 | 104 | // 基础 https 请求 105 | function request(url) { 106 | return new Promise(function (resolve, reject) { 107 | https.get(url, res => { 108 | let buffers = []; 109 | 110 | res.on('data', function (buffer) { 111 | buffers.push(buffer); 112 | }); 113 | 114 | res.on('end', function () { 115 | resolve(JSON.parse(Buffer.concat(buffers).toString('utf8'))); 116 | }); 117 | }).on('error', (e) => { 118 | reject(e); 119 | }); 120 | }); 121 | } 122 | 123 | async function checkUpdates() { 124 | const packageFile = path.resolve(ROOT, 'package.json'); 125 | if (!fs.existsSync(packageFile)) return; 126 | 127 | const packageInfo = fs.readJsonSync(packageFile); 128 | 129 | let msg = []; 130 | 131 | async function versionCheck(name) { 132 | let version = (packageInfo.dependencies || {})[name]; 133 | version = version || (packageInfo.devDependencies || {})[name]; 134 | 135 | version = version ? version.replace(/[<>^=~ ]/ig, '') : null; 136 | 137 | let latest = await request(NPM_BASE_URL + `${name}/latest`); 138 | if (version != null && latest && semver.gt(latest.version, version)) { 139 | msg.push(`${name}@${latest.version}, 当前版本为 ${version}`); 140 | } 141 | } 142 | 143 | try { 144 | await Promise.all([ 145 | versionCheck('ewa'), 146 | versionCheck('ewa-cli') 147 | ]); 148 | 149 | if (msg.length) { 150 | msg.unshift('发现新版本:'); 151 | msg.unshift(''); 152 | msg.push('请运行命令 `ewa upgrade` 更新至最新版'); 153 | msg.push(''); 154 | msg.map(m => log(m, 'success')); 155 | } 156 | } catch (error) { 157 | log('检查版本失败', 'warning'); 158 | } 159 | } 160 | 161 | module.exports = { 162 | isEwaProject, 163 | ensureEwaProject, 164 | log, 165 | request, 166 | checkUpdates, 167 | outputDirByType 168 | }; 169 | -------------------------------------------------------------------------------- /packages/webpack/lib/utils/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint no-console: "off" */ 4 | 5 | const chalk = require('chalk'); 6 | const path = require('path'); 7 | const glob = require('glob'); 8 | 9 | // 常量 10 | const TS_PATTERN = /\.ts$/; 11 | const CSS_PATTERN = /\.(less|sass|scss)$/; 12 | 13 | const WXML_LIKE_PATTERN = /\.(swan|wxml|axml|ttml|qml)$/; 14 | const WXSS_LIKE_PATTERN = /\.(wxss|css|acss|ttss|qss)$/; 15 | const WXS_LIKE_PATTERN = /\.(wxs|sjs|qs)$/; 16 | 17 | // 基于构建环境替换文件后缀 18 | // NOTE: 仅支持 从 微信小程序 转换为其他小程序,不支持 其他小程序 转换为 微信小程序 19 | // NOTE: 无关逻辑需要清理 20 | function chooseCorrectExtnameByBuildTarget(file, target) { 21 | // 替换 ts 后缀 为 js 文件 22 | if (/\.ts$/.test(file)) return file.replace(/\.ts$/, '.js'); 23 | 24 | // 如果构建目标为 微信小程序 25 | if (target === 'weapp') { 26 | if (WXML_LIKE_PATTERN.test(file)) return file.replace(WXML_LIKE_PATTERN, '.wxml'); 27 | if (WXSS_LIKE_PATTERN.test(file)) return file.replace(WXSS_LIKE_PATTERN, '.wxss'); 28 | if (WXS_LIKE_PATTERN.test(file)) return file.replace(WXS_LIKE_PATTERN, '.wxs'); 29 | } 30 | 31 | // 如果构建目标为 百度小程序 32 | if (target === 'swan') { 33 | if (WXML_LIKE_PATTERN.test(file)) return file.replace(WXML_LIKE_PATTERN, '.swan'); 34 | if (WXSS_LIKE_PATTERN.test(file)) return file.replace(WXSS_LIKE_PATTERN, '.css'); 35 | if (WXS_LIKE_PATTERN.test(file)) return file.replace(WXS_LIKE_PATTERN, '.sjs'); 36 | } 37 | 38 | // 如果构建目标为 支付宝小程序 39 | if (target === 'alipay') { 40 | if (WXML_LIKE_PATTERN.test(file)) return file.replace(WXML_LIKE_PATTERN, '.axml'); 41 | if (WXSS_LIKE_PATTERN.test(file)) return file.replace(WXSS_LIKE_PATTERN, '.acss'); 42 | if (WXS_LIKE_PATTERN.test(file)) return file.replace(WXS_LIKE_PATTERN, '.sjs'); 43 | } 44 | 45 | // 如果构建目标为 字节小程序 46 | if (target === 'tt') { 47 | if (WXML_LIKE_PATTERN.test(file)) return file.replace(WXML_LIKE_PATTERN, '.ttml'); 48 | if (WXSS_LIKE_PATTERN.test(file)) return file.replace(WXSS_LIKE_PATTERN, '.ttss'); 49 | if (WXS_LIKE_PATTERN.test(file)) return file.replace(WXS_LIKE_PATTERN, '.sjs'); 50 | } 51 | 52 | // 如果构建目标为 qq小程序 53 | if (target === 'qq') { 54 | if (WXML_LIKE_PATTERN.test(file)) return file.replace(WXML_LIKE_PATTERN, '.qml'); 55 | if (WXSS_LIKE_PATTERN.test(file)) return file.replace(WXSS_LIKE_PATTERN, '.qss'); 56 | if (WXS_LIKE_PATTERN.test(file)) return file.replace(WXS_LIKE_PATTERN, '.qs'); 57 | } 58 | 59 | // 其他文件直接返回 60 | return file; 61 | } 62 | 63 | // 判断是否为 page 或者 component 64 | function isPageOrComponent(file) { 65 | return !!(~file.indexOf('components/') || ~file.indexOf('pages/')); 66 | } 67 | 68 | // 去除 components 和 pages 重复路径, 如 navbar/navbar.js => navbar.js 69 | function resolveOrSimplifyPath(baseDir, filepath, simplifyPath) { 70 | let relativePath = baseDir ? path.relative(baseDir, filepath) : filepath; 71 | if (simplifyPath && isPageOrComponent(relativePath)) { 72 | let extname = path.extname(relativePath); 73 | let name = path.basename(relativePath, extname); 74 | return relativePath.replace(`${name}/${name}${extname}`, `${name}${extname}`); 75 | } else { 76 | return relativePath; 77 | } 78 | } 79 | 80 | // 构建 entries 81 | function buildDynamicEntries({ 82 | baseDir, 83 | simplifyPath = false, 84 | target = '', 85 | ignore = [] 86 | }) { 87 | // 查找所有微信小程序文件 88 | let wxFiles = glob.sync( 89 | path.join(baseDir, '**/*.{wxss,wxs,wxml}') 90 | ); 91 | 92 | // 其他小程序相关文件 93 | // 支持 scss 和 less 当做 wxss 用 94 | // 支持 ts 编译为 js 95 | let otherFiles = glob.sync( 96 | path.join(baseDir, '**/*.{ts,js,json,scss,sass,less}'), 97 | // 忽略 .d.ts 文件 98 | { ignore: ['**/*.d.ts', ...ignore] } 99 | ); 100 | 101 | // 标记为入口文件夹 102 | let entryDirs = { [baseDir]: true }; 103 | 104 | let entries = {}; 105 | 106 | // 遍历所有的微信文件用于生成 entry 对象 107 | wxFiles.map(function (file) { 108 | // 标记为微信页面或组件文件夹 109 | entryDirs[path.dirname(file)] = true; 110 | 111 | let relativePath = resolveOrSimplifyPath(baseDir, file, simplifyPath); 112 | 113 | // 根据构建类型决定文件后缀名 114 | relativePath = chooseCorrectExtnameByBuildTarget(relativePath, target); 115 | 116 | entries[relativePath] = file; 117 | }); 118 | 119 | // 仅当被标记为微信小程序的页面或者组件文件夹的内容才会被作为 entry 120 | otherFiles.forEach(function (file) { 121 | if (entryDirs[path.dirname(file)]) { 122 | let relativePath = resolveOrSimplifyPath(baseDir, file, simplifyPath); 123 | 124 | let entryName = relativePath; 125 | 126 | // 支持直接使用 ts 127 | if (TS_PATTERN.test(relativePath)) entryName = relativePath.replace(TS_PATTERN, '.js'); 128 | 129 | // 支持直接使用 less 或 scss, 需要对应的 cssParser 设置支持 130 | if (CSS_PATTERN.test(relativePath)) entryName = relativePath.replace(CSS_PATTERN, '.wxss'); 131 | 132 | // 根据构建类型决定文件后缀名 133 | entryName = chooseCorrectExtnameByBuildTarget(entryName, target); 134 | 135 | // 选择合适的小程序开发工具配置文件 136 | if (/project\.(config|swan|alipay|tt|qq)\.json$/.test(file)) { 137 | if (target === 'weapp' && entryName === 'project.config.json') { 138 | entries[entryName] = file; 139 | } 140 | if (target === 'tt' && entryName === 'project.tt.json') { 141 | entries['project.config.json'] = file; 142 | } 143 | if (target === 'alipay' && entryName === 'project.alipay.json') { 144 | entries['mini.project.json'] = file; 145 | } 146 | if (target === 'swan' && entryName === 'project.swan.json') { 147 | entries[entryName] = file; 148 | } 149 | if (target === 'qq' && entryName === 'project.qq.json') { 150 | entries['project.config.json'] = file; 151 | } 152 | } else { 153 | // 如果 已存在,则提示错误 154 | // js 文件优先级 高于 ts 155 | // wxss 文件优先级 高于 less 和 sass 156 | if (entries[entryName]) { 157 | log(`入口文件 \`${entryName}\` 已存在,忽略文件 \`${relativePath}\``, 'warning'); 158 | return; 159 | } 160 | 161 | // 添加入 entry 对象 162 | entries[entryName] = file; 163 | } 164 | } 165 | }); 166 | 167 | return entries; 168 | } 169 | 170 | function escapeRegExpString(str) { 171 | // eslint-disable-next-line 172 | return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); 173 | } 174 | 175 | function pathToRegExp(p) { 176 | return new RegExp('^' + escapeRegExpString(p)); 177 | } 178 | 179 | const DEBUG_TYPES = { 180 | error: 'red', 181 | info: 'blue', 182 | warning: 'yellow', 183 | success: 'green' 184 | }; 185 | 186 | function log(msg, type = 'info') { 187 | let color = DEBUG_TYPES[type] || 'blue'; 188 | console.log( 189 | `[${new Date().toString().split(' ')[4]}]`, 190 | chalk[color]('[ewa] ' + msg) 191 | ); 192 | } 193 | 194 | module.exports = { 195 | resolveOrSimplifyPath, 196 | buildDynamicEntries, 197 | escapeRegExpString, 198 | isPageOrComponent, 199 | pathToRegExp, 200 | log 201 | }; 202 | -------------------------------------------------------------------------------- /packages/ewa/src/plugins/apiPromisify.js: -------------------------------------------------------------------------------- 1 | const assign = require('lodash.assign'); 2 | const keys = require('lodash.keys'); 3 | const Queue = require('../utils/Queue'); 4 | const buildArgs = require('../utils/buildArgs'); 5 | 6 | const queue = new Queue(); 7 | 8 | /** 9 | * Promisify a callback function 10 | * @param {Function} fn callback function 11 | * @param {Object} caller caller 12 | * @param {String} type weapp-style|error-first, default to weapp-style 13 | * @return {Function} promisified function 14 | */ 15 | const promisify = function (fn, caller, type) { 16 | if (type === void 0) type = 'weapp-style'; 17 | 18 | return function () { 19 | let args = buildArgs.apply(null, arguments); 20 | 21 | return new Promise(((resolve, reject) => { 22 | switch (type) { 23 | case 'weapp-style': 24 | fn.call(caller, assign({}, args[0], 25 | { 26 | success: function success(res) { 27 | resolve(res); 28 | }, 29 | fail: function fail(err) { 30 | reject(err); 31 | }, 32 | })); 33 | break; 34 | case 'weapp-fix': 35 | fn.apply(caller, args.concat(resolve).concat(reject)); 36 | break; 37 | case 'error-first': 38 | fn.apply( 39 | caller, 40 | args.concat([ 41 | function (err, res) { 42 | return err ? reject(err) : resolve(res); 43 | }, 44 | ]) 45 | ); 46 | break; 47 | default: 48 | break; 49 | } 50 | })); 51 | }; 52 | }; 53 | 54 | // The methods no need to promisify 55 | const noPromiseMethods = [ 56 | // 媒体 57 | 'stopRecord', 58 | 'getRecorderManager', 59 | 'pauseVoice', 60 | 'stopVoice', 61 | 'pauseBackgroundAudio', 62 | 'stopBackgroundAudio', 63 | 'getBackgroundAudioManager', 64 | 'createAudioContext', 65 | 'createInnerAudioContext', 66 | 'createVideoContext', 67 | 'createCameraContext', 68 | 69 | // 位置 70 | 'createMapContext', 71 | 72 | // 设备 73 | 'canIUse', 74 | 'startAccelerometer', 75 | 'stopAccelerometer', 76 | 'startCompass', 77 | 'stopCompass', 78 | 'onBLECharacteristicValueChange', 79 | 'onBLEConnectionStateChange', 80 | 81 | // 界面 82 | 'hideToast', 83 | 'hideLoading', 84 | 'showNavigationBarLoading', 85 | 'hideNavigationBarLoading', 86 | 'navigateBack', 87 | 'createAnimation', 88 | 'pageScrollTo', 89 | 'createSelectorQuery', 90 | 'createCanvasContext', 91 | 'createContext', 92 | 'drawCanvas', 93 | 'hideKeyboard', 94 | 'stopPullDownRefresh', 95 | 96 | // 拓展接口 97 | 'arrayBufferToBase64', 98 | 'base64ToArrayBuffer', 99 | ]; 100 | 101 | const simplifyArgs = { 102 | // network 103 | request: 'url', 104 | downloadFile: 'url', 105 | connectSocket: 'url', 106 | sendSocketMessage: 'data', 107 | 108 | // media 109 | previewImage: 'urls', 110 | getImageInfo: 'src', 111 | saveImageToPhotosAlbum: 'filePath', 112 | playVoice: 'filePath', 113 | playBackgroundAudio: 'dataUrl', 114 | seekBackgroundAudio: 'position', 115 | saveVideoToPhotosAlbum: 'filePath', 116 | 117 | // files 118 | saveFile: 'tempFilePath', 119 | getFileInfo: 'filePath', 120 | getSavedFileInfo: 'filePath', 121 | removeSavedFile: 'filePath', 122 | openDocument: 'filePath', 123 | 124 | // device 125 | setStorage: 'key,data', 126 | getStorage: 'key', 127 | removeStorage: 'key', 128 | openLocation: 'latitude,longitude', 129 | makePhoneCall: 'phoneNumber', 130 | setClipboardData: 'data', 131 | getConnectedBluetoothDevices: 'services', 132 | createBLEConnection: 'deviceId', 133 | closeBLEConnection: 'deviceId', 134 | getBLEDeviceServices: 'deviceId', 135 | startBeaconDiscovery: 'uuids', 136 | setScreenBrightness: 'value', 137 | setKeepScreenOn: 'keepScreenOn', 138 | 139 | // screen 140 | showToast: 'title', 141 | showLoading: 'title,mask', 142 | showModal: 'title,content', 143 | showActionSheet: 'itemList,itemColor', 144 | setNavigationBarTitle: 'title', 145 | setNavigationBarColor: 'frontColor,backgroundColor', 146 | 147 | // tabBar 148 | setTabBarBadge: 'index,text', 149 | removeTabBarBadge: 'idnex', 150 | showTabBarRedDot: 'index', 151 | hideTabBarRedDot: 'index', 152 | showTabBar: 'animation', 153 | hideTabBar: 'animation', 154 | 155 | // topBar 156 | setTopBarText: 'text', 157 | 158 | // navigator 159 | navigateTo: 'url', 160 | redirectTo: 'url', 161 | navigateBack: 'delta', 162 | reLaunch: 'url', 163 | 164 | // pageScroll 165 | pageScrollTo: 'scrollTop,duration', 166 | }; 167 | 168 | const makeObj = function (arr) { 169 | let obj = {}; 170 | arr.forEach((v) => { 171 | obj[v] = 1; 172 | }); 173 | return obj; 174 | }; 175 | 176 | /* 177 | * wx basic api promisify 178 | * useage: 179 | * ewa.login().then().catch() 180 | */ 181 | module.exports = function install(ewa = {}, removeFromPromisify) { 182 | let _api; 183 | let api; 184 | 185 | // 微信小程序支持 186 | if (typeof wx === 'object') { 187 | api = wx; 188 | _api = (ewa.wx = ewa.wx || assign({}, wx)); 189 | } 190 | 191 | // 百度小程序支持 192 | if (typeof swan === 'object') { 193 | api = swan; 194 | _api = (ewa.swan = ewa.swan || assign({}, swan)); 195 | } 196 | 197 | // 头条小程序支持 198 | if (typeof tt === 'object') { 199 | api = tt; 200 | _api = (ewa.tt = ewa.tt || assign({}, tt)); 201 | } 202 | 203 | // 支付宝小程序支持 204 | if (typeof my === 'object') { 205 | api = my; 206 | _api = (ewa.my = ewa.my || assign({}, my)); 207 | } 208 | 209 | // qq小程序支持 210 | if (typeof qq === 'object') { 211 | api = qq; 212 | _api = (ewa.qq = ewa.qq || assign({}, qq)); 213 | } 214 | 215 | let noPromiseMap = {}; 216 | if (removeFromPromisify) { 217 | if (Array.isArray(removeFromPromisify)) { 218 | noPromiseMap = makeObj(noPromiseMethods.concat(removeFromPromisify)); 219 | } else { 220 | noPromiseMap = assign({}, makeObj(noPromiseMethods), removeFromPromisify); 221 | } 222 | } 223 | 224 | keys(_api).forEach((key) => { 225 | if (!noPromiseMap[key] && key.substr(0, 2) !== 'on' && key.substr(-4) !== 'Sync') { 226 | _api[key] = promisify(function () { 227 | let args = buildArgs.apply(null, arguments); 228 | 229 | let fixArgs = args[0]; 230 | let failFn = args.pop(); 231 | let successFn = args.pop(); 232 | if (simplifyArgs[key] && Object.prototype.toString.call(fixArgs) !== '[object Object]') { 233 | fixArgs = {}; 234 | let ps = simplifyArgs[key]; 235 | if (args.length) { 236 | ps.split(',').forEach((p, i) => { 237 | if (args[i]) { 238 | fixArgs[p] = args[i]; 239 | } 240 | }); 241 | } 242 | } 243 | fixArgs.success = successFn; 244 | fixArgs.fail = failFn; 245 | 246 | return api[key].call(api, fixArgs); 247 | }, _api, 'weapp-fix'); 248 | 249 | // enhanced request with queue 250 | if (key === 'request') { 251 | let rq = _api[key]; 252 | 253 | // overwrite request method 254 | _api[key] = function request() { 255 | let args = buildArgs.apply(null, arguments); 256 | return new Promise(((resolve, reject) => { 257 | queue.push(() => rq.apply(_api, args).then(resolve, reject)); 258 | })); 259 | }; 260 | } 261 | } 262 | }); 263 | 264 | ewa.promisify = promisify; 265 | 266 | // 通用接口支持 267 | // TODO: 添加接口差异提示 268 | ewa.api = _api; 269 | }; 270 | -------------------------------------------------------------------------------- /packages/webpack/lib/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const webpack = require('webpack'); 4 | const path = require('path'); 5 | const { existsSync } = require('fs'); 6 | 7 | const WebpackBar = require('webpackbar'); 8 | const NodeSourcePlugin = require('webpack/lib/node/NodeSourcePlugin'); 9 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 10 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 11 | const ESLintWebpackPlugin = require('eslint-webpack-plugin'); 12 | const NodeCommonModuleTemplatePlugin = require('./plugins/NodeCommonModuleTemplatePlugin'); 13 | const AutoCleanUnusedFilesPlugin = require('./plugins/AutoCleanUnusedFilesPlugin'); 14 | const EnsureVendorsExistancePlugin = require('./plugins/EnsureVendorsExistancePlugin'); 15 | const utils = require('./utils'); 16 | 17 | // 常量 18 | const NODE_ENV = process.env.NODE_ENV || 'development'; 19 | const EWA_ENV = process.env.EWA_ENV || 'weapp'; 20 | const IS_DEV = NODE_ENV === 'development'; 21 | const ROOT = process.cwd(); 22 | const ENTRY_DIR = path.join(ROOT, 'src'); 23 | const OUTPUT_DIR = path.join(ROOT, EWA_ENV === 'weapp' ? 'dist' : `dist-${EWA_ENV}`); 24 | const USER_CONFIG_FILE = path.join(ROOT, 'ewa.config.js'); 25 | const APP_JSON_FILE = path.join(ENTRY_DIR, 'app.json'); 26 | const CUSTOM_ENVIRONMENTS = []; 27 | const OUTPUT_GLOBAL_OBJECT_MAP = { 28 | weapp: 'wx', 29 | swan: 'swan', 30 | tt: 'tt', 31 | qq: 'qq', 32 | alipay: 'my', 33 | }; 34 | const OUTPUT_GLOBAL_OBJECT = OUTPUT_GLOBAL_OBJECT_MAP[EWA_ENV]; 35 | const TYPE_NAME_MAPPINGS = { 36 | weapp: '微信小程序', 37 | swan: '百度小程序', 38 | tt: '字节小程序', 39 | qq: 'QQ小程序', 40 | alipay: '支付宝小程序', 41 | }; 42 | 43 | // 默认常量 44 | const DEFAULT_COMMON_MODULE_NAME = 'vendors.js'; 45 | const DEFAULT_ALIAS_DIRS = [ 46 | 'apis', 47 | 'assets', 48 | 'constants', 49 | 'utils' 50 | ]; 51 | const DEFAULT_COPY_FILE_TYPES = [ 52 | 'png', 53 | 'jpeg', 54 | 'jpg', 55 | 'gif', 56 | 'svg', 57 | 'ico', 58 | 'webp', 59 | 'apng' 60 | ]; 61 | const DEFAULT_COMMON_MODULE_PATTERN = /[\\/](node_modules|utils|vendor)[\\/].+\.js/; 62 | const DEFAULT_CSS_PARSER = 'sass'; 63 | const USER_CONFIG = existsSync(USER_CONFIG_FILE) ? require(USER_CONFIG_FILE) : {}; 64 | const APP_JSON_CONFIG = existsSync(APP_JSON_FILE) ? require(APP_JSON_FILE) : {}; 65 | const GLOBAL_COMPONENTS = APP_JSON_CONFIG.usingComponents || {}; 66 | 67 | /** 68 | * 生成 webpack 配置 69 | * options: 70 | * commonModuleName: 通用代码名称,默认为 vendors.js 71 | * commonModulePattern: 通用模块匹配模式,默认为 /[\\/]node_modules[\\/]/ 72 | * simplifyPath: 是否简化路径,作用于 page 和 component,如 index/index.wxml=> index.wxml,默认为 false 73 | * aliasDirs: 文件夹快捷引用 74 | * copyFileTypes: 需要拷贝的文件类型 75 | * rules: webpack loader 规则 76 | * plugins: webpack plugin 77 | * autoCleanUnusedFiles: 开发环境下是否自动清理无用文件,默认为 true 78 | * cssParser: sass 或者 less,默认为 sass 79 | * hashedModuleIds: 是否开启 hashed module id 80 | * cache: 是否开启缓存, 默认为 true 81 | * webpack: 修改并自定义 webpack 配置,如:function(config) { return config; } 82 | */ 83 | function makeConfig() { 84 | let options = Object.assign({ 85 | commonModuleName: DEFAULT_COMMON_MODULE_NAME, 86 | commonModulePattern: DEFAULT_COMMON_MODULE_PATTERN, 87 | aliasDirs: DEFAULT_ALIAS_DIRS, 88 | copyFileTypes: DEFAULT_COPY_FILE_TYPES, 89 | cssParser: DEFAULT_CSS_PARSER, 90 | customEnvironments: CUSTOM_ENVIRONMENTS 91 | }, USER_CONFIG); 92 | 93 | options.simplifyPath = options.simplifyPath === true; 94 | options.autoCleanUnusedFiles = options.autoCleanUnusedFiles !== false; 95 | options.cache = options.cache !== false; 96 | options.rules = options.rules || []; 97 | options.plugins = options.plugins || []; 98 | 99 | const aliasDirs = {}; 100 | options.aliasDirs.forEach(d => aliasDirs[d] = path.join(ENTRY_DIR, `${d}/`)); 101 | 102 | const copyPluginPatterns = [ 103 | { 104 | from: path.posix.join( 105 | // Fix for #46, add glob support for windows 106 | process.platform === 'win32' ? ROOT.replace(/\\/g, '/') : ROOT, 107 | `src/**/*.{${options.copyFileTypes.join(',')}}` 108 | ), 109 | to: OUTPUT_DIR, 110 | context: path.resolve(ROOT, 'src'), 111 | noErrorOnMissing: true 112 | } 113 | ]; 114 | // 支付宝单独开了一个开发中初始编译配置的json文件,放在.kaitian文件夹下 115 | if (EWA_ENV === 'alipay') { 116 | copyPluginPatterns.push({ 117 | from: path.resolve( 118 | ROOT, 119 | 'src/.kaitian' 120 | ), 121 | to: OUTPUT_DIR + '/.kaitian', 122 | noErrorOnMissing: true 123 | }); 124 | } 125 | 126 | // 插件 127 | let plugins = [ 128 | // 支持自定义环境变量的使用 129 | new webpack.EnvironmentPlugin( 130 | ['NODE_ENV', 'EWA_ENV'].concat(options.customEnvironments || []) 131 | ), 132 | 133 | // 进度条显示支持 134 | new WebpackBar(), 135 | 136 | // Mock node env 137 | new NodeSourcePlugin({ 138 | console: false, 139 | global: true, 140 | process: true, 141 | __filename: 'mock', 142 | __dirname: 'mock', 143 | Buffer: true, 144 | setImmediate: true 145 | }), 146 | 147 | // 合并文件 148 | new webpack.optimize.ModuleConcatenationPlugin(), 149 | 150 | // 输出非 js 文件 151 | new ExtractTextPlugin({ filename: '[name]' }), 152 | 153 | // 注入 JS 头部引用 154 | new NodeCommonModuleTemplatePlugin({ 155 | commonModuleName: options.commonModuleName, 156 | OUTPUT_GLOBAL_OBJECT 157 | }), 158 | 159 | // 拷贝 assets 文件 160 | new CopyWebpackPlugin({ 161 | patterns: copyPluginPatterns 162 | }) 163 | ]; 164 | 165 | // 生产环境进一步压缩代码 166 | if (!IS_DEV && options.hashedModuleIds !== false) { 167 | plugins.push( 168 | new webpack.HashedModuleIdsPlugin( 169 | !options.hashedModuleIds || options.hashedModuleIds === true ? { 170 | hashFunction: 'md5', 171 | hashDigest: 'base64', 172 | hashDigestLength: 4 173 | } : options.hashedModuleIds 174 | ) 175 | ); 176 | } else { 177 | // 允许模块命名,方便调试 178 | plugins.push( 179 | new webpack.NamedModulesPlugin() 180 | ); 181 | } 182 | 183 | // 开发环境下,自动清理无用的文件 184 | if (IS_DEV && options.autoCleanUnusedFiles) { 185 | plugins.push(new AutoCleanUnusedFilesPlugin({ 186 | // 排除拷贝的文件 187 | exclude: options.copyFileTypes.map(fileType => { 188 | return `**/*.${fileType}`; 189 | }).concat([ 190 | // 排除公共库文件 和 对应的 sourceMap 文件 191 | options.commonModuleName, 192 | `${options.commonModuleName}.map` 193 | ]) 194 | })); 195 | } 196 | 197 | // 添加 公共模块文件生成检查 198 | plugins.push(new EnsureVendorsExistancePlugin({ 199 | commonModuleName: options.commonModuleName 200 | })); 201 | 202 | // 开发环境下增加 eslint 检查 203 | if (IS_DEV) { 204 | const eslintConfigFile = path.resolve(ROOT, '.eslintrc.js'); 205 | const eslintWebpackConfig = { 206 | context: ENTRY_DIR, 207 | eslintPath: path.dirname(require.resolve('eslint/package.json')), 208 | extensions: ['js', 'ts'], 209 | cache: true, 210 | fix: true, 211 | overrideConfig: { 212 | parser: path.dirname(require.resolve('@babel/eslint-parser/package.json')), 213 | parserOptions: { 214 | babelOptions: { 215 | // 指定 babel 配置文件 216 | configFile: path.resolve(__dirname, './utils/babelConfig.js') 217 | } 218 | } 219 | } 220 | }; 221 | 222 | // 如果项目根目录 eslint 配置存在,则优先使用 223 | if (existsSync(eslintConfigFile)) { 224 | eslintWebpackConfig.overrideConfigFile = eslintConfigFile; 225 | } 226 | 227 | plugins.push(new ESLintWebpackPlugin(eslintWebpackConfig)); 228 | } 229 | 230 | plugins = plugins.concat(options.plugins); 231 | 232 | // Loaders 233 | let rules = []; 234 | 235 | let ruleOpts = { ...options, IS_DEV, ROOT, OUTPUT_DIR, ENTRY_DIR, EWA_ENV, GLOBAL_COMPONENTS }; 236 | const { cssRule, cssExtensions } = require('./rules/css')(ruleOpts); 237 | 238 | // 不同文件类型的处理 239 | rules = rules.concat([ 240 | require('./rules/ts')(ruleOpts), 241 | require('./rules/js')(ruleOpts), 242 | require('./rules/image')(ruleOpts), 243 | require('./rules/wxml')(ruleOpts), 244 | require('./rules/json')(ruleOpts), 245 | require('./rules/wxs')(ruleOpts), 246 | cssRule, 247 | 248 | // 修复 regenerator-runtime 导致使用 Function 的报错问题 249 | { 250 | test: /regenerator-runtime/, 251 | use: [{ 252 | loader: './loaders/fix-regenerator-loader', 253 | options: { type: options.EWA_ENV } 254 | }] 255 | } 256 | ]); 257 | 258 | // 构建优化 259 | const optimization = { 260 | splitChunks: { 261 | cacheGroups: { 262 | commons: { 263 | test: options.commonModulePattern, 264 | name: options.commonModuleName, 265 | chunks: 'all' 266 | } 267 | } 268 | } 269 | }; 270 | 271 | // 开发工具 272 | const devtool = IS_DEV ? 'source-map' : false; 273 | 274 | // 构建模式 275 | const mode = IS_DEV ? 'development' : 'production'; 276 | 277 | // 打印构建信息 278 | utils.log(`构建类型: ${TYPE_NAME_MAPPINGS[EWA_ENV]}`); 279 | utils.log(`构建目录: ${OUTPUT_DIR}`); 280 | 281 | // Webpack 配置 282 | const config = { 283 | stats: { 284 | // copied from `'minimal'` 285 | all: false, 286 | modules: true, 287 | maxModules: 0, 288 | errors: true, 289 | warnings: true, 290 | // our additional options 291 | moduleTrace: true, 292 | errorDetails: true, 293 | builtAt: true, 294 | colors: { 295 | green: '\u001b[32m', 296 | }, 297 | outputPath: true, 298 | timings: true, 299 | }, 300 | devtool, 301 | mode, 302 | context: __dirname, 303 | entry: utils.buildDynamicEntries({ 304 | baseDir: ENTRY_DIR, 305 | simplifyPath: options.simplifyPath, 306 | target: EWA_ENV 307 | }), 308 | target: 'node', 309 | output: { 310 | path: OUTPUT_DIR, 311 | filename: '[name]', 312 | globalObject: OUTPUT_GLOBAL_OBJECT 313 | }, 314 | optimization, 315 | module: { rules }, 316 | plugins, 317 | resolve: { 318 | modules: [ 319 | 'node_modules', 320 | path.resolve(__dirname, '../node_modules'), 321 | path.resolve(__dirname, '../../'), 322 | path.resolve(__dirname, '../../../node_modules') 323 | ], 324 | extensions: ['.ts', '.js', '.html', '.wxml', '.wxs'].concat(cssExtensions), 325 | alias: Object.assign(aliasDirs, { 326 | '@': path.resolve(ROOT, 'src/') 327 | }) 328 | }, 329 | 330 | // 优化 loader 解析目录,方便用户自定义 webpack 配置 331 | resolveLoader: { 332 | modules: [ 333 | 'node_modules', 334 | path.resolve(ROOT, './node_modules') 335 | ] 336 | } 337 | }; 338 | 339 | // 允许自定义 webpack 配置 340 | if (typeof options.webpack === 'function') { 341 | return options.webpack(config) || config; 342 | } 343 | 344 | return config; 345 | } 346 | 347 | module.exports = makeConfig(); 348 | 349 | -------------------------------------------------------------------------------- /packages/ewa/src/plugins/enableState.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | const diff = require('deep-diff'); 4 | const set = require('lodash.set'); 5 | const get = require('lodash.get'); 6 | const cloneDeep = require('lodash.clonedeep'); 7 | const keys = require('lodash.keys'); 8 | 9 | const noop = function () { }; 10 | 11 | // 取出 对象中的部分键值,支持 嵌套 key, 如 'user.gender' 12 | function deepPick(obj, props = []) { 13 | let _obj = {}; 14 | if (!obj) return _obj; 15 | for (let i = 0; i < props.length; i++) { 16 | let prop = props[i]; 17 | _obj[prop] = get(obj, prop); 18 | } 19 | return _obj; 20 | } 21 | 22 | // 日志打印 23 | const logger = function (type, name, stack = [], timeConsumption = 0, changes) { 24 | if (!console) return; 25 | let timeConsumptionMsg = `Diff 耗时: ${timeConsumption}ms`; 26 | try { 27 | if (console.group) { 28 | console.group(type, name, '触发更新'); 29 | console.log(timeConsumptionMsg); 30 | console.log('Diff 结果: ', changes); 31 | // 打印调用堆栈 32 | if (stack && stack.length) { 33 | let _spaces = ''; 34 | let _stack = stack.reduce((res, item, i) => { 35 | if (i === 0) return `${res}\n ${item}`; 36 | _spaces = ` ${_spaces}`; 37 | return `${res}\n${_spaces}└── ${item}`; 38 | }, '调用栈: '); 39 | console.log(_stack); 40 | } 41 | console.groupEnd(); 42 | } else { 43 | console.log(type, name, stack, timeConsumptionMsg, 'Diff 结果: ', changes); 44 | } 45 | } catch (e) { 46 | // Do nothing 47 | } 48 | }; 49 | 50 | // 打印 Diff 相关 信息 51 | const printDiffInfo = function (ctx, debug, time, changes) { 52 | let timeConsumption = time ? +new Date() - time : 0; 53 | let type = ctx.__isPage ? '页面:' : '组件:'; 54 | let name = ctx.__isPage ? ctx.route : ctx.is; 55 | let stack = ctx.__invokeStack || []; 56 | if (debug === true || debug === 'all') logger(type, name, stack, timeConsumption, changes); 57 | if (debug === 'page' && ctx.__isPage) logger(type, name, stack, timeConsumption, changes); 58 | if (debug === 'component' && ctx.__isComponent) logger(type, name, stack, timeConsumption, changes); 59 | }; 60 | 61 | // 添加调用堆栈属性 62 | const addInvokeStackProp = function (ctx) { 63 | Object.defineProperty(ctx, '__invokeStack', { 64 | get() { 65 | if (this.__invokeStack__) return this.__invokeStack__; 66 | 67 | let stack = []; 68 | 69 | if (typeof this.selectOwnerComponent === 'function') { 70 | let parent = this.selectOwnerComponent(); 71 | if (parent) { 72 | if (parent.__isComponent) stack = parent.__invokeStack || []; 73 | if (parent.__isPage) stack = [parent.route]; 74 | } 75 | } 76 | 77 | this.__invokeStack__ = stack.concat(this.is); 78 | 79 | return this.__invokeStack__; 80 | }, 81 | }); 82 | }; 83 | 84 | // 检查并警告 Component 中,data 和 properties 属性冲突 85 | const checkPropertyAndDataConflict = function (ctx, obj) { 86 | if (!ctx.__isComponent) return; 87 | 88 | let changedKeys = keys(obj); 89 | let conflictKeys = []; 90 | for (let i = 0; i < changedKeys.length; i++) { 91 | let k = changedKeys[i]; 92 | if (k in ctx.$$properties) { 93 | conflictKeys.push(k); 94 | } 95 | } 96 | if (conflictKeys.length) { 97 | console.warn(`组件: ${ctx.is} 中, properties 和 data 存在字段冲突: ${conflictKeys.join('、')}, 请尽快调整`); 98 | } 99 | }; 100 | 101 | // 开启 state 支持 102 | function enableState(opts = {}) { 103 | const { 104 | // 是否开启 debug 模式,支持3种参数: true, 'page', 'component' 105 | debug = false, 106 | 107 | // 是否开启 component 支持 108 | component = true, 109 | 110 | // 是否开启 page 支持 111 | page = true, 112 | 113 | // 数组删除操作: true 或者 false 114 | overwriteArrayOnDeleted = true, 115 | 116 | // 是否在 调用 setData 时自动同步 state 117 | autoSync = true, 118 | } = opts; 119 | 120 | // 解析变更内容 121 | function diffAndMergeChanges(state, obj) { 122 | let rawChanges = diff(deepPick(state, keys(obj)), obj); 123 | 124 | if (!rawChanges) return null; 125 | 126 | let changes = {}; 127 | let lastArrayDeletedPath = ''; 128 | let hasChanges = false; 129 | 130 | for (let i = 0; i < rawChanges.length; i++) { 131 | let item = rawChanges[i]; 132 | // NOTE: 暂不处理删除对象属性的情况 133 | if (item.kind !== 'D') { 134 | // 修复路径 a.b.0.1 => a.b[0][1] 135 | let path = item.path 136 | .join('.') 137 | .replace(/\.([0-9]+)\./g, '[$1].') 138 | .replace(/\.([0-9]+)$/, '[$1]'); 139 | 140 | // 处理数组删除的问题,后续更新如果为数组中的元素,直接跳过 141 | if ( 142 | overwriteArrayOnDeleted 143 | && lastArrayDeletedPath 144 | && path.indexOf(lastArrayDeletedPath) === 0 145 | ) continue; 146 | 147 | // 记录变化 148 | let value = item.rhs; 149 | 150 | // 对数组特殊处理 151 | if (item.kind === 'A') { 152 | // 处理数组元素删除的情况,需要业务代码做支持 153 | if (item.item.kind === 'D') { 154 | // 覆盖整个数组 155 | if (overwriteArrayOnDeleted) { 156 | // 如果后续变更依然为同一个数组中的操作,直接跳过 157 | if (lastArrayDeletedPath === path) continue; 158 | 159 | lastArrayDeletedPath = path; 160 | value = get(obj, path); 161 | } else { 162 | // 对特定数组元素置空 163 | path = `${path}[${item.index}]`; 164 | value = null; 165 | } 166 | } else { 167 | // 其他情况如 添加/修改 直接修改值 168 | 169 | // 如果后续变更为数组中的更新,忽略 170 | if (overwriteArrayOnDeleted && lastArrayDeletedPath === path) continue; 171 | 172 | path = `${path}[${item.index}]`; 173 | value = item.item.rhs; 174 | } 175 | } 176 | 177 | // 忽略 undefined 178 | if (value !== void 0) { 179 | hasChanges = true; 180 | // 深拷贝 value,防止对 this.data 的直接修改影响 diff 算法的结果 181 | set(state, path, cloneDeep(value)); 182 | 183 | changes[path] = value; 184 | } 185 | } 186 | } 187 | 188 | return hasChanges ? changes : null; 189 | } 190 | 191 | // 初始化状态函数 192 | function initState() { 193 | // 初始化状态 194 | this.$$state = cloneDeep(this.data); 195 | } 196 | 197 | // 给 setData 打补丁, 198 | function patchSetData() { 199 | // 防止重复打补丁 200 | if (this.__setDataPatched) return; 201 | 202 | this.__setData = this.setData; 203 | 204 | // 防止覆盖 setData 方法失败 205 | try { 206 | this.setData = (obj, callback) => { 207 | // 开启调试 208 | if (debug) printDiffInfo(this, debug, null, '手动调用 setData 无法 diff'); 209 | 210 | // 调用原 setData 211 | this.__setData(obj, () => { 212 | // 如果开启自动同步,则在调用 setData 完成后,自动同步所有数据到 state 中 213 | if (autoSync) initState.call(this); 214 | if (typeof callback === 'function') return callback(); 215 | }); 216 | }; 217 | 218 | this.__setDataPatched = true; 219 | } catch (error) { 220 | if (!patchSetData.warningPrinted && console && console.warn) { 221 | console.warn( 222 | '注意: setData 补丁失败, 如在 Page 或 Component 中混用 setData 和 setState, ' 223 | + '请在每次调用 setData 之后, 手动调用 syncState 以保持 data 和 state 数据同步' 224 | ); 225 | } 226 | // 仅打印一次 227 | patchSetData.warningPrinted = true; 228 | } 229 | } 230 | 231 | // 设置状态函数 232 | // 使用方式和 setData 相同 233 | // 返回值为 Promise, 所以支持 async/await 234 | function setState(obj, callback) { 235 | return new Promise((resolve, reject) => { 236 | // 初始化状态 237 | if (!this.$$state) this.initState(); 238 | 239 | // 记录当前时间 240 | let time = +new Date(); 241 | 242 | // 输出 debug 信息 243 | if (debug === 'conflict' || debug === true) checkPropertyAndDataConflict(this, obj); 244 | 245 | // 计算变更 246 | let changes = diffAndMergeChanges(this.$$state, obj); 247 | 248 | // 构造回调函数,确保 promise 在 callback 调用完成之后 resolve 249 | let cb; 250 | if (typeof callback === 'function') { 251 | cb = function () { 252 | try { 253 | callback(); 254 | resolve(); 255 | } catch (error) { 256 | reject(error); 257 | } 258 | }; 259 | } else { 260 | cb = () => resolve(); 261 | } 262 | 263 | // 如果有变更,则触发更新 264 | if (changes) { 265 | // 开启调试 266 | if (debug) printDiffInfo(this, debug, time, changes); 267 | this.__setData(changes, cb); 268 | } else { 269 | cb(); 270 | } 271 | }); 272 | } 273 | 274 | try { 275 | if (page) { 276 | let $Page = Page; 277 | // Page 功能扩展 278 | // eslint-disable-next-line 279 | Page = function (obj = {}) { 280 | obj.__isPage = true; 281 | 282 | // 修改 onLoad 方法,页面载入时打补丁 283 | let _onLoad = obj.onLoad || noop; 284 | obj.onLoad = function () { 285 | initState.call(this); 286 | patchSetData.call(this); 287 | return _onLoad.apply(this, arguments); 288 | }; 289 | 290 | // 注入 initState, syncState, setState 方法 291 | // initState 和 syncState 意义相同 292 | // 如果 patchSetData 运行失败,如果在 Page 或 Component 中有混用 setData 和 setState 293 | // 的情况,则最好在 调用完 setData 之后,手动调用下 syncState 以保持 data 和 state 状态一致 294 | obj.initState = function () { initState.call(this); }; 295 | obj.syncState = function () { initState.call(this); }; 296 | obj.setState = function () { return setState.apply(this, arguments); }; 297 | 298 | return $Page(obj); 299 | }; 300 | } 301 | 302 | if (component) { 303 | let $Component = Component; 304 | // Component 功能扩展 305 | // eslint-disable-next-line 306 | Component = function (obj = {}) { 307 | let properties = obj.properties || {}; 308 | obj.lifetimes = obj.lifetimes || {}; 309 | obj.methods = obj.methods || {}; 310 | 311 | // 修改 created 方法,组件创建时打补丁 312 | let _created = obj.lifetimes.created || obj.created || noop; 313 | obj.lifetimes.created = obj.created = function () { 314 | patchSetData.call(this); 315 | 316 | // 标识组件 317 | this.__isComponent = true; 318 | 319 | // 打印调用堆栈 320 | if (debug) addInvokeStackProp(this); 321 | 322 | // 保存属性设置 323 | this.$$properties = properties; 324 | return _created.apply(this, arguments); 325 | }; 326 | 327 | // 修改 attached 方法,组件挂载时初始化 state 328 | let _attached = obj.lifetimes.attached || obj.attached || noop; 329 | obj.lifetimes.attached = obj.attached = function () { 330 | initState.call(this); 331 | return _attached.apply(this, arguments); 332 | }; 333 | 334 | // 注入 initState, syncState, setState 方法 335 | // initState 和 syncState 意义相同 336 | // 如果 patchSetData 运行失败,如果在 Page 或 Component 中有混用 setData 和 setState 337 | // 的情况,则最好在 调用完 setData 之后,手动调用下 syncState 以保持 data 和 state 状态一致 338 | obj.methods.initState = function () { initState.call(this); }; 339 | obj.methods.setState = function () { return setState.apply(this, arguments); }; 340 | 341 | return $Component(obj); 342 | }; 343 | } 344 | } catch (e) { 345 | // Page 或者 Component 未定义 346 | console.log('覆盖小程序 Page 或 Component 出错', e); 347 | } 348 | } 349 | 350 | module.exports = enableState; 351 | -------------------------------------------------------------------------------- /packages/ewa/README.md: -------------------------------------------------------------------------------- 1 | EWA (微信小程序增强开发工具) 2 | ========================= 3 | 4 | Enhanced Wechat App Development Toolkit (微信小程序增强开发工具) 5 | 6 | ## 为什么开发这个工具? 7 | 8 | 厌倦了不停的对比 [wepy](https://github.com/Tencent/wepy) 或者 [mpvue](https://github.com/Meituan-Dianping/mpvue) 的特性,间歇性的踩雷,以及 `code once, run everywhere` 的幻想。只想给小程序开发插上效率的翅膀 ~ 9 | 10 | ## 功能特性 11 | 12 | 1. Async/Await 支持 13 | 2. Javascript ES2020 语法 14 | 3. 原生小程序所有功能,无需学习,极易上手 15 | 4. 微信接口 Promise 化 16 | 5. 支持安装 NPM 包 17 | 6. 支持 SCSS(或 LESS) 以及 小于 16k 的 background-image 18 | 7. 支持 source map, 方便调试 19 | 8. 添加新页面或新组件无需重启编译 20 | 9. 允许自定义编译流程 21 | 10. 自动兼容旧版本手机中的显示样式 22 | 11. 支持 WXSS 和 SCSS(或 LESS) 混用 23 | 12. 代码混淆及高度压缩,节省包大小 24 | 13. Typescript 支持 25 | 14. 支持转换成 百度 / 字节跳动 / QQ / 支付宝小程序 26 | 15. 多种小程序开发插件,为小程序开发减负,解放生产力 27 | 28 | 更多特性正在赶来 ... 敬请期待 29 | 30 | ## 安装 31 | 32 | ***需要 node 版本 >= 10.13*** 33 | 34 | ```bash 35 | npm i -g ewa-cli 或者 yarn global add ewa-cli 36 | ``` 37 | 38 | ## 如何使用 39 | 40 | ### 创建新项目 41 | 42 | ```bash 43 | ewa new your_project_name 44 | ``` 45 | 46 | ### 集成到现有小程序项目,仅支持小程序原生开发项目转换 47 | 48 | ***注意:使用此方法,请务必对项目代码做好备份!!!*** 49 | 50 | ```bash 51 | cd your_project_dir && ewa init 52 | ``` 53 | 54 | ### 启动 55 | 56 | 运行 `npm start` 即可启动实时编译 57 | 58 | 运行 `npm run build` 即可编译线上版本(相比实时编译而言,去除了 source map 并增加了代码压缩混淆等,体积更小) 59 | 60 | 上述命令运行成功后,可以看到本地多了个 `dist` 目录,这个目录里就是生成的小程序相关代码。 61 | 62 | 使用[微信开发者工具](https://mp.weixin.qq.com/debug/wxadoc/dev/devtools/devtools.html)选择 `dist` 目录打开,即可预览项目 63 | 64 | ### 目录结构 65 | 66 | ``` 67 | ├── .ewa 特殊占位目录,用于检查是否为 ewa 项目 68 | ├── dist 小程序运行代码目录(该目录由ewa的start 或者 build指令自动编译生成,请不要直接修改该目录下的文件) 69 | ├── node_modules 外部依赖库 70 | ├── src 代码编写的目录(该目录为使用ewa后的开发目录) 71 | │   ├── components 小程序组件目录 72 | │   ├── pages 小程序页面目录 73 | │   │   ├── index 74 | │   │   │   ├── index.js 75 | │   │   │   ├── index.wxml 76 | │   │   │   └── index.wxss 77 | │   │   └── logs 78 | │   │   ├── logs.js 79 | │   │   ├── logs.json 80 | │   │   ├── logs.wxml 81 | │   │   └── logs.wxss 82 | │   ├── templates 小程序模版目录 83 | │   ├── utils 84 | │   │   └── util.js 85 | │   ├── app.js 小程序入口文件 86 | │   ├── app.json 小程序全局配置文件 87 | │   ├── app.wxss 小程序全局样式文件 88 | │   └── project.config.json 微信开发者工具小程序项目配置文件 89 | ├── ewa.config.js ewa 配置文件 90 | ├── .gitignore 91 | ├── .eslintrc.js eslint 配置 92 | └── package.json 93 | ``` 94 | 95 | ### 命令行说明 96 | 97 | #### 概览 98 | 99 | ``` 100 | ewa [args] 101 | 102 | 命令: 103 | ewa new 创建新的微信小程序项目 [别名: create] 104 | ewa init 在现有的小程序项目中初始化 EWA 105 | ewa start 启动 EWA 小程序项目实时编译 [别名: dev] 106 | ewa build 编译小程序静态文件 107 | ewa clean 清理小程序静态文件 108 | ewa upgrade 升级 EWA 工具 109 | ewa generate 快速生成模版 [别名: g] 110 | 111 | 选项: 112 | -v, --version 当前版本号 [布尔] 113 | -h, --help 获取使用帮助 [布尔] 114 | ``` 115 | 116 | #### 实时编译 117 | 118 | ``` 119 | ewa start 120 | 121 | 启动 EWA 小程序项目实时编译 122 | 123 | 选项: 124 | -v, --version 当前版本号 [布尔] 125 | -h, --help 获取使用帮助 [布尔] 126 | -t, --type 构建目标 `weapp` 或 `swan` 或 `alipay` 或 `tt` 或 `qq` 127 | [字符串] [可选值: "weapp", "swan", "alipay", "tt", "qq"] [默认值: "weapp"] 128 | ``` 129 | 130 | #### 构建 131 | 132 | ``` 133 | ewa build 134 | 135 | 编译小程序静态文件 136 | 137 | 选项: 138 | -v, --version 当前版本号 [布尔] 139 | -h, --help 获取使用帮助 [布尔] 140 | -t, --type 构建目标 `weapp` 或 `swan` 或 `alipay` 或 `tt` 或 `qq` 141 | [字符串] [可选值: "weapp", "swan", "alipay", "tt", "qq"] [默认值: "weapp"] 142 | ``` 143 | 144 | #### 快速生成样板文件 145 | 146 | ``` 147 | ewa generate 148 | 149 | 快速生成模版 150 | 151 | 位置: 152 | type 类型 `page` 或 `component` 或 `template` 153 | [字符串] [必需] [可选值: "page", "component", "template"] 154 | name 名称 [字符串] [必需] 155 | 156 | 选项: 157 | -v, --version 当前版本号 [布尔] 158 | -h, --help 获取使用帮助 [布尔] 159 | -d, --target-dir 目标文件夹,默认为 src,也可以指定为 src 中的某个子目录 160 | [字符串] 161 | -i, --index 生成的文件名称为 [name]/index,默认为 [name]/[name] [布尔] 162 | ``` 163 | 164 | #### 清理 dist 目录 165 | 166 | ``` 167 | ewa clean 168 | 169 | 清理小程序静态文件 170 | 171 | 选项: 172 | -v, --version 当前版本号 [布尔] 173 | -h, --help 获取使用帮助 [布尔] 174 | -t, --type 构建目标 `weapp` 或 `swan` 或 `alipay` 或 `tt` 或 `qq` 175 | [字符串] [可选值: "weapp", "swan", "alipay", "tt", "qq"] [默认值: "weapp"] 176 | ``` 177 | 178 | ## 多端支持和环境变量 179 | 180 | ### 多端支持 181 | 182 | 目前 EWA 支持 **微信** / **百度** / **字节跳动** / **QQ** / **支付宝** 5个平台的小程序。 183 | 184 | 只需要基于`微信小程序`开发,可以通过命令行工具自动构建为不同平台的小程序,具体参见上方的命令行说明。 185 | 186 | 多端构建的 dist 目录分别为: 187 | 188 | ``` 189 | 微信: dist 190 | 百度: dist-swan 191 | 字节跳动: dist-tt 192 | QQ: dist-qq 193 | 支付宝: dist-alipay 194 | ``` 195 | 196 | ### 环境变量 197 | 198 | EWA 会提供 `process.env.EWA_ENV` 和 `process.env.NODE_ENV` 来辅助开发同学判断多端和不同的开发环境 199 | 200 | 可以在 .js 或 .ts 文件中直接使用,可选值见下方说明: 201 | 202 | ``` 203 | process.env.EWA_ENV: 多端支持的环境变量 204 | 可选值为 "weapp"、"swan"、"alipay"、"tt"、"qq", 默认是 "weapp" 205 | 206 | process.env.NODE_ENV: 开发环境变量 207 | 可选值为 "development" 和 "production", 分别对应 ewa start 和 ewa build 命令 208 | ``` 209 | 210 | ## 功能插件 211 | 212 | ### 微信接口 Promise 化 213 | 214 | ```javascript 215 | // 引入 216 | const { api } = require('ewa'); 217 | 218 | // 例: 219 | Page({ 220 | async onLoad() { 221 | let { data } = await api.request({ url: 'http://your_api_endpoint' }); 222 | } 223 | }) 224 | ``` 225 | 226 | ### 插件: `enableState` 227 | 228 | #### 用途 229 | 230 | 在 `Page` 和 `Component` 中引入 `this.setState(data, callback)` 方法, 并根据 data 数据自动 diff 出变更, 减少单次 data 提交的数据量,避免超过小程序 1mb 的限制 231 | 232 | #### 常见问题 233 | 234 | 1. 由于小程序本身的 bug, 当增量更新数组元素的时候, wxml 中无法正确获取到数组元素的 length 235 | 236 | #### 使用示例 237 | 238 | ```javascript 239 | // 在 app.js 中引入插件,并初始化 240 | const { enableState } = require('ewa'); 241 | 242 | // 参数支持: 243 | // opts: 参数对象 244 | // debug: 是否开启 debug 模式,支持3种参数: true, 'page', 'component', 默认为 false 245 | // component: 是否开启 component 支持, 默认为 true 246 | // page: 是否开启 page 支持, 默认为 true 247 | // overwriteArrayOnDeleted: 是否在数组发生删除操作是覆盖整个数组 true 或者 false, 默认为 true 248 | // autoSync: 是否在 调用 setData 时自动同步 state, 默认为 true; 如果关闭此操作,在同一个页面或组件中混用 setState 或 setData 的时候,可能会导致BUG, 也可以手动调用 this.syncState() 来手动同步 249 | enableState({ 250 | debug: true, 251 | component: true, 252 | page: true, 253 | overwriteArrayOnDeleted: true, 254 | autoSync: true 255 | }); 256 | 257 | // 上述插件会引入 this.setState 方法,在 Page 和 Component 中均可调用 258 | // setState 方法会自动 diff 并仅提交数据变更 259 | // 例: 260 | Page({ 261 | data: { a: 1, b: 1, c: { d: 1, e: 1 } } 262 | async onLoad() { 263 | // 自动 diff 变化 264 | // 相当于 this.setData({ b: 2, 'c.d': 2 }); 265 | this.setState({ a: 1, b: 2, c: { d: 2, e: 1 } }); 266 | 267 | // this.setState 支持使用 promise 来代替回调函数,如 268 | this.setState({ info: { name: 'My Page Title' } }).then(() => { 269 | // 数据已更新到视图, 这里写完成视图更新后的逻辑 270 | }); 271 | 272 | // 同理,这里可以使用 await 来简化 273 | await this.setState({ info: { name: 'My Page Title' } }); 274 | // 数据已更新到视图, 这里写完成视图更新后的逻辑 275 | } 276 | }) 277 | ``` 278 | 279 | ### 插件: `createStore` 280 | 281 | #### 用途 282 | 283 | 支持设置全局响应式对象, 能够监听对象属性并自动更新到 data 中 284 | 285 | #### 使用示例 286 | 287 | ```javascript 288 | // 1. 创建 store: 对任意纯对象调用 createStore 使其响应式化(以 app.js 中 globalData 为例) 289 | // app.js 中引入 290 | const { createStore } = require('ewa'); 291 | 292 | App({ 293 | ... 294 | globalData: createStore ( 295 | // 全局响应式对象 296 | { 297 | a: 'old1', 298 | b: { 299 | c: 'old2' 300 | } 301 | }, 302 | 303 | // 可以自定义注入到 Page 或 Component 中的方法和属性,也可以不设置 304 | // 如果设置了自定义的方法和变量 305 | // 那么实际在 Page 或 Component 中引用的时候需要相应的替换成 自定义的名称,示例如下: 306 | { 307 | $set: 'yourCustomSet', 308 | $on: 'yourCustomOn', 309 | $emit: 'yourCustomEmit', 310 | $off: 'yourCustomOff', 311 | $once: 'yourCustomOnce', 312 | $watch: 'yourCustomWatch' 313 | } 314 | ) 315 | }) 316 | 317 | // 2. 改变 globalData 以及全局状态更新(支持嵌套属性和数组下标修改) 318 | 319 | // pageA.js 320 | Page({ 321 | data: { 322 | a: '', 323 | b: { 324 | c: '' 325 | } 326 | } 327 | }) 328 | 329 | onLoad() { 330 | App().globalData.a = 'new1' 331 | console.log(this.data.a === 'new1') // true 332 | 333 | App().globalData.b.c = 'new2' 334 | console.log(this.data.b.c === 'new2') // true 335 | } 336 | 337 | 338 | // 3. 注入全局方法 使用示例: 339 | this.$on('test', (val) => { console.log(val) }) 340 | 341 | // 发射数据变化 342 | this.$emit('test', 'value'); 343 | 344 | // 使用方法同 this.$on 只会触发一次 345 | this.$once('test', (val) => {}); 346 | 347 | // 解绑当前实例通过 this.$on(...) 注册的事件 348 | this.$off('test'); 349 | 350 | // 以上方法适用于 351 | // 1. 页面与页面 352 | // 2. 页面与组件 353 | // 3. 组件与组件 354 | 355 | // 注: 所有页面或组件销毁时会自动解绑所有的事件(无需手动调用 `this.$off(...)`) 356 | // `this.$set('coinName', '金币')` 更新所有页面和组件 data 中 'coinName' 的值为 '金币'(支持嵌套属性和数组下标修改) 357 | 358 | // $watch 监听页面或组件 data 中属性 支持监听属性路径形式如 'a[1].b' 359 | 360 | // 使用示例: 361 | Page({ 362 | data: { 363 | prop: '', 364 | obj: { 365 | key: '' 366 | } 367 | }, 368 | $watch: { 369 | // 方式一 370 | 'prop': function(newVal, oldVal) { 371 | }, 372 | // 方式二 373 | 'obj': { 374 | handler: function(newVal, oldVal) { 375 | }, 376 | deep: Boolean, // 深度遍历 377 | immediate: Boolean // 立即触发 378 | } 379 | } 380 | }); 381 | ``` 382 | 383 | ## 配置 384 | 385 | ewa 通过 `ewa.config.js` 来支持个性化配置。如下所示: 386 | 387 | ``` javascript 388 | // ewa.config.js 389 | 390 | module.exports = { 391 | // 公用代码库 (node_modules 打包生成的文件)名称,默认为 vendors.js 392 | commonModuleName: 'vendors.js', 393 | 394 | // 通用模块匹配模式,默认为 /[\\/](node_modules|utils|vendor)[\\/].+\.js/ 395 | // 如需添加多个文件夹,可自定义正则,如 /[\\/](node_modules|utils|custom_dirname)[\\/].+\.js/ 396 | commonModulePattern: /[\\/](node_modules|utils|vendor)[\\/].+\.js/, 397 | 398 | // 是否简化路径,作用于 page 和 component,如 index/index.wxml=> index.wxml,默认为 false 399 | simplifyPath: false, 400 | 401 | // 文件夹快捷引用 402 | aliasDirs: [ 403 | 'apis', 404 | 'assets', 405 | 'constants', 406 | 'utils' 407 | ], 408 | 409 | // 需要拷贝的文件类型 410 | copyFileTypes: [ 411 | 'png', 412 | 'jpeg', 413 | 'jpg', 414 | 'gif', 415 | 'svg', 416 | 'ico' 417 | ], 418 | 419 | // webpack loader 规则 420 | rules: [], 421 | 422 | // webpack 插件 423 | plugins: [], 424 | 425 | // 开发环境下是否自动清理无用文件,默认为 true 426 | autoCleanUnusedFiles: true, 427 | 428 | // css 解析器,sass 或者 less,默认为 sass 429 | cssParser: 'sass', 430 | 431 | // 是否开启 hashed module id 432 | hashedModuleIds: true, 433 | 434 | // 是否开启缓存,默认为 true 435 | cache: true, 436 | 437 | // 自定义环境变量, 默认为 ['NODE_ENV', 'EWA_ENV'] 438 | customEnvironments: [], 439 | 440 | // 嫌不够灵活?直接修改 webpack 配置 441 | webpack: function(config) { 442 | return config; 443 | } 444 | }; 445 | ``` 446 | 447 | ## 常见问题 & Tips 448 | 449 | 1. 可以使用 `@` 来代替 **源代码根目录** 来引入代码或样式,如 `const utils = require('@/utils/util')` 450 | 2. WXSS 中可以直接编写 SCSS 样式代码 451 | 3. WXSS 或 SCSS 中引用绝对路径需要在路径前加 `~` 符号,如:`@import "~@/assets/styles/common.scss";`,具体原因参见: [sass-loader](https://github.com/webpack-contrib/sass-loader#imports) 452 | 4. `ewa build` 后如果无法正常运行小程序,可检查下是否关闭了微信开发者工具中的 `ES6 转 ES5` 和 `增强编译` 选项。原因是:ewa 打包时会将 ES6 转换为 ES5 并混淆压缩,此功能和微信开发者工具自带的 `ES6 转 ES5` 和 `增强编译` 功能有部分重复,多次转换会导致代码无法运行,所以只要关闭即可。 453 | 5. 其他问题欢迎直接在 Github 上提交 issue 454 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | EWA (微信小程序增强开发工具) 2 | ========================= 3 | 4 | Enhanced Wechat App Development Toolkit (微信小程序增强开发工具) 5 | 6 | ## 为什么开发这个工具? 7 | 8 | 厌倦了不停的对比 [taro](https://github.com/NervJS/taro)、[wepy](https://github.com/Tencent/wepy) 或者 [mpvue](https://github.com/Meituan-Dianping/mpvue) 的特性,间歇性的踩雷,构建和运行速度慢以及 `code once, run everywhere` 的幻想。只想给小程序开发插上效率的翅膀 ~ 9 | 10 | ## 功能特性 11 | 12 | 1. Async/Await 支持 13 | 2. Javascript ES2020 语法 14 | 3. 原生小程序所有功能,无需学习,极易上手 15 | 4. 微信接口 Promise 化 16 | 5. 支持安装 NPM 包 17 | 6. 支持 SCSS(或 LESS) 以及 小于 16k 的 background-image 18 | 7. 支持 source map, 方便调试 19 | 8. 添加新页面或新组件无需重启编译 20 | 9. 允许自定义编译流程 21 | 10. 自动兼容旧版本手机中的显示样式 22 | 11. 支持 WXSS 和 SCSS(或 LESS) 混用 23 | 12. 代码混淆及高度压缩,节省包大小 24 | 13. Typescript 支持 25 | 14. 支持转换成 百度 / 字节跳动 / QQ / 支付宝小程序 26 | 15. 多种小程序开发插件,为小程序开发减负,解放生产力 27 | 28 | [更多特性正在赶来 ... 敬请期待](./TODOS.md) 29 | 30 | ## 安装 31 | 32 | ***需要 node 版本 >= 10.13*** 33 | 34 | ```bash 35 | npm i -g ewa-cli 或者 yarn global add ewa-cli 36 | ``` 37 | 38 | ## 如何使用 39 | 40 | ### 创建新项目 41 | 42 | ```bash 43 | ewa new your_project_name 44 | ``` 45 | 46 | ### 集成到现有小程序项目,仅支持小程序原生开发项目转换 47 | 48 | ***注意:使用此方法,请务必对项目代码做好备份!!!*** 49 | 50 | ```bash 51 | cd your_project_dir && ewa init 52 | ``` 53 | 54 | ### 启动 55 | 56 | 运行 `npm start` 即可启动实时编译 57 | 58 | 运行 `npm run build` 即可编译线上版本(相比实时编译而言,去除了 source map 并增加了代码压缩混淆等,体积更小) 59 | 60 | 上述命令运行成功后,可以看到本地多了个 `dist` 目录,这个目录里就是生成的小程序相关代码。 61 | 62 | 使用[微信开发者工具](https://mp.weixin.qq.com/debug/wxadoc/dev/devtools/devtools.html)选择 `dist` 目录打开,即可预览项目 63 | 64 | ### 目录结构 65 | 66 | ``` 67 | ├── .ewa 特殊占位目录,用于检查是否为 ewa 项目 68 | ├── dist 小程序运行代码目录(该目录由ewa的start 或者 build指令自动编译生成,请不要直接修改该目录下的文件) 69 | ├── node_modules 外部依赖库 70 | ├── src 代码编写的目录(该目录为使用ewa后的开发目录) 71 | │   ├── components 小程序组件目录 72 | │   ├── pages 小程序页面目录 73 | │   │   ├── index 74 | │   │   │   ├── index.js 75 | │   │   │   ├── index.wxml 76 | │   │   │   └── index.wxss 77 | │   │   └── logs 78 | │   │   ├── logs.js 79 | │   │   ├── logs.json 80 | │   │   ├── logs.wxml 81 | │   │   └── logs.wxss 82 | │   ├── templates 小程序模版目录 83 | │   ├── utils 84 | │   │   └── util.js 85 | │   ├── app.js 小程序入口文件 86 | │   ├── app.json 小程序全局配置文件 87 | │   ├── app.wxss 小程序全局样式文件 88 | │   └── project.config.json 微信开发者工具小程序项目配置文件 89 | ├── ewa.config.js ewa 配置文件 90 | ├── .gitignore 91 | ├── .eslintrc.js eslint 配置 92 | └── package.json 93 | ``` 94 | 95 | ### 命令行说明 96 | 97 | #### 概览 98 | 99 | ``` 100 | ewa [args] 101 | 102 | 命令: 103 | ewa new 创建新的微信小程序项目 [别名: create] 104 | ewa init 在现有的小程序项目中初始化 EWA 105 | ewa start 启动 EWA 小程序项目实时编译 [别名: dev] 106 | ewa build 编译小程序静态文件 107 | ewa clean 清理小程序静态文件 108 | ewa upgrade 升级 EWA 工具 109 | ewa generate 快速生成模版 [别名: g] 110 | 111 | 选项: 112 | -v, --version 当前版本号 [布尔] 113 | -h, --help 获取使用帮助 [布尔] 114 | ``` 115 | 116 | #### 实时编译 117 | 118 | ``` 119 | ewa start 120 | 121 | 启动 EWA 小程序项目实时编译 122 | 123 | 选项: 124 | -v, --version 当前版本号 [布尔] 125 | -h, --help 获取使用帮助 [布尔] 126 | -t, --type 构建目标 `weapp` 或 `swan` 或 `alipay` 或 `tt` 或 `qq` 127 | [字符串] [可选值: "weapp", "swan", "alipay", "tt", "qq"] [默认值: "weapp"] 128 | ``` 129 | 130 | #### 构建 131 | 132 | ``` 133 | ewa build 134 | 135 | 编译小程序静态文件 136 | 137 | 选项: 138 | -v, --version 当前版本号 [布尔] 139 | -h, --help 获取使用帮助 [布尔] 140 | -t, --type 构建目标 `weapp` 或 `swan` 或 `alipay` 或 `tt` 或 `qq` 141 | [字符串] [可选值: "weapp", "swan", "alipay", "tt", "qq"] [默认值: "weapp"] 142 | ``` 143 | 144 | #### 快速生成样板文件 145 | 146 | ``` 147 | ewa generate 148 | 149 | 快速生成模版 150 | 151 | 位置: 152 | type 类型 `page` 或 `component` 或 `template` 153 | [字符串] [必需] [可选值: "page", "component", "template"] 154 | name 名称 [字符串] [必需] 155 | 156 | 选项: 157 | -v, --version 当前版本号 [布尔] 158 | -h, --help 获取使用帮助 [布尔] 159 | -d, --target-dir 目标文件夹,默认为 src,也可以指定为 src 中的某个子目录 160 | [字符串] 161 | -i, --index 生成的文件名称为 [name]/index,默认为 [name]/[name] [布尔] 162 | ``` 163 | 164 | #### 清理 dist 目录 165 | 166 | ``` 167 | ewa clean 168 | 169 | 清理小程序静态文件 170 | 171 | 选项: 172 | -v, --version 当前版本号 [布尔] 173 | -h, --help 获取使用帮助 [布尔] 174 | -t, --type 构建目标 `weapp` 或 `swan` 或 `alipay` 或 `tt` 或 `qq` 175 | [字符串] [可选值: "weapp", "swan", "alipay", "tt", "qq"] [默认值: "weapp"] 176 | ``` 177 | 178 | ## 多端支持和环境变量 179 | 180 | ### 多端支持 181 | 182 | 目前 EWA 支持 **微信** / **百度** / **字节跳动** / **QQ** / **支付宝** 5个平台的小程序。 183 | 184 | 只需要基于`微信小程序`开发,可以通过命令行工具自动构建为不同平台的小程序,具体参见上方的命令行说明。 185 | 186 | 多端构建的 dist 目录分别为: 187 | 188 | ``` 189 | 微信: dist 190 | 百度: dist-swan 191 | 字节跳动: dist-tt 192 | QQ: dist-qq 193 | 支付宝: dist-alipay 194 | ``` 195 | 196 | ### 环境变量 197 | 198 | EWA 会提供 `process.env.EWA_ENV` 和 `process.env.NODE_ENV` 来辅助开发同学判断多端和不同的开发环境 199 | 200 | 可以在 .js 或 .ts 文件中直接使用,可选值见下方说明: 201 | 202 | ``` 203 | process.env.EWA_ENV: 多端支持的环境变量 204 | 可选值为 "weapp"、"swan"、"alipay"、"tt"、"qq", 默认是 "weapp" 205 | 206 | process.env.NODE_ENV: 开发环境变量 207 | 可选值为 "development" 和 "production", 分别对应 ewa start 和 ewa build 命令 208 | ``` 209 | 210 | ## 功能插件 211 | 212 | ### 微信接口 Promise 化 213 | 214 | ```javascript 215 | // 引入 216 | const { api } = require('ewa'); 217 | 218 | // 例: 219 | Page({ 220 | async onLoad() { 221 | let { data } = await api.request({ url: 'http://your_api_endpoint' }); 222 | } 223 | }) 224 | ``` 225 | 226 | ### 插件: `enableState` 227 | 228 | #### 用途 229 | 230 | 在 `Page` 和 `Component` 中引入 `this.setState(data, callback)` 方法, 并根据 data 数据自动 diff 出变更, 减少单次 data 提交的数据量,避免超过小程序 1mb 的限制 231 | 232 | #### 常见问题 233 | 234 | 1. 由于小程序本身的 bug, 当增量更新数组元素的时候, wxml 中无法正确获取到数组元素的 length 235 | 236 | #### 使用示例 237 | 238 | ```javascript 239 | // 在 app.js 中引入插件,并初始化 240 | const { enableState } = require('ewa'); 241 | 242 | // 参数支持: 243 | // opts: 参数对象 244 | // debug: 是否开启 debug 模式,支持3种参数: true, 'page', 'component', 默认为 false 245 | // component: 是否开启 component 支持, 默认为 true 246 | // page: 是否开启 page 支持, 默认为 true 247 | // overwriteArrayOnDeleted: 是否在数组发生删除操作是覆盖整个数组 true 或者 false, 默认为 true 248 | // autoSync: 是否在 调用 setData 时自动同步 state, 默认为 true; 如果关闭此操作,在同一个页面或组件中混用 setState 或 setData 的时候,可能会导致BUG, 也可以手动调用 this.syncState() 来手动同步 249 | enableState({ 250 | debug: true, 251 | component: true, 252 | page: true, 253 | overwriteArrayOnDeleted: true, 254 | autoSync: true 255 | }); 256 | 257 | // 上述插件会引入 this.setState 方法,在 Page 和 Component 中均可调用 258 | // setState 方法会自动 diff 并仅提交数据变更 259 | // 例: 260 | Page({ 261 | data: { a: 1, b: 1, c: { d: 1, e: 1 } } 262 | async onLoad() { 263 | // 自动 diff 变化 264 | // 相当于 this.setData({ b: 2, 'c.d': 2 }); 265 | this.setState({ a: 1, b: 2, c: { d: 2, e: 1 } }); 266 | 267 | // this.setState 支持使用 promise 来代替回调函数,如 268 | this.setState({ info: { name: 'My Page Title' } }).then(() => { 269 | // 数据已更新到视图, 这里写完成视图更新后的逻辑 270 | }); 271 | 272 | // 同理,这里可以使用 await 来简化 273 | await this.setState({ info: { name: 'My Page Title' } }); 274 | // 数据已更新到视图, 这里写完成视图更新后的逻辑 275 | } 276 | }) 277 | ``` 278 | 279 | ### 插件: `createStore` 280 | 281 | #### 用途 282 | 283 | 支持设置全局响应式对象, 能够监听对象属性并自动更新到 data 中 284 | 285 | #### 使用示例 286 | 287 | ```javascript 288 | // 1. 创建 store: 对任意纯对象调用 createStore 使其响应式化(以 app.js 中 globalData 为例) 289 | // app.js 中引入 290 | const { createStore } = require('ewa'); 291 | 292 | App({ 293 | ... 294 | globalData: createStore ( 295 | // 全局响应式对象 296 | { 297 | a: 'old1', 298 | b: { 299 | c: 'old2' 300 | } 301 | }, 302 | 303 | // 可以自定义注入到 Page 或 Component 中的方法和属性,也可以不设置 304 | // 如果设置了自定义的方法和变量 305 | // 那么实际在 Page 或 Component 中引用的时候需要相应的替换成 自定义的名称,示例如下: 306 | { 307 | $set: 'yourCustomSet', 308 | $on: 'yourCustomOn', 309 | $emit: 'yourCustomEmit', 310 | $off: 'yourCustomOff', 311 | $once: 'yourCustomOnce', 312 | $watch: 'yourCustomWatch' 313 | } 314 | ) 315 | }) 316 | 317 | // 2. 改变 globalData 以及全局状态更新(支持嵌套属性和数组下标修改) 318 | 319 | // pageA.js 320 | Page({ 321 | data: { 322 | a: '', 323 | b: { 324 | c: '' 325 | } 326 | } 327 | }) 328 | 329 | onLoad() { 330 | App().globalData.a = 'new1' 331 | console.log(this.data.a === 'new1') // true 332 | 333 | App().globalData.b.c = 'new2' 334 | console.log(this.data.b.c === 'new2') // true 335 | } 336 | 337 | 338 | // 3. 注入全局方法 使用示例: 339 | this.$on('test', (val) => { console.log(val) }) 340 | 341 | // 发射数据变化 342 | this.$emit('test', 'value'); 343 | 344 | // 使用方法同 this.$on 只会触发一次 345 | this.$once('test', (val) => {}); 346 | 347 | // 解绑当前实例通过 this.$on(...) 注册的事件 348 | this.$off('test'); 349 | 350 | // 以上方法适用于 351 | // 1. 页面与页面 352 | // 2. 页面与组件 353 | // 3. 组件与组件 354 | 355 | // 注: 所有页面或组件销毁时会自动解绑所有的事件(无需手动调用 `this.$off(...)`) 356 | // `this.$set('coinName', '金币')` 更新所有页面和组件 data 中 'coinName' 的值为 '金币'(支持嵌套属性和数组下标修改) 357 | 358 | // $watch 监听页面或组件 data 中属性 支持监听属性路径形式如 'a[1].b' 359 | 360 | // 使用示例: 361 | Page({ 362 | data: { 363 | prop: '', 364 | obj: { 365 | key: '' 366 | } 367 | }, 368 | $watch: { 369 | // 方式一 370 | 'prop': function(newVal, oldVal) { 371 | }, 372 | // 方式二 373 | 'obj': { 374 | handler: function(newVal, oldVal) { 375 | }, 376 | deep: Boolean, // 深度遍历 377 | immediate: Boolean // 立即触发 378 | } 379 | } 380 | }); 381 | ``` 382 | 383 | ## 配置 384 | 385 | ewa 通过 `ewa.config.js` 来支持个性化配置。如下所示: 386 | 387 | ``` javascript 388 | // ewa.config.js 389 | 390 | module.exports = { 391 | // 公用代码库 (node_modules 打包生成的文件)名称,默认为 vendors.js 392 | commonModuleName: 'vendors.js', 393 | 394 | // 通用模块匹配模式,默认为 /[\\/](node_modules|utils|vendor)[\\/].+\.js/ 395 | // 如需添加多个文件夹,可自定义正则,如 /[\\/](node_modules|utils|custom_dirname)[\\/].+\.js/ 396 | commonModulePattern: /[\\/](node_modules|utils|vendor)[\\/].+\.js/, 397 | 398 | // 是否简化路径,作用于 page 和 component,如 index/index.wxml=> index.wxml,默认为 false 399 | simplifyPath: false, 400 | 401 | // 文件夹快捷引用 402 | aliasDirs: [ 403 | 'apis', 404 | 'assets', 405 | 'constants', 406 | 'utils' 407 | ], 408 | 409 | // 需要拷贝的文件类型 410 | copyFileTypes: [ 411 | 'png', 412 | 'jpeg', 413 | 'jpg', 414 | 'gif', 415 | 'svg', 416 | 'ico' 417 | ], 418 | 419 | // webpack loader 规则 420 | rules: [], 421 | 422 | // webpack 插件 423 | plugins: [], 424 | 425 | // 开发环境下是否自动清理无用文件,默认为 true 426 | autoCleanUnusedFiles: true, 427 | 428 | // css 解析器,sass 或者 less,默认为 sass 429 | cssParser: 'sass', 430 | 431 | // 是否开启 hashed module id 432 | hashedModuleIds: true, 433 | 434 | // 是否开启缓存,默认为 true 435 | cache: true, 436 | 437 | // 自定义环境变量, 默认为 ['NODE_ENV', 'EWA_ENV'] 438 | customEnvironments: [], 439 | 440 | // 嫌不够灵活?直接修改 webpack 配置 441 | webpack: function(config) { 442 | return config; 443 | } 444 | }; 445 | ``` 446 | 447 | 448 | ## 更新日志 449 | 450 | 本项目遵从 [Angular Style Commit Message Conventions](https://gist.github.com/stephenparish/9941e89d80e2bc58a153),更新日志请查阅 [Release](https://github.com/lyfeyaj/ewa/releases)。 451 | 452 | ## 常见问题 & Tips 453 | 454 | 1. 可以使用 `@` 来代替 **源代码根目录** 来引入代码或样式,如 `const utils = require('@/utils/util')` 455 | 2. WXSS 中可以直接编写 SCSS 样式代码 456 | 3. WXSS 或 SCSS 中引用绝对路径需要在路径前加 `~` 符号,如:`@import "~@/assets/styles/common.scss";`,具体原因参见: [sass-loader](https://github.com/webpack-contrib/sass-loader#imports) 457 | 4. `ewa build` 后如果无法正常运行小程序,可检查下是否关闭了微信开发者工具中的 `ES6 转 ES5` 和 `增强编译` 选项。原因是:ewa 打包时会将 ES6 转换为 ES5 并混淆压缩,此功能和微信开发者工具自带的 `ES6 转 ES5` 和 `增强编译` 功能有部分重复,多次转换会导致代码无法运行,所以只要关闭即可。 458 | 5. 其他问题欢迎直接在 Github 上提交 issue,也可以添加下方微信反馈(请注明来意 ^_^) 459 | 460 | ![lyfeyaj](https://raw.githubusercontent.com/lyfeyaj/ewa/master/docs/_media/wechat-qrcode.jpg) 461 | -------------------------------------------------------------------------------- /packages/webpack/lib/parsers/wxmlParser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const htmlparser2 = require('htmlparser2'); 4 | const serialize = require('dom-serializer').default; 5 | const path = require('path'); 6 | const utils = require('../utils'); 7 | 8 | /** 9 | * 转换 import 和 include 标签 10 | * 11 | * @param {Object} node 节点对象 12 | * @param {String} type 构建类型 13 | */ 14 | const FILE_TYPES_MAP = { 15 | tt: '.ttml', 16 | swan: '.swan', 17 | alipay: '.axml', 18 | qq: '.qml' 19 | }; 20 | 21 | const WXS_TYPES_MAP = { 22 | tt: 'sjs', 23 | swan: 'sjs', 24 | alipay: 'sjs', 25 | qq: 'qs' 26 | }; 27 | 28 | // 支付宝中 组件attr匹配命中前缀,需要更换写法 onXxxx catchXxxx 29 | const prefixBindMatcher = /^bind:|bind/; 30 | const prefixCatchMatcher = /^catch:|catch/; 31 | 32 | function tranformImport(node, type) { 33 | if (node.name !== 'import' && node.name !== 'include') return; 34 | 35 | const attribs = node.attribs; 36 | if (!attribs.src) return; 37 | 38 | attribs.src = attribs.src.replace(/\.wxml$/i, FILE_TYPES_MAP[type]); 39 | 40 | // src中没有扩展名的添加默认扩展名.swan 41 | if (!/\w+\.\w+$/.test(attribs.src)) attribs.src = attribs.src + FILE_TYPES_MAP[type]; 42 | } 43 | 44 | /** 45 | * 转换模板标签 46 | * 47 | * @param {Object} node 节点对象 48 | * @param {String} type 构建类型 49 | */ 50 | function tranformTemplate(node, type) { 51 | // 仅 百度小程序需要做这个转换 52 | if (type !== 'swan') return; 53 | if (node.name !== 'template') return; 54 | 55 | const attribs = node.attribs; 56 | if (!attribs.data) return; 57 | 58 | attribs.data = `{${attribs.data}}`; 59 | } 60 | 61 | /** 62 | * 转换wxs 63 | * 64 | * @param {Object} node 节点对象 65 | * @param {String} type 构建类型 66 | */ 67 | function transformWxs(node, type) { 68 | if (type === 'weapp') return; 69 | if (node.name === 'wxs') { 70 | 71 | const attribs = node.attribs; 72 | attribs.src = attribs.src.replace(/\.wxs$/i, `.${WXS_TYPES_MAP[type]}`); 73 | 74 | // 支付宝虽然文件名是sjs 但是标签是 75 | if (type === 'alipay') { 76 | node.name = 'import-' + WXS_TYPES_MAP[type]; 77 | attribs.name = attribs.module; 78 | attribs.from = attribs.src; 79 | delete attribs.module; 80 | delete attribs.src; 81 | } else { 82 | node.name = WXS_TYPES_MAP[type]; 83 | } 84 | } 85 | } 86 | 87 | /** 88 | * 转换标签上的 directive 89 | * 90 | * @param {Object} node 节点对象 91 | * @param {String} type 构建类型 92 | */ 93 | const DIRECTIVES_MAP = { 94 | tt: { 95 | 'wx:if': 'tt:if', 96 | 'wx:elif': 'tt:elif', 97 | 'wx:else': 'tt:else', 98 | 99 | 'wx:for': 'tt:for', 100 | 'wx:for-items': 'tt:for-items', 101 | 'wx:for-item': 'tt:for-item', 102 | 'wx:for-index': 'tt:for-index', 103 | 'wx:key': 'tt:key' 104 | }, 105 | swan: { 106 | 'wx:if': 's-if', 107 | 'wx:elif': 's-elif', 108 | 'wx:else': 's-else', 109 | 110 | 'wx:for': 's-for', 111 | 'wx:for-items': 's-for', 112 | 'wx:for-item': 's-for-item', 113 | 'wx:for-index': 's-for-index', 114 | 115 | // swan don't support 116 | 'wx:key': '' 117 | }, 118 | alipay: { 119 | 'wx:if': 'a:if', 120 | 'wx:elif': 'a:elif', 121 | 'wx:else': 'a:else', 122 | 123 | 'wx:for': 'a:for', 124 | 'wx:for-items': 'a:for-items', 125 | 'wx:for-item': 'a:for-item', 126 | 'wx:for-index': 'a:for-index', 127 | 'wx:key': 'a:key', 128 | 129 | // 事件名称替换 130 | // NOTE: 更多兼容需要支持 131 | bindtouchstart: 'onTouchStart', 132 | bindtouchmove: 'onTouchMove', 133 | bindtouchend: 'onTouchEnd', 134 | bindtouchcancel: 'onTouchCancel', 135 | bindtap: 'onTap', 136 | bindlongtap: 'onLongTap', 137 | bindload: 'onLoad', 138 | bindchange: 'onChange', 139 | bindtransition: 'onTransition', 140 | bindanimationfinish: 'onAnimationEnd', 141 | bindscrolltoupper: 'onScrollToUpper', 142 | bindscrolltolower: 'onScrollToLower', 143 | bindscroll: 'onScroll', 144 | binddragstart: 'onTouchStart', 145 | binddragging: 'onTouchMove', 146 | binddragend: 'onTouchEnd', 147 | bindConfirm: 'onConfirm', 148 | 149 | 'bind:touchstart': 'onTouchStart', 150 | 'bind:touchmove': 'onTouchMove', 151 | 'bind:touchend': 'onTouchEnd', 152 | 'bind:touchcancel': 'onTouchCancel', 153 | 'bind:tap': 'onTap', 154 | 'bind:longtap': 'onLongTap', 155 | 'bind:load': 'onLoad', 156 | 'bind:change': 'onChange', 157 | 'bind:transition': 'onTransition', 158 | 'bind:animationfinish': 'onAnimationEnd', 159 | 'bind:scrolltoupper': 'onScrollToUpper', 160 | 'bind:scrolltolower': 'onScrollToLower', 161 | 'bind:scroll': 'onScroll', 162 | 'bind:dragstart': 'onTouchStart', 163 | 'bind:dragging': 'onTouchMove', 164 | 'bind:dragend': 'onTouchEnd', 165 | 166 | 'catch:touchstart': 'catchTouchStart', 167 | 'catch:touchmove': 'catchTouchMove', 168 | 'catch:touchend': 'catchTouchEnd', 169 | 'catch:touchcancel': 'catchTouchCancel', 170 | 'catch:tap': 'catchTap', 171 | 'catch:longtap': 'catchLongTap', 172 | 'catch:load': 'catchLoad', 173 | 'catch:change': 'catchChange', 174 | 'catch:transition': 'catchTransition', 175 | 'catch:animationfinish': 'catchAnimationEnd', 176 | 'catch:scrolltoupper': 'catchScrollToUpper', 177 | 'catch:scrolltolower': 'catchScrollToLower', 178 | 'catch:scroll': 'catchScroll', 179 | 'catch:dragstart': 'catchTouchStart', 180 | 'catch:dragging': 'catchTouchMove', 181 | 'catch:dragend': 'catchTouchEnd', 182 | 183 | 'catchtouchstart': 'catchTouchStart', 184 | 'catchtouchmove': 'catchTouchMove', 185 | 'catchtouchend': 'catchTouchEnd', 186 | 'catchtouchcancel': 'catchTouchCancel', 187 | 'catchtap': 'catchTap', 188 | 'catchlongtap': 'catchLongTap', 189 | 'catchload': 'catchLoad', 190 | 'catchchange': 'catchChange', 191 | 'catchtransition': 'catchTransition', 192 | 'catchanimationfinish': 'catchAnimationEnd', 193 | 'catchscrolltoupper': 'catchScrollToUpper', 194 | 'catchscrolltolower': 'catchScrollToLower', 195 | 'catchscroll': 'catchScroll', 196 | 'catchdragstart': 'catchTouchStart', 197 | 'catchdragging': 'catchTouchMove', 198 | 'catchdragend': 'catchTouchEnd', 199 | }, 200 | qq: { 201 | 'wx:if': 'qq:if', 202 | 'wx:elif': 'qq:elif', 203 | 'wx:else': 'qq:else', 204 | 205 | 'wx:for': 'qq:for', 206 | 'wx:for-items': 'qq:for-items', 207 | 'wx:for-item': 'qq:for-item', 208 | 'wx:for-index': 'qq:for-index', 209 | 'wx:key': 'qq:key' 210 | } 211 | }; 212 | const DIRECTIVES_MAP_KEYS = {}; 213 | Object.keys(DIRECTIVES_MAP).forEach(type => { 214 | DIRECTIVES_MAP_KEYS[type] = Object.keys(DIRECTIVES_MAP[type]); 215 | }); 216 | 217 | function transformDirective(node, file, type) { 218 | let attribs = node.attribs; 219 | 220 | // swan 不支持绝对路径,这里部分替换为相对路径 221 | // 只能转换静态路径,动态拼接路径不支持转换 222 | if (node.name === 'image' && attribs.src) { 223 | if (!/^((\.\/)|(http))/.test(attribs.src)) { 224 | // 如果路径中包含判断逻辑,则不支持转换 225 | if (attribs.src.indexOf('{{') === -1) { 226 | let relativePath = path.relative('/' + path.dirname(file), attribs.src); 227 | attribs.src = relativePath; 228 | } 229 | } 230 | } 231 | 232 | // 百度小程序 swan 中不支持组件包含 type 属性 233 | if (type === 'swan' && ('type' in attribs)) { 234 | utils.log( 235 | `文件: \`${file}\` 中的 ${node.name} 元素包含 \`type\` 属性,会导致百度小程序报错,请替换属性名称`, 236 | 'warning' 237 | ); 238 | } 239 | 240 | // 替换对应的 directive 241 | DIRECTIVES_MAP_KEYS[type].forEach(attr => { 242 | if (!Object.prototype.hasOwnProperty.call(attribs, attr)) return; 243 | let newAttr = DIRECTIVES_MAP[type][attr]; 244 | if (newAttr) { 245 | // 百度小程序需要删除花括号 246 | if (type === 'swan') { 247 | attribs[newAttr] = removeBrackets(attribs[attr]); 248 | } 249 | // 其他小程序仅需要做替换 250 | else { 251 | attribs[newAttr] = attribs[attr]; 252 | } 253 | } 254 | 255 | delete attribs[attr]; 256 | }); 257 | 258 | // alipay中,需要对组件的函数传递处理 bind:xxxxx 改成 onXxxx 259 | if (type === 'alipay') { 260 | Object.keys(attribs).forEach(attr => { 261 | if (prefixBindMatcher.test(attr) || prefixCatchMatcher.test(attr)) { 262 | const newAttr = replaceCompAttr(attr); 263 | attribs[newAttr] = attribs[attr]; 264 | } 265 | }); 266 | } 267 | } 268 | 269 | /** 270 | * 将组件传递的函数的写法 转换为on:Xxxx 271 | * @param {string} oldAttr 原先属性值 bind:xxxx 272 | * @return {string} 处理后的属性值 onXxxx 273 | * */ 274 | function replaceCompAttr(oldAttr) { 275 | let newAttr = ''; 276 | let prefix = ''; 277 | if (prefixBindMatcher.test(oldAttr)) { 278 | newAttr = oldAttr.replace(prefixBindMatcher, ''); 279 | prefix = 'on'; 280 | } else { 281 | newAttr = oldAttr.replace(prefixCatchMatcher, ''); 282 | prefix = 'catch'; 283 | } 284 | newAttr = newAttr.replace(/^[a-zA-Z]{1}/, (s) => s.toUpperCase()); 285 | return prefix + newAttr; 286 | } 287 | 288 | /** 289 | * 丢掉属性值两侧的花括号 290 | * 291 | * @param {string} value 属性值 292 | * @return {string} 293 | */ 294 | function removeBrackets(value) { 295 | // wx:else 情况排除 296 | if (typeof value !== 'string') return value; 297 | value = value.trim(); 298 | if (/^{{.*}}$/.test(value)) return value.slice(2, -2).trim(); 299 | 300 | return value; 301 | } 302 | 303 | /** 304 | * 判断是否{{}}数据绑定 305 | * 306 | * @param {string} value 属性值 307 | * @return {boolean} 308 | */ 309 | function hasBrackets(value = '') { 310 | const trimed = value.trim(); 311 | return /^{{.*}}$/.test(trimed); 312 | } 313 | 314 | /** 315 | * for if 并存处理 316 | * 317 | * wx:for wx:if 并存 => wx:for 高优 318 | * eg: 319 | * hello -> hello 320 | * 321 | * wx:for wx:elif wx:else 并存 => wx:for 高优 322 | * eg: 323 | * hello -> hello 324 | */ 325 | const CONDITION_DIRECTIVES = ['wx:if', 'wx:elif', 'wx:else']; 326 | const FOR_DIRECTIVES = ['wx:for', 'wx:for-items', 'wx:for-item', 'wx:for-index', 'wx:key']; 327 | function curNodeTransformTwoNode(parentAttribs, curNode) { 328 | // copy curNode as new childNode 329 | let newChildNode = Object.assign( 330 | { prev: null, next: null }, 331 | curNode 332 | ); 333 | 334 | // curNode as parentNode 335 | let parentNode = Object.assign( 336 | curNode, 337 | { name: 'block', attribs: parentAttribs } 338 | ); 339 | 340 | newChildNode.parent = parentNode; 341 | parentNode.children = [newChildNode]; 342 | } 343 | function transformForIFDirective(node, type) { 344 | if (type !== 'swan') return; 345 | let attrs = node.attribs; 346 | if (!attrs['wx:for'] || !attrs['wx:if']) return; 347 | 348 | let parentAttribs = {}; 349 | CONDITION_DIRECTIVES.some(conditionItem => { 350 | if (!attrs[conditionItem]) { 351 | return false; 352 | } 353 | 354 | // wx:if 时 for 高优 355 | if (conditionItem === CONDITION_DIRECTIVES[0]) { 356 | FOR_DIRECTIVES.forEach(forItem => { 357 | attrs[forItem] && (parentAttribs[forItem] = attrs[forItem]); 358 | delete attrs[forItem]; 359 | }); 360 | } 361 | 362 | // 其他时 for 低优 363 | else { 364 | parentAttribs[conditionItem] = attrs[conditionItem]; 365 | delete attrs[conditionItem]; 366 | } 367 | 368 | return true; 369 | }); 370 | 371 | curNodeTransformTwoNode(parentAttribs, node); 372 | } 373 | 374 | /** 375 | * 转换数据绑定为双向绑定语法,仅百度小程序需要转换 376 | * 377 | * @param {Object} node 节点对象 378 | * @param {String} type 构建类型 379 | */ 380 | const BIND_DATA_MAP = { 381 | 'scroll-view': ['scroll-top', 'scroll-left', 'scroll-into-view'], 382 | 'input': ['value'], 383 | 'textarea': ['value'], 384 | 'movable-view': ['x', 'y'], 385 | 'slider': ['value'] 386 | }; 387 | function tranformBindData(node, type) { 388 | // 仅百度小程序需要转换 389 | if (type !== 'swan') return; 390 | 391 | const attrs = BIND_DATA_MAP[node.name]; 392 | if (!attrs) return; 393 | const attribs = node.attribs; 394 | attrs.forEach(attr => { 395 | if (!attribs[attr]) return; 396 | if (!hasBrackets(attribs[attr])) return; 397 | 398 | attribs[attr] = `{=${removeBrackets(attribs[attr])}=}`; 399 | }); 400 | } 401 | 402 | /** 403 | * 转换style 404 | * 无请求头的css静态资源url添加https请求头 405 | * 406 | * @param {Object} node 节点对象 407 | */ 408 | function transformStyle(node) { 409 | const attribs = node.attribs; 410 | if (!attribs.style) return; 411 | attribs.style = transformCssStaticUrl(attribs.style); 412 | } 413 | 414 | /** 415 | * 无请求头的css静态资源url添加https请求头 416 | * 417 | * @param {string} content 文件内容 418 | * @return {string} 处理后文件内容 419 | */ 420 | function transformCssStaticUrl(content) { 421 | content = content.replace(/url\((.*)\)/g, function ($1, $2) { 422 | if (!$2) return $1; 423 | const res = $2.replace(/^(['"\s^]?)(\/\/.*)/, function ($1, $2, $3) { 424 | const resUrl = `${$2}https:${$3}`; 425 | return resUrl; 426 | }); 427 | return `url(${res})`; 428 | }); 429 | return content; 430 | } 431 | 432 | // 转换为 swan 抹平差异 433 | function transformToTargetType(node, file, type) { 434 | if (Array.isArray(node)) return node.map(n => transformToTargetType(n, file, type)); 435 | if (node.type !== 'tag') return node; 436 | 437 | node.attribs = node.attribs || {}; 438 | node.children = node.children || []; 439 | 440 | tranformImport(node, type); 441 | tranformTemplate(node, type); 442 | tranformBindData(node, type); 443 | transformForIFDirective(node, type); 444 | transformDirective(node, file, type); 445 | transformStyle(node); 446 | transformWxs(node, type); 447 | 448 | node.children.map(n => transformToTargetType(n, file, type)); 449 | 450 | return node; 451 | } 452 | 453 | // 转换 wxml 为 swan 454 | module.exports = function wxmlParser(content = '', file = '', type = '') { 455 | let nodes = htmlparser2.parseDOM( 456 | content, 457 | { 458 | // 需要能够识别 自闭合标签 459 | xmlMode: false, 460 | decodeEntities: false, 461 | lowerCaseTags: false, 462 | lowerCaseAttributeNames: false, 463 | recognizeCDATA: true, 464 | recognizeSelfClosing: true, 465 | } 466 | ); 467 | 468 | // 转换为 目标构建平台 支持 469 | nodes = transformToTargetType(nodes, file, type); 470 | 471 | let html = serialize( 472 | nodes, 473 | { 474 | selfClosingTags: true, 475 | xmlMode: false, 476 | decodeEntities: false 477 | } 478 | ); 479 | 480 | // NOTE: 文本替换 " => ' 481 | // 由于wxml代码编写不规范,导致单双引号混用,解析时无法正确还原 482 | // 由于每个属性都检查一遍比较消耗性能,这里直接做替换,可能会导致显示的问题 483 | // eslint-disable-next-line quotes 484 | return html.replace(/"/g, "'"); 485 | }; 486 | --------------------------------------------------------------------------------