Code Monkey home page Code Monkey logo

blog's Introduction

QC.L's Blog

感谢大家的关注,如果觉得不错,请点个 star!

年终总结

前端相关

小程序

React

webpack

Vue

Babel

问题笔记

参会总结

D2

印记中文

后端相关

Node.js

MySQL

Redis

跨平台相关

iOS相关

其他

注意: 如需转载,请标明出处 https://github.com/QC-L/blog

blog's People

Contributors

qc-l avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

blog's Issues

VSCode 常用插件指北

文件夹目录 Icon

安装 Material Icon Theme

修改 Powerline 字体

VSCode 修改控制台 zshPowerline 字体:

"terminal.integrated.fontFamily": "Source Code Pro for Powerline"

编写 Github-Bot 指南

最近在维护 React 官网的中文站时,随着时间的推移,参与的人员越来越多。就发现有点力不从心的感觉,因此,决定编写一个 Github-bot 提高自己维护仓库的效率。如自动打 Label,添加 reviewer 等。甚至与 netlify.com 结合,实现 PR 预览等功能。

原理

Github-Bot 原理非常简单,主要是基于 Github API 结合 Webhook 就可以做到一些自动化的操作。如,为 PR 指定 Reviewer,自定添加 Label,判断 PR 名是否符合规范等。

步骤

  1. 申请 GitHub Token
  2. 查阅 Github API
  3. 使用 Node 根据 Github API 接口,生成自己的 API。(可使用第三方的 github-api 库,结合 koa,express 等)
  4. 部署服务,到服务器
  5. 根据接口选择对应的 Webhook

由于本人在研究 iOS 时,使用过 Github API,对 Github 的 API 相对较为了解。因此,本文将采用第三方库的形式编写。使用的是 github-bot 中使用的 octokit/rest.js

Getting Start

创建 github-bot 文件夹:

$ mkdir github-bot && cd github-bot

初始化 package.json

$ npm init -y
or
$ yarn init -y 

安装 octokit/rest.js

$ npm install @octokit/rest
or
$ yarn add @octokit/rest

创建 github.js

$ mkdir src && cd src
$ touch github.js

将以下内容贴到 github.js 中:

// 引入 api 库
const Octokit = require('@octokit/rest');

// 创建 github 连接
const github = new Octokit({
    auth: 'token (personal access tokens)', // 括号里添加 第一步生成的 token
    previews: ['hellcat-preview'] // 查看 https://developer.github.com/v3/previews/
})

async function addLabelsToPullRequest(payload, labels) {
    const owner = payload.repository.owner.login // 获取仓库拥有者
    const repo = payload.repository.name // 获取仓库名
    const number = payload.pull_request.number // 获取 pr 编号

    try {
      // https://octokit.github.io/rest.js/#api-Issues-addLabels
      await github.issues.addLabels({
        owner,
        repo,
        number,
        labels: toArray(labels)
      })
      return true
    } catch (e) {
      console.error(e);
    }
}

上述编写了一个为 PR 添加 Label 的例子,具体可以查阅 octokit/rest

大家可以尝试把 API 中的内容都封装一遍,我看了 Github-Bot 的内容,用的还是 require('github')。因此决定自己重新撸一个,更新到最新版本。

将对应的 API 编写完毕后,可以封装到 koa 中,部署到服务器上,再与 github 的 webhook 结合即可。

webpack 文档更新日志(7.11-7.27)

Hi,大家好,今天又到了 webpack 文档更新日志同步的环节。

老样子,更新日志会分为英文篇和中文篇:

  1. 英文篇会主要介绍 webpack 文档的更新部分;
  2. 中文篇则会介绍中文站的最新进展。

话不多说,开始正题。

英文篇

内容更新

API 部分

  • 新增了在 loader/plugin 中使用 Logger API 的示例
  • Compilation Object 去除了 modifyHash 选项
  • 模块方法中新增了引入 data uri 的示例
import 'data:text/javascript;charset=utf-8;base64,Y29uc29sZS5sb2coJ2lubGluZSAxJyk7';
import { number, fn } from 'data:text/javascript;charset=utf-8;base64,ZXhwb3J0IGNvbnN0IG51bWJlciA9IDQyOwpleHBvcnQgY29uc3QgZm4gPSAoKSA9PiAiSGVsbG8gd29ybGQiOw==';

配置(Configuration)

  • module 中新增了 Rule.mimetype 选项,以支持处理 data uri
  • Optimization 中的 minimizer 选项新增了 ... 参数用于访问默认值。
  • watch 部分不再支持对 watchOptions 直接赋值数字后,等同于选项 aggregateTimeout 的操作

插件 (Plugin)

  • 移除了 HotModuleReplacementPlugin 插件的 multiStepfullBuildTimeout 的选项。
  • splitChunks 新增了 enforceSizeThreshold 选项

loader

  • css-loader 增加了 namedExport 选项
  • css-loader 删除了 exportGlobals 选项

概念

  • 模块联合部分中新增了动态远程容器 —— 此部分还未翻译,有兴趣的可以联系我

指南

  • 当使用插件生成配置中的 entry 时,webpack.config 中的 entry 支持传递空对象
  • 构建性能章节,优化了对 ts-loader 的描述,增加了优化选项
    • 可以使用 transpileOnly 来缩短 ts-loader 的构建时间
    • 但同时会关闭类型检查,可以通过 ForkTsCheckerWebpackPlugin 开启
  • output 章节移除了 jsonpScriptType,统一使用 scriptType

中文篇

内容部分

API

  • 完成了 cli 部分的翻译

loader

  • 完成了 sass-loader 部分的翻译

站点更新

  • 优化了锚点跳转,修复了中文造成页面展示异常的问题

注:站点更新后,大家在翻译标题时,需保留 {#} 中的部分。

其他

关于站点优化,有一篇总结近期会呈现给大家,敬请期待。

React Hook 源码阅读笔记

Hook 推出之后增强了函数组件。让函数组件拥有了如 class 组件一般的特性,甚至超越 class 组件的存在。因此,通过阅读源码来看看 React Hook 的真面目。

我们会从以下几个文件来阅读 Hook 相关源码:

  • React.js
  • ReactHooks.js
  • ReactCurrentDispatcher.js
  • ReactFiberHooks.js

React.js

话不多说,让我们一探究竟!

首先,代码阅读先从入口开始:

React 引入方式如下:

import React from 'react';

如果使用了 Component,Hook 之类代码,引入方式如下所示:

import React, {
  Component,
  useState,
  useEffect
} from 'react';

由上述代码可以知,Hook 绑定在 React 对象之中

按照这条线索寻找,会在 react/src/React.js 文件中发现如下代码:

import {
  useCallback,
  useContext,
  useEffect,
  useImperativeHandle,
  useDebugValue,
  useLayoutEffect,
  useMemo,
  useReducer,
  useRef,
  useState,
} from './ReactHooks';

const React = {
  ... // more code
  useCallback,
  useContext,
  useEffect,
  useImperativeHandle,
  useDebugValue,
  useLayoutEffect,
  useMemo,
  useReducer,
  useRef,
  useState,
  ... // more code
};

export default React;

由此可知,Hook 相关的逻辑都保存在了 react/src/ReactHooks.js 当中。

ReactHooks.js

打开 react/src/ReactHooks.js 文件,先来看看引入的头文件:

import type {ReactContext} from 'shared/ReactTypes'; // 引入 ReactContext 类型 (给 flow 使用)
import invariant from 'shared/invariant';  // invariant 类似于断言,用于阻止代码执行
import warning from 'shared/warning'; // warning 用于抛出警告

import ReactCurrentDispatcher from './ReactCurrentDispatcher'; // 后面会对其进行讲解

从头文件中我们得出以下信息:

  1. 引入了 ReactContext 的类型,说明要给 useContext 作类型判断;
  2. 使用 invariant 作断言,说明某些代码执行不过,可能会中端;
  3. 使用 warning 抛出警告信息;
  4. ReactCurrentDispatcher 暂时不知其作用。

看完头文件,来看看 Hook 的相关实现:

// useState
export function useState<S>(initialState: (() => S) | S) {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

... // more code

export function useEffect(
  create: () => (() => void) | void,
  inputs: Array<mixed> | void | null,
) {
  const dispatcher = resolveDispatcher();
  return dispatcher.useEffect(create, inputs);
}

... // more code

从以上代码可以得到几点信息:

  1. 代码中针对 useContext 部分,使用了 warning 抛出警告
  2. 都调用了 resolveDispatcher() 函数;
  3. resolveDispatcher() 会返回一个 dispatcher
  4. dispatcher 中实现了所有的 hook;
  5. 使用了 Flow(同其他源码)。

接下来,查看 resolveDispatcher() 函数:

function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current;
  invariant(
    dispatcher !== null,
    'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
      ' one of the following reasons:\n' +
      '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
      '2. You might be breaking the Rules of Hooks\n' +
      '3. You might have more than one copy of React in the same app\n' +
      'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
  );
  return dispatcher;
}

通过以上代码可知:

  1. dispatcher 实质上就是通过 ReactCurrentDispatcher.current 获取的;
  2. 在获取 dispatcher 后,使用 invariantdispatcher 进行了类似于断言的操作(用于进行规则和版本鉴别);

综上所述,我猜测 Hook 的核心文件可能是 ReactCurrentDispatcher.js

ReactCurrentDispatcher.js

然而,我错了。
此文件的代码如下:

import type {Dispatcher} from 'react-reconciler/src/ReactFiberHooks';

/**
 * Keeps track of the current dispatcher.
 */
const ReactCurrentDispatcher = {
  /**
   * @internal
   * @type {ReactComponent}
   */
  current: (null: null | Dispatcher),
};

export default ReactCurrentDispatcher;

ReactNative - 打离线包 (二) 携程Moles-Packer框架命令打包

ReactNative - 打离线包 (二) 携程 Moles-Packer 框架命令打包

Moles-Packer 是由携程框架团队研发的,与携程 Moles 框架配套使用的 React Native 打包和拆包工具,同时支持原生的 React Native 项目。

安装

执行命令进行 Moles-Packer 的安装

npm install -g moles-packer

Moles Packer 安装完成后,将创建两个命令:

  • moles-packer
    用于项目构建,可编译业务代码并将其打包,也可同时创建公包。

  • moles-packer-common
    仅用于创建公包及相应的元数据文件。

可通过命令进行 moles-packer 版本的查询

moles-packer -v
moles-packer-common -v

上述操作步骤如下:
具体操作步骤如下

Moles-Packer 参数详解

Options

  --bundle [<bundle.js>] 指示将构建结果合并输出,该文件将被保存在 --output 指定的目录中, 默认情况下 bundle 中不包含公包模块,除非使用 --standalone 选项。
  --common-input 指定公包目录,并在构建过程中使用该目录中的预制公包及其元数据文件。
  --common-modules 指定公包中所包含的模块,多个模块之间使用逗号分隔。
  --common-output 指定公包输出目录。当选项值是相对路径时,如果以 . 或 .. 起始,则相对于当前工作目录;否则认为相对于 --output 选项所指定的输出目录。
  --dev 开发模式下构建的代码,在运行时可以输出更多的调试信息。
  --entry 指定入口文件名称。 默认为 index.js
  --exec-on-required 当且仅当模块首次被实际使用时,执行其相应的定义程序。
  --input 指定项目目录。默认为当前目录
  --minify 默认情形下,Moles Packer 将保持输出代码的可读性,不对其进行压缩和混淆。
  --output 指定输出目录。默认为./build,如果该目录不存在,Moles Packer 会自动创建。 如果该目录已经存在,其中的文件有可能被覆盖。
  --platform 指定构建输出结果适用的操作系统平台。如: ios
  --standalone 是否输出可独立运行的 bundle 文件。该选项应与 --bundle 和 --platform 选项配合使用。

Moles-Packer 打包准备

Moles-Packer 对 React Native 版本有要求

React Native 版本要求

由上可知,Moles-Packer暂时还不支持 React Native 的 0.38.0 以上的版本, 目前 React Native 的最新版本为 0.40.0 。

0.38.0使用moles-packer

因此,构建 React Native 工程需要使用 0.37.0 以下的版本。

Moles-Packer 打包

首先你得有个 React Native 的工程。这里以空工程打包为例:
1.创建适合 Moles-Packer 的 React Native 版本

react-native init ReactNativeV37 --version 0.37.0 

2.执行打包命令

moles-packer --entry index.ios.js --platform ios --standalone --output ./ios/build --verbose 

3.查看执行完打包命令后的结果

[MOLES_PACKER] -- options parsed --

  name            value                                              
  --------------  ---------------------------------------------------
  base            "/Users/QCL/ReactNative/ReactNativeV37"   
  bundle          true                            
  commonOutput    "BASE/ios/build/moles.common"   
  dev             false                           
  entry           "BASE/index.ios.js"             
  execOnRequired  false                           
  input           "BASE/"                         
  isCLI           true                            
  metaOutput      "BASE/ios/build/moles.meta.json"
  minify          false                           
  output          "BASE/ios/build"                
  platform        "ios"                                     
  single          false                           
  standalone      true                            
  verbose         true                            

[MOLES_PACKER] -- Process common bundle --
[COMMON] Create common bundle ( ios )
[2017-1-16 14:25:15] <START> Initializing Packager
[2017-1-16 14:25:15] <START> Building in-memory fs for JavaScript
[2017-1-16 14:25:15] <END>   Building in-memory fs for JavaScript (164ms)
[2017-1-16 14:25:15] <START> Building Haste Map
[2017-1-16 14:25:15] <END>   Building Haste Map (96ms)
[2017-1-16 14:25:15] <END>   Initializing Packager (333ms)
[2017-1-16 14:25:15] <START> Transforming modules
[2017-1-16 14:25:15] <END>   Transforming modules (385ms)
bundle: start
bundle: finish
bundle: Writing bundle output to: .moles/common.ios.jsbundle
bundle: Writing sourcemap output to: .moles/common.ios.sourcemap.json
bundle: Done writing bundle output
bundle: Done writing sourcemap output
[COMMON] Re-create common bundle ( ios )
[COMMON] Minify common bundle ( ios )
[COMMON] COMMON BUNDLE: /Users/QCL/ReactNative/ReactNativeV37/ios/build/moles.common/common.ios.jsbundle ( ios )
[COMMON] COMMON META: /Users/QCL/ReactNative/ReactNativeV37/ios/build/moles.common/common.ios.json ( cross )
[MOLES_PACKER] Common modules ready.
[MOLES_PACKER] -- Process business code --
[SOURCE] index.ios.js ( entry )
[BUNDLE] - ( top )
[BUNDLE] index.ios.js ( entry )
[BUNDLE] - ( bottom )
[TARGET] index.ios.jsbundle ( bundle )
[TARGET] moles.meta.json ( meta )
[MOLES_PACKER] -- end --
[MOLES_PACKER] See /Users/QCL/ReactNative/ReactNativeV37/ios/build

目录层级

4.将 index.ios.jsbundle 引入工程

index.ios.jsbundle

5.修改 AppDelegate.m 中的源码

NSURL *jsCodeLocation;

// jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil];
jsCodeLocation = [NSURL URLWithString:[[NSBundle mainBundle] pathForResource:@"index.ios.jsbundle" ofType:nil]];
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                                    moduleName:@"ReactNativeV37"
                                             initialProperties:nil
                                                 launchOptions:launchOptions];

6.如果要离线真机测试或打包上传应用, 需要进行如下修改

因为 ReactNative 自带 Chrome 的 Debug 模式, 因此需要修改成 Release ,来关闭掉 Debug 模式
修改Release

7.打包上传或真机调试,与iOS工程无异。
具体步骤如下:
打包过程

打包过程中的问题

问题1: 找不到的 node-haste 模块

[MOLES_PACKER] -- Process common bundle --
module.js:474
throw err;
       ^

Error: Cannot find module 'node-haste/lib/DependencyGraph/docblock.js'
at Function.Module._resolveFilename (module.js:472:15)
at Function.Module._load (module.js:420:25)
at Module.require (module.js:500:17)
at require (internal/module.js:20:19)
at _create_common (/usr/local/lib/node_modules/moles-packer/lib/packCommon.js:37:17)
at _ME (/usr/local/lib/node_modules/moles-packer/lib/packCommon.js:282:18)
at _ME (/usr/local/lib/node_modules/moles-packer/lib/pack.js:22:22)
at Object. (/usr/local/lib/node_modules/moles-packer/bin/pack.js:4:1)
at Module._compile (module.js:573:32)
at Object.Module._extensions..js (module.js:582:10)

解决方案

npm install -g node-haste

问题2:找不到的 transform-es5-property-mutators 插件

[MOLES_PACKER] Common modules ready.
[MOLES_PACKER] -- Process business code --
[SOURCE] index.ios.js ( entry )
start ... react / es2015 ... /usr/local/lib/node_modules/moles-packer/node_modules/babel-core/lib/transformation/file/options/option-manager.js:176
throw new ReferenceError(messages.get("pluginUnknown", plugin, loc, i, dirname));
^

ReferenceError: Unknown plugin "transform-es5-property-mutators" specified in "base" at 0, attempted to resolve relative to "/Users/QCL/ReactNative/v031"
at /usr/local/lib/node_modules/moles-packer/node_modules/babel-core/lib/transformation/file/options/option-manager.js:176:17
at Array.map (native)
at Function.normalisePlugins (/usr/local/lib/node_modules/moles-packer/node_modules/babel-core/lib/transformation/file/options/option-manager.js:154:20)
at OptionManager.mergeOptions (/usr/local/lib/node_modules/moles-packer/node_modules/babel-core/lib/transformation/file/options/option-manager.js:229:36)
at OptionManager.init (/usr/local/lib/node_modules/moles-packer/node_modules/babel-core/lib/transformation/file/options/option-manager.js:374:12)
at File.initOptions (/usr/local/lib/node_modules/moles-packer/node_modules/babel-core/lib/transformation/file/index.js:216:65)
at new File (/usr/local/lib/node_modules/moles-packer/node_modules/babel-core/lib/transformation/file/index.js:139:24)
at Pipeline.transform (/usr/local/lib/node_modules/moles-packer/node_modules/babel-core/lib/transformation/pipeline.js:46:16)
at _transform_react (/usr/local/lib/node_modules/moles-packer/lib/transform.js:137:24)
at _one (/usr/local/lib/node_modules/moles-packer/lib/transform.js:110:9)

解决方案

npm install -g babel-plugin-transform-es5-property-mutators

问题3: 找不到的 es2015 模块

[MOLES_PACKER] -- Process business code --
[SOURCE] index.ios.js ( entry )
start ... react / es2015 ... /usr/local/lib/node_modules/moles-packer/node_modules/babel-core/lib/transformation/file/options/option-manager.js:334
        throw e;
        ^

Error: Couldn't find preset "es2015" relative to directory "/Users/QCL/ReactNative/ReactNativeV37"
    at /usr/local/lib/node_modules/moles-packer/node_modules/babel-core/lib/transformation/file/options/option-manager.js:299:19
    at Array.map (native)
    at OptionManager.resolvePresets (/usr/local/lib/node_modules/moles-packer/node_modules/babel-core/lib/transformation/file/options/option-manager.js:270:20)
    at OptionManager.mergePresets (/usr/local/lib/node_modules/moles-packer/node_modules/babel-core/lib/transformation/file/options/option-manager.js:259:10)
    at OptionManager.mergeOptions (/usr/local/lib/node_modules/moles-packer/node_modules/babel-core/lib/transformation/file/options/option-manager.js:244:14)
    at OptionManager.init (/usr/local/lib/node_modules/moles-packer/node_modules/babel-core/lib/transformation/file/options/option-manager.js:374:12)
    at File.initOptions (/usr/local/lib/node_modules/moles-packer/node_modules/babel-core/lib/transformation/file/index.js:216:65)
    at new File (/usr/local/lib/node_modules/moles-packer/node_modules/babel-core/lib/transformation/file/index.js:139:24)
    at Pipeline.transform (/usr/local/lib/node_modules/moles-packer/node_modules/babel-core/lib/transformation/pipeline.js:46:16)
    at _transform_react (/usr/local/lib/node_modules/moles-packer/lib/transform.js:137:24)

解决方案:

npm install babel-preset-es2015

问题4: 找不到 stage-0 模块

[MOLES_PACKER] -- Process business code --
[SOURCE] index.ios.js ( entry )
start ... react / es2015 ... /usr/local/lib/node_modules/moles-packer/node_modules/babel-core/lib/transformation/file/options/option-manager.js:334
        throw e;
        ^

Error: Couldn't find preset "stage-0" relative to directory "/Users/QCL/ReactNative/ReactNativeV37"
    at /usr/local/lib/node_modules/moles-packer/node_modules/babel-core/lib/transformation/file/options/option-manager.js:299:19
    at Array.map (native)
    at OptionManager.resolvePresets (/usr/local/lib/node_modules/moles-packer/node_modules/babel-core/lib/transformation/file/options/option-manager.js:270:20)
    at OptionManager.mergePresets (/usr/local/lib/node_modules/moles-packer/node_modules/babel-core/lib/transformation/file/options/option-manager.js:259:10)
    at OptionManager.mergeOptions (/usr/local/lib/node_modules/moles-packer/node_modules/babel-core/lib/transformation/file/options/option-manager.js:244:14)
    at OptionManager.init (/usr/local/lib/node_modules/moles-packer/node_modules/babel-core/lib/transformation/file/options/option-manager.js:374:12)
    at File.initOptions (/usr/local/lib/node_modules/moles-packer/node_modules/babel-core/lib/transformation/file/index.js:216:65)
    at new File (/usr/local/lib/node_modules/moles-packer/node_modules/babel-core/lib/transformation/file/index.js:139:24)
    at Pipeline.transform (/usr/local/lib/node_modules/moles-packer/node_modules/babel-core/lib/transformation/pipeline.js:46:16)
    at _transform_react (/usr/local/lib/node_modules/moles-packer/lib/transform.js:137:24)

解决方案:

npm install babel-preset-stage-0

问题3和问题4的
解决方案二:

使用 subl 打开 package.json

subl package.json

在 packjson.json 的 devDependencies 中添加如下代码:

"babel-preset-stage-0": "6.16.0",
"babel-preset-es2015": "6.18.0"

添加完成后,如下:

{
  "name": "ReactNativeV37",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "node node_modules/react-native/local-cli/cli.js start",
    "test": "jest"
  },
  "dependencies": {
    "react": "15.3.2",
    "react-native": "0.37.0"
  },
  "jest": {
    "preset": "jest-react-native"
  },
  "devDependencies": {
    "babel-jest": "18.0.0",
    "babel-preset-react-native": "1.9.1",
    "jest": "18.1.0",
    "jest-react-native": "18.0.0",
    "react-test-renderer": "15.3.2",
    "babel-preset-stage-0": "6.16.0",
    "babel-preset-es2015": "6.18.0"
  }
}

最后执行

npm install

参考文章

我在 Github 提的 issues
从零开始,使用 Moles Packer
React Native 版本
CtripMoles 的简书

记 webpack 中文文档的一次优化

webpack 主文档的翻译和迁移工作基本接近尾声,感谢社区小伙伴的参与,才能让文档的翻译工作进行得如此迅速。

这里重点感谢 冯博 和 黄锦华 的积极参与与付出。

