ineo6

ineo6 查看完整档案

上海编辑  |  填写毕业院校  |  填写所在公司/组织 idayer.com 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

ineo6 发布了文章 · 7月13日

GitMaster 是如何定制 file-icons/atom

GitMaster里面展示项目结构时,同时也显示了对应的icon

看起来和Octotree是没什么区别,但其实在维护和更新上是有显著区别的。

Octotree是直接从file-icons/atom复制相关样式和字体文件到项目里,这样耦合的方式很不利于维护,所以我在处理文件图标时进行了额外的处理,把所有文件图标通过npm包的形式引入。

大家可能好奇为什么不直接用file-icons/atom,没有采用的原因有几个:

  • css样式经过Content Script方式注入会污染全局样式
  • 缺少Octicons图标
  • woff2文件指向不对

方案

经过考量,最终采用通过脚本处理文件,然后发布npm包: ineo6/file-icons

下载 file-icons/atom

使用download-git-repoGitHub下载代码。

还使用npm开发过程中常用的chalkora

ora是一个终端加载动画的库,有了它,你的终端不会再苍白。

image.png

chalk的作用是丰富终端文字样式。

image.png

const path = require('path');
const chalk = require('chalk');
const fs = require('fs');
const os = require('os');
const ora = require('ora');
const download = require('download-git-repo');

const cwd = process.cwd();

const origin = 'file-icons/atom';
const branch = "#master";

const tmpDirPrefix = path.join(os.tmpdir(), '.tmp');
const tmpDir = fs.mkdtempSync(tmpDirPrefix);

const spinner = ora(`downloading ${origin}...`);
spinner.start();

download(`${origin}${branch}`, tmpDir, { clone: false }, function (err) {
  spinner.stop();
  if (err) {
    console.log(chalk.red(`Failed to download repo https://github.com/${origin}${branch}`, err));
  } else {
    console.log(chalk.green(`Success to download repo https://github.com/${origin}${branch}`));

    const spinnerExtract = ora('Extract Data...');
    spinnerExtract.start();

    try {
      // 处理代码的逻辑

      spinnerExtract.stop();
      console.log(chalk.green('Done!'));
    } catch (e) {
      spinnerExtract.stop();
      console.log(e.message);
    }
  }
})

less处理

替换@font-face url

文件 styles/fonts.less 里面的内容是如下格式:

@font-face {
    font-family: FontAwesome;
    font-weight: normal;
    font-style: normal;
    src: url("atom://file-icons/fonts/fontawesome.woff2");
}

这个显然无法在前端项目甚至Chrome扩展里正确引用woff2字体。

因为在Chrome扩展里无法引入远程的woff2,所以改为引入扩展目录中的字体,即改成如下格式:

@font-face {
    font-family: FontAwesome;
    font-weight: normal;
    font-style: normal;
    src: url("@{ICON_PATH}/fonts/fontawesome.woff2");
}

然后在webpack里设置less变量ICON_PATH

          'less-loader',
          {
            loader: 'less-loader',
            options: {
              javascriptEnabled: true,
              modifyVars: {
                ICON_PATH: 'chrome-extension://__MSG_@@extension_id__'
              },
            },
          },

如何修改less文件

推荐使用gonzales-pe,它能够解析SCSS, Sass, LESS,并转为AST抽象语法树。

然后我们根据需要修改AST的结构,最终调用astNode.tostring()转换得到代码。

const { parse } = require('gonzales-pe');
const fs = require('fs');
const chalk = require('chalk');

function replaceAtomHost(content) {
    if (content.includes('atom://file-icons/')) {
        content = content.replace('atom://file-icons/', '@{ICON_PATH}/');
    }

    return content;
}

function replaceUrlHost(ast) {
    ast.traverseByType('uri', (node) => {
        node.traverse(item => {
            if (item.is('string')) {
                item.content = replaceAtomHost(item.content)
            }
        });
    });

    return ast;
}

function replaceDeclaration(ast) {
    ast.traverseByType('declaration', (decl) => {
        let isVariable = false;

        decl.traverse((item) => {
            if (item.type === 'property') {
                item.traverse((childNode) => {
                    if (childNode.content === 'custom-font-path') {
                        isVariable = true;
                    }
                });
            }

            if (isVariable) {
                if (item.type === 'value') {
                    const node = item.content[0];

                    node.content = replaceAtomHost(node.content)
                }
            }
            return item;
        });
    });

    return ast;
}

function processFonts(lessFile) {
    const content = fs.readFileSync(lessFile).toString();

    if (content && content.length > 0) {

        let astTree;

        try {
            astTree = parse(content, {
                syntax: 'less'
            })
        } catch (e) {
            console.log(chalk.red(`parse error: ${e}`));
            return;
        }

        try {
            astTree = replaceUrlHost(astTree);
            astTree = replaceDeclaration(astTree);

            return astTree;
        } catch (e) {
            console.log(chalk.red(`transform error: ${e}`));
        }
    }
}

module.exports = function (file) {
    const ast = processFonts(file);

    if (ast) {
        fs.writeFileSync(file, ast.toString());
    }
}

文件处理

.
├── bin              
├── index.js
├── index.less              // 入口样式
├── lib                     // 完成的样式,字体
└── resource                // 待合并资源

file-icons/atom复制以下文件到lib:

  • fonts
  • styles
  • lib/icons
  • lib/utils.js

resource里面内容复制到lib

index.less里面内容如下:

@import "lib/styles/colours.less";
@import "lib/styles/fonts.less";
@import "lib/styles/octicons.less";

.file-icons-wrapper {
  @import "lib/styles/icons.less";
  @import "lib/styles/fix.less";
}

这里通过添加父级file-icons-wrapper来控制样式影响范围。

至此,大致完成了针对file-icons/atom的定制工作。

总结

最终我们通过npm run build命令完成拉取代码,处理文件的。

对应的脚本在bin/update.js

当然最后可以优化的是让任务自动执行,这点可以结合GitHub Actions的定时任务实现。本文就暂不花费篇幅介绍了,感兴趣的可以摸索下。

查看原文

赞 0 收藏 0 评论 0

ineo6 发布了文章 · 7月3日

GitMaster:树形展示项目代码插件,支持GitHub、GitLab、Gitee

什么是GitMaster

相信很多人知道Octotree,是一款针对GitHub的浏览器扩展,主要功能是在网页上展示项目的树形结构和文件代码。

