Docker上部署一个PHP+MySQL网站的方法

1. 前言

1.1 Docker适合什么类型的项目?

Docker存在的意义是解决项目移植时兼容的问题。
好吧,那么Docker是否对于任何项目都合适呢?
就我目前的体验而言,使用Docker做负载均衡是比较合适的,用Docker装载静态的,较小的项目也是合适的。
如果是想用Docker装载一个大型项目,个人感觉不太合适。这个理论有待后期使用的时候再慢慢探索。

1.2 PHP+MySQL项目部署在Docker的几种思路。

1.2.1 使用tomsik68/xampp

这也是本文要重点探讨的,详情见后面内容。

1.2.2 使用httpd然后再自行安装mysql和php

我会单独写一篇文章记录这个方法。

1.2.3 使用pch18/baota

这种方法是将宝塔软件放在Docker里面。我也会单独写一篇文章记录这个方法。但是这种方法我认为有个致命的问题,就是pch18/baota镜像文件太大了,要5G。你没看错,要5G。所以这种方案的可行性非常值得商榷,因为这很明显违背了Docker存在的意义。

2. 安装部署tomsik68/xampp

下面的内容参考:https://hub.docker.com/r/tomsik68/xampp

2.1 拉取镜像

docker pull tomsik68/xampp

2.2 为宿主机创建mysql数据卷

docker volume create mysql

上述命令的mysql名称任意,为了方便识别用的mysql。

2.3 运行容器

宿主机bind mount网页文件的位置:/opt/www
宿主机映射端口地址:41062
mysql目录不能使用文件夹bind mounts的形式,以下方法执行后会因为权限问题报错。

docker run --name myXampp -p 41061:22 -p 41062:80 -it -v /opt/www:/www -v /opt/mysql:/opt/lampp/var/mysql tomsik68/xampp

必须使用下方数据卷的方法:

docker run --name myXampp -p 41061:22 -p 41062:80 -it -v /opt/www:/www -v mysql:/opt/lampp/var/mysql tomsik68/xampp

2.4 测试运行

在/opt/www 中新建index.php文件,写入如下代码:

<?php
echo "hello world";
echo 1+3;
?>

然后在浏览器中打开地址:http://宿主机IP地址或域名:41062/www/index.php
如果看到hello world4,证明容器运行成功了!
如果运行不成功,可能是因为宿主机防火墙问题,或者关闭防火墙,或者放行41062端口即可。

2.5 进入容器查看

如果想要查看容器内部情况,使用以下命令:

docker exec -it myXampp bash

3. 导入一个PHP+MYSQL网站

我们就以安装一个“帝国CMS”为例演示网站导入过程。

3.1 下载解压安装包。

进入/opt/www目录后下载安装包

wget http://ecms.phome.net/downcenter/empirecms/ecms75/download/EmpireCMS_7.5_SC_UTF8.zip

解压

unzip EmpireCMS_7.5_SC_UTF8.zip

为了方便我把解压后的upload文件夹重名名为了ecms

mv upload ecms

3.2 更改文件夹权限

如果不更改,安装时会提示权限不足。

chmod 777 -R ecms

3.3 完成安装

http://宿主机IP地址或域名:41062/www/ecms/e/install/

按步骤提示完成安装即可。
注:安装时使用xampp默认的mysql用户名:root,密码是空。

4. 数据持久化问题

A. 对于一个web站点,有两部分数据需要持久化:数据库+网页代码。
B. 在tomsik68/xampp给出的官方文档中,run命令是这样写的:

docker run --name myXampp -p 41061:22 -p 41062:80 -d -v /opt/www:/www tomsik68/xampp

上面的命令只实现了网页文件的持久化,没有实现数据库持久化。这样的问题就是每当服务器重启的时候,数据库数据就会丢失。
C. 为了解决数据库持久化的问题,在2.3中我将命令修改为以下形式:

docker run --name myXampp -p 41061:22 -p 41062:80 -it -v /opt/www:/www -v mysql:/opt/lampp/var/mysql tomsik68/xampp

这样,即便服务器重启,只需要使用以下命令就可以重新恢复网站:

docker start myXampp

Xshell6 Portable提示“要继续使用此程序,您必须应用最新的更新或使用新版本

1. 前言

Xshell6是收费软件,同时提供免费的体验版本Xshell6 Portable。
然而,在使用Xshell6 Portable一段时间后,会突然间弹出这样的框:
file
其实意思很简单,就是告诉你试用期到了。

2. 推荐解决方案

如果你觉得Xshell6好用,就尽快去官网购买正版软件吧。这是最明智的做法。

3. 应急解决方案

很多时候你可能不方便马上就购买,需要应急。这时有两个方法:

3.1 调整系统时间

只需将电脑的系统时间往前调整一段时间,比如1年。就可以正常打开网站了。

3.2 修改nslicense.dll文件

3.2.1 为编辑器安装二进制插件

为二进制编辑器(UltraEdit、notepad++)安装HEX-Editor。

3.2.2 用编辑器打开nslicense.dll文件

nslicense.dll位置如下:

安装目录\Xshell6Portable\App\Xshell

