├── 2016-03 ├── static │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ └── 6.png └── 手机调试.md ├── 2016-04 ├── static │ └── 1.png ├── use webpack in your node app.md ├── webpack-workflow.md └── do-you-really-understand-how-to-write-a-countdown-by-javascript.md ├── README.md └── 2016-05 └── App embedded H5 communication framework - AppInterface.md /2016-03/static/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gomeplusFED/blog/HEAD/2016-03/static/1.png -------------------------------------------------------------------------------- /2016-03/static/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gomeplusFED/blog/HEAD/2016-03/static/2.png -------------------------------------------------------------------------------- /2016-03/static/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gomeplusFED/blog/HEAD/2016-03/static/3.png -------------------------------------------------------------------------------- /2016-03/static/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gomeplusFED/blog/HEAD/2016-03/static/4.png -------------------------------------------------------------------------------- /2016-03/static/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gomeplusFED/blog/HEAD/2016-03/static/5.png -------------------------------------------------------------------------------- /2016-03/static/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gomeplusFED/blog/HEAD/2016-03/static/6.png -------------------------------------------------------------------------------- /2016-04/static/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gomeplusFED/blog/HEAD/2016-04/static/1.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 美信 前端 Blog 2 | 3 | > 大家平时的开发总结、经验之道或者折腾之路可以落实成文章更新到这里呀! 4 | 5 | 6 | ### 文章提交说明 7 | 8 | blog下有两个分支,`master|dev` ,在写作文章时大家可以在 `dev` 分支下修改、提交或者推送,文章成型了,切换到 `master` 分支上把 `dev` 分支 `merge` 过来,然后提交,更新 `README.md` 中的目录,然后推送到 `github` 。 9 | 10 | ### 目录 11 | 12 | * [调试手机页面的方法](./2016-03/手机调试.md) -- by:luoye 13 | 14 | * [webpack-workflow](./2016-04/webpack-workflow.md) -- by:luoye 15 | 16 | * [use webpack in your node app](./2016-04/use webpack in your node app.md) -- by:luoye 17 | 18 | * [do you really understand how to write a countdown by javascript](./2016-04/do-you-really-understand-how-to-write-a-countdown-by-javascript.md) -- by: xiaojue 19 | 20 | * [App embedded H5 communication framework](./2016-05/App embedded H5 communication framework - AppInterface.md) -- by: yanglang 21 | -------------------------------------------------------------------------------- /2016-03/手机调试.md: -------------------------------------------------------------------------------- 1 | 手机页面的调试 2 | ================== 3 | #### UC浏览器开发版(仅限安卓平台) 4 | > 超级强大,昨天用春鹏的手机试了下,样式、脚本、资源,都可以调试,而且UC在移动端份额最大,针对性很高。 5 | 6 | 1、 [UC浏览器开发版](http://plus.uc.cn/document/webapp/doc5.html) 7 | 8 | 2、 官方的文档已经很详细了,包括两种调试方法,无线和有线。有线的调试很复杂,需要ADB工具,需要数据线,所以还是直接无线调试比较好。 9 | 10 | 3、 无线调试方法简记: 11 | 12 | * 安卓手机安装UC浏览器的开发版 13 | * 保证手机和电脑在同一个局域网 14 | * 记录下手机的ip地址,比如手机ip为 `10.69.5.150` 15 | * 在UC浏览器中打开需要调试的页面 16 | * 电脑上打开 `10.69.5.150:9998` 即可开始开心的调试(和chrome控制台相差不大) 17 | 18 | 19 | #### weinre本地服务器(全平台) 20 | > weinre是专门的网页调试工具,它会在本地创建一个监听服务器,然后你需要在被调试页面插入一个它提供的js,然后weinre就可以监听到,然后即可开始调试。 21 | 22 | 1、 [weinre官网](http://people.apache.org/~pmuellr/weinre/) 23 | 24 | 2、 [weinre npm](https://www.npmjs.com/package/weinre) 25 | 26 | 3、 调试步骤 27 | 28 | * 本地全局安装weinre 29 | 30 | ``` 31 | npm install weinre -g 32 | ``` 33 | 34 | * 启动本地 weinre 服务 35 | 36 | ``` 37 | weinre --boundHost 10.69.5.10 --httpPort 9090 38 | ``` 39 | 40 | * 参数解释 41 | 42 | ``` 43 | --boundHost 10.69.5.10 : 在你局域网的地址开启服务 44 | 45 | --httpPort 9090 : 本地服务器监听端口,不设置默认为8080 46 | ``` 47 | 48 | * 访问pc调试界面 49 | 如果启动服务时指定了ip和端口,访问此ip的指定端口即可。 50 | 本例地址为: `http://10.69.5.10:9090/` 51 | 52 | * 在调试页面插入监听脚本 53 | 在调试页面中插入下图中 Target Script 中的js,本例即 `http://10.69.5.10:9090/target/target-script-min.js#anonymous`。 54 | `#anonymous` 这个是页面标志,不同的标志在选取目标调试页时可以起到识别作用。 55 | ![](./static/1.png) 56 | 假设页面代码如下 57 | ```html 58 | 59 | 60 | 61 | 62 | 63 | 64 | Document 65 | 66 | 67 | 68 |

我是标题

