SuRuiGit

SuRuiGit 查看完整档案

南京编辑宿州学院  |  网络工程 编辑华泰证券  |  前端工程师 编辑 github.com/SuRuiGit 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

SuRuiGit 发布了文章 · 2020-07-30

react组件---完全可控组件、非可控的组件与派生state

一、派生state常见使用问题

大部分使用派生 state 导致的问题,不外乎两个原因:1,直接复制 props 到 state 上;2,如果 props 和 state 不一致就更新 state

直接复制props到state

最常见的误解就是 getDerivedStateFromPropscomponentWillReceiveProps 只会在 props “改变”时才会调用。实际上只要父级重新渲染时,这两个生命周期函数就会重新调用,不管 props 有没有“变化”。所以,在这两个方法内直接复制(_unconditionally_)props 到 state 是不安全的。这样做会导致 state 后没有正确渲染

class EmailInput extends Component {
  state = {
    email: this.props.email
  };

  render() {
    return <input onChange={this.handleChange} value={this.state.email} />;
  }

  handleChange = event => {
    this.setState({ email: event.target.value });
  };
  
  componentWillReceiveProps(nextProps) {
    // Do not do this!
    if (nextProps.email !== this.state.email) {
        this.setState({ email: nextProps.email });
    }
  }
}

class Timer extends Component {
  state = {
    count: 0
  };

  componentDidMount() {
    this.interval = setInterval(
      () =>
        this.setState(prevState => ({
          count: prevState.count + 1
        })),
      1000
    );
  }

  componentWillUnmount() {
    clearInterval(this.interval);
  }

  render() {
    return (
      <Fragment>
        <blockquote>请输入邮箱:</blockquote>
        <EmailInput email="example@google.com" />
        <p>
          此组件每秒会重新渲染一次
        </p>
      </Fragment>
    );
  }
}
render(<Timer />, document.getElementById("root"));

state 的初始值是 props 传来的,当在 <input> 里输入时,修改 state。但是如果父组件重新渲染,我们输入的所有东西都会丢失,即使在重置 state 前比较 nextProps.email !== this.state.email 仍然会导致更新。
这个小例子中,使用 shouldComponentUpdate ,比较 props 的 email 是不是修改再决定要不要重新渲染。但是在实践中,一个组件会接收多个 prop,任何一个 prop 的改变都会导致重新渲染和不正确的状态重置。加上行内函数和对象 prop,创建一个完全可靠的 shouldComponentUpdate 会变得越来越难。而且 shouldComponentUpdate 的最佳实践是用于性能提升,而不是改正不合适的派生 state

在 props 变化后修改 state

继续上面的示例,我们可以只使用 props.email 来更新组件,这样能防止修改 state 导致的 bug

class EmailInput extends Component {
  state = {
    email: this.props.email
  };

  render() {
    return <input onChange={this.handleChange} value={this.state.email} />;
  }

  handleChange = event => {
    this.setState({ email: event.target.value });
  };
  
  componentWillReceiveProps(nextProps) {
    // Do not do this!
    if (nextProps.email !== this.props.email) {
        this.setState({ email: nextProps.email });
    }
  }
}

现在组件只会在 prop 改变时才会改变,但是仍然有个问题。想象一下,如果这是一个密码输入组件,拥有同样 email 的两个账户进行切换时,这个输入框不会重置(用来让用户重新登录)。因为父组件传来的 prop 值没有变化!

幸运的是,有两个方案能解决这些问题。这两者的关键在于,任何数据,都要保证只有一个数据来源,而且避免直接复制它。我们来看看这两个方案。

一、完全可控组件

我们都知道React表单中有受控组件,那么什么是完全可控组件呢,看下列示例你就明白了

function ControlledEmailInput(props) {
  return (
    <label>
      Email: <input value={props.email} onChange={props.handleChange} />
    </label>
  );
}

class App extends Component {
  state = {
    draftEmail: 'some.email@test.com'
  };

  handleEmailChange = event => {
    this.setState({ draftEmail: event.target.value });
  };

  resetForm = () => {
    this.setState({
      draftEmail: 'some.email@test.com'
    });
  };

  render() {
    return (
      <Fragment>
        <h1>此示例展示了什么是完全可控组件</h1>
        <ControlledEmailInput
          email={this.state.draftEmail}
          handleChange={this.handleEmailChange}
        />
        <button onClick={this.resetForm}>重置</button>
      </Fragment>
    );
  }
}
render(<App />, document.getElementById("root"));

从上示例我们就知道了,这不正是我们所见过的组件间通信嘛,子组件中的email完全受父组件数据控制就像提线木偶一样

二、有key的完全不受控组件

让子组件自己存储临时的state数据,子组件仍然可以从 prop 接收“初始值”,但是更改之后的值就和 prop 没关系了

class EmailInput extends Component {
  state = { email: this.props.defaultEmail };

  handleChange = event => {
    this.setState({ email: event.target.value });
  };
  
  resetForm = () => {
    this.setState({ email: 'some.email@test.com' });
  };

  render() {
    return <input onChange={this.handleChange} value={this.state.email} />;
  }
}

class App extends Component {
  inputRef = React.createRef();
  
  state = {
    draftEmail: 'some.email@test.com',
  };
  
  resetForm = () => {
    this.inputRef.current.resetForm()
  };

  render() {
    return (
      <Fragment>
        <h1>此示例展示了什么是有Key的非可控组件</h1>
        <EmailInput
          defaultEmail={this.state.draftEmail}
          ref={this.inputRef}
        />
        <button onClick={this.resetForm}>重置</button>
      </Fragment>
    );
  }
}
render(<App />, document.getElementById("root"));

总结

设计组件时,重要的是确定组件是受控组件还是非受控组件。
不要直接复制 props 的值到 state 中,而是去实现一个受控的组件,然后在父组件里合并两个值。
对于不受控的组件,当你想在 prop 变化时重置 state 的话,可以选择一下几种方式:

  • 建议: 重置内部所有的初始 state,使用 key 属性
  • 选项一:仅更改某些字段,观察特殊属性的变化(比如 props.userID)。
  • 选项二:使用 ref 调用实例方法。
查看原文

赞 1 收藏 1 评论 0

SuRuiGit 发布了文章 · 2020-06-09

react组件---生命周期函数

生命周期函数图谱

QQ20200609-0.png

一、常用的生命周期函数

1.render()

render()
注意

(1) render() 方法是 class 组件中唯一必须实现的方法
(2) render() 函数应该为纯函数

这意味着在不修改组件 state 的情况下,每次调用时都返回相同的结果,并且它不会直接与浏览器交互。如需与浏览器进行交互,请在 `componentDidMount()` 或其他生命周期方法中执行你的操作。保持 `render()` 为纯函数,可以使组件更容易思考。

(3) 如果 shouldComponentUpdate()返回 false,则不会调用render()

2.constructor()

在 React 组件挂载之前,会调用它的构造函数

constructor(props) {
  super(props);
  // 不要在这里调用 this.setState()
  this.state = { counter: 0 };
  this.handleClick = this.handleClick.bind(this);
}

如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数。通常,在 React 中,构造函数仅用于以下两种情况:

注意:

(1) 在 constructor() 函数中不要调用 setState() 方法。如果你的组件需要使用内部 state,请直接在构造函数中为 this.state 赋值初始 state
(2) 要避免在构造函数中引入任何副作用或订阅。如遇到此场景,请将对应的操作放置在 componentDidMount
(3)避免将 props 的值复制给 state!这是一个常见的错误

constructor(props) {
 super(props);
 // 不要这样做
 this.state = { color: props.color };
}

3.componentDidMount()

componentDidMount() 会在组件挂载后(插入 DOM 树中)立即调用,依赖于 DOM 节点的初始化应该放在这里。如需通过网络请求获取数据,添加订阅等

componentDidMount()
注意:

(1) 你可以在 componentDidMount()直接调用 setState()

它将触发额外渲染,但此渲染会发生在浏览器更新屏幕之前。如此保证了即使在 `render()` 两次调用的情况下,用户也不会看到中间状态。请谨慎使用该模式,因为它会导致性能问题。通常,你应该在 `constructor()` 中初始化 state。如果你的渲染依赖于 DOM 节点的大小或位置,比如实现 modals 和 tooltips 等情况下,你可以使用此方式处理

4.componentDidUpdate()

componentDidUpdate() 会在更新后会被立即调用。首次渲染不会执行此方法。

componentDidUpdate(prevProps, prevState, snapshot)
componentDidUpdate(prevProps) {
  // 典型用法(不要忘记比较 props):
  if (this.props.userID !== prevProps.userID) {
    this.fetchData(this.props.userID);
  }
}
注意:

(1) 你也可以在 componentDidUpdate()直接调用 setState(),但请注意它必须被包裹在一个条件语句里,正如上述的例子那样进行处理,否则会导致死循环。它还会导致额外的重新渲染,虽然用户不可见,但会影响组件性能。不要将 props “镜像”给 state,请考虑直接使用 props。
(2) 如果 shouldComponentUpdate() 返回值为 false,则不会调用 componentDidUpdate()

5.componentWillUnmount()

componentWillUnmount()

componentWillUnmount() 会在组件卸载及销毁之前直接调用。在此方法中执行必要的清理操作,例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅等。

注意:

(1) componentWillUnmount()不应调用 setState(),因为该组件将永远不会重新渲染。组件实例卸载后,将永远不会再挂载它。

二、不常用的生命周期函数

1.shouldComponentUpdate()

当 props 或 state 发生变化时,shouldComponentUpdate() 会在渲染执行之前被调用。返回值默认为 true。首次渲染或使用 forceUpdate() 时不会调用该方法。

shouldComponentUpdate(nextProps, nextState)

如果 shouldComponentUpdate() 返回 false,则不会调用 UNSAFE_componentWillUpdate()componentDidUpdate()

注意:

(1) 此方法仅作为性能优化的方式存在,不要试图通过此方法阻止重新渲染,可以使用内置的PureComponent组件,PureComponent会对props 和 state 进行浅层比较,并减少了跳过必要更新的可能性
(2) 不建议在 shouldComponentUpdate() 中进行深层比较或使用 JSON.stringify()。这样非常影响效率,且会损害性能

2.static getDerivedStateFromProps()

getDerivedStateFromProps 会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。它应返回一个对象来更新 state,如果返回 null 则不更新任何内容

static getDerivedStateFromProps(props, state)
注意:

(1) 不管原因是什么,都会在每次渲染前触发此方法。这与 UNSAFE_componentWillReceiveProps 形成对比,后者仅在父组件重新渲染时触发,而不是在内部调用 setState

3.getSnapshotBeforeUpdate()

getSnapshotBeforeUpdate() 在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期的任何返回值将作为参数传递给 componentDidUpdate()

getSnapshotBeforeUpdate(prevProps, prevState)

三、16.13版本过时的生命周期函数

1.UNSAFE_componentWillMount()

UNSAFE_componentWillMount() 在挂载之前被调用。它在 render() 之前调用,因此在此方法中同步调用 setState() 不会触发额外渲染。通常,我们建议使用 constructor() 来初始化 state。

UNSAFE_componentWillMount()
注意:

(1) 避免在此方法中引入任何副作用或订阅。如遇此种情况,请改用 componentDidMount()
(2) 此方法是服务端渲染唯一会调用的生命周期函数

2.UNSAFE_componentWillReceiveProps()

UNSAFE_componentWillReceiveProps() 会在已挂载的组件接收新的 props 之前被调用。如果你需要更新状态以响应 prop 更改(例如,重置它),你可以比较 this.propsnextProps 并在此方法中使用 this.setState() 执行 state 转换

UNSAFE_componentWillReceiveProps(nextProps)
注意:

(1) 如果父组件导致组件重新渲染,即使 props 没有更改,也会调用此方法。如果只想处理更改,请确保进行当前值与变更值的比较

3.UNSAFE_componentWillUpdate()

当组件收到新的 props 或 state 时,会在渲染之前调用 UNSAFE_componentWillUpdate()。使用此作为在更新发生之前执行准备更新的机会。初始渲染不会调用此方法。

UNSAFE_componentWillUpdate(nextProps, nextState)
注意:

(1) 不能此方法中调用 this.setState()
(2) 此方法可以替换为 componentDidUpdate()。如果你在此方法中读取 DOM 信息(例如,为了保存滚动位置),则可以将此逻辑移至 getSnapshotBeforeUpdate()

四、其他API

1.setState()

setState(updater, [callback])

setState() 并不总是立即更新组件。它会批量推迟更新。这使得在调用 setState() 后立即读取 this.state 成为了隐患。为了消除隐患,请使用 componentDidUpdate 或者 setState 的回调函数(setState(updater, callback)),这两种方式都可以保证在应用更新后触发。

this.setState((state, props) => {
  return {counter: state.counter + props.step};
});

2.forceUpdate()

component.forceUpdate(callback)

默认情况下,当组件的 state 或 props 发生变化时,组件将重新渲染。如果 render() 方法依赖于其他数据,则可以调用 forceUpdate() 强制让组件重新渲染。
调用 forceUpdate() 将致使组件调用 render() 方法,此操作会跳过该组件的 shouldComponentUpdate()。但其子组件会触发正常的生命周期方法,包括 shouldComponentUpdate() 方法。如果标记发生变化,React 仍将只更新 DOM。

查看原文

赞 1 收藏 1 评论 0

SuRuiGit 发布了文章 · 2020-06-09

react组件---组件间通信

一、父组件向子组件通信

React 数据流动是单向的,父组件向子组件的 通信也是最常见的方式。父组件通过 props 向子组件传递需要的信息

function EmailInput(props) {
  return (
    <label>
      Email: <input value={props.email} />
    </label>
  );
}

const element = <EmailInput email="123124132@163.com" />;
ReactDOM.render(
  element,
  document.getElementById('root')
);

二、子组件向父组件通信

回调函数

function EmailInput(props) {
  return (
    <label>
      Email: <input value={props.email} onChange={props.onChangeEmail} />
    </label>
  );
}

class App extends React.Component{
   constructor(props){
     super(props);
     this.state = {
       email: ''
     }
   }
   onChangeEmail(e) => {
     console.log(e.target.value)
     this.setState({
       email: e.target.value
     });
   }
   render(){
     let { email } = this.state;
     return (
       <EmailInput email={ eamil } onChangeEmail={this.onChangeEmail} />;
     )
   }
}

ReactDOM.render(
  <APP />,
  document.getElementById('root')
);

三、跨级组件通信Context

Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言

不使用Context的情况

class App extends React.Component {
  render() {
    return <Toolbar theme="dark" />;
  }
}

function Toolbar(props) {
  // Toolbar 组件接受一个额外的“theme”属性,然后传递给 ThemedButton 组件。
  // 如果应用中每一个单独的按钮都需要知道 theme 的值,这会是件很麻烦的事,
  // 因为必须将这个值层层传递所有组件。
  return (
    <div>
      <ThemedButton theme={props.theme} />
    </div>
  );
}

class ThemedButton extends React.Component {
  render() {
    return <Button theme={this.props.theme} />;
  }
}

使用Context的情况

// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext('light');
class App extends React.Component {
  render() {
    // 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
    // 无论多深,任何组件都能读取这个值。
    // 在这个例子中,我们将 “dark” 作为当前的值传递下去。
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  // 指定 contextType 读取当前的 theme context。
  // React 会往上找到最近的 theme Provider,然后使用它的值。
  // 在这个例子中,当前的 theme 值为 “dark”。
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}

Context 主要应用场景在于很多不同层级的组件需要访问同样一些的数据。请谨慎使用,因为这会使得组件的复用性变差。使用 context 比较好的场景是真正意义上的全局信息且不会更改,例如界面主题、用户信息等
如果你只是想避免层层传递一些属性,组件组合(component composition)有时候是一个比 context 更好的解决方案

props层层传递user和avatarSize

<Page user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<PageLayout user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<NavigationBar user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<Link href={user.permalink}>
  <Avatar user={user} size={avatarSize} />
</Link>

将Avatar组件自身向下传递

function Page(props) {
  const user = props.user;
  const userLink = (
    <Link href={user.permalink}>
      <Avatar user={user} size={props.avatarSize} />
    </Link>
  );
  return <PageLayout userLink={userLink} />;
}

// 现在,我们有这样的组件:
<Page user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<PageLayout userLink={...} />
// ... 渲染出 ...
<NavigationBar userLink={...} />
// ... 渲染出 ...
{props.userLink}

这种对组件组合减少了在你的应用中要传递的 props 数量,这在很多场景下会使得你的代码更加干净,使你对根组件有更多的把控。但是,这并不适用于每一个场景:这种将逻辑提升到组件树的更高层次来处理,会使得这些高层组件变得更复杂,并且会强行将低层组件适应这样的形式,这可能不会是你想要的

而且你的组件并不限制于接收单个子组件。你可能会传递多个子组件,甚至会为这些子组件(children)封装多个单独的“接口(slots)”,React 元素本质就是对象(object),所以你可以把它们当作 props,像其他数据一样传递。这种方法可能使你想起别的库中“槽”(slot)的概念,但在 React 中没有“槽”这一概念的限制,你可以将任何东西作为 props 进行传递。

function Page(props) {
  const user = props.user;
  const content = <Feed user={user} />;
  const topBar = (
    <NavigationBar>
      <Link href={user.permalink}>
        <Avatar user={user} size={props.avatarSize} />
      </Link>
    </NavigationBar>
  );
  return (
    <PageLayout
      topBar={topBar}
      content={content}
    />
  );
}
查看原文

赞 6 收藏 3 评论 0

SuRuiGit 发布了文章 · 2020-06-08

react表单---受控组件与非受控组件

一、受控组件

在 React 中,表单元素通过组件的 state 属性来自己维护 state,并根据用户输入调用setState()来进行数据更新,使 React 的 state 成为“唯一数据源”,被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”。

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: ''};

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }

  handleSubmit(event) {
    alert('提交的名字: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          名字:
          <input type="text" value={this.state.value} onChange={this.handleChange} />
        </label>
        <input type="submit" value="提交" />
      </form>
    );
  }
}

