动效对于页面状态的转移是很必要的,大部分时候的动效可以用css+js来实现,不过有些时候,因为产品上的需要,就需要使用按帧播放的动画来实现。其中比较常见的就是gif动图。

就比如我这边最近在项目里面用到的一个动图,原始图如下:

图片体积1.4MB,这么大的图片,肯定是不适合用来做动图。所以需要针对这张图做一些处理。

这里就要说到一个压缩gif图很专业的网站ezgif

经过裁剪,隔帧抽取,糢糊三步之后,最终的动图如下:

184kb

完成上述三步之后,出来的动图是184kb,基本上已经无法优化了。

不过如果网站的条件允许的话,还是有方法优化的。这就是gif to video

gif图本质上就是一个帧动画,里面存储了N张图片,然后按一定的顺序播放。它的存储是比较死板的,哪怕前后两帧的图片非常相似甚至是一模一样,它都会按两张图片来存储。在video里面就不一样了,视频格式会对所存储的帧画面做各种各样的优化。

还是以上面的动图为例,把最终效果的gif图转换为mp4格式:

转换后的视频体积是79kb,比gif图少了58.66%,压缩效果还是很明显的

问题

今天在重构之前写的一个小程序页面,在修改scroll-view组件之后,发现之前的scroll-into-view的跳转失效了

之前的页面是一直正常的,所以官方的关于scroll-view的一些tips是不会关联了

tip: 请勿在 scroll-view 中使用 textarea、map、canvas、video 组件
tip: scroll-into-view 的优先级高于 scroll-top
tip: 在滚动 scroll-view 时会阻止页面回弹,所以在 scroll-view 中滚动,是无法触发 onPullDownRefresh
tip: 若要使用下拉刷新,请使用页面的滚动,而不是 scroll-view ,这样也能通过点击顶部状态栏回到页面顶部

再仔细看官方的说明,发现有这么一句

使用竖向滚动时,需要给<scroll-view/>一个固定高度,通过 WXSS 设置 height。

当前我的页面的scroll-view是能滚动的,不过说不定问题可能出在这里,因为高度设定的100%

先尝试着手动把scroll-view改成一个比较小的高度,再试试调整scroll-into-view的值,发现可以了,那问题就出在height上了。

原因

在旧版本小程序里面navBar是系统自带的,设置height: 100%是能正确展示。

而新版本里面,在app.json里面配置了navigationStyle: 'custom',自己实现了一个自定义样式的导航栏。在调整样式的时候,会让内容区域margin-top: 128px;,这样就不会跟自定义的navBar冲突。

问题应该就出在这了。

scroll-view的父级只有margin-top,没有一个固定的高度,使得scroll-view的height: 100%失效,只要能重新计算出一个准确的高度,那问题就能完美解决。

解决

本来有想过调整scroll-view的父级,不过会是一个比较罗嗦的方法,所以想试试一些css里面的新技术:

scroll-view {
  height: calc(100vh - 128px);
}

基于calc()vh来动态计算scroll-view的高度

试了下效果,正确运行!

为了保证在各个设备和系统下能正确工作,查了一下caniuse:

vh

calc

可以看到这两个特性在iOS和Andriod下都是早早就支持了,可以安心使用的

总结

scroll-view需要一个明确的高度,这个高度的实现方式有两种:

  • 自身的css样式里面设置了明确的高度,vh, px, rem, em等等都可以
  • 自身的css样式里面height: 100%;同时直接父级有一个明确的高度

今天把最近一直在开发的小程序放安卓手机上测试一下,结果某个页面就一直报错:Uncaught TypeError: Converting circular structure to JSON

先说一下基本的环境:

系统:Android 6.0.1
手机:小米4
微信版本:6.6.6

小程序基于mpvue开发

在看到这个错误的时候,明白导致的原因应该是因为一个对象里面有循环引用,然后这个对象不幸的被JSON.stringify给调用了

比如下图这样的:

循环引用示例

可是这个有循环引用的对象在哪就不清楚了。

一开始想的是vue对象的data,因为小程序里面,jscore会把这个data stringify之后发送给webview来渲染页面。一直沿着这个思路在debug