69 | 70 | 71 | 72 | 73 | 74 | ``` 75 | 76 | * 在终端访问此页面(apache可以直接有个本地http服务,nodejs需要自己启一个) 77 | ```js 78 | // 在本地的2333端口开启服务,返回上面的测试页 79 | 'use strict'; 80 | var express = require('express'); 81 | var app = express(); 82 | var pwd = __dirname; 83 | var port = 2333; 84 | app.get('/', function(req, res, next) { 85 | res.sendFile(pwd + '/test.html') 86 | }) 87 | app.listen(2333); 88 | ``` 89 | 此时访问 `10.69.5.10::2333` 就可以看到上面测试页的内容 90 | ![](./static/2.png) 91 | 92 | 然后在局域网的其它终端访问此页面都可以看到 93 | ![](./static/3.png) 94 | 95 | 此时打开 `http://10.69.5.10:9090/client/` 你会看到 96 | ![](./static/4.png) 97 | 98 | 选中 Targets 里的地址即可开始调试,如下图 99 | ![](./static/5.png) 100 | ![](./static/6.png) 101 | 102 | 4、其它 103 | 104 | weinre最大的缺点就是不能调试js,虽然它实现了一个简单的js解释器,能在自己的控制台操作dom,但是本身页面里的调试信息没法在这捕获,遗憾。 105 | 但是它支持全平台,不管是wap还是app,只要是前端的html页面,都可以调试,遇到一些奇葩的兼容性问题很好找。 106 | 如果真心用这个工具,我们还可以在测试服务器搭一个代理服务,任何线上页面走这个代理服务器,都会被注入那段监听的js,测试机访问代理服务器吐出的地址,pc机访问统一的控制台页面,本地什么配置都不用,就可以开始真机调试。 107 | 再深入的话也可以深究一下weinre的js调试问题,那就圆满了。。。脑洞ing。。。 108 | -------------------------------------------------------------------------------- /2016-04/use webpack in your node app.md: -------------------------------------------------------------------------------- 1 | 用 webpack 构建 node 后端代码,使其支持 js 新特性并实现热重载 2 | =========================================================================== 3 | 4 | webpack 在前端领域的模块化和代码构建方面有着无比强大的功能,通过一些特殊的配置甚至可以实现前端代码的实时构建、ES6/7新特性支持以及热重载,这些功能同样可以运用于后台 nodejs 的应用,让后台的开发更加顺畅,服务更加灵活,怎么来呢?往下看。 5 | 6 | 先梳理下我们将要解决的问题: 7 | 8 | * node端代码构建 9 | * ES6/7 新特性支持 10 | * node服务代码热重载 11 | 12 | node端代码构建 13 | ------------------- 14 | node端的代码其实是不用编译或者构建的,整个node的环境有它自己的一个模块化或者依赖机制,但是即使是现在最新的node版本,对ES6/7的支持还是捉襟见肘。当然使用一些第三方库可以做到支持类似 `async/await` 这样的语法,但是毕竟不是规范不是标准,这样看来,node端的代码还是有构建的需要的。这里我们选取的工具就是 `webpack` 以及它的一些 `loader`。 15 | 16 | 首先,一个 `node app` 必定有一个入口文件 `app.js` ,按照 `webpack` 的规则,我们可以把所有的代码打包成一个文件 `bundle.js` ,然后运行这个 `bundle.js` 即可,`webpack.config.js` 如下: 17 | 18 | ```js 19 | var webpcak = require('webpack'); 20 | 21 | module.exports = { 22 | entry: [ 23 | './app.js' 24 | ], 25 | output: { 26 | path: path.resolve(__dirname, 'build'), 27 | filename: 'bundle.js' 28 | } 29 | } 30 | ``` 31 | 32 | 但是有一个很严重的问题,这样打包的话,一些 `npm` 中的模块也会被打包进这个 `bundle.js`,还有 `node` 的一些原生模块,比如 `fs/path` 也会被打包进来,这明显不是我们想要的。所以我们得告诉 `webpack`,你打包的是 `node` 的代码,原生模块就不要打包了,还有 `node_modules` 目录下的模块也不要打包了,`webpack.config.js` 如下: 33 | 34 | ```js 35 | var webpcak = require('webpack'); 36 | 37 | var nodeModules = {}; 38 | fs.readdirSync('node_modules') 39 | .filter(function(x) { 40 | return ['.bin'].indexOf(x) === -1; 41 | }) 42 | .forEach(function(mod) { 43 | nodeModules[mod] = 'commonjs ' + mod; 44 | }); 45 | 46 | module.exports = { 47 | entry: [ 48 | './app.js' 49 | ], 50 | output: { 51 | path: path.resolve(__dirname, 'build'), 52 | filename: 'bundle.js' 53 | }, 54 | target: 'node', 55 | externals: nodeModules 56 | } 57 | ``` 58 | 59 | 主要就是在 `webpack` 的配置中加上 `target: 'node'` 告诉 `webpack` 打包的对象是 `node` 端的代码,这样一些原生模块 `webpack` 就不会做处理。另一个就是 `webpack` 的 `externals` 属性,这个属性的主要作用就是告知 `webpack` 在打包过程中,遇到 `externals` 中声明的模块不用处理。 60 | 61 | 比如在前端中, `jQuery` 的包通过 CDN 的方式以 `script` 标签引入,如果此时在代码中出现 `require('jQuery')` ,并且直接用 `webpack` 打包比定会报错。因为在本地并没有这样的一个模块,此时就必须在 `externals` 中声明 `jQuery` 的存在。也就是 `externals` 中的模块,虽然没有被打包,但是是代码运行是所要依赖的,而这些依赖是直接存在在整个代码运行环境中,并不用做特殊处理。 62 | 63 | 在 `node` 端所要做的处理就是过滤出 `node_modules` 中所有模块,并且放到 `externals`中。 64 | 65 | 这个时候我们的代码应该可以构建成功了,并且是我们期望的形态,但是不出意外的话,你还是跑不起来,因为有不小的坑存在,继续往下看。 66 | 67 | * 坑1:`__durname` `__filename` 指向问题 68 | > 打包之后的代码你会发现 `__durname` `__filename` 全部都是 `/` ,这两个变量在 `webpack` 中做了一些自定义处理,如果想要正确使用,在配置中加上 69 | ```js 70 | context: __dirname, 71 | node: { 72 | __filename: false, 73 | __dirname: false 74 | }, 75 | ``` 76 | 77 | * 坑2:动态 `require` 的上下文问题 78 | >这一块比较大,放到后面讲,跟具体代码有关,和配置无关 79 | 80 | * 坑n:其它的还没发现,估摸不少,遇到了谷歌吧... 81 | 82 | ES6/7 新特性支持 83 | ------------------- 84 | 构建 `node` 端代码的目标之一就是使用ES6/7中的新特性,要实现这样的目标 `babel` 是我们的不二选择。 85 | 86 | 首先,先安装 `babel` 的各种包 `npm install babel-core babel-loader babel-plugin-transform-runtime babel-preset-es2015 babel-preset-stage-0 --save-dev json-loader -d` 87 | 88 | 然后修改 `webpack.config.js` ,如下: 89 | 90 | ```js 91 | var webpcak = require('webpack'); 92 | 93 | var nodeModules = {}; 94 | fs.readdirSync('node_modules') 95 | .filter(function(x) { 96 | return ['.bin'].indexOf(x) === -1; 97 | }) 98 | .forEach(function(mod) { 99 | nodeModules[mod] = 'commonjs ' + mod; 100 | }); 101 | 102 | module.exports = { 103 | entry: [ 104 | './app.js' 105 | ], 106 | output: { 107 | path: path.resolve(__dirname, 'build'), 108 | filename: 'bundle.js' 109 | }, 110 | target: 'node', 111 | externals: nodeModules, 112 | context: __dirname, 113 | node: { 114 | __filename: false, 115 | __dirname: false 116 | }, 117 | module: { 118 | loaders: [{ 119 | test: /\.js$/, 120 | loader: 'babel-loader', 121 | exclude: [ 122 | path.resolve(__dirname, "node_modules"), 123 | ], 124 | query: { 125 | plugins: ['transform-runtime'], 126 | presets: ['es2015', 'stage-0'], 127 | } 128 | }, { 129 | test: /\.json$/, 130 | loader: 'json-loader' 131 | }] 132 | }, 133 | resolve: { 134 | extensions: ['', '.js', '.json'] 135 | } 136 | } 137 | ``` 138 | 139 | 主要就是配置 `webpack` 中的 `loader` ,借此来编译代码。 140 | 141 | 142 | node服务代码热重载 143 | -------------------- 144 | `webpack` 极其牛叉的地方之一,开发的时候,实时的构建代码,并且,实时的更新你已经加载的代码,也就是说,不用手动去刷新浏览器,即可以获取最新的代码并执行。 145 | 146 | 这一点同样可以运用在 `node` 端,实现即时修改即时生效,而不是 `pm2` 那种重启的方式。 147 | 148 | 首先,修改配置文件,如下: 149 | 150 | ```js 151 | entry: [ 152 | 'webpack/hot/poll?1000', 153 | './app.js' 154 | ], 155 | // ... 156 | plugins: [ 157 | new webpack.HotModuleReplacementPlugin() 158 | ] 159 | ``` 160 | 161 | 这个时候,如果执行 `webpack --watch & node app.js` ,你的代码修改之后就可以热重载而不用重启应用,当然,代码中也要做相应改动,如下: 162 | 163 | ```js 164 | var hotModule = require('./hotModule'); 165 | // do something else 166 | 167 | // 如果想要 hotModule 模块热重载 168 | if (module.hot) { 169 | module.hot.accept('./hotModule.js', function() { 170 | var newHotModule = require('./hotModule.js'); 171 | // do something else 172 | }); 173 | } 174 | ``` 175 | 思路就是,如果需要某模块热重载,就把它包一层,如果修改了,`webpack` 重新打包了,重新 `require` 一遍,然后代码即是最新的代码。 176 | 177 | 当然,如果你在某个需要热重载的模块中又依赖另一个模块,或者说动态的依赖了另一个模块,这样的模块并不会热重载。 178 | 179 | webpack 动态 require 180 | ----------------------------- 181 | 动态 `require` 的场景包括: 182 | 183 | * 场景一:在代码运行过程中遍历某个目录,动态 `reauire`,比如 184 | 185 | ```js 186 | //app.js 187 | var rd = require('rd'); 188 | 189 | // 遍历路由文件夹,自动挂载路由 190 | var routers = rd.readFileFilterSync('./routers', /\.js/); 191 | routers.forEach(function(item) { 192 | require(item); 193 | }) 194 | ``` 195 | 196 | 这个时候你会发现 `'./routers'` 下的require都不是自己想要的,然后在 `bundle.js` 中找到打包之后的相应模块后,你可以看到,动态 `require` 的对象都是 `app.js` 同级目录下的 `js` 文件,而不是 `'./routers'` 文件下的 `js` 文件。为什么呢? 197 | 198 | `webpack` 在打包的时候,必须把你可能依赖的文件都打包进来,并且编上号,然后在运行的时候 `require` 相应的模块 `ID` 即可,这个时候 `webpack` 获取的动态模块,就不再是你指定的目录 `'./routers'` 了,而是相对于当前文件的目录,所以,必须修正 `require` 的上下文,修改如下: 199 | 200 | ```js 201 | // 获取正确的模块 202 | var req = require.context("./routers", true, /\.js$/); 203 | var routers = rd.readFileFilterSync('./routers', /\.js/); 204 | routers.forEach(function(item) { 205 | // 使用包涵正确模块的已经被修改过的 `require` 去获取模块 206 | req(item); 207 | }) 208 | ``` 209 | 210 | * 场景二:在 `require` 的模块中含有变量,比如 211 | 212 | ```js 213 | var myModule = require(isMe ? './a.js' : './b.js'); 214 | // 或者 215 | var testMoule = require('./mods' + name + '.js'); 216 | ``` 217 | 218 | 第一种的处理方式在 `webpack` 中的处理是把模块 `./a.js` `./b.js` 都包涵进来,根据变量不同 `require` 不同的模块。 219 | 220 | 第二种的处理方式和场景一类似,获取 `./mods/` 目录下的所有模块,然后重写了 `require` ,然后根据变量不同加载不通的模块,所以自己处理的时候方法类似。 221 | 222 | 223 | 用 ES6/7 写 webpack.config.js 224 | ---------------------------------- 225 | 项目都用 ES6/7 了,配置文件也必须跟上。 226 | 227 | 安装好 `babel` 编译所需要的几个依赖包,然后把 `webpack.config.js` 改为 `webpack.config.babel.js` ,然后新建 `.babelrc` 的 `babel` 配置文件,加入 228 | 229 | ```json 230 | { 231 | "presets": ["es2015"] 232 | } 233 | 234 | ``` 235 | 然后和往常一样执行 `webpack` 的相关命令即可。 236 | 237 | 完整 `webpack.config.babel.js` 如下: 238 | 239 | ```js 240 | import webpack from 'webpack'; 241 | import fs from 'fs'; 242 | import path from 'path'; 243 | 244 | let nodeModules = {}; 245 | fs.readdirSync('node_modules') 246 | .filter((x) => { 247 | return ['.bin'].indexOf(x) === -1; 248 | }) 249 | .forEach((mod) => { 250 | nodeModules[mod] = 'commonjs ' + mod; 251 | }); 252 | 253 | export default { 254 | cache: true, 255 | entry: [ 256 | 'webpack/hot/poll?1000', 257 | './app.js' 258 | ], 259 | output: { 260 | path: path.resolve(__dirname, 'build'), 261 | filename: 'bundle.js' 262 | }, 263 | context: __dirname, 264 | node: { 265 | __filename: false, 266 | __dirname: false 267 | }, 268 | target: 'node', 269 | externals: nodeModules, 270 | module: { 271 | loaders: [{ 272 | test: /\.js$/, 273 | loader: 'babel-loader', 274 | exclude: [ 275 | path.resolve(__dirname, "node_modules"), 276 | ], 277 | query: { 278 | plugins: ['transform-runtime'], 279 | presets: ['es2015', 'stage-0'], 280 | } 281 | }, { 282 | test: /\.json$/, 283 | loader: 'json-loader' 284 | }] 285 | }, 286 | plugins: [ 287 | new webpack.HotModuleReplacementPlugin() 288 | ], 289 | resolve: { 290 | extensions: ['', '.js', '.json'] 291 | } 292 | } 293 | ``` 294 | 295 | 大致流程就是如此,坑肯定还有,遇到的话手动谷歌吧~ 296 | 297 | 298 | 299 | -------------------------------------------------------------------------------- /2016-04/webpack-workflow.md: -------------------------------------------------------------------------------- 1 | 基于 webpack 的 workflow 2 | ================================= 3 | 4 | ## 前端工作流中要解决的问题 5 | 6 | * 资源管理:模块化、组件化、依赖管理等 7 | * 开发流程:dev、debug、proxy、build、deploy 8 | * 周边工具:图形化界面、命令行辅助、自动化工程 9 | 10 | ## 简介 11 | 12 | * [webpack官网](http://webpack.github.io/) 13 | * Webpack 是当下最热门的前端资源模块化管理和打包工具。它 14 | * 能把散碎的静态资源通过一定的规则打包在一起 15 | * 还可以require几乎所有的静态资源,包括但不限于CommonJs 模块、 AMD 模块、 ES6 模块、CSS、图片、 JSON、Coffeescript、 LESS、 Vue、 Jade、Ejs 等等 16 | * 还可以将代码切割成不同的chunk,实现按需加载,异步加载 17 | * 还可以极精细的控制所有静态资源的版本 18 | * 还可以实现代码热替换,开发模式下,不用刷新浏览器,不用实时编译,自身的socket-io已经把内存中的代码换成最新的 19 | * 还可以 `SourceUrls` 和 `SourceMaps`,易于调试 20 | * 还可以和前端最新的技术栈完美融合(Vue、React),针对这两个在webpack中有一整套的工具链 21 | * 还可以使用 `Plugin` 插件,实现各种定制化任务 22 | * 还可以和 `gulp/grunt` 等构建工具协同作战,完成极其复杂的任务 23 | * 还原生支持增量编译,发布代码再也不用等个好几分钟 24 | 25 | ## 资源管理 26 | 27 | 28 | * ### 模块化 29 | 30 | 用 `webpack` 来做模块化,简直神器。 31 | 32 | 前端的模块化一直是一个很受人关注的问题,不同的加载器实现方式都不一样。`lithe/requireJs/seaJs`,这样的预加载器都有自己的一套实现方式,一定程度上解决了js的模块化问题,但是不同的实现方式,让我们不得不对不同的库做兼容性处理,让加载器能够识别,无疑增加了工作量和降低了模块的通用度。即使现在ES6提出了统一的标准,但是离实际使用还有很长的距离。 33 | 34 | 所以,有一个足够灵活,足够强大的模块加载器是个必要的选择。 35 | 36 | 在 `webpack` 中,同时支持 CommonJS、AMD和ES6,灵活性有保证。 37 | 38 | 并且在 `webpack` 的设计理念中,所有的静态资源都是模块,所以借助强大的 `loader` 系统几乎可以加载任意类型的静态资源,并且对模块的输出结果可以做非常精细的控制,根据不同的配置可以达到任意想要的结果。 39 | 40 | ```js 41 | loaders: [{ 42 | test: /.js$/, 43 | loader: 'jsx-loader?harmony' 44 | }, { 45 | test: /.vue$/, 46 | loader: 'vue-loader' 47 | },{ 48 | test: /.css$/, 49 | loader: 'style-loader' 50 | },{ 51 | test: /\.(jpe?g|png|gif|svg)$/i, 52 | loaders: [ 53 | 'image?{bypassOnDebug: true, progressive:true, optimizationLevel: 3, pngquant:{quality: "65-80"}}', 54 | 'url?limit=10000&name=img/[hash:8].[name].[ext]', 55 | ] 56 | }] 57 | ``` 58 | 59 | 这是 `webpack` 配置文件中的 `loaders` 部分,其中,`test` 是正则匹配,匹配到的文件将使用相应的 `loader` 去解析,然后这些文件就成为了模块,可以在其它地方引用。比如: 60 | 61 | ```js 62 | // a.js 63 | // 加载js 64 | var login = require('./src/js/conf/login/login.js'); 65 | 66 | // js文件会被webpack包装,并赋予单独ID,exports出去 67 | 68 | // 加载css 69 | require('./src/css/login.css'); 70 | 71 | // css文件会被以style标签插入到head中,当然,也可以通过ExtractTextPlugin这样的插件以link标签插入到页面中 72 | 73 | // 加载图片 74 | var btnImg = require('./src/img/btn.png'); 75 | var img = document.createElement('img'); 76 | img.src = btnImg; 77 | 78 | // 根据上面的配置,小于10k的图片直接dataurl到src中,并且打包后自动添加hash值,当然,在js中引用,hash值部分也相应的会变化。 79 | 80 | // 加载vue单文件组件 81 | var loginCom = require('./src/js/components/login.vue'); 82 | 83 | // vue有单独的loader读取vue单文件组件 84 | 85 | // 所有模块的加载可以做很多灵活的配置,比如,文件hash值、小于一定大小的图片dataurl、打包后文件命名等 86 | // jade模版,ejs模版,less文件,sass文件都可以直接require,不用预编译,直接用! 87 | 88 | ``` 89 | 90 | * ### 组件化 91 | 92 | 抛开现在流行的组件化解决方案,光以 `webpack` 的角度来看,如果模版,样式,脚本,都可以作为模块来引入,这就使得封装组件变的很简单。比如: 93 | 94 | ```js 95 | // 搜索组件 96 | // js/component/search/index.js 97 | 98 | require('js/component/search/index.css'); 99 | 100 | var tpl = require('js/component/search/template.ejs'); 101 | var htmlStr = tpl({searchContent: options.searchStr}); 102 | 103 | var launchSearch = function(options){} 104 | 105 | module.exports = { 106 | launchSearch: launchSearch, 107 | somethingelse: 'balabala' 108 | }; 109 | ``` 110 | 111 | 使用: 112 | ```js 113 | // js/app.js 114 | var searchComponent = require('js/component/search/index.js'); 115 | 116 | var options = { 117 | searchStr: '水果' 118 | // ... 119 | } 120 | searchComponent.launchSearch(options); 121 | ``` 122 | 这样的开发方式无疑能够极大降低维护成本,当然,如果和现在流行的前端框架,如React、Vue结合起来实现组件化,代码组织会更加清晰,自由度也更高,展开讲又是一个大话题,按下不表。 123 | 124 | 还是贴一下伪代码吧。。以 `Vue` 为例 125 | ```js 126 | // alert.vue 127 | 128 | 133 | 136 | 146 | ``` 147 | 148 | 使用 149 | ```js 150 | // main.vue(同样是一个组件,可能被其它组件(如根组件)依赖) 151 | 152 | 153 | 154 | 155 | 178 | ``` 179 | 180 | 这样的 `.vue` 组件通过 `webpack` 中的 `vue-loader` 插件就可以完成打包。 181 | 182 | 183 | * ### 依赖管理 184 | 185 | 没有什么好说的。。。`npm`。 186 | 毕竟绝大部分的流行库都在 `npm` 上,也都支持模块加载的方式。不排除有一些纯js库(jq插件)需要手动封装下。其实也很简单,如果都遵循 `CommonJs` 的加载方式,而且其中没有其它依赖的话,直接 `require` 即可,如果有依赖,大部分也是 `jq` 等,在文件最前面声明下 `var $ = require('jQuery')` 即可。 187 | 这样,一个工程只需要维护一份 `package.json` ,不管是开发用的包,还是构建用的包都可以统一管理。 188 | 189 | 190 | ## 开发流程 191 | 192 | 对于前端的开发流程,一直的想法是,我们能有一个统一的入口工具,不管是命令行还是图形化界面,可以把调试,代理,构建等一系列操作整合起来,并且尽可能的简化这样的操作,开发不用过多的关注构建问题,也不用为调试、构建浪费更多的精力。 193 | 194 | * ### dev、debug 195 | 196 | 基于 `webpack` 的开发流程主要有两种方式: 197 | 198 | * `webpack` 的实时构建模式 199 | 200 | `webpack --watch` 每次修改代码都会实时的构建,增量的,很快,即使是用了 `uglify` 实时构建压缩也很快(不超过1s) 201 | 202 | * `webpack-dev-server` 203 | 204 | `webpack-dev-server` 是一个小型的 `express` 服务器,它的原理就是使用 `webpack-dev-middleware` 中间件来为通过 `webpack` 生成的静态资源提供web服务。它的内部通过 `socket.io` 连接客户端,可以实时发送编译状态的信息到客户端,从而达到客户端代码的实时热更新,也就是 `HMR` `Hot Module Replacement`。 205 | 206 | `webpack-dev-server` 支持命令行模式和 `NodeJs` 模式,命令行模式就是直接打开 `webpack-dev-server` 本地服务器,参数默认,配置灵活性稍低,但是简单快捷,如下: 207 | 208 | ``` 209 | // --line 行内模式 | --hot 热替换 210 | webpack-dev-server --line --hot 211 | ``` 212 | 213 | 在项目根目录行执行上面的命令之后,本地的 `webpack-dev-server` 服务器已经启动,此目录下的由 `webpack` 生成的静态资源都将被这个本地服务器托管。 214 | 215 | 以 GoH5 为例,把页面内引用的资源改为 `8080` 端口下对应的文件并加入 `http://localhost:8080/webpack-dev-server.js`,此时代码热更新已经生效: 216 | 217 | ```html 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | Go H5 228 | 229 | 230 | 231 | 232 |
233 | 234 |
235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | // 因为我的应用服务启动在 3030 端口,所以这里的地址写了绝对地址,如果同端口,直接写文件名即可 243 | ``` 244 | 245 | 在控制台可以看到 246 | 247 | ``` 248 | [HMR] Vue component hot reload shim applied. // 不同的热重载提示信息不同(Vue/React) 249 | [WDS] Hot Module Replacement enabled. 250 | ``` 251 | 252 | 如果本地修改了代码可以看到 253 | ``` 254 | [WDS] App updated. Recompiling... 255 | [WDS] App hot update... 256 | ``` 257 | 同时,控制台也会输出相关的编译信息: 258 | 259 | ![控制台](./static/1.png) 260 | 261 | 一个字。。。快。。。而且,浏览器并不用刷新呀 (有一定限定条件,并不是一切代码都可以热重载) 262 | 263 | 放一张官网的gif 264 | 265 | ![hot-reload](http://blog.evanyou.me/images/vue-hot.gif) 266 | 267 | 上面的例子主要集中在Vue上,其实大同小异,并且 `webpack` 提供了 `webpack-dev-middleware` 中间件,可以定制一个本地的服务,完成很多任务。 268 | 269 | #### **所以我觉得,前端的开发流程如果有一个统一的本地服务,可能性无极限。** 270 | 271 | * ### proxy 272 | 273 | 线上代码的代理调试,借助 `host`,借助 `Fiddler`,借助 `wamp`,其实。。 274 | 275 | ```js 276 | // webpack.config.js 277 | { 278 | devServer: { 279 | proxy: { 280 | '/some/path*': { 281 | target: 'https://js.meixincdn.com', 282 | secure: false, 283 | }, 284 | }, 285 | }, 286 | } 287 | ``` 288 | 289 | 当然,这样的代理不够灵活,而且不能代理html,但是如果借助本地的服务,我们可以这么做 290 | 291 | ```js 292 | // 先行配置host,可以手动,也可以像 `fd-server` 有个图形化界面,然后 293 | var koa = require('koa'); 294 | var proxy = require('koa-proxy'); 295 | 296 | var app = koa(); 297 | 298 | app.use(proxy({ 299 | host: 'https://localhost:8080/', 300 | match: /\/js.meixincdn.com\// 301 | })); 302 | ``` 303 | 304 | 上面达到的功能就是把 `js.meixincdn.com` 域名的资源转发到 `webpack-dev-server` ,然后就可以愉快的开始开发了 305 | 306 | 307 | * ### build、deploy 308 | 309 | 这一块我的想法还是,放到测试机上去。。 310 | 311 | 测试机上可以跑一个 `gulp watch` 和 `webpack --watch` 的任务,本地提交了 `src` 目录下的文件,测试机检测到文件更新,直接编译,这样的编译都是增量的,肯定很快,然后再该怎么上线怎么上线,怎么发布怎么发布。本地就可以直接忽略 `dist` 目录的改变,不提交其中的内容。 312 | 313 | 还有一点,我是觉得 php 的 smarty 模版可以前端维护,就算不行,静态资源那一块也一定前端维护,这样不管是改时间戳还是直接hash,都很灵活啊~ 314 | 315 | ## 周边工具 316 | 图形化界面、命令行辅助、自动化工程,高大上的整套解决方案,想想就激动。。。 -------------------------------------------------------------------------------- /2016-05/App embedded H5 communication framework - AppInterface.md: -------------------------------------------------------------------------------- 1 | 336 | -------------------------------------------------------------------------------- /2016-04/do-you-really-understand-how-to-write-a-countdown-by-javascript.md: -------------------------------------------------------------------------------- 1 | 你真的知道怎么用javascript来写一个倒计时吗 ? 2 | =========================================================================== 3 | 4 | 倒计时功能真的是前端开发中最日常遇到的一个特效了,但是你真的知道怎么写吗?就在上周,在团队中试水了一下,结果却真的非常有意思,特此总结为一篇博文分享出来。 5 | 6 | 首先我们明确需求,我给出的题目是,用js写一个倒计时,用你能想到的各种方式,不局限于性能,效率,展示形式,扩展性,API设计等。 7 | 8 | 最后我收上来了10几份作业,通过review代码,简单的提炼一下,大家可以看看自己处在哪一个级别。:) 9 | 10 | 基本实现 11 | ------------------- 12 | 基本的一个倒计时的原理非常简单了,使用setTimout或者setInterval来对一个函数进行递归或者重复调用,然后对DOM节点做对应的render处理,并对时间做倒计时的格式化处理。 13 | 14 | 例如下面这个样子: 15 | 16 | ```js 17 | //使用setInterval实现一个5的倒计时。 18 | (function() { 19 | var time = 5; 20 | var p = document.getElementById("time"); 21 | var set = setInterval(function() { 22 | time--; 23 | p.innerHTML = time; 24 | if(time === 0) { 25 | p.innerHTML = ""; 26 | clearInterval(set); 27 | } 28 | }, 1000); 29 | })() 30 | ``` 31 | 32 | 我对上面的写法定义为仅仅考虑了实现功能,而写法是完全面向过程的,操作都放到了匿名函数中处理,而且也没有对时间做格式化处理,但是原理也很简单,使用setInterval不断的改变DOM节点,然后对time变量做--操作。 33 | 34 | 稍微好一点的写法: 35 | 36 | ```js 37 | window.onload = function(){ 38 | showtime(); 39 | function addZero(i){ 40 | if(i<10){ 41 | i = "0" + i; 42 | }return i; 43 | } 44 | function showtime() { 45 | var nowtime = new Date(); 46 | var endtime = new Date("2016/05/20,20:20:20"); 47 | var lefttime = parseInt((endtime.getTime() - nowtime.getTime()) / 1000); 48 | var d = parseInt(lefttime / (24 * 60 * 60)); 49 | var h = parseInt(lefttime / (60 * 60) % 24); 50 | var m = parseInt(lefttime / 60 % 60); 51 | var s = parseInt(lefttime % 60); 52 | h = addZero(h); 53 | m = addZero(m); 54 | s = addZero(s); 55 | document.getElementById("contdown").innerHTML = "倒计时 " + d + ":" + h + ":" + m + ":" + s; 56 | if(lefttime<=0){ 57 | document.getElementById("contdown").innerHTML = "活动已结束"; 58 | return; 59 | } 60 | setTimeout(showtime,1000); 61 | } 62 | } 63 | ``` 64 | 65 | 这种实现方法比上面的写法好一些了,但也是面向过程的,无法扩展,实现上首先,定义了一个showtime方法,然后作为入口函数执行,有一个辅助方法addZero,作用是对数字补位比如9补成09。然后showtime函数中,对时间做了一个差值计算,然后换算了对应的倒计时天,时,分,秒。最后又对DOM做了渲染,这次不一样的地方是使用了setTimeout来进行递归渲染,没有使用setInterval。 66 | 67 | 那么问题出现了,这里涉及到几个点: 68 | 69 | * 补0操作的实现方法,怎么写比较好一点? 70 | * 为什么用setTimeout,又为什么要使用setInterval,到底选哪个好一点? 71 | 72 | leftPad操作的扩展 73 | ------------------- 74 | 75 | ```js 76 | //几个同学几组不同的补零方法实现: 77 | function leftPad(i){ 78 | if(i<10){ 79 | i = "0" + i; 80 | } 81 | return i; 82 | } 83 | function leftPad(i){ 84 | return i < 10 ? '0'+i : i+''; 85 | } 86 | function leftPad(n){ 87 | var n = parseInt(n, 10); 88 | return n > 0 ? n <= 9 ? ('0'+n) : (n+'') :'00'; 89 | } 90 | function leftPad(n, len){ 91 | len = len || 2; 92 | n = n + ''; 93 | var diff = len - n.length; 94 | if (diff > 0) { 95 | n = new Array(diff + 1).join('0') + n; 96 | } 97 | return n; 98 | } 99 | ``` 100 | 101 | 最后来一个之前一阵子曝光度非常高的npm的leftpad模块的实现。 102 | 103 | ```js 104 | function leftpad (str, len, ch) { 105 | str = String(str); 106 | var i = -1; 107 | if (!ch && ch !== 0) ch = ' '; 108 | len = len - str.length; 109 | while (++i < len) { 110 | str = ch + str; 111 | } 112 | return str; 113 | } 114 | ``` 115 | 116 | 简单分析一下,我们自己写的和老外们写的有何区别: 117 | 118 | * 第一种写法返回值有bug,大余10的返回的为number类型,一个js类型未转换的坑。 119 | * 第二种写法弥补了这个bug,知道转换类型,但是只考虑了一个参数,功能仅仅为补0。 120 | * 第三种写法考虑了负数的情况,会自动转换成00。 121 | * 第四种方法终于考虑到了,可能会补更多的0,用了一个创建指定长度数组的方法`[,,].join("0")`来避免了一次循环的使用。 122 | * 第五种,npm中的leftpad模块,考虑了第三种情况,那就是可能想补的不是0,而是空格啊,或者一些其他的什么别的东西,例如`leftPad("foo",5); //" foo"; leftPad("foo",5,0); //"00foo"` 当然他默认补的就是空格,补0要自己填第三个参数。 123 | 124 | 说到这里可见一斑,同样一个函数,不同的程序员考虑问题的方式方法和实现手段,真的是太奇妙了,当然这和经验,水平还有脑洞都有关系。 125 | 126 | setTimeout和setInterval如何选择? 127 | ------------------- 128 | 129 | 这个问题分2个点来说明: 130 | 131 | * 这2个函数的功能区别 132 | * javascript单线程的理解 133 | 134 | setTimeout是延迟一定毫秒后执行,setInterval是每隔一定毫秒后执行。但是真相并不像这两句话一样简单。比如我们这个倒计时例子里该选哪一个作为定时调用呢? 135 | 136 | 首先我们都知道javascript是单线程执行的,所以以上这2个方法都是会被线程阻塞的。 137 | 138 | 比如setInterval延迟设置为1000,如果内部的执行是一个耗时超过1秒的操作,那么每次重复执行的时候会造成2个问题: 139 | 140 | 1,是执行被阻塞,预期是1000毫秒执行一次,实际上必须在阻塞结束后才执行。 141 | 2,阻塞时,setInterval回调会被堆积,当阻塞结束后,堆积只会被消费一个,那么之后的堆积等于浪费了性能和内存。 142 | 143 | 如何解决堆积问题呢?因为阻塞是不可能被解决的,那么最简单的方法就是把setInterval换成setTimeout,使用一个递归来造成每隔多久执行一次的功能。当然阻塞是无法被解决的,这里的阻塞不仅仅有回调中的,还有浏览器中的方方面面的阻塞,比如用户的一些操作行为,其他定时器等外部的阻塞,所以这也就是无论我们如何做,页面开久了,定时器都会不准,或者说,变慢的根本原因。 144 | 145 | 理解了以上,我们就知道该如何选择了。那么我们如何来编写一个高性能可扩展的倒计时呢? 146 | 147 | 我们下面就针对以上的分析继续review代码。 148 | 149 | 到底如何编写面向对象的javascript 150 | ------------------- 151 | 152 | 以下的几个进化过程,组内实现都一一包含了,希望通过分析可以提高一些你对设计模式在js中的灵活理解: 153 | 154 | ```js 155 | var CountDown = { 156 | $ : function(id){/*id选择器*/}, 157 | init :function(startTime,endTime,el){/*执行定时器入口,使用setTimeout调用_timer*/}, 158 | _timer : function(startTime,endTime,el){/*私有方法,处理时间参数等具体业务*/} 159 | } 160 | CountDown.init("","2016,04,23 9:34:44","countdown1"); 161 | ``` 162 | 163 | 点评:单例模式,简单方便好理解,缺点是每次init都会拿一个新定时器,性能不好。继承和扩展能力一般,无法获取实例属性,导致了执行状态都是不可见的。 164 | 165 | ```js 166 | function Countdown(elem, startTime, endTime) { 167 | this.elem = elem; 168 | this.startTime = (new Date(startTime).getTime()) ? (new Date(startTime).getTime()) : (new Date().getTime()); 169 | this.endTime = new Date(endTime).getTime(); 170 | } 171 | Countdown.prototype = { 172 | SetTime: function() {}, 173 | leftPad: function(n) {}, 174 | DownTime: function() {} 175 | } 176 | var test = new Countdown("time", "2016/1/30,12:20:12", "2017/1/30,12:20:12"); 177 | test.SetTime(); 178 | ``` 179 | 180 | 点评:标准的原型构造器写法,简单方便好理解,确定是每次都拿一个新定时器,实例增多后性能同样不好,按道理setTime,leftPad等方法都可以通过继承来实现,方便扩展和复用,prototype上的方法均为辅助方法,按理不应该被外部调用,这里应该封装为私有方法或者前缀+_,优点可以通过实例拿到相关倒计时属性,可以对实例再做扩展操作。 181 | 182 | ```js 183 | var countdown = {}; 184 | countdown.leftPad = function(n, len) {}; 185 | countdown.timeToSecond = function(t) {}; 186 | /** 187 | * 倒计时工厂 188 | * @param {[object]} obj 倒计时配置信息 189 | * @return {[object]} 返回一个倒计时对象 190 | */ 191 | countdown.create = function(obj) { 192 | var o = {}; 193 | o.dom = document.getElementById(obj.id); 194 | o.startMS = +new Date(obj.startTime || 0); 195 | o.endMS = +new Date(obj.endTime || 0); 196 | obj.totalTime && (o.totalTime = countdown.timeToSecond(obj.totalTime)); 197 | 198 | var newCountdown = new countdown.style[obj.style](o); 199 | 200 | newCountdown.go = function(callback) { 201 | callback && (newCountdown.callback = callback); 202 | newCountdown.render(); 203 | clearInterval(newCountdown.timer); 204 | newCountdown.timer = setInterval(newCountdown.render, 1000); 205 | }; 206 | return newCountdown; 207 | }; 208 | countdown.style.style1 = function(obj) { 209 | this.dom = obj.dom; 210 | this.startMS = obj.startMS; 211 | this.endMS = obj.endMS; 212 | var _this = this; 213 | this.render = function() { 214 | var currentMS = +new Date(); 215 | var diff = (_this.endMS - currentMS) / 1000; 216 | var d = parseInt(diff / 60 / 60 / 24); 217 | d = countdown.leftPad(d, 3); 218 | d = d.replace(/(\d)/g, '$1'); 219 | _this.dom.innerHTML = '距离国庆节还有:' + d + '天'; 220 | if (currentMS > _this.endMS) { 221 | clearInterval(_this.timer); 222 | if (_this.callback) { 223 | _this.callback(); 224 | } else { 225 | _this.dom.innerHTML = '国庆节倒计时结束'; 226 | } 227 | } 228 | }; 229 | }; 230 | countdown.style.style2 = function(obj) {}; 231 | countdown.style.style3 = function(obj) {}; 232 | countdown.create({id:"clock3",totalTime:'82:23',style:'style1'}).go(function(){alert('It is over');}); 233 | ``` 234 | 235 | 点评:我尽量的减少了无用的干扰,留下最关键的部分。 236 | 237 | 优点:这里的countdown是一个比较简单的工厂模式实现,实现了一个统一的create方法,create方法上调用了style这个属性上扩展的样式(style1-3)实现,create方法返回的是一个独立的新实例,并统一扩展了go方法,go方法里统一创建定时器并挂载到timer属性,在这里我们也就等同拥有了修改和控制每个工厂造出来的单例的能力,样式做到了可扩展,leftPad,timeToSecond也可以方便通过一个utils对象来进行继承。 238 | 239 | 缺点:没有考虑到上面提到的setTimeout和setInterval的区别,也没有时间校验机制,在性能方面考虑不多。 240 | 241 | ```js 242 | var EventNotifys = []; 243 | var Event = { 244 | notify:function(eventName, data){}, 245 | subscribe: function (eventName, callback) {}, 246 | unsubscribe: function (eventName, callback) {} 247 | }; 248 | var timer = null; 249 | 250 | $.countDown = function(deadline,domParam){ 251 | var that = this, 252 | MILLS_OFFSET = 15; 253 | function CountDown(){ 254 | this.deadline = deadline; 255 | this.domParam = domParam; 256 | }; 257 | CountDown.prototype = { 258 | leftPad: function(n){}, 259 | /** 260 | * 计算时差 261 | * @returns {{sec: string, mini: string, hour: string, day: string, month: string, year: string}} 262 | */ 263 | caculate: function(){}, 264 | /*刷新dom*/ 265 | refresh: function(){} 266 | }; 267 | var countDown = new CountDown(); 268 | /** 269 | * 启动定时器 270 | * @param first 是否首次进入 271 | */ 272 | function startTimer(first){ 273 | !first&&Event.notify('TIMER'); 274 | //若是首次进入,则根据当前时间的毫秒数进行纠偏,延迟1000-当前毫秒数达到整数秒后开始更新UI 275 | //否则直接1秒后更新UI 276 | //若当前毫秒数大于MILLS_OFFSET 15,则修正延时数值与系统时间同步 277 | mills = new Date().getMilliseconds(); 278 | timer = setTimeout(arguments.callee,first?(1000 -mills):(mills>MILLS_OFFSET?(1000-mills):1000)); 279 | console.log(new Date().getMilliseconds()); 280 | } 281 | /** 282 | * 订阅一次事件 283 | */ 284 | Event.subscribe('TIMER',countDown.refresh.bind(countDown)); 285 | //首次初始化时启动定时器 286 | !timer && startTimer(true); 287 | }; 288 | 289 | /*dom结构和样式与js分离,这里指定倒计时的dom节点信息作为配置*/ 290 | $.countDown('20160517 220451',{ 291 | sec: $("#seconds6"), 292 | mini: $("#minute6"), 293 | hour: $("#hour6"), 294 | day: $("#day6"), 295 | month: $("#month6"), 296 | year: $("#year6") 297 | }); 298 | ``` 299 | 300 | 点评:这里也是因为篇幅问题,去掉了多余代码,只留下了核心思路实现。首先是实现了一个jquery的countDown的扩展方法,传入倒计时时间和需要操作的dom节点信息配置,然后实现了一个Event对象,俗称sub/pub观察者的实现,可以广播和订阅还有取消订阅,所有订阅回调存在EventNotifys数组中,之后每个$.countDown方法返回的都是一个标准的单例实例,原型上有一系列的辅助方法,这里的caculate返回的是格式化的时间方便渲染时分离操作,但是这个实例和上面的实例不一样的地方是不包含定时器部分,定时器部分的实现使用了一个timer来实现,然后通过广播的形式,让所有产生的实例共享一个定时器。这里就考虑到性能,页面无论多少个倒计时,都会保持最少的定时器实例。最后在每次订阅的回调中,刷新所有的实例dom,消耗也是最小,并且在每次递归后对阻塞进行了计算,如果误差超过一定阀值,则进行矫正。但是这里的矫正只是对阻塞时间差进行了矫正,并没有和系统时间矫正。 301 | 302 | 优点在上面的实现思路上已经大概说明白了,应该已经可以算是满分了。除了没有和系统时间做矫正,只是对阻塞做了矫正。 303 | 304 | 最后还有一个实现,思路上并没有上面这个一样做到定时器复用,但是优点是文档齐全,前后端(nodejs)通用,内部解耦做的也不错,但是性能上也没有考虑太多,还是使用了setInterval做了实现,比较出彩的是鲁棒性比较好,实现上也是先设计API后对库进行实现封装,最后也通过事件广播的方式对每个定时器做了外部事件解耦,具体参见: https://github.com/luoye-fe/countdown 305 | 306 | API设计确实仁者见仁,智者见智,但是一般的开发者还是考虑不到的,这里值得表扬,最后三个都属于组内很优秀的例子了。 307 | 308 | 最后看看我的吧,毕竟题目是我出的。 309 | 310 | https://gist.github.com/xiaojue/d42e69117b6d53ace81d56e454ce5141 (他们说gist需要翻墙才能看,那我还是以防万一,贴一下。。) 311 | 312 | ```js 313 | /** 314 | * @author xiaojue 315 | * @date 20160420 316 | * @fileoverview 倒计时想太多版 317 | */ 318 | (function() { 319 | 320 | function timer(delay) { 321 | this._queue = []; 322 | this.stop = false; 323 | this._createTimer(delay); 324 | } 325 | 326 | timer.prototype = { 327 | constructor: timer, 328 | _createTimer: function(delay) { 329 | var self = this; 330 | var first = true; 331 | (function() { 332 | var s = new Date(); 333 | for (var i = 0; i < self._queue.length; i++) { 334 | self._queue[i](); 335 | } 336 | if (!self.stop) { 337 | var cost = new Date() - s; 338 | delay = first ? delay : ((cost > delay) ? cost - delay : delay); 339 | setTimeout(arguments.callee, delay); 340 | } 341 | })(); 342 | first = false; 343 | }, 344 | add: function(cb) { 345 | this._queue.push(cb); 346 | this.stop = false; 347 | return this._queue.length - 1; 348 | }, 349 | remove: function(index) { 350 | this._queue.splice(index, 1); 351 | if(!this._queue.length){ 352 | this.stop = true; 353 | } 354 | } 355 | }; 356 | 357 | function TimePool(){ 358 | this._pool = {}; 359 | } 360 | 361 | TimePool.prototype = { 362 | constructor:TimePool, 363 | getTimer:function(delayTime){ 364 | var t = this._pool[delayTime]; 365 | return t ? t : (this._pool[delayTime] = new timer(delayTime)); 366 | }, 367 | removeTimer:function(delayTime){ 368 | if(this._pool[delayTime]){ 369 | delete this._pool[delayTime]; 370 | } 371 | } 372 | }; 373 | 374 | var delayTime = 1000; 375 | var msInterval = new TimePool().getTimer(delayTime); 376 | 377 | function countDown(config) { 378 | var defaultOptions = { 379 | fixNow: 3 * 1000, 380 | fixNowDate: false, 381 | now: new Date().valueOf(), 382 | template: '{d}:{h}:{m}:{s}', 383 | render: function(outstring) { 384 | console.log(outstring); 385 | }, 386 | end: function() { 387 | console.log('the end!'); 388 | }, 389 | endTime: new Date().valueOf() + 5 * 1000 * 60 390 | }; 391 | for (var i in defaultOptions) { 392 | if (defaultOptions.hasOwnProperty(i)) { 393 | this[i] = config[i] || defaultOptions[i]; 394 | } 395 | } 396 | this.init(); 397 | } 398 | 399 | countDown.prototype = { 400 | constructor: countDown, 401 | init: function() { 402 | var self = this; 403 | if (this.fixNowDate) { 404 | var fix = new timer(this.fixNow); 405 | fix.add(function() { 406 | self.getNowTime(function(now) { 407 | self.now = now; 408 | }); 409 | }); 410 | } 411 | var index = msInterval.add(function() { 412 | self.now += delayTime; 413 | if (self.now >= self.endTime) { 414 | msInterval.remove(index); 415 | self.end(); 416 | } else { 417 | self.render(self.getOutString()); 418 | } 419 | }); 420 | }, 421 | getBetween: function() { 422 | return _formatTime(this.endTime - this.now); 423 | }, 424 | getOutString: function() { 425 | var between = this.getBetween(); 426 | return this.template.replace(/{(\w*)}/g, function(m, key) { 427 | return between.hasOwnProperty(key) ? between[key] : ""; 428 | }); 429 | }, 430 | getNowTime: function(cb) { 431 | var xhr = new XMLHttpRequest(); 432 | xhr.open('get', '/', true); 433 | xhr.onreadystatechange = function() { 434 | if (xhr.readyState === 3) { 435 | var now = xhr.getResponseHeader('Date'); 436 | cb(new Date(now).valueOf()); 437 | xhr.abort(); 438 | } 439 | }; 440 | xhr.send(null); 441 | } 442 | }; 443 | 444 | function _cover(num) { 445 | var n = parseInt(num, 10); 446 | return n < 10 ? '0' + n : n; 447 | } 448 | 449 | function _formatTime(ms) { 450 | var s = ms / 1000, 451 | m = s / 60; 452 | return { 453 | d: _cover(m / 60 / 24), 454 | h: _cover(m / 60 % 24), 455 | m: _cover(m % 60), 456 | s: _cover(s % 60) 457 | }; 458 | } 459 | 460 | var now = Date.now(); 461 | 462 | new countDown({}); 463 | new countDown({ 464 | endTime: now + 8 * 1000 465 | }); 466 | 467 | })(); 468 | ``` 469 | 470 | 不要脸的自己点评一下自己:主要说思路,实现分成几个步骤,第一个类是timer,通过setTimeout创建一个倒计时实例,内部对阻塞误差做了矫正,但是缺少一步,如果阻塞大于延迟时间,那么应该下次递归直接执行,而我这里还是会延迟1s,只是相对会减少误差。然后这个timer通过add和remove方法管理一个回调队列,让所有通过这个timer实现的倒计时都共用一个定时器。第二个类TimePool,主要实现一个timer池子,我们之前只想到了倒计时都是1s的,那如果有500ms或者10s的倒计时,这里这个池子是可以装多个timer类的,保证不同延迟下,定时器最小。最后一个类就是countDown类,里面同样是默认配置起手,然后主要介绍`fixNowDate`这个参数,是用来进行系统时间修正的,这里默认的实现是浏览器端的一个修正技巧,主要参见`getNowTime`方法,通过一个xhr请求拿服务端headers中的Date来进行矫正。 471 | 472 | 之后摘出来了一个`template`,对输出的时间格式可以做一个简单的模板替换,抽象了3个公共类,最后的countDonw依赖其他2个类做了最终实现。最后,这段代码也可以复用在nodejs端,只需要修改`getNowTime`的方法为nodejs的即可。 473 | 474 | 我的这段实现,没有考虑太多样式的可扩展,更多是性能上的,但是真实情况下性能如何,还需要实际验证,这一步是没有做的,而且只适用页面多定时器和倒计时的场景,可能还存在一些隐藏的未知bug。所以在这里只是开了一下脑洞,做了一个初步的实现,拿走需谨慎。 475 | 476 | 最后,我不爱写注释,这里要严肃的批评一下我自己…… 477 | 478 | 总结 479 | ------------------- 480 | 481 | 通过一个最简单的倒计时功能,review过团队所有的实现之后,个人感觉收获最多的是,每个工程师的思维和经验确实决定了编码的高度,但是这种思维蛮好培养的。总结一下的话,主要表现在程序的扩展性,性能,鲁棒性,复用性,维护性这几方面。所以以后写代码时只要想的足够多,足够细致,我相信所有人都可以写出非常优秀的好代码的。 482 | 483 | 看完这篇文章后,你还觉得你真的知道怎么写一个javascript倒计时了吗?希望对所有阅读的人都能带来收获。 484 | 485 | 486 | 487 | 488 | --------------------------------------------------------------------------------