# SSR 踩坑记录 import 一个纯js插件,报错 document is undefined, 原因是js文件中直接使用了window。 在node不该出现window/alert/document这些只在浏览器中存在的变量。 在网上搜了一下,有以下几种解决方案: ## jsdom 地址:https://github.com/jsdom/jsdom 这个组件可以在node环境模拟浏览器环境,并输出window,大致用法如下: ``` const { JSDOM } = require("jsdom"); const dom = new JSDOM('<!doctype html><html><body></body></html>',{ url: 'http://localhost' }) global.window = dom.window; global.document = window.document; global.navigator = window.navigator; ``` 在使用中,暴露了一些问题。 首先,被Import的库文件是无法访问到global的变量,因此上述代码需要被添加到库文件,影响很不好。 其次jsdom的体积较大,编译后的大约增加了10M。 ## 动态引入 用require来代替import。import通常写在文件顶部做静态引入,而require不通,可以在代码中被动态引入,这样我们就可以创造条件,使得这部分代码避免在node环境中运行。 常用的手段有: - 利用vue组件的生命周期,某些环节只能在浏览器中运行。 - 利用process.env来辨别当前运行环境。 ## 在html文档中做外链 可以解决问题,但是完全没有发挥webpack的效用,不建议这么做。

# windows配置mongodb备忘 对比macOS,windows下使用MongoDB略显麻烦,为防止以后忘记,特记录一下~ ## 配置启动服务 ### 新建 mongo.config 文件如下 ``` dbpath=C:\data\db logpath=C:\data\log\mongo.log auth=false ``` ### 管理员启动cmd,设置服务,输入命令 ``` mongod.exe --config mongo.config --install --serviceName "mongo" ``` 如果以后需要移除服务,可以使用下面的命令: ``` mongod.exe --remove --serviceName "mongo" ``` ### 启动mongo cmd下输入命令: ``` net start mongo ``` ## 配置权限 ### 添加管理员账号 ``` use admin db.createUser({user:"root",pwd:"root",roles:["root"]}) ``` ### 为test库添加用户 ``` use test db.createUser({user:"test",pwd:"test",roles:[{role:"dbOwner",db:"test"}]}) use admin db.system.users.find() ``` ### 开启认证 mongo.config 中auth设置为true 开启认证前,数据库连接为: mongodb://localhost/test 开启认证后: mongodb://test:test@localhost/test

# Jquery插件封装为AngularJS(1.x)指令 Jquery以及Angular都是基于js的框架, 从理论上说, 共同使用不会存在语法问题, 但可能发生一些逻辑问题: > Angular使用双向数据绑定机制, 不直接操作dom. 如果Jquery直接对一个有着ng-model指令的元素进行操作或者增加事件,则有可能导致双向数据绑定失效. > Jquery将要操作的元素并没有直接写在html文档当中, 而是通过angular的指令动态生成的. 如果在加载页面之后立即执行Jquery操作, 则可能操作失败. ## 解决思路 > 寻找基于angularjs的对应插件 > 把Jquery插件封装成为directive指令, 监视model, 监听插件的事件, 手动做双向数据绑定. 下面是一个树形插件的例子. scope.select是我们想要双向数据绑定的对象, 根据它来get或者set树状的选中项. 初始化指令的时候, 根据select对象的内容, 初始化树状插件, 此时选中的项即为select的内容 当树状插件被用户更改时, 则立即刷新select的内容 ```js app.directive("permenusTree", [function($timeout){ return{ restrict:"A", scope:{ permenusTree:"=", select:"=", }, link:function(scope,ele,attrs,ctl){ console.log("init tree") var selectMode = $(ele).attr('selectMode'); scope.$watch("permenusTree",function(nv){ if(nv){ $(ele).fancytree({ checkbox: true, selectMode: parseInt(selectMode), source:nv, init:function(event, data){ scope.select = (data.tree.getRootNode()); }, select: function(event, data) { scope.select = (data.tree.getRootNode()); scope.$apply(); } }); } }); } }; }]); ```

