2016年12月

有jslint, javascriptlint(jsl), 还有eslint. 貌似eslint是最受欢迎的.
安装eslint

npm i -g eslint

运行:

eslint *.js

在js目录下放一个.eslintrc.json文件, 可以对需要lint的规则做设置.下面是我复制cocos creator的配置,并做了一点修改, 忽略了末尾分号检测.

{
  "extends": "eslint:recommended",
  "rules": {
    "comma-dangle": 0,
    "no-console": 0,
    "no-constant-condition": 0,
    "semi": 0,
    "no-unused-vars": 1
  },
  "env": {
    "browser": true,
    "node": true,
    "es6": true,
    "mocha": true
  },
  "plugins": [
  ],
  "globals": {
    "cc": false,
    "CC_EDITOR": false,
    "CC_DEV": false,
    "CC_JSB": false,
    "_ccsg": false
  }
}

在REPL中比较容易调试和发现问题, 所以这儿介绍REPL的玩法.

node.js服务端需要安装模块.

    npm install ws

进入node, 开启服务,等待客户端连接. 为了便于以后向客户端发送消息, 把连接后的ws对象赋值给全局变量thisws

node
>
var thisws
var Wss = require('./ws').Server

var wss = new Wss({port: 3888})
wss.on('connection', function(ws){
    thisws = ws
    console.log('connected.')
    ws.on('message', function(message){
        console.log("msg: ",message)
    })
})

chrome 打开console,进行连接和监听服务器消息

s = new WebSocket('ws://localhost:3888')
s.onmessage = function(e){console.log(e.data)}

好啦!, 在chrome中send消息可以在node那边收到, 在node中send消息也可以在chrome中收到了
先从chrome中send

s.send('我是chrome') // 在node中将看到这条消息.

再从node中send

thisws.send('我是node服务器') //在chrome中将看到这条消息.

当然,除了chrome自带的WebSocket客户端, 在node中ws也提供客户端, 另外,socket.io也是另一种websocket客户端和服务器的提供者.

  • 总结
    浏览器客户端监听和发送
onclose = function(e){}
onmessage = function(e){}
onopen = function(){}
onerror = function(){}

send(string)
close()

node ws服务端监听和发送

wss.on('connection',function(ws){})
wss.on('error', function(err){})
wss.on('header',function(array){})
wss.on('listening',function(){})

ws.on('message',function(message_string,flags){})
ws.on('close',function(code, reason){})
ws.on('error',function(err){})
ws.on('open',function(){})

ws.onclose = function(e){}
ws.onerror = function(e){}
ws.onmessage = function(e){}
ws.onopen = function(e){}
ws.readyState

ws.send(message_string, function(error){})
ws.close(code, reason)

// 所有客户端列表
wss.clients
wss.close(function(){})

参考文章: WebSocket and Socket.IO
node ws API

开始以前,准备好visual studio 2015, 现在社区版是免费的.这玩意儿很大, 需要装个半天的.

先安装由微软推出的Electron打包为uwp工具 (真的是微软推出啊! 微软现在竟然这么拥抱开源...)

npm install -g electron-windows-store

然后进入管理员权限的powershell运行:

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned

OK, 接下来就打包了, 举我自己的app的例子:

electron-windows-store --input-directory C:\r\vueproj\nhproj\nhpe --output-directory C:\r\vueproj\nhproj\nhpe_uwp --flatten true --package-version 1.0.0.0 --package-name nhpe

哦, 真的好长, 幸好里面的参数还可以简写:

electron-windows-store -i C:\r\vueproj\nhproj\nhpe -o C:\r\vueproj\nhproj\nhpe_uwp -f true -p 1.0.0.0 -n nhpe

需要注意的是, --package-name一项, 名称必须是app运行文件的名称. 比如, 我已经把electron.exe修改为nhpe.exe了,这儿就一定要填写nphe
然后回车后要提示:

? Did you download and install the Desktop App Converter? It is *not* required to run this tool. No

这一步不是必须的, 回答no即可. 如果yes的话, 你需要去这儿安装Desktop App Converter,不过还是懒得麻烦了.
回车后提示:

? You need to install a development certificate in order to run your app. Would you like us to create one?  Yes

这一步必须yes, 否则虽然可以打包成功,然而打好的包由于没有签名而不能运行. 继续回车:

? Please enter your publisher identity:  CN=idarc
? Please enter the location of your Windows Kit's bin folder:  C:\Program Files (x86)\Windows Kits\10\bin\x64

publisher identity这一步必须填写为 CN=xxxx 的格式,否则后面会报错的...
Windows Kit这一步, 如果visual studio 装的OK的话, 直接回车就可以了.
接下来会弹窗让你输入密码. 不用输入密码, 点击"none"即可.
等待一会儿, 如果没有错误的话, 就会在指定的目录下看到nhpe.appx, 双击运行安装即可.
安装完了再运行, 哦, 这货和直接运行electron.exe改名的nhpe.exe为毛没有任何外观上的区别? 微软说区别是可以获取在在其他xbox等上运行的能力,然而我并没有xbox...

参考原文
假如有两个空对象, 判断他们是否相等, 结果一定是false:

var a = {}
var b = {}
a == b // false

因为跟c语言类似, 这儿判断的是地址而不是内容.
那么怎么知道a是空呢?
有三个方法. 这儿先假设有一个对象obj

var obj = {}

方法1

Object.keys(obj).length === 0

方法2

Object.getOwnPropertyNames(obj).length === 0

方法3, 自己判断

// Speed up calls to hasOwnProperty
var hasOwnProperty = Object.prototype.hasOwnProperty;

function isEmpty(obj) {

    // null and undefined are "empty"
    if (obj == null) return true;

    // Assume if it has a length property with a non-zero value
    // that that property is correct.
    if (obj.length > 0)    return false;
    if (obj.length === 0)  return true;

    // If it isn't an object at this point
    // it is empty, but it can't be anything *but* empty
    // Is it empty?  Depends on your application.
    if (typeof obj !== "object") return true;

    // Otherwise, does it have any properties of its own?
    // Note that this doesn't handle
    // toString and valueOf enumeration bugs in IE < 9
    for (var key in obj) {
        if (hasOwnProperty.call(obj, key)) return false;
    }

    return true;
}
// test it
isEmpty(""), // true
isEmpty(33), // true (arguably could be a TypeError)
isEmpty([]), // true
isEmpty({}), // true
isEmpty({length: 0, custom_property: []}), // true

isEmpty("Hello"), // false
isEmpty([1,2,3]), // false
isEmpty({test: 1}), // false
isEmpty({length: 3, custom_property: [1,2,3]}) // false

在chrome中你只能这么做了:

(function()
{
    'use strict';
    var foo = 123;//works fine
    bar = 345;//ReferenceError: bar is not defined
}());

上面这种模式被称之为IIFE(Immediately Invoked Function Expression)即刻执行的函数表达式.
在node中, 你当然也可以用上这种方式, 不过还有一种方式, 在启动node的时候使用--use-strict参数

node --use_strict

试听win10上的网易云音乐的轻音乐, 发现即便是自动推荐的, 也有很多蛮好听的曲子. 去看了一下作曲者, 竟然很多是国内的原创音乐人. 比如这个: 饭碗的彼岸, 就有好几首好听的曲子《小河》《小樱》。
感觉真是高手在民间啊。

java8及其以前是不支持交互式命令环境REPL的.
Java9 将引入一个JShell来支持. 但目前Java9还没有推出, 怎么办呢? 有一个github的项目, 支持java的REPL
使用前, 需要安装JDK(不是JRE), 以及gradle. 下面是命令行中的安装方法