GitMaster的核心功能和Octotree是一致,同时也有自己的特性。

  • 🚀 支持GitHubGitLabGitee
  • 🖊️ 支持私有部署页面,一键标记
  • 🌗 黑暗模式(仅GitHub
  • 🔔 通知提醒功能(仅GitHub
  • ⬇️ 目录、文件单独下载
Gitee就是码云,下面为了行文方便,会统一称为码云。

如何使用

目前支持ChromeEdge,可以商店搜索Git Master或者点击商店页面安装。

安装完成后打开页面 例子 就能看到效果。

设置

建议安装后首先设置下access token,因为默认情况下请求次数是有限制的,超过之后就只能通过设置access token来获取更多的请求次数。

点击右上角设置,在对应的xxx access token栏中输入,最后保存即可,

点击输入框右上角的钥匙图标可以跳转到access token生成页面。

image.png

私有部署页面

默认识别github.comgitlab.comgitee.com,如果你还是使用了企业部署的版本,可以自行标记。

点击浏览器右上角图标,在弹出页面中选择Enable xxx或者Disable xxx

image.png

停靠位置

点击图标可以切换GitMaster出现的位置,另外提示下点击图钉位置可以让插件固定哦~

image.png

黑暗模式

点击箭头指向位置图标即可开启黑暗护眼模式,今天又省了几度电呢~

image.png

文件下载

在项目结构树增加了文件夹数量以及文件大小的显示,如果不需要该功能,可以在选项中关闭,如下图。

image.png

另外在原来代码页面添加了下载指定文件夹、文件的功能,再也不用为了个别内容而下载整个仓库。

image.png

目标是什么?

中间有段时间Octotree其实是支持GitLab,最后又只支持GitHub,我们只能安装多个插件来同时支持GitHubGitLab、码云。

GitMaster的出现正是要解决整个问题,并且会添加更多效率功能,最终希望能够只安装一个插件就能满足大部分的使用。

有什么建议欢迎留言评论,或者到GitHubissue,你的付出一定会让GitMaster走得更远。

https://github.com/ineo6/git-...

查看原文

赞 0 收藏 0 评论 0

ineo6 发布了文章 · 6月5日

GitHub Actions和mp-ci助力微信小程序持续集成

使用GitHub Actionsmp-ciTaro项目添加持续集成,让开发飞上天。

mp-ci

mp-ci是微信小程序(游戏)发布助手, 支持预览和上传。可以直接接入开发流程中,提高研发效率。

基于官方miniprogram-ci封装,比它的老大哥 mini-deploy 好了很多倍,再也不需要安装开发者工具,也不用考虑登录、操作系统的问题。

  • 自动读取appidsetting
  • 上传信息(版本和备注)自动生成
  • 美化执行结果

更多说明直接访问 mp-ci

准备工作

下载私钥

使用前需要使用小程序管理员身份访问"微信公众平台-开发-开发设置"后下载代码上传密钥,并配置 IP 白名单。

15913419587405.jpg

这里需要关闭IP白名单,因为无法确定GitHub服务器的IP范围。

设置私钥到GitHub

mp-ci是通过文件形式接受私钥的,但是把私钥直接放到GitHub上显然是不安全的。

这里我们使用GitHubSecrets来存储私钥内容,然后在执行时再创建私钥文件。

打开仓库的Settings/Secrets页面设置接口,假设我们存储的名字是UPLOAD_PRIVATE_KEY

GitHub Actions 配置

新建.github/workflows/build.yaml文件。

也可以在Actions一栏,选择Node.js或者set up a workflow yourself

15913430675249.jpg

使用以下内容填充:

name: MP CI

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

jobs:
  build:

    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [10.x]

    steps:
    - uses: actions/checkout@v2
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v1
      with:
        node-version: ${{ matrix.node-version }}

    - name: Install Dependencies
      run: npm i

    - name: Build weapp
      run: npm run build:weapp

    # 从 secrets.UPLOAD_PRIVATE_KEY 生成私钥文件
    # see Project/Settings/Secrets
    - name: Generate private key for upload
      run: echo "$UPLOAD_PRIVATE_KEY" > private.key
      env:
        UPLOAD_PRIVATE_KEY: ${{ secrets.UPLOAD_PRIVATE_KEY }}
    # 上传代码
    - name: Upload to WeChat
      run: npx mp-ci upload ./ --pkp=./private.key --no-test

结果

15913426970140.jpg

查看原文

赞 0 收藏 0 评论 0

ineo6 发布了文章 · 2019-12-23

开发常用软件镜像加速收集

pypi

临时使用:

pip install -i https://pypi.tuna.tsinghua.edu.cn/simple some-package

长期使用:

升级 pip 到最新的版本 (>=10.0.0) 后进行配置:

pip install pip -U
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

如果您到 pip 默认源的网络连接较差,临时使用本镜像站来升级 pip:

pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pip -U

可用源地址

nvm

临时使用:

export NVM_NODEJS_ORG_MIRROR="https://npm.taobao.org/mirrors/node/"

长期使用:

echo 'export NVM_NODEJS_ORG_MIRROR="https://npm.taobao.org/mirrors/node/"' >> ~/.bashrc

Ruby Gems

gem

# 添加 TUNA 源并移除默认源
gem sources --add https://mirrors.tuna.tsinghua.edu.cn/rubygems/ --remove https://rubygems.org/
# 列出已有源
gem sources -l
# 应该只有一个

Gemfile 和 Bundler 的项目

bundle config mirror.https://rubygems.org https://mirrors.tuna.tsinghua.edu.cn/rubygems/

可用源地址

CocoaPods

旧版使用方式:

pod repo remove master
pod repo add master https://mirrors.tuna.tsinghua.edu.cn/git/CocoaPods/Specs.git
pod repo update

新版的CocoaPods不允许用pod repo add直接添加master库了,但是依然可以通过下面方式:

$ cd ~/.cocoapods/repos 
$ pod repo remove master
$ git clone https://mirrors.tuna.tsinghua.edu.cn/git/CocoaPods/Specs.git master

最后在Podfile第一行加上:

source 'https://mirrors.tuna.tsinghua.edu.cn/git/CocoaPods/Specs.git'

可用源地址

Flutter

Flutter是一款跨平台的移动应用开发框架,由Google开源。用Flutter开发的应用可以直接编译成ARM代码运行在AndroidiOS系统上。

Flutter安装时需要从Google Storage 下载文件,如您的网络访问Google受阻,需要使用镜像。

临时使用:

export FLUTTER_STORAGE_BASE_URL="https://mirrors.tuna.tsinghua.edu.cn/flutter"

长期使用:

echo 'export FLUTTER_STORAGE_BASE_URL="https://mirrors.tuna.tsinghua.edu.cn/flutter"' >> ~/.bashrc

此外Flutter开发中还需要用到Dart语言的包管理器Pub,其镜像使用方法参见Pub 镜像安装帮助。

Pub

PubDart官方的包管理器。跨平台的前端应开发框架Flutter也基于Dart,并且可以使用大部分Pub中的库。

临时使用:

PUB_HOSTED_URL="https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" pub get # pub
PUB_HOSTED_URL="https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" flutter packages get # flutter

长期使用:

echo 'export PUB_HOSTED_URL="https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"' >> ~/.bashrc

Homebrew

brew && core && cask

设置镜像:

git -C "$(brew --repo)" remote set-url origin https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/brew.git

git -C "$(brew --repo homebrew/core)" remote set-url origin https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/homebrew-core.git

git -C "$(brew --repo homebrew/cask)" remote set-url origin https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/homebrew-cask.git

恢复官方:

git -C "$(brew --repo)" remote set-url origin https://github.com/Homebrew/brew.git

git -C "$(brew --repo homebrew/core)" remote set-url origin https://github.com/Homebrew/homebrew-core.git

git -C "$(brew --repo homebrew/cask)" remote set-url origin https://github.com/Homebrew/homebrew-cask.git

可用源

名称类型地址
清华brewhttps://mirrors.tuna.tsinghua...
清华corehttps://mirrors.tuna.tsinghua...
清华caskhttps://mirrors.tuna.tsinghua...
中科大brewhttps://mirrors.ustc.edu.cn/b...
中科大corehttps://mirrors.ustc.edu.cn/h...
中科大caskhttps://mirrors.ustc.edu.cn/h...

Homebrew-bottles

临时替换:

export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles

长期替换:

echo 'export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles' >> ~/.bash_profile
source ~/.bash_profile

可用源

homebrew-install

解决install脚本无法访问问题,通过镜像加速脚本安装。

使用方法和官方一致:

/usr/bin/ruby -e "$(curl -fsSL https://cdn.jsdelivr.net/gh/ineo6/homebrew-install/install)"

具体食用说明点此

查看原文

赞 0 收藏 0 评论 0

ineo6 发布了文章 · 2019-12-09

create-react-app 优雅定制指南

create-react-app是一款广泛使用的脚手架,默认它只能使用eject命令暴露出webpack配置,其实这样使用很不优雅,修改内容文件的话也不利于维护,react-app-rewired正式解决这样问题的工具,今天我们就好好学习下它的用法。

create-react-app.png

1. 安装 react-app-rewired

create-react-app 2.x with Webpack 4

npm install react-app-rewired --save-dev

create-react-app 1.x or react-scripts-ts with Webpack 3

npm install react-app-rewired@1.6.2 --save-dev

2. 根目录创建config-overrides.js

/* config-overrides.js */

module.exports = function override(config, env) {
  //do stuff with the webpack config...
  return config;
}

当然我们也可以把config-overrides.js放到其他位置,比如我们要指向node_modules中某个第三方库提供的配置文件,就可以添加下面配置到package.json

"config-overrides-path": "node_modules/other-rewire"

3. 替换 react-scripts

打开package.json:

  /* package.json */

  "scripts": {
-   "start": "react-scripts start",
+   "start": "react-app-rewired start",
-   "build": "react-scripts build",
+   "build": "react-app-rewired build",
-   "test": "react-scripts test --env=jsdom",
+   "test": "react-app-rewired test --env=jsdom",
    "eject": "react-scripts eject"
}

4. 配置

定制 Webpack 配置

webpack字段可以用来添加你的额外配置,当然这里面不包含Webpack Dev Server

const { override, overrideDevServer, fixBabelImports, addLessLoader, addWebpackAlias, addWebpackModuleRule } = require('customize-cra');

const removeManifest = () => config => {
    config.plugins = config.plugins.filter(
        p => p.constructor.name !== "ManifestPlugin"
    );
    return config;
};

module.exports = {
    webpack: override(
        removeManifest(),
        fixBabelImports('import', {
            libraryName: 'antd',
            libraryDirectory: 'es',
            style: 'css',
        }),
        addLessLoader(),
        addWebpackModuleRule({
            test: require.resolve('snapsvg/dist/snap.svg.js'),
            use: 'imports-loader?this=>window,fix=>module.exports=0',
        },),
        addWebpackAlias({
            Snap: 'snapsvg/dist/snap.svg.js'
        }),
    ),
    devServer: overrideDevServer(
        ...
    )
}

定制 Jest 配置 - Testing

jest配置

定制 Webpack Dev Server

通过devServer我们可以做一些开发环境的配置,比如设置proxy代理,调整publicPath,通过disableHostCheck禁用转发域名检查等。

CRA 2.0开始,推荐搭配customize-cra使用,里面提供了一些常用的配置,可以方便我们直接使用。

const { override, overrideDevServer, } = require('customize-cra');

const addProxy = () => (configFunction) => {
    configFunction.proxy = {
        '/v2ex/': {
            target: 'https://www.v2ex.com',
            changeOrigin: true,
            pathRewrite: { '^/v2ex': '/' },
        },
    };

    return configFunction;
}

module.exports = {
    webpack: override(
        ...
    ),
    devServer: overrideDevServer(
        addProxy()
    )
}

Paths - 路径变量

paths里面是create-react-app里面的一些路径变量,包含打包目录、dotenv配置地址、html模板地址等。

module.exports = {
  dotenv: resolveApp('.env'),
  appPath: resolveApp('.'),
  appBuild: resolveApp('build'),
  appPublic: resolveApp('public'),
  appHtml: resolveApp('public/index.html'),
  appIndexJs: resolveModule(resolveApp, 'src/index'),
  appPackageJson: resolveApp('package.json'),
  appSrc: resolveApp('src'),
  appTsConfig: resolveApp('tsconfig.json'),
  appJsConfig: resolveApp('jsconfig.json'),
  yarnLockFile: resolveApp('yarn.lock'),
  testsSetup: resolveModule(resolveApp, 'src/setupTests'),
  proxySetup: resolveApp('src/setupProxy.js'),
  appNodeModules: resolveApp('node_modules'),
  publicUrl: getPublicUrl(resolveApp('package.json')),
  servedPath: getServedPath(resolveApp('package.json')),
  // These properties only exist before ejecting:
  ownPath: resolveOwn('.'),
  ownNodeModules: resolveOwn('node_modules'), // This is empty on npm 3
  appTypeDeclarations: resolveApp('src/react-app-env.d.ts'),
  ownTypeDeclarations: resolveOwn('lib/react-app.d.ts'),
};

比如我们要修改appHtmlhtml模板的默认位置,可以这样做:

const path = require('path');


module.exports = {
    paths: function (paths, env) {

        // 指向根目录的test.html
        paths.appHtml = path.resolve(__dirname, "test.html");

        return paths;
    },
}

5. 常用示例

添加多页面入口

首先安装react-app-rewire-multiple-entry

npm install react-app-rewire-multiple-entry --save-dev

然后在config-overrides.js配置:

const { override, overrideDevServer } = require('customize-cra');

const multipleEntry = require('react-app-rewire-multiple-entry')([{
    entry: 'src/pages/options.tsx',
    template: 'public/options.html',
    outPath: '/options.html',
}]);

const addEntry = () => config => {

    multipleEntry.addMultiEntry(config);
    return config;
};

const addEntryProxy = () => (configFunction) => {
    multipleEntry.addEntryProxy(configFunction);
    return configFunction;
}

module.exports = {
    webpack: override(
        addEntry(),
    ),
    devServer: overrideDevServer(
        addEntryProxy(),
    )
}

禁用 ManifestPlugin

const { override, } = require('customize-cra');


const removeManifest = () => config => {
    config.plugins = config.plugins.filter(
        p => p.constructor.name !== "ManifestPlugin"
    );
    return config;
};


module.exports = {
    webpack: override(
        removeManifest(),
    ),
}

antd 按需加载 && less-loader

const { override, fixBabelImports, addLessLoader } = require('customize-cra');

module.exports = {
    webpack: override(
        fixBabelImports('import', {
            libraryName: 'antd',
            libraryDirectory: 'es',
            style: 'css',
        }),
        addLessLoader(),
    ),
}

最后,如果使用上有什么问题欢迎留言,我会尽我所能解答大家的问题。

本文首发: create-react-app 优雅定制指南

另外推荐关注公众号:湖中剑,实时获取文章更新。

wechat-find-me.png

查看原文

赞 3 收藏 1 评论 0

ineo6 发布了文章 · 2019-11-06

mac下镜像飞速安装Homebrew教程

Homebrew是一款包管理工具,目前支持macOSlinux系统。主要有四个部分组成: brewhomebrew-corehomebrew-caskhomebrew-bottles

名称说明
brewHomebrew 源代码仓库
homebrew-coreHomebrew 核心源
homebrew-cask提供 macOS 应用和大型二进制文件的安装
homebrew-bottles预编译二进制软件包

本文主要介绍Homebrew安装方式以及如何加速访问,顺便普及一些必要的知识。

1. 脚本说明

Homebrew默认安装脚本:

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

如果你等待一段时间之后遇到下面提示,就说明无法访问官方脚本地址:

curl: (7) Failed to connect to raw.githubusercontent.com port 443: Operation timed out

请使用下面的脚本:

/usr/bin/ruby -e "$(curl -fsSL https://cdn.jsdelivr.net/gh/ineo6/homebrew-install/install)"

上面脚本中使用了中科大镜像来加速访问。

2. 执行命令

/usr/bin/ruby -e "$(curl -fsSL https://cdn.jsdelivr.net/gh/ineo6/homebrew-install/install)"

如果命令执行中卡在下面信息:

==> Tapping homebrew/core
Cloning into '/usr/local/Homebrew/Library/Taps/homebrew/homebrew-core'...

Command + C中断脚本执行如下命令:

cd "$(brew --repo)/Library/Taps/"
mkdir homebrew && cd homebrew
git clone git://mirrors.ustc.edu.cn/homebrew-core.git

成功执行之后继续执行前文的安装命令。

最后看到==> Installation successful!就说明安装成功了。

最最后执行:

brew update

cask同样也有首次下载缓慢的问题,解决方法大致同上:

cd "$(brew --repo)/Library/Taps/"
cd homebrew
git clone https://mirrors.ustc.edu.cn/homebrew-cask.git

3. 卸载Homebrew

使用官方脚本同样会遇到uninstall地址无法访问问题,可以替换为下面脚本:

/usr/bin/ruby -e "$(curl -fsSL https://cdn.jsdelivr.net/gh/ineo6/homebrew-install/uninstall)"

4. 设置镜像

brewhomebrew/core是必备项目,homebrew/caskhomebrew/bottles按需设置。

通过 brew config 命令查看配置信息。

4.1 中科大源

git -C "$(brew --repo)" remote set-url origin https://mirrors.ustc.edu.cn/brew.git

git -C "$(brew --repo homebrew/core)" remote set-url origin https://mirrors.ustc.edu.cn/homebrew-core.git

git -C "$(brew --repo homebrew/cask)" remote set-url origin https://mirrors.ustc.edu.cn/homebrew-cask.git

brew update

# 长期替换homebrew-bottles
echo 'export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.ustc.edu.cn/homebrew-bottles' >> ~/.bash_profile
source ~/.bash_profile

注意bottles可以临时设置,在终端执行下面命令:

export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.ustc.edu.cn/homebrew-bottles

4.2 清华大学源

git -C "$(brew --repo)" remote set-url origin https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/brew.git

git -C "$(brew --repo homebrew/core)" remote set-url origin https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/homebrew-core.git

git -C "$(brew --repo homebrew/cask)" remote set-url origin https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/homebrew-cask.git

brew update

# 长期替换homebrew-bottles
echo 'export HOMEBREW_BOTTLE_DOMAIN=https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles' >> ~/.bash_profile
source ~/.bash_profile

4.3 恢复

git -C "$(brew --repo)" remote set-url origin https://github.com/Homebrew/brew.git

git -C "$(brew --repo homebrew/core)" remote set-url origin https://github.com/Homebrew/homebrew-core.git

git -C "$(brew --repo homebrew/cask)" remote set-url origin https://github.com/Homebrew/homebrew-cask.git

brew update

homebrew-bottles配置只能手动删除,将 ~/.bash_profile 文件中的 HOMEBREW_BOTTLE_DOMAIN=https://mirrors.xxx.com内容删除,并执行 source ~/.bash_profile

5. 其他

5.1 cask

目前cask是从GitHub上读取软件源,而GitHub Api对匿名访问有限制,如果使用比较频繁的话,可以申请Api Token,然后在环境变量中配置到HOMEBREW_GITHUB_API_TOKEN

.bash_profile中追加:

export HOMEBREW_GITHUB_API_TOKEN=yourtoken

注意:因为cask是基于GitHub下载软件,所以目前是无法加速的。

6. 总结

在前面的过程中我们把brewhomebrew-core的地址都指向到中科大镜像。

原理是通过修改install脚本,在里面预设镜像地址来做到的。

#!/usr/bin/ruby
# This script installs to /usr/local only. To install elsewhere (which is
# unsupported) you can untar https://github.com/Homebrew/brew/tarball/master
# anywhere you like.
HOMEBREW_PREFIX = "/usr/local".freeze
HOMEBREW_REPOSITORY = "/usr/local/Homebrew".freeze
HOMEBREW_CACHE = "#{ENV["HOME"]}/Library/Caches/Homebrew".freeze
# 这里替换了BREW_REPO
BREW_REPO = "https://mirrors.ustc.edu.cn/brew.git".freeze

最后不完美的地方是我们只能预设brew镜像,没找到比较好的办法预设homebrew-corehomebrew-caskgit地址。

参考文章

本文首发博客: mac下镜像飞速安装Homebrew教程

wechat-find-me.png

查看原文

赞 6 收藏 3 评论 10

ineo6 发布了文章 · 2019-11-01

图片上传姿势以及你不知道的Typed Arrays

在思否答题遇到几个关于图片上传的问题,中间都涉及到ArrayBuffer的概念,心心念念想整理下这方面的知识,也希望让更多人能有所收获。

各位看官,一起开始吧。

1. 如何上传文件

前端中上传一般使用FormData创建请求数据,示例如下:

var formData = new FormData();

formData.append("username", "Groucho");

// HTML 文件类型input,由用户选择
formData.append("userfile", fileInputElement.files[0]);

// JavaScript file-like 对象
var content = '<a id="a"><b id="b">hey!</b></a>'; // 新文件的正文...
var blob = new Blob([content], { type: "text/xml"});

formData.append("webmasterfile", blob);

var request = new XMLHttpRequest();
request.open("POST", "http://foo.com/submitform.php");
request.send(formData);
FormData 对象的字段类型可以是 Blob, File, 或者 string,如果它的字段类型不是Blob也不是File,则会被转换成字符串。

我们通过<input type="input"/>选择图片,把获取到的file放到FormData,再提交到服务器。

如果上传多个文件,就追加到同一个字段中。

fileInputElement.files.forEach(file => {
  formData.append('userfile', file);
})

其中的file-likenew Blob的示例说明我们可以构造一个新的文件直接上传。

场景1:剪辑图片上传

我们通过裁剪库可以得到data url或者canvas

cropperjs举例,使用getCroppedCanvas获取到canvas,然后利用自身的toBlob获取到file数据,再通过FormData上传。

转换的核心代码可以参考下面:

canvas = cropper.getCroppedCanvas({
  width: 160,
  height: 160,
});

initialAvatarURL = avatar.src;
avatar.src = canvas.toDataURL();

// 从canvs获取blob数据
canvas.toBlob(function (blob) {
  var formData = new FormData();
  formData.append('avatar', blob, 'avatar.jpg');
  
  // 接下来可以发起请求了
  makeRequest(formData)
})

场景2:base64图片上传

获取到base64形式的图片后,我们通过下面函数转为blob形式:

function btof(base64Data, fileName) {
  const dataArr = base64Data.split(",");
  const byteString = atob(dataArr[1]);

  const options = {
    type: "image/jpeg",
    endings: "native"
  };
  const u8Arr = new Uint8Array(byteString.length);
  for (let i = 0; i < byteString.length; i++) {
    u8Arr[i] = byteString.charCodeAt(i);
  }
  return new File([u8Arr], fileName + ".jpg", options);
}

这样我们拿到了文件file,然后就可以继续上传了。

场景3:URL图片上传

想要直接用图片URL上传,我们可以分成两部来做:

  1. 获取base64
  2. 然后转为file

其中关键代码是如何从URL中创建canvas,这里通过创建Image对象,在图片挂载之后,填充到到canvas中。

var img =
  "https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=508387608,2848974022&fm=26&gp=0.jpg"; //imgurl 就是你的图片路径
  
var image = new Image();
image.src = img;
image.setAttribute("crossOrigin", "Anonymous");
image.onload = function() {
  // 第1步:获取base64形式的图片
  var base64 = getBase64Image(image);

  var formData = new FormData(); 

  // 第2步:转换base64到file
  var file = btof(base64, "test");
  formData.append("imageName", file);
};

function getBase64Image(img) {
  var canvas = document.createElement("canvas");
  canvas.width = img.width;
  canvas.height = img.height;
  var ctx = canvas.getContext("2d");
  ctx.drawImage(img, 0, 0, img.width, img.height);
  var ext = img.src.substring(img.src.lastIndexOf(".") + 1).toLowerCase();
  var dataURL = canvas.toDataURL("image/" + ext);

  return dataURL;
}

<p class="codepen" data-height="355" data-theme-id="0" data-default-tab="js,result" data-user="ineo6" data-slug-hash="MWgpGQZ" data-preview="true" style="height: 355px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="url image转为base64">
<span>See the Pen
url image转为base64
by neo (@ineo6)
on CodePen.</span>
</p>
<script async data-original="https://static.codepen.io/ass...;></script>

2. 思考

虽然前文提到的场景我们解决了,但是里面包含了这些关键词,不得不让人思考:

  • Blob
  • File
  • Uint8Array
  • ArrayBuffer
  • TypedArray
  • Base64
  • atob,btoa

这些关键词都指向"文件"、"二进制"、"编码",也是我们平时不太会注意的点。

之前使用到FileBlob时心里也一直有疑惑。

到底这些有什么作用呢?接下来可以看看我整理的这些知识。

3. 概念

3.1 Blob

Blob 对象表示一个不可变、原始数据的类文件对象。

File接口也是基于Blob对象,并且进行扩展支持用户系统的文件格式。

3.1.1 创建Blob对象

要从其他非blob对象和数据构造Blob,就要使用Blob()构造函数:

var debug = {hello: "world"};
var blob = new Blob([JSON.stringify(debug, null, 2)], {type : 'application/json'});

3.1.1 读取Blob对象

使用FileReader可以读取Blob对象中的内容。

var reader = new FileReader();
reader.addEventListener("loadend", function() {
   //reader.result 就是内容
   console.log(reader.result)
});
reader.readAsArrayBuffer(blob);

3.1.1 Object URLs

Object URLs指的是以blob:开头的地址,可以用来展示图片、文本信息。

这里就有点类似base64图片的展示,所以我们同样可以用来预览图片。

下面代码片段就是把选中的图片转为Object URLs形式。

function handleFiles(files) {
  if (!files.length) {
    fileList.innerHTML = "<p>No file!</p>";
  } else {
    fileList.innerHTML = "";
    var list = document.createElement("ul");
    fileList.appendChild(list);
    for (var i = 0; i < files.length; i++) {
      var li = document.createElement("li");
      list.appendChild(li);
      
      var img = document.createElement("img");
      // 从文件中创建object url
      img.src = window.URL.createObjectURL(files[i]);
      img.height = 60;
      img.onload = function() {
        // 加载完成后记得释放object url
        window.URL.revokeObjectURL(this.src);
      }
      li.appendChild(img);
      var info = document.createElement("span");
      info.innerHTML = files[i].name + ": " + files[i].size + " bytes";
      li.appendChild(info);
    }
  }
}

demo

3.2 Typed Arrays - 类型化数组

类型化数组是一种类似数组的对象,提供了访问原始二进制数据的功能。但是类型化数组和正常数组并不是一类的,Array.isArray()调用会返回false

Typed Arrays有两块内容:

  • 缓冲(ArrayBuffer)
  • 视图(TypedArray 和 DataView)

3.2.1 ArrayBuffer

ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区。
ArrayBuffer 不能直接操作,而是要通过TypedArrayDataView对象来操作,它们会将缓冲区中的数据表示为特定的格式,并通过这些格式来读写缓冲区的内容。

ArrayBuffer主要用来高效快速的访问二进制数据,比如 WebGL, Canvas 2D 或者 Web Audio 所使用的数据。

接下来我们结合TypedArray一起理解下。

3.2.2 TypedArray

TypedArray可以在ArrayBuffer对象之上,根据不同的数据类型建立视图。

// 创建一个8字节的ArrayBuffer
const b = new ArrayBuffer(8);

// 创建一个指向b的Int32视图,开始于字节0,直到缓冲区的末尾
const v1 = new Int32Array(b);

// 创建一个指向b的Uint8视图,开始于字节2,直到缓冲区的末尾
const v2 = new Uint8Array(b, 2);

// 创建一个指向b的Int16视图,开始于字节2,长度为2
const v3 = new Int16Array(b, 2, 2);

Int32Array,Uint8Array之类指的就是TypedArrayTypedArray对象描述的是底层二进制数据缓存区的一个类似数组(array-like)的视图。

它有着众多的成员:

Int8Array(); 
Uint8Array(); 
Uint8ClampedArray();
Int16Array(); 
Uint16Array();
Int32Array(); 
Uint32Array(); 
Float32Array(); 
Float64Array();

15722463800941.jpg

再来看一个小栗子:

var buffer = new ArrayBuffer(2) 
var bytes = new Uint8Array(buffer)

bytes[0] = 65 // ASCII for 'A'
bytes[1] = 66 // ASCII for 'B'

// 查看buffer内容
var blob = new Blob([buffer], {type: 'text/plain'})
var dataUri = window.URL.createObjectURL(blob)
window.open(dataUri) // AB

字节序

上面的例子中,我们先写入'A',再写入'B',当然我们也可以通过Uint16Array一下写入两个字节。

var buffer = new ArrayBuffer(2) // 两个字节的缓冲
var word = new Uint16Array(buffer) // 以16位整型访问缓冲

// 添加'A'到高位,添加'B'到低位
var value = (65 << 8) + 66
word[0] = value

var blob = new Blob([buffer], {type: 'text/plain'})
var dataUri = window.URL.createObjectURL(blob)
window.open(dataUri) // BA

执行这段代码你会发现,为什么看到的是"BA"而不是"AB"?

这是因为还有"字节序"的存在,分别是小端字节序和大端字节序。

比如,一个占据四个字节的 16 进制数0x12345678,决定其大小的最重要的字节是“12”,最不重要的是“78”。小端字节序将最不重要的字节排在前面,储存顺序就是78563412;大端字节序则完全相反,将最重要的字节排在前面,储存顺序就是12345678。

因为浏览器使用的是小端字节序,就导致我们看到的是"BA"。为了解决字节序不统一的问题,我们可以使用DataView设定字节序。

TypedArray.prototype.buffer

TypedArray实例的buffer属性,返回整段内存区域对应的ArrayBuffer对象。该属性为只读属性。

const a = new Float32Array(64);
const b = new Uint8Array(a.buffer);

上面代码的a视图对象和b视图对象,对应同一个ArrayBuffer对象,即同一段内存。

TypedArray.prototype.byteLength,TypedArray.prototype.byteOffset

byteLength属性返回 TypedArray 数组占据的内存长度,单位为字节。byteOffset属性返回 TypedArray 数组从底层ArrayBuffer对象的哪个字节开始。这两个属性都是只读属性。

const b = new ArrayBuffer(8);

const v1 = new Int32Array(b);
const v2 = new Uint8Array(b, 2);
const v3 = new Int16Array(b, 2, 2);

v1.byteLength // 8
v2.byteLength // 6
v3.byteLength // 4

v1.byteOffset // 0
v2.byteOffset // 2
v3.byteOffset // 2
TypedArray.prototype.length

length属性表示 TypedArray 数组含有多少个成员。注意将 length 属性和 byteLength 属性区分,前者是成员长度,后者是字节长度。

const a = new Int16Array(8);

a.length // 8
a.byteLength // 16
TypedArray.prototype.set()

TypedArray数组的set方法用于复制数组(普通数组或 TypedArray 数组),也就是将一段内容完全复制到另一段内存。

const a = new Uint8Array(8);
const b = new Uint8Array(8);

b.set(a);

set方法还可以接受第二个参数,表示从b对象的哪一个成员开始复制a对象。

TypedArray.prototype.subarray()

subarray方法是对于 TypedArray 数组的一部分,再建立一个新的视图。

const a = new Uint16Array(8);
const b = a.subarray(2,3);

a.byteLength // 16
b.byteLength // 2
TypedArray.prototype.slice()

TypeArray 实例的slice方法,可以返回一个指定位置的新的TypedArray实例。

let ui8 = Uint8Array.of(0, 1, 2);
ui8.slice(-1)
// Uint8Array [ 2 ]
TypedArray.of()

TypedArray 数组的所有构造函数,都有一个静态方法of,用于将参数转为一个TypedArray实例。

Float32Array.of(0.151, -8, 3.7)
// Float32Array [ 0.151, -8, 3.7 ]

下面三种方法都会生成同样一个 TypedArray 数组。

// 方法一
let tarr = new Uint8Array([1,2,3]);

// 方法二
let tarr = Uint8Array.of(1,2,3);

// 方法三
let tarr = new Uint8Array(3);
tarr[0] = 1;
tarr[1] = 2;
tarr[2] = 3;
TypedArray.from()

静态方法from接受一个可遍历的数据结构(比如数组)作为参数,返回一个基于这个结构的TypedArray实例。

Uint16Array.from([0, 1, 2])
// Uint16Array [ 0, 1, 2 ]

这个方法还可以将一种TypedArray实例,转为另一种。

const ui16 = Uint16Array.from(Uint8Array.of(0, 1, 2));
ui16 instanceof Uint16Array // true

from方法还可以接受一个函数,作为第二个参数,用来对每个元素进行遍历,功能类似map方法。

Int8Array.of(127, 126, 125).map(x => 2 * x)
// Int8Array [ -2, -4, -6 ]

Int16Array.from(Int8Array.of(127, 126, 125), x => 2 * x)
// Int16Array [ 254, 252, 250 ]

上面的例子中,from方法没有发生溢出,这说明遍历不是针对原来的 8 位整数数组。也就是说,from会将第一个参数指定的 TypedArray 数组,拷贝到另一段内存之中,处理之后再将结果转成指定的数组格式。

复合视图

由于视图的构造函数可以指定起始位置和长度,所以在同一段内存之中,可以依次存放不同类型的数据,这叫做“复合视图”。

const buffer = new ArrayBuffer(24);

const idView = new Uint32Array(buffer, 0, 1);
const usernameView = new Uint8Array(buffer, 4, 16);
const amountDueView = new Float32Array(buffer, 20, 1);

上面代码将一个 24 字节长度的ArrayBuffer对象,分成三个部分:

  • 字节 0 到字节 3:1 个 32 位无符号整数
  • 字节 4 到字节 19:16 个 8 位整数
  • 字节 20 到字节 23:1 个 32 位浮点数

3.2.3 DataView - 视图

如果一段数据包含多种类型,我们还可以使用DataView视图进行操作。

DataView 视图提供 8 个方法写入内存。

dataview.setXXX(byteOffset, value [, littleEndian])

  • byteOffset 偏移量,单位为字节
  • value 设置的数值
  • littleEndian 传入false或undefined表示使用大端字节序

setInt8:写入 1 个字节的 8 位整数。
setUint8:写入 1 个字节的 8 位无符号整数。
setInt16:写入 2 个字节的 16 位整数。
setUint16:写入 2 个字节的 16 位无符号整数。
setInt32:写入 4 个字节的 32 位整数。
setUint32:写入 4 个字节的 32 位无符号整数。
setFloat32:写入 4 个字节的 32 位浮点数。
setFloat64:写入 8 个字节的 64 位浮点数。

相应也有8个方法读取内存:

getInt8:读取 1 个字节,返回一个 8 位整数。
getUint8:读取 1 个字节,返回一个无符号的 8 位整数。
getInt16:读取 2 个字节,返回一个 16 位整数。
getUint16:读取 2 个字节,返回一个无符号的 16 位整数。
getInt32:读取 4 个字节,返回一个 32 位整数。
getUint32:读取 4 个字节,返回一个无符号的 32 位整数。
getFloat32:读取 4 个字节,返回一个 32 位浮点数。
getFloat64:读取 8 个字节,返回一个 64 位浮点数。

下面是表格里是BMP文件的头信息:

Byte描述
2"BM"标记
4文件大小
2保留
2保留
4文件头和位图数据之间的偏移量

我们使用DataView可以这样简单实现:

var buffer = new ArrayBuffer(14)
var view = new DataView(buffer)

view.setUint8(0, 66)     // 写入1字节: 'B'
view.setUint8(1, 67)     // 写入1字节: 'M'
view.setUint32(2, 1234)  // 写入4字节的大小: 1234
view.setUint16(6, 0)     // 写入2字节保留位
view.setUint16(8, 0)     // 写入2字节保留位
view.setUint32(10, 0)    // 写入4字节偏移量

里面对应的结构应该是这样的:

Byte  |    0   |    1   |    2   |    3   |    4   |    5   | ... |
Type  |   I8   |   I8   |                I32                | ... |    
Data  |    B   |    M   |00000000|00000000|00000100|11010010| ... |

回到前面遇到的"BA"问题,我们用DataView重新执行下:

var buffer = new ArrayBuffer(2) 
var view = new DataView(buffer)

var value = (65 << 8) + 66
view.setUint16(0, value)

var blob = new Blob([buffer], {type: 'text/plain'})
var dataUri = window.URL.createObjectURL(blob)
window.open(dataUri) // AB

这下我们得到了正确结果"AB",这个也说明DataView默认使用大端字节序。

参考文章

本文同步发表于作者博客: 图片上传姿势以及你不知道的Typed Arrays

wechat-find-me.png

查看原文

赞 0 收藏 0 评论 0

ineo6 发布了文章 · 2019-10-24

React项目快速搭配eslint,prettier,commitlint,lint-staged

为了实现代码规范,我们在使用中会使用诸多插件,比如eslintprettiercommitlintstylelint等等,在新项目中这样一套组合拳下来,也是稍显繁琐,另外还要定制配置文件,某种程度上来说是体力活。

本文的目的是介绍如何简化配置,统一规范。

1. magic-lint

magic-lint是一款代码规范工具,集检查、美化于一体,能够检查commit信息,通过hook在代码提交时规范代码,里面包含这些:

  • eslint
  • stylelint
  • prettier
  • lint-staged
  • husky
  • commitlint

使用magic-lint之后就不需要单独安装上述插件,可以无门槛使用。

1.1 安装

npm install magic-lint --save-dev

1.2 参数

Usage: magic-lint [options] file.js [file.js] [dir]

# 提交commit触发校验
magic-lint --commit

# 对指定路径 lint
magic-lint --prettier --eslint --stylelint src/

# 只对提交的代码进行 lint
magic-lint --staged --prettier --eslint --stylelint

# 对于某些场景需要指定 lint 工具的子参数
magic-lint --eslint.debug  -s.formatter=json -p.no-semi

Options:
--commit, -C              only check commit msg                               [boolean] [default: false]
--staged, -S              only lint git staged files                          [boolean] [default: false]
--prettier, -p            format code with prettier                           [boolean] [default: false]
--eslint, -e              enable lint javascript                              [boolean] [default: false]
--stylelint, --style, -s  enable lint style                                   [boolean] [default: false]
--fix, -f                 fix all eslint and stylelint auto-fixable problems  [boolean] [default: false]
--quiet, -q               report errors only                                  [boolean] [default: false]
--cwd                     current working directory                           [default: process.cwd()

2. 配置

2.1 基础配置

package.json中添加如:

+ "husky": {
+   "hooks": {
+     "pre-commit": "magic-lint --staged --eslint --stylelint --prettier --fix"",
+     "commit-msg": "magic-lint --commit"
+   }
+ }

2.2 eslint

eslint是一款代码检查工具,使用的时候还需添加具体的配置文件。在React项目中我们一般会使用eslint-config-airbnb

通过执行如下命令可以看到依赖包的版本:

npm info "eslint-config-airbnb@latest" peerDependencies

我们得到如下内容:

{
   eslint: '^5.16.0 || ^6.1.0',
  'eslint-plugin-import': '^2.18.2',
  'eslint-plugin-jsx-a11y': '^6.2.3',
  'eslint-plugin-react': '^7.14.3',
  'eslint-plugin-react-hooks': '^1.7.0'
}

如果使用的npm版本大于4,可以使用下面的命令快速安装依赖,无需手动敲打:

npx install-peerdeps --dev eslint-config-airbnb

安装完成之后在项目根目录创建.eslintrc.js,同样可以使用下面的命令,或者手动创建:

./node_modules/.bin/eslint --init
module.exports = {
    "env": {
        "browser": true,
        "es6": true
    },
    "extends": "airbnb",
    "globals": {
        "Atomics": "readonly",
        "SharedArrayBuffer": "readonly"
    },
    "parserOptions": {
        "ecmaFeatures": {
            "jsx": true
        },
        "ecmaVersion": 2018,
        "sourceType": "module"
    },
    "plugins": [
        "react"
    ],
    "rules": {
    }
};

eslint-config-airbnb本质是eslint配置的定制合集,其实我们也可以根据自身情况维护一套配置,这样在协作中的项目可以统一配置,避免配置的来回复制。

2.3 prettier

prettiereslint需要搭配使用,使用prettier能让我们在保存或者提交代码时格式化代码,避免不同编辑器、开发环境导致的格式问题。

prettier的配置不多,具体的配置介绍可以看下面的介绍,大家结合eslint的规则配置即可。

这里我们使用.prettierrc.js配置方式。

module.exports = {
  // 一行最多 150 字符
  printWidth: 150,
  // 使用 4 个空格缩进
  tabWidth: 4,
  // 不使用缩进符,而使用空格
  useTabs: false,
  // 行尾需要有分号
  semi: true,
  // 使用单引号
  singleQuote: true,
  // 对象的 key 仅在必要时用引号
  quoteProps: 'as-needed',
  // jsx 不使用单引号,而使用双引号
  jsxSingleQuote: false,
  // 末尾是否需要逗号
  trailingComma: 'es5',
  // 大括号内的首尾需要空格
  bracketSpacing: true,
  // jsx 标签的反尖括号需要换行
  jsxBracketSameLine: false,
  // 箭头函数,只有一个参数的时候,也需要括号
  arrowParens: 'always',
  // 每个文件格式化的范围是文件的全部内容
  rangeStart: 0,
  rangeEnd: Infinity,
  // 不需要写文件开头的 @prettier
  requirePragma: false,
  // 不需要自动在文件开头插入 @prettier
  insertPragma: false,
  // 使用默认的折行标准
  proseWrap: 'preserve',
  // 根据显示样式决定 html 要不要折行
  htmlWhitespaceSensitivity: 'css',
  // 换行符使用 lf
  endOfLine: 'lf',
};

这里同样也有排除文件.prettierignore,语法规则和.gitignore一样。

2.4 stylelint

stylelint是一款css代码规范工具,magic-lint里面已经预置了一些配置和插件:

  • stylelint-config-css-modules
  • stylelint-config-prettier
  • stylelint-config-rational-order
  • stylelint-config-standard
  • stylelint-declaration-block-no-ignored-properties
  • stylelint-order

配置文件可以命名.stylelintrc.json,填充如下内容:

{
  "extends": ["stylelint-config-standard", "stylelint-config-css-modules", "stylelint-config-rational-order", "stylelint-config-prettier"],
  "plugins": ["stylelint-order", "stylelint-declaration-block-no-ignored-properties"],
  "rules": {
    "no-descending-specificity": null,
    "plugin/declaration-block-no-ignored-properties": true
  }
}

忽略文件的名称是.stylelintignore,遵循.gitignore语法。

2.5 commitlint

commitlint是一款校验commit提交信息的工具,它可以让我们的提交信息更规范、更有可读性,甚至可以基于提交自动生成changelog

commit的格式要求如下,这段内容同样也可以直接用到git提交模板:

Type(<scope>): <subject>

<body>

<footer>

# Type 字段包含:
#  feat:新功能(feature)
#  fix:修补bug
#  docs:文档(documentation)
#  style: 格式(不影响代码运行的变动)
#  refactor:重构(即不是新增功能,也不是修改bug的代码变动)
#  test:增加测试
#  chore:构建过程或辅助工具的变动
# scope用于说明 commit 影响的范围,比如数据层、控制层、视图层等等。
# subject是 commit 目的的简短描述,不超过50个字符
# Body 部分是对本次 commit 的详细描述,可以分成多行
# Footer用来关闭 Issue或以BREAKING CHANGE开头,后面是对变动的描述、
#  以及变动理由和迁移方法

例子:

git commit -m 'feat: 增加用户搜搜功能'
git commit -m 'fix: 修复用户检测无效的问题'

magic-lint已经内置@commitlint/config-conventional配置方案,它里面包含了以下几个type:

'build',
'ci',
'chore',
'docs',
'feat',
'fix',
'perf',
'refactor',
'revert',
'style',
'test'

3. 写在最后

在前端开发中,需要配置的内容太多太多了,大把的时间花在配置上就真的变成了"前端配置师"。

可能这也是我们都愿意造轮子的原因了,不过最终在工作中能起到作用的轮子就是好轮子。同样如果只是使用别人的轮子,自己又如何能成长呢!

本文同步发表于作者博客: React项目快速搭配eslint,prettier,commitlint,lint-staged

wechat-find-me.png

查看原文

赞 0 收藏 0 评论 0

ineo6 回答了问题 · 2019-10-21

请问wordpress插件的LOGO以及banner在哪里提交

在后台没有提交的入口,需要把文件放到插件svn目录的assets文件中。

可以参考下如何在 WordPress 插件页面展示banner图片

关注 1 回答 1

ineo6 收藏了文章 · 2019-10-10

浏览器事件模型中捕获阶段、目标阶段、冒泡阶段实例详解

如果对事件大概了解,可能知道有事件冒泡这回事,但是冒泡、捕获、传播这些机制可能还没有深入的研究实践一下,我抽时间整理了一下相关的知识。

  • 本文主要对事件机制一些细节进行讨论,过于基础的事件绑定知识方法没有介绍。
  • 特别少的篇幅关注浏览器兼容问题,毕竟原理了解了,兼容性问题可以自己想办法解决了。

在浏览器相对标准化之前,各个浏览器厂商都是自己实现的事件模型,有的用了冒泡,有的用了捕获,W3C为了兼顾之前的标准,将事件发生定义成如下三个阶段:

1、捕获阶段
2、目标阶段
3、冒泡阶段

只是硬生生的说事件机制到底是怎么回事不容易理解,用一个demo为主线说明事件的原理比较容易理解:

HTML

<body>
    <div id="wrapDiv">wrapDiv
        <p id="innerP">innerP
            <span id="textSpan">textSpan</span>
        </p>
    </div>
</body>

CSS

<style>
    #wrapDiv, #innerP, #textSpan{
        margin: 5px;
        padding: 5px;
        box-sizing: border-box;
        cursor: default;
    }
    #wrapDiv{
        width: 300px;
        height: 300px;
        border: indianred 3px solid;
    }
    #innerP{
        width: 200px;
        height: 200px;
        border: hotpink 3px solid;
    }
    #textSpan{
        display: block;
        width: 100px;
        height: 100px;
        border: orange 3px solid;
    }