在 React 中,数据是单向流动的。从示例中,我们能看出来表单的数据源于组件的 state,并 通过 props 传入,这也称为单向数据绑定。然后,我们又通过 onChange 事件处理器将新的表单数 据写回到组件的 state,完成了双向数据绑定。这也意味着我们可以在执行最后 一步 setState 前,对表单值进行清洗和校验。

React 受控组件更新 state 的流程:
(1)可以通过在初始 state 中设置表单的默认值。
(2)每当表单的值发生变化时,调用 onChange 事件处理器。
(3)事件处理器通过合成事件对象 e 拿到改变后的状态,并更新应用的 state。
(4)setState 触发视图的重新渲染,完成表单组件值的更新。

二、非受控组件

如果一个表单组件没有 value props(单选按钮和复选框对应的是 checked prop) 时,就可以称为非受控组件。相应地,你可以使用 defaultValue 和 defaultChecked prop 来表示 组件的默认状态

元素value属性change回调回调获取新值
<input type="text" />value="string"onChangeevent.target.value
<input type="checkbox" />checked={boolean}onChangeevent.target.checked
<input type="radio" />checked={boolean}onChangeevent.target.checked
<textarea />value="string"onChangeevent.target.value
<select />value="option value"onChangeevent.target.value
class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.input = React.createRef();
  }

  handleSubmit(event) {
    alert('A name was submitted: ' + this.input.current.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" defaultValue="Bob" ref={this.input} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

二、受控组件与非受控组件对比

受控组件和非受控组件各有优点,应该根据自己的具体需求选择受控还是非受控

特征非受控受控
一次性取值(比如提交表单时)
提交时验证
实时表单验证
有条件的禁用提交按钮
强制输入格式
一个数据有多个输入
动态输入
查看原文

赞 3 收藏 2 评论 0

SuRuiGit 收藏了文章 · 2019-12-17

vue+node项目部署上线

在线地址: cl8023.comgithub

程序员词汇学习网: www.english4coder.com

云服务器

阿里云 or 腾讯云

  • 阿里云服务器品牌:ECS(Elastic Compute Service)
  • 腾讯云服务器品牌:VCM(Cloud Virtual Machine)

腾讯云or阿里云

两者都可以,具体可以根据自己的需求,都说阿里云稳定,腾讯云便宜,我自己买时发现两者入门级的价格都差不多,就买了阿里云的,以下即以阿里云的服务器操作。(腾讯云服务器操作应该也类似)

购买阿里云服务器ECS

入门级最低配即可,一年300多,每月几十块钱,也可以月付,那样就贵点。

中间有些选项默认就可,镜像选择 公共镜像-CentOS-7.4 64位(最新的)
图中密码用来之后远程登陆服务器使用。

登陆服务器

阿里网页登陆

在 管理控制台-实例 中可以看到刚刚购买的服务器

点击远程连接,出现登陆界面,第一次进入会弹出一个密码,记住这个密码(只会出现一次),之后登陆输入这个密码即可进入阿里云服务器ECS系统。

客户端工具远程登陆

  1. Mac

终端中输入:SSH root@服务器IP地址(公) (SSH root@192.18.222.12)
回车
输入购买服务器时设置的实例密码即可

  1. Windows
  • 下载工具 Xshell
  • 打开Xshell - 文件 - 新建,终端选项选择编码:Unicode(UTF-8)

  • 连接成功

配置环境

Linux 常用命令:

  1. wget:一个从网络上自动下载文件的自由工具,支持通过 HTTP、HTTPS、FTP 三个最常见的 TCP/IP协议 下载,并可以使用 HTTP 代理。"wget" 这个名称来源于 “World Wide Web” 与 “get” 的结合。
  2. tar:压缩解压命令

    • -c:建立压缩档案
    • -x:解压
    • -t:查看内容
    • -r:向压缩归档文件末尾追加文件
    • -u:更新原压缩包中的文件

这五个是独立的命令,压缩解压都要用到其中一个,可以和别的命令连用但只能用其中一个。下面的参数是根据需要在压缩或解压档案时可选的。

- -z:有gzip属性的
- -j:有bz2属性的
- -Z:有compress属性的
- -v:显示所有过程
- -O:将文件解开到标准输出
下面的参数 -f 是必须的
- -f:使用档案名称,最后一个参数,后面只能接档案名
  1. ln:为某一个文件或目录在另一个位置建立一个同步的链接 常用:ln -s 源文件 目标文件
  2. makdir:创建目录
  3. mv:为文件或目录改名、或将文件或目录移入其它位置
  4. rm:删除文件

    • -f:忽略不存在的文件,从不给出提示
    • -r:将参数中列出的全部目录和子目录均递归的删除
  5. yum:提供了查找、安装、删除某一个、一组甚至全部软件包的命令
  6. ls:显示当前目录下文件, ls -f 隐藏文件也显示
  7. netstat -tpln:查看进程端口
  8. kill -9 PID号:关闭进程
  9. cp:拷贝

Linux 目录:
前面进入Linux系统后,一般会在 root(~) 目录下 [root@xxxxxxxxxxx ~]# , cd ..可以即回到根目录,ls查看当前目录下文件

[root@xxxxxxxxxxx ~]#
[root@xxxxxxxxxxx ~]# cd ..
[root@xxxxxxxxxxx /]#
[root@xxxxxxxxxxx /]# ls
bin  boot  dev  etc  home  lib  lib64  lost+found  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
[root@xxxxxxxxxxx /]# cd root
[root@xxxxxxxxxxx ~]#

安装NodeJs

阿里云帮助文档:部署Node.js项目(CentOS)

安装MySQL

主要参考

1. 下载安装包

为了下载到最新的版本,先到官网上找到下载链接
MySQL下载地址

先用浏览器或其他下载工具创建下载任务(如x86,64-bit),然后在下载中找到下载链接复制下来就可以把它删了。

  • 进入root目录:cd /root (也可以其他目录)
  • 下载安装包:

wget https://cdn.mysql.com//Downloads/MySQL-5.7/mysql-5.7.20-linux-glibc2.12-x86_64.tar.gz

  • 下载完成后 ls 可以看到下载的安装包
[root@xxxxxxxxxxx ~]# ls
mysql-5.7.20-linux-glibc2.12-x86_64.tar.gz ......

2. 解压文件

tar -xzvf mysql-5.7.19-linux-glibc2.12-x86_64.tar.gz -C /usr/local/

[root@xxxxxxxxxxx ~]# ls
mysql-5.7.20-linux-glibc2.12-x86_64 (解压得到的目录)
mysql-5.7.20-linux-glibc2.12-x86_64.tar.gz

// 拷贝解压到目录到 /usr/local 目录下,并改名为 mysql
[root@xxxxxxxxxxx ~]# cp mysql-5.7.20-linux-glibc2.12-x86_64 /usr/local/mysql -r
[root@xxxxxxxxxxx ~]# cd /usr/local/mysql
[root@xxxxxxxxxxx mysql]# ls
bin  COPYING  docs  include  lib  man  README  share  support-files

3. 添加系统mysql组和mysql用户

[root@xxxxxxxxxxx ~]# groupadd mysql #建立一个mysql的组
[root@xxxxxxxxxxx ~]# useradd -r -g mysql mysql #建立mysql用户,并且把用户放到mysql组

4. 在 mysql 下添加 data 目录

[root@xxxxxxxxxxx mysql]# mkdir data

5. 更改mysql目录下所有的目录及文件夹所属组合用户

[root@xxxxxxxxxxx mysql]# cd /usr/local/
[root@xxxxxxxxxxx local]# chown -R mysql mysql/
[root@xxxxxxxxxxx local]# chgrp -R mysql mysql/
[root@xxxxxxxxxxx local]# cd mysql/
[root@xxxxxxxxxxx mysql]# ls -l
total 56
drwxr-xr-x  2 mysql mysql  4096 Nov  9 16:00 bin
-rw-r--r--  1 mysql mysql 17987 Nov  9 16:00 COPYING
drwxr-xr-x  6 mysql mysql  4096 Nov  9 18:41 data
drwxr-xr-x  2 mysql mysql  4096 Nov  9 16:00 docs
drwxr-xr-x  3 mysql mysql  4096 Nov  9 16:01 include
drwxr-xr-x  5 mysql mysql  4096 Nov  9 16:01 lib
drwxr-xr-x  4 mysql mysql  4096 Nov  9 16:00 man
-rw-r--r--  1 mysql mysql  2478 Nov  9 16:00 README
drwxr-xr-x 28 mysql mysql  4096 Nov  9 16:00 share
drwxr-xr-x  2 mysql mysql  4096 Nov  9 18:06 support-files

6. 安装和初始化数据库

很多老的教程中都是运行 ./scripts/mysql_install_db --user=mysql 进行安装,但在新版本的mysql中已经没了 scripts 目录,
mysql_install_db 放在了 bin 目录下

[root@xxxxxxxxxxx mysql]# cd bin
[root@xxxxxxxxxxx bin]# ./mysqld --initialize --user=mysql --basedir=/usr/local/mysql/--datadir=/usr/local/mysql/data/


2017-11-09T09:09:52.826209Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).
2017-11-09T09:09:54.885578Z 0 [ERROR] Can't find error-message file '/usr/local/mysql/--datadir=/usr/local/mysql/data/share/errmsg.sys'. Check error-message file location and 'lc-messages-dir' con
figuration directive.2017-08-31T08:50:24.709286Z 0 [Warning] InnoDB: New log files created, LSN=45790
2017-11-09T09:09:55.105938Z 0 [Warning] InnoDB: Creating foreign key constraint system tables.
2017-11-09T09:09:55.218562Z 0 [Warning] No existing UUID has been found, so we assume that this is the first time that this server has been started. Generating a new UUID: c0844cc4-c52d-11e7-b74f-00163e0ae84e.
2017-11-09T09:09:55.221300Z 0 [Warning] Gtid table is not ready to be used. Table 'mysql.gtid_executed' cannot be opened.
2017-11-09T09:09:55.221784Z 1 [Note] A temporary password is generated for root@localhost: uf)qP3+C?jpJ

