├── .babelrc ├── .gitignore ├── README.md ├── __tests__ └── utils │ ├── device.spec.ts │ ├── formula.spec.ts │ ├── index.spec.ts │ ├── is.spec.ts │ ├── only.spec.ts │ └── uri.spec.ts ├── build ├── plugins │ └── copyrightWebpackPlugin.js ├── utils.js ├── webpack.base.config.js ├── webpack.dev.config.js ├── webpack.product.config.js └── webpack.test.config.js ├── config ├── index.js └── pages.js ├── jest.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── src ├── app │ ├── app.ts │ ├── index.ts │ └── modules │ │ ├── ajax.ts │ │ ├── audio.ts │ │ ├── confirm.ts │ │ ├── index.ts │ │ ├── loading.ts │ │ ├── logger.ts │ │ ├── mask.ts │ │ ├── proxy.ts │ │ ├── router.ts │ │ ├── share │ │ ├── browser.ts │ │ ├── index.ts │ │ ├── qq.ts │ │ ├── share.ts │ │ └── wechat.ts │ │ ├── statistic.ts │ │ ├── storage.ts │ │ └── tip.ts ├── assets │ ├── images │ │ └── global │ │ │ ├── icon-close.png │ │ │ ├── share.png │ │ │ └── sprite.png │ └── scss │ │ ├── base │ │ ├── animation.scss │ │ ├── button.scss │ │ ├── common.scss │ │ ├── globalConfirm.scss │ │ ├── globalFooter.scss │ │ ├── globalHeader.scss │ │ ├── globalLoading.scss │ │ ├── globalMask.scss │ │ ├── globalShare.scss │ │ ├── globalTip.scss │ │ ├── normalize.scss │ │ └── variables.scss │ │ ├── demo.scss │ │ └── home.scss ├── constant.ts ├── interface.ts ├── layout │ ├── footer.ejs │ ├── header.ejs │ ├── index.js │ ├── layout.ejs │ ├── loading.ejs │ └── location.ejs ├── pages │ ├── demo │ │ ├── index-render.js │ │ ├── index.ejs │ │ └── index.ts │ └── home │ │ ├── index-render.js │ │ ├── index.ejs │ │ ├── index.ts │ │ └── other.ts └── utils │ ├── device.ts │ ├── domHelper.ts │ ├── formula.ts │ ├── index.ts │ ├── is.ts │ ├── only.ts │ ├── shims.ts │ └── uri.ts └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "modules": false, 7 | "targets": { 8 | "browsers": ["ie >= 8"] 9 | }, 10 | "useBuiltIns": "usage" 11 | } 12 | ], 13 | "@babel/preset-typescript" 14 | ], 15 | "plugins": ["@babel/plugin-transform-runtime"] 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .vscode/ 3 | .idea/ 4 | dist/ 5 | coverage/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

mpa-typescript

2 | 这是一个基于webpack4.0 的前端工程,简化繁杂的前端开发。可配置多环境,可自定义配置多入口。无第三方依赖,代码体积小。支持一些好的写法,比如:Typescript,ES6、curry、compose等。 3 | 4 | ## Install 5 | 6 | ```bash 7 | # 安装依赖 8 | npm install 9 | 10 | # 开始开发 11 | npm start 12 | 13 | # 开发完了,可以构建你的代码 14 | # 测试环境代码构建 15 | npm run build:test 16 | 17 | # 生产环境代码构建 18 | npm run build 19 | ``` 20 | 21 |

Features

