jinmmd

jinmmd 查看完整档案

北京编辑  |  填写毕业院校百度  |  高级系统工程师 编辑编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

jinmmd 回答了问题 · 2019-04-30

Electron运行程序时HTML页面里Jquery代码没有运行?

如果是用 BrowserWindow 打开的不是自己开发的页面,也就是说无法修改 HTML 代码的。建议可以使用 preload 属性【在页面运行其他脚本之前预先加载指定的脚本】。举例如下:

main.js

mainWindow = new BrowserWindow({
    webPreferences: {
      nodeIntegration: true,
      webSecurity: true,
      preload: path.resolve(path.join(__dirname, 'path/to/preload.js'))
    },
    width: 900,
    height: 900
  });

mainWindow.loadURL("https://externalsite.com");

preload.js

document.addEventListener("DOMNodeInserted", function(event) {
    if (!!window && !(!!window.$)) {
        window.$ = window.jQuery = require('path/to/jquery.js');
    }
});

参考资料:jQuery issue on render side

关注 6 回答 4

jinmmd 赞了文章 · 2019-03-12

React Router 中文文档(一)

官方英文文档 - https://reacttraining.com/rea...
版本 - v4.2.0

<BrowserRouter>

<BrowserRouter> 使用 HTML5 提供的 history API (pushState, replaceStatepopstate 事件) 来保持 UI 和 URL 的同步。

import { BrowserRouter } from 'react-router-dom';

<BrowserRouter
  basename={string}
  forceRefresh={bool}
  getUserConfirmation={func}
  keyLength={number}
>
  <App />
</BrowserRouter>

basename: string

所有位置的基准 URL。如果你的应用程序部署在服务器的子目录,则需要将其设置为子目录。basename 的正确格式是前面有一个前导斜杠,但不能有尾部斜杠。

<BrowserRouter basename="/calendar">
  <Link to="/today" />
</BrowserRouter>

上例中的 <Link> 最终将被呈现为:

<a href="/calendar/today" />

forceRefresh: bool

如果为 true ,在导航的过程中整个页面将会刷新。一般情况下,只有在不支持 HTML5 history API 的浏览器中使用此功能。

const supportsHistory = 'pushState' in window.history;

<BrowserRouter forceRefresh={!supportsHistory} />

getUserConfirmation: func

用于确认导航的函数,默认使用 window.confirm。例如,当从 /a 导航至 /b 时,会使用默认的 confirm 函数弹出一个提示,用户点击确定后才进行导航,否则不做任何处理。译注:需要配合 <Prompt> 一起使用。

// 这是默认的确认函数
const getConfirmation = (message, callback) => {
  const allowTransition = window.confirm(message);
  callback(allowTransition);
}

<BrowserRouter getUserConfirmation={getConfirmation} />

keyLength: number

location.key 的长度,默认为 6

<BrowserRouter keyLength={12} />

children: node

要呈现的单个子元素(组件)

<HashRouter>

<HashRouter> 使用 URL 的 hash 部分(即 window.location.hash)来保持 UI 和 URL 的同步。

import { HashRouter } from 'react-router-dom';

<HashRouter>
  <App />
</HashRouter>
注意: 使用 hash 记录导航历史不支持 location.keylocation.state。在以前的版本中,我们视图 shim 这种行为,但是仍有一些问题我们无法解决。任何依赖此行为的代码或插件都将无法正常使用。由于该技术仅用于支持旧式(低版本)浏览器,因此对于一些新式浏览器,我们鼓励你使用 <BrowserHistory> 代替。

basename: string

所有位置的基准 URL。basename 的正确格式是前面有一个前导斜杠,但不能有尾部斜杠。

<HashRouter basename="/calendar">
  <Link to="/today" />
</HashRouter>

上例中的 <Link> 最终将被呈现为:

<a href="#/calendar/today" />

getUserConfirmation: func

用于确认导航的函数,默认使用 window.confirm

// 这是默认的确认函数
const getConfirmation = (message, callback) => {
  const allowTransition = window.confirm(message);
  callback(allowTransition);
}

<HashRouter getUserConfirmation={getConfirmation} />

hashType: string

window.location.hash 使用的 hash 类型,有如下几种:

  • slash - 后面跟一个斜杠,例如 #/ 和 #/sunshine/lollipops
  • noslash - 后面没有斜杠,例如 # 和 #sunshine/lollipops
  • hashbang - Google 风格的 ajax crawlable,例如 #!/ 和 #!/sunshine/lollipops

默认为 slash

children: node

要呈现的单个子元素(组件)

<Link>

为你的应用提供声明式的、可访问的导航链接。

import { Link } from 'react-router-dom';

<Link to="/about">About</Link>

to: string

一个字符串形式的链接地址,通过 pathnamesearchhash 属性创建。

<Link to='/courses?sort=name' />

to: object

一个对象形式的链接地址,可以具有以下任何属性:

  • pathname - 要链接到的路径
  • search - 查询参数
  • hash - URL 中的 hash,例如 #the-hash
  • state - 存储到 location 中的额外状态数据
<Link to={{
  pathname: '/courses',
  search: '?sort=name',
  hash: '#the-hash',
  state: {
    fromDashboard: true
  }
}} />

replace: bool

当设置为 true 时,点击链接后将替换历史堆栈中的当前条目,而不是添加新条目。默认为 false

<Link to="/courses" replace />

innerRef: func

允许访问组件的底层引用。

const refCallback = node => {
  // node 指向最终挂载的 DOM 元素,在卸载时为 null
}

<Link to="/" innerRef={refCallback} />

others

你还可以传递一些其它属性,例如 titleidclassName 等。

<Link to="/" className="nav" title="a title">About</Link>

<NavLink>

一个特殊版本的 <Link>,它会在与当前 URL 匹配时为其呈现元素添加样式属性。

import { NavLink } from 'react-router-dom';

<NavLink to="/about">About</NavLink>

activeClassName: string

当元素处于激活状态时应用的类,默认为 active。它将与 className 属性一起使用。

<NavLink to="/faq" activeClassName="selected">FAQs</NavLink>

activeStyle: object

当元素处于激活状态时应用的样式。

const activeStyle = {
  fontWeight: 'bold',
  color: 'red'
};

<NavLink to="/faq" activeStyle={activeStyle}>FAQs</NavLink>

exact: bool

如果为 true,则只有在位置完全匹配时才应用激活类/样式。

<NavLink exact to="/profile">Profile</NavLink>

strict: bool

如果为 true,则在确定位置是否与当前 URL 匹配时,将考虑位置的路径名后面的斜杠。有关更多信息,请参阅 <Route strict> 文档。

<NavLink strict to="/events/">Events</NavLink>

isActive: func

添加额外逻辑以确定链接是否处于激活状态的函数。如果你要做的不仅仅是验证链接的路径名与当前 URL 的路径名相匹配,那么应该使用它。

// 只有当事件 id 为奇数时才考虑激活
const oddEvent = (match, location) => {
  if (!match) {
    return false;
  }
  const eventID = parseInt(match.params.eventID);
  return !isNaN(eventID) && eventID % 2 === 1;
}

<NavLink to="/events/123" isActive={oddEvent}>Event 123</NavLink>

location: object

isActive 默认比较当前历史位置(通常是当前的浏览器 URL)。你也可以传递一个不同的位置进行比较。

<Prompt>

用于在位置跳转之前给予用户一些确认信息。当你的应用程序进入一个应该阻止用户导航的状态时(比如表单只填写了一半),弹出一个提示。

import { Prompt } from 'react-router-dom';

<Prompt
  when={formIsHalfFilledOut}
  message="你确定要离开当前页面吗?"
/>

message: string

当用户试图离开某个位置时弹出的提示信息。

<Prompt message="你确定要离开当前页面吗?" />

message: func

将在用户试图导航到下一个位置时调用。需要返回一个字符串以向用户显示提示,或者返回 true 以允许直接跳转。

<Prompt message={location => {
  const isApp = location.pathname.startsWith('/app');

  return isApp ? `你确定要跳转到${location.pathname}吗?` : true;
}} />
译注:上例中的 location 对象指的是下一个位置(即用户想要跳转到的位置)。你可以基于它包含的一些信息,判断是否阻止导航,或者允许直接跳转。

when: bool

在应用程序中,你可以始终渲染 <Prompt> 组件,并通过设置 when={true}when={false} 以阻止或允许相应的导航,而不是根据某些条件来决定是否渲染 <Prompt> 组件。

译注:when 只有两种情况,当它的值为 true 时,会弹出提示信息。如果为 false 则不会弹出。见阻止导航示例。
<Prompt when={true} message="你确定要离开当前页面吗?" />

<MemoryRouter>

将 URL 的历史记录保存在内存中的 <Router>(不读取或写入地址栏)。在测试和非浏览器环境中很有用,例如 React Native

import { MemoryRouter } from 'react-router-dom';

<MemoryRouter>
  <App />
</MemoryRouter>

initialEntries: array

历史堆栈中的一系列位置信息。这些可能是带有 {pathname, search, hash, state} 的完整位置对象或简单的字符串 URL。

<MemoryRouter
  initialEntries={[ '/one', '/two', { pathname: '/three' } ]}
  initialIndex={1}
>
  <App/>
</MemoryRouter>

initialIndex: number

initialEntries 数组中的初始位置索引。

getUserConfirmation: func

用于确认导航的函数。当 <MemoryRouter> 直接与 <Prompt> 一起使用时,你必须使用此选项。

keyLength: number

location.key 的长度,默认为 6

children: node

要呈现的单个子元素(组件)

<Redirect>

使用 <Redirect> 会导航到一个新的位置。新的位置将覆盖历史堆栈中的当前条目,例如服务器端重定向(HTTP 3xx)。

import { Route, Redirect } from 'react-router-dom';

<Route exact path="/" render={() => (
  loggedIn ? (
    <Redirect to="/dashboard" />
  ) : (
    <PublicHomePage />
  )
)} />

to: string

要重定向到的 URL,可以是 path-to-regexp 能够理解的任何有效的 URL 路径。所有要使用的 URL 参数必须由 from 提供。

<Redirect to="/somewhere/else" />

to: object

要重定向到的位置,其中 pathname 可以是 path-to-regexp 能够理解的任何有效的 URL 路径。

<Redirect to={{
  pathname: '/login',
  search: '?utm=your+face',
  state: {
    referrer: currentLocation
  }
}} />

上例中的 state 对象可以在重定向到的组件中通过 this.props.location.state 进行访问。而 referrer 键(不是特殊名称)将通过路径名 /login 指向的登录组件中的 this.props.location.state.referrer 进行访问。

push: bool

如果为 true,重定向会将新的位置推入历史记录,而不是替换当前条目。

<Redirect push to="/somewhere/else" />

from: string

要从中进行重定向的路径名,可以是 path-to-regexp 能够理解的任何有效的 URL 路径。所有匹配的 URL 参数都会提供给 to,必须包含在 to 中用到的所有参数,to 未使用的其它参数将被忽略。

只能在 <Switch> 组件内使用 <Redirect from>,以匹配一个位置。有关更多细节,请参阅 <Switch children>

<Switch>
  <Redirect from='/old-path' to='/new-path' />
  <Route path='/new-path' component={Place} />
</Switch>
// 根据匹配参数进行重定向
<Switch>
  <Redirect from='/users/:id' to='/users/profile/:id' />
  <Route path='/users/profile/:id' component={Profile} />