解决:(无视警告)

[root@xxxxxxxxxxx bin]# ./mysqld --initialize --user=mysql --basedir=/usr/local/mysql/ --datadir=/usr/local/mysql/data/ --lc_messages_dir=/usr/local/mysql/share --lc_messages=en_US

7. 配置my.cnf

进入 /usr/local/mysql/support-files/ 目录下,查看是否存在my-default.cnf 文件,如果存在直接 copy 到 /etc/my.cnf 文件中

[root@xxxxxxxxxxx mysql]# cp -a ./support-files/my-default.cnf /etc/my.cnf

如果不存在 my-default.cnf 文件, 则在 /etc/ 目录下创建 my.cnf

[root@xxxxxxxxxxx bin]# cd /etc
[root@xxxxxxxxxxx etc]# vim my.cnf

写入内容

#[mysql]
#basedir=/usr/local/mysql/
#datadir=/usr/local/mysql/data/

8. 启动服务

[root@xxxxxxxxxxx mysql]# cd bin/
[root@xxxxxxxxxxx bin]# ./mysqld_safe --user=mysql &

9. 将mysqld服务加入开机自启动项

[root@xxxxxxxxxxx bin]# cd ../support-files
[root@xxxxxxxxxxx support-files]# cp mysql.server /etc/init.d/mysql
[root@xxxxxxxxxxx support-files]# chmod +x /etc/init.d/mysql
-- 把mysql注册为开机启动的服务
[root@xxxxxxxxxxx support-files]# chkconfig --add mysql

10. 启动服务

[root@xxxxxxxxxxx bin]# service mysql start

若报错 ERROR! The server quit without updating PID file

[root@xxxxxxxxxxx mysql]# rm  /etc/my.cnf
rm: remove regular file '/etc/my.cnf'? y
[root@xxxxxxxxxxx mysql]# /etc/init.d/mysql start
Starting MySQL.Logging to '/usr/local/mysql/data/dbserver.err'.
 SUCCESS!
[root@xxxxxxxxxxx mysql]# service mysql start
Starting MySQL SUCCESS!

11. 登录mysql

[root@xxxxxxxxxxx bin]# ./mysql -u root -p
密码是第6步产生的密码

如果出现错误:

ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: NO)

重改密码

[root@xxxxxxxxxxx bin]# /etc/init.d/mysql stop
[root@xxxxxxxxxxx bin]# mysqld_safe --user=mysql --skip-grant-tables --skip-networking &
[root@xxxxxxxxxxx bin]# mysql -u root mysql
mysql> UPDATE user SET Password=PASSWORD('newpassword') where USER='root';

// 上面语句若出错,换为
update mysql.user set authentication_string=password('newpassword') where user='root'

mysql> FLUSH PRIVILEGES;
mysql> quit

[root@xxxxxxxxxxx bin]# /etc/init.d/mysqld restart
[root@xxxxxxxxxxx bin]# mysql -uroot -p
Enter password:

mysql>

12. 设置远程登录权限

mysql>  grant all privileges on *.* to'root' @'%' identified by 'root';
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql> flush privileges;
Query OK, 0 rows affected (0.06 sec)

mysql> quit
Bye

13. 进程关闭

若以上步骤中出现其他错误,可以看看 mysql 是否关闭了,先关闭端口,然后在试试

[root@xxxxxxxxxxx ~]# netstat -tpln
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      1105/sshd
tcp6       0      0 :::3306                 :::*                    LISTEN      25599/mysqld
[root@xxxxxxxxxxx ~]# kill -9 25599

14. 本地连接数据库

我本地使用的是 Navicat for MySQL
远程连接数据库
远程连接数据库后,创建数据表(可以导出本地数据表,然后Navicat中导入到服务器MySQL中)

上传文件

打包文件

项目根目录下运行

npm run build

等待命令运行结束后,会发现目录下多了 dist 文件夹,这个文件夹就是我们等下要放到服务器中的。

文件传输

  1. 下载文件传输工具 Xftp
  2. 打开 Xftp 新建连接,类似Xshell,选项中勾选 “使用UTF-8编码(E)”

Xftp连接
连接成功后可以看到左侧是本地文件目录,右侧是服务器文件目录,可以很方便的来回拖放文件。

  1. 创建目录文件 /root/projec/myblog (目录层级、名称随意,这里我以次为项目目录)
  2. 将刚刚的 dist 文件夹复制到 /root/project/myblog 目录下,前端资源就OK了
  3. 将 server 文件夹也复制到 /root/project/myblog 目录下

初始化项目

Xshell 连接服务器

// 进入项目目录
[root@izwz9e9bjg74ljcpzr7stvz ~]# cd /root/project/myblog
[root@izwz9e9bjg74ljcpzr7stvz myblog]# ls
dist server

初始化创建 package.json,这一步也可以在本地创建编辑好后上传到服务器目录即可

[root@izwz9e9bjg74ljcpzr7stvz myblog]# npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help json` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
package name: (myblog) 
version: (1.0.0) 
description: 
entry point: (index.js) 
test command: 
git repository: 
keywords: 
author: 
license: (ISC) 
About to write to /root/project/test/myblog/package.json:

{
  "name": "myblog",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}


Is this ok? (yes) yes

// 全部回车即可
[root@izwz9e9bjg74ljcpzr7stvz myblog]# ls
dist  package.json  server

// 打开 package.json 编辑(也可在 Xftp 中右键文件编辑)
[root@izwz9e9bjg74ljcpzr7stvz myblog]# vim package.json

    {
        "name": "my-blog",
        "version": "1.0.0",
        "description": "A Vue.js project",
        "author": "ChenLiang <236338364@qq.com>",
        "private": true,
        "scripts": {
            "dev": "node build/dev-server.js",
            "start": "node build/dev-server.js",
            "build": "node build/build.js"
        },
        "dependencies": {
            "body-parser": "^1.17.2",
            "cookie-parser": "^1.4.3",
            "express": "^4.16.2",
            "express-session": "^1.15.5",
            "formidable": "^1.1.1",
            "highlight.js": "^9.12.0",
            "marked": "^0.3.6",
            "mysql": "^2.14.0",
            "node-sass": "^4.5.3",
            "node-uuid": "^1.4.8"
        },
        "engines": {
            "node": ">= 4.0.0",
            "npm": ">= 3.0.0"
        },
        "browserslist": [
            "> 1%",
            "last 2 versions",
            "not ie <= 8"
        ]
    }

保存退出,运行

[root@izwz9e9bjg74ljcpzr7stvz myblog]# npm install

安装"dependencies"中项目运行需要的所有依赖

修改资源路径

进入文件夹 server,打开 index.js

[root@izwz9e9bjg74ljcpzr7stvz server]# vim index.js

const routerApi = require('./router');
const path = require('path');
const bodyParser = require('body-parser');
const express = require('express');
const app = express();
const cookieParser = require('cookie-parser');
const session = require('express-session');

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false}));
app.use(cookieParser());
app.use(session({
    secret: '8023',
    // cookie: {maxAge: 60000},
    resave: false,
    saveUninitialized: true
}));