文件打开后,需要切换Hex视图:
file

3.2.3 编辑nslicense.dll文件
  • 搜索“7F 0C 81 F9 80 33 E1 01 0F 86 81”
  • 修改“86”为“83”
  • 保存文件
  • 重新打开Xshell发现已经可以使用

注:如果是xshell 5,搜索“7F 0C 81 F9 80 33 E1 01 0F 86 80”然后修改“86”为“83”。

使用命令行安装FastAdmin的详细步骤

1. 为什么要写这篇文章

  • 首先,FastAdmin是个好东西,值得一用和一学。
  • 第二,官方推荐使用命令行安装FastAdmin,因为采用命令行安装的方式可以和FastAdmin随时保持更新同步。
  • 官方默认你已经对以下这些知识和概念有了解:PHP、Git、Node.js、Composer、Bower。然而事实情况是,很多人只是了解PHP,对其它几个技术不甚了解,或者说的直白点,压根没听过。其实这不是什么丢人的事情,因为对于做中小项目的PHPer而言,搞清楚PHP的常规用法就足够了。
  • 写这篇文章的一个主要意图是通过安装过程让大家明白Git、Node.js、Composer、Bower这四个东东都是做什么的。

2. 官方给出的安装说明

强烈建议使用命令行安装,因为采用命令行安装的方式可以和FastAdmin随时保持更新同步。使用命令行安装请提前准备好Git、Node.js、Composer、Bower环境,Linux下FastAdmin的安装请使用以下命令进行安装。
1. 克隆FastAdmin到你本地
git clone https://gitee.com/karson/fastadmin.git
2. 进入目录
cd fastadmin
3. 下载前端插件依赖包
bower install
4. 下载PHP依赖包
composer install
5. 一键创建数据库并导入数据
php think install -u 数据库用户名 -p 数据库密码
6. 添加虚拟主机并绑定到项目的public目录
7. 为了安全,安装完成后会在public目录生成随机后台入口,请通过随机后台入口登录管理后台

3. 分步详细解释

3.1 克隆FastAdmin到你本地

  • git是什么?git是和svn一样的版本管理工具。目前git主要应用的场景还是在开源领域。对于类似FastAdmin这样的开源项目而言,一般都是第一时间将最新的代码放到git的服务器上。通过git clone命令,就可以从git服务器下载最新的项目代码。
  • 这里面的git clone命令是运行在git bash客户端里面的。而git bash则需要在电脑里面安装git软件后才能使用。
  • git软件的官方网址如下:https://git-scm.com/ ,下载安装步骤和普通软件无异,基本上就是【下一步】就可以了。
  • git安装好后,在任何地方点击鼠标右键都会出现Git Bash Here命令选项,点击Git Bash Here后,就会出现git bash小黑窗。
  • 在哪里打开git bash小黑窗,git bash的当前目录就被确定在哪里。
    file
  • 所以呢,在你想要安装FastAdmin的目录下打开Git Bash小黑窗,然后运行命令,就可以将最新的FastAdmin代码下载下来了(注意:下载时会创建一个新的名为FastAdmin的目录):
    git clone https://gitee.com/karson/fastadmin.git

3.2 进入目录

待所有代码都下载完成后,打开一个cmd窗口,然后进入到刚刚生成的fastadmin目录中:

cd fastadmin

向下承接【A】(如果你的电脑已经有Node.js和bower环境)

3.3 下载前端插件依赖包

3.3.1 先安装npm

你想要使用bower的前提是你必须在机器上安装bower。而安装bower的时候需要使用npm,不仅是bower,下面的less也需要用npm来安装。
npm来自哪里呢?来自Node.js!
是不是快绕晕了?没事儿,再梳理下:

  1. 先在本机安装Node.js。具体安装方法见:http://www.uyts.com/archives/278
  2. 我们安装Node.js其实不是为了用Node.js本身,而是为了用安装Node.js时候自动附带安装的npm(有点“买椟还珠”的意思)。有关npm命令的详细介绍,可以参考:http://www.uyts.com/archives/359
  3. npm怎么用呢?打开cmd窗口,直接输入相关的npm命令即可。
3.3.2 理解bower是什么?
  • 先说一句以便更好理解:bower和下面的composer的用处是非常相似的。只不过bower用于管理前端资源,composer用于管理后端php资源。
  • 那么bower是什么呢?bower是一个前端资源的管理器。
  • 举个例子:你在开发网站的时候,会用到很多前端的库,比如:Jquery、Bootstrap、Less等等。在没有bower之前,这些东西你都要自己一个个下载好,然后放到你的项目目录里面。这样做一来麻烦,二来下载的版本很可能每次都不一样,容易出现版本兼容的问题。有了bower后,bower会在你的项目根目录保存一个bower.json文件,里面写明白了这个项目需要用到哪些前端库以及这些库的版本号。这样只需要运行bower命令,就可以自动将bower.json里面提到的前端库自动从bower的服务器上下载下来并放到指定的位置。这种管理机制是非常有用的。
3.3.3 安装bower

打开cmd窗口,输入以下命令:

cnpm install bower -g 
3.3.4 使用bower下载前端插件依赖包