# grunt-pubdnt ## A tool for publishing to docker ## 简介 一个简单轻巧的grunt插件,供web开发用,可以快捷更新代码到服务器的docker环境。 - 适用场景:远程更新存放在docker中的静态文件 - git地址:[grunt-pubdnt](https://github.com/maplesec/grunt-pubdnt) ## 安装及使用 ``` npm install grunt-pubdnt --save-dev ``` 在Gruntfile中增加如下代码: ``` grunt.loadNpmTasks('grunt-pubdnt'); ``` 在grunt.initConfig()中添加: ``` pubdnt: { default_options: { options: { target_server: ["host_ip"], target_port:"host_port", username:"your_name", password:"your_password", docker_name:"docker_name" } }, } ``` 执行 `grunt pubdnt` 则对项目根目录下的 `./dist` 文件夹进行打包,并拷贝到服务器的容器内,解压到 `/code/dist` 路径下 ## 主要功能的实现: 1. 打包 ``` var exec = require('child_process').exec; var cmdStr = "tar -zcvf dist.tar.gz *"; exec(cmdStr, {cwd: "dist/"}, function (err, stdout, stderr) { ... }); ``` 2. 文件传输 ``` var client_scp = require('scp2'); client_scp.scp('dist/dist.tar.gz', { host: my_target_server, username: username, password: password, path: '/root/' }, function (err, stdout) { ... }); ``` 3. 容器的模糊搜索、文件拷贝、解压 ``` $(docker ps -a | grep " + docker_name + " | grep Up | awk '{print $1}') ``` ``` var conn = new Client(); conn.on('ready', function () { console.log("unpressing files..."); conn.exec("docker cp dist.tar.gz $(docker ps -a | grep " + docker_name + " | grep Up | awk '{print $1}'):/code; docker exec -i $(docker ps -a | grep " + docker_name + " | grep Up | awk '{print $1}') tar zxf /code/dist.tar.gz -C /code;", function (err, stream) { ... }); }).connect({ host: my_target_server, port: target_port, username: username, password: password }); ``` ## 附 npm发布流程 1. 首先在npm官网注册账号 2. 设置本地npm registry (安装过cnpm,默认是taobao地址) ``` npm config set registry https://registry.npmjs.org/ ``` 3. 认证 ``` npm adduser ``` 4. 根据package.json的版本号发布 ``` npm publish ```