最近,有位细心的小伙伴在浏览文档时发现了锚点跳转异常的问题,这段时间我针对此问题着手进行了调研和解决。

先来描述下遇到的问题。

遇到的问题

打开 webpack 中文文档,随便找一篇文档介绍。比如直接访问概念 -> 入口,会发现页面空白。

image

具体异常如下:

DOMException: Failed to execute 'querySelector' on 'Document': '#%E5%85%A5%E5%8F%A3entry' is not a valid selector.

从错误中可以很直观的看出,我们向 querySelector 中传入了一个 id 选择器,其值为 #%E5%85%A5%E5%8F%A3entry

由于 id 中不能使用 %,所以报错,而出现 % 的原因是浏览器针对链接进行了 encode 操作,把「入口」转义成了 %E5%85%A5%E5%8F%A3

ps: 这个问题主要是对文中标题进行了中文翻译造成的。

初步解决方案

既然知道了是浏览器对链接进行转义造成的,那么我们针对使用到 querySelector 的地方在传参前转义即可。

webpack 文档的源码中查找 querySelector,最终在 src/components/Page/Page.jsx 中找到了元凶。

const hash = window.location.hash;
if (hash) {
  const element = document.querySelector(hash);
  if (element) {
    element.scrollIntoView();
  }
} else {
  document.documentElement.scrollTop = 0;
}

我们来优化下,防止浏览器转义中文字符:

const hash = window.location.hash;
if (hash) {
  const newHash = decodeURIComponent(hash);
  const element = document.querySelector(newHash);
  if (element) {
    element.scrollIntoView();
  }
} else {
  document.documentElement.scrollTop = 0;
}

如此修改后,发现浏览器能够跳转正常,问题得到了基本解决。

但是点击页面中的列表,又出现了新的问题。

新的问题

文中有大量引用链接,而锚点被改为了中文,链接无法识别也无法跳转

有了新的问题,就要想新的解决方案。

和社区的小伙伴讨论了几种解决方案:

  1. 将英文文档作为中文文档的 submodules,然后做对应关系
  2. 全部改为中文,使用中文锚点
  3. 采用 React 文档的形式,在标题后添加后缀(采用)

分别说下几种思路:

方案 1 的话,实现难度较高,并且对中英文文档要求较高,务必做到一一对应

方案 2 大量引用链接,逐个修改也会费时费力,并且无法做到傻瓜式修改

最终,采用了方案 3 的方式,可以做到与原文档无任何差别,并且保证了跳转。最重要的是,方便修改。

