这篇文章中,我们将要讨论如何用ngrx/effects连接Angular表单和ngrx/store
我们最终的结果是这样的
\\new-story0.component.ts
@Component({
selector: 'new-story-form',
template: `
<form [formGroup]="newStory"
(submit)="submit($event)"
(success)="onSuccess()"
(error)="onError($event)"
connectForm="newStory">
...controls
</form>
<div *ngIf="success">Success!</div>
<div *ngIf="error"></div>
})
class NewStoryFormComponent {...}
方便起见,我们会写一个简单的reducer来组织管理我们应用里的所有的表单
状态(the state)将由一个用ID作为key,表格数据作为value的简单对象构成
举个例子
\\connect-form.reducer.ts
const initialState = {
newStory: {
title: '',
description: ''
},
contactUs: {
email: '',
message: ''
}
}
export function forms(state = initialState, action) {
}
我们先构建一个action——UPDATE_FORM。这个action由两个key:path和value组成
\\connect-form1.reducer.ts
store.dispatch({
type: UPDATE_FORM,
payload: {
path: 'newStory',
value: formValue
}
});
然后这个reducer将负责更新state
\\connect-form2.reducer.ts
export function forms(state = initialState, action) {
if(action.type === UPDATE_FORM) {
// newStory: formValue
return { ...state, [action.payload.path]: action.payload.value }
}
}
我们想要基于state更新表单,所以我们需要path作为输入,然后取出store中正确的片段
\\connect-form.directive.ts
@Directive({
selector: '[connectForm]'
})
export class ConnectFormDirective {
@Input('connectForm') path: string;
constructor(private formGroupDirective: FormGroupDirective,
private store: Store<AppState> ) {
ngOnInit() {
// Update the form value based on the state
this.store.select(state => state.forms[this.path]).take(1).subscribe(formValue => {
this.formGroupDirective.form.patchValue(formValue);
});
}
}
}
我们抓取表单directive实例然后从store里更新表单数据
当表单数据改变时我们也需要更新表单状态。我们可以通过订阅(subscribe)这个valueChanges
的可观察对象(observable)然后调度(dispatch)这个UPDATE_FORM的action来获取值
\\connect-form1.directive.ts
this.formChange = this.formGroupDirective.form.valueChanges
.subscribe(value => {
this.store.dispatch({
type: UPDATE_FORM,
payload: {
value,
path: this.path, // newStory
}
});
})
这就是表单和State同步所要做的全部工作了
有两件事我们要在这个部分完成
有两点原因
通常,没有其他的组件需要这个信息
我们不想每次都重置store
我们将让Angular尽其所能,处理好前端表单校验并重置表单
成功的Action包含表单的path属性所以我们可以知道到底哪个表单需要重置,同时什么时候需要去使用(emit)这个成功的事件
\\connect-form2.directive.ts
const FORM_SUBMIT_SUCCESS = 'FORM_SUBMIT_SUCCESS';
const FORM_SUBMIT_ERROR = 'FORM_SUBMIT_ERROR';
const UPDATE_FORM = 'UPDATE_FORM';
export const formSuccessAction = path => ({
type: FORM_SUBMIT_SUCCESS,
payload: {
path
}
});
同成功的action一样,因为有path的存在,我们也知道何时去使用(emit)错误异常 的事件
\\connect-form3.directive.ts
export const formErrorAction = ( path, error ) => ({
type: FORM_SUBMIT_ERROR,
payload: {
path,
error
}
});
我们需要创建 成功 和 错误异常 的输出 然后 监听 FORM_SUBMIT_ERROR
和 FORM_SUBMIT_SUCCESS
的 action。
因为我们正好要使用 ngrx/effects ,此时我们就可以用 Action 的服务(service)来监听actions了
\\connect-form3.directive.ts
@Directive({
selector: '[connectForm]'
})
export class ConnectFormDirective {
@Input('connectForm') path : string;
@Input() debounce : number = 300;
@Output() error = new EventEmitter();
@Output() success = new EventEmitter();
formChange : Subscription;
formSuccess : Subscription;
formError : Subscription;
constructor( private formGroupDirective : FormGroupDirective,
private actions$ : Actions,
private store : Store<any> ) {
}
ngOnInit() {
this.store.select(state => state.forms[this.path])
.debounceTime(this.debounce)
.take(1).subscribe(val => {
this.formGroupDirective.form.patchValue(val);
});
this.formChange = this.formGroupDirective.form.valueChanges
.debounceTime(this.debounce).subscribe(value => {
this.store.dispatch({
type: UPDATE_FORM,
payload: {
value,
path: this.path,
}
});
});
this.formSuccess = this.actions$
.ofType(FORM_SUBMIT_SUCCESS)
.filter(( { payload } ) => payload.path === this.path)
.subscribe(() => {
this.formGroupDirective.form.reset();
this.success.emit();
});
this.formError = this.actions$
.ofType(FORM_SUBMIT_ERROR)
.filter(( { payload } ) => payload.path === this.path)
.subscribe(( { payload } ) => this.error.emit(payload.error))
}
}
当然,我们不能忘了清空订阅
\\connect-form4.directive.ts
ngOnDestroy() {
this.formChange.unsubscribe();
this.formError.unsubscribe();
this.formSuccess.unsubscribe();
}
最后一步就是在有返回的时候调用表单的actions
\\connect-form4.directive.ts
import {
formErrorAction,
formSuccessAction
} from '../connect-form.directive';
@Effect() addStory$ = this.actions$
.ofType(ADD_STORY)
.switchMap(action =>
this.storyService.add(action.payload)
.switchMap(story => (Observable.from([{
type: 'ADD_STORY_SUCCESS'
}, formSuccessAction('newStory')])))
.catch(err => (Observable.of(formErrorAction('newStory', err))))
)
现在我们可以在组件里显示提醒了
\\new-story.component.ts
@Component({
selector: 'new-story-form',
template: `
<form [formGroup]="newStory"
(submit)="submit($event)"
(success)="onSuccess()"
(error)="onError($event)"
connectForm="newStory">
...controls
<button [disabled]="newStory.invalid" type="submit">Submit</button>
</form>
<div *ngIf="success">Success!</div>
<div *ngIf="error"></div>
`
})
class NewStoryFormComponent {
constructor(private store: Store<AppState> ) {}
onError(error) {
this.error = error;
}
onSuccess() {
this.success = true;
}
submit() {
// You can also take the payload from the form state in your effect
// with the withLatestFrom observable
this.store.dispatch({
type: ADD_STORY,
payload: ...
})
}
}
HttpClient 是对现存的Angular HTTP API 一次进化,现有的HTTP API存在于一个单独的包中,即@angular/common/http。 这样的结构确保了已有的代码库可以慢慢更新到新的API而不至于出现断崖的更新
首先,我们需要更新包版本到 4.3.0-rc.0 版本。
接下来,我们需要把 HttpClientModule
引入到我们的 AppModule
里
\\http-init.ts
import { HttpClientModule } from '@angular/common/http';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
HttpClientModule
],
bootstrap: [AppComponent]
})
export class AppModule { }
现在我们准备好了。让我们来看看三个令人期待的新功能
JSON 作为默认的数据格式而不再需要明确地写出来需要解析
我们再也不需要写下如下的代码
http.get(url).map(res => res.json()).subscribe(...)
现在我们只需要写下
http.get(url).subscribe(...)
拦截器 允许在 管道语法(pipeline)中注入中间件
\\request-interceptor.ts
import {
HttpRequest,
HttpHandler,
HttpEvent
} from '@angular/common/http';
@Injectable()
class JWTInterceptor implements HttpInterceptor {
constructor(private userService: UserService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const JWT = `Bearer ${this.userService.getToken()}`;
req = req.clone({
setHeaders: {
Authorization: JWT
}
});
return next.handle(req);
}
}
如果我们想要注册一个新的拦截器,我们需要去实现(implements)这个 HttpInterceptor
接口(interface)。这个接口有一个方法我们必须要去实现 —— 即拦截器
这个拦截器方法将会给我们一个请求对象(the Request object)、HTTP处理器(the HTTP handler)并且返回一个HttpEvent 类型的可观察对象(observable)
请求和返回对象需要是不可改变的。因此,我们需要在返回之前提前拷贝一个原始请求
接下来,next.handle(req) 方法将会调用一个带上新请求的底层的XHR然后返回一个返回事件的事件流(stream)
\\interceptor-response.ts
@Injectable()
class JWTInterceptor implements HttpInterceptor {
constructor(private router: Router) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(req).map((event: HttpEvent<any>) => {
if (event instanceof HttpResponse) {
// do stuff with response if you want
}
}, (err: any) => {
if (err instanceof HttpErrorResponse {
if (err.status === 401) {
// JWT expired, go to login
}
}
});
}
}
拦截器也可以选择通过应用附加的 Rx 操作符来转换响应事件流对象,在next.handle()中返回。
最后我们需要去做的注册该拦截器,使用 HTTP_INTERCEPTORS 注册 multi Provider:
[ { provide: HTTP_INTERCEPTORS, useClass: JWTInterceptor, multi: true } ]
进度事件可以既用于请求上传也可以用于返回的下载
\\http-progress.ts
import {
HttpEventType,
HttpClient,
HttpRequest
} from '@angular/common/http';
http.request(new HttpRequest(
'POST',
URL,
body,
{
reportProgress: true
})).subscribe(event => {
if (event.type === HttpEventType.DownloadProgress) {
// {
// loaded:11, // Number of bytes uploaded or downloaded.
// total :11 // Total number of bytes to upload or download
// }
}
if (event.type === HttpEventType.UploadProgress) {
// {
// loaded:11, // Number of bytes uploaded or downloaded.
// total :11 // Total number of bytes to upload or download
// }
}
if (event.type === HttpEventType.Response) {
console.log(event.body);
}
})
如果我们想要获得上传、下载进度的提示信息,我们需要传 { reportProgress: true }
给 HttpRequest
对象
这里还有两个新的功能我们今天没有提到:
基于内部测试框架的 Post-request verification
和 flush
功能
类型化,同步响应体访问,包括对 JSON body类型的支持。
以上只是对新的HTTP API和它的新功能的概述,完整的代码可以看 angular/packages/common/http
译者注
应该在哪里注册拦截器呢?
\\app.module.ts
@NgModule({
imports: [ BrowserModule ],
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: JWTInterceptor, multi: true }
],
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
今年年初我离开了资讯公司并开始搭建GO2CINEMA站——一个英国的猫眼电影。我做了一系列优秀的工作来让它更快更简单更安全。而我的习惯是,追求极致的渲染优化。 我用ūsus 解决了HTML的预渲染,ūsus用单页面HTML和内联CSS来渲染页面,而我不喜欢内联70kb 的CSS在每个HTML文档里,尤其是里面大部分是CSS的选择器类名。 就像谷歌做的那样
你是否曾经瞄过一眼谷歌的页面代码,你第一件注意到的事就是CSS的选择器名不会超过一对单词的长度。
但是如何处理呢?
有一点是压缩工具没法做到的 改变选择器的名字,这使得CSS的压缩器没法控制HTML的输出。与此同时,CSS的名字会很长,如果你使用CSS模块化开发,你的CSS模块很有可能包括了 样式表文件的名字、本地用于识别的独立名称和一串随机哈希。css加载器的配置会修饰类名模板的名称,举个例子[name][local][hash:base64:5]。
因此,一个典型的类名会看起来像这样:
` .MovieView__movie-titleyvKVV ;如果你很喜欢详细描述类名,它有可能会变得更长,举个例子
.MovieViewmovie-description-with-summary-paragraph__yvKVV `.
在打包的时候对CSS的类名重命名
而好消息是,如果你用webpack和babel-plugin-react-css-modules,你可以在打包的时候用css-loader getLocalIdent 或者babel-plugin-react-css-modules generateScopedName重命名类名。
//webpack.configuration.js
/**
* @file Webpack configuration.
*/
const path = require('path');
const generateScopedName = (localName, resourcePath) => {
const componentName = resourcePath.split('/').slice(-2, -1);
return componentName + '_' + localName;
};
module.exports = {
module: {
rules: [
{
include: path.resolve(__dirname, '../app'),
loader: 'babel-loader',
options: {
babelrc: false,
extends: path.resolve(__dirname, '../app/webpack.production.babelrc'),
plugins: [
[
'react-css-modules',
{
context: common.context,
filetypes: {
'.scss': {
syntax: 'postcss-scss'
}
},
generateScopedName,
webpackHotModuleReloading: false
}
]
]
},
test: /\.js$/
},
{
test: /\.scss$/,
use: [
{
loader: 'css-loader',
options: {
camelCase: true,
getLocalIdent: (context, localIdentName, localName) => {
return generateScopedName(localName, context.resourcePath);
},
importLoaders: 1,
minimize: true,
modules: true
}
},
'resolve-url-loader'
]
}
]
},
output: {
filename: '[name].[chunkhash].js',
path: path.join(__dirname, './.dist'),
publicPath: '/static/'
},
stats: 'minimal'
};
通过使用babel-plugin-react-css-modules 和 css-loader的相同逻辑来命名,我们可以按我们自己的想法随意改别类名,甚至是随机哈希。而更进一步,我想要更短的选择器类名来直接代替随机哈希。为了改变类名,我创建了一个类名入口然后用incstr来改写每个通过这个入口的不断增加的ID。
这样就能改造出既短又唯一的类名。现在 .a_a
, ` .b_a 诸如此类的类名会代替
.MovieView__movie-titleyvKVV 和
.MovieViewmovie-description-with-summary-paragraph__yvKVV`
这使得GO2CINEMA的css文件束从140kb压缩到53kb
//createUniqueIdGenerator.js
const incstr = require('incstr');
const createUniqueIdGenerator = () => {
const index = {};
const generateNextId = incstr.idGenerator({
// Removed "d" letter to avoid accidental "ad" construct.
// @see https://medium.com/@mbrevda/just-make-sure-ad-isnt-being-used-as-a-class-name-prefix-or-you-might-suffer-the-wrath-of-the-558d65502793
alphabet: 'abcefghijklmnopqrstuvwxyz0123456789'
});
return (name) => {
if (index[name]) {
return index[name];
}
let nextId;
do {
// Class name cannot start with a number.
nextId = generateNextId();
} while (/^[0-9]/.test(nextId));
index[name] = generateNextId();
return index[name];
};
};
const uniqueIdGenerator = createUniqueIdGenerator();
const generateScopedName = (localName, resourcePath) => {
const componentName = resourcePath.split('/').slice(-2, -1);
return uniqueIdGenerator(componentName) + '_' + uniqueIdGenerator(localName);
};
我会加下划线_在CSS类名里去分割组件名和识别名称——这种区分方法有助于压缩。 csso(CSS压缩工具)有作用域设置。作用域里定义了一个类名列表用于做一些专门的标记,即不同作用域的选择器不会选中污染同一个元素,这使得优化方案能更规范地修改规则。利用这个,使用csso-webpack-plugin来后期处理CSS的文件束。
//getScopes.js
const getScopes = (ast) => {
const scopes = {};
const getModuleID = (className) => {
const tokens = className.split('_')[0];
if (tokens.length !== 2) {
return 'default';
}
return tokens[0];
};
csso.syntax.walk(ast, node => {
if (node.type === 'ClassSelector') {
const moduleId = getModuleID(node.name);
if (moduleId) {
if (!scopes[moduleId]) {
scopes[moduleId] = [];
}
if (!scopes[moduleId].includes(node.name)) {
scopes[moduleId].push(node.name);
}
}
}
});
return Object.values(scopes);
};
第一个争议是这种压缩本身压缩算法就可以帮你做到。GO2CINEMA的CSS文件束压缩使用的是Brotli算法,比起原来的长类名的文件束只节省了1kb的体积。另一方面,设置这种压缩只是一次性投资并且这样减少的文档体积需要被解析。它有另一种好处,可以有效阻止那些通过CSS类名来扫描广告的反广告屏蔽插件。
此外,给大家提供一下这种压缩方法后的GO2CINEMA页面
众所周知,SVG图比普通的位图图像小很多,而且可以在高DPI的屏幕上保持清晰度。与CSS3的渐变不同的是,SVG支持IE9。但是在线的SVG可视化图形填充生成代码工具很难搜到(CSS3那些可视化工具倒是很多,译者团队也在开发类似的东西),你甚至有可能都不知道这些在线工具的存在,以防万一把常用的比较好的3个在线工具放在这里。我相信它们能帮到你,不需要你自己绞尽脑汁去思考啦。
一个汇集了许多可重复的SVG图形元素的工具,可以为你的网站背景提供便利
相信使用CSS3可视化生成代码工具的同学对这个也肯定不陌生啦
这个工具可以通过手动调节生成你想要的图形,不过可能需要翻墙
有趣的世界观和优秀的剧情+
超赞的原创bgm(每个人物都有角色曲)+
角色塑造丰满立体+
迷之约会系统+
彩蛋多&隐藏多+
幽默与感动与燃并存+
有趣的弹幕式战斗系统+
尝试突破现实与游戏的界限(让人细思恐极)+
狗控福利(提米太可爱啦)
=Undertale
因为steam周榜推荐了两次,让我第一次知道这个独立游戏《undertale》 我这个人手残,三a大作,电竞王者,情怀神作都没玩过几部,反而黄油和独立游戏玩过不少,很多时候对游戏吸引力的认知不是娱乐性和粘性,而是剧情和衍生文化的吸引力。
玩这个游戏之前也没去关心什么创作背景、作者是一个人、众筹游戏啊等标签,至于IGN10分,steam全好评(现在有差评了,只是差评也是好评内容)我也就是笑笑。
但是,买了之后过一遍一周目你就会发现这个游戏惊人的完整(对,剧情和系统上的惊人地方毕竟只能震住完全不知道这游戏的人,多周目是如何有吸引力的呢,我觉得就是大量细节使得游戏本身的完整和丰富)
如果在不剧透的情况下呢,我也只能说到这里了,就是一个剧情、音乐、交互细节乃至系统都是完美的游戏。第一时间入手正版是不二选择。
顺便,如果你要在不看攻略的情况下玩这个游戏,那么我推荐你购入正版后,下载一个盗版或者破解版玩通一周目,然后删掉盗版进行正版游戏二周目,是我觉得最佳的游戏体验,如果你和我一样不是弹幕游戏大触也是沙包的话那就更好了(即使你是大触这种方法的游戏方式也是极好的)
以下是正文,但是涉及剧透,慎入
剧透分割线
任何体系都是不完备的。 ——哥德尔不完备定律
以前看过一个关于游戏是第九艺术的文章,游戏之所以能独立出来成为一种艺术形式,很大的原因取决于它的交互性和浸入式体验。而那篇文章里举了的例子有这么几个游戏令我印象深刻,《passenger》、《墓园》,都是只是操作一个主人公进行单向操作,但是特点在于不能反悔(返回)对,操作不能通过重启和sl大法保存,给人震撼很强,简单的剧情放在书本和视频上就不行,因为读者作为上帝视角是不能控制的(其实可以,之后再说),而游戏可以强迫玩家进行某种规则。
《undertale》(之后简称UT)也是,存档是不真实的,甚至其实game over画面都是假的,但是一开始又恰恰包含这些游戏元素,没有给玩家提示,使玩家掉入陷阱,直到后面的时候追悔莫及(并不)。很容易联想到一些游戏吧,我这里能想到的就是N+的《君と彼女と彼女の恋》和liar的《腐り姫》单周目不能回头和多周目不能完全重制存档,只不过这两个游戏太虐了又充满了狂气,完全不是正经游戏😓。说回UT,我发现这个系统的问题的时候是在我发现它始终只能存一个存档,我想回到上一个存档只能重来,重来的时候我发觉名字(对,游戏刚开始会起名)不能重来的时候,心想,坏了,其实是假存档,记录是有保存的。。。即使重来也无法抹掉。
游戏的宣传中就有提到【一个人都不用死(⚠️用的是everyone)】所以我一开始想一路杀怪杀过去反抗一下这个规则,直到第一个boss羊妈Toriel。。。
游戏中怪物有两种打法,一种直接【fight】,怼死对方,一种先【action】里面有一些互动和对话选项使对方放弃敌意,但是要躲避多轮攻击(往往难的要死),然后等它名字变黄再【mercy】。而通过战斗【杀死】了羊妈之后,读档重来再次面对羊妈的时候(即使真的从头来过一路到达boss点一只小怪不杀)也会有不一样的对话:
然后你mercy了羊妈离开废墟的时候第二次遇到最终boss小花的时候它会嘲讽你(即使你此时真的一只怪没杀只是一周目打死了羊妈):
知道为什么我推荐先玩盗版,因为这个时候你可以删掉游戏玩正版(而正版因为steam云存档的原因,同一系统下是没机会挽回了QAQ)
这里顺便一说:羊妈的这场boss战不会打死玩家,因为她善良不忍心。但是你乱动去作死还是会死,这时羊妈会捂住嘴,据说作者在游戏早期版本里准备了一张羊妈打死你之后自杀的CG后来删了(太细节了,我还以为被羊妈打死是bug)
所以从某种意义上说这个游戏系统有一种【文字诈骗】的意味在里面,这就要说到接下来的剧情和游戏细节了。
你想要有一段糟糕的时光吗?(do you wanna have a bad time?) - Sans,于最终的门廊
剧情我就不记流水账来说了,因为网上有大把攻略,我依然站在我主观的角度来讲一下: 这个游戏大体剧情就是在说人类掉入怪物的世界,
第一次进入的怪物世界的人类chara不是个好胚子(坏东西),怪物王羊爸asgore和羊妈toriel拿她(他,性别未知)当自己的孩子,chara还和它们的亲儿子asriel成了好朋友,chara因为意外(其实应该是他自己的报复计划,因为chara是被人排挤进入怪物世界的)而死,临死前和asriel说自己想回家,而穿过怪物世界回到人类世界的办法就是需要一个怪物和一个人类灵魂。chara死后asriel悲痛欲绝,带着他的灵魂加上自己来到人类世界,却被人类村民当作凶手,追杀,asriel没有还手也就死在人类的地方。羊爸羊妈一天失去两个孩子,羊爸发誓报复人类,可是敌不过人类,怪物们被封印在地底,需要七个人类灵魂才能回到地上,羊妈则和羊爸分手(离婚)。。。。
然后我们的主人公frisk(对,你自己起了名字也没用游戏结尾会告诉你你的真名)掉入了怪物世界,开始回家之旅。。。
这里就有第一个【文字诈骗】(之前也说文学视频也能控制读者,靠的就是文字诈骗)。玩家进入游戏有起名字、存档、攻击、经验值、等级和金钱的设定,会被RPG的假象所蒙蔽,最明确的在一周目就会揭露的EXP不是Experience point而是Execution Points(处决点数即杀戮点数),LV不是level而是Level of Violence(暴力等级),在最终boss战前sans(重要角色之一)会告诉你这两个意思并对你进行【良心】审判。
其实在早期就可以知道这两个值不是真的,反派角色小花Flowey会说LV是love(肯定骗人的嘛),而在第一个地图废墟中遇到DJ幽灵Napstablook(以后简称DJ幽灵)他有一个帽子戏法会减去你一点经验值(是的,是经验值而不是EXP)你会发现自己的EXP没有减少。。。(但是你要之前杀了怪才能发现有点坑)
存档也是如此,你会发现存档是主角一个特殊技能,而这个技能最终boss小花也有,还会读档来虐你。。。然后所有人都会发现你的读档技能,比如羊爸的boss战,你死了之后再进入boss战,选择-》交谈,会有这样的对话:
包括之前的系统部分说的,都是在暗示存档只是个剧情不是系统。而那主角在存档点复活是为何呢?路边的显示屏和路人npc会告诉你:人类的灵魂无比强大,死后并不会灰飞烟灭。而真结局里的真实实验室得知:灵魂强度取决于【决心】,每次存档提示的【一想到。。。,使你充满了决心】也正是呼应这个设定,真结局的最终boss战对战吸收了所有人灵魂的小花本体asriel的时候也可以原地无限复活正是因为这个时候决心的足够强大。
然后游戏起名字也是有趣,我在玩通游戏之后在mac平台重新下载游戏进入起名字页面,输入每一个符合名字长度要求的游戏中角色名字都会有不同的提示: 现附上角色wiki http://zh.undertale.wikia.com/wiki/Category:%E8%A7%92%E8%89%B2
居然可以用chara的名字,其实chara只有屠杀结局才会出现,这里其实就表明旁白是游戏中的某个角色,在怂恿你杀戮,骗你chara是个【true name】
我不知道其他玩家有没有留意这个细节,起名Flowey,提示是【I】already chose that name。之前有人说旁白是羊爸,因为game over时会有羊爸的鼓励,但是我在这里确定是flowey(asriel),game over时只是asriel灵魂在回放羊爸的台词
下面这个才是随便起一个名字的确定页面
除了这些还有其他关于名字的细节我没有玩出来,摘自wiki:
用Windows前往 C:\Users\YourName\AppData\Local\UNDERTALE (Mac的情况则是 ~/Library/Application Support/com.tobyfox.undertale/) 用记事本开启 “file0”(其他类似于记事本的文书软件亦可) 角色名字会被记录在第一行,修改这个名字即可改变游戏中的名字。数字与符号也可以用在名字之中,然而超过6个字母的话名字会覆蓋周围的其他文字。使用不被允许的名字会没有效果。 主选单中所呈现的名字是储存在同资料夹的 “undertale.ini” 档案中,但只要存盘,就会被”file0”中的新名字给覆蓋。如果你在档案两边名字不同的情况下重置游戏记录的话,则反之”file0”中的名字会被”undertale.ini”所覆蓋。
在更改过名字,并且不是使用不允许使用的名字的话,状态选单中会出现“要修改很容易嘛,对吧?”(”Easy to change, huh?”)的字样。 </em>
同样使用【文字诈骗】的游戏也有很多,我自己能立刻想到的就是AB2的《車輪の国、向日葵の少女》,这也是为何这个作品没法动画化吧。但是UT的诈骗更多,尤其针对RPG,更有颠覆感,也不怪很多玩家被教做人感慨【change my life】
剧情本身也是彩蛋不断,有很多小细节我会放在文章最后着重来说,先说下主线剧情。主要游戏路线就是三个,和平路线-》真·和平路线;中立路线;全屠杀路线; 每个剧情详解想要仔细了解可以看wiki: http://zh.undertale.wikia.com/wiki/Category:%E7%B5%90%E5%B1%80
我大致说一下我自己玩的和平路线和中立路线,这两个路线都是玩家选择做个好人,我因为中立路线只杀了小怪,后面的都没杀所以剧情上只有在结尾是(和平结局)大家都离开地底还是只有你离开地底的区别。玩家从废墟被羊妈保护-》雪镇和骷髅兄弟sans和paprus成为好友和基友(恋人?)-》瀑布遇到追杀人类的怪物英雄undyen-》热域遇到阿宅博士alphys和她做的爱演出杀人机器人matte-》得知离开地底必须要杀死一个怪物得到它的灵魂-》进入核心见到怪物王asgore和他战斗最后宽恕他
-》以为宽恕一切就结束但是小花flowey想要吞噬一切灵魂-》
和所有人成为朋友宽恕小花进入真结局-》发现上一个人类chara和asriel的故事-》
发现小花就是asriel想要报复的灵魂并拯救他-》大家一起离开地底happy end。
剧情的交代方式用的是【橡皮擦】写法,以前看小说写法的时候学习过,开始什么都没有-》中间故事断片出现,越来越多-》拼图越来越完整但是断片开始被擦去,新断片越来越少-》结局回到起点,故事什么都不剩,全部擦除。UT中间有丰富的故事,前后伏笔,但是到结局也就是个和所有人做朋友,离开地下的故事,结尾很简单,但是游戏过程很震撼和虐心。
简单明了的主线+简单的像素风+没有多余技能和一句台词的面瘫主角,这种叙事手法就显得极为高明,把每个中间的细节和伏笔都烘托出来,如果是那种路越走越宽,升级和任务越来越多的传统RPG,支线就显得特别破碎让人失去耐心,然后使得主线特别简短戛然而止(没错我说的就是日厂这些RPG)。
是不是又想起一些独立游戏来,对就是那些日系恐怖小游戏《ib》《梦日记》之类的,实际上我觉得作者刚开始也是要做成恐怖游戏的,怪物的像素设定都很吓人,有一些地方(瀑布区)配乐诡异,细思恐极的细节很多。后来发现玩high了,游戏也朝着幽默和燃的方向去了。 比较有意思的地方就是,真结局里的真实实验室,合成怪和诡异的氛围一个比一个吓人,而且遇怪不是感叹号而是屠杀结局里会变成的遇怪出笑脸。但是在病房躺下会出现一个幽灵给你盖被子,瞬间就反差萌起来。。
至于大屠杀结局,个人觉得是信息量最大的但是太虐,推荐视频或者文字通关。因为真结局之后小花会告诉你,你才是这个世界最大的敌人,你可以重置,然后把所有人记忆消去,把所有人关回地下,奉劝你不要消除存档。而如果你在通关屠杀结局之后重置打通真和平结局,真结局会发生变化:
这是因为在屠杀结局之后,你将有机会重新开始游戏,游戏内的一切也看似完好如初。然而,事实上从此游戏内已经发生了许多改变。因为你的游戏存档内已经被放置了一个为”system_information_963”的文件,而steam正版的情况下,Undertale在游戏运行之前会与Steam云自动同步存盘,就算你取消云同步或将网络断掉也一样。因为在电脑本地将会有一份Steam云存盘备份。云存盘也可以删但是需要通过steam官方删除而且可能会删掉其他游戏的,变相地告知你你做的一切恶果都不可能不留痕迹不付出任何代价。。(所以我没有去玩屠杀结局)
还有一个值得重点介绍的就是sans这个人物,游戏中唯二具有一定上帝视角的人。
Sans是一个较矮且骨骼巨大的骷髅,根据他的Steam交易卡(Steam trading card),穿着一件永远不会拉起拉链的帽衫,下面加一件白色高领毛衣、黑色短裤跟拖鞋。
他总是露齿而笑,并且很少在说话的时候真正“动口”。他有着白色的瞳孔,当他态度严肃时瞳孔会消失。当你与他战斗时,他的眼睛会如霓虹灯般闪烁。这只眼睛一开始只会发光,但当他使用他控制重力的能力时,它将会明显地闪耀。
这个角色在雪镇出现但是会伴随整个流程,在最终boss前会对EXP和LV的意思进行解释并对玩家的行为进行审判。这里就很有意思了,其实sans不是在和角色互动而很大一定程度在和玩家说话,这一点我觉得和小花flowey很像。
举几个例子:读档回到结尾的金色长廊Sans便会告诉玩家他怀疑玩家有穿越时间的能力。他低声告诉玩家一串暗号并让玩家“回溯”到过去跟他对暗号。读档超过两次,Sans便会给玩家他在Snowdin的房间钥匙,并告诉玩家是时候了解真相了。 在Snowdin, Sans的卧室里能找到银色的钥匙, 能开启房子后方Sans的工作室。工作室有着与其他房间完全不同的装饰,有着蓝图,照片,一个徽章和一台坏掉的机器(应该是时光机)。
如果玩家曾杀过至少一个怪物,Sans会根据玩家的表现进行全面的审判。 如果玩家宽恕Papyrus,但是杀过其他怪物,Sans会告诉玩家,未来的发展将会交由玩家来决定。接着他会不发一语得离开。 在某些状况下(重新读档听他讲话),Sans会根据玩家的LV来审判玩家。如果主角LV为1但是EXP大于0,Sans会认为主角是仅仅为了看他会说些什么而杀了某些人。Sans会说“哇哦,你是个挺恶心的人啊。(’wow. you’re a pretty gross person, huh?)” 如果主角LV为2,Sans会说他对主角很可能是因为毫不知情而造成的丝毫差错感到非常伤心,但他接着会说他是开玩笑的,还说“谁会意外地升到LV2?滚出去 (who gets to LV 2 on accident? get outta here.)。” 如果主角LV为3,Sans会给他一个C+评价并告诉主角,主角能做得更好。 如果主角LV大于3,Sans会说主角可能有意地杀害了某些人,可能某些是出于自我防卫的缘故,Sans说他不太确定,因为他那时并没有在观察。 如果主角LV大于9,Sans会说这并不意味着主角还有50%是好的,并责问道“我该说什么才能改变像你这样的存在的想法…?(’what can I say that will change the mind of a being like you…?)” 如果主角LV大于14,Sans会说主角是一个“很坏的人(’pretty bad person)”,但还能变得更糟,主角“在当一个恶魔这件事上做得太差劲了。(pretty much suck at being evil.)”
如果玩家杀了Papyrus,Sans会过来告诉主角,他怀疑主角有某种特殊力量(也就是存档),并且Sans问主角是否认为自己应该承担责任做正确的事。如果玩家回答:“是”,他接下来会当场质问玩家为什么杀了他的兄弟。 如果玩家回答:“不”,他会说他不会依照玩家的观点来审判玩家,并称玩家为“肮脏的兄弟杀手 (dirty brother killer) ”。 不管选什么,Sans都会在提醒主角是他们亲手杀了Papyrus后离开。
如果玩家杀了每个头目并杀了至少一个怪物,Sans会提到怎么每个有资格当领袖的人,在一夜之间全死了,而怪物们若给他领导会陷入困境。他接着说他不是当国王的料,因为他喜欢放松,接着他说这原因是个笑话,事实上,现在这个情况这是他太放松而造成的。他告诉玩家“下地狱去吧”或是“等会见”并结束对话。 如果只杀了Papyrus会让Sans告诉玩家,Toriel尝试欢迎人类到地下世界,但是Undyne阻止了她,因为Undyne知道Papyrus的死亡,最后,Sans告诉玩家地下世界不欢迎他们。 如果玩家只杀了头目,尽管Papyrus死了,Sans仍会具有一种古怪且较轻松的心情。他告诉玩家一只白色小狗坐上王位,而且一切都很和平。
重点来了,在屠杀路线,根据wiki所述,Sans在雪町森林遇到玩家时的态度与往常无异,但他很惊讶玩家拒绝跟他与Papyrus一起玩。在穿过桥之后,Sans会警告玩家最好别攻击他的兄弟。 在最终门廊,Sans问玩家说他们是否还有机会改过向善。不过在玩家铸下那么多罪孽之后,他已经不期待任何答案,接着他询问玩家说是否“想度过一段痛苦的时光”,并警告他们别再往前一步,否则他们会面临到他们所不愿体验到的发展。即便玩家不加以控制,主角也会主动向前,迫使Sans和主角开始了战斗。他向Toriel道歉说没能信守保护人类的承诺;这个承诺很可能也是Sans没有第一时间向主角开战的主要原因。 在说了一些话后,战斗开始了。Sans多次攻击主角,边攻击边说话,自称自己在思虑著为什么他并没有采取行动。他不知道这是否是理解到一切终将重置或者说仅仅是他过于懒惰的结果,他在最后的危急关头才跳出来阻止主角。在这段话后,下一轮他会试图给主角宽恕的机会,告诉他们这样会轻松许多。 如果主角宽恕了他,他会告诉他们他不会让主角的努力白费。他接着在他眨著左眼的时候,用无法闪躲的攻击杀了主角,并告诉他们说:如果我们还是朋友,那就别再回来了。 如果玩家继续和Sans战斗,他会继续攻击主角,没有尽头。他会操作重力将玩家的灵魂在弹幕面板里甩来甩去,一旦玩家撑过了这些攻击。他将接着使用了他的“特殊攻击”,其实就是什么都不做,强制将回合停留在他的回合,让玩家什么都不能做。他希望玩家会因此感到无聊而离开,或者永远受困于这里。(这里特别吊,完全是操作游戏系统)不过,Sans最后睡着了,让玩家能够用他们的灵魂拖曳弹幕面板到“攻击”选项,向睡着的Sans发动攻击。Sans会躲掉地一下攻击,但是瞬间接上的第二下攻击将对他造成致命一击。而Sans受到伤害之后,Sans的伤口开始”流血”(但有种说法是说,Sans流的其实是番茄酱,毕竟….Sans把番茄酱当水喝嘛….)然后警告玩家的所作所为,并在死前走出画面。尽管玩家没有直接目睹Sans的死,但依然升到了20等。
这个时候回过头看,在正常和和平路线里,CORE区域前的宾馆餐厅,sans会严肃的说起他和羊妈隔着门说笑话的青涩经历(羊妈的房间里也有笑话书,羊爸头顶一片绿),并提到和羊妈做的承诺,他说:你知道如果我没有对那个女士承诺过守护掉下来的人类,会发生什么吗?主角一脸懵逼,sans说
Y o u W o u l d B e D e a d W h e r e Y o u S t a n d
真的吓到!想到sans其实是游戏里战斗能力最屌的角色,从最终boss战前可以看出,应该不输小花,不寒而栗。
当然其他角色哪怕是小怪的设定和剧情都很丰富的,比如雪镇的狗夫妇,他们说自己是蹭鼻子亚军,然后你就会在最终的boss战前发现羊爸的房间里有冠军奖杯!然后你就会从瀑布的老乌龟那里知道,羊爸羊妈是因为天天蹭鼻子秀恩爱耽误了正事才分手的卧槽。。热域的两个守卫,这俩是基佬,对付的方法就是让一方热的脱衣服吸引另一个表白哈哈哈。之后的真结局ending会出现这俩的结局,四个字【自行脑补】,顺便狗夫妇在结局的时候会告诉你他们终于得了冠军哈哈哈哈哈哈哈哈……其他的角色就太多了,大家自己玩一下,细节满满。
说一下,战斗系统,作为弹幕游戏真的很有意思,各种弹幕,不限于框框,甚至框框自己也会动,只不过打起来非常难,我打小花就花了一天,小boss都是数小时级别的(手残没办法_(:зゝ∠)_非常虐人)但是想到后面的剧情,我一直充满决心,每次花了时间mercy了对方,想到不是随便kill了他,都很有成就感
这种疯狂的战斗系统带来的少年漫画一样的激昂感,第一次,在2D射击弹幕里玩出的爽快感!
最后再来说下音乐,UT的配乐真的让我想到另一个弹幕游戏系列,东方(对,我是个隐藏已久的车万厨),弹幕游戏往往代表着受苦,一个boss打个成百上千遍,这个时候bgm不耐听就很容易让人失去兴趣。而UT虽然是一个人做的,可以说在音乐上非常用心。
开始游戏的菜单音乐Menu,随着结交朋友的变多,由刺耳跳动变得舒缓柔和。
小花的配乐第一遇到叫做Your Best Friend,Boss战通过变调变成了Your Best Nightmare,一下子扭曲刺耳起来,加上小花boss形态就是克苏鲁啊,san值掉的厉害
羊妈的boss曲Heartache,真的把羊妈的纠结表现出来,下不去手。。
羊爸的boss曲ASGORE,呼应了剧情中羊爸不会起名字的特点,就给自己的曲子叫自己的本名2333
屠杀结局里undyen会出来与你对抗,这个时候的boss曲 Battle Against a True Hero以及之前的小花会求饶的bgm But the Earth Refused to Die 都十分悲凉悲壮,这时透过歌曲你明白你才是游戏的大反派。。。
而和平结局中当度过小花一阶段的单方面屠杀的时候,玩家会得到一定支援小花的伤害会降低,这个时候的boss曲Finale,由慢渐快,由弱渐强,瞬间就充满了决心燃起来了有木有,【我就是要打过你呀!】包括真结局的SAVE the world,都是燃曲,名字也暗示了存档和拯救的双关
其他小boss的曲子也很好听,首推spider dance和Dummy!
还有一些奇葩的地方会配乐,比如瀑布曲有一个小鸭子作为捷径npc带你过河,短短不足一分钟的剧情(就是带你过到对岸)专门配了首燃曲Bird That Carries You Over A Disproportionately Small Gap,再比如出旅馆去CORE的一小段户外的路配了个刮风的音乐 CORE Approach,再比如CORE最后的电梯还配了个像死机了一样的Long Elevator(我真的以为电脑卡住了,这么久都不能动)
个人最喜欢的还是game over的bgm Determination,每次一死我就充满了不能放弃的决心(也许是因为听得多hhhhh)
其实游戏里很多歌都是同一首歌快放慢放拼凑变调来的,作为一个人开发的游戏无可厚非。但是在不同场景听着有不一样的感觉,感叹作者的强大啊
以上歌曲网易云音乐都听得到,顺便网易云音乐识别歌曲失败会随机打开UT的一首歌叫做Bring It In,Guys 是UT的boss曲串烧,看来小编很想拉人入坑啊哈哈哈哈
最最后就是我最喜欢的这个游戏的细节和彩蛋了!
Sans会对玩家说以下这些话:
我就老实说吧。
我是不知道你是怎么来到这里的。
这实际上是一种处理出错的讯息。
所以,如果你到达了这个结局的话...
请告诉任何一个制作这个游戏的作者,好吗?
他们会修正,如果这是个新的程式...
他们甚至可能会增加新的结局。
但,这是最有可能的是...
你就只是个恶心的骇客,不是吗?
就是这样没错,你给我滚。
之前强行通过修改游戏文件修改成角色的名字也是会被游戏发觉然后状态选单中会出现“要修改很容易嘛,对吧?”(”Easy to change, huh?”)的字样。想改也还是能做到的不是吗hhhhh
第一次在废墟羊妈给你打电话说她被一只狗缠住了。后来在瀑布区域钢琴谜题后,会出现传说的神器,然后你想拿系统提示包里充满了狗,就发现神烦狗在包里,丢出来它会把神器拿走,然后留下一个狗剩。。。。。
那你不丢掉狗会怎么样呢,包里有狗打电话给Toriel,会发现电话在自己身上响起。这很可能是为什么Toriel到结局以前从不接听电话的原因──手机又被烦人的狗偷走了。在房间里依Toriel的指示持续等待,后续的电话内容会让玩家知道烦人的狗在偷走手机并躲起来后睡着了,手机里甚至传来狗的打呼声。离开这个房间后将不再收到自Toriel手机打来的电话。
离开神器密室时,烦人的狗会自动从背包里消失,进入房间时又会再次出现。如果试图将它收进手机里的次元箱,会被告知“狗毛充斥了整个箱子。神烦狗只会于玩家身处神器密室时出现于物品栏。唯有编辑游戏档案能使牠留在物品栏之中,但在密室之外使用或丢掉烦人的狗只会使该物件消失,除此之外什么都不会发生。
透过修改游戏档案将神烦狗留在物品栏中,可以在Temmie商店以999G的价格把它卖给Temmie店长。如果在Temmie店长恳求时拒绝这笔交易,价格会进一步被提高到1251G,这使得神烦狗成为本游戏中最具价值的物件。在正常的游戏流程中这并不会发生,真的是惊呆了。。。。
如果在真结局最后的特别鸣谢名单躲过所有名字好像神烦狗会出现睡在结束画面。而在困难模式的结尾,游戏内有一扇一直打不开的门会打开,进去后发现神烦狗在滚键盘,然后告诉你这游戏就是这样做出来的哈哈哈哈哈(作者你就是条狗吗好烦hhhhh)
截取一段提米们的台词,自行感受
你吼,我素提米.这素我的朋友,提米!
你吼,我素提米,这素我的朋友,提米!
你吼,我素提米,他们素我的朋友.
你吼,我素Bob
...
听说人类对提米过敏,没关系!因为提米。。。也对提米过敏!!
(然后这只提米就开始满脸长疙瘩)
提米店也是神奇,这是为数不多啥都可以卖的地方,尤其是狗剩。。(你花钱买这个废物干嘛)提米店长说它要去上大学,然后我在他这里卖东西卖1000块去供他读大学。。。。他就跑去念大学了,脸都不要了哈哈哈哈哈哈
然后他就会卖给你提米装甲。。。防御20雾草,全游戏最强防具,小怪基本无伤。。。。提米你上大学真的太厉害了
。。。。
还有很多细节得多周目才能玩出来啦,最后给看到这里的大家致谢,有兴趣真的推荐去玩一下,36入手一个正版,我觉得作者的用心不会让你失望。
最后没人看到的地方,念两句诗
不是乱写垃圾文章拼凑数量,而是实际业务中确实有很大的数组处理尤其是后端LIST数据类型的处理,要比对数组元素要去重,这里分享一下最近思考的方法
Set的数据类型自带去重的效果,Map可以用has()方法判断key存不存在(这两个都不支持比对数组里的对象的,这里用转成字符串的方法就可以有效解决数组里有复杂数据类型没法一次去重的问题)
// ES6
function unique (arr) {
const seen = new Map();
return arr.filter((a) => {
// 利用Map的校验自己有没有key的能力,每次构造一个value为1的key值为字符串的键值对
return !seen.has(JSON.stringify(a)) && seen.set(JSON.stringify(a), 1);
})
}
// or
function unique(arr) {
let seen = new Array();
// 每一项字符串化
arr.forEach((a) => seen.push(JSON.stringify(a)));
// 利用set自动去重
seen = Array.from(new Set(seen));
// 每一项反字符串化
return seen.map((a) => JSON.parse(a));
}
function unique(array){
var n = [];//临时数组
for(var i = 0;i < array.length; i++){
if(n.indexOf(JSON.stringify(array[i])) == -1) n.push(JSON.stringify(array[i]));
}
for (var j = 0;j < n.length; j++) {
n[j] = JSON.parse(n[j]);
}
return n;
}
PS:以上方法均支持数组里有复杂数据对象的,比如这样的
var a = [
{ a: 1 },
{ b: 2 },
{ a: 3 },
{ c: 2 }
];
indexOf可以换成JQuery的inArray方法,不做去重做数组内容比对也是很容易的
]]>之前屡次需要用css画小箭头,作为一个职业切图仔,还是记录一下,免得每次都要google复制粘贴不一样的东西,同时要考虑简便性和兼容性
这个只用了border,所以兼容性很好
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<style>
#arrow {
border: 10px solid transparent;
border-left: 10px solid #000;
width: 0;
height: 0;
position: absolute;
content: ' ';
}
</style>
</head>
<body>
<div id="arrow" />
</body>
原理其实是一个空白的箭头盖住实心的箭头_(:зゝ∠)_听着好简单
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<link rel="stylesheet" href="">
<style>
#arrow {
border: 10px solid transparent;
border-left: 10px solid #000;
width: 0;
height: 0;
position: absolute;
content: ' ';
}
#arrow:after {
border: 9px solid transparent;
border-left-color: #fff;
width: 0;
height: 0;
position: absolute;
top: -10px;
content: ' ';
left: -11px;
}
</style>
</head>
<body>
<div id="arrow" />
</body>
这样写起来太蠢,用SASS的mixin封装一下就好很多了(此处以⬅箭头为例)
@mixin left-blank-arrow($color,$child-top,$child-left) {
width: 0;
height: 0;
border: 10px solid transparent;
border-right-color: $color;
position: absolute;
&:after{
position: absolute;
left: $child-left;
top: $child-top;
content: "";
width: 0;
height: 0;
border: 9px solid transparent;
border-right-color: #fff;
}
}
@mixin left-arrow($color) {
width: 0;
height: 0;
border: 10px solid transparent;
border-right-color: $color;
position: absolute;
&:after{
/*清除after样式,避免之前的污染*/
content: "";
width: 0;
height: 0;
border: 9px solid transparent;
border-right-color: transparent;
}
}
实际项目中的效果还是不错的:
PS : 还可以SASS写一个判断方向方法直接引用
@mixin arrowDirection($direction, $bdc){
/* 向上小三角 */
@if $direction == top {
border-top: 0;
border-bottom-color: $bdc;
}
/* 向下小三角 */
@else if $direction == bottom {
border-bottom: 0;
border-top-color: $bdc;
}
/* 向左小三角 */
@else if $direction == left {
border-left: 0;
border-right-color: $bdc;
}
/* 向右小三角 */
@else if $direction == right {
border-right: 0;
border-left-color: $bdc;
}
}
还是要探索一些不一样的方法的,虽然CSS3那就没啥兼容性了,不过简单好懂啊,方法提供在那里,有什么理由不用新的呢
<head>
<style>
#arrow {
display: block;
height: 10px;
width: 10px;
border-width: 1px;
border-color: transparent;
border-style: solid;
border-top-color: #000;
border-left-color: #000;
transform: rotate(45deg);
-ms-transform: rotate(45deg); /* IE 9 */
-webkit-transform: rotate(45deg); /* Safari and Chrome */
-o-transform: rotate(45deg); /* Opera */
-moz-transform: rotate(45deg); /* Firefox */
}
</style>
</head>
<body>
<div id="arrow" />
</body>
插件用一下autoprefixer或者用SASS封一下就不用写这么多前缀了,懒得写(逃
今天和小伙伴讨论到当年面试被张云龙大神问的一个问题:有一个高度自适应的div,里面有两个div,一个高度100px,希望另一个填满剩下的高度。
我记得我当时是用的position,可是今天被盼哥一问我给问蒙了,想不起来我咋做的,就口胡说JS实现的。后来小伙伴一提醒我才想起来,实在惭愧,对不起校招进来的层层筛选啊,但是装逼失败归装逼失败,这个东西我想了下很有意思,就拿出来实现了一下,也算对自己有个交代
###一个外部容器,上面边定高,下边自适应
1、position布局,大家都很熟悉的西方那一套,兼容性很好,很一颗赛艇,可是今天被盼哥一问我居然忘了,实在是半瓶子醋实力不行啊
HTML:
\ <div class="container">
<div class="msc1">
</div>
<div class="msc2">
</div>
</div>
CSS:
.container {
position: relative;
background: #cccccc;
width: 400px;
height: 400px;
}
.msc1 {
background: #999999;
height: 100px;
width: 100%;
}
.msc2 {
background: #000;
position: absolute;
top: 100px;
left: 0px;
bottom: 0px;
width: 100%;
}
// 因为最外面设一个高度好查看,所以才在最外面设了400px,原题是要外面自适应的,不影响
2、flex方案,这也是最近在前端微专业学来的,用浩哥的话就是:这种东西你webview玩下就好了,平时谁会用啊。言下之意就是兼容性差,恩。
HTML:同上
CSS:
.container {
background: #cccccc;
width: 400px;
height: 400px;
display: flex;
flex-direction: column;
}
.msc1 {
background: #999999;
height: 100px;
width: 100%;
}
.msc2 {
background: #000;
width: 100%;
flex: 1
}
3、CSS3实现,这里用了calc()的方法可以拿百分比减px,不过这里有个坑,那就是:
计算公式里‘-’两头要带空格,至于为什么呢,我也不知道,因为W3标准就是这么写的
Note that the grammar requires spaces around binary ‘+’ and ‘-’ operators. The ‘*’ and ‘/’ operators do not require spaces.
http://www.w3.org/TR/css3-values/#calc § 8.1.1
HTML同1,
CSS:
.container {
background: #cccccc;
width: 400px;
height: 400px;
}
.msc1 {
background: #999999;
height: 100px;
width: 100%;
}
.msc2{
//需要兼容不同浏览器
height:-moz-calc(100% - 100px);
height:-webkit-calc(100% - 100px);
height: calc(100% - 100px);
width: 100%;
background: #000;
}
3、JS实现,本来我想不起CSS的解法,情急之下口胡说JS获取高度一减就OK,其实这里也有个我不常遇到的情况(平时原生写得少),就是写在css里的元素属性,用object.style.height是读不到的,具体要怎么解决呢,看下面
HTML同1
CSS:
.container {
/*position: relative;*/
background: #cccccc;
width: 400px;
height: 400px;
display: flex;
flex-direction: column;
}
.msc1 {
background: #999999;
height: 100px;
width: 100%;
}
.msc2 {
width: 100%;
background: #000;
}
JS:
function getHeight(className) {
var dom = document.getElementsByClassName(className)[0];
// 先获得dom
var domHeight = window.getComputedStyle(dom).getPropertyValue("height");
// 再去window下找这个元素的属性,然后读到值,然后这个值是个字符串'400px'
return Number(domHeight.split('px')[0]);
}
var containerHeight = getHeight('container');
document.getElementsByClassName('msc2')[0].style.height = String(containerHeight - 100) + 'px';
// 提取出400做加减法,再用style,OK
7-23日补充:玉林师兄说原生JS用clientHeight可以得到高度,这个我给我忘了,这样简单不少,sorry
function getHeight(className) {
var dom = document.getElementsByClassName(className)[0];
var domHeight = dom.clientHeight;
return domHeight;
}
var containerHeight = getHeight('container');
document.getElementsByClassName('msc2')[0].style.height = String(containerHeight - 100) + 'px';
###总结:没事实力不行还是要多实践多看书,少装硬逼多吹水(上面有写错的,实现不佳的,新的补充的欢迎告诉我:-))
]]>之前布局一直都是瞎JB乱写,正好抽时间总结一下在这里记录下来
###一边定宽,一边自适应
1、float布局
HTML:
\<div class="parent">
<div class="left">
<p>left</p>
</div>
<div class="right">
<p style="clear:left;">right</p>
<p>right</p>
</div>
</div>
<!-- bug:一旦在内部元素设置清除浮动就会换行 -->
CSS:
.left {
float: left;
width: 100px;
}
.right {
margin-left: 120px;
}
/*子元素的浮动会受影响,IE有间隔3px的bug*/
改进方案
HTML:
\<div class="parent1">
<div class="left1">
<p>left</p>
</div>
<div class="right-fix">
<div class="right1">
<p>right</p>
<p>right</p>
</div>
</div>
</div>
CSS:
.left1 {
float: left;
width: 100px;
}
.right-fix {
float: right;
width: 100%;
margin-left: -100px;
/*使得right部分不会换行,IE6下没有间隔3px问题*/
}
.right1 {
margin-left: 120px;
}
2、float+overflow布局
HTML同1,
CSS:
.left {
float: left;
width: 100px;
}
.right {
overflow: hidden;
}
/* overflow不为visiable会触发BFC模式,使得right内部不受浮动影响垂直上下排列。不支持IE6 */
3、table布局
HTML同1
CSS:
.parent {
display: table;
width: 100%;
table-layout: fixed;
/* 上边这句话是因为table是根据内容设置宽度,这句话使得优先布局而不是内容,而且会加速table的渲染速度 */
}
.left,
.right {
display: table-cell;
/* 小tips:table-cell不能设置margin但是可以设置padding */
}
.left {
width: 100px;
/* table的特点就是所有table-cell加在一起就是table总宽,所以设置了left的宽度余下的right会自己占满 */
}
4、flex布局
HTML同1
CSS:
.parent {
display: flex;
}
.left {
width: 100px;
margin-right: 20px;
}
.right {
flex:1 1 0;
}
/* flex布局是根据内容来的,影响性能,不适合整个页面布局,适合局部小范围布局 */
###左边不定宽,右边自适应
1、float+overflow布局
HTML同上1
CSS:
.left {
float: left;
margin-right: 20px;
}
.right {
overflow: hidden;
}
.left p {
width: 200px;
/* 靠里面内容撑开 */
}
2、table布局
CSS:
.parent {
display: table;
width: 100%;
/* table-layout: fixed; */
/* 上边这句话是注释掉使得table仍然是根据内容设置宽度 */
}
.left,
.right {
display: table-cell;
}
.left {
width: 0.1%;
/* 宽度设置成极小,不能不设或者1px,否则在IE8有问题 */
/* table的特点就是所有table-cell加在一起就是table总宽,所以设置了left的宽度余下的right会自己占满 */
}
.left p {
width: 200px;
/* 靠里面内容撑开 */
}
3、flex布局
CSS:
.parent2 {
display: flex;
}
.left2 {
/* width: 100px; */
background: #eee;
margin-right: 20px;
}
.right2 {
background: #000;
flex: 1 1 0;
}
.left2 p {
width: 200px;
/* 靠里面内容撑开 */
}
/* flex布局只有兼容性问题*/
BTW table和flex都是默认拉伸左右等高,table存在因为使用padding背景色会填充间距的问题,可以设置backgroud只填充content来解决
而 float做不到左右等高,必须左右设置9999px的padding-bottom和-9999px的margin-bottom来撑开,然后用overflow:hidden截取掉
###多列等宽
1、float
HTML:
\<div class="parent">
<div class="column">
<p>1</p>
</div>
<div class="column">
<p>2</p>
</div>
<div class="column">
<p>3</p>
</div>
<div class="column">
<p>4</p>
</div>
</div>
CSS:
.parent {
margin-left: -20px;
/* 因为设计初衷是四块包括间距在内等分,但是只要三个间距,所以要把父容器延展,留出最左边间距的空间 */
}
.column {
float: left;
width: 25%;
padding-left: 20px;
box-sizing: border-box;
/* border-box将间距包裹在里面 */
}
2、table
HTML:
\<div class="parent-fix">
<div class="parent1">
<div class="column1">
<p>1</p>
</div>
<div class="column1">
<p>2</p>
</div>
<div class="column1">
<p>3</p>
</div>
<div class="column1">
<p>4</p>
</div>
</div>
</div>
CSS:
.parent-fix{
margin-left:-20px;
/* table没法margin */
}
.parent1{
display:table;
width:100%;
table-layout:fixed;
/* fixed特性:除了之前说的之外,多列不设宽度自动等分 */
}
.column1{
display:table-cell;
padding-left:20px;
}
3、flex布局
HTML同1
CSS:
.parent{
display:flex;
}
.column{
flex:1;
/* 不需要考虑留出多余空间,因为flex:1就是在先扣掉margin之后等分 */
}
.column+.column{
margin-left:20px;
}
在布局中,居中是很实用的技巧,很惭愧,会的不多
刚好有机会多接触思考一些居中方法,记录一下
###css水平居中:
\<div class=“parent”>
\<div class=“child”>DEMO</div>
\</div>
1、
.child{
display:inline-block/*内容多宽就多宽*/
}
.parent{
text-align:center;/*只对inline有效*/
}
/*兼容性好,但是center会被继承*/
2、
.child{
display:table;/*类似block,但是宽度跟着内容走*/
margin:0 auto;
}
/*IE8兼容性好,不受parent影响*/
3、
.parent{
position:relative;/*给子元素的absolute提供定位参考*/
}
.child{
position:absolute;/*参照父元素位置,且宽度也随着元素走*/
left:50%;/*只有左边居中*/
transform:translateX(-50%);/*自身宽度50%左移*/
}
/*脱离文档流不受其他元素影响,但是CSS3兼容差*/
4、
.parent{
display:flex;/*宽度auto*/
justify-content:center;/*居中对齐*/
}
或者加一个
.child{
margin: 0 auto;
}
/*不用动子元素但是兼容差*/
###垂直居中
1、
.parent{
display: table-cell;/*单元格布局,根据元素高度划分*/
vertical-align:middle;/*中间*/
}
/*兼容IE8,只修改父元素*/
2、
.parent{
position:relative;
}
.child{
position:absolute;
top:50%;
transform:translateY(-50%);
}
/*有缺点同水平3*/
3、
.parent{
display:flex;/*child会拉伸占满父元素高度*/
align-items:center;
}
/*优缺点同水平4*/
###水平垂直居中
水平+垂直
2+1
3+2
4+3