# ng 动态表单优化 在使用简单内容的动态表单时,如果内容没有key键,那么很容易想到的就是用index来代替key,代码如下 ``` <div class="row margin-bottom-15" *ngFor="let email of emails; let i=index;"> <div class="form-group col-sm-4" [ngClass]="setClass(myForm.controls['email'+i])"> <label [for]="'email'+i"> email <span class='symbol'></span> </label> <input nz-input type="text" [name]="'email'+i" [(ngModel)]="email"> </div> <div class="form-group col-sm-4"> <button *ngIf="emails.length>1" class="btn btn-danger" (click)="remove(i)">移除</button> </div> </div> ``` 使用index来区别每一个数据项的model、name和对应的校验,在数据的增加和编辑时是可以正常工作的,但是在数据的删除时,由于Index是连续的,将会导致数据的错乱。 于是,参考了ng-zorro里面的动态表单: ``` <nz-form-item *ngFor="let control of controlArray;let i = index"> <nz-form-label [nzXs]="24" [nzSm]="4" *ngIf="i==0" [nzFor]="control.controlInstance">Passengers</nz-form-label> <nz-form-control [nzXs]="24" [nzSm]="20" [nzOffset]="i==0?0:4"> <input nz-input style="width: 60%; margin-right:8px;" placeholder="placeholder" [attr.id]="control.id" [formControlName]="control.controlInstance"> <i class="anticon anticon-minus-circle-o dynamic-delete-button" (click)="removeField(control,$event)"></i> <nz-form-explain *ngIf="getFormControl(control.controlInstance)?.dirty&&getFormControl(control.controlInstance)?.hasError('required')"> Please input passenger's name or delete this field. </nz-form-explain> </nz-form-control> </nz-form-item> ``` ``` addField(e?: MouseEvent): void { if (e) { e.preventDefault(); } const id = (this.controlArray.length > 0) ? this.controlArray[ this.controlArray.length - 1 ].id + 1 : 0; const control = { id, controlInstance: `passenger${id}` }; const index = this.controlArray.push(control); console.log(this.controlArray[ this.controlArray.length - 1 ]); this.validateForm.addControl(this.controlArray[ index - 1 ].controlInstance, new FormControl(null, Validators.required)); } removeField(i: { id: number, controlInstance: string }, e: MouseEvent): void { e.preventDefault(); if (this.controlArray.length > 1) { const index = this.controlArray.indexOf(i); this.controlArray.splice(index, 1); console.log(this.controlArray); this.validateForm.removeControl(i.controlInstance); } } getFormControl(name: string): AbstractControl { return this.validateForm.controls[ name ]; } ``` 可以看到,ng-zorro增加了一个自增的id作为键。可以很好的解决index错乱的问题,缺点是引入了一个非必要的属性,向后台提交数据前,需要剔除冗余数据,非常麻烦。 As we all know, 对象可以设置不可枚举属性,在向后台提交数据时会自动忽略这类属性。 按照这个思路,封装了动态表单类,代码如下: ``` export class dynamicFormItems { controlArray: Array<any>; templateFields: Object; constructor(array: Array<any>, fields: Object){ this.controlArray = array; this.controlArray.forEach((item, index) => { if(!item.id){ Object.defineProperty(item, 'id', {value: index, enumerable: false}); } }) this.templateFields = fields; } addField(): void { const id = (this.controlArray.length > 0) ? this.controlArray[this.controlArray.length - 1].id + 1 : 0; const control = { ...this.templateFields } // 设置不可枚举的id Object.defineProperty(control, 'id', {value: id, enumerable: false}); this.controlArray.push(control); } removeField(control: Object):void{ if(this.controlArray.length > 1){ const index = this.controlArray.indexOf(control); this.controlArray.splice(index, 1); } } get(): Array<any> { return this.controlArray; } } ``` 在构造函数中,要求把目标数组以及目标模板作为参数传入。动态表单添加时,按照目标模板创建对象,指定不可枚举的键。 以下是使用示例: ``` this.dynamic = new dynamicFormItems(this.form.emails, { email: ''}) ``` ``` <div class="row margin-bottom-15" *ngFor="let email of dynamic.get(); let i=index;"> <div class="form-group col-sm-4" [ngClass]="setClass(myForm.controls['email'+email.id])"> <label [for]="'email'+email.id"> email <span class='symbol'></span> </label> <input nz-input type="text" [name]="'email'+email.id" [(ngModel)]="email"> </div> <div class="form-group col-sm-4"> <button *ngIf="dynamic.get().length>1" class="btn btn-danger" (click)="dynamic.removeField(email)">移除</button> </div> </div> <button (click)="dynamic.addField()" type="button"> 添加新的email </button> ``` 这样,在提交表单时,可以直接使用this.form.emails,而不必担心对象内有冗余的属性。