</Switch>
译注:经过实践,发现以上“根据匹配参数进行重定向”的示例存在bug,没有效果。to 中的 :id 并不会继承 from 中的 :id 匹配的值,而是直接作为字符串显示到浏览器地址栏!!!

exact: bool

完全匹配,相当于 Route.exact

strict: bool

严格匹配,相当于 Route.strict

<Route>

<Route> 可能是 React Router 中最重要的组件,它可以帮助你理解和学习如何更好的使用 React Router。它最基本的职责是在其 path 属性与某个 location 匹配时呈现一些 UI。

请考虑以下代码:

import { BrowserRouter as Router, Route } from 'react-router-dom';

<Router>
  <div>
    <Route exact path="/" component={Home} />
    <Route path="/news" component={News} />
  </div>
</Router>

如果应用程序的位置是 /,那么 UI 的层次结构将会是:

<div>
  <Home />
  <!-- react-empty: 2 -->
</div>

或者,如果应用程序的位置是 /news,那么 UI 的层次结构将会是:

<div>
  <!-- react-empty: 1 -->
  <News />
</div>

其中 react-empty 注释只是 React 空渲染的实现细节。但对于我们的目的而言,它是有启发性的。路由始终在技术上被“渲染”,即使它的渲染为空。只要应用程序的位置匹配 <Route>path,你的组件就会被渲染。

Route render methods

使用 <Route> 渲染一些内容有以下三种方式:

在不同的情况下使用不同的方式。在指定的 <Route> 中,你应该只使用其中的一种。请参阅下面的解释,了解为什么有三个选项。大多数情况下你会使用 component

Route props

三种渲染方式都将提供相同的三个路由属性:

component

指定只有当位置匹配时才会渲染的 React 组件,该组件会接收 route props 作为属性。

const User = ({ match }) => {
  return <h1>Hello {match.params.username}!</h1>
}

<Route path="/user/:username" component={User} />

当你使用 component(而不是 renderchildren)时,Router 将根据指定的组件,使用 React.createElement 创建一个新的 React 元素。这意味着,如果你向 component 提供一个内联函数,那么每次渲染都会创建一个新组件。这将导致现有组件的卸载和新组件的安装,而不是仅仅更新现有组件。当使用内联函数进行内联渲染时,请使用 renderchildren(见下文)。

render: func

使用 render 可以方便地进行内联渲染和包装,而无需进行上文解释的不必要的组件重装。

你可以传入一个函数,以在位置匹配时调用,而不是使用 component 创建一个新的 React 元素。render 渲染方式接收所有与 component 方式相同的 route props

// 方便的内联渲染
<Route path="/home" render={() => <div>Home</div>} />

// 包装
const FadingRoute = ({ component: Component, ...rest }) => (
  <Route {...rest} render={props => (
    <FadeIn>
      <Component {...props} />
    </FadeIn>
  )} />
)

<FadingRoute path="/cool" component={Something} />
警告:<Route component> 优先于 <Route render>,因此不要在同一个 <Route> 中同时使用两者。

children: func

有时候不论 path 是否匹配位置,你都想渲染一些内容。在这种情况下,你可以使用 children 属性。除了不论是否匹配它都会被调用以外,它的工作原理与 render 完全一样。

children 渲染方式接收所有与 componentrender 方式相同的 route props,除非路由与 URL 不匹配,不匹配时 matchnull。这允许你可以根据路由是否匹配动态地调整用户界面。如下所示,如果路由匹配,我们将添加一个激活类:

const ListItemLink = ({ to, ...rest }) => (
  <Route path={to} children={({ match }) => (
    <li className={match ? 'active' : ''}>
      <Link to={to} {...rest} />
    </li>
  )} />
)

<ul>
  <ListItemLink to="/somewhere" />
  <ListItemLink to="/somewhere-else" />
</ul>

这对动画也很有用:

<Route children={({ match, ...rest }) => (
  {/* Animate 将始终渲染,因此你可以利用生命周期来为其子元素添加进出动画 */}
  <Animate>
    {match && <Something {...rest} />}
  </Animate>
)} />
警告:<Route component><Route render> 优先于 <Route children>,因此不要在同一个 <Route> 中同时使用多个。

path: string

可以是 path-to-regexp 能够理解的任何有效的 URL 路径。

<Route path="/users/:id" component={User} />

没有定义 path<Route> 总是会被匹配。

exact: bool

如果为 true,则只有在 path 完全匹配 location.pathname 时才匹配。

<Route exact path="/one" component={OneComponent} />

图片描述

strict: bool

如果为 true,则具有尾部斜杠的 path 仅与具有尾部斜杠的 location.pathname 匹配。当 location.pathname 中有附加的 URL 片段时,strict 就没有效果了。

<Route strict path="/one/" component={OneComponent} />

图片描述

警告:可以使用 strict 来强制规定 location.pathname 不能具有尾部斜杠,但是为了做到这一点,strictexact 必须都是 true

图片描述

location: object

一般情况下,<Route> 尝试将其 path 与当前历史位置(通常是当前的浏览器 URL)进行匹配。但是,也可以传递具有不同路径名的位置进行匹配。

当你需要将 <Route> 与当前历史位置以外的 location 进行匹配时,此功能非常有用。如过渡动画示例中所示。

如果一个 <Route> 被包含在一个 <Switch> 中,并且需要匹配的位置(或当前历史位置)传递给了 <Switch>,那么传递给 <Route>location 将被 <Switch> 所使用的 location 覆盖。

sensitive: bool

如果为 true,进行匹配时将区分大小写。

<Route sensitive path="/one" component={OneComponent} />

图片描述

<Router>

所有 Router 组件的通用低阶接口。通常情况下,应用程序只会使用其中一个高阶 Router:

使用低阶 <Router> 的最常见用例是同步一个自定义历史记录与一个状态管理库,比如 Redux 或 Mobx。请注意,将 React Router 和状态管理库一起使用并不是必需的,它仅用于深度集成。

import { Router } from 'react-router-dom';
import createBrowserHistory from 'history/createBrowserHistory';

const history = createBrowserHistory();

<Router history={history}>
  <App />
</Router>

history: object

用于导航的历史记录对象。

import createBrowserHistory from 'history/createBrowserHistory';

const customHistory = createBrowserHistory();

<Router history={customHistory} />

children: node

要呈现的单个子元素(组件)

<Router>
  <App />
</Router>

<StaticRouter>

一个永远不会改变位置的 <Router>

这在服务器端渲染场景中非常有用,因为用户实际上没有点击,所以位置实际上并未发生变化。因此,名称是 static(静态的)。当你只需要插入一个位置,并在渲染输出上作出断言以便进行简单测试时,它也很有用。

以下是一个示例,node server 为 <Redirect> 发送 302 状态码,并为其它请求发送常规 HTML:

import { createServer } from 'http';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import { StaticRouter } from 'react-router';

createServer((req, res) => {
  // 这个 context 对象包含了渲染的结果
  const context = {};

  const html = ReactDOMServer.renderToString(
    <StaticRouter location={req.url} context={context}>
      <App />
    </StaticRouter>
  );

  // 如果使用 <Redirect>,context.url 将包含要重定向到的 URL
  if (context.url) {
    res.writeHead(302, {
      Location: context.url
    });
    res.end();
  } else {
    res.write(html);
    res.end();
  }
}).listen(3000);

basename: string

所有位置的基准 URL。basename 的正确格式是前面有一个前导斜杠,但不能有尾部斜杠。

<StaticRouter basename="/calendar">
  <Link to="/today" />
</StaticRouter>

上例中的 <Link> 最终将被呈现为:

<a href="/calendar/today" />

location: string

服务器收到的 URL,可能是 node server 上的 req.url

<StaticRouter location={req.url}>
  <App />
</StaticRouter>

location: object

一个形如 {pathname, search, hash, state} 的位置对象。

<StaticRouter location={{ pathname: '/bubblegum' }}>
  <App />
</StaticRouter>

context: object

一个普通的 JavaScript 对象。在渲染过程中,组件可以向对象添加属性以存储有关渲染的信息。

const context = {};

<StaticRouter context={context}>
  <App />
</StaticRouter>

当一个 <Route> 匹配时,它将把 context 对象传递给呈现为 staticContext 的组件。查看服务器渲染指南以获取有关如何自行完成此操作的更多信息。

渲染之后,可以使用这些属性来配置服务器的响应。

if (context.status === '404') {
  // ...
}

children: node

要呈现的单个子元素(组件)

<Switch>

用于渲染与路径匹配的第一个子 <Route><Redirect>

这与仅仅使用一系列 <Route> 有何不同?

<Switch> 只会渲染一个路由。相反,仅仅定义一系列 <Route> 时,每一个与路径匹配的 <Route> 都将包含在渲染范围内。考虑如下代码:

<Route path="/about" component={About} />
<Route path="/:user" component={User} />
<Route component={NoMatch} />

如果 URL 是 /about,那么 <About><User><NoMatch> 将全部渲染,因为它们都与路径匹配。这是通过设计,允许我们以很多方式将 <Route> 组合成我们的应用程序,例如侧边栏和面包屑、引导标签等。

但是,有时候我们只想选择一个 <Route> 来呈现。比如我们在 URL 为 /about 时不想匹配 /:user(或者显示我们的 404 页面),这该怎么实现呢?以下就是如何使用 <Switch> 做到这一点:

import { Switch, Route } from 'react-router';

<Switch>
  <Route exact path="/" component={Home} />
  <Route path="/about" component={About} />
  <Route path="/:user" component={User} />
  <Route component={NoMatch} />
</Switch>

现在,当我们在 /about 路径时,<Switch> 将开始寻找匹配的 <Route>。我们知道,<Route path="/about" /> 将会被正确匹配,这时 <Switch> 会停止查找匹配项并立即呈现 <About>。同样,如果我们在 /michael 路径时,那么 <User> 会呈现。

这对于动画转换也很有用,因为匹配的 <Route> 与前一个渲染位置相同。

<Fade>
  <Switch>
    {/* 这里只会渲染一个子元素 */}
    <Route />
    <Route />
  </Switch>
</Fade>

<Fade>
  <Route />
  <Route />
  {/* 这里总是会渲染两个子元素,也有可能是空渲染,这使得转换更加麻烦 */}
</Fade>

location: object

用于匹配子元素而不是当前历史位置(通常是当前的浏览器 URL)的 location 对象。

children: node

所有 <Switch> 的子元素都应该是 <Route><Redirect>。只有第一个匹配当前路径的子元素将被呈现。

<Route> 组件使用 path 属性进行匹配,而 <Redirect> 组件使用它们的 from 属性进行匹配。没有 path 属性的 <Route> 或者没有 from 属性的 <Redirect> 将始终与当前路径匹配。

当在 <Switch> 中包含 <Redirect> 时,你可以使用任何 <Route> 拥有的路径匹配属性:pathexactstrictfrom 只是 path 的别名。

如果给 <Switch> 提供一个 location 属性,它将覆盖匹配的子元素上的 location 属性。

<Switch>
  <Route exact path="/" component={Home} />
  <Route path="/users" component={Users} />
  <Redirect from="/accounts" to="/users" />
  <Route component={NoMatch} />
</Switch>
查看原文

赞 165 收藏 137 评论 6

jinmmd 关注了专栏 · 2019-01-30