向上承接【A】

bower install

这样FastAdmin依赖的前端资源就都安装好了。
向下承接【B】(如果你的电脑已经有composer环境)

3.4 下载PHP依赖包

3.4.1 安装composer

上面讲bower的时候也提到了,composer和bower的作用非常相似。只不过composer是用来管理php资源库的。
composer的安装方法请见:http://www.uyts.com/archives/233
composer安装好后,在cmd窗口中就可以直接使用composer命令了。

3.4.2 下载PHP依赖包

向上承接【B】

composer install

这时候,FastAdmin全部的代码就都齐全了。

你肯定会问,这好麻烦啊!直接使用安装包解压不就好了吗?何必搞得这么麻烦?这个问题现在我如何说你也是不会理解的,你需要做一段时间开发后才能明白这么做的意义!

3.5 一键创建数据库并导入数据

首先,确保你的php已经加入了环境变量,这样可以直接在cmd中使用php命令。
然后打开cmd,输入如下命令:

php think install -u 数据库用户名 -p 数据库密码

这样数据库就导入到指定的数据库里面了。

3.6 添加虚拟主机并绑定到项目的public目录

具体方法见:http://www.uyts.com/archives/413

3.7 为了安全,安装完成后会在public目录生成随机后台入口,请通过随机后台入口登录管理后台

3.8 安装less

3.8.1 什么是less

用更加类似程序化的语法去编写CSS,比如可以在less里面创建变量,实现循环等等。
less文件本身不能被浏览器识别,需要被编译成(转换成)CSS后才能运行。

3.8.2 安装

这个安装教程里面没有提,但是为了后面编译less文件方便。可以在cmd里面使用以下命令进行安装:

cnpm install less -g

忘记MySQL数据库的root密码的重置方法(适合WAMP和普通MySQL环境)

1. 场景

1.1 安装WAMP环境

初始状态下,WAMP环境中MySQL的数据库密码是空。可以直接登陆MySQL数据库后修改。如果修改后的root密码忘记了,可以用本文方法进行密码重置。

1.2 普通MySQL环境

忘记了初始密码,需要重置初始密码,可以用本文方法进行密码重置。

2. 重置流程(安装了MySQL服务)

以下操作均在cmd命令行中完成。

2.1 停止MySQL服务

在cmd中通过以下命令停止MySQL服务

net stop mysql

2.2 以安全模式运行MySQL

mysqld –skip-grant-tables

2.3 免密登陆MySQL

保持2.2的cmd窗口不懂,另外打开一个cmd窗口后输入:

mysql -uroot -p

这时会提示输入密码,啥也不用输入直接回即可(我试了,随便输入任何内容回车都可以进入MySQL)。

2.4 重置root密码

2.4.1 MySQL5.7之前的版本
use mysql;
update user set password=password("111111") where user="root";
flush privileges;
exit
2.4.2 MySQL5.7及之后的版本
use mysql;
update user set authentication_string=password("111111") where user="root";
flush privileges;
exit

2.5 重置完成

这时候就可以使用111111这个密码登陆root用户了。

3. 重置流程(没有安装MySQL服务)

3.1 停止MySQL服务

ctrl+alt+delete进入任务管理器,然后结束掉mysqld进程。
file

3.2 以安全模式运行MySQL

在cmd中,先切换到mysql的bin目录,然后在bin目录里面运行:

mysqld –skip-grant-tables

3.3 免密登陆MySQL

保持2.2的cmd窗口不懂,另外打开一个cmd窗口后输入,同样的,在cmd中,先切换到mysql的bin目录,然后在bin目录里面运行:

mysql -uroot -p

这时会提示输入密码,啥也不用输入直接回即可(我试了,随便输入任何内容回车都可以进入MySQL)。

3.4 重置root密码

3.4.1 MySQL5.7之前的版本
use mysql;
update user set password=password("111111") where user="root";
flush privileges;
exit
3.4.2 MySQL5.7及之后的版本
use mysql;
update user set authentication_string=password("111111") where user="root";
flush privileges;
exit

3.5 重置完成

这时候就可以使用111111这个密码登陆root用户了。

4. 注意事项

如果使用wamp环境,有可能在重启MySQL的时候不成功。
解决方法是先关掉所有的cmd窗口,然后在任务管理器中停止掉mysqld进程。
再重启MySQL就可以了。

如何在Apache2.4中建立虚拟目录(虚拟主机)

1. Apache版本号

WAMP环境,Apache版本号:2.4.23
此方法同样适用于普通Apache环境。

2. 目标

在浏览器中输入http://localhost:8787的时候,访问F:\PHP_WORKPLACE\www\fastadmin\public文件夹。

3. 两个文件需要编辑

两个文件的目录如下:
如果非wamp环境,则在Apache安装目录:

  • apache安装目录\conf\extra\httpd-vhosts.conf
  • apache安装目录\conf\httpd.conf

如果是wamp环境,可以直接在wamp菜单下打开这两个文件。
file

3.1 httpd-vhosts.conf

增加如下代码:

<VirtualHost *:8787>
    ServerName localhost
    DocumentRoot F:/PHP_WORKPLACE/www/fastadmin/public
    <Directory  "F:/PHP_WORKPLACE/www/fastadmin/public/">
        Options +Indexes +Includes +FollowSymLinks +MultiViews
        AllowOverride All
        Require local
    </Directory>
</VirtualHost>

其中:
8787为访问时使用的端口号,可以任意指定。
DocumentRoot和Directory为绑定的目录地址。

3.2 httpd.conf

在文件中搜索Listen关键字,找到:
Listen 0.0.0.0:80
在这句的下面,输入:
Listen 0.0.0.0:8787
这里面的8787和httpd-vhosts.conf中的端口号对应。

4. 重启Apache

重启Apache,大功告成!

使用Grunt合并及压缩Javascript代码

1. Grunt安装

cnpm install -g grunt-cli 
cnpm install grunt --save-dev

2. 安装合并和压缩两个插件

cnpm install grunt-contrib-concat --save-dev
cnpm install grunt-contrib-uglify --save-dev

3. 根目录创建Gruntfile.js文件

文件内容如下:

module.exports = function(grunt){
    // 1. 初始化插件配置
    grunt.initConfig({
        //主要编码处
        concat: {
            options: { //可选项配置
                separator: ';'   //使用;连接合并
            },
            build: { //此名称任意
                src:  [
                        'src/com_0_adjustArray.js',
                        'src/com_1_globalVars.js',
                        'src/com_2_1_publicFunsSecret.js',
                        'src/com_2_2_publicFuns.js',
                        'src/com_3_subFuns.js',
                        'src/com_4_execFuns.js'
                    ], 
                dest: "dist/all.js" //输出的js文件
            }
        }
    });
    // 2. 加载插件任务
    grunt.loadNpmTasks('grunt-contrib-concat');
    // 3. 注册构建任务
    grunt.registerTask('default', ['concat']);
};

4. 目录结构

file

4. 执行

cmd中切换到目标目录后,直接运行grunt命令就可以了。
运行成功后,在dist文件夹下回出现合并的all.js文件及压缩后的all.min.js文件了。

5. 错误解决

运行grunt命令式弹出如下错误:

basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")

解决方案很简答,将node_modules文件夹中的依赖全部删除,将package.json里面无关的依赖也删除,重新运行cnpm install命令安装依赖即可。
估计是之前在这个文件夹下测试了太多的程序,导致逻辑混乱吧。

Egg.js随学随记

1. Egg.js安装

1.1 安装Egg.js

cnpm install egg-init -g

采用全局安装,而非--save,原因有待后面分解。

1.2 在VS Code下安装Egg.js代码提示

file

2. 初始化Egg.js项目

egg-init project_name

如果这样输入命令,则在命令运行过程中会出现一个选项,让你选择所要创建的项目类型:
file
这里我选择新建一个simple项目,安装完成后,会弹出安装依赖和启动项目的提示命令:
file
这里要注意,创建项目的是新建一个文件夹。所以请合理的安排安装目录。
已经学到Egg.js了,各种cnpm的命令是什么意思应该很清楚了吧?
所以接下来安装依赖:

cnpm install

安装完成后,启动项目:

npm run dev

3. Egg.js目录约定

3.1 官方约定。

egg-project
├── package.json
├── app.js (可选)
├── agent.js (可选)
├── app
|   ├── router.js
│   ├── controller
│   |   └── home.js
│   ├── service (可选)
│   |   └── user.js
│   ├── middleware (可选)
│   |   └── response_time.js
│   ├── schedule (可选)
│   |   └── my_task.js
│   ├── public (可选)
│   |   └── reset.css
│   ├── view (可选)
│   |   └── home.tpl
│   └── extend (可选)
│       ├── helper.js (可选)
│       ├── request.js (可选)
│       ├── response.js (可选)
│       ├── context.js (可选)
│       ├── application.js (可选)
│       └── agent.js (可选)
├── config
|   ├── plugin.js
|   ├── config.default.js
│   ├── config.prod.js
|   ├── config.test.js (可选)
|   ├── config.local.js (可选)
|   └── config.unittest.js (可选)
└── test
    ├── middleware
    |   └── response_time.test.js
    └── controller
        └── home.test.js

3.2 MVC架构

我们的开发都在app目录下完成,Egg.js使用MVC架构,与文件夹对应关系如下:

├── app
|   ├── router.js(路由规则)
│   ├── controller(Controller)
│   |   └── home.js
│   ├── service (Model)
│   |   └── user.js
│   ├── middleware (中间件)
│   |   └── response_time.js
│   ├── public (静态资源)
│   |   └── reset.css
│   ├── view (View)
│   |   └── home.tpl
│   └── extend (扩展方法)
│       ├── helper.js (可选)
│       ├── request.js (可选)
│       ├── response.js (可选)
│       ├── context.js (可选)
│       ├── application.js (可选)
│       └── agent.js (可选)

4. 一个mini的Egg.js项目

4.1 安装egg-view-ejs插件

cnpm install ejs --save
cnpm install egg-view-ejs --save

安装插件后需要配置两个文件:
文件1:/config/plugin.js