22 | 23 | - ✔︎ 可配置多环境,开发环境、测试环境、预生产环境、生产环境等 24 | - ✔︎ 可自定义配置多入口,简易配置,入口和入口间相互独立 25 | - ✔︎ 拥有许多通用模块,让你从繁杂的网页开发中跳出来 26 | - ✔︎ 基于Typescript实现,让你的Javascript代码更好维护 27 | - ✔ 无第三方依赖,代码体积小,如果你需要其他依赖可自行引入 28 | - ✔ 支持响应式开发 29 | 30 | ## 目录说明 31 | ```bash 32 | ├──build/ * webpack 工程化代码 33 | ├──config/ * 配置文件目录,在这里可以自定义配置多个环境,多个入口文件 34 | ├──src/ * 项目代码目录 35 | ├──├──app/ * app实现 36 | ├──├──├──modules/ * 通用模块 37 | ├──├──assets/ * 前端资源文件 38 | ├──├──layout/ * 基于Ejs的模板文件 39 | ├──├──pages/ * 业务代码目录 40 | ├──├──utis/ * 工具类 41 | ├──├──constant.ts * 通用常量 42 | ├──├──interface.ts * typescript 通用接口文件 43 | ``` 44 | 45 | ## 多开发环境配置 46 | - 在 /config/index.js 文件内,可添加修改项目的环境,目前已配置development、test、production三个环境。 47 | - 在build目录下建立对应的webpack环境文件 48 | - 并在ppackage.json配置对应构建命令即可。可参考test、production的配置。 49 | 50 | ## 节点配置说明 51 | ```js 52 | name: { // 环境名称,如production 53 | devtool: false, // 是否使用devtool 54 | NODE_ENV: 'production', // 全局 NODE_ENV 变量 55 | HOST: 'www.website.com', // 该环境对应的host 56 | API: 'www.website.com/api', // 该环境对应的api 57 | jsSourceMap: false, // 是否使用sourcemap 58 | cssSourceMap: false, // 是否使用sourcemap 59 | eslint: false, // 是否使用eslint 60 | filePath: '', // 构建后资源的目录 61 | staticPath: '', // 静态资源资源的CDN路径,如://cdn.website.com 62 | imgPath: '', // 图片资源的CDN路径,如://img.website.com 63 | filenameHash: true, // 构建后的文件,是否使用hash的形式 64 | }, 65 | ``` 66 | ## 多入口文件配置 67 | 68 | 在/config/pages.js文件内,可以添加多入口文件,具体如下,可参照home文件进行配置 69 | 70 | ```js 71 | { 72 | name: 'home', // 入口名称 73 | path: resolve('src/pages', 'home/index.ts'), // 入口文件路径 74 | filename: 'index.html', // 构建后的名称,支持目录如:onezero/index.html 75 | template: resolve('src/pages', 'home/index-render.js') // 文件模板 76 | }, 77 | ``` 78 | 79 | ## 模块支持 80 | **创建一个app** 81 | 82 | 文件路径: src/app/app.ts
83 | app主体实现 84 | 85 | ```js 86 | new App({ 87 | data: { 88 | // 这里设定的属性,可以通过this.xxx直接操作 89 | }, 90 | watchs: { 91 | // 这里设定的属性,会进行监听,可以通过this.xxx直接操作,如果修改,会触发this.xxxHandler,可以在xxxHandler 92 | }, 93 | bindEvents () { 94 | // 这里可以进行一些时间绑定 95 | }, 96 | init () { 97 | // 这是程序的入口 98 | } 99 | }); 100 | ``` 101 | 102 | **tip 模块的使用** 103 | 104 | 文件路径: src/app/modules/tip.ts
105 | tip模块,支持自定义主题,是否可关闭,自动关闭时间等。 106 | 107 | ```js 108 | this.$tip({ 109 | message: '这是一个提示框', // 提示信息 110 | closable: true, // 是否可主动关闭 111 | duration: 0, // 自动关闭时间,0不自动关闭 112 | callback: () => { // 关闭后回调 113 | console.log('提示框关闭后'); 114 | } 115 | }); 116 | ``` 117 | 118 | **confirm 模块的使用** 119 | 120 | 文件路径: src/app/modules/confirm.ts
121 | confirm模块,支持自定义标题等信息,回调函数支持promise写法。 122 | 123 | ```js 124 | this.$confirm({ 125 | title: '标题', // 标题 126 | okLabel: '确定', // 确认按钮,空则不显示,默认值: 确定 127 | cancelLabel: '取消‘, // 取消按钮,空则不显示,默认值: 取消 128 | content: '内容', // 内容 129 | okCallback: () => { // 确定按钮回调,支持async、promise,可不传 130 | console.log(`click ok at:`, Date.now()); 131 | // support callback for async 132 | return new Promise((resolve, reject) => { 133 | setTimeout(() => { 134 | resolve(); 135 | console.log('close dialog at:', Date.now()); 136 | }, 2000); 137 | }); 138 | }, 139 | cancelCallback, // 取消按钮回调,可不传 140 | }); 141 | ``` 142 | 143 | **当前路由信息** 144 | 145 | 通过路由信息可以获取host、port、path、query的信息 146 | 147 | ```js 148 | console.log(this.$route); 149 | // {"hostname":"127.0.0.1","port":9000,"path":"","query":{"id":"10","action":"justdoit"}} 150 | ``` 151 | 152 | **router路由方法** 153 | 154 | 文件路径: src/app/modules/router.ts
155 | router模块,可调用路由方法进行页面跳转、刷新重载等操作。 156 | 157 | ```js 158 | this.$router.push('/page.html'); 159 | this.$router.repace('/page.html'); 160 | this.$router.goBack(); 161 | this.$router.reload(); 162 | ``` 163 | 164 | **share组件的使用** 165 | 166 | 文件路径: src/app/modules/share.ts
167 | 可以自定义分享信息 168 | 169 | ```js 170 | // 分享信息 171 | const shareInfo = { 172 | title: '测试分享标题', 173 | desc: '测试分享描述', 174 | link: 'http://www.shuxia123.com', 175 | imgUrl: 'http://assets.shuxia123.com/uploads/2019/1554004957941_width_748_height_500.jpg' 176 | }; 177 | this.share = new Share(微信分享的token请求Url, QQ分享的Appid, shareInfo); 178 | // 调用分析 179 | this.share.callShare(); 180 | ``` 181 | 182 | **loading 模块的使用** 183 | 184 | 文件路径: src/app/modules/loading.ts
185 | 适用于移动端H5开发时,需要预加载一些图片资源。 186 | 187 | ```js 188 | const loading = new Loading(['http://assets.shuxia123.com/uploads/2019/1555171314834_width_1289_height_476.png', 189 | 'http://assets.shuxia123.com/uploads/2019/1555170559378_width_800_height_349.png', 190 | 'http://assets.shuxia123.com/uploads/2019/1554905994308_width_500_height_350.jpeg' 191 | ], () => { 192 | console.log('图片加完成') 193 | }); 194 | // 开始图片依赖,加载完后会自动关闭loading页 195 | loading.start(); 196 | ``` 197 | 198 | **ajax、jsonp的使用** 199 | 200 | 文件路径: src/app/modules/ajax.ts 201 | 202 | ```js 203 | this.$ajax(...).then(); 204 | this.$jsonp(...).then(); 205 | ``` 206 | 207 | **curry、compose的使用** -------------------------------------------------------------------------------- /__tests__/utils/device.spec.ts: -------------------------------------------------------------------------------- 1 | import { getTransitionEvent, getAnimationEvent } from '../../src/utils/device'; 2 | 3 | describe('test utils/device', () => { 4 | test('test getTransitionEvent', () => { 5 | expect(getTransitionEvent()).toBe('transitionend'); 6 | // test the remember 7 | expect(getTransitionEvent()).toBe('transitionend'); 8 | }); 9 | 10 | test('test getAnimationEvent', () => { 11 | expect(getAnimationEvent()).toBe('animationend'); 12 | // test the remember 13 | expect(getAnimationEvent()).toBe('animationend'); 14 | }); 15 | }); -------------------------------------------------------------------------------- /__tests__/utils/formula.spec.ts: -------------------------------------------------------------------------------- 1 | import { compose, curry } from '../../src/utils/formula'; 2 | 3 | describe('test utils/formula', () => { 4 | const sum = (a, b) => { 5 | return a + b; 6 | } 7 | 8 | const replaceSharpToSpace = (str) => { 9 | return str.replace(/\#/, ' '); 10 | } 11 | 12 | const wrapperWordName = (str) => { 13 | return str.replace(/(name)/, (word) => `(${word})`); 14 | } 15 | 16 | test('test curry func', () => { 17 | expect(curry(sum)(1, 2)).toBe(3); 18 | }); 19 | 20 | test('test compose func', () => { 21 | expect(compose(replaceSharpToSpace, wrapperWordName)('my name is wangxin#wang')).toBe('my (name) is wangxin wang') 22 | }); 23 | }); -------------------------------------------------------------------------------- /__tests__/utils/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { loadScript, loadImages } from '../../src/utils/index'; 2 | const LOAD_SUCCESS_SRC = 'LOAD_SUCCESS_SRC'; 3 | const LOAD_FAILURE_SRC = 'LOAD_FAILURE_SRC'; 4 | 5 | describe('test utils/index function loadScript', () => { 6 | beforeAll(() => { 7 | Object.defineProperty(document.head, 'appendChild', { 8 | value: function (element) { 9 | if (element.src === `http://localhost/${LOAD_SUCCESS_SRC}`) { 10 | element.onload(); 11 | } else { 12 | element.onerror(); 13 | } 14 | } 15 | }); 16 | }); 17 | 18 | test('test loadScript, success', () => { 19 | return loadScript(LOAD_SUCCESS_SRC).then((response) => { 20 | expect(response).toBe('success'); 21 | }); 22 | }); 23 | 24 | test('test loadScript, error', () => { 25 | return loadScript(LOAD_FAILURE_SRC).then(null, (response) => { 26 | expect(response).toBe('error'); 27 | }); 28 | }); 29 | }); 30 | 31 | describe('test utils/index loadImages', () => { 32 | beforeAll(() => { 33 | Object.defineProperty(Image.prototype, 'src', { 34 | set(src) { 35 | if (src === LOAD_FAILURE_SRC) { 36 | setTimeout(() => this.onerror(new Error('mocked error')), 0); 37 | } else if (src === LOAD_SUCCESS_SRC) { 38 | setTimeout(() => this.onload(), 0); 39 | } 40 | }, 41 | }); 42 | }); 43 | 44 | test('test loadImages', () => { 45 | const mockCallback = jest.fn(); 46 | return loadImages([LOAD_FAILURE_SRC, LOAD_FAILURE_SRC], mockCallback) 47 | .then((response) => { 48 | expect(mockCallback.mock.calls.length).toBe(2); 49 | expect(response).toBe(2); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /__tests__/utils/is.spec.ts: -------------------------------------------------------------------------------- 1 | import Is from '../../src/utils/is'; 2 | 3 | describe('test utils/is', () => { 4 | test('test Is.isWeibo func', () => { 5 | expect(Is.isWeibo()).toBe(false); 6 | }); 7 | 8 | test('test Is.isWechat func', () => { 9 | expect(Is.isWechat()).toBe(false); 10 | }); 11 | 12 | test('test Is.isQQ func', () => { 13 | expect(Is.isQQ()).toBe(false); 14 | }); 15 | 16 | test('test Is.isQZone func', () => { 17 | expect(Is.isQZone()).toBe(false); 18 | }); 19 | 20 | test('test Is.isAndroid func', () => { 21 | expect(Is.isAndroid()).toBe(false); 22 | }); 23 | 24 | test('test Is.isIos func', () => { 25 | expect(Is.isIos()).toBe(false); 26 | }); 27 | 28 | test('test Is.isIphoneX func', () => { 29 | expect(Is.isIphoneX()).toBe(false); 30 | }); 31 | }); -------------------------------------------------------------------------------- /__tests__/utils/only.spec.ts: -------------------------------------------------------------------------------- 1 | import only from '../../src/utils/only'; 2 | 3 | describe('test utils/only', () => { 4 | const object = { 5 | name: 'wangjiaxin', 6 | age: 22, 7 | sexy: 'male' 8 | }; 9 | 10 | test('test only pick func', () => { 11 | expect(only(object, 'name age')).toEqual({ 12 | name: 'wangjiaxin', 13 | age: 22 14 | }); 15 | 16 | expect(only(object, ['name', 'age'])).toEqual({ 17 | name: 'wangjiaxin', 18 | age: 22 19 | }); 20 | }); 21 | }); -------------------------------------------------------------------------------- /__tests__/utils/uri.spec.ts: -------------------------------------------------------------------------------- 1 | import Uri from '../../src/utils/uri'; 2 | 3 | describe('test utils/uri', () => { 4 | test('test uri.stringifyQuery func, url = "" ', () => { 5 | expect(Uri.parse('')).toEqual({ 6 | hostname: '', 7 | port: 80, 8 | path: '', 9 | query: {} 10 | }); 11 | }); 12 | 13 | test('test uri.stringifyQuery func', () => { 14 | expect(Uri.parse('https://www.shuxia123.com:8080/user?id=30&name=kobe')).toEqual({ 15 | hostname: 'www.shuxia123.com', 16 | port: 8080, 17 | path: '/user', 18 | query: { 19 | id: '30', 20 | name: 'kobe' 21 | } 22 | }); 23 | }); 24 | 25 | test('test uri.stringifyQuery func, with filename', () => { 26 | expect(Uri.parse('https://www.shuxia123.com:8080/index.html?id=30&name=kobe')).toEqual({ 27 | hostname: 'www.shuxia123.com', 28 | port: 8080, 29 | path: '/index.html', 30 | query: { 31 | id: '30', 32 | name: 'kobe' 33 | } 34 | }); 35 | }); 36 | 37 | test('test uri.stringifyQuery func, with ?', () => { 38 | expect(Uri.parse('https://www.shuxia123.com:8080/user/index?id=30&name=kobe')).toEqual({ 39 | hostname: 'www.shuxia123.com', 40 | port: 8080, 41 | path: '/user/index', 42 | query: { 43 | id: '30', 44 | name: 'kobe' 45 | } 46 | }); 47 | }); 48 | 49 | test('test uri.stringifyQuery func, with #', () => { 50 | expect(Uri.parse('https://www.shuxia123.com:8080/user#id=30&name=kobe')).toEqual({ 51 | hostname: 'www.shuxia123.com', 52 | port: 8080, 53 | path: '/user', 54 | query: { 55 | id: '30', 56 | name: 'kobe' 57 | } 58 | }); 59 | }); 60 | 61 | test('test uri.format func', () => { 62 | expect(Uri.format( 63 | { 64 | hostname: 'www.shuxia123.com', 65 | port: 8080, 66 | path: '/user', 67 | query: { 68 | id: '30', 69 | name: 'kobe' 70 | } 71 | } 72 | )).toBe('http://www.shuxia123.com:8080/user?id=30&name=kobe'); 73 | }); 74 | }); -------------------------------------------------------------------------------- /build/plugins/copyrightWebpackPlugin.js: -------------------------------------------------------------------------------- 1 | function CopyrightWebpackPlugin (options) { 2 | this.options = options; 3 | } 4 | 5 | CopyrightWebpackPlugin.prototype.apply = function (compiler) { 6 | const options = this.options; 7 | compiler.plugin('emit', function (compilation, callback) { 8 | for (let filename in compilation.assets) { 9 | if (filename.endsWith('.js')) { 10 | compilation.assets[filename]._value = `${compilation.assets[filename]._value}\n${options}`; 11 | } 12 | } 13 | callback(); 14 | }); 15 | }; 16 | 17 | module.exports = CopyrightWebpackPlugin; -------------------------------------------------------------------------------- /build/utils.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | console.log(path.join('/', 'coocss')); 4 | 5 | exports.resolve = function resolve (...args) { 6 | return path.join(__dirname, '..', ...args); 7 | }; -------------------------------------------------------------------------------- /build/webpack.base.config.js: -------------------------------------------------------------------------------- 1 | 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 5 | const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 6 | const CopyrightPlugin = require('./plugins/copyrightWebpackPlugin'); 7 | const { resolve } = require('./utils'); 8 | 9 | module.exports = function webpackBaseConfig (NODE_ENV = 'development') { 10 | const config = require('../config')[NODE_ENV]; 11 | const files = require('../config/pages'); 12 | const IS_DEVELOPMENT = NODE_ENV === 'development'; 13 | let entry = {}; 14 | files.forEach((item) => { 15 | entry[item.name] = item.path; 16 | }); 17 | 18 | const webpackConfig = { 19 | entry, 20 | output: { 21 | path: resolve('./'), 22 | publicPath: config.staticPath, 23 | filename: `${config.filePath}js/${config.filenameHash ? '[name].[chunkhash:8]' : '[name]'}.js`, 24 | chunkFilename: `${config.filePath}js/${config.filenameHash ? '[name].[chunkhash:8]' : '[name]'}.js` 25 | }, 26 | externals: {}, 27 | devtool: config.devtool, 28 | module: { 29 | rules: [ 30 | { 31 | test: /\.ts$/, 32 | loader: 'ts-loader', 33 | exclude: /(node_modules)/ 34 | }, 35 | { 36 | test: /\.ts$/, 37 | enforce: 'pre', 38 | exclude: /node_modules/, 39 | loader: 'source-map-loader' 40 | }, 41 | { 42 | test: /\.html$/, 43 | loader: 'html-loader' 44 | }, 45 | { 46 | test: /\.ejs$/, 47 | loader: 'ejs-loader' 48 | }, 49 | { 50 | test: /\.(png|jpg|gif|svg)$/, 51 | loader: 'url-loader', 52 | query: { 53 | limit: 1, 54 | publicPath: config.imgPath, 55 | name: `${config.filePath}images/${config.filenameHash ? '[name].[hash:8]' : '[name]'}.[ext]` 56 | } 57 | }, 58 | { 59 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 60 | loader: 'file-loader', 61 | query: { 62 | publicPath: config.imgPath, 63 | name: `${config.filePath}fonts/${config.filenameHash ? '[name].[hash:8]' : '[name]'}.[ext]` 64 | } 65 | }, 66 | { 67 | test: /\.scss$/, 68 | include: resolve('src'), 69 | use: [ 70 | { 71 | loader: MiniCssExtractPlugin.loader, 72 | options: { 73 | hmr: IS_DEVELOPMENT, 74 | }, 75 | }, 76 | 'css-loader', 77 | 'postcss-loader', 78 | 'sass-loader' 79 | ] 80 | } 81 | ] 82 | }, 83 | plugins: [ 84 | new webpack.DefinePlugin({ 85 | 'process.env.NODE_ENV': JSON.stringify(NODE_ENV), 86 | 'process.env.STATIC_PATH': JSON.stringify(config.staticPath), 87 | 'process.env.HOST': JSON.stringify(config.HOST) 88 | }) 89 | ], 90 | resolve: { 91 | alias: { 92 | '@app': resolve('src/app/index.ts'), 93 | '@layout': resolve('src/layout/index.js'), 94 | '@modules': resolve('src/app/modules'), 95 | '@utils': resolve('src/utils'), 96 | '@scss': resolve('src/assets/scss') 97 | }, 98 | extensions: ['.ts', '.js', '.json'] 99 | }, 100 | }; 101 | 102 | files.forEach((item) => { 103 | webpackConfig.plugins.push( 104 | new HtmlWebpackPlugin({ 105 | filename: item.filename, 106 | template: item.template, 107 | chunks: ['manifest', 'vendor', 'app', 'common', item.name], 108 | hash: false, 109 | inject: 'body', 110 | xhtml: false, 111 | minify: { 112 | removeComments: true, 113 | } 114 | }) 115 | ); 116 | }); 117 | 118 | // 抽离css,命名采用contenthash 119 | webpackConfig.plugins.push( 120 | new MiniCssExtractPlugin({ 121 | filename: IS_DEVELOPMENT ? 'style.css' : 'css/[name].[contenthash:8].css', 122 | chunkFilename: IS_DEVELOPMENT ? '[id].css' : 'css/[id].[contenthash:8].css', 123 | }) 124 | ); 125 | 126 | // 公共代码 127 | webpackConfig.optimization = { 128 | splitChunks: { 129 | cacheGroups: { 130 | app: { 131 | test: /[\\/]src\/app[\\/]/, 132 | chunks: 'all', 133 | name: 'app', 134 | minChunks: 1, 135 | priority: 10 136 | }, 137 | vendor: { 138 | test: /[\\/]node_modules[\\/]/, 139 | chunks: 'all', 140 | name: 'vendor', 141 | minChunks: 1, 142 | priority: 10 143 | }, 144 | common: { 145 | test: /[\\/]src[\\/]/, 146 | chunks: 'all', 147 | name: 'common', 148 | minChunks: 3, 149 | priority: 10 150 | } 151 | } 152 | }, 153 | moduleIds: 'hashed', 154 | runtimeChunk: { 155 | name: 'manifest', 156 | } 157 | }; 158 | 159 | // 开发环境服务器配置 160 | if (IS_DEVELOPMENT) { 161 | webpackConfig.devServer = { 162 | contentBase: resolve('dist'), 163 | compress: false, 164 | host: '127.0.0.1', 165 | port: config.port, 166 | hot: true, 167 | disableHostCheck: true, 168 | historyApiFallback: true 169 | }; 170 | 171 | // webpack watch 配置 172 | webpackConfig.watchOptions = { 173 | poll: 500, 174 | ignored: 'node_modules' 175 | }; 176 | // 热更新 177 | webpackConfig.plugins.push( 178 | new webpack.HotModuleReplacementPlugin() 179 | ); 180 | } else { 181 | // 压缩css 182 | webpackConfig.plugins.push( 183 | new OptimizeCssAssetsPlugin({ 184 | cssProcessorOptions: { safe: true } 185 | }) 186 | ); 187 | 188 | webpackConfig.plugins.push( 189 | new CopyrightPlugin(`/**\n * 作者: 王佳欣\n * 站点: http://www.shuxia123.com\n */`) 190 | ); 191 | } 192 | return webpackConfig; 193 | }; 194 | -------------------------------------------------------------------------------- /build/webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const NODE_ENV = process.env.NODE_ENV = 'development'; 3 | const webpackBaseConfig = require('./webpack.base.config')(NODE_ENV); 4 | const webpackConfig = merge(webpackBaseConfig, { 5 | mode: 'development' 6 | }); 7 | module.exports = webpackConfig; -------------------------------------------------------------------------------- /build/webpack.product.config.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const NODE_ENV = process.env.NODE_ENV = 'production'; 3 | const webpackBaseConfig = require('./webpack.base.config')(NODE_ENV); 4 | const webpackConfig = merge(webpackBaseConfig, { 5 | mode: 'production' 6 | }); 7 | 8 | module.exports = webpackConfig; -------------------------------------------------------------------------------- /build/webpack.test.config.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const NODE_ENV = process.env.NODE_ENV = 'test'; 3 | const webpackBaseConfig = require('./webpack.base.config')(NODE_ENV); 4 | const webpackConfig = merge(webpackBaseConfig, { 5 | mode: 'development' 6 | }); 7 | 8 | module.exports = webpackConfig; 9 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | name: { // 环境名称 3 | devtool: false, // 是否使用devtool 4 | NODE_ENV: 'production', // 全局 NODE_ENV 变量 5 | HOST: 'www.website.com', // 该环境对应的host 6 | API: 'www.website.com/api', // 该环境对应的api 7 | jsSourceMap: false, // 是否使用sourcemap 8 | cssSourceMap: false, // 是否使用sourcemap 9 | eslint: false, // 是否使用eslint 10 | filePath: '', // 构建后资源的目录 11 | staticPath: '', // 静态资源资源的CDN路径 12 | imgPath: '', // 图片资源的CDN路径 13 | filenameHash: true, // 构建后的文件,是否使用hash的形式 14 | }, 15 | */ 16 | 17 | module.exports = { 18 | production: { 19 | devtool: false, 20 | NODE_ENV: 'production', 21 | HOST: 'www.website.com', 22 | API: 'www.website.com/api', 23 | jsSourceMap: false, 24 | cssSourceMap: false, 25 | eslint: false, 26 | filePath: '', 27 | staticPath: 'https://coocssweb.github.io/html5-typescript/', 28 | imgPath: '', 29 | filenameHash: true, 30 | }, 31 | test: { 32 | devtool: false, 33 | NODE_ENV: 'test', 34 | HOST: 'test.website.com', 35 | API: 'test.website.com/api', 36 | jsSourceMap: false, 37 | cssSourceMap: false, 38 | eslint: false, 39 | filePath: '', 40 | staticPath: 'https://coocssweb.github.io/html5-typescript/', 41 | imgPath: '', 42 | filenameHash: false, 43 | }, 44 | development: { 45 | port: 9001, 46 | devtool: 'source-map', 47 | NODE_ENV: 'development', 48 | HOST: 'dev.website.com', 49 | API: 'dev.website.com/api', 50 | jsSourceMap: false, 51 | cssSourceMap: false, 52 | eslint: false, 53 | staticPath: '/', 54 | filePath: '', 55 | imgPath: '', 56 | filenameHash: false, 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /config/pages.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('../build/utils'); 2 | 3 | console.log(resolve('src/pages', 'demo/index.ts')); 4 | 5 | module.exports = [ 6 | { 7 | name: 'home', 8 | path: resolve('src/pages', 'home/index.ts'), 9 | filename: 'index.html', 10 | template: resolve('src/pages', 'home/index-render.js') 11 | }, 12 | { 13 | name: 'demo', 14 | path: resolve('src/pages', 'demo/index.ts'), 15 | filename: 'demo.html', 16 | template: resolve('src/pages', 'demo/index-render.js') 17 | } 18 | ]; 19 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: [ 3 | "/__tests__" 4 | ], 5 | transform: { 6 | "^.+\\.tsx?$": "ts-jest" 7 | }, 8 | setupFiles: [ 9 | 10 | ], 11 | testRegex: "(/__tests__/.*|\\.(test|spec))\\.(ts|js)$", 12 | collectCoverage: true, 13 | collectCoverageFrom: ["src/**/*.ts"], 14 | moduleFileExtensions: [ 15 | "ts", 16 | "js" 17 | ] 18 | }; 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mpa-typescript", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --config build/webpack.dev.config.js --open --hot", 8 | "build:test": "webpack --config build/webpack.test.config.js", 9 | "build": "webpack -p --config build/webpack.product.config.js --progress --hide-modules", 10 | "test": "jest" 11 | }, 12 | "author": "coocssweb", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "@babel/core": "^7.2.0", 16 | "@babel/plugin-proposal-decorators": "^7.2.0", 17 | "@babel/plugin-proposal-export-namespace-from": "^7.2.0", 18 | "@babel/plugin-proposal-function-sent": "^7.2.0", 19 | "@babel/plugin-proposal-numeric-separator": "^7.2.0", 20 | "@babel/plugin-proposal-throw-expressions": "^7.2.0", 21 | "@babel/plugin-transform-runtime": "^7.2.0", 22 | "@babel/plugin-transform-typescript": "^7.8.7", 23 | "@babel/preset-env": "^7.8.7", 24 | "@babel/preset-es2015": "^7.0.0-beta.53", 25 | "@babel/preset-stage-1": "^7.8.3", 26 | "@babel/preset-typescript": "^7.3.3", 27 | "@babel/runtime": "^7.2.0", 28 | "@types/jest": "^24.0.15", 29 | "autoprefixer": "^6.4.0", 30 | "awesome-typescript-loader": "^5.2.1", 31 | "babel-eslint": "^10.0.1", 32 | "babel-loader": "^8.0.4", 33 | "copy-webpack-plugin": "^4.5.2", 34 | "core-js": "^2.6.5", 35 | "css-loader": "^0.26.0", 36 | "ejs-loader": "^0.3.0", 37 | "extract-text-webpack-plugin": "^2.1.2", 38 | "file-loader": "^3.0.1", 39 | "html-loader": "^0.4.5", 40 | "html-webpack-plugin": "^3.2.0", 41 | "images": "^3.0.0", 42 | "install": "^0.12.2", 43 | "jest": "^24.8.0", 44 | "mini-css-extract-plugin": "^0.6.0", 45 | "node-sass": "^4.5.2", 46 | "optimize-css-assets-webpack-plugin": "^5.0.1", 47 | "postcss-loader": "^3.0.0", 48 | "postcss-px2rem": "^0.3.0", 49 | "sass-loader": "^7.1.0", 50 | "source-map-loader": "^0.2.4", 51 | "style-loader": "^0.23.1", 52 | "ts-jest": "^24.0.2", 53 | "ts-loader": "^8.0.14", 54 | "typescript": "^3.4.4", 55 | "url-loader": "^1.1.2", 56 | "webpack": "^4.30.0", 57 | "webpack-bundle-analyzer": ">=3.3.2", 58 | "webpack-cli": "^3.3.1", 59 | "webpack-dev-server": "^3.3.1", 60 | "webpack-merge": "^4.1.0", 61 | "webpack-stream": "^3.2.0" 62 | }, 63 | "engines": { 64 | "node": ">= 6.0.0", 65 | "npm": ">= 3.0.0" 66 | }, 67 | "dependencies": { 68 | "md5.js": "^1.3.5" 69 | }, 70 | "browserslist": [ 71 | "> 1%", 72 | "ie 9", 73 | "iOS > 7", 74 | "Android >= 4" 75 | ] 76 | } 77 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'autoprefixer': true, 4 | 'postcss-px2rem': { 5 | remUnit: 200, 6 | } 7 | } 8 | }; -------------------------------------------------------------------------------- /src/app/app.ts: -------------------------------------------------------------------------------- 1 | import { Tip, Confirm, Router, Statistic, proxy, ajax, jsonp, Storage } from './modules/index'; 2 | import URI from '@utils/uri'; 3 | import { EMPTY_FUNCTION } from '../constant'; 4 | import { AjaxOptions, JsonpOptions, TipOptions, ConfirmOptions } from '../interface'; 5 | 6 | // extend destination's key to source 7 | const extend = (destination: any, source: any) => { 8 | for (let key in source) { 9 | if (source.hasOwnProperty(key)) { 10 | destination[key] = source[key]; 11 | } 12 | } 13 | return destination; 14 | }; 15 | 16 | function App (data: object, watchs: object) { 17 | this.data = data; 18 | this.watchs = watchs; 19 | this.$router = Router; 20 | this.$route = URI.parse(window.location.href); 21 | this.$statistic = Statistic; 22 | this.$storage = new Storage(); 23 | } 24 | 25 | App.prototype = { 26 | bindEvents: EMPTY_FUNCTION, 27 | 28 | init: EMPTY_FUNCTION, 29 | 30 | $tip: function (options: TipOptions) : void { 31 | new Tip(options); 32 | }, 33 | 34 | $ajax: function (options: AjaxOptions): Promise | string { 35 | return ajax(options); 36 | }, 37 | 38 | $jsonp: function (options: JsonpOptions): Promise { 39 | return jsonp(options); 40 | }, 41 | 42 | $confirm: function (options: ConfirmOptions) { 43 | return new Confirm(options); 44 | }, 45 | 46 | _init: function () : void { 47 | this.bindEvents(); 48 | // get or set a property on this, relate to data[propertyName] 49 | if (this.data) { 50 | for (let key in this.data) { 51 | Object.defineProperty(this, key, { 52 | get () { 53 | return this.data[key]; 54 | }, 55 | set (value) { 56 | this.data[key] = value; 57 | } 58 | }); 59 | } 60 | } 61 | // watch a property, would trigger a property name Handler 62 | if (this.watchs) { 63 | for (let key in this.watchs) { 64 | Object.defineProperty(this, key, { 65 | get () { 66 | return this.watchs[key]; 67 | }, 68 | set (value) { 69 | this.watchs[key] = value; 70 | } 71 | }); 72 | proxy({ 73 | data: this.watchs, 74 | key: key, 75 | callback: this[`${key}Handler`].bind(this) 76 | }); 77 | } 78 | } 79 | 80 | this.init(); 81 | } 82 | }; 83 | 84 | // extends app, return sub 85 | App.extends = (...args: any) => { 86 | const Super = App; 87 | 88 | const Sub = function (data: object, watchs: object) { 89 | App.call(this, data, watchs); 90 | this._init(); 91 | }; 92 | 93 | Sub.prototype = Object.create(Super.prototype); 94 | Sub.prototype.constructor = Sub; 95 | 96 | [...args].map((item) => { 97 | extend( 98 | Sub.prototype, 99 | item 100 | ); 101 | }); 102 | 103 | return Sub; 104 | }; 105 | 106 | export default App; 107 | -------------------------------------------------------------------------------- /src/app/index.ts: -------------------------------------------------------------------------------- 1 | import '@scss/base/common.scss'; 2 | import App from './app'; 3 | 4 | interface AppOptions { 5 | data?: object, 6 | watchs?: object, 7 | bindEvents?: Function, 8 | init?: Function, 9 | [propName: string]: any; 10 | } 11 | 12 | // a tool function for create app 13 | export default class Instance { 14 | constructor (options: AppOptions) { 15 | const optionsExtend = { 16 | ... options 17 | }; 18 | const { data, watchs, ...resets } = optionsExtend; 19 | 20 | return new (App.extends(resets))(data, watchs); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/modules/ajax.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file utils/ajax.ts ajax、jsonp 网络请求 3 | * @author: 王佳欣 4 | * @email: 1974740999@qq.com 5 | */ 6 | import { AjaxOptions, ContentType, JsonpOptions } from '../../interface'; 7 | 8 | /** 9 | * ajax 10 | */ 11 | function Ajax(options: AjaxOptions): Promise | string { 12 | options = { ...Ajax.defaultOptions, ...options }; 13 | const xmlHttp: XMLHttpRequest = new XMLHttpRequest(); 14 | // define request data 15 | if (options.contentType = ContentType.Urlencoded) { 16 | const keysOfData = Object.keys(options.data); 17 | const strQuery = keysOfData.map(key => { 18 | return `${key}=${options.data[key]}` 19 | }).join('&'); 20 | 21 | if (options.method === 'get') { 22 | options.url = options.url.indexOf('?') > -1 23 | ? `${options.url}&${strQuery}` 24 | : `${options.url}?${strQuery}`; 25 | } 26 | else if (options.method === 'post') { 27 | options.data = strQuery; 28 | } 29 | } 30 | 31 | // define request header 32 | xmlHttp.open(options.method, options.url, options.async); 33 | options.token !== '' && xmlHttp.setRequestHeader('Authorization', options.token); 34 | xmlHttp.setRequestHeader('Content-Type', options.contentType); 35 | xmlHttp.setRequestHeader('Accept', options.dataType); 36 | 37 | // when cross domain, set withCredentials to send cookies 38 | if (options.xhrFields.withCredentials) { 39 | xmlHttp.withCredentials = options.xhrFields.withCredentials; 40 | } 41 | 42 | // when http request is not asynchronous 43 | // options.async === false 44 | if (options.async === false) { 45 | xmlHttp.send(options.data ? options.data : null); 46 | return xmlHttp.responseText; 47 | } 48 | 49 | // when http request is asynchronous 50 | // options.async === true 51 | return new Promise ((resolve, reject) => { 52 | xmlHttp.onreadystatechange = function () { 53 | if (xmlHttp.readyState !== 4) { 54 | return; 55 | } 56 | 57 | if (xmlHttp.status >= 200 && xmlHttp.status <= 304) { 58 | resolve(JSON.parse(xmlHttp.responseText)); 59 | } 60 | else { 61 | reject(JSON.parse(xmlHttp.responseText || "{}")); 62 | } 63 | }; 64 | 65 | xmlHttp.send(options.data ? options.data : null); 66 | }); 67 | }; 68 | 69 | Ajax.defaultOptions = { 70 | url: '', 71 | method: 'get', 72 | data: {}, 73 | dataType: 'json', 74 | contentType: ContentType.Urlencoded, 75 | xhrFields: { 76 | withCredentials: false 77 | }, 78 | token: '', 79 | async: true 80 | }; 81 | 82 | export const ajax = Ajax; 83 | 84 | /** 85 | * jsonp 86 | */ 87 | function Jsonp (options: JsonpOptions): Promise { 88 | options = { ...Jsonp.defaultOptions, ...options }; 89 | if (!options.url) { 90 | return Promise.resolve(null); 91 | } 92 | 93 | const script = document.createElement('script'); 94 | const jsonpCallback = `jsonp_${+new Date()}`; 95 | options.jsonpCallback = jsonpCallback; 96 | 97 | const allKeys = Object.keys(options.data); 98 | const strQuery = allKeys.map(key => { 99 | if (key !== 'callback') { 100 | return `${key}=${options.data[key]}`; 101 | } 102 | }).join('&'); 103 | 104 | const url = `${options.url}${options.url.indexOf('?')> -1 ? '&' : '?'}callback=${jsonpCallback}&${strQuery}`; 105 | script.setAttribute('src', url); 106 | 107 | return new Promise ((resolve, reject) => { 108 | window[jsonpCallback] = (data: any) => { 109 | delete window[jsonpCallback]; 110 | document.head.removeChild(script); 111 | resolve(data); 112 | }; 113 | document.head.appendChild(script); 114 | }); 115 | }; 116 | 117 | Jsonp.defaultOptions = { 118 | url: '', 119 | data: {} 120 | }; 121 | 122 | export const jsonp = Jsonp; -------------------------------------------------------------------------------- /src/app/modules/audio.ts: -------------------------------------------------------------------------------- 1 | import { EMPTY_FUNCTION } from '../../constant'; 2 | interface Options { 3 | src: string, 4 | autoPlay: boolean, 5 | loop: boolean, 6 | onPlay?: Function, 7 | onPause?: Function, 8 | onEnded?: Function 9 | }; 10 | 11 | export default class Audio { 12 | static readonly defaultOptions: Options = { 13 | src: '', 14 | autoPlay: false, 15 | loop: false, 16 | onPlay: EMPTY_FUNCTION, 17 | onPause: EMPTY_FUNCTION, 18 | onEnded: EMPTY_FUNCTION 19 | } 20 | 21 | public isPlaying: boolean = false; 22 | private element: HTMLAudioElement; 23 | 24 | constructor (options: Options) { 25 | options = {...Audio.defaultOptions, ...options }; 26 | this.element = this.createAudioElement(options); 27 | } 28 | 29 | private createAudioElement (options: Options): HTMLAudioElement { 30 | const wrapEvent = (userEvent = EMPTY_FUNCTION, proxyEvent = EMPTY_FUNCTION) => { 31 | return (event: Event) => { 32 | try { 33 | proxyEvent(event); 34 | } 35 | finally 36 | { 37 | userEvent(event); 38 | } 39 | }; 40 | }; 41 | 42 | const onPlay = () => { 43 | this.isPlaying = true; 44 | }; 45 | const onPause = () => { 46 | this.isPlaying = false; 47 | }; 48 | const onEnded = () => { 49 | this.isPlaying = !options.loop; 50 | } 51 | 52 | let element = document.createElement('audio'); 53 | element.autoplay = options.autoPlay; 54 | element.setAttribute('src', options.src); 55 | element.setAttribute('loop', options.loop ? 'loop' : ''); 56 | 57 | // events for audio 58 | element.onplay = wrapEvent(options.onPlay, onPlay); 59 | element.onpause = wrapEvent(options.onPause, onPause); 60 | element.onended = wrapEvent(options.onEnded, onEnded); 61 | 62 | return element; 63 | } 64 | 65 | public play () { 66 | this.element.play(); 67 | } 68 | 69 | public pause () { 70 | this.element.pause(); 71 | } 72 | } -------------------------------------------------------------------------------- /src/app/modules/confirm.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file modules/toast.ts 确认模块 3 | * @author: 王佳欣 4 | * @email: 1974740999@qq.com 5 | */ 6 | import Mask from './mask'; 7 | import { getTransitionEvent } from '@utils/device'; 8 | import { EMPTY_FUNCTION } from '../../constant'; 9 | import { ConfirmOptions } from '../../interface'; 10 | 11 | export default class Confirm { 12 | static readonly prefix = 'globalConfirm'; 13 | static defaultOptions: ConfirmOptions = { 14 | title: '', 15 | closable: true, 16 | content: '', 17 | width: 500, 18 | okLabel: '确认', 19 | cancelLabel: '取消', 20 | okCallback: EMPTY_FUNCTION, 21 | cancelCallback: EMPTY_FUNCTION, 22 | }; 23 | 24 | private options: ConfirmOptions; 25 | private transitionEvent: string; 26 | 27 | private $domContainer: HTMLElement; 28 | private $domClosable: HTMLElement; 29 | private $domOk: HTMLElement; 30 | private $domCancel: HTMLElement; 31 | 32 | constructor (options: ConfirmOptions) { 33 | this.options = { ...Confirm.defaultOptions, ...options }; 34 | 35 | // rebind 36 | this.handleOkClick = this.handleOkClick.bind(this); 37 | this.handleCancelClick = this.handleCancelClick.bind(this); 38 | this.handleCloseClick = this.handleCloseClick.bind(this); 39 | this.handleTransitionEnd = this.handleTransitionEnd.bind(this); 40 | this.transitionEvent = getTransitionEvent(); 41 | 42 | this.createElement(); 43 | this.bindEvents(); 44 | } 45 | 46 | private createElement () { 47 | this.$domContainer = document.createElement('div'); 48 | this.$domContainer.classList.add(Confirm.prefix); 49 | this.$domContainer.style.width = `${this.options.width}px`; 50 | this.$domContainer.style.marginLeft = `-${this.options.width / 2}px`; 51 | 52 | // create dialog's header and content and footer 53 | const domHeader = this.createHeaderElement(); 54 | const domContent = document.createElement('div'); 55 | domContent.classList.add(`${Confirm.prefix}-content`); 56 | domContent.innerHTML = this.options.content; 57 | const domFooter = this.createFooterElement(); 58 | this.$domContainer.appendChild(domHeader); 59 | this.$domContainer.appendChild(domContent); 60 | domFooter && this.$domContainer.appendChild(domFooter); 61 | 62 | Mask.open(); 63 | 64 | document.body.append(this.$domContainer); 65 | this.$domContainer.style.marginTop = `-${this.$domContainer.clientHeight / 2}px`; 66 | } 67 | 68 | private createHeaderElement (): HTMLElement { 69 | const domHeader = document.createElement('div'); 70 | domHeader.classList.add(`${Confirm.prefix}-header`); 71 | 72 | if (this.options.title) { 73 | const domTitle = document.createElement('div'); 74 | domTitle.classList.add(`${Confirm.prefix}-title`); 75 | domTitle.innerHTML = this.options.title; 76 | domHeader.appendChild(domTitle); 77 | } 78 | 79 | if (this.options.closable) { 80 | this.$domClosable = document.createElement('i'); 81 | this.$domClosable.classList.add(`${Confirm.prefix}-close`); 82 | this.$domContainer.append(this.$domClosable); 83 | } 84 | 85 | return domHeader; 86 | } 87 | 88 | private createFooterElement (): HTMLElement { 89 | const hasConfirmFooter = this.options.okLabel || this.options.cancelLabel; 90 | if (!hasConfirmFooter) { 91 | return; 92 | } 93 | 94 | let domFooter = document.createElement('div'); 95 | domFooter.classList.add(`${Confirm.prefix}-footer`); 96 | 97 | if (this.options.okLabel) { 98 | this.$domOk = document.createElement('button'); 99 | this.$domOk.innerHTML = this.options.okLabel; 100 | this.$domOk.classList.add(`${Confirm.prefix}-btn`); 101 | this.$domOk.classList.add(`${Confirm.prefix}-btn--ok`); 102 | domFooter.appendChild(this.$domOk); 103 | } 104 | 105 | if (this.options.cancelLabel) { 106 | this.$domCancel = document.createElement('button'); 107 | this.$domCancel.innerHTML = this.options.cancelLabel; 108 | this.$domCancel.classList.add(`${Confirm.prefix}-btn`); 109 | this.$domCancel.classList.add(`${Confirm.prefix}-btn--cancel`); 110 | domFooter.appendChild(this.$domCancel); 111 | } 112 | 113 | return domFooter; 114 | } 115 | 116 | private bindEvents () { 117 | this.$domContainer.addEventListener(this.transitionEvent, this.handleTransitionEnd); 118 | 119 | if (this.$domClosable) { 120 | this.$domClosable.addEventListener('click', this.handleCloseClick); 121 | } 122 | 123 | if (this.$domCancel) { 124 | this.$domCancel.addEventListener('click', this.handleCancelClick); 125 | } 126 | 127 | if (this.$domOk) { 128 | this.$domOk.addEventListener('click', this.handleOkClick); 129 | } 130 | } 131 | 132 | private unbindEvents () { 133 | this.$domContainer.removeEventListener(this.transitionEvent, this.handleTransitionEnd); 134 | 135 | if (this.$domClosable) { 136 | this.$domClosable.removeEventListener('click', this.handleCloseClick); 137 | } 138 | 139 | if (this.$domCancel) { 140 | this.$domCancel.removeEventListener('click', this.handleCancelClick); 141 | } 142 | 143 | if (this.$domOk) { 144 | this.$domOk.removeEventListener('click', this.handleOkClick); 145 | } 146 | } 147 | 148 | private handleTransitionEnd () { 149 | if (this.$domContainer.classList.contains('doOut')) { 150 | this.destory(); 151 | } 152 | } 153 | 154 | private destory() { 155 | this.unbindEvents(); 156 | document.body.removeChild(this.$domContainer); 157 | } 158 | 159 | private async handleOkClick () { 160 | await this.options.okCallback(); 161 | this.close(); 162 | } 163 | 164 | private async handleCancelClick () { 165 | await this.options.cancelCallback(); 166 | this.close(); 167 | } 168 | 169 | private handleCloseClick () { 170 | this.close(); 171 | } 172 | 173 | public close () { 174 | this.$domContainer.classList.add('doOut'); 175 | Mask.close(); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/app/modules/index.ts: -------------------------------------------------------------------------------- 1 | import Tip from './tip'; 2 | import Loading from './loading'; 3 | import Router from './router'; 4 | import Confirm from './confirm'; 5 | import proxy from './proxy'; 6 | import Statistic from './statistic'; 7 | import Storage from './storage'; 8 | import { ajax, jsonp } from './ajax'; 9 | 10 | export { 11 | Tip, 12 | Loading, 13 | Router, 14 | Confirm, 15 | Statistic, 16 | proxy, 17 | ajax, 18 | jsonp, 19 | Storage 20 | }; 21 | -------------------------------------------------------------------------------- /src/app/modules/loading.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file utils/loading.ts 加载动画 3 | * @author: 王佳欣 4 | * @email: 1974740999@qq.com 5 | */ 6 | import '@scss/base/globalLoading.scss'; 7 | import { loadImages } from '@utils/index'; 8 | import { getAnimationEvent } from '@utils/device'; 9 | import { EMPTY_FUNCTION } from '../../constant'; 10 | 11 | export default class Loading { 12 | static readonly prefix = 'globalLoading'; 13 | 14 | private haveLoadedPercent: number = 0; 15 | private haveLoadedCount: number = 0; 16 | private haveLoaded: boolean = false; 17 | private haveLoadedHalf: boolean = false; 18 | private loadedCallback: Function; 19 | private images: Array; 20 | 21 | // global loading's dom 22 | private $loadingContainer: HTMLElement; 23 | private $loadingProgress: HTMLElement; 24 | private $loadingValue: HTMLElement; 25 | private animationEvent: string; 26 | 27 | constructor (images: Array, callback?: Function) { 28 | this.images = images; 29 | this.loadedCallback = callback || EMPTY_FUNCTION; 30 | 31 | // rebind 32 | this.handleDestory = this.handleDestory.bind(this); 33 | 34 | this.$loadingContainer = document.querySelector(`.${Loading.prefix}`); 35 | this.$loadingProgress = document.querySelector(`.${Loading.prefix}-progress`); 36 | this.$loadingValue = document.querySelector(`.${Loading.prefix}-value`); 37 | this.animationEvent = getAnimationEvent(); 38 | 39 | this.bindEvents(); 40 | } 41 | 42 | private bindEvents () { 43 | this.$loadingContainer.addEventListener(this.animationEvent, this.handleDestory); 44 | } 45 | 46 | private handleDestory (e: any) { 47 | if (e.target !== e.currentTarget) { 48 | return; 49 | } 50 | 51 | this.unbindEvents(); 52 | // remove dom 53 | this.$loadingContainer.parentNode.removeChild(this.$loadingContainer); 54 | } 55 | 56 | private unbindEvents () { 57 | this.$loadingContainer.removeEventListener(this.animationEvent, this.handleDestory); 58 | } 59 | 60 | private handleLoadEnd (): void { 61 | this.$loadingContainer.classList.add('doOut'); 62 | } 63 | 64 | private setLoadingStatus (): void { 65 | this.$loadingValue && (this.$loadingValue.innerHTML = `${this.haveLoadedPercent}%`); 66 | this.$loadingProgress && (this.$loadingProgress.style.width = `${this.haveLoadedPercent}%`); 67 | } 68 | 69 | private countDown (timestamp: number): void { 70 | setTimeout(() => { 71 | this.setLoadingStatus(); 72 | 73 | if (this.haveLoadedPercent !== 50 74 | && this.haveLoadedPercent !== 80 75 | && this.haveLoadedPercent < 100) { 76 | this.haveLoadedPercent += 1; 77 | this.countDown(timestamp); 78 | } 79 | else if (this.haveLoadedPercent === 50) { 80 | // 加载50% 做假暂停 81 | this.haveLoadedPercent += this.haveLoadedHalf ? 1 : 0; 82 | this.countDown(timestamp); 83 | } 84 | else if (this.haveLoadedPercent === 80) { 85 | // 加载80% 做假暂停 86 | this.haveLoadedPercent += this.haveLoaded ? 1 : 0; 87 | this.countDown(timestamp); 88 | } 89 | else if (this.haveLoadedPercent > 99) { 90 | this.loadedCallback(); 91 | this.handleLoadEnd(); 92 | } 93 | }, timestamp); 94 | } 95 | 96 | public start (): void { 97 | this.countDown(50); 98 | loadImages(this.images, (loadedCount: number) => { 99 | this.haveLoadedCount = loadedCount; 100 | this.haveLoadedHalf = this.haveLoadedCount > this.images.length / 2; 101 | }).then(() => { 102 | this.haveLoaded = true; 103 | this.haveLoadedPercent = this.haveLoadedPercent < 90 ? 90 : this.haveLoadedPercent; 104 | }); 105 | } 106 | }; 107 | -------------------------------------------------------------------------------- /src/app/modules/logger.ts: -------------------------------------------------------------------------------- 1 | const Logger = class { 2 | // 3 | static readonly reportUrl: 'http://www.shuxia123.com'; 4 | 5 | static readonly supportMethods = ['log', 'info', 'warn', 'debug', 'error']; 6 | 7 | private reportUrl: string; 8 | private store: Array = []; 9 | 10 | constructor (reportUrl: string) { 11 | this.reportUrl = reportUrl; 12 | 13 | Logger.supportMethods.forEach((methodName) => { 14 | this[methodName] = (...params) => { 15 | const method = console[methodName]; 16 | method.apply(console, ...params); 17 | }; 18 | }) 19 | 20 | this.addErrorListener(); 21 | } 22 | 23 | /** 24 | * 添加错误日志上报 25 | */ 26 | addErrorListener () :void { 27 | window.onerror = (msg, url, line, col, error) => { 28 | 29 | }; 30 | } 31 | 32 | }; 33 | 34 | export default Logger; 35 | -------------------------------------------------------------------------------- /src/app/modules/mask.ts: -------------------------------------------------------------------------------- 1 | import { getAnimationEvent } from '@utils/device'; 2 | 3 | export default class Mask { 4 | private static $domMask: HTMLElement; 5 | private static readonly prefix = 'globalMask'; 6 | private static animationEvent = getAnimationEvent(); 7 | 8 | static open () { 9 | if (!Mask.$domMask) { 10 | Mask.$domMask = document.createElement('div'); 11 | Mask.$domMask.classList.add(Mask.prefix); 12 | Mask.$domMask.addEventListener(Mask.animationEvent, () => { 13 | if (Mask.$domMask.classList.contains('doOut')) { 14 | Mask.$domMask.classList.remove('doIn'); 15 | Mask.$domMask.classList.remove('doOut'); 16 | } 17 | }); 18 | document.body.append(Mask.$domMask); 19 | } 20 | Mask.$domMask.classList.add('doIn'); 21 | } 22 | 23 | static close () { 24 | Mask.$domMask.classList.add('doOut'); 25 | } 26 | } -------------------------------------------------------------------------------- /src/app/modules/proxy.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file app/modules/proxy.ts 双向绑定 3 | * @author: 王佳欣 4 | * @email: 1974740999@qq.com 5 | */ 6 | 7 | import { EMPTY_FUNCTION } from '../../constant'; 8 | 9 | interface Target { 10 | data: Object, 11 | key: string, 12 | callback?: Function 13 | } 14 | 15 | export default (target: Target): void => { 16 | const { data, key, callback = EMPTY_FUNCTION } = target; 17 | let value = data[key]; 18 | Object.defineProperty(data, key, { 19 | get () { 20 | return value; 21 | }, 22 | set (newValue) { 23 | if (value === newValue) { 24 | return; 25 | } 26 | 27 | callback(newValue, value); 28 | value = newValue; 29 | } 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /src/app/modules/router.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file utils/router.ts 路由模块 3 | * @author: 王佳欣 4 | * @email: 1974740999@qq.com 5 | */ 6 | 7 | import IS from '@utils/is'; 8 | import URI from '@utils/uri'; 9 | 10 | export default class Router { 11 | 12 | private static formatURL (url: any): string { 13 | let urlResult = ''; 14 | 15 | if (typeof url === 'string' && url.indexOf('http') === 0) { 16 | urlResult = url; 17 | } 18 | else if (typeof url === 'string') { 19 | const uri = URI.parse(window.location.href); 20 | const port = uri.port === 80 ? '' : `:${uri.port}`; 21 | const finalPath = uri.path === '' ? '' : `${uri.path}/`; 22 | const path = url.indexOf('/') === -1 ? finalPath : '' 23 | urlResult = `${document.location.protocol}//${uri.hostname}${port}/${path}${url}` 24 | } 25 | else { 26 | urlResult = URI.format(url); 27 | } 28 | 29 | return urlResult; 30 | } 31 | 32 | public static push (url: any): void { 33 | window.location.href = Router.formatURL(url); 34 | } 35 | 36 | public static replace (url: any) { 37 | window.location.replace(Router.formatURL(url)); 38 | } 39 | 40 | public static goBack () { 41 | // 如果有referrer来路,需要强制重新加载 42 | if (IS.isIos() && document.referrer) { 43 | Router.replace(document.referrer); 44 | } 45 | else { 46 | window.history.go(-1); 47 | } 48 | } 49 | 50 | public static reload () { 51 | window.location.reload(); 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /src/app/modules/share/browser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file app/modules/browser.ts browser 3 | * @author: 王佳欣 4 | * @email: 1974740999@qq.com 5 | */ 6 | import { ShareInfo } from '../../../interface'; 7 | import { getAnimationEvent } from '@utils/device'; 8 | import { isNodeFound } from '@utils/domHelper'; 9 | import Share from './share'; 10 | 11 | export default class Browser extends Share { 12 | static readonly prefix = 'globalShare'; 13 | private $domShare: HTMLElement; 14 | private $domWeibo: Node; 15 | private $domQzone: Node; 16 | private animationEvent: string; 17 | private haveOpend: boolean = false; 18 | private weiboUrl: string; 19 | private qzoneUrl: string; 20 | 21 | constructor (shareInfo: ShareInfo) { 22 | super(); 23 | this.setShareInfo(shareInfo); 24 | 25 | // rebind 26 | this.handleDestory = this.handleDestory.bind(this); 27 | this.handleOut = this.handleOut.bind(this); 28 | this.openShare = this.openShare.bind(this); 29 | 30 | this.animationEvent = getAnimationEvent(); 31 | } 32 | 33 | public setShareInfo (shareInfo: ShareInfo) { 34 | this.weiboUrl = encodeURI(`http://v.t.sina.com.cn/share/share.php?` + 35 | `url=${shareInfo.link}&title=${shareInfo.title}/${shareInfo.desc}&` + 36 | `charset=utf-8&pic=${shareInfo.imgUrl}&searchPic=true`); 37 | 38 | this.qzoneUrl = encodeURI(`http://sns.qzone.qq.com/cgi-bin/qzshare/cgi_qzshare_onekey` + 39 | `?url=${shareInfo.link}&title=${shareInfo.title}&desc=${shareInfo.desc}`+ 40 | `&charset=utf-8&pics=${shareInfo.imgUrl}&site=${shareInfo.link}&otype=share`); 41 | } 42 | 43 | public callShare () { 44 | if (this.haveOpend) { 45 | return; 46 | } 47 | 48 | this.haveOpend = true; 49 | 50 | if (!this.$domShare) { 51 | this.createShareDom(); 52 | } else { 53 | this.$domShare.classList.remove('doHide'); 54 | } 55 | 56 | // bindEvents in events, to avoid call bind fn immediately. 57 | setTimeout(() => { 58 | this.bindEvents(); 59 | }, 0); 60 | } 61 | 62 | private createShareDom () { 63 | this.$domShare = document.createElement('div'); 64 | this.$domShare.classList.add(Browser.prefix); 65 | this.$domShare.classList.add(`${Browser.prefix}—browser`); 66 | this.$domShare.innerHTML = `
67 |

#分享价值#

68 |
    69 |
  • 70 |
    微信
    71 |
    72 | 73 |
    74 |
  • 75 |
  • 76 |
    微博
    77 |
  • 78 |
  • 79 |
    QQ空间
    80 |
  • 81 |
82 | `; 83 | 84 | document.body.appendChild(this.$domShare); 85 | this.$domWeibo = document.querySelector(`.${Browser.prefix}-icon--wb`); 86 | this.$domQzone = document.querySelector(`.${Browser.prefix}-icon--qzone`); 87 | } 88 | 89 | private openShare (e: any) { 90 | window.open(e.currentTarget === this.$domWeibo ? this.weiboUrl : this.qzoneUrl); 91 | } 92 | 93 | private bindEvents () { 94 | document.addEventListener('click', this.handleOut); 95 | this.$domShare.addEventListener(this.animationEvent, this.handleDestory); 96 | this.$domWeibo.addEventListener('click', this.openShare); 97 | this.$domQzone.addEventListener('click', this.openShare); 98 | } 99 | 100 | private unbindEvents () { 101 | document.removeEventListener('click', this.handleOut); 102 | this.$domShare.removeEventListener(this.animationEvent, this.handleDestory); 103 | this.$domWeibo.removeEventListener('click', this.openShare); 104 | this.$domQzone.removeEventListener('click', this.openShare); 105 | } 106 | 107 | private handleOut (e: any) { 108 | if (!isNodeFound(e.target, this.$domShare)) { 109 | this.$domShare.classList.add('doOut'); 110 | } 111 | } 112 | 113 | private handleDestory () { 114 | if (this.$domShare.classList.contains('doOut')) { 115 | this.haveOpend = false; 116 | this.unbindEvents(); 117 | 118 | // remove dom 119 | this.$domShare.classList.add('doHide'); 120 | this.$domShare.classList.remove('doOut'); 121 | } 122 | } 123 | } -------------------------------------------------------------------------------- /src/app/modules/share/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file app/share/index.ts share 3 | * @author: 王佳欣 4 | * @email: 1974740999@qq.com 5 | */ 6 | import { ShareInfo } from '../../../interface'; 7 | import Wechat from './wechat'; 8 | import QQ from './qq'; 9 | import Browser from './browser'; 10 | import IS from '@utils/is'; 11 | import Share from './share'; 12 | 13 | 14 | export default class Index { 15 | private shareInfo: ShareInfo; 16 | private platform: Share; 17 | 18 | static readonly defaultShareInfo: ShareInfo = { 19 | title: document.title, 20 | desc: '', 21 | link: window.location.href, 22 | imgUrl: '' 23 | }; 24 | 25 | constructor (tokenUrl?: string, appId?: string, shareInfo?: ShareInfo) { 26 | shareInfo = { ...Index.defaultShareInfo, ...shareInfo }; 27 | 28 | if (IS.isWechat()) { 29 | this.platform = new Wechat(tokenUrl, shareInfo); 30 | } 31 | else if (IS.isQQ()) { 32 | this.platform = new QQ(appId, shareInfo); 33 | } 34 | else { 35 | this.platform = new Browser(shareInfo); 36 | } 37 | } 38 | 39 | public setShareInfo (shareInfo: ShareInfo) { 40 | shareInfo = { ...Index.defaultShareInfo, ...shareInfo }; 41 | this.platform.setShareInfo(shareInfo); 42 | } 43 | 44 | public callShare () { 45 | this.platform.callShare(); 46 | } 47 | } -------------------------------------------------------------------------------- /src/app/modules/share/qq.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file utils/ajax.ts qq分享 3 | * @author: 王佳欣 4 | * @email: 1974740999@qq.com 5 | */ 6 | import { ShareInfo } from '../../../interface'; 7 | import { loadScript } from '@utils/index'; 8 | import Share from './share'; 9 | 10 | // window property 11 | declare global { 12 | interface Window { 13 | __mqq__: any 14 | } 15 | }; 16 | 17 | export default class QQ extends Share { 18 | private shareInfo: ShareInfo; 19 | private qq: any; 20 | constructor (appId: string, shareInfo: ShareInfo) { 21 | super(); 22 | this.shareInfo = shareInfo; 23 | this.loadQQ(appId).then(() => { 24 | this.setShareInfo(this.shareInfo); 25 | }); 26 | } 27 | 28 | private async loadQQ (appId: string) { 29 | await loadScript (`//open.mobile.qq.com/sdk/qqapi.js?_bid=${appId}`); 30 | this.qq = window.__mqq__; 31 | } 32 | 33 | public setShareInfo (shareInfo: ShareInfo) { 34 | let {title, desc, link, imgUrl} = shareInfo; 35 | this.qq.data.setShareInfo({ 36 | title, 37 | desc, 38 | share_url: link, 39 | image_url: imgUrl, 40 | }); 41 | } 42 | 43 | callShare () { 44 | this.qq.ui.showShareMenu(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/app/modules/share/share.ts: -------------------------------------------------------------------------------- 1 | import { ShareInfo } from '../../../interface'; 2 | 3 | export default abstract class Share { 4 | abstract callShare (): any; 5 | 6 | abstract setShareInfo (shareInfo: ShareInfo): any; 7 | } -------------------------------------------------------------------------------- /src/app/modules/share/wechat.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file app/modules/wechat.ts wechat 3 | * @author: 王佳欣 4 | * @email: 1974740999@qq.com 5 | */ 6 | 7 | import { ShareInfo } from '../../../interface'; 8 | import { getAnimationEvent } from '@utils/device'; 9 | import { loadScript } from '@utils/index'; 10 | import { ajax, jsonp } from '../ajax'; 11 | import Share from './share'; 12 | 13 | // window property 14 | declare global { 15 | interface Window { 16 | wx: any 17 | } 18 | }; 19 | // declare let window: any; 20 | export default class WeChat extends Share { 21 | static readonly prefix = 'globalShare'; 22 | private $domShare: HTMLElement; 23 | private wechat: any; 24 | private shareInfo: ShareInfo; 25 | private animationEvent: string; 26 | private created: boolean = false; 27 | 28 | constructor (tokenUrl: string, shareInfo: ShareInfo) { 29 | super(); 30 | this.shareInfo = shareInfo; 31 | this.animationEvent = getAnimationEvent(); 32 | 33 | // rebind 34 | this.handleDestory = this.handleDestory.bind(this); 35 | this.handleOut = this.handleOut.bind(this); 36 | 37 | this.handleLoadSignature(tokenUrl).then(({appId, timestamp, nonceStr, signature}) => { 38 | this.setConfig({appId, timestamp, nonceStr, signature}); 39 | this.setShareInfo(this.shareInfo); 40 | }); 41 | } 42 | 43 | private async handleLoadSignature (tokenUrl: string) { 44 | // load wechat sdk 45 | await loadScript ('//res.wx.qq.com/open/js/jweixin-1.2.0.js'); 46 | this.wechat = window.wx; 47 | 48 | // request wechat token 49 | const result = await jsonp({ 50 | url: tokenUrl, 51 | dataType: 'json', 52 | data: { 53 | url: location.href, 54 | t: new Date().getTime() 55 | }, 56 | xhrFields: {withCredentials: true}, 57 | }); 58 | 59 | return result; 60 | } 61 | 62 | private setConfig ({appId, timestamp, nonceStr, signature}): void { 63 | this.wechat.config({ 64 | debug: false, 65 | appId, 66 | timestamp, 67 | nonceStr, 68 | signature, 69 | jsApiList: ['onMenuShareTimeline', 'onMenuShareAppMessage', 'onMenuShareQQ', 70 | 'onMenuShareWeibo','onMenuShareQZone','closeWindow','getLocation', 71 | 'openLocation', 'getLocation', 'chooseImage', 'previewImage', 'downloadImage'] 72 | }); 73 | } 74 | 75 | public setShareInfo (shareInfo: ShareInfo): void { 76 | this.wechat.error(function (error: any) { 77 | console.log(error); 78 | }); 79 | 80 | this.wechat.ready(() => { 81 | ['onMenuShareTimeline', 'onMenuShareAppMessage', 'onMenuShareQQ', 82 | 'onMenuShareWeibo', 'onMenuShareQZone'].map((platItem) => { 83 | this.wechat[platItem]({ 84 | ... shareInfo, 85 | trigger (e) { 86 | }, 87 | success(e) { 88 | }, 89 | fail() { 90 | }, 91 | cancel() { 92 | } 93 | }); 94 | }); 95 | }); 96 | } 97 | 98 | public previewImage (images: Array, currentIndex: number): void { 99 | this.wechat.previewImage({ 100 | images, 101 | current: images[currentIndex] 102 | }); 103 | } 104 | 105 | public closeWindow (): void { 106 | this.wechat.closeWindow(); 107 | } 108 | 109 | public callShare () { 110 | if (this.created) { 111 | return; 112 | } 113 | this.created = true; 114 | // create dom 115 | this.$domShare = document.createElement('div'); 116 | this.$domShare.classList.add(WeChat.prefix); 117 | this.$domShare.classList.add(`${WeChat.prefix}—wechat`); 118 | this.bindEvents(); 119 | document.body.appendChild(this.$domShare); 120 | } 121 | 122 | private bindEvents () { 123 | this.$domShare.addEventListener('click', this.handleOut); 124 | this.$domShare.addEventListener(this.animationEvent, this.handleDestory); 125 | } 126 | 127 | private unbindEvents () { 128 | this.$domShare.removeEventListener('click', this.handleOut); 129 | this.$domShare.removeEventListener(this.animationEvent, this.handleDestory); 130 | } 131 | 132 | private handleOut () { 133 | this.$domShare.classList.add('doOut'); 134 | } 135 | 136 | private handleDestory () { 137 | if (this.$domShare.classList.contains('doOut')) { 138 | this.created = false; 139 | this.unbindEvents(); 140 | 141 | // remove dom 142 | document.body.removeChild(this.$domShare); 143 | } 144 | } 145 | }; 146 | -------------------------------------------------------------------------------- /src/app/modules/statistic.ts: -------------------------------------------------------------------------------- 1 | export default class Statistic { 2 | trackPage (): void { 3 | 4 | } 5 | trackEvent (): void { 6 | 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/app/modules/storage.ts: -------------------------------------------------------------------------------- 1 | class Storage { 2 | // `prefix`: 缓存前缀, 实现namespace的功能, 防止重名 3 | static readonly prefix: string = '__'; 4 | 5 | // `splitCode`: 过期时间 和 缓存值之间的切割符, 如: '60000|_|abc' 6 | static readonly splitCode: string = '|_|'; 7 | 8 | // storage support 9 | static readonly storage = localStorage || window.localStorage; 10 | 11 | // `defaultExpiredTime`: 默认30天过期 12 | static readonly defaultExpiredTime = 30 * 24 * 60 * 60 * 1000; 13 | 14 | private prefix: string; 15 | 16 | private splitCode: string; 17 | 18 | constructor (prefix?: string, splitCode?: string) { 19 | this.prefix = prefix || Storage.prefix; 20 | this.splitCode = splitCode || Storage.splitCode; 21 | } 22 | 23 | /** 24 | * 获取缓存key 25 | * @param key 26 | */ 27 | private getStorageKey (key:string): string { 28 | return `${this.prefix}${key}`; 29 | } 30 | 31 | /** 32 | * 设置缓存值 33 | * @param key, storage 的 key 34 | * @param value, storage 的 value 35 | * @param expired, storage 的过期时间 36 | */ 37 | public setItem (key: string, value: any, expired: number = Storage.defaultExpiredTime): void { 38 | const itemKey = this.getStorageKey(key); 39 | const date = new Date(); 40 | const expiredTimestamp = date.getTime() + expired; 41 | Storage.storage.setItem(itemKey, `${expiredTimestamp}${this.splitCode}${value}`); 42 | } 43 | 44 | /** 45 | * 获取缓存值 46 | * @param key, 缓存的key 47 | */ 48 | public getItem (key: string): string { 49 | const itemKey = this.getStorageKey(key); 50 | const value = Storage.storage.getItem(itemKey); 51 | 52 | if (value) { 53 | const localValueSplits = value.split(this.splitCode); 54 | const currentTimestamp = (new Date()).getTime(); 55 | if (+localValueSplits[0] > +(new Date())) { 56 | return localValueSplits[1]; 57 | } 58 | // expired 59 | this.removeItem(key); 60 | } 61 | 62 | return null; 63 | } 64 | 65 | /** 66 | * 删除缓存 67 | * @param key, 缓存的key 68 | */ 69 | public removeItem (key: string): void { 70 | const itemKey = this.getStorageKey(key); 71 | Storage.storage.removeItem(itemKey); 72 | } 73 | } 74 | 75 | export default Storage; -------------------------------------------------------------------------------- /src/app/modules/tip.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file modules/tip.ts 提示模块 3 | * @author: 王佳欣 4 | * @email: 1974740999@qq.com 5 | */ 6 | 7 | import { EMPTY_FUNCTION } from '../../constant'; 8 | import { getAnimationEvent } from '@utils/device'; 9 | import { TipOptions } from 'interface'; 10 | 11 | export default class Tip { 12 | static readonly defaultOptions: TipOptions = { 13 | message: '', 14 | duration: 3000, 15 | theme: 'default', 16 | closable: false, 17 | callback: EMPTY_FUNCTION 18 | }; 19 | static readonly prefix = 'globalTip'; 20 | 21 | private timeId: any; 22 | private animationEvent: string; 23 | private $dom: HTMLElement; 24 | private $domClosable: HTMLElement; 25 | 26 | constructor (options: TipOptions) { 27 | options = { ...Tip.defaultOptions, ...options }; 28 | this.createElement(options); 29 | 30 | // rebind 31 | this.handleTransitionEnd = this.handleTransitionEnd.bind(this); 32 | this.handleCloseClick = this.handleCloseClick.bind(this); 33 | this.animationEvent = getAnimationEvent(); 34 | this.bindEvents(); 35 | 36 | if (options.duration) { 37 | this.timeId = setTimeout(() => { 38 | this.close(); 39 | }, options.duration); 40 | } 41 | } 42 | 43 | private createElement (options: TipOptions) { 44 | this.$dom = document.createElement('div'); 45 | this.$dom.classList.add(Tip.prefix); 46 | this.$dom.classList.add(`${Tip.prefix}--${options.theme}`); 47 | 48 | // create content dom 49 | const $domContent = document.createElement('div'); 50 | $domContent.classList.add(`${Tip.prefix}-content`); 51 | $domContent.innerHTML = options.message; 52 | this.$dom.append($domContent); 53 | 54 | // create close dom 55 | if (options.closable) { 56 | this.$dom.classList.add(`${Tip.prefix}--closable`); 57 | this.$domClosable = document.createElement('i'); 58 | this.$domClosable.classList.add(`${Tip.prefix}-close`); 59 | $domContent.append(this.$domClosable); 60 | } 61 | 62 | document.body.appendChild(this.$dom); 63 | } 64 | 65 | private bindEvents () { 66 | this.$dom.addEventListener(this.animationEvent, this.handleTransitionEnd); 67 | if (this.$domClosable) { 68 | this.$domClosable.addEventListener('click', this.handleCloseClick); 69 | } 70 | } 71 | 72 | private unbindEvents () { 73 | this.$dom.removeEventListener(this.animationEvent, this.handleTransitionEnd); 74 | if (this.$domClosable) { 75 | this.$domClosable.removeEventListener('click', this.handleCloseClick); 76 | } 77 | } 78 | 79 | private destory () { 80 | // clear timer 81 | if (this.timeId) { 82 | clearTimeout(this.timeId); 83 | this.timeId = null; 84 | } 85 | 86 | this.unbindEvents(); 87 | document.body.removeChild(this.$dom); 88 | } 89 | 90 | private handleTransitionEnd (e: any) { 91 | if (e.target === e.currentTarget && this.$dom.classList.contains('doOut')) { 92 | this.destory(); 93 | } 94 | } 95 | 96 | private handleCloseClick () { 97 | this.close(); 98 | } 99 | 100 | public close () { 101 | this.$dom.classList.add('doOut'); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/assets/images/global/icon-close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coocssweb/mpa-typescript/32acb45a45fef97f2315f0b454b0fe8120c8983e/src/assets/images/global/icon-close.png -------------------------------------------------------------------------------- /src/assets/images/global/share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coocssweb/mpa-typescript/32acb45a45fef97f2315f0b454b0fe8120c8983e/src/assets/images/global/share.png -------------------------------------------------------------------------------- /src/assets/images/global/sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coocssweb/mpa-typescript/32acb45a45fef97f2315f0b454b0fe8120c8983e/src/assets/images/global/sprite.png -------------------------------------------------------------------------------- /src/assets/scss/base/animation.scss: -------------------------------------------------------------------------------- 1 | @keyframes fadeIn { 2 | 0%{ 3 | opacity: 0; 4 | } 5 | 100%{ 6 | opacity: 1; 7 | } 8 | } 9 | 10 | @keyframes fadeOut { 11 | 0%{ 12 | opacity: 1; 13 | } 14 | 100%{ 15 | opacity: 0; 16 | } 17 | } 18 | 19 | @keyframes slideInRight { 20 | 0%{ 21 | transform: translate3d(100%, 0, 0); 22 | } 23 | 100%{ 24 | transform: translate3d(0, 0, 0); 25 | } 26 | } 27 | 28 | @keyframes slideOutRight { 29 | 0%{ 30 | transform: translate3d(0, 0, 0); 31 | } 32 | 100%{ 33 | transform: translate3d(100, 0, 0); 34 | } 35 | } 36 | 37 | @keyframes rollOneTurn { 38 | 0% { 39 | transform: rotate(0deg) 40 | } 41 | to { 42 | transform: rotate(1turn) 43 | } 44 | } -------------------------------------------------------------------------------- /src/assets/scss/base/button.scss: -------------------------------------------------------------------------------- 1 | .globalBtn{ 2 | position: relative; 3 | display: inline-block; 4 | text-align: center; 5 | cursor: pointer; 6 | background-image: none; 7 | border: none; 8 | border-radius: 4PX; 9 | outline: 0; 10 | transition: all 300ms; 11 | padding: 10PX 25PX; 12 | text-align: center; 13 | border-width: 1PX; 14 | border-style: solid; 15 | 16 | &:hover{ 17 | opacity: .8; 18 | } 19 | 20 | &-primary{ 21 | background-color: #0070c9; 22 | background: linear-gradient(#42a1ec,#0070c9); 23 | color: #ffffff; 24 | border-color: #07c; 25 | &.disable { 26 | opacity: .3; 27 | &:hover{ 28 | opacity: 0.3; 29 | } 30 | } 31 | } 32 | &-transparent { 33 | color: #0070c9; 34 | border-color: transparent; 35 | &:hover{ 36 | text-decoration: underline; 37 | } 38 | &.disable { 39 | opacity: .3; 40 | &:hover{ 41 | opacity: 0.3; 42 | } 43 | } 44 | } 45 | &--small{ 46 | font-size: $fontSizeL; 47 | padding: 8PX 0PX; 48 | } 49 | &--large{ 50 | font-size: $fontSizeL; 51 | padding: 13PX; 52 | } 53 | } -------------------------------------------------------------------------------- /src/assets/scss/base/common.scss: -------------------------------------------------------------------------------- 1 | @import "./normalize"; 2 | @import "./variables"; 3 | @import "./button"; 4 | @import "./animation"; 5 | @import "./globalMask"; 6 | @import "./globalTip"; 7 | @import "./globalConfirm"; 8 | @import "./globalShare"; 9 | @import "./globalHeader"; 10 | @import "./globalFooter"; 11 | 12 | html, body{ 13 | background: transparent; 14 | -webkit-overflow-scrolling: touch; 15 | height: 100%; 16 | } 17 | 18 | body { 19 | font-family: Microsoft YaHei,Helvetica Neue,HelveticaNeue,Helvetica,TeXGyreHeros,FreeSans,Nimbus Sans L,Liberation Sans,Arial,sans-serif; 20 | font-size: 14PX; 21 | text-rendering: optimizeLegibility; 22 | -webkit-font-smoothing: antialiased; 23 | -moz-osx-font-smoothing: grayscale; 24 | background-color: #ffffff; 25 | } 26 | 27 | #app{ 28 | position: relative; 29 | } 30 | 31 | dd, dl, dt, p, nav, div, ul,li, header, h1, h2, h3, h4, h5, h6,iframe, button{ 32 | margin: 0px; 33 | padding: 0px; 34 | display: block; 35 | } 36 | h2{ 37 | font-weight: normal; 38 | } 39 | input{ 40 | -webkit-appearance: none; 41 | border-radius: 0; 42 | } 43 | a{ 44 | text-decoration: none; 45 | -webkit-tap-highlight-color: rgba(255,255,255,0); 46 | outline: none; 47 | } 48 | 49 | button, div{ 50 | -webkit-tap-highlight-color: rgba(255,255,255,0); 51 | } 52 | 53 | 54 | ul, li, h3{ 55 | list-style: none; 56 | -webkit-tap-highlight-color: rgba(255,255,255,0); 57 | } 58 | 59 | ::-webkit-input-placeholder{ 60 | color: #c0c0c0; 61 | font-size: 14PX; 62 | } 63 | ::-moz-placeholder{ 64 | color: #c0c0c0; 65 | font-size: 14PX; 66 | } 67 | :-ms-input-placeholder{ 68 | color: #c0c0c0; 69 | font-size: 14PX; 70 | } 71 | 72 | input:disabled { 73 | color: #555555; 74 | } 75 | 76 | ::selection { 77 | background-color: #0087cd; 78 | color: #fff; 79 | } 80 | ::selection { 81 | background-color: #0087cd; 82 | color: #fff; 83 | } 84 | 85 | .clearfix:after { 86 | content: "."; 87 | display: block; 88 | height: 0; 89 | line-height: 0; 90 | clear: both; 91 | visibility: hidden; 92 | } 93 | -------------------------------------------------------------------------------- /src/assets/scss/base/globalConfirm.scss: -------------------------------------------------------------------------------- 1 | .globalConfirm{ 2 | position: fixed; 3 | top: 50%; 4 | left: 50%; 5 | background-color: #ffffff; 6 | padding: 50PX 50PX 30PX 50PX; 7 | box-sizing: border-box; 8 | z-index: 101; 9 | animation: fadeIn .3s ease forwards; 10 | &.doOut{ 11 | animation: fadeOut .3s ease forwards; 12 | } 13 | &-header{ 14 | position: relative 15 | } 16 | &-title{ 17 | font-size: 25PX; 18 | color: #000000; 19 | text-align: center; 20 | margin-bottom: 30PX; 21 | } 22 | &-close{ 23 | position: absolute; 24 | right: 20PX; 25 | top: 20PX; 26 | height: 30PX; 27 | width: 30PX; 28 | border-radius: 15PX; 29 | background-repeat: no-repeat; 30 | background-position: center; 31 | cursor: pointer; 32 | background-image: url(../../images/global/icon-close.png); 33 | &:hover{ 34 | opacity: .8; 35 | } 36 | } 37 | &-content{ 38 | line-height: 1.5; 39 | } 40 | &-footer{ 41 | border-top: 1PX solid #f3f3f3; 42 | margin-top: 30PX; 43 | } 44 | 45 | &-btn{ 46 | width: 100%; 47 | padding-left: 0px; 48 | padding-right: 0px; 49 | @extend .globalBtn; 50 | @extend .globalBtn--large; 51 | &--ok{ 52 | @extend .globalBtn-primary; 53 | 54 | } 55 | &--cancel{ 56 | @extend .globalBtn-transparent; 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /src/assets/scss/base/globalFooter.scss: -------------------------------------------------------------------------------- 1 | .globalFooter{ 2 | border-top: 1PX solid #eeefef; 3 | padding: 10PX 0px; 4 | &-wrapper{ 5 | max-width: $globalWidth; 6 | margin: 0px auto; 7 | height: 36PX; 8 | line-height: 36PX; 9 | padding: 0px 20PX; 10 | } 11 | a{ 12 | color: #999999; 13 | margin-right: 20PX; 14 | &:hover{ 15 | text-decoration: underline; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/assets/scss/base/globalHeader.scss: -------------------------------------------------------------------------------- 1 | .globalHeader{ 2 | border-bottom: 1PX solid #eeefef; 3 | padding: 10PX 0px; 4 | &-wrapper{ 5 | max-width: $globalWidth; 6 | margin: 0px auto; 7 | height: 36PX; 8 | line-height: 36PX; 9 | padding: 0px 20PX; 10 | } 11 | &-logo{ 12 | color: #FF5A5F; 13 | font-size: 20PX; 14 | } 15 | } 16 | 17 | .globalLocation{ 18 | &-wrapper{ 19 | max-width: $globalWidth; 20 | margin: 0px auto; 21 | padding: 20PX 0; 22 | } 23 | &-item{ 24 | font-size: $fontSizeM; 25 | color: $colorGrayDark; 26 | &--link{ 27 | color: $colorGrayDarker; 28 | &:hover{ 29 | text-decoration: underline; 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/assets/scss/base/globalLoading.scss: -------------------------------------------------------------------------------- 1 | .globalLoading{ 2 | position: fixed; 3 | top: 0; 4 | right: 0; 5 | bottom: 0; 6 | left: 0; 7 | background-color: #000; 8 | &.doOut{ 9 | animation: fadeOut 1s ease forwards; 10 | } 11 | &-container{ 12 | position: absolute; 13 | top: 50%; 14 | left: 50%; 15 | margin-left: -30px; 16 | margin-top: -50px; 17 | } 18 | &-box{ 19 | width: 60PX; 20 | height: 60PX; 21 | margin: 0px auto; 22 | animation: loadingScaleIn 1s ease forwards; 23 | &.doOut{ 24 | animation: loadingScaleOut 1s ease forwards; 25 | } 26 | @keyframes loadingScaleIn { 27 | 0%{ 28 | transform: scale(0.5); 29 | } 30 | 100%{ 31 | transform: scale(1); 32 | } 33 | } 34 | @keyframes loadingScaleOut { 35 | 0%{ 36 | transform: scale(1); 37 | } 38 | 100%{ 39 | transform: scale(.5); 40 | } 41 | } 42 | } 43 | &-icon{ 44 | display: block; 45 | width: 60PX; 46 | height: 60PX; 47 | animation: rollOneTurn 3s linear infinite; 48 | } 49 | &-value{ 50 | text-align: center; 51 | color: #b8aa79; 52 | font-size: 14PX; 53 | margin-top: 10PX; 54 | } 55 | } -------------------------------------------------------------------------------- /src/assets/scss/base/globalMask.scss: -------------------------------------------------------------------------------- 1 | .globalMask { 2 | position: fixed; 3 | top: 0; 4 | right: 0; 5 | bottom: 0; 6 | left: 0; 7 | background-color: rgba(50, 50, 50, .88); 8 | z-index: 100; 9 | display: none; 10 | &.doIn{ 11 | display: block; 12 | animation: fadeIn .3s ease forwards; 13 | } 14 | &.doOut{ 15 | animation: fadeOut .3s ease forwards; 16 | } 17 | } -------------------------------------------------------------------------------- /src/assets/scss/base/globalShare.scss: -------------------------------------------------------------------------------- 1 | 2 | .globalShare{ 3 | position: fixed; 4 | z-index: 1000; 5 | top: 0; 6 | right: 0; 7 | bottom: 0; 8 | background-color: rgba(255, 255, 255, 1); 9 | width: 300PX; 10 | transform: translate3d(100%, 0, 0); 11 | animation: slideInRight .3s linear forwards; 12 | box-shadow: -1PX 0PX 10PX rgba(0, 0, 0, 0.1); 13 | &.doOut{ 14 | animation: slideOutRight .5s ease-in-out forwards; 15 | } 16 | &.doHide{ 17 | display: none; 18 | } 19 | 20 | &-inner{ 21 | position: absolute; 22 | top: 50%; 23 | left: 80PX; 24 | width: 140PX; 25 | height: 420PX; 26 | margin-top: -150PX; 27 | } 28 | &-title{ 29 | font-size: 16PX; 30 | color: #989896; 31 | font-weight: normal; 32 | } 33 | &-list{ 34 | margin-top: 20PX; 35 | } 36 | &-item{ 37 | position: relative; 38 | padding-top: 15PX; 39 | cursor: pointer; 40 | &:hover{ 41 | .globalShare{ 42 | &-icon{ 43 | color: #df3846; 44 | &--wx{ 45 | i{ 46 | background-position: -2PX -33PX 47 | } 48 | } 49 | &--wb{ 50 | i{ 51 | background-position: -32PX -33PX 52 | } 53 | } 54 | &--qzone{ 55 | i{ 56 | background-position: -62PX -33PX 57 | } 58 | } 59 | } 60 | &-qrcode{ 61 | display: block; 62 | } 63 | } 64 | } 65 | } 66 | &-icon{ 67 | font-size: 16PX; 68 | line-height: 28PX; 69 | color: #2d2f28; 70 | text-indent: 20PX; 71 | i{ 72 | display: block; 73 | float: left; 74 | width: 28PX; 75 | height: 28PX; 76 | background: url(../../images/global/sprite.png) no-repeat; 77 | } 78 | &--wx{ 79 | i{ 80 | background-position: -2PX -3PX; 81 | } 82 | } 83 | &--wb{ 84 | i{ 85 | background-position: -32PX -3PX; 86 | } 87 | } 88 | &--qzone{ 89 | i{ 90 | background-position: -62PX -3PX; 91 | } 92 | } 93 | } 94 | 95 | &-qrcode{ 96 | display: none; 97 | img{ 98 | display: block; 99 | width: 150px; 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /src/assets/scss/base/globalTip.scss: -------------------------------------------------------------------------------- 1 | @keyframes tipIn { 2 | 0%{ 3 | opacity: .5; 4 | transform: translateY(-5PX) translateZ(0); 5 | } 6 | 100%{ 7 | opacity: 1; 8 | transform: translateY(0) translateZ(0); 9 | } 10 | } 11 | 12 | @keyframes tipOut { 13 | 0%{ 14 | opacity: 1; 15 | transform: translateY(0) translateZ(0); 16 | } 17 | 100%{ 18 | opacity: 0; 19 | transform: translateY(0)translateZ(0); 20 | } 21 | } 22 | 23 | .globalTip{ 24 | position: fixed; 25 | width: 100%; 26 | text-align: center; 27 | top: 50PX; 28 | left: 0PX; 29 | opacity: 0; 30 | animation: tipIn .3s ease-out forwards; 31 | z-index: 100; 32 | &-content{ 33 | display: inline-block; 34 | padding: $paddingSizeL $paddingSizeXL; 35 | font-size: $fontSizeL; 36 | border-radius: $borderRadiusS; 37 | position: relative; 38 | z-index: 1; 39 | } 40 | 41 | &-close{ 42 | position: absolute; 43 | top: 50%; 44 | right: 10PX; 45 | height: 26PX; 46 | width: 26PX; 47 | border-radius: 13PX; 48 | background-image: url(../../images/global/icon-close.png); 49 | background-size: 12PX 12PX; 50 | background-position: center; 51 | background-repeat: no-repeat; 52 | margin-top: -12PX; 53 | cursor: pointer; 54 | transition: background-color .3s ease; 55 | &:active{ 56 | background-color: rgba(255, 255, 255, .3); 57 | } 58 | } 59 | 60 | &--closable{ 61 | .globalTip-content{ 62 | padding-right: 3 * $paddingSizeXL; 63 | } 64 | } 65 | 66 | &--default{ 67 | .globalTip{ 68 | &-content{ 69 | box-shadow: 0px 2PX 3PX rgba(0, 0, 0, .4); 70 | background-color: $colorDefault; 71 | color: #ffffff; 72 | } 73 | } 74 | } 75 | 76 | &--primary{ 77 | .globalTip{ 78 | &-content{ 79 | box-shadow: 0px 2PX 3PX rgba(0, 0, 0, .4); 80 | background-color: $colorPrimary; 81 | color: #ffffff; 82 | } 83 | } 84 | } 85 | 86 | &.doOut{ 87 | animation: tipOut .5s ease-out forwards; 88 | } 89 | } -------------------------------------------------------------------------------- /src/assets/scss/base/normalize.scss: -------------------------------------------------------------------------------- 1 | /*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in 9 | * IE on Windows Phone and in iOS. 10 | */ 11 | 12 | html { 13 | line-height: 1.15; /* 1 */ 14 | -ms-text-size-adjust: 100%; /* 2 */ 15 | -webkit-text-size-adjust: 100%; /* 2 */ 16 | } 17 | 18 | /* Sections 19 | ========================================================================== */ 20 | 21 | /** 22 | * Remove the margin in all browsers (opinionated). 23 | */ 24 | 25 | body { 26 | margin: 0; 27 | } 28 | 29 | /** 30 | * Add the correct display in IE 9-. 31 | */ 32 | 33 | article, 34 | aside, 35 | footer, 36 | header, 37 | nav, 38 | section { 39 | display: block; 40 | } 41 | 42 | /** 43 | * Correct the font size and margin on `h1` elements within `section` and 44 | * `article` contexts in Chrome, Firefox, and Safari. 45 | */ 46 | 47 | h1 { 48 | font-size: 2em; 49 | margin: 0.67em 0; 50 | } 51 | 52 | /* Grouping content 53 | ========================================================================== */ 54 | 55 | /** 56 | * Add the correct display in IE 9-. 57 | * 1. Add the correct display in IE. 58 | */ 59 | 60 | figcaption, 61 | figure, 62 | main { /* 1 */ 63 | display: block; 64 | } 65 | 66 | /** 67 | * Add the correct margin in IE 8. 68 | */ 69 | 70 | figure { 71 | margin: 1em 40px; 72 | } 73 | 74 | /** 75 | * 1. Add the correct box sizing in Firefox. 76 | * 2. Show the overflow in Edge and IE. 77 | */ 78 | 79 | hr { 80 | box-sizing: content-box; /* 1 */ 81 | height: 0; /* 1 */ 82 | overflow: visible; /* 2 */ 83 | } 84 | 85 | /** 86 | * 1. Correct the inheritance and scaling of font size in all browsers. 87 | * 2. Correct the odd `em` font sizing in all browsers. 88 | */ 89 | 90 | pre { 91 | font-family: monospace, monospace; /* 1 */ 92 | font-size: 1em; /* 2 */ 93 | } 94 | 95 | /* Text-level semantics 96 | ========================================================================== */ 97 | 98 | /** 99 | * 1. Remove the gray background on active links in IE 10. 100 | * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. 101 | */ 102 | 103 | a { 104 | background-color: transparent; /* 1 */ 105 | -webkit-text-decoration-skip: objects; /* 2 */ 106 | } 107 | 108 | /** 109 | * 1. Remove the bottom border in Chrome 57- and Firefox 39-. 110 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 111 | */ 112 | 113 | abbr[title] { 114 | border-bottom: none; /* 1 */ 115 | text-decoration: underline; /* 2 */ 116 | text-decoration: underline dotted; /* 2 */ 117 | } 118 | 119 | /** 120 | * Prevent the duplicate application of `bolder` by the next rule in Safari 6. 121 | */ 122 | 123 | b, 124 | strong { 125 | font-weight: inherit; 126 | } 127 | 128 | /** 129 | * Add the correct font weight in Chrome, Edge, and Safari. 130 | */ 131 | 132 | b, 133 | strong { 134 | font-weight: bolder; 135 | } 136 | 137 | /** 138 | * 1. Correct the inheritance and scaling of font size in all browsers. 139 | * 2. Correct the odd `em` font sizing in all browsers. 140 | */ 141 | 142 | code, 143 | kbd, 144 | samp { 145 | font-family: monospace, monospace; /* 1 */ 146 | font-size: 1em; /* 2 */ 147 | } 148 | 149 | /** 150 | * Add the correct font style in Android 4.3-. 151 | */ 152 | 153 | dfn { 154 | font-style: italic; 155 | } 156 | 157 | /** 158 | * Add the correct background and color in IE 9-. 159 | */ 160 | 161 | mark { 162 | background-color: #ff0; 163 | color: #000; 164 | } 165 | 166 | /** 167 | * Add the correct font size in all browsers. 168 | */ 169 | 170 | small { 171 | font-size: 80%; 172 | } 173 | 174 | /** 175 | * Prevent `sub` and `sup` elements from affecting the line height in 176 | * all browsers. 177 | */ 178 | 179 | sub, 180 | sup { 181 | font-size: 75%; 182 | line-height: 0; 183 | position: relative; 184 | vertical-align: baseline; 185 | } 186 | 187 | sub { 188 | bottom: -0.25em; 189 | } 190 | 191 | sup { 192 | top: -0.5em; 193 | } 194 | 195 | /* Embedded content 196 | ========================================================================== */ 197 | 198 | /** 199 | * Add the correct display in IE 9-. 200 | */ 201 | 202 | audio, 203 | video { 204 | display: inline-block; 205 | } 206 | 207 | /** 208 | * Add the correct display in iOS 4-7. 209 | */ 210 | 211 | audio:not([controls]) { 212 | display: none; 213 | height: 0; 214 | } 215 | 216 | /** 217 | * Remove the border on images inside links in IE 10-. 218 | */ 219 | 220 | img { 221 | border-style: none; 222 | } 223 | 224 | /** 225 | * Hide the overflow in IE. 226 | */ 227 | 228 | svg:not(:root) { 229 | overflow: hidden; 230 | } 231 | 232 | /* Forms 233 | ========================================================================== */ 234 | 235 | /** 236 | * 1. Change the font styles in all browsers (opinionated). 237 | * 2. Remove the margin in Firefox and Safari. 238 | */ 239 | 240 | button, 241 | input, 242 | optgroup, 243 | select, 244 | textarea { 245 | font-family: sans-serif; /* 1 */ 246 | font-size: 100%; /* 1 */ 247 | line-height: 1.15; /* 1 */ 248 | margin: 0; /* 2 */ 249 | } 250 | 251 | /** 252 | * Show the overflow in IE. 253 | * 1. Show the overflow in Edge. 254 | */ 255 | 256 | button, 257 | input { /* 1 */ 258 | overflow: visible; 259 | } 260 | 261 | /** 262 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 263 | * 1. Remove the inheritance of text transform in Firefox. 264 | */ 265 | 266 | button, 267 | select { /* 1 */ 268 | text-transform: none; 269 | } 270 | 271 | /** 272 | * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` 273 | * controls in Android 4. 274 | * 2. Correct the inability to style clickable types in iOS and Safari. 275 | */ 276 | 277 | button, 278 | html [type="button"], /* 1 */ 279 | [type="reset"], 280 | [type="submit"] { 281 | -webkit-appearance: button; /* 2 */ 282 | } 283 | 284 | /** 285 | * Remove the inner border and padding in Firefox. 286 | */ 287 | 288 | button::-moz-focus-inner, 289 | [type="button"]::-moz-focus-inner, 290 | [type="reset"]::-moz-focus-inner, 291 | [type="submit"]::-moz-focus-inner { 292 | border-style: none; 293 | padding: 0; 294 | } 295 | 296 | /** 297 | * Restore the focus styles unset by the previous rule. 298 | */ 299 | 300 | button:-moz-focusring, 301 | [type="button"]:-moz-focusring, 302 | [type="reset"]:-moz-focusring, 303 | [type="submit"]:-moz-focusring { 304 | outline: 1px dotted ButtonText; 305 | } 306 | 307 | /** 308 | * Correct the padding in Firefox. 309 | */ 310 | 311 | fieldset { 312 | padding: 0.35em 0.75em 0.625em; 313 | } 314 | 315 | /** 316 | * 1. Correct the text wrapping in Edge and IE. 317 | * 2. Correct the color inheritance from `fieldset` elements in IE. 318 | * 3. Remove the padding so developers are not caught out when they zero out 319 | * `fieldset` elements in all browsers. 320 | */ 321 | 322 | legend { 323 | box-sizing: border-box; /* 1 */ 324 | color: inherit; /* 2 */ 325 | display: table; /* 1 */ 326 | max-width: 100%; /* 1 */ 327 | padding: 0; /* 3 */ 328 | white-space: normal; /* 1 */ 329 | } 330 | 331 | /** 332 | * 1. Add the correct display in IE 9-. 333 | * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera. 334 | */ 335 | 336 | progress { 337 | display: inline-block; /* 1 */ 338 | vertical-align: baseline; /* 2 */ 339 | } 340 | 341 | /** 342 | * Remove the default vertical scrollbar in IE. 343 | */ 344 | 345 | textarea { 346 | overflow: auto; 347 | } 348 | 349 | /** 350 | * 1. Add the correct box sizing in IE 10-. 351 | * 2. Remove the padding in IE 10-. 352 | */ 353 | 354 | [type="checkbox"], 355 | [type="radio"] { 356 | box-sizing: border-box; /* 1 */ 357 | padding: 0; /* 2 */ 358 | } 359 | 360 | /** 361 | * Correct the cursor style of increment and decrement buttons in Chrome. 362 | */ 363 | 364 | [type="number"]::-webkit-inner-spin-button, 365 | [type="number"]::-webkit-outer-spin-button { 366 | height: auto; 367 | } 368 | 369 | /** 370 | * 1. Correct the odd appearance in Chrome and Safari. 371 | * 2. Correct the outline style in Safari. 372 | */ 373 | 374 | [type="search"] { 375 | -webkit-appearance: textfield; /* 1 */ 376 | outline-offset: -2px; /* 2 */ 377 | } 378 | 379 | /** 380 | * Remove the inner padding and cancel buttons in Chrome and Safari on macOS. 381 | */ 382 | 383 | [type="search"]::-webkit-search-cancel-button, 384 | [type="search"]::-webkit-search-decoration { 385 | -webkit-appearance: none; 386 | } 387 | 388 | /** 389 | * 1. Correct the inability to style clickable types in iOS and Safari. 390 | * 2. Change font properties to `inherit` in Safari. 391 | */ 392 | 393 | ::-webkit-file-upload-button { 394 | -webkit-appearance: button; /* 1 */ 395 | font: inherit; /* 2 */ 396 | } 397 | 398 | /* Interactive 399 | ========================================================================== */ 400 | 401 | /* 402 | * Add the correct display in IE 9-. 403 | * 1. Add the correct display in Edge, IE, and Firefox. 404 | */ 405 | 406 | details, /* 1 */ 407 | menu { 408 | display: block; 409 | } 410 | 411 | /* 412 | * Add the correct display in all browsers. 413 | */ 414 | 415 | summary { 416 | display: list-item; 417 | } 418 | 419 | /* Scripting 420 | ========================================================================== */ 421 | 422 | /** 423 | * Add the correct display in IE 9-. 424 | */ 425 | 426 | canvas { 427 | display: inline-block; 428 | } 429 | 430 | /** 431 | * Add the correct display in IE. 432 | */ 433 | 434 | template { 435 | display: none; 436 | } 437 | 438 | /* Hidden 439 | ========================================================================== */ 440 | 441 | /** 442 | * Add the correct display in IE 10-. 443 | */ 444 | 445 | [hidden] { 446 | display: none; 447 | } 448 | -------------------------------------------------------------------------------- /src/assets/scss/base/variables.scss: -------------------------------------------------------------------------------- 1 | $globalWidth: 980PX; 2 | 3 | $fontSizeS: 12PX; 4 | $fontSizeM: 14PX; 5 | $fontSizeL: 16PX; 6 | $fontSizeXL: 18PX; 7 | 8 | $paddingSizeS: 5PX; 9 | $paddingSizeM: 10PX; 10 | $paddingSizeL: 15PX; 11 | $paddingSizeXL: 20PX; 12 | 13 | $borderRadiusS: 3PX; 14 | $borderRadiusM: 4PX; 15 | $borderRadiusL: 5PX; 16 | 17 | $colorDefault: rgba(0, 0, 0, 1); 18 | $colorPrimary: rgba(24, 144, 255, 1); 19 | $colorBlack: #000; 20 | $colorGrayDarker: #333333; 21 | $colorGrayDark: #666666; 22 | $colorGray: #999999; -------------------------------------------------------------------------------- /src/assets/scss/demo.scss: -------------------------------------------------------------------------------- 1 | @import "base/variables.scss"; 2 | 3 | .demo{ 4 | height: 300PX; 5 | line-height: 300px; 6 | text-align: center; 7 | font-size: 30PX; 8 | } -------------------------------------------------------------------------------- /src/assets/scss/home.scss: -------------------------------------------------------------------------------- 1 | @import "base/variables.scss"; 2 | 3 | .introduction{ 4 | background-color: #00b934; 5 | color: #ffffff; 6 | padding: 150PX 0; 7 | &-content{ 8 | text-align: center; 9 | } 10 | &-title{ 11 | font-size: 28PX; 12 | } 13 | &-description{ 14 | text-align: center; 15 | font-size: $fontSizeL; 16 | margin: 20PX 0; 17 | padding: 0px 20PX; 18 | line-height: 1.5; 19 | } 20 | &-more{ 21 | text-decoration: underline; 22 | color: #ffffff; 23 | font-size: 18PX; 24 | } 25 | } 26 | .feature{ 27 | padding: 100px 0 110px 0; 28 | text-align: center; 29 | &-title{ 30 | font-size: 16PX; 31 | font-weight: 300; 32 | } 33 | &-description{ 34 | font-size: $fontSizeL; 35 | color: $colorGrayDark; 36 | margin-top: 20px; 37 | } 38 | &-show{ 39 | margin-top: 30PX; 40 | } 41 | &--install{ 42 | background-color: #f5f5f5; 43 | } 44 | } 45 | .supports{ 46 | width: 90%; 47 | max-width: 600PX; 48 | text-align: left; 49 | margin: 0px auto; 50 | color: #333333; 51 | font-size: 16PX; 52 | line-height: 2.0; 53 | box-sizing: border-box; 54 | li{ 55 | padding: 5PX 10px; 56 | &:nth-child(odd){ 57 | background-color: #f5f5f5; 58 | } 59 | } 60 | } 61 | .value--new, .value--old{ 62 | background-color: bisque; 63 | font-style: italic; 64 | } 65 | 66 | pre{ 67 | background-color: #000000; 68 | color: #999999; 69 | width: 90%; 70 | max-width: 600PX; 71 | border-radius: 5PX; 72 | padding: 10PX; 73 | text-align: left; 74 | padding: 20PX; 75 | margin: 0px auto; 76 | box-sizing: border-box; 77 | } 78 | 79 | .button-groups{ 80 | width: 90%; 81 | max-width: 600PX; 82 | margin: 0px auto; 83 | .globalBtn{ 84 | margin-bottom: 10PX; 85 | } 86 | } 87 | 88 | @media screen and (max-width:768px) { 89 | .button-groups{ 90 | .globalBtn{ 91 | width: 100%; 92 | padding-left: 0px; 93 | padding-right: 0px; 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /src/constant.ts: -------------------------------------------------------------------------------- 1 | export const EMPTY_FUNCTION: Function = () => {}; -------------------------------------------------------------------------------- /src/interface.ts: -------------------------------------------------------------------------------- 1 | export enum ContentType { 2 | Urlencoded = "application/x-www-from-urlencoded", 3 | FormData = "multipart/form-data" 4 | } 5 | 6 | export interface TipOptions { 7 | message?: string; 8 | duration?: number; 9 | theme?: string; 10 | closable?: boolean; 11 | callback?: Function; 12 | } 13 | 14 | export interface ConfirmOptions { 15 | title: string; 16 | closable: boolean; 17 | content: any; 18 | width: any; 19 | okLabel: string; 20 | cancelLabel: string; 21 | okCallback: Function; 22 | cancelCallback: Function; 23 | } 24 | 25 | export interface XhrFields { 26 | withCredentials: boolean; 27 | } 28 | 29 | export interface AjaxOptions { 30 | url?: string; 31 | data?: any; 32 | dataType?: string; 33 | contentType?: ContentType; 34 | method?: string; 35 | xhrFields?: XhrFields; 36 | token?: string; 37 | async?: boolean; 38 | } 39 | 40 | export interface JsonpOptions { 41 | url?: string; 42 | data?: object; 43 | [propName: string]: any; 44 | } 45 | 46 | export interface Uri { 47 | protocol?: string; 48 | hostname: string; 49 | port?: number; 50 | path?: string; 51 | query?: Object; 52 | } 53 | 54 | export interface ShareInfo { 55 | title: string; 56 | desc?: string; 57 | link?: string; 58 | imgUrl: string; 59 | } 60 | 61 | export type ControlPosition = { x: number; y: number }; 62 | -------------------------------------------------------------------------------- /src/layout/footer.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/layout/header.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/layout/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file src/layout/index.ts 模板渲染 3 | * @author: 王佳欣 4 | * @email: 1974740999@qq.com 5 | */ 6 | import layoutEjs from './layout.ejs'; 7 | import headerEjs from './header.ejs'; 8 | import footerEjs from './footer.ejs'; 9 | import loadingEjs from './loading.ejs'; 10 | import locationEjs from './location.ejs'; 11 | 12 | export default { 13 | render: ({ title, keyword, description, content, location, loading = false }) => { 14 | const renderData = { 15 | title, 16 | keyword, 17 | description, 18 | header: headerEjs({ STATIC_PATH: process.env.STATIC_PATH }), 19 | footer: footerEjs({ STATIC_PATH: process.env.STATIC_PATH }), 20 | loading: loading ? loadingEjs() : null, 21 | content: typeof content === 'string' ? content : content(), 22 | console: process.env.NODE_ENV !== 'production', 23 | location: location && location.length ? locationEjs({ location }) : null 24 | }; 25 | return layoutEjs(renderData); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/layout/layout.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | <%= title %> 18 | 19 | 20 | 42 | 43 | 44 | <%= header %> 45 | <%= location %> 46 |
47 | <%= content %> 48 |
49 | <%= footer %> 50 | <%= loading%> 51 | <% if (console) {%> 52 | 53 | 56 | <% } %> 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/layout/loading.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |
7 | 0% 8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /src/layout/location.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 你的位置: 4 | <% for (var i = 0; i< location.length; i++) { %> 5 | <% if (location[i].url) { %> 6 | <%= location[i].name %> 7 | <% } else {%> 8 | <%= location[i].name %> 9 | <%}%> 10 | <%if (i != location.length - 1 ) {%> 11 | » 12 | <%}%> 13 | <%}%> 14 |
15 |
-------------------------------------------------------------------------------- /src/pages/demo/index-render.js: -------------------------------------------------------------------------------- 1 | import layout from '@layout'; 2 | import content from './index.ejs'; 3 | const title = '这是另一个页面'; 4 | const keyword = '另一个页面关键字'; 5 | const description = '另一个页面描述'; 6 | 7 | export default layout.render({ 8 | title, 9 | keyword, 10 | description, 11 | content, 12 | loading: true, 13 | location: [{ name: '首页', url: 'index.html' }, { name: 'demo' }] 14 | }); 15 | -------------------------------------------------------------------------------- /src/pages/demo/index.ejs: -------------------------------------------------------------------------------- 1 |
带Loading模块的页面
-------------------------------------------------------------------------------- /src/pages/demo/index.ts: -------------------------------------------------------------------------------- 1 | import "@scss/demo.scss"; 2 | import App from "@app"; 3 | import Loading from "@modules/loading"; 4 | new App({ 5 | data: {}, 6 | watchs: {}, 7 | 8 | bindEvents() {}, 9 | 10 | init() { 11 | console.log("asdfasdf"); 12 | const loading = new Loading( 13 | [ 14 | "http://assets.shuxia123.com/uploads/2019/1555171314834_width_1289_height_476.png", 15 | "http://assets.shuxia123.com/uploads/2019/1555170559378_width_800_height_349.png", 16 | "http://assets.shuxia123.com/uploads/2019/1554905994308_width_500_height_350.jpeg" 17 | ], 18 | () => { 19 | console.log("图片加完成"); 20 | } 21 | ); 22 | loading.start(); 23 | 24 | console.log("2222222222"); 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /src/pages/home/index-render.js: -------------------------------------------------------------------------------- 1 | import layout from '@layout'; 2 | import content from './index.ejs'; 3 | const title = '这是首页'; 4 | const keyword = '首页关键字'; 5 | const description = '首页描述'; 6 | 7 | export default layout.render({ 8 | title, 9 | keyword, 10 | description, 11 | content, 12 | loading: false, 13 | location: [] 14 | }); 15 | -------------------------------------------------------------------------------- /src/pages/home/index.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

它是什么?

4 |

这是一个响应式多页应用的前端工程,让你各式各样的网页开发中跳出来!

5 | 立即了解 6 |
7 |
8 | 9 |
10 |
11 |

特性

12 |

为了让你快速的进入开发,它支持了什么

13 |
14 |
    15 |
  • ✔︎ 完善的的开发体验
  • 16 |
  • ︎✔︎ 可配置多环境,开发环境、测试环境、预生产环境、生产环境等
  • 17 |
  • ✔︎ 可自定义配置多入口,简易配置,入口和入口间相互独立
  • 18 |
  • ✔︎ 很多好用的通用模块,提高开发效率
  • 19 |
  • ✔︎ 无第三方依赖,代码体积小,如果你需要其他依赖可自行引入
  • 20 |
  • ✔︎ TypeScript,让你的代码更强壮
  • 21 |
  • ✔︎ 支持持久化,页面模块不相互影响
  • 22 |
  • ✔︎ 支持响应式
  • 23 |
24 |
25 |
26 | 27 |
28 |

安装

29 |

利用NPM安装起来很简便

30 |
31 |
32 | # 安装依赖
33 | npm install
34 | 
35 | # 开始开发
36 | npm start
37 | 
38 | # 如果你开发完了,可以发布测试 或者 发布生产
39 | # 发布测试
40 | npm run build:test
41 | 
42 | # 发布生产
43 | npm run build
44 | 
45 |
46 |
47 | 48 |
49 |

通用模块

50 |

51 |
52 |
53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 |
61 |
62 |
63 |
64 | -------------------------------------------------------------------------------- /src/pages/home/index.ts: -------------------------------------------------------------------------------- 1 | import '@scss/home.scss'; 2 | import App from '@app'; 3 | import Share from '@modules/share/index'; 4 | import Audio from '@modules/audio'; 5 | 6 | // you can create an other app 7 | // other create a new app, it has independent data and watchs..., 8 | // if you need create a app just when you need 9 | // you can create it in a function and return it, for example singleton mode. 10 | import './other'; 11 | 12 | new App({ 13 | data: { 14 | name: '王佳欣' 15 | }, 16 | watchs: { 17 | sex: 'male' 18 | }, 19 | sexHandler (value: any, oldValue: any) { 20 | console.log('changed sex from', oldValue, '=>', value); 21 | this.$confirm({ 22 | title: '监听sex变化', 23 | okLabel: '', 24 | cancelLabel: '我知道了', 25 | closable: false, 26 | content: `修改 this.sex: "${oldValue}" => "${value}"` 27 | }); 28 | }, 29 | 30 | bindEvents () { 31 | // demo for tip 32 | document.querySelector('.btn-tip').addEventListener('click', () => { 33 | this.$tip({ 34 | message: '这是一个提示框', 35 | closable: true, 36 | duration: 0, 37 | callback: () => { 38 | console.log('提示框关闭后'); 39 | } 40 | }); 41 | }); 42 | 43 | // demo for confirm 44 | document.querySelector('.btn-confirm').addEventListener('click', () => { 45 | this.$confirm({ 46 | title: '确认框标题', 47 | okLabel: '2s, 后关闭', 48 | content: '1.自定义标题、内容;
2.自定义Button;
3.自定义回调;
...等等', 49 | okCallback: () => { 50 | console.log(`click ok at:`, Date.now()); 51 | // support callback for async 52 | return new Promise((resolve, reject) => { 53 | setTimeout(() => { 54 | resolve(); 55 | console.log('close dialog at:', Date.now()); 56 | }, 2000); 57 | }); 58 | } 59 | }); 60 | }); 61 | 62 | // demo for route 63 | document.querySelector('.btn-log').addEventListener('click', () => { 64 | console.log('当前路由信息', this.$route); 65 | this.$confirm({ 66 | title: '当前路由信息', 67 | okLabel: '', 68 | cancelLabel: '我知道了', 69 | closable: false, 70 | content: JSON.stringify(this.$route) 71 | }); 72 | }); 73 | 74 | // demo for proxy 75 | document.querySelector('.btn-proxy').addEventListener('click', () => { 76 | this.sex = this.sex === 'female' ? 'male' : 'female'; 77 | }); 78 | 79 | // call a share 80 | document.querySelector('.btn-share').addEventListener('click', () => { 81 | this.share.callShare(); 82 | }); 83 | 84 | // open a new page 85 | document.querySelector('.btn-open').addEventListener('click', () => { 86 | this.$router.push('/demo.html'); 87 | }); 88 | 89 | // play a audio 90 | document.querySelector('.btn-audio').addEventListener('click', () => { 91 | this.audio.isPlaying 92 | ? this.audio.pause() 93 | : this.audio.play(); 94 | }); 95 | }, 96 | 97 | init () { 98 | this.$btnAudio = document.querySelector('.btn-audio'); 99 | // demo code for share 100 | const shareInfo = { 101 | title: '测试分享标题', 102 | desc: '测试分享描述', 103 | link: 'http://www.shuxia123.com', 104 | imgUrl: 'http://assets.shuxia123.com/uploads/2019/1554004957941_width_748_height_500.jpg' 105 | }; 106 | // ('wechatShareTokenUrl', qqAppid, shareInfo) 107 | this.share = new Share('', null, shareInfo); 108 | 109 | // demo code for ajax 110 | const result = this.$ajax({ 111 | url: 'https://www.shuxia123.com/services/projects', 112 | method: 'get', 113 | data: { 114 | name: 'test' 115 | }, 116 | async: false 117 | }); 118 | 119 | JSON.parse(result) 120 | 121 | // demo code for audio 122 | const that = this; 123 | this.audio = new Audio({ 124 | src: 'https://h5.meitu.com/meituskin/h5/static_photos/iloveyou/music.mp3', 125 | autoPlay: false, 126 | loop: true, 127 | onPlay () { 128 | that.$btnAudio.innerHTML = '暂停音频'; 129 | }, 130 | onPause () { 131 | that.$btnAudio.innerHTML = '重新播放'; 132 | }, 133 | onEnded () { 134 | that.$btnAudio.innerHTML = '播放结束'; 135 | } 136 | }); 137 | 138 | // demo code for storage, expired at 60s 139 | this.$storage.setItem('name', 'wangjx', 1000 * 60); 140 | } 141 | }); 142 | -------------------------------------------------------------------------------- /src/pages/home/other.ts: -------------------------------------------------------------------------------- 1 | import App from '@app'; 2 | 3 | new App({ 4 | data: { 5 | 6 | }, 7 | watchs: { 8 | 9 | }, 10 | bindEvents () { 11 | console.log('other bindEvents'); 12 | }, 13 | init () { 14 | console.log('other init'); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /src/utils/device.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file utils/device.ts 终端相关 3 | * @author: 王佳欣 4 | * @email: 1974740999@qq.com 5 | */ 6 | 7 | export let getTransitionEvent = () => { 8 | let el = document.createElement('fake_a_element'); 9 | 10 | let transitions = { 11 | 'transition':'transitionend', 12 | 'OTransition':'oTransitionEnd', 13 | 'MozTransition':'transitionend', 14 | 'WebkitTransition':'webkitTransitionEnd' 15 | }; 16 | 17 | for (let transition in transitions) { 18 | if (el.style[transition] !== undefined) { 19 | getTransitionEvent = () => { 20 | return transitions[transition]; 21 | }; 22 | return transitions[transition]; 23 | } 24 | } 25 | }; 26 | 27 | export let getAnimationEvent = () => { 28 | let el = document.createElement('fake_a_element'); 29 | 30 | let animations = { 31 | 'animation':'animationend', 32 | 'OAnimation':'oAnimationEnd', 33 | 'MozAnimation':'animationend', 34 | 'WebkitAnimation':'webkitAnimationEnd' 35 | }; 36 | 37 | for (let animation in animations) { 38 | if (el.style[animation] !== undefined) { 39 | getAnimationEvent = () => { 40 | return animations[animation]; 41 | } 42 | return animations[animation]; 43 | } 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /src/utils/domHelper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file utils/domHelper.ts dom相关操作 3 | * @author: 王佳欣 4 | * @email: 1974740999@qq.com 5 | */ 6 | import { ControlPosition } from "../interface"; 7 | 8 | export const isNodeFound = (current: Node, parentNode: Node): boolean => { 9 | // find current target in parentNode 10 | // return is found? 11 | if (current === parentNode) { 12 | return true; 13 | } 14 | 15 | while (current.parentNode) { 16 | current = current.parentNode; 17 | if (current === parentNode) { 18 | return true; 19 | } 20 | } 21 | 22 | return false; 23 | }; 24 | 25 | export let getScrollbarWidth = () => { 26 | // get browser's scrollbar width 27 | // this can help to improve user experience 28 | if (window.innerHeight >= document.body.offsetHeight) { 29 | return 0; 30 | } 31 | const outer = document.createElement("div"); 32 | outer.className = "scrollbar__wrap"; 33 | outer.style.visibility = "hidden"; 34 | outer.style.width = "100px"; 35 | outer.style.position = "absolute"; 36 | outer.style.top = "-9999px"; 37 | document.body.appendChild(outer); 38 | 39 | const widthNoScroll = outer.offsetWidth; 40 | outer.style.overflow = "scroll"; 41 | 42 | const inner = document.createElement("div"); 43 | inner.style.width = "100%"; 44 | outer.appendChild(inner); 45 | 46 | const widthWithScroll = inner.offsetWidth; 47 | outer.parentNode.removeChild(outer); 48 | const scrollBarWidth = widthNoScroll - widthWithScroll; 49 | getScrollbarWidth = () => { 50 | return scrollBarWidth; 51 | }; 52 | return scrollBarWidth; 53 | }; 54 | 55 | export const windowScroll = (canScroll: boolean) => { 56 | // to allow window scroll or not 57 | // no scroll,set body style padding-right = scrollbar 58 | // scroll, setbody padding-right = 0 59 | if (canScroll) { 60 | document.getElementsByTagName("html")[0].style.overflow = "auto"; 61 | document.getElementsByTagName("html")[0].style.paddingRight = "0px"; 62 | } else { 63 | document.getElementsByTagName("html")[0].style.overflow = "hidden"; 64 | document.getElementsByTagName( 65 | "html" 66 | )[0].style.paddingRight = `${getScrollbarWidth()}px`; 67 | } 68 | }; 69 | 70 | export const offsetXYFromParent = ( 71 | // Get from offsetParent 72 | evt: { clientX: number; clientY: number }, 73 | offsetParent: HTMLElement 74 | ): ControlPosition => { 75 | const isBody = offsetParent === offsetParent.ownerDocument.body; 76 | const offsetParentRect = isBody 77 | ? { left: 0, top: 0 } 78 | : offsetParent.getBoundingClientRect(); 79 | 80 | const x = evt.clientX + offsetParent.scrollLeft - offsetParentRect.left; 81 | const y = evt.clientY + offsetParent.scrollTop - offsetParentRect.top; 82 | 83 | return { x, y }; 84 | }; 85 | -------------------------------------------------------------------------------- /src/utils/formula.ts: -------------------------------------------------------------------------------- 1 | // curry a function 2 | export const curry = (fn, ...args) => { 3 | if (args.length < fn.length) { 4 | return curry.bind(null, fn, ...args); 5 | } else { 6 | return fn(...args); 7 | } 8 | }; 9 | 10 | // componse functions 11 | export const compose = (...fns) => { 12 | return fns.reduce((fn, current) => (...args) => fn(current(...args))); 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file utils/index.ts 工具类主文件 3 | * @author: 王佳欣 4 | * @email: 1974740999@qq.com 5 | */ 6 | import { EMPTY_FUNCTION } from '../constant'; 7 | 8 | // load javascript 9 | export const loadScript = (src: string): Promise => { 10 | return new Promise((resolve, reject) => { 11 | const timer = setTimeout(() => { 12 | reject('error'); 13 | }, 5000); 14 | 15 | const script = document.createElement('script'); 16 | script.type = 'text/javascript'; 17 | script.src = src; 18 | script.onload = () => { 19 | clearTimeout(timer); 20 | resolve('success'); 21 | }; 22 | script.onerror = () => { 23 | clearTimeout(timer); 24 | reject('error'); 25 | }; 26 | document.head.appendChild(script); 27 | }); 28 | }; 29 | 30 | // load photos 31 | export const loadImages = (images: Array, callback: Function = EMPTY_FUNCTION): Promise => { 32 | let haveLoadedCount = 0; 33 | return new Promise((resolve, reject) => { 34 | const loadedOne = () => { 35 | haveLoadedCount++; 36 | callback(haveLoadedCount); 37 | if (haveLoadedCount === images.length) { 38 | resolve(haveLoadedCount); 39 | } 40 | }; 41 | 42 | const load = (src: string): void => { 43 | const image = new Image(); 44 | image.onload = loadedOne; 45 | image.onerror = loadedOne; 46 | image.src = src; 47 | }; 48 | 49 | images.map(load); 50 | images.length === 0 && resolve(0); 51 | }); 52 | }; 53 | -------------------------------------------------------------------------------- /src/utils/is.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file utils/is.ts 平台判断 3 | * @author: 王佳欣 4 | * @email: 1974740999@qq.com 5 | */ 6 | interface Is { 7 | isWeibo: Function, 8 | isWechat: Function, 9 | isQQ: Function, 10 | isQZone: Function, 11 | isAndroid: Function, 12 | isIos: Function, 13 | isIphoneX: Function 14 | } 15 | 16 | export default ((): Is => { 17 | const USER_AGENT = navigator.userAgent.toLowerCase(); 18 | return { 19 | isWeibo (): boolean { 20 | const isTest = /weibo/.test(USER_AGENT); 21 | this.isWeibo = () => isTest; 22 | return isTest; 23 | }, 24 | isWechat (): boolean { 25 | const isTest = /micromessenger/.test(USER_AGENT) && !/wxwork/.test(USER_AGENT); 26 | this.isWechat = () => isTest; 27 | return isTest; 28 | }, 29 | isQQ (): boolean { 30 | const isTest = /qq\//gi.test(USER_AGENT); 31 | this.isQQ = () => isTest; 32 | return isTest; 33 | }, 34 | isQZone (): boolean { 35 | const isTest = /qzone\//gi.test(USER_AGENT); 36 | this.isQZone = () => isTest; 37 | return isTest; 38 | }, 39 | isAndroid (): boolean { 40 | const isTest = /android/.test(USER_AGENT); 41 | this.isAndroid = () => isTest; 42 | return isTest; 43 | }, 44 | isIos (): boolean { 45 | const isTest = /iphone|ipad|ipod/.test(USER_AGENT); 46 | this.isIos = () => isTest; 47 | return isTest; 48 | }, 49 | isIphoneX (): boolean { 50 | const isTest = this.isIos() && window.screen.width === 375 && window.screen.height === 812; 51 | this.isIphoneX = () => isTest; 52 | return isTest; 53 | } 54 | }; 55 | })(); 56 | -------------------------------------------------------------------------------- /src/utils/only.ts: -------------------------------------------------------------------------------- 1 | export default (obj = {}, keys: Array|string) => { 2 | if ('string' === typeof keys) keys = keys.split(/\s+/); 3 | return keys.reduce((result: Object, key: string) => { 4 | result[key] = obj[key]; 5 | return result; 6 | }, {}); 7 | }; -------------------------------------------------------------------------------- /src/utils/shims.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file utils/domHelper.ts dom相关操作 3 | * @author: 王佳欣 4 | * @email: 1974740999@qq.com 5 | */ 6 | export function isFunction(func: any): boolean { 7 | return ( 8 | typeof func === "function" || 9 | Object.prototype.toString.call(func) === "[object Function]" 10 | ); 11 | } 12 | 13 | export function isNum(num: any): boolean { 14 | return typeof num === "number" && !isNaN(num); 15 | } 16 | 17 | export function int(a: string): number { 18 | return parseInt(a, 10); 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/uri.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file utils/uri.ts uri相关 3 | * @author: 王佳欣 4 | * @email: 1974740999@qq.com 5 | */ 6 | 7 | import { Uri } from '../interface'; 8 | export default class URI { 9 | private static stringifyQuery (queries: Object): string { 10 | return Object.keys(queries).map(key => { 11 | return `${key}=${queries[key]}` 12 | }).join('&'); 13 | } 14 | 15 | private static parseHost (url: string): {hostname: string, port: number} { 16 | let port = 80; 17 | let hostname = ''; 18 | url = url.replace(/\/\//ig, '/'); 19 | hostname = url; 20 | 21 | let indexOfPath = url.indexOf('/'); 22 | if (indexOfPath !== -1) { 23 | hostname = url.substring(0, indexOfPath); 24 | } 25 | 26 | indexOfPath = hostname.indexOf(':'); 27 | if (indexOfPath !== -1) { 28 | port = parseInt(hostname.substring(indexOfPath + 1, hostname.length) || '80'); 29 | hostname = hostname.substring(0, indexOfPath); 30 | } 31 | 32 | return {hostname, port}; 33 | } 34 | 35 | private static parseQuery (url: string): Object { 36 | const query = {}; 37 | const splits = url.split(/\?|\#/); 38 | 39 | if (splits.length > 1) { 40 | splits[1].split('&').forEach((item) => { 41 | const values = item.split('='); 42 | query[values[0]] = decodeURIComponent(values[1]); 43 | }); 44 | } 45 | 46 | return query; 47 | } 48 | 49 | private static parsePath (url: string): string { 50 | let path = ''; 51 | const firstIndexOfPath = url.indexOf('/') + 1; 52 | if (firstIndexOfPath === -1) { 53 | return ''; 54 | } 55 | 56 | url = url.replace(/\?|\#/, '?'); 57 | let lastIndexOfPath = url.indexOf('/', firstIndexOfPath); 58 | path = lastIndexOfPath === -1 ? '' : url.substring(firstIndexOfPath, lastIndexOfPath); 59 | return path; 60 | } 61 | 62 | public static parse (url: string): Uri { 63 | let pos = url.indexOf('://'); 64 | if (pos !== -1) { 65 | url = url.substring(pos + 3, url.length); 66 | } 67 | 68 | const { hostname, port } = this.parseHost(url); 69 | const path = this.parsePath(url); 70 | const query = this.parseQuery(url); 71 | 72 | return { 73 | hostname, 74 | port, 75 | path, 76 | query 77 | }; 78 | } 79 | 80 | public static format (uri: Uri): string { 81 | const portStr = uri.port ? `:${uri.port}` : ''; 82 | return `${uri.protocol || 'http'}://${uri.hostname}${portStr}${uri.path}?${URI.stringifyQuery(uri.query)}`; 83 | } 84 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module":"es2015", 5 | "lib": [ 6 | "dom", 7 | "es6", 8 | "es2015.promise" 9 | ], 10 | "removeComments": true, 11 | "moduleResolution": "node", 12 | "declaration": false, 13 | "noImplicitAny": false, 14 | "sourceMap": true, 15 | "suppressImplicitAnyIndexErrors": true, 16 | "baseUrl": "./src", 17 | "paths":{ 18 | "@app": ["app/index.ts"], 19 | "@layout": ["layout/index.js"], 20 | "@scss/*": ["assets/scss/*"], 21 | "@utils/*": ["utils/*"], 22 | "@modules/*": ["app/modules/*"] 23 | } 24 | }, 25 | "awesomeTypescriptLoaderOptions": { 26 | "useBabel": true, 27 | "babelOptions": { 28 | "babelrc": true, 29 | }, 30 | "babelCore": "@babel/core" 31 | }, 32 | "compileOnSave": false, 33 | "exclude": [ 34 | "node_modules", 35 | "build", 36 | "config" 37 | ], 38 | "include": [ 39 | "src" 40 | ], 41 | 42 | } --------------------------------------------------------------------------------