├── .babelrc ├── .gitignore ├── README.md ├── doc └── handlock.md ├── example ├── locker-check.html ├── locker-update.html ├── locker.html └── recorder.html ├── lib ├── app.js ├── locker.js └── recorder.js ├── package.json ├── script ├── cdn-uploader.js └── deploy.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"], 3 | "plugins": ["transform-runtime"] 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage/ 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Dependency directory 23 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 24 | node_modules/ 25 | 26 | # IDE config 27 | .idea 28 | 29 | # output 30 | output/ 31 | output.tar.gz 32 | 33 | app/ 34 | dist/ 35 | 36 | runtime/ 37 | 38 | .DS_Store 39 | sftp-config.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 手势密码设置和解锁的实现 2 | 3 | 这是[第三届360前端星计划](http://html5.360.cn/star)的[在线作业](doc/handlock.md)的参考实现。 4 | 5 | ## 在线例子 6 | 7 | [在线示例](http://handlock.test.h5jun.com/example/locker.html) 8 | 9 | ![扫描二维码](https://p.ssl.qhimg.com/t0183f30bf0e1670466.png) 10 | 11 | ## 安装和使用 12 | 13 | 直接在浏览器里引用: 14 | 15 | ```html 16 | 17 | ``` 18 | 19 | ## API 20 | 21 | ### Locker 22 | 23 | 创建一个可以设置密码和验证密码的实例 24 | 25 | ```js 26 | var password = '11121323'; 27 | 28 | var locker = new HandLock.Locker({ 29 | container: document.querySelector('#handlock'), 30 | check: { 31 | checked: function(res){ 32 | if(res.err){ 33 | console.error(res.err); //密码错误或长度太短 34 | }else{ 35 | console.log(`正确,密码是:${res.records}`); 36 | } 37 | }, 38 | }, 39 | update:{ 40 | beforeRepeat: function(res){ 41 | if(res.err){ 42 | console.error(res.err); //密码长度太短 43 | }else{ 44 | console.log(`密码初次输入完成,等待重复输入`); 45 | } 46 | }, 47 | afterRepeat: function(res){ 48 | if(res.err){ 49 | console.error(res.err); //密码长度太短或者两次密码输入不一致 50 | }else{ 51 | console.log(`密码更新完成,新密码是:${res.records}`); 52 | } 53 | }, 54 | } 55 | }); 56 | 57 | locker.check(password); 58 | ``` 59 | 60 | ### 几种 err 状态 61 | 62 | - ERR_NOT_ENOUGH_POINTS 绘制的点数量不足,默认为最少4个点 63 | - ERR_PASSWORD_MISMATCH 密码不一致,check时密码不对或者update时两次输入密码不一致 64 | - ERR_USER_CANCELED 用户切换验证或设置操作时,取消当前的状态 65 | 66 | ### 可配置的参数 67 | 68 | ```js 69 | //recorder.js 70 | const defaultOptions = { 71 | container: null, //创建canvas的容器,如果不填,自动在 body 上创建覆盖全屏的层 72 | focusColor: '#e06555', //当前选中的圆的颜色 73 | fgColor: '#d6dae5', //未选中的圆的颜色 74 | bgColor: '#fff', //canvas背景颜色 75 | n: 3, //圆点的数量: n x n 76 | innerRadius: 10, //圆点的内半径 77 | outerRadius: 25, //圆点的外半径,focus 的时候显示 78 | touchRadius: 35, //判定touch事件的圆半径 79 | render: true, //自动渲染 80 | customStyle: false, //自定义样式 81 | minPoints: 4, //最小允许的点数 82 | }; 83 | ``` 84 | 85 | ```js 86 | //locker.js 87 | const defaultOptions = { 88 | update: { 89 | beforeRepeat: function(){}, //更新密码第一次输入后的事件 90 | afterRepeat: function(){} //更新密码重复输入后的事件 91 | }, 92 | check: { 93 | checked: function(){} //校验密码之后的事件 94 | } 95 | } 96 | ``` 97 | 98 | ### clearPath() 99 | 100 | 清除 canvas 上选中的圆。 101 | 102 | ### cancel() 103 | 104 | 取消当前状态,用于update/check状态切换。 105 | 106 | ## 修改和发布代码 107 | 108 | 下载仓库并安装依赖: 109 | 110 | ```bash 111 | npm install 112 | ``` 113 | 114 | 启动服务: 115 | 116 | ```bash 117 | npm start 118 | ``` 119 | 120 | 发布代码: 121 | 122 | ```bash 123 | npm run deploy 124 | ``` 125 | 126 | ## License 127 | 128 | MIT 129 | -------------------------------------------------------------------------------- /doc/handlock.md: -------------------------------------------------------------------------------- 1 | # 2017 前端星计划选拔作业 2 | 3 | 在移动端设备上,“手势密码”成为一个很常用的 UI 组件。 4 | 5 | 一个手势密码的界面大致如下: 6 | 7 | ![](https://p3.ssl.qhimg.com/t01c6b1b34d845c192b.png) 8 | 9 | 用户用手指按顺序依次划过 9 个原点中的若干个(必须不少于 4 个点),如果划过的点的数量和顺序与之前用户设置的相同,那么当用户的手指离开屏幕时,判定为密码输入正确,否则密码错误。 10 | 11 | 要求:实现一个移动网页,允许用户设置手势密码和验证手势密码。已设置的密码记录在本地 localStorage 中。 12 | 13 | 界面原型和操作流程如下: 14 | 15 | ### stat 1:设置密码 16 | 17 | ![](https://p5.ssl.qhimg.com/t01ad2dbd1fa3195d55.png) 18 | 19 | 用户选择设置密码,提示用户输入手势密码 20 | 21 | ### stat 2:密码长度太短 22 | 23 | ![](https://p3.ssl.qhimg.com/t01e3ccb14544b73cc3.png) 24 | 25 | 如果不足 5 个点,提示用户密码太短 26 | 27 | ### stat 3:再次输入密码 28 | 29 | ![](https://p4.ssl.qhimg.com/t01e29ee99bbe73b256.png) 30 | 31 | 提示用户再次输入密码 32 | 33 | ### stat 4: 两次密码输入不一致 34 | 35 | ![](https://p4.ssl.qhimg.com/t01698b3be9b0d473e7.png) 36 | 37 | 如果用户输入的两次密码不一致,**提示并重置,重新开始设置密码** 38 | 39 | ### stat 5: 密码设置成功 40 | 41 | ![](https://p3.ssl.qhimg.com/t01dc54ccf4133d2b06.png) 42 | 43 | 如果两次输入一致,**密码设置成功,更新 localStorage** 44 | 45 | ### stat 6: 验证密码 - 不正确 46 | 47 | ![](https://p1.ssl.qhimg.com/t01410791e9c637add0.png) 48 | 49 | 切换单选框进入验证密码模式,将用户输入的密码与保存的密码相比较,如果不一致,则提示**输入密码不正确,重置为等待用户输入**。 50 | 51 | ### stat 7: 验证密码 - 正确 52 | 53 | ![](https://p0.ssl.qhimg.com/t019bf08a6f82f1d289.png) 54 | 55 | 如果用户输入的密码与 localStorage 中保存的密码一致,则提示**密码正确**。 56 | 57 | --- 58 | 59 | 请同学们按照上面的需求实现这个网页,在手机上可用。可以不用太考虑古老机器的兼容性,最新的 android 和 iPhone 可用即可。 60 | 61 | **要求:** 62 | 63 | 1. 独立思考,独立完成,严禁抄袭!如果发现抄袭导致代码雷同,抄袭者和被抄袭者将都不会入选,而且会取消以后参加 360 面试的资格。 64 | 65 | 1. 注意保持良好代码风格和注释,可以在 README 文档里写上自己的思路。 66 | 67 | 1. 请在截止日期内完成并提交。 -------------------------------------------------------------------------------- /example/locker-check.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Demo 7 | 26 | 27 | 28 |
29 |
30 | 31 | 43 | 44 | -------------------------------------------------------------------------------- /example/locker-update.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Demo 7 | 26 | 27 | 28 |
29 |
30 | 31 | 54 | 55 | -------------------------------------------------------------------------------- /example/locker.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Demo 7 | 71 | 72 | 73 |
74 |