'use strict';

/** @type Egg.EggPlugin */
module.exports = {
  // had enabled by egg
  // static: {
  //   enable: true,
  // }
  ejs:{//增加部分
      enable: true,
      package: 'egg-view-ejs',
    }
};

文件2:/config/config.default.js

/* eslint valid-jsdoc: "off" */

'use strict';

/**
 * @param {Egg.EggAppInfo} appInfo app info
 */
module.exports = appInfo => {
  /**
   * built-in config
   * @type {Egg.EggAppConfig}
   **/
  const config = exports = {};

  // use for cookie sign key, should change to your own and keep security
  config.keys = appInfo.name + '_1582681748898_3732';

  // add your middleware config here
  config.middleware = [];

  // add your user config here
  const userConfig = {
    // myAppName: 'egg',
  };

  config.view = {//增加部分
    mapping: {
      '.html': 'ejs',
    },
  };

  return {
    ...config,
    ...userConfig,
  };
};

4.2 文件结构

├── app
|   ├── router.js
│   ├── controller
│   |   └── news.js
│   ├── service
│   |   └── news.js
│   ├── public
│       └── images
│              └── welcome.gif
│   ├── view
│       └── index.html
│       └── content.html
│       └── list.html

4.3 文件代码

4.3.1 router.js

'use strict';

/**
 * @param {Egg.Application} app - egg application
 */
module.exports = app => {
  const { router, controller } = app;
  router.get('/', controller.news.index);
  router.get('/list', controller.news.list);
  router.get('/content/:newsid/:newscategory', controller.news.content);
};

4.3.2 service/news.js 模拟从数据库获取并返回数据

'use strict';

const Service = require("egg").Service;

class NewsService extends Service {
    async getNewsList(){
        //模拟从数据库模拟数据
        var dbData = ["A news about Tom","Jerry's news is cooler!","Popeye finally married Olive!"];
        return dbData;
    }
}

module.exports = NewsService;

4.3.3 controller/news.js 处理业务逻辑

'use strict';

const Controller = require('egg').Controller;

class NewsController extends Controller {
  async index() {
    const { ctx } = this;
    await ctx.render("index.html");
  }
  async list() {
    const { ctx } = this;
    var newsList = await ctx.service.news.getNewsList();
    await ctx.render("list.html",{
        newsList:newsList
    });
  }
  async content() {
    const { ctx } = this;
    var getInfo = ctx.query;
    var paraInfo = ctx.params;

    var name = getInfo.name;
    var level = getInfo.level;

    var newsId = paraInfo["newsid"];
    var newsCategory = paraInfo["newscategory"];

    await ctx.render("content.html",{
        name:name,
        level:level,
        newsid:newsId,
        newscategory:newsCategory
    });
  }
}

module.exports = NewsController;

4.3.4 view/三个文件

文件1:view/index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
    </head>
    <body>
        <h1>你好!欢迎进入迷你新闻系统!</h1>
        <h2>在Egg.js中,静态资源直接使用资源目录路径引用即可!比如下面的图片</h2>
        <img src="/public/images/welcome.gif" >
        <h3><a href="/list">点我进入新闻列表</a></h3>
    </body>
</html>

文件2:view/list.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
    </head>
    <body>
        <h1>新闻列表</h1>
        <ul>
            <%for(let i=0;i<newsList.length;i++){%>
            <li><%=newsList[i]%></li>
            <%}%>
        </ul>

        <h3>
            <a href="/content/6/HotNews?name=Tom&level=3">
                这条新闻通过参数和动态路由传递数据,链接地址是:    /content/6/HotNews?name=Tom&level=3
            </a>
        </h3>
    </body>
</html>

文件3:view/content.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
    </head>
    <body>
        <h3>Name is: <%=name%></h3>
        <h3>Level is: <%=level%></h3>
        <h3>News ID is: <%=newsid%></h3>
        <h3>News Category is: <%=newscategory%></h3>
    </body>
</html>

欲学乾坤大挪移,必先搞定九阳神功!

1. 大学为什么先学C语言?

大学计算机系大一课程中,C语言几乎是标配语言。一个学校是否以C语言作为启蒙计算机语言,很大程度上决定了这个学校的教学品质。
为什么这么说呢?因为C语言是最基础(从当代学习计算机编程角度)的计算机语言,把C语言学透,学扎实,后面学任何新语言都会很快。

这点上看是不是很像《倚天屠龙记》里面张无忌学习“九阳神功”和“乾坤大挪移”的桥段?

《倚天屠龙记》里面张无忌被困明教密道的时候,在小昭的帮助下偶得“乾坤大挪移”心法。张无忌在几个时辰里面学会了别人一生都不一定能学会的“乾坤大挪移”。

书中说的很明白,因为张无忌学习了几年的“九阳神功”,明白了很多学习高等武功的基础知识,再学习高等武功的时候便能融会贯通。

那些没有“九阳神功”基础的人,拿到“乾坤大挪移”后只能看的云里雾里,里面的武功招式看似相识,却又非常陌生,无法实际操练。

2.前端程序员先搞定ES6!