</style>

JavaScript

<script>
    var wrapDiv = document.getElementById("wrapDiv");
    var innerP = document.getElementById("innerP");
    var textSpan = document.getElementById("textSpan");

    // 捕获阶段绑定事件
    window.addEventListener("click", function(e){
        console.log("window 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.addEventListener("click", function(e){
        console.log("document 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.body.addEventListener("click", function(e){
        console.log("body 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    innerP.addEventListener("click", function(e){
        console.log("innerP 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    textSpan.addEventListener("click", function(e){
        console.log("textSpan 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    // 冒泡阶段绑定的事件
    window.addEventListener("click", function(e){
        console.log("window 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.addEventListener("click", function(e){
        console.log("document 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.body.addEventListener("click", function(e){
        console.log("body 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    innerP.addEventListener("click", function(e){
        console.log("innerP 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    textSpan.addEventListener("click", function(e){
        console.log("textSpan 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);
</script>

demo页面效果图
图片描述

这个时候,如果点击一下textSpan这个元素,控制台会打印出这样的内容:
图片描述

当按下鼠标点击后,到底发生了什么的,现在我基于上面的例子来说一下:

capture=>start: 捕获阶段开始
window=>operation: window
document=>operation: document
documentElement=>operation: documentElement
body=>operation: body
wrapDiv=>operation: wrapDiv
innerP=>operation: innerP
target=>start: 捕获阶段结束,目标阶段开始
textSpan=>operation: textSpan
textSpan2=>operation: textSpan
bubble=>start: 目标阶段结束,冒泡阶段开始
innerP2=>operation: innerP
wrapDiv2=>operation: wrapDiv
body2=>operation: body
documentElement2=>operation: documentElement
document2=>operation: document
window2=>operation: window
bubbleend=>start: 冒泡阶段结束
capture->window->document->documentElement->body->wrapDiv->innerP->target->textSpan->textSpan2->bubble->innerP2->wrapDiv2->body2->documentElement2->document2->window2->bubbleend

从上面所画的事件传播的过程能够看出来,当点击鼠标后,会先发生事件的捕获

  • 捕获阶段:首先window会获捕获到事件,之后documentdocumentElementbody会捕获到,再之后就是在body中DOM元素一层一层的捕获到事件,有wrapDivinnerP
  • 目标阶段:真正点击的元素textSpan的事件发生了两次,因为在上面的JavaScript代码中,textSapn既在捕获阶段绑定了事件,又在冒泡阶段绑定了事件,所以发生了两次。但是这里有一点是需要注意,在目标阶段并不一定先发生在捕获阶段所绑定的事件,而是先绑定的事件发生,一会会解释一下。
  • 冒泡阶段:会和捕获阶段相反的步骤将事件一步一步的冒泡到window

那可能有一个疑问,我们不用addEventListener绑定的事件会发生在哪个阶段呢,我们来一个测试,顺便再演示一下我在上面的目标阶段所说的目标阶段并不一定先发生捕获阶段所绑定的事件是怎么一回事。
我们重新改一下JavaScript代码:

<script>
    var wrapDiv = document.getElementById("wrapDiv");
    var innerP = document.getElementById("innerP");
    var textSpan = document.getElementById("textSpan");

    // 测试直接绑定的事件到底发生在哪个阶段
    wrapDiv.onclick = function(){
        console.log("wrapDiv onclick 测试直接绑定的事件到底发生在哪个阶段")
    };

    // 捕获阶段绑定事件
    window.addEventListener("click", function(e){
        console.log("window 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.addEventListener("click", function(e){
        console.log("document 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.body.addEventListener("click", function(e){
        console.log("body 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    innerP.addEventListener("click", function(e){
        console.log("innerP 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    textSpan.addEventListener("click", function(){
        console.log("textSpan 冒泡 在捕获之前绑定的")
    }, false);

    textSpan.onclick = function(){
        console.log("textSpan onclick")
    };

    textSpan.addEventListener("click", function(e){
        console.log("textSpan 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    // 冒泡阶段绑定的事件
    window.addEventListener("click", function(e){
        console.log("window 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.addEventListener("click", function(e){
        console.log("document 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.body.addEventListener("click", function(e){
        console.log("body 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    innerP.addEventListener("click", function(e){
        console.log("innerP 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    textSpan.addEventListener("click", function(e){
        console.log("textSpan 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);
</script>

再看控制台的结果:
图片描述

  • 图中第一个被圈出来的解释:textSpan是被点击的元素,也就是目标元素,所有在textSpan上绑定的事件都会发生在目标阶段,在绑定捕获代码之前写了绑定的冒泡阶段的代码,所以在目标元素上就不会遵守先发生捕获后发生冒泡这一规则,而是先绑定的事件先发生。
  • 图中第二个被圈出来的解释:由于wrapDiv不是目标元素,所以它上面绑定的事件会遵守先发生捕获后发生冒泡的规则。所以很明显用onclick直接绑定的事件发生在了冒泡阶段。

target和currentTarget

上面的代码中写了e.targete.currentTarget,还没有说是什么,targetcurrentTarget都是event上面的属性,target是真正发生事件的DOM元素,而currentTarget是当前事件发生在哪个DOM元素上。
可以结合控制台打印出来的信息理解下,目标阶段也就是 target == currentTarget的时候。我没有打印它们两个因为太长了,所以打印了它们的nodeName,但是由于window没有nodeName这个属性,所以是undefined

阻止事件传播

说到事件,一定要说的是如何阻止事件传播。总是有很多帖子说e.stopPropagation()是阻止事件的冒泡的传播,实际上这么说并不是很准确,因为它不仅可以阻止事件在冒泡阶段的传播,还能阻止事件在捕获阶段的传播。
来看一下我们再改一下的JavaScript代码:

<script>
    var wrapDiv = document.getElementById("wrapDiv");
    var innerP = document.getElementById("innerP");
    var textSpan = document.getElementById("textSpan");

    // 测试直接绑定的事件到底发生在哪个阶段
    wrapDiv.onclick = function(){
        console.log("wrapDiv onclick 测试直接绑定的事件到底发生在哪个阶段")
    };

    // 捕获阶段绑定事件
    window.addEventListener("click", function(e){
        console.log("window 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.addEventListener("click", function(e){
        console.log("document 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.body.addEventListener("click", function(e){
        console.log("body 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv 捕获", e.target.nodeName, e.currentTarget.nodeName);
        // 在捕获阶段阻止事件的传播
        e.stopPropagation();
    }, true);

    innerP.addEventListener("click", function(e){
        console.log("innerP 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    textSpan.addEventListener("click", function(){
        console.log("textSpan 冒泡 在捕获之前绑定的")
    }, false);

    textSpan.onclick = function(){
        console.log("textSpan onclick")
    };

    textSpan.addEventListener("click", function(e){
        console.log("textSpan 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    // 冒泡阶段绑定的事件
    window.addEventListener("click", function(e){
        console.log("window 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.addEventListener("click", function(e){
        console.log("document 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.body.addEventListener("click", function(e){
        console.log("body 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    innerP.addEventListener("click", function(e){
        console.log("innerP 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    textSpan.addEventListener("click", function(e){
        console.log("textSpan 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);
</script>

我们在事件的捕获阶段阻止了传播,看一下控制台的结果:
图片描述
实际上我们点击的是textSpan,但是由于在捕获阶段事件就被阻止了传播,所以在textSpan上绑定的事件根本就没有发生,冒泡阶段绑定的事件自然也不会发生,因为阻止事件在捕获阶段传播的特性,e.stopPropagation()很少用到在捕获阶段去阻止事件的传播,大家就以为e.stopPropagation()只能阻止事件在冒泡阶段传播。

阻止事件的默认行为

e.preventDefault()可以阻止事件的默认行为发生,默认行为是指:点击a标签就转跳到其他页面、拖拽一个图片到浏览器会自动打开、点击表单的提交按钮会提交表单等等,因为有的时候我们并不希望发生这些事情,所以需要阻止默认行为,这块的知识比较简单,可以自己去试一下。

与事件相关的兼容性问题

这里只是简单提一下兼容性问题,不做过多的展开。对于绑定事件,ie低版本的浏览器是用attachEvent,而高版本ie和标准浏览器用的是addEventListenerattachEvent不能指定绑定事件发生在捕获阶段还是冒泡阶段,它只能将事件绑定到冒泡阶段,但是并不意味这低版本的ie没有事件捕获,它也是先发生事件捕获,再发生事件冒泡,只不过这个过程无法通过程序控制。

其实事件的兼容性问题特别的多,比如获取事件对象的方式、绑定和解除绑定事件的方式、目标元素的获取方式等等,由于古老的浏览器终究会被淘汰,不过多展开了。

欢迎关注【本期节目】,微信公众号ID:benqijiemu。
这里有:互联网思考、软件&工具推荐、前端技术等。
可以在公众号回复我,希望和大家一起交流所有与互联网相关的事情~
图片描述

查看原文

ineo6 赞了文章 · 2019-10-10

浏览器事件模型中捕获阶段、目标阶段、冒泡阶段实例详解

如果对事件大概了解,可能知道有事件冒泡这回事,但是冒泡、捕获、传播这些机制可能还没有深入的研究实践一下,我抽时间整理了一下相关的知识。

  • 本文主要对事件机制一些细节进行讨论,过于基础的事件绑定知识方法没有介绍。
  • 特别少的篇幅关注浏览器兼容问题,毕竟原理了解了,兼容性问题可以自己想办法解决了。

在浏览器相对标准化之前,各个浏览器厂商都是自己实现的事件模型,有的用了冒泡,有的用了捕获,W3C为了兼顾之前的标准,将事件发生定义成如下三个阶段:

1、捕获阶段
2、目标阶段
3、冒泡阶段

只是硬生生的说事件机制到底是怎么回事不容易理解,用一个demo为主线说明事件的原理比较容易理解:

HTML

<body>
    <div id="wrapDiv">wrapDiv
        <p id="innerP">innerP
            <span id="textSpan">textSpan</span>
        </p>
    </div>
</body>

CSS

<style>
    #wrapDiv, #innerP, #textSpan{
        margin: 5px;
        padding: 5px;
        box-sizing: border-box;
        cursor: default;
    }
    #wrapDiv{
        width: 300px;
        height: 300px;
        border: indianred 3px solid;
    }
    #innerP{
        width: 200px;
        height: 200px;
        border: hotpink 3px solid;
    }
    #textSpan{
        display: block;
        width: 100px;
        height: 100px;
        border: orange 3px solid;
    }
</style>

JavaScript

<script>
    var wrapDiv = document.getElementById("wrapDiv");
    var innerP = document.getElementById("innerP");
    var textSpan = document.getElementById("textSpan");

    // 捕获阶段绑定事件
    window.addEventListener("click", function(e){
        console.log("window 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.addEventListener("click", function(e){
        console.log("document 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.body.addEventListener("click", function(e){
        console.log("body 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    innerP.addEventListener("click", function(e){
        console.log("innerP 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    textSpan.addEventListener("click", function(e){
        console.log("textSpan 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    // 冒泡阶段绑定的事件
    window.addEventListener("click", function(e){
        console.log("window 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.addEventListener("click", function(e){
        console.log("document 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.body.addEventListener("click", function(e){
        console.log("body 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    innerP.addEventListener("click", function(e){
        console.log("innerP 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    textSpan.addEventListener("click", function(e){
        console.log("textSpan 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);
</script>

demo页面效果图
图片描述

这个时候,如果点击一下textSpan这个元素,控制台会打印出这样的内容:
图片描述

当按下鼠标点击后,到底发生了什么的,现在我基于上面的例子来说一下:

capture=>start: 捕获阶段开始
window=>operation: window
document=>operation: document
documentElement=>operation: documentElement
body=>operation: body
wrapDiv=>operation: wrapDiv
innerP=>operation: innerP
target=>start: 捕获阶段结束,目标阶段开始
textSpan=>operation: textSpan
textSpan2=>operation: textSpan
bubble=>start: 目标阶段结束,冒泡阶段开始
innerP2=>operation: innerP
wrapDiv2=>operation: wrapDiv
body2=>operation: body
documentElement2=>operation: documentElement
document2=>operation: document
window2=>operation: window
bubbleend=>start: 冒泡阶段结束
capture->window->document->documentElement->body->wrapDiv->innerP->target->textSpan->textSpan2->bubble->innerP2->wrapDiv2->body2->documentElement2->document2->window2->bubbleend

从上面所画的事件传播的过程能够看出来,当点击鼠标后,会先发生事件的捕获

  • 捕获阶段:首先window会获捕获到事件,之后documentdocumentElementbody会捕获到,再之后就是在body中DOM元素一层一层的捕获到事件,有wrapDivinnerP
  • 目标阶段:真正点击的元素textSpan的事件发生了两次,因为在上面的JavaScript代码中,textSapn既在捕获阶段绑定了事件,又在冒泡阶段绑定了事件,所以发生了两次。但是这里有一点是需要注意,在目标阶段并不一定先发生在捕获阶段所绑定的事件,而是先绑定的事件发生,一会会解释一下。
  • 冒泡阶段:会和捕获阶段相反的步骤将事件一步一步的冒泡到window

那可能有一个疑问,我们不用addEventListener绑定的事件会发生在哪个阶段呢,我们来一个测试,顺便再演示一下我在上面的目标阶段所说的目标阶段并不一定先发生捕获阶段所绑定的事件是怎么一回事。
我们重新改一下JavaScript代码:

<script>
    var wrapDiv = document.getElementById("wrapDiv");
    var innerP = document.getElementById("innerP");
    var textSpan = document.getElementById("textSpan");

    // 测试直接绑定的事件到底发生在哪个阶段
    wrapDiv.onclick = function(){
        console.log("wrapDiv onclick 测试直接绑定的事件到底发生在哪个阶段")
    };

    // 捕获阶段绑定事件
    window.addEventListener("click", function(e){
        console.log("window 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.addEventListener("click", function(e){
        console.log("document 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.body.addEventListener("click", function(e){
        console.log("body 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    innerP.addEventListener("click", function(e){
        console.log("innerP 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    textSpan.addEventListener("click", function(){
        console.log("textSpan 冒泡 在捕获之前绑定的")
    }, false);

    textSpan.onclick = function(){
        console.log("textSpan onclick")
    };

    textSpan.addEventListener("click", function(e){
        console.log("textSpan 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    // 冒泡阶段绑定的事件
    window.addEventListener("click", function(e){
        console.log("window 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.addEventListener("click", function(e){
        console.log("document 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.body.addEventListener("click", function(e){
        console.log("body 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    innerP.addEventListener("click", function(e){
        console.log("innerP 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    textSpan.addEventListener("click", function(e){
        console.log("textSpan 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);
</script>

再看控制台的结果:
图片描述

  • 图中第一个被圈出来的解释:textSpan是被点击的元素,也就是目标元素,所有在textSpan上绑定的事件都会发生在目标阶段,在绑定捕获代码之前写了绑定的冒泡阶段的代码,所以在目标元素上就不会遵守先发生捕获后发生冒泡这一规则,而是先绑定的事件先发生。
  • 图中第二个被圈出来的解释:由于wrapDiv不是目标元素,所以它上面绑定的事件会遵守先发生捕获后发生冒泡的规则。所以很明显用onclick直接绑定的事件发生在了冒泡阶段。

target和currentTarget

上面的代码中写了e.targete.currentTarget,还没有说是什么,targetcurrentTarget都是event上面的属性,target是真正发生事件的DOM元素,而currentTarget是当前事件发生在哪个DOM元素上。
可以结合控制台打印出来的信息理解下,目标阶段也就是 target == currentTarget的时候。我没有打印它们两个因为太长了,所以打印了它们的nodeName,但是由于window没有nodeName这个属性,所以是undefined

阻止事件传播

说到事件,一定要说的是如何阻止事件传播。总是有很多帖子说e.stopPropagation()是阻止事件的冒泡的传播,实际上这么说并不是很准确,因为它不仅可以阻止事件在冒泡阶段的传播,还能阻止事件在捕获阶段的传播。
来看一下我们再改一下的JavaScript代码:

<script>
    var wrapDiv = document.getElementById("wrapDiv");
    var innerP = document.getElementById("innerP");
    var textSpan = document.getElementById("textSpan");

    // 测试直接绑定的事件到底发生在哪个阶段
    wrapDiv.onclick = function(){
        console.log("wrapDiv onclick 测试直接绑定的事件到底发生在哪个阶段")
    };

    // 捕获阶段绑定事件
    window.addEventListener("click", function(e){
        console.log("window 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.addEventListener("click", function(e){
        console.log("document 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.body.addEventListener("click", function(e){
        console.log("body 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv 捕获", e.target.nodeName, e.currentTarget.nodeName);
        // 在捕获阶段阻止事件的传播
        e.stopPropagation();
    }, true);

    innerP.addEventListener("click", function(e){
        console.log("innerP 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    textSpan.addEventListener("click", function(){
        console.log("textSpan 冒泡 在捕获之前绑定的")
    }, false);

    textSpan.onclick = function(){
        console.log("textSpan onclick")
    };

    textSpan.addEventListener("click", function(e){
        console.log("textSpan 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    // 冒泡阶段绑定的事件
    window.addEventListener("click", function(e){
        console.log("window 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.addEventListener("click", function(e){
        console.log("document 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.body.addEventListener("click", function(e){
        console.log("body 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    innerP.addEventListener("click", function(e){
        console.log("innerP 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    textSpan.addEventListener("click", function(e){
        console.log("textSpan 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);
</script>

我们在事件的捕获阶段阻止了传播,看一下控制台的结果:
图片描述
实际上我们点击的是textSpan,但是由于在捕获阶段事件就被阻止了传播,所以在textSpan上绑定的事件根本就没有发生,冒泡阶段绑定的事件自然也不会发生,因为阻止事件在捕获阶段传播的特性,e.stopPropagation()很少用到在捕获阶段去阻止事件的传播,大家就以为e.stopPropagation()只能阻止事件在冒泡阶段传播。

阻止事件的默认行为

e.preventDefault()可以阻止事件的默认行为发生,默认行为是指:点击a标签就转跳到其他页面、拖拽一个图片到浏览器会自动打开、点击表单的提交按钮会提交表单等等,因为有的时候我们并不希望发生这些事情,所以需要阻止默认行为,这块的知识比较简单,可以自己去试一下。

与事件相关的兼容性问题

这里只是简单提一下兼容性问题,不做过多的展开。对于绑定事件,ie低版本的浏览器是用attachEvent,而高版本ie和标准浏览器用的是addEventListenerattachEvent不能指定绑定事件发生在捕获阶段还是冒泡阶段,它只能将事件绑定到冒泡阶段,但是并不意味这低版本的ie没有事件捕获,它也是先发生事件捕获,再发生事件冒泡,只不过这个过程无法通过程序控制。

其实事件的兼容性问题特别的多,比如获取事件对象的方式、绑定和解除绑定事件的方式、目标元素的获取方式等等,由于古老的浏览器终究会被淘汰,不过多展开了。

欢迎关注【本期节目】,微信公众号ID:benqijiemu。
这里有:互联网思考、软件&工具推荐、前端技术等。
可以在公众号回复我,希望和大家一起交流所有与互联网相关的事情~
图片描述

查看原文

赞 27 收藏 99 评论 5

ineo6 发布了文章 · 2019-10-04

React Native 和 Jenkins 不得不说的二三事

app开发和测试过程中我们都会执行npm start命令来启动服务,只是这样还是很繁琐,我们需要人工介入才能发布代码,本篇文章的目的就是介绍如何使用jenkins让我们的项目自动化。

1. 项目准备

原理是打包bundle,然后把文件放到服务器上。

react-native打包文件结果包含bundle文件和图片资源文件。

如果app加载本地bundle或者连接本地开发服务,图片资源是可以正常访问的,但是如果访问的是多级目录地址,例如http://www.xxx.com/awe/bundle.js,图片的地址只会从域名http://www.xxx.com查找,最终导致图片无法显示。

这是因为Image组件加载逻辑不支持多级目录,感兴趣可以查看React Native 图片资源那些事了解详情。

解决方案是Image/resolveAssetSource提供的setCustomSourceTransformer,它可以让我们决定图片的处理逻辑。

我们把serverUrl替换为jsbundleUrl,这样图片就会从bundle同级查找到图片。

如果有其他需求,可以参阅react-native/Libraries/Image/resolveAssetSource中相关代码,自行设置setCustomSourceTransformer

import React from 'react';
import { AppRegistry, Platform } from 'react-native';
import { setCustomSourceTransformer } from 'react-native/Libraries/Image/resolveAssetSource';

import Entry from './src/entry';

if (process.env.NODE_ENV_REAL === "test") {
  // 定制资源的获取方式
  // 由域名根目录调整为bundle所在目录
  // 安卓使用drawable格式
  setCustomSourceTransformer((resolver) => {
    resolver.serverUrl = resolver.jsbundleUrl;

    if (Platform.OS === "android") {
      return resolver.drawableFolderInBundle();
    }

    return resolver.defaultAsset();
  });
}

AppRegistry.registerComponent('awe', () => Entry);

2. jenkins配置

2.1 创建任务

点击"新建任务"开始填写任务信息,输入任务名称并选择"构建一个自由风格的软件项目"。

clipboard.png

2.2 源码管理

"源码管理"中配置项目代码,以Git为例,需要配置两个参数:

  • Repositories 仓库地址和认证方式
  • Branches to build 分支,我们填入$branch,指定为自定义构建参数

clipboard.png

2.3 添加任务参数

在"General"中勾选"参数化构建过程",然后点击"添加参数",在候选列表中选择Git Parameter,配置以下两项:

  • Name: 可访问到的变量名称,如配置为branch后可以通过$branch拿到值
  • Parameter Type:选择Branch,也可以根据情况配置其他选项

这样在前面"源码管理"中配置的$branch就可以访问到仓库的所有分支。

clipboard.png

以及一个选项参数:env(指定目标环境)。候选数据一行一条记录输入即可。

clipboard.png

2.4 添加构建执行Shell

在"构建"中点击"增加构建步骤"按钮,在候选列表中选择"执行 shell"。

下面是一段简单判断env执行不同脚本的代码:

#!/bin/bash

echo -------------------------------------------------------
echo 环境: ${env}
echo -------------------------------------------------------
# 准备工作

yarn install

if [ "$env" == "dev" ]
then
  yarn run bundle-android
  yarn run bundle-ios

elif [ "$env" == "prod" ]
then
  yarn run prod-android
  yarn run prod-ios
fi

2.5 部署和使用

最后大家把生成的bundle文件连同资源文件部署到服务器,app中填写对应的地址。

另外需要注意:

  1. 取消"Use JS Deltas"
  2. 可能需要对bundle设置mime-type
  3. react-native bundle存在缓存问题,使用--reset-cache参数清除

写在最后

至此我们可以安心通过jenkins来发布,其实一开始就有这样做的想法,不过一直迟迟未动,如今总算是实践完成并写了这篇文章。

算上我前面发布的微信小程序和Jenkins不得不说的二三事,这已经是第二篇关于jenkins自动化的文章,有机会我会再写相关实践。

本文同步发表于作者博客: React Native 和 Jenkins 不得不说的二三事
查看原文

赞 0 收藏 0 评论 0

ineo6 回答了问题 · 2019-09-27

如何将URL转换为file对象?

读取url图片获取到base64字符串,最后转为file就可以提交了,具体可以参考我写的示例里面有转换的核心代码:https://codepen.io/ineo6/pen/...

关注 3 回答 2

ineo6 发布了文章 · 2019-09-26

React Native 图片资源那些事

react-native中使用Image组件来显示图片,表面上和htmlimg标签大同小异,但是其source属性包含的逻辑缺复杂的多,同时也和bundle运行的方式也有关系。

本篇文章将重点讲解下Image中图片解析逻辑,以及如何自定义图片解析逻辑。

1. 打包结构

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

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

首先看下iOSandroid打包结果:

clipboard.png

iOS会按照项目结构输出图片资源到我们制定的目录assets下。

android中,drawable-mdpidrawable-xhdpidrawable-xxhdpi等存放不同分辨率屏幕下的图片,文件名的组成是目录和图片名称通过_拼接。

2. 图片链接生成逻辑

代码位于react-native/Libraries/Image/resolveAssetSource

resolveAssetSource.js最终会export以下内容:

module.exports = resolveAssetSource;
module.exports.pickScale = AssetSourceResolver.pickScale;
module.exports.setCustomSourceTransformer = setCustomSourceTransformer;
  • resolveAssetSource: 图片地址拼接工具
  • pickScale: 像素比工具
  • setCustomSourceTransformer: 自定义图片链接处理方式

这里的重点是resolveAssetSource,它会处理Imagesource,并返回图片地址。

创建了AssetSourceResolver,并传入getDevServerURL()getScriptURL()asset

如果存在自定义处理函数_customSourceTransformer,就返回它的执行结果。它的设置就是通过setCustomSourceTransformer来完成的。

否则就调用resolver.defaultAsset,使用默认的逻辑处理图片。

/**
 * `source` is either a number (opaque type returned by require('./foo.png'))
 * or an `ImageSource` like { uri: '<http location || file path>' }
 */
function resolveAssetSource(source: any): ?ResolvedAssetSource {
  if (typeof source === 'object') {
    return source;
  }

  const asset = AssetRegistry.getAssetByID(source);
  if (!asset) {
    return null;
  }

  const resolver = new AssetSourceResolver(
    getDevServerURL(),
    getScriptURL(),
    asset,
  );
  if (_customSourceTransformer) {
    return _customSourceTransformer(resolver);
  }
  return resolver.defaultAsset();
}

接下来看AssetSourceResolver.js的代码。

我们前文初始化AssetSourceResolver,设置了三个参数:

  • serverUrl: 服务地址,格式为"http://www.xxx.com"
  • jsbundleUrl: bundle所在位置
  • asset

里面包含了最终返回图片的逻辑:defaultAsset,我们分析之后可以得到:

bundle放在server

通过如下代码拼接图片地址,这里使用serverUrl要求bundle文件和图片在同级目录并且在域名下,中间不能有二级目录。

    this.fromSource(
      this.serverUrl +
        getScaledAssetPath(this.asset) +
        '?platform=' +
        Platform.OS +
        '&hash=' +
        this.asset.hash,
    );
解决方案是通过setCustomSourceTransformer替换serverUrl,改为jsbundleUrl

bundle内置在app

这里不同平台的处理方式又不一样。

iOS从资源中加载图片

android分为两种:资源和文件系统(file://)

class AssetSourceResolver {
  serverUrl: ?string;
  // where the jsbundle is being run from
  jsbundleUrl: ?string;
  // the asset to resolve
  asset: PackagerAsset;

  constructor(serverUrl: ?string, jsbundleUrl: ?string, asset: PackagerAsset) {
    this.serverUrl = serverUrl;
    this.jsbundleUrl = jsbundleUrl;
    this.asset = asset;
  }
  
  ...
  
  defaultAsset(): ResolvedAssetSource {
    if (this.isLoadedFromServer()) {
      return this.assetServerURL();
    }

    if (Platform.OS === 'android') {
      return this.isLoadedFromFileSystem()
        ? this.drawableFolderInBundle()
        : this.resourceIdentifierWithoutScale();
    } else {
      return this.scaledAssetURLNearBundle();
    }
  }

流程图

3. 写在结尾

我们了解Image组件的图片逻辑之后,就可以按需调整了,通过调用setCustomSourceTransformer传入自定义函数来控制最终图片的访问地址。

我在项目中的处理是bundle部署在服务器上,这种方式会有两个问题:

  1. 图片资源是从域名开始查找,放置在多级目录后就无法访问到图片
  2. 安卓跳过了drawable-x目录

上面的问题都是图片无法显示,不知道看到文章的你是否也想到了解决办法?

本文同步发表于作者博客: React Native 图片资源那些事
查看原文

赞 0 收藏 0 评论 0

ineo6 发布了文章 · 2019-09-23

React Native 混合开发多入口加载方式

在已有app混合开发时,可能会有多个rn界面入口的需求,这个时候我们可以使用RCTRootView中的moduleNameinitialProperties来实现加载包中的不同页面。

目前使用RCTRootView有两种方式:

  • 使用initialProperties传入props属性,在React中读取属性,通过逻辑来渲染不同的Component
  • 配置moduleName,然后AppRegistry.registerComponent注册同名的页面入口

这里贴出使用0.60.5版本中ios项目的代码片段:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];
  RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
                        moduleName:@"AwesomeProject"
                        initialProperties: @{
                           @"screenProps" : @{
                               @"initialRouteName" : @"Home",
                               },
                           }];

  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;
}

initialProperties

这种方式简单使用可以通过state判断切换界面,不过项目使用中还是需要react-navigation这样的导航组件搭配使用,下面贴出的代码就是结合路由的实现方案。

screenPropsreact-navigation中专门用于传递给React组件数据的属性,createAppContainer创建的组件接受该参数screenProps,并传给访问的路由页面。

class App extends React.Component {
    render() {
        const { screenProps } = this.props;

        const stack = createStackNavigator({
            Home: {
                screen: HomeScreen,
            },
            Chat: {
                screen: ChatScreen,
            },
        }, {
            initialRouteName: screenProps.initialRouteName || 'Home',
        });

        const AppContainer = createAppContainer(stack);

        return (
            <AppContainer
                screenProps
            />
        );
    }
}

moduleName

我们按照下面代码注册多个页面入口之后,就可以在原生代码中指定moduleName等于AwesomeProject或者AwesomeProject2来加载不同页面。

AppRegistry.registerComponent("AwesomeProject", () => App);
AppRegistry.registerComponent("AwesomeProject2", () => App2);
本文同步发表于作者博客: React Native 混合开发多入口加载方式
查看原文

赞 0 收藏 0 评论 0

ineo6 回答了问题 · 2019-09-16

vue引入高德地图,刚进入页面的时候会闪一下(红色),只在加载第一次的时候出现

首先是否能提供一个可以重现的示例,在 https://codepen.io/jasonamart... 上面可以直接展示。

  1. 加载两次的原因猜测是因为第一次的init回调没有调用到,可以直接createScript两个js。

也可以参考http://vue-gaode.rxshc.com/的用法。

  1. 红色闪一下的问题

这个从提供的信息没看出问题,猜测是SimpleMarker可能有什么需要设置的,也可以关注下控制台是否有报错。如果提供了可重现的地址是最好的排查方式。

关注 2 回答 2

ineo6 回答了问题 · 2019-09-12

WordPress wp-admin目录下所有的东西都能工作 但是index.php不能访问

首先权限不是越大越好,nginx要能有权限访问到wordpress目录才行。你这个应该有两个问题:

  1. nginx中配置index.php默认带上

可以参考https://www.cnblogs.com/jiqin...

  1. nginx执行用户要和wordpress文件权限所有者用户一致

首先可以在nginx.conf文件头部设置的user是什么,或者通过命令ps axu|grep nginx查看nginx: worker process对应一行,最左侧一列显示的是什么。

wordpress目录执行ls -l可以看到如下位置就是文件权限所拥有的用户。
clipboard.png

假设我们看到的是www,可以执行chown -R www:www ./wordpress修改目录权限。


猜测大概的原因是这样,中间设置权限以及nginx配置也可以多百度下。

关注 3 回答 2

ineo6 发布了文章 · 2019-09-12

linux系统文件权限简明介绍

本文主要介绍在linux系统下文件权限配置,通过阅读该文,你会了解文件权限,同时能正确的配置文件权限,避免盲目操作。成文的原因也是因为自己在程序执行过程中一直会遇到这样的问题,所以最终专门整理了相关知识,希望也能帮到大家。

1. ls -l命令讲解

我们可以通过ls -l命令查看除了文件名称外的其他信息,比如文件型态、权限、拥有者、文件大小等。

这里可以看来自网上的一幅图。

clipboard.png

2. 如何设置权限

在我们能看懂文件权限后,就需要掌握怎么配置权限,这里主要讲解chmodchown两个命令。

2.1 chmod

Linux/Unix 文件调用权限分为三级 : 文件拥有者、群组、其他。利用 chmod 可以藉以控制文件如何被他人所调用。

// -R表示级联更改
chmod [-R] xyz 文件名(这里的xyz表示数字)。

比如下面三种操作都是设置所有人可以读写及执行file

chmod 777 file  

chmod u=rwx,g=rwx,o=rwx file 

chmod a=rwx file

这里我们重点讲解下数字格式777指的是什么。

我们多数用三位八进制数字的形式来表示权限,第一位指定属主的权限,第二位指定组权限,第三位指定其他用户的权限,每位通过4(r-读)、2(w-写)、1(x-执行)三种数值的和来确定权限。如6(4+2)代表有读写权,7(4+2+1)有读、写和执行的权限。

按照上面的数值,r=4,w=2,x=1 ;
若要rwx属性则4+2+1=7;
若要rw-属性则4+2=6;
若要r-x属性则4+1=5;
若要rwxrwxrwx属性则777;
若要rwxr-xr-x属性则755;

2.2 chown

更改文件拥有者。

chown [-cfhvR] [--help] [--version] user[:group] file...

示例:

将文件 file1.txt 的拥有者设为 neo,群体的使用者 neogroup :

chown runoob:runoobgroup file1.txt

将目前目录下的所有文件与子目录的拥有者皆设为 neo,群体的使用者 neogroup:

chown -R neo:neogroup *

3. umask

umask命令可以指定在建立文件时预设的权限掩码。

[权限掩码]是由3个八进制的数字所组成,将现有的存取权限减掉权限掩码后,即可产生建立文件时预设的权限。一般默认的值是022,最终新创建的目录权限为755,文件权限为644。

  1. 对于目录,直接使用777-umask即可,就得到了最终结果。
  2. 对于文件,先使用666-umask。

    • 如果对应位上为偶数:最终权限就是这个偶数值。
    • 如果上面的对应为上有奇数,就对应位+1。
掩码目录文件
022755644
027750640
002775664
006771660
007770660

在终端直接执行umask只对本地登录有效,如果要永久修改,需要把内容umask=022写入到配置文件中,配置文件可以从下一章节中找到。

3.1 针对交互式登陆:

优先级从高到低。

  1. /etc/bashrc
  2. ~/.bashrc
  3. ~/.bash_profile
  4. /etc/profile.d/*.sh
  5. `/etc/profile

3.2 针对非交互登陆:

优先级从高到低。

  1. /etc/profile.d/*.sh
  2. /etc/bashrc
  3. ~/.bashrc

参考文档

本文同步发表于作者博客: linux系统文件权限简明介绍
查看原文

赞 0 收藏 0 评论 0

ineo6 回答了问题 · 2019-09-05

vue 使用代理 请求跨域有问题

路径按照这样试下:

proxyTable: {
      '/Racexxxxx/*': {
       ...
      },
      '/Race/*': {
       ...
      },
    },

关注 5 回答 4

ineo6 回答了问题 · 2019-09-05

axios 能否直接return

看了其他问题之后,我理解是有多个字典列表需要显示出来,所以需要请求多次dictionaries

我提供下我的思路:”把字典列表“封装为组件,组件内部请求数据并渲染列表,因为组件是可以复用的,就不需要考虑多次调用和变量问题。

关注 6 回答 5