// 部署上线时读取静态文件
app.use(express.static(path.join(__dirname, '../dist')));

// 后端api路由
app.use('/api', routerApi);

// 监听端口
app.listen(80);
console.log('success listen at port:80......');

设置静态资源路径,并修改监听端口为80(HTTP端口),api.js 中文件路径相关的也要更改为 ../dist/static.....,嫌麻烦的也可以直接将 server 文件夹移到 dist 下就不用这么麻烦改了。

开放 80 端口

登陆阿里云,进入控制管理台 -> 云服务器 ECS -> 安全组 -> 配置规则 -> 快速创建规则
开放80端口

启动服务

[root@izwz9e9bjg74ljcpzr7stvz server]# node index.js
success listen at port:80......

浏览器打开 服务器IP:80(如:263.182.35.68:80),如无意外,即正常运行访问啦。

绑定域名

进入域名管理后台,解析域名,添加解析
域名绑定
添加主机 @.xxx.com 可以通过 xxx.com 直接访问
绑定成功后,直接输入域名即可访问。

安装 pm2

pm2 是一个带有负载均衡功能的Node应用的进程管理器.

上面我们以 node index.js 启动了项目,当我们退出 Xshell 时,进程就会关闭,无法在访问到项目,而 pm2 就是
解决这种问题的,以 pm2 启动项目后,退出 Xshell 后依然可以正常访问。

// 安装 pm2
[root@izwz9e9bjg74ljcpzr7stvz /]# npm install -g pm2

// 以 -g 全局安装的插件都在 node 安装目录 bin 文件下,
[root@izwz9e9bjg74ljcpzr7stvz bin]# ls
cnpm  node  npm  npx  pm2  pm2-dev  pm2-docker  pm2-runtime

bin 下都是命令语句,为了可以在任何目录都可以使用命令,我们将此文件夹加入环境变量

  1. 查看环境变量 [root@izwz9e9bjg74ljcpzr7stvz ~]# echo $PATH
  2. 永久添加环境变量(影响所有用户)
[root@izwz9e9bjg74ljcpzr7stvz ~]# vim /etc/profile
// 在文档最后,添加:
# node
export NODE_HOME=/root/node-v8.9.1-linux-x64
export PATH=$PATH:$NODE_HOME/bin

保存,退出,然后运行

[root@izwz9e9bjg74ljcpzr7stvz ~]# source /etc/profile

pm2 启动项目

[root@izwz9e9bjg74ljcpzr7stvz ~]# cd /root/project/myblog/server
// 启动进程
[root@izwz9e9bjg74ljcpzr7stvz server]# pm2 start index.js
// 停止进程
[root@izwz9e9bjg74ljcpzr7stvz server]# pm2 stop index.js
// 查看进程
[root@izwz9e9bjg74ljcpzr7stvz server]# pm2 list

刷新页面404

HTML5 History 模式,最后有nginx的配置。

Linux中文乱码 (修改默认编码)

如文件或文件夹含有中文字符时,可能会读取乱码,读取不到文章,需要修改系统默认编码
修改默认编码

Nginx 服务器

上面我们是直接以 node 启动一个服务器,监听 80 端口,这样我们就可以直接以 IP 地址或域名的方式访问,也可以监听其他端口如3000,这样我们就得在地址后加上 : 端口号,显然这样很麻烦,且一般 node 程序基本不监听 80 端口,还可能同时运行几个 node 项目,监听不同的端口,通过二级域名来分别访问。 这里就用到 Nginx 来实现反向代理。(node 利用 node-http-proxy 包也可以实现反向代理,有兴趣自己了解)

Nginx安装

Nginx依赖下面3个包:

  1. SSL功能需要openssl库,下载地址 http://www.openssl.org/
  2. rewrite模块需要pcre库,下载地址 http://www.pcre.org/
  3. gzip模块需要zlib库,下载地址 http://www.zlib.net/
  4. Nginx安装包

进入任意目录下载以上压缩包(版本号改为最新即可):

[root@izwz9e9bjg74ljcpzr7stvz download]# wget http://www.zlib.net/zlib-1.2.11.tar.gz
[root@izwz9e9bjg74ljcpzr7stvz download]# wget https://ftp.pcre.org/pub/pcre/pcre-8.41.tar.gz
[root@izwz9e9bjg74ljcpzr7stvz download]# wget https://www.openssl.org/source/openssl-fips-2.0.16.tar.gz
[root@izwz9e9bjg74ljcpzr7stvz download]# wget http://nginx.org/download/nginx-1.13.7.tar.gz
[root@izwz9e9bjg74ljcpzr7stvz download]# ls
pcre-8.41.tar.gz   zlib-1.2.11.tar.gz
nginx-1.13.7.tar.gz  openssl-fips-2.0.16.tar.gz

解压压缩包:

[root@izwz9e9bjg74ljcpzr7stvz download]# tar zxvf zlib-1.2.11.tar.gz
[root@izwz9e9bjg74ljcpzr7stvz download]# tar tar zxvf pcre-8.41.tar.gz
[root@izwz9e9bjg74ljcpzr7stvz download]# tar zxvf openssl-fips-2.0.16.tar.gz
[root@izwz9e9bjg74ljcpzr7stvz download]# tar zxvf nginx-1.13.7.tar.gz

先安装3个依赖包,分别进入各自解压目录

// 看清各个目录下的是 configure 还是 config
[root@izwz9e9bjg74ljcpzr7stvz zlib-1.2.11]# ./configuer && make && make install
[root@izwz9e9bjg74ljcpzr7stvz pcre-8.41]# ./configuer && make && make install
[root@izwz9e9bjg74ljcpzr7stvz openssl-fips-2.0.16]# ./config && make && make install
[root@izwz9e9bjg74ljcpzr7stvz nginx-1.13.7]# ./configure --with-pcre=../pcre-8.41/ --with-zlib=../zlib-1.2.11/ --with-openssl=../openssl-fips-2.0.16/
[root@izwz9e9bjg74ljcpzr7stvz nginx-1.13.7]# make && make install

安装 C++ 编译环境 (上面安装过程中如若有报错,可以看看是不是因为没有安装这个,可提前安装)

yum install gcc-c++

运行Nginx

安装好的Nginx路径在 /usr/local/nginx

[root@izwz9e9bjg74ljcpzr7stvz ~]# cd /usr/local/nginx
[root@izwz9e9bjg74ljcpzr7stvz nginx]# ls
client_body_temp  conf  fastcgi_temp  html  logs  nginx.conf  proxy_temp  sbin  scgi_temp  uwsgi_temp

配置文件路径:

/usr/local/nginx/conf/nginx.conf

运行Nginx:

[root@izwz9e9bjg74ljcpzr7stvz ~]# cd /usr/local/nginx/sbin
[root@izwz9e9bjg74ljcpzr7stvz sbin]# ./nginx
// 查看是否运行成功
[root@izwz9e9bjg74ljcpzr7stvz sbin]# netstat -ntlp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      3525/nginx: master

浏览器输入 IP 地址或域名即可见到欢迎页面。

使用server命令启动nginx服务