之所以提出这个话题,是因为最近学习前端技术的时候感触颇深。

这些年前端火的不得了,所以就决定自学Node.js,Vue.js,Koa2,Egg.js等等技术。由于自己有着很多年Javascript的基础,觉得直接上手这些技术应该不是难事儿。

最开始学了几天Node.js,觉得一知半解就开始学Vue.js,更加一知半解,然后进入Egg.js,基本上就蒙了。

学习的整体感觉就是,觉得里面的东西都是Javascript,觉得都似曾相识,但是就是觉得别扭,就是记不住。

然后重新梳理一遍,Egg.js基于Koa2,Koa2又基于Node.js,而这一切的一切,都是基于ES6。

于是花了点时间学习ES6,再学习Node.js等其它技术的时候,之前看的云里雾里的一下子豁然开朗。

3. 欲学乾坤大挪移,必先搞定九阳神功!

所以同学们,面对新技术,新知识,千万不要急。

在学习前,一定要先把学习顺序和学习方法搞清楚。

欲成就“乾坤大挪移”大业,必先搞定“九阳神功”!

Koa2随学随记

1. 什么是Koa,Koa2

  • Koa是Express的竞争品。Koa2是Koa的升级版本。
  • Koa和Koa2与Express一样,都是基于Node.js开发的Web框架。
  • 在koa中,一切的流程都是中间件,数据流向遵循洋葱模型,先入后出,是按照类似堆栈的方式组织和执行的,koa-compose是理解koa中间件的关键,在koa中间件中会深入分析。
  • koa2与koa1的最大区别是koa2实现异步是通过async/awaite,koa1实现异步是通过generator/yield,而express实现异步是通过回调函数的方式。
  • koa2与express 提供的API大致相同,express是大而全,内置了大多数的中间件,更让人省心,koa2不绑定任何的框架,干净简洁,小而精,更容易实现定制化,扩展性好。
  • express是没有提供ctx来提供上下流服务,需要更多的手动处理,express本身是不支持洋葱模型的数据流入流出能力的,需要引入其他的插件。
    以上内容参考:https://www.jianshu.com/p/a518c3d9c56d

2. Koa的安装和第一个Koa程序。

2.1 Koa安装

cmd进入工作目录,运行如下命令

cpmn install koa--save

2.2 第一个Koa程序

在工作目录新建app.js,并输入以下代码:

const koa = require("koa");
const app = new koa();

app.use(
    async (ctx) => {
        ctx.body="Hello Koa!"
    }
);

app.listen(3000);

在cmd中运行:

node app.js

浏览器中输入 localhost:3000 便可以访问使用koa建立的web服务器了。

3. Koa路由

3.1 Koa路由功能安装。

Koa的路由功能不是标配(这点和Express不同),需要在安装Koa的基础上安装路由模块Koa-router。
cmd切换到工作目录后使用以下命令安装:

cnpm install koa-router --save

3.2 第一个路由示例

const Koa = require("koa");
const Router = require("koa-router");

var app = new Koa();
var router = new Router();

//逐条指定方式
router.get("/",async(ctx)=>{ //ctx包含request和require等信息.
    ctx.body = "这里是首页";
});
router.get("/news",async(ctx)=>{
    ctx.body = "这里是新闻页面";
});

//连续简写方式
router.get("/list",async(ctx)=>{
    ctx.body = "这里是列表页面";
}).get("/details",async(ctx)=>{
    ctx.body = "这里是细节页面";
});

app.use(router.routes());//使用中间件:启动路由
app.use(router.allowedMethods());//使用中间件:设置响应头,比如页面出错的时候返回404

app.listen(3000,()=>{
    console.log("http://localhost:3000 is ready!");
});

3.3 动态路由

其实更好理解的方式就是通过路由(URL)传参。

const Koa = require("koa");
const Router = require("koa-router");

var app = new Koa();
var router = new Router();

router.get("/product/:name/:brand/:price",(ctx)=>{
    console.log(ctx.params);
    ctx.body="产品页面!";
});

app.use(router.routes());
app.use(router.allowedMethods());

app.listen(3000,()=>{
    console.log("http://localhost:3000 is ready!");
});

访问 http://localhost:3000/page?name=Tom&gender=male ,cmd中结果如下:
file

4. Koa中间件

中间件的概念类似于其它语言中的过滤器。
Koa中间件的运行流程采用的洋葱模式。

const Koa = require("koa");
const Router = require("koa-router");

var app = new Koa();
var router = new Router();

//应用级中间件,app.use("可选路由",函数)
//如果指定路由,则只对目标路由有效,如果不指定,则对所有路由有效.
app.use(async (ctx,next)=>{
    ctx.body="This is the middleware";
    await next();//如果没有next()指令,则其它的代码不会再执行.
});
router.get("/",async(ctx)=>{
    ctx.body += "This is the index page!";//所以呢,为了后期使用中间件更方便,即便初次指定ctx.body,也建议使用+=这种赋值方法.
});

app.use(router.routes());
app.use(router.allowedMethods());

app.listen(3000,()=>{
    console.log("http://localhost:3000 is ready!");
});

5. ejs模板引擎的使用

