├── src ├── template │ ├── app.wxss │ ├── app.js │ ├── common │ │ └── pages │ │ │ └── default │ │ │ ├── default.wxss │ │ │ ├── default.json │ │ │ ├── default.wxml │ │ │ └── default.js │ ├── components │ │ ├── plain-text │ │ │ ├── plain-text.wxss │ │ │ ├── plain-text.wxml │ │ │ ├── plain-text.json │ │ │ └── plain-text.js │ │ └── element │ │ │ └── element.wxss │ ├── adapter │ │ ├── event │ │ │ ├── custom-event.js │ │ │ └── event.js │ │ ├── bom │ │ │ ├── screen.js │ │ │ ├── navigator.js │ │ │ ├── session-storage.js │ │ │ ├── local-storage.js │ │ │ └── history.js │ │ ├── node │ │ │ ├── comment.js │ │ │ ├── element │ │ │ │ ├── a.js │ │ │ │ ├── input.js │ │ │ │ ├── canvas.js │ │ │ │ └── image.js │ │ │ ├── text-node.js │ │ │ ├── node.js │ │ │ ├── style-list.js │ │ │ ├── class-list.js │ │ │ ├── attribute.js │ │ │ └── style.js │ │ ├── util │ │ │ ├── tool.js │ │ │ └── cache.js │ │ ├── tree │ │ │ ├── tag-map.js │ │ │ └── tree.js │ │ └── index.js │ └── element.wxml.tmpl ├── config.js ├── js │ ├── ignore-rewrite-var.js │ ├── ignore-global-var.js │ ├── adjust.js │ ├── page.js.tmpl │ └── index.js ├── json.js ├── html │ ├── index.js │ └── walker.js ├── css │ ├── index.js │ └── adjust.js ├── index.js └── utils.js ├── index.js ├── test ├── demo │ ├── demo05 │ │ ├── a.mp3 │ │ ├── imgs │ │ │ ├── 4-1.png │ │ │ ├── 4-2.png │ │ │ ├── big1.png │ │ │ ├── big2.png │ │ │ ├── big3.png │ │ │ ├── big4.png │ │ │ ├── big5.png │ │ │ ├── big6.png │ │ │ ├── lv1.png │ │ │ ├── lv2.png │ │ │ ├── lv3.png │ │ │ ├── note.png │ │ │ ├── num1.png │ │ │ ├── num2.png │ │ │ ├── num3.png │ │ │ ├── pass.png │ │ │ ├── sure.png │ │ │ ├── yes.png │ │ │ ├── giftbox.png │ │ │ ├── index.png │ │ │ ├── invite.png │ │ │ ├── openbox.png │ │ │ ├── restart.png │ │ │ ├── return.png │ │ │ ├── share.png │ │ │ ├── titlep.png │ │ │ ├── winnbg.png │ │ │ ├── download.png │ │ │ ├── gamelose.png │ │ │ ├── gamerule.png │ │ │ ├── inputimg.png │ │ │ ├── loading3.gif │ │ │ ├── no-award.png │ │ │ ├── startgame.png │ │ │ ├── zhidaole.png │ │ │ ├── background.png │ │ │ ├── downloadBtn.png │ │ │ ├── have-award.jpg │ │ │ ├── have-award1.png │ │ │ ├── jump_award.png │ │ │ └── youxishuoming1.png │ │ ├── js │ │ │ └── rem.js │ │ ├── index.html │ │ └── css │ │ │ └── index.css │ ├── demo04 │ │ ├── images │ │ │ ├── bg.png │ │ │ ├── arrow.png │ │ │ ├── bg1.png │ │ │ ├── bg2.png │ │ │ ├── logo.png │ │ │ ├── title.png │ │ │ ├── border.png │ │ │ ├── border1.png │ │ │ ├── border2.png │ │ │ ├── home_bg.png │ │ │ ├── liner1.png │ │ │ ├── middle.png │ │ │ ├── occupy-img.png │ │ │ ├── final-title.png │ │ │ ├── footer-left.png │ │ │ ├── footer-right.png │ │ │ ├── left-border.png │ │ │ ├── right-border.png │ │ │ ├── static_data1.png │ │ │ ├── static_data2.png │ │ │ ├── static_data3.png │ │ │ ├── static_data4.png │ │ │ ├── border-shading1.png │ │ │ ├── border-shading2.png │ │ │ ├── connect-linear.png │ │ │ ├── current_period.png │ │ │ ├── final-border1.png │ │ │ ├── final-border2.png │ │ │ ├── home-head-right.png │ │ │ ├── previous_period.png │ │ │ ├── static-border.png │ │ │ ├── static_level1.png │ │ │ ├── static_level2.png │ │ │ ├── static_level3.png │ │ │ ├── final-footer-left.png │ │ │ ├── home-top-shading.png │ │ │ ├── final-footer-right.png │ │ │ ├── footer-left-shading.png │ │ │ ├── footer-right-shading.png │ │ │ ├── home-right-shading.png │ │ │ └── footer-middle-shading.png │ │ └── js │ │ │ ├── rem.js │ │ │ └── swiper.animate1.0.2.min.js │ ├── demo03 │ │ ├── images │ │ │ ├── logo.png │ │ │ ├── zjnews_phone@2x.png │ │ │ ├── zjnews_phone@3x.png │ │ │ ├── zjnews_web@2x.png │ │ │ ├── zjnews_web@3x.png │ │ │ ├── zjnews_back_icon@2x.png │ │ │ └── zjnews_back_icon@3x.png │ │ ├── index.html │ │ └── css │ │ │ └── index.css │ ├── demo06 │ │ ├── images │ │ │ ├── 200.gif │ │ │ ├── person.png │ │ │ ├── header_bg.png │ │ │ ├── person_1.png │ │ │ └── header-border.png │ │ ├── webpack.config.js │ │ ├── package.json │ │ ├── js │ │ │ └── rem.js │ │ ├── mock │ │ │ └── vote.js │ │ └── index.html │ ├── demo07 │ │ ├── src │ │ │ ├── main.js │ │ │ ├── Btn.vue │ │ │ ├── App.vue │ │ │ └── Content.vue │ │ ├── .babelrc │ │ ├── dist │ │ │ └── style.css │ │ ├── README.md │ │ ├── package.json │ │ ├── index.html │ │ └── webpack.config.js │ ├── demo09 │ │ ├── .babelrc │ │ ├── src │ │ │ ├── AAA.vue │ │ │ ├── BBB.vue │ │ │ ├── main.js │ │ │ └── App.vue │ │ ├── README.md │ │ ├── dist │ │ │ └── style.css │ │ ├── package.json │ │ ├── index.html │ │ └── webpack.config.js │ ├── demo10 │ │ ├── .babelrc │ │ ├── src │ │ │ ├── AAA.vue │ │ │ ├── BBB.vue │ │ │ ├── main.js │ │ │ └── App.vue │ │ ├── README.md │ │ ├── dist │ │ │ └── style.css │ │ ├── package.json │ │ ├── index.html │ │ └── webpack.config.js │ ├── demo08 │ │ ├── .babelrc │ │ ├── src │ │ │ ├── index.jsx │ │ │ ├── btn.jsx │ │ │ ├── app.jsx │ │ │ └── content.jsx │ │ ├── package.json │ │ ├── webpack.config.js │ │ └── index.html │ ├── demo11 │ │ ├── .babelrc │ │ ├── src │ │ │ ├── index.jsx │ │ │ ├── aaa.jsx │ │ │ ├── bbb.jsx │ │ │ └── app.jsx │ │ ├── webpack.config.js │ │ ├── package.json │ │ └── index.html │ ├── demo12 │ │ ├── .babelrc │ │ ├── src │ │ │ ├── index.jsx │ │ │ ├── aaa.jsx │ │ │ ├── bbb.jsx │ │ │ └── app.jsx │ │ ├── webpack.config.js │ │ ├── package.json │ │ └── index.html │ ├── extend1.js │ ├── extend2.js │ ├── demo02 │ │ ├── index.html │ │ └── css │ │ │ └── index.css │ ├── server.js │ ├── test.js │ └── demo01 │ │ └── index.js ├── html │ └── walker.test.js ├── js │ └── adjust.test.js ├── adapter │ ├── node │ │ ├── style.test.js │ │ ├── class-list.test.js │ │ └── image.test.js │ ├── bom │ │ ├── xml-http-request.test.js │ │ └── cookie.test.js │ └── window.test.js ├── mock.js └── css │ └── adjust.test.js ├── .gitignore ├── .npmignore ├── UPDATE.md ├── LICENSE ├── docs ├── question.md └── plan.md ├── package.json └── .eslintrc.js /src/template/app.wxss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/template/app.js: -------------------------------------------------------------------------------- 1 | App({}) 2 | -------------------------------------------------------------------------------- /src/template/common/pages/default/default.wxss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./src/index'); 2 | -------------------------------------------------------------------------------- /src/template/common/pages/default/default.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /src/template/components/plain-text/plain-text.wxss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | indent: ' ', // 缩进 3 | } 4 | -------------------------------------------------------------------------------- /src/template/components/plain-text/plain-text.wxml: -------------------------------------------------------------------------------- 1 | {{content}} 2 | -------------------------------------------------------------------------------- /src/template/common/pages/default/default.wxml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/template/components/plain-text/plain-text.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": true, 3 | "usingComponents": {} 4 | } -------------------------------------------------------------------------------- /test/demo/demo05/a.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/a.mp3 -------------------------------------------------------------------------------- /src/js/ignore-rewrite-var.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | 'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval' 3 | ] 4 | -------------------------------------------------------------------------------- /test/demo/demo04/images/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/bg.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/4-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/4-1.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/4-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/4-2.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/big1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/big1.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/big2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/big2.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/big3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/big3.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/big4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/big4.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/big5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/big5.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/big6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/big6.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/lv1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/lv1.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/lv2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/lv2.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/lv3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/lv3.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/note.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/note.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/num1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/num1.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/num2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/num2.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/num3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/num3.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/pass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/pass.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/sure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/sure.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/yes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/yes.png -------------------------------------------------------------------------------- /test/demo/demo03/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo03/images/logo.png -------------------------------------------------------------------------------- /test/demo/demo04/images/arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/arrow.png -------------------------------------------------------------------------------- /test/demo/demo04/images/bg1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/bg1.png -------------------------------------------------------------------------------- /test/demo/demo04/images/bg2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/bg2.png -------------------------------------------------------------------------------- /test/demo/demo04/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/logo.png -------------------------------------------------------------------------------- /test/demo/demo04/images/title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/title.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/giftbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/giftbox.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/index.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/invite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/invite.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/openbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/openbox.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/restart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/restart.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/return.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/return.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/share.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/titlep.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/titlep.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/winnbg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/winnbg.png -------------------------------------------------------------------------------- /test/demo/demo06/images/200.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo06/images/200.gif -------------------------------------------------------------------------------- /test/demo/demo04/images/border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/border.png -------------------------------------------------------------------------------- /test/demo/demo04/images/border1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/border1.png -------------------------------------------------------------------------------- /test/demo/demo04/images/border2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/border2.png -------------------------------------------------------------------------------- /test/demo/demo04/images/home_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/home_bg.png -------------------------------------------------------------------------------- /test/demo/demo04/images/liner1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/liner1.png -------------------------------------------------------------------------------- /test/demo/demo04/images/middle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/middle.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/download.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/gamelose.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/gamelose.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/gamerule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/gamerule.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/inputimg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/inputimg.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/loading3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/loading3.gif -------------------------------------------------------------------------------- /test/demo/demo05/imgs/no-award.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/no-award.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/startgame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/startgame.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/zhidaole.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/zhidaole.png -------------------------------------------------------------------------------- /test/demo/demo06/images/person.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo06/images/person.png -------------------------------------------------------------------------------- /test/demo/demo04/images/occupy-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/occupy-img.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/background.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/downloadBtn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/downloadBtn.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/have-award.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/have-award.jpg -------------------------------------------------------------------------------- /test/demo/demo05/imgs/have-award1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/have-award1.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/jump_award.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/jump_award.png -------------------------------------------------------------------------------- /test/demo/demo06/images/header_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo06/images/header_bg.png -------------------------------------------------------------------------------- /test/demo/demo06/images/person_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo06/images/person_1.png -------------------------------------------------------------------------------- /test/demo/demo04/images/final-title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/final-title.png -------------------------------------------------------------------------------- /test/demo/demo04/images/footer-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/footer-left.png -------------------------------------------------------------------------------- /test/demo/demo04/images/footer-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/footer-right.png -------------------------------------------------------------------------------- /test/demo/demo04/images/left-border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/left-border.png -------------------------------------------------------------------------------- /test/demo/demo04/images/right-border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/right-border.png -------------------------------------------------------------------------------- /test/demo/demo04/images/static_data1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/static_data1.png -------------------------------------------------------------------------------- /test/demo/demo04/images/static_data2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/static_data2.png -------------------------------------------------------------------------------- /test/demo/demo04/images/static_data3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/static_data3.png -------------------------------------------------------------------------------- /test/demo/demo04/images/static_data4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/static_data4.png -------------------------------------------------------------------------------- /test/demo/demo05/imgs/youxishuoming1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo05/imgs/youxishuoming1.png -------------------------------------------------------------------------------- /test/demo/demo03/images/zjnews_phone@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo03/images/zjnews_phone@2x.png -------------------------------------------------------------------------------- /test/demo/demo03/images/zjnews_phone@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo03/images/zjnews_phone@3x.png -------------------------------------------------------------------------------- /test/demo/demo03/images/zjnews_web@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo03/images/zjnews_web@2x.png -------------------------------------------------------------------------------- /test/demo/demo03/images/zjnews_web@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo03/images/zjnews_web@3x.png -------------------------------------------------------------------------------- /test/demo/demo04/images/border-shading1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/border-shading1.png -------------------------------------------------------------------------------- /test/demo/demo04/images/border-shading2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/border-shading2.png -------------------------------------------------------------------------------- /test/demo/demo04/images/connect-linear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/connect-linear.png -------------------------------------------------------------------------------- /test/demo/demo04/images/current_period.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/current_period.png -------------------------------------------------------------------------------- /test/demo/demo04/images/final-border1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/final-border1.png -------------------------------------------------------------------------------- /test/demo/demo04/images/final-border2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/final-border2.png -------------------------------------------------------------------------------- /test/demo/demo04/images/home-head-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/home-head-right.png -------------------------------------------------------------------------------- /test/demo/demo04/images/previous_period.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/previous_period.png -------------------------------------------------------------------------------- /test/demo/demo04/images/static-border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/static-border.png -------------------------------------------------------------------------------- /test/demo/demo04/images/static_level1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/static_level1.png -------------------------------------------------------------------------------- /test/demo/demo04/images/static_level2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/static_level2.png -------------------------------------------------------------------------------- /test/demo/demo04/images/static_level3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/static_level3.png -------------------------------------------------------------------------------- /test/demo/demo06/images/header-border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo06/images/header-border.png -------------------------------------------------------------------------------- /test/demo/demo07/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | 4 | new Vue({ 5 | el: '#app', 6 | render: h => h(App) 7 | }) 8 | -------------------------------------------------------------------------------- /test/demo/demo04/images/final-footer-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/final-footer-left.png -------------------------------------------------------------------------------- /test/demo/demo04/images/home-top-shading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/home-top-shading.png -------------------------------------------------------------------------------- /test/demo/demo03/images/zjnews_back_icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo03/images/zjnews_back_icon@2x.png -------------------------------------------------------------------------------- /test/demo/demo03/images/zjnews_back_icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo03/images/zjnews_back_icon@3x.png -------------------------------------------------------------------------------- /test/demo/demo04/images/final-footer-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/final-footer-right.png -------------------------------------------------------------------------------- /test/demo/demo04/images/footer-left-shading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/footer-left-shading.png -------------------------------------------------------------------------------- /test/demo/demo04/images/footer-right-shading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/footer-right-shading.png -------------------------------------------------------------------------------- /test/demo/demo04/images/home-right-shading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/home-right-shading.png -------------------------------------------------------------------------------- /test/demo/demo07/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false 5 | }] 6 | ], 7 | "plugins": [ 8 | ] 9 | } -------------------------------------------------------------------------------- /test/demo/demo09/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false 5 | }] 6 | ], 7 | "plugins": [ 8 | ] 9 | } -------------------------------------------------------------------------------- /test/demo/demo10/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false 5 | }] 6 | ], 7 | "plugins": [ 8 | ] 9 | } -------------------------------------------------------------------------------- /test/demo/demo04/images/footer-middle-shading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/h5-to-miniprogram/HEAD/test/demo/demo04/images/footer-middle-shading.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | package-lock.json 4 | 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | node_modules 12 | test/**/output 13 | -------------------------------------------------------------------------------- /src/template/components/element/element.wxss: -------------------------------------------------------------------------------- 1 | .wx-comp-image { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | 6 | .wx-comp-canvas { 7 | width: 100%; 8 | height: 100%; 9 | } 10 | -------------------------------------------------------------------------------- /test/demo/demo08/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env", 4 | "stage-0", 5 | "react" 6 | ], 7 | "plugins": [ 8 | "transform-runtime" 9 | ] 10 | } -------------------------------------------------------------------------------- /test/demo/demo11/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env", 4 | "stage-0", 5 | "react" 6 | ], 7 | "plugins": [ 8 | "transform-runtime" 9 | ] 10 | } -------------------------------------------------------------------------------- /test/demo/demo12/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env", 4 | "stage-0", 5 | "react" 6 | ], 7 | "plugins": [ 8 | "transform-runtime" 9 | ] 10 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | package-lock.json 4 | 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | node_modules 12 | test/**/output 13 | 14 | test 15 | docs -------------------------------------------------------------------------------- /UPDATE.md: -------------------------------------------------------------------------------- 1 | ## 0.0.5 2 | 3 | * 支持 cookie 4 | * 支持 hashchange 事件 5 | * 支持 document.createComment 接口 6 | * 支持 history 7 | * 支持压缩原 h5 页面代码 8 | 9 | ## 0.0.6 10 | 11 | * 支持当原 h5 页面引用了一些网络 js、css 文件也能进行正常构建转换 12 | 13 | ## next version 14 | -------------------------------------------------------------------------------- /test/demo/demo08/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | 4 | import App from './app.jsx'; 5 | 6 | const rootEl = document.getElementById('app'); 7 | 8 | render( 9 | , 10 | rootEl 11 | ); -------------------------------------------------------------------------------- /test/demo/demo11/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | 4 | import App from './app.jsx'; 5 | 6 | const rootEl = document.getElementById('app'); 7 | 8 | render( 9 | , 10 | rootEl 11 | ); -------------------------------------------------------------------------------- /test/demo/demo12/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | 4 | import App from './app.jsx'; 5 | 6 | const rootEl = document.getElementById('app'); 7 | 8 | render( 9 | , 10 | rootEl 11 | ); -------------------------------------------------------------------------------- /src/template/common/pages/default/default.js: -------------------------------------------------------------------------------- 1 | Page({ 2 | data: { 3 | src: '', 4 | }, 5 | 6 | onLoad({url}) { 7 | if (url) { 8 | this.setData({ 9 | src: decodeURIComponent(url), 10 | }) 11 | } 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /test/demo/demo08/src/btn.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class Btn extends Component { 4 | render() { 5 | return ( 6 | 7 | ); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/demo/extend1.js: -------------------------------------------------------------------------------- 1 | module.exports = function(loadModule, moduleName) { 2 | if (moduleName === 'Window') { 3 | loadModule.prototype.I_am_extend_function = function () { 4 | return 'I am extend function' 5 | } 6 | } else { 7 | return loadModule 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/demo/extend2.js: -------------------------------------------------------------------------------- 1 | module.exports = function(loadModule, moduleName) { 2 | if (moduleName === 'Window') { 3 | loadModule.prototype.I_am_another_extend_function = function () { 4 | return 'I am another extend function' 5 | } 6 | } else { 7 | return loadModule 8 | } 9 | } -------------------------------------------------------------------------------- /src/template/adapter/event/custom-event.js: -------------------------------------------------------------------------------- 1 | const load = require('../index') 2 | 3 | const Event = load('Event') 4 | 5 | class CustomEvent extends Event { 6 | constructor(name = '', options = {}) { 7 | super({ 8 | name, 9 | ...options, 10 | }) 11 | } 12 | } 13 | 14 | module.exports = CustomEvent 15 | -------------------------------------------------------------------------------- /test/demo/demo11/src/aaa.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class AAA extends Component { 4 | render() { 5 | return ( 6 |
7 |

I am aaa

8 |

route: {this.props.match.path}