# ReactNative控制台的实现 仿阿里云的手机客户端,要实现控制台连接。github没有搜到合适的,所以决定自己实现。 ## 思路 之前在PC端使用过term.js,功能简单,逻辑清晰,但由于作者已经不再维护,因此本次使用xterm插件。https://github.com/xtermjs/xterm.js 实现的思路,就是在ReactNative中使用WebView加载网页,网页使用各种信息建立websocket连接,并初始化xterm插件。 跟PC端相比,移动端有以下不同点: * 手机端的软键盘,通常不具备Esc,Ctrl和方向键,需要额外实现软按钮 * 手机端的软键盘,可以收缩或弹出,要求控制台可以自使用缩放 * 软按钮和软键盘结合,实现组合键 (例如 ctrl+c) ## 软按键的实现 * 调研xterm的官方文档,寻找可以手动发送数据的接口。 找到了一个疑似的接口: write 但是测试后发现,这个接口仅仅是把明文写入到前端页面,并没有发送给websoket。 * 简单阅读以下xterm的源码,发现xterm对键盘的响应,是来源于一个textarea元素。当xterm初始化后,会立刻创建一个textarea,并绑定各种按键Listener。 因此想到了使用自定义事件来手动触发,如下: ``` $('textarea.xterm-helper-textarea')[0].dispatchEvent(new KeyboardEvent("keydown", { keyCode: 67 })); ``` 这行代码模拟了键盘按键c的操作,在PC上工作正常,但在android系统下无效。查看了相关论坛,发现不同核心的浏览器,对按键事件的处理方式不同。搜到了一位大神封装的函数,应对各个浏览器: ``` function fireKeyEvent(el, evtType, keyCode, ctrlKeyArg){ var ctrlKey = !!ctrlKeyArg; var doc = el.ownerDocument, win = doc.defaultView || doc.parentWindow, evtObj; if(doc.createEvent){ if(win.KeyEvent) { evtObj = doc.createEvent('KeyEvents'); evtObj.initKeyEvent( evtType, true, true, win, ctrlKey, false, false, false, keyCode, 0 ); } else { evtObj = doc.createEvent('UIEvents'); Object.defineProperty(evtObj, 'keyCode', { get : function() { return this.keyCodeVal; } }); Object.defineProperty(evtObj, 'which', { get : function() { return this.keyCodeVal; } }); evtObj.initUIEvent( evtType, true, true, win, 1 ); evtObj.keyCodeVal = keyCode; evtObj.ctrlKey = ctrlKey; if (evtObj.keyCode !== keyCode) { console.log("keyCode " + evtObj.keyCode + " 和 (" + evtObj.which + ") 不匹配"); } } el.dispatchEvent(evtObj); } else if(doc.createEventObject){ evtObj = doc.createEventObject(); evtObj.keyCode = keyCode; evtObj.ctrlKey = ctrlKey; el.fireEvent('on' + evtType, evtObj); } } ``` 例如你需要触发Esc按键时,执行代码: ``` fireKeyEvent($('textarea.xterm-helper-textarea')[0], "keydown", 27, false); ``` 其中27是Esc的keycode。经测试在安卓和IOS下均工作正常 ## 控制台自由缩放的实现 * 首先,在客户端使用KeyboardAvoidingView插件,防止键盘遮挡屏幕。 ``` <KeyboardAvoidingView behavior={'padding'} style={{flex: 1}}> <WebView> ... </WebView> </KeyboardAvoidingView> ``` 具体用法,可以参考RN官方文档 * 键盘展开或缩放,WebView的可视区域也响应变化。在网页内增加监听: ``` window.onresize = function(){ if(term){ term.resize(getUrlParam('width') / 7.5, parseInt((this.innerHeight - 40) / 14)) } } ``` 上述常量,根据具体的font-size以及网页布局来确定大小 ## 组合键的实现 1. 控制台最常用的组合键应该就是ctrl + c了。实现的思路是: > 用户按下虚拟按键 ctrl, 此时只是网页记录ctrl状态, 并不发送任何数据 > 在ctrl状态下,下一个软键盘的按键独立事件都会被禁止,使用一个组合按键事件来替换 2. xterm的自定义事件:attachCustomKeyEventHandler。 按照官方文档的描述,当有按键事件时,会执行回调,并根据回调的返回决定这个事件是否继续。 在PC端进行测试,可以监听到每个keydown事件。但是在安卓手机下,所有的keydown事件的键盘码都是229,可能是输入法导致。因此无法直接使用此接口。 3. 阅读xterm的源码,发现在有输入法的情况下,xterm并不依赖按键事件,而是根据textarea的变化量判断输入的内容。 很遗憾的是,xterm并没有对这方面暴露出相关接口。无奈,只能亲自修改源码。 ``` CompositionHelper.prototype._handleAnyTextareaChanges = function () { var _this = this; var oldValue = this._textarea.value; setTimeout(function () { if (!_this._isComposing) { var newValue = _this._textarea.value; var diff = newValue.replace(oldValue, ''); if (diff.length > 0) { if(_this._terminal.checkHandler(diff)){ _this._terminal.handler(diff); } } } }, 0); }; ``` _handleAnyTextareaChanges就是textarea发生变化后触发的事件,在其内部增加了自定义了函数checkHandler,并暴露出来。用法和attachCustomKeyEventHandler基本一致。 编写如下代码: ``` term.attachCheckHandler(function(key){ // 有输入法情况下,使用此函数捕捉键盘事件 if(ifControl){ // 接收到的数据是ascii字符,要根据对照表,转为对应的按键码 var output = Ascii2KeyCode(key.charCodeAt()); if(output && output != 229){ ifControl = false; // 发送组合键事件 fireKeyEvent($('textarea.xterm-helper-textarea')[0], "keydown", output, true); // 拒绝发送默认数据,阻止事件 return false; } } }) function Ascii2KeyCode(keycode){ var output; if(keycode >= 97 && keycode <= 122){ output = keycode - 32 }else if(keycode === 127){ output = 56 }else if(keycode === 91){ output = 219 }else if(keycode === 93){ output = 221 }else if(keycode === 229){ output = 229 } return output; } ```

共 6 条
  • 1
前往