5.1 安装koa-views和ejs

cnpm install koa-views --save
cnpm install ejs --save

5.2 第一个ejs示例

本示例共有三个文件。
第1个文件:/根目录/koa-views.js,代码如下:

const Koa = require("koa");
const Router = require("koa-router");
const Views = require("koa-views");

var app = new Koa();
var router = new Router();

app.use(Views("./views",{ //   ./views 指明模板文件夹位置
    //extension:"ejs"    //方案一:识别.ejs文件.
    map:{html:'ejs'}   //方案二:将.html视为.ejs文件,可以在html文件中撰写ejs代码.
}));

app.use(async(ctx,next)=>{   //配置公共变量信息
    ctx.state.userinfo = "Tom";
    await next();
});

router.get("/fejs",async(ctx)=>{
    let title = "hello EJS";
    let arr = ["111","222","333"];
    await ctx.render("firstEJS",{ //await必须加
        title:title,
        list:arr
    });
});

app.use(router.routes());
app.use(router.allowedMethods());

app.listen(3000,()=>{
    console.log("http://localhost:3000 is ready!");
});

第2个文件:/根目录/views/firstEJS.html,代码如下:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
    </head>
    <body>
        <%- include('header.html') -%> <!--注意:最新版本的EJS需要用这种语法引入文件-->
        <h2><%=title%></h2>
        这里是EJS文件
        <ul>
            <%for(var i=0;i<list.length;i++){%>
                <li><%=list[i]%></li>
            <%}%>
        </ul>
        <%=userinfo%>
    </body>
</html>

第3个文件:/根目录/views/header.html,代码如下:

<h1>这里是头部内容!</h1>

命令行运行:

node koa-view.js

浏览器中打开 http://localhost:3000 显示结果如下:
file

6. 获取get和post数据。

6.1 获取get数据

通过ctx.query

const Koa = require("koa");
const Router = require("koa-router");

var app = new Koa();
var router = new Router();

router.get("/page",async(ctx)=>{
    console.log(ctx.query);
    console.log(ctx.querystring);
    ctx.body = "hello page!";
});

app.use(router.routes());
app.use(router.allowedMethods());

app.listen(3000,()=>{
    console.log("http://localhost:3000 is ready!");
});

访问 http://localhost:3000/page?name=Tom&gender=male ,cmd中结果如下:
file

6.2 获取post数据。

6.2.1 安装koa-bodyparser

cnpm install koa-bodyparser --save

6.2.2 获取post数据示例

本示例有两个文件:
第1个文件:/根目录/postdata.js

const Koa = require("koa");
const Router = require("koa-router");
const Views = require("koa-views");
var bodyParser = require("koa-bodyparser");

var app = new Koa();
var router = new Router();

app.use(Views("./views",{ //   ./views 指明模板文件夹位置
    //extension:"ejs"    //方案一:识别.ejs文件.
    map:{html:'ejs'}   //方案二:将.html视为.ejs文件,可以在html文件中撰写ejs代码.
}));

router.get("/",async(ctx,next)=>{
    await ctx.render("index");
});

app.use(bodyParser());
//获取post数据
router.post("/doAdd",async(ctx)=>{
    ctx.body = ctx.request.body;
});

app.use(router.routes());
app.use(router.allowedMethods());

app.listen(3000,()=>{
    console.log("http://localhost:3000 is ready!");
});

第2个文件:/根目录/views/index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
    </head>
    <body>
        <form action="/doAdd" method="post">
            <input type="text" name="username" />
            <input type="password" name="password"/>
            <input type="submit" value="提交"/>
        </form>
    </body>
</html>

在浏览器中访问 http://localhost:3000 随意填写数据后提交,结果如下:
file

7. koa-static静态资源中间件

7.1 安装koa-static

cnpm install koa-static --save

7.2 第一个koa-static示例

与express的static非常相似。
本示例有三个示例文件。
文件1:/根目录/view/index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
        <link rel="stylesheet" type="text/css" href="./css/style.css"/>
    </head>
    <body>
        你好!
    </body>
</html>

文件2:/根目录/static/css/style.css

body{
    font-size:50px;
}

文件3:/根目录/index.js

const Koa = require("koa");
const Router = require("koa-router");
const Views = require("koa-views");
const Static = require("koa-static");

var app = new Koa();
var router = new Router();

app.use(Views("./views",{ //   ./views 指明模板文件夹位置
    //extension:"ejs"    //方案一:识别.ejs文件.
    map:{html:'ejs'}   //方案二:将.html视为.ejs文件,可以在html文件中撰写ejs代码.
}));

router.get("/",async(ctx,next)=>{
    await ctx.render("index");
});

app.use(Static("static"));

app.use(router.routes());
app.use(router.allowedMethods());

app.listen(3000,()=>{
    console.log("http://localhost:3000 is ready!");
});

8. art-template模板引擎的使用

art-template是ejs的竞争品,用法和ejs非常类似。只不过速度比ejs要快。

8.1 art-template安装

cnpm install art-template --save
cnpm install koa-art-template --save

8.2 art-tempalte示例