而我出错的那个页面的data很简单

{
  list: [],
  currPage: 1,
  pageSize: 10,
  isEnd: false,
  isLoading: false
}

唯一可能出问题的地方也就list了,可是试了很多方法,都证明了list不是有循环引用的点。

无奈只好google看看大家的解决方案,然后在mpvue的github的issues里面发现有一个类似的错误的issue

虽然引发错误的原因不一样,不过最后最底下 @anchengjian的一个提示给了我另外一个方向:

除了JSON.stringify之外,console也可能导致类似的错误。

根据这个新的思路,从新读了一遍代码,发现了一句console.log(this),当初调试的时候,为了方便,把整个vue的实例给log了。注释掉这一行,重新编译,在安卓上预览,果然,一切正常了。

这个console.log在开发者工具和ios上都没问题,可到安卓上出了问题。可能是安卓上,微信的x5实现console.log的时候,先调用了JSON.stringify吧

这个问题不想深究了,不过被这种问题耽搁了半天又无益于技术进步,想想还是记录下来,方便后来者。

最近使用Nestjs来搭建梦支点的后台服务,头一回正式的尝试用NodeJs来做一次项目,很多后端的面向对象的概念,弄的有点窒息。看文档的几天,着实头大。

不过好在抗了过来,顺利进入项目开发阶段。

在执行npm run start:watch完成项目的开发之后,打算就按现有完成的功能先把后台上线了让大家使用,然后出了问题。

整个开发过程中,没有尝试过去编译和基于上线环境执行,真到这一步的时候就不行了。

执行npm run prestart:prod,基于tsc来编译项目,一切正常。

再执行命令npm run start:prod,基于编译完的代码来启动服务,失败了。错误大致如下:

npm run start:prod

> nest-typescript-starter@1.2.0 prestart:prod /path/to/project
> tsc


> nest-typescript-starter@1.2.0 start:prod /path/to/project
> node dist/main.js