具体方案就是如下,在标题后加入{#锚点标题}

# Getting Start! {#getting-start}

Hello, Markdown

## Usage {#usage}

[link](https://docschina.org)

### 🔥Fire {#fire}

我是一行**加粗**的文字。

### Usage with configuration file {#usage-with-configuration-file}

以上为实际的输出效果。

如此修改又遇到了难题:

  1. webpack 本身的文档构建不能识别 {#} 这种形式
  2. 即使能够识别,webpack 文档大约有 200 篇左右的文档,要手动修改?

我们开始针对文档站点的部署以及构建进行查看,然后思考如何对解决这两个问题。

魔改源码

先解决 webpack 构建不能识别 {#} 的问题。

翻看 webpack 文档的源码,你会发现 webpack 文档采用的是 remark 对 md 文件进行编译的,中间使用了大量的 remark 相关的插件。

简单介绍下 remarkremark 是 markdown 的处理器。可以将 markdown 转换成你想要的效果。

在 webpack 文档站的 webpack.common.js 文件中有一个 mdPlugin 的属性:

const mdPlugins = [
  require('remark-slug'),
  [
    require('remark-custom-blockquotes'),
    {
      mapping: {
        'T>': 'tip',
        'W>': 'warning',
        '?>': 'todo'
      }
    }
  ],
  [
    require('remark-autolink-headings'),
    {
      behaviour: 'append'
    }
  ],
  [
    require('remark-responsive-tables'),
    {
      classnames: {
        title: 'title',
        description: 'description',
        content: 'content',
        mobile: 'mobile',
        desktop: 'desktop'
      }
    }
  ],
  require('remark-refractor')
];

通过断点调试的方式,可以发现在经过 remark-slug 插件后,文档的标题,就被处理成 html 中对应的 title 和 id 了。

因此,解决第一个问题的方式,就是将 remark-slug 魔改成,可以处理 {#} 的插件。

因此,我编写了 docschina-remark-slugger 插件,以解决 {#} 无法处理的问题。

除了 markdown 需要的编译需要修改之外,还需要针对文档站的左侧导航进行修改。

翻看源码,在 Page.jsx 中找到解析 title 和 id 的部分,进行了解析处理。

上述过程的处理方式,其实很简单,正则匹配到 {#} 中 # 后面的部分,将后面的部分设置为 id,然后将 {} 前的内容,设置为 title 即可。

奇技淫巧

解决了编译问题,接下来就是体力活了。

如何把这么多的文档都加上后缀?

最好省时省力,因为程序员都比较懒(比如我)。

修改文件最好的方式是通过 AST 修改,而修改 markdown 的 AST 网上已经存在需要现成的库。

刚刚上面提到的 remark 就属于这类库。

通过调研,我想到了一个非常取巧的方式。

采用 lint 的方式对源文档进行修改,再结合 git 可以实现对已翻译的文档进行后缀添加。

最后,经过一番调研,最后采用了 textlint 对 md 进行处理。

敲定了方案,开始翻阅文档思考如何解决。

我编写了测试文件 test.md

# Getting Start!

Hello, Markdown

## Usage

[link](https://docschina.org)

### 🔥Fire

我是一行**加粗**的文字。

### Usage with configuration file

这里将所有需要考虑的情况,都编写了进去。

如果想用 textlint 处理 md 文件,编写其对应的规则即可。

而我主要使用的是 lint 的 fix 功能。

因此,在编写规则时,还需编写其 fix 的处理方式。

textlint 的 rule 的实现如下:

const reporter = (context, options = {}) => {
    const {Syntax, RuleError, fixer, report, getSource} = context;
    return {
        [Syntax.Header](node) {
            let text = getSource(node); // Get text
            let match = /^.+(\s*\{#([a-z0-9\-_]+?)\}\s*)$/.exec(text);
            if (!match) {
                const index = text.length
                text = text.replace(/#/g, '')
                        .trim()
                        .replace(specials, '')
                        .replace(emoji(), '')
                        .replace(whitespace, '-')
                text = hyphenate(text)
                text = ` {#${text}}`
                const fix = fixer.insertTextAfter(node, text)
                const ruleError = new RuleError("Found bugs.", {
                    index, // padding of index
                    fix
                });
                report(node, ruleError);
            }
        }
    }
};

这里考虑了很多特殊情况,如标题中出现 emoji 表情,有特殊符号,空格等。

试用一下:

ezgif-7-88d007f4f63b

感觉还不错,基本上解决了问题。

lint 有一个最大的好处就是,可以批量修改文件。

具体所有操作,详见 docschina/webpack.js.org

总结

难题总会有的,想办法解决就好了。

lint 可以不止于 lint。

ReactNative - 打离线包 (一) 原生RN命令打包

ReactNative - 打离线包 (一) 原生RN命令打包

ReactNative 是由 Facebook 基于 React.js 开发的一套跨平台开发框架。
相信看到这篇文章的人对 ReactNative 已经有过一些了解,这里不作过多赘述。
本文主要基于 ReactNative 打离线包这件事进行详解。

离线包

离线包就是把 ReactNative 和你写的 js文件、图片等资源都打包放入 App ,不需要走网络下载。

ReactNative 打包命令说明

使用 react-native bundle --help 来查看打包的具体参数

  react-native bundle [options]
  builds the javascript bundle for offline use

  Options:

    -h, --help                   output usage information
    --entry-file <path>          Path to the root JS file, either absolute or relative to JS root
    --platform [string]          Either "ios" or "android"
    --transformer [string]       Specify a custom transformer to be used
    --dev [boolean]              If false, warnings are disabled and the bundle is minified
    --prepack                    When passed, the output bundle will use the Prepack format.
    --bridge-config [string]     File name of a a JSON export of __fbBatchedBridgeConfig. Used by Prepack. Ex. ./bridgeconfig.json
    --bundle-output <string>     File name where to store the resulting bundle, ex. /tmp/groups.bundle
    --bundle-encoding [string]   Encoding the bundle should be written in (https://nodejs.org/api/buffer.html#buffer_buffer).
    --sourcemap-output [string]  File name where to store the sourcemap file for resulting bundle, ex. /tmp/groups.map
    --assets-dest [string]       Directory name where to store assets referenced in the bundle
    --verbose                    Enables logging
    --reset-cache                Removes cached files
    --config [string]            Path to the CLI configuration file

以上为官方给出的解释,我们来对应的翻译下每条参数的含义。

  react-native bundle [参数]
  构建 js 离线包 

  Options:

    -h, --help                   输出如何使用的信息
    --entry-file <path>          RN入口文件的路径, 绝对路径或相对路径
    --platform [string]          ios 或 andorid
    --transformer [string]       Specify a custom transformer to be used
    --dev [boolean]              如果为false, 警告会不显示并且打出的包的大小会变小
    --prepack                    当通过时, 打包输出将使用Prepack格式化
    --bridge-config [string]     使用Prepack的一个json格式的文件__fbBatchedBridgeConfig 例如: ./bridgeconfig.json
    --bundle-output <string>     打包后的文件输出目录, 例: /tmp/groups.bundle
    --bundle-encoding [string]   打离线包的格式 可参考链接https://nodejs.org/api/buffer.html#buffer_buffer.
    --sourcemap-output [string]  生成Source Map,但0.14之后不再自动生成source map,需要手动指定这个参数。例: /tmp/groups.map
    --assets-dest [string]       打包时图片资源的存储路径
    --verbose                    显示打包过程
    --reset-cache                移除缓存文件
    --config [string]            命令行的配置文件路径

看过了以上的翻译,基本对每条参数都有了一定的了解,我们来实际操作下打包的步骤。

ReactNative 打离线包流程 (举例iOS)

首先你得有个 ReactNative 的工程。这里以空工程打包为例:

1.创建新工程

react-native init RNBundleDemo

2.执行打包命令

react-native bundle --entry-file index.ios.js --bundle-output ./ios/bundle/index.ios.jsbundle --platform ios --assets-dest ./ios/bundle --dev false

3.查看执行完打包命令后的结果

Unable to parse cache file. Will clear and continue.
[2017-1-3 16:58:56] <START> Initializing Packager
[2017-1-3 16:58:56] <START> Building in-memory fs for JavaScript
[2017-1-3 16:58:56] <END>   Building in-memory fs for JavaScript (74ms)
[2017-1-3 16:58:57] <START> Building Haste Map
[2017-1-3 16:58:57] <END>   Building Haste Map (392ms)
[2017-1-3 16:58:57] <END>   Initializing Packager (498ms)
[2017-1-3 16:58:57] <START> Transforming files
[2017-1-3 16:58:57] <END>   Transforming files (436ms)
bundle: start
bundle: finish
bundle: Writing bundle output to: ./ios/bundle/index.ios.jsbundle
bundle: Copying 5 asset files
bundle: Done writing bundle output
bundle: Done copying assets

打包完成后, 目录结构
4.将 assets 和 index.ios.jsbundle 引入工程

引入目录后的层级结构

注意: assets 目录导入工程中时,要选择 Create folder references,因为这是图片素材。

5.修改AppDelegate中的代码

NSURL *jsCodeLocation;

// jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil];
jsCodeLocation = [NSURL URLWithString:[[NSBundle mainBundle] pathForResource:@"index.ios.jsbundle" ofType:nil]];
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                                      moduleName:@"RNBundleDemo"
                                               initialProperties:nil
                                                   launchOptions:launchOptions];

6.如果要真机测试或打包上传应用, 需要进行如下修改

因为 ReactNative 自带 Chrome 的 Debug 模式, 因此需要修改成 Release ,来关闭掉 Debug 模式
修改工程的编译模式

7.打包上传或真机调试,与iOS工程无异。

ReactNative 打离线包中注意事项

  • 打包命令中的路径(文件夹一定要存在)
  • 必须用 Create folder references 的方式引入图片的 assets ,否则引用不到图片
  • 不能用 main.jsbundle 来命名打包后的文件,否则会出现问题

参考文章

https://segmentfault.com/a/1190000004189538
http://www.ruanyifeng.com/blog/2013/01/javascript_source_map.html
http://402v.com/react-nativeru-men-shi-li-jiao-cheng-xiang-mu-da-bao-fa-bu/
https://nodejs.org/api/buffer.html#buffer_buffer
http://reactnative.cn/docs/0.39/running-on-device-ios.html#content

文章预告

下篇文章我会进行携程 moles-packer 框架的分包过程及命令。欢迎大家继续关注 ReactNative - 打离线包 (二) 携程Moles-Packer框架命令打包

Redis 实用操作一 (安装篇)

安装

下载,解压,编译:

$ wget http://download.redis.io/releases/redis-4.0.8.tar.gz
$ tar xzf redis-4.0.8.tar.gz
$ cd redis-4.0.8
$ make

使用 make 编译后,可以使用以下命令进行验证

$ make test

二进制文件是编译完成后在 src 目录下,通过下面的命令启动 Redis 服务:

$ src/redis-server

你可以使用内置的客户端命令redis-cli进行使用:

$ src/redis-cli
redis> set foo bar
OK
redis> get foo
"bar"

深度安装

安装到 /usr/local/bin 目录下:

$ make install

需要进到 ./utils目录下,执行 :

$ cd ./utils
$ sudo ./install_server.sh // 该文件会生成 redis 所需的所有配置文件

注意:在执行该命令时,需要获取最高权限!

ReactNative 0.54.4 基于 iOS 端源码解析 (一) :探究 RCTBundleURLProvider

ReactNative 0.54.4 基于 iOS 端源码解析(一):探究 RCTBundleURLProvider

最近在做优化相关事宜,需要了解 ReactNative 的原理。由于公司相关版本是 0.54.4 ,所以本源码解析也基于 0.54.4 。由于整个 ReactNative 项目分为两端,整体代码体系较为庞大,因此,本人先从 iOS 端着手进行源码分析。

准备

  1. 安装 Node 环境 (安利下本人编写的 install-node-sh):

    curl -o- https://raw.githubusercontent.com/QC-L/install-node-sh/master/install-node.sh | bash
    
  2. 安装最新的 react-native-cli

    npm install -g react-native-cli
    

    or

    yarn add global react-native-cli
    
  3. 初始化 0.54.4 版本的 ReactNative 项目

    react-native init TestOptimize --version 0.54.4
    
  4. 运行:

    react-native run-ios
    

运行结果如下:

源码走起🏂

做过 iOS 原生开发的童鞋应该都有经验,iOS 项目的代码会从 AppDelegate 开始阅读。

打开 AppDelegate.m 文件,熟悉又陌生的代码映入眼帘:

#import "AppDelegate.h"

#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  NSURL *jsCodeLocation;

  jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
  
  RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                                      moduleName:@"TestOptimize"
                                               initialProperties:nil
                                                   launchOptions:launchOptions];
  rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];

  self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
  UIViewController *rootViewController = [UIViewController new];
  rootViewController.view = rootView;
  self.window.rootViewController = rootViewController;
  [self.window makeKeyAndVisible];
  return YES;
}

@end

简单阅览以上代码我们会提出如下两点疑问:

  • RCTBundleURLProvider 是啥?
  • RCTRootView 又是啥?

接下来带着以上两个疑问,开启我们的寻码之旅:

探究 RCTBundleURLProvider🔍

查看生成的 RCTBundleURLProvider 具体做了什么?

其实如果接触 ReactNative 历史版本的话,会很清楚的知道,其实 RCTBundleURLProvider 生成了一个 jsCodeLocationNSURL 对象。另外从名字上也可以看出,这是一个 jsBundleURL 的 Provider(生成器)。

历史版本的 jsCodeLocation 像下面这样👇:

jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.bundle?platform=ios&dev=true"];

大体了解了 RCTBundleURLProvider 的作用,源码读起来:

NSURL *jsCodeLocation;

jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];

从调用了类方法 sharedSetting 可以猜测该类可能是单例

点击查看该方法,可以核实我们的猜测是正确的:

+ (instancetype)sharedSettings
{
  static RCTBundleURLProvider *sharedInstance;
  static dispatch_once_t once_token;
  dispatch_once(&once_token, ^{
    sharedInstance = [RCTBundleURLProvider new];
  });
  return sharedInstance;
}

获得该类实例后,紧接着调用了 jsBundleURLForBundleRoot:fallbackResource: 实例方法:

/**
 * 根据传入的 bundleRoot,生成 jsBundle 的 URL
 *
 * @param bundleRoot 开启 Sever 服务的 bundle 名 默认传入 index
 * @param resourceName 资源名 默认为 main.jsbundle
 * @return NSURL 对象
 */
 - (NSURL *)jsBundleURLForBundleRoot:(NSString *)bundleRoot fallbackResource:(NSString *)resourceName {
  return [self jsBundleURLForBundleRoot:bundleRoot fallbackResource:resourceName fallbackExtension:nil];
}

看到这里,发现 jsBundleURLForBundleRoot:fallbackResource: 实例方法,内部其实调用了 jsBundleURLForBundleRoot:fallbackResource:fallbackExtension:

/**
 根据传入的 bundleRoot 或 resourceName,生成 jsBundle 的 URL

 @param bundleRoot bundleRoot 开启 Sever 服务的 bundle 名
 @param resourceName 资源名,填本地 jsbundle 的资源名,默认为 main.jsbundle
 @param extension 资源名的后缀
 @return NSURL 对象
 */
- (NSURL *)jsBundleURLForBundleRoot:(NSString *)bundleRoot fallbackResource:(NSString *)resourceName fallbackExtension:(NSString *)extension
{
  // 1. 获取 packagerServerHost
  NSString *packagerServerHost = [self packagerServerHost];
  // 2. 判断 packagerServerHost 是否存在
  if (!packagerServerHost) {
    // 3. 如果 packagerServerHost 不存在, 则根据 resourceName(资源名) 和 extension(后缀) 读取本地文件
    // 并返回 url
    return [self jsBundleURLForFallbackResource:resourceName fallbackExtension:extension];
  } else {
    // 4.如果 packagerServerHost 存在, 根据 packagerServerHost 和 bundleRoot 等生成 URL
    // 备注: 后两个参数为 开发模式是否开启 和 压缩模式是否开启
    // 查看 [self defaults] 可以得知默认情况下 dev 开启, minify 关闭
    return [RCTBundleURLProvider jsBundleURLForBundleRoot:bundleRoot
                                             packagerHost:packagerServerHost
                                                enableDev:[self enableDev]
                                       enableMinification:[self enableMinification]];
  }
}

获取 packagerServerHost,具体实现如下:

- (NSString *)packagerServerHost
{
  // NSUserdefaults 中获取 RCT_jsLocation
  NSString *location = [self jsLocation];
  NSLog(@"------------------%@------------------", location);
  // 默认情况下, location 为 null
  if (location != nil) {
    // 不为空, 返回 location
    return location;
  }
  // 如果开发环境
#if RCT_DEV
  // 获取 package 的 Host
  NSString *host = [self guessPackagerHost];
  NSLog(@"=================%@=================", host);
  // 默认情况下, host 为 localhost
  // 此时, 我添加了 ip.txt 文件, 则 host 为 127.0.0.1
  if (host) {
    return host;
  }
#endif
  return nil;
}

上述代码都很简单,不作过多赘述,我们来看一个小细节。

在实例方法 packagerServerHost 中,有这样一个方法叫 guessPackagerHost
内部读取了 ip.txt 的文件,所以当你想要修改 packager 中的 host 的时候,你可以创建该文件,在文件中填入 host 即可。

同样,如下代码也是只在开发环境下运行:

#if RCT_DEV
- (BOOL)isPackagerRunning:(NSString *)host
{
  NSURL *url = [serverRootWithHost(host) URLByAppendingPathComponent:@"status"];
  NSURLRequest *request = [NSURLRequest requestWithURL:url];
  NSURLResponse *response;
  NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:NULL];
  NSString *status = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
  return [status isEqualToString:@"packager-status:running"];
}

- (NSString *)guessPackagerHost
{
  static NSString *ipGuess;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    // 获取 bundle 中 ip.txt 获取路径
    NSString *ipPath = [[NSBundle mainBundle] pathForResource:@"ip" ofType:@"txt"];
    // 将路径文件转换为字符串
    ipGuess = [[NSString stringWithContentsOfFile:ipPath encoding:NSUTF8StringEncoding error:nil]
               stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]];
    NSLog(@"++++++++++++++++++++++%@++++++++++++++++++++++", ipPath);
    NSLog(@"------------------%@------------------", ipGuess);
  });
  // 如果 ip.txt 存在, 并且有内容, 则展示 ip.txt 中的内容, 否则为 localhost
  NSString *host = ipGuess ?: @"localhost";
  // 判断该 host 是否运行
  if ([self isPackagerRunning:host]) {
    // 有效返回 host
    return host;
  }
  // 以上均未返回, 则返回 nil
  return nil;
}
#endif

获取到 Host 之后,后面的逻辑就很容易猜了:

  // 判断 packagerServerHost 是否存在
  if (!packagerServerHost) {
    // 如果 packagerServerHost 不存在, 则根据 resourceName(资源名) 和 extension(后缀) 读取本地文件
    // 并返回 url
    return [self jsBundleURLForFallbackResource:resourceName fallbackExtension:extension];
  } else {
    // 如果 packagerServerHost 存在, 根据 packagerServerHost 和 bundleRoot 等生成 URL
    // 备注: 后两个参数为 开发模式是否开启 和 压缩模式是否开启
    // 查看 [self defaults] 可以得知默认情况下 dev 开启, minify 关闭
    return [RCTBundleURLProvider jsBundleURLForBundleRoot:bundleRoot
                                             packagerHost:packagerServerHost
                                                enableDev:[self enableDev]
                                       enableMinification:[self enableMinification]];
  }

如果 packagerServerHost 存在,则走 else 中的代码;如果 packagerServerHost 不存在,则会根据 resourceName 和 extension 读取本地文件。resourceName 的默认值为 main.jsbundle

如果 packagerServerHost 不存在时,调用的方法实现如下:

- (NSURL *)jsBundleURLForFallbackResource:(NSString *)resourceName
                        fallbackExtension:(NSString *)extension
{
  // 资源名默认为 main
  resourceName = resourceName ?: @"main";
  // 资源后缀默认为 jsbundle
  extension = extension ?: @"jsbundle";
  // 从主 bundle 获取该资源的 url
  return [[NSBundle mainBundle] URLForResource:resourceName withExtension:extension];
}

else 中调用的方法实现:

+ (NSURL *)jsBundleURLForBundleRoot:(NSString *)bundleRoot
                       packagerHost:(NSString *)packagerHost
                          enableDev:(BOOL)enableDev
                 enableMinification:(BOOL)enableMinification
{
  // 根据你起的 bundleRoot 生成路径
  // 默认传入的为 index ,则 path 为 index.bundle
  NSString *path = [NSString stringWithFormat:@"/%@.bundle", bundleRoot];
  // When we support only iOS 8 and above, use queryItems for a better API.
  // 如果默认所有参数都开启, 则最终 query 为 platform=ios&dev=true&minify=false
  NSString *query = [NSString stringWithFormat:@"platform=ios&dev=%@&minify=%@",
                      enableDev ? @"true" : @"false",
                      enableMinification ? @"true": @"false"];
  // 17. 根据 path、packagerHost 及 query 生成 URL
  return [[self class] resourceURLForResourcePath:path packagerHost:packagerHost query:query];
}

PS:其中初始化时,会执行 [self defaults] 产生默认值。其中 dev 为 true,minify 为 false 。

- (NSDictionary *)defaults {
  return @{
    kRCTEnableLiveReloadKey: @NO,
    kRCTEnableDevKey: @YES,
    kRCTEnableMinificationKey: @NO,
  };
}

无论如何,最终都会返回一个 NSURL 对象给 AppDelegate,至此我们对 RCTBundleURLProvider 有了一个基本了解。

下一篇文章,我们将对 RCTRootView 进行源码解析。

[译] webpack 更新日志(一)

webpack 团队于北京时间 10 月 14日凌晨发布了 v5.0.0-beta.0 版本,本文译自 webpack/changelog-v5。此部分主要面向非插件开发的 webpack 使用者。

简要说明

此版本重点关注以下内容:

  • 我们尝试通过持久化存储优化构建性能。
  • 我们尝试采用更好的算法与 defalut 来改善长效缓存。
  • 我们尝试通过更好的 Tree Shaking 和代码生成来改善 bundle 的大小。
  • 我们尝试清除内部结构中奇怪的代码,同时在不影响 v4 功能基础上实现了新特性。
  • 我们目前尝试通过引入破坏性更改来为新特性做准备,以便于我们能尽可能长期地使用 v5。

迁移指南

=> 查阅迁移指南 <=

主要更改

移除废弃的代码

v4 中所有废弃的代码均已删除。

迁移:以确保你的 webapck 4 不打印弃用警告。

以下是已删除但在 v4 中没有弃用警告的内容:

  • 现在必须为 IgnorePlugin 和 BannerPlugin 传递一个 options 对象。

自动移除 Node.js Polyfills

早期,webpack 的目的是允许在浏览器中运行大多数 node.js 模块,但是模块整体格局发生了变化,现在许多模块的主要用途是以编写前端为目的。webpack <= 4 附带了许多 Node.js 核心模块的 polyfil,一旦模块中使用了任何核心模块(即 ”crypto“ 模块),这些模块就会被自动启用。

虽然这使得为 Node.js 编写模块变得简单,但它会将超大的 polyfill 添加到 package 中。在许多情况下,这些 polyfill 并非必要。

webpack 5 会停止自动 polyfill 这些核心模块,并专注于与前端兼容的模块。

迁移:

  • 尽可能尝试使用与前端兼容的模块。
  • 可以为 Node.js 核心模块手动添加 polyfill。错误信息将提示如何进行此操作。
  • package 作者:在 package.json 中使用 browser 字段,以使得 package 与前端代码兼容。为 borwser 提供可选的 implementations/dependencies。

反馈:无论是否喜欢上述修改,请都向我们提出反馈。我们并不确定是否会纳入最终版本。

采用新算法生成 chunk ID 以及 module ID

添加了用于长效缓存的新算法。在生产模式下,默认启用这些功能。

chunkIds: "deterministic", moduleIds: "deterministic"

此算法采用确定性的方式将短数字 ID(3 或 4 个字符)分配给 modules 和 chunks。
这是基于 bundle 大小和长效缓存间的折中方案。

迁移:最好使用 chunkIdsmoduleIds 的默认值。你还可以选择使用旧的默认值,chunkIds: "size", modules: "size",这将生成较小的 bundle,但这会使得它们频繁地进行缓存。

以新算法混淆 export 名称

添加了新算法来处理 export 的名称。默认情况下启用。

如果可能,它将以确定性方式破坏 export 的名称。

迁移:不需要进行任何操作。

为 chunk IDs 命名

在开发模式下默认启用,以新的算法为 chunk id 命名,给 chunk(以及文件名)提供易于理解的名称。
module ID 由其相对于 context 的路径决定。
chunk ID 由 chunk 的内容决定。

因此,你不再需要使用 import(/* webpackChunkName: "name" */ "module") 进行调试。
但是,如果你要控制生产环境的文件名,那仍可使用。

可以在生产中使用 chunkIds: "named",但要确保在使用时不会意外地泄露有关模块名称的敏感信息。

迁移:如果你不喜欢在开发中更改文件名,则可以传递 chunkIds: "natural" 以使用旧的数字模式。

JSON 模块

JSON 模块现在符合规范,并会在使用非默认导出时发出警告。

迁移:使用 default export

(自 alpha.16 起)

嵌套 tree-shaking

webpack 现在可以追踪对 exports 嵌套属性的访问。重新导出 namespace 对象,这可以改善 Tree Shaking 操作(未使用 export elimination 和 export mangling)。

// inner.js
export const a = 1;
export const b = 2;

// module.js
import * as inner from "./inner";
export { inner }

// user.js
import * as module from "./module";
console.log(module.inner.a);

在此示例中,可以在生成模式下移除 export b

(从 alpha.15 起)

内部模块(inner-module) tree-shaking

webpack 4 没有分析模块 export 与 import 之间的依赖关系。webpack 5 有一个新的选项 optimization.innerGraph,该选项在生产模式下默认启用,它对模块中的符号进行分析以找出从 export 到 import 的依赖关系。

如下述模块所示:

import { something } from "./something";

function usingSomething() {
  return something;
}

export function test() {
  return usingSomething();
}

内部图算法将确定仅在使用 export 的 test 时使用 something。这样可以将更多 export 标记为未使用,并从 bundle 中删除更多的代码。

如果设置了 "sideEffects": false,则可以省略更多模块。在此示例中,当未使用 export 的 test 时,将忽略 ./something

如需获取有关未使用的 export 的信息,需使用 optimization.unusedExports。如需删除无副作用的模块,需使用 optimization.sideEffects

此方式可以分析以下符号:

  • 函数声明(function declarations)
  • class 声明(class declarations)
  • 带有 export default 或带有变量声明(variable declarations)的
    • 函数表达式(function expressions)
    • class 语句(class expressions)
    • /*#__PURE__*/ 表达式
    • 局部变量(local variables)
    • imported bindings

反馈:如果您发现此分析中缺少某些内容,请反馈 issues,我们考虑将其添加。

此优化也称为深度作用域分析(Deep Scope Analysis)。

(自 alpha.24 起)

编译器空闲并关闭(idle and close)

现在需要再使用编译器(compilers)后将其关闭。编译器具有 enter 和 leave 空闲状态,并具有这些状态的 hook。插件可以使用这些 hook 执行不重要的工作。(即,持久化缓存将延迟存储到磁盘)。在编译器关闭时,所有剩余工作应尽快完成。回调执行时,表明关闭已完成。

插件及其各自的作者应该会期望某些用户可能会忘记关闭编译器。因此,所有工作最终也应该在空闲时完成。当工作完成时,应防止进程退出。

当传递 callback 时,webpack() 实例会自动调用 close

迁移:使用 node.js API 时,请确保在完成后调用 Complier.close

改进代码生成

此版本添加了新的选项 output.ecmaVersion。它允许为 webpack 生成的运行时代码指定最大 EcmaScript 版本。

webpack 4 仅能于生成 ES5 的代码。webpack 5 现支持 ES5 或 ES2015 的代码。

默认配置将生成 ES2015 的代码。如果你需要支持旧版浏览器(例如,IE11),则可以将其降为 output.ecmaVersion: 5

设置为 output.ecmaVersion: 2015 将使用箭头函数生成较短的代码,以及更多符合规范的代码,使用 const 声明(TDZ)作为 export default

(自 alpha.23 起)

生产模式中的默认压缩(default minimizing)也使用 ecmaVersion 选项生成较小的代码。(自 alpha.31 起)

chunk 分割以及 module size

与之前展示单个数值相比,模块现在以更好的方式展示其 size。除此之外,现在也拥有了不同类型的 size。

目前,SplitChunksPlugin 已知道如何处理这些不同的 size,并将它们应用于 minSizemaxSize
默认情况下,仅处理 javascript 的 size,但你可以传递多个参数来管理它们:

minSize: {
	javascript: 30000,
	style: 50000,
}

迁移:检查构建中使用了哪些类型的 size,并在 splitChunks.minSize 和可选的 splitChunks.maxSize 中进行配置。

持久化缓存

目前包含文件系统缓存。它是可选的,可以通过以下配置启用:

cache: {
  // 1. 设置缓存类型为 filesystem
  type: "filesystem",

  buildDependencies: {
    // 2. 将你的配置添加为 buildDependency 以在更改配置时,使得缓存失效。
    config: [__filename]

    // 3. 如果你还有其他需要构建的内容,可以在此处添加它们
    // 请注意,loader 和所有模块中配置中引用的内容会自动添加
  }
}

重要内容

默认情况下,webpack 会假定其所处的 node_modules 目录由包管理器修改。针对 node_modules 目录,将跳过哈希和时间戳处理。出于性能方面考虑,仅使用 package 的名称和版本。symlinks(例如,npm/yarn link)很友好。除非你使用 cache.managedPaths: [] 选项取消此优化,否则请不要直接在 node_modules 中编辑文件。

默认情况下,缓存将分别存储在 node_modules/.cache/webpack 中(当使用 node_modules 时)和 .pnp/.cache/webpack(当使用 Yarn PnP 时,自 alpha.21 起)。你可能永远不必手动删除它。

(自 alpha.20 起)

当使用 Yarn PnP webpack 时,如果 yarn 的缓存不可变(通常不会发生变化)。你可以通过 cache.immutablePaths: [] 退出此优化。

(自 alpha.21 起)

用于 single-file-target 的 chunk 分割

目前,仅允许启动单个文件 target(如 node,WebWorker,electron main)支持在运行时自动加载引导程序所需的相关代码片段。

这允许对带有 chunks: "all" 的 target 使用 splitChunks

值得注意的是,由于 chunk 加载是异步的,因此这也会使初始估算也为异步操作。当使用 output.library 时,这可能会出现问题,因为导出的值的类型目前为 Promise。从 alpha.14 开始,这将不适用于 target: "node",因为 chunk 加载在此 target 下为同步。

(自 alpha.3 起)

更新解析器

enhanced-resolve 已更新至 v5。具体改进如下:

  • 当使用 Yarn PnP 时,解析器将直接处理无需其他插件
  • 此 resolve 可追踪更多的依赖项,例如文件缺失
  • 别名(aliasing)可能包含多种选择
  • 可以设置别名(aliasing)为 false
  • 性能提升

(自 alpha.18 起)

不包含 JS 的 chunk

不包含 JS 代码的 chunk 将不再生成 JS 文件。

(自 alpha.14 起)

实验阶段特性

并非所有特性从开始就文档。在 webpack 4 中,我们添加了实验性功能,并在 changelog 中指出它们是实验性的,但是从配置中并不能很清楚的了解这些功能是实验性的。

在 webpack 5 中,有一个新的 experiments 配置项,允许启用实验性功能。这样可以清楚地了解启用/使用了哪些实验特性。

虽然 webpack 遵循语义版本控制,但是实验性功能将成为例外。它可能包含 webpack 次要版本的破坏性更改。发生这种情况时,我们将在 changelog 中添加清晰的注释。这促使我们可以更快地迭代实验性功能,同时还可以使用我们在主要版本上停留更长时间以获得稳定的功能。

以下实验性功能将随 webpack 5 一同发布:

  • 像 webpack 4 一样对 .mjs 提供支持(experiments.mjs
  • 像 webpack 4 一样对旧版 WebAssembly 提供支持(experiments.syncWebAssembly
  • 根据更新规范 对新版 WebAssembly 提供支持(experiments.asyncWebAssembly
    • 这使得 WebAssembly 模块成为异步模块
  • Top Level Await Stage 3 阶段提案(experiments.topLevelAwait
    • 在顶层使用 await 使模块成为异步模块
  • 使用 import 引入异步模块(experiments.importAsync
  • 使用 import await 引入异步模块(experiments.importAwait
  • asset 模块类似类似于 file-loaderexperiments.asset)(自 alpha.19 起)
  • 导出 bundle 作为模块(experiments.outputModule)(自 alpha.31 起)
    • 这将从 bundle 中移除 IIFE 的包装器,强制执行严格模式,通过 <script type="module"> 进行懒加载,并在 module 模式下将其进行压缩

请注意,这也意味着针对 .mjs 的支持和 WebAssembly 的支持将被默认禁用

(自 alpha.15 起)

Stats

chunk 间关系默认情况下是隐藏的。可以使用 stats.chunkRelations 进行切换。

(自 alpha.1 起)

Stats 现阶段可以区分 filesauxiliaryFiles

(自 alpha.19 起)

默认情况下,Stats 会隐藏模块和 chunk id。可以使用 stats.ids 进行切换。

所有模块的列表均按照到 entrypoint 的距离排序。可以使用 stats.modulesSort 进行切换。

chunk 模块列表和 chunk 根模块列表分别根据模块名进行排序。可以分别使用 stats.chunkModulesSortstats.chunkRootModulesSort 进行更改。

在串联模块中,嵌套模块列表进行拓扑排序。可以通过 stats.nestedModulesSort 进行更改。

chunks 和 assets 会显示 chunk id 的提示。

(自 alpha.31 起)

最低 Node.js 版本

Node.js 的最低支持版本从 6 变更为 8。

迁移:升级到最新的 node.js 可用版本。

配置变更

结构变更

  • 移除 cache: Object:不能设置为内存缓存对象
  • 添加 cache.type:可以设置为 "memory""filesystem"
  • cache.type = "filesystem" 添加新配置项:
    • cache.cacheDirectory
    • cache.name
    • cache.version
    • cache.store
    • cache.loglevel(自 alpha.20 起被移除)
    • cache.hashAlgorithm
    • cache.idleTimeout(自 alpha.8 起)
    • cache.idleTimeoutForIntialStore(自 alpha.8 起)
    • cache.managedPaths(自 alpha.20 起)
    • cache.immutablePaths(自 alpha.21 起)
    • cache.buildDependencies(自 alpha.20 起)
  • 添加 resolve.cache:允许禁用/启用安全 resolve 缓存
  • 移除 resolve.concord
  • 移除用于原生 node.js 模块自动的 polyfill
    • 移除 node.Buffer
    • 移除 node.console
    • 移除 node.process
    • 移除 node.*(node.js 原生模块)
    • 迁移:使用 resolve.aliasProvidePlugin。发生错误会给出提示。
  • output.filename 可以赋值函数(自 alpha.17 起)
  • 添加 output.assetModuleFilename(自 alpha.19 起)
  • resolve.alias 的值可以为数组或 false(自 alpha.18 起)
  • 添加 optimization.chunkIds: "deterministic"
  • 添加 optimization.moduleIds: "deterministic"
  • 添加 optimization.moduleIds: "hashed"
  • 添加 optimization.moduleIds: "total-size"
  • 移除 module 和 chunk id 的相关的弃用选项
    • 移除 optimization.hashedModuleIds
    • 移除 optimization.namedChunksNamedChunksPlugin 与之相同)
    • 移除 optimization.namedModulesNamedModulesPlugin 与之相同)
    • 移除 optimization.occurrenceOrder
    • 迁移:使用 chunkIdsmoduleIds
  • optimization.splitChunks test 不在匹配 chunk 名
    • 迁移:使用 test 函数
      (module, { chunkGraph }) => chunkGraph.getModuleChunks(module).some(chunk => chunk.name === "name")
  • optimization.splitChunks 中添加 minRemainingSize(自 alpha.13 起)
  • optimization.splitChunks filename 可以赋值函数 (since alpha.17)
  • optimization.splitChunks sizes 可以为每个源类型的 size 对象
    • minSize
    • minRemainingSize
    • maxSize
    • maxAsyncSize(自 alpha.13 起)
    • maxInitialSize
  • optimization.splitChunks 中添加 maxAsyncSizemaxInitialSize:允许为初始和异步 chunk 指定不同的最大 size。
  • 移除 optimization.splitChunks 中的 name: true:不再支持自动命名
    • 迁移:使用默认值。chunkIds: "named" 将为你的文件提供有用的名称以便于调试
  • 添加 optimization.splitChunks.cacheGroups[].idHint:将提示如何命名 chunk id
  • 移除 optimization.splitChunks 中的 automaticNamePrefix
    • 迁移:使用 idHint 替代
  • optimization.splitChunks 中的 filename 不再局限于初始 chunk(自 alpha.11 起)
  • 添加 optimization.mangleExports(自 alpha.10 起)
  • 移除 output.devtoolLineToLine
    • 迁移:无替代方式
  • output.hotUpdateChunkFilename: Function 现在被禁止:不会生效。
  • output.hotUpdateMainFilename: Function 现在被禁止:不会生效。
  • module.rules 中的 resolveparser 将以不同的方式合并(对象会进行深度合并,数组将采用 "..." 进行展开以获取之前的值)(自 alpha.13 起)
  • module.rules 中的 queryloaders 已被移除(自 alpha.13 起)
  • 添加 stats.chunkRootModules:展示 chunk 的根模块
  • 添加 stats.orphanModules:展示未触发的模块。
  • 添加 stats.runtime:展示 runtime 模块
  • 添加 stats.chunkRelations:显示 parent/children/sibling chunk(自 alpha.1 起)
  • 添加 stats.preset:选择 preset(自 alpha.1 起)
  • BannerPlugin.banner 签名变更
    • 移除 data.basename
    • 移除 data.query
    • 迁移:从 filename 中进行提取
  • 移除 SourceMapDevToolPlugin 中的 lineToLine
    • 迁移:无替代方式
  • [hash] 不支持完整编译的 hash
    • 迁移:使用 [fullhash] 或采用更好的 hash 选项
  • [modulehash] 被废弃
    • 迁移:使用 [hash] 代替
  • [moduleid] 被废弃
    • 迁移:使用 [id] 代替
  • 移除 [filebase]
    • 迁移:使用 [base] 代替
  • 基于文件模板的新占位符(即 SourceMapDevToolPlugin)
    • [name]
    • [base]
    • [path]
    • [ext]
  • 当向 externals 传递一个函数时,它将具有不同的函数签名 ({ context, request }, callback)
    • 迁移:更改函数签名
  • 添加 experiments(请参阅上述实验部分,自 alpha.19 起)
  • 添加 watchOptions.followSymlinks(自 alpha.19 起)

默认值变更

  • 默认情况下,仅对 node_modules 启用 module.unsafeCache
  • 在生产模式下,optimization.moduleIds 的默认值从 size 替换为 deterministic
  • 在生产模式下,optimization.chunkIds 的默认值从 total-size 替换为 deterministic
  • none 模式下,optimization.nodeEnv 默认为 false
  • optimization.splitChunks 中的 minRemainingSize 默认为 minSize(自 alpha.13 起)
    • 如果剩余部分过小,这将减少创建 chunk 的数量
  • 当使用 cache 时,resolve(Loader).cache 默认为 true
  • resolve(Loader).cacheWithContext 默认为 false
  • node.global 默认为 false(自 alpha.4 起被移除)
  • resolveLoader.extensions 移除 .json(自 alpha.8 起)
  • 当 node-target 时,node.global 中的 node.__filenamenode.__dirname 默认为 false(自 alpha.14 起)

勘误

如对译文有疑问,欢迎评论或点击阅读原文。

关注我们

相关文章会在公众号首发,扫描下方二维码关注我们,我们将提供前端相关最新最优含量的资讯。

React Hook 基础篇之 useState

最近一直在维护 React 中文文档,重点维护了 Hook 相关的部分,因此总结下 Hook 相关的内容以及自己的一些看法。

注:看了很多文章大家都直接使用了英文的 Hooks,这里想纠正一下,建议大家以后直接使用 Hook 即可。

Hook 简介

简单来讲,Hook 的意义:

  1. 在函数组件中可以使用 state 及 React 其他特性的一种方式,旨在复用代码逻辑;
  2. 替换掉 HOC 及 Render Props 带来的冗余代码,以及对组件结构带来的破坏性;
  3. 降低 React 学习成本,去除学习 JavaScript 中 class 带来的困扰,如函数的 this 绑定问题,不稳定语法提案,以及生命周期带来困扰;
  4. 不用再纠结用哪种组件形式,直接使用函数组件即可。

注:

  1. class 组件不会被替换掉,无需担心之前代码需要重写的问题。
  2. Hook 只能放在函数的顶层。

Hook 的简单使用

系统提供的 Hook 有以下几种:

  1. 基础 Hook
    • useEffect 为组件添加 effect 特性
    • useState 为组件添加 State 特性
    • useContext 为组件添加 Context 特性
  2. 额外 Hook
    • useReducer 类似于 Redux
    • useRef
    • useLayoutEffect
    • useImperativeHandle
    • useCallback
    • useMemo
    • useDebugValue

useState()

state 在函数组件中的使用:

const [value, setValue] = useState('');

useEffect()

简易的验证码倒计时的例子:

const [count, setCount] = useState(60);
useEffect(() => {
  let interval = setInterval(() => {
    setCount(count => count - 1);
  }, 1000);
  return () => {
	clearInterval(interval);
  };
}, []);

使用 Hook 编写 TodoList

TodoList 是大家学习新内容的首选项目,话不多说,直接开始。

环境配置

使用 create-react-app 搭建环境,并引入 ant-design

$ create-react-app react-hook-todo && cd react-hook-todo
$ yarn add antd

修改 App.js 的代码:

import React, { Component } from 'react';
import Button from 'antd/lib/button';
import './App.css';

class App extends Component {
  render() {
    return (
      <div className="App">
        <Button type="primary">添加 Todo</Button>
      </div>
    );
  }
}

export default App;

修改 App.css 的样式:

@import '~antd/dist/antd.css';

.App {
  text-align: center;
}

...

开启 Todo 之旅

首先,修改下组件结构,将 App 组件改为函数式,并添加一个 Input 组件:

import React from 'react';
import Button from 'antd/lib/button';
import Input from 'antd/lib/input';
import './App.css';

function App() {
  return (
    <div className="App">
      <div className="Background">
        <Input className="my-input" style={{width: 200, marginRight: 10}} placeholder="请输入 Todo"></Input>
        <Button type="primary">添加 Todo</Button>
      </div>
    </div>
  );
}

export default App;

接下来,当输入框输完内容,点击添加 Todo,会添加一个 Todo 到列表中。因此,我们需要 state 来存储当前输入的 Todo:

import React, { useState } from 'react';

function App() {
  const [todo, setTodo] = useState('');
  return (
    <div className="App">
      <div className="Background">
        <Input value={todo} className="my-input" onChange={(e) => setTodo(e.target.value)} style={{width: 200, marginRight: 10}} placeholder="请输入 Todo"></Input>
        <Button type="primary">添加 Todo</Button>
      </div>
    </div>
  );
}  

然后,需要将每次输入的 Todo 保存在列表中,因此需要再次引入一个 TodoList 的 state。
并且每次输入完成,点击添加后,应该清除之前的 todo 的值:

function App() {
  const [todo, setTodo] = useState('');
  const [todoList, setTodoList] = useState([]);
  return (
    <div className="App">
      <div className="Background">
        <Input value={todo} className="my-input" onChange={(e) => setTodo(e.target.value)} style={{width: 200, marginRight: 10}} placeholder="请输入 Todo"></Input>
        <Button type="primary" onClick={() => setTodoList(todoList => {
          setTodo('');
          return [...todoList, todo];
        })}>添加 Todo</Button>
      </div>
    </div>
  );
}

最后,我们使用一个列表来展示所生成的 TodoList:

创建 TodoList 组件:

import React from 'react';

function TodoList(props) {
  let { list } = props;
  return (
    <div className="todo-list">
      <ul>
        {
          list.map((item, index) => {
            return <li key={index}>{item}</li>
          })
        }
      </ul>
    </div>
  );
}

export default TodoList;

引入 TodoList 组件,并传入我们生成的 todoList 的 state:

import TodoList from './components/TodoList';

function App() {
  const [todo, setTodo] = useState('');
  const [todoList, setTodoList] = useState([]);
  return (
    <div className="App">
      <div className="Background">
        <Input value={todo} className="my-input" onChange={(e) => setTodo(e.target.value)} style={{width: 200, marginRight: 10}} placeholder="请输入 Todo"></Input>
        <Button type="primary" onClick={() => setTodoList(todoList => { 
          setTodo('');
          return [...todoList, todo];
        })}>添加 Todo</Button>
      </div>
      <TodoList list={todoList}></TodoList>
    </div>
  );
}

总结

上述使用了 useState 巧妙的实现了 TodoList 的效果。总的来说,Hook 还是非常便捷的。下一节,我们将继续完善这个 TodoList,加入更多的 Hook。

参考

Hook 简介
Hook 概览
使用 State Hook

前端面试题 (二)

框架部分

Redux

  • Redux 的基本使用
  • Redux 的高阶 - Action的异步
  • Redux 的 Middleware
  • 如何优雅的管理复杂的 store 结构 安全的取值
  • 介绍 React 的生命周期
    • 何时使用 componentWillReceiveProps

Babel

  • 有没有实现过 loader
  • Babel 的实现
    • 抽象语法树的结构
    • 源代码 -> 编译后的代码
    • 编译过程中 如何生成抽象语法树

Webpack

  • 有没有实现过 webpack 的插件和 loader
  • webpack 的原理

Node.js

  • 有没有使用过 Stream
  • Stream 中的 Duplex 流 与 Transform 流
  • Node 子进程的应用

React diff 算法
React 源码
React Concurrent
Linux 系统的问题
操作系统
数据结构
HashMap

[译] webpack 5 之持久化缓存

继 webpack v5-beta0 发布后,官方又发布了持久化缓存指南。

Opt-in

首先,要注意的是默认情况下不会启用持久化缓存。你可以自行选择启用。

为何如此?
webpack 旨在注重构建安全而非性能。
我们没有打算默认启用这一功能,主要原因在于此功能虽然有 95% 几率提升性能,但仍有 5% 的几率中断你的应用程序/工作流/构建。

这可能听起来很糟,但相信我它并非如此。
只不过需要开发人员进行额外的操作来配置它。

序列化与反序列化功能具有无需配置的开箱即用体验,但开箱即用的部分可能致使缓存失效。

什么是缓存失效?
webpack 需要确认 entry 的缓存何时会失效,并在失效时不再将其用于构建。
因此,当你应用程序修改文件时,就会发生此情况。

示例:修改 magic.js
webpack 必须让 entry 为 magic.js 的缓存失效。
构建将重新处理该文件,即运行 babel,typescript 诸如此类工具,重新解析文件并运行代码生成。
webpack 可能还会致使 entry 为 bundle.js 的缓存失效。
然后根据原模块重新构建此文件。

为此,webpack 追踪了每个模块的 fileDependencies contextDependencies 以及 missingDependencies,并创建了文件系统快照。
此快照会与真实文件系统进行比较,当检测到差异时,将触发对应模块的重新构建。

webpack 给 bundle.js 的缓存 entry 设置了一个 etag,它为所有贡献者的 hash 值。
比较这个 etag,只有当它与缓存 entry 匹配时才能使用。

webpack 4 中的内存缓存也依赖上述这些。
从开发人员角度来说,这些都能够开箱即用,无需额外配置。
但对于 webpack 5 的持久化缓存来说,却充满着挑战。

以下操作均会让 webpack 使 entry 缓存失效:

  • 当 npm 升级 loader 或 plugin 时
  • 当更改配置时
  • 当更改在配置中读取的文件时
  • 当 npm 升级配置中使用的 dependencies 时
  • 当不同命令行参数传递给 build 脚本时
  • 当有自定义构建脚本并进行更改时

这变得非常棘手。
开箱即用的情况下,webpack 无法处理所有这些情况。
这就是我们为什么选择安全的方式,并将持久化缓存变为可选特性的原因。
我们希望读者可以学习如何启用持久化缓存,以为你提供正确的提示。
我们希望你知道需要使用哪种配置来处理你自定义的构建脚本。

构建依赖(dependencies),缓存版本(version)和缓存名(name)

为了处理构建过程中的依赖关系,webpack 提供了三个新工具:

构建依赖(Build dependencies)

此为全新的配置项 cache.buildDependencies,它可以指定构建过程中的代码依赖。
为了使它更简易,webpack 负责解析并遵循配置值的依赖。

值类型有两种:文件和目录。
目录类型必须以斜杠(/)结尾。其他所有内容都解析为文件类型。

对于目录类型来说,会解析其最近的 package.json 中的 dependencies。
对于文件类型来说,我们将查看 node.js 模块缓存以寻找其依赖。

示例:构建通常取决于 webpack 本身的 lib 文件夹:
你可以这样配置:

cache.buildDependencies: {
    defaultWebpack: ["webpack/lib/"]
}

webpack/lib 或 webpack 依赖的库(如,watchpackenhanced-resolved 等)发生任何变化时,其缓存将失效。
webpack/lib 已是默认值,默认情况下无需配置。

另一个示例:构建依旧取决于你的配置文件。
具体配置如下:

cache.buildDependencies: {
    config: [__filename]
}

__filename 变量指向 node.js 中的当前文件。

当配置文件或配置文件中通过 require 依赖的任何内容发生更改时,也会使得持久化缓存失效。
当配置文件通过 require() 引用了所有使用过的插件时,它们也会成为构建依赖项。

如果配置文件通过 fs.readFile 读取文件,则将不会成为构建依赖项,因为 webpack 仅遵循 require()
你需要手动将此类文件添加到 buildDependencies 中。

缓存版本(Version)

构建的某些依赖项不能单纯的依靠对文件的引用,如,从数据库读取的值,环境变量或命令行上传递的值。
对于这些值,我们给出了新的配置项 cache.version

cache.version 类型为 string。传递不同的字符串将使持久化缓存失效。

示例:你的配置中可能会读取环境变量中的 GIT_REV 并将其与 DefinePlugin 一起使用以将其嵌入到 bundle 中。
这使得 GIT_REV 成为你构建的依赖项。
具体配置如下:

cache: {
    version: `${process.env.GIT_REV}`
}

缓存名(Name)

在某些情况下,依赖关系会在多个不同的值间切换,并且对于每个值更改都会使得持久化缓存失效,这显然是浪费资源的。
对于这类值,我们给出了新的配置项 cache.name

cache.name 类型为 string。传递值将创建一个隔离且独立的持久化缓存。

cache.name 被用于对文件名进行持久化缓存。确保仅传递短小且 fs-safe 的名称。

示例:你的配置可以使用 --env.target mobile|desktop 参数为移动端或 PC 用户创建不同的构建。
具体配置如下:

cache: {
    name: `${env.target}`
}

性能优化

对大部分 node_modules 进行哈希处理并加盖时间戳以生存构建和常规依赖项,其代价非常昂贵,并且还会大大降低 webpack 的执行速度。
为避免这种情况出现,webpack 引入了相关的性能优化,默认情况下会跳过 node_modules,并使用 package.json 中的 versionname 作为数据源。

此优化将用于配置项 cache.managedPaths 中的所有 path。
它默认为 webpack 安装了 node_modules 目录。

启用此优化后,请勿手动编辑 node_modules
你可以使用 cache.managedPaths: [] 禁用它。

当使用 Yarn PnP 时,将启用另一个优化。
由于缓存内容不可变,yarn 缓存中的所有文件都将完全跳过哈希和时间戳的操作(甚至不会追踪 versionname)。

此操作由配置项 cache.immutablePaths 控制。
启用 Yarn PnP 时,默认为安装了 webpack 的 yarn 缓存。

不要手动编辑 yarn 缓存,因为这根本不可行。

使用持久化缓存

确保你已阅读并理解以上信息!

此为启用持久化缓存的典型配置:

cache: {
    type: "filesystem",
    buildDependencies: {
        config: [ __filename ] // 当你 CLI 自动添加它时,你可以忽略它
    }
}

Watching

持久化缓存可用于单独构建和连续构建(watch)。

当设置 cache.type: "filesystem" 时,webpack 会在内部以分层方式启用文件系统缓存和内存缓存。
从缓存读取时,会先查看内存缓存,如果内存缓存未找到,则降级到文件系统缓存。
写入缓存将同时写入内存缓存和文件系统缓存。

文件系统缓存不会直接将对磁盘写入的请求进行序列化。它将等到编译过程完成且编译器处于空闲状态才会执行。
如此处理的原因是序列化和磁盘写入会占用资源,并且我们不想额外延迟编译过程。

针对单一构建,其工作流为:

  • Loading cache
  • Building
  • Emitting
  • Display results (stats)
  • Persisting cache (if changed)
  • Process exits

针对连续构建(watch),其工作流为:

  • Loading cache
  • Building
  • Emitting
  • Display results (stats)
  • Attach filesystem watchers
  • Wait cache.idleTimeoutForInitialStore
  • Persisting cache (if changed)
  • On change:
    • Building
    • Emitting
    • Display results (stats)
    • Wait cache.idleTimeout
    • Persisting cache (if changed)

你会发现两个新的配置项 cache.idleTimeoutcache.idleTimeoutForInitialStore,它们控制着持久化缓存之前编译器必须空闲的时长。
cache.idleTimeout 默认为 60s,cache.idleTimeoutForInitialStore 默认为 0s。
由于序列化阻止了事件循环,因此在序列化缓存时不进行缓存检测。
此延迟尝试避免由于快速编辑文件,而在 watch 模式下导致重新编译造成的延迟,同时尝试为下一次冷启动保持持久化缓存的最新状态。
这是一个折中的解决方案,可以设置适合你工作流的值。较小的值会缩短冷启动时间,但会增加延迟重新构建的风险。

错误处理

发生错误要恢复持久化缓存的方式,可以通过删除整个缓存并进行全新的构建,或者通过删除有问题的缓存 entry 并使得该项目保持未缓存状态来进行。

在这种情况下,webpack 的 logger 会发出警告。
欲了解更多,请参阅 infrastructureLogging 的配置项。


Details

正常使用不需要以下信息。

使用 webpack 的高级工具指南

封装 webpack 的工具可以选择其他默认值。
当不允许使用自定义扩展的 webpack 时,由于可以完全控制所有构建的依赖项,因此可以默认打开持久化存储。

CLI 指南

默认情况下,使用 webpack 的 CLI 可能会添加一些构建依赖关系,而 webpack 本身不会。

  • 默认情况下,CLI 会将 cache.buildDependencies.defaultConfig 设置为所用的配置文件
  • CLI 会将命令行参数附加到 cache.version
  • 使用命令行参数时,CLI 可能会在 cache.name 中添加注释。

调试信息

使用如下配置,将输出额外的调试信息:

infrastructureLogging: {
    debug: /webpack\.cache/
}

内部工作流

  • webpack 读取缓存文件。
    • 没有缓存文件 -> 未构建缓存
    • 缓存文件中的 versioncache.version 不匹配 -> 没有构建缓存
  • webpack 将解析快照(resolve snapshot)与文件系统进行对比
    • 匹配到 -> 继续后续流程
    • 没有匹配到:
      • 再次解析所有解析结果(resolve results
        • 没有匹配到 -> 未构建缓存
        • 匹配到 -> 继续后续流程
  • webpack 将构建依赖快照(build dependencies snapshot)与文件系统进行对比
    • 没有匹配到 -> 未构建缓存
    • 匹配到 -> 继续后续流程
  • 对缓存 entry 进行反序列化(在构建过程中对较大的缓存 entry 进行延迟反序列化)
  • 构建运行(有缓存或没有缓存)
    • 追踪构建依赖关系
      • 追踪 cache.buildDependencies
      • 追踪已使用的 loader
  • 新的构建依赖关系已解析完成
    • 解析依赖关系已追踪
    • 解析结果已追踪
  • 创建来自所有新解析依赖项的快照
  • 创建来自所有新构建依赖项的快照
  • 持久化缓存文件序列化到磁盘

序列化

所有支持序列化的 class 都需要注册一个序列化器:

webpack.util.serialization.register(Constructor, request, name, serializer);

Constructor 应为一个 class 或构造器函数。
对于任何需要序列化的对象的 object.constructor 将被用于查找序列化器(serializer)。

request 将被用于加载调用 register 模块。
它应指向当前模块。
它将以这种方式使用:require(request)

name 被用于区分具有相同 request 的多个 register 调用。

serializer 是至少拥有 serializedeserialize 两个方法的对象。

当需序列化对象时,请调用 serializer.serialize(object, context)
context 是至少拥有一个 write(anything) 方法的对象
此方法将内容写入输出流。
传递的值也会被序列化。

当需要反序列化对象时,请调用 serializer.deserialize(context)
context 是至少拥有一个 read(): anything 方法的对象。
此方法会反序列化输入流中的某些内容。
deserialize 必须返回反序列化后的对象。

serializedeserialize 应以相同的顺序读取和写入相同的对象。

示例:

// some-module/lib/MyClass.js
class MyClass {
    constructor(a, b) {
        this.a = a;
        this.b = b;
        this.c = undefined;
    }
}

register(MyClass, "some-module/lib/MyClass", null, {
    seralize(obj, { write }) {
        write(obj.a);
        write(obj.b);
        write(obj.c);
    }
    deserialize({ read }) {
        const obj = new MyClass(read(), read());
        obj.c = read();
        return obj;
    }
});

基本数据类型和引用数据类型的序列化器都已被注册,即 string,number,Array,Set,Map,RegExp,plain objects,Error。

勘误

如对译文有疑问,欢迎评论。

关注我们

相关文章会在公众号首发,扫描下方二维码关注我们,我们将提供前端相关最新最优含量的资讯。

webpack 文档更新日志(6.16-7.10)

伴随着 webpack 5 beta 版本特性的更新,webpack 的文档也在升级中。这期间文档内容、loader 和 plugin 都在发生着变化。为了让国内的前端开发能够及时看到文档中的变化,我们(印记中文团队)决定每半个月向大家同步一篇 webpack 文档的更新日志(包括中文站)。

更新日志分为英文篇和中文篇:

  1. 英文篇会主要介绍 webpack 文档的更新部分;
  2. 中文篇则会介绍翻译工作的最新进展。

英文篇

站点更新

ps: cypress 是基于 Mocha API 的基础上开发的一套开箱即用的 E2E 测试框架,并不依赖前端框架,也无需其他测试工具库,配置简单,并且提供了强大的 GUI 图形工具,可以自动截图录屏,实现时空旅行并在测试流程中 Debug 等等。

内容更新

loader

  • 删除了 bundle-loader 文档
  • 删除了 transform-loader 文档

plugin

  • 新增了 NoEmitOnErrorsPlugin 文档

概念(concepts)

  • 更新了 Module Federation 部分的文档

配置(Configuration)

  • 更新了 output 文档
    • 新增 ouput.scriptType 选项
  • 更新了 resolve 文档
    • 新增 resolve.restrictions 选项

参考资料

中文篇

中文站点有大概长达一年多的时间没有进行维护,最近我们重启了中文站点的翻译工作,对翻译工作感兴趣的戳这里。

同步

为了实现同步,我编写了两个小工具:

  1. 自动与英文站进行同步
  2. 自动下载最新的 loader

以上两个操作都会结合自动合并到中文站,如果产生冲突会自动发起 Pull Request。

再结合 Github Action 就可以实现自动同步。

翻译进展

指南及配置

lcxfs1991dear-lizhihua 以及 QC-L 共同完成。

这两部分内容,已基本完成。

API

文章 译者 校对者 状态
Plugin API chenzesam QC-L 已完成
Logger 接口 wangjq4214 QC-L 已完成
Compilation Object jacob-lcs QC-L 已完成
API 首页 Xeonice QC-L 已完成
Module 变量 carlz812 QC-L/lcxfs1991 已完成
Module 方法 kingzez QC-L 已完成
HMR Geekhyt QC-L 已完成
解析器 pampang QC-L 已完成
Node ssszp QC-L 已完成
CLI Fonkie QC-L 校对修改中
Loader 接口 Aastasia QC-L 校对修改中
Stats 数据 - - 未认领

配置

文章 译者 校对者 状态
配置首页 dear-lizhihua 翻译中
Output navigatorOpera QC-L 已完成
Mode aa875982361 QC-L 已完成
入口及上下文 jungor QC-L 已完成
配置类型 doflamin QC-L 校对修改中
配置语言 NealST QC-L 已完成
DevServer jerexyz QC-L 已完成
DevTool weiyuan0609 QC-L 已完成

迁移

文章 译者 校对者 状态
从 v3 升级到 v4 Catherine001li QC-L/lcxfs1991 已完成
从 v1 升级到 v2 或 v3 miazhuce QC-L 已完成

Loader 及 Plugin

由于 loader 和 plugin 部分内容较多,这里只展示已完成的内容。

Loader

文章 译者 校对者
work-loader jacob-lcs QC-L
babel-loader flytam QC-L
bundle-loader mercurywang QC-L/lcxfs1991
cache-loader mercurywang QC-L
coffee-loader mercurywang QC-L
eslint-loader Xeonice QC-L
less-loader phobal QC-L
source-map-loader mercurywang QC-L

由于 bundle-loader 已弃用,此文档将于近期移除。为了表示对译者尊重,所以这里做了展示。

Plugin

文章 译者 校对者
dll-plugin weiyuan0609 QC-L
hot-module-replacement-plugin navigatorOpera QC-L
i18n-webpack-plugin NealST QC-L
uglifyjs-webpack-plugin jefferyE QC-L

其它

文章 译者 校对者 状态
品牌 helianthuswhite QC-L/lcxfs1991 已完成
对比 mercurywang QC-L 已完成
词汇表 daxiaoxiaodejia QC-L/lcxfs1991 已完成
许可证 GSZS QC-L 已完成

以上为半月以来 webpack 中文文档的更新内容。

感谢所有译者的参与与支持,同时希望更多的开发者参与到社区的维护中来。

MacPorts 安装被墙,无法强制关闭

现在不能重新启动电脑,因为正在安装软件。
正在安装 "MacPorts"。中断安装可能会损坏电脑。您可以在安装完成后重新启动。

解决方案:

$ ps auwx | grep macports
$ sudo kill <process-number>

CentOS 6.5 下 yum 安装 MySQL

CentOS 6.5 下 yum 安装 MySQL

MySQL 在 Windows 及 Mac 下安装直接下载安装包进行安装即可。但在 CentOS 下安装,则需要通过命令进行。

检测安装的历史版本

由于系统之前可能安装过历史版本的 MySQL 及其依赖,因此,检查下,将历史版本的 MySQL 及其依赖移除。

yum list installed | grep mysql

查询结果,如下图:

移除上图中所有安装过的内容:

yum -y remove mysql-community-client.x86_64 mysql-community-common.x86_64 mysql-community-libs.x86_64 mysql-community-release.noarch mysql-community-server.x86_64

移除过程如下图:

再次检查,yum 中并不包含 MySQL 。

yum 安装 MySQL

  1. 下载

    通过 wget 下载 MySQL 安装包:

    wget dev.mysql.com/get/mysql57-community-release-el6-11.noarch.rpm
    

    下载过程如下图所示:

    注意: 在选择安装包时,要和你自己的 CentOS 系统版本匹配,否则安装时会报错。选择地址 Yum Repository

  2. 安装 rpm 包

    让 yum 获取 mysql-comunity-server

    yum install mysql57-community-release-el6-11.noarch.rpm
    

    安装完成,如下图:


    安装完 rpm 后,会在 /etc/yum.repos.d 文件目录下生成安装文件,查看对应安装文件

     ls /etc/yum.repos.d | grep mysql
    

    目录下会生成 mysql-community.repomysql-community-source.repo 俩个安装文件。

  3. 安装 mysql

    yum install mysql-community-server
    

    安装过程,如下图:

  4. 启动 mysql 服务

    service mysqld start
    
  5. 修改 root 用户密码

    如果首次安装,会在 log 信息中体现你的 root 密码:

    grep "password" /var/log/mysqld.log
    

    查询结果:

    2017-10-11T03:27:02.210317Z 1 [Note] A temporary password is generated for root@localhost: QBE|>-3Cz-rQ
    

    登录 mysql 并修改密码:

    mysql -u root -p mysql
    

    输入刚刚你得到的密码,然后会登录成功进入

    接着,修改密码即可

    set password='123456';
    

    此时, 会出现警告, 你当前密码过于简单, 无法生效

    ERROR 1819 (HY000): Your password does not satisfy the current policy requirements
    

    因为, MySQL 5.7 版本以后会验证密码安全程度, 分为3个等级分别是 012。其中 2 安全程度最高, 因此需要密码设的复杂一些。这里, 我们修改为安全程度最低的 0 。设置方式如下:

    set global validate_password_policy=0;
    

    设置为 0 后, 密码验证则只会验证密码长度, 验证长度默认为 8, 我们还可以通过命令将密码长度降低为 6 :

    set global validate_password_length=6;
    

    此时则可以设置密码为 123456 :

    set password='123456';
    

    最后,刷新权限

    flush privileges;
    

参考网址:
https://www.cnblogs.com/ivictor/p/5142809.html

ReactiveCocoa(一) 环境集成(含Xcode8)

众所周知, ReactiveCocoa 是由 Github 工程师主导设计的一款 FRP 应用框架, 关于 RACFRP 在这里不再多作赘述。今天主要来介绍下RAC的引入。

一、引入方式

  • 手动引入
  • CocoaPods
  • Carthage

二、引入前准备

本教程使用 Xcode 8Xcode 7.3.1 同时讲解(因为Xcode8已经发布GM版本, 因此主要讲解Xcode8的适配工作)

新建工程 起名 RACImport
新建工程

三、手动引入

在手动引入之前, 我们先来看看官方给出的引入流程。

RAC官方导入流程

1.添加 ReactiveCocoa 的源到你的工程中, 需要使用到 Git 子模块

这里说到了添加 Git 子模块, 关于添加 Git 以及添加 Git 子模块在这都不作不详细赘述。
首先在工程目录中, 添加一个 Git 仓库:
进入到工程目录中
对应到终端
终端命令进入文件夹

调用git初始化命令

git init

显示初始化成功如下图

初始化

然后调用命令

git submodule add https://github.com/ReactiveCocoa/ReactiveCocoa.git external/ReactiveCocoa

开始下载 Git 子模块
下载子模块

下载完毕后, 提示如下:
子模块下载完毕

此时, 官方给出的导入步骤中的第一步完成

2.运行子模块更新命令

执行以下代码, 更新子模块

git submodule update --init --recursive

运行结果如下:

运行命令后

其中更新完成后, 会多出如下几个依赖库:

依赖库

注: 最新版本中添加了, ReactiveSwift , 并且支持 Xcode 8

Xcode 8

注: 不需做任何操作

Xcode 7.3.1

如果是 Xcode 7.3.1 需要注意, 工程中的 ResultReactiveSwift 都是基于 Swift3 的。语法会有很大变化
因此, 需要从 Git 分支中, Checkout 出旧 tag 版本的 ReactiveCocoa
查看 ReactiveCocoa Git仓库tag 找到 v4.2.2。

进入到 ReactiveCocoa 的目录

进入目录后

git checkout v4.2.1

然后查看工程目录中的 Cartfile 文件, 文件内容如下:

github "antitypical/Result" ~> 2.1.3

接下来执行

carthage update

执行过程如下图

执行完成后, 此步骤完成

3.拖拽 .xcodeproj 文件到你的工程中

Xcode 8

官方给出的文档中, 已经说明需要将 ReactiveCocoa.xcodeproj, Carthage/Checkouts/ReactiveSwift/ReactiveSwift.xcodeproj, and Carthage/Checkouts/Result/Result.xcodeproj 拖拽到你自己的工程中。 这三个文件相互依赖, 因此缺一不可。

在工程中创建一个 Group 起名 Frameworks

导入ReactiveCocoa.xcodeproj

导入ReactiveCocoa.xcodeproj

导入ReactiveSwift.xcodeproj

导入ReactiveSwift.xcodeproj

导入Reuslt.xcodeproj

导入Result.xcodeproj

Xcode 7.3.1

官方给出的文档中, 已经说明需要将 ReactiveCocoa.xcodeproj and Carthage/Checkouts/Result/Result.xcodeproj 拖拽到你自己的工程中。 ( Xcode 7.3.1 的版本中没有 ReactiveSwift.xcodeproj )

导入ReactiveCocoa.xcodeproj

导入ReactiveCocoa

导入Reuslt.xcodeproj

导入Result

4.工程目录的 "General" 的 "Embedded Binaries"添加Framework

注:这里因为一般是iOS开发因此举例导入都是iOS的Framework框架

Xcode 8

分别引入 Result.framework, ReactiveCocoa.framework, ReactiveSwift.framework

添加三种Framework

Xcode 7.3.1

分别引入 Result.framework, ReactiveCocoa.framework

添加两种Framework

引入Framework完成后, 编译完成如果没有错误, 该步骤完成。

5.工程不包含Swift代码, 设置EMBEDDED_CONTENT_CONTAINS_SWIFT

设置 Bulid Settings 中的 EMBEDDED_CONTENT_CONTAINS_SWIFTYES

6.验证

上述几步完成后, 引入 ReactiveCocoa 框架

#import <ReactiveCocoa/ReactiveCocoa.h>

编译, 运行

    RACSignal *singal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        [subscriber sendNext:@"暮落晨曦"];
        return nil;
    }];
    [singal subscribeNext:^(id x) {
        NSLog(@"%@", x);
    }];

查看打印结果, 如果是 暮落晨曦 , 证明框架导入成功。

四、CocoaPods引入

使用 CocoaPods 引入时, 其实相对于手动导入就简单很多。

首先, 在引入 ReactiveCocoa 之前, 先来看看 RAC 最低支持的 iOS 版本是多少。
RAC的支持的版本

1.创建并修改Podfile

明确了最低支持的版本, 在需要使用 CocoaPods 的工程中创建 Podfile 文件

pod init

然后使用如下命令打开 Podfile 文件

open -a Xcode Podfile

打开后的界面如下图:

修改后, 如下图:

代码如下:

# Uncomment this line to define a global platform for your project
platform :ios, '8.0'

target 'RACCocoaPods' do
  # Uncomment this line if you're using Swift or would like to use dynamic frameworks
use_frameworks!
pod 'ReactiveCocoa'

  # Pods for RACCocoaPods

end

2.检测 Repo 中的 RAC是否为最新版本

这里会出现问题, 因为 CocoaPodsrepo 会时刻更新, 因此在安装前先来查看下本地的 repo 中的 ReactiveCocoa 是不是最新版本

在终端中使用以下命令

pod search ReactiveCocoa

搜索结果如下:

Repo中的RAC版本

然后打开 ReactiveCocoa Github 传送门, 查看其分支中的 tags 最新的版本为4.2.2, 如下图

通过图片发现, 本地 Repo 中的库不是最新版本, 因此需要更新下本地 Repo

pod repo update

这个过程可能会非常的漫长, 接下来大家喝杯咖啡休息休息。

更新完成后, 再次搜索, 如下图:
ReactiveCocoa最新版本

3.执行 pod init 方法

pod install --verbose --no-repo-update

Xcode 7.3.1

编译运行, 一切正常。

Xcode 8.0

因为 Xcode 8.0 使用了 Swift 3.0 , 因此使用 Xcode 8.0 打开, 会出现以下问题:
Xcode8 错误

解决方案: 如下图, 按步骤进行即可

解决问题 stackoverflow.com 链接

编译运行, 一切正常

4.验证

上述几步完成后, 引入 ReactiveCocoa 框架

#import <ReactiveCocoa/ReactiveCocoa.h>

编译, 运行

    RACSignal *singal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        [subscriber sendNext:@"暮落晨曦"];
        return nil;
    }];
    [singal subscribeNext:^(id x) {
        NSLog(@"%@", x);
    }];

查看打印结果, 如果是 暮落晨曦 , 证明框架导入成功。

五、Carthage 引入

新建工程, 起名 RACCarthage, 在工程目录中进行如下操作

1.创建 Cartfile 文件

touch Cartfile
open Cartfile

Cartfile 中填入如下代码

github "ReactiveCocoa/ReactiveCocoa"

2.执行 carthage update 命令

保存并关闭后, 在终端中执行如下命令:

carthage update

执行完毕, 效果如下:

carthage update执行完毕

3.将 Framework 引入工程

打开工程目录, 发现工程中多出了一个 Carthage 目录, 如下图所示:
Carthage update

找到 Carthage 目录下的 Framework目录, Carthage -> Build -> iOS 找到 ReactiveCocoa.frameworkResult.framework 两个库, 如下图:

Framwork

打开 RACCarthage.xcodeproj 工程文件, 然后将刚刚找到的两个 Framework 文件引入工程, 如下操作:
Framwork

Xcode 8

编译通过, 一切正常

Xcode 7.3.1

编译通过, 一切正常

4.验证

上述几步完成后, 引入 ReactiveCocoa 框架

#import <ReactiveCocoa/ReactiveCocoa.h>

编译, 运行

RACSignal *singal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"暮落晨曦"];
        return nil;
}];
[singal subscribeNext:^(id x) {
        NSLog(@"%@", x);
}];

