From 8b811fb448131ea5f1365333b73966e522338fa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E7=99=BD=E5=BD=AC?= <30487257+cumt-robin@users.noreply.github.com> Date: Thu, 14 Nov 2024 16:23:51 +0800 Subject: [PATCH] Delete init-scripts directory --- init-scripts/init.sql | 1059 ----------------------------------------- 1 file changed, 1059 deletions(-) delete mode 100644 init-scripts/init.sql diff --git a/init-scripts/init.sql b/init-scripts/init.sql deleted file mode 100644 index 97bb759..0000000 --- a/init-scripts/init.sql +++ /dev/null @@ -1,1059 +0,0 @@ -/* - Navicat MySQL Data Transfer - - Source Server : txy - Source Server Type : MySQL - Source Server Version : 80013 - Source Host : 118.24.73.29:3306 - Source Schema : blog_db - - Target Server Type : MySQL - Target Server Version : 80013 - File Encoding : 65001 - - Date: 14/11/2024 10:59:04 -*/ - -SET NAMES utf8mb4; -SET FOREIGN_KEY_CHECKS = 0; - --- ---------------------------- --- Table structure for article --- ---------------------------- -DROP TABLE IF EXISTS `article`; -CREATE TABLE `article` ( - `id` int(10) NOT NULL AUTO_INCREMENT, - `article_name` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '标题', - `article_text` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '正文markdown', - `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - `update_time` datetime NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, - `author_id` int(10) NOT NULL COMMENT '作者id', - `read_num` int(10) NULL DEFAULT 0 COMMENT '阅读量', - `like_num` int(10) NULL DEFAULT 0 COMMENT '喜欢', - `summary` varchar(3000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '摘要', - `poster` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '封面', - `private` tinyint(1) NULL DEFAULT 0 COMMENT '是否私密', - `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '是否已经被逻辑删除', - PRIMARY KEY (`id`) USING BTREE, - INDEX `author_id`(`author_id` ASC) USING BTREE, - CONSTRAINT `article_ibfk_1` FOREIGN KEY (`author_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE = InnoDB AUTO_INCREMENT = 276 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic; - --- ---------------------------- --- Records of article --- ---------------------------- -INSERT INTO `article` VALUES (147, '解决npm install卡住不动的小尴尬', '## 遇到的问题\n\n```\nnpm install -g @angular/cli\n```\n\n安装angular cli工具时,发现进度条一直卡住不动,相信很多朋友也遇到过。原因应该是国内的网络连接npm速度较慢,甚至很多东西都无法下载安装。那么如何解决这个问题呢?\n## 方案一:安装cnpm镜像\n这个是比较常用的方法,我首先也是使用了这个方法。cnpm的安装方法,参考 https://github.com/cnpm/cnpm\n\n```\nnpm install cnpm -g --registry=https://registry.npmmirror.com\n```\n\ncmd输入以上命令就可以了,然后输入\n\n```\ncnpm install -g @angular/cli\n```\n\n后面的操作跟不使用镜像的操作是差不多的。\n但是笔者在后续使用过程中遇到了一些问题,运行ng eject后发生了一些错误,所以放弃了这个方案,采用了方案二。\n\n## 方案二:使用代理registry\n在网上查阅了一些资料后,决定使用代理的方式,方法也很简单,就是\n\n```\nnpm config set registry https://registry.npmmirror.com\n```\n\n然后后续的install等命令还是通过npm运作,而不是cnpm。\n有点小强迫症的我还是喜欢npm install...', '2019-02-21 23:42:52', '2024-07-25 07:02:51', 1, 69, 0, '安装angular cli工具时,发现进度条一直卡住不动,相信很多朋友也遇到过。原因应该是国内的网络连接npm速度较慢,甚至很多东西都无法下载安装。那么如何解决这个问题呢?', 'https://qncdn.wbjiang.cn/npm%E9%80%9A%E7%94%A8.jpg', 0, 0); -INSERT INTO `article` VALUES (148, '前端自动化工具gulp之入门基础', 'gulp是前端开发过程中经常要用到的工具,非常值得花时间去掌握。利用gulp,我们可以使产品流程脚本化,节约大量的时间,有条不紊地进行业务开发。本文简单讲一下入门gulp需要掌握的东西。\n## 安装gulp\n首先,我们需要在全局安装gulp。\n\n```\nnpm install -g gulp\n```\n\n然后,我们切到项目根目录,在项目中也进行gulp的安装,表明项目对gulp的依赖。\n\n```\nnpm install --save-dev gulp\n```\n\n接着,我们在项目根目录里新建一个gulpfile.js文件,这个是gulp的配置文件。\n## 使用gulp\n学习gulp的使用,我们需要先学习好常用的语法。\n### gulp.src(globs[, options])\n输出符合所匹配模式(glob)的文件。将返回一个stream,可以被piped传递到其他gulp插件中继续操作。\n### gulp.task(name[, deps], fn)\n定义一个gulp任务,name是任务名称。[, deps]是任务依赖。fn是任务回调函数。\n(1)任务依赖的形式可以是:\n\n```\ngulp.task(\'two\', [\'one\'], function() {\n // \'one\' 完成后\n});\ngulp.task(\'one\', function(cb) {\n // cb();\n // return stream;\n // return promise;\n});\n```\n\n其中one应该使用一个callback,或者返回一个promise 或stream,表明依赖的任务完成了。\n(2)回调函数体会是这种形式:\n\n```\ngulp.src().pipe(someplugin())\n```\n\n### gulp.dest(path[, options])\n将pipe进来的stream输出文件到指定的路径下,如:\n\n```\ngulp.src(\'./client/templates/*.jade\')\n .pipe(jade())\n .pipe(gulp.dest(\'./build/templates\'))\n```\n\n### gulp.watch\n#### gulp.watch(glob [, opts], tasks)\n监视文件,并且在文件发生改动时候执行一个或者多个task。监听change事件实现。\n\n```\nvar watcher = gulp.watch(\'js/**/*.js\', [\'uglify\',\'reload\']);\nwatcher.on(\'change\', function(event) {\n console.log(\'File \' + event.path + \' was \' + event.type + \', running tasks...\');\n});\n```\n\n#### gulp.watch(glob [, opts, cb])\n监视文件,并且在文件发生改动时候执行回调函数cb。\n\n```\ngulp.watch(\'js/**/*.js\', function(event) {\n console.log(\'File \' + event.path + \' was \' + event.type + \', running tasks...\');\n});\n```\n\n### gulp.run\ngulp模块的run方法,表示要执行的任务。可能会使用单个参数的形式传递多个任务。任务是尽可能多的并行执行,并且可能不会按照指定的顺序执行。当不需要指定执行顺序时,可以适当使用。\n\n```\ngulp.run(\'task1\',\'task2\',\'task3\');\n```\n\n \n## gulp使用技巧\n### 替代任务依赖写法\n我们需要让任务有秩序地执行,那么可以使用第三方插件gulp-sequence。\n\n```\n// 如果使用gulp-sequence,就不需要附加任务依赖了。数组内的任务平行执行,数组外的按顺序执行。所以这里是svgstore、uglify-js并行执行,然后执行public任务。\ngulp.task(\'sequence1\', function() {\n sequence([\'svgstore\', \'uglify-js\'], \'public\');\n});\n```\n\n### 修复gulp.watch方法只执行一次的问题\n利用gulp-watch,gulp-batch两个工具,用法如下:\n\n```\n// 当监听到svgs目录下任意svg文件变动时,执行svgstore任务\ngulp.task(\'watch\', function() {\n watch(\'./assets/svgs/*.svg\', batch(function(events, done) {\n gulp.start(\'svgstore\', done);\n }));\n});\n```', '2019-02-22 00:02:02', '2024-10-11 19:10:37', 1, 48, 0, 'gulp是前端开发过程中经常要用到的工具,非常值得花时间去掌握。利用gulp,我们可以使产品流程脚本化,节约大量的时间,有条不紊地进行业务开发。本文简单讲一下入门gulp需要掌握的东西。', 'https://qncdn.wbjiang.cn/gulp.png', 0, 0); -INSERT INTO `article` VALUES (149, '解决Chrome的表单密码自动填充问题', '一般的浏览器都会默认开启一个表单自动填充的功能。这给很多用户带来了方便。但是对于项目开发者来说,有时候这就是噩梦。对安全性有考虑的项目,应该都会考虑到禁用这种自动填充的功能。否则,一个用户登录后,浏览器记住了用户名和密码。当另一个人使用这台电脑时,他肯定不用输入什么,就可以登入别人的账号,这可是很危险的事情。\n\n# 问题背景\n\n当我使用原始的表单成功登录一次后时,再次打开浏览器,什么都没有输入,就出现了这样的现象。\n![chrome自动填充](http://qncdn.wbjiang.cn/chrome%E8%87%AA%E5%8A%A8%E5%A1%AB%E5%85%85.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n浏览器帮忙记住了用户名和密码。有的人会说,这个OK啊,让用户把浏览器的填充表单的功能禁止掉不就行了?\n![chrome禁用自动填充](http://qncdn.wbjiang.cn/chrome%E7%A6%81%E7%94%A8%E8%87%AA%E5%8A%A8%E5%A1%AB%E5%85%85.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\nwhat?你指望让用户来完成本应该由你的程序完成的事情?领盒饭走人吧!\n\nso,怎么解决这个问题呢?\n\n# 解决过程\n\n首先想到的是通过autocomplete=\"off\"属性来禁止自动填充,然而发现好像没有起到作用。\n\n接下来我查到的信息是,浏览器会寻找表单中的输入框,自动填充。\n\n```\n\n\n```\n\n灵机一动,我想到在我的真实表单输入框前面放一个隐藏的输入框,如果这个隐藏的输入框代替真实的输入框被浏览器填充,那么问题不久解决了吗。于是,代码变成这样的。\n\n```\n\n \n \n
\n \n \n \n \n
\n
\n \n \n \n \n
\n
\n \n
\n
\n```\n\n再次尝试,发现打开登录页后,浏览器没有填充我的el-input了,顿时感觉轻松了下来。然而,当我点击密码框的时候,出现了密码列表。。。坑爹啊!\n![密码列表](http://qncdn.wbjiang.cn/%E5%AF%86%E7%A0%81%E4%B8%8B%E6%8B%89%E5%88%97%E8%A1%A8.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n还能怎么办,接着寻找解决方案吧!接下来找了很多方法,尝试后都不是有效的。\n\n于是我尝试先去掉自动填充后的屎黄色。。。\n![自动填充后的屎黄色](http://qncdn.wbjiang.cn/%E8%87%AA%E5%8A%A8%E5%A1%AB%E5%85%85%E7%9A%84%E9%BB%84%E8%89%B2%E8%83%8C%E6%99%AF.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n这个填充色是由浏览器伪类input:-webkit-autofill来实现的\n![-webkit-autofill](http://qncdn.wbjiang.cn/webkitautofill%E5%B1%9E%E6%80%A7.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n于是我加了这些代码\n\n```\ninput:-webkit-autofill {\n background-color: #fff;\n}\n```\n但是仍然没有去掉浏览器自带的填充背景色。我就想是不是没有加!important。最后加上了也没有用。看来并不能覆盖掉自带的这个样式属性啊。\n\n在网上搜索后,发现了一个神解决方法,使用盒子阴影来盖住屎黄色,真的可以做到。为这位网友的机智点赞!\n```\ninput:-webkit-autofill {\n box-shadow: 0 0 0px 1000px white inset;\n -webkit-box-shadow: 0 0 0px 1000px white inset;\n}\n```\n\n虽然解决了这个屎黄色背景的问题,但是没有从根本上解决我的需求。接下来的寻找答案的过程中,发现了一个奇妙的解决方案。\n```\n\n```\n\n就是这么简单,去掉了自动填充的烦人功能。说是autocomplete除了on,off以外的值就可以做到。但是我发现除了new-password,没有其他的值可以有效,Amazing!\n\n![问题解决](http://qncdn.wbjiang.cn/%E5%8E%BB%E9%99%A4%E8%87%AA%E5%8A%A8%E5%A1%AB%E5%85%85%E6%88%90%E5%8A%9F.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)', '2019-02-22 00:07:01', '2024-09-03 00:17:01', 1, 34, 0, '一般的浏览器都会默认开启一个表单自动填充的功能。这给很多用户带来了方便。但是对于项目开发者来说,有时候这就是噩梦。对安全性有考虑的项目,应该都会考虑到禁用这种自动填充的功能。否则,一个用户登录后,浏览器记住了用户名和密码。当另一个人使用这台电脑时,他肯定不用输入什么,就可以登入别人的账号,这可是很危险的事情。', 'https://qncdn.wbjiang.cn/chrome%E5%9B%BE%E7%89%87.png', 0, 0); -INSERT INTO `article` VALUES (150, '干货!Git 如何使用多个托管平台管理代码', '考虑到github不能免费创建私有仓库原因,最近开始在使用码云托管项目,这样避免了连接数据库的用户密码等信息直接暴露在公共仓库中。今天突然想到一个点,就是能不能同时把代码推送到github和码云上呢?答案是可以的。\n\n# 背景\n\n首先,我们在开始一个项目时,在本地写了一些代码,需要同时托管到github和码云(gitee)上。这个时候我们要怎么办呢?请接着看。\n\n# 实现方法\n\n## 添加密钥对\n\n在C:\\Users\\robin\\.ssh目录下运行git bash\n```\n// 这个是给github生成的\nssh-keygen -t rsa -C \"1148121254@qq.com\"\n// 这个是给码云生成的\nssh-keygen -t rsa -C \"cumtrobin@163.com\"\n```\n\n生成后自行命名管理,这里不再赘述。接着把公钥分别放在github和码云上。私钥可以用config文件管理\n\n```\n# 配置github.com\nHost github.com \n HostName github.com\n IdentityFile C:\\\\Users\\\\robin\\\\.ssh\\\\id_rsa_github\n PreferredAuthentications publickey\n User cumtRobin\n\n# 配置gitee.com\nHost gitee.com\n HostName gitee.com\n IdentityFile C:\\\\Users\\\\robin\\\\.ssh\\\\id_rsa_gitee\n PreferredAuthentications publickey\n User Tusi\n```\n\n接着我们测试一下\n\n```\nssh -T git@github.com\nssh -T git@gitee.com\n```\n\n成功则会得到这样的反馈\n\n![gitee连接成功](http://qncdn.wbjiang.cn/gitee%E6%B5%8B%E8%AF%95%E8%BF%9E%E6%8E%A5%E6%88%90%E5%8A%9F.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n## 创建仓库\n\n首先是在github和码云上分别创建一个仓库。这个玩过github的都知道,不细说。\n\n接着在本地项目根目录创建git仓库\n\n```\ngit init\n```\n\n## 本地与remote关联\n\n要把两个remote仓库与本地git仓库关联起来,我们直接来运行\n\n```\n// 添加github的远程库\ngit remote add origin git@github.com:cumtRobin/BlogFrontEnd.git\n// 添加码云的远程库\ngit remote add gitee git@gitee.com:tusi/BlogFrontEnd.git\n```\n\n然后我们运行git remote查看添加的远程库列表\n\n```\ngit remote\n// 得到以下值\norigin\ngitee\n```\n\n说明已经添加成功,接着我们分别查看git status,会看到本地有很多文件待提交,接着git add, git commit,最后git push的时候要注意分开push\n\n```\n// push到github主分支\ngit push origin master\n// push到gitee主分支\ngit push gitee master\n```\n\n虽然麻烦了一点,需要push两次,但是目的是初步达成了。如果想要一次性push解决,那也不是没有办法。\n\n# 一次性push\n\n为了避免引起歧义,这里先将origin,gitee的remote库删除\n\n```\ngit remote rm origin\ngit remote rm gitee\n```\n\n重新添加remote\n\n```\ngit remote add all git@github.com:cumtRobin/BlogFrontEnd.git\n```\n\n可以看到,我其实是添加的github的远程库,只不过把它的名字叫做all。接着我们把码云上的remote库也关联起来。\n\n```\ngit remote set-url --add all git@gitee.com:tusi/BlogFrontEnd.git\n```\n\n这样操作以后,就可以运行一条push命令了\n\n```\ngit push all --all\n```\n\n\n\n有人说可以改.git/config文件实现。其实刚才上面的命令修改的就是config文件,但是本人建议,多练练命令行,这样也会加深对git的理解。这时候我们再查看一下.git/config文件。可以看到remote all下面是有两个url的。\n\n```\n[core]\n repositoryformatversion = 0\n filemode = false\n bare = false\n logallrefupdates = true\n symlinks = false\n ignorecase = true\n[remote \"all\"]\n url = git@github.com:cumtRobin/BlogFrontEnd.git\n url = git@gitee.com:tusi/BlogFrontEnd.git\n```\n\n学会了两个托管平台的配置,那使用更多的托管平台也就不难实现了。\n\nps:再分享一个小技巧,由于我在生成ssh密钥时,加了passphrase,导致我每次push都要输入密码,很烦人。\n\n其实,只要重置一下这个passphrase就可以了。\n\n```\n// 进入到.ssh目录,运行git bash\n\nssh-keygen -p\n\n// 再输入密钥名,如id_rsa_github,先输入旧密码,然后一路回车即可,多个密钥重复此操作即可。\n```\n\n![不再需要每次输入密码](http://qncdn.wbjiang.cn/git%E4%B8%8D%E7%94%A8%E8%BE%93%E5%85%A5passphrase.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n---\n2019-04-18\n1. 补充 git pull 的细节\n\n因为都是从本地 push 代码到远程仓库,很久没有从远程仓库拉取代码了,今天不小心在 github 上改了仓库中的 readme 文件,导致和 gitee 不同步。使用 git pull 报错,慌的一批。\n\n```\n$ git pull\nThere is no tracking information for the current branch.\nPlease specify which branch you want to merge with.\nSee git-pull(1) for details.\n\n git pull \n\nIf you wish to set tracking information for this branch you can do so with:\n\n git branch --set-upstream-to=all/ master\n\n```\n\n原来是要使用下面这条命令才行。\n\n```\n$ git pull all master\nFrom github.com:cumtRobin/BlogFrontEnd\n * branch master -> FETCH_HEAD\nAlready up to date.\n\n```\n\n上面的 all 是指 remote ,即远程仓库,master 是指分支名,master 即主干分支。', '2019-02-24 23:29:42', '2024-08-11 15:30:50', 1, 42, 0, '考虑到github不能免费创建私有仓库原因,最近开始在使用码云托管项目,这样避免了连接数据库的用户密码等信息直接暴露在公共仓库中。今天突然想到一个点,就是能不能同时把代码推送到github和码云上呢?答案是可以的。', 'https://qncdn.wbjiang.cn/git%E9%80%9A%E7%94%A8.jpg', 0, 0); -INSERT INTO `article` VALUES (152, '回头再看JS模块化编程', '什么是模块?我们可能要从需求上出发进行理解,当web应用的规模变得越来越大,业务变得越来越复杂时,我们需要将一些函数分门别类,在分类的基础上对函数进行封装,这就形成了模块。下面看一下js模块的一些形式。\n\n# 对象模块\n\n假如我们有多个函数,想作为一个模块使用,最原始的做法就是把这几个函数全部放在一个js文件,通过文件的形式来对js进行划分模块。\n\n```js\n// my_module.js\n\nfunction add(a, b) {\n return a + b;\n}\nfunction multiply(a, b) {\n return a * b;\n}\n```\n\n然而这样的做法会污染全局环境,引用这个js后,window对象就会多了两个方法。那么如何减少这种对全局环境的污染呢?想到的最简单的一个办法就是,把这几个函数都放在一个对象中,只暴露一个对象。\n\n```js\n// my_module.js\n\nvar MyModule = {\n add: function (a, b) {\n return a + b;\n },\n multiply: function (a, b) {\n return a * b;\n }\n}\n```\n\n# IIFE\n\nIIFE(immediately invoked function expression),也就是立即执行函数表达式。假设有这样一个场景,你的模块需要定义默认参数,而你又希望这个默认参数不被外界所改变,那么使用对象模块的方式就没有办法做到了,因为这个对象已经暴露在全局环境中。那么如何能隔离作用域呢?聪明的你已经想到了函数,对的,函数可以做到。我们通过IIFE为window挂载了setColor方法。\n\n```js\n(function(global) {\n var default_option = {\n color: \'blue\'\n }\n global.setColor = function(id, colorValue) {\n document.getElementById(id).style.color = colorValue || default_option.color\n }\n})(this)\n```\n\n这里的default_option就不会暴露在全局环境中,你可以尝试一下在控制台console.log(window.default_option),得到的就是undefined。\n\n# CommonJS\n\n引用百度给出的定义\n\n> CommonJS API定义很多普通应用程序(主要指非浏览器的应用)使用的API,从而填补了这个空白。它的终极目标是提供一个类似Python,Ruby和Java标准库。\n\nCommonJS提供的模块方案认为,一个js就是一个模块,我们经常用到的变量和函数有global,module,exports,require。global是nodejs环境的全局对象,类似与浏览器环境的window,也是根对象,任何在全局环境下定义的变量或函数都是global的属性或方法,global涉及很多东西,这里不再赘述。而module是模块对象,exports包含该模块要导出的变量或函数,require是导入模块的方法。我们首先写两个简单的js来认识它们。\n\n```js\n// a.js\nmodule.exports.add = function (a, b) {\n return a + b;\n}\n\n// b.js\nvar a = require(\'./a.js\');\n\nvar result = a.add(1, 2);\nconsole.log(result); // 输出3\n```\n\n这是最简单的模块写法,a.js通过exports导出add函数,而b.js通过require导入a模块,便可以调用a模块的add函数。\n\n## module\n\n那么我们先来看看module这个对象。在b.js中我们console.log(module),则会打印出模块b的信息。\n\n![moduleB](http://qncdn.wbjiang.cn/moduleB.png)\n\n可以看到,模块b的children里有模块a,说明模块b引用了模块a。我们再观察一下模块a,修改a.js的代码如下,再运行b.js\n\n```js\nconsole.log(module)\n\nmodule.exports.add = function (a, b) {\n return a + b;\n}\n```\n\n![moduleA](http://qncdn.wbjiang.cn/moduleA.png)\n\n可以看到,模块a的parent指向模块b,是因为执行的是b.js,而b.js引用了模块a。注意此时模块a的loaded属性值仍是false,因为此时模块还没加载完成。如果我们在add方法中打日志,而b.js调用a.js的add函数,则会发现此时模块a的loaded已经变成了true\n\n从上面可以了解到,module对象下面有以下属性\n\n- id:模块id,一般默认是模块的路径\n- exports:模块对外导出对象,包含了对外导出的函数和属性\n- parent:指向首次加载本模块的模块(为什么说是首次呢?假设b.js引用了模块a,c.js引用了模块a和模块b,此时运行c.js,模块a的parent指向的是模块b)\n- filename:模块的绝对路径\n- loaded:模块是否已经加载完成\n- children:当前模块引用的其他模块\n- paths:对于加载模块时没给出./ ../ /.../时,加载模块的搜索路径。依次从第一个路径搜索到最后一个路径。\n\n## exports\n\n接下来我们说说exports,在这里要了解module.exports与exports的区别。Node.js 在初始化时执行了 exports = module.exports , 所以 exports 与 module.exports 指向了相同的内存。当不改变两者的指向时,两者还是全等的。因此,我前面的写法 exports.add 只是给 exports 指向的对象上添加了add方法,并未改变其指向。这之后exports与module.exports仍是一致的。到这里大家应该明白了什么情况两者会不相等了。\n\n```js\n// 如果采用这种改变指向的写法,那么之后exports与module.exports就不一样了。\nmodule.exports = {\n add: function (a, b) {\n return a + b\n }\n}\n```\n\n通过exports导出的函数和属性可以被其他模块调用,这一点想必大家都清楚了。\n\n# require\n\n这里先说一下模块的分类,NodeJS中模块分为核心模块和文件模块。核心模块是被编译成二进制代码,引用的时候只需require表示符即可,如require(\'fs\'),不需要加路径的。而引用文件模块时需要加上路径,表示对文件的引用。假如你加载一个自定义的test.js模块时,没有指定路径,那么它会首先从当前目录的node_modules子目录下寻找test.js,如果没有,则查找上一级目录的node_modules子目录,一直查到盘符的根目录为止。也就是前面提到的module.paths的查找顺序。\n\n说到这里,我们再来回顾一下我们在开发时,npm install 安装的一些依赖包。它们的package.json一般都包含了main字段,用来标识入口js文件。\n\n![package.json的main字段](http://qncdn.wbjiang.cn/npm%E5%8C%85%E7%9A%84%E5%85%A5%E5%8F%A3%E8%AF%B4%E6%98%8E.png)\n\n如果没有指定main字段,那么nodejs会默认去加载index.js或者index.node文件。例如:\n\n![index.js作为入口js文件](http://qncdn.wbjiang.cn/npm%E5%8C%85index%E5%85%A5%E5%8F%A3.png)\n\n看到这里,是不是突然有点懂了node_modules哪些依赖包的写法了。好的,接着往下看。\n\n我们在c.js中打出日志,观察require方法的结构。\n\n```js\nconsole.log(require)\n```\n\n![require方法的结构](http://qncdn.wbjiang.cn/require%E6%96%B9%E6%B3%95%E7%BB%93%E6%9E%84.png)\n\n可以知道,require函数包含了以下属性和方法。\n\n- require.resolve():将模块名解析,得到该模块的绝对路径\n- require.main:指向当前执行的主模块\n- require.cache:指向所有缓存的模块\n- require.extensions:根据文件的后缀名,调用不同的执行函数\n\n我们再细致看一下,主要看看resolve和extensions\n\n```js\nvar a = require(\'./a.js\');\nvar b = require(\'./b.js\');\n\nconsole.log(\'resolve测试\')\nconsole.log(require.resolve(\'./a.js\'))\n\nconsole.log(\'extensions测试\')\nconsole.log(require.extensions[\'.js\'].toString())\n\nconsole.log(require.extensions[\'.json\'].toString())\n\nconsole.log(require.extensions[\'.node\'].toString())\n```\n\n得到的结果如下图所示:\n\n![module的resolve和extensions](http://qncdn.wbjiang.cn/module%E7%9A%84extensions.png)\n\n写到这里,算是对模块有一点初步的认识。接下来我们还需要了解AMD,CMD,UMD的概念。由于篇幅太长,接下来我将分篇叙述这些概念,请阅读后续系列文章!以上观点源于自己的一些理解,如有描述不对的地方,请您指正!', '2019-03-01 11:50:50', '2024-11-12 09:19:51', 1, 95, 0, '什么是模块?我们可能要从需求上出发进行理解,当web应用的规模变得越来越大,业务变得越来越复杂时,我们需要将一些函数分门别类,在分类的基础上对函数进行封装,这就形成了模块。', 'https://qncdn.wbjiang.cn/js_code.png', 0, 0); -INSERT INTO `article` VALUES (153, '回头再看JS模块化编程之AMD', '由于CommonJS采用适合服务器端的同步加载方式,这种方式不适合天生异步的浏览器端。在这种形势下,AMD(Asynchronous Module Definition,异步模块定义)应运而生。而require.js正是AMD规范下的产物,因此,我们可以直观地从require.js入手分析AMD。\n\n# require.js\n\n这是[RequireJS官方下载链接](https://requirejs.org/docs/download.html),我本次测试使用的是2.3.5版本。\n\n## 加载require.js\n\n使用RequireJS后,我们不用在html中手动添加蛮蛮多的script标签了,通过模块依赖的方式,RequireJS会自动创建script标签,也使得模块间依赖关系的管理变得更加方便。首先,需要在html中引入require.js,并通过data-main属性指定入口js文件\n\n```html\n\n```\n\n## 定义模块\n\n我们先不关注main.js的实现,先来看看在RequireJS中怎么定义模块。\n\n```js\n// name和deps都是非必选的参数,而callback可以是一个对象,或者是具有返回值的函数\ndefine([name], [deps], callback)\n```\n\n### 简单模块\n\n如果一个模块只包含一些键值对,没有任何依赖,则在define()中定义这些键值对就好了\n\n```js\n// 定义模块时,推荐不显示传入name参数,这样方便优化工具去生成。\ndefine({\n name: \'simpleModule\',\n version: \'1.0.0\',\n add: function(a, b) {\n return a + b;\n }\n})\n```\n\n### 函数式模块\n\n跟上篇文章说到的IIFE是一样的道理,加入我们需要对模块做一些初始化的工作,那么就不能使用简单模块的定义方式了。函数式模块的定义方式如下:\n\n```js\ndefine(function() {\n // ...\n // 这之前可以做一些初始化的变量赋值等等...\n function add(a, b) {\n return a + b\n }\n // 最终return一个对象,暴露给调用者使用\n return {\n add: add\n }\n})\n```\n\n### 存在依赖的模块\n\n假设你要写一个依赖jquery的模块,那么你需要在define方法中声明依赖。\n\n```js\ndefine([\'jquery\'], function($) {\n function setColor(select, color) {\n $(select).css(\'color\', color)\n }\n return {\n setColor: setColor\n }\n})\n```\n\n在官网上还发现一种类似sea.js的依赖写法。\n\n```js\ndefine(function(require, exports, module) {\n var $ = require(\'jquery\')\n function setColor(select, color) {\n $(select).css(\'color\', color)\n }\n return {\n setColor: setColor\n }\n})\n```\n\n这种写法的代码在运行时,首先是不知道依赖的,需要遍历所有的require关键字,找出后面的依赖。这显然是一种更牺牲性能的方法。虽然可以用var $ = require(\'jquery\')这种“同步加载”的形式写代码,但终究不是一个最优的选择。\n\n## 使用模块\n\n在使用模块之前,我们可以通过require.config先配置每个js的路径,方便后续代码的书写。\n\n```js\n// main.js的顶部,我定义了四个模块的path\nrequire.config({\n paths: {\n simple: \'./simple\',\n jquery: \'../jquery-3.3.1\',\n funcModule: \'./func-module\',\n depModule: \'./dep-module\',\n }\n});\n```\n\nRequireJS调用模块的方式如下\n\n```js\n// callback参数列表的顺序与deps中模块的顺序一致\nrequire(deps, callback)\n```\n\n```js\nrequire([\'simple\', \'jquery\', \'funcModule\', \'depModule\'], function(simple, $, funcModule, depModule) {\n console.log(simple)\n console.log($)\n $(\'.word\').css({\n fontSize: \'24px\',\n color: \'blue\'\n })\n var result = funcModule.add(1,2)\n console.log(result)\n\n depModule.setColor(\'.word\', \'yellow\')\n})\n```\n\n到这里我们已经掌握了RequireJS的最基本的用法了。\n\n## 配置项\n\n除了paths外,RequireJS还支持很多的配置项,便于我们快速开发。完整配置可以参考[RequireJS 中文网](http://www.requirejs.cn/)\n\n比较常用的有baseUrl,指定了js文件的查找基路径;还有shim,用来作为垫片支持那些不符合AMD规范的js。\n\n### baseUrl\n\n经过本人测试,baseUrl的路径参考了引用require.js的入口html文件。我们看一下两种不同的文件路径配置就明白了。\n\n首先看第一种\n![配置1](http://qncdn.wbjiang.cn/requirejs_baseurl%E9%85%8D%E7%BD%AE1.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n接着我们看一下第二种,我把requirejs.html移动到了一个html文件夹内。\n![配置2](http://qncdn.wbjiang.cn/requirejs_baseurl%E9%85%8D%E7%BD%AE2.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n这两种不同的文件路径下,baseUrl都必须参考requirejs.html的路径,否则就会发生引用404报错了。\n\n### shim\n\n有些早期的js库并不支持AMD写法,所以需要在requirejs中配置shim才可以使用它们,shim写法如下:\n\n```\nrequire.config({\n shim: {\n \"underscore\" : {\n exports : \"_\";\n },\n \"jquery\" : {\n exports : \"$\";\n }\n }\n})\n```', '2019-03-01 11:51:39', '2024-11-11 15:28:59', 1, 45, 0, '由于CommonJS采用适合服务器端的同步加载方式,这种方式不适合天生异步的浏览器端。在这种形势下,AMD(Asynchronous Module Definition,异步模块定义)应运而生。而require.js正是AMD规范下的产物,因此,我们可以直观地从require.js入手分析AMD。', 'https://qncdn.wbjiang.cn/js_400x300.png', 0, 0); -INSERT INTO `article` VALUES (154, 'windows系统下安装mysql8.0', '# 下载mysql\n\n进入到[官网下载页面](https://www.mysql.com/downloads/),下载免费的社区版就可以了。本人一开始下载了installer,安装卸载了两次,可能是使用不当吧,遇到了很多坑,最终选择了下载zip版本。\n![mysql下载推荐](http://qncdn.wbjiang.cn/mysql%E4%B8%8B%E8%BD%BD%E6%8E%A8%E8%8D%90.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10))\n\n# 如何安装使用\n\n将下载的zip压缩包解压到D:\\Program Files\\MySQL目录下,创建my.ini配置文件\n\n```\n[mysqld]\n# 设置3306端口\nport=3306\n# 设置mysql的安装目录\nbasedir=D:\\Program Files\\MySQL\n# 设置mysql数据库的数据的存放目录\ndatadir=D:\\Program Files\\MySQL\\data\n# 允许最大连接数\nmax_connections=200\n# 允许连接失败的次数。这是为了防止有人从该主机试图攻击数据库系统\nmax_connect_errors=10\n# 服务端使用的字符集默认为UTF8\ncharacter-set-server=utf8\n# 创建新表时将使用的默认存储引擎\ndefault-storage-engine=INNODB\n# 默认使用“mysql_native_password”插件认证\ndefault_authentication_plugin=mysql_native_password\n[mysql]\n# 设置mysql客户端默认字符集\ndefault-character-set=utf8\n[client]\n# 设置mysql客户端连接服务端时默认使用的端口\nport=3306\ndefault-character-set=utf8\n```\n\n然后在bin目录下依次运行以下命令\n\n```\nmysqld --install\nmysqld --initialize\n```\n会得到一个初始的密码,如kh5sN)zd=fsw。\n\n还要配置一下环境变量,在PATH后追加以下内容\n\n```\n;D:\\Program Files\\MySQL\\bin\n```\n\n接着运行net start mysql,可以看到启动mysql服务已经成功。\n\n![mysql初始化](http://qncdn.wbjiang.cn/%E5%88%9D%E5%A7%8B%E5%8C%96mysql.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n# 测试root账号\n\n运行登录命令\n\n```\nmysql -u root -p\n```\n\n输入密码后登录成功。\n![mysql登录成功](http://qncdn.wbjiang.cn/mysql%E7%94%A8root%E8%B4%A6%E5%8F%B7%E6%88%90%E5%8A%9F%E7%99%BB%E5%BD%95.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n接着输入数据库查询语句,随便输什么都报一个错。比如show databases,use mysql等。\n\n> You must reset your password using ALTER USER statement before executing this statement.\n\n上网查了一下后,说是密码过期了,需要修改,我就纳闷了,刚初始化得到的密码就过期了。。。\n\n不说了,运行这个命令,修改一下root的密码。\n\n```\nalter user user() identified by \"123456\";\n```\n\n修改后,就可以正常访问使用mysql数据库了。\n![修改密码后正常使用](http://qncdn.wbjiang.cn/mysql%E6%8F%90%E7%A4%BA%E5%AF%86%E7%A0%81%E8%BF%87%E6%9C%9F.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n---\n\n**2018.8.7**\n\n今天在win10上按照以上步骤安装时,遇到了一个问题。运行net start mysql时报错\n\n```\n发生系统错误 2。\n\n系统找不到指定的文件。\n```\n\n解决方案:\n打开注册表,找到\n\nHKEY_LOCAL_MACHINE -> SYSTEM -> CurrentControlSet -? services -> mysql -> ImagePath\n\n发现这个值是C盘下的某路径,跟我放在D盘的MySQL目录不符。\n\n需要修改成\n\n```\n\"D:\\Program Files\\MySQL\\bin\\mysqld\" MySQL\n```\n\n', '2019-03-01 11:53:52', '2024-09-03 00:49:50', 1, 24, 0, '本文简单聊聊如何在windows系统下安装mysql8.0', 'https://qncdn.wbjiang.cn/mysql%E9%80%9A%E7%94%A8.jpeg', 0, 0); -INSERT INTO `article` VALUES (155, 'WebSocket实现实时通讯(socket.io实现简版聊天室)', '最近一段时间了接触了一些websocket编程。这里记录一个简单的入门demo。该demo利用socket.io实现了一个简单的聊天应用。各位看官老爷,且接着看具体的实现方式。\n\n# 准备工作\n\n本人主要是前端开发,会一点点nodejs。因此这个demo是基于socket.io.js实现的。\n\n## B/S服务端\n\n首先,我们在服务端安装socket.io\n\n```\nnpm install --save socket.io\n```\n\n## B/S客户端\n\n接着,在vue项目中安装socket.io-client\n\n```\nnpm install --save socket.io-client\n```\n\n# 思路\n\n聊天室最基本的功能应该有系统通知,聊天内容等。系统通知针对所有socket连接,即是全局广播;聊天内容则是除当前socket用户的非全局广播。还可能存在系统与某个用户的单独消息互动,这则是单播。本文先不涉及room的概念,高手莫怪。\n\n# 实现方式\n\n## socket服务端\n\n主要代码如下:\n```\nvar http = require(\'http\');\nvar express = require(\'express\');\n\nvar app = express();\nvar server = http.createServer(app)\nvar io = require(\'socket.io\')(server);\n\nserver.listen(app.get(\'port\'), function() {\n console.log(\'Express server listening on port \' + app.get(\'port\'));\n});\n// 监听socket连接\nio.on(\'connection\', function (socket) {\n // 当某用户连上聊天室socket服务时,给他打个招呼\n sendToSingle(socket, {\n event: \'greet_from_server\',\n data: `你好${socket.id}`\n })\n // 对其他用户给出通知:某某某加入了聊天室\n broadcastExceptSelf(socket, {\n event: \'new_user_join\',\n data: {\n user: socket.id\n }\n })\n // 监听用户发的聊天内容\n socket.on(\'chat\', function (data) {\n // 然后广播给其他用户:某某某说了什么\n broadcastExceptSelf(socket, {\n event: \'new_chat_content\',\n data: {\n user: socket.id,\n content: data\n }\n })\n });\n // 监听socket连接断开\n socket.on(\'disconnect\', (reason) => {\n // 广播给其他用户:某某某退出了聊天室\n broadcastExceptSelf(socket, {\n event: \'someone_exit\',\n data: {\n user: socket.id\n }\n })\n });\n});\n// 给当前socket连接单独发消息\nfunction sendToSingle(socket, param) {\n socket.emit(\'singleMsg\', param);\n}\n// 对所有socket连接发消息\nfunction broadcastAll(param) {\n io.emit(\'broadcastAll\', param)\n}\n// 对除当前socket连接的其他所有socket连接发消息\nfunction broadcastExceptSelf(socket, param) {\n socket.broadcast.emit(\'broadcast\', param);\n}\n```\n\n## socket客户端\n\n实现方式也比较简单\n\n```\nimport io from \'socket.io-client\';\n// 创建和管理socket\ncreateSocket() {\n let self = this\n this.socket = io(this.$store.state.config.API_ROOT);\n this.socket.on(\'connect\', function(){\n console.log(\'连上了\')\n });\n // 这里接收服务端发来的单独消息\n this.socket.on(\'singleMsg\', function(msg){\n console.log(msg)\n switch (msg.event) {\n // 如来自服务端的问候,虽然这里没写什么,但是可以据此做一些页面上的效果\n case \'greet_from_server\':\n break\n default:\n break\n }\n })\n // 目前还没用到,可拓展\n this.socket.on(\'broadcastAll\', function(msg){\n console.log(msg)\n })\n // 监听广播\n this.socket.on(\'broadcast\', function(msg){\n console.log(msg)\n switch (msg.event) {\n // 新用户加入聊天室\n case \'new_user_join\':\n self.msgList.push({\n time: new Date().toLocaleString(),\n user: \'系统通知\',\n content: `用户 ${msg.data.user} 加入了聊天室......`\n })\n break\n // 用户退出聊天室\n case \'someone_exit\':\n self.msgList.push({\n time: new Date().toLocaleString(),\n user: \'系统通知\',\n content: `用户 ${msg.data.user} 退出了聊天室......`\n })\n break\n // 接收某用户的聊天内容\n case \'new_chat_content\':\n self.msgList.push({\n time: new Date().toLocaleString(),\n user: msg.data.user,\n content: msg.data.content\n })\n break\n default:\n break\n }\n })\n this.socket.on(\'disconnect\', function(){\n console.log(\'连接断开了\')\n });\n},\n// 监听输入框回车事件\nonInpuKeyDown(e) {\n console.log(e)\n if (e.keyCode == 13) {\n // 将输入的聊天内容推送给服务端\n this.socket.emit(\'chat\', e.target.value)\n this.msgList.push({\n time: new Date().toLocaleString(),\n user: \'我说\',\n content: e.target.value\n })\n this.newMsg = \'\'\n }\n}\n```\n\n我们用打开多个窗口的方式模拟多个用户的加入,我这里开了三个窗口,最后的效果大概是这样的。\n\n用户1看到用户2和用户3谈到了很晚的事情\n![聊天内容1](http://qncdn.wbjiang.cn/%E8%81%8A%E5%A4%A91.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n切到了当前用户2的界面\n![聊天内容2](http://qncdn.wbjiang.cn/%E8%81%8A%E5%A4%A92.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n切到了当前用户3的界面\n![聊天内容3](http://qncdn.wbjiang.cn/%E8%81%8A%E5%A4%A93.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n用户3退出了聊天室,在用户2的界面上可以看到推送的系统通知,然后用户2说了一句话也退出了\n![聊天内容4](http://qncdn.wbjiang.cn/%E8%81%8A%E5%A4%A94.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n用户1看到用户2和用户3的退出情况\n![聊天内容5](http://qncdn.wbjiang.cn/%E8%81%8A%E5%A4%A95.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)', '2019-03-01 11:58:36', '2024-07-25 14:24:36', 1, 66, 0, '最近一段时间了接触了一些websocket编程。这里记录一个简单的入门demo。该demo利用socket.io实现了一个简单的聊天应用。各位看官老爷,且接着看具体的实现方式。', 'https://qncdn.wbjiang.cn/websocket%E9%80%9A%E7%94%A8.jpg', 0, 0); -INSERT INTO `article` VALUES (156, 'Nodejs开发微信公众号--测试号配置篇', '微信公众号感觉入门是真的难啊,受权限的约束,个人开发者真的挺难走的,特别像博主这种主攻前端的人。由于迟迟没把域名备案办下来。先从测试号入手。\n\n# 申请页面信息\n\n打开[测试号申请页面](https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&t=sandbox/index),可以看到需要填写的内容。\n\n![测试号申请页面](http://qncdn.wbjiang.cn/%E6%B5%8B%E8%AF%95%E5%8F%B7%E7%94%B3%E8%AF%B7%E9%A1%B5%E9%9D%A2.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n首先要做的就是接口配置信息这部分内容了。\n\n# 内网穿透\n\n一般我们会在本地进行开发,因此必须使用工具进行内网穿透,将一个公网域名穿透到本地内网。用[NATAPP](https://natapp.cn/)可以做到。使用方法很简单,这里不再赘述。\n\n# NodeJS服务器搭建\n\n我这边使用express生成器搭建服务。\n\n```\n// 全局安装生成器\nnpm install express-generator -g\n// 生成项目,并指定模板引擎\nexpress --view=ejs wechat_express\n\ncd wechat_express\n// 安装node依赖\nnpm install\n// 启动\nset DEBUG=myapp:* & npm start\n```\n新建一个配置文件config/index.js\n\n```\nconst CONFIG = {\n port: \'4300\',\n token: \'你的token\'\n}\nexports.CONFIG = CONFIG;\n```\n\n在app.js中指定端口\n\n```\napp.set(\'port\', config.port);\n```\n\n写路由,进行微信公众号的token验证\n\n```\nvar express = require(\'express\');\nvar crypto = require(\'crypto\');\nvar config = require(\'../config/index\').CONFIG;\nvar router = express.Router();\n\nrouter.get(\'/\',function(req,res){\n console.log(req)\n //1.获取微信服务器Get请求的参数 signature、timestamp、nonce、echostr\n var signature = req.query.signature,//微信加密签名\n timestamp = req.query.timestamp,//时间戳\n nonce = req.query.nonce,//随机数\n echostr = req.query.echostr;//随机字符串\n\n //2.将token、timestamp、nonce三个参数进行字典序排序\n var array = [config.token,timestamp,nonce];\n array.sort();\n\n //3.将三个参数字符串拼接成一个字符串进行sha1加密\n var tempStr = array.join(\'\');\n const hashCode = crypto.createHash(\'sha1\'); //创建加密类型 \n var resultCode = hashCode.update(tempStr,\'utf8\').digest(\'hex\'); //对传入的字符串进行加密\n\n //4.开发者获得加密后的字符串可与signature对比,标识该请求来源于微信\n if(resultCode === signature){\n res.send(echostr);\n }else{\n res.send(\'mismatch\');\n }\n});\n\nmodule.exports = router;\n\n```\n\n启动服务,并在测试号申请页面,填入URL和Token进行验证。验证成功后便可以继续下一步了。\n\n# JS接口安全域名\n\n暂时还未用到,填写URL所在域名即可。', '2019-03-01 12:00:47', '2024-08-16 04:29:46', 1, 28, 0, '微信公众号感觉入门是真的难啊,受权限的约束,个人开发者真的挺难走的,特别像博主这种主攻前端的人。由于迟迟没把域名备案办下来。先从测试号入手。', 'https://qncdn.wbjiang.cn/%E5%BE%AE%E4%BF%A1%E5%85%AC%E4%BC%97%E5%B9%B3%E5%8F%B0.png', 0, 0); -INSERT INTO `article` VALUES (157, 'Nodejs开发微信公众号--获取access_token', '为了梳理代码,我单独给微信的接口进行了一些封装。这是前面认证接口的内容。\n\n封装接口用到了request\n\n```\nnpm install --save request\n```\n\n封装的 js 结构大致是这样的\n\n```\nvar request = require(\'request\');\nvar crypto = require(\'crypto\');\n\nfunction WeChat(config) {\n this.config = config\n this.accessToken = null\n this.getAccessTokenTimer = null\n}\n\nWeChat.prototype.Authenticate = function(req, res) {\n //1.获取微信服务器Get请求的参数 signature、timestamp、nonce、echostr\n var signature = req.query.signature,//微信加密签名\n timestamp = req.query.timestamp,//时间戳\n nonce = req.query.nonce,//随机数\n echostr = req.query.echostr;//随机字符串\n\n //2.将token、timestamp、nonce三个参数进行字典序排序\n var array = [this.config.token,timestamp,nonce];\n array.sort();\n\n //3.将三个参数字符串拼接成一个字符串进行sha1加密\n var tempStr = array.join(\'\');\n const hashCode = crypto.createHash(\'sha1\'); //创建加密类型 \n var resultCode = hashCode.update(tempStr,\'utf8\').digest(\'hex\'); //对传入的字符串进行加密\n\n //4.开发者获得加密后的字符串可与signature对比,标识该请求来源于微信\n if(resultCode === signature){\n res.send(echostr);\n }else{\n res.send(\'mismatch\');\n }\n}\n\nmodule.exports.WeChat = WeChat;\n```\n\n扯多了,回到正题access_token\n\n> access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token。开发者需要进行妥善保存。access_token的存储至少要保留512个字符空间。access_token的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效。\n\n因此,我继续封装了GetAccessToken方法。\n\n```\nWeChat.prototype.GetAccessToken = function() {\n var self = this\n let option = {\n url: \'https://api.weixin.qq.com/cgi-bin/token\',\n qs: {\n grant_type: \'client_credential\',\n appid: this.config.App_Id,\n secret: this.config.App_Secret\n },\n method: \'GET\',\n headers: {\n \"content-type\": \"application/json\"\n }\n }\n return new Promise((resolve, reject) => {\n request(option, function(error, response, body) {\n console.log(error, body)\n var data = JSON.parse(body)\n if (error) {\n reject(error)\n } else {\n switch(data.errcode) {\n case 45009:\n console.log(\'token调用上限\')\n reject(data)\n break\n case 0:\n self.accessToken = {\n access_token: data.access_token,\n expires_in: data.expires_in\n }\n console.log(\'当前access_token\', JSON.stringify(self.accessToken))\n // 定时重新获取access_token\n clearTimeout(this.getAccessTokenTimer)\n this.getAccessTokenTimer = setTimeout(() => {\n self.GetAccessToken()\n }, (data.expires_in - 60) * 1000 || 60000)\n resolve(data)\n break\n }\n }\n })\n })\n}\n```\n\n并在express服务启动的时候调用GetAccessToken,调用成功后会依据 expires_in 起定时器重新获取。\n\n```\nvar wechat = new WeChat(config)\nwechat.GetAccessToken().then(success => {\n console.log(\'初始化获取accessToken成功\')\n}, failure => {\n console.log(\'初始化获取accessToken失败\')\n})\n```\n\n---\n2018.10.17\n考虑到每次重启服务器都会调用GetAccessToken,会导致频繁调用。因此想到一个修改方法,将accessToken作为属性存在wechat对象中的同时,还将其写入到本地文件token.json中。这样服务器重启时,就可以先读取token.json文件中的access_token及expires_in,先判断是否过期,如果过期了,则直接进行access_token更新操作,否则计算出过期时间,用定时器控制在过期时间时进行access_token更新操作。\n\n', '2019-03-01 12:01:34', '2024-08-16 04:50:42', 1, 30, 0, 'Nodejs开发微信公众号很重要的一部分就是处理access_token,来看下我是怎么处理的吧', 'https://qncdn.wbjiang.cn/%E5%A4%A7%E5%89%8D%E7%AB%AF%E6%8A%80%E6%9C%AF%E6%B2%99%E9%BE%99.jpg', 0, 0); -INSERT INTO `article` VALUES (158, '微信小程序的摸索之路--从demo入手揭开神秘面纱', '微信小程序推出已久,除了普通开发版本,如今已经支持云开发版本。框架上的选择也有很多,比较火的应该属 mpvue 和 wepy 和 taro 吧。但是我还是选择先从普通开发版本和原生语言开始入手微信小程序,然后再考虑框架的事情。\n\n# 项目结构\n\n![小程序项目结构](http://qncdn.wbjiang.cn/%E5%B0%8F%E7%A8%8B%E5%BA%8Fdemo%E9%A1%B9%E7%9B%AE%E7%BB%93%E6%9E%841.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n刚接触小程序的我,一看到也是有点懵逼的。但是细心看下来,发现和其他前端框架组织的项目也是大同小异的。我们且不关注项目配置文件 project.config.json 和辅助js模块 util.js,小程序基本上由App和Page两部分组成,我们暂且称这两者都为组件吧。小程序的组件基本上由四个文件组成。 wxml 对应 html,负责模板视图;wxss 对应 css,负责样式表现;js就不用说了,负责逻辑操作;json则是负责组件相关的配置。\n\n# Demo分析\n\n小程序 demo 主要包含两个页面,首页有请求用户授权的按钮,授权后点击用户头像进入日志页面,查看登录日志。\n\n![首页](http://qncdn.wbjiang.cn/%E5%B0%8F%E7%A8%8B%E5%BA%8F%E9%A6%96%E9%A1%B5.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n![请求授权](http://qncdn.wbjiang.cn/%E5%B0%8F%E7%A8%8B%E5%BA%8F%E8%AF%B7%E6%B1%82%E7%94%A8%E6%88%B7%E6%8E%88%E6%9D%83.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n![日志页面](http://qncdn.wbjiang.cn/%E5%B0%8F%E7%A8%8B%E5%BA%8F%E6%97%A5%E5%BF%97%E9%A1%B5%E9%9D%A2.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n## 获取用户信息\n\n该 demo 获取用户信息的思路是:\n\n首先需要检查用户是否已经对小程序进行了个人信息授权,需要调用\n\n```\nwx.getSetting({\n success: res => {\n if (res.authSetting[\'scope.userInfo\']) {\n // 已经授权,可以直接调用 getUserInfo 获取头像昵称,不会弹框\n ......\n }\n }\n})\n```\n\n- **如果用户第一次进入或从未授权个人信息,则不做任何默认操作,此时需要用户手动点击按钮进行授权;**\n\n根据小程序官方解释:**注意:wx.authorize({scope: \"scope.userInfo\"}),无法弹出授权窗口,请使用** \n\n```\n// wxml\n\n\n// js\ngetUserInfo: function(e) {\n console.log(e)\n app.globalData.userInfo = e.detail.userInfo\n this.setData({\n userInfo: e.detail.userInfo,\n hasUserInfo: true\n })\n }\n```\n\n用户点击该按钮时,会返回获取到的用户信息,回调的detail数据与wx.getUserInfo返回的一致。\n\n- **如果已经授权过,则在 App 的 onLaunch 钩子函数中调用 getUserInfo 去获取用户信息,并在 index 页面进行显示。**\n\n这里存在一个潜在的 bug ,App 的 onLaunch 执行后,Index 页面的 onLoad 方法也会随之执行,如果此时 wx.getUserInfo 接口尚未响应完成,则 Index 不能显示出用户信息。解决的方法是在 Index 页面获取 app 实例,并在 app 实例上挂载一个回调函数,然后在 wx.getUserInfo 接口得到响应后,执行该回调函数。\n\n```\n// index.js\n// 获取应用实例\nconst app = getApp()\n\napp.userInfoReadyCallback = res => {\n this.setData({\n userInfo: res.userInfo,\n hasUserInfo: true\n })\n}\n\n\n// app.js\nwx.getSetting({\n success: res => {\n if (res.authSetting[\'scope.userInfo\']) {\n wx.getUserInfo({\n success: res => {\n this.globalData.userInfo = res.userInfo\n // 如果有 index 页面指定的回调函数,则执行\n if (this.userInfoReadyCallback) {\n this.userInfoReadyCallback(res)\n }\n }\n })\n }\n }\n})\n```\n\n## 存储数据和路由\n\n### 本地缓存\n\n该 demo 中存储日志用到了 setStorageSync ,这是一个同步存储本地缓存的方法。与之对应的同步获取本地缓存的方法是 getStorageSync 。说到同步,就不得不提到异步。本地缓存存取的异步方法分别是 getStorage 和 setStorage。小程序的本地缓存与 WebStorage 有异曲同工之妙。\n\n```\nvar logs = wx.getStorageSync(\'logs\') || []\nlogs.unshift(Date.now())\nwx.setStorageSync(\'logs\', logs)\n```\n\n### 路由\n\n小程序提供的路由方法主要有以下几个:\n\n- wx.redirectTo(Object object):关闭当前页面,跳转到应用内的某个页面,但是不允许跳转到 tabbar 页面。传入的 object 包含 url (跳转的页面的路径),success (成功回调函数),fail (失败回调函数),complete (接口调用结束的回调函数,无论成功或失败) 等几个属性及方法。相当于没有当前页的历史记录。\n- wx.navigateTo(Object object):保留当前页面,跳转到应用内的某个页面,但是不能跳到 tabbar 页面。使用 wx.navigateBack 可以返回到原页面。相当于保留了当前页的历史记录。\n- wx.navigateBack(Object object):关闭当前页面,返回上一页面或多级页面。可通过 getCurrentPages() 获取当前的页面栈,决定需要返回几层。传入的 object 不再包含 url,而是 delta,表示后退 delta 页。\n- wx.reLaunch(Object object):关闭所有页面,打开到应用内的某个页面。相当于销毁所有路由历史记录再打开新页面。\n- wx.switchTab(Object object):跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面。', '2019-03-01 16:49:44', '2024-07-24 21:21:16', 1, 43, 0, '微信小程序推出已久,除了普通开发版本,如今已经支持云开发版本。框架上的选择也有很多,比较火的应该属 mpvue 和 wepy 和 taro 吧。但是我还是选择先从普通开发版本和原生语言开始入手微信小程序,然后再考虑框架的事情。', 'https://qncdn.wbjiang.cn/Tusi%E5%8D%9A%E5%AE%A2.jpg', 0, 0); -INSERT INTO `article` VALUES (159, 'vue作用域插槽,你真的懂了吗?', '# 前言\n\n在网上搜了很多关于作用域插槽的解释,感觉没有写得很具体的吧,我认为应该对组件化有很深的理解才会触及到这个问题吧,这里也分享下我自己对于slot-scope的一点理解。\n\n- slot大家看看文档都懂了,无非就是在子组件中挖个坑,坑里面放什么东西由父组件决定。\n\n```\n// 子组件\n\n\n// 父组件\n\n```\n\n 1. 给slot传入普通文本\n\n![slot传入普通文本](http://qncdn.wbjiang.cn/slot%E6%99%AE%E9%80%9A%E6%96%87%E6%9C%AC.png?imageMogr2/auto-orient/blur/1x0/quality/75%7Cwatermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n2. 给slot传入了一个图像处理组件\n\n![slot传入普通文本](http://qncdn.wbjiang.cn/slot%E4%BC%A0%E5%85%A5%E7%BB%84%E4%BB%B6.png?imageMogr2/auto-orient/blur/1x0/quality/75%7Cwatermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n- 具名插槽也很简单,比如有多个插槽,我作为父组件,肯定想区别子组件中的几个插槽,那就要用slot标签的name属性来标识了,而父组件要决定在什么插槽里面放什么内容,就要将name的值赋值给slot属性传递给对应的插槽。如果slot没有name属性,就是匿名插槽了,而父组件中不指定slot属性的内容,就会被丢到匿名插槽中。\n\n```\n// 子组件\n\n\n// 父组件\n\n```\n\n- 最难理解的是作用域插槽。看了文档说明的朋友可能还会有点晕,大概是说在作用域插槽内,父组件可以拿到子组件的数据。子组件可以在slot标签上绑定属性值,如:\n\n```\n\n```\n\n而父组件通过slot-scope绑定的对象下拿到nickName的值。 \n\n```\n\n```\n\n这里大家应该都有疑问。这有什么用?我在子组件用$emit向父组件传递数据不就行了?\n\n# 关于作用域插槽的一点理解\n我觉得要从组件之间的数据流向来思考作用域插槽的应用场景。\n\n> 假设第一个场景,需要你写一个商品卡片组件,并通过循环去展示多个卡片,并且要求能响应每个卡片上的图片或者其他内容的点击事件而跳转到商品详情页,你会怎么写?\n\n![淘宝商品列表](http://qncdn.wbjiang.cn/%E6%B7%98%E5%AE%9D%E5%95%86%E5%93%81%E5%88%97%E8%A1%A8.png?imageMogr2/auto-orient/blur/1x0/quality/75%7Cwatermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n我会使用如下的处理方式,首先将商品卡片写成一个组件Commodity.vue,而在CommodityList.vue中用一个v-for来处理商品卡片列表的展示。\n\n```\n\n```\n\nCommodity组件通过$emit像父组件传递clickCommodity事件,并携带商品数据,父组件即可在onCommodityClick方法中得到数据,进行业务处理,这样便完成了一个基本的由子到父的数据传递。\n\n> 如果再往上抽象一下呢?比如我有多个运营栏目,像淘宝首页有“有好货”,“爱逛街”这样两个栏目,每个栏目下都需要有一个商品卡片列表,那么商品卡片列表CommodityList.vue就要抽成组件了。而这个包含多个运营栏目的vue组件我假设它叫ColumnList.vue,在其中通过v-for调用了CommodityList组件。\n\n![淘宝运营栏目列表](http://qncdn.wbjiang.cn/%E6%B7%98%E5%AE%9D%E5%95%86%E5%93%81%E5%88%97%E8%A1%A8%E5%8A%A0%E8%BF%90%E8%90%A5%E6%A0%8F%E7%9B%AE.png?imageMogr2/auto-orient/blur/1x0/quality/75%7Cwatermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n**注意**:业务来了,我希望把点击商品卡片的业务放在ColumnList.vue中处理。你们想象一下要怎么做?一种土办法就是商品按钮点击时,Commodity组件\\$emit通知CommodityList.vue,而CommodityList接着把事件用\\$emit往上抛,那么ColumnList.vue就能处理这个点击事件了。这样做完全没有问题,但是显得子组件很不纯粹,跟业务都扯上关系了。\n\n那么如何优雅地解决这个问题呢?这个时候,作用域插槽真正派上用场了。\n\n通过作用域插槽将本应该由CommodityList处理的商品卡片点击业务onCommodityClick提升到ColumnList处理。\n\n```\n\n \n \n
\n {{column.columnName}}\n
\n \n \n \n
\n
\n
\n```\n\n而CommodityList组件内部应该是改造成这样,slot接收来自父组件的商品卡片组件,这里面不涉及关于商品组件的业务,只关注其他业务和布局即可。最终就实现了组件和业务的剥离,这也是组件化的精髓所在吧。不知道有没有帮到您呢?\n\n```\n\n \n \n \n\n```\n\n这是我实现的效果,忽略样式吧,原理都懂了,做个漂亮的卡片有多难?\n![淘宝运营栏目列表](http://qncdn.wbjiang.cn/%E4%BD%9C%E7%94%A8%E5%9F%9F%E6%8F%92%E6%A7%BD%E5%AE%9E%E7%8E%B0%E7%9A%84%E4%B8%89%E7%BA%A7%E7%BB%84%E4%BB%B6.png?imageMogr2/auto-orient/blur/1x0/quality/75%7Cwatermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n![淘宝运营栏目列表](http://qncdn.wbjiang.cn/%E4%BD%9C%E7%94%A8%E5%9F%9F%E6%8F%92%E6%A7%BD%E7%82%B9%E5%87%BB%E6%9F%90%E4%B8%80%E9%A1%B9.png?imageMogr2/auto-orient/blur/1x0/quality/75%7Cwatermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n> 总结一下,作用域插槽适合的场景是至少包含三级以上的组件层级,是一种优秀的组件化方案!\n', '2019-03-08 17:34:00', '2024-08-04 07:31:18', 1, 59, 0, '在网上搜了很多关于作用域插槽的解释,感觉没有写得很具体的吧,我认为应该对组件化有很深的理解才会触及到这个问题吧,这里也分享下我自己对于slot-scope的一点理解。', 'https://qncdn.wbjiang.cn/vue_375x300.png', 0, 0); -INSERT INTO `article` VALUES (160, 'Linux CentOS7系统下安装mysql8.0.13版本', '1.进入到https://www.mysql.com/downloads/msyql下载页,选择社区版\n\n2.查看linux版本,选择对应的版本下载\n![linux的mysql版本](http://qncdn.wbjiang.cn/linux%E7%9A%84mysql%E7%89%88%E6%9C%AC.png?imageMogr2/auto-orient/blur/1x0/quality/75%7Cwatermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n3.将下载的文件mysql-8.0.13-linux-glibc2.12-x86_64.tar.xz拷贝到linux服务器上的某目录下,然后解压,再复制到usr/local目录,并改名为mysql\n\n```\n[root@VM_0_14_centos mysql]# tar -xvf mysql-8.0.13-linux-glibc2.12-x86_64.tar.xz\n[root@VM_0_14_centos mysql]# cp -rv mysql-8.0.13-linux-glibc2.12-x86_64 /usr/local\n[root@VM_0_14_centos mysql]# cd /usr/local \n[root@VM_0_14_centos local]# mv mysql-8.0.13-linux-glibc2.12-x86_64 mysql\n```\n\n4.添加mysql用户\n\n```\nuseradd -s /sbin/nologin -M mysql\n```\n\n5.msyql初始化\n\n```\n/usr/local/mysql/bin/mysqld --initialize --user=mysql\n```\n\n此时会生成临时密码\n\n```\n[root@VM_0_14_centos mysql]# /usr/local/mysql/bin/mysqld --initialize --user=mysql\n2019-01-20T10:56:07.718326Z 0 [System] [MY-013169] [Server] /usr/local/mysql/bin/mysqld (mysqld 8.0.13) initializing of server in progress as process 5826\n2019-01-20T10:56:16.915217Z 5 [Note] [MY-010454] [Server] A temporary password is generated for root@localhost: twi=Tlsi<0O!\n2019-01-20T10:56:20.410563Z 0 [System] [MY-013170] [Server] /usr/local/mysql/bin/mysqld (mysqld 8.0.13) initializing of server has completed\n```\n\n6.复制启动、关闭脚本\n\n```\ncp /usr/local/mysql/support-files/mysql.server /etc/init.d/mysqld\n```\n\n7.修改配置文件,wq保存退出\n\n```\nvim /etc/my.cnf\n\n[mysqld]\n basedir = /usr/local/mysql \n datadir = /var/lib/mysql\n socket = /var/lib/mysql/mysql.sock\n character-set-server=utf8\n [client]\n socket = /var/lib/mysql/mysql.sock\n default-character-set=utf8\n```\n\n8.启动数据库服务\n\n```\nservice mysqld start\n```\n报错\n\n> mysqld_safe Directory \'/var/lib/mysql\' for UNIX socket file don\'t exists.\n\n一是因为没有/var/lib/mysql这个目录,二是没有写的权限,mysql.sock文件无法生成。\n\n```\n[root@VM_0_14_centos lib]# mkdir mysql\n[root@VM_0_14_centos lib]# chmod 777 /var/lib/mysql\n```\n\n再次运行service mysqld start报另一个错\n\n```\nStarting MySQL. ERROR! The server quit without updating PID file (/var/lib/mysql/VM_0_14_centos.pid).\n```\n\n打印出具体报错信息\n\n```\n[root@VM_0_14_centos mysql]# cat VM_0_14_centos.err\n\n2019-01-20T11:11:45.906800Z 0 [System] [MY-010116] [Server] /usr/local/mysql/bin/mysqld (mysqld 8.0.13) starting as process 7788\n2019-01-20T11:11:45.910813Z 0 [Warning] [MY-013242] [Server] --character-set-server: \'utf8\' is currently an alias for the character set UTF8MB3, but will be an alias for UTF8MB4 in a future release. Please consider using UTF8MB4 in order to be unambiguous.\n2019-01-20T11:11:45.925456Z 1 [ERROR] [MY-011011] [Server] Failed to find valid data directory.\n2019-01-20T11:11:45.925586Z 0 [ERROR] [MY-010020] [Server] Data Dictionary initialization failed.\n2019-01-20T11:11:45.925600Z 0 [ERROR] [MY-010119] [Server] Aborting\n2019-01-20T11:11:45.926342Z 0 [System] [MY-010910] [Server] /usr/local/mysql/bin/mysqld: Shutdown complete (mysqld 8.0.13) MySQL Community Server - GPL.\n2019-01-20T11:12:00.049920Z 0 [System] [MY-010116] [Server] /usr/local/mysql/bin/mysqld (mysqld 8.0.13) starting as process 7975\n2019-01-20T11:12:00.052469Z 0 [Warning] [MY-013242] [Server] --character-set-server: \'utf8\' is currently an alias for the character set UTF8MB3, but will be an alias for UTF8MB4 in a future release. Please consider using UTF8MB4 in order to be unambiguous.\n2019-01-20T11:12:00.060600Z 1 [ERROR] [MY-011011] [Server] Failed to find valid data directory.\n2019-01-20T11:12:00.060745Z 0 [ERROR] [MY-010020] [Server] Data Dictionary initialization failed.\n2019-01-20T11:12:00.060759Z 0 [ERROR] [MY-010119] [Server] Aborting\n2019-01-20T11:12:00.061610Z 0 [System] [MY-010910] [Server] /usr/local/mysql/bin/mysqld: Shutdown complete (mysqld 8.0.13) MySQL Community Server - GPL.\n```\n\n看不出来具体是哪里的问题,于是运行service --status-all,有报错信息\n\n> ERROR! MySQL is not running, but lock file (/var/lock/subsys/mysql) exists\n\n有网友说删了该文件就可以,结果我删了也没用。\n\n那就接着排查刚才的err文件,关键的错误应该是这两行\n\n```\n2019-01-20T11:11:45.925456Z 1 [ERROR] [MY-011011] [Server] Failed to find valid data directory.\n2019-01-20T11:11:45.925586Z 0 [ERROR] [MY-010020] [Server] Data Dictionary initialization failed.\n```\n\n于是查找my.cnf,与data目录有关的就是datadir=/var/lib/mysql这一条配置了,我尝试性地删了这一行,结果成功了,service mysqld start成功!\n\n9.mysql -u root -p登录mysql报错\n解决方法如下:\n\n```\ncd /usr/local/bin \nln -fs /usr/local/mysql/bin/mysql mysql\n```\n\n\n10.show databases报错\n\n```\nyou must reset your password using ALTER USER statement before executing this statement.\n```\n\n解决方法:\n\n```\nalter user user() identified by \'123456\';\n```\n\n11.用ip无法远程登录mysql,只能用localhost在linux服务器登录\n修改权限配置\n\n```\ngrant all privileges on *.* to \'root\'@\'%\' identified by \'123456\';\n```\n\n但是报错\n\n> You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near \'identified by \'123456\' at line 1\n\n解决方法:\n\n```\nuse mysql;\nupdate user set host = \'%\' where user = \'root\';\nflush privileges;\n```\n\n接着用navicat连接时报错\n\n> Client does not support authentication protocol requested by server; consider upgrading MySQL client\n\n解决方法:\n\n```\nALTER USER \'root\'@\'*\' IDENTIFIED WITH mysql_native_password BY \'123456\';\n```\n\n', '2019-03-14 11:48:43', '2024-07-25 01:04:28', 1, 46, 0, '又发水文了,主题是Linux CentOS7系统下安装mysql8.0.13版本。', 'https://qncdn.wbjiang.cn/linux_400x300.png', 0, 0); -INSERT INTO `article` VALUES (161, 'CentOS7系统下修改mysql8.0版本密码', '# 前言\n忘记mysql登录密码是很常见的操作,今天讲一下linux centos7下mysql8.0版本修改密码的方法。\n\n# 踩坑\n网上很多文章说的是mysql5.x版本的修改密码方法,按照这些方法做就会遇到坑了。\n\n忘记密码了,首先尝试修改mysql的配置文件/etc/my.cnf,有的人安装目录可能不太一样,配置文件会是/etc/mysql/my.cnf或者其他的目录下。\n\n![mysql配置文件](http://qncdn.wbjiang.cn/mysql%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6.png?imageMogr2/auto-orient/blur/1x0/quality/75%7Cwatermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n在[mysqld]下面添加一行,可以跳过密码登录\n\n```\nskip-grant-tables\n```\n\n重启mysqld服务\n\n```\nservice mysqld restart\n```\n\n输入mysql回车进入mysql命令行,尝试执行\n\n```\nupdate user set password=password(\"123456\") where user=\"root\";\n```\n\n直接就报语句错误了,看来可能是password函数有问题。\n\n> ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near \'(\"123456\") where user=\"root\"\' at line 1\n\n接着尝试另一个方法。\n\n```\nmysql> ALTER USER \'root\'@\'*\' IDENTIFIED WITH mysql_native_password BY \'123456\'\n```\n\n也报错,--skip-grant-tables模式下,不能运行这条语句。\n\n> ERROR 1290 (HY000): The MySQL server is running with the --skip-grant-tables option so it cannot execute this statement\n\n于是我先查查user表的数据。\n\n```\nselect user, password from user\n```\n![mysql8 user表没有password列](http://qncdn.wbjiang.cn/mysql8user%E8%A1%A8%E6%B2%A1%E6%9C%89password.png?imageMogr2/auto-orient/blur/1x0/quality/75%7Cwatermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n发现user表中根本没有password这个字段,上网查了后发现只有authentication_string,在mysql5.7.9后就废弃了password字段和password()函数。\n\n需要先将authentication_string设置为空\n\n```\nupdate user set authentication_string = ‘’ where user = ‘root’;\n```\n\n然后退出mysql,删除/etc/my.cnf的skip-grant-tables,重启mysqld服务。\n\n接着尝试登录mysql\n\n```\nmysql -uroot -p\n```\n\n直接回车登录mysql,再使用alter修改用户密码\n\n```\nalter user ‘root’@’%’ indentified by ‘123456’;\n```\n\n提示成功!!!\n', '2019-03-14 11:50:15', '2024-07-25 01:06:13', 1, 53, 0, '忘记mysql登录密码是很常见的操作,今天讲一下linux centos7下mysql8.0版本修改密码的方法。网上很多文章说的是mysql5.x版本的修改密码方法,按照这些方法做就会遇到坑了。', 'https://qncdn.wbjiang.cn/key_400x300.png', 0, 0); -INSERT INTO `article` VALUES (163, '前端攻城狮HTML5自查手册', '\n# 前言\nHTML5自2014年发布以来,已经有快5个年头了。但是很多人对H5有哪些新特性,兼容性如何仍然是一头雾水的。为了让自己以后方便查阅,本文整理一下H5的相关知识点,不做深入的探讨,错误之处还请指正!\n\n# HTML5标签上的改动\n[HTML 5 参考手册](http://www.w3school.com.cn/html5/html5_reference.asp)\n## 废弃或不支持的标签\n\n - <acronym>\n\n定义首字母缩略词。HTML5 不支持 <acronym> 标签。请使用 <abbr> 标签代替它。\n\n- <applet>\n\n可以嵌入Java语言编写的小应用程序。HTML5 不支持 <applet> 标签。请使用 <object> 标签代替它。在 HTML 4.01 中,<applet> 元素 已废弃。\n\n- <basefont>\n\n只有 IE 9 和更早版本的 IE 浏览器支持 <basefont> 标签。应该避免使用该标签。在 HTML 4.01 中,<basefont> 元素 已废弃。\n\n- <big>\n\n用来制作更大的文本。HTML5 不支持 <big> 标签。请用 CSS 代替。\n\n- <center>\n\n对其所包括的文本进行水平居中。在 HTML 4.01 中,<center> 元素 已废弃\n\n- <dir>\n\n被用来定义目录列表,类似ul,ol。在 HTML 4.01 中,<dir> 元素 已废弃。\n\n- <font>\n\n规定文本的字体、字体尺寸、字体颜色。在 HTML 4.01 中,<font> 元素 已废弃。\n\n- <frame>\n\n定义 中的子窗口(框架),必须放在<frameset>标签中,且不能与<body>共存。HTML5 不支持 <frame> 标签。\n\n- <frameset>\n\n定义一个框架集,被用来组织一个或者多个<frame>元素。每个<frame>有各自独立的文档。HTML5 不支持<frameset>标签。\n\n- <isindex>\n\n使浏览器显示一个对话框,提示用户输入单行文本,该特性已经从 Web 标准中删除。\n\n- <noframes>\n\n可为那些不支持框架的浏览器显示文本。HTML5 不支持 <noframes>标签。\n\n```\n\n \n \n \n Sorry, your browser does not handle frames!\n\n```\n\n- <strike>\n\n定义加删除线文本。在 HTML 4.01 中,<strike> 元素 已废弃。HTML5 不支持 <strike> 标签。请用 <del> 标签代替。\n\n- <tt>\n\n定义打字机文本。HTML5 不支持<tt>标签。请用 CSS 代替。\n\n## 新增的标签\nIE 9+、Firefox、Opera、Chrome 和 Safari 都支持新增的大部分 H5 标签。\n### 结构标签\n- <main>\n\n规定文档的主要内容。在一个文档中,不能出现一个以上的 <main> 元素。<main> 元素不能是以下元素的后代:<article><aside><footer><header><nav>。所有浏览器都支持<main>标签,除了 Internet Explorer。\n\n- <article>\n\n定义独立的内容,内容本身必须是有意义的且必须是独立于文档的其余部分。比如:论坛帖子,博客文章,新闻故事,评论。\n\n- <aside>\n\n常用作侧边栏。\n\n- <section>\n\n定义了文档的某个区域。比如章节、头部、底部或者文档的其他区域。\n\n- <header>\n\n定义文档或者文档的一部分区域的页眉。\n\n- <hgroup>\n\n被用来对标题元素进行分组。\n\n- <footer>\n\n定义文档或者文档的一部分区域的页脚。\n\n- <nav>\n\n定义导航链接的部分。\n### 媒体标签\n- <audio>\n\n定义声音,比如音乐或其他音频流。支持的3种文件格式:MP3、Wav、Ogg。\n\n- <video>\n\n定义视频,比如电影片段或其他视频流。支持三种视频格式:MP4、WebM、Ogg。\n\n- <track>\n\n为媒体元素(比如 <audio> and <video>)规定外部文本轨道。IE 10、Opera 和 Chrome 浏览器支持 <track> 标签,其他浏览器不支持。\n\n- <source>\n\n为媒体元素(比如 <audio> and <video>)定义媒体资源。\n### 其他标签\n- <canvas>\n\n画布,可以绘制丰富的图形,赋予了html更多想象的空间。\n\n- <datalist>\n\n配合<option>标签制作下拉列表,与<select>不同的一点是,<datalist>支持输入,模糊匹配。\n\n- <details>\n\n类似于折叠面板的一个控件,规定了用户可见的或者隐藏的需求的补充细节。<summary>标签可以为 <details> 定义标题。标题是可见的,用户点击标题时,会显示出 <details>。目前,只有 Chrome 和 Safari 6 支持 <details> 标签。\n\n- <summary>\n\n与<details>标签配合使用。只有 Chrome 和 Safari 6 支持 <summary> 标签。\n\n- <embed>\n\n定义了一个容器,用来嵌入外部应用或者互动程序(插件),例如flash等。\n\n- <figure>\n\n规定独立的流内容(图像、图表、照片、代码等等)。\n\n- <figcaption>\n\n<figcaption>元素被用来为<figure>元素定义标题。\n\n- <mark>\n\n定义带有记号的文本。请在需要突出显示文本时使用<mark> 标签。\n\n- <meter>\n\n定义度量衡。仅用于已知最大和最小值的度量。不能作为一个进度条来使用。Firefox、Opera、Chrome 和 Safari 6 支持 <meter> 标签。IE不支持该标签。\n\n- <progress>\n\n定义运行中的任务进度(进程)。有value和max属性。\n\n- <output>\n\n作为计算结果输出显示(比如执行脚本的输出)。配合两个<input>使用,可实时求和。Internet Explorer 浏览器不支持 <output> 标签。\n\n- <ruby>\n\n定义 ruby 注释(中文注音或字符)。\n\n- <rp>\n\n在 ruby 注释中使用,以定义不支持 ruby 元素的浏览器所显示的内容。\n\n- <rt>\n\n定义字符(中文注音或字符)的解释或发音。\n\n- <time>\n\n定义公历的时间(24 小时制)或日期,时间和时区偏移是可选的。用datetime属性对标签中的文字作时间解释。\n\n- <bdi>\n\n允许您设置一段文本,使其脱离其父元素的文本方向设置。具体应用不详。\n\n### 让IE8及以下版本也支持H5新标签\n我们经常会用到<main><article><aside><footer><header><nav>来进行页面布局,那么如何解决IE8及以下版本支持这些标签呢?只要利用createElement让浏览器识别这些标签,并在css中给他们设置一些属性即可,比如display:block。现成的解决方案就是[htmlshiv.js](https://github.com/aFarkas/html5shiv)。\n\n```\n\n```\n\n如果有打印需求,则需要html5shiv-printshiv.js,它包含 html5shiv.js 的全部功能,并且额外支持 IE6-8 网页打印时 HTML5 元素样式化。\n\n# HTML5属性上的改动\n## 新增的属性\nIE 9+、Firefox、Opera、Chrome 和 Safari 都支持新增的大部分 H5 属性,特殊情况会在每一项处有说明。\n\n[HTML5标准属性](http://www.w3school.com.cn/html5/html5_ref_standardattributes.asp)\n\n- contenteditable\n\n规定是否允许用户编辑内容。可用于制作富文本等功能。兼容性较好,见下图。\n![contenteditable兼容性良好](http://qncdn.wbjiang.cn/contenteditable%E5%85%BC%E5%AE%B9%E6%80%A7%E8%89%AF%E5%A5%BD.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n- contextmenu\n\n规定了元素的上下文菜单。当用户右击元素时将显示上下文菜单。contextmenu 属性的值是需要打开的<menu>元素的 id。兼容性不好,目前只有 Firefox 浏览器支持 contextmenu 属性。\n\n- data-*\n\n管理自定义属性。自定义属性可通过元素的dataset进行访问。如ele.dataset.customAttr。\n\n兼容性见下图,IE6~8也支持data-*,但是不能通过dataset访问,必须用getAttribute访问。\n![h5自定义属性兼容性](http://qncdn.wbjiang.cn/h5%E8%87%AA%E5%AE%9A%E4%B9%89%E5%B1%9E%E6%80%A7%E5%85%BC%E5%AE%B9%E6%80%A7.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n- draggable\n\n规定元素是否可拖动。链接和图像默认是可拖动的。\n```\n\n```\n\n主要关注的内容有属性draggable,事件ondragstart,事件ondragover,事件ondrop,数据属性dataTransfer,以及dataTransfer下的两个方法setDatagetData。\n\n简单demo可以参考[HTML5拖放教程](http://www.runoob.com/html/html5-draganddrop.html)。\n\n- hidden\n\nhidden 属性规定对元素进行隐藏。IE兼容性不太好,避免使用,用css替代即可。\n![hidden属性兼容性](http://qncdn.wbjiang.cn/hidden%E5%85%BC%E5%AE%B9%E6%80%A7.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n- spellcheck\n\n规定是否对元素内容进行拼写检查。启用后会对单词进行拼写检查,不正确的单词会有波浪线提示。\nInternet Explorer 10, Firefox, Opera, Chrome, 和 Safari 浏览器支持 spellcheck 属性。\n\n```\n\n```\n\n## 更丰富的表单\n### input支持更多type\n HTML5 中的新类型:color、date、datetime、datetime-local、month、week、time、email、number、range、search、tel 和 url。其实也是一种语义化的表现。\n\n|值| 描述 |\n|--|--|\n| color | 定义拾色器。兼容性很差,对IE,Edge,Safari等浏览器不友好,详细情况见[兼容性](https://www.caniuse.com/#search=color)|\n| date | 定义 date 控件(包括年、月、日,不包括时间)。[兼容性](https://www.caniuse.com/#search=date)很差。|\n| datetime | 定义 date 和 time 控件(包括年、月、日、时、分、秒、几分之一秒,基于 UTC 时区)。[兼容性](https://www.caniuse.com/#search=datetime)很差。|\n| datetime-local | 定义 date 和 time 控件(包括年、月、日、时、分、秒、几分之一秒,不带时区)。[兼容性](https://www.caniuse.com/#search=datetime-local)很差。|\n| month | 定义 month 和 year 控件(不带时区)。[兼容性](https://www.caniuse.com/#search=month)很差。|\n| week | 定义 week 和 year 控件(不带时区)。[兼容性](https://www.caniuse.com/#search=week)很差。|\n| time | 定义用于输入时间的控件(不带时区)。[兼容性](https://www.caniuse.com/#search=time)很差。|\n| email | 定义用于 e-mail 地址的字段。会对邮箱进行格式检查。支持IE10以上,详细情况见[兼容性](https://www.caniuse.com/#search=email)|\n| number | 定义用于输入数字的字段。在各个浏览器上有一些[差异](https://www.caniuse.com/#search=number)![number兼容性](http://qncdn.wbjiang.cn/number%E5%85%BC%E5%AE%B9%E6%80%A7.png?imageMogr2/auto-orient/blur/1x0/quality/75\\|watermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10) |\n| range | 定义滑块。支持IE10以上,详细情况见[兼容性](https://www.caniuse.com/#search=range)|\n| search | 定义用于输入搜索字符串的文本字段。支持IE10以上,但是在UI表现上与text没有差别。[查看详情](https://www.caniuse.com/#search=search)|\n| tel | 定义用于输入电话号码的字段。支持IE10以上,详细情况见[兼容性](https://www.caniuse.com/#search=tel)|\n| url | 定义用于输入 URL 的字段。会对url进行格式检查。支持IE10以上,详细情况见[兼容性](https://www.caniuse.com/#search=url)|\n\n### 其他表单控件属性\n|属性| 描述 |\n|--|--|\n| placeholder | 可描述输入字段预期值的简短的提示信息,支持IE10以上,适用于下面的 input 类型:text、search、url、tel、email 和 password。|\n| autofocus | 页面加载时自动获得焦点,支持IE10以上。|\n| multiple | 规定允许用户输入到 input 元素的多个值。适用于以下 input 类型:email 和 file。常见于上传文件时选择多个文件。|\n| form | 规定 input 元素所属的一个或多个表单的 id 列表,以空格分隔。可以实现将 input 放在 form 标签外部。但是不支持IE。|\n| required | 规定必需在提交表单之前填写输入字段,支持IE10以上。|\n| maxlength | 规定 input 元素中允许的最大字符数,适用于text类型。|\n| minlength | 规定 input 元素中允许的最小字符数,适用于text类型。|\n| max | 规定 input 元素的最大值,max 和 min 属性适用于以下 input 类型:number、range、date、datetime、datetime-local、month、time 和 week。支持IE10以上,不支持firefox,其中IE10不支持max用于date 和 time类型。|\n| min | 规定 input 元素的最小值。|\n| pattern | 规定 input 元素的正则表达式校验。适用于下面的 input 类型:text、search、url、tel、email 和 password。应该配合 title 属性提示用户。|\n# HTML5其他新特性\n支持IE9+\n## 音视频\n|标签| 描述 |\n|--|--|\n| source | 为媒体元素(比如 video 和 audio)定义媒体资源。主要定义其 src 属性和 type 属性,src 规定媒体文件的 URL,type 规定媒体资源的 MIME 类型。|\n| audio | 定义音频。对mp3文件的兼容性最好。![audio媒体支持](http://qncdn.wbjiang.cn/audio%E5%AA%92%E4%BD%93%E6%94%AF%E6%8C%81.png?imageMogr2/auto-orient/blur/1x0/quality/75%7Cwatermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)|\n| video | 定义视频。对MP4文件的兼容性最好。![video媒体支持](http://qncdn.wbjiang.cn/video%E5%AA%92%E4%BD%93%E6%94%AF%E6%8C%81.png?imageMogr2/auto-orient/blur/1x0/quality/75%7Cwatermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)|\n\n## 画布canvas\n可以说是前端高级部分了,这里一言难尽,慢慢学习吧。支持IE9+。与之相关的svg也是支持IE9+。\n\n## Web存储\n主要包括sessionStorage和localStorage,操作的API都类似,区别是sessionStorage是会话级存储,localStorage是持久化存储。兼容性挺好,支持IE8+。\n\n## 地理定位geolocation\nnavigator下的一个属性,鉴于该特性可能侵犯用户的隐私,除非用户同意,否则用户位置信息是不可用的。支持IE9+。\n\n## HTML5 Application Cache\n实现网页离线访问的利器。支持IE10+。相关的最新技术还有PWA等。\n\n## Web Worker\n让js也能做多线程的事情,相关内容可以参考[Web Worker 使用教程](http://www.ruanyifeng.com/blog/2018/07/web-worker.html)。[MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API/Using_web_workers)上也有比较详细的解释。\n\n## HTML 5 服务器发送事件 EventSource\nEventSource 接口用于接收服务器发送的事件。它通过HTTP连接到一个服务器,以text/event-stream 格式接收事件, 不关闭连接(即长连接)。兼容性不是很好,IE和Edge直接废了。不过有一个兼容方案 [event-source-polyfill](https://www.npmjs.com/package/event-source-polyfill)。\n\n## HTML5 WebSocket\nWebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。对IE10+能较好兼容,对于不兼容的浏览器,也有很多优雅降级方案,一般是降级成ajax轮询等,像[socket.io](https://socket.io/)。\n\n## 通知接口Notification\nNotifications API 的通知接口用于向用户显示桌面通知。查看[具体用法](https://developer.mozilla.org/zh-CN/docs/Web/API/notification)。[兼容性](https://www.caniuse.com/#search=Notification)不是很好,但是用起来网站的逼格高不少,如果是IE就直接放弃吧。\n', '2019-04-15 22:38:14', '2024-08-08 08:48:43', 1, 56, 0, 'HTML5自2014年发布以来,已经有快5个年头了。但是很多人对H5有哪些新特性,兼容性如何仍然是一头雾水的。为了让自己以后方便查阅,本文整理一下H5的相关知识点,不做深入的探讨,错误之处还请指正!', 'https://qncdn.wbjiang.cn/html%E6%A0%87%E7%AD%BE.png', 0, 0); -INSERT INTO `article` VALUES (164, 'ubuntu系统下sudo权限用户安装nodejs和nginx', '# 前言\n\n为了支撑公司某 ios app 上线,今天做了个隐私政策 h5 页面并上线,顺手体验了一把 ubuntu系统的 sudo 权限。本来想用 nodejsexpress 框架搭个简单的静态资源托管服务,然后用 nginx 做下反向代理。但是在安装 express-generator 时遇到点问题,可能 sudo 权限玩得不够熟练,跟 root 用户还是有很大区别的。本文简单说下自己在 ubuntu 系统下 sudo 权限用户安装 nodejsnginx 的过程。\n\n# 安装nodejs\n\n## 下载安装包\n\n话不多说,直接上[nodejs下载链接](https://nodejs.org/en/download/)。\n\n![nodejs下载](http://qncdn.wbjiang.cn/%E4%B8%8B%E8%BD%BDubuntu64%E4%BD%8Dnodejs%E5%8C%85.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n## 解压和软连接\n\n下载到的 nodejs 包是一个 .tar.xz 格式的包,解压命令如下:\n\n```\ntar -xvf node-v10.15.3-linux-x64.tar.xz\n```\n\n![解压nodejs包](http://qncdn.wbjiang.cn/ubuntu%E8%A7%A3%E5%8E%8Bnodejs.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n为了让 nodenpm 命令行全局可用,我采用了软连接的方式\n\n```\nsudo ln -s /home/devadmin/frontend/download/node/bin/node /usr/local/bin/node\nsudo ln -s /home/devadmin/frontend/download/node/bin/npm /usr/local/bin/npm\n```\n\n![软连接](http://qncdn.wbjiang.cn/sudo%E8%BD%AF%E8%BF%9E%E6%8E%A5node%E5%92%8Cnpm%E5%91%BD%E4%BB%A4.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n然后就可以舒服地使用 nodenpm 命令行了。\n\n## 设置npm代理\n\n为了提升 npm install 的速度和体验,我还用到了 npm 代理。\n\n```\nnpm config set registry https://registry.npm.taobao.org\n```\n\n![npm设置代理](http://qncdn.wbjiang.cn/ubuntu%E7%9A%84npm%E8%AE%BE%E7%BD%AE%E4%BB%A3%E7%90%86.png)\n\n这样 nodejs 就算安装完成了。\n\n# 安装nginx\n\n安装 nginx 时采用的是 apt-get 的下载方式\n\n```\nsudo apt-get install nginx\n```\n## nginx关注点\n\n安装后要知道的几点是:\n\n - 配置文件所在目录\n\n```\n/etc/nginx/\n```\n\n - 静态资源所在目录\n\n```\n/usr/share/nginx/\n```\n\n - nginx主程序\n\n```\n/usr/sbin/nginx\n```\n\n - nginx日志所在目录\n\n```\n/var/log/nginx/\n```\n\n## 配置文件的坑\n\n配置文件 nginx.conf 有个坑,需要把配置中的两行注释掉才有效。\n\n```\n# include /etc/nginx/conf.d/*.conf;\n# include /etc/nginx/sites-enabled/*;\n```\n\n## 其他的坑\n\n遇到了 xftp 无法上传文件的情况,一般是文件夹权限不够,可以提高权限,然后再尝试。\n\n```\nsudo chmod 777 dirname\n```\n', '2019-04-16 22:57:38', '2024-09-04 03:12:29', 1, 85, 0, '为了支撑公司某 ios app 上线,今天做了个隐私政策 h5 页面并上线,顺手体验了一把 ubuntu系统的 sudo 权限。本来想用 nodejs 的 express 框架搭个简单的静态资源托管服务,然后用 nginx 做下反向代理。但是在安装 express-generator 时遇到点问题,可能 sudo 权限玩得不够熟练,跟 root 用户还是有很大区别的。本文简单说下自己在 ubuntu 系统下 sudo 权限用户安装 nodejs 和 nginx 的过程。', 'https://qncdn.wbjiang.cn/ubuntu2_400x300.png', 0, 0); -INSERT INTO `article` VALUES (165, '让Nodejs支持H5 History模式(connect-history-api-fallback源码分析)', '# 导读\n\n本文主要是对`connect-history-api-fallback`库进行一次源码分析。`connect-history-api-fallback`是一个用于支持SPA History路由模式的`nodejs`库。阅读本文前,应对`HTML5 History`模式有一定程度的了解!\n\n# 源码分析\n\n```js\n/** \n * 前端需要开启history模式,而后端根据url并不知道前端在请求api还是在请求页面,如localhost:4200/home这种url,前端理所当然认为“我需要得到html,并跳转到首页”,然而后端并不能区分。\n * 因此需要一种判断机制,来使得后端能分析出前端的请求目的。\n * connect-history-api-fallback 这个中间件正好帮我们完成了上述分析操作,来看下它是怎么实现的吧!\n * 第一次把自己的源码分析思路写出来,说得不对的地方,请指出!\n */\n\n\'use strict\';\n\nvar url = require(\'url\');\n\nexports = module.exports = function historyApiFallback(options) {\n // 接收配置参数\n options = options || {};\n // 初始化日志管理器\n var logger = getLogger(options);\n\n // 中间件是要返回一个函数的,函数形参有req, res, next\n return function(req, res, next) {\n var headers = req.headers;\n if (req.method !== \'GET\') {\n // 如果请求方法不是GET类型,说明不需要返回html,那么就调用next(),把请求交给下一个中间件\n logger(\n \'Not rewriting\',\n req.method,\n req.url,\n \'because the method is not GET.\'\n );\n return next();\n } else if (!headers || typeof headers.accept !== \'string\') {\n // 如果没有请求头,或者请求头中的accept不是字符串,说明不是一个标准的http请求,也不予处理,把请求交给下一个中间件\n logger(\n \'Not rewriting\',\n req.method,\n req.url,\n \'because the client did not send an HTTP accept header.\'\n );\n return next();\n } else if (headers.accept.indexOf(\'application/json\') === 0) {\n // 如果客户端希望得到application/json类型的响应,说明也不是在请求html,也不予处理,把请求交给下一个中间件\n logger(\n \'Not rewriting\',\n req.method,\n req.url,\n \'because the client prefers JSON.\'\n );\n return next();\n } else if (!acceptsHtml(headers.accept, options)) {\n // 如果请求头中不包含配置的Accept或者默认的[\'text/html\', \'*/*\'],那么说明也不是在请求html,也不予处理,把请求交给下一个中间件\n logger(\n \'Not rewriting\',\n req.method,\n req.url,\n \'because the client does not accept HTML.\'\n );\n return next();\n }\n\n // 走到这里说明是在请求html了,要开始秀操作了\n\n // 首先利用url模块的parse方法解析下url,会得到一个对象,包括protocol,hash,path, pathname, query, search等字段,类似浏览器的location对象\n var parsedUrl = url.parse(req.url);\n var rewriteTarget;\n // 然后得到配置中的rewrites,也就是重定向配置;\n // 重定向配置是一个数组,每一项都包含from和to两个属性;\n // from是用来正则匹配pathname是否需要重定向的;\n // to则是重定向的url,to可以是一个字符串,也可以是一个回调方法来返回一个字符串,回调函数接收一个上下文参数context,context包含三个属性(parsedUrl,match,request)\n options.rewrites = options.rewrites || [];\n // 遍历一波重定向配置\n for (var i = 0; i < options.rewrites.length; i++) {\n var rewrite = options.rewrites[i];\n // 利用字符串的match方法去匹配\n var match = parsedUrl.pathname.match(rewrite.from);\n if (match !== null) {\n // 如果match不是null,说明pathname和重定向配置匹配上了\n rewriteTarget = evaluateRewriteRule(parsedUrl, match, rewrite.to, req);\n\n if(rewriteTarget.charAt(0) !== \'/\') {\n // 推荐使用/开头的绝对路径作为重定向url\n logger(\n \'We recommend using an absolute path for the rewrite target.\',\n \'Received a non-absolute rewrite target\',\n rewriteTarget,\n \'for URL\',\n req.url\n );\n }\n\n logger(\'Rewriting\', req.method, req.url, \'to\', rewriteTarget);\n // 进行重定向url操作\n req.url = rewriteTarget;\n return next();\n }\n }\n\n var pathname = parsedUrl.pathname;\n // 首先说明一下:校验逻辑默认是会去检查url中最后的.号的,有.号的说明在请求文件,那就跟history模式就没什么鸟关系了\n // 我暂且将上述规则成为“点号校验规则”\n // disableDotRule为true,代表禁用点号校验规则\n if (pathname.lastIndexOf(\'.\') > pathname.lastIndexOf(\'/\') &&\n options.disableDotRule !== true) {\n // 如果pathname的最后一个/之后还有.,说明请求的是/a/b/c/d.*的文件(*代表任意文件类型);\n // 如果此时配置disableDotRule为false,说明开启点号校验规则,那么不予处理,交给其他中间件\n logger(\n \'Not rewriting\',\n req.method,\n req.url,\n \'because the path includes a dot (.) character.\'\n );\n return next();\n }\n\n // 如果pathname最后一个/之后没有.,或者disableDotRule为true,都会走到最后一步:重写url\n // 重写url有默认值/index.html,也可以通过配置中的index自定义\n rewriteTarget = options.index || \'/index.html\';\n logger(\'Rewriting\', req.method, req.url, \'to\', rewriteTarget);\n // 重写url\n req.url = rewriteTarget;\n // 此时再将执行权交给下一个中间件(url都换成index.html了,后面的路由等中间件也不会再处理了,然后前端接收到html就开始解析路由了,目的达到!)\n next();\n };\n};\n\n// 判断重定向配置中的to\nfunction evaluateRewriteRule(parsedUrl, match, rule, req) {\n if (typeof rule === \'string\') {\n // 如果是字符串,直接返回\n return rule;\n } else if (typeof rule !== \'function\') {\n // 如果不是函数,抛出错误\n throw new Error(\'Rewrite rule can only be of type string or function.\');\n }\n\n // 执行自定义的回调函数,得到一个重定向的url\n return rule({\n parsedUrl: parsedUrl,\n match: match,\n request: req\n });\n}\n\n// 判断请求头的accept是不是包含在配置数组或默认数组的范围内\nfunction acceptsHtml(header, options) {\n options.htmlAcceptHeaders = options.htmlAcceptHeaders || [\'text/html\', \'*/*\'];\n for (var i = 0; i < options.htmlAcceptHeaders.length; i++) {\n if (header.indexOf(options.htmlAcceptHeaders[i]) !== -1) {\n return true;\n }\n }\n return false;\n}\n\n// 处理日志\nfunction getLogger(options) {\n if (options && options.logger) {\n // 如果有指定的日志方法,则使用指定的日志方法\n return options.logger;\n } else if (options && options.verbose) {\n // 否则,如果配置了verbose,默认使用console.log作为日志方法\n return console.log.bind(console);\n }\n // 否则就没有日志方法,就不记录日志咯\n return function(){};\n}\n\n```\n\n', '2019-05-17 13:39:15', '2024-11-13 07:02:07', 1, 187, 0, '本文主要是对connect-history-api-fallback库进行一次源码分析。connect-history-api-fallback是一个用于支持SPA History路由模式的nodejs库。阅读本文前,应对HTML5 History模式有一定程度的了解!', 'https://qncdn.wbjiang.cn/nodejs.png', 0, 0); -INSERT INTO `article` VALUES (166, 'Gerrit的这些坑,我帮你踩了', '# 前言\n现在还是有不少公司在使用Gerrit进行代码托管和代码审查的,比如我司。跟GitHub相比,Gerrit的使用还是有些差异的。\n\n# Gerrit使用', '2019-05-28 16:17:42', '2019-05-28 17:41:22', 1, 8, 0, '现在还是有不少公司在使用Gerrit进行代码托管和代码审查的,比如我司。跟GitHub相比,Gerrit的使用还是有些差异的。', 'https://qncdn.wbjiang.cn/gerrit.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d2JqaWFuZy5jbg==/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10', 1, 0); -INSERT INTO `article` VALUES (167, 'VSCode缩进方式转换', '# 前言\n\n首先要明确的是,一般我们推荐的是采用空格进行缩进,因为tab不是一个标准的东西,如果使用tab缩进,可能在不同的系统中表现有差异。我个人还是喜欢4空格缩进,所以对于某些2空格缩进的代码,我还是有必要去做下转换的。下面以2空格缩进转4空格缩进为例进行说明。\n\n# 2空格缩进转4空格缩进方法\n\n第一步,查看并确认下我们当前的缩进方式。\n\n![查看当前缩进方式](http://qncdn.wbjiang.cn/%E6%9F%A5%E7%9C%8B%E5%BD%93%E5%89%8D%E7%BC%A9%E8%BF%9B%E6%96%B9%E5%BC%8F.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d3d3LndiamlhbmcuY24=/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n可以看到,是以2空格作为缩进。\n\n![2空格缩进](http://qncdn.wbjiang.cn/2%E7%A9%BA%E6%A0%BC%E7%BC%A9%E8%BF%9B.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d3d3LndiamlhbmcuY24=/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n接着,我们把缩进转换为tab。\n\n![空格转换为tab](http://qncdn.wbjiang.cn/%E7%A9%BA%E6%A0%BC%E8%BD%AC%E6%8D%A2%E4%B8%BAtab.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d3d3LndiamlhbmcuY24=/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n\n然后,我们再设置以tab为缩进方式,并指定数值为4(代表1个tab表现为4个空格,但它毕竟还是tab,不是标准的空格)。\n\n![以tab为缩进方式](http://qncdn.wbjiang.cn/%E4%BB%A5tab%E4%B8%BA%E7%BC%A9%E8%BF%9B%E6%96%B9%E5%BC%8F.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d3d3LndiamlhbmcuY24=/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n最后我们把缩进转为空格,这样2空格就变为4空格了。\n\n![缩进转为空格](http://qncdn.wbjiang.cn/%E7%BC%A9%E8%BF%9B%E8%BD%AC%E4%B8%BA%E7%A9%BA%E6%A0%BC.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d3d3LndiamlhbmcuY24=/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n![缩进变为4空格了](http://qncdn.wbjiang.cn/%E7%BC%A9%E8%BF%9B%E5%8F%98%E4%B8%BA4%E7%A9%BA%E6%A0%BC%E4%BA%86.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d3d3LndiamlhbmcuY24=/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n同理,4空格变为2空格也是类似的。', '2019-05-30 11:56:04', '2024-10-29 05:12:26', 1, 267, 0, '一般我们推荐的是采用空格进行缩进,因为tab不是一个标准的东西,如果使用tab缩进,可能在不同的系统中表现有差异。我个人还是喜欢4空格缩进,所以对于某些2空格缩进的代码,我还是有必要去做下转换的。下面以2空格缩进转4空格缩进为例进行说明。', 'https://qncdn.wbjiang.cn/vscode.jpg', 0, 0); -INSERT INTO `article` VALUES (168, 'vue全家桶版本升级排错', '# 背景\n\n如果你使用了`element-ui`的`el-tabs`组件,并且想要单独升级`element-ui`至`2.10.0`,你会发现,使用了`el-tabs`组件的页面只要打开就卡死。原因是`element-ui~2.10.0`采用了不兼容`vue~2.5.10`的写法。于是我尝试系统性升级`vue`全家桶,这也是为系统赋予更多能力做准备。结果遇到一些报错,这里记录一下。\n\n# 升级过程\n\n## 当前版本\n\n`vue: 2.5.10`\n\n`vue-loader: 13.5.0`\n\n`vue-router: 3.0.1`\n\n`vuex: 3.0.1`\n\n`axios: 0.17.1`\n\n`element-ui: 2.2.2`\n\n## 目标版本\n\n`vue: 2.6.10`\n\n`vue-loader: 15.7.0`\n\n`vue-router: 3.0.3`\n\n`vuex: 3.1.1`\n\n`axios: 0.18.1`\n\n`element-ui: 2.10.0`\n\n## 报错1(包版本不匹配)\n\n修改`package.json`中的依赖包版本号之后,`npm install`一波后就报错了。\n\n```javascript\nVue packages version mismatch:\n\n- vue@2.6.10\n- vue-template-compiler@2.5.10\n\nThis may cause things to work incorrectly. Make sure to use the same version for both.\nIf you are using vue-loader@>=10.0, simply update vue-template-compiler.\nIf you are using vue-loader@<10.0 or vueify, re-installing vue-loader/vueify should bump vue-template-compiler to the latest.\n\n\n @ ./src/router/modules/test/index.js 22:23-67\n @ ./src/router/common.js\n @ ./src/router/index.js\n @ ./src/main.js\n @ multi (webpack)-dev-server/client?http://localhost:9532 webpack/hot/dev-server babel-polyfill ./src/main.js\n```\n\n**分析:**`vue`和`vue-template-compiler`两个包的版本不匹配,需要升级`vue-template-compile`。github搜索这个包搜不到,最后在[npm包官网](https://www.npmjs.com/package/vue-template-compiler)找到了。\n\n**解决方案:**升级`vue-template-compile: 2.6.10`\n\n## 报错2(vue-loader)\n\n```javascript\n|\n|
\n| \n| \n\n @ ./src/views/backend/enterprise/holiday/add-public-holiday.vue 1:0-97 30:4-35:6 30:81-35:5\n @ ./src/views lazy ^\\.\\/.*$\n @ ./src/authority/generate-routes.js\n @ ./src/store/modules/user.js\n @ ./src/store/index.js\n @ ./src/main.js\n @ multi (webpack)-dev-server/client?http://localhost:9532 webpack/hot/dev-server babel-polyfill ./src/main.js\n\n error in ./src/views/backend/enterprise/holiday/add-special-holiday.vue?vue&type=template&id=09f84cb0&\n\nModule parse failed: Unexpected token (2:0)\nYou may need an appropriate loader to handle this file type.\n```\n\n**分析:**经观察,发现可能是不识别`vue`文件或其中某部分,于是从`vue-loader`入手,也在网上查阅了一些资料,需要在`webpack`的`plugins`中加入`vue-loader/lib/plugin`。\n\n**解决方案:**\n\n```javascript\nconst VueLoaderPlugin = require(\'vue-loader/lib/plugin\')\n\nplugins: [\n new VueLoaderPlugin(),\n // 其他插件\n ...\n]\n```\n\n## 报错3(postcss-loader)\n\n```javascript\n(Emitted value instead of an instance of Error)\n\n ⚠️ PostCSS Loader\n\nPrevious source map found, but options.sourceMap isn\'t set.\nIn this case the loader will discard the source map entirely for performance reasons.\nSee https://github.com/postcss/postcss-loader#sourcemap for more information.\n\n\n\n @ ./node_modules/vue-style-loader!./node_modules/css-loader?{\"sourceMap\":false}!./node_modules/vue-loader/lib/loaders/stylePostLoader.js!./node_modules/postcss-loader/lib?{\"sourceMap\":false}!./node_modules/vue-loader/lib?{\"loaders\":{\"css\":[\"vue-style-loader\",{\"loader\":\"css-loader\",\"options\":{\"sourceMap\":false}}],\"postcss\":[\"vue-style-loader\",{\"loader\":\"css-loader\",\"options\":{\"sourceMap\":false}}],\"less\":[\"vue-style-loader\",{\"loader\":\"css-loader\",\"options\":{\"sourceMap\":false}},{\"loader\":\"less-loader\",\"options\":{\"sourceMap\":false}}],\"sass\":[\"vue-style-loader\",{\"loader\":\"css-loader\",\"options\":{\"sourceMap\":false}},{\"loader\":\"sass-loader\",\"options\":{\"indentedSyntax\":true,\"sourceMap\":false}}],\"scss\":[\"vue-style-loader\",{\"loader\":\"css-loader\",\"options\":{\"sourceMap\":false}},{\"loader\":\"sass-loader\",\"options\":{\"sourceMap\":false}}],\"stylus\":[\"vue-style-loader\",{\"loader\":\"css-loader\",\"options\":{\"sourceMap\":false}},{\"loader\":\"stylus-loader\",\"options\":{\"sourceMap\":false}}],\"styl\":[\"vue-style-loader\",{\"loader\":\"css-loader\",\"options\":{\"sourceMap\":false}},{\"loader\":\"stylus-loader\",\"options\":{\"sourceMap\":false}}]},\"cssSourceMap\":false,\"cacheBusting\":true,\"transformToRequire\":{\"video\":[\"src\",\"poster\"],\"source\":\"src\",\"img\":\"src\",\"image\":\"xlink:href\"}}!./src/views/iot-supervise/truck/truck-carousel.vue?vue&type=style&index=0&lang=css& 4:14-1577 14:3-18:5 15:22-1585\n @ ./src/views/iot-supervise/truck/truck-carousel.vue?vue&type=style&index=0&lang=css&\n @ ./src/views/iot-supervise/truck/truck-carousel.vue\n @ ./src/views lazy ^\\.\\/.*$\n @ ./src/authority/generate-routes.js\n @ ./src/store/modules/user.js\n @ ./src/store/index.js\n @ ./src/main.js\n @ multi (webpack)-dev-server/client?http://localhost:9532 webpack/hot/dev-server babel-polyfill ./src/main.js\n```\n\n**分析:**这里面的错误是关于`postcss-loader`的,只要将`config/index.js`中`dev.cssSourceMap`设置为`true`即可。\n\n## 警告1(svg-sprite-loader)\n\n升级过程中还遇到了一个警告,虽然不影响功能,但是看着还是很难受的。\n\n```javascript\nin ./src/icons/svg/workList.svg\n\nsvg-sprite-loader exception. 28 rules applies to D:\\coollu\\projects\\coollu-v3\\source-code\\v1.0.1\\update-elementui-test\\src\\icons\\svg\\workList.svg\n\n @ ./src/icons/svg \\.svg$\n @ ./src/icons/index.js\n @ ./src/main.js\n @ multi (webpack)-dev-server/client?http://localhost:9532 webpack/hot/dev-server babel-polyfill ./src/main.js\n```\n\n搜索关键词后,发现网上并没有此类答案。因此我考虑是版本问题,于是升级`svg-sprite-loader`至最新版本`4.1.6`,解决了这个警告问题。\n\n------\n\n## 总结\n\n至此升级过程就完成了!顺便一提,系统性升级必须要经过全面测试,否则你难以保证完全向下兼容哦!', '2019-06-28 14:37:49', '2024-11-13 16:30:58', 1, 336, 0, '记录一下升级vue全家桶版本的过程', 'https://qncdn.wbjiang.cn/vue_375x300.png', 0, 0); -INSERT INTO `article` VALUES (169, '一张图带你了解webpack的require.context', '很多人应该像我一样,对于`webpack`的`require.context`都是一知半解吧。网上很多关于`require.context`的使用案例,但是我没找到可以帮助我理解这个知识点的,于是也决定自己来探索一下,下面以网上流行的`svg`图标方案为例说明。对了,本文的重点是`require.context`,并不会去解释`svg symbol`方案`svg-sprite-loader`。\n\n# 关键代码\n\n![关键代码](http://qncdn.wbjiang.cn/require.context关键代码.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d3d3LndiamlhbmcuY24=/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n`src/icons/index.js`\n\n```javascript\nconst context = require.context(\"./svg\", true, /\\.svg$/)\n\ncontext.keys().map(context)\n```\n\n`main.js`\n\n```javascript\nimport \'@/icons\'\n```\n\n`webpack.base.config.js`\n\n```javascript\n{\n test: /\\.svg$/,\n loader: \"svg-sprite-loader\",\n include: [resolve(\"src/icons\")],\n options: {\n symbolId: \"icon-[name]\"\n }\n},\n{\n test: /\\.(png|jpe?g|gif|svg)(\\?.*)?$/,\n loader: \"url-loader\",\n exclude: [resolve(\"src/icons\")],\n options: {\n limit: 10000,\n name: utils.assetsPath(\"img/[name].[hash:7].[ext]\")\n }\n},\n```\n\n# why?\n\n![](http://qncdn.wbjiang.cn/nickyang.jpg?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d3d3LndiamlhbmcuY24=/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n很多人跟我一样,一开始只想说,为什么这样就可以,why???\n\n要知道是什么,就上打印大法。\n\n```javascript\nconst context = require.context(\"./svg\", true, /\\.svg$/)\n// 看看你是何方神圣\nconsole.log(context)\n\ncontext.keys().map(context)\n```\n\n下面就真的以一张图进行解释,有问题的欢迎留言交流呀!\n\n![一张图说明](http://qncdn.wbjiang.cn/require.context%E5%88%86%E6%9E%90.png)', '2019-07-10 14:22:34', '2024-11-12 11:59:37', 1, 1363, 0, '很多人应该像我一样,对于webpack的require.context都是一知半解吧。网上很多关于require.context的使用案例,但是我没找到可以帮助我理解这个知识点的,于是也决定自己来探索一下,下面以网上流行的svg图标方案为例说明。对了,本文的重点是require.context,并不会去解释svg symbol方案svg-sprite-loader。', 'https://qncdn.wbjiang.cn/riven_375x300.png', 0, 0); -INSERT INTO `article` VALUES (170, 'vue项目中引入iconfont', '# 背景\n\n对于前端而言,图标的发展可谓日新月异。从`img`标签,到雪碧图,再到字体图标,`svg`,甚至`svg`也有了类似于雪碧图的方案`svg-sprite-loader`。雪碧图没有什么好讲的了,只是简单地利用了`background-position`来做图标定位。今天咱们先聊聊怎么使用字体图标和`svg`图标。其实字体图标也不陌生了,`bootstrap`,`font-awesome`,`element-ui`等`UI`库都基本标配了字体图标。\n\n# 简单说下原理\n\n`unicode`预留了`E000-F8FF`范围作为私有保留区域,这个区间的`unicode`码非常适合做字体图标,前端根据`unicode`码就能显示对应的图标。\n\n# vue项目引入iconfont\n\n## 1. 在iconfont新建项目\n\n![iconfont新建项目](http://qncdn.wbjiang.cn/iconfont%E6%96%B0%E5%BB%BA%E9%A1%B9%E7%9B%AE.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d3d3LndiamlhbmcuY24=/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n注:这里修正一下,前缀应该是`test-icon-`。\n\n## 2. 添加图标至项目\n\n![添加图标至项目](http://qncdn.wbjiang.cn/%E6%B7%BB%E5%8A%A0%E5%9B%BE%E6%A0%87%E8%87%B3%E9%A1%B9%E7%9B%AE.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d3d3LndiamlhbmcuY24=/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n## 3. 使用iconfont\n\n### Unicode方式(不推荐)\n\n#### 在线使用\n\n- `index.scss`中引入在线字体\n\n```\n@font-face {\n font-family: \'iconfont\'; /* project id 1254715 */\n src: url(\'//at.alicdn.com/t/font_1254715_s1khj1whikd.eot\');\n src: url(\'//at.alicdn.com/t/font_1254715_s1khj1whikd.eot?#iefix\') format(\'embedded-opentype\'),\n url(\'//at.alicdn.com/t/font_1254715_s1khj1whikd.woff2\') format(\'woff2\'),\n url(\'//at.alicdn.com/t/font_1254715_s1khj1whikd.woff\') format(\'woff\'),\n url(\'//at.alicdn.com/t/font_1254715_s1khj1whikd.ttf\') format(\'truetype\'),\n url(\'//at.alicdn.com/t/font_1254715_s1khj1whikd.svg#iconfont\') format(\'svg\');\n}\n```\n\n- 页面中使用\n\n 使用时很不友好,使用的是`unicode`码表示,使用图标还必须去`iconfont`项目去查询下`unicode`码。\n\n```\n\n```\n\n效果图如下:\n\n![iconfont效果图](http://qncdn.wbjiang.cn/iconfont%E6%95%88%E6%9E%9C%E5%9B%BE.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d3d3LndiamlhbmcuY24=/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n#### 本地使用\n\n有时候网络不是那么给力的,或者是内网环境,那么就不要考虑用在线引用的方式了。\n\n1. 本地使用需要先将字体库下载并放到项目中。\n\n![iconfont项目下载](http://qncdn.wbjiang.cn/unicode%E4%B8%8B%E8%BD%BD%E8%87%B3%E6%9C%AC%E5%9C%B0.png)\n\n2. 在全局样式文件中定义如下代码\n\n ```\n @font-face {\n font-family: \"iconfont\";\n src: url(\'../fonts/iconfont.eot\'); /* IE9*/\n src: url(\'../fonts/iconfont.eot#iefix\') format(\'embedded-opentype\'), /* IE6-IE8 */\n url(\'../fonts/iconfont.woff\') format(\'woff\'), /* chrome, firefox */\n url(\'../fonts/iconfont.woff2\') format(\'woff2\'), /* chrome, firefox */\n url(\'../fonts/iconfont.ttf\') format(\'truetype\'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/\n url(\'../assets/fonts/iconfont.svg#iconfont\') format(\'svg\'); /* iOS 4.1- */\n }\n \n .iconfont {\n font-family: \"iconfont\" !important;\n font-size: 16px;\n font-style: normal;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n }\n ```\n\n![unicode方式本地引用iconfont](http://qncdn.wbjiang.cn/unicode%E6%96%B9%E5%BC%8F%E6%9C%AC%E5%9C%B0%E5%BC%95%E7%94%A8iconfont.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d3d3LndiamlhbmcuY24=/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n3. 使用方式\n\n 与在线引用方式是一样的,都是使用`unicode`码去展示图标。\n\n ```\n \n ```\n\n#### 总结\n\n- 兼容性最好,支持`ie6+`,及所有现代浏览器。\n- 支持按字体的方式去动态调整图标大小,颜色等等。\n- 但是因为是字体,所以不支持多色。只能使用平台里单色的图标,就算项目里有多色图标也会自动去色。\n\n### Font class方式(较友好)\n\n一种更友好的封装,类似于`font-awesome`,我们只要使用`class`,就可以调用图标了。其原理就是利用`before`伪元素来显示图标。\n\n#### 在线使用\n\n超级简单,只要在线生成代码,引用在线的`css`文件即可使用。\n\n![复制在线fontclass的css文件路径](http://qncdn.wbjiang.cn/%E5%9C%A8%E7%BA%BF%E5%AD%97%E4%BD%93%E5%9B%BE%E6%A0%87%E4%BB%A3%E7%A0%81.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d3d3LndiamlhbmcuY24=/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n在`index.html`中引用它。\n\n```html\n\n```\n\n项目中就可以使用字体图标了。\n\n```html\n\n```\n\n#### 本地使用\n\n与`unicode`方式类似,下载代码到本地。因为我是用`scss`管理样式的,需要在下载的代码中提取出关键部分。除了引用字体库,还要将其中的`iconfont.css`中定义的`before`伪元素全部复制到自己的`scss`文件中。\n\n```\n@font-face {\n font-family: \"iconfont\";\n src: url(\'../fonts/iconfont.eot\'); /* IE9*/\n src: url(\'../fonts/iconfont.eot#iefix\') format(\'embedded-opentype\'), /* IE6-IE8 */\n url(\'../fonts/iconfont.woff\') format(\'woff\'), /* chrome, firefox */\n url(\'../fonts/iconfont.woff2\') format(\'woff2\'), /* chrome, firefox */\n url(\'../fonts/iconfont.ttf\') format(\'truetype\'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/\n url(\'../assets/fonts/iconfont.svg#iconfont\') format(\'svg\'); /* iOS 4.1- */\n}\n\n.iconfont {\n font-family: \"iconfont\" !important;\n font-size: 16px;\n font-style: normal;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\n// 列了一部分举例\n.cl-icon-user:before {\n content: \"\\e64b\";\n}\n\n.cl-icon-video:before {\n content: \"\\e66b\";\n}\n\n.cl-icon-pause:before {\n content: \"\\e7bd\";\n}\n\n.cl-icon-orgnazation:before {\n content: \"\\e61b\";\n}\n```\n\n#### 总结\n\n- 兼容性良好,支持`ie8+`,及所有现代浏览器。\n- 相比于`unicode`语意明确,书写更直观。可以很容易分辨这个`icon`是什么。\n- 因为使用`class`来定义图标,所以当要替换图标时,只需要修改`class`里面的`unicode`引用。\n- 不过因为本质上还是使用的字体,所以多色图标还是不支持的。\n\n#### 建议\n\n由于加了新的图标需要重新在`iconfont.cn`重新生成代码,所以这种方式也不算很方便,但是相对于`unicode`还是高级不少。根据我的经验,建议在调试时,不要每次图标更新,就下载到本地更换。应该先使用在线使用的方式,调试完毕确认无误后,再下载到本地使用,这样对于效率提升有很大帮助。\n\n### symbol方式(支持多色图标)\n\n`svg`的`symbol`提供了类似于雪碧图的功能,让`svg`的使用变得更简单,也可以满足做图标系统的需求。可以参考[张大大博客](https://www.zhangxinxu.com/wordpress/2014/07/introduce-svg-sprite-technology/)了解更多关于`svg symbol`的知识。\n\n#### 在线使用\n\n首先在`iconfont`项目中选择`symbol`方式,并在线生成`js`代码\n\n![在线svg symbol代码](http://qncdn.wbjiang.cn/%E5%9C%A8%E7%BA%BFsvg%20symbol%E4%BB%A3%E7%A0%81.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d3d3LndiamlhbmcuY24=/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n然后在`index.html`中引入这个js文件\n\n```html\n\n```\n\n这个`js`的作用是在文档中生成`svg symbol`\n\n![1562638406124](http://qncdn.wbjiang.cn/symbol%E6%A0%87%E7%AD%BE.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d3d3LndiamlhbmcuY24=/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n最后就可以在页面中通过`use`标签使用`svg`图标了。`xlink:href`的值设置为对应的`symbol`的`id`即可。\n\n```html\n\n \n\n```\n\n效果如下:\n\n![多色svg效果图](http://qncdn.wbjiang.cn/%E5%A4%9A%E8%89%B2svg%E6%95%88%E6%9E%9C%E5%9B%BE.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d3d3LndiamlhbmcuY24=/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n多色图标还是酷!\n\n#### 本地使用\n\n本地使用也是一样的道理,主要是依赖这个在线生成的`js`文件,将在线`js`文件的链接在浏览器空标签中打开,就可以得到其内容,然后复制内容,自己命名一个`js`文件,并把它放在本地项目静态资源目录下,引用即可。\n\n![symbol的js文件](http://qncdn.wbjiang.cn/symbol%E7%9A%84js%E6%96%87%E4%BB%B6.png?imageMogr2/auto-orient/blur/1x0/quality/75|watermark/2/text/d3d3LndiamlhbmcuY24=/font/5qW35L2T/fontsize/640/fill/IzQ5NzZEQg==/dissolve/90/gravity/SouthWest/dx/10/dy/10)\n\n```html\n\n```\n\n#### 图标自动管理(必看)\n\n即使使用了`symbol`方式,当设计小姐姐新增图标时,我们还是无法避免重新生成图标代码。那么有没有更优雅的解决方案呢?答案是有的。`svg-sprite-loader` + `require.context`。\n\n`svg-sprite-loader`网上已经有太多文章了。\n\n关于`require.context`,我倒是有一点自己的理解。请查看[一张图带你了解webpack的require.context](http://blog.wbjiang.cn/article/169)。\n\n#### 总结\n\n- 支持多色图标了,不再受单色限制。\n- 支持丰富的`css`属性进行定制。\n- 兼容性较差,支持 `ie9+`,及现代浏览器。\n- 浏览器渲染`svg`的性能一般,还不如`png`。', '2019-07-10 15:16:32', '2024-11-12 12:07:43', 1, 455, 0, '对于前端而言,图标的发展可谓日新月异。从img标签,到雪碧图,再到字体图标,svg,甚至svg也有了类似于雪碧图的方案svg-sprite-loader。今天咱们先聊聊怎么使用字体图标和svg图标。', 'https://qncdn.wbjiang.cn/iconfont.jpg', 0, 0); -INSERT INTO `article` VALUES (171, '一种在地图中处理曲线的通用方法', '本文分享一种可以用于处理曲线的算法,是本人在百度地图开源库基础上改造的,可以用于其他地图场景中处理点数据,只要两个以上的点,就可以得到平滑的曲线。例如小程序,将处理后得到的点赋值给`polyline`的points即可。\n\n```javascript\nfunction getCurveByTwoPoints(obj1, obj2) {\n if (!obj1 || !obj2) {\n return null\n }\n var B1 = function (x) {\n return 1 - 2 * x + x * x\n };\n var B2 = function (x) {\n return 2 * x - 2 * x * x\n };\n var B3 = function (x) {\n return x * x\n };\n curveCoordinates = [];\n var count = 30;\n var isFuture = false;\n var t, h, h2, lat3, lng3, j, t2;\n var LnArray = [];\n var i = 0;\n var inc = 0;\n if (typeof(obj2) == \"undefined\") {\n if (typeof(curveCoordinates) != \"undefined\") {\n curveCoordinates = []\n }\n return\n }\n var lat1 = parseFloat(obj1.lat);\n var lat2 = parseFloat(obj2.lat);\n var lng1 = parseFloat(obj1.lng);\n var lng2 = parseFloat(obj2.lng);\n if (lng2 > lng1) {\n if (parseFloat(lng2 - lng1) > 180) {\n if (lng1 < 0) {\n lng1 = parseFloat(180 + 180 + lng1)\n }\n }\n }\n if (lng1 > lng2) {\n if (parseFloat(lng1 - lng2) > 180) {\n if (lng2 < 0) {\n lng2 = parseFloat(180 + 180 + lng2)\n }\n }\n }\n j = 0;\n t2 = 0;\n if (lat2 == lat1) {\n t = 0;\n h = lng1 - lng2\n } else {\n if (lng2 == lng1) {\n t = Math.PI / 2;\n h = lat1 - lat2\n } else {\n t = Math.atan((lat2 - lat1) / (lng2 - lng1));\n h = (lat2 - lat1) / Math.sin(t)\n }\n }\n if (t2 == 0) {\n t2 = (t + (Math.PI / 5))\n }\n h2 = h / 2;\n lng3 = h2 * Math.cos(t2) + lng1;\n lat3 = h2 * Math.sin(t2) + lat1;\n for (i = 0; i < count + 1; i++) {\n curveCoordinates.push(\n {\n lng: (lng1 * B1(inc) + lng3 * B2(inc)) + lng2 * B3(inc),\n lat: (lat1 * B1(inc) + lat3 * B2(inc) + lat2 * B3(inc))\n }\n );\n inc = inc + (1 / count)\n }\n return curveCoordinates\n}\n\nfunction getCurvePoints(points) {\n var curvePoints = [];\n for (var i = 0; i < points.length - 1; i++) {\n var p = getCurveByTwoPoints(points[i], points[i + 1]);\n if (p && p.length > 0) {\n curvePoints = curvePoints.concat(p)\n }\n }\n return curvePoints\n}\n\nlet trackPoints = [{lng:113.281, lat:29.203}, {lng:113.567, lat:29.301}]\n\nlet convertPoints = getCurvePoints(trackPoints)\n\nconsole.log(convertPoints)\n```\n\n最后再给一个微信小程序应用实例\n\n`map.wxml`\n\n```html\n// 举个小程序应用的例子\n\n```\n\n`map.js`\n\n```javascript\nPage({\n data: {\n markers: [{\n iconPath: \"/resources/marker.png\",\n id: 0,\n latitude: 23.099994,\n longitude: 113.324520,\n width: 50,\n height: 50\n }],\n polyline: [],\n controls: [{\n id: 1,\n iconPath: \'/resources/location.png\',\n position: {\n left: 0,\n top: 300 - 50,\n width: 50,\n height: 50\n },\n clickable: true\n }]\n },\n onLoad() {\n this.setData({\n polyline:[{\n points: this.getCurvePoints([{ lng: 113.3245211, lat: 23.10229 }, { lng: 113.324520, lat: 23.21229 }]),\n color: \"#FF0000DD\",\n width: 2\n }]\n })\n },\n regionchange(e) {\n console.log(e.type)\n },\n markertap(e) {\n console.log(e.markerId)\n },\n controltap(e) {\n console.log(e.controlId)\n },\n getCurveByTwoPoints(obj1, obj2) {\n if (!obj1 || !obj2) {\n return null\n }\n var B1 = function (x) {\n return 1 - 2 * x + x * x\n };\n var B2 = function (x) {\n return 2 * x - 2 * x * x\n };\n var B3 = function (x) {\n return x * x\n };\n var curveCoordinates = [];\n var count = 30;\n var isFuture = false;\n var t, h, h2, lat3, lng3, j, t2;\n var LnArray = [];\n var i = 0;\n var inc = 0;\n if (typeof (obj2) == \"undefined\") {\n if (typeof (curveCoordinates) != \"undefined\") {\n curveCoordinates = []\n }\n return\n }\n var lat1 = parseFloat(obj1.lat);\n var lat2 = parseFloat(obj2.lat);\n var lng1 = parseFloat(obj1.lng);\n var lng2 = parseFloat(obj2.lng);\n if (lng2 > lng1) {\n if (parseFloat(lng2 - lng1) > 180) {\n if (lng1 < 0) {\n lng1 = parseFloat(180 + 180 + lng1)\n }\n }\n }\n if (lng1 > lng2) {\n if (parseFloat(lng1 - lng2) > 180) {\n if (lng2 < 0) {\n lng2 = parseFloat(180 + 180 + lng2)\n }\n }\n }\n j = 0;\n t2 = 0;\n if (lat2 == lat1) {\n t = 0;\n h = lng1 - lng2\n } else {\n if (lng2 == lng1) {\n t = Math.PI / 2;\n h = lat1 - lat2\n } else {\n t = Math.atan((lat2 - lat1) / (lng2 - lng1));\n h = (lat2 - lat1) / Math.sin(t)\n }\n }\n if (t2 == 0) {\n t2 = (t + (Math.PI / 5))\n }\n h2 = h / 2;\n lng3 = h2 * Math.cos(t2) + lng1;\n lat3 = h2 * Math.sin(t2) + lat1;\n for (i = 0; i < count + 1; i++) {\n curveCoordinates.push(\n {\n longitude: (lng1 * B1(inc) + lng3 * B2(inc)) + lng2 * B3(inc),\n latitude: (lat1 * B1(inc) + lat3 * B2(inc) + lat2 * B3(inc))\n }\n );\n inc = inc + (1 / count)\n }\n return curveCoordinates\n },\n getCurvePoints(points) {\n var curvePoints = [];\n for (var i = 0; i < points.length - 1; i++) {\n var p = this.getCurveByTwoPoints(points[i], points[i + 1]);\n if (p && p.length > 0) {\n curvePoints = curvePoints.concat(p)\n }\n }\n return curvePoints\n }\n})\n```\n\n\n\n献上效果图:\n\n![小程序地图曲线](http://qncdn.wbjiang.cn/%E5%B0%8F%E7%A8%8B%E5%BA%8F%E5%A4%9A%E6%9B%B2%E7%BA%BF.png)', '2019-07-12 14:20:54', '2024-11-07 07:03:11', 1, 190, 0, '本文分享一种可以用于处理曲线的算法,是本人在百度地图开源库基础上改造的,可以用于其他地图场景中处理点数据,只要两个以上的点,就可以得到平滑的曲线。例如小程序,将处理后得到的点赋值给polyline的points即可。', 'https://qncdn.wbjiang.cn/map.png', 0, 0); -INSERT INTO `article` VALUES (172, '微信小程序自定义tabBar', '本文分享一下微信小程序自定义`tabBar`的几种实现方式。\n\n# 模拟的tabBar页面(不推荐)\n\n## 使用策略\n\n- `app.json`不配置`tabBar`,用普通`page`来代替`tabbar`页面,暂且称之为模拟的`tabbar`页面。\n\n ![tabbar效果图1](http://qncdn.wbjiang.cn/tabbar1.png)\n\n- 每个模拟的`tabbar`页面都需要引入自定义`tabbar`组件。\n\n1. 自定义的`tabbar`组件写法如下:\n\n`/components/index-tabbar/index.json`\n\n```\n{\n \"component\": true,\n \"usingComponents\": {\n \"van-tabbar\": \"vant-weapp/tabbar/index\",\n \"van-tabbar-item\": \"vant-weapp/tabbar-item/index\"\n }\n}\n```\n\n`/components/index-tabbar/index.wxml`\n\n```xml\n\n \n 首页\n 分类\n 留言\n 我的\n \n\n\n```\n\n`/components/index-tabbar/index.js`\n\n```javascript\nComponent({\n properties: {\n active: {\n type: String,\n value: \'index\'\n },\n },\n methods: {\n onChange(event) {\n wx.redirectTo({\n url: `/pages/${event.detail}/index`,\n })\n }\n }\n})\n```\n\n2. 模拟的`tabbar`页面写法如下:\n\n`/pages/home/index.json`\n\n```\n{\n \"usingComponents\": {\n \"index-tabbar\": \"/components/index-tabbar/index\"\n }\n}\n```\n\n`/pages/home/index.wxml`\n\n```xml\n\n 首页\n \n\n```\n\n- 跳转页面使用`wx.redirectTo`\n\n## 总结\n\n由于`wx.redirectTo`跳转页面是跳转的普通页面,页面渲染也自然会导致自定义的`tabbar`组件重新渲染,所以会出现底部`tabbar`闪一下的视觉体验,很尴尬。\n\n# Component伪装Page(还不错)\n\n## 使用策略\n\n将上述`4`个模拟的`tabBar`页面换成组件写法,然后根据条件进行`wx:if`控制。\n\n1. 改造首页,分类,留言,我的,将其由页面改为组件\n\n`/pages/home/index.json`\n\n```\n{\n \"component\": true\n}\n```\n\n`/pages/home/index.wxml`\n\n```xml\n\n 首页\n\n```\n\n`/pages/home/index.js`\n\n```javascript\nComponent({})\n```\n\n2. `index-tabbar`组件改造\n\n`/components/index-tabbar/index.wxml`\n\n```xml\n\n \n \n {{item.label}}\n \n \n\n\n\n```\n\n`/components/index-tabbar/index.js`\n\n```javascript\nComponent({\n properties: {\n active: {\n type: String,\n value: \'home\'\n },\n panels: {\n type: Array,\n value: []\n },\n },\n methods: {\n onChange(event) {\n this.triggerEvent(\'changeTab\', event.detail)\n }\n }\n})\n\n\n```\n\n2. 入口页`index`改写成如下\n\n`/pages/index/index.json`\n\n```\n{\n \"usingComponents\": {\n \"index-tabbar\": \"/components/index-tabbar/index\",\n \"home-panel\": \"../home/index\",\n \"category-panel\": \"../category/index\",\n \"msgs-panel\": \"../msgs/index\",\n \"my-panel\": \"../my/index\"\n }\n}\n\n```\n\n`/pages/index/index.wxml`\n\n```xml\n\n 首页\n 分类\n 留言\n 我的\n \n\n\n\n```\n\n`/pages/index/index.js`\n\n```javascript\nPage({\n data: {\n activeTab: \'home\',\n panels: [\n { name: \'home\', icon: \'home-o\', label: \'首页\' },\n { name: \'category\', icon: \'label-o\', badge: \'5\', label: \'分类\' },\n { name: \'msgs\', icon: \'comment-o\', badge: \'99+\', label: \'留言\' },\n { name: \'my\', icon: \'user-o\', label: \'我的\' }\n ]\n },\n onTabChange(event) {\n this.setData({\n activeTab: event.detail\n })\n }\n})\n\n\n```\n\n效果如下:\n\n![tabbar效果图2](http://qncdn.wbjiang.cn/tabbar2.png)\n\n## 总结\n\n由于是通过`wx:if`控制组件的创建和销毁,是局部更新,所以不会导致底部`tabbar`的重新渲染,所以底部闪一下的问题就解决了。缺点我想是如果频繁切换`tab`可能导致`wx:if`的渲染开销大吧。\n\n# 官方自定义tabBar\n\n官方也提供了自定义`tabbar`的方法,见[自定义 tabBar](https://developers.weixin.qq.com/miniprogram/dev/framework/ability/custom-tabbar.html)。\n\n> 基础库 2.5.0 开始支持,低版本需做[兼容处理](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)。', '2019-07-16 18:52:33', '2024-11-12 19:48:48', 1, 616, 0, '本文分享一下微信小程序自定义tabBar的几种实现方式。', 'https://qncdn.wbjiang.cn/weapp.jpg', 0, 0); -INSERT INTO `article` VALUES (173, '从部署上做到前后端分离', '# 前言\n\n记得在[让Nodejs支持H5 History模式(connect-history-api-fallback源码分析)](https://blog.wbjiang.cn/article/165)一文中提到了`HTML5`的`History Mode`。然而在最近的使用过程中发现`connect-history-api-fallback`这个包效果并不是那么理想,用一段时间就会报错。而且本身我的博客项目前后端并未完全分离,虽然开发时是独立的工程,但是前端工程打包后还是放在了`express`的静态资源文件夹下进行部署。考虑到这两个痛点,我决定在`nginx`配置中对前后端进行部署分离。\n\n# 前端独立部署\n\n前端工程`npm run build`打包后,不再`copy`到后端工程`public`目录下。而是独立部署在`nginx`的静态资源目录下,我放置的目录是`/usr/nginx/share/html/blog`\n\n![1564989575700](http://qncdn.wbjiang.cn/nginx%E5%8D%9A%E5%AE%A2%E5%89%8D%E7%AB%AF.png)\n\n相关`nginx`配置如下:\n\n```shell\n#博客转发 blog.wbjiang.cn\nserver {\n listen 80;\n server_name blog.wbjiang.cn;\n root /usr/share/nginx/html/blog;\n access_log logs/blog.log;\n error_log logs/blog.error;\n\n #博客静态文件\n location / {\n try_files $uri /index.html;\n }\n}\n```\n\n**小建议:**可以在开发新功能完毕后,就将打包完毕的代码提交到仓库的`release`分支,然后直接在`linux`服务器上对应目录下的`Git`仓库中`git pull`,也算是半自动化部署了(后面也准备研究下全自动化部署)。\n\n# 后端接口转发\n\n`blog.wbjiang.cn/api`前缀的视为接口请求,统一转发到`express`后台服务。配置如下:\n\n```shell\n#api转发\nlocation /api {\n proxy_pass http://blog_pool;\n proxy_http_version 1.1;\n proxy_set_header Host $host;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header Upgrade $http_upgrade;\n proxy_set_header Connection \"upgrade\";\n rewrite ^/api/(.*)$ /$1 break;\n}\n```\n\n负载均衡池配置(虽然只有一个服务,手动“狗头”)\n\n```\nupstream blog_pool{\n server 127.0.0.1:8002;\n}\n```\n\n# 重启服务\n\n`pm2`重启\n\n```shell\npm2 restart blog\n```\n\n`nginx`重启\n\n```shell\nnginx -s reload\n```', '2019-08-05 15:42:42', '2024-07-24 15:32:37', 1, 181, 0, '记得在让Nodejs支持H5 History模式(connect-history-api-fallback源码分析)一文中提到了HTML5的History Mode。然而在最近的使用过程中发现connect-history-api-fallback这个包效果并不是那么理想,用一段时间就会报错。而且本身我的博客项目前后端并未完全分离,虽然开发时是独立的工程,但是前端工程打包后还是放在了express的静态资源文件夹下进行部署。考虑到这两个痛点,我决定在nginx配置中对前后端进行部署分离。', 'https://qncdn.wbjiang.cn/riven2_400x300.png', 0, 0); -INSERT INTO `article` VALUES (180, '在Linux和Windows系统中输出目录结构', '# 前言\n\n一直以来就想在写文章时,能以文本形式(而不是截图)附上项目的目录结构,今天终于知道怎么操作了,在这分享一下。\n\n# Linux\n\n首先说下Linux上输出目录结构的方法。\n\n## yum安装tree\n\n需要支持tree命令,首先是要安装tree包的。\n\n```shell\nyum -y install tree\n```\n\n然后在你的项目目录下执行tree命令即可\n\n![linux目录结构](http://qncdn.wbjiang.cn/linux%E6%A0%91%E7%BB%93%E6%9E%84.png)\n\n还可以输出带颜色的结构\n\n```\ntree -C\n```\n\n![带颜色的](http://qncdn.wbjiang.cn/linux%E7%9B%AE%E5%BD%95%E6%A0%91%E5%B8%A6%E9%A2%9C%E8%89%B2.png)\n\n# Windows\n\n不需要特意安装什么,直接输入命令:\n\n```\ntree /f\n```\n\n![windows tree命令](http://qncdn.wbjiang.cn/windowstree%E5%91%BD%E4%BB%A4.png)\n\n更多参数请参考[Windows Commands / tree](https://docs.microsoft.com/zh-cn/windows-server/administration/windows-commands/tree)\n\n', '2019-08-15 14:27:39', '2024-06-18 17:47:12', 1, 78, 0, '一直以来就想在写文章时,能以文本形式(而不是截图)附上项目的目录结构,今天终于知道怎么操作了,在这分享一下', 'https://qncdn.wbjiang.cn/book3_600x400.png', 0, 0); -INSERT INTO `article` VALUES (181, 'sea.js的同步魔法', '前些时间也是想写点关于`CMD`模块规范的文字,以便帮助自己理解。今天看到一篇知乎回答,算是给了我一点启发。\n\n# 同步写法却不阻塞?\n\n先上一个`sea.js`很经典的模块写法:\n\n```javascript\n// 定义一个模块\ndefine(function(require, exports, module) {\n // 加载jquery模块\n var $ = require(\'jquery\');\n // 直接使用模块里的方法\n $(\'#header\').hide();\n});\n```\n\n按道理加载模块,就是需要等`jquery.js`加载完毕才能使用,应该是一个异步的过程,为什么可以写成同步的形式呢?这是用了什么黑科技?\n\n原来作者玉伯大佬用了一个小魔法来“欺骗”我们。而[卢勃](https://www.zhihu.com/people/lubobill1990/activities)大神在[知乎](https://www.zhihu.com/question/20342350/answer/14828786)给了一个很精彩的解释,这里直接分享下:\n\n![sea.js同步写法解释](http://qncdn.wbjiang.cn/sea.js%E5%90%8C%E6%AD%A5%E5%86%99%E6%B3%95%E8%A7%A3%E9%87%8A.png)\n\n\n也就是说,`require.js`和`sea.js`都是在执行模块前预加载了依赖的模块,并没有比`require.js`显得更“懒加载”,只是所依赖模块的代码执行时机不同。`require.js`加载时执行,而`sea.js`是使用时执行。\n\n其实从代码的写法也看得出来,`require.js`的依赖模块在加载后便有了执行结果,并作为回调函数的实参传入。\n\n- `reuiqre.js`写法:\n\n```javascript\n// 加载完jquery.js后,得到的执行结果$作为参数传入了回调函数\ndefine([\'jquery\'], function($) {\n $(\'#header\').hide();\n});\n```\n\n- `sea.js`写法:\n\n```javascript\n// 预加载了jquery.js\ndefine(function(require, exports, module) {\n // 执行jquery.js模块,并得到结果赋值给$\n var $ = require(\'jquery\');\n // 调用jquery.js模块提供的方法\n $(\'#header\').hide();\n});\n```\n\n从这一点上来看,两者在性能上并没有太多差异。因为最影响页面渲染速度的当然是资源的加载速度,既然都是预加载,那么加载模块资源的耗时是一样的(网络情况相同时)。\n\n而模块代码的执行时机并没有那么影响性能(除非你的模块太大),现在的`js`引擎如`V8`引擎足够强,没什么压力。\n\n# 懒加载是否存在?\n\n懒加载是存在的。我刚才说的`sea.js`并没有比`require.js`更显得“懒加载”是指模块加载的时机上两者是一致的,都是预先加载,而不是说不能懒加载。\n\n比如说,有一个模块,页面渲染时,我不需要加载使用,但是在做了某种交互时(比如点了按钮),才需要加载使用,这个时候“懒加载”的作用就体现了。下面以`require.js`举个实例:\n\n```javascript\nrequire.config({\n baseUrl: \'./assets/js/\',\n paths: {\n modulea: \'module-a\',\n moduleb: \'module-b\'\n }\n})\n\nrequire([\"modulea\"], function(modulea) {\n var btnNode = document.querySelector(\'#btn-load\');\n var node1 = document.createElement(\'span\');\n node1.innerText = \'模块A已经加载!\'\n btnNode.insertAdjacentElement(\'beforebegin\', node1)\n btnNode.addEventListener(\'click\', function() {\n require([\"moduleb\"], function(moduleb) {\n var node2 = document.createElement(\'span\');\n node2.innerText = \'模块B已经加载!\'\n btnNode.insertAdjacentElement(\'afterend\', node2)\n });\n })\n});\n```\n\n- 页面渲染时只加载模块A\n\n ![页面渲染时只加载模块A](http://qncdn.wbjiang.cn/%E5%8F%AA%E5%8A%A0%E8%BD%BD%E4%BA%86A.png)\n\n- 点击按钮后加载模块B\n\n ![点击按钮后加载模块B](http://qncdn.wbjiang.cn/%E5%8A%A8%E6%80%81%E5%8A%A0%E8%BD%BD%E4%BA%86B.png)\n\n# 总结\n\n虽然`AMD`和`CMD`两种思想有一些差异,但都不失为一种优秀的模块化方案,为大佬们打call!', '2019-08-24 13:52:04', '2024-08-02 13:57:10', 1, 34, 0, '前些时间也是想写点关于CMD模块规范的文字,以便帮助自己理解。今天看到一篇知乎回答,算是给了我一点启发。', 'https://qncdn.wbjiang.cn/mangseng_600x400.png', 0, 0); -INSERT INTO `article` VALUES (182, 'Chrome远程调试手机端UC浏览器', '今天在手机`UC`上发现我的一个网页打不开,而在`PC`上是正常的,因此需要通过`Chrome`远程调试手机端`UC`浏览器查下问题,折腾了老久才弄好。\n\n# 获取 Google USB 驱动程序\n\n1. 首先将手机通过`USB`接口与`PC`连接\n\n2. 接着要确认手机`USB`驱动程序是不是正常,可以在设备管理器中查看,如果设备左侧没有黄色感叹号,则说明正常。\n\n ![查看设备驱动状态](http://qncdn.wbjiang.cn/%E6%9F%A5%E7%9C%8B%E8%AE%BE%E5%A4%87%E9%A9%B1%E5%8A%A8%E7%8A%B6%E6%80%81.png)\n\n \n\n 如果不正常就需要手动安装了,给个链接:[获取 Google USB 驱动程序](https://developer.android.com/studio/run/win-usb.html?hl=zh-cn)\n\n\n\n# 开发者选项\n\n1. 打开手机的开发者选项\n\n 进入手机中 **我的设备 -> 全部参数**,连续7次点击版本号,以`Redmi K20 Pro`为例,是点击`MIUI版本`七次。\n\n ![连续点击版本号七次](http://qncdn.wbjiang.cn/%E7%82%B9%E5%87%BB%E7%89%88%E6%9C%AC%E5%8F%B7%E4%B8%83%E6%AC%A1.png)\n\n 接着就可以进入 **更多设置 -> 开发者选项** 中开启开发者选项了。\n\n ![开启开发者选项](http://qncdn.wbjiang.cn/%E5%BC%80%E5%90%AF%E5%BC%80%E5%8F%91%E8%80%85%E9%80%89%E9%A1%B9.png)\n\n2. 允许USB调试\n\n 接着滚动到下方 **调试** 处,允许 **USB调试**。\n\n ![允许USB调试](http://qncdn.wbjiang.cn/%E5%85%81%E8%AE%B8USB%E8%B0%83%E8%AF%95.png)\n\n# 开始调试\n\n1. 打开`chrome://inspect/`,开启`Discover USB devices`\n\n ![chrome inspect](http://qncdn.wbjiang.cn/chrome%20inspect%E7%95%8C%E9%9D%A2.png)\n\n2. 使用`UC`开发版访问需要调试的网页\n\n3. 点击`chrome`上对应网页的`inspect`打开调试界面。\n\n ![调试Elements](http://qncdn.wbjiang.cn/%E8%B0%83%E8%AF%95uc%E6%89%8B%E6%9C%BA%E7%AB%AFElements.png)\n\n4. 也可以断点调试,基本上与`chrome PC`端调试无异。\n\n ![断点调试](http://qncdn.wbjiang.cn/%E8%BF%9C%E7%A8%8B%E6%96%AD%E7%82%B9%E8%B0%83%E8%AF%95.png)\n\n\n\n# 建议\n\n其实最重要的应该是`fanqiang`吧,如果遇到无法调试,报`HTTP 404`这类的问题,基本上是要`fanqiang`了。^_^', '2019-08-28 16:49:06', '2024-07-24 19:24:13', 1, 27, 0, '今天在手机UC上发现我的一个网页打不开,而在PC上是正常的,因此需要通过Chrome远程调试手机端UC浏览器查下问题,折腾了老久才弄好。', 'https://qncdn.wbjiang.cn/uc-mini.svg', 0, 0); -INSERT INTO `article` VALUES (183, '可能是最详细的UMD模块入门指南', '# 学习UMD\n\n## 介绍\n\n这个[仓库](https://github.com/cumt-robin/umd-learning)记录了一些关于`javascript UMD`模块规范的`demo`,对我学习`UMD`规范有了很大帮助,希望也能帮助到你。\n\n## 回顾\n\n之前也写了几篇关于`javascript`模块的博客,链接如下:\n\n- [回头再看JS模块化编程](https://blog.wbjiang.cn/article/152)\n- [回头再看JS模块化编程之AMD](https://blog.wbjiang.cn/article/153)\n- [sea.js的同步魔法](https://blog.wbjiang.cn/article/181)\n\n近几天准备总结一下`javascript`模块的知识点,所以建了这个[Git仓库](https://github.com/cumt-robin/umd-learning),如果能帮助到您,麻烦点个`star`哦,非常感谢!\n\n这篇博客主要说下自己关于`UMD`的一点认知和思考,从实现一个简单的`UMD`模块,再到实现一个有依赖关系的`UMD`模块,整个过程加深了我对`UMD`模块的理解。\n\n## 什么是UMD\n\n所谓`UMD (Universal Module Definition)`,就是一种`javascript`通用模块定义规范,让你的模块能在`javascript`所有运行环境中发挥作用。\n\n## 简单UMD模块的实现\n\n实现一个`UMD`模块,就要考虑现有的主流`javascript`模块规范了,如`CommonJS`, `AMD`, `CMD`等。那么如何才能同时满足这几种规范呢?\n\n首先要想到,模块最终是要导出一个对象,函数,或者变量。\n\n而不同的模块规范,关于模块导出这部分的定义是完全不一样的。\n\n因此,我们需要一种过渡机制。\n\n首先,我们需要一个`factory`,也就是工厂函数,它只负责返回你需要导出的内容(对象,函数,变量等)。\n\n我们从导出一个简单的对象开始。\n\n```javascript\nfunction factory() {\n return {\n name: \'我是一个umd模块\'\n }\n}\n```\n\n### 全局对象挂载属性\n\n假设不考虑`CommonJS`, `AMD`, `CMD`,仅仅将这个模块作为全局对象的一个属性应该怎么写呢?\n\n```javascript\n(function(root, factory) {\n console.log(\'没有模块环境,直接挂载在全局对象上\')\n root.umdModule = factory();\n}(this, function() {\n return {\n name: \'我是一个umd模块\'\n }\n}))\n```\n\n我们把`factory`写成一个匿名函数,利用`IIFE`(立即执行函数)去执行工厂函数,返回的对象赋值给`root.umdModule`,这里的`root`就是指向全局对象`this`,其值可能是`window`或者`global`,视运行环境而定。\n\n打开[效果页面链接](https://cumt-robin.github.io/umd-learning/umd-simple-used-by-global.html)(要看源码的话,点开[Git仓库](https://github.com/cumt-robin/umd-learning)),观察`Network`的文件加载顺序,可以看到,原则就是依赖先行。\n\n![global调用UMD模块](http://qncdn.wbjiang.cn/global%E8%B0%83%E7%94%A8UMD%E6%A8%A1%E5%9D%97.png)\n\n### 再进一步,兼容AMD规范\n\n要兼容`AMD`也简单,判断一下环境,是否满足`AMD`规范。如果满足,则使用`require.js`提供的`define`函数定义模块。\n\n```javascript\n(function(root, factory) {\n if (typeof define === \'function\' && define.amd) {\n // 如果环境中有define函数,并且define函数具备amd属性,则可以判断当前环境满足AMD规范\n console.log(\'是AMD模块规范,如require.js\')\n define(factory)\n } else {\n console.log(\'没有模块环境,直接挂载在全局对象上\')\n root.umdModule = factory();\n }\n}(this, function() {\n return {\n name: \'我是一个umd模块\'\n }\n}))\n```\n\n打开[效果页面链接](https://cumt-robin.github.io/umd-learning/umd-simple-used-by-requirejs.html),可以看到,原则是调用者先加载,所依赖的模块后加载。\n\n![requirejs调用UMD模块](http://qncdn.wbjiang.cn/requirejs%E8%B0%83%E7%94%A8UMD%E6%A8%A1%E5%9D%97.png)\n\n### 起飞,直接UMD\n\n同理,接着判断当前环境是否满足`CommonJS`或`CMD`规范,分别使用相应的模块定义方法进行模块定义。\n\n```javascript\n(function(root, factory) {\n if (typeof module === \'object\' && typeof module.exports === \'object\') {\n console.log(\'是commonjs模块规范,nodejs环境\')\n module.exports = factory();\n } else if (typeof define === \'function\' && define.amd) {\n console.log(\'是AMD模块规范,如require.js\')\n define(factory)\n } else if (typeof define === \'function\' && define.cmd) {\n console.log(\'是CMD模块规范,如sea.js\')\n define(function(require, exports, module) {\n module.exports = factory()\n })\n } else {\n console.log(\'没有模块环境,直接挂载在全局对象上\')\n root.umdModule = factory();\n }\n}(this, function() {\n return {\n name: \'我是一个umd模块\'\n }\n}))\n```\n\n最终,使用`require.js`, `sea.js`, `nodejs`或全局对象挂载属性等方式都能完美地使用`umd-module.js`这个模块,实现了大一统。\n\n给个`sea.js`调用`UMD`的效果页面链接,[sea.js调用UMD模块](https://cumt-robin.github.io/umd-learning/umd-simple-used-by-seajs.html)\n\n而`nodejs`调用`UMD`模块需要执行`node`命令,\n\n```javascript\nnode umd-simple-used-by-nodejs\n```\n\n效果如下:\n\n![nodejs调用umd模块](http://qncdn.wbjiang.cn/nodejs%E8%B0%83%E7%94%A8UMD%E6%A8%A1%E5%9D%97.png)\n\n## 有依赖关系的UMD模块\n\n当然,我们不能止步于此,模块会被调用,当然也会调用其他模块。因此我们还需要实现一个有依赖关系的`UMD`模块,来验证`UMD`规范的可行性。\n\n### 全局对象挂载属性\n\n这个简单,在`html`中你的模块前引入所依赖的模块即可。`umd-module-depended`和`umd-module`都是`UMD`模块,后者依赖前者。\n\n[](https://github.com/cumt-robin/umd-learning)\n\n```html\n\n\n \n Test UMD\n \n \n \n \n \n \n

测试UMD模块

\n

\n

\n

\n \n\n```\n\n点开[效果页面链接](https://cumt-robin.github.io/umd-learning/umd-dep-used-by-global.html),看得更清楚明白!\n\n### 兼容AMD规范\n\n我们先在入口文件`umd-main-requirejs.js`中,定义好模块路径,方便调用。\n\n```javascript\nrequire.config({\n baseUrl: \"./assets/js/umd-dep/\",\n paths: {\n umd: \"umd-module\",\n depModule: \"umd-module-depended\"\n }\n});\n```\n\n被依赖的模块`umd-module-depended`,只需要简单实现`UMD`规范即可。\n\n而调用者`umd-module`,则需要做一些处理。按照`require.js`的规范来即可, `define`时,指定依赖的模块`depModule`,而匿名工厂函数需要在参数上接收依赖的模块`depModule`。\n\n```javascript\n(function(root, factory) {\n if (typeof define === \'function\' && define.amd) {\n console.log(\'是AMD模块规范,如require.js\')\n define([\'depModule\'], factory)\n } else {\n console.log(\'没有模块环境,直接挂载在全局对象上\')\n root.umdModule = factory(root.depModule);\n }\n}(this, function(depModule) {\n console.log(\'我调用了依赖模块\', depModule)\n // ...省略了一些代码,去代码仓库看吧\n return {\n name: \'我自己是一个umd模块\'\n }\n}))\n```\n\n打开[效果页面链接](https://cumt-robin.github.io/umd-learning/umd-dep-used-by-requirejs.html),看得更清楚明白!\n\n### UMD依赖写法\n\n同理,各种规范要求你怎么写模块依赖,你就怎么写就行。\n\n```javascript\n(function(root, factory) {\n if (typeof module === \'object\' && typeof module.exports === \'object\') {\n console.log(\'是commonjs模块规范,nodejs环境\')\n var depModule = require(\'./umd-module-depended\')\n module.exports = factory(depModule);\n } else if (typeof define === \'function\' && define.amd) {\n console.log(\'是AMD模块规范,如require.js\')\n define([\'depModule\'], factory)\n } else if (typeof define === \'function\' && define.cmd) {\n console.log(\'是CMD模块规范,如sea.js\')\n define(function(require, exports, module) {\n var depModule = require(\'depModule\')\n module.exports = factory(depModule)\n })\n } else {\n console.log(\'没有模块环境,直接挂载在全局对象上\')\n root.umdModule = factory(root.depModule);\n }\n}(this, function(depModule) {\n console.log(\'我调用了依赖模块\', depModule)\n // ...省略了一些代码,去代码仓库看吧\n return {\n name: \'我自己是一个umd模块\'\n }\n}))\n\n```\n\n给个`sea.js`调用的[示例链接](https://cumt-robin.github.io/umd-learning/umd-dep-used-by-seajs.html)。\n\n而`nodejs`调用也是通过命令行测试,\n\n```shell\nnode umd-dep-used-by-nodejs\n```\n\n效果如下:\n\n![nodejs调用有依赖的UMD模块](http://qncdn.wbjiang.cn/nodejs%E8%B0%83%E7%94%A8%E6%9C%89%E4%BE%9D%E8%B5%96%E7%9A%84UMD%E6%A8%A1%E5%9D%97.png)\n\n# 总结\n\n以上实现了简单的`UMD`模块,也验证了`UMD`模块间存在依赖关系时的可行性。虽然本文是以简单对象导出为例,但足以作为我们深入`UMD`规范的起点,加油!\n\n最后厚着脸皮求个`star`,[点亮我吧](https://github.com/cumt-robin/umd-learning)\n\n------\n\n扫一扫下方小程序二维码或搜索`Tusi博客`,即刻阅读最新文章!\n\n![Tusi博客](http://qncdn.wbjiang.cn/Tusi%E5%8D%9A%E5%AE%A2.jpg)', '2019-08-29 13:34:46', '2024-06-18 17:50:17', 1, 43, 0, '记录了一些关于javascript UMD模块规范的demo,对我学习UMD规范有了很大帮助,希望也能帮助到你。', 'https://qncdn.wbjiang.cn/hug_600x400.png', 0, 0); -INSERT INTO `article` VALUES (184, '拥抱webpack4,有效缩减构建时间57%+', '# 背景\n\n最近有感觉到,随着系统模块数量的增加,`wepack`编译打包的速度越来越慢,于是我想给项目做一下优化升级,也借此机会系统地学习一下`webpack4`。\n\n# 升级过程\n\n## 当前版本\n\n```\n\"dependencies\": {\n \"@fullcalendar/core\": \"^4.2.0\",\n \"@fullcalendar/daygrid\": \"^4.2.0\",\n \"@fullcalendar/interaction\": \"^4.2.0\",\n \"@fullcalendar/vue\": \"^4.2.2\",\n \"axios\": \"0.18.1\",\n \"babel-polyfill\": \"6.26.0\",\n \"echarts\": \"4.0.4\",\n \"element-ui\": \"2.10.0\",\n \"jquery\": \"3.3.1\",\n \"js-cookie\": \"2.2.0\",\n \"js-md5\": \"0.7.3\",\n \"lodash\": \"4.17.5\",\n \"moment\": \"^2.24.0\",\n \"node-sass\": \"^4.11.0\",\n \"normalize.css\": \"7.0.0\",\n \"nprogress\": \"0.2.0\",\n \"qs\": \"6.5.1\",\n \"vue\": \"2.6.10\",\n \"vue-router\": \"3.0.3\",\n \"vuex\": \"3.1.1\"\n},\n\"devDependencies\": {\n \"autoprefixer\": \"7.2.3\",\n \"babel-core\": \"6.26.0\",\n \"babel-eslint\": \"8.0.3\",\n \"babel-helper-vue-jsx-merge-props\": \"2.0.3\",\n \"babel-loader\": \"7.1.2\",\n \"babel-plugin-syntax-jsx\": \"6.18.0\",\n \"babel-plugin-transform-runtime\": \"6.23.0\",\n \"babel-plugin-transform-vue-jsx\": \"3.7.0\",\n \"babel-preset-env\": \"1.6.1\",\n \"babel-preset-stage-2\": \"6.24.1\",\n \"chalk\": \"2.3.0\",\n \"copy-webpack-plugin\": \"4.2.3\",\n \"css-loader\": \"0.28.7\",\n \"eslint\": \"4.13.1\",\n \"eslint-friendly-formatter\": \"3.0.0\",\n \"eslint-loader\": \"1.9.0\",\n \"eslint-plugin-html\": \"4.0.1\",\n \"eventsource-polyfill\": \"0.9.6\",\n \"extract-text-webpack-plugin\": \"3.0.2\",\n \"file-loader\": \"1.1.5\",\n \"friendly-errors-webpack-plugin\": \"1.6.1\",\n \"html-webpack-plugin\": \"2.30.1\",\n \"node-notifier\": \"5.1.2\",\n \"optimize-css-assets-webpack-plugin\": \"3.2.0\",\n \"ora\": \"1.3.0\",\n \"portfinder\": \"1.0.13\",\n \"postcss-import\": \"11.0.0\",\n \"postcss-loader\": \"2.0.9\",\n \"rimraf\": \"2.6.2\",\n \"sass-loader\": \"6.0.6\",\n \"semver\": \"5.4.1\",\n \"shelljs\": \"0.7.8\",\n \"svg-sprite-loader\": \"4.1.6\",\n \"uglifyjs-webpack-plugin\": \"1.1.3\",\n \"url-loader\": \"0.6.2\",\n \"vue-loader\": \"15.7.0\",\n \"vue-style-loader\": \"4.1.2\",\n \"vue-template-compiler\": \"2.6.10\",\n \"webpack\": \"3.10.0\",\n \"webpack-bundle-analyzer\": \"2.9.1\",\n \"webpack-dev-server\": \"2.9.7\",\n \"webpack-merge\": \"4.1.1\"\n}\n```\n\n## 目标版本\n\n```\n\"dependencies\": {\n \"@fullcalendar/core\": \"^4.2.0\",\n \"@fullcalendar/daygrid\": \"^4.2.0\",\n \"@fullcalendar/interaction\": \"^4.2.0\",\n \"@fullcalendar/vue\": \"^4.2.2\",\n \"axios\": \"0.18.1\",\n \"babel-polyfill\": \"6.26.0\",\n \"echarts\": \"4.0.4\",\n \"element-ui\": \"2.10.0\",\n \"jquery\": \"3.3.1\",\n \"js-cookie\": \"2.2.0\",\n \"js-md5\": \"0.7.3\",\n \"lodash\": \"4.17.5\",\n \"moment\": \"^2.24.0\",\n \"node-sass\": \"^4.11.0\",\n \"normalize.css\": \"7.0.0\",\n \"nprogress\": \"0.2.0\",\n \"qs\": \"6.5.1\",\n \"vue\": \"2.6.10\",\n \"vue-router\": \"3.0.3\",\n \"vuex\": \"3.1.1\"\n},\n\"devDependencies\": {\n \"autoprefixer\": \"9.6.1\",\n \"babel-core\": \"6.26.3\",\n \"babel-eslint\": \"10.0.3\",\n \"babel-helper-vue-jsx-merge-props\": \"2.0.3\",\n \"babel-loader\": \"^7.1.5\",\n \"babel-plugin-syntax-jsx\": \"6.18.0\",\n \"babel-plugin-transform-runtime\": \"6.23.0\",\n \"babel-plugin-transform-vue-jsx\": \"3.7.0\",\n \"babel-preset-env\": \"1.7.0\",\n \"babel-preset-stage-2\": \"6.24.1\",\n \"chalk\": \"2.4.2\",\n \"copy-webpack-plugin\": \"5.0.4\",\n \"css-loader\": \"3.2.0\",\n \"eslint\": \"6.3.0\",\n \"eslint-friendly-formatter\": \"3.0.0\",\n \"eslint-import-resolver-webpack\": \"^0.11.1\",\n \"eslint-loader\": \"3.0.0\",\n \"eslint-plugin-vue\": \"^5.2.3\",\n \"eventsource-polyfill\": \"0.9.6\",\n \"file-loader\": \"4.2.0\",\n \"friendly-errors-webpack-plugin\": \"1.7.0\",\n \"html-webpack-plugin\": \"3.2.0\",\n \"mini-css-extract-plugin\": \"^0.8.0\",\n \"node-notifier\": \"5.1.2\",\n \"optimize-css-assets-webpack-plugin\": \"3.2.0\",\n \"ora\": \"1.3.0\",\n \"portfinder\": \"1.0.13\",\n \"postcss-import\": \"12.0.1\",\n \"postcss-loader\": \"3.0.0\",\n \"rimraf\": \"2.6.2\",\n \"sass-loader\": \"8.0.0\",\n \"semver\": \"5.4.1\",\n \"shelljs\": \"0.7.8\",\n \"svg-sprite-loader\": \"4.1.6\",\n \"uglifyjs-webpack-plugin\": \"2.2.0\",\n \"url-loader\": \"2.1.0\",\n \"vue-loader\": \"15.7.1\",\n \"vue-style-loader\": \"4.1.2\",\n \"vue-template-compiler\": \"2.6.10\",\n \"webpack\": \"4.39.3\",\n \"webpack-bundle-analyzer\": \"3.4.1\",\n \"webpack-cli\": \"^3.3.8\",\n \"webpack-dev-server\": \"3.8.0\",\n \"webpack-merge\": \"4.2.2\"\n}\n```\n\n## 第一步\n\n升级`webpack`到`4.39.3`版本,`npm run dev`遇到了报错......\n\n## npm run dev报错\n\n### webpack-dev-server版本过低\n\n```shell\nError: Cannot find module \'webpack/bin/config-yargs\'\n```\n\n应该是`webpack`与`webpack-dev-server`版本不符,于是升级`webpack-dev-server`到`3.8.0`版本。\n\n### webpack-cli缺失\n\n```shell\nThe CLI moved into a separate package: webpack-cli\nPlease install \'webpack-cli\' in addition to webpack itself to use the CLI\n-> When using npm: npm i -D webpack-cli\n-> When using yarn: yarn add -D webpack-cli\ninternal/modules/cjs/loader.js:584\n throw err;\n ^\n\nError: Cannot find module \'webpack-cli/bin/config-yargs\'\n```\n\n`webpack4`将`webpack-cli`单独分离出来了,因此提示我们安装`webpack-cli`,那就直接安装吧。\n\n### html-webpack-plugin版本问题\n\n```\n10% building 2/2 modules 0 active(node:8596) DeprecationWarning: Tapable.plugin is deprecated. Use new API on `.hooks` instead\n(node:8596) DeprecationWarning: Tapable.apply is deprecated. Call apply on the plugin directly instead\n53% building 363/366 modules 3 active D:\\coollu\\projects\\coollu-v3\\source-code\\develop\\coollu-cloud-web\\node_modules\\core-js\\modules\\_array-reduce.jsD:\\coollu\\projects\\coollu-v3\\source-code\\develop\\coollu-cloud-web\\node_modules\\html-webpack-plugin\\lib\\compiler.js:81\n var outputName = compilation.mainTemplate.applyPluginsWaterfall(\'asset-path\', outputOptions.filename, {\n ^\n\nTypeError: compilation.mainTemplate.applyPluginsWaterfall is not a function\n```\n\n考虑是`html-webpack-plugin`版本问题,升级至`3.2.0`\n\n### extract-text-webpack-plugin?\n\n```\n10% building 2/2 modules 0 active(node:19732) DeprecationWarning: Tapable.plugin is deprecated. Use new API on `.hooks` instead\n```\n\n查到是因为`extract-text-webpack-plugin`不再支持`webpack4.3`,需要改用`mini-css-extract-plugin`。\n\n**ps:** `extract-text-webpack-plugin`是用来抽取依赖的`.css`文件的,防止样式全部打包在`js bundle`里太大。改用了`mini-css-extract-plugin`后,该报错并未消除,考虑要用`compiler`钩子重写一些东西,先在这埋个坑,后面弄明白了再来填坑。\n\n### eslint-loader升版本\n\n```\nModule build failed (from ./node_modules/eslint-loader/index.js):\nTypeError: Cannot read property \'eslint\' of undefined\n at Object.module.exports (D:\\coollu\\projects\\coollu-v3\\source-code\\develop\\coollu-cloud-web\\node_modules\\eslint-loader\\index.js:148:18)\n```\n\n升级`eslint-loader`\n\n### file-loader升版本\n\n```\nModule build failed (from ./node_modules/file-loader/dist/cjs.js):\nTypeError: Cannot read property \'context\' of undefined\n at Object.loader (D:\\coollu\\projects\\coollu-v3\\source-code\\develop\\coollu-cloud-web\\node_modules\\file-loader\\dist\\index.js:34:49)\n```\n\n升级`file-loader`\n\n## npm run build报错\n\n### 改用splitChunks\n\n```\nwebpack.optimize.CommonsChunkPlugin has been removed, please use config.optimization.splitChunks instead.\n```\n\n使用`webpack4`的`optimization.splitChunks`替代`CommonsChunkPlugin`\n\n### vue-loader升版本\n\n```\nERROR in ./src/App.vue?vue&type=style&index=0&id=7c362b6c&lang=scss&scoped=tr (./node_modules/mini-css-extract-plugin/dist/loader.js??ref--10-0!./node_mods/vue-loader/lib??vue-loader-options!./src/App.vue?vue&type=style&index=0&id=62b6c&lang=scss&scoped=true&)\nModule build failed (from ./node_modules/mini-css-extract-plugin/dist/loader.:\nModuleParseError: Module parse failed: Unexpected character \'#\' (14:0)\nFile was processed with these loaders:\n * ./node_modules/vue-loader/lib/index.js\nYou may need an additional loader to handle the result of these loaders.\n```\n\n考虑是`vue-loader`版本问题,先升级`vue-loader@15.7.1`\n\n### babel-loader降版本\n\n```\nERROR in ./src/main.js\nModule build failed (from ./node_modules/babel-loader/lib/index.js):\nError: Cannot find module \'@babel/core\'\n babel-loader@8 requires Babel 7.x (the package \'@babel/core\'). If you\'d like to use Babel 6.x (\'babel-core\'), you should install \'babel-loader@7\'.\n```\n\n把`babel-loader@8`降低了版本,调整为`babel-loader@7`\n\n**ps**: 想了一下,觉得可能其他的`loader`版本也会过低,于是将其他的`loader`都进行了升级,具体见[package.json](#目标版本)。\n\n## 优化打包速度\n\n### happypack\n\n一个号称用多进程策略提升`webpack`打包速度的插件,真的挺管用的。\n\n> happypack允许您并行转换多个文件,从而加快了webpack的构建速度。\n\n安装:\n\n```\nnpm install --save-dev happypack\n```\n\n简单配置如下:\n\n```javascript\nconst HappyPack = require(\'happypack\')\n\n// webpack配置,只列出关于happypack的配置\nrules: [\n // ...其他rule\n {\n test: /\\.js$/,\n // 注释掉原来的babel-loader,改用happypack/loader\n // loader: \"babel-loader\",\n use: [\'happypack/loader\'],\n include: [\n resolve(\"src\")\n ]\n }\n],\nplugins: [\n // ...其他plugin\n // 安装说明简单配置了一下\n new HappyPack({\n // 将我们刚才注释的loader放在这,告诉happypack\n loaders: [\'babel-loader\'],\n // 开启4个子进程,据说是最优解\n threads: 4\n })\n]\n```\n\n# 总结\n\n经过大量`npm`包版本的调整,以及`webpack`配置的修改(主要是`optimization`的调整;把`extract-text-webpack-plugin`换成了`mini-css-extract-plugin`;加入了`happypack`),报错基本上消除了,经测试,`dev`和`prod`环境都没有功能上的问题,热加载,编译,打包速度确实得到了显著提升。\n\n- 热加载\n\n 速度得到了显著提升,之前改一行代码,热加载编译的时间差不多要花`1min`,让人难受;优化后,基本上控制在`<=5s`\n\n- `webpack`升级前打包:\n\n ```\n Hash: 35f207120dd3736758dd\n Version: webpack 3.10.0\n Time: 95987ms\n ```\n\n 大概需要`96s`的打包时间。\n\n- `webpack`升级后打包:\n\n ```\n Hash: fb73468076752cad58f6\n Version: webpack 4.39.3\n Time: 61597ms\n ```\n\n 打包时间降低到`61.6s`,节约了`34.4s`,打包效率提升了`35.8%`以上。\n\n- 使用`happypack`后:\n\n ```\n Happy[1]: Version: 5.0.1. Threads: 4\n Happy[1]: All set; signaling webpack to proceed.\n Hash: a635e8b39b7064adf41c\n Version: webpack 4.39.3\n Time: 41047ms\n ```\n\n 打包时间降低到`41s`,再次节约了`20.6s`!总共节约了`55s`,与升级前相比,打包效率提升了`57%`以上。\n\n当然可优化的空间还很大,`webpack4`还有很多东西值得我们去折腾,优化之路还在继续!', '2019-09-11 16:09:31', '2024-09-20 15:01:50', 1, 40, 0, '最近有感觉到,随着系统模块数量的增加,wepack编译打包的速度越来越慢,于是我想给项目做一下优化升级,也借此机会系统地学习一下webpack4', 'https://qncdn.wbjiang.cn/webpack_400x400.png', 0, 0); -INSERT INTO `article` VALUES (185, 'Gerrit常见命令及最佳实践', '# 概述\n\n本文记录了笔者在使用`Gerrit`(一种免费、开放源代码的代码审查软件)过程中的一些微小的经验,在这里做个简单的分享。\n\n# 克隆工程\n\n```shell\ngit clone ssh://tusi@xx.xx.cn:29428/project-name\n```\n\n如果使用了`Git`代理,请将`xx.xx.cn:29428`换成代理后的`ip:port`\n\n```shell\ngit clone ssh://tusi@ip:port/project-name\n```\n\n# 创建develop分支\n\n一般我们不会将代码直接提交到`master`分支,而是会选择在`develop`分支进行开发\n\n```shell\ngit checkout -b develop origin/develop\n```\n\n# 添加到暂存区\n\n修改代码后,将所修改的代码从工作区添加到暂存区\n\n```shell\n// 添加所有文件到暂存区\ngit add .\n// 添加某目录或文件到暂存区\ngit add src\n```\n\n# 提交暂存区改动\n\n将暂存区内容提交到版本库\n\n```shell\ngit commit -m \'测试commit\'\n```\n\n# 推送到远程分支\n\n```shell\ngit push origin HEAD:refs/for/develop\n```\n\n# 常见报错\n\n## missing Change-Id in commit message footer\n\n先执行这两条命令,命令中的信息改成自己的\n\n```shell\ngitdir=$(git rev-parse --git-dir); scp -p -P 80 tusi@ip:hooks/commit-msg ${gitdir}/hooks/\ngit commit --amend\n```\n\n再次`push`\n\n## Gerrit merge conflict\n\n1. 在`Gerrit`上`abandon`这次`push`\n2. 软回滚\n\n```\ngit reset --soft origin/master\n```\n\n3. `pull`代码\n\n```\ngit pull\n```\n\n4. 再次`commit`, `push`\n\n# 最佳实践\n\n## git status检查仓库状态\n\n一个很好的习惯,`add`, `commit`, `push`等操作前后都可以用`git status`检查下,有助于理解`Git`的原理。\n\n```shell\ngit status\n```\n\n## hotfix合入master\n\n```shell\ngit merge origin/hotfix/20190909\ngit push origin HEAD:refs/for/master\n```\n\n## 强制与远程分支同步\n\n慎重操作!!!会覆盖掉本地代码!\n\n```shell\ngit reset --hard origin/develop\n```\n\n## git add 后想撤销\n\n不小心添加了文件到暂存区?使用以下命令:\n\n```shell\ngit checkout -- src/main.js\n```\n\n## git commit 后想回退\n\n```shell\n// 不小心commit了1次\ngit reset --soft HEAD^\n// 不小心commit了2次\ngit reset --soft HEAD~2\n```\n\n## 紧急bug来了,临时保存feature代码\n\n1. 先保存代码\n\n```shell\ngit stash\n```\n\n2. 检查确认下\n\n```shell\ngit stash list\n```\n\n3. 切换分支去修复`bug`\n4. 修复完毕,切回`feature`分支,释出`stash`代码接着干\n\n```shell\ngit stash pop\n```', '2019-09-16 11:22:36', '2024-06-18 09:50:40', 1, 21, 0, '本文记录了笔者在使用Gerrit(一种免费、开放源代码的代码审查软件)过程中的一些微小的经验,在这里做个简单的分享。', 'https://qncdn.wbjiang.cn/book_600x400.png', 0, 0); -INSERT INTO `article` VALUES (186, '即将是史上最全的meta大全', '# 概述\n\n本文的目的是搜集当前主流的`meta`配置,方便开发者快速开发调试。在这里不会做各种`meta`的深入分析,只是简单的介绍,让大家知道有这个东西。\n\n# meta简述\n\n- `meta`用于描述 `HTML` 文档的元数据。通常用于指定网页的描述,关键词,作者及其他元数据。\n- 元数据可以被使用浏览器(如何显示内容或加载页面),搜索引擎(关键词),或其他 `Web` 服务调用。\n- `meta`从一定程度上影响`seo`。\n\n# meta支持哪些属性\n\n| 属性 | 值 | 描述 |\n| ---------- | ------------------------------------------------------------ | ------------------------------------------------- |\n| charset | *character_set* | 定义文档的字符编码。 |\n| content | *text* | 定义与 http-equiv 或 name 属性相关的元信息。 |\n| http-equiv | content-type
default-style
refresh | 把 content 属性关联到 HTTP 头部。 |\n| name | application-name
author
description
generator
keywords | 把 content 属性关联到一个名称。 |\n| scheme | *format/URI* | HTML5不支持。 定义用于翻译 content 属性值的格式。 |\n\n# http-equiv\n\n`meta`标签上的`http-equiv`属性与`http`头部信息相关,而且是响应头,因为`html`本质上是通过服务器响应得到的。`http-equiv`用于伪装 `HTTP` 响应头部信息。那么`http-equiv`有哪些类型呢?让我们一起看下。\n\n| 值 | 描述 |\n| ---------------- | ------------------------------------------------------------ |\n| cache-control | 控制文档的缓存机制。允许的值如下:
`public`:所有内容都将被缓存(客户端和代理服务器都可缓存)
`private`:内容只缓存到私有缓存中(仅客户端可以缓存,代理服务器不可缓存)
`no-cache`:不缓存,前提是通过服务器的缓存验证机制,如过期,内容改变等校验规则
`no-store`:所有内容都不会被缓存到缓存或 `Internet` 临时文件中
**(设置了貌似无效,还是说不会出现在响应头吗?哪位大神可以解释下)** |\n| content-language | 响应体的语言。如`zh-CN`, `en-US`等
**(设置了貌似无效)** |\n| content-type | 返回内容的`MIME`类型 |\n| date | 原始服务器消息发出的时间,`GMT`时间格式 |\n| expires | 响应过期的日期和时间,`GMT`时间格式
``
**(设置了貌似无效)** |\n| last-modified | 请求资源的最后修改时间,`GMT`时间格式
**(设置了貌似无效)** |\n| location | 用来重定向接收方到非请求`URL`的位置来完成请求或标识新的资源
**(设置了貌似无效)** |\n| refresh | 定义间隔多久后刷新页面。单位是秒。 |\n| set-cookie | 创建一个 `cookie` ,包含 `cookie` 名,`cookie` 值,过期时间。
**(设置了貌似无效)** |\n| window-target | 用来防止别人在框架里调用自己的页面。
``
**(设置了貌似无效)** |\n| Pragma | 向后兼容只支持 `HTTP/1.0` 协议的缓存服务器,那时候 `HTTP/1.1` 协议中的 `Cache-Control` 还没有出来。
``
**(设置了貌似无效)** |\n\n**注意:**以上都是在`chrome`浏览器最新版本, `vue dev`环境下测试的,不代表所有浏览器和服务器表现。\n\n# 常见meta\n\n1. 指定字符编码\n\n ```html\n \n ```\n\n2. `IE`杀手,推荐所有前端工程师采用,让我们干掉`IE`的市场份额。\n\n ```html\n \n \n \n \n \n \n \n \n \n \n \n ```\n\n3. `viewport`常见设置,一般适用于移动端。视口宽度设为理想宽度,禁止缩放。\n\n ```html\n \n ```\n\n4. `meta`三剑客\n\n ```html\n \n \n \n ```\n\n5. `UC`浏览器私有`meta`\n\n ```html\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n ```\n\n6. `QQ`浏览器`X5`内核私有`meta`(现在微信内置浏览器的内核也是`X5`哦)\n\n ```html\n \n \n \n \n \n \n ```\n\n7. 苹果机适配\n\n ```html\n \n \n \n \n \n \n \n \n ```\n\n8. 其他优化和适配手段\n\n ```html\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n 。\n \n \n origin-when-crossorigin\n \n \n \n \n \n \n \n \n \n \n \n ```', '2019-09-18 15:32:39', '2024-09-03 14:35:01', 1, 46, 0, '本文的目的是搜集当前主流的meta配置,方便开发者快速开发调试。在这里不会做各种meta的深入分析,只是简单的介绍,让大家知道有这个东西。', 'https://qncdn.wbjiang.cn/tag_480x270.png', 0, 0); -INSERT INTO `article` VALUES (187, '发布一个简单的npm包', '本文简单地记录了发布一个简单`npm`包的过程,以便后续参考使用。\n\n# 初始化npm init\n\n通过`npm init`创建一个`package.json`文件\n\n```\nD:\\robin\\lib\\weapp-utils>npm init\nThis utility will walk you through creating a package.json file.\nIt only covers the most common items, and tries to guess sensible defaults.\n\nSee `npm help json` for definitive documentation on these fields\nand exactly what they do.\n\nUse `npm install ` afterwards to install a package and\nsave it as a dependency in the package.json file.\n\nPress ^C at any time to quit.\npackage name: (weapp-utils)\nversion: (1.0.0)\ndescription: some foundmental utils for weapp\nentry point: (lib/index.js)\ntest command:\ngit repository:\nkeywords: weapp,utils\nauthor: tusi666\nlicense: (ISC) MIT\nAbout to write to D:\\robin\\lib\\weapp-utils\\package.json:\n\n{\n \"name\": \"weapp-utils\",\n \"version\": \"1.0.0\",\n \"description\": \"some foundmental utils for weapp\",\n \"main\": \"lib/index.js\",\n \"scripts\": {\n \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n },\n \"keywords\": [\n \"weapp\",\n \"utils\"\n ],\n \"author\": \"tusi666\",\n \"license\": \"MIT\"\n}\n```\n\n其中`main`字段是入口文件\n\n# 写好README\n\n一个完备的`README`文件是必要的,以便别人了解你的包是做什么用途。\n\n# 确认registry\n\n一般我们开发时会修改`npm registry`为`https://registry.npm.taobao.org`。\n\n但是发布`npm`包时,我们需要将其改回来,不然是会报错的。\n\n```shell\nnpm config set registry http://registry.npmjs.org/\n```\n\n# npm注册账号\n\n打开`npm`[官网](https://www.npmjs.com/),开始注册账号。\n\n**ps:**记得要验证邮箱哦!\n\n# 添加npm账户\n\n使用`npm adduser`添加账户,别名`npm login`\n\n```\nD:\\robin\\lib\\weapp-utils>npm adduser\nUsername: tusi666\nPassword:\nEmail: (this IS public) cumtrobin@163.com\nLogged in as tusi666 on https://registry.npm.taobao.org/.\n```\n\n# 添加github仓库\n\n在`package.json`添加配置项,不加也没事,看自己需求。\n\n```json\n\"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/xxx/zqh_test2.git\"\n}\n```\n\n# 发布\n\n```shell\nnpm publish\n```\n\n如果发布时报这样的错,\n\n```shell\nThe operation was rejected by your operating system.\nnpm ERR! It\'s possible that the file was already in use (by a text editor or antivirus),\nnpm ERR! or that you lack permissions to access it.\n```\n\n建议还是检查下`registry`,或者`npm adduser`是不是成功了。\n\n发布成功,会有这样的提示,\n\n```shell\nnpm notice\nnpm notice package: weapp-utils@1.0.0\nnpm notice === Tarball Contents ===\nnpm notice 397B package.json\nnpm notice 1.1kB LICENSE\nnpm notice 2.7kB README.md\nnpm notice 12.9kB lib/index.js\nnpm notice === Tarball Details ===\nnpm notice name: weapp-utils\nnpm notice version: 1.0.0\nnpm notice package size: 5.1 kB\nnpm notice unpacked size: 17.1 kB\nnpm notice shasum: a7f2f428d9334dd1dd749d2a492dbc4df7195d0d\nnpm notice integrity: sha512-Cp8jPhOMq73y6[...]bfofe7X+4cLeg==\nnpm notice total files: 4\nnpm notice\n+ weapp-utils@1.0.0\n```\n\n上`npm`搜索`weapp-utils`,发现有了!\n\n![npm查询到所发布的包](https://qncdn.wbjiang.cn/npm%E6%9F%A5%E8%AF%A2%E5%88%B0%E5%8F%91%E5%B8%83%E7%9A%84%E5%8C%85.png)\n\n# 调用\n\n发布成功了,也要验证下,是否可正常使用。\n\n```javascript\nimport { merge } from \"weapp-utils\"\n\nlet mergedOptions = merge(DEFAULT_OPTIONS, options)\n```', '2019-09-21 15:19:42', '2024-09-02 17:10:24', 1, 48, 0, '本文简单地记录了发布一个简单npm包的过程,以便后续参考使用。', 'https://qncdn.wbjiang.cn/npm%E9%80%9A%E7%94%A8.jpg', 0, 0); -INSERT INTO `article` VALUES (188, '块级元素和行内元素', '最近给自己定了一个小目标,一周温习一个基础知识点,并输出一篇手记。看自己是否能坚持下去。^_^\n\n# 块级元素\n\n块级元素占据独立的空间,有以下特点:\n\n- 独占一行或多行\n- 宽度,高度,内外边距可以设置,且有效\n- 宽度默认是父容器的100%\n- 可以作为其他块级元素和行内元素的父容器(文本类块级元素不建议作为其他块级元素的容器,如`p, h1~h6`)\n\n![块级元素效果](https://qncdn.wbjiang.cn/%E5%9D%97%E7%BA%A7%E5%85%83%E7%B4%A0%E6%95%88%E6%9E%9C.png)\n\n常见的块级元素有:`div, h1~h6, hgroup, p, table, form, ul, ol, hr, header, main, footer, aside, article, section, video, audio, canvas, pre, option` \n\n# 行内元素\n\n行内元素不占据独立空间,依靠自身内容撑开宽高,与同属一个父容器的其他行内元素在同一行上依次排列,根据`white-space`属性值来决定是否换行。它们具备以下特征:\n\n- 不独占一行,但内容过长时会根据`white-space`控制换行。\n\n- 宽度,高度的设置是无效的。内外边距只能设置左右方向(设置`padding-top`, `padding-bottom`, `margin-top`, `margin-bottom`是无效的)。但是有一点要注意,`padding-top`和`padding-bottom`对自身有表现效果,但是不影响周围元素的布局,看图说话:\n\n ![行内元素上下边距效果](https://qncdn.wbjiang.cn/%E8%A1%8C%E5%86%85%E5%85%83%E7%B4%A0%E4%B8%8A%E4%B8%8B%E5%86%85%E8%BE%B9%E8%B7%9D%E6%95%88%E6%9E%9C.png)\n\n- 宽度由自身内容决定。\n\n- 行内元素不建议作为块级元素的容器(`a`标签例外)\n\n常见的行内元素有:`span, i, code, strong, a, br, sub, sup, label ` \n\n对于不确定的元素,可以设置`width`来测试下,如果`width`不生效,说明是行内元素啦。\n\n# 行内块级元素\n\n行内块级元素也不会独占一行,但是可设置宽高,内外边距等。\n\n常见的行内块级元素有:`input, button, img, select, textarea `\n\n# CSS显示转换\n\n## display: block;\n\n让元素表现为块级元素\n\n## display: inline;\n\n让元素表现为行内元素\n\n## display: inline-block;\n\n让元素表现为行内块级元素\n\n------', '2019-09-26 12:44:51', '2024-09-04 00:52:12', 1, 25, 0, '最近给自己定了一个小目标,一周温习一个基础知识点,并输出一篇手记。看自己是否能坚持下去。^_^', 'https://qncdn.wbjiang.cn/tag_480x270.png', 0, 0); -INSERT INTO `article` VALUES (189, '耐人寻味的CSS属性white-space', '《耐人寻味的CSS属性white-space》,本文说的`white-space`是一个控制换行和空白处理的`CSS`属性。我曾经被这个属性烦死,一直没记住,今天决定还是写下来好好琢磨下。\n\n# 属性值\n\n## normal\n\n**默认值**,正常换行,空白和换行符会被浏览器忽略。啥意思呢?\n\n- 正常换行的意思是,单词间会正常换行,如果下一个单词太长,不足以在当前行剩余部分完整展示,则会在下一行显示。哪些情况算一个单词呢?\n\n - 一个中文字\n\n - 一个英文单词\n\n ```\n // 这是两个单词\n Tusi Blog\n // 这只算一个单词\n TusiBlog\n ```\n\n - 连续的数字或符号也只算一个单词\n\n ```\n // 这只算一个单词,如果超长也不会换行,会挤出横向滚动条\n 10000000000000000000000+2000000000000000000*200000000000000\n ```\n\n- 空白和换行符会被浏览器忽略。就是你输入连续的空格,只会表现出一个空格的效果;如果敲了回车,也不会换行。\n\n```html\n\n
00000000 00000000000000000>
\n\n00000000 00000000000000000\n```\n\n## pre\n\n- 行为方式类似`HTML`中的`pre`标签。`pre`标签一般用来包裹源代码。\n- 不会自动换行(想想,你写代码时,不回车会换行吗?),除非在文本中遇到换行符(敲了回车)或使用了`br`标签。\n- 空白会被浏览器保留。意思就是连续的空格会被保留,不会合并成一个。\n\n![pre效果图](https://qncdn.wbjiang.cn/pre%E6%95%88%E6%9E%9C.png)\n\n## nowrap\n\n- 不换行,内容再多也不换行。\n\n- 忽略换行符,也就是说回车也不会换行,直到遇到`br`标签为止。\n\n## pre-wrap\n\n- 正常换行\n- 连续的空白符会被保留\n- 换行符(回车)也是有效的\n\n## pre-line\n\n- 正常换行\n- 连续空白符会被合并成一个\n- 换行符(回车)也有效\n\n## inherit\n\n继承父元素的`white-space`属性值\n\n# 总结\n\n可以从几个方面来对比下这几种属性值的差异。\n\n| / | 是否正常换行 | 是否合并连续空白符 | 换行符是否有效 |\n| -------- | ------------ | ------------------ | -------------- |\n| normal | 是 | 是 | 否 |\n| pre | 否 | 否 | 是 |\n| nowrap | 否 | 是 | 否 |\n| pre-wrap | 是 | 否 | 是 |\n| pre-line | 是 | 是 | 是 |\n\n妈呀,还是挺难记的,多多复习!', '2019-10-13 11:18:18', '2024-08-08 03:57:45', 1, 37, 0, '《耐人寻味的CSS属性white-space》,本文说的white-space是一个控制换行和空白处理的CSS属性。我曾经被这个属性烦死,一直没记住,今天决定还是写下来好好琢磨下。', 'https://qncdn.wbjiang.cn/css.jpg', 0, 0); -INSERT INTO `article` VALUES (190, '如何判断IE OCX插件正常安装?', '项目中用到了一个第三方的`ie ocx`控件,而经常遇到客户和测试小伙伴反馈相关功能无法正常使用,也没有友好提示。考虑到这个问题,必须要有一个`ie ocx`控件的检查机制。\n\n# 检查原理\n\n创建`ActiveXObject`对象去检查`ocx`控件\n\n```javascript\nlet newObj = new ActiveXObject(servername, typename[, location]) \n```\n\n# 参数问题\n\n看起来很简单的,但是用起来我懵逼了,应用程序对象名称`servername`这个参数怎么填呢?\n\n插件供应商只提供了控件安装包,示例程序,`clsid`\n\n```html\n\n```\n\n于是我想应该可以从`clsid`入手研究。\n\n## 什么是clsid\n\n> class identifier(类标识符)也称为CLASSID或CLSID,是与某一个类对象相联系的唯一标记(UUID)。一个准备创建多个对象的类对象应将其CLSID注册到系统注册数据库的任务表中,以使客户能够定位并装载与该对象有关的可执行代码。\n\n以上摘自百度百科,可以看到`clsid`跟`uuid`是类似的原理,用来进行插件的唯一标识。\n\n## 根据clsid怎么查到servername\n\n在`MDN`上搜索`ActiveXObject`词条,可以看到这么一句:\n\n> 您可以在`HKEY_CLASSES_ROOT`注册注册表项中识别主机PC上的`servername.typename的`值。\n\n哦,可以看到是从注册表中去查的。于是我运行`regedit`打开注册表查看,虽然知道是在`HKEY_CLASSES_ROOT`目录下,但是这也太多了吧,怎么找得到?\n\n![注册表HKEY_CLASSES_ROOT](https://qncdn.wbjiang.cn/%E6%B3%A8%E5%86%8C%E8%A1%A8HKEY_CLASSES_ROOT.png)\n\n当然还是要靠搜索功能,于是我根据`clsid`的值`27E1A157-6A29-48AE-86C2-14591D90B4D4`进行查找\n\n![搜索clsid](https://qncdn.wbjiang.cn/%E6%90%9C%E7%B4%A2clsid.png)\n\n搜索时间有点长,但是最终还是查到了,位置如下:\n\n`计算机\\HKEY_CLASSES_ROOT\\SDS_CMSCtrl.SDS_CMSCtrlCtrl.1`\n\n![ocx插件在注册表的位置](https://qncdn.wbjiang.cn/ocx%E6%8F%92%E4%BB%B6%E5%9C%A8%E6%B3%A8%E5%86%8C%E8%A1%A8%E7%9A%84%E4%BD%8D%E7%BD%AE.png)\n\n于是我猜想,`servername`应该就是`SDS_CMSCtrl.SDS_CMSCtrlCtrl.1`。经测试,果不其然。检查代码如下:\n\n```javascript\ntry {\n const ocx = new ActiveXObject(\'SDS_CMSCtrl.SDS_CMSCtrlCtrl.1\')\n console.log(ocx)\n} catch (error) {\n this.$alert(\'您还未安装视频插件!\', \'提示\')\n}\n```\n\n这样一来,如果用户没有安装插件,马上能够得到提示,perfect!\n\n![ocx未安装的友好提示](https://qncdn.wbjiang.cn/ocx%E6%9C%AA%E5%AE%89%E8%A3%85%E7%9A%84%E5%8F%8B%E5%A5%BD%E6%8F%90%E7%A4%BA.png)\n\n------', '2019-10-16 13:46:59', '2024-08-05 03:18:27', 1, 18, 0, '项目中用到了一个第三方的ie ocx控件,而经常遇到客户和测试小伙伴反馈相关功能无法正常使用,也没有友好提示。考虑到这个问题,必须要有一个ie ocx控件的检查机制。', 'https://qncdn.wbjiang.cn/ie_600x400.png', 0, 0); -INSERT INTO `article` VALUES (191, '耐人寻味的CSS属性font-family', '`font-family`是一个网站用户体验的第一入口,非常有必要花功夫来研究一下。我们首先需要了解衬线字体和无衬线字体,接着了解中英文的常用字体及其适用性。\n\n# 衬线字体\n\n衬线(`serif`)的笔画有粗有细的变化,在每一笔画上都自有风格,笔画末端会有修饰,强调艺术感,适合用于博客,旅游,文化,艺术类网站。\n\n# 无衬线字体\n\n无衬线(`sans-serif`)的字体工整方正,给人正式的感觉,适合政务类,企业类网站使用。\n\n# 中文字体\n\n## Windows\n\n- `simsun`,宋体,也是`windows`下大部分浏览器的默认字体,`font-size`较大时清晰度不佳。\n- `Microsoft Yahei`,无衬线字体,微软雅黑,是微软委托中国方正设计的一款中文字体。\n\n## Mac OS\n\n- `STHeiti`,华文黑体,`OS X 10.6`之前的简体中文系统界面默认字体,也是目前`Chrome`游览器下的默认字体。\n- `STXihei`,华文细黑,比`STHeiti`文字更细。\n- `Heiti SC`,黑体-简,从 `OS X 10.6` 开始,黑体-简代替华文黑体用作简体中文系统界面默认字体,显示效果不错,但是喇叭口设计遭人诟病。\n- `Hiragino Sans GB`,冬青黑体,清新的专业印刷字体,小字号时足够清晰,拥有很多人的追捧。\n- `PingFang SC`,苹方,在`Mac OS X EL Capitan`上,苹果为中国用户打造,去掉了为人诟病的喇叭口。\n\n## Linux\n\n- `WenQuanYi Micro Hei`,文泉驿微米黑,`Linux`最佳简体中文字体。\n\n# 英文字体\n\n## Windows\n\n- `Arial`,无衬线西文字体,显示效果一般。\n- `Tahoma`,无衬线字体,显示效果比`Arial`要好。\n- `Verdana`,无衬线字体,优点在于它在小字上仍结构清晰端整、阅读辨识容易。\n\n## Mac OS\n\n- `Times New Roman`,衬线字体,`Mac`平台`Safari`下默认的字体。\n- `Helvetica`、`Helvetica Neue`,被广泛使用。\n- `San Francisco`,与苹方一样,`mac os`最新的西文字体。\n\n# font-family设置原则\n\n- 西文优先:西文字体中大多不包含中文,西文优先,中文紧随其后,这样就不会影响到中文字体的选择。\n- 从新到旧:优先体验最好的字体,向下兼容。\n- 兼容多种操作系统:考虑`windows, mac os, android, linux`等系统。\n- 补充字体族:最后根据衬线`serif`或无衬线`sans-serif`来补充字体族,当所有设置的字体都找不到时,让操作系统有选择字体的方向。\n\n# font-family推荐\n\n```css\nfont-family: \"Helvetica Neue\", Helvetica, \"PingFang SC\", Tahoma, \"Hiragino Sans GB\", \"Heiti SC\", Arial, \"Microsoft YaHei\", \"WenQuanYi Micro Hei\", sans-serif;\n```', '2019-10-23 15:03:49', '2024-08-05 13:30:04', 1, 27, 0, 'font-family是一个网站用户体验的第一入口,非常有必要花功夫来研究一下。我们首先需要了解衬线字体和无衬线字体,接着了解中英文的常用字体及其适用性。', 'https://qncdn.wbjiang.cn/welcome_600x400.png', 0, 0); -INSERT INTO `article` VALUES (192, 'gradle环境搭建', '最近我在尝试了解跨平台技术的发展,首先则是想到了`cordova`。环境配置过程中有依赖`gradle`,下面简单记录了在`windos10`系统下搭建`gradle`环境的过程。\n\n# 什么是gradle\n\n> Gradle是一个基于Apache Ant和Apache Maven概念的项目自动化构建开源工具。 \n\n# 检查java环境\n\n首先要检查是否正常安装了`java`环境。\n\n```\nC:\\>java -version\njava version \"1.8.0_201\"\nJava(TM) SE Runtime Environment (build 1.8.0_201-b09)\nJava HotSpot(TM) 64-Bit Server VM (build 25.201-b09, mixed mode)\n```\n\n如果没安装,可以参考[jdk下载与安装简明教程](http://hexo.wbjiang.cn/jdk下载和安装简明教程.html)。\n\n# 下载gradle二进制安装包\n\n[v5.6.3版本下载链接]( https://gradle.org/next-steps/?version=5.6.3&format=bin )\n\n# 安装和环境变量配置\n\n首先在`C`盘新建一个` Gradle `目录,然后将安装包解压到该目录下,最终得到的目录是这样的。\n\n```\nC:\\Gradle\\gradle-5.6.3\n```\n\n然后我们需要给**环境变量-系统变量-Path**新增一项\n\n```\nC:\\Gradle\\gradle-5.6.3\\bin\n```\n\n完成后,即可命令行检查下`gradle`是否安装配置正常。\n\n```\nC:\\>gradle -v\n\n------------------------------------------------------------\nGradle 5.6.3\n------------------------------------------------------------\n\nBuild time: 2019-10-18 00:28:36 UTC\nRevision: bd168bbf5d152c479186a897f2cea494b7875d13\n\nKotlin: 1.3.41\nGroovy: 2.5.4\nAnt: Apache Ant(TM) version 1.9.14 compiled on March 12 2019\nJVM: 1.8.0_201 (Oracle Corporation 25.201-b09)\nOS: Windows 10 10.0 amd64\n```\n\n------', '2019-10-28 14:55:13', '2024-08-08 01:11:08', 1, 28, 0, '最近我在尝试了解跨平台技术的发展,首先则是想到了cordova。环境配置过程中有依赖gradle,下面简单记录了在windos10系统下搭建gradle环境的过程。', 'https://qncdn.wbjiang.cn/gradle.png', 0, 0); -INSERT INTO `article` VALUES (193, 'cordova开发环境搭建', '最近我在尝试了解跨平台技术的发展,首先则是想到了`cordova`。本文简单记录下`cordova`环境搭建的过程。\n\n# 安装cordova\n\n首先是要`npm`全局安装`cordova`\n\n```shell\nnpm install -g cordova\n```\n\n# 创建应用\n\n安装的`cordova`类似于`create-react-app`这种脚手架,可以通过命令行直接创建应用\n\n```shell\ncordova create myapp\n```\n\n# 添加平台支持\n\n`cordova`可以支持`ios`, `android`, `web`三端。\n\n```shell\ncordova platform add ios\ncordova platform add android\ncordova platform add browser\n```\n\n![cordova platforms](https://qncdn.wbjiang.cn/cordova%E5%B9%B3%E5%8F%B0%E6%94%AF%E6%8C%81.png)\n\n进入`android`目录下,可以看到很多`.java`文件,而`ios`目录下是很多的`object-c`文件,`browser`目录下则是熟悉的`web`工程。\n\n并且可以看到,每个平台下都有一个`cordova`目录,我初步猜想,这应该是负责和不同平台通讯交互的`cordova`核心。\n\n# 运行App\n\n## Web\n\n`web`端是最直观最简单的,直接运行如下命令即可。\n\n```shell\ncordova run browser\n```\n\n## Android\n\n对于`Android`和`IOS`,我们则需要先检查相关环境是否安装正常。\n\n```shell\n$ cordova requirements\n\nRequirements check results for android:\nJava JDK: installed 1.8.0\nAndroid SDK: not installed\nFailed to find \'ANDROID_HOME\' environment variable. Try setting it manually.\nDetected \'adb\' command at C:\\Windows\\system32 but no \'platform-tools\' directory found near.\nTry reinstall Android SDK or update your PATH to include valid path to SDK\\platform-tools directory.\nAndroid target: not installed\nandroid: Command failed with exit code ENOENT Error output:\n\'android\' �����ڲ����ⲿ���Ҳ���ǿ����еij���\n���������ļ���\nGradle: not installed\nCould not find gradle wrapper within Android SDK. Could not find Android SDK directory.\nMight need to install Android SDK or set up \'ANDROID_HOME\' env variable.\n\nRequirements check results for browser:\n\nRequirements check results for ios:\nApple macOS: not installed\nCordova tooling for iOS requires Apple macOS\nSome of requirements check failed\n```\n\n可以看到,我的电脑环境并不满足`android`和`ios`平台的要求。\n\n首先我们来满足下`android`平台的环境要求。\n\n### JDK\n\n首先是`JDK`,可以通过`java`和`javac`命令来检查下。\n\n```shell\nC:\\>java -version\njava version \"1.8.0_201\"\nJava(TM) SE Runtime Environment (build 1.8.0_201-b09)\nJava HotSpot(TM) 64-Bit Server VM (build 25.201-b09, mixed mode)\n```\n\n如果没安装,可以参考[jdk下载与安装简明教程](http://hexo.wbjiang.cn/jdk下载和安装简明教程.html)。\n\n### Gradle\n\n> Gradle是一个基于Apache Ant和Apache Maven概念的项目自动化构建开源工具。 \n\n具体安装过程可以参考[gradle环境搭建](http://hexo.wbjiang.cn/gradle环境搭建.html)。\n\n### Android SDK\n\n首先我们安装[Android Studio](https://developer.android.google.cn/studio)。根据安装指引,我们会安装好`Android SDK`。\n\n在此安装过程中,遇到了一个报错:\n\n```\nAndroid SDK Tools, SDK Patch Applier v4 and 5 more SDK components were not installed\n```\n\n感谢这位[大佬提供的解决方案](https://blog.csdn.net/qq_36784975/article/details/89096195),迅速解决了问题,这里顺便记下`SDK`的安装目录。\n\n```\nC:\\Users\\Jiang.Wenbin\\AppData\\Local\\Android\\Sdk\n```\n\n接着我们需要在**环境变量-系统变量-Path**中新增两项:\n\n```\nC:\\Users\\Jiang.Wenbin\\AppData\\Local\\Android\\Sdk\\platform-tools\nC:\\Users\\Jiang.Wenbin\\AppData\\Local\\Android\\Sdk\\tools\n\n```\n\n并且新增一项**系统变量ANDROID_HOME**,变量值为:\n\n```\nC:\\Users\\Jiang.Wenbin\\AppData\\Local\\Android\\Sdk\n\n```\n\n试运行命令`cordova run android`,出现了如下警告\n\n```\n$ cordova run android\nChecking Java JDK and Android SDK versions\nANDROID_SDK_ROOT=undefined (recommended setting)\nANDROID_HOME=C:\\Users\\Jiang.Wenbin\\AppData\\Local\\Android\\Sdk (DEPRECATED)\nStarting a Gradle Daemon (subsequent builds will be faster)\n\n```\n\n于是我又新增了一项**系统变量ANDROID_SDK_ROOT**,变量值与**ANDROID_HOME**一样。\n\n重新跑`cordova run android`命令,首先看到警告如下:\n\n```\n> Configure project :app\nChecking the license for package Android SDK Platform 28 in C:\\Users\\Jiang.Wenbin\\AppData\\Local\\Android\\Sdk\\licenses\nWarning: License for package Android SDK Platform 28 not accepted.\n\n```\n\n上网一查,原来是没有同意相关协议。我们来到`C:\\Users\\Jiang.Wenbin\\AppData\\Local\\Android\\Sdk\\tools\\bin`目录下运行命令行,输入`sdkmanager --licenses`,然后就会弹出一堆协议,没办法,无脑输入`y`同意吧。\n\n再次运行`cordova run android`,发现了新的报错信息:\n\n```\nNo target specified and no devices found, deploying to emulator\nNo emulator images (avds) found.\n1. Download desired System Image by running: \"C:\\Users\\Jiang.Wenbin\\AppData\\Local\\Android\\Sdk\\tools\\android.bat\" sdk\n2. Create an AVD by running: \"C:\\Users\\Jiang.Wenbin\\AppData\\Local\\Android\\Sdk\\tools\\android.bat\" avd\nHINT: For a faster emulator, use an Intel System Image and install the HAXM device driver\n\n```\n\n可以看到,是没有找到设备的原因。需要将手机连接到`PC`,并且打开开发者选项,允许`USB`调试。再次尝试,已经可以看到界面了。\n\n![cordova app界面](https://qncdn.wbjiang.cn/cordova%20app%E7%95%8C%E9%9D%A2.jpg)\n\n### Plugins\n\n我们来试试调用一些原生`API`,比如调用原生`Dialog`, 调用相机等。我们先试下`Dialog`。\n\n#### Dialog\n\n首先需要插件:\n\n```\ncordova plugin add cordova-plugin-dialogs\n\n```\n\n接着我们在`deviceready`事件之后调用`Dialog`\n\n```javascript\ndocument.addEventListener(\'deviceready\', onDeviceReady, false);\n\nfunction onDeviceReady() {\n navigator.notification.alert(\n \'欢迎欢迎!\', // message\n alertDismissed, // callback\n \'试下Dialog\', // title\n \'好的\' // buttonName\n );\n}\n\nfunction alertDismissed() {\n // 点击按钮后的回调\n}\n\n```\n\n调试后发现弹出的中文乱码了,需要在`index.html`加个`meta`\n\n```html\n\n\n```\n\n![cordova_dialog](https://qncdn.wbjiang.cn/cordova_dialog.png)\n\n#### Camera\n\n接着我们试下调用相机,首先也是安装插件:\n\n```\ncordova plugin add cordova-plugin-camera\n\n```\n\n尝试调用相机拍照,并将得到的照片通过`img`元素显示出来:\n\n```javascript\n// Application Constructor\ninitialize: function() {\n const _this = this;\n document.addEventListener(\'deviceready\', this.onDeviceReady.bind(this), false);\n // 点击按钮打开相机\n document.querySelector(\'#btnOpenCamera\').addEventListener(\'click\', function() {\n _this.openCamera()\n })\n},\nopenCamera: function() {\n const cameraOptions = {\n // 默认输出格式为base64\n destinationType: Camera.DestinationType.DATA_URL,\n // 输出png格式\n encodingType: Camera.EncodingType.PNG\n };\n\n navigator.camera.getPicture(cameraSuccess, cameraError, cameraOptions);\n\n // 相机拍照成功\n function cameraSuccess(base64Str) {\n console.log(base64Str)\n // 给图片元素赋值src\n document.querySelector(\'#captureImg\').src = prefixBase64PNG(base64Str)\n }\n\n function cameraError(err) {\n console.error(err)\n }\n\n function prefixBase64PNG(base64Str) {\n return \'data:image/png;base64,\' + base64Str;\n }\n}\n\n```\n\n效果如下:\n\n![cordova_camera](https://qncdn.wbjiang.cn/cordova_camera.jpg)\n\n## IOS\n\n还没钱买`IOS`设备,尴尬。。。\n\n------', '2019-11-01 14:16:18', '2024-08-02 07:39:06', 1, 31, 0, '最近我在尝试了解跨平台技术的发展,首先则是想到了cordova。本文简单记录下cordova环境搭建的过程。', 'https://qncdn.wbjiang.cn/cordova%E6%9E%B6%E6%9E%84.png', 0, 0); -INSERT INTO `article` VALUES (194, 'ionic初体验', '搞了一波`cordova`后,算是对`Hybrid`有了一点点微小的认知。为了快速开发,`ionic`无疑是更好的选择,它底层的打包和通信机制基于`cordova`实现,在上层实现了自己的`UI`组件,可以结合`Angular`或`React`使用,并且宣称将在未来支持`Vue`。\n\n# 环境准备\n\n如果已经安装了`cordova`,则单独安装`ionic`即可,否则需要一并安装。\n\n```\nnpm install -g ionic cordova\n```\n\n# 创建项目\n\n通过`start`命令来新建一个`ionic`项目。\n\n```\nionic start my-app\n```\n\n并且可以支持传入模板,以及项目类型,具体参考[ionic start](https://ionicframework.com/docs/cli/commands/start)。\n\n我们在这里创建一个基于`angular`的`tabs`导航的`app`。\n\n```\nionic start myapp tabs --type=ionic-angular\n```\n\n当然也可以直接从一个更完善的模板开始。\n\n```\nionic start myapp super --type=ionic-angular\n```\n\n这几种方式可以都试试看。\n\n# 运行项目\n\n## 在浏览器运行web版\n\n在尝试`npm start`调用`ionic-app-scripts serve`启动项目时,发现报错找不到`@ionic/app-scripts`模块,尝试重新安装该模块,`node-gyp`模块又报了这个错:\n\n```\nError: Can\'t find Python executable \"python\", you can set the PYTHON env variable.\n```\n\n查询[node-gyp]( https://www.npmjs.com/package/node-gyp/v/3.8.0 )后,官方提供了两种解决方案\n\n![解决找不到python模块的问题](https://qncdn.wbjiang.cn/%E8%A7%A3%E5%86%B3%E6%89%BE%E4%B8%8D%E5%88%B0python%E6%A8%A1%E5%9D%97%E7%9A%84%E9%97%AE%E9%A2%98.png)\n\n我采用了第一种方案:\n\n```\nnpm install --global --production windows-build-tools\n```\n\n**ps:** 必须以系统管理员方式运行命令行。\n\n接着重新安装一遍`@ionic/app-scripts`,然后重新运行项目,冇问题啦。\n\n```\nnpm uninstall @ionic/app-scripts\nnpm install --save-dev @ionic/app-scripts\nnpm start\n```\n\n![ionic界面](https://qncdn.wbjiang.cn/ionic%E7%95%8C%E9%9D%A2.png)\n\n## 支持android和ios\n\n```she\nionic cordova platform add ios\nionic cordova platform add android\n```\n\n## android调试\n\n首先检查下设备连接是否正常\n\n```shell\nD:\\robin\\frontend\\hybrid\\ionic\\ionic-blog> adb devices\nList of devices attached\n5fdba1e7 device\n```\n\n使用`ionic cli`提供的命令运行`app`\n\n```shell\n// -l是--livereload的简写\nionic cordova run android -l\n```\n\n此时注意在手机上同意**“继续安装”**,否则是不会成功的。安装成功则可以看到成功的提示。\n\n```\n> cordova.cmd build android --device\n[app-scripts] [16:05:33] lint finished in 3.95 s\n> native-run.cmd android --app platforms\\android\\app\\build\\outputs\\apk\\debug\\app-debug.apk --device --forward 8100:8100 --forward 35729:35729 --forward 53703:53703\n[native-run] Selected hardware device 5fdba1e7\n[native-run] Forwarded device port 35729 to host port 35729\n[native-run] Forwarded device port 8100 to host port 8100\n[native-run] Forwarded device port 53703 to host port 53703\n[native-run] Installing platforms\\android\\app\\build\\outputs\\apk\\debug\\app-debug.apk...\n[native-run] Starting application activity io.ionic.starter/io.ionic.starter.MainActivity...\n[native-run] Run Successful\n```\n\n![ionic界面](https://qncdn.wbjiang.cn/ionic%E7%95%8C%E9%9D%A2.jpg)\n\n此时还可以在`Chrome`浏览器上输入`chrome://inspect`进行调试。\n\n![chrome inspect](https://qncdn.wbjiang.cn/ionic_chrome_inspect.png)\n\n手机上的操作会同步到`Chrome`浏览器上。![ionic远程调试动图](https://qncdn.wbjiang.cn/ionic%E8%BF%9C%E7%A8%8B%E8%B0%83%E8%AF%95%E5%8A%A8%E5%9B%BE.gif)\n\n并且还支持断点调试。\n\n![ionic断点调试](https://qncdn.wbjiang.cn/ionic%E6%96%AD%E7%82%B9%E8%B0%83%E8%AF%95.png)\n\n## ios调试\n\n`ios`就先不试了,没设备。。。\n\n------', '2019-11-05 18:41:07', '2024-08-09 12:37:13', 1, 43, 0, '体验一把ionic', 'https://qncdn.wbjiang.cn/ionic.svg', 0, 0); -INSERT INTO `article` VALUES (196, '用初中数学知识撸一个canvas环形进度条', '周末好,今天给大家带来一款接地气的环形进度条组件`vue-awesome-progress`。近日被设计小姐姐要求实现这么一个环形进度条效果,大体由四部分组成,分别是底色圆环,进度弧,环内文字,进度圆点。设计稿截图如下:\n\n![环形进度条设计稿](https://qncdn.wbjiang.cn/%E7%8E%AF%E7%8A%B6%E8%BF%9B%E5%BA%A6%E6%9D%A1%E8%AE%BE%E8%AE%A1%E7%A8%BF.png)\n\n我的第一反应还是找现成的组件,市面上很多组件都实现了前3点,独独没找到能画进度圆点的组件,不然稍加定制也能复用。既然没有现成的组件,只有自己用`vue + canvas`撸一个了。\n\n![我也很无奈啊](https://qncdn.wbjiang.cn/%E6%88%91%E4%B9%9F%E5%BE%88%E6%97%A0%E5%A5%88%E5%95%8A.jpg)\n\n# 效果图\n\n先放个效果图,然后再说下具体实现过程,各位看官且听我慢慢道来。\n\n![环形进度条效果图](https://qncdn.wbjiang.cn/%E7%8E%AF%E5%BD%A2%E8%BF%9B%E5%BA%A6%E6%9D%A1%E6%95%88%E6%9E%9C%E5%9B%BE.gif)\n\n# 安装与使用\n\n[源码地址]( https://github.com/cumt-robin/vue-awesome-progress ),欢迎`star`和提`issue`。\n\n## 安装\n\n```shell\nnpm install --save vue-awesome-progress\n```\n\n## 使用\n\n### 全局注册\n\n```javascript\nimport Vue from \'vue\'\nimport VueAwesomeProgress from \"vue-awesome-progress\"\nVue.use(VueAwesomeProgress)\n```\n\n### 局部使用\n\n```\nimport VueAwesomeProgress from \"vue-awesome-progress\"\n\nexport default {\n components: {\n VueAwesomeProgress\n },\n // 其他代码\n}\n```\n\n### script标签引入组件\n\n同时也支持直接使用`script`标签引入哦,满足有这部分需求的朋友。\n\n```html\n\n\n\n \n \n\n\n
\n \n\n\n```\n\n# 静态展示\n\n任何事都不是一蹴而就的,我们首先来实现一个静态的效果,然后再实现动画效果,甚至是复杂的控制逻辑。\n\n## 确定画布大小\n\n第一步是确定画布大小。从设计稿我们可以直观地看到,整个环形进度条的最外围是由进度圆点确定的,而进度圆点的圆心在圆环圆周上。\n\n![环形进度条模型](https://qncdn.wbjiang.cn/%E7%8E%AF%E5%BD%A2%E8%BF%9B%E5%BA%A6%E6%9D%A1%E6%A8%A1%E5%9E%8B.png)\n\n因此我们得出伪代码如下:\n\n```javascript\n// canvasSize: canvas宽度/高度\n// outerRadius: 外围半径\n// pointRadius: 圆点半径\n// pointRadius: 圆环半径\ncanvasSize = 2 * outerRadius = 2 * (pointRadius + circleRadius)\n```\n\n据此我们可以定义如下组件属性:\n\n```javascript\nprops: {\n circleRadius: {\n type: Number,\n default: 40\n },\n pointRadius: {\n type: Number,\n default: 6\n }\n},\ncomputed: {\n // 外围半径\n outerRadius() {\n return this.circleRadius + this.pointRadius\n },\n // canvas宽/高\n canvasSize() {\n return 2 * this.outerRadius + \'px\'\n }\n}\n```\n\n那么`canvas`大小也可以先进行绑定了\n\n```html\n\n```\n\n## 获取绘图上下文\n\n` getContext(\'2d\') `方法返回一个用于在`canvas`上绘图的环境,支持一系列`2d`绘图`API`。 \n\n```javascript\nmounted() {\n // 在$nextTick初始化画布,不然dom还未渲染好\n this.$nextTick(() => {\n this.initCanvas()\n })\n},\nmethods: {\n initCanvas() {\n var canvas = this.$refs.canvasDemo;\n var ctx = canvas.getContext(\'2d\');\n }\n}\n```\n\n## 画底色圆环\n\n完成了上述步骤后,我们就可以着手画各个元素了。我们先画圆环,这时我们还要定义两个属性,分别是圆环线宽`circleWidth`和圆环颜色`circleColor`。\n\n```javascript\ncircleWidth: {\n type: Number,\n default: 2\n},\ncircleColor: {\n type: String,\n default: \'#3B77E3\'\n}\n```\n\n`canvas`提供的画圆弧的方法是`ctx.arc()`,需要提供圆心坐标,半径,起止弧度,是否逆时针等参数。\n\n```javascript\nctx.arc(x, y, radius, startAngle, endAngle, anticlockwise);\n```\n\n 我们知道,`Web`网页中的坐标系是这样的,从绝对定位的设置上其实就能看出来(`top`,`left`设置正负值会发生什么变化),而且原点`(0, 0)`是在盒子(比如说`canvas`)的左上角哦。 \n\n![web坐标系](https://qncdn.wbjiang.cn/web%E5%9D%90%E6%A0%87%E7%B3%BB.jpeg)\n\n对于角度而言,`0°`是`x`轴正向,默认是顺时针方向旋转。\n\n圆环的圆心就是`canvas`的中心,所以`x `, `y` 取`outerRadius`的值就可以了。\n\n```javascript\nctx.strokeStyle = this.circleColor;\nctx.lineWidth = this.circleWidth;\nctx.beginPath();\nctx.arc(this.outerRadius, this.outerRadius, this.circleRadius, 0, this.deg2Arc(360));\nctx.stroke();\n```\n\n注意`arc`传的是弧度参数,而不是我们常理解的`360°`这种概念,因此我们需要将我们理解的`360°`转为弧度。\n\n```javascript\n// deg转弧度\ndeg2Arc(deg) {\n return deg / 180 * Math.PI\n}\n```\n\n![画圆环](https://qncdn.wbjiang.cn/%E7%94%BB%E5%9C%86%E7%8E%AF.png)\n\n## 画文字\n\n调用`fillText`绘制文字,利用`canvas.clientWidth / 2`和`canvas.clientWidth / 2`取得中点坐标,结合控制文字对齐的两个属性`textAlign`和`textBaseline`,我们可以将文字绘制在画布中央。文字的值由`label`属性接收,字体大小由`fontSize`属性接收,颜色则取的`fontColor`。\n\n```javascript\nif (this.label) {\n ctx.font = `${this.fontSize}px Arial,\"Microsoft YaHei\"`\n ctx.fillStyle = this.fontColor;\n ctx.textAlign = \'center\'\n ctx.textBaseline = \'middle\'\n ctx.fillText(this.label, canvas.clientWidth / 2, canvas.clientWidth / 2);\n}\n```\n\n![画文字](https://qncdn.wbjiang.cn/%E7%94%BB%E6%96%87%E5%AD%97.png)\n\n## 画进度弧\n\n支持普通颜色和渐变色,`withGradient`默认为`true`,代表使用渐变色绘制进度弧,渐变方向我默认给的从上到下。如果希望使用普通颜色,`withGradient`传`false`即可,并可以通过`lineColor`自定义颜色。\n\n```javascript\nif (this.withGradient) {\n this.gradient = ctx.createLinearGradient(this.circleRadius, 0, this.circleRadius, this.circleRadius * 2);\n this.lineColorStops.forEach(item => {\n this.gradient.addColorStop(item.percent, item.color);\n });\n}\n```\n\n其中`lineColorStops`是渐变色的颜色偏移断点,由父组件传入,可传入任意个颜色断点,格式如下:\n\n```javascript\ncolorStops2: [\n { percent: 0, color: \'#FF9933\' },\n { percent: 1, color: \'#FF4949\' }\n]\n```\n\n画一条从上到下的进度弧,即`270°`到`90°`\n\n```javascript\nctx.strokeStyle = this.withGradient ? this.gradient : this.lineColor;\nctx.lineWidth = this.lineWidth;\nctx.beginPath();\nctx.arc(this.outerRadius, this.outerRadius, this.circleRadius, this.deg2Arc(270), this.deg2Arc(90));\nctx.stroke();\n```\n\n![画进度弧](https://qncdn.wbjiang.cn/%E7%94%BB%E8%BF%9B%E5%BA%A6%E5%BC%A7.png)\n\n其中`lineWidth`是弧线的宽度,由父组件传入\n\n```javascript\nlineWidth: {\n type: Number,\n default: 8\n}\n```\n\n## 画进度圆点\n\n最后我们需要把进度圆点补上,我们先写死一个角度`90°`,显而易见,圆点坐标为`(this.outerRadius, this.outerRadius + this.circleRadius)`\n\n![90度圆点坐标](https://qncdn.wbjiang.cn/90%E5%BA%A6%E5%9C%86%E7%82%B9%E5%9D%90%E6%A0%87.png)\n\n画圆点的代码如下:\n\n```javascript\nctx.fillStyle = this.pointColor;\nctx.beginPath();\nctx.arc(this.outerRadius, this.outerRadius + this.circleRadius, this.pointRadius, 0, this.deg2Arc(360));\nctx.fill();\n```\n\n其中`pointRadius`是圆点的半径,由父组件传入:\n\n```javascript\npointRadius: {\n type: Number,\n default: 6\n}\n```\n\n![90度画圆点](https://qncdn.wbjiang.cn/90%E5%BA%A6%E7%94%BB%E5%9C%86%E7%82%B9.png)\n\n## 角度自定义\n\n当然,进度条的角度是灵活定义的,包括开始角度,结束角度,都应该由调用者随意给出。因此我们再定义一个属性`angleRange`,用于接收起止角度。\n\n```javascript\nangleRange: {\n type: Array,\n default: function() {\n return [270, 90]\n }\n}\n```\n\n有了这个属性,我们就可以随意地画进度弧和圆点了,哈哈哈哈。\n\n![等等,你忘了这个场景](https://qncdn.wbjiang.cn/%E7%AD%89%E7%AD%89%E4%BD%A0%E5%BF%98%E4%BA%86%E8%BF%99%E4%B8%AA%E5%9C%BA%E6%99%AF.jpg)\n\n老哥,这种圆点坐标怎么求?\n\n![特殊角度怎么求圆点圆心坐标](https://qncdn.wbjiang.cn/%E7%89%B9%E6%AE%8A%E8%A7%92%E5%BA%A6%E6%80%8E%E4%B9%88%E6%B1%82%E5%9D%90%E6%A0%87.png)\n\n噗......看来高兴过早了,最重要的是根据不同角度求得圆点的圆心坐标,这让我顿时犯了难。\n\n![你要冷静](https://qncdn.wbjiang.cn/%E6%96%8C%E5%93%A5%E4%BD%A0%E8%A6%81%E5%86%B7%E9%9D%99.gif)\n\n经过冷静思考,我脑子里闪过了一个利用正余弦公式求坐标的思路,但前提是坐标系原点如果在圆环外接矩形的左上角才好算。仔细想想,冇问题啦,我先给坐标系平移一下,最后求出来结果,再补个平移差值不就行了嘛。\n\n![平移坐标系后求圆点坐标](https://qncdn.wbjiang.cn/%E5%B9%B3%E7%A7%BB%E5%9D%90%E6%A0%87%E7%B3%BB%E5%90%8E%E6%B1%82%E5%9C%86%E7%82%B9%E5%9D%90%E6%A0%87.png)\n\n画图工具不是很熟练,这里图没画好,线歪了,请忽略细节。\n\n好的,我们先给坐标系向右下方平移`pointRadius`,最后求得结果再加上`pointRadius`就好了。伪代码如下:\n\n```javascript\n// realx:真实的x坐标\n// realy:真实的y坐标\n// resultx:平移后求取的x坐标\n// resultx:平移后求取的y坐标\n// pointRadius 圆点半径\nrealx = resultx + pointRadius\nrealy = resulty + pointRadius\n```\n\n求解坐标的思路大概如下,分四个范围判断,得出求解公式,应该还可以化简,不过我数学太菜了,先这样吧。\n\n```javascript\ngetPositionsByDeg(deg) {\n let x = 0;\n let y = 0;\n if (deg >= 0 && deg <= 90) {\n // 0~90度\n x = this.circleRadius * (1 + Math.cos(this.deg2Arc(deg)))\n y = this.circleRadius * (1 + Math.sin(this.deg2Arc(deg)))\n } else if (deg > 90 && deg <= 180) {\n // 90~180度\n x = this.circleRadius * (1 - Math.cos(this.deg2Arc(180 - deg)))\n y = this.circleRadius * (1 + Math.sin(this.deg2Arc(180 - deg)))\n } else if (deg > 180 && deg <= 270) {\n // 180~270度\n x = this.circleRadius * (1 - Math.sin(this.deg2Arc(270 - deg)))\n y = this.circleRadius * (1 - Math.cos(this.deg2Arc(270 - deg)))\n } else {\n // 270~360度\n x = this.circleRadius * (1 + Math.cos(this.deg2Arc(360 - deg)))\n y = this.circleRadius * (1 - Math.sin(this.deg2Arc(360 - deg)))\n }\n return { x, y }\n}\n```\n\n最后再补上偏移值即可。\n\n```javascript\nconst pointPosition = this.getPositionsByDeg(nextDeg);\nctx.arc(pointPosition.x + this.pointRadius, pointPosition.y + this.pointRadius, this.pointRadius, 0, this.deg2Arc(360));\n```\n\n![ 任意角度画弧线和圆点 ](https://qncdn.wbjiang.cn/%E4%BB%BB%E6%84%8F%E8%A7%92%E5%BA%A6%E7%94%BB%E5%BC%A7%E7%BA%BF%E5%92%8C%E5%9C%86%E7%82%B9.png)\n\n这样,一个基本的`canvas`环形进度条就成型了。\n\n# 动画展示\n\n静态的东西逼格自然是不够的,因此我们需要再搞点动画效果装装逼。\n\n## 基础动画\n\n我们先简单实现一个线性的动画效果。基本思路是把开始角度和结束角度的差值分为`N`段,利用`window.requestAnimationFrame`依次执行动画。\n\n比如从`30°`到`90°`,我给它分为6段,每次画`10°`。要注意`canvas`画这种动画过程一般是要重复地清空画布并重绘的,所以第一次我画的弧线范围就是`30°~40°`,第二次我画的弧线范围就是`30°~50°`,以此类推......\n\n基本的代码结构如下,具体代码请参考[vue-awesome-progress]( https://github.com/cumt-robin/vue-awesome-progress ) `v1.1.0`版本,如果顺手帮忙点个`star`也是极好的。\n\n```javascript\nanimateDrawArc(canvas, ctx, startDeg, endDeg, nextDeg, step) {\n window.requestAnimationFrame(() => {\n // 清空画布\n ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight);\n // 求下一个目标角度\n nextDeg = this.getTargetDeg(nextDeg || startDeg, endDeg, step);\n // 画圆环\n // 画文字\n // 画进度弧线\n // 画进度圆点\n if (nextDeg !== endDeg) {\n // 满足条件继续调用动画,否则结束动画\n this.animateDrawArc(canvas, ctx, startDeg, endDeg, nextDeg, step)\n }\n }\n}\n```\n\n![线性动画](https://qncdn.wbjiang.cn/%E7%8E%AF%E5%BD%A2%E8%BF%9B%E5%BA%A6%E6%9D%A1%E6%99%AE%E9%80%9A%E5%8A%A8%E7%94%BB.gif)\n\n## 缓动效果\n\n线性动画显得有点单调,可操作性不大,因此我考虑引入贝塞尔缓动函数`easing`,并且支持传入动画执行时间周期`duration`,增强了可定制性,使用体验更好。这里不列出实现代码了,请前往[vue-awesome-progress]( https://github.com/cumt-robin/vue-awesome-progress )查看。\n\n```html\n\n\n\n\n// 省略部分...\n\n\n\n\n```\n\n![环形进度条缓动效果](https://qncdn.wbjiang.cn/%E7%8E%AF%E5%BD%A2%E8%BF%9B%E5%BA%A6%E6%9D%A1%E7%BC%93%E5%8A%A8%E6%95%88%E6%9E%9C.gif)\n\n可以看到,当传入不同的动画周期`duration`和缓动参数`easing`时,动画效果各异,完全取决于使用者自己。\n\n# 其他效果\n\n当然根据组件支持的属性,我们也可以定制出其他效果,比如不显示文字,不显示圆点,弧线线宽与圆环线宽一样,不使用渐变色,不需要动画,等等。我们后续也会考虑支持更多能力,比如控制进度,数字动态增长等!具体使用方法,请参考[vue-awesome-progress]( https://github.com/cumt-robin/vue-awesome-progress )。\n\n![其他效果案例](https://qncdn.wbjiang.cn/%E7%8E%AF%E5%BD%A2%E8%BF%9B%E5%BA%A6%E6%9D%A1%E5%85%B6%E4%BB%96%E6%95%88%E6%9E%9C%E6%A1%88%E4%BE%8B.gif)\n\n# 更新日志\n\n**2020年04月10日更新**\n\n支持进度控制,只需要修改组件的属性值`percentage`即可。\n![进度控制](https://s1.ax1x.com/2020/04/10/GIvQCF.gif)\n\n------\n**2019年11月10日更新**\n\n由于我从业务场景出发做了这个组件,没有考虑到大部分场景都是传百分比控制进度的,因此在`v1.4.0`版本做了如下修正:\n\n1. 废弃`angle-range`,改用`percentage`控制进度,同时提供`start-deg`属性控制起始角度;\n\n2. `with-gradient`改为`use-gradient`\n\n3. 通过`show-text`控制是否显示进度文字\n\n4. 支持通过`format`函数自定义显示文字的规则\n\n\n![v1.4.0版本效果](https://qncdn.wbjiang.cn/v1.4.0%E6%95%88%E6%9E%9C%E5%9B%BE.gif)\n\n# 结语\n\n写完这个组件有让我感觉到,程序员最终不是输给了代码和技术的快速迭代,而是输给了自己的逻辑思维能力和数学功底。就[vue-awesome-progress]( https://github.com/cumt-robin/vue-awesome-progress )这个组件而言,根据这个思路,我们也能迅速开发出适用于`React`,`Angular`以及其他框架生态下的组件。工作三年有余,接触了不少框架和技术,经历了`MVVM`,`Hybrid`,`小程序`,`跨平台`,`大前端`,` serverless `的大火,也时常感慨“学不动了”,在这个快速演进的代码世界里常常感到失落。好在自己还没有丢掉分析问题的能力,而不仅仅是调用各种`API`和插件,这可能是程序员最宝贵的财富吧。前路坎坷,我辈当不忘初心,愿你出走半生,归来仍是少年!\n\n------', '2019-11-09 12:25:01', '2024-08-11 00:51:54', 1, 82, 0, '周末好,今天给大家带来一款接地气的环形进度条组件vue-awesome-progress。', 'https://qncdn.wbjiang.cn/vue-awesome-progress.png', 0, 0); -INSERT INTO `article` VALUES (197, '因为new Date(),我给IE跪了', '处理日期格式是日常工作中的常事,我们经常会对日期字符串和日期对象之间进行转换。今天在`IE`浏览器就踩了这么一个日期转换的坑。\n\n# new Date()的坑\n\n后端返回的日期字符串格式为:`yyyy-MM-dd HH:mm:ss`,看到这个格式,大部分人都会觉得这应该是标准格式吧,我也是这么认为的,觉得没有任何兼容问题。转换语句如下:\n\n```javascript\nvar str2DateObj = new Date(\'2019-11-04 10:10:10\')\nconsole.log(str2DateObj)\n// 输出:VM796:2 Mon Nov 04 2019 10:10:10 GMT+0800 (中国标准时间)\n```\n\n但是`IE`就是这么`diao`,我就不支持这个格式。\n\n```javascript\nvar str2DateObj = new Date(\'2019-11-04 10:10:10\')\nconsole.log(str2DateObj)\n[date] Invalid Date[date] Invalid Date\n```\n\n# 解决方案\n\n## 自行解析\n\n将得到的日期字符串进行拆分解析,分别得到年月日时分秒,然后再`new Date`\n\n```javascript\n// 注意,月是从0开始的\nnew Date(2019, 10, 4, 10, 10, 10)\n```\n\n## 借助外力\n\n正好项目也用了`moment`这个日期时间库,那就交给它处理吧。\n\n```javascript\n// no problem\nmoment(\'2019-11-04 10:10:10\')\n```', '2019-11-15 09:50:00', '2024-08-16 14:16:29', 1, 40, 0, '处理日期格式是日常工作中的常事,我们经常会对日期字符串和日期对象之间进行转换。今天在IE浏览器就踩了这么一个日期转换的坑。', 'https://qncdn.wbjiang.cn/IE%E4%BD%A0%E5%88%AB%E8%B7%91.jpg', 0, 0); -INSERT INTO `article` VALUES (198, '解决办公IP变化后git无法推送远程仓库的问题', '最近公司乔迁新址,在提交代码时遇到了无法`git push`的问题。报错如下:\n\n```\nThe RSA host key for github.com has changed,\nand the key for the corresponding IP address 42.243.156.48\nis unknown. This could either mean that\nDNS SPOOFING is happening or the IP address for the host\nand its host key have changed at the same time.\n```\n\n经检查,`ssh`密钥对是没有问题的,问题出在了`known_hosts`文件,办公`ip`变化了,而`known_hosts`中保留的是原来的`ip`,导致不识别当前`ip`而验证失败。\n\n解决方法也很简单,首先找到`.ssh`目录,我的是\n\n```\nC:\\Users\\Jiang.Wenbin\\.ssh\n```\n\n我们删除掉`known_hosts`文件,然后打开`git bash`,视个人情况选择性输入如下命令:\n\n```shell\n// 连接github\nssh -T git@github.com\n// 连接gitee\nssh -T git@gitee.com\n// 连接coding.net\nssh -T git@git.coding.net\n```\n\n在弹出询问后输入`yes`即可。\n\n这里在连接`github`时比较特殊,遇到了一个报错\n\n```\ngit@github.com: Permission denied (publickey,gssapi-keyex,gssapi-with-mic,password).\n```\n\n其实是我开启了网络代理或者`fanqiang`工具引起的,关闭后正常了。\n\n再次`git push`代码就没问题了。', '2019-11-17 15:54:24', '2024-08-14 05:40:08', 1, 53, 0, '最近公司乔迁新址,在提交代码时遇到了无法git push的问题。', 'https://qncdn.wbjiang.cn/abstract_city_600x400.png', 0, 0); -INSERT INTO `article` VALUES (199, 'Git多个远程仓库不同步时的补救办法', '`git`本地仓库是可以与多个远程仓库关联的,如果想知道怎么配置,请参考[Git如何使用多个托管平台管理代码](http://hexo.wbjiang.cn/干货!Git-如何使用多个托管平台管理代码.html) 。\n\n当`git remote`关联了多个远程仓库时,总会遇到一些问题。今天就遇到了两个远程仓库不一致导致无法`push`的情况。\n\n\n# 远程仓库间出现差异\n\n大概情况是这样的,我是一个本地仓库关联了`github`和`gitee`两个远程仓库。\n\n```shell\ngit remote add all git@github.com:cumt-robin/BlogFrontEnd.git\ngit remote set-url --add all git@gitee.com:tusi/BlogFrontEnd.git\n```\n\n由于不小心在远程仓库`gitee`上手动修改了`README.md`文件,导致两个远程仓库出现了差异。所以当我在本地完成了一部分功能,准备提交到远程仓库时,出现了报错。\n\n```shell\n$ git push all --all\nEverything up-to-date\nTo gitee.com:tusi/BlogFrontEnd.git\n ! [rejected] master -> master (fetch first)\nerror: failed to push some refs to \'git@gitee.com:tusi/BlogFrontEnd.git\'\nhint: Updates were rejected because the remote contains work that you do\nhint: not have locally. This is usually caused by another repository pushing\nhint: to the same ref. You may want to first integrate the remote changes\nhint: (e.g., \'git pull ...\') before pushing again.\nhint: See the \'Note about fast-forwards\' in \'git push --help\' for details.\n```\n\n# 解决方案\n\n由于是`gitee`的仓库多修改了一点东西,因此在本地再加一个`remote`,单独关联`gitee`。\n\n```shell\n$ git remote add gitee git@gitee.com:tusi/BlogFrontEnd.git\n```\n\n将`gitee`的代码拉到本地`master`。\n\n```shell\n$ git pull gitee master\nremote: Enumerating objects: 1, done.\nremote: Counting objects: 100% (1/1), done.\nremote: Total 1 (delta 0), reused 0 (delta 0)\nUnpacking objects: 100% (1/1), done.\nFrom gitee.com:tusi/BlogFrontEnd\n * branch master -> FETCH_HEAD\n * [new branch] master -> gitee/master\nAlready up to date!\nMerge made by the \'recursive\' strategy.\n```\n\n再将本地`master`推送到远程`all`。\n\n```shell\n$ git push all --all\nEnumerating objects: 2, done.\nCounting objects: 100% (2/2), done.\nDelta compression using up to 6 threads\nCompressing objects: 100% (2/2), done.\nWriting objects: 100% (2/2), 499 bytes | 499.00 KiB/s, done.\nTotal 2 (delta 0), reused 0 (delta 0)\nTo github.com:cumt-robin/BlogFrontEnd.git\n 1557ece..8391333 master -> master\nEnumerating objects: 2, done.\nCounting objects: 100% (2/2), done.\nDelta compression using up to 6 threads\nCompressing objects: 100% (2/2), done.\nWriting objects: 100% (2/2), 917 bytes | 917.00 KiB/s, done.\nTotal 2 (delta 0), reused 0 (delta 0)\nremote: Powered By Gitee.com\nTo gitee.com:tusi/BlogFrontEnd.git\n 8912ff5..8391333 master -> master\n```\n\n问题得以解决!\n\n------', '2019-11-19 18:40:23', '2024-07-25 08:06:35', 1, 34, 0, '当git remote关联了多个远程仓库时,总会遇到一些问题。今天就遇到了两个远程仓库不一致导致无法push的情况。', 'https://qncdn.wbjiang.cn/git%E5%A4%9A%E4%BB%93%E5%BA%93%E5%B7%AE%E5%BC%82.png', 0, 0); -INSERT INTO `article` VALUES (200, '前端API层架构,也许你做得还不够', '上午好,今天为大家分享下个人对于前端`API`层架构的一点经验和看法。架构设计是一条永远走不完的路,没有最好,只有更好。这个道理适用于软件设计的各个场景,前端`API`层的设计也不例外,如果您觉得在调用接口时还存在诸多槽点,那就说明您的接口层架构还待优化。今天我以`vue + axios`为例,为大家梳理下我的一些经历和设想。\n\n# 石器时代,痛苦\n\n直接调用`axios`,真的痛苦,每个调用的地方都要进行响应状态的判断,冗余代码超级多。\n\n```javascript\nimport axios from \"axios\"\n\naxios.get(\'/usercenter/user/page?pageNo=1&pageSize=10\').then(res => {\n const data = res.data\n // 判断请求状态,success字段为true代表成功,视前后端约束而定\n if (data.success) {\n // 结果成功后的业务代码\n } else {\n // 结果失败后的业务代码\n }\n})\n```\n\n看起来确实很难受,每调用一次接口,就有这么多重复的工作!\n\n# 青铜器时代,中规中矩\n\n为了解决直接调用`axios`的痛点,我们一般会利用`Promise`对`axios`二次封装,对接口响应状态进行集中判断,对外暴露`get`, `post`, `put`, `delete`等`http`方法。\n\n## axios二次封装\n\n```javascript\nimport axios from \"axios\"\nimport router from \"@/router\"\nimport { BASE_URL } from \"@/router/base-url\"\nimport { errorMsg } from \"@/utils/msg\";\nimport { stringify } from \"@/utils/helper\";\n// 创建axios实例\nconst v3api = axios.create({\n baseURL: process.env.BASE_API,\n timeout: 10000\n});\n// axios实例默认配置\nv3api.defaults.headers.common[\'Content-Type\'] = \'application/x-www-form-urlencoded\';\nv3api.defaults.transformRequest = data => {\n return stringify(data)\n}\n// 返回状态拦截,进行状态的集中判断\nv3api.interceptors.response.use(\n response => {\n const res = response.data;\n if (res.success) {\n return Promise.resolve(res)\n } else {\n // 内部错误码处理\n if (res.code === 1401) {\n errorMsg(res.message || \'登录已过期,请重新登录!\')\n router.replace({ path: `${BASE_URL}/login` })\n } else {\n // 默认的错误提示\n errorMsg(res.message || \'网络异常,请稍后重试!\')\n }\n return Promise.reject(res);\n }\n },\n error => {\n if (/timeout\\sof\\s\\d+ms\\sexceeded/.test(error.message)) {\n // 超时\n errorMsg(\'网络出了点问题,请稍后重试!\')\n }\n if (error.response) {\n // http状态码判断\n switch (error.response.status) {\n // http status handler\n case 404:\n errorMsg(\'请求的资源不存在!\')\n break\n case 500:\n errorMsg(\'内部错误,请稍后重试!\')\n break\n case 503:\n errorMsg(\'服务器正在维护,请稍等!\')\n break\n }\n }\n return Promise.reject(error.response)\n }\n)\n\n// 处理get请求\nconst get = (url, params, config = {}) => v3api.get(url, { ...config, params })\n// 处理delete请求,为了防止和关键词delete冲突,方法名定义为deletes\nconst deletes = (url, params, config = {}) => v3api.delete(url, { ...config, params })\n// 处理post请求\nconst post = (url, params, config = {}) => v3api.post(url, params, config)\n// 处理put请求\nconst put = (url, params, config = {}) => v3api.put(url, params, config)\nexport default {\n get,\n deletes,\n post,\n put\n}\n```\n\n## 调用者不再判断请求状态\n\n```javascript\nimport api from \"@/api\";\n\nmethods: {\n getUserPageData() {\n api.get(\'/usercenter/user/page?pageNo=1&pageSize=10\').then(res => {\n // 状态已经集中判断了,这里直接写成功的逻辑\n // 业务代码......\n const result = res.result;\n }).catch(res => {\n // 失败的情况写在catch中\n })\n }\n}\n```\n\n## async/await改造\n\n使用语义化的异步函数\n\n```javascript\nmethods: {\n async getUserPageData() {\n try {\n const res = await api.get(\'/usercenter/user/page?pageNo=1&pageSize=10\') \n // 业务代码......\n const { result } = res;\n } catch(error) {\n // 失败的情况写在catch中\n }\n }\n}\n```\n\n## 存在的问题\n\n- 语义化程度有限,调用接口还是需要查询接口`url`\n- 前端`api`层难以维护,如后端接口发生改动,前端每处都需要大改。\n- 如果`UI`组件的数据模型与后端接口要求的数据结构存在差异,每处调用接口前都需要进行数据处理,抹平差异,比如`[1,2,3]`转`1,2,3`这种(当然,这只是最简单的一个例子)。这样如果数据处理不慎,调用者出错几率太高!\n- 难以满足特殊化场景,举个例子,一个查询的场景,后端要求,如果输入了搜索关键词`keyword`,必须调用`/user/search`接口,如果没有输入关键词,只能调用`/user/page`接口。如果每个调用者都要判断是不是输入了关键词,再决定调用哪个接口,你觉得出错几率有多大,用起来烦不烦?\n- 产品说,这些场景需要优化,默认按创建时间降序排序。我擦,又一个个改一遍?\n- ......\n\n那么怎么解决这些问题呢?请耐心接着看......\n\n# 铁器时代,it\'s cool\n\n我想到的方案是在底层封装和调用者之间再增加一层`API`适配层(适配层,取量身定制之意),在适配层做统一处理,包括参数处理,请求头处理,特殊化处理等,提炼出更语义化的方法,让调用者“傻瓜式”调用,不再为了查找接口`url`和处理数据结构这些重复的工作而烦恼,把`ViewModel`层绑定的数据模型直接丢给适配层统一处理。\n\n## 对齐微服务架构\n\n 首先,为了对齐后端微服务架构,在前端将`API`调用分为三个模块。 \n\n```\n├─api\n index.js axios底层封装\n ├─base 负责调用基础服务,basecenter\n ├─iot 负责调用物联网服务,iotcenter\n └─user 负责调用用户相关服务,usercenter\n```\n\n 每个模块下都定义了统一的微服务命名空间,例如`/src/api/user/index.js`: \n\n```javascript\nexport const namespace = \'usercenter\';\n```\n\n## 特性模块\n\n每个功能特性都有独立的`js`模块,以角色管理相关接口为例,模块是`/src/api/user/role.js` \n\n```javascript\nimport api from \'../index\'\nimport { paramsFilter } from \"@/utils/helper\";\nimport { namespace } from \"./index\"\nconst feature = \'role\'\n\n// 添加角色\nexport const addRole = params => api.post(`/${namespace}/${feature}/add`, paramsFilter(params));\n// 删除角色\nexport const deleteRole = id => api.deletes(`/${namespace}/${feature}/delete`, { id });\n// 更新角色\nexport const updateRole = params => api.put(`/${namespace}/${feature}/update`, paramsFilter(params));\n// 条件查询角色\nexport const findRoles = params => api.get(`/${namespace}/${feature}/find`, paramsFilter(params));\n// 查询所有角色,不传参调用find接口代表查询所有角色\nexport const getAllRoles = () => findRoles();\n// 获取角色详情\nexport const getRoleDetail = id => api.get(`/${namespace}/${feature}/detail`, { id });\n// 分页查询角色\nexport const getRolePage = params => api.get(`/${namespace}/${feature}/page`, paramsFilter(params));\n// 搜索角色\nexport const searchRole = params => params.keyword ? api.get(`/${namespace}/${feature}/search`, paramsFilter(params)) : getRolePage(params);\n```\n\n- 每一条接口都根据`RESTful`风格,调用增(`api.post`)删(`api.deletes`)改(`api.put`)查(`api.get`)的底层方法,对外输出语义化方法。\n- 调用的`url`由三部分组成,格式:`/微服务命名空间/特性命名空间/方法`\n- 接口适配层函数命名规范:\n\n- - 新增:`addXXX`\n - 删除:`deleteXXX`\n - 更新:`updateXXX`\n - 根据ID查询记录:`getXXXDetail`\n - 条件查询一条记录:`findOneXXX`\n - 条件查询:`findXXXs`\n - 查询所有记录:`getAllXXXs`\n - 分页查询:`getXXXPage`\n - 搜索:`searchXXX`\n - 其余个性化接口根据语义进行命名\n\n## 解决问题\n\n- 语义化程度更高,配合`vscode`的代码提示功能,用起来不要太爽!\n\n- 迅速响应接口改动,适配层统一处理\n\n- 集中进行数据处理(对于公用的数据处理,我们用`paramsFilter`解决,对于特殊的情况,再另行处理),调用者安心做业务即可\n\n- 满足特殊场景,佛系应对后端和产品朋友\n\n - 针对上节提到的关键字查询场景,我们在适配层通过在入参中判断是否有`keyword`字段,决定调用`search`还是`page`接口。对外我们只需暴露`searchRole`方法,调用者只需要调用`searchRole`方法即可,无需做其他考虑。\n\n ```javascript\n export const searchRole = params => params.keyword ? api.get(`/${namespace}/${feature}/search`, paramsFilter(params)) : getRolePage(params);\n ```\n\n - 针对产品突然加的排序需求,我们可以在适配层去做默认入参的处理。\n\n 首先,我们新建一个专门管理默认参数的`js`,如`src/api/default-options.js`\n\n ```javascript\n // 默认按创建时间降序的参数对象\n export const SORT_BY_CREATETIME_OPTIONS = {\n sortField: \'createTime\',\n // desc代表降序,asc是升序\n sortType: \'desc\'\n }\n ```\n\n 接着,我们在接口适配层做集中化处理\n\n ```javascript\n import api from \'../index\'\n import { SORT_BY_CREATETIME_OPTIONS } from \"../default-options\"\n import { paramsFilter } from \"@/utils/helper\";\n import { namespace } from \"./index\"\n const feature = \'role\'\n \n export const getRolePage = params => api.get(`/${namespace}/${feature}/page`, paramsFilter({ ...SORT_BY_CREATETIME_OPTIONS, ...params }));\n ```\n\n `SORT_BY_CREATETIME_OPTIONS`放在前面,是为了满足如果出现其他排序需求,调用者传入的排序字段能覆盖掉默认参数。\n\n## mock先行\n\n一个完善的`API`层设计,肯定是离不开`mock`的。在后端提供接口之前,前端必须通过模拟数据并行开发,否则进度无法保证。那么如何设计一个跟真实接口契合度高的`mock`系统呢?我这里简单做下分享。\n\n- 首先,创建`mock`专用的`axios`实例\n\n我们在`src`目录下新建`mock`目录,并在`src/mock/index.js`简单封装一个`axios`实例\n\n```javascript\n// 仅限模拟数据使用\nimport axios from \"axios\"\nconst mock = axios.create({\n baseURL: \'\'\n});\n// 返回状态拦截\nmock.interceptors.response.use(\n response => {\n return Promise.resolve(response.data)\n },\n error => {\n return Promise.reject(error.response)\n }\n)\n\nexport default mock\n```\n\n- `mock`同样也要分模块,以`usercenter`微服务下的角色管理`mock`接口为例\n\n```\n├─mock\n index.js mock底层axios封装\n ├─user 负责调用基础服务,usercenter\n ├─role\n ├─index.js\n```\n\n我们在`src/mock/user/role/index.js`中简单模拟一个获取所有角色的接口`getAllRoles`\n\n```javascript\nimport mock from \"@/mock\";\n\nexport const getAllRoles = () => mock.get(\'/static/mock/user/role/getAllRoles.json\')\n```\n\n可以看到,我们是在`mock`接口中获取了`static/mock`目录下的`json`数据。因此我们需要根据接口文档或者约定好的数据结构准备好`getAllRoles.json`数据\n\n```\n{\n \"success\": true,\n \"result\": {\n \"pageNo\": 1,\n \"pageSize\": 10,\n \"total\": 2,\n \"list\": [\n {\n \"id\": 1,\n \"createTime\": \"2019-11-19 12:53:05\",\n \"updateTime\": \"2019-12-03 09:53:41\",\n \"name\": \"管理员\",\n \"code\": \"管理员\",\n \"description\": \"一个拥有部分权限的管理员角色\",\n \"sort\": 1,\n \"menuIds\": \"789,2,55,983,54\",\n \"menuNames\": \"数据字典, 后台, 账户信息, 修改密码, 账户中心\"\n },\n {\n \"id\": 2,\n \"createTime\": \"2019-11-27 17:18:54\",\n \"updateTime\": \"2019-12-01 19:14:30\",\n \"name\": \"前台测试\",\n \"code\": \"前台测试\",\n \"description\": \"一个拥有部分权限的前台测试角色\",\n \"sort\": 2,\n \"menuIds\": \"15,4,1\",\n \"menuNames\": \"油耗统计, 车联网, 物联网监管系统\"\n }\n ]\n },\n \"message\": \"请求成功\",\n \"code\": 0\n}\n```\n\n- 我们来看看`mock`是怎么做的\n\n先看下真实接口的调用方式\n\n```javascript\nimport { getAllRoles } from \"@/api/user/role\";\n\ncreated() {\n this.getAllRolesData()\n},\nmethods: {\n async getAllRolesData() {\n const res = await getAllRoles()\n console.log(res)\n }\n}\n```\n\n那么`mock`时怎么做呢?非常简单,只要将`mock`中提供的方法替代掉`api`提供的方法即可。\n\n```javascript\n// import { getAllRoles } from \"@/api/user/role\";\nimport { getAllRoles } from \"@/mock/user/role\";\n```\n\n可以看到,这种`mock`方式与调用真实接口的契合度还是挺高的,正式调试接口时,只需将注释的代码调整即可,过渡非常平滑!\n\n- 注意,在生产环境下,为了防止打包时将`static/mock`目录下的内容`copy`到`dist`目录下,我们需要配置下`CopyWebpackPlugin`,以`vue-cli@2`为例,我们修改`webpack.base.conf.js`即可。\n\n```javascript\nconst devMode = process.env.NODE_ENV === \'development\';\n\nnew CopyWebpackPlugin([\n {\n from: path.resolve(__dirname, \'../static\'),\n to: devMode ? config.dev.assetsSubDirectory : config.build.assetsSubDirectory,\n ignore: devMode ? \'\' : \'mock/**/*\'\n }\n])\n```\n\n# 蒸汽时代,真香\n\n下一步的设想,使用类型安全的`typescript`,让前端`API`层真正做到面向接口文档编程,规范入参,出参,可选参数,等等,提高可维护性,在编码阶段就大大降低出错几率。虽然还在重构阶段,但是我想说,重拾`typescript`是真香,突然怀念使用`Angular`的那两年了,期待`vue3.0`能将`typescript`结合得更加完美......\n\n# 电气时代,更多畅想\n\n未来还有无限可能,面对日渐复杂和多样化的业务场景,我们会提炼出更好的架构和设计模式。目前有一个不成熟的设想,是否能在接口设计上做到更规范化,后端输出接口文档的同时,提炼出`API json`之类的数据结构?前端拿到`API json`,通过`nodejs`文件编程的能力,自动化生成前端接口层代码,解放双手。 \n\n# 结语\n\n当然,以上只是我的一点点经验和设想,是在我能力范围内能想到的东西,希望能帮助到一些有需要的同学。如果大佬们有更好的经验,可以指点一二。\n\n------\n[首发链接](https://juejin.im/post/5de7169451882512454b18d8)\n\n------\n\n往期精彩:\n\n- [用初中数学知识撸一个canvas环形进度条]( https://juejin.im/post/5dc626125188253aec025a60 )', '2019-12-04 10:18:33', '2024-08-11 21:27:24', 1, 91, 0, '上午好,今天为大家分享下个人对于前端API层架构的一点经验和看法。架构设计是一条永远走不完的路,没有最好,只有更好。这个道理适用于软件设计的各个场景,前端API层的设计也不例外。', 'https://qncdn.wbjiang.cn/%E5%89%8D%E7%AB%AFAPI%E5%B1%82%E6%9E%B6%E6%9E%84.png', 0, 0); -INSERT INTO `article` VALUES (201, '从一道面试题简单谈谈发布订阅和观察者模式', '今天的话题是`javascript`中常被提及的「发布订阅模式和观察者模式」,提到这,我不由得想起了一次面试。记得在去年的一次求职面试过程中,面试官问我,“你在项目中是怎么处理非父子组件之间的通信的?”。我答道,“有用到`vuex`,有的场景也会用`EventEmitter2`”。面试官继续问,“那你能手写代码,实现一个简单的`EventEmitter`吗?”\n\n# 手写EventEmitter\n\n我犹豫了一会儿,想到使用`EventEmitter2`时,主要是用`emit`发事件,用`on`监听事件,还有`off`销毁事件监听者,`removeAllListeners`销毁指定事件的所有监听者,还有`once`之类的方法。考虑到时间关系,我想着就先实现发事件,监听事件,移除监听者这几个功能。当时可能有点紧张,不过有惊无险,在面试官给了一点提示后,顺利地写出来了!现在把这部分代码也记下来。\n\n```javascript\nclass EventEmitter {\n constructor() {\n // 维护事件及监听者\n this.listeners = {}\n }\n /**\n * 注册事件监听者\n * @param {String} type 事件类型\n * @param {Function} cb 回调函数\n */\n on(type, cb) {\n if (!this.listeners[type]) {\n this.listeners[type] = []\n }\n this.listeners[type].push(cb)\n }\n /**\n * 发布事件\n * @param {String} type 事件类型\n * @param {...any} args 参数列表,把emit传递的参数赋给回调函数\n */\n emit(type, ...args) {\n if (this.listeners[type]) {\n this.listeners[type].forEach(cb => {\n cb(...args)\n })\n }\n }\n /**\n * 移除某个事件的一个监听者\n * @param {String} type 事件类型\n * @param {Function} cb 回调函数\n */\n off(type, cb) {\n if (this.listeners[type]) {\n const targetIndex = this.listeners[type].findIndex(item => item === cb)\n if (targetIndex !== -1) {\n this.listeners[type].splice(targetIndex, 1)\n }\n if (this.listeners[type].length === 0) {\n delete this.listeners[type]\n }\n }\n }\n /**\n * 移除某个事件的所有监听者\n * @param {String} type 事件类型\n */\n offAll(type) {\n if (this.listeners[type]) {\n delete this.listeners[type]\n }\n }\n}\n// 创建事件管理器实例\nconst ee = new EventEmitter()\n// 注册一个chifan事件监听者\nee.on(\'chifan\', function() { console.log(\'吃饭了,我们走!\') })\n// 发布事件chifan\nee.emit(\'chifan\')\n// 也可以emit传递参数\nee.on(\'chifan\', function(address, food) { console.log(`吃饭了,我们去${address}吃${food}!`) })\nee.emit(\'chifan\', \'三食堂\', \'铁板饭\') // 此时会打印两条信息,因为前面注册了两个chifan事件的监听者\n\n// 测试移除事件监听\nconst toBeRemovedListener = function() { console.log(\'我是一个可以被移除的监听者\') }\nee.on(\'testoff\', toBeRemovedListener)\nee.emit(\'testoff\')\nee.off(\'testoff\', toBeRemovedListener)\nee.emit(\'testoff\') // 此时事件监听已经被移除,不会再有console.log打印出来了\n\n// 测试移除chifan的所有事件监听\nee.offAll(\'chifan\')\nconsole.log(ee) // 此时可以看到ee.listeners已经变成空对象了,再emit发送chifan事件也不会有反应了\n```\n\n有了这个自己写的简单版本的`EventEmitter`,我们就不用依赖第三方库啦。对了,`vue`也可以帮我们做这样的事情。\n\n```javascript\nconst ee = new Vue();\nee.$on(\'chifan\', function(address, food) { console.log(`吃饭了,我们去${address}吃${food}!`) })\nee.$emit(\'chifan\', \'三食堂\', \'铁板饭\')\n```\n\n所以我们可以单独`new`一个`Vue`的实例,作为事件管理器导出给外部使用。想测试的朋友可以直接打开`vue`官网,在控制台试试,也可以在自己的`vue`项目中实践下哦。\n\n# 发布订阅模式\n\n其实仔细看看,`EventEmitter`就是一个典型的发布订阅模式,实现了事件调度中心。发布订阅模式中,包含发布者,事件调度中心,订阅者三个角色。我们刚刚实现的`EventEmitter`的一个实例`ee`就是一个事件调度中心,发布者和订阅者是松散耦合的,互不关心对方是否存在,他们关注的是事件本身。发布者借用事件调度中心提供的`emit`方法发布事件,而订阅者则通过`on`进行订阅。\n\n如果还不是很清楚的话,我们把代码换下单词,是不是变得容易理解一点呢?\n\n```javascript\nclass PubSub {\n constructor() {\n // 维护事件及订阅行为\n this.events = {}\n }\n /**\n * 注册事件订阅行为\n * @param {String} type 事件类型\n * @param {Function} cb 回调函数\n */\n subscribe(type, cb) {\n if (!this.events[type]) {\n this.events[type] = []\n }\n this.events[type].push(cb)\n }\n /**\n * 发布事件\n * @param {String} type 事件类型\n * @param {...any} args 参数列表\n */\n publish(type, ...args) {\n if (this.events[type]) {\n this.events[type].forEach(cb => {\n cb(...args)\n })\n }\n }\n /**\n * 移除某个事件的一个订阅行为\n * @param {String} type 事件类型\n * @param {Function} cb 回调函数\n */\n unsubscribe(type, cb) {\n if (this.events[type]) {\n const targetIndex = this.events[type].findIndex(item => item === cb)\n if (targetIndex !== -1) {\n this.events[type].splice(targetIndex, 1)\n }\n if (this.events[type].length === 0) {\n delete this.events[type]\n }\n }\n }\n /**\n * 移除某个事件的所有订阅行为\n * @param {String} type 事件类型\n */\n unsubscribeAll(type) {\n if (this.events[type]) {\n delete this.events[type]\n }\n }\n}\n```\n\n## 画图分析\n\n最后,我们画个图加深下理解:\n\n![发布订阅模式图解](https://qncdn.wbjiang.cn/%E5%8F%91%E5%B8%83%E8%AE%A2%E9%98%85.png)\n\n## 特点\n\n- 发布订阅模式中,对于发布者`Publisher`和订阅者`Subscriber`没有特殊的约束,他们好似是匿名活动,借助事件调度中心提供的接口发布和订阅事件,互不了解对方是谁。\n- 松散耦合,灵活度高,常用作事件总线\n- 易理解,可类比于`DOM`事件中的`dispatchEvent`和`addEventListener`。\n\n## 缺点\n\n- 当事件类型越来越多时,难以维护,需要考虑事件命名的规范,也要防范数据流混乱。\n\n# 观察者模式\n\n观察者模式与发布订阅模式相比,耦合度更高,通常用来实现一些响应式的效果。在观察者模式中,只有两个主体,分别是目标对象`Subject`,观察者`Observer`。\n\n- 观察者需`Observer`要实现`update`方法,供目标对象调用。`update`方法中可以执行自定义的业务代码。\n- 目标对象`Subject`也通常被叫做被观察者或主题,它的职能很单一,可以理解为,它只管理一种事件。`Subject`需要维护自身的观察者数组`observerList`,当自身发生变化时,通过调用自身的`notify`方法,依次通知每一个观察者执行`update`方法。\n\n按照这种定义,我们可以实现一个简单版本的观察者模式。\n\n```javascript\n// 观察者\nclass Observer {\n /**\n * 构造器\n * @param {Function} cb 回调函数,收到目标对象通知时执行\n */\n constructor(cb){\n if (typeof cb === \'function\') {\n this.cb = cb\n } else {\n throw new Error(\'Observer构造器必须传入函数类型!\')\n }\n }\n /**\n * 被目标对象通知时执行\n */\n update() {\n this.cb()\n }\n}\n\n// 目标对象\nclass Subject {\n constructor() {\n // 维护观察者列表\n this.observerList = []\n }\n /**\n * 添加一个观察者\n * @param {Observer} observer Observer实例\n */\n addObserver(observer) {\n this.observerList.push(observer)\n }\n /**\n * 通知所有的观察者\n */\n notify() {\n this.observerList.forEach(observer => {\n observer.update()\n })\n }\n}\n\nconst observerCallback = function() {\n console.log(\'我被通知了\')\n}\nconst observer = new Observer(observerCallback)\n\nconst subject = new Subject();\nsubject.addObserver(observer);\nsubject.notify();\n```\n\n## 画图分析\n\n最后也整张图理解下观察者模式:\n\n![观察者模式](https://qncdn.wbjiang.cn/%E8%A7%82%E5%AF%9F%E8%80%85%E6%A8%A1%E5%BC%8F%E5%9B%BE%E8%A7%A3.png)\n\n## 特点\n\n- 角色很明确,没有事件调度中心作为中间者,目标对象`Subject`和观察者`Observer`都要实现约定的成员方法。\n- 双方联系更紧密,目标对象的主动性很强,自己收集和维护观察者,并在状态变化时主动通知观察者更新。\n\n## 缺点\n\n我还没体会到,这里不做评价\n\n# 结语\n\n关于这个话题,网上文章挺多的,观点上可能也有诸多分歧。重复造轮子,纯属帮助自己加深理解。\n\n本人水平有限,以上仅是个人观点,如有错误之处,还请斧正!如果能帮到您理解发布订阅模式和观察者模式,非常荣幸!\n\n如果有兴趣看看我这糟糕的代码,请点击[github](https://github.com/cumt-robin/just-demos),祝大家生活愉快!\n\n------', '2019-12-12 10:47:36', '2024-08-03 20:28:50', 1, 105, 0, '今天的话题是javascript中常被提及的「发布订阅模式和观察者模式」,提到这,我不由得想起了一次面试。', 'https://qncdn.wbjiang.cn/%E5%8F%91%E5%B8%83%E8%AE%A2%E9%98%85&%E8%A7%82%E5%AF%9F%E8%80%85.png', 0, 0); -INSERT INTO `article` VALUES (202, '入门babel,我们需要了解些什么', '说实话,我从工作开始就一直在接触`babel`,然而对于`babel`并没有一个清晰的认识,只知道`babel`是用于编译`javascript`,让开发者能使用超前的`ES6+`语法进行开发。自己配置`babel`的时候,总是遇到很多困惑,下面我就以`babel@7`为例,重新简单认识下`babel`。\n\n# 什么是babel\n\n> Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。\n\n`babel`的配置文件一般是根目录下的`.babelrc`,`babel@7`目前已经支持`babel.config.js`,不妨用`babel.config.js`试试。\n\n# 泰拳警告\n\n![泰拳警告](https://qncdn.wbjiang.cn/%E6%B3%B0%E6%8B%B3%E8%AD%A6%E5%91%8A.jpg)\n\n`babel`提供的基础能力是语法转换,或者叫语法糖转换。比如把箭头函数转为普通的`function`,而对于`ES6`新引入的全局对象是默认不做处理的,如`Promise`, `Map`, `Set`, `Reflect`, `Proxy`等。对于这些全局对象和新的`API`,需要用垫片`polyfill`处理,`core-js`有提供这些内容。\n\n所以`babel`做的事情主要是:\n\n1. 根据你的配置做语法糖解析,转换\n2. 根据你的配置塞入垫片`polyfill`\n\n如果不搞清楚这点,`babel`的文档看起来会很吃力!\n\n# 必须掌握的概念\n\n## plugins\n\n`babel`默认不做任何处理,需要借助插件来完成语法的解析,转换,输出。\n\n插件分为语法插件`Syntax Plugins`和转换插件`Transform Plugins`。\n\n### 语法插件\n\n语法插件仅允许`babel`解析语法,不做转换操作。我们主要关注的是转换插件。\n\n### 转换插件\n\n转换插件,顾名思义,负责的是语法转换。\n\n> 转换插件将启用相应的语法插件,如果启用了某个语法的转换插件,则不必再另行指定相应的语法插件了。\n\n语法转换插件有很多,从`ES3`到`ES2018`,甚至是一些实验性的语法和相关框架生态下的语法,都有相关的插件支持。\n\n语法转换插件主要做的事情有:\n\n利用`@babel/parser`进行词法分析和语法分析,转换为`AST` **-->** 利用`babel-traverse`进行`AST`转换(涉及添加,更新及移除节点等操作) **-->** 利用`babel-generator`生成目标环境`js`代码\n\n### 插件简写\n\n`babel@7`之前的缩写形式是这样的:\n\n```javascript\n// 完整写法\nplugins: [\n \"babel-plugin-transform-runtime\"\n]\n// 简写形式\nplugins: [\n \"transform-runtime\"\n]\n```\n\n而在`babel@7`之后,由于`plugins`都归到了`@babel`目录下,所以简写形式也有所改变:\n\n```java\n// babel@7插件完整写法\nplugins: [\n \"@babel/plugin-transform-runtime\"\n]\n// 简写形式,需要保留目录\nplugins: [\n \"@babel/transform-runtime\"\n]\n```\n\n### 插件开发\n\n我们自己也可以开发插件,官网上的一个非常简单的小例子:\n\n```javascript\nexport default function() {\n return {\n visitor: {\n Identifier(path) {\n const name = path.node.name;\n // reverse the name: JavaScript -> tpircSavaJ\n path.node.name = name\n .split(\"\")\n .reverse()\n .join(\"\");\n },\n },\n };\n}\n```\n\n## presets\n\n`preset`,意为“预设”,其实是一组`plugin`的集合。我的理解是,根据这项配置,`babel`会为你预设(或称为“内置”)好一些`ECMA`标准,草案,或提案下的语法或`API`,甚至是你自己写的一些语法规则。当然,这都是基于`plugin`实现的。\n\n### 官方presets\n\n- [@babel/preset-env](https://babeljs.io/docs/en/babel-preset-env)\n- [@babel/preset-flow](https://babeljs.io/docs/en/babel-preset-flow)\n- [@babel/preset-react](https://babeljs.io/docs/en/babel-preset-react)\n- [@babel/preset-typescript](https://babeljs.io/docs/en/babel-preset-typescript)\n\n### @babel/preset-env\n\n`@babel/preset-env`提供了一种智能的预设,根据配置的`options`来决定支持哪些能力。\n\n我们看看关键的`options`有哪些。\n\n- targets\n\n描述你的项目要支持的目标环境。写法源于开源项目[browserslist](https://github.com/browserslist/browserslist)。这项配置应该根据你需要兼容的浏览器而设置,不必与其他人一模一样。示例如下:\n\n```javascript\n\"targets\": {\n \"browsers\": [\"> 1%\", \"last 2 versions\", \"not ie <= 9\"]\n}\n```\n\n- loose\n\n可以直译为“松散模式”,默认为`false`,即为`normal`模式。简单地说,就是`normal`模式转换出来的代码更贴合`ES6`风格,更严谨;而`loose`模式更像我们平时的写法。以`class`写法举例:\n\n我们先写个简单的`class`:\n\n```javascript\nclass TestBabelLoose {\n constractor(name) {\n this.name = name\n }\n getName() {\n return this.name\n }\n}\n\nnew TestBabelLoose(\'Tusi\')\n```\n\n使用`normal`模式编译得到结果如下:\n\n```javascript\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\nvar TestBabelLoose =\n/*#__PURE__*/\nfunction () {\n function TestBabelLoose() {\n _classCallCheck(this, TestBabelLoose);\n }\n\n _createClass(TestBabelLoose, [{\n key: \"constractor\",\n value: function constractor(name) {\n this.name = name;\n }\n }, {\n key: \"getName\",\n value: function getName() {\n return this.name;\n }\n }]);\n\n return TestBabelLoose;\n}();\n\nnew TestBabelLoose(\'Tusi\');\n```\n\n而使用`loose`模式编译得到结果是这样的,是不是更符合我们用`prototype`实现类的写法?\n\n```javascript\n\"use strict\";\n\nvar TestBabelLoose =\n/*#__PURE__*/\nfunction () {\n function TestBabelLoose() {}\n\n var _proto = TestBabelLoose.prototype;\n\n _proto.constractor = function constractor(name) {\n this.name = name;\n };\n\n _proto.getName = function getName() {\n return this.name;\n };\n\n return TestBabelLoose;\n}();\n\nnew TestBabelLoose(\'Tusi\');\n```\n\n个人推荐配置`loose: false`,当然也要结合项目实际去考量哪种模式更合适。\n\n- modules\n\n可选值有:`\"amd\" | \"umd\" | \"systemjs\" | \"commonjs\" | \"cjs\" | \"auto\" | false`,默认是`auto`\n\n该配置将决定是否把`ES6`模块语法转换为其他模块类型。注意,`cjs`是`commonjs`的别名。\n\n其实我一直有个疑惑,为什么我看到的开源组件中,基本都是设置的`modules: false`?后面终于明白了,原来这样做的目的是把转换模块类型的处理权交给了`webpack`,由`webpack`去处理这项任务。所以,如果你也使用`webpack`,那么设置`modules: false`就没错啦。\n\n- useBuiltIns\n\n可选值有:`\"entry\" | \"usage\" | false`,默认是`false`\n\n该配置将决定`@babel/preset-env`如何去处理`polyfill`\n\n`\"entry\"`\n\n如果`useBuiltIns`设置为`\"entry\"`,我们需要安装`@babel/polyfill`,并且在入口文件引入`@babel/polyfill`,最终会被转换为`core-js`模块和`regenerator-runtime/runtime`。对了,`@babel/polyfill`也不会处理`stage <=3`的提案。\n\n我们用一段包含了`Promise`的代码来做下测试:\n\n```javascript\nimport \"@babel/polyfill\";\n\nclass TestBabelLoose {\n constractor(name) {\n this.name = name\n }\n getName() {\n return this.name\n }\n testPromise() {\n return new Promise(resolve => {\n resolve()\n })\n }\n}\nnew TestBabelLoose(\'Tusi\')\n```\n\n但是编译后,貌似引入了很多`polyfill`啊,一共149个,怎么不是按需引入呢?嗯...你需要往下看了。\n\n```javascript\nimport \"core-js/modules/es6.array.map\";\nimport \"core-js/modules/es6.map\";\nimport \"core-js/modules/es6.promise\";\nimport \"core-js/modules/es7.promise.finally\";\nimport \"regenerator-runtime/runtime\";\n// 此处省略了144个包。。。\n```\n\n`\"usage\"`\n\n如果`useBuiltIns`设置为`\"usage\"`,我们无需安装`@babel/polyfill`,`babel`会根据你实际用到的语法特性导入相应的`polyfill`,有点按需加载的意思。\n\n```javascript\n// 上个例子中,如果改用useBuiltIns: \'usage\',最终转换的结果,只有四个模块\nimport \"core-js/modules/es6.object.define-property\";\nimport \"core-js/modules/es6.promise\";\nimport \"core-js/modules/es6.object.to-string\";\nimport \"core-js/modules/es6.function.name\";\n```\n\n配置`\"usage\"`时,常搭配`corejs`选项来指定`core-js`主版本号\n\n```javascript\nuseBuiltIns: \"usage\",\ncorejs: 3\n```\n\n`false`\n\n如果`useBuiltIns`设置为`false`,`babel`不会自动为每个文件加上`polyfill`,也不会把`import \"@babel/polyfill\"`转为一个个独立的`core-js`模块。\n\n- `@babel/preset-env`还有一些配置,自己慢慢去折腾吧......\n\n### stage-x\n\n`stage-x`描述的是`ECMA`标准相关的内容。根据`TC39`(`ECMA`39号技术专家委员会)的提案划分界限,`stage-x`大致分为以下几个阶段:\n\n- stage-0:`strawman`,还只是一种设想,只能由`TC39`成员或者`TC39`贡献者提出。\n- stage-1:`proposal`,提案阶段,比较正式的提议,只能由`TC39`成员发起,这个提案要解决的问题须有正式的书面描述,一般会提出一些案例,以及`API`,语法,算法的雏形。\n- stage-2:`draft`,草案,有了初始规范,必须对功能的语法和语义进行正式描述,包括一些实验性的实现,也可以提出一些待办事项。\n- stage-3:`condidate`,候选,该提议基本已经实现,需要等待实践验证,用户反馈及验收测试通过。\n- stage-4:`finished`,已完成,必须通过`Test262`验收测试,下一步就是纳入到`ECMA`标准中。比如一些`ES2016`,`ES2017`的语法就是通过这个阶段被合入`ECMA`标准中了。\n\n有兴趣了解的可以关注[ecma262](https://github.com/tc39/ecma262)。\n\n> 需要注意的是,babel@7已经移除了stage-x的preset,stage-4部分的功能已经被@babel/preset-env集成了,而如果你需要stage <= 3部分的功能,则需要自行通过plugins组装。\n\n```\nAs of v7.0.0-beta.55, we\'ve removed Babel\'s Stage presets.\nPlease consider reading our blog post on this decision at\nhttps://babeljs.io/blog/2018/07/27/removing-babels-stage-presets\nfor more details. TL;DR is that it\'s more beneficial in the long run to explicitly add which proposals to use.\nIf you want the same configuration as before:\n{\n \"plugins\": [\n // Stage 2\n [\"@babel/plugin-proposal-decorators\", { \"legacy\": true }],\n \"@babel/plugin-proposal-function-sent\",\n \"@babel/plugin-proposal-export-namespace-from\",\n \"@babel/plugin-proposal-numeric-separator\",\n \"@babel/plugin-proposal-throw-expressions\",\n // Stage 3\n \"@babel/plugin-syntax-dynamic-import\",\n \"@babel/plugin-syntax-import-meta\",\n [\"@babel/plugin-proposal-class-properties\", { \"loose\": false }],\n \"@babel/plugin-proposal-json-strings\"\n ]\n}\n```\n\n### 自己写preset\n\n如需创建一个自己的`preset`,只需导出一份配置即可,主要是通过写`plugins`来实现`preset`。此外,我们也可以在自己的`preset`中包含第三方的`preset`。\n\n```javascript\nmodule.exports = function() {\n return {\n // 增加presets项去包含别人的preset\n presets: [\n require(\"@babel/preset-env\")\n ],\n // 用插件来包装成自己的preset\n plugins: [\n \"pluginA\",\n \"pluginB\",\n \"pluginC\"\n ]\n };\n}\n```\n\n# @babel/runtime\n\n`babel`运行时,很重要的一个东西,它一定程度上决定了你产出的包的大小!一般适合于组件库开发,而不是应用级的产品开发。\n\n## 说明\n\n这里有两个东西要注意,一个是`@babel/runtime`,它包含了大量的语法转换包,会根据情况被按需引入。另一个是`@babel/plugin-transform-runtime`,它是插件,负责在`babel`转换代码时分析词法语法,分析出你真正用到的`ES6+`语法,然后在`transformed code`中引入对应的`@babel/runtime`中的包,实现按需引入。\n\n举个例子,我用到了展开运算符`...`,那么经过`@babel/plugin-transform-runtime`处理后的结果是这样的:\n\n```javascript\n/* 0 */\n/***/ (function(module, exports, __webpack_require__) {\n\nvar arrayWithoutHoles = __webpack_require__(2);\n\nvar iterableToArray = __webpack_require__(3);\n\nvar nonIterableSpread = __webpack_require__(4);\n\nfunction _toConsumableArray(arr) {\n return arrayWithoutHoles(arr) || iterableToArray(arr) || nonIterableSpread();\n}\n\nmodule.exports = _toConsumableArray;\n \n// EXTERNAL MODULE: ../node_modules/@babel/runtime/helpers/toConsumableArray.js\nvar toConsumableArray = __webpack_require__(0);\nvar toConsumableArray_default = /*#__PURE__*/__webpack_require__.n(toConsumableArray);\n```\n\n## 安装和简单配置\n\n`@babel/runtime`是需要按需引入到生产环境中的,而`@babel/plugin-transform-runtime`是`babel`辅助插件。因此安装方式如下:\n\n```\nnpm i --save @babel/runtime\nnpm i --save-dev @babel/plugin-transform-runtime\n```\n\n配置时也挺简单:\n\n```javascript\nconst buildConfig = {\n presets: [\n // ......\n ],\n plugins: [\n \"@babel/plugin-transform-runtime\"\n ],\n // ......\n}\n```\n\n## @babel/runtime和useBuiltIns: \'usage\'有什么区别?\n\n两者看起来都实现了按需加载的能力,但是实际上作用是不一样的。`@babel/runtime`处理的是语法支持,把新的语法糖转为目标环境支持的语法;而`useBuiltIns: \'usage\'`处理的是垫片`polyfill`,为旧的环境提供新的全局对象,如`Promise`等,提供新的原型方法支持,如`Array.prototype.includes`等。如果你开发的是组件库,一般不建议处理`polyfill`的,应该由调用者去做这些支持,防止重复的`polyfill`。\n\n- 开发组件时,如果仅使用`@babel/plugin-transform-runtime`\n\n![@babel/runtime打包分析](https://qncdn.wbjiang.cn/babel-runtime.png)\n\n- 加上`useBuiltIns: \'usage\'`,多了很多不必要的包。\n\n![@babel/runtime + useBuiltIns: \'usage\'打包分析](https://qncdn.wbjiang.cn/babel-runtime%E4%BB%A5%E5%8F%8AuseBuiltIns.png)\n\n# babel@7要注意的地方\n\n最后简单地提一下使用`babel@7`要注意的地方,当然更详细的内容还是要看[babel官方](https://babeljs.io/docs/en/v7-migration)。\n\n- `babel@7`相关的包命名都改了,基本是`@babel/plugin-xxx`, `@babel/preset-xxx`这种形式。这是开发插件体系时一个比较标准的命名和目录组织规范。\n- 建议用`babel.config.js`代替`.babelrc`,这在你要支持不同环境时特别有用。\n- `babel@7`已经移除了`stage-x`的`presets`,也不鼓励再使用`@babel/polyfill`。\n- 不要再使用`babel-preset-es2015`, `babel-preset-es2016`等`preset`了,应该用`@babel/preset-env`代替。\n- ......\n\n# 结语\n\n本人只是对`babel`有个粗略的认识,所以这是一篇`babel`入门的简单介绍,并没有提到深入的内容,可能也存在错误之处。自己翻来覆去也看过好几遍`babel`的文档了,一直觉得收获不大,也没理解到什么东西,在与`webpack`配合使用的过程中,还是有很多疑惑没搞懂的。其实错在自己不该在复杂的项目中直接去实践。在最近重新学习`webpack`和`babel`的过程中,我觉得,对于不是很懂的东西,我们不妨从写一个`hello world`开始,因为不是每个人都是理解能力超群的天才......\n\n-----', '2019-12-17 09:43:47', '2024-08-03 12:21:07', 1, 44, 0, '说实话,我从工作开始就一直在接触babel,然而对于babel并没有一个清晰的认识,只知道babel是用于编译javascript,让开发者能使用超前的ES6+语法进行开发。自己配置babel的时候,总是遇到很多困惑,下面我就以babel@7为例,重新简单认识下babel。', 'https://qncdn.wbjiang.cn/babel.png', 0, 0); -INSERT INTO `article` VALUES (203, '自动化部署的一小步,前端搬砖的一大步', '在`nodejs`日渐普及的大背景下,**前端工程化**的发展可谓日新月异。构建打包这种日常任务脚本化已经是常态了,`webpack`和`gulp`已经家喻户晓自然不必多说,而**持续集成/持续交付/持续部署**也越来越得到各个前端`Team`的重视,业界也有了很多成熟的概念或者方案,如`Hudson`, `Jenkins`, `Travis CI `, `Circle CI`, `DevOps`, `git hook`。然而对于小白来讲,如果直接上手这些内容,很容易混淆概念,陷入迷茫。如果为了用`Jenkins`而用`Jenkins`,那不是我的做事风格,我必须搞清楚这项技术能给我带来什么。所以我干脆回归问题本质,从最简单的**工作流**入手,先**解决手动部署的效率问题**。\n\n> 前面说这么多废话纯属凑字数,对了,本文讲的内容比较简单,不适合工作流已经很完善的同学\n\n# 自动构建\n\n**构建不是本文的重点**,也不是一篇短文能够讲清楚的,这里就一笔带过了。\n\n## 构建工具\n\n使用主流的构建工具如`webpack`, `gulp`, `rollup`等。\n\n## 构建目标\n\n通过脚本化的形式组织`代码检查`,`编译`,`压缩`,`混淆`,`资源处理`,`devServer`等工作流事务。\n\n# 手动部署\n\n## 踩过的坑\n\n本人曾经也尝试过两种手动部署的方法。\n\n- 搬砖模式,将构建完毕的文件夹通过`xftp`传输到服务器`/usr/share/nginx/html`目录下。\n- 将构建完毕的文件夹用`git`分支管理起来,推送到远程仓库,然后在`linux`服务器上拉取这部分代码。\n\n第一种方法显然已经属于刀耕火种模式了,不过我竟然用了很久。唉,没办法,业务缠身的我只能挤出时间来优化工作流。\n\n第二种方法我自己私下也用过,后来一想,好像可以用[git hook](https://www.git-scm.com/book/zh/v2/自定义-Git-Git-钩子)来改造优化下,也是实现自动部署的好方法。有兴趣的同学可以试试`git hook`。\n\n# 自动部署\n\n## 写脚本\n\n先写个自动构建部署的脚本,主要是包含了切`git`分支,拉取最新代码,构建打包,传输文件到服务器这些步骤。\n\n> scp 命令用于 Linux 之间复制文件和目录\n\n```shell\n#!/bin/bash\ngit checkout develop\ngit pull\nnpm run build:test\nscp -r ./dist/. username@162.81.49.85:/usr/share/nginx/html/projectname/\n```\n\n**ps:**`ip`已经被我胡乱改了一把,别试着攻击我了。\n\n然而我发现在使用部署脚本的过程中,**每次操作都要输入密码**,很烦人。\n\n## ssh认证\n\n虽然很讨厌输密码,但是密码是安全的保证,如果不输入密码,只能通过`ssh`安全访问了。\n\n首先是在自己工作电脑的`~/.ssh`目录下**创建密钥对**。\n\n```shell\nssh-keygen -t rsa\n```\n\n根据个人情况按需修改密钥对的文件名,输入密码时回车即可,代表不需要使用密码\n\n![生成ssh密钥](https://qncdn.wbjiang.cn/生成密钥.png)\n\n接着要**把公钥传输到服务器**上\n\n```shell\nscp ~/.ssh/id_rsa.pub username@162.81.49.85:/home/username/.ssh/authorized_keys\n```\n\n> 如果服务器已经存在`authorized_keys`文件,那么可以直接在服务器上修改`authorized_keys`文件,在文件末加入你自己的`id_rsa.pub`内容即可。\n\n然后我们再修改部署脚本,改用`ssh`认证方式向`linux`服务器传输文件。\n\n```shell\n#!/bin/bash\ngit checkout develop\nnpm run build:test\nscp -i ~/.ssh/id_rsa -r ./dist/. username@162.81.49.85:/usr/share/nginx/html/projectname/\n```\n\n> `scp`的`-i`参数指定传输时使用的密钥文件,这样就可以通过`ssh`安全访问,而不用再每次输入密码了。`-r`参数则是`recursive`,代表递归复制整个目录。\n\n最后我们可以修改下`package.json`,通过`npm scripts`来执行`sh`\n\n```json\n\"scripts\": {\n \"deploy:test\": \"deploy-test.sh\"\n}\n```\n\n配合`vscode`的`npm scripts`快捷方式,用起来就很舒服了。\n\n![npm scripts](https://qncdn.wbjiang.cn/npmscripts.png)\n\n注意,如果`linux`文件**权限不够**也可能报错的,别忘了给`authorized_keys`文件赋予权限,**拥有者可读可写**即可。\n\n```shell\nchmod 600 authorized_keys\n```\n\n好了,按下那个`deploy:test`,静静等待一会吧。此时别忘了扭扭脖子,按按腰啊,程序员还是要注意身体,对自己好一点。\n\n![scp传输中](https://qncdn.wbjiang.cn/scp传输中.png)\n\n随着`bash`窗口的自动关闭,部署工作也画上了句号。\n\n![完工](https://qncdn.wbjiang.cn/fun3.gif)\n\n# last but not least\n\n这里还要考虑的一个问题是,部署过程中会不会造成用户访问问题?\n\n答案是**会影响用户访问**。比如部署脚本执行过程中,已经替换了`index.html`,正在部署静态资源,此时用户正好进入网站,新的`index.html`却访问不到新的静态资源,网页白屏报错。\n\n解决方法是**先上静态资源,再上页面**。因为静态资源经`webpack`构建后都带上了`hash`值,先上静态资源不会影响原有的版本,所以我们还需要再优化下部署脚本,分解下传输过程。\n\n很头疼的是`scp`命令竟然不能忽略文件,这就有点麻烦了。\n\n如果打包后的`dist`根目录文件不算很多,可以考虑手动列举的方式来排列传输顺序。举个例子:\n\n```shell\n#!/bin/bash\ngit checkout develop\ngit pull\nnpm run build:test\nscp -i ~/.ssh/id_rsa -r ./dist/static username@162.81.49.85:/usr/share/nginx/html/projectname/\nscp -i ~/.ssh/id_rsa ./dist/favicon.ico username@162.81.49.85:/usr/share/nginx/html/projectname/favicon.ico\nscp -i ~/.ssh/id_rsa ./dist/element-icons.ttf username@162.81.49.85:/usr/share/nginx/html/projectname/element-icons.ttf\nscp -i ~/.ssh/id_rsa ./dist/element-icons.woff username@162.81.49.85:/usr/share/nginx/html/projectname/element-icons.woff\nscp -i ~/.ssh/id_rsa ./dist/index.html username@162.81.49.85:/usr/share/nginx/html/projectname/index.html\n```\n\n如果觉得这样很傻X,那么可以考虑下`rsync`了,`rsync`是可以通过`--exclude`忽略文件的,这样的话理论上只需要写两条传输命令即可,也不用考虑后续构建可能会新增的内容。不过在`windows`和`linux`之间用`rsync`还是蛮复杂的,留给各位大佬自己探索啦。', '2020-01-16 22:07:18', '2024-10-31 16:41:24', 1, 59, 0, '手撸一个前端自动部署脚本', 'https://qncdn.wbjiang.cn/前端自动化部署海报.png', 0, 0); -INSERT INTO `article` VALUES (204, '前端自动化部署的深度实践', '年前我也在**自动化部署**这方面下了点功夫,将自己的学习所得在[自动化部署的一小步,前端搬砖的一大步](https://juejin.im/post/5e206168f265da3e2b2d7560)这篇博客中做了分享。感谢两位网友`@_shanks`和`@TomCzHen`的意见,让我有了继续优化部署流程的动力。本文主要是在自动化部署流程中,对**版本管理**和**流程合理性**等方面做了一些改进,配合规范的工作流,使用体验更佳!\n\n# 更新日志自动生成\n\n之前我都是手动修改`CHANGELOG.md`,用来记录更新日志,感觉操作起来有点心累,也不是很规范。好在已有前人种树,于是我就考虑利用`conventional-changelog-cli`自动生成和更新`CHANGELOG.md`,真的好用!\n\n![真香警告](https://qncdn.wbjiang.cn/真香(程序员版).gif)\n\n## 什么是conventional-changelog\n\n> Generate a changelog from git metadata\n\n根据`git`元数据生成更新日志,而`conventional-changelog-cli`则是相关的命令行工具。\n\n## 安装conventional-changelog-cli\n\n```shell\nnpm install -g conventional-changelog-cli\n```\n\n## 初始化生成CHANGELOG.md\n\n```shell\ncd my-project\nconventional-changelog -p angular -i CHANGELOG.md -s\n```\n\n以上命令是基于最后一次的`Feature`, `Fix`, `Performance Improvement or Breaking Changes`等类型的`commit`记录生成或更新`CHANGELOG.md`。如果你希望根据之前所有的`commit`记录生成完整的`CHANGELOG.md`,那么可以试试下面这条命令:\n\n```shell\nconventional-changelog -p angular -i CHANGELOG.md -s -r 0\n```\n\n# 工作流\n\n## 代码添加到暂存区\n\n这一步没有什么特殊,日常撸代码,然后将工作区的内容添加到暂存区。\n\n```shell\ngit add .\n```\n\n## 规范commit message\n\n> 一个规范的commit message一般分为三个部分Header,Body 和 Footer。Header包含type, scope, subject等部分,分别用于描述commit类型,影响范围,commit简述。Body则是详细描述,可以分多行写。Footer主要用于描述不兼容改动(Breaking Change)或者关闭issue(Closes #issue)。\n\n格式如下:\n\n```xml\n(): \n\n\n\n