现在nginx启动、关闭比较麻烦,关闭要找到PID号,然后杀死进程,启动要进入到 /usr/local/nginx/sbin 目录下使用命令,为此我们通过设置System V脚本来使用server命令启动、关闭、重启nginx服务。

  1. 在 /etc/init.d 目录下创建nginx启动脚本文件

    [root@izwz9e9bjg74ljcpzr7stvz ~]# cd /etc/init.d
    [root@izwz9e9bjg74ljcpzr7stvz init.d]# vim nginx
  2. 将以下代码复制粘贴进去,然后保存。 注意 NGINX_BIN、CONFIGFILE、PIDFILE 三个目录要对应好,默认是对应好的。在网上找了好多相关脚本代码,都有很多问题,好像是和 CentOS 版本有关,下面脚本我在 CentOS 7 下使用正常。

    #! /bin/sh
    # chkconfig: 2345 55 25
    # Description: Startup script for nginx webserver on Debian. Place in /etc/init.d and
    # run 'update-rc.d -f nginx defaults', or use the appropriate command on your
    # distro. For CentOS/Redhat run: 'chkconfig --add nginx'
    
    ### BEGIN INIT INFO
    # Provides:          nginx
    # Required-Start:    $all
    # Required-Stop:     $all
    # Default-Start:     2 3 4 5
    # Default-Stop:      0 1 6
    # Short-Description: starts the nginx web server
    # Description:       starts nginx using start-stop-daemon
    ### END INIT INFO
    
    # Author:   licess
    # website:  http://lnmp.org
    
    PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
    NAME=nginx
    NGINX_BIN=/usr/local/nginx/sbin/$NAME
    CONFIGFILE=/usr/local/nginx/conf/$NAME.conf
    PIDFILE=/usr/local/nginx/logs/$NAME.pid
    
    case "$1" in
        start)
            echo -n "Starting $NAME... "
    
            if netstat -tnpl | grep -q nginx;then
                echo "$NAME (pid `pidof $NAME`) already running."
                exit 1
            fi
    
            $NGINX_BIN -c $CONFIGFILE
    
            if [ "$?" != 0 ] ; then
                echo " failed"
                exit 1
            else
                echo " done"
            fi
            ;;
    
        stop)
            echo -n "Stoping $NAME... "
    
            if ! netstat -tnpl | grep -q nginx; then
                echo "$NAME is not running."
                exit 1
            fi
    
            $NGINX_BIN -s stop
    
            if [ "$?" != 0 ] ; then
                echo " failed. Use force-quit"
                exit 1
            else
                echo " done"
            fi
            ;;
    
        status)
            if netstat -tnpl | grep -q nginx; then
                PID=`pidof nginx`
                echo "$NAME (pid $PID) is running..."
            else
                echo "$NAME is stopped"
                exit 0
            fi
            ;;
    
        force-quit)
            echo -n "Terminating $NAME... "
    
            if ! netstat -tnpl | grep -q nginx; then
                echo "$NAME is not running."
                exit 1
            fi
    
            kill `pidof $NAME`
    
            if [ "$?" != 0 ] ; then
                echo " failed"
                exit 1
            else
                echo " done"
            fi
            ;;
    
        restart)
            $0 stop
            sleep 1
            $0 start
            ;;
    
        reload)
            echo -n "Reload service $NAME... "
    
            if netstat -tnpl | grep -q nginx; then
                $NGINX_BIN -s reload
                echo " done"
            else
                echo "$NAME is not running, can't reload."
                exit 1
            fi
            ;;
    
        configtest)
            echo -n "Test $NAME configure files... "
    
            $NGINX_BIN -t
            ;;
    
        *)
            echo "Usage: $0 {start|stop|force-quit|restart|reload|status|configtest}"
            exit 1
            ;;
    
    esac
  3. 修改脚本权限

    chmod a+x /etc/init.d/nginx
  4. 注册成服务

    chkconfig --add nginx
  5. 设置开机启动

    chkconfig nginx on

这样就可以在任意目录通过service启动、关闭nginx

[root@izwz9e9bjg74ljcpzr7stvz ~]# service nginx start
[root@izwz9e9bjg74ljcpzr7stvz ~]# service nginx stop
[root@izwz9e9bjg74ljcpzr7stvz ~]# service nginx restart

配置nginx.conf反向代理多个node项目

  1. 启动多个node项目,分别监听不同端口,如

    • 项目1,监听端口3000,为博客项目,域名访问 www.cl8023.com 或 cl8023.com
    • 项目2,监听端口8023,为游戏项目,域名访问 game.cl8023.com
  2. 在阿里云服务区控制台开放端口3000和8023,(80端口是必须的,nginx监听)
  3. 绑定二级域名 game.cl8023.com,添加域名解析

    • 记录类型:A
    • 主机记录:game
    • 解析线路:默认
    • 记录纸:IP地址
    • TTL至:10分钟(默认)
  4. 修改nginx配置
    进入目录 /usr/local/nginx/conf 修改配置文件nginx.conf

    [root@izwz9e9bjg74ljcpzr7stvz ~]# cd /usr/local/nginx/conf
    [root@izwz9e9bjg74ljcpzr7stvz conf]# ls
    fastcgi.conf          fastcgi_params          koi-utf  mime.types          nginx.conf          scgi_params          uwsgi_params          win-utf
    fastcgi.conf.default  fastcgi_params.default  koi-win  mime.types.default  nginx.conf.default  scgi_params.default  uwsgi_params.default
    [root@izwz9e9bjg74ljcpzr7stvz conf]# vim nginx.conf
    // server 内容替换为
        server {
            listen 80;
            server_name game.cl8023.com;
            location / {
                proxy_set_header   Host      $http_host;
                proxy_pass         http://127.0.0.1:8023;
                proxy_redirect     off;
                proxy_set_header   X-Real-IP       $remote_addr;
                proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
            }
        }
    
        server {
            listen 80;
            server_name cl8023.com www.cl8023.com;
            # 解决刷新404的问题
            location /blog {
                try_files $uri $uri/ /index.html;
            }
            location / {
                proxy_set_header   Host      $http_host;
                proxy_pass         http://127.0.0.1:3000;
                proxy_redirect     off;
                proxy_set_header   X-Real-IP       $remote_addr;
                proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
            }
        }

    若只配置一个server,game.cl8023.com、cl8023.com、www.cl8023.com 都将可以访问到这个端口。想要反响代理更多端口,可再增加server,也可以将server单独出来为一个文件,如game-8023.conf,blog-3000.conf,然后在nginx.conf中引入文件地址即可

    http {
        ......
        include ./vhost/game-8023.conf; 
        include ./vhost/blog-3000.conf;
        ......
    }
  5. 重启nginx

    [root@izwz9e9bjg74ljcpzr7stvz ~]# service nginx restart

无误的话便可以使用不同的域名访问不同的项目。

查看原文

SuRuiGit 评论了文章 · 2018-11-28

微信小程序开发---自定义tabBar

最近开发微信小程序,公司要求做一个类似闲鱼的tabbar,但是网上大多资料的tabbar都会在页面切换的时候重新渲染,所以我写了一个不会重新渲染的tabbar,有需要的直接拿走不谢。https://github.com/SuRuiGit/w...

使用步骤如下:

第一步:找到项目中的tabbarComponent目录,拷贝到你的工程中,然后将tabbarComponent->icon图标替换成你自己的tabbar图片

第二步:到app.json中配置tabBar,这里我就不细说了,只强调闲鱼的tabbar中间的按钮是跳到二级页面,所以不配置在tabBar的list中

第三步:在app.js的onLaunch方法中调用wx.hideTabBar();隐藏系统自带的tabBar

第四步:在app.js中的globalData中加入自定义tabbar的参数,再加入一个方法给tabBar.list配置中的页面使用,代码如下

globalData: {
    userInfo: null,
    tabBar: {
      "backgroundColor": "#ffffff",
      "color": "#979795",
      "selectedColor": "#1c1c1b",
      "list": [
        {
          "pagePath": "/page/index/index",
          "iconPath": "icon/icon_home.png",
          "selectedIconPath": "icon/icon_home_HL.png",
          "text": "首页"
        },
        {
          "pagePath": "/page/myRelease/index",
          "iconPath": "icon/icon_release.png",
          "isSpecial": true,
          "text": "发布"
        },
        {
          "pagePath": "/page/mine/index",
          "iconPath": "icon/icon_mine.png",
          "selectedIconPath": "icon/icon_mine_HL.png",
          "text": "我的"
        }
      ]
    }
  }
editTabbar: function() {
    let tabbar = this.globalData.tabBar;
    let currentPages = getCurrentPages();
    let _this = currentPages[currentPages.length - 1];
    let pagePath = _this.route;
    (pagePath.indexOf('/') != 0) && (pagePath = '/' + pagePath);
    for (let i in tabbar.list) {
      tabbar.list[i].selected = false;
      (tabbar.list[i].pagePath == pagePath) && (tabbar.list[i].selected = true);
    }
    _this.setData({
      tabbar: tabbar
    });
  },

第五步:在tabBar的list中配置的页面的.js文件的data中加入tabbar:{},并在onload方法中调用app.editTabbar();

第六步:在tabBar的list中配置的页面的.json文件中加入

"usingComponents": {

"tabbar": "../../tabbarComponent/tabbar"

}

在.wxml文件中加入<tabbar tabbar="{{tabbar}}"></tabbar>

到目前为止,自定义tabbar就完成啦,撒花!!!!!

查看原文

SuRuiGit 收藏了文章 · 2018-11-05

2018你成长了么?一份给你的前端技术清单

2018 眼看就要过去了,今年的你相较去年技术上有怎样的收获呢?

记得年初的时候我给自己制定了一个学习计划,现在回顾来看完成度还不错。但仍有些遗憾,一些技术点没有时间去好好学习。

在学习中我发现,像文章这样的知识往往是碎片化的,而前端涉及到的面很多,如果不将这些知识有效梳理,则无法形成体系、相互串联。最后有一种东懂一块,西了解一点的感觉。因此,我结合工作体会抽象出了一些前端基础技术能力,并将这段时间学习或产出的一些不错的内容根据这些能力进行整理,形成了一份前端技术清单(github 地址)。

不论你是正在自学前端遇到了瓶颈,还是对某些技术熟练掌握但某些还未涉足,都希望这份清单能对你有所帮助。

由于个人精力有限,一些技术点的归纳可能有失偏颇,或者目前并未纳入进来,因此 github 上的清单内容 也会不断更新。目前只包含纯前端基础内容,NodeJS 、客户端泛前端、小程序、可视化等内容先留着坑吧。

清单内容↓↓↓

0. 年度报告