查看打印结果, 如果是 暮落晨曦 , 证明框架导入成功。

六、总结

至此, 三种引入方式都已介绍完毕。 写这篇博客时, 恰好赶上 Xcode 8 GM 发行, 因此, 在博客中就将 Xcode 8 的集成方式也做了详细描述, 希望能帮到大家。如有疑问或错误, 欢迎评论指出。

补充:
ReactiveCocoa 库已支持 Swift3.0 , 但是还会出现问题,因为 CocoaPods 版本过低。需要将 CocoaPods 升级至 1.1.0 ,即可正常运行。

版权声明: 如需转载, 请说明出处。谢谢!by QC-L

小程序踩坑指北

  1. 微信小程序 scrollview 无法横向滚动时
    解决方案: 子元素设置 display:inline-block;

  2. 微信小程序 scrollview 横向滚动时,子元素错位
    解决方案: 子元素添加 vertical-align: top;

  3. 微信小程序提交审核不要出现朋友圈字样,否则会被拒

  4. 微信小程序登录场景,如有必须登录的页面,可以在必须登录页面的 onLoad 方法调用时,重定向到登录页面
    ps: 如果页面数量很多,则可以写个通用方法,统一处理下需要登录操作的 Page 。

  5. flex 布局换行时,每行元素间会产生的间隙
    解决方案: 父元素添加 align-content: flex-start; ,改变多根轴线时的对齐方式,此方式对一根轴线时无效

  6. 文字基线对齐,使用 flex 布局
    解决方案: align-items: baseline;