git clone https://github.com/albertlatacz/java-repl.git
cd java-repl
gradle shadowJar
cd build/libs
java -jar javarepl-dev.jar

需要注意的是,gradle这一步需要执行数分钟, 我的电脑上是8分多钟.
java运行这一步, 需要是类似于jdk1.8.0_111/bin/java这样的目录

由于当前cocos creator音频部分的文档还没有, 这儿简单介绍一下音频部分.

  • 普通的音频的组件是cc.AudioSource, 可以附加在一个node节点上, 同时只能播放一个音频, 添加组件后, 需要将音频拖放到Clip一栏里面(属性为audio-clip).

组件可以进行的操作包括播放/暂停/停止/继续

audioSource.play()
audioSource.pause()
audioSource.stop()
audioSource.resume()

可以获取播放进度和总长度

audioSource.getCurrentTime()  //单位为秒的浮点数
audioSource.getDuration()  //同样是单位为秒的浮点数

从属性浏览器中可以发现还有如下属性:
clip
volume
mute
loop
playOnLoad
preload

  • 使用音频引擎

引擎是一个cc下的对象cc.audioEngine来操作的.

引擎并不需要去操作一个node对象, 而是直接操作一个资源管理器里面的源文件.所以在定义cc.Class的时候与audioSource不同. 以url的方式定义, 音乐文件(很可能)需要放在resources目录中.