[Nest] 41866   - 2018-1-28 14:17:30   [ExceptionHandler] Unexpected token import
/path/to/project/src/entites/exchange-orders.entity.ts:1
(function (exports, require, module, __filename, __dirname) { import {Entity, Column, PrimaryGeneratedColumn} from 'typeorm';
                                                              ^^^^^^

SyntaxError: Unexpected token import
    at createScript (vm.js:80:10)
    at Object.runInThisContext (vm.js:139:10)
    at Module._compile (module.js:607:28)
    at Object.Module._extensions..js (module.js:654:10)
    at Module.load (module.js:556:32)
    at tryModuleLoad (module.js:499:12)
    at Function.Module._load (module.js:491:3)
    at Module.require (module.js:587:17)

看错误,是nodejs识别不了import这个标识符,可是编译之后的代码没有import了。而且我这边的代码执行,是都执行的编译之后dist目录的,为啥会依赖了src目录下的文件,还报错了?

仔细看了一下dist目录里面的文件,都是相对路径引用相关模块,不存在会引用到src。

在网上用搜索一番之后,把问题定位到了typeorm上。在这个项目里面,按照官方在sql上的最佳实践,使用typeorm来管理数据。为了方便管理mysql连接的相关配置,就在项目根目录新建了一个ormconfig.json

{
  "type": "mysql",
  "host": "localhost",
  "port": 3306,
  "username": "user",
  "password": "pwd",
  "database": "db",
  "entities": ["src/**/**.entity{.ts,.js}"],
  "synchronize": true
}

基于entities指定的通配符,就能实现entity的自动加载。

看来问题也就出现在这个配置上了,肯定是在production环境下,typeorm还是加载了src目录下的entity,这样自然就出了问题。

所以解决方法就是如何能让这个配置根据环境自动去调整entities的值,找到了typeorm上关于这个配置文件的文档, 里面说到可以以js文件的方式输出配置。

这样一来就可以动态了。

修改后的ormconfig.js如下:

const SOURCE_PATH = process.env.NODE_ENV === 'production' ? 'dist' : 'src';
module.exports = {
  "type": "mysql",
  "host": "localhost",
  "port": 3306,
  "username": "user",
  "password": "pwd",
  "database": "db",
  "entities": [`${SOURCE_PATH}/**/**.entity{.ts,.js}`],
  "synchronize": true
};

然后在package.json里面修改start:prod命令:

"start:prod": " NODE_ENV=production node dist/main.js"

重新执行代码,顺利通过!!

react-router下,基于Switch,可以实现针对未匹配的路由调用指定的component来展现

<Switch>
  <Route exact path="/" component={Home}/>
  <Route path="/about" component={About}/>
  <Route path="/:user" component={User}/>
  <Route component={NoMatch}/>
</Switch>

现在的需求是:在发现有未匹配的路由时,自动跳转到首页

如果把上面的代码改成:

<Switch>
  <Route exact path="/" component={Home}/>
  <Route path="/about" component={About}/>
  <Route path="/:user" component={User}/>
  <Route component={Home}/>
</Switch>

这个可以实现的时,未匹配的路由展现首页的内容,可是当前的url还是那个未被匹配的路由。这样的显示就会有点怪异。

针对这个问题,可以使用react-router里面的另外一个组件Redirect

基于Redirect新建如下组件:

const RouteFallback = (props) => {
    console.log('route fallback with location: ', props.location);
    return <Redirect to={{
        pathname: '/',
        from: props.location
    }} />
}

再重新调整Switch:

<Switch>
  <Route exact path="/" component={Home}/>
  <Route path="/about" component={About}/>
  <Route path="/:user" component={User}/>
  <Route component={RouteFallback}/>
</Switch>

这样就能实现针对非法路径的自动跳转首页的功能了

起因

今天上午把双十二活动相关的页面给上线了,这次的发版,涉及的内容点比较多,上线的时候,没仔细想,打了一个tag就直接在线上git pull,然后活动页挂了…

糟糕的一连串操作

我是用的pm2 list查看的项目的状态,发现状态变成errored才知道出了问题。

这时候一回想才意识到,有些模块没安装。

在本地执行tig -- package.json,快速的查找两个版本之间都安装过哪些模块,然后对应的在线上npm install。但是一直被webpakcuglifyjs-webpack-plugin干扰的没法正常安装。

一狠心,rm -rf webpack直接把webpack给移除了。再次执行安装,还是同样的错误,再找到node_modules/.bin/webpack,移除(后面才知道用npm rm/uninstall 才是更正确的做法)。

webpack的错误总算没了,然后安装又报403的错误,自有的npm服务verdaccio出错了。

这时候终于意识到,问题不是一会儿能解决的,时间来来回回也过了十分钟,赶紧回滚服务到稳定版不:

  1. git checkout v1.18.1,把代码回滚到之前稳定版本。
  2. 修改pm2的配置文件ecosystem.config.js,配置关闭watch模式,这样后面安装模块和分支切换不会干扰正在运行的服务
  3. pm2 restart来重启服务,顺利关闭watch模式,服务本身也没有报错。

回滚完成。

接下来先是登上内网服务器来修复npm服务的问题:

  1. ps aux | grep verdaccio找到verdaccio的进程号,kill掉
  2. verdaccio & 重启verdaccio
  3. 在线上尝试npm install一个模块,还是有问题
  4. 发现线上访问npm服务用的域名是之前废弃的
  5. 编辑~/.npmrc 调整相关配置
  6. 再次安装,成功通过

现在npm服务正常,开始来面对之前出现的webpack出错的问题。我这个是线上服务器,是用不上webpack的,在安装模块的时候,出现这样的问题,很不正常。

Google了一下,发现可以用npm install --only=production来让npm只安装package.json里面dependencies部分的模块。

编辑package.json,整理dependencies和devDependencies,使得对应的模块在对应的分类中。

执行npm install --only=production安装所有更新,之前查package.json的编辑历史那一步是多余的。

更新完毕之后,

  1. git checkout .去掉所有临时修改的文件。
  2. git checkout master回到master分支
  3. pm2 restart来重启服务
  4. pm2 listpm2 log来检测状态和日志

一切正常,完成上线。

后续操作

在本地,重新审查了一遍package.json,一个模块一个模块的查找,去掉代码里面已经没有使用的模块,划分清除相关模块归属。

编辑ecosystem.config.js,配置development环境里面开启watch模式,production环境禁用。

打tag。

整个操作算是完成。

总结

这次的发版算是一次当头棒喝,在尝试用NodeJs来做后端的前台服务这么久,一直顺风顺水,有点忘却应有的严谨。

后端代码的线上环境没法跟前端这样明确,很多时候一个git pull没法解决所有问题。

需要一个持续集成的好的工作流。

这次出问题的一个主要原因就是太长时间的一个大的任务分支,导致整个项目跨度太大,这样在合并主分支并上线的时候风险自然就大了。

下次上线,一个正确的更新应该类似这样:git pull;npm install --only=production;pm2 restart {server}

还是不打算把node_modules/目录的内容放到git仓库里,虽然这样可以保证模块的同步,可是会增加仓库的体积,也会对其他成员的开发环境造成干扰。

或许可以考虑用Docker来包裹服务,保证其代码和环境的一致性。

最近在尝试使用styled-components来做React下的样式开发,这样可以利用js语言的灵活来增强css的能力。

在多屏适配这块,一直是在使用淘宝的lib-flexible

在用styled-components处理px到rem的转换时,有点问题了。styled-components自己本着基础库的原则,是没有附加这些复杂的配置,所以一开始想的是自己写一个函数来做单位换算:

function r(pxValue) {
    const ratio = 20; // 根据项目配置比例的方式自行设定
    pxValue = parseInt(pxValue);

    return pxValue / ratio + 'rem';
}

然后在写样式的时候这样写

// 保留了px的单位而不是传递数字,使得代码具有可读性,方便维护
let AvatarImage = styled(Image)`
    width: ${r('132px')}; 
    height: ${r('132px')};
`;

这样满足了基本需求。

不过作为喜欢偷懒喜欢折腾的程序员,总觉得这样写很啰嗦,所以想着能不能也跟styled-components那样使用标签模版的功能

所以也就有了第二版的function r:

function r(pxValue) {
    const ratio = 20; // 根据项目配置比例的方式自行设定

    // 针对template literals
    if (Array.isArray(pxValue)) {
        pxValue = pxValue[0];
    }

    pxValue = parseInt(pxValue);

    return pxValue / ratio + 'rem';
}

在样式书写上,就可以这样写了:

let AvatarImage = styled(Image)`
    width: ${r`132px`};
    height: ${r`132px`};
`;

少写了两个括号,看着也清晰多了。

不过还是不太满意,因为样式里面如果数值一多,写起来还是有点费事。

既然能够实现单个单位的换算,那用正则表达式匹配所有样式字符串里面的px值,再替换为rem值,再把计算的结果返回给styled-components不就可以了。

想法是好的,不过在读取解析已有的样式模版字符串,正则匹配,还有如何把结果传递给styled-components这些方面,坑还是很多的,折腾了一个多小时,也总算解决了。

这里面,最核心的解析已有的样式模版字符串,styled-components提供了一个helper: css,使得工作量大大减少。

function r(pxValue) {
    const ratio = 20; // 根据项目配置比例的方式自行设定

    // 针对template literals
    if (Array.isArray(pxValue)) {
        pxValue = pxValue[0];
    }

    pxValue = parseInt(pxValue);

    return pxValue / ratio + 'rem';
}

// 把字符串样式里面的px单位换算成rem的
// 支持多行匹配
function transformPxToRem(style) {
    // 避免处理了函数等情况
    if (typeof style !== 'string') {
        return style;
    }

    return style.replace(/\d+px/gm, matched => {
        return r(matched);
    });
}

// 实现在把样式传递给styled之前,预先用transformPxToRem处理
function t(strings, ...interpolations) {
    let styles = css(strings, ...interpolations); // css是styled-components的一个helper
    styles = styles.map(transformPxToRem);

    // 模拟raw的调用
    return [[""], styles]
};

基于以上代码,在最终书写样式的时候就可以这样:

let AvatarImage = styled(Image)(...t`
    width: 132px;
    height: 132px;
`);

以函数的形式调用styled-compoent,而不是标签模版的形式,同时解构t函数的返回值,这样的结果和styled-components直接计算标签模版是一样的。

不过在实现这个功能之后,在写这篇文章的时候,发现lib-flexible不再用rem作为多端适配的方案了,而是改用的vm

不过最核心的预处理部分已经实现了,后面的调整还是很容易做的。

后期的计划:

现在css helper返回的是一个数组,可以考虑join成字符串,去掉多余的空白符,然后实现css to object和object to css。然后再加上plugin机制,使得整个系统更加灵活多变。

说着说着,好像实现了一个简版的postCss。

现在styled-components是有增加babel plugin的计划来打通postCss和styled-compoent,期待。

起因

今天尝试使用webpck的import()来做代码分割。

代码类似如下:

import('./nice-scroll').then(init => init(dom))

结果报错:

ERROR in ./js/utils/auto-set-height.js
Module build failed: SyntaxError: ‘import’ and ‘export’ may only appear at the top level (20:8)

分析

在package.json里面确认了一下,我用的webpack版本是^3.5.4,这个是一定支持import()方法的,那么问题应该就出在babel上了。

先截取我的babel-loader的部分配置:

use: {
    loader: 'babel-loader',
    options: {
        // 不采用.babelrc的配置
        "babelrc": false,
        "presets": [
            ["react"],
            ["es2015"]
        ],
        "plugins": [
            "transform-object-rest-spread",
            "transform-class-properties"
        ]
    }
}

一开始我的猜想是plugin es2015里面的transform-es2015-modules-commonjs先于webpack处理了代码,所以报错。

找了一下禁用的方法,改配置如下:

use: {
    loader: 'babel-loader',
    options: {
        // 不采用.babelrc的配置
        "babelrc": false,
        "presets": [
            ["react"],
            ["es2015", {module: false}]
        ],
        "plugins": [
            "transform-object-rest-spread",
            "transform-class-properties"
        ]
    }
}

还是不行。

后面一直各种关键词的搜索,偶然在github上面找到这个问题Dynamic `import()` crashing in `ModuleConcatenationPlugin`的一个回答:

Nope; babel will always process the code before webpack ever sees it; unless for some reason the file itself is being excluded from being processed by babel-loader.
This error is unrelated to babel; it’s a webpack issue.

这个回答,里面说到在webpack面对的代码都是babel处理过的,这个让我坚信问题还是在babel这块,继续搜索。

意外找到这个这篇文章:代码分离 – 使用import()。里面说到了一个插件:syntax-dynamic-import

Babel官方关于这个插件的描述是:

Syntax only
This plugin only allows Babel to parse this syntax. If you want to transform it then see dynamic-import-webpack or dynamic-import-node.

显然这是一个语法解析的插件,使得babel能够理解dynamic import的语法。再联系上面的报错信息里面的SyntaxError,结果有点明显了。

解决

npm install --save-dev babel-plugin-syntax-dynamic-import

然后调整babel-loader配置如下:

use: {
    loader: 'babel-loader',
    options: {
        // 不采用.babelrc的配置
        "babelrc": false,
        "presets": [
            ["react"],
            ["es2015", { "modules": false }]
        ],
        "plugins": [
            "syntax-dynamic-import",
            "transform-object-rest-spread",
            "transform-class-properties"
        ]
    }
}

重启webpack,顺利通过编译!!!

最近一直有事没看博客,直到昨晚Google给发了封邮件,说检测到服务器上的错误呈现增加趋势

乘今天有点空闲,上了服务器看看。

发现wordpress和mysql这两个docker container都是正常运行,没挂掉。

按照以前的套路

docker restart mysql
docker restart wordpress

结果不行,看来不是小问题了。

针对这两个docker分别用docker logs命令查看了日志,mysql的没发现什么比较明显的错误报告,但是wordpress里面就满屏都是了

Warning: mysqli::mysqli(): (HY000/2002): Connection refused in - on line 10

MySQL Connection Error: (2002) Connection refused

上网找了半天,发现有人说可以通过重启docker来修复

试了一下,我的服务器是CentOS的,命令如下:

service docker stop
service docker start

重启完了之后,发现wordpress的container停掉了,用docker start wordpress给启用了

再次刷新网站,恢复正常了!