【小试牛刀】Stage-2 装饰器初探

目前 JavaScript 的装饰器处于提案 Stage-2 阶段,且尚未稳定。因此与所有提案一样,在未来可能会发生变化。本文为个人的思考和见解,如有异议欢迎拍砖。

装饰器这个概念想必大家并不陌生,笔者最早遇到这个词是在学习 Java 的时候,主要应用是在 Java 的 Spring 中,其中有个概念叫 AOP(面向切面编程)。

当然这个概念在 JavaScript 中也已有其他的应用,比如 TypeScript,MobX 等。社区也有 Redux 相关的解决方案,如 @connect 装饰器的使用。

这里给大家带来的是有关 Babel 7.1.0 中实现的 @babel/babel-plugin-proposal-decorators 相关应用。

环境搭建

由于该提案依赖于 Babel 的提案插件,因此需要搭建一个简易的 Babel 编译环境。

新建目录,初始化 package.json:

$ mkdir test-decorator && cd test-decorator
$ npm init -y

在 package.json 中添加 Babel 相关 package:

yarn add -D @babel/cli @babel/core @babel/preset-env

创建 .babelrc

touch .babelrc

添加如下配置:

{
  "presets": ["@babel/preset-env"]
}

在目录中添加 src/index.js

class TestDecorator {
  method() {}
}

package.json 中添加 build script 命令

{
  "name": "test-decorator",
  "version": "1.0.0",
  "description": "",
  "main": "src/index.js",
  "scripts": {
-    "test": "echo \"Error: no test specified\" && exit 1",
+    "build": "babel src -d dist"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.2.3",
    "@babel/core": "^7.2.2",
    "@babel/preset-env": "^7.3.1"
  }
}

创建 index.html 文件,并引入 dist/index.js

<!doctype html>
<html>
<head>
  <title>测试</title>
</head>
<body>
  <script src="./dist/index.js"></script>
</body>
</html>

执行 yarn build 即可。

装饰器初窥

1.编写一个类装饰器函数

修改 src/index.js:

class TestDecorator {
  method() {}
}
// 类装饰器函数
function decorator(value) {
	return function(classDescriptor) {
       console.log(classDescriptor);
	}
}	

2.使用类装饰器

+ @decorator
class TestDecorator {
  method() {}
}

3.安装 Babel 插件,并修改 .babelrc 文件:

$ yarn add -D @babel/plugin-proposal-decorators
{
   "presets": ["@babel/preset-env"],
+  "plugins": [
+    ["@babel/plugin-proposal-decorators", {
+      "decoratorsBeforeExport": true // 用于标识装饰器所处位置(提案中讨论的点)
+    }]
+  ]
}

注:decoratorsBeforeExport 是必需设置的选项,否则会抛出异常。为 true 时,会修饰在 export class 上方;为 false 时,会修饰在 export class 前。

// decoratorsBeforeExport: false
export @decorator class TestDecorator {}

// decoratorsBeforeExport: true
@decorator
export class TestDecorator {}

如不设置 decoratorsBeforeExport 异常如下:

Error: [BABEL] /test-decorator/src/index.js: The decorators plugin requires a 'decoratorsBeforeExport' option, whose value must be a boolean. If you want to use the legacy decorators semantics, you can set the 'legacy: true' option. (While processing: "/test-decorator/node_modules/@babel/plugin-proposal-decorators/lib/index.js")

4.执行 yarn build,打开 index.html 查看控制台结果。

image

装饰器参数详解

装饰器修饰的位置不同,所得的参数有所不同。并且有参数的装饰器与无参数的装饰器也有所区别。
装饰器可修饰内容如下:

  • class
  • class method
  • class fields