本示例有三个文件:
文件1:/根目录/art_template.js

const Koa = require("koa");
const Router = require("koa-router");
const render = require("koa-art-template");

var app = new Koa();
var router = new Router();

render(app,{
    root:"views",//目录
    extname:".html",//文件名后缀,可以任意指定
    debug:process.env.NODE_ENV !== "production"//是否开启调试模式
});

router.get("/",async(ctx)=>{
    let list = {
        name:"Tom"
    };
    await ctx.render("index",{
        list:list
    });
});

app.use(router.routes());
app.use(router.allowedMethods());

app.listen(3000,()=>{
    console.log("http://localhost:3000 is ready!");
});

文件2:/根目录/views/index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
    </head>
    <body>
        {{include "./header.html"}}
        <br/> 
        <% include("./header.html") %>
        <br/>
        你好!
        <br/>
        {{list.name}}           <!--两种模板语法都可以-->
        <br/>
        <%=list.name%>            <!--两种模板语法都可以-->
    </body>
</html>

文件2:/根目录/views/header.html

<h1>这里是头部内容!</h1>

Javascript的异步机制

0. 异步入坑。

讨论这个问题一定是在入坑Node.js后,而且很有可能是在入坑Express和Koa后。在ES5和ES6中,与异步相关的机制主要涉及以下几个概念:

  • 回调函数(ES5)
  • Promise + then(ES6)
  • Generator + next(ES6)
  • async + await(ES6)

1. 回调函数

Javascript异步操作最基本的解决方案。
由于name的定义是异步的,存在1秒的时延,所以name变量只有通过回调函数才能获取到。

function getData(callback){
    setTimeout(()=>{
        var name = "Tom";
        callback(name);
    },1000);
}
getData((data)=>{
    console.log(`Name is ${data}`);
});

2. Promise + then

ES6异步的另一种解决方案。和回调函数的语法和逻辑最为相像。

function getData(resolve,reject){
    setTimeout(()=>{
        try{
            var name = "Tom";
            resolve(name);
        }catch(e){
            reject(e);
        }
    },1000);
}
var p = new Promise(getData);
p.then((data)=>{
    console.log(`My name is ${data}!`);
});

上面的代码也可以合并简化一下:

var p = new Promise(function(resolve,reject){
    setTimeout(function(){
        try{
            var name = "Tom";
            resolve(name);
        }catch(e){
            reject(e);
        }
    },1000);
});
p.then((data)=>{
    console.log(`My name is ${data}!`);
});

3. Generator + next

3.1 基本概念。

形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();

上面代码定义了一个 Generator 函数helloWorldGenerator,它内部有两个yield表达式(hello和world),即该函数有三个状态:hello,world 和 return 语句(结束执行)。

然后,Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是上一章介绍的遍历器对象(Iterator Object)。

下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。

hw.next()
// { value: 'hello', done: false }

hw.next()
// { value: 'world', done: false }

hw.next()
// { value: 'ending', done: true }

hw.next()
// { value: undefined, done: true }

上面代码一共调用了四次next方法。

第一次调用,Generator 函数开始执行,直到遇到第一个yield表达式为止。next方法返回一个对象,它的value属性就是当前yield表达式的值hello,done属性的值false,表示遍历还没有结束。

第二次调用,Generator 函数从上次yield表达式停下的地方,一直执行到下一个yield表达式。next方法返回的对象的value属性就是当前yield表达式的值world,done属性的值false,表示遍历还没有结束。

第三次调用,Generator 函数从上次yield表达式停下的地方,一直执行到return语句(如果没有return语句,就执行到函数结束)。next方法返回的对象的value属性,就是紧跟在return语句后面的表达式的值(如果没有return语句,则value属性的值为undefined),done属性的值true,表示遍历已经结束。

第四次调用,此时 Generator 函数已经运行完毕,next方法返回对象的value属性为undefined,done属性为true。以后再调用next方法,返回的都是这个值。

3.2 异步

function* gen(x) {
  var y = yield x + 2;
  return y;
}

var g = gen(1);
g.next() // { value: 3, done: false }
g.next() // { value: undefined, done: true }

上面代码中,调用 Generator 函数,会返回一个内部指针(即遍历器)g。这是 Generator 函数不同于普通函数的另一个地方,即执行它不会返回结果,返回的是指针对象。调用指针g的next方法,会移动内部指针(即执行异步任务的第一段),指向第一个遇到的yield语句,上例是执行到x + 2为止。

换言之,next方法的作用是分阶段执行Generator函数。每次调用next方法,会返回一个对象,表示当前阶段的信息(value属性和done属性)。value属性是yield语句后面表达式的值,表示当前阶段的值;done属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段。

4. async + await

async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。

async function getStockPriceByName(name) {
  const symbol = await getStockSymbol(name);
  const stockPrice = await getStockPrice(symbol);
  return stockPrice;
}

getStockPriceByName('goog').then(function (result) {
  console.log(result);
});

上面代码是一个获取股票报价的函数,函数前面的async关键字,表明该函数内部有异步操作。调用该函数时,会立即返回一个Promise对象。(此部分参考:https://es6.ruanyifeng.com)