手势解锁

75 |
验证密码,请绘制密码图案
76 |
77 |
请连接至少4个点
78 |
79 | 80 | 81 |
82 |
83 | 84 | 154 | 155 | -------------------------------------------------------------------------------- /example/recorder.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Demo 7 | 32 | 33 | 34 |
35 |
36 | 37 | 38 |
39 | 40 | 70 | 71 | -------------------------------------------------------------------------------- /lib/app.js: -------------------------------------------------------------------------------- 1 | import Recorder from './recorder.js'; 2 | import Locker from './locker.js'; 3 | 4 | export { 5 | /** add your code here **/ 6 | Recorder, 7 | Locker, 8 | } 9 | -------------------------------------------------------------------------------- /lib/locker.js: -------------------------------------------------------------------------------- 1 | import Recorder from './recorder.js'; 2 | 3 | const defaultOptions = { 4 | update: { 5 | beforeRepeat: function(){}, 6 | afterRepeat: function(){} 7 | }, 8 | check: { 9 | checked: function(){} 10 | } 11 | } 12 | 13 | export default class Locker extends Recorder{ 14 | static get ERR_PASSWORD_MISMATCH(){ 15 | return 'password mismatch!'; 16 | } 17 | static get MODE_UPDATE(){ 18 | return 'update'; 19 | } 20 | static get MODE_CHECK(){ 21 | return 'check'; 22 | } 23 | constructor(options = {}) { 24 | options.update = Object.assign({}, defaultOptions.update, options.update); 25 | options.check = Object.assign({}, defaultOptions.check, options.check); 26 | super(options); 27 | } 28 | async update(){ 29 | if(this.mode !== Locker.MODE_UPDATE){ 30 | await this.cancel(); 31 | this.mode = Locker.MODE_UPDATE; 32 | } 33 | 34 | let beforeRepeat = this.options.update.beforeRepeat, 35 | afterRepeat = this.options.update.afterRepeat; 36 | 37 | let first = await this.record(); 38 | 39 | if(first.err && first.err.message === Locker.ERR_USER_CANCELED){ 40 | return Promise.resolve(first); 41 | } 42 | 43 | if(first.err){ 44 | this.update(); 45 | beforeRepeat.call(this, first); 46 | return Promise.resolve(first); 47 | } 48 | 49 | beforeRepeat.call(this, first); 50 | 51 | let second = await this.record(); 52 | 53 | if(second.err && second.err.message === Locker.ERR_USER_CANCELED){ 54 | return Promise.resolve(second); 55 | } 56 | 57 | if(!second.err && first.records !== second.records){ 58 | second.err = new Error(Locker.ERR_PASSWORD_MISMATCH); 59 | } 60 | 61 | this.update(); 62 | afterRepeat.call(this, second); 63 | return Promise.resolve(second); 64 | } 65 | async check(password){ 66 | if(this.mode !== Locker.MODE_CHECK){ 67 | await this.cancel(); 68 | this.mode = Locker.MODE_CHECK; 69 | } 70 | 71 | let checked = this.options.check.checked; 72 | 73 | let res = await this.record(); 74 | 75 | if(res.err && res.err.message === Locker.ERR_USER_CANCELED){ 76 | return Promise.resolve(res); 77 | } 78 | 79 | if(!res.err && password !== res.records){ 80 | res.err = new Error(Locker.ERR_PASSWORD_MISMATCH) 81 | } 82 | 83 | checked.call(this, res); 84 | this.check(password); 85 | return Promise.resolve(res); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lib/recorder.js: -------------------------------------------------------------------------------- 1 | function getCanvasPoint(canvas, x, y){ 2 | let rect = canvas.getBoundingClientRect(); 3 | return { 4 | x: 2 * (x - rect.left), //canvas 显示大小缩放为实际大小的 50%。为了让图形在 Retina 屏上清晰 5 | y: 2 * (y - rect.top), 6 | }; 7 | } 8 | 9 | function distance(p1, p2){ 10 | let x = p2.x - p1.x, y = p2.y - p1.y; 11 | return Math.sqrt(x * x + y * y); 12 | } 13 | 14 | //画实心圆 15 | function drawSolidCircle(ctx, color, x, y, r){ 16 | ctx.fillStyle = color; 17 | ctx.beginPath(); 18 | ctx.arc(x, y, r, 0, Math.PI * 2, true); 19 | ctx.closePath(); 20 | ctx.fill(); 21 | } 22 | 23 | //画空心圆 24 | function drawHollowCircle(ctx, color, x, y, r){ 25 | ctx.strokeStyle = color; 26 | ctx.beginPath(); 27 | ctx.arc(x, y, r, 0, Math.PI * 2, true); 28 | ctx.closePath(); 29 | ctx.stroke(); 30 | } 31 | 32 | //画线段 33 | function drawLine(ctx, color, x1, y1, x2, y2){ 34 | ctx.strokeStyle = color; 35 | ctx.beginPath(); 36 | ctx.moveTo(x1, y1); 37 | ctx.lineTo(x2, y2); 38 | ctx.stroke(); 39 | ctx.closePath(); 40 | } 41 | 42 | const defaultOptions = { 43 | container: null, //创建canvas的容器,如果不填,自动在 body 上创建覆盖全屏的层 44 | focusColor: '#e06555', //当前选中的圆的颜色 45 | fgColor: '#d6dae5', //未选中的圆的颜色 46 | bgColor: '#fff', //canvas背景颜色 47 | n: 3, //圆点的数量: n x n 48 | innerRadius: 20, //圆点的内半径 49 | outerRadius: 50, //圆点的外半径,focus 的时候显示 50 | touchRadius: 70, //判定touch事件的圆半径 51 | render: true, //自动渲染 52 | customStyle: false, //自定义样式 53 | minPoints: 4, //最小允许的点数 54 | }; 55 | 56 | export default class Recorder{ 57 | static get ERR_NOT_ENOUGH_POINTS(){ 58 | return 'not enough points'; 59 | } 60 | static get ERR_USER_CANCELED(){ 61 | return 'user canceled'; 62 | } 63 | static get ERR_NO_TASK(){ 64 | return 'no task'; 65 | } 66 | constructor(options){ 67 | options = Object.assign({}, defaultOptions, options); 68 | 69 | this.options = options; 70 | this.path = []; 71 | 72 | if(options.render){ 73 | this.render(); 74 | } 75 | } 76 | render(){ 77 | if(this.circleCanvas) return false; 78 | 79 | let options = this.options; 80 | let container = options.container || document.createElement('div'); 81 | 82 | if(!options.container && !options.customStyle){ 83 | Object.assign(container.style, { 84 | position: 'absolute', 85 | top: 0, 86 | left: 0, 87 | width: '100%', 88 | height: '100%', 89 | lineHeight: '100%', 90 | overflow: 'hidden', 91 | backgroundColor: options.bgColor 92 | }); 93 | document.body.appendChild(container); 94 | } 95 | this.container = container; 96 | 97 | let {width, height} = container.getBoundingClientRect(); 98 | 99 | //画圆的 canvas,也是最外层监听事件的 canvas 100 | let circleCanvas = document.createElement('canvas'); 101 | 102 | //2 倍大小,为了支持 retina 屏 103 | circleCanvas.width = circleCanvas.height = 2 * Math.min(width, height); 104 | if(!options.customStyle){ 105 | Object.assign(circleCanvas.style, { 106 | position: 'absolute', 107 | top: '50%', 108 | left: '50%', 109 | transform: 'translate(-50%, -50%) scale(0.5)', 110 | }); 111 | } 112 | 113 | //画固定线条的 canvas 114 | let lineCanvas = circleCanvas.cloneNode(true); 115 | 116 | //画不固定线条的 canvas 117 | let moveCanvas = circleCanvas.cloneNode(true); 118 | 119 | container.appendChild(lineCanvas); 120 | container.appendChild(moveCanvas); 121 | container.appendChild(circleCanvas); 122 | 123 | this.lineCanvas = lineCanvas; 124 | this.moveCanvas = moveCanvas; 125 | this.circleCanvas = circleCanvas; 126 | 127 | this.container.addEventListener('touchmove', 128 | evt => evt.preventDefault(), {passive: false}); 129 | 130 | this.clearPath(); 131 | return true; 132 | } 133 | clearPath(){ 134 | if(!this.circleCanvas) this.render(); 135 | 136 | let {circleCanvas, lineCanvas, moveCanvas} = this, 137 | circleCtx = circleCanvas.getContext('2d'), 138 | lineCtx = lineCanvas.getContext('2d'), 139 | moveCtx = moveCanvas.getContext('2d'), 140 | width = circleCanvas.width, 141 | {n, fgColor, innerRadius} = this.options; 142 | 143 | circleCtx.clearRect(0, 0, width, width); 144 | lineCtx.clearRect(0, 0, width, width); 145 | moveCtx.clearRect(0, 0, width, width); 146 | 147 | let range = Math.round(width / (n + 1)); 148 | 149 | let circles = []; 150 | 151 | //drawCircleCenters 152 | for(let i = 1; i <= n; i++){ 153 | for(let j = 1; j <= n; j++){ 154 | let y = range * i, x = range * j; 155 | drawSolidCircle(circleCtx, fgColor, x, y, innerRadius); 156 | let circlePoint = {x, y}; 157 | circlePoint.pos = [i, j]; 158 | circles.push(circlePoint); 159 | } 160 | } 161 | 162 | this.circles = circles; 163 | } 164 | async cancel(){ 165 | if(this.recordingTask){ 166 | return this.recordingTask.cancel(); 167 | } 168 | return Promise.resolve({err: new Error(Recorder.ERR_NO_TASK)}); 169 | } 170 | async record(){ 171 | if(this.recordingTask) return this.recordingTask.promise; 172 | 173 | let {circleCanvas, lineCanvas, moveCanvas, options} = this, 174 | circleCtx = circleCanvas.getContext('2d'), 175 | lineCtx = lineCanvas.getContext('2d'), 176 | moveCtx = moveCanvas.getContext('2d'); 177 | 178 | circleCanvas.addEventListener('touchstart', ()=>{ 179 | this.clearPath(); 180 | }); 181 | 182 | let records = []; 183 | 184 | let handler = evt => { 185 | let {clientX, clientY} = evt.changedTouches[0], 186 | {bgColor, focusColor, innerRadius, outerRadius, touchRadius} = options, 187 | touchPoint = getCanvasPoint(moveCanvas, clientX, clientY); 188 | 189 | for(let i = 0; i < this.circles.length; i++){ 190 | let point = this.circles[i], 191 | x0 = point.x, 192 | y0 = point.y; 193 | 194 | if(distance(point, touchPoint) < touchRadius){ 195 | drawSolidCircle(circleCtx, bgColor, x0, y0, outerRadius); 196 | drawSolidCircle(circleCtx, focusColor, x0, y0, innerRadius); 197 | drawHollowCircle(circleCtx, focusColor, x0, y0, outerRadius); 198 | 199 | if(records.length){ 200 | let p2 = records[records.length - 1], 201 | x1 = p2.x, 202 | y1 = p2.y; 203 | 204 | drawLine(lineCtx, focusColor, x0, y0, x1, y1); 205 | } 206 | 207 | let circle = this.circles.splice(i, 1); 208 | records.push(circle[0]); 209 | break; 210 | } 211 | } 212 | 213 | if(records.length){ 214 | let point = records[records.length - 1], 215 | x0 = point.x, 216 | y0 = point.y, 217 | x1 = touchPoint.x, 218 | y1 = touchPoint.y; 219 | 220 | moveCtx.clearRect(0, 0, moveCanvas.width, moveCanvas.height); 221 | drawLine(moveCtx, focusColor, x0, y0, x1, y1); 222 | } 223 | }; 224 | 225 | 226 | circleCanvas.addEventListener('touchstart', handler); 227 | circleCanvas.addEventListener('touchmove', handler); 228 | 229 | let recordingTask = {}; 230 | let promise = new Promise((resolve, reject) => { 231 | recordingTask.cancel = (res = {}) => { 232 | let promise = this.recordingTask.promise; 233 | 234 | res.err = res.err || new Error(Recorder.ERR_USER_CANCELED); 235 | circleCanvas.removeEventListener('touchstart', handler); 236 | circleCanvas.removeEventListener('touchmove', handler); 237 | document.removeEventListener('touchend', done); 238 | resolve(res); 239 | this.recordingTask = null; 240 | 241 | return promise; 242 | } 243 | 244 | let done = evt => { 245 | moveCtx.clearRect(0, 0, moveCanvas.width, moveCanvas.height); 246 | if(!records.length) return; 247 | 248 | circleCanvas.removeEventListener('touchstart', handler); 249 | circleCanvas.removeEventListener('touchmove', handler); 250 | document.removeEventListener('touchend', done); 251 | 252 | let err = null; 253 | 254 | if(records.length < options.minPoints){ 255 | err = new Error(Recorder.ERR_NOT_ENOUGH_POINTS); 256 | } 257 | 258 | //这里可以选择一些复杂的编码方式,本例子用最简单的直接把坐标转成字符串 259 | let res = {err, records: records.map(o => o.pos.join('')).join('')}; 260 | 261 | resolve(res); 262 | this.recordingTask = null; 263 | }; 264 | document.addEventListener('touchend', done); 265 | }); 266 | 267 | recordingTask.promise = promise; 268 | 269 | this.recordingTask = recordingTask; 270 | 271 | return promise; 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hand-lock", 3 | "version": "0.2.1", 4 | "description": "", 5 | "main": "index.js", 6 | "directories": { 7 | "example": "example" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1", 11 | "start": "webpack-dev-server --quiet & http-server example -c-1 -p 8081", 12 | "deploy": "rm -rf dist/* && ./script/deploy.js" 13 | }, 14 | "keywords": [], 15 | "author": "", 16 | "license": "MIT", 17 | "devDependencies": { 18 | "babel-core": "^6.24.0", 19 | "babel-loader": "^6.4.1", 20 | "babel-plugin-transform-runtime": "^6.23.0", 21 | "babel-preset-env": "^1.3.2", 22 | "http-server": "^0.9.0", 23 | "webpack": "^2.3.3", 24 | "webpack-dev-server": "^2.4.2" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /script/cdn-uploader.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | upload: function(file){ 4 | try{ 5 | let qcdn = require('@q/qcdn'); 6 | return qcdn.upload(file, { 7 | https: true, 8 | keepName: true 9 | }); 10 | }catch(ex){ 11 | return Promise.reject('no cdn uploader specified!'); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /script/deploy.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const webpack = require('webpack'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | 7 | let webpackConf = require('../webpack.config.js'); 8 | 9 | webpack(webpackConf({production: true}), function(err, stats){ 10 | let cdnUploader = require('./cdn-uploader'), 11 | output = stats.compilation.compiler.options.output, 12 | file = path.resolve(output.path, output.filename); 13 | 14 | cdnUploader.upload(file).then(function(res){ 15 | let readmeFile = path.resolve(__dirname, '..', 'README.md'); 16 | let content = fs.readFileSync(readmeFile, 'utf-8'); 17 | content = content.replace(/script src="(.*)"/igm, `script src="${res[file]}"`); 18 | fs.writeFileSync(readmeFile, content); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(env = {}){ 2 | 3 | const webpack = require('webpack'), 4 | path = require('path'), 5 | fs = require('fs'), 6 | packageConf = JSON.parse(fs.readFileSync('package.json', 'utf-8')); 7 | 8 | let version = packageConf.version, 9 | library = packageConf.name.replace(/(?:^|-)(\w)/g, (_, m) => m.toUpperCase()), 10 | proxyPort = 8081, 11 | plugins = [], 12 | jsLoaders = []; 13 | 14 | if(env.production){ 15 | //compress js in production environment 16 | plugins.push( 17 | new webpack.optimize.UglifyJsPlugin({ 18 | compress: { 19 | warnings: false, 20 | drop_console: false, 21 | } 22 | }) 23 | ); 24 | } 25 | 26 | if(fs.existsSync('./.babelrc')){ 27 | //use babel 28 | let babelConf = JSON.parse(fs.readFileSync('.babelrc')); 29 | jsLoaders.push({ 30 | loader: 'babel-loader', 31 | options: babelConf 32 | }); 33 | } 34 | 35 | return { 36 | entry: './lib/app.js', 37 | output: { 38 | filename: env.production ? `${library}-${version}.min.js` : `${library}.js`, 39 | path: path.resolve(__dirname, 'dist'), 40 | publicPath: '/js/', 41 | library: `${library}`, 42 | libraryTarget: 'umd' 43 | }, 44 | 45 | plugins: plugins, 46 | 47 | module: { 48 | rules : [{ 49 | test: /\.js$/, 50 | exclude: /(node_modules|bower_components)/, 51 | use: jsLoaders 52 | }] 53 | }, 54 | 55 | devServer: { 56 | proxy: { 57 | "*": `http://127.0.0.1:${proxyPort}`, 58 | } 59 | } 60 | }; 61 | } 62 | --------------------------------------------------------------------------------