调用装饰器时,参数结构对比如下:

  1. 修饰 class

    与 TypeScript 以前之前的装饰器的区别在于,声明的装饰器方法的参数,为 Descriptor 类型。

    参数 说明
    kind class 标识,用于说明当前修饰的内容
    elements [Descriptor] 描述符组成的数组,描述类中所有的元素

    其中 elements 中对象与 method 和 fields 相同,后面介绍。

  2. 修饰 class method修饰 class fields

    参数 说明 method fields
    kind 标识,用于说明当前修饰的内容 method field
    descriptor 与 Object.defineProperty 中的 descriptor 相同 [object Object] [object Object]
    key 所修饰的 method 或 fileds 的名称。可以是 String,Symbol 或 Private Name method x
    placement 可选的参数为 "static","prototype" 或者 "own" `prototype static

    其中关于 key 的描述,在提案中有这么一段。

    For private fields or accessors, the key will be a Private Name--this is similar to a String or Symbol, except that it is invalid to use with property access [] or with operations such as Object.defineProperty. Instead, it can only be used with decorators.

    可能有些小伙伴未了解过 Object.defineProperty,这里针对 descriptor 介绍下其包含哪些属性:

    参数 说明 默认值
    configurable true 当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除 false
    enumerable false 当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中 false
    value method 该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等) undefined
    writable true 当且仅当该属性的writable为true时,value才能被赋值运算符改变。 false
    get undefined 当且仅当该属性的writable为true时,value才能被赋值运算符改变。 undefined
    set undefined 一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。 undefined

    注意: 如果一个描述符不具有 value, writable, get 和 set 任意一个关键字,那么它将被认为是一个数据描述符。如果一个描述符同时有 (value 或 writable) 和 (get 或 set) 关键字,将会产生一个异常

如想尝试的可以自己打印一下,Demo 地址

示例

@classDecoratorArgs(function () {})
@classDecorator
class TestDecorator {
  @filedsDecoratorArgs('fileds')
  @filedsDecorator
  x = 10 // 注意,如果要在 class 中使用 fileds 需要额外引用 @babel/plugin-proposal-class-properties
  @methodDecoratorArgs(20)
  @methodDecorator
  method() {}
}

// 无参数 class decorator
function classDecorator(classDescriptor) {
  console.log(classDescriptor);
}

// 无参数 fileds decorator
function filedsDecorator(filedsDescriptor) {
  console.log(filedsDescriptor);
}

// 无参数 method decorator
function methodDecorator(elementDescriptor) {
  console.log(elementDescriptor);
}

// 有参数 class decorator
function classDecoratorArgs(args) {
  console.log(args);
  return function(classDescriptor) {
    console.log(classDescriptor);
  }
}

// 有参数 fileds decorator
function filedsDecoratorArgs(args) {
  console.log(args);
  return function(filedsDescriptor) {
    console.log(filedsDescriptor);
  }
}

// 有参数 method decorator
function methodDecoratorArgs(args) {
  console.log(args);
  return function(elementDescriptor) {
    console.log(elementDescriptor);
  }
}

相关内容推荐

装饰器应用:

参考资料:

下一篇带大家动手实现一个装饰器,敬请期待。

AFNetworking 3.0迁移指南

AFNetworking 3.0

AFNetworking是一款在OS X和iOS下都令人喜爱的网络库。为了迎合iOS新版本的升级, AFNetworking在3.0版本中删除了基于 NSURLConnection API的所有支持。如果你的项目以前使用过这些API,建议您立即升级到基于 NSURLSession 的API的AFNetworking的版本。本指南将引导您完成这个过程。

本指南是为了引导使用AFNetworking 2.x升级到最新的版本API,以达到过渡的目的,并且解释了新增和更改的设计结构。

新设备要求: iOS 7, Mac OS X 10.9, watchOS 2, tvOS 9, & Xcode 7

AFNetworking 3.0正式支持的iOS 7, Mac OS X的10.9, watchOS 2 , tvOS 9 和Xcode 7。如果你想使用AFNetworking在针对较旧版本的SDK项目,请检查README的兼容性信息。

NSURLConnection的API已废弃

AFNetworking 1.0建立在NSURLConnection的基础API之上 ,AFNetworking 2.0开始使用NSURLConnection的基础API ,以及较新基于NSURLSession的API的选项。 AFNetworking 3.0现已完全基于NSURLSession的API,这降低了维护的负担,同时支持苹果增强关于NSURLSession提供的任何额外功能。由于Xcode 7中,NSURLConnection的API已经正式被苹果弃用。虽然该API将继续运行,但将没有新功能将被添加,并且苹果已经通知所有基于网络的功能,以充分使NSURLSession向前发展。

AFNetworking 2.X将继续获得关键的隐患和安全补丁,但没有新的功能将被添加。Alamofire(Swift下的网络请求)软件基金会建议,所有的项目迁移到基于NSURLSession的API。

弃用的类

下面的类已从AFNetworking 3.0中废弃:

  • AFURLConnectionOperation
  • AFHTTPRequestOperation
  • AFHTTPRequestOperationManager

修改的类

下面的类包含基于NSURLConnection的API的内部实现。他们已经被使用NSURLSession重构:

  • UIImageView+AFNetworking
  • UIWebView+AFNetworking
  • UIButton+AFNetworking

迁移

AFHTTPRequestOperationManager 核心代码

如果你以前使用 AFHTTPRequestOperationManager , 你将需要迁移去使用 AFHTTPSessionManager。 以下的类在两者过渡间并没有变化:

  • securityPolicy
  • requestSerializer
  • responseSerializer

接下来举一个关于AFHTTPSessionManager的简单例子。注意HTTP网络请求返回的不再是AFHTTPRequestOperation, 修改成为了NSURLSessionTask,并且成功和失败的Block块中的参数也变更为了NSURLSessionTask,而不再是AFHTTPRequestOperation。

AFNetworking 2.x
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager GET:@"请求的url" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
        NSLog(@"成功");
} failure:^(AFHTTPRequestOperation *operation, NSError*error) {
        NSLog(@"失败");
}];
AFNetworking 3.0
AFHTTPSessionManager *session = [AFHTTPSessionManager manager];
[session GET:@"请求的url" parameters:nil success:^(NSURLSessionDataTask *task, id responseObject) {
        NSLog(@"成功");
} failure:^(NSURLSessionDataTask *task, NSError *error) {
        NSLog(@"失败");        
}];

AFHTTPRequestOperation 核心代码

与NSURLConnection对象不同,每个共享应用范围的设置如会话管理、缓存策略、Cookie存储以及URL协议等,这些NSURLSession对象都可以单独进行配置。使用特定的配置来初始化会话,它可以发送任务来获取数据,并上传或下载文件。

在AFNetworking 2.0中,使用AFHTTPRequestOperation,有可能创建一个没有额外开销的独立的网络请求来获取数据。NSURLSession则需要更多的开销,为了获得所要请求的数据。

接下来,将要通过AFHTTPSessionManager创建一个对象,并创建一个任务和启动它。

AFNetworking 2.x
NSURL *URL = [NSURL URLWithString:@""];
NSURLRequest *request = [NSURLRequest requestWithURL:URL];
AFHTTPRequestOperation *op = [[AFHTTPRequestOperation alloc] initWithRequest:request];
op.responseSerializer = [AFJSONResponseSerializer serializer];
[op setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
        NSLog(@"JSON: %@", responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        NSLog(@"Error: %@", error);
}];
[[NSOperationQueue mainQueue] addOperation:op];
AFNetworking 3.0
NSURL *URL = [NSURL URLWithString:@""];
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager GET:URL.absoluteString parameters:nil success:^(NSURLSessionTask *task, id responseObject) {
        NSLog(@"JSON: %@", responseObject);
} failure:^(NSURLSessionTask *operation, NSError *error) {
        NSLog(@"Error: %@", error);
}];

UIKit的迁移

图片下载已经被重构,以遵循AlamofireImage架构与新的AFImageDownloader类。这个类的图片下载职责的代理人是UIButton与UIImageView的类目,并且提供了一些方法,在必要时可以自定义。类别中,下载远程图片的实际方法没有改变。

UIWebView的类目被重构为使用AFHTTPSessionManager作为其网络请求。

UIAlertView的类目被废弃

从AFNetworking 3.0后UIAlertView的类目因过时而被废弃。并没有提供UIAlertController类目的计划,因为这是应用程序应处理的逻辑,而不是这个库。

原文链接: AFNetworking 3.0
纯属个人翻译,如有错误,还请纠正。

从 Vue 源码学习编译及 TypeScript(一) —— parse

最近在学习 TS 和编译相关的,为了加深学习笔者参考了 vue-next 的 complier-core 部分。

准备

目录结构

在开始源码阅读前,需要掌握一些基本信息,如项目依赖,构建方式,配置文件等。

首先,先来了解下整个目录的大体结构:

.
├── __tests__/
├── api-extractor.json
├── dist/
├── index.js
├── node_modules/
├── package.json
└── src/

从以上目录及文件信息,我们可以得知如下信息:

  1. 此 package 依赖了 estree-walkersource-map 以及 babel
  2. package.json 中不包含 scripts 字段,说明由全局统一构建;
  3. 项目构建用到了微软的 @microsoft/api-extractor
  4. 测试框架采用了 Jest。
  5. 此 package 的入口为 index.js,而 index.js 根据环境 NODE_ENV,区分了是否为 production

了解了基本信息之后,我们先来编译一下 complier-core 部分:

yarn build compiler-core -t

编译后,dist 目录内容如下:

├── dist
    ├── compiler-core.cjs.js         # 包含异常的 cjs
    ├── compiler-core.cjs.prod.js    # 用于生产的 cjs
    ├── compiler-core.d.ts           # ts 声明文件
    └── compiler-core.esm-bundler.js # 用于 esm 的构建器

了解了基本的项目结构后,我们再来了解下编译。

编译原理

这里以 Babel 为例,简单介绍下相关的编译原理:

Babel 采用 AST 的形式(Abstract Syntax Tree,抽象语法树)对 JavaScript 源代码进行处理。

具体工作流参照下图:

Babel v7

Babel 中不同的 package 完成不同的工作。

  • @babel/parser 将源码解析生成 AST
    1. 词法分析(Lexical analysis)
    2. 语法分析(Syntax analysis)
    3. 语义分析(Semantic analysis)
  • @babel/traverse 转换修改 AST
  • @babel/generator 根据 AST 生成新的源码,但并不会帮你格式化代码(可以使用 prettier)
  • @babel/core 核心库,很多 Babel 组件依赖,用于加载 preset 和 plugin
  • @babel/types types 包含所有 AST 中使用的类型,便于修改 AST
  • @babel/template 采用 template 的形式简化修改 AST 的过程

ps: 编译器基本原理相似,因此,对比学习的方式最佳。

complier-core 概览

了解了 Babel 的大概原理,那我们再来看看 complier-core/src 中的文件:

└── src
    ├── ast.ts
    ├── codegen.ts
    ├── compile.ts
    ├── errors.ts
    ├── index.ts
    ├── options.ts
    ├── parse.ts
    ├── runtimeHelpers.ts
    ├── transform.ts
    ├── transforms
    │   ├── hoistStatic.ts
    │   ├── noopDirectiveTransform.ts
    │   ├── transformElement.ts
    │   ├── transformExpression.ts
    │   ├── transformSlotOutlet.ts
    │   ├── transformText.ts
    │   ├── vBind.ts
    │   ├── vFor.ts
    │   ├── vIf.ts
    │   ├── vModel.ts
    │   ├── vOn.ts
    │   ├── vOnce.ts
    │   └── vSlot.ts
    └── utils.ts

看完目录,我们就基本能找到对应关系,也基本能了解每个 ts 文件的作用:

  • parse 等价于 @babel/parser
  • transform 等价于 @babel/traverse
  • codegen 等价于 @babel/generator

我们把 Babel 的图替换下,得出下图:

vue-next-complier

这里我们来贴一段源码,大家就可以理解:

function baseCompile(template, options = {}) {
    // ...
    const prefixIdentifiers =  (options.prefixIdentifiers === true || isModuleMode);
    // ...
    const ast = shared.isString(template) ? baseParse(template, options) : template;
    const [nodeTransforms, directiveTransforms] = getBaseTransformPreset(prefixIdentifiers);
    transform(ast, {
        ...options,
        prefixIdentifiers,
        nodeTransforms: [
            ...nodeTransforms,
            ...(options.nodeTransforms || []) // user transforms
        ],
        directiveTransforms: {
            ...directiveTransforms,
            ...(options.directiveTransforms || {}) // user transforms
        }
    });
    return generate(ast, {
        ...options,
        prefixIdentifiers
    });
}

ps: 代码中省略了异常处理部分,只保留了核心代码,便于理解。

从上述代码中,我们可以看出 compiler-core 预留了 options.nodeTransforms,也就意味着 AST 转换部分支持自定义

大致了解了 complier-core 所做的事,那我们使用 complier-core 来编译一段 vue template 的代码。

编译

官方推出了 vue-next-template-explorer 供大家预览,所以这里我们使用此网站进行编译

编译前:

<div v-if="item.isShow" v-for="(item, index) in items">{{item.name}}</div>

编译后:

import { renderList as _renderList, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock, toDisplayString as _toDisplayString, createVNode as _createVNode, createCommentVNode as _createCommentVNode } from "vue"

export function render(_ctx, _cache) {
  return (_ctx.item.isShow)
    ? (_openBlock(true), _createBlock(_Fragment, { key: 0 }, _renderList(_ctx.items, (item, index) => {
        return (_openBlock(), _createBlock("div", null, _toDisplayString(item.name), 1 /* TEXT */))
      }), 256 /* UNKEYED_FRAGMENT */))
    : _createCommentVNode("v-if", true)
}

template 经过 complier-core 编译后,会被转换为 render 函数。

了解了转换结果,我们开始正式的 complier 的学习。

Parse 阶段 —— template -> AST

如图中所示,vue 的 template 模板会被转成 AST,这个过程对应代码中的 parse.ts

接下来会分为两部分去分析 Parse,一是 TypeScript,二则是 Parse 的核心逻辑。

ps: 由于 index.ts 是将所有 ts 文件引入并导出,因此不做过多解释。

1.TypeScript

基础语法请参考 TS 官方文档,这里只讲解一些实用的内容。

先来看这样一个 type:

type MergedParserOptions = Omit<Required<ParserOptions>, OptionalOptions> &
  Pick<ParserOptions, OptionalOptions>
  • Required
  • Pick
  • Omit

Required

/**
 * Make all properties in T required
 */
type Required<T> = {
    [P in keyof T]-?: T[P];
};

其实很好理解,字面意思,就是必须的(必选项)。

其中 -? 为核心操作,将可选变为必选

与之对应的,是 Partial, 将选项变为可选

除此之外,keyof 有必要介绍下。

keyof

keyof 有点像 Object.keys,会取出 interface 中的所有 key,并产生联合类型。

Pick

/**
 * From T, pick a set of properties whose keys are in the union K
 */
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

复杂内容简单化,将 K extends keyof T 单独提取出来。

K extends keyof T 这里的含义是,K 包含在 keyof T 的键联合类型内。

从 T 中取出联合类型 K 的属性,并生成新的 type。

Omit

/**
 * Exclude from T those types that are assignable to U
 */
type Exclude<T, U> = T extends U ? never : T;

/**
 * Construct a type with the properties of T except for those in type K.
 */
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

其中 Exclude 代表移除掉 T 中 U 相关的属性。

Omit 则为移除 T 中联合类型 K 的属性,并生成新的 type。

解释 MergedParserOptions

type OptionalOptions = 'isNativeTag' | 'isBuiltInComponent'
type MergedParserOptions = Omit<Required<ParserOptions>, OptionalOptions> &
  Pick<ParserOptions, OptionalOptions>

其实简单来说,interface ParserOptions 中与联合类型 OptionalOptions 所对应的属性为可选项,而除了联合类型 OptionalOptions 外的属性为必填项。

验证

下方源码中为默认的 parser 选项,除了 isNativeTagisBuiltInComponent 以外,均为默认值。

export const defaultParserOptions: MergedParserOptions = {
  delimiters: [`{{`, `}}`],
  getNamespace: () => Namespaces.HTML,
  getTextMode: () => TextModes.DATA,
  isVoidTag: NO,
  isPreTag: NO,
  isCustomElement: NO,
  decodeEntities: (rawText: string): string =>
    rawText.replace(decodeRE, (_, p1) => decodeMap[p1]),
  onError: defaultOnError
}

以上是 parse.ts 文件中稍微高级一些的 ts 用法,用到了 Utility Types

2.Parse 核心逻辑

在学习核心逻辑之前,我们看看 vue-next 是如何对编译器进行调试的。

本地调试

在文章开始时,我们提到了 vue-next-template-explorer,这个工具除了给大家学习参考外,也是编译器的调试工具。

翻看源码时,发现了 template-explorer 的启动命令

yarn dev-compiler
yarn open

template-explorer

接下来,我们就可以对源代码为所欲为了~

核心逻辑

我们继续沿用,文章开始的例子(因为例子中包含了 if 和 for):

<div v-if="item.isShow" v-for="(item, index) in items">{{item.name}}</div>

我们先找到 Parse 的主函数 baseParse

export function baseParse(
  content: string,
  options: ParserOptions = {}
): RootNode {
  const context = createParserContext(content, options)
  const start = getCursor(context)
  return createRoot(
    parseChildren(context, TextModes.DATA, []),
    getSelection(context, start)
  )
}

我们在最开始已经了解了 template 在 parse 阶段,会被编译成 AST。

由此可以得知,上述代码中 root 为解析后的 AST 对象,其类型为 RootNode。

AST 本质上就是一个 JSON 对象,让我们来看看上述 template 的 AST 的基本结构:

{
  "type": 0,
  "children": [
    {
      "type": 1,
      "ns": 0,
      "tag": "div",
      "tagType": 0,
      "props": [
        {
          "type": 7,
          "name": "if",
          "exp": {
            "type": 4,
            "content": "item.isShow",
            "isStatic": false,
            "isConstant": false,
            "loc": {
              "start": {
                "column": 12,
                "line": 1,
                "offset": 11
              },
              "end": {
                "column": 23,
                "line": 1,
                "offset": 22
              },
              "source": "item.isShow"
            }
          },
          "modifiers": [],
          "loc": {
            "start": {
              "column": 6,
              "line": 1,
              "offset": 5
            },
            "end": {
              "column": 24,
              "line": 1,
              "offset": 23
            },
            "source": "v-if=\"item.isShow\""
          }
        },
        {
          "type": 7,
          "name": "for",
          "exp": {
            "type": 4,
            "content": "(item, index) in items",
            "isStatic": false,
            "isConstant": false,
            "loc": {
              "start": {
                "column": 32,
                "line": 1,
                "offset": 31
              },
              "end": {
                "column": 54,
                "line": 1,
                "offset": 53
              },
              "source": "(item, index) in items"
            }
          },
          "modifiers": [],
          "loc": {
            "start": {
              "column": 25,
              "line": 1,
              "offset": 24
            },
            "end": {
              "column": 55,
              "line": 1,
              "offset": 54
            },
            "source": "v-for=\"(item, index) in items\""
          }
        }
      ],
      "isSelfClosing": false,
      "children": [
        {
          "type": 5,
          "content": {
            "type": 4,
            "isStatic": false,
            "isConstant": false,
            "content": "item.name",
            "loc": {
              "start": {
                "column": 58,
                "line": 1,
                "offset": 57
              },
              "end": {
                "column": 67,
                "line": 1,
                "offset": 66
              },
              "source": "item.name"
            }
          },
          "loc": {
            "start": {
              "column": 56,
              "line": 1,
              "offset": 55
            },
            "end": {
              "column": 69,
              "line": 1,
              "offset": 68
            },
            "source": "{{item.name}}"
          }
        }
      ],
      "loc": {
        "start": {
          "column": 1,
          "line": 1,
          "offset": 0
        },
        "end": {
          "column": 75,
          "line": 1,
          "offset": 74
        },
        "source": "<div v-if=\"item.isShow\" v-for=\"(item, index) in items\">{{item.name}}</div>"
      }
    }
  ],
  "helpers": [],
  "components": [],
  "directives": [],
  "hoists": [],
  "imports": [],
  "cached": 0,
  "temps": 0,
  "loc": {
    "start": {
      "column": 1,
      "line": 1,
      "offset": 0
    },
    "end": {
      "column": 75,
      "line": 1,
      "offset": 74
    },
    "source": "<div v-if=\"item.isShow\" v-for=\"(item, index) in items\">{{item.name}}</div>"
  }
}

baseParse 中调用了 5 个函数:

  • createParserContext
  • getCursor
  • createRoot
  • getSelection
  • parseChildren —— 核心处理逻辑
createParserContext

此函数创建了一个 context,用于关联上下文保存数据。

function createParserContext(
  content: string,
  options: ParserOptions
): ParserContext {
  return {
    options: {
      ...defaultParserOptions,
      ...options
    },
    column: 1,
    line: 1,
    offset: 0,
    originalSource: content,
    source: content,
    inPre: false,
    inVPre: false
  }
}
getCursor

Using cursors, one can search an AST for a selected node and replace, delete, update, or detach it. —— AST_Cursors

cursor 可以理解为对每个节点加了一个下标,此方法用于获取上下文中 cursor 的值。

cusor 由 column、line 以及 offset 组成。

function getCursor(context: ParserContext): Position {
  const { column, line, offset } = context
  return { column, line, offset }
}
createRoot

其含义是,创建 AST JSON 的根。

大家可以理解为每个 template 的根都是一样的。

参数为 childrenloc

export const locStub: SourceLocation = {
  source: '',
  start: { line: 1, column: 1, offset: 0 },
  end: { line: 1, column: 1, offset: 0 }
}

export function createRoot(
  children: TemplateChildNode[],
  loc = locStub
): RootNode {
  return {
    type: NodeTypes.ROOT,
    children,
    helpers: [],
    components: [],
    directives: [],
    hoists: [],
    imports: [],
    cached: 0,
    temps: 0,
    codegenNode: undefined,
    loc
  }
}

getSelection

function getSelection(
  context: ParserContext,
  start: Position,
  end?: Position
): SourceLocation {
  end = end || getCursor(context)
  return {
    start,
    end,
    source: context.originalSource.slice(start.offset, end.offset)
  }
}

parseChildren

此函数为核心处理逻辑。(最重要的放在最后)

大家在大学时,都学过树的遍历方式。

  • 深度优先遍历
  • 广度优先遍历

这里 AST 的本质就是一颗树,因此上述遍历方式均有效。

那如果将 template -> AST,会如何做?

比如,这个例子:

<div>
  <div>
    <span>示例</span>
  </div>
</div>

抛开自动闭合、注释、属性、指令及插值等特性,简化版的 AST 如下:

ps: 上述抛开的特性在源码中均有处理。

{
  type: 0,
  children: [
    {
      tag: 'div',
      children: [
        {
          tag: 'div',
          children: [
            {
              tag: 'span',
              children: [
                {
                  content: '示例'
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}

对源码进行逐行处理,根据 <</ 来判断是节点开始,还是节点结束。

逐行解析。

vue-next 在解析时,处理了几种文本类型:

文本类型 适用
DATA 通用类型
RCDATA <textarea>
RAWTEXT <style>,<script>
CDATA 用于处理 XML 中的 <![CDATA[]]>
ATTRIBUTE_VALUE 属性

以注释代替代码:

function parseChildren(
  context: ParserContext,
  mode: TextModes,
  ancestors: ElementNode[]
): TemplateChildNode[] {
  // while 循环,判断是否结束,以模板最后的结束符为准
  while (!isEnd(context, mode, ancestors)) {
    // 处理插值
    // 处理注释
    // 处理 tag
    //   递归调用 parseChildren
    // 处理 element(自定义组件)
    //   递归调用 parseChildren
    // 处理所有属性
    //   处理指令
  }
}

参考资料

React Hook 学习笔记

React Hook 学习笔记

阅读 React Hook 简介

  1. 可以在 class 以外的地方使用 state 等特性,例如,在函数组件中使用 state。
  2. React Native 将在下一个稳定版支持 Hook
  3. Hook 优势
    • API 完全可选
    • 向后兼容
    • 16.8.0 版本可用
    • 渐进式,可与已有代码协同
  4. 不会因为 Hook 就将 class 组件从 React 中移除。
  5. Hook 会更有助于你理解 React,提供了强大的 API,以便于你更好的组合已知的 React 特性。
  6. Hook 主要解决的问题:
    • 组件间状态难以复用,而使用 HOC 或者 Render Props 对项目侵入性又很大,会有嵌套地狱的问题。
    • 为复杂的组件解耦,减轻组件生命周期的压力。
    • class 是学习 React 的困难之一,还有 JavaScript 中 this 的指向问题。
    • 何时使用函数组件,何时使用 class 组件的分歧

阅读 React Hook 概览

  1. State Hook 的示例。可以在函数组件中使用 state。

     import React, { useState } from 'react';
    
     function Example() {
         // 声明一个叫 "count" 的 state 变量。
         const [count, setCount] = useState(0);
    
         return (
             <div>
                 <p>You clicked {count} times</p>
                 <button onClick={() => setCount(count + 1)}>
                     Click me
                 </button>
             </div>
         );
     }
    
  2. 阅读示例,其中 useState 就是 Hook。参数为 state 的初始值。

  3. 声明多个 state,其中 useState 语法用到了数组解构,这样可以为 state 起名字。

    function ExampleWithManyStates() {
       // 声明多个 state 变量!
       const [age, setAge] = useState(42);
       const [fruit, setFruit] = useState('banana');
       const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
       // ...
    }
    
  4. Effect Hook 的示例。可以在函数组件中增加操作副作用相关的能力。

    import React, { useState, useEffect } from 'react';
    
     function Example() {
         const [count, setCount] = useState(0);
    
         // 相当于 componentDidMount 和 componentDidUpdate:
         useEffect(() => {
             // 使用浏览器的 API 更新页面标题
             document.title = `You clicked ${count} times`;
         });
    
         return (
             <div>
                 <p>You clicked {count} times</p>
                 <button onClick={() => setCount(count + 1)}>
                     Click me
                 </button>
             </div>
         );
     }
    
  5. 调用 useEffect 可以生成副作用函数。

  6. 在函数组件中可以实现副作用函数相关事情,比如异步请求等。

  7. 使用 useEffect 时返回一个函数,可以清除副作用。

     useEffect(() => {
         ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    
         return () => {
             ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
         };
     });
    
  8. 副作用函数 useEffectuseState 相同,都可以多次调用。

  9. Hook 的使用规则

    • 只能应用于函数体最外层(即不能应用于循环,条件及子函数中)
    • 只能在函数组件中使用 Hook
  10. 创建自己的 Hook
    官方给出的自定义 Hook 的例子,这是一段抽离状态逻辑的代码,可以实现在多个函数组件中复用。
    这点在 class 组件中无法做到

    import React, { useState, useEffect } from 'react';
    
    function useFriendStatus(friendID) {
        const [isOnline, setIsOnline] = useState(null);
    
        function handleStatusChange(status) {
            setIsOnline(status.isOnline);
        }
    
        useEffect(() => {
            ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
            return () => {
                ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
            };
        });
    
        return isOnline;
    }
    
  11. 自定义 Hook 其实就是官方 Hook 的组合体。注意:自定义 Hook 请和官方一样使用 use 开头。 useSomething 的形式。

  12. 除了 state 和 effect,官方还有其他的 Hook,如:useContentuseReducer 等。

关于打包 library 时,引入 js 报出 `Cannot assign to read only property 'exports' of object '#<Object>'` 异常的问题

Babel 官方在 webpack issues 给出的解释如下:

Now that Babel 7.x is out, I'll just say that this should essentially be resolved. The only time you should see this, with Babel 7.x, is if you're doing one of:

  1. You've actually using import and module.exports in the same file, which is not allowed by Webpack. You can sidestep this by setting "modules": "commonjs", which will make Babel compile the import to a require. This breaks tree shaking, as mentioned above though, so fixing your code would be a better idea.
  2. You're using useBultins: 'entry'/'usage, or @babel/plugin-transform-runtime, and you are running Babel on CommonJS files (either your own, or in random node_modules, if you're trying to compile that). Babel assumes files are ES modules by default, meaning those transforms would insert import statements into your file, triggering case 1. above. You can avoid this issue by setting sourceType: "unambiguous" in your Babel configuration, which will tell it to guess the type, like Webpack does, instead of assuming all files are modules.

如打包 library,请将 libraryTarget: 'commonjs2' 改为 libraryTarget: 'umd',则可避免此问题。

前端面试题 - 基础篇(HTML + CSS)

HTML

  • HTML5新特性,语义化
    • 新特性:
      1. 语义化特性,添加<header><nav>等标签
      2. 多媒体, 用于媒介回放的 video 和 audio 元素
      3. 图像效果,用于绘画的 canvas 元素,svg元素等
      4. 离线 & 存储,对本地离线存储的更好的支持,local Store,Cookies等
      5. 设备兼容特性 ,HTML5提供了前所未有的数据与应用接入开放接口。使外部应用可以直接与浏览器内部的数据直接相连,
      6. 连接特性,更有效的连接工作效率,使得基于页面的实时聊天,更快速的网页游戏体验,更优化的在线交流得到了实现。HTML5拥有更有效的服务器推送技术,Server-Sent Event和WebSockets就是其中的两个特性,这两个特性能够帮助我们实现服务器将数据“推送”到客户端的功能
      7. 性能与集成特性,HTML5会通过XMLHttpRequest2等技术,帮助您的Web应用和网站在多样化的环境中更快速的工作
    • 新增的标签
      1.多媒体:<audio>, <video>, <source>, <embed>, <track>
      2.新表单元素:<datalist>, <output>, <keygen>
      3.新文档节段和纲要:<header>页面头部、<section>章节、<aside>边栏、<article>文档内容、<footer>页面底部、<section>章节等。
    • 语义化:语义化的HTML就是写出的 HTML 代码符合内容的结构化(内容语义化),选择合适的标签(代码语义化),能够便于开发者阅读和写出更优雅的代码的同时让浏览器的爬虫和机器很好地解析。
  • 浏览器的标准模式和怪异模式
    • 标准模式:浏览器按W3C标准解析执行代码;
    • 怪异模式:使用浏览器自己的方式解析执行代码,因为不同浏览器解析执行的方式不一样,所以称之为怪异模式
  • xhtml和html的区别
    • html: 超文本标记语言。
    • xhtml: 可理解为 xml + html,由于前期 HTML 的规范过于散漫,因此,利用 xml 的规范来要求 html 就产生了 xhtml 。
      • 所有标签都必须小写
      • 标签必须成双成对
      • 标签顺序必须正确
      • 所有属性都必须使用双引号
      • 不允许使用target="_blank"
  • 使用data-的好处
    1.自定义属性,可以被js很好的操作
    2.H5的新属性
    3.通过js的element.dataset.或jQuery的data('')拿到,*可以为url等字符
    4.框架的数据绑定,例如data-ng-if="cs==1"
  • meta标签
    meta 标签是 html 标记语言 head 的一个关键标签,提供文档字符集、使用语言、作者等基本信息,以及对关键词和网页等级的设定等,最大的作用是能够做搜索引擎优化(SEO)。
  • canvas
  • HTML废弃的标签
  • IE6 bug,和一些定位写法
  • css js放置位置和原因
  • 什么是渐进式渲染
  • html模板语言
  • meta viewport原理

CSS

  • 盒模型,box-sizing
  • CSS3新特性,伪类,伪元素,锚伪类
  • CSS实现隐藏页面的方式
  • 如何实现水平居中和垂直居中。
  • 说说position,display
  • 请解释*{box-sizing:border-box;}的作用,并说明使用它的好处
  • 浮动元素引起的问题和解决办法?绝对定位和相对定位,元素浮动后的display值
  • link和@import引入css的区别
  • 解释一下css3的flexbox,以及适用场景
  • inline和inline-block的区别
  • 哪些是块级元素那些是行级元素,各有什么特点
  • grid布局
  • table布局的作用
  • 实现两栏布局有哪些方法?
  • css dpi
  • 你知道attribute和property的区别么
  • css布局问题?css实现三列布局怎么做?如果中间是自适应又怎么做?
  • 流式布局如何实现,响应式布局如何实现
  • 移动端布局方案
  • 实现三栏布局(圣杯布局,双飞翼布局,flex布局)
  • 清除浮动的原理
  • overflow:hidden有什么缺点?
  • padding百分比是相对于父级宽度还是自身的宽度
  • css3动画,transition和animation的区别,animation的属性,加速度,重力的模拟实现
  • CSS 3 如何实现旋转图片(transform: rotate)
  • sass less
  • 对移动端开发了解多少?(响应式设计、Zepto;@media、viewport、JavaScript 正则表达式判断平台。)
  • 什么是bfc,如何创建bfc?解决什么问题?
  • CSS中的长度单位(px,pt,rem,em,ex,vw,vh,vh,vmin,vmax)
  • CSS 选择器的优先级是怎样的?
  • 雪碧图
  • svg
  • 媒体查询的原理是什么?
  • CSS 的加载是异步的吗?表现在什么地方?
  • 常遇到的浏览器兼容性问题有哪些?常用的hack的技巧
  • 外边距合并
  • 解释一下“::before”和“:after”中的双冒号和单冒号的区别

Babel 原理学习笔记

研读了 the-super-tiny-compiler 的代码

整个过程分为 4 个阶段:

1.词法分析,生成词法结构
2.生成 AST
3.转译代码,生成新 AST
4.根据新的 AST 生成新的代码

一个简单的解析代码如下,其中包含每个阶段的输出,以及自己的一些理解注释:

const operationStr = `(add 4 (subtract 5 3))`;
console.log('原代码字符串: ', operationStr);
function tokenizer(input) {
  var current = 0;
  var tokens = [];

  while(current < input.length) {
    var char = input[current];

    // 判断左括号
    if (char === '(') {
      var token = {
        type: 'paren',
        value: char
      }
      tokens.push(token);
      current++;
      continue;
    }
    // 判断右括号
    if (char === ')') {
      var token = {
        type: 'paren',
        value: char
      }
      tokens.push(token);
      current++;
      continue;
    }
    // 判断是否为空格
    var WHITESPACE = /\s/;
    if (WHITESPACE.test(char)) {
      current++;
      continue;
    }
    // 当遇到数字时, 从这里开始
    var NUMBER = /[0-9]/;
    if (NUMBER.test(char)) {
      var value = '';
      while(NUMBER.test(char)) {
        value += char;
        char = input[++current];
      }

      var token = {
        type: 'number',
        value: value
      }
      tokens.push(token);
      continue;
    }

    var LETTERS = /[a-z]/i;
    if (LETTERS.test(char)) {
      var value = '';
      while(LETTERS.test(char)) {
        value += char;
        char = input[++current];
      }
      var token = {
        type: 'name',
        value: value
      }
      tokens.push(token);
      continue;
    }
    throw new Error('I dont know what this character is: ' + char);
  }
  return tokens;
}

// var tokens = tokenizer(operationStr);
// console.log('生成的词法数组为:');
// console.log(tokens);

// 生成 AST
function parser(tokens) {
  let current = 0;
  function walk() {
    let token = tokens[current];
    if (token.type === 'number') {
      current++;
      return {
        type: 'NumberLiteral',
        value: token.value
      }
    }

    if (token.type === 'string') {
      current++;
      return {
        type: 'StringLiteral',
        value: token.value
      }
    }

    if (
      token.type === 'paren' && 
      token.value === '('
    ) {
      token = tokens[++current];

      let node = {
        type: 'CallExpression',
        name: token.value,
        params: []
      }

      token = tokens[++current];
      while(
        (token.type !== 'paren') ||
        (token.type === 'paren' && token.value !== ')')
      ) {
        node.params.push(walk());
        token = tokens[current];
      }

      current++;
      return node;
    }
    throw new TypeError(token.type);
  }

  let ast = {
    type: 'Program',
    body: []
  }

  while (current < tokens.length) {
    ast.body.push(walk());
  }

  return ast;
}

// let ast = parser(tokens);
// console.log('生成的 AST 结构为: ');
// console.dir(ast, {
//   color: true,
//   depth: null
// });

// AST 遍历器
// 访问器模式
function traverser(ast, visitor) {

  function traverseArray(array, parent) {
    array.forEach(function (child) {
      traverseNode(child, parent);
    })
  }

  function traverseNode(node, parent) {
    let method = visitor[node.type];
    if (method && method.enter) {
      method.enter(node, parent);
    }

    switch(node.type) {
      case 'Program':
        traverseArray(node.body, node);
        break;
      case 'CallExpression':
        traverseArray(node.params, node);
        break;
      case 'NumberLiteral':
      case 'StringLiteral': 
        break; 
      default:
        throw new TypeError(node.type);
    }

    if (method && method.exit) {
      method.exit(node, parent);
    }
  }

  traverseNode(ast, null);
}

// 转换器
function transformer(ast) {
  let newAst = {
    type: 'Program',
    body: []
  };

  ast._content = newAst.body;
  traverser(ast, {
    CallExpression: {
      enter(node, parent) {
        let expression = {
          type: 'CallExpression',
          callee: {
            type: 'Identifier',
            name: node.name
          },
          arguments: []
        }

        node._content = expression.arguments;

        if (parent.type !== 'CallExpression') {
          expression = {
            type: 'ExpressionStatement',
            expression: expression
          }
        }

        parent._content.push(expression);
      }
    },
    NumberLiteral: {
      enter(node, parent) {
        parent._content.push({
          type: 'NumberLiteral',
          value: node.value
        })
      }
    },
    StringLiteral: {
      enter(node, parent) {
        parent._content.push({
          type: 'StringLiteral',
          value: node.value
        })
      }
    }
  });

  return newAst;
}

// let newAst = transformer(ast);
// console.log('新生成的 AST 结构为: ');
// console.dir(newAst, {
//   color: true,
//   depth: null
// });


function codeGenerator(node) {
  switch (node.type) {
    case 'Program':
      return node.body.map(codeGenerator).join('\n');
    case 'ExpressionStatement':
      return codeGenerator(node.expression) + ';'
    case 'CallExpression':
      return (
        codeGenerator(node.callee) + '(' + node.arguments.map(codeGenerator).join(', ') + ')'
      );
    case 'Identifier':
      return node.name;
    case 'NumberLiteral':
      return node.value;
    case 'StringLiteral':
      return '"' + node.value + '"';
    default:
      throw new TypeError(node.type);
  }
}

function compiler(input) {
  // 词法分析,生成词法结构
  let tokens = tokenizer(input);
  // 生成 AST
  let ast    = parser(tokens);
  // 转译代码,生成新 AST
  let newAst = transformer(ast);
  // 生成新的代码
  let output = codeGenerator(newAst);
  return output;
}

var output = compiler(operationStr);
console.log('转译后的代码: ', output);

function add(a, b) {
  console.log(a, b);
  return a + b;
}

function subtract(a, b) {
  console.log(a, b);
  return a - b;
}

eval(output);

第十四届 D2 参会总结

今年相比去年较为突出的改变是多了实时翻译环节,感谢宋离同学。

本次的议题围绕微前端和 Serverless 展开,而从个人发展和业务需求的角度,所以选择了以下议题(其他的后面看视频):

  • Let's work together on the future of JavaScript through TC39
  • JS 语言在引擎级别的执行过程
  • 微前端沙盒体系
  • 前端工程下一站:IDE
  • Babel: Under the Hood
  • 基于浏览器的实时构建探索之路
  • 打酱油

废话不多说,开始正文:

Let's work together on the future of JavaScript through TC39 - Daniel Ehrenberg / TC39 核心成员

Daniel Ehrenberg

本次议题主要介绍了 TC39 以及其工作流程,并对提案进行了详细介绍。

ECMA 为相关文档,不推荐从头阅读,选自己感兴趣的部分读一读。

TC39 是 ECMA 的一个工作组,ECMA 是国际标准化组织。TC39 会有定期会议,每两个月会开三天会,会议会讨论语言的更改,寻求共识。(而非投票制)

会议记录

提案共分为 4 个阶段,分别是 Stage-1,Stage-2,Stage-3,Stage-4,提案进入 Stage-4 后,会在下一个 ECMAScript 正式版中发布,如 ES2020 会发布目前处于 Stage-4 阶段的提案

接着,littledan 介绍了一些新提案:

Stage-4

Stage-3

Stage-2

Stage-1

Stage-0

以上为各阶段的部分提案,具体提案可在各提案的文档中找到:

同时还可以在这些文档中还能找到会议有关的记录。

Babel: Under the Hood - Nicolò Ribaudo / Babel 核心贡献者

Nicolò Ribaudo

随着 webpackrollup 等打包工具的普及,现在的前端开发基本上都已离不开 Babel,我接触 Babel 是在 2017 年,那时将 Babel 英文文档翻译为了中文,也参与了新版 Babel 官网(v7)的建设工作。

和 Nicolò 也在 Github 上有过互动,真是年轻有为的开发者。

Nicolò 在此次演讲中主要介绍了 Babel 的原理以及部分使用。基本上将 Babel 一些通用的功能做了介绍,很赞。

Babel 非传统的 Complier(编译器),而是个将 JavaScript 转为 JavaScript 的 Complier。

补充:Babel 将其他语言转为 JS 也可以转译为其他语言(通过编写插件),这个操作 Henry Zhu 在一次会议上做过演示,将 PHP 转成 JS,可以参考 React Rally 上 Henry 的视频 的视频(6 分钟的左右)。
当时为了方便 Babel 官网的读者,我将视频在优酷上同步了一份。

Babel 采用 AST 的形式(Abstract Syntax Tree,抽象语法树)对源代码进行处理。

正常来说,我们在开发时简单语法可以通过正则匹配的形式进行修改。

正则匹配

但是如果遇到复杂的情况,正则就无法胜任了。

正则无法胜任

因此采用 AST。

AST

Babel 中不同的 package 完成不同的工作。

  • @babel/parser 将源码解析生成 AST
    1. 词法分析(Lexical analysis)
    2. 语法分析(Syntax analysis)
    3. 语义分析(Semantic analysis)
  • @babel/traverse 转换修改 AST
  • @babel/generator 根据 AST 生成新的源码,但并不会帮你格式化代码(可以使用 prettier)
  • @babel/core 核心库,很多 Babel 组件依赖,用于加载 preset 和 plugin
  • @babel/types types 包含所有 AST 中使用的类型,便于修改 AST
  • @babel/template 采用 template 的形式简化修改 AST 的过程

Nicolò 对于上述库做了介绍,并说明了使用方式。

各库的具体使用不做过多介绍,这里聊聊 traverse

以代码 n => n * 2 举例,其 AST 结构为:

AST

在使用 @babel/traverse 转换 AST 时,大体如下:

const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;

const jsContent = `n => n * 2`
const ast = parser.parse(jsContent, { /* ... */});

// 采用深度优先遍历(DFS)
traverse(ast, {
  // 进入
  enter(path) {
    console.log(`${path.type} enter`)
    // console.dir(path, { depth: 1 })
  },
  // 退出
  exit(path) {
    console.log(`${path.type} exit`)
    // console.dir(path, { depth: 1 })
  }
})

根据 AST 图示,然后与输出结果对比:

其中每个节点都包含 enterexit

traverse(ast, {
  // 箭头函数
  ArrowFunctionExpression: {
    enter(path) {
      console.log(`${path.type} enter`)
      // console.dir(path, { depth: 1 })
    }
  }
})

如果只需要 enter,则可简化为:

traverse(ast, {
  // 箭头函数
  ArrowFunctionExpression(path) {
    console.log(`${path.type} enter`)
    // console.dir(path, { depth: 1 })
  }
})

最后 Nicolò 讲解了如何编写 babel 插件,插件的编写过程请参考 babel-handbook 插件手册

注意:handbook 是 babel v6 时的教程,但意思大体相同。请参考思路。

前端面试题分析 (一)

前端面试题

1.说一下你了解CSS盒模型

CSS盒模型分为 标准盒模型 和 怪异盒模型

标准盒模型的大小 = (border.left + padding.left + width + border.right + padding.right) * (border.top + border.bottom + height + padding.top + padding.bottom)

标准盒模型占据的空间大小 = (border.left + padding.left + width + border.right + padding.right + margin.left + margin.right) * (border.top + border.bottom + height + padding.top + padding.bottom + margin.top + margin.bottom)

标准盒模型的宽 = 内容的宽

标准盒模型的 box-sizing 为 content-box

怪异盒模型则是设置 box-sizing 为 border-box

怪异盒模型的大小 = width * height

如果在IE6,7,8中 DOCTYPE 缺失会触发怪异模式。

2.position

默认情况下, position 为 static 。top、left、right、bottom会无效。

position 设置为 static,静止,无法移动

position 设置为 relative,相对定位,其他元素不动,自己相对于 参照物 偏移。不脱离文档流。

position 设置为 absolute,绝对定位,脱离文档流,该元素的位置就是以它父元素 position 不为 static 的元素作为参考,如果父元素皆为 static ,则以 body 作为参考。

position 设置为 fixed,固定定位,相对于视口定位

z-index 调整 元素的层级,其中会出现层叠上下文。

top、left、right、bottom、z-index 在 static 下均无效

3.跨域的几种方式

https://juejin.im/post/5a2f92c65188253e2470f16d

jsonp 跨域

前端 url 的 querystring 传递 callback=函数名
后端 取到函数名 调用函数并传参
前端 在 script 中实现对应函数,调用参数即可

postmessage + iframe

网页 B

  window.addEventListener('message', function (ev) {
    alert('data from localhost:63342 ---> ' + ev.data);

    var data = JSON.parse(ev.data);
    console.log(typeof data)
    if (data) {
      data.number = 18;
      window.parent.postMessage(JSON.stringify(data), 'http://localhost:63342/%E9%98%BF%E9%87%8C%E9%9D%A2%E8%AF%95/postmessage/a.html?_ijt=6h2njcp326r5smomgj6jdcufsm')
    }
  }, false)

网页 A

  var myFrame = document.getElementById('myFrame')
  myFrame.onload = function (ev) {
    var data = {
      name: 'liqichang'
    };
    myFrame.contentWindow.postMessage(JSON.stringify(data), 'http://localhost:8080');
  }

  window.addEventListener('message', function(e) {
    alert('data from localhost:8080 ---> ' + e.data);
  }, false);
CORS 跨域

服务端配置

Access-Control-Allow-Origin: * // 接受任意域名的请求
Access-Control-Allow-Credentials: true // 是否允许发送Cookie
Access-Control-Request-Method: GET, POST, PUT, OPTIONS // 设置允许的请求方式
Access-Control-Expose-Headers: // 需要接受的响应头参数名

客户端
简单请求满足以下条件:

  • 请求方式只能为HEAD、POST 或者 GET
  • http头信息不超出一下字段:
    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • DPR
    • Viewport-Width
    • Width
    • Sava-Data
  • Content-Type只包含以下 3 点:
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain

非简单请求:

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。

Websocket 跨域
express 代理跨域

4. JS的数据类型

数据类型详解

基本数据类型: number string boolean undefined null

引用数据类型: Object Array Function Data

5.JavaScript闭包

  1. 闭包就是能够读取其他函数内部变量的函数。
  2. 变量的值始终保持在内存中

1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值

函数的返回值是函数,这就是闭包
Closures (闭包)是使用被作用域封闭的变量,函数,闭包等执行的一个函数的作用域。通常我们用和其相应的函数来指代这些作用域。(可以访问独立数据的函数)

关于 yarn 的更新

官方给出的建议是,不使用 yarn/npm 管理 yarn,因此采用的是 brew 的管理方式。(stackoverflow 的讨论

方式一:

更新 yarn,先移除 yarn,重新下载

brew remove yarn
brew install yarn --ignore-dependencies

按如上操作,会出现以下警告⚠️

Warning: --ignore-dependencies is an unsupported Homebrew developer flag!
Adjust your PATH to put any preferred versions of applications earlier in the
PATH rather than using this unsupported flag!

并且会报出如下错误❌:

Error: An exception occurred within a child process:
  RuntimeError: /usr/local/opt/node not present or broken
Please reinstall node. Sorry :(

其原因是,找不到 yarn 的依赖项(node)。
我们需要给对应的文件夹创建一个 node 的软链:

mkdir /usr/local/opt/node
ln -s ~/.nvm/versions/node/v12.16.1/bin/node /usr/local/opt/node 

再重新执行 install 即可:

brew install yarn

然后就是开心的✅:

==> Downloading https://yarnpkg.com/downloads/1.22.4/yarn-v1.22.4.tar.gz
==> Downloading from https://github-production-release-asset-2e65be.s3.amazonaws.com/49970642/487f6380-61e3-11ea-8294-0d743f488e
######################################################################## 100.0%
🍺  /usr/local/Cellar/yarn/1.22.4: 14 files, 5MB, built in 15 seconds

方式二:

brew upgrade yarn

参考资料:

yarnpkg/website#913
https://classic.yarnpkg.com/en/docs/usage
yarnpkg/yarn#7306

【译】Vue 3 正式进入 RC 阶段!

原文链接:vuejs/rfcs#189

作者:Evan You

译者:QC-L

非常高兴的宣布 Vue 3.0 已经进入候选(Release Candidate)阶段!

进入 RC 阶段,意味着 Vue 3 的核心 API 及实现均已稳定。原则上,我们不希望在正式版发布之前再引入新的特性或重大更新(breaking changes)。许多官方维护的 framework 已基本支持 v3。请参阅此链接了解最新更改。

全新的文档

Vue 文档团队已经将文档更新至 v3,可直接访问 v3.vuejs.org!这是一项艰巨的工作,要感谢文档团队的辛勤付出: @NataliaTepluhina@bencodezen@phanan 以及 @sdras。新文档经过了精心设计,以涵盖 v2 和 v3 之间的差异,可以直接运行在 VuePress 上,并且改进了代码示例,可以内联编辑。

有关新特性和更改的快速概览,请参阅迁移指南

请注意,新文档(尤其是『迁移指南』)仍在开发中,我们将会在整个 RC 阶段继续完善它,

DevTools 初步支持 v3

由于 @Akryum 出色工作,我们还发布了初步支持 v3 的 Vue DevTools 的 Beta 版本。

通过对 devtool 的深度重构,现在已经可能很好地将其核心逻辑与对不同 Vue 版本的支持进行分离。此界面还拥有使用了 Tailwind CSS 实现的新外观。目前,仅支持了组件检查的功能 —— 但很快就会支持其他功能。

Vue DevTools 的 beta 版本仍在 Chrome 的网上应用商店进行审核,你可以通过访问上方链接中的说明在本地下载并进行安装。

更新:DevTools 的审核已经通过,已发布至 Chrome 的网上应用商店

注意:此插件依赖的 Vue 版本是 vue@^3.0.0-rc.1

试用

如果你想试用 Vue 3,可以通过以下几种方式进行:

Codepen 上试用。

使用 Vite 启动一个项目:

npm init vite-app hello-vue3

Vite 在单文件组件(SFC)中默认支持了 <script setup><style vars>

我们有一个进行中的 PR,会在 vue-cli 中针对 v3 的 first-class 进行支持 —— 即将发布。

接下来的工作

目前尚未完成 RC 版本对 IE11 完整支持,因此会继续努力完成。

同时,我们会将工作的重心转向文档,迁移以及兼容性方面。我们目前的目标是为使用 v3 开发新项目提供完善的文档,并帮助相关库的作者升级其 package 以更好地支持 v3。文档团队将根据社区的反馈继续完善迁移指南和 v3 的文档。

将零散的应用程序从 v2 升级到 v3 进展可能会非常缓慢。我们将提供 codemods 和工具来帮助大家进行此类项目的迁移,但是大多数情况下,这取决于项目本身的依赖能以多快的速度升级至 v3。因此,升级前需评估风险和时间成本,再决定是否升级 —— Vue 2 会继续维护。我们计划在 3.0 发布后,设置一个过渡期,以通过兼容性插件将新特性反向移植到 v2 中。我们已经在 @vue/composition-api 中验证了此方法的可行性。

实验阶段特性

RC 版本中提供了一些特性,但已标记为实验性特性:

这些特性已发布,目的是收集大家在实际项目中使用情况的反馈,但是它们可能仍会进行重大更改/调整。并且它们会在 3.0 中保持实验状态,并最终成为 3.1 的一部分。

从0到1搭建React开发环境(一):基础篇

配置

  1. Yarn
  2. webpack
  3. Babel
  4. ES6
  5. React

初始化

创建工作目录, hello-react-webpack:

mkdir hello-react-webpack
cd hello-react-webpack
yarn init

配置基本 webpack

yarn add webpack --dev

创建 webpack.config.js :

touch webpack.config.js

配置基本入口,出口:

const path = require('path');
module.exports = {
  entry: "./src/index.js",
  output: {
    path: path.resolve("dist"), // 根据当前路径生成,dist绝对路径
    filename: "bundle.js"
  }
}

配置 babel v7

yarn add @babel/preset-react @babel/preset-env @babel/core [email protected] --dev

创建 .babelrc 文件,加入如下代码:

{
  "presets": ["@babel/preset-env", "@babel/preset-react"]
}

配置 webpack.config.js :

const path = require('path');
module.exports = {
  entry: "./src/index.js",
  output: {
    path: path.resolve("dist"),
    filename: "bundle.js",
    public: "/"
  },
+  module: {
+    rules: [
+      {
+        test: /\.js$/,
+        use: "babel-loader"
+      }
+    ]
+  }
}

配置 React

下载 reactreact-dom:

yarn add react react-dom

创建 src/index.js

mkdir src
touch src/index.js

index.js 中,编写如下代码:

import React from "react";
import ReactDOM from "react-dom";

ReactDOM.render(
  <h1>测试</h1>,
  document.getElementById("root")
);

配置 html-webpack-plugin

下载插件 html-webpack-plugin:

yarn add html-webpack-plugin --dev

webpack.config.js 中配置 plugins:

const path = require('path');
+ const HTMLWebpackPlugin = require("html-webpack-plugin");

module.exports = {
 /* ... */
+ plugins: [
+   new HTMLWebpackPlugin({
+     template: "index.html",
+     filename: "index.html",
+     inject: "body"
+   })
+ ]
}

在根目录创建 index.html

touch index.html

编写如下内容:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
<div id="root"></div>  
</body>
</html>

配置 webpack-dev-server

安装 wepack-dev-server:

yarn add webpack-dev-server --dev

因为使用 webpack-dev-server 会涉及到热更新,因此需要在 webpack.config.js 中添加插件:

const path = require('path');

const HTMLWebpackPlugin = require("html-webpack-plugin");
+ const webpack = require("webpack");
module.exports = {
 /* ... */
  plugins: [
    new HTMLWebpackPlugin({
      template: "index.html",
      filename: "index.html",
      inject: "body"
    }),
+    new webpack.NamedModulesPlugin(),
+    new webpack.HotModuleReplacementPlugin()
  ]
}

并配置 inlinehot

devServer: {
  inline: true,
  hot: true
}

配置 CSS

安装 style-loadercss-loader :

yarn add style-loader css-loader --dev

webpack.config.js 中的 rules 添加

module: {
  rules: [
      {
        test: /\.js$/,
        use: "babel-loader"
      },
      {
        test: /\.css$/,
        use: [
          "style-loader",
          "css-loader"
        ]
      }
  ]
}

配置 package.jsonscripts

package.json 中添加如下代码:

"scripts": {
  "start": "webpack-dev-server"
},

最后, 执行 start 即可:

yarn start

前端面试题 - 基础篇(JavaScript)

JavaScript

  • js的基本类型有哪些?引用类型有哪些?null和undefined的区别。
  • 如何判断一个变量是Array类型?如何判断一个变量是Number类型?(都不止一种)
  • Object是引用类型嘛?引用类型和基本类型有什么区别?哪个是存在堆哪一个是存在栈上面的?
  • JS常见的dom操作api
  • 解释一下事件冒泡和事件捕获
  • 事件委托(手写例子),事件冒泡和捕获,如何阻止冒泡?如何组织默认事件?
  • 对闭包的理解?什么时候构成闭包?闭包的实现方法?闭包的优缺点?
  • this有哪些使用场景?跟C,Java中的this有什么区别?如何改变this的值?
  • call,apply,bind
  • 显示原型和隐式原型,手绘原型链,原型链是什么?为什么要有原型链
  • 创建对象的多种方式
  • 实现继承的多种方式和优缺点
  • new 一个对象具体做了什么
  • 手写Ajax,XMLHttpRequest
  • 变量提升
  • 举例说明一个匿名函数的典型用例
  • 指出JS的宿主对象和原生对象的区别,为什么扩展JS内置对象不是好的做法?有哪些内置对象和内置函数?
  • attribute和property的区别
  • document load和document DOMContentLoaded两个事件的区别
  • === 和 == , [] === [], undefined === undefined,[] == [], undefined == undefined
  • typeof能够得到哪些值
  • 什么是“use strict”,好处和坏处
  • 函数的作用域是什么?js 的作用域有几种?
  • JS如何实现重载和多态
  • 常用的数组api,字符串api
  • 原生事件绑定(跨浏览器),dom0和dom2的区别?
  • 给定一个元素获取它相对于视图窗口的坐标
  • 如何实现图片滚动懒加载
  • js 的字符串类型有哪些方法? 正则表达式的函数怎么使用?
  • 深拷贝
  • 编写一个通用的事件监听函数
  • web端cookie的设置和获取
  • setTimeout和promise的执行顺序
  • JavaScript 的事件流模型都有什么?
  • navigator对象,location和history
  • js的垃圾回收机制
  • 内存泄漏的原因和场景
  • DOM事件的绑定的几种方式
  • DOM事件中target和currentTarget的区别
  • typeof 和 instanceof 区别,instanceof原理
  • js动画和css3动画比较
  • JavaScript 倒计时(setTimeout)
  • js处理异常
  • js的设计模式知道那些
  • 轮播图的实现,以及轮播图组件开发,轮播10000张图片过程
  • websocket的工作原理和机制。
  • 手指点击可以触控的屏幕时,是什么事件?
  • 什么是函数柯里化?以及说一下JS的API有哪些应用到了函数柯里化的实现?(函数柯里化一些了解,以及在函数式编程的应用,最后说了一下JS中bind函数和数组的reduce方法用到了函数柯里化。)
  • JS代码调试

在iPhone X上构建你的APP

在iPhone X上构建你的APP

文章为 Building Apps for iPhone X Fall 2017 - Session 201 - iOS 的文字实录。以视频中主人公为第一视角,结合本文作者的一些理解的进行了内容讲解。

iPhone X

前言

要适配你的App,只需按照 iOS11 SDK 进行修改,就可以充分利用iPhone X搭载的超视网膜显示屏。

如果你的App主要基于标准的 UIKit 控件,并且使用 AutoLayout 那么接下来的任务就会很轻松了,因为绝大多数工作都由 UIKit 为你代劳了。

如果你使用的是自定义控件没有使用 AutoLayout ,再或者你的 App 是一款自定义全屏的 App 像很多游戏那样,你也不必担心,虽然你确实有些工作要做。但整个适配过程中并没有什么难点,而且我们有很多内建支持工具,比如全新推出的 Safe Area Layout Guide

无论如何,你都需要全面测试你的 App,尤其是在横屏模式下,以确保万无一失。

iPhone X Simulator

最新版 Xcode 包含支持 iPhone X 的 模拟器 让你可以改变绝大多数的布局, 尤其是调整关于 Safe Area 的布局。对于一些 App, 比如使用了 Metal 或是使用了前置摄像头等硬件功能的 App ,最好在实际设备上进行测试。

让我们来看看全新的 iPhone X Simulator:
iPhoneX

同其他 iPhone 或 iPad Simulator 一样,你可以直接使用系统内置 App,这样就可以很好的通过实例观察不同的 UIKit 组件在 iPhone X 上的表现。

比如文件App,就展示了很多最新的 iOS11 API 的实际应用。比如一体式的 SerchBar 和 Large NavigationBar Titles 。

别忘了,你还可以在 Simulator 中登录 iCloud 帐户,并访问你的 iCloud Drive 。这样你就可以方便的将文件或者照片等测试文件传输到 Simulator 中。
iCloud

另外一个不错的例子就是通讯录,它展示了 TableView 如何在 iPhone X 上呈现。一定要将 Simulator 旋转至横向模式。这样就可以看到一些效果,比如 Section Header 横跨屏幕,而 TableViewCell 则遵照 SafeArea 的原则,并保持缩进。稍后还会讲到 TableView 。
Address
接下来我们来看看我负责的项目 WWDC App,我花了一点时间,让它适配 iPhone X,我想分享一下我遇到的有关布局的问题以及我的解决方案。

适配 iPhone X

WWDC是一款真实存在的App,它已经面世了很多年。这些年来,很多工程师都参与了它的编写。它既有很多标准控件和 AutoLayout,也包含自定义View。App 中较老的部分甚至使用了手动布局。我会用这款 App 来强调三处需要针对 iPhone X 进行适配的地方。

首先,我在 Xcode 9 中打开工程文件,将 Base SDK 设为 iOS11,这样就可以以原生分辨率运行 App 了。

当设置你的 App 时,如果你发现 App 没有完整在 iPhone X 下运行,请检查一下你是否配置了 Launch Storyboard,因为这部分设置是必须的。(编辑注: 如果没有使用Launch Storyboard的话)

我们的初始视图是 Videos 标签页,效果如下图看起来还不错。这些全部使用的是今年的新代码,其中使用了遵循 AutoLayout 的 UICollectionView,以及 UINavigationBar 和 UIToolBar 控件等。所以绝大多数界面的布局都没问题,因为 UIKit 为我代劳了大部分工作。

有一个地方没有使用 AutoLayout ,那就是 News 标签页。效果如下图,其实这个 View 看起来还不错,尽管所有 UI 都是手动布局,尽管我们没有直接使用 AutoLayout,负责布局的代码也会注意到 layout margin insets,UIKit 会自动调整布局适应 Safe Area

AutoLayout 适配 Safe Area

我遇到的第一个适配问题就是再 News 标签页中的全屏图片浏览器。尽管这个 View 使用了 AutoLayout ,但其中 PageControl 的位置太靠下了,已经与主屏幕指示器重叠在了一起。

这里的主要问题在于页面空间的底部约束关联的是 SuperView ,也就是 Home 指示器后面的整个屏幕。所以,我们不应该根据父视图进行约束,而应该将 PageControl 按照底部的 Safe Area Layout Guide 进行约束。修改方式如下:

在调整约束前,需要先启用 StoryBoard/Xib 的 Safe Area Layout Guide。Xcode 9 以前的 StoryBoard/Xib 不会自动启用该选项。需要进入 文档检查器 -> Interface Builder Document -> 勾选 Use Safe Area Layout Guides 复选框

注意: iOS StoryBoard 打开 Use Safe Area Layout Guides 功能会自动升级绑定在 top 和 bottom 的 layout guide 约束,leading Edge 以及 trailing Edge。因此,勾选后一定要检查测试所有 AutoLayout 的约束。

Storyboard Safe Area

将如图所示右侧的 Use Safe Area Layout Guide 勾选上。

勾选之后,效果如下:

多出一个叫 Safe Area 的区域,如上图所示。

此时我们来看下 PageControl 的约束,之前约束都是与 SuperView 构建的关系,现在全部变成了 Safe Area。这样就不会遮挡 Home 指示器了。 怎么样很简单吧?

Xib Safe Area

操作步骤与 Storyboard 相同

  1. 先勾选 Use Safe Area Layout Guide
  2. 再修改 PageControl 的 bottom 约束,将 SuperView 改成与 Safe Area

操作步骤如gif图

经过上述调整,我们的 PageControl 就已经不再遮挡 Home 指示器了,并且在横竖屏下均有效。

SearchBar 适配问题

接下来,来看看我遇到的第二个问题。问题出在 Videos 标签页,同样看上去也还不错,但当我调出 SearchBar 时,看起来位置似乎有问题,让我们和通讯录进行一下对比。

SearchBar 的背景颜色似乎不太对,Size 也不太对。如果我旋转到横屏模式,可以看出 SearchBar 和 Cancel 按钮都被屏幕的圆角裁剪掉了一部分。

这个例子说明 Safe Area 的存在显得至关重要,对于这种搜索栏 WWDC 的做法是直接显示了一个 UISearchController ,而在 iOS 11 中 SearchBar 可以集成在 NavigationBar 中,并且给出正确的显示方式。让我们来看下代码如何修改:

这是显示 SearchController 的代码,需要做两处改动。

  1. 将 searchController 赋值给 navigationItem.searchController
  2. 让 searchController 变为活跃状态

注意: 该过程只在 iOS 11 下有效,因此,其他版本保持原有行为。

修改前:

    fileprivate func presentSearchController(initialSearchTeat searchTest: String? = nil) {
        let searchController = UISearchController(searchResultsController: nil)
        searchController.searchResultsUpdater = self as? UISearchResultsUpdating
        searchController.obscuresBackgroundDuringPresentation = false
        searchController.searchBar.text = searchTest
        
        present(searchController, animated: true, completion: nil)
    }

修改后:

    fileprivate func presentSearchController(initialSearchTeat searchTest: String? = nil) {
        let searchController = UISearchController(searchResultsController: nil)
        searchController.searchResultsUpdater = self as? UISearchResultsUpdating
        searchController.obscuresBackgroundDuringPresentation = false
        searchController.searchBar.text = searchTest
        
        if #available(iOS 11.0, *) {
            self.navigationItem.searchController = searchController
            searchController.isActive = true
        } else {
            present(searchController, animated: true, completion: nil)
        }
    }

现在 SearchBar 刚好在 Safe Area 中,并且这全部是 NavigationBar 自动帮我们处理的。如果你的 UI 效果中有 SearchBar 在 navigationBar 上的话,你一定要在 iOS 11 上做类似的处理。

TableView 适配问题

现在我们来看看 App 中的第三处改动,需要改进的地方。

如下是 Schedule 标签页,我们使用了 UITableView ,布局在竖屏模式下看起了不错,但这里搜索栏的样式也不太对。这个搜索栏恰好是作为 Header 视图插入到 TableView 中的。但我们可以像刚才那样改动,也就是让 SearchBar 集成到 NavigationBar 中。

布局切换到横屏模式所有UI看上去都遵循了 Safe Area 布局,但仔细观察 TableView 的 SectionHeader ,它自定义的 BackgroundColor 似乎有问题,颜色应该像通讯录 App 里的 TableView 那样一直延伸到屏幕边缘。

运行下代码会发现,App 将背景颜色设置给了 headerView 的 contentView ,这看上去很合理。

    func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
        let header = view as! UITableViewHeaderFooterView
        let font = UIFont.preferredFont(forTextStyle: .headline)
        
        header.textLabel?.font = font
        
        header.contentView?.backgroundColor = UIColor.lightGray
    }

事实上,在除了 iPhone X 之外的 iPhone 上都没有这个问题。那问题出在了哪里呢?

我们要研究下 TableView 在 iPhone X 上是如何布局 Cell 的。

原理

为了帮助大家理解,我们通过 Xcode 的 View Hierarchy Debugger 进行视图层级的查看。

这是我们刚刚看到的视图,通过 View Hierarchy Debugger 可以调节视图的层级和控制视图的显示/隐藏。

只显示 TableView ,你会发现它的尺寸是整个屏幕。

调节可见范围来显示 TableView 的 Cell 。

你会发现 Cell 是与屏幕一样宽的。

选中其中一个 Cell。

再用 Safe Area 来表示它的位置。

继续调节可见范围,我们可以看到 Cell 的 contentView ,自动布局在了 Safe Area 中。

虽然 Cell 的 Size 与屏幕一样,但 Cell 的 contentView 却和 Safe Area 的 Size 相同。
这样发生了刚刚我们发现的问题。

刚刚我们看到的界面有些混乱,我们进行一些简化并加上一些标记。

默认情况下,TableViewCell 会包含 ContentView,这样就可以将内容适配在 Safe Area 内部。但这种行为是可以由你控制的。

在 Xcode 中你可以勾选 ContentView 的 Insets To Safe Area 选项,代码中也有对应的属性可以设置。如果不勾选或不设置,contentView 就不会适配 Safe Area,而是会与 cell 一样大小。

无论 ContentView 如何设置,它的 Layout Margin 始终默认与 Safe Area 关联。与 ContentView 适配类似,也有一些属性可以让你控制 Layout Margin 。关于这一点以及其他与边距相关的选项你可以查阅文档以及 WWDC 视频。

解决方案

已经知道了原因是 Cell 的 contentView 的 Size 是与 Safe Area 相同的。通过代码我们可以了解到,我们只设置了 contentView 的背景颜色。此时,我们有几种解决方案来解决这个问题。
其中一种是禁用 TableView 的默认将 ContentView 适配 Safe Area 的行为,但这样会影响 contentView 里的其他内容。
这里最好的解决方案就是设定 backgroundView 的 backgroundColor。backgroundView 与 contentView 不同,它与 Cell 是一样大小,不受 Safe Area 影响。

    func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
        let header = view as! UITableViewHeaderFooterView
        let font = UIFont.preferredFont(forTextStyle: .headline)
        
        header.textLabel?.font = font
        
        header.backgroundView?.backgroundColor = UIColor.lightGray
    }

修改完毕后,编译运行。颜色就充满了整个 Cell 。但 ContentView 中的内容并没有发生变化。

以上就是我在为 WWDC 适配 iPhone X 的时候遇到的三个问题示例。

总结

适配 iPhone X ,需要注意以下几点:

  • 遵循 iOS 11 SDK,使用 Launch Storyboard,可以让你的 App 与原生分辨率一致
  • 测试UI时,横竖屏幕都要进行,绝大多数问题出在横屏下(左右横屏都要测)
  • 遵循 Safe Area 可以避免绝大多数适配问题
    • AutoLayout: 设置 safeAreaLayoutGuide
    • 手动布局: 使用 safeAreaInsets ,自由计算所需布局的数据
  • 不要让控件遮挡屏幕底部的 Home 指示器

关于 Home 指示器以及 iPhone X 设计方面的内容,请查看

Session Name Session Number From
Designing for iPhone X Session 801 Fall 2017
AutoLayout Techniques in Interface Builder Session 412 WWDC 2017
Modern User Interaction on iOS Session 219 WWDC 2017
Updating Your App for iOS 11 Session 204 WWDC 2017

一定要看 Designing for iPhone X 这个 Session,因为有很多关于 iPhone X 适配的细节包含在其中。

版权声明: QC-L, 如需转载请联系作者并标明出处,谢谢
如果觉得写得还不错,欢迎Star

React 相关

  • 在 backbone 里用 react 渲染
  • 在 angular 里用 react 渲染
  • 在 vue 里用 react 渲染
  • 在 xxx 里用 react 渲染
    然后再从 react 里拿出 dom,调用 dom api 或者 用 jQuery 插件, d3 套这个 dom

语言层面:

  • 用 coffee, es5, es6, jsx, ts, tsx 写

react 组件设计:

  • pure component
  • functional component
  • smart, dumb component
  • higher order component
  • hoc render hijacking
  • 会用 props.children React.children cloneElement
  • 提供 instance method
  • context

内部实现:

  • 懂 setState 是异步的
  • 懂 synthetic event
  • 懂 react-dom 分层和 react 没有关系
  • 懂 reconciler
  • 懂 fiber

测试:

  • 会写 jest
  • 会写 snapshot test

炫技:

  • 写一个顺滑的拖拽
  • 用一个 component 递归渲染
  • 写一个 react router

未来:

  • 知道 react 将来要增删哪些 api
  • 用 preact, inferno 替换 react

最最重要:

  • 懂 react 这个单词是什么意思

VSCode 将某文件设置为某语言

例如,将 .stylelintrc 设置为 json 语言,并高亮:

.vscode/setting.json 中设置如下代码:

{
  "files.associations": {
    ".stylelintrc": "json"
  }
}

Charles 网络封包截取工具 - HTTP/HTTPS

Charles

在平时的开发与测试过程中,总会遇到网络问题。我们为了调试与服务器端的网络通讯协议,常常需要截取网络封包来分析。Charles 通过将自己设置成系统的网络访问代理服务器,使得所有的网络访问请求都通过它来完成,从而实现了网络封包的截取和分析。

准备工作

由于本人是 iPhone 手机,这里以 iPhone 为主 (ps: 主要是安卓种类繁多,说不过来)

  1. iPhone + Charles(抓包工具)
  2. 抓包

iPhone + Charles(抓包工具)

安装 Charles

根据自己的系统,从 Charles 官网 下载,安装即可。
安装完成后,配置 Charles 相关。

截取手机上的网络数据包

Charles 通常用来截取本地上的网络数据包,但是当我们需要时,我们也可以用来截取其它设备上的网络请求。下面我就以 iPhone 为例,讲解如何进行相应操作。

设置系统 Charles

启动 Charles 的代理功能,在 Charles 中的 Proxy -> Proxy Setting ,具体设置如下图:
http-proxy-setting

设置 iPhone HTTP 代理

首先,获取当前 Mac 的 IP 地址

  1. option/alt + 鼠标左键点击 Wi-Fi 图标,会展示 IP 地址
  2. Charles 的菜单中 Help -> Local IP Address, 也能获取当前 IP 地址 (推荐)

ip
接下来,让 Mac 作为 iPhone 的代理

设置 -> 无线局域网 -> 你 Wi-Fi 名称 -> 详情 -> HTTP 代理 -> 配置代理
  1. 进到代理配置页,将 “关闭” 切换为 “手动”。
  2. 将你获取到的 IP 填入“服务器”的选项中
  3. 端口填入 8080
  4. 记得点击存储,否则代理信息不会保存

    此时, Mac 已经作为了 iPhone 的 HTTP 代理。但是,并不能截取到 HTTPS,因此,还需要进行额外的配置,使其可以截取 HTTPS
    请注意,当 Charles 弹出如下图弹框时,请一定选择 Allow !!!! 请一定选择 Allow !!!! 请一定选择 Allow !!!!

设置 iPhone HTTPS 代理

HTTPS 需要进行如下额外配置:

  1. Mac 安装证书
  2. 手机安装证书
  3. Mac 配置 HTTPS 代理
Mac 安装证书

找到 Charles 菜单,选择 Help -> SSL Proxying -> Install Charles Root Certificate,然后输入系统的帐号密码,即可在 钥匙串访问 中看到添加好的证书。
打开钥匙串访问,点击 登录 -> 证书 ,找到如下证书:

如果该证书不被用户信任,则需要修改信任等级,双击证书根据下图进行修改:

至此,Mac 的证书安装完毕。

iPhone 安装证书

找到 Charles 菜单,选择 Help -> SSL Proxying -> Install Charles Root Certificate on a Mobile Device or Remote Browser

会弹出如下弹窗:

使用已经进行 HTTP 代理的 iPhone 的 Safari 访问 chls.pro/ssl , 安装证书即可。

注意: iOS 10.3 以上需要针对 通用 -> 关于本机 -> 证书信任设置 -> Charles Proxy Custom Root Certification 进行信任操作,10.3以上必须要做!

Mac 配置 HTTPS 代理

找到 Charles 菜单,选择 Proxy -> SSL Proxying Setting -> SSL Proxying,图片效果如下:

选择 SSL Proxying Setting 后,具体配置内容如下:

基于vue-cli脚手架工具构建Vue项目

基于vue-cli脚手架工具构建Vue项目

Vue.js 是目前最火的前端框架,几乎没有之一,资深程序员这样评价它:"Vue.js 兼具 Angular.js 和 React.js 的优点,并剔除它们的缺点",大多前端工程师都视 Vue.js 为心中最理想的框架。

学习 Vue.js 建议查看 Vue 官方中文文档。当然如果英语能力好的话,推荐查看 Vue 官方英文文档,因为对某些 API 的理解,还是英文的文档更容易一些。个人看法,勿喷。

简介

vue-cli 是一个快速构建 Vue.js Project 的脚手架工具。

安装

注: Node.js 版本(>=4.x, 6.x 更高) npm 版本 3+ 并且保证有 Git 环境

npm install -g vue-cli

用法

vue init <template-name> <project-name>

例:

vue init webpack first-vue-project

上述命令是在当前目录下,通过vue-cli命令根据 vuejs-templates/webpack 下的模板,生成一个包含 webpack 的 Vue 项目,名为 first-vue-project

相关模板

模板名 模板介绍
browserify 包含 browserify 和 vueify 功能齐全的项目模板,可以设置热更新(Hot Reload)、代码检测 (Lint) 以及单元测试
browserify-simple 快速构建简易 browserify 和 vueify 项目的模板
pwa vue-cli 基于 webpack 模板构建 PWA(渐进式网页应用)
simple 简单构建一个只包含 HTML 的项目
webpack 一个功能齐全的 webpack + vue-loader 模板,模板中还可以设置热更新、代码检测(Lint)、测试以及 CSS 提取
webpack-simple 快速构建基于 webpack + vue-loader 的简易项目模板

以上模板可通过命令 list 查看

vue list

操作结果如下图所示:

vue-list.png

自定义template

官方的 template 可能有时并不能满足你的需求,此时,你可以 fork 官方的模板,进行自己的改造,并通过 vue-cli 工具根据改造后的 template 进行构建:

vue init username/repo my-project

例:

vue init QC-L/webpack-multi-page-template vue-multi-page-demo

其中 username/repo 为 Github 的 repo 标题,例如 QC-L/webpack-multi-page-template
构建工具会根据你提供的标题,进行模板下载,并进行 template 构建

本地template

除了 Github 源之外,你还可以使用本地 template 进行构建:

vue init ~/fs/path/to-custom-template my-project

例:

vue init ~/vue/template/webpack-multi-page-template vue-multi-page-demo

此时,template-name 就为你本地 template 的完整路径。

参考文章

https://github.com/vuejs/vue-cli

版权声明: QC-L,如需转载请联系作者并标明出处
如果觉得还不错,欢迎 Star

印记中文 - 通过 DocSearch 实现文档搜索

DocSearch 由 algolia 社区提供,为众多知名项目提供了文档搜索功能。

最近翻看了很多中文文档,发现其中并未包含文档搜索功能,即使包含也是使用的原文档的搜索功能。这样对于广大查阅中文文档的开发者及其不友好,因此,个人对 docsearch 进行了研究。并同时为 BabelReactVuefewebpack 等中文网添加了搜索功能。此篇文章会针对文档添加搜索功能进行详细讲解。

集成 docsearch

docsearch 为文档搜索提供了强有力的支持,因此,使得文档搜索变得非常容易。以下是官方在 github 中对该项目的描述:

Building a good search for a documentation is a complex challenge. We happen to have a lot of experience doing that, and we want to share it with the world. For free.

具体集成步骤如下:

  • 第一步:docsearch 上提交表格信息,其中包括 Website 和 E-mail 两部分

  • 第二步: 等待官方回复邮件,回复的邮件中会询问你是否是 Website 的维护者,因为可能需要你添加部分 js 代码和 css 样式。回复时间大约在半天到一天左右,具体要看你申请的时间(毕竟有时差)

    官方回复基本如下:

    Thank you for your interest in Algolia DocSearch! We would love to help your project with the search but we'll need to inject a small JavaScript snippet in the page: are you able to do that? Are you a maintainer of the website?

    只要回复官方,说你是维护者即可。

  • 第三步: 申请成功后,大约一天左右,官方会将你所需要的 API Key 及你所需以邮件的形式通知你,按照步骤集成即可。

    js 部分代码如下:

    <!-- at the end of the HEAD -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css" />
    <!-- at the end of the BODY -->
    <script type="text/javascript" 
    src="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.js"></script>
    <script type="text/javascript"> docsearch({
          apiKey: 'xxxx',
          indexName: 'xxxxx',
          inputSelector: '### REPLACE ME ####',
          debug: false // Set debug to true if you want to inspect the dropdown
    });
    </script>
    
  • 第四步: 大功告成,测试即可。

整个过程下来大约在两天左右,其中有一天是他们的服务器在对你网站的文档进行爬取操作。并且该爬取操作会每隔 24 小时进行一次。

总结

添加文档搜索功能,是为了更好的服务大家。使得大家有更好的中文文档体验,后续我们还会为大家提供更多更好的服务。欢迎更多关注我们的小伙伴参与进来,还有更多的文档需要我们去搞定。

印记中文希望有更多好的项目进驻,无论是前端、后台、客户端、AI等等,我们尊崇谁推动、谁负责、谁主导的原则,印记中文会作为你强大的服务器资源及工程化流程支持,助你更好地进行技术文档的翻译或者社区的搭建。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.