// 在操作audioSource的AudioSourceControl.js文件中, 定义属性为cc.AudioSource类型. 然后, 从资源管理器中拖动一个音乐文件到audiosource节点的clip上. 再从层级管理器中拖动audiosource节点到另一个节点的AudioSourceControl.js属性的audioSource上
cc.Class({
    properties: {
        audioSource: {
            type: cc.AudioSource,
            default: null
        },
    // 省略其他代码
})

// 在操作audioEngine的AudioEngineControl.js文件中, 定义**url**为cc.AudioClip类型. 然后, 从资源管理器中拖动一个音乐文件到使用此AudioEngineControl.js的节点的audio属性上.
// 从示例代码中来看, 这个文件是放在resources目录中的,也就是说, 需要动态加载的.
cc.Class({
    properties: {
        audio: {
            url: cc.AudioClip,
            default: null
        },
    // 省略其他代码
})

使用引擎播放, 每次会产生一个audio id, 记住这个id, 用来在后面对该播放中的音乐进行操作.

// 播放,暂停,继续, 停止
var id = cc.audioEngine.play(this.audio, false, 1);  //参数分别为filepath, loop, volume. filepath示例为res/raw-assets/resources/audio/music_logo.mp3, 如果是拖进来的就不用管这个了.
cc.audioEngine.pause(id)
cc.audioEngine.resume(id)
cc.audioEngine.stop(id)

对所有的播放中音乐统一操作:

        cc.audioEngine.stopAll();
        cc.audioEngine.pauseAll();
        cc.audioEngine.resumeAll();

获取各种状态

cc.audioEngine.getCurrentTime(id) // 秒数, 有对应set方法
cc.audioEngine.getDuration(id)  // 秒数
cc.audioEngine.getStatus(id)  // 返回值-1: 停止状态, 1: 播放状态, 2: 暂停状态.
cc.audioEngine.getVolume(id) // 有对应set方法

  • 下面放出cocos creator的示例代码, 先是AudioSourceControl
cc.Class({
    extends: cc.Component,

    properties: {
        audioSource: {
            type: cc.AudioSource,
            default: null
        },

        label: {
            type: cc.Label,
            default: null
        }
    },

    // use this for initialization
    onLoad: function () {
        // cc.audioEngine.setMaxWebAudioSize(1024*10);
    },

    update: function () {
        if (!this.label) {
            return;
        }
        var audio = this.audioSource;
        this.label.string = audio.getCurrentTime().toFixed(1) + ' s / ' + audio.getDuration().toFixed(1) + ' s';
    },

    play: function () {
        this.audioSource.play();
    },

    pause: function () {
        this.audioSource.pause();
    },

    stop: function () {
        this.audioSource.stop();
    },

    resume: function () {
        this.audioSource.resume();
    }
});

  • 再是audioEngineControl
cc.Class({
    extends: cc.Component,

    properties: {
        audio: {
            url: cc.AudioClip,
            default: null
        },

        label: {
            type: cc.Label,
            default: null
        }
    },

    onLoad: function () {
        this.maxNum = cc.audioEngine.getMaxAudioInstance();
        this.audioPool = [];

        // check deprecated
        ['playMusic', 'playEffect'].forEach(function (name) {
            if (!cc.audioEngine[name]) {
                cc.warn('.' + name + ' is not found!');
            }
        });
    },

    update: function () {
        if (!this.label) return;
        for (var i=0; i<this.audioPool.length; i++) {
            var id = this.audioPool[i];
            var state = cc.audioEngine.getState(id);
            if (state < 0) {
                this.audioPool.splice(i, 1);
                i--;
            }
        }
        this.label.string = 'Instance: ' + this.audioPool.length + ' / ' + this.maxNum;
    },

    play: function () {
        if (!this.audio) return;
        var id = cc.audioEngine.play(this.audio, false, 1);
        this.audioPool.push(id);
    },

    stopAll: function () {
        if (!this.audio) return;
        cc.audioEngine.stopAll();
    },

    pauseAll: function () {
        if (!this.audio) return;
        cc.audioEngine.pauseAll();
    },

    resumeAll: function () {
        if (!this.audio) return;
        cc.audioEngine.resumeAll();
    },
});

微软目前的开源项目:

  • 代码编辑器Visual Studio Code, 基于github的开源项目electron
  • Edge浏览器的JS引擎Chakra, 甚至被允许用于node.js, 以替换V8.
  • ANGLE, OpenGL ES到DirectX的翻译器
  • HoloJS, 微软增强现实眼镜Hololens的开发库
  • WinJS, 是的, 你可以用WinJS+HTML+JavaScript开发win10的UWP软件, 哦, 不过这和electron有什么区别?

cocos creator集electron, node.js, vue.js, 浏览器对象, canvas, webGL于一身, 哦看起来还是很强大的.

结构

/assets 根目录下面主要编辑的文件都在这儿
/library, /temp 可以认为都是临时文件, 貌似为了加速编辑器某些处理产生的, 和assets里的资源是匹配关系, 如果匹配出了问题, 这些都删掉, creator会自动再次生成.
/local 编辑器布局, 一般情况下并没有什么用, 貌似删了也没有关系
/settings 构建发布等相关配置, 蛮重要的,要保留.
package.json 也规定了一些重要的信息, 要保留.

运行后还会生成很多.meta文件, 是一个资源文件对应一个.meta, 里面主要是资源文件的uuid和其他一些信息.

assets资源

所以, 主要的编辑工作都是在assets里面. 再说说assets, 文件可以包括:
.fire 场景文件, 规定了场景中各个内容, 也就是creator中层级管理器里面显示的那些东东. 其实是一个json文件.
.js 脚本文件, 当然了, 是JavaScript, 这个后面专门说一下

资源文件:
图片 包括.png, jpg等, 这个不消说, 是在场景中添加到精灵里面的;
粒子 以.plist结尾. plist和json文件作用相同, 只不过是xml形式的. 粒子文件的粒子图片可以是独立图片, 也可以是内嵌在plist中的base64数据.独立图片的话, 需要和plist放在一个目录中. cocos支持的粒子文件在windows上可以用Particle Editor编辑.
字体 可以使用ttf字体或者是位图字体.fnt, windows下使用BMFont制作, MAC下使用glyph designer制作. windows下的制作方法查看这儿

动画 动画目前就我所知, 有三种形式:

  1. 通过creator编辑的动画, 以.anim结尾, 实际上也是json文件. 这个应该和项目是紧密耦合的, 因为在文件里面规定了frame帧引用的资源是uuid形式的, 而uuid是在项目中生成的.
  2. spine骨骼动画 是使用spine软件制作的骨骼动画, 收费才能使用, 恩, 因此我也不知道这个文件是什么样子的.
  3. DragonBones骨骼动画 是使用DragonBones软件制作的骨骼动画, 常被称为龙骨动画, 免费开源, 但似乎以前cocos不支持, 现在貌似也支持的吧太好, 我下载了一个动画加入, 发现骨头缺了一块...不厚道地揣测, 也许是因为龙骨软件的开发商是Egret, 也就是另一个游戏引擎白鹭的开发方, 白鹭引擎貌似现在发展的也不错, 在知乎上还能看到两个引擎的拥护者对喷, 是由于这种竞争关系导致的支持不好吗? 龙骨动画包括三个文件, xxx_ske.json, xxx_tex.json, _tex.png. 在creator中, 将xxx_ske.json拖进层级管理器, 再把_tex.png拖到Dragon Atlas Asset中即可.
  4. 音乐等其他文件, 目前暂未涉及.

js脚本与nodejs和浏览器js异同

js脚本很大程度上结合了node.js和浏览器环境, 然而又有所不同.

目前发现与nodejs一致的有:

  • 可以使用node.js的require加载自定义在assets中的任何js模块.
  • js中定义的所有变量var ,function 都只能在本js文件中引用.
  • 要让require的方式暴露本模块中的变量, 同nodejs一样,需要通过module.exports的方式.
  • 可以使用ES2015语法, 如let, const, 箭头函数等

不一致的有:

  • 不能使用node.js自带的模块, 如fs, path等.

与浏览器环境一致的有:

  • 包含一个全局变量window, 向window.xxx赋值,可以在任一js中访问. window其实本来是BOM(浏览器对象模型)的一部分, 然而可以在这儿使用.

据说还可以使用npm install安装第三方纯js库使用, 尚未试过.
另外根据帮助文档, cocos creator的项目中也有一个类似于electron的index.html文件, 目前暂未开放出来提前修改.

js脚本运行方式

  • 事实上, 在assets目录中的js脚本, 会在游戏一开始加载的时候全部运行一遍, 运行的顺序似乎并没有什么规律. 如果要控制运行顺序参考这儿
  • 所以, 在js正文中所书写的内容都会被运行.
  • 通过require('js文件名')可以直接引用任意目录下的js 文件, 所以js文件就算在不同的目录下也不能同名.

js脚本与其他资源文件的关联

  • 所有的资源是以节点树的方式组织, 有点类似于DOM树, 节点树的在'层级管理器'中显示.
  • 在js文件中定义一个cc.Class, 就可以与其他资源节点关联, 每个js文件最多只能有一个cc.Class
  • 这时把js拖到资源节点的属性面板中, 在cc.Class的properties中定义的内容可以显示在面板上,
  • 如果将内容定义为内置的类型, 则可以将节点拖放到属性面板的该类型上, 在程序中可以对此节点进行引用. 如
cc.Class({
    extends: cc.Component,
    properties: {
        label: cc.Label,
        }
    },
    accessLabel: function(){
        // 访问 this.label
    }
}
  • 通过几个父子方法, 从本节点可以到达任意其他节点. 如果本节点不是一个node,要获取node后才能使用.
    this.label.node.parent //父节点
    this.label.node.children // 子节点的array
    this.label.node.parent.children // 父节点的子节点array. 由于父节点是node,所以可以直接使用node下的元素children
    this.label.node.getParent() === this.label.node.parent // true. 目前暂不清楚get方法与直接访问元素是否可能有什么区别.
    this.label.node.getChildern() === this.label.node.children //true
    this.label.node.getChildByName('childname') // 按名字查找 
  • 通过修改某个节点的父节点来改变这个节点的位置.
    this.label.node.parent = another.node

js脚本对任一资源引用

通过cc.find()方法, 可以从根节点到达任意子节点

cc.find('Canvas/Label') //返回Canvas下的Label节点.

js对某个节点的及其组件component的操作

  • 获取到某个节点后, 你就可以对节点下面的属性直接操作.
this.label.node.x = 123  // 其他在属性检查器中看得到的都可以这样操作
  • 要操作节点上的某个组件, 使用getComponent和addComponent
this.label.getComponent(cc.Widget).top  = 123 // getComponent可以传入一个cc类型
this.label.getComponent('js_file_name') //可以传入字符串, 获取节点上附加的脚本组件
this.label.addComponent(cc.Layout) //也可以通过addComponent添加组件
  • 使用new cc.Node()创建新的节点, 还可以通过cc.instantiate克隆一个节点.
var newNode = new cc.Node('newNodeName') 
var clonedNode = cc.instantiate(anExistedNode)
  • 使用destory()删除一个节点
    somenode.destory()

节点生命周期

  • 附加在节点上的js组件,可以获得节点的生命周期变更通知, 并通过回调函数进行操作.生命周期的顺序是:
    onLoad
    onEnable
    start
    update, lastUpdate 依次循环
    onDisable
    onDestroy

  • 对节点的active赋值true/false, 将触发onEnable/onDisable事件

somenode.active = false // onDisable
somenode.active = true // onEnable
  • 对节点调用onDestroy, 触发onDisable, onDestory事件
    somenode.destroy() // onDisable, onDestory
  • 节点destory以后, 可以用isValid判断是否被destory了. 恩, 我认为这个名称按照上面的active命名方式, 应该叫valid..., 要么需要把active改为isActive以表明其boolean身份.
somenode.isValid  //true
somenode.destroy()
somenode.isValid  //false
  • 需要注意的是, 节点生命随着scene的load开始, scene关闭结束. 节点(node)的生命周期和js本身的只执行一次不同, 请注意区分.

js与被附属节点的关联

  • 通过this可以获取到当前的js脚本组件, 通过this.node可以获取js脚本所附属的节点.
  • 由于properties和自定义function都是通过this来引用的, 所以, 在cc.Class中定义properties和function的时候要注意了, 这两者都不要起一些creator保留的名字, 列了一下, 这样的保留字还是很多的:
__cid__
__classname__
__eventTargets
__instanceId
__onNodeActivated
__preload
__scriptAsset
__scriptUuid
_deserialize
_destroyImmediate
_destruct
_enabled
_getLocalBounds
_id
_instantiate
_isOnLoadCalled
_name
_objFlags
_onPreDestroy
addComponent
constructor
destroy
enabled
enabledInHierarchy
getComponent
getComponentInChildren
getComponents
getComponentsInChildren
isRunning
lateUpdate
name
node
onEnable
onFocusInEditor
onLoad
onLostFocusInEditor
onRestore
playJump
playRun
resetInEditor
schedule
scheduleOnce
start
unschedule
unscheduleAllCallbacks
update
uuid

场景切换与固定部分节点

  • 加载另一个场景

    cc.director.loadScene('anotherScene', callSomeFunctionWhenLoaded)

  • 预加载另一个场景

    cc.director.preLoadScene('anotherScene', callSomeFunctionWhenPreloaded)

  • 保留一个场景中的节点到下一个场景.

    cc.game.addPersistRootNode('nodeName')

  • 取消保留节点

    cc.game.removePersistRootNode('nodeName')

排列节点树中的节点

  • 前面说过了, 把一个节点下挂到其他节点下,只需要继续改这个节点的parent属性就行了
somenode.parent = someNewParentNode
  • 然而, 与兄弟节点间的排序, 该怎么做呢? 可以用父节点的addChild()和removeChild(), 每次addChild()以后, 被增加的节点都会自动加到父节点树的最后面, 也就是场景的最顶端.
var pa = somenode.parent
pa.removeChild(somenode)
pa.addChild(somenode)  // 将somenode移动到了父节点树的最后面.

值得注意的是, 虽然可以用pa.children直接访问到children的array, 然而此array做pop/push/shift/unshift等操作, 似乎并不会对节点树有任何影响. 因此还是乖乖用addChild和removeChild吧!
* 还可以在本级使用setSiblingIndex(index), 可以调整自己在本级中的位置为index

somenode.getSiblingIndex() // 例如, 0, 是在最低端
somenode.setSiblingIndex(2) // 例如, 2, 上移两层.

发射和监听事件

  • 只能从节点上发送和监听事件
  • 通过在本节点上使用emit发射的事件, 只能被本节点接收到.
  • 通过在本节点上dispatchEvent发射的事件, 通过冒泡的方式逐层被上级节点接收到.

英文原文写得好,在这儿

这儿介绍使用nodejs的方式运行, 当然你要装好了nodejs.

以mac版为例, 打开sublime, 选择"Tools->Build System->New Build System...", sublime将打开一个"untitled.sublime-build"文件, 里面有一些json内容, 删掉这些内容, 替换为:

{
"cmd": ["node", "$file"],
"selector": "source.js"
}

保存文件, 取名为"node".
打开一个js文件, 按"command+B"(windows下是ctrl+B)试试, 看console界面有没有输出.
如果输出提示找不到node, 需要修改上面文件的地址为绝对路径.
在mac下打开终端, 输入

which node

我的mac上显示的是

/usr/local/bin/node

将显示node的绝对路径, 复制到上面的文件中, 修改为:

{
"cmd": ["/usr/local/bin/node", "$file"],
"selector": "source.js"
}

再试试!

以下在windows系统中测试过.
默认vim编辑完一个文件后, 要生成一个以~为结尾的备份文件.
不需要该文件, 则需要在用户的根目录如/user/name/下设置 _vimrc 文件.

加入

set nobackup

也可以指定一个其备份的地方:

set backupdir=D:/Program/ Files/Vim /tmp

有时,为了设计美观的关系, 需要字体留一定空隙, 使用css属性letter-spacing, 如下代码:

<div style="text-align: center;">
    <div style="font-size: 88px;color: #f7517f;">loading</div>
    <div style="font-size: 28px;letter-spacing:45px;color:#806d92">&#x5450;&#x558A;Pro</div>
</div>

显示效果如下:

loading
呐喊Pro

然而, 却会发现, 下面的文字和上面的没有letter-spacing的文字对不齐.
用鼠标选择下面的文字, 你会发现letter-spacing的45px空间是加在每一个字符后面的, 这样字符都会靠左一点点.
怎么解决呢? 有的网友建议在前面加空格, 当然是转义的空格了:

&nbsp;

代码改为:

<div style="text-align: center;">
    <div style="font-size: 88px;color: #f7517f;">loading</div>
    <div style="font-size: 28px;letter-spacing:45px;color:#806d92">&nbsp;&#x5450;&#x558A;Pro</div>
</div>

显示效果如下:

loading
 呐喊Pro

然而加上空格你又会发现,由于空格还是占用了空间, 整体字符又会靠右一点点...

css的问题还是要回到css的方法上解决, 正确的姿势之一是增加text-indent, 如下:

<div style="text-align: center;">
    <div style="font-size: 88px;color: #f7517f;">loading</div>
    <div style="font-size: 28px;letter-spacing:45px;text-indent: 45px;color:#806d92">&#x5450;&#x558A;Pro</div>
</div>

显示效果如下:

loading
呐喊Pro

除了text-indent方案以外,应该还可以使用padding-left. 不过, 能在text的css属性内解决的, 就不要放到box的属性去解决, 所以text-indent的方式要更优雅一些吧