9 |
10 | ); 11 | } 12 | } -------------------------------------------------------------------------------- /test/demo/demo11/src/bbb.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class BBB extends Component { 4 | render() { 5 | return ( 6 |
7 |

I am bbb

8 |

route: {this.props.match.path}

9 |
10 | ); 11 | } 12 | } -------------------------------------------------------------------------------- /test/demo/demo12/src/aaa.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class AAA extends Component { 4 | render() { 5 | return ( 6 |
7 |

I am aaa

8 |

route: {this.props.match.path}

9 |
10 | ); 11 | } 12 | } -------------------------------------------------------------------------------- /test/demo/demo12/src/bbb.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class BBB extends Component { 4 | render() { 5 | return ( 6 |
7 |

I am bbb

8 |

route: {this.props.match.path}

9 |
10 | ); 11 | } 12 | } -------------------------------------------------------------------------------- /test/demo/demo09/src/AAA.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/demo/demo09/src/BBB.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/demo/demo10/src/AAA.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/demo/demo10/src/BBB.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/demo/demo07/src/Btn.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/demo/demo07/dist/style.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | .even { 52 | color: green; 53 | } 54 | .odd { 55 | color: red; 56 | } 57 | -------------------------------------------------------------------------------- /test/demo/demo07/README.md: -------------------------------------------------------------------------------- 1 | # demo7 2 | 3 | > A Vue.js project 4 | 5 | ## Build Setup 6 | 7 | ``` bash 8 | # install dependencies 9 | npm install 10 | 11 | # serve with hot reload at localhost:8080 12 | npm run dev 13 | 14 | # build for production with minification 15 | npm run build 16 | ``` 17 | 18 | For detailed explanation on how things work, consult the [docs for vue-loader](http://vuejs.github.io/vue-loader). 19 | -------------------------------------------------------------------------------- /test/demo/demo09/README.md: -------------------------------------------------------------------------------- 1 | # demo7 2 | 3 | > A Vue.js project 4 | 5 | ## Build Setup 6 | 7 | ``` bash 8 | # install dependencies 9 | npm install 10 | 11 | # serve with hot reload at localhost:8080 12 | npm run dev 13 | 14 | # build for production with minification 15 | npm run build 16 | ``` 17 | 18 | For detailed explanation on how things work, consult the [docs for vue-loader](http://vuejs.github.io/vue-loader). 19 | -------------------------------------------------------------------------------- /test/demo/demo10/README.md: -------------------------------------------------------------------------------- 1 | # demo7 2 | 3 | > A Vue.js project 4 | 5 | ## Build Setup 6 | 7 | ``` bash 8 | # install dependencies 9 | npm install 10 | 11 | # serve with hot reload at localhost:8080 12 | npm run dev 13 | 14 | # build for production with minification 15 | npm run build 16 | ``` 17 | 18 | For detailed explanation on how things work, consult the [docs for vue-loader](http://vuejs.github.io/vue-loader). 19 | -------------------------------------------------------------------------------- /src/json.js: -------------------------------------------------------------------------------- 1 | const _ = require('./utils') 2 | const config = require('./config') 3 | 4 | module.exports = { 5 | /** 6 | * 生成配置 7 | */ 8 | async generate(options) { 9 | const output = options.output 10 | const content = JSON.stringify({ 11 | usingComponents: { 12 | body: '../../components/element/element', 13 | } 14 | }, null, config.indent.length) 15 | 16 | // 输出到 output 中 17 | await _.writeFile(output, content) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/demo/demo10/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import App from './App.vue' 4 | import AAA from './AAA.vue' 5 | import BBB from './BBB.vue' 6 | 7 | Vue.use(Router) 8 | 9 | const router = new Router({ 10 | mode: 'history', // 是否使用 history api 11 | routes: [ 12 | { path: '/h5-to-miniprogram/aaa', component: AAA }, 13 | { path: '/h5-to-miniprogram/bbb', component: BBB } 14 | ] 15 | }) 16 | 17 | new Vue({ 18 | el: '#app', 19 | router, 20 | render: h => h(App) 21 | }) 22 | -------------------------------------------------------------------------------- /test/demo/demo09/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import App from './App.vue' 4 | import AAA from './AAA.vue' 5 | import BBB from './BBB.vue' 6 | 7 | Vue.use(Router) 8 | 9 | const router = new Router({ 10 | // mode: 'history', // 是否使用 history api 11 | routes: [ 12 | { path: '/h5-to-miniprogram/aaa', component: AAA }, 13 | { path: '/h5-to-miniprogram/bbb', component: BBB } 14 | ] 15 | }) 16 | 17 | new Vue({ 18 | el: '#app', 19 | router, 20 | render: h => h(App) 21 | }) 22 | -------------------------------------------------------------------------------- /test/demo/demo08/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "webpack --progress --hide-modules" 4 | }, 5 | "devDependencies": { 6 | "babel-core": "^6.26.3", 7 | "babel-loader": "^7.1.5", 8 | "babel-plugin-transform-runtime": "^6.23.0", 9 | "babel-preset-env": "^1.7.0", 10 | "babel-preset-react": "^6.24.1", 11 | "babel-preset-stage-0": "^6.24.1", 12 | "webpack": "^4.20.2", 13 | "webpack-cli": "^3.1.2" 14 | }, 15 | "dependencies": { 16 | "react": "^16.5.2", 17 | "react-dom": "^16.5.2" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/demo/demo08/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | mode: 'development', 5 | entry: path.join(__dirname, './src/index.jsx'), 6 | output: { 7 | path: path.join(__dirname, './dist'), 8 | filename: 'build.js', 9 | }, 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.(js|jsx)$/, 14 | use: 'babel-loader', 15 | exclude: /node_modules/ 16 | } 17 | ] 18 | }, 19 | devtool: 'inline', 20 | } 21 | -------------------------------------------------------------------------------- /test/demo/demo11/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | mode: 'development', 5 | entry: path.join(__dirname, './src/index.jsx'), 6 | output: { 7 | path: path.join(__dirname, './dist'), 8 | filename: 'build.js', 9 | }, 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.(js|jsx)$/, 14 | use: 'babel-loader', 15 | exclude: /node_modules/ 16 | } 17 | ] 18 | }, 19 | devtool: 'inline', 20 | } 21 | -------------------------------------------------------------------------------- /test/demo/demo12/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | mode: 'development', 5 | entry: path.join(__dirname, './src/index.jsx'), 6 | output: { 7 | path: path.join(__dirname, './dist'), 8 | filename: 'build.js', 9 | }, 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.(js|jsx)$/, 14 | use: 'babel-loader', 15 | exclude: /node_modules/ 16 | } 17 | ] 18 | }, 19 | devtool: 'inline', 20 | } 21 | -------------------------------------------------------------------------------- /test/demo/demo02/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | News 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/template/adapter/bom/screen.js: -------------------------------------------------------------------------------- 1 | const load = require('../index') 2 | 3 | const EventTarget = load('EventTarget') 4 | 5 | class Screen extends EventTarget { 6 | constructor() { 7 | super() 8 | 9 | this._width = 0 10 | this._height = 0 11 | } 12 | 13 | _$init(info) { 14 | this._width = info.screenWidth 15 | this._height = info.screenHeight 16 | } 17 | 18 | /** 19 | * 对外属性和方法 20 | */ 21 | get width() { 22 | return this._width 23 | } 24 | 25 | get height() { 26 | return this._height 27 | } 28 | } 29 | 30 | module.exports = Screen 31 | -------------------------------------------------------------------------------- /test/demo/demo11/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "webpack --progress --hide-modules" 4 | }, 5 | "devDependencies": { 6 | "babel-core": "^6.26.3", 7 | "babel-loader": "^7.1.5", 8 | "babel-plugin-transform-runtime": "^6.23.0", 9 | "babel-preset-env": "^1.7.0", 10 | "babel-preset-react": "^6.24.1", 11 | "babel-preset-stage-0": "^6.24.1", 12 | "webpack": "^4.20.2", 13 | "webpack-cli": "^3.1.2" 14 | }, 15 | "dependencies": { 16 | "react": "^16.5.2", 17 | "react-dom": "^16.5.2", 18 | "react-router-dom": "^4.3.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/demo/demo12/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "webpack --progress --hide-modules" 4 | }, 5 | "devDependencies": { 6 | "babel-core": "^6.26.3", 7 | "babel-loader": "^7.1.5", 8 | "babel-plugin-transform-runtime": "^6.23.0", 9 | "babel-preset-env": "^1.7.0", 10 | "babel-preset-react": "^6.24.1", 11 | "babel-preset-stage-0": "^6.24.1", 12 | "webpack": "^4.20.2", 13 | "webpack-cli": "^3.1.2" 14 | }, 15 | "dependencies": { 16 | "react": "^16.5.2", 17 | "react-dom": "^16.5.2", 18 | "react-router-dom": "^4.3.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/template/adapter/node/comment.js: -------------------------------------------------------------------------------- 1 | const load = require('../index') 2 | 3 | const Node = load('Node') 4 | 5 | class Comment extends Node { 6 | constructor(options, tree) { 7 | options.type = 'comment' 8 | 9 | super(options, tree) 10 | } 11 | 12 | /** 13 | * 对应的 dom 信息 14 | */ 15 | get _$domInfo() { 16 | return { 17 | nodeId: this._nodeId, 18 | pageId: this._pageId, 19 | type: this._type, 20 | } 21 | } 22 | 23 | /** 24 | * 对外属性和方法 25 | */ 26 | get nodeType() { 27 | return Node.COMMENT_NODE 28 | } 29 | } 30 | 31 | module.exports = Comment 32 | -------------------------------------------------------------------------------- /test/demo/demo07/src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /test/demo/demo07/src/Content.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 22 | 23 | 32 | -------------------------------------------------------------------------------- /test/demo/demo09/dist/style.css: -------------------------------------------------------------------------------- 1 | 2 | .tabbar { 3 | display: flex; 4 | list-style: none; 5 | } 6 | .tabbar li { 7 | position: relative; 8 | display: block; 9 | height: 50px; 10 | width: 80px; 11 | text-align: center; 12 | line-height: 50px; 13 | background: #dff1e7; 14 | margin: 5px; 15 | } 16 | .tabbar li .link { 17 | display: block; 18 | width: 100%; 19 | height: 100%; 20 | } 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /test/demo/demo10/dist/style.css: -------------------------------------------------------------------------------- 1 | 2 | .tabbar { 3 | display: flex; 4 | list-style: none; 5 | } 6 | .tabbar li { 7 | position: relative; 8 | display: block; 9 | height: 50px; 10 | width: 80px; 11 | text-align: center; 12 | line-height: 50px; 13 | background: #dff1e7; 14 | margin: 5px; 15 | } 16 | .tabbar li .link { 17 | display: block; 18 | width: 100%; 19 | height: 100%; 20 | } 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /test/demo/demo04/js/rem.js: -------------------------------------------------------------------------------- 1 | (function(doc, win){ 2 | const docEl = doc.documentElement; 3 | const resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize'; 4 | const recale = function(){ 5 | const width = docEl.clientWidth; 6 | if (!width) return; 7 | if(width > 750){ 8 | docEl.style.fontSize = '100px'; 9 | }else{ 10 | docEl.style.fontSize = width / 6.4 + 'px'; 11 | } 12 | }; 13 | 14 | if(document.addEventListener){ 15 | win.addEventListener(resizeEvt, recale, false); 16 | doc.addEventListener('DOMContentLoaded', recale, false); 17 | } 18 | })(document, window); -------------------------------------------------------------------------------- /src/template/components/plain-text/plain-text.js: -------------------------------------------------------------------------------- 1 | const load = require('../../adapter/index') 2 | 3 | const cache = load('cache') 4 | 5 | Component({ 6 | properties: { 7 | // 文本内容 8 | content: { 9 | type: String, 10 | default: '', 11 | public: true, 12 | }, 13 | }, 14 | options: { 15 | addGlobalClass: true, // 开启全局样式 16 | }, 17 | attached() { 18 | const nodeId = this.dataset.privateNodeId 19 | const pageId = this.dataset.privatePageId 20 | 21 | this.nodeId = nodeId 22 | this.pageId = pageId 23 | 24 | // 存储 nodeId 到 component 实例的映射 25 | cache.setNodeComp(pageId, nodeId, this) 26 | 27 | // 记录 dom 28 | this.domNode = cache.getNode(pageId, nodeId) 29 | }, 30 | }) 31 | -------------------------------------------------------------------------------- /test/demo/demo06/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './js/index.js', 5 | 6 | output: { 7 | path: path.resolve(__dirname, 'dist'), 8 | filename: 'bundle.js', 9 | publicPath: '/dist' 10 | }, 11 | 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.js$/, 16 | exclude: /node_modules/, 17 | use: [ 18 | { 19 | loader: 'babel-loader', 20 | options: { 21 | presets: ['es2015'] 22 | } 23 | } 24 | ] 25 | } 26 | ] 27 | } 28 | } -------------------------------------------------------------------------------- /src/template/element.wxml.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{DATA}} 7 | 8 | -------------------------------------------------------------------------------- /test/demo/demo06/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vote", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1", 7 | "start": "webpack-dev-server -p", 8 | "build": "webpack -p" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "babel": "^6.23.0", 15 | "babel-core": "^6.26.0", 16 | "babel-loader": "^7.1.2", 17 | "babel-preset-es2015": "^6.24.1", 18 | "better-scroll": "^1.2.3", 19 | "es6-promise": "^4.1.1", 20 | "express": "^4.15.4", 21 | "jquery": "^3.2.1", 22 | "webpack": "^3.5.5", 23 | "webpack-dev-server": "^2.7.1" 24 | }, 25 | "devDependencies": {}, 26 | "description": "" 27 | } 28 | -------------------------------------------------------------------------------- /test/demo/demo08/src/app.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import Btn from './btn.jsx' 4 | import Content from './content.jsx' 5 | 6 | export default class App extends Component { 7 | constructor() { 8 | super(); 9 | 10 | this.state = { 11 | count: 1, 12 | text: 'add', 13 | }; 14 | this.add = this.add.bind(this); 15 | } 16 | 17 | add() { 18 | this.setState({ 19 | count: this.state.count + 1, 20 | }); 21 | } 22 | 23 | render() { 24 | return ( 25 |
26 | 27 | 28 |
29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/html/walker.test.js: -------------------------------------------------------------------------------- 1 | const mock = require('../mock') 2 | const walker = require('../../src/html/walker') 3 | const load = require('../../src/template/adapter') 4 | 5 | const parser = load('parser') 6 | 7 | test('walk html ast', () => { 8 | const parsed = parser.parse(mock.html) 9 | const res = walker.walk(parsed[0]) 10 | 11 | expect(res.title).toBe('test') 12 | expect(res.style).toEqual([ 13 | {src: './css/a.css', type: 'outer'}, 14 | {src: './css/b.css', type: 'outer'}, 15 | {content: '.a {\n color: green;\n }', type: 'inner'} 16 | ]) 17 | expect(res.script).toEqual([ 18 | {src: './js/a.js', type: 'outer'}, 19 | {src: './js/b.js', type: 'outer'}, 20 | {content: 'console.log(\'hello\');', type: 'inner'} 21 | ]) 22 | expect(res.body.tagName).toBe('body') 23 | }) 24 | -------------------------------------------------------------------------------- /test/demo/demo09/src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 17 | 18 | 39 | -------------------------------------------------------------------------------- /test/demo/demo10/src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 17 | 18 | 39 | -------------------------------------------------------------------------------- /test/demo/demo07/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot", 4 | "build": "cross-env NODE_ENV=production webpack --progress --hide-modules" 5 | }, 6 | "dependencies": { 7 | "vue": "^2.5.11" 8 | }, 9 | "browserslist": [ 10 | "> 1%", 11 | "last 2 versions", 12 | "not ie <= 8" 13 | ], 14 | "devDependencies": { 15 | "babel-core": "^6.26.0", 16 | "babel-loader": "^7.1.2", 17 | "babel-preset-env": "^1.6.0", 18 | "babel-preset-stage-3": "^6.24.1", 19 | "cross-env": "^5.0.5", 20 | "css-loader": "^0.28.7", 21 | "extract-text-webpack-plugin": "^3.0.2", 22 | "file-loader": "^1.1.4", 23 | "vue-loader": "^13.0.5", 24 | "vue-template-compiler": "^2.4.4", 25 | "webpack": "^3.6.0", 26 | "webpack-dev-server": "^2.9.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/demo/demo05/js/rem.js: -------------------------------------------------------------------------------- 1 | (function (doc, win) { 2 | var docEl = doc.documentElement, 3 | resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize', 4 | 5 | recalc = function () { 6 | var clientWidth = docEl.clientWidth; 7 | if (!clientWidth) return; 8 | if(clientWidth > 750){ //针对移动版的页面控制在750px内 9 | docEl.style.fontSize = '100px'; 10 | } 11 | if(clientWidth <750){ 12 | // docEl.style.fontSize = 20 * (clientWidth / 320) + 'px'; 13 | docEl.style.fontSize = clientWidth/ 6.4+ 'px'; 14 | 15 | } 16 | 17 | 18 | }; 19 | if (!doc.addEventListener) return; 20 | win.addEventListener(resizeEvt, recalc, false); 21 | doc.addEventListener('DOMContentLoaded', recalc, false); 22 | })(document, window); -------------------------------------------------------------------------------- /src/html/index.js: -------------------------------------------------------------------------------- 1 | const load = require('../template/adapter') 2 | const walker = require('./walker') 3 | const _ = require('../utils') 4 | 5 | const parser = load('parser') 6 | 7 | module.exports = { 8 | /** 9 | * 解析 html 10 | */ 11 | parse(content) { 12 | const ast = parser.parse(content) 13 | 14 | if (ast.length !== 1) throw new Error('invalid entry html') 15 | 16 | return walker.walk(ast[0]) 17 | }, 18 | 19 | /** 20 | * 生成页面 21 | */ 22 | async generate(options) { 23 | const output = options.output 24 | const body = options.body 25 | const bodyClass = (body.attrs.class || '') + ' h5-body' // 增加默认 class 26 | 27 | await _.writeFile(output, ``) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/demo/demo11/src/app.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { HashRouter as Router, Route, Link } from 'react-router-dom'; 3 | 4 | import AAA from './aaa.jsx' 5 | import BBB from './bbb.jsx' 6 | 7 | export default class App extends Component { 8 | render() { 9 | return ( 10 | 11 |
12 |
    13 |
  • aaa
  • 14 |
  • bbb
  • 15 |
16 | 17 | 18 |
19 |
20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/demo/demo12/src/app.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { BrowserRouter as Router, Route, Link } from 'react-router-dom'; 3 | 4 | import AAA from './aaa.jsx' 5 | import BBB from './bbb.jsx' 6 | 7 | export default class App extends Component { 8 | render() { 9 | return ( 10 | 11 |
12 |
    13 |
  • aaa
  • 14 |
  • bbb
  • 15 |
16 | 17 | 18 |
19 |
20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/demo/demo09/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot", 4 | "build": "cross-env NODE_ENV=production webpack --progress --hide-modules" 5 | }, 6 | "dependencies": { 7 | "vue": "^2.5.11", 8 | "vue-router": "^3.0.1" 9 | }, 10 | "browserslist": [ 11 | "> 1%", 12 | "last 2 versions", 13 | "not ie <= 8" 14 | ], 15 | "devDependencies": { 16 | "babel-core": "^6.26.0", 17 | "babel-loader": "^7.1.2", 18 | "babel-preset-env": "^1.6.0", 19 | "babel-preset-stage-3": "^6.24.1", 20 | "cross-env": "^5.0.5", 21 | "css-loader": "^0.28.7", 22 | "extract-text-webpack-plugin": "^3.0.2", 23 | "file-loader": "^1.1.4", 24 | "vue-loader": "^13.0.5", 25 | "vue-template-compiler": "^2.4.4", 26 | "webpack": "^3.6.0", 27 | "webpack-dev-server": "^2.9.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/demo/demo10/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot", 4 | "build": "cross-env NODE_ENV=production webpack --progress --hide-modules" 5 | }, 6 | "dependencies": { 7 | "vue": "^2.5.11", 8 | "vue-router": "^3.0.1" 9 | }, 10 | "browserslist": [ 11 | "> 1%", 12 | "last 2 versions", 13 | "not ie <= 8" 14 | ], 15 | "devDependencies": { 16 | "babel-core": "^6.26.0", 17 | "babel-loader": "^7.1.2", 18 | "babel-preset-env": "^1.6.0", 19 | "babel-preset-stage-3": "^6.24.1", 20 | "cross-env": "^5.0.5", 21 | "css-loader": "^0.28.7", 22 | "extract-text-webpack-plugin": "^3.0.2", 23 | "file-loader": "^1.1.4", 24 | "vue-loader": "^13.0.5", 25 | "vue-template-compiler": "^2.4.4", 26 | "webpack": "^3.6.0", 27 | "webpack-dev-server": "^2.9.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/demo/demo08/src/content.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class Content extends Component { 4 | constructor(props) { 5 | super(); 6 | 7 | const count = props.count || 0 8 | 9 | this.state = { 10 | isEven: count % 2 === 0, 11 | }; 12 | } 13 | 14 | componentWillReceiveProps(nextProps) { 15 | const count = nextProps.count || 0 16 | 17 | this.setState({ 18 | isEven: count % 2 === 0, 19 | }); 20 | } 21 | 22 | render() { 23 | return ( 24 |
25 | this is a vue demo 26 | { 27 | this.state.isEven ? (even({this.props.count})) : (odd({this.props.count})) 28 | } 29 |
30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/demo/demo02/css/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | padding: 0; 3 | margin: 0; 4 | } 5 | .container{ 6 | width: 100%; 7 | overflow: hidden; 8 | position: relative; 9 | } 10 | 11 | table{ 12 | width: 100%; 13 | border-collapse: collapse; 14 | } 15 | 16 | table tr:first-child{ 17 | width: 100%; 18 | height: 150px; 19 | border-bottom: 1px solid #f3f5f7; 20 | } 21 | 22 | table tr:first-child td:last-child{ 23 | vertical-align: top; 24 | padding-top: 10px; 25 | } 26 | 27 | table tr:first-child td:last-child p{ 28 | height: 20px; 29 | line-height: 20px; 30 | } 31 | 32 | tr{ 33 | width: 100%; 34 | border-bottom: 1px solid #f3f5f7; 35 | height: 40px; 36 | font-size: 12px; 37 | /* border-collapse: separate; */ 38 | } 39 | 40 | td{ 41 | padding-left: 10px; 42 | } 43 | 44 | tr td:last-child{ 45 | width: 60%; 46 | font-weight: 700; 47 | } 48 | 49 | .head-img { 50 | display:block; 51 | margin: 0 auto; 52 | width: 112px; 53 | height: 123px; 54 | } 55 | -------------------------------------------------------------------------------- /test/demo/demo09/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | vue 10 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /test/demo/demo10/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | vue 10 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/js/ignore-global-var.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | 'define', 'module', 'exports', 'require', 'global', 'window', 'document', 3 | // js 全局变量 4 | 'Array', 'ArrayBuffer', 'Boolean', 'DataView', 'Date', 'Error', 'EvalError', 'Float32Array', 'Float64Array', 'Function', 'Infinity', 'Int16Array', 'Int32Array', 'Int8Array', 'Intl', 'Intl.Collator', 'Intl.DateTimeFormat', 'Intl.NumberFormat', 'JSON', 'Map', 'Math', 'NaN', 'Number', 'Object', 'Promise', 'Proxy', 'RangeError', 'ReferenceError', 'Reflect', 'RegExp', 'Set', 'String', 'Symbol', 'SyntaxError', 'TypeError', 'URIError', 'Uint16Array', 'Uint32Array', 'Uint8Array', 'Uint8ClampedArray', 'WeakMap', 'WeakSet', 'WebAssembly', 'decodeURI', 'decodeURIComponent', 'encodeURI', 'encodeURIComponent', 'escape', 'eval', 'isFinite', 'isNaN', 'null', 'parseFloat', 'parseInt', 'undefined', 'unescape', 'console', 'requestAnimationFrame', 'cancelAnimationFrame', 5 | // 直接在 window 里实现 6 | 'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval', 7 | // 小程序特有全局量 8 | 'App', 'getApp', 'Page', 'getCurrentPages', 'wx', 'requirePlugin', 'definePlugin' 9 | ] 10 | -------------------------------------------------------------------------------- /test/demo/server.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa') 2 | const Router = require('koa-router') 3 | const bodyParser = require('koa-bodyparser') 4 | 5 | const app = new Koa() 6 | const router = new Router() 7 | 8 | app.use(bodyParser()) 9 | 10 | router.get('/demo2/test', ctx => { 11 | ctx.cookies.set('I_AM_SERVER1', 'THIS_IS_THE_VALUE', { 12 | maxAge: 10 * 60 * 1000, 13 | httpOnly: false, 14 | path: '/', 15 | }) 16 | ctx.cookies.set('I_AM_SERVER2', 'THIS_IS_THE_ANOTHER_VALUE', { 17 | maxAge: 10 * 60 * 1000, 18 | httpOnly: false, 19 | path: '/', 20 | }) 21 | ctx.body = {} 22 | }) 23 | 24 | router.post('/action', ctx => { 25 | const needDo = ctx.request.body.needDo 26 | 27 | if (needDo === 'showRequestCookie') { 28 | const header = ctx.request.header 29 | ctx.body = { 30 | cookie: header.cookie, 31 | } 32 | } else { 33 | ctx.body = {} 34 | } 35 | }) 36 | 37 | app.use(router.routes()) 38 | app.use(router.allowedMethods()) 39 | app.listen(9420) 40 | 41 | console.log('server listen 9420') 42 | -------------------------------------------------------------------------------- /test/js/adjust.test.js: -------------------------------------------------------------------------------- 1 | const adjust = require('../../src/js/adjust') 2 | 3 | test('adjust js content: replace global var', async () => { 4 | const res = await adjust('a = function(b, c) {d(); setTimeout(() => {}, 0); e = new Date();}') 5 | expect(res).toBe('window[\'a\'] = function (b, c) {\n window[\'d\']();\n setTimeout(() => {}, 0);\n window[\'e\'] = new Date();\n};') 6 | }) 7 | 8 | test('adjust js content: global function declaration', async () => { 9 | const res = await adjust('function aa(a, b) {return a;}') 10 | expect(res).toBe('function aa(a, b) {\n return a;\n}\n\nwindow[\'aa\'] = aa;') 11 | }) 12 | 13 | test('adjust js content: global function declaration expression', async () => { 14 | const res = await adjust('var bb = function aa(a, b) {return a;}') 15 | expect(res).toBe('var bb = function aa(a, b) {\n return a;\n};\n\nwindow[\'aa\'] = aa;') 16 | }) 17 | 18 | test('adjust js content: remove comment', async () => { 19 | const res = await adjust('var a = /* a */ 12; // a\n/* a\na\na\n */\n// a\nvar b = 21;') 20 | expect(res).toBe('var a = 12;\nvar b = 21;') 21 | }) 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 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 | -------------------------------------------------------------------------------- /test/demo/demo07/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | vue 10 | 30 | 31 | 32 | 33 |
34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/template/adapter/node/element/a.js: -------------------------------------------------------------------------------- 1 | const load = require('../../index') 2 | 3 | const Element = load('Element') 4 | 5 | class HTMLAnchorElement extends Element { 6 | /** 7 | * 调用 _generateHtml 接口时用于处理额外的属性, 8 | */ 9 | _$dealWithAttrsForGenerateHtml(html, node) { 10 | const href = node.href 11 | if (href) html += ` href="${href}"` 12 | 13 | const target = node.target 14 | if (target) html += ` target="${target}"` 15 | 16 | return html 17 | } 18 | 19 | /** 20 | * 调用 outerHTML 的 setter 时用于处理额外的属性 21 | */ 22 | _$dealWithAttrsForOuterHTML(node) { 23 | this.href = node.href || '' 24 | this.target = node.target || '' 25 | } 26 | 27 | /** 28 | * 调用 cloneNode 接口时用于处理额外的属性 29 | */ 30 | _$dealWithAttrsForCloneNode() { 31 | return { 32 | href: this.href, 33 | target: this.target, 34 | } 35 | } 36 | 37 | /** 38 | * 对外属性和方法 39 | */ 40 | get href() { 41 | return this._attrs.get('href') 42 | } 43 | 44 | set href(value) { 45 | value = '' + value 46 | this._attrs.set('href', value) 47 | } 48 | 49 | get target() { 50 | return this._attrs.get('target') 51 | } 52 | 53 | set target(value) { 54 | value = '' + value 55 | this._attrs.set('target', value) 56 | } 57 | } 58 | 59 | module.exports = HTMLAnchorElement 60 | -------------------------------------------------------------------------------- /test/demo/demo08/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | react 10 | 37 | 38 | 39 |
40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /docs/question.md: -------------------------------------------------------------------------------- 1 | # api 2 | 3 | ## 可实现,但是会带来副作用 4 | 5 | ### dom 6 | 7 | * 各个元素的滚动事件,使用 scroll-view 会改变元素的嵌套,会影响结构。同时安卓端的 scroll 事件有引起阻塞的风险。 8 | * scrollTop、scrollLeft、scrollHeight、scrollWidth 等的设置和获取。 9 | * drop 和 drag。 10 | * wxss 和 background 无法使用本地图片,必须是网络图片。 11 | 12 | ## 无法完全按照标准实现 13 | 14 | ### dom 15 | 16 | * input 标签的部分功能,比如 type=file 等,无法完美实现。 17 | * computedStyle、boundingClientRect、clientWidth、clientHeight 等无法获取即时的结果,只能异步获取。(理论上可以在 appService 端实现 renderTree 和 layoutTree,但是开销太大) 18 | * location 中的页面跳转功能。 19 | * XMLHttpRequest。 20 | * WebSocket。 21 | * Worker。 22 | 23 | ### 样式 24 | 25 | * 标签嵌套规则可能会变,导致样式会有差异,如 video 组件等。 26 | 27 | ### 其他 28 | 29 | * canvas、video 等组件未提供的接口无法实现。 30 | * img 标签和小程序 image 组件实现不同,image 组件是使用 backgroundImage 来实现的,在一些样式表现上会有差异。 31 | * 原生组件层级问题,cover-view 和 cover-image 无法完整模拟 dom 节点。 32 | * iframe 可用 web-view 替代,但是 web-view 不允许标准通信,也不允许自定义大小。 33 | 34 | ## 目前无法实现 35 | 36 | ### dom 37 | 38 | * svg。 39 | * webgl。 40 | * range。 41 | * 事件系统的 preventDefault 接口。 42 | * 事件系统的 beforeunload、scroll 等事件。 43 | * 还有无法手动模拟触发 dom 上的事件。 44 | 45 | ### 样式 46 | 47 | * wxss 不支持属性选择器。 48 | * 因为不支持动态修改 html 的 font-size,wxss 支持 rem 有限制,html 的 font-size 默认是 width / 20。 49 | 50 | ### 其他 51 | 52 | * 动态追加脚本、wxss、JSONP,webpack 中的动态添加 css、js 功能无法使用。 53 | * cookie 操作,请求携带 cookie。 54 | * FileReader。 55 | * 桌面提醒。 56 | 57 | ## 第三方库 58 | 59 | ### jQuery 60 | 61 | ### vue 62 | 63 | ### react 64 | -------------------------------------------------------------------------------- /src/template/adapter/util/tool.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 驼峰转连字符 3 | */ 4 | function toDash(str) { 5 | return str.replace(/[A-Z]/g, all => `-${all.toLowerCase()}`) 6 | } 7 | 8 | /** 9 | * 连字符转驼峰 10 | */ 11 | function toCamel(str) { 12 | return str.replace(/-([a-zA-Z])/g, (all, $1) => $1.toUpperCase()) 13 | } 14 | 15 | /** 16 | * 获取唯一 id 17 | */ 18 | let seed = +new Date() 19 | function getId() { 20 | return seed++ 21 | } 22 | 23 | /** 24 | * 替换样式中的路径 25 | */ 26 | function replaceStyleUrl(value, filter, pageKey) { 27 | return value.replace(/\burl\((?:["']?)([^"']*)(?:["']?)\)/ig, (all, $1) => `url("${filter($1, pageKey)}")`) 28 | } 29 | 30 | /** 31 | * 替换样式中的 rem 32 | */ 33 | const realRem = 375 / 20 34 | function replaceStyleRem(value, rem) { 35 | return value.replace(/((?:\d*.\d+)|(?:\d+))rem/ig, (all, $1) => { 36 | if ($1[0] === '.') $1 = '0' + $1 37 | 38 | return (parseFloat($1, 10) * rem / realRem).toFixed(3) + 'rem' 39 | }) 40 | } 41 | 42 | /** 43 | * 节流,一个同步流中只调用一次该函数 44 | */ 45 | const waitFuncSet = new WeakSet() 46 | function throttle(func, timeout = 0) { 47 | return () => { 48 | if (waitFuncSet.has(func)) return 49 | 50 | waitFuncSet.add(func) 51 | 52 | setTimeout(() => { 53 | waitFuncSet.delete(func) 54 | func() 55 | }, timeout) 56 | } 57 | } 58 | 59 | module.exports = { 60 | toDash, 61 | toCamel, 62 | getId, 63 | replaceStyleUrl, 64 | replaceStyleRem, 65 | throttle, 66 | } 67 | -------------------------------------------------------------------------------- /test/demo/demo11/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | react 10 | 39 | 40 | 41 |
42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /test/demo/demo12/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | react 10 | 39 | 40 | 41 |
42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /test/demo/demo06/js/rem.js: -------------------------------------------------------------------------------- 1 | // (function(win, doc){ 2 | // const docEl = doc.documentElement; 3 | // const resEvt = 'orientationchange' in window ? 'orientationchange' : 'resize'; 4 | // const reclac = function(){ 5 | // if(!docEl.clientWidth) return; 6 | // const width = docEl.clientWidth; 7 | 8 | // if(width > 750) docEl.style.fontSize = '100px'; 9 | // else docEl.style.fontSize = width / 6.4 + 'px'; 10 | // } 11 | 12 | // if(doc.addEventListener){ 13 | // win.addEventListener(resEvt, reclac, false); 14 | // doc.addEventListener('DOMCententLoaded', reclac, false); 15 | // } 16 | // })(window, document); 17 | 18 | (function (doc, win) { 19 | var docEl = doc.documentElement, 20 | resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize', 21 | 22 | recalc = function () { 23 | var clientWidth = docEl.clientWidth; 24 | if (!clientWidth) return; 25 | if(clientWidth > 750){ //针对移动版的页面控制在750px内 26 | docEl.style.fontSize = '100px'; 27 | } 28 | if(clientWidth <750){ 29 | // docEl.style.fontSize = 20 * (clientWidth / 320) + 'px'; 30 | docEl.style.fontSize = clientWidth/ 6.4+ 'px'; 31 | 32 | } 33 | 34 | 35 | }; 36 | if (!doc.addEventListener) return; 37 | win.addEventListener(resizeEvt, recalc, false); 38 | doc.addEventListener('DOMContentLoaded', recalc, false); 39 | })(document, window); -------------------------------------------------------------------------------- /src/template/adapter/node/text-node.js: -------------------------------------------------------------------------------- 1 | const load = require('../index') 2 | 3 | const Node = load('Node') 4 | const tool = load('tool') 5 | 6 | class TextNode extends Node { 7 | constructor(options, tree) { 8 | options.type = 'text' 9 | 10 | super(options, tree) 11 | 12 | this._content = options.content || '' 13 | } 14 | 15 | /** 16 | * 更新父组件树 17 | */ 18 | _triggerParentUpdate() { 19 | if (this.parentNode) this.parentNode._$trigger('_childNodesUpdate') 20 | } 21 | 22 | /** 23 | * 对应的 dom 信息 24 | */ 25 | get _$domInfo() { 26 | return { 27 | nodeId: this._nodeId, 28 | pageId: this._pageId, 29 | type: this._type, 30 | content: this._content, 31 | } 32 | } 33 | 34 | /** 35 | * 对外属性和方法 36 | */ 37 | get nodeType() { 38 | return Node.TEXT_NODE 39 | } 40 | 41 | get textContent() { 42 | return this._content 43 | } 44 | 45 | set textContent(value) { 46 | value += '' 47 | 48 | this._content = value 49 | this._triggerParentUpdate() 50 | } 51 | 52 | get nodeValue() { 53 | return this.textContent 54 | } 55 | 56 | set nodeValue(value) { 57 | this.textContent = value 58 | } 59 | 60 | get nodeName() { 61 | return '#text' 62 | } 63 | 64 | cloneNode() { 65 | return this.ownerDocument._$createTextNode({ 66 | content: this._content, 67 | nodeId: `b-${tool.getId()}`, // 运行时生成,使用 b- 前缀 68 | }) 69 | } 70 | } 71 | 72 | module.exports = TextNode 73 | -------------------------------------------------------------------------------- /test/demo/demo03/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 | 12 |
13 | 14 |
15 | 29 |
30 |
31 |
32 |
33 | 34 | 抱歉,来晚一步,这篇已经不存在了 35 |
36 |
37 | 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "h5-to-miniprogram", 3 | "version": "0.0.8", 4 | "description": "a simple tool for transforming h5 to miniprogam", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint \"src/**/*.js\" --fix", 8 | "lint-test": "eslint \"test/**/*.test.js\" --fix", 9 | "test": "jest \"./test/(.*)/(.*).test.js\" --silent --bail" 10 | }, 11 | "keywords": [ 12 | "miniprogram", 13 | "h5", 14 | "transform", 15 | "weapp", 16 | "mp", 17 | "html", 18 | "html5" 19 | ], 20 | "jest": { 21 | "testEnvironment": "node" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/wechat-miniprogram/h5-to-miniprogram.git" 26 | }, 27 | "author": "wechat-miniprogram", 28 | "license": "MIT", 29 | "devDependencies": { 30 | "eslint": "^5.3.0", 31 | "eslint-config-airbnb-base": "13.1.0", 32 | "eslint-plugin-import": "^2.14.0", 33 | "eslint-plugin-node": "^7.0.1", 34 | "eslint-plugin-promise": "^3.8.0", 35 | "jest": "^23.5.0", 36 | "jsdom": "^12.0.0", 37 | "koa": "^2.6.2", 38 | "koa-bodyparser": "^4.2.1", 39 | "koa-router": "^7.4.0", 40 | "rimraf": "^2.6.2" 41 | }, 42 | "dependencies": { 43 | "@babel/core": "^7.1.2", 44 | "acorn": "^6.0.2", 45 | "acorn-globals": "^4.3.0", 46 | "babel-plugin-minify-mangle-names": "^0.5.0", 47 | "cssnano": "^4.1.7", 48 | "magic-string": "^0.25.1", 49 | "postcss": "^7.0.2", 50 | "request": "^2.88.0", 51 | "request-promise-native": "^1.0.5" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test/demo/demo06/mock/vote.js: -------------------------------------------------------------------------------- 1 | const Express = require('express'); 2 | const app = new Express(); 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | 6 | app.get('/:id', function(req, res){ 7 | const page = req.params.id; 8 | res.setHeader("Access-Control-Allow-Origin", "*"); 9 | // console.log(page); 10 | fs.readFile(path.resolve(__dirname, 'mock.json'), function(err, data){ 11 | if(err){ 12 | throw err; 13 | } 14 | const static = JSON.parse(data).data; 15 | const arr = Array.from(static); 16 | const len = arr.length; 17 | const next = page - 0 + 1 <= len ? true : false; 18 | for(let value of arr){ 19 | if(value.page == page){ 20 | res.json({data: value.data, next: next}); 21 | } 22 | } 23 | }); 24 | }); 25 | 26 | app.get('/update/:id', function(req, res){ 27 | const page = req.params.id; 28 | res.setHeader('Access-Control-Allow-Origin', '*'); 29 | fs.readFile(path.resolve(__dirname, 'update.json'), function(err, data){ 30 | if(err){ 31 | throw err; 32 | } 33 | const static = JSON.parse(data).data; 34 | const arr = Array.from(static); 35 | const next = page - 0 + 1 <= arr.length ? true : false; 36 | let result = []; 37 | for(let value of arr){ 38 | if(value.page <= page){ 39 | result = result.concat(value.data); 40 | } 41 | } 42 | res.json({data: result, next: next}); 43 | }); 44 | }); 45 | 46 | app.listen(3001); -------------------------------------------------------------------------------- /test/adapter/node/style.test.js: -------------------------------------------------------------------------------- 1 | const mock = require('../../mock') 2 | const load = require('../../../src/template/adapter') 3 | const html = require('../../../src/html') 4 | 5 | const Document = load('Document') 6 | const tool = load('tool') 7 | const cache = load('cache') 8 | const Style = load('Style') 9 | 10 | const nodeIdMap = {} 11 | const pageId = `p-${tool.getId()}` 12 | let document 13 | 14 | beforeAll(async () => { 15 | const pageInfo = await html.parse(mock.html) 16 | 17 | document = new Document(pageId, 'index', pageInfo.body, nodeIdMap) 18 | cache.init(pageId, { 19 | document, 20 | nodeIdMap, 21 | }) 22 | }) 23 | 24 | test('style', () => { 25 | let updateCount = 0 26 | const element = document.createElement('div') 27 | const style = new Style(element, 'position: absolute; top: 0; left: 0;', () => { 28 | updateCount++ 29 | }) 30 | 31 | expect(style.position).toBe('absolute') 32 | expect(style.top).toBe('0') 33 | expect(style.top).toBe('0') 34 | expect(style.width).toBe('') 35 | 36 | style.width = '100%' 37 | expect(style.width).toBe('100%') 38 | style.height = '13px' 39 | expect(style.height).toBe('13px') 40 | expect(updateCount).toBe(2) 41 | 42 | expect(style.cssText).toBe('position:absolute;top:0;left:0;width:100%;height:13px;') 43 | 44 | style.cssText = 'position: relative; display: block;' 45 | expect(style.position).toBe('relative') 46 | expect(style.top).toBe('') 47 | expect(style.left).toBe('') 48 | expect(style.display).toBe('block') 49 | expect(updateCount).toBe(3) 50 | 51 | expect(style.cssText).toBe('position:relative;display:block;') 52 | }) 53 | -------------------------------------------------------------------------------- /test/demo/test.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const rimraf = require('rimraf') 3 | 4 | const toMiniprogram = require('../../index') 5 | 6 | rimraf.sync(path.join(__dirname, './output/common')) 7 | toMiniprogram({ 8 | // 入口 9 | entry: { 10 | demo01: path.join(__dirname, './demo01/index.html'), 11 | demo02: path.join(__dirname, './demo02/index.html'), // jquery + 表单 + 异步请求 12 | demo03: path.join(__dirname, './demo03/index.html'), 13 | // demo04: path.join(__dirname, './demo04/index.html'), // jquery 14 | // demo05: path.join(__dirname, './demo05/index.html'), // canvas 15 | // demo06: path.join(__dirname, './demo06/index.html'), 16 | // demo07: path.join(__dirname, './demo07/index.html'), // webpack + vue 17 | // demo08: path.join(__dirname, './demo08/index.html'), // webpack + react 18 | demo09: path.join(__dirname, './demo09/index.html'), // webpack + vue + vue-router(hash) 19 | demo10: path.join(__dirname, './demo10/index.html'), // webpack + vue + vue-router(history) 20 | demo11: path.join(__dirname, './demo11/index.html'), // webpack + react + react-router(hash) 21 | demo12: path.join(__dirname, './demo12/index.html'), // webpack + react + react-router(history) 22 | }, 23 | // 输出目录 24 | output: path.join(__dirname, './output'), 25 | // 相关配置文件 26 | config: path.join(__dirname, './config.js'), 27 | // 扩展实现文件 28 | extend: [path.join(__dirname, './extend1.js'), path.join(__dirname, './extend2.js')], 29 | // 压缩配置 30 | compress: { 31 | jsInH5: true, // 原 h5 页面中的 js 32 | cssInH5: true, // 原 h5 页面中的 css 33 | }, 34 | }).then(res => { 35 | console.log('done') 36 | }).catch(err => { 37 | console.error(err) 38 | }) 39 | -------------------------------------------------------------------------------- /test/demo/demo07/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 4 | 5 | module.exports = { 6 | // mode: 'development', 7 | entry: './src/main.js', 8 | output: { 9 | path: path.resolve(__dirname, './dist'), 10 | publicPath: '/dist/', 11 | filename: 'build.js' 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.css$/, 17 | use: [ 18 | 'vue-style-loader', 19 | 'css-loader' 20 | ], 21 | }, { 22 | test: /\.vue$/, 23 | loader: 'vue-loader', 24 | options: { 25 | loaders: { 26 | css: ExtractTextPlugin.extract({ 27 | use: 'css-loader', 28 | fallback: 'vue-style-loader' // <- 这是vue-loader的依赖,所以如果使用npm3,则不需要显式安装 29 | }) 30 | } 31 | // other vue-loader options go here 32 | } 33 | }, 34 | { 35 | test: /\.js$/, 36 | loader: 'babel-loader', 37 | exclude: /node_modules/ 38 | }, 39 | { 40 | test: /\.(png|jpg|gif|svg)$/, 41 | loader: 'file-loader', 42 | options: { 43 | name: '[name].[ext]?[hash]' 44 | } 45 | } 46 | ] 47 | }, 48 | resolve: { 49 | alias: { 50 | 'vue$': 'vue/dist/vue.esm.js' 51 | }, 52 | extensions: ['*', '.js', '.vue', '.json'] 53 | }, 54 | devServer: { 55 | historyApiFallback: true, 56 | noInfo: true, 57 | overlay: true 58 | }, 59 | performance: { 60 | hints: false 61 | }, 62 | plugins: [ 63 | new ExtractTextPlugin('style.css') 64 | ], 65 | } 66 | -------------------------------------------------------------------------------- /test/demo/demo09/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 4 | 5 | module.exports = { 6 | // mode: 'development', 7 | entry: './src/main.js', 8 | output: { 9 | path: path.resolve(__dirname, './dist'), 10 | publicPath: '/dist/', 11 | filename: 'build.js' 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.css$/, 17 | use: [ 18 | 'vue-style-loader', 19 | 'css-loader' 20 | ], 21 | }, { 22 | test: /\.vue$/, 23 | loader: 'vue-loader', 24 | options: { 25 | loaders: { 26 | css: ExtractTextPlugin.extract({ 27 | use: 'css-loader', 28 | fallback: 'vue-style-loader' // <- 这是vue-loader的依赖,所以如果使用npm3,则不需要显式安装 29 | }) 30 | } 31 | // other vue-loader options go here 32 | } 33 | }, 34 | { 35 | test: /\.js$/, 36 | loader: 'babel-loader', 37 | exclude: /node_modules/ 38 | }, 39 | { 40 | test: /\.(png|jpg|gif|svg)$/, 41 | loader: 'file-loader', 42 | options: { 43 | name: '[name].[ext]?[hash]' 44 | } 45 | } 46 | ] 47 | }, 48 | resolve: { 49 | alias: { 50 | 'vue$': 'vue/dist/vue.esm.js' 51 | }, 52 | extensions: ['*', '.js', '.vue', '.json'] 53 | }, 54 | devServer: { 55 | historyApiFallback: true, 56 | noInfo: true, 57 | overlay: true 58 | }, 59 | performance: { 60 | hints: false 61 | }, 62 | plugins: [ 63 | new ExtractTextPlugin('style.css') 64 | ], 65 | } 66 | -------------------------------------------------------------------------------- /test/demo/demo10/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 4 | 5 | module.exports = { 6 | // mode: 'development', 7 | entry: './src/main.js', 8 | output: { 9 | path: path.resolve(__dirname, './dist'), 10 | publicPath: '/dist/', 11 | filename: 'build.js' 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.css$/, 17 | use: [ 18 | 'vue-style-loader', 19 | 'css-loader' 20 | ], 21 | }, { 22 | test: /\.vue$/, 23 | loader: 'vue-loader', 24 | options: { 25 | loaders: { 26 | css: ExtractTextPlugin.extract({ 27 | use: 'css-loader', 28 | fallback: 'vue-style-loader' // <- 这是vue-loader的依赖,所以如果使用npm3,则不需要显式安装 29 | }) 30 | } 31 | // other vue-loader options go here 32 | } 33 | }, 34 | { 35 | test: /\.js$/, 36 | loader: 'babel-loader', 37 | exclude: /node_modules/ 38 | }, 39 | { 40 | test: /\.(png|jpg|gif|svg)$/, 41 | loader: 'file-loader', 42 | options: { 43 | name: '[name].[ext]?[hash]' 44 | } 45 | } 46 | ] 47 | }, 48 | resolve: { 49 | alias: { 50 | 'vue$': 'vue/dist/vue.esm.js' 51 | }, 52 | extensions: ['*', '.js', '.vue', '.json'] 53 | }, 54 | devServer: { 55 | historyApiFallback: true, 56 | noInfo: true, 57 | overlay: true 58 | }, 59 | performance: { 60 | hints: false 61 | }, 62 | plugins: [ 63 | new ExtractTextPlugin('style.css') 64 | ], 65 | } 66 | -------------------------------------------------------------------------------- /test/demo/demo04/js/swiper.animate1.0.2.min.js: -------------------------------------------------------------------------------- 1 | //本插件由www.swiper.com.cn提供 2 | function swiperAnimateCache(){for(allBoxes=window.document.documentElement.querySelectorAll(".ani"),i=0;i ', count); 17 | 18 | function showLoading() { 19 | loading.style.display = 'block'; 20 | setTimeout(function() { 21 | showPage1(); 22 | hideLoading(); 23 | }, 1000); 24 | } 25 | 26 | function hideLoading() { 27 | loading.style.display = 'none'; 28 | } 29 | 30 | function showPage1() { 31 | pages[0].style.display = 'block'; 32 | pages[1].style.display = 'none'; 33 | selectTapLine.classList.remove('pull-right'); 34 | hideLoading(); 35 | } 36 | 37 | function showPage2() { 38 | pages[0].style.display = 'none'; 39 | pages[1].style.display = 'block'; 40 | selectTapLine.classList.add('pull-right'); 41 | hideLoading(); 42 | } 43 | 44 | tabs[0].addEventListener('click', function(evt) { 45 | showPage1(); 46 | }); 47 | 48 | tabs[1].addEventListener('click', function(evt) { 49 | showPage2(); 50 | }); 51 | 52 | showLoading(); 53 | 54 | if (window.I_am_extend_function) { 55 | console.log('window extend function: ', window.I_am_extend_function, window.I_am_extend_function()); 56 | } 57 | 58 | if (window.I_am_another_extend_function) { 59 | console.log('window another extend function: ', window.I_am_another_extend_function, window.I_am_another_extend_function()); 60 | } 61 | -------------------------------------------------------------------------------- /src/template/adapter/node/node.js: -------------------------------------------------------------------------------- 1 | const load = require('../index') 2 | 3 | const EventTarget = load('EventTarget') 4 | const cache = load('cache') 5 | 6 | class Node extends EventTarget { 7 | constructor(options, tree) { 8 | super() 9 | 10 | this._nodeId = options.nodeId // 唯一标识 11 | this._type = options.type 12 | this._parentNode = null 13 | this._tree = tree 14 | this._pageId = tree.pageId 15 | } 16 | 17 | /** 18 | * 内部 nodeId 19 | */ 20 | get _$nodeId() { 21 | return this._nodeId 22 | } 23 | 24 | /** 25 | * 内部 pageId 26 | */ 27 | get _$pageId() { 28 | return this._pageId 29 | } 30 | 31 | /** 32 | * 更新 parentNode 33 | */ 34 | _$updateParent(parentNode = null) { 35 | this._parentNode = parentNode 36 | } 37 | 38 | /** 39 | * 对外属性和方法 40 | */ 41 | get parentNode() { 42 | return this._parentNode 43 | } 44 | 45 | get nodeValue() { 46 | return null 47 | } 48 | 49 | get previousSibling() { 50 | const childNodes = this.parentNode && this.parentNode.childNodes || [] 51 | const index = childNodes.indexOf(this) 52 | 53 | if (index > 0) { 54 | return childNodes[index - 1] 55 | } 56 | 57 | return null 58 | } 59 | 60 | get previousElementSibling() { 61 | const childNodes = this.parentNode && this.parentNode.childNodes || [] 62 | const index = childNodes.indexOf(this) 63 | 64 | if (index > 0) { 65 | for (let i = index - 1; i >= 0; i--) { 66 | if (childNodes[i].nodeType === Node.ELEMENT_NODE) { 67 | return childNodes[i] 68 | } 69 | } 70 | } 71 | 72 | return null 73 | } 74 | 75 | get ownerDocument() { 76 | return cache.getDocument(this._pageId) || null 77 | } 78 | } 79 | 80 | // 静态属性 81 | Node.ELEMENT_NODE = 1 82 | Node.TEXT_NODE = 3 83 | Node.CDATA_SECTION_NODE = 4 84 | Node.PROCESSING_INSTRUCTION_NODE = 7 85 | Node.COMMENT_NODE = 8 86 | Node.DOCUMENT_NODE = 9 87 | Node.DOCUMENT_TYPE_NODE = 10 88 | Node.DOCUMENT_FRAGMENT_NODE = 11 89 | 90 | module.exports = Node 91 | -------------------------------------------------------------------------------- /src/template/adapter/bom/navigator.js: -------------------------------------------------------------------------------- 1 | class Navigator { 2 | constructor() { 3 | this._language = '' 4 | this._wxVersion = '' 5 | this._brand = '' // 手机品牌 6 | this._model = '' // 手机型号 7 | this._platform = '' 8 | this._system = '' // 操作系统版本 9 | 10 | this._userAgent = '' 11 | } 12 | 13 | _$init(info) { 14 | this._language = info.language 15 | this._wxVersion = info.version 16 | this._brand = info.brand 17 | this._model = info.model 18 | this._platform = info.platform 19 | this._system = info.system 20 | 21 | // 拼装 userAgent 22 | const appVersion = '5.0' 23 | const appleWebKitVersion = '602.3.12' 24 | let platformContext 25 | if (this._platform === 'ios') { 26 | // 拆分系统版本号 27 | let systemVersion = this._system.split(' ') 28 | if (systemVersion.length >= 2) { 29 | systemVersion = systemVersion[1].split('.').join('_') 30 | } else { 31 | systemVersion = '' 32 | } 33 | 34 | platformContext = `${this._brand}; CPU ${this._brand} OS ${systemVersion} like Mac OS X` 35 | } else if (this._platform === 'android') { 36 | platformContext = `Linux; ${this._system}; ${this._model}` 37 | } else { 38 | // 在开发者工具上,默认在 windows x64 上运行 39 | platformContext = 'Windows NT 6.1; win64; x64' 40 | } 41 | 42 | this._userAgent = `${this.appCodeName}/${appVersion} (${platformContext}) AppleWebKit/${appleWebKitVersion} (KHTML, like Gecko) Mobile MicroMessenger/${this._wxVersion} Language/${this.language}` 43 | } 44 | 45 | /** 46 | * 对外属性和方法 47 | */ 48 | get userAgent() { 49 | return this._userAgent 50 | } 51 | 52 | get appCodeName() { 53 | return 'Mozilla' 54 | } 55 | 56 | get appName() { 57 | return 'Netscape' 58 | } 59 | 60 | get language() { 61 | return this._language 62 | } 63 | 64 | get languages() { 65 | return [this._language] 66 | } 67 | 68 | get platform() { 69 | return this._platform 70 | } 71 | 72 | get product() { 73 | return 'Gecko' 74 | } 75 | } 76 | 77 | module.exports = Navigator 78 | -------------------------------------------------------------------------------- /test/adapter/node/class-list.test.js: -------------------------------------------------------------------------------- 1 | const mock = require('../../mock') 2 | const load = require('../../../src/template/adapter') 3 | 4 | const ClassList = load('ClassList') 5 | 6 | test('class-list', () => { 7 | let updateCount = 0 8 | const classList = new ClassList('a b c', () => { 9 | updateCount++ 10 | }) 11 | 12 | // item 13 | expect(classList.item(0)).toBe('a') 14 | expect(classList.item(1)).toBe('b') 15 | expect(classList.item(2)).toBe('c') 16 | expect(classList.item(3)).toBe(undefined) 17 | 18 | // contains 19 | expect(classList.contains('a')).toBe(true) 20 | expect(classList.contains('b')).toBe(true) 21 | expect(classList.contains('c')).toBe(true) 22 | expect(classList.contains('d')).toBe(false) 23 | expect(updateCount).toBe(0) 24 | 25 | // add 26 | classList.add('d') 27 | expect(classList.item(3)).toBe('d') 28 | expect(classList.contains('d')).toBe(true) 29 | expect(updateCount).toBe(1) 30 | 31 | // remove 32 | classList.remove('b') 33 | expect(classList.item(1)).toBe('c') 34 | expect(classList.item(2)).toBe('d') 35 | expect(classList.item(3)).toBe(undefined) 36 | expect(classList.contains('b')).toBe(false) 37 | expect(updateCount).toBe(2) 38 | 39 | // toggle 40 | expect(classList.toggle('c')).toBe(false) 41 | expect(classList.contains('c')).toBe(false) 42 | expect(classList.toggle('c')).toBe(true) 43 | expect(classList.contains('c')).toBe(true) 44 | expect(updateCount).toBe(4) 45 | 46 | expect(classList.toggle('c', true)).toBe(true) 47 | expect(classList.contains('c')).toBe(true) 48 | expect(classList.toggle('c', false)).toBe(false) 49 | expect(classList.contains('c')).toBe(false) 50 | expect(classList.toggle('c', false)).toBe(false) 51 | expect(classList.contains('c')).toBe(false) 52 | expect(classList.toggle('c', true)).toBe(true) 53 | expect(classList.contains('c')).toBe(true) 54 | expect(updateCount).toBe(6) 55 | 56 | // toString 57 | expect(classList.toString()).toBe('a d c') 58 | 59 | classList._$parse('c b a dd') 60 | expect(classList.toString()).toBe('c b a dd') 61 | expect(updateCount).toBe(7) 62 | }) 63 | -------------------------------------------------------------------------------- /src/template/adapter/node/style-list.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 支持的样式属性列表,默认只包含常用的样式属性 3 | */ 4 | 5 | module.exports = [ 6 | 'position', 'top', 'bottom', 'right', 'left', 'float', 'clear', 7 | 'display', 'width', 'height', 'maxHeight', 'maxWidth', 'minHeight', 'minWidth', 'flexBasis', 'flexGrow', 'flexShrink', 'flexDirection', 'flexWrap', 'justifyContent', 'alignItems', 8 | 'padding', 'paddingBottom', 'paddingLeft', 'paddingRight', 'paddingTop', 9 | 'margin', 'marginBottom', 'marginLeft', 'marginRight', 'marginTop', 10 | 11 | 'background', 'backgroundClip', 'backgroundColor', 'backgroundImage', 'backgroundOrigin', 'backgroundPosition', 'backgroundRepeat', 'backgroundSize', 12 | 'border', 'borderRadius', 'borderBottomColor', 'borderBottomLeftRadius', 'borderBottomRightRadius', 'borderBottomStyle', 'borderBottomWidth', 'borderCollapse', 'borderImageOutset', 'borderImageRepeat', 'borderImageSlice', 'borderImageSource', 'borderImageWidth', 'borderLeftColor', 'borderLeftStyle', 'borderLeftWidth', 'borderRightColor', 'borderRightStyle', 'borderRightWidth', 'borderTopColor', 'borderTopLeftRadius', 'borderTopRightRadius', 'borderTopStyle', 'borderTopWidth', 13 | 'outline', 14 | 15 | 'animation', 'animationDelay', 'animationDirection', 'animationDuration', 'animationFillMode', 'animationIterationCount', 'animationName', 'animationPlayState', 'animationTimingFunction', 16 | 'transition', 'transitionDelay', 'transitionDuration', 'transitionProperty', 'transitionTimingFunction', 17 | 'transform', 'transformOrigin', 'perspective', 'perspectiveOrigin', 'backfaceVisibility', 18 | 19 | 'font', 'fontFamily', 'fontSize', 'fontStyle', 'fontWeight', 20 | 'color', 'textAlign', 'textDecoration', 'textIndent', 'textRendering', 'textShadow', 'textOverflow', 'textTransform', 21 | 'wordBreak', 'wordSpacing', 'wordWrap', 'lineHeight', 'letterSpacing', 'whiteSpace', 'userSelect', 22 | 23 | 'visibility', 'opacity', 'zIndex', 'zoom', 'overflow', 'overflowX', 'overflowY', 24 | 'boxShadow', 'boxSizing', 'content', 'cursor', 'direction', 'listStyle', 'objectFit', 'pointerEvents', 'resize', 'verticalAlign', 'willChange', 'clip', 'clipPath', 'fill' 25 | ] 26 | -------------------------------------------------------------------------------- /src/template/adapter/node/element/input.js: -------------------------------------------------------------------------------- 1 | const load = require('../../index') 2 | 3 | const Element = load('Element') 4 | 5 | const DEFAULT_MAX_LENGTH = '140' 6 | 7 | class HTMLInputElement extends Element { 8 | /** 9 | * 调用 _generateHtml 接口时用于处理额外的属性, 10 | */ 11 | _$dealWithAttrsForGenerateHtml(html, node) { 12 | const type = node.type 13 | if (type) html += ` type="${type}"` 14 | 15 | const value = node.value 16 | if (value) html += ` type="${value}"` 17 | 18 | const disabled = node.disabled 19 | if (disabled) html += ' disabled' 20 | 21 | const maxlength = node.maxlength 22 | if (maxlength) html += ` maxlength="${maxlength}"` 23 | 24 | return html 25 | } 26 | 27 | /** 28 | * 调用 outerHTML 的 setter 时用于处理额外的属性 29 | */ 30 | _$dealWithAttrsForOuterHTML(node) { 31 | this.type = node.type || '' 32 | this.value = node.value || '' 33 | this.disabled = node.disabled || '' 34 | this.maxlength = node.maxlength || DEFAULT_MAX_LENGTH 35 | } 36 | 37 | /** 38 | * 调用 cloneNode 接口时用于处理额外的属性 39 | */ 40 | _$dealWithAttrsForCloneNode() { 41 | return { 42 | type: this.type, 43 | value: this.value, 44 | disabled: this.disabled, 45 | maxlength: this.maxlength, 46 | } 47 | } 48 | 49 | /** 50 | * 对外属性和方法 51 | */ 52 | get type() { 53 | return this._attrs.get('type') 54 | } 55 | 56 | set type(value) { 57 | value = '' + value 58 | this._attrs.set('type', value) 59 | } 60 | 61 | get value() { 62 | return this._attrs.get('value') 63 | } 64 | 65 | set value(value) { 66 | value = '' + value 67 | this._attrs.set('value', value) 68 | } 69 | 70 | get disabled() { 71 | return !!this._attrs.get('disabled') 72 | } 73 | 74 | set disabled(value) { 75 | value = !!value 76 | this._attrs.set('disabled', value) 77 | } 78 | 79 | get maxlength() { 80 | return this._attrs.get('maxlength') || DEFAULT_MAX_LENGTH 81 | } 82 | 83 | set maxlength(value) { 84 | value = '' + value 85 | this._attrs.set('maxlength', value) 86 | } 87 | } 88 | 89 | module.exports = HTMLInputElement 90 | -------------------------------------------------------------------------------- /docs/plan.md: -------------------------------------------------------------------------------- 1 | # dom 2 | 3 | * [x] 调整节点:appendChild、insertBefore、removeChild、replcaeChild 4 | * [x] 亲戚节点:parentNode、childNodes、children、firstChild、lastChild、previousSibling、previousElementSibling 5 | * [x] 创建节点:createElement、createTextNode、createDocumentFragment、cloneNode 6 | * [x] 选择节点:getElementById、getElementsByTagName、getElementsByClassName、querySelector、querySelectorAll 7 | * [x] 基础属性:classList、style、id、className、nodeType、dataset 8 | * [x] 调整属性:attributes、setAttribute、getAttribute、removeAttribute 9 | * [x] 布局属性:getComputedStyle、getBoundingClientRect、clientWidth、clientHeight(异步拉取) 10 | * [x] 调整内容:innerHTML、outerHTML、textContent、innerText、nodeValue 11 | * [x] 事件:addEventListener、removeEventListener、捕获冒泡模型,stopPropagation、onXXX、dispatchEvent、CustomEvent 12 | * [x] 基础标签:img、table 相关、a、canvas(2d)、input(type=text/password/number) 13 | * [x] 其他:Image 14 | 15 | * [ ] 内容调整:insertAdjacentHTML 16 | * [ ] 基础标签:video 17 | * [ ] 基础标签:audio 18 | * [ ] 基础标签:表单相关 19 | 20 | # 样式 21 | 22 | * [x] 基础选择器:类选择器、标签选择器 23 | * [x] 亲属选择器:后代选择器、子选择器、兄弟选择器 24 | 25 | * [ ] 其他选择器:伪类选择器、伪元素选择器、id 选择器 26 | 27 | # bom 28 | 29 | * [x] 请求:XMLHttpRequest 30 | * [x] 存储:localStorage、sessionStorage、cookie 31 | * [x] 设备:screen、innerHeight/innerWidth、outerHeight/outerWidth、navigator 32 | * [x] 路由:location、页面跳转、window.open、history 33 | 34 | * [ ] 路由:锚跳转 35 | * [ ] 请求:上传、websocket 36 | 37 | # 其他 38 | 39 | * [x] 插件扩展 40 | * [x] 压缩配置 41 | 42 | * [ ] sourceMap 43 | 44 | # trick 45 | 46 | * 小程序没有正经的全局对象(如 window、global),需要找出 js 中用到的所有全局变量并修改成 window['xxx'] 的方式来访问,同时所有全局函数的声明也需要找出并挂在 window 下。 47 | * getComputedStyle、getBoundingClientRect 无法同步获取,因此在页面初始化的时候拉取一次,后续但凡遇到更新则再进行一次异步拉取。 48 | * 并不是所有标签名都支持,有一些 h5 标签会用统一替换成 element 标签,所以需要修改 wxss 里的标签选择器。 49 | * wxss 不支持 * 选择器,需要替换成标签选择器。 50 | * 为了保证 wxss 中的兄弟、子节点选择器可以使用,element 自定义组件中不使用任何额外的标签嵌套,video、img 等特殊标签除外。 51 | * 小程序中部分资源相关字段(如 background-image)只支持网络资源,因此需要提供 imageFilter 字段来过滤图片资源,替换资源路径为对应的网络路径。 52 | * 小程序页面的 route 和网页的 url 实现不一致,因此需要提供 urlMap 来提供每个页面对应的 url,用于页面跳转和 location 接口。 53 | * 小程序不支持动态修改样式,因此不支持动态设置 font-size。小程序根节点的 font-size 默认为`屏幕宽度 / 20 px`,因此开发者可以直接按照这个数值来进行开发,或者提供 375px 的屏幕宽度时根节点的 font-size 值,由框架进行换算。 54 | * 去掉了大部分的异常。 55 | 56 | # 性能 57 | 58 | * iphoneX 初次渲染,1000 个节点,160 ms 左右。 59 | * 小米 5s 初次渲染,1000 个节点,1250 ms 左右。 60 | -------------------------------------------------------------------------------- /test/mock.js: -------------------------------------------------------------------------------- 1 | let selectorQueryRes = [] 2 | 3 | class SelectorQuery { 4 | in() { 5 | return this 6 | } 7 | selectAll() { 8 | return this 9 | } 10 | fields() { 11 | return this 12 | } 13 | exec(callback) { 14 | callback(selectorQueryRes) 15 | } 16 | } 17 | 18 | global.wx = { 19 | createSelectorQuery() { 20 | return new SelectorQuery() 21 | }, 22 | } 23 | 24 | module.exports = { 25 | html: ` 26 | 27 | 28 | 29 | 30 | 31 | 32 | test 33 | 34 | 35 | 40 | 41 | 42 |
43 |
44 |
45 |
123
46 |
321
47 |
48 |
middle
49 |
50 | 1 51 | 2 52 | 3 53 |
54 |
tail
55 |
56 |
57 | 58 | 59 | 62 | 63 | 64 | `, 65 | wx: global.wx, 66 | setSelectorQueryRes(res) { 67 | selectorQueryRes = [res] 68 | }, 69 | }; 70 | -------------------------------------------------------------------------------- /src/template/adapter/bom/session-storage.js: -------------------------------------------------------------------------------- 1 | const load = require('../index') 2 | 3 | const Event = load('Event') 4 | 5 | class SessionStorage { 6 | constructor(pageId, window) { 7 | this._keys = [] 8 | this._map = {} 9 | this._window = window 10 | } 11 | 12 | /** 13 | * 触发 window 的 storage 事件 14 | */ 15 | _triggerStorage(key, newValue, oldValue, force) { 16 | if (!force && newValue === oldValue) return 17 | 18 | this._window._$trigger('storage', { 19 | event: new Event({ 20 | name: 'storage', 21 | target: this._window, 22 | _$extra: { 23 | key, 24 | newValue, 25 | oldValue, 26 | storageArea: this, 27 | url: this._window.location.href, 28 | } 29 | }) 30 | }) 31 | } 32 | 33 | /** 34 | * 对外属性和方法 35 | */ 36 | get length() { 37 | return this._keys.length 38 | } 39 | 40 | key(num) { 41 | if (typeof num !== 'number' || !isFinite(num) || num < 0) return null 42 | 43 | const key = this._keys[num] 44 | 45 | if (!key) return null 46 | 47 | return this._map[key] || null 48 | } 49 | 50 | getItem(key) { 51 | if (!key || typeof key !== 'string') return null 52 | 53 | return this._map[key] || null 54 | } 55 | 56 | setItem(key, data) { 57 | if (!key || typeof key !== 'string' || typeof data !== 'string') return 58 | 59 | const oldValue = this._map[key] || null 60 | 61 | this._map[key] = data 62 | 63 | // 调整顺序 64 | const index = this._keys.indexOf(key) 65 | if (index >= 0) this._keys.splice(index, 1) 66 | this._keys.push(key) 67 | 68 | this._triggerStorage(key, data, oldValue) 69 | } 70 | 71 | removeItem(key) { 72 | if (!key || typeof key !== 'string') return 73 | 74 | const oldValue = this._map[key] || null 75 | 76 | delete this._map[key] 77 | 78 | // 删除 key 79 | const index = this._keys.indexOf(key) 80 | if (index >= 0) this._keys.splice(index, 1) 81 | 82 | this._triggerStorage(key, null, oldValue) 83 | } 84 | 85 | clear() { 86 | this._keys.forEach(key => { 87 | delete this._map[key] 88 | }) 89 | 90 | this._keys.length = 0 91 | 92 | this._triggerStorage(null, null, null, true) 93 | } 94 | } 95 | 96 | module.exports = SessionStorage 97 | -------------------------------------------------------------------------------- /src/template/adapter/util/cache.js: -------------------------------------------------------------------------------- 1 | const pageMap = {} 2 | const pageKeyMap = {} 3 | 4 | /** 5 | * 初始化 6 | */ 7 | function init(pageId, options) { 8 | pageMap[pageId] = { 9 | document: options.document, 10 | window: options.window, 11 | nodeIdMap: options.nodeIdMap, 12 | nodeCompMap: {}, 13 | } 14 | } 15 | 16 | /** 17 | * 销毁 18 | */ 19 | function destroy(pageId) { 20 | delete pageMap[pageId] 21 | delete pageKeyMap[pageId] 22 | } 23 | 24 | /** 25 | * 获取 document 26 | */ 27 | function getDocument(pageId) { 28 | return pageMap[pageId] && pageMap[pageId].document 29 | } 30 | 31 | /** 32 | * 获取 window 33 | */ 34 | function getWindow(pageId) { 35 | return pageMap[pageId] && pageMap[pageId].window 36 | } 37 | 38 | /** 39 | * 设置页面 key 40 | */ 41 | function setPageKey(pageId, pageKey) { 42 | pageKeyMap[pageId] = pageKey 43 | } 44 | 45 | /** 46 | * 获取页面 key 47 | */ 48 | function getPageKey(pageId) { 49 | return pageKeyMap[pageId] || '' 50 | } 51 | 52 | /** 53 | * 存储 domNode 映射 54 | */ 55 | function setNode(pageId, nodeId, domNode = null) { 56 | const document = pageMap[pageId] && pageMap[pageId].document 57 | 58 | // 运行前调用,不做任何操作 59 | if (!document) return 60 | // 相当于删除映射 61 | if (!domNode) return pageMap[pageId].nodeIdMap[nodeId] = domNode 62 | 63 | let parentNode = domNode.parentNode 64 | 65 | while (parentNode && parentNode !== document.body) { 66 | parentNode = parentNode.parentNode 67 | } 68 | 69 | pageMap[pageId].nodeIdMap[nodeId] = parentNode === document.body ? domNode : null 70 | } 71 | 72 | /** 73 | * 根据 nodeId 获取 domNode 74 | */ 75 | function getNode(pageId, nodeId) { 76 | return pageMap[pageId].nodeIdMap[nodeId] 77 | } 78 | 79 | /** 80 | * 存储 component 实例映射 81 | */ 82 | function setNodeComp(pageId, nodeId, comp) { 83 | pageMap[pageId].nodeCompMap[nodeId] = comp 84 | } 85 | 86 | /** 87 | * 根据 nodeId 获取 component 实例 88 | */ 89 | function getNodeComp(pageId, nodeId) { 90 | return pageMap[pageId].nodeCompMap[nodeId] 91 | } 92 | 93 | module.exports = { 94 | init, 95 | destroy, 96 | getDocument, 97 | getWindow, 98 | setPageKey, 99 | getPageKey, 100 | setNode, 101 | getNode, 102 | setNodeComp, 103 | getNodeComp, 104 | } 105 | -------------------------------------------------------------------------------- /src/html/walker.js: -------------------------------------------------------------------------------- 1 | const load = require('../template/adapter') 2 | 3 | const tool = load('tool') 4 | const tagMap = load('tagMap') 5 | 6 | /** 7 | * 深度优先遍历 ast 8 | */ 9 | function walk(ast) { 10 | const stack = [ast] 11 | const res = { 12 | title: '', 13 | body: [], 14 | style: [], 15 | script: [], 16 | } 17 | 18 | while (stack.length) { 19 | const node = stack.pop() 20 | const { 21 | type, tagName = '', attrs = [], children = [] 22 | } = node 23 | 24 | node.nodeId = `a-${tool.getId()}` // 给每个节点设置唯一标识,a- 前缀表示是在运行前生成的,b- 前缀表示是在运行时生成的 25 | 26 | if (type !== 'element') continue // 跳过非元素节点 27 | 28 | const compName = tagMap[tagName] 29 | const attrsMap = {} 30 | 31 | // 属性列表转化成 map 32 | for (const attr of attrs) { 33 | const name = attr.name 34 | let value = attr.value 35 | 36 | if (name === 'style') value = value.replace('"', '\'') 37 | 38 | attrsMap[name] = value 39 | } 40 | 41 | node.attrs = attrsMap 42 | node.compName = compName 43 | 44 | if (tagName === 'title') { 45 | // 标题 46 | const child = children[0] || {} 47 | if (child.type === 'text') res.title = child.content 48 | } else if (tagName === 'link' && attrsMap.rel === 'stylesheet') { 49 | // 外部样式 50 | res.style.push({ 51 | type: 'outer', 52 | src: attrsMap.href, 53 | }) 54 | } else if (tagName === 'style') { 55 | // 内嵌样式 56 | const child = children[0] || {} 57 | if (child.type === 'text') { 58 | res.style.push({ 59 | type: 'inner', 60 | content: child.content, 61 | }) 62 | } 63 | } else if (tagName === 'body') { 64 | // 节点树 65 | res.body = node 66 | } else if (tagName === 'script') { 67 | // script 标签 68 | const child = children[0] || {} 69 | if (attrsMap.src) { 70 | res.script.push({ 71 | type: 'outer', 72 | src: attrsMap.src, 73 | }) 74 | } else if (child.type === 'text') { 75 | res.script.push({ 76 | type: 'inner', 77 | content: child.content, 78 | }) 79 | } 80 | } 81 | 82 | for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]) // 子节点入栈 83 | } 84 | 85 | return res 86 | } 87 | 88 | module.exports = { 89 | walk 90 | } 91 | -------------------------------------------------------------------------------- /src/css/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const request = require('request-promise-native') 4 | 5 | const _ = require('../utils') 6 | const adjust = require('./adjust') 7 | 8 | module.exports = { 9 | /** 10 | * 生成样式 11 | */ 12 | async generate(options) { 13 | const entry = options.entry 14 | const output = options.output 15 | const commonOutput = options.commonOutput 16 | const cssList = options.cssList 17 | const config = options.config 18 | const entryKey = options.entryKey 19 | const needCompress = !!options.compress.cssInH5 20 | const proxy = options.proxy 21 | const dirPath = path.dirname(entry) 22 | const adjustConfig = {entryKey, needCompress, ...config} 23 | 24 | let content = [ 25 | '/* original user agent stylesheet */\n@import "../../common/wxss/original.wxss";' 26 | ] 27 | 28 | // 遍历页面中的静态 css 29 | for (const css of cssList) { 30 | if (css.type === 'inner') { 31 | content.push(`/* style tag */\n${css.content}`) 32 | } else if (css.type === 'outer') { 33 | let cssPath = css.src 34 | let linkContent 35 | 36 | if (_.isRelative(cssPath)) { 37 | // 相对路径 38 | cssPath = path.join(dirPath, cssPath) 39 | linkContent = await _.readFile(cssPath) 40 | } else { 41 | // 其他 42 | const isFileExists = await _.checkFileExists(cssPath) 43 | 44 | if (isFileExists) { 45 | // 绝对路径 46 | linkContent = await _.readFile(cssPath) 47 | } else { 48 | // 网络 url 49 | linkContent = await request.get({url: cssPath, proxy}) 50 | } 51 | } 52 | 53 | if (linkContent) { 54 | // 写入 common 目录 55 | const extname = path.extname(cssPath) 56 | const filename = `${path.basename(cssPath, extname)}-${_.hash(linkContent)}.wxss` 57 | const adjustContent = await adjust(linkContent, adjustConfig) 58 | 59 | await _.writeFile(path.join(commonOutput, filename), adjustContent) 60 | content.push(`/* link style: ${css.src} */\n@import "../../common/wxss/${filename}";`) 61 | } 62 | } 63 | } 64 | 65 | content = content.join('\n\n') 66 | 67 | // 输出到 output 中 68 | const adjustContent = await adjust(content, adjustConfig) 69 | await _.writeFile(output, adjustContent) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/template/adapter/bom/local-storage.js: -------------------------------------------------------------------------------- 1 | const load = require('../index') 2 | 3 | const Event = load('Event') 4 | 5 | class LocalStorage { 6 | constructor(pageId, window) { 7 | this._keys = [] 8 | this._length = 0 9 | this._window = window 10 | } 11 | 12 | /** 13 | * 更新 storage 信息 14 | */ 15 | _updateInfo() { 16 | try { 17 | const info = wx.getStorageInfoSync() 18 | 19 | this._keys = info.keys 20 | this._length = info.currentSize 21 | } catch (err) { 22 | // eslint-disable-next-line no-console 23 | console.warn('getStorageInfoSync fail') 24 | } 25 | } 26 | 27 | /** 28 | * 触发 window 的 storage 事件 29 | */ 30 | _triggerStorage(key, newValue, oldValue, force) { 31 | if (!force && newValue === oldValue) return 32 | 33 | this._window._$trigger('storage', { 34 | event: new Event({ 35 | name: 'storage', 36 | target: this._window, 37 | _$extra: { 38 | key, 39 | newValue, 40 | oldValue, 41 | storageArea: this, 42 | url: this._window.location.href, 43 | } 44 | }) 45 | }) 46 | } 47 | 48 | /** 49 | * 对外属性和方法 50 | */ 51 | get length() { 52 | return this._length 53 | } 54 | 55 | key(num) { 56 | if (typeof num !== 'number' || !isFinite(num) || num < 0) return null 57 | 58 | const key = this._keys[num] 59 | 60 | if (!key) return null 61 | 62 | return wx.getStorageSync(key) || null 63 | } 64 | 65 | getItem(key) { 66 | if (!key || typeof key !== 'string') return null 67 | 68 | return wx.getStorageSync(key) || null 69 | } 70 | 71 | setItem(key, data) { 72 | if (!key || typeof key !== 'string' || typeof data !== 'string') return 73 | 74 | const oldValue = wx.getStorageSync(key) || null 75 | 76 | wx.setStorageSync(key, data) 77 | this._updateInfo() 78 | this._triggerStorage(key, data, oldValue) 79 | } 80 | 81 | removeItem(key) { 82 | if (!key || typeof key !== 'string') return 83 | 84 | const oldValue = wx.getStorageSync(key) || null 85 | 86 | wx.removeStorageSync(key) 87 | this._updateInfo() 88 | this._triggerStorage(key, null, oldValue) 89 | } 90 | 91 | clear() { 92 | wx.clearStorageSync() 93 | this._updateInfo() 94 | this._triggerStorage(null, null, null, true) 95 | } 96 | } 97 | 98 | module.exports = LocalStorage 99 | -------------------------------------------------------------------------------- /test/demo/demo06/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | vote waterfall flow 8 | 9 | 10 | 11 |
12 |
13 | 评选时间:2015年6月 14 |
15 |
16 |
2015浙江在线优秀员工评选
17 |
2015 THE LIST OF THE GOOD EMPLOYEES
18 |
19 |
20 | 34 |
35 | 第一轮投票剩余时间:3天23小时15分钟59秒 36 |
37 |
38 |
39 |
40 |
41 | arrow
42 |
43 |
44 |
45 | loading 46 |
47 |
    48 |
49 |
50 | 51 | 我是底线 52 | 53 |
54 |
55 | loading 56 |
57 |
58 |
59 | 60 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/js/adjust.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 调整 js 3 | */ 4 | 5 | const acorn = require('acorn') 6 | const acornGlobals = require('acorn-globals') 7 | const MagicString = require('magic-string') 8 | const babel = require('@babel/core') 9 | 10 | const ignoreGlobalVar = require('./ignore-global-var') 11 | 12 | /** 13 | * 调整全局变量 14 | */ 15 | function replaceGlobal(ast, magicString) { 16 | // 查找全局变量 17 | const globalVars = acornGlobals(ast) 18 | 19 | // 替换全局变量 20 | globalVars.forEach(globalVar => { 21 | globalVar.nodes.forEach(node => { 22 | if (node.name && ignoreGlobalVar.indexOf(node.name) < 0) { 23 | magicString.overwrite(node.start, node.end, `window['${node.name}']`) 24 | } 25 | }) 26 | }) 27 | 28 | return magicString.toString() 29 | } 30 | 31 | /** 32 | * 将最顶层的函数声明挂在 window 下 33 | */ 34 | function adjustTopFuncDecl(ast, magicString) { 35 | const topFuncs = [] 36 | 37 | if (ast.type === 'Program') { 38 | ast.body.forEach(node => { 39 | if (node.type === 'FunctionDeclaration' && node.id && node.id.type === 'Identifier') { 40 | // 函数声明 41 | topFuncs.push(node.id.name) 42 | } else if (node.type === 'VariableDeclaration' && Array.isArray(node.declarations)) { 43 | // 函数声明表达式 44 | node.declarations.forEach(declaration => { 45 | const init = declaration.init 46 | 47 | if (init && init.type === 'FunctionExpression' && init.id && init.id.type === 'Identifier') { 48 | topFuncs.push(init.id.name) 49 | } 50 | }) 51 | } 52 | }) 53 | } 54 | 55 | const appendLine = topFuncs.map(funcName => `window['${funcName}'] = ${funcName}`).join(';') 56 | if (appendLine) magicString.append(`\n${appendLine};`) 57 | 58 | return magicString.toString() 59 | } 60 | 61 | module.exports = async function (code, needCompress) { 62 | const comments = [] 63 | const ast = acorn.parse(code, { 64 | sourceType: 'script', 65 | locations: true, 66 | onComment(block, text, start, end) { 67 | comments.push({ 68 | block, text, start, end 69 | }) 70 | } 71 | }) 72 | const magicString = new MagicString(code) 73 | 74 | code = replaceGlobal(ast, magicString) 75 | code = adjustTopFuncDecl(ast, magicString) 76 | 77 | const plugins = [] 78 | if (needCompress) { 79 | plugins.push('babel-plugin-minify-mangle-names') 80 | } 81 | 82 | // 转换代码 83 | code = await babel.transform(code, { 84 | minified: needCompress, 85 | compact: needCompress, // 不包含多余的空格符和换行符 86 | comments: false, 87 | plugins, 88 | }) 89 | 90 | return code.code 91 | } 92 | -------------------------------------------------------------------------------- /src/template/adapter/tree/tag-map.js: -------------------------------------------------------------------------------- 1 | /** 2 | * dom 标签 - 自定义组件名映射表 3 | */ 4 | 5 | module.exports = { 6 | a: 'a', 7 | abbr: 'element', 8 | address: 'address', 9 | area: 'element', 10 | article: 'article', 11 | aside: 'aside', 12 | audio: 'h5-audio', 13 | b: 'element', 14 | base: 'element', 15 | bdi: 'element', 16 | bdo: 'element', 17 | blockquote: 'element', 18 | body: 'body', 19 | br: 'br', 20 | button: 'h5-button', 21 | canvas: 'h5-canvas', 22 | caption: 'element', 23 | cite: 'element', 24 | code: 'element', 25 | col: 'element', 26 | colgroup: 'element', 27 | data: 'element', 28 | datalist: 'element', 29 | dd: 'element', 30 | del: 'element', 31 | dfn: 'element', 32 | div: 'div', 33 | dl: 'element', 34 | dt: 'element', 35 | em: 'element', 36 | embed: 'element', 37 | fieldset: 'element', 38 | figcaption: 'element', 39 | figure: 'element', 40 | footer: 'footer', 41 | form: 'h5-form', 42 | h1: 'h1', 43 | h2: 'h2', 44 | h3: 'h3', 45 | h4: 'h4', 46 | h5: 'h5', 47 | h6: 'h6', 48 | head: '', 49 | header: 'header', 50 | hr: 'hr', 51 | html: '', 52 | i: 'element', 53 | iframe: 'iframe', 54 | img: 'img', 55 | input: 'h5-input', 56 | ins: 'element', 57 | kbd: 'element', 58 | keygen: 'element', 59 | label: 'label', 60 | legend: 'element', 61 | li: 'li', 62 | link: '', 63 | main: 'element', 64 | map: 'element', 65 | mark: 'element', 66 | meta: '', 67 | meter: 'element', 68 | nav: 'nav', 69 | noscript: 'element', 70 | object: 'element', 71 | ol: 'ol', 72 | optgroup: 'element', 73 | option: 'element', 74 | output: 'element', 75 | p: 'p', 76 | param: 'element', 77 | pre: 'element', 78 | progress: 'element', 79 | q: 'element', 80 | rb: 'element', 81 | rp: 'element', 82 | rt: 'element', 83 | rtc: 'element', 84 | ruby: 'element', 85 | s: 'element', 86 | samp: 'element', 87 | script: '', 88 | section: 'section', 89 | select: 'element', 90 | small: 'element', 91 | source: 'element', 92 | span: 'span', 93 | strong: 'strong', 94 | style: '', 95 | sub: 'element', 96 | sup: 'element', 97 | table: 'table', 98 | tbody: 'tbody', 99 | td: 'td', 100 | template: '', 101 | textarea: 'element', 102 | tfoot: 'element', 103 | th: 'th', 104 | thead: 'thead', 105 | time: 'element', 106 | title: '', 107 | tr: 'tr', 108 | track: 'element', 109 | u: 'element', 110 | ul: 'ul', 111 | var: 'element', 112 | video: 'h5-video', 113 | wbr: 'element', 114 | } 115 | -------------------------------------------------------------------------------- /src/template/adapter/node/class-list.js: -------------------------------------------------------------------------------- 1 | /** 2 | * babel extends 无法直接继承 Array,所以换种方法来继承:https://babeljs.io/docs/en/caveats/#classes 3 | */ 4 | 5 | function ClassList(className, onUpdate) { 6 | this._doUpdate = onUpdate 7 | 8 | this._$parse(className, true) 9 | } 10 | 11 | ClassList.prototype = Object.assign([], { 12 | /** 13 | * 解析 className 14 | */ 15 | _$parse(className = '', notTriggerUpdate) { 16 | this.length = 0 // 置空当前内容 17 | 18 | className = className.trim() 19 | className = className ? className.split(/\s+/) : [] 20 | 21 | for (const item of className) { 22 | this.push(item) 23 | } 24 | 25 | if (!notTriggerUpdate) this._doUpdate() 26 | }, 27 | 28 | /** 29 | * 对外属性和方法 30 | */ 31 | item(index) { 32 | return this[index] 33 | }, 34 | 35 | contains(className) { 36 | if (typeof className !== 'string') return false 37 | 38 | return this.indexOf(className) !== -1 39 | }, 40 | 41 | add(...args) { 42 | let isUpdate = false 43 | 44 | for (let className of args) { 45 | if (typeof className !== 'string') continue 46 | 47 | className = className.trim() 48 | 49 | if (className && this.indexOf(className) === -1) { 50 | this.push(className) 51 | isUpdate = true 52 | } 53 | } 54 | 55 | if (isUpdate) this._doUpdate() 56 | }, 57 | 58 | remove(...args) { 59 | let isUpdate = false 60 | 61 | for (let className of args) { 62 | if (typeof className !== 'string') continue 63 | 64 | className = className.trim() 65 | 66 | if (!className) continue 67 | 68 | const index = this.indexOf(className) 69 | if (index >= 0) { 70 | this.splice(index, 1) 71 | isUpdate = true 72 | } 73 | } 74 | 75 | if (isUpdate) this._doUpdate() 76 | }, 77 | 78 | toggle(className, force) { 79 | if (typeof className !== 'string') return false 80 | 81 | className = className.trim() 82 | 83 | if (!className) return false 84 | 85 | const isNotContain = this.indexOf(className) === -1 86 | let action = isNotContain ? 'add' : 'remove' 87 | action = force === true ? 'add' : force === false ? 'remove' : action 88 | 89 | if (action === 'add') { 90 | this.add(className) 91 | } else { 92 | this.remove(className) 93 | } 94 | 95 | return force === true || force === false ? force : isNotContain 96 | }, 97 | 98 | toString() { 99 | return this.join(' ') 100 | }, 101 | }) 102 | 103 | module.exports = ClassList 104 | -------------------------------------------------------------------------------- /src/template/adapter/node/element/canvas.js: -------------------------------------------------------------------------------- 1 | const load = require('../../index') 2 | 3 | const Element = load('Element') 4 | 5 | const NEED_TO_CALL_DRAW = ['fill', 'stroke', 'fillRect', 'fillText', 'strokeRect', 'strokeText', 'drawImage'] 6 | 7 | class HTMLCanvasElement extends Element { 8 | constructor(options, tree) { 9 | super(options, tree) 10 | 11 | this._drawTimer = null 12 | this._context = null // for canvas 13 | this._initRect() 14 | } 15 | 16 | /** 17 | * 初始化长宽 18 | */ 19 | _initRect() { 20 | let width = this.getAttribute('width') 21 | let height = this.getAttribute('height') 22 | 23 | if (width) { 24 | if (/^[.\d]+$/.test(width)) width = `${width}px` 25 | this._style.width = width 26 | } 27 | 28 | if (height) { 29 | if (/^[.\d]+$/.test(height)) height = `${height}px` 30 | this._style.height = height 31 | } 32 | } 33 | 34 | /** 35 | * 绘制 canvas 36 | */ 37 | _draw() { 38 | if (this._drawTimer) return 39 | 40 | this._drawTimer = setTimeout(() => { 41 | this._context.draw(true, () => { 42 | this._drawTimer = null 43 | }) 44 | }, 0) 45 | } 46 | 47 | /** 48 | * 设置 context 49 | */ 50 | get _$context() { 51 | return this._context 52 | } 53 | 54 | set _$context(context) { 55 | const that = this 56 | 57 | // 接口微调 58 | NEED_TO_CALL_DRAW.forEach(name => { 59 | context[`_$${name}`] = context[name] 60 | context[name] = function (...args) { 61 | if (name === 'drawImage' && typeof args[0] === 'object') { 62 | // 第一个参数改为 image 路径 63 | args[0] = args[0].src 64 | } 65 | 66 | // eslint-disable-next-line no-unused-expressions 67 | this[`_$${name}`] && this[`_$${name}`](...args) 68 | that._draw() 69 | } 70 | }) 71 | 72 | this._context = context 73 | } 74 | 75 | /** 76 | * 对外属性和方法 77 | */ 78 | get width() { 79 | return parseInt(this.getAttribute('width'), 10) || 0 80 | } 81 | 82 | set width(value) { 83 | this.setAttribute('width', value) 84 | this._initRect() 85 | } 86 | 87 | get height() { 88 | return parseInt(this.getAttribute('height'), 10) || 0 89 | } 90 | 91 | set height(value) { 92 | this.setAttribute('height', value) 93 | this._initRect() 94 | } 95 | 96 | getContext(contextType) { 97 | // 目前只支持 2d 98 | if (contextType !== '2d') console.warn(`not support context type for canvas.getContext: ${contextType}`) 99 | 100 | return this._context 101 | } 102 | } 103 | 104 | module.exports = HTMLCanvasElement 105 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'extends': [ 3 | 'airbnb-base', 4 | 'plugin:promise/recommended' 5 | ], 6 | 'parserOptions': { 7 | 'ecmaVersion': 9, 8 | 'ecmaFeatures': { 9 | 'jsx': false 10 | }, 11 | 'sourceType': 'module' 12 | }, 13 | 'env': { 14 | 'es6': true, 15 | 'node': true, 16 | 'jest': true 17 | }, 18 | 'plugins': [ 19 | 'import', 20 | 'node', 21 | 'promise' 22 | ], 23 | 'rules': { 24 | 'arrow-parens': 'off', 25 | 'comma-dangle': [ 26 | 'error', 27 | 'only-multiline' 28 | ], 29 | 'complexity': ['error', 10], 30 | 'func-names': 'off', 31 | 'global-require': 'off', 32 | 'handle-callback-err': [ 33 | 'error', 34 | '^(err|error)$' 35 | ], 36 | 'import/no-unresolved': [ 37 | 'error', 38 | { 39 | 'caseSensitive': true, 40 | 'commonjs': true, 41 | 'ignore': ['^[^.]'] 42 | } 43 | ], 44 | 'import/prefer-default-export': 'off', 45 | 'linebreak-style': 'off', 46 | 'no-catch-shadow': 'error', 47 | 'no-continue': 'off', 48 | 'no-div-regex': 'warn', 49 | 'no-else-return': 'off', 50 | 'no-param-reassign': 'off', 51 | 'no-plusplus': 'off', 52 | 'no-shadow': 'off', 53 | 'no-multi-assign': 'off', 54 | 'no-underscore-dangle': 'off', 55 | 'node/no-deprecated-api': 'error', 56 | 'node/process-exit-as-throw': 'error', 57 | 'object-curly-spacing': [ 58 | 'error', 59 | 'never' 60 | ], 61 | 'operator-linebreak': [ 62 | 'error', 63 | 'after', 64 | { 65 | 'overrides': { 66 | ':': 'before', 67 | '?': 'before' 68 | } 69 | } 70 | ], 71 | 'prefer-arrow-callback': 'off', 72 | 'prefer-destructuring': 'off', 73 | 'prefer-template': 'off', 74 | 'quote-props': [ 75 | 1, 76 | 'as-needed', 77 | { 78 | 'unnecessary': true 79 | } 80 | ], 81 | 'semi': [ 82 | 'error', 83 | 'never' 84 | ], 85 | // 补充 86 | 'no-return-assign': 'off', 87 | 'complexity': 'off', 88 | 'no-use-before-define': 'off', 89 | 'max-len': 'off', 90 | 'no-restricted-syntax': 'off', 91 | 'no-await-in-loop': 'off', 92 | 'no-console': 'off', 93 | 'class-methods-use-this': 'off', 94 | 'no-nested-ternary': 'off', 95 | 'no-mixed-operators': 'off', 96 | 'no-lonely-if': 'off', 97 | 'consistent-return': 'off', 98 | 'no-restricted-globals': 'off', 99 | 'promise/always-return': 'off' 100 | }, 101 | 'globals': { 102 | 'window': true, 103 | 'document': true, 104 | 'App': true, 105 | 'Page': true, 106 | 'Component': true, 107 | 'Behavior': true, 108 | 'wx': true, 109 | 'getCurrentPages': true, 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /test/demo/demo05/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | html5 拼图游戏 8 | 9 | 10 | 11 | 12 | 13 | 14 | music 15 | 16 | 17 |
18 | 19 | 20 |
    21 |
  • startgame
  • 22 |
  • gamerule
  • 23 |
24 | 25 |
26 | rule 27 |
understand
28 |
29 | 30 |
31 | 32 | 33 | 34 |
35 | 36 |
37 | 38 |
39 | second 40 |
41 | 42 | 43 | 44 |
45 | source 46 |
47 | guanqia 48 | 00:60 49 |
50 |
51 | 52 | 请选择更高版本的浏览器 53 |
54 | 55 |
56 | lose 57 |
    58 |
  • 59 | restart 60 |
  • 61 |
  • 62 | invite 63 |
  • 64 |
65 | 66 |
67 | 68 |
69 | winnbg 70 | openbox 71 |
72 |
73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/js/page.js.tmpl: -------------------------------------------------------------------------------- 1 | Page({ 2 | data: { 3 | pageId: '', 4 | }, 5 | 6 | onLoad(query) { 7 | const pageId = this.pageId = `p-${tool.getId()}` // 页面 id 8 | const nodeIdMap = {} 9 | const document = this.document = new Document(pageId, pageKey, ast, nodeIdMap); 10 | const window = this.window = new Window(pageId, pageKey); 11 | 12 | // 处理页面参数,只有当页面是其他页面打开或跳转时才处理 13 | if (query.type === 'open' || query.type === 'jump') { 14 | if (query.search) window.location.search = decodeURIComponent(query.search) 15 | if (query.hash) window.location.hash = decodeURIComponent(query.hash) 16 | } 17 | 18 | initGlobalVar(window, document); 19 | initDocumentVar(window, document); 20 | 21 | cache.init(pageId, { 22 | document, 23 | window, 24 | nodeIdMap, 25 | }); 26 | }, 27 | 28 | onUnload() { 29 | cache.destroy(this.pageId) 30 | }, 31 | 32 | onReady() { 33 | this.setData({ 34 | pageId: this.pageId 35 | }, () => { 36 | // 延迟执行,保证 webview 上的信息可以完整获取 37 | setTimeout(() => { 38 | this.window._$fetchWebviewInfo(this).then(() => { 39 | try { 40 | this.init() 41 | } catch (err) { 42 | console.error(err) 43 | } 44 | }).catch(err => { 45 | console.warn('webview 端数据获取失败,可能会造成 getComputedStyle、getBoundingClientRect 等接口无法使用') 46 | 47 | try { 48 | this.init() 49 | } catch (err) { 50 | console.error(err) 51 | } 52 | }) 53 | }, 0) 54 | }) 55 | }, 56 | 57 | /** 58 | * 初始化页面 59 | */ 60 | init() { 61 | const window = this.window 62 | const document = this.document 63 | 64 | // 监听节点变化 65 | this.onDomTreeUpdate = this.onDomTreeUpdate.bind(this) 66 | window.addEventListener('_domTreeUpdate', tool.throttle(this.onDomTreeUpdate, 100)) 67 | 68 | run(window, document); // 执行 appService 端页面初始化逻辑 69 | 70 | // document 的 DOMContentLoaded 事件 71 | document.documentElement._$trigger('DOMContentLoaded', { 72 | event: new Event({ 73 | name: 'DOMContentLoaded', 74 | target: document, 75 | eventPhase: Event.AT_TARGET 76 | }), 77 | currentTarget: document, 78 | }) 79 | 80 | // window 的 load 事件 81 | window._$trigger('load', { 82 | event: new Event({ 83 | name: 'load', 84 | target: window, 85 | eventPhase: Event.AT_TARGET 86 | }), 87 | currentTarget: window, 88 | }) 89 | }, 90 | 91 | /** 92 | * 监听节点变化 93 | */ 94 | onDomTreeUpdate() { 95 | this.window._$fetchWebviewInfo(this).then(() => { 96 | // ignore 97 | }).catch(err => { 98 | console.warn('webview 端数据获取失败,可能会造成 getComputedStyle、getBoundingClientRect 等接口无法使用') 99 | this.init() 100 | }) 101 | } 102 | }); 103 | -------------------------------------------------------------------------------- /test/css/adjust.test.js: -------------------------------------------------------------------------------- 1 | const adjust = require('../../src/css/adjust') 2 | 3 | test('adjust css content: normal tagname', async () => { 4 | const res = await adjust('a, p {width:12px;} #id {height: 100%;}') 5 | expect(res).toBe('a, p { width:12px;}#id { height: 100%;}') 6 | }) 7 | 8 | test('adjust css content: map tagname', async () => { 9 | const res = await adjust('a, p > video {width:12px;}') 10 | expect(res).toBe('a, p > h5-video { width:12px;}') 11 | }) 12 | 13 | test('adjust css content: html tagname', async () => { 14 | const res = await adjust('a, html, p {width:12px;}') 15 | expect(res).toBe('a, page, p { width:12px;}') 16 | }) 17 | 18 | test('adjust css content: * tagname', async () => { 19 | const res = await adjust('* {background:url(http://a.do);}') 20 | expect(res).toBe('a, abbr, address, area, article, aside, h5-audio, b, base, bdi, bdo, blockquote, body, br, h5-button, h5-canvas, caption, cite, code, col, colgroup, data, datalist, dd, del, dfn, div, dl, dt, em, embed, fieldset, figcaption, figure, footer, h5-form, h1, h2, h3, h4, h5, h6, header, hr, i, iframe, img, h5-input, ins, kbd, keygen, label, legend, li, main, map, mark, meter, nav, noscript, object, ol, optgroup, option, output, p, param, pre, progress, q, rb, rp, rt, rtc, ruby, s, samp, section, select, small, source, span, strong, sub, sup, table, tbody, td, textarea, tfoot, th, thead, time, tr, track, u, ul, var, h5-video, wbr, html, page { background:url(\"http://a.do\");}') 21 | }) 22 | 23 | test('adjust css content: resFilter', async () => { 24 | let count = 0 25 | const res3 = await adjust('#test-image {background: url(\'http://a.b.c/0\');background: url("http://a.b.c/1");background: url(http://a.b.c/2);background-image: url(\'http://a.b.c/3\');background-image: url("http://a.b.c/4");background-image: url(http://a.b.c/5);src: url(\'http://a.b.c/6\');src: url("http://a.b.c/7");src: url(http://a.b.c/8);other: url(\'http://a.b.c/9\');other: url("http://a.b.c/10");other: url(http://a.b.c/11);}', { 26 | resFilter(src, entry) { 27 | expect(src).toBe(`http://a.b.c/${count++}`) 28 | expect(entry).toBe('xxx') 29 | 30 | return src.replace('a.b', 'b.a') 31 | }, 32 | entryKey: 'xxx', 33 | }) 34 | expect(count).toBe(9) 35 | expect(res3).toBe('#test-image { background: url("http://b.a.c/0"); background: url("http://b.a.c/1"); background: url("http://b.a.c/2"); background-image: url("http://b.a.c/3"); background-image: url("http://b.a.c/4"); background-image: url("http://b.a.c/5"); src: url("http://b.a.c/6"); src: url("http://b.a.c/7"); src: url("http://b.a.c/8"); other: url(\'http://a.b.c/9\'); other: url("http://a.b.c/10"); other: url(http://a.b.c/11);}') 36 | }) 37 | 38 | test('adjust css content: rem', async () => { 39 | const res = await adjust('#a {width: 10rem;height: 5rem;}', { 40 | rem: 25, 41 | }) 42 | expect(res).toBe('#a { width: 13.333rem; height: 6.667rem;}') 43 | }) 44 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const _ = require('./utils') 4 | const template = require('./template') 5 | const html = require('./html') 6 | const css = require('./css') 7 | const js = require('./js') 8 | const json = require('./json') 9 | 10 | module.exports = async function (options) { 11 | const configPath = options.config 12 | const extendPath = options.extend 13 | let entry = options.entry || {} 14 | const output = options.output || path.join(process.pwd(), './h5_to_miniprogram_output') 15 | const compress = options.compress === true ? { 16 | jsInH5: true, 17 | cssInH5: true, 18 | } : (options.compress || {}) 19 | const proxy = options.proxy 20 | 21 | let config = {} 22 | let indexEntry = '' // 首页 23 | 24 | // 载入配置 25 | try { 26 | // eslint-disable-next-line import/no-dynamic-require 27 | config = require(configPath) 28 | indexEntry = config.index 29 | } catch (err) { 30 | // ignore 31 | } 32 | 33 | // 参数调整 34 | if (typeof entry === 'string') entry = {index: entry} 35 | 36 | // 创建 output 37 | await _.recursiveMkdir(output) 38 | 39 | // 调整首页顺序 40 | const entryKeys = Object.keys(entry) 41 | const index = entryKeys.indexOf(indexEntry) 42 | 43 | if (index >= 0) { 44 | entryKeys.splice(index, 1) 45 | entryKeys.unshift(indexEntry) 46 | } 47 | 48 | // 遍历所有页面 49 | for (const entryKey of entryKeys) { 50 | const entryPath = entry[entryKey] 51 | 52 | // 检查 entry 53 | if (!entryPath || typeof entryPath !== 'string') { 54 | throw new Error('entry path must be a string') 55 | } 56 | 57 | const isEntryExists = await _.checkFileExists(entryPath) 58 | if (!isEntryExists) { 59 | throw new Error('entry path is not a valid path') 60 | } 61 | 62 | // 读取并解析 entry 63 | const entryHtml = await _.readFile(entryPath) 64 | const entryInfo = await html.parse(entryHtml) 65 | 66 | // 生成页面 67 | await html.generate({ 68 | output: path.join(output, `./pages/${entryKey}/${entryKey}.wxml`), 69 | body: entryInfo.body, 70 | }) 71 | 72 | // 生成样式 73 | await css.generate({ 74 | entry: entryPath, 75 | output: path.join(output, `./pages/${entryKey}/${entryKey}.wxss`), 76 | commonOutput: path.join(output, './common/wxss'), 77 | cssList: entryInfo.style, 78 | config, 79 | entryKey, 80 | compress, 81 | proxy, 82 | }) 83 | 84 | // 生成脚本 85 | await js.generate({ 86 | entry: entryPath, 87 | output: path.join(output, `./pages/${entryKey}/${entryKey}.js`), 88 | commonOutput: path.join(output, './common/js'), 89 | jsList: entryInfo.script, 90 | body: entryInfo.body, 91 | entryKey, 92 | compress, 93 | proxy, 94 | }) 95 | 96 | // 生成配置 97 | await json.generate({ 98 | output: path.join(output, `./pages/${entryKey}/${entryKey}.json`), 99 | }) 100 | } 101 | 102 | // 生成其他文件 103 | await template.generate({ 104 | output, 105 | entryKeys, 106 | configPath, 107 | extendPath, 108 | compress, 109 | }) 110 | } 111 | -------------------------------------------------------------------------------- /test/demo/demo03/css/index.css: -------------------------------------------------------------------------------- 1 | *{ 2 | padding: 0; 3 | margin:0; 4 | } 5 | 6 | html,body{ 7 | height: 100%; 8 | } 9 | 10 | @media screen and (min-width: 750px){ 11 | .header{ 12 | position: absolute; 13 | width: 100%; 14 | height: 80px; 15 | background: rgb(209, 14, 38); 16 | display: flex; 17 | align-items: center; 18 | justify-content: center; 19 | min-width: 1000px; 20 | border-bottom: 1px solid rgba(255, 255, 255, 0.1); 21 | } 22 | 23 | .header .logo{ 24 | flex: 0 0 80px; 25 | height: 50%; 26 | background: url('../images/logo.png') no-repeat; 27 | } 28 | 29 | .header .arrow_left{ 30 | margin-left: 20px; 31 | color: rgba(174, 10, 41, 0.9); 32 | } 33 | 34 | .header .nav{ 35 | flex: 0 0 800px; 36 | height: 100%; 37 | list-style: none; 38 | display: flex; 39 | margin: 0 20px; 40 | } 41 | 42 | .header .nav .nav-item{ 43 | flex: 0 0 80px; 44 | height: 100%; 45 | text-align: center; 46 | color: #fff; 47 | line-height: 80px; 48 | } 49 | 50 | .header .arrow_right{ 51 | color: rgba(174, 10, 41, 0.9); 52 | } 53 | 54 | .header .nav .active{ 55 | background: rgba(174, 10, 41, 0.9); 56 | } 57 | 58 | .header .mobile{ 59 | display: none; 60 | } 61 | 62 | .content{ 63 | min-width: 1000px; 64 | width: 100%; 65 | height: 100%; 66 | background: #f3f5f7; 67 | } 68 | .content .tips{ 69 | display: block; 70 | margin: 0 auto; 71 | position: relative; 72 | top: 50%; 73 | transform: translateY(-50%); 74 | text-align: center; 75 | } 76 | 77 | .content .tips img{ 78 | width: 100px; 79 | height: 80px; 80 | display: block; 81 | margin: 0 auto; 82 | margin-bottom: 20px; 83 | } 84 | 85 | .content .tips span{ 86 | font-size: 12px; 87 | } 88 | } 89 | 90 | @media screen and (max-width: 750px){ 91 | .header{ 92 | width: 100%; 93 | height: 40px; 94 | position: absolute; 95 | } 96 | 97 | .header .arrow_left,.arrow_right{ 98 | display: none; 99 | } 100 | 101 | .header .nav{ 102 | list-style: none; 103 | display: flex; 104 | background: #f3f5f7; 105 | } 106 | 107 | .header .nav .nav-item{ 108 | flex: 0 0 30px; 109 | /* width: 40px; */ 110 | height: 100%; 111 | line-height: 40px; 112 | margin-left: 10px; 113 | font-size: 12px; 114 | } 115 | 116 | .header .nav .active{ 117 | color: rgb(209, 14, 38); 118 | } 119 | 120 | .header .pc{ 121 | display: none; 122 | } 123 | 124 | .header .nav .icon-plus{ 125 | font-size: 16px; 126 | line-height: 40px; 127 | color: #999; 128 | flex: 0 0 40px; 129 | text-align: center 130 | } 131 | 132 | .content{ 133 | width: 100%; 134 | height: 100%; 135 | } 136 | 137 | .tips{ 138 | position: relative; 139 | top: 50%; 140 | transform: translateY(-50%); 141 | text-align: center; 142 | } 143 | 144 | .tips img{ 145 | width: 50px; 146 | height: 40px; 147 | display: block; 148 | margin: 0 auto; 149 | margin-bottom: 20px; 150 | } 151 | 152 | .tips span{ 153 | font-size: 10px; 154 | 155 | } 156 | } -------------------------------------------------------------------------------- /src/template/adapter/bom/history.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 暂不维护跳转后页面的历史,不做页面刷新的操作 3 | */ 4 | const load = require('../index') 5 | 6 | const Location = load('Location') 7 | const EventTarget = load('EventTarget') 8 | 9 | class History extends EventTarget { 10 | constructor(location) { 11 | super() 12 | 13 | this._location = location 14 | this._stack = [{ 15 | state: null, 16 | title: '', 17 | url: location.href, 18 | }] 19 | this._currentIndex = 0 20 | 21 | this._location.addEventListener('_addToHistory', evt => { 22 | this._currentIndex++ 23 | this._stack = this._stack.slice(0, this._currentIndex) 24 | this._stack.push({ 25 | state: null, 26 | title: '', 27 | url: evt.href 28 | }) 29 | }) 30 | } 31 | 32 | /** 33 | * 检查是否同源 34 | */ 35 | _checkOrigin(url) { 36 | const {protocol, hostname, port} = Location._$parse(url) 37 | 38 | return (!protocol || this._location.protocol === protocol) && (!hostname || this._location.hostname === hostname) && ((!hostname && !port) || this._location.port === port) 39 | } 40 | 41 | /** 42 | * 对外属性和方法 43 | */ 44 | get state() { 45 | const current = this._stack[this._currentIndex] 46 | return current && current.state || null 47 | } 48 | 49 | get length() { 50 | return this._stack.length 51 | } 52 | 53 | back() { 54 | this.go(-1) 55 | } 56 | 57 | forward() { 58 | this.go(1) 59 | } 60 | 61 | go(delta) { 62 | if (typeof delta === 'number') { 63 | const next = this._currentIndex + delta 64 | 65 | if (next >= 0 && next < this._stack.length && this._currentIndex !== next) { 66 | this._currentIndex = next 67 | // 替换 href,但不做跳转(理论上此处应该做跳转,但是在小程序环境里不适合) 68 | this._location._$setHrefWithoutCheck(this._stack[this._currentIndex].url) 69 | 70 | this._$trigger('popstate', { 71 | event: { 72 | state: this.state 73 | } 74 | }) 75 | } 76 | } else { 77 | // 刷新当前页面 78 | this._location.reload() 79 | } 80 | } 81 | 82 | pushState(state = null, title, url) { 83 | if (!url || typeof url !== 'string') return 84 | 85 | if (this._checkOrigin(url)) { 86 | // 同源才允许操作 87 | if (title && typeof title === 'string') { 88 | // 设置标题 89 | wx.setNavigationBarTitle({title}) 90 | } 91 | 92 | 93 | this._currentIndex++ 94 | this._stack = this._stack.slice(0, this._currentIndex) 95 | 96 | // 替换 href,但不做跳转 97 | this._location._$setHrefWithoutCheck(url) 98 | 99 | this._stack.push({state, title, url: this._location.href}) 100 | } 101 | } 102 | 103 | replaceState(state = null, title, url) { 104 | if (!url || typeof url !== 'string') return 105 | 106 | if (this._checkOrigin(url)) { 107 | // 同源才允许操作 108 | if (title && typeof title === 'string') { 109 | // 设置标题 110 | wx.setNavigationBarTitle({title}) 111 | } 112 | 113 | // 替换 href,但不做跳转 114 | this._location._$setHrefWithoutCheck(url) 115 | 116 | this._stack.splice(this._currentIndex, 1, {state, title, url: this._location.href}) 117 | } 118 | } 119 | } 120 | 121 | module.exports = History 122 | -------------------------------------------------------------------------------- /src/template/adapter/tree/tree.js: -------------------------------------------------------------------------------- 1 | const load = require('../index') 2 | 3 | const QuerySelector = load('QuerySelector') 4 | 5 | /** 6 | * 遍历 dom 树,收集类和标签对应的节点列表 7 | */ 8 | function walkDomTree(node, cache) { 9 | const tagMap = cache.tagMap = cache.tagMap || {} 10 | const classMap = cache.classMap = cache.classMap || {} 11 | 12 | const children = node.children || [] 13 | 14 | for (const child of children) { 15 | const {tagName, classList} = child 16 | 17 | // 标签 18 | tagMap[tagName] = tagMap[tagName] || [] 19 | tagMap[tagName].push(child) 20 | 21 | // 类 22 | for (const className of classList) { 23 | classMap[className] = classMap[className] || [] 24 | classMap[className].push(child) 25 | } 26 | 27 | // 递归遍历 28 | walkDomTree(child, cache) 29 | } 30 | } 31 | 32 | class Tree { 33 | constructor(pageId, root, nodeIdMap, document) { 34 | this.pageId = pageId 35 | this.root = document._$createElement(root, this) 36 | this.nodeIdMap = nodeIdMap 37 | this.idMap = {} 38 | this.document = document 39 | 40 | this.querySelector = new QuerySelector() 41 | if (nodeIdMap) nodeIdMap[root.nodeId] = this.root 42 | 43 | this.walk(root, this.root) 44 | } 45 | 46 | /** 47 | * 遍历 ast 48 | */ 49 | walk(ast, parentNode) { 50 | const children = ast.children 51 | const idMap = this.idMap 52 | const nodeIdMap = this.nodeIdMap 53 | const document = this.document 54 | 55 | if (!children || !children.length) return 56 | 57 | // 遍历子节点 58 | for (const child of children) { 59 | let childNode 60 | 61 | if (child.type === 'element') { 62 | childNode = document._$createElement(child, this) 63 | } else if (child.type === 'text') { 64 | childNode = document._$createTextNode(child, this) 65 | } 66 | 67 | // 处理 id 缓存 68 | const id = childNode.id 69 | if (id && !idMap[id]) { 70 | idMap[id] = childNode 71 | } 72 | 73 | // 处理 nodeId 缓存 74 | if (nodeIdMap) nodeIdMap[child.nodeId] = childNode 75 | 76 | // 插入子节点 77 | parentNode.appendChild(childNode) 78 | 79 | // 遍历子节点的 ast 80 | this.walk(child, childNode) 81 | } 82 | } 83 | 84 | /** 85 | * 更新 idMap 86 | */ 87 | updateIdMap(id, node) { 88 | this.idMap[id] = node 89 | } 90 | 91 | /** 92 | * 根据 id 获取节点 93 | */ 94 | getById(id) { 95 | return this.idMap[id] 96 | } 97 | 98 | /** 99 | * 根据标签名获取节点列表 100 | */ 101 | getByTagName(tagName, node) { 102 | const cache = {} 103 | walkDomTree(node || this.root, cache) 104 | 105 | return cache.tagMap[tagName.toUpperCase()] || [] 106 | } 107 | 108 | /** 109 | * 根据类名获取节点列表 110 | */ 111 | getByClassName(className, node) { 112 | const cache = {} 113 | walkDomTree(node || this.root, cache) 114 | 115 | return cache.classMap[className] || [] 116 | } 117 | 118 | /** 119 | * 查询符合条件的节点 120 | */ 121 | query(selector, node) { 122 | const cache = {} 123 | walkDomTree(node || this.root, cache) 124 | 125 | return this.querySelector.exec(selector, node || this.root, { 126 | idMap: this.idMap, 127 | tagMap: cache.tagMap, 128 | classMap: cache.classMap, 129 | }) 130 | } 131 | } 132 | 133 | module.exports = Tree 134 | -------------------------------------------------------------------------------- /test/adapter/bom/xml-http-request.test.js: -------------------------------------------------------------------------------- 1 | const mock = require('../../mock') 2 | const load = require('../../../src/template/adapter') 3 | 4 | const XMLHttpRequest = load('XMLHttpRequest') 5 | 6 | test('xml-http-request', async () => { 7 | global.wx.request = function(options) { 8 | expect(options.url).toBe('https://a.b.c?a=12#haha') 9 | expect(options.header).toEqual({ 10 | "Accept": "*/*", 11 | 'Content-type': 'application/x-www-form-urlencoded', 12 | }) 13 | expect(options.method).toBe('POST') 14 | expect(options.dataType).toBe('text') 15 | expect(options.responseType).toBe('text') 16 | 17 | const success = options.success 18 | const fail = options.fail 19 | const complete = options.complete 20 | 21 | if (options.data === 'fail') { 22 | fail({ 23 | errMsg: 'some error', 24 | }) 25 | complete() 26 | } else if (options.data === 'timeout') { 27 | // ignore 28 | } else { 29 | success({ 30 | data: options.data, 31 | statusCode: 200, 32 | header: { 33 | 'Content-Type': 'application/javascript', 34 | }, 35 | }) 36 | complete() 37 | } 38 | } 39 | 40 | let readyStateChangeCount = 0 41 | let errorCount = 0 42 | let loadStartCount = 0 43 | let loadEndCount = 0 44 | let loadCount = 0 45 | let timeoutCount = 0 46 | const xhr = new XMLHttpRequest() 47 | 48 | xhr.onreadystatechange = function() { 49 | readyStateChangeCount++ 50 | } 51 | 52 | xhr.onerror = function() { 53 | errorCount++ 54 | } 55 | 56 | xhr.onloadstart = function() { 57 | loadStartCount++ 58 | } 59 | 60 | xhr.onloadend = function() { 61 | expect(loadStartCount).toBe(loadEndCount + 1) 62 | loadEndCount++ 63 | } 64 | 65 | xhr.onload = function() { 66 | expect(loadEndCount).toBe(loadCount + 1) 67 | loadCount++ 68 | } 69 | 70 | xhr.ontimeout = function() { 71 | timeoutCount++ 72 | } 73 | 74 | expect(xhr.readyState).toBe(XMLHttpRequest.UNSENT) 75 | expect(readyStateChangeCount).toBe(0) 76 | 77 | xhr.open('POST', 'https://a.b.c?a=12#haha') 78 | expect(xhr.readyState).toBe(XMLHttpRequest.OPENED) 79 | expect(readyStateChangeCount).toBe(1) 80 | 81 | xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded') 82 | xhr.responseType = 'text' 83 | xhr.timeout = 200 84 | 85 | // fail 86 | expect(errorCount).toBe(0) 87 | xhr.send('fail') 88 | expect(xhr.readyState).toBe(XMLHttpRequest.DONE) 89 | expect(readyStateChangeCount).toBe(2) 90 | expect(errorCount).toBe(1) 91 | 92 | // timeout 93 | expect(timeoutCount).toBe(0) 94 | xhr._readyState = XMLHttpRequest.OPENED // 利用私有字段调整 readyState,为了再次发送请求 95 | xhr.send('timeout') 96 | await new Promise((resolve, reject) => setTimeout(resolve, 300)) 97 | expect(xhr.readyState).toBe(XMLHttpRequest.DONE) 98 | expect(readyStateChangeCount).toBe(3) 99 | expect(timeoutCount).toBe(1) 100 | 101 | // success 102 | expect(loadStartCount).toBe(0) 103 | expect(loadEndCount).toBe(0) 104 | expect(loadCount).toBe(0) 105 | xhr._readyState = XMLHttpRequest.OPENED // 利用私有字段调整 readyState,为了再次发送请求 106 | xhr.send('success') 107 | expect(xhr.readyState).toBe(XMLHttpRequest.DONE) 108 | expect(readyStateChangeCount).toBe(6) 109 | expect(loadStartCount).toBe(1) 110 | expect(loadEndCount).toBe(1) 111 | expect(loadCount).toBe(1) 112 | expect(xhr.responseText).toBe('success') 113 | expect(xhr.response).toBe('success') 114 | }) 115 | -------------------------------------------------------------------------------- /test/adapter/window.test.js: -------------------------------------------------------------------------------- 1 | const mock = require('../mock') 2 | const load = require('../../src/template/adapter') 3 | const html = require('../../src/html') 4 | 5 | const Window = load('Window') 6 | const Document = load('Document') 7 | const tool = load('tool') 8 | const cache = load('cache') 9 | 10 | const nodeIdMap = {} 11 | const pageId = `p-${tool.getId()}` 12 | let document 13 | let window 14 | 15 | beforeAll(async () => { 16 | global.wx.getSystemInfoSync = function() { 17 | return { 18 | screenHeight: 300, 19 | screenWidth: 200, 20 | windowHeight: 280, 21 | windowWidth: 190, 22 | } 23 | } 24 | 25 | const pageInfo = await html.parse(mock.html) 26 | 27 | document = new Document(pageId, 'index', pageInfo.body, nodeIdMap) 28 | window = new Window(pageId, 'index') 29 | cache.init(pageId, { 30 | document, 31 | window, 32 | nodeIdMap, 33 | }) 34 | }) 35 | 36 | test('window: self', () => { 37 | expect(window.self).toBe(window) 38 | }) 39 | 40 | test('window: screen/innerHeight/innerWidth/outerHeight/outerWidth', () => { 41 | expect(window.screen.height).toBe(300) 42 | expect(window.screen.width).toBe(200) 43 | 44 | expect(window.outerHeight).toBe(300) 45 | expect(window.outerWidth).toBe(200) 46 | expect(window.innerHeight).toBe(280) 47 | expect(window.innerWidth).toBe(190) 48 | }) 49 | 50 | test('window: getComputedStyle', async () => { 51 | const node1 = document.getElementById('bb') 52 | const node2 = document.querySelector('footer') 53 | 54 | // 设置测试数据 55 | mock.setSelectorQueryRes([{ 56 | display: 'block', 57 | dataset: { 58 | privateNodeId: node1._$nodeId, 59 | privatePageId: pageId, 60 | }, 61 | }, { 62 | position: 'absolute', 63 | dataset: { 64 | privateNodeId: node2._$nodeId, 65 | privatePageId: pageId, 66 | }, 67 | }]) 68 | 69 | await window._$fetchWebviewInfo() // 拉取相关信息 70 | expect(window.getComputedStyle(node1).display).toBe('block') 71 | expect(window.getComputedStyle(node1).getPropertyValue('display')).toBe('block') 72 | expect(window.getComputedStyle(node1).position).toBe('') 73 | expect(window.getComputedStyle(node1).getPropertyValue('position')).toBe('') 74 | expect(window.getComputedStyle(node2).position).toBe('absolute') 75 | expect(window.getComputedStyle(node2).getPropertyValue('position')).toBe('absolute') 76 | expect(window.getComputedStyle(node2).display).toBe('') 77 | expect(window.getComputedStyle(node2).getPropertyValue('display')).toBe('') 78 | 79 | // 设置测试数据 80 | mock.setSelectorQueryRes([{ 81 | display: 'inline', 82 | dataset: { 83 | privateNodeId: node1._$nodeId, 84 | privatePageId: pageId, 85 | }, 86 | }, { 87 | position: 'relative', 88 | dataset: { 89 | privateNodeId: node2._$nodeId, 90 | privatePageId: pageId, 91 | }, 92 | }]) 93 | 94 | await window._$fetchWebviewInfo() // 拉取相关信息 95 | expect(window.getComputedStyle(node1).display).toBe('inline') 96 | expect(window.getComputedStyle(node1).getPropertyValue('display')).toBe('inline') 97 | expect(window.getComputedStyle(node1).position).toBe('') 98 | expect(window.getComputedStyle(node1).getPropertyValue('position')).toBe('') 99 | expect(window.getComputedStyle(node2).position).toBe('relative') 100 | expect(window.getComputedStyle(node2).getPropertyValue('position')).toBe('relative') 101 | expect(window.getComputedStyle(node2).display).toBe('') 102 | expect(window.getComputedStyle(node2).getPropertyValue('display')).toBe('') 103 | }) 104 | -------------------------------------------------------------------------------- /src/js/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const request = require('request-promise-native') 4 | 5 | const _ = require('../utils') 6 | const config = require('../config') 7 | const adjust = require('./adjust') 8 | 9 | module.exports = { 10 | /** 11 | * 生成脚本 12 | */ 13 | async generate(options) { 14 | const entry = options.entry 15 | const output = options.output 16 | const commonOutput = options.commonOutput 17 | const jsList = options.jsList 18 | const body = options.body 19 | const entryKey = options.entryKey 20 | const needCompress = !!options.compress.jsInH5 21 | const proxy = options.proxy 22 | const dirPath = path.dirname(entry) 23 | 24 | // 依赖 25 | const deps = [ 26 | 'const ast = require(\'./ast\');', // 页面的 ast 27 | 'const config = require(\'../../config\');', // 配置 28 | 'const initGlobalVar = require(\'../../common/js/init-global-var\');', // 全局变量初始化 29 | 'const initDocumentVar = require(\'../../common/js/init-document-var\');', // document 字段初始化 30 | 'const load = require(\'../../adapter/index\');\n', 31 | ['Window', 'Document', 'cache', 'tool', 'Event'].map(item => `const ${item} = load('${item}');`).join('\n') 32 | ].join('\n') 33 | 34 | let content = [ 35 | deps, 36 | `const pageKey = '${entryKey}';\nconst run = function(window, document) {`, 37 | `${config.indent}const global = null;` 38 | ] 39 | 40 | // 遍历页面中的静态 js 41 | for (const js of jsList) { 42 | if (js.type === 'inner') { 43 | const adjustContent = await adjust(js.content, needCompress) 44 | 45 | content.push(`${config.indent}/* inner script */\n${adjustContent}`) 46 | } else if (js.type === 'outer') { 47 | let jsPath = js.src 48 | let srcContent 49 | 50 | if (_.isRelative(jsPath)) { 51 | // 相对路径 52 | jsPath = path.join(dirPath, jsPath) 53 | srcContent = await _.readFile(jsPath) 54 | } else { 55 | // 其他 56 | const isFileExists = await _.checkFileExists(jsPath) 57 | 58 | if (isFileExists) { 59 | // 绝对路径 60 | srcContent = await _.readFile(jsPath) 61 | } else { 62 | // 网络 url 63 | srcContent = await request.get({url: jsPath, proxy}) 64 | } 65 | } 66 | 67 | if (srcContent) { 68 | // 写入 common 目录 69 | const extname = path.extname(jsPath) 70 | const filename = `${path.basename(jsPath, extname)}-${_.hash(srcContent)}.js` 71 | const adjustContent = await adjust(srcContent, needCompress) 72 | 73 | await _.writeFile(path.join(commonOutput, filename), [ 74 | 'module.exports=function(window,document){', 75 | 'var module=undefined;', 76 | 'var global=window;', 77 | `(function(){${adjustContent}}).call(window)`, // 保证 this 指向 window 78 | '};', 79 | ].join('')) 80 | content.push(`${config.indent}/* outer script: ${js.src} */\n${config.indent}require('../../common/js/${filename}')(window, document);`) 81 | } 82 | } 83 | } 84 | 85 | content.push('};') 86 | 87 | // 插入 Page 声明 88 | const pageTmpl = await _.readFile(path.join(__dirname, './page.js.tmpl')) 89 | content.push(pageTmpl) 90 | 91 | content = content.join('\n\n') 92 | 93 | // 输出页面 js 94 | await _.writeFile(output, content) 95 | 96 | // 输出页面 ast 97 | const astContent = needCompress ? JSON.stringify(body) : JSON.stringify(body, null, config.indent) 98 | await _.writeFile(path.join(path.dirname(output), './ast.js'), `module.exports = ${astContent};`) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/template/adapter/node/attribute.js: -------------------------------------------------------------------------------- 1 | const load = require('../index') 2 | 3 | const config = load('config') 4 | const cache = load('cache') 5 | 6 | const resFilter = config.resFilter || (src => src) 7 | 8 | class Attribute { 9 | constructor(element, onUpdate) { 10 | this._element = element 11 | this._doUpdate = onUpdate 12 | this._map = {} 13 | this._list = [] 14 | 15 | this.triggerUpdate() 16 | } 17 | 18 | /** 19 | * 属性列表,需要动态更新 20 | */ 21 | get list() { 22 | return this._list 23 | } 24 | 25 | /** 26 | * 设置属性 27 | */ 28 | set(name, value) { 29 | const element = this._element 30 | const map = this._map 31 | const pageKey = cache.getPageKey(element._$pageId) 32 | 33 | if (name === 'id') { 34 | map.id = value 35 | } else if (name === 'class') { 36 | element.className = value 37 | } else if (name === 'style') { 38 | element.style.cssText = value 39 | } else { 40 | if (name === 'src') { 41 | // 需要调整资源路径 42 | value = resFilter(value, pageKey) 43 | } 44 | 45 | map[name] = value 46 | 47 | // 其他字段的设置需要触发父组件更新 48 | this._doUpdate() 49 | } 50 | 51 | this.triggerUpdate() 52 | } 53 | 54 | /** 55 | * 取属性 56 | */ 57 | get(name) { 58 | const element = this._element 59 | const map = this._map 60 | 61 | if (name === 'class') { 62 | return element.className 63 | } else if (name === 'style') { 64 | return element.style.cssText 65 | } else { 66 | return map[name] || '' 67 | } 68 | } 69 | 70 | /** 71 | * 判断属性是否存在 72 | */ 73 | has(name) { 74 | const element = this._element 75 | const map = this._map 76 | 77 | if (name === 'id') { 78 | return !!element.id 79 | } else if (name === 'class') { 80 | return !!element.className 81 | } else if (name === 'style') { 82 | return !!element.style.cssText 83 | } else { 84 | return Object.prototype.hasOwnProperty.call(map, name) 85 | } 86 | } 87 | 88 | /** 89 | * 删除属性 90 | */ 91 | remove(name) { 92 | const element = this._element 93 | const map = this._map 94 | 95 | if (name === 'id') { 96 | element.id = '' 97 | } else if (name === 'class' || name === 'style') { 98 | this.set(name, '') 99 | } else { 100 | // 其他字段的设置需要触发父组件更新 101 | delete map[name] 102 | this._doUpdate() 103 | } 104 | 105 | this.triggerUpdate() 106 | } 107 | 108 | /** 109 | * 更新属性列表 110 | */ 111 | triggerUpdate() { 112 | const map = this._map 113 | const list = this._list 114 | 115 | // 清空旧的列表 116 | list.forEach(item => { 117 | delete list[item.name] 118 | }) 119 | delete list.class 120 | delete list.style 121 | list.length = 0 122 | 123 | // 添加新列表 124 | Object.keys(map).forEach(name => { 125 | if (name !== 'id') { 126 | const item = {name, value: map[name]} 127 | 128 | list.push(item) 129 | list[name] = item 130 | } 131 | }) 132 | 133 | const idValue = this.get('id') 134 | const classValue = this.get('class') 135 | const styleValue = this.get('style') 136 | if (idValue) { 137 | const item = {name: 'id', value: idValue} 138 | 139 | list.push(item) 140 | list.id = item 141 | } 142 | if (classValue) { 143 | const item = {name: 'class', value: classValue} 144 | 145 | list.push(item) 146 | list.class = item 147 | } 148 | if (styleValue) { 149 | const item = {name: 'style', value: styleValue} 150 | 151 | list.push(item) 152 | list.style = item 153 | } 154 | } 155 | } 156 | 157 | module.exports = Attribute 158 | -------------------------------------------------------------------------------- /src/template/adapter/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 使用加载器的方式,方便追加插件 3 | */ 4 | 5 | const defaultConfig = {} 6 | 7 | /** 8 | * 封装 native 方法的 toString 9 | */ 10 | function wrapNative(clazz) { 11 | if (typeof clazz === 'function' || typeof clazz === 'object') { 12 | Object.getOwnPropertyNames(clazz).forEach(key => { 13 | try { 14 | const value = clazz[key] 15 | if (typeof value === 'function') value.toString = () => `function ${value.name} { [native code] }` 16 | } catch (err) { 17 | // ignore 18 | } 19 | }) 20 | } 21 | 22 | if (typeof clazz.prototype === 'function' || typeof clazz.prototype === 'object') { 23 | Object.getOwnPropertyNames(clazz.prototype).forEach(key => { 24 | try { 25 | const value = clazz.prototype[key] 26 | if (typeof value === 'function') value.toString = () => `function ${value.name} { [native code] }` 27 | } catch (err) { 28 | // ignore 29 | } 30 | }) 31 | } 32 | 33 | return clazz 34 | } 35 | 36 | 37 | const MODULE_MAP = { 38 | // bom 相关 39 | Cookie: () => require('./bom/cookie'), 40 | History: () => require('./bom/history'), 41 | LocalStorage: () => require('./bom/local-storage'), 42 | Location: () => require('./bom/location'), 43 | Navigator: () => require('./bom/navigator'), 44 | Screen: () => require('./bom/screen'), 45 | SessionStorage: () => require('./bom/session-storage'), 46 | XMLHttpRequest: () => require('./bom/xml-http-request'), 47 | 48 | // 事件相关 49 | CustomEvent: () => require('./event/custom-event'), 50 | EventTarget: () => require('./event/event-target'), 51 | Event: () => require('./event/event'), 52 | 53 | // 节点相关 54 | A: () => require('./node/element/a'), 55 | Canvas: () => require('./node/element/canvas'), 56 | Image: () => require('./node/element/image'), 57 | Input: () => require('./node/element/input'), 58 | Attribute: () => require('./node/attribute'), 59 | ClassList: () => require('./node/class-list'), 60 | Comment: () => require('./node/comment'), 61 | Element: () => require('./node/element'), 62 | Node: () => require('./node/node'), 63 | styleList: () => require('./node/style-list'), 64 | Style: () => require('./node/style'), 65 | TextNode: () => require('./node/text-node'), 66 | 67 | // tree 相关 68 | parser: () => require('./tree/parser'), 69 | QuerySelector: () => require('./tree/query-selector'), 70 | tagMap: () => require('./tree/tag-map'), 71 | Tree: () => require('./tree/tree'), 72 | 73 | // 工具相关 74 | cache: () => require('./util/cache'), 75 | tool: () => require('./util/tool'), 76 | 77 | // 其他 78 | Document: () => require('./document'), 79 | Window: () => require('./window'), 80 | 81 | // 配置 82 | config() { 83 | let config 84 | 85 | try { 86 | // eslint-disable-next-line import/no-unresolved 87 | config = require('../config') 88 | } catch (err) { 89 | config = defaultConfig 90 | } 91 | 92 | if (typeof config.urlMap === 'string') { 93 | config.urlMap = {index: config.urlMap} 94 | } 95 | 96 | return config 97 | }, 98 | 99 | // 扩展 100 | extend() { 101 | try { 102 | // eslint-disable-next-line import/no-unresolved 103 | return require('../extend') 104 | } catch (err) { 105 | // ignore 106 | } 107 | } 108 | } 109 | const MODULE_CACHE = {} 110 | 111 | module.exports = function (moduleName) { 112 | const extend = MODULE_MAP.extend() 113 | 114 | if (!MODULE_CACHE[moduleName]) { 115 | const loader = MODULE_MAP[moduleName] 116 | let loadModule = loader ? loader() : null 117 | loadModule = typeof extend === 'function' ? extend(loadModule, moduleName) || loadModule : loadModule 118 | 119 | MODULE_CACHE[moduleName] = loadModule ? wrapNative(loadModule) : loadModule 120 | } 121 | 122 | return MODULE_CACHE[moduleName] 123 | } 124 | -------------------------------------------------------------------------------- /src/template/adapter/node/style.js: -------------------------------------------------------------------------------- 1 | const load = require('../index') 2 | 3 | const tool = load('tool') 4 | const config = load('config') 5 | const styleList = load('styleList') 6 | const cache = load('cache') 7 | 8 | const resFilter = config.resFilter || (src => src) 9 | const rem = typeof config.rem === 'number' && isFinite(config.rem) ? config.rem : 0 10 | 11 | /** 12 | * 解析样式串 13 | */ 14 | function parse(styleText, pageKey) { 15 | const rules = {} 16 | 17 | if (styleText) { 18 | styleText.split(';').forEach(rule => { 19 | rule = rule.trim() 20 | 21 | if (!rule) return 22 | 23 | const split = rule.indexOf(':') 24 | 25 | if (split === -1) return 26 | 27 | const name = tool.toCamel(rule.substr(0, split).trim()) 28 | let value = rule.substr(split + 1).trim() 29 | 30 | if (name === 'background' || name === 'backgroundImage') { 31 | // 需要调整资源路径 32 | value = tool.replaceStyleUrl(value, resFilter, pageKey) 33 | } 34 | 35 | if (rem) { 36 | // 需要调整 rem 37 | value = tool.replaceStyleRem(value, rem) 38 | } 39 | 40 | rules[name] = value 41 | }) 42 | } 43 | 44 | return rules 45 | } 46 | 47 | class Style { 48 | constructor(element, styleText = '', onUpdate) { 49 | this._element = element 50 | this._doUpdate = onUpdate || (() => {}) 51 | this._disableCheckUpdate = false // 是否禁止检查更新 52 | 53 | // 设置各个属性的 getter、setter 54 | const properties = {} 55 | 56 | styleList.forEach(name => { 57 | properties[name] = { 58 | get() { 59 | return this[`_${name}`] || '' 60 | }, 61 | set(value) { 62 | const oldValue = this[`_${name}`] 63 | 64 | this[`_${name}`] = value !== undefined ? '' + value : undefined 65 | 66 | if (oldValue !== value) this._checkUpdate() 67 | }, 68 | } 69 | }) 70 | Object.defineProperties(this, properties) 71 | 72 | // 解析样式 73 | const pageKey = cache.getPageKey(element._$pageId) 74 | const rules = parse(styleText, pageKey) 75 | 76 | this._disableCheckUpdate = true // 初始化不当做更新 77 | for (const name of styleList) { 78 | this[name] = rules[name] 79 | } 80 | this._disableCheckUpdate = false 81 | } 82 | 83 | /** 84 | * 检查更新 85 | */ 86 | _checkUpdate() { 87 | if (!this._disableCheckUpdate) { 88 | this._doUpdate() 89 | } 90 | } 91 | 92 | /** 93 | * 设置样式 94 | */ 95 | _$setStyle(style) { 96 | if (typeof style === 'string') { 97 | this.cssText = style 98 | } else if (typeof style === 'object') { 99 | this._disableCheckUpdate = true // 将每条规则的设置合并为一次更新 100 | for (const name of styleList) { 101 | this[name] = style[name] 102 | } 103 | this._disableCheckUpdate = false 104 | this._checkUpdate() 105 | } 106 | } 107 | 108 | /** 109 | * 对外属性和方法 110 | */ 111 | get cssText() { 112 | const joinText = styleList.filter(name => this[`_${name}`]).map(name => `${tool.toDash(name)}:${this['_' + name]}`).join(';').trim() 113 | return joinText ? `${joinText};` : '' 114 | } 115 | 116 | set cssText(styleText) { 117 | if (typeof styleText !== 'string') return 118 | 119 | styleText = styleText.replace('"', '\'') 120 | 121 | // 解析样式 122 | const pageKey = cache.getPageKey(this._element._$pageId) 123 | const rules = parse(styleText, pageKey) 124 | 125 | this._disableCheckUpdate = true // 将每条规则的设置合并为一次更新 126 | for (const name of styleList) { 127 | this[name] = rules[name] 128 | } 129 | this._disableCheckUpdate = false 130 | this._checkUpdate() 131 | } 132 | 133 | getPropertyValue(name) { 134 | if (typeof name !== 'string') return '' 135 | 136 | name = tool.toCamel(name) 137 | return this[name] || '' 138 | } 139 | } 140 | 141 | module.exports = Style 142 | -------------------------------------------------------------------------------- /src/template/adapter/event/event.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 检查节点间的关系 3 | */ 4 | function checkRelation(node1, node2) { 5 | if (node1 === node2) return true 6 | 7 | while (node1) { 8 | if (node1 === node2) return true 9 | 10 | node1 = node1.parentNode 11 | } 12 | 13 | return false 14 | } 15 | 16 | class Event { 17 | constructor(options) { 18 | this._name = options.name.toLowerCase() 19 | this._target = options.target 20 | this._timeStamp = options.timeStamp || Date.now() 21 | this._currentTarget = options.currentTarget || options.target 22 | this._eventPhase = options.eventPhase || Event.NONE 23 | this._detail = options.detail || null 24 | this._canBubble = true 25 | this._bubbles = options.bubbles || false 26 | this._touches = null 27 | this._targetTouches = null 28 | this._changedTouches = null 29 | this._cancelable = false 30 | 31 | // 补充字段 32 | const extra = options._$extra 33 | if (extra) { 34 | Object.keys(extra).forEach(key => { 35 | this[key] = extra[key] 36 | }) 37 | } 38 | 39 | // 处理 touches 40 | if (options.touches && options.touches.length) { 41 | this._touches = options.touches.map(touch => { 42 | touch.target = options.target 43 | return touch 44 | }) 45 | 46 | this._$checkTargetTouches() 47 | } 48 | 49 | // 处理 changedTouches 50 | if (options.changedTouches && options.changedTouches.length) { 51 | this._changedTouches = options.changedTouches.map(touch => { 52 | touch.target = options.target 53 | return touch 54 | }) 55 | } 56 | } 57 | 58 | /** 59 | * 返回事件时否还可以冒泡 60 | */ 61 | get _$canBubble() { 62 | return this._canBubble 63 | } 64 | 65 | /** 66 | * 设置 target 67 | */ 68 | _$setTarget(target) { 69 | this._target = target 70 | } 71 | 72 | /** 73 | * 设置 currentTarget 74 | */ 75 | _$setCurrentTarget(currentTarget) { 76 | this._currentTarget = currentTarget 77 | this._$checkTargetTouches() 78 | } 79 | 80 | /** 81 | * 设置事件所处阶段 82 | */ 83 | _$setEventPhase(eventPhase) { 84 | this._eventPhase = eventPhase 85 | } 86 | 87 | /** 88 | * 检查 targetTouches 89 | */ 90 | _$checkTargetTouches() { 91 | if (this._touches && this._touches.length) { 92 | this._targetTouches = this._touches.filter(touch => checkRelation(touch.target, this._currentTarget)) 93 | } 94 | } 95 | 96 | /** 97 | * 对外属性和方法 98 | */ 99 | get bubbles() { 100 | return this._bubbles 101 | } 102 | 103 | get cancelable() { 104 | return this._cancelable 105 | } 106 | 107 | get target() { 108 | return this._target 109 | } 110 | 111 | get currentTarget() { 112 | return this._currentTarget 113 | } 114 | 115 | get eventPhase() { 116 | return this._eventPhase 117 | } 118 | 119 | get type() { 120 | return this._name 121 | } 122 | 123 | get timeStamp() { 124 | return this._timeStamp 125 | } 126 | 127 | get touches() { 128 | return this._touches 129 | } 130 | 131 | get targetTouches() { 132 | return this._targetTouches 133 | } 134 | 135 | get changedTouches() { 136 | return this._changedTouches 137 | } 138 | 139 | get detail() { 140 | return this._detail 141 | } 142 | 143 | preventDefault() { 144 | // 目前仅支持 a 标签的点击阻止 145 | this._cancelable = true 146 | } 147 | 148 | stopPropagation() { 149 | if (this.eventPhase === Event.NONE) return 150 | 151 | this._canBubble = false 152 | } 153 | 154 | initEvent(name = '', bubbles) { 155 | if (typeof name !== 'string') return 156 | 157 | this._name = name.toLowerCase() 158 | this._bubbles = bubbles === undefined ? this._bubbles : !!bubbles 159 | } 160 | } 161 | 162 | // 静态属性 163 | Event.NONE = 0 164 | Event.CAPTURING_PHASE = 1 165 | Event.AT_TARGET = 2 166 | Event.BUBBLING_PHASE = 3 167 | 168 | module.exports = Event 169 | -------------------------------------------------------------------------------- /src/template/adapter/node/element/image.js: -------------------------------------------------------------------------------- 1 | const load = require('../../index') 2 | 3 | const Element = load('Element') 4 | const Event = load('Event') 5 | const tool = load('tool') 6 | const config = load('config') 7 | 8 | const resFilter = config.resFilter || (src => src) 9 | 10 | class Image extends Element { 11 | constructor(options, tree) { 12 | const width = options.width 13 | const height = options.height 14 | 15 | if (typeof width === 'number' && width >= 0) options.attrs.width = width 16 | if (typeof height === 'number' && height >= 0) options.attrs.height = height 17 | 18 | super(options, tree) 19 | 20 | this._naturalWidth = 0 21 | this._naturalHeight = 0 22 | 23 | this._initRect() 24 | } 25 | 26 | /** 27 | * 初始化长宽 28 | */ 29 | _initRect() { 30 | const width = parseInt(this.getAttribute('width'), 10) 31 | const height = parseInt(this.getAttribute('height'), 10) 32 | 33 | if (typeof width === 'number' && width >= 0) this._style.width = `${width}px` 34 | if (typeof height === 'number' && height >= 0) this._style.height = `${height}px` 35 | } 36 | 37 | /** 38 | * 用于图片加载回调后设置 39 | */ 40 | set _$width(value) { 41 | const width = this.getAttribute('width') 42 | 43 | if (!width && width !== 0) this.setAttribute('width', value) 44 | } 45 | 46 | /** 47 | * 用于图片加载回调后设置 48 | */ 49 | set _$height(value) { 50 | const height = this.getAttribute('width') 51 | 52 | if (!height && height !== 0) this.setAttribute('height', value) 53 | } 54 | 55 | /** 56 | * 对外属性和方法 57 | */ 58 | get src() { 59 | return this.getAttribute('src') || '' 60 | } 61 | 62 | set src(value) { 63 | if (!value || typeof value !== 'string') return 64 | 65 | value = tool.replaceStyleUrl(value, resFilter, this.ownerDocument._$pageKey) 66 | 67 | this.setAttribute('src', value) 68 | 69 | setTimeout(() => { 70 | wx.getImageInfo({ 71 | src: this.src, 72 | success: res => { 73 | // 加载成功,调整图片的宽高 74 | const width = parseInt(this.getAttribute('width'), 10) 75 | const height = parseInt(this.getAttribute('height'), 10) 76 | 77 | if (typeof width !== 'number' || !isFinite(width) || width < 0) this.setAttribute('width', res.width) 78 | if (typeof height !== 'number' || !isFinite(height) || height < 0) this.setAttribute('height', res.height) 79 | 80 | this._naturalWidth = res.width 81 | this._naturalHeight = res.height 82 | 83 | this._initRect() 84 | 85 | // 触发 load 事件 86 | this._$trigger('load', { 87 | event: new Event({ 88 | name: 'load', 89 | target: this, 90 | eventPhase: Event.AT_TARGET 91 | }), 92 | currentTarget: this, 93 | }) 94 | }, 95 | fail: () => { 96 | // 加载失败,触发 error 事件 97 | this._$trigger('error', { 98 | event: new Event({ 99 | name: 'error', 100 | target: this, 101 | eventPhase: Event.AT_TARGET 102 | }), 103 | currentTarget: this, 104 | }) 105 | }, 106 | }) 107 | }, 0) 108 | } 109 | 110 | get width() { 111 | return +this.getAttribute('width') || 0 112 | } 113 | 114 | set width(value) { 115 | if (typeof value !== 'number' || !isFinite(value) || value < 0) return 116 | 117 | this.setAttribute('width', value) 118 | this._initRect() 119 | } 120 | 121 | get height() { 122 | return +this.getAttribute('height') || 0 123 | } 124 | 125 | set height(value) { 126 | if (typeof value !== 'number' || !isFinite(value) || value < 0) return 127 | 128 | this.setAttribute('height', value) 129 | this._initRect() 130 | } 131 | 132 | get naturalWidth() { 133 | return this._naturalWidth 134 | } 135 | 136 | get naturalHeight() { 137 | return this._naturalHeight 138 | } 139 | } 140 | 141 | module.exports = Image 142 | -------------------------------------------------------------------------------- /test/adapter/bom/cookie.test.js: -------------------------------------------------------------------------------- 1 | const mock = require('../../mock') 2 | const load = require('../../../src/template/adapter') 3 | 4 | const Cookie = load('Cookie') 5 | 6 | test('cookie', async () => { 7 | const cookie = new Cookie() 8 | const url1 = 'http://sub.host.com/p/a/t/h?query=string#hash' 9 | const url2 = 'http://xxx.sub.host.com/p/a/t/h?query=string#hash' 10 | const url3 = 'http://sub2.host.com/p/a/t/h?query=string#hash' 11 | const url4 = 'https://sub.host.com/p/a/t/h?query=string#hash' 12 | 13 | // key-value 14 | cookie.setCookie('aaa=bbb', url1) 15 | expect(cookie.getCookie(url1)).toBe('aaa=bbb') 16 | cookie.setCookie('ccc=ddd\nasdf', url1) 17 | cookie.setCookie('eee=fff;asdf', url1) 18 | cookie.setCookie('ggg=;asdf', url1) 19 | cookie.setCookie('hhh\n=;asdf', url1) 20 | expect(cookie.getCookie(url1)).toBe('aaa=bbb; ccc=ddd; eee=fff; ggg=') 21 | cookie.setCookie('aaa=abc', url1) 22 | cookie.setCookie('ccc=cba\nasdf', url1) 23 | expect(cookie.getCookie(url1)).toBe('aaa=abc; ccc=cba; eee=fff; ggg=') 24 | 25 | // maxAge 26 | cookie.setCookie('aaa=bbb; max-age=1', url1) 27 | await new Promise((resolve, reject) => setTimeout(resolve, 2000)) 28 | expect(cookie.getCookie(url1)).toBe('ccc=cba; eee=fff; ggg=') 29 | cookie.setCookie('ccc=ddd; max-age=-10', url1) 30 | expect(cookie.getCookie(url1)).toBe('eee=fff; ggg=') 31 | cookie.setCookie('eee=fff; max-age=', url1) 32 | expect(cookie.getCookie(url1)).toBe('eee=fff; ggg=') 33 | cookie.setCookie('eee=fff; max-age=abc', url1) 34 | expect(cookie.getCookie(url1)).toBe('eee=fff; ggg=') 35 | 36 | // expires 37 | cookie.setCookie(`eee=fff; expires=${(new Date(Date.now())).toUTCString()}`, url1) 38 | expect(cookie.getCookie(url1)).toBe('ggg=') 39 | cookie.setCookie(`ggg=fff; expires=${(new Date(Date.now() + 1000)).toUTCString()}`, url1) 40 | expect(cookie.getCookie(url1)).toBe('ggg=fff') 41 | await new Promise((resolve, reject) => setTimeout(resolve, 2000)) 42 | expect(cookie.getCookie(url1)).toBe('') 43 | 44 | // max-age 优先于 expires 45 | cookie.setCookie(`aaa=bbb; max-age=1000; expires=${(new Date(Date.now())).toUTCString()}`, url1) 46 | expect(cookie.getCookie(url1)).toBe('aaa=bbb') 47 | cookie.setCookie(`aaa=bbb; max-age=0; expires=${(new Date(Date.now() + 1000 * 1000)).toUTCString()}`, url1) 48 | expect(cookie.getCookie(url1)).toBe('') 49 | 50 | // domain 51 | cookie.setCookie('aaa=bbb; domain=host.com', url1) 52 | cookie.setCookie('abc=cba; domain=sub.host.com', url1) 53 | expect(cookie.getCookie(url1)).toBe('aaa=bbb; abc=cba') 54 | cookie.setCookie('ccc=ddd; domain=xxx.sub.host.com', url1) // 不符合 domain,不写入 cookie 55 | expect(cookie.getCookie(url1)).toBe('aaa=bbb; abc=cba') 56 | cookie.setCookie('eee=fff; domain=xxx.sub.host.com', url2) 57 | cookie.setCookie('ggg=; domain=sub2.host.com', url3) 58 | expect(cookie.getCookie(url1)).toBe('aaa=bbb; abc=cba') 59 | expect(cookie.getCookie(url2)).toBe('aaa=bbb; abc=cba; eee=fff') 60 | expect(cookie.getCookie(url3)).toBe('aaa=bbb; ggg=') 61 | 62 | // path 63 | cookie.setCookie('eee=fff; path=/p/a/t/x', url1) 64 | expect(cookie.getCookie(url1)).toBe('aaa=bbb; abc=cba') 65 | cookie.setCookie('ggg=hhh; path=/p/a/t', url1) 66 | expect(cookie.getCookie(url1)).toBe('aaa=bbb; abc=cba; ggg=hhh') 67 | cookie.setCookie('iii=jjj; path=/p/a/x', url1) 68 | expect(cookie.getCookie(url1)).toBe('aaa=bbb; abc=cba; ggg=hhh') 69 | cookie.setCookie('kkk=lll; path=/p/a', url1) 70 | expect(cookie.getCookie(url1)).toBe('aaa=bbb; abc=cba; ggg=hhh; kkk=lll') 71 | 72 | // secure 73 | cookie.setCookie('mmm=nnn; secure', url1) 74 | expect(cookie.getCookie(url1)).toBe('aaa=bbb; abc=cba; ggg=hhh; kkk=lll') 75 | expect(cookie.getCookie(url4)).toBe('aaa=bbb; abc=cba; ggg=hhh; kkk=lll; mmm=nnn') 76 | 77 | // httpOnly 78 | cookie.setCookie('ooo=ppp; httpOnly', url1) 79 | expect(cookie.getCookie(url1)).toBe('aaa=bbb; abc=cba; ggg=hhh; kkk=lll; ooo=ppp') 80 | expect(cookie.getCookie(url4)).toBe('aaa=bbb; abc=cba; ggg=hhh; kkk=lll; mmm=nnn') 81 | }) 82 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const crypto = require('crypto') 4 | 5 | /** 6 | * 异步函数封装成 promise 7 | */ 8 | function wrap(func, scope) { 9 | return function (...args) { 10 | if (args.length) { 11 | const temp = args.pop() 12 | if (typeof temp !== 'function') { 13 | args.push(temp) 14 | } 15 | } 16 | 17 | return new Promise(function (resolve, reject) { 18 | args.push(function (err, data) { 19 | if (err) reject(err) 20 | else resolve(data) 21 | }) 22 | 23 | func.apply((scope || null), args) 24 | }) 25 | } 26 | } 27 | 28 | const accessSync = wrap(fs.access) 29 | const statSync = wrap(fs.stat) 30 | const renameSync = wrap(fs.rename) 31 | const mkdirSync = wrap(fs.mkdir) 32 | const readFileSync = wrap(fs.readFile) 33 | const writeFileSync = wrap(fs.writeFile) 34 | const readdirSync = wrap(fs.readdir) 35 | 36 | /** 37 | * 检查文件是否存在 38 | */ 39 | async function checkFileExists(filePath) { 40 | try { 41 | await accessSync(filePath) 42 | return true 43 | } catch (err) { 44 | return false 45 | } 46 | } 47 | 48 | /** 49 | * 检查路径是否是目录 50 | */ 51 | async function isDirectory(filePath) { 52 | try { 53 | const stat = await statSync(filePath) 54 | 55 | return stat && stat.isDirectory() 56 | } catch (err) { 57 | return false 58 | } 59 | } 60 | 61 | /** 62 | * 递归创建目录 63 | */ 64 | async function recursiveMkdir(dirPath) { 65 | const prevDirPath = path.dirname(dirPath) 66 | try { 67 | await accessSync(prevDirPath) 68 | } catch (err) { 69 | // 上一级目录不存在 70 | await recursiveMkdir(prevDirPath) 71 | } 72 | 73 | try { 74 | await accessSync(dirPath) 75 | 76 | const isDir = await isDirectory(dirPath) 77 | 78 | if (!isDir) { 79 | // 存在同名非目录文件 80 | await renameSync(dirPath, `${dirPath}.bak`) // rename to a file with the suffix ending in '.bak' 81 | await mkdirSync(dirPath) 82 | } 83 | } catch (err) { 84 | // 目录不存在 85 | await mkdirSync(dirPath) 86 | } 87 | } 88 | 89 | /** 90 | * 读文件 91 | */ 92 | async function readFile(filePath) { 93 | try { 94 | return await readFileSync(filePath, 'utf8') 95 | } catch (err) { 96 | // eslint-disable-next-line no-console 97 | return console.error(err) 98 | } 99 | } 100 | 101 | /** 102 | * 写文件 103 | */ 104 | async function writeFile(filePath, data) { 105 | try { 106 | await recursiveMkdir(path.dirname(filePath)) 107 | return await writeFileSync(filePath, data, 'utf8') 108 | } catch (err) { 109 | // eslint-disable-next-line no-console 110 | return console.error(err) 111 | } 112 | } 113 | 114 | /** 115 | * 拷贝文件 116 | */ 117 | async function copyFile(srcPath, distPath) { 118 | await recursiveMkdir(path.dirname(distPath)) 119 | 120 | return new Promise((resolve, reject) => { 121 | fs.createReadStream(srcPath).pipe(fs.createWriteStream(distPath)) 122 | .on('finish', () => resolve()) 123 | .on('error', err => reject(err)) 124 | }) 125 | } 126 | 127 | /** 128 | * 拷贝目录 129 | */ 130 | async function copyDirectory(srcPath, distPath) { 131 | await recursiveMkdir(distPath) 132 | 133 | const fileList = await readdirSync(srcPath) 134 | 135 | for (const fileName of fileList) { 136 | const filePath = path.join(srcPath, fileName) 137 | const outputPath = path.join(distPath, fileName) 138 | const isDir = await isDirectory(filePath) 139 | 140 | if (isDir) { 141 | await copyDirectory(filePath, outputPath) 142 | } else { 143 | await copyFile(filePath, outputPath) 144 | } 145 | } 146 | } 147 | 148 | /** 149 | * 判断是否是相对路径 150 | */ 151 | function isRelative(filePath) { 152 | return filePath.indexOf('./') === 0 || filePath.indexOf('../') === 0 || (filePath[0] !== '/' && filePath.indexOf('://') === -1) 153 | } 154 | 155 | /** 156 | * hash,采用 md5 157 | */ 158 | function hash(content) { 159 | return crypto.createHash('md5').update(content).digest('hex') 160 | } 161 | 162 | module.exports = { 163 | checkFileExists, 164 | isDirectory, 165 | recursiveMkdir, 166 | readFile, 167 | writeFile, 168 | copyFile, 169 | copyDirectory, 170 | isRelative, 171 | hash, 172 | } 173 | -------------------------------------------------------------------------------- /test/adapter/node/image.test.js: -------------------------------------------------------------------------------- 1 | const mock = require('../../mock') 2 | const load = require('../../../src/template/adapter') 3 | const html = require('../../../src/html') 4 | 5 | const Window = load('Window') 6 | const Document = load('Document') 7 | const cache = load('cache') 8 | const tool = load('tool') 9 | 10 | const nodeIdMap = {} 11 | const pageId = `p-${tool.getId()}` 12 | let document 13 | let window 14 | 15 | beforeAll(async () => { 16 | const pageInfo = await html.parse(mock.html) 17 | 18 | document = new Document(pageId, 'index', pageInfo.body, nodeIdMap) 19 | window = new Window(pageId, 'index') 20 | cache.init(pageId, { 21 | document, 22 | window, 23 | nodeIdMap, 24 | }) 25 | }) 26 | 27 | test('image', async () => { 28 | let isSuccess = false 29 | global.wx.getImageInfo = function(options) { 30 | setTimeout(() => { 31 | if (isSuccess) { 32 | options.success({ 33 | width: 100, 34 | height: 88, 35 | }) 36 | } else { 37 | options.fail() 38 | } 39 | }, 10) 40 | } 41 | 42 | const Image = window.Image 43 | 44 | // 带宽高 45 | const image1 = new Image(50, 60) 46 | let image1Count = 0 47 | let expectImage1Count = 0 48 | image1.onload = function() { 49 | expect(image1.width).toBe(50) 50 | expect(image1.height).toBe(60) 51 | expect(image1.naturalWidth).toBe(100) 52 | expect(image1.naturalHeight).toBe(88) 53 | expect(image1.src).toBe('https://c.b.a') 54 | expect(image1Count).toBe(expectImage1Count) 55 | } 56 | image1.onerror = function() { 57 | expect(image1.width).toBe(50) 58 | expect(image1.height).toBe(60) 59 | expect(image1.naturalWidth).toBe(0) 60 | expect(image1.naturalHeight).toBe(0) 61 | expect(image1.src).toBe('https://a.b.c') 62 | expect(image1Count).toBe(expectImage1Count) 63 | } 64 | expect(image1.width).toBe(50) 65 | expect(image1.height).toBe(60) 66 | expect(image1.naturalWidth).toBe(0) 67 | expect(image1.naturalHeight).toBe(0) 68 | expect(image1.src).toBe('') 69 | image1Count++ 70 | expectImage1Count = 1 71 | image1.src = 'https://a.b.c' 72 | await new Promise((resolve, reject) => setTimeout(resolve, 20)) 73 | isSuccess = true // 下一次请求为成功 74 | image1Count++ 75 | expectImage1Count = 2 76 | image1.src = 'https://c.b.a' 77 | await new Promise((resolve, reject) => setTimeout(resolve, 20)) 78 | image1Count++ 79 | expectImage1Count = 3 80 | image1.src = 'https://c.b.a' 81 | await new Promise((resolve, reject) => setTimeout(resolve, 20)) 82 | 83 | // 不带宽高 84 | isSuccess = false 85 | const image2 = new Image() 86 | let image2Count = 0 87 | let expectImage2Count = 0 88 | image2.onload = function() { 89 | expect(image2.width).toBe(100) 90 | expect(image2.height).toBe(88) 91 | expect(image2.naturalWidth).toBe(100) 92 | expect(image2.naturalHeight).toBe(88) 93 | expect(image2.src).toBe('https://f.e.d') 94 | expect(image2Count).toBe(expectImage2Count) 95 | } 96 | image2.onerror = function() { 97 | expect(image2.width).toBe(0) 98 | expect(image2.height).toBe(0) 99 | expect(image2.naturalWidth).toBe(0) 100 | expect(image2.naturalHeight).toBe(0) 101 | expect(image2.src).toBe('https://d.e.f') 102 | expect(image2Count).toBe(expectImage2Count) 103 | } 104 | expect(image2.width).toBe(0) 105 | expect(image2.height).toBe(0) 106 | expect(image2.naturalWidth).toBe(0) 107 | expect(image2.naturalHeight).toBe(0) 108 | expect(image2.src).toBe('') 109 | image2Count++ 110 | expectImage2Count = 1 111 | image2.src = 'https://d.e.f' 112 | await new Promise((resolve, reject) => setTimeout(resolve, 20)) 113 | isSuccess = true // 下一次请求为成功 114 | image2Count++ 115 | expectImage2Count = 2 116 | image2.src = 'https://f.e.d' 117 | await new Promise((resolve, reject) => setTimeout(resolve, 20)) 118 | image2Count++ 119 | expectImage2Count = 3 120 | image2.src = 'https://f.e.d' 121 | await new Promise((resolve, reject) => setTimeout(resolve, 20)) 122 | }) 123 | -------------------------------------------------------------------------------- /src/css/adjust.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 调整 css 3 | */ 4 | 5 | const postcss = require('postcss') 6 | const cssnano = require('cssnano') 7 | 8 | const load = require('../template/adapter') 9 | const config = require('../config') 10 | 11 | const tagMap = load('tagMap') 12 | const tool = load('tool') 13 | 14 | const tagList = Object.keys(tagMap) 15 | const replaceRegexp = new RegExp(`(\\W|\\b)(${tagList.join('|')})(\\W|\\b)`, 'ig') 16 | const prefixRegexp = /[a-zA-Z0-9.#_-]/ 17 | const suffixRegexp = /[a-zA-Z0-9_-]/ 18 | const indentSpaceRegexp = /[ ]*$/ 19 | 20 | /** 21 | * 替换标签名 22 | */ 23 | const replaceTagNamePlugin = postcss.plugin('replaceTagName', () => root => { 24 | root.walk(child => { 25 | if (child.type === 'rule') { 26 | const selectors = [] 27 | 28 | child.selectors.forEach(selector => { 29 | // 处理标签名选择器 30 | selector = selector.replace(replaceRegexp, (all, $1, tagName, $2) => { 31 | if (prefixRegexp.test($1) || suffixRegexp.test($2)) { 32 | // 非标签选择器 33 | return all 34 | } 35 | 36 | tagName = tagName.toLowerCase() 37 | const compName = tagMap[tagName] 38 | 39 | if (tagName === 'html') { 40 | // 页面单独处理 41 | return `${$1}page${$2}` 42 | } else if (compName && compName !== 'element') { 43 | // 使用组件名的节点 44 | return `${$1}${compName}${$2}` 45 | } else { 46 | // 其他用原本的标签名 47 | return `${$1}${tagName}${$2}` 48 | } 49 | }) 50 | 51 | // 处理 * 号选择器 52 | selector = selector.replace(/(.*)\*(.*)/g, (all, $1, $2) => { 53 | tagList.forEach(tagName => { 54 | const compName = tagMap[tagName] 55 | 56 | if (!compName) return 57 | if (compName !== 'element') { 58 | selectors.push(`${$1}${compName}${$2}`) 59 | } else { 60 | selectors.push(`${$1}${tagName}${$2}`) 61 | } 62 | }) 63 | 64 | selectors.push(`${$1}html${$2}`) 65 | selectors.push(`${$1}page${$2}`) 66 | 67 | return '' 68 | }) 69 | 70 | if (selector.trim()) selectors.push(selector) 71 | }) 72 | 73 | child.selectors = selectors 74 | } 75 | }) 76 | }) 77 | 78 | /** 79 | * 处理缩进 80 | */ 81 | const formatPlugin = postcss.plugin('formatPlugin', () => root => { 82 | root.walk(child => { 83 | if (child.raws.before !== undefined) { 84 | // 处理缩进 85 | child.raws.before = child.raws.before.replace(indentSpaceRegexp, () => { 86 | const parent = child.parent 87 | 88 | if (parent && parent.raws.before !== undefined) { 89 | return `${parent.raws.before.match(indentSpaceRegexp)[0]}${config.indent}` 90 | } 91 | 92 | return '' 93 | }) 94 | 95 | if (child.raws.after !== undefined) { 96 | child.raws.after = child.raws.after.replace(indentSpaceRegexp, () => child.raws.before.match(indentSpaceRegexp)[0]) 97 | } 98 | } 99 | }) 100 | }) 101 | 102 | /** 103 | * 调整图片链接 104 | */ 105 | const replaceImagePlugin = function (resFilter, entryKey) { 106 | return postcss.plugin('replaceImagePlugin', () => root => { 107 | root.walk(child => { 108 | if (child.type === 'decl' && (child.prop === 'background' || child.prop === 'background-image' || child.prop === 'src')) { 109 | child.value = tool.replaceStyleUrl(child.value, resFilter, entryKey) 110 | } 111 | }) 112 | }) 113 | } 114 | 115 | /** 116 | * 调整 rem 117 | */ 118 | const adjustRemPlugin = function (rem) { 119 | return postcss.plugin('adjustRemPlugin', () => root => { 120 | root.walk(child => { 121 | if (child.type === 'decl' && rem) { 122 | child.value = tool.replaceStyleRem(child.value, rem) 123 | } 124 | }) 125 | }) 126 | } 127 | 128 | module.exports = async function (code, config = {}) { 129 | const resFilter = typeof config.resFilter === 'function' ? config.resFilter : (src => src) 130 | const rem = typeof config.rem === 'number' && isFinite(config.rem) ? config.rem : 0 131 | const entryKey = config.entryKey 132 | const needCompress = config.needCompress 133 | const pluginList = [replaceTagNamePlugin, formatPlugin, replaceImagePlugin(resFilter, entryKey), adjustRemPlugin(rem)] 134 | 135 | if (needCompress) { 136 | // 压缩 137 | pluginList.push(cssnano({ 138 | preset: ['default', { 139 | discardComments: { 140 | removeAll: true, 141 | }, 142 | }] 143 | })) 144 | } 145 | 146 | code = await postcss(pluginList).process(code, { 147 | from: undefined, // 主要是不想看到那个 warning 148 | map: null, 149 | }) 150 | 151 | return code.css 152 | } 153 | -------------------------------------------------------------------------------- /test/demo/demo05/css/index.css: -------------------------------------------------------------------------------- 1 | *{ 2 | padding: 0; 3 | margin: 0; 4 | } 5 | 6 | body{ 7 | height: 100vh; 8 | position: relative; 9 | } 10 | 11 | audio{ 12 | width: 0; 13 | height: 0; 14 | position: absolute; 15 | 16 | } 17 | 18 | .music-btn{ 19 | position: fixed; 20 | width: .93rem; 21 | height: .73rem; 22 | top: 2%; 23 | right: 3%; 24 | z-index: 99; 25 | } 26 | 27 | 28 | 29 | /* first part */ 30 | .first-part{ 31 | width: 100%; 32 | height: 100%; 33 | background-image: url('../imgs/index.png'); 34 | background-size: 100% 100%; 35 | } 36 | 37 | /* button group */ 38 | .first-part .btn_group{ 39 | width: 100%; 40 | list-style: none; 41 | position: absolute; 42 | height: auto; 43 | bottom: 16%; 44 | } 45 | 46 | .first-part .btn_group .btn{ 47 | float: left; 48 | width: 40%; 49 | position: relative; 50 | margin: 5%; 51 | } 52 | 53 | .first-part .btn_group .btn img{ 54 | width: 100%; 55 | } 56 | 57 | .first-part .gamerule-part{ 58 | display: none; 59 | position: absolute; 60 | height: 100%; 61 | width: 100%; 62 | z-index: 5; 63 | background-color: rgba(0, 0, 0, 0.7); 64 | } 65 | 66 | .gamerule-part .rule{ 67 | width: 6.56rem; 68 | height: 7.29rem; 69 | margin: 1rem auto 0; 70 | display: block; 71 | } 72 | 73 | .gamerule-part .btn{ 74 | width: 40%; 75 | position: relative; 76 | margin: 20px auto 0; 77 | } 78 | 79 | .gamerule-part .btn img{ 80 | width: 100%; 81 | } 82 | 83 | /* game part */ 84 | 85 | .game-part { 86 | height: 100vh; 87 | background-image: url(../imgs/background.png); 88 | background-size: 100% 100%; 89 | display: none; 90 | position: relative; 91 | } 92 | 93 | .second-part{ 94 | width: 100%; 95 | position: relative; 96 | height: 100%; 97 | } 98 | 99 | .second-part .game-loading{ 100 | position: absolute; 101 | width: 100%; 102 | height: 100%; 103 | top: 0; 104 | left: 0; 105 | background-color: rgba(0, 0, 0, 0.7); 106 | z-index: 10; 107 | } 108 | 109 | .second-part .game-loading img{ 110 | width: 3.24rem; 111 | height: 3.24rem; 112 | position: relative; 113 | display: block; 114 | top: 50%; 115 | margin: -1.62rem auto 0; 116 | } 117 | 118 | .second-part header { 119 | width: 100%; 120 | height: 25%; 121 | position: relative; 122 | } 123 | 124 | .second-part header .source-image { 125 | width: 80px; 126 | height: 80px; 127 | float: left; 128 | margin-top: 10%; 129 | margin-left: 8%; 130 | border: 1px solid #ca4174; 131 | box-shadow: 0 0 10px black; 132 | } 133 | 134 | .second-part header .user-date { 135 | height: 100%; 136 | float: right; 137 | width: 50%; 138 | position: relative; 139 | } 140 | 141 | .second-part header .user-date .guanqia { 142 | height: 2.64rem; 143 | width: 4.72rem; 144 | position: relative; 145 | margin-top: 10%; 146 | margin-right: 5%; 147 | float: right; 148 | } 149 | 150 | .second-part header .user-date .time{ 151 | font-size: 22px; 152 | color: #ca4174; 153 | position: absolute; 154 | top: 0.9rem; 155 | left: 1.2rem; 156 | letter-spacing: 5px; 157 | text-shadow: 0 0 15px black; 158 | } 159 | 160 | .second-part canvas { 161 | width: 6.5rem !important; 162 | height: 6.5rem !important; 163 | border: 2px solid #ca4174; 164 | display: block; 165 | margin: 0.5rem auto 0; 166 | box-shadow: 0 0 10px black; 167 | } 168 | 169 | .game-over{ 170 | padding-top: 1px; 171 | display: none; 172 | height: 100%; 173 | position: relative; 174 | background-color: rgba(0, 0, 0, 0.7); 175 | } 176 | 177 | .game-over .lose{ 178 | width: 6.48rem; 179 | height: 5.58rem; 180 | display: block; 181 | margin: 2rem auto 0; 182 | } 183 | 184 | .game-over ul{ 185 | list-style: none; 186 | width: 100%; 187 | margin: .2rem auto; 188 | } 189 | 190 | .game-over ul li{ 191 | width: 50%; 192 | height: auto; 193 | float: left; 194 | text-align: center; 195 | } 196 | 197 | .game-over ul li img{ 198 | width: 2.96rem; 199 | height: 0.94rem; 200 | } 201 | 202 | .game-over .share { 203 | position: absolute; 204 | width: 100%; 205 | height: 100%; 206 | top: 0; 207 | left: 0; 208 | display: none; 209 | } 210 | 211 | .game-success{ 212 | display: none; 213 | width: 100%; 214 | height: 100%; 215 | position: relative; 216 | padding-top: 1px; 217 | text-align: center; 218 | } 219 | 220 | .game-success .win{ 221 | width: 6.48rem; 222 | height: 5.58rem; 223 | margin-top: 1rem; 224 | } 225 | 226 | .game-success .open{ 227 | width: 2.97rem; 228 | height: 0.94rem; 229 | margin-top: .2rem; 230 | } 231 | --------------------------------------------------------------------------------