1. 基础拾遗

温故而知新,不知则习之,是以牢固根基。

1.1. JavaScript

1.2. CSS

1.3. 浏览器

2. 工程化与工具

软件规模的扩大带来了工程化的需求,前端也不例外。随着 NodeJS 的出现,前端工程师可以使用熟悉的 JS 快速开发所需的工具。工具链生态的繁荣也是前端圈繁荣的一个写照。

2.1. webpack

2.2. Gulp

2.3. Linter

2.4. 静态类型(Typescript/Flow)

2.5. Babel

2.6. CSS预处理与模块化

3. 性能优化

性能优化其实就是在理解浏览器的基础上“因地制宜”,因此可以配合1.3节“浏览器”部分进行理解。

强烈推荐把 Google Web 上性能优化 Tab 中的文章都通读一遍,其基本涵盖了现代浏览器中性能优化的所有点,非常系统。下面也摘录了其中一些个人认为非常不错的篇幅。

3.1. 加载性能

3.2. 运行时性能

3.3. 前端缓存

3.4. 性能调试与实践

3.5. 性能指标

4. 安全

很多安全风险老生常谈,但是往往到出现问题时,才会被重视或者意识到。

4.1. XSS

4.2. CSRF

4.3. CSP

4.4. HTTPS

4.5. 安全实录

5. 自动化测试

自动化测试是软件工程的重要部分之一,但却极容易被忽视。

5.1. 单元测试

5.2. 端到端测试 (E2E)

5.3. 其他

6. 框架与类库

如果说基础知识是道,那框架与工具可能就是术;学习与理解它们,但千万不要成为它们的奴隶。

6.1. React

6.2. Vue

6.3. Redux

6.4. RxJS

7. 新技术/方向

前端领域新技术、新方向层出不穷,这里汇总一些新技术方向;作为开发者需要多了解但是不要盲从

7.1. PWA

7.2. CSS Houdini

7.3. Web Components

7.4. 微前端(Micro Frontends)

7.5. HTTP/2

7.6. WebAssembly

8. 业务相关

在业务中往往还有一些与“业务无关”的场景需求,不论是什么业务几乎都会遇到;因此,在变与不变中,我们更需要去抽象出这些问题。

8.1. 数据打点上报

8.2. 前端监控

8.3. A/B测试

8.4. “服务端推”

8.5. 动效

9. 不归类的好文

开卷有益。
查看原文

SuRuiGit 关注了标签 · 2018-10-24

前端

Web前端开发是从网页制作演变而来的,名称上有很明显的时代特征。在互联网的演化进程中,网页制作是Web 1.0时代的产物,那时网站的主要内容都是静态的,用户使用网站的行为也以浏览为主。2005年以后,互联网进入Web 2.0时代,各种类似桌面软件的Web应用大量涌现,网站的前端由此发生了翻天覆地的变化。网页不再只是承载单一的文字和图片,各种富媒体让网页的内容更加生动,网页上软件化的交互形式为用户提供了更好的使用体验,这些都是基于前端技术实现的。

Web前端优化
  1. 尽量减少HTTP请求 (Make Fewer HTTP Requests)
  2. 减少 DNS 查找 (Reduce DNS Lookups)
  3. 避免重定向 (Avoid Redirects)
  4. 使得 Ajax 可缓存 (Make Ajax Cacheable)
  5. 延迟载入组件 (Post-load Components)
  6. 预载入组件 (Preload Components)
  7. 减少 DOM 元素数量 (Reduce the Number of DOM Elements)
  8. 切分组件到多个域 (Split Components Across Domains)
  9. 最小化 iframe 的数量 (Minimize the Number of iframes)
  10. 杜绝 http 404 错误 (No 404s)

关注 194880

SuRuiGit 关注了标签 · 2018-10-24

react.js

React (sometimes styled React.js or ReactJS) is an open-source JavaScript library for creating user interfaces that aims to address challenges encountered in developing single-page applications. It is maintained by Facebook, Instagram and a community of individual developers and corporations.

关注 69849

SuRuiGit 发布了文章 · 2018-10-23

react工程搭建系列之---移动端适配与antd-mobile高清适配方案

一、逻辑像素(css像素)与物理像素(设备像素)

机型逻辑像素物理像素Scale Factor
iphone 3GS320 x 480320 x 4801x
iphone 4320 x 480640 x 9602x
iphone 4S320 x 480640 x 9602x
iphone 5320 x 568640 x 11362x
iphone 5C320 x 568640 x 11362x
iphone 5S320 x 568640 x 11362x
iphone 5SE320 x 568640 x 11362x
iphone 6375 x 667750 x 11342x
iphone 6P414 x 7361080 x 19202.6x
iphone 6S375 x 667750 x 11342x
iphone 6SP414 x 7361080 x 19202.6x
iphone 7375 x 667750 x 11342x
iphone 7P414 x 7361080 x 19202.6x
  • 设备像素:设备硬件的物理像素
  • 逻辑像素:软件所支持的像素
  • dpr(Device Pixel Ratio: Number of device pixels per CSS Pixel): 设备像素比
    也叫dppx 就是一个css像素控制几个物理像素,物理分辨率/逻辑分辨率(css分辨率)= dpr
  • iphone 3GS,可以看到一个逻辑像素是由一个物理像素构成,随着技术发展出现了Retina屏使得设备分辨率提高一倍,一个逻辑像素可以由 (640/320)* (960/480) = 4个物理像素构成,这样屏幕看起来更清晰

图片描述

二、三种viewport

1.the visual viewport

the visual viewport是在屏幕上显示页面的一部分,用户可以滚动以更改他看到的页面部分,或者缩放以更改可视视口的大小
图片描述

the visual viewport的大小等于window.innerWidth/Height

2.the layout viewport

css布局尤其是百分比宽是相对于the layout viewport来计算的,the layout viewport比the visual viewport宽的多。
浏览器会控制layout viewport尺寸使其在完全缩小的情况下覆盖整个屏幕,这时the visual viewport=the layout viewport
图片描述

因此,the layout viewport的宽度和高度等于在最大缩小模式下可以在屏幕上显示的任何宽度和高度。当用户放大这些尺寸时保持不变
图片描述

the layout viewport的大小等于document.documentElement.clientWidth/Height

3. the ideal viewport

它为每个设备上的web页面提供了一个理想尺寸,每个设备的理想尺寸都会不同。在非Retina屏的时代,the ideal viewport等于物理像素数,但这不是必须的。具有高物理像素密度的新型设备任然保留了原有的ideal viewport,因为它非常适合设备。
4S以上版本包含4S,iPhone理想的视口是320x480,无论它是否有视网膜屏幕。那是因为320x480是这些iPhone上web页面的理想尺寸。

关于ideal viewport有两点很关键:

  1. the layout viewport可以被设置成the ideal viewport,使用meta标签的The width=device-width 和initial-scale=1指令实现
  2. 所有的scale指令是相对于the ideal viewport而言,不管the layout viewport拥有多大的宽度,因此maximum-scale=3 意味着web页面可以放大到the ideal viewport的300%

三、meta viewport

1.meta viewport标签

meta viewport标签包含有关视口(viewports)和缩放(zooming)的浏览器指令。特别是,它允许Web开发人员设置layout viewport的宽度,这个宽度直接影响到width:20%这样的css声明的计算

meta viewport标签具有以下语法:

<meta name="viewport" content="name=value,name=value">

2.指令

viewport mata标签的每一对name/value都是一条指令。总共有6条指令:

  1. width: 用来设置layout viewport的宽度。
  2. initial-scale: 用来设置页面的初始缩放值以及layout viewport的宽度。
  3. minimum-scale: 用来设置允许的最小缩放值(例如,用户可以缩小至什么程度)。
  4. maximum-scale: 用来设置允许的最大缩放值(例如,用户可以放大至什么程度)。
  5. height: 期望用于设置layout viewport的高度。但一直没被支持。
  6. user-scalable: 当设置为no时,则禁止用户进行缩放。

3.device-width值

width指令有一个特殊的值:device-width。它能将layout viewport的宽度设置成ideal viewport宽度。 理论上同样有一个类似的device-height值,但实际上这个值并不起作用。

四、缩放对viewport的影响

1.缩放

缩放是棘手的。理论上讲很简单:确定用户可以放大或缩小的缩放系数(zoom factor)。这里存在两个问题:

  1. 我们不能够直接读取缩放系数,而是需要读取visual viewport的宽度,它与缩放系数成反比关系。缩放系数越大,visual viewport的宽度越小。因此,最小缩放系数决定了最大visual viewport宽度,反之亦然。
  2. 事实证明,无论layout viewport的当前大小是什么,所有缩放因子都相对于ideal viewport

因此关于缩放这个名字的问题,缩放实际上是比例,而viewport meta的指令称之为initial-scale、minimum-scale、maximum-scale。其它浏览器为了保持和针对iPhone适配的网站兼容也只好被迫实现了这些指令。
这三个指令期望一个缩放因子,例如2意味着“缩放到ideal viewport宽度的200%”

