├── 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 |
2 |
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 | 
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 | 
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 |
--------------------------------------------------------------------------------