西厂 XUX

来自 阿里巴巴信息平台用户体验团队 的小伙伴们,分享前端设计方面的有趣知识。

关注 114

jinmmd 赞了文章 · 2018-03-01

iOS 客户端对于运营商劫持的一点点对抗方式

网络劫持一般有两种情况,一种是DNS劫持,另一种是HTTP劫持

从表现上区分这两种劫持非常简单。

如果是DNS劫持,你输入的网址是google.com,然后出来的页面是百度。

如果是HTTP劫持,你打开了google.com,可是右下角弹出了百度推广的不孕不育广告。

URL域名解析成ip地址的过程被称作 DNS 解析。在这个过程中,由于 DNS 请求报文是明文状态,可能会在请求过程中被监测,然后攻击者伪装DNS服务器向主机发送带有假ip地址的响应报文,从而使得主机访问到假的服务器。这个就是DNS劫持的根本原理。

而另一种就是HTTP劫持。在运营商的路由器节点上,设置协议检测,一旦发现是HTTP请求,而且是html类型请求,则拦截处理。后续做法往往分为2种,1种是类似DNS劫持返回302让用户浏览器跳转到另外的地址,还有1种是在服务器返回的 HTML 数据中插入 js 或 dom 节点,从而使网页中出现自己的广告等等垃圾信息。

一般来说,针对各种网络劫持,大部分工作都是由前端来完成,针对这一方面的研究,也大多都是前端开发方向。但是其实客户端也可以通过一些方法来防劫持。

作为客户端开发,我们应该先了解我们的URL Loading System。

虽然 URL 加载系统包含的内容众多,但代码的设计上却非常良好,没有把复杂的操作暴露出来,开发者只需要在用到的时候进行设置。(苹果官方文档About the URL Loading System,是每个 iOS 开发者都应该认真研究的。)

DNS劫持

DNS劫持的问题,就可以基于 NSURLProtocol 实现 LocalDNS 防劫持方案。

关于LocalDNS防劫持方案,可以参考一篇大神文章DNS防劫持

简单来说,在网页发起请求的时候获取请求域名,然后在本地进行解析得到ip,返回一个直接访问网页ip地址的请求。

结构体struct hostent用来表示地址信息:

struct hostent {
    char *h_name;                     // official name of host
    char **h_aliases;                 // alias list
    int h_addrtype;                   // host address type——AF_INET || AF_INET6
    int h_length;                     // length of address
    char **h_addr_list;               // list of addresses
};

通过C函数gethostbyname,使用递归查询的方式将传入的域名转换成struct hostent结构体,在本地将URL解析成123.123.25.53这种ip地址。具体实现参考文章中的代码。

另外还可以用从服务器下发对应的DNS解析列表来代替递归查询这种比较低效的方式,文章中也有介绍。

而无论是那种方式,NSURLProtocol都是处理的核心部分。

NSURLProtocol

NSURLProtocol 或许是 URL 加载系统中最功能强大但同时也是最晦涩的部分了。它是一个抽象类,你可以通过子类化来定义新的或已经存在的 URL 加载行为。

用了它,你不必改动应用在网络调用上的其他部分,就可以改变URL加载行为的全部细节。 NSURLProtocol 就是一个苹果允许的中间人攻击。

下面这么多需求,都可以通过 NSURLProtocol,在不改动其他代码的情况下,比较简单地就能实现:

  • 拦截图片加载请求,转为从本地文件加载

  • 为了测试对 HTTP 返回内容进行mock和stub

  • 对发出请求的header进行格式化

  • 对发出的媒体请求进行签名

  • 创建本地代理服务,用于数据变化时对URL请求的更改

  • 故意制造畸形或非法返回数据来测试程序的鲁棒性

  • 过滤请求和返回中的敏感信息

  • 在既有协议基础上完成对 NSURLConnection 的实现且与原逻辑不产生矛盾

官方文档对 NSURLProtocol 的描述是这样的:

An NSURLProtocol object handles the loading of protocol-specific URL data. The NSURLProtocol class itself is an abstract class that provides the infrastructure for processing URLs with a specific URL scheme. You create subclasses for any custom protocols or URL schemes that your app supports.

在每一个 HTTP 请求开始时,URL 加载系统创建一个合适的 NSURLProtocol 对象处理对应的 URL 请求,而我们需要做的就是写一个继承自 NSURLProtocol 的类,并通过 - registerClass: 方法注册我们的协议类,然后 URL 加载系统就会在请求发出时使用我们创建的协议对象对该请求进行处理。

如何使用 NSURLProtocol 拦截 HTTP 请求?有这个么几个问题需要去解决:

  • 如何决定哪些请求需要当前协议对象处理?

  • 对当前的请求对象需要进行哪些处理?

  • NSURLProtocol 如何实例化?

  • 如何发出 HTTP 请求并且将响应传递给调用者?

这几个问题其实都可以通过 NSURLProtocol 为我们提供的 API 来解决,决定请求是否需要当前协议对象处理的方法是:+ canInitWithRequest,每一次请求都会有一个 NSURLRequest 实例,上述方法会拿到所有的请求对象,我们就可以根据对应的请求选择是否处理该对象。

那么究竟是否要处理对应的请求。由于网页存在动态链接的可能性,简单的返回YES可能会创建大量的NSURLProtocol对象,因此我们需要保证每个请求能且仅能被返回一次YES

请求经过 + canInitWithRequest: 方法过滤之后,我们得到了所有要处理的请求,接下来需要对请求进行一定的操作,而这都会在 + canonicalRequestForRequest: 中进行,虽然它与 + canInitWithRequest: 方法传入的 request 对象都是一个,但是最好不要在 + canInitWithRequest: 中操作对象,可能会有语义上的问题。

所以,我们需要覆写 + canonicalRequestForRequest: 方法提供一个标准的请求对象:

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
    return request;
}

这里就可以对request做一些修改,比如加个header什么的,只需要最后能返回一个NSURLRequest即可。

如果处理请求返回了YES,那么下面两个回调对应请求开始和结束阶段。在这里可以标记请求对象已经被处理过。

- (void)startLoading;
- (void)stopLoading;

在之前的 iOS 客户端基于 WebP 图片格式的流量优化 这篇文章中,就是利用- (void)startLoading;来替换图片请求的。当时替换 WebP 图片的核心功能,实际上就是在NSURLProtocol中完成的。

HTTP劫持

实际上,这种劫持对于大部分客户端来说,是无能为力的,需要前端来处理。不过,有些特殊情况下,客户端也可以有一些针对 HTTP劫持的办法。

比如像媒体新闻类客户端的文章中,防止js注入

在具体的实现方式之前,需要有一些准备工作,就是关于URL Loading System中的NSURLCache

NSURLCache

NSURLCache 为您的应用的 URL 请求提供了内存中以及磁盘上的综合缓存机制。 作为基础类库 URL Loading System 的一部分,任何通过 NSURLConnection 加载的请求都将被 NSURLCache 处理。

当一个请求完成得到来自服务器的Response,在本地保存作为cache。下一次同一个请求再发起时,本地保存的Response就会马上返回,不需要连接服务器。NSURLCache 会 自动 且 透明 地返回回应。

在NSURLConnection加载系统中,缓存被设计为request对象的一个属性,由NSURLRequest对象的cachePolicy属性指定。而在NSURLSession加载系统中,缓存被设计为 NSURLSessionConfiguration对像的一个属性,该属性所指定的策略被该session的所有request所共享。

作为一个Cache,它头文件中提供的方法并不复杂,就是基本的增删查改,(其中增和改可以算是一个功能,没有就增,改就是覆盖)。主要方法仅六个:

// 初始化方法
- (instancetype)initWithMemoryCapacity:(NSUInteger)memoryCapacity diskCapacity:(NSUInteger)diskCapacity diskPath:(nullable NSString *)path;
// 查询方法
- (nullable NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request;
// 存储方法
- (void)storeCachedResponse:(NSCachedURLResponse *)cachedResponse forRequest:(NSURLRequest *)request;
// 删除方法
- (void)removeCachedResponseForRequest:(NSURLRequest *)request;
- (void)removeAllCachedResponses;
- (void)removeCachedResponsesSinceDate:(NSDate *)date NS_AVAILABLE(10_10, 8_0);

非常简洁的类,功能高度封装,用起来很简单。但是它也开辟了一个新世界,就是你可以实现一个子类,来接管系统的URLCache功能。只需要一个简单的步骤:

- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 
{
  STURLCache *URLCache = [[STURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024
                                                       diskCapacity:20 * 1024 * 1024
                                                           diskPath:nil];
  [NSURLCache setSharedURLCache:URLCache];
}

这样,STURLCache就可以接管缓存的管理了。当系统调用URLCache的增删改查方法时,都可以由子类来接管。

NSURLCache的缓存策略,以及和HTTP header之间的关系,可以参考NSHipster NSURLCache文章,不再深入。

这样,配合NSURLProtocol,就可以对缓存做精准控制了。

js签名防注入

这个方法,适用于新闻类app,因为新闻类app的web页都是自己写的,所以,js和css都是可知的。

防注入的方式就是这样:

  1. 发版前,在 bundle 中存一份最新的前端js文件

  2. 后台在返回 js 文件 URL 的时候,对 js 文件内容进行 SHA-256 ,得到的 hash 值拼接到 js 的文件名中

  3. 请求 js 资源文件时,在NSURLCache中的- (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request方法中拦截请求

  4. 拦截请求之后,判断本地是否有缓存,如果有,则直接返回缓存文件包装成 response

  5. 下载 js 资源时,走NSURLProtocol 代理方法- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data,对 data 进行SHA-256签名比对,如果签名一致,将 data 通过;如果签名不一致,代表 js 被污染,直接丢弃,从bundle取出本地预存的 js 文件返回回来。

代码本身不会有什么难处,所以就只写出基本逻辑。

在自定义的URLCache实现文件中:

- (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request {
    NSString* ua = [request valueForHTTPHeaderField:@"User-Agent"];
    if (!EmptyString(ua) && [ua lf_containsSubString:@"AppleWebKit"]) {
      
        if ([CacheManager shouldVerifyHashCode:request]) { //包含64位hashcode的js css文件
              // 取本地JS缓存
            NSData *resultData = [CacheManager getJSCache];
            if (resultData) {
               NSURLResponse *response = [[NSURLResponse alloc] initWithURL:request.URL MIMEType:nil expectedContentLength:resultData.length textEncodingName:nil];
                return [[NSCachedURLResponse alloc] initWithResponse:response data:resultData];
            } else {
              return nil;
        }
    }
    return [super cachedResponseForRequest:request];
}

- (void)storeCachedResponse:(NSCachedURLResponse *)cachedResponse forRequest:(NSURLRequest *)request {
    NSString* ua = [request valueForHTTPHeaderField:@"User-Agent"];
    if ([CacheManager shouldVerifyHashCode:request] && [ua lf_containsSubString:@"AppleWebKit"]) {
      // 将请求回来的,并且通过验证的新js放到缓存中
        [[CacheManager defaultManager] storeCachedResponse:cachedResponse forRequest:request])
         return;
     }    
    [super storeCachedResponse:cachedResponse forRequest:request];
}

而在自定义的NSURLProtocol子类中

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    NSString *ua = [request valueForHTTPHeaderField:@"User-Agent"];
    if ([CacheManager shouldVerifyHashCode:request] && [ua lf_containsSubString:@"AppleWebKit"]) {
      // 拦截js请求
        return YES;
    }
    return NO;
}

// 收到请求返回data的代理方法
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
  if([data verifySHA256Success]) {
      [self.client URLProtocol:self didLoadData:data]; 
  } else {
    localData = [CacheManager bundleCacheFromUrl:url];
    [self.client URLProtocol:self didLoadData:localData]; 
  }
}

比较抽象的一点就是,这个方案是不是不能再更新 js 了?

当然不会,因为当后台的 js 文件有更新时,新 js 文件的签名就会发生变化,js 文件的URL也就自然变化,于是本地请求的时候,缓存是无法命中的,所以,也就会直接走下载 js 的那个路径。

这种方案的缺点就是:

  1. 在发生 js 劫持的时候,只能使用本地 js,可能会比最新版本 js 落后

  2. js 文件必须是由自己的服务端提供,并控制,才好对 js 进行签名,所以适用范围略窄

作为这个方案的扩充,可以考虑再次利用NSURLProtocol,当发现 js 被污染,重定向URL,此URL由服务端返回一个加密的 js 文件,对称加密,密钥插入在 js 的密文中,本地解密 js 文件,就可以保证得到最新的,安全的 js 文件了。

不过话说回来,既然都这么费劲的话,为啥不让前端来帮忙做呢,或者直接上HTTPS,才是真正的防劫持之道。

总结

关于运营商网络劫持,我本人毕竟不是前端开发,所以有很多问题可以理解也不是很准确,肯定也不是太专业。只是提供一个比较少看到的功能实现。其实防劫持最终的解决办法就是HTTPS,不过我们还是可以通过这方面的思索,来尝试一些更深入,更好玩的东西。而且可以更深入地去探索苹果框架下的URL Loading System。

References

NSHipster NSURLProtocol

NSHipster NSURLCache

DNS劫持

iOS 开发中使用 NSURLProtocol 拦截 HTTP 请求

About the URL Loading System


更多其他文章欢迎访问我的博客 http://suntao.me

查看原文

赞 2 收藏 3 评论 1

jinmmd 赞了文章 · 2017-10-20

GitLab-CI 从安装到差点放弃

故事是这样的..
我们源码从github迁移到自主搭建的gitlab服务器管理,以前用github的时候是使用jenkins进行持续集成的,本来应用上jenkins我只要配一下webhook就可以了,可我就是任性。
我心想,既然已经迁移到gitlab了,为何不用用gitlab-ci呢,更何况gitlab宣称集成了gitlab-ci,应该很快就能应用上。
我正是这样把自己推进坑的。

名词解释

进坑前先理清一些名词,以及他们之间的关系。

1. Gitlab

GitLab是一个利用Ruby on Rails开发的开源应用程序,实现一个自托管的Git项目仓库,可通过Web界面进行访问公开的或者私人项目。
它拥有与GitHub类似的功能,能够浏览源代码,管理缺陷和注释。可以管理团队对仓库的访问,它非常易于浏览提交过的版本并提供一个文件历史库。团队成员可以利用内置的简单聊天程序(Wall)进行交流。它还提供一个代码片段收集功能可以轻松实现代码复用,便于日后有需要的时候进行查找。

2. Gitlab-CI

Gitlab-CI是GitLab Continuous Integration(Gitlab持续集成)的简称。
从Gitlab的8.0版本开始,gitlab就全面集成了Gitlab-CI,并且对所有项目默认开启。
只要在项目仓库的根目录添加.gitlab-ci.yml文件,并且配置了Runner(运行器),那么每一次合并请求(MR)或者push都会触发CI pipeline

3. Gitlab-runner

Gitlab-runner.gitlab-ci.yml脚本的运行器,Gitlab-runner是基于Gitlab-CI的API进行构建的相互隔离的机器(或虚拟机)。GitLab Runner 不需要和Gitlab安装在同一台机器上,但是考虑到GitLab Runner的资源消耗问题和安全问题,也不建议这两者安装在同一台机器上。

Gitlab Runner分为两种,Shared runners和Specific runners。
Specific runners只能被指定的项目使用,Shared runners则可以运行所有开启 Allow shared runners选项的项目。

4. Pipelines

Pipelines是定义于.gitlab-ci.yml中的不同阶段的不同任务。
我把Pipelines理解为流水线,流水线包含有多个阶段(stages),每个阶段包含有一个或多个工序(jobs),比如先购料、组装、测试、包装再上线销售,每一次push或者MR都要经过流水线之后才可以合格出厂。而.gitlab-ci.yml正是定义了这条流水线有哪些阶段,每个阶段要做什么事。

5. Badges

徽章,当Pipelines执行完成,会生成徽章,你可以将这些徽章加入到你的README.md文件或者你的网站。

徽章的链接形如:
http://example.gitlab.com/namespace/project/badges/branch/build.svg
我们用gitlab项目的徽章作为例子:

安装配置

这里跳过Gitlab的安装,请自行谷歌。但是对于安装gitlab有一点提醒,就是建议使用官方推荐的集成安装包的方式安装,通过源码安装会有很多坑踩不完。

安装gitlab-ci-multi-runner

  1. 如果想要使用docker runner,则需要安装docker。(可选)
    curl -sSL https://get.docker.com/ | sh
    因为docker需要linux内核在3.10或以上,安装前可以通过uname -r查看Linux内核版本。
  2. 添加Gitlab的官方源:

    # For Debian/Ubuntu
    curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-ci-multi-runner/script.deb.sh | sudo bash
    
    # For CentOS
    curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-ci-multi-runner/script.rpm.sh | sudo bash
    
  3. 安装

    # For Debian/Ubuntu
    sudo apt-get install gitlab-ci-multi-runner
    
    # For CentOS
    sudo yum install gitlab-ci-multi-runner
  4. 注册Runner
    Runner需要注册到Gitlab才可以被项目所使用,一个gitlab-ci-multi-runner服务可以注册多个Runner。

    $ sudo gitlab-ci-multi-runner register
    
    Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com )
    https://mygitlab.com/ci
    Please enter the gitlab-ci token for this runner
    xxx-xxx-xxx
    Please enter the gitlab-ci description for this runner
    my-runner
    INFO[0034] fcf5c619 Registering runner... succeeded
    Please enter the executor: shell, docker, docker-ssh, ssh?
    docker
    Please enter the Docker image (eg. ruby:2.1):
    node:4.5.0
    INFO[0037] Runner registered successfully. Feel free to start it, but if it's
    running already the config should be automatically reloaded!
  5. 更新Runner
    如果需要更新Runner,只需要执行以下脚本:
# For Debian/Ubuntu
sudo apt-get update
sudo apt-get install gitlab-ci-multi-runner

# For CentOS
sudo yum update
sudo yum install gitlab-ci-multi-runner
  1. Runner高级配置
    通过gitlab-ci-multi-runner register注册的Runner配置会存储在/etc/gitlab-runner/config.toml中,如果需要修改可直接编辑该文件。详见这里
concurrent = 4
check_interval = 0

[[runners]]
name = "test"
url = "http://your-domain.com/ci"
token = "your-token"
executor = "docker"
[runners.docker]
  tls_verify = false
  image = "node:4.5.0"
  privileged = false
  disable_cache = false
  volumes = ["/cache"]
[runners.cache]
[runners.kubernetes]
  host = ""
  cert_file = ""
  key_file = ""
  ca_file = ""
  image = ""
  namespace = ""
  privileged = false
  cpus = ""
  memory = ""
  service_cpus = ""
  service_memory = ""

到这里我们的Runner就安装配置好了,接下来是对项目根目录中.gitlab-ci.yml进行配置。

配置构建任务

  1. 在项目根目录添加.gitlab-ci.yml文件
    关于该文件的各项配置请见
  2. 示例:
# 这里使用了我自己的docker image,配置了自己需要的环境
image: wuyanxin/node

variables:
MYSQL_DATABASE: wan_ark-unit
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"

# 关于service请见: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service
services:
- mysql:5.6
- redis:3.2.4

stages:
- test
- eslint
- deploy

before_script:
  - echo 'REDIS_HOST=redis' >> .env
  - echo 'DB_HOST=mysql' >> .env
  - yarn install

test_service:
stage: test
script:
  - npm run build
  - npm test

eslint_src: 
stage: eslint
script:
  - npm run lint
allow_failure: true

deploy:
stage: deploy
script:
  - echo 'deployd!'
only: 
  - master

这里使用了nodejs项目作为例子,其他语言类似语法。

  1. 执行结果
    pipeline 截图
    详细截图

最后

这是我最近对于Gitlab CI的实验记录,对于Gitlab CI的使用体验我给82分。虽然在实验过程中踩了很多坑,真的踩到差点放弃了,所以记录一下我的实验过程,希望对他人有帮助。
关于在这个过程中踩到的坑以及构建速度优化请关注我下期文章。

参考

  1. https://zh.wikipedia.org/wiki...
  2. https://docs.gitlab.com/ce/ci...
查看原文

赞 47 收藏 73 评论 28

jinmmd 赞了文章 · 2017-10-20

用 GitLab CI 进行持续集成

简介

从 GitLab 8.0 开始,GitLab CI 就已经集成在 GitLab 中,我们只要在项目中添加一个 .gitlab-ci.yml 文件,然后添加一个 Runner,即可进行持续集成。 而且随着 GitLab 的升级,GitLab CI 变得越来越强大,本文将介绍如何使用 GitLab CI 进行持续集成。

一些概念

在介绍 GitLab CI 之前,我们先看看一些持续集成相关的概念。

Pipeline

一次 Pipeline 其实相当于一次构建任务,里面可以包含多个流程,如安装依赖、运行测试、编译、部署测试服务器、部署生产服务器等流程。

任何提交或者 Merge Request 的合并都可以触发 Pipeline,如下图所示:

+------------------+           +----------------+
|                  |  trigger  |                |
|   Commit / MR    +---------->+    Pipeline    |
|                  |           |                |
+------------------+           +----------------+

Stages

Stages 表示构建阶段,说白了就是上面提到的流程。我们可以在一次 Pipeline 中定义多个 Stages,这些 Stages 会有以下特点:

  • 所有 Stages 会按照顺序运行,即当一个 Stage 完成后,下一个 Stage 才会开始

  • 只有当所有 Stages 完成后,该构建任务 (Pipeline) 才会成功

  • 如果任何一个 Stage 失败,那么后面的 Stages 不会执行,该构建任务 (Pipeline) 失败

因此,Stages 和 Pipeline 的关系就是:

+--------------------------------------------------------+
|                                                        |
|  Pipeline                                              |
|                                                        |
|  +-----------+     +------------+      +------------+  |
|  |  Stage 1  |---->|   Stage 2  |----->|   Stage 3  |  |
|  +-----------+     +------------+      +------------+  |
|                                                        |
+--------------------------------------------------------+

Jobs

Jobs 表示构建工作,表示某个 Stage 里面执行的工作。我们可以在 Stages 里面定义多个 Jobs,这些 Jobs 会有以下特点:

  • 相同 Stage 中的 Jobs 会并行执行

  • 相同 Stage 中的 Jobs 都执行成功时,该 Stage 才会成功

  • 如果任何一个 Job 失败,那么该 Stage 失败,即该构建任务 (Pipeline) 失败

所以,Jobs 和 Stage 的关系图就是:

+------------------------------------------+
|                                          |
|  Stage 1                                 |
|                                          |
|  +---------+  +---------+  +---------+   |
|  |  Job 1  |  |  Job 2  |  |  Job 3  |   |
|  +---------+  +---------+  +---------+   |
|                                          |
+------------------------------------------+

GitLab Runner

简介

理解了上面的基本概念之后,有没有觉得少了些什么东西 —— 由谁来执行这些构建任务呢?
答案就是 GitLab Runner 了!

想问为什么不是 GitLab CI 来运行那些构建任务?

一般来说,构建任务都会占用很多的系统资源 (譬如编译代码),而 GitLab CI 又是 GitLab 的一部分,如果由 GitLab CI 来运行构建任务的话,在执行构建任务的时候,GitLab 的性能会大幅下降。

GitLab CI 最大的作用是管理各个项目的构建状态,因此,运行构建任务这种浪费资源的事情就交给 GitLab Runner 来做拉!

因为 GitLab Runner 可以安装到不同的机器上,所以在构建任务运行期间并不会影响到 GitLab 的性能~

安装

安装 GitLab Runner 太简单了,按照着 官方文档 的教程来就好拉!

下面是 Debian/Ubuntu/CentOS 的安装方法,其他系统去参考官方文档:

# For Debian/Ubuntu
$ curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-ci-multi-runner/script.deb.sh | sudo bash
$ sudo apt-get install gitlab-ci-multi-runner

# For CentOS
$ curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-ci-multi-runner/script.rpm.sh | sudo bash
$ sudo yum install gitlab-ci-multi-runner

注册 Runner

安装好 GitLab Runner 之后,我们只要启动 Runner 然后和 CI 绑定就可以了:

  • 打开你 GitLab 中的项目页面,在项目设置中找到 runners

  • 运行 sudo gitlab-ci-multi-runner register

  • 输入 CI URL

  • 输入 Token

  • 输入 Runner 的名字

  • 选择 Runner 的类型,简单起见还是选 Shell 吧

  • 完成

当注册好 Runner 之后,可以用 sudo gitlab-ci-multi-runner list 命令来查看各个 Runner 的状态:

$ sudo gitlab-runner list
Listing configured runners          ConfigFile=/etc/gitlab-runner/config.toml
my-runner                           Executor=shell Token=cd1cd7cf243afb47094677855aacd3 URL=http://mygitlab.com/ci

.gitlab-ci.yml

简介

配置好 Runner 之后,我们要做的事情就是在项目根目录中添加 .gitlab-ci.yml 文件了。当我们添加了 .gitlab-ci.yml 文件后,每次提交代码或者合并 MR 都会自动运行构建任务了。

还记得 Pipeline 是怎么触发的吗?Pipeline 也是通过提交代码或者合并 MR 来触发的!那么 Pipeline 和 .gitlab-ci.yml 有什么关系呢?

其实 .gitlab-ci.yml 就是在定义 Pipeline 而已拉!

基本写法

我们先来看看 .gitlab-ci.yml 是怎么写的:

# 定义 stages
stages:
  - build
  - test

# 定义 job
job1:
  stage: test
  script:
    - echo "I am job1"
    - echo "I am in test stage"

# 定义 job
job2:
  stage: build
  script:
    - echo "I am job2"
    - echo "I am in build stage"

写起来很简单吧!用 stages 关键字来定义 Pipeline 中的各个构建阶段,然后用一些非关键字来定义 jobs。

每个 job 中可以可以再用 stage 关键字来指定该 job 对应哪个 stage。job 里面的 script 关键字是最关键的地方了,也是每个 job 中必须要包含的,它表示每个 job 要执行的命令。

回想一下我们之前提到的 Stages 和 Jobs 的关系,然后猜猜上面例子的运行结果?

I am job2
I am in build stage
I am job1
I am in test stage

根据我们在 stages 中的定义,build 阶段要在 test 阶段之前运行,所以 stage:build 的 jobs 会先运行,之后才会运行 stage:test 的 jobs。

常用的关键字

下面介绍一些常用的关键字,想要更加详尽的内容请前往 官方文档

stages

定义 Stages,默认有三个 Stages,分别是 build, test, deploy

types

stages 的别名。

before_script

定义任何 Jobs 运行前都会执行的命令。

after_script

要求 GitLab 8.7+ 和 GitLab Runner 1.2+

定义任何 Jobs 运行完后都会执行的命令。

variables && Job.variables

要求 GitLab Runner 0.5.0+

定义环境变量。如果定义了 Job 级别的环境变量的话,该 Job 会优先使用 Job 级别的环境变量。

cache && Job.cache

要求 GitLab Runner 0.7.0+

定义需要缓存的文件。每个 Job 开始的时候,Runner 都会删掉 .gitignore 里面的文件。如果有些文件 (如 node_modules/) 需要多个 Jobs 共用的话,我们只能让每个 Job 都先执行一遍 npm install

这样很不方便,因此我们需要对这些文件进行缓存。缓存了的文件除了可以跨 Jobs 使用外,还可以跨 Pipeline 使用。

具体用法请查看 官方文档

Job.script

定义 Job 要运行的命令,必填项。

Job.stage

定义 Job 的 stage,默认为 test

Job.artifacts

定义 Job 中生成的附件。当该 Job 运行成功后,生成的文件可以作为附件 (如生成的二进制文件) 保留下来,打包发送到 GitLab,之后我们可以在 GitLab 的项目页面下下载该附件。

注意,不要把 artifactscache 混淆了。

实用例子

下面给出一个我自己在用的例子:

stages:
  - install_deps
  - test
  - build
  - deploy_test
  - deploy_production

cache:
  key: ${CI_BUILD_REF_NAME}
  paths:
    - node_modules/
    - dist/


# 安装依赖
install_deps:
  stage: install_deps
  only:
    - develop
    - master
  script:
    - npm install


# 运行测试用例
test:
  stage: test
  only:
    - develop
    - master
  script:
    - npm run test


# 编译
build:
  stage: build
  only:
    - develop
    - master
  script:
    - npm run clean
    - npm run build:client
    - npm run build:server


# 部署测试服务器
deploy_test:
  stage: deploy_test
  only:
    - develop
  script:
    - pm2 delete app || true
    - pm2 start app.js --name app


# 部署生产服务器
deploy_production:
  stage: deploy_production
  only:
    - master
  script:
    - bash scripts/deploy/deploy.sh

上面的配置把一次 Pipeline 分成五个阶段:

  • 安装依赖(install_deps)

  • 运行测试(test)

  • 编译(build)

  • 部署测试服务器(deploy_test)

  • 部署生产服务器(deploy_production)

设置 Job.only 后,只有当 develop 分支和 master 分支有提交的时候才会触发相关的 Jobs。
注意,我这里用 GitLab Runner 所在的服务器作为测试服务器。

出处

http://scarletsky.github.io/2...

参考资料

https://about.gitlab.com/gitl...
http://docs.gitlab.com/ce/ci/...
http://docs.gitlab.com/ce/ci/...
https://gitlab.com/gitlab-org...
https://gitlab.com/gitlab-org...
http://stackbox.cn/2016-02-gi...

查看原文

赞 40 收藏 116 评论 4

jinmmd 赞了文章 · 2017-09-14

在 React 项目中使用 React-intl 实现多语言支持

封面图片来源:https://alpha.wallhaven.cc

最近在项目中添加了语言国际化的功能。

语言国际化,也有人说成是语言本地化,其实就是为Web App添加多语言,我们的项目当前包含了中文版和英文版,按理来说『逐字替换』也不是多大事儿,但是,这么Low的做法,有钱途吗?

一开始的时候,我考虑的是传统的为整个项目添加config文件,根据不同的语言和地区,加载不同的config文件,就能够达到界面语言切换的目的。当然,也正是因为这个想法太过于幼稚,所以才被称为『一开始』的想法。语言的国际化不仅仅是将界面上的UI文字翻译成另一种语言,还包括了日期&时间显示,数字显示(英文环境下每隔3位一个逗号:1,000),量词的显示(一个苹果是apple,两个苹果就应该是apples),甚至还有一个字符串中间插了一个变量的情况("今天午饭吃了{count}个鸡腿")...

图片描述

所以这并不只是一个简单的字符替换问题,并且,我们要很方便的导出一个目录,放到word或者page当中,给到其他同事对照着进行翻译工作,这个非常重要!!难道你要让产品经理直接在代码里改么?或者你想一个一个搜索替换?不考虑清楚就干的话,相信我,You'll pay for this。

作为一个有追求的代码家,你肯定不希望在index.html当中增加一行<Script>引用吧?另外,UI中的文字全部都是使用图片的那个同学,请起立,滚。如果想要在一个React项目中,优雅的import something from somewhere,然后将界面中的文字用<首字母大写 /> 组件替代,最后通过简单的配置实现语言的国际化,那我们就用React-intl吧。

注意:本文说的是用法,源码我也没有拜读过,太深的东西去github给作者留言吧。

图片描述

React-intl

项目地址:https://github.com/yahoo/reac...

React-intl是雅虎的语言国际化开源项目FormatJS的一部分,通过其提供的组件和API可以与ReactJS绑定。上面这句话援引了官方文档的说辞,主要表达的是,这是一个很屌的开源项目,有大团队支持,使用量也很大,不会太坑爹,你们放心用。虽然雅虎都快被收购了。

React-intl提供了两种使用方法,一种是引用React组建,另一种是直接调取API,官方更加推荐在React项目中使用前者,只有在无法使用React组件的地方,才应该调用框架提供的API,事实上,我在项目的过程中真的遇到了无法使用组件的情况,这个我会另外写一篇文章来描述。

React-intl提供的React组件有如下几种:

<IntlProvider /> 包裹在需要语言国际化的组建的最外层,为包含在其中的所有组建提供包含id和字符串的键值对。(如:"homepage.title":"Hommily";

日期时间

a. <FormattedDate /> 用于格式化日期,能够将一个时间戳格式化成不同语言中的日期格式。

传入时间戳作为参数:

<FormattedDate 
    value={new Date(1459832991883)}
/>

输出结果:

<span>4/5/2016</span>

b. <FormattedTime> 用于格式化时间,效果与<FormattedDate />相似。

传入时间戳作为参数:

<FormattedTime 
   value={new Date(1459832991883)}
/>

输出结果:

<span>1:09 AM</span>

c. <FormattedRelative /> 通过这个组件可以显示传入组件的某个时间戳和当前时间的关系,比如 “ 10 minutes ago"。

传入时间戳作为参数:

<FormattedRelative 
    value={Date.now()}
/>

输出结果:

<span>now</span>

10秒之后的输出结果:

<span>10 seconds ago</span>

1分钟之后的输出结果:

<span>1 minute ago</span>

数字量词

a. <FormattedNumber /> 这个组件最主要的用途是用来给一串数字标逗号,比如10000这个数字,在中文的语言环境中应该是1,0000,是每隔4位加一个逗号,而在英语的环境中是10,000,每隔3位加一个逗号。

传入数字作为参数:

<FormattedNumber 
    value={1000}
/>

输出结果:

<span>1,000</span>

b. <FormattedPlural /> 这个组件可用于格式化量词,在中文的语境中,其实不太会用得到,比如我们说一个鸡腿,那么量词就是‘个’,我们说两个鸡腿,量词还是‘个’,不会发生变化。但是在英文的语言环境中,描述一个苹果的时候,量词是apple,当苹果数量为两个时,就会变成apples,这个组件的作用就在于此。

传入组件的参数中,value为数量,其他的为不同数量时对应的量词,在下面的例子中,一个的时候量词为message,两个的时候量词为messages。实际上可以传入组件的量词包括 zero, one, two, few, many, other 已经涵盖了所有的情况。

<FormattedPlural
    value={10}
    one='message'
    other='messages'/>

传入组件的量词参数可以是一个字符串,也可以是一个组件,我们可以选择传入<FormattedMessage />组件,就可以实现量词的不同语言的切换。

输出结果:

<span>messages</span>

字符串的格式化

a. <FormattedMessage /> 这个组件用于格式化字符串,是所有的组件中使用频率最高的组件,因为基本上,UI上面的每一个字符串都应该用这个组件替代。这个组件的功能丰富,除了可以根据配置输出不同语言的简单字符串之外,还可以输出包含动态变化的参数的复杂字符串,具体的用法在后面的例子中会慢慢叙述。

比如我们在locale配置文件中写了如下内容:

const app = {
    greeting:'Hello Howard!",
}

export default app;

使用这个组件的时候,我们这么写:

<FormattedMessage
    id='app.greeting'
    description='say hello to Howard'
    defaultMessage='Hello, Howard!'
    />

id指代的是这个字符串在locale配置文件中的属性名,description指的是对于这个位置替代的字符串的描述,便于维护代码,不写的话也不会影响输出的结果,当在locale配置文件中没有找到这个id的时候,输出的结果就是defaultMessage的值。

输出的结果:

<span>Hello, Howard!</span>

b. <FormattedHTMLMessage /> 这个组件的用法和<FormattedMessage />完全相同,唯一的不同就是输出的字符串可以包含HTML标签,但是官方不太推荐使用这个方法,如果可以想办法用<FormattedMessage />的话,就不应该使用这个组件,我揣测应该是性能方面不如<FormattedMessage />,这个组件的用法我就不举例了。

Well,到此为止,已经把React-intl提供的所有组件介绍完了,下面就给大家介绍一下具体如何去使用吧。

React-intl 使用步骤

(本文例子运行在OSX环境,Window操作方法的终端在安装的时候要注意用管理员身份运行)

1. 安装React-intl

假设你已经在你的系统中安装了node.js和npm,如果你还不知道这两个是什么东西,请自行百度,对,在百度都能找到答案。

打开终端,进入项目根目录,输入以下指令安装React-intl:

npm install react-intl -save

注意:为了兼容Safari各个版本,需要同时安装 intl,intl在大部分的『现代』浏览器中是默认自带的,但是Safari和IE11以下的版本就没有了,这里需要留个心眼。

安装intl需要在终端中输入以下指令:

npm install intl --save

这里还有一个注意:由于React-intl的每一个组件的使用方法大同小异,和ReactJS的语法完全一致,所以我就仅仅描述如何使用<FormattedMessage />这个组件的用法,借此抛砖引玉,相信看完之后已经足够帮助你迅速的去使用这个开源框架了。

2. 引用React-intl

import { FormattedMessage } from 'react-intl'; 

由于我使用的是ES6 的语法,所以是支持直接引用组件的。你当然可以使用ES5的方式引用,但是,这样有前途么?

require ReactIntl from 'react-intl';

3. 创建locale配置文件

这里,我们将文件命名为zh_CN.js和en_US.js,代表中文和美式英语的配置包。

在zh_CN.js编写如下代码:

const zh_CN = {
            hello:"你好,方浩!",
            superHello:"你好,{ someone } !"
        }
export default zh_CN;    

在en_US.js编写如下代码:

const en_US = {
            hello:"Hello, Howard!",
            superHello:"Hello, { someone } !"
        }    
export default en_US;

于是,我们就创建好了locale文件,但是,在实际的项目中配置文件不会这么简单,您可能需要根据业务需求按照不同的页面或者不同的功能块创建不同的文件树,然后用模块化的方法将不同的配置文件进行组织,已达成你的目标,这里我也就没能力逼逼太多了。

你可能想问,{ someone } 是什么鬼?其实悟性高一些的话就应该已经猜到,这个应该就是前面提到过的在字符串中插入动态参数的用法,事实上也是这样的。

4. 使用<IntlProvider />

使用<IntlProvider /> 组件包裹住需要您需要进行语言国际化的组件,用法和React-redux的<Provider />差不多,当<IntlProvider /> 包裹住某个组件的时候,这个组件本身和组件内部包含的子组件就可以获得所有React-intl提供的接口以及在<IntlProvider /> 中引入的locale配置文件的内容。

import React from 'react';
import { render } from 'react-dom';
//引入locale配置文件,具体路径根据实际情况填写
import zh_CN from './zh_CN';
import en-US from './en-US';
//如果浏览器没有自带intl,则需要在使用npm安装intl之后添加如下代码
import intl from 'intl';
addLocaleDate([...en,...zh]);

...
...

render(    
        <IntlProvider 
            locale={'en'} 
            messages={en_US}
        >
            <App />
        </IntlProvider>,    
        document.getElementById('container')
);

<IntlProvider />需要传递两个参数:

locale是传递需要国际化的语言的缩写,通过这个参数可以确定格式化日期,数字,量词的时候按照哪一种语言的规则,这个是规则是intl提供的,一般浏览器会内置这个库,但是在Safari和IE11之前需要自己安装,安装的方法前面已经提及,请自己翻阅。

messages是用于传递刚刚我们在第3步中定义的配置文件的,从示例代码中我们可以看出,首先我们使用Import语句引入了配置文件,然后将配置文件的内容传递给了messages这个参数,此时<App />组件中的所有组件都可以拿到配置文件中的内容了。

那个跳起来的同学,请先坐下,我猜你是想问,是不是每次都要手动修改这两个参数以适配不同语言呢?

其实不然,我们完全可以按照下面的做法自动识别当前浏览器的语言:

chooseLocale(){
    switch(navigator.language.split('_')[0]){
        case 'en':
            return 'en_US';
            break;
        case 'zh':
            return 'zh_CN';
            break;
        ...
        ...
        ...
        default:
            return 'en_US';
            break;
    }
}
render(    
        <IntlProvider 
            locale={navigator.language} 
            messages={chooseLocale()}
            >
            <App />
        </IntlProvider>,    
        document.getElementById('container')
);

您还需要知道的是,<IntlProvider />是可以嵌套使用的,也就是说,在一个<IntlProvider />内部还可以有N个<IntlProvider />,这个功能的实际意义就是可以在英文网站中嵌套一个中文的或者德语的或者法语的板块,应用起来会更加灵活一些。

5. 使用<FormattedMessage />

前面的几个步骤其实都是为了这个步骤做铺垫的,在添加了<IntlProvoder />之后,我们就可以在其包裹的<App />及<App />包含的所有组件中获取到配置文件的信息,传入<FormattedMessage />组件的id参数也能其在配置文件中对应的字符串了。

使用的方法如下:

<FormattedMessage
    id='hello'
    description='say hello to Howard.'
    defaultMessage='Hello, Howard'
    />

在Js执行的时候,组件就会找到配置文件中,‘hello'键名对应的字符串'Hello, Howard!'.

输出的结果为:

<span>Hello, Howard!</span>

那么如何输出含有动态参数的字符串呢?比如Hello,Johnson!,如果我要问候的对象是一个变量呢?

那就这么写呗。。

<FormattedMessage
    id='superHello'
    description='say hello to Howard.'
    defaultMessage='Hello, {someone}'
    values={
        someone:this.props.name,
    }
    />

以上的例子中,赋给someone的就是一个变量(假设这个变量是通过参数传进这个组件的),注意,如果是这样的话,那么locale配置文件中就要这么写。

 superHello:"你好,{ someone } !"

前面其实提过了,怕你忘了...我已经悄无声息的把id换成了superHello。

更牛逼的是,这个someone还可以包含HTML标签!

<FormattedMessage
    id='superHello'
    description='say hello to Howard.'
    defaultMessage='Hello, {someone}'
    values={
        someone:<b>this.props.name</b>,
    }
    />

输出结果:

<span>Hello, <b>Howard</b>!</span>

于是,这个名字就被加粗了。

眼尖的同学又要跳起来了,“webFunc,为什么所有的输出都带一个<span>标签,我就不能换成别的么?”

不要着急,我正要说这个,对于这个问题,官方的文档是这么说的。

By default <formattedMessage> will render the formatted string into
a <span>. If you need to customize rendering, you can either wrap it
with another React element (recommended), specify a different tagName
(e.g., 'div'), or pass a function as the child.

翻译过来就是,默认的是会包裹在<span>标签中的,如果想要让输出的字符串包裹在其他标签中的话,比如你想包裹在<div>中,你就把<FormattedMessage />组件包含在一对<div>中间,这是一种官方更加推荐的做法。

<div>
<FormattedMessage
    id='hello'
    description='say hello to Howard.'
    defaultMessage='Hello, Howard!"
    />
</div>

Well, that's stupid...

或者你可以给<FormattedMessage>传入一个tagName的参数。比如:

<FormattedMessage
    id='hello'
    tagName = 'div'
    description='say hello to Howard.'
    defaultMessage='Hello, Howard!'
    />

就会输出:

<div>Hello, Howard!</div>

比较奇葩的是,也是我揣测作者不推荐使用这种方法的原因是...只要你高兴,tagName可以传入任意字符串,比如 shit:

<FormattedMessage
    id='hello'
    tagName = 'shit'
    description='say hello to Howard.'
    defaultMessage='Hello, Howard!'
    />

就会输出:

<shit>Hello, Howard!</shit>

Yes, shit happens.

看到这里,你应该已经会使用React-intl对你的项目进行语言国际化了,没有进一步描述的地方,请自行查阅官方文档(项目地址:https://github.com/yahoo/reac...,或者给我留言,虽然我不一定会及时回复。

--写在后面:

语言国际化应该是一个比较经常遇到的需求,但是我在完成项目的过程中,看到的中文的资料却相当少,虽然这不是一篇非常牛叉的技术文章,但是可能会帮到很多人,如若如此,也便满足了。
——方浩(webFunc)

对了,你可以关注一下我的微信公众号:webcoding

图片描述

查看原文

赞 45 收藏 88 评论 35

jinmmd 赞了文章 · 2017-09-11

我遇到的前端面试题2017

图片描述

本文首发于我的博客:http://blog.dunizb.com
原文链接:http://blog.dunizb.com/2017/09/08/interview-questions-2017/

转载声明
最近发现有人和网站盗用我的文章,有的转载却自己标为原创,没有明确显示原文作者、原文出处及原文链接。我的网络ID是:Dunizb。请自觉遵守网络文章转载规范以及开源协议。

想知道自己什么水平就出去面试,抛砖引玉,详细答案还需要自己去补充....

更新记录

  • 2019-07-28更新:修改第13题RESTful API的答案
  • 2018-06-12更新:修改第1题答案
  • 2017-10-19更新:修改22题深浅拷贝的答案
  • 2017-10-18更新:修正部分题目答案,答案并非十分准确,仅供参考,此文部分题目答案故意省掉了一些高精尖、新奇特的东西,比如创建对象我写了三种,《JS高程》上可不止三种,一切以常用记得住的为宗旨,所以,对于部分答案有疑问的同学,可以留言讨论或自行斟酌
  • 2017-10-12更新:有部分题目属于后端范畴,或者是大前端范畴,因为我以前做Java后端的(关于我),故偶尔会遇到后端相关的一些问题,但是没有遇到问纯Java技术问题。如果你对某些后端题目不理解就直接跳过吧。

金九银十,在九月之前把工作落实了,经历了好几个公司的面试,得到一些信息,和大家分享:

  1. 大部分公司(创业公司)都趋向于招一个牛逼的前端而不是三四个平庸的前端
  2. 性能优化、ES6必问
  3. 招聘要求上清一色的要求有一门后端语言的经验
  4. 招聘要求写的和面试相关性并不是很高

以下是我整理我面试遇到的一些我觉得具有代表性的题目,刚好30题,吐血献上!

0.谈谈对前端安全的理解,有什么,怎么防范

前端安全问题主要有XSS、CSRF攻击
XSS:跨站脚本攻击
它允许用户将恶意代码植入到提供给其他用户使用的页面中,可以简单的理解为一种javascript代码注入。
XSS的防御措施:

  1. 过滤转义输入输出
  2. 避免使用evalnew Function等执行字符串的方法,除非确定字符串和用户输入无关
  3. 使用cookiehttpOnly属性,加上了这个属性的cookie字段,js是无法进行读写的
  4. 使用innerHTMLdocument.write的时候,如果数据是用户输入的,那么需要对象关键字符进行过滤与转义

CSRF:跨站请求伪造
其实就是网站中的一些提交行为,被黑客利用,在你访问黑客的网站的时候进行操作,会被操作到其他网站上
CSRF防御措施:

  1. 检测http referer是否是同域名
  2. 避免登录的session长时间存储在客户端中
  3. 关键请求使用验证码或者token机制

其他的一些攻击方法还有HTTP劫持、界面操作劫持

1.使用箭头函数需要注意的地方

当要求动态上下文的时候,你就不能使用箭头函数,比如:定义方法,用构造器创建对象,处理时间时用 this 获取目标。

箭头函数与传统函数的区别,主要集中在以下方面:

  • 没有this、super、arguments 和 new.target 绑定,这些值由最近一层非箭头函数决定。
  • 不能通过 new 关键字调用,所以不能用作构造函数,否则程序会抛出错误(SyntaxError)。
  • 没有原型。由于不可以通过new 关键字调用箭头函数,因而没有构建原型的需求,所以箭头函数不存在 prototype 这个属性。
  • 不可以改变 this 的绑定,函数内部的 this 值不可以被改变,在函数的生命周期内始终保持一致。
  • 不支持 arguments 对象,所以你必须通过命名参数和不定参数这两种形式访问函数的参数。
  • 不支持重复的命名参数,无论在严格还是非严格模式下都不支持,而在传统的函数规定中只有在严格模式下才不能有重复的命名参数。

2.webpack.load的原理

loaders是你用在app源码上的转换元件。他们是用node.js运行的,把源文件作为参数,返回新的资源的函数。

3.ES6 let、const

let
let是更完美的var

  1. let声明的变量拥有块级作用域,let声明仍然保留了提升的特性,但不会盲目提升。
  2. let声明的全局变量不是全局对象的属性。不可以通过window.变量名的方式访问
  3. 形如for (let x…)的循环在每次迭代时都为x创建新的绑定
  4. let声明的变量直到控制流到达该变量被定义的代码行时才会被装载,所以在到达之前使用该变量会触发错误。

const
定义常量值,不可以重新赋值,但是如果值是一个对象,可以改变对象里的属性值

const OBJ = {"a":1, "b":2};
OBJ.a = 3;
OBJ = {};// 重新赋值,报错!
console.log(OBJ.a); // 3

4.CSS3 box-sizing的作用

设置CSS盒模型为标准模型或IE模型。标准模型的宽度只包括content,二IE模型包括border和padding

box-sizing属性可以为三个值之一:

  1. content-box,默认值,border和padding不计算入width之内
  2. padding-box,padding计算入width内
  3. border-box,border和padding计算入width之内

5.说说HTML5中有趣的标签(新标签及语义化)

如果代码写的语义化,有利于SEO。搜索引擎就会很容易的读懂该网页要表达的意思。例如文本模块要有大标题,合理利用h1-h6,列表形式的代码使用ul或ol,重要的文字使用strong等等。总之就是要充分利用各种HTML标签完成他们本职的工作

6.git命令,如何批量删除分支

git branch |grep 'branchName' |xargs git branch -D,从分支列表中匹配到指定分支,然后一个一个(分成小块)传递给删除分支的命令,最后进行删除。(参考这里)

7.创建对象的三种方法

第一种方式,字面量

var o1 = {name: "o1"}

第二种方式,通过构造函数

var o2 = new Object({name: "o2"})
var M = function(name){ this.name = name }
var o3 = new M("o3")

第三种方式,Object.create

var  p = {name: "p"}
var o4 = Object.create(p)

新创建的对o4的原型就是p,同时o4也拥有了属性name

8.JS实现继承的几种方式

借用构造函数实现继承

function Parent1(){
    this.name = "parent1"
}
function Child1(){
    Parent1.call(this);
    this.type = "child1";
}

缺点:Child1无法继承Parent1的原型对象,并没有真正的实现继承(部分继承)

借用原型链实现继承

function Parent2(){
    this.name = "parent2";
    this.play = [1,2,3];
}
function Child2(){
    this.type = "child2";
}
Child2.prototype = new Parent2();

缺点:原型对象的属性是共享的

组合式继承

function Parent3(){
    this.name = "parent3";
    this.play = [1,2,3];
}
function Child3(){
    Parent3.call(this);
    this.type = "child3";
}
Child3.prototype = Object.create(Parent3.prototype);
Child3.prototype.constructor = Child3;

9.当new Foo()时发生了什么

1.创建了一个新对象
2.将新创建的空对象的隐式原型指向其构造函数的显示原型。
3.将this指向这个新对象
4.如果无返回值或者返回一个非对象值,则将新对象返回;如果返回值是一个新对象的话那么直接直接返回该对象。
参考《JS高程》6.2.2

10.你做过哪些性能优化

雪碧图,移动端响应式图片,静态资源CDN,减少Dom操作(事件代理、fragment),压缩JS和CSS、HTML等,DNS预解析

11.浏览器渲染原理

首先来看一张图:

  1. HTML被解析成DOM Tree,CSS被解析成CSS Rule Tree
  2. 把DOM Tree和CSS Rule Tree经过整合生成Render Tree(布局阶段)
  3. 元素按照算出来的规则,把元素放到它该出现的位置,通过显卡画到屏幕上
更多详情看这里

12.前端路由的原理

什么是路由?简单的说,路由是根据不同的 url 地址展示不同的内容或页面

使用场景?前端路由更多用在单页应用上, 也就是SPA, 因为单页应用, 基本上都是前后端分离的, 后端自然也就不会给前端提供路由。

前端的路由和后端的路由在实现技术上不一样,但是原理都是一样的。在 HTML5 的 history API 出现之前,前端的路由都是通过 hash 来实现的,hash 能兼容低版本的浏览器。

两种实现前端路由的方式
HTML5 History两个新增的API:history.pushStatehistory.replaceState,两个 API 都会操作浏览器的历史记录,而不会引起页面的刷新。

Hash就是url 中看到 # ,我们需要一个根据监听哈希变化触发的事件( hashchange) 事件。我们用 window.location 处理哈希的改变时不会重新渲染页面,而是当作新页面加到历史记录中,这样我们跳转页面就可以在 hashchange 事件中注册 ajax 从而改变页面内容。

优点
从性能和用户体验的层面来比较的话,后端路由每次访问一个新页面的时候都要向服务器发送请求,然后服务器再响应请求,这个过程肯定会有延迟。而前端路由在访问一个新页面的时候仅仅是变换了一下路径而已,没有了网络延迟,对于用户体验来说会有相当大的提升。

更多内容请看这里

缺点
使用浏览器的前进,后退键的时候会重新发送请求,没有合理地利用缓存。

13.Restful API是什么,如何设计RESTful API?

RESTful API是指符合REST设计风格的Web API,为了使的接口安全、易用、可维护以及可扩张,一般设计RESTful API需要考虑以下几个方面:

  1. 通信用HTTPS安全协议
  2. 在URL中加入版本号
  3. URL中的路径不能有动词,只能用名词
  4. 用HTTP方法对资源进行增删改查的操作
  5. 用HTTP状态吗传达执行结果和失败原因
  6. 为集合提供过滤、排序、分页功能
  7. 用查询字符串或HTTP首部Accpet进行内容协商,指定返回结果的数据格式
  8. 及时更新文档,每个接口都有对应的说明

14.script标签的defer、async的区别

defer是在HTML解析完之后才会执行,如果是多个,按照加载的顺序依次执行
async是在加载完成后立即执行,如果是多个,执行顺序和加载顺序无关

15.同源与跨域

什么是同源策略?
限制从一个源加载的文档或脚本如何与来自另一个源的资源进行交互。
一个源指的是主机名、协议和端口号的组合,必须相同

跨域通信的几种方式

  • JSONP
  • Hash
  • postMessage
  • WebSocket
  • CORS

JSONP原理
基本原理:利用script标签的异步加载特性实现
给服务端传一个回调函数,服务器返回一个传递过去的回调函数名称的JS代码

更多请查看:《前后端通信类知识》

16.作用域与闭包、原型相关问题

16.1 作用域

域表示的就是范围,即作用域,就是一个名字在什么地方可以使用,什么时候不能使用。
简单的说,作用域是针对变量的,比如我们创建一个函数 a1,函数里面又包了一个子函数 a2

// 全局作用域
functiona a1() {
    // a1作用域
    function a2() {
        // a2作用域
    }
}

此时就存 在三个作用域:全局作用域,a1 作用域,a2 作用域;即全局作用域包含了 a1 的作用域,a2 的作用域包含了 a1 的作用域。

a2 在查找变量的时候会先从自身的作用域区查找,找不到再到上一级 a1 的作用域查找,如果还没找到就到全局作用域区查找,这样就形成了一个作用域链

16.2 闭包

什么是闭包?
当一个内部函数被其外部函数之外的变量引用时,就形成了一个闭包。

简单的来说,所谓的闭包就是一个具有封闭的对外不公开的,包裹结构或空间。

为什么函数可以构成闭包?
闭包就是一个具有封闭与包裹功能的结构,是为了实现具有私有访问空间的函数的。函数可以构成闭包。函数内部定义的数据函数外部无法访问,即函数具有封闭性;函数可以封装代码即具有包裹性,所以函数可以构成闭包。

16.3 闭包有什么用(特性)

闭包的作用,就是保存自己私有的变量,通过提供的接口(方法)给外部使用,但外部不能直接访问该变量。

当我们需要在模块中定义一些变量,并希望这些变量一直保存在内存中但又不会“污染”全局的变量时,就可以用闭包来定义这个模块。

闭包的缺点:闭包的缺点就是常驻内存,会增大内存使用量,使用不当很容易造成内存泄露。

函数套函数就是闭包吗?:不是!,当一个内部函数被其外部函数之外的变量引用时,才会形成了一个闭包。

16.4 闭包的基本模型

对象模式
函数内部定义个一个对象,对象中绑定多个函数(方法),返回对象,利用对象的方法访问函数内的数据

function createPerson() {
    var __name__ = "";
    return {
        getName: function () {
            return __name__;
        },
        setName: function( value ) {
            // 如果不姓张就报错
            if ( value.charAt(0) === '张' ) {
                __name__ = value;
            } else {
                throw new Error( '姓氏不对,不能取名' );
            }
        }
    }
}
var p = createPerson();
p.set_Name( '张三丰' );
console.log( p.get_Name() );
p.set_Name( '张王富贵' );
console.log( p.get_Name() );

函数模式
函数内部定义一个新函数,返回新函数,用新函数获得函数内的数据

function foo() {
    var num = Math.random();
    function func() {
        return mun;
    }
    return func;
}
var f = foo();
// f 可以直接访问这个 num
var res1 = f();
var res2 = f();

沙箱模式
沙箱模式就是一个自调用函数,代码写到函数中一样会执行,但是不会与外界有任何的影响,比如jQuery

(function () {
   var jQuery = function () { // 所有的算法 }
   // .... // .... jQuery.each = function () {}
   window.jQuery = window.$ = jQuery;
})();
$.each( ... )

原型

原型是什么
原型就是一个普通的对象,每个对象都有一个原型(Object除外),原型能存储我们的方法,构造函数创建出来的实例对象能够引用原型中的方法。

查看原型
以前一般使用对象的__proto__属性,ES6推出后,推荐用Object.getPrototypeOf()方法来获取对象的原型

什么是原型链?
凡是对象就有原型,那么原型又是对象,因此凡是给定一个对象,那么就可以找到他的原型,原型还有原型,那么如此下去,就构成一个对象的序列,称该结构为原型链。

更多内容请看这里

17.如何进行错误监控

前端错误的分类

  • 即时运行错误(代码错误)
  • 资源加载错误

错误的捕获方式
即时运行错误的捕获方式:

  • try...catch
  • window.onerror

资源加载错误:

  • object.onerror(如img,script)
  • performance.getEntries()
  • Error事件捕获
延伸:跨域的js运行错误可以捕获吗,错误提示什么,应该怎么处理?
可以。
Script error
1.在script标签增加crossorigin属性
2.设置js资源响应头Access-Control-Allow-Orgin:*

上报错误的基本原理
采用Ajax通信方式上报
利用Image对象上报

18.DOM事件类

DOM事件的级别

  • DOM0,element.onclick = function(){}
  • DOM2,element.addEventListener('click', function(){}, false);

DOM事件模型是什么:指的是冒泡和捕获
DOM事件流是什么:捕获阶段 -> 目标阶段 -> 冒泡阶段
描述DOM事件捕获的具体流程
window --> document --> documentElement(html标签) --> body --> .... --> 目标对象
Event对象常见应用

  • event.preventDefault(),阻止默认行为
  • event.stopPropagation(),阻止事件冒泡
  • event.stopImmediatePropagation(),阻止剩余的事件处理函数执行并且防止事件冒泡到DOM树上,这个方法不接受任何参数。
  • event.currentTarget,返回绑定事件的元素
  • event.target,返回触发事件的元素

如何自定义事件
Event,不能传递参数

var eve = new Event('自定义事件名');
ev.addEventListener('自定义事件名', function(){
    console.log('自定义事件')
});
ev.dispatchEvent(eve);

CustomEvent,还可以指定参数

19.本地起了一个http server,为什么只能在同一个WIFI(局域网)上访问?

你没有公网IP当然就不能被外网访问了。常见的WIFI情况下,一般的ip会是~192.168.0.x·这样的,只是对局域网(同WIFI下)可见,但是外网是访问不了的。(segmentfault上的答案

20.回流和重绘

参考《如何写出高性能DOM?》

21.数组去重的方法

参考:《JavaScript数组去重》

22.深拷贝与浅拷贝

是什么
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存(内存区域没有隔离)。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存(内存区域隔离),修改新对象不会改到原对象。在多层对象上,浅拷贝只拷贝一层
浅拷贝举例

var Chinese = {
  nation:'中国'
};
var Doctor ={
  career:'医生'
}
function extendCopy(p) {
  var c = {};
  for (var i in p) { 
    c[i] = p[i];
  }
  return c;
}
var Doctor = extendCopy(Chinese);
Doctor.career = '医生';
alert(Doctor.nation); // 中国

深拷贝举例

function deepCopy(p, c) {
  var c = c || {};
  for (var i in p) {
    if (typeof p[i] === 'object') {
      c[i] = (p[i].constructor === Array) ? [] : {};
      deepCopy(p[i], c[i]);
    } else {
      c[i] = p[i];
    }
  }
  return c;
}

参考文章:阮一峰:Javascript面向对象编程(三):非构造函数的继承

深拷贝实现方式

  • 手动复制方式,如上面的代码,缺点就是
  • Object.assign,ES6 的新函数,可以帮助我们达成跟上面一样的功能。
var obj1 = { a: 10, b: 20, c: 30 };
var obj2 = Object.assign({}, obj1);
obj2.b = 100;
console.log(obj1);
// { a: 10, b: 20, c: 30 } <-- 沒被改到
console.log(obj2);
// { a: 10, b: 100, c: 30 }
  • 转成 JSON 再转回来

用JSON.stringify把对象转成字符串,再用JSON.parse把字符串转成新的对象。
缺点:只有可以转成JSON格式的对象才可以这样用,像function没办法转成JSON。

  • jquery,有提供一个$.extend可以用来做 Deep Copy。
  1. lodash,也有提供_.cloneDeep用来做 Deep Copy。
  2. 递归实现深拷贝
function clone( o ) {
    var temp = {};
    for( var k in o ) {
        if( typeof o[ k ] == 'object' ){
             temp[ k ] = clone( o[ k ] );
        } else {
             temp[ k ] = o[ k ];
        }
    }
    return temp;
}

参考文章:关于 JS 中的浅拷贝和深拷贝,进击JavaScript之(四)玩转递归与数列

23.如何快速合并雪碧图

  • Gulp:gulp-css-spriter
  • webpack:optimize-css-assets-webpack-plugin
  • Go!Png
  • 在线工具

24.代码优化基本方法

减少HTTP请求
HTML优化:

  • 使用语义化标签
  • 减少iframe:iframe是SEO的大忌,iframe有好处也有弊端
  • 避免重定向

CSS优化:

  • 布局代码写前面
  • 删除空样式
  • 不滥用浮动,字体,需要加载的网络字体根据网站需求再添加
  • 选择器性能优化
  • 避免使用表达式,避免用id写样式

js优化:

  • 压缩
  • 减少重复代码

图片优化:

  • 使用WebP
  • 图片合并,CSS sprite技术

减少DOM操作

  • 缓存已经访问过的元素
  • "离线"更新节点, 再将它们添加到树中
  • 避免使用 JavaScript 输出页面布局--应该是 CSS 的事儿

使用JSON格式来进行数据交换
使用CDN加速
使用HTTP缓存:添加 ExpiresCache-Control 信息头
使用DNS预解析
Chrome内置了DNS Prefetching技术, Firefox 3.5 也引入了这一特性,由于Chrome和Firefox 3.5本身对DNS预解析做了相应优化设置,所以设置DNS预解析的不良影响之一就是可能会降低Google Chrome浏览器及火狐Firefox 3.5浏览器的用户体验。
预解析的实现:

  1. 用meta信息来告知浏览器, 当前页面要做DNS预解析:<meta http-equiv="x-dns-prefetch-control" content="on" />
  2. 在页面header中使用link标签来强制对DNS预解析: <link rel="dns-prefetch" href="http://bdimg.share.baidu.com" />

25.HTTPS的握手过程

  1. 浏览器将自己支持的一套加密规则发送给服务器。
  2. 服务器从中选出一组加密算法与HASH算法,并将自己的身份信息以证书的形式发回给浏览器。证书里面包含了网站地址,加密公钥,以及证书的颁发机构等信息。
  3. 浏览器获得网站证书之后浏览器要做以下工作:

    • 验证证书的合法
    • 如果证书受信任,或者是用户接受了不受信的证书,浏览器会生成一串随机数的密码,并用证书中提供的公钥加密。
    • 使用约定好的HASH算法计算握手消息,并使用生成的随机数对消息进行加密,最后将之前生成的所有信息发送给服务器
  4. 网站接收浏览器发来的数据之后要做以下的操作:

    • 使用自己的私钥将信息解密取出密码,使用密码解密浏览器发来的握手消息,并验证HASH是否与浏览器发来的一致。
    • 使用密码加密一段握手消息,发送给浏览器。
  5. 浏览器解密并计算握手消息的HASH,如果与服务端发来的HASH一致,此时握手过程结束,之后所有的通信数据将由之前浏览器生成的随机密码并利用对称加密算法进行加密。

参考文章:《HTTPS 工作原理和 TCP 握手机制》

26.BFC相关问题

BFC(Block formatting context)直译为"块级格式化上下文"。它是一个独立的渲染区域,只有 Block-level box 参 与, 它规定了内部的 Block-level Box 如何布局,并且与这个区域外部毫不相干。

BFC的渲染特点

  • BFC这个元素的垂直方向的边距会发生重叠,垂直方向的距离由margin决定,取最大值
  • BFC的区域不会与浮动元素的box重叠(清除浮动原理
  • 计算BFC的高度的时候,浮动元素也会参与计算

哪些元素会生成 BFC

BFC 就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此。

  • 根元素
  • overflow不为visible
  • float不为none
  • position为absolute或fixed
  • display为inline-block、table-cell、table-caption、flex、inline-flex

BFC的使用场景

他的很常用的一个应用场景就是解决边距重叠、清楚浮动的问题.

27.响应式图片

1.JS或者服务端硬编码,resize事件,判断屏幕大小加载不同的图片
2.img srcset 方法
3.picture标签 -> source
4.svg
5.第三方库polyfill

28.判断一个变量是否是数组

var a = []; 
// 1.基于instanceof 
a instanceof Array; 
// 2.基于constructor 
a.constructor === Array; 
// 3.基于Object.prototype.isPrototypeOf 
Array.prototype.isPrototypeOf(a); 
// 4.基于getPrototypeOf 
Object.getPrototypeOf(a) === Array.prototype; 
// 5.基于Object.prototype.toString 
Object.prototype.toString.apply(a) === '[object Array]';
// 6.Array.isArray
Array.isArray([]); // true

以上,除了Object.prototype.toString外,其它方法都不能正确判断变量的类型。

29.UTF-8和Unicode的区别

UTF-8就是在互联网上使用最广的一种unicode的实现方式。
Unicode的出现是为了统一地区性文字编码方案,为解决unicode如何在网络上传输的问题,于是面向传输的众多 UTF(UCS Transfer Format)标准出现了,顾名思义,UTF-8就是每次8个位传输数据,而UTF-16就是每次16个位。
ASCII --> 地区性编码(GBK) --> Unicode --> UTF-8
知乎参考回答


参考
慕课网实战课程《前端跳槽面试必备技巧》

查看原文

赞 263 收藏 1811 评论 36

认证与成就

  • 获得 57 次点赞
  • 获得 9 枚徽章 获得 0 枚金徽章, 获得 4 枚银徽章, 获得 5 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2014-04-09
个人主页被 479 人浏览