2.公式

visual viewport width = ideal viewport width / zoom factor
zoom factor = ideal viewport width / visual viewport width

3.理解

我们先来梳理一下:

  • 我们平时开发的css是基于layout viewport来计算
  • 在完全缩小的情况下layout viewport=visual viewport
  • 使用meta viewport的width指令设置layout viewport的宽,当width=device-width 和initial-scale=1时,layout viewport=ideal viewport。以iphone4S为例,ideal width是320,此时layout viewport也是320,初始缩放系数是1也就是没有缩放
  • 当用户进行缩放的时候layout viewport是不会变的,visual viewport与缩放系数成反比

我的理解:

这里以手机拍照为例,用手机后置摄像头拍摄电脑上的一个网页,调整手机与电脑之间的距离使得整个页面刚好拍进手机里,但是网页变小了,这时layout viewport=visual viewport,这种情况就叫做完全缩小
缩放系数,如果我想让网页变大,调近手机与电脑之间的距离,这时网页变大了,但是网页看不全了也就是可视区域变小了;同理我想让网页变小,那么调远手机与电脑之间的距离,这时网页变小了,但是网页能看到的东西多了,也就是可视区域变大了

五、移动端适配方案

1.目前行业内流行几种适配方法

  • JS根据屏幕动态计算 使用js判断页面宽度算出页面应有的font-size
  • 媒体查询 使用媒体查询 来兼容不同尺寸屏幕 设置不同尺寸下的rem大小
  • flex布局 CSS3中提出的新布局方案 移动端的兼容性较好

使用rem作为移动端尺寸单位替代px,那么1rem=?px
目前1rem有三种方案:

  1. 1rem=16px
    这个是默认的大小
  2. 1rem= 75px
    这个是手淘团队在flexible方案中在iphone6中的显示结果
    flexible方案核心就是根据屏幕的dpr和尺寸 动态算出当前页的rem大小 动态的修改meta标签
    该方案目前也被应用在手淘首页中
  3. 1rem=100px
    这个是阿里旗下的蚂蚁金服在Ant-mobile中的方案
    ant-mobile也有自己高清解决方案 其核心跟flexible类似
    现应用于ant-mobile中

如果项目中使用的是1rem=16px,又集成了antd-mobile,那么就会导致antd-mobile中的组件特别小,这就面临着方案转换的问题,如果我们在项目的样式中以rem作为单位,现在是16px转100px,如果以后用75px那么所有的样式文件中rem就都需要进行转换工程量很大。所以,样式文件中我们还使用px作为单位,然后使用插件将px转成rem,这样就算有方案转换,我们也只需要修改插件中的配置和一些脚本文件

图片描述

六、高清适配方案

1.在public/index.html中删除meta viewport标签,然后用下列代码动态生成meta viewport标签

'use strict';

/**
 * @param {Number} [baseFontSize = 100] - 基础fontSize, 默认100px;
 * @param {Number} [fontscale = 1] - 有的业务希望能放大一定比例的字体;
 */
const win = window;
export default win.flex = (baseFontSize, fontscale) => {
  const _baseFontSize = baseFontSize || 100;
  const _fontscale = fontscale || 1;

  const doc = win.document;
  const ua = navigator.userAgent;
  const matches = ua.match(/Android[\S\s]+AppleWebkit\/(\d{3})/i);
  const UCversion = ua.match(/U3\/((\d+|\.){5,})/i);
  const isUCHd = UCversion && parseInt(UCversion[1].split('.').join(''), 10) >= 80;
  const isIos = navigator.appVersion.match(/(iphone|ipad|ipod)/gi);
  let dpr = win.devicePixelRatio || 1;
  if (!isIos && !(matches && matches[1] > 534) && !isUCHd) {
    // 如果非iOS, 非Android4.3以上, 非UC内核, 就不执行高清, dpr设为1;
    dpr = 1;
  }
  const scale = 1 / dpr;

  let metaEl = doc.querySelector('meta[name="viewport"]');
  if (!metaEl) {
    metaEl = doc.createElement('meta');
    metaEl.setAttribute('name', 'viewport');
    doc.head.appendChild(metaEl);
  }
  metaEl.setAttribute('content', `width=device-width,user-scalable=no,initial-scale=${scale},maximum-scale=${scale},minimum-scale=${scale}`);
  doc.documentElement.style.fontSize = `${_baseFontSize / 2 * dpr * _fontscale}px`;
};
flex(100, 1);

代码理解:以iphone6为准,dpr=2,scale=1/2,fontSize=100;由之前介绍的viewport可以知道,scale=1/2那么visual viewport=2*ideal viewport

2.修改package.json

"theme": {
    "hd": "2px",
    "brand-primary": "red",
    "color-text-base": "#333"
  },

3.修改config-overrides.js在webpack配置中使用 postcss-pxtorem 把 px 转成 rem 单位

安装react-app-rewire-postcss

npm install react-app-rewire-postcss --save-dev

配置postcss,完整代码如下:

const { injectBabelPlugin, getLoader } = require('react-app-rewired');
const rewirePostcss = require('react-app-rewire-postcss');
const pxtorem = require('postcss-pxtorem');
const autoprefixer = require('autoprefixer');
const theme = require('./package.json').theme;
const fileLoaderMatcher = function (rule) {
    return rule.loader && rule.loader.indexOf(`file-loader`) != -1;
}
module.exports = function override(config, env) {
    // do stuff with the webpack config...
    config = injectBabelPlugin(['import', {
        libraryName: 'antd-mobile',
        // style: 'css',
        style: true, // use less for customized theme
    }], config);
    console.log(config.module.rules[2].oneOf);

    // sass
    config.module.rules[2].oneOf.unshift(
        {
            test: /\.scss$/,
            use: [
                require.resolve('style-loader'),
                require.resolve('css-loader'),
                require.resolve('sass-loader'),
                {
                    loader: require.resolve('postcss-loader'),
                    options: {
                        // Necessary for external CSS imports to work
                        // https://github.com/facebookincubator/create-react-app/issues/2677
                        ident: 'postcss',
                        plugins: () => [
                            require('postcss-flexbugs-fixes'),
                            autoprefixer({
                                browsers: [
                                    '>1%',
                                    'last 4 versions',
                                    'Firefox ESR',
                                    'not ie < 9', // React doesn't support IE8 anyway
                                ],
                                flexbox: 'no-2009',
                            })
                        ],
                    },
                }
            ]
        }
    );
    //less
    config.module.rules[2].oneOf.unshift(
        {
            test: /\.less$/,
            use: [
                require.resolve('style-loader'),
                require.resolve('css-loader'),
                {
                    loader: require.resolve('postcss-loader'),
                    options: {
                        // Necessary for external CSS imports to work
                        // https://github.com/facebookincubator/create-react-app/issues/2677
                        ident: 'postcss',
                        plugins: () => [
                            require('postcss-flexbugs-fixes'),
                            autoprefixer({
                                browsers: [
                                    '>1%',
                                    'last 4 versions',
                                    'Firefox ESR',
                                    'not ie < 9', // React doesn't support IE8 anyway
                                ],
                                flexbox: 'no-2009',
                            }),
                        ],
                    },
                },
                {
                    loader: require.resolve('less-loader'),
                    options: {
                        // theme vars, also can use theme.js instead of this.
                        modifyVars: theme,
                    },
                },
            ]
        }
    );

    config = rewirePostcss(config,{
        plugins: () => [
            require('postcss-flexbugs-fixes'),
            require('postcss-preset-env')({
                autoprefixer: {
                    flexbox: 'no-2009',
                },
                stage: 3,
            }),
            pxtorem({
                rootValue: 100,    //以100px为准,不同方案修改这里
                propWhiteList: [],
            })
        ],
    });

    // file-loader exclude
    let l = getLoader(config.module.rules, fileLoaderMatcher);
    l.exclude.push(/\.scss$/);
    l.exclude.push(/\.less$/);
    return config;
};

将src/App.css改成scss格式并修改App.js如下:

/*src/App.scss*/
.App {
  text-align: center;
  .App-Button{
    width: 750px;
    height: 88px;
  }
}
/*src/App.js*/
import React, { Component } from 'react';
import './App.scss';
import {Button} from 'antd-mobile';

class App extends Component {
  render() {
    return (
      <div className="App">
          <Button type='primary' className='App-Button'>{document.documentElement.clientWidth}</Button>
      </div>
    );
  }
}

export default App;

最终运行结果如下图:

图片描述

可以看到px已经被转换成rem了,layout viewport = 750px

项目地址:https://github.com/SuRuiGit/m...

查看原文

赞 26 收藏 18 评论 8

认证与成就

  • 获得 73 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2018-01-15
个人主页被 1.4k 人浏览